devflow-kit 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to DevFlow will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.8.1] - 2026-03-22
9
+
10
+ ### Changed
11
+ - **Init wizard**: individual feature prompts with explanatory notes replace extras multiselect
12
+ - **Init wizard**: scope-aware `.claudeignore` batch install across all discovered projects (user scope)
13
+ - **Init wizard**: project discovery via `~/.claude/history.jsonl` to find all Claude-used git repos
14
+ - **Init wizard**: managed settings sudo confirmation moved to prompt phase (before spinner)
15
+ - **Init wizard**: safe-delete prompt moved to prompt phase for uninterrupted install
16
+
17
+ ### Added
18
+ - `--hud` flag for `devflow init` to explicitly enable HUD
19
+ - `discoverProjectGitRoots()` utility for finding projects from Claude history
20
+
21
+ ### Removed
22
+ - Extras multiselect (`buildExtrasOptions`) — replaced by individual feature prompts
23
+
24
+ ---
25
+
8
26
  ## [1.8.0] - 2026-03-22
9
27
 
10
28
  ### Added
@@ -958,6 +976,7 @@ devflow init
958
976
  ---
959
977
 
960
978
  [Unreleased]: https://github.com/dean0x/devflow/compare/v1.4.0...HEAD
979
+ [1.8.1]: https://github.com/dean0x/devflow/compare/v1.8.0...v1.8.1
961
980
  [1.8.0]: https://github.com/dean0x/devflow/compare/v1.7.0...v1.8.0
962
981
  [1.7.0]: https://github.com/dean0x/devflow/compare/v1.6.1...v1.7.0
963
982
  [1.6.1]: https://github.com/dean0x/devflow/compare/v1.6.0...v1.6.1
package/README.md CHANGED
@@ -248,8 +248,8 @@ Session context is saved and restored automatically via Working Memory hooks —
248
248
  | `--teams` / `--no-teams` | Enable/disable Agent Teams (experimental, default: off) |
249
249
  | `--ambient` / `--no-ambient` | Enable/disable ambient mode (default: on) |
250
250
  | `--memory` / `--no-memory` | Enable/disable working memory (default: on) |
251
+ | `--hud` / `--no-hud` | Enable/disable HUD status line (default: on) |
251
252
  | `--hud-only` | Install only the HUD (no plugins, hooks, or extras) |
252
- | `--no-hud` | Disable HUD status line |
253
253
  | `--verbose` | Show detailed output |
254
254
 
255
255
  ### HUD Options
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { type PluginDefinition } from '../plugins.js';
3
- export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList } from '../utils/post-install.js';
3
+ export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList, discoverProjectGitRoots } from '../utils/post-install.js';
4
4
  export { addAmbientHook, removeAmbientHook, hasAmbientHook } from './ambient.js';
5
5
  export { addMemoryHooks, removeMemoryHooks, hasMemoryHooks } from './memory.js';
6
6
  export { addHudStatusLine, removeHudStatusLine, hasHudStatusLine } from './hud.js';
@@ -12,16 +12,5 @@ export declare function parsePluginSelection(input: string, validPlugins: Plugin
12
12
  selected: string[];
13
13
  invalid: string[];
14
14
  };
15
- export type ExtraId = 'settings' | 'claudeignore' | 'gitignore' | 'docs' | 'safe-delete';
16
- interface ExtraOption {
17
- value: ExtraId;
18
- label: string;
19
- hint: string;
20
- }
21
- /**
22
- * Build the list of configuration extras available for the given scope/git context.
23
- * Pure function — no I/O, no side effects.
24
- */
25
- export declare function buildExtrasOptions(scope: 'user' | 'local', gitRoot: string | null): ExtraOption[];
26
15
  export declare const initCommand: Command;
27
16
  //# sourceMappingURL=init.d.ts.map
@@ -9,7 +9,7 @@ import { getInstallationPaths } from '../utils/paths.js';
9
9
  import { getGitRoot } from '../utils/git.js';
10
10
  import { isClaudeCliAvailable } from '../utils/cli.js';
11
11
  import { installViaCli, installViaFileCopy, copyDirectory } from '../utils/installer.js';
12
- import { installSettings, installManagedSettings, installClaudeignore, updateGitignore, createDocsStructure, createMemoryDir, migrateMemoryFiles, } from '../utils/post-install.js';
12
+ import { installSettings, installManagedSettings, installClaudeignore, discoverProjectGitRoots, updateGitignore, createDocsStructure, createMemoryDir, migrateMemoryFiles, } from '../utils/post-install.js';
13
13
  import { DEVFLOW_PLUGINS, LEGACY_SKILL_NAMES, LEGACY_COMMAND_NAMES, buildAssetMaps } from '../plugins.js';
14
14
  import { detectPlatform, detectShell, getProfilePath, getSafeDeleteInfo, hasSafeDelete } from '../utils/safe-delete.js';
15
15
  import { generateSafeDeleteBlock, installToProfile, removeFromProfile, getInstalledVersion, SAFE_DELETE_BLOCK_VERSION } from '../utils/safe-delete-install.js';
@@ -19,7 +19,7 @@ import { addHudStatusLine, removeHudStatusLine } from './hud.js';
19
19
  import { loadConfig as loadHudConfig, saveConfig as saveHudConfig } from '../hud/config.js';
20
20
  import { readManifest, writeManifest, resolvePluginList, detectUpgrade } from '../utils/manifest.js';
21
21
  // Re-export pure functions for tests (canonical source is post-install.ts)
22
- export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList } from '../utils/post-install.js';
22
+ export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList, discoverProjectGitRoots } from '../utils/post-install.js';
23
23
  export { addAmbientHook, removeAmbientHook, hasAmbientHook } from './ambient.js';
24
24
  export { addMemoryHooks, removeMemoryHooks, hasMemoryHooks } from './memory.js';
25
25
  export { addHudStatusLine, removeHudStatusLine, hasHudStatusLine } from './hud.js';
@@ -38,26 +38,6 @@ export function parsePluginSelection(input, validPlugins) {
38
38
  const invalid = selected.filter(p => !validNames.includes(p));
39
39
  return { selected, invalid };
40
40
  }
41
- /**
42
- * Build the list of configuration extras available for the given scope/git context.
43
- * Pure function — no I/O, no side effects.
44
- */
45
- export function buildExtrasOptions(scope, gitRoot) {
46
- const options = [
47
- { value: 'settings', label: 'Settings & Working Memory', hint: 'Model defaults, session memory hooks, status line' },
48
- ];
49
- if (gitRoot) {
50
- options.push({ value: 'claudeignore', label: '.claudeignore', hint: 'Exclude secrets, deps, build artifacts from Claude context' });
51
- }
52
- if (scope === 'local' && gitRoot) {
53
- options.push({ value: 'gitignore', label: '.gitignore entries', hint: 'Add .claude/ and .devflow/ to .gitignore' });
54
- }
55
- if (scope === 'local') {
56
- options.push({ value: 'docs', label: '.docs/ directory', hint: 'Review reports, dev logs, status tracking for this project' });
57
- }
58
- options.push({ value: 'safe-delete', label: 'Safe-delete (rm → trash)', hint: 'Override rm to use trash CLI — prevents accidental deletion' });
59
- return options;
60
- }
61
41
  export const initCommand = new Command('init')
62
42
  .description('Initialize DevFlow for Claude Code')
63
43
  .option('--scope <type>', 'Installation scope: user or local (project-only)', /^(user|local)$/i)
@@ -69,6 +49,7 @@ export const initCommand = new Command('init')
69
49
  .option('--no-ambient', 'Disable ambient mode')
70
50
  .option('--memory', 'Enable working memory (session context preservation)')
71
51
  .option('--no-memory', 'Disable working memory hooks')
52
+ .option('--hud', 'Enable HUD (git info, context usage, session stats)')
72
53
  .option('--no-hud', 'Disable HUD status line')
73
54
  .option('--hud-only', 'Install only the HUD (no plugins, hooks, or extras)')
74
55
  .action(async (options) => {
@@ -193,12 +174,29 @@ export const initCommand = new Command('init')
193
174
  }
194
175
  }
195
176
  else if (process.stdin.isTTY) {
177
+ // Short hints to prevent overflow in multiselect — full descriptions live in plugins.ts
178
+ const pluginHints = {
179
+ 'devflow-specify': 'feature specs → GitHub issues',
180
+ 'devflow-implement': 'explore, plan, code, review',
181
+ 'devflow-code-review': 'parallel specialized reviewers',
182
+ 'devflow-resolve': 'fix review issues by risk',
183
+ 'devflow-debug': 'competing hypotheses',
184
+ 'devflow-self-review': 'Simplifier + Scrutinizer',
185
+ 'devflow-typescript': 'TypeScript patterns',
186
+ 'devflow-react': 'React patterns',
187
+ 'devflow-accessibility': 'WCAG compliance',
188
+ 'devflow-frontend-design': 'typography, color, spacing',
189
+ 'devflow-go': 'Go patterns',
190
+ 'devflow-java': 'Java patterns',
191
+ 'devflow-python': 'Python patterns',
192
+ 'devflow-rust': 'Rust patterns',
193
+ };
196
194
  const choices = DEVFLOW_PLUGINS
197
195
  .filter(pl => pl.name !== 'devflow-core-skills' && pl.name !== 'devflow-ambient' && pl.name !== 'devflow-audit-claude')
198
196
  .map(pl => ({
199
197
  value: pl.name,
200
198
  label: pl.name.replace('devflow-', ''),
201
- hint: pl.description,
199
+ hint: pluginHints[pl.name] ?? pl.description,
202
200
  }));
203
201
  const preSelected = DEVFLOW_PLUGINS
204
202
  .filter(pl => !pl.optional && pl.name !== 'devflow-core-skills' && pl.name !== 'devflow-ambient')
@@ -224,11 +222,14 @@ export const initCommand = new Command('init')
224
222
  teamsEnabled = false;
225
223
  }
226
224
  else {
225
+ p.note('Agent Teams enable peer debate between agents — adversarial\n' +
226
+ 'review, competing hypotheses in debugging, and consensus-driven\n' +
227
+ 'exploration. Experimental: may be unstable.', 'Agent Teams');
227
228
  const teamsChoice = await p.select({
228
229
  message: 'Enable Agent Teams?',
229
230
  options: [
230
- { value: false, label: 'No (Recommended)', hint: 'Experimental — may be unstable' },
231
- { value: true, label: 'Yes', hint: 'Advanced — peer debate in review, exploration, debugging' },
231
+ { value: false, label: 'Not yet', hint: 'Recommended' },
232
+ { value: true, label: 'Yes', hint: 'Experimental' },
232
233
  ],
233
234
  });
234
235
  if (p.isCancel(teamsChoice)) {
@@ -246,11 +247,15 @@ export const initCommand = new Command('init')
246
247
  ambientEnabled = true;
247
248
  }
248
249
  else {
250
+ p.note('Auto-classifies every prompt by intent and depth. Loads relevant\n' +
251
+ 'skills automatically and escalates to full agent pipelines\n' +
252
+ '(review, debug, implement) when the task warrants it.\n\n' +
253
+ 'Adds a small amount of context to each prompt for classification.', 'Ambient Mode');
249
254
  const ambientChoice = await p.select({
250
255
  message: 'Enable ambient mode?',
251
256
  options: [
252
- { value: true, label: 'Yes (Recommended)', hint: 'Classifies intent, loads skills, orchestrates agents' },
253
- { value: false, label: 'No', hint: 'Full control — load skills manually' },
257
+ { value: true, label: 'Yes', hint: 'Recommended' },
258
+ { value: false, label: 'No', hint: 'Manual skill loading via slash commands' },
254
259
  ],
255
260
  });
256
261
  if (p.isCancel(ambientChoice)) {
@@ -268,8 +273,13 @@ export const initCommand = new Command('init')
268
273
  memoryEnabled = true;
269
274
  }
270
275
  else {
276
+ p.note('Preserves session context across /clear, restarts, and context\n' +
277
+ 'compaction. Clear your session at any point and resume right\n' +
278
+ 'where you left off.\n\n' +
279
+ 'Runs a background agent on session stop that consumes additional\n' +
280
+ 'tokens. Consider skipping if token usage is a concern.', 'Working Memory');
271
281
  const memoryChoice = await p.confirm({
272
- message: 'Enable working memory? (automatic session context preservation)',
282
+ message: 'Enable working memory? (Recommended)',
273
283
  initialValue: true,
274
284
  });
275
285
  if (p.isCancel(memoryChoice)) {
@@ -287,8 +297,10 @@ export const initCommand = new Command('init')
287
297
  hudEnabled = true;
288
298
  }
289
299
  else {
300
+ p.note('The HUD displays git branch, context usage, and session stats\n' +
301
+ 'in the Claude Code status bar. Configurable via devflow hud.', 'HUD');
290
302
  const hudChoice = await p.confirm({
291
- message: 'Enable HUD status line?',
303
+ message: 'Enable HUD? (Recommended)',
292
304
  initialValue: true,
293
305
  });
294
306
  if (p.isCancel(hudChoice)) {
@@ -297,14 +309,110 @@ export const initCommand = new Command('init')
297
309
  }
298
310
  hudEnabled = hudChoice;
299
311
  }
300
- // Security deny list placement (user scope + TTY only)
312
+ // .claudeignore prompt (needs early git detection)
313
+ const earlyGitRoot = await getGitRoot();
314
+ let claudeignoreEnabled = true;
315
+ let discoveredProjects = [];
316
+ if (earlyGitRoot && process.stdin.isTTY) {
317
+ if (scope === 'user') {
318
+ // User scope: discover all projects and offer batch install
319
+ discoveredProjects = await discoverProjectGitRoots();
320
+ p.note('Scans all projects Claude has worked on and creates a\n' +
321
+ '.claudeignore in each git repository. Excludes secrets,\n' +
322
+ 'API keys, dependencies, and build artifacts from context.', '.claudeignore');
323
+ if (discoveredProjects.length > 0) {
324
+ const maxShow = 5;
325
+ const projectLines = discoveredProjects.slice(0, maxShow).join('\n');
326
+ const overflow = discoveredProjects.length > maxShow
327
+ ? `\n... (${discoveredProjects.length - maxShow} more)`
328
+ : '';
329
+ p.note(projectLines + overflow, `Discovered ${discoveredProjects.length} projects`);
330
+ const claudeignoreChoice = await p.confirm({
331
+ message: `Install .claudeignore to ${discoveredProjects.length} projects? (Recommended)`,
332
+ initialValue: true,
333
+ });
334
+ if (p.isCancel(claudeignoreChoice)) {
335
+ p.cancel('Installation cancelled.');
336
+ process.exit(0);
337
+ }
338
+ claudeignoreEnabled = claudeignoreChoice;
339
+ }
340
+ else {
341
+ // No projects discovered — fall back to current project only
342
+ const claudeignoreChoice = await p.confirm({
343
+ message: 'Create .claudeignore? (Recommended)',
344
+ initialValue: true,
345
+ });
346
+ if (p.isCancel(claudeignoreChoice)) {
347
+ p.cancel('Installation cancelled.');
348
+ process.exit(0);
349
+ }
350
+ claudeignoreEnabled = claudeignoreChoice;
351
+ }
352
+ }
353
+ else {
354
+ // Local scope: single-project note
355
+ p.note('Creates a .claudeignore in this project that excludes\n' +
356
+ 'secrets, API keys, dependencies, and build artifacts from\n' +
357
+ 'Claude\'s context window.', '.claudeignore');
358
+ const claudeignoreChoice = await p.confirm({
359
+ message: 'Create .claudeignore? (Recommended)',
360
+ initialValue: true,
361
+ });
362
+ if (p.isCancel(claudeignoreChoice)) {
363
+ p.cancel('Installation cancelled.');
364
+ process.exit(0);
365
+ }
366
+ claudeignoreEnabled = claudeignoreChoice;
367
+ }
368
+ }
369
+ else if (!earlyGitRoot) {
370
+ claudeignoreEnabled = false;
371
+ }
372
+ // Safe-delete detection + prompt (all detection early, action deferred)
373
+ const platform = detectPlatform();
374
+ const shell = detectShell();
375
+ const safeDeleteInfo = getSafeDeleteInfo(platform);
376
+ const safeDeleteAvailable = hasSafeDelete(platform);
377
+ const profilePath = getProfilePath(shell);
378
+ let safeDeleteAction = 'skip';
379
+ let safeDeleteBlock = null;
380
+ if (process.stdin.isTTY && profilePath && safeDeleteAvailable) {
381
+ const trashCmd = safeDeleteInfo.command;
382
+ safeDeleteBlock = generateSafeDeleteBlock(shell, process.platform, trashCmd);
383
+ if (safeDeleteBlock) {
384
+ const installedVersion = await getInstalledVersion(profilePath);
385
+ if (installedVersion === SAFE_DELETE_BLOCK_VERSION) {
386
+ safeDeleteAction = 'skip'; // already current
387
+ }
388
+ else if (installedVersion > 0) {
389
+ safeDeleteAction = 'upgrade'; // auto-upgrade, no prompt needed
390
+ }
391
+ else {
392
+ // Fresh install — prompt
393
+ p.note('Overrides rm to use your system trash CLI instead of permanent\n' +
394
+ 'deletion. Prevents accidental data loss from rm -rf.', 'Safe Delete');
395
+ const safeDeleteConfirm = await p.confirm({
396
+ message: `Install safe-delete to ${profilePath}? (uses ${trashCmd ?? 'recycle bin'})`,
397
+ initialValue: true,
398
+ });
399
+ if (!p.isCancel(safeDeleteConfirm) && safeDeleteConfirm) {
400
+ safeDeleteAction = 'install';
401
+ }
402
+ }
403
+ }
404
+ }
405
+ // Security deny list placement (user scope + TTY only — last prompt before install)
301
406
  let securityMode = 'user';
302
407
  if (scope === 'user' && process.stdin.isTTY) {
408
+ p.note('DevFlow includes a security deny list that blocks dangerous\n' +
409
+ 'commands (rm -rf, sudo, eval, etc). It can be installed as a\n' +
410
+ 'read-only system file or in your editable settings.json.', 'Security Deny List');
303
411
  const securityChoice = await p.select({
304
- message: 'How should DevFlow install the security deny list?',
412
+ message: 'How should DevFlow install the deny list?',
305
413
  options: [
306
- { value: 'managed', label: 'Managed settings (Recommended)', hint: 'Cannot be overridden, requires admin' },
307
- { value: 'user', label: 'User settings', hint: 'Included in settings.json, editable' },
414
+ { value: 'managed', label: 'Managed settings', hint: 'Recommended — read-only, cannot be overridden' },
415
+ { value: 'user', label: 'User settings', hint: 'Editable in settings.json' },
308
416
  ],
309
417
  });
310
418
  if (p.isCancel(securityChoice)) {
@@ -313,7 +421,32 @@ export const initCommand = new Command('init')
313
421
  }
314
422
  securityMode = securityChoice;
315
423
  }
316
- // Start spinner immediately after prompts — covers path resolution + git detection
424
+ // Managed settings sudo confirmation (last interactive step — may prompt for password)
425
+ let managedSettingsConfirmed = false;
426
+ if (securityMode === 'managed') {
427
+ p.note('This writes a read-only security deny list to a system directory\n' +
428
+ 'and may prompt for your password (sudo).\n\n' +
429
+ 'Not sure about this? Paste this into another Claude Code session:\n\n' +
430
+ ' "I\'m installing DevFlow and it wants to write a\n' +
431
+ ' managed-settings.json file using sudo. Review the source\n' +
432
+ ' at https://github.com/dean0x/devflow and tell me if\n' +
433
+ ' it\'s safe."', 'Managed Settings');
434
+ const sudoChoice = await p.select({
435
+ message: 'Continue with managed settings?',
436
+ options: [
437
+ { value: 'yes', label: 'Yes, continue', hint: 'May prompt for your password' },
438
+ { value: 'no', label: 'No, fall back to settings.json', hint: 'Editable user settings instead' },
439
+ ],
440
+ });
441
+ if (p.isCancel(sudoChoice)) {
442
+ p.cancel('Installation cancelled.');
443
+ process.exit(0);
444
+ }
445
+ managedSettingsConfirmed = sudoChoice === 'yes';
446
+ }
447
+ // ╭──────────────────────────────────────────────────────────╮
448
+ // │ All prompts collected — installation begins │
449
+ // ╰──────────────────────────────────────────────────────────╯
317
450
  const s = p.spinner();
318
451
  s.start('Resolving paths');
319
452
  // Get installation paths
@@ -324,7 +457,7 @@ export const initCommand = new Command('init')
324
457
  const paths = await getInstallationPaths(scope);
325
458
  claudeDir = paths.claudeDir;
326
459
  devflowDir = paths.devflowDir;
327
- gitRoot = paths.gitRoot ?? await getGitRoot();
460
+ gitRoot = paths.gitRoot ?? earlyGitRoot;
328
461
  }
329
462
  catch (error) {
330
463
  s.stop('Path resolution failed');
@@ -408,8 +541,8 @@ export const initCommand = new Command('init')
408
541
  process.exit(1);
409
542
  }
410
543
  }
411
- s.stop('Plugins installed');
412
544
  // Clean up stale skills from previous installations
545
+ s.message('Cleaning up');
413
546
  const skillsDir = path.join(claudeDir, 'skills');
414
547
  let staleRemoved = 0;
415
548
  for (const legacy of LEGACY_SKILL_NAMES) {
@@ -443,131 +576,90 @@ export const initCommand = new Command('init')
443
576
  if (staleCommandsRemoved > 0 && verbose) {
444
577
  p.log.info(`Cleaned up ${staleCommandsRemoved} legacy command(s)`);
445
578
  }
446
- // === Configuration extras ===
447
- const extrasOptions = buildExtrasOptions(scope, gitRoot);
448
- let selectedExtras;
449
- if (process.stdin.isTTY) {
450
- const extrasSelection = await p.multiselect({
451
- message: 'Configure extras',
452
- options: extrasOptions,
453
- initialValues: extrasOptions.map(o => o.value),
454
- required: false,
455
- });
456
- if (p.isCancel(extrasSelection)) {
457
- p.cancel('Installation cancelled.');
458
- process.exit(0);
459
- }
460
- selectedExtras = extrasSelection;
579
+ // === Settings & hooks (all automatic based on collected choices) ===
580
+ s.message('Configuring settings');
581
+ // Determine effective security mode (managed settings executed later, after safe-delete)
582
+ let effectiveSecurityMode = securityMode;
583
+ if (securityMode === 'managed' && !managedSettingsConfirmed) {
584
+ effectiveSecurityMode = 'user';
461
585
  }
462
- else {
463
- selectedExtras = extrasOptions.map(o => o.value);
464
- }
465
- // Settings may trigger its own TTY sub-prompt — run outside spinner
466
- if (selectedExtras.includes('settings')) {
467
- // Attempt managed settings write if user chose managed mode
468
- let effectiveSecurityMode = securityMode;
469
- if (securityMode === 'managed') {
470
- p.note('This writes a read-only security deny list to a system directory\n' +
471
- 'and may prompt for your password (sudo).\n\n' +
472
- 'Not sure about this? Paste this into another Claude Code session:\n\n' +
473
- ' "I\'m installing DevFlow and it wants to write a\n' +
474
- ' managed-settings.json file using sudo. Review the source\n' +
475
- ' at https://github.com/dean0x/devflow and tell me if\n' +
476
- ' it\'s safe."', 'Managed Settings');
477
- const sudoChoice = await p.select({
478
- message: 'Continue with managed settings?',
479
- options: [
480
- { value: 'yes', label: 'Yes, continue', hint: 'May prompt for your password' },
481
- { value: 'no', label: 'No, fall back to settings.json', hint: 'Deny list stored in editable user settings instead' },
482
- ],
483
- });
484
- if (p.isCancel(sudoChoice)) {
485
- p.cancel('Installation cancelled.');
486
- process.exit(0);
487
- }
488
- if (sudoChoice === 'yes') {
489
- const managed = await installManagedSettings(rootDir, verbose);
490
- if (!managed) {
491
- p.log.warn('Managed settings write failed — falling back to user settings');
492
- effectiveSecurityMode = 'user';
493
- }
494
- }
495
- else {
496
- effectiveSecurityMode = 'user';
497
- }
498
- }
499
- await installSettings(claudeDir, rootDir, devflowDir, verbose, teamsEnabled, effectiveSecurityMode);
500
- // Install ambient hook if enabled
586
+ await installSettings(claudeDir, rootDir, devflowDir, verbose, teamsEnabled, effectiveSecurityMode);
587
+ const settingsPath = path.join(claudeDir, 'settings.json');
588
+ // Configure ambient hook, memory hooks, and HUD statusLine in a single read-modify-write pass
589
+ try {
590
+ let content = await fs.readFile(settingsPath, 'utf-8');
591
+ const original = content;
592
+ // Ambient hook
501
593
  if (ambientEnabled) {
502
- const settingsPath = path.join(claudeDir, 'settings.json');
503
- try {
504
- const content = await fs.readFile(settingsPath, 'utf-8');
505
- const updated = addAmbientHook(content, devflowDir);
506
- if (updated !== content) {
507
- await fs.writeFile(settingsPath, updated, 'utf-8');
508
- if (verbose) {
509
- p.log.success('Ambient mode hook installed');
510
- }
511
- }
594
+ content = addAmbientHook(content, devflowDir);
595
+ }
596
+ // Memory hooks — always remove-then-add to upgrade hook format (e.g., .sh → run-hook)
597
+ const cleaned = removeMemoryHooks(content);
598
+ content = memoryEnabled ? addMemoryHooks(cleaned, devflowDir) : cleaned;
599
+ // HUD statusLine
600
+ content = hudEnabled
601
+ ? addHudStatusLine(content, devflowDir)
602
+ : removeHudStatusLine(content);
603
+ if (content !== original) {
604
+ await fs.writeFile(settingsPath, content, 'utf-8');
605
+ if (verbose) {
606
+ if (ambientEnabled)
607
+ p.log.success('Ambient mode hook installed');
608
+ p.log.info(`Working memory ${memoryEnabled ? 'enabled' : 'disabled'}`);
609
+ p.log.info(`HUD ${hudEnabled ? 'enabled' : 'disabled'}`);
512
610
  }
513
- catch { /* settings.json may not exist yet */ }
514
611
  }
515
- // Manage memory hooks based on user choice
516
- const settingsPath = path.join(claudeDir, 'settings.json');
517
- try {
518
- const content = await fs.readFile(settingsPath, 'utf-8');
519
- // Always remove-then-add to upgrade hook format (e.g., .sh → run-hook)
520
- const cleaned = removeMemoryHooks(content);
521
- const updated = memoryEnabled
522
- ? addMemoryHooks(cleaned, devflowDir)
523
- : cleaned;
524
- if (updated !== content) {
525
- await fs.writeFile(settingsPath, updated, 'utf-8');
526
- if (verbose) {
527
- p.log.info(`Working memory ${memoryEnabled ? 'enabled' : 'disabled'}`);
528
- }
612
+ }
613
+ catch { /* settings.json may not exist yet */ }
614
+ // Ensure .memory/ exists when memory is enabled (hooks are no-ops without it)
615
+ if (memoryEnabled) {
616
+ await createMemoryDir(verbose);
617
+ await migrateMemoryFiles(verbose);
618
+ }
619
+ // Configure HUD
620
+ const existingHud = loadHudConfig();
621
+ saveHudConfig({ enabled: hudEnabled, detail: existingHud.detail });
622
+ // File extras
623
+ if (claudeignoreEnabled) {
624
+ if (scope === 'user' && discoveredProjects.length > 0) {
625
+ const results = await Promise.all(discoveredProjects.map(root => installClaudeignore(root, rootDir, verbose)));
626
+ const created = results.filter(Boolean).length;
627
+ if (created > 0) {
628
+ p.log.success(`.claudeignore created in ${created} project(s)`);
529
629
  }
530
- }
531
- catch { /* settings.json may not exist yet */ }
532
- // Ensure .memory/ exists when memory is enabled (hooks are no-ops without it)
533
- if (memoryEnabled) {
534
- await createMemoryDir(verbose);
535
- await migrateMemoryFiles(verbose);
536
- }
537
- // Configure HUD
538
- const existingHud = loadHudConfig();
539
- saveHudConfig({ enabled: hudEnabled, detail: existingHud.detail });
540
- // Update statusLine in settings.json (add or remove based on choice)
541
- try {
542
- const hudContent = await fs.readFile(settingsPath, 'utf-8');
543
- const hudUpdated = hudEnabled
544
- ? addHudStatusLine(hudContent, devflowDir)
545
- : removeHudStatusLine(hudContent);
546
- if (hudUpdated !== hudContent) {
547
- await fs.writeFile(settingsPath, hudUpdated, 'utf-8');
548
- if (verbose) {
549
- p.log.info(`HUD ${hudEnabled ? 'enabled' : 'disabled'}`);
550
- }
630
+ else {
631
+ p.log.info(`.claudeignore already exists in all ${discoveredProjects.length} project(s)`);
551
632
  }
552
633
  }
553
- catch { /* settings.json may not exist yet */ }
554
- }
555
- const fileExtras = selectedExtras.filter(e => e !== 'settings' && e !== 'safe-delete');
556
- if (fileExtras.length > 0) {
557
- const sExtras = p.spinner();
558
- sExtras.start('Configuring extras');
559
- if (selectedExtras.includes('claudeignore') && gitRoot) {
634
+ else if (gitRoot) {
560
635
  await installClaudeignore(gitRoot, rootDir, verbose);
561
636
  }
562
- if (selectedExtras.includes('gitignore') && gitRoot) {
563
- await updateGitignore(gitRoot, verbose);
564
- }
565
- if (selectedExtras.includes('docs')) {
566
- await createDocsStructure(verbose);
637
+ }
638
+ if (scope === 'local' && gitRoot) {
639
+ await updateGitignore(gitRoot, verbose);
640
+ }
641
+ if (scope === 'local') {
642
+ await createDocsStructure(verbose);
643
+ }
644
+ // Safe-delete execution (decision was captured during prompt phase)
645
+ if (safeDeleteAction === 'install' && safeDeleteBlock && profilePath) {
646
+ await installToProfile(profilePath, safeDeleteBlock);
647
+ }
648
+ else if (safeDeleteAction === 'upgrade' && safeDeleteBlock && profilePath) {
649
+ await removeFromProfile(profilePath);
650
+ await installToProfile(profilePath, safeDeleteBlock);
651
+ }
652
+ // Managed settings (last install step — sudo may prompt for password)
653
+ if (securityMode === 'managed' && managedSettingsConfirmed) {
654
+ s.stop('Installing managed settings (may prompt for password)');
655
+ const managed = await installManagedSettings(rootDir, verbose);
656
+ if (!managed) {
657
+ p.log.warn('Managed settings write failed — falling back to user settings');
567
658
  }
568
- sExtras.stop('Extras configured');
659
+ s.start('Finalizing');
569
660
  }
570
- // Summary output
661
+ s.stop('Installation complete');
662
+ // === Summary ===
571
663
  if (usedNativeCli) {
572
664
  p.log.success('Installed via Claude plugin system');
573
665
  }
@@ -581,54 +673,34 @@ export const initCommand = new Command('init')
581
673
  .join(' ');
582
674
  p.note(commandsNote, 'Available commands');
583
675
  }
584
- // Safe-delete auto-install (gated by extras selection)
585
- if (selectedExtras.includes('safe-delete')) {
586
- const platform = detectPlatform();
587
- const shell = detectShell();
588
- const safeDeleteInfo = getSafeDeleteInfo(platform);
589
- const safeDeleteAvailable = hasSafeDelete(platform);
590
- const profilePath = getProfilePath(shell);
591
- if (process.stdin.isTTY && profilePath) {
592
- if (!safeDeleteAvailable && safeDeleteInfo.installHint) {
593
- p.log.info(`Install ${color.cyan(safeDeleteInfo.command ?? 'trash')} first: ${color.dim(safeDeleteInfo.installHint)}`);
594
- p.log.info(`Then re-run ${color.cyan('devflow init')} to auto-configure safe-delete.`);
595
- }
596
- else if (safeDeleteAvailable) {
597
- const trashCmd = safeDeleteInfo.command;
598
- const block = generateSafeDeleteBlock(shell, process.platform, trashCmd);
599
- if (block) {
600
- const installedVersion = await getInstalledVersion(profilePath);
601
- if (installedVersion === SAFE_DELETE_BLOCK_VERSION) {
602
- p.log.info(`Safe-delete already configured in ${color.dim(profilePath)}`);
603
- }
604
- else if (installedVersion > 0) {
605
- await removeFromProfile(profilePath);
606
- await installToProfile(profilePath, block);
607
- p.log.success(`Safe-delete upgraded in ${color.dim(profilePath)}`);
608
- p.log.info('Restart your shell or run: ' + color.cyan(`source ${profilePath}`));
609
- }
610
- else {
611
- const confirm = await p.confirm({
612
- message: `Install safe-delete to ${profilePath}? (overrides rm to use ${trashCmd ?? 'recycle bin'})`,
613
- initialValue: true,
614
- });
615
- if (!p.isCancel(confirm) && confirm) {
616
- await installToProfile(profilePath, block);
617
- p.log.success(`Safe-delete installed to ${color.dim(profilePath)}`);
618
- p.log.info('Restart your shell or run: ' + color.cyan(`source ${profilePath}`));
619
- }
620
- }
621
- }
622
- }
676
+ // Safe-delete status messages (after spinner)
677
+ if (process.stdin.isTTY && profilePath) {
678
+ if (safeDeleteAction === 'install') {
679
+ p.log.success(`Safe-delete installed to ${color.dim(profilePath)}`);
680
+ p.log.info('Restart your shell or run: ' + color.cyan(`source ${profilePath}`));
623
681
  }
624
- else if (!process.stdin.isTTY) {
625
- if (safeDeleteAvailable && safeDeleteInfo.command) {
626
- p.log.info(`Safe-delete available (${safeDeleteInfo.command}). Run interactively to auto-install.`);
627
- }
628
- else if (safeDeleteInfo.installHint) {
629
- p.log.info(`Protect against accidental ${color.red('rm -rf')}: ${color.cyan(safeDeleteInfo.installHint)}`);
682
+ else if (safeDeleteAction === 'upgrade') {
683
+ p.log.success(`Safe-delete upgraded in ${color.dim(profilePath)}`);
684
+ p.log.info('Restart your shell or run: ' + color.cyan(`source ${profilePath}`));
685
+ }
686
+ else if (safeDeleteAvailable && safeDeleteBlock) {
687
+ const installedVersion = await getInstalledVersion(profilePath);
688
+ if (installedVersion === SAFE_DELETE_BLOCK_VERSION) {
689
+ p.log.info(`Safe-delete already configured in ${color.dim(profilePath)}`);
630
690
  }
631
691
  }
692
+ else if (!safeDeleteAvailable && safeDeleteInfo.installHint) {
693
+ p.log.info(`Install ${color.cyan(safeDeleteInfo.command ?? 'trash')} first: ${color.dim(safeDeleteInfo.installHint)}`);
694
+ p.log.info(`Then re-run ${color.cyan('devflow init')} to auto-configure safe-delete.`);
695
+ }
696
+ }
697
+ else if (!process.stdin.isTTY) {
698
+ if (safeDeleteAvailable && safeDeleteInfo.command) {
699
+ p.log.info(`Safe-delete available (${safeDeleteInfo.command}). Run interactively to auto-install.`);
700
+ }
701
+ else if (safeDeleteInfo.installHint) {
702
+ p.log.info(`Protect against accidental ${color.red('rm -rf')}: ${color.cyan(safeDeleteInfo.installHint)}`);
703
+ }
632
704
  }
633
705
  // Verbose mode: show details
634
706
  if (verbose) {
@@ -55,8 +55,15 @@ export type SecurityMode = 'managed' | 'user';
55
55
  export declare function installSettings(claudeDir: string, rootDir: string, devflowDir: string, verbose: boolean, teamsEnabled?: boolean, securityMode?: SecurityMode): Promise<void>;
56
56
  /**
57
57
  * Create .claudeignore in git repository root (skip if already exists).
58
+ * Returns true if a new file was created, false if it already existed or on error.
58
59
  */
59
- export declare function installClaudeignore(gitRoot: string, rootDir: string, verbose: boolean): Promise<void>;
60
+ export declare function installClaudeignore(gitRoot: string, rootDir: string, verbose: boolean): Promise<boolean>;
61
+ /**
62
+ * Discover git repository roots from Claude's project history.
63
+ * Parses ~/.claude/history.jsonl for unique project paths that are valid git repos.
64
+ * @param homeDir - Override home directory (dependency injection for tests)
65
+ */
66
+ export declare function discoverProjectGitRoots(homeDir?: string): Promise<string[]>;
60
67
  /**
61
68
  * Update .gitignore with DevFlow entries (for local scope installs).
62
69
  */
@@ -1,6 +1,7 @@
1
1
  import { promises as fs, writeFileSync, unlinkSync } from 'fs';
2
2
  import { execSync } from 'child_process';
3
3
  import * as path from 'path';
4
+ import * as os from 'os';
4
5
  import * as p from '@clack/prompts';
5
6
  import { getManagedSettingsPath } from './paths.js';
6
7
  function isNodeSystemError(error) {
@@ -353,6 +354,7 @@ export async function installSettings(claudeDir, rootDir, devflowDir, verbose, t
353
354
  }
354
355
  /**
355
356
  * Create .claudeignore in git repository root (skip if already exists).
357
+ * Returns true if a new file was created, false if it already existed or on error.
356
358
  */
357
359
  export async function installClaudeignore(gitRoot, rootDir, verbose) {
358
360
  const claudeignorePath = path.join(gitRoot, '.claudeignore');
@@ -363,6 +365,7 @@ export async function installClaudeignore(gitRoot, rootDir, verbose) {
363
365
  if (verbose) {
364
366
  p.log.success('.claudeignore created');
365
367
  }
368
+ return true;
366
369
  }
367
370
  catch (error) {
368
371
  if (isNodeSystemError(error) && error.code === 'EEXIST') {
@@ -371,7 +374,45 @@ export async function installClaudeignore(gitRoot, rootDir, verbose) {
371
374
  else if (verbose) {
372
375
  p.log.warn(`Could not create .claudeignore: ${error}`);
373
376
  }
377
+ return false;
378
+ }
379
+ }
380
+ /**
381
+ * Discover git repository roots from Claude's project history.
382
+ * Parses ~/.claude/history.jsonl for unique project paths that are valid git repos.
383
+ * @param homeDir - Override home directory (dependency injection for tests)
384
+ */
385
+ export async function discoverProjectGitRoots(homeDir) {
386
+ const historyPath = path.join(homeDir ?? os.homedir(), '.claude', 'history.jsonl');
387
+ let content;
388
+ try {
389
+ content = await fs.readFile(historyPath, 'utf-8');
390
+ }
391
+ catch {
392
+ return [];
393
+ }
394
+ const projects = new Set();
395
+ for (const line of content.split('\n')) {
396
+ if (!line.trim())
397
+ continue;
398
+ try {
399
+ const entry = JSON.parse(line);
400
+ if (typeof entry?.project === 'string') {
401
+ projects.add(path.resolve(entry.project));
402
+ }
403
+ }
404
+ catch {
405
+ // Malformed line — skip
406
+ }
374
407
  }
408
+ const results = await Promise.allSettled([...projects].map(async (project) => {
409
+ await fs.access(path.join(project, '.git'));
410
+ return project;
411
+ }));
412
+ const gitRoots = results
413
+ .filter((r) => r.status === 'fulfilled')
414
+ .map((r) => r.value);
415
+ return gitRoots.sort();
375
416
  }
376
417
  /**
377
418
  * Update .gitignore with DevFlow entries (for local scope installs).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devflow-kit",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "Agentic Development Toolkit for Claude Code - Enhance AI-assisted development with intelligent commands and workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Dean0x"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.8.1",
8
8
  "homepage": "https://github.com/dean0x/devflow",
9
9
  "repository": "https://github.com/dean0x/devflow",
10
10
  "license": "MIT",