@sylphx/flow 1.8.1 → 2.0.0
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 +79 -0
- package/UPGRADE.md +140 -0
- package/assets/output-styles/silent.md +4 -0
- package/package.json +2 -1
- package/src/commands/flow/execute-v2.ts +278 -0
- package/src/commands/flow/execute.ts +1 -18
- package/src/commands/flow/types.ts +3 -2
- package/src/commands/flow-command.ts +32 -69
- package/src/commands/flow-orchestrator.ts +18 -55
- package/src/commands/run-command.ts +12 -6
- package/src/commands/settings-command.ts +529 -0
- package/src/core/attach-manager.ts +482 -0
- package/src/core/backup-manager.ts +308 -0
- package/src/core/cleanup-handler.ts +166 -0
- package/src/core/flow-executor.ts +323 -0
- package/src/core/git-stash-manager.ts +133 -0
- package/src/core/project-manager.ts +274 -0
- package/src/core/secrets-manager.ts +229 -0
- package/src/core/session-manager.ts +268 -0
- package/src/core/template-loader.ts +189 -0
- package/src/core/upgrade-manager.ts +79 -47
- package/src/index.ts +13 -27
- package/src/services/first-run-setup.ts +220 -0
- package/src/services/global-config.ts +337 -0
- package/src/utils/__tests__/package-manager-detector.test.ts +163 -0
- package/src/utils/agent-enhancer.ts +40 -22
- package/src/utils/errors.ts +9 -0
- package/src/utils/package-manager-detector.ts +139 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager (Updated for ~/.sylphx-flow)
|
|
3
|
+
* Manages temporary Flow sessions with multi-project support
|
|
4
|
+
* All sessions stored in ~/.sylphx-flow/sessions/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { ProjectManager } from './project-manager.js';
|
|
11
|
+
|
|
12
|
+
export interface Session {
|
|
13
|
+
projectHash: string;
|
|
14
|
+
projectPath: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
pid: number;
|
|
17
|
+
startTime: string;
|
|
18
|
+
backupPath: string;
|
|
19
|
+
status: 'active' | 'completed' | 'crashed';
|
|
20
|
+
target: 'claude-code' | 'opencode';
|
|
21
|
+
cleanupRequired: boolean;
|
|
22
|
+
// Multi-session support
|
|
23
|
+
isOriginal: boolean; // First session that created backup
|
|
24
|
+
sharedBackupId: string; // Shared backup ID for all sessions
|
|
25
|
+
refCount: number; // Number of active sessions
|
|
26
|
+
activePids: number[]; // All active PIDs sharing this session
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class SessionManager {
|
|
30
|
+
private projectManager: ProjectManager;
|
|
31
|
+
|
|
32
|
+
constructor(projectManager: ProjectManager) {
|
|
33
|
+
this.projectManager = projectManager;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Start a new session for a project (with multi-session support)
|
|
38
|
+
*/
|
|
39
|
+
async startSession(
|
|
40
|
+
projectPath: string,
|
|
41
|
+
projectHash: string,
|
|
42
|
+
target: 'claude-code' | 'opencode',
|
|
43
|
+
backupPath: string,
|
|
44
|
+
sessionId?: string
|
|
45
|
+
): Promise<{ session: Session; isFirstSession: boolean }> {
|
|
46
|
+
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
47
|
+
|
|
48
|
+
// Ensure sessions directory exists
|
|
49
|
+
await fs.mkdir(path.dirname(paths.sessionFile), { recursive: true });
|
|
50
|
+
|
|
51
|
+
// Check for existing session
|
|
52
|
+
const existingSession = await this.getActiveSession(projectHash);
|
|
53
|
+
|
|
54
|
+
if (existingSession) {
|
|
55
|
+
// Join existing session (don't create new backup)
|
|
56
|
+
existingSession.refCount++;
|
|
57
|
+
existingSession.activePids.push(process.pid);
|
|
58
|
+
|
|
59
|
+
await fs.writeFile(paths.sessionFile, JSON.stringify(existingSession, null, 2));
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
session: existingSession,
|
|
63
|
+
isFirstSession: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// First session - create new (use provided sessionId or generate one)
|
|
68
|
+
const newSessionId = sessionId || `session-${Date.now()}`;
|
|
69
|
+
const session: Session = {
|
|
70
|
+
projectHash,
|
|
71
|
+
projectPath,
|
|
72
|
+
sessionId: newSessionId,
|
|
73
|
+
pid: process.pid,
|
|
74
|
+
startTime: new Date().toISOString(),
|
|
75
|
+
backupPath,
|
|
76
|
+
status: 'active',
|
|
77
|
+
target,
|
|
78
|
+
cleanupRequired: true,
|
|
79
|
+
isOriginal: true,
|
|
80
|
+
sharedBackupId: newSessionId,
|
|
81
|
+
refCount: 1,
|
|
82
|
+
activePids: [process.pid],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
await fs.writeFile(paths.sessionFile, JSON.stringify(session, null, 2));
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
session,
|
|
89
|
+
isFirstSession: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Mark session as completed (with reference counting)
|
|
95
|
+
*/
|
|
96
|
+
async endSession(projectHash: string): Promise<{ shouldRestore: boolean; session: Session | null }> {
|
|
97
|
+
try {
|
|
98
|
+
const session = await this.getActiveSession(projectHash);
|
|
99
|
+
|
|
100
|
+
if (!session) {
|
|
101
|
+
return { shouldRestore: false, session: null };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
105
|
+
|
|
106
|
+
// Remove current PID from active PIDs
|
|
107
|
+
session.activePids = session.activePids.filter(pid => pid !== process.pid);
|
|
108
|
+
session.refCount = session.activePids.length;
|
|
109
|
+
|
|
110
|
+
if (session.refCount === 0) {
|
|
111
|
+
// Last session - mark completed and cleanup
|
|
112
|
+
session.status = 'completed';
|
|
113
|
+
session.cleanupRequired = false;
|
|
114
|
+
|
|
115
|
+
const flowHome = this.projectManager.getFlowHomeDir();
|
|
116
|
+
|
|
117
|
+
// Archive to history
|
|
118
|
+
const historyPath = path.join(
|
|
119
|
+
flowHome,
|
|
120
|
+
'sessions',
|
|
121
|
+
'history',
|
|
122
|
+
`${session.sessionId}.json`
|
|
123
|
+
);
|
|
124
|
+
await fs.mkdir(path.dirname(historyPath), { recursive: true });
|
|
125
|
+
await fs.writeFile(historyPath, JSON.stringify(session, null, 2));
|
|
126
|
+
|
|
127
|
+
// Remove active session file
|
|
128
|
+
await fs.unlink(paths.sessionFile);
|
|
129
|
+
|
|
130
|
+
return { shouldRestore: true, session };
|
|
131
|
+
} else {
|
|
132
|
+
// Still have active sessions, update session file
|
|
133
|
+
await fs.writeFile(paths.sessionFile, JSON.stringify(session, null, 2));
|
|
134
|
+
|
|
135
|
+
return { shouldRestore: false, session };
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// Session file might not exist
|
|
139
|
+
return { shouldRestore: false, session: null };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get active session for a project
|
|
145
|
+
*/
|
|
146
|
+
async getActiveSession(projectHash: string): Promise<Session | null> {
|
|
147
|
+
try {
|
|
148
|
+
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
149
|
+
const data = await fs.readFile(paths.sessionFile, 'utf-8');
|
|
150
|
+
return JSON.parse(data);
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Detect orphaned sessions (from crashes) across all projects
|
|
158
|
+
* Handles multi-session by checking all PIDs
|
|
159
|
+
*/
|
|
160
|
+
async detectOrphanedSessions(): Promise<Map<string, Session>> {
|
|
161
|
+
const orphaned = new Map<string, Session>();
|
|
162
|
+
|
|
163
|
+
// Get all active projects
|
|
164
|
+
const projects = await this.projectManager.getActiveProjects();
|
|
165
|
+
|
|
166
|
+
for (const { hash } of projects) {
|
|
167
|
+
const session = await this.getActiveSession(hash);
|
|
168
|
+
|
|
169
|
+
if (!session) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check all active PIDs
|
|
174
|
+
const stillRunning = [];
|
|
175
|
+
for (const pid of session.activePids) {
|
|
176
|
+
if (await this.checkPIDRunning(pid)) {
|
|
177
|
+
stillRunning.push(pid);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Update active PIDs and refCount
|
|
182
|
+
session.activePids = stillRunning;
|
|
183
|
+
session.refCount = stillRunning.length;
|
|
184
|
+
|
|
185
|
+
if (session.refCount === 0 && session.cleanupRequired) {
|
|
186
|
+
// All sessions crashed
|
|
187
|
+
orphaned.set(hash, session);
|
|
188
|
+
} else if (session.refCount !== session.activePids.length) {
|
|
189
|
+
// Some PIDs crashed, update session file
|
|
190
|
+
const paths = this.projectManager.getProjectPaths(hash);
|
|
191
|
+
await fs.writeFile(paths.sessionFile, JSON.stringify(session, null, 2));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return orphaned;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if process is still running
|
|
200
|
+
*/
|
|
201
|
+
private async checkPIDRunning(pid: number): Promise<boolean> {
|
|
202
|
+
try {
|
|
203
|
+
// Send signal 0 to check if process exists
|
|
204
|
+
process.kill(pid, 0);
|
|
205
|
+
return true;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Recover from crashed session
|
|
213
|
+
*/
|
|
214
|
+
async recoverSession(projectHash: string, session: Session): Promise<void> {
|
|
215
|
+
session.status = 'crashed';
|
|
216
|
+
session.cleanupRequired = false;
|
|
217
|
+
|
|
218
|
+
const flowHome = this.projectManager.getFlowHomeDir();
|
|
219
|
+
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
220
|
+
|
|
221
|
+
// Archive to history
|
|
222
|
+
const historyPath = path.join(flowHome, 'sessions', 'history', `${session.sessionId}.json`);
|
|
223
|
+
await fs.mkdir(path.dirname(historyPath), { recursive: true });
|
|
224
|
+
await fs.writeFile(historyPath, JSON.stringify(session, null, 2));
|
|
225
|
+
|
|
226
|
+
// Remove active session
|
|
227
|
+
try {
|
|
228
|
+
await fs.unlink(paths.sessionFile);
|
|
229
|
+
} catch {
|
|
230
|
+
// File might not exist
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Clean up old session history
|
|
236
|
+
*/
|
|
237
|
+
async cleanupOldSessions(keepLast: number = 10): Promise<void> {
|
|
238
|
+
const flowHome = this.projectManager.getFlowHomeDir();
|
|
239
|
+
const historyDir = path.join(flowHome, 'sessions', 'history');
|
|
240
|
+
|
|
241
|
+
if (!existsSync(historyDir)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const files = await fs.readdir(historyDir);
|
|
246
|
+
const sessions = await Promise.all(
|
|
247
|
+
files.map(async (file) => {
|
|
248
|
+
const filePath = path.join(historyDir, file);
|
|
249
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
250
|
+
const session = JSON.parse(data) as Session;
|
|
251
|
+
return { file, session };
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Sort by start time (newest first)
|
|
256
|
+
sessions.sort(
|
|
257
|
+
(a, b) =>
|
|
258
|
+
new Date(b.session.startTime).getTime() -
|
|
259
|
+
new Date(a.session.startTime).getTime()
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Remove old sessions
|
|
263
|
+
const toRemove = sessions.slice(keepLast);
|
|
264
|
+
for (const { file } of toRemove) {
|
|
265
|
+
await fs.unlink(path.join(historyDir, file));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Loader
|
|
3
|
+
* Loads Flow templates from assets directory
|
|
4
|
+
* Supports both claude-code and opencode targets
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import type { FlowTemplates } from './attach-manager.js';
|
|
12
|
+
|
|
13
|
+
export class TemplateLoader {
|
|
14
|
+
private assetsDir: string;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
// Get assets directory relative to this file
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
this.assetsDir = path.join(__dirname, '..', '..', 'assets');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load all templates for target
|
|
25
|
+
* Uses flat assets directory structure (no target-specific subdirectories)
|
|
26
|
+
*/
|
|
27
|
+
async loadTemplates(target: 'claude-code' | 'opencode'): Promise<FlowTemplates> {
|
|
28
|
+
const templates: FlowTemplates = {
|
|
29
|
+
agents: [],
|
|
30
|
+
commands: [],
|
|
31
|
+
rules: undefined,
|
|
32
|
+
mcpServers: [],
|
|
33
|
+
hooks: [],
|
|
34
|
+
singleFiles: [],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Load agents
|
|
38
|
+
const agentsDir = path.join(this.assetsDir, 'agents');
|
|
39
|
+
if (existsSync(agentsDir)) {
|
|
40
|
+
templates.agents = await this.loadAgents(agentsDir);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Load commands (slash-commands directory)
|
|
44
|
+
const commandsDir = path.join(this.assetsDir, 'slash-commands');
|
|
45
|
+
if (existsSync(commandsDir)) {
|
|
46
|
+
templates.commands = await this.loadCommands(commandsDir);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Load rules (check multiple possible locations)
|
|
50
|
+
const rulesLocations = [
|
|
51
|
+
path.join(this.assetsDir, 'rules', 'AGENTS.md'),
|
|
52
|
+
path.join(this.assetsDir, 'AGENTS.md'),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
for (const rulesPath of rulesLocations) {
|
|
56
|
+
if (existsSync(rulesPath)) {
|
|
57
|
+
templates.rules = await fs.readFile(rulesPath, 'utf-8');
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Load MCP servers (if any)
|
|
63
|
+
const mcpConfigPath = path.join(this.assetsDir, 'mcp-servers.json');
|
|
64
|
+
if (existsSync(mcpConfigPath)) {
|
|
65
|
+
templates.mcpServers = await this.loadMCPServers(mcpConfigPath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Load output styles (single files)
|
|
69
|
+
const outputStylesDir = path.join(this.assetsDir, 'output-styles');
|
|
70
|
+
if (existsSync(outputStylesDir)) {
|
|
71
|
+
templates.singleFiles = await this.loadSingleFiles(outputStylesDir);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return templates;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load agents from directory
|
|
79
|
+
*/
|
|
80
|
+
private async loadAgents(
|
|
81
|
+
agentsDir: string
|
|
82
|
+
): Promise<Array<{ name: string; content: string }>> {
|
|
83
|
+
const agents = [];
|
|
84
|
+
const files = await fs.readdir(agentsDir);
|
|
85
|
+
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
if (!file.endsWith('.md')) continue;
|
|
88
|
+
|
|
89
|
+
const content = await fs.readFile(path.join(agentsDir, file), 'utf-8');
|
|
90
|
+
agents.push({ name: file, content });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return agents;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Load commands/modes from directory
|
|
98
|
+
*/
|
|
99
|
+
private async loadCommands(
|
|
100
|
+
commandsDir: string
|
|
101
|
+
): Promise<Array<{ name: string; content: string }>> {
|
|
102
|
+
const commands = [];
|
|
103
|
+
const files = await fs.readdir(commandsDir);
|
|
104
|
+
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
if (!file.endsWith('.md')) continue;
|
|
107
|
+
|
|
108
|
+
const content = await fs.readFile(path.join(commandsDir, file), 'utf-8');
|
|
109
|
+
commands.push({ name: file, content });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return commands;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Load MCP servers configuration
|
|
117
|
+
*/
|
|
118
|
+
private async loadMCPServers(
|
|
119
|
+
configPath: string
|
|
120
|
+
): Promise<Array<{ name: string; config: any }>> {
|
|
121
|
+
const data = await fs.readFile(configPath, 'utf-8');
|
|
122
|
+
const config = JSON.parse(data);
|
|
123
|
+
|
|
124
|
+
const servers = [];
|
|
125
|
+
for (const [name, serverConfig] of Object.entries(config)) {
|
|
126
|
+
servers.push({ name, config: serverConfig });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return servers;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Load hooks from directory
|
|
134
|
+
*/
|
|
135
|
+
private async loadHooks(
|
|
136
|
+
hooksDir: string
|
|
137
|
+
): Promise<Array<{ name: string; content: string }>> {
|
|
138
|
+
const hooks = [];
|
|
139
|
+
const files = await fs.readdir(hooksDir);
|
|
140
|
+
|
|
141
|
+
for (const file of files) {
|
|
142
|
+
if (!file.endsWith('.js')) continue;
|
|
143
|
+
|
|
144
|
+
const content = await fs.readFile(path.join(hooksDir, file), 'utf-8');
|
|
145
|
+
hooks.push({ name: file, content });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return hooks;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Load single files (CLAUDE.md, .cursorrules, etc.)
|
|
153
|
+
*/
|
|
154
|
+
private async loadSingleFiles(
|
|
155
|
+
singleFilesDir: string
|
|
156
|
+
): Promise<Array<{ path: string; content: string }>> {
|
|
157
|
+
const files = [];
|
|
158
|
+
const entries = await fs.readdir(singleFilesDir);
|
|
159
|
+
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const filePath = path.join(singleFilesDir, entry);
|
|
162
|
+
const stat = await fs.stat(filePath);
|
|
163
|
+
|
|
164
|
+
if (stat.isFile()) {
|
|
165
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
166
|
+
files.push({ path: entry, content });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return files;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get assets directory path
|
|
175
|
+
*/
|
|
176
|
+
getAssetsDir(): string {
|
|
177
|
+
return this.assetsDir;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if templates exist (uses flat directory structure)
|
|
182
|
+
*/
|
|
183
|
+
async hasTemplates(target: 'claude-code' | 'opencode'): Promise<boolean> {
|
|
184
|
+
// Check if any template directories exist
|
|
185
|
+
const agentsDir = path.join(this.assetsDir, 'agents');
|
|
186
|
+
const commandsDir = path.join(this.assetsDir, 'slash-commands');
|
|
187
|
+
return existsSync(agentsDir) || existsSync(commandsDir);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -8,6 +8,7 @@ import type { ProjectState } from './state-detector.js';
|
|
|
8
8
|
import { CLIError } from '../utils/error-handler.js';
|
|
9
9
|
import { ConfigService } from '../services/config-service.js';
|
|
10
10
|
import { getProjectSettingsFile } from '../config/constants.js';
|
|
11
|
+
import { detectPackageManager, getUpgradeCommand, type PackageManager } from '../utils/package-manager-detector.js';
|
|
11
12
|
|
|
12
13
|
const execAsync = promisify(exec);
|
|
13
14
|
|
|
@@ -87,86 +88,115 @@ export class UpgradeManager {
|
|
|
87
88
|
};
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
async upgradeFlow(state: ProjectState): Promise<boolean> {
|
|
91
|
+
async upgradeFlow(state: ProjectState, autoInstall: boolean = false): Promise<boolean> {
|
|
91
92
|
if (!state.outdated || !state.latestVersion) {
|
|
92
93
|
return false;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
const
|
|
96
|
+
const packageManager = detectPackageManager(this.projectPath);
|
|
97
|
+
const spinner = ora('Upgrading Sylphx Flow...').start();
|
|
96
98
|
|
|
97
99
|
try {
|
|
98
|
-
//
|
|
100
|
+
// Backup current config
|
|
99
101
|
if (!this.options.skipBackup) {
|
|
100
102
|
await this.backupConfig();
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
if (this.options.dryRun) {
|
|
104
|
-
|
|
106
|
+
const cmd = getUpgradeCommand('@sylphx/flow', packageManager);
|
|
107
|
+
spinner.succeed(`Dry run: ${state.version} → ${state.latestVersion}`);
|
|
108
|
+
console.log(chalk.dim(` Would run: ${cmd}`));
|
|
105
109
|
return true;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
// Auto-install using detected package manager
|
|
113
|
+
if (autoInstall) {
|
|
114
|
+
const installCmd = getUpgradeCommand('@sylphx/flow', packageManager);
|
|
115
|
+
spinner.text = `Installing latest version via ${packageManager}...`;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await execAsync(installCmd);
|
|
119
|
+
spinner.succeed(`Upgraded to ${state.latestVersion} using ${packageManager}`);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
spinner.warn(`Auto-install failed, please run: ${installCmd}`);
|
|
122
|
+
if (this.options.verbose) {
|
|
123
|
+
console.error(error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
// Show manual upgrade command
|
|
128
|
+
const installCmd = getUpgradeCommand('@sylphx/flow', packageManager);
|
|
129
|
+
spinner.info(`To upgrade, run: ${chalk.cyan(installCmd)}`);
|
|
130
|
+
|
|
131
|
+
// Update config metadata
|
|
132
|
+
const configPath = path.join(this.projectPath, getProjectSettingsFile());
|
|
133
|
+
try {
|
|
134
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
135
|
+
config.version = state.latestVersion;
|
|
136
|
+
config.lastUpdated = new Date().toISOString();
|
|
137
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
138
|
+
} catch {
|
|
139
|
+
// Cannot update config
|
|
140
|
+
}
|
|
121
141
|
}
|
|
122
142
|
|
|
123
|
-
spinner.succeed(`已升级到 ${state.latestVersion}`);
|
|
124
143
|
return true;
|
|
125
144
|
} catch (error) {
|
|
126
|
-
spinner.fail('
|
|
145
|
+
spinner.fail('Upgrade failed');
|
|
127
146
|
throw new CLIError(
|
|
128
|
-
|
|
147
|
+
`Failed to upgrade Sylphx Flow: ${error instanceof Error ? error.message : String(error)}`,
|
|
129
148
|
'UPGRADE_FAILED'
|
|
130
149
|
);
|
|
131
150
|
}
|
|
132
151
|
}
|
|
133
152
|
|
|
134
|
-
async upgradeTarget(state: ProjectState): Promise<boolean> {
|
|
153
|
+
async upgradeTarget(state: ProjectState, autoInstall: boolean = false): Promise<boolean> {
|
|
135
154
|
if (!state.target || !state.targetLatestVersion) {
|
|
136
155
|
return false;
|
|
137
156
|
}
|
|
138
157
|
|
|
139
|
-
const spinner = ora(
|
|
158
|
+
const spinner = ora(`Upgrading ${state.target}...`).start();
|
|
140
159
|
|
|
141
160
|
try {
|
|
142
161
|
if (state.target === 'claude-code') {
|
|
143
|
-
await this.upgradeClaudeCode();
|
|
162
|
+
await this.upgradeClaudeCode(autoInstall);
|
|
144
163
|
} else if (state.target === 'opencode') {
|
|
145
164
|
await this.upgradeOpenCode();
|
|
146
165
|
}
|
|
147
166
|
|
|
148
|
-
spinner.succeed(`${state.target}
|
|
167
|
+
spinner.succeed(`${state.target} upgraded to latest version`);
|
|
149
168
|
return true;
|
|
150
169
|
} catch (error) {
|
|
151
|
-
spinner.fail(`${state.target}
|
|
170
|
+
spinner.fail(`${state.target} upgrade failed`);
|
|
152
171
|
throw new CLIError(
|
|
153
|
-
|
|
172
|
+
`Failed to upgrade ${state.target}: ${error instanceof Error ? error.message : String(error)}`,
|
|
154
173
|
'TARGET_UPGRADE_FAILED'
|
|
155
174
|
);
|
|
156
175
|
}
|
|
157
176
|
}
|
|
158
177
|
|
|
159
|
-
private async upgradeClaudeCode(): Promise<void> {
|
|
178
|
+
private async upgradeClaudeCode(autoInstall: boolean = false): Promise<void> {
|
|
160
179
|
if (this.options.dryRun) {
|
|
161
|
-
console.log('
|
|
180
|
+
console.log('Dry run: claude update');
|
|
162
181
|
return;
|
|
163
182
|
}
|
|
164
183
|
|
|
165
|
-
|
|
166
|
-
|
|
184
|
+
if (autoInstall) {
|
|
185
|
+
// Use detected package manager to install latest version
|
|
186
|
+
const packageManager = detectPackageManager(this.projectPath);
|
|
187
|
+
const installCmd = getUpgradeCommand('@anthropic-ai/claude-code', packageManager);
|
|
188
|
+
const { stdout } = await execAsync(installCmd);
|
|
167
189
|
|
|
168
|
-
|
|
169
|
-
|
|
190
|
+
if (this.options.verbose) {
|
|
191
|
+
console.log(stdout);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// Claude Code has built-in update command
|
|
195
|
+
const { stdout } = await execAsync('claude update');
|
|
196
|
+
|
|
197
|
+
if (this.options.verbose) {
|
|
198
|
+
console.log(stdout);
|
|
199
|
+
}
|
|
170
200
|
}
|
|
171
201
|
}
|
|
172
202
|
|
|
@@ -256,12 +286,18 @@ export class UpgradeManager {
|
|
|
256
286
|
|
|
257
287
|
private async getLatestFlowVersion(): Promise<string | null> {
|
|
258
288
|
try {
|
|
259
|
-
//
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
return packageJson.version || null;
|
|
289
|
+
// Check npm registry for latest published version
|
|
290
|
+
const { stdout } = await execAsync('npm view @sylphx/flow version');
|
|
291
|
+
return stdout.trim();
|
|
263
292
|
} catch {
|
|
264
|
-
|
|
293
|
+
// Fallback: read from local package.json
|
|
294
|
+
try {
|
|
295
|
+
const packagePath = path.join(__dirname, '..', '..', 'package.json');
|
|
296
|
+
const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf-8'));
|
|
297
|
+
return packageJson.version || null;
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
265
301
|
}
|
|
266
302
|
}
|
|
267
303
|
|
|
@@ -279,17 +315,13 @@ export class UpgradeManager {
|
|
|
279
315
|
return null;
|
|
280
316
|
}
|
|
281
317
|
|
|
282
|
-
private async getLatestTargetVersion(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
318
|
+
private async getLatestTargetVersion(): Promise<string | null> {
|
|
319
|
+
try {
|
|
320
|
+
const { stdout } = await execAsync('npm view @anthropic-ai/claude-code version');
|
|
321
|
+
return stdout.trim();
|
|
322
|
+
} catch {
|
|
323
|
+
return null;
|
|
290
324
|
}
|
|
291
|
-
|
|
292
|
-
return null;
|
|
293
325
|
}
|
|
294
326
|
|
|
295
327
|
static async isUpgradeAvailable(): Promise<boolean> {
|