agileflow 2.99.0 → 2.99.1
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/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/lib/dashboard-protocol.js +38 -0
- package/lib/dashboard-server.js +189 -7
- package/lib/feedback.js +35 -9
- package/lib/git-operations.js +4 -1
- package/lib/merge-operations.js +16 -0
- package/lib/progress.js +7 -6
- package/lib/session-operations.js +601 -0
- package/lib/session-switching.js +186 -0
- package/lib/template-loader.js +4 -2
- package/lib/worktree-operations.js +5 -25
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +12 -0
- package/scripts/batch-pmap-loop.js +11 -4
- package/scripts/claude-tmux.sh +186 -103
- package/scripts/lib/configure-features.js +6 -4
- package/scripts/lib/configure-repair.js +11 -2
- package/scripts/lib/process-cleanup.js +9 -5
- package/scripts/obtain-context.js +5 -0
- package/scripts/session-manager.js +144 -993
- package/scripts/spawn-parallel.js +15 -11
- package/src/core/commands/configure.md +55 -0
- package/src/core/commands/serve.md +127 -0
- package/src/core/commands/session/end.md +83 -22
- package/src/core/commands/session/new.md +197 -83
|
@@ -13,76 +13,50 @@
|
|
|
13
13
|
* - lib/merge-operations.js - Merge, conflict resolution, smart-merge
|
|
14
14
|
* - lib/worktree-operations.js - Worktree creation, cleanup, thread types
|
|
15
15
|
* - lib/session-display.js - Kanban, table formatting, health checks
|
|
16
|
+
* - lib/session-operations.js - Session CRUD, listing, stale lock cleanup
|
|
17
|
+
* - lib/session-switching.js - Session switching, thread type management
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
const fs = require('fs');
|
|
19
21
|
const path = require('path');
|
|
20
|
-
const { spawnSync } = require('child_process');
|
|
21
22
|
|
|
22
23
|
// Shared utilities
|
|
23
24
|
const { c } = require('../lib/colors');
|
|
24
|
-
const {
|
|
25
|
-
getProjectRoot,
|
|
26
|
-
getStatusPath,
|
|
27
|
-
getSessionStatePath,
|
|
28
|
-
getAgileflowDir,
|
|
29
|
-
} = require('../lib/paths');
|
|
30
|
-
const { safeReadJSON } = require('../lib/errors');
|
|
31
|
-
const { isValidBranchName, isValidSessionNickname } = require('../lib/validate');
|
|
25
|
+
const { getProjectRoot, getAgileflowDir } = require('../lib/paths');
|
|
32
26
|
|
|
33
27
|
// Session registry
|
|
34
28
|
const { SessionRegistry } = require('../lib/session-registry');
|
|
35
|
-
const { sessionThreadMachine } = require('../lib/state-machine');
|
|
36
29
|
|
|
37
30
|
// Lock file operations
|
|
38
31
|
const {
|
|
39
|
-
getLockPath: _getLockPath,
|
|
40
32
|
readLock: _readLock,
|
|
41
33
|
readLockAsync: _readLockAsync,
|
|
42
34
|
writeLock: _writeLock,
|
|
43
35
|
removeLock: _removeLock,
|
|
44
|
-
isPidAlive,
|
|
45
36
|
isSessionActive: _isSessionActive,
|
|
46
37
|
isSessionActiveAsync: _isSessionActiveAsync,
|
|
47
38
|
} = require('../lib/lock-file');
|
|
48
39
|
|
|
49
|
-
// Flag detection for session propagation
|
|
50
|
-
const { getInheritedFlags, detectParentSessionFlags } = require('../lib/flag-detection');
|
|
51
|
-
|
|
52
40
|
// Git operations module
|
|
53
41
|
const gitOps = require('../lib/git-operations');
|
|
54
42
|
const {
|
|
55
43
|
gitCache,
|
|
56
44
|
execGitAsync,
|
|
57
|
-
getCurrentBranch,
|
|
58
45
|
getMainBranch,
|
|
59
46
|
SESSION_PHASES,
|
|
60
|
-
determinePhaseFromGitState,
|
|
61
|
-
getSessionPhaseEarlyExit,
|
|
62
47
|
getSessionPhase,
|
|
63
48
|
getSessionPhaseAsync,
|
|
64
49
|
getSessionPhasesAsync,
|
|
65
50
|
} = gitOps;
|
|
66
51
|
|
|
67
52
|
// Worktree operations module
|
|
68
|
-
const
|
|
69
|
-
const {
|
|
70
|
-
THREAD_TYPES,
|
|
71
|
-
DEFAULT_WORKTREE_TIMEOUT_MS,
|
|
72
|
-
isGitWorktree,
|
|
73
|
-
detectThreadType,
|
|
74
|
-
progressIndicator,
|
|
75
|
-
createWorktreeWithTimeout,
|
|
76
|
-
cleanupFailedWorktree,
|
|
77
|
-
} = worktreeOps;
|
|
53
|
+
const { THREAD_TYPES, detectThreadType } = require('../lib/worktree-operations');
|
|
78
54
|
|
|
79
55
|
// Session display module
|
|
80
56
|
const displayOps = require('../lib/session-display');
|
|
81
57
|
const {
|
|
82
58
|
getFileDetails,
|
|
83
59
|
getSessionsHealth: _getSessionsHealth,
|
|
84
|
-
formatKanbanBoard,
|
|
85
|
-
groupSessionsByPhase,
|
|
86
60
|
renderKanbanBoard,
|
|
87
61
|
renderKanbanBoardAsync,
|
|
88
62
|
formatSessionsTable,
|
|
@@ -91,42 +65,19 @@ const {
|
|
|
91
65
|
// Merge operations module
|
|
92
66
|
const mergeOps = require('../lib/merge-operations');
|
|
93
67
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!_featureFlags) {
|
|
98
|
-
try {
|
|
99
|
-
_featureFlags = require('../lib/feature-flags');
|
|
100
|
-
} catch (e) {
|
|
101
|
-
_featureFlags = null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return _featureFlags;
|
|
105
|
-
}
|
|
106
|
-
function getTeamManager() {
|
|
107
|
-
if (!_teamManager) {
|
|
108
|
-
try {
|
|
109
|
-
_teamManager = require('./team-manager');
|
|
110
|
-
} catch (e) {
|
|
111
|
-
_teamManager = null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return _teamManager;
|
|
115
|
-
}
|
|
68
|
+
// Extracted modules
|
|
69
|
+
const { createSessionOperations } = require('../lib/session-operations');
|
|
70
|
+
const { createSessionSwitching } = require('../lib/session-switching');
|
|
116
71
|
|
|
117
72
|
// Constants
|
|
118
73
|
const ROOT = getProjectRoot();
|
|
119
74
|
const SESSIONS_DIR = path.join(getAgileflowDir(ROOT), 'sessions');
|
|
120
|
-
const REGISTRY_PATH = path.join(SESSIONS_DIR, 'registry.json');
|
|
121
75
|
|
|
122
76
|
// Injectable registry instance for testing
|
|
123
77
|
let _registryInstance = null;
|
|
124
78
|
let _registryInitialized = false;
|
|
125
79
|
|
|
126
|
-
//
|
|
127
|
-
// Registry Management
|
|
128
|
-
// ============================================================================
|
|
129
|
-
|
|
80
|
+
// --- Registry Management ---
|
|
130
81
|
function getRegistryInstance() {
|
|
131
82
|
if (!_registryInstance) {
|
|
132
83
|
_registryInstance = new SessionRegistry(ROOT);
|
|
@@ -146,12 +97,6 @@ function resetRegistryCache() {
|
|
|
146
97
|
}
|
|
147
98
|
}
|
|
148
99
|
|
|
149
|
-
function ensureSessionsDir() {
|
|
150
|
-
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
151
|
-
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
100
|
function loadRegistry() {
|
|
156
101
|
const registryInstance = getRegistryInstance();
|
|
157
102
|
if (!_registryInitialized) {
|
|
@@ -171,13 +116,7 @@ function saveRegistry(registryData) {
|
|
|
171
116
|
return registry.saveSync(registryData);
|
|
172
117
|
}
|
|
173
118
|
|
|
174
|
-
//
|
|
175
|
-
// Lock File Wrappers (bind to SESSIONS_DIR)
|
|
176
|
-
// ============================================================================
|
|
177
|
-
|
|
178
|
-
function getLockPath(sessionId) {
|
|
179
|
-
return _getLockPath(SESSIONS_DIR, sessionId);
|
|
180
|
-
}
|
|
119
|
+
// --- Lock File Wrappers (bind to SESSIONS_DIR) ---
|
|
181
120
|
function readLock(sessionId) {
|
|
182
121
|
return _readLock(SESSIONS_DIR, sessionId);
|
|
183
122
|
}
|
|
@@ -197,655 +136,103 @@ async function isSessionActiveAsync(sessionId) {
|
|
|
197
136
|
return _isSessionActiveAsync(SESSIONS_DIR, sessionId);
|
|
198
137
|
}
|
|
199
138
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
pid,
|
|
214
|
-
reason: 'pid_dead',
|
|
215
|
-
path: session.path,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function cleanupStaleLocks(registry, options = {}) {
|
|
220
|
-
const { dryRun = false } = options;
|
|
221
|
-
const cleanedSessions = [];
|
|
222
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
223
|
-
const result = processStalelock(id, session, readLock(id), dryRun);
|
|
224
|
-
if (result) cleanedSessions.push(result);
|
|
225
|
-
}
|
|
226
|
-
return { count: cleanedSessions.length, sessions: cleanedSessions };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async function cleanupStaleLocksAsync(registry, options = {}) {
|
|
230
|
-
const { dryRun = false } = options;
|
|
231
|
-
const sessionEntries = Object.entries(registry.sessions);
|
|
232
|
-
if (sessionEntries.length === 0) return { count: 0, sessions: [] };
|
|
233
|
-
|
|
234
|
-
const lockResults = await Promise.all(
|
|
235
|
-
sessionEntries.map(async ([id, session]) => ({
|
|
236
|
-
id,
|
|
237
|
-
session,
|
|
238
|
-
lock: await readLockAsync(id),
|
|
239
|
-
}))
|
|
240
|
-
);
|
|
139
|
+
// --- Instantiate extracted modules ---
|
|
140
|
+
const sessionOps = createSessionOperations({
|
|
141
|
+
ROOT,
|
|
142
|
+
loadRegistry,
|
|
143
|
+
saveRegistry,
|
|
144
|
+
readLock,
|
|
145
|
+
readLockAsync,
|
|
146
|
+
writeLock,
|
|
147
|
+
removeLock,
|
|
148
|
+
isSessionActive,
|
|
149
|
+
isSessionActiveAsync,
|
|
150
|
+
c,
|
|
151
|
+
});
|
|
241
152
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
153
|
+
const sessionSwitchOps = createSessionSwitching({
|
|
154
|
+
ROOT,
|
|
155
|
+
loadRegistry,
|
|
156
|
+
saveRegistry,
|
|
157
|
+
});
|
|
245
158
|
|
|
246
|
-
|
|
247
|
-
|
|
159
|
+
// Destructure for local use and re-export
|
|
160
|
+
const {
|
|
161
|
+
cleanupStaleLocks,
|
|
162
|
+
cleanupStaleLocksAsync,
|
|
163
|
+
registerSession,
|
|
164
|
+
unregisterSession,
|
|
165
|
+
getSession,
|
|
166
|
+
createSession,
|
|
167
|
+
createTeamSession,
|
|
168
|
+
getSessions,
|
|
169
|
+
getSessionsAsync,
|
|
170
|
+
getActiveSessionCount,
|
|
171
|
+
fullStatus,
|
|
172
|
+
deleteSession,
|
|
173
|
+
} = sessionOps;
|
|
248
174
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
175
|
+
const {
|
|
176
|
+
switchSession,
|
|
177
|
+
clearActiveSession,
|
|
178
|
+
getActiveSession,
|
|
179
|
+
getSessionThreadType,
|
|
180
|
+
setSessionThreadType,
|
|
181
|
+
transitionThread,
|
|
182
|
+
getValidThreadTransitions,
|
|
183
|
+
} = sessionSwitchOps;
|
|
252
184
|
|
|
185
|
+
// --- Session Health ---
|
|
253
186
|
function getSessionsHealth(options = {}) {
|
|
254
187
|
return _getSessionsHealth(options, loadRegistry);
|
|
255
188
|
}
|
|
256
189
|
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
const cwd = process.cwd();
|
|
278
|
-
const branch = getCurrentBranch();
|
|
279
|
-
const story = getCurrentStory();
|
|
280
|
-
const pid = process.ppid || process.pid;
|
|
281
|
-
|
|
282
|
-
let existingId = null;
|
|
283
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
284
|
-
if (session.path === cwd) {
|
|
285
|
-
existingId = id;
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (existingId) {
|
|
291
|
-
registry.sessions[existingId].branch = branch;
|
|
292
|
-
registry.sessions[existingId].story = story ? story.id : null;
|
|
293
|
-
registry.sessions[existingId].last_active = new Date().toISOString();
|
|
294
|
-
if (nickname) registry.sessions[existingId].nickname = nickname;
|
|
295
|
-
if (threadType && THREAD_TYPES.includes(threadType)) {
|
|
296
|
-
registry.sessions[existingId].thread_type = threadType;
|
|
297
|
-
}
|
|
298
|
-
writeLock(existingId, pid);
|
|
299
|
-
saveRegistry(registry);
|
|
300
|
-
return { id: existingId, isNew: false };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const sessionId = String(registry.next_id);
|
|
304
|
-
registry.next_id++;
|
|
305
|
-
const isMain = cwd === ROOT && !isGitWorktree(cwd);
|
|
306
|
-
const detectedType =
|
|
307
|
-
threadType && THREAD_TYPES.includes(threadType) ? threadType : detectThreadType(null, !isMain);
|
|
308
|
-
|
|
309
|
-
registry.sessions[sessionId] = {
|
|
310
|
-
path: cwd,
|
|
311
|
-
branch,
|
|
312
|
-
story: story ? story.id : null,
|
|
313
|
-
nickname: nickname || null,
|
|
314
|
-
created: new Date().toISOString(),
|
|
315
|
-
last_active: new Date().toISOString(),
|
|
316
|
-
is_main: isMain,
|
|
317
|
-
thread_type: detectedType,
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
writeLock(sessionId, pid);
|
|
321
|
-
saveRegistry(registry);
|
|
322
|
-
return { id: sessionId, isNew: true, thread_type: detectedType };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function unregisterSession(sessionId) {
|
|
326
|
-
const registry = loadRegistry();
|
|
327
|
-
if (registry.sessions[sessionId]) {
|
|
328
|
-
registry.sessions[sessionId].last_active = new Date().toISOString();
|
|
329
|
-
removeLock(sessionId);
|
|
330
|
-
saveRegistry(registry);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function getSession(sessionId) {
|
|
335
|
-
const registry = loadRegistry();
|
|
336
|
-
const session = registry.sessions[sessionId];
|
|
337
|
-
if (!session) return null;
|
|
338
|
-
const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
339
|
-
return { id: sessionId, ...session, thread_type: threadType, active: isSessionActive(sessionId) };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async function createSession(options = {}) {
|
|
343
|
-
const registry = loadRegistry();
|
|
344
|
-
const sessionId = String(registry.next_id);
|
|
345
|
-
const projectName = registry.project_name;
|
|
346
|
-
|
|
347
|
-
const nickname = options.nickname || null;
|
|
348
|
-
const branchName = options.branch || `session-${sessionId}`;
|
|
349
|
-
const dirName = nickname || sessionId;
|
|
350
|
-
|
|
351
|
-
if (!isValidBranchName(branchName)) {
|
|
352
|
-
return {
|
|
353
|
-
success: false,
|
|
354
|
-
error: `Invalid branch name: "${branchName}". Use only letters, numbers, hyphens, underscores, and forward slashes.`,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
if (nickname && !isValidSessionNickname(nickname)) {
|
|
358
|
-
return {
|
|
359
|
-
success: false,
|
|
360
|
-
error: `Invalid nickname: "${nickname}". Use only letters, numbers, hyphens, and underscores.`,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const worktreePath = path.resolve(ROOT, '..', `${projectName}-${dirName}`);
|
|
365
|
-
if (fs.existsSync(worktreePath)) {
|
|
366
|
-
return { success: false, error: `Directory already exists: ${worktreePath}` };
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Create branch if needed
|
|
370
|
-
const checkRef = spawnSync(
|
|
371
|
-
'git',
|
|
372
|
-
['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`],
|
|
373
|
-
{ cwd: ROOT, encoding: 'utf8' }
|
|
374
|
-
);
|
|
375
|
-
let branchCreatedByUs = false;
|
|
376
|
-
if (checkRef.status !== 0) {
|
|
377
|
-
const createBranch = spawnSync('git', ['branch', branchName], { cwd: ROOT, encoding: 'utf8' });
|
|
378
|
-
if (createBranch.status !== 0) {
|
|
379
|
-
return {
|
|
380
|
-
success: false,
|
|
381
|
-
error: `Failed to create branch: ${createBranch.stderr || 'unknown error'}`,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
branchCreatedByUs = true;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const timeoutMs = options.timeout || DEFAULT_WORKTREE_TIMEOUT_MS;
|
|
388
|
-
const stopProgress = progressIndicator(
|
|
389
|
-
'Creating worktree (this may take a while for large repos)'
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
try {
|
|
393
|
-
await createWorktreeWithTimeout(worktreePath, branchName, timeoutMs);
|
|
394
|
-
stopProgress();
|
|
395
|
-
process.stderr.write(`✓ Worktree created successfully\n`);
|
|
396
|
-
} catch (error) {
|
|
397
|
-
stopProgress();
|
|
398
|
-
cleanupFailedWorktree(worktreePath, branchName, branchCreatedByUs);
|
|
399
|
-
return { success: false, error: error.message };
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Copy env files
|
|
403
|
-
const envFiles = ['.env', '.env.local', '.env.development', '.env.test', '.env.production'];
|
|
404
|
-
const copiedEnvFiles = [];
|
|
405
|
-
for (const envFile of envFiles) {
|
|
406
|
-
const src = path.join(ROOT, envFile);
|
|
407
|
-
const dest = path.join(worktreePath, envFile);
|
|
408
|
-
if (fs.existsSync(src) && !fs.existsSync(dest)) {
|
|
409
|
-
try {
|
|
410
|
-
fs.copyFileSync(src, dest);
|
|
411
|
-
copiedEnvFiles.push(envFile);
|
|
412
|
-
} catch (e) {
|
|
413
|
-
/* ignore */
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Copy config folders
|
|
419
|
-
const configFoldersToCopy = ['.claude', '.agileflow'];
|
|
420
|
-
const copiedFolders = [];
|
|
421
|
-
for (const folder of configFoldersToCopy) {
|
|
422
|
-
const src = path.join(ROOT, folder);
|
|
423
|
-
const dest = path.join(worktreePath, folder);
|
|
424
|
-
if (fs.existsSync(src)) {
|
|
425
|
-
try {
|
|
426
|
-
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
427
|
-
copiedFolders.push(folder);
|
|
428
|
-
} catch (e) {
|
|
429
|
-
/* ignore */
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Symlink sessions directory
|
|
435
|
-
const sessionsSymlinkSrc = path.join(ROOT, '.agileflow', 'sessions');
|
|
436
|
-
const sessionsSymlinkDest = path.join(worktreePath, '.agileflow', 'sessions');
|
|
437
|
-
if (fs.existsSync(sessionsSymlinkSrc)) {
|
|
438
|
-
try {
|
|
439
|
-
if (fs.existsSync(sessionsSymlinkDest))
|
|
440
|
-
fs.rmSync(sessionsSymlinkDest, { recursive: true, force: true });
|
|
441
|
-
const relPath = path.relative(path.dirname(sessionsSymlinkDest), sessionsSymlinkSrc);
|
|
442
|
-
fs.symlinkSync(relPath, sessionsSymlinkDest, 'dir');
|
|
443
|
-
} catch (e) {
|
|
444
|
-
/* ignore */
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Symlink docs
|
|
449
|
-
const foldersToSymlink = ['docs'];
|
|
450
|
-
const symlinkedFolders = [];
|
|
451
|
-
for (const folder of foldersToSymlink) {
|
|
452
|
-
const src = path.join(ROOT, folder);
|
|
453
|
-
const dest = path.join(worktreePath, folder);
|
|
454
|
-
if (fs.existsSync(src)) {
|
|
455
|
-
try {
|
|
456
|
-
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
457
|
-
const relPath = path.relative(worktreePath, src);
|
|
458
|
-
fs.symlinkSync(relPath, dest, 'dir');
|
|
459
|
-
symlinkedFolders.push(folder);
|
|
460
|
-
} catch (e) {
|
|
461
|
-
try {
|
|
462
|
-
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
463
|
-
copiedFolders.push(folder);
|
|
464
|
-
} catch (copyErr) {
|
|
465
|
-
/* ignore */
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Detect inherited flags from parent Claude session
|
|
472
|
-
const inheritedFlags = options.inheritFlags !== false ? getInheritedFlags() : '';
|
|
473
|
-
|
|
474
|
-
registry.next_id++;
|
|
475
|
-
registry.sessions[sessionId] = {
|
|
476
|
-
path: worktreePath,
|
|
477
|
-
branch: branchName,
|
|
478
|
-
story: null,
|
|
479
|
-
nickname,
|
|
480
|
-
created: new Date().toISOString(),
|
|
481
|
-
last_active: new Date().toISOString(),
|
|
482
|
-
is_main: false,
|
|
483
|
-
thread_type: options.thread_type || 'parallel',
|
|
484
|
-
inherited_flags: inheritedFlags || null,
|
|
485
|
-
};
|
|
486
|
-
saveRegistry(registry);
|
|
487
|
-
|
|
488
|
-
// Build the command with inherited flags
|
|
489
|
-
const claudeCmd = inheritedFlags ? `claude ${inheritedFlags}` : 'claude';
|
|
490
|
-
|
|
491
|
-
return {
|
|
492
|
-
success: true,
|
|
493
|
-
sessionId,
|
|
494
|
-
path: worktreePath,
|
|
495
|
-
branch: branchName,
|
|
496
|
-
thread_type: registry.sessions[sessionId].thread_type,
|
|
497
|
-
command: `cd "${worktreePath}" && ${claudeCmd}`,
|
|
498
|
-
inheritedFlags: inheritedFlags || null,
|
|
499
|
-
envFilesCopied: copiedEnvFiles,
|
|
500
|
-
foldersCopied: copiedFolders,
|
|
501
|
-
foldersSymlinked: symlinkedFolders,
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* Create a native Agent Teams session instead of a worktree session.
|
|
507
|
-
* Falls back to worktree mode if Agent Teams is not enabled.
|
|
508
|
-
*
|
|
509
|
-
* @param {object} options - { template, nickname }
|
|
510
|
-
* @returns {object} Result with session info
|
|
511
|
-
*/
|
|
512
|
-
function createTeamSession(options = {}) {
|
|
513
|
-
const ff = getFeatureFlags();
|
|
514
|
-
const templateName = options.template || 'fullstack';
|
|
515
|
-
|
|
516
|
-
// Check if Agent Teams is enabled
|
|
517
|
-
if (!ff || !ff.isAgentTeamsEnabled({ rootDir: ROOT })) {
|
|
518
|
-
console.error(`${c.yellow}Agent Teams not enabled. Falling back to worktree mode.${c.reset}`);
|
|
519
|
-
return createSession({
|
|
520
|
-
nickname: options.nickname || `team-${templateName}`,
|
|
521
|
-
thread_type: 'parallel',
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Use team-manager to start the team
|
|
526
|
-
const tm = getTeamManager();
|
|
527
|
-
if (!tm) {
|
|
528
|
-
return { success: false, error: 'team-manager module not available' };
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const teamResult = tm.startTeam(ROOT, templateName);
|
|
532
|
-
if (!teamResult.ok) {
|
|
533
|
-
return { success: false, error: teamResult.error || 'Failed to start team' };
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Register as a session in the registry
|
|
537
|
-
const registry = loadRegistry();
|
|
538
|
-
const sessionId = String(registry.next_id);
|
|
539
|
-
registry.next_id++;
|
|
540
|
-
|
|
541
|
-
registry.sessions[sessionId] = {
|
|
542
|
-
path: ROOT,
|
|
543
|
-
branch: getCurrentBranch(),
|
|
544
|
-
story: null,
|
|
545
|
-
nickname: options.nickname || `team-${templateName}`,
|
|
546
|
-
created: new Date().toISOString(),
|
|
547
|
-
last_active: new Date().toISOString(),
|
|
548
|
-
is_main: true,
|
|
549
|
-
type: 'team',
|
|
550
|
-
thread_type: 'team',
|
|
551
|
-
team_name: templateName,
|
|
552
|
-
team_lead: teamResult.lead || null,
|
|
553
|
-
teammates: teamResult.teammates || [],
|
|
554
|
-
};
|
|
555
|
-
saveRegistry(registry);
|
|
556
|
-
|
|
557
|
-
return {
|
|
558
|
-
success: true,
|
|
559
|
-
sessionId,
|
|
560
|
-
type: 'team',
|
|
561
|
-
template: templateName,
|
|
562
|
-
mode: teamResult.mode,
|
|
563
|
-
teammates: teamResult.teammates || [],
|
|
564
|
-
path: ROOT,
|
|
565
|
-
branch: getCurrentBranch(),
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function buildSessionsList(registrySessions, activeChecks, cwd) {
|
|
570
|
-
const sessions = Object.entries(registrySessions).map(([id, session]) => ({
|
|
571
|
-
id,
|
|
572
|
-
...session,
|
|
573
|
-
active: activeChecks[id] || false,
|
|
574
|
-
current: session.path === cwd,
|
|
575
|
-
}));
|
|
576
|
-
sessions.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
|
577
|
-
return sessions;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function getSessions() {
|
|
581
|
-
const registry = loadRegistry();
|
|
582
|
-
const cleanupResult = cleanupStaleLocks(registry);
|
|
583
|
-
const cwd = process.cwd();
|
|
584
|
-
const activeChecks = {};
|
|
585
|
-
for (const id of Object.keys(registry.sessions)) activeChecks[id] = isSessionActive(id);
|
|
586
|
-
return {
|
|
587
|
-
sessions: buildSessionsList(registry.sessions, activeChecks, cwd),
|
|
588
|
-
cleaned: cleanupResult.count,
|
|
589
|
-
cleanedSessions: cleanupResult.sessions,
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
async function getSessionsAsync() {
|
|
594
|
-
const registry = loadRegistry();
|
|
595
|
-
const cleanupResult = await cleanupStaleLocksAsync(registry);
|
|
596
|
-
const sessionEntries = Object.entries(registry.sessions);
|
|
597
|
-
const cwd = process.cwd();
|
|
598
|
-
const activeResults = await Promise.all(
|
|
599
|
-
sessionEntries.map(async ([id]) => [id, await isSessionActiveAsync(id)])
|
|
600
|
-
);
|
|
601
|
-
const activeChecks = Object.fromEntries(activeResults);
|
|
602
|
-
return {
|
|
603
|
-
sessions: buildSessionsList(registry.sessions, activeChecks, cwd),
|
|
604
|
-
cleaned: cleanupResult.count,
|
|
605
|
-
cleanedSessions: cleanupResult.sessions,
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function getActiveSessionCount() {
|
|
610
|
-
const { sessions } = getSessions();
|
|
611
|
-
const cwd = process.cwd();
|
|
612
|
-
return sessions.filter(s => s.active && s.path !== cwd).length;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function deleteSession(sessionId, removeWorktree = false) {
|
|
616
|
-
const registry = loadRegistry();
|
|
617
|
-
const session = registry.sessions[sessionId];
|
|
618
|
-
if (!session) return { success: false, error: `Session ${sessionId} not found` };
|
|
619
|
-
if (session.is_main) return { success: false, error: 'Cannot delete main session' };
|
|
620
|
-
|
|
621
|
-
removeLock(sessionId);
|
|
622
|
-
if (removeWorktree && fs.existsSync(session.path)) {
|
|
623
|
-
const { execFileSync } = require('child_process');
|
|
624
|
-
try {
|
|
625
|
-
execFileSync('git', ['worktree', 'remove', session.path], { cwd: ROOT, encoding: 'utf8' });
|
|
626
|
-
} catch (e) {
|
|
627
|
-
try {
|
|
628
|
-
execFileSync('git', ['worktree', 'remove', '--force', session.path], {
|
|
629
|
-
cwd: ROOT,
|
|
630
|
-
encoding: 'utf8',
|
|
631
|
-
});
|
|
632
|
-
} catch (e2) {
|
|
633
|
-
return { success: false, error: `Failed to remove worktree: ${e2.message}` };
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
delete registry.sessions[sessionId];
|
|
638
|
-
saveRegistry(registry);
|
|
639
|
-
return { success: true };
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// ============================================================================
|
|
643
|
-
// Session Switching
|
|
644
|
-
// ============================================================================
|
|
645
|
-
|
|
646
|
-
const SESSION_STATE_PATH = getSessionStatePath(ROOT);
|
|
647
|
-
|
|
648
|
-
function switchSession(sessionIdOrNickname) {
|
|
649
|
-
const registry = loadRegistry();
|
|
650
|
-
let targetSession = null,
|
|
651
|
-
targetId = null;
|
|
652
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
653
|
-
if (id === sessionIdOrNickname || session.nickname === sessionIdOrNickname) {
|
|
654
|
-
targetSession = session;
|
|
655
|
-
targetId = id;
|
|
656
|
-
break;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
if (!targetSession)
|
|
660
|
-
return { success: false, error: `Session "${sessionIdOrNickname}" not found` };
|
|
661
|
-
if (!fs.existsSync(targetSession.path))
|
|
662
|
-
return { success: false, error: `Session directory does not exist: ${targetSession.path}` };
|
|
190
|
+
// Merge operation wrappers (delegate to merge-operations module)
|
|
191
|
+
const checkMergeability = id => mergeOps.checkMergeability(id, loadRegistry);
|
|
192
|
+
const getMergePreview = id => mergeOps.getMergePreview(id, loadRegistry);
|
|
193
|
+
const integrateSession = (id, opts = {}) =>
|
|
194
|
+
mergeOps.integrateSession(id, opts, loadRegistry, saveRegistry, removeLock);
|
|
195
|
+
const commitChanges = (id, opts = {}) => mergeOps.commitChanges(id, opts, loadRegistry);
|
|
196
|
+
const stashChanges = id => mergeOps.stashChanges(id, loadRegistry);
|
|
197
|
+
const unstashChanges = id => mergeOps.unstashChanges(id);
|
|
198
|
+
const discardChanges = id => mergeOps.discardChanges(id, loadRegistry);
|
|
199
|
+
const categorizeFile = fp => mergeOps.categorizeFile(fp);
|
|
200
|
+
const getMergeStrategy = cat => mergeOps.getMergeStrategy(cat);
|
|
201
|
+
const getConflictingFiles = id => mergeOps.getConflictingFiles(id, loadRegistry);
|
|
202
|
+
const getMergeHistory = () => mergeOps.getMergeHistory();
|
|
203
|
+
const smartMerge = (id, opts = {}) =>
|
|
204
|
+
mergeOps.smartMerge(id, opts, loadRegistry, saveRegistry, removeLock, unregisterSession);
|
|
205
|
+
|
|
206
|
+
// --- CLI Interface ---
|
|
207
|
+
function main() {
|
|
208
|
+
const args = process.argv.slice(2);
|
|
209
|
+
const command = args[0];
|
|
663
210
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
} catch (e) {
|
|
669
|
-
/* start fresh */
|
|
211
|
+
function requireId(label = 'Session ID') {
|
|
212
|
+
if (!args[1]) {
|
|
213
|
+
console.log(JSON.stringify({ success: false, error: `${label} required` }));
|
|
214
|
+
return null;
|
|
670
215
|
}
|
|
216
|
+
return args[1];
|
|
671
217
|
}
|
|
672
218
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
registry.sessions[targetId].last_active = new Date().toISOString();
|
|
687
|
-
saveRegistry(registry);
|
|
688
|
-
|
|
689
|
-
return {
|
|
690
|
-
success: true,
|
|
691
|
-
session: {
|
|
692
|
-
id: targetId,
|
|
693
|
-
nickname: targetSession.nickname,
|
|
694
|
-
path: targetSession.path,
|
|
695
|
-
branch: targetSession.branch,
|
|
696
|
-
},
|
|
697
|
-
path: targetSession.path,
|
|
698
|
-
addDirCommand: `/add-dir ${targetSession.path}`,
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function clearActiveSession() {
|
|
703
|
-
if (!fs.existsSync(SESSION_STATE_PATH)) return { success: true };
|
|
704
|
-
try {
|
|
705
|
-
const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
|
706
|
-
delete sessionState.active_session;
|
|
707
|
-
fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
|
|
708
|
-
return { success: true };
|
|
709
|
-
} catch (e) {
|
|
710
|
-
return { success: false, error: e.message };
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function getActiveSession() {
|
|
715
|
-
if (!fs.existsSync(SESSION_STATE_PATH)) return { active: false };
|
|
716
|
-
try {
|
|
717
|
-
const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
|
718
|
-
return sessionState.active_session
|
|
719
|
-
? { active: true, session: sessionState.active_session }
|
|
720
|
-
: { active: false };
|
|
721
|
-
} catch (e) {
|
|
722
|
-
return { active: false };
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// ============================================================================
|
|
727
|
-
// Thread Type Management
|
|
728
|
-
// ============================================================================
|
|
729
|
-
|
|
730
|
-
function getSessionThreadType(sessionId = null) {
|
|
731
|
-
const registry = loadRegistry();
|
|
732
|
-
const cwd = process.cwd();
|
|
733
|
-
let targetId = sessionId;
|
|
734
|
-
if (!targetId) {
|
|
735
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
736
|
-
if (session.path === cwd) {
|
|
737
|
-
targetId = id;
|
|
738
|
-
break;
|
|
219
|
+
function parseOpts(startIdx, allowedKeys, boolKeys = []) {
|
|
220
|
+
const options = {};
|
|
221
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
222
|
+
const arg = args[i];
|
|
223
|
+
if (!arg.startsWith('--')) continue;
|
|
224
|
+
const eqIndex = arg.indexOf('=');
|
|
225
|
+
let key, value;
|
|
226
|
+
if (eqIndex !== -1) { key = arg.slice(2, eqIndex); value = arg.slice(eqIndex + 1); }
|
|
227
|
+
else { key = arg.slice(2); value = args[++i]; }
|
|
228
|
+
if (!allowedKeys.includes(key)) {
|
|
229
|
+
console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
|
|
230
|
+
return null;
|
|
739
231
|
}
|
|
232
|
+
options[key] = boolKeys.includes(key) ? value !== 'false' : value;
|
|
740
233
|
}
|
|
234
|
+
return options;
|
|
741
235
|
}
|
|
742
|
-
if (!targetId || !registry.sessions[targetId])
|
|
743
|
-
return { success: false, error: 'Session not found' };
|
|
744
|
-
const session = registry.sessions[targetId];
|
|
745
|
-
const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
746
|
-
return { success: true, thread_type: threadType, session_id: targetId, is_main: session.is_main };
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
function setSessionThreadType(sessionId, threadType) {
|
|
750
|
-
if (!THREAD_TYPES.includes(threadType)) {
|
|
751
|
-
return {
|
|
752
|
-
success: false,
|
|
753
|
-
error: `Invalid thread type: ${threadType}. Valid: ${THREAD_TYPES.join(', ')}`,
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
const registry = loadRegistry();
|
|
757
|
-
if (!registry.sessions[sessionId])
|
|
758
|
-
return { success: false, error: `Session ${sessionId} not found` };
|
|
759
|
-
registry.sessions[sessionId].thread_type = threadType;
|
|
760
|
-
saveRegistry(registry);
|
|
761
|
-
return { success: true, thread_type: threadType };
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function transitionThread(sessionId, targetType, options = {}) {
|
|
765
|
-
const { force = false } = options;
|
|
766
|
-
const registry = loadRegistry();
|
|
767
|
-
const session = registry.sessions[sessionId];
|
|
768
|
-
if (!session) return { success: false, error: `Session ${sessionId} not found` };
|
|
769
|
-
|
|
770
|
-
const currentType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
771
|
-
const result = sessionThreadMachine.transition(currentType, targetType, { force });
|
|
772
|
-
if (!result.success)
|
|
773
|
-
return { success: false, from: currentType, to: targetType, error: result.error };
|
|
774
|
-
if (result.noop) return { success: true, from: currentType, to: targetType, noop: true };
|
|
775
|
-
|
|
776
|
-
registry.sessions[sessionId].thread_type = targetType;
|
|
777
|
-
registry.sessions[sessionId].thread_transitioned_at = new Date().toISOString();
|
|
778
|
-
saveRegistry(registry);
|
|
779
|
-
return { success: true, from: currentType, to: targetType, forced: result.forced || false };
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function getValidThreadTransitions(sessionId) {
|
|
783
|
-
const registry = loadRegistry();
|
|
784
|
-
const session = registry.sessions[sessionId];
|
|
785
|
-
if (!session) return { success: false, error: `Session ${sessionId} not found` };
|
|
786
|
-
const currentType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
787
|
-
const validTransitions = sessionThreadMachine.getValidTransitions(currentType);
|
|
788
|
-
return { success: true, current: currentType, validTransitions };
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// ============================================================================
|
|
792
|
-
// Merge Operation Wrappers (delegate to merge-operations module)
|
|
793
|
-
// ============================================================================
|
|
794
|
-
|
|
795
|
-
function checkMergeability(sessionId) {
|
|
796
|
-
return mergeOps.checkMergeability(sessionId, loadRegistry);
|
|
797
|
-
}
|
|
798
|
-
function getMergePreview(sessionId) {
|
|
799
|
-
return mergeOps.getMergePreview(sessionId, loadRegistry);
|
|
800
|
-
}
|
|
801
|
-
function integrateSession(sessionId, options = {}) {
|
|
802
|
-
return mergeOps.integrateSession(sessionId, options, loadRegistry, saveRegistry, removeLock);
|
|
803
|
-
}
|
|
804
|
-
function generateCommitMessage(session) {
|
|
805
|
-
return mergeOps.generateCommitMessage(session);
|
|
806
|
-
}
|
|
807
|
-
function commitChanges(sessionId, options = {}) {
|
|
808
|
-
return mergeOps.commitChanges(sessionId, options, loadRegistry);
|
|
809
|
-
}
|
|
810
|
-
function stashChanges(sessionId) {
|
|
811
|
-
return mergeOps.stashChanges(sessionId, loadRegistry);
|
|
812
|
-
}
|
|
813
|
-
function unstashChanges(sessionId) {
|
|
814
|
-
return mergeOps.unstashChanges(sessionId);
|
|
815
|
-
}
|
|
816
|
-
function discardChanges(sessionId) {
|
|
817
|
-
return mergeOps.discardChanges(sessionId, loadRegistry);
|
|
818
|
-
}
|
|
819
|
-
function categorizeFile(filePath) {
|
|
820
|
-
return mergeOps.categorizeFile(filePath);
|
|
821
|
-
}
|
|
822
|
-
function getMergeStrategy(category) {
|
|
823
|
-
return mergeOps.getMergeStrategy(category);
|
|
824
|
-
}
|
|
825
|
-
function getConflictingFiles(sessionId) {
|
|
826
|
-
return mergeOps.getConflictingFiles(sessionId, loadRegistry);
|
|
827
|
-
}
|
|
828
|
-
function getMergeHistory() {
|
|
829
|
-
return mergeOps.getMergeHistory();
|
|
830
|
-
}
|
|
831
|
-
function smartMerge(sessionId, options = {}) {
|
|
832
|
-
return mergeOps.smartMerge(
|
|
833
|
-
sessionId,
|
|
834
|
-
options,
|
|
835
|
-
loadRegistry,
|
|
836
|
-
saveRegistry,
|
|
837
|
-
removeLock,
|
|
838
|
-
unregisterSession
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// ============================================================================
|
|
843
|
-
// CLI Interface
|
|
844
|
-
// ============================================================================
|
|
845
|
-
|
|
846
|
-
function main() {
|
|
847
|
-
const args = process.argv.slice(2);
|
|
848
|
-
const command = args[0];
|
|
849
236
|
|
|
850
237
|
switch (command) {
|
|
851
238
|
case 'register': {
|
|
@@ -856,50 +243,25 @@ function main() {
|
|
|
856
243
|
}
|
|
857
244
|
|
|
858
245
|
case 'unregister': {
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
console.log(JSON.stringify({ success: true }));
|
|
863
|
-
} else console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
|
|
246
|
+
const id = requireId(); if (!id) return;
|
|
247
|
+
unregisterSession(id);
|
|
248
|
+
console.log(JSON.stringify({ success: true }));
|
|
864
249
|
break;
|
|
865
250
|
}
|
|
866
251
|
|
|
867
252
|
case 'create': {
|
|
868
|
-
const options =
|
|
869
|
-
|
|
870
|
-
for (let i = 1; i < args.length; i++) {
|
|
871
|
-
const arg = args[i];
|
|
872
|
-
if (arg.startsWith('--')) {
|
|
873
|
-
const key = arg.slice(2).split('=')[0];
|
|
874
|
-
if (!allowedKeys.includes(key)) {
|
|
875
|
-
console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
const eqIndex = arg.indexOf('=');
|
|
879
|
-
if (eqIndex !== -1) options[key] = arg.slice(eqIndex + 1);
|
|
880
|
-
else if (args[i + 1] && !args[i + 1].startsWith('--')) options[key] = args[++i];
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// Team mode: create a native Agent Teams session
|
|
253
|
+
const options = parseOpts(1, ['nickname', 'branch', 'timeout', 'mode', 'template']);
|
|
254
|
+
if (!options) return;
|
|
885
255
|
if (options.mode === 'team') {
|
|
886
|
-
|
|
887
|
-
template: options.template || 'fullstack',
|
|
888
|
-
|
|
889
|
-
});
|
|
890
|
-
console.log(JSON.stringify(result));
|
|
256
|
+
console.log(JSON.stringify(createTeamSession({
|
|
257
|
+
template: options.template || 'fullstack', nickname: options.nickname,
|
|
258
|
+
})));
|
|
891
259
|
break;
|
|
892
260
|
}
|
|
893
|
-
|
|
894
261
|
if (options.timeout) {
|
|
895
262
|
options.timeout = parseInt(options.timeout, 10);
|
|
896
263
|
if (isNaN(options.timeout) || options.timeout < 1000) {
|
|
897
|
-
console.log(
|
|
898
|
-
JSON.stringify({
|
|
899
|
-
success: false,
|
|
900
|
-
error: 'Timeout must be a number >= 1000 (milliseconds)',
|
|
901
|
-
})
|
|
902
|
-
);
|
|
264
|
+
console.log(JSON.stringify({ success: false, error: 'Timeout must be a number >= 1000 (milliseconds)' }));
|
|
903
265
|
return;
|
|
904
266
|
}
|
|
905
267
|
}
|
|
@@ -958,234 +320,71 @@ function main() {
|
|
|
958
320
|
}
|
|
959
321
|
|
|
960
322
|
case 'get': {
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
const session = getSession(sessionId);
|
|
967
|
-
if (!session) {
|
|
968
|
-
console.log(JSON.stringify({ success: false, error: `Session ${sessionId} not found` }));
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
323
|
+
const id = requireId(); if (!id) return;
|
|
324
|
+
const session = getSession(id);
|
|
325
|
+
if (!session) { console.log(JSON.stringify({ success: false, error: `Session ${id} not found` })); return; }
|
|
971
326
|
console.log(JSON.stringify({ success: true, ...session }));
|
|
972
327
|
break;
|
|
973
328
|
}
|
|
974
329
|
|
|
975
330
|
case 'full-status': {
|
|
976
|
-
|
|
977
|
-
const cwd = process.cwd();
|
|
978
|
-
const registry = loadRegistry();
|
|
979
|
-
const branch = getCurrentBranch();
|
|
980
|
-
const story = getCurrentStory();
|
|
981
|
-
const pid = process.ppid || process.pid;
|
|
982
|
-
|
|
983
|
-
let sessionId = null,
|
|
984
|
-
isNew = false;
|
|
985
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
986
|
-
if (session.path === cwd) {
|
|
987
|
-
sessionId = id;
|
|
988
|
-
break;
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
if (sessionId) {
|
|
993
|
-
registry.sessions[sessionId].branch = branch;
|
|
994
|
-
registry.sessions[sessionId].story = story ? story.id : null;
|
|
995
|
-
registry.sessions[sessionId].last_active = new Date().toISOString();
|
|
996
|
-
if (nickname) registry.sessions[sessionId].nickname = nickname;
|
|
997
|
-
if (!registry.sessions[sessionId].thread_type)
|
|
998
|
-
registry.sessions[sessionId].thread_type = registry.sessions[sessionId].is_main
|
|
999
|
-
? 'base'
|
|
1000
|
-
: 'parallel';
|
|
1001
|
-
writeLock(sessionId, pid);
|
|
1002
|
-
} else {
|
|
1003
|
-
sessionId = String(registry.next_id);
|
|
1004
|
-
registry.next_id++;
|
|
1005
|
-
const isMain = cwd === ROOT && !isGitWorktree(cwd);
|
|
1006
|
-
registry.sessions[sessionId] = {
|
|
1007
|
-
path: cwd,
|
|
1008
|
-
branch,
|
|
1009
|
-
story: story ? story.id : null,
|
|
1010
|
-
nickname: nickname || null,
|
|
1011
|
-
created: new Date().toISOString(),
|
|
1012
|
-
last_active: new Date().toISOString(),
|
|
1013
|
-
is_main: isMain,
|
|
1014
|
-
thread_type: isMain ? 'base' : 'parallel',
|
|
1015
|
-
};
|
|
1016
|
-
writeLock(sessionId, pid);
|
|
1017
|
-
isNew = true;
|
|
1018
|
-
}
|
|
1019
|
-
saveRegistry(registry);
|
|
1020
|
-
|
|
1021
|
-
const cleanupResult = cleanupStaleLocks(registry);
|
|
1022
|
-
const filteredCleanup = {
|
|
1023
|
-
count: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)).length,
|
|
1024
|
-
sessions: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)),
|
|
1025
|
-
};
|
|
1026
|
-
|
|
1027
|
-
const sessions = [];
|
|
1028
|
-
let otherActive = 0;
|
|
1029
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
1030
|
-
const active = isSessionActive(id);
|
|
1031
|
-
const isCurrent = session.path === cwd;
|
|
1032
|
-
sessions.push({ id, ...session, active, current: isCurrent });
|
|
1033
|
-
if (active && !isCurrent) otherActive++;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
console.log(
|
|
1037
|
-
JSON.stringify({
|
|
1038
|
-
registered: true,
|
|
1039
|
-
id: sessionId,
|
|
1040
|
-
isNew,
|
|
1041
|
-
current: sessions.find(s => s.current) || null,
|
|
1042
|
-
otherActive,
|
|
1043
|
-
total: sessions.length,
|
|
1044
|
-
cleaned: filteredCleanup.count,
|
|
1045
|
-
cleanedSessions: filteredCleanup.sessions,
|
|
1046
|
-
})
|
|
1047
|
-
);
|
|
331
|
+
console.log(JSON.stringify(fullStatus(args[1] || null)));
|
|
1048
332
|
break;
|
|
1049
333
|
}
|
|
1050
334
|
|
|
1051
335
|
case 'check-merge': {
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
console.log(JSON.stringify(checkMergeability(sessionId)));
|
|
336
|
+
const id = requireId(); if (!id) return;
|
|
337
|
+
console.log(JSON.stringify(checkMergeability(id)));
|
|
1058
338
|
break;
|
|
1059
339
|
}
|
|
1060
340
|
case 'merge-preview': {
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
console.log(JSON.stringify(getMergePreview(sessionId)));
|
|
341
|
+
const id = requireId(); if (!id) return;
|
|
342
|
+
console.log(JSON.stringify(getMergePreview(id)));
|
|
1067
343
|
break;
|
|
1068
344
|
}
|
|
1069
|
-
|
|
1070
345
|
case 'integrate': {
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
1076
|
-
const options = {};
|
|
1077
|
-
const allowedKeys = ['strategy', 'deleteBranch', 'deleteWorktree', 'message'];
|
|
1078
|
-
for (let i = 2; i < args.length; i++) {
|
|
1079
|
-
const arg = args[i];
|
|
1080
|
-
if (arg.startsWith('--')) {
|
|
1081
|
-
const eqIndex = arg.indexOf('=');
|
|
1082
|
-
let key, value;
|
|
1083
|
-
if (eqIndex !== -1) {
|
|
1084
|
-
key = arg.slice(2, eqIndex);
|
|
1085
|
-
value = arg.slice(eqIndex + 1);
|
|
1086
|
-
} else {
|
|
1087
|
-
key = arg.slice(2);
|
|
1088
|
-
value = args[++i];
|
|
1089
|
-
}
|
|
1090
|
-
if (!allowedKeys.includes(key)) {
|
|
1091
|
-
console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
if (key === 'deleteBranch' || key === 'deleteWorktree') options[key] = value !== 'false';
|
|
1095
|
-
else options[key] = value;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
console.log(JSON.stringify(integrateSession(sessionId, options)));
|
|
346
|
+
const id = requireId(); if (!id) return;
|
|
347
|
+
const opts = parseOpts(2, ['strategy', 'deleteBranch', 'deleteWorktree', 'message'], ['deleteBranch', 'deleteWorktree']);
|
|
348
|
+
if (!opts) return;
|
|
349
|
+
console.log(JSON.stringify(integrateSession(id, opts)));
|
|
1099
350
|
break;
|
|
1100
351
|
}
|
|
1101
|
-
|
|
1102
352
|
case 'commit-changes': {
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
const options = {};
|
|
1109
|
-
for (let i = 2; i < args.length; i++) {
|
|
1110
|
-
const arg = args[i];
|
|
1111
|
-
if (arg.startsWith('--message=')) options.message = arg.slice(10);
|
|
1112
|
-
else if (arg === '--message' && args[i + 1]) options.message = args[++i];
|
|
1113
|
-
}
|
|
1114
|
-
console.log(JSON.stringify(commitChanges(sessionId, options)));
|
|
353
|
+
const id = requireId(); if (!id) return;
|
|
354
|
+
const opts = parseOpts(2, ['message']);
|
|
355
|
+
if (!opts) return;
|
|
356
|
+
console.log(JSON.stringify(commitChanges(id, opts)));
|
|
1115
357
|
break;
|
|
1116
358
|
}
|
|
1117
|
-
|
|
1118
359
|
case 'stash': {
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
console.log(JSON.stringify(stashChanges(sessionId)));
|
|
360
|
+
const id = requireId(); if (!id) return;
|
|
361
|
+
console.log(JSON.stringify(stashChanges(id)));
|
|
1125
362
|
break;
|
|
1126
363
|
}
|
|
1127
364
|
case 'unstash': {
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
console.log(JSON.stringify(unstashChanges(sessionId)));
|
|
365
|
+
const id = requireId(); if (!id) return;
|
|
366
|
+
console.log(JSON.stringify(unstashChanges(id)));
|
|
1134
367
|
break;
|
|
1135
368
|
}
|
|
1136
369
|
case 'discard-changes': {
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
console.log(JSON.stringify(discardChanges(sessionId)));
|
|
370
|
+
const id = requireId(); if (!id) return;
|
|
371
|
+
console.log(JSON.stringify(discardChanges(id)));
|
|
1143
372
|
break;
|
|
1144
373
|
}
|
|
1145
|
-
|
|
1146
374
|
case 'smart-merge': {
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
}
|
|
1152
|
-
const options = {};
|
|
1153
|
-
const allowedKeys = ['strategy', 'deleteBranch', 'deleteWorktree', 'message'];
|
|
1154
|
-
for (let i = 2; i < args.length; i++) {
|
|
1155
|
-
const arg = args[i];
|
|
1156
|
-
if (arg.startsWith('--')) {
|
|
1157
|
-
const eqIndex = arg.indexOf('=');
|
|
1158
|
-
let key, value;
|
|
1159
|
-
if (eqIndex !== -1) {
|
|
1160
|
-
key = arg.slice(2, eqIndex);
|
|
1161
|
-
value = arg.slice(eqIndex + 1);
|
|
1162
|
-
} else {
|
|
1163
|
-
key = arg.slice(2);
|
|
1164
|
-
value = args[++i];
|
|
1165
|
-
}
|
|
1166
|
-
if (!allowedKeys.includes(key)) {
|
|
1167
|
-
console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
if (key === 'deleteBranch' || key === 'deleteWorktree') options[key] = value !== 'false';
|
|
1171
|
-
else options[key] = value;
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
console.log(JSON.stringify(smartMerge(sessionId, options), null, 2));
|
|
375
|
+
const id = requireId(); if (!id) return;
|
|
376
|
+
const opts = parseOpts(2, ['strategy', 'deleteBranch', 'deleteWorktree', 'message'], ['deleteBranch', 'deleteWorktree']);
|
|
377
|
+
if (!opts) return;
|
|
378
|
+
console.log(JSON.stringify(smartMerge(id, opts), null, 2));
|
|
1175
379
|
break;
|
|
1176
380
|
}
|
|
1177
|
-
|
|
1178
381
|
case 'merge-history': {
|
|
1179
382
|
console.log(JSON.stringify(getMergeHistory(), null, 2));
|
|
1180
383
|
break;
|
|
1181
384
|
}
|
|
1182
385
|
case 'switch': {
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
console.log(JSON.stringify({ success: false, error: 'Session ID or nickname required' }));
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
console.log(JSON.stringify(switchSession(sessionIdOrNickname), null, 2));
|
|
386
|
+
const id = requireId('Session ID or nickname'); if (!id) return;
|
|
387
|
+
console.log(JSON.stringify(switchSession(id), null, 2));
|
|
1189
388
|
break;
|
|
1190
389
|
}
|
|
1191
390
|
case 'active': {
|
|
@@ -1252,71 +451,23 @@ ${c.cyan}Commands:${c.reset}
|
|
|
1252
451
|
}
|
|
1253
452
|
}
|
|
1254
453
|
|
|
1255
|
-
//
|
|
1256
|
-
// Exports
|
|
1257
|
-
// ============================================================================
|
|
1258
|
-
|
|
454
|
+
// --- Exports ---
|
|
1259
455
|
module.exports = {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
getActiveSessionCount,
|
|
1276
|
-
deleteSession,
|
|
1277
|
-
isSessionActive,
|
|
1278
|
-
isSessionActiveAsync,
|
|
1279
|
-
cleanupStaleLocks,
|
|
1280
|
-
cleanupStaleLocksAsync,
|
|
1281
|
-
// Session switching
|
|
1282
|
-
switchSession,
|
|
1283
|
-
clearActiveSession,
|
|
1284
|
-
getActiveSession,
|
|
1285
|
-
// Thread type tracking
|
|
1286
|
-
THREAD_TYPES,
|
|
1287
|
-
detectThreadType,
|
|
1288
|
-
getSessionThreadType,
|
|
1289
|
-
setSessionThreadType,
|
|
1290
|
-
transitionThread,
|
|
1291
|
-
getValidThreadTransitions,
|
|
1292
|
-
// Merge operations (delegated to module)
|
|
1293
|
-
getMainBranch,
|
|
1294
|
-
checkMergeability,
|
|
1295
|
-
getMergePreview,
|
|
1296
|
-
integrateSession,
|
|
1297
|
-
commitChanges,
|
|
1298
|
-
stashChanges,
|
|
1299
|
-
unstashChanges,
|
|
1300
|
-
discardChanges,
|
|
1301
|
-
smartMerge,
|
|
1302
|
-
getConflictingFiles,
|
|
1303
|
-
categorizeFile,
|
|
1304
|
-
getMergeStrategy,
|
|
1305
|
-
getMergeHistory,
|
|
1306
|
-
// Kanban visualization
|
|
1307
|
-
SESSION_PHASES,
|
|
1308
|
-
getSessionPhase,
|
|
1309
|
-
getSessionPhaseAsync,
|
|
1310
|
-
getSessionPhasesAsync,
|
|
1311
|
-
renderKanbanBoard,
|
|
1312
|
-
renderKanbanBoardAsync,
|
|
1313
|
-
// Display
|
|
1314
|
-
formatSessionsTable,
|
|
1315
|
-
getFileDetails,
|
|
1316
|
-
getSessionsHealth,
|
|
1317
|
-
// Internal utilities (for testing)
|
|
1318
|
-
execGitAsync,
|
|
1319
|
-
gitCache,
|
|
456
|
+
injectRegistry, getRegistryInstance, resetRegistryCache,
|
|
457
|
+
loadRegistry, saveRegistry,
|
|
458
|
+
registerSession, unregisterSession, getSession, createSession, createTeamSession,
|
|
459
|
+
getSessions, getSessionsAsync, getActiveSessionCount, deleteSession,
|
|
460
|
+
isSessionActive, isSessionActiveAsync, cleanupStaleLocks, cleanupStaleLocksAsync,
|
|
461
|
+
switchSession, clearActiveSession, getActiveSession,
|
|
462
|
+
THREAD_TYPES, detectThreadType, getSessionThreadType, setSessionThreadType,
|
|
463
|
+
transitionThread, getValidThreadTransitions,
|
|
464
|
+
getMainBranch, checkMergeability, getMergePreview, integrateSession,
|
|
465
|
+
commitChanges, stashChanges, unstashChanges, discardChanges,
|
|
466
|
+
smartMerge, getConflictingFiles, categorizeFile, getMergeStrategy, getMergeHistory,
|
|
467
|
+
SESSION_PHASES, getSessionPhase, getSessionPhaseAsync, getSessionPhasesAsync,
|
|
468
|
+
renderKanbanBoard, renderKanbanBoardAsync,
|
|
469
|
+
formatSessionsTable, getFileDetails, getSessionsHealth,
|
|
470
|
+
execGitAsync, gitCache,
|
|
1320
471
|
};
|
|
1321
472
|
|
|
1322
473
|
if (require.main === module) main();
|