proteum 2.2.3 → 2.2.7
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/AGENTS.md +6 -6
- package/README.md +4 -4
- package/agents/project/AGENTS.md +11 -9
- package/agents/project/app-root/AGENTS.md +1 -1
- package/agents/project/diagnostics.md +11 -9
- package/agents/project/optimizations.md +2 -2
- package/agents/project/root/AGENTS.md +10 -8
- package/agents/project/tests/AGENTS.md +2 -1
- package/cli/commands/configure.ts +14 -35
- package/cli/commands/dev.ts +105 -52
- package/cli/compiler/artifacts/manifest.ts +1 -5
- package/cli/presentation/commands.ts +9 -9
- package/cli/presentation/help.ts +1 -1
- package/cli/scaffold/index.ts +2 -5
- package/cli/scaffold/templates.ts +1 -7
- package/cli/utils/agents.ts +281 -199
- package/package.json +1 -1
- package/server/services/router/request/index.ts +1 -18
- package/server/services/router/request/ip.test.cjs +60 -0
- package/server/services/router/request/ip.ts +71 -0
- package/tests/agents-utils.test.cjs +207 -0
- package/tests/dev-transpile-watch.test.cjs +513 -0
package/cli/utils/agents.ts
CHANGED
|
@@ -21,13 +21,11 @@ type TConfigureProjectAgentInstructionsArgs = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
type TAgentInstructionDefinition = {
|
|
24
|
-
includeWorktreeBootstrap?: boolean;
|
|
25
24
|
projectPath: string;
|
|
26
|
-
sourcePath: string;
|
|
27
25
|
ensureParentDir?: boolean;
|
|
28
26
|
};
|
|
29
27
|
|
|
30
|
-
type
|
|
28
|
+
type TEnsureInstructionFilesResult = {
|
|
31
29
|
blocked: string[];
|
|
32
30
|
created: string[];
|
|
33
31
|
overwritten: string[];
|
|
@@ -47,58 +45,41 @@ export type TConfigureProjectAgentInstructionsResult = {
|
|
|
47
45
|
updatedGitignores: string[];
|
|
48
46
|
};
|
|
49
47
|
|
|
50
|
-
export type TProjectAgentFileInspection = {
|
|
51
|
-
existing: string[];
|
|
52
|
-
missing: string[];
|
|
53
|
-
};
|
|
54
|
-
|
|
55
48
|
/*----------------------------------
|
|
56
49
|
- CONSTANTS
|
|
57
50
|
----------------------------------*/
|
|
58
51
|
|
|
59
52
|
const managedInstructionStubHeader = '# Proteum Managed Instructions';
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'```',
|
|
67
|
-
'npx proteum configure agents',
|
|
68
|
-
'npm i',
|
|
69
|
-
'npx prisma generate',
|
|
70
|
-
'npx proteum refresh',
|
|
71
|
-
'```',
|
|
72
|
-
'',
|
|
73
|
-
'Run it before reading project-specific AGENTS files or making code changes. If the command is interactive, answer according to the current repository layout, then continue by reading the generated AGENTS.md files.',
|
|
74
|
-
];
|
|
53
|
+
const managedInstructionStubFinalLine =
|
|
54
|
+
'If the canonical file cannot be read, stop and run `npx proteum configure agents` before continuing.';
|
|
55
|
+
const managedInstructionSectionHeader = '# Proteum Instructions';
|
|
56
|
+
const managedInstructionSectionStart = '<!-- proteum-instructions:start -->';
|
|
57
|
+
const managedInstructionSectionEnd = '<!-- proteum-instructions:end -->';
|
|
58
|
+
const managedInstructionSectionIntro = 'This section is managed by `proteum configure agents`.';
|
|
75
59
|
|
|
76
60
|
const sharedAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
77
|
-
{ projectPath: 'CODING_STYLE.md'
|
|
78
|
-
{ projectPath: 'diagnostics.md'
|
|
79
|
-
{ projectPath: 'optimizations.md'
|
|
80
|
-
{ projectPath: path.join('client', 'AGENTS.md')
|
|
81
|
-
{ projectPath: path.join('client', 'pages', 'AGENTS.md')
|
|
82
|
-
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
86
|
-
{ projectPath: path.join('server', 'routes', 'AGENTS.md'), sourcePath: path.join('server', 'routes', 'AGENTS.md') },
|
|
87
|
-
{ projectPath: path.join('tests', 'e2e', 'AGENTS.md'), sourcePath: path.join('tests', 'AGENTS.md') },
|
|
61
|
+
{ projectPath: 'CODING_STYLE.md' },
|
|
62
|
+
{ projectPath: 'diagnostics.md' },
|
|
63
|
+
{ projectPath: 'optimizations.md' },
|
|
64
|
+
{ projectPath: path.join('client', 'AGENTS.md') },
|
|
65
|
+
{ projectPath: path.join('client', 'pages', 'AGENTS.md') },
|
|
66
|
+
{ projectPath: path.join('server', 'services', 'AGENTS.md') },
|
|
67
|
+
{ projectPath: path.join('server', 'routes', 'AGENTS.md') },
|
|
68
|
+
{ projectPath: path.join('tests', 'e2e', 'AGENTS.md') },
|
|
88
69
|
];
|
|
89
70
|
|
|
90
71
|
const standaloneAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
91
|
-
{ projectPath: 'AGENTS.md'
|
|
72
|
+
{ projectPath: 'AGENTS.md' },
|
|
92
73
|
...sharedAppAgentInstructionDefinitions,
|
|
93
74
|
];
|
|
94
75
|
|
|
95
76
|
const monorepoAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
96
|
-
{ projectPath: 'AGENTS.md'
|
|
77
|
+
{ projectPath: 'AGENTS.md' },
|
|
97
78
|
...sharedAppAgentInstructionDefinitions,
|
|
98
79
|
];
|
|
99
80
|
|
|
100
81
|
const monorepoRootAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
101
|
-
{ projectPath: 'AGENTS.md'
|
|
82
|
+
{ projectPath: 'AGENTS.md' },
|
|
102
83
|
];
|
|
103
84
|
|
|
104
85
|
const legacyProjectInstructionGitignoreBlockStart = '# Proteum-managed instruction symlinks';
|
|
@@ -134,41 +115,44 @@ export function configureProjectAgentInstructions({
|
|
|
134
115
|
updated: [],
|
|
135
116
|
updatedGitignores: [],
|
|
136
117
|
};
|
|
118
|
+
const embeddedInstructions = renderEmbeddedProjectInstructions({ coreRoot });
|
|
137
119
|
|
|
138
120
|
if (mode === 'monorepo' && normalizedMonorepoRoot) {
|
|
139
121
|
result.monorepoRoot = normalizedMonorepoRoot;
|
|
140
122
|
|
|
141
|
-
const rootInstructions = getRootAgentInstructionDefinitions(
|
|
142
|
-
const
|
|
123
|
+
const rootInstructions = getRootAgentInstructionDefinitions();
|
|
124
|
+
const rootFiles = ensureInstructionFiles(
|
|
143
125
|
normalizedMonorepoRoot,
|
|
144
126
|
rootInstructions,
|
|
145
127
|
'[agents]',
|
|
146
128
|
path.join(coreRoot, 'agents', 'project'),
|
|
129
|
+
embeddedInstructions,
|
|
147
130
|
{
|
|
148
131
|
dryRun,
|
|
149
132
|
overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
|
|
150
133
|
},
|
|
151
134
|
);
|
|
152
|
-
mergeInstructionResults(result,
|
|
135
|
+
mergeInstructionResults(result, rootFiles, normalizedMonorepoRoot);
|
|
153
136
|
|
|
154
|
-
if (!dryRun &&
|
|
137
|
+
if (!dryRun && removeInstructionGitignoreEntries({ rootDir: normalizedMonorepoRoot, instructionDefinitions: rootInstructions }))
|
|
155
138
|
result.updatedGitignores.push(path.join(normalizedMonorepoRoot, '.gitignore'));
|
|
156
139
|
}
|
|
157
140
|
|
|
158
|
-
const appInstructions = getAppAgentInstructionDefinitions({
|
|
159
|
-
const
|
|
141
|
+
const appInstructions = getAppAgentInstructionDefinitions({ mode });
|
|
142
|
+
const appFiles = ensureInstructionFiles(
|
|
160
143
|
normalizedAppRoot,
|
|
161
144
|
appInstructions,
|
|
162
145
|
'[agents]',
|
|
163
146
|
path.join(coreRoot, 'agents', 'project'),
|
|
147
|
+
embeddedInstructions,
|
|
164
148
|
{
|
|
165
149
|
dryRun,
|
|
166
150
|
overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
|
|
167
151
|
},
|
|
168
152
|
);
|
|
169
|
-
mergeInstructionResults(result,
|
|
153
|
+
mergeInstructionResults(result, appFiles, normalizedAppRoot);
|
|
170
154
|
|
|
171
|
-
if (!dryRun &&
|
|
155
|
+
if (!dryRun && removeInstructionGitignoreEntries({ rootDir: normalizedAppRoot, instructionDefinitions: appInstructions }))
|
|
172
156
|
result.updatedGitignores.push(path.join(normalizedAppRoot, '.gitignore'));
|
|
173
157
|
|
|
174
158
|
return result;
|
|
@@ -176,51 +160,15 @@ export function configureProjectAgentInstructions({
|
|
|
176
160
|
|
|
177
161
|
export const configureProjectAgentSymlinks = configureProjectAgentInstructions;
|
|
178
162
|
|
|
179
|
-
export function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
getAppAgentInstructionDefinitions({ coreRoot, mode: 'standalone' }).map((instructionDefinition) =>
|
|
183
|
-
`/${normalizeProjectPathForGitignore(instructionDefinition.projectPath)}`,
|
|
184
|
-
),
|
|
185
|
-
),
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function renderProjectInstructionGitignoreBlock({ coreRoot }: TProjectInstructionArgs) {
|
|
190
|
-
return renderInstructionGitignoreBlock({
|
|
191
|
-
instructionDefinitions: getAppAgentInstructionDefinitions({ coreRoot, mode: 'standalone' }),
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function inspectProjectAgentFiles({ appRoot }: { appRoot: string }): TProjectAgentFileInspection {
|
|
196
|
-
const normalizedAppRoot = path.resolve(appRoot);
|
|
197
|
-
const expectedAgentPaths = Array.from(
|
|
198
|
-
new Set(
|
|
199
|
-
standaloneAppAgentInstructionDefinitions
|
|
200
|
-
.map((instructionDefinition) => instructionDefinition.projectPath)
|
|
201
|
-
.filter((projectPath) => projectPath.endsWith('AGENTS.md')),
|
|
202
|
-
),
|
|
203
|
-
);
|
|
204
|
-
const result: TProjectAgentFileInspection = {
|
|
205
|
-
existing: [],
|
|
206
|
-
missing: [],
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
for (const projectPath of expectedAgentPaths) {
|
|
210
|
-
const absolutePath = path.join(normalizedAppRoot, projectPath);
|
|
211
|
-
const parentPath = path.dirname(absolutePath);
|
|
212
|
-
|
|
213
|
-
if (projectPath !== 'AGENTS.md' && !fs.existsSync(parentPath)) continue;
|
|
163
|
+
export function resolveProjectAgentMonorepoRoot(appRoot: string) {
|
|
164
|
+
const normalizedAppRoot = resolveCanonicalPath(appRoot);
|
|
165
|
+
const likelyRepoRoot = findLikelyRepoRoot(normalizedAppRoot);
|
|
214
166
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
167
|
+
if (!likelyRepoRoot) return undefined;
|
|
168
|
+
if (likelyRepoRoot === normalizedAppRoot) return undefined;
|
|
169
|
+
if (!isInsideDirectory({ child: normalizedAppRoot, parent: likelyRepoRoot })) return undefined;
|
|
219
170
|
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return result;
|
|
171
|
+
return likelyRepoRoot;
|
|
224
172
|
}
|
|
225
173
|
|
|
226
174
|
/*----------------------------------
|
|
@@ -228,52 +176,19 @@ export function inspectProjectAgentFiles({ appRoot }: { appRoot: string }): TPro
|
|
|
228
176
|
----------------------------------*/
|
|
229
177
|
|
|
230
178
|
function getAppAgentInstructionDefinitions({
|
|
231
|
-
coreRoot,
|
|
232
179
|
mode,
|
|
233
|
-
}:
|
|
234
|
-
const agentSourceRoot = path.join(coreRoot, 'agents', 'project');
|
|
180
|
+
}: { mode: 'monorepo' | 'standalone' }) {
|
|
235
181
|
const sourceDefinitions =
|
|
236
182
|
mode === 'monorepo' ? monorepoAppAgentInstructionDefinitions : standaloneAppAgentInstructionDefinitions;
|
|
237
183
|
|
|
238
|
-
return
|
|
239
|
-
agentSourceRoot,
|
|
240
|
-
instructionDefinitions: sourceDefinitions,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function getRootAgentInstructionDefinitions({ coreRoot }: TProjectInstructionArgs) {
|
|
245
|
-
return resolveAgentInstructionDefinitions({
|
|
246
|
-
agentSourceRoot: path.join(coreRoot, 'agents', 'project'),
|
|
247
|
-
instructionDefinitions: monorepoRootAgentInstructionDefinitions,
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function resolveAgentInstructionDefinitions({
|
|
252
|
-
agentSourceRoot,
|
|
253
|
-
instructionDefinitions,
|
|
254
|
-
}: {
|
|
255
|
-
agentSourceRoot: string;
|
|
256
|
-
instructionDefinitions: TAgentInstructionDefinition[];
|
|
257
|
-
}) {
|
|
258
|
-
return instructionDefinitions.map((instructionDefinition) => ({
|
|
259
|
-
...instructionDefinition,
|
|
260
|
-
sourcePath: path.join(agentSourceRoot, instructionDefinition.sourcePath),
|
|
261
|
-
}));
|
|
184
|
+
return sourceDefinitions.map((instructionDefinition) => ({ ...instructionDefinition }));
|
|
262
185
|
}
|
|
263
186
|
|
|
264
|
-
function
|
|
265
|
-
|
|
266
|
-
new Set(
|
|
267
|
-
instructionDefinitions.map(
|
|
268
|
-
(instructionDefinition) => `/${normalizeProjectPathForGitignore(instructionDefinition.projectPath)}`,
|
|
269
|
-
),
|
|
270
|
-
),
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
return [projectInstructionGitignoreBlockStart, ...entries, projectInstructionGitignoreBlockEnd].join('\n');
|
|
187
|
+
function getRootAgentInstructionDefinitions() {
|
|
188
|
+
return monorepoRootAgentInstructionDefinitions.map((instructionDefinition) => ({ ...instructionDefinition }));
|
|
274
189
|
}
|
|
275
190
|
|
|
276
|
-
function
|
|
191
|
+
function removeInstructionGitignoreEntries({
|
|
277
192
|
rootDir,
|
|
278
193
|
instructionDefinitions,
|
|
279
194
|
}: {
|
|
@@ -310,22 +225,22 @@ function ensureInstructionGitignoreEntries({
|
|
|
310
225
|
}
|
|
311
226
|
|
|
312
227
|
const baseContent = trimTrailingBlankLines(filteredLines).join('\n');
|
|
313
|
-
const
|
|
314
|
-
const nextContent = baseContent ? `${baseContent}\n\n${managedBlock}\n` : `${managedBlock}\n`;
|
|
228
|
+
const nextContent = baseContent ? `${baseContent}\n` : '';
|
|
315
229
|
|
|
316
230
|
if (nextContent === fs.readFileSync(gitignoreFilepath, 'utf8')) return false;
|
|
317
231
|
|
|
318
232
|
fs.writeFileSync(gitignoreFilepath, nextContent);
|
|
319
|
-
logVerbose(`[agents]
|
|
233
|
+
logVerbose(`[agents] Removed Proteum-managed instruction ignore entries from ${path.relative(rootDir, gitignoreFilepath) || '.gitignore'}.`);
|
|
320
234
|
|
|
321
235
|
return true;
|
|
322
236
|
}
|
|
323
237
|
|
|
324
|
-
function
|
|
238
|
+
function ensureInstructionFiles(
|
|
325
239
|
rootDir: string,
|
|
326
240
|
instructionDefinitions: TAgentInstructionDefinition[],
|
|
327
241
|
logPrefix: string,
|
|
328
242
|
managedSourceRoot: string,
|
|
243
|
+
managedSectionContent: string,
|
|
329
244
|
{
|
|
330
245
|
dryRun,
|
|
331
246
|
overwriteBlockedPaths,
|
|
@@ -333,8 +248,8 @@ function ensureInstructionStubs(
|
|
|
333
248
|
dryRun: boolean;
|
|
334
249
|
overwriteBlockedPaths: Set<string>;
|
|
335
250
|
},
|
|
336
|
-
):
|
|
337
|
-
const result:
|
|
251
|
+
): TEnsureInstructionFilesResult {
|
|
252
|
+
const result: TEnsureInstructionFilesResult = {
|
|
338
253
|
blocked: [],
|
|
339
254
|
created: [],
|
|
340
255
|
overwritten: [],
|
|
@@ -353,54 +268,51 @@ function ensureInstructionStubs(
|
|
|
353
268
|
continue;
|
|
354
269
|
}
|
|
355
270
|
|
|
356
|
-
const sourceFilepath = instructionDefinition.sourcePath;
|
|
357
|
-
if (!fs.existsSync(sourceFilepath)) throw new Error(`Missing project instruction asset: ${sourceFilepath}`);
|
|
358
|
-
|
|
359
|
-
const stubContent = renderInstructionStub({
|
|
360
|
-
includeWorktreeBootstrap: instructionDefinition.includeWorktreeBootstrap === true,
|
|
361
|
-
projectFilepath,
|
|
362
|
-
sourceFilepath,
|
|
363
|
-
});
|
|
364
|
-
|
|
365
271
|
const existingState = inspectExistingPath({
|
|
366
272
|
managedSourceRoot,
|
|
367
273
|
projectFilepath,
|
|
368
|
-
sourceFilepath,
|
|
369
|
-
stubContent,
|
|
370
274
|
});
|
|
371
275
|
|
|
372
|
-
if (existingState.kind === '
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
276
|
+
if (existingState.kind === 'file') {
|
|
277
|
+
const nextContent = upsertManagedInstructionSection(existingState.content, managedSectionContent);
|
|
278
|
+
if (nextContent === existingState.content) {
|
|
279
|
+
result.skipped.push(relativeProjectPath);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
376
282
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
283
|
+
if (!dryRun) fs.writeFileSync(projectFilepath, nextContent);
|
|
284
|
+
result.updated.push(relativeProjectPath);
|
|
285
|
+
logVerbose(`${logPrefix} Updated ${relativeProjectPath}`);
|
|
380
286
|
continue;
|
|
381
287
|
}
|
|
382
288
|
|
|
383
289
|
if (existingState.kind === 'managed-different') {
|
|
384
290
|
if (!dryRun) {
|
|
385
291
|
fs.removeSync(projectFilepath);
|
|
386
|
-
fs.writeFileSync(projectFilepath,
|
|
292
|
+
fs.writeFileSync(projectFilepath, managedSectionContent);
|
|
387
293
|
}
|
|
388
294
|
result.updated.push(relativeProjectPath);
|
|
389
295
|
logVerbose(`${logPrefix} Updated ${relativeProjectPath}`);
|
|
390
296
|
continue;
|
|
391
297
|
}
|
|
392
298
|
|
|
299
|
+
const normalizedProjectFilepath = normalizeAbsolutePath(projectFilepath);
|
|
300
|
+
if (existingState.kind === 'blocked' && !overwriteBlockedPaths.has(normalizedProjectFilepath)) {
|
|
301
|
+
result.blocked.push(relativeProjectPath);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
393
305
|
if (existingState.kind === 'blocked') {
|
|
394
306
|
if (!dryRun) {
|
|
395
307
|
fs.removeSync(projectFilepath);
|
|
396
|
-
fs.writeFileSync(projectFilepath,
|
|
308
|
+
fs.writeFileSync(projectFilepath, managedSectionContent);
|
|
397
309
|
}
|
|
398
310
|
result.overwritten.push(relativeProjectPath);
|
|
399
311
|
logVerbose(`${logPrefix} Replaced ${relativeProjectPath}`);
|
|
400
312
|
continue;
|
|
401
313
|
}
|
|
402
314
|
|
|
403
|
-
if (!dryRun) fs.writeFileSync(projectFilepath,
|
|
315
|
+
if (!dryRun) fs.writeFileSync(projectFilepath, managedSectionContent);
|
|
404
316
|
result.created.push(relativeProjectPath);
|
|
405
317
|
logVerbose(`${logPrefix} Created ${relativeProjectPath}`);
|
|
406
318
|
}
|
|
@@ -408,45 +320,12 @@ function ensureInstructionStubs(
|
|
|
408
320
|
return result;
|
|
409
321
|
}
|
|
410
322
|
|
|
411
|
-
function renderInstructionStub({
|
|
412
|
-
includeWorktreeBootstrap,
|
|
413
|
-
projectFilepath,
|
|
414
|
-
sourceFilepath,
|
|
415
|
-
}: {
|
|
416
|
-
includeWorktreeBootstrap: boolean;
|
|
417
|
-
projectFilepath: string;
|
|
418
|
-
sourceFilepath: string;
|
|
419
|
-
}) {
|
|
420
|
-
const sourcePath = normalizeProjectPathForGitignore(path.relative(path.dirname(projectFilepath), sourceFilepath));
|
|
421
|
-
const lines = [
|
|
422
|
-
...(includeWorktreeBootstrap ? [...newProteumWorktreeBootstrapBlock, ''] : []),
|
|
423
|
-
managedInstructionStubHeader,
|
|
424
|
-
'',
|
|
425
|
-
managedInstructionStubIntro,
|
|
426
|
-
'',
|
|
427
|
-
'Before reading or applying instructions from this file, read and follow the canonical Proteum instruction file at:',
|
|
428
|
-
'',
|
|
429
|
-
`\`${sourcePath}\``,
|
|
430
|
-
'',
|
|
431
|
-
'Resolve that path relative to this file. Treat the canonical file as if its full contents were written here.',
|
|
432
|
-
'',
|
|
433
|
-
'If the canonical file cannot be read, stop and run `npx proteum configure agents` before continuing.',
|
|
434
|
-
'',
|
|
435
|
-
];
|
|
436
|
-
|
|
437
|
-
return lines.join('\n');
|
|
438
|
-
}
|
|
439
|
-
|
|
440
323
|
function inspectExistingPath({
|
|
441
324
|
managedSourceRoot,
|
|
442
325
|
projectFilepath,
|
|
443
|
-
sourceFilepath,
|
|
444
|
-
stubContent,
|
|
445
326
|
}: {
|
|
446
327
|
managedSourceRoot: string;
|
|
447
328
|
projectFilepath: string;
|
|
448
|
-
sourceFilepath: string;
|
|
449
|
-
stubContent: string;
|
|
450
329
|
}) {
|
|
451
330
|
if (!pathEntryExists(projectFilepath)) return { kind: 'missing' as const };
|
|
452
331
|
|
|
@@ -454,30 +333,35 @@ function inspectExistingPath({
|
|
|
454
333
|
if (!stats.isSymbolicLink()) {
|
|
455
334
|
if (!stats.isFile()) return { kind: 'blocked' as const };
|
|
456
335
|
|
|
457
|
-
const
|
|
458
|
-
if (existingContent === stubContent) return { kind: 'match' as const };
|
|
459
|
-
if (isManagedInstructionStub(existingContent)) return { kind: 'managed-different' as const };
|
|
336
|
+
const content = fs.readFileSync(projectFilepath, 'utf8');
|
|
460
337
|
|
|
461
|
-
return { kind: '
|
|
338
|
+
return { kind: 'file' as const, content };
|
|
462
339
|
}
|
|
463
340
|
|
|
464
341
|
const existingTarget = resolveSymlinkTarget(projectFilepath);
|
|
465
342
|
const normalizedExistingTarget = normalizeAbsolutePath(existingTarget);
|
|
466
|
-
const normalizedSourceFilepath = normalizeAbsolutePath(sourceFilepath);
|
|
467
343
|
const normalizedManagedSourceRoot = normalizeAbsolutePath(managedSourceRoot);
|
|
468
344
|
|
|
469
|
-
if (
|
|
470
|
-
normalizedExistingTarget === normalizedSourceFilepath ||
|
|
471
|
-
normalizedExistingTarget === normalizedManagedSourceRoot ||
|
|
472
|
-
normalizedExistingTarget.startsWith(`${normalizedManagedSourceRoot}/`)
|
|
473
|
-
)
|
|
345
|
+
if (isManagedInstructionSymlinkTarget({ normalizedExistingTarget, normalizedManagedSourceRoot }))
|
|
474
346
|
return { kind: 'managed-different' as const };
|
|
475
347
|
|
|
476
348
|
return { kind: 'blocked' as const };
|
|
477
349
|
}
|
|
478
350
|
|
|
479
|
-
function
|
|
480
|
-
|
|
351
|
+
function isManagedInstructionSymlinkTarget({
|
|
352
|
+
normalizedExistingTarget,
|
|
353
|
+
normalizedManagedSourceRoot,
|
|
354
|
+
}: {
|
|
355
|
+
normalizedExistingTarget: string;
|
|
356
|
+
normalizedManagedSourceRoot: string;
|
|
357
|
+
}) {
|
|
358
|
+
if (normalizedExistingTarget === normalizedManagedSourceRoot) return true;
|
|
359
|
+
if (normalizedExistingTarget.startsWith(`${normalizedManagedSourceRoot}/`)) return true;
|
|
360
|
+
|
|
361
|
+
const targetSegments = normalizedExistingTarget.split('/');
|
|
362
|
+
return targetSegments.some(
|
|
363
|
+
(segment, index) => segment === 'agents' && targetSegments[index + 1] === 'project',
|
|
364
|
+
);
|
|
481
365
|
}
|
|
482
366
|
|
|
483
367
|
function resolveSymlinkTarget(projectFilepath: string) {
|
|
@@ -488,7 +372,7 @@ function resolveSymlinkTarget(projectFilepath: string) {
|
|
|
488
372
|
|
|
489
373
|
function mergeInstructionResults(
|
|
490
374
|
result: TConfigureProjectAgentInstructionsResult,
|
|
491
|
-
next:
|
|
375
|
+
next: TEnsureInstructionFilesResult,
|
|
492
376
|
rootDir: string,
|
|
493
377
|
) {
|
|
494
378
|
result.created.push(...next.created.map((entry) => formatResultPath(rootDir, entry)));
|
|
@@ -498,10 +382,204 @@ function mergeInstructionResults(
|
|
|
498
382
|
result.blocked.push(...next.blocked.map((entry) => formatResultPath(rootDir, entry)));
|
|
499
383
|
}
|
|
500
384
|
|
|
385
|
+
function renderEmbeddedProjectInstructions({ coreRoot }: TProjectInstructionArgs) {
|
|
386
|
+
const agentSourceRoot = path.join(coreRoot, 'agents', 'project');
|
|
387
|
+
if (!fs.existsSync(agentSourceRoot)) throw new Error(`Missing project instruction source root: ${agentSourceRoot}`);
|
|
388
|
+
|
|
389
|
+
const sourceFiles = collectMarkdownFiles(agentSourceRoot).sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
390
|
+
const lines = [
|
|
391
|
+
managedInstructionSectionHeader,
|
|
392
|
+
managedInstructionSectionStart,
|
|
393
|
+
'',
|
|
394
|
+
managedInstructionSectionIntro,
|
|
395
|
+
'',
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
for (const sourceFile of sourceFiles) {
|
|
399
|
+
const content = fs.readFileSync(sourceFile.filepath, 'utf8');
|
|
400
|
+
const demotedContent = demoteMarkdownHeadings(content).trim();
|
|
401
|
+
|
|
402
|
+
lines.push(`## Source: ${sourceFile.relativePath}`, '');
|
|
403
|
+
if (demotedContent) lines.push(demotedContent, '');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
lines.push(managedInstructionSectionEnd, '');
|
|
407
|
+
|
|
408
|
+
return lines.join('\n');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function collectMarkdownFiles(rootDir: string, currentDir = rootDir): { filepath: string; relativePath: string }[] {
|
|
412
|
+
const files: { filepath: string; relativePath: string }[] = [];
|
|
413
|
+
|
|
414
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
415
|
+
const filepath = path.join(currentDir, entry.name);
|
|
416
|
+
|
|
417
|
+
if (entry.isDirectory()) {
|
|
418
|
+
files.push(...collectMarkdownFiles(rootDir, filepath));
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
423
|
+
|
|
424
|
+
files.push({
|
|
425
|
+
filepath,
|
|
426
|
+
relativePath: normalizeProjectPathForGitignore(path.relative(rootDir, filepath)),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return files;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function demoteMarkdownHeadings(content: string) {
|
|
434
|
+
const lines = content.split(/\r?\n/);
|
|
435
|
+
let activeFence: string | undefined;
|
|
436
|
+
|
|
437
|
+
return lines
|
|
438
|
+
.map((line) => {
|
|
439
|
+
const fenceMatch = line.match(/^\s*(```+|~~~+)/);
|
|
440
|
+
if (fenceMatch) {
|
|
441
|
+
const marker = fenceMatch[1].startsWith('`') ? '`' : '~';
|
|
442
|
+
activeFence = activeFence === marker ? undefined : marker;
|
|
443
|
+
return line;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (activeFence) return line;
|
|
447
|
+
|
|
448
|
+
return line.replace(/^(#{1,5})(\s+)/, '#$1$2');
|
|
449
|
+
})
|
|
450
|
+
.join('\n');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function upsertManagedInstructionSection(content: string, managedSectionContent: string) {
|
|
454
|
+
const existingRange = findManagedInstructionSectionRange(content);
|
|
455
|
+
|
|
456
|
+
if (!existingRange) {
|
|
457
|
+
const legacyStubRange = findLegacyManagedInstructionStubRange(content);
|
|
458
|
+
|
|
459
|
+
if (legacyStubRange) {
|
|
460
|
+
const before = content.slice(0, legacyStubRange.start);
|
|
461
|
+
const after = content.slice(legacyStubRange.end);
|
|
462
|
+
|
|
463
|
+
return joinMarkdownSections([before, managedSectionContent, after]);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return joinMarkdownSections([content, managedSectionContent]);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const before = content.slice(0, existingRange.start);
|
|
470
|
+
const after = content.slice(existingRange.end);
|
|
471
|
+
|
|
472
|
+
return joinMarkdownSections([before, managedSectionContent, after]);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function findManagedInstructionSectionRange(content: string) {
|
|
476
|
+
const markerStartIndex = content.indexOf(managedInstructionSectionStart);
|
|
477
|
+
if (markerStartIndex === -1) return undefined;
|
|
478
|
+
|
|
479
|
+
const markerEndIndex = content.indexOf(managedInstructionSectionEnd, markerStartIndex);
|
|
480
|
+
if (markerEndIndex === -1) return undefined;
|
|
481
|
+
|
|
482
|
+
const rangeEnd = markerEndIndex + managedInstructionSectionEnd.length;
|
|
483
|
+
const contentThroughStartMarker = content.slice(0, markerStartIndex + managedInstructionSectionStart.length);
|
|
484
|
+
const headerPattern = new RegExp(
|
|
485
|
+
`(^|\\n)${escapeRegExp(managedInstructionSectionHeader)}\\s*\\n(?:[ \\t]*\\n)*${escapeRegExp(managedInstructionSectionStart)}$`,
|
|
486
|
+
);
|
|
487
|
+
const headerMatch = contentThroughStartMarker.match(headerPattern);
|
|
488
|
+
|
|
489
|
+
if (!headerMatch) return { start: markerStartIndex, end: rangeEnd };
|
|
490
|
+
|
|
491
|
+
const matchedContent = headerMatch[0];
|
|
492
|
+
const leadingNewlineOffset = matchedContent.startsWith('\n') ? 1 : 0;
|
|
493
|
+
const rangeStart = markerStartIndex + managedInstructionSectionStart.length - matchedContent.length + leadingNewlineOffset;
|
|
494
|
+
|
|
495
|
+
return { start: rangeStart, end: rangeEnd };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function findLegacyManagedInstructionStubRange(content: string) {
|
|
499
|
+
const lines = content.split(/(?<=\n)/);
|
|
500
|
+
let offset = 0;
|
|
501
|
+
|
|
502
|
+
for (let index = 0; index < lines.length; index++) {
|
|
503
|
+
const line = lines[index];
|
|
504
|
+
|
|
505
|
+
if (line.trim() !== managedInstructionStubHeader) {
|
|
506
|
+
offset += line.length;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let endOffset = content.length;
|
|
511
|
+
let scanOffset = offset + line.length;
|
|
512
|
+
|
|
513
|
+
for (let scanIndex = index + 1; scanIndex < lines.length; scanIndex++) {
|
|
514
|
+
const currentLine = lines[scanIndex];
|
|
515
|
+
|
|
516
|
+
scanOffset += currentLine.length;
|
|
517
|
+
if (currentLine.trim() !== managedInstructionStubFinalLine) continue;
|
|
518
|
+
|
|
519
|
+
let blankIndex = scanIndex + 1;
|
|
520
|
+
let blankOffset = scanOffset;
|
|
521
|
+
|
|
522
|
+
while (blankIndex < lines.length && lines[blankIndex].trim() === '') {
|
|
523
|
+
blankOffset += lines[blankIndex].length;
|
|
524
|
+
blankIndex += 1;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
endOffset = blankOffset;
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return { start: offset, end: endOffset };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function joinMarkdownSections(sections: string[]) {
|
|
538
|
+
return `${sections
|
|
539
|
+
.map((section) => trimBlankLines(section.split(/\r?\n/)).join('\n'))
|
|
540
|
+
.filter(Boolean)
|
|
541
|
+
.join('\n\n')}\n`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function trimBlankLines(lines: string[]) {
|
|
545
|
+
const trimmedLines = trimTrailingBlankLines(lines);
|
|
546
|
+
|
|
547
|
+
while (trimmedLines.length > 0 && trimmedLines[0].trim() === '') trimmedLines.shift();
|
|
548
|
+
|
|
549
|
+
return trimmedLines;
|
|
550
|
+
}
|
|
551
|
+
|
|
501
552
|
function formatResultPath(rootDir: string, relativePath: string) {
|
|
502
553
|
return normalizeProjectPathForGitignore(path.join(rootDir, relativePath));
|
|
503
554
|
}
|
|
504
555
|
|
|
556
|
+
export function resolveCanonicalPath(inputPath: string) {
|
|
557
|
+
const resolvedPath = path.resolve(inputPath);
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
return fs.realpathSync(resolvedPath);
|
|
561
|
+
} catch {
|
|
562
|
+
return resolvedPath;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function isInsideDirectory({ child, parent }: { child: string; parent: string }) {
|
|
567
|
+
const relativePath = path.relative(parent, child);
|
|
568
|
+
return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function findLikelyRepoRoot(startPath: string) {
|
|
572
|
+
let currentPath = path.resolve(startPath);
|
|
573
|
+
|
|
574
|
+
while (true) {
|
|
575
|
+
if (pathEntryExists(path.join(currentPath, '.git'))) return resolveCanonicalPath(currentPath);
|
|
576
|
+
|
|
577
|
+
const parentPath = path.dirname(currentPath);
|
|
578
|
+
if (parentPath === currentPath) return undefined;
|
|
579
|
+
currentPath = parentPath;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
505
583
|
function normalizeAbsolutePath(filepath: string) {
|
|
506
584
|
return filepath.replace(/\\/g, '/');
|
|
507
585
|
}
|
|
@@ -530,6 +608,10 @@ function trimTrailingBlankLines(lines: string[]) {
|
|
|
530
608
|
return trimmedLines;
|
|
531
609
|
}
|
|
532
610
|
|
|
611
|
+
function escapeRegExp(value: string) {
|
|
612
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
613
|
+
}
|
|
614
|
+
|
|
533
615
|
function pathEntryExists(filepath: string) {
|
|
534
616
|
try {
|
|
535
617
|
fs.lstatSync(filepath);
|