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.
@@ -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 };
@@ -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 ((value.startsWith('"') && value.endsWith('"')) ||
94
- (value.startsWith("'") && value.endsWith("'"))) {
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 frames = ['⠋', '⠙', '', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
66
- let frameIndex = 0;
67
- let elapsed = 0;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.99.0",
3
+ "version": "2.99.1",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -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
- console.log(`${c.blue}Running tests for:${c.reset} ${currentItem}`);
265
- console.log(`${c.dim}${'─'.repeat(50)}${c.reset}`);
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
- console.log(`${c.dim}Resolving pattern: ${pattern}${c.reset}`);
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}`);