@imdeadpool/guardex 7.0.19 → 7.0.21

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.
@@ -0,0 +1,398 @@
1
+ const {
2
+ path,
3
+ packageJson,
4
+ TOOL_NAME,
5
+ SHORT_TOOL_NAME,
6
+ LEGACY_NAMES,
7
+ GUARDEX_REPO_TOGGLE_ENV,
8
+ CLI_COMMAND_DESCRIPTIONS,
9
+ AGENT_BOT_DESCRIPTIONS,
10
+ DOCTOR_AUTO_FINISH_DETAIL_LIMIT,
11
+ DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX,
12
+ DOCTOR_AUTO_FINISH_MESSAGE_MAX,
13
+ } = require('../context');
14
+
15
+ function runtimeVersion() {
16
+ return `${packageJson.name}/${packageJson.version} ${process.platform}-${process.arch} node-${process.version}`;
17
+ }
18
+
19
+ function supportsAnsiColors() {
20
+ const forced = String(process.env.FORCE_COLOR || '').trim().toLowerCase();
21
+ if (['0', 'false', 'no', 'off'].includes(forced)) {
22
+ return false;
23
+ }
24
+ if (forced.length > 0) {
25
+ return true;
26
+ }
27
+ if (process.env.NO_COLOR) {
28
+ return false;
29
+ }
30
+ return Boolean(process.stdout.isTTY) && process.env.TERM !== 'dumb';
31
+ }
32
+
33
+ function colorize(text, colorCode) {
34
+ if (!supportsAnsiColors()) {
35
+ return text;
36
+ }
37
+ return `\u001B[${colorCode}m${text}\u001B[0m`;
38
+ }
39
+
40
+ function doctorOutputColorCode(status) {
41
+ const normalized = String(status || '').trim().toLowerCase();
42
+ if (['active', 'done', 'ok', 'safe', 'success'].includes(normalized)) {
43
+ return '32';
44
+ }
45
+ if (normalized === 'disabled') {
46
+ return '36';
47
+ }
48
+ if (['degraded', 'pending', 'skip', 'warn', 'warning'].includes(normalized)) {
49
+ return '33';
50
+ }
51
+ if (['error', 'fail', 'inactive', 'unsafe'].includes(normalized)) {
52
+ return '31';
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function colorizeDoctorOutput(text, status) {
58
+ const colorCode = doctorOutputColorCode(status);
59
+ return colorCode ? colorize(text, colorCode) : text;
60
+ }
61
+
62
+ function detectAutoFinishDetailStatus(detail) {
63
+ const trimmed = String(detail || '').trim();
64
+ const match = trimmed.match(/^\[(\w+)\]/);
65
+ if (match) {
66
+ return match[1].toLowerCase();
67
+ }
68
+ if (/^Skipped\b/i.test(trimmed) || /^No local agent branches found\b/i.test(trimmed)) {
69
+ return 'skip';
70
+ }
71
+ return null;
72
+ }
73
+
74
+ function detectAutoFinishSummaryStatus(summary) {
75
+ if (!summary || summary.enabled === false) {
76
+ return detectAutoFinishDetailStatus(summary?.details?.[0]);
77
+ }
78
+ if ((summary.failed || 0) > 0) {
79
+ return 'fail';
80
+ }
81
+ if ((summary.completed || 0) > 0) {
82
+ return 'done';
83
+ }
84
+ if ((summary.skipped || 0) > 0) {
85
+ return 'skip';
86
+ }
87
+ return null;
88
+ }
89
+
90
+ function statusDot(status) {
91
+ if (status === 'active') {
92
+ return colorize('●', '32');
93
+ }
94
+ if (status === 'inactive') {
95
+ return colorize('●', '31');
96
+ }
97
+ if (status === 'disabled') {
98
+ return colorize('●', '36');
99
+ }
100
+ return colorize('●', '33');
101
+ }
102
+
103
+ function commandCatalogLines(indent = ' ') {
104
+ const maxCommandLength = CLI_COMMAND_DESCRIPTIONS.reduce(
105
+ (max, [command]) => Math.max(max, command.length),
106
+ 0,
107
+ );
108
+ return CLI_COMMAND_DESCRIPTIONS.map(
109
+ ([command, description]) => `${indent}${command.padEnd(maxCommandLength + 2)}${description}`,
110
+ );
111
+ }
112
+
113
+ function agentBotCatalogLines(indent = ' ') {
114
+ const maxCommandLength = AGENT_BOT_DESCRIPTIONS.reduce(
115
+ (max, [command]) => Math.max(max, command.length),
116
+ 0,
117
+ );
118
+ return AGENT_BOT_DESCRIPTIONS.map(
119
+ ([command, description]) => `${indent}${command.padEnd(maxCommandLength + 2)}${description}`,
120
+ );
121
+ }
122
+
123
+ function repoToggleLines(indent = ' ') {
124
+ return [
125
+ `${indent}Set repo-root .env: ${GUARDEX_REPO_TOGGLE_ENV}=0 disables Guardex, ${GUARDEX_REPO_TOGGLE_ENV}=1 enables it again`,
126
+ ];
127
+ }
128
+
129
+ function printToolLogsSummary() {
130
+ const usageLine = ` $ ${SHORT_TOOL_NAME} <command> [options]`;
131
+ const commandDetails = commandCatalogLines(' ');
132
+ const agentBotDetails = agentBotCatalogLines(' ');
133
+ const repoToggleDetails = repoToggleLines(' ');
134
+
135
+ if (!supportsAnsiColors()) {
136
+ console.log(`${TOOL_NAME}-tools logs:`);
137
+ console.log(' USAGE');
138
+ console.log(usageLine);
139
+ console.log(' COMMANDS');
140
+ for (const line of commandDetails) {
141
+ console.log(line);
142
+ }
143
+ console.log(' AGENT BOT');
144
+ for (const line of agentBotDetails) {
145
+ console.log(line);
146
+ }
147
+ console.log(' REPO TOGGLE');
148
+ for (const line of repoToggleDetails) {
149
+ console.log(line);
150
+ }
151
+ return;
152
+ }
153
+
154
+ const title = colorize(`${TOOL_NAME}-tools logs`, '1;36');
155
+ const usageHeader = colorize('USAGE', '1');
156
+ const commandsHeader = colorize('COMMANDS', '1');
157
+ const agentBotHeader = colorize('AGENT BOT', '1');
158
+ const repoToggleHeader = colorize('REPO TOGGLE', '1');
159
+ const pipe = colorize('│', '90');
160
+ const tee = colorize('├', '90');
161
+ const corner = colorize('└', '90');
162
+
163
+ console.log(`${title}:`);
164
+ console.log(` ${tee}─ ${usageHeader}`);
165
+ console.log(` ${pipe}${usageLine}`);
166
+ console.log(` ${tee}─ ${commandsHeader}`);
167
+ for (const line of commandDetails) {
168
+ if (!line) {
169
+ console.log(` ${pipe}`);
170
+ continue;
171
+ }
172
+ console.log(` ${pipe}${line.slice(2)}`);
173
+ }
174
+ console.log(` ${tee}─ ${agentBotHeader}`);
175
+ for (const line of agentBotDetails) {
176
+ if (!line) {
177
+ console.log(` ${pipe}`);
178
+ continue;
179
+ }
180
+ console.log(` ${pipe}${line.slice(2)}`);
181
+ }
182
+ console.log(` ${tee}─ ${repoToggleHeader}`);
183
+ for (const line of repoToggleDetails) {
184
+ if (!line) {
185
+ console.log(` ${pipe}`);
186
+ continue;
187
+ }
188
+ console.log(` ${pipe}${line.slice(2)}`);
189
+ }
190
+ console.log(` ${corner}─ ${colorize(`Try '${TOOL_NAME} doctor' for one-step repair + verification.`, '2')}`);
191
+ }
192
+
193
+ function usage(options = {}) {
194
+ const { outsideGitRepo = false } = options;
195
+
196
+ console.log(`A command-line tool that sets up hardened multi-agent safety for git repositories.
197
+
198
+ VERSION
199
+ ${runtimeVersion()}
200
+
201
+ USAGE
202
+ $ ${SHORT_TOOL_NAME} <command> [options]
203
+
204
+ COMMANDS
205
+ ${commandCatalogLines().join('\n')}
206
+
207
+ AGENT BOT
208
+ ${agentBotCatalogLines().join('\n')}
209
+
210
+ REPO TOGGLE
211
+ ${repoToggleLines().join('\n')}
212
+
213
+ NOTES
214
+ - No command = ${SHORT_TOOL_NAME} status. ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup.
215
+ - Global installs need Y/N approval; GitHub CLI (gh) is required for PR automation.
216
+ - Target another repo: ${SHORT_TOOL_NAME} <cmd> --target <repo-path>.
217
+ - On protected main, setup/install/fix/doctor auto-sandbox via agent branch + PR flow.
218
+ - Run '${SHORT_TOOL_NAME} cleanup' to prune merged agent branches/worktrees.
219
+ - Legacy aliases: ${LEGACY_NAMES.join(', ')}.`);
220
+
221
+ if (outsideGitRepo) {
222
+ console.log(`
223
+ [${TOOL_NAME}] No git repository detected in current directory.
224
+ [${TOOL_NAME}] Start from a repo root, or pass an explicit target:
225
+ ${TOOL_NAME} setup --target <path-to-git-repo>
226
+ ${TOOL_NAME} doctor --target <path-to-git-repo>`);
227
+ }
228
+ }
229
+
230
+ function formatElapsedDuration(ms) {
231
+ const durationMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
232
+ if (durationMs < 1000) {
233
+ return `${Math.round(durationMs)}ms`;
234
+ }
235
+ if (durationMs < 10_000) {
236
+ return `${(durationMs / 1000).toFixed(1)}s`;
237
+ }
238
+ return `${Math.round(durationMs / 1000)}s`;
239
+ }
240
+
241
+ function truncateMiddle(value, maxLength) {
242
+ const text = String(value || '');
243
+ const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
244
+ if (!limit || text.length <= limit) {
245
+ return text;
246
+ }
247
+
248
+ const visible = limit - 1;
249
+ const headLength = Math.ceil(visible / 2);
250
+ const tailLength = Math.floor(visible / 2);
251
+ return `${text.slice(0, headLength)}…${text.slice(text.length - tailLength)}`;
252
+ }
253
+
254
+ function truncateTail(value, maxLength) {
255
+ const text = String(value || '');
256
+ const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
257
+ if (!limit || text.length <= limit) {
258
+ return text;
259
+ }
260
+ return `${text.slice(0, limit - 1)}…`;
261
+ }
262
+
263
+ function compactAutoFinishPathSegments(message) {
264
+ return String(message || '').replace(/\((\/[^)]+)\)/g, (_, rawPath) => {
265
+ if (
266
+ rawPath.includes(`${path.sep}.omx${path.sep}agent-worktrees${path.sep}`) ||
267
+ rawPath.includes(`${path.sep}.omc${path.sep}agent-worktrees${path.sep}`)
268
+ ) {
269
+ return `(${path.basename(rawPath)})`;
270
+ }
271
+ return `(${truncateMiddle(rawPath, 72)})`;
272
+ });
273
+ }
274
+
275
+ function detectRecoverableAutoFinishConflict(message) {
276
+ const text = String(message || '').trim();
277
+ if (!text) {
278
+ return null;
279
+ }
280
+
281
+ if (/rebase --continue/i.test(text) && /rebase --abort/i.test(text)) {
282
+ return {
283
+ rawLabel: 'auto-finish requires manual rebase.',
284
+ summary: 'manual rebase required in the source-probe worktree; run rebase --continue or rebase --abort',
285
+ };
286
+ }
287
+
288
+ if (/Rebase\/merge '.+' into '.+' and resolve conflicts before finishing\./i.test(text)) {
289
+ return {
290
+ rawLabel: 'auto-finish requires manual rebase or merge.',
291
+ summary: 'manual rebase or merge required before auto-finish can continue',
292
+ };
293
+ }
294
+
295
+ if (/Merge conflict detected while merging/i.test(text)) {
296
+ return {
297
+ rawLabel: 'auto-finish requires manual merge resolution.',
298
+ summary: 'manual merge resolution required before auto-finish can continue',
299
+ };
300
+ }
301
+
302
+ return null;
303
+ }
304
+
305
+ function summarizeAutoFinishDetail(detail) {
306
+ const trimmed = String(detail || '').trim();
307
+ const match = trimmed.match(/^\[(\w+)\]\s+([^:]+):\s*(.*)$/);
308
+ if (!match) {
309
+ return truncateTail(compactAutoFinishPathSegments(trimmed), DOCTOR_AUTO_FINISH_MESSAGE_MAX);
310
+ }
311
+
312
+ const [, status, rawBranch, rawMessage] = match;
313
+ const branch = truncateMiddle(rawBranch, DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX);
314
+ let message = String(rawMessage || '').trim();
315
+ const recoverableConflict = status === 'skip' ? detectRecoverableAutoFinishConflict(message) : null;
316
+
317
+ if (recoverableConflict) {
318
+ message = recoverableConflict.summary;
319
+ } else if (status === 'fail') {
320
+ message = message.replace(/^auto-finish failed\.?\s*/i, '');
321
+ if (/\[agent-sync-guard\]/.test(message) && /Resolve conflicts/i.test(message)) {
322
+ message = 'rebase conflict in finish flow; run rebase --continue or rebase --abort in the source-probe worktree';
323
+ } else if (/unable to compute ahead\/behind/i.test(message)) {
324
+ const aheadBehindMatch = message.match(/unable to compute ahead\/behind(?: \([^)]+\))?/i);
325
+ if (aheadBehindMatch) {
326
+ message = aheadBehindMatch[0];
327
+ }
328
+ } else if (/remote ref does not exist/i.test(message)) {
329
+ message = 'branch merged, but the remote ref was already removed during cleanup';
330
+ }
331
+ }
332
+
333
+ message = compactAutoFinishPathSegments(message)
334
+ .replace(/\s+\|\s+/g, '; ')
335
+ .trim();
336
+
337
+ return `[${status}] ${branch}: ${truncateTail(message, DOCTOR_AUTO_FINISH_MESSAGE_MAX)}`;
338
+ }
339
+
340
+ function printAutoFinishSummary(summary, options = {}) {
341
+ const enabled = Boolean(summary && summary.enabled);
342
+ const details = Array.isArray(summary && summary.details) ? summary.details : [];
343
+ const baseBranch = String(options.baseBranch || summary?.baseBranch || '').trim();
344
+ const verbose = Boolean(options.verbose);
345
+ const detailLimit = Number.isFinite(options.detailLimit)
346
+ ? Math.max(0, options.detailLimit)
347
+ : DOCTOR_AUTO_FINISH_DETAIL_LIMIT;
348
+
349
+ if (enabled) {
350
+ console.log(
351
+ colorizeDoctorOutput(
352
+ `[${TOOL_NAME}] Auto-finish sweep (base=${baseBranch}): attempted=${summary.attempted}, completed=${summary.completed}, skipped=${summary.skipped}, failed=${summary.failed}`,
353
+ detectAutoFinishSummaryStatus(summary),
354
+ ),
355
+ );
356
+ const visibleDetails = verbose ? details : details.slice(0, detailLimit).map(summarizeAutoFinishDetail);
357
+ for (const detail of visibleDetails) {
358
+ console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ${detail}`, detectAutoFinishDetailStatus(detail)));
359
+ }
360
+ if (!verbose && details.length > visibleDetails.length) {
361
+ console.log(
362
+ colorizeDoctorOutput(
363
+ `[${TOOL_NAME}] … ${details.length - visibleDetails.length} more branch result(s). Re-run with --verbose-auto-finish for full details.`,
364
+ 'warn',
365
+ ),
366
+ );
367
+ }
368
+ return;
369
+ }
370
+
371
+ if (details.length > 0) {
372
+ const detail = verbose ? details[0] : summarizeAutoFinishDetail(details[0]);
373
+ console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ${detail}`, detectAutoFinishDetailStatus(detail)));
374
+ }
375
+ }
376
+
377
+ module.exports = {
378
+ runtimeVersion,
379
+ supportsAnsiColors,
380
+ colorize,
381
+ doctorOutputColorCode,
382
+ colorizeDoctorOutput,
383
+ detectAutoFinishDetailStatus,
384
+ detectAutoFinishSummaryStatus,
385
+ statusDot,
386
+ commandCatalogLines,
387
+ agentBotCatalogLines,
388
+ repoToggleLines,
389
+ printToolLogsSummary,
390
+ usage,
391
+ formatElapsedDuration,
392
+ truncateMiddle,
393
+ truncateTail,
394
+ compactAutoFinishPathSegments,
395
+ detectRecoverableAutoFinishConflict,
396
+ summarizeAutoFinishDetail,
397
+ printAutoFinishSummary,
398
+ };
@@ -0,0 +1,68 @@
1
+ function createSandboxApi(deps) {
2
+ const {
3
+ protectedBaseWriteBlock,
4
+ runInstallInternal,
5
+ ensureSetupProtectedBranches,
6
+ ensureParentWorkspaceView,
7
+ buildParentWorkspaceView,
8
+ runFixInternal,
9
+ } = deps;
10
+
11
+ function assertProtectedMainWriteAllowed(options, commandName) {
12
+ const blocked = protectedBaseWriteBlock(options);
13
+ if (!blocked) {
14
+ return;
15
+ }
16
+
17
+ throw new Error(
18
+ `${commandName} blocked on protected branch '${blocked.branch}' in an initialized repo.\n` +
19
+ `Keep local '${blocked.branch}' pull-only: start an agent branch/worktree first:\n` +
20
+ ` gx branch start "<task>" "codex"\n` +
21
+ `Override once only when intentional: --allow-protected-base-write`,
22
+ );
23
+ }
24
+
25
+ function runSetupBootstrapInternal(options) {
26
+ const installPayload = runInstallInternal(options);
27
+ installPayload.operations.push(
28
+ ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun)),
29
+ );
30
+
31
+ let parentWorkspace = null;
32
+ if (options.parentWorkspaceView) {
33
+ installPayload.operations.push(
34
+ ensureParentWorkspaceView(installPayload.repoRoot, Boolean(options.dryRun)),
35
+ );
36
+ if (!options.dryRun) {
37
+ parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot);
38
+ }
39
+ }
40
+
41
+ const fixPayload = runFixInternal({
42
+ target: installPayload.repoRoot,
43
+ dryRun: options.dryRun,
44
+ force: options.force,
45
+ forceManagedPaths: options.forceManagedPaths,
46
+ dropStaleLocks: true,
47
+ skipAgents: options.skipAgents,
48
+ skipPackageJson: options.skipPackageJson,
49
+ skipGitignore: options.skipGitignore,
50
+ allowProtectedBaseWrite: options.allowProtectedBaseWrite,
51
+ });
52
+
53
+ return {
54
+ installPayload,
55
+ fixPayload,
56
+ parentWorkspace,
57
+ };
58
+ }
59
+
60
+ return {
61
+ assertProtectedMainWriteAllowed,
62
+ runSetupBootstrapInternal,
63
+ };
64
+ }
65
+
66
+ module.exports = {
67
+ createSandboxApi,
68
+ };
@@ -0,0 +1,148 @@
1
+ const {
2
+ fs,
3
+ path,
4
+ TOOL_NAME,
5
+ SHORT_TOOL_NAME,
6
+ toDestinationPath,
7
+ EXECUTABLE_RELATIVE_PATHS,
8
+ CRITICAL_GUARDRAIL_PATHS,
9
+ } = require('../context');
10
+
11
+ function ensureParentDir(repoRoot, filePath, dryRun) {
12
+ if (dryRun) return;
13
+
14
+ const parentDir = path.dirname(filePath);
15
+ const relativeParentDir = path.relative(repoRoot, parentDir);
16
+ const segments = relativeParentDir.split(path.sep).filter(Boolean);
17
+ let currentPath = repoRoot;
18
+
19
+ for (const segment of segments) {
20
+ currentPath = path.join(currentPath, segment);
21
+ if (fs.existsSync(currentPath) && !fs.statSync(currentPath).isDirectory()) {
22
+ const blockingPath = path.relative(repoRoot, currentPath) || path.basename(currentPath);
23
+ const targetPath = path.relative(repoRoot, filePath) || path.basename(filePath);
24
+ throw new Error(
25
+ `Path conflict: ${blockingPath} exists as a file, but ${targetPath} needs it to be a directory. ` +
26
+ `Remove or rename ${blockingPath} and rerun '${SHORT_TOOL_NAME} setup'.`,
27
+ );
28
+ }
29
+ }
30
+
31
+ fs.mkdirSync(parentDir, { recursive: true });
32
+ }
33
+
34
+ function ensureExecutable(destinationPath, relativePath, dryRun) {
35
+ if (dryRun) return;
36
+ if (EXECUTABLE_RELATIVE_PATHS.has(relativePath)) {
37
+ fs.chmodSync(destinationPath, 0o755);
38
+ }
39
+ }
40
+
41
+ function isCriticalGuardrailPath(relativePath) {
42
+ return CRITICAL_GUARDRAIL_PATHS.has(relativePath);
43
+ }
44
+
45
+ function shellSingleQuote(value) {
46
+ return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
47
+ }
48
+
49
+ function renderShellDispatchShim(commandParts) {
50
+ const rendered = commandParts.map((part) => shellSingleQuote(part)).join(' ');
51
+ return (
52
+ '#!/usr/bin/env bash\n' +
53
+ 'set -euo pipefail\n' +
54
+ '\n' +
55
+ 'if [[ -n "${GUARDEX_CLI_ENTRY:-}" ]]; then\n' +
56
+ ' node_bin="${GUARDEX_NODE_BIN:-node}"\n' +
57
+ ` exec "$node_bin" "$GUARDEX_CLI_ENTRY" ${rendered} "$@"\n` +
58
+ 'fi\n' +
59
+ '\n' +
60
+ 'resolve_guardex_cli() {\n' +
61
+ ' if [[ -n "${GUARDEX_CLI_BIN:-}" ]]; then\n' +
62
+ ' printf \'%s\' "$GUARDEX_CLI_BIN"\n' +
63
+ ' return 0\n' +
64
+ ' fi\n' +
65
+ ' if command -v gx >/dev/null 2>&1; then\n' +
66
+ ' printf \'%s\' "gx"\n' +
67
+ ' return 0\n' +
68
+ ' fi\n' +
69
+ ' if command -v gitguardex >/dev/null 2>&1; then\n' +
70
+ ' printf \'%s\' "gitguardex"\n' +
71
+ ' return 0\n' +
72
+ ' fi\n' +
73
+ ' echo "[gitguardex-shim] Missing gx CLI in PATH." >&2\n' +
74
+ ' exit 1\n' +
75
+ '}\n' +
76
+ '\n' +
77
+ 'cli_bin="$(resolve_guardex_cli)"\n' +
78
+ `exec "$cli_bin" ${rendered} "$@"\n`
79
+ );
80
+ }
81
+
82
+ function renderPythonDispatchShim(commandParts) {
83
+ return (
84
+ '#!/usr/bin/env python3\n' +
85
+ 'import os\n' +
86
+ 'import shutil\n' +
87
+ 'import subprocess\n' +
88
+ 'import sys\n' +
89
+ '\n' +
90
+ `COMMAND = ${JSON.stringify(commandParts)}\n` +
91
+ '\n' +
92
+ 'entry = os.environ.get("GUARDEX_CLI_ENTRY")\n' +
93
+ 'if entry:\n' +
94
+ ' node_bin = os.environ.get("GUARDEX_NODE_BIN") or shutil.which("node") or "node"\n' +
95
+ ' raise SystemExit(subprocess.call([node_bin, entry, *COMMAND, *sys.argv[1:]]))\n' +
96
+ 'cli = os.environ.get("GUARDEX_CLI_BIN") or shutil.which("gx") or shutil.which("gitguardex")\n' +
97
+ 'if not cli:\n' +
98
+ ' sys.stderr.write("[gitguardex-shim] Missing gx CLI in PATH.\\n")\n' +
99
+ ' raise SystemExit(1)\n' +
100
+ 'raise SystemExit(subprocess.call([cli, *COMMAND, *sys.argv[1:]]))\n'
101
+ );
102
+ }
103
+
104
+ function managedForceConflictMessage(relativePath) {
105
+ return (
106
+ `Refusing to overwrite existing file without --force: ${relativePath}\n` +
107
+ `Use '--force ${relativePath}' to rewrite only this managed file, or '--force' to rewrite all managed files.`
108
+ );
109
+ }
110
+
111
+ function printOperations(title, payload, dryRun = false) {
112
+ console.log(`[${TOOL_NAME}] ${title}: ${payload.repoRoot}`);
113
+ for (const operation of payload.operations) {
114
+ const note = operation.note ? ` (${operation.note})` : '';
115
+ console.log(` - ${operation.status.padEnd(12)} ${operation.file}${note}`);
116
+ }
117
+ console.log(
118
+ ` - hooksPath ${payload.hookResult.status} ${payload.hookResult.key}=${payload.hookResult.value}`,
119
+ );
120
+
121
+ if (dryRun) {
122
+ console.log(`[${TOOL_NAME}] Dry run complete. No files were modified.`);
123
+ }
124
+ }
125
+
126
+ function printStandaloneOperations(title, rootLabel, operations, dryRun = false) {
127
+ console.log(`[${TOOL_NAME}] ${title}: ${rootLabel}`);
128
+ for (const operation of operations) {
129
+ const note = operation.note ? ` (${operation.note})` : '';
130
+ console.log(` - ${operation.status.padEnd(12)} ${operation.file}${note}`);
131
+ }
132
+ if (dryRun) {
133
+ console.log(`[${TOOL_NAME}] Dry run complete. No files were modified.`);
134
+ }
135
+ }
136
+
137
+ module.exports = {
138
+ toDestinationPath,
139
+ ensureParentDir,
140
+ ensureExecutable,
141
+ isCriticalGuardrailPath,
142
+ shellSingleQuote,
143
+ renderShellDispatchShim,
144
+ renderPythonDispatchShim,
145
+ managedForceConflictMessage,
146
+ printOperations,
147
+ printStandaloneOperations,
148
+ };