feat(init): enhance flow with project discovery and improved return types#157
feat(init): enhance flow with project discovery and improved return types#157
Conversation
…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> = { |
There was a problem hiding this comment.
[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
src/cli/utils/post-install.ts
Outdated
| if (!line.trim()) continue; | ||
| try { | ||
| const entry = JSON.parse(line); | ||
| if (typeof entry.project === 'string') { |
There was a problem hiding this comment.
[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
src/cli/utils/post-install.ts
Outdated
| } | ||
| } | ||
|
|
||
| const gitRoots: string[] = []; |
There was a problem hiding this comment.
[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) => { |
There was a problem hiding this comment.
[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?', |
There was a problem hiding this comment.
[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) => { |
There was a problem hiding this comment.
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:
- Move pluginHints to PluginDefinition (prevents drift)
- Add path.resolve() to security path validation
- Parallelize fs.access and installClaudeignore operations
- Extract handler into named phases (collectInitChoices/executeInstallation)
- 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
… 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.
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
~/.claude/history.jsonland find all valid git repositories Claude has worked onbooleanto track success (true if newly created, false if already existed or error)discoverProjectGitRootsfor test accessibility and clean up unused importsBreaking Changes
None — all changes are backward compatible or internal refactoring.
Testing
Related Issues
Closes discovery and batch configuration requirements for user-scope installs.