@sylphx/flow 1.6.12 → 1.7.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 +32 -0
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @sylphx/flow
|
|
2
2
|
|
|
3
|
+
## 1.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add orphaned hooks detection and removal to sync command
|
|
8
|
+
|
|
9
|
+
The sync command now properly detects and prompts for removal of hooks that exist locally but are not in the configuration. This ensures full synchronization between local settings and the Flow configuration.
|
|
10
|
+
|
|
11
|
+
**New Features:**
|
|
12
|
+
|
|
13
|
+
- Detects orphaned hooks in `.claude/settings.json`
|
|
14
|
+
- Shows orphaned hooks in sync preview
|
|
15
|
+
- Allows users to select which orphaned hooks to remove
|
|
16
|
+
- Properly cleans up settings.json after removal
|
|
17
|
+
|
|
18
|
+
**Breaking Changes:**
|
|
19
|
+
|
|
20
|
+
- Internal API: `selectUnknownFilesToRemove()` now returns `SelectedToRemove` object instead of `string[]`
|
|
21
|
+
|
|
22
|
+
## 1.6.13
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- 746d576: Fix missing chalk import in claude-code target causing ReferenceError in dry-run mode
|
|
27
|
+
- ea6aa39: fix(sync): display hooks configuration in sync preview
|
|
28
|
+
|
|
29
|
+
When running `sylphx-flow --sync`, the sync preview now shows that hooks will be configured/updated. This makes it clear to users that hook settings are being synchronized along with other Flow templates.
|
|
30
|
+
|
|
31
|
+
Previously, hooks were being updated during sync but this wasn't visible in the sync preview output, leading to confusion about whether hooks were being synced.
|
|
32
|
+
|
|
33
|
+
- 6ea9757: Test repository link in Slack notification
|
|
34
|
+
|
|
3
35
|
## 1.6.12
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -380,7 +380,7 @@ async function executeSetupPhase(prompt: string | undefined, options: FlowOption
|
|
|
380
380
|
|
|
381
381
|
// Handle sync mode - delete template files first
|
|
382
382
|
if (options.sync && !options.dryRun) {
|
|
383
|
-
const { buildSyncManifest, showSyncPreview, selectUnknownFilesToRemove, showFinalSummary, confirmSync, executeSyncDelete, removeMCPServers } = await import('../utils/sync-utils.js');
|
|
383
|
+
const { buildSyncManifest, showSyncPreview, selectUnknownFilesToRemove, showFinalSummary, confirmSync, executeSyncDelete, removeMCPServers, removeHooks } = await import('../utils/sync-utils.js');
|
|
384
384
|
|
|
385
385
|
// Need target to build manifest
|
|
386
386
|
const targetId = await selectAndValidateTarget(initOptions);
|
|
@@ -396,7 +396,7 @@ async function executeSetupPhase(prompt: string | undefined, options: FlowOption
|
|
|
396
396
|
|
|
397
397
|
// Show preview
|
|
398
398
|
console.log(chalk.cyan.bold('━━━ 🔄 Synchronizing Files\n'));
|
|
399
|
-
showSyncPreview(manifest, process.cwd());
|
|
399
|
+
showSyncPreview(manifest, process.cwd(), target);
|
|
400
400
|
|
|
401
401
|
// Select unknown files to remove
|
|
402
402
|
const selectedUnknowns = await selectUnknownFilesToRemove(manifest);
|
|
@@ -415,20 +415,27 @@ async function executeSetupPhase(prompt: string | undefined, options: FlowOption
|
|
|
415
415
|
const { templates, unknowns } = await executeSyncDelete(manifest, selectedUnknowns);
|
|
416
416
|
|
|
417
417
|
// Remove MCP servers
|
|
418
|
-
const mcpServersToRemove = selectedUnknowns.filter(s => !s.includes('/'));
|
|
419
418
|
let mcpRemoved = 0;
|
|
420
|
-
if (
|
|
421
|
-
mcpRemoved = await removeMCPServers(process.cwd(),
|
|
419
|
+
if (selectedUnknowns.mcpServers.length > 0) {
|
|
420
|
+
mcpRemoved = await removeMCPServers(process.cwd(), selectedUnknowns.mcpServers);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Remove hooks
|
|
424
|
+
let hooksRemoved = 0;
|
|
425
|
+
if (selectedUnknowns.hooks.length > 0) {
|
|
426
|
+
hooksRemoved = await removeHooks(process.cwd(), selectedUnknowns.hooks);
|
|
422
427
|
}
|
|
423
428
|
|
|
424
429
|
// Summary
|
|
425
430
|
console.log(chalk.green(`\n✓ Synced ${templates} templates`));
|
|
426
|
-
|
|
427
|
-
|
|
431
|
+
const totalRemoved = unknowns + mcpRemoved + hooksRemoved;
|
|
432
|
+
if (totalRemoved > 0) {
|
|
433
|
+
console.log(chalk.green(`✓ Removed ${totalRemoved} items`));
|
|
428
434
|
}
|
|
429
|
-
const
|
|
435
|
+
const totalSelected = selectedUnknowns.files.length + selectedUnknowns.mcpServers.length + selectedUnknowns.hooks.length;
|
|
436
|
+
const preserved = manifest.agents.unknown.length + manifest.slashCommands.unknown.length + manifest.rules.unknown.length + manifest.mcpServers.notInRegistry.length + manifest.hooks.orphaned.length - totalSelected;
|
|
430
437
|
if (preserved > 0) {
|
|
431
|
-
console.log(chalk.green(`✓ Preserved ${preserved} custom
|
|
438
|
+
console.log(chalk.green(`✓ Preserved ${preserved} custom items`));
|
|
432
439
|
}
|
|
433
440
|
console.log('');
|
|
434
441
|
} else if (!options.sync) {
|
|
@@ -725,7 +732,7 @@ async function executeFlowOnce(prompt: string | undefined, options: FlowOptions)
|
|
|
725
732
|
|
|
726
733
|
// Handle sync mode - delete template files first
|
|
727
734
|
if (options.sync && !options.dryRun) {
|
|
728
|
-
const { buildSyncManifest, showSyncPreview, selectUnknownFilesToRemove, showFinalSummary, confirmSync, executeSyncDelete, removeMCPServers } = await import('../utils/sync-utils.js');
|
|
735
|
+
const { buildSyncManifest, showSyncPreview, selectUnknownFilesToRemove, showFinalSummary, confirmSync, executeSyncDelete, removeMCPServers, removeHooks } = await import('../utils/sync-utils.js');
|
|
729
736
|
|
|
730
737
|
// Need target to build manifest
|
|
731
738
|
const targetId = await selectAndValidateTarget(initOptions);
|
|
@@ -741,7 +748,7 @@ async function executeFlowOnce(prompt: string | undefined, options: FlowOptions)
|
|
|
741
748
|
|
|
742
749
|
// Show preview
|
|
743
750
|
console.log(chalk.cyan.bold('━━━ 🔄 Synchronizing Files\n'));
|
|
744
|
-
showSyncPreview(manifest, process.cwd());
|
|
751
|
+
showSyncPreview(manifest, process.cwd(), target);
|
|
745
752
|
|
|
746
753
|
// Select unknown files to remove
|
|
747
754
|
const selectedUnknowns = await selectUnknownFilesToRemove(manifest);
|
|
@@ -760,20 +767,27 @@ async function executeFlowOnce(prompt: string | undefined, options: FlowOptions)
|
|
|
760
767
|
const { templates, unknowns } = await executeSyncDelete(manifest, selectedUnknowns);
|
|
761
768
|
|
|
762
769
|
// Remove MCP servers
|
|
763
|
-
const mcpServersToRemove = selectedUnknowns.filter(s => !s.includes('/'));
|
|
764
770
|
let mcpRemoved = 0;
|
|
765
|
-
if (
|
|
766
|
-
mcpRemoved = await removeMCPServers(process.cwd(),
|
|
771
|
+
if (selectedUnknowns.mcpServers.length > 0) {
|
|
772
|
+
mcpRemoved = await removeMCPServers(process.cwd(), selectedUnknowns.mcpServers);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Remove hooks
|
|
776
|
+
let hooksRemoved = 0;
|
|
777
|
+
if (selectedUnknowns.hooks.length > 0) {
|
|
778
|
+
hooksRemoved = await removeHooks(process.cwd(), selectedUnknowns.hooks);
|
|
767
779
|
}
|
|
768
780
|
|
|
769
781
|
// Summary
|
|
770
782
|
console.log(chalk.green(`\n✓ Synced ${templates} templates`));
|
|
771
|
-
|
|
772
|
-
|
|
783
|
+
const totalRemoved = unknowns + mcpRemoved + hooksRemoved;
|
|
784
|
+
if (totalRemoved > 0) {
|
|
785
|
+
console.log(chalk.green(`✓ Removed ${totalRemoved} items`));
|
|
773
786
|
}
|
|
774
|
-
const
|
|
787
|
+
const totalSelected = selectedUnknowns.files.length + selectedUnknowns.mcpServers.length + selectedUnknowns.hooks.length;
|
|
788
|
+
const preserved = manifest.agents.unknown.length + manifest.slashCommands.unknown.length + manifest.rules.unknown.length + manifest.mcpServers.notInRegistry.length + manifest.hooks.orphaned.length - totalSelected;
|
|
775
789
|
if (preserved > 0) {
|
|
776
|
-
console.log(chalk.green(`✓ Preserved ${preserved} custom
|
|
790
|
+
console.log(chalk.green(`✓ Preserved ${preserved} custom items`));
|
|
777
791
|
}
|
|
778
792
|
console.log('');
|
|
779
793
|
} else {
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import fsPromises from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
5
6
|
import { FileInstaller } from '../core/installers/file-installer.js';
|
|
6
7
|
import { MCPInstaller } from '../core/installers/mcp-installer.js';
|
|
7
8
|
import type { AgentMetadata } from '../types/target-config.types.js';
|
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
|
+
}
|