copilot-usage-studio 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/README.md +42 -13
- package/data/github-copilot-pricing.json +34 -6
- package/dist/copilot-usage-studio/browser/chunk-2SAR2Q6M.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-5JYQJM5G.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-7FKLWMFH.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-EWN3YOHT.js +1 -0
- package/dist/copilot-usage-studio/browser/{chunk-NXH37FKU.js → chunk-KS5W2HMH.js} +1 -1
- package/dist/copilot-usage-studio/browser/{chunk-TVSYR63W.js → chunk-OS2XPCVW.js} +2 -2
- package/dist/copilot-usage-studio/browser/{chunk-QQOFM3HF.js → chunk-U4H425LY.js} +1 -1
- package/dist/copilot-usage-studio/browser/chunk-VEKXIM47.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-WOIYAEKB.js +4 -0
- package/dist/copilot-usage-studio/browser/{chunk-J47KKACI.js → chunk-XHID4XVE.js} +1 -1
- package/dist/copilot-usage-studio/browser/index.html +2 -2
- package/dist/copilot-usage-studio/browser/main-OZIMQRXC.js +1 -0
- package/dist/copilot-usage-studio/browser/styles-7RDDQXQV.css +1 -0
- package/docs/local-deployment.md +67 -11
- package/docs/pricing.md +3 -3
- package/docs/scanner-api.md +28 -0
- package/lib/cli.mjs +15 -1
- package/lib/local-runtime.mjs +455 -19
- package/lib/scanner-worker.mjs +25 -0
- package/package.json +17 -1
- package/scripts/pricing-utils.mjs +5 -0
- package/scripts/scan-vscode-sessions.mjs +383 -1475
- package/scripts/scanner-customization-evidence.mjs +576 -0
- package/scripts/scanner-customization-inventory.mjs +1021 -0
- package/scripts/scanner-memory.mjs +229 -0
- package/scripts/scanner-session-parser.mjs +1237 -0
- package/scripts/scanner-traversal.mjs +203 -0
- package/scripts/scanner-workspace.mjs +209 -0
- package/dist/copilot-usage-studio/browser/chunk-CUKTUQ6W.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-EYAHOEVS.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-FXNLFSUB.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-HJNYITFH.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-KDAJN6DF.js +0 -4
- package/dist/copilot-usage-studio/browser/main-TL27ZSEQ.js +0 -1
- package/dist/copilot-usage-studio/browser/styles-GZOQ5QIH.css +0 -1
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, statSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { homedir, platform } from 'node:os';
|
|
4
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { userDirForRoot } from './scanner-traversal.mjs';
|
|
7
|
+
|
|
8
|
+
const customizationFileLimit = 1000;
|
|
9
|
+
const customizationFileSizeLimit = 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
function stablePathKey(file) {
|
|
12
|
+
const resolved = resolve(file);
|
|
13
|
+
return platform() === 'win32' ? resolved.toLowerCase() : resolved;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function safeJson(text) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(text);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stripJsonComments(text) {
|
|
25
|
+
let output = '';
|
|
26
|
+
let inString = false;
|
|
27
|
+
let escaped = false;
|
|
28
|
+
let inLineComment = false;
|
|
29
|
+
let inBlockComment = false;
|
|
30
|
+
|
|
31
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
32
|
+
const character = text[index];
|
|
33
|
+
const next = text[index + 1];
|
|
34
|
+
|
|
35
|
+
if (inLineComment) {
|
|
36
|
+
if (character === '\n' || character === '\r') {
|
|
37
|
+
inLineComment = false;
|
|
38
|
+
output += character;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (inBlockComment) {
|
|
44
|
+
if (character === '*' && next === '/') {
|
|
45
|
+
inBlockComment = false;
|
|
46
|
+
index += 1;
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (inString) {
|
|
52
|
+
output += character;
|
|
53
|
+
if (escaped) {
|
|
54
|
+
escaped = false;
|
|
55
|
+
} else if (character === '\\') {
|
|
56
|
+
escaped = true;
|
|
57
|
+
} else if (character === '"') {
|
|
58
|
+
inString = false;
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (character === '"') {
|
|
64
|
+
inString = true;
|
|
65
|
+
output += character;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (character === '/' && next === '/') {
|
|
70
|
+
inLineComment = true;
|
|
71
|
+
index += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (character === '/' && next === '*') {
|
|
76
|
+
inBlockComment = true;
|
|
77
|
+
index += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
output += character;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return output;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function memoryTitle(content, file) {
|
|
88
|
+
const heading = String(content)
|
|
89
|
+
.split(/\r?\n/)
|
|
90
|
+
.map((line) => line.match(/^#{1,3}\s+(.+?)\s*#*$/)?.[1]?.trim())
|
|
91
|
+
.find(Boolean);
|
|
92
|
+
|
|
93
|
+
if (heading) {
|
|
94
|
+
return heading.slice(0, 160);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return basename(file, extname(file))
|
|
98
|
+
.replace(/[-_]+/g, ' ')
|
|
99
|
+
.replace(/\b\w/g, (character) => character.toUpperCase())
|
|
100
|
+
.slice(0, 160);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function memoryExcerpt(content) {
|
|
104
|
+
return String(content)
|
|
105
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
106
|
+
.replace(/[\x60*_>~-]+/g, ' ')
|
|
107
|
+
.replace(/\s+/g, ' ')
|
|
108
|
+
.trim()
|
|
109
|
+
.slice(0, 280);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createCustomizationInventoryScanner(context = {}) {
|
|
113
|
+
const diagnostics = () => context.diagnostics?.() ?? context.diagnostics ?? { warnings: [] };
|
|
114
|
+
const listDirs = (...args) => context.listDirs?.(...args) ?? [];
|
|
115
|
+
const listFilesRecursive = (...args) => context.listFilesRecursive?.(...args) ?? [];
|
|
116
|
+
const workspaceName = (...args) => context.workspaceName?.(...args) ?? '';
|
|
117
|
+
const workspaceFolderPath = (...args) => context.workspaceFolderPath?.(...args) ?? '';
|
|
118
|
+
|
|
119
|
+
function readJsoncFile(file) {
|
|
120
|
+
if (!existsSync(file)) {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const json = stripJsonComments(readFileSync(file, 'utf8')).replace(/,\s*([}\]])/g, '$1');
|
|
126
|
+
return safeJson(json) ?? {};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
diagnostics().warnings.push(String(file) + ': settings file skipped: ' + error.message);
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readJsonl(file) {
|
|
134
|
+
if (context.readJsonl) {
|
|
135
|
+
return context.readJsonl(file);
|
|
136
|
+
}
|
|
137
|
+
if (!existsSync(file)) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return readFileSync(file, 'utf8')
|
|
142
|
+
.split(/\r?\n/)
|
|
143
|
+
.map((line) => line.trim())
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.map((line) => safeJson(line))
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseSimpleFrontmatter(content) {
|
|
150
|
+
const text = String(content ?? '');
|
|
151
|
+
if (!text.startsWith('---')) {
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
156
|
+
if (!match) {
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const result = {};
|
|
161
|
+
const lines = match[1].split(/\r?\n/);
|
|
162
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
163
|
+
const line = lines[index];
|
|
164
|
+
const scalar = line.match(/^([A-Za-z][\w-]*)\s*:\s*(.*)$/);
|
|
165
|
+
if (!scalar) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const key = scalar[1];
|
|
170
|
+
let value = scalar[2].trim();
|
|
171
|
+
if (value === '|') {
|
|
172
|
+
const block = [];
|
|
173
|
+
index += 1;
|
|
174
|
+
while (index < lines.length && /^\s+/.test(lines[index])) {
|
|
175
|
+
block.push(lines[index].replace(/^\s{2}/, ''));
|
|
176
|
+
index += 1;
|
|
177
|
+
}
|
|
178
|
+
index -= 1;
|
|
179
|
+
result[key] = block.join('\n').trim();
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const list = [];
|
|
184
|
+
while (index + 1 < lines.length && /^\s*-\s+/.test(lines[index + 1])) {
|
|
185
|
+
index += 1;
|
|
186
|
+
list.push(lines[index].replace(/^\s*-\s+/, '').replace(/^["']|["']$/g, '').trim());
|
|
187
|
+
}
|
|
188
|
+
result[key] = list.length ? list : value.replace(/^["']|["']$/g, '');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function markdownTitle(content, file) {
|
|
195
|
+
const frontmatter = parseSimpleFrontmatter(content);
|
|
196
|
+
if (frontmatter.title) {
|
|
197
|
+
return String(frontmatter.title).slice(0, 160);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return memoryTitle(content, file);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function titleFromFileName(file) {
|
|
204
|
+
return basename(file, extname(file))
|
|
205
|
+
.replace(/\.instructions$/i, '')
|
|
206
|
+
.replace(/\.skill$/i, '')
|
|
207
|
+
.replace(/\.agent$/i, '')
|
|
208
|
+
.replace(/^copilot-instructions$/i, 'Copilot Instructions')
|
|
209
|
+
.replace(/[-_]+/g, ' ')
|
|
210
|
+
.replace(/\b\w/g, (character) => character.toUpperCase())
|
|
211
|
+
.slice(0, 160);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isMarkdownFile(file) {
|
|
215
|
+
return extname(file).toLowerCase() === '.md';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isCustomizationSourceFile(file) {
|
|
219
|
+
const extension = extname(file).toLowerCase();
|
|
220
|
+
return extension === '.md' || extension === '.json';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function looksLikeCopilotCustomizationPath(file) {
|
|
224
|
+
const normalized = file.replace(/\\/g, '/').toLowerCase();
|
|
225
|
+
const name = basename(file).toLowerCase();
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
normalized.includes('/.github/instructions/') ||
|
|
229
|
+
normalized.includes('/.claude/rules/') ||
|
|
230
|
+
normalized.includes('/.copilot/instructions/') ||
|
|
231
|
+
normalized.includes('/.github/skills/') ||
|
|
232
|
+
normalized.includes('/.claude/skills/') ||
|
|
233
|
+
normalized.includes('/.agents/skills/') ||
|
|
234
|
+
normalized.includes('/.copilot/skills/') ||
|
|
235
|
+
normalized.includes('/.github/prompts/') ||
|
|
236
|
+
normalized.includes('/.copilot/prompts/') ||
|
|
237
|
+
normalized.includes('/.github/hooks/') ||
|
|
238
|
+
normalized.includes('/.copilot/hooks/') ||
|
|
239
|
+
normalized.includes('/.github/agents/') ||
|
|
240
|
+
normalized.includes('/.claude/agents/') ||
|
|
241
|
+
normalized.includes('/user/prompts/') ||
|
|
242
|
+
normalized.includes('/.copilot/agents/') ||
|
|
243
|
+
name === 'copilot-instructions.md' ||
|
|
244
|
+
name === 'agents.md' ||
|
|
245
|
+
name === 'claude.md' ||
|
|
246
|
+
name === 'claude.local.md' ||
|
|
247
|
+
name === 'gemini.md' ||
|
|
248
|
+
name === 'settings.json' ||
|
|
249
|
+
name === 'settings.local.json' ||
|
|
250
|
+
name.endsWith('.instructions.md') ||
|
|
251
|
+
name.endsWith('.prompt.md') ||
|
|
252
|
+
name.endsWith('.skill.md') ||
|
|
253
|
+
name.endsWith('.agent.md') ||
|
|
254
|
+
name === 'skill.md'
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function customizationKind(file) {
|
|
259
|
+
const normalized = file.replace(/\\/g, '/').toLowerCase();
|
|
260
|
+
const name = basename(file).toLowerCase();
|
|
261
|
+
|
|
262
|
+
if (
|
|
263
|
+
normalized.includes('/.github/instructions/') ||
|
|
264
|
+
normalized.includes('/.claude/rules/') ||
|
|
265
|
+
normalized.includes('/.copilot/instructions/') ||
|
|
266
|
+
name === 'copilot-instructions.md' ||
|
|
267
|
+
name === 'agents.md' ||
|
|
268
|
+
name === 'claude.md' ||
|
|
269
|
+
name === 'claude.local.md' ||
|
|
270
|
+
name === 'gemini.md' ||
|
|
271
|
+
name.endsWith('.instructions.md')
|
|
272
|
+
) {
|
|
273
|
+
return 'instruction';
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
normalized.includes('/.github/skills/') ||
|
|
277
|
+
normalized.includes('/.claude/skills/') ||
|
|
278
|
+
normalized.includes('/.agents/skills/') ||
|
|
279
|
+
normalized.includes('/.copilot/skills/') ||
|
|
280
|
+
name === 'skill.md' ||
|
|
281
|
+
name.endsWith('.skill.md')
|
|
282
|
+
) {
|
|
283
|
+
return 'skill';
|
|
284
|
+
}
|
|
285
|
+
if (
|
|
286
|
+
normalized.includes('/.github/prompts/') ||
|
|
287
|
+
normalized.includes('/.copilot/prompts/') ||
|
|
288
|
+
normalized.includes('/user/prompts/') ||
|
|
289
|
+
name.endsWith('.prompt.md')
|
|
290
|
+
) {
|
|
291
|
+
return 'prompt';
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
normalized.includes('/.github/hooks/') ||
|
|
295
|
+
normalized.includes('/.copilot/hooks/') ||
|
|
296
|
+
name === 'settings.json' ||
|
|
297
|
+
name === 'settings.local.json'
|
|
298
|
+
) {
|
|
299
|
+
return 'hook';
|
|
300
|
+
}
|
|
301
|
+
if (
|
|
302
|
+
normalized.includes('/.github/agents/') ||
|
|
303
|
+
normalized.includes('/.claude/agents/') ||
|
|
304
|
+
normalized.includes('/.copilot/agents/') ||
|
|
305
|
+
name.endsWith('.agent.md')
|
|
306
|
+
) {
|
|
307
|
+
return 'agent';
|
|
308
|
+
}
|
|
309
|
+
return 'other';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function customizationFromFile(file, root, workspace, forcedKind = '') {
|
|
313
|
+
try {
|
|
314
|
+
const stats = statSync(file);
|
|
315
|
+
if (stats.size > customizationFileSizeLimit) {
|
|
316
|
+
diagnostics().skippedOversizedCustomizations += 1;
|
|
317
|
+
diagnostics().warnings.push(`${file}: customization skipped because it exceeds 1 MiB.`);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const content = readFileSync(file, 'utf8');
|
|
322
|
+
const frontmatter = parseSimpleFrontmatter(content);
|
|
323
|
+
const kind = forcedKind || customizationKind(file);
|
|
324
|
+
const relativePath = relative(root, file);
|
|
325
|
+
const description = String(frontmatter.description ?? '').trim();
|
|
326
|
+
const applyTo = Array.isArray(frontmatter.applyTo)
|
|
327
|
+
? frontmatter.applyTo.map(String)
|
|
328
|
+
: String(frontmatter.applyTo ?? '')
|
|
329
|
+
.split(',')
|
|
330
|
+
.map((value) => value.trim())
|
|
331
|
+
.filter(Boolean);
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
id: createHash('sha256').update(stablePathKey(file)).digest('hex').slice(0, 24),
|
|
335
|
+
kind,
|
|
336
|
+
title: frontmatter.title ? markdownTitle(content, file) : titleFromFileName(file),
|
|
337
|
+
name: String(frontmatter.id ?? basename(file, extname(file))).trim(),
|
|
338
|
+
description: description || memoryExcerpt(content).slice(0, 180),
|
|
339
|
+
applyTo,
|
|
340
|
+
triggers: Array.isArray(frontmatter.triggers) ? frontmatter.triggers.map(String) : [],
|
|
341
|
+
scope: frontmatter.scope ? String(frontmatter.scope) : 'workspace',
|
|
342
|
+
workspace,
|
|
343
|
+
sourcePath: resolve(file),
|
|
344
|
+
relativePath,
|
|
345
|
+
createdAt: stats.birthtimeMs > 0 ? stats.birthtime.toISOString() : '',
|
|
346
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
347
|
+
sizeBytes: stats.size,
|
|
348
|
+
characterCount: content.length,
|
|
349
|
+
lineCount: content ? content.split(/\r?\n/).length : 0,
|
|
350
|
+
excerpt: memoryExcerpt(content),
|
|
351
|
+
_content: content,
|
|
352
|
+
};
|
|
353
|
+
} catch (error) {
|
|
354
|
+
diagnostics().skippedUnreadableCustomizations += 1;
|
|
355
|
+
diagnostics().warnings.push(`${file}: customization skipped: ${error.message}`);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function nearestGitRoot(folder) {
|
|
361
|
+
let current = resolve(folder);
|
|
362
|
+
|
|
363
|
+
while (true) {
|
|
364
|
+
if (existsSync(join(current, '.git'))) {
|
|
365
|
+
return current;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const parent = dirname(current);
|
|
369
|
+
if (parent === current) {
|
|
370
|
+
return '';
|
|
371
|
+
}
|
|
372
|
+
current = parent;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function customizationCandidateBases(folder) {
|
|
377
|
+
const resolvedFolder = resolve(folder);
|
|
378
|
+
const gitRoot = nearestGitRoot(resolvedFolder);
|
|
379
|
+
if (!gitRoot) {
|
|
380
|
+
return [resolvedFolder];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const bases = [];
|
|
384
|
+
let current = resolvedFolder;
|
|
385
|
+
while (true) {
|
|
386
|
+
bases.push(current);
|
|
387
|
+
if (current === gitRoot) {
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const parent = dirname(current);
|
|
392
|
+
if (parent === current) {
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
current = parent;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return [...new Set(bases)];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function containedByBase(candidate, base) {
|
|
402
|
+
const resolvedCandidate = resolve(candidate);
|
|
403
|
+
const resolvedBase = resolve(base);
|
|
404
|
+
return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${sep}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function directCustomizationFiles(base) {
|
|
408
|
+
const files = [
|
|
409
|
+
join(base, '.github', 'copilot-instructions.md'),
|
|
410
|
+
join(base, '.claude', 'CLAUDE.md'),
|
|
411
|
+
join(base, '.claude', 'CLAUDE.local.md'),
|
|
412
|
+
join(base, '.claude', 'settings.json'),
|
|
413
|
+
join(base, '.claude', 'settings.local.json'),
|
|
414
|
+
join(base, 'AGENTS.md'),
|
|
415
|
+
join(base, 'CLAUDE.md'),
|
|
416
|
+
join(base, 'GEMINI.md'),
|
|
417
|
+
].filter((file) => existsSync(file) && isCustomizationSourceFile(file));
|
|
418
|
+
|
|
419
|
+
for (const file of files) {
|
|
420
|
+
recordCustomizationLocation(file, 'file');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return files;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function customizationFilesFromKnownRoots(base) {
|
|
427
|
+
const roots = [
|
|
428
|
+
join(base, '.github', 'instructions'),
|
|
429
|
+
join(base, '.claude', 'rules'),
|
|
430
|
+
join(base, '.copilot', 'instructions'),
|
|
431
|
+
join(base, '.github', 'skills'),
|
|
432
|
+
join(base, '.claude', 'skills'),
|
|
433
|
+
join(base, '.agents', 'skills'),
|
|
434
|
+
join(base, '.copilot', 'skills'),
|
|
435
|
+
join(base, '.github', 'prompts'),
|
|
436
|
+
join(base, 'prompts'),
|
|
437
|
+
join(base, '.copilot', 'prompts'),
|
|
438
|
+
join(base, '.github', 'hooks'),
|
|
439
|
+
join(base, '.copilot', 'hooks'),
|
|
440
|
+
join(base, '.github', 'agents'),
|
|
441
|
+
join(base, '.claude', 'agents'),
|
|
442
|
+
join(base, '.copilot', 'agents'),
|
|
443
|
+
].filter(existsSync);
|
|
444
|
+
|
|
445
|
+
return roots.flatMap((root) => {
|
|
446
|
+
diagnostics().scannedCustomizationRoots += 1;
|
|
447
|
+
recordCustomizationLocation(root, 'root');
|
|
448
|
+
const files = listFilesRecursive(
|
|
449
|
+
root,
|
|
450
|
+
(file) => isCustomizationSourceFile(file) && looksLikeCopilotCustomizationPath(file),
|
|
451
|
+
customizationFileLimit,
|
|
452
|
+
{ label: 'customization', maxDepth: 5, maxDirs: 300 },
|
|
453
|
+
);
|
|
454
|
+
if (files.length >= customizationFileLimit) {
|
|
455
|
+
diagnostics().warnings.push(`${root}: customization scan capped at ${customizationFileLimit} files.`);
|
|
456
|
+
}
|
|
457
|
+
return files;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function configuredCustomizationLocationEntries(workspaceDir, workspaceFolder) {
|
|
462
|
+
const entries = [];
|
|
463
|
+
const userDir = userDirForRoot(workspaceDir);
|
|
464
|
+
const settingsFiles = [
|
|
465
|
+
userDir ? { file: join(userDir, 'settings.json'), base: workspaceFolder, scope: 'user-settings' } : null,
|
|
466
|
+
{ file: join(workspaceFolder, '.vscode', 'settings.json'), base: workspaceFolder, scope: 'workspace-settings' },
|
|
467
|
+
].filter(Boolean);
|
|
468
|
+
const settingKinds = [
|
|
469
|
+
['chat.instructionsFilesLocations', 'instruction'],
|
|
470
|
+
['chat.promptFilesLocations', 'prompt'],
|
|
471
|
+
['chat.agentFilesLocations', 'agent'],
|
|
472
|
+
['chat.agentSkillsLocations', 'skill'],
|
|
473
|
+
['chat.hookFilesLocations', 'hook'],
|
|
474
|
+
];
|
|
475
|
+
|
|
476
|
+
for (const settingsFile of settingsFiles) {
|
|
477
|
+
const settings = readJsoncFile(settingsFile.file);
|
|
478
|
+
for (const [settingKey, kind] of settingKinds) {
|
|
479
|
+
const configured = settings[settingKey];
|
|
480
|
+
if (!configured || typeof configured !== 'object' || Array.isArray(configured)) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
for (const [location, enabled] of Object.entries(configured)) {
|
|
485
|
+
if (enabled !== true) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const normalized = normalizeLocalPathCandidate(location);
|
|
490
|
+
const path = normalized.startsWith('~/') || normalized.startsWith('~\\')
|
|
491
|
+
? resolve(homedir(), normalized.slice(2))
|
|
492
|
+
: isAbsolute(normalized)
|
|
493
|
+
? resolve(normalized)
|
|
494
|
+
: resolve(settingsFile.base, normalized);
|
|
495
|
+
entries.push({ path, kind, source: settingsFile.scope, settingKey });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return entries;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function strictCustomizationLocationEntries(workspaceFolder, options = {}) {
|
|
504
|
+
const discovery = options.customizationDiscovery;
|
|
505
|
+
if (!discovery?.strict || !Array.isArray(discovery.locations)) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const currentWorkspaceFolder = resolve(workspaceFolder);
|
|
510
|
+
return discovery.locations
|
|
511
|
+
.filter((entry) => {
|
|
512
|
+
const entryWorkspace = entry?.workspaceFolder ? resolve(String(entry.workspaceFolder)) : '';
|
|
513
|
+
return !entryWorkspace || entryWorkspace === currentWorkspaceFolder;
|
|
514
|
+
})
|
|
515
|
+
.map((entry) => ({
|
|
516
|
+
path: resolve(String(entry.path ?? '')),
|
|
517
|
+
kind: normalizeCustomizationKind(entry.kind),
|
|
518
|
+
source: String(entry.source ?? 'vscode-setting'),
|
|
519
|
+
settingKey: String(entry.settingKey ?? ''),
|
|
520
|
+
rawLocation: String(entry.rawLocation ?? entry.path ?? ''),
|
|
521
|
+
}))
|
|
522
|
+
.filter((entry) => entry.path && entry.kind);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function defaultUserCustomizationLocationEntries() {
|
|
526
|
+
return [
|
|
527
|
+
{
|
|
528
|
+
path: join(homedir(), '.copilot', 'skills'),
|
|
529
|
+
kind: 'skill',
|
|
530
|
+
source: 'user-default',
|
|
531
|
+
settingKey: 'chat.agentSkillsLocations',
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
path: join(homedir(), '.claude', 'skills'),
|
|
535
|
+
kind: 'skill',
|
|
536
|
+
source: 'user-default',
|
|
537
|
+
settingKey: 'chat.agentSkillsLocations',
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
path: join(homedir(), '.agents', 'skills'),
|
|
541
|
+
kind: 'skill',
|
|
542
|
+
source: 'user-default',
|
|
543
|
+
settingKey: 'chat.agentSkillsLocations',
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
path: join(homedir(), '.copilot', 'hooks'),
|
|
547
|
+
kind: 'hook',
|
|
548
|
+
source: 'user-default',
|
|
549
|
+
settingKey: 'chat.hookFilesLocations',
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
path: join(homedir(), '.claude', 'settings.json'),
|
|
553
|
+
kind: 'hook',
|
|
554
|
+
source: 'user-default',
|
|
555
|
+
settingKey: 'chat.hookFilesLocations',
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
path: join(homedir(), '.claude', 'settings.local.json'),
|
|
559
|
+
kind: 'hook',
|
|
560
|
+
source: 'user-default',
|
|
561
|
+
settingKey: 'chat.hookFilesLocations',
|
|
562
|
+
},
|
|
563
|
+
];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function configuredCustomizationFilePredicate(kind, file, options = {}) {
|
|
567
|
+
const name = basename(file).toLowerCase();
|
|
568
|
+
const extension = extname(file).toLowerCase();
|
|
569
|
+
|
|
570
|
+
if (options.explicitFile === true || options.trustedLocation === true) {
|
|
571
|
+
if (kind === 'hook') {
|
|
572
|
+
return extension === '.json';
|
|
573
|
+
}
|
|
574
|
+
if (['instruction', 'skill', 'prompt', 'agent'].includes(kind)) {
|
|
575
|
+
return extension === '.md';
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (kind === 'instruction') {
|
|
580
|
+
return extension === '.md' && (
|
|
581
|
+
name === 'copilot-instructions.md' ||
|
|
582
|
+
name === 'agents.md' ||
|
|
583
|
+
name === 'claude.md' ||
|
|
584
|
+
name === 'claude.local.md' ||
|
|
585
|
+
name === 'gemini.md' ||
|
|
586
|
+
name.endsWith('.instructions.md')
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (kind === 'skill') {
|
|
591
|
+
return extension === '.md' && (name === 'skill.md' || name.endsWith('.skill.md'));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (kind === 'prompt') {
|
|
595
|
+
return extension === '.md' && name.endsWith('.prompt.md');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (kind === 'hook') {
|
|
599
|
+
return extension === '.json';
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return isCustomizationSourceFile(file) && looksLikeCopilotCustomizationPath(file);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function normalizeCustomizationKind(kind) {
|
|
606
|
+
return ['instruction', 'skill', 'prompt', 'hook', 'agent'].includes(String(kind))
|
|
607
|
+
? String(kind)
|
|
608
|
+
: 'other';
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function customizationLocationKind(source) {
|
|
612
|
+
return {
|
|
613
|
+
'vscode-default': 'vscode-default-root',
|
|
614
|
+
'vscode-user-setting': 'vscode-user-setting-root',
|
|
615
|
+
'vscode-workspace-setting': 'vscode-workspace-setting-root',
|
|
616
|
+
'vscode-workspace-folder-setting': 'vscode-workspace-folder-setting-root',
|
|
617
|
+
'vscode-parent-repo-default': 'vscode-parent-repo-default-root',
|
|
618
|
+
'user-default': 'user-default-root',
|
|
619
|
+
}[source] ?? 'vscode-setting-root';
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function customizationBasesFromStrictEntries(entries, options = {}) {
|
|
623
|
+
return [...new Set(
|
|
624
|
+
entries
|
|
625
|
+
.filter((entry) => includeCustomizationPath(entry.path, options))
|
|
626
|
+
.map((entry) => {
|
|
627
|
+
if (pathHasGlob(entry.path)) {
|
|
628
|
+
return globBaseDir(entry.path);
|
|
629
|
+
}
|
|
630
|
+
if (existsSync(entry.path) && statSync(entry.path).isFile()) {
|
|
631
|
+
return dirname(entry.path);
|
|
632
|
+
}
|
|
633
|
+
return entry.path;
|
|
634
|
+
})
|
|
635
|
+
.filter(Boolean)
|
|
636
|
+
.map((entry) => resolve(entry)),
|
|
637
|
+
)];
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function pathHasGlob(path) {
|
|
641
|
+
return /[*?[\]{}]/.test(path);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function globBaseDir(path) {
|
|
645
|
+
const resolved = resolve(path);
|
|
646
|
+
const normalized = resolved.replace(/\\/g, '/');
|
|
647
|
+
const parts = normalized.split('/');
|
|
648
|
+
const baseParts = [];
|
|
649
|
+
for (const part of parts) {
|
|
650
|
+
if (/[*?[\]{}]/.test(part)) {
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
baseParts.push(part);
|
|
654
|
+
}
|
|
655
|
+
const base = baseParts.join('/') || dirname(resolved);
|
|
656
|
+
return resolve(base);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function globPathRegex(path) {
|
|
660
|
+
const normalized = normalizedPathForGlob(path);
|
|
661
|
+
let pattern = '';
|
|
662
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
663
|
+
const char = normalized[index];
|
|
664
|
+
const next = normalized[index + 1];
|
|
665
|
+
if (char === '*' && next === '*') {
|
|
666
|
+
pattern += '.*';
|
|
667
|
+
index += 1;
|
|
668
|
+
} else if (char === '*') {
|
|
669
|
+
pattern += '[^/]*';
|
|
670
|
+
} else if (char === '?') {
|
|
671
|
+
pattern += '[^/]';
|
|
672
|
+
} else {
|
|
673
|
+
pattern += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return new RegExp(`^${pattern}$`, 'i');
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function normalizedPathForGlob(path) {
|
|
680
|
+
return resolve(path).replace(/\\/g, '/');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function customizationsFromConfiguredSettings(workspaceDir, workspaceFolder, workspace, options = {}) {
|
|
684
|
+
const files = new Map();
|
|
685
|
+
const strictEntries = strictCustomizationLocationEntries(workspaceFolder, options);
|
|
686
|
+
const entries = strictEntries ?? [
|
|
687
|
+
...configuredCustomizationLocationEntries(workspaceDir, workspaceFolder),
|
|
688
|
+
...defaultUserCustomizationLocationEntries(),
|
|
689
|
+
];
|
|
690
|
+
for (const entry of entries) {
|
|
691
|
+
if (!includeCustomizationPath(entry.path, options)) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const expandedFiles = customizationFilesFromConfiguredEntry(entry, options);
|
|
695
|
+
if (!expandedFiles.length) {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
diagnostics().scannedCustomizationRoots += 1;
|
|
700
|
+
recordCustomizationLocation(entry.path, customizationLocationKind(entry.source));
|
|
701
|
+
for (const file of expandedFiles) {
|
|
702
|
+
files.set(stablePathKey(file.path), { path: resolve(file.path), base: file.base, kind: entry.kind });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return [...files.values()]
|
|
707
|
+
.map((entry) => customizationFromFile(entry.path, entry.base, workspace, entry.kind))
|
|
708
|
+
.filter(Boolean);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function customizationFilesFromConfiguredEntry(entry, options = {}) {
|
|
712
|
+
if (pathHasGlob(entry.path)) {
|
|
713
|
+
return customizationFilesFromGlobEntry(entry, options);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (!existsSync(entry.path)) {
|
|
717
|
+
return [];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (statSync(entry.path).isFile()) {
|
|
721
|
+
return configuredCustomizationFilePredicate(entry.kind, entry.path, { explicitFile: true }) &&
|
|
722
|
+
includeCustomizationPath(entry.path, options)
|
|
723
|
+
? [{ path: resolve(entry.path), base: dirname(entry.path) }]
|
|
724
|
+
: [];
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (!statSync(entry.path).isDirectory()) {
|
|
728
|
+
return [];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return listFilesRecursive(
|
|
732
|
+
entry.path,
|
|
733
|
+
(candidate) =>
|
|
734
|
+
configuredCustomizationFilePredicate(entry.kind, candidate, { trustedLocation: true }) &&
|
|
735
|
+
includeCustomizationPath(candidate, options),
|
|
736
|
+
customizationFileLimit,
|
|
737
|
+
{ label: 'customization', maxDepth: 5, maxDirs: 300 },
|
|
738
|
+
).map((file) => ({ path: resolve(file), base: entry.path }));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function customizationFilesFromGlobEntry(entry, options = {}) {
|
|
742
|
+
const base = globBaseDir(entry.path);
|
|
743
|
+
if (!base || !existsSync(base) || !statSync(base).isDirectory()) {
|
|
744
|
+
return [];
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const pattern = globPathRegex(entry.path);
|
|
748
|
+
return listFilesRecursive(
|
|
749
|
+
base,
|
|
750
|
+
(candidate) =>
|
|
751
|
+
pattern.test(normalizedPathForGlob(candidate)) &&
|
|
752
|
+
configuredCustomizationFilePredicate(entry.kind, candidate, { trustedLocation: true }) &&
|
|
753
|
+
includeCustomizationPath(candidate, options),
|
|
754
|
+
customizationFileLimit,
|
|
755
|
+
{ label: 'customization', maxDepth: 8, maxDirs: 500 },
|
|
756
|
+
).map((file) => ({ path: resolve(file), base }));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function localMarkdownPathCandidates(text, bases, options = {}) {
|
|
760
|
+
const content = String(text ?? '');
|
|
761
|
+
const candidates = new Set();
|
|
762
|
+
const fileTagPattern = /<file>([^<>"']+?\.md)<\/file>/gi;
|
|
763
|
+
const windowsPathPattern = /[a-zA-Z]:[\\/][^<>"'\r\n]+?\.md/gi;
|
|
764
|
+
const posixPathPattern = /\/(?:[^<>"'\r\n]+\/)*[^<>"'\r\n]+?\.md/gi;
|
|
765
|
+
|
|
766
|
+
for (const pattern of [fileTagPattern, windowsPathPattern, posixPathPattern]) {
|
|
767
|
+
for (const match of content.matchAll(pattern)) {
|
|
768
|
+
candidates.add((match[1] ?? match[0]).trim().replace(/[),.;\]}]+$/g, ''));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const files = [];
|
|
773
|
+
for (const rawCandidate of candidates) {
|
|
774
|
+
const candidate = normalizeLocalPathCandidate(rawCandidate);
|
|
775
|
+
const possiblePaths = isAbsolute(candidate)
|
|
776
|
+
? [candidate]
|
|
777
|
+
: bases.map((base) => resolve(base, candidate));
|
|
778
|
+
|
|
779
|
+
for (const possiblePath of possiblePaths) {
|
|
780
|
+
if (
|
|
781
|
+
existsSync(possiblePath) &&
|
|
782
|
+
isMarkdownFile(possiblePath) &&
|
|
783
|
+
looksLikeCopilotCustomizationPath(possiblePath) &&
|
|
784
|
+
(options.allowAbsoluteOutsideBases === true && isAbsolute(candidate) ||
|
|
785
|
+
bases.some((base) => containedByBase(possiblePath, base)))
|
|
786
|
+
) {
|
|
787
|
+
files.push(resolve(possiblePath));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return files;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function normalizeLocalPathCandidate(candidate) {
|
|
796
|
+
let value = String(candidate ?? '').trim().replace(/^["']|["']$/g, '');
|
|
797
|
+
if (/^file:/i.test(value)) {
|
|
798
|
+
try {
|
|
799
|
+
value = fileURLToPath(value);
|
|
800
|
+
} catch {
|
|
801
|
+
value = value.replace(/^file:\/+/i, '');
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
try {
|
|
805
|
+
value = decodeURIComponent(value);
|
|
806
|
+
} catch {
|
|
807
|
+
// Keep the raw value when VS Code logged a non-URI-encoded path fragment.
|
|
808
|
+
}
|
|
809
|
+
value = value.replace(/\\/g, '/').replace(/[),.;\]}]+$/g, '');
|
|
810
|
+
|
|
811
|
+
if (platform() === 'win32' && /^\/[a-zA-Z]:\//.test(value)) {
|
|
812
|
+
value = value.slice(1);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return value;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function discoveryFolderPathCandidates(text) {
|
|
819
|
+
const content = String(text ?? '');
|
|
820
|
+
const folders = new Set();
|
|
821
|
+
const absolutePathPattern = /(?:[a-zA-Z]:[\\/][^,\]\r\n]+|\/[a-zA-Z]:[\\/][^,\]\r\n]+|~[\\/][^,\]\r\n]+|\/[^,\]\r\n]+)/g;
|
|
822
|
+
|
|
823
|
+
for (const match of content.matchAll(absolutePathPattern)) {
|
|
824
|
+
const candidate = normalizeLocalPathCandidate(match[0]);
|
|
825
|
+
if (!candidate) {
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const resolved = candidate.startsWith('~/') || candidate.startsWith('~\\')
|
|
829
|
+
? resolve(homedir(), candidate.slice(2))
|
|
830
|
+
: resolve(candidate);
|
|
831
|
+
if (existsSync(resolved) && statSync(resolved).isDirectory() && looksLikeCustomizationRoot(resolved)) {
|
|
832
|
+
folders.add(resolved);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return [...folders];
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function looksLikeCustomizationRoot(folder) {
|
|
840
|
+
const normalized = folder.replace(/\\/g, '/').toLowerCase();
|
|
841
|
+
return (
|
|
842
|
+
normalized.endsWith('/.github/instructions') ||
|
|
843
|
+
normalized.endsWith('/.claude/rules') ||
|
|
844
|
+
normalized.endsWith('/.copilot/instructions') ||
|
|
845
|
+
normalized.endsWith('/.github/skills') ||
|
|
846
|
+
normalized.endsWith('/.claude/skills') ||
|
|
847
|
+
normalized.endsWith('/.agents/skills') ||
|
|
848
|
+
normalized.endsWith('/.copilot/skills') ||
|
|
849
|
+
normalized.includes('/.github/skills/') ||
|
|
850
|
+
normalized.includes('/.claude/skills/') ||
|
|
851
|
+
normalized.includes('/.agents/skills/') ||
|
|
852
|
+
normalized.includes('/.copilot/skills/') ||
|
|
853
|
+
normalized.endsWith('/.github/prompts') ||
|
|
854
|
+
normalized.endsWith('/prompts') ||
|
|
855
|
+
normalized.endsWith('/.copilot/prompts') ||
|
|
856
|
+
normalized.endsWith('/.github/hooks') ||
|
|
857
|
+
normalized.endsWith('/.copilot/hooks') ||
|
|
858
|
+
normalized.endsWith('/.github/agents') ||
|
|
859
|
+
normalized.endsWith('/.claude/agents') ||
|
|
860
|
+
normalized.endsWith('/.copilot/agents')
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function customizationsFromDiscoveryFolders(debugRoot, workspace, options = {}) {
|
|
865
|
+
if (!existsSync(debugRoot)) {
|
|
866
|
+
return [];
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const folders = new Set();
|
|
870
|
+
for (const sessionDir of listDirs(debugRoot)) {
|
|
871
|
+
const main = readJsonl(join(sessionDir, 'main.jsonl'));
|
|
872
|
+
for (const event of main) {
|
|
873
|
+
const category = String(event.attrs?.category ?? '').toLowerCase();
|
|
874
|
+
const eventName = String(event.name ?? '').toLowerCase();
|
|
875
|
+
if (event.type !== 'discovery' && category !== 'customization' && !eventName.includes('discovery')) {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
for (const folder of discoveryFolderPathCandidates(event.attrs?.details)) {
|
|
879
|
+
folders.add(folder);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const files = new Map();
|
|
885
|
+
for (const folder of folders) {
|
|
886
|
+
if (!includeCustomizationPath(folder, options)) {
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
diagnostics().scannedCustomizationRoots += 1;
|
|
890
|
+
recordCustomizationLocation(folder, 'debug-discovery-root');
|
|
891
|
+
for (const file of listFilesRecursive(
|
|
892
|
+
folder,
|
|
893
|
+
(candidate) =>
|
|
894
|
+
isCustomizationSourceFile(candidate) &&
|
|
895
|
+
looksLikeCopilotCustomizationPath(candidate) &&
|
|
896
|
+
includeCustomizationPath(candidate, options),
|
|
897
|
+
customizationFileLimit,
|
|
898
|
+
{ label: 'customization', maxDepth: 5, maxDirs: 300 },
|
|
899
|
+
)) {
|
|
900
|
+
files.set(stablePathKey(file), { path: resolve(file), base: folder });
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return [...files.values()]
|
|
905
|
+
.map((entry) => customizationFromFile(entry.path, entry.base, workspace))
|
|
906
|
+
.filter(Boolean);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function customizationsFromDebugReferences(debugRoot, bases, workspace, options = {}) {
|
|
910
|
+
if (!existsSync(debugRoot) || !bases.length) {
|
|
911
|
+
return [];
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const files = new Map();
|
|
915
|
+
|
|
916
|
+
for (const sessionDir of listDirs(debugRoot)) {
|
|
917
|
+
for (const file of readdirSync(sessionDir).map((entry) => join(sessionDir, entry))) {
|
|
918
|
+
if (!statSync(file).isFile()) {
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
const extension = extname(file).toLowerCase();
|
|
922
|
+
if (!['.json', '.txt'].includes(extension)) {
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
const stats = statSync(file);
|
|
926
|
+
if (stats.size > 2 * 1024 * 1024) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
for (const candidate of localMarkdownPathCandidates(readFileSync(file, 'utf8'), bases, {
|
|
931
|
+
allowAbsoluteOutsideBases: options.customizationDiscovery?.strict !== true,
|
|
932
|
+
})) {
|
|
933
|
+
if (!includeCustomizationPath(candidate, options)) {
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const base = bases.find((candidateBase) => containedByBase(candidate, candidateBase)) ?? dirname(candidate);
|
|
937
|
+
recordCustomizationLocation(candidate, 'debug-reference');
|
|
938
|
+
files.set(stablePathKey(candidate), { path: resolve(candidate), base });
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return [...files.values()]
|
|
944
|
+
.map((entry) => customizationFromFile(entry.path, entry.base, workspace))
|
|
945
|
+
.filter(Boolean);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function customizationsFromWorkspace(workspaceDir, options = {}) {
|
|
949
|
+
const folder = workspaceFolderPath(workspaceDir);
|
|
950
|
+
if (!folder) {
|
|
951
|
+
return { customizations: [], bases: [] };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const strictEntries = strictCustomizationLocationEntries(folder, options);
|
|
955
|
+
const strictDiscovery = strictEntries !== null;
|
|
956
|
+
const bases = strictDiscovery
|
|
957
|
+
? customizationBasesFromStrictEntries(strictEntries, options)
|
|
958
|
+
: customizationCandidateBases(folder);
|
|
959
|
+
const files = new Map();
|
|
960
|
+
|
|
961
|
+
if (!strictDiscovery) {
|
|
962
|
+
for (const base of bases) {
|
|
963
|
+
const directFiles = directCustomizationFiles(base);
|
|
964
|
+
if (directFiles.length) {
|
|
965
|
+
diagnostics().scannedCustomizationRoots += 1;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
for (const file of [...directFiles, ...customizationFilesFromKnownRoots(base)]) {
|
|
969
|
+
files.set(stablePathKey(file), { path: resolve(file), base });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const workspace = workspaceName(workspaceDir);
|
|
975
|
+
const knownCustomizations = [...files.values()]
|
|
976
|
+
.map((entry) => customizationFromFile(entry.path, entry.base, workspace))
|
|
977
|
+
.filter(Boolean);
|
|
978
|
+
|
|
979
|
+
return {
|
|
980
|
+
bases,
|
|
981
|
+
customizations: [
|
|
982
|
+
...knownCustomizations,
|
|
983
|
+
...customizationsFromConfiguredSettings(workspaceDir, folder, workspace, options),
|
|
984
|
+
],
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function recordCustomizationLocation(path, kind) {
|
|
989
|
+
const location = { kind, path: resolve(path) };
|
|
990
|
+
const key = `${location.kind}:${location.path}`;
|
|
991
|
+
if (diagnostics().scannedCustomizationLocations.some((item) => `${item.kind}:${item.path}` === key)) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (diagnostics().scannedCustomizationLocations.length < 200) {
|
|
995
|
+
diagnostics().scannedCustomizationLocations.push(location);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function isSystemCustomizationPath(file) {
|
|
1000
|
+
const normalized = resolve(file).replace(/\\/g, '/').toLowerCase();
|
|
1001
|
+
return (
|
|
1002
|
+
/(^|\/)\.vscode(?:-insiders)?\/extensions\//.test(normalized) ||
|
|
1003
|
+
normalized.includes('/resources/app/extensions/')
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function includeCustomizationPath(file, options = {}) {
|
|
1008
|
+
if (options.includeSystemCustomizations === true || !isSystemCustomizationPath(file)) {
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
diagnostics().skippedSystemCustomizations += 1;
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
return {
|
|
1017
|
+
customizationsFromDebugReferences,
|
|
1018
|
+
customizationsFromDiscoveryFolders,
|
|
1019
|
+
customizationsFromWorkspace,
|
|
1020
|
+
};
|
|
1021
|
+
}
|