agileflow 2.89.2 → 2.90.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 +10 -0
- package/README.md +3 -3
- package/lib/content-sanitizer.js +463 -0
- package/lib/error-codes.js +544 -0
- package/lib/errors.js +336 -5
- package/lib/feedback.js +561 -0
- package/lib/path-resolver.js +396 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/session-registry.js +461 -0
- package/lib/smart-json-file.js +653 -0
- package/lib/table-formatter.js +504 -0
- package/lib/transient-status.js +374 -0
- package/lib/ui-manager.js +612 -0
- package/lib/validate-args.js +213 -0
- package/lib/validate-names.js +143 -0
- package/lib/validate-paths.js +434 -0
- package/lib/validate.js +38 -584
- package/package.json +4 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/configure-detect.js +383 -0
- package/scripts/lib/configure-features.js +811 -0
- package/scripts/lib/configure-repair.js +314 -0
- package/scripts/lib/configure-utils.js +115 -0
- package/scripts/lib/frontmatter-parser.js +3 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +176 -0
- package/scripts/tui/index.js +75 -0
- package/scripts/tui/lib/crashRecovery.js +302 -0
- package/scripts/tui/lib/eventStream.js +316 -0
- package/scripts/tui/lib/keyboard.js +252 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +278 -0
- package/scripts/tui/panels/SessionPanel.js +178 -0
- package/scripts/tui/panels/TracePanel.js +333 -0
- package/src/core/commands/tui.md +91 -0
- package/tools/cli/commands/config.js +10 -33
- package/tools/cli/commands/doctor.js +48 -40
- package/tools/cli/commands/list.js +49 -37
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +12 -41
- package/tools/cli/installers/core/installer.js +75 -12
- package/tools/cli/installers/ide/_interface.js +238 -0
- package/tools/cli/installers/ide/codex.js +2 -2
- package/tools/cli/installers/ide/manager.js +15 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
- package/tools/cli/lib/ide-registry.js +186 -0
- package/tools/cli/lib/npm-utils.js +16 -3
- package/tools/cli/lib/self-update.js +148 -0
- package/tools/cli/lib/validation-middleware.js +491 -0
|
@@ -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
|
+
};
|