switchman-dev 0.1.8 → 0.1.9
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 +118 -303
- package/package.json +1 -1
- package/src/cli/commands/audit.js +77 -0
- package/src/cli/commands/claude.js +37 -0
- package/src/cli/commands/gate.js +278 -0
- package/src/cli/commands/lease.js +256 -0
- package/src/cli/commands/mcp.js +45 -0
- package/src/cli/commands/monitor.js +191 -0
- package/src/cli/commands/queue.js +549 -0
- package/src/cli/commands/task.js +248 -0
- package/src/cli/commands/telemetry.js +108 -0
- package/src/cli/commands/worktree.js +85 -0
- package/src/cli/index.js +780 -1697
- package/src.zip +0 -0
- package/tests.zip +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
export function registerTaskCommands(program, {
|
|
2
|
+
acquireNextTaskLeaseWithRetries,
|
|
3
|
+
analyzeTaskScope,
|
|
4
|
+
chalk,
|
|
5
|
+
completeTaskWithRetries,
|
|
6
|
+
createTask,
|
|
7
|
+
failTask,
|
|
8
|
+
getCurrentWorktreeName,
|
|
9
|
+
getDb,
|
|
10
|
+
getRepo,
|
|
11
|
+
listTasks,
|
|
12
|
+
printErrorWithNext,
|
|
13
|
+
pushSyncEvent,
|
|
14
|
+
releaseFileClaims,
|
|
15
|
+
retryStaleTasks,
|
|
16
|
+
retryTask,
|
|
17
|
+
startTaskLease,
|
|
18
|
+
statusBadge,
|
|
19
|
+
taskJsonWithLease,
|
|
20
|
+
}) {
|
|
21
|
+
const taskCmd = program.command('task').description('Manage the task list');
|
|
22
|
+
taskCmd.addHelpText('after', `
|
|
23
|
+
Examples:
|
|
24
|
+
switchman task add "Fix login bug" --priority 8
|
|
25
|
+
switchman task list --status pending
|
|
26
|
+
switchman task done task-123
|
|
27
|
+
`);
|
|
28
|
+
|
|
29
|
+
taskCmd
|
|
30
|
+
.command('add <title>')
|
|
31
|
+
.description('Add a new task to the queue')
|
|
32
|
+
.option('-d, --description <desc>', 'Task description')
|
|
33
|
+
.option('-p, --priority <n>', 'Priority 1-10 (default 5)', '5')
|
|
34
|
+
.option('--id <id>', 'Custom task ID')
|
|
35
|
+
.action((title, opts) => {
|
|
36
|
+
const repoRoot = getRepo();
|
|
37
|
+
const db = getDb(repoRoot);
|
|
38
|
+
const taskId = createTask(db, {
|
|
39
|
+
id: opts.id,
|
|
40
|
+
title,
|
|
41
|
+
description: opts.description,
|
|
42
|
+
priority: parseInt(opts.priority),
|
|
43
|
+
});
|
|
44
|
+
db.close();
|
|
45
|
+
const scopeWarning = analyzeTaskScope(title, opts.description || '');
|
|
46
|
+
console.log(`${chalk.green('✓')} Task created: ${chalk.cyan(taskId)}`);
|
|
47
|
+
pushSyncEvent('task_added', { task_id: taskId, title, priority: parseInt(opts.priority) }).catch(() => {});
|
|
48
|
+
console.log(` ${chalk.dim(title)}`);
|
|
49
|
+
if (scopeWarning) {
|
|
50
|
+
console.log(chalk.yellow(` warning: ${scopeWarning.summary}`));
|
|
51
|
+
console.log(chalk.yellow(` next: ${scopeWarning.next_step}`));
|
|
52
|
+
console.log(chalk.cyan(` try: ${scopeWarning.command}`));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
taskCmd
|
|
57
|
+
.command('list')
|
|
58
|
+
.description('List all tasks')
|
|
59
|
+
.option('-s, --status <status>', 'Filter by status (pending|in_progress|done|failed)')
|
|
60
|
+
.action((opts) => {
|
|
61
|
+
const repoRoot = getRepo();
|
|
62
|
+
const db = getDb(repoRoot);
|
|
63
|
+
const tasks = listTasks(db, opts.status);
|
|
64
|
+
db.close();
|
|
65
|
+
|
|
66
|
+
if (!tasks.length) {
|
|
67
|
+
console.log(chalk.dim('No tasks found.'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log('');
|
|
72
|
+
for (const t of tasks) {
|
|
73
|
+
const badge = statusBadge(t.status);
|
|
74
|
+
const worktree = t.worktree ? chalk.cyan(t.worktree) : chalk.dim('unassigned');
|
|
75
|
+
console.log(`${badge} ${chalk.bold(t.title)}`);
|
|
76
|
+
console.log(` ${chalk.dim('id:')} ${t.id} ${chalk.dim('worktree:')} ${worktree} ${chalk.dim('priority:')} ${t.priority}`);
|
|
77
|
+
if (t.description) console.log(` ${chalk.dim(t.description)}`);
|
|
78
|
+
console.log('');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
taskCmd
|
|
83
|
+
.command('assign <taskId> <worktree>')
|
|
84
|
+
.description('Assign a task to a workspace (compatibility shim for lease acquire)')
|
|
85
|
+
.option('--agent <name>', 'Agent name (e.g. claude-code)')
|
|
86
|
+
.action((taskId, worktree, opts) => {
|
|
87
|
+
const repoRoot = getRepo();
|
|
88
|
+
const db = getDb(repoRoot);
|
|
89
|
+
const lease = startTaskLease(db, taskId, worktree, opts.agent);
|
|
90
|
+
db.close();
|
|
91
|
+
if (lease) {
|
|
92
|
+
console.log(`${chalk.green('✓')} Assigned ${chalk.cyan(taskId)} → ${chalk.cyan(worktree)} (${chalk.dim(lease.id)})`);
|
|
93
|
+
} else {
|
|
94
|
+
console.log(chalk.red(`Could not assign task. It may not exist or is not in 'pending' status.`));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
taskCmd
|
|
99
|
+
.command('retry <taskId>')
|
|
100
|
+
.description('Return a failed or stale completed task to pending so it can be revalidated')
|
|
101
|
+
.option('--reason <text>', 'Reason to record for the retry')
|
|
102
|
+
.option('--json', 'Output raw JSON')
|
|
103
|
+
.action((taskId, opts) => {
|
|
104
|
+
const repoRoot = getRepo();
|
|
105
|
+
const db = getDb(repoRoot);
|
|
106
|
+
const task = retryTask(db, taskId, opts.reason || 'manual retry');
|
|
107
|
+
db.close();
|
|
108
|
+
|
|
109
|
+
if (!task) {
|
|
110
|
+
printErrorWithNext(`Task ${taskId} is not retryable.`, 'switchman task list --status failed');
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (opts.json) {
|
|
116
|
+
console.log(JSON.stringify(task, null, 2));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`${chalk.green('✓')} Reset ${chalk.cyan(task.id)} to pending`);
|
|
121
|
+
pushSyncEvent('task_retried', { task_id: task.id, title: task.title, reason: opts.reason || 'manual retry' }).catch(() => {});
|
|
122
|
+
console.log(` ${chalk.dim('title:')} ${task.title}`);
|
|
123
|
+
console.log(`${chalk.yellow('next:')} switchman task assign ${task.id} <workspace>`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
taskCmd
|
|
127
|
+
.command('retry-stale')
|
|
128
|
+
.description('Return all currently stale tasks to pending so they can be revalidated together')
|
|
129
|
+
.option('--pipeline <id>', 'Only retry stale tasks for one pipeline')
|
|
130
|
+
.option('--reason <text>', 'Reason to record for the retry', 'bulk stale retry')
|
|
131
|
+
.option('--json', 'Output raw JSON')
|
|
132
|
+
.action((opts) => {
|
|
133
|
+
const repoRoot = getRepo();
|
|
134
|
+
const db = getDb(repoRoot);
|
|
135
|
+
const result = retryStaleTasks(db, {
|
|
136
|
+
pipelineId: opts.pipeline || null,
|
|
137
|
+
reason: opts.reason,
|
|
138
|
+
});
|
|
139
|
+
db.close();
|
|
140
|
+
|
|
141
|
+
if (opts.json) {
|
|
142
|
+
console.log(JSON.stringify(result, null, 2));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (result.retried.length === 0) {
|
|
147
|
+
const scope = result.pipeline_id ? ` for ${result.pipeline_id}` : '';
|
|
148
|
+
console.log(chalk.dim(`No stale tasks to retry${scope}.`));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(`${chalk.green('✓')} Reset ${result.retried.length} stale task(s) to pending`);
|
|
153
|
+
pushSyncEvent('task_retried', {
|
|
154
|
+
pipeline_id: result.pipeline_id || null,
|
|
155
|
+
task_count: result.retried.length,
|
|
156
|
+
task_ids: result.retried.map((task) => task.id),
|
|
157
|
+
reason: opts.reason,
|
|
158
|
+
}).catch(() => {});
|
|
159
|
+
if (result.pipeline_id) {
|
|
160
|
+
console.log(` ${chalk.dim('pipeline:')} ${result.pipeline_id}`);
|
|
161
|
+
}
|
|
162
|
+
console.log(` ${chalk.dim('tasks:')} ${result.retried.map((task) => task.id).join(', ')}`);
|
|
163
|
+
console.log(`${chalk.yellow('next:')} switchman status`);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
taskCmd
|
|
167
|
+
.command('done <taskId>')
|
|
168
|
+
.description('Mark a task as complete and release all file claims')
|
|
169
|
+
.action((taskId) => {
|
|
170
|
+
const repoRoot = getRepo();
|
|
171
|
+
try {
|
|
172
|
+
const result = completeTaskWithRetries(repoRoot, taskId);
|
|
173
|
+
if (result?.status === 'already_done') {
|
|
174
|
+
console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} was already marked done — no new changes were recorded`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (result?.status === 'failed') {
|
|
178
|
+
console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} is currently failed — retry it before marking it done again`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (result?.status === 'not_in_progress') {
|
|
182
|
+
console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} is not currently in progress — start a lease before marking it done`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (result?.status === 'no_active_lease') {
|
|
186
|
+
console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} has no active lease — reacquire the task before marking it done`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
console.log(`${chalk.green('✓')} Task ${chalk.cyan(taskId)} marked done — file claims released`);
|
|
190
|
+
pushSyncEvent('task_done', { task_id: taskId }).catch(() => {});
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error(chalk.red(err.message));
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
taskCmd
|
|
198
|
+
.command('fail <taskId> [reason]')
|
|
199
|
+
.description('Mark a task as failed')
|
|
200
|
+
.action((taskId, reason) => {
|
|
201
|
+
const repoRoot = getRepo();
|
|
202
|
+
const db = getDb(repoRoot);
|
|
203
|
+
failTask(db, taskId, reason);
|
|
204
|
+
releaseFileClaims(db, taskId);
|
|
205
|
+
db.close();
|
|
206
|
+
console.log(`${chalk.red('✗')} Task ${chalk.cyan(taskId)} marked failed`);
|
|
207
|
+
pushSyncEvent('task_failed', { task_id: taskId, reason: reason || null }).catch(() => {});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
taskCmd
|
|
211
|
+
.command('next')
|
|
212
|
+
.description('Get the next pending task quickly (use `lease next` for the full workflow)')
|
|
213
|
+
.option('--json', 'Output as JSON')
|
|
214
|
+
.option('--worktree <name>', 'Workspace to assign the task to (defaults to the current folder name)')
|
|
215
|
+
.option('--agent <name>', 'Agent identifier for logging (e.g. claude-code)')
|
|
216
|
+
.addHelpText('after', `
|
|
217
|
+
Examples:
|
|
218
|
+
switchman task next
|
|
219
|
+
switchman task next --json
|
|
220
|
+
`)
|
|
221
|
+
.action((opts) => {
|
|
222
|
+
const repoRoot = getRepo();
|
|
223
|
+
const worktreeName = getCurrentWorktreeName(opts.worktree);
|
|
224
|
+
const { task, lease, exhausted } = acquireNextTaskLeaseWithRetries(repoRoot, worktreeName, opts.agent || null);
|
|
225
|
+
|
|
226
|
+
if (!task) {
|
|
227
|
+
if (opts.json) console.log(JSON.stringify({ task: null }));
|
|
228
|
+
else if (exhausted) console.log(chalk.dim('No pending tasks.'));
|
|
229
|
+
else console.log(chalk.yellow('Tasks were claimed by other agents during assignment. Run again to get the next one.'));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!lease) {
|
|
234
|
+
if (opts.json) console.log(JSON.stringify({ task: null, message: 'Task claimed by another agent — try again' }));
|
|
235
|
+
else console.log(chalk.yellow('Task was just claimed by another agent. Run again to get the next one.'));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (opts.json) {
|
|
240
|
+
console.log(JSON.stringify(taskJsonWithLease(task, worktreeName, lease), null, 2));
|
|
241
|
+
} else {
|
|
242
|
+
console.log(`${chalk.green('✓')} Assigned: ${chalk.bold(task.title)}`);
|
|
243
|
+
console.log(` ${chalk.dim('id:')} ${task.id} ${chalk.dim('worktree:')} ${chalk.cyan(worktreeName)} ${chalk.dim('lease:')} ${chalk.dim(lease.id)} ${chalk.dim('priority:')} ${task.priority}`);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return taskCmd;
|
|
248
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export function registerTelemetryCommands(program, {
|
|
2
|
+
chalk,
|
|
3
|
+
disableTelemetry,
|
|
4
|
+
enableTelemetry,
|
|
5
|
+
getTelemetryConfigPath,
|
|
6
|
+
getTelemetryRuntimeConfig,
|
|
7
|
+
loadTelemetryConfig,
|
|
8
|
+
printErrorWithNext,
|
|
9
|
+
sendTelemetryEvent,
|
|
10
|
+
}) {
|
|
11
|
+
const telemetryCmd = program.command('telemetry').description('Control anonymous opt-in telemetry for Switchman');
|
|
12
|
+
|
|
13
|
+
telemetryCmd
|
|
14
|
+
.command('status')
|
|
15
|
+
.description('Show whether telemetry is enabled and where events would be sent')
|
|
16
|
+
.option('--home <path>', 'Override the home directory for telemetry config')
|
|
17
|
+
.option('--json', 'Output raw JSON')
|
|
18
|
+
.action((opts) => {
|
|
19
|
+
const config = loadTelemetryConfig(opts.home || undefined);
|
|
20
|
+
const runtime = getTelemetryRuntimeConfig();
|
|
21
|
+
const payload = {
|
|
22
|
+
enabled: config.telemetry_enabled === true,
|
|
23
|
+
configured: Boolean(runtime.apiKey) && !runtime.disabled,
|
|
24
|
+
install_id: config.telemetry_install_id,
|
|
25
|
+
destination: runtime.apiKey && !runtime.disabled ? runtime.host : null,
|
|
26
|
+
config_path: getTelemetryConfigPath(opts.home || undefined),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (opts.json) {
|
|
30
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`Telemetry: ${payload.enabled ? chalk.green('enabled') : chalk.yellow('disabled')}`);
|
|
35
|
+
console.log(`Configured destination: ${payload.configured ? chalk.cyan(payload.destination) : chalk.dim('not configured')}`);
|
|
36
|
+
console.log(`Config file: ${chalk.dim(payload.config_path)}`);
|
|
37
|
+
if (payload.install_id) {
|
|
38
|
+
console.log(`Install ID: ${chalk.dim(payload.install_id)}`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
telemetryCmd
|
|
43
|
+
.command('enable')
|
|
44
|
+
.description('Enable anonymous telemetry for setup and operator workflows')
|
|
45
|
+
.option('--home <path>', 'Override the home directory for telemetry config')
|
|
46
|
+
.action((opts) => {
|
|
47
|
+
const runtime = getTelemetryRuntimeConfig();
|
|
48
|
+
if (!runtime.apiKey || runtime.disabled) {
|
|
49
|
+
printErrorWithNext('Telemetry destination is not configured. Set SWITCHMAN_TELEMETRY_API_KEY first.', 'switchman telemetry status');
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const result = enableTelemetry(opts.home || undefined);
|
|
54
|
+
console.log(`${chalk.green('✓')} Telemetry enabled`);
|
|
55
|
+
console.log(` ${chalk.dim(result.path)}`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
telemetryCmd
|
|
59
|
+
.command('disable')
|
|
60
|
+
.description('Disable anonymous telemetry')
|
|
61
|
+
.option('--home <path>', 'Override the home directory for telemetry config')
|
|
62
|
+
.action((opts) => {
|
|
63
|
+
const result = disableTelemetry(opts.home || undefined);
|
|
64
|
+
console.log(`${chalk.green('✓')} Telemetry disabled`);
|
|
65
|
+
console.log(` ${chalk.dim(result.path)}`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
telemetryCmd
|
|
69
|
+
.command('test')
|
|
70
|
+
.description('Send one test telemetry event and report whether delivery succeeded')
|
|
71
|
+
.option('--home <path>', 'Override the home directory for telemetry config')
|
|
72
|
+
.option('--json', 'Output raw JSON')
|
|
73
|
+
.action(async (opts) => {
|
|
74
|
+
const result = await sendTelemetryEvent('telemetry_test', {
|
|
75
|
+
app_version: program.version(),
|
|
76
|
+
os: process.platform,
|
|
77
|
+
node_version: process.version,
|
|
78
|
+
source: 'switchman-cli-test',
|
|
79
|
+
}, { homeDir: opts.home || undefined });
|
|
80
|
+
|
|
81
|
+
if (opts.json) {
|
|
82
|
+
console.log(JSON.stringify(result, null, 2));
|
|
83
|
+
if (!result.ok) process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (result.ok) {
|
|
88
|
+
console.log(`${chalk.green('✓')} Telemetry test event delivered`);
|
|
89
|
+
console.log(` ${chalk.dim('destination:')} ${chalk.cyan(result.destination)}`);
|
|
90
|
+
if (result.status) {
|
|
91
|
+
console.log(` ${chalk.dim('status:')} ${result.status}`);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
printErrorWithNext(`Telemetry test failed (${result.reason || 'unknown_error'}).`, 'switchman telemetry status');
|
|
97
|
+
console.log(` ${chalk.dim('destination:')} ${result.destination || 'unknown'}`);
|
|
98
|
+
if (result.status) {
|
|
99
|
+
console.log(` ${chalk.dim('status:')} ${result.status}`);
|
|
100
|
+
}
|
|
101
|
+
if (result.error) {
|
|
102
|
+
console.log(` ${chalk.dim('error:')} ${result.error}`);
|
|
103
|
+
}
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return telemetryCmd;
|
|
108
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export function registerWorktreeCommands(program, {
|
|
2
|
+
chalk,
|
|
3
|
+
evaluateRepoCompliance,
|
|
4
|
+
getDb,
|
|
5
|
+
getRepo,
|
|
6
|
+
installMcpConfig,
|
|
7
|
+
listGitWorktrees,
|
|
8
|
+
listWorktrees,
|
|
9
|
+
registerWorktree,
|
|
10
|
+
statusBadge,
|
|
11
|
+
}) {
|
|
12
|
+
const wtCmd = program.command('worktree').alias('workspace').description('Manage registered workspaces (Git worktrees)');
|
|
13
|
+
wtCmd.addHelpText('after', `
|
|
14
|
+
Plain English:
|
|
15
|
+
worktree = the Git feature behind each agent workspace
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
switchman worktree list
|
|
19
|
+
switchman workspace list
|
|
20
|
+
switchman worktree sync
|
|
21
|
+
`);
|
|
22
|
+
|
|
23
|
+
wtCmd
|
|
24
|
+
.command('add <name> <path> <branch>')
|
|
25
|
+
.description('Register a workspace with Switchman')
|
|
26
|
+
.option('--agent <name>', 'Agent assigned to this worktree')
|
|
27
|
+
.action((name, path, branch, opts) => {
|
|
28
|
+
const repoRoot = getRepo();
|
|
29
|
+
const db = getDb(repoRoot);
|
|
30
|
+
registerWorktree(db, { name, path, branch, agent: opts.agent });
|
|
31
|
+
db.close();
|
|
32
|
+
console.log(`${chalk.green('✓')} Registered worktree: ${chalk.cyan(name)}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
wtCmd
|
|
36
|
+
.command('list')
|
|
37
|
+
.description('List all registered workspaces')
|
|
38
|
+
.action(() => {
|
|
39
|
+
const repoRoot = getRepo();
|
|
40
|
+
const db = getDb(repoRoot);
|
|
41
|
+
const worktrees = listWorktrees(db);
|
|
42
|
+
const gitWorktrees = listGitWorktrees(repoRoot);
|
|
43
|
+
|
|
44
|
+
if (!worktrees.length && !gitWorktrees.length) {
|
|
45
|
+
db.close();
|
|
46
|
+
console.log(chalk.dim('No workspaces found. Run `switchman setup --agents 3` or `switchman worktree sync`.'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const complianceReport = evaluateRepoCompliance(db, repoRoot, gitWorktrees);
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(chalk.bold('Git Worktrees:'));
|
|
53
|
+
for (const wt of gitWorktrees) {
|
|
54
|
+
const dbInfo = worktrees.find((d) => d.path === wt.path);
|
|
55
|
+
const complianceInfo = complianceReport.worktreeCompliance.find((entry) => entry.worktree === wt.name) || null;
|
|
56
|
+
const agent = dbInfo?.agent ? chalk.cyan(dbInfo.agent) : chalk.dim('no agent');
|
|
57
|
+
const status = dbInfo?.status ? statusBadge(dbInfo.status) : chalk.dim('unregistered');
|
|
58
|
+
const compliance = complianceInfo?.compliance_state ? statusBadge(complianceInfo.compliance_state) : dbInfo?.compliance_state ? statusBadge(dbInfo.compliance_state) : chalk.dim('unknown');
|
|
59
|
+
console.log(` ${chalk.bold(wt.name.padEnd(20))} ${status} ${compliance} branch: ${chalk.cyan(wt.branch || 'unknown')} agent: ${agent}`);
|
|
60
|
+
console.log(` ${chalk.dim(wt.path)}`);
|
|
61
|
+
if ((complianceInfo?.unclaimed_changed_files || []).length > 0) {
|
|
62
|
+
console.log(` ${chalk.red('files:')} ${complianceInfo.unclaimed_changed_files.slice(0, 5).join(', ')}${complianceInfo.unclaimed_changed_files.length > 5 ? ` ${chalk.dim(`+${complianceInfo.unclaimed_changed_files.length - 5} more`)}` : ''}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
console.log('');
|
|
66
|
+
db.close();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
wtCmd
|
|
70
|
+
.command('sync')
|
|
71
|
+
.description('Sync Git workspaces into the Switchman database')
|
|
72
|
+
.action(() => {
|
|
73
|
+
const repoRoot = getRepo();
|
|
74
|
+
const db = getDb(repoRoot);
|
|
75
|
+
const gitWorktrees = listGitWorktrees(repoRoot);
|
|
76
|
+
for (const wt of gitWorktrees) {
|
|
77
|
+
registerWorktree(db, { name: wt.name, path: wt.path, branch: wt.branch || 'unknown' });
|
|
78
|
+
}
|
|
79
|
+
db.close();
|
|
80
|
+
installMcpConfig([...new Set([repoRoot, ...gitWorktrees.map((wt) => wt.path)])]);
|
|
81
|
+
console.log(`${chalk.green('✓')} Synced ${gitWorktrees.length} worktree(s) from git`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return wtCmd;
|
|
85
|
+
}
|