/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // Copies type definitions from the sibling `agent-host-protocol` repo into // `src/vs/platform/agentHost/common/state/protocol/`. Run via: // // npx tsx scripts/sync-agent-host-protocol.ts // // Transformations applied: // 1. Converts 2-space indentation to tabs. // 2. Merges duplicate imports from the same module. // 3. Formats with the project's tsfmt.json settings. // 4. Adds Microsoft copyright header. // // URI stays as `string` (the protocol's canonical representation). VS Code code // should call `URI.parse()` at point-of-use where a URI class is needed. import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; import * as ts from 'typescript'; const ROOT = path.resolve(__dirname, '..'); const PROTOCOL_REPO = path.resolve(ROOT, '../agent-host-protocol'); const TYPES_DIR = path.join(PROTOCOL_REPO, 'types'); const DEST_DIR = path.join(ROOT, 'src/vs/platform/agentHost/common/state/protocol'); // Load tsfmt.json formatting options once const TSFMT_PATH = path.join(ROOT, 'tsfmt.json'); const FORMAT_OPTIONS: ts.FormatCodeSettings = JSON.parse(fs.readFileSync(TSFMT_PATH, 'utf-8')); /** * Formats a TypeScript source string using the TypeScript language service * formatter with the project's tsfmt.json settings. */ function formatTypeScript(content: string, fileName: string): string { const host: ts.LanguageServiceHost = { getCompilationSettings: () => ({}), getScriptFileNames: () => [fileName], getScriptVersion: () => '1', getScriptSnapshot: (name: string) => name === fileName ? ts.ScriptSnapshot.fromString(content) : undefined, getCurrentDirectory: () => ROOT, getDefaultLibFileName: () => '', fileExists: () => false, readFile: () => undefined, }; const ls = ts.createLanguageService(host); const edits = ls.getFormattingEditsForDocument(fileName, FORMAT_OPTIONS); // Apply edits in reverse order to preserve offsets for (let i = edits.length - 1; i >= 0; i--) { const edit = edits[i]; content = content.substring(0, edit.span.start) + edit.newText + content.substring(edit.span.start + edit.span.length); } ls.dispose(); return content; } const COPYRIGHT = `/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/`; const BANNER = '// allow-any-unicode-comment-file\n// DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts'; // Files to copy. All go into protocol/. const FILES: { src: string; dest: string }[] = [ { src: 'state.ts', dest: 'state.ts' }, { src: 'actions.ts', dest: 'actions.ts' }, { src: 'action-origin.generated.ts', dest: 'action-origin.generated.ts' }, { src: 'reducers.ts', dest: 'reducers.ts' }, { src: 'commands.ts', dest: 'commands.ts' }, { src: 'errors.ts', dest: 'errors.ts' }, { src: 'notifications.ts', dest: 'notifications.ts' }, { src: 'messages.ts', dest: 'messages.ts' }, { src: 'version/registry.ts', dest: 'version/registry.ts' }, ]; function getSourceCommitHash(): string { try { return execSync('git rev-parse --short HEAD', { cwd: PROTOCOL_REPO, encoding: 'utf-8' }).trim(); } catch { return 'unknown'; } } function stripExistingHeader(content: string): string { return content.replace(/^\/\*\*?[\s\S]*?\*\/\s*/, ''); } function convertIndentation(content: string): string { const lines = content.split('\n'); return lines.map(line => { const match = line.match(/^( +)/); if (!match) { return line; } const spaces = match[1].length; const tabs = Math.floor(spaces / 2); const remainder = spaces % 2; return '\t'.repeat(tabs) + ' '.repeat(remainder) + line.slice(spaces); }).join('\n'); } /** * Merges duplicate imports from the same module. * Combines `import type { A }` and `import { B }` from the same module into * `import { B, type A }` to satisfy the no-duplicate-imports lint rule. */ function mergeDuplicateImports(content: string): string { // Collapse multi-line imports into single lines first content = content.replace(/import\s+(type\s+)?\{([^}]+)\}\s+from\s+'([^']+)';/g, (_match, typeKeyword, names, mod) => { const collapsed = names.replace(/\s+/g, ' ').trim(); return typeKeyword ? `import type { ${collapsed} } from '${mod}';` : `import { ${collapsed} } from '${mod}';`; }); const importsByModule = new Map(); const otherLines: string[] = []; const seenModules = new Set(); for (const line of content.split('\n')) { const typeMatch = line.match(/^import type \{([^}]+)\} from '([^']+)';$/); const valueMatch = line.match(/^import \{([^}]+)\} from '([^']+)';$/); if (typeMatch) { const [, names, mod] = typeMatch; if (!importsByModule.has(mod)) { importsByModule.set(mod, { typeNames: [], valueNames: [] }); } importsByModule.get(mod)!.typeNames.push(...names.split(',').map(s => s.trim()).filter(s => s.length > 0)); if (!seenModules.has(mod)) { seenModules.add(mod); otherLines.push(`__IMPORT_PLACEHOLDER__${mod}`); } } else if (valueMatch) { const [, names, mod] = valueMatch; if (!importsByModule.has(mod)) { importsByModule.set(mod, { typeNames: [], valueNames: [] }); } importsByModule.get(mod)!.valueNames.push(...names.split(',').map(s => s.trim()).filter(s => s.length > 0)); if (!seenModules.has(mod)) { seenModules.add(mod); otherLines.push(`__IMPORT_PLACEHOLDER__${mod}`); } } else { otherLines.push(line); } } return otherLines.map(line => { if (line.startsWith('__IMPORT_PLACEHOLDER__')) { const mod = line.substring('__IMPORT_PLACEHOLDER__'.length); const entry = importsByModule.get(mod)!; const uniqueTypes = [...new Set(entry.typeNames)]; const uniqueValues = [...new Set(entry.valueNames)]; if (uniqueValues.length > 0 && uniqueTypes.length > 0) { const allNames = [...uniqueValues, ...uniqueTypes.map(n => `type ${n}`)]; return `import { ${allNames.join(', ')} } from '${mod}';`; } else if (uniqueValues.length > 0) { return `import { ${uniqueValues.join(', ')} } from '${mod}';`; } else { return `import type { ${uniqueTypes.join(', ')} } from '${mod}';`; } } return line; }).join('\n'); } function processFile(src: string, dest: string): void { let content = fs.readFileSync(src, 'utf-8'); content = stripExistingHeader(content); // Merge duplicate imports from the same module content = mergeDuplicateImports(content); content = convertIndentation(content); content = content.split('\n').map(line => line.trimEnd()).join('\n'); const header = `${COPYRIGHT}\n\n${BANNER}\n`; content = header + '\n' + content; if (!content.endsWith('\n')) { content += '\n'; } const destPath = path.join(DEST_DIR, dest); fs.mkdirSync(path.dirname(destPath), { recursive: true }); content = formatTypeScript(content, dest); fs.writeFileSync(destPath, content, 'utf-8'); console.log(` ${dest}`); } // ---- Main ------------------------------------------------------------------- function main() { if (!fs.existsSync(TYPES_DIR)) { console.error(`ERROR: Cannot find ${TYPES_DIR}`); console.error('Clone agent-host-protocol as a sibling of the VS Code repo:'); console.error(' git clone git@github.com:microsoft/agent-host-protocol.git ../agent-host-protocol'); process.exit(1); } const commitHash = getSourceCommitHash(); console.log(`Syncing from agent-host-protocol @ ${commitHash}`); console.log(` Source: ${TYPES_DIR}`); console.log(` Dest: ${DEST_DIR}`); console.log(); // Copy protocol files for (const file of FILES) { const srcPath = path.join(TYPES_DIR, file.src); if (!fs.existsSync(srcPath)) { console.error(` SKIP (not found): ${file.src}`); continue; } processFile(srcPath, file.dest); } // Write the source commit hash to a single version file const versionFile = path.join(DEST_DIR, '.ahp-version'); fs.writeFileSync(versionFile, commitHash + '\n', 'utf-8'); console.log(` .ahp-version -> ${commitHash}`); console.log(); console.log('Done.'); } main();