Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/scripts/sync-agent-host-protocol.ts
13371 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
// Copies type definitions from the sibling `agent-host-protocol` repo into
7
// `src/vs/platform/agentHost/common/state/protocol/`. Run via:
8
//
9
// npx tsx scripts/sync-agent-host-protocol.ts
10
//
11
// Transformations applied:
12
// 1. Converts 2-space indentation to tabs.
13
// 2. Merges duplicate imports from the same module.
14
// 3. Formats with the project's tsfmt.json settings.
15
// 4. Adds Microsoft copyright header.
16
//
17
// URI stays as `string` (the protocol's canonical representation). VS Code code
18
// should call `URI.parse()` at point-of-use where a URI class is needed.
19
20
import * as fs from 'fs';
21
import * as path from 'path';
22
import { execSync } from 'child_process';
23
import * as ts from 'typescript';
24
25
const ROOT = path.resolve(__dirname, '..');
26
const PROTOCOL_REPO = path.resolve(ROOT, '../agent-host-protocol');
27
const TYPES_DIR = path.join(PROTOCOL_REPO, 'types');
28
const DEST_DIR = path.join(ROOT, 'src/vs/platform/agentHost/common/state/protocol');
29
30
// Load tsfmt.json formatting options once
31
const TSFMT_PATH = path.join(ROOT, 'tsfmt.json');
32
const FORMAT_OPTIONS: ts.FormatCodeSettings = JSON.parse(fs.readFileSync(TSFMT_PATH, 'utf-8'));
33
34
/**
35
* Formats a TypeScript source string using the TypeScript language service
36
* formatter with the project's tsfmt.json settings.
37
*/
38
function formatTypeScript(content: string, fileName: string): string {
39
const host: ts.LanguageServiceHost = {
40
getCompilationSettings: () => ({}),
41
getScriptFileNames: () => [fileName],
42
getScriptVersion: () => '1',
43
getScriptSnapshot: (name: string) => name === fileName ? ts.ScriptSnapshot.fromString(content) : undefined,
44
getCurrentDirectory: () => ROOT,
45
getDefaultLibFileName: () => '',
46
fileExists: () => false,
47
readFile: () => undefined,
48
};
49
const ls = ts.createLanguageService(host);
50
const edits = ls.getFormattingEditsForDocument(fileName, FORMAT_OPTIONS);
51
// Apply edits in reverse order to preserve offsets
52
for (let i = edits.length - 1; i >= 0; i--) {
53
const edit = edits[i];
54
content = content.substring(0, edit.span.start) + edit.newText + content.substring(edit.span.start + edit.span.length);
55
}
56
ls.dispose();
57
return content;
58
}
59
60
const COPYRIGHT = `/*---------------------------------------------------------------------------------------------
61
* Copyright (c) Microsoft Corporation. All rights reserved.
62
* Licensed under the MIT License. See License.txt in the project root for license information.
63
*--------------------------------------------------------------------------------------------*/`;
64
65
const BANNER = '// allow-any-unicode-comment-file\n// DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts';
66
67
// Files to copy. All go into protocol/.
68
const FILES: { src: string; dest: string }[] = [
69
{ src: 'state.ts', dest: 'state.ts' },
70
{ src: 'actions.ts', dest: 'actions.ts' },
71
{ src: 'action-origin.generated.ts', dest: 'action-origin.generated.ts' },
72
{ src: 'reducers.ts', dest: 'reducers.ts' },
73
{ src: 'commands.ts', dest: 'commands.ts' },
74
{ src: 'errors.ts', dest: 'errors.ts' },
75
{ src: 'notifications.ts', dest: 'notifications.ts' },
76
{ src: 'messages.ts', dest: 'messages.ts' },
77
{ src: 'version/registry.ts', dest: 'version/registry.ts' },
78
];
79
80
function getSourceCommitHash(): string {
81
try {
82
return execSync('git rev-parse --short HEAD', { cwd: PROTOCOL_REPO, encoding: 'utf-8' }).trim();
83
} catch {
84
return 'unknown';
85
}
86
}
87
88
function stripExistingHeader(content: string): string {
89
return content.replace(/^\/\*\*?[\s\S]*?\*\/\s*/, '');
90
}
91
92
function convertIndentation(content: string): string {
93
const lines = content.split('\n');
94
return lines.map(line => {
95
const match = line.match(/^( +)/);
96
if (!match) {
97
return line;
98
}
99
const spaces = match[1].length;
100
const tabs = Math.floor(spaces / 2);
101
const remainder = spaces % 2;
102
return '\t'.repeat(tabs) + ' '.repeat(remainder) + line.slice(spaces);
103
}).join('\n');
104
}
105
106
/**
107
* Merges duplicate imports from the same module.
108
* Combines `import type { A }` and `import { B }` from the same module into
109
* `import { B, type A }` to satisfy the no-duplicate-imports lint rule.
110
*/
111
function mergeDuplicateImports(content: string): string {
112
// Collapse multi-line imports into single lines first
113
content = content.replace(/import\s+(type\s+)?\{([^}]+)\}\s+from\s+'([^']+)';/g, (_match, typeKeyword, names, mod) => {
114
const collapsed = names.replace(/\s+/g, ' ').trim();
115
return typeKeyword ? `import type { ${collapsed} } from '${mod}';` : `import { ${collapsed} } from '${mod}';`;
116
});
117
118
const importsByModule = new Map<string, { typeNames: string[]; valueNames: string[] }>();
119
const otherLines: string[] = [];
120
const seenModules = new Set<string>();
121
122
for (const line of content.split('\n')) {
123
const typeMatch = line.match(/^import type \{([^}]+)\} from '([^']+)';$/);
124
const valueMatch = line.match(/^import \{([^}]+)\} from '([^']+)';$/);
125
126
if (typeMatch) {
127
const [, names, mod] = typeMatch;
128
if (!importsByModule.has(mod)) {
129
importsByModule.set(mod, { typeNames: [], valueNames: [] });
130
}
131
importsByModule.get(mod)!.typeNames.push(...names.split(',').map(s => s.trim()).filter(s => s.length > 0));
132
if (!seenModules.has(mod)) {
133
seenModules.add(mod);
134
otherLines.push(`__IMPORT_PLACEHOLDER__${mod}`);
135
}
136
} else if (valueMatch) {
137
const [, names, mod] = valueMatch;
138
if (!importsByModule.has(mod)) {
139
importsByModule.set(mod, { typeNames: [], valueNames: [] });
140
}
141
importsByModule.get(mod)!.valueNames.push(...names.split(',').map(s => s.trim()).filter(s => s.length > 0));
142
if (!seenModules.has(mod)) {
143
seenModules.add(mod);
144
otherLines.push(`__IMPORT_PLACEHOLDER__${mod}`);
145
}
146
} else {
147
otherLines.push(line);
148
}
149
}
150
151
return otherLines.map(line => {
152
if (line.startsWith('__IMPORT_PLACEHOLDER__')) {
153
const mod = line.substring('__IMPORT_PLACEHOLDER__'.length);
154
const entry = importsByModule.get(mod)!;
155
const uniqueTypes = [...new Set(entry.typeNames)];
156
const uniqueValues = [...new Set(entry.valueNames)];
157
158
if (uniqueValues.length > 0 && uniqueTypes.length > 0) {
159
const allNames = [...uniqueValues, ...uniqueTypes.map(n => `type ${n}`)];
160
return `import { ${allNames.join(', ')} } from '${mod}';`;
161
} else if (uniqueValues.length > 0) {
162
return `import { ${uniqueValues.join(', ')} } from '${mod}';`;
163
} else {
164
return `import type { ${uniqueTypes.join(', ')} } from '${mod}';`;
165
}
166
}
167
return line;
168
}).join('\n');
169
}
170
171
172
173
174
175
function processFile(src: string, dest: string): void {
176
let content = fs.readFileSync(src, 'utf-8');
177
content = stripExistingHeader(content);
178
179
// Merge duplicate imports from the same module
180
content = mergeDuplicateImports(content);
181
182
content = convertIndentation(content);
183
content = content.split('\n').map(line => line.trimEnd()).join('\n');
184
185
const header = `${COPYRIGHT}\n\n${BANNER}\n`;
186
content = header + '\n' + content;
187
188
if (!content.endsWith('\n')) {
189
content += '\n';
190
}
191
192
const destPath = path.join(DEST_DIR, dest);
193
fs.mkdirSync(path.dirname(destPath), { recursive: true });
194
content = formatTypeScript(content, dest);
195
fs.writeFileSync(destPath, content, 'utf-8');
196
console.log(` ${dest}`);
197
}
198
199
// ---- Main -------------------------------------------------------------------
200
201
function main() {
202
if (!fs.existsSync(TYPES_DIR)) {
203
console.error(`ERROR: Cannot find ${TYPES_DIR}`);
204
console.error('Clone agent-host-protocol as a sibling of the VS Code repo:');
205
console.error(' git clone [email protected]:microsoft/agent-host-protocol.git ../agent-host-protocol');
206
process.exit(1);
207
}
208
209
const commitHash = getSourceCommitHash();
210
console.log(`Syncing from agent-host-protocol @ ${commitHash}`);
211
console.log(` Source: ${TYPES_DIR}`);
212
console.log(` Dest: ${DEST_DIR}`);
213
console.log();
214
215
// Copy protocol files
216
for (const file of FILES) {
217
const srcPath = path.join(TYPES_DIR, file.src);
218
if (!fs.existsSync(srcPath)) {
219
console.error(` SKIP (not found): ${file.src}`);
220
continue;
221
}
222
processFile(srcPath, file.dest);
223
}
224
225
// Write the source commit hash to a single version file
226
const versionFile = path.join(DEST_DIR, '.ahp-version');
227
fs.writeFileSync(versionFile, commitHash + '\n', 'utf-8');
228
console.log(` .ahp-version -> ${commitHash}`);
229
230
console.log();
231
console.log('Done.');
232
}
233
234
main();
235
236