legion-cc 0.3.0 → 0.6.0
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/VERSION +1 -1
- package/bin/install.js +126 -18
- package/bin/legion-tools.cjs +304 -7
- package/bin/lib/config.cjs +18 -3
- package/bin/lib/core.cjs +30 -17
- package/bin/lib/init.cjs +12 -1
- package/bin/lib/session.cjs +1 -1
- package/bin/lib/settings.cjs +271 -0
- package/bin/lib/state.cjs +70 -2
- package/commands/legion/devops/cycle.md +1 -0
- package/commands/legion/devops/init.md +1 -0
- package/commands/legion/devops/quick.md +2 -0
- package/commands/legion/resume.md +2 -0
- package/commands/legion/settings.md +62 -0
- package/commands/legion/status.md +1 -0
- package/commands/legion/update.md +41 -0
- package/hooks/legion-context-monitor.js +20 -15
- package/hooks/legion-statusline.js +33 -34
- package/package.json +1 -1
- package/references/agent-routing.md +35 -0
- package/references/devops/agent-map.md +23 -0
- package/references/devops/sub-agents/architect-sub-agents.md +63 -0
- package/references/devops/sub-agents/collapse-rules.md +80 -0
- package/references/devops/sub-agents/execute-sub-agents.md +74 -0
- package/references/devops/sub-agents/plan-sub-agents.md +36 -0
- package/references/devops/sub-agents/review-sub-agents.md +42 -0
- package/templates/config.json +9 -1
- package/workflows/core/context-load.md +12 -2
- package/workflows/devops/architect.md +6 -0
- package/workflows/devops/cycle.md +55 -1
- package/workflows/devops/execute.md +6 -0
- package/workflows/devops/init.md +8 -0
- package/workflows/devops/quick.md +11 -0
- package/workflows/resume.md +31 -0
- package/workflows/settings.md +82 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.6.0
|
package/bin/install.js
CHANGED
|
@@ -111,9 +111,9 @@ ${c(yellow, 'Environment:')}
|
|
|
111
111
|
|
|
112
112
|
// ─── Determine paths ─────────────────────────────────────────────────────────
|
|
113
113
|
|
|
114
|
-
const configDir =
|
|
115
|
-
|| flagConfigDir
|
|
116
|
-
|
|
114
|
+
const configDir = path.resolve(
|
|
115
|
+
process.env.CLAUDE_CONFIG_DIR || flagConfigDir || path.join(os.homedir(), '.claude')
|
|
116
|
+
);
|
|
117
117
|
|
|
118
118
|
// ─── Banner ──────────────────────────────────────────────────────────────────
|
|
119
119
|
|
|
@@ -215,6 +215,20 @@ function walkDir(dir, fileList) {
|
|
|
215
215
|
return fileList;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Check if a path is safely contained within a parent directory.
|
|
220
|
+
* Uses path.resolve() + separator-aware startsWith to prevent prefix collisions.
|
|
221
|
+
* @param {string} child - Path to check
|
|
222
|
+
* @param {string} parent - Expected parent directory
|
|
223
|
+
* @returns {boolean}
|
|
224
|
+
*/
|
|
225
|
+
function isInsideDir(child, parent) {
|
|
226
|
+
const resolvedChild = path.resolve(child);
|
|
227
|
+
const resolvedParent = path.resolve(parent);
|
|
228
|
+
return resolvedChild === resolvedParent
|
|
229
|
+
|| resolvedChild.startsWith(resolvedParent + path.sep);
|
|
230
|
+
}
|
|
231
|
+
|
|
218
232
|
/**
|
|
219
233
|
* Ensure a directory exists (create recursively if needed).
|
|
220
234
|
*/
|
|
@@ -476,7 +490,7 @@ function doInstall() {
|
|
|
476
490
|
logStep('Generating file manifest...');
|
|
477
491
|
generateManifest();
|
|
478
492
|
|
|
479
|
-
// ── Step
|
|
493
|
+
// ── Step 6: Print summary ───────────────────────────────────────────────
|
|
480
494
|
|
|
481
495
|
printSummary();
|
|
482
496
|
}
|
|
@@ -580,25 +594,47 @@ function cleanupStaleFiles() {
|
|
|
580
594
|
|
|
581
595
|
if (!oldManifest || !oldManifest.files) return;
|
|
582
596
|
|
|
583
|
-
// Build set of files that exist in the NEW install
|
|
597
|
+
// Build set of files that exist in the NEW install by walking SOURCE dirs
|
|
598
|
+
// and mapping them to their destination relative paths.
|
|
599
|
+
// This avoids the bug of scanning DESTINATION (which already has new + old files
|
|
600
|
+
// after cpSync), making all files appear "current" and never detecting stale ones.
|
|
584
601
|
const newFiles = new Set();
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
path.join(
|
|
602
|
+
|
|
603
|
+
const sourceMappings = [
|
|
604
|
+
{ src: path.join(srcDir, 'commands', 'legion'), destPrefix: path.join('commands', 'legion') },
|
|
605
|
+
{ src: path.join(srcDir, 'workflows'), destPrefix: path.join('legion', 'workflows') },
|
|
606
|
+
{ src: path.join(srcDir, 'templates'), destPrefix: path.join('legion', 'templates') },
|
|
607
|
+
{ src: path.join(srcDir, 'references'), destPrefix: path.join('legion', 'references') },
|
|
608
|
+
{ src: path.join(srcDir, 'bin', 'lib'), destPrefix: path.join('legion', 'bin', 'lib') },
|
|
588
609
|
];
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
610
|
+
|
|
611
|
+
for (const { src, destPrefix } of sourceMappings) {
|
|
612
|
+
for (const filePath of walkDir(src)) {
|
|
613
|
+
const relInSrc = path.relative(src, filePath);
|
|
614
|
+
newFiles.add(path.join(destPrefix, relInSrc));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Individual source files with explicit destination paths
|
|
619
|
+
const singleFiles = [
|
|
620
|
+
{ src: path.join(srcDir, 'bin', 'legion-tools.cjs'), dest: path.join('legion', 'bin', 'legion-tools.cjs') },
|
|
621
|
+
{ src: path.join(srcDir, 'VERSION'), dest: path.join('legion', 'VERSION') },
|
|
622
|
+
{ src: path.join(srcDir, 'hooks', 'legion-context-monitor.js'), dest: path.join('hooks', 'legion-context-monitor.js') },
|
|
623
|
+
{ src: path.join(srcDir, 'hooks', 'legion-statusline.js'), dest: path.join('hooks', 'legion-statusline.js') },
|
|
592
624
|
];
|
|
593
625
|
|
|
594
|
-
for (const
|
|
595
|
-
|
|
596
|
-
newFiles.add(
|
|
626
|
+
for (const { src, dest } of singleFiles) {
|
|
627
|
+
if (fs.existsSync(src)) {
|
|
628
|
+
newFiles.add(dest);
|
|
597
629
|
}
|
|
598
630
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
631
|
+
|
|
632
|
+
// Agent files (these exist in source agents/ dir)
|
|
633
|
+
const agentsSrcDir = path.join(srcDir, 'agents');
|
|
634
|
+
if (fs.existsSync(agentsSrcDir)) {
|
|
635
|
+
const agentFiles = fs.readdirSync(agentsSrcDir).filter(f => f.endsWith('.md'));
|
|
636
|
+
for (const agentFile of agentFiles) {
|
|
637
|
+
newFiles.add(path.join('agents', agentFile));
|
|
602
638
|
}
|
|
603
639
|
}
|
|
604
640
|
|
|
@@ -606,7 +642,11 @@ function cleanupStaleFiles() {
|
|
|
606
642
|
let removedCount = 0;
|
|
607
643
|
for (const rel of Object.keys(oldManifest.files)) {
|
|
608
644
|
if (!newFiles.has(rel)) {
|
|
609
|
-
const absPath = path.
|
|
645
|
+
const absPath = path.resolve(configDir, rel);
|
|
646
|
+
if (!isInsideDir(absPath, configDir)) {
|
|
647
|
+
logWarn(`Skipping suspicious manifest entry: ${rel}`);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
610
650
|
if (fs.existsSync(absPath)) {
|
|
611
651
|
if (flagDryRun) {
|
|
612
652
|
console.log(` ${c(dim, '[dry-run]')} would remove stale: ${rel}`);
|
|
@@ -623,6 +663,74 @@ function cleanupStaleFiles() {
|
|
|
623
663
|
if (removedCount > 0) {
|
|
624
664
|
logOk(`Cleaned up ${removedCount} stale file(s) from previous install`);
|
|
625
665
|
}
|
|
666
|
+
|
|
667
|
+
// Clean up empty directories left behind after stale file removal
|
|
668
|
+
cleanEmptyDirs();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ─── Cleanup empty directories ───────────────────────────────────────────────
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Bottom-up removal of empty directories within the configDir subtree.
|
|
675
|
+
* Called after stale file cleanup to remove directories that are now empty.
|
|
676
|
+
* Guard: only deletes directories within configDir.
|
|
677
|
+
*/
|
|
678
|
+
function cleanEmptyDirs() {
|
|
679
|
+
const legionDir = path.join(configDir, 'legion');
|
|
680
|
+
const commandsLegionDir = path.join(configDir, 'commands', 'legion');
|
|
681
|
+
|
|
682
|
+
const dirsToCheck = [legionDir, commandsLegionDir];
|
|
683
|
+
|
|
684
|
+
for (const rootDir of dirsToCheck) {
|
|
685
|
+
if (!fs.existsSync(rootDir)) continue;
|
|
686
|
+
_removeEmptyDirsRecursive(rootDir);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Recursively remove empty directories bottom-up.
|
|
692
|
+
* Returns true if the directory itself was removed (i.e. it became empty).
|
|
693
|
+
*
|
|
694
|
+
* @param {string} dir - Absolute path to directory
|
|
695
|
+
* @returns {boolean}
|
|
696
|
+
*/
|
|
697
|
+
function _removeEmptyDirsRecursive(dir) {
|
|
698
|
+
// Guard: never delete outside configDir
|
|
699
|
+
if (!isInsideDir(dir, configDir)) return false;
|
|
700
|
+
|
|
701
|
+
let entries;
|
|
702
|
+
try {
|
|
703
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
704
|
+
} catch (_e) {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Recurse into subdirectories first (bottom-up)
|
|
709
|
+
for (const entry of entries) {
|
|
710
|
+
if (entry.isDirectory()) {
|
|
711
|
+
_removeEmptyDirsRecursive(path.join(dir, entry.name));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Re-read after potential subdirectory removals
|
|
716
|
+
try {
|
|
717
|
+
entries = fs.readdirSync(dir);
|
|
718
|
+
} catch (_e) {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (entries.length === 0) {
|
|
723
|
+
if (flagDryRun) {
|
|
724
|
+
console.log(` ${c(dim, '[dry-run]')} would remove empty dir: ${path.relative(configDir, dir)}/`);
|
|
725
|
+
} else {
|
|
726
|
+
try {
|
|
727
|
+
fs.rmdirSync(dir);
|
|
728
|
+
return true;
|
|
729
|
+
} catch (_e) { /* silent */ }
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return false;
|
|
626
734
|
}
|
|
627
735
|
|
|
628
736
|
// ─── Generate manifest ──────────────────────────────────────────────────────
|
package/bin/legion-tools.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
5
6
|
|
|
6
7
|
// ─── Library Imports ─────────────────────────────────────────────────────────
|
|
7
8
|
|
|
@@ -11,6 +12,7 @@ const state = require('./lib/state.cjs');
|
|
|
11
12
|
const init = require('./lib/init.cjs');
|
|
12
13
|
const session = require('./lib/session.cjs');
|
|
13
14
|
const domain = require('./lib/domain.cjs');
|
|
15
|
+
const settings = require('./lib/settings.cjs');
|
|
14
16
|
|
|
15
17
|
// ─── CLI Helpers ─────────────────────────────────────────────────────────────
|
|
16
18
|
|
|
@@ -28,8 +30,12 @@ function usage() {
|
|
|
28
30
|
' task-record <type> <desc> <status> Add a task record to STATE.md',
|
|
29
31
|
' context-load [path] Load context from .legion/codebase/ and .legion/planning/',
|
|
30
32
|
' domain list|info [name] Domain registry operations',
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
' settings list|get|set|reset|migrate View and manage project settings',
|
|
34
|
+
' session-record <summary> Create a session record in .legion/planning/sessions/',
|
|
35
|
+
' artifacts scan|latest Find untracked artifacts in planning dir',
|
|
36
|
+
' clean [--dry-run] Remove orphaned files not in manifest',
|
|
37
|
+
' update Check for updates and print install command',
|
|
38
|
+
' update-check Check for newer version on npm',
|
|
33
39
|
'',
|
|
34
40
|
'Examples:',
|
|
35
41
|
' legion-tools init devops',
|
|
@@ -61,7 +67,7 @@ function out(data) {
|
|
|
61
67
|
*/
|
|
62
68
|
function die(msg, code) {
|
|
63
69
|
console.error(`${core.UI.cross} Error: ${msg}`);
|
|
64
|
-
process.exit(code
|
|
70
|
+
process.exit(code ?? 1);
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
/**
|
|
@@ -123,7 +129,7 @@ function cmdState(args) {
|
|
|
123
129
|
case 'update': {
|
|
124
130
|
const field = args[1];
|
|
125
131
|
const value = args.slice(2).join(' ');
|
|
126
|
-
if (!field ||
|
|
132
|
+
if (!field || args.length < 3) {
|
|
127
133
|
die('Usage: legion-tools state update <field> <value>');
|
|
128
134
|
}
|
|
129
135
|
state.updateField(planningDir, field, value);
|
|
@@ -151,6 +157,10 @@ function cmdState(args) {
|
|
|
151
157
|
target = target[part];
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
if (target === undefined) {
|
|
161
|
+
die(`Field not found: ${field}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
154
164
|
if (typeof target === 'object') {
|
|
155
165
|
out(target);
|
|
156
166
|
} else {
|
|
@@ -332,7 +342,7 @@ function cmdContextLoad(args) {
|
|
|
332
342
|
*/
|
|
333
343
|
function _listFiles(dir, base, depth, results) {
|
|
334
344
|
if (depth > 3 || results.length >= 100) return results;
|
|
335
|
-
const entries =
|
|
345
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
336
346
|
for (const entry of entries) {
|
|
337
347
|
if (results.length >= 100) break;
|
|
338
348
|
const full = path.join(dir, entry.name);
|
|
@@ -382,7 +392,7 @@ function cmdDomain(args) {
|
|
|
382
392
|
core.banner(info.name, 'info');
|
|
383
393
|
console.log(` Source: ${info.source}`);
|
|
384
394
|
console.log(` Version: ${info.version}`);
|
|
385
|
-
console.log(` Pipeline: ${info.pipeline.join(' -> ')}`);
|
|
395
|
+
console.log(` Pipeline: ${Array.isArray(info.pipeline) ? info.pipeline.join(' -> ') : 'not configured'}`);
|
|
386
396
|
console.log('');
|
|
387
397
|
console.log(' Agents:');
|
|
388
398
|
for (const [role, agent] of Object.entries(info.agents)) {
|
|
@@ -403,6 +413,197 @@ function cmdDomain(args) {
|
|
|
403
413
|
}
|
|
404
414
|
}
|
|
405
415
|
|
|
416
|
+
// ─── Command: artifacts ───────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
function cmdArtifacts(args) {
|
|
419
|
+
const subcommand = args[0];
|
|
420
|
+
|
|
421
|
+
if (!subcommand) {
|
|
422
|
+
die('Usage: legion-tools artifacts scan|latest');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const planningDir = requirePlanningDir();
|
|
426
|
+
|
|
427
|
+
switch (subcommand) {
|
|
428
|
+
case 'scan': {
|
|
429
|
+
const result = state.findUntrackedArtifacts(planningDir);
|
|
430
|
+
|
|
431
|
+
if (result.untracked.length === 0) {
|
|
432
|
+
console.log(` ${core.UI.check} No untracked artifacts found (max tracked: #${result.maxTracked}).`);
|
|
433
|
+
console.log('');
|
|
434
|
+
out({ untracked: [], maxTracked: result.maxTracked });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
console.log(` ${core.UI.diamond} Found ${result.untracked.length} untracked artifact(s) (max tracked: #${result.maxTracked}):`);
|
|
439
|
+
console.log('');
|
|
440
|
+
for (const art of result.untracked) {
|
|
441
|
+
console.log(` #${String(art.num).padStart(3, '0')} [${art.type}] ${path.basename(art.file)}`);
|
|
442
|
+
}
|
|
443
|
+
console.log('');
|
|
444
|
+
|
|
445
|
+
out(result);
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
case 'latest': {
|
|
450
|
+
const result = state.findUntrackedArtifacts(planningDir);
|
|
451
|
+
|
|
452
|
+
if (result.untracked.length === 0) {
|
|
453
|
+
console.log(` ${core.UI.check} No untracked artifacts.`);
|
|
454
|
+
console.log('');
|
|
455
|
+
out(null);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const latest = result.untracked[result.untracked.length - 1];
|
|
460
|
+
console.log(` ${core.UI.diamond} Latest untracked: #${String(latest.num).padStart(3, '0')} [${latest.type}]`);
|
|
461
|
+
console.log(` ${latest.file}`);
|
|
462
|
+
console.log('');
|
|
463
|
+
|
|
464
|
+
out(latest);
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
default:
|
|
469
|
+
die(`Unknown artifacts subcommand: ${subcommand}. Use scan|latest`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ─── Command: clean ───────────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
function cmdClean(args) {
|
|
476
|
+
const fs = require('fs');
|
|
477
|
+
const os = require('os');
|
|
478
|
+
|
|
479
|
+
const dryRun = args.includes('--dry-run');
|
|
480
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
481
|
+
const manifestPath = path.join(configDir, 'legion-file-manifest.json');
|
|
482
|
+
|
|
483
|
+
if (!fs.existsSync(manifestPath)) {
|
|
484
|
+
die('No legion-file-manifest.json found. Install Legion first with: npx legion-cc');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const manifest = core.readJsonSafe(manifestPath, null);
|
|
488
|
+
if (!manifest || !manifest.files) {
|
|
489
|
+
die('Invalid manifest file.');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const manifestFiles = new Set(Object.keys(manifest.files));
|
|
493
|
+
|
|
494
|
+
// Walk installed dirs and find files not in manifest
|
|
495
|
+
const installedDirs = [
|
|
496
|
+
path.join(configDir, 'legion'),
|
|
497
|
+
path.join(configDir, 'commands', 'legion'),
|
|
498
|
+
];
|
|
499
|
+
|
|
500
|
+
const installedSingleFiles = [
|
|
501
|
+
path.join(configDir, 'hooks', 'legion-context-monitor.js'),
|
|
502
|
+
path.join(configDir, 'hooks', 'legion-statusline.js'),
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
const allInstalled = new Set();
|
|
506
|
+
|
|
507
|
+
for (const dir of installedDirs) {
|
|
508
|
+
if (!fs.existsSync(dir)) continue;
|
|
509
|
+
_walkDirForClean(dir, configDir, allInstalled);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (const f of installedSingleFiles) {
|
|
513
|
+
if (fs.existsSync(f)) {
|
|
514
|
+
allInstalled.add(path.relative(configDir, f));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Find orphans: files on disk but not in manifest
|
|
519
|
+
const orphans = [];
|
|
520
|
+
for (const rel of allInstalled) {
|
|
521
|
+
if (!manifestFiles.has(rel)) {
|
|
522
|
+
orphans.push(rel);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (orphans.length === 0) {
|
|
527
|
+
console.log(` ${core.UI.check} No orphaned files found. Installation is clean.`);
|
|
528
|
+
console.log('');
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
console.log(` Found ${orphans.length} orphaned file(s):`);
|
|
533
|
+
console.log('');
|
|
534
|
+
|
|
535
|
+
let removedCount = 0;
|
|
536
|
+
for (const rel of orphans.sort()) {
|
|
537
|
+
const absPath = path.join(configDir, rel);
|
|
538
|
+
if (dryRun) {
|
|
539
|
+
console.log(` [dry-run] would remove: ${rel}`);
|
|
540
|
+
} else {
|
|
541
|
+
try {
|
|
542
|
+
if (rel.includes('..') || rel.includes('\0')) continue;
|
|
543
|
+
fs.unlinkSync(absPath);
|
|
544
|
+
console.log(` ${core.UI.check} removed: ${rel}`);
|
|
545
|
+
removedCount++;
|
|
546
|
+
} catch (err) {
|
|
547
|
+
console.log(` ${core.UI.cross} failed: ${rel} (${err.message})`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
console.log('');
|
|
553
|
+
if (dryRun) {
|
|
554
|
+
console.log(` [dry-run] Would remove ${orphans.length} file(s).`);
|
|
555
|
+
} else {
|
|
556
|
+
console.log(` ${core.UI.check} Removed ${removedCount} orphaned file(s).`);
|
|
557
|
+
}
|
|
558
|
+
console.log('');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Walk a directory recursively and add relative paths to the set.
|
|
563
|
+
*/
|
|
564
|
+
function _walkDirForClean(dir, baseDir, resultSet) {
|
|
565
|
+
const fs = require('fs');
|
|
566
|
+
if (!fs.existsSync(dir)) return;
|
|
567
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
568
|
+
for (const entry of entries) {
|
|
569
|
+
const full = path.join(dir, entry.name);
|
|
570
|
+
if (entry.isDirectory()) {
|
|
571
|
+
_walkDirForClean(full, baseDir, resultSet);
|
|
572
|
+
} else if (entry.isFile()) {
|
|
573
|
+
resultSet.add(path.relative(baseDir, full));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ─── Command: update ──────────────────────────────────────────────────────
|
|
579
|
+
|
|
580
|
+
function cmdUpdate() {
|
|
581
|
+
const vc = require('./lib/version-check.cjs');
|
|
582
|
+
const current = vc.getInstalledVersion();
|
|
583
|
+
|
|
584
|
+
console.log('');
|
|
585
|
+
console.log(` Installed: v${current}`);
|
|
586
|
+
console.log(' Checking npm registry...');
|
|
587
|
+
|
|
588
|
+
const result = vc.checkForUpdate();
|
|
589
|
+
|
|
590
|
+
if (!result) {
|
|
591
|
+
console.log(` ${core.UI.warn} Could not reach npm registry.`);
|
|
592
|
+
console.log('');
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (result.available) {
|
|
597
|
+
console.log(` ${core.UI.diamond} Update available: v${result.current} \u2192 v${result.latest}`);
|
|
598
|
+
console.log('');
|
|
599
|
+
console.log(' To update, run:');
|
|
600
|
+
console.log(` npx legion-cc@latest`);
|
|
601
|
+
} else {
|
|
602
|
+
console.log(` ${core.UI.check} Already on latest version (v${result.current})`);
|
|
603
|
+
}
|
|
604
|
+
console.log('');
|
|
605
|
+
}
|
|
606
|
+
|
|
406
607
|
// ─── Command: update-check ───────────────────────────────────────────────
|
|
407
608
|
|
|
408
609
|
function cmdUpdateCheck() {
|
|
@@ -431,6 +632,86 @@ function cmdUpdateCheck() {
|
|
|
431
632
|
out(result);
|
|
432
633
|
}
|
|
433
634
|
|
|
635
|
+
// ─── Command: settings ──────────────────────────────────────────────────────
|
|
636
|
+
|
|
637
|
+
function cmdSettings(args) {
|
|
638
|
+
const subcommand = args[0] || 'list';
|
|
639
|
+
const planningDir = requirePlanningDir();
|
|
640
|
+
let cfg = config.loadConfig(planningDir);
|
|
641
|
+
|
|
642
|
+
if (!cfg) {
|
|
643
|
+
die('No config.json found. Run "legion-tools init <domain>" first.');
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
switch (subcommand) {
|
|
647
|
+
case 'list': {
|
|
648
|
+
core.banner(cfg.domain || 'legion', 'settings');
|
|
649
|
+
console.log(settings.formatSettingsTree(cfg));
|
|
650
|
+
out(cfg);
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
case 'get': {
|
|
655
|
+
const key = args[1];
|
|
656
|
+
if (!key) {
|
|
657
|
+
die('Usage: legion-tools settings get <key>');
|
|
658
|
+
}
|
|
659
|
+
const value = settings.getSetting(cfg, key);
|
|
660
|
+
if (value === undefined) {
|
|
661
|
+
die(`Setting not found: ${key}`);
|
|
662
|
+
}
|
|
663
|
+
if (typeof value === 'object') {
|
|
664
|
+
out(value);
|
|
665
|
+
} else {
|
|
666
|
+
console.log(value);
|
|
667
|
+
}
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
case 'set': {
|
|
672
|
+
const key = args[1];
|
|
673
|
+
const value = args.slice(2).join(' ');
|
|
674
|
+
if (!key || args.length < 3) {
|
|
675
|
+
die('Usage: legion-tools settings set <key> <value>');
|
|
676
|
+
}
|
|
677
|
+
const result = settings.setSetting(cfg, key, value);
|
|
678
|
+
if (!result.success) {
|
|
679
|
+
die(result.error);
|
|
680
|
+
}
|
|
681
|
+
config.saveConfig(planningDir, cfg);
|
|
682
|
+
console.log(`${core.UI.check} ${key} = ${settings.getSetting(cfg, key)}`);
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
case 'reset': {
|
|
687
|
+
const key = args[1];
|
|
688
|
+
cfg = settings.resetSetting(cfg, key);
|
|
689
|
+
config.saveConfig(planningDir, cfg);
|
|
690
|
+
if (key) {
|
|
691
|
+
console.log(`${core.UI.check} Reset ${key} to default`);
|
|
692
|
+
} else {
|
|
693
|
+
console.log(`${core.UI.check} Reset all settings to ${cfg.domain} defaults`);
|
|
694
|
+
}
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
case 'migrate': {
|
|
699
|
+
const result = settings.migrateConfig(cfg);
|
|
700
|
+
if (result.migrated) {
|
|
701
|
+
config.saveConfig(planningDir, result.config);
|
|
702
|
+
console.log(`${core.UI.check} Config migrated to v${result.config.version}`);
|
|
703
|
+
console.log(settings.formatSettingsTree(result.config));
|
|
704
|
+
} else {
|
|
705
|
+
console.log(`${core.UI.check} Config is already up to date (v${cfg.version})`);
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
default:
|
|
711
|
+
die(`Unknown settings subcommand: ${subcommand}. Use list|get|set|reset|migrate`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
434
715
|
// ─── Main Router ─────────────────────────────────────────────────────────────
|
|
435
716
|
|
|
436
717
|
function main() {
|
|
@@ -471,6 +752,18 @@ function main() {
|
|
|
471
752
|
case 'domain':
|
|
472
753
|
cmdDomain(commandArgs);
|
|
473
754
|
break;
|
|
755
|
+
case 'settings':
|
|
756
|
+
cmdSettings(commandArgs);
|
|
757
|
+
break;
|
|
758
|
+
case 'artifacts':
|
|
759
|
+
cmdArtifacts(commandArgs);
|
|
760
|
+
break;
|
|
761
|
+
case 'clean':
|
|
762
|
+
cmdClean(commandArgs);
|
|
763
|
+
break;
|
|
764
|
+
case 'update':
|
|
765
|
+
cmdUpdate();
|
|
766
|
+
break;
|
|
474
767
|
case 'update-check':
|
|
475
768
|
cmdUpdateCheck();
|
|
476
769
|
break;
|
|
@@ -479,4 +772,8 @@ function main() {
|
|
|
479
772
|
}
|
|
480
773
|
}
|
|
481
774
|
|
|
482
|
-
|
|
775
|
+
try {
|
|
776
|
+
main();
|
|
777
|
+
} catch (err) {
|
|
778
|
+
die(err.message || String(err));
|
|
779
|
+
}
|
package/bin/lib/config.cjs
CHANGED
|
@@ -7,7 +7,7 @@ const { readJsonSafe, writeJsonSafe } = require('./core.cjs');
|
|
|
7
7
|
|
|
8
8
|
const DOMAIN_DEFAULTS = {
|
|
9
9
|
devops: {
|
|
10
|
-
version: '0.
|
|
10
|
+
version: '0.2.0',
|
|
11
11
|
domain: 'devops',
|
|
12
12
|
agents: {
|
|
13
13
|
architect: 'devops-architect',
|
|
@@ -30,9 +30,14 @@ const DOMAIN_DEFAULTS = {
|
|
|
30
30
|
after_execute: false,
|
|
31
31
|
after_review: false,
|
|
32
32
|
},
|
|
33
|
+
parallelism: { minAgents: 5, maxAgents: 10 },
|
|
34
|
+
mcp: {
|
|
35
|
+
enforce: true,
|
|
36
|
+
discovery: true,
|
|
37
|
+
},
|
|
33
38
|
},
|
|
34
39
|
backend: {
|
|
35
|
-
version: '0.
|
|
40
|
+
version: '0.2.0',
|
|
36
41
|
domain: 'backend',
|
|
37
42
|
agents: {
|
|
38
43
|
architect: 'backend-architect',
|
|
@@ -55,9 +60,14 @@ const DOMAIN_DEFAULTS = {
|
|
|
55
60
|
after_execute: false,
|
|
56
61
|
after_review: false,
|
|
57
62
|
},
|
|
63
|
+
parallelism: { minAgents: 5, maxAgents: 10 },
|
|
64
|
+
mcp: {
|
|
65
|
+
enforce: true,
|
|
66
|
+
discovery: true,
|
|
67
|
+
},
|
|
58
68
|
},
|
|
59
69
|
frontend: {
|
|
60
|
-
version: '0.
|
|
70
|
+
version: '0.2.0',
|
|
61
71
|
domain: 'frontend',
|
|
62
72
|
agents: {
|
|
63
73
|
architect: 'frontend-architect',
|
|
@@ -80,6 +90,11 @@ const DOMAIN_DEFAULTS = {
|
|
|
80
90
|
after_execute: false,
|
|
81
91
|
after_review: false,
|
|
82
92
|
},
|
|
93
|
+
parallelism: { minAgents: 5, maxAgents: 10 },
|
|
94
|
+
mcp: {
|
|
95
|
+
enforce: true,
|
|
96
|
+
discovery: true,
|
|
97
|
+
},
|
|
83
98
|
},
|
|
84
99
|
};
|
|
85
100
|
|