agileflow 2.51.0 → 2.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -460
- package/package.json +18 -3
- package/scripts/agileflow-configure.js +134 -63
- package/scripts/agileflow-welcome.js +161 -31
- package/scripts/generators/agent-registry.js +45 -57
- package/scripts/generators/command-registry.js +48 -32
- package/scripts/generators/index.js +2 -6
- package/scripts/generators/inject-babysit.js +9 -2
- package/scripts/generators/inject-help.js +3 -1
- package/scripts/generators/inject-readme.js +7 -3
- package/scripts/generators/skill-registry.js +60 -33
- package/scripts/get-env.js +13 -12
- package/scripts/lib/frontmatter-parser.js +82 -0
- package/scripts/obtain-context.js +79 -26
- package/scripts/session-coordinator.sh +232 -0
- package/scripts/session-manager.js +512 -0
- package/src/core/agents/orchestrator.md +275 -0
- package/src/core/commands/adr.md +38 -16
- package/src/core/commands/agent.md +39 -22
- package/src/core/commands/assign.md +17 -0
- package/src/core/commands/auto.md +60 -46
- package/src/core/commands/babysit.md +302 -637
- package/src/core/commands/baseline.md +20 -0
- package/src/core/commands/blockers.md +33 -48
- package/src/core/commands/board.md +19 -0
- package/src/core/commands/changelog.md +20 -0
- package/src/core/commands/ci.md +17 -0
- package/src/core/commands/context.md +43 -40
- package/src/core/commands/debt.md +76 -45
- package/src/core/commands/deploy.md +20 -0
- package/src/core/commands/deps.md +40 -46
- package/src/core/commands/diagnose.md +24 -18
- package/src/core/commands/docs.md +18 -0
- package/src/core/commands/epic.md +31 -0
- package/src/core/commands/feedback.md +33 -21
- package/src/core/commands/handoff.md +29 -0
- package/src/core/commands/help.md +16 -7
- package/src/core/commands/impact.md +31 -61
- package/src/core/commands/metrics.md +17 -35
- package/src/core/commands/packages.md +21 -0
- package/src/core/commands/pr.md +15 -0
- package/src/core/commands/readme-sync.md +42 -9
- package/src/core/commands/research.md +58 -11
- package/src/core/commands/retro.md +42 -50
- package/src/core/commands/review.md +22 -27
- package/src/core/commands/session/end.md +53 -297
- package/src/core/commands/session/history.md +38 -257
- package/src/core/commands/session/init.md +44 -446
- package/src/core/commands/session/new.md +152 -0
- package/src/core/commands/session/resume.md +51 -447
- package/src/core/commands/session/status.md +32 -244
- package/src/core/commands/sprint.md +33 -0
- package/src/core/commands/status.md +18 -0
- package/src/core/commands/story-validate.md +32 -0
- package/src/core/commands/story.md +21 -6
- package/src/core/commands/template.md +18 -0
- package/src/core/commands/tests.md +22 -0
- package/src/core/commands/update.md +72 -58
- package/src/core/commands/validate-expertise.md +25 -37
- package/src/core/commands/velocity.md +33 -74
- package/src/core/commands/verify.md +16 -0
- package/src/core/experts/documentation/expertise.yaml +16 -2
- package/src/core/skills/agileflow-retro-facilitator/SKILL.md +57 -219
- package/src/core/skills/agileflow-retro-facilitator/cookbook/4ls.md +86 -0
- package/src/core/skills/agileflow-retro-facilitator/cookbook/glad-sad-mad.md +79 -0
- package/src/core/skills/agileflow-retro-facilitator/cookbook/start-stop-continue.md +142 -0
- package/src/core/skills/agileflow-retro-facilitator/prompts/action-items.md +83 -0
- package/src/core/skills/writing-skills/SKILL.md +352 -0
- package/src/core/skills/writing-skills/testing-skills-with-subagents.md +232 -0
- package/tools/cli/agileflow-cli.js +4 -2
- package/tools/cli/commands/config.js +20 -13
- package/tools/cli/commands/doctor.js +25 -9
- package/tools/cli/commands/list.js +10 -6
- package/tools/cli/commands/setup.js +54 -3
- package/tools/cli/commands/status.js +6 -8
- package/tools/cli/commands/uninstall.js +5 -5
- package/tools/cli/commands/update.js +51 -7
- package/tools/cli/installers/core/installer.js +8 -4
- package/tools/cli/installers/ide/_base-ide.js +58 -1
- package/tools/cli/installers/ide/claude-code.js +3 -61
- package/tools/cli/installers/ide/codex.js +440 -0
- package/tools/cli/installers/ide/cursor.js +21 -51
- package/tools/cli/installers/ide/manager.js +2 -6
- package/tools/cli/installers/ide/windsurf.js +20 -50
- package/tools/cli/lib/content-injector.js +26 -49
- package/tools/cli/lib/docs-setup.js +3 -2
- package/tools/cli/lib/npm-utils.js +39 -12
- package/tools/cli/lib/ui.js +31 -10
- package/tools/cli/lib/version-checker.js +3 -3
- package/tools/postinstall.js +2 -3
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* session-manager.js - Multi-session coordination for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Manages parallel Claude Code sessions with:
|
|
6
|
+
* - Numbered session IDs (1, 2, 3...)
|
|
7
|
+
* - PID-based liveness detection
|
|
8
|
+
* - Git worktree automation
|
|
9
|
+
* - Registry persistence
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync, spawnSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
// ANSI colors
|
|
17
|
+
const c = {
|
|
18
|
+
reset: '\x1b[0m',
|
|
19
|
+
bold: '\x1b[1m',
|
|
20
|
+
dim: '\x1b[2m',
|
|
21
|
+
red: '\x1b[31m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
yellow: '\x1b[33m',
|
|
24
|
+
blue: '\x1b[34m',
|
|
25
|
+
cyan: '\x1b[36m',
|
|
26
|
+
brand: '\x1b[38;2;232;104;58m',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Find project root (has .agileflow or .git)
|
|
30
|
+
function getProjectRoot() {
|
|
31
|
+
let dir = process.cwd();
|
|
32
|
+
while (dir !== '/') {
|
|
33
|
+
if (fs.existsSync(path.join(dir, '.agileflow')) || fs.existsSync(path.join(dir, '.git'))) {
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
dir = path.dirname(dir);
|
|
37
|
+
}
|
|
38
|
+
return process.cwd();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ROOT = getProjectRoot();
|
|
42
|
+
const SESSIONS_DIR = path.join(ROOT, '.agileflow', 'sessions');
|
|
43
|
+
const REGISTRY_PATH = path.join(SESSIONS_DIR, 'registry.json');
|
|
44
|
+
|
|
45
|
+
// Ensure sessions directory exists
|
|
46
|
+
function ensureSessionsDir() {
|
|
47
|
+
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
48
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Load or create registry
|
|
53
|
+
function loadRegistry() {
|
|
54
|
+
ensureSessionsDir();
|
|
55
|
+
|
|
56
|
+
if (fs.existsSync(REGISTRY_PATH)) {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error(`${c.red}Error loading registry: ${e.message}${c.reset}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create default registry
|
|
65
|
+
const registry = {
|
|
66
|
+
schema_version: '1.0.0',
|
|
67
|
+
next_id: 1,
|
|
68
|
+
project_name: path.basename(ROOT),
|
|
69
|
+
sessions: {},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
saveRegistry(registry);
|
|
73
|
+
return registry;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Save registry
|
|
77
|
+
function saveRegistry(registry) {
|
|
78
|
+
ensureSessionsDir();
|
|
79
|
+
registry.updated = new Date().toISOString();
|
|
80
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if PID is alive
|
|
84
|
+
function isPidAlive(pid) {
|
|
85
|
+
if (!pid) return false;
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, 0);
|
|
88
|
+
return true;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Get lock file path for session
|
|
95
|
+
function getLockPath(sessionId) {
|
|
96
|
+
return path.join(SESSIONS_DIR, `${sessionId}.lock`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Read lock file
|
|
100
|
+
function readLock(sessionId) {
|
|
101
|
+
const lockPath = getLockPath(sessionId);
|
|
102
|
+
if (!fs.existsSync(lockPath)) return null;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const content = fs.readFileSync(lockPath, 'utf8');
|
|
106
|
+
const lock = {};
|
|
107
|
+
content.split('\n').forEach(line => {
|
|
108
|
+
const [key, value] = line.split('=');
|
|
109
|
+
if (key && value) lock[key.trim()] = value.trim();
|
|
110
|
+
});
|
|
111
|
+
return lock;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Write lock file
|
|
118
|
+
function writeLock(sessionId, pid) {
|
|
119
|
+
const lockPath = getLockPath(sessionId);
|
|
120
|
+
const content = `pid=${pid}\nstarted=${Math.floor(Date.now() / 1000)}\n`;
|
|
121
|
+
fs.writeFileSync(lockPath, content);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Remove lock file
|
|
125
|
+
function removeLock(sessionId) {
|
|
126
|
+
const lockPath = getLockPath(sessionId);
|
|
127
|
+
if (fs.existsSync(lockPath)) {
|
|
128
|
+
fs.unlinkSync(lockPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check if session is active (has lock with alive PID)
|
|
133
|
+
function isSessionActive(sessionId) {
|
|
134
|
+
const lock = readLock(sessionId);
|
|
135
|
+
if (!lock || !lock.pid) return false;
|
|
136
|
+
return isPidAlive(parseInt(lock.pid, 10));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Clean up stale locks
|
|
140
|
+
function cleanupStaleLocks(registry) {
|
|
141
|
+
let cleaned = 0;
|
|
142
|
+
|
|
143
|
+
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
144
|
+
const lock = readLock(id);
|
|
145
|
+
if (lock && !isPidAlive(parseInt(lock.pid, 10))) {
|
|
146
|
+
removeLock(id);
|
|
147
|
+
cleaned++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return cleaned;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Get current git branch
|
|
155
|
+
function getCurrentBranch() {
|
|
156
|
+
try {
|
|
157
|
+
return execSync('git branch --show-current', { cwd: ROOT, encoding: 'utf8' }).trim();
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return 'unknown';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Get current story from status.json
|
|
164
|
+
function getCurrentStory() {
|
|
165
|
+
try {
|
|
166
|
+
const statusPath = path.join(ROOT, 'docs', '09-agents', 'status.json');
|
|
167
|
+
if (!fs.existsSync(statusPath)) return null;
|
|
168
|
+
|
|
169
|
+
const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
170
|
+
for (const [id, story] of Object.entries(status.stories || {})) {
|
|
171
|
+
if (story.status === 'in_progress') {
|
|
172
|
+
return { id, title: story.title };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch (e) {}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Register current session (called on startup)
|
|
180
|
+
function registerSession(nickname = null) {
|
|
181
|
+
const registry = loadRegistry();
|
|
182
|
+
const cwd = process.cwd();
|
|
183
|
+
const branch = getCurrentBranch();
|
|
184
|
+
const story = getCurrentStory();
|
|
185
|
+
const pid = process.ppid || process.pid; // Parent PID (Claude Code) or current
|
|
186
|
+
|
|
187
|
+
// Check if this path already has a session
|
|
188
|
+
let existingId = null;
|
|
189
|
+
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
190
|
+
if (session.path === cwd) {
|
|
191
|
+
existingId = id;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (existingId) {
|
|
197
|
+
// Update existing session
|
|
198
|
+
registry.sessions[existingId].branch = branch;
|
|
199
|
+
registry.sessions[existingId].story = story ? story.id : null;
|
|
200
|
+
registry.sessions[existingId].last_active = new Date().toISOString();
|
|
201
|
+
if (nickname) registry.sessions[existingId].nickname = nickname;
|
|
202
|
+
|
|
203
|
+
writeLock(existingId, pid);
|
|
204
|
+
saveRegistry(registry);
|
|
205
|
+
|
|
206
|
+
return { id: existingId, isNew: false };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Create new session
|
|
210
|
+
const sessionId = String(registry.next_id);
|
|
211
|
+
registry.next_id++;
|
|
212
|
+
|
|
213
|
+
registry.sessions[sessionId] = {
|
|
214
|
+
path: cwd,
|
|
215
|
+
branch,
|
|
216
|
+
story: story ? story.id : null,
|
|
217
|
+
nickname: nickname || null,
|
|
218
|
+
created: new Date().toISOString(),
|
|
219
|
+
last_active: new Date().toISOString(),
|
|
220
|
+
is_main: cwd === ROOT,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
writeLock(sessionId, pid);
|
|
224
|
+
saveRegistry(registry);
|
|
225
|
+
|
|
226
|
+
return { id: sessionId, isNew: true };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Unregister session (called on exit)
|
|
230
|
+
function unregisterSession(sessionId) {
|
|
231
|
+
const registry = loadRegistry();
|
|
232
|
+
|
|
233
|
+
if (registry.sessions[sessionId]) {
|
|
234
|
+
registry.sessions[sessionId].last_active = new Date().toISOString();
|
|
235
|
+
removeLock(sessionId);
|
|
236
|
+
saveRegistry(registry);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Create new session with worktree
|
|
241
|
+
function createSession(options = {}) {
|
|
242
|
+
const registry = loadRegistry();
|
|
243
|
+
const sessionId = String(registry.next_id);
|
|
244
|
+
const projectName = registry.project_name;
|
|
245
|
+
|
|
246
|
+
const nickname = options.nickname || null;
|
|
247
|
+
const branchName = options.branch || `session-${sessionId}`;
|
|
248
|
+
const dirName = nickname || sessionId;
|
|
249
|
+
const worktreePath = path.resolve(ROOT, '..', `${projectName}-${dirName}`);
|
|
250
|
+
|
|
251
|
+
// Check if directory already exists
|
|
252
|
+
if (fs.existsSync(worktreePath)) {
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
error: `Directory already exists: ${worktreePath}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Create branch if it doesn't exist
|
|
260
|
+
try {
|
|
261
|
+
execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, { cwd: ROOT });
|
|
262
|
+
} catch (e) {
|
|
263
|
+
// Branch doesn't exist, create it
|
|
264
|
+
try {
|
|
265
|
+
execSync(`git branch ${branchName}`, { cwd: ROOT, encoding: 'utf8' });
|
|
266
|
+
} catch (e2) {
|
|
267
|
+
return { success: false, error: `Failed to create branch: ${e2.message}` };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Create worktree
|
|
272
|
+
try {
|
|
273
|
+
execSync(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
274
|
+
cwd: ROOT,
|
|
275
|
+
encoding: 'utf8',
|
|
276
|
+
stdio: 'pipe',
|
|
277
|
+
});
|
|
278
|
+
} catch (e) {
|
|
279
|
+
return { success: false, error: `Failed to create worktree: ${e.message}` };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Register session
|
|
283
|
+
registry.next_id++;
|
|
284
|
+
registry.sessions[sessionId] = {
|
|
285
|
+
path: worktreePath,
|
|
286
|
+
branch: branchName,
|
|
287
|
+
story: null,
|
|
288
|
+
nickname,
|
|
289
|
+
created: new Date().toISOString(),
|
|
290
|
+
last_active: new Date().toISOString(),
|
|
291
|
+
is_main: false,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
saveRegistry(registry);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
success: true,
|
|
298
|
+
sessionId,
|
|
299
|
+
path: worktreePath,
|
|
300
|
+
branch: branchName,
|
|
301
|
+
command: `cd "${worktreePath}" && claude`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Get all sessions with status
|
|
306
|
+
function getSessions() {
|
|
307
|
+
const registry = loadRegistry();
|
|
308
|
+
const cleaned = cleanupStaleLocks(registry);
|
|
309
|
+
|
|
310
|
+
const sessions = [];
|
|
311
|
+
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
312
|
+
sessions.push({
|
|
313
|
+
id,
|
|
314
|
+
...session,
|
|
315
|
+
active: isSessionActive(id),
|
|
316
|
+
current: session.path === process.cwd(),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Sort by ID (numeric)
|
|
321
|
+
sessions.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
|
322
|
+
|
|
323
|
+
return { sessions, cleaned };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Get count of active sessions (excluding current)
|
|
327
|
+
function getActiveSessionCount() {
|
|
328
|
+
const { sessions } = getSessions();
|
|
329
|
+
const cwd = process.cwd();
|
|
330
|
+
return sessions.filter(s => s.active && s.path !== cwd).length;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Delete session (and optionally worktree)
|
|
334
|
+
function deleteSession(sessionId, removeWorktree = false) {
|
|
335
|
+
const registry = loadRegistry();
|
|
336
|
+
const session = registry.sessions[sessionId];
|
|
337
|
+
|
|
338
|
+
if (!session) {
|
|
339
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (session.is_main) {
|
|
343
|
+
return { success: false, error: 'Cannot delete main session' };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Remove lock
|
|
347
|
+
removeLock(sessionId);
|
|
348
|
+
|
|
349
|
+
// Remove worktree if requested
|
|
350
|
+
if (removeWorktree && fs.existsSync(session.path)) {
|
|
351
|
+
try {
|
|
352
|
+
execSync(`git worktree remove "${session.path}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
353
|
+
} catch (e) {
|
|
354
|
+
// Try force remove
|
|
355
|
+
try {
|
|
356
|
+
execSync(`git worktree remove --force "${session.path}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
357
|
+
} catch (e2) {
|
|
358
|
+
return { success: false, error: `Failed to remove worktree: ${e2.message}` };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Remove from registry
|
|
364
|
+
delete registry.sessions[sessionId];
|
|
365
|
+
saveRegistry(registry);
|
|
366
|
+
|
|
367
|
+
return { success: true };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Format sessions for display
|
|
371
|
+
function formatSessionsTable(sessions) {
|
|
372
|
+
const lines = [];
|
|
373
|
+
|
|
374
|
+
lines.push(`${c.cyan}Active Sessions:${c.reset}`);
|
|
375
|
+
lines.push(`${'─'.repeat(70)}`);
|
|
376
|
+
|
|
377
|
+
for (const session of sessions) {
|
|
378
|
+
const status = session.active ? `${c.green}●${c.reset}` : `${c.dim}○${c.reset}`;
|
|
379
|
+
const current = session.current ? ` ${c.yellow}(current)${c.reset}` : '';
|
|
380
|
+
const name = session.nickname ? `"${session.nickname}"` : session.branch;
|
|
381
|
+
const story = session.story ? `${c.blue}${session.story}${c.reset}` : `${c.dim}-${c.reset}`;
|
|
382
|
+
|
|
383
|
+
lines.push(` ${status} [${c.bold}${session.id}${c.reset}] ${name}${current}`);
|
|
384
|
+
lines.push(` ${c.dim}Story:${c.reset} ${story} ${c.dim}│ Path:${c.reset} ${session.path}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
lines.push(`${'─'.repeat(70)}`);
|
|
388
|
+
|
|
389
|
+
return lines.join('\n');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// CLI interface
|
|
393
|
+
function main() {
|
|
394
|
+
const args = process.argv.slice(2);
|
|
395
|
+
const command = args[0];
|
|
396
|
+
|
|
397
|
+
switch (command) {
|
|
398
|
+
case 'register': {
|
|
399
|
+
const nickname = args[1] || null;
|
|
400
|
+
const result = registerSession(nickname);
|
|
401
|
+
console.log(JSON.stringify(result));
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
case 'unregister': {
|
|
406
|
+
const sessionId = args[1];
|
|
407
|
+
if (sessionId) {
|
|
408
|
+
unregisterSession(sessionId);
|
|
409
|
+
console.log(JSON.stringify({ success: true }));
|
|
410
|
+
} else {
|
|
411
|
+
console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
|
|
412
|
+
}
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
case 'create': {
|
|
417
|
+
const options = {};
|
|
418
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
419
|
+
const key = args[i].replace('--', '');
|
|
420
|
+
const value = args[i + 1];
|
|
421
|
+
options[key] = value;
|
|
422
|
+
}
|
|
423
|
+
const result = createSession(options);
|
|
424
|
+
console.log(JSON.stringify(result));
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
case 'list': {
|
|
429
|
+
const { sessions, cleaned } = getSessions();
|
|
430
|
+
if (args.includes('--json')) {
|
|
431
|
+
console.log(JSON.stringify({ sessions, cleaned }));
|
|
432
|
+
} else {
|
|
433
|
+
console.log(formatSessionsTable(sessions));
|
|
434
|
+
if (cleaned > 0) {
|
|
435
|
+
console.log(`${c.dim}Cleaned ${cleaned} stale lock(s)${c.reset}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
case 'count': {
|
|
442
|
+
const count = getActiveSessionCount();
|
|
443
|
+
console.log(JSON.stringify({ count }));
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
case 'delete': {
|
|
448
|
+
const sessionId = args[1];
|
|
449
|
+
const removeWorktree = args.includes('--remove-worktree');
|
|
450
|
+
const result = deleteSession(sessionId, removeWorktree);
|
|
451
|
+
console.log(JSON.stringify(result));
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
case 'status': {
|
|
456
|
+
const { sessions } = getSessions();
|
|
457
|
+
const cwd = process.cwd();
|
|
458
|
+
const current = sessions.find(s => s.path === cwd);
|
|
459
|
+
const others = sessions.filter(s => s.active && s.path !== cwd);
|
|
460
|
+
|
|
461
|
+
console.log(
|
|
462
|
+
JSON.stringify({
|
|
463
|
+
current: current || null,
|
|
464
|
+
otherActive: others.length,
|
|
465
|
+
total: sessions.length,
|
|
466
|
+
})
|
|
467
|
+
);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
case 'help':
|
|
472
|
+
default:
|
|
473
|
+
console.log(`
|
|
474
|
+
${c.brand}${c.bold}Session Manager${c.reset} - Multi-session coordination for Claude Code
|
|
475
|
+
|
|
476
|
+
${c.cyan}Commands:${c.reset}
|
|
477
|
+
register [nickname] Register current directory as a session
|
|
478
|
+
unregister <id> Unregister a session (remove lock)
|
|
479
|
+
create [--nickname X] Create new session with git worktree
|
|
480
|
+
list [--json] List all sessions
|
|
481
|
+
count Count other active sessions
|
|
482
|
+
delete <id> [--remove-worktree] Delete session
|
|
483
|
+
status Get current session status
|
|
484
|
+
help Show this help
|
|
485
|
+
|
|
486
|
+
${c.cyan}Examples:${c.reset}
|
|
487
|
+
node session-manager.js register
|
|
488
|
+
node session-manager.js create --nickname auth
|
|
489
|
+
node session-manager.js list
|
|
490
|
+
node session-manager.js delete 2 --remove-worktree
|
|
491
|
+
`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Export for use as module
|
|
496
|
+
module.exports = {
|
|
497
|
+
loadRegistry,
|
|
498
|
+
saveRegistry,
|
|
499
|
+
registerSession,
|
|
500
|
+
unregisterSession,
|
|
501
|
+
createSession,
|
|
502
|
+
getSessions,
|
|
503
|
+
getActiveSessionCount,
|
|
504
|
+
deleteSession,
|
|
505
|
+
isSessionActive,
|
|
506
|
+
cleanupStaleLocks,
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Run CLI if executed directly
|
|
510
|
+
if (require.main === module) {
|
|
511
|
+
main();
|
|
512
|
+
}
|