braeburn 1.0.0 → 1.1.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 +39 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +144 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/index.js +6 -1
- package/package.json +5 -4
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# braeburn
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/braeburn)
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g braeburn
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
braeburn # run all enabled steps interactively
|
|
17
|
+
braeburn -y # run all steps, auto-accept everything
|
|
18
|
+
braeburn homebrew npm # run specific steps only
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
| Command | Description |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `braeburn [steps...] [-y]` | Run update steps (default) |
|
|
26
|
+
| `braeburn log [step]` | View the latest output log for a step |
|
|
27
|
+
| `braeburn config` | View current configuration |
|
|
28
|
+
| `braeburn config update --no-<step>` | Disable a step |
|
|
29
|
+
| `braeburn config update --<step>` | Re-enable a step |
|
|
30
|
+
|
|
31
|
+
## Steps
|
|
32
|
+
|
|
33
|
+
`homebrew` `mas` `ohmyzsh` `npm` `pip` `pyenv` `nvm` `dotnet` `macos` `cleanup`
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- macOS
|
|
38
|
+
- Node.js ≥ 24
|
|
39
|
+
- [Homebrew](https://brew.sh) (required)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { writeConfig, PROTECTED_STEP_IDS } from "../config.js";
|
|
4
|
+
import { LOGO_ART } from "../logo.js";
|
|
5
|
+
// Module-level render state (scoped to this screen, separate from update screen)
|
|
6
|
+
let prevLines = 0;
|
|
7
|
+
function render(content) {
|
|
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() {
|
|
15
|
+
const lines = [
|
|
16
|
+
chalk.yellow(LOGO_ART),
|
|
17
|
+
"",
|
|
18
|
+
` ${chalk.bold.white("Welcome to braeburn!")}`,
|
|
19
|
+
"",
|
|
20
|
+
` ${chalk.dim("Checking which tools are installed\u2026")}`,
|
|
21
|
+
"",
|
|
22
|
+
];
|
|
23
|
+
return lines.join("\n") + "\n";
|
|
24
|
+
}
|
|
25
|
+
function buildSetupScreen(items, cursorIndex) {
|
|
26
|
+
const lines = [
|
|
27
|
+
chalk.yellow(LOGO_ART),
|
|
28
|
+
"",
|
|
29
|
+
` ${chalk.bold.white("Welcome to braeburn!")}`,
|
|
30
|
+
"",
|
|
31
|
+
` Select the update tools you\u2019d like to enable. For anything that isn\u2019t`,
|
|
32
|
+
` installed yet, braeburn will offer to set it up via Homebrew when you run it.`,
|
|
33
|
+
"",
|
|
34
|
+
` ${chalk.dim("\u2191\u2193 navigate Space toggle Return confirm")}`,
|
|
35
|
+
"",
|
|
36
|
+
];
|
|
37
|
+
for (let i = 0; i < items.length; i++) {
|
|
38
|
+
const item = items[i];
|
|
39
|
+
const isCursor = i === cursorIndex;
|
|
40
|
+
const cursor = isCursor ? chalk.cyan("\u203a") : " ";
|
|
41
|
+
const checkbox = item.selected ? chalk.green("\u25cf") : chalk.dim("\u25cb");
|
|
42
|
+
const namePadded = item.step.name.padEnd(18);
|
|
43
|
+
const name = isCursor ? chalk.bold.white(namePadded) : chalk.white(namePadded);
|
|
44
|
+
let status;
|
|
45
|
+
if (item.isProtected) {
|
|
46
|
+
status = chalk.dim("required");
|
|
47
|
+
}
|
|
48
|
+
else if (item.isAvailable) {
|
|
49
|
+
status = chalk.green("installed");
|
|
50
|
+
}
|
|
51
|
+
else if (item.step.brewPackageToInstall) {
|
|
52
|
+
status = chalk.yellow(`not installed`) + chalk.dim(` \u2192 will offer to install via Homebrew`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
status = chalk.dim("not installed");
|
|
56
|
+
}
|
|
57
|
+
lines.push(` ${cursor} ${checkbox} ${name} ${status}`);
|
|
58
|
+
if (isCursor) {
|
|
59
|
+
lines.push(` ${chalk.dim(item.step.description)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const enabledCount = items.filter((i) => i.selected).length;
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push(` ${chalk.dim(`${enabledCount} of ${items.length} tools selected`)}`);
|
|
65
|
+
lines.push("");
|
|
66
|
+
return lines.join("\n") + "\n";
|
|
67
|
+
}
|
|
68
|
+
export async function runSetupCommand(allSteps) {
|
|
69
|
+
// Hide cursor; restore on exit
|
|
70
|
+
process.stdout.write("\x1b[?25l");
|
|
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
|
|
77
|
+
render(buildLoadingScreen());
|
|
78
|
+
const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
|
|
79
|
+
const items = allSteps.map((step, i) => ({
|
|
80
|
+
step,
|
|
81
|
+
selected: true, // all enabled by default — user opts out
|
|
82
|
+
isProtected: PROTECTED_STEP_IDS.has(step.id),
|
|
83
|
+
isAvailable: availabilityResults[i],
|
|
84
|
+
}));
|
|
85
|
+
let cursorIndex = 0;
|
|
86
|
+
render(buildSetupScreen(items, cursorIndex));
|
|
87
|
+
// Interactive selection loop
|
|
88
|
+
await new Promise((resolve) => {
|
|
89
|
+
readline.emitKeypressEvents(process.stdin);
|
|
90
|
+
if (process.stdin.isTTY)
|
|
91
|
+
process.stdin.setRawMode(true);
|
|
92
|
+
const handleKeypress = (_char, key) => {
|
|
93
|
+
if (key?.ctrl && key?.name === "c") {
|
|
94
|
+
process.stdout.write("\x1b[?25h\n");
|
|
95
|
+
process.exit(130);
|
|
96
|
+
}
|
|
97
|
+
if (key?.name === "up" || key?.name === "k") {
|
|
98
|
+
cursorIndex = Math.max(0, cursorIndex - 1);
|
|
99
|
+
render(buildSetupScreen(items, cursorIndex));
|
|
100
|
+
}
|
|
101
|
+
else if (key?.name === "down" || key?.name === "j") {
|
|
102
|
+
cursorIndex = Math.min(items.length - 1, cursorIndex + 1);
|
|
103
|
+
render(buildSetupScreen(items, cursorIndex));
|
|
104
|
+
}
|
|
105
|
+
else if (key?.name === "space") {
|
|
106
|
+
const item = items[cursorIndex];
|
|
107
|
+
if (!item.isProtected) {
|
|
108
|
+
item.selected = !item.selected;
|
|
109
|
+
render(buildSetupScreen(items, cursorIndex));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (key?.name === "return") {
|
|
113
|
+
process.stdin.removeListener("keypress", handleKeypress);
|
|
114
|
+
if (process.stdin.isTTY)
|
|
115
|
+
process.stdin.setRawMode(false);
|
|
116
|
+
process.stdin.pause();
|
|
117
|
+
resolve();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
process.stdin.on("keypress", handleKeypress);
|
|
121
|
+
process.stdin.resume();
|
|
122
|
+
});
|
|
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
|
+
const stepsConfig = {};
|
|
128
|
+
for (const item of items) {
|
|
129
|
+
if (!item.isProtected && !item.selected) {
|
|
130
|
+
stepsConfig[item.step.id] = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await writeConfig({ steps: stepsConfig });
|
|
134
|
+
// Clear the setup screen and print a brief confirmation before the update starts
|
|
135
|
+
if (prevLines > 0) {
|
|
136
|
+
process.stdout.write(`\x1b[${prevLines}A\x1b[J`);
|
|
137
|
+
}
|
|
138
|
+
process.stdout.write(chalk.yellow(LOGO_ART) + "\n");
|
|
139
|
+
process.stdout.write("\n");
|
|
140
|
+
process.stdout.write(` ${chalk.green("\u2713")} Setup complete! Starting your first update\u2026\n`);
|
|
141
|
+
process.stdout.write("\n");
|
|
142
|
+
// Small pause so the confirmation is readable before the update screen takes over
|
|
143
|
+
await new Promise((res) => setTimeout(res, 800));
|
|
144
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type BraeburnConfig = {
|
|
|
4
4
|
steps: Record<string, boolean>;
|
|
5
5
|
};
|
|
6
6
|
export declare function resolveConfigPath(): Promise<string>;
|
|
7
|
+
export declare function configFileExists(): Promise<boolean>;
|
|
7
8
|
export declare function readConfig(): Promise<BraeburnConfig>;
|
|
8
9
|
export declare function writeConfig(config: BraeburnConfig): Promise<void>;
|
|
9
10
|
export declare function isStepEnabled(config: BraeburnConfig, stepId: string): boolean;
|
package/dist/config.js
CHANGED
|
@@ -21,6 +21,10 @@ export async function resolveConfigPath() {
|
|
|
21
21
|
}
|
|
22
22
|
return join(homedir(), ".braeburn", "config");
|
|
23
23
|
}
|
|
24
|
+
export async function configFileExists() {
|
|
25
|
+
const configPath = await resolveConfigPath();
|
|
26
|
+
return pathExists(configPath);
|
|
27
|
+
}
|
|
24
28
|
export async function readConfig() {
|
|
25
29
|
const configPath = await resolveConfigPath();
|
|
26
30
|
try {
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,8 @@ import { homebrewStep, masStep, ohmyzshStep, npmStep, pipStep, pyenvStep, nvmSte
|
|
|
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
|
-
import {
|
|
10
|
+
import { runSetupCommand } from "./commands/setup.js";
|
|
11
|
+
import { readConfig, isStepEnabled, PROTECTED_STEP_IDS, configFileExists } from "./config.js";
|
|
11
12
|
const ALL_STEPS = [
|
|
12
13
|
homebrewStep,
|
|
13
14
|
masStep,
|
|
@@ -58,6 +59,10 @@ Examples:
|
|
|
58
59
|
`)
|
|
59
60
|
.action(async (stepArguments, options) => {
|
|
60
61
|
const autoYes = options.yes === true || options.force === true;
|
|
62
|
+
// First-run: if no config file exists yet, show the setup wizard.
|
|
63
|
+
if (!(await configFileExists())) {
|
|
64
|
+
await runSetupCommand(ALL_STEPS);
|
|
65
|
+
}
|
|
61
66
|
let stepsToRun = stepArguments.length === 0
|
|
62
67
|
? ALL_STEPS
|
|
63
68
|
: resolveStepsByIds(stepArguments);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braeburn",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "macOS system updater CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,22 +10,23 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
13
|
+
"node": ">=24"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc",
|
|
17
|
+
"postbuild": "chmod +x dist/index.js",
|
|
17
18
|
"dev": "tsx src/index.ts",
|
|
18
19
|
"start": "node dist/index.js",
|
|
19
20
|
"prepublishOnly": "npm run build"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"chalk": "^5.3.0",
|
|
23
|
-
"commander": "^
|
|
24
|
+
"commander": "^14.0.0",
|
|
24
25
|
"execa": "^9.3.0",
|
|
25
26
|
"smol-toml": "^1.6.0"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^
|
|
29
|
+
"@types/node": "^24.0.0",
|
|
29
30
|
"tsx": "^4.7.0",
|
|
30
31
|
"typescript": "^5.5.0"
|
|
31
32
|
}
|