braeburn 2.0.0 → 2.1.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/dist/cli.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { Command } from "commander";
2
+ import type { Step } from "./steps/index.js";
3
+ import { applyUpdateCommandResult, runUpdateCommand } from "./commands/update.js";
4
+ import { runLogCommand, runLogListCommand } from "./commands/log.js";
5
+ import { runConfigCommand, runConfigUpdateCommand } from "./commands/config.js";
6
+ import { runSetupCommand } from "./commands/setup.js";
7
+ import { configFileExists, readConfig } from "./config.js";
8
+ type CliProcess = Pick<NodeJS.Process, "stderr"> & {
9
+ exitCode: string | number | null | undefined;
10
+ };
11
+ type CliCommandDependencies = {
12
+ applyUpdateCommandResult: typeof applyUpdateCommandResult;
13
+ configFileExists: typeof configFileExists;
14
+ readConfig: typeof readConfig;
15
+ runConfigCommand: typeof runConfigCommand;
16
+ runConfigUpdateCommand: typeof runConfigUpdateCommand;
17
+ runLogCommand: typeof runLogCommand;
18
+ runLogListCommand: typeof runLogListCommand;
19
+ runSetupCommand: typeof runSetupCommand;
20
+ runUpdateCommand: typeof runUpdateCommand;
21
+ };
22
+ type CreateBraeburnProgramOptions = {
23
+ allSteps?: Step[];
24
+ dependencies?: Partial<CliCommandDependencies>;
25
+ processLike?: CliProcess;
26
+ version?: string;
27
+ };
28
+ export type StepResolution = {
29
+ status: "resolved";
30
+ steps: Step[];
31
+ } | {
32
+ status: "unknown-step";
33
+ stepId: string;
34
+ };
35
+ export declare function resolveStepsByIds(stepIds: string[], allSteps: Step[]): StepResolution;
36
+ export declare function reportCliError(error: unknown, processLike?: CliProcess): void;
37
+ export declare function createBraeburnProgram(options?: CreateBraeburnProgramOptions): Command;
38
+ export declare function runBraeburnCli(argv?: string[]): Promise<void>;
39
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,226 @@
1
+ import { Command } from "commander";
2
+ import { createRequire } from "node:module";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join } from "node:path";
5
+ import { ALL_STEPS } from "./steps/catalog.js";
6
+ import { applyUpdateCommandResult, runUpdateCommand } from "./commands/update.js";
7
+ import { runLogCommand, runLogListCommand } from "./commands/log.js";
8
+ import { runConfigCommand, runConfigUpdateCommand } from "./commands/config.js";
9
+ import { runSetupCommand } from "./commands/setup.js";
10
+ import { ConfigReadError, PROTECTED_STEP_IDS, configFileExists, isLogoEnabled, isStepEnabled, readConfig, } from "./config.js";
11
+ const requireFromThis = createRequire(import.meta.url);
12
+ function resolveBraeburnVersion() {
13
+ const packageJson = requireFromThis(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"));
14
+ return packageJson.version;
15
+ }
16
+ function resolveDependencies(overrides) {
17
+ return {
18
+ applyUpdateCommandResult: overrides?.applyUpdateCommandResult ?? applyUpdateCommandResult,
19
+ configFileExists: overrides?.configFileExists ?? configFileExists,
20
+ readConfig: overrides?.readConfig ?? readConfig,
21
+ runConfigCommand: overrides?.runConfigCommand ?? runConfigCommand,
22
+ runConfigUpdateCommand: overrides?.runConfigUpdateCommand ?? runConfigUpdateCommand,
23
+ runLogCommand: overrides?.runLogCommand ?? runLogCommand,
24
+ runLogListCommand: overrides?.runLogListCommand ?? runLogListCommand,
25
+ runSetupCommand: overrides?.runSetupCommand ?? runSetupCommand,
26
+ runUpdateCommand: overrides?.runUpdateCommand ?? runUpdateCommand,
27
+ };
28
+ }
29
+ export function resolveStepsByIds(stepIds, allSteps) {
30
+ const stepsById = new Map(allSteps.map((step) => [step.id, step]));
31
+ const resolvedSteps = [];
32
+ for (const stepId of stepIds) {
33
+ const step = stepsById.get(stepId);
34
+ if (!step) {
35
+ return { status: "unknown-step", stepId };
36
+ }
37
+ resolvedSteps.push(step);
38
+ }
39
+ return { status: "resolved", steps: resolvedSteps };
40
+ }
41
+ export function reportCliError(error, processLike = process) {
42
+ if (error instanceof ConfigReadError) {
43
+ processLike.stderr.write(`${error.message}\n`);
44
+ processLike.exitCode = 1;
45
+ return;
46
+ }
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ processLike.stderr.write(`${message}\n`);
49
+ processLike.exitCode = 1;
50
+ }
51
+ export function createBraeburnProgram(options = {}) {
52
+ const allSteps = options.allSteps ?? ALL_STEPS;
53
+ const dependencies = resolveDependencies(options.dependencies);
54
+ const processLike = options.processLike ?? process;
55
+ const braeburnVersion = options.version ?? resolveBraeburnVersion();
56
+ const program = new Command();
57
+ program
58
+ .name("braeburn")
59
+ .description("macOS system updater")
60
+ .version(braeburnVersion)
61
+ .helpCommand(false);
62
+ program
63
+ .command("update", { isDefault: true })
64
+ .description("Run system update steps (default command)")
65
+ .argument("[steps...]", `Steps to run — omit to run all.\nAvailable: ${allSteps.map((step) => step.id).join(", ")}`)
66
+ .option("-y, --yes", "Auto-accept all prompts (default yes to everything)")
67
+ .option("-f, --force", "Alias for --yes")
68
+ .option("--no-logo", "Hide the logo")
69
+ .addHelpText("after", `
70
+ Step descriptions:
71
+ System / Runtimes (default: off — larger changes, enabled intentionally):
72
+ pyenv Upgrade pyenv, install latest Python 3.x (requires: pyenv or brew)
73
+ nvm Install latest Node.js via nvm (requires: ~/.nvm)
74
+
75
+ System / Apps & Packages:
76
+ homebrew Update Homebrew itself and all installed formulae
77
+ mas Upgrade Mac App Store apps (requires: mas)
78
+ macos Check for macOS updates, prompt to install
79
+
80
+ System / CLI Tools:
81
+ npm Update global npm packages (requires: npm)
82
+ braeburn Update braeburn CLI itself (requires: npm)
83
+ pip Update global pip3 packages (requires: pip3) ⚠ may be fragile
84
+ dotnet Update .NET global tools (requires: dotnet)
85
+
86
+ System / Shell:
87
+ ohmyzsh Update Oh My Zsh (requires: ~/.oh-my-zsh)
88
+
89
+ System / Maintenance:
90
+ cleanup homebrew cleanup (remove outdated Homebrew cache/downloads)
91
+
92
+ Examples:
93
+ braeburn Run all enabled steps interactively
94
+ braeburn -y Run all enabled steps, auto-accept everything
95
+ braeburn -fy Same as above
96
+ braeburn homebrew npm Run only the homebrew and npm steps
97
+ braeburn homebrew -y Run only homebrew, auto-accept
98
+ braeburn nvm pyenv Run only the runtime steps
99
+ `)
100
+ .action(async (stepArguments, updateOptions) => {
101
+ const autoYes = updateOptions.yes === true || updateOptions.force === true;
102
+ if (!(await dependencies.configFileExists())) {
103
+ await dependencies.runSetupCommand(allSteps);
104
+ }
105
+ const config = await dependencies.readConfig();
106
+ let stepsToRun = allSteps;
107
+ if (stepArguments.length > 0) {
108
+ const stepResolution = resolveStepsByIds(stepArguments, allSteps);
109
+ if (stepResolution.status === "unknown-step") {
110
+ processLike.stderr.write(`Unknown step: "${stepResolution.stepId}". Run braeburn --help to see available steps.\n`);
111
+ processLike.exitCode = 1;
112
+ return;
113
+ }
114
+ stepsToRun = stepResolution.steps;
115
+ }
116
+ if (stepArguments.length === 0) {
117
+ stepsToRun = stepsToRun.filter((step) => isStepEnabled(config, step.id));
118
+ }
119
+ const logoIsEnabled = updateOptions.logo !== false && isLogoEnabled(config);
120
+ const updateCommandResult = await dependencies.runUpdateCommand({
121
+ steps: stepsToRun,
122
+ promptMode: autoYes ? "auto-accept" : "interactive",
123
+ logoVisibility: logoIsEnabled ? "visible" : "hidden",
124
+ version: braeburnVersion,
125
+ });
126
+ dependencies.applyUpdateCommandResult(updateCommandResult, processLike);
127
+ });
128
+ program
129
+ .command("log")
130
+ .description("View the most recent output log for a given step")
131
+ .argument("[step]", "Step ID to view logs for (e.g. homebrew, npm, pip)")
132
+ .option("--homebrew", "Show latest Homebrew log")
133
+ .option("--brew", "Alias for --homebrew")
134
+ .option("--mas", "Show latest Mac App Store log")
135
+ .option("--ohmyzsh", "Show latest Oh My Zsh log")
136
+ .option("--npm", "Show latest npm log")
137
+ .option("--braeburn", "Show latest braeburn log")
138
+ .option("--pip", "Show latest pip3 log")
139
+ .option("--pyenv", "Show latest pyenv log")
140
+ .option("--nvm", "Show latest nvm log")
141
+ .option("--dotnet", "Show latest .NET log")
142
+ .option("--macos", "Show latest macOS update log")
143
+ .option("--cleanup", "Show latest homebrew cleanup log")
144
+ .addHelpText("after", `
145
+ Examples:
146
+ braeburn log List all available step logs
147
+ braeburn log homebrew Show the latest Homebrew run log
148
+ braeburn log --brew Same as above
149
+ braeburn log npm | less Pipe log output through less
150
+ `)
151
+ .action(async (stepArgument, logOptions) => {
152
+ const stepIdFromFlag = logOptions.brew === true
153
+ ? "homebrew"
154
+ : allSteps.map((step) => step.id).find((stepId) => logOptions[stepId] === true);
155
+ const resolvedStepId = stepArgument ?? stepIdFromFlag;
156
+ if (!resolvedStepId) {
157
+ dependencies.runLogListCommand();
158
+ return;
159
+ }
160
+ await dependencies.runLogCommand({ stepId: resolvedStepId });
161
+ });
162
+ const configCommand = program
163
+ .command("config")
164
+ .description("View or edit braeburn configuration")
165
+ .action(() => {
166
+ configCommand.outputHelp();
167
+ });
168
+ configCommand
169
+ .command("list")
170
+ .description("Print current configuration")
171
+ .action(async () => {
172
+ await dependencies.runConfigCommand({
173
+ allSteps,
174
+ outputMode: "non-interactive",
175
+ });
176
+ });
177
+ const configurableSteps = allSteps.filter((step) => !PROTECTED_STEP_IDS.has(step.id));
178
+ const configUpdateCommand = configCommand
179
+ .command("update")
180
+ .description("Edit configuration (interactive by default, flags for direct updates)")
181
+ .addHelpText("after", `
182
+ Examples:
183
+ braeburn config update Open interactive config editor
184
+ braeburn config update --no-logo Hide the logo
185
+ braeburn config update --no-ohmyzsh Disable Oh My Zsh updates
186
+ braeburn config update --no-pip --no-nvm Disable pip and nvm updates
187
+ braeburn config update --ohmyzsh Re-enable Oh My Zsh updates
188
+ `);
189
+ configUpdateCommand.option("--no-logo", "Hide the logo");
190
+ configUpdateCommand.option("--logo", "Show the logo");
191
+ for (const step of configurableSteps) {
192
+ configUpdateCommand.option(`--no-${step.id}`, `Disable ${step.name} updates`);
193
+ configUpdateCommand.option(`--${step.id}`, `Enable ${step.name} updates`);
194
+ }
195
+ configUpdateCommand.action(async function () {
196
+ // Commander defaults --no-* to true, so we use getOptionValueSource to detect explicit CLI flags.
197
+ const settingUpdates = {};
198
+ for (const step of configurableSteps) {
199
+ const source = configUpdateCommand.getOptionValueSource(step.id);
200
+ if (source === "cli") {
201
+ settingUpdates[step.id] = configUpdateCommand.opts()[step.id] ? "enable" : "disable";
202
+ }
203
+ }
204
+ const logoSource = configUpdateCommand.getOptionValueSource("logo");
205
+ if (logoSource === "cli") {
206
+ settingUpdates.logo = configUpdateCommand.opts().logo ? "enable" : "disable";
207
+ }
208
+ if (Object.keys(settingUpdates).length === 0) {
209
+ await dependencies.runConfigCommand({
210
+ allSteps,
211
+ outputMode: "interactive",
212
+ });
213
+ return;
214
+ }
215
+ await dependencies.runConfigUpdateCommand({ settingUpdates, allSteps });
216
+ });
217
+ return program;
218
+ }
219
+ export async function runBraeburnCli(argv = process.argv) {
220
+ try {
221
+ await createBraeburnProgram().parseAsync(argv);
222
+ }
223
+ catch (error) {
224
+ reportCliError(error);
225
+ }
226
+ }
@@ -1,3 +1,4 @@
1
+ import { type BraeburnConfig } from "../config.js";
1
2
  import { type Step, type StepCategoryId } from "../steps/index.js";
2
3
  export type SelectionState = "selected" | "deselected";
3
4
  export type ProtectionStatus = "protected" | "configurable";
@@ -17,4 +18,6 @@ export type SelectableStep = {
17
18
  };
18
19
  export declare function buildLoadingScreen(): string;
19
20
  export declare function buildSetupScreen(items: SelectableStep[], cursorIndex: number): string;
21
+ export declare function buildSetupItems(allSteps: Step[], availabilityResults: boolean[]): SelectableStep[];
22
+ export declare function buildConfigFromSetupItems(items: SelectableStep[]): BraeburnConfig;
20
23
  export declare function runSetupCommand(allSteps: Step[]): Promise<void>;
@@ -64,26 +64,42 @@ export function buildSetupScreen(items, cursorIndex) {
64
64
  lines.push("");
65
65
  return lines.join("\n") + "\n";
66
66
  }
67
+ export function buildSetupItems(allSteps, availabilityResults) {
68
+ return allSteps.map((step, stepIndex) => ({
69
+ step: {
70
+ id: step.id,
71
+ name: step.name,
72
+ description: step.description,
73
+ categoryId: step.categoryId,
74
+ brewPackageToInstall: step.brewPackageToInstall,
75
+ },
76
+ selection: PROTECTED_STEP_IDS.has(step.id) || CONSERVATIVE_DEFAULT_ON_STEP_IDS.has(step.id)
77
+ ? "selected"
78
+ : "deselected",
79
+ protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
80
+ availability: availabilityResults[stepIndex] ? "available" : "unavailable",
81
+ }));
82
+ }
83
+ export function buildConfigFromSetupItems(items) {
84
+ const draftConfig = {
85
+ defaultsProfile: "conservative-v2",
86
+ steps: {},
87
+ };
88
+ for (const item of items) {
89
+ if (item.protection === "protected") {
90
+ continue;
91
+ }
92
+ draftConfig.steps[item.step.id] = item.selection === "selected";
93
+ }
94
+ return cleanConfigForWrite(draftConfig);
95
+ }
67
96
  export async function runSetupCommand(allSteps) {
68
97
  const render = createScreenRenderer();
69
98
  const restoreCursor = hideCursorDuringExecution({ screenBuffer: "alternate" });
70
99
  try {
71
100
  render(buildLoadingScreen());
72
101
  const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
73
- const items = allSteps.map((step, stepIndex) => ({
74
- step: {
75
- id: step.id,
76
- name: step.name,
77
- description: step.description,
78
- categoryId: step.categoryId,
79
- brewPackageToInstall: step.brewPackageToInstall,
80
- },
81
- selection: PROTECTED_STEP_IDS.has(step.id) || CONSERVATIVE_DEFAULT_ON_STEP_IDS.has(step.id)
82
- ? "selected"
83
- : "deselected",
84
- protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
85
- availability: availabilityResults[stepIndex] ? "available" : "unavailable",
86
- }));
102
+ const items = buildSetupItems(allSteps, availabilityResults);
87
103
  let cursorIndex = 0;
88
104
  render(buildSetupScreen(items, cursorIndex));
89
105
  await new Promise((resolve) => {
@@ -120,17 +136,7 @@ export async function runSetupCommand(allSteps) {
120
136
  process.stdin.on("keypress", handleKeypress);
121
137
  process.stdin.resume();
122
138
  });
123
- const draftConfig = {
124
- defaultsProfile: "conservative-v2",
125
- steps: {},
126
- };
127
- for (const item of items) {
128
- if (item.protection === "protected") {
129
- continue;
130
- }
131
- draftConfig.steps[item.step.id] = item.selection === "selected";
132
- }
133
- await writeConfig(cleanConfigForWrite(draftConfig));
139
+ await writeConfig(buildConfigFromSetupItems(items));
134
140
  const confirmationLines = [
135
141
  chalk.yellow(LOGO_ART),
136
142
  "",
@@ -1,5 +1,5 @@
1
1
  import { type PromptMode } from "../update/engine.js";
2
- import { type LogoVisibility } from "../update/state.js";
2
+ import { type LogoVisibility, type UpdateState } from "../update/state.js";
3
3
  import type { Step } from "../steps/index.js";
4
4
  type RunUpdateCommandOptions = {
5
5
  steps: Step[];
@@ -13,6 +13,7 @@ export type UpdateCommandResult = {
13
13
  type ExitCodeWritable = {
14
14
  exitCode: string | number | null | undefined;
15
15
  };
16
+ export declare function shouldRenderRuntimeStateImmediately(state: UpdateState, lastRuntimeRenderTime: number, currentTime: number): boolean;
16
17
  export declare function applyUpdateCommandResult(updateCommandResult: UpdateCommandResult, processWithExitCode: ExitCodeWritable): void;
17
18
  export declare function runUpdateCommand(options: RunUpdateCommandOptions): Promise<UpdateCommandResult>;
18
19
  export {};
@@ -6,9 +6,19 @@ import { hideCursorDuringExecution } from "../ui/terminal.js";
6
6
  import { runUpdateEngine } from "../update/engine.js";
7
7
  import { countFailedSteps } from "../update/state.js";
8
8
  import { cancelActiveShellCommand } from "../runner.js";
9
+ const RUNTIME_RENDER_INTERVAL_MS = 100;
9
10
  function shouldCaptureRuntimeAbortKey(state) {
10
11
  return state?.currentPhase === "running" || state?.currentPhase === "installing";
11
12
  }
13
+ function shouldThrottleRuntimeRender(state) {
14
+ return state.runCompletion !== "finished" && shouldCaptureRuntimeAbortKey(state);
15
+ }
16
+ export function shouldRenderRuntimeStateImmediately(state, lastRuntimeRenderTime, currentTime) {
17
+ if (!shouldThrottleRuntimeRender(state)) {
18
+ return true;
19
+ }
20
+ return currentTime - lastRuntimeRenderTime >= RUNTIME_RENDER_INTERVAL_MS;
21
+ }
12
22
  export function applyUpdateCommandResult(updateCommandResult, processWithExitCode) {
13
23
  if (updateCommandResult.failedStepCount > 0) {
14
24
  processWithExitCode.exitCode = 1;
@@ -22,6 +32,25 @@ export async function runUpdateCommand(options) {
22
32
  let runtimeAbortKeyCaptureEnabled = false;
23
33
  let animationFrameIndex = 0;
24
34
  let updateCommandResult = { failedStepCount: 0 };
35
+ let lastRuntimeRenderTime = 0;
36
+ const renderState = (state) => {
37
+ renderScreen(buildScreenWithAnimationFrame(state, animationFrameIndex));
38
+ if (shouldThrottleRuntimeRender(state)) {
39
+ lastRuntimeRenderTime = Date.now();
40
+ }
41
+ };
42
+ const renderStateWithRuntimeThrottle = (state) => {
43
+ if (!shouldThrottleRuntimeRender(state)) {
44
+ renderState(state);
45
+ return;
46
+ }
47
+ const currentTime = Date.now();
48
+ if (shouldRenderRuntimeStateImmediately(state, lastRuntimeRenderTime, currentTime)) {
49
+ renderState(state);
50
+ return;
51
+ }
52
+ // The animation timer renders the latest runtime state at the next frame.
53
+ };
25
54
  const handleRuntimeKeypress = (typedCharacter, key) => {
26
55
  if (key?.ctrl && key?.name === "c") {
27
56
  process.stdout.write("\x1b[?25h\n");
@@ -67,8 +96,8 @@ export async function runUpdateCommand(options) {
67
96
  return;
68
97
  }
69
98
  animationFrameIndex += 1;
70
- renderScreen(buildScreenWithAnimationFrame(latestState, animationFrameIndex));
71
- }, 100);
99
+ renderState(latestState);
100
+ }, RUNTIME_RENDER_INTERVAL_MS);
72
101
  try {
73
102
  const finalState = await runUpdateEngine({
74
103
  steps: options.steps,
@@ -85,7 +114,7 @@ export async function runUpdateCommand(options) {
85
114
  else {
86
115
  disableRuntimeAbortKeyCapture();
87
116
  }
88
- renderScreen(buildScreenWithAnimationFrame(state, animationFrameIndex));
117
+ renderStateWithRuntimeThrottle(state);
89
118
  },
90
119
  });
91
120
  finalScreen = buildScreen(finalState);
package/dist/config.d.ts CHANGED
@@ -7,10 +7,18 @@ export type BraeburnConfig = {
7
7
  logo?: boolean;
8
8
  defaultsProfile?: ConfigDefaultsProfile;
9
9
  };
10
+ export declare class ConfigReadError extends Error {
11
+ readonly configPath: string;
12
+ readonly originalError: unknown;
13
+ constructor(configPath: string, originalError: unknown);
14
+ }
15
+ export declare function parseConfig(rawConfig: string): BraeburnConfig;
10
16
  export declare function resolveConfigPath(): Promise<string>;
11
17
  export declare function configFileExists(): Promise<boolean>;
12
18
  export declare function readConfig(): Promise<BraeburnConfig>;
19
+ export declare function readConfigFromPath(configPath: string): Promise<BraeburnConfig>;
13
20
  export declare function writeConfig(config: BraeburnConfig): Promise<void>;
21
+ export declare function writeConfigToPath(configPath: string, config: BraeburnConfig): Promise<void>;
14
22
  export declare function isSettingEnabled(config: BraeburnConfig, settingId: string): boolean;
15
23
  export declare function isStepEnabled(config: BraeburnConfig, stepId: string): boolean;
16
24
  export declare function isLogoEnabled(config: BraeburnConfig): boolean;
package/dist/config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFile, writeFile, mkdir, access } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { parse, stringify } from "smol-toml";
5
5
  export const PROTECTED_STEP_IDS = new Set(["homebrew"]);
@@ -9,14 +9,67 @@ const EMPTY_CONFIG = { steps: {} };
9
9
  const LOGO_SETTING_ID = "logo";
10
10
  const DEFAULTS_PROFILE_SETTING_ID = "defaultsProfile";
11
11
  const LEGACY_PROFILE = "legacy";
12
+ export class ConfigReadError extends Error {
13
+ configPath;
14
+ originalError;
15
+ constructor(configPath, originalError) {
16
+ const reason = originalError instanceof Error ? originalError.message : String(originalError);
17
+ super(`Could not read braeburn config at ${configPath}: ${reason}`);
18
+ this.name = "ConfigReadError";
19
+ this.configPath = configPath;
20
+ this.originalError = originalError;
21
+ }
22
+ }
12
23
  function resolveDefaultsProfile(config) {
13
24
  return config.defaultsProfile ?? LEGACY_PROFILE;
14
25
  }
15
26
  function parseDefaultsProfile(rawValue) {
27
+ if (rawValue === undefined) {
28
+ return undefined;
29
+ }
16
30
  if (rawValue === "legacy" || rawValue === "conservative-v2") {
17
31
  return rawValue;
18
32
  }
19
- return undefined;
33
+ throw new Error(`"${DEFAULTS_PROFILE_SETTING_ID}" must be "legacy" or "conservative-v2".`);
34
+ }
35
+ function parseSteps(rawValue) {
36
+ if (rawValue === undefined) {
37
+ return {};
38
+ }
39
+ if (typeof rawValue !== "object" || rawValue === null || Array.isArray(rawValue)) {
40
+ throw new Error('"steps" must be a table of step IDs to boolean values.');
41
+ }
42
+ const steps = {};
43
+ for (const [stepId, enabled] of Object.entries(rawValue)) {
44
+ if (typeof enabled !== "boolean") {
45
+ throw new Error(`"steps.${stepId}" must be true or false.`);
46
+ }
47
+ steps[stepId] = enabled;
48
+ }
49
+ return steps;
50
+ }
51
+ function parseLogo(rawValue) {
52
+ if (rawValue === undefined || typeof rawValue === "boolean") {
53
+ return rawValue;
54
+ }
55
+ throw new Error('"logo" must be true or false.');
56
+ }
57
+ function isMissingFileError(error) {
58
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
59
+ }
60
+ export function parseConfig(rawConfig) {
61
+ const parsed = parse(rawConfig);
62
+ const steps = parseSteps(parsed.steps);
63
+ const logo = parseLogo(parsed.logo);
64
+ const defaultsProfile = parseDefaultsProfile(parsed[DEFAULTS_PROFILE_SETTING_ID]);
65
+ const config = { steps };
66
+ if (logo !== undefined) {
67
+ config.logo = logo;
68
+ }
69
+ if (defaultsProfile !== undefined) {
70
+ config.defaultsProfile = defaultsProfile;
71
+ }
72
+ return config;
20
73
  }
21
74
  function isStepEnabledByDefault(profile, stepId) {
22
75
  if (profile === "conservative-v2") {
@@ -49,20 +102,26 @@ export async function configFileExists() {
49
102
  }
50
103
  export async function readConfig() {
51
104
  const configPath = await resolveConfigPath();
105
+ return readConfigFromPath(configPath);
106
+ }
107
+ export async function readConfigFromPath(configPath) {
52
108
  try {
53
109
  const raw = await readFile(configPath, "utf-8");
54
- const parsed = parse(raw);
55
- const parsedSteps = parsed.steps;
56
- const defaultsProfile = parseDefaultsProfile(parsed[DEFAULTS_PROFILE_SETTING_ID]);
57
- return { steps: parsedSteps ?? {}, logo: parsed.logo, defaultsProfile };
110
+ return parseConfig(raw);
58
111
  }
59
- catch {
60
- return structuredClone(EMPTY_CONFIG);
112
+ catch (error) {
113
+ if (isMissingFileError(error)) {
114
+ return structuredClone(EMPTY_CONFIG);
115
+ }
116
+ throw new ConfigReadError(configPath, error);
61
117
  }
62
118
  }
63
119
  export async function writeConfig(config) {
64
120
  const configPath = await resolveConfigPath();
65
- await mkdir(join(configPath, ".."), { recursive: true });
121
+ await writeConfigToPath(configPath, config);
122
+ }
123
+ export async function writeConfigToPath(configPath, config) {
124
+ await mkdir(dirname(configPath), { recursive: true });
66
125
  await writeFile(configPath, stringify(config), "utf-8");
67
126
  }
68
127
  export function isSettingEnabled(config, settingId) {
package/dist/index.js CHANGED
@@ -1,179 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import { createRequire } from "node:module";
4
- import { fileURLToPath } from "node:url";
5
- import { dirname, join } from "node:path";
6
- import { ALL_STEPS } from "./steps/catalog.js";
7
- import { applyUpdateCommandResult, runUpdateCommand } from "./commands/update.js";
8
- import { runLogCommand, runLogListCommand } from "./commands/log.js";
9
- import { runConfigCommand, runConfigUpdateCommand } from "./commands/config.js";
10
- import { runSetupCommand } from "./commands/setup.js";
11
- import { readConfig, isStepEnabled, isLogoEnabled, PROTECTED_STEP_IDS, configFileExists } from "./config.js";
12
- const STEP_IDS_BY_NAME = new Map(ALL_STEPS.map((step) => [step.id, step]));
13
- const requireFromThis = createRequire(import.meta.url);
14
- const packageJson = requireFromThis(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"));
15
- const BRAEBURN_VERSION = packageJson.version;
16
- const program = new Command();
17
- program
18
- .name("braeburn")
19
- .description("macOS system updater")
20
- .version(BRAEBURN_VERSION)
21
- .helpCommand(false);
22
- program
23
- .command("update", { isDefault: true })
24
- .description("Run system update steps (default command)")
25
- .argument("[steps...]", `Steps to run — omit to run all.\nAvailable: ${ALL_STEPS.map((step) => step.id).join(", ")}`)
26
- .option("-y, --yes", "Auto-accept all prompts (default yes to everything)")
27
- .option("-f, --force", "Alias for --yes")
28
- .option("--no-logo", "Hide the logo")
29
- .addHelpText("after", `
30
- Step descriptions:
31
- System / Runtimes (default: off — larger changes, enabled intentionally):
32
- pyenv Upgrade pyenv, install latest Python 3.x (requires: pyenv or brew)
33
- nvm Install latest Node.js via nvm (requires: ~/.nvm)
34
-
35
- System / Apps & Packages:
36
- homebrew Update Homebrew itself and all installed formulae
37
- mas Upgrade Mac App Store apps (requires: mas)
38
- macos Check for macOS updates, prompt to install
39
-
40
- System / CLI Tools:
41
- npm Update global npm packages (requires: npm)
42
- braeburn Update braeburn CLI itself (requires: npm)
43
- pip Update global pip3 packages (requires: pip3) ⚠ may be fragile
44
- dotnet Update .NET global tools (requires: dotnet)
45
-
46
- System / Shell:
47
- ohmyzsh Update Oh My Zsh (requires: ~/.oh-my-zsh)
48
-
49
- System / Maintenance:
50
- cleanup homebrew cleanup (remove outdated Homebrew cache/downloads)
51
-
52
- Examples:
53
- braeburn Run all enabled steps interactively
54
- braeburn -y Run all enabled steps, auto-accept everything
55
- braeburn -fy Same as above
56
- braeburn homebrew npm Run only the homebrew and npm steps
57
- braeburn homebrew -y Run only homebrew, auto-accept
58
- braeburn nvm pyenv Run only the runtime steps
59
- `)
60
- .action(async (stepArguments, options) => {
61
- const autoYes = options.yes === true || options.force === true;
62
- if (!(await configFileExists())) {
63
- await runSetupCommand(ALL_STEPS);
64
- }
65
- const config = await readConfig();
66
- let stepsToRun = stepArguments.length === 0
67
- ? ALL_STEPS
68
- : resolveStepsByIds(stepArguments);
69
- if (stepArguments.length === 0) {
70
- stepsToRun = stepsToRun.filter((step) => isStepEnabled(config, step.id));
71
- }
72
- const logoIsEnabled = options.logo !== false && isLogoEnabled(config);
73
- const updateCommandResult = await runUpdateCommand({
74
- steps: stepsToRun,
75
- promptMode: autoYes ? "auto-accept" : "interactive",
76
- logoVisibility: logoIsEnabled ? "visible" : "hidden",
77
- version: BRAEBURN_VERSION,
78
- });
79
- applyUpdateCommandResult(updateCommandResult, process);
80
- });
81
- program
82
- .command("log")
83
- .description("View the most recent output log for a given step")
84
- .argument("[step]", "Step ID to view logs for (e.g. homebrew, npm, pip)")
85
- .option("--homebrew", "Show latest Homebrew log")
86
- .option("--mas", "Show latest Mac App Store log")
87
- .option("--ohmyzsh", "Show latest Oh My Zsh log")
88
- .option("--npm", "Show latest npm log")
89
- .option("--braeburn", "Show latest braeburn log")
90
- .option("--pip", "Show latest pip3 log")
91
- .option("--pyenv", "Show latest pyenv log")
92
- .option("--nvm", "Show latest nvm log")
93
- .option("--dotnet", "Show latest .NET log")
94
- .option("--macos", "Show latest macOS update log")
95
- .option("--cleanup", "Show latest homebrew cleanup log")
96
- .addHelpText("after", `
97
- Examples:
98
- braeburn log List all available step logs
99
- braeburn log homebrew Show the latest Homebrew run log
100
- braeburn log --brew Same as above
101
- braeburn log npm | less Pipe log output through less
102
- `)
103
- .action((stepArgument, options) => {
104
- const stepIdFromFlag = ALL_STEPS.map((step) => step.id).find((stepId) => options[stepId] === true);
105
- const resolvedStepId = stepArgument ?? stepIdFromFlag;
106
- if (!resolvedStepId) {
107
- runLogListCommand();
108
- return;
109
- }
110
- runLogCommand({ stepId: resolvedStepId });
111
- });
112
- const configCommand = program
113
- .command("config")
114
- .description("View or edit braeburn configuration")
115
- .action(() => {
116
- configCommand.outputHelp();
117
- });
118
- configCommand
119
- .command("list")
120
- .description("Print current configuration")
121
- .action(async () => {
122
- await runConfigCommand({
123
- allSteps: ALL_STEPS,
124
- outputMode: "non-interactive",
125
- });
126
- });
127
- const configurableSteps = ALL_STEPS.filter((step) => !PROTECTED_STEP_IDS.has(step.id));
128
- const configUpdateCommand = configCommand
129
- .command("update")
130
- .description("Edit configuration (interactive by default, flags for direct updates)")
131
- .addHelpText("after", `
132
- Examples:
133
- braeburn config update Open interactive config editor
134
- braeburn config update --no-logo Hide the logo
135
- braeburn config update --no-ohmyzsh Disable Oh My Zsh updates
136
- braeburn config update --no-pip --no-nvm Disable pip and nvm updates
137
- braeburn config update --ohmyzsh Re-enable Oh My Zsh updates
138
- `);
139
- configUpdateCommand.option(`--no-logo`, `Hide the logo`);
140
- configUpdateCommand.option(`--logo`, `Show the logo`);
141
- for (const step of configurableSteps) {
142
- configUpdateCommand.option(`--no-${step.id}`, `Disable ${step.name} updates`);
143
- configUpdateCommand.option(`--${step.id}`, `Enable ${step.name} updates`);
144
- }
145
- configUpdateCommand.action(async function () {
146
- // Commander defaults --no-* to true, so we use getOptionValueSource to detect explicit CLI flags.
147
- const settingUpdates = {};
148
- for (const step of configurableSteps) {
149
- const source = configUpdateCommand.getOptionValueSource(step.id);
150
- if (source === "cli") {
151
- settingUpdates[step.id] = configUpdateCommand.opts()[step.id] ? "enable" : "disable";
152
- }
153
- }
154
- const logoSource = configUpdateCommand.getOptionValueSource("logo");
155
- if (logoSource === "cli") {
156
- settingUpdates["logo"] = configUpdateCommand.opts().logo ? "enable" : "disable";
157
- }
158
- if (Object.keys(settingUpdates).length === 0) {
159
- await runConfigCommand({
160
- allSteps: ALL_STEPS,
161
- outputMode: "interactive",
162
- });
163
- return;
164
- }
165
- await runConfigUpdateCommand({ settingUpdates, allSteps: ALL_STEPS });
166
- });
167
- function resolveStepsByIds(stepIds) {
168
- const resolvedSteps = [];
169
- for (const stepId of stepIds) {
170
- const step = STEP_IDS_BY_NAME.get(stepId);
171
- if (!step) {
172
- process.stderr.write(`Unknown step: "${stepId}". Run braeburn --help to see available steps.\n`);
173
- process.exit(1);
174
- }
175
- resolvedSteps.push(step);
176
- }
177
- return resolvedSteps;
178
- }
179
- program.parse();
2
+ import { runBraeburnCli } from "./cli.js";
3
+ await runBraeburnCli();
package/dist/runner.d.ts CHANGED
@@ -10,11 +10,17 @@ type RunCommandOptions = {
10
10
  onOutputLine: OutputLineCallback;
11
11
  logWriter: StepLogWriter;
12
12
  };
13
+ export type ShellCommandRunner = {
14
+ cancelActiveShellCommand: () => boolean;
15
+ runShellCommand: (options: RunCommandOptions) => Promise<void>;
16
+ runShellCommandAndCaptureOutput: (options: RunCommandOptions) => Promise<string>;
17
+ };
13
18
  export declare class ShellCommandCanceledError extends Error {
14
19
  readonly shellCommand: string;
15
20
  readonly originalError: unknown;
16
21
  constructor(shellCommand: string, originalError: unknown);
17
22
  }
23
+ export declare function createShellCommandRunner(): ShellCommandRunner;
18
24
  export declare function cancelActiveShellCommand(): boolean;
19
25
  export declare function runShellCommand(options: RunCommandOptions): Promise<void>;
20
26
  export declare function runShellCommandAndCaptureOutput(options: RunCommandOptions): Promise<string>;
package/dist/runner.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { execa } from "execa";
2
2
  const FAILURE_OUTPUT_TAIL_LINE_LIMIT = 20;
3
- let activeShellCommandSubprocess;
4
- const userCanceledSubprocesses = new WeakSet();
3
+ const DEFAULT_CAPTURE_MODE = "disabled";
4
+ const MAX_BUFFERED_OUTPUT_LINE_LENGTH = 16_384;
5
5
  export class ShellCommandCanceledError extends Error {
6
6
  shellCommand;
7
7
  originalError;
@@ -12,21 +12,30 @@ export class ShellCommandCanceledError extends Error {
12
12
  this.originalError = originalError;
13
13
  }
14
14
  }
15
- export function cancelActiveShellCommand() {
16
- if (!activeShellCommandSubprocess) {
15
+ function createLimitedLineBuffer(lineLimit) {
16
+ const lines = [];
17
+ return {
18
+ add(line) {
19
+ lines.push(line);
20
+ if (lines.length > lineLimit) {
21
+ lines.shift();
22
+ }
23
+ },
24
+ lines() {
25
+ return [...lines];
26
+ },
27
+ };
28
+ }
29
+ function cancelActiveShellCommandForState(state) {
30
+ if (!state.activeShellCommandSubprocess) {
17
31
  return false;
18
32
  }
19
- userCanceledSubprocesses.add(activeShellCommandSubprocess);
20
- activeShellCommandSubprocess.kill("SIGTERM");
33
+ state.userCanceledSubprocesses.add(state.activeShellCommandSubprocess);
34
+ state.activeShellCommandSubprocess.kill("SIGTERM");
21
35
  return true;
22
36
  }
23
- function splitNonEmptyLines(text) {
24
- if (!text) {
25
- return [];
26
- }
27
- return text.split(/\r?\n|\r/).filter(Boolean);
28
- }
29
- function buildFailureSummaryLines(shellCommand, error) {
37
+ function buildFailureSummaryLines(details) {
38
+ const { shellCommand, error, stdoutTailLines, stderrTailLines } = details;
30
39
  const defaultMessage = error instanceof Error ? error.message : String(error);
31
40
  if (typeof error !== "object" || error === null) {
32
41
  return [
@@ -48,12 +57,10 @@ function buildFailureSummaryLines(shellCommand, error) {
48
57
  else {
49
58
  summaryLines.push(`[braeburn] Error: ${defaultMessage}`);
50
59
  }
51
- const stderrTailLines = splitNonEmptyLines(errorDetails.stderr).slice(-FAILURE_OUTPUT_TAIL_LINE_LIMIT);
52
60
  if (stderrTailLines.length > 0) {
53
61
  summaryLines.push(`[braeburn] stderr tail (${stderrTailLines.length}):`);
54
62
  summaryLines.push(...stderrTailLines.map((line) => ` ${line}`));
55
63
  }
56
- const stdoutTailLines = splitNonEmptyLines(errorDetails.stdout).slice(-FAILURE_OUTPUT_TAIL_LINE_LIMIT);
57
64
  if (stdoutTailLines.length > 0) {
58
65
  summaryLines.push(`[braeburn] stdout tail (${stdoutTailLines.length}):`);
59
66
  summaryLines.push(...stdoutTailLines.map((line) => ` ${line}`));
@@ -73,6 +80,10 @@ function createBufferedLineEmitter(emitLine) {
73
80
  }
74
81
  emitLine(linePart);
75
82
  }
83
+ while (pendingText.length > MAX_BUFFERED_OUTPUT_LINE_LENGTH) {
84
+ emitLine(pendingText.slice(0, MAX_BUFFERED_OUTPUT_LINE_LENGTH));
85
+ pendingText = pendingText.slice(MAX_BUFFERED_OUTPUT_LINE_LENGTH);
86
+ }
76
87
  },
77
88
  flushPendingLine() {
78
89
  if (pendingText.length === 0) {
@@ -83,17 +94,46 @@ function createBufferedLineEmitter(emitLine) {
83
94
  },
84
95
  };
85
96
  }
86
- async function executeShellCommand(options) {
87
- const capturedOutputLines = [];
97
+ async function executeShellCommand(state, options, captureMode = DEFAULT_CAPTURE_MODE) {
98
+ const capturedOutputLines = captureMode === "enabled" ? [] : undefined;
99
+ const stdoutTailLines = createLimitedLineBuffer(FAILURE_OUTPUT_TAIL_LINE_LIMIT);
100
+ const stderrTailLines = createLimitedLineBuffer(FAILURE_OUTPUT_TAIL_LINE_LIMIT);
101
+ let logWriteQueue = Promise.resolve();
102
+ let logWriteFailure;
88
103
  const subprocess = execa("bash", ["-c", options.shellCommand], {
89
- all: true,
104
+ buffer: false,
90
105
  reject: true,
91
106
  });
92
- activeShellCommandSubprocess = subprocess;
107
+ state.activeShellCommandSubprocess = subprocess;
108
+ const queueLogWrite = (line) => {
109
+ logWriteQueue = logWriteQueue.then(async () => {
110
+ if (logWriteFailure) {
111
+ return;
112
+ }
113
+ try {
114
+ await options.logWriter(line);
115
+ }
116
+ catch (error) {
117
+ logWriteFailure = error;
118
+ }
119
+ });
120
+ };
121
+ const waitForQueuedLogWrites = async () => {
122
+ await logWriteQueue;
123
+ if (logWriteFailure) {
124
+ throw logWriteFailure;
125
+ }
126
+ };
93
127
  const emitOutputLine = (line) => {
94
- capturedOutputLines.push(line.text);
128
+ if (line.source === "stdout") {
129
+ stdoutTailLines.add(line.text);
130
+ }
131
+ else {
132
+ stderrTailLines.add(line.text);
133
+ }
134
+ capturedOutputLines?.push(line.text);
95
135
  options.onOutputLine(line);
96
- options.logWriter(line.text);
136
+ queueLogWrite(line.text);
97
137
  };
98
138
  const stdoutEmitter = createBufferedLineEmitter((line) => {
99
139
  emitOutputLine({ text: line, source: "stdout" });
@@ -111,36 +151,64 @@ async function executeShellCommand(options) {
111
151
  await subprocess;
112
152
  stdoutEmitter.flushPendingLine();
113
153
  stderrEmitter.flushPendingLine();
114
- return { capturedOutput: capturedOutputLines.join("\n") };
154
+ await waitForQueuedLogWrites();
155
+ return { capturedOutput: capturedOutputLines?.join("\n") };
115
156
  }
116
157
  catch (error) {
117
158
  stdoutEmitter.flushPendingLine();
118
159
  stderrEmitter.flushPendingLine();
119
- const commandWasCanceledByUser = userCanceledSubprocesses.has(subprocess);
120
- const failureSummaryLines = buildFailureSummaryLines(options.shellCommand, error);
160
+ const commandWasCanceledByUser = state.userCanceledSubprocesses.has(subprocess);
161
+ const failureSummaryLines = buildFailureSummaryLines({
162
+ shellCommand: options.shellCommand,
163
+ error,
164
+ stdoutTailLines: stdoutTailLines.lines(),
165
+ stderrTailLines: stderrTailLines.lines(),
166
+ });
121
167
  if (commandWasCanceledByUser) {
122
168
  failureSummaryLines.push("[braeburn] Command canceled by user input (q).");
123
169
  }
124
170
  for (const line of failureSummaryLines) {
125
- await options.logWriter(line);
171
+ queueLogWrite(line);
126
172
  }
173
+ await waitForQueuedLogWrites();
127
174
  if (commandWasCanceledByUser) {
128
175
  throw new ShellCommandCanceledError(options.shellCommand, error);
129
176
  }
130
177
  throw error;
131
178
  }
132
179
  finally {
133
- if (activeShellCommandSubprocess === subprocess) {
134
- activeShellCommandSubprocess = undefined;
180
+ if (state.activeShellCommandSubprocess === subprocess) {
181
+ state.activeShellCommandSubprocess = undefined;
135
182
  }
136
183
  }
137
184
  }
185
+ export function createShellCommandRunner() {
186
+ const state = {
187
+ activeShellCommandSubprocess: undefined,
188
+ userCanceledSubprocesses: new WeakSet(),
189
+ };
190
+ return {
191
+ cancelActiveShellCommand() {
192
+ return cancelActiveShellCommandForState(state);
193
+ },
194
+ async runShellCommand(options) {
195
+ await executeShellCommand(state, options);
196
+ },
197
+ async runShellCommandAndCaptureOutput(options) {
198
+ const result = await executeShellCommand(state, options, "enabled");
199
+ return result.capturedOutput ?? "";
200
+ },
201
+ };
202
+ }
203
+ const defaultShellCommandRunner = createShellCommandRunner();
204
+ export function cancelActiveShellCommand() {
205
+ return defaultShellCommandRunner.cancelActiveShellCommand();
206
+ }
138
207
  export async function runShellCommand(options) {
139
- await executeShellCommand(options);
208
+ await defaultShellCommandRunner.runShellCommand(options);
140
209
  }
141
210
  export async function runShellCommandAndCaptureOutput(options) {
142
- const result = await executeShellCommand(options);
143
- return result.capturedOutput;
211
+ return defaultShellCommandRunner.runShellCommandAndCaptureOutput(options);
144
212
  }
145
213
  export async function doesShellCommandSucceed(options) {
146
214
  const result = await execa("bash", ["-c", options.shellCommand], {
@@ -9,6 +9,7 @@ type UpdateEngineDependencies = {
9
9
  runCommand: typeof runShellCommand;
10
10
  createStepRunContext: typeof createDefaultStepRunContext;
11
11
  };
12
+ export declare const CURRENT_OUTPUT_LINE_LIMIT = 200;
12
13
  type RunUpdateEngineOptions = {
13
14
  steps: Step[];
14
15
  promptMode: PromptMode;
@@ -3,6 +3,7 @@ import { runShellCommand, ShellCommandCanceledError, } from "../runner.js";
3
3
  import { createDefaultStepRunContext } from "../steps/index.js";
4
4
  import { toDisplaySteps } from "./displayStep.js";
5
5
  import { createInitialUpdateState, } from "./state.js";
6
+ export const CURRENT_OUTPUT_LINE_LIMIT = 200;
6
7
  function resolveDependencies(dependencyOverrides) {
7
8
  return {
8
9
  createLogWriter: dependencyOverrides?.createLogWriter ?? createLogWriterForStep,
@@ -26,6 +27,12 @@ async function resolvePrompt(promptMode, askForConfirmation) {
26
27
  function reportState(state, onStateChanged) {
27
28
  onStateChanged(state);
28
29
  }
30
+ function appendCurrentOutputLine(state, line) {
31
+ state.currentOutputLines.push(line);
32
+ if (state.currentOutputLines.length > CURRENT_OUTPUT_LINE_LIMIT) {
33
+ state.currentOutputLines.splice(0, state.currentOutputLines.length - CURRENT_OUTPUT_LINE_LIMIT);
34
+ }
35
+ }
29
36
  function wasStepCanceledByUser(error) {
30
37
  return error instanceof ShellCommandCanceledError;
31
38
  }
@@ -90,7 +97,7 @@ export async function runUpdateEngine(options) {
90
97
  await dependencies.runCommand({
91
98
  shellCommand: `brew install ${step.brewPackageToInstall}`,
92
99
  onOutputLine: (line) => {
93
- state.currentOutputLines.push(line);
100
+ appendCurrentOutputLine(state, line);
94
101
  reportState(state, options.onStateChanged);
95
102
  },
96
103
  logWriter: installLogWriter,
@@ -130,7 +137,7 @@ export async function runUpdateEngine(options) {
130
137
  const stepLogWriter = await dependencies.createLogWriter(step.id);
131
138
  try {
132
139
  await step.run(dependencies.createStepRunContext((line) => {
133
- state.currentOutputLines.push(line);
140
+ appendCurrentOutputLine(state, line);
134
141
  reportState(state, options.onStateChanged);
135
142
  }, stepLogWriter));
136
143
  state.currentPhase = "complete";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braeburn",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "macOS system updater CLI",
5
5
  "type": "module",
6
6
  "bin": {