@sylphx/flow 2.0.0 → 2.1.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.
@@ -7,6 +7,7 @@
7
7
  import fs from 'node:fs/promises';
8
8
  import path from 'node:path';
9
9
  import { existsSync } from 'node:fs';
10
+ import { createHash } from 'node:crypto';
10
11
  import chalk from 'chalk';
11
12
  import { ProjectManager } from './project-manager.js';
12
13
  import type { BackupManifest } from './backup-manager.js';
@@ -52,6 +53,18 @@ export class AttachManager {
52
53
  this.configService = new GlobalConfigService();
53
54
  }
54
55
 
56
+ /**
57
+ * Calculate SHA256 hash of file content
58
+ */
59
+ private async calculateFileHash(filePath: string): Promise<string> {
60
+ try {
61
+ const content = await fs.readFile(filePath, 'utf-8');
62
+ return createHash('sha256').update(content).digest('hex');
63
+ } catch {
64
+ return '';
65
+ }
66
+ }
67
+
55
68
  /**
56
69
  * Get target-specific directory names
57
70
  */
@@ -369,7 +382,7 @@ ${rules}
369
382
  // Track in manifest
370
383
  manifest.backup.config = {
371
384
  path: configPath,
372
- hash: '', // TODO: calculate hash
385
+ hash: await this.calculateFileHash(configPath),
373
386
  mcpServersCount: Object.keys(config.mcp.servers).length,
374
387
  };
375
388
  }
@@ -407,7 +420,9 @@ ${rules}
407
420
  }
408
421
 
409
422
  /**
410
- * Attach single files (CLAUDE.md, .cursorrules, etc.)
423
+ * Attach single files (output styles like silent.md)
424
+ * NOTE: These files are placed in the target config directory (.claude/ or .opencode/),
425
+ * NOT in the project root directory.
411
426
  */
412
427
  private async attachSingleFiles(
413
428
  projectPath: string,
@@ -415,33 +430,22 @@ ${rules}
415
430
  result: AttachResult,
416
431
  manifest: BackupManifest
417
432
  ): Promise<void> {
433
+ // Get target from manifest to determine correct directory
434
+ const target = manifest.target;
435
+ const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
436
+
418
437
  for (const file of singleFiles) {
419
- const filePath = path.join(projectPath, file.path);
438
+ // Write to target config directory, not project root
439
+ const filePath = path.join(targetDir, file.path);
420
440
  const existed = existsSync(filePath);
421
441
 
422
442
  if (existed) {
423
- // User has file, append Flow content
424
- const userContent = await fs.readFile(filePath, 'utf-8');
425
-
426
- // Check if already appended
427
- if (userContent.includes('<!-- Sylphx Flow Enhancement -->')) {
428
- continue;
429
- }
430
-
431
- const merged = `${userContent}
432
-
433
- ---
434
-
435
- **Sylphx Flow Enhancement:**
436
-
437
- ${file.content}
438
- `;
439
-
440
- await fs.writeFile(filePath, merged);
443
+ // User has file, overwrite with Flow content (backed up already)
444
+ await fs.writeFile(filePath, file.content);
441
445
 
442
446
  manifest.backup.singleFiles[file.path] = {
443
447
  existed: true,
444
- originalSize: userContent.length,
448
+ originalSize: (await fs.readFile(filePath, 'utf-8')).length,
445
449
  flowContentAdded: true,
446
450
  };
447
451
  } else {
@@ -169,6 +169,7 @@ export class FlowExecutor {
169
169
 
170
170
  /**
171
171
  * Clear user settings in replace mode
172
+ * This ensures a clean slate for Flow's configuration
172
173
  */
173
174
  private async clearUserSettings(
174
175
  projectPath: string,
@@ -188,7 +189,7 @@ export class FlowExecutor {
188
189
  ? { agents: 'agents', commands: 'commands' }
189
190
  : { agents: 'agent', commands: 'command' };
190
191
 
191
- // Clear agents directory
192
+ // 1. Clear agents directory (including AGENTS.md rules file)
192
193
  const agentsDir = path.join(targetDir, dirs.agents);
193
194
  if (existsSync(agentsDir)) {
194
195
  const files = await fs.readdir(agentsDir);
@@ -197,7 +198,7 @@ export class FlowExecutor {
197
198
  }
198
199
  }
199
200
 
200
- // Clear commands directory
201
+ // 2. Clear commands directory
201
202
  const commandsDir = path.join(targetDir, dirs.commands);
202
203
  if (existsSync(commandsDir)) {
203
204
  const files = await fs.readdir(commandsDir);
@@ -206,31 +207,69 @@ export class FlowExecutor {
206
207
  }
207
208
  }
208
209
 
209
- // Clear MCP configuration
210
+ // 3. Clear hooks directory
211
+ const hooksDir = path.join(targetDir, 'hooks');
212
+ if (existsSync(hooksDir)) {
213
+ const files = await fs.readdir(hooksDir);
214
+ for (const file of files) {
215
+ await fs.unlink(path.join(hooksDir, file));
216
+ }
217
+ }
218
+
219
+ // 4. Clear MCP configuration completely
210
220
  const configPath = target === 'claude-code'
211
221
  ? path.join(targetDir, 'settings.json')
212
222
  : path.join(targetDir, '.mcp.json');
213
223
 
214
224
  if (existsSync(configPath)) {
215
- // Clear MCP servers section only, keep other settings
216
- const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
217
225
  if (target === 'claude-code') {
218
- if (config.mcp?.servers) {
219
- config.mcp.servers = {};
226
+ // For Claude Code, clear entire MCP section to remove all user config
227
+ const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
228
+ if (config.mcp) {
229
+ // Remove entire MCP configuration, not just servers
230
+ delete config.mcp;
220
231
  await fs.writeFile(configPath, JSON.stringify(config, null, 2));
221
232
  }
222
233
  } else {
223
- // For opencode, clear the entire .mcp.json
234
+ // For OpenCode, clear the entire .mcp.json file
224
235
  await fs.writeFile(configPath, JSON.stringify({ servers: {} }, null, 2));
225
236
  }
226
237
  }
227
238
 
228
- // Clear hooks directory if exists
229
- const hooksDir = path.join(targetDir, 'hooks');
230
- if (existsSync(hooksDir)) {
231
- const files = await fs.readdir(hooksDir);
232
- for (const file of files) {
233
- await fs.unlink(path.join(hooksDir, file));
239
+ // 5. Clear AGENTS.md rules file (for OpenCode)
240
+ // Claude Code AGENTS.md is already handled in agents directory
241
+ if (target === 'opencode') {
242
+ const rulesPath = path.join(targetDir, 'AGENTS.md');
243
+ if (existsSync(rulesPath)) {
244
+ await fs.unlink(rulesPath);
245
+ }
246
+ }
247
+
248
+ // 6. Clear single files (output styles like silent.md)
249
+ // These are now in the target directory, not project root
250
+ const singleFiles = ['silent.md']; // Add other known single files here
251
+ for (const fileName of singleFiles) {
252
+ const filePath = path.join(targetDir, fileName);
253
+ if (existsSync(filePath)) {
254
+ await fs.unlink(filePath);
255
+ }
256
+ }
257
+
258
+ // 7. Clean up any Flow-created files in project root (legacy bug cleanup)
259
+ // This handles files that were incorrectly created in project root
260
+ const legacySingleFiles = ['silent.md'];
261
+ for (const fileName of legacySingleFiles) {
262
+ const filePath = path.join(projectPath, fileName);
263
+ if (existsSync(filePath)) {
264
+ // Only delete if it looks like a Flow-created file
265
+ try {
266
+ const content = await fs.readFile(filePath, 'utf-8');
267
+ if (content.includes('Sylphx Flow') || content.includes('Silent Execution Style')) {
268
+ await fs.unlink(filePath);
269
+ }
270
+ } catch {
271
+ // Ignore errors - file might not be readable
272
+ }
234
273
  }
235
274
  }
236
275
  }
@@ -257,60 +257,3 @@ export async function installFile(
257
257
  }
258
258
  }
259
259
 
260
- /**
261
- * File installer interface for backward compatibility
262
- */
263
- export interface FileInstaller {
264
- installToDirectory(
265
- sourceDir: string,
266
- targetDir: string,
267
- transform: FileTransformFn,
268
- options?: InstallOptions
269
- ): Promise<ProcessResult[]>;
270
- appendToFile(
271
- sourceDir: string,
272
- targetFile: string,
273
- transform: FileTransformFn,
274
- options?: InstallOptions
275
- ): Promise<void>;
276
- installFile(
277
- sourceFile: string,
278
- targetFile: string,
279
- transform: FileTransformFn,
280
- options?: InstallOptions
281
- ): Promise<void>;
282
- }
283
-
284
- /**
285
- * Composable file installer
286
- * Handles copying files from source to destination with optional transformation
287
- * @deprecated Use standalone functions (installToDirectory, appendToFile, installFile) for new code
288
- */
289
- export class FileInstaller {
290
- async installToDirectory(
291
- sourceDir: string,
292
- targetDir: string,
293
- transform: FileTransformFn,
294
- options: InstallOptions = {}
295
- ): Promise<ProcessResult[]> {
296
- return installToDirectory(sourceDir, targetDir, transform, options);
297
- }
298
-
299
- async appendToFile(
300
- sourceDir: string,
301
- targetFile: string,
302
- transform: FileTransformFn,
303
- options: InstallOptions = {}
304
- ): Promise<void> {
305
- return appendToFile(sourceDir, targetFile, transform, options);
306
- }
307
-
308
- async installFile(
309
- sourceFile: string,
310
- targetFile: string,
311
- transform: FileTransformFn,
312
- options: InstallOptions = {}
313
- ): Promise<void> {
314
- return installFile(sourceFile, targetFile, transform, options);
315
- }
316
- }
@@ -178,36 +178,3 @@ export function createMCPInstaller(target: Target): MCPInstaller {
178
178
  };
179
179
  }
180
180
 
181
- /**
182
- * @deprecated Use createMCPInstaller() for new code
183
- */
184
- export class MCPInstaller {
185
- private installer: ReturnType<typeof createMCPInstaller>;
186
-
187
- constructor(target: Target) {
188
- this.installer = createMCPInstaller(target);
189
- }
190
-
191
- async selectServers(options: { quiet?: boolean } = {}): Promise<MCPServerID[]> {
192
- return this.installer.selectServers(options);
193
- }
194
-
195
- async configureServers(
196
- selectedServers: MCPServerID[],
197
- options: { quiet?: boolean } = {}
198
- ): Promise<Record<MCPServerID, Record<string, string>>> {
199
- return this.installer.configureServers(selectedServers, options);
200
- }
201
-
202
- async installServers(
203
- selectedServers: MCPServerID[],
204
- serverConfigsMap: Record<MCPServerID, Record<string, string>>,
205
- options: { quiet?: boolean } = {}
206
- ): Promise<void> {
207
- return this.installer.installServers(selectedServers, serverConfigsMap, options);
208
- }
209
-
210
- async setupMCP(options: { quiet?: boolean; dryRun?: boolean } = {}): Promise<MCPInstallResult> {
211
- return this.installer.setupMCP(options);
212
- }
213
- }
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Sylphx Flow - Legacy CLI
4
- * Project initialization and development flow management
3
+ * Sylphx Flow CLI
4
+ * AI-powered development flow management
5
5
  */
6
6
 
7
7
  import { readFileSync } from 'node:fs';
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Auto-Upgrade Service
3
+ * Automatically checks and upgrades Flow and target CLI before each execution
4
+ */
5
+
6
+ import { exec } from 'node:child_process';
7
+ import { promisify } from 'node:util';
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import chalk from 'chalk';
11
+ import ora from 'ora';
12
+ import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
13
+ import { TargetInstaller } from './target-installer.js';
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ export interface UpgradeStatus {
18
+ flowNeedsUpgrade: boolean;
19
+ targetNeedsUpgrade: boolean;
20
+ flowVersion: { current: string; latest: string } | null;
21
+ targetVersion: { current: string; latest: string } | null;
22
+ }
23
+
24
+ export interface AutoUpgradeOptions {
25
+ verbose?: boolean;
26
+ skipFlow?: boolean;
27
+ skipTarget?: boolean;
28
+ }
29
+
30
+ export class AutoUpgrade {
31
+ private projectPath: string;
32
+ private options: AutoUpgradeOptions;
33
+ private targetInstaller: TargetInstaller;
34
+
35
+ constructor(projectPath: string = process.cwd(), options: AutoUpgradeOptions = {}) {
36
+ this.projectPath = projectPath;
37
+ this.options = options;
38
+ this.targetInstaller = new TargetInstaller(projectPath);
39
+ }
40
+
41
+ /**
42
+ * Check for available upgrades for Flow and target CLI
43
+ * @param targetId - Optional target CLI ID to check for upgrades
44
+ * @returns Upgrade status indicating what needs upgrading
45
+ */
46
+ async checkForUpgrades(targetId?: string): Promise<UpgradeStatus> {
47
+ const [flowVersion, targetVersion] = await Promise.all([
48
+ this.options.skipFlow ? null : this.checkFlowVersion(),
49
+ this.options.skipTarget || !targetId ? null : this.checkTargetVersion(targetId),
50
+ ]);
51
+
52
+ return {
53
+ flowNeedsUpgrade: !!flowVersion,
54
+ targetNeedsUpgrade: !!targetVersion,
55
+ flowVersion,
56
+ targetVersion,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Check Flow version
62
+ */
63
+ private async checkFlowVersion(): Promise<{ current: string; latest: string } | null> {
64
+ try {
65
+ // Get current version from package.json
66
+ const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
67
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
68
+ const currentVersion = packageJson.version;
69
+
70
+ // Get latest version from npm
71
+ const { stdout } = await execAsync('npm view @sylphx/flow version');
72
+ const latestVersion = stdout.trim();
73
+
74
+ if (currentVersion !== latestVersion) {
75
+ return { current: currentVersion, latest: latestVersion };
76
+ }
77
+
78
+ return null;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Check target CLI version
86
+ */
87
+ private async checkTargetVersion(
88
+ targetId: string
89
+ ): Promise<{ current: string; latest: string } | null> {
90
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
91
+ if (!installation) {
92
+ return null;
93
+ }
94
+
95
+ try {
96
+ // Get current version
97
+ const { stdout: currentOutput } = await execAsync(installation.checkCommand);
98
+ const currentMatch = currentOutput.match(/v?(\d+\.\d+\.\d+)/);
99
+ if (!currentMatch) {
100
+ return null;
101
+ }
102
+ const currentVersion = currentMatch[1];
103
+
104
+ // Get latest version from npm
105
+ const { stdout: latestOutput } = await execAsync(`npm view ${installation.package} version`);
106
+ const latestVersion = latestOutput.trim();
107
+
108
+ if (currentVersion !== latestVersion) {
109
+ return { current: currentVersion, latest: latestVersion };
110
+ }
111
+
112
+ return null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Upgrade Flow to latest version using detected package manager
120
+ * @returns True if upgrade successful, false otherwise
121
+ */
122
+ async upgradeFlow(): Promise<boolean> {
123
+ const packageManager = detectPackageManager(this.projectPath);
124
+ const spinner = ora('Upgrading Flow...').start();
125
+
126
+ try {
127
+ const upgradeCmd = getUpgradeCommand('@sylphx/flow', packageManager);
128
+ await execAsync(upgradeCmd);
129
+
130
+ spinner.succeed(chalk.green('✓ Flow upgraded to latest version'));
131
+ return true;
132
+ } catch (error) {
133
+ spinner.fail(chalk.red('✗ Flow upgrade failed'));
134
+
135
+ if (this.options.verbose) {
136
+ console.error(error);
137
+ }
138
+
139
+ return false;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Upgrade target CLI to latest version
145
+ * Tries built-in upgrade command first, falls back to package manager
146
+ * @param targetId - Target CLI ID to upgrade
147
+ * @returns True if upgrade successful, false otherwise
148
+ */
149
+ async upgradeTarget(targetId: string): Promise<boolean> {
150
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
151
+ if (!installation) {
152
+ return false;
153
+ }
154
+
155
+ const packageManager = detectPackageManager(this.projectPath);
156
+ const spinner = ora(`Upgrading ${installation.name}...`).start();
157
+
158
+ try {
159
+ // For Claude Code, use built-in update command if available
160
+ if (targetId === 'claude-code') {
161
+ try {
162
+ await execAsync('claude update');
163
+ spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
164
+ return true;
165
+ } catch {
166
+ // Fall back to npm upgrade
167
+ }
168
+ }
169
+
170
+ // For OpenCode, use built-in upgrade command if available
171
+ if (targetId === 'opencode') {
172
+ try {
173
+ await execAsync('opencode upgrade');
174
+ spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
175
+ return true;
176
+ } catch {
177
+ // Fall back to npm upgrade
178
+ }
179
+ }
180
+
181
+ // Fall back to npm/bun/pnpm/yarn upgrade
182
+ const upgradeCmd = getUpgradeCommand(installation.package, packageManager);
183
+ await execAsync(upgradeCmd);
184
+
185
+ spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
186
+ return true;
187
+ } catch (error) {
188
+ spinner.fail(chalk.red(`✗ ${installation.name} upgrade failed`));
189
+
190
+ if (this.options.verbose) {
191
+ console.error(error);
192
+ }
193
+
194
+ return false;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Run auto-upgrade check and upgrade if needed
200
+ * Shows upgrade status and performs upgrades automatically
201
+ * @param targetId - Optional target CLI ID to check and upgrade
202
+ */
203
+ async runAutoUpgrade(targetId?: string): Promise<void> {
204
+ console.log(chalk.cyan('🔄 Checking for updates...\n'));
205
+
206
+ const status = await this.checkForUpgrades(targetId);
207
+
208
+ // Show upgrade status
209
+ if (status.flowNeedsUpgrade && status.flowVersion) {
210
+ console.log(
211
+ chalk.yellow(
212
+ `📦 Flow update available: ${status.flowVersion.current} → ${status.flowVersion.latest}`
213
+ )
214
+ );
215
+ } else if (!this.options.skipFlow) {
216
+ console.log(chalk.green('✓ Flow is up to date'));
217
+ }
218
+
219
+ if (status.targetNeedsUpgrade && status.targetVersion && targetId) {
220
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
221
+ console.log(
222
+ chalk.yellow(
223
+ `📦 ${installation?.name} update available: ${status.targetVersion.current} → ${status.targetVersion.latest}`
224
+ )
225
+ );
226
+ } else if (!this.options.skipTarget && targetId) {
227
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
228
+ console.log(chalk.green(`✓ ${installation?.name} is up to date`));
229
+ }
230
+
231
+ // Perform upgrades if needed
232
+ if (status.flowNeedsUpgrade || status.targetNeedsUpgrade) {
233
+ console.log(chalk.cyan('\n📦 Installing updates...\n'));
234
+
235
+ if (status.flowNeedsUpgrade) {
236
+ await this.upgradeFlow();
237
+ }
238
+
239
+ if (status.targetNeedsUpgrade && targetId) {
240
+ await this.upgradeTarget(targetId);
241
+ }
242
+
243
+ console.log(chalk.green('\n✓ All tools upgraded\n'));
244
+ } else {
245
+ console.log();
246
+ }
247
+ }
248
+ }
@@ -10,7 +10,7 @@ import { existsSync } from 'node:fs';
10
10
 
11
11
  export interface GlobalSettings {
12
12
  version: string;
13
- defaultTarget?: 'claude-code' | 'opencode';
13
+ defaultTarget?: 'claude-code' | 'opencode' | 'cursor' | 'ask-every-time';
14
14
  defaultAgent?: string; // Default agent to use (e.g., 'coder', 'writer', 'reviewer', 'orchestrator')
15
15
  firstRun: boolean;
16
16
  lastUpdated: string;