braeburn 1.3.1 → 1.4.1

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/README.md +3 -1
  2. package/dist/commands/config.d.ts +7 -0
  3. package/dist/commands/config.js +187 -24
  4. package/dist/commands/setup.d.ts +9 -1
  5. package/dist/commands/setup.js +66 -55
  6. package/dist/commands/update.d.ts +2 -2
  7. package/dist/commands/update.js +22 -101
  8. package/dist/index.js +22 -17
  9. package/dist/runner.js +2 -2
  10. package/dist/steps/catalog.d.ts +2 -0
  11. package/dist/steps/catalog.js +13 -0
  12. package/dist/steps/cleanup.d.ts +1 -1
  13. package/dist/steps/cleanup.js +1 -1
  14. package/dist/steps/dotnet.d.ts +1 -1
  15. package/dist/steps/dotnet.js +1 -1
  16. package/dist/steps/homebrew.d.ts +1 -1
  17. package/dist/steps/homebrew.js +1 -1
  18. package/dist/steps/index.d.ts +2 -25
  19. package/dist/steps/index.js +1 -23
  20. package/dist/steps/macos.d.ts +1 -1
  21. package/dist/steps/mas.d.ts +1 -1
  22. package/dist/steps/mas.js +1 -1
  23. package/dist/steps/npm.d.ts +1 -1
  24. package/dist/steps/npm.js +1 -1
  25. package/dist/steps/nvm.d.ts +1 -1
  26. package/dist/steps/nvm.js +1 -1
  27. package/dist/steps/ohmyzsh.d.ts +1 -1
  28. package/dist/steps/ohmyzsh.js +1 -1
  29. package/dist/steps/pip.d.ts +1 -1
  30. package/dist/steps/pip.js +1 -1
  31. package/dist/steps/pyenv.d.ts +1 -1
  32. package/dist/steps/pyenv.js +1 -1
  33. package/dist/steps/runtime.d.ts +7 -0
  34. package/dist/steps/runtime.js +23 -0
  35. package/dist/steps/types.d.ts +21 -0
  36. package/dist/steps/types.js +1 -0
  37. package/dist/ui/currentStep.d.ts +2 -3
  38. package/dist/ui/header.d.ts +3 -4
  39. package/dist/ui/screen.js +1 -5
  40. package/dist/ui/state.d.ts +1 -30
  41. package/dist/ui/state.js +1 -14
  42. package/dist/ui/terminal.d.ts +1 -0
  43. package/dist/ui/terminal.js +14 -3
  44. package/dist/ui/versionReport.d.ts +0 -1
  45. package/dist/ui/versionReport.js +0 -16
  46. package/dist/update/displayStep.d.ts +4 -0
  47. package/dist/update/displayStep.js +11 -0
  48. package/dist/update/engine.d.ts +23 -0
  49. package/dist/update/engine.js +126 -0
  50. package/dist/update/state.d.ts +38 -0
  51. package/dist/update/state.js +15 -0
  52. package/dist/update/versionCollector.d.ts +2 -0
  53. package/dist/update/versionCollector.js +16 -0
  54. package/package.json +1 -1
package/README.md CHANGED
@@ -24,7 +24,9 @@ braeburn homebrew npm # run specific steps only
24
24
  |---|---|
25
25
  | `braeburn [steps...] [-y]` | Run update steps (default) |
26
26
  | `braeburn log [step]` | View the latest output log for a step |
27
- | `braeburn config` | View current configuration |
27
+ | `braeburn config` | Show config subcommand help |
28
+ | `braeburn config list` | Print current configuration |
29
+ | `braeburn config update` | Open interactive configuration editor |
28
30
  | `braeburn config update --no-<step>` | Disable a step |
29
31
  | `braeburn config update --<step>` | Re-enable a step |
30
32
 
@@ -2,6 +2,7 @@ import { type BraeburnConfig } from "../config.js";
2
2
  import type { Step } from "../steps/index.js";
3
3
  type RunConfigCommandOptions = {
4
4
  allSteps: Step[];
5
+ outputMode: "interactive" | "non-interactive";
5
6
  };
6
7
  type DesiredState = "enable" | "disable";
7
8
  type RunConfigUpdateCommandOptions = {
@@ -20,4 +21,10 @@ type ConfigUpdateResult = {
20
21
  };
21
22
  export declare function applyConfigUpdates(config: BraeburnConfig, settingUpdates: Record<string, DesiredState>): ConfigUpdateResult;
22
23
  export declare function runConfigUpdateCommand(options: RunConfigUpdateCommandOptions): Promise<void>;
24
+ type BuildConfigTableOutputOptions = {
25
+ config: BraeburnConfig;
26
+ configPath: string;
27
+ allSteps: Step[];
28
+ };
29
+ export declare function buildConfigTableOutput(options: BuildConfigTableOutputOptions): string;
23
30
  export {};
@@ -1,33 +1,22 @@
1
+ import readline from "node:readline";
1
2
  import chalk from "chalk";
2
3
  import { readConfig, writeConfig, resolveConfigPath, isSettingEnabled, isStepEnabled, isLogoEnabled, applySettingToConfig, PROTECTED_STEP_IDS, DEFAULT_OFF_STEP_IDS, } from "../config.js";
4
+ import { createScreenRenderer } from "../ui/screen.js";
5
+ import { hideCursorDuringExecution } from "../ui/terminal.js";
3
6
  export async function runConfigCommand(options) {
4
- const { allSteps } = options;
7
+ const { allSteps, outputMode } = options;
5
8
  const config = await readConfig();
6
9
  const configPath = await resolveConfigPath();
7
- const STEP_COL = 12;
8
- const DIVIDER = "─".repeat(STEP_COL + 16);
9
- process.stdout.write(`Config: ${chalk.dim(configPath)}\n\n`);
10
- process.stdout.write(`${"Step".padEnd(STEP_COL)}Status\n`);
11
- process.stdout.write(`${DIVIDER}\n`);
12
- const logoEnabled = isLogoEnabled(config);
13
- process.stdout.write(`${"logo".padEnd(STEP_COL)}${logoEnabled ? chalk.green("enabled") : chalk.red("disabled")}\n`);
14
- process.stdout.write(`${DIVIDER}\n`);
15
- for (const step of allSteps) {
16
- const isProtected = PROTECTED_STEP_IDS.has(step.id);
17
- const enabled = isStepEnabled(config, step.id);
18
- let statusText;
19
- if (isProtected) {
20
- statusText = chalk.dim("always enabled");
21
- }
22
- else if (enabled) {
23
- statusText = chalk.green("enabled");
24
- }
25
- else {
26
- statusText = chalk.red("disabled");
27
- }
28
- process.stdout.write(`${step.id.padEnd(STEP_COL)}${statusText}\n`);
10
+ if (outputMode === "non-interactive") {
11
+ process.stdout.write(buildConfigTableOutput({ config, configPath, allSteps }));
12
+ return;
13
+ }
14
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
15
+ process.stderr.write("Interactive mode requires a TTY. Showing non-interactive output.\n\n");
16
+ process.stdout.write(buildConfigTableOutput({ config, configPath, allSteps }));
17
+ return;
29
18
  }
30
- process.stdout.write(`\n`);
19
+ await runInteractiveConfigView({ config, configPath, allSteps });
31
20
  }
32
21
  export function applyConfigUpdates(config, settingUpdates) {
33
22
  let updatedConfig = config;
@@ -74,6 +63,180 @@ export async function runConfigUpdateCommand(options) {
74
63
  const configPath = await resolveConfigPath();
75
64
  process.stdout.write(`\nConfig saved to ${chalk.dim(configPath)}\n`);
76
65
  }
66
+ export function buildConfigTableOutput(options) {
67
+ const { config, configPath, allSteps } = options;
68
+ const lines = [];
69
+ const stepColumnWidth = 12;
70
+ const divider = "─".repeat(stepColumnWidth + 16);
71
+ lines.push(`Config: ${chalk.dim(configPath)}`);
72
+ lines.push("");
73
+ lines.push(`${"Step".padEnd(stepColumnWidth)}Status`);
74
+ lines.push(divider);
75
+ const logoEnabled = isLogoEnabled(config);
76
+ lines.push(`${"logo".padEnd(stepColumnWidth)}${logoEnabled ? chalk.green("enabled") : chalk.red("disabled")}`);
77
+ lines.push(divider);
78
+ for (const step of allSteps) {
79
+ const isProtected = PROTECTED_STEP_IDS.has(step.id);
80
+ const enabled = isStepEnabled(config, step.id);
81
+ let statusText;
82
+ if (isProtected) {
83
+ statusText = chalk.dim("always enabled");
84
+ }
85
+ else if (enabled) {
86
+ statusText = chalk.green("enabled");
87
+ }
88
+ else {
89
+ statusText = chalk.red("disabled");
90
+ }
91
+ lines.push(`${step.id.padEnd(stepColumnWidth)}${statusText}`);
92
+ }
93
+ lines.push("");
94
+ return lines.join("\n") + "\n";
95
+ }
96
+ function buildConfigViewItems(config, allSteps) {
97
+ const viewItems = [
98
+ {
99
+ id: "logo",
100
+ label: "logo",
101
+ description: "Show the braeburn logo in command output",
102
+ protection: "configurable",
103
+ selection: isLogoEnabled(config) ? "enabled" : "disabled",
104
+ },
105
+ ];
106
+ for (const step of allSteps) {
107
+ viewItems.push({
108
+ id: step.id,
109
+ label: step.id,
110
+ description: step.description,
111
+ protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
112
+ selection: isStepEnabled(config, step.id) ? "enabled" : "disabled",
113
+ });
114
+ }
115
+ return viewItems;
116
+ }
117
+ function buildInteractiveConfigScreen(options) {
118
+ const { configPath, items, cursorIndex } = options;
119
+ const lines = [];
120
+ lines.push(`Config: ${chalk.dim(configPath)}`);
121
+ lines.push("");
122
+ lines.push(` ${chalk.dim("↑↓ navigate Space toggle Return save q quit")}`);
123
+ lines.push("");
124
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
125
+ const item = items[itemIndex];
126
+ const isCursor = itemIndex === cursorIndex;
127
+ const cursor = isCursor ? chalk.cyan("›") : " ";
128
+ const marker = item.selection === "enabled" ? chalk.green("●") : chalk.dim("○");
129
+ const label = isCursor ? chalk.bold.white(item.label.padEnd(12)) : chalk.white(item.label.padEnd(12));
130
+ let status;
131
+ if (item.protection === "protected") {
132
+ status = chalk.dim("always enabled");
133
+ }
134
+ else if (item.selection === "enabled") {
135
+ status = chalk.green("enabled");
136
+ }
137
+ else {
138
+ status = chalk.red("disabled");
139
+ }
140
+ lines.push(` ${cursor} ${marker} ${label} ${status}`);
141
+ if (isCursor) {
142
+ lines.push(` ${chalk.dim(item.description)}`);
143
+ }
144
+ }
145
+ const enabledCount = items.filter((item) => item.selection === "enabled").length;
146
+ lines.push("");
147
+ lines.push(` ${chalk.dim(`${enabledCount} of ${items.length} settings enabled`)}`);
148
+ lines.push("");
149
+ return lines.join("\n") + "\n";
150
+ }
151
+ async function runInteractiveConfigView(options) {
152
+ const { config, configPath, allSteps } = options;
153
+ const render = createScreenRenderer();
154
+ const restoreCursor = hideCursorDuringExecution({ screenBuffer: "alternate" });
155
+ const items = buildConfigViewItems(config, allSteps);
156
+ let cursorIndex = 0;
157
+ let completionOutput = "";
158
+ try {
159
+ render(buildInteractiveConfigScreen({ configPath, items, cursorIndex }));
160
+ const interactionResult = await new Promise((resolve) => {
161
+ readline.emitKeypressEvents(process.stdin);
162
+ if (process.stdin.isTTY) {
163
+ process.stdin.setRawMode(true);
164
+ }
165
+ const completeInteraction = (result) => {
166
+ process.stdin.removeListener("keypress", onKeypress);
167
+ if (process.stdin.isTTY) {
168
+ process.stdin.setRawMode(false);
169
+ }
170
+ process.stdin.pause();
171
+ resolve(result);
172
+ };
173
+ const onKeypress = (_char, key) => {
174
+ if (key.ctrl && key.name === "c") {
175
+ process.exit(130);
176
+ }
177
+ if (key.name === "up" || key.name === "k") {
178
+ cursorIndex = Math.max(0, cursorIndex - 1);
179
+ render(buildInteractiveConfigScreen({ configPath, items, cursorIndex }));
180
+ return;
181
+ }
182
+ if (key.name === "down" || key.name === "j") {
183
+ cursorIndex = Math.min(items.length - 1, cursorIndex + 1);
184
+ render(buildInteractiveConfigScreen({ configPath, items, cursorIndex }));
185
+ return;
186
+ }
187
+ if (key.name === "space") {
188
+ const selectedItem = items[cursorIndex];
189
+ if (selectedItem.protection === "configurable") {
190
+ selectedItem.selection = selectedItem.selection === "enabled" ? "disabled" : "enabled";
191
+ render(buildInteractiveConfigScreen({ configPath, items, cursorIndex }));
192
+ }
193
+ return;
194
+ }
195
+ if (key.name === "return") {
196
+ completeInteraction("save");
197
+ return;
198
+ }
199
+ if (key.name === "q" || key.name === "escape") {
200
+ completeInteraction("cancel");
201
+ }
202
+ };
203
+ process.stdin.on("keypress", onKeypress);
204
+ process.stdin.resume();
205
+ });
206
+ if (interactionResult === "cancel") {
207
+ completionOutput = "No changes saved.\n";
208
+ }
209
+ else {
210
+ const settingUpdates = {};
211
+ for (const item of items) {
212
+ if (item.protection === "protected") {
213
+ continue;
214
+ }
215
+ settingUpdates[item.id] = item.selection === "enabled" ? "enable" : "disable";
216
+ }
217
+ const { updatedConfig, changes } = applyConfigUpdates(config, settingUpdates);
218
+ if (changes.length === 0) {
219
+ completionOutput = "No changes — already set as requested.\n";
220
+ }
221
+ else {
222
+ await writeCleanConfig(updatedConfig);
223
+ const outputLines = [];
224
+ for (const { label, from, to } of changes) {
225
+ const fromLabel = from === "enable" ? chalk.green("enabled") : chalk.red("disabled");
226
+ const toLabel = to === "enable" ? chalk.green("enabled") : chalk.red("disabled");
227
+ outputLines.push(` ${label.padEnd(12)} ${fromLabel} → ${toLabel}`);
228
+ }
229
+ outputLines.push("");
230
+ outputLines.push(`Config saved to ${chalk.dim(configPath)}`);
231
+ completionOutput = `${outputLines.join("\n")}\n`;
232
+ }
233
+ }
234
+ }
235
+ finally {
236
+ restoreCursor();
237
+ }
238
+ process.stdout.write(completionOutput);
239
+ }
77
240
  async function writeCleanConfig(config) {
78
241
  const cleaned = { steps: {} };
79
242
  for (const [stepId, value] of Object.entries(config.steps)) {
@@ -1,9 +1,17 @@
1
1
  import type { Step } from "../steps/index.js";
2
+ import type { StepStage } from "../update/state.js";
2
3
  export type SelectionState = "selected" | "deselected";
3
4
  export type ProtectionStatus = "protected" | "configurable";
4
5
  export type AvailabilityStatus = "available" | "unavailable";
6
+ export type SetupStepView = {
7
+ id: string;
8
+ name: string;
9
+ description: string;
10
+ stage: StepStage;
11
+ brewPackageToInstall?: string;
12
+ };
5
13
  export type SelectableStep = {
6
- step: Step;
14
+ step: SetupStepView;
7
15
  selection: SelectionState;
8
16
  protection: ProtectionStatus;
9
17
  availability: AvailabilityStatus;
@@ -73,64 +73,75 @@ export function buildSetupScreen(items, cursorIndex) {
73
73
  }
74
74
  export async function runSetupCommand(allSteps) {
75
75
  const render = createScreenRenderer();
76
- hideCursorDuringExecution();
77
- render(buildLoadingScreen());
78
- const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
79
- const items = allSteps.map((step, stepIndex) => ({
80
- step,
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
- }));
85
- let cursorIndex = 0;
86
- render(buildSetupScreen(items, cursorIndex));
87
- await new Promise((resolve) => {
88
- readline.emitKeypressEvents(process.stdin);
89
- if (process.stdin.isTTY)
90
- process.stdin.setRawMode(true);
91
- const handleKeypress = (_char, key) => {
92
- if (key?.ctrl && key?.name === "c") {
93
- process.exit(130);
94
- }
95
- if (key?.name === "up" || key?.name === "k") {
96
- cursorIndex = Math.max(0, cursorIndex - 1);
97
- render(buildSetupScreen(items, cursorIndex));
98
- }
99
- else if (key?.name === "down" || key?.name === "j") {
100
- cursorIndex = Math.min(items.length - 1, cursorIndex + 1);
101
- render(buildSetupScreen(items, cursorIndex));
102
- }
103
- else if (key?.name === "space") {
104
- const item = items[cursorIndex];
105
- if (item.protection === "configurable") {
106
- item.selection = item.selection === "selected" ? "deselected" : "selected";
76
+ const restoreCursor = hideCursorDuringExecution({ screenBuffer: "alternate" });
77
+ try {
78
+ render(buildLoadingScreen());
79
+ const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
80
+ const items = allSteps.map((step, stepIndex) => ({
81
+ step: {
82
+ id: step.id,
83
+ name: step.name,
84
+ description: step.description,
85
+ stage: step.stage,
86
+ brewPackageToInstall: step.brewPackageToInstall,
87
+ },
88
+ selection: PROTECTED_STEP_IDS.has(step.id) || step.stage === "tools" ? "selected" : "deselected",
89
+ protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
90
+ availability: availabilityResults[stepIndex] ? "available" : "unavailable",
91
+ }));
92
+ let cursorIndex = 0;
93
+ render(buildSetupScreen(items, cursorIndex));
94
+ await new Promise((resolve) => {
95
+ readline.emitKeypressEvents(process.stdin);
96
+ if (process.stdin.isTTY)
97
+ process.stdin.setRawMode(true);
98
+ const handleKeypress = (_char, key) => {
99
+ if (key?.ctrl && key?.name === "c") {
100
+ process.exit(130);
101
+ }
102
+ if (key?.name === "up" || key?.name === "k") {
103
+ cursorIndex = Math.max(0, cursorIndex - 1);
107
104
  render(buildSetupScreen(items, cursorIndex));
108
105
  }
106
+ else if (key?.name === "down" || key?.name === "j") {
107
+ cursorIndex = Math.min(items.length - 1, cursorIndex + 1);
108
+ render(buildSetupScreen(items, cursorIndex));
109
+ }
110
+ else if (key?.name === "space") {
111
+ const item = items[cursorIndex];
112
+ if (item.protection === "configurable") {
113
+ item.selection = item.selection === "selected" ? "deselected" : "selected";
114
+ render(buildSetupScreen(items, cursorIndex));
115
+ }
116
+ }
117
+ else if (key?.name === "return") {
118
+ process.stdin.removeListener("keypress", handleKeypress);
119
+ if (process.stdin.isTTY)
120
+ process.stdin.setRawMode(false);
121
+ process.stdin.pause();
122
+ resolve();
123
+ }
124
+ };
125
+ process.stdin.on("keypress", handleKeypress);
126
+ process.stdin.resume();
127
+ });
128
+ const stepsConfig = {};
129
+ for (const item of items) {
130
+ if (item.protection === "configurable" && item.selection === "deselected") {
131
+ stepsConfig[item.step.id] = false;
109
132
  }
110
- else if (key?.name === "return") {
111
- process.stdin.removeListener("keypress", handleKeypress);
112
- if (process.stdin.isTTY)
113
- process.stdin.setRawMode(false);
114
- process.stdin.pause();
115
- resolve();
116
- }
117
- };
118
- process.stdin.on("keypress", handleKeypress);
119
- process.stdin.resume();
120
- });
121
- const stepsConfig = {};
122
- for (const item of items) {
123
- if (item.protection === "configurable" && item.selection === "deselected") {
124
- stepsConfig[item.step.id] = false;
125
133
  }
134
+ await writeConfig({ steps: stepsConfig });
135
+ const confirmationLines = [
136
+ chalk.yellow(LOGO_ART),
137
+ "",
138
+ ` ${chalk.green("\u2713")} Setup complete! Starting your first update\u2026`,
139
+ "",
140
+ ];
141
+ render(confirmationLines.join("\n") + "\n");
142
+ await new Promise((resolve) => setTimeout(resolve, 800));
143
+ }
144
+ finally {
145
+ restoreCursor();
126
146
  }
127
- await writeConfig({ steps: stepsConfig });
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));
136
147
  }
@@ -1,6 +1,6 @@
1
+ import { type PromptMode } from "../update/engine.js";
2
+ import type { LogoVisibility } from "../update/state.js";
1
3
  import type { Step } from "../steps/index.js";
2
- type PromptMode = "interactive" | "auto-accept";
3
- type LogoVisibility = "visible" | "hidden";
4
4
  type RunUpdateCommandOptions = {
5
5
  steps: Step[];
6
6
  promptMode: PromptMode;
@@ -1,109 +1,30 @@
1
- import { runShellCommand } from "../runner.js";
2
- import { createLogWriterForStep } from "../logger.js";
3
- import { collectVersions } from "../ui/versionReport.js";
1
+ import { collectVersions } from "../update/versionCollector.js";
4
2
  import { captureYesNo } from "../ui/prompt.js";
5
- import { createInitialAppState } from "../ui/state.js";
6
3
  import { buildScreen, createScreenRenderer } from "../ui/screen.js";
7
4
  import { hideCursorDuringExecution } from "../ui/terminal.js";
8
- import { createDefaultStepRunContext } from "../steps/index.js";
5
+ import { runUpdateEngine } from "../update/engine.js";
9
6
  export async function runUpdateCommand(options) {
10
- const { steps, version } = options;
11
- let autoAccept = options.promptMode === "auto-accept";
12
- const state = createInitialAppState(steps, version, options.logoVisibility);
13
7
  const renderScreen = createScreenRenderer();
14
- hideCursorDuringExecution();
15
- renderScreen(buildScreen(state));
16
- for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
17
- const step = steps[stepIndex];
18
- state.currentStepIndex = stepIndex;
19
- state.currentPhase = "checking-availability";
20
- state.currentOutputLines = [];
21
- state.currentPrompt = undefined;
22
- renderScreen(buildScreen(state));
23
- const isAvailable = await step.checkIsAvailable();
24
- if (!isAvailable && !step.brewPackageToInstall) {
25
- state.currentPhase = "not-available";
26
- renderScreen(buildScreen(state));
27
- state.completedStepRecords.push({ phase: "not-available", summaryNote: "not installed" });
28
- continue;
29
- }
30
- if (!isAvailable && step.brewPackageToInstall) {
31
- state.currentPhase = "prompting-to-install";
32
- state.currentPrompt = {
33
- question: `Install ${step.name} via Homebrew? (brew install ${step.brewPackageToInstall})`,
34
- };
35
- renderScreen(buildScreen(state));
36
- const installAnswer = autoAccept ? "yes" : await captureYesNo();
37
- if (installAnswer === "force")
38
- autoAccept = true;
39
- const shouldInstall = installAnswer !== "no";
40
- state.currentPrompt = undefined;
41
- if (!shouldInstall) {
42
- state.currentPhase = "skipped";
8
+ const restoreCursor = hideCursorDuringExecution({ screenBuffer: "alternate" });
9
+ let finalScreen = "";
10
+ try {
11
+ const finalState = await runUpdateEngine({
12
+ steps: options.steps,
13
+ promptMode: options.promptMode,
14
+ version: options.version,
15
+ logoVisibility: options.logoVisibility,
16
+ askForConfirmation: captureYesNo,
17
+ collectVersions,
18
+ onStateChanged: (state) => {
43
19
  renderScreen(buildScreen(state));
44
- state.completedStepRecords.push({ phase: "skipped" });
45
- continue;
46
- }
47
- state.currentPhase = "installing";
48
- renderScreen(buildScreen(state));
49
- try {
50
- const installLogWriter = await createLogWriterForStep(`${step.id}-install`);
51
- await runShellCommand({
52
- shellCommand: `brew install ${step.brewPackageToInstall}`,
53
- onOutputLine: (line) => {
54
- state.currentOutputLines.push(line);
55
- renderScreen(buildScreen(state));
56
- },
57
- logWriter: installLogWriter,
58
- });
59
- }
60
- catch {
61
- state.currentPhase = "failed";
62
- renderScreen(buildScreen(state));
63
- state.completedStepRecords.push({ phase: "failed", summaryNote: "install failed" });
64
- continue;
65
- }
66
- }
67
- state.currentPhase = "prompting-to-run";
68
- state.currentPrompt = { question: `Run ${step.name} update?`, warning: step.warning };
69
- renderScreen(buildScreen(state));
70
- const runAnswer = autoAccept ? "yes" : await captureYesNo();
71
- if (runAnswer === "force")
72
- autoAccept = true;
73
- const shouldRun = runAnswer !== "no";
74
- state.currentPrompt = undefined;
75
- if (!shouldRun) {
76
- state.currentPhase = "skipped";
77
- renderScreen(buildScreen(state));
78
- state.completedStepRecords.push({ phase: "skipped" });
79
- continue;
80
- }
81
- state.currentPhase = "running";
82
- state.currentOutputLines = [];
83
- renderScreen(buildScreen(state));
84
- const stepLogWriter = await createLogWriterForStep(step.id);
85
- try {
86
- await step.run(createDefaultStepRunContext((line) => {
87
- state.currentOutputLines.push(line);
88
- renderScreen(buildScreen(state));
89
- }, stepLogWriter));
90
- state.currentPhase = "complete";
91
- state.currentOutputLines = [];
92
- renderScreen(buildScreen(state));
93
- state.completedStepRecords.push({ phase: "complete", summaryNote: "updated" });
94
- }
95
- catch (error) {
96
- const errorMessage = error instanceof Error ? error.message : String(error);
97
- state.currentPhase = "failed";
98
- state.currentOutputLines = [];
99
- renderScreen(buildScreen(state));
100
- state.completedStepRecords.push({ phase: "failed", summaryNote: errorMessage });
101
- }
20
+ },
21
+ });
22
+ finalScreen = buildScreen(finalState);
23
+ }
24
+ finally {
25
+ restoreCursor();
26
+ }
27
+ if (finalScreen) {
28
+ process.stdout.write(finalScreen);
102
29
  }
103
- state.runCompletion = "finished";
104
- state.currentOutputLines = [];
105
- state.currentPrompt = undefined;
106
- renderScreen(buildScreen(state));
107
- state.versionReport = await collectVersions();
108
- renderScreen(buildScreen(state));
109
30
  }
package/dist/index.js CHANGED
@@ -3,24 +3,12 @@ import { Command } from "commander";
3
3
  import { createRequire } from "node:module";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { dirname, join } from "node:path";
6
- import { homebrewStep, masStep, ohmyzshStep, npmStep, pipStep, pyenvStep, nvmStep, dotnetStep, macosStep, cleanupStep, } from "./steps/index.js";
6
+ import { ALL_STEPS } from "./steps/catalog.js";
7
7
  import { 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";
11
11
  import { readConfig, isStepEnabled, isLogoEnabled, PROTECTED_STEP_IDS, configFileExists } from "./config.js";
12
- const ALL_STEPS = [
13
- pyenvStep,
14
- nvmStep,
15
- homebrewStep,
16
- masStep,
17
- ohmyzshStep,
18
- npmStep,
19
- pipStep,
20
- dotnetStep,
21
- macosStep,
22
- cleanupStep,
23
- ];
24
12
  const STEP_IDS_BY_NAME = new Map(ALL_STEPS.map((step) => [step.id, step]));
25
13
  const requireFromThis = createRequire(import.meta.url);
26
14
  const packageJson = requireFromThis(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"));
@@ -115,15 +103,25 @@ Examples:
115
103
  const configCommand = program
116
104
  .command("config")
117
105
  .description("View or edit braeburn configuration")
106
+ .action(() => {
107
+ configCommand.outputHelp();
108
+ });
109
+ configCommand
110
+ .command("list")
111
+ .description("Print current configuration")
118
112
  .action(async () => {
119
- await runConfigCommand({ allSteps: ALL_STEPS });
113
+ await runConfigCommand({
114
+ allSteps: ALL_STEPS,
115
+ outputMode: "non-interactive",
116
+ });
120
117
  });
121
118
  const configurableSteps = ALL_STEPS.filter((step) => !PROTECTED_STEP_IDS.has(step.id));
122
119
  const configUpdateCommand = configCommand
123
120
  .command("update")
124
- .description("Enable or disable individual update steps")
121
+ .description("Edit configuration (interactive by default, flags for direct updates)")
125
122
  .addHelpText("after", `
126
123
  Examples:
124
+ braeburn config update Open interactive config editor
127
125
  braeburn config update --no-logo Hide the logo
128
126
  braeburn config update --no-ohmyzsh Disable Oh My Zsh updates
129
127
  braeburn config update --no-pip --no-nvm Disable pip and nvm updates
@@ -135,7 +133,7 @@ for (const step of configurableSteps) {
135
133
  configUpdateCommand.option(`--no-${step.id}`, `Disable ${step.name} updates`);
136
134
  configUpdateCommand.option(`--${step.id}`, `Enable ${step.name} updates`);
137
135
  }
138
- configUpdateCommand.action(function () {
136
+ configUpdateCommand.action(async function () {
139
137
  // Commander defaults --no-* to true, so we use getOptionValueSource to detect explicit CLI flags.
140
138
  const settingUpdates = {};
141
139
  for (const step of configurableSteps) {
@@ -148,7 +146,14 @@ configUpdateCommand.action(function () {
148
146
  if (logoSource === "cli") {
149
147
  settingUpdates["logo"] = configUpdateCommand.opts().logo ? "enable" : "disable";
150
148
  }
151
- runConfigUpdateCommand({ settingUpdates, allSteps: ALL_STEPS });
149
+ if (Object.keys(settingUpdates).length === 0) {
150
+ await runConfigCommand({
151
+ allSteps: ALL_STEPS,
152
+ outputMode: "interactive",
153
+ });
154
+ return;
155
+ }
156
+ await runConfigUpdateCommand({ settingUpdates, allSteps: ALL_STEPS });
152
157
  });
153
158
  function resolveStepsByIds(stepIds) {
154
159
  const resolvedSteps = [];
package/dist/runner.js CHANGED
@@ -5,14 +5,14 @@ export async function runShellCommand(options) {
5
5
  reject: true,
6
6
  });
7
7
  subprocess.stdout?.on("data", (chunk) => {
8
- const lines = String(chunk).split("\n").filter(Boolean);
8
+ const lines = String(chunk).split(/\r?\n|\r/).filter(Boolean);
9
9
  for (const line of lines) {
10
10
  options.onOutputLine({ text: line, source: "stdout" });
11
11
  options.logWriter(line);
12
12
  }
13
13
  });
14
14
  subprocess.stderr?.on("data", (chunk) => {
15
- const lines = String(chunk).split("\n").filter(Boolean);
15
+ const lines = String(chunk).split(/\r?\n|\r/).filter(Boolean);
16
16
  for (const line of lines) {
17
17
  options.onOutputLine({ text: line, source: "stderr" });
18
18
  options.logWriter(line);
@@ -0,0 +1,2 @@
1
+ import { type Step } from "./index.js";
2
+ export declare const ALL_STEPS: Step[];