@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 +14 -0
- package/assets/agents/builder.md +8 -1
- package/assets/skills/{trust-safety → abuse-prevention}/SKILL.md +3 -3
- package/assets/skills/{security → appsec}/SKILL.md +2 -2
- package/assets/skills/{discovery → competitive-analysis}/SKILL.md +3 -3
- package/assets/skills/{data-architecture → data-modeling}/SKILL.md +3 -3
- package/assets/skills/{operability → deployments}/SKILL.md +3 -3
- package/assets/slash-commands/continue.md +8 -1
- package/assets/slash-commands/review.md +5 -2
- package/package.json +1 -1
- package/src/commands/flow/execute-v2.ts +1 -0
- package/src/commands/flow-command.ts +101 -0
- package/src/core/__tests__/backup-restore.test.ts +247 -0
- package/src/core/cleanup-handler.ts +45 -39
- package/src/core/flow-executor.ts +10 -4
- package/src/index.ts +2 -0
- package/src/utils/config/mcp-config.ts +9 -8
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
|
package/assets/agents/builder.md
CHANGED
|
@@ -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
|
|
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:
|
|
3
|
-
description:
|
|
2
|
+
name: abuse-prevention
|
|
3
|
+
description: Abuse prevention - rate limiting, moderation, bad actors. Use when fighting abuse.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Abuse Prevention Guideline
|
|
7
7
|
|
|
8
8
|
## Tech Stack
|
|
9
9
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description:
|
|
2
|
+
name: competitive-analysis
|
|
3
|
+
description: Competitive analysis - market research, feature gaps. Use when exploring what competitors do.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Competitive Analysis Guideline
|
|
7
7
|
|
|
8
8
|
## Tech Stack
|
|
9
9
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: data-
|
|
3
|
-
description: Data
|
|
2
|
+
name: data-modeling
|
|
3
|
+
description: Data modeling - entities, relationships, schemas. Use when designing data structures.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Data
|
|
6
|
+
# Data Modeling Guideline
|
|
7
7
|
|
|
8
8
|
## Tech Stack
|
|
9
9
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description:
|
|
2
|
+
name: deployments
|
|
3
|
+
description: Deployments - rollback, feature flags, ops tooling. Use when shipping to production.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
@@ -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
|
-
//
|
|
39
|
-
process.on('
|
|
40
|
-
|
|
44
|
+
// SIGINT (Ctrl+C) - perform async cleanup then exit
|
|
45
|
+
process.on('SIGINT', () => {
|
|
46
|
+
this.handleSignal('SIGINT', 0);
|
|
41
47
|
});
|
|
42
48
|
|
|
43
|
-
//
|
|
44
|
-
process.on('
|
|
45
|
-
|
|
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
|
-
//
|
|
50
|
-
process.on('
|
|
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
|
-
|
|
59
|
-
process.exit(1);
|
|
57
|
+
this.handleSignal('uncaughtException', 1);
|
|
60
58
|
});
|
|
61
59
|
|
|
62
|
-
// Unhandled rejections
|
|
63
|
-
process.on('unhandledRejection',
|
|
60
|
+
// Unhandled rejections - perform async cleanup then exit with error
|
|
61
|
+
process.on('unhandledRejection', (reason) => {
|
|
64
62
|
console.error('\nUnhandled Rejection:', reason);
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
*
|
|
79
|
+
* Handle signal with async cleanup
|
|
80
|
+
* Ensures cleanup completes before process exit
|
|
72
81
|
*/
|
|
73
|
-
private
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
const { shouldRestore, session } = await this.sessionManager.endSession(
|
|
80
|
-
this.currentProjectHash
|
|
81
|
-
);
|
|
89
|
+
this.cleanupInProgress = true;
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
*
|
|
95
|
-
*
|
|
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 -
|
|
100
|
-
|
|
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
|
-
{
|
|
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:
|
|
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:
|
|
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]
|
|
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:
|
|
127
|
-
required:
|
|
128
|
-
secret:
|
|
129
|
-
defaultValue:
|
|
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
|
});
|