switchman-dev 0.1.7 → 0.1.8
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/.github/workflows/ci.yml +26 -0
- package/CHANGELOG.md +36 -0
- package/CLAUDE.md +113 -0
- package/README.md +130 -16
- package/examples/README.md +9 -2
- package/package.json +6 -1
- package/src/cli/index.js +1413 -73
- package/src/core/ci.js +1 -1
- package/src/core/db.js +143 -21
- package/src/core/enforcement.js +122 -10
- package/src/core/ignore.js +1 -0
- package/src/core/licence.js +365 -0
- package/src/core/mcp.js +41 -2
- package/src/core/merge-gate.js +5 -3
- package/src/core/outcome.js +43 -44
- package/src/core/pipeline.js +66 -35
- package/src/core/planner.js +10 -6
- package/src/core/policy.js +1 -1
- package/src/core/queue.js +11 -2
- package/src/core/sync.js +216 -0
- package/src/mcp/server.js +18 -6
- package/tests.zip +0 -0
package/src/cli/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* switchman status - Show the repo dashboard
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { program } from 'commander';
|
|
19
|
+
import { Help, program } from 'commander';
|
|
20
20
|
import chalk from 'chalk';
|
|
21
21
|
import ora from 'ora';
|
|
22
22
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
@@ -24,7 +24,7 @@ import { tmpdir } from 'os';
|
|
|
24
24
|
import { dirname, join, posix } from 'path';
|
|
25
25
|
import { execSync, spawn } from 'child_process';
|
|
26
26
|
|
|
27
|
-
import { cleanupCrashedLandingTempWorktrees, findRepoRoot,
|
|
27
|
+
import { cleanupCrashedLandingTempWorktrees, createGitWorktree, findRepoRoot, getWorktreeBranch, getWorktreeChangedFiles, gitAssessBranchFreshness, gitBranchExists, listGitWorktrees } from '../core/git.js';
|
|
28
28
|
import { matchesPathPatterns } from '../core/ignore.js';
|
|
29
29
|
import {
|
|
30
30
|
initDb, openDb,
|
|
@@ -37,18 +37,20 @@ import {
|
|
|
37
37
|
createPolicyOverride, listPolicyOverrides, revokePolicyOverride,
|
|
38
38
|
finishOperationJournalEntry, listOperationJournal, listTempResources, updateTempResource,
|
|
39
39
|
claimFiles, releaseFileClaims, getActiveFileClaims, checkFileConflicts, retryTask,
|
|
40
|
-
|
|
40
|
+
upsertTaskSpec,
|
|
41
|
+
listAuditEvents, pruneDatabaseMaintenance, verifyAuditTrail,
|
|
41
42
|
} from '../core/db.js';
|
|
42
43
|
import { scanAllWorktrees } from '../core/detector.js';
|
|
43
|
-
import { getWindsurfMcpConfigPath, upsertAllProjectMcpConfigs, upsertWindsurfMcpConfig } from '../core/mcp.js';
|
|
44
|
-
import { gatewayAppendFile, gatewayMakeDirectory, gatewayMovePath, gatewayRemovePath, gatewayWriteFile, installGateHooks, monitorWorktreesOnce, runCommitGate, runWrappedCommand, writeEnforcementPolicy } from '../core/enforcement.js';
|
|
44
|
+
import { ensureProjectLocalMcpGitExcludes, getWindsurfMcpConfigPath, upsertAllProjectMcpConfigs, upsertWindsurfMcpConfig } from '../core/mcp.js';
|
|
45
|
+
import { evaluateRepoCompliance, gatewayAppendFile, gatewayMakeDirectory, gatewayMovePath, gatewayRemovePath, gatewayWriteFile, installGateHooks, monitorWorktreesOnce, runCommitGate, runWrappedCommand, writeEnforcementPolicy } from '../core/enforcement.js';
|
|
45
46
|
import { runAiMergeGate } from '../core/merge-gate.js';
|
|
46
47
|
import { clearMonitorState, getMonitorStatePath, isProcessRunning, readMonitorState, writeMonitorState } from '../core/monitor.js';
|
|
47
48
|
import { buildPipelinePrSummary, cleanupPipelineLandingRecovery, commentPipelinePr, createPipelineFollowupTasks, evaluatePipelinePolicyGate, executePipeline, exportPipelinePrBundle, getPipelineLandingBranchStatus, getPipelineLandingExplainReport, getPipelineStatus, inferPipelineIdFromBranch, materializePipelineLandingBranch, preparePipelineLandingRecovery, preparePipelineLandingTarget, publishPipelinePr, repairPipelineState, resumePipelineLandingRecovery, runPipeline, startPipeline, summarizePipelinePolicyState, syncPipelinePr } from '../core/pipeline.js';
|
|
48
49
|
import { installGitHubActionsWorkflow, resolveGitHubOutputTargets, writeGitHubCiStatus, writeGitHubPipelineLandingStatus } from '../core/ci.js';
|
|
49
50
|
import { importCodeObjectsToStore, listCodeObjects, materializeCodeObjects, materializeSemanticIndex, updateCodeObjectSource } from '../core/semantic.js';
|
|
50
|
-
import { buildQueueStatusSummary, resolveQueueSource, runMergeQueue } from '../core/queue.js';
|
|
51
|
+
import { buildQueueStatusSummary, evaluateQueueRepoGate, resolveQueueSource, runMergeQueue } from '../core/queue.js';
|
|
51
52
|
import { DEFAULT_CHANGE_POLICY, DEFAULT_LEASE_POLICY, getChangePolicyPath, loadChangePolicy, loadLeasePolicy, writeChangePolicy, writeLeasePolicy } from '../core/policy.js';
|
|
53
|
+
import { planPipelineTasks } from '../core/planner.js';
|
|
52
54
|
import {
|
|
53
55
|
captureTelemetryEvent,
|
|
54
56
|
disableTelemetry,
|
|
@@ -59,6 +61,9 @@ import {
|
|
|
59
61
|
maybePromptForTelemetry,
|
|
60
62
|
sendTelemetryEvent,
|
|
61
63
|
} from '../core/telemetry.js';
|
|
64
|
+
import { checkLicence, clearCredentials, FREE_AGENT_LIMIT, getRetentionDaysForCurrentPlan, loginWithGitHub, PRO_PAGE_URL, readCredentials } from '../core/licence.js';
|
|
65
|
+
import { homedir } from 'os';
|
|
66
|
+
import { cleanupOldSyncEvents, pullActiveTeamMembers, pullTeamState, pushSyncEvent } from '../core/sync.js';
|
|
62
67
|
|
|
63
68
|
const originalProcessEmit = process.emit.bind(process);
|
|
64
69
|
process.emit = function patchedProcessEmit(event, ...args) {
|
|
@@ -95,6 +100,177 @@ function getDb(repoRoot) {
|
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
function getOptionalDb(repoRoot) {
|
|
104
|
+
try {
|
|
105
|
+
return openDb(repoRoot);
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function slugifyValue(value) {
|
|
112
|
+
return String(value || '')
|
|
113
|
+
.toLowerCase()
|
|
114
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
115
|
+
.replace(/^-+|-+$/g, '')
|
|
116
|
+
.slice(0, 40) || 'plan';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function capitalizeSentence(value) {
|
|
120
|
+
const text = String(value || '').trim();
|
|
121
|
+
if (!text) return text;
|
|
122
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatHumanList(values = []) {
|
|
126
|
+
if (values.length === 0) return '';
|
|
127
|
+
if (values.length === 1) return values[0];
|
|
128
|
+
if (values.length === 2) return `${values[0]} and ${values[1]}`;
|
|
129
|
+
return `${values.slice(0, -1).join(', ')}, and ${values[values.length - 1]}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readPlanningFile(repoRoot, fileName, maxChars = 1200) {
|
|
133
|
+
const filePath = join(repoRoot, fileName);
|
|
134
|
+
if (!existsSync(filePath)) return null;
|
|
135
|
+
try {
|
|
136
|
+
const text = readFileSync(filePath, 'utf8').trim();
|
|
137
|
+
if (!text) return null;
|
|
138
|
+
return {
|
|
139
|
+
file: fileName,
|
|
140
|
+
text: text.slice(0, maxChars),
|
|
141
|
+
};
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function extractMarkdownSignal(text) {
|
|
148
|
+
const lines = String(text || '')
|
|
149
|
+
.split('\n')
|
|
150
|
+
.map((line) => line.trim())
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
const normalized = line.replace(/^#+\s*/, '').replace(/^[-*]\s+/, '').trim();
|
|
154
|
+
if (!normalized) continue;
|
|
155
|
+
if (/^switchman\b/i.test(normalized)) continue;
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function deriveGoalFromBranch(branchName) {
|
|
162
|
+
const raw = String(branchName || '').replace(/^refs\/heads\//, '').trim();
|
|
163
|
+
if (!raw || ['main', 'master', 'trunk', 'develop', 'development'].includes(raw)) return null;
|
|
164
|
+
const tail = raw.split('/').pop() || raw;
|
|
165
|
+
const tokens = tail
|
|
166
|
+
.replace(/^\d+[-_]?/, '')
|
|
167
|
+
.split(/[-_]/)
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.filter((token) => !['feature', 'feat', 'fix', 'bugfix', 'chore', 'task', 'issue', 'story', 'work'].includes(token.toLowerCase()));
|
|
170
|
+
if (tokens.length === 0) return null;
|
|
171
|
+
return capitalizeSentence(tokens.join(' '));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getRecentCommitSubjects(repoRoot, limit = 6) {
|
|
175
|
+
try {
|
|
176
|
+
return execSync(`git log --pretty=%s -n ${limit}`, {
|
|
177
|
+
cwd: repoRoot,
|
|
178
|
+
encoding: 'utf8',
|
|
179
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
180
|
+
}).trim().split('\n').map((line) => line.trim()).filter(Boolean);
|
|
181
|
+
} catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function summarizeRecentCommitContext(branchGoal, subjects) {
|
|
187
|
+
if (!subjects.length) return null;
|
|
188
|
+
const topicWords = String(branchGoal || '')
|
|
189
|
+
.toLowerCase()
|
|
190
|
+
.split(/\s+/)
|
|
191
|
+
.filter((word) => word.length >= 4);
|
|
192
|
+
const relatedCount = topicWords.length > 0
|
|
193
|
+
? subjects.filter((subject) => {
|
|
194
|
+
const lower = subject.toLowerCase();
|
|
195
|
+
return topicWords.some((word) => lower.includes(word));
|
|
196
|
+
}).length
|
|
197
|
+
: 0;
|
|
198
|
+
const effectiveCount = relatedCount > 0 ? relatedCount : Math.min(subjects.length, 3);
|
|
199
|
+
const topicLabel = relatedCount > 0 && topicWords.length > 0 ? `${topicWords[0]}-related ` : '';
|
|
200
|
+
return `${effectiveCount} recent ${topicLabel}commit${effectiveCount === 1 ? '' : 's'}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function collectPlanContext(repoRoot, explicitGoal = null) {
|
|
204
|
+
const planningFiles = ['CLAUDE.md', 'ROADMAP.md', 'tasks.md', 'TASKS.md', 'TODO.md', 'README.md']
|
|
205
|
+
.map((fileName) => readPlanningFile(repoRoot, fileName))
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
const planningByName = new Map(planningFiles.map((entry) => [entry.file, entry]));
|
|
208
|
+
const branch = getWorktreeBranch(process.cwd()) || null;
|
|
209
|
+
const branchGoal = deriveGoalFromBranch(branch);
|
|
210
|
+
const recentCommitSubjects = getRecentCommitSubjects(repoRoot, 6);
|
|
211
|
+
const recentCommitSummary = summarizeRecentCommitContext(branchGoal, recentCommitSubjects);
|
|
212
|
+
const preferredPlanningFile = planningByName.get('CLAUDE.md')
|
|
213
|
+
|| planningByName.get('tasks.md')
|
|
214
|
+
|| planningByName.get('TASKS.md')
|
|
215
|
+
|| planningByName.get('ROADMAP.md')
|
|
216
|
+
|| planningByName.get('TODO.md')
|
|
217
|
+
|| planningByName.get('README.md')
|
|
218
|
+
|| null;
|
|
219
|
+
const planningSignal = preferredPlanningFile ? extractMarkdownSignal(preferredPlanningFile.text) : null;
|
|
220
|
+
const title = capitalizeSentence(explicitGoal || branchGoal || planningSignal || 'Plan the next coordinated change');
|
|
221
|
+
const descriptionParts = [];
|
|
222
|
+
if (preferredPlanningFile?.text) descriptionParts.push(preferredPlanningFile.text);
|
|
223
|
+
if (recentCommitSubjects.length > 0) descriptionParts.push(`Recent git history summary: ${recentCommitSubjects.slice(0, 3).join('; ')}.`);
|
|
224
|
+
const description = descriptionParts.join('\n\n').trim() || null;
|
|
225
|
+
|
|
226
|
+
const found = [];
|
|
227
|
+
const used = [];
|
|
228
|
+
if (explicitGoal) {
|
|
229
|
+
used.push('explicit goal');
|
|
230
|
+
}
|
|
231
|
+
if (branch) {
|
|
232
|
+
found.push(`branch ${branch}`);
|
|
233
|
+
if (branchGoal) used.push('branch name');
|
|
234
|
+
}
|
|
235
|
+
if (preferredPlanningFile?.file) {
|
|
236
|
+
found.push(preferredPlanningFile.file);
|
|
237
|
+
used.push(preferredPlanningFile.file);
|
|
238
|
+
}
|
|
239
|
+
if (recentCommitSummary) {
|
|
240
|
+
found.push(recentCommitSummary);
|
|
241
|
+
used.push('recent git history');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
branch,
|
|
246
|
+
title,
|
|
247
|
+
description,
|
|
248
|
+
found,
|
|
249
|
+
used: [...new Set(used)],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function resolvePlanningWorktrees(repoRoot, db = null) {
|
|
254
|
+
if (db) {
|
|
255
|
+
const registered = listWorktrees(db)
|
|
256
|
+
.filter((worktree) => worktree.name !== 'main' && worktree.status !== 'missing')
|
|
257
|
+
.map((worktree) => ({ name: worktree.name, path: worktree.path, branch: worktree.branch }));
|
|
258
|
+
if (registered.length > 0) return registered;
|
|
259
|
+
}
|
|
260
|
+
return listGitWorktrees(repoRoot)
|
|
261
|
+
.filter((worktree) => !worktree.isMain)
|
|
262
|
+
.map((worktree) => ({ name: worktree.name, path: worktree.path, branch: worktree.branch || null }));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function planTaskPriority(taskSpec = null) {
|
|
266
|
+
const taskType = taskSpec?.task_type || 'implementation';
|
|
267
|
+
if (taskType === 'implementation') return 8;
|
|
268
|
+
if (taskType === 'tests') return 7;
|
|
269
|
+
if (taskType === 'docs') return 6;
|
|
270
|
+
if (taskType === 'governance') return 6;
|
|
271
|
+
return 5;
|
|
272
|
+
}
|
|
273
|
+
|
|
98
274
|
function resolvePrNumberFromEnv(env = process.env) {
|
|
99
275
|
if (env.SWITCHMAN_PR_NUMBER) return String(env.SWITCHMAN_PR_NUMBER);
|
|
100
276
|
if (env.GITHUB_PR_NUMBER) return String(env.GITHUB_PR_NUMBER);
|
|
@@ -1783,12 +1959,24 @@ function buildDoctorReport({ db, repoRoot, tasks, activeLeases, staleLeases, sca
|
|
|
1783
1959
|
};
|
|
1784
1960
|
});
|
|
1785
1961
|
|
|
1786
|
-
const
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1962
|
+
const worktreeByName = new Map((scanReport.worktrees || []).map((worktree) => [worktree.name, worktree]));
|
|
1963
|
+
const blockedWorktrees = scanReport.unclaimedChanges.map((entry) => {
|
|
1964
|
+
const worktreeInfo = worktreeByName.get(entry.worktree) || null;
|
|
1965
|
+
const reasonCode = entry.reasons?.[0]?.reason_code || null;
|
|
1966
|
+
const isDirtyWorktree = reasonCode === 'no_active_lease';
|
|
1967
|
+
return {
|
|
1968
|
+
worktree: entry.worktree,
|
|
1969
|
+
path: worktreeInfo?.path || null,
|
|
1970
|
+
files: entry.files,
|
|
1971
|
+
reason_code: reasonCode,
|
|
1972
|
+
next_step: isDirtyWorktree
|
|
1973
|
+
? 'commit or discard the changed files in that worktree, then rescan before continuing'
|
|
1974
|
+
: (nextStepForReason(reasonCode) || 'inspect the changed files and bring them back under Switchman claims'),
|
|
1975
|
+
command: worktreeInfo?.path
|
|
1976
|
+
? `cd ${JSON.stringify(worktreeInfo.path)} && git status`
|
|
1977
|
+
: 'switchman scan',
|
|
1978
|
+
};
|
|
1979
|
+
});
|
|
1792
1980
|
|
|
1793
1981
|
const fileConflicts = scanReport.fileConflicts.map((conflict) => ({
|
|
1794
1982
|
file: conflict.file,
|
|
@@ -1838,9 +2026,9 @@ function buildDoctorReport({ db, repoRoot, tasks, activeLeases, staleLeases, sca
|
|
|
1838
2026
|
...blockedWorktrees.map((entry) => ({
|
|
1839
2027
|
kind: 'unmanaged_changes',
|
|
1840
2028
|
title: `${entry.worktree} has unmanaged changed files`,
|
|
1841
|
-
detail: `${entry.files.slice(0,
|
|
2029
|
+
detail: `${entry.files.slice(0, 5).join(', ')}${entry.files.length > 5 ? ` +${entry.files.length - 5} more` : ''}${entry.path ? ` • ${entry.path}` : ''}`,
|
|
1842
2030
|
next_step: entry.next_step,
|
|
1843
|
-
command:
|
|
2031
|
+
command: entry.command,
|
|
1844
2032
|
severity: 'block',
|
|
1845
2033
|
})),
|
|
1846
2034
|
...fileConflicts.map((conflict) => ({
|
|
@@ -1997,6 +2185,7 @@ function buildUnifiedStatusReport({
|
|
|
1997
2185
|
queueItems,
|
|
1998
2186
|
queueSummary,
|
|
1999
2187
|
recentQueueEvents,
|
|
2188
|
+
retentionDays = 7,
|
|
2000
2189
|
}) {
|
|
2001
2190
|
const queueAttention = [
|
|
2002
2191
|
...queueItems
|
|
@@ -2101,6 +2290,7 @@ function buildUnifiedStatusReport({
|
|
|
2101
2290
|
...queueAttention.map((item) => item.next_step),
|
|
2102
2291
|
])].slice(0, 6),
|
|
2103
2292
|
suggested_commands: [...new Set(attention.length > 0 ? suggestedCommands : defaultSuggestedCommands)].slice(0, 6),
|
|
2293
|
+
retention_days: retentionDays,
|
|
2104
2294
|
};
|
|
2105
2295
|
}
|
|
2106
2296
|
|
|
@@ -2108,6 +2298,9 @@ async function collectStatusSnapshot(repoRoot) {
|
|
|
2108
2298
|
const db = getDb(repoRoot);
|
|
2109
2299
|
try {
|
|
2110
2300
|
const leasePolicy = loadLeasePolicy(repoRoot);
|
|
2301
|
+
const retentionDays = await getRetentionDaysForCurrentPlan();
|
|
2302
|
+
pruneDatabaseMaintenance(db, { retentionDays });
|
|
2303
|
+
cleanupOldSyncEvents({ retentionDays }).catch(() => {});
|
|
2111
2304
|
|
|
2112
2305
|
if (leasePolicy.reap_on_status_check) {
|
|
2113
2306
|
reapStaleLeases(db, leasePolicy.stale_after_minutes, {
|
|
@@ -2147,13 +2340,40 @@ async function collectStatusSnapshot(repoRoot) {
|
|
|
2147
2340
|
queueItems,
|
|
2148
2341
|
queueSummary,
|
|
2149
2342
|
recentQueueEvents,
|
|
2343
|
+
retentionDays,
|
|
2150
2344
|
});
|
|
2151
2345
|
} finally {
|
|
2152
2346
|
db.close();
|
|
2153
2347
|
}
|
|
2154
2348
|
}
|
|
2155
2349
|
|
|
2156
|
-
function
|
|
2350
|
+
function summarizeTeamCoordinationState(events = [], myUserId = null) {
|
|
2351
|
+
const visibleEvents = events.filter((event) => event.user_id !== myUserId);
|
|
2352
|
+
if (visibleEvents.length === 0) {
|
|
2353
|
+
return {
|
|
2354
|
+
members: 0,
|
|
2355
|
+
queue_events: 0,
|
|
2356
|
+
lease_events: 0,
|
|
2357
|
+
claim_events: 0,
|
|
2358
|
+
latest_queue_event: null,
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
const activeMembers = new Set(visibleEvents.map((event) => event.user_id || `${event.payload?.email || 'unknown'}:${event.worktree || 'unknown'}`));
|
|
2363
|
+
const queueEvents = visibleEvents.filter((event) => ['queue_added', 'queue_merged', 'queue_blocked'].includes(event.event_type));
|
|
2364
|
+
const leaseEvents = visibleEvents.filter((event) => ['lease_acquired', 'task_done', 'task_failed', 'task_retried'].includes(event.event_type));
|
|
2365
|
+
const claimEvents = visibleEvents.filter((event) => ['claim_added', 'claim_released'].includes(event.event_type));
|
|
2366
|
+
|
|
2367
|
+
return {
|
|
2368
|
+
members: activeMembers.size,
|
|
2369
|
+
queue_events: queueEvents.length,
|
|
2370
|
+
lease_events: leaseEvents.length,
|
|
2371
|
+
claim_events: claimEvents.length,
|
|
2372
|
+
latest_queue_event: queueEvents[0] || null,
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
function renderUnifiedStatusReport(report, { teamActivity = [], teamSummary = null } = {}) {
|
|
2157
2377
|
const healthColor = colorForHealth(report.health);
|
|
2158
2378
|
const badge = healthColor(healthLabel(report.health));
|
|
2159
2379
|
const mergeColor = report.merge_readiness.ci_gate_ok ? chalk.green : chalk.red;
|
|
@@ -2212,10 +2432,43 @@ function renderUnifiedStatusReport(report) {
|
|
|
2212
2432
|
console.log(`${chalk.bold('Run next:')} ${chalk.cyan(primaryCommand)}`);
|
|
2213
2433
|
console.log(`${chalk.dim('why:')} ${nextStepLine}`);
|
|
2214
2434
|
console.log(chalk.dim(`policy: ${formatRelativePolicy(report.lease_policy)} • requeue on reap ${report.lease_policy.requeue_task_on_reap ? 'on' : 'off'}`));
|
|
2435
|
+
console.log(chalk.dim(`history retention: ${report.retention_days || 7} days`));
|
|
2215
2436
|
if (report.merge_readiness.policy_state?.active) {
|
|
2216
2437
|
console.log(chalk.dim(`change policy: ${report.merge_readiness.policy_state.domains.join(', ')} • ${report.merge_readiness.policy_state.enforcement} • missing ${report.merge_readiness.policy_state.missing_task_types.join(', ') || 'none'}`));
|
|
2217
2438
|
}
|
|
2218
2439
|
|
|
2440
|
+
// ── Team activity (Pro cloud sync) ──────────────────────────────────────────
|
|
2441
|
+
if (teamSummary && teamSummary.members > 0) {
|
|
2442
|
+
console.log('');
|
|
2443
|
+
console.log(chalk.bold('Shared cloud state:'));
|
|
2444
|
+
console.log(` ${chalk.dim('members:')} ${teamSummary.members} ${chalk.dim('leases:')} ${teamSummary.lease_events} ${chalk.dim('claims:')} ${teamSummary.claim_events} ${chalk.dim('queue:')} ${teamSummary.queue_events}`);
|
|
2445
|
+
if (teamSummary.latest_queue_event) {
|
|
2446
|
+
console.log(` ${chalk.dim('latest queue event:')} ${chalk.cyan(teamSummary.latest_queue_event.event_type)} ${chalk.dim(teamSummary.latest_queue_event.payload?.source_ref || teamSummary.latest_queue_event.payload?.item_id || '')}`.trim());
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
if (teamActivity.length > 0) {
|
|
2450
|
+
console.log('');
|
|
2451
|
+
console.log(chalk.bold('Team activity:'));
|
|
2452
|
+
for (const member of teamActivity) {
|
|
2453
|
+
const email = member.payload?.email ?? chalk.dim(member.user_id?.slice(0, 8) ?? 'unknown');
|
|
2454
|
+
const worktree = chalk.cyan(member.worktree ?? 'unknown');
|
|
2455
|
+
const eventLabel = {
|
|
2456
|
+
task_added: 'added a task',
|
|
2457
|
+
task_done: 'completed a task',
|
|
2458
|
+
task_failed: 'failed a task',
|
|
2459
|
+
task_retried: 'retried a task',
|
|
2460
|
+
lease_acquired: `working on: ${chalk.dim(member.payload?.title ?? '')}`,
|
|
2461
|
+
claim_added: `claimed ${chalk.dim(member.payload?.file_count ?? 0)} file(s)`,
|
|
2462
|
+
claim_released: 'released file claims',
|
|
2463
|
+
queue_added: `queued ${chalk.dim(member.payload?.source_ref ?? member.payload?.item_id ?? 'work')}`,
|
|
2464
|
+
queue_merged: `landed ${chalk.dim(member.payload?.source_ref ?? member.payload?.item_id ?? 'work')}`,
|
|
2465
|
+
queue_blocked: `blocked ${chalk.dim(member.payload?.source_ref ?? member.payload?.item_id ?? 'work')}`,
|
|
2466
|
+
status_ping: 'active',
|
|
2467
|
+
}[member.event_type] ?? member.event_type;
|
|
2468
|
+
console.log(` ${chalk.dim('○')} ${email} · ${worktree} · ${eventLabel}`);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2219
2472
|
const runningLines = report.active_work.length > 0
|
|
2220
2473
|
? report.active_work.slice(0, 5).map((item) => {
|
|
2221
2474
|
const boundary = item.boundary_validation
|
|
@@ -2230,6 +2483,27 @@ function renderUnifiedStatusReport(report) {
|
|
|
2230
2483
|
|
|
2231
2484
|
const blockedItems = report.attention.filter((item) => item.severity === 'block');
|
|
2232
2485
|
const warningItems = report.attention.filter((item) => item.severity !== 'block');
|
|
2486
|
+
const isQuietEmptyState = report.active_work.length === 0
|
|
2487
|
+
&& blockedItems.length === 0
|
|
2488
|
+
&& warningItems.length === 0
|
|
2489
|
+
&& report.queue.items.length === 0
|
|
2490
|
+
&& report.next_up.length === 0
|
|
2491
|
+
&& report.failed_tasks.length === 0;
|
|
2492
|
+
|
|
2493
|
+
if (isQuietEmptyState) {
|
|
2494
|
+
console.log('');
|
|
2495
|
+
console.log(healthColor('='.repeat(72)));
|
|
2496
|
+
console.log(`${badge} ${chalk.bold('switchman status')} ${chalk.dim('• mission control for parallel agents')}`);
|
|
2497
|
+
console.log(`${chalk.dim(report.repo_root)}`);
|
|
2498
|
+
console.log(`${chalk.dim(report.summary)}`);
|
|
2499
|
+
console.log(healthColor('='.repeat(72)));
|
|
2500
|
+
console.log('');
|
|
2501
|
+
console.log(chalk.green('Nothing is running yet.'));
|
|
2502
|
+
console.log(`Add work with: ${chalk.cyan('switchman task add "Your first task" --priority 8')}`);
|
|
2503
|
+
console.log(`Or prove the flow in 30 seconds with: ${chalk.cyan('switchman demo')}`);
|
|
2504
|
+
console.log('');
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2233
2507
|
|
|
2234
2508
|
const blockedLines = blockedItems.length > 0
|
|
2235
2509
|
? blockedItems.slice(0, 4).flatMap((item) => {
|
|
@@ -2405,10 +2679,12 @@ function completeTaskWithRetries(repoRoot, taskId, attempts = 20) {
|
|
|
2405
2679
|
let db = null;
|
|
2406
2680
|
try {
|
|
2407
2681
|
db = openDb(repoRoot);
|
|
2408
|
-
completeTask(db, taskId);
|
|
2409
|
-
|
|
2682
|
+
const result = completeTask(db, taskId);
|
|
2683
|
+
if (result?.status === 'completed') {
|
|
2684
|
+
releaseFileClaims(db, taskId);
|
|
2685
|
+
}
|
|
2410
2686
|
db.close();
|
|
2411
|
-
return;
|
|
2687
|
+
return result;
|
|
2412
2688
|
} catch (err) {
|
|
2413
2689
|
lastError = err;
|
|
2414
2690
|
try { db?.close(); } catch { /* no-op */ }
|
|
@@ -2421,6 +2697,210 @@ function completeTaskWithRetries(repoRoot, taskId, attempts = 20) {
|
|
|
2421
2697
|
throw lastError;
|
|
2422
2698
|
}
|
|
2423
2699
|
|
|
2700
|
+
function startBackgroundMonitor(repoRoot, { intervalMs = 2000, quarantine = false } = {}) {
|
|
2701
|
+
const existingState = readMonitorState(repoRoot);
|
|
2702
|
+
if (existingState && isProcessRunning(existingState.pid)) {
|
|
2703
|
+
return {
|
|
2704
|
+
already_running: true,
|
|
2705
|
+
state: existingState,
|
|
2706
|
+
state_path: getMonitorStatePath(repoRoot),
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
const logPath = join(repoRoot, '.switchman', 'monitor.log');
|
|
2711
|
+
const child = spawn(process.execPath, [
|
|
2712
|
+
process.argv[1],
|
|
2713
|
+
'monitor',
|
|
2714
|
+
'watch',
|
|
2715
|
+
'--interval-ms',
|
|
2716
|
+
String(intervalMs),
|
|
2717
|
+
...(quarantine ? ['--quarantine'] : []),
|
|
2718
|
+
'--daemonized',
|
|
2719
|
+
], {
|
|
2720
|
+
cwd: repoRoot,
|
|
2721
|
+
detached: true,
|
|
2722
|
+
stdio: 'ignore',
|
|
2723
|
+
});
|
|
2724
|
+
child.unref();
|
|
2725
|
+
|
|
2726
|
+
const statePath = writeMonitorState(repoRoot, {
|
|
2727
|
+
pid: child.pid,
|
|
2728
|
+
interval_ms: intervalMs,
|
|
2729
|
+
quarantine: Boolean(quarantine),
|
|
2730
|
+
log_path: logPath,
|
|
2731
|
+
started_at: new Date().toISOString(),
|
|
2732
|
+
});
|
|
2733
|
+
|
|
2734
|
+
return {
|
|
2735
|
+
already_running: false,
|
|
2736
|
+
state: readMonitorState(repoRoot),
|
|
2737
|
+
state_path: statePath,
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
function renderMonitorEvent(event) {
|
|
2742
|
+
const ownerText = event.owner_worktree
|
|
2743
|
+
? `${event.owner_worktree}${event.owner_task_id ? ` (${event.owner_task_id})` : ''}`
|
|
2744
|
+
: null;
|
|
2745
|
+
const claimCommand = event.task_id
|
|
2746
|
+
? `switchman claim ${event.task_id} ${event.worktree} ${event.file_path}`
|
|
2747
|
+
: null;
|
|
2748
|
+
|
|
2749
|
+
if (event.status === 'denied') {
|
|
2750
|
+
console.log(`${chalk.yellow('⚠')} ${chalk.cyan(event.worktree)} modified ${chalk.yellow(event.file_path)} without governed ownership`);
|
|
2751
|
+
if (ownerText) {
|
|
2752
|
+
console.log(` ${chalk.dim('Owned by:')} ${chalk.cyan(ownerText)}${event.owner_task_title ? ` ${chalk.dim(`— ${event.owner_task_title}`)}` : ''}`);
|
|
2753
|
+
}
|
|
2754
|
+
if (claimCommand) {
|
|
2755
|
+
console.log(` ${chalk.dim('Run:')} ${chalk.cyan(claimCommand)}`);
|
|
2756
|
+
}
|
|
2757
|
+
console.log(` ${chalk.dim('Or:')} ${chalk.cyan('switchman status')} ${chalk.dim('to inspect current claims and blockers')}`);
|
|
2758
|
+
if (event.enforcement_action) {
|
|
2759
|
+
console.log(` ${chalk.dim('Action:')} ${event.enforcement_action}`);
|
|
2760
|
+
}
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
const ownerSuffix = ownerText ? ` ${chalk.dim(`(${ownerText})`)}` : '';
|
|
2765
|
+
console.log(`${chalk.green('✓')} ${chalk.cyan(event.worktree)} ${chalk.yellow(event.file_path)} ${chalk.dim(event.change_type)}${ownerSuffix}`);
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
function resolveMonitoredWorktrees(db, repoRoot) {
|
|
2769
|
+
const registeredByPath = new Map(
|
|
2770
|
+
listWorktrees(db)
|
|
2771
|
+
.filter((worktree) => worktree.path)
|
|
2772
|
+
.map((worktree) => [worktree.path, worktree])
|
|
2773
|
+
);
|
|
2774
|
+
|
|
2775
|
+
return listGitWorktrees(repoRoot).map((worktree) => {
|
|
2776
|
+
const registered = registeredByPath.get(worktree.path);
|
|
2777
|
+
if (!registered) return worktree;
|
|
2778
|
+
return {
|
|
2779
|
+
...worktree,
|
|
2780
|
+
name: registered.name,
|
|
2781
|
+
path: registered.path || worktree.path,
|
|
2782
|
+
branch: registered.branch || worktree.branch,
|
|
2783
|
+
};
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
function discoverMergeCandidates(db, repoRoot, { targetBranch = 'main' } = {}) {
|
|
2788
|
+
const worktrees = listWorktrees(db).filter((worktree) => worktree.name !== 'main');
|
|
2789
|
+
const activeLeases = new Set(listLeases(db, 'active').map((lease) => lease.worktree));
|
|
2790
|
+
const tasks = listTasks(db);
|
|
2791
|
+
const queueItems = listMergeQueue(db).filter((item) => item.status !== 'merged');
|
|
2792
|
+
const alreadyQueued = new Set(queueItems.map((item) => item.source_worktree).filter(Boolean));
|
|
2793
|
+
|
|
2794
|
+
const eligible = [];
|
|
2795
|
+
const blocked = [];
|
|
2796
|
+
const skipped = [];
|
|
2797
|
+
|
|
2798
|
+
for (const worktree of worktrees) {
|
|
2799
|
+
const doneTasks = tasks.filter((task) => task.worktree === worktree.name && task.status === 'done');
|
|
2800
|
+
if (doneTasks.length === 0) {
|
|
2801
|
+
skipped.push({
|
|
2802
|
+
worktree: worktree.name,
|
|
2803
|
+
branch: worktree.branch,
|
|
2804
|
+
reason: 'no_completed_tasks',
|
|
2805
|
+
summary: 'no completed tasks are assigned to this worktree yet',
|
|
2806
|
+
command: `switchman task list --status done`,
|
|
2807
|
+
});
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
if (!worktree.branch || worktree.branch === targetBranch) {
|
|
2812
|
+
skipped.push({
|
|
2813
|
+
worktree: worktree.name,
|
|
2814
|
+
branch: worktree.branch || null,
|
|
2815
|
+
reason: 'no_merge_branch',
|
|
2816
|
+
summary: `worktree is on ${targetBranch} already`,
|
|
2817
|
+
command: `switchman worktree list`,
|
|
2818
|
+
});
|
|
2819
|
+
continue;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
if (!gitBranchExists(repoRoot, worktree.branch)) {
|
|
2823
|
+
blocked.push({
|
|
2824
|
+
worktree: worktree.name,
|
|
2825
|
+
branch: worktree.branch,
|
|
2826
|
+
reason: 'missing_branch',
|
|
2827
|
+
summary: `branch ${worktree.branch} is not available in git`,
|
|
2828
|
+
command: `switchman worktree sync`,
|
|
2829
|
+
});
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
if (activeLeases.has(worktree.name)) {
|
|
2834
|
+
blocked.push({
|
|
2835
|
+
worktree: worktree.name,
|
|
2836
|
+
branch: worktree.branch,
|
|
2837
|
+
reason: 'active_lease',
|
|
2838
|
+
summary: 'an active lease is still running in this worktree',
|
|
2839
|
+
command: `switchman status`,
|
|
2840
|
+
});
|
|
2841
|
+
continue;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
if (alreadyQueued.has(worktree.name)) {
|
|
2845
|
+
skipped.push({
|
|
2846
|
+
worktree: worktree.name,
|
|
2847
|
+
branch: worktree.branch,
|
|
2848
|
+
reason: 'already_queued',
|
|
2849
|
+
summary: 'worktree is already in the landing queue',
|
|
2850
|
+
command: `switchman queue status`,
|
|
2851
|
+
});
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
const dirtyFiles = getWorktreeChangedFiles(worktree.path, repoRoot);
|
|
2856
|
+
if (dirtyFiles.length > 0) {
|
|
2857
|
+
blocked.push({
|
|
2858
|
+
worktree: worktree.name,
|
|
2859
|
+
branch: worktree.branch,
|
|
2860
|
+
path: worktree.path,
|
|
2861
|
+
files: dirtyFiles,
|
|
2862
|
+
reason: 'dirty_worktree',
|
|
2863
|
+
summary: `worktree has uncommitted changes: ${dirtyFiles.slice(0, 5).join(', ')}${dirtyFiles.length > 5 ? ` +${dirtyFiles.length - 5} more` : ''}`,
|
|
2864
|
+
command: `cd ${JSON.stringify(worktree.path)} && git status`,
|
|
2865
|
+
});
|
|
2866
|
+
continue;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
const freshness = gitAssessBranchFreshness(repoRoot, targetBranch, worktree.branch);
|
|
2870
|
+
eligible.push({
|
|
2871
|
+
worktree: worktree.name,
|
|
2872
|
+
branch: worktree.branch,
|
|
2873
|
+
path: worktree.path,
|
|
2874
|
+
done_task_count: doneTasks.length,
|
|
2875
|
+
done_task_titles: doneTasks.slice(0, 3).map((task) => task.title),
|
|
2876
|
+
freshness,
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
return { eligible, blocked, skipped, queue_items: queueItems };
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
function printMergeDiscovery(discovery) {
|
|
2884
|
+
console.log('');
|
|
2885
|
+
console.log(chalk.bold(`Checking ${discovery.eligible.length + discovery.blocked.length + discovery.skipped.length} worktree(s)...`));
|
|
2886
|
+
|
|
2887
|
+
for (const entry of discovery.eligible) {
|
|
2888
|
+
const freshness = entry.freshness?.state && entry.freshness.state !== 'unknown'
|
|
2889
|
+
? ` ${chalk.dim(`(${entry.freshness.state})`)}`
|
|
2890
|
+
: '';
|
|
2891
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(entry.worktree)} ${chalk.dim(entry.branch)}${freshness}`);
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
for (const entry of discovery.blocked) {
|
|
2895
|
+
console.log(` ${chalk.yellow('!')} ${chalk.cyan(entry.worktree)} ${chalk.dim(entry.branch || 'no branch')} ${chalk.dim(`— ${entry.summary}`)}`);
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
for (const entry of discovery.skipped) {
|
|
2899
|
+
if (entry.reason === 'no_completed_tasks') continue;
|
|
2900
|
+
console.log(` ${chalk.dim('·')} ${chalk.cyan(entry.worktree)} ${chalk.dim(entry.branch || 'no branch')} ${chalk.dim(`— ${entry.summary}`)}`);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2424
2904
|
// ─── Program ──────────────────────────────────────────────────────────────────
|
|
2425
2905
|
|
|
2426
2906
|
program
|
|
@@ -2429,23 +2909,87 @@ program
|
|
|
2429
2909
|
.version('0.1.0');
|
|
2430
2910
|
|
|
2431
2911
|
program.showHelpAfterError('(run with --help for usage examples)');
|
|
2912
|
+
const ROOT_HELP_COMMANDS = new Set([
|
|
2913
|
+
'advanced',
|
|
2914
|
+
'demo',
|
|
2915
|
+
'setup',
|
|
2916
|
+
'verify-setup',
|
|
2917
|
+
'login',
|
|
2918
|
+
'upgrade',
|
|
2919
|
+
'plan',
|
|
2920
|
+
'task',
|
|
2921
|
+
'status',
|
|
2922
|
+
'merge',
|
|
2923
|
+
'repair',
|
|
2924
|
+
'help',
|
|
2925
|
+
]);
|
|
2926
|
+
program.configureHelp({
|
|
2927
|
+
visibleCommands(cmd) {
|
|
2928
|
+
const commands = Help.prototype.visibleCommands.call(this, cmd);
|
|
2929
|
+
if (cmd.parent) return commands;
|
|
2930
|
+
return commands.filter((command) => ROOT_HELP_COMMANDS.has(command.name()) && !command._switchmanAdvanced);
|
|
2931
|
+
},
|
|
2932
|
+
});
|
|
2432
2933
|
program.addHelpText('after', `
|
|
2433
2934
|
Start here:
|
|
2434
2935
|
switchman demo
|
|
2435
|
-
switchman setup --agents
|
|
2936
|
+
switchman setup --agents 3
|
|
2937
|
+
switchman task add "Your task" --priority 8
|
|
2436
2938
|
switchman status --watch
|
|
2437
|
-
switchman gate ci
|
|
2939
|
+
switchman gate ci && switchman queue run
|
|
2438
2940
|
|
|
2439
|
-
|
|
2440
|
-
switchman
|
|
2441
|
-
switchman
|
|
2442
|
-
switchman
|
|
2941
|
+
For you (the operator):
|
|
2942
|
+
switchman demo
|
|
2943
|
+
switchman setup
|
|
2944
|
+
switchman task add
|
|
2945
|
+
switchman status
|
|
2946
|
+
switchman merge
|
|
2947
|
+
switchman repair
|
|
2948
|
+
switchman upgrade
|
|
2949
|
+
switchman login
|
|
2950
|
+
switchman plan "Add authentication" (Pro)
|
|
2951
|
+
|
|
2952
|
+
For your agents (via CLAUDE.md or MCP):
|
|
2953
|
+
switchman lease next
|
|
2954
|
+
switchman claim
|
|
2955
|
+
switchman task done
|
|
2956
|
+
switchman write
|
|
2957
|
+
switchman wrap
|
|
2443
2958
|
|
|
2444
2959
|
Docs:
|
|
2445
2960
|
README.md
|
|
2446
|
-
docs/setup-
|
|
2961
|
+
docs/setup-claude-code.md
|
|
2962
|
+
|
|
2963
|
+
Power tools:
|
|
2964
|
+
switchman advanced --help
|
|
2447
2965
|
`);
|
|
2448
2966
|
|
|
2967
|
+
const advancedCmd = program
|
|
2968
|
+
.command('advanced')
|
|
2969
|
+
.description('Show advanced, experimental, and power-user command groups')
|
|
2970
|
+
.addHelpText('after', `
|
|
2971
|
+
Advanced operator commands:
|
|
2972
|
+
switchman pipeline <...>
|
|
2973
|
+
switchman audit <...>
|
|
2974
|
+
switchman policy <...>
|
|
2975
|
+
switchman monitor <...>
|
|
2976
|
+
switchman repair
|
|
2977
|
+
|
|
2978
|
+
Experimental commands:
|
|
2979
|
+
switchman semantic <...>
|
|
2980
|
+
switchman object <...>
|
|
2981
|
+
|
|
2982
|
+
Compatibility aliases:
|
|
2983
|
+
switchman doctor
|
|
2984
|
+
|
|
2985
|
+
Tip:
|
|
2986
|
+
The main help keeps the day-one workflow small on purpose.
|
|
2987
|
+
`)
|
|
2988
|
+
.action(() => {
|
|
2989
|
+
advancedCmd.outputHelp();
|
|
2990
|
+
});
|
|
2991
|
+
advancedCmd._switchmanAdvanced = false;
|
|
2992
|
+
|
|
2449
2993
|
program
|
|
2450
2994
|
.command('demo')
|
|
2451
2995
|
.description('Create a throwaway repo that proves overlapping claims are blocked and safe landing works')
|
|
@@ -2508,12 +3052,16 @@ program
|
|
|
2508
3052
|
}
|
|
2509
3053
|
|
|
2510
3054
|
const mcpConfigWrites = installMcpConfig([...new Set([repoRoot, ...gitWorktrees.map((wt) => wt.path)])]);
|
|
3055
|
+
const mcpExclude = ensureProjectLocalMcpGitExcludes(repoRoot);
|
|
2511
3056
|
|
|
2512
3057
|
db.close();
|
|
2513
3058
|
spinner.succeed(`Initialized in ${chalk.cyan(repoRoot)}`);
|
|
2514
3059
|
console.log(chalk.dim(` Found and registered ${gitWorktrees.length} git worktree(s)`));
|
|
2515
3060
|
console.log(chalk.dim(` Database: .switchman/switchman.db`));
|
|
2516
3061
|
console.log(chalk.dim(` MCP config: ${mcpConfigWrites.filter((result) => result.changed).length} file(s) written`));
|
|
3062
|
+
if (mcpExclude.managed) {
|
|
3063
|
+
console.log(chalk.dim(` MCP excludes: ${mcpExclude.changed ? 'updated' : 'already set'} in .git/info/exclude`));
|
|
3064
|
+
}
|
|
2517
3065
|
console.log('');
|
|
2518
3066
|
console.log(`Next steps:`);
|
|
2519
3067
|
console.log(` ${chalk.cyan('switchman task add "Fix the login bug"')} — add a task`);
|
|
@@ -2533,6 +3081,8 @@ program
|
|
|
2533
3081
|
.description('One-command setup: create agent workspaces and initialise Switchman')
|
|
2534
3082
|
.option('-a, --agents <n>', 'Number of agent workspaces to create (default: 3)', '3')
|
|
2535
3083
|
.option('--prefix <prefix>', 'Branch prefix (default: switchman)', 'switchman')
|
|
3084
|
+
.option('--no-monitor', 'Do not start the background rogue-edit monitor')
|
|
3085
|
+
.option('--monitor-interval-ms <ms>', 'Polling interval for the background monitor', '2000')
|
|
2536
3086
|
.addHelpText('after', `
|
|
2537
3087
|
Examples:
|
|
2538
3088
|
switchman setup --agents 5
|
|
@@ -2546,6 +3096,24 @@ Examples:
|
|
|
2546
3096
|
process.exit(1);
|
|
2547
3097
|
}
|
|
2548
3098
|
|
|
3099
|
+
if (agentCount > FREE_AGENT_LIMIT) {
|
|
3100
|
+
const licence = await checkLicence();
|
|
3101
|
+
if (!licence.valid) {
|
|
3102
|
+
console.log('');
|
|
3103
|
+
console.log(chalk.yellow(` ⚠ Free tier supports up to ${FREE_AGENT_LIMIT} agents.`));
|
|
3104
|
+
console.log('');
|
|
3105
|
+
console.log(` You requested ${chalk.cyan(agentCount)} agents — that requires ${chalk.bold('Switchman Pro')}.`);
|
|
3106
|
+
console.log('');
|
|
3107
|
+
console.log(` ${chalk.dim('Upgrade at:')} ${chalk.cyan(PRO_PAGE_URL)}`);
|
|
3108
|
+
console.log(` ${chalk.dim('Or run: ')} ${chalk.cyan('switchman upgrade')}`);
|
|
3109
|
+
console.log('');
|
|
3110
|
+
process.exit(1);
|
|
3111
|
+
}
|
|
3112
|
+
if (licence.offline) {
|
|
3113
|
+
console.log(chalk.dim(` Pro licence verified (offline cache · ${Math.ceil((7 * 24 * 60 * 60 * 1000 - (Date.now() - (licence.cached_at ?? 0))) / (24 * 60 * 60 * 1000))}d remaining)`));
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
|
|
2549
3117
|
const repoRoot = getRepo();
|
|
2550
3118
|
const spinner = ora('Setting up Switchman...').start();
|
|
2551
3119
|
|
|
@@ -2591,6 +3159,12 @@ Examples:
|
|
|
2591
3159
|
}
|
|
2592
3160
|
|
|
2593
3161
|
const mcpConfigWrites = installMcpConfig([...new Set([repoRoot, ...created.map((wt) => wt.path)])]);
|
|
3162
|
+
const mcpExclude = ensureProjectLocalMcpGitExcludes(repoRoot);
|
|
3163
|
+
|
|
3164
|
+
const monitorIntervalMs = Math.max(100, Number.parseInt(opts.monitorIntervalMs, 10) || 2000);
|
|
3165
|
+
const monitorState = opts.monitor
|
|
3166
|
+
? startBackgroundMonitor(repoRoot, { intervalMs: monitorIntervalMs, quarantine: false })
|
|
3167
|
+
: null;
|
|
2594
3168
|
|
|
2595
3169
|
db.close();
|
|
2596
3170
|
|
|
@@ -2610,6 +3184,20 @@ Examples:
|
|
|
2610
3184
|
const status = result.created ? 'created' : result.changed ? 'updated' : 'unchanged';
|
|
2611
3185
|
console.log(` ${chalk.green('✓')} ${chalk.cyan(result.path)} ${chalk.dim(`(${status})`)}`);
|
|
2612
3186
|
}
|
|
3187
|
+
if (mcpExclude.managed) {
|
|
3188
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(mcpExclude.path)} ${chalk.dim(`(${mcpExclude.changed ? 'updated' : 'unchanged'})`)}`);
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
if (opts.monitor) {
|
|
3192
|
+
console.log('');
|
|
3193
|
+
console.log(chalk.bold('Monitor:'));
|
|
3194
|
+
if (monitorState?.already_running) {
|
|
3195
|
+
console.log(` ${chalk.green('✓')} Background rogue-edit monitor already running ${chalk.dim(`(pid ${monitorState.state.pid})`)}`);
|
|
3196
|
+
} else {
|
|
3197
|
+
console.log(` ${chalk.green('✓')} Started background rogue-edit monitor ${chalk.dim(`(pid ${monitorState?.state?.pid ?? 'unknown'})`)}`);
|
|
3198
|
+
}
|
|
3199
|
+
console.log(` ${chalk.dim('interval:')} ${monitorIntervalMs}ms`);
|
|
3200
|
+
}
|
|
2613
3201
|
|
|
2614
3202
|
console.log('');
|
|
2615
3203
|
console.log(chalk.bold('Next steps:'));
|
|
@@ -2618,6 +3206,13 @@ Examples:
|
|
|
2618
3206
|
console.log(` 2. Open Claude Code or Cursor in the workspaces above — the local MCP config will attach Switchman automatically`);
|
|
2619
3207
|
console.log(` 3. Keep the repo dashboard open while work starts:`);
|
|
2620
3208
|
console.log(` ${chalk.cyan('switchman status --watch')}`);
|
|
3209
|
+
console.log(` 4. Run the final check and land finished work:`);
|
|
3210
|
+
console.log(` ${chalk.cyan('switchman gate ci')}`);
|
|
3211
|
+
console.log(` ${chalk.cyan('switchman queue run')}`);
|
|
3212
|
+
if (opts.monitor) {
|
|
3213
|
+
console.log(` 5. Watch for rogue edits or direct writes in real time:`);
|
|
3214
|
+
console.log(` ${chalk.cyan('switchman monitor status')}`);
|
|
3215
|
+
}
|
|
2621
3216
|
console.log('');
|
|
2622
3217
|
|
|
2623
3218
|
const verification = collectSetupVerification(repoRoot);
|
|
@@ -2800,6 +3395,132 @@ Examples:
|
|
|
2800
3395
|
});
|
|
2801
3396
|
|
|
2802
3397
|
|
|
3398
|
+
// ── plan ──────────────────────────────────────────────────────────────────────
|
|
3399
|
+
|
|
3400
|
+
program
|
|
3401
|
+
.command('plan [goal]')
|
|
3402
|
+
.description('Pro: suggest a parallel task plan from an explicit goal')
|
|
3403
|
+
.option('--apply', 'Create the suggested tasks in Switchman')
|
|
3404
|
+
.option('--max-tasks <n>', 'Maximum number of suggested tasks', '6')
|
|
3405
|
+
.option('--json', 'Output raw JSON')
|
|
3406
|
+
.addHelpText('after', `
|
|
3407
|
+
Examples:
|
|
3408
|
+
switchman plan "Add authentication"
|
|
3409
|
+
switchman plan "Add authentication" --apply
|
|
3410
|
+
`)
|
|
3411
|
+
.action(async (goal, opts) => {
|
|
3412
|
+
const repoRoot = getRepo();
|
|
3413
|
+
const db = getOptionalDb(repoRoot);
|
|
3414
|
+
|
|
3415
|
+
try {
|
|
3416
|
+
const licence = await checkLicence();
|
|
3417
|
+
if (!licence.valid) {
|
|
3418
|
+
console.log('');
|
|
3419
|
+
console.log(chalk.yellow(' ⚠ AI planning requires Switchman Pro.'));
|
|
3420
|
+
console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman upgrade')}`);
|
|
3421
|
+
console.log(` ${chalk.dim('Or visit:')} ${chalk.cyan(PRO_PAGE_URL)}`);
|
|
3422
|
+
console.log('');
|
|
3423
|
+
process.exitCode = 1;
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
if (!goal || !goal.trim()) {
|
|
3428
|
+
console.log('');
|
|
3429
|
+
console.log(chalk.yellow(' ⚠ AI planning currently requires an explicit goal.'));
|
|
3430
|
+
console.log(` ${chalk.dim('Try:')} ${chalk.cyan('switchman plan "Add authentication"')}`);
|
|
3431
|
+
console.log(` ${chalk.dim('Then:')} ${chalk.cyan('switchman plan "Add authentication" --apply')}`);
|
|
3432
|
+
console.log('');
|
|
3433
|
+
process.exitCode = 1;
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
const context = collectPlanContext(repoRoot, goal || null);
|
|
3438
|
+
const planningWorktrees = resolvePlanningWorktrees(repoRoot, db);
|
|
3439
|
+
const pipelineId = `plan-${slugifyValue(context.title)}-${Date.now().toString(36)}`;
|
|
3440
|
+
const plannedTasks = planPipelineTasks({
|
|
3441
|
+
pipelineId,
|
|
3442
|
+
title: context.title,
|
|
3443
|
+
description: context.description,
|
|
3444
|
+
worktrees: planningWorktrees,
|
|
3445
|
+
maxTasks: Math.max(1, parseInt(opts.maxTasks, 10) || 6),
|
|
3446
|
+
repoRoot,
|
|
3447
|
+
});
|
|
3448
|
+
|
|
3449
|
+
if (opts.json) {
|
|
3450
|
+
const payload = {
|
|
3451
|
+
title: context.title,
|
|
3452
|
+
context: {
|
|
3453
|
+
found: context.found,
|
|
3454
|
+
used: context.used,
|
|
3455
|
+
branch: context.branch,
|
|
3456
|
+
},
|
|
3457
|
+
planned_tasks: plannedTasks.map((task) => ({
|
|
3458
|
+
id: task.id,
|
|
3459
|
+
title: task.title,
|
|
3460
|
+
suggested_worktree: task.suggested_worktree || null,
|
|
3461
|
+
task_type: task.task_spec?.task_type || null,
|
|
3462
|
+
dependencies: task.dependencies || [],
|
|
3463
|
+
})),
|
|
3464
|
+
apply_ready: Boolean(db),
|
|
3465
|
+
};
|
|
3466
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
console.log(chalk.bold('Reading repo context...'));
|
|
3471
|
+
if (context.found.length > 0) {
|
|
3472
|
+
console.log(`${chalk.dim('Found:')} ${context.found.join(', ')}`);
|
|
3473
|
+
} else {
|
|
3474
|
+
console.log(chalk.dim('Found: local repo context only'));
|
|
3475
|
+
}
|
|
3476
|
+
console.log('');
|
|
3477
|
+
console.log(`${chalk.bold('Suggested plan based on:')} ${context.used.length > 0 ? formatHumanList(context.used) : 'available repo context'}`);
|
|
3478
|
+
console.log('');
|
|
3479
|
+
|
|
3480
|
+
plannedTasks.forEach((task, index) => {
|
|
3481
|
+
const worktreeLabel = task.suggested_worktree ? chalk.cyan(task.suggested_worktree) : chalk.dim('unassigned');
|
|
3482
|
+
console.log(` ${chalk.green('✓')} ${chalk.bold(`${index + 1}.`)} ${task.title} ${chalk.dim('→')} ${worktreeLabel}`);
|
|
3483
|
+
});
|
|
3484
|
+
|
|
3485
|
+
if (!opts.apply) {
|
|
3486
|
+
console.log('');
|
|
3487
|
+
if (!db) {
|
|
3488
|
+
console.log(chalk.dim('Preview only — run `switchman setup --agents 3` first if you want Switchman to create and track these tasks.'));
|
|
3489
|
+
} else {
|
|
3490
|
+
console.log(chalk.dim('Preview only — rerun with `switchman plan --apply` to create these tasks.'));
|
|
3491
|
+
}
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
if (!db) {
|
|
3496
|
+
console.log('');
|
|
3497
|
+
console.log(`${chalk.red('✗')} Switchman is not set up in this repo yet.`);
|
|
3498
|
+
console.log(` ${chalk.yellow('next:')} ${chalk.cyan('switchman setup --agents 3')}`);
|
|
3499
|
+
process.exitCode = 1;
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
console.log('');
|
|
3504
|
+
for (const task of plannedTasks) {
|
|
3505
|
+
createTask(db, {
|
|
3506
|
+
id: task.id,
|
|
3507
|
+
title: task.title,
|
|
3508
|
+
description: `Planned from: ${context.title}`,
|
|
3509
|
+
priority: planTaskPriority(task.task_spec),
|
|
3510
|
+
});
|
|
3511
|
+
upsertTaskSpec(db, task.id, task.task_spec);
|
|
3512
|
+
console.log(` ${chalk.green('✓')} Created ${chalk.cyan(task.id)} ${chalk.dim(task.title)}`);
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
console.log('');
|
|
3516
|
+
console.log(`${chalk.green('✓')} Planned ${plannedTasks.length} task(s) from repo context.`);
|
|
3517
|
+
console.log(` ${chalk.yellow('next:')} ${chalk.cyan('switchman status --watch')}`);
|
|
3518
|
+
} finally {
|
|
3519
|
+
db?.close();
|
|
3520
|
+
}
|
|
3521
|
+
});
|
|
3522
|
+
|
|
3523
|
+
|
|
2803
3524
|
// ── task ──────────────────────────────────────────────────────────────────────
|
|
2804
3525
|
|
|
2805
3526
|
const taskCmd = program.command('task').description('Manage the task list');
|
|
@@ -2828,6 +3549,7 @@ taskCmd
|
|
|
2828
3549
|
db.close();
|
|
2829
3550
|
const scopeWarning = analyzeTaskScope(title, opts.description || '');
|
|
2830
3551
|
console.log(`${chalk.green('✓')} Task created: ${chalk.cyan(taskId)}`);
|
|
3552
|
+
pushSyncEvent('task_added', { task_id: taskId, title, priority: parseInt(opts.priority) }).catch(() => {});
|
|
2831
3553
|
console.log(` ${chalk.dim(title)}`);
|
|
2832
3554
|
if (scopeWarning) {
|
|
2833
3555
|
console.log(chalk.yellow(` warning: ${scopeWarning.summary}`));
|
|
@@ -2901,6 +3623,7 @@ taskCmd
|
|
|
2901
3623
|
}
|
|
2902
3624
|
|
|
2903
3625
|
console.log(`${chalk.green('✓')} Reset ${chalk.cyan(task.id)} to pending`);
|
|
3626
|
+
pushSyncEvent('task_retried', { task_id: task.id, title: task.title, reason: opts.reason || 'manual retry' }).catch(() => {});
|
|
2904
3627
|
console.log(` ${chalk.dim('title:')} ${task.title}`);
|
|
2905
3628
|
console.log(`${chalk.yellow('next:')} switchman task assign ${task.id} <workspace>`);
|
|
2906
3629
|
});
|
|
@@ -2932,6 +3655,12 @@ taskCmd
|
|
|
2932
3655
|
}
|
|
2933
3656
|
|
|
2934
3657
|
console.log(`${chalk.green('✓')} Reset ${result.retried.length} stale task(s) to pending`);
|
|
3658
|
+
pushSyncEvent('task_retried', {
|
|
3659
|
+
pipeline_id: result.pipeline_id || null,
|
|
3660
|
+
task_count: result.retried.length,
|
|
3661
|
+
task_ids: result.retried.map((task) => task.id),
|
|
3662
|
+
reason: opts.reason,
|
|
3663
|
+
}).catch(() => {});
|
|
2935
3664
|
if (result.pipeline_id) {
|
|
2936
3665
|
console.log(` ${chalk.dim('pipeline:')} ${result.pipeline_id}`);
|
|
2937
3666
|
}
|
|
@@ -2945,8 +3674,25 @@ taskCmd
|
|
|
2945
3674
|
.action((taskId) => {
|
|
2946
3675
|
const repoRoot = getRepo();
|
|
2947
3676
|
try {
|
|
2948
|
-
completeTaskWithRetries(repoRoot, taskId);
|
|
3677
|
+
const result = completeTaskWithRetries(repoRoot, taskId);
|
|
3678
|
+
if (result?.status === 'already_done') {
|
|
3679
|
+
console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} was already marked done — no new changes were recorded`);
|
|
3680
|
+
return;
|
|
3681
|
+
}
|
|
3682
|
+
if (result?.status === 'failed') {
|
|
3683
|
+
console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} is currently failed — retry it before marking it done again`);
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
if (result?.status === 'not_in_progress') {
|
|
3687
|
+
console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} is not currently in progress — start a lease before marking it done`);
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
if (result?.status === 'no_active_lease') {
|
|
3691
|
+
console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} has no active lease — reacquire the task before marking it done`);
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
2949
3694
|
console.log(`${chalk.green('✓')} Task ${chalk.cyan(taskId)} marked done — file claims released`);
|
|
3695
|
+
pushSyncEvent('task_done', { task_id: taskId }).catch(() => {});
|
|
2950
3696
|
} catch (err) {
|
|
2951
3697
|
console.error(chalk.red(err.message));
|
|
2952
3698
|
process.exitCode = 1;
|
|
@@ -2963,6 +3709,7 @@ taskCmd
|
|
|
2963
3709
|
releaseFileClaims(db, taskId);
|
|
2964
3710
|
db.close();
|
|
2965
3711
|
console.log(`${chalk.red('✗')} Task ${chalk.cyan(taskId)} marked failed`);
|
|
3712
|
+
pushSyncEvent('task_failed', { task_id: taskId, reason: reason || null }).catch(() => {});
|
|
2966
3713
|
});
|
|
2967
3714
|
|
|
2968
3715
|
taskCmd
|
|
@@ -3090,6 +3837,13 @@ Pipeline landing rule:
|
|
|
3090
3837
|
|
|
3091
3838
|
const result = enqueueMergeItem(db, payload);
|
|
3092
3839
|
db.close();
|
|
3840
|
+
pushSyncEvent('queue_added', {
|
|
3841
|
+
item_id: result.id,
|
|
3842
|
+
source_type: result.source_type,
|
|
3843
|
+
source_ref: result.source_ref,
|
|
3844
|
+
source_worktree: result.source_worktree || null,
|
|
3845
|
+
target_branch: result.target_branch,
|
|
3846
|
+
}, { worktree: result.source_worktree || null }).catch(() => {});
|
|
3093
3847
|
|
|
3094
3848
|
if (opts.json) {
|
|
3095
3849
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -3177,6 +3931,15 @@ What it helps you answer:
|
|
|
3177
3931
|
return;
|
|
3178
3932
|
}
|
|
3179
3933
|
|
|
3934
|
+
if (items.length === 0) {
|
|
3935
|
+
console.log('');
|
|
3936
|
+
console.log(chalk.bold('switchman queue status'));
|
|
3937
|
+
console.log('');
|
|
3938
|
+
console.log('Queue is empty.');
|
|
3939
|
+
console.log(`Add finished work with: ${chalk.cyan('switchman queue add --worktree agent1')}`);
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3180
3943
|
const queueHealth = summary.counts.blocked > 0
|
|
3181
3944
|
? 'block'
|
|
3182
3945
|
: summary.counts.retrying > 0 || summary.counts.held > 0 || summary.counts.wave_blocked > 0 || summary.counts.escalated > 0
|
|
@@ -3389,9 +4152,26 @@ Examples:
|
|
|
3389
4152
|
for (const entry of aggregate.processed) {
|
|
3390
4153
|
const item = entry.item;
|
|
3391
4154
|
if (entry.status === 'merged') {
|
|
4155
|
+
pushSyncEvent('queue_merged', {
|
|
4156
|
+
item_id: item.id,
|
|
4157
|
+
source_type: item.source_type,
|
|
4158
|
+
source_ref: item.source_ref,
|
|
4159
|
+
source_worktree: item.source_worktree || null,
|
|
4160
|
+
target_branch: item.target_branch,
|
|
4161
|
+
merged_commit: item.merged_commit || null,
|
|
4162
|
+
}, { worktree: item.source_worktree || null }).catch(() => {});
|
|
3392
4163
|
console.log(`${chalk.green('✓')} Merged ${chalk.cyan(item.id)} into ${chalk.bold(item.target_branch)}`);
|
|
3393
4164
|
console.log(` ${chalk.dim('commit:')} ${item.merged_commit}`);
|
|
3394
4165
|
} else {
|
|
4166
|
+
pushSyncEvent('queue_blocked', {
|
|
4167
|
+
item_id: item.id,
|
|
4168
|
+
source_type: item.source_type,
|
|
4169
|
+
source_ref: item.source_ref,
|
|
4170
|
+
source_worktree: item.source_worktree || null,
|
|
4171
|
+
target_branch: item.target_branch,
|
|
4172
|
+
error_code: item.last_error_code || null,
|
|
4173
|
+
error_summary: item.last_error_summary || null,
|
|
4174
|
+
}, { worktree: item.source_worktree || null }).catch(() => {});
|
|
3395
4175
|
console.log(`${chalk.red('✗')} Blocked ${chalk.cyan(item.id)}`);
|
|
3396
4176
|
console.log(` ${chalk.red('why:')} ${item.last_error_summary}`);
|
|
3397
4177
|
if (item.next_action) console.log(` ${chalk.yellow('next:')} ${item.next_action}`);
|
|
@@ -3415,8 +4195,144 @@ Examples:
|
|
|
3415
4195
|
}
|
|
3416
4196
|
});
|
|
3417
4197
|
|
|
3418
|
-
|
|
3419
|
-
.command('
|
|
4198
|
+
program
|
|
4199
|
+
.command('merge')
|
|
4200
|
+
.description('Queue finished worktrees and land safe work through one guided front door')
|
|
4201
|
+
.option('--target <branch>', 'Target branch to merge into', 'main')
|
|
4202
|
+
.option('--dry-run', 'Preview mergeable work without queueing or landing anything')
|
|
4203
|
+
.option('--json', 'Output raw JSON')
|
|
4204
|
+
.addHelpText('after', `
|
|
4205
|
+
Examples:
|
|
4206
|
+
switchman merge
|
|
4207
|
+
switchman merge --dry-run
|
|
4208
|
+
switchman merge --target release
|
|
4209
|
+
`)
|
|
4210
|
+
.action(async (opts) => {
|
|
4211
|
+
const repoRoot = getRepo();
|
|
4212
|
+
const db = getDb(repoRoot);
|
|
4213
|
+
|
|
4214
|
+
try {
|
|
4215
|
+
const discovery = discoverMergeCandidates(db, repoRoot, { targetBranch: opts.target || 'main' });
|
|
4216
|
+
const queued = [];
|
|
4217
|
+
|
|
4218
|
+
for (const entry of discovery.eligible) {
|
|
4219
|
+
queued.push(enqueueMergeItem(db, {
|
|
4220
|
+
sourceType: 'worktree',
|
|
4221
|
+
sourceRef: entry.branch,
|
|
4222
|
+
sourceWorktree: entry.worktree,
|
|
4223
|
+
targetBranch: opts.target || 'main',
|
|
4224
|
+
submittedBy: 'switchman merge',
|
|
4225
|
+
}));
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
const queueItems = listMergeQueue(db);
|
|
4229
|
+
const summary = buildQueueStatusSummary(queueItems, { db, repoRoot });
|
|
4230
|
+
const mergeOrder = summary.recommended_sequence
|
|
4231
|
+
.filter((item) => ['land_now', 'prepare_next'].includes(item.lane))
|
|
4232
|
+
.map((item) => item.source_ref);
|
|
4233
|
+
|
|
4234
|
+
if (opts.json) {
|
|
4235
|
+
if (opts.dryRun) {
|
|
4236
|
+
console.log(JSON.stringify({ discovery, queued, summary, merge_order: mergeOrder, dry_run: true }, null, 2));
|
|
4237
|
+
db.close();
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
const gate = await evaluateQueueRepoGate(db, repoRoot);
|
|
4242
|
+
const runnableCount = listMergeQueue(db).filter((item) => ['queued', 'retrying'].includes(item.status)).length;
|
|
4243
|
+
const result = gate.ok
|
|
4244
|
+
? await runMergeQueue(db, repoRoot, {
|
|
4245
|
+
targetBranch: opts.target || 'main',
|
|
4246
|
+
maxItems: Math.max(1, runnableCount),
|
|
4247
|
+
mergeBudget: Math.max(1, runnableCount),
|
|
4248
|
+
followPlan: false,
|
|
4249
|
+
})
|
|
4250
|
+
: null;
|
|
4251
|
+
console.log(JSON.stringify({ discovery, queued, summary, merge_order: mergeOrder, gate, result }, null, 2));
|
|
4252
|
+
db.close();
|
|
4253
|
+
return;
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
printMergeDiscovery(discovery);
|
|
4257
|
+
|
|
4258
|
+
if (discovery.blocked.length > 0) {
|
|
4259
|
+
console.log('');
|
|
4260
|
+
console.log(chalk.bold('Needs attention before landing:'));
|
|
4261
|
+
for (const entry of discovery.blocked) {
|
|
4262
|
+
console.log(` ${chalk.yellow(entry.worktree)}: ${entry.summary}`);
|
|
4263
|
+
console.log(` ${chalk.dim('run:')} ${chalk.cyan(entry.command)}`);
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
if (mergeOrder.length > 0) {
|
|
4268
|
+
console.log('');
|
|
4269
|
+
console.log(`${chalk.bold('Merge order:')} ${mergeOrder.join(' → ')}`);
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4272
|
+
if (opts.dryRun) {
|
|
4273
|
+
console.log('');
|
|
4274
|
+
console.log(chalk.dim('Dry run only — nothing was landed.'));
|
|
4275
|
+
db.close();
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
if (discovery.eligible.length === 0) {
|
|
4280
|
+
console.log('');
|
|
4281
|
+
console.log(chalk.dim('No finished worktrees are ready to land yet.'));
|
|
4282
|
+
console.log(`${chalk.yellow('next:')} ${chalk.cyan('switchman status')}`);
|
|
4283
|
+
db.close();
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
const gate = await evaluateQueueRepoGate(db, repoRoot);
|
|
4288
|
+
if (!gate.ok) {
|
|
4289
|
+
console.log('');
|
|
4290
|
+
console.log(`${chalk.red('✗')} Merge gate blocked landing`);
|
|
4291
|
+
console.log(` ${chalk.dim(gate.summary)}`);
|
|
4292
|
+
console.log(` ${chalk.yellow('next:')} ${chalk.cyan('switchman gate ci')}`);
|
|
4293
|
+
db.close();
|
|
4294
|
+
return;
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
console.log('');
|
|
4298
|
+
const runnableCount = listMergeQueue(db).filter((item) => ['queued', 'retrying'].includes(item.status)).length;
|
|
4299
|
+
const result = await runMergeQueue(db, repoRoot, {
|
|
4300
|
+
targetBranch: opts.target || 'main',
|
|
4301
|
+
maxItems: Math.max(1, runnableCount),
|
|
4302
|
+
mergeBudget: Math.max(1, runnableCount),
|
|
4303
|
+
followPlan: false,
|
|
4304
|
+
});
|
|
4305
|
+
|
|
4306
|
+
const merged = result.processed.filter((item) => item.status === 'merged');
|
|
4307
|
+
for (const mergedItem of merged) {
|
|
4308
|
+
console.log(` ${chalk.green('✓')} Landed ${chalk.cyan(mergedItem.item.source_ref)} into ${chalk.bold(mergedItem.item.target_branch)}`);
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
if (merged.length > 0 && !result.deferred && result.processed.every((item) => item.status === 'merged')) {
|
|
4312
|
+
console.log('');
|
|
4313
|
+
console.log(`${chalk.green('✓')} Done. ${merged.length} worktree(s) landed cleanly.`);
|
|
4314
|
+
db.close();
|
|
4315
|
+
return;
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
const blocked = result.processed.find((item) => item.status !== 'merged')?.item || result.deferred || null;
|
|
4319
|
+
if (blocked) {
|
|
4320
|
+
console.log('');
|
|
4321
|
+
console.log(`${chalk.yellow('!')} Landing stopped at ${chalk.cyan(blocked.source_ref)}`);
|
|
4322
|
+
if (blocked.last_error_summary) console.log(` ${chalk.dim(blocked.last_error_summary)}`);
|
|
4323
|
+
if (blocked.next_action) console.log(` ${chalk.yellow('next:')} ${blocked.next_action}`);
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
db.close();
|
|
4327
|
+
} catch (err) {
|
|
4328
|
+
db.close();
|
|
4329
|
+
console.error(chalk.red(err.message));
|
|
4330
|
+
process.exitCode = 1;
|
|
4331
|
+
}
|
|
4332
|
+
});
|
|
4333
|
+
|
|
4334
|
+
queueCmd
|
|
4335
|
+
.command('retry <itemId>')
|
|
3420
4336
|
.description('Retry a blocked merge queue item')
|
|
3421
4337
|
.option('--json', 'Output raw JSON')
|
|
3422
4338
|
.action((itemId, opts) => {
|
|
@@ -3771,6 +4687,7 @@ explainCmd
|
|
|
3771
4687
|
// ── pipeline ──────────────────────────────────────────────────────────────────
|
|
3772
4688
|
|
|
3773
4689
|
const pipelineCmd = program.command('pipeline').description('Create and summarize issue-to-PR execution pipelines');
|
|
4690
|
+
pipelineCmd._switchmanAdvanced = true;
|
|
3774
4691
|
pipelineCmd.addHelpText('after', `
|
|
3775
4692
|
Examples:
|
|
3776
4693
|
switchman pipeline start "Harden auth API permissions"
|
|
@@ -4621,6 +5538,7 @@ Examples:
|
|
|
4621
5538
|
}
|
|
4622
5539
|
|
|
4623
5540
|
console.log(`${chalk.green('✓')} Lease acquired: ${chalk.bold(task.title)}`);
|
|
5541
|
+
pushSyncEvent('lease_acquired', { task_id: task.id, title: task.title }, { worktree: worktreeName }).catch(() => {});
|
|
4624
5542
|
console.log(` ${chalk.dim('task:')} ${task.id} ${chalk.dim('lease:')} ${lease.id}`);
|
|
4625
5543
|
console.log(` ${chalk.dim('worktree:')} ${chalk.cyan(worktreeName)} ${chalk.dim('priority:')} ${task.priority}`);
|
|
4626
5544
|
});
|
|
@@ -4803,25 +5721,31 @@ wtCmd
|
|
|
4803
5721
|
const db = getDb(repoRoot);
|
|
4804
5722
|
const worktrees = listWorktrees(db);
|
|
4805
5723
|
const gitWorktrees = listGitWorktrees(repoRoot);
|
|
4806
|
-
db.close();
|
|
4807
5724
|
|
|
4808
5725
|
if (!worktrees.length && !gitWorktrees.length) {
|
|
5726
|
+
db.close();
|
|
4809
5727
|
console.log(chalk.dim('No workspaces found. Run `switchman setup --agents 3` or `switchman worktree sync`.'));
|
|
4810
5728
|
return;
|
|
4811
5729
|
}
|
|
4812
5730
|
|
|
4813
5731
|
// Show git worktrees (source of truth) annotated with db info
|
|
5732
|
+
const complianceReport = evaluateRepoCompliance(db, repoRoot, gitWorktrees);
|
|
4814
5733
|
console.log('');
|
|
4815
5734
|
console.log(chalk.bold('Git Worktrees:'));
|
|
4816
5735
|
for (const wt of gitWorktrees) {
|
|
4817
5736
|
const dbInfo = worktrees.find(d => d.path === wt.path);
|
|
5737
|
+
const complianceInfo = complianceReport.worktreeCompliance.find((entry) => entry.worktree === wt.name) || null;
|
|
4818
5738
|
const agent = dbInfo?.agent ? chalk.cyan(dbInfo.agent) : chalk.dim('no agent');
|
|
4819
5739
|
const status = dbInfo?.status ? statusBadge(dbInfo.status) : chalk.dim('unregistered');
|
|
4820
|
-
const compliance = dbInfo?.compliance_state ? statusBadge(dbInfo.compliance_state) : chalk.dim('unknown');
|
|
5740
|
+
const compliance = complianceInfo?.compliance_state ? statusBadge(complianceInfo.compliance_state) : dbInfo?.compliance_state ? statusBadge(dbInfo.compliance_state) : chalk.dim('unknown');
|
|
4821
5741
|
console.log(` ${chalk.bold(wt.name.padEnd(20))} ${status} ${compliance} branch: ${chalk.cyan(wt.branch || 'unknown')} agent: ${agent}`);
|
|
4822
5742
|
console.log(` ${chalk.dim(wt.path)}`);
|
|
5743
|
+
if ((complianceInfo?.unclaimed_changed_files || []).length > 0) {
|
|
5744
|
+
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`)}` : ''}`);
|
|
5745
|
+
}
|
|
4823
5746
|
}
|
|
4824
5747
|
console.log('');
|
|
5748
|
+
db.close();
|
|
4825
5749
|
});
|
|
4826
5750
|
|
|
4827
5751
|
wtCmd
|
|
@@ -4845,13 +5769,14 @@ program
|
|
|
4845
5769
|
.command('claim <taskId> <worktree> [files...]')
|
|
4846
5770
|
.description('Lock files for a task before editing')
|
|
4847
5771
|
.option('--agent <name>', 'Agent name')
|
|
4848
|
-
.option('--force', '
|
|
5772
|
+
.option('--force', 'Emergency override for manual recovery when a conflicting claim is known to be stale or wrong')
|
|
4849
5773
|
.addHelpText('after', `
|
|
4850
5774
|
Examples:
|
|
4851
5775
|
switchman claim task-123 agent2 src/auth.js src/server.js
|
|
4852
5776
|
switchman claim task-123 agent2 src/auth.js --agent cursor
|
|
4853
5777
|
|
|
4854
5778
|
Use this before editing files in a shared repo.
|
|
5779
|
+
Only use --force for operator-led recovery after checking switchman status or switchman explain claim <path>.
|
|
4855
5780
|
`)
|
|
4856
5781
|
.action((taskId, worktree, files, opts) => {
|
|
4857
5782
|
if (!files.length) {
|
|
@@ -4870,7 +5795,7 @@ Use this before editing files in a shared repo.
|
|
|
4870
5795
|
for (const c of conflicts) {
|
|
4871
5796
|
console.log(` ${chalk.yellow(c.file)} → already claimed by worktree ${chalk.cyan(c.claimedBy.worktree)} (task: ${c.claimedBy.task_title})`);
|
|
4872
5797
|
}
|
|
4873
|
-
console.log(chalk.dim('\
|
|
5798
|
+
console.log(chalk.dim('\nDo not use --force as a shortcut. Check status or explain the claim first, then only override if the existing claim is known-bad.'));
|
|
4874
5799
|
console.log(`${chalk.yellow('next:')} switchman status`);
|
|
4875
5800
|
process.exitCode = 1;
|
|
4876
5801
|
return;
|
|
@@ -4878,6 +5803,12 @@ Use this before editing files in a shared repo.
|
|
|
4878
5803
|
|
|
4879
5804
|
const lease = claimFiles(db, taskId, worktree, files, opts.agent);
|
|
4880
5805
|
console.log(`${chalk.green('✓')} Claimed ${files.length} file(s) for task ${chalk.cyan(taskId)} (${chalk.dim(lease.id)})`);
|
|
5806
|
+
pushSyncEvent('claim_added', {
|
|
5807
|
+
task_id: taskId,
|
|
5808
|
+
lease_id: lease.id,
|
|
5809
|
+
file_count: files.length,
|
|
5810
|
+
files: files.slice(0, 10),
|
|
5811
|
+
}, { worktree }).catch(() => {});
|
|
4881
5812
|
files.forEach(f => console.log(` ${chalk.dim(f)}`));
|
|
4882
5813
|
} catch (err) {
|
|
4883
5814
|
printErrorWithNext(err.message, 'switchman task list --status in_progress');
|
|
@@ -4893,9 +5824,11 @@ program
|
|
|
4893
5824
|
.action((taskId) => {
|
|
4894
5825
|
const repoRoot = getRepo();
|
|
4895
5826
|
const db = getDb(repoRoot);
|
|
5827
|
+
const task = getTask(db, taskId);
|
|
4896
5828
|
releaseFileClaims(db, taskId);
|
|
4897
5829
|
db.close();
|
|
4898
5830
|
console.log(`${chalk.green('✓')} Released all claims for task ${chalk.cyan(taskId)}`);
|
|
5831
|
+
pushSyncEvent('claim_released', { task_id: taskId }, { worktree: task?.worktree || null }).catch(() => {});
|
|
4899
5832
|
});
|
|
4900
5833
|
|
|
4901
5834
|
program
|
|
@@ -5171,12 +6104,14 @@ program
|
|
|
5171
6104
|
.description('Show one dashboard view of what is running, blocked, and ready next')
|
|
5172
6105
|
.option('--json', 'Output raw JSON')
|
|
5173
6106
|
.option('--watch', 'Keep refreshing status in the terminal')
|
|
6107
|
+
.option('--repair', 'Repair safe interrupted queue and pipeline state before rendering status')
|
|
5174
6108
|
.option('--watch-interval-ms <n>', 'Polling interval for --watch mode', '2000')
|
|
5175
6109
|
.option('--max-cycles <n>', 'Maximum refresh cycles before exiting', '0')
|
|
5176
6110
|
.addHelpText('after', `
|
|
5177
6111
|
Examples:
|
|
5178
6112
|
switchman status
|
|
5179
6113
|
switchman status --watch
|
|
6114
|
+
switchman status --repair
|
|
5180
6115
|
switchman status --json
|
|
5181
6116
|
|
|
5182
6117
|
Use this first when the repo feels stuck.
|
|
@@ -5194,13 +6129,40 @@ Use this first when the repo feels stuck.
|
|
|
5194
6129
|
console.clear();
|
|
5195
6130
|
}
|
|
5196
6131
|
|
|
6132
|
+
let repairResult = null;
|
|
6133
|
+
if (opts.repair) {
|
|
6134
|
+
const repairDb = getDb(repoRoot);
|
|
6135
|
+
try {
|
|
6136
|
+
repairResult = repairRepoState(repairDb, repoRoot);
|
|
6137
|
+
} finally {
|
|
6138
|
+
repairDb.close();
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
|
|
5197
6142
|
const report = await collectStatusSnapshot(repoRoot);
|
|
6143
|
+
const [teamActivity, teamState] = await Promise.all([
|
|
6144
|
+
pullActiveTeamMembers(),
|
|
6145
|
+
pullTeamState(),
|
|
6146
|
+
]);
|
|
6147
|
+
const myUserId = readCredentials()?.user_id;
|
|
6148
|
+
const otherMembers = teamActivity.filter(e => e.user_id !== myUserId);
|
|
6149
|
+
const teamSummary = summarizeTeamCoordinationState(teamState, myUserId);
|
|
5198
6150
|
cycles += 1;
|
|
5199
6151
|
|
|
5200
6152
|
if (opts.json) {
|
|
5201
|
-
|
|
6153
|
+
const payload = watch ? { ...report, watch: true, cycles } : report;
|
|
6154
|
+
const withTeam = { ...payload, team_sync: { summary: teamSummary, recent_events: teamState.filter((event) => event.user_id !== myUserId).slice(0, 25) } };
|
|
6155
|
+
console.log(JSON.stringify(opts.repair ? { ...withTeam, repair: repairResult } : withTeam, null, 2));
|
|
5202
6156
|
} else {
|
|
5203
|
-
|
|
6157
|
+
if (opts.repair && repairResult) {
|
|
6158
|
+
printRepairSummary(repairResult, {
|
|
6159
|
+
repairedHeading: `${chalk.green('✓')} Repaired safe interrupted repo state before rendering status`,
|
|
6160
|
+
noRepairHeading: `${chalk.green('✓')} No repo repair action needed before rendering status`,
|
|
6161
|
+
limit: 6,
|
|
6162
|
+
});
|
|
6163
|
+
console.log('');
|
|
6164
|
+
}
|
|
6165
|
+
renderUnifiedStatusReport(report, { teamActivity: otherMembers, teamSummary });
|
|
5204
6166
|
if (watch) {
|
|
5205
6167
|
const signature = buildWatchSignature(report);
|
|
5206
6168
|
const watchState = lastSignature === null
|
|
@@ -5267,9 +6229,11 @@ program
|
|
|
5267
6229
|
}
|
|
5268
6230
|
});
|
|
5269
6231
|
|
|
5270
|
-
program
|
|
6232
|
+
const doctorCmd = program
|
|
5271
6233
|
.command('doctor')
|
|
5272
|
-
.description('Show one operator-focused health view: what is running, what is blocked, and what to do next')
|
|
6234
|
+
.description('Show one operator-focused health view: what is running, what is blocked, and what to do next');
|
|
6235
|
+
doctorCmd._switchmanAdvanced = true;
|
|
6236
|
+
doctorCmd
|
|
5273
6237
|
.option('--repair', 'Repair safe interrupted queue and pipeline state before reporting health')
|
|
5274
6238
|
.option('--json', 'Output raw JSON')
|
|
5275
6239
|
.addHelpText('after', `
|
|
@@ -5407,6 +6371,7 @@ Examples:
|
|
|
5407
6371
|
`);
|
|
5408
6372
|
|
|
5409
6373
|
const auditCmd = program.command('audit').description('Inspect and verify the tamper-evident audit trail');
|
|
6374
|
+
auditCmd._switchmanAdvanced = true;
|
|
5410
6375
|
|
|
5411
6376
|
auditCmd
|
|
5412
6377
|
.command('change <pipelineId>')
|
|
@@ -5729,6 +6694,7 @@ gateCmd
|
|
|
5729
6694
|
const semanticCmd = program
|
|
5730
6695
|
.command('semantic')
|
|
5731
6696
|
.description('Inspect or materialize the derived semantic code-object view');
|
|
6697
|
+
semanticCmd._switchmanAdvanced = true;
|
|
5732
6698
|
|
|
5733
6699
|
semanticCmd
|
|
5734
6700
|
.command('materialize')
|
|
@@ -5745,6 +6711,7 @@ semanticCmd
|
|
|
5745
6711
|
const objectCmd = program
|
|
5746
6712
|
.command('object')
|
|
5747
6713
|
.description('Experimental object-source mode backed by canonical exported code objects');
|
|
6714
|
+
objectCmd._switchmanAdvanced = true;
|
|
5748
6715
|
|
|
5749
6716
|
objectCmd
|
|
5750
6717
|
.command('import')
|
|
@@ -5826,6 +6793,7 @@ objectCmd
|
|
|
5826
6793
|
// ── monitor ──────────────────────────────────────────────────────────────────
|
|
5827
6794
|
|
|
5828
6795
|
const monitorCmd = program.command('monitor').description('Observe workspaces for runtime file changes');
|
|
6796
|
+
monitorCmd._switchmanAdvanced = true;
|
|
5829
6797
|
|
|
5830
6798
|
monitorCmd
|
|
5831
6799
|
.command('once')
|
|
@@ -5835,7 +6803,7 @@ monitorCmd
|
|
|
5835
6803
|
.action((opts) => {
|
|
5836
6804
|
const repoRoot = getRepo();
|
|
5837
6805
|
const db = getDb(repoRoot);
|
|
5838
|
-
const worktrees =
|
|
6806
|
+
const worktrees = resolveMonitoredWorktrees(db, repoRoot);
|
|
5839
6807
|
const result = monitorWorktreesOnce(db, repoRoot, worktrees, { quarantine: opts.quarantine });
|
|
5840
6808
|
db.close();
|
|
5841
6809
|
|
|
@@ -5851,9 +6819,7 @@ monitorCmd
|
|
|
5851
6819
|
|
|
5852
6820
|
console.log(`${chalk.green('✓')} Observed ${result.summary.total} file change(s)`);
|
|
5853
6821
|
for (const event of result.events) {
|
|
5854
|
-
|
|
5855
|
-
const action = event.enforcement_action ? ` ${chalk.dim(event.enforcement_action)}` : '';
|
|
5856
|
-
console.log(` ${badge} ${chalk.cyan(event.worktree)} ${chalk.yellow(event.file_path)} ${chalk.dim(event.change_type)}${event.reason_code ? ` ${chalk.dim(event.reason_code)}` : ''}${action}`);
|
|
6822
|
+
renderMonitorEvent(event);
|
|
5857
6823
|
}
|
|
5858
6824
|
});
|
|
5859
6825
|
|
|
@@ -5887,14 +6853,12 @@ monitorCmd
|
|
|
5887
6853
|
|
|
5888
6854
|
while (!stopped) {
|
|
5889
6855
|
const db = getDb(repoRoot);
|
|
5890
|
-
const worktrees =
|
|
6856
|
+
const worktrees = resolveMonitoredWorktrees(db, repoRoot);
|
|
5891
6857
|
const result = monitorWorktreesOnce(db, repoRoot, worktrees, { quarantine: opts.quarantine });
|
|
5892
6858
|
db.close();
|
|
5893
6859
|
|
|
5894
6860
|
for (const event of result.events) {
|
|
5895
|
-
|
|
5896
|
-
const action = event.enforcement_action ? ` ${chalk.dim(event.enforcement_action)}` : '';
|
|
5897
|
-
console.log(` ${badge} ${chalk.cyan(event.worktree)} ${chalk.yellow(event.file_path)} ${chalk.dim(event.change_type)}${event.reason_code ? ` ${chalk.dim(event.reason_code)}` : ''}${action}`);
|
|
6861
|
+
renderMonitorEvent(event);
|
|
5898
6862
|
}
|
|
5899
6863
|
|
|
5900
6864
|
if (stopped) break;
|
|
@@ -5912,39 +6876,18 @@ monitorCmd
|
|
|
5912
6876
|
.action((opts) => {
|
|
5913
6877
|
const repoRoot = getRepo();
|
|
5914
6878
|
const intervalMs = Number.parseInt(opts.intervalMs, 10);
|
|
5915
|
-
const
|
|
6879
|
+
const state = startBackgroundMonitor(repoRoot, {
|
|
6880
|
+
intervalMs,
|
|
6881
|
+
quarantine: Boolean(opts.quarantine),
|
|
6882
|
+
});
|
|
5916
6883
|
|
|
5917
|
-
if (
|
|
5918
|
-
console.log(chalk.yellow(`Monitor already running with pid ${
|
|
6884
|
+
if (state.already_running) {
|
|
6885
|
+
console.log(chalk.yellow(`Monitor already running with pid ${state.state.pid}`));
|
|
5919
6886
|
return;
|
|
5920
6887
|
}
|
|
5921
6888
|
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
process.argv[1],
|
|
5925
|
-
'monitor',
|
|
5926
|
-
'watch',
|
|
5927
|
-
'--interval-ms',
|
|
5928
|
-
String(intervalMs),
|
|
5929
|
-
...(opts.quarantine ? ['--quarantine'] : []),
|
|
5930
|
-
'--daemonized',
|
|
5931
|
-
], {
|
|
5932
|
-
cwd: repoRoot,
|
|
5933
|
-
detached: true,
|
|
5934
|
-
stdio: 'ignore',
|
|
5935
|
-
});
|
|
5936
|
-
child.unref();
|
|
5937
|
-
|
|
5938
|
-
const statePath = writeMonitorState(repoRoot, {
|
|
5939
|
-
pid: child.pid,
|
|
5940
|
-
interval_ms: intervalMs,
|
|
5941
|
-
quarantine: Boolean(opts.quarantine),
|
|
5942
|
-
log_path: logPath,
|
|
5943
|
-
started_at: new Date().toISOString(),
|
|
5944
|
-
});
|
|
5945
|
-
|
|
5946
|
-
console.log(`${chalk.green('✓')} Started monitor pid ${chalk.cyan(String(child.pid))}`);
|
|
5947
|
-
console.log(`${chalk.dim('State:')} ${statePath}`);
|
|
6889
|
+
console.log(`${chalk.green('✓')} Started monitor pid ${chalk.cyan(String(state.state.pid))}`);
|
|
6890
|
+
console.log(`${chalk.dim('State:')} ${state.state_path}`);
|
|
5948
6891
|
});
|
|
5949
6892
|
|
|
5950
6893
|
monitorCmd
|
|
@@ -5996,9 +6939,38 @@ monitorCmd
|
|
|
5996
6939
|
console.log(` ${chalk.dim('started_at')} ${state.started_at}`);
|
|
5997
6940
|
});
|
|
5998
6941
|
|
|
6942
|
+
program
|
|
6943
|
+
.command('watch')
|
|
6944
|
+
.description('Watch worktrees for direct writes and rogue edits in real time')
|
|
6945
|
+
.option('--interval-ms <ms>', 'Polling interval in milliseconds', '2000')
|
|
6946
|
+
.option('--quarantine', 'Move or restore denied runtime changes immediately after detection')
|
|
6947
|
+
.action(async (opts) => {
|
|
6948
|
+
const repoRoot = getRepo();
|
|
6949
|
+
const child = spawn(process.execPath, [
|
|
6950
|
+
process.argv[1],
|
|
6951
|
+
'monitor',
|
|
6952
|
+
'watch',
|
|
6953
|
+
'--interval-ms',
|
|
6954
|
+
String(opts.intervalMs || '2000'),
|
|
6955
|
+
...(opts.quarantine ? ['--quarantine'] : []),
|
|
6956
|
+
], {
|
|
6957
|
+
cwd: repoRoot,
|
|
6958
|
+
stdio: 'inherit',
|
|
6959
|
+
});
|
|
6960
|
+
|
|
6961
|
+
await new Promise((resolve, reject) => {
|
|
6962
|
+
child.on('exit', (code) => {
|
|
6963
|
+
process.exitCode = code ?? 0;
|
|
6964
|
+
resolve();
|
|
6965
|
+
});
|
|
6966
|
+
child.on('error', reject);
|
|
6967
|
+
});
|
|
6968
|
+
});
|
|
6969
|
+
|
|
5999
6970
|
// ── policy ───────────────────────────────────────────────────────────────────
|
|
6000
6971
|
|
|
6001
6972
|
const policyCmd = program.command('policy').description('Manage enforcement and change-governance policy');
|
|
6973
|
+
policyCmd._switchmanAdvanced = true;
|
|
6002
6974
|
|
|
6003
6975
|
policyCmd
|
|
6004
6976
|
.command('init')
|
|
@@ -6144,4 +7116,372 @@ policyCmd
|
|
|
6144
7116
|
}
|
|
6145
7117
|
});
|
|
6146
7118
|
|
|
7119
|
+
|
|
7120
|
+
|
|
7121
|
+
// ── login ──────────────────────────────────────────────────────────────────────
|
|
7122
|
+
|
|
7123
|
+
program
|
|
7124
|
+
.command('login')
|
|
7125
|
+
.description('Sign in with GitHub to activate Switchman Pro')
|
|
7126
|
+
.option('--invite <token>', 'Join a team with an invite token')
|
|
7127
|
+
.option('--status', 'Show current login status')
|
|
7128
|
+
.addHelpText('after', `
|
|
7129
|
+
Examples:
|
|
7130
|
+
switchman login
|
|
7131
|
+
switchman login --status
|
|
7132
|
+
switchman login --invite tk_8f3a2c
|
|
7133
|
+
`)
|
|
7134
|
+
.action(async (opts) => {
|
|
7135
|
+
// Show current status
|
|
7136
|
+
if (opts.status) {
|
|
7137
|
+
const creds = readCredentials();
|
|
7138
|
+
if (!creds?.access_token) {
|
|
7139
|
+
console.log('');
|
|
7140
|
+
console.log(` ${chalk.dim('Status:')} Not logged in`);
|
|
7141
|
+
console.log(` ${chalk.dim('Run: ')} ${chalk.cyan('switchman login')}`);
|
|
7142
|
+
console.log('');
|
|
7143
|
+
return;
|
|
7144
|
+
}
|
|
7145
|
+
|
|
7146
|
+
const licence = await checkLicence();
|
|
7147
|
+
console.log('');
|
|
7148
|
+
if (licence.valid) {
|
|
7149
|
+
console.log(` ${chalk.green('✓')} Logged in as ${chalk.cyan(creds.email ?? 'unknown')}`);
|
|
7150
|
+
console.log(` ${chalk.dim('Plan:')} ${licence.plan ?? 'Pro'}`);
|
|
7151
|
+
if (licence.current_period_end) {
|
|
7152
|
+
console.log(` ${chalk.dim('Renews:')} ${new Date(licence.current_period_end).toLocaleDateString()}`);
|
|
7153
|
+
}
|
|
7154
|
+
if (licence.offline) {
|
|
7155
|
+
console.log(` ${chalk.dim('(offline cache)')}`);
|
|
7156
|
+
}
|
|
7157
|
+
} else {
|
|
7158
|
+
console.log(` ${chalk.yellow('⚠')} Logged in as ${chalk.cyan(creds.email ?? 'unknown')} but no active Pro licence`);
|
|
7159
|
+
console.log(` ${chalk.dim('Upgrade at:')} ${chalk.cyan(PRO_PAGE_URL)}`);
|
|
7160
|
+
}
|
|
7161
|
+
console.log('');
|
|
7162
|
+
return;
|
|
7163
|
+
}
|
|
7164
|
+
|
|
7165
|
+
// Handle --invite token
|
|
7166
|
+
if (opts.invite) {
|
|
7167
|
+
const SUPABASE_URL = process.env.SWITCHMAN_SUPABASE_URL
|
|
7168
|
+
?? 'https://afilbolhlkiingnsupgr.supabase.co';
|
|
7169
|
+
const SUPABASE_ANON = process.env.SWITCHMAN_SUPABASE_ANON
|
|
7170
|
+
?? 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFmaWxib2xobGtpaW5nbnN1cGdyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM1OTIzOTIsImV4cCI6MjA4OTE2ODM5Mn0.8TBfHfRB0vEyKPMWBd6i1DNwx1nS9UqprIAsJf35n88';
|
|
7171
|
+
|
|
7172
|
+
const creds = readCredentials();
|
|
7173
|
+
if (!creds?.access_token) {
|
|
7174
|
+
console.log('');
|
|
7175
|
+
console.log(chalk.yellow(' You need to sign in first before accepting an invite.'));
|
|
7176
|
+
console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman login')} ${chalk.dim('then try again with --invite')}`);
|
|
7177
|
+
console.log('');
|
|
7178
|
+
process.exit(1);
|
|
7179
|
+
}
|
|
7180
|
+
|
|
7181
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
7182
|
+
const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
|
|
7183
|
+
global: { headers: { Authorization: `Bearer ${creds.access_token}` } }
|
|
7184
|
+
});
|
|
7185
|
+
|
|
7186
|
+
const { data: { user } } = await sb.auth.getUser();
|
|
7187
|
+
if (!user) {
|
|
7188
|
+
console.log(chalk.red(' ✗ Could not verify your account. Run: switchman login'));
|
|
7189
|
+
process.exit(1);
|
|
7190
|
+
}
|
|
7191
|
+
|
|
7192
|
+
// Look up the invite
|
|
7193
|
+
const { data: invite, error: inviteError } = await sb
|
|
7194
|
+
.from('team_invites')
|
|
7195
|
+
.select('id, team_id, email, accepted')
|
|
7196
|
+
.eq('token', opts.invite)
|
|
7197
|
+
.maybeSingle();
|
|
7198
|
+
|
|
7199
|
+
if (inviteError || !invite) {
|
|
7200
|
+
console.log('');
|
|
7201
|
+
console.log(chalk.red(' ✗ Invite token not found or already used.'));
|
|
7202
|
+
console.log(` ${chalk.dim('Ask your teammate to send a new invite.')}`);
|
|
7203
|
+
console.log('');
|
|
7204
|
+
process.exit(1);
|
|
7205
|
+
}
|
|
7206
|
+
|
|
7207
|
+
if (invite.accepted) {
|
|
7208
|
+
console.log('');
|
|
7209
|
+
console.log(chalk.yellow(' ⚠ This invite has already been accepted.'));
|
|
7210
|
+
console.log('');
|
|
7211
|
+
process.exit(1);
|
|
7212
|
+
}
|
|
7213
|
+
|
|
7214
|
+
// Add user to the team
|
|
7215
|
+
const { error: memberError } = await sb
|
|
7216
|
+
.from('team_members')
|
|
7217
|
+
.insert({ team_id: invite.team_id, user_id: user.id, role: 'member' });
|
|
7218
|
+
|
|
7219
|
+
if (memberError && !memberError.message.includes('duplicate')) {
|
|
7220
|
+
console.log(chalk.red(` ✗ Could not join team: ${memberError.message}`));
|
|
7221
|
+
process.exit(1);
|
|
7222
|
+
}
|
|
7223
|
+
|
|
7224
|
+
// Mark invite as accepted
|
|
7225
|
+
await sb
|
|
7226
|
+
.from('team_invites')
|
|
7227
|
+
.update({ accepted: true })
|
|
7228
|
+
.eq('id', invite.id);
|
|
7229
|
+
|
|
7230
|
+
console.log('');
|
|
7231
|
+
console.log(` ${chalk.green('✓')} Joined the team successfully`);
|
|
7232
|
+
console.log(` ${chalk.dim('Your agents now share coordination with your teammates.')}`);
|
|
7233
|
+
console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman status')} ${chalk.dim('to see the shared view.')}`);
|
|
7234
|
+
console.log('');
|
|
7235
|
+
return;
|
|
7236
|
+
}
|
|
7237
|
+
|
|
7238
|
+
// Already logged in?
|
|
7239
|
+
const existing = readCredentials();
|
|
7240
|
+
if (existing?.access_token) {
|
|
7241
|
+
const licence = await checkLicence();
|
|
7242
|
+
if (licence.valid) {
|
|
7243
|
+
console.log('');
|
|
7244
|
+
console.log(` ${chalk.green('✓')} Already logged in as ${chalk.cyan(existing.email ?? 'unknown')}`);
|
|
7245
|
+
console.log(` ${chalk.dim('Plan:')} ${licence.plan ?? 'Pro'}`);
|
|
7246
|
+
console.log(` ${chalk.dim('Run')} ${chalk.cyan('switchman login --status')} ${chalk.dim('to see full details')}`);
|
|
7247
|
+
console.log('');
|
|
7248
|
+
return;
|
|
7249
|
+
}
|
|
7250
|
+
}
|
|
7251
|
+
|
|
7252
|
+
console.log('');
|
|
7253
|
+
console.log(chalk.bold(' Switchman Pro — sign in with GitHub'));
|
|
7254
|
+
console.log('');
|
|
7255
|
+
|
|
7256
|
+
const spinner = ora('Waiting for GitHub sign-in...').start();
|
|
7257
|
+
spinner.stop();
|
|
7258
|
+
|
|
7259
|
+
const result = await loginWithGitHub();
|
|
7260
|
+
|
|
7261
|
+
if (!result.success) {
|
|
7262
|
+
console.log(` ${chalk.red('✗')} Sign in failed: ${result.error ?? 'unknown error'}`);
|
|
7263
|
+
console.log(` ${chalk.dim('Try again or visit:')} ${chalk.cyan(PRO_PAGE_URL)}`);
|
|
7264
|
+
console.log('');
|
|
7265
|
+
process.exit(1);
|
|
7266
|
+
}
|
|
7267
|
+
|
|
7268
|
+
// Verify the licence
|
|
7269
|
+
const licence = await checkLicence();
|
|
7270
|
+
|
|
7271
|
+
console.log(` ${chalk.green('✓')} Signed in as ${chalk.cyan(result.email ?? 'unknown')}`);
|
|
7272
|
+
|
|
7273
|
+
if (licence.valid) {
|
|
7274
|
+
console.log(` ${chalk.green('✓')} Switchman Pro active`);
|
|
7275
|
+
console.log(` ${chalk.dim('Plan:')} ${licence.plan ?? 'Pro'}`);
|
|
7276
|
+
console.log('');
|
|
7277
|
+
console.log(` ${chalk.dim('Credentials saved · valid 24h · 7-day offline grace')}`);
|
|
7278
|
+
console.log('');
|
|
7279
|
+
console.log(` Run ${chalk.cyan('switchman setup --agents 10')} to use unlimited agents.`);
|
|
7280
|
+
} else {
|
|
7281
|
+
console.log(` ${chalk.yellow('⚠')} No active Pro licence found`);
|
|
7282
|
+
console.log('');
|
|
7283
|
+
console.log(` If you just paid, it may take a moment to activate.`);
|
|
7284
|
+
console.log(` ${chalk.dim('Upgrade at:')} ${chalk.cyan(PRO_PAGE_URL)}`);
|
|
7285
|
+
}
|
|
7286
|
+
|
|
7287
|
+
console.log('');
|
|
7288
|
+
});
|
|
7289
|
+
|
|
7290
|
+
|
|
7291
|
+
// ── logout ─────────────────────────────────────────────────────────────────────
|
|
7292
|
+
|
|
7293
|
+
program
|
|
7294
|
+
.command('logout')
|
|
7295
|
+
.description('Sign out and remove saved credentials')
|
|
7296
|
+
.action(() => {
|
|
7297
|
+
clearCredentials();
|
|
7298
|
+
console.log('');
|
|
7299
|
+
console.log(` ${chalk.green('✓')} Signed out — credentials removed`);
|
|
7300
|
+
console.log('');
|
|
7301
|
+
});
|
|
7302
|
+
|
|
7303
|
+
|
|
7304
|
+
// ── upgrade ────────────────────────────────────────────────────────────────────
|
|
7305
|
+
|
|
7306
|
+
program
|
|
7307
|
+
.command('upgrade')
|
|
7308
|
+
.description('Open the Switchman Pro page in your browser')
|
|
7309
|
+
.action(async () => {
|
|
7310
|
+
console.log('');
|
|
7311
|
+
console.log(` Opening ${chalk.cyan(PRO_PAGE_URL)}...`);
|
|
7312
|
+
console.log('');
|
|
7313
|
+
const { default: open } = await import('open');
|
|
7314
|
+
await open(PRO_PAGE_URL);
|
|
7315
|
+
});
|
|
7316
|
+
|
|
7317
|
+
// ── team ───────────────────────────────────────────────────────────────────────
|
|
7318
|
+
|
|
7319
|
+
const teamCmd = program
|
|
7320
|
+
.command('team')
|
|
7321
|
+
.description('Manage your Switchman Pro team');
|
|
7322
|
+
|
|
7323
|
+
teamCmd
|
|
7324
|
+
.command('invite <email>')
|
|
7325
|
+
.description('Invite a teammate to your shared coordination')
|
|
7326
|
+
.addHelpText('after', `
|
|
7327
|
+
Examples:
|
|
7328
|
+
switchman team invite alice@example.com
|
|
7329
|
+
`)
|
|
7330
|
+
.action(async (email) => {
|
|
7331
|
+
const licence = await checkLicence();
|
|
7332
|
+
if (!licence.valid) {
|
|
7333
|
+
console.log('');
|
|
7334
|
+
console.log(chalk.yellow(' ⚠ Team invites require Switchman Pro.'));
|
|
7335
|
+
console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman upgrade')}`);
|
|
7336
|
+
console.log('');
|
|
7337
|
+
process.exit(1);
|
|
7338
|
+
}
|
|
7339
|
+
|
|
7340
|
+
const repoRoot = getRepo();
|
|
7341
|
+
const creds = readCredentials();
|
|
7342
|
+
if (!creds?.access_token) {
|
|
7343
|
+
console.log('');
|
|
7344
|
+
console.log(chalk.yellow(' ⚠ You need to be logged in.'));
|
|
7345
|
+
console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman login')}`);
|
|
7346
|
+
console.log('');
|
|
7347
|
+
process.exit(1);
|
|
7348
|
+
}
|
|
7349
|
+
|
|
7350
|
+
const SUPABASE_URL = process.env.SWITCHMAN_SUPABASE_URL
|
|
7351
|
+
?? 'https://afilbolhlkiingnsupgr.supabase.co';
|
|
7352
|
+
const SUPABASE_ANON = process.env.SWITCHMAN_SUPABASE_ANON
|
|
7353
|
+
?? 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFmaWxib2xobGtpaW5nbnN1cGdyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM1OTIzOTIsImV4cCI6MjA4OTE2ODM5Mn0.8TBfHfRB0vEyKPMWBd6i1DNwx1nS9UqprIAsJf35n88';
|
|
7354
|
+
|
|
7355
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
7356
|
+
const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
|
|
7357
|
+
global: { headers: { Authorization: `Bearer ${creds.access_token}` } }
|
|
7358
|
+
});
|
|
7359
|
+
|
|
7360
|
+
const { data: { user } } = await sb.auth.getUser();
|
|
7361
|
+
if (!user) {
|
|
7362
|
+
console.log(chalk.red(' ✗ Could not verify your account. Run: switchman login'));
|
|
7363
|
+
process.exit(1);
|
|
7364
|
+
}
|
|
7365
|
+
|
|
7366
|
+
// Get or create team
|
|
7367
|
+
let teamId;
|
|
7368
|
+
const { data: membership } = await sb
|
|
7369
|
+
.from('team_members')
|
|
7370
|
+
.select('team_id')
|
|
7371
|
+
.eq('user_id', user.id)
|
|
7372
|
+
.maybeSingle();
|
|
7373
|
+
|
|
7374
|
+
if (membership?.team_id) {
|
|
7375
|
+
teamId = membership.team_id;
|
|
7376
|
+
} else {
|
|
7377
|
+
// Create a new team
|
|
7378
|
+
const { data: team, error: teamError } = await sb
|
|
7379
|
+
.from('teams')
|
|
7380
|
+
.insert({ owner_id: user.id, name: 'My Team' })
|
|
7381
|
+
.select('id')
|
|
7382
|
+
.single();
|
|
7383
|
+
|
|
7384
|
+
if (teamError) {
|
|
7385
|
+
console.log(chalk.red(` ✗ Could not create team: ${teamError.message}`));
|
|
7386
|
+
process.exit(1);
|
|
7387
|
+
}
|
|
7388
|
+
|
|
7389
|
+
teamId = team.id;
|
|
7390
|
+
|
|
7391
|
+
// Add the owner as a member
|
|
7392
|
+
await sb.from('team_members').insert({
|
|
7393
|
+
team_id: teamId,
|
|
7394
|
+
user_id: user.id,
|
|
7395
|
+
role: 'owner',
|
|
7396
|
+
});
|
|
7397
|
+
}
|
|
7398
|
+
|
|
7399
|
+
// Create the invite
|
|
7400
|
+
const { data: invite, error: inviteError } = await sb
|
|
7401
|
+
.from('team_invites')
|
|
7402
|
+
.insert({
|
|
7403
|
+
team_id: teamId,
|
|
7404
|
+
invited_by: user.id,
|
|
7405
|
+
email,
|
|
7406
|
+
})
|
|
7407
|
+
.select('token')
|
|
7408
|
+
.single();
|
|
7409
|
+
|
|
7410
|
+
if (inviteError) {
|
|
7411
|
+
console.log(chalk.red(` ✗ Could not create invite: ${inviteError.message}`));
|
|
7412
|
+
process.exit(1);
|
|
7413
|
+
}
|
|
7414
|
+
|
|
7415
|
+
console.log('');
|
|
7416
|
+
console.log(` ${chalk.green('✓')} Invite created for ${chalk.cyan(email)}`);
|
|
7417
|
+
console.log('');
|
|
7418
|
+
console.log(` They can join with:`);
|
|
7419
|
+
console.log(` ${chalk.cyan(`switchman login --invite ${invite.token}`)}`);
|
|
7420
|
+
console.log('');
|
|
7421
|
+
});
|
|
7422
|
+
|
|
7423
|
+
teamCmd
|
|
7424
|
+
.command('list')
|
|
7425
|
+
.description('List your team members')
|
|
7426
|
+
.action(async () => {
|
|
7427
|
+
const licence = await checkLicence();
|
|
7428
|
+
if (!licence.valid) {
|
|
7429
|
+
console.log('');
|
|
7430
|
+
console.log(chalk.yellow(' ⚠ Team features require Switchman Pro.'));
|
|
7431
|
+
console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman upgrade')}`);
|
|
7432
|
+
console.log('');
|
|
7433
|
+
process.exit(1);
|
|
7434
|
+
}
|
|
7435
|
+
|
|
7436
|
+
const creds = readCredentials();
|
|
7437
|
+
if (!creds?.access_token) {
|
|
7438
|
+
console.log(chalk.red(' ✗ Not logged in. Run: switchman login'));
|
|
7439
|
+
process.exit(1);
|
|
7440
|
+
}
|
|
7441
|
+
|
|
7442
|
+
const SUPABASE_URL = process.env.SWITCHMAN_SUPABASE_URL
|
|
7443
|
+
?? 'https://afilbolhlkiingnsupgr.supabase.co';
|
|
7444
|
+
const SUPABASE_ANON = process.env.SWITCHMAN_SUPABASE_ANON
|
|
7445
|
+
?? 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFmaWxib2xobGtpaW5nbnN1cGdyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM1OTIzOTIsImV4cCI6MjA4OTE2ODM5Mn0.8TBfHfRB0vEyKPMWBd6i1DNwx1nS9UqprIAsJf35n88';
|
|
7446
|
+
|
|
7447
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
7448
|
+
const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
|
|
7449
|
+
global: { headers: { Authorization: `Bearer ${creds.access_token}` } }
|
|
7450
|
+
});
|
|
7451
|
+
|
|
7452
|
+
const { data: membership } = await sb
|
|
7453
|
+
.from('team_members')
|
|
7454
|
+
.select('team_id')
|
|
7455
|
+
.eq('user_id', (await sb.auth.getUser()).data.user?.id)
|
|
7456
|
+
.maybeSingle();
|
|
7457
|
+
|
|
7458
|
+
if (!membership?.team_id) {
|
|
7459
|
+
console.log('');
|
|
7460
|
+
console.log(` ${chalk.dim('No team yet. Invite someone with:')} ${chalk.cyan('switchman team invite <email>')}`);
|
|
7461
|
+
console.log('');
|
|
7462
|
+
return;
|
|
7463
|
+
}
|
|
7464
|
+
|
|
7465
|
+
const { data: members } = await sb
|
|
7466
|
+
.from('team_members')
|
|
7467
|
+
.select('user_id, role, joined_at')
|
|
7468
|
+
.eq('team_id', membership.team_id);
|
|
7469
|
+
|
|
7470
|
+
const { data: invites } = await sb
|
|
7471
|
+
.from('team_invites')
|
|
7472
|
+
.select('email, token, accepted, created_at')
|
|
7473
|
+
.eq('team_id', membership.team_id)
|
|
7474
|
+
.eq('accepted', false);
|
|
7475
|
+
|
|
7476
|
+
console.log('');
|
|
7477
|
+
for (const m of members ?? []) {
|
|
7478
|
+
const roleLabel = m.role === 'owner' ? chalk.dim('(owner)') : chalk.dim('(member)');
|
|
7479
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(m.user_id.slice(0, 8))} ${roleLabel}`);
|
|
7480
|
+
}
|
|
7481
|
+
for (const inv of invites ?? []) {
|
|
7482
|
+
console.log(` ${chalk.dim('○')} ${chalk.cyan(inv.email)} ${chalk.dim('(invited)')}`);
|
|
7483
|
+
}
|
|
7484
|
+
console.log('');
|
|
7485
|
+
});
|
|
7486
|
+
|
|
6147
7487
|
program.parse();
|