@sylphx/flow 2.14.1 ā 2.15.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 +42 -0
- package/package.json +1 -1
- package/src/commands/flow/execute-v2.ts +67 -112
- package/src/core/attach-manager.ts +0 -21
- package/src/core/backup-manager.ts +70 -89
- package/src/core/cleanup-handler.ts +8 -28
- package/src/core/flow-executor.ts +21 -89
- package/src/core/git-stash-manager.ts +3 -16
- package/src/services/auto-upgrade.ts +66 -46
- package/src/utils/agent-enhancer.ts +2 -3
- package/src/utils/display/banner.ts +10 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @sylphx/flow
|
|
2
2
|
|
|
3
|
+
## 2.15.1 (2025-12-17)
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
- **cli**: Make CLI output truly minimal and modern
|
|
8
|
+
- Remove all verbose status messages (backup, restore, session recovery)
|
|
9
|
+
- Remove emoji icons (š§, ā) for cleaner output
|
|
10
|
+
- Remove "Attached" and "Running" status lines
|
|
11
|
+
- Silent git worktree operations
|
|
12
|
+
- Silent crash recovery on startup
|
|
13
|
+
- Only show header: `flow {version} ā {target}`
|
|
14
|
+
|
|
15
|
+
### ā»ļø Refactoring
|
|
16
|
+
|
|
17
|
+
- **cli:** make output truly minimal and modern ([3315e41](https://github.com/SylphxAI/flow/commit/3315e41fa34839d7c866af1ebbe3b6f2d9e3ee71))
|
|
18
|
+
|
|
19
|
+
## 2.15.0 (2025-12-17)
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
- **cli**: Redesign CLI output with modern, minimalist interface
|
|
24
|
+
- Single-line header: `flow {version} ā {target}`
|
|
25
|
+
- Consolidated status: `ā Attached {n} agents, {n} commands, {n} MCP`
|
|
26
|
+
- Silent operations by default (backup, cleanup, provider selection)
|
|
27
|
+
- Upgrade notification: `ā Flow {version} (next run)`
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
- **auto-upgrade**: Remove auto-restart after Flow upgrade - new version used on next run instead
|
|
32
|
+
|
|
33
|
+
### ⨠Features
|
|
34
|
+
|
|
35
|
+
- **cli:** redesign output with modern minimalist interface ([654146d](https://github.com/SylphxAI/flow/commit/654146d3ba7a635b39a599125300e60cfdba2bec))
|
|
36
|
+
|
|
37
|
+
### š Bug Fixes
|
|
38
|
+
|
|
39
|
+
- **auto-upgrade:** don't restart after Flow upgrade ([8207417](https://github.com/SylphxAI/flow/commit/8207417921dc60357e97a2ed8db7d6d79f0b5a3e))
|
|
40
|
+
|
|
41
|
+
### š§ Chores
|
|
42
|
+
|
|
43
|
+
- fix package.json formatting ([1e9d940](https://github.com/SylphxAI/flow/commit/1e9d94086e7186fa2a3e80c8f6c98483840d865d))
|
|
44
|
+
|
|
3
45
|
## 2.14.1 (2025-12-17)
|
|
4
46
|
|
|
5
47
|
### Documentation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/flow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.1",
|
|
4
4
|
"description": "One CLI to rule them all. Unified orchestration layer for Claude Code, OpenCode, Cursor and all AI development tools. Auto-detection, auto-installation, auto-upgrade.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Execution Logic for Flow Command (V2 - Attach Mode)
|
|
3
|
-
*
|
|
3
|
+
* Minimal, modern CLI output design
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
6
9
|
import chalk from 'chalk';
|
|
7
10
|
import inquirer from 'inquirer';
|
|
8
11
|
import { FlowExecutor } from '../../core/flow-executor.js';
|
|
@@ -12,13 +15,29 @@ import { GlobalConfigService } from '../../services/global-config.js';
|
|
|
12
15
|
import { TargetInstaller } from '../../services/target-installer.js';
|
|
13
16
|
import type { RunCommandOptions } from '../../types.js';
|
|
14
17
|
import { extractAgentInstructions, loadAgentContent } from '../../utils/agent-enhancer.js';
|
|
15
|
-
import {
|
|
18
|
+
import { showHeader } from '../../utils/display/banner.js';
|
|
16
19
|
import { CLIError } from '../../utils/error-handler.js';
|
|
17
20
|
import { UserCancelledError } from '../../utils/errors.js';
|
|
18
21
|
import { ensureTargetInstalled, promptForTargetSelection } from '../../utils/target-selection.js';
|
|
19
22
|
import { resolvePrompt } from './prompt.js';
|
|
20
23
|
import type { FlowOptions } from './types.js';
|
|
21
24
|
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = path.dirname(__filename);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get Flow version from package.json
|
|
30
|
+
*/
|
|
31
|
+
async function getFlowVersion(): Promise<string> {
|
|
32
|
+
try {
|
|
33
|
+
const packageJsonPath = path.join(__dirname, '..', '..', '..', 'package.json');
|
|
34
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
|
|
35
|
+
return packageJson.version;
|
|
36
|
+
} catch {
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
/**
|
|
23
42
|
* Configure provider environment variables
|
|
24
43
|
*/
|
|
@@ -40,14 +59,14 @@ function configureProviderEnv(provider: 'kimi' | 'zai', apiKey: string): void {
|
|
|
40
59
|
}
|
|
41
60
|
|
|
42
61
|
/**
|
|
43
|
-
* Select and configure provider for Claude Code
|
|
62
|
+
* Select and configure provider for Claude Code (silent unless prompting)
|
|
44
63
|
*/
|
|
45
64
|
async function selectProvider(configService: GlobalConfigService): Promise<void> {
|
|
46
65
|
try {
|
|
47
66
|
const providerConfig = await configService.loadProviderConfig();
|
|
48
67
|
const defaultProvider = providerConfig.claudeCode.defaultProvider;
|
|
49
68
|
|
|
50
|
-
// If not "ask-every-time", use the default provider
|
|
69
|
+
// If not "ask-every-time", use the default provider silently
|
|
51
70
|
if (defaultProvider !== 'ask-every-time') {
|
|
52
71
|
if (defaultProvider === 'kimi' || defaultProvider === 'zai') {
|
|
53
72
|
const provider = providerConfig.claudeCode.providers[defaultProvider];
|
|
@@ -63,7 +82,7 @@ async function selectProvider(configService: GlobalConfigService): Promise<void>
|
|
|
63
82
|
{
|
|
64
83
|
type: 'list',
|
|
65
84
|
name: 'selectedProvider',
|
|
66
|
-
message: 'Select provider
|
|
85
|
+
message: 'Select provider:',
|
|
67
86
|
choices: [
|
|
68
87
|
{ name: 'Default (Claude Code built-in)', value: 'default' },
|
|
69
88
|
{ name: 'Kimi', value: 'kimi' },
|
|
@@ -83,7 +102,6 @@ async function selectProvider(configService: GlobalConfigService): Promise<void>
|
|
|
83
102
|
if (rememberChoice) {
|
|
84
103
|
providerConfig.claudeCode.defaultProvider = selectedProvider;
|
|
85
104
|
await configService.saveProviderConfig(providerConfig);
|
|
86
|
-
console.log(chalk.dim(' (Saved to settings)\n'));
|
|
87
105
|
}
|
|
88
106
|
|
|
89
107
|
// Configure environment variables based on selection
|
|
@@ -91,16 +109,11 @@ async function selectProvider(configService: GlobalConfigService): Promise<void>
|
|
|
91
109
|
const provider = providerConfig.claudeCode.providers[selectedProvider];
|
|
92
110
|
|
|
93
111
|
if (!provider?.apiKey) {
|
|
94
|
-
console.log(chalk.yellow('
|
|
112
|
+
console.log(chalk.yellow(' API key not configured. Use: sylphx-flow settings'));
|
|
95
113
|
return;
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
configureProviderEnv(selectedProvider, provider.apiKey);
|
|
99
|
-
|
|
100
|
-
const providerName = selectedProvider === 'kimi' ? 'Kimi' : 'Z.ai';
|
|
101
|
-
console.log(chalk.green(`ā Using ${providerName} provider\n`));
|
|
102
|
-
} else {
|
|
103
|
-
console.log(chalk.green('ā Using default Claude Code provider\n'));
|
|
104
117
|
}
|
|
105
118
|
} catch (error: unknown) {
|
|
106
119
|
// Handle user cancellation (Ctrl+C)
|
|
@@ -140,7 +153,7 @@ function executeTargetCommand(
|
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
/**
|
|
143
|
-
* Main flow execution with attach mode (V2)
|
|
156
|
+
* Main flow execution with attach mode (V2) - Minimal output design
|
|
144
157
|
*/
|
|
145
158
|
export async function executeFlowV2(
|
|
146
159
|
prompt: string | undefined,
|
|
@@ -148,39 +161,29 @@ export async function executeFlowV2(
|
|
|
148
161
|
): Promise<void> {
|
|
149
162
|
const projectPath = process.cwd();
|
|
150
163
|
|
|
151
|
-
// Show welcome banner
|
|
152
|
-
showWelcome();
|
|
153
|
-
|
|
154
164
|
// Initialize config service early to check for saved preferences
|
|
155
165
|
const configService = new GlobalConfigService();
|
|
156
166
|
await configService.initialize();
|
|
157
167
|
|
|
158
|
-
// Step 1: Determine target
|
|
168
|
+
// Step 1: Determine target (silent auto-detect, only prompt when necessary)
|
|
159
169
|
const targetInstaller = new TargetInstaller(projectPath);
|
|
160
170
|
const installedTargets = await targetInstaller.detectInstalledTargets();
|
|
161
171
|
const settings = await configService.loadSettings();
|
|
162
172
|
|
|
163
173
|
let selectedTargetId: string | null = null;
|
|
164
174
|
|
|
165
|
-
// Distinguish between three cases:
|
|
166
|
-
// 1. User explicitly set "ask-every-time" ā always prompt
|
|
167
|
-
// 2. User has no setting (undefined/null) ā allow auto-detect
|
|
168
|
-
// 3. User has specific target ā use that target
|
|
169
175
|
const isAskEveryTime = settings.defaultTarget === 'ask-every-time';
|
|
170
176
|
const hasNoSetting = !settings.defaultTarget;
|
|
171
177
|
const hasSpecificTarget = settings.defaultTarget && settings.defaultTarget !== 'ask-every-time';
|
|
172
178
|
|
|
173
179
|
if (isAskEveryTime) {
|
|
174
|
-
// User explicitly wants to be asked every time
|
|
175
|
-
console.log(chalk.cyan('š Detecting installed AI CLIs...\n'));
|
|
176
|
-
|
|
180
|
+
// User explicitly wants to be asked every time
|
|
177
181
|
selectedTargetId = await promptForTargetSelection(
|
|
178
182
|
installedTargets,
|
|
179
|
-
'Select AI CLI
|
|
183
|
+
'Select AI CLI:',
|
|
180
184
|
'execution'
|
|
181
185
|
);
|
|
182
186
|
|
|
183
|
-
const installation = targetInstaller.getInstallationInfo(selectedTargetId);
|
|
184
187
|
const installed = await ensureTargetInstalled(
|
|
185
188
|
selectedTargetId,
|
|
186
189
|
targetInstaller,
|
|
@@ -190,28 +193,17 @@ export async function executeFlowV2(
|
|
|
190
193
|
if (!installed) {
|
|
191
194
|
process.exit(1);
|
|
192
195
|
}
|
|
193
|
-
|
|
194
|
-
if (installedTargets.includes(selectedTargetId)) {
|
|
195
|
-
console.log(chalk.green(`ā Using ${installation?.name}\n`));
|
|
196
|
-
}
|
|
197
196
|
} else if (hasNoSetting) {
|
|
198
|
-
// No setting - use auto-detection
|
|
197
|
+
// No setting - use auto-detection
|
|
199
198
|
if (installedTargets.length === 1) {
|
|
200
|
-
// Exactly 1 target found - use it automatically
|
|
201
199
|
selectedTargetId = installedTargets[0];
|
|
202
|
-
const installation = targetInstaller.getInstallationInfo(selectedTargetId);
|
|
203
|
-
console.log(chalk.green(`ā Using ${installation?.name} (auto-detected)\n`));
|
|
204
200
|
} else {
|
|
205
|
-
// 0 or multiple targets - prompt for selection
|
|
206
|
-
console.log(chalk.cyan('š Detecting installed AI CLIs...\n'));
|
|
207
|
-
|
|
208
201
|
selectedTargetId = await promptForTargetSelection(
|
|
209
202
|
installedTargets,
|
|
210
|
-
'Select AI CLI
|
|
203
|
+
'Select AI CLI:',
|
|
211
204
|
'execution'
|
|
212
205
|
);
|
|
213
206
|
|
|
214
|
-
const installation = targetInstaller.getInstallationInfo(selectedTargetId);
|
|
215
207
|
const installed = await ensureTargetInstalled(
|
|
216
208
|
selectedTargetId,
|
|
217
209
|
targetInstaller,
|
|
@@ -221,110 +213,77 @@ export async function executeFlowV2(
|
|
|
221
213
|
if (!installed) {
|
|
222
214
|
process.exit(1);
|
|
223
215
|
}
|
|
224
|
-
|
|
225
|
-
if (installedTargets.includes(selectedTargetId)) {
|
|
226
|
-
console.log(chalk.green(`ā Using ${installation?.name}\n`));
|
|
227
|
-
}
|
|
228
216
|
}
|
|
229
217
|
} else if (hasSpecificTarget) {
|
|
230
|
-
// User has a specific target preference
|
|
218
|
+
// User has a specific target preference
|
|
231
219
|
selectedTargetId = settings.defaultTarget;
|
|
232
|
-
const installation = targetInstaller.getInstallationInfo(selectedTargetId);
|
|
233
220
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
console.log(chalk.
|
|
237
|
-
} else {
|
|
238
|
-
// Preferred target not installed - try to install it
|
|
239
|
-
console.log(chalk.yellow(`ā ļø ${installation?.name} is set as default but not installed\n`));
|
|
221
|
+
if (!installedTargets.includes(selectedTargetId)) {
|
|
222
|
+
const installation = targetInstaller.getInstallationInfo(selectedTargetId);
|
|
223
|
+
console.log(chalk.yellow(`\n ${installation?.name} not installed`));
|
|
240
224
|
const installed = await targetInstaller.install(selectedTargetId, true);
|
|
241
225
|
|
|
242
226
|
if (!installed) {
|
|
243
|
-
|
|
244
|
-
console.log(
|
|
245
|
-
chalk.red(
|
|
246
|
-
`\nā Cannot proceed: ${installation?.name} is not installed and auto-install failed`
|
|
247
|
-
)
|
|
248
|
-
);
|
|
249
|
-
console.log(chalk.yellow(' Please either:'));
|
|
250
|
-
console.log(chalk.cyan(' 1. Install manually (see instructions above)'));
|
|
251
|
-
console.log(chalk.cyan(' 2. Change default target: sylphx-flow settings\n'));
|
|
227
|
+
console.log(chalk.red(` Cannot proceed: installation failed\n`));
|
|
252
228
|
process.exit(1);
|
|
253
229
|
}
|
|
254
|
-
|
|
255
|
-
console.log();
|
|
256
230
|
}
|
|
257
231
|
}
|
|
258
232
|
|
|
259
|
-
//
|
|
233
|
+
// Get version and target name for header
|
|
234
|
+
const version = await getFlowVersion();
|
|
235
|
+
const targetInstallation = targetInstaller.getInstallationInfo(selectedTargetId);
|
|
236
|
+
const targetName = targetInstallation?.name || selectedTargetId;
|
|
237
|
+
|
|
238
|
+
// Show minimal header
|
|
239
|
+
showHeader(version, targetName);
|
|
240
|
+
|
|
241
|
+
// Step 2: Auto-upgrade (silent, returns status)
|
|
260
242
|
const autoUpgrade = new AutoUpgrade(projectPath);
|
|
261
|
-
await autoUpgrade.runAutoUpgrade(selectedTargetId);
|
|
243
|
+
const upgradeResult = await autoUpgrade.runAutoUpgrade(selectedTargetId);
|
|
262
244
|
|
|
263
|
-
//
|
|
264
|
-
if (
|
|
265
|
-
console.log(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
console.log(chalk.dim(
|
|
269
|
-
} else {
|
|
270
|
-
console.log(
|
|
271
|
-
chalk.yellow('š Replace mode (default): All settings will use Flow configuration')
|
|
272
|
-
);
|
|
273
|
-
console.log(chalk.dim(' Use --merge to keep your existing settings\n'));
|
|
245
|
+
// Show upgrade notice (minimal - only if upgraded)
|
|
246
|
+
if (upgradeResult.flowUpgraded && upgradeResult.flowVersion) {
|
|
247
|
+
console.log(chalk.dim(`ā flow ${upgradeResult.flowVersion.latest}`));
|
|
248
|
+
}
|
|
249
|
+
if (upgradeResult.targetUpgraded && upgradeResult.targetVersion) {
|
|
250
|
+
console.log(chalk.dim(`ā ${targetName.toLowerCase()} ${upgradeResult.targetVersion.latest}`));
|
|
274
251
|
}
|
|
275
252
|
|
|
276
253
|
// Create executor
|
|
277
254
|
const executor = new FlowExecutor();
|
|
278
|
-
const _projectManager = executor.getProjectManager();
|
|
279
255
|
|
|
280
|
-
// Step
|
|
256
|
+
// Step 3: Execute attach mode lifecycle
|
|
281
257
|
try {
|
|
282
258
|
// Attach Flow environment (backup ā attach ā register cleanup)
|
|
283
|
-
await executor.execute(projectPath, {
|
|
259
|
+
const attachResult = await executor.execute(projectPath, {
|
|
284
260
|
verbose: options.verbose,
|
|
285
261
|
skipBackup: false,
|
|
286
262
|
skipSecrets: false,
|
|
287
263
|
merge: options.merge || false,
|
|
288
264
|
});
|
|
289
265
|
|
|
290
|
-
// Step 3: Use the target we already selected (don't re-detect)
|
|
291
|
-
// selectedTargetId was determined earlier based on settings/auto-detect/prompt
|
|
292
266
|
const targetId = selectedTargetId;
|
|
293
267
|
|
|
294
|
-
//
|
|
268
|
+
// Provider selection (Claude Code only, silent unless prompting)
|
|
295
269
|
if (targetId === 'claude-code') {
|
|
296
270
|
await selectProvider(configService);
|
|
297
271
|
}
|
|
298
272
|
|
|
299
|
-
//
|
|
300
|
-
const settings = await configService.loadSettings();
|
|
273
|
+
// Determine which agent to use
|
|
301
274
|
const flowConfig = await configService.loadFlowConfig();
|
|
275
|
+
let agent = options.agent || settings.defaultAgent || 'coder';
|
|
302
276
|
|
|
303
|
-
//
|
|
304
|
-
const agent = options.agent || settings.defaultAgent || 'coder';
|
|
305
|
-
|
|
306
|
-
// Check if agent is enabled
|
|
277
|
+
// Check if agent is enabled (silent fallback)
|
|
307
278
|
if (!flowConfig.agents[agent]?.enabled) {
|
|
308
|
-
console.log(chalk.yellow(`ā ļø Agent '${agent}' is not enabled in settings`));
|
|
309
|
-
console.log(chalk.yellow(` Enable it with: sylphx-flow settings`));
|
|
310
|
-
console.log(chalk.yellow(` Using 'coder' agent instead\n`));
|
|
311
|
-
// Fallback to first enabled agent or coder
|
|
312
279
|
const enabledAgents = await configService.getEnabledAgents();
|
|
313
|
-
|
|
314
|
-
options.agent = fallbackAgent;
|
|
280
|
+
agent = enabledAgents.length > 0 ? enabledAgents[0] : 'coder';
|
|
315
281
|
}
|
|
316
282
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// Load enabled rules and output styles from config
|
|
283
|
+
// Load agent content
|
|
320
284
|
const enabledRules = await configService.getEnabledRules();
|
|
321
285
|
const enabledOutputStyles = await configService.getEnabledOutputStyles();
|
|
322
286
|
|
|
323
|
-
console.log(chalk.dim(` Enabled rules: ${enabledRules.join(', ')}`));
|
|
324
|
-
console.log(chalk.dim(` Enabled output styles: ${enabledOutputStyles.join(', ')}\n`));
|
|
325
|
-
|
|
326
|
-
// Load agent content with enabled rules and output styles
|
|
327
|
-
// Rules are filtered: intersection of agent's frontmatter rules and globally enabled rules
|
|
328
287
|
const agentContent = await loadAgentContent(
|
|
329
288
|
agent,
|
|
330
289
|
options.agentFile,
|
|
@@ -334,7 +293,6 @@ export async function executeFlowV2(
|
|
|
334
293
|
const agentInstructions = extractAgentInstructions(agentContent);
|
|
335
294
|
|
|
336
295
|
const systemPrompt = `AGENT INSTRUCTIONS:\n${agentInstructions}`;
|
|
337
|
-
|
|
338
296
|
const userPrompt = prompt?.trim() || '';
|
|
339
297
|
|
|
340
298
|
// Prepare run options
|
|
@@ -352,30 +310,27 @@ export async function executeFlowV2(
|
|
|
352
310
|
// Step 4: Execute command
|
|
353
311
|
await executeTargetCommand(targetId, systemPrompt, userPrompt, runOptions);
|
|
354
312
|
|
|
355
|
-
// Step 5: Cleanup (
|
|
313
|
+
// Step 5: Cleanup (silent)
|
|
356
314
|
await executor.cleanup(projectPath);
|
|
357
|
-
|
|
358
|
-
console.log(chalk.green('ā Session complete\n'));
|
|
359
315
|
} catch (error) {
|
|
360
316
|
// Handle user cancellation gracefully
|
|
361
317
|
if (error instanceof UserCancelledError) {
|
|
362
|
-
console.log(chalk.yellow('\n
|
|
318
|
+
console.log(chalk.yellow('\n Cancelled'));
|
|
363
319
|
try {
|
|
364
320
|
await executor.cleanup(projectPath);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
console.error(chalk.red(' ā Cleanup failed:'), cleanupError);
|
|
321
|
+
} catch {
|
|
322
|
+
// Silent cleanup failure
|
|
368
323
|
}
|
|
369
324
|
process.exit(0);
|
|
370
325
|
}
|
|
371
326
|
|
|
372
|
-
console.error(chalk.red
|
|
327
|
+
console.error(chalk.red('\n Error:'), error);
|
|
373
328
|
|
|
374
329
|
// Ensure cleanup even on error
|
|
375
330
|
try {
|
|
376
331
|
await executor.cleanup(projectPath);
|
|
377
|
-
} catch
|
|
378
|
-
|
|
332
|
+
} catch {
|
|
333
|
+
// Silent cleanup failure
|
|
379
334
|
}
|
|
380
335
|
|
|
381
336
|
throw error;
|
|
@@ -8,7 +8,6 @@ import { createHash } from 'node:crypto';
|
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import fs from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import chalk from 'chalk';
|
|
12
11
|
import { MCP_SERVER_REGISTRY, type MCPServerID } from '../config/servers.js';
|
|
13
12
|
import { GlobalConfigService } from '../services/global-config.js';
|
|
14
13
|
import type { Target } from '../types/target.types.js';
|
|
@@ -184,9 +183,6 @@ export class AttachManager {
|
|
|
184
183
|
await this.attachSingleFiles(projectPath, templates.singleFiles, result, manifest);
|
|
185
184
|
}
|
|
186
185
|
|
|
187
|
-
// Show conflict warnings
|
|
188
|
-
this.showConflictWarnings(result);
|
|
189
|
-
|
|
190
186
|
return result;
|
|
191
187
|
}
|
|
192
188
|
|
|
@@ -421,21 +417,4 @@ export class AttachManager {
|
|
|
421
417
|
result.singleFilesMerged.push(file.path);
|
|
422
418
|
}
|
|
423
419
|
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Show conflict warnings to user
|
|
427
|
-
*/
|
|
428
|
-
private showConflictWarnings(result: AttachResult): void {
|
|
429
|
-
if (result.conflicts.length === 0) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
console.log(chalk.yellow('\nā ļø Conflicts detected:\n'));
|
|
434
|
-
|
|
435
|
-
for (const conflict of result.conflicts) {
|
|
436
|
-
console.log(chalk.yellow(` ⢠${conflict.type}: ${conflict.name} - ${conflict.action}`));
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
console.log(chalk.dim("\n Don't worry! All overridden content will be restored on exit.\n"));
|
|
440
|
-
}
|
|
441
420
|
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import fs from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import ora from 'ora';
|
|
11
10
|
import type { Target } from '../types/target.types.js';
|
|
12
11
|
import type { ProjectManager } from './project-manager.js';
|
|
13
12
|
import { targetManager } from './target-manager.js';
|
|
@@ -96,72 +95,63 @@ export class BackupManager {
|
|
|
96
95
|
// Ensure backup directory exists
|
|
97
96
|
await fs.mkdir(backupPath, { recursive: true });
|
|
98
97
|
|
|
99
|
-
|
|
98
|
+
// Get target config directory
|
|
99
|
+
const targetConfigDir = this.projectManager.getTargetConfigDir(projectPath, target);
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// Use configDir from target config (e.g., '.claude', '.opencode')
|
|
108
|
-
const backupTargetDir = path.join(backupPath, target.config.configDir);
|
|
109
|
-
await this.copyDirectory(targetConfigDir, backupTargetDir);
|
|
110
|
-
}
|
|
101
|
+
// Backup entire target directory if it exists
|
|
102
|
+
if (existsSync(targetConfigDir)) {
|
|
103
|
+
// Use configDir from target config (e.g., '.claude', '.opencode')
|
|
104
|
+
const backupTargetDir = path.join(backupPath, target.config.configDir);
|
|
105
|
+
await this.copyDirectory(targetConfigDir, backupTargetDir);
|
|
106
|
+
}
|
|
111
107
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Create symlink to latest (with fallback for Windows)
|
|
132
|
-
const latestLink = paths.latestBackup;
|
|
133
|
-
if (existsSync(latestLink)) {
|
|
134
|
-
await fs.unlink(latestLink);
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
await fs.symlink(sessionId, latestLink);
|
|
138
|
-
} catch (symlinkError: unknown) {
|
|
139
|
-
// Windows without admin/Developer Mode can't create symlinks
|
|
140
|
-
// Fall back to writing session ID to a file
|
|
141
|
-
if (
|
|
142
|
-
symlinkError instanceof Error &&
|
|
143
|
-
'code' in symlinkError &&
|
|
144
|
-
symlinkError.code === 'EPERM'
|
|
145
|
-
) {
|
|
146
|
-
await fs.writeFile(latestLink, sessionId, 'utf-8');
|
|
147
|
-
} else {
|
|
148
|
-
throw symlinkError;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
108
|
+
// Create manifest (store target ID as string for JSON serialization)
|
|
109
|
+
const manifest: BackupManifest = {
|
|
110
|
+
sessionId,
|
|
111
|
+
timestamp,
|
|
112
|
+
projectPath,
|
|
113
|
+
target: targetId,
|
|
114
|
+
backup: {
|
|
115
|
+
agents: { user: [], flow: [] },
|
|
116
|
+
commands: { user: [], flow: [] },
|
|
117
|
+
singleFiles: {},
|
|
118
|
+
},
|
|
119
|
+
secrets: {
|
|
120
|
+
mcpEnvExtracted: false,
|
|
121
|
+
storedAt: '',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await fs.writeFile(path.join(backupPath, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
151
126
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
timestamp,
|
|
157
|
-
projectPath,
|
|
158
|
-
target: targetId,
|
|
159
|
-
backupPath,
|
|
160
|
-
};
|
|
161
|
-
} catch (error) {
|
|
162
|
-
spinner.fail('Backup failed');
|
|
163
|
-
throw error;
|
|
127
|
+
// Create symlink to latest (with fallback for Windows)
|
|
128
|
+
const latestLink = paths.latestBackup;
|
|
129
|
+
if (existsSync(latestLink)) {
|
|
130
|
+
await fs.unlink(latestLink);
|
|
164
131
|
}
|
|
132
|
+
try {
|
|
133
|
+
await fs.symlink(sessionId, latestLink);
|
|
134
|
+
} catch (symlinkError: unknown) {
|
|
135
|
+
// Windows without admin/Developer Mode can't create symlinks
|
|
136
|
+
// Fall back to writing session ID to a file
|
|
137
|
+
if (
|
|
138
|
+
symlinkError instanceof Error &&
|
|
139
|
+
'code' in symlinkError &&
|
|
140
|
+
symlinkError.code === 'EPERM'
|
|
141
|
+
) {
|
|
142
|
+
await fs.writeFile(latestLink, sessionId, 'utf-8');
|
|
143
|
+
} else {
|
|
144
|
+
throw symlinkError;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
sessionId,
|
|
150
|
+
timestamp,
|
|
151
|
+
projectPath,
|
|
152
|
+
target: targetId,
|
|
153
|
+
backupPath,
|
|
154
|
+
};
|
|
165
155
|
}
|
|
166
156
|
|
|
167
157
|
/**
|
|
@@ -175,38 +165,29 @@ export class BackupManager {
|
|
|
175
165
|
throw new Error(`Backup not found: ${sessionId}`);
|
|
176
166
|
}
|
|
177
167
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// Read manifest
|
|
182
|
-
const manifestPath = path.join(backupPath, 'manifest.json');
|
|
183
|
-
const manifest: BackupManifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
184
|
-
|
|
185
|
-
const projectPath = manifest.projectPath;
|
|
186
|
-
const targetId = manifest.target;
|
|
168
|
+
// Read manifest
|
|
169
|
+
const manifestPath = path.join(backupPath, 'manifest.json');
|
|
170
|
+
const manifest: BackupManifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
187
171
|
|
|
188
|
-
|
|
189
|
-
|
|
172
|
+
const projectPath = manifest.projectPath;
|
|
173
|
+
const targetId = manifest.target;
|
|
190
174
|
|
|
191
|
-
|
|
192
|
-
|
|
175
|
+
// Resolve target to get config
|
|
176
|
+
const target = this.resolveTarget(targetId);
|
|
193
177
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
await fs.rm(targetConfigDir, { recursive: true, force: true });
|
|
197
|
-
}
|
|
178
|
+
// Get target config directory
|
|
179
|
+
const targetConfigDir = this.projectManager.getTargetConfigDir(projectPath, target);
|
|
198
180
|
|
|
199
|
-
|
|
200
|
-
|
|
181
|
+
// Remove current target directory
|
|
182
|
+
if (existsSync(targetConfigDir)) {
|
|
183
|
+
await fs.rm(targetConfigDir, { recursive: true, force: true });
|
|
184
|
+
}
|
|
201
185
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
186
|
+
// Restore from backup using target config's configDir
|
|
187
|
+
const backupTargetDir = path.join(backupPath, target.config.configDir);
|
|
205
188
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
spinner.fail('Restore failed');
|
|
209
|
-
throw error;
|
|
189
|
+
if (existsSync(backupTargetDir)) {
|
|
190
|
+
await this.copyDirectory(backupTargetDir, targetConfigDir);
|
|
210
191
|
}
|
|
211
192
|
}
|
|
212
193
|
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Handles process signals and ensures backup restoration
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import chalk from 'chalk';
|
|
8
7
|
import type { BackupManager } from './backup-manager.js';
|
|
9
8
|
import type { ProjectManager } from './project-manager.js';
|
|
10
9
|
import type { SessionManager } from './session-manager.js';
|
|
@@ -43,30 +42,26 @@ export class CleanupHandler {
|
|
|
43
42
|
|
|
44
43
|
// SIGINT (Ctrl+C)
|
|
45
44
|
process.on('SIGINT', async () => {
|
|
46
|
-
console.log(chalk.yellow('\nā ļø Interrupted by user, cleaning up...'));
|
|
47
45
|
await this.onSignal('SIGINT');
|
|
48
46
|
process.exit(0);
|
|
49
47
|
});
|
|
50
48
|
|
|
51
49
|
// SIGTERM
|
|
52
50
|
process.on('SIGTERM', async () => {
|
|
53
|
-
console.log(chalk.yellow('\nā ļø Terminated, cleaning up...'));
|
|
54
51
|
await this.onSignal('SIGTERM');
|
|
55
52
|
process.exit(0);
|
|
56
53
|
});
|
|
57
54
|
|
|
58
55
|
// Uncaught exceptions
|
|
59
56
|
process.on('uncaughtException', async (error) => {
|
|
60
|
-
console.error(
|
|
61
|
-
console.error(error);
|
|
57
|
+
console.error('\nUncaught Exception:', error);
|
|
62
58
|
await this.onSignal('uncaughtException');
|
|
63
59
|
process.exit(1);
|
|
64
60
|
});
|
|
65
61
|
|
|
66
62
|
// Unhandled rejections
|
|
67
63
|
process.on('unhandledRejection', async (reason) => {
|
|
68
|
-
console.error(
|
|
69
|
-
console.error(reason);
|
|
64
|
+
console.error('\nUnhandled Rejection:', reason);
|
|
70
65
|
await this.onSignal('unhandledRejection');
|
|
71
66
|
process.exit(1);
|
|
72
67
|
});
|
|
@@ -97,6 +92,7 @@ export class CleanupHandler {
|
|
|
97
92
|
|
|
98
93
|
/**
|
|
99
94
|
* Signal-based cleanup (SIGINT, SIGTERM, etc.) with multi-session support
|
|
95
|
+
* Silent operation - no console output
|
|
100
96
|
*/
|
|
101
97
|
private async onSignal(_signal: string): Promise<void> {
|
|
102
98
|
if (!this.currentProjectHash) {
|
|
@@ -104,29 +100,22 @@ export class CleanupHandler {
|
|
|
104
100
|
}
|
|
105
101
|
|
|
106
102
|
try {
|
|
107
|
-
console.log(chalk.cyan('š§¹ Cleaning up...'));
|
|
108
|
-
|
|
109
103
|
const { shouldRestore, session } = await this.sessionManager.endSession(
|
|
110
104
|
this.currentProjectHash
|
|
111
105
|
);
|
|
112
106
|
|
|
113
107
|
if (shouldRestore && session) {
|
|
114
|
-
// Last session - restore environment
|
|
115
|
-
console.log(chalk.cyan(' Restoring environment...'));
|
|
116
108
|
await this.backupManager.restoreBackup(this.currentProjectHash, session.sessionId);
|
|
117
|
-
console.log(chalk.green('ā Environment restored'));
|
|
118
|
-
} else if (!shouldRestore && session) {
|
|
119
|
-
// Other sessions still running
|
|
120
|
-
console.log(chalk.yellow(` ${session.refCount} session(s) still running`));
|
|
121
109
|
}
|
|
122
|
-
} catch (
|
|
123
|
-
|
|
110
|
+
} catch (_error) {
|
|
111
|
+
// Silent fail
|
|
124
112
|
}
|
|
125
113
|
}
|
|
126
114
|
|
|
127
115
|
/**
|
|
128
116
|
* Recover on startup (for all projects)
|
|
129
117
|
* Checks for orphaned sessions from crashes
|
|
118
|
+
* Silent operation - no console output
|
|
130
119
|
*/
|
|
131
120
|
async recoverOnStartup(): Promise<void> {
|
|
132
121
|
const orphanedSessions = await this.sessionManager.detectOrphanedSessions();
|
|
@@ -135,21 +124,12 @@ export class CleanupHandler {
|
|
|
135
124
|
return;
|
|
136
125
|
}
|
|
137
126
|
|
|
138
|
-
console.log(chalk.cyan(`\nš§ Recovering ${orphanedSessions.size} crashed session(s)...\n`));
|
|
139
|
-
|
|
140
127
|
for (const [projectHash, session] of orphanedSessions) {
|
|
141
|
-
console.log(chalk.dim(` Project: ${session.projectPath}`));
|
|
142
|
-
|
|
143
128
|
try {
|
|
144
|
-
// Restore backup
|
|
145
129
|
await this.backupManager.restoreBackup(projectHash, session.sessionId);
|
|
146
|
-
|
|
147
|
-
// Clean up session
|
|
148
130
|
await this.sessionManager.recoverSession(projectHash, session);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
} catch (error) {
|
|
152
|
-
console.error(chalk.red(' ā Recovery failed:'), error);
|
|
131
|
+
} catch (_error) {
|
|
132
|
+
// Silent fail - don't interrupt startup
|
|
153
133
|
}
|
|
154
134
|
}
|
|
155
135
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import type { Target } from '../types/target.types.js';
|
|
9
|
-
import { AttachManager
|
|
9
|
+
import { AttachManager } from './attach-manager.js';
|
|
10
10
|
import { BackupManager } from './backup-manager.js';
|
|
11
11
|
import { CleanupHandler } from './cleanup-handler.js';
|
|
12
12
|
import { GitStashManager } from './git-stash-manager.js';
|
|
@@ -50,8 +50,12 @@ export class FlowExecutor {
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Execute complete flow with attach mode (with multi-session support)
|
|
53
|
+
* Returns summary for caller to display
|
|
53
54
|
*/
|
|
54
|
-
async execute(
|
|
55
|
+
async execute(
|
|
56
|
+
projectPath: string,
|
|
57
|
+
options: FlowExecutorOptions = {}
|
|
58
|
+
): Promise<{ joined: boolean; agents?: number; commands?: number; mcp?: number }> {
|
|
55
59
|
// Initialize Flow directories
|
|
56
60
|
await this.projectManager.initialize();
|
|
57
61
|
|
|
@@ -72,44 +76,30 @@ export class FlowExecutor {
|
|
|
72
76
|
const existingSession = await this.sessionManager.getActiveSession(projectHash);
|
|
73
77
|
|
|
74
78
|
if (existingSession) {
|
|
75
|
-
// Joining existing session
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const { session } = await this.sessionManager.startSession(
|
|
79
|
+
// Joining existing session - silent
|
|
80
|
+
await this.sessionManager.startSession(
|
|
79
81
|
projectPath,
|
|
80
82
|
projectHash,
|
|
81
83
|
target,
|
|
82
84
|
existingSession.backupPath
|
|
83
85
|
);
|
|
84
|
-
|
|
85
|
-
// Register cleanup hooks
|
|
86
86
|
this.cleanupHandler.registerCleanupHooks(projectHash);
|
|
87
|
-
|
|
88
|
-
console.log(chalk.green(` ā Joined session (${session.refCount} active session(s))\n`));
|
|
89
|
-
console.log(chalk.green('ā Flow environment ready!\n'));
|
|
90
|
-
return;
|
|
87
|
+
return { joined: true };
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
// First session - stash
|
|
94
|
-
// Step 3: Stash git changes to hide Flow's modifications from git status
|
|
95
|
-
console.log(chalk.cyan('š Checking git status...'));
|
|
90
|
+
// First session - stash, backup, attach (all silent)
|
|
96
91
|
await this.gitStashManager.stashSettingsChanges(projectPath);
|
|
97
|
-
|
|
98
|
-
console.log(chalk.cyan('š¾ Creating backup...'));
|
|
99
92
|
const backup = await this.backupManager.createBackup(projectPath, projectHash, target);
|
|
100
93
|
|
|
101
|
-
//
|
|
94
|
+
// Extract and save secrets (silent)
|
|
102
95
|
if (!options.skipSecrets) {
|
|
103
|
-
console.log(chalk.cyan('š Extracting secrets...'));
|
|
104
96
|
const secrets = await this.secretsManager.extractMCPSecrets(projectPath, projectHash, target);
|
|
105
|
-
|
|
106
97
|
if (Object.keys(secrets.servers).length > 0) {
|
|
107
98
|
await this.secretsManager.saveSecrets(projectHash, secrets);
|
|
108
|
-
console.log(chalk.green(` ā Saved ${Object.keys(secrets.servers).length} MCP secret(s)`));
|
|
109
99
|
}
|
|
110
100
|
}
|
|
111
101
|
|
|
112
|
-
//
|
|
102
|
+
// Start session
|
|
113
103
|
const { session } = await this.sessionManager.startSession(
|
|
114
104
|
projectPath,
|
|
115
105
|
projectHash,
|
|
@@ -118,21 +108,14 @@ export class FlowExecutor {
|
|
|
118
108
|
backup.sessionId
|
|
119
109
|
);
|
|
120
110
|
|
|
121
|
-
// Step 6: Register cleanup hooks
|
|
122
111
|
this.cleanupHandler.registerCleanupHooks(projectHash);
|
|
123
112
|
|
|
124
|
-
//
|
|
113
|
+
// Clear and attach (silent)
|
|
125
114
|
if (!options.merge) {
|
|
126
|
-
console.log(chalk.cyan('š Clearing existing settings...'));
|
|
127
115
|
await this.clearUserSettings(projectPath, target);
|
|
128
116
|
}
|
|
129
117
|
|
|
130
|
-
// Step 8: Load templates
|
|
131
|
-
console.log(chalk.cyan('š¦ Loading Flow templates...'));
|
|
132
118
|
const templates = await this.templateLoader.loadTemplates(target);
|
|
133
|
-
|
|
134
|
-
// Step 9: Attach Flow environment
|
|
135
|
-
console.log(chalk.cyan('š Attaching Flow environment...'));
|
|
136
119
|
const manifest = await this.backupManager.getManifest(projectHash, session.sessionId);
|
|
137
120
|
|
|
138
121
|
if (!manifest) {
|
|
@@ -147,13 +130,15 @@ export class FlowExecutor {
|
|
|
147
130
|
manifest
|
|
148
131
|
);
|
|
149
132
|
|
|
150
|
-
// Update manifest with attach results
|
|
151
133
|
await this.backupManager.updateManifest(projectHash, session.sessionId, manifest);
|
|
152
134
|
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
135
|
+
// Return summary for caller to display
|
|
136
|
+
return {
|
|
137
|
+
joined: false,
|
|
138
|
+
agents: attachResult.agentsAdded.length,
|
|
139
|
+
commands: attachResult.commandsAdded.length,
|
|
140
|
+
mcp: attachResult.mcpServersAdded.length,
|
|
141
|
+
};
|
|
157
142
|
}
|
|
158
143
|
|
|
159
144
|
/**
|
|
@@ -260,65 +245,12 @@ export class FlowExecutor {
|
|
|
260
245
|
}
|
|
261
246
|
|
|
262
247
|
/**
|
|
263
|
-
* Cleanup after execution
|
|
248
|
+
* Cleanup after execution (silent)
|
|
264
249
|
*/
|
|
265
250
|
async cleanup(projectPath: string): Promise<void> {
|
|
266
251
|
const projectHash = this.projectManager.getProjectHash(projectPath);
|
|
267
|
-
|
|
268
|
-
console.log(chalk.cyan('\nš§¹ Cleaning up...'));
|
|
269
|
-
|
|
270
252
|
await this.cleanupHandler.cleanup(projectHash);
|
|
271
|
-
|
|
272
|
-
// Restore stashed git changes
|
|
273
253
|
await this.gitStashManager.popSettingsChanges(projectPath);
|
|
274
|
-
|
|
275
|
-
console.log(chalk.green(' ā Environment restored'));
|
|
276
|
-
console.log(chalk.green(' ā Secrets preserved for next run\n'));
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Show attach summary
|
|
281
|
-
*/
|
|
282
|
-
private showAttachSummary(result: AttachResult): void {
|
|
283
|
-
const items = [];
|
|
284
|
-
|
|
285
|
-
if (result.agentsAdded.length > 0) {
|
|
286
|
-
items.push(`${result.agentsAdded.length} agent${result.agentsAdded.length > 1 ? 's' : ''}`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (result.commandsAdded.length > 0) {
|
|
290
|
-
items.push(
|
|
291
|
-
`${result.commandsAdded.length} command${result.commandsAdded.length > 1 ? 's' : ''}`
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (result.mcpServersAdded.length > 0) {
|
|
296
|
-
items.push(
|
|
297
|
-
`${result.mcpServersAdded.length} MCP server${result.mcpServersAdded.length > 1 ? 's' : ''}`
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (result.hooksAdded.length > 0) {
|
|
302
|
-
items.push(`${result.hooksAdded.length} hook${result.hooksAdded.length > 1 ? 's' : ''}`);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (result.rulesAppended) {
|
|
306
|
-
items.push('rules');
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (items.length > 0) {
|
|
310
|
-
console.log(chalk.green(` ā Added: ${items.join(', ')}`));
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const overridden =
|
|
314
|
-
result.agentsOverridden.length +
|
|
315
|
-
result.commandsOverridden.length +
|
|
316
|
-
result.mcpServersOverridden.length +
|
|
317
|
-
result.hooksOverridden.length;
|
|
318
|
-
|
|
319
|
-
if (overridden > 0) {
|
|
320
|
-
console.log(chalk.yellow(` ā Overridden: ${overridden} item${overridden > 1 ? 's' : ''}`));
|
|
321
|
-
}
|
|
322
254
|
}
|
|
323
255
|
|
|
324
256
|
/**
|
|
@@ -8,7 +8,6 @@ import { exec } from 'node:child_process';
|
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { promisify } from 'node:util';
|
|
11
|
-
import chalk from 'chalk';
|
|
12
11
|
|
|
13
12
|
const execAsync = promisify(exec);
|
|
14
13
|
|
|
@@ -93,14 +92,8 @@ export class GitStashManager {
|
|
|
93
92
|
// File might not exist or not tracked, skip it
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
|
-
|
|
97
|
-
if (this.skipWorktreeFiles.length > 0) {
|
|
98
|
-
console.log(
|
|
99
|
-
chalk.dim(` ā Hiding ${this.skipWorktreeFiles.length} settings file(s) from git\n`)
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
95
|
} catch (_error) {
|
|
103
|
-
|
|
96
|
+
// Silent fail
|
|
104
97
|
}
|
|
105
98
|
}
|
|
106
99
|
|
|
@@ -123,15 +116,9 @@ export class GitStashManager {
|
|
|
123
116
|
}
|
|
124
117
|
}
|
|
125
118
|
|
|
126
|
-
console.log(
|
|
127
|
-
chalk.dim(` ā Restored git tracking for ${this.skipWorktreeFiles.length} file(s)\n`)
|
|
128
|
-
);
|
|
129
119
|
this.skipWorktreeFiles = [];
|
|
130
|
-
} catch {
|
|
131
|
-
|
|
132
|
-
console.log(
|
|
133
|
-
chalk.yellow(' Run manually: git update-index --no-skip-worktree .claude/* .opencode/*\n')
|
|
134
|
-
);
|
|
120
|
+
} catch (_error) {
|
|
121
|
+
// Silent fail
|
|
135
122
|
}
|
|
136
123
|
}
|
|
137
124
|
|
|
@@ -163,7 +163,7 @@ export class AutoUpgrade {
|
|
|
163
163
|
|
|
164
164
|
await execAsync(upgradeCmd);
|
|
165
165
|
|
|
166
|
-
spinner.succeed(chalk.green('ā Flow upgraded
|
|
166
|
+
spinner.succeed(chalk.green('ā Flow upgraded (new version used on next run)'));
|
|
167
167
|
return true;
|
|
168
168
|
} catch (error) {
|
|
169
169
|
spinner.fail(chalk.red('ā Flow upgrade failed'));
|
|
@@ -232,64 +232,84 @@ export class AutoUpgrade {
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
|
-
* Run auto-upgrade check and upgrade if needed
|
|
236
|
-
* Shows upgrade status and performs upgrades automatically
|
|
235
|
+
* Run auto-upgrade check and upgrade if needed (silent)
|
|
237
236
|
* @param targetId - Optional target CLI ID to check and upgrade
|
|
237
|
+
* @returns Upgrade result with status info
|
|
238
238
|
*/
|
|
239
|
-
async runAutoUpgrade(targetId?: string): Promise<
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
async runAutoUpgrade(targetId?: string): Promise<{
|
|
240
|
+
flowUpgraded: boolean;
|
|
241
|
+
flowVersion?: { current: string; latest: string };
|
|
242
|
+
targetUpgraded: boolean;
|
|
243
|
+
targetVersion?: { current: string; latest: string };
|
|
244
|
+
}> {
|
|
242
245
|
const status = await this.checkForUpgrades(targetId);
|
|
246
|
+
const result = {
|
|
247
|
+
flowUpgraded: false,
|
|
248
|
+
flowVersion: status.flowVersion ?? undefined,
|
|
249
|
+
targetUpgraded: false,
|
|
250
|
+
targetVersion: status.targetVersion ?? undefined,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Perform upgrades silently
|
|
254
|
+
if (status.flowNeedsUpgrade) {
|
|
255
|
+
result.flowUpgraded = await this.upgradeFlowSilent();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (status.targetNeedsUpgrade && targetId) {
|
|
259
|
+
result.targetUpgraded = await this.upgradeTargetSilent(targetId);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
243
264
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Upgrade Flow silently (no spinner/output)
|
|
267
|
+
*/
|
|
268
|
+
private async upgradeFlowSilent(): Promise<boolean> {
|
|
269
|
+
try {
|
|
270
|
+
const flowPm = await this.detectFlowPackageManager();
|
|
271
|
+
const upgradeCmd = getUpgradeCommand('@sylphx/flow', flowPm);
|
|
272
|
+
await execAsync(upgradeCmd);
|
|
273
|
+
return true;
|
|
274
|
+
} catch {
|
|
275
|
+
return false;
|
|
253
276
|
}
|
|
277
|
+
}
|
|
254
278
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
} else if (!this.options.skipTarget && targetId) {
|
|
263
|
-
const installation = this.targetInstaller.getInstallationInfo(targetId);
|
|
264
|
-
console.log(chalk.green(`ā ${installation?.name} is up to date`));
|
|
279
|
+
/**
|
|
280
|
+
* Upgrade target CLI silently (no spinner/output)
|
|
281
|
+
*/
|
|
282
|
+
private async upgradeTargetSilent(targetId: string): Promise<boolean> {
|
|
283
|
+
const installation = this.targetInstaller.getInstallationInfo(targetId);
|
|
284
|
+
if (!installation) {
|
|
285
|
+
return false;
|
|
265
286
|
}
|
|
266
287
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// Re-exec the process to use the new version
|
|
275
|
-
console.log(chalk.cyan('\nš Restarting with updated version...\n'));
|
|
276
|
-
const { spawn } = await import('node:child_process');
|
|
277
|
-
const child = spawn(process.argv[0], process.argv.slice(1), {
|
|
278
|
-
stdio: 'inherit',
|
|
279
|
-
env: { ...process.env, SYLPHX_FLOW_UPGRADED: '1' },
|
|
280
|
-
});
|
|
281
|
-
child.on('exit', (code) => process.exit(code ?? 0));
|
|
282
|
-
return; // Don't continue with old code
|
|
288
|
+
try {
|
|
289
|
+
if (targetId === 'claude-code') {
|
|
290
|
+
try {
|
|
291
|
+
await execAsync('claude update');
|
|
292
|
+
return true;
|
|
293
|
+
} catch {
|
|
294
|
+
// Fall back to npm
|
|
283
295
|
}
|
|
284
296
|
}
|
|
285
297
|
|
|
286
|
-
if (
|
|
287
|
-
|
|
298
|
+
if (targetId === 'opencode') {
|
|
299
|
+
try {
|
|
300
|
+
await execAsync('opencode upgrade');
|
|
301
|
+
return true;
|
|
302
|
+
} catch {
|
|
303
|
+
// Fall back to npm
|
|
304
|
+
}
|
|
288
305
|
}
|
|
289
306
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
307
|
+
const packageManager = detectPackageManager(this.projectPath);
|
|
308
|
+
const upgradeCmd = getUpgradeCommand(installation.package, packageManager);
|
|
309
|
+
await execAsync(upgradeCmd);
|
|
310
|
+
return true;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
293
313
|
}
|
|
294
314
|
}
|
|
295
315
|
}
|
|
@@ -59,8 +59,7 @@ async function loadRules(ruleNames?: string[]): Promise<string> {
|
|
|
59
59
|
const stripped = await yamlUtils.stripFrontMatter(content);
|
|
60
60
|
sections.push(stripped);
|
|
61
61
|
} catch (_error) {
|
|
62
|
-
//
|
|
63
|
-
console.warn(`Warning: Rule file not found: ${ruleName}.md`);
|
|
62
|
+
// Silent - rule file not found, continue with other rules
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
|
|
@@ -89,7 +88,7 @@ async function loadOutputStyles(styleNames?: string[]): Promise<string> {
|
|
|
89
88
|
const stripped = await yamlUtils.stripFrontMatter(content);
|
|
90
89
|
sections.push(stripped);
|
|
91
90
|
} catch (_error) {
|
|
92
|
-
|
|
91
|
+
// Silent - output style file not found, continue
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
94
|
} else {
|
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Banner Display Utilities
|
|
3
|
-
*
|
|
3
|
+
* Minimal, modern CLI output
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import boxen from 'boxen';
|
|
7
6
|
import chalk from 'chalk';
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
9
|
+
* Show minimal header: flow {version} ā {target}
|
|
10
|
+
*/
|
|
11
|
+
export function showHeader(version: string, target: string): void {
|
|
12
|
+
console.log(`\n${chalk.cyan('flow')} ${chalk.dim(version)} ${chalk.dim('ā')} ${target}\n`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated Use showHeader instead
|
|
11
17
|
*/
|
|
12
18
|
export function showWelcome(): void {
|
|
13
|
-
|
|
14
|
-
boxen(
|
|
15
|
-
`${chalk.cyan.bold('Sylphx Flow')} ${chalk.dim('- AI-Powered Development Framework')}\n` +
|
|
16
|
-
`${chalk.dim('Auto-initialization ⢠Smart upgrades ⢠One-click launch')}`,
|
|
17
|
-
{
|
|
18
|
-
padding: 1,
|
|
19
|
-
margin: { bottom: 1 },
|
|
20
|
-
borderStyle: 'round',
|
|
21
|
-
borderColor: 'cyan',
|
|
22
|
-
}
|
|
23
|
-
)
|
|
24
|
-
);
|
|
19
|
+
// No-op for backward compatibility during migration
|
|
25
20
|
}
|