@sylphx/flow 2.1.2 → 2.1.4
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 +23 -0
- package/README.md +44 -0
- package/package.json +79 -73
- package/src/commands/flow/execute-v2.ts +39 -30
- package/src/commands/flow/index.ts +2 -4
- package/src/commands/flow/prompt.ts +5 -3
- package/src/commands/flow/types.ts +0 -9
- package/src/commands/flow-command.ts +20 -13
- package/src/commands/hook-command.ts +1 -3
- package/src/commands/settings-command.ts +36 -33
- package/src/config/ai-config.ts +60 -41
- package/src/core/agent-loader.ts +11 -6
- package/src/core/attach-manager.ts +92 -84
- package/src/core/backup-manager.ts +35 -29
- package/src/core/cleanup-handler.ts +11 -8
- package/src/core/error-handling.ts +23 -30
- package/src/core/flow-executor.ts +58 -76
- package/src/core/formatting/bytes.ts +2 -4
- package/src/core/functional/async.ts +5 -4
- package/src/core/functional/error-handler.ts +2 -2
- package/src/core/git-stash-manager.ts +21 -10
- package/src/core/installers/file-installer.ts +0 -1
- package/src/core/installers/mcp-installer.ts +0 -1
- package/src/core/project-manager.ts +24 -18
- package/src/core/secrets-manager.ts +54 -73
- package/src/core/session-manager.ts +20 -22
- package/src/core/state-detector.ts +139 -80
- package/src/core/template-loader.ts +13 -31
- package/src/core/upgrade-manager.ts +122 -69
- package/src/index.ts +8 -5
- package/src/services/auto-upgrade.ts +1 -1
- package/src/services/config-service.ts +41 -29
- package/src/services/global-config.ts +2 -2
- package/src/services/target-installer.ts +9 -7
- package/src/targets/claude-code.ts +28 -15
- package/src/targets/opencode.ts +17 -6
- package/src/types/cli.types.ts +2 -2
- package/src/types/provider.types.ts +1 -7
- package/src/types/session.types.ts +11 -11
- package/src/types/target.types.ts +3 -1
- package/src/types/todo.types.ts +1 -1
- package/src/types.ts +1 -1
- package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
- package/src/utils/agent-enhancer.ts +111 -3
- package/src/utils/config/paths.ts +3 -1
- package/src/utils/config/target-utils.ts +2 -2
- package/src/utils/display/banner.ts +2 -2
- package/src/utils/display/notifications.ts +58 -45
- package/src/utils/display/status.ts +29 -12
- package/src/utils/files/file-operations.ts +1 -1
- package/src/utils/files/sync-utils.ts +38 -41
- package/src/utils/index.ts +19 -27
- package/src/utils/package-manager-detector.ts +15 -5
- package/src/utils/security/security.ts +8 -4
- package/src/utils/target-selection.ts +5 -2
- package/src/utils/version.ts +4 -2
- package/src/commands/flow/execute.ts +0 -453
- package/src/commands/flow/setup.ts +0 -312
- package/src/commands/flow-orchestrator.ts +0 -328
- package/src/commands/init-command.ts +0 -92
- package/src/commands/init-core.ts +0 -331
- package/src/commands/run-command.ts +0 -126
- package/src/core/agent-manager.ts +0 -174
- package/src/core/loop-controller.ts +0 -200
- package/src/core/rule-loader.ts +0 -147
- package/src/core/rule-manager.ts +0 -240
- package/src/services/claude-config-service.ts +0 -252
- package/src/services/first-run-setup.ts +0 -220
- package/src/services/smart-config-service.ts +0 -269
- package/src/types/api.types.ts +0 -9
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* Stores secrets in ~/.sylphx-flow/secrets/{project-hash}/
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
7
8
|
import fs from 'node:fs/promises';
|
|
8
9
|
import path from 'node:path';
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import { ProjectManager } from './project-manager.js';
|
|
10
|
+
import type { Target } from '../types/target.types.js';
|
|
11
|
+
import type { ProjectManager } from './project-manager.js';
|
|
12
|
+
import { targetManager } from './target-manager.js';
|
|
13
13
|
|
|
14
14
|
export interface MCPSecrets {
|
|
15
15
|
version: string;
|
|
@@ -30,19 +30,29 @@ export class SecretsManager {
|
|
|
30
30
|
this.projectManager = projectManager;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Resolve target from ID string to Target object
|
|
35
|
+
*/
|
|
36
|
+
private resolveTarget(targetId: string): Target {
|
|
37
|
+
const targetOption = targetManager.getTarget(targetId);
|
|
38
|
+
if (targetOption._tag === 'None') {
|
|
39
|
+
throw new Error(`Unknown target: ${targetId}`);
|
|
40
|
+
}
|
|
41
|
+
return targetOption.value;
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
/**
|
|
34
45
|
* Extract MCP secrets from project config
|
|
35
46
|
*/
|
|
36
47
|
async extractMCPSecrets(
|
|
37
48
|
projectPath: string,
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
_projectHash: string,
|
|
50
|
+
targetOrId: Target | string
|
|
40
51
|
): Promise<MCPSecrets> {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
: path.join(targetDir, '.mcp.json');
|
|
52
|
+
const target = typeof targetOrId === 'string' ? this.resolveTarget(targetOrId) : targetOrId;
|
|
53
|
+
// configFile is at project root, not in targetDir
|
|
54
|
+
const configPath = path.join(projectPath, target.config.configFile);
|
|
55
|
+
const mcpPath = target.config.mcpConfigPath;
|
|
46
56
|
|
|
47
57
|
const secrets: MCPSecrets = {
|
|
48
58
|
version: '1.0.0',
|
|
@@ -57,28 +67,32 @@ export class SecretsManager {
|
|
|
57
67
|
try {
|
|
58
68
|
const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
59
69
|
|
|
60
|
-
// Extract MCP server secrets
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
// Extract MCP server secrets using target's mcpConfigPath
|
|
71
|
+
const mcpServers = config[mcpPath] as Record<string, unknown> | undefined;
|
|
72
|
+
if (mcpServers) {
|
|
73
|
+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
63
74
|
const server = serverConfig as any;
|
|
64
75
|
|
|
65
|
-
// Extract env vars (sensitive)
|
|
66
|
-
|
|
76
|
+
// Extract env vars (sensitive) - handle both 'env' and 'environment' keys
|
|
77
|
+
const envVars = server.env || server.environment;
|
|
78
|
+
if (envVars && Object.keys(envVars).length > 0) {
|
|
67
79
|
secrets.servers[serverName] = {
|
|
68
|
-
env:
|
|
80
|
+
env: envVars,
|
|
69
81
|
};
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
// Extract args (may contain secrets)
|
|
73
|
-
|
|
84
|
+
// Extract args (may contain secrets) - handle both 'args' and 'command' array
|
|
85
|
+
const args =
|
|
86
|
+
server.args || (Array.isArray(server.command) ? server.command.slice(1) : undefined);
|
|
87
|
+
if (args && Array.isArray(args) && args.length > 0) {
|
|
74
88
|
if (!secrets.servers[serverName]) {
|
|
75
89
|
secrets.servers[serverName] = {};
|
|
76
90
|
}
|
|
77
|
-
secrets.servers[serverName].args =
|
|
91
|
+
secrets.servers[serverName].args = args;
|
|
78
92
|
}
|
|
79
93
|
}
|
|
80
94
|
}
|
|
81
|
-
} catch (
|
|
95
|
+
} catch (_error) {
|
|
82
96
|
// Config file exists but cannot be parsed, skip
|
|
83
97
|
}
|
|
84
98
|
|
|
@@ -123,19 +137,18 @@ export class SecretsManager {
|
|
|
123
137
|
*/
|
|
124
138
|
async restoreSecrets(
|
|
125
139
|
projectPath: string,
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
_projectHash: string,
|
|
141
|
+
targetOrId: Target | string,
|
|
128
142
|
secrets: MCPSecrets
|
|
129
143
|
): Promise<void> {
|
|
130
144
|
if (Object.keys(secrets.servers).length === 0) {
|
|
131
145
|
return;
|
|
132
146
|
}
|
|
133
147
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
: path.join(targetDir, '.mcp.json');
|
|
148
|
+
const target = typeof targetOrId === 'string' ? this.resolveTarget(targetOrId) : targetOrId;
|
|
149
|
+
// configFile is at project root, not in targetDir
|
|
150
|
+
const configPath = path.join(projectPath, target.config.configFile);
|
|
151
|
+
const mcpPath = target.config.mcpConfigPath;
|
|
139
152
|
|
|
140
153
|
if (!existsSync(configPath)) {
|
|
141
154
|
return;
|
|
@@ -144,18 +157,24 @@ export class SecretsManager {
|
|
|
144
157
|
try {
|
|
145
158
|
const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
146
159
|
|
|
147
|
-
// Restore secrets to MCP servers
|
|
148
|
-
|
|
160
|
+
// Restore secrets to MCP servers using target's mcpConfigPath
|
|
161
|
+
const mcpServers = config[mcpPath] as Record<string, unknown> | undefined;
|
|
162
|
+
if (mcpServers) {
|
|
149
163
|
for (const [serverName, serverSecrets] of Object.entries(secrets.servers)) {
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
const serverConfig = mcpServers[serverName] as Record<string, unknown> | undefined;
|
|
165
|
+
if (serverConfig) {
|
|
166
|
+
// Restore env vars - use the key that exists in config
|
|
152
167
|
if (serverSecrets.env) {
|
|
153
|
-
|
|
168
|
+
if ('environment' in serverConfig) {
|
|
169
|
+
serverConfig.environment = serverSecrets.env;
|
|
170
|
+
} else {
|
|
171
|
+
serverConfig.env = serverSecrets.env;
|
|
172
|
+
}
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
// Restore args
|
|
157
176
|
if (serverSecrets.args) {
|
|
158
|
-
|
|
177
|
+
serverConfig.args = serverSecrets.args;
|
|
159
178
|
}
|
|
160
179
|
}
|
|
161
180
|
}
|
|
@@ -163,49 +182,11 @@ export class SecretsManager {
|
|
|
163
182
|
|
|
164
183
|
// Write updated config
|
|
165
184
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
166
|
-
} catch (
|
|
185
|
+
} catch (_error) {
|
|
167
186
|
// Config restore failed, skip
|
|
168
187
|
}
|
|
169
188
|
}
|
|
170
189
|
|
|
171
|
-
/**
|
|
172
|
-
* Encrypt secrets (optional - for enhanced security)
|
|
173
|
-
*/
|
|
174
|
-
private async encrypt(data: string): Promise<string> {
|
|
175
|
-
// Use machine ID + user HOME as stable key source
|
|
176
|
-
const keySource = `${os.homedir()}-${os.hostname()}`;
|
|
177
|
-
const key = crypto.scryptSync(keySource, 'sylphx-flow-salt', 32);
|
|
178
|
-
const iv = crypto.randomBytes(16);
|
|
179
|
-
|
|
180
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
181
|
-
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
182
|
-
encrypted += cipher.final('hex');
|
|
183
|
-
|
|
184
|
-
const authTag = cipher.getAuthTag();
|
|
185
|
-
|
|
186
|
-
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Decrypt secrets (optional)
|
|
191
|
-
*/
|
|
192
|
-
private async decrypt(encrypted: string): Promise<string> {
|
|
193
|
-
const [ivHex, authTagHex, encryptedData] = encrypted.split(':');
|
|
194
|
-
|
|
195
|
-
const keySource = `${os.homedir()}-${os.hostname()}`;
|
|
196
|
-
const key = crypto.scryptSync(keySource, 'sylphx-flow-salt', 32);
|
|
197
|
-
const iv = Buffer.from(ivHex, 'hex');
|
|
198
|
-
const authTag = Buffer.from(authTagHex, 'hex');
|
|
199
|
-
|
|
200
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
201
|
-
decipher.setAuthTag(authTag);
|
|
202
|
-
|
|
203
|
-
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
|
204
|
-
decrypted += decipher.final('utf8');
|
|
205
|
-
|
|
206
|
-
return decrypted;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
190
|
/**
|
|
210
191
|
* Clear secrets for a project
|
|
211
192
|
*/
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* All sessions stored in ~/.sylphx-flow/sessions/
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
7
8
|
import fs from 'node:fs/promises';
|
|
8
9
|
import path from 'node:path';
|
|
9
|
-
import {
|
|
10
|
-
import { ProjectManager } from './project-manager.js';
|
|
10
|
+
import type { Target } from '../types/target.types.js';
|
|
11
|
+
import type { ProjectManager } from './project-manager.js';
|
|
11
12
|
|
|
12
13
|
export interface Session {
|
|
13
14
|
projectHash: string;
|
|
@@ -17,13 +18,13 @@ export interface Session {
|
|
|
17
18
|
startTime: string;
|
|
18
19
|
backupPath: string;
|
|
19
20
|
status: 'active' | 'completed' | 'crashed';
|
|
20
|
-
target:
|
|
21
|
+
target: string;
|
|
21
22
|
cleanupRequired: boolean;
|
|
22
23
|
// Multi-session support
|
|
23
|
-
isOriginal: boolean;
|
|
24
|
-
sharedBackupId: string;
|
|
25
|
-
refCount: number;
|
|
26
|
-
activePids: number[];
|
|
24
|
+
isOriginal: boolean; // First session that created backup
|
|
25
|
+
sharedBackupId: string; // Shared backup ID for all sessions
|
|
26
|
+
refCount: number; // Number of active sessions
|
|
27
|
+
activePids: number[]; // All active PIDs sharing this session
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export class SessionManager {
|
|
@@ -39,10 +40,12 @@ export class SessionManager {
|
|
|
39
40
|
async startSession(
|
|
40
41
|
projectPath: string,
|
|
41
42
|
projectHash: string,
|
|
42
|
-
|
|
43
|
+
targetOrId: Target | string,
|
|
43
44
|
backupPath: string,
|
|
44
45
|
sessionId?: string
|
|
45
46
|
): Promise<{ session: Session; isFirstSession: boolean }> {
|
|
47
|
+
// Get target ID for storage
|
|
48
|
+
const targetId = typeof targetOrId === 'string' ? targetOrId : targetOrId.id;
|
|
46
49
|
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
47
50
|
|
|
48
51
|
// Ensure sessions directory exists
|
|
@@ -74,7 +77,7 @@ export class SessionManager {
|
|
|
74
77
|
startTime: new Date().toISOString(),
|
|
75
78
|
backupPath,
|
|
76
79
|
status: 'active',
|
|
77
|
-
target,
|
|
80
|
+
target: targetId,
|
|
78
81
|
cleanupRequired: true,
|
|
79
82
|
isOriginal: true,
|
|
80
83
|
sharedBackupId: newSessionId,
|
|
@@ -93,7 +96,9 @@ export class SessionManager {
|
|
|
93
96
|
/**
|
|
94
97
|
* Mark session as completed (with reference counting)
|
|
95
98
|
*/
|
|
96
|
-
async endSession(
|
|
99
|
+
async endSession(
|
|
100
|
+
projectHash: string
|
|
101
|
+
): Promise<{ shouldRestore: boolean; session: Session | null }> {
|
|
97
102
|
try {
|
|
98
103
|
const session = await this.getActiveSession(projectHash);
|
|
99
104
|
|
|
@@ -104,7 +109,7 @@ export class SessionManager {
|
|
|
104
109
|
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
105
110
|
|
|
106
111
|
// Remove current PID from active PIDs
|
|
107
|
-
session.activePids = session.activePids.filter(pid => pid !== process.pid);
|
|
112
|
+
session.activePids = session.activePids.filter((pid) => pid !== process.pid);
|
|
108
113
|
session.refCount = session.activePids.length;
|
|
109
114
|
|
|
110
115
|
if (session.refCount === 0) {
|
|
@@ -115,12 +120,7 @@ export class SessionManager {
|
|
|
115
120
|
const flowHome = this.projectManager.getFlowHomeDir();
|
|
116
121
|
|
|
117
122
|
// Archive to history
|
|
118
|
-
const historyPath = path.join(
|
|
119
|
-
flowHome,
|
|
120
|
-
'sessions',
|
|
121
|
-
'history',
|
|
122
|
-
`${session.sessionId}.json`
|
|
123
|
-
);
|
|
123
|
+
const historyPath = path.join(flowHome, 'sessions', 'history', `${session.sessionId}.json`);
|
|
124
124
|
await fs.mkdir(path.dirname(historyPath), { recursive: true });
|
|
125
125
|
await fs.writeFile(historyPath, JSON.stringify(session, null, 2));
|
|
126
126
|
|
|
@@ -134,7 +134,7 @@ export class SessionManager {
|
|
|
134
134
|
|
|
135
135
|
return { shouldRestore: false, session };
|
|
136
136
|
}
|
|
137
|
-
} catch (
|
|
137
|
+
} catch (_error) {
|
|
138
138
|
// Session file might not exist
|
|
139
139
|
return { shouldRestore: false, session: null };
|
|
140
140
|
}
|
|
@@ -203,7 +203,7 @@ export class SessionManager {
|
|
|
203
203
|
// Send signal 0 to check if process exists
|
|
204
204
|
process.kill(pid, 0);
|
|
205
205
|
return true;
|
|
206
|
-
} catch (
|
|
206
|
+
} catch (_error) {
|
|
207
207
|
return false;
|
|
208
208
|
}
|
|
209
209
|
}
|
|
@@ -254,9 +254,7 @@ export class SessionManager {
|
|
|
254
254
|
|
|
255
255
|
// Sort by start time (newest first)
|
|
256
256
|
sessions.sort(
|
|
257
|
-
(a, b) =>
|
|
258
|
-
new Date(b.session.startTime).getTime() -
|
|
259
|
-
new Date(a.session.startTime).getTime()
|
|
257
|
+
(a, b) => new Date(b.session.startTime).getTime() - new Date(a.session.startTime).getTime()
|
|
260
258
|
);
|
|
261
259
|
|
|
262
260
|
// Remove old sessions
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { projectSettings } from '../utils/config/settings.js';
|
|
5
|
-
import { targetManager } from './target-manager.js';
|
|
6
4
|
import { ConfigService } from '../services/config-service.js';
|
|
5
|
+
import type { Target } from '../types/target.types.js';
|
|
6
|
+
import { targetManager } from './target-manager.js';
|
|
7
7
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = path.dirname(__filename);
|
|
@@ -28,7 +28,13 @@ export interface ProjectState {
|
|
|
28
28
|
lastUpdated: Date | null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export type RecommendedAction =
|
|
31
|
+
export type RecommendedAction =
|
|
32
|
+
| 'FULL_INIT'
|
|
33
|
+
| 'RUN_ONLY'
|
|
34
|
+
| 'REPAIR'
|
|
35
|
+
| 'UPGRADE'
|
|
36
|
+
| 'UPGRADE_TARGET'
|
|
37
|
+
| 'CLEAN_INIT';
|
|
32
38
|
|
|
33
39
|
export class StateDetector {
|
|
34
40
|
private projectPath: string;
|
|
@@ -37,6 +43,17 @@ export class StateDetector {
|
|
|
37
43
|
this.projectPath = projectPath;
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Resolve target from ID string to Target object
|
|
48
|
+
*/
|
|
49
|
+
private resolveTarget(targetId: string): Target | null {
|
|
50
|
+
const targetOption = targetManager.getTarget(targetId);
|
|
51
|
+
if (targetOption._tag === 'None') {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return targetOption.value;
|
|
55
|
+
}
|
|
56
|
+
|
|
40
57
|
async detect(): Promise<ProjectState> {
|
|
41
58
|
const state: ProjectState = {
|
|
42
59
|
initialized: false,
|
|
@@ -77,37 +94,16 @@ export class StateDetector {
|
|
|
77
94
|
state.outdated = this.isVersionOutdated(state.version, state.latestVersion);
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
await this.
|
|
86
|
-
// OpenCode doesn't have separate hooks directory (hooks config in opencode.jsonc)
|
|
87
|
-
state.components.hooks.installed = false;
|
|
88
|
-
// OpenCode appends output styles to AGENTS.md
|
|
89
|
-
state.components.outputStyles.installed = await this.checkOutputStylesInAGENTS();
|
|
90
|
-
await this.checkComponent('slashCommands', '.opencode/command', '*.md', state);
|
|
91
|
-
} else {
|
|
92
|
-
// Claude Code (default)
|
|
93
|
-
await this.checkComponent('agents', '.claude/agents', '*.md', state);
|
|
94
|
-
|
|
95
|
-
// Claude Code includes rules and output styles in agent files
|
|
96
|
-
// So we mark them as installed if agents are installed
|
|
97
|
-
state.components.rules.installed = state.components.agents.installed;
|
|
98
|
-
state.components.rules.count = state.components.agents.count;
|
|
99
|
-
|
|
100
|
-
state.components.outputStyles.installed = state.components.agents.installed;
|
|
101
|
-
|
|
102
|
-
// Check hooks (optional for Claude Code)
|
|
103
|
-
await this.checkComponent('hooks', '.claude/hooks', '*.js', state);
|
|
104
|
-
|
|
105
|
-
// Check slash commands
|
|
106
|
-
await this.checkComponent('slashCommands', '.claude/commands', '*.md', state);
|
|
97
|
+
// Resolve target to get config
|
|
98
|
+
const target = state.target ? this.resolveTarget(state.target) : null;
|
|
99
|
+
|
|
100
|
+
// Check components based on target config
|
|
101
|
+
if (target) {
|
|
102
|
+
await this.checkComponentsForTarget(target, state);
|
|
107
103
|
}
|
|
108
104
|
|
|
109
105
|
// Check MCP
|
|
110
|
-
const mcpConfig = await this.checkMCPConfig(
|
|
106
|
+
const mcpConfig = await this.checkMCPConfig(target);
|
|
111
107
|
state.components.mcp.installed = mcpConfig.exists;
|
|
112
108
|
state.components.mcp.serverCount = mcpConfig.serverCount;
|
|
113
109
|
state.components.mcp.version = mcpConfig.version;
|
|
@@ -121,14 +117,45 @@ export class StateDetector {
|
|
|
121
117
|
|
|
122
118
|
// Check corruption
|
|
123
119
|
state.corrupted = await this.checkCorruption(state);
|
|
124
|
-
|
|
125
|
-
} catch (error) {
|
|
120
|
+
} catch (_error) {
|
|
126
121
|
state.corrupted = true;
|
|
127
122
|
}
|
|
128
123
|
|
|
129
124
|
return state;
|
|
130
125
|
}
|
|
131
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Check components based on target configuration
|
|
129
|
+
*/
|
|
130
|
+
private async checkComponentsForTarget(target: Target, state: ProjectState): Promise<void> {
|
|
131
|
+
// Check agents using target's agentDir
|
|
132
|
+
await this.checkComponent('agents', target.config.agentDir, '*.md', state);
|
|
133
|
+
|
|
134
|
+
// Check rules based on target config
|
|
135
|
+
if (target.config.rulesFile) {
|
|
136
|
+
// Target has separate rules file (e.g., OpenCode's AGENTS.md)
|
|
137
|
+
await this.checkFileComponent('rules', target.config.rulesFile, state);
|
|
138
|
+
// Check output styles in rules file
|
|
139
|
+
state.components.outputStyles.installed = await this.checkOutputStylesInFile(
|
|
140
|
+
target.config.rulesFile
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
// Rules are included in agent files (e.g., Claude Code)
|
|
144
|
+
state.components.rules.installed = state.components.agents.installed;
|
|
145
|
+
state.components.rules.count = state.components.agents.count;
|
|
146
|
+
state.components.outputStyles.installed = state.components.agents.installed;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check hooks - look for hooks directory in configDir
|
|
150
|
+
const hooksDir = path.join(target.config.configDir, 'hooks');
|
|
151
|
+
await this.checkComponent('hooks', hooksDir, '*.js', state);
|
|
152
|
+
|
|
153
|
+
// Check slash commands using target's slashCommandsDir
|
|
154
|
+
if (target.config.slashCommandsDir) {
|
|
155
|
+
await this.checkComponent('slashCommands', target.config.slashCommandsDir, '*.md', state);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
132
159
|
recommendAction(state: ProjectState): RecommendedAction {
|
|
133
160
|
if (!state.initialized) {
|
|
134
161
|
return 'FULL_INIT';
|
|
@@ -142,8 +169,11 @@ export class StateDetector {
|
|
|
142
169
|
return 'UPGRADE';
|
|
143
170
|
}
|
|
144
171
|
|
|
145
|
-
if (
|
|
146
|
-
|
|
172
|
+
if (
|
|
173
|
+
state.targetVersion &&
|
|
174
|
+
state.targetLatestVersion &&
|
|
175
|
+
this.isVersionOutdated(state.targetVersion, state.targetLatestVersion)
|
|
176
|
+
) {
|
|
147
177
|
return 'UPGRADE_TARGET';
|
|
148
178
|
}
|
|
149
179
|
|
|
@@ -170,8 +200,11 @@ export class StateDetector {
|
|
|
170
200
|
explanations.push('Run `bun dev:flow upgrade` to upgrade');
|
|
171
201
|
}
|
|
172
202
|
|
|
173
|
-
if (
|
|
174
|
-
|
|
203
|
+
if (
|
|
204
|
+
state.targetVersion &&
|
|
205
|
+
state.targetLatestVersion &&
|
|
206
|
+
this.isVersionOutdated(state.targetVersion, state.targetLatestVersion)
|
|
207
|
+
) {
|
|
175
208
|
explanations.push(`${state.target} update available`);
|
|
176
209
|
explanations.push(`Run \`bun dev:flow upgrade-target\` to upgrade`);
|
|
177
210
|
}
|
|
@@ -210,7 +243,10 @@ export class StateDetector {
|
|
|
210
243
|
): Promise<void> {
|
|
211
244
|
try {
|
|
212
245
|
const fullPath = path.join(this.projectPath, componentPath);
|
|
213
|
-
const exists = await fs
|
|
246
|
+
const exists = await fs
|
|
247
|
+
.access(fullPath)
|
|
248
|
+
.then(() => true)
|
|
249
|
+
.catch(() => false);
|
|
214
250
|
|
|
215
251
|
if (!exists) {
|
|
216
252
|
state.components[componentName].installed = false;
|
|
@@ -219,19 +255,30 @@ export class StateDetector {
|
|
|
219
255
|
|
|
220
256
|
// 计算文件数量
|
|
221
257
|
const files = await fs.readdir(fullPath).catch(() => []);
|
|
222
|
-
const count =
|
|
223
|
-
|
|
258
|
+
const count =
|
|
259
|
+
pattern === '*.js'
|
|
260
|
+
? files.filter((f) => f.endsWith('.js')).length
|
|
261
|
+
: pattern === '*.md'
|
|
262
|
+
? files.filter((f) => f.endsWith('.md')).length
|
|
263
|
+
: files.length;
|
|
224
264
|
|
|
225
265
|
// Component is only installed if it has files
|
|
226
266
|
state.components[componentName].installed = count > 0;
|
|
227
267
|
|
|
228
|
-
if (
|
|
268
|
+
if (
|
|
269
|
+
componentName === 'agents' ||
|
|
270
|
+
componentName === 'slashCommands' ||
|
|
271
|
+
componentName === 'rules'
|
|
272
|
+
) {
|
|
229
273
|
state.components[componentName].count = count;
|
|
230
274
|
}
|
|
231
275
|
|
|
232
276
|
// 这里可以读取版本信息(如果保存了的话)
|
|
233
277
|
const versionPath = path.join(fullPath, '.version');
|
|
234
|
-
const versionExists = await fs
|
|
278
|
+
const versionExists = await fs
|
|
279
|
+
.access(versionPath)
|
|
280
|
+
.then(() => true)
|
|
281
|
+
.catch(() => false);
|
|
235
282
|
if (versionExists) {
|
|
236
283
|
state.components[componentName].version = await fs.readFile(versionPath, 'utf-8');
|
|
237
284
|
}
|
|
@@ -247,7 +294,10 @@ export class StateDetector {
|
|
|
247
294
|
): Promise<void> {
|
|
248
295
|
try {
|
|
249
296
|
const fullPath = path.join(this.projectPath, filePath);
|
|
250
|
-
const exists = await fs
|
|
297
|
+
const exists = await fs
|
|
298
|
+
.access(fullPath)
|
|
299
|
+
.then(() => true)
|
|
300
|
+
.catch(() => false);
|
|
251
301
|
|
|
252
302
|
state.components[componentName].installed = exists;
|
|
253
303
|
|
|
@@ -260,53 +310,53 @@ export class StateDetector {
|
|
|
260
310
|
}
|
|
261
311
|
}
|
|
262
312
|
|
|
263
|
-
private async
|
|
313
|
+
private async checkOutputStylesInFile(filePath: string): Promise<boolean> {
|
|
264
314
|
try {
|
|
265
|
-
const
|
|
266
|
-
const exists = await fs
|
|
315
|
+
const fullPath = path.join(this.projectPath, filePath);
|
|
316
|
+
const exists = await fs
|
|
317
|
+
.access(fullPath)
|
|
318
|
+
.then(() => true)
|
|
319
|
+
.catch(() => false);
|
|
267
320
|
|
|
268
321
|
if (!exists) {
|
|
269
322
|
return false;
|
|
270
323
|
}
|
|
271
324
|
|
|
272
|
-
// Check if
|
|
273
|
-
const content = await fs.readFile(
|
|
325
|
+
// Check if file contains output styles section
|
|
326
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
274
327
|
return content.includes('# Output Styles');
|
|
275
328
|
} catch {
|
|
276
329
|
return false;
|
|
277
330
|
}
|
|
278
331
|
}
|
|
279
332
|
|
|
280
|
-
private async checkMCPConfig(
|
|
333
|
+
private async checkMCPConfig(
|
|
334
|
+
target?: Target | null
|
|
335
|
+
): Promise<{ exists: boolean; serverCount: number; version: string | null }> {
|
|
281
336
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (target === 'opencode') {
|
|
286
|
-
// OpenCode uses opencode.jsonc with mcp key
|
|
287
|
-
mcpPath = path.join(this.projectPath, 'opencode.jsonc');
|
|
288
|
-
serversKey = 'mcp';
|
|
289
|
-
} else {
|
|
290
|
-
// Claude Code uses .mcp.json with mcpServers key
|
|
291
|
-
mcpPath = path.join(this.projectPath, '.mcp.json');
|
|
292
|
-
serversKey = 'mcpServers';
|
|
337
|
+
if (!target) {
|
|
338
|
+
return { exists: false, serverCount: 0, version: null };
|
|
293
339
|
}
|
|
294
340
|
|
|
295
|
-
|
|
341
|
+
// Use target config for MCP file path and servers key
|
|
342
|
+
const mcpPath = path.join(this.projectPath, target.config.configFile);
|
|
343
|
+
const serversKey = target.config.mcpConfigPath;
|
|
344
|
+
|
|
345
|
+
const exists = await fs
|
|
346
|
+
.access(mcpPath)
|
|
347
|
+
.then(() => true)
|
|
348
|
+
.catch(() => false);
|
|
296
349
|
|
|
297
350
|
if (!exists) {
|
|
298
351
|
return { exists: false, serverCount: 0, version: null };
|
|
299
352
|
}
|
|
300
353
|
|
|
301
|
-
// Use
|
|
354
|
+
// Use target's readConfig method for proper parsing (handles JSONC, etc.)
|
|
302
355
|
let content: any;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
content = await fileUtils.readConfig(opencodeTarget.config, this.projectPath);
|
|
308
|
-
} else {
|
|
309
|
-
// Claude Code uses plain JSON
|
|
356
|
+
try {
|
|
357
|
+
content = await target.readConfig(this.projectPath);
|
|
358
|
+
} catch {
|
|
359
|
+
// Fallback to plain JSON parsing
|
|
310
360
|
content = JSON.parse(await fs.readFile(mcpPath, 'utf-8'));
|
|
311
361
|
}
|
|
312
362
|
|
|
@@ -322,19 +372,24 @@ export class StateDetector {
|
|
|
322
372
|
}
|
|
323
373
|
}
|
|
324
374
|
|
|
325
|
-
private async checkTargetVersion(
|
|
375
|
+
private async checkTargetVersion(
|
|
376
|
+
targetId: string
|
|
377
|
+
): Promise<{ version: string | null; latestVersion: string | null }> {
|
|
326
378
|
try {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
379
|
+
const target = this.resolveTarget(targetId);
|
|
380
|
+
if (!target) {
|
|
381
|
+
return { version: null, latestVersion: null };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Check if target has executeCommand (CLI-based target)
|
|
385
|
+
// Only CLI targets like claude-code have version checking capability
|
|
386
|
+
if (target.executeCommand && target.id === 'claude-code') {
|
|
331
387
|
const { exec } = await import('node:child_process');
|
|
332
388
|
const { promisify } = await import('node:util');
|
|
333
389
|
const execAsync = promisify(exec);
|
|
334
390
|
|
|
335
391
|
try {
|
|
336
392
|
const { stdout } = await execAsync('claude --version');
|
|
337
|
-
// 解析版本号
|
|
338
393
|
const match = stdout.match(/v?(\d+\.\d+\.\d+)/);
|
|
339
394
|
return {
|
|
340
395
|
version: match ? match[1] : null,
|
|
@@ -366,14 +421,18 @@ export class StateDetector {
|
|
|
366
421
|
}
|
|
367
422
|
|
|
368
423
|
private async checkCorruption(state: ProjectState): Promise<boolean> {
|
|
369
|
-
//
|
|
424
|
+
// Check for contradictory states
|
|
370
425
|
if (state.initialized && !state.target) {
|
|
371
|
-
return true; //
|
|
426
|
+
return true; // Initialized but no target
|
|
372
427
|
}
|
|
373
428
|
|
|
374
|
-
//
|
|
375
|
-
if (state.initialized && state.target
|
|
376
|
-
|
|
429
|
+
// Check required components based on target
|
|
430
|
+
if (state.initialized && state.target) {
|
|
431
|
+
const target = this.resolveTarget(state.target);
|
|
432
|
+
// CLI-based targets (category: 'cli') require agents to be installed
|
|
433
|
+
if (target && target.category === 'cli' && !state.components.agents.installed) {
|
|
434
|
+
return true; // CLI target initialized but no agents
|
|
435
|
+
}
|
|
377
436
|
}
|
|
378
437
|
|
|
379
438
|
return false;
|