@sylphx/flow 3.18.0 → 3.19.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 +41 -0
- package/package.json +1 -1
- package/src/config/targets.ts +1 -1
- package/src/core/__tests__/backup-restore.test.ts +1 -1
- package/src/core/__tests__/cleanup-handler.test.ts +292 -0
- package/src/core/__tests__/git-stash-manager.test.ts +246 -0
- package/src/core/__tests__/secrets-manager.test.ts +126 -0
- package/src/core/__tests__/session-cleanup.test.ts +147 -0
- package/src/core/attach-manager.ts +7 -77
- package/src/core/backup-manager.ts +8 -20
- package/src/core/cleanup-handler.ts +179 -7
- package/src/core/error-handling.ts +0 -30
- package/src/core/flow-executor.ts +58 -76
- package/src/core/git-stash-manager.ts +50 -68
- package/src/core/project-manager.ts +12 -14
- package/src/core/session-manager.ts +28 -33
- package/src/core/state-detector.ts +4 -15
- package/src/core/target-resolver.ts +14 -9
- package/src/core/template-loader.ts +7 -33
- package/src/core/upgrade-manager.ts +4 -15
- package/src/index.ts +6 -35
- package/src/targets/claude-code.ts +16 -107
- package/src/targets/functional/claude-code-logic.ts +47 -103
- package/src/targets/opencode.ts +2 -158
- package/src/targets/shared/target-operations.ts +1 -54
- package/src/types/target.types.ts +4 -24
- package/src/utils/config/target-config.ts +8 -14
- package/src/utils/config/target-utils.ts +1 -50
- package/src/utils/files/sync-utils.ts +5 -5
- package/src/utils/object-utils.ts +10 -2
- package/src/utils/security/secret-utils.ts +2 -2
|
@@ -4,14 +4,18 @@
|
|
|
4
4
|
* All projects store data in ~/.sylphx-flow/ isolated by project hash
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { exec } from 'node:child_process';
|
|
7
8
|
import crypto from 'node:crypto';
|
|
8
9
|
import { existsSync } from 'node:fs';
|
|
9
10
|
import fs from 'node:fs/promises';
|
|
10
11
|
import os from 'node:os';
|
|
11
12
|
import path from 'node:path';
|
|
13
|
+
import { promisify } from 'node:util';
|
|
12
14
|
import type { Target } from '../types/target.types.js';
|
|
13
15
|
import { targetManager } from './target-manager.js';
|
|
14
16
|
|
|
17
|
+
const execAsync = promisify(exec);
|
|
18
|
+
|
|
15
19
|
export interface ProjectPaths {
|
|
16
20
|
sessionFile: string;
|
|
17
21
|
backupsDir: string;
|
|
@@ -63,26 +67,20 @@ export class ProjectManager {
|
|
|
63
67
|
* Initialize Flow directories
|
|
64
68
|
*/
|
|
65
69
|
async initialize(): Promise<void> {
|
|
66
|
-
|
|
67
|
-
this.flowHomeDir,
|
|
68
|
-
path.join(this.flowHomeDir, '
|
|
69
|
-
path.join(this.flowHomeDir, '
|
|
70
|
-
path.join(this.flowHomeDir, '
|
|
71
|
-
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
for (const dir of dirs) {
|
|
75
|
-
await fs.mkdir(dir, { recursive: true });
|
|
76
|
-
}
|
|
70
|
+
await Promise.all([
|
|
71
|
+
fs.mkdir(path.join(this.flowHomeDir, 'sessions'), { recursive: true }),
|
|
72
|
+
fs.mkdir(path.join(this.flowHomeDir, 'backups'), { recursive: true }),
|
|
73
|
+
fs.mkdir(path.join(this.flowHomeDir, 'secrets'), { recursive: true }),
|
|
74
|
+
fs.mkdir(path.join(this.flowHomeDir, 'templates'), { recursive: true }),
|
|
75
|
+
]);
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
/**
|
|
80
|
-
* Check if a command is available on the system
|
|
79
|
+
* Check if a command is available on the system (non-blocking)
|
|
81
80
|
*/
|
|
82
81
|
private async isCommandAvailable(command: string): Promise<boolean> {
|
|
83
82
|
try {
|
|
84
|
-
|
|
85
|
-
execSync(`which ${command}`, { stdio: 'ignore' });
|
|
83
|
+
await execAsync(`which ${command}`, { timeout: 5000 });
|
|
86
84
|
return true;
|
|
87
85
|
} catch {
|
|
88
86
|
return false;
|
|
@@ -208,6 +208,34 @@ export class SessionManager {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Prune old session history files to prevent unbounded accumulation
|
|
213
|
+
* Keeps the most recent N history entries (sorted by session timestamp in filename)
|
|
214
|
+
*/
|
|
215
|
+
async cleanupSessionHistory(keepLast: number = 50): Promise<void> {
|
|
216
|
+
const flowHome = this.projectManager.getFlowHomeDir();
|
|
217
|
+
const historyDir = path.join(flowHome, 'sessions', 'history');
|
|
218
|
+
|
|
219
|
+
if (!existsSync(historyDir)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const files = await fs.readdir(historyDir);
|
|
224
|
+
const sessionFiles = files
|
|
225
|
+
.filter((f) => f.endsWith('.json'))
|
|
226
|
+
.sort() // session-{timestamp}.json sorts chronologically
|
|
227
|
+
.reverse(); // newest first
|
|
228
|
+
|
|
229
|
+
// Delete files beyond keepLast
|
|
230
|
+
for (const file of sessionFiles.slice(keepLast)) {
|
|
231
|
+
try {
|
|
232
|
+
await fs.unlink(path.join(historyDir, file));
|
|
233
|
+
} catch {
|
|
234
|
+
// Ignore errors — file might already be deleted
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
211
239
|
/**
|
|
212
240
|
* Recover from crashed session
|
|
213
241
|
*/
|
|
@@ -230,37 +258,4 @@ export class SessionManager {
|
|
|
230
258
|
// File might not exist
|
|
231
259
|
}
|
|
232
260
|
}
|
|
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) => new Date(b.session.startTime).getTime() - new Date(a.session.startTime).getTime()
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
// Remove old sessions
|
|
261
|
-
const toRemove = sessions.slice(keepLast);
|
|
262
|
-
for (const { file } of toRemove) {
|
|
263
|
-
await fs.unlink(path.join(historyDir, file));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
261
|
}
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { ConfigService } from '../services/config-service.js';
|
|
5
5
|
import type { Target } from '../types/target.types.js';
|
|
6
|
-
import {
|
|
6
|
+
import { tryResolveTarget } from './target-resolver.js';
|
|
7
7
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = path.dirname(__filename);
|
|
@@ -43,17 +43,6 @@ export class StateDetector {
|
|
|
43
43
|
this.projectPath = projectPath;
|
|
44
44
|
}
|
|
45
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
|
-
|
|
57
46
|
async detect(): Promise<ProjectState> {
|
|
58
47
|
const state: ProjectState = {
|
|
59
48
|
initialized: false,
|
|
@@ -95,7 +84,7 @@ export class StateDetector {
|
|
|
95
84
|
}
|
|
96
85
|
|
|
97
86
|
// Resolve target to get config
|
|
98
|
-
const target = state.target ?
|
|
87
|
+
const target = state.target ? tryResolveTarget(state.target) : null;
|
|
99
88
|
|
|
100
89
|
// Check components based on target config
|
|
101
90
|
if (target) {
|
|
@@ -376,7 +365,7 @@ export class StateDetector {
|
|
|
376
365
|
targetId: string
|
|
377
366
|
): Promise<{ version: string | null; latestVersion: string | null }> {
|
|
378
367
|
try {
|
|
379
|
-
const target =
|
|
368
|
+
const target = tryResolveTarget(targetId);
|
|
380
369
|
if (!target) {
|
|
381
370
|
return { version: null, latestVersion: null };
|
|
382
371
|
}
|
|
@@ -428,7 +417,7 @@ export class StateDetector {
|
|
|
428
417
|
|
|
429
418
|
// Check required components based on target
|
|
430
419
|
if (state.initialized && state.target) {
|
|
431
|
-
const target =
|
|
420
|
+
const target = tryResolveTarget(state.target);
|
|
432
421
|
// CLI-based targets (category: 'cli') require agents to be installed
|
|
433
422
|
if (target && target.category === 'cli' && !state.components.agents.installed) {
|
|
434
423
|
return true; // CLI target initialized but no agents
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Target Resolver
|
|
3
|
-
* Shared utility for resolving target IDs to Target objects
|
|
4
|
-
* Eliminates duplication across BackupManager and SecretsManager
|
|
2
|
+
* Target Resolver — Single Source of Truth for target ID → Target resolution
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
import type { Target } from '../types/target.types.js';
|
|
8
6
|
import { targetManager } from './target-manager.js';
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
|
-
* Resolve
|
|
9
|
+
* Resolve target ID to Target object, returns null if not found
|
|
10
|
+
*/
|
|
11
|
+
export function tryResolveTarget(targetId: string): Target | null {
|
|
12
|
+
const targetOption = targetManager.getTarget(targetId);
|
|
13
|
+
return targetOption._tag === 'Some' ? targetOption.value : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve target ID to Target object
|
|
12
18
|
* @throws Error if target ID is not found
|
|
13
19
|
*/
|
|
14
20
|
export function resolveTarget(targetId: string): Target {
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
21
|
+
const target = tryResolveTarget(targetId);
|
|
22
|
+
if (!target) {
|
|
17
23
|
throw new Error(`Unknown target: ${targetId}`);
|
|
18
24
|
}
|
|
19
|
-
return
|
|
25
|
+
return target;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
/**
|
|
23
|
-
* Resolve target
|
|
24
|
-
* Returns the Target object in both cases
|
|
29
|
+
* Resolve target from either string ID or Target object
|
|
25
30
|
*/
|
|
26
31
|
export function resolveTargetOrId(targetOrId: Target | string): Target {
|
|
27
32
|
return typeof targetOrId === 'string' ? resolveTarget(targetOrId) : targetOrId;
|
|
@@ -30,15 +30,13 @@ export class TemplateLoader {
|
|
|
30
30
|
const commandsDir = path.join(this.assetsDir, 'slash-commands');
|
|
31
31
|
const skillsDir = path.join(this.assetsDir, 'skills');
|
|
32
32
|
const mcpConfigPath = path.join(this.assetsDir, 'mcp-servers.json');
|
|
33
|
-
const outputStylesDir = path.join(this.assetsDir, 'output-styles');
|
|
34
33
|
|
|
35
34
|
// Load all directories in parallel
|
|
36
|
-
const [agents, commands, skills, mcpServers,
|
|
35
|
+
const [agents, commands, skills, mcpServers, rules] = await Promise.all([
|
|
37
36
|
existsSync(agentsDir) ? this.loadAgents(agentsDir) : [],
|
|
38
37
|
existsSync(commandsDir) ? this.loadCommands(commandsDir) : [],
|
|
39
38
|
existsSync(skillsDir) ? this.loadSkills(skillsDir) : [],
|
|
40
39
|
existsSync(mcpConfigPath) ? this.loadMCPServers(mcpConfigPath) : [],
|
|
41
|
-
existsSync(outputStylesDir) ? this.loadSingleFiles(outputStylesDir) : [],
|
|
42
40
|
this.loadRules(),
|
|
43
41
|
]);
|
|
44
42
|
|
|
@@ -48,8 +46,7 @@ export class TemplateLoader {
|
|
|
48
46
|
skills,
|
|
49
47
|
rules,
|
|
50
48
|
mcpServers,
|
|
51
|
-
|
|
52
|
-
singleFiles,
|
|
49
|
+
singleFiles: [],
|
|
53
50
|
};
|
|
54
51
|
}
|
|
55
52
|
|
|
@@ -134,11 +131,13 @@ export class TemplateLoader {
|
|
|
134
131
|
/**
|
|
135
132
|
* Load MCP servers configuration
|
|
136
133
|
*/
|
|
137
|
-
private async loadMCPServers(
|
|
134
|
+
private async loadMCPServers(
|
|
135
|
+
configPath: string
|
|
136
|
+
): Promise<Array<{ name: string; config: Record<string, unknown> }>> {
|
|
138
137
|
const data = await fs.readFile(configPath, 'utf-8');
|
|
139
|
-
const config = JSON.parse(data)
|
|
138
|
+
const config = JSON.parse(data) as Record<string, Record<string, unknown>>;
|
|
140
139
|
|
|
141
|
-
const servers = [];
|
|
140
|
+
const servers: Array<{ name: string; config: Record<string, unknown> }> = [];
|
|
142
141
|
for (const [name, serverConfig] of Object.entries(config)) {
|
|
143
142
|
servers.push({ name, config: serverConfig });
|
|
144
143
|
}
|
|
@@ -146,31 +145,6 @@ export class TemplateLoader {
|
|
|
146
145
|
return servers;
|
|
147
146
|
}
|
|
148
147
|
|
|
149
|
-
/**
|
|
150
|
-
* Load single files (parallel loading)
|
|
151
|
-
*/
|
|
152
|
-
private async loadSingleFiles(
|
|
153
|
-
singleFilesDir: string
|
|
154
|
-
): Promise<Array<{ path: string; content: string }>> {
|
|
155
|
-
const entries = await fs.readdir(singleFilesDir);
|
|
156
|
-
|
|
157
|
-
const results = await Promise.all(
|
|
158
|
-
entries.map(async (entry) => {
|
|
159
|
-
const filePath = path.join(singleFilesDir, entry);
|
|
160
|
-
const stat = await fs.stat(filePath);
|
|
161
|
-
|
|
162
|
-
if (!stat.isFile()) {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
167
|
-
return { path: entry, content };
|
|
168
|
-
})
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
return results.filter((r): r is { path: string; content: string } => r !== null);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
148
|
/**
|
|
175
149
|
* Get assets directory path
|
|
176
150
|
*/
|
|
@@ -9,7 +9,7 @@ import { CLIError } from '../utils/error-handler.js';
|
|
|
9
9
|
import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
|
|
10
10
|
import { createSpinner, log } from '../utils/prompts/index.js';
|
|
11
11
|
import type { ProjectState } from './state-detector.js';
|
|
12
|
-
import {
|
|
12
|
+
import { tryResolveTarget } from './target-resolver.js';
|
|
13
13
|
|
|
14
14
|
const execAsync = promisify(exec);
|
|
15
15
|
|
|
@@ -152,23 +152,12 @@ export class UpgradeManager {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
/**
|
|
156
|
-
* Resolve target from ID string to Target object
|
|
157
|
-
*/
|
|
158
|
-
private resolveTarget(targetId: string): Target | null {
|
|
159
|
-
const targetOption = targetManager.getTarget(targetId);
|
|
160
|
-
if (targetOption._tag === 'None') {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
return targetOption.value;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
155
|
async upgradeTarget(state: ProjectState, autoInstall: boolean = false): Promise<boolean> {
|
|
167
156
|
if (!state.target || !state.targetLatestVersion) {
|
|
168
157
|
return false;
|
|
169
158
|
}
|
|
170
159
|
|
|
171
|
-
const target =
|
|
160
|
+
const target = tryResolveTarget(state.target);
|
|
172
161
|
if (!target) {
|
|
173
162
|
return false;
|
|
174
163
|
}
|
|
@@ -263,7 +252,7 @@ export class UpgradeManager {
|
|
|
263
252
|
const configPath = path.join(this.projectPath, getProjectSettingsFile());
|
|
264
253
|
const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
265
254
|
if (config.target) {
|
|
266
|
-
return
|
|
255
|
+
return tryResolveTarget(config.target);
|
|
267
256
|
}
|
|
268
257
|
} catch {
|
|
269
258
|
// Cannot read config
|
|
@@ -355,7 +344,7 @@ export class UpgradeManager {
|
|
|
355
344
|
}
|
|
356
345
|
|
|
357
346
|
private async getCurrentTargetVersion(targetId: string): Promise<string | null> {
|
|
358
|
-
const target =
|
|
347
|
+
const target = tryResolveTarget(targetId);
|
|
359
348
|
if (!target) {
|
|
360
349
|
return null;
|
|
361
350
|
}
|
package/src/index.ts
CHANGED
|
@@ -98,48 +98,19 @@ export async function runCLI(): Promise<void> {
|
|
|
98
98
|
* Set up global error handlers for uncaught exceptions and unhandled rejections
|
|
99
99
|
*/
|
|
100
100
|
function setupGlobalErrorHandling(): void {
|
|
101
|
-
// Handle
|
|
102
|
-
process.on('
|
|
103
|
-
|
|
104
|
-
console.error(` ${error.message}`);
|
|
105
|
-
if (process.env.NODE_ENV === 'development') {
|
|
106
|
-
console.error(' Stack trace:', error.stack);
|
|
107
|
-
}
|
|
108
|
-
process.exit(1);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Handle unhandled promise rejections
|
|
112
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
113
|
-
// Ignore AbortError - this is expected when user cancels operations
|
|
101
|
+
// Handle unhandled promise rejections (non-fatal, log only)
|
|
102
|
+
process.on('unhandledRejection', (reason) => {
|
|
103
|
+
// Ignore AbortError — expected when user cancels operations
|
|
114
104
|
if (reason instanceof Error && reason.name === 'AbortError') {
|
|
115
105
|
return;
|
|
116
106
|
}
|
|
117
|
-
|
|
118
|
-
// Only log unhandled rejections in development mode
|
|
119
|
-
// Don't exit the process - let the application handle errors gracefully
|
|
120
107
|
if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
|
|
121
|
-
console.error('✗ Unhandled Promise Rejection:');
|
|
122
|
-
console.error(` Reason: ${reason}`);
|
|
123
|
-
console.error(' Promise:', promise);
|
|
108
|
+
console.error('✗ Unhandled Promise Rejection:', reason);
|
|
124
109
|
}
|
|
125
110
|
});
|
|
126
111
|
|
|
127
|
-
//
|
|
128
|
-
process.
|
|
129
|
-
console.log('\nSylphx Flow CLI terminated by user');
|
|
130
|
-
process.exit(0);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
process.on('SIGTERM', () => {
|
|
134
|
-
console.log('\nSylphx Flow CLI terminated');
|
|
135
|
-
process.exit(0);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Ensure clean exit by allowing the event loop to drain
|
|
139
|
-
process.on('beforeExit', () => {
|
|
140
|
-
// Node.js will exit automatically after this handler completes
|
|
141
|
-
// No explicit process.exit() needed
|
|
142
|
-
});
|
|
112
|
+
// SIGINT/SIGTERM are handled by CleanupHandler which does async backup restoration.
|
|
113
|
+
// DO NOT register handlers here — process.exit() would preempt cleanup.
|
|
143
114
|
}
|
|
144
115
|
|
|
145
116
|
/**
|
|
@@ -2,11 +2,8 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import fsPromises from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import { installToDirectory } from '../core/installers/file-installer.js';
|
|
6
|
-
import { createMCPInstaller } from '../core/installers/mcp-installer.js';
|
|
7
5
|
import type { AgentMetadata, FrontMatterMetadata } from '../types/target-config.types.js';
|
|
8
6
|
import type { CommonOptions, MCPServerConfigUnion, SetupResult, Target } from '../types.js';
|
|
9
|
-
import { getAgentsDir } from '../utils/config/paths.js';
|
|
10
7
|
import {
|
|
11
8
|
type ConfigData,
|
|
12
9
|
fileUtils,
|
|
@@ -16,9 +13,9 @@ import {
|
|
|
16
13
|
} from '../utils/config/target-utils.js';
|
|
17
14
|
import { CLIError } from '../utils/error-handler.js';
|
|
18
15
|
import { sanitize } from '../utils/security/security.js';
|
|
16
|
+
import { DEFAULT_CLAUDE_CODE_ENV } from './functional/claude-code-logic.js';
|
|
19
17
|
import {
|
|
20
18
|
detectTargetConfig,
|
|
21
|
-
setupSlashCommandsTo,
|
|
22
19
|
stripFrontMatter,
|
|
23
20
|
transformMCPConfig as transformMCP,
|
|
24
21
|
} from './shared/index.js';
|
|
@@ -70,6 +67,7 @@ export const claudeCodeTarget: Target = {
|
|
|
70
67
|
createConfigFile: true,
|
|
71
68
|
useSecretFiles: false,
|
|
72
69
|
},
|
|
70
|
+
supportsMCP: true,
|
|
73
71
|
},
|
|
74
72
|
|
|
75
73
|
/**
|
|
@@ -271,7 +269,7 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
271
269
|
const child = spawn('claude', args, {
|
|
272
270
|
stdio: 'inherit',
|
|
273
271
|
shell: false,
|
|
274
|
-
env: process.env,
|
|
272
|
+
env: { ...process.env, ...DEFAULT_CLAUDE_CODE_ENV },
|
|
275
273
|
});
|
|
276
274
|
|
|
277
275
|
child.on('spawn', () => {
|
|
@@ -298,17 +296,17 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
298
296
|
});
|
|
299
297
|
});
|
|
300
298
|
} catch (error: unknown) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
299
|
+
if (error instanceof Error) {
|
|
300
|
+
const errWithCode = error as Error & { code?: string | number };
|
|
301
|
+
if (errWithCode.code === 'ENOENT') {
|
|
302
|
+
throw new CLIError('Claude Code not found. Please install it first.', 'CLAUDE_NOT_FOUND');
|
|
303
|
+
}
|
|
304
|
+
if (errWithCode.code !== undefined) {
|
|
305
|
+
throw new CLIError(`Claude Code exited with code ${errWithCode.code}`, 'CLAUDE_ERROR');
|
|
306
|
+
}
|
|
307
|
+
throw new CLIError(`Failed to execute Claude Code: ${error.message}`, 'CLAUDE_ERROR');
|
|
307
308
|
}
|
|
308
|
-
throw new CLIError(
|
|
309
|
-
`Failed to execute Claude Code: ${(error as Error).message}`,
|
|
310
|
-
'CLAUDE_ERROR'
|
|
311
|
-
);
|
|
309
|
+
throw new CLIError(`Failed to execute Claude Code: ${String(error)}`, 'CLAUDE_ERROR');
|
|
312
310
|
}
|
|
313
311
|
},
|
|
314
312
|
|
|
@@ -370,10 +368,10 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
370
368
|
transformRulesContent: stripFrontMatter,
|
|
371
369
|
|
|
372
370
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
371
|
+
* Apply Claude Code settings (attribution, hooks, env, thinking mode)
|
|
372
|
+
* Merges Flow defaults into .claude/settings.json, preserving user settings
|
|
375
373
|
*/
|
|
376
|
-
async
|
|
374
|
+
async applySettings(cwd: string, _options: CommonOptions): Promise<SetupResult> {
|
|
377
375
|
const { processSettings, generateHookCommands } = await import(
|
|
378
376
|
'./functional/claude-code-logic.js'
|
|
379
377
|
);
|
|
@@ -425,95 +423,6 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
425
423
|
message: 'Configured notification hook',
|
|
426
424
|
};
|
|
427
425
|
},
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Setup agents for Claude Code
|
|
431
|
-
* Install agents to .claude/agents/ directory with rules appended
|
|
432
|
-
* Output styles are applied dynamically at runtime based on user settings
|
|
433
|
-
*/
|
|
434
|
-
async setupAgents(cwd: string, options: CommonOptions): Promise<SetupResult> {
|
|
435
|
-
const { enhanceAgentContent } = await import('../utils/agent-enhancer.js');
|
|
436
|
-
const agentsDir = path.join(cwd, this.config.agentDir);
|
|
437
|
-
|
|
438
|
-
const results = await installToDirectory(
|
|
439
|
-
getAgentsDir(),
|
|
440
|
-
agentsDir,
|
|
441
|
-
async (content, sourcePath) => {
|
|
442
|
-
// Extract rules from ORIGINAL content before transformation
|
|
443
|
-
const { metadata } = await yamlUtils.extractFrontMatter(content);
|
|
444
|
-
const rules = metadata.rules as string[] | undefined;
|
|
445
|
-
|
|
446
|
-
// Transform agent content (converts to Claude Code format, strips unsupported fields)
|
|
447
|
-
const transformed = await this.transformAgentContent(content, undefined, sourcePath);
|
|
448
|
-
|
|
449
|
-
// Enhance with rules only (output styles are applied dynamically at runtime)
|
|
450
|
-
const enhanced = await enhanceAgentContent(transformed, rules, []);
|
|
451
|
-
|
|
452
|
-
return enhanced;
|
|
453
|
-
},
|
|
454
|
-
{
|
|
455
|
-
...options,
|
|
456
|
-
showProgress: false, // UI handled by init-command
|
|
457
|
-
}
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
return { count: results.length };
|
|
461
|
-
},
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Setup output styles for Claude Code
|
|
465
|
-
* Output styles are appended to each agent file
|
|
466
|
-
*/
|
|
467
|
-
async setupOutputStyles(_cwd: string, _options: CommonOptions): Promise<SetupResult> {
|
|
468
|
-
// Output styles are appended to each agent file during setupAgents
|
|
469
|
-
// No separate installation needed
|
|
470
|
-
return {
|
|
471
|
-
count: 0,
|
|
472
|
-
message: 'Output styles included in agent files',
|
|
473
|
-
};
|
|
474
|
-
},
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Setup rules for Claude Code
|
|
478
|
-
* Rules are appended to each agent file
|
|
479
|
-
*/
|
|
480
|
-
async setupRules(_cwd: string, _options: CommonOptions): Promise<SetupResult> {
|
|
481
|
-
// Rules are appended to each agent file during setupAgents
|
|
482
|
-
// No separate CLAUDE.md file needed
|
|
483
|
-
return {
|
|
484
|
-
count: 0,
|
|
485
|
-
message: 'Rules included in agent files',
|
|
486
|
-
};
|
|
487
|
-
},
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Setup MCP servers for Claude Code
|
|
491
|
-
* Select, configure, install, and approve MCP servers
|
|
492
|
-
*/
|
|
493
|
-
async setupMCP(cwd: string, options: CommonOptions): Promise<SetupResult> {
|
|
494
|
-
const installer = createMCPInstaller(this);
|
|
495
|
-
const result = await installer.setupMCP({ ...options, quiet: true });
|
|
496
|
-
|
|
497
|
-
// Approve servers in Claude Code settings
|
|
498
|
-
if (result.selectedServers.length > 0 && !options.dryRun) {
|
|
499
|
-
if (this.approveMCPServers) {
|
|
500
|
-
await this.approveMCPServers(cwd, result.selectedServers);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
return { count: result.selectedServers.length };
|
|
505
|
-
},
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Setup slash commands for Claude Code
|
|
509
|
-
* Install slash command templates to .claude/commands/ directory
|
|
510
|
-
*/
|
|
511
|
-
async setupSlashCommands(cwd: string, options: CommonOptions): Promise<SetupResult> {
|
|
512
|
-
if (!this.config.slashCommandsDir) {
|
|
513
|
-
return { count: 0 };
|
|
514
|
-
}
|
|
515
|
-
return setupSlashCommandsTo(path.join(cwd, this.config.slashCommandsDir), undefined, options);
|
|
516
|
-
},
|
|
517
426
|
};
|
|
518
427
|
|
|
519
428
|
/**
|