braeburn 1.4.2 → 1.5.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
@@ -32,20 +32,20 @@ braeburn homebrew npm # run specific steps only
32
32
 
33
33
  ## Steps
34
34
 
35
- 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.
35
+ Steps are grouped by system capability. For new setups, braeburn uses a conservative default profile: only package-manager-driven CLI/tooling updates are enabled by default.
36
36
 
37
- | Step | Stage | Default | Requires |
37
+ | Step | Category | Default (new setup) | Requires |
38
38
  |---|---|---|---|
39
- | `pyenv` | runtime | off | `pyenv` or Homebrew |
40
- | `nvm` | runtime | off | `~/.nvm` |
41
- | `homebrew` | tools | on | `brew` (required) |
42
- | `mas` | tools | on | `mas` |
43
- | `ohmyzsh` | tools | on | `~/.oh-my-zsh` |
44
- | `npm` | tools | on | `npm` |
45
- | `pip` | tools | on | `pip3` |
46
- | `dotnet` | tools | on | `dotnet` |
47
- | `macos` | tools | on | |
48
- | `cleanup` | tools | on | `brew` |
39
+ | `pyenv` | Runtimes | off | `pyenv` or Homebrew |
40
+ | `nvm` | Runtimes | off | `~/.nvm` |
41
+ | `homebrew` | Apps & Packages | on | `brew` (required) |
42
+ | `mas` | Apps & Packages | off | `mas` |
43
+ | `macos` | Apps & Packages | off | |
44
+ | `npm` | CLI Tools | on | `npm` |
45
+ | `pip` | CLI Tools | on | `pip3` |
46
+ | `dotnet` | CLI Tools | on | `dotnet` |
47
+ | `ohmyzsh` | Shell | off | `~/.oh-my-zsh` |
48
+ | `cleanup` (`homebrew cleanup`) | Maintenance | off | `brew` |
49
49
 
50
50
  ## Requirements
51
51
 
@@ -1,5 +1,5 @@
1
1
  import { type BraeburnConfig } from "../config.js";
2
- import type { Step } from "../steps/index.js";
2
+ import { type Step } from "../steps/index.js";
3
3
  type RunConfigCommandOptions = {
4
4
  allSteps: Step[];
5
5
  outputMode: "interactive" | "non-interactive";
@@ -1,6 +1,7 @@
1
1
  import readline from "node:readline";
2
2
  import chalk from "chalk";
3
- import { readConfig, writeConfig, resolveConfigPath, isSettingEnabled, isStepEnabled, isLogoEnabled, applySettingToConfig, PROTECTED_STEP_IDS, DEFAULT_OFF_STEP_IDS, } from "../config.js";
3
+ import { readConfig, writeConfig, resolveConfigPath, isSettingEnabled, isStepEnabled, isLogoEnabled, applySettingToConfig, cleanConfigForWrite, PROTECTED_STEP_IDS, } from "../config.js";
4
+ import { buildCategorySectionsInOrder, getStepCategoryLabel, } from "../steps/index.js";
4
5
  import { createScreenRenderer } from "../ui/screen.js";
5
6
  import { hideCursorDuringExecution } from "../ui/terminal.js";
6
7
  export async function runConfigCommand(options) {
@@ -50,7 +51,7 @@ export async function runConfigUpdateCommand(options) {
50
51
  }
51
52
  const config = await readConfig();
52
53
  const { updatedConfig, changes } = applyConfigUpdates(config, settingUpdates);
53
- await writeCleanConfig(updatedConfig);
54
+ await writeConfig(cleanConfigForWrite(updatedConfig));
54
55
  if (changes.length === 0) {
55
56
  process.stdout.write("No changes — already set as requested.\n");
56
57
  return;
@@ -66,52 +67,61 @@ export async function runConfigUpdateCommand(options) {
66
67
  export function buildConfigTableOutput(options) {
67
68
  const { config, configPath, allSteps } = options;
68
69
  const lines = [];
69
- const stepColumnWidth = 12;
70
- const divider = "─".repeat(stepColumnWidth + 16);
70
+ const settingColumnWidth = 24;
71
71
  lines.push(`Config: ${chalk.dim(configPath)}`);
72
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");
73
+ lines.push(chalk.bold("System"));
74
+ lines.push(`${"Setting".padEnd(settingColumnWidth)}Status`);
75
+ lines.push("─".repeat(settingColumnWidth + 16));
76
+ for (const section of buildCategorySectionsInOrder(allSteps)) {
77
+ lines.push(chalk.dim(` ${getStepCategoryLabel(section.categoryId)}`));
78
+ for (const step of section.items) {
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.name.padEnd(settingColumnWidth)}${statusText}`);
90
92
  }
91
- lines.push(`${step.id.padEnd(stepColumnWidth)}${statusText}`);
93
+ lines.push("");
92
94
  }
95
+ lines.push(chalk.bold("Interface"));
96
+ lines.push(`${"Setting".padEnd(settingColumnWidth)}Status`);
97
+ lines.push("─".repeat(settingColumnWidth + 16));
98
+ const logoEnabled = isLogoEnabled(config);
99
+ lines.push(`${"logo".padEnd(settingColumnWidth)}${logoEnabled ? chalk.green("enabled") : chalk.red("disabled")}`);
93
100
  lines.push("");
94
101
  return lines.join("\n") + "\n";
95
102
  }
96
103
  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
- });
104
+ const viewItems = [];
105
+ for (const section of buildCategorySectionsInOrder(allSteps)) {
106
+ for (const step of section.items) {
107
+ viewItems.push({
108
+ id: step.id,
109
+ label: step.name,
110
+ description: step.description,
111
+ sectionLabel: `System / ${getStepCategoryLabel(step.categoryId)}`,
112
+ protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
113
+ selection: isStepEnabled(config, step.id) ? "enabled" : "disabled",
114
+ });
115
+ }
114
116
  }
117
+ viewItems.push({
118
+ id: "logo",
119
+ label: "logo",
120
+ description: "Show the braeburn logo in command output",
121
+ sectionLabel: "Interface",
122
+ protection: "configurable",
123
+ selection: isLogoEnabled(config) ? "enabled" : "disabled",
124
+ });
115
125
  return viewItems;
116
126
  }
117
127
  function buildInteractiveConfigScreen(options) {
@@ -124,9 +134,13 @@ function buildInteractiveConfigScreen(options) {
124
134
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
125
135
  const item = items[itemIndex];
126
136
  const isCursor = itemIndex === cursorIndex;
137
+ const previousItem = items[itemIndex - 1];
138
+ if (!previousItem || previousItem.sectionLabel !== item.sectionLabel) {
139
+ lines.push(` ${chalk.dim(`── ${item.sectionLabel} ──────────────────────────────────────────────`)}`);
140
+ }
127
141
  const cursor = isCursor ? chalk.cyan("›") : " ";
128
142
  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));
143
+ const label = isCursor ? chalk.bold.white(item.label.padEnd(24)) : chalk.white(item.label.padEnd(24));
130
144
  let status;
131
145
  if (item.protection === "protected") {
132
146
  status = chalk.dim("always enabled");
@@ -170,7 +184,7 @@ async function runInteractiveConfigView(options) {
170
184
  process.stdin.pause();
171
185
  resolve(result);
172
186
  };
173
- const onKeypress = (_char, key) => {
187
+ const onKeypress = (_typedCharacter, key) => {
174
188
  if (key.ctrl && key.name === "c") {
175
189
  process.exit(130);
176
190
  }
@@ -219,7 +233,7 @@ async function runInteractiveConfigView(options) {
219
233
  completionOutput = "No changes — already set as requested.\n";
220
234
  }
221
235
  else {
222
- await writeCleanConfig(updatedConfig);
236
+ await writeConfig(cleanConfigForWrite(updatedConfig));
223
237
  const outputLines = [];
224
238
  for (const { label, from, to } of changes) {
225
239
  const fromLabel = from === "enable" ? chalk.green("enabled") : chalk.red("disabled");
@@ -237,18 +251,3 @@ async function runInteractiveConfigView(options) {
237
251
  }
238
252
  process.stdout.write(completionOutput);
239
253
  }
240
- async function writeCleanConfig(config) {
241
- const cleaned = { steps: {} };
242
- for (const [stepId, value] of Object.entries(config.steps)) {
243
- if (value === false && !DEFAULT_OFF_STEP_IDS.has(stepId)) {
244
- cleaned.steps[stepId] = false;
245
- }
246
- else if (value === true && DEFAULT_OFF_STEP_IDS.has(stepId)) {
247
- cleaned.steps[stepId] = true;
248
- }
249
- }
250
- if (config.logo === false) {
251
- cleaned.logo = false;
252
- }
253
- await writeConfig(cleaned);
254
- }
@@ -1,5 +1,4 @@
1
- import type { Step } from "../steps/index.js";
2
- import type { StepStage } from "../update/state.js";
1
+ import { type Step, type StepCategoryId } from "../steps/index.js";
3
2
  export type SelectionState = "selected" | "deselected";
4
3
  export type ProtectionStatus = "protected" | "configurable";
5
4
  export type AvailabilityStatus = "available" | "unavailable";
@@ -7,7 +6,7 @@ export type SetupStepView = {
7
6
  id: string;
8
7
  name: string;
9
8
  description: string;
10
- stage: StepStage;
9
+ categoryId: StepCategoryId;
11
10
  brewPackageToInstall?: string;
12
11
  };
13
12
  export type SelectableStep = {
@@ -1,9 +1,10 @@
1
1
  import readline from "node:readline";
2
2
  import chalk from "chalk";
3
- import { writeConfig, PROTECTED_STEP_IDS } from "../config.js";
3
+ import { writeConfig, PROTECTED_STEP_IDS, CONSERVATIVE_DEFAULT_ON_STEP_IDS, cleanConfigForWrite, } from "../config.js";
4
4
  import { LOGO_ART } from "../logo.js";
5
5
  import { createScreenRenderer } from "../ui/screen.js";
6
6
  import { hideCursorDuringExecution } from "../ui/terminal.js";
7
+ import { getStepCategoryLabel, } from "../steps/index.js";
7
8
  export function buildLoadingScreen() {
8
9
  const lines = [
9
10
  chalk.yellow(LOGO_ART),
@@ -27,21 +28,13 @@ export function buildSetupScreen(items, cursorIndex) {
27
28
  ` ${chalk.dim("\u2191\u2193 navigate Space toggle Return confirm")}`,
28
29
  "",
29
30
  ];
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
31
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
34
32
  const item = items[itemIndex];
35
33
  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
- }
34
+ const previousItem = items[itemIndex - 1];
35
+ if (!previousItem || previousItem.step.categoryId !== item.step.categoryId) {
36
+ const categoryLabel = getStepCategoryLabel(item.step.categoryId);
37
+ lines.push(` ${chalk.dim(`── System / ${categoryLabel} ──────────────────────────────────────────`)}`);
45
38
  }
46
39
  const cursor = isCursor ? chalk.cyan("\u203a") : " ";
47
40
  const checkbox = item.selection === "selected" ? chalk.green("\u25cf") : chalk.dim("\u25cb");
@@ -82,10 +75,12 @@ export async function runSetupCommand(allSteps) {
82
75
  id: step.id,
83
76
  name: step.name,
84
77
  description: step.description,
85
- stage: step.stage,
78
+ categoryId: step.categoryId,
86
79
  brewPackageToInstall: step.brewPackageToInstall,
87
80
  },
88
- selection: PROTECTED_STEP_IDS.has(step.id) || step.stage === "tools" ? "selected" : "deselected",
81
+ selection: PROTECTED_STEP_IDS.has(step.id) || CONSERVATIVE_DEFAULT_ON_STEP_IDS.has(step.id)
82
+ ? "selected"
83
+ : "deselected",
89
84
  protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
90
85
  availability: availabilityResults[stepIndex] ? "available" : "unavailable",
91
86
  }));
@@ -95,7 +90,7 @@ export async function runSetupCommand(allSteps) {
95
90
  readline.emitKeypressEvents(process.stdin);
96
91
  if (process.stdin.isTTY)
97
92
  process.stdin.setRawMode(true);
98
- const handleKeypress = (_char, key) => {
93
+ const handleKeypress = (_typedCharacter, key) => {
99
94
  if (key?.ctrl && key?.name === "c") {
100
95
  process.exit(130);
101
96
  }
@@ -125,13 +120,17 @@ export async function runSetupCommand(allSteps) {
125
120
  process.stdin.on("keypress", handleKeypress);
126
121
  process.stdin.resume();
127
122
  });
128
- const stepsConfig = {};
123
+ const draftConfig = {
124
+ defaultsProfile: "conservative-v2",
125
+ steps: {},
126
+ };
129
127
  for (const item of items) {
130
- if (item.protection === "configurable" && item.selection === "deselected") {
131
- stepsConfig[item.step.id] = false;
128
+ if (item.protection === "protected") {
129
+ continue;
132
130
  }
131
+ draftConfig.steps[item.step.id] = item.selection === "selected";
133
132
  }
134
- await writeConfig({ steps: stepsConfig });
133
+ await writeConfig(cleanConfigForWrite(draftConfig));
135
134
  const confirmationLines = [
136
135
  chalk.yellow(LOGO_ART),
137
136
  "",
@@ -1,12 +1,27 @@
1
1
  import { collectVersions } from "../update/versionCollector.js";
2
2
  import { captureYesNo } from "../ui/prompt.js";
3
- import { buildScreen, createScreenRenderer } from "../ui/screen.js";
3
+ import { buildScreen, buildScreenWithAnimationFrame, createScreenRenderer } from "../ui/screen.js";
4
4
  import { hideCursorDuringExecution } from "../ui/terminal.js";
5
5
  import { runUpdateEngine } from "../update/engine.js";
6
6
  export async function runUpdateCommand(options) {
7
7
  const renderScreen = createScreenRenderer();
8
8
  const restoreCursor = hideCursorDuringExecution({ screenBuffer: "alternate" });
9
9
  let finalScreen = "";
10
+ let latestState = undefined;
11
+ let animationFrameIndex = 0;
12
+ const animationTimer = setInterval(() => {
13
+ if (!latestState) {
14
+ return;
15
+ }
16
+ if (latestState.runCompletion === "finished") {
17
+ return;
18
+ }
19
+ if (latestState.currentPhase !== "running" && latestState.currentPhase !== "installing") {
20
+ return;
21
+ }
22
+ animationFrameIndex += 1;
23
+ renderScreen(buildScreenWithAnimationFrame(latestState, animationFrameIndex));
24
+ }, 100);
10
25
  try {
11
26
  const finalState = await runUpdateEngine({
12
27
  steps: options.steps,
@@ -16,12 +31,14 @@ export async function runUpdateCommand(options) {
16
31
  askForConfirmation: captureYesNo,
17
32
  collectVersions,
18
33
  onStateChanged: (state) => {
19
- renderScreen(buildScreen(state));
34
+ latestState = state;
35
+ renderScreen(buildScreenWithAnimationFrame(state, animationFrameIndex));
20
36
  },
21
37
  });
22
38
  finalScreen = buildScreen(finalState);
23
39
  }
24
40
  finally {
41
+ clearInterval(animationTimer);
25
42
  restoreCursor();
26
43
  }
27
44
  if (finalScreen) {
package/dist/config.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  export declare const PROTECTED_STEP_IDS: Set<string>;
2
- export declare const DEFAULT_OFF_STEP_IDS: Set<string>;
2
+ export type ConfigDefaultsProfile = "legacy" | "conservative-v2";
3
+ export declare const LEGACY_DEFAULT_OFF_STEP_IDS: Set<string>;
4
+ export declare const CONSERVATIVE_DEFAULT_ON_STEP_IDS: Set<string>;
3
5
  export type BraeburnConfig = {
4
6
  steps: Record<string, boolean>;
5
7
  logo?: boolean;
8
+ defaultsProfile?: ConfigDefaultsProfile;
6
9
  };
7
10
  export declare function resolveConfigPath(): Promise<string>;
8
11
  export declare function configFileExists(): Promise<boolean>;
@@ -12,3 +15,4 @@ export declare function isSettingEnabled(config: BraeburnConfig, settingId: stri
12
15
  export declare function isStepEnabled(config: BraeburnConfig, stepId: string): boolean;
13
16
  export declare function isLogoEnabled(config: BraeburnConfig): boolean;
14
17
  export declare function applySettingToConfig(config: BraeburnConfig, settingId: string, desiredState: "enable" | "disable"): BraeburnConfig;
18
+ export declare function cleanConfigForWrite(config: BraeburnConfig): BraeburnConfig;
package/dist/config.js CHANGED
@@ -3,9 +3,30 @@ import { 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"]);
6
- export const DEFAULT_OFF_STEP_IDS = new Set(["nvm", "pyenv"]);
6
+ export const LEGACY_DEFAULT_OFF_STEP_IDS = new Set(["nvm", "pyenv"]);
7
+ export const CONSERVATIVE_DEFAULT_ON_STEP_IDS = new Set(["homebrew", "npm", "pip", "dotnet"]);
7
8
  const EMPTY_CONFIG = { steps: {} };
8
9
  const LOGO_SETTING_ID = "logo";
10
+ const DEFAULTS_PROFILE_SETTING_ID = "defaultsProfile";
11
+ const LEGACY_PROFILE = "legacy";
12
+ function resolveDefaultsProfile(config) {
13
+ return config.defaultsProfile ?? LEGACY_PROFILE;
14
+ }
15
+ function parseDefaultsProfile(rawValue) {
16
+ if (rawValue === "legacy" || rawValue === "conservative-v2") {
17
+ return rawValue;
18
+ }
19
+ return undefined;
20
+ }
21
+ function isStepEnabledByDefault(profile, stepId) {
22
+ if (profile === "conservative-v2") {
23
+ return CONSERVATIVE_DEFAULT_ON_STEP_IDS.has(stepId);
24
+ }
25
+ return !LEGACY_DEFAULT_OFF_STEP_IDS.has(stepId);
26
+ }
27
+ function shouldPersistStepOverride(profile, stepId, desiredState) {
28
+ return isStepEnabledByDefault(profile, stepId) !== (desiredState === "enable");
29
+ }
9
30
  async function pathExists(targetPath) {
10
31
  try {
11
32
  await access(targetPath);
@@ -31,7 +52,9 @@ export async function readConfig() {
31
52
  try {
32
53
  const raw = await readFile(configPath, "utf-8");
33
54
  const parsed = parse(raw);
34
- return { steps: parsed.steps ?? {}, logo: parsed.logo };
55
+ const parsedSteps = parsed.steps;
56
+ const defaultsProfile = parseDefaultsProfile(parsed[DEFAULTS_PROFILE_SETTING_ID]);
57
+ return { steps: parsedSteps ?? {}, logo: parsed.logo, defaultsProfile };
35
58
  }
36
59
  catch {
37
60
  return structuredClone(EMPTY_CONFIG);
@@ -47,9 +70,12 @@ export function isSettingEnabled(config, settingId) {
47
70
  return true;
48
71
  if (settingId === LOGO_SETTING_ID)
49
72
  return config.logo !== false;
50
- if (DEFAULT_OFF_STEP_IDS.has(settingId))
51
- return config.steps[settingId] === true;
52
- return config.steps[settingId] !== false;
73
+ const defaultsProfile = resolveDefaultsProfile(config);
74
+ const explicitStepOverride = config.steps[settingId];
75
+ if (explicitStepOverride === undefined) {
76
+ return isStepEnabledByDefault(defaultsProfile, settingId);
77
+ }
78
+ return explicitStepOverride === true;
53
79
  }
54
80
  export function isStepEnabled(config, stepId) {
55
81
  return isSettingEnabled(config, stepId);
@@ -68,20 +94,33 @@ export function applySettingToConfig(config, settingId, desiredState) {
68
94
  }
69
95
  return updatedConfig;
70
96
  }
71
- if (DEFAULT_OFF_STEP_IDS.has(settingId)) {
72
- if (desiredState === "enable") {
73
- updatedConfig.steps[settingId] = true;
97
+ const defaultsProfile = resolveDefaultsProfile(updatedConfig);
98
+ const shouldPersistOverride = shouldPersistStepOverride(defaultsProfile, settingId, desiredState);
99
+ if (!shouldPersistOverride) {
100
+ delete updatedConfig.steps[settingId];
101
+ return updatedConfig;
102
+ }
103
+ updatedConfig.steps[settingId] = desiredState === "enable";
104
+ return updatedConfig;
105
+ }
106
+ export function cleanConfigForWrite(config) {
107
+ const defaultsProfile = resolveDefaultsProfile(config);
108
+ const cleaned = { steps: {} };
109
+ for (const [stepId, explicitState] of Object.entries(config.steps)) {
110
+ if (PROTECTED_STEP_IDS.has(stepId)) {
111
+ continue;
74
112
  }
75
- else {
76
- delete updatedConfig.steps[settingId];
113
+ const desiredState = explicitState === true ? "enable" : "disable";
114
+ if (!shouldPersistStepOverride(defaultsProfile, stepId, desiredState)) {
115
+ continue;
77
116
  }
78
- return updatedConfig;
117
+ cleaned.steps[stepId] = explicitState;
79
118
  }
80
- if (desiredState === "enable") {
81
- delete updatedConfig.steps[settingId];
119
+ if (config.logo === false) {
120
+ cleaned.logo = false;
82
121
  }
83
- else {
84
- updatedConfig.steps[settingId] = false;
122
+ if (config.defaultsProfile && config.defaultsProfile !== LEGACY_PROFILE) {
123
+ cleaned.defaultsProfile = config.defaultsProfile;
85
124
  }
86
- return updatedConfig;
125
+ return cleaned;
87
126
  }
package/dist/index.js CHANGED
@@ -28,19 +28,25 @@ program
28
28
  .option("--no-logo", "Hide the logo")
29
29
  .addHelpText("after", `
30
30
  Step descriptions:
31
- Runtime stage (default: off — update the version managers and runtimes themselves):
31
+ System / Runtimes (default: off — larger changes, enabled intentionally):
32
32
  pyenv Upgrade pyenv, install latest Python 3.x (requires: pyenv or brew)
33
33
  nvm Install latest Node.js via nvm (requires: ~/.nvm)
34
34
 
35
- Tools stage (default: on — update packages installed via the managers above):
35
+ System / Apps & Packages:
36
36
  homebrew Update Homebrew itself and all installed formulae
37
37
  mas Upgrade Mac App Store apps (requires: mas)
38
- ohmyzsh Update Oh My Zsh (requires: ~/.oh-my-zsh)
38
+ macos Check for macOS updates, prompt to install
39
+
40
+ System / CLI Tools:
39
41
  npm Update global npm packages (requires: npm)
40
42
  pip Update global pip3 packages (requires: pip3) ⚠ may be fragile
41
43
  dotnet Update .NET global tools (requires: dotnet)
42
- macos Check for macOS updates, prompt to install
43
- cleanup Clean up Homebrew cache and old downloads
44
+
45
+ System / Shell:
46
+ ohmyzsh Update Oh My Zsh (requires: ~/.oh-my-zsh)
47
+
48
+ System / Maintenance:
49
+ cleanup homebrew cleanup (remove outdated Homebrew cache/downloads)
44
50
 
45
51
  Examples:
46
52
  braeburn Run all enabled steps interactively
@@ -48,7 +54,7 @@ Examples:
48
54
  braeburn -fy Same as above
49
55
  braeburn homebrew npm Run only the homebrew and npm steps
50
56
  braeburn homebrew -y Run only homebrew, auto-accept
51
- braeburn nvm pyenv Run only the runtime-stage steps
57
+ braeburn nvm pyenv Run only the runtime steps
52
58
  `)
53
59
  .action(async (stepArguments, options) => {
54
60
  const autoYes = options.yes === true || options.force === true;
@@ -83,7 +89,7 @@ program
83
89
  .option("--nvm", "Show latest nvm log")
84
90
  .option("--dotnet", "Show latest .NET log")
85
91
  .option("--macos", "Show latest macOS update log")
86
- .option("--cleanup", "Show latest cleanup log")
92
+ .option("--cleanup", "Show latest homebrew cleanup log")
87
93
  .addHelpText("after", `
88
94
  Examples:
89
95
  braeburn log List all available step logs
@@ -1,13 +1,26 @@
1
1
  import { pyenvStep, nvmStep, homebrewStep, masStep, ohmyzshStep, npmStep, pipStep, dotnetStep, macosStep, cleanupStep, } from "./index.js";
2
- export const ALL_STEPS = [
3
- pyenvStep,
4
- nvmStep,
5
- homebrewStep,
6
- masStep,
7
- ohmyzshStep,
8
- npmStep,
9
- pipStep,
10
- dotnetStep,
11
- macosStep,
12
- cleanupStep,
2
+ const STEP_BY_ID = {
3
+ pyenv: pyenvStep,
4
+ nvm: nvmStep,
5
+ homebrew: homebrewStep,
6
+ mas: masStep,
7
+ macos: macosStep,
8
+ npm: npmStep,
9
+ pip: pipStep,
10
+ dotnet: dotnetStep,
11
+ ohmyzsh: ohmyzshStep,
12
+ cleanup: cleanupStep,
13
+ };
14
+ const STEP_EXECUTION_ORDER = [
15
+ "pyenv",
16
+ "nvm",
17
+ "homebrew",
18
+ "mas",
19
+ "macos",
20
+ "npm",
21
+ "pip",
22
+ "dotnet",
23
+ "ohmyzsh",
24
+ "cleanup",
13
25
  ];
26
+ export const ALL_STEPS = STEP_EXECUTION_ORDER.map((stepId) => STEP_BY_ID[stepId]);
@@ -0,0 +1,7 @@
1
+ export type StepCategoryId = "apps-packages" | "cli-tools" | "runtimes" | "shell" | "maintenance";
2
+ export type StepCategoryDefinition = {
3
+ id: StepCategoryId;
4
+ label: string;
5
+ };
6
+ export declare function listStepCategoryDefinitions(): StepCategoryDefinition[];
7
+ export declare function getStepCategoryLabel(categoryId: StepCategoryId): string;
@@ -0,0 +1,18 @@
1
+ const STEP_CATEGORY_DEFINITIONS = [
2
+ { id: "runtimes", label: "Runtimes" },
3
+ { id: "apps-packages", label: "Apps & Packages" },
4
+ { id: "cli-tools", label: "CLI Tools" },
5
+ { id: "shell", label: "Shell" },
6
+ { id: "maintenance", label: "Maintenance" },
7
+ ];
8
+ const STEP_CATEGORY_DEFINITION_BY_ID = new Map(STEP_CATEGORY_DEFINITIONS.map((categoryDefinition) => [categoryDefinition.id, categoryDefinition]));
9
+ export function listStepCategoryDefinitions() {
10
+ return [...STEP_CATEGORY_DEFINITIONS];
11
+ }
12
+ export function getStepCategoryLabel(categoryId) {
13
+ const categoryDefinition = STEP_CATEGORY_DEFINITION_BY_ID.get(categoryId);
14
+ if (!categoryDefinition) {
15
+ return categoryId;
16
+ }
17
+ return categoryDefinition.label;
18
+ }
@@ -1,8 +1,8 @@
1
1
  import { checkCommandExists } from "./runtime.js";
2
2
  const cleanupStep = {
3
3
  id: "cleanup",
4
- name: "Cleanup",
5
- stage: "tools",
4
+ name: "homebrew cleanup",
5
+ categoryId: "maintenance",
6
6
  description: "Remove outdated Homebrew downloads and cached versions",
7
7
  async checkIsAvailable() {
8
8
  return checkCommandExists("brew");
@@ -2,7 +2,7 @@ import { checkCommandExists } from "./runtime.js";
2
2
  const dotnetStep = {
3
3
  id: "dotnet",
4
4
  name: ".NET",
5
- stage: "tools",
5
+ categoryId: "cli-tools",
6
6
  description: "Update all globally installed .NET tools",
7
7
  async checkIsAvailable() {
8
8
  return checkCommandExists("dotnet");
@@ -0,0 +1,10 @@
1
+ import type { StepCategoryId } from "./categories.js";
2
+ type CategorizedItem = {
3
+ categoryId: StepCategoryId;
4
+ };
5
+ export type CategorySection<TItem> = {
6
+ categoryId: StepCategoryId;
7
+ items: TItem[];
8
+ };
9
+ export declare function buildCategorySectionsInOrder<TItem extends CategorizedItem>(orderedItems: TItem[]): CategorySection<TItem>[];
10
+ export {};
@@ -0,0 +1,15 @@
1
+ export function buildCategorySectionsInOrder(orderedItems) {
2
+ const sections = [];
3
+ for (const item of orderedItems) {
4
+ const lastSection = sections[sections.length - 1];
5
+ if (!lastSection || lastSection.categoryId !== item.categoryId) {
6
+ sections.push({
7
+ categoryId: item.categoryId,
8
+ items: [item],
9
+ });
10
+ continue;
11
+ }
12
+ lastSection.items.push(item);
13
+ }
14
+ return sections;
15
+ }
@@ -2,7 +2,7 @@ import { checkCommandExists } from "./runtime.js";
2
2
  const homebrewStep = {
3
3
  id: "homebrew",
4
4
  name: "Homebrew",
5
- stage: "tools",
5
+ categoryId: "apps-packages",
6
6
  description: "Update Homebrew itself and upgrade all installed formulae",
7
7
  async checkIsAvailable() {
8
8
  return checkCommandExists("brew");
@@ -1,4 +1,8 @@
1
- export type { StepRunContext, StepStage, Step, } from "./types.js";
1
+ export type { StepRunContext, Step, } from "./types.js";
2
+ export type { StepCategoryId, StepCategoryDefinition, } from "./categories.js";
3
+ export { listStepCategoryDefinitions, getStepCategoryLabel, } from "./categories.js";
4
+ export type { CategorySection, } from "./grouping.js";
5
+ export { buildCategorySectionsInOrder, } from "./grouping.js";
2
6
  export { checkCommandExists, checkPathExists, runStep, createDefaultStepRunContext, } from "./runtime.js";
3
7
  export { default as homebrewStep } from "./homebrew.js";
4
8
  export { default as masStep } from "./mas.js";
@@ -1,3 +1,5 @@
1
+ export { listStepCategoryDefinitions, getStepCategoryLabel, } from "./categories.js";
2
+ export { buildCategorySectionsInOrder, } from "./grouping.js";
1
3
  export { checkCommandExists, checkPathExists, runStep, createDefaultStepRunContext, } from "./runtime.js";
2
4
  export { default as homebrewStep } from "./homebrew.js";
3
5
  export { default as masStep } from "./mas.js";
@@ -1,7 +1,7 @@
1
1
  const macosStep = {
2
2
  id: "macos",
3
3
  name: "macOS",
4
- stage: "tools",
4
+ categoryId: "apps-packages",
5
5
  description: "Check for and optionally install macOS system software updates",
6
6
  async checkIsAvailable() {
7
7
  return true;
package/dist/steps/mas.js CHANGED
@@ -2,7 +2,7 @@ import { checkCommandExists } from "./runtime.js";
2
2
  const masStep = {
3
3
  id: "mas",
4
4
  name: "Mac App Store",
5
- stage: "tools",
5
+ categoryId: "apps-packages",
6
6
  description: "Upgrade all Mac App Store apps via the mas CLI tool",
7
7
  brewPackageToInstall: "mas",
8
8
  async checkIsAvailable() {
package/dist/steps/npm.js CHANGED
@@ -2,7 +2,7 @@ import { checkCommandExists } from "./runtime.js";
2
2
  const npmStep = {
3
3
  id: "npm",
4
4
  name: "npm",
5
- stage: "tools",
5
+ categoryId: "cli-tools",
6
6
  description: "Update all globally installed npm packages",
7
7
  async checkIsAvailable() {
8
8
  return checkCommandExists("npm");
package/dist/steps/nvm.js CHANGED
@@ -16,7 +16,7 @@ const NVM_INSTALL_COMMAND = `${NVM_SOURCE_PREFIX} && ` +
16
16
  const nvmStep = {
17
17
  id: "nvm",
18
18
  name: "Node.js (nvm)",
19
- stage: "runtime",
19
+ categoryId: "runtimes",
20
20
  description: "Install the latest Node.js via nvm, migrating packages from the current version",
21
21
  async checkIsAvailable() {
22
22
  return checkPathExists(NVM_SCRIPT_PATH);
@@ -5,7 +5,7 @@ const OH_MY_ZSH_UPGRADE_SCRIPT_PATH = join(homedir(), ".oh-my-zsh", "tools", "up
5
5
  const ohmyzshStep = {
6
6
  id: "ohmyzsh",
7
7
  name: "Oh My Zsh",
8
- stage: "tools",
8
+ categoryId: "shell",
9
9
  description: "Update Oh My Zsh to the latest version",
10
10
  async checkIsAvailable() {
11
11
  return checkPathExists(OH_MY_ZSH_UPGRADE_SCRIPT_PATH);
package/dist/steps/pip.js CHANGED
@@ -3,7 +3,7 @@ const PIP_UPDATE_ALL_OUTDATED_SHELL_COMMAND = "pip3 list --outdated --format=col
3
3
  const pipStep = {
4
4
  id: "pip",
5
5
  name: "pip3",
6
- stage: "tools",
6
+ categoryId: "cli-tools",
7
7
  description: "Update all globally installed pip3 packages",
8
8
  warning: "This updates all global pip3 packages, which can occasionally break tools.",
9
9
  async checkIsAvailable() {
@@ -3,7 +3,7 @@ const FIND_LATEST_STABLE_PYTHON_SHELL_COMMAND = "pyenv install -l | grep -E '^\\
3
3
  const pyenvStep = {
4
4
  id: "pyenv",
5
5
  name: "pyenv",
6
- stage: "runtime",
6
+ categoryId: "runtimes",
7
7
  description: "Upgrade pyenv via Homebrew and install the latest Python 3.x",
8
8
  brewPackageToInstall: "pyenv",
9
9
  async checkIsAvailable() {
@@ -1,5 +1,6 @@
1
1
  import type { OutputLineCallback } from "../runner.js";
2
2
  import type { StepLogWriter } from "../logger.js";
3
+ import type { StepCategoryId } from "./categories.js";
3
4
  export type StepRunContext = {
4
5
  onOutputLine: OutputLineCallback;
5
6
  logWriter: StepLogWriter;
@@ -8,12 +9,11 @@ export type StepRunContext = {
8
9
  shellCommand: string;
9
10
  }) => Promise<string>;
10
11
  };
11
- export type StepStage = "runtime" | "tools";
12
12
  export type Step = {
13
13
  id: string;
14
14
  name: string;
15
15
  description: string;
16
- stage: StepStage;
16
+ categoryId: StepCategoryId;
17
17
  warning?: string;
18
18
  brewPackageToInstall?: string;
19
19
  checkIsAvailable: () => Promise<boolean>;
@@ -0,0 +1 @@
1
+ export declare function getActivityIndicatorFrame(frameIndex: number): string;
@@ -0,0 +1,5 @@
1
+ const ACTIVITY_FRAMES = ["◐", "◓", "◑", "◒"];
2
+ export function getActivityIndicatorFrame(frameIndex) {
3
+ const normalizedFrameIndex = Math.abs(Math.trunc(frameIndex));
4
+ return ACTIVITY_FRAMES[normalizedFrameIndex % ACTIVITY_FRAMES.length];
5
+ }
@@ -4,6 +4,7 @@ type ActiveStepOptions = {
4
4
  stepNumber: number;
5
5
  totalSteps: number;
6
6
  phase: StepPhase;
7
+ activityFrameIndex?: number;
7
8
  };
8
9
  export declare function buildActiveStepLines(options: ActiveStepOptions): string[];
9
10
  export {};
@@ -1,6 +1,8 @@
1
1
  import chalk from "chalk";
2
+ import { getActivityIndicatorFrame } from "./activityIndicator.js";
2
3
  export function buildActiveStepLines(options) {
3
4
  const { step, stepNumber, totalSteps, phase } = options;
5
+ const activityFrameIndex = options.activityFrameIndex ?? 0;
4
6
  const isRunning = phase === "running" || phase === "installing";
5
7
  const lines = [
6
8
  chalk.dim(` ${"─".repeat(3)} Step ${stepNumber}/${totalSteps} `) +
@@ -10,7 +12,7 @@ export function buildActiveStepLines(options) {
10
12
  ];
11
13
  if (isRunning) {
12
14
  const label = phase === "installing" ? "Installing..." : "Running...";
13
- lines.push(` ${chalk.blue("▶")} ${label}`);
15
+ lines.push(` ${chalk.blue(getActivityIndicatorFrame(activityFrameIndex))} ${label}`);
14
16
  }
15
17
  return lines;
16
18
  }
@@ -2,7 +2,7 @@ import type { DisplayStep, StepPhase, CompletedStepRecord, LogoVisibility } from
2
2
  import type { TerminalDimensions } from "./outputBox.js";
3
3
  type LogoLayout = "side-by-side" | "stacked" | "none";
4
4
  export declare function determineLogoLayout(logoLines: string[], dimensions?: TerminalDimensions): LogoLayout;
5
- export declare function stepTrackerIcon(phase: StepPhase): string;
5
+ export declare function stepTrackerIcon(phase: StepPhase, activityFrameIndex?: number): string;
6
6
  export declare function isActivePhase(phase: StepPhase): boolean;
7
7
  export declare function deriveAllStepPhases(steps: DisplayStep[], currentStepIndex: number, currentPhase: StepPhase, completedStepRecords: CompletedStepRecord[]): StepPhase[];
8
8
  type BuildHeaderOptions = {
@@ -12,6 +12,7 @@ type BuildHeaderOptions = {
12
12
  currentStepIndex: number;
13
13
  currentPhase: StepPhase;
14
14
  completedStepRecords: CompletedStepRecord[];
15
+ activityFrameIndex?: number;
15
16
  terminalDimensions?: TerminalDimensions;
16
17
  };
17
18
  export declare function buildHeaderLines(options: BuildHeaderOptions): string[];
package/dist/ui/header.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { LOGO_ART } from "../logo.js";
3
+ import { buildCategorySectionsInOrder, getStepCategoryLabel } from "../steps/index.js";
4
+ import { getActivityIndicatorFrame } from "./activityIndicator.js";
3
5
  const LOGO_COLUMN_WIDTH = 32;
4
6
  const LOGO_SEPARATOR = " ";
5
7
  const MIN_SIDE_BY_SIDE_COLS = LOGO_COLUMN_WIDTH + LOGO_SEPARATOR.length + 20; // 56
@@ -14,16 +16,16 @@ export function determineLogoLayout(logoLines, dimensions) {
14
16
  }
15
17
  return "none";
16
18
  }
17
- export function stepTrackerIcon(phase) {
19
+ export function stepTrackerIcon(phase, activityFrameIndex = 0) {
18
20
  if (phase === "complete")
19
21
  return chalk.green("✓ ");
20
22
  if (phase === "failed")
21
23
  return chalk.red("✗ ");
22
24
  if (phase === "skipped" || phase === "not-available")
23
25
  return chalk.dim("– ");
24
- if (phase === "running" ||
25
- phase === "installing" ||
26
- phase === "prompting-to-run" ||
26
+ if (phase === "running" || phase === "installing")
27
+ return chalk.cyan(`${getActivityIndicatorFrame(activityFrameIndex)} `);
28
+ if (phase === "prompting-to-run" ||
27
29
  phase === "prompting-to-install" ||
28
30
  phase === "checking-availability")
29
31
  return chalk.cyan("→ ");
@@ -47,26 +49,17 @@ export function deriveAllStepPhases(steps, currentStepIndex, currentPhase, compl
47
49
  }
48
50
  export function buildHeaderLines(options) {
49
51
  const { steps, version, logoVisibility, currentStepIndex, currentPhase, completedStepRecords } = options;
52
+ const activityFrameIndex = options.activityFrameIndex ?? 0;
50
53
  const phases = deriveAllStepPhases(steps, currentStepIndex, currentPhase, completedStepRecords);
51
- const hasRuntimeSteps = steps.some((step) => step.stage === "runtime");
52
- const hasToolsSteps = steps.some((step) => step.stage === "tools");
53
- const showStageLabels = hasRuntimeSteps && hasToolsSteps;
54
54
  const stepLines = [];
55
- for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
56
- const step = steps[stepIndex];
57
- if (showStageLabels) {
58
- const isFirstRuntime = step.stage === "runtime" && (stepIndex === 0 || steps[stepIndex - 1].stage !== "runtime");
59
- const isFirstTools = step.stage === "tools" && (stepIndex === 0 || steps[stepIndex - 1].stage !== "tools");
60
- if (isFirstRuntime) {
61
- stepLines.push(chalk.dim("Runtimes"));
62
- }
63
- else if (isFirstTools) {
64
- stepLines.push(chalk.dim("Tools"));
65
- }
55
+ const indexedSteps = steps.map((step, stepIndex) => ({ step, stepIndex, categoryId: step.categoryId }));
56
+ for (const section of buildCategorySectionsInOrder(indexedSteps)) {
57
+ stepLines.push(chalk.dim(`System / ${getStepCategoryLabel(section.categoryId)}`));
58
+ for (const indexedStep of section.items) {
59
+ const icon = stepTrackerIcon(phases[indexedStep.stepIndex], activityFrameIndex);
60
+ const name = isActivePhase(phases[indexedStep.stepIndex]) ? chalk.white(indexedStep.step.name) : chalk.dim(indexedStep.step.name);
61
+ stepLines.push(`${icon}${name}`);
66
62
  }
67
- const icon = stepTrackerIcon(phases[stepIndex]);
68
- const name = isActivePhase(phases[stepIndex]) ? chalk.white(step.name) : chalk.dim(step.name);
69
- stepLines.push(`${icon}${name}`);
70
63
  }
71
64
  const rightColumnLines = [
72
65
  `${chalk.bold.white("braeburn")} ${chalk.dim("v" + version)}`,
@@ -3,3 +3,4 @@ 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;
6
+ export declare function buildScreenWithAnimationFrame(state: AppState, activityFrameIndex: number, terminalDimensions?: TerminalDimensions): string;
package/dist/ui/screen.js CHANGED
@@ -10,6 +10,9 @@ export function createScreenRenderer(output = process.stdout) {
10
10
  };
11
11
  }
12
12
  export function buildScreen(state, terminalDimensions) {
13
+ return buildScreenWithAnimationFrame(state, 0, terminalDimensions);
14
+ }
15
+ export function buildScreenWithAnimationFrame(state, activityFrameIndex, terminalDimensions) {
13
16
  const lines = [];
14
17
  const failedStepIds = state.completedStepRecords.flatMap((record, stepIndex) => {
15
18
  if (record.phase !== "failed") {
@@ -28,6 +31,7 @@ export function buildScreen(state, terminalDimensions) {
28
31
  currentStepIndex: state.currentStepIndex,
29
32
  currentPhase: state.currentPhase,
30
33
  completedStepRecords: state.completedStepRecords,
34
+ activityFrameIndex,
31
35
  terminalDimensions,
32
36
  }));
33
37
  lines.push("");
@@ -50,6 +54,7 @@ export function buildScreen(state, terminalDimensions) {
50
54
  stepNumber: state.currentStepIndex + 1,
51
55
  totalSteps: state.steps.length,
52
56
  phase: state.currentPhase,
57
+ activityFrameIndex,
53
58
  }));
54
59
  const isShowingOutput = (state.currentPhase === "running" || state.currentPhase === "installing") &&
55
60
  state.currentOutputLines.length > 0;
@@ -1 +1 @@
1
- export { type StepStage, 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, } from "../update/state.js";
@@ -3,7 +3,7 @@ export function toDisplayStep(step) {
3
3
  id: step.id,
4
4
  name: step.name,
5
5
  description: step.description,
6
- stage: step.stage,
6
+ categoryId: step.categoryId,
7
7
  };
8
8
  }
9
9
  export function toDisplaySteps(steps) {
@@ -1,10 +1,10 @@
1
1
  import type { CommandOutputLine } from "../runner.js";
2
- export type StepStage = "runtime" | "tools";
2
+ import type { StepCategoryId } from "../steps/categories.js";
3
3
  export type DisplayStep = {
4
4
  id: string;
5
5
  name: string;
6
6
  description: string;
7
- stage: StepStage;
7
+ categoryId: StepCategoryId;
8
8
  };
9
9
  export type StepPhase = "pending" | "checking-availability" | "prompting-to-install" | "installing" | "prompting-to-run" | "running" | "complete" | "failed" | "skipped" | "not-available";
10
10
  export type CompletedStepRecord = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braeburn",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "macOS system updater CLI",
5
5
  "type": "module",
6
6
  "bin": {