braeburn 1.6.0 → 2.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.
@@ -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 } from "../update/state.js";
3
3
  import type { Step } from "../steps/index.js";
4
4
  type RunUpdateCommandOptions = {
5
5
  steps: Step[];
@@ -7,5 +7,12 @@ type RunUpdateCommandOptions = {
7
7
  logoVisibility: LogoVisibility;
8
8
  version: string;
9
9
  };
10
- export declare function runUpdateCommand(options: RunUpdateCommandOptions): Promise<void>;
10
+ export type UpdateCommandResult = {
11
+ failedStepCount: number;
12
+ };
13
+ type ExitCodeWritable = {
14
+ exitCode: string | number | null | undefined;
15
+ };
16
+ export declare function applyUpdateCommandResult(updateCommandResult: UpdateCommandResult, processWithExitCode: ExitCodeWritable): void;
17
+ export declare function runUpdateCommand(options: RunUpdateCommandOptions): Promise<UpdateCommandResult>;
11
18
  export {};
@@ -4,10 +4,16 @@ import { captureYesNo } from "../ui/prompt.js";
4
4
  import { buildScreen, buildScreenWithAnimationFrame, createScreenRenderer } from "../ui/screen.js";
5
5
  import { hideCursorDuringExecution } from "../ui/terminal.js";
6
6
  import { runUpdateEngine } from "../update/engine.js";
7
+ import { countFailedSteps } from "../update/state.js";
7
8
  import { cancelActiveShellCommand } from "../runner.js";
8
9
  function shouldCaptureRuntimeAbortKey(state) {
9
10
  return state?.currentPhase === "running" || state?.currentPhase === "installing";
10
11
  }
12
+ export function applyUpdateCommandResult(updateCommandResult, processWithExitCode) {
13
+ if (updateCommandResult.failedStepCount > 0) {
14
+ processWithExitCode.exitCode = 1;
15
+ }
16
+ }
11
17
  export async function runUpdateCommand(options) {
12
18
  const renderScreen = createScreenRenderer();
13
19
  const restoreCursor = hideCursorDuringExecution({ screenBuffer: "alternate" });
@@ -15,6 +21,7 @@ export async function runUpdateCommand(options) {
15
21
  let latestState = undefined;
16
22
  let runtimeAbortKeyCaptureEnabled = false;
17
23
  let animationFrameIndex = 0;
24
+ let updateCommandResult = { failedStepCount: 0 };
18
25
  const handleRuntimeKeypress = (typedCharacter, key) => {
19
26
  if (key?.ctrl && key?.name === "c") {
20
27
  process.stdout.write("\x1b[?25h\n");
@@ -82,6 +89,9 @@ export async function runUpdateCommand(options) {
82
89
  },
83
90
  });
84
91
  finalScreen = buildScreen(finalState);
92
+ updateCommandResult = {
93
+ failedStepCount: countFailedSteps(finalState.completedStepRecords),
94
+ };
85
95
  }
86
96
  finally {
87
97
  disableRuntimeAbortKeyCapture();
@@ -91,4 +101,5 @@ export async function runUpdateCommand(options) {
91
101
  if (finalScreen) {
92
102
  process.stdout.write(finalScreen);
93
103
  }
104
+ return updateCommandResult;
94
105
  }
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { createRequire } from "node:module";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { dirname, join } from "node:path";
6
6
  import { ALL_STEPS } from "./steps/catalog.js";
7
- import { runUpdateCommand } from "./commands/update.js";
7
+ import { applyUpdateCommandResult, runUpdateCommand } from "./commands/update.js";
8
8
  import { runLogCommand, runLogListCommand } from "./commands/log.js";
9
9
  import { runConfigCommand, runConfigUpdateCommand } from "./commands/config.js";
10
10
  import { runSetupCommand } from "./commands/setup.js";
@@ -70,12 +70,13 @@ Examples:
70
70
  stepsToRun = stepsToRun.filter((step) => isStepEnabled(config, step.id));
71
71
  }
72
72
  const logoIsEnabled = options.logo !== false && isLogoEnabled(config);
73
- await runUpdateCommand({
73
+ const updateCommandResult = await runUpdateCommand({
74
74
  steps: stepsToRun,
75
75
  promptMode: autoYes ? "auto-accept" : "interactive",
76
76
  logoVisibility: logoIsEnabled ? "visible" : "hidden",
77
77
  version: BRAEBURN_VERSION,
78
78
  });
79
+ applyUpdateCommandResult(updateCommandResult, process);
79
80
  });
80
81
  program
81
82
  .command("log")
@@ -1,5 +1,5 @@
1
1
  import { type TerminalDimensions } from "./outputLines.js";
2
- import type { AppState } from "./state.js";
2
+ import { type AppState } from "./state.js";
3
3
  export type ScreenRenderer = (content: string) => void;
4
4
  export declare function createScreenRenderer(output?: NodeJS.WritableStream): ScreenRenderer;
5
5
  export declare function buildScreen(state: AppState, terminalDimensions?: TerminalDimensions): string;
package/dist/ui/screen.js CHANGED
@@ -3,6 +3,7 @@ import { buildActiveStepLines } from "./currentStep.js";
3
3
  import { buildStepOutputLines } from "./outputLines.js";
4
4
  import { buildPromptLines } from "./prompt.js";
5
5
  import { buildFailedStepLogHintLines, buildVersionReportLines } from "./versionReport.js";
6
+ import { countFailedSteps } from "./state.js";
6
7
  export function createScreenRenderer(output = process.stdout) {
7
8
  return (content) => {
8
9
  output.write("\x1b[H\x1b[2J");
@@ -12,18 +13,48 @@ export function createScreenRenderer(output = process.stdout) {
12
13
  export function buildScreen(state, terminalDimensions) {
13
14
  return buildScreenWithAnimationFrame(state, 0, terminalDimensions);
14
15
  }
15
- export function buildScreenWithAnimationFrame(state, activityFrameIndex, terminalDimensions) {
16
- const lines = [];
17
- const failedStepIds = state.completedStepRecords.flatMap((record, stepIndex) => {
18
- if (record.phase !== "failed") {
16
+ function buildFailedStepLogHints(state) {
17
+ return state.completedStepRecords.flatMap((completedStepRecord, stepIndex) => {
18
+ if (completedStepRecord.phase !== "failed") {
19
19
  return [];
20
20
  }
21
21
  const failedStep = state.steps[stepIndex];
22
22
  if (!failedStep) {
23
23
  return [];
24
24
  }
25
- return [failedStep.id];
25
+ return [{
26
+ stepId: failedStep.id,
27
+ logStepId: completedStepRecord.logStepId ?? failedStep.id,
28
+ }];
26
29
  });
30
+ }
31
+ function findLatestFailedStepDisplay(state) {
32
+ for (let stepIndex = state.completedStepRecords.length - 1; stepIndex >= 0; stepIndex -= 1) {
33
+ const completedStepRecord = state.completedStepRecords[stepIndex];
34
+ if (completedStepRecord?.phase !== "failed") {
35
+ continue;
36
+ }
37
+ const failedStep = state.steps[stepIndex];
38
+ if (!failedStep) {
39
+ continue;
40
+ }
41
+ const failureOutputLines = completedStepRecord.failureOutputLines;
42
+ if (!failureOutputLines || failureOutputLines.length === 0) {
43
+ continue;
44
+ }
45
+ return {
46
+ step: failedStep,
47
+ stepNumber: stepIndex + 1,
48
+ failureOutputLines,
49
+ };
50
+ }
51
+ return undefined;
52
+ }
53
+ export function buildScreenWithAnimationFrame(state, activityFrameIndex, terminalDimensions) {
54
+ const lines = [];
55
+ const failedStepCount = countFailedSteps(state.completedStepRecords);
56
+ const failedStepLogHints = buildFailedStepLogHints(state);
57
+ const latestFailedStepDisplay = findLatestFailedStepDisplay(state);
27
58
  lines.push(...buildHeaderLines({
28
59
  steps: state.steps,
29
60
  version: state.version,
@@ -38,11 +69,26 @@ export function buildScreenWithAnimationFrame(state, activityFrameIndex, termina
38
69
  if (state.runCompletion === "finished") {
39
70
  if (state.versionReport) {
40
71
  lines.push("");
41
- lines.push(...buildVersionReportLines(state.versionReport));
72
+ lines.push(...buildVersionReportLines({
73
+ versions: state.versionReport,
74
+ failedStepCount,
75
+ }));
76
+ }
77
+ if (failedStepLogHints.length > 0) {
78
+ lines.push("");
79
+ lines.push(...buildFailedStepLogHintLines(failedStepLogHints));
42
80
  }
43
- if (failedStepIds.length > 0) {
81
+ if (latestFailedStepDisplay) {
82
+ lines.push("");
83
+ lines.push(...buildActiveStepLines({
84
+ step: latestFailedStepDisplay.step,
85
+ stepNumber: latestFailedStepDisplay.stepNumber,
86
+ totalSteps: state.steps.length,
87
+ phase: "failed",
88
+ activityFrameIndex,
89
+ }));
44
90
  lines.push("");
45
- lines.push(...buildFailedStepLogHintLines(failedStepIds));
91
+ lines.push(...buildStepOutputLines(latestFailedStepDisplay.failureOutputLines, terminalDimensions));
46
92
  }
47
93
  }
48
94
  else {
@@ -56,7 +102,9 @@ export function buildScreenWithAnimationFrame(state, activityFrameIndex, termina
56
102
  phase: state.currentPhase,
57
103
  activityFrameIndex,
58
104
  }));
59
- const isShowingOutput = (state.currentPhase === "running" || state.currentPhase === "installing") &&
105
+ const isShowingOutput = (state.currentPhase === "running" ||
106
+ state.currentPhase === "installing" ||
107
+ state.currentPhase === "failed") &&
60
108
  state.currentOutputLines.length > 0;
61
109
  if (isShowingOutput) {
62
110
  lines.push("");
@@ -1 +1 @@
1
- export { type DisplayStep, type StepPhase, type CompletedStepRecord, type CurrentPrompt, type ResolvedVersion, type LogoVisibility, type RunCompletion, type UpdateState, type AppState, createInitialUpdateState, createInitialAppState, } from "../update/state.js";
1
+ export { type DisplayStep, type StepPhase, type CompletedStepRecord, type CurrentPrompt, type ResolvedVersion, type LogoVisibility, type RunCompletion, type UpdateState, type AppState, createInitialUpdateState, createInitialAppState, countFailedSteps, } from "../update/state.js";
package/dist/ui/state.js CHANGED
@@ -1 +1 @@
1
- export { createInitialUpdateState, createInitialAppState, } from "../update/state.js";
1
+ export { createInitialUpdateState, createInitialAppState, countFailedSteps, } from "../update/state.js";
@@ -1,3 +1,11 @@
1
1
  import type { ResolvedVersion } from "./state.js";
2
- export declare function buildVersionReportLines(versions: ResolvedVersion[]): string[];
3
- export declare function buildFailedStepLogHintLines(failedStepIds: string[]): string[];
2
+ export type VersionReportOptions = {
3
+ versions: ResolvedVersion[];
4
+ failedStepCount: number;
5
+ };
6
+ export type FailedStepLogHint = {
7
+ stepId: string;
8
+ logStepId: string;
9
+ };
10
+ export declare function buildVersionReportLines(options: VersionReportOptions): string[];
11
+ export declare function buildFailedStepLogHintLines(failedStepLogHints: FailedStepLogHint[]): string[];
@@ -1,12 +1,19 @@
1
1
  import chalk from "chalk";
2
- export function buildVersionReportLines(versions) {
2
+ function buildCompletionSummaryLine(failedStepCount) {
3
+ if (failedStepCount === 0) {
4
+ return ` ${chalk.green.bold("✓")} ${chalk.bold("All done!")}`;
5
+ }
6
+ const failureLabel = failedStepCount === 1 ? "1 step failed" : `${failedStepCount} steps failed`;
7
+ return ` ${chalk.red.bold("✗")} ${chalk.bold(`Done (${failureLabel})`)}`;
8
+ }
9
+ export function buildVersionReportLines(options) {
3
10
  return [
4
11
  chalk.dim(" ─── Versions ─────────────────────────"),
5
- ...versions.map(({ label, value }) => ` ${chalk.dim("·")} ${chalk.bold(label + ":")} ${chalk.dim(value)}`),
12
+ ...options.versions.map(({ label, value }) => ` ${chalk.dim("·")} ${chalk.bold(label + ":")} ${chalk.dim(value)}`),
6
13
  "",
7
- ` ${chalk.green.bold("✓")} ${chalk.bold("All done!")}`,
14
+ buildCompletionSummaryLine(options.failedStepCount),
8
15
  ];
9
16
  }
10
- export function buildFailedStepLogHintLines(failedStepIds) {
11
- return failedStepIds.map((stepId) => ` ${chalk.red.bold("✗")} ${chalk.bold(`Step ${stepId} failed.`)} ${chalk.dim(`Please run braeburn log --${stepId} to see what happened.`)}`);
17
+ export function buildFailedStepLogHintLines(failedStepLogHints) {
18
+ return failedStepLogHints.map(({ stepId, logStepId }) => ` ${chalk.red.bold("✗")} ${chalk.bold(`Step ${stepId} failed.`)} ${chalk.dim(`Please run braeburn log ${logStepId} to see what happened.`)}`);
12
19
  }
@@ -1,5 +1,5 @@
1
1
  import { createLogWriterForStep } from "../logger.js";
2
- import { runShellCommand, ShellCommandCanceledError } from "../runner.js";
2
+ 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";
@@ -29,6 +29,24 @@ function reportState(state, onStateChanged) {
29
29
  function wasStepCanceledByUser(error) {
30
30
  return error instanceof ShellCommandCanceledError;
31
31
  }
32
+ function createFailureOutputLines(currentOutputLines, error) {
33
+ if (currentOutputLines.length > 0) {
34
+ return [...currentOutputLines];
35
+ }
36
+ const errorMessage = error instanceof Error && error.message.length > 0
37
+ ? error.message
38
+ : "Command failed.";
39
+ return [{ text: errorMessage, source: "stderr" }];
40
+ }
41
+ function createFailedStepRecord(options) {
42
+ const errorMessage = options.error instanceof Error ? options.error.message : String(options.error);
43
+ return {
44
+ phase: "failed",
45
+ summaryNote: wasStepCanceledByUser(options.error) ? "canceled by user" : errorMessage,
46
+ logStepId: options.logStepId,
47
+ failureOutputLines: [...options.failureOutputLines],
48
+ };
49
+ }
32
50
  export async function runUpdateEngine(options) {
33
51
  const dependencies = resolveDependencies(options.dependencies);
34
52
  const state = createInitialUpdateState(toDisplaySteps(options.steps), options.version, options.logoVisibility);
@@ -82,12 +100,15 @@ export async function runUpdateEngine(options) {
82
100
  if (wasStepCanceledByUser(error)) {
83
101
  promptMode = "interactive";
84
102
  }
103
+ const failureOutputLines = createFailureOutputLines(state.currentOutputLines, error);
85
104
  state.currentPhase = "failed";
105
+ state.currentOutputLines = failureOutputLines;
86
106
  reportState(state, options.onStateChanged);
87
- state.completedStepRecords.push({
88
- phase: "failed",
89
- summaryNote: wasStepCanceledByUser(error) ? "canceled by user" : "install failed",
90
- });
107
+ state.completedStepRecords.push(createFailedStepRecord({
108
+ error,
109
+ failureOutputLines,
110
+ logStepId: `${step.id}-install`,
111
+ }));
91
112
  continue;
92
113
  }
93
114
  }
@@ -121,14 +142,15 @@ export async function runUpdateEngine(options) {
121
142
  if (wasStepCanceledByUser(error)) {
122
143
  promptMode = "interactive";
123
144
  }
124
- const errorMessage = error instanceof Error ? error.message : String(error);
145
+ const failureOutputLines = createFailureOutputLines(state.currentOutputLines, error);
125
146
  state.currentPhase = "failed";
126
- state.currentOutputLines = [];
147
+ state.currentOutputLines = failureOutputLines;
127
148
  reportState(state, options.onStateChanged);
128
- state.completedStepRecords.push({
129
- phase: "failed",
130
- summaryNote: wasStepCanceledByUser(error) ? "canceled by user" : errorMessage,
131
- });
149
+ state.completedStepRecords.push(createFailedStepRecord({
150
+ error,
151
+ failureOutputLines,
152
+ logStepId: step.id,
153
+ }));
132
154
  }
133
155
  }
134
156
  state.runCompletion = "finished";
@@ -10,6 +10,8 @@ export type StepPhase = "pending" | "checking-availability" | "prompting-to-inst
10
10
  export type CompletedStepRecord = {
11
11
  phase: StepPhase;
12
12
  summaryNote?: string;
13
+ logStepId?: string;
14
+ failureOutputLines?: CommandOutputLine[];
13
15
  };
14
16
  export type CurrentPrompt = {
15
17
  question: string;
@@ -36,3 +38,4 @@ export type UpdateState = {
36
38
  export type AppState = UpdateState;
37
39
  export declare function createInitialUpdateState(steps: DisplayStep[], version: string, logoVisibility: LogoVisibility): UpdateState;
38
40
  export declare const createInitialAppState: typeof createInitialUpdateState;
41
+ export declare function countFailedSteps(completedStepRecords: CompletedStepRecord[]): number;
@@ -13,3 +13,6 @@ export function createInitialUpdateState(steps, version, logoVisibility) {
13
13
  };
14
14
  }
15
15
  export const createInitialAppState = createInitialUpdateState;
16
+ export function countFailedSteps(completedStepRecords) {
17
+ return completedStepRecords.filter((completedStepRecord) => completedStepRecord.phase === "failed").length;
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braeburn",
3
- "version": "1.6.0",
3
+ "version": "2.0.0",
4
4
  "description": "macOS system updater CLI",
5
5
  "type": "module",
6
6
  "bin": {