@imdeadpool/guardex 7.0.20 → 7.0.22
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 +66 -30
- package/package.json +1 -1
- package/src/cli/args.js +804 -2
- package/src/cli/main.js +744 -5101
- package/src/context.js +197 -33
- package/src/doctor/index.js +1071 -0
- package/src/finish/index.js +456 -358
- package/src/git/index.js +645 -32
- package/src/output/index.js +8 -1
- package/src/sandbox/index.js +301 -52
- package/src/scaffold/index.js +681 -22
- package/src/toolchain/index.js +622 -178
- package/templates/scripts/agent-branch-finish.sh +56 -5
- package/templates/scripts/agent-worktree-prune.sh +15 -1
- package/templates/scripts/codex-agent.sh +14 -2
- package/templates/vscode/guardex-active-agents/README.md +3 -1
- package/templates/vscode/guardex-active-agents/extension.js +321 -12
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/package.json +5 -1
- package/templates/vscode/guardex-active-agents/session-schema.js +233 -29
package/src/toolchain/index.js
CHANGED
|
@@ -1,223 +1,667 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} = deps;
|
|
26
|
-
|
|
27
|
-
function maybeSelfUpdateBeforeStatus() {
|
|
28
|
-
const check = checkForGuardexUpdate();
|
|
29
|
-
if (!check.checked || !check.updateAvailable) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
1
|
+
const {
|
|
2
|
+
fs,
|
|
3
|
+
path,
|
|
4
|
+
cp,
|
|
5
|
+
packageJson,
|
|
6
|
+
TOOL_NAME,
|
|
7
|
+
SHORT_TOOL_NAME,
|
|
8
|
+
OPENSPEC_PACKAGE,
|
|
9
|
+
NPX_BIN,
|
|
10
|
+
GUARDEX_HOME_DIR,
|
|
11
|
+
GLOBAL_TOOLCHAIN_SERVICES,
|
|
12
|
+
GLOBAL_TOOLCHAIN_PACKAGES,
|
|
13
|
+
OPTIONAL_LOCAL_COMPANION_TOOLS,
|
|
14
|
+
REQUIRED_SYSTEM_TOOLS,
|
|
15
|
+
NPM_BIN,
|
|
16
|
+
OPENSPEC_BIN,
|
|
17
|
+
envFlagIsTruthy,
|
|
18
|
+
} = require('../context');
|
|
19
|
+
const { run } = require('../core/runtime');
|
|
20
|
+
const { colorize } = require('../output');
|
|
21
|
+
|
|
22
|
+
function isInteractiveTerminal() {
|
|
23
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
24
|
+
}
|
|
32
25
|
|
|
33
|
-
|
|
26
|
+
const stdinWaitArray = new Int32Array(new SharedArrayBuffer(4));
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
function sleepSyncMs(milliseconds) {
|
|
29
|
+
Atomics.wait(stdinWaitArray, 0, 0, milliseconds);
|
|
30
|
+
}
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
function readSingleLineFromStdin() {
|
|
33
|
+
let input = '';
|
|
34
|
+
const buffer = Buffer.alloc(1);
|
|
35
|
+
|
|
36
|
+
while (true) {
|
|
37
|
+
let bytesRead = 0;
|
|
38
|
+
try {
|
|
39
|
+
bytesRead = fs.readSync(process.stdin.fd, buffer, 0, 1);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error && ['EAGAIN', 'EWOULDBLOCK', 'EINTR'].includes(error.code)) {
|
|
42
|
+
sleepSyncMs(15);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
return input;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
if (bytesRead === 0) {
|
|
49
|
+
if (process.stdin.isTTY) {
|
|
50
|
+
sleepSyncMs(15);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
return input;
|
|
54
|
+
}
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return;
|
|
56
|
+
const char = buffer.toString('utf8', 0, bytesRead);
|
|
57
|
+
if (char === '\n' || char === '\r') {
|
|
58
|
+
return input;
|
|
52
59
|
}
|
|
60
|
+
input += char;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
function parseAutoApproval(name) {
|
|
65
|
+
const raw = process.env[name];
|
|
66
|
+
if (raw == null) return null;
|
|
67
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
68
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
69
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseVersionString(version) {
|
|
74
|
+
const match = String(version || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
75
|
+
if (!match) return null;
|
|
76
|
+
return [
|
|
77
|
+
Number.parseInt(match[1], 10),
|
|
78
|
+
Number.parseInt(match[2], 10),
|
|
79
|
+
Number.parseInt(match[3], 10),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function compareParsedVersions(left, right) {
|
|
84
|
+
if (!left || !right) return 0;
|
|
85
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
86
|
+
const leftValue = left[index] || 0;
|
|
87
|
+
const rightValue = right[index] || 0;
|
|
88
|
+
if (leftValue > rightValue) return 1;
|
|
89
|
+
if (leftValue < rightValue) return -1;
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isNewerVersion(latest, current) {
|
|
95
|
+
const latestParts = parseVersionString(latest);
|
|
96
|
+
const currentParts = parseVersionString(current);
|
|
97
|
+
|
|
98
|
+
if (!latestParts || !currentParts) {
|
|
99
|
+
return String(latest || '').trim() !== String(current || '').trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return compareParsedVersions(latestParts, currentParts) > 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseNpmVersionOutput(stdout) {
|
|
106
|
+
const trimmed = String(stdout || '').trim();
|
|
107
|
+
if (!trimmed) return '';
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(trimmed);
|
|
111
|
+
if (Array.isArray(parsed)) {
|
|
112
|
+
return String(parsed[parsed.length - 1] || '').trim();
|
|
58
113
|
}
|
|
114
|
+
return String(parsed || '').trim();
|
|
115
|
+
} catch {
|
|
116
|
+
const firstLine = trimmed.split('\n').map((line) => line.trim()).find(Boolean);
|
|
117
|
+
return firstLine || '';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
59
120
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
121
|
+
function checkForGuardexUpdate() {
|
|
122
|
+
if (envFlagIsTruthy(process.env.GUARDEX_SKIP_UPDATE_CHECK)) {
|
|
123
|
+
return { checked: false, reason: 'disabled' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const forceCheck = envFlagIsTruthy(process.env.GUARDEX_FORCE_UPDATE_CHECK);
|
|
127
|
+
if (!forceCheck && !isInteractiveTerminal()) {
|
|
128
|
+
return { checked: false, reason: 'non-interactive' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const result = run(NPM_BIN, ['view', packageJson.name, 'version', '--json'], { timeout: 5000 });
|
|
132
|
+
if (result.status !== 0) {
|
|
133
|
+
return { checked: false, reason: 'lookup-failed' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const latest = parseNpmVersionOutput(result.stdout);
|
|
137
|
+
if (!latest) {
|
|
138
|
+
return { checked: false, reason: 'invalid-latest-version' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
checked: true,
|
|
143
|
+
current: packageJson.version,
|
|
144
|
+
latest,
|
|
145
|
+
updateAvailable: isNewerVersion(latest, packageJson.version),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function printUpdateAvailableBanner(current, latest) {
|
|
150
|
+
const title = colorize('UPDATE AVAILABLE', '1;33');
|
|
151
|
+
console.log(`[${TOOL_NAME}] ${title}`);
|
|
152
|
+
console.log(`[${TOOL_NAME}] Current: ${current}`);
|
|
153
|
+
console.log(`[${TOOL_NAME}] Latest : ${latest}`);
|
|
154
|
+
console.log(`[${TOOL_NAME}] Command: ${NPM_BIN} i -g ${packageJson.name}@latest`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readInstalledGuardexVersion() {
|
|
158
|
+
const installInfo = readInstalledGuardexInstallInfo();
|
|
159
|
+
return installInfo ? installInfo.version : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readInstalledGuardexInstallInfo() {
|
|
163
|
+
try {
|
|
164
|
+
const rootResult = run(NPM_BIN, ['root', '-g'], { timeout: 5000 });
|
|
165
|
+
if (rootResult.status !== 0) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const globalRoot = String(rootResult.stdout || '').trim();
|
|
169
|
+
if (!globalRoot) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const installedPkgPath = path.join(globalRoot, packageJson.name, 'package.json');
|
|
173
|
+
if (!fs.existsSync(installedPkgPath)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const parsed = JSON.parse(fs.readFileSync(installedPkgPath, 'utf8'));
|
|
177
|
+
if (parsed && typeof parsed.version === 'string') {
|
|
178
|
+
let binRelative = null;
|
|
179
|
+
if (typeof parsed.bin === 'string') {
|
|
180
|
+
binRelative = parsed.bin;
|
|
181
|
+
} else if (parsed.bin && typeof parsed.bin === 'object') {
|
|
182
|
+
const invokedName = path.basename(process.argv[1] || '');
|
|
183
|
+
binRelative =
|
|
184
|
+
parsed.bin[invokedName] ||
|
|
185
|
+
parsed.bin[SHORT_TOOL_NAME] ||
|
|
186
|
+
Object.values(parsed.bin).find((value) => typeof value === 'string') ||
|
|
187
|
+
null;
|
|
84
188
|
}
|
|
189
|
+
const packageRoot = path.dirname(installedPkgPath);
|
|
190
|
+
const binPath = binRelative ? path.join(packageRoot, binRelative) : null;
|
|
191
|
+
return {
|
|
192
|
+
version: parsed.version,
|
|
193
|
+
packageRoot,
|
|
194
|
+
binPath,
|
|
195
|
+
};
|
|
85
196
|
}
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
86
202
|
|
|
87
|
-
|
|
88
|
-
|
|
203
|
+
function restartIntoUpdatedGuardex(expectedVersion) {
|
|
204
|
+
const installInfo = readInstalledGuardexInstallInfo();
|
|
205
|
+
if (!installInfo || installInfo.version !== expectedVersion || installInfo.version === packageJson.version) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!installInfo.binPath || !fs.existsSync(installInfo.binPath)) {
|
|
209
|
+
console.log(`[${TOOL_NAME}] Restart required to use ${installInfo.version}. Rerun ${SHORT_TOOL_NAME}.`);
|
|
210
|
+
return;
|
|
89
211
|
}
|
|
90
212
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
213
|
+
console.log(`[${TOOL_NAME}] Restarting into ${installInfo.version}…`);
|
|
214
|
+
const restartResult = cp.spawnSync(
|
|
215
|
+
process.execPath,
|
|
216
|
+
[installInfo.binPath, ...process.argv.slice(2)],
|
|
217
|
+
{
|
|
218
|
+
cwd: process.cwd(),
|
|
219
|
+
env: {
|
|
220
|
+
...process.env,
|
|
221
|
+
GUARDEX_SKIP_UPDATE_CHECK: '1',
|
|
222
|
+
},
|
|
223
|
+
stdio: 'inherit',
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
if (restartResult.error) {
|
|
227
|
+
console.log(
|
|
228
|
+
`[${TOOL_NAME}] Restart into ${installInfo.version} failed. Rerun ${SHORT_TOOL_NAME}.`,
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
process.exit(restartResult.status == null ? 0 : restartResult.status);
|
|
233
|
+
}
|
|
96
234
|
|
|
97
|
-
|
|
235
|
+
function checkForOpenSpecPackageUpdate() {
|
|
236
|
+
if (envFlagIsTruthy(process.env.GUARDEX_SKIP_OPENSPEC_UPDATE_CHECK)) {
|
|
237
|
+
return { checked: false, reason: 'disabled' };
|
|
238
|
+
}
|
|
98
239
|
|
|
99
|
-
|
|
100
|
-
|
|
240
|
+
const forceCheck = envFlagIsTruthy(process.env.GUARDEX_FORCE_OPENSPEC_UPDATE_CHECK);
|
|
241
|
+
if (!forceCheck && !isInteractiveTerminal()) {
|
|
242
|
+
return { checked: false, reason: 'non-interactive' };
|
|
243
|
+
}
|
|
101
244
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
245
|
+
const detection = detectGlobalToolchainPackages();
|
|
246
|
+
if (!detection.ok) {
|
|
247
|
+
return { checked: false, reason: 'package-detect-failed' };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const current = String((detection.installedVersions || {})[OPENSPEC_PACKAGE] || '').trim();
|
|
251
|
+
if (!current) {
|
|
252
|
+
return { checked: false, reason: 'not-installed' };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const latestResult = run(NPM_BIN, ['view', OPENSPEC_PACKAGE, 'version', '--json'], { timeout: 5000 });
|
|
256
|
+
if (latestResult.status !== 0) {
|
|
257
|
+
return { checked: false, reason: 'lookup-failed' };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const latest = parseNpmVersionOutput(latestResult.stdout);
|
|
261
|
+
if (!latest) {
|
|
262
|
+
return { checked: false, reason: 'invalid-latest-version' };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
checked: true,
|
|
267
|
+
current,
|
|
268
|
+
latest,
|
|
269
|
+
updateAvailable: isNewerVersion(latest, current),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function printOpenSpecUpdateAvailableBanner(current, latest) {
|
|
274
|
+
const title = colorize('OPENSPEC UPDATE AVAILABLE', '1;33');
|
|
275
|
+
console.log(`[${TOOL_NAME}] ${title}`);
|
|
276
|
+
console.log(`[${TOOL_NAME}] Current: ${current}`);
|
|
277
|
+
console.log(`[${TOOL_NAME}] Latest : ${latest}`);
|
|
278
|
+
console.log(`[${TOOL_NAME}] Command: ${NPM_BIN} i -g ${OPENSPEC_PACKAGE}@latest`);
|
|
279
|
+
console.log(`[${TOOL_NAME}] Then : ${OPENSPEC_BIN} update`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function promptYesNoStrict(question) {
|
|
283
|
+
while (true) {
|
|
284
|
+
process.stdout.write(`${question} [y/n] `);
|
|
285
|
+
const answer = readSingleLineFromStdin().trim().toLowerCase();
|
|
286
|
+
|
|
287
|
+
if (answer === 'y' || answer === 'yes') {
|
|
288
|
+
process.stdout.write('\n');
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
if (answer === 'n' || answer === 'no') {
|
|
292
|
+
process.stdout.write('\n');
|
|
293
|
+
return false;
|
|
105
294
|
}
|
|
106
295
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
111
|
-
: autoApproval;
|
|
296
|
+
process.stdout.write('Please answer with y or n.\n');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
112
299
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
300
|
+
function resolveGlobalInstallApproval(options) {
|
|
301
|
+
if (options.yesGlobalInstall && options.noGlobalInstall) {
|
|
302
|
+
throw new Error('Cannot use both --yes-global-install and --no-global-install');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (options.yesGlobalInstall) {
|
|
306
|
+
return { approved: true, source: 'flag' };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (options.noGlobalInstall) {
|
|
310
|
+
return { approved: false, source: 'flag' };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!isInteractiveTerminal()) {
|
|
314
|
+
return { approved: false, source: 'non-interactive-default' };
|
|
315
|
+
}
|
|
316
|
+
return { approved: true, source: 'prompt' };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function getGlobalToolchainService(packageName) {
|
|
320
|
+
const service = GLOBAL_TOOLCHAIN_SERVICES.find(
|
|
321
|
+
(candidate) => candidate.packageName === packageName,
|
|
322
|
+
);
|
|
323
|
+
return service || { name: packageName, packageName };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function formatGlobalToolchainServiceName(packageName) {
|
|
327
|
+
return getGlobalToolchainService(packageName).name;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function describeMissingGlobalDependencyWarnings(packageNames) {
|
|
331
|
+
return packageNames
|
|
332
|
+
.map((packageName) => getGlobalToolchainService(packageName))
|
|
333
|
+
.filter((service) => service.dependencyUrl)
|
|
334
|
+
.map(
|
|
335
|
+
(service) =>
|
|
336
|
+
`Guardex needs ${service.name} as a dependency: ${service.dependencyUrl}`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function describeCompanionInstallCommands(missingPackages, missingLocalTools) {
|
|
341
|
+
const commands = [];
|
|
342
|
+
if (missingPackages.length > 0) {
|
|
343
|
+
commands.push(`${NPM_BIN} i -g ${missingPackages.join(' ')}`);
|
|
344
|
+
}
|
|
345
|
+
for (const tool of missingLocalTools) {
|
|
346
|
+
commands.push(tool.installCommand);
|
|
347
|
+
}
|
|
348
|
+
return commands;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools) {
|
|
352
|
+
const dependencyWarnings = describeMissingGlobalDependencyWarnings(missingPackages);
|
|
353
|
+
const installCommands = describeCompanionInstallCommands(missingPackages, missingLocalTools);
|
|
354
|
+
const dependencyPrefix = dependencyWarnings.length > 0
|
|
355
|
+
? `${dependencyWarnings.join(' ')} `
|
|
356
|
+
: '';
|
|
357
|
+
return `${dependencyPrefix}Install missing companion tools now? (${installCommands.join(' && ')})`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function detectGlobalToolchainPackages() {
|
|
361
|
+
const result = run(NPM_BIN, ['list', '-g', '--depth=0', '--json']);
|
|
362
|
+
if (result.status !== 0) {
|
|
363
|
+
const stderr = (result.stderr || '').trim();
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
error: stderr || 'Unable to detect globally installed npm packages',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let parsed;
|
|
371
|
+
try {
|
|
372
|
+
parsed = JSON.parse(result.stdout || '{}');
|
|
373
|
+
} catch (error) {
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
error: `Failed to parse npm list output: ${error.message}`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const dependencyMap = parsed && parsed.dependencies && typeof parsed.dependencies === 'object'
|
|
381
|
+
? parsed.dependencies
|
|
382
|
+
: {};
|
|
383
|
+
const installedSet = new Set(Object.keys(dependencyMap));
|
|
384
|
+
|
|
385
|
+
const installed = [];
|
|
386
|
+
const missing = [];
|
|
387
|
+
const installedVersions = {};
|
|
388
|
+
for (const pkg of GLOBAL_TOOLCHAIN_PACKAGES) {
|
|
389
|
+
if (installedSet.has(pkg)) {
|
|
390
|
+
installed.push(pkg);
|
|
391
|
+
const rawVersion = dependencyMap[pkg] && dependencyMap[pkg].version;
|
|
392
|
+
const version = String(rawVersion || '').trim();
|
|
393
|
+
if (version) {
|
|
394
|
+
installedVersions[pkg] = version;
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
missing.push(pkg);
|
|
116
398
|
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return { ok: true, installed, missing, installedVersions };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function detectRequiredSystemTools() {
|
|
405
|
+
const services = [];
|
|
406
|
+
for (const tool of REQUIRED_SYSTEM_TOOLS) {
|
|
407
|
+
const result = run(tool.command, ['--version']);
|
|
408
|
+
const active = result.status === 0;
|
|
409
|
+
const rawReason = result.error && result.error.code
|
|
410
|
+
? result.error.code
|
|
411
|
+
: (result.stderr || '').trim();
|
|
412
|
+
const reason = rawReason.split('\n')[0] || '';
|
|
413
|
+
services.push({
|
|
414
|
+
name: tool.name,
|
|
415
|
+
displayName: tool.displayName || tool.name,
|
|
416
|
+
command: tool.command,
|
|
417
|
+
installHint: tool.installHint,
|
|
418
|
+
status: active ? 'active' : 'inactive',
|
|
419
|
+
reason,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return services;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function detectOptionalLocalCompanionTools() {
|
|
426
|
+
return OPTIONAL_LOCAL_COMPANION_TOOLS.map((tool) => {
|
|
427
|
+
const detectedPath = tool.candidatePaths
|
|
428
|
+
.map((relativePath) => path.join(GUARDEX_HOME_DIR, relativePath))
|
|
429
|
+
.find((candidatePath) => fs.existsSync(candidatePath));
|
|
430
|
+
return {
|
|
431
|
+
name: tool.name,
|
|
432
|
+
displayName: tool.displayName || tool.name,
|
|
433
|
+
installCommand: tool.installCommand,
|
|
434
|
+
installArgs: [...tool.installArgs],
|
|
435
|
+
status: detectedPath ? 'active' : 'inactive',
|
|
436
|
+
detectedPath: detectedPath || null,
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function askGlobalInstallForMissing(options, missingPackages, missingLocalTools) {
|
|
442
|
+
const approval = resolveGlobalInstallApproval(options);
|
|
443
|
+
if (!approval.approved) {
|
|
444
|
+
return approval;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (approval.source === 'prompt') {
|
|
448
|
+
const approved = promptYesNoStrict(
|
|
449
|
+
buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools),
|
|
450
|
+
);
|
|
451
|
+
return { approved, source: 'prompt' };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return approval;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function maybeSelfUpdateBeforeStatus() {
|
|
458
|
+
const check = checkForGuardexUpdate();
|
|
459
|
+
if (!check.checked || !check.updateAvailable) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
117
462
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
463
|
+
printUpdateAvailableBanner(check.current, check.latest);
|
|
464
|
+
|
|
465
|
+
const autoApproval = parseAutoApproval('GUARDEX_AUTO_UPDATE_APPROVAL');
|
|
466
|
+
const interactive = isInteractiveTerminal();
|
|
467
|
+
|
|
468
|
+
if (!interactive && autoApproval == null) {
|
|
469
|
+
console.log(`[${TOOL_NAME}] Non-interactive shell; skipping auto-update prompt.`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const shouldUpdate = interactive
|
|
474
|
+
? promptYesNoStrict(
|
|
475
|
+
`Update now? (${NPM_BIN} i -g ${packageJson.name}@latest)`,
|
|
476
|
+
)
|
|
477
|
+
: autoApproval;
|
|
478
|
+
|
|
479
|
+
if (!shouldUpdate) {
|
|
480
|
+
console.log(`[${TOOL_NAME}] Skipped update.`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const installResult = run(NPM_BIN, ['i', '-g', `${packageJson.name}@latest`], { stdio: 'inherit' });
|
|
485
|
+
if (installResult.status !== 0) {
|
|
486
|
+
console.log(`[${TOOL_NAME}] Update failed. You can retry manually.`);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const postInstallVersion = readInstalledGuardexVersion();
|
|
491
|
+
if (postInstallVersion != null && postInstallVersion !== check.latest) {
|
|
492
|
+
console.log(
|
|
493
|
+
`[${TOOL_NAME}] Installed version is still ${postInstallVersion} (expected ${check.latest}). ` +
|
|
494
|
+
`Retrying with pinned version ${check.latest}...`,
|
|
495
|
+
);
|
|
496
|
+
const pinnedResult = run(
|
|
497
|
+
NPM_BIN,
|
|
498
|
+
['i', '-g', `${packageJson.name}@${check.latest}`],
|
|
499
|
+
{ stdio: 'inherit' },
|
|
500
|
+
);
|
|
501
|
+
if (pinnedResult.status !== 0) {
|
|
502
|
+
console.log(
|
|
503
|
+
`[${TOOL_NAME}] Pinned retry failed. Run manually: ${NPM_BIN} i -g ${packageJson.name}@${check.latest}`,
|
|
504
|
+
);
|
|
121
505
|
return;
|
|
122
506
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
507
|
+
const pinnedVersion = readInstalledGuardexVersion();
|
|
508
|
+
if (pinnedVersion != null && pinnedVersion !== check.latest) {
|
|
509
|
+
console.log(
|
|
510
|
+
`[${TOOL_NAME}] On-disk version still ${pinnedVersion} after pinned retry. ` +
|
|
511
|
+
`Investigate: ${NPM_BIN} root -g && ${NPM_BIN} cache verify`,
|
|
512
|
+
);
|
|
127
513
|
return;
|
|
128
514
|
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
console.log(`[${TOOL_NAME}] Updated to latest published version.`);
|
|
518
|
+
restartIntoUpdatedGuardex(check.latest);
|
|
519
|
+
}
|
|
129
520
|
|
|
130
|
-
|
|
521
|
+
function maybeOpenSpecUpdateBeforeStatus() {
|
|
522
|
+
const check = checkForOpenSpecPackageUpdate();
|
|
523
|
+
if (!check.checked || !check.updateAvailable) {
|
|
524
|
+
return;
|
|
131
525
|
}
|
|
132
526
|
|
|
133
|
-
|
|
134
|
-
const approval = resolveGlobalInstallApproval(options);
|
|
135
|
-
if (approval.source === 'flag' && !approval.approved) {
|
|
136
|
-
return {
|
|
137
|
-
status: 'skipped',
|
|
138
|
-
reason: approval.source,
|
|
139
|
-
missingPackages: [],
|
|
140
|
-
missingLocalTools: [],
|
|
141
|
-
};
|
|
142
|
-
}
|
|
527
|
+
printOpenSpecUpdateAvailableBanner(check.current, check.latest);
|
|
143
528
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
529
|
+
const autoApproval = parseAutoApproval('GUARDEX_AUTO_OPENSPEC_UPDATE_APPROVAL');
|
|
530
|
+
const interactive = isInteractiveTerminal();
|
|
147
531
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
532
|
+
if (!interactive && autoApproval == null) {
|
|
533
|
+
console.log(`[${TOOL_NAME}] Non-interactive shell; skipping OpenSpec update prompt.`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const shouldUpdate = interactive
|
|
538
|
+
? promptYesNoStrict(
|
|
539
|
+
`Update OpenSpec now? (${NPM_BIN} i -g ${OPENSPEC_PACKAGE}@latest && ${OPENSPEC_BIN} update)`,
|
|
540
|
+
)
|
|
541
|
+
: autoApproval;
|
|
542
|
+
|
|
543
|
+
if (!shouldUpdate) {
|
|
544
|
+
console.log(`[${TOOL_NAME}] Skipped OpenSpec update.`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const installResult = run(NPM_BIN, ['i', '-g', `${OPENSPEC_PACKAGE}@latest`], { stdio: 'inherit' });
|
|
549
|
+
if (installResult.status !== 0) {
|
|
550
|
+
console.log(`[${TOOL_NAME}] OpenSpec npm install failed. You can retry manually.`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const toolUpdateResult = run(OPENSPEC_BIN, ['update'], { stdio: 'inherit' });
|
|
555
|
+
if (toolUpdateResult.status !== 0) {
|
|
556
|
+
console.log(`[${TOOL_NAME}] OpenSpec tool update failed. Run '${OPENSPEC_BIN} update' manually.`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
console.log(`[${TOOL_NAME}] OpenSpec updated to latest package and tool plugins refreshed.`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function installGlobalToolchain(options) {
|
|
564
|
+
const approval = resolveGlobalInstallApproval(options);
|
|
565
|
+
if (approval.source === 'flag' && !approval.approved) {
|
|
566
|
+
return {
|
|
567
|
+
status: 'skipped',
|
|
568
|
+
reason: approval.source,
|
|
569
|
+
missingPackages: [],
|
|
570
|
+
missingLocalTools: [],
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (options.dryRun) {
|
|
575
|
+
return { status: 'dry-run-skip' };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const detection = detectGlobalToolchainPackages();
|
|
579
|
+
const localCompanionTools = detectOptionalLocalCompanionTools();
|
|
580
|
+
if (!detection.ok) {
|
|
581
|
+
console.log(`[${TOOL_NAME}] Could not detect global packages: ${detection.error}`);
|
|
582
|
+
} else {
|
|
583
|
+
if (detection.installed.length > 0) {
|
|
584
|
+
console.log(
|
|
585
|
+
`[${TOOL_NAME}] Already installed globally: ` +
|
|
586
|
+
`${detection.installed.map((pkg) => formatGlobalToolchainServiceName(pkg)).join(', ')}`,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
const installedLocalTools = localCompanionTools
|
|
590
|
+
.filter((tool) => tool.status === 'active')
|
|
591
|
+
.map((tool) => tool.name);
|
|
592
|
+
if (installedLocalTools.length > 0) {
|
|
593
|
+
console.log(`[${TOOL_NAME}] Already installed locally: ${installedLocalTools.join(', ')}`);
|
|
168
594
|
}
|
|
595
|
+
if (detection.missing.length === 0 && localCompanionTools.every((tool) => tool.status === 'active')) {
|
|
596
|
+
return { status: 'already-installed' };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
169
599
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
600
|
+
const missingPackages = detection.ok ? detection.missing : [...GLOBAL_TOOLCHAIN_PACKAGES];
|
|
601
|
+
const missingLocalTools = localCompanionTools.filter((tool) => tool.status !== 'active');
|
|
602
|
+
const installApproval = askGlobalInstallForMissing(options, missingPackages, missingLocalTools);
|
|
603
|
+
if (!installApproval.approved) {
|
|
604
|
+
return {
|
|
605
|
+
status: 'skipped',
|
|
606
|
+
reason: installApproval.source,
|
|
607
|
+
missingPackages,
|
|
608
|
+
missingLocalTools,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const installed = [];
|
|
613
|
+
if (missingPackages.length > 0) {
|
|
614
|
+
console.log(
|
|
615
|
+
`[${TOOL_NAME}] Installing global toolchain: npm i -g ${missingPackages.join(' ')}`,
|
|
616
|
+
);
|
|
617
|
+
const result = run(NPM_BIN, ['i', '-g', ...missingPackages], { stdio: 'inherit' });
|
|
618
|
+
if (result.status !== 0) {
|
|
619
|
+
const stderr = (result.stderr || '').trim();
|
|
174
620
|
return {
|
|
175
|
-
status: '
|
|
176
|
-
reason:
|
|
177
|
-
missingPackages,
|
|
178
|
-
missingLocalTools,
|
|
621
|
+
status: 'failed',
|
|
622
|
+
reason: stderr || 'npm global install failed',
|
|
179
623
|
};
|
|
180
624
|
}
|
|
625
|
+
installed.push(...missingPackages);
|
|
626
|
+
}
|
|
181
627
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
status: 'failed',
|
|
192
|
-
reason: stderr || 'npm global install failed',
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
installed.push(...missingPackages);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
for (const tool of missingLocalTools) {
|
|
199
|
-
console.log(`[${TOOL_NAME}] Installing local companion tool: ${tool.installCommand}`);
|
|
200
|
-
const result = run(NPX_BIN, tool.installArgs, { stdio: 'inherit' });
|
|
201
|
-
if (result.status !== 0) {
|
|
202
|
-
const stderr = (result.stderr || '').trim();
|
|
203
|
-
return {
|
|
204
|
-
status: 'failed',
|
|
205
|
-
reason: stderr || `${tool.name} install failed`,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
installed.push(tool.name);
|
|
628
|
+
for (const tool of missingLocalTools) {
|
|
629
|
+
console.log(`[${TOOL_NAME}] Installing local companion tool: ${tool.installCommand}`);
|
|
630
|
+
const result = run(NPX_BIN, tool.installArgs, { stdio: 'inherit' });
|
|
631
|
+
if (result.status !== 0) {
|
|
632
|
+
const stderr = (result.stderr || '').trim();
|
|
633
|
+
return {
|
|
634
|
+
status: 'failed',
|
|
635
|
+
reason: stderr || `${tool.name} install failed`,
|
|
636
|
+
};
|
|
209
637
|
}
|
|
210
|
-
|
|
211
|
-
return { status: 'installed', packages: installed };
|
|
638
|
+
installed.push(tool.name);
|
|
212
639
|
}
|
|
213
640
|
|
|
214
|
-
return {
|
|
215
|
-
maybeSelfUpdateBeforeStatus,
|
|
216
|
-
maybeOpenSpecUpdateBeforeStatus,
|
|
217
|
-
installGlobalToolchain,
|
|
218
|
-
};
|
|
641
|
+
return { status: 'installed', packages: installed };
|
|
219
642
|
}
|
|
220
643
|
|
|
221
644
|
module.exports = {
|
|
222
|
-
|
|
645
|
+
isInteractiveTerminal,
|
|
646
|
+
parseAutoApproval,
|
|
647
|
+
checkForGuardexUpdate,
|
|
648
|
+
printUpdateAvailableBanner,
|
|
649
|
+
readInstalledGuardexVersion,
|
|
650
|
+
readInstalledGuardexInstallInfo,
|
|
651
|
+
restartIntoUpdatedGuardex,
|
|
652
|
+
checkForOpenSpecPackageUpdate,
|
|
653
|
+
printOpenSpecUpdateAvailableBanner,
|
|
654
|
+
promptYesNoStrict,
|
|
655
|
+
resolveGlobalInstallApproval,
|
|
656
|
+
getGlobalToolchainService,
|
|
657
|
+
formatGlobalToolchainServiceName,
|
|
658
|
+
describeMissingGlobalDependencyWarnings,
|
|
659
|
+
describeCompanionInstallCommands,
|
|
660
|
+
detectGlobalToolchainPackages,
|
|
661
|
+
detectRequiredSystemTools,
|
|
662
|
+
detectOptionalLocalCompanionTools,
|
|
663
|
+
askGlobalInstallForMissing,
|
|
664
|
+
maybeSelfUpdateBeforeStatus,
|
|
665
|
+
maybeOpenSpecUpdateBeforeStatus,
|
|
666
|
+
installGlobalToolchain,
|
|
223
667
|
};
|