feat(init): add interactive config wizard for first-time setup#169
feat(init): add interactive config wizard for first-time setup#169snipcodeit merged 2 commits intomainfrom
Conversation
Adds lib/config-wizard.cjs with runWizard() and shouldRunWizard() helpers. Updates commands/init.md with a new run_config_wizard step that runs after GitHub setup and before the completion report. The wizard prompts for GitHub username (auto-detected), default issue state filter, default issue limit, and default assignee filter, then writes answers to .mgw/config.json. Skippable via --no-config flag or automatically bypassed when .mgw/config.json already exists. Closes #125 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
snipcodeit
left a comment
There was a problem hiding this comment.
Review: PR #169 — interactive config wizard
lib/config-wizard.cjs is clean and well-structured. shouldRunWizard and detectGitHubUsername are the right abstractions. Two blocking issues before merge.
🔴 runWizard hangs in non-interactive (CI/piped) mode
runWizard creates a readline interface with terminal: true without checking process.stdin.isTTY. In any non-interactive context (CI, piped stdin, mgw:init < /dev/null), readline will:
- Start waiting for input from a closed/non-TTY stream
- Potentially hang indefinitely (depending on Node version and platform)
- Or throw/emit an error that crashes init
shouldRunWizard checks --no-config and pre-existing config but does NOT check process.stdin.isTTY. This means:
- CI runs that do not pass
--no-configwill attempt to launch the wizard - The wizard will hang waiting for input that never comes
Fix shouldRunWizard to also check TTY:
function shouldRunWizard(mgwDir, argv) {
const args = argv || process.argv;
if (args.includes("--no-config")) return false;
if (!process.stdin.isTTY) return false; // ← add this
const configPath = path.join(mgwDir, "config.json");
if (fs.existsSync(configPath)) return false;
return true;
}And in runWizard, add a guard at the top:
async function runWizard(mgwDir) {
if (!process.stdin.isTTY) {
throw new Error("runWizard called in non-interactive mode");
}
...
}🔴 Merge conflict with PR #168
Both this PR and PR #168 modify commands/init.md. They add different steps (run_config_wizard here, install_completions in #168) and both update the <report> section and <success_criteria>. They will conflict when merged sequentially.
The two PRs need to coordinate on merge order and the second merge must manually reconcile both changes. This should be documented in both PR descriptions.
🟡 No tests for lib/config-wizard.cjs
shouldRunWizard and detectGitHubUsername are pure/side-effect-free functions that should have unit tests in test/config-wizard.test.cjs:
describe("shouldRunWizard", () => {
it("returns false when --no-config in argv");
it("returns false when config.json already exists");
it("returns true when no config and no --no-config flag");
// Note: isTTY check requires mocking process.stdin
});
describe("detectGitHubUsername", () => {
it("returns null when gh command fails");
// Full test requires mocking execSync
});Without tests, the edge cases (especially the --no-config flag matching) have no coverage.
🟡 readline with terminal: true forces echo even in piped contexts
Even if the TTY check is added, the terminal: true option forces readline to treat output as a terminal and emit ANSI escape sequences for line editing. This can corrupt output when stdout is being captured. Consider using terminal: false or detecting the appropriate value:
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: process.stdout.isTTY, // use TTY mode only when stdout supports it
});✅ Good points
shouldRunWizardas a separate exported function is testable and composabledetectGitHubUsernameusingstdio: ["pipe", "pipe", "pipe"]prevents inheriting parent stderr — correct- Config written with
JSON.stringify(config, null, 2) + "\n"— proper formatted JSON with trailing newline created_attimestamp in config is useful for debugging and upgrade paths- Error handling: wizard errors do not abort the overall init — correct design
promptChoicewith numbered list + "(default)" marker is good UX
- shouldRunWizard: return false early when process.stdin.isTTY is falsy (prevents interactive prompts in piped/automated contexts) - runWizard: throw at entry when stdin is not a TTY - readline.createInterface: use terminal: process.stdout.isTTY instead of terminal: true to suppress ANSI escapes when stdout is piped - commands/init.md: add merge note for conflict with PR #124 - test/config-wizard.test.cjs: 8 unit tests covering shouldRunWizard all exit paths and detectGitHubUsername failure path
Summary
lib/config-wizard.cjswithrunWizard()andshouldRunWizard()— the complete wizard implementation using Node.jsreadlinegh api user), default issue state filter, default issue limit, and default assignee filter, then writes.mgw/config.jsoncommands/init.mdwith a newrun_config_wizardstep inserted after the GitHub setup and before the completion report--no-configflag or automatically bypassed when.mgw/config.jsonalready existsCloses #125
Milestone Context
Changes
lib/config-wizard.cjs(new) — wizard logic:shouldRunWizard()checks for--no-configflag and pre-existing config;runWizard()drives the interactive prompt session and writes.mgw/config.json;detectGitHubUsername()auto-detects viagh api usercommands/init.md— addedrun_config_wizardstep with full pseudocode; updatedargument-hint, objective description, report layout, and success criteria to reflect config.json outputTest Plan
mgw:initin a fresh repo with no.mgw/config.json— wizard should prompt all four questions and write.mgw/config.jsonmgw:init --no-config— wizard should be skipped entirely, report showsskippedmgw:initwhen.mgw/config.jsonalready exists — wizard should be skipped, report showsexistsshouldRunWizard()returnsfalsewhen--no-configis in argvshouldRunWizard()returnsfalsewhen config file existsdetectGitHubUsername()returns null gracefully whenghis not authenticated