switchman-dev 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/examples/README.md +18 -0
- package/examples/demo.sh +124 -0
- package/examples/setup.sh +13 -6
- package/examples/taskapi/.cursor/mcp.json +8 -0
- package/examples/taskapi/.mcp.json +8 -0
- package/examples/taskapi/src/middleware/auth.js +4 -0
- package/examples/taskapi/src/middleware/validate.js +4 -0
- package/examples/taskapi/src/routes/tasks.js +4 -0
- package/examples/taskapi/src/server.js +4 -0
- package/examples/walkthrough.sh +16 -9
- package/package.json +1 -1
- package/src/cli/index.js +632 -99
package/src/cli/index.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { program } from 'commander';
|
|
20
20
|
import chalk from 'chalk';
|
|
21
21
|
import ora from 'ora';
|
|
22
|
+
import { existsSync } from 'fs';
|
|
22
23
|
import { join } from 'path';
|
|
23
24
|
import { execSync, spawn } from 'child_process';
|
|
24
25
|
|
|
@@ -170,6 +171,40 @@ function renderMiniBar(items) {
|
|
|
170
171
|
return items.map(({ label, value, color = chalk.white }) => `${color('■')} ${label}:${value}`).join(chalk.dim(' '));
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
function renderChip(label, value, color = chalk.white) {
|
|
175
|
+
return color(`[${label}:${value}]`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function renderSignalStrip(signals) {
|
|
179
|
+
return signals.join(chalk.dim(' '));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatClockTime(isoString) {
|
|
183
|
+
if (!isoString) return null;
|
|
184
|
+
const date = new Date(isoString);
|
|
185
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
186
|
+
return date.toLocaleTimeString('en-GB', {
|
|
187
|
+
hour: '2-digit',
|
|
188
|
+
minute: '2-digit',
|
|
189
|
+
second: '2-digit',
|
|
190
|
+
hour12: false,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildWatchSignature(report) {
|
|
195
|
+
return JSON.stringify({
|
|
196
|
+
health: report.health,
|
|
197
|
+
summary: report.summary,
|
|
198
|
+
counts: report.counts,
|
|
199
|
+
active_work: report.active_work,
|
|
200
|
+
attention: report.attention,
|
|
201
|
+
queue_summary: report.queue?.summary || null,
|
|
202
|
+
next_up: report.next_up || null,
|
|
203
|
+
next_steps: report.next_steps,
|
|
204
|
+
suggested_commands: report.suggested_commands,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
173
208
|
function formatRelativePolicy(policy) {
|
|
174
209
|
return `stale ${policy.stale_after_minutes}m • heartbeat ${policy.heartbeat_interval_seconds}s • auto-reap ${policy.reap_on_status_check ? 'on' : 'off'}`;
|
|
175
210
|
}
|
|
@@ -178,6 +213,155 @@ function sleepSync(ms) {
|
|
|
178
213
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
179
214
|
}
|
|
180
215
|
|
|
216
|
+
function boolBadge(ok) {
|
|
217
|
+
return ok ? chalk.green('OK ') : chalk.yellow('CHECK');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function printErrorWithNext(message, nextCommand = null) {
|
|
221
|
+
console.error(chalk.red(message));
|
|
222
|
+
if (nextCommand) {
|
|
223
|
+
console.error(`${chalk.yellow('next:')} ${nextCommand}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function collectSetupVerification(repoRoot, { homeDir = null } = {}) {
|
|
228
|
+
const dbPath = join(repoRoot, '.switchman', 'switchman.db');
|
|
229
|
+
const rootMcpPath = join(repoRoot, '.mcp.json');
|
|
230
|
+
const cursorMcpPath = join(repoRoot, '.cursor', 'mcp.json');
|
|
231
|
+
const claudeGuidePath = join(repoRoot, 'CLAUDE.md');
|
|
232
|
+
const checks = [];
|
|
233
|
+
const nextSteps = [];
|
|
234
|
+
let workspaces = [];
|
|
235
|
+
let db = null;
|
|
236
|
+
|
|
237
|
+
const dbExists = existsSync(dbPath);
|
|
238
|
+
checks.push({
|
|
239
|
+
key: 'database',
|
|
240
|
+
ok: dbExists,
|
|
241
|
+
label: 'Project database',
|
|
242
|
+
detail: dbExists ? '.switchman/switchman.db is ready' : 'Switchman database is missing',
|
|
243
|
+
});
|
|
244
|
+
if (!dbExists) {
|
|
245
|
+
nextSteps.push('Run `switchman init` or `switchman setup --agents 3` in this repo.');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (dbExists) {
|
|
249
|
+
try {
|
|
250
|
+
db = getDb(repoRoot);
|
|
251
|
+
workspaces = listWorktrees(db);
|
|
252
|
+
} catch {
|
|
253
|
+
checks.push({
|
|
254
|
+
key: 'database_open',
|
|
255
|
+
ok: false,
|
|
256
|
+
label: 'Database access',
|
|
257
|
+
detail: 'Switchman could not open the project database',
|
|
258
|
+
});
|
|
259
|
+
nextSteps.push('Re-run `switchman init` if the project database looks corrupted.');
|
|
260
|
+
} finally {
|
|
261
|
+
try { db?.close(); } catch { /* no-op */ }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const agentWorkspaces = workspaces.filter((entry) => entry.name !== 'main');
|
|
266
|
+
const workspaceReady = agentWorkspaces.length > 0;
|
|
267
|
+
checks.push({
|
|
268
|
+
key: 'workspaces',
|
|
269
|
+
ok: workspaceReady,
|
|
270
|
+
label: 'Agent workspaces',
|
|
271
|
+
detail: workspaceReady
|
|
272
|
+
? `${agentWorkspaces.length} agent workspace(s) registered`
|
|
273
|
+
: 'No agent workspaces are registered yet',
|
|
274
|
+
});
|
|
275
|
+
if (!workspaceReady) {
|
|
276
|
+
nextSteps.push('Run `switchman setup --agents 3` to create agent workspaces.');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const rootMcpExists = existsSync(rootMcpPath);
|
|
280
|
+
checks.push({
|
|
281
|
+
key: 'claude_mcp',
|
|
282
|
+
ok: rootMcpExists,
|
|
283
|
+
label: 'Claude Code MCP',
|
|
284
|
+
detail: rootMcpExists ? '.mcp.json is present in the repo root' : '.mcp.json is missing from the repo root',
|
|
285
|
+
});
|
|
286
|
+
if (!rootMcpExists) {
|
|
287
|
+
nextSteps.push('Re-run `switchman setup --agents 3` to restore the repo-local MCP config.');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const cursorMcpExists = existsSync(cursorMcpPath);
|
|
291
|
+
checks.push({
|
|
292
|
+
key: 'cursor_mcp',
|
|
293
|
+
ok: cursorMcpExists,
|
|
294
|
+
label: 'Cursor MCP',
|
|
295
|
+
detail: cursorMcpExists ? '.cursor/mcp.json is present in the repo root' : '.cursor/mcp.json is missing from the repo root',
|
|
296
|
+
});
|
|
297
|
+
if (!cursorMcpExists) {
|
|
298
|
+
nextSteps.push('Re-run `switchman setup --agents 3` if you want Cursor to attach automatically.');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const claudeGuideExists = existsSync(claudeGuidePath);
|
|
302
|
+
checks.push({
|
|
303
|
+
key: 'claude_md',
|
|
304
|
+
ok: claudeGuideExists,
|
|
305
|
+
label: 'Claude guide',
|
|
306
|
+
detail: claudeGuideExists ? 'CLAUDE.md is present' : 'CLAUDE.md is optional but recommended for Claude Code',
|
|
307
|
+
});
|
|
308
|
+
if (!claudeGuideExists) {
|
|
309
|
+
nextSteps.push('If you use Claude Code, add `CLAUDE.md` from the repo root setup guide.');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const windsurfConfigExists = existsSync(getWindsurfMcpConfigPath(homeDir || undefined));
|
|
313
|
+
checks.push({
|
|
314
|
+
key: 'windsurf_mcp',
|
|
315
|
+
ok: windsurfConfigExists,
|
|
316
|
+
label: 'Windsurf MCP',
|
|
317
|
+
detail: windsurfConfigExists
|
|
318
|
+
? 'Windsurf shared MCP config is installed'
|
|
319
|
+
: 'Windsurf shared MCP config is optional and not installed',
|
|
320
|
+
});
|
|
321
|
+
if (!windsurfConfigExists) {
|
|
322
|
+
nextSteps.push('If you use Windsurf, run `switchman mcp install --windsurf` once.');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const ok = checks.every((item) => item.ok || ['claude_md', 'windsurf_mcp'].includes(item.key));
|
|
326
|
+
return {
|
|
327
|
+
ok,
|
|
328
|
+
repo_root: repoRoot,
|
|
329
|
+
checks,
|
|
330
|
+
workspaces: workspaces.map((entry) => ({
|
|
331
|
+
name: entry.name,
|
|
332
|
+
path: entry.path,
|
|
333
|
+
branch: entry.branch,
|
|
334
|
+
})),
|
|
335
|
+
suggested_commands: [
|
|
336
|
+
'switchman status --watch',
|
|
337
|
+
'switchman task add "Your first task" --priority 8',
|
|
338
|
+
'switchman gate ci',
|
|
339
|
+
...nextSteps.some((step) => step.includes('Windsurf')) ? ['switchman mcp install --windsurf'] : [],
|
|
340
|
+
],
|
|
341
|
+
next_steps: [...new Set(nextSteps)].slice(0, 6),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function renderSetupVerification(report, { compact = false } = {}) {
|
|
346
|
+
console.log(chalk.bold(compact ? 'First-run check:' : 'Setup verification:'));
|
|
347
|
+
for (const check of report.checks) {
|
|
348
|
+
const badge = boolBadge(check.ok);
|
|
349
|
+
console.log(` ${badge} ${check.label} ${chalk.dim(`— ${check.detail}`)}`);
|
|
350
|
+
}
|
|
351
|
+
if (report.next_steps.length > 0) {
|
|
352
|
+
console.log('');
|
|
353
|
+
console.log(chalk.bold('Fix next:'));
|
|
354
|
+
for (const step of report.next_steps) {
|
|
355
|
+
console.log(` - ${step}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
console.log('');
|
|
359
|
+
console.log(chalk.bold('Try next:'));
|
|
360
|
+
for (const command of report.suggested_commands.slice(0, 4)) {
|
|
361
|
+
console.log(` ${chalk.cyan(command)}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
181
365
|
function summarizeLeaseScope(db, lease) {
|
|
182
366
|
const reservations = listScopeReservations(db, { leaseId: lease.id });
|
|
183
367
|
const pathScopes = reservations
|
|
@@ -665,13 +849,34 @@ function renderUnifiedStatusReport(report) {
|
|
|
665
849
|
const badge = healthColor(healthLabel(report.health));
|
|
666
850
|
const mergeColor = report.merge_readiness.ci_gate_ok ? chalk.green : chalk.red;
|
|
667
851
|
const queueCounts = report.counts.queue;
|
|
852
|
+
const blockedCount = report.attention.filter((item) => item.severity === 'block').length;
|
|
853
|
+
const warningCount = report.attention.filter((item) => item.severity !== 'block').length;
|
|
854
|
+
const focusItem = blockedCount > 0
|
|
855
|
+
? report.attention.find((item) => item.severity === 'block')
|
|
856
|
+
: warningCount > 0
|
|
857
|
+
? report.attention.find((item) => item.severity !== 'block')
|
|
858
|
+
: report.next_up[0];
|
|
859
|
+
const focusLine = focusItem
|
|
860
|
+
? ('title' in focusItem
|
|
861
|
+
? `${focusItem.title}${focusItem.detail ? ` ${chalk.dim(`• ${focusItem.detail}`)}` : ''}`
|
|
862
|
+
: `${focusItem.title} ${chalk.dim(focusItem.id)}`)
|
|
863
|
+
: 'Nothing urgent. Safe to keep parallel work moving.';
|
|
864
|
+
const queueLoad = queueCounts.queued + queueCounts.retrying + queueCounts.merging + queueCounts.blocked;
|
|
865
|
+
const landingLabel = report.merge_readiness.ci_gate_ok ? 'ready' : 'hold';
|
|
668
866
|
|
|
669
867
|
console.log('');
|
|
670
|
-
console.log(healthColor('='.repeat(
|
|
671
|
-
console.log(`${badge} ${chalk.bold('switchman status')} ${chalk.dim('•
|
|
868
|
+
console.log(healthColor('='.repeat(72)));
|
|
869
|
+
console.log(`${badge} ${chalk.bold('switchman status')} ${chalk.dim('• mission control for parallel agents')}`);
|
|
672
870
|
console.log(`${chalk.dim(report.repo_root)}`);
|
|
673
871
|
console.log(`${chalk.dim(report.summary)}`);
|
|
674
|
-
console.log(healthColor('='.repeat(
|
|
872
|
+
console.log(healthColor('='.repeat(72)));
|
|
873
|
+
console.log(renderSignalStrip([
|
|
874
|
+
renderChip('health', healthLabel(report.health), healthColor),
|
|
875
|
+
renderChip('blocked', blockedCount, blockedCount > 0 ? chalk.red : chalk.green),
|
|
876
|
+
renderChip('watch', warningCount, warningCount > 0 ? chalk.yellow : chalk.green),
|
|
877
|
+
renderChip('landing', landingLabel, mergeColor),
|
|
878
|
+
renderChip('queue', queueLoad, queueLoad > 0 ? chalk.blue : chalk.green),
|
|
879
|
+
]));
|
|
675
880
|
console.log(renderMetricRow([
|
|
676
881
|
{ label: 'tasks', value: `${report.counts.pending}/${report.counts.in_progress}/${report.counts.done}/${report.counts.failed}`, color: chalk.white },
|
|
677
882
|
{ label: 'leases', value: `${report.counts.active_leases} active`, color: chalk.blue },
|
|
@@ -685,13 +890,18 @@ function renderUnifiedStatusReport(report) {
|
|
|
685
890
|
{ label: 'merging', value: queueCounts.merging, color: chalk.blue },
|
|
686
891
|
{ label: 'merged', value: queueCounts.merged, color: chalk.green },
|
|
687
892
|
]));
|
|
893
|
+
console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
|
|
688
894
|
console.log(chalk.dim(`policy: ${formatRelativePolicy(report.lease_policy)} • requeue on reap ${report.lease_policy.requeue_task_on_reap ? 'on' : 'off'}`));
|
|
689
895
|
|
|
690
896
|
const runningLines = report.active_work.length > 0
|
|
691
897
|
? report.active_work.slice(0, 5).map((item) => {
|
|
692
|
-
const boundary = item.boundary_validation
|
|
693
|
-
|
|
694
|
-
|
|
898
|
+
const boundary = item.boundary_validation
|
|
899
|
+
? ` ${renderChip('validation', item.boundary_validation.status, item.boundary_validation.status === 'accepted' ? chalk.green : chalk.yellow)}`
|
|
900
|
+
: '';
|
|
901
|
+
const stale = (item.dependency_invalidations?.length || 0) > 0
|
|
902
|
+
? ` ${renderChip('stale', item.dependency_invalidations.length, chalk.yellow)}`
|
|
903
|
+
: '';
|
|
904
|
+
return `${chalk.cyan(item.worktree)} -> ${item.task_title} ${chalk.dim(item.task_id)}${item.scope_summary ? ` ${chalk.dim(item.scope_summary)}` : ''}${boundary}${stale}`;
|
|
695
905
|
})
|
|
696
906
|
: [chalk.dim('Nothing active right now.')];
|
|
697
907
|
|
|
@@ -700,7 +910,7 @@ function renderUnifiedStatusReport(report) {
|
|
|
700
910
|
|
|
701
911
|
const blockedLines = blockedItems.length > 0
|
|
702
912
|
? blockedItems.slice(0, 4).flatMap((item) => {
|
|
703
|
-
const lines = [`${chalk.red
|
|
913
|
+
const lines = [`${renderChip('BLOCKED', item.kind || 'item', chalk.red)} ${item.title}`];
|
|
704
914
|
if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
|
|
705
915
|
lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
|
|
706
916
|
if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
|
|
@@ -710,8 +920,7 @@ function renderUnifiedStatusReport(report) {
|
|
|
710
920
|
|
|
711
921
|
const warningLines = warningItems.length > 0
|
|
712
922
|
? warningItems.slice(0, 4).flatMap((item) => {
|
|
713
|
-
const
|
|
714
|
-
const lines = [`${tone} ${item.title}`];
|
|
923
|
+
const lines = [`${renderChip('WATCH', item.kind || 'item', chalk.yellow)} ${item.title}`];
|
|
715
924
|
if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
|
|
716
925
|
lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
|
|
717
926
|
if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
|
|
@@ -728,7 +937,7 @@ function renderUnifiedStatusReport(report) {
|
|
|
728
937
|
.filter((entry) => ['blocked', 'retrying', 'merging'].includes(entry.status))
|
|
729
938
|
.slice(0, 4)
|
|
730
939
|
.flatMap((item) => {
|
|
731
|
-
const lines = [`${
|
|
940
|
+
const lines = [`${renderChip(item.status.toUpperCase(), item.id, item.status === 'blocked' ? chalk.red : item.status === 'retrying' ? chalk.yellow : chalk.blue)} ${item.source_type}:${item.source_ref} ${chalk.dim(`retries:${item.retry_count}/${item.max_retries}`)}`];
|
|
732
941
|
if (item.last_error_summary) lines.push(` ${chalk.red('why:')} ${item.last_error_summary}`);
|
|
733
942
|
if (item.next_action) lines.push(` ${chalk.yellow('next:')} ${item.next_action}`);
|
|
734
943
|
return lines;
|
|
@@ -738,7 +947,7 @@ function renderUnifiedStatusReport(report) {
|
|
|
738
947
|
|
|
739
948
|
const nextActionLines = [
|
|
740
949
|
...(report.next_up.length > 0
|
|
741
|
-
? report.next_up.map((task) => `
|
|
950
|
+
? report.next_up.map((task) => `${renderChip('NEXT', `p${task.priority}`, chalk.green)} ${task.title} ${chalk.dim(task.id)}`)
|
|
742
951
|
: [chalk.dim('No pending tasks waiting right now.')]),
|
|
743
952
|
'',
|
|
744
953
|
...report.suggested_commands.slice(0, 4).map((command) => `${chalk.cyan('$')} ${command}`),
|
|
@@ -859,6 +1068,23 @@ program
|
|
|
859
1068
|
.description('Conflict-aware task coordinator for parallel AI coding agents')
|
|
860
1069
|
.version('0.1.0');
|
|
861
1070
|
|
|
1071
|
+
program.showHelpAfterError('(run with --help for usage examples)');
|
|
1072
|
+
program.addHelpText('after', `
|
|
1073
|
+
Start here:
|
|
1074
|
+
switchman setup --agents 5
|
|
1075
|
+
switchman status --watch
|
|
1076
|
+
switchman gate ci
|
|
1077
|
+
|
|
1078
|
+
Most useful commands:
|
|
1079
|
+
switchman task add "Implement auth helper" --priority 9
|
|
1080
|
+
switchman lease next --json
|
|
1081
|
+
switchman queue run --watch
|
|
1082
|
+
|
|
1083
|
+
Docs:
|
|
1084
|
+
README.md
|
|
1085
|
+
docs/setup-cursor.md
|
|
1086
|
+
`);
|
|
1087
|
+
|
|
862
1088
|
// ── init ──────────────────────────────────────────────────────────────────────
|
|
863
1089
|
|
|
864
1090
|
program
|
|
@@ -902,6 +1128,11 @@ program
|
|
|
902
1128
|
.description('One-command setup: create agent workspaces and initialise Switchman')
|
|
903
1129
|
.option('-a, --agents <n>', 'Number of agent workspaces to create (default: 3)', '3')
|
|
904
1130
|
.option('--prefix <prefix>', 'Branch prefix (default: switchman)', 'switchman')
|
|
1131
|
+
.addHelpText('after', `
|
|
1132
|
+
Examples:
|
|
1133
|
+
switchman setup --agents 5
|
|
1134
|
+
switchman setup --agents 3 --prefix team
|
|
1135
|
+
`)
|
|
905
1136
|
.action((opts) => {
|
|
906
1137
|
const agentCount = parseInt(opts.agents);
|
|
907
1138
|
|
|
@@ -984,12 +1215,41 @@ program
|
|
|
984
1215
|
console.log(` ${chalk.cyan('switchman status')}`);
|
|
985
1216
|
console.log('');
|
|
986
1217
|
|
|
1218
|
+
const verification = collectSetupVerification(repoRoot);
|
|
1219
|
+
renderSetupVerification(verification, { compact: true });
|
|
1220
|
+
|
|
987
1221
|
} catch (err) {
|
|
988
1222
|
spinner.fail(err.message);
|
|
989
1223
|
process.exit(1);
|
|
990
1224
|
}
|
|
991
1225
|
});
|
|
992
1226
|
|
|
1227
|
+
program
|
|
1228
|
+
.command('verify-setup')
|
|
1229
|
+
.description('Check whether this repo is ready for a smooth first Switchman run')
|
|
1230
|
+
.option('--json', 'Output raw JSON')
|
|
1231
|
+
.option('--home <path>', 'Override the home directory for editor config checks')
|
|
1232
|
+
.addHelpText('after', `
|
|
1233
|
+
Examples:
|
|
1234
|
+
switchman verify-setup
|
|
1235
|
+
switchman verify-setup --json
|
|
1236
|
+
|
|
1237
|
+
Use this after setup or whenever editor/config wiring feels off.
|
|
1238
|
+
`)
|
|
1239
|
+
.action((opts) => {
|
|
1240
|
+
const repoRoot = getRepo();
|
|
1241
|
+
const report = collectSetupVerification(repoRoot, { homeDir: opts.home || null });
|
|
1242
|
+
|
|
1243
|
+
if (opts.json) {
|
|
1244
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1245
|
+
if (!report.ok) process.exitCode = 1;
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
renderSetupVerification(report);
|
|
1250
|
+
if (!report.ok) process.exitCode = 1;
|
|
1251
|
+
});
|
|
1252
|
+
|
|
993
1253
|
|
|
994
1254
|
// ── mcp ───────────────────────────────────────────────────────────────────────
|
|
995
1255
|
|
|
@@ -1001,6 +1261,11 @@ mcpCmd
|
|
|
1001
1261
|
.option('--windsurf', 'Write Windsurf MCP config to ~/.codeium/mcp_config.json')
|
|
1002
1262
|
.option('--home <path>', 'Override the home directory for config writes (useful for testing)')
|
|
1003
1263
|
.option('--json', 'Output raw JSON')
|
|
1264
|
+
.addHelpText('after', `
|
|
1265
|
+
Examples:
|
|
1266
|
+
switchman mcp install --windsurf
|
|
1267
|
+
switchman mcp install --windsurf --json
|
|
1268
|
+
`)
|
|
1004
1269
|
.action((opts) => {
|
|
1005
1270
|
if (!opts.windsurf) {
|
|
1006
1271
|
console.error(chalk.red('Choose an editor install target, for example `switchman mcp install --windsurf`.'));
|
|
@@ -1030,6 +1295,12 @@ mcpCmd
|
|
|
1030
1295
|
// ── task ──────────────────────────────────────────────────────────────────────
|
|
1031
1296
|
|
|
1032
1297
|
const taskCmd = program.command('task').description('Manage the task list');
|
|
1298
|
+
taskCmd.addHelpText('after', `
|
|
1299
|
+
Examples:
|
|
1300
|
+
switchman task add "Fix login bug" --priority 8
|
|
1301
|
+
switchman task list --status pending
|
|
1302
|
+
switchman task done task-123
|
|
1303
|
+
`);
|
|
1033
1304
|
|
|
1034
1305
|
taskCmd
|
|
1035
1306
|
.command('add <title>')
|
|
@@ -1127,10 +1398,15 @@ taskCmd
|
|
|
1127
1398
|
|
|
1128
1399
|
taskCmd
|
|
1129
1400
|
.command('next')
|
|
1130
|
-
.description('Get
|
|
1401
|
+
.description('Get the next pending task quickly (use `lease next` for the full workflow)')
|
|
1131
1402
|
.option('--json', 'Output as JSON')
|
|
1132
1403
|
.option('--worktree <name>', 'Workspace to assign the task to (defaults to the current folder name)')
|
|
1133
1404
|
.option('--agent <name>', 'Agent identifier for logging (e.g. claude-code)')
|
|
1405
|
+
.addHelpText('after', `
|
|
1406
|
+
Examples:
|
|
1407
|
+
switchman task next
|
|
1408
|
+
switchman task next --json
|
|
1409
|
+
`)
|
|
1134
1410
|
.action((opts) => {
|
|
1135
1411
|
const repoRoot = getRepo();
|
|
1136
1412
|
const worktreeName = getCurrentWorktreeName(opts.worktree);
|
|
@@ -1159,7 +1435,13 @@ taskCmd
|
|
|
1159
1435
|
|
|
1160
1436
|
// ── queue ─────────────────────────────────────────────────────────────────────
|
|
1161
1437
|
|
|
1162
|
-
const queueCmd = program.command('queue').description('Land finished work safely back onto main, one item at a time');
|
|
1438
|
+
const queueCmd = program.command('queue').alias('land').description('Land finished work safely back onto main, one item at a time');
|
|
1439
|
+
queueCmd.addHelpText('after', `
|
|
1440
|
+
Examples:
|
|
1441
|
+
switchman queue add --worktree agent1
|
|
1442
|
+
switchman queue status
|
|
1443
|
+
switchman queue run --watch
|
|
1444
|
+
`);
|
|
1163
1445
|
|
|
1164
1446
|
queueCmd
|
|
1165
1447
|
.command('add [branch]')
|
|
@@ -1170,6 +1452,12 @@ queueCmd
|
|
|
1170
1452
|
.option('--max-retries <n>', 'Maximum automatic retries', '1')
|
|
1171
1453
|
.option('--submitted-by <name>', 'Operator or automation name')
|
|
1172
1454
|
.option('--json', 'Output raw JSON')
|
|
1455
|
+
.addHelpText('after', `
|
|
1456
|
+
Examples:
|
|
1457
|
+
switchman queue add feature/auth-hardening
|
|
1458
|
+
switchman queue add --worktree agent2
|
|
1459
|
+
switchman queue add --pipeline pipe-123
|
|
1460
|
+
`)
|
|
1173
1461
|
.action((branch, opts) => {
|
|
1174
1462
|
const repoRoot = getRepo();
|
|
1175
1463
|
const db = getDb(repoRoot);
|
|
@@ -1179,7 +1467,7 @@ queueCmd
|
|
|
1179
1467
|
if (opts.worktree) {
|
|
1180
1468
|
const worktree = listWorktrees(db).find((entry) => entry.name === opts.worktree);
|
|
1181
1469
|
if (!worktree) {
|
|
1182
|
-
throw new Error(`
|
|
1470
|
+
throw new Error(`Workspace ${opts.worktree} is not registered.`);
|
|
1183
1471
|
}
|
|
1184
1472
|
payload = {
|
|
1185
1473
|
sourceType: 'worktree',
|
|
@@ -1207,7 +1495,7 @@ queueCmd
|
|
|
1207
1495
|
submittedBy: opts.submittedBy || null,
|
|
1208
1496
|
};
|
|
1209
1497
|
} else {
|
|
1210
|
-
throw new Error('
|
|
1498
|
+
throw new Error('Choose one source to land: a branch name, `--worktree`, or `--pipeline`.');
|
|
1211
1499
|
}
|
|
1212
1500
|
|
|
1213
1501
|
const result = enqueueMergeItem(db, payload);
|
|
@@ -1223,7 +1511,7 @@ queueCmd
|
|
|
1223
1511
|
if (result.source_worktree) console.log(` ${chalk.dim('worktree:')} ${result.source_worktree}`);
|
|
1224
1512
|
} catch (err) {
|
|
1225
1513
|
db.close();
|
|
1226
|
-
|
|
1514
|
+
printErrorWithNext(err.message, 'switchman queue add --help');
|
|
1227
1515
|
process.exitCode = 1;
|
|
1228
1516
|
}
|
|
1229
1517
|
});
|
|
@@ -1266,6 +1554,19 @@ queueCmd
|
|
|
1266
1554
|
.command('status')
|
|
1267
1555
|
.description('Show an operator-friendly merge queue summary')
|
|
1268
1556
|
.option('--json', 'Output raw JSON')
|
|
1557
|
+
.addHelpText('after', `
|
|
1558
|
+
Plain English:
|
|
1559
|
+
Use this when finished branches are waiting to land and you want one safe queue view.
|
|
1560
|
+
|
|
1561
|
+
Examples:
|
|
1562
|
+
switchman queue status
|
|
1563
|
+
switchman queue status --json
|
|
1564
|
+
|
|
1565
|
+
What it helps you answer:
|
|
1566
|
+
- what lands next
|
|
1567
|
+
- what is blocked
|
|
1568
|
+
- what command should I run now
|
|
1569
|
+
`)
|
|
1269
1570
|
.action((opts) => {
|
|
1270
1571
|
const repoRoot = getRepo();
|
|
1271
1572
|
const db = getDb(repoRoot);
|
|
@@ -1281,18 +1582,77 @@ queueCmd
|
|
|
1281
1582
|
return;
|
|
1282
1583
|
}
|
|
1283
1584
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1585
|
+
const queueHealth = summary.counts.blocked > 0 ? 'block' : summary.counts.retrying > 0 ? 'warn' : 'healthy';
|
|
1586
|
+
const queueHealthColor = colorForHealth(queueHealth);
|
|
1587
|
+
const focus = summary.blocked[0] || summary.retrying[0] || summary.next || null;
|
|
1588
|
+
const focusLine = focus
|
|
1589
|
+
? `${focus.id} ${focus.source_type}:${focus.source_ref}${focus.last_error_summary ? ` ${chalk.dim(`• ${focus.last_error_summary}`)}` : ''}`
|
|
1590
|
+
: 'Nothing waiting. Landing queue is clear.';
|
|
1591
|
+
|
|
1592
|
+
console.log('');
|
|
1593
|
+
console.log(queueHealthColor('='.repeat(72)));
|
|
1594
|
+
console.log(`${queueHealthColor(healthLabel(queueHealth))} ${chalk.bold('switchman queue status')} ${chalk.dim('• landing mission control')}`);
|
|
1595
|
+
console.log(queueHealthColor('='.repeat(72)));
|
|
1596
|
+
console.log(renderSignalStrip([
|
|
1597
|
+
renderChip('queued', summary.counts.queued, summary.counts.queued > 0 ? chalk.yellow : chalk.green),
|
|
1598
|
+
renderChip('retrying', summary.counts.retrying, summary.counts.retrying > 0 ? chalk.yellow : chalk.green),
|
|
1599
|
+
renderChip('blocked', summary.counts.blocked, summary.counts.blocked > 0 ? chalk.red : chalk.green),
|
|
1600
|
+
renderChip('merging', summary.counts.merging, summary.counts.merging > 0 ? chalk.blue : chalk.green),
|
|
1601
|
+
renderChip('merged', summary.counts.merged, summary.counts.merged > 0 ? chalk.green : chalk.white),
|
|
1602
|
+
]));
|
|
1603
|
+
console.log(renderMetricRow([
|
|
1604
|
+
{ label: 'items', value: items.length, color: chalk.white },
|
|
1605
|
+
{ label: 'validating', value: summary.counts.validating, color: chalk.blue },
|
|
1606
|
+
{ label: 'rebasing', value: summary.counts.rebasing, color: chalk.blue },
|
|
1607
|
+
{ label: 'target', value: summary.next?.target_branch || 'main', color: chalk.cyan },
|
|
1608
|
+
]));
|
|
1609
|
+
console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
|
|
1610
|
+
|
|
1611
|
+
const queueFocusLines = summary.next
|
|
1612
|
+
? [
|
|
1613
|
+
`${renderChip('NEXT', summary.next.id, chalk.green)} ${summary.next.source_type}:${summary.next.source_ref} ${chalk.dim(`retries:${summary.next.retry_count}/${summary.next.max_retries}`)}`,
|
|
1614
|
+
` ${chalk.yellow('run:')} switchman queue run`,
|
|
1615
|
+
]
|
|
1616
|
+
: [chalk.dim('No queued landing work right now.')];
|
|
1617
|
+
|
|
1618
|
+
const queueBlockedLines = summary.blocked.length > 0
|
|
1619
|
+
? summary.blocked.slice(0, 4).flatMap((item) => {
|
|
1620
|
+
const lines = [`${renderChip('BLOCKED', item.id, chalk.red)} ${item.source_type}:${item.source_ref} ${chalk.dim(`retries:${item.retry_count}/${item.max_retries}`)}`];
|
|
1621
|
+
if (item.last_error_summary) lines.push(` ${chalk.red('why:')} ${item.last_error_summary}`);
|
|
1622
|
+
if (item.next_action) lines.push(` ${chalk.yellow('next:')} ${item.next_action}`);
|
|
1623
|
+
return lines;
|
|
1624
|
+
})
|
|
1625
|
+
: [chalk.green('Nothing blocked.')];
|
|
1626
|
+
|
|
1627
|
+
const queueWatchLines = items.filter((item) => ['retrying', 'merging', 'rebasing', 'validating'].includes(item.status)).length > 0
|
|
1628
|
+
? items
|
|
1629
|
+
.filter((item) => ['retrying', 'merging', 'rebasing', 'validating'].includes(item.status))
|
|
1630
|
+
.slice(0, 4)
|
|
1631
|
+
.flatMap((item) => {
|
|
1632
|
+
const lines = [`${renderChip(item.status.toUpperCase(), item.id, item.status === 'retrying' ? chalk.yellow : chalk.blue)} ${item.source_type}:${item.source_ref}`];
|
|
1633
|
+
if (item.last_error_summary) lines.push(` ${chalk.dim(item.last_error_summary)}`);
|
|
1634
|
+
return lines;
|
|
1635
|
+
})
|
|
1636
|
+
: [chalk.green('No in-flight queue items right now.')];
|
|
1637
|
+
|
|
1638
|
+
const queueCommandLines = [
|
|
1639
|
+
`${chalk.cyan('$')} switchman queue run`,
|
|
1640
|
+
`${chalk.cyan('$')} switchman queue status --json`,
|
|
1641
|
+
...(summary.blocked[0] ? [`${chalk.cyan('$')} switchman queue retry ${summary.blocked[0].id}`] : []),
|
|
1642
|
+
];
|
|
1643
|
+
|
|
1644
|
+
console.log('');
|
|
1645
|
+
for (const block of [
|
|
1646
|
+
renderPanel('Landing focus', queueFocusLines, chalk.green),
|
|
1647
|
+
renderPanel('Blocked', queueBlockedLines, summary.counts.blocked > 0 ? chalk.red : chalk.green),
|
|
1648
|
+
renderPanel('In flight', queueWatchLines, queueWatchLines[0] === 'No in-flight queue items right now.' ? chalk.green : chalk.blue),
|
|
1649
|
+
renderPanel('Next commands', queueCommandLines, chalk.cyan),
|
|
1650
|
+
]) {
|
|
1651
|
+
for (const line of block) console.log(line);
|
|
1652
|
+
console.log('');
|
|
1293
1653
|
}
|
|
1654
|
+
|
|
1294
1655
|
if (recentEvents.length > 0) {
|
|
1295
|
-
console.log('');
|
|
1296
1656
|
console.log(chalk.bold('Recent Queue Events:'));
|
|
1297
1657
|
for (const event of recentEvents) {
|
|
1298
1658
|
console.log(` ${chalk.cyan(event.queue_item_id)} ${chalk.dim(event.event_type)} ${chalk.dim(event.status || '')} ${chalk.dim(event.created_at)}`.trim());
|
|
@@ -1302,13 +1662,19 @@ queueCmd
|
|
|
1302
1662
|
|
|
1303
1663
|
queueCmd
|
|
1304
1664
|
.command('run')
|
|
1305
|
-
.description('Process
|
|
1665
|
+
.description('Process landing-queue items one at a time')
|
|
1306
1666
|
.option('--max-items <n>', 'Maximum queue items to process', '1')
|
|
1307
1667
|
.option('--target <branch>', 'Default target branch', 'main')
|
|
1308
1668
|
.option('--watch', 'Keep polling for new queue items')
|
|
1309
1669
|
.option('--watch-interval-ms <n>', 'Polling interval for --watch mode', '1000')
|
|
1310
1670
|
.option('--max-cycles <n>', 'Maximum watch cycles before exiting (mainly for tests)')
|
|
1311
1671
|
.option('--json', 'Output raw JSON')
|
|
1672
|
+
.addHelpText('after', `
|
|
1673
|
+
Examples:
|
|
1674
|
+
switchman queue run
|
|
1675
|
+
switchman queue run --watch
|
|
1676
|
+
switchman queue run --watch --watch-interval-ms 1000
|
|
1677
|
+
`)
|
|
1312
1678
|
.action(async (opts) => {
|
|
1313
1679
|
const repoRoot = getRepo();
|
|
1314
1680
|
|
|
@@ -1379,7 +1745,7 @@ queueCmd
|
|
|
1379
1745
|
db.close();
|
|
1380
1746
|
|
|
1381
1747
|
if (!item) {
|
|
1382
|
-
|
|
1748
|
+
printErrorWithNext(`Queue item ${itemId} is not retryable.`, 'switchman queue status');
|
|
1383
1749
|
process.exitCode = 1;
|
|
1384
1750
|
return;
|
|
1385
1751
|
}
|
|
@@ -1402,7 +1768,7 @@ queueCmd
|
|
|
1402
1768
|
db.close();
|
|
1403
1769
|
|
|
1404
1770
|
if (!item) {
|
|
1405
|
-
|
|
1771
|
+
printErrorWithNext(`Queue item ${itemId} does not exist.`, 'switchman queue status');
|
|
1406
1772
|
process.exitCode = 1;
|
|
1407
1773
|
return;
|
|
1408
1774
|
}
|
|
@@ -1413,6 +1779,12 @@ queueCmd
|
|
|
1413
1779
|
// ── pipeline ──────────────────────────────────────────────────────────────────
|
|
1414
1780
|
|
|
1415
1781
|
const pipelineCmd = program.command('pipeline').description('Create and summarize issue-to-PR execution pipelines');
|
|
1782
|
+
pipelineCmd.addHelpText('after', `
|
|
1783
|
+
Examples:
|
|
1784
|
+
switchman pipeline start "Harden auth API permissions"
|
|
1785
|
+
switchman pipeline exec pipe-123 "/path/to/agent-command"
|
|
1786
|
+
switchman pipeline status pipe-123
|
|
1787
|
+
`);
|
|
1416
1788
|
|
|
1417
1789
|
pipelineCmd
|
|
1418
1790
|
.command('start <title>')
|
|
@@ -1450,6 +1822,14 @@ pipelineCmd
|
|
|
1450
1822
|
.command('status <pipelineId>')
|
|
1451
1823
|
.description('Show task status for a pipeline')
|
|
1452
1824
|
.option('--json', 'Output raw JSON')
|
|
1825
|
+
.addHelpText('after', `
|
|
1826
|
+
Plain English:
|
|
1827
|
+
Use this when one goal has been split into several tasks and you want to see what is running, stuck, or next.
|
|
1828
|
+
|
|
1829
|
+
Examples:
|
|
1830
|
+
switchman pipeline status pipe-123
|
|
1831
|
+
switchman pipeline status pipe-123 --json
|
|
1832
|
+
`)
|
|
1453
1833
|
.action((pipelineId, opts) => {
|
|
1454
1834
|
const repoRoot = getRepo();
|
|
1455
1835
|
const db = getDb(repoRoot);
|
|
@@ -1463,20 +1843,74 @@ pipelineCmd
|
|
|
1463
1843
|
return;
|
|
1464
1844
|
}
|
|
1465
1845
|
|
|
1846
|
+
const pipelineHealth = result.status === 'blocked'
|
|
1847
|
+
? 'block'
|
|
1848
|
+
: result.counts.failed > 0
|
|
1849
|
+
? 'warn'
|
|
1850
|
+
: result.counts.in_progress > 0
|
|
1851
|
+
? 'warn'
|
|
1852
|
+
: 'healthy';
|
|
1853
|
+
const pipelineHealthColor = colorForHealth(pipelineHealth);
|
|
1854
|
+
const failedTask = result.tasks.find((task) => task.status === 'failed');
|
|
1855
|
+
const runningTask = result.tasks.find((task) => task.status === 'in_progress');
|
|
1856
|
+
const nextPendingTask = result.tasks.find((task) => task.status === 'pending');
|
|
1857
|
+
const focusTask = failedTask || runningTask || nextPendingTask || result.tasks[0] || null;
|
|
1858
|
+
const focusLine = focusTask
|
|
1859
|
+
? `${focusTask.title} ${chalk.dim(focusTask.id)}`
|
|
1860
|
+
: 'No pipeline tasks found.';
|
|
1861
|
+
|
|
1862
|
+
console.log('');
|
|
1863
|
+
console.log(pipelineHealthColor('='.repeat(72)));
|
|
1864
|
+
console.log(`${pipelineHealthColor(healthLabel(pipelineHealth))} ${chalk.bold('switchman pipeline status')} ${chalk.dim('• pipeline mission control')}`);
|
|
1466
1865
|
console.log(`${chalk.bold(result.title)} ${chalk.dim(result.pipeline_id)}`);
|
|
1467
|
-
console.log(
|
|
1468
|
-
|
|
1866
|
+
console.log(pipelineHealthColor('='.repeat(72)));
|
|
1867
|
+
console.log(renderSignalStrip([
|
|
1868
|
+
renderChip('done', result.counts.done, result.counts.done > 0 ? chalk.green : chalk.white),
|
|
1869
|
+
renderChip('running', result.counts.in_progress, result.counts.in_progress > 0 ? chalk.blue : chalk.green),
|
|
1870
|
+
renderChip('pending', result.counts.pending, result.counts.pending > 0 ? chalk.yellow : chalk.green),
|
|
1871
|
+
renderChip('failed', result.counts.failed, result.counts.failed > 0 ? chalk.red : chalk.green),
|
|
1872
|
+
]));
|
|
1873
|
+
console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
|
|
1874
|
+
|
|
1875
|
+
const runningLines = result.tasks.filter((task) => task.status === 'in_progress').slice(0, 4).map((task) => {
|
|
1469
1876
|
const worktree = task.worktree || task.suggested_worktree || 'unassigned';
|
|
1470
1877
|
const blocked = task.blocked_by?.length ? ` ${chalk.dim(`blocked by ${task.blocked_by.join(', ')}`)}` : '';
|
|
1471
1878
|
const type = task.task_spec?.task_type ? ` ${chalk.dim(`[${task.task_spec.task_type}]`)}` : '';
|
|
1472
|
-
|
|
1879
|
+
return `${chalk.cyan(worktree)} -> ${task.title}${type} ${chalk.dim(task.id)}${blocked}`;
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
const blockedLines = result.tasks.filter((task) => task.status === 'failed').slice(0, 4).flatMap((task) => {
|
|
1883
|
+
const type = task.task_spec?.task_type ? ` ${chalk.dim(`[${task.task_spec.task_type}]`)}` : '';
|
|
1884
|
+
const lines = [`${renderChip('BLOCKED', task.id, chalk.red)} ${task.title}${type}`];
|
|
1473
1885
|
if (task.failure?.summary) {
|
|
1474
1886
|
const reasonLabel = humanizeReasonCode(task.failure.reason_code);
|
|
1475
|
-
|
|
1476
|
-
}
|
|
1477
|
-
if (task.next_action) {
|
|
1478
|
-
console.log(` ${chalk.yellow('next:')} ${task.next_action}`);
|
|
1887
|
+
lines.push(` ${chalk.red('why:')} ${task.failure.summary} ${chalk.dim(`(${reasonLabel})`)}`);
|
|
1479
1888
|
}
|
|
1889
|
+
if (task.next_action) lines.push(` ${chalk.yellow('next:')} ${task.next_action}`);
|
|
1890
|
+
return lines;
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
const nextLines = result.tasks.filter((task) => task.status === 'pending').slice(0, 4).map((task) => {
|
|
1894
|
+
const worktree = task.suggested_worktree || task.worktree || 'unassigned';
|
|
1895
|
+
const blocked = task.blocked_by?.length ? ` ${chalk.dim(`blocked by ${task.blocked_by.join(', ')}`)}` : '';
|
|
1896
|
+
return `${renderChip('NEXT', task.id, chalk.green)} ${task.title} ${chalk.dim(worktree)}${blocked}`;
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
const commandLines = [
|
|
1900
|
+
`${chalk.cyan('$')} switchman pipeline exec ${result.pipeline_id} "/path/to/agent-command"`,
|
|
1901
|
+
`${chalk.cyan('$')} switchman pipeline pr ${result.pipeline_id}`,
|
|
1902
|
+
...(result.counts.failed > 0 ? [`${chalk.cyan('$')} switchman pipeline status ${result.pipeline_id}`] : []),
|
|
1903
|
+
];
|
|
1904
|
+
|
|
1905
|
+
console.log('');
|
|
1906
|
+
for (const block of [
|
|
1907
|
+
renderPanel('Running now', runningLines.length > 0 ? runningLines : [chalk.dim('No tasks are actively running.')], runningLines.length > 0 ? chalk.cyan : chalk.green),
|
|
1908
|
+
renderPanel('Blocked', blockedLines.length > 0 ? blockedLines : [chalk.green('Nothing blocked.')], blockedLines.length > 0 ? chalk.red : chalk.green),
|
|
1909
|
+
renderPanel('Next up', nextLines.length > 0 ? nextLines : [chalk.dim('No pending tasks left.')], chalk.green),
|
|
1910
|
+
renderPanel('Next commands', commandLines, chalk.cyan),
|
|
1911
|
+
]) {
|
|
1912
|
+
for (const line of block) console.log(line);
|
|
1913
|
+
console.log('');
|
|
1480
1914
|
}
|
|
1481
1915
|
} catch (err) {
|
|
1482
1916
|
db.close();
|
|
@@ -1666,6 +2100,14 @@ pipelineCmd
|
|
|
1666
2100
|
.option('--retry-backoff-ms <ms>', 'Base backoff in milliseconds between retry attempts', '0')
|
|
1667
2101
|
.option('--timeout-ms <ms>', 'Default command timeout in milliseconds when a task spec does not provide one', '0')
|
|
1668
2102
|
.option('--json', 'Output raw JSON')
|
|
2103
|
+
.addHelpText('after', `
|
|
2104
|
+
Plain English:
|
|
2105
|
+
pipeline = one goal, broken into smaller safe tasks
|
|
2106
|
+
|
|
2107
|
+
Examples:
|
|
2108
|
+
switchman pipeline exec pipe-123 "/path/to/agent-command"
|
|
2109
|
+
switchman pipeline exec pipe-123 "npm test"
|
|
2110
|
+
`)
|
|
1669
2111
|
.action(async (pipelineId, agentCommand, opts) => {
|
|
1670
2112
|
const repoRoot = getRepo();
|
|
1671
2113
|
const db = getDb(repoRoot);
|
|
@@ -1699,20 +2141,34 @@ pipelineCmd
|
|
|
1699
2141
|
console.log(chalk.dim(result.pr.markdown.split('\n')[0]));
|
|
1700
2142
|
} catch (err) {
|
|
1701
2143
|
db.close();
|
|
1702
|
-
|
|
2144
|
+
printErrorWithNext(err.message, `switchman pipeline status ${pipelineId}`);
|
|
1703
2145
|
process.exitCode = 1;
|
|
1704
2146
|
}
|
|
1705
2147
|
});
|
|
1706
2148
|
|
|
1707
2149
|
// ── lease ────────────────────────────────────────────────────────────────────
|
|
1708
2150
|
|
|
1709
|
-
const leaseCmd = program.command('lease').description('Manage active work
|
|
2151
|
+
const leaseCmd = program.command('lease').alias('session').description('Manage active work sessions and keep long-running tasks alive');
|
|
2152
|
+
leaseCmd.addHelpText('after', `
|
|
2153
|
+
Plain English:
|
|
2154
|
+
lease = a task currently checked out by an agent
|
|
2155
|
+
|
|
2156
|
+
Examples:
|
|
2157
|
+
switchman lease next --json
|
|
2158
|
+
switchman lease heartbeat lease-123
|
|
2159
|
+
switchman lease reap
|
|
2160
|
+
`);
|
|
1710
2161
|
|
|
1711
2162
|
leaseCmd
|
|
1712
2163
|
.command('acquire <taskId> <worktree>')
|
|
1713
|
-
.description('
|
|
2164
|
+
.description('Start a tracked work session for a specific pending task')
|
|
1714
2165
|
.option('--agent <name>', 'Agent identifier for logging')
|
|
1715
2166
|
.option('--json', 'Output as JSON')
|
|
2167
|
+
.addHelpText('after', `
|
|
2168
|
+
Examples:
|
|
2169
|
+
switchman lease acquire task-123 agent2
|
|
2170
|
+
switchman lease acquire task-123 agent2 --agent cursor
|
|
2171
|
+
`)
|
|
1716
2172
|
.action((taskId, worktree, opts) => {
|
|
1717
2173
|
const repoRoot = getRepo();
|
|
1718
2174
|
const db = getDb(repoRoot);
|
|
@@ -1722,7 +2178,7 @@ leaseCmd
|
|
|
1722
2178
|
|
|
1723
2179
|
if (!lease || !task) {
|
|
1724
2180
|
if (opts.json) console.log(JSON.stringify({ lease: null, task: null }));
|
|
1725
|
-
else
|
|
2181
|
+
else printErrorWithNext('Could not start a work session. The task may not exist or may already be in progress.', 'switchman task list --status pending');
|
|
1726
2182
|
process.exitCode = 1;
|
|
1727
2183
|
return;
|
|
1728
2184
|
}
|
|
@@ -1742,10 +2198,16 @@ leaseCmd
|
|
|
1742
2198
|
|
|
1743
2199
|
leaseCmd
|
|
1744
2200
|
.command('next')
|
|
1745
|
-
.description('
|
|
2201
|
+
.description('Start the next pending task and open a tracked work session for it')
|
|
1746
2202
|
.option('--json', 'Output as JSON')
|
|
1747
|
-
.option('--worktree <name>', '
|
|
2203
|
+
.option('--worktree <name>', 'Workspace to assign the task to (defaults to the current folder name)')
|
|
1748
2204
|
.option('--agent <name>', 'Agent identifier for logging')
|
|
2205
|
+
.addHelpText('after', `
|
|
2206
|
+
Examples:
|
|
2207
|
+
switchman lease next
|
|
2208
|
+
switchman lease next --json
|
|
2209
|
+
switchman lease next --worktree agent2 --agent cursor
|
|
2210
|
+
`)
|
|
1749
2211
|
.action((opts) => {
|
|
1750
2212
|
const repoRoot = getRepo();
|
|
1751
2213
|
const worktreeName = getCurrentWorktreeName(opts.worktree);
|
|
@@ -1753,7 +2215,7 @@ leaseCmd
|
|
|
1753
2215
|
|
|
1754
2216
|
if (!task) {
|
|
1755
2217
|
if (opts.json) console.log(JSON.stringify({ task: null, lease: null }));
|
|
1756
|
-
else if (exhausted) console.log(chalk.dim('No pending tasks.'));
|
|
2218
|
+
else if (exhausted) console.log(chalk.dim('No pending tasks. Add one with `switchman task add "Your task"`.'));
|
|
1757
2219
|
else console.log(chalk.yellow('Tasks were claimed by other agents during assignment. Run again to get the next one.'));
|
|
1758
2220
|
return;
|
|
1759
2221
|
}
|
|
@@ -1816,7 +2278,7 @@ leaseCmd
|
|
|
1816
2278
|
|
|
1817
2279
|
if (!lease) {
|
|
1818
2280
|
if (opts.json) console.log(JSON.stringify({ lease: null }));
|
|
1819
|
-
else
|
|
2281
|
+
else printErrorWithNext(`No active work session found for ${leaseId}.`, 'switchman lease list --status active');
|
|
1820
2282
|
process.exitCode = 1;
|
|
1821
2283
|
return;
|
|
1822
2284
|
}
|
|
@@ -1832,9 +2294,14 @@ leaseCmd
|
|
|
1832
2294
|
|
|
1833
2295
|
leaseCmd
|
|
1834
2296
|
.command('reap')
|
|
1835
|
-
.description('
|
|
2297
|
+
.description('Clean up abandoned work sessions and release their file locks')
|
|
1836
2298
|
.option('--stale-after-minutes <minutes>', 'Age threshold for staleness')
|
|
1837
2299
|
.option('--json', 'Output as JSON')
|
|
2300
|
+
.addHelpText('after', `
|
|
2301
|
+
Examples:
|
|
2302
|
+
switchman lease reap
|
|
2303
|
+
switchman lease reap --stale-after-minutes 20
|
|
2304
|
+
`)
|
|
1838
2305
|
.action((opts) => {
|
|
1839
2306
|
const repoRoot = getRepo();
|
|
1840
2307
|
const db = getDb(repoRoot);
|
|
@@ -1919,11 +2386,20 @@ leasePolicyCmd
|
|
|
1919
2386
|
|
|
1920
2387
|
// ── worktree ───────────────────────────────────────────────────────────────────
|
|
1921
2388
|
|
|
1922
|
-
const wtCmd = program.command('worktree').description('Manage worktrees');
|
|
2389
|
+
const wtCmd = program.command('worktree').alias('workspace').description('Manage registered workspaces (Git worktrees)');
|
|
2390
|
+
wtCmd.addHelpText('after', `
|
|
2391
|
+
Plain English:
|
|
2392
|
+
worktree = the Git feature behind each agent workspace
|
|
2393
|
+
|
|
2394
|
+
Examples:
|
|
2395
|
+
switchman worktree list
|
|
2396
|
+
switchman workspace list
|
|
2397
|
+
switchman worktree sync
|
|
2398
|
+
`);
|
|
1923
2399
|
|
|
1924
2400
|
wtCmd
|
|
1925
2401
|
.command('add <name> <path> <branch>')
|
|
1926
|
-
.description('Register a
|
|
2402
|
+
.description('Register a workspace with Switchman')
|
|
1927
2403
|
.option('--agent <name>', 'Agent assigned to this worktree')
|
|
1928
2404
|
.action((name, path, branch, opts) => {
|
|
1929
2405
|
const repoRoot = getRepo();
|
|
@@ -1935,7 +2411,7 @@ wtCmd
|
|
|
1935
2411
|
|
|
1936
2412
|
wtCmd
|
|
1937
2413
|
.command('list')
|
|
1938
|
-
.description('List all registered
|
|
2414
|
+
.description('List all registered workspaces')
|
|
1939
2415
|
.action(() => {
|
|
1940
2416
|
const repoRoot = getRepo();
|
|
1941
2417
|
const db = getDb(repoRoot);
|
|
@@ -1944,7 +2420,7 @@ wtCmd
|
|
|
1944
2420
|
db.close();
|
|
1945
2421
|
|
|
1946
2422
|
if (!worktrees.length && !gitWorktrees.length) {
|
|
1947
|
-
console.log(chalk.dim('No
|
|
2423
|
+
console.log(chalk.dim('No workspaces found. Run `switchman setup --agents 3` or `switchman worktree sync`.'));
|
|
1948
2424
|
return;
|
|
1949
2425
|
}
|
|
1950
2426
|
|
|
@@ -1964,7 +2440,7 @@ wtCmd
|
|
|
1964
2440
|
|
|
1965
2441
|
wtCmd
|
|
1966
2442
|
.command('sync')
|
|
1967
|
-
.description('Sync
|
|
2443
|
+
.description('Sync Git workspaces into the Switchman database')
|
|
1968
2444
|
.action(() => {
|
|
1969
2445
|
const repoRoot = getRepo();
|
|
1970
2446
|
const db = getDb(repoRoot);
|
|
@@ -1981,12 +2457,20 @@ wtCmd
|
|
|
1981
2457
|
|
|
1982
2458
|
program
|
|
1983
2459
|
.command('claim <taskId> <worktree> [files...]')
|
|
1984
|
-
.description('
|
|
2460
|
+
.description('Lock files for a task before editing')
|
|
1985
2461
|
.option('--agent <name>', 'Agent name')
|
|
1986
2462
|
.option('--force', 'Claim even if conflicts exist')
|
|
2463
|
+
.addHelpText('after', `
|
|
2464
|
+
Examples:
|
|
2465
|
+
switchman claim task-123 agent2 src/auth.js src/server.js
|
|
2466
|
+
switchman claim task-123 agent2 src/auth.js --agent cursor
|
|
2467
|
+
|
|
2468
|
+
Use this before editing files in a shared repo.
|
|
2469
|
+
`)
|
|
1987
2470
|
.action((taskId, worktree, files, opts) => {
|
|
1988
2471
|
if (!files.length) {
|
|
1989
|
-
console.log(chalk.yellow('No files specified.
|
|
2472
|
+
console.log(chalk.yellow('No files specified.'));
|
|
2473
|
+
console.log(`${chalk.yellow('next:')} switchman claim <taskId> <workspace> file1 file2`);
|
|
1990
2474
|
return;
|
|
1991
2475
|
}
|
|
1992
2476
|
const repoRoot = getRepo();
|
|
@@ -2000,7 +2484,8 @@ program
|
|
|
2000
2484
|
for (const c of conflicts) {
|
|
2001
2485
|
console.log(` ${chalk.yellow(c.file)} → already claimed by worktree ${chalk.cyan(c.claimedBy.worktree)} (task: ${c.claimedBy.task_title})`);
|
|
2002
2486
|
}
|
|
2003
|
-
console.log(chalk.dim('\nUse --force to claim anyway, or
|
|
2487
|
+
console.log(chalk.dim('\nUse --force to claim anyway, or pick different files first.'));
|
|
2488
|
+
console.log(`${chalk.yellow('next:')} switchman status`);
|
|
2004
2489
|
process.exitCode = 1;
|
|
2005
2490
|
return;
|
|
2006
2491
|
}
|
|
@@ -2009,7 +2494,7 @@ program
|
|
|
2009
2494
|
console.log(`${chalk.green('✓')} Claimed ${files.length} file(s) for task ${chalk.cyan(taskId)} (${chalk.dim(lease.id)})`);
|
|
2010
2495
|
files.forEach(f => console.log(` ${chalk.dim(f)}`));
|
|
2011
2496
|
} catch (err) {
|
|
2012
|
-
|
|
2497
|
+
printErrorWithNext(err.message, 'switchman task list --status in_progress');
|
|
2013
2498
|
process.exitCode = 1;
|
|
2014
2499
|
} finally {
|
|
2015
2500
|
db.close();
|
|
@@ -2181,6 +2666,12 @@ program
|
|
|
2181
2666
|
.description('Scan all workspaces for conflicts')
|
|
2182
2667
|
.option('--json', 'Output raw JSON')
|
|
2183
2668
|
.option('--quiet', 'Only show conflicts')
|
|
2669
|
+
.addHelpText('after', `
|
|
2670
|
+
Examples:
|
|
2671
|
+
switchman scan
|
|
2672
|
+
switchman scan --quiet
|
|
2673
|
+
switchman scan --json
|
|
2674
|
+
`)
|
|
2184
2675
|
.action(async (opts) => {
|
|
2185
2676
|
const repoRoot = getRepo();
|
|
2186
2677
|
const db = getDb(repoRoot);
|
|
@@ -2296,12 +2787,21 @@ program
|
|
|
2296
2787
|
.option('--watch', 'Keep refreshing status in the terminal')
|
|
2297
2788
|
.option('--watch-interval-ms <n>', 'Polling interval for --watch mode', '2000')
|
|
2298
2789
|
.option('--max-cycles <n>', 'Maximum refresh cycles before exiting', '0')
|
|
2790
|
+
.addHelpText('after', `
|
|
2791
|
+
Examples:
|
|
2792
|
+
switchman status
|
|
2793
|
+
switchman status --watch
|
|
2794
|
+
switchman status --json
|
|
2795
|
+
|
|
2796
|
+
Use this first when the repo feels stuck.
|
|
2797
|
+
`)
|
|
2299
2798
|
.action(async (opts) => {
|
|
2300
2799
|
const repoRoot = getRepo();
|
|
2301
2800
|
const watch = Boolean(opts.watch);
|
|
2302
2801
|
const watchIntervalMs = Math.max(100, Number.parseInt(opts.watchIntervalMs, 10) || 2000);
|
|
2303
2802
|
const maxCycles = Math.max(0, Number.parseInt(opts.maxCycles, 10) || 0);
|
|
2304
2803
|
let cycles = 0;
|
|
2804
|
+
let lastSignature = null;
|
|
2305
2805
|
|
|
2306
2806
|
while (true) {
|
|
2307
2807
|
if (watch && process.stdout.isTTY && !opts.json) {
|
|
@@ -2316,8 +2816,16 @@ program
|
|
|
2316
2816
|
} else {
|
|
2317
2817
|
renderUnifiedStatusReport(report);
|
|
2318
2818
|
if (watch) {
|
|
2819
|
+
const signature = buildWatchSignature(report);
|
|
2820
|
+
const watchState = lastSignature === null
|
|
2821
|
+
? chalk.cyan('baseline snapshot')
|
|
2822
|
+
: signature === lastSignature
|
|
2823
|
+
? chalk.dim('no repo changes since last refresh')
|
|
2824
|
+
: chalk.green('change detected');
|
|
2825
|
+
const updatedAt = formatClockTime(report.generated_at);
|
|
2826
|
+
lastSignature = signature;
|
|
2319
2827
|
console.log('');
|
|
2320
|
-
console.log(chalk.dim(`
|
|
2828
|
+
console.log(chalk.dim(`Live watch • updated ${updatedAt || 'just now'} • ${watchState}${maxCycles > 0 ? ` • cycle ${cycles}/${maxCycles}` : ''} • refresh ${watchIntervalMs}ms`));
|
|
2321
2829
|
}
|
|
2322
2830
|
}
|
|
2323
2831
|
|
|
@@ -2332,6 +2840,14 @@ program
|
|
|
2332
2840
|
.command('doctor')
|
|
2333
2841
|
.description('Show one operator-focused health view: what is running, what is blocked, and what to do next')
|
|
2334
2842
|
.option('--json', 'Output raw JSON')
|
|
2843
|
+
.addHelpText('after', `
|
|
2844
|
+
Plain English:
|
|
2845
|
+
Use this when the repo feels risky, noisy, or stuck and you want the health summary plus exact next moves.
|
|
2846
|
+
|
|
2847
|
+
Examples:
|
|
2848
|
+
switchman doctor
|
|
2849
|
+
switchman doctor --json
|
|
2850
|
+
`)
|
|
2335
2851
|
.action(async (opts) => {
|
|
2336
2852
|
const repoRoot = getRepo();
|
|
2337
2853
|
const db = getDb(repoRoot);
|
|
@@ -2356,66 +2872,83 @@ program
|
|
|
2356
2872
|
return;
|
|
2357
2873
|
}
|
|
2358
2874
|
|
|
2359
|
-
const
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
console.log('');
|
|
2875
|
+
const doctorColor = colorForHealth(report.health);
|
|
2876
|
+
const blockedCount = report.attention.filter((item) => item.severity === 'block').length;
|
|
2877
|
+
const warningCount = report.attention.filter((item) => item.severity !== 'block').length;
|
|
2878
|
+
const focusItem = report.attention[0] || report.active_work[0] || null;
|
|
2879
|
+
const focusLine = focusItem
|
|
2880
|
+
? `${focusItem.title || focusItem.task_title}${focusItem.detail ? ` ${chalk.dim(`• ${focusItem.detail}`)}` : ''}`
|
|
2881
|
+
: 'Nothing urgent. Repo health looks steady.';
|
|
2367
2882
|
|
|
2368
|
-
console.log(
|
|
2369
|
-
console.log(
|
|
2370
|
-
console.log(
|
|
2371
|
-
console.log(
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2883
|
+
console.log('');
|
|
2884
|
+
console.log(doctorColor('='.repeat(72)));
|
|
2885
|
+
console.log(`${doctorColor(healthLabel(report.health))} ${chalk.bold('switchman doctor')} ${chalk.dim('• repo health mission control')}`);
|
|
2886
|
+
console.log(chalk.dim(repoRoot));
|
|
2887
|
+
console.log(chalk.dim(report.summary));
|
|
2888
|
+
console.log(doctorColor('='.repeat(72)));
|
|
2889
|
+
console.log(renderSignalStrip([
|
|
2890
|
+
renderChip('blocked', blockedCount, blockedCount > 0 ? chalk.red : chalk.green),
|
|
2891
|
+
renderChip('watch', warningCount, warningCount > 0 ? chalk.yellow : chalk.green),
|
|
2892
|
+
renderChip('leases', report.counts.active_leases, report.counts.active_leases > 0 ? chalk.blue : chalk.green),
|
|
2893
|
+
renderChip('stale', report.counts.stale_leases, report.counts.stale_leases > 0 ? chalk.red : chalk.green),
|
|
2894
|
+
renderChip('merge', report.merge_readiness.ci_gate_ok ? 'clear' : 'hold', report.merge_readiness.ci_gate_ok ? chalk.green : chalk.red),
|
|
2895
|
+
]));
|
|
2896
|
+
console.log(renderMetricRow([
|
|
2897
|
+
{ label: 'tasks', value: `${report.counts.pending}/${report.counts.in_progress}/${report.counts.done}/${report.counts.failed}`, color: chalk.white },
|
|
2898
|
+
{ label: 'AI gate', value: report.merge_readiness.ai_gate_status, color: report.merge_readiness.ai_gate_status === 'blocked' ? chalk.red : report.merge_readiness.ai_gate_status === 'warn' ? chalk.yellow : chalk.green },
|
|
2899
|
+
]));
|
|
2900
|
+
console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
|
|
2901
|
+
|
|
2902
|
+
const runningLines = report.active_work.length > 0
|
|
2903
|
+
? report.active_work.slice(0, 5).map((item) => {
|
|
2377
2904
|
const leaseId = activeLeases.find((lease) => lease.task_id === item.task_id && lease.worktree === item.worktree)?.id || null;
|
|
2378
2905
|
const boundary = item.boundary_validation
|
|
2379
|
-
? ` ${
|
|
2906
|
+
? ` ${renderChip('validation', item.boundary_validation.status, item.boundary_validation.status === 'accepted' ? chalk.green : chalk.yellow)}`
|
|
2380
2907
|
: '';
|
|
2381
2908
|
const stale = (item.dependency_invalidations?.length || 0) > 0
|
|
2382
|
-
? ` ${
|
|
2909
|
+
? ` ${renderChip('stale', item.dependency_invalidations.length, chalk.yellow)}`
|
|
2383
2910
|
: '';
|
|
2384
|
-
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2911
|
+
return `${chalk.cyan(item.worktree)} -> ${item.task_title} ${chalk.dim(item.task_id)}${leaseId ? ` ${chalk.dim(`lease:${leaseId}`)}` : ''}${item.scope_summary ? ` ${chalk.dim(item.scope_summary)}` : ''}${boundary}${stale}`;
|
|
2912
|
+
})
|
|
2913
|
+
: [chalk.dim('Nothing active right now.')];
|
|
2914
|
+
|
|
2915
|
+
const attentionLines = report.attention.length > 0
|
|
2916
|
+
? report.attention.slice(0, 6).flatMap((item) => {
|
|
2917
|
+
const lines = [`${item.severity === 'block' ? renderChip('BLOCKED', item.kind || 'item', chalk.red) : renderChip('WATCH', item.kind || 'item', chalk.yellow)} ${item.title}`];
|
|
2918
|
+
if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
|
|
2919
|
+
lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
|
|
2920
|
+
if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
|
|
2921
|
+
return lines;
|
|
2922
|
+
})
|
|
2923
|
+
: [chalk.green('Nothing urgent.')];
|
|
2924
|
+
|
|
2925
|
+
const nextStepLines = [
|
|
2926
|
+
...report.next_steps.slice(0, 4).map((step) => `- ${step}`),
|
|
2927
|
+
'',
|
|
2928
|
+
...report.suggested_commands.slice(0, 4).map((command) => `${chalk.cyan('$')} ${command}`),
|
|
2929
|
+
];
|
|
2387
2930
|
|
|
2388
2931
|
console.log('');
|
|
2389
2932
|
console.log(chalk.bold('Attention now:'));
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
if (item.detail) console.log(` ${chalk.dim(item.detail)}`);
|
|
2397
|
-
console.log(` ${chalk.yellow('next:')} ${item.next_step}`);
|
|
2398
|
-
if (item.command) console.log(` ${chalk.cyan('run:')} ${item.command}`);
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
console.log('');
|
|
2403
|
-
console.log(chalk.bold('Recommended next steps:'));
|
|
2404
|
-
for (const step of report.next_steps) {
|
|
2405
|
-
console.log(` - ${step}`);
|
|
2406
|
-
}
|
|
2407
|
-
if (report.suggested_commands.length > 0) {
|
|
2933
|
+
for (const block of [
|
|
2934
|
+
renderPanel('Running now', runningLines, chalk.cyan),
|
|
2935
|
+
renderPanel('Attention now', attentionLines, report.attention.some((item) => item.severity === 'block') ? chalk.red : report.attention.length > 0 ? chalk.yellow : chalk.green),
|
|
2936
|
+
renderPanel('Recommended next steps', nextStepLines, chalk.green),
|
|
2937
|
+
]) {
|
|
2938
|
+
for (const line of block) console.log(line);
|
|
2408
2939
|
console.log('');
|
|
2409
|
-
console.log(chalk.bold('Suggested commands:'));
|
|
2410
|
-
for (const command of report.suggested_commands) {
|
|
2411
|
-
console.log(` ${chalk.cyan(command)}`);
|
|
2412
|
-
}
|
|
2413
2940
|
}
|
|
2414
2941
|
});
|
|
2415
2942
|
|
|
2416
2943
|
// ── gate ─────────────────────────────────────────────────────────────────────
|
|
2417
2944
|
|
|
2418
2945
|
const gateCmd = program.command('gate').description('Safety checks for edits, merges, and CI');
|
|
2946
|
+
gateCmd.addHelpText('after', `
|
|
2947
|
+
Examples:
|
|
2948
|
+
switchman gate ci
|
|
2949
|
+
switchman gate ai
|
|
2950
|
+
switchman gate install-ci
|
|
2951
|
+
`);
|
|
2419
2952
|
|
|
2420
2953
|
const auditCmd = program.command('audit').description('Inspect and verify the tamper-evident audit trail');
|
|
2421
2954
|
|