Skip to content

Commit dc8a1e6

Browse files
Copilotclaude
andcommitted
feat: add --quiet flag for scriptable, low-noise runs
`bootcamp <repo-url> --quiet` (`-q`) suppresses the banner, run header, detected-stack table, progress spinners, score summary, and file-tree listing, emitting only the output directory path on stdout. This makes the command compose cleanly in scripts and CI: OUT=$(bootcamp https://github.com/owner/repo --quiet) open "$OUT/BOOTCAMP.md" Failures and warnings are still written to stderr so problems remain visible. `--quiet` and `--verbose` are mutually exclusive and error out together. Implementation: - BootcampOptions.quiet; ProgressTracker gains a quiet mode (silent spinner via ora isSilent, but fail/warn still print to stderr) - main-command gates all decorative console output behind !quiet - agent.ts "Using model"/tools banner and output-writer diagram messages honor quiet too Tests: 4 ProgressTracker unit tests (silence + stderr fail/warn), 1 main-command behavior test (banner suppressed, output dir printed), 2 E2E tests (quiet stdout is just the path; --quiet --verbose rejected). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9887363 commit dc8a1e6

11 files changed

Lines changed: 421 additions & 57 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `--quiet`/`-q` flag for the main `bootcamp <repo-url>` command: suppresses the banner, run header, detected-stack table, progress spinners, score summary, and file-tree listing, printing only the output directory path on stdout so the command composes cleanly in scripts and CI (`OUT=$(bootcamp <url> --quiet)`). Failures and warnings are still written to stderr. Mutually exclusive with `--verbose`.
1213
- `bootcamp styles` command (alias `style`) to list the built-in style packs and the documentation sections each one enables, so users can choose a `--style` without reading the source. Prints a per-pack summary (tone, depth, emoji, first-task count, enabled sections) plus a section-coverage matrix, flags the default pack (`oss`), and supports `--json` for scripting.
1314
- `bootcamp init` command to scaffold a `.bootcamprc.json` configuration file in the current directory. Refuses to overwrite an existing config unless `--force`, supports `--print` to preview the config on stdout without writing, `--path` for a custom output location, and `--style` to preset a built-in style pack.
1415
- `bootcamp health <repo-url>` command: a standalone, deterministic onboarding-readiness score for any repository (local path or remote URL) without invoking the LLM. Prints a human-readable report by default, supports `--json` for scripting, and offers a CI gate via `--check`/`--min-score` (exits non-zero when the score falls below the threshold). Reuses the same `computeRepoHealth` engine that powers `HEALTH.md`.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ Onboarding Risk: 18/100 (A) 🟢
146146
| Generated files | 14+ |
147147
| Test suite | 1,000+ tests |
148148
| Source files | 48 TypeScript modules |
149-
| Test files | 70 Vitest files |
149+
| Test files | 71 Vitest files |
150150
| Lines of code | 13,381 TypeScript LOC (src/) |
151151
| Languages supported | 10+ |
152152
| Generation time | < 60 seconds |
@@ -569,6 +569,7 @@ npm install -g @mermaid-js/mermaid-cli
569569
| `--watch-force` | Allow destructive `git reset --hard` fallback in watch mode | false |
570570
| `--stats` | Show detailed statistics | false |
571571
| `-v, --verbose` | Show tool calls and reasoning | false |
572+
| `-q, --quiet` | Suppress banner/progress; print only the output path (scripting/CI) | false |
572573

573574
## Commands
574575

src/agent.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -932,8 +932,10 @@ export async function analyzeRepo(
932932
);
933933

934934
stats.model = model;
935-
console.log(chalk.blue(`\nUsing model: ${model}`));
936-
console.log(chalk.yellow(`⚡ Fast mode: no tools, inline file contents\n`));
935+
if (!options.quiet) {
936+
console.log(chalk.blue(`\nUsing model: ${model}`));
937+
console.log(chalk.yellow(`⚡ Fast mode: no tools, inline file contents\n`));
938+
}
937939

938940
const prompt = createFastAnalysisPrompt(
939941
repoPath,
@@ -1029,8 +1031,10 @@ export async function analyzeRepo(
10291031
);
10301032

10311033
stats.model = model;
1032-
console.log(chalk.blue(`\nUsing model: ${model}`));
1033-
console.log(chalk.gray(`Tools available: ${tools.map((t) => t.name).join(", ")}\n`));
1034+
if (!options.quiet) {
1035+
console.log(chalk.blue(`\nUsing model: ${model}`));
1036+
console.log(chalk.gray(`Tools available: ${tools.map((t) => t.name).join(", ")}\n`));
1037+
}
10341038

10351039
const prompt = createAnalysisPrompt(
10361040
repoInfo,

src/cli.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ interface MainActionOptions {
5454
clone?: boolean;
5555
noClone?: boolean;
5656
verbose?: boolean;
57+
quiet?: boolean;
5758
model?: string;
5859
keepTemp?: boolean;
5960
jsonOnly?: boolean;
@@ -198,6 +199,7 @@ program
198199
.option("--json-only", "Only generate repo_facts.json, skip markdown docs")
199200
.option("--stats", "Show detailed statistics after generation")
200201
.option("-v, --verbose", "Show detailed progress including tool calls")
202+
.option("-q, --quiet", "Suppress banner, progress, and file tree; print only the output path (for scripting/CI)")
201203
.option("-i, --interactive", "Start interactive Q&A mode after generation")
202204
.option("--transcript", "Save interactive session transcript to TRANSCRIPT.md")
203205
.option("-c, --compare <ref>", "Compare with another git ref (tag, branch, commit)")
@@ -223,6 +225,7 @@ program
223225
maxFiles: parseInt(opts.maxFiles || "200", 10),
224226
noClone: isNegativeOptionEnabled(opts, "noClone", "clone"),
225227
verbose: opts.verbose || false,
228+
quiet: opts.quiet || false,
226229
model: opts.model,
227230
keepTemp: opts.keepTemp || false,
228231
jsonOnly: opts.jsonOnly || false,
@@ -251,6 +254,11 @@ program
251254
},
252255
};
253256

257+
if (options.quiet && options.verbose) {
258+
console.error(chalk.red("--quiet and --verbose are mutually exclusive."));
259+
process.exit(1);
260+
}
261+
254262
if (!["onboarding", "architecture", "contributing", "all"].includes(options.focus)) {
255263
console.error(chalk.red(`Invalid focus: ${options.focus}`));
256264
process.exit(1);

src/commands/main-command.ts

Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -99,36 +99,39 @@ async function generateOutputs({
9999
}
100100

101101
export async function runMainCommand(repoUrl: string, options: BootcampOptions): Promise<void> {
102-
const progress = new ProgressTracker(options.verbose);
102+
const quiet = options.quiet === true;
103+
const progress = new ProgressTracker(options.verbose, quiet);
103104
const runStats: Partial<RunStats> = {};
104105
const startTime = Date.now();
105106

106107
const { config, styleConfig, outputFormat } = await resolveRunConfiguration(options);
107108

108-
console.log(chalk.cyan(`
109+
if (!quiet) {
110+
console.log(chalk.cyan(`
109111
╦═╗╔═╗╔═╗╔═╗ ╔╗ ╔═╗╔═╗╔╦╗╔═╗╔═╗╔╦╗╔═╗
110112
╠╦╝║╣ ╠═╝║ ║ ╠╩╗║ ║║ ║ ║ ║ ╠═╣║║║╠═╝
111-
╩╚═╚═╝╩ ╚═╝ ╚═╝╚═╝╚═╝ ╩ ╚═╝╩ ╩╩ ╩╩
113+
╩╚═╚═╝╩ ╚═╝ ╚═╝╚═╝╚═╝ ╩ ╚═╝╩ ╩╩ ╩╩
112114
`));
113-
console.log(chalk.white.bold(" Turn any repo into a Day 1 onboarding kit\n"));
114-
115-
console.log(chalk.dim("─".repeat(50)));
116-
console.log(chalk.white(` Repository: ${chalk.cyan(repoUrl)}`));
117-
console.log(chalk.white(` Branch: ${chalk.cyan(options.branch || "default")}`));
118-
console.log(chalk.white(` Focus: ${chalk.cyan(options.focus)}`));
119-
console.log(chalk.white(` Audience: ${chalk.cyan(options.audience)}`));
120-
console.log(chalk.white(` Style: ${chalk.cyan(styleConfig.name)}`));
121-
console.log(chalk.white(` Format: ${chalk.cyan(outputFormat)}`));
122-
if (options.model) {
123-
console.log(chalk.white(` Model: ${chalk.cyan(options.model)}`));
124-
}
125-
if (options.compare) {
126-
console.log(chalk.white(` Compare: ${chalk.cyan(options.compare)}`));
127-
}
128-
console.log(chalk.dim("─".repeat(50)));
129-
console.log();
115+
console.log(chalk.white.bold(" Turn any repo into a Day 1 onboarding kit\n"));
116+
117+
console.log(chalk.dim("─".repeat(50)));
118+
console.log(chalk.white(` Repository: ${chalk.cyan(repoUrl)}`));
119+
console.log(chalk.white(` Branch: ${chalk.cyan(options.branch || "default")}`));
120+
console.log(chalk.white(` Focus: ${chalk.cyan(options.focus)}`));
121+
console.log(chalk.white(` Audience: ${chalk.cyan(options.audience)}`));
122+
console.log(chalk.white(` Style: ${chalk.cyan(styleConfig.name)}`));
123+
console.log(chalk.white(` Format: ${chalk.cyan(outputFormat)}`));
124+
if (options.model) {
125+
console.log(chalk.white(` Model: ${chalk.cyan(options.model)}`));
126+
}
127+
if (options.compare) {
128+
console.log(chalk.white(` Compare: ${chalk.cyan(options.compare)}`));
129+
}
130+
console.log(chalk.dim("─".repeat(50)));
131+
console.log();
130132

131-
ProgressTracker.printPhaseOverview();
133+
ProgressTracker.printPhaseOverview();
134+
}
132135

133136
let repoInfo: RepoInfo;
134137
let repoSource: RepoSource | null = null;
@@ -143,8 +146,10 @@ export async function runMainCommand(repoUrl: string, options: BootcampOptions):
143146
repoInfo = parseGitHubUrl(repoUrl);
144147
}
145148
const targetLabel = repoSource?.isLocal ? repoSource.path : repoInfo.fullName;
146-
console.log(chalk.white(`Target: ${chalk.bold(targetLabel)}`));
147-
console.log();
149+
if (!quiet) {
150+
console.log(chalk.white(`Target: ${chalk.bold(targetLabel)}`));
151+
console.log();
152+
}
148153
} catch (error: unknown) {
149154
console.error(chalk.red(`Failed to resolve repository: ${(error as Error).message}`));
150155
process.exit(1);
@@ -182,13 +187,15 @@ export async function runMainCommand(repoUrl: string, options: BootcampOptions):
182187
process.exit(1);
183188
}
184189

185-
console.log(chalk.cyan("\nDetected Stack:"));
186-
console.log(chalk.white(` Languages: ${scanResult.stack.languages.join(", ") || "Unknown"}`));
187-
console.log(chalk.white(` Frameworks: ${scanResult.stack.frameworks.join(", ") || "None"}`));
188-
console.log(chalk.white(` Build: ${scanResult.stack.buildSystem || "Unknown"}`));
189-
console.log(chalk.white(` CI: ${scanResult.stack.hasCi ? "Yes" : "No"}`));
190-
console.log(chalk.white(` Docker: ${scanResult.stack.hasDocker ? "Yes" : "No"}`));
191-
console.log();
190+
if (!quiet) {
191+
console.log(chalk.cyan("\nDetected Stack:"));
192+
console.log(chalk.white(` Languages: ${scanResult.stack.languages.join(", ") || "Unknown"}`));
193+
console.log(chalk.white(` Frameworks: ${scanResult.stack.frameworks.join(", ") || "None"}`));
194+
console.log(chalk.white(` Build: ${scanResult.stack.buildSystem || "Unknown"}`));
195+
console.log(chalk.white(` CI: ${scanResult.stack.hasCi ? "Yes" : "No"}`));
196+
console.log(chalk.white(` Docker: ${scanResult.stack.hasDocker ? "Yes" : "No"}`));
197+
console.log();
198+
}
192199

193200
const analysisStart = Date.now();
194201
progress.startPhase("analyze");
@@ -250,7 +257,7 @@ export async function runMainCommand(repoUrl: string, options: BootcampOptions):
250257
runStats.generateTime = Date.now() - generateStart;
251258
progress.succeed(`Generated ${documentCount} files`);
252259

253-
if (!options.jsonOnly) {
260+
if (!quiet && !options.jsonOnly) {
254261
const grade = getSecurityGrade(security.score);
255262
const scoreColor = security.score >= 80 ? chalk.green : security.score >= 60 ? chalk.yellow : chalk.red;
256263
console.log(chalk.cyan("\nSecurity Score: ") + scoreColor(`${security.score}/100 (${grade})`));
@@ -303,25 +310,35 @@ export async function runMainCommand(repoUrl: string, options: BootcampOptions):
303310
progress.warn("Could not clean up temporary files");
304311
}
305312
} else if (options.interactive && shouldCleanupRepo) {
306-
console.log(chalk.gray(`Keeping clone for interactive mode: ${repoPath}`));
313+
if (!quiet) console.log(chalk.gray(`Keeping clone for interactive mode: ${repoPath}`));
307314
} else if (shouldCleanupRepo) {
308-
console.log(chalk.gray(`Temporary clone kept at: ${repoPath}`));
315+
if (!quiet) console.log(chalk.gray(`Temporary clone kept at: ${repoPath}`));
309316
} else {
310-
console.log(chalk.gray(`Using local repository path: ${repoPath}`));
317+
if (!quiet) console.log(chalk.gray(`Using local repository path: ${repoPath}`));
311318
}
312319

313320
progress.stop();
314321
runStats.totalTime = Date.now() - startTime;
315322

316-
console.log();
317-
console.log(chalk.green(" ╔══════════════════════════════════════════════════════╗"));
318-
console.log(chalk.green(" ║") + chalk.white.bold(" ✓ Bootcamp Generated Successfully! ") + chalk.green("║"));
319-
console.log(chalk.green(" ╚══════════════════════════════════════════════════════╝"));
320-
console.log();
321-
console.log(chalk.white(` 📁 Output: ${chalk.cyan.bold(outputDir + "/")}`));
322-
console.log();
323+
if (quiet) {
324+
// Minimal, script-friendly completion line. The output directory is the
325+
// one piece of information a non-interactive caller needs.
326+
if (!options.jsonOnly) {
327+
console.log(outputDir);
328+
}
329+
} else {
330+
console.log();
331+
console.log(chalk.green(" ╔══════════════════════════════════════════════════════╗"));
332+
console.log(chalk.green(" ║") + chalk.white.bold(" ✓ Bootcamp Generated Successfully! ") + chalk.green("║"));
333+
console.log(chalk.green(" ╚══════════════════════════════════════════════════════╝"));
334+
console.log();
335+
}
336+
if (!quiet) {
337+
console.log(chalk.white(` 📁 Output: ${chalk.cyan.bold(outputDir + "/")}`));
338+
console.log();
339+
}
323340

324-
if (!options.jsonOnly) {
341+
if (!quiet && !options.jsonOnly) {
325342
const formatName = (name: string) => formatDocName(name, outputFormat);
326343
console.log(chalk.dim(" Generated files:"));
327344
console.log(chalk.white(" ├── ") + chalk.cyan(formatName("BOOTCAMP.md")) + chalk.dim(" → 1-page overview (start here!)"));
@@ -380,16 +397,18 @@ export async function runMainCommand(repoUrl: string, options: BootcampOptions):
380397
}
381398
}
382399

383-
console.log(chalk.white(" 🚀 ") + chalk.white.bold("Next step: ") + chalk.cyan(`open ${outputDir}/${formatDocName("BOOTCAMP.md", outputFormat)}`));
384-
console.log();
400+
if (!quiet) {
401+
console.log(chalk.white(" 🚀 ") + chalk.white.bold("Next step: ") + chalk.cyan(`open ${outputDir}/${formatDocName("BOOTCAMP.md", outputFormat)}`));
402+
console.log();
403+
}
385404

386405
if (options.watch) {
387406
const watchHandle = startWatch(repoPath, {
388407
intervalSeconds: options.watchInterval || 30,
389408
allowHardReset: options.watchForce || false,
390409
verbose: options.verbose,
391410
onChangeDetected: async () => {
392-
const wp = new ProgressTracker(options.verbose);
411+
const wp = new ProgressTracker(options.verbose, quiet);
393412

394413
wp.startPhase("scan", `max ${options.maxFiles} files`);
395414
const newScan = await scanRepositoryFiles(repoPath, options.maxFiles);

src/progress.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,20 @@ export class ProgressTracker {
5454
private updateInterval: NodeJS.Timeout | null = null;
5555
private lastMessage: string = "";
5656
private verbose: boolean;
57+
private quiet: boolean;
5758

58-
constructor(verbose: boolean = false) {
59+
constructor(verbose: boolean = false, quiet: boolean = false) {
5960
this.spinner = ora({
6061
spinner: "dots",
6162
color: "cyan",
63+
// In quiet mode, never render the spinner; failures/warnings are still
64+
// surfaced explicitly (see fail/warn).
65+
isSilent: quiet,
6266
});
6367
this.startTime = Date.now();
6468
this.phaseStartTime = Date.now();
6569
this.verbose = verbose;
70+
this.quiet = quiet;
6671
}
6772

6873
/**
@@ -188,20 +193,32 @@ export class ProgressTracker {
188193
}
189194

190195
/**
191-
* Fail current phase
196+
* Fail current phase.
197+
*
198+
* In quiet mode the spinner is silenced, so the failure is written directly
199+
* to stderr — failures must always be visible, even when progress chrome is
200+
* suppressed for scripting.
192201
*/
193202
fail(message: string): void {
194203
this.stopInterval();
195-
this.spinner.fail(message);
204+
if (this.quiet) {
205+
console.error(chalk.red(`✖ ${message}`));
206+
} else {
207+
this.spinner.fail(message);
208+
}
196209
this.currentPhase = null;
197210
}
198211

199212
/**
200-
* Warn on current phase
213+
* Warn on current phase. Surfaced on stderr in quiet mode (see {@link fail}).
201214
*/
202215
warn(message: string): void {
203216
this.stopInterval();
204-
this.spinner.warn(message);
217+
if (this.quiet) {
218+
console.error(chalk.yellow(`⚠ ${message}`));
219+
} else {
220+
this.spinner.warn(message);
221+
}
205222
}
206223

207224
/**

src/services/output-writer.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,13 @@ export async function writeGeneratedOutputs({
7575
const format = options.diagramFormat || "svg";
7676
const renderResult = await renderOutputDiagrams(outputDir, format);
7777
if (renderResult.rendered) {
78-
console.log(chalk.cyan("\nDiagrams rendered: ") + chalk.white(renderResult.files.map((f) => basename(f)).join(", ")));
78+
if (!options.quiet) {
79+
console.log(chalk.cyan("\nDiagrams rendered: ") + chalk.white(renderResult.files.map((f) => basename(f)).join(", ")));
80+
}
7981
} else if (renderResult.error) {
80-
console.log(chalk.yellow(`\nDiagram rendering skipped: ${renderResult.error}`));
82+
if (!options.quiet) {
83+
console.log(chalk.yellow(`\nDiagram rendering skipped: ${renderResult.error}`));
84+
}
8185
}
8286
}
8387

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export interface BootcampOptions {
1515
/** When true, the main command expects a local repository path input instead of cloning. */
1616
noClone: boolean;
1717
verbose: boolean;
18+
/** When true, suppress decorative output (banner, phase overview, spinners, file tree) for scripting/CI. */
19+
quiet?: boolean;
1820
model?: string;
1921
keepTemp?: boolean;
2022
jsonOnly?: boolean;

0 commit comments

Comments
 (0)