Files
vscode/scripts/sync-agent-host-protocol.ts
Rob Lourens 492790d5ca Don't write ahp commit hash to every file (#305695)
* Don't write ahp commit hash to every file

Co-authored-by: Copilot <copilot@github.com>

* undo

---------

Co-authored-by: Copilot <copilot@github.com>
2026-03-27 15:54:33 +00:00

235 lines
8.4 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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<string, { typeNames: string[]; valueNames: string[] }>();
const otherLines: string[] = [];
const seenModules = new Set<string>();
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();