@imdeadpool/guardex 5.0.2 → 5.0.4
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/CONTRIBUTING.md +1 -0
- package/README.md +149 -303
- package/bin/multiagent-safety.js +532 -23
- package/package.json +2 -2
- package/templates/AGENTS.multiagent-safety.md +5 -3
- package/templates/githooks/pre-commit +9 -0
- package/templates/githooks/pre-push +19 -2
- package/templates/scripts/agent-branch-finish.sh +141 -8
- package/templates/scripts/agent-branch-start.sh +40 -6
- package/templates/scripts/agent-file-locks.py +1 -0
- package/templates/scripts/codex-agent.sh +21 -4
- package/templates/scripts/review-bot-watch.sh +330 -0
package/bin/multiagent-safety.js
CHANGED
|
@@ -15,6 +15,15 @@ const GLOBAL_TOOLCHAIN_PACKAGES = [
|
|
|
15
15
|
'@fission-ai/openspec',
|
|
16
16
|
'@imdeadpool/codex-account-switcher',
|
|
17
17
|
];
|
|
18
|
+
const GH_BIN = process.env.MUSAFETY_GH_BIN || 'gh';
|
|
19
|
+
const REQUIRED_SYSTEM_TOOLS = [
|
|
20
|
+
{
|
|
21
|
+
name: 'gh',
|
|
22
|
+
displayName: 'GitHub (gh)',
|
|
23
|
+
command: GH_BIN,
|
|
24
|
+
installHint: 'https://cli.github.com/',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
18
27
|
const MAINTAINER_RELEASE_REPO = path.resolve(
|
|
19
28
|
process.env.MUSAFETY_RELEASE_REPO || '/tmp/multiagent-safety',
|
|
20
29
|
);
|
|
@@ -33,11 +42,13 @@ const TEMPLATE_FILES = [
|
|
|
33
42
|
'scripts/agent-branch-start.sh',
|
|
34
43
|
'scripts/agent-branch-finish.sh',
|
|
35
44
|
'scripts/codex-agent.sh',
|
|
45
|
+
'scripts/review-bot-watch.sh',
|
|
36
46
|
'scripts/agent-worktree-prune.sh',
|
|
37
47
|
'scripts/agent-file-locks.py',
|
|
38
48
|
'scripts/install-agent-git-hooks.sh',
|
|
39
49
|
'scripts/openspec/init-plan-workspace.sh',
|
|
40
50
|
'githooks/pre-commit',
|
|
51
|
+
'githooks/pre-push',
|
|
41
52
|
'codex/skills/guardex/SKILL.md',
|
|
42
53
|
'claude/commands/guardex.md',
|
|
43
54
|
];
|
|
@@ -46,16 +57,19 @@ const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
|
46
57
|
'scripts/agent-branch-start.sh',
|
|
47
58
|
'scripts/agent-branch-finish.sh',
|
|
48
59
|
'scripts/codex-agent.sh',
|
|
60
|
+
'scripts/review-bot-watch.sh',
|
|
49
61
|
'scripts/agent-worktree-prune.sh',
|
|
50
62
|
'scripts/agent-file-locks.py',
|
|
51
63
|
'scripts/install-agent-git-hooks.sh',
|
|
52
64
|
'scripts/openspec/init-plan-workspace.sh',
|
|
53
65
|
'.githooks/pre-commit',
|
|
66
|
+
'.githooks/pre-push',
|
|
54
67
|
]);
|
|
55
68
|
|
|
56
69
|
const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
57
70
|
'AGENTS.md',
|
|
58
71
|
'.githooks/pre-commit',
|
|
72
|
+
'.githooks/pre-push',
|
|
59
73
|
'scripts/agent-branch-start.sh',
|
|
60
74
|
'scripts/agent-branch-finish.sh',
|
|
61
75
|
'scripts/agent-worktree-prune.sh',
|
|
@@ -71,11 +85,13 @@ const MANAGED_GITIGNORE_PATHS = [
|
|
|
71
85
|
'scripts/agent-branch-start.sh',
|
|
72
86
|
'scripts/agent-branch-finish.sh',
|
|
73
87
|
'scripts/codex-agent.sh',
|
|
88
|
+
'scripts/review-bot-watch.sh',
|
|
74
89
|
'scripts/agent-worktree-prune.sh',
|
|
75
90
|
'scripts/agent-file-locks.py',
|
|
76
91
|
'scripts/install-agent-git-hooks.sh',
|
|
77
92
|
'scripts/openspec/init-plan-workspace.sh',
|
|
78
93
|
'.githooks/pre-commit',
|
|
94
|
+
'.githooks/pre-push',
|
|
79
95
|
'oh-my-codex/',
|
|
80
96
|
'.codex/skills/guardex/SKILL.md',
|
|
81
97
|
'.claude/commands/guardex.md',
|
|
@@ -141,10 +157,12 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
|
|
|
141
157
|
gx setup
|
|
142
158
|
# alias: gx init
|
|
143
159
|
|
|
144
|
-
- Setup detects global OMX/OpenSpec/codex-auth first.
|
|
160
|
+
- Setup detects global OMX/OpenSpec/codex-auth npm packages first.
|
|
145
161
|
- If one is missing and setup asks for approval, reply explicitly:
|
|
146
162
|
- y = run: npm i -g oh-my-codex @fission-ai/openspec @imdeadpool/codex-account-switcher (missing ones only)
|
|
147
163
|
- n = skip global installs
|
|
164
|
+
- Setup also checks GitHub CLI (gh), required for PR/merge automation.
|
|
165
|
+
- If gh is missing: install it from https://cli.github.com/ and rerun gx setup.
|
|
148
166
|
|
|
149
167
|
3) If setup reports warnings/errors, repair + re-check:
|
|
150
168
|
gx doctor
|
|
@@ -176,6 +194,7 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
|
|
|
176
194
|
`;
|
|
177
195
|
|
|
178
196
|
const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
|
|
197
|
+
gh --version
|
|
179
198
|
gx setup
|
|
180
199
|
gx doctor
|
|
181
200
|
bash scripts/codex-agent.sh "task" "agent-name"
|
|
@@ -297,8 +316,9 @@ NOTES
|
|
|
297
316
|
- Short alias: ${SHORT_TOOL_NAME}
|
|
298
317
|
- ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup
|
|
299
318
|
- ${TOOL_NAME} setup asks for Y/N approval before global installs
|
|
319
|
+
- ${TOOL_NAME} setup checks GitHub CLI (gh) and prints install guidance if missing
|
|
300
320
|
- In initialized repos, setup/install/fix block in-place writes on protected main by default
|
|
301
|
-
- doctor auto-
|
|
321
|
+
- doctor auto-runs in a sandbox agent branch/worktree on protected main and tries auto-finish PR flow
|
|
302
322
|
- agent-branch-finish merges by default and keeps agent branches/worktrees until explicit cleanup
|
|
303
323
|
- use '${SHORT_TOOL_NAME} cleanup' to remove merged agent branches/worktrees (optionally remote refs too)
|
|
304
324
|
- Legacy command aliases are still supported: ${LEGACY_NAMES.join(', ')}`);
|
|
@@ -515,6 +535,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
515
535
|
|
|
516
536
|
const wantedScripts = {
|
|
517
537
|
'agent:codex': 'bash ./scripts/codex-agent.sh',
|
|
538
|
+
'agent:review:watch': 'bash ./scripts/review-bot-watch.sh',
|
|
518
539
|
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
|
|
519
540
|
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
|
|
520
541
|
'agent:cleanup': `${SHORT_TOOL_NAME} cleanup`,
|
|
@@ -694,18 +715,19 @@ function hasGuardexBootstrapFiles(repoRoot) {
|
|
|
694
715
|
'AGENTS.md',
|
|
695
716
|
'scripts/agent-branch-start.sh',
|
|
696
717
|
'.githooks/pre-commit',
|
|
718
|
+
'.githooks/pre-push',
|
|
697
719
|
LOCK_FILE_RELATIVE,
|
|
698
720
|
];
|
|
699
721
|
return required.every((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)));
|
|
700
722
|
}
|
|
701
723
|
|
|
702
|
-
function protectedBaseWriteBlock(options) {
|
|
724
|
+
function protectedBaseWriteBlock(options, { requireBootstrap = true } = {}) {
|
|
703
725
|
if (options.dryRun || options.allowProtectedBaseWrite) {
|
|
704
726
|
return null;
|
|
705
727
|
}
|
|
706
728
|
|
|
707
729
|
const repoRoot = resolveRepoRoot(options.target);
|
|
708
|
-
if (!hasGuardexBootstrapFiles(repoRoot)) {
|
|
730
|
+
if (requireBootstrap && !hasGuardexBootstrapFiles(repoRoot)) {
|
|
709
731
|
return null;
|
|
710
732
|
}
|
|
711
733
|
|
|
@@ -771,13 +793,96 @@ function buildSandboxDoctorArgs(options, sandboxTarget) {
|
|
|
771
793
|
return args;
|
|
772
794
|
}
|
|
773
795
|
|
|
774
|
-
function
|
|
796
|
+
function isSpawnFailure(result) {
|
|
797
|
+
return Boolean(result?.error) && typeof result?.status !== 'number';
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function doctorSandboxBranchPrefix() {
|
|
801
|
+
const now = new Date();
|
|
802
|
+
const stamp = [
|
|
803
|
+
now.getUTCFullYear(),
|
|
804
|
+
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
805
|
+
String(now.getUTCDate()).padStart(2, '0'),
|
|
806
|
+
].join('') + '-' + [
|
|
807
|
+
String(now.getUTCHours()).padStart(2, '0'),
|
|
808
|
+
String(now.getUTCMinutes()).padStart(2, '0'),
|
|
809
|
+
String(now.getUTCSeconds()).padStart(2, '0'),
|
|
810
|
+
].join('');
|
|
811
|
+
return `agent/gx/${stamp}`;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function doctorSandboxWorktreePath(repoRoot, branchName) {
|
|
815
|
+
return path.join(repoRoot, '.omx', 'agent-worktrees', branchName.replace(/\//g, '__'));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function gitRefExists(repoRoot, ref) {
|
|
819
|
+
return run('git', ['-C', repoRoot, 'show-ref', '--verify', '--quiet', ref]).status === 0;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function resolveDoctorSandboxStartRef(repoRoot, baseBranch) {
|
|
823
|
+
run('git', ['-C', repoRoot, 'fetch', 'origin', baseBranch, '--quiet'], { timeout: 20_000 });
|
|
824
|
+
if (gitRefExists(repoRoot, `refs/remotes/origin/${baseBranch}`)) {
|
|
825
|
+
return `origin/${baseBranch}`;
|
|
826
|
+
}
|
|
827
|
+
if (gitRefExists(repoRoot, `refs/heads/${baseBranch}`)) {
|
|
828
|
+
return baseBranch;
|
|
829
|
+
}
|
|
830
|
+
throw new Error(`Unable to find base ref for sandbox doctor: ${baseBranch}`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function startDoctorSandboxFallback(blocked) {
|
|
834
|
+
const branchPrefix = doctorSandboxBranchPrefix();
|
|
835
|
+
let selectedBranch = '';
|
|
836
|
+
let selectedWorktreePath = '';
|
|
837
|
+
|
|
838
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
839
|
+
const suffix = attempt === 0 ? 'gx-doctor' : `${attempt + 1}-gx-doctor`;
|
|
840
|
+
const candidateBranch = `${branchPrefix}-${suffix}`;
|
|
841
|
+
const candidateWorktreePath = doctorSandboxWorktreePath(blocked.repoRoot, candidateBranch);
|
|
842
|
+
if (gitRefExists(blocked.repoRoot, `refs/heads/${candidateBranch}`)) {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (fs.existsSync(candidateWorktreePath)) {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
selectedBranch = candidateBranch;
|
|
849
|
+
selectedWorktreePath = candidateWorktreePath;
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (!selectedBranch || !selectedWorktreePath) {
|
|
854
|
+
throw new Error('Unable to allocate unique sandbox branch/worktree for doctor');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
fs.mkdirSync(path.dirname(selectedWorktreePath), { recursive: true });
|
|
858
|
+
const startRef = resolveDoctorSandboxStartRef(blocked.repoRoot, blocked.branch);
|
|
859
|
+
const addResult = run(
|
|
860
|
+
'git',
|
|
861
|
+
['-C', blocked.repoRoot, 'worktree', 'add', '-b', selectedBranch, selectedWorktreePath, startRef],
|
|
862
|
+
);
|
|
863
|
+
if (isSpawnFailure(addResult)) {
|
|
864
|
+
throw addResult.error;
|
|
865
|
+
}
|
|
866
|
+
if (addResult.status !== 0) {
|
|
867
|
+
throw new Error((addResult.stderr || addResult.stdout || 'failed to create doctor sandbox').trim());
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return {
|
|
871
|
+
metadata: {
|
|
872
|
+
branch: selectedBranch,
|
|
873
|
+
worktreePath: selectedWorktreePath,
|
|
874
|
+
},
|
|
875
|
+
stdout:
|
|
876
|
+
`[agent-branch-start] Created branch: ${selectedBranch}\n` +
|
|
877
|
+
`[agent-branch-start] Worktree: ${selectedWorktreePath}\n`,
|
|
878
|
+
stderr: addResult.stderr || '',
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function startDoctorSandbox(blocked) {
|
|
775
883
|
const startScript = path.join(blocked.repoRoot, 'scripts', 'agent-branch-start.sh');
|
|
776
884
|
if (!fs.existsSync(startScript)) {
|
|
777
|
-
|
|
778
|
-
`doctor sandbox fallback is unavailable because '${startScript}' is missing.\n` +
|
|
779
|
-
`Run '${SHORT_TOOL_NAME} setup --allow-protected-base-write' once to restore branch-start tooling.`,
|
|
780
|
-
);
|
|
885
|
+
return startDoctorSandboxFallback(blocked);
|
|
781
886
|
}
|
|
782
887
|
|
|
783
888
|
const startResult = run('bash', [
|
|
@@ -789,7 +894,7 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
789
894
|
'--base',
|
|
790
895
|
blocked.branch,
|
|
791
896
|
], { cwd: blocked.repoRoot });
|
|
792
|
-
if (startResult
|
|
897
|
+
if (isSpawnFailure(startResult)) {
|
|
793
898
|
throw startResult.error;
|
|
794
899
|
}
|
|
795
900
|
if (startResult.status !== 0) {
|
|
@@ -801,18 +906,308 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
801
906
|
throw new Error(`Failed to parse sandbox worktree from agent-branch-start output:\n${startResult.stdout}`);
|
|
802
907
|
}
|
|
803
908
|
|
|
909
|
+
return {
|
|
910
|
+
metadata,
|
|
911
|
+
stdout: startResult.stdout || '',
|
|
912
|
+
stderr: startResult.stderr || '',
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function parseGitPathList(output) {
|
|
917
|
+
return String(output || '')
|
|
918
|
+
.split('\n')
|
|
919
|
+
.map((line) => line.trim())
|
|
920
|
+
.filter((line) => line && line !== LOCK_FILE_RELATIVE);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function collectDoctorChangedPaths(worktreePath) {
|
|
924
|
+
const changed = new Set();
|
|
925
|
+
const commands = [
|
|
926
|
+
['diff', '--name-only'],
|
|
927
|
+
['diff', '--cached', '--name-only'],
|
|
928
|
+
['ls-files', '--others', '--exclude-standard'],
|
|
929
|
+
];
|
|
930
|
+
for (const gitArgs of commands) {
|
|
931
|
+
const result = run('git', ['-C', worktreePath, ...gitArgs], { timeout: 20_000 });
|
|
932
|
+
for (const filePath of parseGitPathList(result.stdout)) {
|
|
933
|
+
changed.add(filePath);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return Array.from(changed);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function collectDoctorDeletedPaths(worktreePath) {
|
|
940
|
+
const deleted = new Set();
|
|
941
|
+
const commands = [
|
|
942
|
+
['diff', '--name-only', '--diff-filter=D'],
|
|
943
|
+
['diff', '--cached', '--name-only', '--diff-filter=D'],
|
|
944
|
+
];
|
|
945
|
+
for (const gitArgs of commands) {
|
|
946
|
+
const result = run('git', ['-C', worktreePath, ...gitArgs], { timeout: 20_000 });
|
|
947
|
+
for (const filePath of parseGitPathList(result.stdout)) {
|
|
948
|
+
deleted.add(filePath);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return Array.from(deleted);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function claimDoctorChangedLocks(metadata) {
|
|
955
|
+
const lockScript = path.join(metadata.worktreePath, 'scripts', 'agent-file-locks.py');
|
|
956
|
+
if (!fs.existsSync(lockScript) || !metadata.branch) {
|
|
957
|
+
return {
|
|
958
|
+
status: 'skipped',
|
|
959
|
+
note: 'lock helper unavailable in sandbox',
|
|
960
|
+
changedCount: 0,
|
|
961
|
+
deletedCount: 0,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const changedPaths = collectDoctorChangedPaths(metadata.worktreePath);
|
|
966
|
+
const deletedPaths = collectDoctorDeletedPaths(metadata.worktreePath);
|
|
967
|
+
if (changedPaths.length > 0) {
|
|
968
|
+
run('python3', [lockScript, 'claim', '--branch', metadata.branch, ...changedPaths], {
|
|
969
|
+
cwd: metadata.worktreePath,
|
|
970
|
+
timeout: 30_000,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
if (deletedPaths.length > 0) {
|
|
974
|
+
run('python3', [lockScript, 'allow-delete', '--branch', metadata.branch, ...deletedPaths], {
|
|
975
|
+
cwd: metadata.worktreePath,
|
|
976
|
+
timeout: 30_000,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return {
|
|
981
|
+
status: 'claimed',
|
|
982
|
+
note: 'claimed locks for doctor auto-commit',
|
|
983
|
+
changedCount: changedPaths.length,
|
|
984
|
+
deletedCount: deletedPaths.length,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function autoCommitDoctorSandboxChanges(metadata) {
|
|
989
|
+
if (!metadata.worktreePath || !metadata.branch) {
|
|
990
|
+
return {
|
|
991
|
+
status: 'skipped',
|
|
992
|
+
note: 'missing sandbox branch metadata',
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
claimDoctorChangedLocks(metadata);
|
|
997
|
+
run('git', ['-C', metadata.worktreePath, 'add', '-A'], { timeout: 20_000 });
|
|
998
|
+
const staged = run(
|
|
999
|
+
'git',
|
|
1000
|
+
['-C', metadata.worktreePath, 'diff', '--cached', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`],
|
|
1001
|
+
{ timeout: 20_000 },
|
|
1002
|
+
);
|
|
1003
|
+
const stagedFiles = parseGitPathList(staged.stdout);
|
|
1004
|
+
if (stagedFiles.length === 0) {
|
|
1005
|
+
return {
|
|
1006
|
+
status: 'no-changes',
|
|
1007
|
+
note: 'no committable doctor changes found in sandbox',
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const commitResult = run(
|
|
1012
|
+
'git',
|
|
1013
|
+
['-C', metadata.worktreePath, 'commit', '-m', 'Auto-finish: gx doctor repairs'],
|
|
1014
|
+
{ timeout: 30_000 },
|
|
1015
|
+
);
|
|
1016
|
+
if (commitResult.status !== 0) {
|
|
1017
|
+
return {
|
|
1018
|
+
status: 'failed',
|
|
1019
|
+
note: 'doctor sandbox auto-commit failed',
|
|
1020
|
+
stdout: commitResult.stdout || '',
|
|
1021
|
+
stderr: commitResult.stderr || '',
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
return {
|
|
1026
|
+
status: 'committed',
|
|
1027
|
+
note: 'doctor sandbox repairs committed',
|
|
1028
|
+
commitMessage: 'Auto-finish: gx doctor repairs',
|
|
1029
|
+
stagedFiles,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function hasOriginRemote(repoRoot) {
|
|
1034
|
+
return run('git', ['-C', repoRoot, 'remote', 'get-url', 'origin']).status === 0;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function isCommandAvailable(commandName) {
|
|
1038
|
+
return run('which', [commandName]).status === 0;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function finishDoctorSandboxBranch(blocked, metadata) {
|
|
1042
|
+
const finishScript = path.join(metadata.worktreePath, 'scripts', 'agent-branch-finish.sh');
|
|
1043
|
+
if (!fs.existsSync(finishScript)) {
|
|
1044
|
+
return {
|
|
1045
|
+
status: 'skipped',
|
|
1046
|
+
note: `${path.relative(metadata.worktreePath, finishScript)} missing in sandbox`,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
if (!hasOriginRemote(blocked.repoRoot)) {
|
|
1050
|
+
return {
|
|
1051
|
+
status: 'skipped',
|
|
1052
|
+
note: 'origin remote missing; skipped auto-finish',
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const ghBin = process.env.MUSAFETY_GH_BIN || 'gh';
|
|
1057
|
+
if (!isCommandAvailable(ghBin)) {
|
|
1058
|
+
return {
|
|
1059
|
+
status: 'skipped',
|
|
1060
|
+
note: `'${ghBin}' not available; skipped auto-finish PR flow`,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
const ghAuthStatus = run(ghBin, ['auth', 'status'], { timeout: 20_000 });
|
|
1064
|
+
if (ghAuthStatus.status !== 0) {
|
|
1065
|
+
return {
|
|
1066
|
+
status: 'skipped',
|
|
1067
|
+
note: `'${ghBin}' auth unavailable; skipped auto-finish PR flow`,
|
|
1068
|
+
stderr: ghAuthStatus.stderr || '',
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const finishResult = run(
|
|
1073
|
+
'bash',
|
|
1074
|
+
[finishScript, '--branch', metadata.branch, '--via-pr'],
|
|
1075
|
+
{ cwd: metadata.worktreePath, timeout: 180_000 },
|
|
1076
|
+
);
|
|
1077
|
+
if (isSpawnFailure(finishResult)) {
|
|
1078
|
+
return {
|
|
1079
|
+
status: 'failed',
|
|
1080
|
+
note: 'doctor sandbox finish flow errored',
|
|
1081
|
+
stdout: finishResult.stdout || '',
|
|
1082
|
+
stderr: finishResult.stderr || '',
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
if (finishResult.status !== 0) {
|
|
1086
|
+
return {
|
|
1087
|
+
status: 'failed',
|
|
1088
|
+
note: 'doctor sandbox finish flow failed',
|
|
1089
|
+
stdout: finishResult.stdout || '',
|
|
1090
|
+
stderr: finishResult.stderr || '',
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
status: 'completed',
|
|
1096
|
+
note: 'doctor sandbox finish flow completed',
|
|
1097
|
+
stdout: finishResult.stdout || '',
|
|
1098
|
+
stderr: finishResult.stderr || '',
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function runDoctorInSandbox(options, blocked) {
|
|
1103
|
+
const startResult = startDoctorSandbox(blocked);
|
|
1104
|
+
const metadata = startResult.metadata;
|
|
1105
|
+
|
|
804
1106
|
const sandboxTarget = resolveSandboxTarget(blocked.repoRoot, metadata.worktreePath, options.target);
|
|
805
1107
|
const nestedResult = run(
|
|
806
1108
|
process.execPath,
|
|
807
1109
|
[__filename, ...buildSandboxDoctorArgs(options, sandboxTarget)],
|
|
808
1110
|
{ cwd: metadata.worktreePath },
|
|
809
1111
|
);
|
|
810
|
-
if (nestedResult
|
|
1112
|
+
if (isSpawnFailure(nestedResult)) {
|
|
811
1113
|
throw nestedResult.error;
|
|
812
1114
|
}
|
|
813
1115
|
|
|
1116
|
+
let autoCommitResult = {
|
|
1117
|
+
status: 'skipped',
|
|
1118
|
+
note: 'sandbox doctor did not complete successfully',
|
|
1119
|
+
};
|
|
1120
|
+
let finishResult = {
|
|
1121
|
+
status: 'skipped',
|
|
1122
|
+
note: 'sandbox doctor did not complete successfully',
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
let lockSyncResult = {
|
|
1126
|
+
status: 'skipped',
|
|
1127
|
+
note: 'sandbox doctor did not complete successfully',
|
|
1128
|
+
};
|
|
1129
|
+
if (nestedResult.status === 0) {
|
|
1130
|
+
if (!options.dryRun) {
|
|
1131
|
+
autoCommitResult = autoCommitDoctorSandboxChanges(metadata);
|
|
1132
|
+
if (autoCommitResult.status === 'committed') {
|
|
1133
|
+
finishResult = finishDoctorSandboxBranch(blocked, metadata);
|
|
1134
|
+
} else if (autoCommitResult.status === 'no-changes') {
|
|
1135
|
+
finishResult = {
|
|
1136
|
+
status: 'skipped',
|
|
1137
|
+
note: 'no doctor changes to auto-finish',
|
|
1138
|
+
};
|
|
1139
|
+
} else if (autoCommitResult.status !== 'failed') {
|
|
1140
|
+
finishResult = {
|
|
1141
|
+
status: 'skipped',
|
|
1142
|
+
note: 'auto-commit did not run',
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
} else {
|
|
1146
|
+
autoCommitResult = {
|
|
1147
|
+
status: 'skipped',
|
|
1148
|
+
note: 'dry-run skips doctor sandbox auto-commit',
|
|
1149
|
+
};
|
|
1150
|
+
finishResult = {
|
|
1151
|
+
status: 'skipped',
|
|
1152
|
+
note: 'dry-run skips doctor sandbox finish flow',
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const sandboxLockPath = path.join(metadata.worktreePath, LOCK_FILE_RELATIVE);
|
|
1157
|
+
const baseLockPath = path.join(blocked.repoRoot, LOCK_FILE_RELATIVE);
|
|
1158
|
+
if (!fs.existsSync(baseLockPath)) {
|
|
1159
|
+
lockSyncResult = {
|
|
1160
|
+
status: 'skipped',
|
|
1161
|
+
note: `${LOCK_FILE_RELATIVE} missing in protected base workspace`,
|
|
1162
|
+
};
|
|
1163
|
+
} else if (!fs.existsSync(sandboxLockPath)) {
|
|
1164
|
+
lockSyncResult = {
|
|
1165
|
+
status: 'skipped',
|
|
1166
|
+
note: `${LOCK_FILE_RELATIVE} missing in sandbox worktree`,
|
|
1167
|
+
};
|
|
1168
|
+
} else {
|
|
1169
|
+
const sourceContent = fs.readFileSync(sandboxLockPath, 'utf8');
|
|
1170
|
+
const destinationContent = fs.readFileSync(baseLockPath, 'utf8');
|
|
1171
|
+
if (sourceContent === destinationContent) {
|
|
1172
|
+
lockSyncResult = {
|
|
1173
|
+
status: 'unchanged',
|
|
1174
|
+
note: `${LOCK_FILE_RELATIVE} already in sync`,
|
|
1175
|
+
};
|
|
1176
|
+
} else {
|
|
1177
|
+
fs.mkdirSync(path.dirname(baseLockPath), { recursive: true });
|
|
1178
|
+
fs.writeFileSync(baseLockPath, sourceContent, 'utf8');
|
|
1179
|
+
lockSyncResult = {
|
|
1180
|
+
status: 'synced',
|
|
1181
|
+
note: `${LOCK_FILE_RELATIVE} synced from sandbox`,
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
814
1187
|
if (options.json) {
|
|
815
|
-
if (nestedResult.stdout)
|
|
1188
|
+
if (nestedResult.stdout) {
|
|
1189
|
+
if (nestedResult.status === 0) {
|
|
1190
|
+
try {
|
|
1191
|
+
const parsed = JSON.parse(nestedResult.stdout);
|
|
1192
|
+
process.stdout.write(
|
|
1193
|
+
JSON.stringify(
|
|
1194
|
+
{
|
|
1195
|
+
...parsed,
|
|
1196
|
+
sandboxLockSync: lockSyncResult,
|
|
1197
|
+
sandboxAutoCommit: autoCommitResult,
|
|
1198
|
+
sandboxFinish: finishResult,
|
|
1199
|
+
},
|
|
1200
|
+
null,
|
|
1201
|
+
2,
|
|
1202
|
+
) + '\n',
|
|
1203
|
+
);
|
|
1204
|
+
} catch {
|
|
1205
|
+
process.stdout.write(nestedResult.stdout);
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
process.stdout.write(nestedResult.stdout);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
816
1211
|
if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
|
|
817
1212
|
} else {
|
|
818
1213
|
console.log(
|
|
@@ -823,6 +1218,41 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
823
1218
|
if (startResult.stderr) process.stderr.write(startResult.stderr);
|
|
824
1219
|
if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
|
|
825
1220
|
if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
|
|
1221
|
+
if (nestedResult.status === 0) {
|
|
1222
|
+
if (autoCommitResult.status === 'committed') {
|
|
1223
|
+
console.log(
|
|
1224
|
+
`[${TOOL_NAME}] Auto-committed doctor repairs in sandbox branch '${metadata.branch}'.`,
|
|
1225
|
+
);
|
|
1226
|
+
} else if (autoCommitResult.status === 'failed') {
|
|
1227
|
+
console.log(`[${TOOL_NAME}] Doctor sandbox auto-commit failed; branch left for manual follow-up.`);
|
|
1228
|
+
if (autoCommitResult.stdout) process.stdout.write(autoCommitResult.stdout);
|
|
1229
|
+
if (autoCommitResult.stderr) process.stderr.write(autoCommitResult.stderr);
|
|
1230
|
+
} else {
|
|
1231
|
+
console.log(`[${TOOL_NAME}] Doctor sandbox auto-commit skipped: ${autoCommitResult.note}.`);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
if (finishResult.status === 'completed') {
|
|
1235
|
+
console.log(`[${TOOL_NAME}] Auto-finish flow completed for sandbox branch '${metadata.branch}'.`);
|
|
1236
|
+
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
1237
|
+
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
1238
|
+
} else if (finishResult.status === 'failed') {
|
|
1239
|
+
console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
|
|
1240
|
+
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
1241
|
+
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
1242
|
+
} else {
|
|
1243
|
+
console.log(`[${TOOL_NAME}] Auto-finish skipped: ${finishResult.note}.`);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (lockSyncResult.status === 'synced') {
|
|
1247
|
+
console.log(
|
|
1248
|
+
`[${TOOL_NAME}] Synced repaired lock registry back to protected branch workspace (${LOCK_FILE_RELATIVE}).`,
|
|
1249
|
+
);
|
|
1250
|
+
} else if (lockSyncResult.status === 'unchanged') {
|
|
1251
|
+
console.log(`[${TOOL_NAME}] Lock registry already synced in protected branch workspace.`);
|
|
1252
|
+
} else {
|
|
1253
|
+
console.log(`[${TOOL_NAME}] Lock registry sync skipped: ${lockSyncResult.note}.`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
826
1256
|
}
|
|
827
1257
|
|
|
828
1258
|
if (typeof nestedResult.status === 'number') {
|
|
@@ -1361,6 +1791,12 @@ function isInteractiveTerminal() {
|
|
|
1361
1791
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1362
1792
|
}
|
|
1363
1793
|
|
|
1794
|
+
const stdinWaitArray = new Int32Array(new SharedArrayBuffer(4));
|
|
1795
|
+
|
|
1796
|
+
function sleepSyncMs(milliseconds) {
|
|
1797
|
+
Atomics.wait(stdinWaitArray, 0, 0, milliseconds);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1364
1800
|
function readSingleLineFromStdin() {
|
|
1365
1801
|
let input = '';
|
|
1366
1802
|
const buffer = Buffer.alloc(1);
|
|
@@ -1369,11 +1805,19 @@ function readSingleLineFromStdin() {
|
|
|
1369
1805
|
let bytesRead = 0;
|
|
1370
1806
|
try {
|
|
1371
1807
|
bytesRead = fs.readSync(process.stdin.fd, buffer, 0, 1);
|
|
1372
|
-
} catch {
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
if (error && ['EAGAIN', 'EWOULDBLOCK', 'EINTR'].includes(error.code)) {
|
|
1810
|
+
sleepSyncMs(15);
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1373
1813
|
return input;
|
|
1374
1814
|
}
|
|
1375
1815
|
|
|
1376
1816
|
if (bytesRead === 0) {
|
|
1817
|
+
if (process.stdin.isTTY) {
|
|
1818
|
+
sleepSyncMs(15);
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1377
1821
|
return input;
|
|
1378
1822
|
}
|
|
1379
1823
|
|
|
@@ -1513,9 +1957,8 @@ function maybeSelfUpdateBeforeStatus() {
|
|
|
1513
1957
|
}
|
|
1514
1958
|
|
|
1515
1959
|
const shouldUpdate = interactive
|
|
1516
|
-
?
|
|
1960
|
+
? promptYesNoStrict(
|
|
1517
1961
|
`Update now? (${NPM_BIN} i -g ${packageJson.name}@latest)`,
|
|
1518
|
-
false,
|
|
1519
1962
|
)
|
|
1520
1963
|
: autoApproval;
|
|
1521
1964
|
|
|
@@ -1608,6 +2051,27 @@ function detectGlobalToolchainPackages() {
|
|
|
1608
2051
|
return { ok: true, installed, missing };
|
|
1609
2052
|
}
|
|
1610
2053
|
|
|
2054
|
+
function detectRequiredSystemTools() {
|
|
2055
|
+
const services = [];
|
|
2056
|
+
for (const tool of REQUIRED_SYSTEM_TOOLS) {
|
|
2057
|
+
const result = run(tool.command, ['--version']);
|
|
2058
|
+
const active = result.status === 0;
|
|
2059
|
+
const rawReason = result.error && result.error.code
|
|
2060
|
+
? result.error.code
|
|
2061
|
+
: (result.stderr || '').trim();
|
|
2062
|
+
const reason = rawReason.split('\n')[0] || '';
|
|
2063
|
+
services.push({
|
|
2064
|
+
name: tool.name,
|
|
2065
|
+
displayName: tool.displayName || tool.name,
|
|
2066
|
+
command: tool.command,
|
|
2067
|
+
installHint: tool.installHint,
|
|
2068
|
+
status: active ? 'active' : 'inactive',
|
|
2069
|
+
reason,
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
return services;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
1611
2075
|
function askGlobalInstallForMissing(options, missingPackages) {
|
|
1612
2076
|
const approval = resolveGlobalInstallApproval(options);
|
|
1613
2077
|
if (!approval.approved) {
|
|
@@ -1937,7 +2401,7 @@ function status(rawArgs) {
|
|
|
1937
2401
|
});
|
|
1938
2402
|
|
|
1939
2403
|
const toolchain = detectGlobalToolchainPackages();
|
|
1940
|
-
const
|
|
2404
|
+
const npmServices = GLOBAL_TOOLCHAIN_PACKAGES.map((pkg) => {
|
|
1941
2405
|
if (!toolchain.ok) {
|
|
1942
2406
|
return { name: pkg, status: 'unknown' };
|
|
1943
2407
|
}
|
|
@@ -1946,6 +2410,15 @@ function status(rawArgs) {
|
|
|
1946
2410
|
status: toolchain.installed.includes(pkg) ? 'active' : 'inactive',
|
|
1947
2411
|
};
|
|
1948
2412
|
});
|
|
2413
|
+
const requiredSystemTools = detectRequiredSystemTools();
|
|
2414
|
+
const services = [
|
|
2415
|
+
...npmServices,
|
|
2416
|
+
...requiredSystemTools.map((tool) => ({
|
|
2417
|
+
name: tool.name,
|
|
2418
|
+
displayName: tool.displayName || tool.name,
|
|
2419
|
+
status: tool.status,
|
|
2420
|
+
})),
|
|
2421
|
+
];
|
|
1949
2422
|
|
|
1950
2423
|
const targetPath = path.resolve(options.target);
|
|
1951
2424
|
const inGitRepo = isGitRepo(targetPath);
|
|
@@ -1991,7 +2464,19 @@ function status(rawArgs) {
|
|
|
1991
2464
|
|
|
1992
2465
|
console.log(`[${TOOL_NAME}] Global services:`);
|
|
1993
2466
|
for (const service of services) {
|
|
1994
|
-
|
|
2467
|
+
const serviceLabel = service.displayName || service.name;
|
|
2468
|
+
console.log(` - ${statusDot(service.status)} ${serviceLabel}: ${service.status}`);
|
|
2469
|
+
}
|
|
2470
|
+
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
|
|
2471
|
+
if (missingSystemTools.length > 0) {
|
|
2472
|
+
const tools = missingSystemTools
|
|
2473
|
+
.map((tool) => tool.displayName || tool.name)
|
|
2474
|
+
.join(', ');
|
|
2475
|
+
console.log(`[${TOOL_NAME}] ⚠️ Missing required system tool(s): ${tools}`);
|
|
2476
|
+
for (const tool of missingSystemTools) {
|
|
2477
|
+
const reasonText = tool.reason ? ` (${tool.reason})` : '';
|
|
2478
|
+
console.log(` - install ${tool.name}: ${tool.installHint}${reasonText}`);
|
|
2479
|
+
}
|
|
1995
2480
|
}
|
|
1996
2481
|
|
|
1997
2482
|
if (!scanResult) {
|
|
@@ -2004,6 +2489,16 @@ function status(rawArgs) {
|
|
|
2004
2489
|
|
|
2005
2490
|
if (scanResult.errors === 0 && scanResult.warnings === 0) {
|
|
2006
2491
|
console.log(`[${TOOL_NAME}] Repo safety service: ${statusDot('active')} active.`);
|
|
2492
|
+
} else if (scanResult.errors === 0) {
|
|
2493
|
+
console.log(
|
|
2494
|
+
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.warnings} warning(s)).`,
|
|
2495
|
+
);
|
|
2496
|
+
console.log(`[${TOOL_NAME}] Run '${TOOL_NAME} scan' to review warning details.`);
|
|
2497
|
+
} else if (scanResult.warnings === 0) {
|
|
2498
|
+
console.log(
|
|
2499
|
+
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.errors} error(s)).`,
|
|
2500
|
+
);
|
|
2501
|
+
console.log(`[${TOOL_NAME}] Run '${TOOL_NAME} scan' for detailed findings.`);
|
|
2007
2502
|
} else {
|
|
2008
2503
|
console.log(
|
|
2009
2504
|
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.errors} error(s), ${scanResult.warnings} warning(s)).`,
|
|
@@ -2084,7 +2579,7 @@ function doctor(rawArgs) {
|
|
|
2084
2579
|
allowProtectedBaseWrite: false,
|
|
2085
2580
|
});
|
|
2086
2581
|
|
|
2087
|
-
const blocked = protectedBaseWriteBlock(options);
|
|
2582
|
+
const blocked = protectedBaseWriteBlock(options, { requireBootstrap: false });
|
|
2088
2583
|
if (blocked) {
|
|
2089
2584
|
runDoctorInSandbox(options, blocked);
|
|
2090
2585
|
return;
|
|
@@ -2093,7 +2588,8 @@ function doctor(rawArgs) {
|
|
|
2093
2588
|
assertProtectedMainWriteAllowed(options, 'doctor');
|
|
2094
2589
|
const fixPayload = runFixInternal(options);
|
|
2095
2590
|
const scanResult = runScanInternal({ target: options.target, json: false });
|
|
2096
|
-
const
|
|
2591
|
+
const safe = scanResult.errors === 0 && scanResult.warnings === 0;
|
|
2592
|
+
const musafe = safe;
|
|
2097
2593
|
|
|
2098
2594
|
if (options.json) {
|
|
2099
2595
|
process.stdout.write(
|
|
@@ -2101,6 +2597,7 @@ function doctor(rawArgs) {
|
|
|
2101
2597
|
{
|
|
2102
2598
|
repoRoot: scanResult.repoRoot,
|
|
2103
2599
|
branch: scanResult.branch,
|
|
2600
|
+
safe,
|
|
2104
2601
|
musafe,
|
|
2105
2602
|
fix: {
|
|
2106
2603
|
operations: fixPayload.operations,
|
|
@@ -2123,11 +2620,11 @@ function doctor(rawArgs) {
|
|
|
2123
2620
|
|
|
2124
2621
|
printOperations('Doctor/fix', fixPayload, options.dryRun);
|
|
2125
2622
|
printScanResult(scanResult, false);
|
|
2126
|
-
if (
|
|
2127
|
-
console.log(`[${TOOL_NAME}] ✅ Repo is
|
|
2623
|
+
if (safe) {
|
|
2624
|
+
console.log(`[${TOOL_NAME}] ✅ Repo is fully safe.`);
|
|
2128
2625
|
} else {
|
|
2129
2626
|
console.log(
|
|
2130
|
-
`[${TOOL_NAME}] ⚠️ Repo is not fully
|
|
2627
|
+
`[${TOOL_NAME}] ⚠️ Repo is not fully safe yet (${scanResult.errors} error(s), ${scanResult.warnings} warning(s)).`,
|
|
2131
2628
|
);
|
|
2132
2629
|
}
|
|
2133
2630
|
setExitCodeFromScan(scanResult);
|
|
@@ -2243,7 +2740,7 @@ function setup(rawArgs) {
|
|
|
2243
2740
|
`[${TOOL_NAME}] ✅ Global tools installed (${(globalInstallStatus.packages || []).join(', ')}).`,
|
|
2244
2741
|
);
|
|
2245
2742
|
} else if (globalInstallStatus.status === 'already-installed') {
|
|
2246
|
-
console.log(`[${TOOL_NAME}] ✅ OMX/OpenSpec/codex-auth global tools already installed. Skipping.`);
|
|
2743
|
+
console.log(`[${TOOL_NAME}] ✅ OMX/OpenSpec/codex-auth npm global tools already installed. Skipping.`);
|
|
2247
2744
|
} else if (globalInstallStatus.status === 'failed') {
|
|
2248
2745
|
console.log(
|
|
2249
2746
|
`[${TOOL_NAME}] ⚠️ Global install failed: ${globalInstallStatus.reason}\n` +
|
|
@@ -2256,6 +2753,18 @@ function setup(rawArgs) {
|
|
|
2256
2753
|
`Use --yes-global-install to force or run interactively for Y/N prompt.`,
|
|
2257
2754
|
);
|
|
2258
2755
|
}
|
|
2756
|
+
const requiredSystemTools = detectRequiredSystemTools();
|
|
2757
|
+
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
|
|
2758
|
+
if (missingSystemTools.length === 0) {
|
|
2759
|
+
console.log(`[${TOOL_NAME}] ✅ Required system tools available (${requiredSystemTools.map((tool) => tool.name).join(', ')}).`);
|
|
2760
|
+
} else {
|
|
2761
|
+
const names = missingSystemTools.map((tool) => tool.name).join(', ');
|
|
2762
|
+
console.log(`[${TOOL_NAME}] ⚠️ Missing required system tool(s): ${names}`);
|
|
2763
|
+
for (const tool of missingSystemTools) {
|
|
2764
|
+
const reasonText = tool.reason ? ` (${tool.reason})` : '';
|
|
2765
|
+
console.log(`[${TOOL_NAME}] Install ${tool.name}: ${tool.installHint}${reasonText}`);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2259
2768
|
|
|
2260
2769
|
assertProtectedMainWriteAllowed(options, 'setup');
|
|
2261
2770
|
const installPayload = runInstallInternal(options);
|