braeburn 1.2.2 → 1.3.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/braeburn)](https://www.npmjs.com/package/braeburn)
4
4
 
5
- A macOS system updater CLI. Runs update steps for Homebrew, Mac App Store, Oh My Zsh, npm, pip, pyenv, nvm, .NET, and macOS itself.
5
+ A macOS system updater CLI. Keeps tools installed via Homebrew, npm, pip, .NET, and others up to date.
6
6
 
7
7
  ## Install
8
8
 
@@ -30,7 +30,20 @@ braeburn homebrew npm # run specific steps only
30
30
 
31
31
  ## Steps
32
32
 
33
- `homebrew` `mas` `ohmyzsh` `npm` `pip` `pyenv` `nvm` `dotnet` `macos` `cleanup`
33
+ Steps run in two stages. The runtime stage runs first and is **off by default** — upgrading a runtime is a larger change than upgrading a tool, and is best done intentionally.
34
+
35
+ | Step | Stage | Default | Requires |
36
+ |---|---|---|---|
37
+ | `pyenv` | runtime | off | `pyenv` or Homebrew |
38
+ | `nvm` | runtime | off | `~/.nvm` |
39
+ | `homebrew` | tools | on | `brew` (required) |
40
+ | `mas` | tools | on | `mas` |
41
+ | `ohmyzsh` | tools | on | `~/.oh-my-zsh` |
42
+ | `npm` | tools | on | `npm` |
43
+ | `pip` | tools | on | `pip3` |
44
+ | `dotnet` | tools | on | `dotnet` |
45
+ | `macos` | tools | on | — |
46
+ | `cleanup` | tools | on | `brew` |
34
47
 
35
48
  ## Requirements
36
49
 
@@ -1,12 +1,23 @@
1
+ import { type BraeburnConfig } from "../config.js";
1
2
  import type { Step } from "../steps/index.js";
2
3
  type RunConfigCommandOptions = {
3
4
  allSteps: Step[];
4
5
  };
6
+ type DesiredState = "enable" | "disable";
5
7
  type RunConfigUpdateCommandOptions = {
6
- stepUpdates: Record<string, boolean>;
7
- logoUpdate: boolean | undefined;
8
+ settingUpdates: Record<string, DesiredState>;
8
9
  allSteps: Step[];
9
10
  };
10
11
  export declare function runConfigCommand(options: RunConfigCommandOptions): Promise<void>;
12
+ type ConfigChange = {
13
+ label: string;
14
+ from: DesiredState;
15
+ to: DesiredState;
16
+ };
17
+ type ConfigUpdateResult = {
18
+ updatedConfig: BraeburnConfig;
19
+ changes: ConfigChange[];
20
+ };
21
+ export declare function applyConfigUpdates(config: BraeburnConfig, settingUpdates: Record<string, DesiredState>): ConfigUpdateResult;
11
22
  export declare function runConfigUpdateCommand(options: RunConfigUpdateCommandOptions): Promise<void>;
12
23
  export {};
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { readConfig, writeConfig, resolveConfigPath, isStepEnabled, isLogoEnabled, PROTECTED_STEP_IDS, } from "../config.js";
2
+ import { readConfig, writeConfig, resolveConfigPath, isSettingEnabled, isStepEnabled, isLogoEnabled, applySettingToConfig, PROTECTED_STEP_IDS, DEFAULT_OFF_STEP_IDS, } from "../config.js";
3
3
  export async function runConfigCommand(options) {
4
4
  const { allSteps } = options;
5
5
  const config = await readConfig();
@@ -29,10 +29,22 @@ export async function runConfigCommand(options) {
29
29
  }
30
30
  process.stdout.write(`\n`);
31
31
  }
32
+ export function applyConfigUpdates(config, settingUpdates) {
33
+ let updatedConfig = config;
34
+ const changes = [];
35
+ for (const [settingId, desiredState] of Object.entries(settingUpdates)) {
36
+ const currentState = isSettingEnabled(config, settingId) ? "enable" : "disable";
37
+ if (currentState !== desiredState) {
38
+ changes.push({ label: settingId, from: currentState, to: desiredState });
39
+ }
40
+ updatedConfig = applySettingToConfig(updatedConfig, settingId, desiredState);
41
+ }
42
+ return { updatedConfig, changes };
43
+ }
32
44
  export async function runConfigUpdateCommand(options) {
33
- const { stepUpdates, logoUpdate, allSteps } = options;
34
- if (Object.keys(stepUpdates).length === 0 && logoUpdate === undefined) {
35
- const configurableSteps = allSteps.filter((s) => !PROTECTED_STEP_IDS.has(s.id));
45
+ const { settingUpdates, allSteps } = options;
46
+ if (Object.keys(settingUpdates).length === 0) {
47
+ const configurableSteps = allSteps.filter((step) => !PROTECTED_STEP_IDS.has(step.id));
36
48
  process.stdout.write("No changes — pass flags to enable or disable steps:\n\n");
37
49
  process.stdout.write(` ${"--no-logo".padEnd(18)} hide the logo\n`);
38
50
  process.stdout.write(` ${"--logo".padEnd(18)} show the logo\n`);
@@ -48,41 +60,15 @@ export async function runConfigUpdateCommand(options) {
48
60
  return;
49
61
  }
50
62
  const config = await readConfig();
51
- const changes = [];
52
- if (logoUpdate !== undefined) {
53
- const currentlyEnabled = isLogoEnabled(config);
54
- if (currentlyEnabled !== logoUpdate) {
55
- changes.push({ label: "logo", from: currentlyEnabled, to: logoUpdate });
56
- }
57
- if (logoUpdate) {
58
- delete config.logo;
59
- }
60
- else {
61
- config.logo = false;
62
- }
63
- }
64
- for (const [stepId, newEnabled] of Object.entries(stepUpdates)) {
65
- const currentlyEnabled = isStepEnabled(config, stepId);
66
- if (currentlyEnabled !== newEnabled) {
67
- changes.push({ label: stepId, from: currentlyEnabled, to: newEnabled });
68
- }
69
- if (newEnabled) {
70
- // Re-enabling: remove from config so absent = enabled (keeps file minimal)
71
- delete config.steps[stepId];
72
- }
73
- else {
74
- config.steps[stepId] = false;
75
- }
76
- }
77
- // Write even if no visible changes, in case the user is re-confirming state
78
- await writeCleanConfig(config);
63
+ const { updatedConfig, changes } = applyConfigUpdates(config, settingUpdates);
64
+ await writeCleanConfig(updatedConfig);
79
65
  if (changes.length === 0) {
80
66
  process.stdout.write("No changes — already set as requested.\n");
81
67
  return;
82
68
  }
83
69
  for (const { label, from, to } of changes) {
84
- const fromLabel = from ? chalk.green("enabled") : chalk.red("disabled");
85
- const toLabel = to ? chalk.green("enabled") : chalk.red("disabled");
70
+ const fromLabel = from === "enable" ? chalk.green("enabled") : chalk.red("disabled");
71
+ const toLabel = to === "enable" ? chalk.green("enabled") : chalk.red("disabled");
86
72
  process.stdout.write(` ${label.padEnd(12)} ${fromLabel} → ${toLabel}\n`);
87
73
  }
88
74
  const configPath = await resolveConfigPath();
@@ -90,10 +76,13 @@ export async function runConfigUpdateCommand(options) {
90
76
  }
91
77
  async function writeCleanConfig(config) {
92
78
  const cleaned = { steps: {} };
93
- for (const [stepId, enabled] of Object.entries(config.steps)) {
94
- if (enabled === false) {
79
+ for (const [stepId, value] of Object.entries(config.steps)) {
80
+ if (value === false && !DEFAULT_OFF_STEP_IDS.has(stepId)) {
95
81
  cleaned.steps[stepId] = false;
96
82
  }
83
+ else if (value === true && DEFAULT_OFF_STEP_IDS.has(stepId)) {
84
+ cleaned.steps[stepId] = true;
85
+ }
97
86
  }
98
87
  if (config.logo === false) {
99
88
  cleaned.logo = false;
@@ -1,2 +1,13 @@
1
1
  import type { Step } from "../steps/index.js";
2
+ export type SelectionState = "selected" | "deselected";
3
+ export type ProtectionStatus = "protected" | "configurable";
4
+ export type AvailabilityStatus = "available" | "unavailable";
5
+ export type SelectableStep = {
6
+ step: Step;
7
+ selection: SelectionState;
8
+ protection: ProtectionStatus;
9
+ availability: AvailabilityStatus;
10
+ };
11
+ export declare function buildLoadingScreen(): string;
12
+ export declare function buildSetupScreen(items: SelectableStep[], cursorIndex: number): string;
2
13
  export declare function runSetupCommand(allSteps: Step[]): Promise<void>;
@@ -2,16 +2,9 @@ import readline from "node:readline";
2
2
  import chalk from "chalk";
3
3
  import { writeConfig, PROTECTED_STEP_IDS } from "../config.js";
4
4
  import { LOGO_ART } from "../logo.js";
5
- // Module-level render state (scoped to this screen, separate from update screen)
6
- let prevLines = 0;
7
- function render(content) {
8
- if (prevLines > 0) {
9
- process.stdout.write(`\x1b[${prevLines}A\x1b[J`);
10
- }
11
- process.stdout.write(content);
12
- prevLines = (content.match(/\n/g) ?? []).length;
13
- }
14
- function buildLoadingScreen() {
5
+ import { createScreenRenderer } from "../ui/screen.js";
6
+ import { hideCursorDuringExecution } from "../ui/terminal.js";
7
+ export function buildLoadingScreen() {
15
8
  const lines = [
16
9
  chalk.yellow(LOGO_ART),
17
10
  "",
@@ -22,7 +15,7 @@ function buildLoadingScreen() {
22
15
  ];
23
16
  return lines.join("\n") + "\n";
24
17
  }
25
- function buildSetupScreen(items, cursorIndex) {
18
+ export function buildSetupScreen(items, cursorIndex) {
26
19
  const lines = [
27
20
  chalk.yellow(LOGO_ART),
28
21
  "",
@@ -34,18 +27,31 @@ function buildSetupScreen(items, cursorIndex) {
34
27
  ` ${chalk.dim("\u2191\u2193 navigate Space toggle Return confirm")}`,
35
28
  "",
36
29
  ];
37
- for (let i = 0; i < items.length; i++) {
38
- const item = items[i];
39
- const isCursor = i === cursorIndex;
30
+ const hasRuntimeItems = items.some((item) => item.step.stage === "runtime");
31
+ const hasToolsItems = items.some((item) => item.step.stage === "tools");
32
+ const showStageLabels = hasRuntimeItems && hasToolsItems;
33
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
34
+ const item = items[itemIndex];
35
+ const isCursor = itemIndex === cursorIndex;
36
+ if (showStageLabels) {
37
+ const isFirstRuntime = item.step.stage === "runtime" && (itemIndex === 0 || items[itemIndex - 1].step.stage !== "runtime");
38
+ const isFirstTools = item.step.stage === "tools" && (itemIndex === 0 || items[itemIndex - 1].step.stage !== "tools");
39
+ if (isFirstRuntime) {
40
+ lines.push(` ${chalk.dim("── Runtimes ─────────────────────────────────────────────────────────")}`);
41
+ }
42
+ else if (isFirstTools) {
43
+ lines.push(` ${chalk.dim("── Tools ────────────────────────────────────────────────────────────")}`);
44
+ }
45
+ }
40
46
  const cursor = isCursor ? chalk.cyan("\u203a") : " ";
41
- const checkbox = item.selected ? chalk.green("\u25cf") : chalk.dim("\u25cb");
47
+ const checkbox = item.selection === "selected" ? chalk.green("\u25cf") : chalk.dim("\u25cb");
42
48
  const namePadded = item.step.name.padEnd(18);
43
49
  const name = isCursor ? chalk.bold.white(namePadded) : chalk.white(namePadded);
44
50
  let status;
45
- if (item.isProtected) {
51
+ if (item.protection === "protected") {
46
52
  status = chalk.dim("required");
47
53
  }
48
- else if (item.isAvailable) {
54
+ else if (item.availability === "available") {
49
55
  status = chalk.green("installed");
50
56
  }
51
57
  else if (item.step.brewPackageToInstall) {
@@ -59,39 +65,31 @@ function buildSetupScreen(items, cursorIndex) {
59
65
  lines.push(` ${chalk.dim(item.step.description)}`);
60
66
  }
61
67
  }
62
- const enabledCount = items.filter((i) => i.selected).length;
68
+ const enabledCount = items.filter((item) => item.selection === "selected").length;
63
69
  lines.push("");
64
70
  lines.push(` ${chalk.dim(`${enabledCount} of ${items.length} tools selected`)}`);
65
71
  lines.push("");
66
72
  return lines.join("\n") + "\n";
67
73
  }
68
74
  export async function runSetupCommand(allSteps) {
69
- // Hide cursor; restore on exit
70
- process.stdout.write("\x1b[?25l");
71
- process.on("exit", () => process.stdout.write("\x1b[?25h"));
72
- process.on("SIGINT", () => {
73
- process.stdout.write("\x1b[?25h\n");
74
- process.exit(130);
75
- });
76
- // Show loading screen while we check availability in parallel
75
+ const render = createScreenRenderer();
76
+ hideCursorDuringExecution();
77
77
  render(buildLoadingScreen());
78
78
  const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
79
- const items = allSteps.map((step, i) => ({
79
+ const items = allSteps.map((step, stepIndex) => ({
80
80
  step,
81
- selected: true, // all enabled by default user opts out
82
- isProtected: PROTECTED_STEP_IDS.has(step.id),
83
- isAvailable: availabilityResults[i],
81
+ selection: PROTECTED_STEP_IDS.has(step.id) || step.stage === "tools" ? "selected" : "deselected",
82
+ protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
83
+ availability: availabilityResults[stepIndex] ? "available" : "unavailable",
84
84
  }));
85
85
  let cursorIndex = 0;
86
86
  render(buildSetupScreen(items, cursorIndex));
87
- // Interactive selection loop
88
87
  await new Promise((resolve) => {
89
88
  readline.emitKeypressEvents(process.stdin);
90
89
  if (process.stdin.isTTY)
91
90
  process.stdin.setRawMode(true);
92
91
  const handleKeypress = (_char, key) => {
93
92
  if (key?.ctrl && key?.name === "c") {
94
- process.stdout.write("\x1b[?25h\n");
95
93
  process.exit(130);
96
94
  }
97
95
  if (key?.name === "up" || key?.name === "k") {
@@ -104,8 +102,8 @@ export async function runSetupCommand(allSteps) {
104
102
  }
105
103
  else if (key?.name === "space") {
106
104
  const item = items[cursorIndex];
107
- if (!item.isProtected) {
108
- item.selected = !item.selected;
105
+ if (item.protection === "configurable") {
106
+ item.selection = item.selection === "selected" ? "deselected" : "selected";
109
107
  render(buildSetupScreen(items, cursorIndex));
110
108
  }
111
109
  }
@@ -120,25 +118,19 @@ export async function runSetupCommand(allSteps) {
120
118
  process.stdin.on("keypress", handleKeypress);
121
119
  process.stdin.resume();
122
120
  });
123
- // Restore cursor
124
- process.stdout.write("\x1b[?25h");
125
- // Persist choices — only write explicit false entries (keeps file minimal, matches
126
- // the opt-out convention used everywhere else in the codebase)
127
121
  const stepsConfig = {};
128
122
  for (const item of items) {
129
- if (!item.isProtected && !item.selected) {
123
+ if (item.protection === "configurable" && item.selection === "deselected") {
130
124
  stepsConfig[item.step.id] = false;
131
125
  }
132
126
  }
133
127
  await writeConfig({ steps: stepsConfig });
134
- // Clear the setup screen and print a brief confirmation before the update starts
135
- if (prevLines > 0) {
136
- process.stdout.write(`\x1b[${prevLines}A\x1b[J`);
137
- }
138
- process.stdout.write(chalk.yellow(LOGO_ART) + "\n");
139
- process.stdout.write("\n");
140
- process.stdout.write(` ${chalk.green("\u2713")} Setup complete! Starting your first update\u2026\n`);
141
- process.stdout.write("\n");
142
- // Small pause so the confirmation is readable before the update screen takes over
143
- await new Promise((res) => setTimeout(res, 800));
128
+ const confirmationLines = [
129
+ chalk.yellow(LOGO_ART),
130
+ "",
131
+ ` ${chalk.green("\u2713")} Setup complete! Starting your first update\u2026`,
132
+ "",
133
+ ];
134
+ render(confirmationLines.join("\n") + "\n");
135
+ await new Promise((resolve) => setTimeout(resolve, 800));
144
136
  }
@@ -1,8 +1,10 @@
1
1
  import type { Step } from "../steps/index.js";
2
+ type PromptMode = "interactive" | "auto-accept";
3
+ type LogoVisibility = "visible" | "hidden";
2
4
  type RunUpdateCommandOptions = {
3
5
  steps: Step[];
4
- autoYes: boolean;
5
- showLogo: boolean;
6
+ promptMode: PromptMode;
7
+ logoVisibility: LogoVisibility;
6
8
  version: string;
7
9
  };
8
10
  export declare function runUpdateCommand(options: RunUpdateCommandOptions): Promise<void>;
@@ -3,21 +3,19 @@ import { createLogWriterForStep } from "../logger.js";
3
3
  import { collectVersions } from "../ui/versionReport.js";
4
4
  import { captureYesNo } from "../ui/prompt.js";
5
5
  import { createInitialAppState } from "../ui/state.js";
6
- import { buildScreen, renderScreen } from "../ui/screen.js";
6
+ import { buildScreen, createScreenRenderer } from "../ui/screen.js";
7
+ import { hideCursorDuringExecution } from "../ui/terminal.js";
8
+ import { createDefaultStepRunContext } from "../steps/index.js";
7
9
  export async function runUpdateCommand(options) {
8
10
  const { steps, version } = options;
9
- let autoYes = options.autoYes;
10
- const state = createInitialAppState(steps, version, options.showLogo);
11
- process.stdout.write("\x1b[?25l");
12
- process.on("exit", () => process.stdout.write("\x1b[?25h"));
13
- process.on("SIGINT", () => {
14
- process.stdout.write("\x1b[?25h\n");
15
- process.exit(130);
16
- });
11
+ let autoAccept = options.promptMode === "auto-accept";
12
+ const state = createInitialAppState(steps, version, options.logoVisibility);
13
+ const renderScreen = createScreenRenderer();
14
+ hideCursorDuringExecution();
17
15
  renderScreen(buildScreen(state));
18
- for (let i = 0; i < steps.length; i++) {
19
- const step = steps[i];
20
- state.currentStepIndex = i;
16
+ for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
17
+ const step = steps[stepIndex];
18
+ state.currentStepIndex = stepIndex;
21
19
  state.currentPhase = "checking-availability";
22
20
  state.currentOutputLines = [];
23
21
  state.currentPrompt = undefined;
@@ -35,9 +33,9 @@ export async function runUpdateCommand(options) {
35
33
  question: `Install ${step.name} via Homebrew? (brew install ${step.brewPackageToInstall})`,
36
34
  };
37
35
  renderScreen(buildScreen(state));
38
- const installAnswer = autoYes ? "yes" : await captureYesNo();
36
+ const installAnswer = autoAccept ? "yes" : await captureYesNo();
39
37
  if (installAnswer === "force")
40
- autoYes = true;
38
+ autoAccept = true;
41
39
  const shouldInstall = installAnswer !== "no";
42
40
  state.currentPrompt = undefined;
43
41
  if (!shouldInstall) {
@@ -66,15 +64,12 @@ export async function runUpdateCommand(options) {
66
64
  continue;
67
65
  }
68
66
  }
69
- const pipWarning = step.id === "pip"
70
- ? "This updates all global pip3 packages, which can occasionally break tools."
71
- : undefined;
72
67
  state.currentPhase = "prompting-to-run";
73
- state.currentPrompt = { question: `Run ${step.name} update?`, warning: pipWarning };
68
+ state.currentPrompt = { question: `Run ${step.name} update?`, warning: step.warning };
74
69
  renderScreen(buildScreen(state));
75
- const runAnswer = autoYes ? "yes" : await captureYesNo();
70
+ const runAnswer = autoAccept ? "yes" : await captureYesNo();
76
71
  if (runAnswer === "force")
77
- autoYes = true;
72
+ autoAccept = true;
78
73
  const shouldRun = runAnswer !== "no";
79
74
  state.currentPrompt = undefined;
80
75
  if (!shouldRun) {
@@ -88,13 +83,10 @@ export async function runUpdateCommand(options) {
88
83
  renderScreen(buildScreen(state));
89
84
  const stepLogWriter = await createLogWriterForStep(step.id);
90
85
  try {
91
- await step.run({
92
- onOutputLine: (line) => {
93
- state.currentOutputLines.push(line);
94
- renderScreen(buildScreen(state));
95
- },
96
- logWriter: stepLogWriter,
97
- });
86
+ await step.run(createDefaultStepRunContext((line) => {
87
+ state.currentOutputLines.push(line);
88
+ renderScreen(buildScreen(state));
89
+ }, stepLogWriter));
98
90
  state.currentPhase = "complete";
99
91
  state.currentOutputLines = [];
100
92
  renderScreen(buildScreen(state));
@@ -108,7 +100,7 @@ export async function runUpdateCommand(options) {
108
100
  state.completedStepRecords.push({ phase: "failed", summaryNote: errorMessage });
109
101
  }
110
102
  }
111
- state.isFinished = true;
103
+ state.runCompletion = "finished";
112
104
  state.currentOutputLines = [];
113
105
  state.currentPrompt = undefined;
114
106
  renderScreen(buildScreen(state));
package/dist/config.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- /** Steps that cannot be disabled — brew is a hard runtime dependency. */
2
1
  export declare const PROTECTED_STEP_IDS: Set<string>;
2
+ export declare const DEFAULT_OFF_STEP_IDS: Set<string>;
3
3
  export type BraeburnConfig = {
4
4
  steps: Record<string, boolean>;
5
5
  logo?: boolean;
@@ -8,5 +8,7 @@ export declare function resolveConfigPath(): Promise<string>;
8
8
  export declare function configFileExists(): Promise<boolean>;
9
9
  export declare function readConfig(): Promise<BraeburnConfig>;
10
10
  export declare function writeConfig(config: BraeburnConfig): Promise<void>;
11
+ export declare function isSettingEnabled(config: BraeburnConfig, settingId: string): boolean;
11
12
  export declare function isStepEnabled(config: BraeburnConfig, stepId: string): boolean;
12
13
  export declare function isLogoEnabled(config: BraeburnConfig): boolean;
14
+ export declare function applySettingToConfig(config: BraeburnConfig, settingId: string, desiredState: "enable" | "disable"): BraeburnConfig;
package/dist/config.js CHANGED
@@ -2,12 +2,13 @@ import { readFile, writeFile, mkdir, access } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { parse, stringify } from "smol-toml";
5
- /** Steps that cannot be disabled — brew is a hard runtime dependency. */
6
5
  export const PROTECTED_STEP_IDS = new Set(["homebrew"]);
6
+ export const DEFAULT_OFF_STEP_IDS = new Set(["nvm", "pyenv"]);
7
7
  const EMPTY_CONFIG = { steps: {} };
8
- async function pathExists(p) {
8
+ const LOGO_SETTING_ID = "logo";
9
+ async function pathExists(targetPath) {
9
10
  try {
10
- await access(p);
11
+ await access(targetPath);
11
12
  return true;
12
13
  }
13
14
  catch {
@@ -41,13 +42,46 @@ export async function writeConfig(config) {
41
42
  await mkdir(join(configPath, ".."), { recursive: true });
42
43
  await writeFile(configPath, stringify(config), "utf-8");
43
44
  }
44
- export function isStepEnabled(config, stepId) {
45
- if (PROTECTED_STEP_IDS.has(stepId))
45
+ export function isSettingEnabled(config, settingId) {
46
+ if (PROTECTED_STEP_IDS.has(settingId))
46
47
  return true;
47
- // Absent from config means enabled (opt-out model)
48
- return config.steps[stepId] !== false;
48
+ if (settingId === LOGO_SETTING_ID)
49
+ return config.logo !== false;
50
+ if (DEFAULT_OFF_STEP_IDS.has(settingId))
51
+ return config.steps[settingId] === true;
52
+ return config.steps[settingId] !== false;
53
+ }
54
+ export function isStepEnabled(config, stepId) {
55
+ return isSettingEnabled(config, stepId);
49
56
  }
50
57
  export function isLogoEnabled(config) {
51
- // Absent from config means enabled (opt-out model)
52
- return config.logo !== false;
58
+ return isSettingEnabled(config, LOGO_SETTING_ID);
59
+ }
60
+ export function applySettingToConfig(config, settingId, desiredState) {
61
+ const updatedConfig = structuredClone(config);
62
+ if (settingId === LOGO_SETTING_ID) {
63
+ if (desiredState === "enable") {
64
+ delete updatedConfig.logo;
65
+ }
66
+ else {
67
+ updatedConfig.logo = false;
68
+ }
69
+ return updatedConfig;
70
+ }
71
+ if (DEFAULT_OFF_STEP_IDS.has(settingId)) {
72
+ if (desiredState === "enable") {
73
+ updatedConfig.steps[settingId] = true;
74
+ }
75
+ else {
76
+ delete updatedConfig.steps[settingId];
77
+ }
78
+ return updatedConfig;
79
+ }
80
+ if (desiredState === "enable") {
81
+ delete updatedConfig.steps[settingId];
82
+ }
83
+ else {
84
+ updatedConfig.steps[settingId] = false;
85
+ }
86
+ return updatedConfig;
53
87
  }
package/dist/index.js CHANGED
@@ -10,13 +10,13 @@ import { runConfigCommand, runConfigUpdateCommand } from "./commands/config.js";
10
10
  import { runSetupCommand } from "./commands/setup.js";
11
11
  import { readConfig, isStepEnabled, isLogoEnabled, PROTECTED_STEP_IDS, configFileExists } from "./config.js";
12
12
  const ALL_STEPS = [
13
+ pyenvStep,
14
+ nvmStep,
13
15
  homebrewStep,
14
16
  masStep,
15
17
  ohmyzshStep,
16
18
  npmStep,
17
19
  pipStep,
18
- pyenvStep,
19
- nvmStep,
20
20
  dotnetStep,
21
21
  macosStep,
22
22
  cleanupStep,
@@ -34,33 +34,36 @@ program
34
34
  program
35
35
  .command("update", { isDefault: true })
36
36
  .description("Run system update steps (default command)")
37
- .argument("[steps...]", `Steps to run — omit to run all.\nAvailable: ${ALL_STEPS.map((s) => s.id).join(", ")}`)
37
+ .argument("[steps...]", `Steps to run — omit to run all.\nAvailable: ${ALL_STEPS.map((step) => step.id).join(", ")}`)
38
38
  .option("-y, --yes", "Auto-accept all prompts (default yes to everything)")
39
39
  .option("-f, --force", "Alias for --yes")
40
40
  .option("--no-logo", "Hide the logo")
41
41
  .addHelpText("after", `
42
42
  Step descriptions:
43
+ Runtime stage (default: off — update the version managers and runtimes themselves):
44
+ pyenv Upgrade pyenv, install latest Python 3.x (requires: pyenv or brew)
45
+ nvm Install latest Node.js via nvm (requires: ~/.nvm)
46
+
47
+ Tools stage (default: on — update packages installed via the managers above):
43
48
  homebrew Update Homebrew itself and all installed formulae
44
49
  mas Upgrade Mac App Store apps (requires: mas)
45
50
  ohmyzsh Update Oh My Zsh (requires: ~/.oh-my-zsh)
46
51
  npm Update global npm packages (requires: npm)
47
52
  pip Update global pip3 packages (requires: pip3) ⚠ may be fragile
48
- pyenv Upgrade pyenv, install latest Python 3.x (requires: pyenv or brew)
49
- nvm Update Node.js via nvm (requires: ~/.nvm)
50
53
  dotnet Update .NET global tools (requires: dotnet)
51
54
  macos Check for macOS updates, prompt to install
52
55
  cleanup Clean up Homebrew cache and old downloads
53
56
 
54
57
  Examples:
55
- braeburn Run all steps interactively
56
- braeburn -y Run all steps, auto-accept everything
58
+ braeburn Run all enabled steps interactively
59
+ braeburn -y Run all enabled steps, auto-accept everything
57
60
  braeburn -fy Same as above
58
61
  braeburn homebrew npm Run only the homebrew and npm steps
59
62
  braeburn homebrew -y Run only homebrew, auto-accept
63
+ braeburn nvm pyenv Run only the runtime-stage steps
60
64
  `)
61
65
  .action(async (stepArguments, options) => {
62
66
  const autoYes = options.yes === true || options.force === true;
63
- // First-run: if no config file exists yet, show the setup wizard.
64
67
  if (!(await configFileExists())) {
65
68
  await runSetupCommand(ALL_STEPS);
66
69
  }
@@ -68,17 +71,14 @@ Examples:
68
71
  let stepsToRun = stepArguments.length === 0
69
72
  ? ALL_STEPS
70
73
  : resolveStepsByIds(stepArguments);
71
- // When no explicit steps are requested, filter out steps disabled in config.
72
- // Explicit step arguments always bypass config (user knows what they want).
73
74
  if (stepArguments.length === 0) {
74
75
  stepsToRun = stepsToRun.filter((step) => isStepEnabled(config, step.id));
75
76
  }
76
- // CLI --no-logo overrides config; otherwise defer to config preference.
77
- const showLogo = options.logo !== false && isLogoEnabled(config);
77
+ const logoIsEnabled = options.logo !== false && isLogoEnabled(config);
78
78
  await runUpdateCommand({
79
79
  steps: stepsToRun,
80
- autoYes,
81
- showLogo,
80
+ promptMode: autoYes ? "auto-accept" : "interactive",
81
+ logoVisibility: logoIsEnabled ? "visible" : "hidden",
82
82
  version: BRAEBURN_VERSION,
83
83
  });
84
84
  });
@@ -118,7 +118,7 @@ const configCommand = program
118
118
  .action(async () => {
119
119
  await runConfigCommand({ allSteps: ALL_STEPS });
120
120
  });
121
- const configurableSteps = ALL_STEPS.filter((s) => !PROTECTED_STEP_IDS.has(s.id));
121
+ const configurableSteps = ALL_STEPS.filter((step) => !PROTECTED_STEP_IDS.has(step.id));
122
122
  const configUpdateCommand = configCommand
123
123
  .command("update")
124
124
  .description("Enable or disable individual update steps")
@@ -136,20 +136,19 @@ for (const step of configurableSteps) {
136
136
  configUpdateCommand.option(`--${step.id}`, `Enable ${step.name} updates`);
137
137
  }
138
138
  configUpdateCommand.action(function () {
139
- // Use getOptionValueSource to detect only flags explicitly passed on the CLI.
140
- // Commander defaults --no-* to true, so we can't rely on option values alone.
141
- const stepUpdates = {};
139
+ // Commander defaults --no-* to true, so we use getOptionValueSource to detect explicit CLI flags.
140
+ const settingUpdates = {};
142
141
  for (const step of configurableSteps) {
143
142
  const source = configUpdateCommand.getOptionValueSource(step.id);
144
143
  if (source === "cli") {
145
- stepUpdates[step.id] = configUpdateCommand.opts()[step.id];
144
+ settingUpdates[step.id] = configUpdateCommand.opts()[step.id] ? "enable" : "disable";
146
145
  }
147
146
  }
148
147
  const logoSource = configUpdateCommand.getOptionValueSource("logo");
149
- const logoUpdate = logoSource === "cli"
150
- ? configUpdateCommand.opts().logo
151
- : undefined;
152
- runConfigUpdateCommand({ stepUpdates, logoUpdate, allSteps: ALL_STEPS });
148
+ if (logoSource === "cli") {
149
+ settingUpdates["logo"] = configUpdateCommand.opts().logo ? "enable" : "disable";
150
+ }
151
+ runConfigUpdateCommand({ settingUpdates, allSteps: ALL_STEPS });
153
152
  });
154
153
  function resolveStepsByIds(stepIds) {
155
154
  const resolvedSteps = [];
package/dist/logger.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export type StepLogWriter = (line: string) => Promise<void>;
2
- export declare function createLogWriterForStep(stepId: string): Promise<StepLogWriter>;
3
- export declare function findLatestLogFileForStep(stepId: string): string | null;
4
- export declare function listAllStepIdsWithLogs(): string[];
2
+ export declare function createLogWriterForStep(stepId: string, logDirectory?: string): Promise<StepLogWriter>;
3
+ export declare function findLatestLogFileForStep(stepId: string, logDirectory?: string): string | null;
4
+ export declare function listAllStepIdsWithLogs(logDirectory?: string): string[];