Skip to content

feat(init): enhance flow with project discovery and improved return types#157

Merged
dean0x merged 5 commits intomainfrom
chore/init-flow-improvements
Mar 22, 2026
Merged

feat(init): enhance flow with project discovery and improved return types#157
dean0x merged 5 commits intomainfrom
chore/init-flow-improvements

Conversation

@dean0x
Copy link
Owner

@dean0x dean0x commented Mar 22, 2026

Summary

Refactor initialization flow to enable batch .claudeignore installation across all discovered projects. This improves the user experience for multi-project setups by automating discovery and consistent configuration.

Changes

  • discoverProjectGitRoots() — New function to parse ~/.claude/history.jsonl and find all valid git repositories Claude has worked on
  • installClaudeignore() — Now returns boolean to track success (true if newly created, false if already existed or error)
  • init.ts — Re-export discoverProjectGitRoots for test accessibility and clean up unused imports
  • Tests — Updated to reflect API changes and removed dead code paths

Breaking Changes

None — all changes are backward compatible or internal refactoring.

Testing

  • All existing tests pass (134/134)
  • New behavior validated with test fixtures
  • Batch installation path tested with multiple projects

Related Issues

Closes discovery and batch configuration requirements for user-scope installs.

…ypes

Refactor init flow to support batch .claudeignore installation across
discovered projects:

- Add discoverProjectGitRoots() to parse ~/.claude/history.jsonl for
  valid git repositories
- Change installClaudeignore() to return boolean (true if newly created)
  to support batch progress tracking
- Re-export discoverProjectGitRoots from init.ts for test accessibility
- Clean up unused imports and dead exports (buildExtrasOptions, has*
  checks for hooks)
- Update tests to reflect API changes and removal of dead code

This enables user-scope installations to automatically discover and
configure all projects Claude has worked on with a single prompt.

Co-Authored-By: Claude <noreply@anthropic.com>
}
} else if (process.stdin.isTTY) {
// Short hints to prevent overflow in multiselect — full descriptions live in plugins.ts
const pluginHints: Record<string, string> = {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING - 85% confidence] Hardcoded pluginHints map creates DRY violation and drift risk

This map duplicates plugin descriptions that already live in plugins.ts as PluginDefinition.description. If a plugin is added, renamed, or its description changes, this map can silently fall out of sync. The fallback ?? pl.description masks the drift.

Fix: Add a shortHint field to PluginDefinition in plugins.ts so hints stay co-located with their source of truth:

// plugins.ts
interface PluginDefinition {
  name: string;
  description: string;
  shortHint?: string;  // Short hint for multiselect UI
}

// init.ts - replace hardcoded map:
hint: pl.shortHint ?? pl.description,

This enforces the Open/Closed Principle — adding a new plugin should only require changes in one place.

— Claude Code

if (!line.trim()) continue;
try {
const entry = JSON.parse(line);
if (typeof entry.project === 'string') {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING - 82% confidence] Unvalidated paths from history.jsonl used as write targets

The discoverProjectGitRoots function reads paths from ~/.claude/history.jsonl and passes them directly to installClaudeignore which writes files. While history.jsonl is user-local (not attacker-controlled), paths are not normalized or validated. A corrupted or hand-edited entry like /etc or /tmp/../../etc could cause file creation in unexpected directories. The wx flag prevents overwrites, but risk remains.

Fix: Add path normalization and validation:

for (const project of projects) {
  const resolved = path.resolve(project);
  try {
    await fs.access(path.join(resolved, '.git'));
    gitRoots.push(resolved);
  } catch {
    // Not a git repo or doesn't exist — skip
  }
}

The path.resolve()" normalizes relative paths and prevents path traversal sequences. The .git" check acts as an implicit allowlist gate.

— Claude Code

}
}

const gitRoots: string[] = [];
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING - 85% confidence] Sequential fs.access calls in loop scale poorly with project count

The for..of loop over discovered projects performs sequential filesystem stat calls, each taking ~1-5ms. For users with 50+ projects, this adds 50-250ms of blocking I/O before the first prompt appears — visible as a hang with no feedback.

Fix: Parallelize with Promise.allSettled:

const results = await Promise.allSettled(
  [...projects].map(async (project) => {
    await fs.access(path.join(project, '.git'));
    return project;
  })
);
const gitRoots = results
  .filter((r): r is PromiseFulfilledResult<string> => r.status === 'fulfilled')
  .map(r => r.value);
return gitRoots.sort();

This parallelizes I/O while handling failures gracefully (skips non-git paths).

— Claude Code

.option('--hud', 'Enable HUD (git info, context usage, session stats)')
.option('--no-hud', 'Disable HUD status line')
.option('--hud-only', 'Install only the HUD (no plugins, hooks, or extras)')
.action(async (options: InitOptions) => {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING - 95% confidence] Action handler monolith exceeds all complexity thresholds

The .action(async ...) handler spans ~765 lines with estimated cyclomatic complexity ~176 (up from ~145 on main). It handles prompts, path resolution, plugin installation, settings configuration, safe-delete, memory, HUD, ambient, claudeignore discovery, and summary output — 10+ distinct responsibilities in one closure.

Standard thresholds: >200 lines = CRITICAL, >20 complexity = CRITICAL.

The PR already introduces a clear architectural boundary with the // All prompts collected — installation begins comment. This naturally splits into extractable functions.

Fix: Extract into three named phases:

interface InitChoices {
  scope: 'user' | 'local';
  selectedPlugins: string[];
  teamsEnabled: boolean;
  ambientEnabled: boolean;
  memoryEnabled: boolean;
  hudEnabled: boolean;
  securityMode: SecurityMode;
  managedSettingsConfirmed: boolean;
  claudeignoreEnabled: boolean;
  discoveredProjects: string[];
  safeDeleteAction: 'install' | 'upgrade' | 'skip';
  safeDeleteBlock: string | null;
}

async function collectInitChoices(options: InitOptions, version: string): Promise<InitChoices> { ... }
async function executeInstallation(choices: InitChoices, version: string): Promise<void> { ... }
function printSummary(choices: InitChoices, ...): void { ... }

// In .action():
const choices = await collectInitChoices(options, version);
await executeInstallation(choices, version);
printSummary(choices, ...);

This makes the orchestration logic testable and brings the file under the 500-line threshold.

— Claude Code

'Agent Teams',
);
const teamsChoice = await p.select({
message: 'Enable Agent Teams?',
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING - 92% confidence] Repetitive cancel-check boilerplate (12 occurrences)

The identical 3-line pattern repeats 12 times:

if (p.isCancel(result)) {
  p.cancel('Installation cancelled.');
  process.exit(0);
}

This inflates cyclomatic complexity and code size. Extract a helper:

function cancelGuard<T>(result: T | symbol): T {
  if (p.isCancel(result)) {
    p.cancel('Installation cancelled.');
    process.exit(0);
  }
  return result as T;
}

Then use it at each prompt: const choice = cancelGuard(await p.select(...)). This reduces ~36 lines of boilerplate to a single definition + usage pattern, cutting complexity by ~12 points.

— Claude Code

.option('--hud', 'Enable HUD (git info, context usage, session stats)')
.option('--no-hud', 'Disable HUD status line')
.option('--hud-only', 'Install only the HUD (no plugins, hooks, or extras)')
.action(async (options: InitOptions) => {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary: Code Review Complete

PR #157: feat(init): enhance flow with project discovery and improved return types

Inline Comments Posted (≥80% confidence)

  • pluginHints hardcoded map (85% confidence) — DRY violation, move to PluginDefinition
  • Unvalidated paths from history.jsonl (82% confidence) — Add path.resolve() normalization
  • Sequential fs.access in discovery loop (85% confidence) — Use Promise.allSettled
  • Action handler monolith (95% confidence) — Extract into collectInitChoices/executeInstallation/printSummary
  • Cancel-check boilerplate (92% confidence) — Extract cancelGuard helper

Should-Fix Issues (80-89% confidence, recommended for follow-up)

  • discoverProjectGitRoots not injectable (82%) — Accept optional homedir parameter
  • Sequential installClaudeignore batch (82%) — Use Promise.all instead of for loop
  • Three reads of settings.json (80-84%) — Consolidate into single read/transform/write
  • Untyped JSON.parse (82%) — Use unknown with type guard
  • Inconsistent Recommended signaling (85%) — Apply consistent messaging pattern

Documentation Issues (80-88% confidence)

  • CHANGELOG not updated (85%) — Add entries for init flow changes, --hud flag, project discovery
  • README missing --hud flag (88%) — Add to CLI options table
  • discoverProjectGitRoots missing JSDoc (82%) — Document @returns

Pre-existing Issues Not Blocking

  • Untyped JSON.parse throughout post-install.ts — codebase-wide pattern, low priority
  • Shell injection pattern in managed settings — pre-existing, address separately
  • Re-export barrel in init.ts — low layering concern

Overall Assessment

Recommendation: CHANGES_REQUESTED

The PR's goal (moving prompts before installation) is architecturally sound and improves UX. The new discoverProjectGitRoots function and project discovery feature are well-implemented. However, 5 blocking issues must be resolved before merge:

  1. Move pluginHints to PluginDefinition (prevents drift)
  2. Add path.resolve() to security path validation
  3. Parallelize fs.access and installClaudeignore operations
  4. Extract handler into named phases (collectInitChoices/executeInstallation)
  5. Consolidate cancel-check boilerplate

The documentation updates (CHANGELOG, README, JSDoc) should also be included.

Estimated effort: 3-4 hours for blocking fixes + docs + regression testing.

— Claude Code

Dean Sharon and others added 4 commits March 22, 2026 10:36
… perf

- Type JSON.parse result as unknown with proper type guard instead of any
- Normalize history.jsonl paths with path.resolve() before use
- Parallelize fs.access checks with Promise.allSettled

Co-Authored-By: Claude <noreply@anthropic.com>
- Populate [Unreleased] section with init flow changes (individual
  feature prompts, project discovery, batch .claudeignore, removed
  extras multiselect)
- Add --hud / --no-hud row to README init flags table
- Add optional homeDir parameter to discoverProjectGitRoots for
  dependency injection, replacing process.env.HOME mutation in tests

Co-Authored-By: Claude <noreply@anthropic.com>
Security deny list and sudo password prompt now appear after all
other choices (safe-delete, .claudeignore, etc.) so the password
entry doesn't interrupt the flow mid-wizard.
@dean0x dean0x merged commit d10f245 into main Mar 22, 2026
3 of 4 checks passed
@dean0x dean0x deleted the chore/init-flow-improvements branch March 22, 2026 10:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant