braeburn 1.2.2 → 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 +15 -2
- package/dist/commands/config.d.ts +13 -2
- package/dist/commands/config.js +25 -36
- package/dist/commands/setup.d.ts +11 -0
- package/dist/commands/setup.js +41 -49
- package/dist/commands/update.d.ts +4 -2
- package/dist/commands/update.js +20 -28
- package/dist/config.d.ts +3 -1
- package/dist/config.js +43 -9
- package/dist/index.js +22 -23
- package/dist/logger.d.ts +3 -3
- package/dist/logger.js +15 -15
- package/dist/logo.js +0 -2
- package/dist/runner.d.ts +2 -1
- package/dist/runner.js +2 -2
- package/dist/steps/cleanup.js +3 -2
- package/dist/steps/dotnet.js +3 -3
- package/dist/steps/homebrew.js +3 -2
- package/dist/steps/index.d.ts +8 -0
- package/dist/steps/index.js +10 -1
- package/dist/steps/macos.js +6 -8
- package/dist/steps/mas.js +3 -2
- package/dist/steps/npm.js +3 -3
- package/dist/steps/nvm.js +3 -3
- package/dist/steps/ohmyzsh.js +3 -3
- package/dist/steps/pip.js +4 -5
- package/dist/steps/pyenv.js +6 -8
- package/dist/ui/header.d.ts +9 -2
- package/dist/ui/header.js +35 -22
- package/dist/ui/outputBox.d.ts +5 -1
- package/dist/ui/outputBox.js +14 -8
- package/dist/ui/screen.d.ts +4 -2
- package/dist/ui/screen.js +14 -11
- package/dist/ui/state.d.ts +5 -3
- package/dist/ui/state.js +3 -3
- package/dist/ui/terminal.d.ts +6 -0
- package/dist/ui/terminal.js +16 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/braeburn)
|
|
4
4
|
|
|
5
|
-
A macOS system updater CLI.
|
|
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
|
-
|
|
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,12 +1,23 @@
|
|
|
1
|
+
import { type BraeburnConfig } from "../config.js";
|
|
1
2
|
import type { Step } from "../steps/index.js";
|
|
2
3
|
type RunConfigCommandOptions = {
|
|
3
4
|
allSteps: Step[];
|
|
4
5
|
};
|
|
6
|
+
type DesiredState = "enable" | "disable";
|
|
5
7
|
type RunConfigUpdateCommandOptions = {
|
|
6
|
-
|
|
7
|
-
logoUpdate: boolean | undefined;
|
|
8
|
+
settingUpdates: Record<string, DesiredState>;
|
|
8
9
|
allSteps: Step[];
|
|
9
10
|
};
|
|
10
11
|
export declare function runConfigCommand(options: RunConfigCommandOptions): Promise<void>;
|
|
12
|
+
type ConfigChange = {
|
|
13
|
+
label: string;
|
|
14
|
+
from: DesiredState;
|
|
15
|
+
to: DesiredState;
|
|
16
|
+
};
|
|
17
|
+
type ConfigUpdateResult = {
|
|
18
|
+
updatedConfig: BraeburnConfig;
|
|
19
|
+
changes: ConfigChange[];
|
|
20
|
+
};
|
|
21
|
+
export declare function applyConfigUpdates(config: BraeburnConfig, settingUpdates: Record<string, DesiredState>): ConfigUpdateResult;
|
|
11
22
|
export declare function runConfigUpdateCommand(options: RunConfigUpdateCommandOptions): Promise<void>;
|
|
12
23
|
export {};
|
package/dist/commands/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { readConfig, writeConfig, resolveConfigPath, isStepEnabled, isLogoEnabled, 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();
|
|
@@ -29,10 +29,22 @@ export async function runConfigCommand(options) {
|
|
|
29
29
|
}
|
|
30
30
|
process.stdout.write(`\n`);
|
|
31
31
|
}
|
|
32
|
+
export function applyConfigUpdates(config, settingUpdates) {
|
|
33
|
+
let updatedConfig = config;
|
|
34
|
+
const changes = [];
|
|
35
|
+
for (const [settingId, desiredState] of Object.entries(settingUpdates)) {
|
|
36
|
+
const currentState = isSettingEnabled(config, settingId) ? "enable" : "disable";
|
|
37
|
+
if (currentState !== desiredState) {
|
|
38
|
+
changes.push({ label: settingId, from: currentState, to: desiredState });
|
|
39
|
+
}
|
|
40
|
+
updatedConfig = applySettingToConfig(updatedConfig, settingId, desiredState);
|
|
41
|
+
}
|
|
42
|
+
return { updatedConfig, changes };
|
|
43
|
+
}
|
|
32
44
|
export async function runConfigUpdateCommand(options) {
|
|
33
|
-
const {
|
|
34
|
-
if (Object.keys(
|
|
35
|
-
const configurableSteps = allSteps.filter((
|
|
45
|
+
const { settingUpdates, allSteps } = options;
|
|
46
|
+
if (Object.keys(settingUpdates).length === 0) {
|
|
47
|
+
const configurableSteps = allSteps.filter((step) => !PROTECTED_STEP_IDS.has(step.id));
|
|
36
48
|
process.stdout.write("No changes — pass flags to enable or disable steps:\n\n");
|
|
37
49
|
process.stdout.write(` ${"--no-logo".padEnd(18)} hide the logo\n`);
|
|
38
50
|
process.stdout.write(` ${"--logo".padEnd(18)} show the logo\n`);
|
|
@@ -48,41 +60,15 @@ export async function runConfigUpdateCommand(options) {
|
|
|
48
60
|
return;
|
|
49
61
|
}
|
|
50
62
|
const config = await readConfig();
|
|
51
|
-
const changes =
|
|
52
|
-
|
|
53
|
-
const currentlyEnabled = isLogoEnabled(config);
|
|
54
|
-
if (currentlyEnabled !== logoUpdate) {
|
|
55
|
-
changes.push({ label: "logo", from: currentlyEnabled, to: logoUpdate });
|
|
56
|
-
}
|
|
57
|
-
if (logoUpdate) {
|
|
58
|
-
delete config.logo;
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
config.logo = false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
for (const [stepId, newEnabled] of Object.entries(stepUpdates)) {
|
|
65
|
-
const currentlyEnabled = isStepEnabled(config, stepId);
|
|
66
|
-
if (currentlyEnabled !== newEnabled) {
|
|
67
|
-
changes.push({ label: stepId, from: currentlyEnabled, to: newEnabled });
|
|
68
|
-
}
|
|
69
|
-
if (newEnabled) {
|
|
70
|
-
// Re-enabling: remove from config so absent = enabled (keeps file minimal)
|
|
71
|
-
delete config.steps[stepId];
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
config.steps[stepId] = false;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// Write even if no visible changes, in case the user is re-confirming state
|
|
78
|
-
await writeCleanConfig(config);
|
|
63
|
+
const { updatedConfig, changes } = applyConfigUpdates(config, settingUpdates);
|
|
64
|
+
await writeCleanConfig(updatedConfig);
|
|
79
65
|
if (changes.length === 0) {
|
|
80
66
|
process.stdout.write("No changes — already set as requested.\n");
|
|
81
67
|
return;
|
|
82
68
|
}
|
|
83
69
|
for (const { label, from, to } of changes) {
|
|
84
|
-
const fromLabel = from ? chalk.green("enabled") : chalk.red("disabled");
|
|
85
|
-
const toLabel = to ? chalk.green("enabled") : chalk.red("disabled");
|
|
70
|
+
const fromLabel = from === "enable" ? chalk.green("enabled") : chalk.red("disabled");
|
|
71
|
+
const toLabel = to === "enable" ? chalk.green("enabled") : chalk.red("disabled");
|
|
86
72
|
process.stdout.write(` ${label.padEnd(12)} ${fromLabel} → ${toLabel}\n`);
|
|
87
73
|
}
|
|
88
74
|
const configPath = await resolveConfigPath();
|
|
@@ -90,10 +76,13 @@ export async function runConfigUpdateCommand(options) {
|
|
|
90
76
|
}
|
|
91
77
|
async function writeCleanConfig(config) {
|
|
92
78
|
const cleaned = { steps: {} };
|
|
93
|
-
for (const [stepId,
|
|
94
|
-
if (
|
|
79
|
+
for (const [stepId, value] of Object.entries(config.steps)) {
|
|
80
|
+
if (value === false && !DEFAULT_OFF_STEP_IDS.has(stepId)) {
|
|
95
81
|
cleaned.steps[stepId] = false;
|
|
96
82
|
}
|
|
83
|
+
else if (value === true && DEFAULT_OFF_STEP_IDS.has(stepId)) {
|
|
84
|
+
cleaned.steps[stepId] = true;
|
|
85
|
+
}
|
|
97
86
|
}
|
|
98
87
|
if (config.logo === false) {
|
|
99
88
|
cleaned.logo = false;
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import type { Step } from "../steps/index.js";
|
|
2
|
+
export type SelectionState = "selected" | "deselected";
|
|
3
|
+
export type ProtectionStatus = "protected" | "configurable";
|
|
4
|
+
export type AvailabilityStatus = "available" | "unavailable";
|
|
5
|
+
export type SelectableStep = {
|
|
6
|
+
step: Step;
|
|
7
|
+
selection: SelectionState;
|
|
8
|
+
protection: ProtectionStatus;
|
|
9
|
+
availability: AvailabilityStatus;
|
|
10
|
+
};
|
|
11
|
+
export declare function buildLoadingScreen(): string;
|
|
12
|
+
export declare function buildSetupScreen(items: SelectableStep[], cursorIndex: number): string;
|
|
2
13
|
export declare function runSetupCommand(allSteps: Step[]): Promise<void>;
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,16 +2,9 @@ import readline from "node:readline";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { writeConfig, PROTECTED_STEP_IDS } from "../config.js";
|
|
4
4
|
import { LOGO_ART } from "../logo.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function
|
|
8
|
-
if (prevLines > 0) {
|
|
9
|
-
process.stdout.write(`\x1b[${prevLines}A\x1b[J`);
|
|
10
|
-
}
|
|
11
|
-
process.stdout.write(content);
|
|
12
|
-
prevLines = (content.match(/\n/g) ?? []).length;
|
|
13
|
-
}
|
|
14
|
-
function buildLoadingScreen() {
|
|
5
|
+
import { createScreenRenderer } from "../ui/screen.js";
|
|
6
|
+
import { hideCursorDuringExecution } from "../ui/terminal.js";
|
|
7
|
+
export function buildLoadingScreen() {
|
|
15
8
|
const lines = [
|
|
16
9
|
chalk.yellow(LOGO_ART),
|
|
17
10
|
"",
|
|
@@ -22,7 +15,7 @@ function buildLoadingScreen() {
|
|
|
22
15
|
];
|
|
23
16
|
return lines.join("\n") + "\n";
|
|
24
17
|
}
|
|
25
|
-
function buildSetupScreen(items, cursorIndex) {
|
|
18
|
+
export function buildSetupScreen(items, cursorIndex) {
|
|
26
19
|
const lines = [
|
|
27
20
|
chalk.yellow(LOGO_ART),
|
|
28
21
|
"",
|
|
@@ -34,18 +27,31 @@ function buildSetupScreen(items, cursorIndex) {
|
|
|
34
27
|
` ${chalk.dim("\u2191\u2193 navigate Space toggle Return confirm")}`,
|
|
35
28
|
"",
|
|
36
29
|
];
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
34
|
+
const item = items[itemIndex];
|
|
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
|
+
}
|
|
40
46
|
const cursor = isCursor ? chalk.cyan("\u203a") : " ";
|
|
41
|
-
const checkbox = item.selected ? chalk.green("\u25cf") : chalk.dim("\u25cb");
|
|
47
|
+
const checkbox = item.selection === "selected" ? chalk.green("\u25cf") : chalk.dim("\u25cb");
|
|
42
48
|
const namePadded = item.step.name.padEnd(18);
|
|
43
49
|
const name = isCursor ? chalk.bold.white(namePadded) : chalk.white(namePadded);
|
|
44
50
|
let status;
|
|
45
|
-
if (item.
|
|
51
|
+
if (item.protection === "protected") {
|
|
46
52
|
status = chalk.dim("required");
|
|
47
53
|
}
|
|
48
|
-
else if (item.
|
|
54
|
+
else if (item.availability === "available") {
|
|
49
55
|
status = chalk.green("installed");
|
|
50
56
|
}
|
|
51
57
|
else if (item.step.brewPackageToInstall) {
|
|
@@ -59,39 +65,31 @@ function buildSetupScreen(items, cursorIndex) {
|
|
|
59
65
|
lines.push(` ${chalk.dim(item.step.description)}`);
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
|
-
const enabledCount = items.filter((
|
|
68
|
+
const enabledCount = items.filter((item) => item.selection === "selected").length;
|
|
63
69
|
lines.push("");
|
|
64
70
|
lines.push(` ${chalk.dim(`${enabledCount} of ${items.length} tools selected`)}`);
|
|
65
71
|
lines.push("");
|
|
66
72
|
return lines.join("\n") + "\n";
|
|
67
73
|
}
|
|
68
74
|
export async function runSetupCommand(allSteps) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
process.on("exit", () => process.stdout.write("\x1b[?25h"));
|
|
72
|
-
process.on("SIGINT", () => {
|
|
73
|
-
process.stdout.write("\x1b[?25h\n");
|
|
74
|
-
process.exit(130);
|
|
75
|
-
});
|
|
76
|
-
// Show loading screen while we check availability in parallel
|
|
75
|
+
const render = createScreenRenderer();
|
|
76
|
+
hideCursorDuringExecution();
|
|
77
77
|
render(buildLoadingScreen());
|
|
78
78
|
const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
|
|
79
|
-
const items = allSteps.map((step,
|
|
79
|
+
const items = allSteps.map((step, stepIndex) => ({
|
|
80
80
|
step,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
84
|
}));
|
|
85
85
|
let cursorIndex = 0;
|
|
86
86
|
render(buildSetupScreen(items, cursorIndex));
|
|
87
|
-
// Interactive selection loop
|
|
88
87
|
await new Promise((resolve) => {
|
|
89
88
|
readline.emitKeypressEvents(process.stdin);
|
|
90
89
|
if (process.stdin.isTTY)
|
|
91
90
|
process.stdin.setRawMode(true);
|
|
92
91
|
const handleKeypress = (_char, key) => {
|
|
93
92
|
if (key?.ctrl && key?.name === "c") {
|
|
94
|
-
process.stdout.write("\x1b[?25h\n");
|
|
95
93
|
process.exit(130);
|
|
96
94
|
}
|
|
97
95
|
if (key?.name === "up" || key?.name === "k") {
|
|
@@ -104,8 +102,8 @@ export async function runSetupCommand(allSteps) {
|
|
|
104
102
|
}
|
|
105
103
|
else if (key?.name === "space") {
|
|
106
104
|
const item = items[cursorIndex];
|
|
107
|
-
if (
|
|
108
|
-
item.
|
|
105
|
+
if (item.protection === "configurable") {
|
|
106
|
+
item.selection = item.selection === "selected" ? "deselected" : "selected";
|
|
109
107
|
render(buildSetupScreen(items, cursorIndex));
|
|
110
108
|
}
|
|
111
109
|
}
|
|
@@ -120,25 +118,19 @@ export async function runSetupCommand(allSteps) {
|
|
|
120
118
|
process.stdin.on("keypress", handleKeypress);
|
|
121
119
|
process.stdin.resume();
|
|
122
120
|
});
|
|
123
|
-
// Restore cursor
|
|
124
|
-
process.stdout.write("\x1b[?25h");
|
|
125
|
-
// Persist choices — only write explicit false entries (keeps file minimal, matches
|
|
126
|
-
// the opt-out convention used everywhere else in the codebase)
|
|
127
121
|
const stepsConfig = {};
|
|
128
122
|
for (const item of items) {
|
|
129
|
-
if (
|
|
123
|
+
if (item.protection === "configurable" && item.selection === "deselected") {
|
|
130
124
|
stepsConfig[item.step.id] = false;
|
|
131
125
|
}
|
|
132
126
|
}
|
|
133
127
|
await writeConfig({ steps: stepsConfig });
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// Small pause so the confirmation is readable before the update screen takes over
|
|
143
|
-
await new Promise((res) => setTimeout(res, 800));
|
|
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));
|
|
144
136
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Step } from "../steps/index.js";
|
|
2
|
+
type PromptMode = "interactive" | "auto-accept";
|
|
3
|
+
type LogoVisibility = "visible" | "hidden";
|
|
2
4
|
type RunUpdateCommandOptions = {
|
|
3
5
|
steps: Step[];
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
promptMode: PromptMode;
|
|
7
|
+
logoVisibility: LogoVisibility;
|
|
6
8
|
version: string;
|
|
7
9
|
};
|
|
8
10
|
export declare function runUpdateCommand(options: RunUpdateCommandOptions): Promise<void>;
|
package/dist/commands/update.js
CHANGED
|
@@ -3,21 +3,19 @@ import { createLogWriterForStep } from "../logger.js";
|
|
|
3
3
|
import { collectVersions } from "../ui/versionReport.js";
|
|
4
4
|
import { captureYesNo } from "../ui/prompt.js";
|
|
5
5
|
import { createInitialAppState } from "../ui/state.js";
|
|
6
|
-
import { buildScreen,
|
|
6
|
+
import { buildScreen, createScreenRenderer } from "../ui/screen.js";
|
|
7
|
+
import { hideCursorDuringExecution } from "../ui/terminal.js";
|
|
8
|
+
import { createDefaultStepRunContext } from "../steps/index.js";
|
|
7
9
|
export async function runUpdateCommand(options) {
|
|
8
10
|
const { steps, version } = options;
|
|
9
|
-
let
|
|
10
|
-
const state = createInitialAppState(steps, version, options.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
process.on("SIGINT", () => {
|
|
14
|
-
process.stdout.write("\x1b[?25h\n");
|
|
15
|
-
process.exit(130);
|
|
16
|
-
});
|
|
11
|
+
let autoAccept = options.promptMode === "auto-accept";
|
|
12
|
+
const state = createInitialAppState(steps, version, options.logoVisibility);
|
|
13
|
+
const renderScreen = createScreenRenderer();
|
|
14
|
+
hideCursorDuringExecution();
|
|
17
15
|
renderScreen(buildScreen(state));
|
|
18
|
-
for (let
|
|
19
|
-
const step = steps[
|
|
20
|
-
state.currentStepIndex =
|
|
16
|
+
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
17
|
+
const step = steps[stepIndex];
|
|
18
|
+
state.currentStepIndex = stepIndex;
|
|
21
19
|
state.currentPhase = "checking-availability";
|
|
22
20
|
state.currentOutputLines = [];
|
|
23
21
|
state.currentPrompt = undefined;
|
|
@@ -35,9 +33,9 @@ export async function runUpdateCommand(options) {
|
|
|
35
33
|
question: `Install ${step.name} via Homebrew? (brew install ${step.brewPackageToInstall})`,
|
|
36
34
|
};
|
|
37
35
|
renderScreen(buildScreen(state));
|
|
38
|
-
const installAnswer =
|
|
36
|
+
const installAnswer = autoAccept ? "yes" : await captureYesNo();
|
|
39
37
|
if (installAnswer === "force")
|
|
40
|
-
|
|
38
|
+
autoAccept = true;
|
|
41
39
|
const shouldInstall = installAnswer !== "no";
|
|
42
40
|
state.currentPrompt = undefined;
|
|
43
41
|
if (!shouldInstall) {
|
|
@@ -66,15 +64,12 @@ export async function runUpdateCommand(options) {
|
|
|
66
64
|
continue;
|
|
67
65
|
}
|
|
68
66
|
}
|
|
69
|
-
const pipWarning = step.id === "pip"
|
|
70
|
-
? "This updates all global pip3 packages, which can occasionally break tools."
|
|
71
|
-
: undefined;
|
|
72
67
|
state.currentPhase = "prompting-to-run";
|
|
73
|
-
state.currentPrompt = { question: `Run ${step.name} update?`, warning:
|
|
68
|
+
state.currentPrompt = { question: `Run ${step.name} update?`, warning: step.warning };
|
|
74
69
|
renderScreen(buildScreen(state));
|
|
75
|
-
const runAnswer =
|
|
70
|
+
const runAnswer = autoAccept ? "yes" : await captureYesNo();
|
|
76
71
|
if (runAnswer === "force")
|
|
77
|
-
|
|
72
|
+
autoAccept = true;
|
|
78
73
|
const shouldRun = runAnswer !== "no";
|
|
79
74
|
state.currentPrompt = undefined;
|
|
80
75
|
if (!shouldRun) {
|
|
@@ -88,13 +83,10 @@ export async function runUpdateCommand(options) {
|
|
|
88
83
|
renderScreen(buildScreen(state));
|
|
89
84
|
const stepLogWriter = await createLogWriterForStep(step.id);
|
|
90
85
|
try {
|
|
91
|
-
await step.run({
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
96
|
-
logWriter: stepLogWriter,
|
|
97
|
-
});
|
|
86
|
+
await step.run(createDefaultStepRunContext((line) => {
|
|
87
|
+
state.currentOutputLines.push(line);
|
|
88
|
+
renderScreen(buildScreen(state));
|
|
89
|
+
}, stepLogWriter));
|
|
98
90
|
state.currentPhase = "complete";
|
|
99
91
|
state.currentOutputLines = [];
|
|
100
92
|
renderScreen(buildScreen(state));
|
|
@@ -108,7 +100,7 @@ export async function runUpdateCommand(options) {
|
|
|
108
100
|
state.completedStepRecords.push({ phase: "failed", summaryNote: errorMessage });
|
|
109
101
|
}
|
|
110
102
|
}
|
|
111
|
-
state.
|
|
103
|
+
state.runCompletion = "finished";
|
|
112
104
|
state.currentOutputLines = [];
|
|
113
105
|
state.currentPrompt = undefined;
|
|
114
106
|
renderScreen(buildScreen(state));
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** Steps that cannot be disabled — brew is a hard runtime dependency. */
|
|
2
1
|
export declare const PROTECTED_STEP_IDS: Set<string>;
|
|
2
|
+
export declare const DEFAULT_OFF_STEP_IDS: Set<string>;
|
|
3
3
|
export type BraeburnConfig = {
|
|
4
4
|
steps: Record<string, boolean>;
|
|
5
5
|
logo?: boolean;
|
|
@@ -8,5 +8,7 @@ export declare function resolveConfigPath(): Promise<string>;
|
|
|
8
8
|
export declare function configFileExists(): Promise<boolean>;
|
|
9
9
|
export declare function readConfig(): Promise<BraeburnConfig>;
|
|
10
10
|
export declare function writeConfig(config: BraeburnConfig): Promise<void>;
|
|
11
|
+
export declare function isSettingEnabled(config: BraeburnConfig, settingId: string): boolean;
|
|
11
12
|
export declare function isStepEnabled(config: BraeburnConfig, stepId: string): boolean;
|
|
12
13
|
export declare function isLogoEnabled(config: BraeburnConfig): boolean;
|
|
14
|
+
export declare function applySettingToConfig(config: BraeburnConfig, settingId: string, desiredState: "enable" | "disable"): BraeburnConfig;
|
package/dist/config.js
CHANGED
|
@@ -2,12 +2,13 @@ import { readFile, writeFile, mkdir, access } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { parse, stringify } from "smol-toml";
|
|
5
|
-
/** Steps that cannot be disabled — brew is a hard runtime dependency. */
|
|
6
5
|
export const PROTECTED_STEP_IDS = new Set(["homebrew"]);
|
|
6
|
+
export const DEFAULT_OFF_STEP_IDS = new Set(["nvm", "pyenv"]);
|
|
7
7
|
const EMPTY_CONFIG = { steps: {} };
|
|
8
|
-
|
|
8
|
+
const LOGO_SETTING_ID = "logo";
|
|
9
|
+
async function pathExists(targetPath) {
|
|
9
10
|
try {
|
|
10
|
-
await access(
|
|
11
|
+
await access(targetPath);
|
|
11
12
|
return true;
|
|
12
13
|
}
|
|
13
14
|
catch {
|
|
@@ -41,13 +42,46 @@ export async function writeConfig(config) {
|
|
|
41
42
|
await mkdir(join(configPath, ".."), { recursive: true });
|
|
42
43
|
await writeFile(configPath, stringify(config), "utf-8");
|
|
43
44
|
}
|
|
44
|
-
export function
|
|
45
|
-
if (PROTECTED_STEP_IDS.has(
|
|
45
|
+
export function isSettingEnabled(config, settingId) {
|
|
46
|
+
if (PROTECTED_STEP_IDS.has(settingId))
|
|
46
47
|
return true;
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
if (settingId === LOGO_SETTING_ID)
|
|
49
|
+
return config.logo !== false;
|
|
50
|
+
if (DEFAULT_OFF_STEP_IDS.has(settingId))
|
|
51
|
+
return config.steps[settingId] === true;
|
|
52
|
+
return config.steps[settingId] !== false;
|
|
53
|
+
}
|
|
54
|
+
export function isStepEnabled(config, stepId) {
|
|
55
|
+
return isSettingEnabled(config, stepId);
|
|
49
56
|
}
|
|
50
57
|
export function isLogoEnabled(config) {
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
return isSettingEnabled(config, LOGO_SETTING_ID);
|
|
59
|
+
}
|
|
60
|
+
export function applySettingToConfig(config, settingId, desiredState) {
|
|
61
|
+
const updatedConfig = structuredClone(config);
|
|
62
|
+
if (settingId === LOGO_SETTING_ID) {
|
|
63
|
+
if (desiredState === "enable") {
|
|
64
|
+
delete updatedConfig.logo;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
updatedConfig.logo = false;
|
|
68
|
+
}
|
|
69
|
+
return updatedConfig;
|
|
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
|
+
}
|
|
80
|
+
if (desiredState === "enable") {
|
|
81
|
+
delete updatedConfig.steps[settingId];
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
updatedConfig.steps[settingId] = false;
|
|
85
|
+
}
|
|
86
|
+
return updatedConfig;
|
|
53
87
|
}
|
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,
|
|
@@ -34,33 +34,36 @@ program
|
|
|
34
34
|
program
|
|
35
35
|
.command("update", { isDefault: true })
|
|
36
36
|
.description("Run system update steps (default command)")
|
|
37
|
-
.argument("[steps...]", `Steps to run — omit to run all.\nAvailable: ${ALL_STEPS.map((
|
|
37
|
+
.argument("[steps...]", `Steps to run — omit to run all.\nAvailable: ${ALL_STEPS.map((step) => step.id).join(", ")}`)
|
|
38
38
|
.option("-y, --yes", "Auto-accept all prompts (default yes to everything)")
|
|
39
39
|
.option("-f, --force", "Alias for --yes")
|
|
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;
|
|
63
|
-
// First-run: if no config file exists yet, show the setup wizard.
|
|
64
67
|
if (!(await configFileExists())) {
|
|
65
68
|
await runSetupCommand(ALL_STEPS);
|
|
66
69
|
}
|
|
@@ -68,17 +71,14 @@ Examples:
|
|
|
68
71
|
let stepsToRun = stepArguments.length === 0
|
|
69
72
|
? ALL_STEPS
|
|
70
73
|
: resolveStepsByIds(stepArguments);
|
|
71
|
-
// When no explicit steps are requested, filter out steps disabled in config.
|
|
72
|
-
// Explicit step arguments always bypass config (user knows what they want).
|
|
73
74
|
if (stepArguments.length === 0) {
|
|
74
75
|
stepsToRun = stepsToRun.filter((step) => isStepEnabled(config, step.id));
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
-
const showLogo = options.logo !== false && isLogoEnabled(config);
|
|
77
|
+
const logoIsEnabled = options.logo !== false && isLogoEnabled(config);
|
|
78
78
|
await runUpdateCommand({
|
|
79
79
|
steps: stepsToRun,
|
|
80
|
-
autoYes,
|
|
81
|
-
|
|
80
|
+
promptMode: autoYes ? "auto-accept" : "interactive",
|
|
81
|
+
logoVisibility: logoIsEnabled ? "visible" : "hidden",
|
|
82
82
|
version: BRAEBURN_VERSION,
|
|
83
83
|
});
|
|
84
84
|
});
|
|
@@ -118,7 +118,7 @@ const configCommand = program
|
|
|
118
118
|
.action(async () => {
|
|
119
119
|
await runConfigCommand({ allSteps: ALL_STEPS });
|
|
120
120
|
});
|
|
121
|
-
const configurableSteps = ALL_STEPS.filter((
|
|
121
|
+
const configurableSteps = ALL_STEPS.filter((step) => !PROTECTED_STEP_IDS.has(step.id));
|
|
122
122
|
const configUpdateCommand = configCommand
|
|
123
123
|
.command("update")
|
|
124
124
|
.description("Enable or disable individual update steps")
|
|
@@ -136,20 +136,19 @@ for (const step of configurableSteps) {
|
|
|
136
136
|
configUpdateCommand.option(`--${step.id}`, `Enable ${step.name} updates`);
|
|
137
137
|
}
|
|
138
138
|
configUpdateCommand.action(function () {
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
const stepUpdates = {};
|
|
139
|
+
// Commander defaults --no-* to true, so we use getOptionValueSource to detect explicit CLI flags.
|
|
140
|
+
const settingUpdates = {};
|
|
142
141
|
for (const step of configurableSteps) {
|
|
143
142
|
const source = configUpdateCommand.getOptionValueSource(step.id);
|
|
144
143
|
if (source === "cli") {
|
|
145
|
-
|
|
144
|
+
settingUpdates[step.id] = configUpdateCommand.opts()[step.id] ? "enable" : "disable";
|
|
146
145
|
}
|
|
147
146
|
}
|
|
148
147
|
const logoSource = configUpdateCommand.getOptionValueSource("logo");
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
runConfigUpdateCommand({
|
|
148
|
+
if (logoSource === "cli") {
|
|
149
|
+
settingUpdates["logo"] = configUpdateCommand.opts().logo ? "enable" : "disable";
|
|
150
|
+
}
|
|
151
|
+
runConfigUpdateCommand({ settingUpdates, allSteps: ALL_STEPS });
|
|
153
152
|
});
|
|
154
153
|
function resolveStepsByIds(stepIds) {
|
|
155
154
|
const resolvedSteps = [];
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type StepLogWriter = (line: string) => Promise<void>;
|
|
2
|
-
export declare function createLogWriterForStep(stepId: string): Promise<StepLogWriter>;
|
|
3
|
-
export declare function findLatestLogFileForStep(stepId: string): string | null;
|
|
4
|
-
export declare function listAllStepIdsWithLogs(): string[];
|
|
2
|
+
export declare function createLogWriterForStep(stepId: string, logDirectory?: string): Promise<StepLogWriter>;
|
|
3
|
+
export declare function findLatestLogFileForStep(stepId: string, logDirectory?: string): string | null;
|
|
4
|
+
export declare function listAllStepIdsWithLogs(logDirectory?: string): string[];
|