agileflow 2.89.1 → 2.89.3

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.
@@ -0,0 +1,811 @@
1
+ /**
2
+ * configure-features.js - Feature enable/disable handlers for agileflow-configure
3
+ *
4
+ * Extracted from agileflow-configure.js (US-0094)
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const {
10
+ c,
11
+ log,
12
+ success,
13
+ warn,
14
+ error,
15
+ info,
16
+ header,
17
+ ensureDir,
18
+ readJSON,
19
+ writeJSON,
20
+ updateGitignore,
21
+ } = require('./configure-utils');
22
+
23
+ // ============================================================================
24
+ // CONFIGURATION CONSTANTS
25
+ // ============================================================================
26
+
27
+ const FEATURES = {
28
+ sessionstart: { hook: 'SessionStart', script: 'agileflow-welcome.js', type: 'node' },
29
+ precompact: { hook: 'PreCompact', script: 'precompact-context.sh', type: 'bash' },
30
+ ralphloop: { hook: 'Stop', script: 'ralph-loop.js', type: 'node' },
31
+ selfimprove: { hook: 'Stop', script: 'auto-self-improve.js', type: 'node' },
32
+ archival: { script: 'archive-completed-stories.sh', requiresHook: 'sessionstart' },
33
+ statusline: { script: 'agileflow-statusline.sh' },
34
+ autoupdate: { metadataOnly: true },
35
+ damagecontrol: {
36
+ preToolUseHooks: true,
37
+ scripts: ['damage-control-bash.js', 'damage-control-edit.js', 'damage-control-write.js'],
38
+ patternsFile: 'damage-control-patterns.yaml',
39
+ },
40
+ askuserquestion: { metadataOnly: true },
41
+ };
42
+
43
+ const PROFILES = {
44
+ full: {
45
+ description: 'All features enabled (including experimental Stop hooks)',
46
+ enable: [
47
+ 'sessionstart',
48
+ 'precompact',
49
+ 'archival',
50
+ 'statusline',
51
+ 'ralphloop',
52
+ 'selfimprove',
53
+ 'askuserquestion',
54
+ ],
55
+ archivalDays: 30,
56
+ },
57
+ basic: {
58
+ description: 'Essential hooks + archival (SessionStart + PreCompact + Archival)',
59
+ enable: ['sessionstart', 'precompact', 'archival', 'askuserquestion'],
60
+ disable: ['statusline', 'ralphloop', 'selfimprove'],
61
+ archivalDays: 30,
62
+ },
63
+ minimal: {
64
+ description: 'SessionStart + archival only',
65
+ enable: ['sessionstart', 'archival'],
66
+ disable: ['precompact', 'statusline', 'ralphloop', 'selfimprove', 'askuserquestion'],
67
+ archivalDays: 30,
68
+ },
69
+ none: {
70
+ description: 'Disable all AgileFlow features',
71
+ disable: [
72
+ 'sessionstart',
73
+ 'precompact',
74
+ 'archival',
75
+ 'statusline',
76
+ 'ralphloop',
77
+ 'selfimprove',
78
+ 'askuserquestion',
79
+ ],
80
+ },
81
+ };
82
+
83
+ const STATUSLINE_COMPONENTS = [
84
+ 'agileflow',
85
+ 'model',
86
+ 'story',
87
+ 'epic',
88
+ 'wip',
89
+ 'context',
90
+ 'cost',
91
+ 'git',
92
+ ];
93
+
94
+ // Scripts directory
95
+ const SCRIPTS_DIR = path.join(process.cwd(), '.agileflow', 'scripts');
96
+
97
+ // ============================================================================
98
+ // HELPER FUNCTIONS
99
+ // ============================================================================
100
+
101
+ const scriptExists = scriptName => fs.existsSync(path.join(SCRIPTS_DIR, scriptName));
102
+ const getScriptPath = scriptName => `.agileflow/scripts/${scriptName}`;
103
+
104
+ // ============================================================================
105
+ // METADATA MANAGEMENT
106
+ // ============================================================================
107
+
108
+ /**
109
+ * Update metadata file with provided updates
110
+ * @param {object} updates - Updates to apply (archival, features, updates)
111
+ * @param {string} version - Current version string
112
+ */
113
+ function updateMetadata(updates, version) {
114
+ const metaPath = 'docs/00-meta/agileflow-metadata.json';
115
+
116
+ if (!fs.existsSync(metaPath)) {
117
+ ensureDir('docs/00-meta');
118
+ writeJSON(metaPath, { version, created: new Date().toISOString() });
119
+ }
120
+
121
+ const meta = readJSON(metaPath) || {};
122
+
123
+ // Deep merge
124
+ if (updates.archival) {
125
+ meta.archival = { ...meta.archival, ...updates.archival };
126
+ }
127
+ if (updates.features) {
128
+ meta.features = meta.features || {};
129
+ Object.entries(updates.features).forEach(([key, value]) => {
130
+ meta.features[key] = { ...meta.features[key], ...value };
131
+ });
132
+ }
133
+ if (updates.updates) {
134
+ meta.updates = { ...meta.updates, ...updates.updates };
135
+ }
136
+
137
+ meta.version = version;
138
+ meta.updated = new Date().toISOString();
139
+
140
+ writeJSON(metaPath, meta);
141
+ }
142
+
143
+ // ============================================================================
144
+ // ENABLE FEATURE
145
+ // ============================================================================
146
+
147
+ /**
148
+ * Enable a feature
149
+ * @param {string} feature - Feature name
150
+ * @param {object} options - Options (archivalDays, mode, protectionLevel, isUpgrade)
151
+ * @param {string} version - Current version string
152
+ * @returns {boolean} Success
153
+ */
154
+ function enableFeature(feature, options = {}, version) {
155
+ const config = FEATURES[feature];
156
+ if (!config) {
157
+ error(`Unknown feature: ${feature}`);
158
+ return false;
159
+ }
160
+
161
+ ensureDir('.claude');
162
+
163
+ const settings = readJSON('.claude/settings.json') || {};
164
+ settings.hooks = settings.hooks || {};
165
+ settings.permissions = settings.permissions || { allow: [], deny: [], ask: [] };
166
+
167
+ // Handle hook-based features
168
+ if (config.hook) {
169
+ if (!enableHookFeature(feature, config, settings, version)) {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ // Handle archival
175
+ if (feature === 'archival') {
176
+ if (!enableArchival(settings, options, version)) {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ // Handle statusLine
182
+ if (feature === 'statusline') {
183
+ if (!enableStatusLine(settings, version)) {
184
+ return false;
185
+ }
186
+ }
187
+
188
+ // Handle autoupdate (metadata only)
189
+ if (feature === 'autoupdate') {
190
+ updateMetadata({ updates: { autoUpdate: true, showChangelog: true } }, version);
191
+ success('Auto-update enabled');
192
+ info('AgileFlow will check for updates every session and update automatically');
193
+ return true;
194
+ }
195
+
196
+ // Handle askuserquestion (metadata only)
197
+ if (feature === 'askuserquestion') {
198
+ const mode = options.mode || 'all';
199
+ updateMetadata(
200
+ {
201
+ features: {
202
+ askUserQuestion: {
203
+ enabled: true,
204
+ mode,
205
+ version,
206
+ at: new Date().toISOString(),
207
+ },
208
+ },
209
+ },
210
+ version
211
+ );
212
+ success(`AskUserQuestion enabled (mode: ${mode})`);
213
+ info('All commands will end with AskUserQuestion tool for guided interaction');
214
+ return true;
215
+ }
216
+
217
+ // Handle damage control
218
+ if (feature === 'damagecontrol') {
219
+ return enableDamageControl(settings, options, version);
220
+ }
221
+
222
+ writeJSON('.claude/settings.json', settings);
223
+ updateMetadata(
224
+ { features: { [feature]: { enabled: true, version, at: new Date().toISOString() } } },
225
+ version
226
+ );
227
+ updateGitignore();
228
+
229
+ return true;
230
+ }
231
+
232
+ /**
233
+ * Enable a hook-based feature
234
+ */
235
+ function enableHookFeature(feature, config, settings, version) {
236
+ const scriptPath = getScriptPath(config.script);
237
+
238
+ if (!scriptExists(config.script)) {
239
+ error(`Script not found: ${scriptPath}`);
240
+ info('Run "npx agileflow update" to reinstall scripts');
241
+ return false;
242
+ }
243
+
244
+ const absoluteScriptPath = path.join(process.cwd(), scriptPath);
245
+ const isStopHook = config.hook === 'Stop';
246
+ const command =
247
+ config.type === 'node'
248
+ ? `node ${absoluteScriptPath}${isStopHook ? ' 2>/dev/null || true' : ''}`
249
+ : `bash ${absoluteScriptPath}${isStopHook ? ' 2>/dev/null || true' : ''}`;
250
+
251
+ if (isStopHook) {
252
+ // Stop hooks stack - add to existing
253
+ if (!settings.hooks.Stop) {
254
+ settings.hooks.Stop = [{ matcher: '', hooks: [] }];
255
+ } else if (!Array.isArray(settings.hooks.Stop) || settings.hooks.Stop.length === 0) {
256
+ settings.hooks.Stop = [{ matcher: '', hooks: [] }];
257
+ } else if (!settings.hooks.Stop[0].hooks) {
258
+ settings.hooks.Stop[0].hooks = [];
259
+ }
260
+
261
+ const hasHook = settings.hooks.Stop[0].hooks.some(h => h.command?.includes(config.script));
262
+ if (!hasHook) {
263
+ settings.hooks.Stop[0].hooks.push({ type: 'command', command });
264
+ success(`Stop hook added (${config.script})`);
265
+ } else {
266
+ info(`${feature} already enabled`);
267
+ }
268
+ } else {
269
+ // Other hooks replace entirely
270
+ settings.hooks[config.hook] = [{ matcher: '', hooks: [{ type: 'command', command }] }];
271
+ success(`${config.hook} hook enabled (${config.script})`);
272
+ }
273
+
274
+ return true;
275
+ }
276
+
277
+ /**
278
+ * Enable archival feature
279
+ */
280
+ function enableArchival(settings, options, version) {
281
+ const days = options.archivalDays || 30;
282
+ const scriptPath = getScriptPath('archive-completed-stories.sh');
283
+
284
+ if (!scriptExists('archive-completed-stories.sh')) {
285
+ error(`Script not found: ${scriptPath}`);
286
+ info('Run "npx agileflow update" to reinstall scripts');
287
+ return false;
288
+ }
289
+
290
+ const absoluteScriptPath = path.join(process.cwd(), scriptPath);
291
+ if (settings.hooks.SessionStart?.[0]?.hooks) {
292
+ const hasArchival = settings.hooks.SessionStart[0].hooks.some(h =>
293
+ h.command?.includes('archive-completed-stories')
294
+ );
295
+ if (!hasArchival) {
296
+ settings.hooks.SessionStart[0].hooks.push({
297
+ type: 'command',
298
+ command: `bash ${absoluteScriptPath} --quiet`,
299
+ });
300
+ }
301
+ }
302
+
303
+ updateMetadata({ archival: { enabled: true, threshold_days: days } }, version);
304
+ success(`Archival enabled (${days} days)`);
305
+ return true;
306
+ }
307
+
308
+ /**
309
+ * Enable status line feature
310
+ */
311
+ function enableStatusLine(settings, version) {
312
+ const scriptPath = getScriptPath('agileflow-statusline.sh');
313
+
314
+ if (!scriptExists('agileflow-statusline.sh')) {
315
+ error(`Script not found: ${scriptPath}`);
316
+ info('Run "npx agileflow update" to reinstall scripts');
317
+ return false;
318
+ }
319
+
320
+ const absoluteScriptPath = path.join(process.cwd(), scriptPath);
321
+ settings.statusLine = {
322
+ type: 'command',
323
+ command: `bash ${absoluteScriptPath}`,
324
+ padding: 0,
325
+ };
326
+ success('Status line enabled');
327
+ return true;
328
+ }
329
+
330
+ /**
331
+ * Enable damage control feature
332
+ */
333
+ function enableDamageControl(settings, options, version) {
334
+ const level = options.protectionLevel || 'standard';
335
+
336
+ // Verify all required scripts exist
337
+ const requiredScripts = [
338
+ 'damage-control-bash.js',
339
+ 'damage-control-edit.js',
340
+ 'damage-control-write.js',
341
+ ];
342
+ for (const script of requiredScripts) {
343
+ if (!scriptExists(script)) {
344
+ error(`Script not found: ${getScriptPath(script)}`);
345
+ info('Run "npx agileflow update" to reinstall scripts');
346
+ return false;
347
+ }
348
+ }
349
+
350
+ // Deploy patterns file if not exists
351
+ const patternsDir = path.join(process.cwd(), '.agileflow', 'config');
352
+ const patternsDest = path.join(patternsDir, 'damage-control-patterns.yaml');
353
+ if (!fs.existsSync(patternsDest)) {
354
+ ensureDir(patternsDir);
355
+ const templatePath = path.join(
356
+ process.cwd(),
357
+ '.agileflow',
358
+ 'templates',
359
+ 'damage-control-patterns.yaml'
360
+ );
361
+ if (fs.existsSync(templatePath)) {
362
+ fs.copyFileSync(templatePath, patternsDest);
363
+ success('Deployed damage control patterns');
364
+ } else {
365
+ warn('No patterns template found - hooks will use defaults');
366
+ }
367
+ }
368
+
369
+ // Initialize PreToolUse array
370
+ if (!settings.hooks.PreToolUse) {
371
+ settings.hooks.PreToolUse = [];
372
+ }
373
+
374
+ const addPreToolUseHook = (matcher, scriptName) => {
375
+ const scriptFullPath = path.join(process.cwd(), '.agileflow', 'scripts', scriptName);
376
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(h => h.matcher !== matcher);
377
+ settings.hooks.PreToolUse.push({
378
+ matcher,
379
+ hooks: [{ type: 'command', command: `node ${scriptFullPath}`, timeout: 5 }],
380
+ });
381
+ };
382
+
383
+ addPreToolUseHook('Bash', 'damage-control-bash.js');
384
+ addPreToolUseHook('Edit', 'damage-control-edit.js');
385
+ addPreToolUseHook('Write', 'damage-control-write.js');
386
+
387
+ success('Damage control PreToolUse hooks enabled');
388
+
389
+ updateMetadata(
390
+ {
391
+ features: {
392
+ damagecontrol: {
393
+ enabled: true,
394
+ protectionLevel: level,
395
+ version,
396
+ at: new Date().toISOString(),
397
+ },
398
+ },
399
+ },
400
+ version
401
+ );
402
+
403
+ writeJSON('.claude/settings.json', settings);
404
+ updateGitignore();
405
+
406
+ return true;
407
+ }
408
+
409
+ // ============================================================================
410
+ // DISABLE FEATURE
411
+ // ============================================================================
412
+
413
+ /**
414
+ * Disable a feature
415
+ * @param {string} feature - Feature name
416
+ * @param {string} version - Current version string
417
+ * @returns {boolean} Success
418
+ */
419
+ function disableFeature(feature, version) {
420
+ const config = FEATURES[feature];
421
+ if (!config) {
422
+ error(`Unknown feature: ${feature}`);
423
+ return false;
424
+ }
425
+
426
+ if (!fs.existsSync('.claude/settings.json')) {
427
+ info(`${feature} already disabled (no settings file)`);
428
+ return true;
429
+ }
430
+
431
+ const settings = readJSON('.claude/settings.json');
432
+ if (!settings) return false;
433
+
434
+ // Disable hook
435
+ if (config.hook && settings.hooks?.[config.hook]) {
436
+ if (config.hook === 'Stop') {
437
+ // Stop hooks stack - remove only this script
438
+ if (settings.hooks.Stop?.[0]?.hooks) {
439
+ const before = settings.hooks.Stop[0].hooks.length;
440
+ settings.hooks.Stop[0].hooks = settings.hooks.Stop[0].hooks.filter(
441
+ h => !h.command?.includes(config.script)
442
+ );
443
+ const after = settings.hooks.Stop[0].hooks.length;
444
+
445
+ if (before > after) {
446
+ success(`Stop hook removed (${config.script})`);
447
+ }
448
+
449
+ if (settings.hooks.Stop[0].hooks.length === 0) {
450
+ delete settings.hooks.Stop;
451
+ }
452
+ }
453
+ } else {
454
+ delete settings.hooks[config.hook];
455
+ success(`${config.hook} hook disabled`);
456
+ }
457
+ }
458
+
459
+ // Disable archival
460
+ if (feature === 'archival') {
461
+ if (settings.hooks?.SessionStart?.[0]?.hooks) {
462
+ settings.hooks.SessionStart[0].hooks = settings.hooks.SessionStart[0].hooks.filter(
463
+ h => !h.command?.includes('archive-completed-stories')
464
+ );
465
+ }
466
+ updateMetadata({ archival: { enabled: false } }, version);
467
+ success('Archival disabled');
468
+ }
469
+
470
+ // Disable statusLine
471
+ if (feature === 'statusline' && settings.statusLine) {
472
+ delete settings.statusLine;
473
+ success('Status line disabled');
474
+ }
475
+
476
+ // Disable autoupdate
477
+ if (feature === 'autoupdate') {
478
+ updateMetadata({ updates: { autoUpdate: false } }, version);
479
+ success('Auto-update disabled');
480
+ return true;
481
+ }
482
+
483
+ // Disable askuserquestion
484
+ if (feature === 'askuserquestion') {
485
+ updateMetadata(
486
+ {
487
+ features: {
488
+ askUserQuestion: {
489
+ enabled: false,
490
+ version,
491
+ at: new Date().toISOString(),
492
+ },
493
+ },
494
+ },
495
+ version
496
+ );
497
+ success('AskUserQuestion disabled');
498
+ info('Commands will end with natural text questions instead of AskUserQuestion tool');
499
+ return true;
500
+ }
501
+
502
+ // Disable damage control
503
+ if (feature === 'damagecontrol') {
504
+ if (settings.hooks?.PreToolUse && Array.isArray(settings.hooks.PreToolUse)) {
505
+ const before = settings.hooks.PreToolUse.length;
506
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(h => {
507
+ const isDamageControlHook = h.hooks?.some(hk => hk.command?.includes('damage-control'));
508
+ return !isDamageControlHook;
509
+ });
510
+ const after = settings.hooks.PreToolUse.length;
511
+
512
+ if (before > after) {
513
+ success(`Removed ${before - after} damage control PreToolUse hook(s)`);
514
+ }
515
+
516
+ if (settings.hooks.PreToolUse.length === 0) {
517
+ delete settings.hooks.PreToolUse;
518
+ }
519
+ }
520
+
521
+ updateMetadata(
522
+ {
523
+ features: {
524
+ damagecontrol: {
525
+ enabled: false,
526
+ version,
527
+ at: new Date().toISOString(),
528
+ },
529
+ },
530
+ },
531
+ version
532
+ );
533
+
534
+ writeJSON('.claude/settings.json', settings);
535
+ success('Damage control disabled');
536
+ return true;
537
+ }
538
+
539
+ writeJSON('.claude/settings.json', settings);
540
+ updateMetadata(
541
+ { features: { [feature]: { enabled: false, version, at: new Date().toISOString() } } },
542
+ version
543
+ );
544
+
545
+ return true;
546
+ }
547
+
548
+ // ============================================================================
549
+ // PROFILES
550
+ // ============================================================================
551
+
552
+ /**
553
+ * Apply a preset profile
554
+ * @param {string} profileName - Profile name
555
+ * @param {object} options - Options
556
+ * @param {string} version - Current version string
557
+ * @returns {boolean} Success
558
+ */
559
+ function applyProfile(profileName, options = {}, version) {
560
+ const profile = PROFILES[profileName];
561
+ if (!profile) {
562
+ error(`Unknown profile: ${profileName}`);
563
+ log('Available: ' + Object.keys(PROFILES).join(', '));
564
+ return false;
565
+ }
566
+
567
+ header(`Applying "${profileName}" profile`);
568
+ log(profile.description, c.dim);
569
+
570
+ if (profile.enable) {
571
+ profile.enable.forEach(f =>
572
+ enableFeature(f, { archivalDays: profile.archivalDays || options.archivalDays }, version)
573
+ );
574
+ }
575
+
576
+ if (profile.disable) {
577
+ profile.disable.forEach(f => disableFeature(f, version));
578
+ }
579
+
580
+ return true;
581
+ }
582
+
583
+ // ============================================================================
584
+ // STATUSLINE COMPONENTS
585
+ // ============================================================================
586
+
587
+ /**
588
+ * Set statusline component visibility
589
+ * @param {string[]} enableComponents - Components to enable
590
+ * @param {string[]} disableComponents - Components to disable
591
+ * @returns {boolean} Success
592
+ */
593
+ function setStatuslineComponents(enableComponents = [], disableComponents = []) {
594
+ const metaPath = 'docs/00-meta/agileflow-metadata.json';
595
+
596
+ if (!fs.existsSync(metaPath)) {
597
+ warn('No metadata file found - run with --enable=statusline first');
598
+ return false;
599
+ }
600
+
601
+ const meta = readJSON(metaPath);
602
+ if (!meta) {
603
+ error('Cannot parse metadata file');
604
+ return false;
605
+ }
606
+
607
+ meta.features = meta.features || {};
608
+ meta.features.statusline = meta.features.statusline || {};
609
+ meta.features.statusline.components = meta.features.statusline.components || {};
610
+
611
+ // Set defaults
612
+ STATUSLINE_COMPONENTS.forEach(comp => {
613
+ if (meta.features.statusline.components[comp] === undefined) {
614
+ meta.features.statusline.components[comp] = true;
615
+ }
616
+ });
617
+
618
+ // Enable specified
619
+ enableComponents.forEach(comp => {
620
+ if (STATUSLINE_COMPONENTS.includes(comp)) {
621
+ meta.features.statusline.components[comp] = true;
622
+ success(`Statusline component enabled: ${comp}`);
623
+ } else {
624
+ warn(`Unknown component: ${comp} (available: ${STATUSLINE_COMPONENTS.join(', ')})`);
625
+ }
626
+ });
627
+
628
+ // Disable specified
629
+ disableComponents.forEach(comp => {
630
+ if (STATUSLINE_COMPONENTS.includes(comp)) {
631
+ meta.features.statusline.components[comp] = false;
632
+ success(`Statusline component disabled: ${comp}`);
633
+ } else {
634
+ warn(`Unknown component: ${comp} (available: ${STATUSLINE_COMPONENTS.join(', ')})`);
635
+ }
636
+ });
637
+
638
+ meta.updated = new Date().toISOString();
639
+ writeJSON(metaPath, meta);
640
+
641
+ return true;
642
+ }
643
+
644
+ /**
645
+ * List statusline components
646
+ */
647
+ function listStatuslineComponents() {
648
+ const metaPath = 'docs/00-meta/agileflow-metadata.json';
649
+
650
+ header('Statusline Components');
651
+
652
+ if (!fs.existsSync(metaPath)) {
653
+ log(' No configuration found (defaults: all enabled)', c.dim);
654
+ STATUSLINE_COMPONENTS.forEach(comp => {
655
+ log(` ${comp}: enabled (default)`, c.green);
656
+ });
657
+ return;
658
+ }
659
+
660
+ const meta = readJSON(metaPath);
661
+ const components = meta?.features?.statusline?.components || {};
662
+
663
+ STATUSLINE_COMPONENTS.forEach(comp => {
664
+ const enabled = components[comp] !== false;
665
+ const icon = enabled ? '' : '';
666
+ const color = enabled ? c.green : c.dim;
667
+ log(` ${icon} ${comp}: ${enabled ? 'enabled' : 'disabled'}`, color);
668
+ });
669
+
670
+ log('\nTo toggle: --show=<component> or --hide=<component>', c.dim);
671
+ log(`Components: ${STATUSLINE_COMPONENTS.join(', ')}`, c.dim);
672
+ }
673
+
674
+ // ============================================================================
675
+ // MIGRATION
676
+ // ============================================================================
677
+
678
+ /**
679
+ * Migrate settings to new format
680
+ * @returns {boolean} Whether migration occurred
681
+ */
682
+ function migrateSettings() {
683
+ header('Migrating Settings...');
684
+
685
+ if (!fs.existsSync('.claude/settings.json')) {
686
+ warn('No settings.json to migrate');
687
+ return false;
688
+ }
689
+
690
+ const settings = readJSON('.claude/settings.json');
691
+ if (!settings) {
692
+ error('Cannot parse settings.json');
693
+ return false;
694
+ }
695
+
696
+ let migrated = false;
697
+
698
+ // Migrate hooks to new format
699
+ if (settings.hooks) {
700
+ ['SessionStart', 'PreCompact', 'UserPromptSubmit', 'Stop'].forEach(hookName => {
701
+ const hook = settings.hooks[hookName];
702
+ if (!hook) return;
703
+
704
+ if (typeof hook === 'string') {
705
+ const isNode = hook.includes('node ') || hook.endsWith('.js');
706
+ settings.hooks[hookName] = [
707
+ { matcher: '', hooks: [{ type: 'command', command: isNode ? hook : `bash ${hook}` }] },
708
+ ];
709
+ success(`Migrated ${hookName} from string format`);
710
+ migrated = true;
711
+ } else if (Array.isArray(hook) && hook.length > 0) {
712
+ const first = hook[0];
713
+ if (first.enabled !== undefined || first.command !== undefined) {
714
+ if (first.command) {
715
+ settings.hooks[hookName] = [
716
+ { matcher: '', hooks: [{ type: 'command', command: first.command }] },
717
+ ];
718
+ success(`Migrated ${hookName} from old object format`);
719
+ migrated = true;
720
+ }
721
+ } else if (first.matcher === undefined) {
722
+ settings.hooks[hookName] = [
723
+ { matcher: '', hooks: first.hooks || [{ type: 'command', command: 'echo "hook"' }] },
724
+ ];
725
+ success(`Migrated ${hookName} - added matcher`);
726
+ migrated = true;
727
+ }
728
+ }
729
+ });
730
+ }
731
+
732
+ // Migrate statusLine
733
+ if (settings.statusLine) {
734
+ if (typeof settings.statusLine === 'string') {
735
+ settings.statusLine = { type: 'command', command: settings.statusLine, padding: 0 };
736
+ success('Migrated statusLine from string format');
737
+ migrated = true;
738
+ } else if (!settings.statusLine.type) {
739
+ settings.statusLine.type = 'command';
740
+ if (settings.statusLine.refreshInterval) {
741
+ delete settings.statusLine.refreshInterval;
742
+ settings.statusLine.padding = 0;
743
+ }
744
+ success('Migrated statusLine - added type:command');
745
+ migrated = true;
746
+ }
747
+ }
748
+
749
+ if (migrated) {
750
+ fs.copyFileSync('.claude/settings.json', '.claude/settings.json.backup');
751
+ info('Backed up to .claude/settings.json.backup');
752
+ writeJSON('.claude/settings.json', settings);
753
+ success('Settings migrated successfully!');
754
+ } else {
755
+ info('No migration needed - formats are correct');
756
+ }
757
+
758
+ return migrated;
759
+ }
760
+
761
+ /**
762
+ * Upgrade outdated features to latest version
763
+ * @param {object} status - Status object from detectConfig
764
+ * @param {string} version - Current version
765
+ * @returns {boolean} Whether any features were upgraded
766
+ */
767
+ function upgradeFeatures(status, version) {
768
+ header('Upgrading Outdated Features...');
769
+
770
+ let upgraded = 0;
771
+
772
+ Object.entries(status.features).forEach(([feature, data]) => {
773
+ if (data.enabled && data.outdated) {
774
+ log(`\nUpgrading ${feature}...`, c.cyan);
775
+ if (
776
+ enableFeature(feature, { archivalDays: data.threshold || 30, isUpgrade: true }, version)
777
+ ) {
778
+ upgraded++;
779
+ }
780
+ }
781
+ });
782
+
783
+ if (upgraded === 0) {
784
+ info('No features needed upgrading');
785
+ } else {
786
+ success(`Upgraded ${upgraded} feature(s) to v${version}`);
787
+ }
788
+
789
+ return upgraded > 0;
790
+ }
791
+
792
+ module.exports = {
793
+ // Constants
794
+ FEATURES,
795
+ PROFILES,
796
+ STATUSLINE_COMPONENTS,
797
+ // Feature management
798
+ enableFeature,
799
+ disableFeature,
800
+ applyProfile,
801
+ updateMetadata,
802
+ // Statusline components
803
+ setStatuslineComponents,
804
+ listStatuslineComponents,
805
+ // Migration
806
+ migrateSettings,
807
+ upgradeFeatures,
808
+ // Helpers
809
+ scriptExists,
810
+ getScriptPath,
811
+ };