switchman-dev 0.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/CLAUDE.md +98 -0
- package/README.md +243 -0
- package/examples/README.md +117 -0
- package/examples/setup.sh +102 -0
- package/examples/taskapi/.switchman/switchman.db +0 -0
- package/examples/taskapi/package-lock.json +4736 -0
- package/examples/taskapi/package.json +18 -0
- package/examples/taskapi/src/db.js +179 -0
- package/examples/taskapi/src/middleware/auth.js +96 -0
- package/examples/taskapi/src/middleware/validate.js +133 -0
- package/examples/taskapi/src/routes/tasks.js +65 -0
- package/examples/taskapi/src/routes/users.js +38 -0
- package/examples/taskapi/src/server.js +7 -0
- package/examples/taskapi/tests/api.test.js +112 -0
- package/examples/teardown.sh +37 -0
- package/examples/walkthrough.sh +172 -0
- package/examples/worktrees/agent-rate-limiting/package-lock.json +4736 -0
- package/examples/worktrees/agent-rate-limiting/package.json +18 -0
- package/examples/worktrees/agent-rate-limiting/src/db.js +179 -0
- package/examples/worktrees/agent-rate-limiting/src/middleware/auth.js +96 -0
- package/examples/worktrees/agent-rate-limiting/src/middleware/validate.js +133 -0
- package/examples/worktrees/agent-rate-limiting/src/routes/tasks.js +65 -0
- package/examples/worktrees/agent-rate-limiting/src/routes/users.js +38 -0
- package/examples/worktrees/agent-rate-limiting/src/server.js +7 -0
- package/examples/worktrees/agent-rate-limiting/tests/api.test.js +112 -0
- package/examples/worktrees/agent-tests/package-lock.json +4736 -0
- package/examples/worktrees/agent-tests/package.json +18 -0
- package/examples/worktrees/agent-tests/src/db.js +179 -0
- package/examples/worktrees/agent-tests/src/middleware/auth.js +96 -0
- package/examples/worktrees/agent-tests/src/middleware/validate.js +133 -0
- package/examples/worktrees/agent-tests/src/routes/tasks.js +65 -0
- package/examples/worktrees/agent-tests/src/routes/users.js +38 -0
- package/examples/worktrees/agent-tests/src/server.js +7 -0
- package/examples/worktrees/agent-tests/tests/api.test.js +112 -0
- package/examples/worktrees/agent-validation/package-lock.json +4736 -0
- package/examples/worktrees/agent-validation/package.json +18 -0
- package/examples/worktrees/agent-validation/src/db.js +179 -0
- package/examples/worktrees/agent-validation/src/middleware/auth.js +96 -0
- package/examples/worktrees/agent-validation/src/middleware/validate.js +133 -0
- package/examples/worktrees/agent-validation/src/routes/tasks.js +65 -0
- package/examples/worktrees/agent-validation/src/routes/users.js +38 -0
- package/examples/worktrees/agent-validation/src/server.js +7 -0
- package/examples/worktrees/agent-validation/tests/api.test.js +112 -0
- package/package.json +29 -0
- package/src/cli/index.js +602 -0
- package/src/core/db.js +240 -0
- package/src/core/detector.js +172 -0
- package/src/core/git.js +265 -0
- package/src/mcp/server.js +555 -0
- package/tests/test.js +259 -0
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* switchman CLI
|
|
4
|
+
* Conflict-aware task coordinator for parallel AI coding agents
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* switchman init - Initialize in current repo
|
|
8
|
+
* switchman task add - Add a task to the queue
|
|
9
|
+
* switchman task list - List all tasks
|
|
10
|
+
* switchman task assign - Assign task to a worktree
|
|
11
|
+
* switchman task done - Mark task complete
|
|
12
|
+
* switchman worktree add - Register a worktree
|
|
13
|
+
* switchman worktree list - List registered worktrees
|
|
14
|
+
* switchman scan - Scan for conflicts across worktrees
|
|
15
|
+
* switchman claim - Claim files for a task
|
|
16
|
+
* switchman status - Show full system status
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { program } from 'commander';
|
|
20
|
+
import chalk from 'chalk';
|
|
21
|
+
import ora from 'ora';
|
|
22
|
+
import { readFileSync } from 'fs';
|
|
23
|
+
import { join, dirname } from 'path';
|
|
24
|
+
import { fileURLToPath } from 'url';
|
|
25
|
+
import { execSync } from 'child_process';
|
|
26
|
+
|
|
27
|
+
import { findRepoRoot, listGitWorktrees, createGitWorktree } from '../core/git.js';
|
|
28
|
+
import {
|
|
29
|
+
initDb, openDb,
|
|
30
|
+
createTask, assignTask, completeTask, failTask, listTasks, getTask, getNextPendingTask,
|
|
31
|
+
registerWorktree, listWorktrees,
|
|
32
|
+
claimFiles, releaseFileClaims, getActiveFileClaims, checkFileConflicts,
|
|
33
|
+
logConflict,
|
|
34
|
+
} from '../core/db.js';
|
|
35
|
+
import { scanAllWorktrees } from '../core/detector.js';
|
|
36
|
+
|
|
37
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
|
|
39
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function getRepo() {
|
|
42
|
+
try {
|
|
43
|
+
return findRepoRoot();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(chalk.red(err.message));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getDb(repoRoot) {
|
|
51
|
+
try {
|
|
52
|
+
return openDb(repoRoot);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(chalk.red(err.message));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function statusBadge(status) {
|
|
60
|
+
const colors = {
|
|
61
|
+
pending: chalk.yellow,
|
|
62
|
+
in_progress: chalk.blue,
|
|
63
|
+
done: chalk.green,
|
|
64
|
+
failed: chalk.red,
|
|
65
|
+
idle: chalk.gray,
|
|
66
|
+
busy: chalk.blue,
|
|
67
|
+
};
|
|
68
|
+
return (colors[status] || chalk.white)(status.toUpperCase().padEnd(11));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function printTable(rows, columns) {
|
|
72
|
+
if (!rows.length) return;
|
|
73
|
+
const widths = columns.map(col =>
|
|
74
|
+
Math.max(col.label.length, ...rows.map(r => String(r[col.key] || '').length))
|
|
75
|
+
);
|
|
76
|
+
const header = columns.map((col, i) => col.label.padEnd(widths[i])).join(' ');
|
|
77
|
+
console.log(chalk.dim(header));
|
|
78
|
+
console.log(chalk.dim('─'.repeat(header.length)));
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
console.log(columns.map((col, i) => {
|
|
81
|
+
const val = String(row[col.key] || '');
|
|
82
|
+
return col.format ? col.format(val) : val.padEnd(widths[i]);
|
|
83
|
+
}).join(' '));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Program ──────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
program
|
|
90
|
+
.name('switchman')
|
|
91
|
+
.description('Conflict-aware task coordinator for parallel AI coding agents')
|
|
92
|
+
.version('0.1.0');
|
|
93
|
+
|
|
94
|
+
// ── init ──────────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
program
|
|
97
|
+
.command('init')
|
|
98
|
+
.description('Initialize switchman in the current git repository')
|
|
99
|
+
.action(() => {
|
|
100
|
+
const repoRoot = getRepo();
|
|
101
|
+
const spinner = ora('Initializing switchman...').start();
|
|
102
|
+
try {
|
|
103
|
+
const db = initDb(repoRoot);
|
|
104
|
+
|
|
105
|
+
// Auto-register existing git worktrees
|
|
106
|
+
const gitWorktrees = listGitWorktrees(repoRoot);
|
|
107
|
+
for (const wt of gitWorktrees) {
|
|
108
|
+
registerWorktree(db, { name: wt.name, path: wt.path, branch: wt.branch || 'unknown' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
db.close();
|
|
112
|
+
spinner.succeed(`Initialized in ${chalk.cyan(repoRoot)}`);
|
|
113
|
+
console.log(chalk.dim(` Found and registered ${gitWorktrees.length} git worktree(s)`));
|
|
114
|
+
console.log(chalk.dim(` Database: .switchman/switchman.db`));
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log(`Next steps:`);
|
|
117
|
+
console.log(` ${chalk.cyan('switchman task add "Fix the login bug"')} — add a task`);
|
|
118
|
+
console.log(` ${chalk.cyan('switchman scan')} — check for conflicts`);
|
|
119
|
+
console.log(` ${chalk.cyan('switchman status')} — view full status`);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
spinner.fail(err.message);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
// ── setup ─────────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
program
|
|
130
|
+
.command('setup')
|
|
131
|
+
.description('One-command setup: create agent worktrees and initialise switchman')
|
|
132
|
+
.option('-a, --agents <n>', 'Number of agent worktrees to create (default: 3)', '3')
|
|
133
|
+
.option('--prefix <prefix>', 'Branch prefix (default: switchman)', 'switchman')
|
|
134
|
+
.action((opts) => {
|
|
135
|
+
const agentCount = parseInt(opts.agents);
|
|
136
|
+
|
|
137
|
+
if (isNaN(agentCount) || agentCount < 1 || agentCount > 10) {
|
|
138
|
+
console.error(chalk.red('--agents must be a number between 1 and 10'));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const repoRoot = getRepo();
|
|
143
|
+
const spinner = ora('Setting up Switchman...').start();
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// git worktree add requires at least one commit
|
|
147
|
+
try {
|
|
148
|
+
execSync('git rev-parse HEAD', {
|
|
149
|
+
cwd: repoRoot,
|
|
150
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
151
|
+
});
|
|
152
|
+
} catch {
|
|
153
|
+
spinner.fail('Your repo needs at least one commit before worktrees can be created.');
|
|
154
|
+
console.log(chalk.dim(' Run: git commit --allow-empty -m "init" then try again'));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Init the switchman database
|
|
159
|
+
const db = initDb(repoRoot);
|
|
160
|
+
|
|
161
|
+
// Create one worktree per agent
|
|
162
|
+
const created = [];
|
|
163
|
+
for (let i = 1; i <= agentCount; i++) {
|
|
164
|
+
const name = `agent${i}`;
|
|
165
|
+
const branch = `${opts.prefix}/agent${i}`;
|
|
166
|
+
spinner.text = `Creating worktree ${i}/${agentCount}...`;
|
|
167
|
+
try {
|
|
168
|
+
const wtPath = createGitWorktree(repoRoot, name, branch);
|
|
169
|
+
registerWorktree(db, { name, path: wtPath, branch });
|
|
170
|
+
created.push({ name, path: wtPath, branch });
|
|
171
|
+
} catch {
|
|
172
|
+
// Worktree already exists — register it without failing
|
|
173
|
+
const repoName = repoRoot.split('/').pop();
|
|
174
|
+
const wtPath = join(repoRoot, '..', `${repoName}-${name}`);
|
|
175
|
+
registerWorktree(db, { name, path: wtPath, branch });
|
|
176
|
+
created.push({ name, path: wtPath, branch, existed: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Register the main worktree too
|
|
181
|
+
const gitWorktrees = listGitWorktrees(repoRoot);
|
|
182
|
+
for (const wt of gitWorktrees) {
|
|
183
|
+
registerWorktree(db, { name: wt.name, path: wt.path, branch: wt.branch || 'unknown' });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
db.close();
|
|
187
|
+
|
|
188
|
+
const label = agentCount === 1 ? 'workspace' : 'workspaces';
|
|
189
|
+
spinner.succeed(`Switchman ready — ${agentCount} agent ${label} created`);
|
|
190
|
+
console.log('');
|
|
191
|
+
|
|
192
|
+
for (const wt of created) {
|
|
193
|
+
const note = wt.existed ? chalk.dim(' (already existed, re-registered)') : '';
|
|
194
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(wt.path)}${note}`);
|
|
195
|
+
console.log(` ${chalk.dim('branch:')} ${wt.branch}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log('');
|
|
199
|
+
console.log(chalk.bold('Next steps:'));
|
|
200
|
+
console.log(` 1. Add your tasks:`);
|
|
201
|
+
console.log(` ${chalk.cyan('switchman task add "Your first task" --priority 8')}`);
|
|
202
|
+
console.log(` 2. Open Claude Code in each folder above — agents will coordinate automatically`);
|
|
203
|
+
console.log(` 3. Check status at any time:`);
|
|
204
|
+
console.log(` ${chalk.cyan('switchman status')}`);
|
|
205
|
+
console.log('');
|
|
206
|
+
|
|
207
|
+
} catch (err) {
|
|
208
|
+
spinner.fail(err.message);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
// ── task ──────────────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
const taskCmd = program.command('task').description('Manage the task queue');
|
|
217
|
+
|
|
218
|
+
taskCmd
|
|
219
|
+
.command('add <title>')
|
|
220
|
+
.description('Add a new task to the queue')
|
|
221
|
+
.option('-d, --description <desc>', 'Task description')
|
|
222
|
+
.option('-p, --priority <n>', 'Priority 1-10 (default 5)', '5')
|
|
223
|
+
.option('--id <id>', 'Custom task ID')
|
|
224
|
+
.action((title, opts) => {
|
|
225
|
+
const repoRoot = getRepo();
|
|
226
|
+
const db = getDb(repoRoot);
|
|
227
|
+
const taskId = createTask(db, {
|
|
228
|
+
id: opts.id,
|
|
229
|
+
title,
|
|
230
|
+
description: opts.description,
|
|
231
|
+
priority: parseInt(opts.priority),
|
|
232
|
+
});
|
|
233
|
+
db.close();
|
|
234
|
+
console.log(`${chalk.green('✓')} Task created: ${chalk.cyan(taskId)}`);
|
|
235
|
+
console.log(` ${chalk.dim(title)}`);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
taskCmd
|
|
239
|
+
.command('list')
|
|
240
|
+
.description('List all tasks')
|
|
241
|
+
.option('-s, --status <status>', 'Filter by status (pending|in_progress|done|failed)')
|
|
242
|
+
.action((opts) => {
|
|
243
|
+
const repoRoot = getRepo();
|
|
244
|
+
const db = getDb(repoRoot);
|
|
245
|
+
const tasks = listTasks(db, opts.status);
|
|
246
|
+
db.close();
|
|
247
|
+
|
|
248
|
+
if (!tasks.length) {
|
|
249
|
+
console.log(chalk.dim('No tasks found.'));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log('');
|
|
254
|
+
for (const t of tasks) {
|
|
255
|
+
const badge = statusBadge(t.status);
|
|
256
|
+
const worktree = t.worktree ? chalk.cyan(t.worktree) : chalk.dim('unassigned');
|
|
257
|
+
console.log(`${badge} ${chalk.bold(t.title)}`);
|
|
258
|
+
console.log(` ${chalk.dim('id:')} ${t.id} ${chalk.dim('worktree:')} ${worktree} ${chalk.dim('priority:')} ${t.priority}`);
|
|
259
|
+
if (t.description) console.log(` ${chalk.dim(t.description)}`);
|
|
260
|
+
console.log('');
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
taskCmd
|
|
265
|
+
.command('assign <taskId> <worktree>')
|
|
266
|
+
.description('Assign a task to a worktree')
|
|
267
|
+
.option('--agent <name>', 'Agent name (e.g. claude-code)')
|
|
268
|
+
.action((taskId, worktree, opts) => {
|
|
269
|
+
const repoRoot = getRepo();
|
|
270
|
+
const db = getDb(repoRoot);
|
|
271
|
+
const ok = assignTask(db, taskId, worktree, opts.agent);
|
|
272
|
+
db.close();
|
|
273
|
+
if (ok) {
|
|
274
|
+
console.log(`${chalk.green('✓')} Assigned ${chalk.cyan(taskId)} → ${chalk.cyan(worktree)}`);
|
|
275
|
+
} else {
|
|
276
|
+
console.log(chalk.red(`Could not assign task. It may not exist or is not in 'pending' status.`));
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
taskCmd
|
|
281
|
+
.command('done <taskId>')
|
|
282
|
+
.description('Mark a task as complete and release all file claims')
|
|
283
|
+
.action((taskId) => {
|
|
284
|
+
const repoRoot = getRepo();
|
|
285
|
+
const db = getDb(repoRoot);
|
|
286
|
+
completeTask(db, taskId);
|
|
287
|
+
releaseFileClaims(db, taskId);
|
|
288
|
+
db.close();
|
|
289
|
+
console.log(`${chalk.green('✓')} Task ${chalk.cyan(taskId)} marked done — file claims released`);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
taskCmd
|
|
293
|
+
.command('fail <taskId> [reason]')
|
|
294
|
+
.description('Mark a task as failed')
|
|
295
|
+
.action((taskId, reason) => {
|
|
296
|
+
const repoRoot = getRepo();
|
|
297
|
+
const db = getDb(repoRoot);
|
|
298
|
+
failTask(db, taskId, reason);
|
|
299
|
+
releaseFileClaims(db, taskId);
|
|
300
|
+
db.close();
|
|
301
|
+
console.log(`${chalk.red('✗')} Task ${chalk.cyan(taskId)} marked failed`);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
taskCmd
|
|
305
|
+
.command('next')
|
|
306
|
+
.description('Get and assign the next pending task (for agent automation)')
|
|
307
|
+
.option('--json', 'Output as JSON')
|
|
308
|
+
.option('--worktree <name>', 'Worktree to assign the task to (defaults to current worktree name)')
|
|
309
|
+
.option('--agent <name>', 'Agent identifier for logging (e.g. claude-code)')
|
|
310
|
+
.action((opts) => {
|
|
311
|
+
const repoRoot = getRepo();
|
|
312
|
+
const db = getDb(repoRoot);
|
|
313
|
+
const task = getNextPendingTask(db);
|
|
314
|
+
|
|
315
|
+
if (!task) {
|
|
316
|
+
db.close();
|
|
317
|
+
if (opts.json) console.log(JSON.stringify({ task: null }));
|
|
318
|
+
else console.log(chalk.dim('No pending tasks.'));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Determine worktree name: explicit flag, or derive from cwd
|
|
323
|
+
const worktreeName = opts.worktree || process.cwd().split('/').pop();
|
|
324
|
+
const assigned = assignTask(db, task.id, worktreeName, opts.agent || null);
|
|
325
|
+
db.close();
|
|
326
|
+
|
|
327
|
+
if (!assigned) {
|
|
328
|
+
// Race condition: another agent grabbed it between get and assign
|
|
329
|
+
if (opts.json) console.log(JSON.stringify({ task: null, message: 'Task claimed by another agent — try again' }));
|
|
330
|
+
else console.log(chalk.yellow('Task was just claimed by another agent. Run again to get the next one.'));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (opts.json) {
|
|
335
|
+
console.log(JSON.stringify({ task: { ...task, worktree: worktreeName, status: 'in_progress' } }, null, 2));
|
|
336
|
+
} else {
|
|
337
|
+
console.log(`${chalk.green('✓')} Assigned: ${chalk.bold(task.title)}`);
|
|
338
|
+
console.log(` ${chalk.dim('id:')} ${task.id} ${chalk.dim('worktree:')} ${chalk.cyan(worktreeName)} ${chalk.dim('priority:')} ${task.priority}`);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ── worktree ───────────────────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
const wtCmd = program.command('worktree').description('Manage worktrees');
|
|
345
|
+
|
|
346
|
+
wtCmd
|
|
347
|
+
.command('add <name> <path> <branch>')
|
|
348
|
+
.description('Register a worktree with switchman')
|
|
349
|
+
.option('--agent <name>', 'Agent assigned to this worktree')
|
|
350
|
+
.action((name, path, branch, opts) => {
|
|
351
|
+
const repoRoot = getRepo();
|
|
352
|
+
const db = getDb(repoRoot);
|
|
353
|
+
registerWorktree(db, { name, path, branch, agent: opts.agent });
|
|
354
|
+
db.close();
|
|
355
|
+
console.log(`${chalk.green('✓')} Registered worktree: ${chalk.cyan(name)}`);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
wtCmd
|
|
359
|
+
.command('list')
|
|
360
|
+
.description('List all registered worktrees')
|
|
361
|
+
.action(() => {
|
|
362
|
+
const repoRoot = getRepo();
|
|
363
|
+
const db = getDb(repoRoot);
|
|
364
|
+
const worktrees = listWorktrees(db);
|
|
365
|
+
const gitWorktrees = listGitWorktrees(repoRoot);
|
|
366
|
+
db.close();
|
|
367
|
+
|
|
368
|
+
if (!worktrees.length && !gitWorktrees.length) {
|
|
369
|
+
console.log(chalk.dim('No worktrees found.'));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Show git worktrees (source of truth) annotated with db info
|
|
374
|
+
console.log('');
|
|
375
|
+
console.log(chalk.bold('Git Worktrees:'));
|
|
376
|
+
for (const wt of gitWorktrees) {
|
|
377
|
+
const dbInfo = worktrees.find(d => d.path === wt.path);
|
|
378
|
+
const agent = dbInfo?.agent ? chalk.cyan(dbInfo.agent) : chalk.dim('no agent');
|
|
379
|
+
const status = dbInfo?.status ? statusBadge(dbInfo.status) : chalk.dim('unregistered');
|
|
380
|
+
console.log(` ${chalk.bold(wt.name.padEnd(20))} ${status} branch: ${chalk.cyan(wt.branch || 'unknown')} agent: ${agent}`);
|
|
381
|
+
console.log(` ${chalk.dim(wt.path)}`);
|
|
382
|
+
}
|
|
383
|
+
console.log('');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
wtCmd
|
|
387
|
+
.command('sync')
|
|
388
|
+
.description('Sync git worktrees into the switchman database')
|
|
389
|
+
.action(() => {
|
|
390
|
+
const repoRoot = getRepo();
|
|
391
|
+
const db = getDb(repoRoot);
|
|
392
|
+
const gitWorktrees = listGitWorktrees(repoRoot);
|
|
393
|
+
for (const wt of gitWorktrees) {
|
|
394
|
+
registerWorktree(db, { name: wt.name, path: wt.path, branch: wt.branch || 'unknown' });
|
|
395
|
+
}
|
|
396
|
+
db.close();
|
|
397
|
+
console.log(`${chalk.green('✓')} Synced ${gitWorktrees.length} worktree(s) from git`);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// ── claim ──────────────────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
program
|
|
403
|
+
.command('claim <taskId> <worktree> [files...]')
|
|
404
|
+
.description('Claim files for a task (warns if conflicts exist)')
|
|
405
|
+
.option('--agent <name>', 'Agent name')
|
|
406
|
+
.option('--force', 'Claim even if conflicts exist')
|
|
407
|
+
.action((taskId, worktree, files, opts) => {
|
|
408
|
+
if (!files.length) {
|
|
409
|
+
console.log(chalk.yellow('No files specified. Use: switchman claim <taskId> <worktree> file1 file2 ...'));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const repoRoot = getRepo();
|
|
413
|
+
const db = getDb(repoRoot);
|
|
414
|
+
|
|
415
|
+
// Check for existing claims
|
|
416
|
+
const conflicts = checkFileConflicts(db, files, worktree);
|
|
417
|
+
|
|
418
|
+
if (conflicts.length > 0 && !opts.force) {
|
|
419
|
+
console.log(chalk.red(`\n⚠ Claim conflicts detected:`));
|
|
420
|
+
for (const c of conflicts) {
|
|
421
|
+
console.log(` ${chalk.yellow(c.file)} → already claimed by worktree ${chalk.cyan(c.claimedBy.worktree)} (task: ${c.claimedBy.task_title})`);
|
|
422
|
+
}
|
|
423
|
+
console.log(chalk.dim('\nUse --force to claim anyway, or resolve conflicts first.'));
|
|
424
|
+
db.close();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
claimFiles(db, taskId, worktree, files, opts.agent);
|
|
429
|
+
db.close();
|
|
430
|
+
console.log(`${chalk.green('✓')} Claimed ${files.length} file(s) for task ${chalk.cyan(taskId)}`);
|
|
431
|
+
files.forEach(f => console.log(` ${chalk.dim(f)}`));
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
program
|
|
435
|
+
.command('release <taskId>')
|
|
436
|
+
.description('Release all file claims for a task')
|
|
437
|
+
.action((taskId) => {
|
|
438
|
+
const repoRoot = getRepo();
|
|
439
|
+
const db = getDb(repoRoot);
|
|
440
|
+
releaseFileClaims(db, taskId);
|
|
441
|
+
db.close();
|
|
442
|
+
console.log(`${chalk.green('✓')} Released all claims for task ${chalk.cyan(taskId)}`);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// ── scan ───────────────────────────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
program
|
|
448
|
+
.command('scan')
|
|
449
|
+
.description('Scan all worktrees for conflicts')
|
|
450
|
+
.option('--json', 'Output raw JSON')
|
|
451
|
+
.option('--quiet', 'Only show conflicts')
|
|
452
|
+
.action(async (opts) => {
|
|
453
|
+
const repoRoot = getRepo();
|
|
454
|
+
const db = getDb(repoRoot);
|
|
455
|
+
const spinner = ora('Scanning worktrees for conflicts...').start();
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const report = await scanAllWorktrees(db, repoRoot);
|
|
459
|
+
db.close();
|
|
460
|
+
spinner.stop();
|
|
461
|
+
|
|
462
|
+
if (opts.json) {
|
|
463
|
+
console.log(JSON.stringify(report, null, 2));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
console.log('');
|
|
468
|
+
console.log(chalk.bold(`Conflict Scan Report`));
|
|
469
|
+
console.log(chalk.dim(`${report.scannedAt}`));
|
|
470
|
+
console.log('');
|
|
471
|
+
|
|
472
|
+
// Worktrees summary
|
|
473
|
+
if (!opts.quiet) {
|
|
474
|
+
console.log(chalk.bold('Worktrees:'));
|
|
475
|
+
for (const wt of report.worktrees) {
|
|
476
|
+
const files = report.fileMap?.[wt.name] || [];
|
|
477
|
+
console.log(` ${chalk.cyan(wt.name.padEnd(20))} branch: ${(wt.branch || 'unknown').padEnd(30)} ${chalk.dim(files.length + ' changed file(s)')}`);
|
|
478
|
+
}
|
|
479
|
+
console.log('');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// File-level overlaps (uncommitted)
|
|
483
|
+
if (report.fileConflicts.length > 0) {
|
|
484
|
+
console.log(chalk.yellow(`⚠ Files being edited in multiple worktrees (uncommitted):`));
|
|
485
|
+
for (const fc of report.fileConflicts) {
|
|
486
|
+
console.log(` ${chalk.yellow(fc.file)}`);
|
|
487
|
+
console.log(` ${chalk.dim('edited in:')} ${fc.worktrees.join(', ')}`);
|
|
488
|
+
}
|
|
489
|
+
console.log('');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Branch-level conflicts
|
|
493
|
+
if (report.conflicts.length > 0) {
|
|
494
|
+
console.log(chalk.red(`✗ Branch conflicts detected:`));
|
|
495
|
+
for (const c of report.conflicts) {
|
|
496
|
+
const icon = c.type === 'merge_conflict' ? chalk.red('MERGE CONFLICT') : chalk.yellow('FILE OVERLAP');
|
|
497
|
+
console.log(` ${icon}`);
|
|
498
|
+
console.log(` ${chalk.cyan(c.worktreeA)} (${c.branchA}) ↔ ${chalk.cyan(c.worktreeB)} (${c.branchB})`);
|
|
499
|
+
if (c.conflictingFiles.length) {
|
|
500
|
+
console.log(` Conflicting files:`);
|
|
501
|
+
c.conflictingFiles.forEach(f => console.log(` ${chalk.yellow(f)}`));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
console.log('');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// All clear
|
|
508
|
+
if (report.conflicts.length === 0 && report.fileConflicts.length === 0) {
|
|
509
|
+
console.log(chalk.green(`✓ No conflicts detected across ${report.worktrees.length} worktree(s)`));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
} catch (err) {
|
|
513
|
+
spinner.fail(err.message);
|
|
514
|
+
db.close();
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// ── status ─────────────────────────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
program
|
|
522
|
+
.command('status')
|
|
523
|
+
.description('Show full system status: tasks, worktrees, claims, and conflicts')
|
|
524
|
+
.action(async () => {
|
|
525
|
+
const repoRoot = getRepo();
|
|
526
|
+
const db = getDb(repoRoot);
|
|
527
|
+
|
|
528
|
+
console.log('');
|
|
529
|
+
console.log(chalk.bold.cyan('━━━ switchman status ━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
530
|
+
console.log(chalk.dim(`Repo: ${repoRoot}`));
|
|
531
|
+
console.log('');
|
|
532
|
+
|
|
533
|
+
// Tasks
|
|
534
|
+
const tasks = listTasks(db);
|
|
535
|
+
const pending = tasks.filter(t => t.status === 'pending');
|
|
536
|
+
const inProgress = tasks.filter(t => t.status === 'in_progress');
|
|
537
|
+
const done = tasks.filter(t => t.status === 'done');
|
|
538
|
+
const failed = tasks.filter(t => t.status === 'failed');
|
|
539
|
+
|
|
540
|
+
console.log(chalk.bold('Tasks:'));
|
|
541
|
+
console.log(` ${chalk.yellow('Pending')} ${pending.length}`);
|
|
542
|
+
console.log(` ${chalk.blue('In Progress')} ${inProgress.length}`);
|
|
543
|
+
console.log(` ${chalk.green('Done')} ${done.length}`);
|
|
544
|
+
console.log(` ${chalk.red('Failed')} ${failed.length}`);
|
|
545
|
+
|
|
546
|
+
if (inProgress.length > 0) {
|
|
547
|
+
console.log('');
|
|
548
|
+
console.log(chalk.bold('Active Tasks:'));
|
|
549
|
+
for (const t of inProgress) {
|
|
550
|
+
console.log(` ${chalk.cyan(t.worktree || 'unassigned')} → ${t.title}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (pending.length > 0) {
|
|
555
|
+
console.log('');
|
|
556
|
+
console.log(chalk.bold('Next Up:'));
|
|
557
|
+
const next = pending.slice(0, 3);
|
|
558
|
+
for (const t of next) {
|
|
559
|
+
console.log(` [p${t.priority}] ${t.title} ${chalk.dim(t.id)}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// File Claims
|
|
564
|
+
const claims = getActiveFileClaims(db);
|
|
565
|
+
if (claims.length > 0) {
|
|
566
|
+
console.log('');
|
|
567
|
+
console.log(chalk.bold(`Active File Claims (${claims.length}):`));
|
|
568
|
+
const byWorktree = {};
|
|
569
|
+
for (const c of claims) {
|
|
570
|
+
if (!byWorktree[c.worktree]) byWorktree[c.worktree] = [];
|
|
571
|
+
byWorktree[c.worktree].push(c.file_path);
|
|
572
|
+
}
|
|
573
|
+
for (const [wt, files] of Object.entries(byWorktree)) {
|
|
574
|
+
console.log(` ${chalk.cyan(wt)}: ${files.slice(0, 5).join(', ')}${files.length > 5 ? ` +${files.length - 5} more` : ''}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Quick conflict scan
|
|
579
|
+
console.log('');
|
|
580
|
+
const spinner = ora('Running conflict scan...').start();
|
|
581
|
+
try {
|
|
582
|
+
const report = await scanAllWorktrees(db, repoRoot);
|
|
583
|
+
spinner.stop();
|
|
584
|
+
|
|
585
|
+
const totalConflicts = report.conflicts.length + report.fileConflicts.length;
|
|
586
|
+
if (totalConflicts === 0) {
|
|
587
|
+
console.log(chalk.green(`✓ No conflicts across ${report.worktrees.length} worktree(s)`));
|
|
588
|
+
} else {
|
|
589
|
+
console.log(chalk.red(`⚠ ${totalConflicts} conflict(s) detected — run 'switchman scan' for details`));
|
|
590
|
+
}
|
|
591
|
+
} catch {
|
|
592
|
+
spinner.stop();
|
|
593
|
+
console.log(chalk.dim('Could not run conflict scan'));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
db.close();
|
|
597
|
+
console.log('');
|
|
598
|
+
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
599
|
+
console.log('');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
program.parse();
|