@firatcand/roster 1.0.0 → 1.0.2
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 +9 -6
- package/bin/roster.js +1243 -164
- package/package.json +1 -1
- package/templates/scaffold/design/EXPERT.md +5 -5
- package/templates/scaffold/dreamer/agent.md +1 -1
- package/templates/scaffold/gtm/EXPERT.md +7 -7
- package/templates/scaffold/ops/EXPERT.md +6 -6
- package/templates/scaffold/product/EXPERT.md +5 -5
- package/templates/scaffold/scripts/lib/bindings-prompt.sh +3 -1
- package/templates/scaffold/scripts/new-agent.sh +8 -6
package/bin/roster.js
CHANGED
|
@@ -10,6 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
11
|
import YAML, { parse } from "yaml";
|
|
12
12
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
13
|
+
import { createInterface } from "node:readline";
|
|
13
14
|
//#region src/lib/paths.ts
|
|
14
15
|
function findRosterRoot() {
|
|
15
16
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -99,12 +100,13 @@ var RosterError = class extends Error {
|
|
|
99
100
|
function isRosterError(err) {
|
|
100
101
|
return err instanceof RosterError;
|
|
101
102
|
}
|
|
102
|
-
function permissionError(targetPath, cause) {
|
|
103
|
+
function permissionError(targetPath, cause, scope) {
|
|
103
104
|
const syscall = cause.syscall ? ` (${cause.syscall})` : "";
|
|
105
|
+
const remedy = scope === "project" ? ` Check filesystem permissions on the workspace, or run: chmod -R u+w ${targetPath}` : ` Re-run with sudo, or run: sudo chown -R "$USER" ${targetPath}`;
|
|
104
106
|
return new RosterError({
|
|
105
107
|
header: `${chalk.red.bold("roster:")} permission denied`,
|
|
106
108
|
body: ` ${cause.code ?? "EACCES"}${syscall} writing ${targetPath}`,
|
|
107
|
-
remedy
|
|
109
|
+
remedy,
|
|
108
110
|
exitCode: 1
|
|
109
111
|
});
|
|
110
112
|
}
|
|
@@ -170,6 +172,32 @@ function userCancelledInstall() {
|
|
|
170
172
|
exitCode: 2
|
|
171
173
|
});
|
|
172
174
|
}
|
|
175
|
+
function workspaceRequiredError(cwd) {
|
|
176
|
+
return new RosterError({
|
|
177
|
+
header: `${chalk.red.bold("roster:")} project-level install requires a roster workspace`,
|
|
178
|
+
body: [` CWD: ${cwd}`, ` Expected: config/project.yaml (created by ${chalk.bold("roster init")})`].join("\n"),
|
|
179
|
+
remedy: [
|
|
180
|
+
` Either:`,
|
|
181
|
+
` - cd into a roster workspace, or`,
|
|
182
|
+
` - re-run with ${chalk.bold("--scope user")} to install to your home directory.`
|
|
183
|
+
].join("\n"),
|
|
184
|
+
exitCode: 2
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function toolsNotDetectedError(requestedKeys, detectedKeys) {
|
|
188
|
+
const missing = requestedKeys.filter((k) => !detectedKeys.includes(k));
|
|
189
|
+
const detectedLabel = detectedKeys.length === 0 ? "(none)" : detectedKeys.join(", ");
|
|
190
|
+
return new RosterError({
|
|
191
|
+
header: `${chalk.red.bold("roster:")} requested tool${missing.length === 1 ? "" : "s"} not detected: ${missing.join(", ")}`,
|
|
192
|
+
body: [` --tool requested: ${requestedKeys.join(", ")}`, ` detected on this machine: ${detectedLabel}`].join("\n"),
|
|
193
|
+
remedy: [
|
|
194
|
+
` Either:`,
|
|
195
|
+
` - install the missing tool${missing.length === 1 ? "" : "s"} first, or`,
|
|
196
|
+
` - drop the missing key${missing.length === 1 ? "" : "s"} from ${chalk.bold("--tool")}.`
|
|
197
|
+
].join("\n"),
|
|
198
|
+
exitCode: 3
|
|
199
|
+
});
|
|
200
|
+
}
|
|
173
201
|
function linuxClaudeUnsupportedError() {
|
|
174
202
|
return new RosterError({
|
|
175
203
|
header: `${chalk.red.bold("roster:")} Claude Desktop scheduling is not available on Linux`,
|
|
@@ -460,7 +488,7 @@ async function prepareTargetForWrite(targetPath, kind, logger, confirm) {
|
|
|
460
488
|
}
|
|
461
489
|
return true;
|
|
462
490
|
}
|
|
463
|
-
async function copyOne(srcPath, targetPath, kind, logger, confirm) {
|
|
491
|
+
async function copyOne(srcPath, targetPath, kind, logger, confirm, scope) {
|
|
464
492
|
try {
|
|
465
493
|
if (!await prepareTargetForWrite(targetPath, kind, logger, confirm)) return false;
|
|
466
494
|
await copy(srcPath, targetPath, {
|
|
@@ -469,17 +497,17 @@ async function copyOne(srcPath, targetPath, kind, logger, confirm) {
|
|
|
469
497
|
});
|
|
470
498
|
return true;
|
|
471
499
|
} catch (err) {
|
|
472
|
-
if (isEacces(err)) throw permissionError(targetPath, err);
|
|
500
|
+
if (isEacces(err)) throw permissionError(targetPath, err, scope);
|
|
473
501
|
throw err;
|
|
474
502
|
}
|
|
475
503
|
}
|
|
476
|
-
async function writeRenderedOne(targetPath, contents, kind, logger, confirm) {
|
|
504
|
+
async function writeRenderedOne(targetPath, contents, kind, logger, confirm, scope) {
|
|
477
505
|
try {
|
|
478
506
|
if (!await prepareTargetForWrite(targetPath, kind, logger, confirm)) return false;
|
|
479
507
|
writeFileSync(targetPath, contents);
|
|
480
508
|
return true;
|
|
481
509
|
} catch (err) {
|
|
482
|
-
if (isEacces(err)) throw permissionError(targetPath, err);
|
|
510
|
+
if (isEacces(err)) throw permissionError(targetPath, err, scope);
|
|
483
511
|
throw err;
|
|
484
512
|
}
|
|
485
513
|
}
|
|
@@ -496,7 +524,7 @@ async function installToTool(tool, opts) {
|
|
|
496
524
|
await ensureDir(tool.skillsTarget);
|
|
497
525
|
if (tool.agentsTarget) await ensureDir(tool.agentsTarget);
|
|
498
526
|
} catch (err) {
|
|
499
|
-
if (isEacces(err)) throw permissionError(tool.skillsTarget, err);
|
|
527
|
+
if (isEacces(err)) throw permissionError(tool.skillsTarget, err, opts.scope);
|
|
500
528
|
throw err;
|
|
501
529
|
}
|
|
502
530
|
let skillsCount = 0;
|
|
@@ -514,7 +542,7 @@ async function installToTool(tool, opts) {
|
|
|
514
542
|
const renderedSkillMd = join(targetPath, "SKILL.md");
|
|
515
543
|
assertWithinRoot(targetPath, tool.configRoot, "skill targetPath");
|
|
516
544
|
info(chalk.dim(` + skill ${dirent.name} -> ${targetPath}`));
|
|
517
|
-
if (await copyOne(srcPath, targetPath, "skill", logger, confirm)) {
|
|
545
|
+
if (await copyOne(srcPath, targetPath, "skill", logger, confirm, opts.scope)) {
|
|
518
546
|
renderSkillFrontmatter(renderedSkillMd, tool.key);
|
|
519
547
|
skillsCount++;
|
|
520
548
|
}
|
|
@@ -543,15 +571,15 @@ async function installToTool(tool, opts) {
|
|
|
543
571
|
throw err;
|
|
544
572
|
}
|
|
545
573
|
info(chalk.dim(` + agent ${dirent.name} -> ${tomlTarget}`));
|
|
546
|
-
const wroteToml = await writeRenderedOne(tomlTarget, rendered.toml, "agent", logger, confirm);
|
|
547
|
-
const wrotePersona = await writeRenderedOne(personaTarget, rendered.personaBody, "agent persona", logger, confirm);
|
|
574
|
+
const wroteToml = await writeRenderedOne(tomlTarget, rendered.toml, "agent", logger, confirm, opts.scope);
|
|
575
|
+
const wrotePersona = await writeRenderedOne(personaTarget, rendered.personaBody, "agent persona", logger, confirm, opts.scope);
|
|
548
576
|
if (wroteToml && wrotePersona) agentsCount++;
|
|
549
577
|
continue;
|
|
550
578
|
}
|
|
551
579
|
const targetPath = join(tool.agentsTarget, dirent.name);
|
|
552
580
|
assertWithinRoot(targetPath, tool.configRoot, "agent targetPath");
|
|
553
581
|
info(chalk.dim(` + agent ${dirent.name} -> ${targetPath}`));
|
|
554
|
-
if (await copyOne(srcPath, targetPath, "agent", logger, confirm)) agentsCount++;
|
|
582
|
+
if (await copyOne(srcPath, targetPath, "agent", logger, confirm, opts.scope)) agentsCount++;
|
|
555
583
|
}
|
|
556
584
|
}
|
|
557
585
|
return {
|
|
@@ -569,19 +597,46 @@ const KNOWN_TOOL_KEYS = [
|
|
|
569
597
|
"gemini"
|
|
570
598
|
];
|
|
571
599
|
const TOOL_LIST$1 = KNOWN_TOOL_KEYS.join(" | ");
|
|
600
|
+
const SCOPE_LIST$1 = "(project | user)";
|
|
572
601
|
function isToolKey(value) {
|
|
573
602
|
return KNOWN_TOOL_KEYS.includes(value);
|
|
574
603
|
}
|
|
604
|
+
function isScope$1(value) {
|
|
605
|
+
return value === "project" || value === "user";
|
|
606
|
+
}
|
|
607
|
+
function parseToolValue(value) {
|
|
608
|
+
const parts = value.split(",").map((s) => s.trim());
|
|
609
|
+
if (parts.some((p) => p.length === 0)) return {
|
|
610
|
+
ok: false,
|
|
611
|
+
message: `--tool received an empty value (check for stray commas)`
|
|
612
|
+
};
|
|
613
|
+
const keys = [];
|
|
614
|
+
for (const part of parts) {
|
|
615
|
+
if (!isToolKey(part)) return {
|
|
616
|
+
ok: false,
|
|
617
|
+
message: `unknown tool '${part}'; expected one of: ${TOOL_LIST$1}`
|
|
618
|
+
};
|
|
619
|
+
if (!keys.includes(part)) keys.push(part);
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
ok: true,
|
|
623
|
+
keys
|
|
624
|
+
};
|
|
625
|
+
}
|
|
575
626
|
function parseInstallArgs(args) {
|
|
576
627
|
let silent = false;
|
|
577
628
|
let verbose = false;
|
|
629
|
+
let yes = false;
|
|
578
630
|
let all = false;
|
|
579
631
|
let toolValue = null;
|
|
580
632
|
let toolFlagSeen = false;
|
|
633
|
+
let scopeValue = null;
|
|
634
|
+
let scopeFlagSeen = false;
|
|
581
635
|
for (let i = 0; i < args.length; i++) {
|
|
582
636
|
const arg = args[i];
|
|
583
637
|
if (arg === "--silent") silent = true;
|
|
584
638
|
else if (arg === "--verbose") verbose = true;
|
|
639
|
+
else if (arg === "--yes" || arg === "-y") yes = true;
|
|
585
640
|
else if (arg === "--all") all = true;
|
|
586
641
|
else if (arg === "--tool") {
|
|
587
642
|
toolFlagSeen = true;
|
|
@@ -600,57 +655,150 @@ function parseInstallArgs(args) {
|
|
|
600
655
|
message: `--tool requires a tool name (${TOOL_LIST$1})`
|
|
601
656
|
};
|
|
602
657
|
toolValue = value;
|
|
658
|
+
} else if (arg === "--scope") {
|
|
659
|
+
scopeFlagSeen = true;
|
|
660
|
+
const next = args[i + 1];
|
|
661
|
+
if (next === void 0 || next.startsWith("-")) return {
|
|
662
|
+
kind: "err",
|
|
663
|
+
message: `--scope requires a value: ${SCOPE_LIST$1}`
|
|
664
|
+
};
|
|
665
|
+
scopeValue = next;
|
|
666
|
+
i++;
|
|
667
|
+
} else if (arg.startsWith("--scope=")) {
|
|
668
|
+
scopeFlagSeen = true;
|
|
669
|
+
const value = arg.slice(8);
|
|
670
|
+
if (value === "") return {
|
|
671
|
+
kind: "err",
|
|
672
|
+
message: `--scope requires a value: ${SCOPE_LIST$1}`
|
|
673
|
+
};
|
|
674
|
+
scopeValue = value;
|
|
603
675
|
}
|
|
604
676
|
}
|
|
605
677
|
if (all && toolFlagSeen) return {
|
|
606
678
|
kind: "err",
|
|
607
679
|
message: "flags --all and --tool are mutually exclusive"
|
|
608
680
|
};
|
|
681
|
+
let scope = null;
|
|
682
|
+
if (scopeFlagSeen) {
|
|
683
|
+
if (scopeValue === null || !isScope$1(scopeValue)) return {
|
|
684
|
+
kind: "err",
|
|
685
|
+
message: `unknown scope '${scopeValue ?? ""}'; expected one of: ${SCOPE_LIST$1}`
|
|
686
|
+
};
|
|
687
|
+
scope = scopeValue;
|
|
688
|
+
}
|
|
689
|
+
let target;
|
|
609
690
|
if (toolFlagSeen && toolValue !== null) {
|
|
610
|
-
|
|
691
|
+
const parsed = parseToolValue(toolValue);
|
|
692
|
+
if (!parsed.ok) return {
|
|
611
693
|
kind: "err",
|
|
612
|
-
message:
|
|
694
|
+
message: parsed.message
|
|
613
695
|
};
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
verbose,
|
|
618
|
-
target: {
|
|
619
|
-
mode: "tool",
|
|
620
|
-
key: toolValue
|
|
621
|
-
}
|
|
696
|
+
target = {
|
|
697
|
+
mode: "tools",
|
|
698
|
+
keys: parsed.keys
|
|
622
699
|
};
|
|
623
|
-
}
|
|
624
|
-
|
|
700
|
+
} else if (all) target = { mode: "all" };
|
|
701
|
+
else target = { mode: "interactive" };
|
|
702
|
+
return {
|
|
625
703
|
kind: "ok",
|
|
626
704
|
silent,
|
|
627
705
|
verbose,
|
|
628
|
-
|
|
706
|
+
yes,
|
|
707
|
+
scope,
|
|
708
|
+
target
|
|
629
709
|
};
|
|
710
|
+
}
|
|
711
|
+
//#endregion
|
|
712
|
+
//#region src/lib/install-scope.ts
|
|
713
|
+
function detectWorkspace(cwd) {
|
|
714
|
+
return existsSync(join(cwd, "config", "project.yaml"));
|
|
715
|
+
}
|
|
716
|
+
function defaultScopeForContext(workspaceExists) {
|
|
717
|
+
return workspaceExists ? "project" : "user";
|
|
718
|
+
}
|
|
719
|
+
function projectPathsFor(toolKey, workspaceRoot, hasAgents) {
|
|
720
|
+
const agentsPath = (root) => hasAgents ? join(workspaceRoot, root, "agents") : null;
|
|
721
|
+
switch (toolKey) {
|
|
722
|
+
case "claude": return {
|
|
723
|
+
configRoot: join(workspaceRoot, ".claude"),
|
|
724
|
+
skillsTarget: join(workspaceRoot, ".claude", "skills"),
|
|
725
|
+
agentsTarget: agentsPath(".claude")
|
|
726
|
+
};
|
|
727
|
+
case "codex": return {
|
|
728
|
+
configRoot: join(workspaceRoot, ".codex"),
|
|
729
|
+
skillsTarget: join(workspaceRoot, ".codex", "skills"),
|
|
730
|
+
agentsTarget: agentsPath(".codex")
|
|
731
|
+
};
|
|
732
|
+
case "gemini": return {
|
|
733
|
+
configRoot: join(workspaceRoot, ".gemini"),
|
|
734
|
+
skillsTarget: join(workspaceRoot, ".gemini", "extensions"),
|
|
735
|
+
agentsTarget: agentsPath(".gemini")
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function toolForScope(tool, scope, workspaceRoot) {
|
|
740
|
+
if (scope === "user") return tool;
|
|
741
|
+
if (workspaceRoot === void 0) throw new Error("toolForScope: workspaceRoot is required when scope is \"project\"");
|
|
742
|
+
const hasAgents = tool.agentsTarget !== null;
|
|
743
|
+
const paths = projectPathsFor(tool.key, workspaceRoot, hasAgents);
|
|
630
744
|
return {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
verbose,
|
|
634
|
-
target: { mode: "interactive" }
|
|
745
|
+
...tool,
|
|
746
|
+
...paths
|
|
635
747
|
};
|
|
636
748
|
}
|
|
637
749
|
//#endregion
|
|
638
750
|
//#region src/lib/doctor-args.ts
|
|
751
|
+
const SCOPE_LIST = "(project | user)";
|
|
752
|
+
function isScope(value) {
|
|
753
|
+
return value === "project" || value === "user";
|
|
754
|
+
}
|
|
639
755
|
function parseDoctorArgs(args) {
|
|
640
756
|
let json = false;
|
|
641
757
|
let silent = false;
|
|
642
758
|
let fix = false;
|
|
643
759
|
let dryRun = false;
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
760
|
+
let scopeValue = null;
|
|
761
|
+
let scopeFlagSeen = false;
|
|
762
|
+
for (let i = 0; i < args.length; i++) {
|
|
763
|
+
const arg = args[i];
|
|
764
|
+
if (arg === "--json") json = true;
|
|
765
|
+
else if (arg === "--silent") silent = true;
|
|
766
|
+
else if (arg === "--fix") fix = true;
|
|
767
|
+
else if (arg === "--dry-run") dryRun = true;
|
|
768
|
+
else if (arg === "--scope") {
|
|
769
|
+
scopeFlagSeen = true;
|
|
770
|
+
const next = args[i + 1];
|
|
771
|
+
if (next === void 0 || next.startsWith("-")) return {
|
|
772
|
+
kind: "err",
|
|
773
|
+
message: `--scope requires a value: ${SCOPE_LIST}`
|
|
774
|
+
};
|
|
775
|
+
scopeValue = next;
|
|
776
|
+
i++;
|
|
777
|
+
} else if (arg.startsWith("--scope=")) {
|
|
778
|
+
scopeFlagSeen = true;
|
|
779
|
+
const value = arg.slice(8);
|
|
780
|
+
if (value === "") return {
|
|
781
|
+
kind: "err",
|
|
782
|
+
message: `--scope requires a value: ${SCOPE_LIST}`
|
|
783
|
+
};
|
|
784
|
+
scopeValue = value;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
let scope = null;
|
|
788
|
+
if (scopeFlagSeen) {
|
|
789
|
+
if (scopeValue === null || !isScope(scopeValue)) return {
|
|
790
|
+
kind: "err",
|
|
791
|
+
message: `unknown scope '${scopeValue ?? ""}'; expected one of: ${SCOPE_LIST}`
|
|
792
|
+
};
|
|
793
|
+
scope = scopeValue;
|
|
794
|
+
}
|
|
648
795
|
return {
|
|
649
796
|
kind: "ok",
|
|
650
797
|
json,
|
|
651
798
|
silent,
|
|
652
799
|
fix,
|
|
653
|
-
dryRun
|
|
800
|
+
dryRun,
|
|
801
|
+
scope
|
|
654
802
|
};
|
|
655
803
|
}
|
|
656
804
|
const KEBAB_RE$1 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
@@ -2716,7 +2864,7 @@ async function executeInit(opts) {
|
|
|
2716
2864
|
const totalChanged = filesWritten.length + filesUpdated.length + filesLinked.length;
|
|
2717
2865
|
if (!silent) {
|
|
2718
2866
|
info("");
|
|
2719
|
-
info(`${chalk.green("✓")} Initialized ${chalk.bold(projectName)} in ${opts.cwd}`);
|
|
2867
|
+
info(`${chalk.green("✓")} Initialized roster workspace ${chalk.bold(`'${projectName}'`)} in ${opts.cwd} ${chalk.dim("(current directory)")}`);
|
|
2720
2868
|
if (totalChanged > 8) info(chalk.dim(`Files: ${totalChanged} written/linked`));
|
|
2721
2869
|
else {
|
|
2722
2870
|
const changed = [
|
|
@@ -2729,7 +2877,7 @@ async function executeInit(opts) {
|
|
|
2729
2877
|
for (const w of warnings) info(chalk.yellow(w));
|
|
2730
2878
|
if (gitInitialized) info(chalk.dim("Git: initialized .git/"));
|
|
2731
2879
|
info("");
|
|
2732
|
-
info(`${chalk.dim("Next: ")}${chalk.bold("open
|
|
2880
|
+
info(`${chalk.dim("Next: ")}you are already in this directory — ${chalk.bold("open Claude Code here")}${chalk.dim(" and run ")}${chalk.bold("/chief-of-staff audit-repo")}`);
|
|
2733
2881
|
}
|
|
2734
2882
|
return {
|
|
2735
2883
|
status: "ok",
|
|
@@ -3151,7 +3299,7 @@ function validateSchedulesInCwd(cwd) {
|
|
|
3151
3299
|
}
|
|
3152
3300
|
//#endregion
|
|
3153
3301
|
//#region src/lib/dotenv-parse.ts
|
|
3154
|
-
const KEY_RE = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*/;
|
|
3302
|
+
const KEY_RE$1 = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*/;
|
|
3155
3303
|
function parseEnvFile(content) {
|
|
3156
3304
|
const out = /* @__PURE__ */ new Map();
|
|
3157
3305
|
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
@@ -3159,7 +3307,7 @@ function parseEnvFile(content) {
|
|
|
3159
3307
|
const trimmed = rawLine.replace(/^\s+/, "");
|
|
3160
3308
|
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
3161
3309
|
const candidate = trimmed.startsWith("export ") ? trimmed.slice(7).replace(/^\s+/, "") : trimmed;
|
|
3162
|
-
const m = candidate.match(KEY_RE);
|
|
3310
|
+
const m = candidate.match(KEY_RE$1);
|
|
3163
3311
|
if (m === null) continue;
|
|
3164
3312
|
const key = m[1];
|
|
3165
3313
|
const parsed = parseValue(candidate.slice(m[0].length));
|
|
@@ -3244,6 +3392,376 @@ function isWindows() {
|
|
|
3244
3392
|
return getPlatform() === "win32";
|
|
3245
3393
|
}
|
|
3246
3394
|
//#endregion
|
|
3395
|
+
//#region src/lib/agent-config-schema.ts
|
|
3396
|
+
const AGENT_RE = /^[a-z][a-z0-9-]*(\/[a-z][a-z0-9-]*)?$/;
|
|
3397
|
+
const ENV_VAR_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
3398
|
+
const FORBIDDEN_FS_PREFIXES = [
|
|
3399
|
+
"/Users/",
|
|
3400
|
+
"/home/",
|
|
3401
|
+
"/etc/",
|
|
3402
|
+
"/var/",
|
|
3403
|
+
"/tmp/",
|
|
3404
|
+
"/opt/"
|
|
3405
|
+
];
|
|
3406
|
+
const workspaceRootedPath = z.string().refine((p) => p.startsWith("/") && !FORBIDDEN_FS_PREFIXES.some((pfx) => p.startsWith(pfx)), { message: "must be a workspace-root-relative path starting with '/' (rejected: literal absolute fs paths /Users/ /home/ /etc/ /var/ /tmp/ /opt/)" });
|
|
3407
|
+
const toolBindingSchema = z.object({
|
|
3408
|
+
env_var: z.string().regex(ENV_VAR_RE, { message: "env_var: must be SCREAMING_SNAKE_CASE" }),
|
|
3409
|
+
required: z.boolean()
|
|
3410
|
+
}).strict();
|
|
3411
|
+
const agentConfigSchema = z.object({
|
|
3412
|
+
agent: z.string().regex(AGENT_RE, { message: "agent: must match '<function>/<agent>' with kebab-case segments" }),
|
|
3413
|
+
plans_dir: z.string().min(1),
|
|
3414
|
+
guideline_refs: z.record(z.string().min(1), workspaceRootedPath).nullish(),
|
|
3415
|
+
tools: z.record(z.string().min(1), toolBindingSchema).nullish()
|
|
3416
|
+
}).strict();
|
|
3417
|
+
function isInsideRoot(root, p) {
|
|
3418
|
+
const rel = relative(root, p);
|
|
3419
|
+
return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute(rel);
|
|
3420
|
+
}
|
|
3421
|
+
function loadAgentConfig(workspaceRoot, agentPath) {
|
|
3422
|
+
if (!AGENT_RE.test(agentPath)) return {
|
|
3423
|
+
ok: false,
|
|
3424
|
+
errors: [{
|
|
3425
|
+
kind: "invalid-agent-path",
|
|
3426
|
+
message: `agentPath '${agentPath}' must match '<agent>' or '<function>/<agent>' with kebab-case segments`
|
|
3427
|
+
}]
|
|
3428
|
+
};
|
|
3429
|
+
let realRoot;
|
|
3430
|
+
try {
|
|
3431
|
+
realRoot = realpathSync(workspaceRoot);
|
|
3432
|
+
} catch (err) {
|
|
3433
|
+
return {
|
|
3434
|
+
ok: false,
|
|
3435
|
+
errors: [{
|
|
3436
|
+
kind: "missing-file",
|
|
3437
|
+
message: `workspaceRoot '${workspaceRoot}' not accessible: ${err.message}`,
|
|
3438
|
+
path: workspaceRoot
|
|
3439
|
+
}]
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
const configPath = join(realRoot, agentPath, "config.yaml");
|
|
3443
|
+
let raw;
|
|
3444
|
+
try {
|
|
3445
|
+
raw = readFileSync(configPath, "utf8");
|
|
3446
|
+
} catch (err) {
|
|
3447
|
+
const e = err;
|
|
3448
|
+
if (e.code === "ENOENT") return {
|
|
3449
|
+
ok: false,
|
|
3450
|
+
errors: [{
|
|
3451
|
+
kind: "missing-file",
|
|
3452
|
+
message: `config.yaml not found at ${configPath}`,
|
|
3453
|
+
path: configPath
|
|
3454
|
+
}]
|
|
3455
|
+
};
|
|
3456
|
+
return {
|
|
3457
|
+
ok: false,
|
|
3458
|
+
errors: [{
|
|
3459
|
+
kind: "missing-file",
|
|
3460
|
+
message: `failed to read ${configPath}: ${e.message}`,
|
|
3461
|
+
path: configPath
|
|
3462
|
+
}]
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
let doc;
|
|
3466
|
+
try {
|
|
3467
|
+
doc = YAML.parse(raw);
|
|
3468
|
+
} catch (err) {
|
|
3469
|
+
return {
|
|
3470
|
+
ok: false,
|
|
3471
|
+
errors: [{
|
|
3472
|
+
kind: "yaml-parse",
|
|
3473
|
+
message: `YAML parse error in ${configPath}: ${err.message}`,
|
|
3474
|
+
path: configPath
|
|
3475
|
+
}]
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
const parsed = agentConfigSchema.safeParse(doc);
|
|
3479
|
+
if (!parsed.success) return {
|
|
3480
|
+
ok: false,
|
|
3481
|
+
errors: parsed.error.issues.map((issue) => ({
|
|
3482
|
+
kind: "schema",
|
|
3483
|
+
message: issue.message,
|
|
3484
|
+
path: issue.path.length > 0 ? issue.path.join(".") : void 0
|
|
3485
|
+
}))
|
|
3486
|
+
};
|
|
3487
|
+
const config = parsed.data;
|
|
3488
|
+
const errors = [];
|
|
3489
|
+
if (config.agent !== agentPath) errors.push({
|
|
3490
|
+
kind: "agent-field-mismatch",
|
|
3491
|
+
message: `agent field '${config.agent}' does not match agentPath argument '${agentPath}'`,
|
|
3492
|
+
path: "agent"
|
|
3493
|
+
});
|
|
3494
|
+
const refs = config.guideline_refs ?? {};
|
|
3495
|
+
for (const [key, ref] of Object.entries(refs)) {
|
|
3496
|
+
const wantsDirectory = ref.endsWith("/");
|
|
3497
|
+
const stripped = ref.replace(/^\/+/, "");
|
|
3498
|
+
const absolute = resolve(realRoot, stripped);
|
|
3499
|
+
if (!isInsideRoot(realRoot, absolute)) {
|
|
3500
|
+
errors.push({
|
|
3501
|
+
kind: "ref-escapes-workspace",
|
|
3502
|
+
message: `guideline_refs.${key}: '${ref}' escapes outside workspace root after resolution`,
|
|
3503
|
+
path: `guideline_refs.${key}`,
|
|
3504
|
+
ref
|
|
3505
|
+
});
|
|
3506
|
+
continue;
|
|
3507
|
+
}
|
|
3508
|
+
let realAbsolute;
|
|
3509
|
+
try {
|
|
3510
|
+
realAbsolute = realpathSync(absolute);
|
|
3511
|
+
} catch (err) {
|
|
3512
|
+
const e = err;
|
|
3513
|
+
if (e.code === "ENOENT") errors.push({
|
|
3514
|
+
kind: "ref-not-found",
|
|
3515
|
+
message: `guideline_refs.${key}: '${ref}' resolves to ${absolute} which does not exist`,
|
|
3516
|
+
path: `guideline_refs.${key}`,
|
|
3517
|
+
ref
|
|
3518
|
+
});
|
|
3519
|
+
else errors.push({
|
|
3520
|
+
kind: "ref-not-found",
|
|
3521
|
+
message: `guideline_refs.${key}: realpath ${absolute} failed: ${e.message}`,
|
|
3522
|
+
path: `guideline_refs.${key}`,
|
|
3523
|
+
ref
|
|
3524
|
+
});
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
if (!isInsideRoot(realRoot, realAbsolute)) {
|
|
3528
|
+
errors.push({
|
|
3529
|
+
kind: "ref-escapes-workspace",
|
|
3530
|
+
message: `guideline_refs.${key}: '${ref}' resolves via symlink to ${realAbsolute} outside workspace root`,
|
|
3531
|
+
path: `guideline_refs.${key}`,
|
|
3532
|
+
ref
|
|
3533
|
+
});
|
|
3534
|
+
continue;
|
|
3535
|
+
}
|
|
3536
|
+
let stat;
|
|
3537
|
+
try {
|
|
3538
|
+
stat = statSync(realAbsolute);
|
|
3539
|
+
} catch (err) {
|
|
3540
|
+
errors.push({
|
|
3541
|
+
kind: "ref-not-found",
|
|
3542
|
+
message: `guideline_refs.${key}: stat ${realAbsolute} failed: ${err.message}`,
|
|
3543
|
+
path: `guideline_refs.${key}`,
|
|
3544
|
+
ref
|
|
3545
|
+
});
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
if (wantsDirectory && !stat.isDirectory()) errors.push({
|
|
3549
|
+
kind: "ref-shape-mismatch",
|
|
3550
|
+
message: `guideline_refs.${key}: '${ref}' ends with '/' but ${realAbsolute} is not a directory`,
|
|
3551
|
+
path: `guideline_refs.${key}`,
|
|
3552
|
+
ref
|
|
3553
|
+
});
|
|
3554
|
+
else if (!wantsDirectory && !stat.isFile()) errors.push({
|
|
3555
|
+
kind: "ref-shape-mismatch",
|
|
3556
|
+
message: `guideline_refs.${key}: '${ref}' has no trailing '/' but ${realAbsolute} is not a regular file (add '/' if you meant a directory)`,
|
|
3557
|
+
path: `guideline_refs.${key}`,
|
|
3558
|
+
ref
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
if (errors.length > 0) return {
|
|
3562
|
+
ok: false,
|
|
3563
|
+
errors
|
|
3564
|
+
};
|
|
3565
|
+
return {
|
|
3566
|
+
ok: true,
|
|
3567
|
+
config,
|
|
3568
|
+
refsChecked: Object.keys(refs).length
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
//#endregion
|
|
3572
|
+
//#region src/lib/env-merge.ts
|
|
3573
|
+
function resolveAgentEnv(workspaceRoot, agentPath) {
|
|
3574
|
+
const workspace = tryReadAndParse(join(workspaceRoot, ".env"));
|
|
3575
|
+
const agent = tryReadAndParse(join(workspaceRoot, agentPath, ".env"));
|
|
3576
|
+
const out = {};
|
|
3577
|
+
for (const [k, v] of workspace) {
|
|
3578
|
+
if (v.length === 0) continue;
|
|
3579
|
+
out[k] = v;
|
|
3580
|
+
}
|
|
3581
|
+
for (const [k, v] of agent) if (v.length === 0) delete out[k];
|
|
3582
|
+
else out[k] = v;
|
|
3583
|
+
return out;
|
|
3584
|
+
}
|
|
3585
|
+
function tryReadAndParse(path) {
|
|
3586
|
+
try {
|
|
3587
|
+
return parseEnvFile(readFileSync(path, "utf8"));
|
|
3588
|
+
} catch {
|
|
3589
|
+
return /* @__PURE__ */ new Map();
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
//#endregion
|
|
3593
|
+
//#region src/lib/doctor-agent-env-audit.ts
|
|
3594
|
+
const SKIP_TOP$1 = new Set([
|
|
3595
|
+
"roster",
|
|
3596
|
+
"node_modules",
|
|
3597
|
+
"plans",
|
|
3598
|
+
"spec",
|
|
3599
|
+
"docs",
|
|
3600
|
+
"bin",
|
|
3601
|
+
"lib",
|
|
3602
|
+
"skills",
|
|
3603
|
+
"agents",
|
|
3604
|
+
"templates",
|
|
3605
|
+
"test",
|
|
3606
|
+
"src"
|
|
3607
|
+
]);
|
|
3608
|
+
function collectAgentEnvFiles(cwd) {
|
|
3609
|
+
const out = [];
|
|
3610
|
+
let topEntries;
|
|
3611
|
+
try {
|
|
3612
|
+
topEntries = readdirSync(cwd);
|
|
3613
|
+
} catch {
|
|
3614
|
+
return [];
|
|
3615
|
+
}
|
|
3616
|
+
for (const top of topEntries) {
|
|
3617
|
+
if (top.startsWith(".")) continue;
|
|
3618
|
+
if (SKIP_TOP$1.has(top)) continue;
|
|
3619
|
+
const topDir = join(cwd, top);
|
|
3620
|
+
let topSt;
|
|
3621
|
+
try {
|
|
3622
|
+
topSt = statSync(topDir);
|
|
3623
|
+
} catch {
|
|
3624
|
+
continue;
|
|
3625
|
+
}
|
|
3626
|
+
if (!topSt.isDirectory()) continue;
|
|
3627
|
+
const topEnv = join(topDir, ".env");
|
|
3628
|
+
try {
|
|
3629
|
+
if (statSync(topEnv).isFile()) out.push(topEnv);
|
|
3630
|
+
} catch {}
|
|
3631
|
+
let agents;
|
|
3632
|
+
try {
|
|
3633
|
+
agents = readdirSync(topDir);
|
|
3634
|
+
} catch {
|
|
3635
|
+
continue;
|
|
3636
|
+
}
|
|
3637
|
+
for (const agent of agents) {
|
|
3638
|
+
if (agent.startsWith(".")) continue;
|
|
3639
|
+
const agentDir = join(topDir, agent);
|
|
3640
|
+
let aSt;
|
|
3641
|
+
try {
|
|
3642
|
+
aSt = statSync(agentDir);
|
|
3643
|
+
} catch {
|
|
3644
|
+
continue;
|
|
3645
|
+
}
|
|
3646
|
+
if (!aSt.isDirectory()) continue;
|
|
3647
|
+
const envPath = join(agentDir, ".env");
|
|
3648
|
+
let eSt;
|
|
3649
|
+
try {
|
|
3650
|
+
eSt = statSync(envPath);
|
|
3651
|
+
} catch {
|
|
3652
|
+
continue;
|
|
3653
|
+
}
|
|
3654
|
+
if (!eSt.isFile()) continue;
|
|
3655
|
+
out.push(envPath);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
return out;
|
|
3659
|
+
}
|
|
3660
|
+
const KEY_RE = /^([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
3661
|
+
function findLastLineForKey(rawContent, key) {
|
|
3662
|
+
let content = rawContent;
|
|
3663
|
+
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
3664
|
+
const lines = content.split(/\r?\n/);
|
|
3665
|
+
let lastMatch = null;
|
|
3666
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3667
|
+
const trimmed = (lines[i] ?? "").replace(/^\s+/, "");
|
|
3668
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
3669
|
+
const m = (trimmed.startsWith("export ") ? trimmed.slice(7).replace(/^\s+/, "") : trimmed).match(KEY_RE);
|
|
3670
|
+
if (m === null) continue;
|
|
3671
|
+
if (m[1] === key) lastMatch = i + 1;
|
|
3672
|
+
}
|
|
3673
|
+
return lastMatch;
|
|
3674
|
+
}
|
|
3675
|
+
function auditAgentEnvRedundancy(cwd) {
|
|
3676
|
+
let workspaceMap;
|
|
3677
|
+
try {
|
|
3678
|
+
workspaceMap = parseEnvFile(readFileSync(join(cwd, ".env"), "utf8"));
|
|
3679
|
+
} catch {
|
|
3680
|
+
return {
|
|
3681
|
+
status: "ok",
|
|
3682
|
+
items: []
|
|
3683
|
+
};
|
|
3684
|
+
}
|
|
3685
|
+
const items = [];
|
|
3686
|
+
for (const envPath of collectAgentEnvFiles(cwd)) {
|
|
3687
|
+
let raw;
|
|
3688
|
+
try {
|
|
3689
|
+
raw = readFileSync(envPath, "utf8");
|
|
3690
|
+
} catch {
|
|
3691
|
+
continue;
|
|
3692
|
+
}
|
|
3693
|
+
const agentMap = parseEnvFile(raw);
|
|
3694
|
+
for (const [key, value] of agentMap) {
|
|
3695
|
+
const wsValue = workspaceMap.get(key);
|
|
3696
|
+
if (wsValue === void 0) continue;
|
|
3697
|
+
if (wsValue !== value) continue;
|
|
3698
|
+
const line = findLastLineForKey(raw, key);
|
|
3699
|
+
if (line === null) continue;
|
|
3700
|
+
items.push({
|
|
3701
|
+
agentEnvPath: relative(cwd, envPath),
|
|
3702
|
+
line,
|
|
3703
|
+
key
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
items.sort((a, b) => {
|
|
3708
|
+
if (a.agentEnvPath !== b.agentEnvPath) return a.agentEnvPath.localeCompare(b.agentEnvPath);
|
|
3709
|
+
return a.line - b.line;
|
|
3710
|
+
});
|
|
3711
|
+
return {
|
|
3712
|
+
status: items.length === 0 ? "ok" : "warn",
|
|
3713
|
+
items
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
function removeLineForKey(absPath, oneBasedLine, expectedKey, expectedValue, dryRun) {
|
|
3717
|
+
let raw;
|
|
3718
|
+
try {
|
|
3719
|
+
raw = readFileSync(absPath, "utf8");
|
|
3720
|
+
} catch (err) {
|
|
3721
|
+
return {
|
|
3722
|
+
kind: "error",
|
|
3723
|
+
message: err.message
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
const hadTrailingNewline = raw.length > 0 && raw.charCodeAt(raw.length - 1) === 10;
|
|
3727
|
+
const newline = /\r\n/.test(raw) ? "\r\n" : "\n";
|
|
3728
|
+
const lines = raw.split(/\r?\n/);
|
|
3729
|
+
const effectiveLineCount = hadTrailingNewline ? lines.length - 1 : lines.length;
|
|
3730
|
+
if (oneBasedLine < 1 || oneBasedLine > effectiveLineCount) return {
|
|
3731
|
+
kind: "changed",
|
|
3732
|
+
reason: "line out of range"
|
|
3733
|
+
};
|
|
3734
|
+
const targetLine = lines[oneBasedLine - 1] ?? "";
|
|
3735
|
+
const trimmed = targetLine.replace(/^\s+/, "");
|
|
3736
|
+
const m = (trimmed.startsWith("export ") ? trimmed.slice(7).replace(/^\s+/, "") : trimmed).match(KEY_RE);
|
|
3737
|
+
if (m === null || m[1] !== expectedKey) return {
|
|
3738
|
+
kind: "changed",
|
|
3739
|
+
reason: "line no longer declares expected key"
|
|
3740
|
+
};
|
|
3741
|
+
const parsedAtLine = parseEnvFile(targetLine).get(expectedKey);
|
|
3742
|
+
if (parsedAtLine === void 0 || parsedAtLine !== expectedValue) return {
|
|
3743
|
+
kind: "changed",
|
|
3744
|
+
reason: "line value no longer matches workspace"
|
|
3745
|
+
};
|
|
3746
|
+
if (dryRun) return { kind: "would-remove" };
|
|
3747
|
+
const next = lines.slice(0, oneBasedLine - 1).concat(lines.slice(oneBasedLine)).join(newline);
|
|
3748
|
+
let mode = 384;
|
|
3749
|
+
try {
|
|
3750
|
+
mode = statSync(absPath).mode & 511;
|
|
3751
|
+
} catch {}
|
|
3752
|
+
const tmpPath = absPath + ".tmp." + String(process.pid);
|
|
3753
|
+
try {
|
|
3754
|
+
writeFileSync(tmpPath, next, { mode });
|
|
3755
|
+
renameSync(tmpPath, absPath);
|
|
3756
|
+
} catch (err) {
|
|
3757
|
+
return {
|
|
3758
|
+
kind: "error",
|
|
3759
|
+
message: err.message
|
|
3760
|
+
};
|
|
3761
|
+
}
|
|
3762
|
+
return { kind: "removed" };
|
|
3763
|
+
}
|
|
3764
|
+
//#endregion
|
|
3247
3765
|
//#region src/lib/doctor-secrets-audit.ts
|
|
3248
3766
|
function formatMode(mode) {
|
|
3249
3767
|
return "0" + (mode & 511).toString(8).padStart(3, "0");
|
|
@@ -3275,85 +3793,142 @@ function auditEnvPermissions(cwd) {
|
|
|
3275
3793
|
autoFixable: true
|
|
3276
3794
|
};
|
|
3277
3795
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
for (let i = 0; i < lines.length; i++) {
|
|
3283
|
-
const line = lines[i];
|
|
3284
|
-
for (const m of line.matchAll(VAR_REF_RE)) {
|
|
3285
|
-
const key = m[1] ?? m[2] ?? "";
|
|
3286
|
-
if (key.length === 0) continue;
|
|
3287
|
-
const list = out.get(key);
|
|
3288
|
-
if (list) list.push(i + 1);
|
|
3289
|
-
else out.set(key, [i + 1]);
|
|
3290
|
-
}
|
|
3291
|
-
}
|
|
3292
|
-
return out;
|
|
3796
|
+
function classifyAgentEnvMode(modeBits) {
|
|
3797
|
+
if (modeBits === 384) return "ok";
|
|
3798
|
+
if ((modeBits & 18) !== 0) return "fail";
|
|
3799
|
+
return "warn";
|
|
3293
3800
|
}
|
|
3294
|
-
|
|
3295
|
-
"roster",
|
|
3296
|
-
"node_modules",
|
|
3297
|
-
"plans",
|
|
3298
|
-
"spec",
|
|
3299
|
-
"docs",
|
|
3300
|
-
"bin",
|
|
3301
|
-
"lib",
|
|
3302
|
-
"skills",
|
|
3303
|
-
"agents",
|
|
3304
|
-
"templates",
|
|
3305
|
-
"test",
|
|
3306
|
-
"src"
|
|
3307
|
-
]);
|
|
3308
|
-
function collectConfigYamls(cwd) {
|
|
3801
|
+
function listAgentDirs(cwd) {
|
|
3309
3802
|
const out = [];
|
|
3310
3803
|
let topEntries;
|
|
3311
3804
|
try {
|
|
3312
3805
|
topEntries = readdirSync(cwd);
|
|
3313
3806
|
} catch {
|
|
3314
|
-
return
|
|
3807
|
+
return out;
|
|
3315
3808
|
}
|
|
3316
3809
|
for (const top of topEntries) {
|
|
3317
3810
|
if (top.startsWith(".")) continue;
|
|
3318
3811
|
if (SKIP_TOP.has(top)) continue;
|
|
3319
|
-
const
|
|
3320
|
-
let
|
|
3812
|
+
const topDir = join(cwd, top);
|
|
3813
|
+
let topStat;
|
|
3321
3814
|
try {
|
|
3322
|
-
|
|
3815
|
+
topStat = statSync(topDir);
|
|
3323
3816
|
} catch {
|
|
3324
3817
|
continue;
|
|
3325
3818
|
}
|
|
3326
|
-
if (!
|
|
3327
|
-
|
|
3819
|
+
if (!topStat.isDirectory()) continue;
|
|
3820
|
+
out.push(topDir);
|
|
3821
|
+
let children;
|
|
3328
3822
|
try {
|
|
3329
|
-
|
|
3823
|
+
children = readdirSync(topDir);
|
|
3330
3824
|
} catch {
|
|
3331
3825
|
continue;
|
|
3332
3826
|
}
|
|
3333
|
-
for (const
|
|
3334
|
-
if (
|
|
3335
|
-
const
|
|
3336
|
-
let
|
|
3827
|
+
for (const child of children) {
|
|
3828
|
+
if (child.startsWith(".")) continue;
|
|
3829
|
+
const childDir = join(topDir, child);
|
|
3830
|
+
let childStat;
|
|
3337
3831
|
try {
|
|
3338
|
-
|
|
3832
|
+
childStat = statSync(childDir);
|
|
3339
3833
|
} catch {
|
|
3340
3834
|
continue;
|
|
3341
3835
|
}
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3836
|
+
if (!childStat.isDirectory()) continue;
|
|
3837
|
+
out.push(childDir);
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
return out;
|
|
3841
|
+
}
|
|
3842
|
+
function auditAgentEnvPermissions(cwd) {
|
|
3843
|
+
if (isWindows()) return {
|
|
3844
|
+
status: "skip-platform",
|
|
3845
|
+
reason: "win32-mode-bits-not-portable"
|
|
3846
|
+
};
|
|
3847
|
+
const items = [];
|
|
3848
|
+
let aggregate = "ok";
|
|
3849
|
+
for (const agentDir of listAgentDirs(cwd)) {
|
|
3850
|
+
const envPath = join(agentDir, ".env");
|
|
3851
|
+
let st;
|
|
3852
|
+
try {
|
|
3853
|
+
st = statSync(envPath);
|
|
3854
|
+
} catch {
|
|
3855
|
+
continue;
|
|
3856
|
+
}
|
|
3857
|
+
if (!st.isFile()) continue;
|
|
3858
|
+
const modeBits = st.mode & 511;
|
|
3859
|
+
const mode = formatMode(st.mode);
|
|
3860
|
+
const agentPath = relative(cwd, agentDir);
|
|
3861
|
+
const cls = classifyAgentEnvMode(modeBits);
|
|
3862
|
+
if (cls === "ok") items.push({
|
|
3863
|
+
status: "ok",
|
|
3864
|
+
agentPath,
|
|
3865
|
+
envPath,
|
|
3866
|
+
mode
|
|
3867
|
+
});
|
|
3868
|
+
else if (cls === "warn") {
|
|
3869
|
+
items.push({
|
|
3870
|
+
status: "warn",
|
|
3871
|
+
agentPath,
|
|
3872
|
+
envPath,
|
|
3873
|
+
mode,
|
|
3874
|
+
expected: "0600",
|
|
3875
|
+
autoFixable: true
|
|
3876
|
+
});
|
|
3877
|
+
if (aggregate === "ok") aggregate = "warn";
|
|
3878
|
+
} else {
|
|
3879
|
+
items.push({
|
|
3880
|
+
status: "fail",
|
|
3881
|
+
agentPath,
|
|
3882
|
+
envPath,
|
|
3883
|
+
mode,
|
|
3884
|
+
expected: "0600",
|
|
3885
|
+
autoFixable: true
|
|
3886
|
+
});
|
|
3887
|
+
aggregate = "fail";
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
return {
|
|
3891
|
+
status: aggregate,
|
|
3892
|
+
items
|
|
3893
|
+
};
|
|
3894
|
+
}
|
|
3895
|
+
const VAR_REF_RE = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
|
|
3896
|
+
function extractVarRefs(content) {
|
|
3897
|
+
const out = /* @__PURE__ */ new Map();
|
|
3898
|
+
const lines = content.split(/\r?\n/);
|
|
3899
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3900
|
+
const line = lines[i];
|
|
3901
|
+
for (const m of line.matchAll(VAR_REF_RE)) {
|
|
3902
|
+
const key = m[1] ?? m[2] ?? "";
|
|
3903
|
+
if (key.length === 0) continue;
|
|
3904
|
+
const list = out.get(key);
|
|
3905
|
+
if (list) list.push(i + 1);
|
|
3906
|
+
else out.set(key, [i + 1]);
|
|
3353
3907
|
}
|
|
3354
3908
|
}
|
|
3355
3909
|
return out;
|
|
3356
3910
|
}
|
|
3911
|
+
const SKIP_TOP = new Set([
|
|
3912
|
+
"roster",
|
|
3913
|
+
"node_modules",
|
|
3914
|
+
"plans",
|
|
3915
|
+
"spec",
|
|
3916
|
+
"docs",
|
|
3917
|
+
"bin",
|
|
3918
|
+
"lib",
|
|
3919
|
+
"skills",
|
|
3920
|
+
"agents",
|
|
3921
|
+
"templates",
|
|
3922
|
+
"test",
|
|
3923
|
+
"src",
|
|
3924
|
+
"config",
|
|
3925
|
+
"guidelines",
|
|
3926
|
+
"logs",
|
|
3927
|
+
"scripts"
|
|
3928
|
+
]);
|
|
3929
|
+
function collectConfigYamls(cwd) {
|
|
3930
|
+
return listV1AgentPaths(cwd).map((rel) => join(cwd, rel, "config.yaml"));
|
|
3931
|
+
}
|
|
3357
3932
|
const SHELL_VARS = new Set([
|
|
3358
3933
|
"HOME",
|
|
3359
3934
|
"PATH",
|
|
@@ -3528,20 +4103,203 @@ function auditPromptLeak(cwd, schedules) {
|
|
|
3528
4103
|
items
|
|
3529
4104
|
};
|
|
3530
4105
|
}
|
|
4106
|
+
const AGENT_SEGMENT_RE = /^[a-z][a-z0-9-]*$/;
|
|
4107
|
+
function listV1AgentPaths(cwd) {
|
|
4108
|
+
const out = [];
|
|
4109
|
+
let topEntries;
|
|
4110
|
+
try {
|
|
4111
|
+
topEntries = readdirSync(cwd);
|
|
4112
|
+
} catch {
|
|
4113
|
+
return [];
|
|
4114
|
+
}
|
|
4115
|
+
for (const top of topEntries) {
|
|
4116
|
+
if (top.startsWith(".")) continue;
|
|
4117
|
+
if (SKIP_TOP.has(top)) continue;
|
|
4118
|
+
if (!AGENT_SEGMENT_RE.test(top)) continue;
|
|
4119
|
+
const fnDir = join(cwd, top);
|
|
4120
|
+
let st;
|
|
4121
|
+
try {
|
|
4122
|
+
st = statSync(fnDir);
|
|
4123
|
+
} catch {
|
|
4124
|
+
continue;
|
|
4125
|
+
}
|
|
4126
|
+
if (!st.isDirectory()) continue;
|
|
4127
|
+
const topCfg = join(fnDir, "config.yaml");
|
|
4128
|
+
let topIsLeafAgent = false;
|
|
4129
|
+
try {
|
|
4130
|
+
if (statSync(topCfg).isFile()) {
|
|
4131
|
+
out.push(top);
|
|
4132
|
+
topIsLeafAgent = true;
|
|
4133
|
+
}
|
|
4134
|
+
} catch {}
|
|
4135
|
+
if (topIsLeafAgent) continue;
|
|
4136
|
+
let children;
|
|
4137
|
+
try {
|
|
4138
|
+
children = readdirSync(fnDir);
|
|
4139
|
+
} catch {
|
|
4140
|
+
continue;
|
|
4141
|
+
}
|
|
4142
|
+
for (const child of children) {
|
|
4143
|
+
if (child.startsWith(".")) continue;
|
|
4144
|
+
if (!AGENT_SEGMENT_RE.test(child)) continue;
|
|
4145
|
+
const agentDir = join(fnDir, child);
|
|
4146
|
+
let agentSt;
|
|
4147
|
+
try {
|
|
4148
|
+
agentSt = statSync(agentDir);
|
|
4149
|
+
} catch {
|
|
4150
|
+
continue;
|
|
4151
|
+
}
|
|
4152
|
+
if (!agentSt.isDirectory()) continue;
|
|
4153
|
+
const cfg = join(agentDir, "config.yaml");
|
|
4154
|
+
try {
|
|
4155
|
+
if (statSync(cfg).isFile()) out.push(top + "/" + child);
|
|
4156
|
+
} catch {
|
|
4157
|
+
continue;
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
out.sort();
|
|
4162
|
+
return out;
|
|
4163
|
+
}
|
|
4164
|
+
function auditAgentEnvRefs(cwd) {
|
|
4165
|
+
const errors = [];
|
|
4166
|
+
const warns = [];
|
|
4167
|
+
for (const agent of listV1AgentPaths(cwd)) {
|
|
4168
|
+
const loaded = loadAgentConfig(cwd, agent);
|
|
4169
|
+
if (!loaded.ok) continue;
|
|
4170
|
+
const tools = loaded.config.tools ?? {};
|
|
4171
|
+
const resolved = resolveAgentEnv(cwd, agent);
|
|
4172
|
+
for (const [binding, t] of Object.entries(tools)) {
|
|
4173
|
+
if (Object.prototype.hasOwnProperty.call(resolved, t.env_var)) continue;
|
|
4174
|
+
const miss = {
|
|
4175
|
+
agent,
|
|
4176
|
+
binding,
|
|
4177
|
+
key: t.env_var,
|
|
4178
|
+
required: t.required
|
|
4179
|
+
};
|
|
4180
|
+
if (t.required) errors.push(miss);
|
|
4181
|
+
else warns.push(miss);
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
return {
|
|
4185
|
+
status: errors.length > 0 ? "fail" : warns.length > 0 ? "warn" : "ok",
|
|
4186
|
+
errors,
|
|
4187
|
+
warns
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
3531
4190
|
function runSecretsAudit(opts) {
|
|
3532
4191
|
const envPermissions = auditEnvPermissions(opts.cwd);
|
|
4192
|
+
const agentEnvPermissions = auditAgentEnvPermissions(opts.cwd);
|
|
3533
4193
|
const envKeyReferences = auditEnvKeyReferences(opts.cwd);
|
|
3534
4194
|
const templateSecretLiterals = auditTemplateSecretLiterals(opts.rosterRoot);
|
|
3535
4195
|
const promptLeak = auditPromptLeak(opts.cwd, opts.schedules);
|
|
4196
|
+
const agentEnvRefs = auditAgentEnvRefs(opts.cwd);
|
|
4197
|
+
const agentEnvRedundancy = auditAgentEnvRedundancy(opts.cwd);
|
|
4198
|
+
const envOk = envPermissions.status === "ok" || envPermissions.status === "absent" || envPermissions.status === "skip-platform";
|
|
4199
|
+
const agentEnvOk = agentEnvPermissions.status !== "fail";
|
|
3536
4200
|
return {
|
|
3537
|
-
ok:
|
|
4201
|
+
ok: envOk && agentEnvOk && envKeyReferences.status === "ok" && templateSecretLiterals.status === "ok" && agentEnvRefs.errors.length === 0,
|
|
3538
4202
|
envPermissions,
|
|
4203
|
+
agentEnvPermissions,
|
|
3539
4204
|
envKeyReferences,
|
|
3540
4205
|
templateSecretLiterals,
|
|
3541
|
-
promptLeak
|
|
4206
|
+
promptLeak,
|
|
4207
|
+
agentEnvRefs,
|
|
4208
|
+
agentEnvRedundancy
|
|
3542
4209
|
};
|
|
3543
4210
|
}
|
|
3544
4211
|
//#endregion
|
|
4212
|
+
//#region src/lib/agent-env-fix-prompt.ts
|
|
4213
|
+
async function confirmAndDeleteRedundantLines(items, cwd, deps, dryRun) {
|
|
4214
|
+
const out = {
|
|
4215
|
+
deleted: [],
|
|
4216
|
+
failed: [],
|
|
4217
|
+
skipped: [],
|
|
4218
|
+
nonTtySkipped: false
|
|
4219
|
+
};
|
|
4220
|
+
if (items.length === 0) return out;
|
|
4221
|
+
if (!deps.isTTY) {
|
|
4222
|
+
out.nonTtySkipped = true;
|
|
4223
|
+
return out;
|
|
4224
|
+
}
|
|
4225
|
+
let workspaceMap = /* @__PURE__ */ new Map();
|
|
4226
|
+
try {
|
|
4227
|
+
workspaceMap = parseEnvFile(readFileSync(join(cwd, ".env"), "utf8"));
|
|
4228
|
+
} catch {}
|
|
4229
|
+
const rl = createInterface({
|
|
4230
|
+
input: deps.stdin,
|
|
4231
|
+
output: deps.stdout,
|
|
4232
|
+
terminal: false
|
|
4233
|
+
});
|
|
4234
|
+
let stdinClosed = false;
|
|
4235
|
+
rl.once("close", () => {
|
|
4236
|
+
stdinClosed = true;
|
|
4237
|
+
});
|
|
4238
|
+
try {
|
|
4239
|
+
for (const item of items) {
|
|
4240
|
+
const absPath = join(cwd, item.agentEnvPath);
|
|
4241
|
+
const label = `${item.agentEnvPath}:${item.line} ${item.key}`;
|
|
4242
|
+
if (stdinClosed) {
|
|
4243
|
+
out.skipped.push(`${label}: stdin closed`);
|
|
4244
|
+
continue;
|
|
4245
|
+
}
|
|
4246
|
+
deps.stdout.write(`Delete ${label}? [y/N] `);
|
|
4247
|
+
const answer = await readOneLine(rl);
|
|
4248
|
+
if (answer === null) {
|
|
4249
|
+
stdinClosed = true;
|
|
4250
|
+
out.skipped.push(`${label}: stdin closed`);
|
|
4251
|
+
continue;
|
|
4252
|
+
}
|
|
4253
|
+
const normalized = answer.trim().toLowerCase();
|
|
4254
|
+
if (normalized !== "y" && normalized !== "yes") {
|
|
4255
|
+
out.skipped.push(label);
|
|
4256
|
+
continue;
|
|
4257
|
+
}
|
|
4258
|
+
const expectedValue = workspaceMap.get(item.key);
|
|
4259
|
+
if (expectedValue === void 0) {
|
|
4260
|
+
out.failed.push({
|
|
4261
|
+
what: label,
|
|
4262
|
+
error: "workspace .env no longer declares this key"
|
|
4263
|
+
});
|
|
4264
|
+
continue;
|
|
4265
|
+
}
|
|
4266
|
+
const result = removeLineForKey(absPath, item.line, item.key, expectedValue, dryRun);
|
|
4267
|
+
if (result.kind === "removed") out.deleted.push(`${label}: removed`);
|
|
4268
|
+
else if (result.kind === "would-remove") out.deleted.push(`${label}: would remove (dry-run)`);
|
|
4269
|
+
else if (result.kind === "changed") out.failed.push({
|
|
4270
|
+
what: label,
|
|
4271
|
+
error: `file changed (${result.reason})`
|
|
4272
|
+
});
|
|
4273
|
+
else out.failed.push({
|
|
4274
|
+
what: label,
|
|
4275
|
+
error: result.message
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
4278
|
+
} finally {
|
|
4279
|
+
rl.close();
|
|
4280
|
+
}
|
|
4281
|
+
return out;
|
|
4282
|
+
}
|
|
4283
|
+
function readOneLine(rl) {
|
|
4284
|
+
return new Promise((resolve) => {
|
|
4285
|
+
let settled = false;
|
|
4286
|
+
const onLine = (line) => {
|
|
4287
|
+
if (settled) return;
|
|
4288
|
+
settled = true;
|
|
4289
|
+
rl.off("close", onClose);
|
|
4290
|
+
resolve(line);
|
|
4291
|
+
};
|
|
4292
|
+
const onClose = () => {
|
|
4293
|
+
if (settled) return;
|
|
4294
|
+
settled = true;
|
|
4295
|
+
rl.off("line", onLine);
|
|
4296
|
+
resolve(null);
|
|
4297
|
+
};
|
|
4298
|
+
rl.once("line", onLine);
|
|
4299
|
+
rl.once("close", onClose);
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
//#endregion
|
|
3545
4303
|
//#region src/lib/codex-preflight.ts
|
|
3546
4304
|
function readAuthJson(codexHome) {
|
|
3547
4305
|
const authPath = join(codexHome, "auth.json");
|
|
@@ -4512,7 +5270,29 @@ function runSchedulingDriftAudit(opts) {
|
|
|
4512
5270
|
}
|
|
4513
5271
|
//#endregion
|
|
4514
5272
|
//#region src/commands/doctor.ts
|
|
4515
|
-
function
|
|
5273
|
+
function listSkillDirNames(target) {
|
|
5274
|
+
try {
|
|
5275
|
+
return readdirSync(target, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
5276
|
+
} catch {
|
|
5277
|
+
return [];
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
function detectShadowCollisions(workspaceRoot) {
|
|
5281
|
+
const collisions = [];
|
|
5282
|
+
for (const userTool of allTools()) {
|
|
5283
|
+
const projectTool = toolForScope(userTool, "project", workspaceRoot);
|
|
5284
|
+
const userNames = new Set(listSkillDirNames(userTool.skillsTarget));
|
|
5285
|
+
const projectNames = listSkillDirNames(projectTool.skillsTarget);
|
|
5286
|
+
for (const name of projectNames) if (userNames.has(name)) collisions.push({
|
|
5287
|
+
tool: userTool.key,
|
|
5288
|
+
skillName: name,
|
|
5289
|
+
userPath: join(userTool.skillsTarget, name),
|
|
5290
|
+
projectPath: join(projectTool.skillsTarget, name)
|
|
5291
|
+
});
|
|
5292
|
+
}
|
|
5293
|
+
return collisions;
|
|
5294
|
+
}
|
|
5295
|
+
function tildify$3(path) {
|
|
4516
5296
|
const home = homedir();
|
|
4517
5297
|
return path.startsWith(home) ? "~" + path.slice(home.length) : path;
|
|
4518
5298
|
}
|
|
@@ -4553,7 +5333,7 @@ function workspaceLabel(status) {
|
|
|
4553
5333
|
function renderSchedulingSection(report) {
|
|
4554
5334
|
if (report.files.length === 0) return [];
|
|
4555
5335
|
const lines = [""];
|
|
4556
|
-
lines.push(`Scheduling ${tildify$
|
|
5336
|
+
lines.push(`Scheduling ${tildify$3(report.cwd)}`);
|
|
4557
5337
|
for (const file of report.files) if (file.status === "pass") {
|
|
4558
5338
|
const entryWord = file.entryCount === 1 ? "entry" : "entries";
|
|
4559
5339
|
lines.push(` ${chalk.green("✓")} ${file.relativePath} ${chalk.dim("OK")} ${chalk.dim(`(${file.entryCount} ${entryWord})`)}`);
|
|
@@ -4566,7 +5346,7 @@ function renderSchedulingSection(report) {
|
|
|
4566
5346
|
function renderWorkspaceSection(audit) {
|
|
4567
5347
|
if (!audit.contextMdExists && audit.items.length === 0) return [];
|
|
4568
5348
|
const lines = [""];
|
|
4569
|
-
lines.push(`Workspace ${tildify$
|
|
5349
|
+
lines.push(`Workspace ${tildify$3(audit.cwd)}`);
|
|
4570
5350
|
if (audit.contextMdExists) lines.push(` ${chalk.green("✓")} CONTEXT.md ${chalk.dim("present")}`);
|
|
4571
5351
|
else lines.push(` ${chalk.red("✗")} CONTEXT.md ${chalk.red("MISSING")}`);
|
|
4572
5352
|
for (const item of audit.items) {
|
|
@@ -4593,7 +5373,7 @@ const NO_FIX_REQUESTED = Object.freeze({
|
|
|
4593
5373
|
fixed: [],
|
|
4594
5374
|
failed: []
|
|
4595
5375
|
});
|
|
4596
|
-
function runFixes(cwd, workspace, envPerms, dryRun = false) {
|
|
5376
|
+
function runFixes(cwd, workspace, envPerms, agentEnvPerms, dryRun = false) {
|
|
4597
5377
|
const fixed = [];
|
|
4598
5378
|
const failed = [];
|
|
4599
5379
|
const applied = true;
|
|
@@ -4630,12 +5410,142 @@ function runFixes(cwd, workspace, envPerms, dryRun = false) {
|
|
|
4630
5410
|
error: err.message
|
|
4631
5411
|
});
|
|
4632
5412
|
}
|
|
5413
|
+
if (agentEnvPerms.status === "warn" || agentEnvPerms.status === "fail") for (const item of agentEnvPerms.items) {
|
|
5414
|
+
if (item.status === "ok") continue;
|
|
5415
|
+
const label = `${item.agentPath}/.env`;
|
|
5416
|
+
if (dryRun) {
|
|
5417
|
+
fixed.push(`${label}: would chmod 0600`);
|
|
5418
|
+
continue;
|
|
5419
|
+
}
|
|
5420
|
+
try {
|
|
5421
|
+
chmodSync(item.envPath, 384);
|
|
5422
|
+
fixed.push(`${label}: chmod 0600`);
|
|
5423
|
+
} catch (err) {
|
|
5424
|
+
failed.push({
|
|
5425
|
+
what: label,
|
|
5426
|
+
error: err.message
|
|
5427
|
+
});
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
4633
5430
|
return {
|
|
4634
5431
|
applied,
|
|
4635
5432
|
fixed,
|
|
4636
5433
|
failed
|
|
4637
5434
|
};
|
|
4638
5435
|
}
|
|
5436
|
+
function uniqueKeysFromRefs(refs) {
|
|
5437
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
5438
|
+
for (const m of [...refs.errors, ...refs.warns]) {
|
|
5439
|
+
const list = byKey.get(m.key);
|
|
5440
|
+
if (list) list.push(m);
|
|
5441
|
+
else byKey.set(m.key, [m]);
|
|
5442
|
+
}
|
|
5443
|
+
return Array.from(byKey.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([key, refs]) => ({
|
|
5444
|
+
key,
|
|
5445
|
+
refs
|
|
5446
|
+
}));
|
|
5447
|
+
}
|
|
5448
|
+
function appendKeysToEnvFile(cwd, keys) {
|
|
5449
|
+
const envPath = join(cwd, ".env");
|
|
5450
|
+
const added = [];
|
|
5451
|
+
const skipped = [];
|
|
5452
|
+
let existing = "";
|
|
5453
|
+
let hadFile = false;
|
|
5454
|
+
try {
|
|
5455
|
+
existing = readFileSync(envPath, "utf8");
|
|
5456
|
+
hadFile = true;
|
|
5457
|
+
} catch {}
|
|
5458
|
+
let existingKeys = /* @__PURE__ */ new Set();
|
|
5459
|
+
try {
|
|
5460
|
+
existingKeys = new Set(parseEnvKeys(existing));
|
|
5461
|
+
} catch {}
|
|
5462
|
+
const newLines = [];
|
|
5463
|
+
for (const key of keys) {
|
|
5464
|
+
if (existingKeys.has(key)) {
|
|
5465
|
+
skipped.push(key);
|
|
5466
|
+
continue;
|
|
5467
|
+
}
|
|
5468
|
+
newLines.push(`${key}=`);
|
|
5469
|
+
added.push(key);
|
|
5470
|
+
}
|
|
5471
|
+
if (newLines.length === 0) return {
|
|
5472
|
+
added,
|
|
5473
|
+
skipped
|
|
5474
|
+
};
|
|
5475
|
+
let body = existing;
|
|
5476
|
+
if (body.length > 0 && !body.endsWith("\n")) body += "\n";
|
|
5477
|
+
body += newLines.join("\n") + "\n";
|
|
5478
|
+
writeFileSync(envPath, body, { encoding: "utf8" });
|
|
5479
|
+
if (!hadFile && !isWindows()) try {
|
|
5480
|
+
chmodSync(envPath, 384);
|
|
5481
|
+
} catch {}
|
|
5482
|
+
return {
|
|
5483
|
+
added,
|
|
5484
|
+
skipped
|
|
5485
|
+
};
|
|
5486
|
+
}
|
|
5487
|
+
async function defaultAgentEnvPrompt(uniqueKeys) {
|
|
5488
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
5489
|
+
return checkbox({
|
|
5490
|
+
message: "Select env keys to append to /.env (empty value — fill in after):",
|
|
5491
|
+
choices: uniqueKeys.map(({ key, refs }) => {
|
|
5492
|
+
return {
|
|
5493
|
+
value: key,
|
|
5494
|
+
name: `${key} ${refs.some((r) => r.required) ? "(required)" : "(optional)"} ← ${refs.map((r) => r.agent).join(", ")}`
|
|
5495
|
+
};
|
|
5496
|
+
})
|
|
5497
|
+
});
|
|
5498
|
+
}
|
|
5499
|
+
async function applyAgentEnvFix(cwd, refs, opts) {
|
|
5500
|
+
const fixed = [];
|
|
5501
|
+
const failed = [];
|
|
5502
|
+
const uniqueKeys = uniqueKeysFromRefs(refs);
|
|
5503
|
+
if (uniqueKeys.length === 0) return {
|
|
5504
|
+
fixed,
|
|
5505
|
+
failed
|
|
5506
|
+
};
|
|
5507
|
+
if (opts.dryRun) {
|
|
5508
|
+
for (const { key, refs: agents } of uniqueKeys) {
|
|
5509
|
+
const agentList = agents.map((r) => r.agent).join(", ");
|
|
5510
|
+
fixed.push(`/.env: would append ${key}= ${chalk.dim(`(referenced by ${agentList})`)}`);
|
|
5511
|
+
}
|
|
5512
|
+
return {
|
|
5513
|
+
fixed,
|
|
5514
|
+
failed
|
|
5515
|
+
};
|
|
5516
|
+
}
|
|
5517
|
+
let selected;
|
|
5518
|
+
try {
|
|
5519
|
+
selected = await (opts.prompt ?? defaultAgentEnvPrompt)(uniqueKeys);
|
|
5520
|
+
} catch (err) {
|
|
5521
|
+
failed.push({
|
|
5522
|
+
what: "/.env (interactive prompt)",
|
|
5523
|
+
error: err.message
|
|
5524
|
+
});
|
|
5525
|
+
return {
|
|
5526
|
+
fixed,
|
|
5527
|
+
failed
|
|
5528
|
+
};
|
|
5529
|
+
}
|
|
5530
|
+
if (selected.length === 0) return {
|
|
5531
|
+
fixed,
|
|
5532
|
+
failed
|
|
5533
|
+
};
|
|
5534
|
+
try {
|
|
5535
|
+
const { added, skipped } = appendKeysToEnvFile(cwd, selected);
|
|
5536
|
+
for (const k of added) fixed.push(`/.env: appended ${k}=`);
|
|
5537
|
+
for (const k of skipped) fixed.push(`/.env: ${k} already present (skipped)`);
|
|
5538
|
+
} catch (err) {
|
|
5539
|
+
failed.push({
|
|
5540
|
+
what: "/.env",
|
|
5541
|
+
error: err.message
|
|
5542
|
+
});
|
|
5543
|
+
}
|
|
5544
|
+
return {
|
|
5545
|
+
fixed,
|
|
5546
|
+
failed
|
|
5547
|
+
};
|
|
5548
|
+
}
|
|
4639
5549
|
function renderFixSection(outcome) {
|
|
4640
5550
|
if (!outcome.applied) return [];
|
|
4641
5551
|
if (outcome.fixed.length === 0 && outcome.failed.length === 0) return [];
|
|
@@ -4707,10 +5617,15 @@ function renderSafetySection(audit) {
|
|
|
4707
5617
|
}
|
|
4708
5618
|
function renderSecretsSection(audit) {
|
|
4709
5619
|
const env = audit.envPermissions;
|
|
5620
|
+
const agentEnv = audit.agentEnvPermissions;
|
|
4710
5621
|
const refs = audit.envKeyReferences;
|
|
4711
5622
|
const templates = audit.templateSecretLiterals;
|
|
4712
5623
|
const leak = audit.promptLeak;
|
|
4713
|
-
|
|
5624
|
+
const agentRefs = audit.agentEnvRefs;
|
|
5625
|
+
const redundancy = audit.agentEnvRedundancy;
|
|
5626
|
+
const noEnv = env.status === "absent" || env.status === "skip-platform";
|
|
5627
|
+
const agentEnvClean = agentEnv.status === "skip-platform" || agentEnv.status === "ok" && agentEnv.items.length === 0;
|
|
5628
|
+
if (noEnv && agentEnvClean && refs.status === "ok" && templates.status === "ok" && leak.status === "ok" && agentRefs.status === "ok" && redundancy.status === "ok") return [];
|
|
4714
5629
|
const lines = [""];
|
|
4715
5630
|
lines.push(chalk.bold("Secrets"));
|
|
4716
5631
|
if (env.status === "ok") lines.push(` ${chalk.green("✓")} .env permissions ${chalk.dim("OK")} ${chalk.dim(`(${env.mode})`)}`);
|
|
@@ -4718,6 +5633,28 @@ function renderSecretsSection(audit) {
|
|
|
4718
5633
|
lines.push(` ${chalk.red("✗")} .env permissions ${chalk.red("FAIL")} ${chalk.dim(`(got ${env.mode}, expected ${env.expected})`)}`);
|
|
4719
5634
|
lines.push(` ${chalk.dim("→ Run `roster doctor --fix` to chmod 0600.")}`);
|
|
4720
5635
|
} else if (env.status === "skip-platform") lines.push(` ${chalk.dim("-")} .env permissions ${chalk.dim("SKIPPED")} ${chalk.dim("(windows mode bits not portable)")}`);
|
|
5636
|
+
if (agentEnv.status === "ok" && agentEnv.items.length > 0) lines.push(` ${chalk.green("✓")} agent .env perms ${chalk.dim("OK")} ${chalk.dim(`(${agentEnv.items.length} agent .env${agentEnv.items.length === 1 ? "" : "s"})`)}`);
|
|
5637
|
+
else if (agentEnv.status === "warn") {
|
|
5638
|
+
const warnCount = agentEnv.items.filter((i) => i.status === "warn").length;
|
|
5639
|
+
lines.push(` ${chalk.yellow("!")} agent .env perms ${chalk.yellow("WARN")} ${chalk.dim(`(${warnCount} agent .env${warnCount === 1 ? "" : "s"} not 0600)`)}`);
|
|
5640
|
+
for (const item of agentEnv.items.slice(0, 10)) {
|
|
5641
|
+
if (item.status === "ok") continue;
|
|
5642
|
+
lines.push(` ${chalk.yellow("-")} ${item.agentPath}/.env ${chalk.dim(`(got ${item.mode}, expected 0600)`)}`);
|
|
5643
|
+
}
|
|
5644
|
+
lines.push(` ${chalk.dim("→ Run `roster doctor --fix` to chmod 0600.")}`);
|
|
5645
|
+
} else if (agentEnv.status === "fail") {
|
|
5646
|
+
const failCount = agentEnv.items.filter((i) => i.status === "fail").length;
|
|
5647
|
+
const warnCount = agentEnv.items.filter((i) => i.status === "warn").length;
|
|
5648
|
+
const detail = failCount === 1 ? "1 world-writable" : `${failCount} world-writable`;
|
|
5649
|
+
const extra = warnCount > 0 ? `, ${warnCount} other not 0600` : "";
|
|
5650
|
+
lines.push(` ${chalk.red("✗")} agent .env perms ${chalk.red("FAIL")} ${chalk.dim(`(${detail}${extra})`)}`);
|
|
5651
|
+
for (const item of agentEnv.items.slice(0, 10)) {
|
|
5652
|
+
if (item.status === "ok") continue;
|
|
5653
|
+
const marker = item.status === "fail" ? chalk.red("-") : chalk.yellow("-");
|
|
5654
|
+
lines.push(` ${marker} ${item.agentPath}/.env ${chalk.dim(`(got ${item.mode}, expected 0600)`)}`);
|
|
5655
|
+
}
|
|
5656
|
+
lines.push(` ${chalk.dim("→ Run `roster doctor --fix` to chmod 0600.")}`);
|
|
5657
|
+
} else if (agentEnv.status === "skip-platform") lines.push(` ${chalk.dim("-")} agent .env perms ${chalk.dim("SKIPPED")} ${chalk.dim("(windows mode bits not portable)")}`);
|
|
4721
5658
|
if (refs.status === "fail") {
|
|
4722
5659
|
lines.push(` ${chalk.red("✗")} env-key refs ${chalk.red("FAIL")} ${chalk.dim(`(${refs.missing.length} unreferenced)`)}`);
|
|
4723
5660
|
for (const m of refs.missing.slice(0, 10)) {
|
|
@@ -4733,6 +5670,28 @@ function renderSecretsSection(audit) {
|
|
|
4733
5670
|
lines.push(` ${chalk.yellow("!")} prompt-leak ${chalk.yellow("WARN")} ${chalk.dim(`(${leak.items.length} reference${leak.items.length === 1 ? "" : "s"})`)}`);
|
|
4734
5671
|
for (const item of leak.items.slice(0, 5)) lines.push(` ${chalk.yellow("-")} ${item.schedule}: ${item.reference} ${chalk.dim("in " + item.file + ":" + item.line)}`);
|
|
4735
5672
|
}
|
|
5673
|
+
if (agentRefs.status === "fail" || agentRefs.status === "warn") {
|
|
5674
|
+
const e = agentRefs.errors.length;
|
|
5675
|
+
const w = agentRefs.warns.length;
|
|
5676
|
+
const counts = [];
|
|
5677
|
+
if (e > 0) counts.push(`${e} error${e === 1 ? "" : "s"}`);
|
|
5678
|
+
if (w > 0) counts.push(`${w} warn${w === 1 ? "" : "s"}`);
|
|
5679
|
+
const tag = agentRefs.status === "fail" ? chalk.red("FAIL") : chalk.yellow("WARN");
|
|
5680
|
+
const symbol = agentRefs.status === "fail" ? chalk.red("✗") : chalk.yellow("!");
|
|
5681
|
+
lines.push(` ${symbol} agent-env-refs ${tag} ${chalk.dim(`(${counts.join(", ")})`)}`);
|
|
5682
|
+
for (const m of agentRefs.errors.slice(0, 10)) lines.push(` ${chalk.red("-")} ${m.agent}: ${m.key} ${chalk.dim(`(tools.${m.binding}, required)`)}`);
|
|
5683
|
+
if (agentRefs.errors.length > 10) lines.push(` ${chalk.dim(`… (${agentRefs.errors.length - 10} more errors)`)}`);
|
|
5684
|
+
for (const m of agentRefs.warns.slice(0, 5)) lines.push(` ${chalk.yellow("-")} ${m.agent}: ${m.key} ${chalk.dim(`(tools.${m.binding}, optional)`)}`);
|
|
5685
|
+
if (agentRefs.warns.length > 5) lines.push(` ${chalk.dim(`… (${agentRefs.warns.length - 5} more warns)`)}`);
|
|
5686
|
+
if (e > 0 || w > 0) lines.push(` ${chalk.dim("→ Run `roster doctor --fix` to append missing keys to /.env.")}`);
|
|
5687
|
+
}
|
|
5688
|
+
if (redundancy.status === "warn") {
|
|
5689
|
+
const n = redundancy.items.length;
|
|
5690
|
+
lines.push(` ${chalk.yellow("!")} agent .env redundancy ${chalk.yellow("WARN")} ${chalk.dim(`(${n} entr${n === 1 ? "y" : "ies"})`)}`);
|
|
5691
|
+
for (const item of redundancy.items.slice(0, 10)) lines.push(` ${chalk.yellow("-")} ${item.agentEnvPath}:${item.line} ${item.key} ${chalk.dim("matches workspace .env")}`);
|
|
5692
|
+
if (n > 10) lines.push(` ${chalk.dim(`… (${n - 10} more)`)}`);
|
|
5693
|
+
lines.push(` ${chalk.dim("→ Run `roster doctor --fix` to prompt removal of redundant lines.")}`);
|
|
5694
|
+
}
|
|
4736
5695
|
return lines;
|
|
4737
5696
|
}
|
|
4738
5697
|
function renderStaleFiresSection(stale) {
|
|
@@ -4774,12 +5733,13 @@ function renderSchedulingDriftSection(audit) {
|
|
|
4774
5733
|
}
|
|
4775
5734
|
return lines;
|
|
4776
5735
|
}
|
|
4777
|
-
function renderText$1(results, summary, workarounds) {
|
|
5736
|
+
function renderText$1(results, summary, workarounds, scope) {
|
|
4778
5737
|
const lines = [""];
|
|
4779
5738
|
lines.push(chalk.bold("roster doctor"));
|
|
5739
|
+
lines.push(chalk.dim(`Install scope: ${scope} (${scope === "project" ? "workspace-local" : "home directory"})`));
|
|
4780
5740
|
for (const r of results) {
|
|
4781
5741
|
lines.push("");
|
|
4782
|
-
lines.push(`${chalk.bold(r.toolName)} ${chalk.dim(tildify$
|
|
5742
|
+
lines.push(`${chalk.bold(r.toolName)} ${chalk.dim(tildify$3(r.configRoot))}`);
|
|
4783
5743
|
for (const item of r.items) {
|
|
4784
5744
|
const kindCol = chalk.dim(item.kind.padEnd(5));
|
|
4785
5745
|
const nameCol = item.name.padEnd(22);
|
|
@@ -4803,8 +5763,38 @@ function renderText$1(results, summary, workarounds) {
|
|
|
4803
5763
|
}
|
|
4804
5764
|
return lines;
|
|
4805
5765
|
}
|
|
4806
|
-
function
|
|
4807
|
-
|
|
5766
|
+
function renderShadowSection(shadows) {
|
|
5767
|
+
if (shadows.length === 0) return [];
|
|
5768
|
+
const lines = [""];
|
|
5769
|
+
lines.push(chalk.bold("Shadow collisions") + chalk.dim(` (${shadows.length})`));
|
|
5770
|
+
lines.push(chalk.dim(" Same skill installed at both scopes. The user-scope copy wins;"));
|
|
5771
|
+
lines.push(chalk.dim(" the workspace skill is silently ignored. Remove one."));
|
|
5772
|
+
for (const s of shadows) {
|
|
5773
|
+
lines.push(` ${chalk.yellow("!")} ${chalk.bold(s.skillName)} ${chalk.dim("(")}${s.tool}${chalk.dim(")")}`);
|
|
5774
|
+
lines.push(` user: ${tildify$3(s.userPath)}`);
|
|
5775
|
+
lines.push(` project: ${s.projectPath}`);
|
|
5776
|
+
}
|
|
5777
|
+
return lines;
|
|
5778
|
+
}
|
|
5779
|
+
function renderInteractiveFixSection(outcome) {
|
|
5780
|
+
if (!(outcome.deleted.length > 0 || outcome.failed.length > 0 || outcome.skipped.length > 0 || outcome.nonTtySkipped)) return [];
|
|
5781
|
+
const lines = [""];
|
|
5782
|
+
lines.push(chalk.bold("agent .env redundancy --fix"));
|
|
5783
|
+
if (outcome.nonTtySkipped) {
|
|
5784
|
+
lines.push(` ${chalk.dim("-")} skipped — re-run \`roster doctor --fix\` in an interactive terminal`);
|
|
5785
|
+
return lines;
|
|
5786
|
+
}
|
|
5787
|
+
for (const f of outcome.deleted) lines.push(` ${chalk.green("✓")} ${f}`);
|
|
5788
|
+
for (const f of outcome.skipped) lines.push(` ${chalk.dim("-")} ${f}: kept`);
|
|
5789
|
+
for (const f of outcome.failed) lines.push(` ${chalk.red("✗")} ${f.what}: ${f.error}`);
|
|
5790
|
+
return lines;
|
|
5791
|
+
}
|
|
5792
|
+
async function executeDoctor(opts) {
|
|
5793
|
+
const workspaceExists = detectWorkspace(opts.cwd);
|
|
5794
|
+
const effectiveScope = opts.scope ?? defaultScopeForContext(workspaceExists);
|
|
5795
|
+
const userScopeTools = detectTools();
|
|
5796
|
+
const detected = effectiveScope === "project" ? allTools().map((t) => toolForScope(t, "project", opts.cwd)).filter((t) => existsSync(t.configRoot)) : userScopeTools;
|
|
5797
|
+
const shadows = workspaceExists ? detectShadowCollisions(opts.cwd) : [];
|
|
4808
5798
|
const sources = {
|
|
4809
5799
|
skills: join(ROSTER_ROOT, "skills"),
|
|
4810
5800
|
agents: join(ROSTER_ROOT, "agents")
|
|
@@ -4837,11 +5827,8 @@ function executeDoctor(opts) {
|
|
|
4837
5827
|
const workarounds = computeWorkarounds(results);
|
|
4838
5828
|
let workspaceFinal = workspace;
|
|
4839
5829
|
const initialEnvPerms = auditEnvPermissions(opts.cwd);
|
|
5830
|
+
const initialAgentEnvPerms = auditAgentEnvPermissions(opts.cwd);
|
|
4840
5831
|
let fixOutcome = NO_FIX_REQUESTED;
|
|
4841
|
-
if (opts.fix) {
|
|
4842
|
-
fixOutcome = runFixes(opts.cwd, workspace, initialEnvPerms, opts.dryRun);
|
|
4843
|
-
if (!opts.dryRun && fixOutcome.fixed.length > 0) workspaceFinal = auditWorkspace(opts.cwd);
|
|
4844
|
-
}
|
|
4845
5832
|
const schedulesForLeak = listAllScheduleEntries(opts.cwd);
|
|
4846
5833
|
const codexHomeOverride = process.env["ROSTER_CODEX_HOME"];
|
|
4847
5834
|
const safety = runSafetyAudit({
|
|
@@ -4852,20 +5839,70 @@ function executeDoctor(opts) {
|
|
|
4852
5839
|
env: process.env,
|
|
4853
5840
|
...codexHomeOverride !== void 0 && codexHomeOverride !== "" ? { codexHome: codexHomeOverride } : {}
|
|
4854
5841
|
});
|
|
4855
|
-
const
|
|
5842
|
+
const initialSecrets = runSecretsAudit({
|
|
4856
5843
|
cwd: opts.cwd,
|
|
4857
5844
|
rosterRoot: ROSTER_ROOT,
|
|
4858
5845
|
schedules: schedulesForLeak
|
|
4859
5846
|
});
|
|
5847
|
+
if (opts.fix) {
|
|
5848
|
+
fixOutcome = runFixes(opts.cwd, workspace, initialEnvPerms, initialAgentEnvPerms, opts.dryRun);
|
|
5849
|
+
const refs = initialSecrets.agentEnvRefs;
|
|
5850
|
+
const anyMissing = refs.errors.length > 0 || refs.warns.length > 0;
|
|
5851
|
+
const canInteract = !opts.json && process.stdin.isTTY === true;
|
|
5852
|
+
if (anyMissing) if (opts.dryRun) {
|
|
5853
|
+
const out = await applyAgentEnvFix(opts.cwd, refs, { dryRun: true });
|
|
5854
|
+
fixOutcome.fixed.push(...out.fixed);
|
|
5855
|
+
fixOutcome.failed.push(...out.failed);
|
|
5856
|
+
} else if (canInteract) {
|
|
5857
|
+
const out = await applyAgentEnvFix(opts.cwd, refs, { dryRun: false });
|
|
5858
|
+
fixOutcome.fixed.push(...out.fixed);
|
|
5859
|
+
fixOutcome.failed.push(...out.failed);
|
|
5860
|
+
} else {
|
|
5861
|
+
const reason = opts.json ? "interactive prompt suppressed under --json" : "no TTY (non-interactive shell)";
|
|
5862
|
+
fixOutcome.failed.push({
|
|
5863
|
+
what: "/.env (agent-env-refs)",
|
|
5864
|
+
error: `--fix skipped: ${reason}. Rerun in an interactive shell to append missing keys.`
|
|
5865
|
+
});
|
|
5866
|
+
}
|
|
5867
|
+
if (!opts.dryRun && fixOutcome.fixed.length > 0) workspaceFinal = auditWorkspace(opts.cwd);
|
|
5868
|
+
}
|
|
5869
|
+
const secrets = opts.fix && !opts.dryRun && fixOutcome.fixed.length > 0 ? runSecretsAudit({
|
|
5870
|
+
cwd: opts.cwd,
|
|
5871
|
+
rosterRoot: ROSTER_ROOT,
|
|
5872
|
+
schedules: schedulesForLeak
|
|
5873
|
+
}) : initialSecrets;
|
|
4860
5874
|
const schedulingDrift = runSchedulingDriftAudit({
|
|
4861
5875
|
cwd: opts.cwd,
|
|
4862
5876
|
homeDir: home
|
|
4863
5877
|
});
|
|
4864
5878
|
const allOk = results.every((r) => r.ok) && workspaceFinal.ok && scheduling.ok && safety.ok && secrets.ok && schedulingDrift.ok;
|
|
5879
|
+
if (!opts.json && !opts.silent) {
|
|
5880
|
+
for (const line of renderText$1(results, summary, workarounds, effectiveScope)) console.log(line);
|
|
5881
|
+
for (const line of renderShadowSection(shadows)) console.log(line);
|
|
5882
|
+
for (const line of renderWorkspaceSection(workspaceFinal)) console.log(line);
|
|
5883
|
+
for (const line of renderSchedulingSection(scheduling)) console.log(line);
|
|
5884
|
+
for (const line of renderSchedulingDriftSection(schedulingDrift)) console.log(line);
|
|
5885
|
+
for (const line of renderStaleFiresSection(schedulingDrift.staleFires)) console.log(line);
|
|
5886
|
+
for (const line of renderSafetySection(safety)) console.log(line);
|
|
5887
|
+
for (const line of renderSecretsSection(secrets)) console.log(line);
|
|
5888
|
+
for (const line of renderFixSection(fixOutcome)) console.log(line);
|
|
5889
|
+
}
|
|
5890
|
+
let interactiveOutcome = null;
|
|
5891
|
+
if (opts.fix && secrets.agentEnvRedundancy.items.length > 0) {
|
|
5892
|
+
const isTTY = !opts.silent && !opts.json && (process.stdin.isTTY ?? false);
|
|
5893
|
+
interactiveOutcome = await confirmAndDeleteRedundantLines(secrets.agentEnvRedundancy.items, opts.cwd, {
|
|
5894
|
+
isTTY,
|
|
5895
|
+
stdin: process.stdin,
|
|
5896
|
+
stdout: process.stdout
|
|
5897
|
+
}, opts.dryRun);
|
|
5898
|
+
if (!opts.json && !opts.silent) for (const line of renderInteractiveFixSection(interactiveOutcome)) console.log(line);
|
|
5899
|
+
}
|
|
4865
5900
|
if (opts.json) {
|
|
4866
5901
|
const payload = {
|
|
4867
5902
|
ok: allOk,
|
|
4868
5903
|
rosterVersion: getPackageVersion(),
|
|
5904
|
+
scope: effectiveScope,
|
|
5905
|
+
shadows,
|
|
4869
5906
|
tools: results,
|
|
4870
5907
|
summary,
|
|
4871
5908
|
workspace: workspaceFinal,
|
|
@@ -4874,20 +5911,11 @@ function executeDoctor(opts) {
|
|
|
4874
5911
|
safety,
|
|
4875
5912
|
secrets,
|
|
4876
5913
|
scheduling_drift: schedulingDrift,
|
|
4877
|
-
fix: fixOutcome
|
|
5914
|
+
fix: fixOutcome,
|
|
5915
|
+
interactive_fix: interactiveOutcome
|
|
4878
5916
|
};
|
|
4879
5917
|
console.log(JSON.stringify(payload, null, 2));
|
|
4880
|
-
} else if (!opts.silent)
|
|
4881
|
-
for (const line of renderText$1(results, summary, workarounds)) console.log(line);
|
|
4882
|
-
for (const line of renderWorkspaceSection(workspaceFinal)) console.log(line);
|
|
4883
|
-
for (const line of renderSchedulingSection(scheduling)) console.log(line);
|
|
4884
|
-
for (const line of renderSchedulingDriftSection(schedulingDrift)) console.log(line);
|
|
4885
|
-
for (const line of renderStaleFiresSection(schedulingDrift.staleFires)) console.log(line);
|
|
4886
|
-
for (const line of renderSafetySection(safety)) console.log(line);
|
|
4887
|
-
for (const line of renderSecretsSection(secrets)) console.log(line);
|
|
4888
|
-
for (const line of renderFixSection(fixOutcome)) console.log(line);
|
|
4889
|
-
if (opts.dryRun) console.log(chalk.dim(opts.fix ? "--dry-run: nothing applied; lines above are what `--fix` would have done." : "--dry-run: read-only audit; pass `--fix` to preview repairs."));
|
|
4890
|
-
}
|
|
5918
|
+
} else if (!opts.silent && opts.dryRun) console.log(chalk.dim(opts.fix ? "--dry-run: nothing applied; lines above are what `--fix` would have done." : "--dry-run: read-only audit; pass `--fix` to preview repairs."));
|
|
4891
5919
|
return allOk ? 0 : 1;
|
|
4892
5920
|
}
|
|
4893
5921
|
//#endregion
|
|
@@ -5152,7 +6180,7 @@ function buildListReport(cwd, now = /* @__PURE__ */ new Date()) {
|
|
|
5152
6180
|
warnings
|
|
5153
6181
|
};
|
|
5154
6182
|
}
|
|
5155
|
-
function tildify$
|
|
6183
|
+
function tildify$2(path) {
|
|
5156
6184
|
return path;
|
|
5157
6185
|
}
|
|
5158
6186
|
function fmtTs$1(d) {
|
|
@@ -5169,7 +6197,7 @@ function lastStatusCell(row) {
|
|
|
5169
6197
|
function renderListText(report) {
|
|
5170
6198
|
const lines = [""];
|
|
5171
6199
|
lines.push(chalk.bold("roster schedule list"));
|
|
5172
|
-
lines.push(chalk.dim(`cwd: ${tildify$
|
|
6200
|
+
lines.push(chalk.dim(`cwd: ${tildify$2(report.cwd)}`));
|
|
5173
6201
|
if (report.rows.length === 0) {
|
|
5174
6202
|
lines.push("");
|
|
5175
6203
|
lines.push(chalk.dim("(no schedules registered)"));
|
|
@@ -5815,7 +6843,7 @@ function renderClaudeHandoff(workspacePath, prompt, name, silent, dryRun) {
|
|
|
5815
6843
|
lines.push("");
|
|
5816
6844
|
return lines;
|
|
5817
6845
|
}
|
|
5818
|
-
function renderCodexBanner(workspacePath,
|
|
6846
|
+
function renderCodexBanner(workspacePath, name, installMode, silent) {
|
|
5819
6847
|
if (silent) return [];
|
|
5820
6848
|
return [
|
|
5821
6849
|
"",
|
|
@@ -5868,7 +6896,7 @@ async function executeRun(opts) {
|
|
|
5868
6896
|
let codexPath;
|
|
5869
6897
|
if (opts.dryRun) codexPath = opts.codexBinaryPathOverride ?? env["ROSTER_CODEX_PATH"] ?? "<codex on PATH>";
|
|
5870
6898
|
else codexPath = resolveCodexBinaryPath(env, opts.codexBinaryPathOverride);
|
|
5871
|
-
for (const line of renderCodexBanner(resolved.workspacePath,
|
|
6899
|
+
for (const line of renderCodexBanner(resolved.workspacePath, resolved.entry.name, resolved.entry.install_mode, opts.silent)) console.log(line);
|
|
5872
6900
|
if (opts.dryRun) {
|
|
5873
6901
|
if (!opts.silent) {
|
|
5874
6902
|
console.log(chalk.dim(`--dry-run: spawn skipped. Would run: ${codexPath} exec -C ${resolved.workspacePath} <prompt>`));
|
|
@@ -5911,7 +6939,7 @@ async function executeRun(opts) {
|
|
|
5911
6939
|
//#endregion
|
|
5912
6940
|
//#region src/commands/schedule.ts
|
|
5913
6941
|
const READONLY_DRYRUN_LINE = chalk.dim("--dry-run: read-only command; nothing would be written.");
|
|
5914
|
-
function tildify$
|
|
6942
|
+
function tildify$1(path) {
|
|
5915
6943
|
const home = homedir();
|
|
5916
6944
|
return path.startsWith(home) ? "~" + path.slice(home.length) : path;
|
|
5917
6945
|
}
|
|
@@ -5921,7 +6949,7 @@ function countTotalErrors(report) {
|
|
|
5921
6949
|
function renderText(report) {
|
|
5922
6950
|
const lines = [""];
|
|
5923
6951
|
lines.push(chalk.bold("roster schedule validate"));
|
|
5924
|
-
lines.push(chalk.dim(`cwd: ${tildify$
|
|
6952
|
+
lines.push(chalk.dim(`cwd: ${tildify$1(report.cwd)}`));
|
|
5925
6953
|
if (report.files.length === 0) {
|
|
5926
6954
|
lines.push("");
|
|
5927
6955
|
lines.push(chalk.dim("No roster/<function>/schedules.yaml files found."));
|
|
@@ -6592,14 +7620,14 @@ function installHook(host) {
|
|
|
6592
7620
|
}
|
|
6593
7621
|
//#endregion
|
|
6594
7622
|
//#region src/commands/hooks.ts
|
|
6595
|
-
function tildify
|
|
7623
|
+
function tildify(path) {
|
|
6596
7624
|
const home = homedir();
|
|
6597
7625
|
return path.startsWith(home) ? "~" + path.slice(home.length) : path;
|
|
6598
7626
|
}
|
|
6599
7627
|
function summarize(result) {
|
|
6600
7628
|
const hostLabel = result.host === "claude" ? "Claude Code" : "Codex CLI";
|
|
6601
|
-
if (result.status === "installed") return `${chalk.green("✓")} ${chalk.bold(hostLabel)} — SessionStart hook → ${tildify
|
|
6602
|
-
if (result.status === "already-present") return `${chalk.dim("·")} ${chalk.bold(hostLabel)} — SessionStart hook already installed (${tildify
|
|
7629
|
+
if (result.status === "installed") return `${chalk.green("✓")} ${chalk.bold(hostLabel)} — SessionStart hook → ${tildify(result.configFile)}`;
|
|
7630
|
+
if (result.status === "already-present") return `${chalk.dim("·")} ${chalk.bold(hostLabel)} — SessionStart hook already installed (${tildify(result.configFile)})`;
|
|
6603
7631
|
return `${chalk.yellow("⚠")} ${chalk.bold(hostLabel)} — skipped: ${result.reason ?? "host not detected"}`;
|
|
6604
7632
|
}
|
|
6605
7633
|
function hostsForTarget(target) {
|
|
@@ -7763,9 +8791,12 @@ const SUBCOMMANDS = new Set([
|
|
|
7763
8791
|
"migrate",
|
|
7764
8792
|
"pending"
|
|
7765
8793
|
]);
|
|
7766
|
-
function
|
|
8794
|
+
function displayPath(path, cwd) {
|
|
7767
8795
|
const home = homedir();
|
|
7768
|
-
|
|
8796
|
+
if (path.startsWith(home)) return "~" + path.slice(home.length);
|
|
8797
|
+
const rel = relative(cwd, path);
|
|
8798
|
+
if (!rel.startsWith("..") && !rel.startsWith("/")) return "./" + rel;
|
|
8799
|
+
return path;
|
|
7769
8800
|
}
|
|
7770
8801
|
function printBanner(version) {
|
|
7771
8802
|
console.log();
|
|
@@ -7798,8 +8829,10 @@ function printHelp(version) {
|
|
|
7798
8829
|
` -v, --version ${chalk.dim("Print version and exit")}`,
|
|
7799
8830
|
` --silent ${chalk.dim("Suppress non-error output (install)")}`,
|
|
7800
8831
|
` --verbose ${chalk.dim("Log each file path written (install)")}`,
|
|
7801
|
-
` --all ${chalk.dim("Install to every detected tool (install)")}`,
|
|
7802
|
-
` --tool <name>
|
|
8832
|
+
` --all ${chalk.dim("Install to every detected tool (alias of --tool all) (install)")}`,
|
|
8833
|
+
` --tool <name[,name...]> ${chalk.dim("Install to one or more tools: claude | codex | gemini (install)")}`,
|
|
8834
|
+
` --scope <project|user> ${chalk.dim("Install at workspace-local or home-dir scope (install)")}`,
|
|
8835
|
+
` --yes, -y ${chalk.dim("Skip prompts; use safe defaults (install)")}`,
|
|
7803
8836
|
` --tool <name> ${chalk.dim("Required scheduler tool: claude | codex (schedule install)")}`,
|
|
7804
8837
|
` --migrate ${chalk.dim("Upgrade pre-CONTEXT.md workspace, preserving CLAUDE.md content (init)")}`,
|
|
7805
8838
|
` --json ${chalk.dim("Emit machine-readable JSON (doctor, schedule validate)")}`,
|
|
@@ -7835,23 +8868,28 @@ function toolHints(tools) {
|
|
|
7835
8868
|
installLink: t.installLink
|
|
7836
8869
|
}));
|
|
7837
8870
|
}
|
|
7838
|
-
function summarizeInstall(tool, result) {
|
|
7839
|
-
const skillsLine = `${result.skillsCount} skills → ${
|
|
7840
|
-
const agentsLine = result.agentsTarget ? `${result.agentsCount} agents → ${
|
|
8871
|
+
function summarizeInstall(tool, result, cwd) {
|
|
8872
|
+
const skillsLine = `${result.skillsCount} skills → ${displayPath(result.skillsTarget, cwd)}`;
|
|
8873
|
+
const agentsLine = result.agentsTarget ? `${result.agentsCount} agents → ${displayPath(result.agentsTarget, cwd)}` : `${result.agentsCount} agents → (n/a)`;
|
|
7841
8874
|
return `${chalk.green("✓")} ${chalk.bold(tool.name)} — ${skillsLine}, ${agentsLine}`;
|
|
7842
8875
|
}
|
|
7843
|
-
async function promptForTools(detected) {
|
|
7844
|
-
if (detected.length === 1) return detected;
|
|
8876
|
+
async function promptForTools(detected, undetected) {
|
|
8877
|
+
if (detected.length === 1 && undetected.length === 0) return detected;
|
|
7845
8878
|
const { checkbox, confirm } = await import("@inquirer/prompts");
|
|
8879
|
+
const choices = [...detected.map((t) => ({
|
|
8880
|
+
name: t.name,
|
|
8881
|
+
value: t.key,
|
|
8882
|
+
checked: true
|
|
8883
|
+
})), ...undetected.map((t) => ({
|
|
8884
|
+
name: t.name,
|
|
8885
|
+
value: t.key,
|
|
8886
|
+
disabled: "(not detected)"
|
|
8887
|
+
}))];
|
|
7846
8888
|
let selectedKeys;
|
|
7847
8889
|
try {
|
|
7848
8890
|
selectedKeys = await checkbox({
|
|
7849
8891
|
message: "Install roster into which AI tools?",
|
|
7850
|
-
choices
|
|
7851
|
-
name: t.name,
|
|
7852
|
-
value: t.key,
|
|
7853
|
-
checked: true
|
|
7854
|
-
}))
|
|
8892
|
+
choices
|
|
7855
8893
|
});
|
|
7856
8894
|
} catch {
|
|
7857
8895
|
return null;
|
|
@@ -7867,10 +8905,31 @@ async function promptForTools(detected) {
|
|
|
7867
8905
|
return null;
|
|
7868
8906
|
}
|
|
7869
8907
|
if (exitAnyway) return null;
|
|
7870
|
-
return promptForTools(detected);
|
|
8908
|
+
return promptForTools(detected, undetected);
|
|
7871
8909
|
}
|
|
7872
8910
|
return detected.filter((t) => selectedKeys.includes(t.key));
|
|
7873
8911
|
}
|
|
8912
|
+
async function promptForScope(workspaceExists, cwd) {
|
|
8913
|
+
const { select } = await import("@inquirer/prompts");
|
|
8914
|
+
const projectHint = workspaceExists ? `workspace-local — skills land in ${displayPath(join(cwd, ".<tool>"), cwd)}/skills/` : "workspace-local — REQUIRES roster init (config/project.yaml not found here)";
|
|
8915
|
+
try {
|
|
8916
|
+
return await select({
|
|
8917
|
+
message: "Install at which scope?",
|
|
8918
|
+
choices: [{
|
|
8919
|
+
name: "project",
|
|
8920
|
+
value: "project",
|
|
8921
|
+
description: projectHint
|
|
8922
|
+
}, {
|
|
8923
|
+
name: "user",
|
|
8924
|
+
value: "user",
|
|
8925
|
+
description: "home directory — skills land in ~/.<tool>/, visible to every Claude Code project on this machine"
|
|
8926
|
+
}],
|
|
8927
|
+
default: workspaceExists ? "project" : "user"
|
|
8928
|
+
});
|
|
8929
|
+
} catch {
|
|
8930
|
+
return null;
|
|
8931
|
+
}
|
|
8932
|
+
}
|
|
7874
8933
|
async function runInstall(args) {
|
|
7875
8934
|
const parsed = parseInstallArgs(args);
|
|
7876
8935
|
if (parsed.kind === "err") throw new RosterError({
|
|
@@ -7879,39 +8938,58 @@ async function runInstall(args) {
|
|
|
7879
8938
|
remedy: ` Run ${chalk.bold("roster --help")} for usage.`,
|
|
7880
8939
|
exitCode: 1
|
|
7881
8940
|
});
|
|
7882
|
-
const { silent, verbose, target } = parsed;
|
|
8941
|
+
const { silent, verbose, yes, scope: requestedScope, target } = parsed;
|
|
7883
8942
|
const version = getPackageVersion();
|
|
7884
8943
|
if (!silent) printBanner(version);
|
|
8944
|
+
const cwd = process.cwd();
|
|
8945
|
+
const isTTY = process.stdin.isTTY === true;
|
|
8946
|
+
const workspaceExists = detectWorkspace(cwd);
|
|
8947
|
+
const nonInteractive = yes || !isTTY;
|
|
7885
8948
|
const detected = detectTools();
|
|
7886
|
-
if (detected.length === 0) throw noToolsError(toolHints(allTools()));
|
|
7887
8949
|
let targetTools;
|
|
7888
|
-
if (target.mode === "all")
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
targetTools =
|
|
7898
|
-
|
|
7899
|
-
|
|
8950
|
+
if (target.mode === "all") {
|
|
8951
|
+
if (detected.length === 0) throw noToolsError(toolHints(allTools()));
|
|
8952
|
+
targetTools = detected;
|
|
8953
|
+
} else if (target.mode === "tools") {
|
|
8954
|
+
const detectedKeys = detected.map((t) => t.key);
|
|
8955
|
+
if (target.keys.filter((k) => !detectedKeys.includes(k)).length > 0) throw toolsNotDetectedError(target.keys, detectedKeys);
|
|
8956
|
+
targetTools = detected.filter((t) => target.keys.includes(t.key));
|
|
8957
|
+
} else {
|
|
8958
|
+
if (detected.length === 0) throw noToolsError(toolHints(allTools()));
|
|
8959
|
+
if (nonInteractive) targetTools = detected;
|
|
8960
|
+
else {
|
|
8961
|
+
const picked = await promptForTools(detected, allTools().filter((t) => !detected.some((d) => d.key === t.key)));
|
|
8962
|
+
if (picked === null) throw userCancelledInstall();
|
|
8963
|
+
targetTools = picked;
|
|
8964
|
+
}
|
|
8965
|
+
}
|
|
8966
|
+
let scope;
|
|
8967
|
+
if (requestedScope !== null) scope = requestedScope;
|
|
8968
|
+
else if (nonInteractive) scope = defaultScopeForContext(workspaceExists);
|
|
8969
|
+
else {
|
|
8970
|
+
const picked = await promptForScope(workspaceExists, cwd);
|
|
8971
|
+
if (picked === null) throw userCancelledInstall();
|
|
8972
|
+
scope = picked;
|
|
8973
|
+
}
|
|
8974
|
+
if (scope === "project" && !workspaceExists) throw workspaceRequiredError(cwd);
|
|
7900
8975
|
const skillsSrc = join(ROSTER_ROOT, "skills");
|
|
7901
8976
|
const agentsSrc = join(ROSTER_ROOT, "agents");
|
|
7902
|
-
const confirmFn =
|
|
8977
|
+
const confirmFn = nonInteractive ? async () => false : void 0;
|
|
7903
8978
|
for (const tool of targetTools) {
|
|
7904
|
-
const
|
|
8979
|
+
const scopedTool = scope === "project" ? toolForScope(tool, "project", cwd) : tool;
|
|
8980
|
+
const result = await installToTool(scopedTool, {
|
|
7905
8981
|
skills: skillsSrc,
|
|
7906
8982
|
agents: agentsSrc,
|
|
7907
8983
|
silent: !verbose,
|
|
8984
|
+
scope,
|
|
7908
8985
|
...confirmFn ? { confirm: confirmFn } : {}
|
|
7909
8986
|
});
|
|
7910
|
-
if (!silent) console.log(summarizeInstall(
|
|
8987
|
+
if (!silent) console.log(summarizeInstall(scopedTool, result, cwd));
|
|
7911
8988
|
}
|
|
7912
8989
|
if (!silent) {
|
|
7913
8990
|
console.log();
|
|
7914
|
-
console.log(`${chalk.dim("Next: ")}${chalk.bold("
|
|
8991
|
+
if (scope === "project") console.log(`${chalk.dim("Next: ")}${chalk.bold("open Claude Code (or your AI tool) in this directory")}${chalk.dim(" — skills are workspace-local.")}`);
|
|
8992
|
+
else console.log(`${chalk.dim("Next: ")}${chalk.bold("roster init")}${chalk.dim(" to scaffold a workspace, then re-run install at project scope.")}`);
|
|
7915
8993
|
}
|
|
7916
8994
|
return 0;
|
|
7917
8995
|
}
|
|
@@ -8072,7 +9150,7 @@ async function runHooks(args) {
|
|
|
8072
9150
|
exitCode: 1
|
|
8073
9151
|
});
|
|
8074
9152
|
}
|
|
8075
|
-
function runDoctor(args) {
|
|
9153
|
+
async function runDoctor(args) {
|
|
8076
9154
|
const parsed = parseDoctorArgs(args);
|
|
8077
9155
|
if (parsed.kind === "err") throw new RosterError({
|
|
8078
9156
|
header: `${chalk.red.bold("roster:")} ${parsed.message}`,
|
|
@@ -8080,12 +9158,13 @@ function runDoctor(args) {
|
|
|
8080
9158
|
remedy: ` Run ${chalk.bold("roster --help")} for usage.`,
|
|
8081
9159
|
exitCode: 1
|
|
8082
9160
|
});
|
|
8083
|
-
const code = executeDoctor({
|
|
9161
|
+
const code = await executeDoctor({
|
|
8084
9162
|
json: parsed.json,
|
|
8085
9163
|
silent: parsed.silent,
|
|
8086
9164
|
fix: parsed.fix,
|
|
8087
9165
|
dryRun: parsed.dryRun,
|
|
8088
|
-
cwd: process.cwd()
|
|
9166
|
+
cwd: process.cwd(),
|
|
9167
|
+
scope: parsed.scope
|
|
8089
9168
|
});
|
|
8090
9169
|
if (code === 3 && !parsed.json) throw noToolsError(toolHints(allTools()));
|
|
8091
9170
|
return code;
|
|
@@ -8111,7 +9190,7 @@ async function main() {
|
|
|
8111
9190
|
if (isSubcommand(first)) {
|
|
8112
9191
|
if (first === "install") return runInstall(rest);
|
|
8113
9192
|
if (first === "init") return await runInit(rest);
|
|
8114
|
-
if (first === "doctor") return runDoctor(rest);
|
|
9193
|
+
if (first === "doctor") return await runDoctor(rest);
|
|
8115
9194
|
if (first === "schedule") return await runSchedule(rest);
|
|
8116
9195
|
if (first === "review") return await runReview(rest);
|
|
8117
9196
|
if (first === "hooks") return await runHooks(rest);
|