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.
@@ -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 TEnsureInstructionStubsResult = {
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 managedInstructionStubIntro = 'This file is managed by `proteum configure agents`.';
61
- const newProteumWorktreeBootstrapBlock = [
62
- '## New Proteum Worktree Bootstrap',
63
- '',
64
- 'When starting work in a newly created Proteum worktree, immediately copy the `.env` file from the main worktree, change it to use a different free port by updating `PORT`, `URL`, and `URL_INTERNAL`, and run:',
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', sourcePath: 'CODING_STYLE.md' },
78
- { projectPath: 'diagnostics.md', sourcePath: 'diagnostics.md' },
79
- { projectPath: 'optimizations.md', sourcePath: 'optimizations.md' },
80
- { projectPath: path.join('client', 'AGENTS.md'), sourcePath: path.join('client', 'AGENTS.md') },
81
- { projectPath: path.join('client', 'pages', 'AGENTS.md'), sourcePath: path.join('client', 'pages', 'AGENTS.md') },
82
- {
83
- projectPath: path.join('server', 'services', 'AGENTS.md'),
84
- sourcePath: path.join('server', 'services', 'AGENTS.md'),
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', sourcePath: 'AGENTS.md', includeWorktreeBootstrap: true },
72
+ { projectPath: 'AGENTS.md' },
92
73
  ...sharedAppAgentInstructionDefinitions,
93
74
  ];
94
75
 
95
76
  const monorepoAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
96
- { projectPath: 'AGENTS.md', sourcePath: path.join('app-root', 'AGENTS.md') },
77
+ { projectPath: 'AGENTS.md' },
97
78
  ...sharedAppAgentInstructionDefinitions,
98
79
  ];
99
80
 
100
81
  const monorepoRootAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
101
- { projectPath: 'AGENTS.md', sourcePath: path.join('root', 'AGENTS.md'), includeWorktreeBootstrap: true },
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({ coreRoot });
142
- const rootStubs = ensureInstructionStubs(
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, rootStubs, normalizedMonorepoRoot);
135
+ mergeInstructionResults(result, rootFiles, normalizedMonorepoRoot);
153
136
 
154
- if (!dryRun && ensureInstructionGitignoreEntries({ rootDir: normalizedMonorepoRoot, instructionDefinitions: rootInstructions }))
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({ coreRoot, mode });
159
- const appStubs = ensureInstructionStubs(
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, appStubs, normalizedAppRoot);
153
+ mergeInstructionResults(result, appFiles, normalizedAppRoot);
170
154
 
171
- if (!dryRun && ensureInstructionGitignoreEntries({ rootDir: normalizedAppRoot, instructionDefinitions: appInstructions }))
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 getProjectInstructionGitignoreEntries({ coreRoot }: TProjectInstructionArgs) {
180
- return Array.from(
181
- new Set(
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
- if (fs.existsSync(absolutePath)) {
216
- result.existing.push(projectPath);
217
- continue;
218
- }
167
+ if (!likelyRepoRoot) return undefined;
168
+ if (likelyRepoRoot === normalizedAppRoot) return undefined;
169
+ if (!isInsideDirectory({ child: normalizedAppRoot, parent: likelyRepoRoot })) return undefined;
219
170
 
220
- result.missing.push(projectPath);
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
- }: TProjectInstructionArgs & { mode: 'monorepo' | 'standalone' }) {
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 resolveAgentInstructionDefinitions({
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 renderInstructionGitignoreBlock({ instructionDefinitions }: { instructionDefinitions: TAgentInstructionDefinition[] }) {
265
- const entries = Array.from(
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 ensureInstructionGitignoreEntries({
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 managedBlock = renderInstructionGitignoreBlock({ instructionDefinitions });
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] Updated ${path.relative(rootDir, gitignoreFilepath) || '.gitignore'} with Proteum-managed instruction ignore entries.`);
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 ensureInstructionStubs(
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
- ): TEnsureInstructionStubsResult {
337
- const result: TEnsureInstructionStubsResult = {
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 === 'match') {
373
- result.skipped.push(relativeProjectPath);
374
- continue;
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
- const normalizedProjectFilepath = normalizeAbsolutePath(projectFilepath);
378
- if (existingState.kind === 'blocked' && !overwriteBlockedPaths.has(normalizedProjectFilepath)) {
379
- result.blocked.push(relativeProjectPath);
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, stubContent);
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, stubContent);
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, stubContent);
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 existingContent = fs.readFileSync(projectFilepath, 'utf8');
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: 'blocked' as const };
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 isManagedInstructionStub(content: string) {
480
- return content.includes(`${managedInstructionStubHeader}\n\n${managedInstructionStubIntro}\n`);
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: TEnsureInstructionStubsResult,
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);