@imdeadpool/guardex 7.0.22 → 7.0.24

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.
@@ -0,0 +1,282 @@
1
+ const TASK_SIZE_UPPER_BOUNDS = {
2
+ 'narrow-patch': 1_800_000,
3
+ 'medium-change': 4_000_000,
4
+ 'large-change': 8_000_000,
5
+ };
6
+
7
+ const TASK_SIZE_VALUES = Object.keys(TASK_SIZE_UPPER_BOUNDS);
8
+ const TASK_SIZE_SET = new Set(TASK_SIZE_VALUES);
9
+ const FRAGMENTATION_PRESET_SCORES = {
10
+ clean: 0,
11
+ 'few-extra-checks': 5,
12
+ 'repeated-follow-ups': 10,
13
+ looping: 18,
14
+ 'dominant-loop': 25,
15
+ };
16
+ const FINISH_PATH_PRESET_SCORES = {
17
+ 'clear-early': 0,
18
+ 'minor-hesitation': 5,
19
+ 'late-decision': 10,
20
+ reopening: 15,
21
+ };
22
+ const POST_PROOF_PRESET_SCORES = {
23
+ 'stops-soon': 0,
24
+ 'small-tail': 5,
25
+ 'notable-tail': 10,
26
+ 'heavy-tail': 15,
27
+ };
28
+ const DRIVER_TIE_BREAK = ['fragmentation', 'writeStdin', 'finishPath', 'postProof', 'cost'];
29
+ const DRIVER_LABELS = {
30
+ cost: 'cost vs expected scope',
31
+ fragmentation: 'turn fragmentation',
32
+ writeStdin: 'write_stdin churn',
33
+ finishPath: 'finish-path discipline',
34
+ postProof: 'post-proof drift',
35
+ };
36
+ const LABEL_BANDS = [
37
+ { max: 15, label: 'Healthy' },
38
+ { max: 30, label: 'Mildly fragmented' },
39
+ { max: 50, label: 'Inefficient' },
40
+ { max: 75, label: 'Runaway' },
41
+ { max: 100, label: 'Catastrophic' },
42
+ ];
43
+ const SESSION_SEVERITY_SUBCOMMAND = 'session-severity';
44
+ const SESSION_SEVERITY_USAGE_ARGS = [
45
+ `--task-size <${TASK_SIZE_VALUES.join('|')}>`,
46
+ '--tokens <count>',
47
+ '--exec-count <count>',
48
+ '--write-stdin-count <count>',
49
+ '--completion-before-tail <yes|no>',
50
+ '[--expected-bound <count>]',
51
+ `[--fragmentation <${Object.keys(FRAGMENTATION_PRESET_SCORES).join('|')}|0-25>]`,
52
+ `[--finish-path <${Object.keys(FINISH_PATH_PRESET_SCORES).join('|')}|0-15>]`,
53
+ `[--post-proof <${Object.keys(POST_PROOF_PRESET_SCORES).join('|')}|0-15>]`,
54
+ '[--json]',
55
+ ].join(' ');
56
+ const SESSION_SEVERITY_COMMAND_TAIL = `${SESSION_SEVERITY_SUBCOMMAND} ${SESSION_SEVERITY_USAGE_ARGS}`;
57
+ const SESSION_SEVERITY_EXAMPLE_ARGS = [
58
+ '--task-size',
59
+ 'narrow-patch',
60
+ '--tokens',
61
+ '3850000',
62
+ '--exec-count',
63
+ '18',
64
+ '--write-stdin-count',
65
+ '6',
66
+ '--completion-before-tail',
67
+ 'yes',
68
+ '--fragmentation',
69
+ '14',
70
+ '--finish-path',
71
+ '6',
72
+ '--post-proof',
73
+ '4',
74
+ ];
75
+ const SESSION_SEVERITY_EXAMPLE_TAIL = `${SESSION_SEVERITY_SUBCOMMAND} ${SESSION_SEVERITY_EXAMPLE_ARGS.join(' ')}`;
76
+
77
+ function formatInteger(value) {
78
+ return Number(value).toLocaleString('en-US');
79
+ }
80
+
81
+ function parseRequiredPositiveInteger(name, rawValue, { allowZero = true } = {}) {
82
+ const parsed = Number.parseInt(String(rawValue || ''), 10);
83
+ if (!Number.isFinite(parsed) || (!allowZero && parsed <= 0) || (allowZero && parsed < 0)) {
84
+ throw new Error(`${name} requires ${allowZero ? 'a non-negative integer' : 'a positive integer'} value`);
85
+ }
86
+ return parsed;
87
+ }
88
+
89
+ function parseBooleanFlag(name, rawValue) {
90
+ const normalized = String(rawValue || '').trim().toLowerCase();
91
+ if (normalized === 'yes' || normalized === 'true' || normalized === '1') {
92
+ return true;
93
+ }
94
+ if (normalized === 'no' || normalized === 'false' || normalized === '0') {
95
+ return false;
96
+ }
97
+ throw new Error(`${name} requires yes/no (or true/false, 1/0)`);
98
+ }
99
+
100
+ function clampScore(value, min, max) {
101
+ return Math.max(min, Math.min(max, Math.round(value)));
102
+ }
103
+
104
+ function parseTaskSize(rawTaskSize) {
105
+ const normalized = String(rawTaskSize || '').trim();
106
+ if (!TASK_SIZE_SET.has(normalized)) {
107
+ throw new Error(`--task-size must be one of: ${TASK_SIZE_VALUES.join(', ')}`);
108
+ }
109
+ return normalized;
110
+ }
111
+
112
+ function resolveExpectedUpperBound(taskSize, rawExpectedBound) {
113
+ if (rawExpectedBound) {
114
+ return parseRequiredPositiveInteger('--expected-bound', rawExpectedBound, { allowZero: false });
115
+ }
116
+ return TASK_SIZE_UPPER_BOUNDS[taskSize];
117
+ }
118
+
119
+ function scoreCost(tokens, expectedUpperBound) {
120
+ const ratio = tokens / expectedUpperBound;
121
+ if (ratio <= 1.0) return 0;
122
+ if (ratio <= 1.5) return 5;
123
+ if (ratio <= 2.5) return 10;
124
+ if (ratio <= 4.0) return 18;
125
+ if (ratio <= 6.0) return 24;
126
+ return 30;
127
+ }
128
+
129
+ function scoreFragmentation(execCount, override) {
130
+ if (override) {
131
+ if (Object.prototype.hasOwnProperty.call(FRAGMENTATION_PRESET_SCORES, override)) {
132
+ return FRAGMENTATION_PRESET_SCORES[override];
133
+ }
134
+ return clampScore(parseRequiredPositiveInteger('--fragmentation', override), 0, 25);
135
+ }
136
+ if (execCount <= 4) return 0;
137
+ if (execCount <= 8) return 5;
138
+ if (execCount <= 16) return 10;
139
+ if (execCount <= 28) return 18;
140
+ return 25;
141
+ }
142
+
143
+ function scoreWriteStdin(writeStdinCount) {
144
+ if (writeStdinCount <= 0) return 0;
145
+ if (writeStdinCount <= 3) return 5;
146
+ if (writeStdinCount <= 6) return 10;
147
+ return 15;
148
+ }
149
+
150
+ function scoreFinishPath(completionBeforeTail, override) {
151
+ if (override) {
152
+ if (Object.prototype.hasOwnProperty.call(FINISH_PATH_PRESET_SCORES, override)) {
153
+ return FINISH_PATH_PRESET_SCORES[override];
154
+ }
155
+ return clampScore(parseRequiredPositiveInteger('--finish-path', override), 0, 15);
156
+ }
157
+ return completionBeforeTail ? 0 : 5;
158
+ }
159
+
160
+ function scorePostProof(completionBeforeTail, override) {
161
+ if (override) {
162
+ if (Object.prototype.hasOwnProperty.call(POST_PROOF_PRESET_SCORES, override)) {
163
+ return POST_PROOF_PRESET_SCORES[override];
164
+ }
165
+ return clampScore(parseRequiredPositiveInteger('--post-proof', override), 0, 15);
166
+ }
167
+ return completionBeforeTail ? 0 : 10;
168
+ }
169
+
170
+ function labelForTotal(total) {
171
+ return LABEL_BANDS.find((band) => total <= band.max)?.label || LABEL_BANDS[LABEL_BANDS.length - 1].label;
172
+ }
173
+
174
+ function buildSessionSeverityReport(options) {
175
+ const taskSize = parseTaskSize(options.taskSize);
176
+ const tokens = parseRequiredPositiveInteger('--tokens', options.tokens);
177
+ const execCount = parseRequiredPositiveInteger('--exec-count', options.execCount);
178
+ const writeStdinCount = parseRequiredPositiveInteger('--write-stdin-count', options.writeStdinCount);
179
+ const completionBeforeTail = parseBooleanFlag('--completion-before-tail', options.completionBeforeTail);
180
+ const expectedUpperBound = resolveExpectedUpperBound(taskSize, options.expectedBound);
181
+ const costRatio = tokens / expectedUpperBound;
182
+ const scores = {
183
+ cost: scoreCost(tokens, expectedUpperBound),
184
+ fragmentation: scoreFragmentation(execCount, options.fragmentation),
185
+ writeStdin: scoreWriteStdin(writeStdinCount),
186
+ finishPath: scoreFinishPath(completionBeforeTail, options.finishPath),
187
+ postProof: scorePostProof(completionBeforeTail, options.postProof),
188
+ };
189
+ const total = scores.cost + scores.fragmentation + scores.writeStdin + scores.finishPath + scores.postProof;
190
+ const label = labelForTotal(total);
191
+ const rankedDimensions = Object.entries(scores)
192
+ .map(([key, score]) => ({ key, score, label: DRIVER_LABELS[key] }))
193
+ .filter((entry) => entry.score > 0)
194
+ .sort((left, right) => {
195
+ if (right.score !== left.score) {
196
+ return right.score - left.score;
197
+ }
198
+ return DRIVER_TIE_BREAK.indexOf(left.key) - DRIVER_TIE_BREAK.indexOf(right.key);
199
+ });
200
+ const primaryDriver = rankedDimensions[0] ? rankedDimensions[0].label : 'none';
201
+ const secondaries = rankedDimensions.slice(1).map((entry) => entry.label);
202
+
203
+ return {
204
+ taskSize,
205
+ expectedUpperBound,
206
+ tokens,
207
+ execCount,
208
+ writeStdinCount,
209
+ completionBeforeTail,
210
+ costRatio,
211
+ scores: {
212
+ ...scores,
213
+ total,
214
+ },
215
+ label,
216
+ primaryDriver,
217
+ secondaries,
218
+ outputLine: `Score ${total}/100 — ${label}. Primary: ${primaryDriver}. Secondaries: ${
219
+ secondaries.length > 0 ? secondaries.join(', ') : 'none'
220
+ }.`,
221
+ };
222
+ }
223
+
224
+ function renderSessionSeverityReport(report) {
225
+ return [
226
+ report.outputLine,
227
+ '',
228
+ `Task size: ${report.taskSize}`,
229
+ `Expected upper bound: ${report.expectedUpperBound}`,
230
+ `Actual tokens: ${report.tokens}`,
231
+ `Exec count: ${report.execCount}`,
232
+ `write_stdin count: ${report.writeStdinCount}`,
233
+ `Completion before tail churn: ${report.completionBeforeTail ? 'yes' : 'no'}`,
234
+ `Cost ratio: ${report.costRatio.toFixed(2)}x`,
235
+ '',
236
+ `A. Cost vs expected scope: ${report.scores.cost}`,
237
+ `B. Turn fragmentation: ${report.scores.fragmentation}`,
238
+ `C. write_stdin churn: ${report.scores.writeStdin}`,
239
+ `D. Finish-path discipline: ${report.scores.finishPath}`,
240
+ `E. Post-proof drift: ${report.scores.postProof}`,
241
+ '',
242
+ `Total: ${report.scores.total}`,
243
+ `Label: ${report.label}`,
244
+ `Primary driver: ${report.primaryDriver}`,
245
+ `Secondary drivers: ${report.secondaries.length > 0 ? report.secondaries.join(', ') : 'none'}`,
246
+ ].join('\n');
247
+ }
248
+
249
+ function renderSessionSeverityHelpDetails() {
250
+ const taskSizeDefaults = TASK_SIZE_VALUES
251
+ .map((taskSize) => `${taskSize}=${formatInteger(TASK_SIZE_UPPER_BOUNDS[taskSize])}`)
252
+ .join(', ');
253
+ const labelBands = LABEL_BANDS
254
+ .map((band, index) => {
255
+ const min = index === 0 ? 0 : LABEL_BANDS[index - 1].max + 1;
256
+ return `${band.label}=${min}-${band.max}`;
257
+ })
258
+ .join(', ');
259
+ return [`Task-size defaults: ${taskSizeDefaults}`, `Label bands: ${labelBands}`].join('\n');
260
+ }
261
+
262
+ function renderSessionSeverityCommand(toolName) {
263
+ return `${toolName} report ${SESSION_SEVERITY_COMMAND_TAIL}`;
264
+ }
265
+
266
+ function renderSessionSeverityExample(toolName) {
267
+ return `${toolName} report ${SESSION_SEVERITY_EXAMPLE_TAIL}`;
268
+ }
269
+
270
+ module.exports = {
271
+ LABEL_BANDS,
272
+ SESSION_SEVERITY_COMMAND_TAIL,
273
+ SESSION_SEVERITY_EXAMPLE_ARGS,
274
+ SESSION_SEVERITY_EXAMPLE_TAIL,
275
+ TASK_SIZE_UPPER_BOUNDS,
276
+ buildSessionSeverityReport,
277
+ renderSessionSeverityReport,
278
+ renderSessionSeverityHelpDetails,
279
+ renderSessionSeverityCommand,
280
+ renderSessionSeverityExample,
281
+ labelForTotal,
282
+ };
@@ -1,6 +1,7 @@
1
1
  const {
2
2
  fs,
3
3
  path,
4
+ PACKAGE_ROOT,
4
5
  TOOL_NAME,
5
6
  SHORT_TOOL_NAME,
6
7
  GUARDEX_HOME_DIR,
@@ -9,6 +10,7 @@ const {
9
10
  HOOK_NAMES,
10
11
  LOCK_FILE_RELATIVE,
11
12
  LEGACY_MANAGED_PACKAGE_SCRIPTS,
13
+ PACKAGE_ROOT_SOURCE_OVERRIDES,
12
14
  USER_LEVEL_SKILL_ASSETS,
13
15
  AGENTS_MARKER_START,
14
16
  AGENTS_MARKER_END,
@@ -24,6 +26,7 @@ const {
24
26
  EXECUTABLE_RELATIVE_PATHS,
25
27
  CRITICAL_GUARDRAIL_PATHS,
26
28
  } = require('../context');
29
+ const { parse: parseJsonc, printParseErrorCode } = require('jsonc-parser');
27
30
  const { run } = require('../core/runtime');
28
31
 
29
32
  function ensureParentDir(repoRoot, filePath, dryRun) {
@@ -172,17 +175,13 @@ function ensureHookShim(repoRoot, hookName, options = {}) {
172
175
  );
173
176
  }
174
177
 
175
- function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
176
- const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
177
- const destinationRelativePath = toDestinationPath(relativeTemplatePath);
178
- const destinationPath = path.join(repoRoot, destinationRelativePath);
179
-
180
- const sourceContent = fs.readFileSync(sourcePath, 'utf8');
178
+ function copyManagedSourceFile(repoRoot, sourcePath, destinationPath, destinationRelativePath, force, dryRun) {
179
+ const sourceContent = fs.readFileSync(sourcePath);
181
180
  const destinationExists = fs.existsSync(destinationPath);
182
181
 
183
182
  if (destinationExists) {
184
- const existingContent = fs.readFileSync(destinationPath, 'utf8');
185
- if (existingContent === sourceContent) {
183
+ const existingContent = fs.readFileSync(destinationPath);
184
+ if (existingContent.equals(sourceContent)) {
186
185
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
187
186
  return { status: 'unchanged', file: destinationRelativePath };
188
187
  }
@@ -193,7 +192,7 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
193
192
 
194
193
  ensureParentDir(repoRoot, destinationPath, dryRun);
195
194
  if (!dryRun) {
196
- fs.writeFileSync(destinationPath, sourceContent, 'utf8');
195
+ fs.writeFileSync(destinationPath, sourceContent);
197
196
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
198
197
  }
199
198
 
@@ -204,22 +203,54 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
204
203
  return { status: destinationExists ? 'overwritten' : 'created', file: destinationRelativePath };
205
204
  }
206
205
 
206
+ function normalizeTemplatePath(relativeTemplatePath) {
207
+ return String(relativeTemplatePath).replace(/\\/g, '/');
208
+ }
209
+
210
+ function usesPackageRootSource(repoRoot, relativeTemplatePath) {
211
+ return (
212
+ path.resolve(repoRoot) === PACKAGE_ROOT &&
213
+ PACKAGE_ROOT_SOURCE_OVERRIDES.has(normalizeTemplatePath(relativeTemplatePath))
214
+ );
215
+ }
216
+
217
+ function resolveTemplateSourcePath(repoRoot, relativeTemplatePath) {
218
+ if (usesPackageRootSource(repoRoot, relativeTemplatePath)) {
219
+ return path.join(PACKAGE_ROOT, relativeTemplatePath);
220
+ }
221
+ return path.join(TEMPLATE_ROOT, relativeTemplatePath);
222
+ }
223
+
224
+ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
225
+ const sourcePath = resolveTemplateSourcePath(repoRoot, relativeTemplatePath);
226
+ const destinationRelativePath = toDestinationPath(relativeTemplatePath);
227
+ const destinationPath = path.join(repoRoot, destinationRelativePath);
228
+ return copyManagedSourceFile(
229
+ repoRoot,
230
+ sourcePath,
231
+ destinationPath,
232
+ destinationRelativePath,
233
+ force,
234
+ dryRun,
235
+ );
236
+ }
237
+
207
238
  function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {
208
- const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
239
+ const sourcePath = resolveTemplateSourcePath(repoRoot, relativeTemplatePath);
209
240
  const destinationRelativePath = toDestinationPath(relativeTemplatePath);
210
241
  const destinationPath = path.join(repoRoot, destinationRelativePath);
211
- const sourceContent = fs.readFileSync(sourcePath, 'utf8');
242
+ const sourceContent = fs.readFileSync(sourcePath);
212
243
 
213
244
  if (fs.existsSync(destinationPath)) {
214
- const existingContent = fs.readFileSync(destinationPath, 'utf8');
215
- if (existingContent === sourceContent) {
245
+ const existingContent = fs.readFileSync(destinationPath);
246
+ if (existingContent.equals(sourceContent)) {
216
247
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
217
248
  return { status: 'unchanged', file: destinationRelativePath };
218
249
  }
219
250
 
220
251
  if (isCriticalGuardrailPath(destinationRelativePath)) {
221
252
  if (!dryRun) {
222
- fs.writeFileSync(destinationPath, sourceContent, 'utf8');
253
+ fs.writeFileSync(destinationPath, sourceContent);
223
254
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
224
255
  }
225
256
  return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: destinationRelativePath };
@@ -230,13 +261,38 @@ function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {
230
261
 
231
262
  ensureParentDir(repoRoot, destinationPath, dryRun);
232
263
  if (!dryRun) {
233
- fs.writeFileSync(destinationPath, sourceContent, 'utf8');
264
+ fs.writeFileSync(destinationPath, sourceContent);
234
265
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
235
266
  }
236
267
 
237
268
  return { status: 'created', file: destinationRelativePath };
238
269
  }
239
270
 
271
+ function materializePackageRepoTemplateFiles(repoRoot, relativeTemplatePaths, dryRun) {
272
+ if (path.resolve(repoRoot) !== PACKAGE_ROOT) {
273
+ return [];
274
+ }
275
+
276
+ const operations = [];
277
+ for (const relativeTemplatePath of relativeTemplatePaths) {
278
+ if (!PACKAGE_ROOT_SOURCE_OVERRIDES.has(normalizeTemplatePath(relativeTemplatePath))) {
279
+ continue;
280
+ }
281
+ const templateRelativePath = path.posix.join('templates', normalizeTemplatePath(relativeTemplatePath));
282
+ operations.push(
283
+ copyManagedSourceFile(
284
+ PACKAGE_ROOT,
285
+ path.join(PACKAGE_ROOT, relativeTemplatePath),
286
+ path.join(PACKAGE_ROOT, templateRelativePath),
287
+ templateRelativePath,
288
+ true,
289
+ dryRun,
290
+ ),
291
+ );
292
+ }
293
+ return operations;
294
+ }
295
+
240
296
  function lockFilePath(repoRoot) {
241
297
  return path.join(repoRoot, LOCK_FILE_RELATIVE);
242
298
  }
@@ -521,121 +577,13 @@ function ensureManagedGitignore(repoRoot, dryRun) {
521
577
  return { status: 'updated', file: '.gitignore', note: 'appended gitguardex-managed entries' };
522
578
  }
523
579
 
524
- function stripJsonComments(source) {
525
- let result = '';
526
- let inString = false;
527
- let escapeNext = false;
528
- let inLineComment = false;
529
- let inBlockComment = false;
530
-
531
- for (let index = 0; index < source.length; index += 1) {
532
- const current = source[index];
533
- const next = source[index + 1];
534
-
535
- if (inLineComment) {
536
- if (current === '\n' || current === '\r') {
537
- inLineComment = false;
538
- result += current;
539
- }
540
- continue;
541
- }
542
-
543
- if (inBlockComment) {
544
- if (current === '*' && next === '/') {
545
- inBlockComment = false;
546
- index += 1;
547
- continue;
548
- }
549
- if (current === '\n' || current === '\r') {
550
- result += current;
551
- }
552
- continue;
553
- }
554
-
555
- if (inString) {
556
- result += current;
557
- if (escapeNext) {
558
- escapeNext = false;
559
- } else if (current === '\\') {
560
- escapeNext = true;
561
- } else if (current === '"') {
562
- inString = false;
563
- }
564
- continue;
565
- }
566
-
567
- if (current === '"') {
568
- inString = true;
569
- result += current;
570
- continue;
571
- }
572
-
573
- if (current === '/' && next === '/') {
574
- inLineComment = true;
575
- index += 1;
576
- continue;
577
- }
578
-
579
- if (current === '/' && next === '*') {
580
- inBlockComment = true;
581
- index += 1;
582
- continue;
583
- }
584
-
585
- result += current;
586
- }
587
-
588
- return result;
589
- }
590
-
591
- function stripJsonTrailingCommas(source) {
592
- let result = '';
593
- let inString = false;
594
- let escapeNext = false;
595
-
596
- for (let index = 0; index < source.length; index += 1) {
597
- const current = source[index];
598
-
599
- if (inString) {
600
- result += current;
601
- if (escapeNext) {
602
- escapeNext = false;
603
- } else if (current === '\\') {
604
- escapeNext = true;
605
- } else if (current === '"') {
606
- inString = false;
607
- }
608
- continue;
609
- }
610
-
611
- if (current === '"') {
612
- inString = true;
613
- result += current;
614
- continue;
615
- }
616
-
617
- if (current === ',') {
618
- let lookahead = index + 1;
619
- while (lookahead < source.length && /\s/.test(source[lookahead])) {
620
- lookahead += 1;
621
- }
622
- if (source[lookahead] === '}' || source[lookahead] === ']') {
623
- continue;
624
- }
625
- }
626
-
627
- result += current;
628
- }
629
-
630
- return result;
631
- }
632
-
633
580
  function parseJsonObjectLikeFile(source, relativePath) {
634
- let parsed;
635
- try {
636
- parsed = JSON.parse(stripJsonTrailingCommas(stripJsonComments(source)));
637
- } catch (error) {
638
- throw new Error(`Unable to parse ${relativePath} as JSON or JSONC: ${error.message}`);
581
+ const errors = [];
582
+ const parsed = parseJsonc(source, errors, { allowTrailingComma: true });
583
+
584
+ if (errors.length > 0) {
585
+ const formattedErrors = errors.map((entry) => printParseErrorCode(entry.error)).join(', ');
586
+ throw new Error(`Unable to parse ${relativePath} as JSON or JSONC: ${formattedErrors}`);
639
587
  }
640
588
 
641
589
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
@@ -806,6 +754,7 @@ module.exports = {
806
754
  ensureHookShim,
807
755
  copyTemplateFile,
808
756
  ensureTemplateFilePresent,
757
+ materializePackageRepoTemplateFiles,
809
758
  ensureOmxScaffold,
810
759
  ensureLockRegistry,
811
760
  lockStateOrError,
@@ -815,8 +764,6 @@ module.exports = {
815
764
  removeLegacyManagedRepoFile,
816
765
  ensureAgentsSnippet,
817
766
  ensureManagedGitignore,
818
- stripJsonComments,
819
- stripJsonTrailingCommas,
820
767
  parseJsonObjectLikeFile,
821
768
  buildRepoVscodeSettings,
822
769
  ensureRepoVscodeSettings,
@@ -17,50 +17,18 @@ const {
17
17
  envFlagIsTruthy,
18
18
  } = require('../context');
19
19
  const { run } = require('../core/runtime');
20
+ const {
21
+ parseVersionString,
22
+ compareParsedVersions,
23
+ isNewerVersion,
24
+ } = require('../core/versions');
25
+ const { readSingleLineFromStdin } = require('../core/stdin');
20
26
  const { colorize } = require('../output');
21
27
 
22
28
  function isInteractiveTerminal() {
23
29
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
24
30
  }
25
31
 
26
- const stdinWaitArray = new Int32Array(new SharedArrayBuffer(4));
27
-
28
- function sleepSyncMs(milliseconds) {
29
- Atomics.wait(stdinWaitArray, 0, 0, milliseconds);
30
- }
31
-
32
- function readSingleLineFromStdin() {
33
- let input = '';
34
- const buffer = Buffer.alloc(1);
35
-
36
- while (true) {
37
- let bytesRead = 0;
38
- try {
39
- bytesRead = fs.readSync(process.stdin.fd, buffer, 0, 1);
40
- } catch (error) {
41
- if (error && ['EAGAIN', 'EWOULDBLOCK', 'EINTR'].includes(error.code)) {
42
- sleepSyncMs(15);
43
- continue;
44
- }
45
- return input;
46
- }
47
-
48
- if (bytesRead === 0) {
49
- if (process.stdin.isTTY) {
50
- sleepSyncMs(15);
51
- continue;
52
- }
53
- return input;
54
- }
55
-
56
- const char = buffer.toString('utf8', 0, bytesRead);
57
- if (char === '\n' || char === '\r') {
58
- return input;
59
- }
60
- input += char;
61
- }
62
- }
63
-
64
32
  function parseAutoApproval(name) {
65
33
  const raw = process.env[name];
66
34
  if (raw == null) return null;
@@ -70,38 +38,6 @@ function parseAutoApproval(name) {
70
38
  return null;
71
39
  }
72
40
 
73
- function parseVersionString(version) {
74
- const match = String(version || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
75
- if (!match) return null;
76
- return [
77
- Number.parseInt(match[1], 10),
78
- Number.parseInt(match[2], 10),
79
- Number.parseInt(match[3], 10),
80
- ];
81
- }
82
-
83
- function compareParsedVersions(left, right) {
84
- if (!left || !right) return 0;
85
- for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
86
- const leftValue = left[index] || 0;
87
- const rightValue = right[index] || 0;
88
- if (leftValue > rightValue) return 1;
89
- if (leftValue < rightValue) return -1;
90
- }
91
- return 0;
92
- }
93
-
94
- function isNewerVersion(latest, current) {
95
- const latestParts = parseVersionString(latest);
96
- const currentParts = parseVersionString(current);
97
-
98
- if (!latestParts || !currentParts) {
99
- return String(latest || '').trim() !== String(current || '').trim();
100
- }
101
-
102
- return compareParsedVersions(latestParts, currentParts) > 0;
103
- }
104
-
105
41
  function parseNpmVersionOutput(stdout) {
106
42
  const trimmed = String(stdout || '').trim();
107
43
  if (!trimmed) return '';