braeburn 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/braeburn)](https://www.npmjs.com/package/braeburn)
4
4
 
5
- A macOS system updater CLI. Runs update steps for Homebrew, Mac App Store, Oh My Zsh, npm, pip, pyenv, nvm, .NET, and macOS itself.
5
+ A macOS system updater CLI. Keeps tools installed via Homebrew, npm, pip, .NET, and others up to date.
6
6
 
7
7
  ## Install
8
8
 
@@ -30,7 +30,20 @@ braeburn homebrew npm # run specific steps only
30
30
 
31
31
  ## Steps
32
32
 
33
- `homebrew` `mas` `ohmyzsh` `npm` `pip` `pyenv` `nvm` `dotnet` `macos` `cleanup`
33
+ Steps run in two stages. The runtime stage runs first and is **off by default** — upgrading a runtime is a larger change than upgrading a tool, and is best done intentionally.
34
+
35
+ | Step | Stage | Default | Requires |
36
+ |---|---|---|---|
37
+ | `pyenv` | runtime | off | `pyenv` or Homebrew |
38
+ | `nvm` | runtime | off | `~/.nvm` |
39
+ | `homebrew` | tools | on | `brew` (required) |
40
+ | `mas` | tools | on | `mas` |
41
+ | `ohmyzsh` | tools | on | `~/.oh-my-zsh` |
42
+ | `npm` | tools | on | `npm` |
43
+ | `pip` | tools | on | `pip3` |
44
+ | `dotnet` | tools | on | `dotnet` |
45
+ | `macos` | tools | on | — |
46
+ | `cleanup` | tools | on | `brew` |
34
47
 
35
48
  ## Requirements
36
49
 
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { readConfig, writeConfig, resolveConfigPath, isSettingEnabled, isStepEnabled, isLogoEnabled, applySettingToConfig, PROTECTED_STEP_IDS, } from "../config.js";
2
+ import { readConfig, writeConfig, resolveConfigPath, isSettingEnabled, isStepEnabled, isLogoEnabled, applySettingToConfig, PROTECTED_STEP_IDS, DEFAULT_OFF_STEP_IDS, } from "../config.js";
3
3
  export async function runConfigCommand(options) {
4
4
  const { allSteps } = options;
5
5
  const config = await readConfig();
@@ -76,10 +76,13 @@ export async function runConfigUpdateCommand(options) {
76
76
  }
77
77
  async function writeCleanConfig(config) {
78
78
  const cleaned = { steps: {} };
79
- for (const [stepId, enabled] of Object.entries(config.steps)) {
80
- if (enabled === false) {
79
+ for (const [stepId, value] of Object.entries(config.steps)) {
80
+ if (value === false && !DEFAULT_OFF_STEP_IDS.has(stepId)) {
81
81
  cleaned.steps[stepId] = false;
82
82
  }
83
+ else if (value === true && DEFAULT_OFF_STEP_IDS.has(stepId)) {
84
+ cleaned.steps[stepId] = true;
85
+ }
83
86
  }
84
87
  if (config.logo === false) {
85
88
  cleaned.logo = false;
@@ -27,9 +27,22 @@ export function buildSetupScreen(items, cursorIndex) {
27
27
  ` ${chalk.dim("\u2191\u2193 navigate Space toggle Return confirm")}`,
28
28
  "",
29
29
  ];
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;
30
33
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
31
34
  const item = items[itemIndex];
32
35
  const isCursor = itemIndex === cursorIndex;
36
+ if (showStageLabels) {
37
+ const isFirstRuntime = item.step.stage === "runtime" && (itemIndex === 0 || items[itemIndex - 1].step.stage !== "runtime");
38
+ const isFirstTools = item.step.stage === "tools" && (itemIndex === 0 || items[itemIndex - 1].step.stage !== "tools");
39
+ if (isFirstRuntime) {
40
+ lines.push(` ${chalk.dim("── Runtimes ─────────────────────────────────────────────────────────")}`);
41
+ }
42
+ else if (isFirstTools) {
43
+ lines.push(` ${chalk.dim("── Tools ────────────────────────────────────────────────────────────")}`);
44
+ }
45
+ }
33
46
  const cursor = isCursor ? chalk.cyan("\u203a") : " ";
34
47
  const checkbox = item.selection === "selected" ? chalk.green("\u25cf") : chalk.dim("\u25cb");
35
48
  const namePadded = item.step.name.padEnd(18);
@@ -65,7 +78,7 @@ export async function runSetupCommand(allSteps) {
65
78
  const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
66
79
  const items = allSteps.map((step, stepIndex) => ({
67
80
  step,
68
- selection: "selected",
81
+ selection: PROTECTED_STEP_IDS.has(step.id) || step.stage === "tools" ? "selected" : "deselected",
69
82
  protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
70
83
  availability: availabilityResults[stepIndex] ? "available" : "unavailable",
71
84
  }));
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export declare const PROTECTED_STEP_IDS: Set<string>;
2
+ export declare const DEFAULT_OFF_STEP_IDS: Set<string>;
2
3
  export type BraeburnConfig = {
3
4
  steps: Record<string, boolean>;
4
5
  logo?: boolean;
package/dist/config.js CHANGED
@@ -3,6 +3,7 @@ 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
7
  const EMPTY_CONFIG = { steps: {} };
7
8
  const LOGO_SETTING_ID = "logo";
8
9
  async function pathExists(targetPath) {
@@ -46,6 +47,8 @@ export function isSettingEnabled(config, settingId) {
46
47
  return true;
47
48
  if (settingId === LOGO_SETTING_ID)
48
49
  return config.logo !== false;
50
+ if (DEFAULT_OFF_STEP_IDS.has(settingId))
51
+ return config.steps[settingId] === true;
49
52
  return config.steps[settingId] !== false;
50
53
  }
51
54
  export function isStepEnabled(config, stepId) {
@@ -65,6 +68,15 @@ export function applySettingToConfig(config, settingId, desiredState) {
65
68
  }
66
69
  return updatedConfig;
67
70
  }
71
+ if (DEFAULT_OFF_STEP_IDS.has(settingId)) {
72
+ if (desiredState === "enable") {
73
+ updatedConfig.steps[settingId] = true;
74
+ }
75
+ else {
76
+ delete updatedConfig.steps[settingId];
77
+ }
78
+ return updatedConfig;
79
+ }
68
80
  if (desiredState === "enable") {
69
81
  delete updatedConfig.steps[settingId];
70
82
  }
package/dist/index.js CHANGED
@@ -10,13 +10,13 @@ import { runConfigCommand, runConfigUpdateCommand } from "./commands/config.js";
10
10
  import { runSetupCommand } from "./commands/setup.js";
11
11
  import { readConfig, isStepEnabled, isLogoEnabled, PROTECTED_STEP_IDS, configFileExists } from "./config.js";
12
12
  const ALL_STEPS = [
13
+ pyenvStep,
14
+ nvmStep,
13
15
  homebrewStep,
14
16
  masStep,
15
17
  ohmyzshStep,
16
18
  npmStep,
17
19
  pipStep,
18
- pyenvStep,
19
- nvmStep,
20
20
  dotnetStep,
21
21
  macosStep,
22
22
  cleanupStep,
@@ -40,23 +40,27 @@ program
40
40
  .option("--no-logo", "Hide the logo")
41
41
  .addHelpText("after", `
42
42
  Step descriptions:
43
+ Runtime stage (default: off — update the version managers and runtimes themselves):
44
+ pyenv Upgrade pyenv, install latest Python 3.x (requires: pyenv or brew)
45
+ nvm Install latest Node.js via nvm (requires: ~/.nvm)
46
+
47
+ Tools stage (default: on — update packages installed via the managers above):
43
48
  homebrew Update Homebrew itself and all installed formulae
44
49
  mas Upgrade Mac App Store apps (requires: mas)
45
50
  ohmyzsh Update Oh My Zsh (requires: ~/.oh-my-zsh)
46
51
  npm Update global npm packages (requires: npm)
47
52
  pip Update global pip3 packages (requires: pip3) ⚠ may be fragile
48
- pyenv Upgrade pyenv, install latest Python 3.x (requires: pyenv or brew)
49
- nvm Update Node.js via nvm (requires: ~/.nvm)
50
53
  dotnet Update .NET global tools (requires: dotnet)
51
54
  macos Check for macOS updates, prompt to install
52
55
  cleanup Clean up Homebrew cache and old downloads
53
56
 
54
57
  Examples:
55
- braeburn Run all steps interactively
56
- braeburn -y Run all steps, auto-accept everything
58
+ braeburn Run all enabled steps interactively
59
+ braeburn -y Run all enabled steps, auto-accept everything
57
60
  braeburn -fy Same as above
58
61
  braeburn homebrew npm Run only the homebrew and npm steps
59
62
  braeburn homebrew -y Run only homebrew, auto-accept
63
+ braeburn nvm pyenv Run only the runtime-stage steps
60
64
  `)
61
65
  .action(async (stepArguments, options) => {
62
66
  const autoYes = options.yes === true || options.force === true;
@@ -2,6 +2,7 @@ import { checkCommandExists } from "./index.js";
2
2
  const cleanupStep = {
3
3
  id: "cleanup",
4
4
  name: "Cleanup",
5
+ stage: "tools",
5
6
  description: "Remove outdated Homebrew downloads and cached versions",
6
7
  async checkIsAvailable() {
7
8
  return checkCommandExists("brew");
@@ -2,6 +2,7 @@ import { checkCommandExists } from "./index.js";
2
2
  const dotnetStep = {
3
3
  id: "dotnet",
4
4
  name: ".NET",
5
+ stage: "tools",
5
6
  description: "Update all globally installed .NET tools",
6
7
  async checkIsAvailable() {
7
8
  return checkCommandExists("dotnet");
@@ -2,6 +2,7 @@ import { checkCommandExists } from "./index.js";
2
2
  const homebrewStep = {
3
3
  id: "homebrew",
4
4
  name: "Homebrew",
5
+ stage: "tools",
5
6
  description: "Update Homebrew itself and upgrade all installed formulae",
6
7
  async checkIsAvailable() {
7
8
  return checkCommandExists("brew");
@@ -8,10 +8,12 @@ export type StepRunContext = {
8
8
  shellCommand: string;
9
9
  }) => Promise<string>;
10
10
  };
11
+ export type StepStage = "runtime" | "tools";
11
12
  export type Step = {
12
13
  id: string;
13
14
  name: string;
14
15
  description: string;
16
+ stage: StepStage;
15
17
  warning?: string;
16
18
  brewPackageToInstall?: string;
17
19
  checkIsAvailable: () => Promise<boolean>;
@@ -1,6 +1,7 @@
1
1
  const macosStep = {
2
2
  id: "macos",
3
3
  name: "macOS",
4
+ stage: "tools",
4
5
  description: "Check for and optionally install macOS system software updates",
5
6
  async checkIsAvailable() {
6
7
  return true;
package/dist/steps/mas.js CHANGED
@@ -2,6 +2,7 @@ import { checkCommandExists } from "./index.js";
2
2
  const masStep = {
3
3
  id: "mas",
4
4
  name: "Mac App Store",
5
+ stage: "tools",
5
6
  description: "Upgrade all Mac App Store apps via the mas CLI tool",
6
7
  brewPackageToInstall: "mas",
7
8
  async checkIsAvailable() {
package/dist/steps/npm.js CHANGED
@@ -2,6 +2,7 @@ import { checkCommandExists } from "./index.js";
2
2
  const npmStep = {
3
3
  id: "npm",
4
4
  name: "npm",
5
+ stage: "tools",
5
6
  description: "Update all globally installed npm packages",
6
7
  async checkIsAvailable() {
7
8
  return checkCommandExists("npm");
package/dist/steps/nvm.js CHANGED
@@ -8,6 +8,7 @@ const NVM_SOURCE_PREFIX = `export NVM_DIR="${NVM_DIRECTORY}" && source "$NVM_DIR
8
8
  const nvmStep = {
9
9
  id: "nvm",
10
10
  name: "Node.js (nvm)",
11
+ stage: "runtime",
11
12
  description: "Install the latest Node.js via nvm, migrating packages from the current version",
12
13
  async checkIsAvailable() {
13
14
  return checkPathExists(NVM_DIRECTORY);
@@ -5,6 +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
9
  description: "Update Oh My Zsh to the latest version",
9
10
  async checkIsAvailable() {
10
11
  return checkPathExists(OH_MY_ZSH_UPGRADE_SCRIPT_PATH);
package/dist/steps/pip.js CHANGED
@@ -3,6 +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
7
  description: "Update all globally installed pip3 packages",
7
8
  warning: "This updates all global pip3 packages, which can occasionally break tools.",
8
9
  async checkIsAvailable() {
@@ -3,6 +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
7
  description: "Upgrade pyenv via Homebrew and install the latest Python 3.x",
7
8
  brewPackageToInstall: "pyenv",
8
9
  async checkIsAvailable() {
package/dist/ui/header.js CHANGED
@@ -48,15 +48,31 @@ export function deriveAllStepPhases(steps, currentStepIndex, currentPhase, compl
48
48
  export function buildHeaderLines(options) {
49
49
  const { steps, version, logoVisibility, currentStepIndex, currentPhase, completedStepRecords } = options;
50
50
  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
+ 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
+ }
66
+ }
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
+ }
51
71
  const rightColumnLines = [
52
72
  `${chalk.bold.white("braeburn")} ${chalk.dim("v" + version)}`,
53
73
  chalk.dim("macOS system updater"),
54
74
  "",
55
- ...steps.map((step, index) => {
56
- const icon = stepTrackerIcon(phases[index]);
57
- const name = isActivePhase(phases[index]) ? chalk.white(step.name) : chalk.dim(step.name);
58
- return `${icon}${name}`;
59
- }),
75
+ ...stepLines,
60
76
  ];
61
77
  if (logoVisibility === "hidden") {
62
78
  return rightColumnLines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braeburn",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "macOS system updater CLI",
5
5
  "type": "module",
6
6
  "bin": {