@sylphx/flow 2.28.0 → 2.28.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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @sylphx/flow
2
2
 
3
+ ## 2.28.1 (2025-12-19)
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - **core:** fix async cleanup handlers and add quickstart command ([1a37c9a](https://github.com/SylphxAI/flow/commit/1a37c9a5f38682d887fcdd5c5a177d2ff7eb51a3))
8
+
9
+ ### ♻️ Refactoring
10
+
11
+ - **skills:** rename skills for clarity and update references ([416dfe7](https://github.com/SylphxAI/flow/commit/416dfe79d9622a8c78f89cd775561d461f5b5955))
12
+
13
+ ### ✅ Tests
14
+
15
+ - **core:** add integration tests for backup→restore lifecycle ([e023a8c](https://github.com/SylphxAI/flow/commit/e023a8cc3b41fdd8b0c7f21561644cbf7484917b))
16
+
3
17
  ## 2.28.0 (2025-12-18)
4
18
 
5
19
  ### ✨ Features
@@ -43,7 +43,14 @@ Build toward that.
43
43
 
44
44
  **Ultrathink.** Many minds beat one. Delegate workers to explore from different angles. They critique, you synthesize. Never self-assess.
45
45
 
46
- **Skills.** Before acting on any domain — invoke the skill. Read the guidelines. Then exceed them.
46
+ **Skills.** Before acting on any domain — invoke the skill:
47
+ ```
48
+ abuse-prevention, account-security, admin, appsec, auth, billing,
49
+ code-quality, competitive-analysis, data-modeling, database, delivery,
50
+ deployments, growth, i18n, ledger, observability, performance, pricing,
51
+ privacy, pwa, referral, seo, storage, support, uiux
52
+ ```
53
+ Skills contain: tech stack decisions, non-negotiables, driving questions. Then exceed them.
47
54
 
48
55
  **Act.** No permission needed. No workarounds. Ship it.
49
56
 
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: trust-safety
3
- description: Trust and safety - abuse prevention, rate limiting. Use when fighting bad actors.
2
+ name: abuse-prevention
3
+ description: Abuse prevention - rate limiting, moderation, bad actors. Use when fighting abuse.
4
4
  ---
5
5
 
6
- # Trust Safety Guideline
6
+ # Abuse Prevention Guideline
7
7
 
8
8
  ## Tech Stack
9
9
 
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: security
2
+ name: appsec
3
3
  description: Application security - OWASP, validation, secrets. Use when securing the app.
4
4
  ---
5
5
 
6
- # Security Guideline
6
+ # AppSec Guideline
7
7
 
8
8
  ## Tech Stack
9
9
 
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: discovery
3
- description: Feature discovery - competitive analysis. Use when exploring features.
2
+ name: competitive-analysis
3
+ description: Competitive analysis - market research, feature gaps. Use when exploring what competitors do.
4
4
  ---
5
5
 
6
- # Discovery Guideline
6
+ # Competitive Analysis Guideline
7
7
 
8
8
  ## Tech Stack
9
9
 
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: data-architecture
3
- description: Data architecture - models, relationships. Use when designing data structures.
2
+ name: data-modeling
3
+ description: Data modeling - entities, relationships, schemas. Use when designing data structures.
4
4
  ---
5
5
 
6
- # Data Architecture Guideline
6
+ # Data Modeling Guideline
7
7
 
8
8
  ## Tech Stack
9
9
 
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: operability
3
- description: Operations - deployment, rollback, feature flags. Use for ops tooling.
2
+ name: deployments
3
+ description: Deployments - rollback, feature flags, ops tooling. Use when shipping to production.
4
4
  ---
5
5
 
6
- # Operability Guideline
6
+ # Deployments Guideline
7
7
 
8
8
  ## Tech Stack
9
9
 
@@ -16,6 +16,13 @@ Push for world-class:
16
16
 
17
17
  **Never self-assess.** Delegate to workers — they critique, you synthesize. Final Gate.
18
18
 
19
- **Invoke skills** before acting. Then exceed them.
19
+ **Invoke skills** before acting on any domain:
20
+ ```
21
+ abuse-prevention, account-security, admin, appsec, auth, billing,
22
+ code-quality, competitive-analysis, data-modeling, database, delivery,
23
+ deployments, growth, i18n, ledger, observability, performance, pricing,
24
+ privacy, pwa, referral, seo, storage, support, uiux
25
+ ```
26
+ Skills contain: tech stack decisions, non-negotiables, driving questions. Then exceed them.
20
27
 
21
28
  `/continue`
@@ -19,9 +19,12 @@ args:
19
19
 
20
20
  1. **Invoke skills** — Load guidelines for relevant domains:
21
21
  ```
22
- auth, account-security, billing, security, database, performance, observability...
22
+ abuse-prevention, account-security, admin, appsec, auth, billing,
23
+ code-quality, competitive-analysis, data-modeling, database, delivery,
24
+ deployments, growth, i18n, ledger, observability, performance, pricing,
25
+ privacy, pwa, referral, seo, storage, support, uiux
23
26
  ```
24
- Skills contain: tech stack decisions, non-negotiables, patterns, anti-patterns.
27
+ Skills contain: tech stack decisions, non-negotiables, driving questions.
25
28
 
26
29
  2. **Understand** — How is this implemented? Architecture, choices, tradeoffs.
27
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/flow",
3
- "version": "2.28.0",
3
+ "version": "2.28.1",
4
4
  "description": "One CLI to rule them all. Unified orchestration layer for AI coding assistants. Auto-detection, auto-installation, auto-upgrade.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -260,6 +260,7 @@ export async function executeFlowV2(
260
260
  verbose: options.verbose,
261
261
  skipBackup: false,
262
262
  skipSecrets: false,
263
+ skipProjectDocs: true, // Use /init command for project docs
263
264
  merge: options.merge || false,
264
265
  });
265
266
 
@@ -147,6 +147,107 @@ export const doctorCommand = new Command('doctor')
147
147
  }
148
148
  });
149
149
 
150
+ /**
151
+ * Quickstart command - interactive onboarding tutorial
152
+ */
153
+ export const quickstartCommand = new Command('quickstart')
154
+ .description('Interactive 2-minute tutorial to get started with Flow')
155
+ .action(async () => {
156
+ const { default: inquirer } = await import('inquirer');
157
+
158
+ console.log(chalk.cyan.bold('\n🚀 Welcome to Sylphx Flow!\n'));
159
+ console.log(chalk.dim('This 2-minute tutorial will get you up and running.\n'));
160
+
161
+ // Step 1: Check for AI CLI
162
+ console.log(chalk.bold('Step 1/4: Checking your environment\n'));
163
+
164
+ const detector = new StateDetector();
165
+ const state = await detector.detect();
166
+
167
+ if (state.claudeCodeInstalled) {
168
+ console.log(chalk.green(' ✓ Claude Code installed\n'));
169
+ } else {
170
+ console.log(chalk.yellow(' ⚠ Claude Code not found\n'));
171
+ const { installNow } = await inquirer.prompt([
172
+ {
173
+ type: 'confirm',
174
+ name: 'installNow',
175
+ message: 'Install Claude Code now?',
176
+ default: true,
177
+ },
178
+ ]);
179
+
180
+ if (installNow) {
181
+ console.log(chalk.dim('\n Installing Claude Code...\n'));
182
+ const { exec } = await import('node:child_process');
183
+ const { promisify } = await import('node:util');
184
+ const execAsync = promisify(exec);
185
+ try {
186
+ await execAsync('npm install -g @anthropic-ai/claude-code');
187
+ console.log(chalk.green(' ✓ Claude Code installed\n'));
188
+ } catch {
189
+ console.log(chalk.red(' ✗ Installation failed. Run manually:'));
190
+ console.log(chalk.dim(' npm install -g @anthropic-ai/claude-code\n'));
191
+ }
192
+ }
193
+ }
194
+
195
+ // Step 2: Explain core concept
196
+ console.log(chalk.bold('Step 2/4: How Flow works\n'));
197
+ console.log(chalk.dim(' Flow orchestrates AI coding assistants with:'));
198
+ console.log(chalk.dim(' • Specialized agents (coder, writer, reviewer)'));
199
+ console.log(chalk.dim(' • Smart context (less prompting, more building)'));
200
+ console.log(chalk.dim(' • Clean git (your config stays untouched)\n'));
201
+
202
+ await inquirer.prompt([
203
+ {
204
+ type: 'input',
205
+ name: 'continue',
206
+ message: chalk.dim('Press Enter to continue...'),
207
+ },
208
+ ]);
209
+
210
+ // Step 3: Show example usage
211
+ console.log(chalk.bold('\nStep 3/4: Example commands\n'));
212
+ console.log(chalk.cyan(' Basic usage:'));
213
+ console.log(chalk.white(' sylphx-flow "fix the login bug"'));
214
+ console.log(chalk.white(' sylphx-flow "add dark mode"'));
215
+ console.log(chalk.white(' sylphx-flow "write tests for auth"'));
216
+ console.log('');
217
+ console.log(chalk.cyan(' With agents:'));
218
+ console.log(chalk.white(' sylphx-flow --agent coder "implement feature"'));
219
+ console.log(chalk.white(' sylphx-flow --agent writer "document API"'));
220
+ console.log(chalk.white(' sylphx-flow --agent reviewer "review security"'));
221
+ console.log('');
222
+
223
+ await inquirer.prompt([
224
+ {
225
+ type: 'input',
226
+ name: 'continue',
227
+ message: chalk.dim('Press Enter to continue...'),
228
+ },
229
+ ]);
230
+
231
+ // Step 4: Try it
232
+ console.log(chalk.bold('\nStep 4/4: Try it yourself!\n'));
233
+ const { tryNow } = await inquirer.prompt([
234
+ {
235
+ type: 'confirm',
236
+ name: 'tryNow',
237
+ message: 'Run Flow now with a sample task?',
238
+ default: true,
239
+ },
240
+ ]);
241
+
242
+ if (tryNow) {
243
+ console.log(chalk.dim('\nLaunching Flow...\n'));
244
+ await executeFlow('describe this codebase briefly', { agent: 'builder' } as FlowOptions);
245
+ } else {
246
+ console.log(chalk.green("\n✨ You're ready to go!\n"));
247
+ console.log(chalk.dim(' Run: sylphx-flow "your first task"\n'));
248
+ }
249
+ });
250
+
150
251
  /**
151
252
  * Upgrade command - upgrade components
152
253
  */
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Integration tests for backup → attach → restore lifecycle
3
+ * Tests the complete flow of:
4
+ * 1. Creating backup of existing config
5
+ * 2. Attaching Flow templates
6
+ * 3. Restoring original config on cleanup
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
13
+ import { AttachManager } from '../attach-manager.js';
14
+ import { BackupManager } from '../backup-manager.js';
15
+ import { ProjectManager } from '../project-manager.js';
16
+ import { targetManager } from '../target-manager.js';
17
+
18
+ describe('Backup → Attach → Restore Lifecycle', () => {
19
+ let tempDir: string;
20
+ let projectManager: ProjectManager;
21
+ let backupManager: BackupManager;
22
+ let attachManager: AttachManager;
23
+ let projectPath: string;
24
+ let projectHash: string;
25
+
26
+ // Get Claude Code target for tests
27
+ const getClaudeTarget = () => {
28
+ const targetOption = targetManager.getTarget('claude-code');
29
+ if (targetOption._tag === 'None') {
30
+ throw new Error('Claude Code target not found');
31
+ }
32
+ return targetOption.value;
33
+ };
34
+
35
+ beforeEach(async () => {
36
+ // Create temp directories
37
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flow-test-'));
38
+ projectPath = path.join(tempDir, 'test-project');
39
+ fs.mkdirSync(projectPath, { recursive: true });
40
+
41
+ // Initialize managers
42
+ projectManager = new ProjectManager();
43
+
44
+ // Override Flow data directory to use temp
45
+ const flowDataDir = path.join(tempDir, '.sylphx-flow');
46
+ fs.mkdirSync(flowDataDir, { recursive: true });
47
+ (projectManager as any).flowDataDir = flowDataDir;
48
+
49
+ backupManager = new BackupManager(projectManager);
50
+ attachManager = new AttachManager(projectManager);
51
+
52
+ // Get project hash
53
+ projectHash = projectManager.getProjectHash(projectPath);
54
+ });
55
+
56
+ afterEach(async () => {
57
+ // Cleanup temp directory
58
+ if (tempDir && fs.existsSync(tempDir)) {
59
+ fs.rmSync(tempDir, { recursive: true, force: true });
60
+ }
61
+ });
62
+
63
+ describe('Empty project (no existing config)', () => {
64
+ it('should create backup with empty manifest', async () => {
65
+ const target = getClaudeTarget();
66
+ const backup = await backupManager.createBackup(projectPath, projectHash, target);
67
+
68
+ expect(backup.sessionId).toMatch(/^session-\d+$/);
69
+ expect(backup.projectPath).toBe(projectPath);
70
+ expect(backup.target).toBe('claude-code');
71
+ expect(fs.existsSync(backup.backupPath)).toBe(true);
72
+
73
+ // Check manifest exists
74
+ const manifest = await backupManager.getManifest(projectHash, backup.sessionId);
75
+ expect(manifest).not.toBeNull();
76
+ expect(manifest?.backup.agents.user).toEqual([]);
77
+ expect(manifest?.backup.agents.flow).toEqual([]);
78
+ });
79
+
80
+ it('should restore to empty state after attach', async () => {
81
+ const target = getClaudeTarget();
82
+ const backup = await backupManager.createBackup(projectPath, projectHash, target);
83
+
84
+ // Simulate attach by creating .claude directory
85
+ const claudeDir = path.join(projectPath, '.claude');
86
+ fs.mkdirSync(claudeDir, { recursive: true });
87
+ fs.writeFileSync(path.join(claudeDir, 'settings.json'), '{"mcpServers":{}}');
88
+
89
+ // Verify .claude exists after "attach"
90
+ expect(fs.existsSync(claudeDir)).toBe(true);
91
+
92
+ // Restore
93
+ await backupManager.restoreBackup(projectHash, backup.sessionId);
94
+
95
+ // .claude should be removed (back to empty state)
96
+ expect(fs.existsSync(claudeDir)).toBe(false);
97
+ });
98
+ });
99
+
100
+ describe('Project with existing config', () => {
101
+ const existingConfig = {
102
+ mcpServers: {
103
+ 'my-server': {
104
+ command: 'my-cmd',
105
+ args: ['--flag'],
106
+ },
107
+ },
108
+ };
109
+
110
+ beforeEach(() => {
111
+ // Create existing .claude config
112
+ const claudeDir = path.join(projectPath, '.claude');
113
+ fs.mkdirSync(claudeDir, { recursive: true });
114
+ fs.writeFileSync(path.join(claudeDir, 'settings.json'), JSON.stringify(existingConfig));
115
+
116
+ // Create existing agent file
117
+ const agentsDir = path.join(claudeDir, 'agents');
118
+ fs.mkdirSync(agentsDir, { recursive: true });
119
+ fs.writeFileSync(path.join(agentsDir, 'my-agent.md'), '# My Custom Agent\n\nMy custom prompt.');
120
+ });
121
+
122
+ it('should backup existing config', async () => {
123
+ const target = getClaudeTarget();
124
+ const backup = await backupManager.createBackup(projectPath, projectHash, target);
125
+
126
+ // Check backup directory has .claude
127
+ const backupClaudeDir = path.join(backup.backupPath, '.claude');
128
+ expect(fs.existsSync(backupClaudeDir)).toBe(true);
129
+
130
+ // Check settings.json was backed up
131
+ const backupSettings = path.join(backupClaudeDir, 'settings.json');
132
+ expect(fs.existsSync(backupSettings)).toBe(true);
133
+
134
+ const settingsContent = JSON.parse(fs.readFileSync(backupSettings, 'utf-8'));
135
+ expect(settingsContent).toEqual(existingConfig);
136
+
137
+ // Check agent was backed up
138
+ const backupAgentFile = path.join(backupClaudeDir, 'agents', 'my-agent.md');
139
+ expect(fs.existsSync(backupAgentFile)).toBe(true);
140
+ });
141
+
142
+ it('should restore existing config after modifications', async () => {
143
+ const target = getClaudeTarget();
144
+ const backup = await backupManager.createBackup(projectPath, projectHash, target);
145
+
146
+ // Simulate modifications (what attach would do)
147
+ const claudeDir = path.join(projectPath, '.claude');
148
+
149
+ // Modify settings.json
150
+ const newConfig = {
151
+ mcpServers: {
152
+ 'flow-server': { command: 'flow-cmd' },
153
+ },
154
+ };
155
+ fs.writeFileSync(path.join(claudeDir, 'settings.json'), JSON.stringify(newConfig));
156
+
157
+ // Add new agent file
158
+ fs.writeFileSync(path.join(claudeDir, 'agents', 'builder.md'), '# Builder Agent');
159
+
160
+ // Remove original agent
161
+ fs.unlinkSync(path.join(claudeDir, 'agents', 'my-agent.md'));
162
+
163
+ // Verify modifications
164
+ expect(fs.existsSync(path.join(claudeDir, 'agents', 'my-agent.md'))).toBe(false);
165
+ expect(fs.existsSync(path.join(claudeDir, 'agents', 'builder.md'))).toBe(true);
166
+
167
+ // Restore
168
+ await backupManager.restoreBackup(projectHash, backup.sessionId);
169
+
170
+ // Verify restoration
171
+ const restoredSettings = JSON.parse(fs.readFileSync(path.join(claudeDir, 'settings.json'), 'utf-8'));
172
+ expect(restoredSettings).toEqual(existingConfig);
173
+
174
+ // Original agent should be back
175
+ expect(fs.existsSync(path.join(claudeDir, 'agents', 'my-agent.md'))).toBe(true);
176
+
177
+ // Flow agent should be gone
178
+ expect(fs.existsSync(path.join(claudeDir, 'agents', 'builder.md'))).toBe(false);
179
+ });
180
+ });
181
+
182
+ describe('Backup cleanup', () => {
183
+ it('should keep only specified number of backups', async () => {
184
+ const target = getClaudeTarget();
185
+
186
+ // Create 5 backups
187
+ const backups: string[] = [];
188
+ for (let i = 0; i < 5; i++) {
189
+ const backup = await backupManager.createBackup(projectPath, projectHash, target);
190
+ backups.push(backup.sessionId);
191
+ // Small delay to ensure unique timestamps
192
+ await new Promise((resolve) => setTimeout(resolve, 10));
193
+ }
194
+
195
+ // Should have 5 backups
196
+ const listBefore = await backupManager.listBackups(projectHash);
197
+ expect(listBefore.length).toBe(5);
198
+
199
+ // Cleanup to keep last 2
200
+ await backupManager.cleanupOldBackups(projectHash, 2);
201
+
202
+ // Should have 2 backups
203
+ const listAfter = await backupManager.listBackups(projectHash);
204
+ expect(listAfter.length).toBe(2);
205
+
206
+ // The 2 most recent should remain
207
+ const remainingIds = listAfter.map((b) => b.sessionId);
208
+ expect(remainingIds).toContain(backups[4]);
209
+ expect(remainingIds).toContain(backups[3]);
210
+ });
211
+ });
212
+
213
+ describe('Manifest tracking', () => {
214
+ it('should update manifest with attach results', async () => {
215
+ const target = getClaudeTarget();
216
+ const backup = await backupManager.createBackup(projectPath, projectHash, target);
217
+
218
+ // Get original manifest
219
+ const manifest = await backupManager.getManifest(projectHash, backup.sessionId);
220
+ expect(manifest).not.toBeNull();
221
+
222
+ // Update manifest (simulating attach results)
223
+ manifest!.backup.agents.flow = ['builder.md', 'coder.md'];
224
+ manifest!.backup.commands.flow = ['init.md', 'review.md'];
225
+ manifest!.secrets.mcpEnvExtracted = true;
226
+
227
+ await backupManager.updateManifest(projectHash, backup.sessionId, manifest!);
228
+
229
+ // Read updated manifest
230
+ const updatedManifest = await backupManager.getManifest(projectHash, backup.sessionId);
231
+ expect(updatedManifest?.backup.agents.flow).toEqual(['builder.md', 'coder.md']);
232
+ expect(updatedManifest?.backup.commands.flow).toEqual(['init.md', 'review.md']);
233
+ expect(updatedManifest?.secrets.mcpEnvExtracted).toBe(true);
234
+ });
235
+ });
236
+
237
+ describe('Error handling', () => {
238
+ it('should throw on restore of non-existent backup', async () => {
239
+ await expect(backupManager.restoreBackup(projectHash, 'session-nonexistent')).rejects.toThrow('Backup not found');
240
+ });
241
+
242
+ it('should return null manifest for non-existent session', async () => {
243
+ const manifest = await backupManager.getManifest(projectHash, 'session-nonexistent');
244
+ expect(manifest).toBeNull();
245
+ });
246
+ });
247
+ });
@@ -2,6 +2,10 @@
2
2
  * Cleanup Handler
3
3
  * Manages graceful cleanup on exit and crash recovery
4
4
  * Handles process signals and ensures backup restoration
5
+ *
6
+ * IMPORTANT: Node.js 'exit' event handlers MUST be synchronous.
7
+ * We use SIGINT/SIGTERM handlers to perform async cleanup before exiting.
8
+ * The 'exit' handler is only a last-resort sync cleanup.
5
9
  */
6
10
 
7
11
  import type { BackupManager } from './backup-manager.js';
@@ -13,6 +17,8 @@ export class CleanupHandler {
13
17
  private backupManager: BackupManager;
14
18
  private registered = false;
15
19
  private currentProjectHash: string | null = null;
20
+ private cleanupInProgress = false;
21
+ private cleanupCompleted = false;
16
22
 
17
23
  constructor(
18
24
  projectManager: ProjectManager,
@@ -35,64 +41,63 @@ export class CleanupHandler {
35
41
  this.currentProjectHash = projectHash;
36
42
  this.registered = true;
37
43
 
38
- // Normal exit
39
- process.on('exit', async () => {
40
- await this.onExit();
44
+ // SIGINT (Ctrl+C) - perform async cleanup then exit
45
+ process.on('SIGINT', () => {
46
+ this.handleSignal('SIGINT', 0);
41
47
  });
42
48
 
43
- // SIGINT (Ctrl+C)
44
- process.on('SIGINT', async () => {
45
- await this.onSignal('SIGINT');
46
- process.exit(0);
49
+ // SIGTERM - perform async cleanup then exit
50
+ process.on('SIGTERM', () => {
51
+ this.handleSignal('SIGTERM', 0);
47
52
  });
48
53
 
49
- // SIGTERM
50
- process.on('SIGTERM', async () => {
51
- await this.onSignal('SIGTERM');
52
- process.exit(0);
53
- });
54
-
55
- // Uncaught exceptions
56
- process.on('uncaughtException', async (error) => {
54
+ // Uncaught exceptions - perform async cleanup then exit with error
55
+ process.on('uncaughtException', (error) => {
57
56
  console.error('\nUncaught Exception:', error);
58
- await this.onSignal('uncaughtException');
59
- process.exit(1);
57
+ this.handleSignal('uncaughtException', 1);
60
58
  });
61
59
 
62
- // Unhandled rejections
63
- process.on('unhandledRejection', async (reason) => {
60
+ // Unhandled rejections - perform async cleanup then exit with error
61
+ process.on('unhandledRejection', (reason) => {
64
62
  console.error('\nUnhandled Rejection:', reason);
65
- await this.onSignal('unhandledRejection');
66
- process.exit(1);
63
+ this.handleSignal('unhandledRejection', 1);
64
+ });
65
+
66
+ // 'exit' handler - SYNC ONLY, last resort
67
+ // This catches cases where process.exit() was called without going through signals
68
+ process.on('exit', () => {
69
+ // If cleanup wasn't done via signal handlers, log warning
70
+ // (We can't do async cleanup here - just flag it)
71
+ if (!this.cleanupCompleted && this.currentProjectHash) {
72
+ // Recovery will happen on next startup via recoverOnStartup()
73
+ // This is the intended safety net for abnormal exits
74
+ }
67
75
  });
68
76
  }
69
77
 
70
78
  /**
71
- * Normal exit cleanup (with multi-session support)
79
+ * Handle signal with async cleanup
80
+ * Ensures cleanup completes before process exit
72
81
  */
73
- private async onExit(): Promise<void> {
74
- if (!this.currentProjectHash) {
82
+ private handleSignal(signal: string, exitCode: number): void {
83
+ // Prevent double cleanup
84
+ if (this.cleanupInProgress || this.cleanupCompleted) {
85
+ process.exit(exitCode);
75
86
  return;
76
87
  }
77
88
 
78
- try {
79
- const { shouldRestore, session } = await this.sessionManager.endSession(
80
- this.currentProjectHash
81
- );
89
+ this.cleanupInProgress = true;
82
90
 
83
- if (shouldRestore && session) {
84
- // Last session - restore backup silently on normal exit
85
- await this.backupManager.restoreBackup(this.currentProjectHash, session.sessionId);
86
- await this.backupManager.cleanupOldBackups(this.currentProjectHash, 3);
87
- }
88
- } catch (_error) {
89
- // Silent fail on exit
90
- }
91
+ // Perform async cleanup, then exit
92
+ this.onSignal(signal).finally(() => {
93
+ this.cleanupCompleted = true;
94
+ process.exit(exitCode);
95
+ });
91
96
  }
92
97
 
93
98
  /**
94
- * Signal-based cleanup (SIGINT, SIGTERM, etc.) with multi-session support
95
- * Silent operation - no console output
99
+ * Async cleanup for signals and manual cleanup
100
+ * Handles session end and backup restoration
96
101
  */
97
102
  private async onSignal(_signal: string): Promise<void> {
98
103
  if (!this.currentProjectHash) {
@@ -106,9 +111,10 @@ export class CleanupHandler {
106
111
 
107
112
  if (shouldRestore && session) {
108
113
  await this.backupManager.restoreBackup(this.currentProjectHash, session.sessionId);
114
+ await this.backupManager.cleanupOldBackups(this.currentProjectHash, 3);
109
115
  }
110
116
  } catch (_error) {
111
- // Silent fail
117
+ // Silent fail - recovery will happen on next startup
112
118
  }
113
119
  }
114
120
 
@@ -4,10 +4,10 @@
4
4
  * Handles backup → attach → run → restore lifecycle
5
5
  */
6
6
 
7
- import chalk from 'chalk';
8
7
  import { existsSync } from 'node:fs';
9
8
  import fs from 'node:fs/promises';
10
9
  import path from 'node:path';
10
+ import chalk from 'chalk';
11
11
  import type { Target } from '../types/target.types.js';
12
12
  import { AttachManager } from './attach-manager.js';
13
13
  import { BackupManager } from './backup-manager.js';
@@ -23,6 +23,7 @@ export interface FlowExecutorOptions {
23
23
  verbose?: boolean;
24
24
  skipBackup?: boolean;
25
25
  skipSecrets?: boolean;
26
+ skipProjectDocs?: boolean; // Skip auto-creating PRODUCT.md/ARCHITECTURE.md
26
27
  merge?: boolean; // Merge mode: keep user files (default: replace all)
27
28
  }
28
29
 
@@ -96,8 +97,10 @@ export class FlowExecutor {
96
97
  return { joined: true };
97
98
  }
98
99
 
99
- // First session - ensure project docs, stash, backup, attach (all silent)
100
- await this.ensureProjectDocs(projectPath);
100
+ // First session - optionally create project docs, stash, backup, attach (all silent)
101
+ if (!options.skipProjectDocs) {
102
+ await this.ensureProjectDocs(projectPath);
103
+ }
101
104
  await this.gitStashManager.stashSettingsChanges(projectPath);
102
105
  const backup = await this.backupManager.createBackup(projectPath, projectHash, target);
103
106
 
@@ -271,7 +274,10 @@ export class FlowExecutor {
271
274
  const templatesDir = this.templateLoader.getAssetsDir();
272
275
  const templates = [
273
276
  { name: 'PRODUCT.md', template: path.join(templatesDir, 'templates', 'PRODUCT.md') },
274
- { name: 'ARCHITECTURE.md', template: path.join(templatesDir, 'templates', 'ARCHITECTURE.md') },
277
+ {
278
+ name: 'ARCHITECTURE.md',
279
+ template: path.join(templatesDir, 'templates', 'ARCHITECTURE.md'),
280
+ },
275
281
  ];
276
282
 
277
283
  for (const { name, template } of templates) {
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import { executeFlow } from './commands/flow/execute-v2.js';
12
12
  import {
13
13
  doctorCommand,
14
14
  flowCommand,
15
+ quickstartCommand,
15
16
  setupCommand,
16
17
  statusCommand,
17
18
  upgradeCommand,
@@ -71,6 +72,7 @@ export function createCLI(): Command {
71
72
 
72
73
  // Add subcommands - these can still be used explicitly
73
74
  program.addCommand(flowCommand);
75
+ program.addCommand(quickstartCommand);
74
76
  program.addCommand(setupCommand);
75
77
  program.addCommand(statusCommand);
76
78
  program.addCommand(doctorCommand);
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
 
3
3
  import inquirer from 'inquirer';
4
- import type { MCPServerID } from '../../config/servers.js';
4
+ import type { EnvVarConfig, MCPServerDefinition, MCPServerID } from '../../config/servers.js';
5
5
  import { getAllServerIDs, MCP_SERVER_REGISTRY } from '../../config/servers.js';
6
6
  import { targetManager } from '../../core/target-manager.js';
7
7
  import { getNestedProperty, setNestedProperty } from './target-config.js';
@@ -93,7 +93,7 @@ export class MCPConfigurator {
93
93
  return serverId as MCPServerID;
94
94
  }
95
95
 
96
- private async configureServer(server: any): Promise<Record<string, string>> {
96
+ private async configureServer(server: MCPServerDefinition): Promise<Record<string, string>> {
97
97
  const fields = this.buildConfigFields(server);
98
98
  const values: Record<string, string> = {};
99
99
 
@@ -108,11 +108,12 @@ export class MCPConfigurator {
108
108
  return values;
109
109
  }
110
110
 
111
- private buildConfigFields(server: any): ConfigField[] {
111
+ private buildConfigFields(server: MCPServerDefinition): ConfigField[] {
112
112
  const fields: ConfigField[] = [];
113
113
 
114
114
  if (server.envVars) {
115
- Object.entries(server.envVars).forEach(([key, config]: [string, any]) => {
115
+ Object.entries(server.envVars).forEach(([key, config]) => {
116
+ const envConfig = config as EnvVarConfig;
116
117
  let options: string[] | undefined;
117
118
 
118
119
  if (key === 'EMBEDDING_MODEL') {
@@ -123,10 +124,10 @@ export class MCPConfigurator {
123
124
 
124
125
  fields.push({
125
126
  name: key,
126
- description: config.description,
127
- required: config.required,
128
- secret: config.secret || false,
129
- defaultValue: config.default,
127
+ description: envConfig.description,
128
+ required: envConfig.required,
129
+ secret: envConfig.secret || false,
130
+ defaultValue: envConfig.default,
130
131
  currentValue: this.existingValues[key],
131
132
  options,
132
133
  });