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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-switching.js - Session switching and thread type management
|
|
3
|
+
*
|
|
4
|
+
* Extracted from session-manager.js to reduce file size.
|
|
5
|
+
* Uses factory pattern for dependency injection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const { getSessionStatePath } = require('./paths');
|
|
12
|
+
const { sessionThreadMachine } = require('./state-machine');
|
|
13
|
+
const { THREAD_TYPES } = require('./worktree-operations');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create session switching operations bound to the given dependencies.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} deps
|
|
19
|
+
* @param {string} deps.ROOT - Project root path
|
|
20
|
+
* @param {Function} deps.loadRegistry - Load registry data
|
|
21
|
+
* @param {Function} deps.saveRegistry - Save registry data
|
|
22
|
+
*/
|
|
23
|
+
function createSessionSwitching(deps) {
|
|
24
|
+
const { ROOT, loadRegistry, saveRegistry } = deps;
|
|
25
|
+
|
|
26
|
+
const SESSION_STATE_PATH = getSessionStatePath(ROOT);
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Session Switching
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
function switchSession(sessionIdOrNickname) {
|
|
33
|
+
const registry = loadRegistry();
|
|
34
|
+
let targetSession = null,
|
|
35
|
+
targetId = null;
|
|
36
|
+
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
37
|
+
if (id === sessionIdOrNickname || session.nickname === sessionIdOrNickname) {
|
|
38
|
+
targetSession = session;
|
|
39
|
+
targetId = id;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!targetSession)
|
|
44
|
+
return { success: false, error: `Session "${sessionIdOrNickname}" not found` };
|
|
45
|
+
if (!fs.existsSync(targetSession.path))
|
|
46
|
+
return { success: false, error: `Session directory does not exist: ${targetSession.path}` };
|
|
47
|
+
|
|
48
|
+
let sessionState = {};
|
|
49
|
+
if (fs.existsSync(SESSION_STATE_PATH)) {
|
|
50
|
+
try {
|
|
51
|
+
sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
/* start fresh */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sessionState.active_session = {
|
|
58
|
+
id: targetId,
|
|
59
|
+
nickname: targetSession.nickname,
|
|
60
|
+
path: targetSession.path,
|
|
61
|
+
branch: targetSession.branch,
|
|
62
|
+
switched_at: new Date().toISOString(),
|
|
63
|
+
original_cwd: ROOT,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const stateDir = path.dirname(SESSION_STATE_PATH);
|
|
67
|
+
if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true });
|
|
68
|
+
fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
|
|
69
|
+
|
|
70
|
+
registry.sessions[targetId].last_active = new Date().toISOString();
|
|
71
|
+
saveRegistry(registry);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
session: {
|
|
76
|
+
id: targetId,
|
|
77
|
+
nickname: targetSession.nickname,
|
|
78
|
+
path: targetSession.path,
|
|
79
|
+
branch: targetSession.branch,
|
|
80
|
+
},
|
|
81
|
+
path: targetSession.path,
|
|
82
|
+
addDirCommand: `/add-dir ${targetSession.path}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function clearActiveSession() {
|
|
87
|
+
if (!fs.existsSync(SESSION_STATE_PATH)) return { success: true };
|
|
88
|
+
try {
|
|
89
|
+
const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
|
90
|
+
delete sessionState.active_session;
|
|
91
|
+
fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
|
|
92
|
+
return { success: true };
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return { success: false, error: e.message };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getActiveSession() {
|
|
99
|
+
if (!fs.existsSync(SESSION_STATE_PATH)) return { active: false };
|
|
100
|
+
try {
|
|
101
|
+
const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
|
102
|
+
return sessionState.active_session
|
|
103
|
+
? { active: true, session: sessionState.active_session }
|
|
104
|
+
: { active: false };
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return { active: false };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Thread Type Management
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
function getSessionThreadType(sessionId = null) {
|
|
115
|
+
const registry = loadRegistry();
|
|
116
|
+
const cwd = process.cwd();
|
|
117
|
+
let targetId = sessionId;
|
|
118
|
+
if (!targetId) {
|
|
119
|
+
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
120
|
+
if (session.path === cwd) {
|
|
121
|
+
targetId = id;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!targetId || !registry.sessions[targetId])
|
|
127
|
+
return { success: false, error: 'Session not found' };
|
|
128
|
+
const session = registry.sessions[targetId];
|
|
129
|
+
const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
130
|
+
return { success: true, thread_type: threadType, session_id: targetId, is_main: session.is_main };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function setSessionThreadType(sessionId, threadType) {
|
|
134
|
+
if (!THREAD_TYPES.includes(threadType)) {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
error: `Invalid thread type: ${threadType}. Valid: ${THREAD_TYPES.join(', ')}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const registry = loadRegistry();
|
|
141
|
+
if (!registry.sessions[sessionId])
|
|
142
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
143
|
+
registry.sessions[sessionId].thread_type = threadType;
|
|
144
|
+
saveRegistry(registry);
|
|
145
|
+
return { success: true, thread_type: threadType };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function transitionThread(sessionId, targetType, options = {}) {
|
|
149
|
+
const { force = false } = options;
|
|
150
|
+
const registry = loadRegistry();
|
|
151
|
+
const session = registry.sessions[sessionId];
|
|
152
|
+
if (!session) return { success: false, error: `Session ${sessionId} not found` };
|
|
153
|
+
|
|
154
|
+
const currentType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
155
|
+
const result = sessionThreadMachine.transition(currentType, targetType, { force });
|
|
156
|
+
if (!result.success)
|
|
157
|
+
return { success: false, from: currentType, to: targetType, error: result.error };
|
|
158
|
+
if (result.noop) return { success: true, from: currentType, to: targetType, noop: true };
|
|
159
|
+
|
|
160
|
+
registry.sessions[sessionId].thread_type = targetType;
|
|
161
|
+
registry.sessions[sessionId].thread_transitioned_at = new Date().toISOString();
|
|
162
|
+
saveRegistry(registry);
|
|
163
|
+
return { success: true, from: currentType, to: targetType, forced: result.forced || false };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getValidThreadTransitions(sessionId) {
|
|
167
|
+
const registry = loadRegistry();
|
|
168
|
+
const session = registry.sessions[sessionId];
|
|
169
|
+
if (!session) return { success: false, error: `Session ${sessionId} not found` };
|
|
170
|
+
const currentType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
171
|
+
const validTransitions = sessionThreadMachine.getValidTransitions(currentType);
|
|
172
|
+
return { success: true, current: currentType, validTransitions };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
switchSession,
|
|
177
|
+
clearActiveSession,
|
|
178
|
+
getActiveSession,
|
|
179
|
+
getSessionThreadType,
|
|
180
|
+
setSessionThreadType,
|
|
181
|
+
transitionThread,
|
|
182
|
+
getValidThreadTransitions,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { createSessionSwitching };
|
package/lib/template-loader.js
CHANGED
|
@@ -90,8 +90,10 @@ function extractSimpleFrontmatter(raw) {
|
|
|
90
90
|
const key = line.substring(0, colonIdx).trim();
|
|
91
91
|
let value = line.substring(colonIdx + 1).trim();
|
|
92
92
|
// Remove quotes if present
|
|
93
|
-
if (
|
|
94
|
-
|
|
93
|
+
if (
|
|
94
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
95
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
96
|
+
) {
|
|
95
97
|
value = value.slice(1, -1);
|
|
96
98
|
}
|
|
97
99
|
frontmatter[key] = value;
|
|
@@ -57,36 +57,16 @@ function detectThreadType(session, isWorktree = false) {
|
|
|
57
57
|
/**
|
|
58
58
|
* Display progress feedback during long operations.
|
|
59
59
|
* Returns a function to stop the progress indicator.
|
|
60
|
+
* Uses stderr to avoid corrupting stdout JSON output from callers.
|
|
60
61
|
*
|
|
61
62
|
* @param {string} message - Progress message
|
|
62
63
|
* @returns {function} Stop function
|
|
63
64
|
*/
|
|
64
65
|
function progressIndicator(message) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// For TTY (interactive terminal), show spinner
|
|
70
|
-
if (process.stderr.isTTY) {
|
|
71
|
-
const interval = setInterval(() => {
|
|
72
|
-
process.stderr.write(`\r${frames[frameIndex++ % frames.length]} ${message}`);
|
|
73
|
-
}, 80);
|
|
74
|
-
return () => {
|
|
75
|
-
clearInterval(interval);
|
|
76
|
-
process.stderr.write(`\r${' '.repeat(message.length + 2)}\r`);
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// For non-TTY (Claude Code, piped output), emit periodic updates to stderr
|
|
81
|
-
process.stderr.write(`⏳ ${message}...\n`);
|
|
82
|
-
const interval = setInterval(() => {
|
|
83
|
-
elapsed += 10;
|
|
84
|
-
process.stderr.write(`⏳ Still working... (${elapsed}s elapsed)\n`);
|
|
85
|
-
}, 10000); // Update every 10 seconds
|
|
86
|
-
|
|
87
|
-
return () => {
|
|
88
|
-
clearInterval(interval);
|
|
89
|
-
};
|
|
66
|
+
const { FeedbackSpinner } = require('./feedback');
|
|
67
|
+
const spinner = new FeedbackSpinner(message, { stream: process.stderr });
|
|
68
|
+
spinner.start();
|
|
69
|
+
return () => spinner.succeed();
|
|
90
70
|
}
|
|
91
71
|
|
|
92
72
|
/**
|
package/package.json
CHANGED
|
@@ -44,6 +44,7 @@ const {
|
|
|
44
44
|
upgradeFeatures,
|
|
45
45
|
} = require('./lib/configure-features');
|
|
46
46
|
const { listScripts, showVersionInfo, repairScripts } = require('./lib/configure-repair');
|
|
47
|
+
const { feedback } = require('../lib/feedback');
|
|
47
48
|
|
|
48
49
|
// ============================================================================
|
|
49
50
|
// VERSION
|
|
@@ -281,7 +282,10 @@ function main() {
|
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
// Always detect first
|
|
285
|
+
const spinner = feedback.spinner('Detecting configuration...');
|
|
286
|
+
spinner.start();
|
|
284
287
|
const status = detectConfig(VERSION);
|
|
288
|
+
spinner.succeed('Configuration detected');
|
|
285
289
|
const { hasIssues, hasOutdated } = printStatus(status);
|
|
286
290
|
|
|
287
291
|
// Detect only mode
|
|
@@ -317,11 +321,16 @@ function main() {
|
|
|
317
321
|
actions.disabled = p.disable || [];
|
|
318
322
|
}
|
|
319
323
|
|
|
324
|
+
// Enable/disable specific features with progress tracking
|
|
325
|
+
const totalChanges = enable.length + disable.length;
|
|
326
|
+
const featureTask = totalChanges > 1 ? feedback.task('Applying feature changes', totalChanges) : null;
|
|
327
|
+
|
|
320
328
|
// Enable specific features
|
|
321
329
|
enable.forEach(f => {
|
|
322
330
|
if (enableFeature(f, { archivalDays }, VERSION)) {
|
|
323
331
|
actions.enabled.push(f);
|
|
324
332
|
}
|
|
333
|
+
if (featureTask) featureTask.step(`Enabled ${f}`);
|
|
325
334
|
});
|
|
326
335
|
|
|
327
336
|
// Disable specific features
|
|
@@ -329,8 +338,11 @@ function main() {
|
|
|
329
338
|
if (disableFeature(f, VERSION)) {
|
|
330
339
|
actions.disabled.push(f);
|
|
331
340
|
}
|
|
341
|
+
if (featureTask) featureTask.step(`Disabled ${f}`);
|
|
332
342
|
});
|
|
333
343
|
|
|
344
|
+
if (featureTask) featureTask.complete('Feature changes applied');
|
|
345
|
+
|
|
334
346
|
// Print summary if anything changed
|
|
335
347
|
if (actions.enabled.length > 0 || actions.disabled.length > 0) {
|
|
336
348
|
printSummary(actions);
|
|
@@ -27,6 +27,7 @@ const { c } = require('../lib/colors');
|
|
|
27
27
|
const { getProjectRoot } = require('../lib/paths');
|
|
28
28
|
const { safeReadJSON, safeWriteJSON } = require('../lib/errors');
|
|
29
29
|
const { parseIntBounded } = require('../lib/validate');
|
|
30
|
+
const { feedback } = require('../lib/feedback');
|
|
30
31
|
|
|
31
32
|
// ===== SESSION STATE HELPERS =====
|
|
32
33
|
|
|
@@ -261,10 +262,14 @@ function handleBatchLoop(rootDir) {
|
|
|
261
262
|
items[currentItem].iterations = (items[currentItem].iterations || 0) + 1;
|
|
262
263
|
|
|
263
264
|
// Run tests for this file
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
const testSpinner = feedback.spinner(`Running tests for: ${currentItem}`);
|
|
266
|
+
testSpinner.start();
|
|
267
267
|
const testResult = runTestsForFile(rootDir, currentItem);
|
|
268
|
+
if (testResult.passed) {
|
|
269
|
+
testSpinner.succeed(`Tests passed for: ${currentItem}`);
|
|
270
|
+
} else {
|
|
271
|
+
testSpinner.fail(`Tests failed for: ${currentItem}`);
|
|
272
|
+
}
|
|
268
273
|
|
|
269
274
|
if (testResult.passed) {
|
|
270
275
|
console.log(`${c.green}✓ Tests passed${c.reset} (${(testResult.duration / 1000).toFixed(1)}s)`);
|
|
@@ -373,8 +378,10 @@ async function handleInit(args, rootDir) {
|
|
|
373
378
|
const maxIterations = parseIntBounded(maxArg ? maxArg.split('=')[1] : null, 50, 1, 200);
|
|
374
379
|
|
|
375
380
|
// Resolve glob pattern
|
|
376
|
-
|
|
381
|
+
const globSpinner = feedback.spinner(`Resolving pattern: ${pattern}`);
|
|
382
|
+
globSpinner.start();
|
|
377
383
|
const files = await resolveGlob(pattern, rootDir);
|
|
384
|
+
globSpinner.succeed(`Resolved ${files.length} files`);
|
|
378
385
|
|
|
379
386
|
if (files.length === 0) {
|
|
380
387
|
console.log(`${c.yellow}No files found matching: ${pattern}${c.reset}`);
|