@sylphx/flow 1.6.13 → 1.8.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 +52 -0
- package/assets/agents/coder.md +72 -119
- package/assets/agents/orchestrator.md +26 -90
- package/assets/agents/reviewer.md +76 -47
- package/assets/agents/writer.md +82 -63
- package/assets/rules/code-standards.md +9 -33
- package/assets/rules/core.md +49 -58
- package/package.json +1 -1
- package/src/commands/flow-command.ts +32 -18
- package/src/targets/claude-code.ts +1 -0
- package/src/utils/sync-utils.ts +158 -17
package/src/utils/sync-utils.ts
CHANGED
|
@@ -47,6 +47,10 @@ interface SyncManifest {
|
|
|
47
47
|
inRegistry: string[];
|
|
48
48
|
notInRegistry: string[];
|
|
49
49
|
};
|
|
50
|
+
hooks: {
|
|
51
|
+
inConfig: string[]; // Hooks from config (will be synced)
|
|
52
|
+
orphaned: string[]; // Hooks that exist locally but not in config (ask user)
|
|
53
|
+
};
|
|
50
54
|
preserve: string[];
|
|
51
55
|
}
|
|
52
56
|
|
|
@@ -82,6 +86,7 @@ export async function buildSyncManifest(cwd: string, target: Target): Promise<Sy
|
|
|
82
86
|
slashCommands: { inFlow: [], unknown: [], missing: [] },
|
|
83
87
|
rules: { inFlow: [], unknown: [], missing: [] },
|
|
84
88
|
mcpServers: { inRegistry: [], notInRegistry: [] },
|
|
89
|
+
hooks: { inConfig: [], orphaned: [] },
|
|
85
90
|
preserve: [],
|
|
86
91
|
};
|
|
87
92
|
|
|
@@ -158,6 +163,34 @@ export async function buildSyncManifest(cwd: string, target: Target): Promise<Sy
|
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
165
|
|
|
166
|
+
// Hooks - detect orphaned hooks (only for targets that support hooks)
|
|
167
|
+
if (target.setupHooks) {
|
|
168
|
+
const settingsPath = path.join(cwd, '.claude', 'settings.json');
|
|
169
|
+
if (fs.existsSync(settingsPath)) {
|
|
170
|
+
try {
|
|
171
|
+
const content = await fs.promises.readFile(settingsPath, 'utf-8');
|
|
172
|
+
const settings = JSON.parse(content);
|
|
173
|
+
|
|
174
|
+
if (settings.hooks) {
|
|
175
|
+
const existingHookTypes = Object.keys(settings.hooks);
|
|
176
|
+
|
|
177
|
+
// Expected hooks from config (currently only Notification)
|
|
178
|
+
// In the future, this could be read from Flow config
|
|
179
|
+
const expectedHookTypes = ['Notification'];
|
|
180
|
+
|
|
181
|
+
manifest.hooks.inConfig = existingHookTypes.filter(type =>
|
|
182
|
+
expectedHookTypes.includes(type)
|
|
183
|
+
);
|
|
184
|
+
manifest.hooks.orphaned = existingHookTypes.filter(type =>
|
|
185
|
+
!expectedHookTypes.includes(type)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.warn(chalk.yellow('⚠ Failed to read settings.json'));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
161
194
|
// Files to preserve
|
|
162
195
|
manifest.preserve = [
|
|
163
196
|
'.sylphx-flow/',
|
|
@@ -173,7 +206,7 @@ export async function buildSyncManifest(cwd: string, target: Target): Promise<Sy
|
|
|
173
206
|
/**
|
|
174
207
|
* Show sync preview with categorization
|
|
175
208
|
*/
|
|
176
|
-
export function showSyncPreview(manifest: SyncManifest, cwd: string): void {
|
|
209
|
+
export function showSyncPreview(manifest: SyncManifest, cwd: string, target?: Target): void {
|
|
177
210
|
console.log(chalk.cyan.bold('━━━ 🔄 Sync Preview\n'));
|
|
178
211
|
|
|
179
212
|
// Will sync section
|
|
@@ -183,7 +216,9 @@ export function showSyncPreview(manifest: SyncManifest, cwd: string): void {
|
|
|
183
216
|
manifest.rules.inFlow.length > 0 ||
|
|
184
217
|
manifest.mcpServers.inRegistry.length > 0;
|
|
185
218
|
|
|
186
|
-
|
|
219
|
+
const hasHooksSupport = target?.setupHooks !== undefined;
|
|
220
|
+
|
|
221
|
+
if (hasFlowFiles || hasHooksSupport) {
|
|
187
222
|
console.log(chalk.green('Will sync (delete + reinstall):\n'));
|
|
188
223
|
|
|
189
224
|
if (manifest.agents.inFlow.length > 0) {
|
|
@@ -217,6 +252,20 @@ export function showSyncPreview(manifest: SyncManifest, cwd: string): void {
|
|
|
217
252
|
});
|
|
218
253
|
console.log('');
|
|
219
254
|
}
|
|
255
|
+
|
|
256
|
+
// Show hooks if target supports them
|
|
257
|
+
if (hasHooksSupport) {
|
|
258
|
+
console.log(chalk.dim(' Settings:'));
|
|
259
|
+
|
|
260
|
+
if (manifest.hooks.inConfig.length > 0) {
|
|
261
|
+
manifest.hooks.inConfig.forEach((hookType) => {
|
|
262
|
+
console.log(chalk.dim(` ✓ ${hookType} hook`));
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
console.log(chalk.dim(` ✓ Hooks configuration`));
|
|
266
|
+
}
|
|
267
|
+
console.log('');
|
|
268
|
+
}
|
|
220
269
|
}
|
|
221
270
|
|
|
222
271
|
// Missing templates section (will be installed)
|
|
@@ -258,7 +307,8 @@ export function showSyncPreview(manifest: SyncManifest, cwd: string): void {
|
|
|
258
307
|
manifest.agents.unknown.length > 0 ||
|
|
259
308
|
manifest.slashCommands.unknown.length > 0 ||
|
|
260
309
|
manifest.rules.unknown.length > 0 ||
|
|
261
|
-
manifest.mcpServers.notInRegistry.length > 0
|
|
310
|
+
manifest.mcpServers.notInRegistry.length > 0 ||
|
|
311
|
+
manifest.hooks.orphaned.length > 0;
|
|
262
312
|
|
|
263
313
|
if (hasUnknownFiles) {
|
|
264
314
|
console.log(chalk.yellow('Unknown files (not in Flow templates):\n'));
|
|
@@ -294,6 +344,14 @@ export function showSyncPreview(manifest: SyncManifest, cwd: string): void {
|
|
|
294
344
|
});
|
|
295
345
|
console.log('');
|
|
296
346
|
}
|
|
347
|
+
|
|
348
|
+
if (manifest.hooks.orphaned.length > 0) {
|
|
349
|
+
console.log(chalk.dim(' Hooks:'));
|
|
350
|
+
manifest.hooks.orphaned.forEach((hookType) => {
|
|
351
|
+
console.log(chalk.dim(` ? ${hookType} hook`));
|
|
352
|
+
});
|
|
353
|
+
console.log('');
|
|
354
|
+
}
|
|
297
355
|
} else {
|
|
298
356
|
console.log(chalk.green('✓ No unknown files\n'));
|
|
299
357
|
}
|
|
@@ -309,10 +367,19 @@ export function showSyncPreview(manifest: SyncManifest, cwd: string): void {
|
|
|
309
367
|
console.log('');
|
|
310
368
|
}
|
|
311
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Selected items to remove with type information
|
|
372
|
+
*/
|
|
373
|
+
export interface SelectedToRemove {
|
|
374
|
+
files: string[];
|
|
375
|
+
mcpServers: string[];
|
|
376
|
+
hooks: string[];
|
|
377
|
+
}
|
|
378
|
+
|
|
312
379
|
/**
|
|
313
380
|
* Select unknown files to remove
|
|
314
381
|
*/
|
|
315
|
-
export async function selectUnknownFilesToRemove(manifest: SyncManifest): Promise<
|
|
382
|
+
export async function selectUnknownFilesToRemove(manifest: SyncManifest): Promise<SelectedToRemove> {
|
|
316
383
|
const unknownFiles: Array<{ name: string; value: string; type: string }> = [];
|
|
317
384
|
|
|
318
385
|
// Collect all unknown files
|
|
@@ -348,8 +415,16 @@ export async function selectUnknownFilesToRemove(manifest: SyncManifest): Promis
|
|
|
348
415
|
});
|
|
349
416
|
});
|
|
350
417
|
|
|
418
|
+
manifest.hooks.orphaned.forEach((hookType) => {
|
|
419
|
+
unknownFiles.push({
|
|
420
|
+
name: `Hook: ${hookType}`,
|
|
421
|
+
value: hookType,
|
|
422
|
+
type: 'hook',
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
351
426
|
if (unknownFiles.length === 0) {
|
|
352
|
-
return [];
|
|
427
|
+
return { files: [], mcpServers: [], hooks: [] };
|
|
353
428
|
}
|
|
354
429
|
|
|
355
430
|
const { default: inquirer } = await import('inquirer');
|
|
@@ -362,7 +437,27 @@ export async function selectUnknownFilesToRemove(manifest: SyncManifest): Promis
|
|
|
362
437
|
},
|
|
363
438
|
]);
|
|
364
439
|
|
|
365
|
-
|
|
440
|
+
// Categorize selected items by type
|
|
441
|
+
const selectedSet = new Set(selected);
|
|
442
|
+
const result: SelectedToRemove = {
|
|
443
|
+
files: [],
|
|
444
|
+
mcpServers: [],
|
|
445
|
+
hooks: [],
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
for (const item of unknownFiles) {
|
|
449
|
+
if (selectedSet.has(item.value)) {
|
|
450
|
+
if (item.type === 'mcp') {
|
|
451
|
+
result.mcpServers.push(item.value);
|
|
452
|
+
} else if (item.type === 'hook') {
|
|
453
|
+
result.hooks.push(item.value);
|
|
454
|
+
} else {
|
|
455
|
+
result.files.push(item.value);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return result;
|
|
366
461
|
}
|
|
367
462
|
|
|
368
463
|
/**
|
|
@@ -370,7 +465,7 @@ export async function selectUnknownFilesToRemove(manifest: SyncManifest): Promis
|
|
|
370
465
|
*/
|
|
371
466
|
export function showFinalSummary(
|
|
372
467
|
manifest: SyncManifest,
|
|
373
|
-
selectedUnknowns:
|
|
468
|
+
selectedUnknowns: SelectedToRemove
|
|
374
469
|
): void {
|
|
375
470
|
console.log(chalk.cyan.bold('\n━━━ 📋 Final Summary\n'));
|
|
376
471
|
|
|
@@ -395,22 +490,30 @@ export function showFinalSummary(
|
|
|
395
490
|
}
|
|
396
491
|
|
|
397
492
|
// Will remove (selected unknowns)
|
|
398
|
-
|
|
493
|
+
const totalToRemove = selectedUnknowns.files.length + selectedUnknowns.mcpServers.length + selectedUnknowns.hooks.length;
|
|
494
|
+
if (totalToRemove > 0) {
|
|
399
495
|
console.log(chalk.red('Remove (selected):\n'));
|
|
400
|
-
selectedUnknowns.forEach((file) => {
|
|
401
|
-
|
|
402
|
-
|
|
496
|
+
selectedUnknowns.files.forEach((file) => {
|
|
497
|
+
console.log(chalk.dim(` - ${path.basename(file)}`));
|
|
498
|
+
});
|
|
499
|
+
selectedUnknowns.mcpServers.forEach((server) => {
|
|
500
|
+
console.log(chalk.dim(` - MCP: ${server}`));
|
|
501
|
+
});
|
|
502
|
+
selectedUnknowns.hooks.forEach((hook) => {
|
|
503
|
+
console.log(chalk.dim(` - Hook: ${hook}`));
|
|
403
504
|
});
|
|
404
505
|
console.log('');
|
|
405
506
|
}
|
|
406
507
|
|
|
407
508
|
// Will preserve
|
|
509
|
+
const allSelected = [...selectedUnknowns.files, ...selectedUnknowns.mcpServers, ...selectedUnknowns.hooks];
|
|
408
510
|
const preservedUnknowns = [
|
|
409
511
|
...manifest.agents.unknown,
|
|
410
512
|
...manifest.slashCommands.unknown,
|
|
411
513
|
...manifest.rules.unknown,
|
|
412
514
|
...manifest.mcpServers.notInRegistry,
|
|
413
|
-
|
|
515
|
+
...manifest.hooks.orphaned,
|
|
516
|
+
].filter((file) => !allSelected.includes(file));
|
|
414
517
|
|
|
415
518
|
if (preservedUnknowns.length > 0) {
|
|
416
519
|
console.log(chalk.green('Preserve:\n'));
|
|
@@ -427,7 +530,7 @@ export function showFinalSummary(
|
|
|
427
530
|
*/
|
|
428
531
|
export async function executeSyncDelete(
|
|
429
532
|
manifest: SyncManifest,
|
|
430
|
-
selectedUnknowns:
|
|
533
|
+
selectedUnknowns: SelectedToRemove
|
|
431
534
|
): Promise<{ templates: number; unknowns: number }> {
|
|
432
535
|
const flowFiles = [
|
|
433
536
|
...manifest.agents.inFlow,
|
|
@@ -454,10 +557,7 @@ export async function executeSyncDelete(
|
|
|
454
557
|
}
|
|
455
558
|
|
|
456
559
|
// Delete selected unknown files
|
|
457
|
-
for (const file of selectedUnknowns) {
|
|
458
|
-
// Skip MCP servers (handled separately)
|
|
459
|
-
if (!file.includes('/')) continue;
|
|
460
|
-
|
|
560
|
+
for (const file of selectedUnknowns.files) {
|
|
461
561
|
try {
|
|
462
562
|
await fs.promises.unlink(file);
|
|
463
563
|
console.log(chalk.dim(` ✓ Deleted: ${path.basename(file)}`));
|
|
@@ -530,3 +630,44 @@ export async function removeMCPServers(cwd: string, serversToRemove: string[]):
|
|
|
530
630
|
return 0;
|
|
531
631
|
}
|
|
532
632
|
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Remove hooks from .claude/settings.json
|
|
636
|
+
*/
|
|
637
|
+
export async function removeHooks(cwd: string, hooksToRemove: string[]): Promise<number> {
|
|
638
|
+
if (hooksToRemove.length === 0) {
|
|
639
|
+
return 0;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const settingsPath = path.join(cwd, '.claude', 'settings.json');
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
const content = await fs.promises.readFile(settingsPath, 'utf-8');
|
|
646
|
+
const settings = JSON.parse(content);
|
|
647
|
+
|
|
648
|
+
if (!settings.hooks) {
|
|
649
|
+
return 0;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
let removedCount = 0;
|
|
653
|
+
for (const hookType of hooksToRemove) {
|
|
654
|
+
if (settings.hooks[hookType]) {
|
|
655
|
+
delete settings.hooks[hookType];
|
|
656
|
+
console.log(chalk.dim(` ✓ Removed Hook: ${hookType}`));
|
|
657
|
+
removedCount++;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Write back
|
|
662
|
+
await fs.promises.writeFile(
|
|
663
|
+
settingsPath,
|
|
664
|
+
JSON.stringify(settings, null, 2) + '\n',
|
|
665
|
+
'utf-8'
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
return removedCount;
|
|
669
|
+
} catch (error) {
|
|
670
|
+
console.warn(chalk.yellow('⚠ Failed to update settings.json'));
|
|
671
|
+
return 0;
|
|
672
|
+
}
|
|
673
|
+
}
|