braeburn 1.2.1 → 1.2.3
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/dist/commands/config.d.ts +13 -2
- package/dist/commands/config.js +20 -34
- package/dist/commands/setup.d.ts +11 -0
- package/dist/commands/setup.js +28 -49
- package/dist/commands/update.d.ts +4 -2
- package/dist/commands/update.js +20 -28
- package/dist/config.d.ts +2 -1
- package/dist/config.js +31 -9
- package/dist/index.js +12 -17
- 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 +2 -2
- package/dist/steps/dotnet.js +2 -3
- package/dist/steps/homebrew.js +2 -2
- package/dist/steps/index.d.ts +6 -0
- package/dist/steps/index.js +10 -1
- package/dist/steps/macos.js +5 -8
- package/dist/steps/mas.js +2 -2
- package/dist/steps/npm.js +2 -3
- package/dist/steps/nvm.js +2 -3
- package/dist/steps/ohmyzsh.js +2 -3
- package/dist/steps/pip.js +3 -5
- package/dist/steps/pyenv.js +5 -8
- package/dist/ui/header.d.ts +9 -2
- package/dist/ui/header.js +33 -11
- 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
|
@@ -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, } 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();
|
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,18 @@ function buildSetupScreen(items, cursorIndex) {
|
|
|
34
27
|
` ${chalk.dim("\u2191\u2193 navigate Space toggle Return confirm")}`,
|
|
35
28
|
"",
|
|
36
29
|
];
|
|
37
|
-
for (let
|
|
38
|
-
const item = items[
|
|
39
|
-
const isCursor =
|
|
30
|
+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
31
|
+
const item = items[itemIndex];
|
|
32
|
+
const isCursor = itemIndex === cursorIndex;
|
|
40
33
|
const cursor = isCursor ? chalk.cyan("\u203a") : " ";
|
|
41
|
-
const checkbox = item.selected ? chalk.green("\u25cf") : chalk.dim("\u25cb");
|
|
34
|
+
const checkbox = item.selection === "selected" ? chalk.green("\u25cf") : chalk.dim("\u25cb");
|
|
42
35
|
const namePadded = item.step.name.padEnd(18);
|
|
43
36
|
const name = isCursor ? chalk.bold.white(namePadded) : chalk.white(namePadded);
|
|
44
37
|
let status;
|
|
45
|
-
if (item.
|
|
38
|
+
if (item.protection === "protected") {
|
|
46
39
|
status = chalk.dim("required");
|
|
47
40
|
}
|
|
48
|
-
else if (item.
|
|
41
|
+
else if (item.availability === "available") {
|
|
49
42
|
status = chalk.green("installed");
|
|
50
43
|
}
|
|
51
44
|
else if (item.step.brewPackageToInstall) {
|
|
@@ -59,39 +52,31 @@ function buildSetupScreen(items, cursorIndex) {
|
|
|
59
52
|
lines.push(` ${chalk.dim(item.step.description)}`);
|
|
60
53
|
}
|
|
61
54
|
}
|
|
62
|
-
const enabledCount = items.filter((
|
|
55
|
+
const enabledCount = items.filter((item) => item.selection === "selected").length;
|
|
63
56
|
lines.push("");
|
|
64
57
|
lines.push(` ${chalk.dim(`${enabledCount} of ${items.length} tools selected`)}`);
|
|
65
58
|
lines.push("");
|
|
66
59
|
return lines.join("\n") + "\n";
|
|
67
60
|
}
|
|
68
61
|
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
|
|
62
|
+
const render = createScreenRenderer();
|
|
63
|
+
hideCursorDuringExecution();
|
|
77
64
|
render(buildLoadingScreen());
|
|
78
65
|
const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
|
|
79
|
-
const items = allSteps.map((step,
|
|
66
|
+
const items = allSteps.map((step, stepIndex) => ({
|
|
80
67
|
step,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
selection: "selected",
|
|
69
|
+
protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
|
|
70
|
+
availability: availabilityResults[stepIndex] ? "available" : "unavailable",
|
|
84
71
|
}));
|
|
85
72
|
let cursorIndex = 0;
|
|
86
73
|
render(buildSetupScreen(items, cursorIndex));
|
|
87
|
-
// Interactive selection loop
|
|
88
74
|
await new Promise((resolve) => {
|
|
89
75
|
readline.emitKeypressEvents(process.stdin);
|
|
90
76
|
if (process.stdin.isTTY)
|
|
91
77
|
process.stdin.setRawMode(true);
|
|
92
78
|
const handleKeypress = (_char, key) => {
|
|
93
79
|
if (key?.ctrl && key?.name === "c") {
|
|
94
|
-
process.stdout.write("\x1b[?25h\n");
|
|
95
80
|
process.exit(130);
|
|
96
81
|
}
|
|
97
82
|
if (key?.name === "up" || key?.name === "k") {
|
|
@@ -104,8 +89,8 @@ export async function runSetupCommand(allSteps) {
|
|
|
104
89
|
}
|
|
105
90
|
else if (key?.name === "space") {
|
|
106
91
|
const item = items[cursorIndex];
|
|
107
|
-
if (
|
|
108
|
-
item.
|
|
92
|
+
if (item.protection === "configurable") {
|
|
93
|
+
item.selection = item.selection === "selected" ? "deselected" : "selected";
|
|
109
94
|
render(buildSetupScreen(items, cursorIndex));
|
|
110
95
|
}
|
|
111
96
|
}
|
|
@@ -120,25 +105,19 @@ export async function runSetupCommand(allSteps) {
|
|
|
120
105
|
process.stdin.on("keypress", handleKeypress);
|
|
121
106
|
process.stdin.resume();
|
|
122
107
|
});
|
|
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
108
|
const stepsConfig = {};
|
|
128
109
|
for (const item of items) {
|
|
129
|
-
if (
|
|
110
|
+
if (item.protection === "configurable" && item.selection === "deselected") {
|
|
130
111
|
stepsConfig[item.step.id] = false;
|
|
131
112
|
}
|
|
132
113
|
}
|
|
133
114
|
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));
|
|
115
|
+
const confirmationLines = [
|
|
116
|
+
chalk.yellow(LOGO_ART),
|
|
117
|
+
"",
|
|
118
|
+
` ${chalk.green("\u2713")} Setup complete! Starting your first update\u2026`,
|
|
119
|
+
"",
|
|
120
|
+
];
|
|
121
|
+
render(confirmationLines.join("\n") + "\n");
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
144
123
|
}
|
|
@@ -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,4 +1,3 @@
|
|
|
1
|
-
/** Steps that cannot be disabled — brew is a hard runtime dependency. */
|
|
2
1
|
export declare const PROTECTED_STEP_IDS: Set<string>;
|
|
3
2
|
export type BraeburnConfig = {
|
|
4
3
|
steps: Record<string, boolean>;
|
|
@@ -8,5 +7,7 @@ export declare function resolveConfigPath(): Promise<string>;
|
|
|
8
7
|
export declare function configFileExists(): Promise<boolean>;
|
|
9
8
|
export declare function readConfig(): Promise<BraeburnConfig>;
|
|
10
9
|
export declare function writeConfig(config: BraeburnConfig): Promise<void>;
|
|
10
|
+
export declare function isSettingEnabled(config: BraeburnConfig, settingId: string): boolean;
|
|
11
11
|
export declare function isStepEnabled(config: BraeburnConfig, stepId: string): boolean;
|
|
12
12
|
export declare function isLogoEnabled(config: BraeburnConfig): boolean;
|
|
13
|
+
export declare function applySettingToConfig(config: BraeburnConfig, settingId: string, desiredState: "enable" | "disable"): BraeburnConfig;
|
package/dist/config.js
CHANGED
|
@@ -2,12 +2,12 @@ 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"]);
|
|
7
6
|
const EMPTY_CONFIG = { steps: {} };
|
|
8
|
-
|
|
7
|
+
const LOGO_SETTING_ID = "logo";
|
|
8
|
+
async function pathExists(targetPath) {
|
|
9
9
|
try {
|
|
10
|
-
await access(
|
|
10
|
+
await access(targetPath);
|
|
11
11
|
return true;
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
@@ -41,13 +41,35 @@ export async function writeConfig(config) {
|
|
|
41
41
|
await mkdir(join(configPath, ".."), { recursive: true });
|
|
42
42
|
await writeFile(configPath, stringify(config), "utf-8");
|
|
43
43
|
}
|
|
44
|
-
export function
|
|
45
|
-
if (PROTECTED_STEP_IDS.has(
|
|
44
|
+
export function isSettingEnabled(config, settingId) {
|
|
45
|
+
if (PROTECTED_STEP_IDS.has(settingId))
|
|
46
46
|
return true;
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
if (settingId === LOGO_SETTING_ID)
|
|
48
|
+
return config.logo !== false;
|
|
49
|
+
return config.steps[settingId] !== false;
|
|
50
|
+
}
|
|
51
|
+
export function isStepEnabled(config, stepId) {
|
|
52
|
+
return isSettingEnabled(config, stepId);
|
|
49
53
|
}
|
|
50
54
|
export function isLogoEnabled(config) {
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
return isSettingEnabled(config, LOGO_SETTING_ID);
|
|
56
|
+
}
|
|
57
|
+
export function applySettingToConfig(config, settingId, desiredState) {
|
|
58
|
+
const updatedConfig = structuredClone(config);
|
|
59
|
+
if (settingId === LOGO_SETTING_ID) {
|
|
60
|
+
if (desiredState === "enable") {
|
|
61
|
+
delete updatedConfig.logo;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
updatedConfig.logo = false;
|
|
65
|
+
}
|
|
66
|
+
return updatedConfig;
|
|
67
|
+
}
|
|
68
|
+
if (desiredState === "enable") {
|
|
69
|
+
delete updatedConfig.steps[settingId];
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
updatedConfig.steps[settingId] = false;
|
|
73
|
+
}
|
|
74
|
+
return updatedConfig;
|
|
53
75
|
}
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ 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")
|
|
@@ -60,7 +60,6 @@ Examples:
|
|
|
60
60
|
`)
|
|
61
61
|
.action(async (stepArguments, options) => {
|
|
62
62
|
const autoYes = options.yes === true || options.force === true;
|
|
63
|
-
// First-run: if no config file exists yet, show the setup wizard.
|
|
64
63
|
if (!(await configFileExists())) {
|
|
65
64
|
await runSetupCommand(ALL_STEPS);
|
|
66
65
|
}
|
|
@@ -68,17 +67,14 @@ Examples:
|
|
|
68
67
|
let stepsToRun = stepArguments.length === 0
|
|
69
68
|
? ALL_STEPS
|
|
70
69
|
: 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
70
|
if (stepArguments.length === 0) {
|
|
74
71
|
stepsToRun = stepsToRun.filter((step) => isStepEnabled(config, step.id));
|
|
75
72
|
}
|
|
76
|
-
|
|
77
|
-
const showLogo = options.logo !== false && isLogoEnabled(config);
|
|
73
|
+
const logoIsEnabled = options.logo !== false && isLogoEnabled(config);
|
|
78
74
|
await runUpdateCommand({
|
|
79
75
|
steps: stepsToRun,
|
|
80
|
-
autoYes,
|
|
81
|
-
|
|
76
|
+
promptMode: autoYes ? "auto-accept" : "interactive",
|
|
77
|
+
logoVisibility: logoIsEnabled ? "visible" : "hidden",
|
|
82
78
|
version: BRAEBURN_VERSION,
|
|
83
79
|
});
|
|
84
80
|
});
|
|
@@ -118,7 +114,7 @@ const configCommand = program
|
|
|
118
114
|
.action(async () => {
|
|
119
115
|
await runConfigCommand({ allSteps: ALL_STEPS });
|
|
120
116
|
});
|
|
121
|
-
const configurableSteps = ALL_STEPS.filter((
|
|
117
|
+
const configurableSteps = ALL_STEPS.filter((step) => !PROTECTED_STEP_IDS.has(step.id));
|
|
122
118
|
const configUpdateCommand = configCommand
|
|
123
119
|
.command("update")
|
|
124
120
|
.description("Enable or disable individual update steps")
|
|
@@ -136,20 +132,19 @@ for (const step of configurableSteps) {
|
|
|
136
132
|
configUpdateCommand.option(`--${step.id}`, `Enable ${step.name} updates`);
|
|
137
133
|
}
|
|
138
134
|
configUpdateCommand.action(function () {
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
const stepUpdates = {};
|
|
135
|
+
// Commander defaults --no-* to true, so we use getOptionValueSource to detect explicit CLI flags.
|
|
136
|
+
const settingUpdates = {};
|
|
142
137
|
for (const step of configurableSteps) {
|
|
143
138
|
const source = configUpdateCommand.getOptionValueSource(step.id);
|
|
144
139
|
if (source === "cli") {
|
|
145
|
-
|
|
140
|
+
settingUpdates[step.id] = configUpdateCommand.opts()[step.id] ? "enable" : "disable";
|
|
146
141
|
}
|
|
147
142
|
}
|
|
148
143
|
const logoSource = configUpdateCommand.getOptionValueSource("logo");
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
runConfigUpdateCommand({
|
|
144
|
+
if (logoSource === "cli") {
|
|
145
|
+
settingUpdates["logo"] = configUpdateCommand.opts().logo ? "enable" : "disable";
|
|
146
|
+
}
|
|
147
|
+
runConfigUpdateCommand({ settingUpdates, allSteps: ALL_STEPS });
|
|
153
148
|
});
|
|
154
149
|
function resolveStepsByIds(stepIds) {
|
|
155
150
|
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[];
|
package/dist/logger.js
CHANGED
|
@@ -2,41 +2,41 @@ import { mkdir, appendFile } from "node:fs/promises";
|
|
|
2
2
|
import { existsSync, readdirSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
const
|
|
6
|
-
async function
|
|
7
|
-
if (existsSync(
|
|
5
|
+
const DEFAULT_LOG_DIRECTORY = join(homedir(), ".braeburn", "logs");
|
|
6
|
+
async function ensureDirectoryExists(directoryPath) {
|
|
7
|
+
if (existsSync(directoryPath)) {
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
|
-
await mkdir(
|
|
10
|
+
await mkdir(directoryPath, { recursive: true });
|
|
11
11
|
}
|
|
12
|
-
export async function createLogWriterForStep(stepId) {
|
|
13
|
-
await
|
|
12
|
+
export async function createLogWriterForStep(stepId, logDirectory = DEFAULT_LOG_DIRECTORY) {
|
|
13
|
+
await ensureDirectoryExists(logDirectory);
|
|
14
14
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
15
|
-
const logFilePath = join(
|
|
15
|
+
const logFilePath = join(logDirectory, `${stepId}-${timestamp}.log`);
|
|
16
16
|
const writeLineToLog = async (line) => {
|
|
17
17
|
await appendFile(logFilePath, line + "\n", "utf-8");
|
|
18
18
|
};
|
|
19
19
|
return writeLineToLog;
|
|
20
20
|
}
|
|
21
|
-
export function findLatestLogFileForStep(stepId) {
|
|
22
|
-
if (!existsSync(
|
|
21
|
+
export function findLatestLogFileForStep(stepId, logDirectory = DEFAULT_LOG_DIRECTORY) {
|
|
22
|
+
if (!existsSync(logDirectory)) {
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
|
-
const allFiles = readdirSync(
|
|
25
|
+
const allFiles = readdirSync(logDirectory);
|
|
26
26
|
const filesForThisStep = allFiles
|
|
27
27
|
.filter((fileName) => fileName.startsWith(`${stepId}-`))
|
|
28
28
|
.sort()
|
|
29
|
-
.reverse();
|
|
29
|
+
.reverse();
|
|
30
30
|
if (filesForThisStep.length === 0) {
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
|
-
return join(
|
|
33
|
+
return join(logDirectory, filesForThisStep[0]);
|
|
34
34
|
}
|
|
35
|
-
export function listAllStepIdsWithLogs() {
|
|
36
|
-
if (!existsSync(
|
|
35
|
+
export function listAllStepIdsWithLogs(logDirectory = DEFAULT_LOG_DIRECTORY) {
|
|
36
|
+
if (!existsSync(logDirectory)) {
|
|
37
37
|
return [];
|
|
38
38
|
}
|
|
39
|
-
const allFiles = readdirSync(
|
|
39
|
+
const allFiles = readdirSync(logDirectory);
|
|
40
40
|
const stepIds = new Set(allFiles
|
|
41
41
|
.map((fileName) => fileName.split("-")[0])
|
|
42
42
|
.filter((maybeStepId) => Boolean(maybeStepId)));
|
package/dist/logo.js
CHANGED
package/dist/runner.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { StepLogWriter } from "./logger.js";
|
|
2
|
+
export type OutputSource = "stdout" | "stderr";
|
|
2
3
|
export type CommandOutputLine = {
|
|
3
4
|
text: string;
|
|
4
|
-
|
|
5
|
+
source: OutputSource;
|
|
5
6
|
};
|
|
6
7
|
export type OutputLineCallback = (line: CommandOutputLine) => void;
|
|
7
8
|
type RunCommandOptions = {
|
package/dist/runner.js
CHANGED
|
@@ -7,14 +7,14 @@ export async function runShellCommand(options) {
|
|
|
7
7
|
subprocess.stdout?.on("data", (chunk) => {
|
|
8
8
|
const lines = String(chunk).split("\n").filter(Boolean);
|
|
9
9
|
for (const line of lines) {
|
|
10
|
-
options.onOutputLine({ text: line,
|
|
10
|
+
options.onOutputLine({ text: line, source: "stdout" });
|
|
11
11
|
options.logWriter(line);
|
|
12
12
|
}
|
|
13
13
|
});
|
|
14
14
|
subprocess.stderr?.on("data", (chunk) => {
|
|
15
15
|
const lines = String(chunk).split("\n").filter(Boolean);
|
|
16
16
|
for (const line of lines) {
|
|
17
|
-
options.onOutputLine({ text: line,
|
|
17
|
+
options.onOutputLine({ text: line, source: "stderr" });
|
|
18
18
|
options.logWriter(line);
|
|
19
19
|
}
|
|
20
20
|
});
|
package/dist/steps/cleanup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { checkCommandExists
|
|
1
|
+
import { checkCommandExists } from "./index.js";
|
|
2
2
|
const cleanupStep = {
|
|
3
3
|
id: "cleanup",
|
|
4
4
|
name: "Cleanup",
|
|
@@ -7,7 +7,7 @@ const cleanupStep = {
|
|
|
7
7
|
return checkCommandExists("brew");
|
|
8
8
|
},
|
|
9
9
|
async run(context) {
|
|
10
|
-
await runStep("brew cleanup"
|
|
10
|
+
await context.runStep("brew cleanup");
|
|
11
11
|
},
|
|
12
12
|
};
|
|
13
13
|
export default cleanupStep;
|
package/dist/steps/dotnet.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { checkCommandExists
|
|
1
|
+
import { checkCommandExists } from "./index.js";
|
|
2
2
|
const dotnetStep = {
|
|
3
3
|
id: "dotnet",
|
|
4
4
|
name: ".NET",
|
|
5
5
|
description: "Update all globally installed .NET tools",
|
|
6
|
-
// No brewPackageToInstall — .NET has its own installer
|
|
7
6
|
async checkIsAvailable() {
|
|
8
7
|
return checkCommandExists("dotnet");
|
|
9
8
|
},
|
|
10
9
|
async run(context) {
|
|
11
|
-
await runStep("dotnet tool update --global --all"
|
|
10
|
+
await context.runStep("dotnet tool update --global --all");
|
|
12
11
|
},
|
|
13
12
|
};
|
|
14
13
|
export default dotnetStep;
|
package/dist/steps/homebrew.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { checkCommandExists
|
|
1
|
+
import { checkCommandExists } from "./index.js";
|
|
2
2
|
const homebrewStep = {
|
|
3
3
|
id: "homebrew",
|
|
4
4
|
name: "Homebrew",
|
|
@@ -7,7 +7,7 @@ const homebrewStep = {
|
|
|
7
7
|
return checkCommandExists("brew");
|
|
8
8
|
},
|
|
9
9
|
async run(context) {
|
|
10
|
-
await runStep("brew update && brew upgrade"
|
|
10
|
+
await context.runStep("brew update && brew upgrade");
|
|
11
11
|
},
|
|
12
12
|
};
|
|
13
13
|
export default homebrewStep;
|
package/dist/steps/index.d.ts
CHANGED
|
@@ -3,11 +3,16 @@ import type { StepLogWriter } from "../logger.js";
|
|
|
3
3
|
export type StepRunContext = {
|
|
4
4
|
onOutputLine: OutputLineCallback;
|
|
5
5
|
logWriter: StepLogWriter;
|
|
6
|
+
runStep: (shellCommand: string) => Promise<void>;
|
|
7
|
+
captureOutput: (options: {
|
|
8
|
+
shellCommand: string;
|
|
9
|
+
}) => Promise<string>;
|
|
6
10
|
};
|
|
7
11
|
export type Step = {
|
|
8
12
|
id: string;
|
|
9
13
|
name: string;
|
|
10
14
|
description: string;
|
|
15
|
+
warning?: string;
|
|
11
16
|
brewPackageToInstall?: string;
|
|
12
17
|
checkIsAvailable: () => Promise<boolean>;
|
|
13
18
|
run: (context: StepRunContext) => Promise<void>;
|
|
@@ -25,3 +30,4 @@ export { default as cleanupStep } from "./cleanup.js";
|
|
|
25
30
|
export declare function checkCommandExists(command: string): Promise<boolean>;
|
|
26
31
|
export declare function checkPathExists(filePath: string): Promise<boolean>;
|
|
27
32
|
export declare function runStep(shellCommand: string, context: StepRunContext): Promise<void>;
|
|
33
|
+
export declare function createDefaultStepRunContext(onOutputLine: OutputLineCallback, logWriter: StepLogWriter): StepRunContext;
|
package/dist/steps/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { doesShellCommandSucceed, runShellCommand, } from "../runner.js";
|
|
1
|
+
import { doesShellCommandSucceed, runShellCommand, captureShellCommandOutput, } from "../runner.js";
|
|
2
2
|
export { default as homebrewStep } from "./homebrew.js";
|
|
3
3
|
export { default as masStep } from "./mas.js";
|
|
4
4
|
export { default as ohmyzshStep } from "./ohmyzsh.js";
|
|
@@ -22,3 +22,12 @@ export async function runStep(shellCommand, context) {
|
|
|
22
22
|
logWriter: context.logWriter,
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
+
export function createDefaultStepRunContext(onOutputLine, logWriter) {
|
|
26
|
+
const context = {
|
|
27
|
+
onOutputLine,
|
|
28
|
+
logWriter,
|
|
29
|
+
runStep: (shellCommand) => runStep(shellCommand, context),
|
|
30
|
+
captureOutput: captureShellCommandOutput,
|
|
31
|
+
};
|
|
32
|
+
return context;
|
|
33
|
+
}
|
package/dist/steps/macos.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { runStep, } from "./index.js";
|
|
2
|
-
import { captureShellCommandOutput } from "../runner.js";
|
|
3
|
-
// softwareupdate always exists on macOS — no availability check needed
|
|
4
1
|
const macosStep = {
|
|
5
2
|
id: "macos",
|
|
6
3
|
name: "macOS",
|
|
@@ -9,7 +6,7 @@ const macosStep = {
|
|
|
9
6
|
return true;
|
|
10
7
|
},
|
|
11
8
|
async run(context) {
|
|
12
|
-
const updateListOutput = await
|
|
9
|
+
const updateListOutput = await context.captureOutput({
|
|
13
10
|
shellCommand: "softwareupdate -l 2>&1",
|
|
14
11
|
});
|
|
15
12
|
context.logWriter(updateListOutput);
|
|
@@ -17,16 +14,16 @@ const macosStep = {
|
|
|
17
14
|
if (noUpdatesAvailable) {
|
|
18
15
|
context.onOutputLine({
|
|
19
16
|
text: "macOS is already up to date.",
|
|
20
|
-
|
|
17
|
+
source: "stdout",
|
|
21
18
|
});
|
|
22
19
|
return;
|
|
23
20
|
}
|
|
24
|
-
context.onOutputLine({ text: updateListOutput,
|
|
21
|
+
context.onOutputLine({ text: updateListOutput, source: "stdout" });
|
|
25
22
|
context.onOutputLine({
|
|
26
23
|
text: "Updates found — installing now...",
|
|
27
|
-
|
|
24
|
+
source: "stdout",
|
|
28
25
|
});
|
|
29
|
-
await runStep("softwareupdate -ia --verbose"
|
|
26
|
+
await context.runStep("softwareupdate -ia --verbose");
|
|
30
27
|
},
|
|
31
28
|
};
|
|
32
29
|
export default macosStep;
|
package/dist/steps/mas.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { checkCommandExists
|
|
1
|
+
import { checkCommandExists } from "./index.js";
|
|
2
2
|
const masStep = {
|
|
3
3
|
id: "mas",
|
|
4
4
|
name: "Mac App Store",
|
|
@@ -8,7 +8,7 @@ const masStep = {
|
|
|
8
8
|
return checkCommandExists("mas");
|
|
9
9
|
},
|
|
10
10
|
async run(context) {
|
|
11
|
-
await runStep("mas upgrade"
|
|
11
|
+
await context.runStep("mas upgrade");
|
|
12
12
|
},
|
|
13
13
|
};
|
|
14
14
|
export default masStep;
|
package/dist/steps/npm.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { checkCommandExists
|
|
1
|
+
import { checkCommandExists } from "./index.js";
|
|
2
2
|
const npmStep = {
|
|
3
3
|
id: "npm",
|
|
4
4
|
name: "npm",
|
|
5
5
|
description: "Update all globally installed npm packages",
|
|
6
|
-
// No brewPackageToInstall — npm comes bundled with Node.js
|
|
7
6
|
async checkIsAvailable() {
|
|
8
7
|
return checkCommandExists("npm");
|
|
9
8
|
},
|
|
10
9
|
async run(context) {
|
|
11
|
-
await runStep("npm update -g"
|
|
10
|
+
await context.runStep("npm update -g");
|
|
12
11
|
},
|
|
13
12
|
};
|
|
14
13
|
export default npmStep;
|
package/dist/steps/nvm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { checkPathExists
|
|
3
|
+
import { checkPathExists } from "./index.js";
|
|
4
4
|
const NVM_DIRECTORY = join(homedir(), ".nvm");
|
|
5
5
|
// nvm is a shell function sourced from nvm.sh — it cannot be invoked as a
|
|
6
6
|
// standalone binary, so we source it explicitly inside each bash invocation.
|
|
@@ -9,12 +9,11 @@ const nvmStep = {
|
|
|
9
9
|
id: "nvm",
|
|
10
10
|
name: "Node.js (nvm)",
|
|
11
11
|
description: "Install the latest Node.js via nvm, migrating packages from the current version",
|
|
12
|
-
// No brewPackageToInstall — nvm is installed via a curl script, not Homebrew
|
|
13
12
|
async checkIsAvailable() {
|
|
14
13
|
return checkPathExists(NVM_DIRECTORY);
|
|
15
14
|
},
|
|
16
15
|
async run(context) {
|
|
17
|
-
await runStep(`${NVM_SOURCE_PREFIX} && nvm install node --reinstall-packages-from=node
|
|
16
|
+
await context.runStep(`${NVM_SOURCE_PREFIX} && nvm install node --reinstall-packages-from=node`);
|
|
18
17
|
},
|
|
19
18
|
};
|
|
20
19
|
export default nvmStep;
|
package/dist/steps/ohmyzsh.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { checkPathExists
|
|
3
|
+
import { checkPathExists } from "./index.js";
|
|
4
4
|
const OH_MY_ZSH_UPGRADE_SCRIPT_PATH = join(homedir(), ".oh-my-zsh", "tools", "upgrade.sh");
|
|
5
5
|
const ohmyzshStep = {
|
|
6
6
|
id: "ohmyzsh",
|
|
7
7
|
name: "Oh My Zsh",
|
|
8
8
|
description: "Update Oh My Zsh to the latest version",
|
|
9
|
-
// No brewPackageToInstall — Oh My Zsh is installed via a curl script, not Homebrew
|
|
10
9
|
async checkIsAvailable() {
|
|
11
10
|
return checkPathExists(OH_MY_ZSH_UPGRADE_SCRIPT_PATH);
|
|
12
11
|
},
|
|
13
12
|
async run(context) {
|
|
14
|
-
await runStep(`zsh "${OH_MY_ZSH_UPGRADE_SCRIPT_PATH}"
|
|
13
|
+
await context.runStep(`zsh "${OH_MY_ZSH_UPGRADE_SCRIPT_PATH}"`);
|
|
15
14
|
},
|
|
16
15
|
};
|
|
17
16
|
export default ohmyzshStep;
|
package/dist/steps/pip.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { checkCommandExists
|
|
2
|
-
// Updating all global pip3 packages can occasionally break tools that depend
|
|
3
|
-
// on specific versions. The warning is surfaced in the UI before running.
|
|
1
|
+
import { checkCommandExists } from "./index.js";
|
|
4
2
|
const PIP_UPDATE_ALL_OUTDATED_SHELL_COMMAND = "pip3 list --outdated --format=columns | tail -n +3 | awk '{print $1}' | xargs -n1 pip3 install -U";
|
|
5
3
|
const pipStep = {
|
|
6
4
|
id: "pip",
|
|
7
5
|
name: "pip3",
|
|
8
6
|
description: "Update all globally installed pip3 packages",
|
|
9
|
-
|
|
7
|
+
warning: "This updates all global pip3 packages, which can occasionally break tools.",
|
|
10
8
|
async checkIsAvailable() {
|
|
11
9
|
return checkCommandExists("pip3");
|
|
12
10
|
},
|
|
13
11
|
async run(context) {
|
|
14
|
-
await runStep(PIP_UPDATE_ALL_OUTDATED_SHELL_COMMAND
|
|
12
|
+
await context.runStep(PIP_UPDATE_ALL_OUTDATED_SHELL_COMMAND);
|
|
15
13
|
},
|
|
16
14
|
};
|
|
17
15
|
export default pipStep;
|
package/dist/steps/pyenv.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import { checkCommandExists,
|
|
2
|
-
import { captureShellCommandOutput } from "../runner.js";
|
|
3
|
-
// Dynamically finds the latest stable Python 3.x.y release known to pyenv,
|
|
4
|
-
// excluding pre-releases (alpha, beta, release candidates, dev builds).
|
|
1
|
+
import { checkCommandExists, } from "./index.js";
|
|
5
2
|
const FIND_LATEST_STABLE_PYTHON_SHELL_COMMAND = "pyenv install -l | grep -E '^\\s+3\\.[0-9]+\\.[0-9]+$' | grep -vE 'dev|a[0-9]|b[0-9]|rc[0-9]' | tail -1 | tr -d ' '";
|
|
6
3
|
const pyenvStep = {
|
|
7
4
|
id: "pyenv",
|
|
@@ -12,18 +9,18 @@ const pyenvStep = {
|
|
|
12
9
|
return checkCommandExists("pyenv");
|
|
13
10
|
},
|
|
14
11
|
async run(context) {
|
|
15
|
-
await runStep("brew upgrade pyenv"
|
|
16
|
-
const latestPythonVersion = await
|
|
12
|
+
await context.runStep("brew upgrade pyenv");
|
|
13
|
+
const latestPythonVersion = await context.captureOutput({
|
|
17
14
|
shellCommand: FIND_LATEST_STABLE_PYTHON_SHELL_COMMAND,
|
|
18
15
|
});
|
|
19
16
|
if (!latestPythonVersion) {
|
|
20
17
|
context.onOutputLine({
|
|
21
18
|
text: "Could not determine latest Python version — skipping pyenv install.",
|
|
22
|
-
|
|
19
|
+
source: "stderr",
|
|
23
20
|
});
|
|
24
21
|
return;
|
|
25
22
|
}
|
|
26
|
-
await runStep(`pyenv install --skip-existing ${latestPythonVersion}
|
|
23
|
+
await context.runStep(`pyenv install --skip-existing ${latestPythonVersion}`);
|
|
27
24
|
},
|
|
28
25
|
};
|
|
29
26
|
export default pyenvStep;
|
package/dist/ui/header.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { Step } from "../steps/index.js";
|
|
2
|
-
import type { StepPhase, CompletedStepRecord } from "./state.js";
|
|
2
|
+
import type { StepPhase, CompletedStepRecord, LogoVisibility } from "./state.js";
|
|
3
|
+
import type { TerminalDimensions } from "./outputBox.js";
|
|
4
|
+
type LogoLayout = "side-by-side" | "stacked" | "none";
|
|
5
|
+
export declare function determineLogoLayout(logoLines: string[], dimensions?: TerminalDimensions): LogoLayout;
|
|
6
|
+
export declare function stepTrackerIcon(phase: StepPhase): string;
|
|
7
|
+
export declare function isActivePhase(phase: StepPhase): boolean;
|
|
8
|
+
export declare function deriveAllStepPhases(steps: Step[], currentStepIndex: number, currentPhase: StepPhase, completedStepRecords: CompletedStepRecord[]): StepPhase[];
|
|
3
9
|
type BuildHeaderOptions = {
|
|
4
10
|
steps: Step[];
|
|
5
11
|
version: string;
|
|
6
|
-
|
|
12
|
+
logoVisibility: LogoVisibility;
|
|
7
13
|
currentStepIndex: number;
|
|
8
14
|
currentPhase: StepPhase;
|
|
9
15
|
completedStepRecords: CompletedStepRecord[];
|
|
16
|
+
terminalDimensions?: TerminalDimensions;
|
|
10
17
|
};
|
|
11
18
|
export declare function buildHeaderLines(options: BuildHeaderOptions): string[];
|
|
12
19
|
export {};
|
package/dist/ui/header.js
CHANGED
|
@@ -2,7 +2,19 @@ import chalk from "chalk";
|
|
|
2
2
|
import { LOGO_ART } from "../logo.js";
|
|
3
3
|
const LOGO_COLUMN_WIDTH = 32;
|
|
4
4
|
const LOGO_SEPARATOR = " ";
|
|
5
|
-
|
|
5
|
+
const MIN_SIDE_BY_SIDE_COLS = LOGO_COLUMN_WIDTH + LOGO_SEPARATOR.length + 20; // 56
|
|
6
|
+
export function determineLogoLayout(logoLines, dimensions) {
|
|
7
|
+
const cols = dimensions?.columns ?? process.stdout.columns ?? 80;
|
|
8
|
+
const rows = dimensions?.rows ?? process.stdout.rows ?? 24;
|
|
9
|
+
if (cols >= MIN_SIDE_BY_SIDE_COLS) {
|
|
10
|
+
return "side-by-side";
|
|
11
|
+
}
|
|
12
|
+
if (rows >= logoLines.length + 6) {
|
|
13
|
+
return "stacked";
|
|
14
|
+
}
|
|
15
|
+
return "none";
|
|
16
|
+
}
|
|
17
|
+
export function stepTrackerIcon(phase) {
|
|
6
18
|
if (phase === "complete")
|
|
7
19
|
return chalk.green("✓ ");
|
|
8
20
|
if (phase === "failed")
|
|
@@ -17,15 +29,15 @@ function stepTrackerIcon(phase) {
|
|
|
17
29
|
return chalk.cyan("→ ");
|
|
18
30
|
return chalk.dim("· ");
|
|
19
31
|
}
|
|
20
|
-
function isActivePhase(phase) {
|
|
32
|
+
export function isActivePhase(phase) {
|
|
21
33
|
return (phase === "running" ||
|
|
22
34
|
phase === "installing" ||
|
|
23
35
|
phase === "prompting-to-run" ||
|
|
24
36
|
phase === "prompting-to-install" ||
|
|
25
37
|
phase === "checking-availability");
|
|
26
38
|
}
|
|
27
|
-
function deriveAllStepPhases(steps, currentStepIndex, currentPhase, completedStepRecords) {
|
|
28
|
-
return steps.map((
|
|
39
|
+
export function deriveAllStepPhases(steps, currentStepIndex, currentPhase, completedStepRecords) {
|
|
40
|
+
return steps.map((_step, index) => {
|
|
29
41
|
if (index < completedStepRecords.length)
|
|
30
42
|
return completedStepRecords[index].phase;
|
|
31
43
|
if (index === currentStepIndex)
|
|
@@ -34,7 +46,7 @@ function deriveAllStepPhases(steps, currentStepIndex, currentPhase, completedSte
|
|
|
34
46
|
});
|
|
35
47
|
}
|
|
36
48
|
export function buildHeaderLines(options) {
|
|
37
|
-
const { steps, version,
|
|
49
|
+
const { steps, version, logoVisibility, currentStepIndex, currentPhase, completedStepRecords } = options;
|
|
38
50
|
const phases = deriveAllStepPhases(steps, currentStepIndex, currentPhase, completedStepRecords);
|
|
39
51
|
const rightColumnLines = [
|
|
40
52
|
`${chalk.bold.white("braeburn")} ${chalk.dim("v" + version)}`,
|
|
@@ -46,18 +58,28 @@ export function buildHeaderLines(options) {
|
|
|
46
58
|
return `${icon}${name}`;
|
|
47
59
|
}),
|
|
48
60
|
];
|
|
49
|
-
if (
|
|
61
|
+
if (logoVisibility === "hidden") {
|
|
50
62
|
return rightColumnLines;
|
|
51
63
|
}
|
|
52
64
|
const logoLines = LOGO_ART.split("\n");
|
|
65
|
+
const layout = determineLogoLayout(logoLines, options.terminalDimensions);
|
|
66
|
+
if (layout === "none") {
|
|
67
|
+
return rightColumnLines;
|
|
68
|
+
}
|
|
69
|
+
if (layout === "stacked") {
|
|
70
|
+
return [
|
|
71
|
+
...logoLines.map((line) => chalk.yellow(line)),
|
|
72
|
+
"",
|
|
73
|
+
...rightColumnLines,
|
|
74
|
+
];
|
|
75
|
+
}
|
|
53
76
|
const totalLines = Math.max(logoLines.length, rightColumnLines.length);
|
|
54
77
|
const result = [];
|
|
55
|
-
for (let
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
const rawLogoLine = (logoLines[i] ?? "").padEnd(LOGO_COLUMN_WIDTH);
|
|
78
|
+
for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
|
|
79
|
+
// Padding must happen before chalk; ANSI escape codes break .padEnd() alignment.
|
|
80
|
+
const rawLogoLine = (logoLines[lineIndex] ?? "").padEnd(LOGO_COLUMN_WIDTH);
|
|
59
81
|
const logoColumn = chalk.yellow(rawLogoLine);
|
|
60
|
-
const rightColumn = rightColumnLines[
|
|
82
|
+
const rightColumn = rightColumnLines[lineIndex] ?? "";
|
|
61
83
|
result.push(`${logoColumn}${LOGO_SEPARATOR}${rightColumn}`);
|
|
62
84
|
}
|
|
63
85
|
return result;
|
package/dist/ui/outputBox.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { CommandOutputLine } from "../runner.js";
|
|
2
|
-
export
|
|
2
|
+
export type TerminalDimensions = {
|
|
3
|
+
columns: number;
|
|
4
|
+
rows: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function buildOutputBoxLines(lines: CommandOutputLine[], stepName: string, dimensions?: TerminalDimensions): string[];
|
package/dist/ui/outputBox.js
CHANGED
|
@@ -3,17 +3,23 @@ const INDENT = " ";
|
|
|
3
3
|
const HEADER_LINES_APPROXIMATE = 18;
|
|
4
4
|
const OUTPUT_BOX_CHROME_LINES = 3;
|
|
5
5
|
const MINIMUM_VISIBLE_LINES = 5;
|
|
6
|
-
function maxVisibleLines() {
|
|
7
|
-
const rows = process.stdout.rows ?? 40;
|
|
6
|
+
function maxVisibleLines(rows) {
|
|
8
7
|
const available = rows - HEADER_LINES_APPROXIMATE - OUTPUT_BOX_CHROME_LINES;
|
|
9
8
|
return Math.max(MINIMUM_VISIBLE_LINES, available);
|
|
10
9
|
}
|
|
11
|
-
function boxWidth() {
|
|
12
|
-
return Math.min(
|
|
10
|
+
function boxWidth(columns) {
|
|
11
|
+
return Math.min(columns, 120) - INDENT.length * 2;
|
|
13
12
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
function resolveTerminalDimensions(dimensions) {
|
|
14
|
+
return dimensions ?? {
|
|
15
|
+
columns: process.stdout.columns ?? 80,
|
|
16
|
+
rows: process.stdout.rows ?? 40,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function buildOutputBoxLines(lines, stepName, dimensions) {
|
|
20
|
+
const resolved = resolveTerminalDimensions(dimensions);
|
|
21
|
+
const visibleLines = lines.slice(-maxVisibleLines(resolved.rows));
|
|
22
|
+
const width = boxWidth(resolved.columns);
|
|
17
23
|
const headerLabel = `─ ${stepName} output `;
|
|
18
24
|
const topDashes = "─".repeat(Math.max(0, width - headerLabel.length - 2));
|
|
19
25
|
const topBorder = chalk.dim(`${INDENT}┌${headerLabel}${topDashes}┐`);
|
|
@@ -21,7 +27,7 @@ export function buildOutputBoxLines(lines, stepName) {
|
|
|
21
27
|
const result = [topBorder];
|
|
22
28
|
for (const line of visibleLines) {
|
|
23
29
|
const truncated = line.text.slice(0, width - 4);
|
|
24
|
-
const colored = line.
|
|
30
|
+
const colored = line.source === "stderr" ? chalk.yellow(truncated) : chalk.dim(truncated);
|
|
25
31
|
result.push(`${INDENT}${chalk.dim("│")} ${colored}`);
|
|
26
32
|
}
|
|
27
33
|
result.push(bottomBorder);
|
package/dist/ui/screen.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type TerminalDimensions } from "./outputBox.js";
|
|
1
2
|
import type { AppState } from "./state.js";
|
|
2
|
-
export
|
|
3
|
-
export declare function
|
|
3
|
+
export type ScreenRenderer = (content: string) => void;
|
|
4
|
+
export declare function createScreenRenderer(output?: NodeJS.WritableStream): ScreenRenderer;
|
|
5
|
+
export declare function buildScreen(state: AppState, terminalDimensions?: TerminalDimensions): string;
|
package/dist/ui/screen.js
CHANGED
|
@@ -3,26 +3,29 @@ import { buildActiveStepLines } from "./currentStep.js";
|
|
|
3
3
|
import { buildOutputBoxLines } from "./outputBox.js";
|
|
4
4
|
import { buildPromptLines } from "./prompt.js";
|
|
5
5
|
import { buildVersionReportLines } from "./versionReport.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
export function createScreenRenderer(output = process.stdout) {
|
|
7
|
+
let previousLineCount = 0;
|
|
8
|
+
return (content) => {
|
|
9
|
+
if (previousLineCount > 0) {
|
|
10
|
+
output.write(`\x1b[${previousLineCount}A\x1b[J`);
|
|
11
|
+
}
|
|
12
|
+
output.write(content);
|
|
13
|
+
previousLineCount = (content.match(/\n/g) ?? []).length;
|
|
14
|
+
};
|
|
13
15
|
}
|
|
14
|
-
export function buildScreen(state) {
|
|
16
|
+
export function buildScreen(state, terminalDimensions) {
|
|
15
17
|
const lines = [];
|
|
16
18
|
lines.push(...buildHeaderLines({
|
|
17
19
|
steps: state.steps,
|
|
18
20
|
version: state.version,
|
|
19
|
-
|
|
21
|
+
logoVisibility: state.logoVisibility,
|
|
20
22
|
currentStepIndex: state.currentStepIndex,
|
|
21
23
|
currentPhase: state.currentPhase,
|
|
22
24
|
completedStepRecords: state.completedStepRecords,
|
|
25
|
+
terminalDimensions,
|
|
23
26
|
}));
|
|
24
27
|
lines.push("");
|
|
25
|
-
if (state.
|
|
28
|
+
if (state.runCompletion === "finished") {
|
|
26
29
|
if (state.versionReport) {
|
|
27
30
|
lines.push("");
|
|
28
31
|
lines.push(...buildVersionReportLines(state.versionReport));
|
|
@@ -42,7 +45,7 @@ export function buildScreen(state) {
|
|
|
42
45
|
state.currentOutputLines.length > 0;
|
|
43
46
|
if (isShowingOutput) {
|
|
44
47
|
lines.push("");
|
|
45
|
-
lines.push(...buildOutputBoxLines(state.currentOutputLines, currentStep.name));
|
|
48
|
+
lines.push(...buildOutputBoxLines(state.currentOutputLines, currentStep.name, terminalDimensions));
|
|
46
49
|
}
|
|
47
50
|
if (state.currentPrompt) {
|
|
48
51
|
lines.push("");
|
package/dist/ui/state.d.ts
CHANGED
|
@@ -13,16 +13,18 @@ export type ResolvedVersion = {
|
|
|
13
13
|
label: string;
|
|
14
14
|
value: string;
|
|
15
15
|
};
|
|
16
|
+
export type LogoVisibility = "visible" | "hidden";
|
|
17
|
+
export type RunCompletion = "in-progress" | "finished";
|
|
16
18
|
export type AppState = {
|
|
17
19
|
steps: Step[];
|
|
18
20
|
version: string;
|
|
19
|
-
|
|
21
|
+
logoVisibility: LogoVisibility;
|
|
20
22
|
currentStepIndex: number;
|
|
21
23
|
currentPhase: StepPhase;
|
|
22
24
|
completedStepRecords: CompletedStepRecord[];
|
|
23
25
|
currentOutputLines: CommandOutputLine[];
|
|
24
26
|
currentPrompt: CurrentPrompt | undefined;
|
|
25
|
-
|
|
27
|
+
runCompletion: RunCompletion;
|
|
26
28
|
versionReport: ResolvedVersion[] | undefined;
|
|
27
29
|
};
|
|
28
|
-
export declare function createInitialAppState(steps: Step[], version: string,
|
|
30
|
+
export declare function createInitialAppState(steps: Step[], version: string, logoVisibility: LogoVisibility): AppState;
|
package/dist/ui/state.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export function createInitialAppState(steps, version,
|
|
1
|
+
export function createInitialAppState(steps, version, logoVisibility) {
|
|
2
2
|
return {
|
|
3
3
|
steps,
|
|
4
4
|
version,
|
|
5
|
-
|
|
5
|
+
logoVisibility,
|
|
6
6
|
currentStepIndex: 0,
|
|
7
7
|
currentPhase: "checking-availability",
|
|
8
8
|
completedStepRecords: [],
|
|
9
9
|
currentOutputLines: [],
|
|
10
10
|
currentPrompt: undefined,
|
|
11
|
-
|
|
11
|
+
runCompletion: "in-progress",
|
|
12
12
|
versionReport: undefined,
|
|
13
13
|
};
|
|
14
14
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function hideCursorDuringExecution(options = {}) {
|
|
2
|
+
const output = options.output ?? process.stdout;
|
|
3
|
+
output.write("\x1b[?25l");
|
|
4
|
+
const restoreOnExit = () => output.write("\x1b[?25h");
|
|
5
|
+
const restoreAndExitOnInterrupt = () => {
|
|
6
|
+
output.write("\x1b[?25h\n");
|
|
7
|
+
process.exit(130);
|
|
8
|
+
};
|
|
9
|
+
process.on("exit", restoreOnExit);
|
|
10
|
+
process.on("SIGINT", restoreAndExitOnInterrupt);
|
|
11
|
+
return () => {
|
|
12
|
+
process.removeListener("exit", restoreOnExit);
|
|
13
|
+
process.removeListener("SIGINT", restoreAndExitOnInterrupt);
|
|
14
|
+
output.write("\x1b[?25h");
|
|
15
|
+
};
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braeburn",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "macOS system updater CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"postbuild": "chmod +x dist/index.js",
|
|
18
18
|
"dev": "tsx src/index.ts",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
20
22
|
"prepublishOnly": "npm run build"
|
|
21
23
|
},
|
|
22
24
|
"dependencies": {
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"@types/node": "^24.0.0",
|
|
30
32
|
"tsx": "^4.7.0",
|
|
31
|
-
"typescript": "^5.5.0"
|
|
33
|
+
"typescript": "^5.5.0",
|
|
34
|
+
"vitest": "^4.0.18"
|
|
32
35
|
}
|
|
33
36
|
}
|