braeburn 1.0.0

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/dist/commands/config.d.ts +11 -0
  3. package/dist/commands/config.js +81 -0
  4. package/dist/commands/log.d.ts +6 -0
  5. package/dist/commands/log.js +35 -0
  6. package/dist/commands/update.d.ts +8 -0
  7. package/dist/commands/update.js +110 -0
  8. package/dist/config.d.ts +9 -0
  9. package/dist/config.js +45 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +150 -0
  12. package/dist/logger.d.ts +4 -0
  13. package/dist/logger.js +44 -0
  14. package/dist/logo.d.ts +1 -0
  15. package/dist/logo.js +14 -0
  16. package/dist/runner.d.ts +21 -0
  17. package/dist/runner.js +34 -0
  18. package/dist/steps/cleanup.d.ts +3 -0
  19. package/dist/steps/cleanup.js +13 -0
  20. package/dist/steps/dotnet.d.ts +3 -0
  21. package/dist/steps/dotnet.js +14 -0
  22. package/dist/steps/homebrew.d.ts +3 -0
  23. package/dist/steps/homebrew.js +13 -0
  24. package/dist/steps/index.d.ts +27 -0
  25. package/dist/steps/index.js +24 -0
  26. package/dist/steps/macos.d.ts +3 -0
  27. package/dist/steps/macos.js +32 -0
  28. package/dist/steps/mas.d.ts +3 -0
  29. package/dist/steps/mas.js +14 -0
  30. package/dist/steps/npm.d.ts +3 -0
  31. package/dist/steps/npm.js +14 -0
  32. package/dist/steps/nvm.d.ts +3 -0
  33. package/dist/steps/nvm.js +20 -0
  34. package/dist/steps/ohmyzsh.d.ts +3 -0
  35. package/dist/steps/ohmyzsh.js +17 -0
  36. package/dist/steps/pip.d.ts +3 -0
  37. package/dist/steps/pip.js +17 -0
  38. package/dist/steps/pyenv.d.ts +3 -0
  39. package/dist/steps/pyenv.js +29 -0
  40. package/dist/ui/currentStep.d.ts +10 -0
  41. package/dist/ui/currentStep.js +16 -0
  42. package/dist/ui/header.d.ts +11 -0
  43. package/dist/ui/header.js +61 -0
  44. package/dist/ui/outputBox.d.ts +2 -0
  45. package/dist/ui/outputBox.js +29 -0
  46. package/dist/ui/prompt.d.ts +3 -0
  47. package/dist/ui/prompt.js +37 -0
  48. package/dist/ui/screen.d.ts +3 -0
  49. package/dist/ui/screen.js +54 -0
  50. package/dist/ui/state.d.ts +27 -0
  51. package/dist/ui/state.js +13 -0
  52. package/dist/ui/versionReport.d.ts +3 -0
  53. package/dist/ui/versionReport.js +25 -0
  54. package/package.json +32 -0
@@ -0,0 +1,13 @@
1
+ import { checkCommandExists, runStep } from "./index.js";
2
+ const homebrewStep = {
3
+ id: "homebrew",
4
+ name: "Homebrew",
5
+ description: "Update Homebrew itself and upgrade all installed formulae",
6
+ async checkIsAvailable() {
7
+ return checkCommandExists("brew");
8
+ },
9
+ async run(context) {
10
+ await runStep("brew update && brew upgrade", context);
11
+ },
12
+ };
13
+ export default homebrewStep;
@@ -0,0 +1,27 @@
1
+ import { type OutputLineCallback } from "../runner.js";
2
+ import type { StepLogWriter } from "../logger.js";
3
+ export type StepRunContext = {
4
+ onOutputLine: OutputLineCallback;
5
+ logWriter: StepLogWriter;
6
+ };
7
+ export type Step = {
8
+ id: string;
9
+ name: string;
10
+ description: string;
11
+ brewPackageToInstall?: string;
12
+ checkIsAvailable: () => Promise<boolean>;
13
+ run: (context: StepRunContext) => Promise<void>;
14
+ };
15
+ export { default as homebrewStep } from "./homebrew.js";
16
+ export { default as masStep } from "./mas.js";
17
+ export { default as ohmyzshStep } from "./ohmyzsh.js";
18
+ export { default as npmStep } from "./npm.js";
19
+ export { default as pipStep } from "./pip.js";
20
+ export { default as pyenvStep } from "./pyenv.js";
21
+ export { default as nvmStep } from "./nvm.js";
22
+ export { default as dotnetStep } from "./dotnet.js";
23
+ export { default as macosStep } from "./macos.js";
24
+ export { default as cleanupStep } from "./cleanup.js";
25
+ export declare function checkCommandExists(command: string): Promise<boolean>;
26
+ export declare function checkPathExists(filePath: string): Promise<boolean>;
27
+ export declare function runStep(shellCommand: string, context: StepRunContext): Promise<void>;
@@ -0,0 +1,24 @@
1
+ import { doesShellCommandSucceed, runShellCommand, } from "../runner.js";
2
+ export { default as homebrewStep } from "./homebrew.js";
3
+ export { default as masStep } from "./mas.js";
4
+ export { default as ohmyzshStep } from "./ohmyzsh.js";
5
+ export { default as npmStep } from "./npm.js";
6
+ export { default as pipStep } from "./pip.js";
7
+ export { default as pyenvStep } from "./pyenv.js";
8
+ export { default as nvmStep } from "./nvm.js";
9
+ export { default as dotnetStep } from "./dotnet.js";
10
+ export { default as macosStep } from "./macos.js";
11
+ export { default as cleanupStep } from "./cleanup.js";
12
+ export async function checkCommandExists(command) {
13
+ return doesShellCommandSucceed({ shellCommand: `command -v ${command}` });
14
+ }
15
+ export async function checkPathExists(filePath) {
16
+ return doesShellCommandSucceed({ shellCommand: `test -e "${filePath}"` });
17
+ }
18
+ export async function runStep(shellCommand, context) {
19
+ await runShellCommand({
20
+ shellCommand,
21
+ onOutputLine: context.onOutputLine,
22
+ logWriter: context.logWriter,
23
+ });
24
+ }
@@ -0,0 +1,3 @@
1
+ import { type Step } from "./index.js";
2
+ declare const macosStep: Step;
3
+ export default macosStep;
@@ -0,0 +1,32 @@
1
+ import { runStep, } from "./index.js";
2
+ import { captureShellCommandOutput } from "../runner.js";
3
+ // softwareupdate always exists on macOS — no availability check needed
4
+ const macosStep = {
5
+ id: "macos",
6
+ name: "macOS",
7
+ description: "Check for and optionally install macOS system software updates",
8
+ async checkIsAvailable() {
9
+ return true;
10
+ },
11
+ async run(context) {
12
+ const updateListOutput = await captureShellCommandOutput({
13
+ shellCommand: "softwareupdate -l 2>&1",
14
+ });
15
+ context.logWriter(updateListOutput);
16
+ const noUpdatesAvailable = updateListOutput.includes("No new software available");
17
+ if (noUpdatesAvailable) {
18
+ context.onOutputLine({
19
+ text: "macOS is already up to date.",
20
+ isError: false,
21
+ });
22
+ return;
23
+ }
24
+ context.onOutputLine({ text: updateListOutput, isError: false });
25
+ context.onOutputLine({
26
+ text: "Updates found — installing now...",
27
+ isError: false,
28
+ });
29
+ await runStep("softwareupdate -ia --verbose", context);
30
+ },
31
+ };
32
+ export default macosStep;
@@ -0,0 +1,3 @@
1
+ import { type Step } from "./index.js";
2
+ declare const masStep: Step;
3
+ export default masStep;
@@ -0,0 +1,14 @@
1
+ import { checkCommandExists, runStep } from "./index.js";
2
+ const masStep = {
3
+ id: "mas",
4
+ name: "Mac App Store",
5
+ description: "Upgrade all Mac App Store apps via the mas CLI tool",
6
+ brewPackageToInstall: "mas",
7
+ async checkIsAvailable() {
8
+ return checkCommandExists("mas");
9
+ },
10
+ async run(context) {
11
+ await runStep("mas upgrade", context);
12
+ },
13
+ };
14
+ export default masStep;
@@ -0,0 +1,3 @@
1
+ import { type Step } from "./index.js";
2
+ declare const npmStep: Step;
3
+ export default npmStep;
@@ -0,0 +1,14 @@
1
+ import { checkCommandExists, runStep } from "./index.js";
2
+ const npmStep = {
3
+ id: "npm",
4
+ name: "npm",
5
+ description: "Update all globally installed npm packages",
6
+ // No brewPackageToInstall — npm comes bundled with Node.js
7
+ async checkIsAvailable() {
8
+ return checkCommandExists("npm");
9
+ },
10
+ async run(context) {
11
+ await runStep("npm update -g", context);
12
+ },
13
+ };
14
+ export default npmStep;
@@ -0,0 +1,3 @@
1
+ import { type Step } from "./index.js";
2
+ declare const nvmStep: Step;
3
+ export default nvmStep;
@@ -0,0 +1,20 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { checkPathExists, runStep } from "./index.js";
4
+ const NVM_DIRECTORY = join(homedir(), ".nvm");
5
+ // nvm is a shell function sourced from nvm.sh — it cannot be invoked as a
6
+ // standalone binary, so we source it explicitly inside each bash invocation.
7
+ const NVM_SOURCE_PREFIX = `export NVM_DIR="${NVM_DIRECTORY}" && source "$NVM_DIR/nvm.sh"`;
8
+ const nvmStep = {
9
+ id: "nvm",
10
+ name: "Node.js (nvm)",
11
+ description: "Install the latest Node.js via nvm, migrating packages from the current version",
12
+ // No brewPackageToInstall — nvm is installed via a curl script, not Homebrew
13
+ async checkIsAvailable() {
14
+ return checkPathExists(NVM_DIRECTORY);
15
+ },
16
+ async run(context) {
17
+ await runStep(`${NVM_SOURCE_PREFIX} && nvm install node --reinstall-packages-from=node`, context);
18
+ },
19
+ };
20
+ export default nvmStep;
@@ -0,0 +1,3 @@
1
+ import { type Step } from "./index.js";
2
+ declare const ohmyzshStep: Step;
3
+ export default ohmyzshStep;
@@ -0,0 +1,17 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { checkPathExists, runStep } from "./index.js";
4
+ const OH_MY_ZSH_UPGRADE_SCRIPT_PATH = join(homedir(), ".oh-my-zsh", "tools", "upgrade.sh");
5
+ const ohmyzshStep = {
6
+ id: "ohmyzsh",
7
+ name: "Oh My Zsh",
8
+ description: "Update Oh My Zsh to the latest version",
9
+ // No brewPackageToInstall — Oh My Zsh is installed via a curl script, not Homebrew
10
+ async checkIsAvailable() {
11
+ return checkPathExists(OH_MY_ZSH_UPGRADE_SCRIPT_PATH);
12
+ },
13
+ async run(context) {
14
+ await runStep(`zsh "${OH_MY_ZSH_UPGRADE_SCRIPT_PATH}"`, context);
15
+ },
16
+ };
17
+ export default ohmyzshStep;
@@ -0,0 +1,3 @@
1
+ import { type Step } from "./index.js";
2
+ declare const pipStep: Step;
3
+ export default pipStep;
@@ -0,0 +1,17 @@
1
+ import { checkCommandExists, runStep } from "./index.js";
2
+ // Updating all global pip3 packages can occasionally break tools that depend
3
+ // on specific versions. The warning is surfaced in the UI before running.
4
+ const PIP_UPDATE_ALL_OUTDATED_SHELL_COMMAND = "pip3 list --outdated --format=columns | tail -n +3 | awk '{print $1}' | xargs -n1 pip3 install -U";
5
+ const pipStep = {
6
+ id: "pip",
7
+ name: "pip3",
8
+ description: "Update all globally installed pip3 packages",
9
+ // No brewPackageToInstall — pip3 comes with Python
10
+ async checkIsAvailable() {
11
+ return checkCommandExists("pip3");
12
+ },
13
+ async run(context) {
14
+ await runStep(PIP_UPDATE_ALL_OUTDATED_SHELL_COMMAND, context);
15
+ },
16
+ };
17
+ export default pipStep;
@@ -0,0 +1,3 @@
1
+ import { type Step } from "./index.js";
2
+ declare const pyenvStep: Step;
3
+ export default pyenvStep;
@@ -0,0 +1,29 @@
1
+ import { checkCommandExists, runStep, } from "./index.js";
2
+ import { captureShellCommandOutput } from "../runner.js";
3
+ // Dynamically finds the latest stable Python 3.x.y release known to pyenv,
4
+ // excluding pre-releases (alpha, beta, release candidates, dev builds).
5
+ const FIND_LATEST_STABLE_PYTHON_SHELL_COMMAND = "pyenv install -l | grep -E '^\\s+3\\.[0-9]+\\.[0-9]+$' | grep -vE 'dev|a[0-9]|b[0-9]|rc[0-9]' | tail -1 | tr -d ' '";
6
+ const pyenvStep = {
7
+ id: "pyenv",
8
+ name: "pyenv",
9
+ description: "Upgrade pyenv via Homebrew and install the latest Python 3.x",
10
+ brewPackageToInstall: "pyenv",
11
+ async checkIsAvailable() {
12
+ return checkCommandExists("pyenv");
13
+ },
14
+ async run(context) {
15
+ await runStep("brew upgrade pyenv", context);
16
+ const latestPythonVersion = await captureShellCommandOutput({
17
+ shellCommand: FIND_LATEST_STABLE_PYTHON_SHELL_COMMAND,
18
+ });
19
+ if (!latestPythonVersion) {
20
+ context.onOutputLine({
21
+ text: "Could not determine latest Python version — skipping pyenv install.",
22
+ isError: true,
23
+ });
24
+ return;
25
+ }
26
+ await runStep(`pyenv install --skip-existing ${latestPythonVersion}`, context);
27
+ },
28
+ };
29
+ export default pyenvStep;
@@ -0,0 +1,10 @@
1
+ import type { Step } from "../steps/index.js";
2
+ import type { StepPhase } from "./state.js";
3
+ type ActiveStepOptions = {
4
+ step: Step;
5
+ stepNumber: number;
6
+ totalSteps: number;
7
+ phase: StepPhase;
8
+ };
9
+ export declare function buildActiveStepLines(options: ActiveStepOptions): string[];
10
+ export {};
@@ -0,0 +1,16 @@
1
+ import chalk from "chalk";
2
+ export function buildActiveStepLines(options) {
3
+ const { step, stepNumber, totalSteps, phase } = options;
4
+ const isRunning = phase === "running" || phase === "installing";
5
+ const lines = [
6
+ chalk.dim(` ${"─".repeat(3)} Step ${stepNumber}/${totalSteps} `) +
7
+ chalk.bold.white(step.name) +
8
+ chalk.dim(` ${"─".repeat(20)}`),
9
+ ` ${chalk.dim("·")} ${chalk.dim.italic(step.description)}`,
10
+ ];
11
+ if (isRunning) {
12
+ const label = phase === "installing" ? "Installing..." : "Running...";
13
+ lines.push(` ${chalk.blue("▶")} ${label}`);
14
+ }
15
+ return lines;
16
+ }
@@ -0,0 +1,11 @@
1
+ import type { Step } from "../steps/index.js";
2
+ import type { StepPhase, CompletedStepRecord } from "./state.js";
3
+ type BuildHeaderOptions = {
4
+ steps: Step[];
5
+ version: string;
6
+ currentStepIndex: number;
7
+ currentPhase: StepPhase;
8
+ completedStepRecords: CompletedStepRecord[];
9
+ };
10
+ export declare function buildHeaderLines(options: BuildHeaderOptions): string[];
11
+ export {};
@@ -0,0 +1,61 @@
1
+ import chalk from "chalk";
2
+ import { LOGO_ART } from "../logo.js";
3
+ const LOGO_COLUMN_WIDTH = 32;
4
+ const LOGO_SEPARATOR = " ";
5
+ function stepTrackerIcon(phase) {
6
+ if (phase === "complete")
7
+ return chalk.green("✓ ");
8
+ if (phase === "failed")
9
+ return chalk.red("✗ ");
10
+ if (phase === "skipped" || phase === "not-available")
11
+ return chalk.dim("– ");
12
+ if (phase === "running" ||
13
+ phase === "installing" ||
14
+ phase === "prompting-to-run" ||
15
+ phase === "prompting-to-install" ||
16
+ phase === "checking-availability")
17
+ return chalk.cyan("→ ");
18
+ return chalk.dim("· ");
19
+ }
20
+ function isActivePhase(phase) {
21
+ return (phase === "running" ||
22
+ phase === "installing" ||
23
+ phase === "prompting-to-run" ||
24
+ phase === "prompting-to-install" ||
25
+ phase === "checking-availability");
26
+ }
27
+ function deriveAllStepPhases(steps, currentStepIndex, currentPhase, completedStepRecords) {
28
+ return steps.map((_, index) => {
29
+ if (index < completedStepRecords.length)
30
+ return completedStepRecords[index].phase;
31
+ if (index === currentStepIndex)
32
+ return currentPhase;
33
+ return "pending";
34
+ });
35
+ }
36
+ export function buildHeaderLines(options) {
37
+ const { steps, version, currentStepIndex, currentPhase, completedStepRecords } = options;
38
+ const phases = deriveAllStepPhases(steps, currentStepIndex, currentPhase, completedStepRecords);
39
+ const logoLines = LOGO_ART.split("\n");
40
+ const rightColumnLines = [
41
+ `${chalk.bold.white("braeburn")} ${chalk.dim("v" + version)}`,
42
+ chalk.dim("macOS system updater"),
43
+ "",
44
+ ...steps.map((step, index) => {
45
+ const icon = stepTrackerIcon(phases[index]);
46
+ const name = isActivePhase(phases[index]) ? chalk.white(step.name) : chalk.dim(step.name);
47
+ return `${icon}${name}`;
48
+ }),
49
+ ];
50
+ const totalLines = Math.max(logoLines.length, rightColumnLines.length);
51
+ const result = [];
52
+ for (let i = 0; i < totalLines; i++) {
53
+ // Pad the raw logo line to a fixed width before applying colour, so the
54
+ // ANSI escape codes don't affect visual alignment.
55
+ const rawLogoLine = (logoLines[i] ?? "").padEnd(LOGO_COLUMN_WIDTH);
56
+ const logoColumn = chalk.yellow(rawLogoLine);
57
+ const rightColumn = rightColumnLines[i] ?? "";
58
+ result.push(`${logoColumn}${LOGO_SEPARATOR}${rightColumn}`);
59
+ }
60
+ return result;
61
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandOutputLine } from "../runner.js";
2
+ export declare function buildOutputBoxLines(lines: CommandOutputLine[], stepName: string): string[];
@@ -0,0 +1,29 @@
1
+ import chalk from "chalk";
2
+ const INDENT = " ";
3
+ const HEADER_LINES_APPROXIMATE = 18;
4
+ const OUTPUT_BOX_CHROME_LINES = 3;
5
+ const MINIMUM_VISIBLE_LINES = 5;
6
+ function maxVisibleLines() {
7
+ const rows = process.stdout.rows ?? 40;
8
+ const available = rows - HEADER_LINES_APPROXIMATE - OUTPUT_BOX_CHROME_LINES;
9
+ return Math.max(MINIMUM_VISIBLE_LINES, available);
10
+ }
11
+ function boxWidth() {
12
+ return Math.min(process.stdout.columns ?? 80, 120) - INDENT.length * 2;
13
+ }
14
+ export function buildOutputBoxLines(lines, stepName) {
15
+ const visibleLines = lines.slice(-maxVisibleLines());
16
+ const width = boxWidth();
17
+ const headerLabel = `─ ${stepName} output `;
18
+ const topDashes = "─".repeat(Math.max(0, width - headerLabel.length - 2));
19
+ const topBorder = chalk.dim(`${INDENT}┌${headerLabel}${topDashes}┐`);
20
+ const bottomBorder = chalk.dim(`${INDENT}└${"─".repeat(width - 2)}┘`);
21
+ const result = [topBorder];
22
+ for (const line of visibleLines) {
23
+ const truncated = line.text.slice(0, width - 4);
24
+ const colored = line.isError ? chalk.yellow(truncated) : chalk.dim(truncated);
25
+ result.push(`${INDENT}${chalk.dim("│")} ${colored}`);
26
+ }
27
+ result.push(bottomBorder);
28
+ return result;
29
+ }
@@ -0,0 +1,3 @@
1
+ import type { CurrentPrompt } from "./state.js";
2
+ export declare function captureYesNo(): Promise<boolean>;
3
+ export declare function buildPromptLines(prompt: CurrentPrompt): string[];
@@ -0,0 +1,37 @@
1
+ import readline from "node:readline";
2
+ import chalk from "chalk";
3
+ export function captureYesNo() {
4
+ return new Promise((resolve) => {
5
+ readline.emitKeypressEvents(process.stdin);
6
+ if (process.stdin.isTTY) {
7
+ process.stdin.setRawMode(true);
8
+ }
9
+ const handleKeypress = (character, key) => {
10
+ if (key?.ctrl && key?.name === "c") {
11
+ process.stdout.write("\x1b[?25h\n");
12
+ process.exit(130);
13
+ }
14
+ const isConfirm = character === "y" || character === "Y" || key?.name === "return";
15
+ const isDecline = character === "n" || character === "N";
16
+ if (!isConfirm && !isDecline)
17
+ return;
18
+ process.stdin.removeListener("keypress", handleKeypress);
19
+ if (process.stdin.isTTY) {
20
+ process.stdin.setRawMode(false);
21
+ }
22
+ process.stdin.pause();
23
+ resolve(isConfirm);
24
+ };
25
+ process.stdin.on("keypress", handleKeypress);
26
+ process.stdin.resume();
27
+ });
28
+ }
29
+ export function buildPromptLines(prompt) {
30
+ const lines = [];
31
+ if (prompt.warning) {
32
+ lines.push(` ${chalk.yellow("⚠")} ${chalk.yellow(prompt.warning)}`);
33
+ lines.push("");
34
+ }
35
+ lines.push(` ${chalk.cyan("?")} ${prompt.question} ${chalk.dim("[Y/n]")}`);
36
+ return lines;
37
+ }
@@ -0,0 +1,3 @@
1
+ import type { AppState } from "./state.js";
2
+ export declare function renderScreen(content: string): void;
3
+ export declare function buildScreen(state: AppState): string;
@@ -0,0 +1,54 @@
1
+ import { buildHeaderLines } from "./header.js";
2
+ import { buildActiveStepLines } from "./currentStep.js";
3
+ import { buildOutputBoxLines } from "./outputBox.js";
4
+ import { buildPromptLines } from "./prompt.js";
5
+ import { buildVersionReportLines } from "./versionReport.js";
6
+ let previousLineCount = 0;
7
+ export function renderScreen(content) {
8
+ if (previousLineCount > 0) {
9
+ process.stdout.write(`\x1b[${previousLineCount}A\x1b[J`);
10
+ }
11
+ process.stdout.write(content);
12
+ previousLineCount = (content.match(/\n/g) ?? []).length;
13
+ }
14
+ export function buildScreen(state) {
15
+ const lines = [];
16
+ lines.push(...buildHeaderLines({
17
+ steps: state.steps,
18
+ version: state.version,
19
+ currentStepIndex: state.currentStepIndex,
20
+ currentPhase: state.currentPhase,
21
+ completedStepRecords: state.completedStepRecords,
22
+ }));
23
+ lines.push("");
24
+ if (state.isFinished) {
25
+ if (state.versionReport) {
26
+ lines.push("");
27
+ lines.push(...buildVersionReportLines(state.versionReport));
28
+ }
29
+ }
30
+ else {
31
+ const currentStep = state.steps[state.currentStepIndex];
32
+ if (currentStep) {
33
+ lines.push("");
34
+ lines.push(...buildActiveStepLines({
35
+ step: currentStep,
36
+ stepNumber: state.currentStepIndex + 1,
37
+ totalSteps: state.steps.length,
38
+ phase: state.currentPhase,
39
+ }));
40
+ const isShowingOutput = (state.currentPhase === "running" || state.currentPhase === "installing") &&
41
+ state.currentOutputLines.length > 0;
42
+ if (isShowingOutput) {
43
+ lines.push("");
44
+ lines.push(...buildOutputBoxLines(state.currentOutputLines, currentStep.name));
45
+ }
46
+ if (state.currentPrompt) {
47
+ lines.push("");
48
+ lines.push(...buildPromptLines(state.currentPrompt));
49
+ }
50
+ }
51
+ }
52
+ lines.push("");
53
+ return lines.join("\n") + "\n";
54
+ }
@@ -0,0 +1,27 @@
1
+ import type { Step } from "../steps/index.js";
2
+ import type { CommandOutputLine } from "../runner.js";
3
+ export type StepPhase = "pending" | "checking-availability" | "prompting-to-install" | "installing" | "prompting-to-run" | "running" | "complete" | "failed" | "skipped" | "not-available";
4
+ export type CompletedStepRecord = {
5
+ phase: StepPhase;
6
+ summaryNote?: string;
7
+ };
8
+ export type CurrentPrompt = {
9
+ question: string;
10
+ warning?: string;
11
+ };
12
+ export type ResolvedVersion = {
13
+ label: string;
14
+ value: string;
15
+ };
16
+ export type AppState = {
17
+ steps: Step[];
18
+ version: string;
19
+ currentStepIndex: number;
20
+ currentPhase: StepPhase;
21
+ completedStepRecords: CompletedStepRecord[];
22
+ currentOutputLines: CommandOutputLine[];
23
+ currentPrompt: CurrentPrompt | undefined;
24
+ isFinished: boolean;
25
+ versionReport: ResolvedVersion[] | undefined;
26
+ };
27
+ export declare function createInitialAppState(steps: Step[], version: string): AppState;
@@ -0,0 +1,13 @@
1
+ export function createInitialAppState(steps, version) {
2
+ return {
3
+ steps,
4
+ version,
5
+ currentStepIndex: 0,
6
+ currentPhase: "checking-availability",
7
+ completedStepRecords: [],
8
+ currentOutputLines: [],
9
+ currentPrompt: undefined,
10
+ isFinished: false,
11
+ versionReport: undefined,
12
+ };
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { ResolvedVersion } from "./state.js";
2
+ export declare function collectVersions(): Promise<ResolvedVersion[]>;
3
+ export declare function buildVersionReportLines(versions: ResolvedVersion[]): string[];
@@ -0,0 +1,25 @@
1
+ import chalk from "chalk";
2
+ import { captureShellCommandOutput } from "../runner.js";
3
+ const VERSION_ENTRIES = [
4
+ { label: "macOS", shellCommand: "sw_vers -productVersion" },
5
+ { label: "Homebrew", shellCommand: "brew --version | head -n1" },
6
+ { label: "Node", shellCommand: "node -v 2>/dev/null" },
7
+ { label: "NPM", shellCommand: "npm -v 2>/dev/null" },
8
+ { label: "Python", shellCommand: "python3 --version 2>/dev/null" },
9
+ { label: "pip3", shellCommand: "pip3 --version 2>/dev/null | cut -d' ' -f1-2" },
10
+ { label: "Zsh", shellCommand: "zsh --version 2>/dev/null" },
11
+ ];
12
+ export async function collectVersions() {
13
+ return Promise.all(VERSION_ENTRIES.map(async ({ label, shellCommand }) => {
14
+ const value = await captureShellCommandOutput({ shellCommand }).catch(() => "");
15
+ return { label, value: value || "not installed" };
16
+ }));
17
+ }
18
+ export function buildVersionReportLines(versions) {
19
+ return [
20
+ chalk.dim(" ─── Versions ─────────────────────────"),
21
+ ...versions.map(({ label, value }) => ` ${chalk.dim("·")} ${chalk.bold(label + ":")} ${chalk.dim(value)}`),
22
+ "",
23
+ ` ${chalk.green.bold("✓")} ${chalk.bold("All done!")}`,
24
+ ];
25
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "braeburn",
3
+ "version": "1.0.0",
4
+ "description": "macOS system updater CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "braeburn": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsx src/index.ts",
18
+ "start": "node dist/index.js",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "chalk": "^5.3.0",
23
+ "commander": "^12.1.0",
24
+ "execa": "^9.3.0",
25
+ "smol-toml": "^1.6.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.0.0",
29
+ "tsx": "^4.7.0",
30
+ "typescript": "^5.5.0"
31
+ }
32
+ }