@imdeadpool/guardex 7.0.21 → 7.0.23
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/README.md +39 -29
- package/package.json +5 -1
- package/src/cli/args.js +89 -0
- package/src/cli/main.js +645 -2873
- package/src/context.js +195 -31
- package/src/core/stdin.js +52 -0
- package/src/core/versions.js +33 -0
- package/src/doctor/index.js +1071 -0
- package/src/finish/index.js +456 -358
- package/src/git/index.js +604 -1
- package/src/hooks/index.js +64 -0
- package/src/output/index.js +72 -5
- package/src/report/session-severity.js +213 -0
- package/src/sandbox/index.js +301 -52
- package/src/scaffold/index.js +627 -0
- package/src/toolchain/index.js +559 -179
- package/templates/AGENTS.multiagent-safety.md +25 -0
- package/templates/scripts/agent-branch-finish.sh +86 -6
- package/templates/scripts/agent-session-state.js +62 -1
- package/templates/scripts/agent-worktree-prune.sh +15 -1
- package/templates/scripts/codex-agent.sh +38 -0
- package/templates/scripts/install-vscode-active-agents-extension.js +38 -11
- package/templates/scripts/openspec/init-plan-workspace.sh +34 -3
- package/templates/vscode/guardex-active-agents/README.md +9 -6
- package/templates/vscode/guardex-active-agents/extension.js +805 -77
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/package.json +15 -3
- package/templates/vscode/guardex-active-agents/session-schema.js +311 -4
package/src/scaffold/index.js
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
const {
|
|
2
2
|
fs,
|
|
3
3
|
path,
|
|
4
|
+
PACKAGE_ROOT,
|
|
4
5
|
TOOL_NAME,
|
|
5
6
|
SHORT_TOOL_NAME,
|
|
7
|
+
GUARDEX_HOME_DIR,
|
|
8
|
+
AGENT_WORKTREE_RELATIVE_DIRS,
|
|
9
|
+
TEMPLATE_ROOT,
|
|
10
|
+
HOOK_NAMES,
|
|
11
|
+
LOCK_FILE_RELATIVE,
|
|
12
|
+
LEGACY_MANAGED_PACKAGE_SCRIPTS,
|
|
13
|
+
PACKAGE_ROOT_SOURCE_OVERRIDES,
|
|
14
|
+
USER_LEVEL_SKILL_ASSETS,
|
|
15
|
+
AGENTS_MARKER_START,
|
|
16
|
+
AGENTS_MARKER_END,
|
|
17
|
+
GITIGNORE_MARKER_START,
|
|
18
|
+
GITIGNORE_MARKER_END,
|
|
19
|
+
SHARED_VSCODE_SETTINGS_RELATIVE,
|
|
20
|
+
REPO_SCAN_IGNORED_FOLDERS_SETTING,
|
|
21
|
+
MANAGED_REPO_SCAN_IGNORED_FOLDERS,
|
|
22
|
+
REPO_SCAFFOLD_DIRECTORIES,
|
|
23
|
+
OMX_SCAFFOLD_DIRECTORIES,
|
|
24
|
+
OMX_SCAFFOLD_FILES,
|
|
6
25
|
toDestinationPath,
|
|
7
26
|
EXECUTABLE_RELATIVE_PATHS,
|
|
8
27
|
CRITICAL_GUARDRAIL_PATHS,
|
|
9
28
|
} = require('../context');
|
|
29
|
+
const { parse: parseJsonc, printParseErrorCode } = require('jsonc-parser');
|
|
30
|
+
const { run } = require('../core/runtime');
|
|
10
31
|
|
|
11
32
|
function ensureParentDir(repoRoot, filePath, dryRun) {
|
|
12
33
|
if (dryRun) return;
|
|
@@ -108,6 +129,589 @@ function managedForceConflictMessage(relativePath) {
|
|
|
108
129
|
);
|
|
109
130
|
}
|
|
110
131
|
|
|
132
|
+
function renderManagedFile(repoRoot, relativePath, content, options = {}) {
|
|
133
|
+
const destinationPath = path.join(repoRoot, relativePath);
|
|
134
|
+
const destinationExists = fs.existsSync(destinationPath);
|
|
135
|
+
const force = Boolean(options.force);
|
|
136
|
+
const dryRun = Boolean(options.dryRun);
|
|
137
|
+
|
|
138
|
+
if (destinationExists) {
|
|
139
|
+
const existingContent = fs.readFileSync(destinationPath, 'utf8');
|
|
140
|
+
if (existingContent === content) {
|
|
141
|
+
ensureExecutable(destinationPath, relativePath, dryRun);
|
|
142
|
+
return { status: 'unchanged', file: relativePath };
|
|
143
|
+
}
|
|
144
|
+
if (!force && !isCriticalGuardrailPath(relativePath)) {
|
|
145
|
+
throw new Error(managedForceConflictMessage(relativePath));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
ensureParentDir(repoRoot, destinationPath, dryRun);
|
|
150
|
+
if (!dryRun) {
|
|
151
|
+
fs.writeFileSync(destinationPath, content, 'utf8');
|
|
152
|
+
ensureExecutable(destinationPath, relativePath, dryRun);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (destinationExists && !force && isCriticalGuardrailPath(relativePath)) {
|
|
156
|
+
return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: relativePath };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { status: destinationExists ? 'overwritten' : 'created', file: relativePath };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function ensureGeneratedScriptShim(repoRoot, spec, options = {}) {
|
|
163
|
+
const content = spec.kind === 'python'
|
|
164
|
+
? renderPythonDispatchShim(spec.command)
|
|
165
|
+
: renderShellDispatchShim(spec.command);
|
|
166
|
+
return renderManagedFile(repoRoot, spec.relativePath, content, options);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function ensureHookShim(repoRoot, hookName, options = {}) {
|
|
170
|
+
return renderManagedFile(
|
|
171
|
+
repoRoot,
|
|
172
|
+
path.posix.join('.githooks', hookName),
|
|
173
|
+
renderShellDispatchShim(['hook', 'run', hookName]),
|
|
174
|
+
options,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function copyManagedSourceFile(repoRoot, sourcePath, destinationPath, destinationRelativePath, force, dryRun) {
|
|
179
|
+
const sourceContent = fs.readFileSync(sourcePath);
|
|
180
|
+
const destinationExists = fs.existsSync(destinationPath);
|
|
181
|
+
|
|
182
|
+
if (destinationExists) {
|
|
183
|
+
const existingContent = fs.readFileSync(destinationPath);
|
|
184
|
+
if (existingContent.equals(sourceContent)) {
|
|
185
|
+
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
186
|
+
return { status: 'unchanged', file: destinationRelativePath };
|
|
187
|
+
}
|
|
188
|
+
if (!force && !isCriticalGuardrailPath(destinationRelativePath)) {
|
|
189
|
+
throw new Error(managedForceConflictMessage(destinationRelativePath));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
ensureParentDir(repoRoot, destinationPath, dryRun);
|
|
194
|
+
if (!dryRun) {
|
|
195
|
+
fs.writeFileSync(destinationPath, sourceContent);
|
|
196
|
+
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (destinationExists && !force && isCriticalGuardrailPath(destinationRelativePath)) {
|
|
200
|
+
return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: destinationRelativePath };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { status: destinationExists ? 'overwritten' : 'created', file: destinationRelativePath };
|
|
204
|
+
}
|
|
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
|
+
|
|
238
|
+
function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {
|
|
239
|
+
const sourcePath = resolveTemplateSourcePath(repoRoot, relativeTemplatePath);
|
|
240
|
+
const destinationRelativePath = toDestinationPath(relativeTemplatePath);
|
|
241
|
+
const destinationPath = path.join(repoRoot, destinationRelativePath);
|
|
242
|
+
const sourceContent = fs.readFileSync(sourcePath);
|
|
243
|
+
|
|
244
|
+
if (fs.existsSync(destinationPath)) {
|
|
245
|
+
const existingContent = fs.readFileSync(destinationPath);
|
|
246
|
+
if (existingContent.equals(sourceContent)) {
|
|
247
|
+
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
248
|
+
return { status: 'unchanged', file: destinationRelativePath };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isCriticalGuardrailPath(destinationRelativePath)) {
|
|
252
|
+
if (!dryRun) {
|
|
253
|
+
fs.writeFileSync(destinationPath, sourceContent);
|
|
254
|
+
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
255
|
+
}
|
|
256
|
+
return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: destinationRelativePath };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { status: 'skipped-conflict', file: destinationRelativePath };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
ensureParentDir(repoRoot, destinationPath, dryRun);
|
|
263
|
+
if (!dryRun) {
|
|
264
|
+
fs.writeFileSync(destinationPath, sourceContent);
|
|
265
|
+
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { status: 'created', file: destinationRelativePath };
|
|
269
|
+
}
|
|
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
|
+
|
|
296
|
+
function lockFilePath(repoRoot) {
|
|
297
|
+
return path.join(repoRoot, LOCK_FILE_RELATIVE);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function ensureOmxScaffold(repoRoot, dryRun) {
|
|
301
|
+
const operations = [];
|
|
302
|
+
|
|
303
|
+
for (const relativeDir of REPO_SCAFFOLD_DIRECTORIES) {
|
|
304
|
+
const absoluteDir = path.join(repoRoot, relativeDir);
|
|
305
|
+
if (fs.existsSync(absoluteDir)) {
|
|
306
|
+
if (!fs.statSync(absoluteDir).isDirectory()) {
|
|
307
|
+
throw new Error(`Expected directory at ${relativeDir} but found a file.`);
|
|
308
|
+
}
|
|
309
|
+
operations.push({ status: 'unchanged', file: relativeDir });
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!dryRun) {
|
|
314
|
+
fs.mkdirSync(absoluteDir, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
operations.push({ status: 'created', file: relativeDir });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const relativeDir of OMX_SCAFFOLD_DIRECTORIES) {
|
|
320
|
+
const absoluteDir = path.join(repoRoot, relativeDir);
|
|
321
|
+
if (fs.existsSync(absoluteDir)) {
|
|
322
|
+
if (!fs.statSync(absoluteDir).isDirectory()) {
|
|
323
|
+
throw new Error(`Expected directory at ${relativeDir} but found a file.`);
|
|
324
|
+
}
|
|
325
|
+
operations.push({ status: 'unchanged', file: relativeDir });
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!dryRun) {
|
|
330
|
+
fs.mkdirSync(absoluteDir, { recursive: true });
|
|
331
|
+
}
|
|
332
|
+
operations.push({ status: 'created', file: relativeDir });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const [relativeFile, defaultContent] of OMX_SCAFFOLD_FILES.entries()) {
|
|
336
|
+
const absoluteFile = path.join(repoRoot, relativeFile);
|
|
337
|
+
if (fs.existsSync(absoluteFile)) {
|
|
338
|
+
if (!fs.statSync(absoluteFile).isFile()) {
|
|
339
|
+
throw new Error(`Expected file at ${relativeFile} but found a directory.`);
|
|
340
|
+
}
|
|
341
|
+
operations.push({ status: 'unchanged', file: relativeFile });
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!dryRun) {
|
|
346
|
+
fs.mkdirSync(path.dirname(absoluteFile), { recursive: true });
|
|
347
|
+
fs.writeFileSync(absoluteFile, defaultContent, 'utf8');
|
|
348
|
+
}
|
|
349
|
+
operations.push({ status: 'created', file: relativeFile });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return operations;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function ensureLockRegistry(repoRoot, dryRun) {
|
|
356
|
+
const absolutePath = lockFilePath(repoRoot);
|
|
357
|
+
if (fs.existsSync(absolutePath)) {
|
|
358
|
+
return { status: 'unchanged', file: LOCK_FILE_RELATIVE };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!dryRun) {
|
|
362
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
363
|
+
fs.writeFileSync(absolutePath, JSON.stringify({ locks: {} }, null, 2) + '\n', 'utf8');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { status: 'created', file: LOCK_FILE_RELATIVE };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function lockStateOrError(repoRoot) {
|
|
370
|
+
const lockPath = lockFilePath(repoRoot);
|
|
371
|
+
if (!fs.existsSync(lockPath)) {
|
|
372
|
+
return { ok: false, error: `${LOCK_FILE_RELATIVE} is missing` };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
377
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.locks !== 'object' || parsed.locks === null) {
|
|
378
|
+
return { ok: false, error: `${LOCK_FILE_RELATIVE} has invalid schema (expected { locks: {} })` };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
for (const [filePath, entry] of Object.entries(parsed.locks)) {
|
|
382
|
+
if (!entry || typeof entry !== 'object') {
|
|
383
|
+
parsed.locks[filePath] = { branch: '', claimed_at: '', allow_delete: false };
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (!Object.prototype.hasOwnProperty.call(entry, 'allow_delete')) {
|
|
387
|
+
entry.allow_delete = false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { ok: true, raw: parsed, locks: parsed.locks };
|
|
392
|
+
} catch (error) {
|
|
393
|
+
return { ok: false, error: `${LOCK_FILE_RELATIVE} is invalid JSON: ${error.message}` };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function writeLockState(repoRoot, payload, dryRun) {
|
|
398
|
+
if (dryRun) return;
|
|
399
|
+
const lockPath = lockFilePath(repoRoot);
|
|
400
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
401
|
+
fs.writeFileSync(lockPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function removeLegacyPackageScripts(repoRoot, dryRun) {
|
|
405
|
+
const packagePath = path.join(repoRoot, 'package.json');
|
|
406
|
+
if (!fs.existsSync(packagePath)) {
|
|
407
|
+
return { status: 'skipped', file: 'package.json', note: 'package.json not found' };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let pkg;
|
|
411
|
+
try {
|
|
412
|
+
pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
413
|
+
} catch (error) {
|
|
414
|
+
throw new Error(`Unable to parse package.json in target repo: ${error.message}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const existingScripts = pkg.scripts && typeof pkg.scripts === 'object'
|
|
418
|
+
? pkg.scripts
|
|
419
|
+
: {};
|
|
420
|
+
pkg.scripts = existingScripts;
|
|
421
|
+
let changed = false;
|
|
422
|
+
for (const [key, value] of Object.entries(LEGACY_MANAGED_PACKAGE_SCRIPTS)) {
|
|
423
|
+
if (existingScripts[key] === value) {
|
|
424
|
+
delete existingScripts[key];
|
|
425
|
+
changed = true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!changed) {
|
|
430
|
+
return { status: 'unchanged', file: 'package.json', note: 'no Guardex-managed agent:* scripts found' };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!dryRun) {
|
|
434
|
+
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { status: dryRun ? 'would-update' : 'updated', file: 'package.json', note: 'removed Guardex-managed agent:* scripts' };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function installUserLevelAsset(asset, options = {}) {
|
|
441
|
+
const dryRun = Boolean(options.dryRun);
|
|
442
|
+
const force = Boolean(options.force);
|
|
443
|
+
const destinationPath = path.join(GUARDEX_HOME_DIR, asset.destination);
|
|
444
|
+
const sourceContent = fs.readFileSync(asset.source, 'utf8');
|
|
445
|
+
const destinationExists = fs.existsSync(destinationPath);
|
|
446
|
+
|
|
447
|
+
if (destinationExists) {
|
|
448
|
+
const existingContent = fs.readFileSync(destinationPath, 'utf8');
|
|
449
|
+
if (existingContent === sourceContent) {
|
|
450
|
+
return { status: 'unchanged', file: asset.destination };
|
|
451
|
+
}
|
|
452
|
+
if (!force) {
|
|
453
|
+
return { status: 'skipped-conflict', file: asset.destination };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!dryRun) {
|
|
458
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
459
|
+
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
|
|
460
|
+
}
|
|
461
|
+
return { status: destinationExists ? (dryRun ? 'would-update' : 'updated') : 'created', file: asset.destination };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function removeLegacyManagedRepoFile(repoRoot, relativePath, options = {}) {
|
|
465
|
+
const dryRun = Boolean(options.dryRun);
|
|
466
|
+
const force = Boolean(options.force);
|
|
467
|
+
const absolutePath = path.join(repoRoot, relativePath);
|
|
468
|
+
if (!fs.existsSync(absolutePath)) {
|
|
469
|
+
return { status: 'unchanged', file: relativePath, note: 'not present' };
|
|
470
|
+
}
|
|
471
|
+
if (!fs.statSync(absolutePath).isFile()) {
|
|
472
|
+
return { status: 'skipped-conflict', file: relativePath, note: 'not a regular file' };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const skillAsset = USER_LEVEL_SKILL_ASSETS.find((asset) => asset.destination === relativePath);
|
|
476
|
+
if (skillAsset) {
|
|
477
|
+
const userLevelPath = path.join(GUARDEX_HOME_DIR, skillAsset.destination);
|
|
478
|
+
if (!fs.existsSync(userLevelPath)) {
|
|
479
|
+
return { status: 'skipped', file: relativePath, note: 'user-level replacement not installed' };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const templateRelative = skillAsset
|
|
484
|
+
? skillAsset.source.slice(TEMPLATE_ROOT.length + 1)
|
|
485
|
+
: relativePath.replace(/^\./, '');
|
|
486
|
+
const sourcePath = path.join(TEMPLATE_ROOT, templateRelative);
|
|
487
|
+
if (!fs.existsSync(sourcePath)) {
|
|
488
|
+
return { status: 'skipped', file: relativePath, note: 'template source missing' };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
|
492
|
+
const existingContent = fs.readFileSync(absolutePath, 'utf8');
|
|
493
|
+
if (existingContent !== sourceContent && !force) {
|
|
494
|
+
return { status: 'skipped-conflict', file: relativePath, note: 'local edits differ from managed template' };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!dryRun) {
|
|
498
|
+
fs.rmSync(absolutePath, { force: true });
|
|
499
|
+
}
|
|
500
|
+
return { status: dryRun ? 'would-remove' : 'removed', file: relativePath };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function ensureAgentsSnippet(repoRoot, dryRun) {
|
|
504
|
+
const agentsPath = path.join(repoRoot, 'AGENTS.md');
|
|
505
|
+
const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
|
|
506
|
+
const managedRegex = new RegExp(
|
|
507
|
+
`${AGENTS_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${AGENTS_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
|
|
508
|
+
'm',
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
if (!fs.existsSync(agentsPath)) {
|
|
512
|
+
if (!dryRun) {
|
|
513
|
+
fs.writeFileSync(agentsPath, `# AGENTS\n\n${snippet}\n`, 'utf8');
|
|
514
|
+
}
|
|
515
|
+
return { status: 'created', file: 'AGENTS.md' };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const existing = fs.readFileSync(agentsPath, 'utf8');
|
|
519
|
+
if (managedRegex.test(existing)) {
|
|
520
|
+
const next = existing.replace(managedRegex, snippet);
|
|
521
|
+
if (next === existing) {
|
|
522
|
+
return { status: 'unchanged', file: 'AGENTS.md' };
|
|
523
|
+
}
|
|
524
|
+
if (!dryRun) {
|
|
525
|
+
fs.writeFileSync(agentsPath, next, 'utf8');
|
|
526
|
+
}
|
|
527
|
+
return { status: 'updated', file: 'AGENTS.md', note: 'refreshed gitguardex-managed block' };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (existing.includes(AGENTS_MARKER_START)) {
|
|
531
|
+
return { status: 'unchanged', file: 'AGENTS.md', note: 'existing marker found without managed end marker' };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
535
|
+
if (!dryRun) {
|
|
536
|
+
fs.writeFileSync(agentsPath, `${existing}${separator}${snippet}\n`, 'utf8');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return { status: 'updated', file: 'AGENTS.md' };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function ensureManagedGitignore(repoRoot, dryRun) {
|
|
543
|
+
const gitignorePath = path.join(repoRoot, '.gitignore');
|
|
544
|
+
const managedBlock = [
|
|
545
|
+
GITIGNORE_MARKER_START,
|
|
546
|
+
...require('../context').MANAGED_GITIGNORE_PATHS,
|
|
547
|
+
GITIGNORE_MARKER_END,
|
|
548
|
+
].join('\n');
|
|
549
|
+
const managedRegex = new RegExp(
|
|
550
|
+
`${GITIGNORE_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${GITIGNORE_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
|
|
551
|
+
'm',
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
555
|
+
if (!dryRun) {
|
|
556
|
+
fs.writeFileSync(gitignorePath, `${managedBlock}\n`, 'utf8');
|
|
557
|
+
}
|
|
558
|
+
return { status: 'created', file: '.gitignore', note: 'added gitguardex-managed entries' };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
562
|
+
if (managedRegex.test(existing)) {
|
|
563
|
+
const next = existing.replace(managedRegex, managedBlock);
|
|
564
|
+
if (next === existing) {
|
|
565
|
+
return { status: 'unchanged', file: '.gitignore' };
|
|
566
|
+
}
|
|
567
|
+
if (!dryRun) {
|
|
568
|
+
fs.writeFileSync(gitignorePath, next, 'utf8');
|
|
569
|
+
}
|
|
570
|
+
return { status: 'updated', file: '.gitignore', note: 'refreshed gitguardex-managed entries' };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
574
|
+
if (!dryRun) {
|
|
575
|
+
fs.writeFileSync(gitignorePath, `${existing}${separator}${managedBlock}\n`, 'utf8');
|
|
576
|
+
}
|
|
577
|
+
return { status: 'updated', file: '.gitignore', note: 'appended gitguardex-managed entries' };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function parseJsonObjectLikeFile(source, relativePath) {
|
|
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}`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
590
|
+
throw new Error(`${relativePath} must contain a top-level object.`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return parsed;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function uniqueStringList(values) {
|
|
597
|
+
const seen = new Set();
|
|
598
|
+
const result = [];
|
|
599
|
+
|
|
600
|
+
for (const value of values) {
|
|
601
|
+
if (typeof value !== 'string' || seen.has(value)) {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
seen.add(value);
|
|
605
|
+
result.push(value);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function buildRepoVscodeSettings(existingSettings = {}) {
|
|
612
|
+
const nextSettings = { ...existingSettings };
|
|
613
|
+
const existingIgnoredFolders = Array.isArray(existingSettings[REPO_SCAN_IGNORED_FOLDERS_SETTING])
|
|
614
|
+
? existingSettings[REPO_SCAN_IGNORED_FOLDERS_SETTING]
|
|
615
|
+
: [];
|
|
616
|
+
|
|
617
|
+
nextSettings[REPO_SCAN_IGNORED_FOLDERS_SETTING] = uniqueStringList([
|
|
618
|
+
...existingIgnoredFolders,
|
|
619
|
+
...MANAGED_REPO_SCAN_IGNORED_FOLDERS,
|
|
620
|
+
]);
|
|
621
|
+
|
|
622
|
+
return nextSettings;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function ensureRepoVscodeSettings(repoRoot, dryRun) {
|
|
626
|
+
const settingsPath = path.join(repoRoot, SHARED_VSCODE_SETTINGS_RELATIVE);
|
|
627
|
+
const destinationExists = fs.existsSync(settingsPath);
|
|
628
|
+
const existingContent = destinationExists ? fs.readFileSync(settingsPath, 'utf8') : '';
|
|
629
|
+
const existingSettings = destinationExists
|
|
630
|
+
? parseJsonObjectLikeFile(existingContent, SHARED_VSCODE_SETTINGS_RELATIVE)
|
|
631
|
+
: {};
|
|
632
|
+
const nextContent = `${JSON.stringify(buildRepoVscodeSettings(existingSettings), null, 2)}\n`;
|
|
633
|
+
|
|
634
|
+
if (destinationExists && existingContent === nextContent) {
|
|
635
|
+
return { status: 'unchanged', file: SHARED_VSCODE_SETTINGS_RELATIVE };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
ensureParentDir(repoRoot, settingsPath, dryRun);
|
|
639
|
+
if (!dryRun) {
|
|
640
|
+
fs.writeFileSync(settingsPath, nextContent, 'utf8');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
status: destinationExists ? 'updated' : 'created',
|
|
645
|
+
file: SHARED_VSCODE_SETTINGS_RELATIVE,
|
|
646
|
+
note: 'shared VS Code repo scan ignores for Guardex worktrees',
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function normalizeWorkspacePath(relativePath) {
|
|
651
|
+
return String(relativePath || '.').replace(/\\/g, '/');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function buildParentWorkspaceView(repoRoot) {
|
|
655
|
+
const parentDir = path.dirname(repoRoot);
|
|
656
|
+
const workspaceFileName = `${path.basename(repoRoot)}-branches.code-workspace`;
|
|
657
|
+
const workspacePath = path.join(parentDir, workspaceFileName);
|
|
658
|
+
const repoRelativePath = normalizeWorkspacePath(path.relative(parentDir, repoRoot) || '.');
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
workspacePath,
|
|
662
|
+
payload: {
|
|
663
|
+
folders: [
|
|
664
|
+
{ path: repoRelativePath },
|
|
665
|
+
...AGENT_WORKTREE_RELATIVE_DIRS.map((relativeDir) => ({
|
|
666
|
+
path: normalizeWorkspacePath(
|
|
667
|
+
path.join(repoRelativePath === '.' ? '' : repoRelativePath, relativeDir),
|
|
668
|
+
),
|
|
669
|
+
})),
|
|
670
|
+
],
|
|
671
|
+
settings: {
|
|
672
|
+
'scm.alwaysShowRepositories': true,
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function ensureParentWorkspaceView(repoRoot, dryRun) {
|
|
679
|
+
const { workspacePath, payload } = buildParentWorkspaceView(repoRoot);
|
|
680
|
+
const operationFile = path.relative(repoRoot, workspacePath) || path.basename(workspacePath);
|
|
681
|
+
const nextContent = `${JSON.stringify(payload, null, 2)}\n`;
|
|
682
|
+
const note = 'parent VS Code workspace view';
|
|
683
|
+
|
|
684
|
+
if (!fs.existsSync(workspacePath)) {
|
|
685
|
+
if (!dryRun) {
|
|
686
|
+
fs.writeFileSync(workspacePath, nextContent, 'utf8');
|
|
687
|
+
}
|
|
688
|
+
return { status: dryRun ? 'would-create' : 'created', file: operationFile, note };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const currentContent = fs.readFileSync(workspacePath, 'utf8');
|
|
692
|
+
if (currentContent === nextContent) {
|
|
693
|
+
return { status: 'unchanged', file: operationFile, note };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (!dryRun) {
|
|
697
|
+
fs.writeFileSync(workspacePath, nextContent, 'utf8');
|
|
698
|
+
}
|
|
699
|
+
return { status: dryRun ? 'would-update' : 'updated', file: operationFile, note };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function configureHooks(repoRoot, dryRun) {
|
|
703
|
+
if (dryRun) {
|
|
704
|
+
return { status: 'would-set', key: 'core.hooksPath', value: '.githooks' };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const result = run('git', ['-C', repoRoot, 'config', 'core.hooksPath', '.githooks']);
|
|
708
|
+
if (result.status !== 0) {
|
|
709
|
+
throw new Error(`Failed to set git hooksPath: ${(result.stderr || '').trim()}`);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return { status: 'set', key: 'core.hooksPath', value: '.githooks' };
|
|
713
|
+
}
|
|
714
|
+
|
|
111
715
|
function printOperations(title, payload, dryRun = false) {
|
|
112
716
|
console.log(`[${TOOL_NAME}] ${title}: ${payload.repoRoot}`);
|
|
113
717
|
for (const operation of payload.operations) {
|
|
@@ -135,6 +739,8 @@ function printStandaloneOperations(title, rootLabel, operations, dryRun = false)
|
|
|
135
739
|
}
|
|
136
740
|
|
|
137
741
|
module.exports = {
|
|
742
|
+
HOOK_NAMES,
|
|
743
|
+
LOCK_FILE_RELATIVE,
|
|
138
744
|
toDestinationPath,
|
|
139
745
|
ensureParentDir,
|
|
140
746
|
ensureExecutable,
|
|
@@ -143,6 +749,27 @@ module.exports = {
|
|
|
143
749
|
renderShellDispatchShim,
|
|
144
750
|
renderPythonDispatchShim,
|
|
145
751
|
managedForceConflictMessage,
|
|
752
|
+
renderManagedFile,
|
|
753
|
+
ensureGeneratedScriptShim,
|
|
754
|
+
ensureHookShim,
|
|
755
|
+
copyTemplateFile,
|
|
756
|
+
ensureTemplateFilePresent,
|
|
757
|
+
materializePackageRepoTemplateFiles,
|
|
758
|
+
ensureOmxScaffold,
|
|
759
|
+
ensureLockRegistry,
|
|
760
|
+
lockStateOrError,
|
|
761
|
+
writeLockState,
|
|
762
|
+
removeLegacyPackageScripts,
|
|
763
|
+
installUserLevelAsset,
|
|
764
|
+
removeLegacyManagedRepoFile,
|
|
765
|
+
ensureAgentsSnippet,
|
|
766
|
+
ensureManagedGitignore,
|
|
767
|
+
parseJsonObjectLikeFile,
|
|
768
|
+
buildRepoVscodeSettings,
|
|
769
|
+
ensureRepoVscodeSettings,
|
|
770
|
+
buildParentWorkspaceView,
|
|
771
|
+
ensureParentWorkspaceView,
|
|
772
|
+
configureHooks,
|
|
146
773
|
printOperations,
|
|
147
774
|
printStandaloneOperations,
|
|
148
775
|
};
|