@imdeadpool/guardex 7.0.43 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/package.json +2 -1
- package/skills/gx-act/SKILL.md +82 -0
- package/src/agents/inspect.js +17 -4
- package/src/agents/launch.js +10 -1
- package/src/agents/status.js +9 -6
- package/src/budget/index.js +2 -1
- package/src/cli/args.js +52 -2
- package/src/cli/commands/agents.js +364 -0
- package/src/cli/commands/bootstrap.js +92 -0
- package/src/cli/commands/branch.js +127 -0
- package/src/cli/commands/claude.js +674 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/finish.js +26 -0
- package/src/cli/commands/mcp.js +122 -0
- package/src/cli/commands/misc.js +304 -0
- package/src/cli/commands/pr.js +439 -0
- package/src/cli/commands/prompt.js +92 -0
- package/src/cli/commands/release.js +305 -0
- package/src/cli/commands/report.js +244 -0
- package/src/cli/commands/review.js +32 -0
- package/src/cli/commands/setup.js +242 -0
- package/src/cli/commands/status.js +338 -0
- package/src/cli/commands/watch.js +234 -0
- package/src/cli/main.js +68 -3726
- package/src/cli/shared/repo-env.js +161 -0
- package/src/cli/shared/sandbox.js +417 -0
- package/src/cli/shared/scaffolding.js +535 -0
- package/src/cli/shared/toolchain-shims.js +420 -0
- package/src/context.js +229 -11
- package/src/core/runtime.js +6 -1
- package/src/doctor/index.js +42 -13
- package/src/finish/index.js +147 -5
- package/src/finish/preflight.js +177 -0
- package/src/finish/review-gate.js +182 -0
- package/src/git/index.js +446 -4
- package/src/hooks/index.js +0 -64
- package/src/mcp/collect.js +370 -0
- package/src/mcp/server.js +157 -0
- package/src/output/index.js +67 -1
- package/src/pr-review.js +23 -0
- package/src/pr.js +381 -0
- package/src/sandbox/index.js +13 -2
- package/src/scaffold/agent-worktree-prep.js +213 -0
- package/src/scaffold/index.js +108 -10
- package/src/speckit/index.js +226 -0
- package/src/terminal/index.js +1 -76
- package/src/terminal/tmux.js +0 -1
- package/src/toolchain/index.js +20 -0
- package/templates/AGENTS.monorepo-apps.md +26 -0
- package/templates/AGENTS.multiagent-safety.md +61 -347
- package/templates/AGENTS.multiagent-safety.min.md +11 -0
- package/templates/codex/skills/gx-act/SKILL.md +82 -0
- package/templates/githooks/pre-commit +22 -19
- package/templates/scripts/agent-branch-finish.sh +8 -30
- package/templates/scripts/agent-branch-merge.sh +4 -1
- package/templates/scripts/agent-branch-start.sh +88 -3
- package/templates/scripts/agent-preflight.sh +31 -5
- package/templates/scripts/agent-worktree-prune.sh +1 -1
- package/templates/scripts/codex-agent.sh +0 -91
- package/src/agents/detect.js +0 -160
- package/src/cockpit/keybindings.js +0 -224
- package/src/cockpit/layout.js +0 -224
package/src/cli/main.js
CHANGED
|
@@ -1,151 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// Thin dispatcher for the `gx` CLI. Every subcommand handler lives in
|
|
4
|
+
// src/cli/commands/<verb>.js; shared scaffolding/sandbox helpers live in
|
|
5
|
+
// src/cli/shared/. The dispatcher only:
|
|
6
|
+
// - routes argv to the right handler module
|
|
7
|
+
// - applies typo/deprecation/suggestion normalization
|
|
8
|
+
// - implements the no-args default flow (cockpit + status + auto-doctor)
|
|
9
|
+
//
|
|
10
|
+
// All handler bodies were extracted verbatim during the v7.0.43 refactor.
|
|
2
11
|
|
|
3
|
-
const
|
|
4
|
-
const sandboxModule = require('../sandbox');
|
|
12
|
+
const { cp, path, packageJson, TOOL_NAME, SHORT_TOOL_NAME, DEPRECATED_COMMAND_ALIASES } = require('../context');
|
|
5
13
|
const toolchainModule = require('../toolchain');
|
|
6
|
-
const finishCommands = require('../finish');
|
|
7
|
-
const doctorModule = require('../doctor');
|
|
8
|
-
const submoduleModule = require('../submodule');
|
|
9
|
-
const agentInspect = require('../agents/inspect');
|
|
10
|
-
const agentStatus = require('../agents/status');
|
|
11
|
-
const agentCleanupSessions = require('../agents/cleanup-sessions');
|
|
12
|
-
const { finishAgentSession } = require('../agents/finish');
|
|
13
|
-
const sessionSeverityReport = require('../report/session-severity');
|
|
14
14
|
const budgetModule = require('../budget');
|
|
15
15
|
const ciInitModule = require('../ci-init');
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const prReviewModule = require('../pr-review');
|
|
19
|
-
const {
|
|
20
|
-
fs,
|
|
21
|
-
path,
|
|
22
|
-
cp,
|
|
23
|
-
packageJson,
|
|
24
|
-
TOOL_NAME,
|
|
25
|
-
SHORT_TOOL_NAME,
|
|
26
|
-
OPENSPEC_PACKAGE,
|
|
27
|
-
NPX_BIN,
|
|
28
|
-
GUARDEX_HOME_DIR,
|
|
29
|
-
GLOBAL_TOOLCHAIN_SERVICES,
|
|
30
|
-
GLOBAL_TOOLCHAIN_PACKAGES,
|
|
31
|
-
OPTIONAL_LOCAL_COMPANION_TOOLS,
|
|
32
|
-
GH_BIN,
|
|
33
|
-
REQUIRED_SYSTEM_TOOLS,
|
|
34
|
-
MAINTAINER_RELEASE_REPO,
|
|
35
|
-
NPM_BIN,
|
|
36
|
-
OPENSPEC_BIN,
|
|
37
|
-
SCORECARD_BIN,
|
|
38
|
-
GIT_PROTECTED_BRANCHES_KEY,
|
|
39
|
-
GIT_BASE_BRANCH_KEY,
|
|
40
|
-
GIT_SYNC_STRATEGY_KEY,
|
|
41
|
-
GUARDEX_REPO_TOGGLE_ENV,
|
|
42
|
-
DEFAULT_PROTECTED_BRANCHES,
|
|
43
|
-
DEFAULT_BASE_BRANCH,
|
|
44
|
-
DEFAULT_SYNC_STRATEGY,
|
|
45
|
-
COMPOSE_HINT_FILES,
|
|
46
|
-
TEMPLATE_ROOT,
|
|
47
|
-
HOOK_NAMES,
|
|
48
|
-
TEMPLATE_FILES,
|
|
49
|
-
LEGACY_WORKFLOW_SHIM_SPECS,
|
|
50
|
-
LEGACY_MANAGED_REPO_FILES,
|
|
51
|
-
REQUIRED_MANAGED_REPO_FILES,
|
|
52
|
-
LEGACY_MANAGED_PACKAGE_SCRIPTS,
|
|
53
|
-
PACKAGE_SCRIPT_ASSETS,
|
|
54
|
-
USER_LEVEL_SKILL_ASSETS,
|
|
55
|
-
EXECUTABLE_RELATIVE_PATHS,
|
|
56
|
-
CRITICAL_GUARDRAIL_PATHS,
|
|
57
|
-
LOCK_FILE_RELATIVE,
|
|
58
|
-
AGENTS_BOTS_STATE_RELATIVE,
|
|
59
|
-
AGENTS_MARKER_START,
|
|
60
|
-
AGENTS_MARKER_END,
|
|
61
|
-
GITIGNORE_MARKER_START,
|
|
62
|
-
GITIGNORE_MARKER_END,
|
|
63
|
-
SHARED_VSCODE_SETTINGS_RELATIVE,
|
|
64
|
-
REPO_SCAN_IGNORED_FOLDERS_SETTING,
|
|
65
|
-
AGENT_WORKTREE_RELATIVE_DIRS,
|
|
66
|
-
MANAGED_REPO_SCAN_IGNORED_FOLDERS,
|
|
67
|
-
MANAGED_GITIGNORE_PATHS,
|
|
68
|
-
REPO_SCAFFOLD_DIRECTORIES,
|
|
69
|
-
OMX_SCAFFOLD_DIRECTORIES,
|
|
70
|
-
OMX_SCAFFOLD_FILES,
|
|
71
|
-
TARGETED_FORCEABLE_MANAGED_PATHS,
|
|
72
|
-
DEPRECATED_COMMAND_ALIASES,
|
|
73
|
-
envFlagIsTruthy,
|
|
74
|
-
defaultAgentWorktreeRelativeDir,
|
|
75
|
-
listAiSetupPartNames,
|
|
76
|
-
parseAiSetupPartNames,
|
|
77
|
-
renderAiSetupPrompt,
|
|
78
|
-
AI_SETUP_PROMPT,
|
|
79
|
-
AI_SETUP_COMMANDS,
|
|
80
|
-
SCORECARD_RISK_BY_CHECK,
|
|
81
|
-
} = require('../context');
|
|
82
|
-
const {
|
|
83
|
-
gitRun,
|
|
84
|
-
resolveRepoRoot,
|
|
85
|
-
isGitRepo,
|
|
86
|
-
discoverNestedGitRepos,
|
|
87
|
-
uniquePreserveOrder,
|
|
88
|
-
listLocalUserBranches,
|
|
89
|
-
listLocalAgentBranches,
|
|
90
|
-
mapWorktreePathsByBranch,
|
|
91
|
-
gitRefExists,
|
|
92
|
-
hasSignificantWorkingTreeChanges,
|
|
93
|
-
readConfiguredProtectedBranches,
|
|
94
|
-
readProtectedBranches,
|
|
95
|
-
ensureSetupProtectedBranches,
|
|
96
|
-
ensureSubmoduleAutoSync,
|
|
97
|
-
writeProtectedBranches,
|
|
98
|
-
readGitConfig,
|
|
99
|
-
resolveBaseBranch,
|
|
100
|
-
resolveSyncStrategy,
|
|
101
|
-
currentBranchName,
|
|
102
|
-
repoHasHeadCommit,
|
|
103
|
-
readBranchDisplayName,
|
|
104
|
-
hasOriginRemote: repoHasOriginRemote,
|
|
105
|
-
detectComposeHintFiles,
|
|
106
|
-
printSetupRepoHints,
|
|
107
|
-
ensureRepoBranch,
|
|
108
|
-
ensureOriginBaseRef,
|
|
109
|
-
workingTreeIsDirty,
|
|
110
|
-
aheadBehind,
|
|
111
|
-
lockRegistryStatus,
|
|
112
|
-
listAgentWorktrees,
|
|
113
|
-
listLocalAgentBranchesForFinish,
|
|
114
|
-
worktreeHasLocalChanges,
|
|
115
|
-
branchExists,
|
|
116
|
-
resolveFinishBaseBranch,
|
|
117
|
-
branchMergedIntoBase,
|
|
118
|
-
syncOperation,
|
|
119
|
-
} = require('../git');
|
|
120
|
-
const {
|
|
121
|
-
run,
|
|
122
|
-
extractTargetedArgs,
|
|
123
|
-
packageAssetEnv,
|
|
124
|
-
runPackageAsset,
|
|
125
|
-
runReviewBotCommand,
|
|
126
|
-
invokePackageAsset,
|
|
127
|
-
} = require('../core/runtime');
|
|
128
|
-
const {
|
|
129
|
-
parseVersionString,
|
|
130
|
-
compareParsedVersions,
|
|
131
|
-
isNewerVersion,
|
|
132
|
-
} = require('../core/versions');
|
|
133
|
-
const { readSingleLineFromStdin } = require('../core/stdin');
|
|
134
|
-
const {
|
|
135
|
-
normalizeManagedForcePath,
|
|
136
|
-
parseCommonArgs,
|
|
137
|
-
parseSetupArgs,
|
|
138
|
-
parseDoctorArgs,
|
|
139
|
-
parseTargetFlag,
|
|
140
|
-
parseReviewArgs,
|
|
141
|
-
parsePrReviewArgs,
|
|
142
|
-
parseAgentsArgs,
|
|
143
|
-
parseReportArgs,
|
|
144
|
-
parseSyncArgs,
|
|
145
|
-
parseCleanupArgs,
|
|
146
|
-
parseMergeArgs,
|
|
147
|
-
parseFinishArgs,
|
|
148
|
-
} = require('./args');
|
|
16
|
+
const speckitModule = require('../speckit');
|
|
17
|
+
const { usage, startTransientSpinner } = require('../output');
|
|
149
18
|
const {
|
|
150
19
|
maybeSuggestCommand,
|
|
151
20
|
normalizeCommandOrThrow,
|
|
@@ -153,778 +22,56 @@ const {
|
|
|
153
22
|
extractFlag,
|
|
154
23
|
} = require('./dispatch');
|
|
155
24
|
const {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
} = require('
|
|
25
|
+
isInteractiveTerminal,
|
|
26
|
+
legacyDefaultStatusEnabled,
|
|
27
|
+
defaultCockpitDisabled,
|
|
28
|
+
parseBooleanLike,
|
|
29
|
+
} = require('./shared/repo-env');
|
|
30
|
+
const { resolveRepoRoot } = require('../git');
|
|
31
|
+
|
|
32
|
+
// Subcommand modules (each owns one or a small cluster of verbs).
|
|
33
|
+
const { status } = require('./commands/status');
|
|
34
|
+
const { setup } = require('./commands/setup');
|
|
35
|
+
const { install, fix, scan } = require('./commands/bootstrap');
|
|
36
|
+
const { doctor } = require('./commands/doctor');
|
|
37
|
+
const { review, prReview } = require('./commands/review');
|
|
38
|
+
const { pr: prCommand } = require('./commands/pr');
|
|
39
|
+
const { claude: claudeCommand } = require('./commands/claude');
|
|
40
|
+
const { agents } = require('./commands/agents');
|
|
41
|
+
const { mcp } = require('./commands/mcp');
|
|
42
|
+
const { report } = require('./commands/report');
|
|
43
|
+
const { release } = require('./commands/release');
|
|
44
|
+
const { watch } = require('./commands/watch');
|
|
169
45
|
const {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
} = require('../scaffold');
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* @typedef {Object} AutoFinishSummary
|
|
203
|
-
* @property {boolean} [enabled]
|
|
204
|
-
* @property {number} [attempted]
|
|
205
|
-
* @property {number} [completed]
|
|
206
|
-
* @property {number} [skipped]
|
|
207
|
-
* @property {number} [failed]
|
|
208
|
-
* @property {string[]} [details]
|
|
209
|
-
* @property {string} [baseBranch]
|
|
210
|
-
*/
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* @typedef {Object} OperationResult
|
|
214
|
-
* @property {string} status
|
|
215
|
-
* @property {string} note
|
|
216
|
-
* @property {string} [stdout]
|
|
217
|
-
* @property {string} [stderr]
|
|
218
|
-
* @property {string} [prUrl]
|
|
219
|
-
* @property {string[]} [stagedFiles]
|
|
220
|
-
* @property {string} [commitMessage]
|
|
221
|
-
* @property {unknown[]} [operations]
|
|
222
|
-
* @property {OperationResult} [cleanup]
|
|
223
|
-
* @property {OperationResult} [hookRefresh]
|
|
224
|
-
*/
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* @typedef {Object} SandboxMetadata
|
|
228
|
-
* @property {string} branch
|
|
229
|
-
* @property {string} worktreePath
|
|
230
|
-
*/
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* @typedef {Object} SandboxStartResult
|
|
234
|
-
* @property {SandboxMetadata} metadata
|
|
235
|
-
* @property {string} [stdout]
|
|
236
|
-
* @property {string} [stderr]
|
|
237
|
-
*/
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* @typedef {Object} DoctorLockSyncState
|
|
241
|
-
* @property {OperationResult} result
|
|
242
|
-
* @property {string | null} sandboxLockContent
|
|
243
|
-
*/
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* @typedef {Object} DoctorSandboxExecution
|
|
247
|
-
* @property {OperationResult} autoCommit
|
|
248
|
-
* @property {OperationResult} finish
|
|
249
|
-
* @property {OperationResult} protectedBaseRepairSync
|
|
250
|
-
* @property {OperationResult} lockSync
|
|
251
|
-
* @property {OperationResult} omxScaffoldSync
|
|
252
|
-
* @property {AutoFinishSummary} autoFinish
|
|
253
|
-
* @property {string | null} sandboxLockContent
|
|
254
|
-
*/
|
|
255
|
-
|
|
256
|
-
function appendForceArgs(args, options) {
|
|
257
|
-
if (!options.force) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
args.push('--force');
|
|
261
|
-
for (const managedPath of options.forceManagedPaths || []) {
|
|
262
|
-
args.push(managedPath);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function shouldForceManagedPath(options, relativePath) {
|
|
267
|
-
if (!options.force) {
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
const targetedPaths = Array.isArray(options.forceManagedPaths) ? options.forceManagedPaths : [];
|
|
271
|
-
if (targetedPaths.length === 0) {
|
|
272
|
-
return true;
|
|
273
|
-
}
|
|
274
|
-
const normalized = normalizeManagedForcePath(relativePath);
|
|
275
|
-
return normalized !== null && targetedPaths.includes(normalized);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function ensureTargetedLegacyWorkflowShims(repoRoot, options) {
|
|
279
|
-
const targetedPaths = Array.isArray(options.forceManagedPaths) ? options.forceManagedPaths : [];
|
|
280
|
-
if (targetedPaths.length === 0) {
|
|
281
|
-
return [];
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const operations = [];
|
|
285
|
-
for (const shim of LEGACY_WORKFLOW_SHIM_SPECS) {
|
|
286
|
-
if (!shouldForceManagedPath(options, shim.relativePath)) {
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
operations.push(ensureGeneratedScriptShim(repoRoot, shim, { dryRun: options.dryRun, force: true }));
|
|
290
|
-
}
|
|
291
|
-
return operations;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function normalizeWorkspacePath(relativePath) {
|
|
295
|
-
return String(relativePath || '.').replace(/\\/g, '/');
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function isCommandAvailable(commandName) {
|
|
299
|
-
return run('which', [commandName]).status === 0;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function buildParentWorkspaceView(repoRoot) {
|
|
303
|
-
const parentDir = path.dirname(repoRoot);
|
|
304
|
-
const workspaceFileName = `${path.basename(repoRoot)}-branches.code-workspace`;
|
|
305
|
-
const workspacePath = path.join(parentDir, workspaceFileName);
|
|
306
|
-
const repoRelativePath = normalizeWorkspacePath(path.relative(parentDir, repoRoot) || '.');
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
workspacePath,
|
|
310
|
-
payload: {
|
|
311
|
-
folders: [
|
|
312
|
-
{ path: repoRelativePath },
|
|
313
|
-
...AGENT_WORKTREE_RELATIVE_DIRS.map((relativeDir) => ({
|
|
314
|
-
path: normalizeWorkspacePath(path.join(repoRelativePath === '.' ? '' : repoRelativePath, relativeDir)),
|
|
315
|
-
})),
|
|
316
|
-
],
|
|
317
|
-
settings: {
|
|
318
|
-
'scm.alwaysShowRepositories': true,
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function ensureParentWorkspaceView(repoRoot, dryRun) {
|
|
325
|
-
const { workspacePath, payload } = buildParentWorkspaceView(repoRoot);
|
|
326
|
-
const operationFile = path.relative(repoRoot, workspacePath) || path.basename(workspacePath);
|
|
327
|
-
const nextContent = `${JSON.stringify(payload, null, 2)}\n`;
|
|
328
|
-
const note = 'parent VS Code workspace view';
|
|
329
|
-
|
|
330
|
-
if (!fs.existsSync(workspacePath)) {
|
|
331
|
-
if (!dryRun) {
|
|
332
|
-
fs.writeFileSync(workspacePath, nextContent, 'utf8');
|
|
333
|
-
}
|
|
334
|
-
return { status: dryRun ? 'would-create' : 'created', file: operationFile, note };
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const currentContent = fs.readFileSync(workspacePath, 'utf8');
|
|
338
|
-
if (currentContent === nextContent) {
|
|
339
|
-
return { status: 'unchanged', file: operationFile, note };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (!dryRun) {
|
|
343
|
-
fs.writeFileSync(workspacePath, nextContent, 'utf8');
|
|
344
|
-
}
|
|
345
|
-
return { status: dryRun ? 'would-update' : 'updated', file: operationFile, note };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function hasGuardexBootstrapFiles(repoRoot) {
|
|
349
|
-
const required = [
|
|
350
|
-
'AGENTS.md',
|
|
351
|
-
'.githooks/pre-commit',
|
|
352
|
-
'.githooks/pre-push',
|
|
353
|
-
LOCK_FILE_RELATIVE,
|
|
354
|
-
];
|
|
355
|
-
return required.every((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)));
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function protectedBaseWriteBlock(options, { requireBootstrap = true } = {}) {
|
|
359
|
-
if (options.dryRun || options.allowProtectedBaseWrite) {
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const repoRoot = resolveRepoRoot(options.target);
|
|
364
|
-
if (requireBootstrap && !hasGuardexBootstrapFiles(repoRoot)) {
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const branch = currentBranchName(repoRoot);
|
|
369
|
-
if (branch !== 'main') {
|
|
370
|
-
return null;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const protectedBranches = readProtectedBranches(repoRoot);
|
|
374
|
-
if (!protectedBranches.includes(branch)) {
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return {
|
|
379
|
-
repoRoot,
|
|
380
|
-
branch,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function assertProtectedMainWriteAllowed(options, commandName) {
|
|
385
|
-
return sandboxModule.assertProtectedMainWriteAllowed(options, commandName);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function runSetupBootstrapInternal(options) {
|
|
389
|
-
const installPayload = runInstallInternal(options);
|
|
390
|
-
installPayload.operations.push(
|
|
391
|
-
ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun)),
|
|
392
|
-
);
|
|
393
|
-
installPayload.operations.push(
|
|
394
|
-
...ensureSubmoduleAutoSync(installPayload.repoRoot, Boolean(options.dryRun)),
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
let parentWorkspace = null;
|
|
398
|
-
if (options.parentWorkspaceView) {
|
|
399
|
-
installPayload.operations.push(
|
|
400
|
-
ensureParentWorkspaceView(installPayload.repoRoot, Boolean(options.dryRun)),
|
|
401
|
-
);
|
|
402
|
-
if (!options.dryRun) {
|
|
403
|
-
parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const fixPayload = runFixInternal({
|
|
408
|
-
target: installPayload.repoRoot,
|
|
409
|
-
dryRun: options.dryRun,
|
|
410
|
-
force: options.force,
|
|
411
|
-
forceManagedPaths: options.forceManagedPaths,
|
|
412
|
-
dropStaleLocks: true,
|
|
413
|
-
skipAgents: options.skipAgents,
|
|
414
|
-
skipPackageJson: options.skipPackageJson,
|
|
415
|
-
skipGitignore: options.skipGitignore,
|
|
416
|
-
allowProtectedBaseWrite: options.allowProtectedBaseWrite,
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
return {
|
|
420
|
-
installPayload,
|
|
421
|
-
fixPayload,
|
|
422
|
-
parentWorkspace,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function extractAgentBranchStartMetadata(output) {
|
|
427
|
-
const outputText = String(output || '');
|
|
428
|
-
const branchMatch = outputText.match(/^\[agent-branch-start\] (?:Created branch|Reusing existing branch): (.+)$/m);
|
|
429
|
-
const worktreeMatch = outputText.match(/^\[agent-branch-start\] Worktree: (.+)$/m);
|
|
430
|
-
return {
|
|
431
|
-
branch: branchMatch ? branchMatch[1].trim() : '',
|
|
432
|
-
worktreePath: worktreeMatch ? worktreeMatch[1].trim() : '',
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
|
|
437
|
-
const resolvedTarget = path.resolve(targetPath);
|
|
438
|
-
const relativeTarget = path.relative(repoRoot, resolvedTarget);
|
|
439
|
-
if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) {
|
|
440
|
-
throw new Error(`sandbox target must stay inside repo root: ${resolvedTarget}`);
|
|
441
|
-
}
|
|
442
|
-
if (!relativeTarget || relativeTarget === '.') {
|
|
443
|
-
return worktreePath;
|
|
444
|
-
}
|
|
445
|
-
return path.join(worktreePath, relativeTarget);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function buildSandboxSetupArgs(options, sandboxTarget) {
|
|
449
|
-
const args = ['setup', '--target', sandboxTarget, '--no-global-install', '--no-recursive'];
|
|
450
|
-
appendForceArgs(args, options);
|
|
451
|
-
if (options.skipAgents) args.push('--skip-agents');
|
|
452
|
-
if (options.skipPackageJson) args.push('--skip-package-json');
|
|
453
|
-
if (options.skipGitignore) args.push('--no-gitignore');
|
|
454
|
-
if (options.dryRun) args.push('--dry-run');
|
|
455
|
-
return args;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function isSpawnFailure(result) {
|
|
459
|
-
return Boolean(result?.error) && typeof result?.status !== 'number';
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function protectedBaseSandboxBranchPrefix() {
|
|
463
|
-
const now = new Date();
|
|
464
|
-
const stamp = [
|
|
465
|
-
now.getUTCFullYear(),
|
|
466
|
-
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
467
|
-
String(now.getUTCDate()).padStart(2, '0'),
|
|
468
|
-
].join('') + '-' + [
|
|
469
|
-
String(now.getUTCHours()).padStart(2, '0'),
|
|
470
|
-
String(now.getUTCMinutes()).padStart(2, '0'),
|
|
471
|
-
String(now.getUTCSeconds()).padStart(2, '0'),
|
|
472
|
-
].join('');
|
|
473
|
-
return `agent/gx/${stamp}`;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function protectedBaseSandboxWorktreePath(repoRoot, branchName) {
|
|
477
|
-
return path.join(repoRoot, defaultAgentWorktreeRelativeDir(), branchName.replace(/\//g, '__'));
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function resolveProtectedBaseSandboxStartRef(repoRoot, baseBranch) {
|
|
481
|
-
run('git', ['-C', repoRoot, 'fetch', 'origin', baseBranch, '--quiet'], { timeout: 20_000 });
|
|
482
|
-
if (gitRefExists(repoRoot, `refs/remotes/origin/${baseBranch}`)) {
|
|
483
|
-
return `origin/${baseBranch}`;
|
|
484
|
-
}
|
|
485
|
-
if (gitRefExists(repoRoot, `refs/heads/${baseBranch}`)) {
|
|
486
|
-
return baseBranch;
|
|
487
|
-
}
|
|
488
|
-
if (currentBranchName(repoRoot) === baseBranch) {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
throw new Error(`Unable to find base ref for sandbox bootstrap: ${baseBranch}`);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function startProtectedBaseSandboxFallback(blocked, sandboxSuffix) {
|
|
495
|
-
const branchPrefix = protectedBaseSandboxBranchPrefix();
|
|
496
|
-
let selectedBranch = '';
|
|
497
|
-
let selectedWorktreePath = '';
|
|
498
|
-
|
|
499
|
-
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
500
|
-
const suffix = attempt === 0 ? sandboxSuffix : `${attempt + 1}-${sandboxSuffix}`;
|
|
501
|
-
const candidateBranch = `${branchPrefix}-${suffix}`;
|
|
502
|
-
const candidateWorktreePath = protectedBaseSandboxWorktreePath(blocked.repoRoot, candidateBranch);
|
|
503
|
-
if (gitRefExists(blocked.repoRoot, `refs/heads/${candidateBranch}`)) {
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
if (fs.existsSync(candidateWorktreePath)) {
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
selectedBranch = candidateBranch;
|
|
510
|
-
selectedWorktreePath = candidateWorktreePath;
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (!selectedBranch || !selectedWorktreePath) {
|
|
515
|
-
throw new Error('Unable to allocate unique sandbox branch/worktree');
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
fs.mkdirSync(path.dirname(selectedWorktreePath), { recursive: true });
|
|
519
|
-
const startRef = resolveProtectedBaseSandboxStartRef(blocked.repoRoot, blocked.branch);
|
|
520
|
-
const addArgs = startRef
|
|
521
|
-
? ['-C', blocked.repoRoot, 'worktree', 'add', '-b', selectedBranch, selectedWorktreePath, startRef]
|
|
522
|
-
: ['-C', blocked.repoRoot, 'worktree', 'add', '--orphan', selectedWorktreePath];
|
|
523
|
-
const addResult = run('git', addArgs);
|
|
524
|
-
if (isSpawnFailure(addResult)) {
|
|
525
|
-
throw addResult.error;
|
|
526
|
-
}
|
|
527
|
-
if (addResult.status !== 0) {
|
|
528
|
-
throw new Error((addResult.stderr || addResult.stdout || 'failed to create sandbox').trim());
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (!startRef) {
|
|
532
|
-
const renameResult = run(
|
|
533
|
-
'git',
|
|
534
|
-
['-C', selectedWorktreePath, 'branch', '-m', selectedBranch],
|
|
535
|
-
{ timeout: 20_000 },
|
|
536
|
-
);
|
|
537
|
-
if (isSpawnFailure(renameResult)) {
|
|
538
|
-
throw renameResult.error;
|
|
539
|
-
}
|
|
540
|
-
if (renameResult.status !== 0) {
|
|
541
|
-
throw new Error(
|
|
542
|
-
(renameResult.stderr || renameResult.stdout || 'failed to name orphan sandbox branch').trim(),
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return {
|
|
548
|
-
metadata: {
|
|
549
|
-
branch: selectedBranch,
|
|
550
|
-
worktreePath: selectedWorktreePath,
|
|
551
|
-
},
|
|
552
|
-
stdout:
|
|
553
|
-
`[agent-branch-start] Created branch: ${selectedBranch}\n` +
|
|
554
|
-
`[agent-branch-start] Worktree: ${selectedWorktreePath}\n`,
|
|
555
|
-
stderr: addResult.stderr || '',
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function startProtectedBaseSandbox(blocked, { taskName, sandboxSuffix }) {
|
|
560
|
-
if (sandboxSuffix === 'gx-doctor') {
|
|
561
|
-
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const startResult = runPackageAsset('branchStart', [
|
|
565
|
-
'--task',
|
|
566
|
-
taskName,
|
|
567
|
-
'--agent',
|
|
568
|
-
SHORT_TOOL_NAME,
|
|
569
|
-
'--base',
|
|
570
|
-
blocked.branch,
|
|
571
|
-
], { cwd: blocked.repoRoot });
|
|
572
|
-
if (isSpawnFailure(startResult)) {
|
|
573
|
-
throw startResult.error;
|
|
574
|
-
}
|
|
575
|
-
if (startResult.status !== 0) {
|
|
576
|
-
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const metadata = extractAgentBranchStartMetadata(startResult.stdout);
|
|
580
|
-
const currentBranch = currentBranchName(blocked.repoRoot);
|
|
581
|
-
const worktreePath = metadata.worktreePath ? path.resolve(metadata.worktreePath) : '';
|
|
582
|
-
const repoRootPath = path.resolve(blocked.repoRoot);
|
|
583
|
-
const hasSafeWorktree = Boolean(worktreePath) && worktreePath !== repoRootPath;
|
|
584
|
-
const branchChanged = Boolean(currentBranch) && currentBranch !== blocked.branch;
|
|
585
|
-
|
|
586
|
-
if (!hasSafeWorktree || branchChanged) {
|
|
587
|
-
const restoreResult = ensureRepoBranch(blocked.repoRoot, blocked.branch);
|
|
588
|
-
if (!restoreResult.ok) {
|
|
589
|
-
const detail = [restoreResult.stderr, restoreResult.stdout].filter(Boolean).join('\n').trim();
|
|
590
|
-
throw new Error(
|
|
591
|
-
`sandbox startup switched protected base checkout and could not restore '${blocked.branch}'.` +
|
|
592
|
-
(detail ? `\n${detail}` : ''),
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
return {
|
|
599
|
-
metadata,
|
|
600
|
-
stdout: startResult.stdout || '',
|
|
601
|
-
stderr: startResult.stderr || '',
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function cleanupProtectedBaseSandbox(repoRoot, metadata) {
|
|
606
|
-
const result = {
|
|
607
|
-
worktree: 'skipped',
|
|
608
|
-
branch: 'skipped',
|
|
609
|
-
note: 'missing sandbox metadata',
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
if (!metadata?.worktreePath || !metadata?.branch) {
|
|
613
|
-
return result;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if (fs.existsSync(metadata.worktreePath)) {
|
|
617
|
-
const removeResult = run(
|
|
618
|
-
'git',
|
|
619
|
-
['-C', repoRoot, 'worktree', 'remove', '--force', metadata.worktreePath],
|
|
620
|
-
{ timeout: 30_000 },
|
|
621
|
-
);
|
|
622
|
-
if (isSpawnFailure(removeResult)) {
|
|
623
|
-
throw removeResult.error;
|
|
624
|
-
}
|
|
625
|
-
if (removeResult.status !== 0) {
|
|
626
|
-
throw new Error(
|
|
627
|
-
(removeResult.stderr || removeResult.stdout || 'failed to remove sandbox worktree').trim(),
|
|
628
|
-
);
|
|
629
|
-
}
|
|
630
|
-
result.worktree = 'removed';
|
|
631
|
-
} else {
|
|
632
|
-
result.worktree = 'missing';
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
if (gitRefExists(repoRoot, `refs/heads/${metadata.branch}`)) {
|
|
636
|
-
const branchDeleteResult = run(
|
|
637
|
-
'git',
|
|
638
|
-
['-C', repoRoot, 'branch', '-D', metadata.branch],
|
|
639
|
-
{ timeout: 20_000 },
|
|
640
|
-
);
|
|
641
|
-
if (isSpawnFailure(branchDeleteResult)) {
|
|
642
|
-
throw branchDeleteResult.error;
|
|
643
|
-
}
|
|
644
|
-
if (branchDeleteResult.status !== 0) {
|
|
645
|
-
throw new Error(
|
|
646
|
-
(branchDeleteResult.stderr || branchDeleteResult.stdout || 'failed to delete sandbox branch').trim(),
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
result.branch = 'deleted';
|
|
650
|
-
} else {
|
|
651
|
-
result.branch = 'missing';
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
result.note = 'sandbox worktree pruned';
|
|
655
|
-
return result;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function runSetupInSandbox(options, blocked, repoLabel = '') {
|
|
659
|
-
const startResult = startProtectedBaseSandbox(blocked, {
|
|
660
|
-
taskName: `${SHORT_TOOL_NAME}-setup`,
|
|
661
|
-
sandboxSuffix: 'gx-setup',
|
|
662
|
-
});
|
|
663
|
-
const metadata = startResult.metadata;
|
|
664
|
-
|
|
665
|
-
if (startResult.stdout) process.stdout.write(startResult.stdout);
|
|
666
|
-
if (startResult.stderr) process.stderr.write(startResult.stderr);
|
|
667
|
-
console.log(
|
|
668
|
-
`[${TOOL_NAME}] setup blocked on protected branch '${blocked.branch}' in an initialized repo; ` +
|
|
669
|
-
'refreshing through a sandbox worktree and syncing managed bootstrap files back locally.',
|
|
670
|
-
);
|
|
671
|
-
|
|
672
|
-
const sandboxTarget = resolveSandboxTarget(blocked.repoRoot, metadata.worktreePath, options.target);
|
|
673
|
-
const nestedResult = run(
|
|
674
|
-
process.execPath,
|
|
675
|
-
[__filename, ...buildSandboxSetupArgs(options, sandboxTarget)],
|
|
676
|
-
{ cwd: metadata.worktreePath, env: { GUARDEX_DOCTOR_SANDBOX: '1' } },
|
|
677
|
-
);
|
|
678
|
-
if (isSpawnFailure(nestedResult)) {
|
|
679
|
-
throw nestedResult.error;
|
|
680
|
-
}
|
|
681
|
-
if (nestedResult.status !== 0) {
|
|
682
|
-
if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
|
|
683
|
-
if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
|
|
684
|
-
throw new Error(
|
|
685
|
-
`sandboxed setup failed for protected branch '${blocked.branch}'. ` +
|
|
686
|
-
`Inspect sandbox at ${metadata.worktreePath}`,
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const syncOptions = {
|
|
691
|
-
...options,
|
|
692
|
-
target: blocked.repoRoot,
|
|
693
|
-
recursive: false,
|
|
694
|
-
allowProtectedBaseWrite: true,
|
|
695
|
-
};
|
|
696
|
-
const { installPayload, fixPayload, parentWorkspace } = runSetupBootstrapInternal(syncOptions);
|
|
697
|
-
printOperations(`Setup/install${repoLabel}`, installPayload, syncOptions.dryRun);
|
|
698
|
-
printOperations(`Setup/fix${repoLabel}`, fixPayload, syncOptions.dryRun);
|
|
699
|
-
if (!syncOptions.dryRun && parentWorkspace) {
|
|
700
|
-
console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const scanResult = runScanInternal({ target: blocked.repoRoot, json: false });
|
|
704
|
-
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
705
|
-
const autoFinishSummary = doctorModule.autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
706
|
-
baseBranch: currentBaseBranch,
|
|
707
|
-
dryRun: syncOptions.dryRun,
|
|
708
|
-
});
|
|
709
|
-
printScanResult(scanResult, false);
|
|
710
|
-
if (autoFinishSummary.enabled) {
|
|
711
|
-
console.log(
|
|
712
|
-
`[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
|
|
713
|
-
);
|
|
714
|
-
for (const detail of autoFinishSummary.details) {
|
|
715
|
-
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
716
|
-
}
|
|
717
|
-
} else if (autoFinishSummary.details.length > 0) {
|
|
718
|
-
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const prunePayload = doctorModule.pruneStaleAgentWorktrees(scanResult.repoRoot, {
|
|
722
|
-
baseBranch: currentBaseBranch,
|
|
723
|
-
dryRun: syncOptions.dryRun,
|
|
724
|
-
});
|
|
725
|
-
printWorktreePruneSummary(prunePayload, { baseBranch: currentBaseBranch });
|
|
726
|
-
|
|
727
|
-
const cleanupResult = cleanupProtectedBaseSandbox(blocked.repoRoot, metadata);
|
|
728
|
-
console.log(
|
|
729
|
-
`[${TOOL_NAME}] Protected-base setup sandbox cleanup: ${cleanupResult.note} ` +
|
|
730
|
-
`(worktree=${cleanupResult.worktree}, branch=${cleanupResult.branch}).`,
|
|
731
|
-
);
|
|
732
|
-
|
|
733
|
-
return {
|
|
734
|
-
scanResult,
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
function todayDateStamp() {
|
|
740
|
-
return new Date().toISOString().slice(0, 10);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function inferGithubRepoFromOrigin(repoRoot) {
|
|
744
|
-
const rawOrigin = readGitConfig(repoRoot, 'remote.origin.url');
|
|
745
|
-
if (!rawOrigin) return '';
|
|
746
|
-
|
|
747
|
-
const httpsMatch = rawOrigin.match(/github\.com[:/](.+?)(?:\.git)?$/i);
|
|
748
|
-
if (!httpsMatch) return '';
|
|
749
|
-
const slug = (httpsMatch[1] || '').replace(/^\/+/, '').trim();
|
|
750
|
-
if (!slug || !slug.includes('/')) return '';
|
|
751
|
-
return `github.com/${slug}`;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function inferGithubRepoSlug(rawValue) {
|
|
755
|
-
const raw = String(rawValue || '').trim();
|
|
756
|
-
if (!raw) return '';
|
|
757
|
-
const match = raw.match(/github\.com[:/](.+?)(?:\.git)?$/i);
|
|
758
|
-
if (!match) return '';
|
|
759
|
-
const slug = String(match[1] || '')
|
|
760
|
-
.replace(/^\/+/, '')
|
|
761
|
-
.replace(/^github\.com\//i, '')
|
|
762
|
-
.trim();
|
|
763
|
-
if (!slug || !slug.includes('/')) return '';
|
|
764
|
-
return slug;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function resolveScorecardRepo(repoRoot, explicitRepo) {
|
|
768
|
-
if (explicitRepo) {
|
|
769
|
-
return explicitRepo.trim();
|
|
770
|
-
}
|
|
771
|
-
const inferred = inferGithubRepoFromOrigin(repoRoot);
|
|
772
|
-
if (inferred) return inferred;
|
|
773
|
-
throw new Error(
|
|
774
|
-
'Unable to infer GitHub repo from origin remote. Pass --repo github.com/<owner>/<repo>.',
|
|
775
|
-
);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function runScorecardJson(repo) {
|
|
779
|
-
const result = run(SCORECARD_BIN, ['--repo', repo, '--format', 'json'], { allowFailure: true });
|
|
780
|
-
if (result.status !== 0) {
|
|
781
|
-
const details = (result.stderr || result.stdout || '').trim();
|
|
782
|
-
throw new Error(
|
|
783
|
-
`Failed to run scorecard CLI ('${SCORECARD_BIN} --repo ${repo} --format json').${details ? `\n${details}` : ''}`,
|
|
784
|
-
);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
try {
|
|
788
|
-
return JSON.parse(result.stdout || '{}');
|
|
789
|
-
} catch (error) {
|
|
790
|
-
throw new Error(`Unable to parse scorecard JSON output: ${error.message}`);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function readScorecardJsonFile(filePath) {
|
|
795
|
-
const absolute = path.resolve(filePath);
|
|
796
|
-
if (!fs.existsSync(absolute)) {
|
|
797
|
-
throw new Error(`scorecard JSON file not found: ${absolute}`);
|
|
798
|
-
}
|
|
799
|
-
try {
|
|
800
|
-
return JSON.parse(fs.readFileSync(absolute, 'utf8'));
|
|
801
|
-
} catch (error) {
|
|
802
|
-
throw new Error(`Unable to parse scorecard JSON file: ${error.message}`);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
function normalizeScorecardChecks(payload) {
|
|
807
|
-
const rawChecks = Array.isArray(payload?.checks) ? payload.checks : [];
|
|
808
|
-
return rawChecks.map((check) => {
|
|
809
|
-
const name = String(check?.name || 'Unknown');
|
|
810
|
-
const rawScore = Number(check?.score);
|
|
811
|
-
const score = Number.isFinite(rawScore) ? rawScore : 0;
|
|
812
|
-
return {
|
|
813
|
-
name,
|
|
814
|
-
score,
|
|
815
|
-
risk: SCORECARD_RISK_BY_CHECK[name] || 'Unknown',
|
|
816
|
-
};
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
function renderScorecardBaselineMarkdown({ repo, score, checks, capturedAt, scorecardVersion, reportDate }) {
|
|
821
|
-
const rows = checks
|
|
822
|
-
.map((item) => `| ${item.name} | ${item.score} | ${item.risk} |`)
|
|
823
|
-
.join('\n');
|
|
824
|
-
|
|
825
|
-
return [
|
|
826
|
-
'# OpenSSF Scorecard Baseline Report',
|
|
827
|
-
'',
|
|
828
|
-
`- **Repository:** \`${repo}\``,
|
|
829
|
-
'- **Source:** generated by `gx report scorecard`',
|
|
830
|
-
`- **Captured at:** ${capturedAt}`,
|
|
831
|
-
`- **Scorecard version:** \`${scorecardVersion}\``,
|
|
832
|
-
`- **Overall score:** **${score} / 10**`,
|
|
833
|
-
'',
|
|
834
|
-
'## Check breakdown',
|
|
835
|
-
'',
|
|
836
|
-
'| Check | Score | Risk |',
|
|
837
|
-
'|---|---:|---|',
|
|
838
|
-
rows || '| (none) | 0 | Unknown |',
|
|
839
|
-
'',
|
|
840
|
-
`## Report date`,
|
|
841
|
-
'',
|
|
842
|
-
`- ${reportDate}`,
|
|
843
|
-
'',
|
|
844
|
-
].join('\n');
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function renderScorecardRemediationPlanMarkdown({ baselineRelativePath, checks }) {
|
|
848
|
-
const failing = checks.filter((item) => item.score < 10);
|
|
849
|
-
const failingRows = failing
|
|
850
|
-
.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name))
|
|
851
|
-
.map((item) => `| ${item.name} | ${item.score} | ${item.risk} |`)
|
|
852
|
-
.join('\n');
|
|
853
|
-
|
|
854
|
-
return [
|
|
855
|
-
'# OpenSSF Scorecard Remediation Plan',
|
|
856
|
-
'',
|
|
857
|
-
`Based on baseline report: \`${baselineRelativePath}\`.`,
|
|
858
|
-
'',
|
|
859
|
-
'## Failing checks',
|
|
860
|
-
'',
|
|
861
|
-
'| Check | Score | Risk |',
|
|
862
|
-
'|---|---:|---|',
|
|
863
|
-
(failingRows || '| None | 10 | N/A |'),
|
|
864
|
-
'',
|
|
865
|
-
'## Priority order',
|
|
866
|
-
'',
|
|
867
|
-
'1. Fix **High** risk checks first (especially score 0 items).',
|
|
868
|
-
'2. Then close **Medium** risk checks with score < 10.',
|
|
869
|
-
'3. Finally address **Low** risk ecosystem/process checks.',
|
|
870
|
-
'',
|
|
871
|
-
'## Verification loop',
|
|
872
|
-
'',
|
|
873
|
-
'1. Run scorecard again.',
|
|
874
|
-
'2. Re-generate baseline + remediation files.',
|
|
875
|
-
'3. Compare score deltas and track improved checks.',
|
|
876
|
-
'',
|
|
877
|
-
].join('\n');
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
function parseBranchList(rawValue) {
|
|
881
|
-
return String(rawValue || '')
|
|
882
|
-
.split(/[\s,]+/)
|
|
883
|
-
.map((item) => item.trim())
|
|
884
|
-
.filter(Boolean);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function originRemoteLooksLikeGithub(repoRoot) {
|
|
888
|
-
const originUrl = readGitConfig(repoRoot, 'remote.origin.url');
|
|
889
|
-
if (!originUrl) {
|
|
890
|
-
return false;
|
|
891
|
-
}
|
|
892
|
-
return /github\.com[:/]/i.test(originUrl);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function isInteractiveTerminal() {
|
|
896
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function legacyDefaultStatusEnabled() {
|
|
900
|
-
return envFlagIsTruthy(process.env.GUARDEX_LEGACY_STATUS);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function defaultCockpitDisabled() {
|
|
904
|
-
const raw = process.env.GUARDEX_DEFAULT_COCKPIT;
|
|
905
|
-
if (raw == null) return false;
|
|
906
|
-
const normalized = String(raw).trim().toLowerCase();
|
|
907
|
-
return ['0', 'false', 'no', 'off'].includes(normalized);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
function parseAutoApproval(name) {
|
|
911
|
-
const raw = process.env[name];
|
|
912
|
-
if (raw == null) return null;
|
|
913
|
-
const normalized = String(raw).trim().toLowerCase();
|
|
914
|
-
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
915
|
-
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
916
|
-
return null;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function parseBooleanLike(raw) {
|
|
920
|
-
if (raw == null) return null;
|
|
921
|
-
const normalized = String(raw).trim().toLowerCase();
|
|
922
|
-
if (!normalized) return null;
|
|
923
|
-
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
924
|
-
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
925
|
-
return null;
|
|
926
|
-
}
|
|
927
|
-
|
|
46
|
+
prompt,
|
|
47
|
+
printAgentsSnippet,
|
|
48
|
+
copyPrompt,
|
|
49
|
+
copyCommands,
|
|
50
|
+
} = require('./commands/prompt');
|
|
51
|
+
const {
|
|
52
|
+
branch,
|
|
53
|
+
pivot,
|
|
54
|
+
ship,
|
|
55
|
+
locks,
|
|
56
|
+
worktree,
|
|
57
|
+
} = require('./commands/branch');
|
|
58
|
+
const {
|
|
59
|
+
cleanup,
|
|
60
|
+
merge,
|
|
61
|
+
finish,
|
|
62
|
+
sync,
|
|
63
|
+
} = require('./commands/finish');
|
|
64
|
+
const {
|
|
65
|
+
hook,
|
|
66
|
+
internal,
|
|
67
|
+
installAgentSkills,
|
|
68
|
+
migrate,
|
|
69
|
+
submodule,
|
|
70
|
+
cockpit,
|
|
71
|
+
protect,
|
|
72
|
+
} = require('./commands/misc');
|
|
73
|
+
|
|
74
|
+
// `gx` (no args) — auto-doctor wiring. Reused only by the default flow.
|
|
928
75
|
function autoDoctorEnabledForCurrentSession() {
|
|
929
76
|
const explicit = parseBooleanLike(process.env.GUARDEX_AUTO_DOCTOR);
|
|
930
77
|
if (explicit != null) {
|
|
@@ -999,2823 +146,13 @@ async function maybeAutoRunDoctorFromDefaultStatus(statusPayload) {
|
|
|
999
146
|
return true;
|
|
1000
147
|
}
|
|
1001
148
|
|
|
1002
|
-
function parseDotenvAssignmentValue(raw) {
|
|
1003
|
-
let value = String(raw || '').trim();
|
|
1004
|
-
if (!value) return '';
|
|
1005
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
|
|
1006
|
-
return value.slice(1, -1).trim();
|
|
1007
|
-
}
|
|
1008
|
-
value = value.replace(/\s+#.*$/, '').trim();
|
|
1009
|
-
return value;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
function readRepoDotenvValue(repoRoot, name) {
|
|
1013
|
-
const envPath = path.join(repoRoot, '.env');
|
|
1014
|
-
if (!fs.existsSync(envPath)) return null;
|
|
1015
|
-
const pattern = new RegExp(`^\\s*(?:export\\s+)?${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*(.*)$`);
|
|
1016
|
-
const lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/);
|
|
1017
|
-
for (const line of lines) {
|
|
1018
|
-
const trimmed = line.trim();
|
|
1019
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
1020
|
-
const match = line.match(pattern);
|
|
1021
|
-
if (!match) continue;
|
|
1022
|
-
return parseDotenvAssignmentValue(match[1]);
|
|
1023
|
-
}
|
|
1024
|
-
return null;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
function resolveGuardexRepoToggle(repoRoot, env = process.env) {
|
|
1028
|
-
const envRaw = env[GUARDEX_REPO_TOGGLE_ENV];
|
|
1029
|
-
const envEnabled = parseBooleanLike(envRaw);
|
|
1030
|
-
if (envEnabled !== null) {
|
|
1031
|
-
return {
|
|
1032
|
-
enabled: envEnabled,
|
|
1033
|
-
source: 'process environment',
|
|
1034
|
-
raw: String(envRaw).trim(),
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
const dotenvRaw = readRepoDotenvValue(repoRoot, GUARDEX_REPO_TOGGLE_ENV);
|
|
1039
|
-
const dotenvEnabled = parseBooleanLike(dotenvRaw);
|
|
1040
|
-
if (dotenvEnabled !== null) {
|
|
1041
|
-
return {
|
|
1042
|
-
enabled: dotenvEnabled,
|
|
1043
|
-
source: 'repo .env',
|
|
1044
|
-
raw: String(dotenvRaw).trim(),
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
return {
|
|
1049
|
-
enabled: true,
|
|
1050
|
-
source: 'default',
|
|
1051
|
-
raw: '',
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
function describeGuardexRepoToggle(toggle) {
|
|
1056
|
-
if (!toggle || toggle.source === 'default') {
|
|
1057
|
-
return 'default enabled mode';
|
|
1058
|
-
}
|
|
1059
|
-
return `${toggle.source} (${GUARDEX_REPO_TOGGLE_ENV}=${toggle.raw})`;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function parseNpmVersionOutput(stdout) {
|
|
1063
|
-
const trimmed = String(stdout || '').trim();
|
|
1064
|
-
if (!trimmed) return '';
|
|
1065
|
-
|
|
1066
|
-
try {
|
|
1067
|
-
const parsed = JSON.parse(trimmed);
|
|
1068
|
-
if (Array.isArray(parsed)) {
|
|
1069
|
-
return String(parsed[parsed.length - 1] || '').trim();
|
|
1070
|
-
}
|
|
1071
|
-
return String(parsed || '').trim();
|
|
1072
|
-
} catch {
|
|
1073
|
-
const firstLine = trimmed.split('\n').map((line) => line.trim()).find(Boolean);
|
|
1074
|
-
return firstLine || '';
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
function checkForGuardexUpdate() {
|
|
1079
|
-
if (envFlagIsTruthy(process.env.GUARDEX_SKIP_UPDATE_CHECK)) {
|
|
1080
|
-
return { checked: false, reason: 'disabled' };
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
const forceCheck = envFlagIsTruthy(process.env.GUARDEX_FORCE_UPDATE_CHECK);
|
|
1084
|
-
if (!forceCheck && !isInteractiveTerminal()) {
|
|
1085
|
-
return { checked: false, reason: 'non-interactive' };
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
const result = run(NPM_BIN, ['view', packageJson.name, 'version', '--json'], { timeout: 5000 });
|
|
1089
|
-
if (result.status !== 0) {
|
|
1090
|
-
return { checked: false, reason: 'lookup-failed' };
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
const latest = parseNpmVersionOutput(result.stdout);
|
|
1094
|
-
if (!latest) {
|
|
1095
|
-
return { checked: false, reason: 'invalid-latest-version' };
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
return {
|
|
1099
|
-
checked: true,
|
|
1100
|
-
current: packageJson.version,
|
|
1101
|
-
latest,
|
|
1102
|
-
updateAvailable: isNewerVersion(latest, packageJson.version),
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
function printUpdateAvailableBanner(current, latest) {
|
|
1107
|
-
const title = colorize('UPDATE AVAILABLE', '1;33');
|
|
1108
|
-
console.log(`[${TOOL_NAME}] ${title}`);
|
|
1109
|
-
console.log(`[${TOOL_NAME}] Current: ${current}`);
|
|
1110
|
-
console.log(`[${TOOL_NAME}] Latest : ${latest}`);
|
|
1111
|
-
console.log(`[${TOOL_NAME}] Command: ${NPM_BIN} i -g ${packageJson.name}@latest`);
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
function maybeSelfUpdateBeforeStatus() {
|
|
1115
|
-
return toolchainModule.maybeSelfUpdateBeforeStatus();
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
function readInstalledGuardexVersion() {
|
|
1119
|
-
const installInfo = readInstalledGuardexInstallInfo();
|
|
1120
|
-
return installInfo ? installInfo.version : null;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
function readInstalledGuardexInstallInfo() {
|
|
1124
|
-
// Resolves the globally-installed package's on-disk version so we can
|
|
1125
|
-
// verify npm actually wrote new bytes. Uses `npm root -g` to locate the
|
|
1126
|
-
// global install root so we don't accidentally read the running source
|
|
1127
|
-
// tree (which is the file the CLI was spawned from — that IS the global
|
|
1128
|
-
// copy in the normal case, but a bump should be visible via a fresh read
|
|
1129
|
-
// either way). Returns null if we can't determine it.
|
|
1130
|
-
try {
|
|
1131
|
-
const rootResult = run(NPM_BIN, ['root', '-g'], { timeout: 5000 });
|
|
1132
|
-
if (rootResult.status !== 0) {
|
|
1133
|
-
return null;
|
|
1134
|
-
}
|
|
1135
|
-
const globalRoot = String(rootResult.stdout || '').trim();
|
|
1136
|
-
if (!globalRoot) {
|
|
1137
|
-
return null;
|
|
1138
|
-
}
|
|
1139
|
-
const installedPkgPath = path.join(globalRoot, packageJson.name, 'package.json');
|
|
1140
|
-
if (!fs.existsSync(installedPkgPath)) {
|
|
1141
|
-
return null;
|
|
1142
|
-
}
|
|
1143
|
-
const parsed = JSON.parse(fs.readFileSync(installedPkgPath, 'utf8'));
|
|
1144
|
-
if (parsed && typeof parsed.version === 'string') {
|
|
1145
|
-
let binRelative = null;
|
|
1146
|
-
if (typeof parsed.bin === 'string') {
|
|
1147
|
-
binRelative = parsed.bin;
|
|
1148
|
-
} else if (parsed.bin && typeof parsed.bin === 'object') {
|
|
1149
|
-
const invokedName = path.basename(process.argv[1] || '');
|
|
1150
|
-
binRelative =
|
|
1151
|
-
parsed.bin[invokedName] ||
|
|
1152
|
-
parsed.bin[SHORT_TOOL_NAME] ||
|
|
1153
|
-
Object.values(parsed.bin).find((value) => typeof value === 'string') ||
|
|
1154
|
-
null;
|
|
1155
|
-
}
|
|
1156
|
-
const packageRoot = path.dirname(installedPkgPath);
|
|
1157
|
-
const binPath = binRelative ? path.join(packageRoot, binRelative) : null;
|
|
1158
|
-
return {
|
|
1159
|
-
version: parsed.version,
|
|
1160
|
-
packageRoot,
|
|
1161
|
-
binPath,
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
} catch (error) {
|
|
1165
|
-
return null;
|
|
1166
|
-
}
|
|
1167
|
-
return null;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
function restartIntoUpdatedGuardex(expectedVersion) {
|
|
1171
|
-
const installInfo = readInstalledGuardexInstallInfo();
|
|
1172
|
-
if (!installInfo || installInfo.version !== expectedVersion || installInfo.version === packageJson.version) {
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1175
|
-
if (!installInfo.binPath || !fs.existsSync(installInfo.binPath)) {
|
|
1176
|
-
console.log(`[${TOOL_NAME}] Restart required to use ${installInfo.version}. Rerun ${SHORT_TOOL_NAME}.`);
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
console.log(`[${TOOL_NAME}] Restarting into ${installInfo.version}…`);
|
|
1181
|
-
const restartResult = cp.spawnSync(
|
|
1182
|
-
process.execPath,
|
|
1183
|
-
[installInfo.binPath, ...process.argv.slice(2)],
|
|
1184
|
-
{
|
|
1185
|
-
cwd: process.cwd(),
|
|
1186
|
-
env: {
|
|
1187
|
-
...process.env,
|
|
1188
|
-
GUARDEX_SKIP_UPDATE_CHECK: '1',
|
|
1189
|
-
},
|
|
1190
|
-
stdio: 'inherit',
|
|
1191
|
-
},
|
|
1192
|
-
);
|
|
1193
|
-
if (restartResult.error) {
|
|
1194
|
-
console.log(
|
|
1195
|
-
`[${TOOL_NAME}] Restart into ${installInfo.version} failed. Rerun ${SHORT_TOOL_NAME}.`,
|
|
1196
|
-
);
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
process.exit(restartResult.status == null ? 0 : restartResult.status);
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
function checkForOpenSpecPackageUpdate() {
|
|
1203
|
-
if (envFlagIsTruthy(process.env.GUARDEX_SKIP_OPENSPEC_UPDATE_CHECK)) {
|
|
1204
|
-
return { checked: false, reason: 'disabled' };
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
const forceCheck = envFlagIsTruthy(process.env.GUARDEX_FORCE_OPENSPEC_UPDATE_CHECK);
|
|
1208
|
-
if (!forceCheck && !isInteractiveTerminal()) {
|
|
1209
|
-
return { checked: false, reason: 'non-interactive' };
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
const detection = detectGlobalToolchainPackages();
|
|
1213
|
-
if (!detection.ok) {
|
|
1214
|
-
return { checked: false, reason: 'package-detect-failed' };
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const current = String((detection.installedVersions || {})[OPENSPEC_PACKAGE] || '').trim();
|
|
1218
|
-
if (!current) {
|
|
1219
|
-
return { checked: false, reason: 'not-installed' };
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
const latestResult = run(NPM_BIN, ['view', OPENSPEC_PACKAGE, 'version', '--json'], { timeout: 5000 });
|
|
1223
|
-
if (latestResult.status !== 0) {
|
|
1224
|
-
return { checked: false, reason: 'lookup-failed' };
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
const latest = parseNpmVersionOutput(latestResult.stdout);
|
|
1228
|
-
if (!latest) {
|
|
1229
|
-
return { checked: false, reason: 'invalid-latest-version' };
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
return {
|
|
1233
|
-
checked: true,
|
|
1234
|
-
current,
|
|
1235
|
-
latest,
|
|
1236
|
-
updateAvailable: isNewerVersion(latest, current),
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function printOpenSpecUpdateAvailableBanner(current, latest) {
|
|
1241
|
-
const title = colorize('OPENSPEC UPDATE AVAILABLE', '1;33');
|
|
1242
|
-
console.log(`[${TOOL_NAME}] ${title}`);
|
|
1243
|
-
console.log(`[${TOOL_NAME}] Current: ${current}`);
|
|
1244
|
-
console.log(`[${TOOL_NAME}] Latest : ${latest}`);
|
|
1245
|
-
console.log(`[${TOOL_NAME}] Command: ${NPM_BIN} i -g ${OPENSPEC_PACKAGE}@latest`);
|
|
1246
|
-
console.log(`[${TOOL_NAME}] Then : ${OPENSPEC_BIN} update`);
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
function maybeOpenSpecUpdateBeforeStatus() {
|
|
1250
|
-
return toolchainModule.maybeOpenSpecUpdateBeforeStatus();
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
function promptYesNoStrict(question) {
|
|
1254
|
-
while (true) {
|
|
1255
|
-
process.stdout.write(`${question} [y/n] `);
|
|
1256
|
-
const answer = readSingleLineFromStdin().trim().toLowerCase();
|
|
1257
|
-
|
|
1258
|
-
if (answer === 'y' || answer === 'yes') {
|
|
1259
|
-
process.stdout.write('\n');
|
|
1260
|
-
return true;
|
|
1261
|
-
}
|
|
1262
|
-
if (answer === 'n' || answer === 'no') {
|
|
1263
|
-
process.stdout.write('\n');
|
|
1264
|
-
return false;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
process.stdout.write('Please answer with y or n.\n');
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
function resolveGlobalInstallApproval(options) {
|
|
1272
|
-
if (options.yesGlobalInstall && options.noGlobalInstall) {
|
|
1273
|
-
throw new Error('Cannot use both --yes-global-install and --no-global-install');
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
if (options.yesGlobalInstall) {
|
|
1277
|
-
return { approved: true, source: 'flag' };
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
if (options.noGlobalInstall) {
|
|
1281
|
-
return { approved: false, source: 'flag' };
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
if (!isInteractiveTerminal()) {
|
|
1285
|
-
return { approved: false, source: 'non-interactive-default' };
|
|
1286
|
-
}
|
|
1287
|
-
return { approved: true, source: 'prompt' };
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
function getGlobalToolchainService(packageName) {
|
|
1291
|
-
const service = GLOBAL_TOOLCHAIN_SERVICES.find(
|
|
1292
|
-
(candidate) => candidate.packageName === packageName,
|
|
1293
|
-
);
|
|
1294
|
-
return service || { name: packageName, packageName };
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
function formatGlobalToolchainServiceName(packageName) {
|
|
1298
|
-
return getGlobalToolchainService(packageName).name;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
function describeMissingGlobalDependencyWarnings(packageNames) {
|
|
1302
|
-
return packageNames
|
|
1303
|
-
.map((packageName) => getGlobalToolchainService(packageName))
|
|
1304
|
-
.filter((service) => service.dependencyUrl)
|
|
1305
|
-
.map(
|
|
1306
|
-
(service) =>
|
|
1307
|
-
`Guardex needs ${service.name} as a dependency: ${service.dependencyUrl}`,
|
|
1308
|
-
);
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
function buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools) {
|
|
1312
|
-
const dependencyWarnings = describeMissingGlobalDependencyWarnings(missingPackages);
|
|
1313
|
-
const installCommands = describeCompanionInstallCommands(missingPackages, missingLocalTools);
|
|
1314
|
-
const dependencyPrefix = dependencyWarnings.length > 0
|
|
1315
|
-
? `${dependencyWarnings.join(' ')} `
|
|
1316
|
-
: '';
|
|
1317
|
-
return `${dependencyPrefix}Install missing companion tools now? (${installCommands.join(' && ')})`;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function detectGlobalToolchainPackages() {
|
|
1321
|
-
const result = run(NPM_BIN, ['list', '-g', '--depth=0', '--json']);
|
|
1322
|
-
if (result.status !== 0) {
|
|
1323
|
-
const stderr = (result.stderr || '').trim();
|
|
1324
|
-
return {
|
|
1325
|
-
ok: false,
|
|
1326
|
-
error: stderr || 'Unable to detect globally installed npm packages',
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
let parsed;
|
|
1331
|
-
try {
|
|
1332
|
-
parsed = JSON.parse(result.stdout || '{}');
|
|
1333
|
-
} catch (error) {
|
|
1334
|
-
return {
|
|
1335
|
-
ok: false,
|
|
1336
|
-
error: `Failed to parse npm list output: ${error.message}`,
|
|
1337
|
-
};
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
const dependencyMap = parsed && parsed.dependencies && typeof parsed.dependencies === 'object'
|
|
1341
|
-
? parsed.dependencies
|
|
1342
|
-
: {};
|
|
1343
|
-
const installedSet = new Set(Object.keys(dependencyMap));
|
|
1344
|
-
|
|
1345
|
-
const installed = [];
|
|
1346
|
-
const missing = [];
|
|
1347
|
-
const installedVersions = {};
|
|
1348
|
-
for (const pkg of GLOBAL_TOOLCHAIN_PACKAGES) {
|
|
1349
|
-
if (installedSet.has(pkg)) {
|
|
1350
|
-
installed.push(pkg);
|
|
1351
|
-
const rawVersion = dependencyMap[pkg] && dependencyMap[pkg].version;
|
|
1352
|
-
const version = String(rawVersion || '').trim();
|
|
1353
|
-
if (version) {
|
|
1354
|
-
installedVersions[pkg] = version;
|
|
1355
|
-
}
|
|
1356
|
-
} else {
|
|
1357
|
-
missing.push(pkg);
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
return { ok: true, installed, missing, installedVersions };
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
function detectRequiredSystemTools() {
|
|
1365
|
-
const services = [];
|
|
1366
|
-
for (const tool of REQUIRED_SYSTEM_TOOLS) {
|
|
1367
|
-
const result = run(tool.command, ['--version']);
|
|
1368
|
-
const active = result.status === 0;
|
|
1369
|
-
const rawReason = result.error && result.error.code
|
|
1370
|
-
? result.error.code
|
|
1371
|
-
: (result.stderr || '').trim();
|
|
1372
|
-
const reason = rawReason.split('\n')[0] || '';
|
|
1373
|
-
services.push({
|
|
1374
|
-
name: tool.name,
|
|
1375
|
-
displayName: tool.displayName || tool.name,
|
|
1376
|
-
command: tool.command,
|
|
1377
|
-
installHint: tool.installHint,
|
|
1378
|
-
status: active ? 'active' : 'inactive',
|
|
1379
|
-
reason,
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
|
-
return services;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
function detectOptionalLocalCompanionTools() {
|
|
1386
|
-
return OPTIONAL_LOCAL_COMPANION_TOOLS.map((tool) => {
|
|
1387
|
-
const detectedPath = tool.candidatePaths
|
|
1388
|
-
.map((relativePath) => path.join(GUARDEX_HOME_DIR, relativePath))
|
|
1389
|
-
.find((candidatePath) => fs.existsSync(candidatePath));
|
|
1390
|
-
return {
|
|
1391
|
-
name: tool.name,
|
|
1392
|
-
displayName: tool.displayName || tool.name,
|
|
1393
|
-
installCommand: tool.installCommand,
|
|
1394
|
-
installArgs: [...tool.installArgs],
|
|
1395
|
-
status: detectedPath ? 'active' : 'inactive',
|
|
1396
|
-
detectedPath: detectedPath || null,
|
|
1397
|
-
};
|
|
1398
|
-
});
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
function describeCompanionInstallCommands(missingPackages, missingLocalTools) {
|
|
1402
|
-
const commands = [];
|
|
1403
|
-
if (missingPackages.length > 0) {
|
|
1404
|
-
commands.push(`${NPM_BIN} i -g ${missingPackages.join(' ')}`);
|
|
1405
|
-
}
|
|
1406
|
-
for (const tool of missingLocalTools) {
|
|
1407
|
-
commands.push(tool.installCommand);
|
|
1408
|
-
}
|
|
1409
|
-
return commands;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
function askGlobalInstallForMissing(options, missingPackages, missingLocalTools) {
|
|
1413
|
-
const approval = resolveGlobalInstallApproval(options);
|
|
1414
|
-
if (!approval.approved) {
|
|
1415
|
-
return approval;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
if (approval.source === 'prompt') {
|
|
1419
|
-
const approved = promptYesNoStrict(
|
|
1420
|
-
buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools),
|
|
1421
|
-
);
|
|
1422
|
-
return { approved, source: 'prompt' };
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
return approval;
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
function installGlobalToolchain(options) {
|
|
1429
|
-
return toolchainModule.installGlobalToolchain(options);
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
function findStaleLockPaths(repoRoot, locks) {
|
|
1433
|
-
const stale = [];
|
|
1434
|
-
|
|
1435
|
-
for (const [filePath, rawEntry] of Object.entries(locks)) {
|
|
1436
|
-
const entry = rawEntry && typeof rawEntry === 'object' ? rawEntry : {};
|
|
1437
|
-
const ownerBranch = String(entry.branch || '');
|
|
1438
|
-
|
|
1439
|
-
const hasOwner = ownerBranch.length > 0;
|
|
1440
|
-
const localRef = hasOwner ? `refs/heads/${ownerBranch}` : null;
|
|
1441
|
-
const remoteRef = hasOwner ? `refs/remotes/origin/${ownerBranch}` : null;
|
|
1442
|
-
const branchExists = hasOwner
|
|
1443
|
-
? gitRefExists(repoRoot, localRef) || gitRefExists(repoRoot, remoteRef)
|
|
1444
|
-
: false;
|
|
1445
|
-
|
|
1446
|
-
const pathExists = fs.existsSync(path.join(repoRoot, filePath));
|
|
1447
|
-
|
|
1448
|
-
if (!hasOwner || !branchExists || !pathExists) {
|
|
1449
|
-
stale.push(filePath);
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
return stale;
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
function runInstallInternal(options) {
|
|
1457
|
-
const repoRoot = resolveRepoRoot(options.target);
|
|
1458
|
-
const guardexToggle = resolveGuardexRepoToggle(repoRoot);
|
|
1459
|
-
if (!guardexToggle.enabled) {
|
|
1460
|
-
return {
|
|
1461
|
-
repoRoot,
|
|
1462
|
-
operations: [
|
|
1463
|
-
{
|
|
1464
|
-
status: 'skipped',
|
|
1465
|
-
file: '.env',
|
|
1466
|
-
note: `Guardex disabled by ${describeGuardexRepoToggle(guardexToggle)}`,
|
|
1467
|
-
},
|
|
1468
|
-
],
|
|
1469
|
-
hookResult: { status: 'skipped', key: 'core.hooksPath', value: '(unchanged)' },
|
|
1470
|
-
guardexEnabled: false,
|
|
1471
|
-
guardexToggle,
|
|
1472
|
-
};
|
|
1473
|
-
}
|
|
1474
|
-
const operations = [];
|
|
1475
|
-
|
|
1476
|
-
if (!options.skipGitignore) {
|
|
1477
|
-
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
1478
|
-
}
|
|
1479
|
-
operations.push(ensureRepoVscodeSettings(repoRoot, Boolean(options.dryRun)));
|
|
1480
|
-
|
|
1481
|
-
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
1482
|
-
|
|
1483
|
-
for (const templateFile of TEMPLATE_FILES) {
|
|
1484
|
-
operations.push(
|
|
1485
|
-
copyTemplateFile(
|
|
1486
|
-
repoRoot,
|
|
1487
|
-
templateFile,
|
|
1488
|
-
shouldForceManagedPath(options, toDestinationPath(templateFile)),
|
|
1489
|
-
Boolean(options.dryRun),
|
|
1490
|
-
),
|
|
1491
|
-
);
|
|
1492
|
-
}
|
|
1493
|
-
operations.push(...materializePackageRepoTemplateFiles(repoRoot, TEMPLATE_FILES, Boolean(options.dryRun)));
|
|
1494
|
-
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
|
|
1495
|
-
for (const hookName of HOOK_NAMES) {
|
|
1496
|
-
const hookRelativePath = path.posix.join('.githooks', hookName);
|
|
1497
|
-
operations.push(
|
|
1498
|
-
ensureHookShim(repoRoot, hookName, {
|
|
1499
|
-
dryRun: options.dryRun,
|
|
1500
|
-
force: shouldForceManagedPath(options, hookRelativePath),
|
|
1501
|
-
}),
|
|
1502
|
-
);
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
1506
|
-
|
|
1507
|
-
if (!options.skipAgents) {
|
|
1508
|
-
operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
1509
|
-
operations.push(ensureClaudeAgentsLink(repoRoot, Boolean(options.dryRun)));
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
|
|
1513
|
-
|
|
1514
|
-
return { repoRoot, operations, hookResult, guardexEnabled: true, guardexToggle };
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
function runFixInternal(options) {
|
|
1518
|
-
const repoRoot = resolveRepoRoot(options.target);
|
|
1519
|
-
const guardexToggle = resolveGuardexRepoToggle(repoRoot);
|
|
1520
|
-
if (!guardexToggle.enabled) {
|
|
1521
|
-
return {
|
|
1522
|
-
repoRoot,
|
|
1523
|
-
operations: [
|
|
1524
|
-
{
|
|
1525
|
-
status: 'skipped',
|
|
1526
|
-
file: '.env',
|
|
1527
|
-
note: `Guardex disabled by ${describeGuardexRepoToggle(guardexToggle)}`,
|
|
1528
|
-
},
|
|
1529
|
-
],
|
|
1530
|
-
hookResult: { status: 'skipped', key: 'core.hooksPath', value: '(unchanged)' },
|
|
1531
|
-
guardexEnabled: false,
|
|
1532
|
-
guardexToggle,
|
|
1533
|
-
};
|
|
1534
|
-
}
|
|
1535
|
-
const operations = [];
|
|
1536
|
-
|
|
1537
|
-
if (!options.skipGitignore) {
|
|
1538
|
-
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
1539
|
-
}
|
|
1540
|
-
operations.push(ensureRepoVscodeSettings(repoRoot, Boolean(options.dryRun)));
|
|
1541
|
-
|
|
1542
|
-
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
1543
|
-
|
|
1544
|
-
for (const templateFile of TEMPLATE_FILES) {
|
|
1545
|
-
if (shouldForceManagedPath(options, toDestinationPath(templateFile))) {
|
|
1546
|
-
operations.push(copyTemplateFile(repoRoot, templateFile, true, Boolean(options.dryRun)));
|
|
1547
|
-
continue;
|
|
1548
|
-
}
|
|
1549
|
-
operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
|
|
1550
|
-
}
|
|
1551
|
-
operations.push(...materializePackageRepoTemplateFiles(repoRoot, TEMPLATE_FILES, Boolean(options.dryRun)));
|
|
1552
|
-
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
|
|
1553
|
-
for (const hookName of HOOK_NAMES) {
|
|
1554
|
-
const hookRelativePath = path.posix.join('.githooks', hookName);
|
|
1555
|
-
operations.push(
|
|
1556
|
-
ensureHookShim(repoRoot, hookName, {
|
|
1557
|
-
dryRun: options.dryRun,
|
|
1558
|
-
force: shouldForceManagedPath(options, hookRelativePath),
|
|
1559
|
-
}),
|
|
1560
|
-
);
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
1564
|
-
|
|
1565
|
-
const lockState = lockStateOrError(repoRoot);
|
|
1566
|
-
if (!lockState.ok) {
|
|
1567
|
-
if (!options.dryRun) {
|
|
1568
|
-
writeLockState(repoRoot, { locks: {} }, false);
|
|
1569
|
-
}
|
|
1570
|
-
operations.push({
|
|
1571
|
-
status: options.dryRun ? 'would-reset' : 'reset',
|
|
1572
|
-
file: LOCK_FILE_RELATIVE,
|
|
1573
|
-
note: 'invalid lock state reset to empty',
|
|
1574
|
-
});
|
|
1575
|
-
} else {
|
|
1576
|
-
const staleLockPaths = options.dropStaleLocks ? findStaleLockPaths(repoRoot, lockState.locks) : [];
|
|
1577
|
-
if (staleLockPaths.length > 0) {
|
|
1578
|
-
const updated = { ...lockState.raw, locks: { ...lockState.locks } };
|
|
1579
|
-
for (const filePath of staleLockPaths) {
|
|
1580
|
-
delete updated.locks[filePath];
|
|
1581
|
-
}
|
|
1582
|
-
writeLockState(repoRoot, updated, Boolean(options.dryRun));
|
|
1583
|
-
operations.push({
|
|
1584
|
-
status: options.dryRun ? 'would-prune' : 'pruned',
|
|
1585
|
-
file: LOCK_FILE_RELATIVE,
|
|
1586
|
-
note: `removed ${staleLockPaths.length} stale lock(s)`,
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
if (!options.skipAgents) {
|
|
1592
|
-
operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
1593
|
-
operations.push(ensureClaudeAgentsLink(repoRoot, Boolean(options.dryRun)));
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
|
|
1597
|
-
|
|
1598
|
-
return { repoRoot, operations, hookResult, guardexEnabled: true, guardexToggle };
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
function runScanInternal(options) {
|
|
1602
|
-
const repoRoot = resolveRepoRoot(options.target);
|
|
1603
|
-
const guardexToggle = resolveGuardexRepoToggle(repoRoot);
|
|
1604
|
-
const branch = readBranchDisplayName(repoRoot);
|
|
1605
|
-
if (!guardexToggle.enabled) {
|
|
1606
|
-
return {
|
|
1607
|
-
repoRoot,
|
|
1608
|
-
branch,
|
|
1609
|
-
findings: [],
|
|
1610
|
-
errors: 0,
|
|
1611
|
-
warnings: 0,
|
|
1612
|
-
guardexEnabled: false,
|
|
1613
|
-
guardexToggle,
|
|
1614
|
-
};
|
|
1615
|
-
}
|
|
1616
|
-
const findings = [];
|
|
1617
|
-
|
|
1618
|
-
const requiredPaths = [
|
|
1619
|
-
...OMX_SCAFFOLD_DIRECTORIES,
|
|
1620
|
-
...Array.from(OMX_SCAFFOLD_FILES.keys()),
|
|
1621
|
-
...REQUIRED_MANAGED_REPO_FILES,
|
|
1622
|
-
];
|
|
1623
|
-
|
|
1624
|
-
for (const relativePath of requiredPaths) {
|
|
1625
|
-
const absolutePath = path.join(repoRoot, relativePath);
|
|
1626
|
-
if (!fs.existsSync(absolutePath)) {
|
|
1627
|
-
findings.push({
|
|
1628
|
-
level: 'error',
|
|
1629
|
-
code: 'missing-managed-file',
|
|
1630
|
-
path: relativePath,
|
|
1631
|
-
message: `Missing managed repo file: ${relativePath}`,
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
const hooksPathResult = gitRun(repoRoot, ['config', '--get', 'core.hooksPath'], { allowFailure: true });
|
|
1637
|
-
const hooksPath = hooksPathResult.status === 0 ? hooksPathResult.stdout.trim() : '';
|
|
1638
|
-
if (hooksPath !== '.githooks') {
|
|
1639
|
-
findings.push({
|
|
1640
|
-
level: 'warn',
|
|
1641
|
-
code: 'hooks-path-mismatch',
|
|
1642
|
-
message: `git core.hooksPath is '${hooksPath || '(unset)'}' (expected '.githooks')`,
|
|
1643
|
-
});
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
const lockState = lockStateOrError(repoRoot);
|
|
1647
|
-
if (!lockState.ok) {
|
|
1648
|
-
findings.push({
|
|
1649
|
-
level: 'error',
|
|
1650
|
-
code: 'lock-state-invalid',
|
|
1651
|
-
message: lockState.error,
|
|
1652
|
-
});
|
|
1653
|
-
} else {
|
|
1654
|
-
for (const [filePath, rawEntry] of Object.entries(lockState.locks)) {
|
|
1655
|
-
const entry = rawEntry && typeof rawEntry === 'object' ? rawEntry : {};
|
|
1656
|
-
const ownerBranch = String(entry.branch || '');
|
|
1657
|
-
const allowDelete = Boolean(entry.allow_delete);
|
|
1658
|
-
|
|
1659
|
-
if (!ownerBranch) {
|
|
1660
|
-
findings.push({
|
|
1661
|
-
level: 'warn',
|
|
1662
|
-
code: 'lock-missing-owner',
|
|
1663
|
-
path: filePath,
|
|
1664
|
-
message: `Lock entry has no owner branch: ${filePath}`,
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
const absolutePath = path.join(repoRoot, filePath);
|
|
1669
|
-
if (!fs.existsSync(absolutePath)) {
|
|
1670
|
-
findings.push({
|
|
1671
|
-
level: 'warn',
|
|
1672
|
-
code: 'lock-target-missing',
|
|
1673
|
-
path: filePath,
|
|
1674
|
-
message: `Locked path is missing from disk: ${filePath}`,
|
|
1675
|
-
});
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
if (ownerBranch) {
|
|
1679
|
-
const localRef = `refs/heads/${ownerBranch}`;
|
|
1680
|
-
const remoteRef = `refs/remotes/origin/${ownerBranch}`;
|
|
1681
|
-
if (!gitRefExists(repoRoot, localRef) && !gitRefExists(repoRoot, remoteRef)) {
|
|
1682
|
-
findings.push({
|
|
1683
|
-
level: 'warn',
|
|
1684
|
-
code: 'stale-branch-lock',
|
|
1685
|
-
path: filePath,
|
|
1686
|
-
message: `Lock owner branch not found locally/remotely: ${ownerBranch} (${filePath})`,
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
if (allowDelete && CRITICAL_GUARDRAIL_PATHS.has(filePath)) {
|
|
1692
|
-
findings.push({
|
|
1693
|
-
level: 'error',
|
|
1694
|
-
code: 'guardrail-delete-approved',
|
|
1695
|
-
path: filePath,
|
|
1696
|
-
message: `Critical guardrail file is delete-approved: ${filePath}`,
|
|
1697
|
-
});
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
const errors = findings.filter((item) => item.level === 'error');
|
|
1703
|
-
const warnings = findings.filter((item) => item.level === 'warn');
|
|
1704
|
-
|
|
1705
|
-
return {
|
|
1706
|
-
repoRoot,
|
|
1707
|
-
branch,
|
|
1708
|
-
findings,
|
|
1709
|
-
errors: errors.length,
|
|
1710
|
-
warnings: warnings.length,
|
|
1711
|
-
guardexEnabled: true,
|
|
1712
|
-
guardexToggle,
|
|
1713
|
-
};
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
function printWorktreePruneSummary(payload, options = {}) {
|
|
1717
|
-
if (!payload || payload.enabled === false) {
|
|
1718
|
-
if (payload && payload.details && payload.details[0]) {
|
|
1719
|
-
console.log(`[${TOOL_NAME}] ${payload.details[0]}`);
|
|
1720
|
-
}
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
if (!payload.ran) {
|
|
1724
|
-
return;
|
|
1725
|
-
}
|
|
1726
|
-
const baseLabel = options.baseBranch ? ` (base=${options.baseBranch})` : '';
|
|
1727
|
-
const tag = payload.status === 'failed' ? '⚠️' : (payload.status === 'dry-run' ? '🔍' : '🧹');
|
|
1728
|
-
console.log(
|
|
1729
|
-
`[${TOOL_NAME}] ${tag} Stale agent-worktree prune${baseLabel}: status=${payload.status}`,
|
|
1730
|
-
);
|
|
1731
|
-
for (const detail of payload.details || []) {
|
|
1732
|
-
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
function printScanResult(scan, json = false) {
|
|
1737
|
-
if (json) {
|
|
1738
|
-
process.stdout.write(
|
|
1739
|
-
JSON.stringify(
|
|
1740
|
-
{
|
|
1741
|
-
repoRoot: scan.repoRoot,
|
|
1742
|
-
branch: scan.branch,
|
|
1743
|
-
guardexEnabled: scan.guardexEnabled !== false,
|
|
1744
|
-
guardexToggle: scan.guardexToggle || null,
|
|
1745
|
-
errors: scan.errors,
|
|
1746
|
-
warnings: scan.warnings,
|
|
1747
|
-
findings: scan.findings,
|
|
1748
|
-
},
|
|
1749
|
-
null,
|
|
1750
|
-
2,
|
|
1751
|
-
) + '\n',
|
|
1752
|
-
);
|
|
1753
|
-
return;
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
console.log(`[${TOOL_NAME}] Scan target: ${scan.repoRoot}`);
|
|
1757
|
-
console.log(`[${TOOL_NAME}] Branch: ${scan.branch}`);
|
|
1758
|
-
|
|
1759
|
-
if (scan.guardexEnabled === false) {
|
|
1760
|
-
console.log(
|
|
1761
|
-
colorizeDoctorOutput(
|
|
1762
|
-
`[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(scan.guardexToggle)}).`,
|
|
1763
|
-
'disabled',
|
|
1764
|
-
),
|
|
1765
|
-
);
|
|
1766
|
-
return;
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
if (scan.findings.length === 0) {
|
|
1770
|
-
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ✅ No safety issues detected.`, 'safe'));
|
|
1771
|
-
return;
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
for (const item of scan.findings) {
|
|
1775
|
-
const target = item.path ? ` (${item.path})` : '';
|
|
1776
|
-
console.log(
|
|
1777
|
-
colorizeDoctorOutput(
|
|
1778
|
-
`[${item.level.toUpperCase()}] ${item.code}${target}: ${item.message}`,
|
|
1779
|
-
item.level,
|
|
1780
|
-
),
|
|
1781
|
-
);
|
|
1782
|
-
}
|
|
1783
|
-
console.log(
|
|
1784
|
-
colorizeDoctorOutput(
|
|
1785
|
-
`[${TOOL_NAME}] Summary: ${scan.errors} error(s), ${scan.warnings} warning(s).`,
|
|
1786
|
-
scan.errors > 0 ? 'error' : 'warn',
|
|
1787
|
-
),
|
|
1788
|
-
);
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
function setExitCodeFromScan(scan) {
|
|
1792
|
-
if (scan.guardexEnabled === false) {
|
|
1793
|
-
process.exitCode = 0;
|
|
1794
|
-
return;
|
|
1795
|
-
}
|
|
1796
|
-
if (scan.errors > 0) {
|
|
1797
|
-
process.exitCode = 2;
|
|
1798
|
-
return;
|
|
1799
|
-
}
|
|
1800
|
-
if (scan.warnings > 0) {
|
|
1801
|
-
process.exitCode = 1;
|
|
1802
|
-
return;
|
|
1803
|
-
}
|
|
1804
|
-
process.exitCode = 0;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
function printStatusRepairHint(scanResult) {
|
|
1808
|
-
if (!scanResult || scanResult.guardexEnabled === false) {
|
|
1809
|
-
return;
|
|
1810
|
-
}
|
|
1811
|
-
if (scanResult.errors === 0 && scanResult.warnings === 0) {
|
|
1812
|
-
return;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
const scanHint = scanResult.errors === 0
|
|
1816
|
-
? `review warning details with '${SHORT_TOOL_NAME} scan'`
|
|
1817
|
-
: `inspect detailed findings with '${SHORT_TOOL_NAME} scan'`;
|
|
1818
|
-
console.log(
|
|
1819
|
-
`[${TOOL_NAME}] Quick fix: run '${SHORT_TOOL_NAME} doctor' to repair drift, or ${scanHint}.`,
|
|
1820
|
-
);
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
function countAgentWorktrees(repoRoot) {
|
|
1824
|
-
if (!repoRoot || typeof repoRoot !== 'string') return 0;
|
|
1825
|
-
const relPaths = ['.omc/agent-worktrees', '.omx/agent-worktrees'];
|
|
1826
|
-
let count = 0;
|
|
1827
|
-
for (const rel of relPaths) {
|
|
1828
|
-
try {
|
|
1829
|
-
const entries = fs.readdirSync(path.join(repoRoot, rel), { withFileTypes: true });
|
|
1830
|
-
count += entries.filter((entry) => entry.isDirectory()).length;
|
|
1831
|
-
} catch (_err) {
|
|
1832
|
-
// missing dir or permission error; not an active-agent signal
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
return count;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
function deriveNextStepHint({ scanResult, worktreeCount, invoked, inGitRepo }) {
|
|
1839
|
-
if (!inGitRepo) {
|
|
1840
|
-
return `${invoked} setup --target <path-to-git-repo> # initialize guardrails in a repo`;
|
|
1841
|
-
}
|
|
1842
|
-
if (!scanResult) {
|
|
1843
|
-
return `${invoked} setup # bootstrap repo guardrails`;
|
|
1844
|
-
}
|
|
1845
|
-
if (scanResult.guardexEnabled === false) {
|
|
1846
|
-
return `set GUARDEX_ON=1 in .env # re-enable guardrails, then '${invoked} doctor'`;
|
|
1847
|
-
}
|
|
1848
|
-
const branch = scanResult.branch || '';
|
|
1849
|
-
if (branch.startsWith('agent/')) {
|
|
1850
|
-
return `${invoked} branch finish --branch "${branch}" --via-pr --wait-for-merge --cleanup`;
|
|
1851
|
-
}
|
|
1852
|
-
if (worktreeCount > 0) {
|
|
1853
|
-
const plural = worktreeCount === 1 ? 'worktree' : 'worktrees';
|
|
1854
|
-
return `${invoked} finish --all # ${worktreeCount} active agent ${plural}`;
|
|
1855
|
-
}
|
|
1856
|
-
if (scanResult.errors > 0 || scanResult.warnings > 0) {
|
|
1857
|
-
return `${invoked} doctor # repair drift`;
|
|
1858
|
-
}
|
|
1859
|
-
return `${invoked} branch start "<task>" "<agent-name>" # start a sandboxed agent task`;
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
function collectServicesSnapshot() {
|
|
1863
|
-
const toolchain = toolchainModule.detectGlobalToolchainPackages();
|
|
1864
|
-
const npmServices = GLOBAL_TOOLCHAIN_PACKAGES.map((pkg) => {
|
|
1865
|
-
const service = toolchainModule.getGlobalToolchainService(pkg);
|
|
1866
|
-
if (!toolchain.ok) {
|
|
1867
|
-
return {
|
|
1868
|
-
name: service.name,
|
|
1869
|
-
displayName: service.name,
|
|
1870
|
-
packageName: pkg,
|
|
1871
|
-
dependencyUrl: service.dependencyUrl || null,
|
|
1872
|
-
status: 'unknown',
|
|
1873
|
-
};
|
|
1874
|
-
}
|
|
1875
|
-
return {
|
|
1876
|
-
name: service.name,
|
|
1877
|
-
displayName: service.name,
|
|
1878
|
-
packageName: pkg,
|
|
1879
|
-
dependencyUrl: service.dependencyUrl || null,
|
|
1880
|
-
status: toolchain.installed.includes(pkg) ? 'active' : 'inactive',
|
|
1881
|
-
};
|
|
1882
|
-
});
|
|
1883
|
-
const localCompanionServices = toolchainModule.detectOptionalLocalCompanionTools().map((tool) => ({
|
|
1884
|
-
name: tool.name,
|
|
1885
|
-
displayName: tool.displayName || tool.name,
|
|
1886
|
-
installCommand: tool.installCommand,
|
|
1887
|
-
installArgs: Array.isArray(tool.installArgs) ? [...tool.installArgs] : [],
|
|
1888
|
-
status: tool.status,
|
|
1889
|
-
}));
|
|
1890
|
-
const requiredSystemTools = toolchainModule.detectRequiredSystemTools();
|
|
1891
|
-
const services = [
|
|
1892
|
-
...npmServices,
|
|
1893
|
-
...localCompanionServices.map((tool) => ({
|
|
1894
|
-
name: tool.name,
|
|
1895
|
-
displayName: tool.displayName,
|
|
1896
|
-
status: tool.status,
|
|
1897
|
-
})),
|
|
1898
|
-
...requiredSystemTools.map((tool) => ({
|
|
1899
|
-
name: tool.name,
|
|
1900
|
-
displayName: tool.displayName || tool.name,
|
|
1901
|
-
command: tool.command,
|
|
1902
|
-
status: tool.status,
|
|
1903
|
-
})),
|
|
1904
|
-
];
|
|
1905
|
-
return { toolchain, npmServices, localCompanionServices, requiredSystemTools, services };
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
function maybePromptInstallMissingCompanions(snapshot) {
|
|
1909
|
-
if (envFlagIsTruthy(process.env.GUARDEX_SKIP_COMPANION_PROMPT)) {
|
|
1910
|
-
return { handled: false, installed: false };
|
|
1911
|
-
}
|
|
1912
|
-
const interactive = Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY);
|
|
1913
|
-
const autoApproval = toolchainModule.parseAutoApproval('GUARDEX_AUTO_COMPANION_APPROVAL');
|
|
1914
|
-
if (!interactive && autoApproval == null) {
|
|
1915
|
-
return { handled: false, installed: false };
|
|
1916
|
-
}
|
|
1917
|
-
if (!snapshot.toolchain.ok) {
|
|
1918
|
-
return { handled: false, installed: false };
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
const missingPackages = snapshot.npmServices
|
|
1922
|
-
.filter((service) => service.status !== 'active')
|
|
1923
|
-
.map((service) => service.packageName);
|
|
1924
|
-
const missingLocalTools = snapshot.localCompanionServices.filter((tool) => tool.status !== 'active');
|
|
1925
|
-
if (missingPackages.length === 0 && missingLocalTools.length === 0) {
|
|
1926
|
-
return { handled: false, installed: false };
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
const missingNames = [
|
|
1930
|
-
...missingPackages.map((pkg) => toolchainModule.formatGlobalToolchainServiceName(pkg)),
|
|
1931
|
-
...missingLocalTools.map((tool) => tool.displayName || tool.name),
|
|
1932
|
-
];
|
|
1933
|
-
console.log(`[${TOOL_NAME}] Missing companion tools: ${missingNames.join(', ')}.`);
|
|
1934
|
-
|
|
1935
|
-
const promptText = toolchainModule.buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools);
|
|
1936
|
-
const approved = interactive
|
|
1937
|
-
? toolchainModule.promptYesNoStrict(promptText)
|
|
1938
|
-
: autoApproval;
|
|
1939
|
-
|
|
1940
|
-
if (!approved) {
|
|
1941
|
-
console.log(
|
|
1942
|
-
`[${TOOL_NAME}] Skipped companion install. Set GUARDEX_SKIP_COMPANION_PROMPT=1 to silence this prompt, ` +
|
|
1943
|
-
`or run '${getInvokedCliName()} setup --install-only' later to install manually.`,
|
|
1944
|
-
);
|
|
1945
|
-
return { handled: true, installed: false };
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
const result = toolchainModule.performCompanionInstall(missingPackages, missingLocalTools);
|
|
1949
|
-
if (result.status === 'installed') {
|
|
1950
|
-
console.log(
|
|
1951
|
-
`[${TOOL_NAME}] ✅ Companion tools installed (${(result.packages || []).join(', ')}).`,
|
|
1952
|
-
);
|
|
1953
|
-
return { handled: true, installed: true };
|
|
1954
|
-
}
|
|
1955
|
-
if (result.status === 'failed') {
|
|
1956
|
-
console.log(
|
|
1957
|
-
`[${TOOL_NAME}] ⚠️ Companion install failed: ${result.reason}. ` +
|
|
1958
|
-
`Retry with '${getInvokedCliName()} setup --install-only'.`,
|
|
1959
|
-
);
|
|
1960
|
-
return { handled: true, installed: false };
|
|
1961
|
-
}
|
|
1962
|
-
return { handled: true, installed: false };
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
function status(rawArgs) {
|
|
1966
|
-
const { found: verboseFlag, remaining: afterVerbose } = extractFlag(rawArgs, '--verbose');
|
|
1967
|
-
const options = parseCommonArgs(afterVerbose, {
|
|
1968
|
-
target: process.cwd(),
|
|
1969
|
-
json: false,
|
|
1970
|
-
});
|
|
1971
|
-
const forceExpand = envFlagIsTruthy(process.env.GUARDEX_VERBOSE_STATUS) || verboseFlag;
|
|
1972
|
-
const invokedBasename = getInvokedCliName();
|
|
1973
|
-
|
|
1974
|
-
let snapshot = collectServicesSnapshot();
|
|
1975
|
-
if (!options.json) {
|
|
1976
|
-
const result = maybePromptInstallMissingCompanions(snapshot);
|
|
1977
|
-
if (result.installed) {
|
|
1978
|
-
snapshot = collectServicesSnapshot();
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
let { toolchain, npmServices, localCompanionServices, requiredSystemTools, services } = snapshot;
|
|
1982
|
-
|
|
1983
|
-
const targetPath = path.resolve(options.target);
|
|
1984
|
-
const inGitRepo = isGitRepo(targetPath);
|
|
1985
|
-
const scanResult = inGitRepo ? runScanInternal({ target: targetPath, json: false }) : null;
|
|
1986
|
-
const repoServiceStatus = scanResult
|
|
1987
|
-
? (scanResult.guardexEnabled === false
|
|
1988
|
-
? 'disabled'
|
|
1989
|
-
: (scanResult.errors === 0 && scanResult.warnings === 0 ? 'active' : 'degraded'))
|
|
1990
|
-
: 'inactive';
|
|
1991
|
-
|
|
1992
|
-
const payload = {
|
|
1993
|
-
cli: {
|
|
1994
|
-
name: packageJson.name,
|
|
1995
|
-
version: packageJson.version,
|
|
1996
|
-
runtime: runtimeVersion(),
|
|
1997
|
-
},
|
|
1998
|
-
services,
|
|
1999
|
-
repo: {
|
|
2000
|
-
target: targetPath,
|
|
2001
|
-
inGitRepo,
|
|
2002
|
-
serviceStatus: repoServiceStatus,
|
|
2003
|
-
guardexEnabled: scanResult ? scanResult.guardexEnabled !== false : null,
|
|
2004
|
-
guardexToggle: scanResult ? scanResult.guardexToggle || null : null,
|
|
2005
|
-
scan: scanResult
|
|
2006
|
-
? {
|
|
2007
|
-
repoRoot: scanResult.repoRoot,
|
|
2008
|
-
branch: scanResult.branch,
|
|
2009
|
-
errors: scanResult.errors,
|
|
2010
|
-
warnings: scanResult.warnings,
|
|
2011
|
-
findings: scanResult.findings.length,
|
|
2012
|
-
}
|
|
2013
|
-
: null,
|
|
2014
|
-
},
|
|
2015
|
-
detectionError: toolchain.ok ? null : toolchain.error,
|
|
2016
|
-
};
|
|
2017
|
-
|
|
2018
|
-
if (options.json) {
|
|
2019
|
-
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
2020
|
-
process.exitCode = 0;
|
|
2021
|
-
return payload;
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
const compact = !forceExpand;
|
|
2025
|
-
const activeServiceCount = services.filter((service) => service.status === 'active').length;
|
|
2026
|
-
const inactiveServiceCount = services.length - activeServiceCount;
|
|
2027
|
-
|
|
2028
|
-
console.log(`[${TOOL_NAME}] CLI: ${payload.cli.runtime}`);
|
|
2029
|
-
if (!toolchain.ok) {
|
|
2030
|
-
const detectionError = compact
|
|
2031
|
-
? String(toolchain.error || '').split(/\r?\n/).find(Boolean) || 'unknown error'
|
|
2032
|
-
: toolchain.error;
|
|
2033
|
-
console.log(`[${TOOL_NAME}] ⚠️ Could not detect global services: ${detectionError}`);
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
if (compact) {
|
|
2037
|
-
const serviceSummary = inactiveServiceCount === 0
|
|
2038
|
-
? `${activeServiceCount}/${services.length} ${statusDot('active')} active`
|
|
2039
|
-
: `${activeServiceCount}/${services.length} ${statusDot('degraded')} active (${inactiveServiceCount} inactive)`;
|
|
2040
|
-
console.log(
|
|
2041
|
-
`[${TOOL_NAME}] Global services: ${serviceSummary}`,
|
|
2042
|
-
);
|
|
2043
|
-
} else {
|
|
2044
|
-
console.log(`[${TOOL_NAME}] Global services:`);
|
|
2045
|
-
for (const service of services) {
|
|
2046
|
-
const serviceLabel = service.displayName || service.name;
|
|
2047
|
-
console.log(` - ${statusDot(service.status)} ${serviceLabel}: ${service.status}`);
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
const inactiveOptionalCompanions = [...npmServices, ...localCompanionServices]
|
|
2051
|
-
.filter((service) => service.status !== 'active')
|
|
2052
|
-
.map((service) => service.displayName || service.name);
|
|
2053
|
-
if (inactiveOptionalCompanions.length > 0) {
|
|
2054
|
-
if (compact) {
|
|
2055
|
-
console.log(
|
|
2056
|
-
`[${TOOL_NAME}] Optional companion tools inactive: ${inactiveOptionalCompanions.length} (run '${SHORT_TOOL_NAME} setup')`,
|
|
2057
|
-
);
|
|
2058
|
-
} else {
|
|
2059
|
-
console.log(
|
|
2060
|
-
`[${TOOL_NAME}] Optional companion tools inactive: ${inactiveOptionalCompanions.join(', ')}`,
|
|
2061
|
-
);
|
|
2062
|
-
for (const warning of toolchainModule.describeMissingGlobalDependencyWarnings(
|
|
2063
|
-
npmServices
|
|
2064
|
-
.filter((service) => service.status === 'inactive')
|
|
2065
|
-
.map((service) => service.packageName),
|
|
2066
|
-
)) {
|
|
2067
|
-
console.log(`[${TOOL_NAME}] ${warning}`);
|
|
2068
|
-
}
|
|
2069
|
-
console.log(
|
|
2070
|
-
`[${TOOL_NAME}] Run '${SHORT_TOOL_NAME} setup' to install missing companions with an explicit Y/N prompt.`,
|
|
2071
|
-
);
|
|
2072
|
-
}
|
|
2073
|
-
}
|
|
2074
|
-
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
|
|
2075
|
-
if (missingSystemTools.length > 0) {
|
|
2076
|
-
const tools = missingSystemTools
|
|
2077
|
-
.map((tool) => tool.displayName || tool.name)
|
|
2078
|
-
.join(', ');
|
|
2079
|
-
console.log(`[${TOOL_NAME}] ⚠️ Missing required system tool(s): ${tools}`);
|
|
2080
|
-
if (!compact) {
|
|
2081
|
-
for (const tool of missingSystemTools) {
|
|
2082
|
-
const reasonText = tool.reason ? ` (${tool.reason})` : '';
|
|
2083
|
-
console.log(` - install ${tool.name}: ${tool.installHint}${reasonText}`);
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
if (!scanResult) {
|
|
2089
|
-
console.log(
|
|
2090
|
-
`[${TOOL_NAME}] Repo safety service: ${statusDot('inactive')} inactive (no git repository at target).`,
|
|
2091
|
-
);
|
|
2092
|
-
const inactiveHint = deriveNextStepHint({
|
|
2093
|
-
scanResult: null,
|
|
2094
|
-
worktreeCount: 0,
|
|
2095
|
-
invoked: invokedBasename,
|
|
2096
|
-
inGitRepo,
|
|
2097
|
-
});
|
|
2098
|
-
console.log(`[${TOOL_NAME}] Next: ${inactiveHint}`);
|
|
2099
|
-
printToolLogsSummary({ invokedBasename, compact });
|
|
2100
|
-
process.exitCode = 0;
|
|
2101
|
-
return payload;
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
if (scanResult.guardexEnabled === false) {
|
|
2105
|
-
console.log(
|
|
2106
|
-
`[${TOOL_NAME}] Repo safety service: ${statusDot('disabled')} disabled (${describeGuardexRepoToggle(scanResult.guardexToggle)}).`,
|
|
2107
|
-
);
|
|
2108
|
-
console.log(`[${TOOL_NAME}] Repo: ${scanResult.repoRoot}`);
|
|
2109
|
-
console.log(`[${TOOL_NAME}] Branch: ${scanResult.branch}`);
|
|
2110
|
-
const worktreeCountDisabled = countAgentWorktrees(scanResult.repoRoot);
|
|
2111
|
-
if (worktreeCountDisabled > 0) {
|
|
2112
|
-
const plural = worktreeCountDisabled === 1 ? 'worktree' : 'worktrees';
|
|
2113
|
-
console.log(
|
|
2114
|
-
`[${TOOL_NAME}] ⚠ ${worktreeCountDisabled} active agent ${plural} under .omc/agent-worktrees or .omx/agent-worktrees.`,
|
|
2115
|
-
);
|
|
2116
|
-
}
|
|
2117
|
-
const disabledHint = deriveNextStepHint({
|
|
2118
|
-
scanResult,
|
|
2119
|
-
worktreeCount: worktreeCountDisabled,
|
|
2120
|
-
invoked: invokedBasename,
|
|
2121
|
-
inGitRepo,
|
|
2122
|
-
});
|
|
2123
|
-
console.log(`[${TOOL_NAME}] Next: ${disabledHint}`);
|
|
2124
|
-
printToolLogsSummary({ invokedBasename, compact });
|
|
2125
|
-
process.exitCode = 0;
|
|
2126
|
-
return payload;
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
if (scanResult.errors === 0 && scanResult.warnings === 0) {
|
|
2130
|
-
console.log(`[${TOOL_NAME}] Repo safety service: ${statusDot('active')} active.`);
|
|
2131
|
-
} else if (scanResult.errors === 0) {
|
|
2132
|
-
console.log(
|
|
2133
|
-
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.warnings} warning(s)).`,
|
|
2134
|
-
);
|
|
2135
|
-
} else if (scanResult.warnings === 0) {
|
|
2136
|
-
console.log(
|
|
2137
|
-
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.errors} error(s)).`,
|
|
2138
|
-
);
|
|
2139
|
-
} else {
|
|
2140
|
-
console.log(
|
|
2141
|
-
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.errors} error(s), ${scanResult.warnings} warning(s)).`,
|
|
2142
|
-
);
|
|
2143
|
-
}
|
|
2144
|
-
printStatusRepairHint(scanResult);
|
|
2145
|
-
console.log(`[${TOOL_NAME}] Repo: ${scanResult.repoRoot}`);
|
|
2146
|
-
console.log(`[${TOOL_NAME}] Branch: ${scanResult.branch}`);
|
|
2147
|
-
const worktreeCountActive = countAgentWorktrees(scanResult.repoRoot);
|
|
2148
|
-
if (worktreeCountActive > 0) {
|
|
2149
|
-
const plural = worktreeCountActive === 1 ? 'worktree' : 'worktrees';
|
|
2150
|
-
console.log(
|
|
2151
|
-
`[${TOOL_NAME}] ⚠ ${worktreeCountActive} active agent ${plural} → ${invokedBasename} finish --all`,
|
|
2152
|
-
);
|
|
2153
|
-
}
|
|
2154
|
-
const activeHint = deriveNextStepHint({
|
|
2155
|
-
scanResult,
|
|
2156
|
-
worktreeCount: worktreeCountActive,
|
|
2157
|
-
invoked: invokedBasename,
|
|
2158
|
-
inGitRepo,
|
|
2159
|
-
});
|
|
2160
|
-
console.log(`[${TOOL_NAME}] Next: ${activeHint}`);
|
|
2161
|
-
printToolLogsSummary({ invokedBasename, compact });
|
|
2162
|
-
|
|
2163
|
-
process.exitCode = 0;
|
|
2164
|
-
return payload;
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
function install(rawArgs) {
|
|
2168
|
-
const options = parseCommonArgs(rawArgs, {
|
|
2169
|
-
target: process.cwd(),
|
|
2170
|
-
force: false,
|
|
2171
|
-
skipAgents: false,
|
|
2172
|
-
skipPackageJson: false,
|
|
2173
|
-
skipGitignore: false,
|
|
2174
|
-
dryRun: false,
|
|
2175
|
-
allowProtectedBaseWrite: false,
|
|
2176
|
-
});
|
|
2177
|
-
|
|
2178
|
-
assertProtectedMainWriteAllowed(options, 'install');
|
|
2179
|
-
const payload = runInstallInternal(options);
|
|
2180
|
-
printOperations('Install target', payload, options.dryRun);
|
|
2181
|
-
|
|
2182
|
-
if (!options.dryRun) {
|
|
2183
|
-
if (payload.guardexEnabled === false) {
|
|
2184
|
-
console.log(
|
|
2185
|
-
`[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(payload.guardexToggle)}). Skipping repo bootstrap.`,
|
|
2186
|
-
);
|
|
2187
|
-
process.exitCode = 0;
|
|
2188
|
-
return;
|
|
2189
|
-
}
|
|
2190
|
-
if (!options.skipAgents) {
|
|
2191
|
-
console.log(`[${TOOL_NAME}] AGENTS.md managed policy block is configured by install.`);
|
|
2192
|
-
}
|
|
2193
|
-
console.log(`[${TOOL_NAME}] Installed. Next step: ${TOOL_NAME} setup`);
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
process.exitCode = 0;
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
function fix(rawArgs) {
|
|
2200
|
-
const options = parseCommonArgs(rawArgs, {
|
|
2201
|
-
target: process.cwd(),
|
|
2202
|
-
dropStaleLocks: true,
|
|
2203
|
-
skipAgents: false,
|
|
2204
|
-
skipPackageJson: false,
|
|
2205
|
-
skipGitignore: false,
|
|
2206
|
-
dryRun: false,
|
|
2207
|
-
allowProtectedBaseWrite: false,
|
|
2208
|
-
});
|
|
2209
|
-
|
|
2210
|
-
assertProtectedMainWriteAllowed(options, 'fix');
|
|
2211
|
-
const payload = runFixInternal(options);
|
|
2212
|
-
printOperations('Fix target', payload, options.dryRun);
|
|
2213
|
-
|
|
2214
|
-
if (!options.dryRun) {
|
|
2215
|
-
if (payload.guardexEnabled === false) {
|
|
2216
|
-
console.log(
|
|
2217
|
-
`[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(payload.guardexToggle)}). Skipping repo repair.`,
|
|
2218
|
-
);
|
|
2219
|
-
process.exitCode = 0;
|
|
2220
|
-
return;
|
|
2221
|
-
}
|
|
2222
|
-
console.log(`[${TOOL_NAME}] Repair complete. Next step: ${TOOL_NAME} scan`);
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
process.exitCode = 0;
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
function scan(rawArgs) {
|
|
2229
|
-
const options = parseCommonArgs(rawArgs, {
|
|
2230
|
-
target: process.cwd(),
|
|
2231
|
-
json: false,
|
|
2232
|
-
});
|
|
2233
|
-
|
|
2234
|
-
const result = runScanInternal(options);
|
|
2235
|
-
printScanResult(result, options.json);
|
|
2236
|
-
setExitCodeFromScan(result);
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
function doctor(rawArgs) {
|
|
2240
|
-
const options = parseDoctorArgs(rawArgs);
|
|
2241
|
-
const topRepoRoot = resolveRepoRoot(options.target);
|
|
2242
|
-
const discoveredRepos = options.recursive
|
|
2243
|
-
? discoverNestedGitRepos(topRepoRoot, {
|
|
2244
|
-
maxDepth: options.nestedMaxDepth,
|
|
2245
|
-
extraSkip: options.nestedSkipDirs,
|
|
2246
|
-
includeSubmodules: options.includeSubmodules,
|
|
2247
|
-
skipRelativeDirs: AGENT_WORKTREE_RELATIVE_DIRS,
|
|
2248
|
-
})
|
|
2249
|
-
: [topRepoRoot];
|
|
2250
|
-
|
|
2251
|
-
if (discoveredRepos.length > 1) {
|
|
2252
|
-
if (!options.json) {
|
|
2253
|
-
console.log(
|
|
2254
|
-
`[${TOOL_NAME}] Detected ${discoveredRepos.length} git repos under ${topRepoRoot}. ` +
|
|
2255
|
-
`Repairing each with doctor (use --single-repo or --current to limit to the target).`,
|
|
2256
|
-
);
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
const repoResults = [];
|
|
2260
|
-
let aggregateExitCode = 0;
|
|
2261
|
-
for (let repoIndex = 0; repoIndex < discoveredRepos.length; repoIndex += 1) {
|
|
2262
|
-
const repoPath = discoveredRepos[repoIndex];
|
|
2263
|
-
const progressLabel = `${repoIndex + 1}/${discoveredRepos.length}`;
|
|
2264
|
-
if (!options.json) {
|
|
2265
|
-
console.log(`[${TOOL_NAME}] ── Doctor target: ${repoPath} [${progressLabel}] ──`);
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
const childArgs = [
|
|
2269
|
-
path.resolve(__filename),
|
|
2270
|
-
'doctor',
|
|
2271
|
-
'--single-repo',
|
|
2272
|
-
'--target',
|
|
2273
|
-
repoPath,
|
|
2274
|
-
...(options.force ? ['--force', ...(options.forceManagedPaths || [])] : []),
|
|
2275
|
-
...(options.dropStaleLocks ? [] : ['--keep-stale-locks']),
|
|
2276
|
-
...(options.skipAgents ? ['--skip-agents'] : []),
|
|
2277
|
-
...(options.skipPackageJson ? ['--skip-package-json'] : []),
|
|
2278
|
-
...(options.skipGitignore ? ['--no-gitignore'] : []),
|
|
2279
|
-
...(options.dryRun ? ['--dry-run'] : []),
|
|
2280
|
-
// Recursive child doctor runs should report pending PR state immediately instead of blocking the parent loop.
|
|
2281
|
-
'--no-wait-for-merge',
|
|
2282
|
-
...(options.verboseAutoFinish ? ['--verbose-auto-finish'] : []),
|
|
2283
|
-
...(options.json ? ['--json'] : []),
|
|
2284
|
-
...(options.allowProtectedBaseWrite ? ['--allow-protected-base-write'] : []),
|
|
2285
|
-
];
|
|
2286
|
-
const startedAt = Date.now();
|
|
2287
|
-
const nestedResult = options.json
|
|
2288
|
-
? run(process.execPath, childArgs, { cwd: topRepoRoot })
|
|
2289
|
-
: cp.spawnSync(process.execPath, childArgs, {
|
|
2290
|
-
cwd: topRepoRoot,
|
|
2291
|
-
encoding: 'utf8',
|
|
2292
|
-
stdio: 'inherit',
|
|
2293
|
-
});
|
|
2294
|
-
if (isSpawnFailure(nestedResult)) {
|
|
2295
|
-
throw nestedResult.error;
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
const exitCode = typeof nestedResult.status === 'number' ? nestedResult.status : 1;
|
|
2299
|
-
if (exitCode !== 0 && aggregateExitCode === 0) {
|
|
2300
|
-
aggregateExitCode = exitCode;
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
if (options.json) {
|
|
2304
|
-
let parsedResult = null;
|
|
2305
|
-
if (nestedResult.stdout) {
|
|
2306
|
-
try {
|
|
2307
|
-
parsedResult = JSON.parse(nestedResult.stdout);
|
|
2308
|
-
} catch {
|
|
2309
|
-
parsedResult = null;
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
repoResults.push(
|
|
2313
|
-
parsedResult
|
|
2314
|
-
? { repoRoot: repoPath, exitCode, result: parsedResult }
|
|
2315
|
-
: {
|
|
2316
|
-
repoRoot: repoPath,
|
|
2317
|
-
exitCode,
|
|
2318
|
-
stdout: nestedResult.stdout || '',
|
|
2319
|
-
stderr: nestedResult.stderr || '',
|
|
2320
|
-
},
|
|
2321
|
-
);
|
|
2322
|
-
} else {
|
|
2323
|
-
console.log(
|
|
2324
|
-
`[${TOOL_NAME}] Doctor target complete: ${repoPath} [${progressLabel}] in ${formatElapsedDuration(Date.now() - startedAt)}.`,
|
|
2325
|
-
);
|
|
2326
|
-
if (repoIndex < discoveredRepos.length - 1) {
|
|
2327
|
-
process.stdout.write('\n');
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
if (options.json) {
|
|
2333
|
-
process.stdout.write(
|
|
2334
|
-
JSON.stringify(
|
|
2335
|
-
{
|
|
2336
|
-
repoRoot: topRepoRoot,
|
|
2337
|
-
recursive: true,
|
|
2338
|
-
repos: repoResults,
|
|
2339
|
-
},
|
|
2340
|
-
null,
|
|
2341
|
-
2,
|
|
2342
|
-
) + '\n',
|
|
2343
|
-
);
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
process.exitCode = aggregateExitCode;
|
|
2347
|
-
return;
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
const singleRepoOptions = {
|
|
2351
|
-
...options,
|
|
2352
|
-
target: topRepoRoot,
|
|
2353
|
-
};
|
|
2354
|
-
|
|
2355
|
-
if (!singleRepoOptions.json) {
|
|
2356
|
-
printRequiredSystemToolStatus();
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
const blocked = protectedBaseWriteBlock(singleRepoOptions, { requireBootstrap: false });
|
|
2360
|
-
if (blocked) {
|
|
2361
|
-
doctorModule.runDoctorInSandbox(singleRepoOptions, blocked, {
|
|
2362
|
-
startProtectedBaseSandbox,
|
|
2363
|
-
cleanupProtectedBaseSandbox,
|
|
2364
|
-
ensureOmxScaffold,
|
|
2365
|
-
configureHooks,
|
|
2366
|
-
autoFinishReadyAgentBranches: doctorModule.autoFinishReadyAgentBranches,
|
|
2367
|
-
});
|
|
2368
|
-
const primaryBaseBranch = currentBranchName(blocked.repoRoot);
|
|
2369
|
-
const prunePayload = doctorModule.pruneStaleAgentWorktrees(blocked.repoRoot, {
|
|
2370
|
-
baseBranch: primaryBaseBranch,
|
|
2371
|
-
dryRun: singleRepoOptions.dryRun,
|
|
2372
|
-
});
|
|
2373
|
-
printWorktreePruneSummary(prunePayload, { baseBranch: primaryBaseBranch });
|
|
2374
|
-
return;
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
assertProtectedMainWriteAllowed(singleRepoOptions, 'doctor');
|
|
2378
|
-
const fixPayload = runFixInternal(singleRepoOptions);
|
|
2379
|
-
const scanResult = runScanInternal({ target: singleRepoOptions.target, json: false });
|
|
2380
|
-
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
2381
|
-
const autoFinishSummary = scanResult.guardexEnabled === false
|
|
2382
|
-
? {
|
|
2383
|
-
enabled: false,
|
|
2384
|
-
attempted: 0,
|
|
2385
|
-
completed: 0,
|
|
2386
|
-
skipped: 0,
|
|
2387
|
-
failed: 0,
|
|
2388
|
-
details: [],
|
|
2389
|
-
}
|
|
2390
|
-
: doctorModule.autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
2391
|
-
baseBranch: currentBaseBranch,
|
|
2392
|
-
dryRun: singleRepoOptions.dryRun,
|
|
2393
|
-
waitForMerge: singleRepoOptions.waitForMerge,
|
|
2394
|
-
});
|
|
2395
|
-
const prunePayload = scanResult.guardexEnabled === false
|
|
2396
|
-
? { enabled: false, ran: false, status: 'skipped', details: ['Guardex disabled for this repo.'] }
|
|
2397
|
-
: doctorModule.pruneStaleAgentWorktrees(scanResult.repoRoot, {
|
|
2398
|
-
baseBranch: currentBaseBranch,
|
|
2399
|
-
dryRun: singleRepoOptions.dryRun,
|
|
2400
|
-
});
|
|
2401
|
-
const safe = scanResult.guardexEnabled === false || (scanResult.errors === 0 && scanResult.warnings === 0);
|
|
2402
|
-
const musafe = safe;
|
|
2403
|
-
|
|
2404
|
-
if (singleRepoOptions.json) {
|
|
2405
|
-
process.stdout.write(
|
|
2406
|
-
JSON.stringify(
|
|
2407
|
-
{
|
|
2408
|
-
repoRoot: scanResult.repoRoot,
|
|
2409
|
-
branch: scanResult.branch,
|
|
2410
|
-
safe,
|
|
2411
|
-
musafe,
|
|
2412
|
-
fix: {
|
|
2413
|
-
operations: fixPayload.operations,
|
|
2414
|
-
hookResult: fixPayload.hookResult,
|
|
2415
|
-
dryRun: Boolean(singleRepoOptions.dryRun),
|
|
2416
|
-
},
|
|
2417
|
-
scan: {
|
|
2418
|
-
guardexEnabled: scanResult.guardexEnabled !== false,
|
|
2419
|
-
guardexToggle: scanResult.guardexToggle || null,
|
|
2420
|
-
errors: scanResult.errors,
|
|
2421
|
-
warnings: scanResult.warnings,
|
|
2422
|
-
findings: scanResult.findings,
|
|
2423
|
-
},
|
|
2424
|
-
autoFinish: autoFinishSummary,
|
|
2425
|
-
worktreePrune: prunePayload,
|
|
2426
|
-
},
|
|
2427
|
-
null,
|
|
2428
|
-
2,
|
|
2429
|
-
) + '\n',
|
|
2430
|
-
);
|
|
2431
|
-
setExitCodeFromScan(scanResult);
|
|
2432
|
-
return;
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
printOperations('Doctor/fix', fixPayload, options.dryRun);
|
|
2436
|
-
printScanResult(scanResult, false);
|
|
2437
|
-
if (scanResult.guardexEnabled === false) {
|
|
2438
|
-
console.log(`[${TOOL_NAME}] Repo-local Guardex enforcement is intentionally disabled.`);
|
|
2439
|
-
setExitCodeFromScan(scanResult);
|
|
2440
|
-
return;
|
|
2441
|
-
}
|
|
2442
|
-
printAutoFinishSummary(autoFinishSummary, {
|
|
2443
|
-
baseBranch: currentBaseBranch,
|
|
2444
|
-
verbose: singleRepoOptions.verboseAutoFinish,
|
|
2445
|
-
});
|
|
2446
|
-
printWorktreePruneSummary(prunePayload, { baseBranch: currentBaseBranch });
|
|
2447
|
-
if (safe) {
|
|
2448
|
-
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ✅ Repo is fully safe.`, 'safe'));
|
|
2449
|
-
} else {
|
|
2450
|
-
console.log(
|
|
2451
|
-
colorizeDoctorOutput(
|
|
2452
|
-
`[${TOOL_NAME}] ⚠️ Repo is not fully safe yet (${scanResult.errors} error(s), ${scanResult.warnings} warning(s)).`,
|
|
2453
|
-
scanResult.errors > 0 ? 'unsafe' : 'warn',
|
|
2454
|
-
),
|
|
2455
|
-
);
|
|
2456
|
-
}
|
|
2457
|
-
setExitCodeFromScan(scanResult);
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
function review(rawArgs) {
|
|
2461
|
-
const options = parseReviewArgs(rawArgs);
|
|
2462
|
-
const repoRoot = resolveRepoRoot(options.target);
|
|
2463
|
-
const result = runReviewBotCommand(repoRoot, options.passthroughArgs);
|
|
2464
|
-
if (isSpawnFailure(result)) {
|
|
2465
|
-
throw result.error;
|
|
2466
|
-
}
|
|
2467
|
-
|
|
2468
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
2469
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
2470
|
-
process.exitCode = typeof result.status === 'number' ? result.status : 1;
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
|
-
function prReview(rawArgs) {
|
|
2474
|
-
const options = parsePrReviewArgs(rawArgs);
|
|
2475
|
-
const result = prReviewModule.runPrReview(options);
|
|
2476
|
-
prReviewModule.printPrReviewResult(result);
|
|
2477
|
-
process.exitCode = 0;
|
|
2478
|
-
}
|
|
2479
|
-
|
|
2480
|
-
function agentsStatePathForRepo(repoRoot) {
|
|
2481
|
-
return path.join(repoRoot, AGENTS_BOTS_STATE_RELATIVE);
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
function readAgentsState(repoRoot) {
|
|
2485
|
-
const statePath = agentsStatePathForRepo(repoRoot);
|
|
2486
|
-
if (!fs.existsSync(statePath)) {
|
|
2487
|
-
return null;
|
|
2488
|
-
}
|
|
2489
|
-
try {
|
|
2490
|
-
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
2491
|
-
} catch (_error) {
|
|
2492
|
-
return null;
|
|
2493
|
-
}
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
function writeAgentsState(repoRoot, state) {
|
|
2497
|
-
const statePath = agentsStatePathForRepo(repoRoot);
|
|
2498
|
-
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
2499
|
-
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
function processAlive(pid) {
|
|
2503
|
-
const normalizedPid = Number.parseInt(String(pid || ''), 10);
|
|
2504
|
-
if (!Number.isInteger(normalizedPid) || normalizedPid <= 0) {
|
|
2505
|
-
return false;
|
|
2506
|
-
}
|
|
2507
|
-
try {
|
|
2508
|
-
process.kill(normalizedPid, 0);
|
|
2509
|
-
} catch (_error) {
|
|
2510
|
-
return false;
|
|
2511
|
-
}
|
|
2512
|
-
|
|
2513
|
-
const state = readProcessState(normalizedPid);
|
|
2514
|
-
if (state.startsWith('Z')) {
|
|
2515
|
-
return false;
|
|
2516
|
-
}
|
|
2517
|
-
return true;
|
|
2518
|
-
}
|
|
2519
|
-
|
|
2520
|
-
function sleepSeconds(seconds) {
|
|
2521
|
-
const result = run('sleep', [String(seconds)]);
|
|
2522
|
-
if (isSpawnFailure(result) || result.status !== 0) {
|
|
2523
|
-
throw new Error(`sleep command failed for ${seconds}s`);
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
function readProcessCommand(pid) {
|
|
2528
|
-
const result = run('ps', ['-o', 'command=', '-p', String(pid)]);
|
|
2529
|
-
if (isSpawnFailure(result) || result.status !== 0) {
|
|
2530
|
-
return '';
|
|
2531
|
-
}
|
|
2532
|
-
return String(result.stdout || '').trim();
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
function readProcessState(pid) {
|
|
2536
|
-
const result = run('ps', ['-o', 'stat=', '-p', String(pid)]);
|
|
2537
|
-
if (isSpawnFailure(result) || result.status !== 0) {
|
|
2538
|
-
return '';
|
|
2539
|
-
}
|
|
2540
|
-
return String(result.stdout || '').trim();
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
function stopAgentProcessByPid(pid, expectedToken = '') {
|
|
2544
|
-
const normalizedPid = Number.parseInt(String(pid || ''), 10);
|
|
2545
|
-
if (!Number.isInteger(normalizedPid) || normalizedPid <= 0) {
|
|
2546
|
-
return { status: 'invalid', pid: normalizedPid };
|
|
2547
|
-
}
|
|
2548
|
-
if (!processAlive(normalizedPid)) {
|
|
2549
|
-
return { status: 'not-running', pid: normalizedPid };
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
if (expectedToken) {
|
|
2553
|
-
const cmdline = readProcessCommand(normalizedPid);
|
|
2554
|
-
if (cmdline && !cmdline.includes(expectedToken)) {
|
|
2555
|
-
return { status: 'mismatch', pid: normalizedPid, command: cmdline };
|
|
2556
|
-
}
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
try {
|
|
2560
|
-
process.kill(-normalizedPid, 'SIGTERM');
|
|
2561
|
-
} catch (_error) {
|
|
2562
|
-
try {
|
|
2563
|
-
process.kill(normalizedPid, 'SIGTERM');
|
|
2564
|
-
} catch (_err) {
|
|
2565
|
-
return { status: 'term-failed', pid: normalizedPid };
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
const deadline = Date.now() + 3_000;
|
|
2570
|
-
while (Date.now() < deadline) {
|
|
2571
|
-
if (!processAlive(normalizedPid)) {
|
|
2572
|
-
return { status: 'stopped', pid: normalizedPid };
|
|
2573
|
-
}
|
|
2574
|
-
sleepSeconds(0.1);
|
|
2575
|
-
}
|
|
2576
|
-
|
|
2577
|
-
try {
|
|
2578
|
-
process.kill(-normalizedPid, 'SIGKILL');
|
|
2579
|
-
} catch (_error) {
|
|
2580
|
-
try {
|
|
2581
|
-
process.kill(normalizedPid, 'SIGKILL');
|
|
2582
|
-
} catch (_err) {
|
|
2583
|
-
return { status: 'kill-failed', pid: normalizedPid };
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
sleepSeconds(0.1);
|
|
2587
|
-
|
|
2588
|
-
return {
|
|
2589
|
-
status: processAlive(normalizedPid) ? 'kill-failed' : 'stopped',
|
|
2590
|
-
pid: normalizedPid,
|
|
2591
|
-
};
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
|
-
function spawnDetachedAgentProcess({ command, args, cwd, logPath }) {
|
|
2595
|
-
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
2596
|
-
const logHandle = fs.openSync(logPath, 'a');
|
|
2597
|
-
fs.writeSync(
|
|
2598
|
-
logHandle,
|
|
2599
|
-
`[${new Date().toISOString()}] spawn: ${command} ${args.join(' ')}\n`,
|
|
2600
|
-
);
|
|
2601
|
-
const child = cp.spawn(command, args, {
|
|
2602
|
-
cwd,
|
|
2603
|
-
detached: true,
|
|
2604
|
-
stdio: ['ignore', logHandle, logHandle],
|
|
2605
|
-
env: process.env,
|
|
2606
|
-
});
|
|
2607
|
-
fs.closeSync(logHandle);
|
|
2608
|
-
if (child.error) {
|
|
2609
|
-
throw child.error;
|
|
2610
|
-
}
|
|
2611
|
-
child.unref();
|
|
2612
|
-
const pid = Number.parseInt(String(child.pid || ''), 10);
|
|
2613
|
-
if (!Number.isInteger(pid) || pid <= 0) {
|
|
2614
|
-
throw new Error(`Failed to spawn detached process for ${command}`);
|
|
2615
|
-
}
|
|
2616
|
-
return pid;
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
function agents(rawArgs) {
|
|
2620
|
-
const options = parseAgentsArgs(rawArgs);
|
|
2621
|
-
if (['files', 'diff', 'locks'].includes(options.subcommand)) {
|
|
2622
|
-
process.stdout.write(agentInspect.runInspectCommand(options));
|
|
2623
|
-
process.exitCode = 0;
|
|
2624
|
-
return;
|
|
2625
|
-
}
|
|
2626
|
-
|
|
2627
|
-
const repoRoot = resolveRepoRoot(options.target);
|
|
2628
|
-
const statePath = agentsStatePathForRepo(repoRoot);
|
|
2629
|
-
|
|
2630
|
-
if (options.subcommand === 'finish') {
|
|
2631
|
-
const result = finishAgentSession(repoRoot, options);
|
|
2632
|
-
if (options.json) {
|
|
2633
|
-
process.stdout.write(`${JSON.stringify(result.evidence, null, 2)}\n`);
|
|
2634
|
-
}
|
|
2635
|
-
process.exitCode = 0;
|
|
2636
|
-
return;
|
|
2637
|
-
}
|
|
2638
|
-
|
|
2639
|
-
if (options.subcommand === 'cleanup-sessions') {
|
|
2640
|
-
process.stdout.write(agentCleanupSessions.runCleanupSessionsCommand(repoRoot, options));
|
|
2641
|
-
process.exitCode = 0;
|
|
2642
|
-
return;
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
if (options.subcommand === 'start') {
|
|
2646
|
-
if (agentsStart.shouldUseInteractivePanel(options, process.stdin, process.stdout)) {
|
|
2647
|
-
agentsStart.startInteractiveAgentPanel(repoRoot, options, {
|
|
2648
|
-
onDone(result) {
|
|
2649
|
-
process.exitCode = result.status;
|
|
2650
|
-
},
|
|
2651
|
-
});
|
|
2652
|
-
return;
|
|
2653
|
-
}
|
|
2654
|
-
if (options.dryRun) {
|
|
2655
|
-
const output = agentsStart.dryRunStart(options, repoRoot);
|
|
2656
|
-
process.stdout.write(output.endsWith('\n') ? output : `${output}\n`);
|
|
2657
|
-
process.exitCode = 0;
|
|
2658
|
-
return;
|
|
2659
|
-
}
|
|
2660
|
-
if (options.panel && !options.task) {
|
|
2661
|
-
process.stderr.write('[gitguardex] gx agents start --panel requires an interactive terminal when no task is provided.\n');
|
|
2662
|
-
process.exitCode = 1;
|
|
2663
|
-
return;
|
|
2664
|
-
}
|
|
2665
|
-
if (options.task) {
|
|
2666
|
-
const result = agentsStart.startAgentLane(repoRoot, options);
|
|
2667
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
2668
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
2669
|
-
process.exitCode = result.status;
|
|
2670
|
-
return;
|
|
2671
|
-
}
|
|
2672
|
-
|
|
2673
|
-
const existingState = readAgentsState(repoRoot);
|
|
2674
|
-
const existingReviewPid = Number.parseInt(String(existingState?.review?.pid || ''), 10);
|
|
2675
|
-
const existingCleanupPid = Number.parseInt(String(existingState?.cleanup?.pid || ''), 10);
|
|
2676
|
-
const reviewRunning = processAlive(existingReviewPid);
|
|
2677
|
-
const cleanupRunning = processAlive(existingCleanupPid);
|
|
2678
|
-
|
|
2679
|
-
if (reviewRunning && cleanupRunning) {
|
|
2680
|
-
console.log(
|
|
2681
|
-
`[${TOOL_NAME}] Repo agents already running (review pid=${existingReviewPid}, cleanup pid=${existingCleanupPid}).`,
|
|
2682
|
-
);
|
|
2683
|
-
process.exitCode = 0;
|
|
2684
|
-
return;
|
|
2685
|
-
}
|
|
2686
|
-
|
|
2687
|
-
const reviewLogPath = path.join(repoRoot, '.omx', 'logs', 'agent-review.log');
|
|
2688
|
-
const cleanupLogPath = path.join(repoRoot, '.omx', 'logs', 'agent-cleanup.log');
|
|
2689
|
-
|
|
2690
|
-
let reviewPid = existingReviewPid;
|
|
2691
|
-
let cleanupPid = existingCleanupPid;
|
|
2692
|
-
let startedAny = false;
|
|
2693
|
-
let reusedAny = false;
|
|
2694
|
-
|
|
2695
|
-
if (!reviewRunning) {
|
|
2696
|
-
reviewPid = spawnDetachedAgentProcess({
|
|
2697
|
-
command: process.execPath,
|
|
2698
|
-
args: [
|
|
2699
|
-
path.resolve(__filename),
|
|
2700
|
-
'internal',
|
|
2701
|
-
'run-shell',
|
|
2702
|
-
'reviewBot',
|
|
2703
|
-
'--target',
|
|
2704
|
-
repoRoot,
|
|
2705
|
-
'--interval',
|
|
2706
|
-
String(options.reviewIntervalSeconds),
|
|
2707
|
-
],
|
|
2708
|
-
cwd: repoRoot,
|
|
2709
|
-
logPath: reviewLogPath,
|
|
2710
|
-
});
|
|
2711
|
-
startedAny = true;
|
|
2712
|
-
} else {
|
|
2713
|
-
reusedAny = true;
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
if (!cleanupRunning) {
|
|
2717
|
-
cleanupPid = spawnDetachedAgentProcess({
|
|
2718
|
-
command: process.execPath,
|
|
2719
|
-
args: [
|
|
2720
|
-
path.resolve(__filename),
|
|
2721
|
-
'cleanup',
|
|
2722
|
-
'--target',
|
|
2723
|
-
repoRoot,
|
|
2724
|
-
'--watch',
|
|
2725
|
-
'--interval',
|
|
2726
|
-
String(options.cleanupIntervalSeconds),
|
|
2727
|
-
'--idle-minutes',
|
|
2728
|
-
String(options.idleMinutes),
|
|
2729
|
-
],
|
|
2730
|
-
cwd: repoRoot,
|
|
2731
|
-
logPath: cleanupLogPath,
|
|
2732
|
-
});
|
|
2733
|
-
startedAny = true;
|
|
2734
|
-
} else {
|
|
2735
|
-
reusedAny = true;
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
const priorReviewInterval = Number.parseInt(String(existingState?.review?.intervalSeconds || ''), 10);
|
|
2739
|
-
const priorCleanupInterval = Number.parseInt(String(existingState?.cleanup?.intervalSeconds || ''), 10);
|
|
2740
|
-
const priorIdleMinutes = Number.parseInt(String(existingState?.cleanup?.idleMinutes || ''), 10);
|
|
2741
|
-
const reviewIntervalSeconds = reviewRunning && Number.isInteger(priorReviewInterval) && priorReviewInterval >= 5
|
|
2742
|
-
? priorReviewInterval
|
|
2743
|
-
: options.reviewIntervalSeconds;
|
|
2744
|
-
const cleanupIntervalSeconds = cleanupRunning && Number.isInteger(priorCleanupInterval) && priorCleanupInterval >= 5
|
|
2745
|
-
? priorCleanupInterval
|
|
2746
|
-
: options.cleanupIntervalSeconds;
|
|
2747
|
-
const idleMinutes = cleanupRunning && Number.isInteger(priorIdleMinutes) && priorIdleMinutes >= 1
|
|
2748
|
-
? priorIdleMinutes
|
|
2749
|
-
: options.idleMinutes;
|
|
2750
|
-
|
|
2751
|
-
writeAgentsState(repoRoot, {
|
|
2752
|
-
schemaVersion: 1,
|
|
2753
|
-
repoRoot,
|
|
2754
|
-
startedAt: new Date().toISOString(),
|
|
2755
|
-
review: {
|
|
2756
|
-
pid: reviewPid,
|
|
2757
|
-
intervalSeconds: reviewIntervalSeconds,
|
|
2758
|
-
script: path.resolve(__filename),
|
|
2759
|
-
logPath: reviewLogPath,
|
|
2760
|
-
},
|
|
2761
|
-
cleanup: {
|
|
2762
|
-
pid: cleanupPid,
|
|
2763
|
-
intervalSeconds: cleanupIntervalSeconds,
|
|
2764
|
-
idleMinutes,
|
|
2765
|
-
script: path.resolve(__filename),
|
|
2766
|
-
logPath: cleanupLogPath,
|
|
2767
|
-
},
|
|
2768
|
-
});
|
|
2769
|
-
|
|
2770
|
-
console.log(
|
|
2771
|
-
`[${TOOL_NAME}] Started repo agents in ${repoRoot} (review pid=${reviewPid}, cleanup pid=${cleanupPid}).`,
|
|
2772
|
-
);
|
|
2773
|
-
if (reusedAny && startedAny) {
|
|
2774
|
-
console.log(`[${TOOL_NAME}] Reused healthy bot process(es) and started only missing ones.`);
|
|
2775
|
-
}
|
|
2776
|
-
console.log(`[${TOOL_NAME}] Logs: ${reviewLogPath}, ${cleanupLogPath}`);
|
|
2777
|
-
process.exitCode = 0;
|
|
2778
|
-
return;
|
|
2779
|
-
}
|
|
2780
|
-
|
|
2781
|
-
if (options.subcommand === 'stop') {
|
|
2782
|
-
if (options.pid) {
|
|
2783
|
-
const stopResult = stopAgentProcessByPid(options.pid);
|
|
2784
|
-
const success = ['stopped', 'not-running'].includes(stopResult.status);
|
|
2785
|
-
console.log(
|
|
2786
|
-
`[${TOOL_NAME}] Stopped agent pid ${options.pid} (${stopResult.status}).`,
|
|
2787
|
-
);
|
|
2788
|
-
process.exitCode = success ? 0 : 1;
|
|
2789
|
-
return;
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
|
-
const existingState = readAgentsState(repoRoot);
|
|
2793
|
-
if (!existingState) {
|
|
2794
|
-
console.log(`[${TOOL_NAME}] Repo agents are not running for ${repoRoot}.`);
|
|
2795
|
-
process.exitCode = 0;
|
|
2796
|
-
return;
|
|
2797
|
-
}
|
|
2798
|
-
|
|
2799
|
-
const reviewStop = stopAgentProcessByPid(existingState?.review?.pid, 'internal run-shell reviewBot');
|
|
2800
|
-
const cleanupStop = stopAgentProcessByPid(existingState?.cleanup?.pid, `${path.basename(__filename)} cleanup`);
|
|
2801
|
-
|
|
2802
|
-
if (fs.existsSync(statePath)) {
|
|
2803
|
-
fs.unlinkSync(statePath);
|
|
2804
|
-
}
|
|
2805
|
-
|
|
2806
|
-
console.log(
|
|
2807
|
-
`[${TOOL_NAME}] Stopped repo agents in ${repoRoot} (review=${reviewStop.status}, cleanup=${cleanupStop.status}).`,
|
|
2808
|
-
);
|
|
2809
|
-
process.exitCode = 0;
|
|
2810
|
-
return;
|
|
2811
|
-
}
|
|
2812
|
-
|
|
2813
|
-
process.stdout.write(agentStatus.runStatusCommand(repoRoot, options));
|
|
2814
|
-
process.exitCode = 0;
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
function report(rawArgs) {
|
|
2818
|
-
const options = parseReportArgs(rawArgs);
|
|
2819
|
-
const subcommand = options.subcommand || 'help';
|
|
2820
|
-
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
2821
|
-
const sessionSeverityHelpDetails = sessionSeverityReport.renderSessionSeverityHelpDetails()
|
|
2822
|
-
.split('\n')
|
|
2823
|
-
.map((line) => ` ${line}`)
|
|
2824
|
-
.join('\n');
|
|
2825
|
-
console.log(
|
|
2826
|
-
`${TOOL_NAME} report commands:\n` +
|
|
2827
|
-
` ${TOOL_NAME} report scorecard [--target <path>] [--repo github.com/<owner>/<repo>] [--scorecard-json <file>] [--output-dir <path>] [--date YYYY-MM-DD] [--dry-run] [--json]\n` +
|
|
2828
|
-
` ${sessionSeverityReport.renderSessionSeverityCommand(TOOL_NAME)}\n` +
|
|
2829
|
-
`${sessionSeverityHelpDetails}\n` +
|
|
2830
|
-
`\n` +
|
|
2831
|
-
`Examples:\n` +
|
|
2832
|
-
` ${TOOL_NAME} report scorecard --repo github.com/recodeecom/multiagent-safety\n` +
|
|
2833
|
-
` ${TOOL_NAME} report scorecard --scorecard-json ./scorecard.json --date 2026-04-10\n` +
|
|
2834
|
-
` ${sessionSeverityReport.renderSessionSeverityExample(TOOL_NAME)}`,
|
|
2835
|
-
);
|
|
2836
|
-
process.exitCode = 0;
|
|
2837
|
-
return;
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
if (subcommand === 'session-severity') {
|
|
2841
|
-
const payload = sessionSeverityReport.buildSessionSeverityReport(options);
|
|
2842
|
-
if (options.json) {
|
|
2843
|
-
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
2844
|
-
process.exitCode = 0;
|
|
2845
|
-
return;
|
|
2846
|
-
}
|
|
2847
|
-
console.log(sessionSeverityReport.renderSessionSeverityReport(payload));
|
|
2848
|
-
process.exitCode = 0;
|
|
2849
|
-
return;
|
|
2850
|
-
}
|
|
2851
|
-
|
|
2852
|
-
if (subcommand !== 'scorecard') {
|
|
2853
|
-
throw new Error(`Unknown report subcommand: ${subcommand}`);
|
|
2854
|
-
}
|
|
2855
|
-
|
|
2856
|
-
const repoRoot = resolveRepoRoot(options.target);
|
|
2857
|
-
const repo = resolveScorecardRepo(repoRoot, options.repo);
|
|
2858
|
-
const payload = options.scorecardJson
|
|
2859
|
-
? readScorecardJsonFile(options.scorecardJson)
|
|
2860
|
-
: runScorecardJson(repo);
|
|
2861
|
-
|
|
2862
|
-
const reportDate = options.date || todayDateStamp();
|
|
2863
|
-
const outputDir = path.resolve(options.outputDir || path.join(repoRoot, 'docs', 'reports'));
|
|
2864
|
-
const baselinePath = path.join(outputDir, `openssf-scorecard-baseline-${reportDate}.md`);
|
|
2865
|
-
const remediationPath = path.join(outputDir, `openssf-scorecard-remediation-plan-${reportDate}.md`);
|
|
2866
|
-
|
|
2867
|
-
const checks = normalizeScorecardChecks(payload);
|
|
2868
|
-
const rawScore = Number(payload?.score);
|
|
2869
|
-
const score = Number.isFinite(rawScore) ? rawScore : 0;
|
|
2870
|
-
const capturedAt = String(payload?.date || new Date().toISOString());
|
|
2871
|
-
const scorecardVersion = String(payload?.scorecard?.version || payload?.version || 'unknown');
|
|
2872
|
-
|
|
2873
|
-
const baselineMarkdown = renderScorecardBaselineMarkdown({
|
|
2874
|
-
repo,
|
|
2875
|
-
score,
|
|
2876
|
-
checks,
|
|
2877
|
-
capturedAt,
|
|
2878
|
-
scorecardVersion,
|
|
2879
|
-
reportDate,
|
|
2880
|
-
});
|
|
2881
|
-
|
|
2882
|
-
const remediationMarkdown = renderScorecardRemediationPlanMarkdown({
|
|
2883
|
-
baselineRelativePath: path.relative(repoRoot, baselinePath) || path.basename(baselinePath),
|
|
2884
|
-
checks,
|
|
2885
|
-
});
|
|
2886
|
-
|
|
2887
|
-
if (!options.dryRun) {
|
|
2888
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
2889
|
-
fs.writeFileSync(baselinePath, baselineMarkdown, 'utf8');
|
|
2890
|
-
fs.writeFileSync(remediationPath, remediationMarkdown, 'utf8');
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
if (options.json) {
|
|
2894
|
-
process.stdout.write(
|
|
2895
|
-
JSON.stringify(
|
|
2896
|
-
{
|
|
2897
|
-
repoRoot,
|
|
2898
|
-
repo,
|
|
2899
|
-
score,
|
|
2900
|
-
checks: checks.length,
|
|
2901
|
-
outputDir,
|
|
2902
|
-
baselinePath,
|
|
2903
|
-
remediationPath,
|
|
2904
|
-
dryRun: Boolean(options.dryRun),
|
|
2905
|
-
},
|
|
2906
|
-
null,
|
|
2907
|
-
2,
|
|
2908
|
-
) + '\n',
|
|
2909
|
-
);
|
|
2910
|
-
process.exitCode = 0;
|
|
2911
|
-
return;
|
|
2912
|
-
}
|
|
2913
|
-
|
|
2914
|
-
console.log(`[${TOOL_NAME}] Report target: ${repoRoot}`);
|
|
2915
|
-
console.log(`[${TOOL_NAME}] Scorecard repo: ${repo}`);
|
|
2916
|
-
console.log(`[${TOOL_NAME}] Score: ${score}/10`);
|
|
2917
|
-
if (options.dryRun) {
|
|
2918
|
-
console.log(`[${TOOL_NAME}] Dry run report paths:`);
|
|
2919
|
-
} else {
|
|
2920
|
-
console.log(`[${TOOL_NAME}] Generated reports:`);
|
|
2921
|
-
}
|
|
2922
|
-
console.log(` - ${baselinePath}`);
|
|
2923
|
-
console.log(` - ${remediationPath}`);
|
|
2924
|
-
process.exitCode = 0;
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
|
-
function setup(rawArgs) {
|
|
2928
|
-
const options = parseSetupArgs(rawArgs, {
|
|
2929
|
-
target: process.cwd(),
|
|
2930
|
-
force: false,
|
|
2931
|
-
skipAgents: false,
|
|
2932
|
-
skipPackageJson: false,
|
|
2933
|
-
skipGitignore: false,
|
|
2934
|
-
dryRun: false,
|
|
2935
|
-
yesGlobalInstall: false,
|
|
2936
|
-
noGlobalInstall: false,
|
|
2937
|
-
allowProtectedBaseWrite: false,
|
|
2938
|
-
});
|
|
2939
|
-
|
|
2940
|
-
const globalInstallStatus = toolchainModule.installGlobalToolchain(options);
|
|
2941
|
-
if (globalInstallStatus.status === 'installed') {
|
|
2942
|
-
console.log(
|
|
2943
|
-
`[${TOOL_NAME}] ✅ Companion tools installed (${(globalInstallStatus.packages || []).join(', ')}).`,
|
|
2944
|
-
);
|
|
2945
|
-
} else if (globalInstallStatus.status === 'already-installed') {
|
|
2946
|
-
console.log(`[${TOOL_NAME}] ✅ Companion tools already installed. Skipping.`);
|
|
2947
|
-
} else if (globalInstallStatus.status === 'failed') {
|
|
2948
|
-
const installCommands = toolchainModule.describeCompanionInstallCommands(
|
|
2949
|
-
GLOBAL_TOOLCHAIN_PACKAGES,
|
|
2950
|
-
OPTIONAL_LOCAL_COMPANION_TOOLS,
|
|
2951
|
-
);
|
|
2952
|
-
console.log(
|
|
2953
|
-
`[${TOOL_NAME}] ⚠️ Global install failed: ${globalInstallStatus.reason}\n` +
|
|
2954
|
-
`[${TOOL_NAME}] Continue with local safety setup. You can retry later with:\n` +
|
|
2955
|
-
installCommands.map((command) => ` ${command}`).join('\n'),
|
|
2956
|
-
);
|
|
2957
|
-
} else if (globalInstallStatus.status === 'skipped' && globalInstallStatus.reason === 'non-interactive-default') {
|
|
2958
|
-
console.log(
|
|
2959
|
-
`[${TOOL_NAME}] Skipping companion installs (non-interactive mode). ` +
|
|
2960
|
-
`Use --yes-global-install to force or run interactively for Y/N prompt.`,
|
|
2961
|
-
);
|
|
2962
|
-
} else if (globalInstallStatus.status === 'skipped') {
|
|
2963
|
-
console.log(`[${TOOL_NAME}] ⚠️ Companion installs skipped by user choice.`);
|
|
2964
|
-
for (const warning of toolchainModule.describeMissingGlobalDependencyWarnings(
|
|
2965
|
-
globalInstallStatus.missingPackages || [],
|
|
2966
|
-
)) {
|
|
2967
|
-
console.log(`[${TOOL_NAME}] ⚠️ ${warning}`);
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
|
|
2971
|
-
printRequiredSystemToolStatus();
|
|
2972
|
-
|
|
2973
|
-
const topRepoRoot = resolveRepoRoot(options.target);
|
|
2974
|
-
const discoveredRepos = options.recursive
|
|
2975
|
-
? discoverNestedGitRepos(topRepoRoot, {
|
|
2976
|
-
maxDepth: options.nestedMaxDepth,
|
|
2977
|
-
extraSkip: options.nestedSkipDirs,
|
|
2978
|
-
includeSubmodules: options.includeSubmodules,
|
|
2979
|
-
skipRelativeDirs: AGENT_WORKTREE_RELATIVE_DIRS,
|
|
2980
|
-
})
|
|
2981
|
-
: [topRepoRoot];
|
|
2982
|
-
|
|
2983
|
-
if (discoveredRepos.length > 1) {
|
|
2984
|
-
console.log(
|
|
2985
|
-
`[${TOOL_NAME}] Detected ${discoveredRepos.length} git repos under ${topRepoRoot}. Installing into each (use --no-recursive or --current to limit to the top-level).`,
|
|
2986
|
-
);
|
|
2987
|
-
for (const repoPath of discoveredRepos) {
|
|
2988
|
-
const marker = repoPath === topRepoRoot ? ' (top-level)' : '';
|
|
2989
|
-
console.log(`[${TOOL_NAME}] - ${repoPath}${marker}`);
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
|
|
2993
|
-
let aggregateErrors = 0;
|
|
2994
|
-
let aggregateWarnings = 0;
|
|
2995
|
-
let lastScanResult = null;
|
|
2996
|
-
|
|
2997
|
-
for (const repoPath of discoveredRepos) {
|
|
2998
|
-
const perRepoOptions = { ...options, target: repoPath };
|
|
2999
|
-
const repoLabel = discoveredRepos.length > 1 ? ` [${path.relative(topRepoRoot, repoPath) || '.'}]` : '';
|
|
3000
|
-
|
|
3001
|
-
if (discoveredRepos.length > 1) {
|
|
3002
|
-
console.log(`[${TOOL_NAME}] ── Setup target: ${repoPath} ──`);
|
|
3003
|
-
}
|
|
3004
|
-
|
|
3005
|
-
const blocked = protectedBaseWriteBlock(perRepoOptions);
|
|
3006
|
-
if (blocked) {
|
|
3007
|
-
const sandboxResult = runSetupInSandbox(perRepoOptions, blocked, repoLabel);
|
|
3008
|
-
aggregateErrors += sandboxResult.scanResult.errors;
|
|
3009
|
-
aggregateWarnings += sandboxResult.scanResult.warnings;
|
|
3010
|
-
lastScanResult = sandboxResult.scanResult;
|
|
3011
|
-
const primaryBaseBranch = currentBranchName(blocked.repoRoot);
|
|
3012
|
-
const prunePayload = doctorModule.pruneStaleAgentWorktrees(blocked.repoRoot, {
|
|
3013
|
-
baseBranch: primaryBaseBranch,
|
|
3014
|
-
dryRun: perRepoOptions.dryRun,
|
|
3015
|
-
});
|
|
3016
|
-
printWorktreePruneSummary(prunePayload, { baseBranch: primaryBaseBranch });
|
|
3017
|
-
continue;
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
const { installPayload, fixPayload, parentWorkspace } = runSetupBootstrapInternal(perRepoOptions);
|
|
3021
|
-
printOperations(`Setup/install${repoLabel}`, installPayload, perRepoOptions.dryRun);
|
|
3022
|
-
printOperations(`Setup/fix${repoLabel}`, fixPayload, perRepoOptions.dryRun);
|
|
3023
|
-
|
|
3024
|
-
if (perRepoOptions.dryRun) {
|
|
3025
|
-
continue;
|
|
3026
|
-
}
|
|
3027
|
-
|
|
3028
|
-
if (parentWorkspace) {
|
|
3029
|
-
console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`);
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
|
-
const scanResult = runScanInternal({ target: repoPath, json: false });
|
|
3033
|
-
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
3034
|
-
const autoFinishSummary = doctorModule.autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
3035
|
-
baseBranch: currentBaseBranch,
|
|
3036
|
-
dryRun: perRepoOptions.dryRun,
|
|
3037
|
-
});
|
|
3038
|
-
printScanResult(scanResult, false);
|
|
3039
|
-
printAutoFinishSummary(autoFinishSummary, {
|
|
3040
|
-
baseBranch: currentBaseBranch,
|
|
3041
|
-
});
|
|
3042
|
-
const prunePayload = scanResult.guardexEnabled === false
|
|
3043
|
-
? { enabled: false, ran: false, status: 'skipped', details: ['Guardex disabled for this repo.'] }
|
|
3044
|
-
: doctorModule.pruneStaleAgentWorktrees(scanResult.repoRoot, {
|
|
3045
|
-
baseBranch: currentBaseBranch,
|
|
3046
|
-
dryRun: perRepoOptions.dryRun,
|
|
3047
|
-
});
|
|
3048
|
-
printWorktreePruneSummary(prunePayload, { baseBranch: currentBaseBranch });
|
|
3049
|
-
printSetupRepoHints(scanResult.repoRoot, currentBaseBranch, repoLabel);
|
|
3050
|
-
|
|
3051
|
-
aggregateErrors += scanResult.errors;
|
|
3052
|
-
aggregateWarnings += scanResult.warnings;
|
|
3053
|
-
lastScanResult = scanResult;
|
|
3054
|
-
}
|
|
3055
|
-
|
|
3056
|
-
if (options.dryRun) {
|
|
3057
|
-
console.log(`[${TOOL_NAME}] Dry run setup done.`);
|
|
3058
|
-
process.exitCode = 0;
|
|
3059
|
-
return;
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
if (aggregateErrors === 0 && aggregateWarnings === 0) {
|
|
3063
|
-
const repoCount = discoveredRepos.length;
|
|
3064
|
-
const suffix = repoCount > 1 ? ` (${repoCount} repos)` : '';
|
|
3065
|
-
console.log(`[${TOOL_NAME}] ✅ Setup complete.${suffix}`);
|
|
3066
|
-
console.log(`[${TOOL_NAME}] Copy AI setup prompt with: ${SHORT_TOOL_NAME} prompt`);
|
|
3067
|
-
console.log(
|
|
3068
|
-
`[${TOOL_NAME}] OpenSpec core workflow: /opsx:propose -> /opsx:apply -> /opsx:archive`,
|
|
3069
|
-
);
|
|
3070
|
-
console.log(
|
|
3071
|
-
`[${TOOL_NAME}] Optional expanded OpenSpec profile: openspec config profile <profile-name> && openspec update`,
|
|
3072
|
-
);
|
|
3073
|
-
console.log(`[${TOOL_NAME}] OpenSpec guide: docs/openspec-getting-started.md`);
|
|
3074
|
-
}
|
|
3075
|
-
|
|
3076
|
-
if (lastScanResult) {
|
|
3077
|
-
setExitCodeFromScan({
|
|
3078
|
-
...lastScanResult,
|
|
3079
|
-
errors: aggregateErrors,
|
|
3080
|
-
warnings: aggregateWarnings,
|
|
3081
|
-
});
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
function printRequiredSystemToolStatus() {
|
|
3086
|
-
const requiredSystemTools = toolchainModule.detectRequiredSystemTools();
|
|
3087
|
-
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
|
|
3088
|
-
if (missingSystemTools.length === 0) {
|
|
3089
|
-
console.log(`[${TOOL_NAME}] ✅ Required system tools available (${requiredSystemTools.map((tool) => tool.name).join(', ')}).`);
|
|
3090
|
-
return;
|
|
3091
|
-
}
|
|
3092
|
-
|
|
3093
|
-
const names = missingSystemTools.map((tool) => tool.name).join(', ');
|
|
3094
|
-
console.log(`[${TOOL_NAME}] ⚠️ Missing required system tool(s): ${names}`);
|
|
3095
|
-
for (const tool of missingSystemTools) {
|
|
3096
|
-
const reasonText = tool.reason ? ` (${tool.reason})` : '';
|
|
3097
|
-
console.log(`[${TOOL_NAME}] Install ${tool.name}: ${tool.installHint}${reasonText}`);
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
function ensureMainBranch(repoRoot) {
|
|
3102
|
-
const branchResult = gitRun(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFailure: true });
|
|
3103
|
-
if (branchResult.status !== 0) {
|
|
3104
|
-
throw new Error(`Unable to detect current branch in ${repoRoot}`);
|
|
3105
|
-
}
|
|
3106
|
-
|
|
3107
|
-
const branch = branchResult.stdout.trim();
|
|
3108
|
-
if (branch !== 'main') {
|
|
3109
|
-
throw new Error(`Release blocked: current branch is '${branch}' (required: 'main')`);
|
|
3110
|
-
}
|
|
3111
|
-
}
|
|
3112
|
-
|
|
3113
|
-
function ensureCleanWorkingTree(repoRoot) {
|
|
3114
|
-
const statusResult = gitRun(repoRoot, ['status', '--porcelain'], { allowFailure: true });
|
|
3115
|
-
if (statusResult.status !== 0) {
|
|
3116
|
-
throw new Error(`Unable to read git status in ${repoRoot}`);
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
const dirty = statusResult.stdout.trim();
|
|
3120
|
-
if (dirty.length > 0) {
|
|
3121
|
-
throw new Error('Release blocked: working tree is not clean');
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
function readReleaseRepoPackageJson(repoRoot) {
|
|
3126
|
-
const manifestPath = path.join(repoRoot, 'package.json');
|
|
3127
|
-
if (!fs.existsSync(manifestPath)) {
|
|
3128
|
-
throw new Error(`Release blocked: package.json missing in ${repoRoot}`);
|
|
3129
|
-
}
|
|
3130
|
-
|
|
3131
|
-
try {
|
|
3132
|
-
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
3133
|
-
} catch (error) {
|
|
3134
|
-
throw new Error(`Release blocked: unable to parse package.json in ${repoRoot}: ${error.message}`);
|
|
3135
|
-
}
|
|
3136
|
-
}
|
|
3137
|
-
|
|
3138
|
-
function resolveReleaseGithubRepo(repoRoot) {
|
|
3139
|
-
const releasePackageJson = readReleaseRepoPackageJson(repoRoot);
|
|
3140
|
-
const fromManifest = inferGithubRepoSlug(
|
|
3141
|
-
releasePackageJson.repository &&
|
|
3142
|
-
(releasePackageJson.repository.url || releasePackageJson.repository),
|
|
3143
|
-
);
|
|
3144
|
-
if (fromManifest) {
|
|
3145
|
-
return fromManifest;
|
|
3146
|
-
}
|
|
3147
|
-
|
|
3148
|
-
const fromOrigin = inferGithubRepoSlug(readGitConfig(repoRoot, 'remote.origin.url'));
|
|
3149
|
-
if (fromOrigin) {
|
|
3150
|
-
return fromOrigin;
|
|
3151
|
-
}
|
|
3152
|
-
|
|
3153
|
-
throw new Error(
|
|
3154
|
-
'Release blocked: unable to resolve GitHub repo from package.json repository URL or origin remote.',
|
|
3155
|
-
);
|
|
3156
|
-
}
|
|
3157
|
-
|
|
3158
|
-
function readRepoReadme(repoRoot) {
|
|
3159
|
-
const readmePath = path.join(repoRoot, 'README.md');
|
|
3160
|
-
if (!fs.existsSync(readmePath)) {
|
|
3161
|
-
throw new Error(`Release blocked: README.md missing in ${repoRoot}`);
|
|
3162
|
-
}
|
|
3163
|
-
return fs.readFileSync(readmePath, 'utf8');
|
|
3164
|
-
}
|
|
3165
|
-
|
|
3166
|
-
function parseReadmeReleaseEntries(readmeContent) {
|
|
3167
|
-
const releaseNotesIndex = String(readmeContent || '').indexOf('## Release notes');
|
|
3168
|
-
if (releaseNotesIndex < 0) {
|
|
3169
|
-
throw new Error('Release blocked: README.md is missing the "## Release notes" section');
|
|
3170
|
-
}
|
|
3171
|
-
|
|
3172
|
-
const releaseNotesContent = String(readmeContent || '').slice(releaseNotesIndex);
|
|
3173
|
-
const entries = [];
|
|
3174
|
-
const lines = releaseNotesContent.split(/\r?\n/);
|
|
3175
|
-
let currentTag = '';
|
|
3176
|
-
let currentLines = [];
|
|
3177
|
-
|
|
3178
|
-
function flushEntry() {
|
|
3179
|
-
if (!currentTag) {
|
|
3180
|
-
return;
|
|
3181
|
-
}
|
|
3182
|
-
const body = currentLines.join('\n').trim();
|
|
3183
|
-
if (body) {
|
|
3184
|
-
entries.push({ tag: currentTag, body, version: parseVersionString(currentTag) });
|
|
3185
|
-
}
|
|
3186
|
-
currentTag = '';
|
|
3187
|
-
currentLines = [];
|
|
3188
|
-
}
|
|
3189
|
-
|
|
3190
|
-
for (const line of lines) {
|
|
3191
|
-
const headingMatch = line.match(/^###\s+(v\d+\.\d+\.\d+)\s*$/);
|
|
3192
|
-
if (headingMatch) {
|
|
3193
|
-
flushEntry();
|
|
3194
|
-
currentTag = headingMatch[1];
|
|
3195
|
-
continue;
|
|
3196
|
-
}
|
|
3197
|
-
|
|
3198
|
-
if (!currentTag) {
|
|
3199
|
-
continue;
|
|
3200
|
-
}
|
|
3201
|
-
|
|
3202
|
-
if (/^<\/details>\s*$/.test(line) || /^##\s+/.test(line)) {
|
|
3203
|
-
flushEntry();
|
|
3204
|
-
continue;
|
|
3205
|
-
}
|
|
3206
|
-
|
|
3207
|
-
currentLines.push(line);
|
|
3208
|
-
}
|
|
3209
|
-
|
|
3210
|
-
flushEntry();
|
|
3211
|
-
|
|
3212
|
-
if (entries.length === 0) {
|
|
3213
|
-
throw new Error('Release blocked: README.md did not yield any versioned release-note sections');
|
|
3214
|
-
}
|
|
3215
|
-
|
|
3216
|
-
return entries;
|
|
3217
|
-
}
|
|
3218
|
-
|
|
3219
|
-
function resolvePreviousPublishedReleaseTag(repoSlug, currentTag) {
|
|
3220
|
-
const result = run(GH_BIN, ['release', 'list', '--repo', repoSlug, '--limit', '20'], {
|
|
3221
|
-
timeout: 20_000,
|
|
3222
|
-
});
|
|
3223
|
-
if (result.error) {
|
|
3224
|
-
throw new Error(`Release blocked: unable to run '${GH_BIN} release list': ${result.error.message}`);
|
|
3225
|
-
}
|
|
3226
|
-
if (result.status !== 0) {
|
|
3227
|
-
const details = (result.stderr || result.stdout || '').trim();
|
|
3228
|
-
throw new Error(`Release blocked: unable to list GitHub releases.${details ? `\n${details}` : ''}`);
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
|
-
const tags = String(result.stdout || '')
|
|
3232
|
-
.split('\n')
|
|
3233
|
-
.map((line) => line.split('\t')[0].trim())
|
|
3234
|
-
.filter(Boolean);
|
|
3235
|
-
|
|
3236
|
-
return tags.find((tag) => tag !== currentTag) || '';
|
|
3237
|
-
}
|
|
3238
|
-
|
|
3239
|
-
function selectReleaseEntriesForWindow(entries, currentTag, previousTag) {
|
|
3240
|
-
const currentVersion = parseVersionString(currentTag);
|
|
3241
|
-
if (!currentVersion) {
|
|
3242
|
-
throw new Error(`Release blocked: invalid current version tag '${currentTag}'`);
|
|
3243
|
-
}
|
|
3244
|
-
const previousVersion = previousTag ? parseVersionString(previousTag) : null;
|
|
3245
|
-
|
|
3246
|
-
const selected = entries.filter((entry) => {
|
|
3247
|
-
if (!entry.version) return false;
|
|
3248
|
-
if (compareParsedVersions(entry.version, currentVersion) > 0) return false;
|
|
3249
|
-
if (!previousVersion) return entry.tag === currentTag;
|
|
3250
|
-
return compareParsedVersions(entry.version, previousVersion) > 0;
|
|
3251
|
-
});
|
|
3252
|
-
|
|
3253
|
-
if (!selected.some((entry) => entry.tag === currentTag)) {
|
|
3254
|
-
throw new Error(`Release blocked: README.md is missing release notes for ${currentTag}`);
|
|
3255
|
-
}
|
|
3256
|
-
|
|
3257
|
-
return selected;
|
|
3258
|
-
}
|
|
3259
|
-
|
|
3260
|
-
function renderGeneratedReleaseNotes(entries, currentTag, previousTag) {
|
|
3261
|
-
const intro = previousTag ? `Changes since ${previousTag}.` : `Changes in ${currentTag}.`;
|
|
3262
|
-
const sections = entries
|
|
3263
|
-
.map((entry) => `### ${entry.tag}\n${entry.body}`)
|
|
3264
|
-
.join('\n\n');
|
|
3265
|
-
return `GitGuardex ${currentTag}\n\n${intro}\n\n${sections}`;
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
function describeGhAuthFailure(ghBin, authStatus) {
|
|
3269
|
-
if (authStatus.error) {
|
|
3270
|
-
return `unable to run '${ghBin} auth status': ${authStatus.error.message}`;
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
const authDetails = (authStatus.stderr || authStatus.stdout || '').trim();
|
|
3274
|
-
const apiProbe = run(ghBin, ['api', 'user', '--jq', '.login'], { timeout: 20_000 });
|
|
3275
|
-
if (apiProbe.status === 0) {
|
|
3276
|
-
return '';
|
|
3277
|
-
}
|
|
3278
|
-
|
|
3279
|
-
const apiDetails = (apiProbe.stderr || apiProbe.stdout || apiProbe.error?.message || '').trim();
|
|
3280
|
-
if (/error connecting to api\.github\.com|could not resolve host|failed to connect|network is unreachable|connection timed out|temporary failure in name resolution/i.test(apiDetails)) {
|
|
3281
|
-
return `GitHub API is unreachable, so '${ghBin} auth status' cannot validate the stored token. This is a network or sandbox connectivity problem, not proof that the token is invalid.${apiDetails ? `\n${apiDetails}` : ''}`;
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
return `'${ghBin}' auth is unavailable.${authDetails ? `\n${authDetails}` : ''}`;
|
|
3285
|
-
}
|
|
3286
|
-
|
|
3287
|
-
function buildReleaseNotesFromReadme(repoRoot, currentTag, previousTag) {
|
|
3288
|
-
const readme = readRepoReadme(repoRoot);
|
|
3289
|
-
const entries = parseReadmeReleaseEntries(readme);
|
|
3290
|
-
const selected = selectReleaseEntriesForWindow(entries, currentTag, previousTag);
|
|
3291
|
-
return renderGeneratedReleaseNotes(selected, currentTag, previousTag);
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
function release(rawArgs) {
|
|
3295
|
-
if (rawArgs.length > 0) {
|
|
3296
|
-
throw new Error(`Unknown option: ${rawArgs[0]}`);
|
|
3297
|
-
}
|
|
3298
|
-
|
|
3299
|
-
const repoRoot = resolveRepoRoot(process.cwd());
|
|
3300
|
-
if (path.resolve(repoRoot) !== MAINTAINER_RELEASE_REPO) {
|
|
3301
|
-
throw new Error(
|
|
3302
|
-
`Release blocked: command only allowed in ${MAINTAINER_RELEASE_REPO} (current: ${repoRoot})`,
|
|
3303
|
-
);
|
|
3304
|
-
}
|
|
3305
|
-
|
|
3306
|
-
ensureMainBranch(repoRoot);
|
|
3307
|
-
ensureCleanWorkingTree(repoRoot);
|
|
3308
|
-
|
|
3309
|
-
if (!isCommandAvailable(GH_BIN)) {
|
|
3310
|
-
throw new Error(`Release blocked: '${GH_BIN}' is not available`);
|
|
3311
|
-
}
|
|
3312
|
-
|
|
3313
|
-
const ghAuthStatus = run(GH_BIN, ['auth', 'status'], { timeout: 20_000 });
|
|
3314
|
-
if (ghAuthStatus.status !== 0) {
|
|
3315
|
-
const ghAuthFailure = describeGhAuthFailure(GH_BIN, ghAuthStatus);
|
|
3316
|
-
if (ghAuthFailure) {
|
|
3317
|
-
throw new Error(`Release blocked: ${ghAuthFailure}`);
|
|
3318
|
-
}
|
|
3319
|
-
}
|
|
3320
|
-
|
|
3321
|
-
const releasePackageJson = readReleaseRepoPackageJson(repoRoot);
|
|
3322
|
-
const repoSlug = resolveReleaseGithubRepo(repoRoot);
|
|
3323
|
-
const currentTag = `v${releasePackageJson.version}`;
|
|
3324
|
-
const previousTag = resolvePreviousPublishedReleaseTag(repoSlug, currentTag);
|
|
3325
|
-
const notes = buildReleaseNotesFromReadme(repoRoot, currentTag, previousTag);
|
|
3326
|
-
const headCommit = gitRun(repoRoot, ['rev-parse', 'HEAD']).stdout.trim();
|
|
3327
|
-
|
|
3328
|
-
const existingRelease = run(GH_BIN, ['release', 'view', currentTag, '--repo', repoSlug], {
|
|
3329
|
-
timeout: 20_000,
|
|
3330
|
-
});
|
|
3331
|
-
if (existingRelease.error) {
|
|
3332
|
-
throw new Error(`Release blocked: unable to run '${GH_BIN} release view': ${existingRelease.error.message}`);
|
|
3333
|
-
}
|
|
3334
|
-
|
|
3335
|
-
const releaseArgs =
|
|
3336
|
-
existingRelease.status === 0
|
|
3337
|
-
? ['release', 'edit', currentTag, '--repo', repoSlug, '--title', currentTag, '--notes', notes]
|
|
3338
|
-
: [
|
|
3339
|
-
'release',
|
|
3340
|
-
'create',
|
|
3341
|
-
currentTag,
|
|
3342
|
-
'--repo',
|
|
3343
|
-
repoSlug,
|
|
3344
|
-
'--target',
|
|
3345
|
-
headCommit,
|
|
3346
|
-
'--title',
|
|
3347
|
-
currentTag,
|
|
3348
|
-
'--notes',
|
|
3349
|
-
notes,
|
|
3350
|
-
];
|
|
3351
|
-
|
|
3352
|
-
console.log(
|
|
3353
|
-
`[${TOOL_NAME}] ${existingRelease.status === 0 ? 'Updating' : 'Creating'} GitHub release ${currentTag} on ${repoSlug}`,
|
|
3354
|
-
);
|
|
3355
|
-
if (previousTag) {
|
|
3356
|
-
console.log(`[${TOOL_NAME}] Aggregating README release notes newer than ${previousTag}.`);
|
|
3357
|
-
} else {
|
|
3358
|
-
console.log(`[${TOOL_NAME}] No earlier published GitHub release found; using only ${currentTag}.`);
|
|
3359
|
-
}
|
|
3360
|
-
|
|
3361
|
-
const releaseResult = run(GH_BIN, releaseArgs, { cwd: repoRoot, timeout: 60_000 });
|
|
3362
|
-
if (releaseResult.error) {
|
|
3363
|
-
throw new Error(`Release blocked: unable to run '${GH_BIN} release': ${releaseResult.error.message}`);
|
|
3364
|
-
}
|
|
3365
|
-
if (releaseResult.status !== 0) {
|
|
3366
|
-
const details = (releaseResult.stderr || releaseResult.stdout || '').trim();
|
|
3367
|
-
throw new Error(`GitHub release command failed.${details ? `\n${details}` : ''}`);
|
|
3368
|
-
}
|
|
3369
|
-
|
|
3370
|
-
const releaseUrl = String(releaseResult.stdout || '').trim();
|
|
3371
|
-
if (releaseUrl) {
|
|
3372
|
-
console.log(releaseUrl);
|
|
3373
|
-
}
|
|
3374
|
-
|
|
3375
|
-
console.log(`[${TOOL_NAME}] ✅ GitHub release ${currentTag} is synced to the README history.`);
|
|
3376
|
-
process.exitCode = 0;
|
|
3377
|
-
}
|
|
3378
|
-
|
|
3379
|
-
function printAgentsSnippet() {
|
|
3380
|
-
const snippetPath = path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md');
|
|
3381
|
-
process.stdout.write(fs.readFileSync(snippetPath, 'utf8'));
|
|
3382
|
-
}
|
|
3383
|
-
|
|
3384
|
-
function copyPrompt() {
|
|
3385
|
-
process.stdout.write(AI_SETUP_PROMPT);
|
|
3386
|
-
process.exitCode = 0;
|
|
3387
|
-
}
|
|
3388
|
-
|
|
3389
|
-
function copyCommands() {
|
|
3390
|
-
process.stdout.write(AI_SETUP_COMMANDS);
|
|
3391
|
-
process.exitCode = 0;
|
|
3392
|
-
}
|
|
3393
|
-
|
|
3394
|
-
function prompt(rawArgs) {
|
|
3395
|
-
const args = Array.isArray(rawArgs) ? rawArgs : [];
|
|
3396
|
-
let variant = 'prompt';
|
|
3397
|
-
let listParts = false;
|
|
3398
|
-
const selectedParts = [];
|
|
3399
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
3400
|
-
const arg = args[index];
|
|
3401
|
-
if (arg === '--exec' || arg === '--commands') variant = 'exec';
|
|
3402
|
-
else if (arg === '--snippet' || arg === '--agents') variant = 'snippet';
|
|
3403
|
-
else if (arg === '--prompt' || arg === '--full') variant = 'prompt';
|
|
3404
|
-
else if (arg === '--list-parts') listParts = true;
|
|
3405
|
-
else if (arg === '--part' || arg === '--parts') {
|
|
3406
|
-
const rawValue = args[index + 1];
|
|
3407
|
-
if (!rawValue || rawValue.startsWith('--')) {
|
|
3408
|
-
throw new Error(`${arg} requires a value`);
|
|
3409
|
-
}
|
|
3410
|
-
selectedParts.push(...parseAiSetupPartNames(rawValue));
|
|
3411
|
-
index += 1;
|
|
3412
|
-
} else if (arg.startsWith('--part=')) {
|
|
3413
|
-
selectedParts.push(...parseAiSetupPartNames(arg.slice('--part='.length)));
|
|
3414
|
-
} else if (arg.startsWith('--parts=')) {
|
|
3415
|
-
selectedParts.push(...parseAiSetupPartNames(arg.slice('--parts='.length)));
|
|
3416
|
-
}
|
|
3417
|
-
else if (arg === '-h' || arg === '--help') variant = 'help';
|
|
3418
|
-
else throw new Error(`Unknown option: ${arg}`);
|
|
3419
|
-
}
|
|
3420
|
-
if (variant === 'help') {
|
|
3421
|
-
console.log(
|
|
3422
|
-
`${SHORT_TOOL_NAME} prompt commands:\n` +
|
|
3423
|
-
` ${SHORT_TOOL_NAME} prompt Print AI setup checklist\n` +
|
|
3424
|
-
` ${SHORT_TOOL_NAME} prompt --exec Print setup commands only (shell-ready)\n` +
|
|
3425
|
-
` ${SHORT_TOOL_NAME} prompt --part <name> Print only the named checklist slice(s)\n` +
|
|
3426
|
-
` ${SHORT_TOOL_NAME} prompt --exec --part <name> Print only the named exec-capable slice(s)\n` +
|
|
3427
|
-
` ${SHORT_TOOL_NAME} prompt --list-parts List prompt part names\n` +
|
|
3428
|
-
` ${SHORT_TOOL_NAME} prompt --exec --list-parts List exec-capable prompt part names\n` +
|
|
3429
|
-
` ${SHORT_TOOL_NAME} prompt --snippet Print the AGENTS.md managed-block template`,
|
|
3430
|
-
);
|
|
3431
|
-
process.exitCode = 0;
|
|
3432
|
-
return;
|
|
3433
|
-
}
|
|
3434
|
-
if (variant === 'snippet') {
|
|
3435
|
-
if (listParts || selectedParts.length > 0) {
|
|
3436
|
-
throw new Error('--snippet does not support --list-parts or --part');
|
|
3437
|
-
}
|
|
3438
|
-
return printAgentsSnippet();
|
|
3439
|
-
}
|
|
3440
|
-
if (listParts) {
|
|
3441
|
-
if (selectedParts.length > 0) {
|
|
3442
|
-
throw new Error('--list-parts does not support --part');
|
|
3443
|
-
}
|
|
3444
|
-
process.stdout.write(`${listAiSetupPartNames({ execOnly: variant === 'exec' }).join('\n')}\n`);
|
|
3445
|
-
process.exitCode = 0;
|
|
3446
|
-
return;
|
|
3447
|
-
}
|
|
3448
|
-
process.stdout.write(renderAiSetupPrompt({ exec: variant === 'exec', parts: selectedParts }));
|
|
3449
|
-
process.exitCode = 0;
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
|
-
function branch(rawArgs) {
|
|
3453
|
-
const activeCwd = process.cwd();
|
|
3454
|
-
const [subcommand, ...rest] = rawArgs;
|
|
3455
|
-
if (subcommand === 'start') {
|
|
3456
|
-
const { target, passthrough } = extractTargetedArgs(rest);
|
|
3457
|
-
invokePackageAsset('branchStart', passthrough, { cwd: resolveRepoRoot(target) });
|
|
3458
|
-
return;
|
|
3459
|
-
}
|
|
3460
|
-
if (subcommand === 'finish') {
|
|
3461
|
-
const { target, passthrough } = extractTargetedArgs(rest);
|
|
3462
|
-
invokePackageAsset('branchFinish', passthrough, {
|
|
3463
|
-
cwd: resolveRepoRoot(target),
|
|
3464
|
-
env: { GUARDEX_FINISH_ACTIVE_CWD: activeCwd },
|
|
3465
|
-
});
|
|
3466
|
-
return;
|
|
3467
|
-
}
|
|
3468
|
-
if (subcommand === 'merge') return merge(rest);
|
|
3469
|
-
throw new Error(
|
|
3470
|
-
`Usage: ${SHORT_TOOL_NAME} branch <start|finish|merge> [options] ` +
|
|
3471
|
-
`(examples: '${SHORT_TOOL_NAME} branch start "<task>" "<agent>"', '${SHORT_TOOL_NAME} branch finish --branch <agent/...>')`,
|
|
3472
|
-
);
|
|
3473
|
-
}
|
|
3474
|
-
|
|
3475
|
-
// `gx pivot` — single-tool-call escape from a protected branch into an isolated
|
|
3476
|
-
// agent worktree. AI agents (Claude Code / Codex) cannot set the bypass env
|
|
3477
|
-
// vars from inside a tool call, so they need a whitelisted command that does
|
|
3478
|
-
// the whole hop: branch+worktree creation, dirty-tree migration, and a clean
|
|
3479
|
-
// trailer (`WORKTREE_PATH=...`, `BRANCH=...`, `NEXT_STEP=cd ...`) the agent can
|
|
3480
|
-
// parse to know exactly where to `cd`.
|
|
3481
|
-
//
|
|
3482
|
-
// On an existing agent/* branch, `gx pivot` short-circuits and just prints the
|
|
3483
|
-
// current worktree path — safe to call as a no-op.
|
|
3484
|
-
function pivot(rawArgs) {
|
|
3485
|
-
const { target, passthrough } = extractTargetedArgs(rawArgs);
|
|
3486
|
-
const repoRoot = resolveRepoRoot(target);
|
|
3487
|
-
const headProc = run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoRoot });
|
|
3488
|
-
const currentBranch = String(headProc.stdout || '').trim();
|
|
3489
|
-
if (currentBranch.startsWith('agent/')) {
|
|
3490
|
-
const wtProc = run('git', ['rev-parse', '--show-toplevel'], { cwd: repoRoot });
|
|
3491
|
-
const wtPath = String(wtProc.stdout || '').trim() || repoRoot;
|
|
3492
|
-
process.stdout.write(`[${TOOL_NAME} pivot] Already on agent branch '${currentBranch}'.\n`);
|
|
3493
|
-
process.stdout.write(`WORKTREE_PATH=${wtPath}\n`);
|
|
3494
|
-
process.stdout.write(`BRANCH=${currentBranch}\n`);
|
|
3495
|
-
process.stdout.write(`NEXT_STEP=cd "${wtPath}"\n`);
|
|
3496
|
-
process.exitCode = 0;
|
|
3497
|
-
return;
|
|
3498
|
-
}
|
|
3499
|
-
const result = runPackageAsset('branchStart', passthrough, { cwd: repoRoot });
|
|
3500
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
3501
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
3502
|
-
if (result.status !== 0) {
|
|
3503
|
-
process.exitCode = result.status || 1;
|
|
3504
|
-
return;
|
|
3505
|
-
}
|
|
3506
|
-
const stdoutText = String(result.stdout || '');
|
|
3507
|
-
const wtMatch = stdoutText.match(/^\[agent-branch-start\] Worktree:\s+(.+)$/m);
|
|
3508
|
-
const branchMatch = stdoutText.match(/^\[agent-branch-start\] (?:Created branch|Reusing existing branch):\s+(.+)$/m);
|
|
3509
|
-
if (wtMatch) {
|
|
3510
|
-
const wtPath = wtMatch[1].trim();
|
|
3511
|
-
process.stdout.write('\n');
|
|
3512
|
-
process.stdout.write(`WORKTREE_PATH=${wtPath}\n`);
|
|
3513
|
-
if (branchMatch) process.stdout.write(`BRANCH=${branchMatch[1].trim()}\n`);
|
|
3514
|
-
process.stdout.write(`NEXT_STEP=cd "${wtPath}"\n`);
|
|
3515
|
-
}
|
|
3516
|
-
process.exitCode = 0;
|
|
3517
|
-
}
|
|
3518
|
-
|
|
3519
|
-
// `gx ship` — alias for the canonical "I am done" command. Defaults to
|
|
3520
|
-
// `finish --via-pr --wait-for-merge --cleanup` so AI agents don't strand
|
|
3521
|
-
// commits or worktrees by accident. Any explicit user-supplied flags survive.
|
|
3522
|
-
function ship(rawArgs) {
|
|
3523
|
-
const args = Array.isArray(rawArgs) ? rawArgs.slice() : [];
|
|
3524
|
-
const ensureFlag = (flag) => {
|
|
3525
|
-
if (!args.includes(flag)) args.push(flag);
|
|
3526
|
-
};
|
|
3527
|
-
ensureFlag('--via-pr');
|
|
3528
|
-
ensureFlag('--wait-for-merge');
|
|
3529
|
-
ensureFlag('--cleanup');
|
|
3530
|
-
return finish(args);
|
|
3531
|
-
}
|
|
3532
|
-
|
|
3533
|
-
function locks(rawArgs) {
|
|
3534
|
-
const { target, passthrough } = extractTargetedArgs(rawArgs);
|
|
3535
|
-
const result = runPackageAsset('lockTool', passthrough, { cwd: resolveRepoRoot(target) });
|
|
3536
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
3537
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
3538
|
-
process.exitCode = result.status;
|
|
3539
|
-
}
|
|
3540
|
-
|
|
3541
|
-
function worktree(rawArgs) {
|
|
3542
|
-
const activeCwd = process.cwd();
|
|
3543
|
-
const [subcommand, ...rest] = rawArgs;
|
|
3544
|
-
if (subcommand === 'prune') {
|
|
3545
|
-
const { target, passthrough } = extractTargetedArgs(rest);
|
|
3546
|
-
invokePackageAsset('worktreePrune', passthrough, {
|
|
3547
|
-
cwd: resolveRepoRoot(target),
|
|
3548
|
-
env: { GUARDEX_PRUNE_ACTIVE_CWD: process.env.GUARDEX_PRUNE_ACTIVE_CWD || activeCwd },
|
|
3549
|
-
});
|
|
3550
|
-
return;
|
|
3551
|
-
}
|
|
3552
|
-
throw new Error(`Usage: ${SHORT_TOOL_NAME} worktree prune [cleanup-options]`);
|
|
3553
|
-
}
|
|
3554
|
-
|
|
3555
|
-
function hook(rawArgs) {
|
|
3556
|
-
return hooksModule.hook(rawArgs, {
|
|
3557
|
-
extractTargetedArgs,
|
|
3558
|
-
run,
|
|
3559
|
-
resolveRepoRoot,
|
|
3560
|
-
packageAssetEnv,
|
|
3561
|
-
configureHooks,
|
|
3562
|
-
TEMPLATE_ROOT,
|
|
3563
|
-
HOOK_NAMES,
|
|
3564
|
-
TOOL_NAME,
|
|
3565
|
-
SHORT_TOOL_NAME,
|
|
3566
|
-
});
|
|
3567
|
-
}
|
|
3568
|
-
|
|
3569
|
-
function internal(rawArgs) {
|
|
3570
|
-
return hooksModule.internal(rawArgs, {
|
|
3571
|
-
extractTargetedArgs,
|
|
3572
|
-
resolveRepoRoot,
|
|
3573
|
-
runReviewBotCommand,
|
|
3574
|
-
runPackageAsset,
|
|
3575
|
-
});
|
|
3576
|
-
}
|
|
3577
|
-
|
|
3578
|
-
function installAgentSkills(rawArgs) {
|
|
3579
|
-
let dryRun = false;
|
|
3580
|
-
let force = false;
|
|
3581
|
-
for (const arg of rawArgs) {
|
|
3582
|
-
if (arg === '--dry-run') {
|
|
3583
|
-
dryRun = true;
|
|
3584
|
-
continue;
|
|
3585
|
-
}
|
|
3586
|
-
if (arg === '--force') {
|
|
3587
|
-
force = true;
|
|
3588
|
-
continue;
|
|
3589
|
-
}
|
|
3590
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
3591
|
-
}
|
|
3592
|
-
|
|
3593
|
-
const operations = USER_LEVEL_SKILL_ASSETS.map((asset) => installUserLevelAsset(asset, { dryRun, force }));
|
|
3594
|
-
printStandaloneOperations('User-level Guardex skills', GUARDEX_HOME_DIR, operations, dryRun);
|
|
3595
|
-
process.exitCode = 0;
|
|
3596
|
-
}
|
|
3597
|
-
|
|
3598
|
-
function migrate(rawArgs) {
|
|
3599
|
-
const { target, passthrough } = extractTargetedArgs(rawArgs);
|
|
3600
|
-
let dryRun = false;
|
|
3601
|
-
let force = false;
|
|
3602
|
-
let installSkills = false;
|
|
3603
|
-
for (const arg of passthrough) {
|
|
3604
|
-
if (arg === '--dry-run') {
|
|
3605
|
-
dryRun = true;
|
|
3606
|
-
continue;
|
|
3607
|
-
}
|
|
3608
|
-
if (arg === '--force') {
|
|
3609
|
-
force = true;
|
|
3610
|
-
continue;
|
|
3611
|
-
}
|
|
3612
|
-
if (arg === '--install-agent-skills') {
|
|
3613
|
-
installSkills = true;
|
|
3614
|
-
continue;
|
|
3615
|
-
}
|
|
3616
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
3617
|
-
}
|
|
3618
|
-
|
|
3619
|
-
const repoRoot = resolveRepoRoot(target);
|
|
3620
|
-
const fixPayload = runFixInternal({
|
|
3621
|
-
target: repoRoot,
|
|
3622
|
-
dryRun,
|
|
3623
|
-
force,
|
|
3624
|
-
skipAgents: false,
|
|
3625
|
-
skipPackageJson: true,
|
|
3626
|
-
skipGitignore: false,
|
|
3627
|
-
dropStaleLocks: true,
|
|
3628
|
-
});
|
|
3629
|
-
printOperations('Migrate/fix', fixPayload, dryRun);
|
|
3630
|
-
|
|
3631
|
-
if (installSkills) {
|
|
3632
|
-
const skillOps = USER_LEVEL_SKILL_ASSETS.map((asset) => installUserLevelAsset(asset, { dryRun, force }));
|
|
3633
|
-
printStandaloneOperations('Migrate/install-agent-skills', GUARDEX_HOME_DIR, skillOps, dryRun);
|
|
3634
|
-
}
|
|
3635
|
-
|
|
3636
|
-
const removableLegacyFiles = LEGACY_MANAGED_REPO_FILES.filter(
|
|
3637
|
-
(relativePath) => !REQUIRED_MANAGED_REPO_FILES.includes(relativePath),
|
|
3638
|
-
);
|
|
3639
|
-
const removalOps = removableLegacyFiles.map((relativePath) => removeLegacyManagedRepoFile(repoRoot, relativePath, { dryRun, force }));
|
|
3640
|
-
removalOps.push(removeLegacyPackageScripts(repoRoot, dryRun));
|
|
3641
|
-
printStandaloneOperations('Migrate/cleanup', repoRoot, removalOps, dryRun);
|
|
3642
|
-
process.exitCode = 0;
|
|
3643
|
-
}
|
|
3644
|
-
|
|
3645
|
-
function cleanup(rawArgs) {
|
|
3646
|
-
return finishCommands.cleanup(rawArgs);
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
function merge(rawArgs) {
|
|
3650
|
-
return finishCommands.merge(rawArgs);
|
|
3651
|
-
}
|
|
3652
|
-
|
|
3653
|
-
function finish(rawArgs, defaults = {}) {
|
|
3654
|
-
return finishCommands.finish(rawArgs, defaults);
|
|
3655
|
-
}
|
|
3656
|
-
|
|
3657
|
-
function sync(rawArgs) {
|
|
3658
|
-
return finishCommands.sync(rawArgs);
|
|
3659
|
-
}
|
|
3660
|
-
|
|
3661
|
-
function submodule(rawArgs) {
|
|
3662
|
-
const parsed = parseTargetFlag(rawArgs || [], process.cwd());
|
|
3663
|
-
const [subcommand, ...rest] = parsed.args;
|
|
3664
|
-
|
|
3665
|
-
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
3666
|
-
console.log(
|
|
3667
|
-
`${TOOL_NAME} submodule commands:\n` +
|
|
3668
|
-
` ${TOOL_NAME} submodule advance [<path>] [--push] [--dry-run] [--branch <ref>] [--no-commit] [--target <path>]\n\n` +
|
|
3669
|
-
` advance — for each submodule listed in .gitmodules, fetch the tracked branch's\n` +
|
|
3670
|
-
` remote tip, advance the parent pointer, and (when on a non-protected\n` +
|
|
3671
|
-
` branch) commit the bump. Use --push to publish in one step.`,
|
|
3672
|
-
);
|
|
3673
|
-
return;
|
|
3674
|
-
}
|
|
3675
|
-
|
|
3676
|
-
if (subcommand !== 'advance') {
|
|
3677
|
-
throw new Error(`Unknown submodule subcommand: ${subcommand}. Try '${SHORT_TOOL_NAME} submodule help'.`);
|
|
3678
|
-
}
|
|
3679
|
-
|
|
3680
|
-
let push = false;
|
|
3681
|
-
let dryRun = false;
|
|
3682
|
-
let commit = true;
|
|
3683
|
-
let branchOverride = '';
|
|
3684
|
-
let pathArg = '';
|
|
3685
|
-
for (let i = 0; i < rest.length; i += 1) {
|
|
3686
|
-
const arg = rest[i];
|
|
3687
|
-
if (arg === '--push') {
|
|
3688
|
-
push = true;
|
|
3689
|
-
continue;
|
|
3690
|
-
}
|
|
3691
|
-
if (arg === '--dry-run' || arg === '-n') {
|
|
3692
|
-
dryRun = true;
|
|
3693
|
-
continue;
|
|
3694
|
-
}
|
|
3695
|
-
if (arg === '--no-commit') {
|
|
3696
|
-
commit = false;
|
|
3697
|
-
continue;
|
|
3698
|
-
}
|
|
3699
|
-
if (arg === '--branch' || arg === '-b') {
|
|
3700
|
-
branchOverride = rest[i + 1] || '';
|
|
3701
|
-
i += 1;
|
|
3702
|
-
continue;
|
|
3703
|
-
}
|
|
3704
|
-
if (arg.startsWith('--branch=')) {
|
|
3705
|
-
branchOverride = arg.slice('--branch='.length);
|
|
3706
|
-
continue;
|
|
3707
|
-
}
|
|
3708
|
-
if (arg.startsWith('--')) {
|
|
3709
|
-
throw new Error(`Unknown option for '${SHORT_TOOL_NAME} submodule advance': ${arg}`);
|
|
3710
|
-
}
|
|
3711
|
-
if (pathArg) {
|
|
3712
|
-
throw new Error(`'${SHORT_TOOL_NAME} submodule advance' accepts at most one submodule path (got '${pathArg}' and '${arg}')`);
|
|
3713
|
-
}
|
|
3714
|
-
pathArg = arg;
|
|
3715
|
-
}
|
|
3716
|
-
|
|
3717
|
-
const result = submoduleModule.advance({
|
|
3718
|
-
target: parsed.target,
|
|
3719
|
-
path: pathArg,
|
|
3720
|
-
push,
|
|
3721
|
-
dryRun,
|
|
3722
|
-
commit,
|
|
3723
|
-
branch: branchOverride,
|
|
3724
|
-
});
|
|
3725
|
-
submoduleModule.printAdvanceResult(result);
|
|
3726
|
-
}
|
|
3727
|
-
|
|
3728
|
-
function cockpit(rawArgs) {
|
|
3729
|
-
cockpitModule.openCockpit(rawArgs, {
|
|
3730
|
-
resolveRepoRoot,
|
|
3731
|
-
toolName: TOOL_NAME,
|
|
3732
|
-
});
|
|
3733
|
-
process.exitCode = 0;
|
|
3734
|
-
}
|
|
3735
|
-
|
|
3736
|
-
function protect(rawArgs) {
|
|
3737
|
-
const parsed = parseTargetFlag(rawArgs, process.cwd());
|
|
3738
|
-
const [subcommand, ...rest] = parsed.args;
|
|
3739
|
-
const repoRoot = resolveRepoRoot(parsed.target);
|
|
3740
|
-
|
|
3741
|
-
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
3742
|
-
console.log(
|
|
3743
|
-
`${TOOL_NAME} protect commands:\n` +
|
|
3744
|
-
` ${TOOL_NAME} protect list [--target <path>]\n` +
|
|
3745
|
-
` ${TOOL_NAME} protect add <branch...> [--target <path>]\n` +
|
|
3746
|
-
` ${TOOL_NAME} protect remove <branch...> [--target <path>]\n` +
|
|
3747
|
-
` ${TOOL_NAME} protect set <branch...> [--target <path>]\n` +
|
|
3748
|
-
` ${TOOL_NAME} protect reset [--target <path>]`,
|
|
3749
|
-
);
|
|
3750
|
-
process.exitCode = 0;
|
|
3751
|
-
return;
|
|
3752
|
-
}
|
|
3753
|
-
|
|
3754
|
-
const requestedBranches = uniquePreserveOrder(parseBranchList(rest.join(' ')));
|
|
3755
|
-
|
|
3756
|
-
if (subcommand === 'list') {
|
|
3757
|
-
const branches = readProtectedBranches(repoRoot);
|
|
3758
|
-
console.log(`[${TOOL_NAME}] Protected branches (${branches.length}): ${branches.join(', ')}`);
|
|
3759
|
-
process.exitCode = 0;
|
|
3760
|
-
return;
|
|
3761
|
-
}
|
|
3762
|
-
|
|
3763
|
-
if (subcommand === 'add') {
|
|
3764
|
-
if (requestedBranches.length === 0) {
|
|
3765
|
-
throw new Error('protect add requires one or more branch names');
|
|
3766
|
-
}
|
|
3767
|
-
const current = readProtectedBranches(repoRoot);
|
|
3768
|
-
const next = uniquePreserveOrder([...current, ...requestedBranches]);
|
|
3769
|
-
writeProtectedBranches(repoRoot, next);
|
|
3770
|
-
console.log(`[${TOOL_NAME}] Protected branches updated: ${next.join(', ')}`);
|
|
3771
|
-
process.exitCode = 0;
|
|
3772
|
-
return;
|
|
3773
|
-
}
|
|
3774
|
-
|
|
3775
|
-
if (subcommand === 'remove') {
|
|
3776
|
-
if (requestedBranches.length === 0) {
|
|
3777
|
-
throw new Error('protect remove requires one or more branch names');
|
|
3778
|
-
}
|
|
3779
|
-
const current = readProtectedBranches(repoRoot);
|
|
3780
|
-
const removals = new Set(requestedBranches);
|
|
3781
|
-
const next = current.filter((branch) => !removals.has(branch));
|
|
3782
|
-
writeProtectedBranches(repoRoot, next);
|
|
3783
|
-
console.log(
|
|
3784
|
-
`[${TOOL_NAME}] Protected branches updated: ` +
|
|
3785
|
-
`${(next.length > 0 ? next : DEFAULT_PROTECTED_BRANCHES).join(', ')}`,
|
|
3786
|
-
);
|
|
3787
|
-
if (next.length === 0) {
|
|
3788
|
-
console.log(`[${TOOL_NAME}] Reset to defaults (${DEFAULT_PROTECTED_BRANCHES.join(', ')}) because list was empty.`);
|
|
3789
|
-
}
|
|
3790
|
-
process.exitCode = 0;
|
|
3791
|
-
return;
|
|
3792
|
-
}
|
|
3793
|
-
|
|
3794
|
-
if (subcommand === 'set') {
|
|
3795
|
-
if (requestedBranches.length === 0) {
|
|
3796
|
-
throw new Error('protect set requires one or more branch names');
|
|
3797
|
-
}
|
|
3798
|
-
writeProtectedBranches(repoRoot, requestedBranches);
|
|
3799
|
-
console.log(`[${TOOL_NAME}] Protected branches set: ${requestedBranches.join(', ')}`);
|
|
3800
|
-
process.exitCode = 0;
|
|
3801
|
-
return;
|
|
3802
|
-
}
|
|
3803
|
-
|
|
3804
|
-
if (subcommand === 'reset') {
|
|
3805
|
-
writeProtectedBranches(repoRoot, []);
|
|
3806
|
-
console.log(`[${TOOL_NAME}] Protected branches reset to defaults: ${DEFAULT_PROTECTED_BRANCHES.join(', ')}`);
|
|
3807
|
-
process.exitCode = 0;
|
|
3808
|
-
return;
|
|
3809
|
-
}
|
|
3810
|
-
|
|
3811
|
-
throw new Error(`Unknown protect subcommand: ${subcommand}`);
|
|
3812
|
-
}
|
|
3813
|
-
|
|
3814
149
|
async function main() {
|
|
3815
150
|
const args = process.argv.slice(2);
|
|
3816
151
|
|
|
3817
152
|
if (args.length === 0) {
|
|
3818
153
|
if (isInteractiveTerminal() && !legacyDefaultStatusEnabled() && !defaultCockpitDisabled()) {
|
|
154
|
+
// Lazy-require: cockpit pulls ~32 modules; load only when actually rendering it.
|
|
155
|
+
const cockpitModule = require('../cockpit');
|
|
3819
156
|
cockpitModule.openDefaultCockpit({
|
|
3820
157
|
resolveRepoRoot,
|
|
3821
158
|
toolName: TOOL_NAME,
|
|
@@ -3873,6 +210,8 @@ async function main() {
|
|
|
3873
210
|
|
|
3874
211
|
if (command === 'prompt') return prompt(rest);
|
|
3875
212
|
if (command === 'pr-review') return prReview(rest);
|
|
213
|
+
if (command === 'pr') return prCommand(rest);
|
|
214
|
+
if (command === 'claude') return claudeCommand(rest);
|
|
3876
215
|
if (command === 'doctor') return doctor(rest);
|
|
3877
216
|
if (command === 'branch') return branch(rest);
|
|
3878
217
|
if (command === 'pivot') return pivot(rest);
|
|
@@ -3884,6 +223,7 @@ async function main() {
|
|
|
3884
223
|
if (command === 'install-agent-skills') return installAgentSkills(rest);
|
|
3885
224
|
if (command === 'internal') return internal(rest);
|
|
3886
225
|
if (command === 'agents') return agents(rest);
|
|
226
|
+
if (command === 'mcp') return mcp(rest);
|
|
3887
227
|
if (command === 'cockpit') return cockpit(rest);
|
|
3888
228
|
if (command === 'merge') return merge(rest);
|
|
3889
229
|
if (command === 'finish') return finish(rest);
|
|
@@ -3893,8 +233,10 @@ async function main() {
|
|
|
3893
233
|
if (command === 'submodule') return submodule(rest);
|
|
3894
234
|
if (command === 'cleanup') return cleanup(rest);
|
|
3895
235
|
if (command === 'release') return release(rest);
|
|
236
|
+
if (command === 'watch') return watch(rest);
|
|
3896
237
|
if (command === 'budget') return budgetModule.runBudgetCommand(rest);
|
|
3897
238
|
if (command === 'ci-init') return ciInitModule.runCiInitCommand(rest);
|
|
239
|
+
if (command === 'speckit') return speckitModule.runSpeckitCommand(rest);
|
|
3898
240
|
|
|
3899
241
|
const suggestion = maybeSuggestCommand(command);
|
|
3900
242
|
if (suggestion) {
|