braeburn 2.0.0 → 2.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/dist/cli.d.ts +39 -0
- package/dist/cli.js +226 -0
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.js +31 -25
- package/dist/commands/update.d.ts +2 -1
- package/dist/commands/update.js +32 -3
- package/dist/config.d.ts +8 -0
- package/dist/config.js +68 -9
- package/dist/index.js +2 -178
- package/dist/runner.d.ts +6 -0
- package/dist/runner.js +98 -30
- package/dist/update/engine.d.ts +1 -0
- package/dist/update/engine.js +9 -2
- package/package.json +1 -1
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { Step } from "./steps/index.js";
|
|
3
|
+
import { applyUpdateCommandResult, runUpdateCommand } from "./commands/update.js";
|
|
4
|
+
import { runLogCommand, runLogListCommand } from "./commands/log.js";
|
|
5
|
+
import { runConfigCommand, runConfigUpdateCommand } from "./commands/config.js";
|
|
6
|
+
import { runSetupCommand } from "./commands/setup.js";
|
|
7
|
+
import { configFileExists, readConfig } from "./config.js";
|
|
8
|
+
type CliProcess = Pick<NodeJS.Process, "stderr"> & {
|
|
9
|
+
exitCode: string | number | null | undefined;
|
|
10
|
+
};
|
|
11
|
+
type CliCommandDependencies = {
|
|
12
|
+
applyUpdateCommandResult: typeof applyUpdateCommandResult;
|
|
13
|
+
configFileExists: typeof configFileExists;
|
|
14
|
+
readConfig: typeof readConfig;
|
|
15
|
+
runConfigCommand: typeof runConfigCommand;
|
|
16
|
+
runConfigUpdateCommand: typeof runConfigUpdateCommand;
|
|
17
|
+
runLogCommand: typeof runLogCommand;
|
|
18
|
+
runLogListCommand: typeof runLogListCommand;
|
|
19
|
+
runSetupCommand: typeof runSetupCommand;
|
|
20
|
+
runUpdateCommand: typeof runUpdateCommand;
|
|
21
|
+
};
|
|
22
|
+
type CreateBraeburnProgramOptions = {
|
|
23
|
+
allSteps?: Step[];
|
|
24
|
+
dependencies?: Partial<CliCommandDependencies>;
|
|
25
|
+
processLike?: CliProcess;
|
|
26
|
+
version?: string;
|
|
27
|
+
};
|
|
28
|
+
export type StepResolution = {
|
|
29
|
+
status: "resolved";
|
|
30
|
+
steps: Step[];
|
|
31
|
+
} | {
|
|
32
|
+
status: "unknown-step";
|
|
33
|
+
stepId: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function resolveStepsByIds(stepIds: string[], allSteps: Step[]): StepResolution;
|
|
36
|
+
export declare function reportCliError(error: unknown, processLike?: CliProcess): void;
|
|
37
|
+
export declare function createBraeburnProgram(options?: CreateBraeburnProgramOptions): Command;
|
|
38
|
+
export declare function runBraeburnCli(argv?: string[]): Promise<void>;
|
|
39
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { ALL_STEPS } from "./steps/catalog.js";
|
|
6
|
+
import { applyUpdateCommandResult, runUpdateCommand } from "./commands/update.js";
|
|
7
|
+
import { runLogCommand, runLogListCommand } from "./commands/log.js";
|
|
8
|
+
import { runConfigCommand, runConfigUpdateCommand } from "./commands/config.js";
|
|
9
|
+
import { runSetupCommand } from "./commands/setup.js";
|
|
10
|
+
import { ConfigReadError, PROTECTED_STEP_IDS, configFileExists, isLogoEnabled, isStepEnabled, readConfig, } from "./config.js";
|
|
11
|
+
const requireFromThis = createRequire(import.meta.url);
|
|
12
|
+
function resolveBraeburnVersion() {
|
|
13
|
+
const packageJson = requireFromThis(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"));
|
|
14
|
+
return packageJson.version;
|
|
15
|
+
}
|
|
16
|
+
function resolveDependencies(overrides) {
|
|
17
|
+
return {
|
|
18
|
+
applyUpdateCommandResult: overrides?.applyUpdateCommandResult ?? applyUpdateCommandResult,
|
|
19
|
+
configFileExists: overrides?.configFileExists ?? configFileExists,
|
|
20
|
+
readConfig: overrides?.readConfig ?? readConfig,
|
|
21
|
+
runConfigCommand: overrides?.runConfigCommand ?? runConfigCommand,
|
|
22
|
+
runConfigUpdateCommand: overrides?.runConfigUpdateCommand ?? runConfigUpdateCommand,
|
|
23
|
+
runLogCommand: overrides?.runLogCommand ?? runLogCommand,
|
|
24
|
+
runLogListCommand: overrides?.runLogListCommand ?? runLogListCommand,
|
|
25
|
+
runSetupCommand: overrides?.runSetupCommand ?? runSetupCommand,
|
|
26
|
+
runUpdateCommand: overrides?.runUpdateCommand ?? runUpdateCommand,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function resolveStepsByIds(stepIds, allSteps) {
|
|
30
|
+
const stepsById = new Map(allSteps.map((step) => [step.id, step]));
|
|
31
|
+
const resolvedSteps = [];
|
|
32
|
+
for (const stepId of stepIds) {
|
|
33
|
+
const step = stepsById.get(stepId);
|
|
34
|
+
if (!step) {
|
|
35
|
+
return { status: "unknown-step", stepId };
|
|
36
|
+
}
|
|
37
|
+
resolvedSteps.push(step);
|
|
38
|
+
}
|
|
39
|
+
return { status: "resolved", steps: resolvedSteps };
|
|
40
|
+
}
|
|
41
|
+
export function reportCliError(error, processLike = process) {
|
|
42
|
+
if (error instanceof ConfigReadError) {
|
|
43
|
+
processLike.stderr.write(`${error.message}\n`);
|
|
44
|
+
processLike.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48
|
+
processLike.stderr.write(`${message}\n`);
|
|
49
|
+
processLike.exitCode = 1;
|
|
50
|
+
}
|
|
51
|
+
export function createBraeburnProgram(options = {}) {
|
|
52
|
+
const allSteps = options.allSteps ?? ALL_STEPS;
|
|
53
|
+
const dependencies = resolveDependencies(options.dependencies);
|
|
54
|
+
const processLike = options.processLike ?? process;
|
|
55
|
+
const braeburnVersion = options.version ?? resolveBraeburnVersion();
|
|
56
|
+
const program = new Command();
|
|
57
|
+
program
|
|
58
|
+
.name("braeburn")
|
|
59
|
+
.description("macOS system updater")
|
|
60
|
+
.version(braeburnVersion)
|
|
61
|
+
.helpCommand(false);
|
|
62
|
+
program
|
|
63
|
+
.command("update", { isDefault: true })
|
|
64
|
+
.description("Run system update steps (default command)")
|
|
65
|
+
.argument("[steps...]", `Steps to run — omit to run all.\nAvailable: ${allSteps.map((step) => step.id).join(", ")}`)
|
|
66
|
+
.option("-y, --yes", "Auto-accept all prompts (default yes to everything)")
|
|
67
|
+
.option("-f, --force", "Alias for --yes")
|
|
68
|
+
.option("--no-logo", "Hide the logo")
|
|
69
|
+
.addHelpText("after", `
|
|
70
|
+
Step descriptions:
|
|
71
|
+
System / Runtimes (default: off — larger changes, enabled intentionally):
|
|
72
|
+
pyenv Upgrade pyenv, install latest Python 3.x (requires: pyenv or brew)
|
|
73
|
+
nvm Install latest Node.js via nvm (requires: ~/.nvm)
|
|
74
|
+
|
|
75
|
+
System / Apps & Packages:
|
|
76
|
+
homebrew Update Homebrew itself and all installed formulae
|
|
77
|
+
mas Upgrade Mac App Store apps (requires: mas)
|
|
78
|
+
macos Check for macOS updates, prompt to install
|
|
79
|
+
|
|
80
|
+
System / CLI Tools:
|
|
81
|
+
npm Update global npm packages (requires: npm)
|
|
82
|
+
braeburn Update braeburn CLI itself (requires: npm)
|
|
83
|
+
pip Update global pip3 packages (requires: pip3) ⚠ may be fragile
|
|
84
|
+
dotnet Update .NET global tools (requires: dotnet)
|
|
85
|
+
|
|
86
|
+
System / Shell:
|
|
87
|
+
ohmyzsh Update Oh My Zsh (requires: ~/.oh-my-zsh)
|
|
88
|
+
|
|
89
|
+
System / Maintenance:
|
|
90
|
+
cleanup homebrew cleanup (remove outdated Homebrew cache/downloads)
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
braeburn Run all enabled steps interactively
|
|
94
|
+
braeburn -y Run all enabled steps, auto-accept everything
|
|
95
|
+
braeburn -fy Same as above
|
|
96
|
+
braeburn homebrew npm Run only the homebrew and npm steps
|
|
97
|
+
braeburn homebrew -y Run only homebrew, auto-accept
|
|
98
|
+
braeburn nvm pyenv Run only the runtime steps
|
|
99
|
+
`)
|
|
100
|
+
.action(async (stepArguments, updateOptions) => {
|
|
101
|
+
const autoYes = updateOptions.yes === true || updateOptions.force === true;
|
|
102
|
+
if (!(await dependencies.configFileExists())) {
|
|
103
|
+
await dependencies.runSetupCommand(allSteps);
|
|
104
|
+
}
|
|
105
|
+
const config = await dependencies.readConfig();
|
|
106
|
+
let stepsToRun = allSteps;
|
|
107
|
+
if (stepArguments.length > 0) {
|
|
108
|
+
const stepResolution = resolveStepsByIds(stepArguments, allSteps);
|
|
109
|
+
if (stepResolution.status === "unknown-step") {
|
|
110
|
+
processLike.stderr.write(`Unknown step: "${stepResolution.stepId}". Run braeburn --help to see available steps.\n`);
|
|
111
|
+
processLike.exitCode = 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
stepsToRun = stepResolution.steps;
|
|
115
|
+
}
|
|
116
|
+
if (stepArguments.length === 0) {
|
|
117
|
+
stepsToRun = stepsToRun.filter((step) => isStepEnabled(config, step.id));
|
|
118
|
+
}
|
|
119
|
+
const logoIsEnabled = updateOptions.logo !== false && isLogoEnabled(config);
|
|
120
|
+
const updateCommandResult = await dependencies.runUpdateCommand({
|
|
121
|
+
steps: stepsToRun,
|
|
122
|
+
promptMode: autoYes ? "auto-accept" : "interactive",
|
|
123
|
+
logoVisibility: logoIsEnabled ? "visible" : "hidden",
|
|
124
|
+
version: braeburnVersion,
|
|
125
|
+
});
|
|
126
|
+
dependencies.applyUpdateCommandResult(updateCommandResult, processLike);
|
|
127
|
+
});
|
|
128
|
+
program
|
|
129
|
+
.command("log")
|
|
130
|
+
.description("View the most recent output log for a given step")
|
|
131
|
+
.argument("[step]", "Step ID to view logs for (e.g. homebrew, npm, pip)")
|
|
132
|
+
.option("--homebrew", "Show latest Homebrew log")
|
|
133
|
+
.option("--brew", "Alias for --homebrew")
|
|
134
|
+
.option("--mas", "Show latest Mac App Store log")
|
|
135
|
+
.option("--ohmyzsh", "Show latest Oh My Zsh log")
|
|
136
|
+
.option("--npm", "Show latest npm log")
|
|
137
|
+
.option("--braeburn", "Show latest braeburn log")
|
|
138
|
+
.option("--pip", "Show latest pip3 log")
|
|
139
|
+
.option("--pyenv", "Show latest pyenv log")
|
|
140
|
+
.option("--nvm", "Show latest nvm log")
|
|
141
|
+
.option("--dotnet", "Show latest .NET log")
|
|
142
|
+
.option("--macos", "Show latest macOS update log")
|
|
143
|
+
.option("--cleanup", "Show latest homebrew cleanup log")
|
|
144
|
+
.addHelpText("after", `
|
|
145
|
+
Examples:
|
|
146
|
+
braeburn log List all available step logs
|
|
147
|
+
braeburn log homebrew Show the latest Homebrew run log
|
|
148
|
+
braeburn log --brew Same as above
|
|
149
|
+
braeburn log npm | less Pipe log output through less
|
|
150
|
+
`)
|
|
151
|
+
.action(async (stepArgument, logOptions) => {
|
|
152
|
+
const stepIdFromFlag = logOptions.brew === true
|
|
153
|
+
? "homebrew"
|
|
154
|
+
: allSteps.map((step) => step.id).find((stepId) => logOptions[stepId] === true);
|
|
155
|
+
const resolvedStepId = stepArgument ?? stepIdFromFlag;
|
|
156
|
+
if (!resolvedStepId) {
|
|
157
|
+
dependencies.runLogListCommand();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
await dependencies.runLogCommand({ stepId: resolvedStepId });
|
|
161
|
+
});
|
|
162
|
+
const configCommand = program
|
|
163
|
+
.command("config")
|
|
164
|
+
.description("View or edit braeburn configuration")
|
|
165
|
+
.action(() => {
|
|
166
|
+
configCommand.outputHelp();
|
|
167
|
+
});
|
|
168
|
+
configCommand
|
|
169
|
+
.command("list")
|
|
170
|
+
.description("Print current configuration")
|
|
171
|
+
.action(async () => {
|
|
172
|
+
await dependencies.runConfigCommand({
|
|
173
|
+
allSteps,
|
|
174
|
+
outputMode: "non-interactive",
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
const configurableSteps = allSteps.filter((step) => !PROTECTED_STEP_IDS.has(step.id));
|
|
178
|
+
const configUpdateCommand = configCommand
|
|
179
|
+
.command("update")
|
|
180
|
+
.description("Edit configuration (interactive by default, flags for direct updates)")
|
|
181
|
+
.addHelpText("after", `
|
|
182
|
+
Examples:
|
|
183
|
+
braeburn config update Open interactive config editor
|
|
184
|
+
braeburn config update --no-logo Hide the logo
|
|
185
|
+
braeburn config update --no-ohmyzsh Disable Oh My Zsh updates
|
|
186
|
+
braeburn config update --no-pip --no-nvm Disable pip and nvm updates
|
|
187
|
+
braeburn config update --ohmyzsh Re-enable Oh My Zsh updates
|
|
188
|
+
`);
|
|
189
|
+
configUpdateCommand.option("--no-logo", "Hide the logo");
|
|
190
|
+
configUpdateCommand.option("--logo", "Show the logo");
|
|
191
|
+
for (const step of configurableSteps) {
|
|
192
|
+
configUpdateCommand.option(`--no-${step.id}`, `Disable ${step.name} updates`);
|
|
193
|
+
configUpdateCommand.option(`--${step.id}`, `Enable ${step.name} updates`);
|
|
194
|
+
}
|
|
195
|
+
configUpdateCommand.action(async function () {
|
|
196
|
+
// Commander defaults --no-* to true, so we use getOptionValueSource to detect explicit CLI flags.
|
|
197
|
+
const settingUpdates = {};
|
|
198
|
+
for (const step of configurableSteps) {
|
|
199
|
+
const source = configUpdateCommand.getOptionValueSource(step.id);
|
|
200
|
+
if (source === "cli") {
|
|
201
|
+
settingUpdates[step.id] = configUpdateCommand.opts()[step.id] ? "enable" : "disable";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const logoSource = configUpdateCommand.getOptionValueSource("logo");
|
|
205
|
+
if (logoSource === "cli") {
|
|
206
|
+
settingUpdates.logo = configUpdateCommand.opts().logo ? "enable" : "disable";
|
|
207
|
+
}
|
|
208
|
+
if (Object.keys(settingUpdates).length === 0) {
|
|
209
|
+
await dependencies.runConfigCommand({
|
|
210
|
+
allSteps,
|
|
211
|
+
outputMode: "interactive",
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
await dependencies.runConfigUpdateCommand({ settingUpdates, allSteps });
|
|
216
|
+
});
|
|
217
|
+
return program;
|
|
218
|
+
}
|
|
219
|
+
export async function runBraeburnCli(argv = process.argv) {
|
|
220
|
+
try {
|
|
221
|
+
await createBraeburnProgram().parseAsync(argv);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
reportCliError(error);
|
|
225
|
+
}
|
|
226
|
+
}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BraeburnConfig } from "../config.js";
|
|
1
2
|
import { type Step, type StepCategoryId } from "../steps/index.js";
|
|
2
3
|
export type SelectionState = "selected" | "deselected";
|
|
3
4
|
export type ProtectionStatus = "protected" | "configurable";
|
|
@@ -17,4 +18,6 @@ export type SelectableStep = {
|
|
|
17
18
|
};
|
|
18
19
|
export declare function buildLoadingScreen(): string;
|
|
19
20
|
export declare function buildSetupScreen(items: SelectableStep[], cursorIndex: number): string;
|
|
21
|
+
export declare function buildSetupItems(allSteps: Step[], availabilityResults: boolean[]): SelectableStep[];
|
|
22
|
+
export declare function buildConfigFromSetupItems(items: SelectableStep[]): BraeburnConfig;
|
|
20
23
|
export declare function runSetupCommand(allSteps: Step[]): Promise<void>;
|
package/dist/commands/setup.js
CHANGED
|
@@ -64,26 +64,42 @@ export function buildSetupScreen(items, cursorIndex) {
|
|
|
64
64
|
lines.push("");
|
|
65
65
|
return lines.join("\n") + "\n";
|
|
66
66
|
}
|
|
67
|
+
export function buildSetupItems(allSteps, availabilityResults) {
|
|
68
|
+
return allSteps.map((step, stepIndex) => ({
|
|
69
|
+
step: {
|
|
70
|
+
id: step.id,
|
|
71
|
+
name: step.name,
|
|
72
|
+
description: step.description,
|
|
73
|
+
categoryId: step.categoryId,
|
|
74
|
+
brewPackageToInstall: step.brewPackageToInstall,
|
|
75
|
+
},
|
|
76
|
+
selection: PROTECTED_STEP_IDS.has(step.id) || CONSERVATIVE_DEFAULT_ON_STEP_IDS.has(step.id)
|
|
77
|
+
? "selected"
|
|
78
|
+
: "deselected",
|
|
79
|
+
protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
|
|
80
|
+
availability: availabilityResults[stepIndex] ? "available" : "unavailable",
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
export function buildConfigFromSetupItems(items) {
|
|
84
|
+
const draftConfig = {
|
|
85
|
+
defaultsProfile: "conservative-v2",
|
|
86
|
+
steps: {},
|
|
87
|
+
};
|
|
88
|
+
for (const item of items) {
|
|
89
|
+
if (item.protection === "protected") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
draftConfig.steps[item.step.id] = item.selection === "selected";
|
|
93
|
+
}
|
|
94
|
+
return cleanConfigForWrite(draftConfig);
|
|
95
|
+
}
|
|
67
96
|
export async function runSetupCommand(allSteps) {
|
|
68
97
|
const render = createScreenRenderer();
|
|
69
98
|
const restoreCursor = hideCursorDuringExecution({ screenBuffer: "alternate" });
|
|
70
99
|
try {
|
|
71
100
|
render(buildLoadingScreen());
|
|
72
101
|
const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
|
|
73
|
-
const items = allSteps
|
|
74
|
-
step: {
|
|
75
|
-
id: step.id,
|
|
76
|
-
name: step.name,
|
|
77
|
-
description: step.description,
|
|
78
|
-
categoryId: step.categoryId,
|
|
79
|
-
brewPackageToInstall: step.brewPackageToInstall,
|
|
80
|
-
},
|
|
81
|
-
selection: PROTECTED_STEP_IDS.has(step.id) || CONSERVATIVE_DEFAULT_ON_STEP_IDS.has(step.id)
|
|
82
|
-
? "selected"
|
|
83
|
-
: "deselected",
|
|
84
|
-
protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
|
|
85
|
-
availability: availabilityResults[stepIndex] ? "available" : "unavailable",
|
|
86
|
-
}));
|
|
102
|
+
const items = buildSetupItems(allSteps, availabilityResults);
|
|
87
103
|
let cursorIndex = 0;
|
|
88
104
|
render(buildSetupScreen(items, cursorIndex));
|
|
89
105
|
await new Promise((resolve) => {
|
|
@@ -120,17 +136,7 @@ export async function runSetupCommand(allSteps) {
|
|
|
120
136
|
process.stdin.on("keypress", handleKeypress);
|
|
121
137
|
process.stdin.resume();
|
|
122
138
|
});
|
|
123
|
-
|
|
124
|
-
defaultsProfile: "conservative-v2",
|
|
125
|
-
steps: {},
|
|
126
|
-
};
|
|
127
|
-
for (const item of items) {
|
|
128
|
-
if (item.protection === "protected") {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
draftConfig.steps[item.step.id] = item.selection === "selected";
|
|
132
|
-
}
|
|
133
|
-
await writeConfig(cleanConfigForWrite(draftConfig));
|
|
139
|
+
await writeConfig(buildConfigFromSetupItems(items));
|
|
134
140
|
const confirmationLines = [
|
|
135
141
|
chalk.yellow(LOGO_ART),
|
|
136
142
|
"",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PromptMode } from "../update/engine.js";
|
|
2
|
-
import { type LogoVisibility } from "../update/state.js";
|
|
2
|
+
import { type LogoVisibility, type UpdateState } from "../update/state.js";
|
|
3
3
|
import type { Step } from "../steps/index.js";
|
|
4
4
|
type RunUpdateCommandOptions = {
|
|
5
5
|
steps: Step[];
|
|
@@ -13,6 +13,7 @@ export type UpdateCommandResult = {
|
|
|
13
13
|
type ExitCodeWritable = {
|
|
14
14
|
exitCode: string | number | null | undefined;
|
|
15
15
|
};
|
|
16
|
+
export declare function shouldRenderRuntimeStateImmediately(state: UpdateState, lastRuntimeRenderTime: number, currentTime: number): boolean;
|
|
16
17
|
export declare function applyUpdateCommandResult(updateCommandResult: UpdateCommandResult, processWithExitCode: ExitCodeWritable): void;
|
|
17
18
|
export declare function runUpdateCommand(options: RunUpdateCommandOptions): Promise<UpdateCommandResult>;
|
|
18
19
|
export {};
|
package/dist/commands/update.js
CHANGED
|
@@ -6,9 +6,19 @@ import { hideCursorDuringExecution } from "../ui/terminal.js";
|
|
|
6
6
|
import { runUpdateEngine } from "../update/engine.js";
|
|
7
7
|
import { countFailedSteps } from "../update/state.js";
|
|
8
8
|
import { cancelActiveShellCommand } from "../runner.js";
|
|
9
|
+
const RUNTIME_RENDER_INTERVAL_MS = 100;
|
|
9
10
|
function shouldCaptureRuntimeAbortKey(state) {
|
|
10
11
|
return state?.currentPhase === "running" || state?.currentPhase === "installing";
|
|
11
12
|
}
|
|
13
|
+
function shouldThrottleRuntimeRender(state) {
|
|
14
|
+
return state.runCompletion !== "finished" && shouldCaptureRuntimeAbortKey(state);
|
|
15
|
+
}
|
|
16
|
+
export function shouldRenderRuntimeStateImmediately(state, lastRuntimeRenderTime, currentTime) {
|
|
17
|
+
if (!shouldThrottleRuntimeRender(state)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return currentTime - lastRuntimeRenderTime >= RUNTIME_RENDER_INTERVAL_MS;
|
|
21
|
+
}
|
|
12
22
|
export function applyUpdateCommandResult(updateCommandResult, processWithExitCode) {
|
|
13
23
|
if (updateCommandResult.failedStepCount > 0) {
|
|
14
24
|
processWithExitCode.exitCode = 1;
|
|
@@ -22,6 +32,25 @@ export async function runUpdateCommand(options) {
|
|
|
22
32
|
let runtimeAbortKeyCaptureEnabled = false;
|
|
23
33
|
let animationFrameIndex = 0;
|
|
24
34
|
let updateCommandResult = { failedStepCount: 0 };
|
|
35
|
+
let lastRuntimeRenderTime = 0;
|
|
36
|
+
const renderState = (state) => {
|
|
37
|
+
renderScreen(buildScreenWithAnimationFrame(state, animationFrameIndex));
|
|
38
|
+
if (shouldThrottleRuntimeRender(state)) {
|
|
39
|
+
lastRuntimeRenderTime = Date.now();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const renderStateWithRuntimeThrottle = (state) => {
|
|
43
|
+
if (!shouldThrottleRuntimeRender(state)) {
|
|
44
|
+
renderState(state);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const currentTime = Date.now();
|
|
48
|
+
if (shouldRenderRuntimeStateImmediately(state, lastRuntimeRenderTime, currentTime)) {
|
|
49
|
+
renderState(state);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// The animation timer renders the latest runtime state at the next frame.
|
|
53
|
+
};
|
|
25
54
|
const handleRuntimeKeypress = (typedCharacter, key) => {
|
|
26
55
|
if (key?.ctrl && key?.name === "c") {
|
|
27
56
|
process.stdout.write("\x1b[?25h\n");
|
|
@@ -67,8 +96,8 @@ export async function runUpdateCommand(options) {
|
|
|
67
96
|
return;
|
|
68
97
|
}
|
|
69
98
|
animationFrameIndex += 1;
|
|
70
|
-
|
|
71
|
-
},
|
|
99
|
+
renderState(latestState);
|
|
100
|
+
}, RUNTIME_RENDER_INTERVAL_MS);
|
|
72
101
|
try {
|
|
73
102
|
const finalState = await runUpdateEngine({
|
|
74
103
|
steps: options.steps,
|
|
@@ -85,7 +114,7 @@ export async function runUpdateCommand(options) {
|
|
|
85
114
|
else {
|
|
86
115
|
disableRuntimeAbortKeyCapture();
|
|
87
116
|
}
|
|
88
|
-
|
|
117
|
+
renderStateWithRuntimeThrottle(state);
|
|
89
118
|
},
|
|
90
119
|
});
|
|
91
120
|
finalScreen = buildScreen(finalState);
|
package/dist/config.d.ts
CHANGED
|
@@ -7,10 +7,18 @@ export type BraeburnConfig = {
|
|
|
7
7
|
logo?: boolean;
|
|
8
8
|
defaultsProfile?: ConfigDefaultsProfile;
|
|
9
9
|
};
|
|
10
|
+
export declare class ConfigReadError extends Error {
|
|
11
|
+
readonly configPath: string;
|
|
12
|
+
readonly originalError: unknown;
|
|
13
|
+
constructor(configPath: string, originalError: unknown);
|
|
14
|
+
}
|
|
15
|
+
export declare function parseConfig(rawConfig: string): BraeburnConfig;
|
|
10
16
|
export declare function resolveConfigPath(): Promise<string>;
|
|
11
17
|
export declare function configFileExists(): Promise<boolean>;
|
|
12
18
|
export declare function readConfig(): Promise<BraeburnConfig>;
|
|
19
|
+
export declare function readConfigFromPath(configPath: string): Promise<BraeburnConfig>;
|
|
13
20
|
export declare function writeConfig(config: BraeburnConfig): Promise<void>;
|
|
21
|
+
export declare function writeConfigToPath(configPath: string, config: BraeburnConfig): Promise<void>;
|
|
14
22
|
export declare function isSettingEnabled(config: BraeburnConfig, settingId: string): boolean;
|
|
15
23
|
export declare function isStepEnabled(config: BraeburnConfig, stepId: string): boolean;
|
|
16
24
|
export declare function isLogoEnabled(config: BraeburnConfig): boolean;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, access } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { parse, stringify } from "smol-toml";
|
|
5
5
|
export const PROTECTED_STEP_IDS = new Set(["homebrew"]);
|
|
@@ -9,14 +9,67 @@ const EMPTY_CONFIG = { steps: {} };
|
|
|
9
9
|
const LOGO_SETTING_ID = "logo";
|
|
10
10
|
const DEFAULTS_PROFILE_SETTING_ID = "defaultsProfile";
|
|
11
11
|
const LEGACY_PROFILE = "legacy";
|
|
12
|
+
export class ConfigReadError extends Error {
|
|
13
|
+
configPath;
|
|
14
|
+
originalError;
|
|
15
|
+
constructor(configPath, originalError) {
|
|
16
|
+
const reason = originalError instanceof Error ? originalError.message : String(originalError);
|
|
17
|
+
super(`Could not read braeburn config at ${configPath}: ${reason}`);
|
|
18
|
+
this.name = "ConfigReadError";
|
|
19
|
+
this.configPath = configPath;
|
|
20
|
+
this.originalError = originalError;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
12
23
|
function resolveDefaultsProfile(config) {
|
|
13
24
|
return config.defaultsProfile ?? LEGACY_PROFILE;
|
|
14
25
|
}
|
|
15
26
|
function parseDefaultsProfile(rawValue) {
|
|
27
|
+
if (rawValue === undefined) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
16
30
|
if (rawValue === "legacy" || rawValue === "conservative-v2") {
|
|
17
31
|
return rawValue;
|
|
18
32
|
}
|
|
19
|
-
|
|
33
|
+
throw new Error(`"${DEFAULTS_PROFILE_SETTING_ID}" must be "legacy" or "conservative-v2".`);
|
|
34
|
+
}
|
|
35
|
+
function parseSteps(rawValue) {
|
|
36
|
+
if (rawValue === undefined) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
if (typeof rawValue !== "object" || rawValue === null || Array.isArray(rawValue)) {
|
|
40
|
+
throw new Error('"steps" must be a table of step IDs to boolean values.');
|
|
41
|
+
}
|
|
42
|
+
const steps = {};
|
|
43
|
+
for (const [stepId, enabled] of Object.entries(rawValue)) {
|
|
44
|
+
if (typeof enabled !== "boolean") {
|
|
45
|
+
throw new Error(`"steps.${stepId}" must be true or false.`);
|
|
46
|
+
}
|
|
47
|
+
steps[stepId] = enabled;
|
|
48
|
+
}
|
|
49
|
+
return steps;
|
|
50
|
+
}
|
|
51
|
+
function parseLogo(rawValue) {
|
|
52
|
+
if (rawValue === undefined || typeof rawValue === "boolean") {
|
|
53
|
+
return rawValue;
|
|
54
|
+
}
|
|
55
|
+
throw new Error('"logo" must be true or false.');
|
|
56
|
+
}
|
|
57
|
+
function isMissingFileError(error) {
|
|
58
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
59
|
+
}
|
|
60
|
+
export function parseConfig(rawConfig) {
|
|
61
|
+
const parsed = parse(rawConfig);
|
|
62
|
+
const steps = parseSteps(parsed.steps);
|
|
63
|
+
const logo = parseLogo(parsed.logo);
|
|
64
|
+
const defaultsProfile = parseDefaultsProfile(parsed[DEFAULTS_PROFILE_SETTING_ID]);
|
|
65
|
+
const config = { steps };
|
|
66
|
+
if (logo !== undefined) {
|
|
67
|
+
config.logo = logo;
|
|
68
|
+
}
|
|
69
|
+
if (defaultsProfile !== undefined) {
|
|
70
|
+
config.defaultsProfile = defaultsProfile;
|
|
71
|
+
}
|
|
72
|
+
return config;
|
|
20
73
|
}
|
|
21
74
|
function isStepEnabledByDefault(profile, stepId) {
|
|
22
75
|
if (profile === "conservative-v2") {
|
|
@@ -49,20 +102,26 @@ export async function configFileExists() {
|
|
|
49
102
|
}
|
|
50
103
|
export async function readConfig() {
|
|
51
104
|
const configPath = await resolveConfigPath();
|
|
105
|
+
return readConfigFromPath(configPath);
|
|
106
|
+
}
|
|
107
|
+
export async function readConfigFromPath(configPath) {
|
|
52
108
|
try {
|
|
53
109
|
const raw = await readFile(configPath, "utf-8");
|
|
54
|
-
|
|
55
|
-
const parsedSteps = parsed.steps;
|
|
56
|
-
const defaultsProfile = parseDefaultsProfile(parsed[DEFAULTS_PROFILE_SETTING_ID]);
|
|
57
|
-
return { steps: parsedSteps ?? {}, logo: parsed.logo, defaultsProfile };
|
|
110
|
+
return parseConfig(raw);
|
|
58
111
|
}
|
|
59
|
-
catch {
|
|
60
|
-
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (isMissingFileError(error)) {
|
|
114
|
+
return structuredClone(EMPTY_CONFIG);
|
|
115
|
+
}
|
|
116
|
+
throw new ConfigReadError(configPath, error);
|
|
61
117
|
}
|
|
62
118
|
}
|
|
63
119
|
export async function writeConfig(config) {
|
|
64
120
|
const configPath = await resolveConfigPath();
|
|
65
|
-
await
|
|
121
|
+
await writeConfigToPath(configPath, config);
|
|
122
|
+
}
|
|
123
|
+
export async function writeConfigToPath(configPath, config) {
|
|
124
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
66
125
|
await writeFile(configPath, stringify(config), "utf-8");
|
|
67
126
|
}
|
|
68
127
|
export function isSettingEnabled(config, settingId) {
|
package/dist/index.js
CHANGED
|
@@ -1,179 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { ALL_STEPS } from "./steps/catalog.js";
|
|
7
|
-
import { applyUpdateCommandResult, runUpdateCommand } from "./commands/update.js";
|
|
8
|
-
import { runLogCommand, runLogListCommand } from "./commands/log.js";
|
|
9
|
-
import { runConfigCommand, runConfigUpdateCommand } from "./commands/config.js";
|
|
10
|
-
import { runSetupCommand } from "./commands/setup.js";
|
|
11
|
-
import { readConfig, isStepEnabled, isLogoEnabled, PROTECTED_STEP_IDS, configFileExists } from "./config.js";
|
|
12
|
-
const STEP_IDS_BY_NAME = new Map(ALL_STEPS.map((step) => [step.id, step]));
|
|
13
|
-
const requireFromThis = createRequire(import.meta.url);
|
|
14
|
-
const packageJson = requireFromThis(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"));
|
|
15
|
-
const BRAEBURN_VERSION = packageJson.version;
|
|
16
|
-
const program = new Command();
|
|
17
|
-
program
|
|
18
|
-
.name("braeburn")
|
|
19
|
-
.description("macOS system updater")
|
|
20
|
-
.version(BRAEBURN_VERSION)
|
|
21
|
-
.helpCommand(false);
|
|
22
|
-
program
|
|
23
|
-
.command("update", { isDefault: true })
|
|
24
|
-
.description("Run system update steps (default command)")
|
|
25
|
-
.argument("[steps...]", `Steps to run — omit to run all.\nAvailable: ${ALL_STEPS.map((step) => step.id).join(", ")}`)
|
|
26
|
-
.option("-y, --yes", "Auto-accept all prompts (default yes to everything)")
|
|
27
|
-
.option("-f, --force", "Alias for --yes")
|
|
28
|
-
.option("--no-logo", "Hide the logo")
|
|
29
|
-
.addHelpText("after", `
|
|
30
|
-
Step descriptions:
|
|
31
|
-
System / Runtimes (default: off — larger changes, enabled intentionally):
|
|
32
|
-
pyenv Upgrade pyenv, install latest Python 3.x (requires: pyenv or brew)
|
|
33
|
-
nvm Install latest Node.js via nvm (requires: ~/.nvm)
|
|
34
|
-
|
|
35
|
-
System / Apps & Packages:
|
|
36
|
-
homebrew Update Homebrew itself and all installed formulae
|
|
37
|
-
mas Upgrade Mac App Store apps (requires: mas)
|
|
38
|
-
macos Check for macOS updates, prompt to install
|
|
39
|
-
|
|
40
|
-
System / CLI Tools:
|
|
41
|
-
npm Update global npm packages (requires: npm)
|
|
42
|
-
braeburn Update braeburn CLI itself (requires: npm)
|
|
43
|
-
pip Update global pip3 packages (requires: pip3) ⚠ may be fragile
|
|
44
|
-
dotnet Update .NET global tools (requires: dotnet)
|
|
45
|
-
|
|
46
|
-
System / Shell:
|
|
47
|
-
ohmyzsh Update Oh My Zsh (requires: ~/.oh-my-zsh)
|
|
48
|
-
|
|
49
|
-
System / Maintenance:
|
|
50
|
-
cleanup homebrew cleanup (remove outdated Homebrew cache/downloads)
|
|
51
|
-
|
|
52
|
-
Examples:
|
|
53
|
-
braeburn Run all enabled steps interactively
|
|
54
|
-
braeburn -y Run all enabled steps, auto-accept everything
|
|
55
|
-
braeburn -fy Same as above
|
|
56
|
-
braeburn homebrew npm Run only the homebrew and npm steps
|
|
57
|
-
braeburn homebrew -y Run only homebrew, auto-accept
|
|
58
|
-
braeburn nvm pyenv Run only the runtime steps
|
|
59
|
-
`)
|
|
60
|
-
.action(async (stepArguments, options) => {
|
|
61
|
-
const autoYes = options.yes === true || options.force === true;
|
|
62
|
-
if (!(await configFileExists())) {
|
|
63
|
-
await runSetupCommand(ALL_STEPS);
|
|
64
|
-
}
|
|
65
|
-
const config = await readConfig();
|
|
66
|
-
let stepsToRun = stepArguments.length === 0
|
|
67
|
-
? ALL_STEPS
|
|
68
|
-
: resolveStepsByIds(stepArguments);
|
|
69
|
-
if (stepArguments.length === 0) {
|
|
70
|
-
stepsToRun = stepsToRun.filter((step) => isStepEnabled(config, step.id));
|
|
71
|
-
}
|
|
72
|
-
const logoIsEnabled = options.logo !== false && isLogoEnabled(config);
|
|
73
|
-
const updateCommandResult = await runUpdateCommand({
|
|
74
|
-
steps: stepsToRun,
|
|
75
|
-
promptMode: autoYes ? "auto-accept" : "interactive",
|
|
76
|
-
logoVisibility: logoIsEnabled ? "visible" : "hidden",
|
|
77
|
-
version: BRAEBURN_VERSION,
|
|
78
|
-
});
|
|
79
|
-
applyUpdateCommandResult(updateCommandResult, process);
|
|
80
|
-
});
|
|
81
|
-
program
|
|
82
|
-
.command("log")
|
|
83
|
-
.description("View the most recent output log for a given step")
|
|
84
|
-
.argument("[step]", "Step ID to view logs for (e.g. homebrew, npm, pip)")
|
|
85
|
-
.option("--homebrew", "Show latest Homebrew log")
|
|
86
|
-
.option("--mas", "Show latest Mac App Store log")
|
|
87
|
-
.option("--ohmyzsh", "Show latest Oh My Zsh log")
|
|
88
|
-
.option("--npm", "Show latest npm log")
|
|
89
|
-
.option("--braeburn", "Show latest braeburn log")
|
|
90
|
-
.option("--pip", "Show latest pip3 log")
|
|
91
|
-
.option("--pyenv", "Show latest pyenv log")
|
|
92
|
-
.option("--nvm", "Show latest nvm log")
|
|
93
|
-
.option("--dotnet", "Show latest .NET log")
|
|
94
|
-
.option("--macos", "Show latest macOS update log")
|
|
95
|
-
.option("--cleanup", "Show latest homebrew cleanup log")
|
|
96
|
-
.addHelpText("after", `
|
|
97
|
-
Examples:
|
|
98
|
-
braeburn log List all available step logs
|
|
99
|
-
braeburn log homebrew Show the latest Homebrew run log
|
|
100
|
-
braeburn log --brew Same as above
|
|
101
|
-
braeburn log npm | less Pipe log output through less
|
|
102
|
-
`)
|
|
103
|
-
.action((stepArgument, options) => {
|
|
104
|
-
const stepIdFromFlag = ALL_STEPS.map((step) => step.id).find((stepId) => options[stepId] === true);
|
|
105
|
-
const resolvedStepId = stepArgument ?? stepIdFromFlag;
|
|
106
|
-
if (!resolvedStepId) {
|
|
107
|
-
runLogListCommand();
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
runLogCommand({ stepId: resolvedStepId });
|
|
111
|
-
});
|
|
112
|
-
const configCommand = program
|
|
113
|
-
.command("config")
|
|
114
|
-
.description("View or edit braeburn configuration")
|
|
115
|
-
.action(() => {
|
|
116
|
-
configCommand.outputHelp();
|
|
117
|
-
});
|
|
118
|
-
configCommand
|
|
119
|
-
.command("list")
|
|
120
|
-
.description("Print current configuration")
|
|
121
|
-
.action(async () => {
|
|
122
|
-
await runConfigCommand({
|
|
123
|
-
allSteps: ALL_STEPS,
|
|
124
|
-
outputMode: "non-interactive",
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
const configurableSteps = ALL_STEPS.filter((step) => !PROTECTED_STEP_IDS.has(step.id));
|
|
128
|
-
const configUpdateCommand = configCommand
|
|
129
|
-
.command("update")
|
|
130
|
-
.description("Edit configuration (interactive by default, flags for direct updates)")
|
|
131
|
-
.addHelpText("after", `
|
|
132
|
-
Examples:
|
|
133
|
-
braeburn config update Open interactive config editor
|
|
134
|
-
braeburn config update --no-logo Hide the logo
|
|
135
|
-
braeburn config update --no-ohmyzsh Disable Oh My Zsh updates
|
|
136
|
-
braeburn config update --no-pip --no-nvm Disable pip and nvm updates
|
|
137
|
-
braeburn config update --ohmyzsh Re-enable Oh My Zsh updates
|
|
138
|
-
`);
|
|
139
|
-
configUpdateCommand.option(`--no-logo`, `Hide the logo`);
|
|
140
|
-
configUpdateCommand.option(`--logo`, `Show the logo`);
|
|
141
|
-
for (const step of configurableSteps) {
|
|
142
|
-
configUpdateCommand.option(`--no-${step.id}`, `Disable ${step.name} updates`);
|
|
143
|
-
configUpdateCommand.option(`--${step.id}`, `Enable ${step.name} updates`);
|
|
144
|
-
}
|
|
145
|
-
configUpdateCommand.action(async function () {
|
|
146
|
-
// Commander defaults --no-* to true, so we use getOptionValueSource to detect explicit CLI flags.
|
|
147
|
-
const settingUpdates = {};
|
|
148
|
-
for (const step of configurableSteps) {
|
|
149
|
-
const source = configUpdateCommand.getOptionValueSource(step.id);
|
|
150
|
-
if (source === "cli") {
|
|
151
|
-
settingUpdates[step.id] = configUpdateCommand.opts()[step.id] ? "enable" : "disable";
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const logoSource = configUpdateCommand.getOptionValueSource("logo");
|
|
155
|
-
if (logoSource === "cli") {
|
|
156
|
-
settingUpdates["logo"] = configUpdateCommand.opts().logo ? "enable" : "disable";
|
|
157
|
-
}
|
|
158
|
-
if (Object.keys(settingUpdates).length === 0) {
|
|
159
|
-
await runConfigCommand({
|
|
160
|
-
allSteps: ALL_STEPS,
|
|
161
|
-
outputMode: "interactive",
|
|
162
|
-
});
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
await runConfigUpdateCommand({ settingUpdates, allSteps: ALL_STEPS });
|
|
166
|
-
});
|
|
167
|
-
function resolveStepsByIds(stepIds) {
|
|
168
|
-
const resolvedSteps = [];
|
|
169
|
-
for (const stepId of stepIds) {
|
|
170
|
-
const step = STEP_IDS_BY_NAME.get(stepId);
|
|
171
|
-
if (!step) {
|
|
172
|
-
process.stderr.write(`Unknown step: "${stepId}". Run braeburn --help to see available steps.\n`);
|
|
173
|
-
process.exit(1);
|
|
174
|
-
}
|
|
175
|
-
resolvedSteps.push(step);
|
|
176
|
-
}
|
|
177
|
-
return resolvedSteps;
|
|
178
|
-
}
|
|
179
|
-
program.parse();
|
|
2
|
+
import { runBraeburnCli } from "./cli.js";
|
|
3
|
+
await runBraeburnCli();
|
package/dist/runner.d.ts
CHANGED
|
@@ -10,11 +10,17 @@ type RunCommandOptions = {
|
|
|
10
10
|
onOutputLine: OutputLineCallback;
|
|
11
11
|
logWriter: StepLogWriter;
|
|
12
12
|
};
|
|
13
|
+
export type ShellCommandRunner = {
|
|
14
|
+
cancelActiveShellCommand: () => boolean;
|
|
15
|
+
runShellCommand: (options: RunCommandOptions) => Promise<void>;
|
|
16
|
+
runShellCommandAndCaptureOutput: (options: RunCommandOptions) => Promise<string>;
|
|
17
|
+
};
|
|
13
18
|
export declare class ShellCommandCanceledError extends Error {
|
|
14
19
|
readonly shellCommand: string;
|
|
15
20
|
readonly originalError: unknown;
|
|
16
21
|
constructor(shellCommand: string, originalError: unknown);
|
|
17
22
|
}
|
|
23
|
+
export declare function createShellCommandRunner(): ShellCommandRunner;
|
|
18
24
|
export declare function cancelActiveShellCommand(): boolean;
|
|
19
25
|
export declare function runShellCommand(options: RunCommandOptions): Promise<void>;
|
|
20
26
|
export declare function runShellCommandAndCaptureOutput(options: RunCommandOptions): Promise<string>;
|
package/dist/runner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
2
|
const FAILURE_OUTPUT_TAIL_LINE_LIMIT = 20;
|
|
3
|
-
|
|
4
|
-
const
|
|
3
|
+
const DEFAULT_CAPTURE_MODE = "disabled";
|
|
4
|
+
const MAX_BUFFERED_OUTPUT_LINE_LENGTH = 16_384;
|
|
5
5
|
export class ShellCommandCanceledError extends Error {
|
|
6
6
|
shellCommand;
|
|
7
7
|
originalError;
|
|
@@ -12,21 +12,30 @@ export class ShellCommandCanceledError extends Error {
|
|
|
12
12
|
this.originalError = originalError;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
function createLimitedLineBuffer(lineLimit) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
return {
|
|
18
|
+
add(line) {
|
|
19
|
+
lines.push(line);
|
|
20
|
+
if (lines.length > lineLimit) {
|
|
21
|
+
lines.shift();
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
lines() {
|
|
25
|
+
return [...lines];
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function cancelActiveShellCommandForState(state) {
|
|
30
|
+
if (!state.activeShellCommandSubprocess) {
|
|
17
31
|
return false;
|
|
18
32
|
}
|
|
19
|
-
userCanceledSubprocesses.add(activeShellCommandSubprocess);
|
|
20
|
-
activeShellCommandSubprocess.kill("SIGTERM");
|
|
33
|
+
state.userCanceledSubprocesses.add(state.activeShellCommandSubprocess);
|
|
34
|
+
state.activeShellCommandSubprocess.kill("SIGTERM");
|
|
21
35
|
return true;
|
|
22
36
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
return text.split(/\r?\n|\r/).filter(Boolean);
|
|
28
|
-
}
|
|
29
|
-
function buildFailureSummaryLines(shellCommand, error) {
|
|
37
|
+
function buildFailureSummaryLines(details) {
|
|
38
|
+
const { shellCommand, error, stdoutTailLines, stderrTailLines } = details;
|
|
30
39
|
const defaultMessage = error instanceof Error ? error.message : String(error);
|
|
31
40
|
if (typeof error !== "object" || error === null) {
|
|
32
41
|
return [
|
|
@@ -48,12 +57,10 @@ function buildFailureSummaryLines(shellCommand, error) {
|
|
|
48
57
|
else {
|
|
49
58
|
summaryLines.push(`[braeburn] Error: ${defaultMessage}`);
|
|
50
59
|
}
|
|
51
|
-
const stderrTailLines = splitNonEmptyLines(errorDetails.stderr).slice(-FAILURE_OUTPUT_TAIL_LINE_LIMIT);
|
|
52
60
|
if (stderrTailLines.length > 0) {
|
|
53
61
|
summaryLines.push(`[braeburn] stderr tail (${stderrTailLines.length}):`);
|
|
54
62
|
summaryLines.push(...stderrTailLines.map((line) => ` ${line}`));
|
|
55
63
|
}
|
|
56
|
-
const stdoutTailLines = splitNonEmptyLines(errorDetails.stdout).slice(-FAILURE_OUTPUT_TAIL_LINE_LIMIT);
|
|
57
64
|
if (stdoutTailLines.length > 0) {
|
|
58
65
|
summaryLines.push(`[braeburn] stdout tail (${stdoutTailLines.length}):`);
|
|
59
66
|
summaryLines.push(...stdoutTailLines.map((line) => ` ${line}`));
|
|
@@ -73,6 +80,10 @@ function createBufferedLineEmitter(emitLine) {
|
|
|
73
80
|
}
|
|
74
81
|
emitLine(linePart);
|
|
75
82
|
}
|
|
83
|
+
while (pendingText.length > MAX_BUFFERED_OUTPUT_LINE_LENGTH) {
|
|
84
|
+
emitLine(pendingText.slice(0, MAX_BUFFERED_OUTPUT_LINE_LENGTH));
|
|
85
|
+
pendingText = pendingText.slice(MAX_BUFFERED_OUTPUT_LINE_LENGTH);
|
|
86
|
+
}
|
|
76
87
|
},
|
|
77
88
|
flushPendingLine() {
|
|
78
89
|
if (pendingText.length === 0) {
|
|
@@ -83,17 +94,46 @@ function createBufferedLineEmitter(emitLine) {
|
|
|
83
94
|
},
|
|
84
95
|
};
|
|
85
96
|
}
|
|
86
|
-
async function executeShellCommand(options) {
|
|
87
|
-
const capturedOutputLines = [];
|
|
97
|
+
async function executeShellCommand(state, options, captureMode = DEFAULT_CAPTURE_MODE) {
|
|
98
|
+
const capturedOutputLines = captureMode === "enabled" ? [] : undefined;
|
|
99
|
+
const stdoutTailLines = createLimitedLineBuffer(FAILURE_OUTPUT_TAIL_LINE_LIMIT);
|
|
100
|
+
const stderrTailLines = createLimitedLineBuffer(FAILURE_OUTPUT_TAIL_LINE_LIMIT);
|
|
101
|
+
let logWriteQueue = Promise.resolve();
|
|
102
|
+
let logWriteFailure;
|
|
88
103
|
const subprocess = execa("bash", ["-c", options.shellCommand], {
|
|
89
|
-
|
|
104
|
+
buffer: false,
|
|
90
105
|
reject: true,
|
|
91
106
|
});
|
|
92
|
-
activeShellCommandSubprocess = subprocess;
|
|
107
|
+
state.activeShellCommandSubprocess = subprocess;
|
|
108
|
+
const queueLogWrite = (line) => {
|
|
109
|
+
logWriteQueue = logWriteQueue.then(async () => {
|
|
110
|
+
if (logWriteFailure) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await options.logWriter(line);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
logWriteFailure = error;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const waitForQueuedLogWrites = async () => {
|
|
122
|
+
await logWriteQueue;
|
|
123
|
+
if (logWriteFailure) {
|
|
124
|
+
throw logWriteFailure;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
93
127
|
const emitOutputLine = (line) => {
|
|
94
|
-
|
|
128
|
+
if (line.source === "stdout") {
|
|
129
|
+
stdoutTailLines.add(line.text);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
stderrTailLines.add(line.text);
|
|
133
|
+
}
|
|
134
|
+
capturedOutputLines?.push(line.text);
|
|
95
135
|
options.onOutputLine(line);
|
|
96
|
-
|
|
136
|
+
queueLogWrite(line.text);
|
|
97
137
|
};
|
|
98
138
|
const stdoutEmitter = createBufferedLineEmitter((line) => {
|
|
99
139
|
emitOutputLine({ text: line, source: "stdout" });
|
|
@@ -111,36 +151,64 @@ async function executeShellCommand(options) {
|
|
|
111
151
|
await subprocess;
|
|
112
152
|
stdoutEmitter.flushPendingLine();
|
|
113
153
|
stderrEmitter.flushPendingLine();
|
|
114
|
-
|
|
154
|
+
await waitForQueuedLogWrites();
|
|
155
|
+
return { capturedOutput: capturedOutputLines?.join("\n") };
|
|
115
156
|
}
|
|
116
157
|
catch (error) {
|
|
117
158
|
stdoutEmitter.flushPendingLine();
|
|
118
159
|
stderrEmitter.flushPendingLine();
|
|
119
|
-
const commandWasCanceledByUser = userCanceledSubprocesses.has(subprocess);
|
|
120
|
-
const failureSummaryLines = buildFailureSummaryLines(
|
|
160
|
+
const commandWasCanceledByUser = state.userCanceledSubprocesses.has(subprocess);
|
|
161
|
+
const failureSummaryLines = buildFailureSummaryLines({
|
|
162
|
+
shellCommand: options.shellCommand,
|
|
163
|
+
error,
|
|
164
|
+
stdoutTailLines: stdoutTailLines.lines(),
|
|
165
|
+
stderrTailLines: stderrTailLines.lines(),
|
|
166
|
+
});
|
|
121
167
|
if (commandWasCanceledByUser) {
|
|
122
168
|
failureSummaryLines.push("[braeburn] Command canceled by user input (q).");
|
|
123
169
|
}
|
|
124
170
|
for (const line of failureSummaryLines) {
|
|
125
|
-
|
|
171
|
+
queueLogWrite(line);
|
|
126
172
|
}
|
|
173
|
+
await waitForQueuedLogWrites();
|
|
127
174
|
if (commandWasCanceledByUser) {
|
|
128
175
|
throw new ShellCommandCanceledError(options.shellCommand, error);
|
|
129
176
|
}
|
|
130
177
|
throw error;
|
|
131
178
|
}
|
|
132
179
|
finally {
|
|
133
|
-
if (activeShellCommandSubprocess === subprocess) {
|
|
134
|
-
activeShellCommandSubprocess = undefined;
|
|
180
|
+
if (state.activeShellCommandSubprocess === subprocess) {
|
|
181
|
+
state.activeShellCommandSubprocess = undefined;
|
|
135
182
|
}
|
|
136
183
|
}
|
|
137
184
|
}
|
|
185
|
+
export function createShellCommandRunner() {
|
|
186
|
+
const state = {
|
|
187
|
+
activeShellCommandSubprocess: undefined,
|
|
188
|
+
userCanceledSubprocesses: new WeakSet(),
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
cancelActiveShellCommand() {
|
|
192
|
+
return cancelActiveShellCommandForState(state);
|
|
193
|
+
},
|
|
194
|
+
async runShellCommand(options) {
|
|
195
|
+
await executeShellCommand(state, options);
|
|
196
|
+
},
|
|
197
|
+
async runShellCommandAndCaptureOutput(options) {
|
|
198
|
+
const result = await executeShellCommand(state, options, "enabled");
|
|
199
|
+
return result.capturedOutput ?? "";
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const defaultShellCommandRunner = createShellCommandRunner();
|
|
204
|
+
export function cancelActiveShellCommand() {
|
|
205
|
+
return defaultShellCommandRunner.cancelActiveShellCommand();
|
|
206
|
+
}
|
|
138
207
|
export async function runShellCommand(options) {
|
|
139
|
-
await
|
|
208
|
+
await defaultShellCommandRunner.runShellCommand(options);
|
|
140
209
|
}
|
|
141
210
|
export async function runShellCommandAndCaptureOutput(options) {
|
|
142
|
-
|
|
143
|
-
return result.capturedOutput;
|
|
211
|
+
return defaultShellCommandRunner.runShellCommandAndCaptureOutput(options);
|
|
144
212
|
}
|
|
145
213
|
export async function doesShellCommandSucceed(options) {
|
|
146
214
|
const result = await execa("bash", ["-c", options.shellCommand], {
|
package/dist/update/engine.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ type UpdateEngineDependencies = {
|
|
|
9
9
|
runCommand: typeof runShellCommand;
|
|
10
10
|
createStepRunContext: typeof createDefaultStepRunContext;
|
|
11
11
|
};
|
|
12
|
+
export declare const CURRENT_OUTPUT_LINE_LIMIT = 200;
|
|
12
13
|
type RunUpdateEngineOptions = {
|
|
13
14
|
steps: Step[];
|
|
14
15
|
promptMode: PromptMode;
|
package/dist/update/engine.js
CHANGED
|
@@ -3,6 +3,7 @@ import { runShellCommand, ShellCommandCanceledError, } from "../runner.js";
|
|
|
3
3
|
import { createDefaultStepRunContext } from "../steps/index.js";
|
|
4
4
|
import { toDisplaySteps } from "./displayStep.js";
|
|
5
5
|
import { createInitialUpdateState, } from "./state.js";
|
|
6
|
+
export const CURRENT_OUTPUT_LINE_LIMIT = 200;
|
|
6
7
|
function resolveDependencies(dependencyOverrides) {
|
|
7
8
|
return {
|
|
8
9
|
createLogWriter: dependencyOverrides?.createLogWriter ?? createLogWriterForStep,
|
|
@@ -26,6 +27,12 @@ async function resolvePrompt(promptMode, askForConfirmation) {
|
|
|
26
27
|
function reportState(state, onStateChanged) {
|
|
27
28
|
onStateChanged(state);
|
|
28
29
|
}
|
|
30
|
+
function appendCurrentOutputLine(state, line) {
|
|
31
|
+
state.currentOutputLines.push(line);
|
|
32
|
+
if (state.currentOutputLines.length > CURRENT_OUTPUT_LINE_LIMIT) {
|
|
33
|
+
state.currentOutputLines.splice(0, state.currentOutputLines.length - CURRENT_OUTPUT_LINE_LIMIT);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
29
36
|
function wasStepCanceledByUser(error) {
|
|
30
37
|
return error instanceof ShellCommandCanceledError;
|
|
31
38
|
}
|
|
@@ -90,7 +97,7 @@ export async function runUpdateEngine(options) {
|
|
|
90
97
|
await dependencies.runCommand({
|
|
91
98
|
shellCommand: `brew install ${step.brewPackageToInstall}`,
|
|
92
99
|
onOutputLine: (line) => {
|
|
93
|
-
state
|
|
100
|
+
appendCurrentOutputLine(state, line);
|
|
94
101
|
reportState(state, options.onStateChanged);
|
|
95
102
|
},
|
|
96
103
|
logWriter: installLogWriter,
|
|
@@ -130,7 +137,7 @@ export async function runUpdateEngine(options) {
|
|
|
130
137
|
const stepLogWriter = await dependencies.createLogWriter(step.id);
|
|
131
138
|
try {
|
|
132
139
|
await step.run(dependencies.createStepRunContext((line) => {
|
|
133
|
-
state
|
|
140
|
+
appendCurrentOutputLine(state, line);
|
|
134
141
|
reportState(state, options.onStateChanged);
|
|
135
142
|
}, stepLogWriter));
|
|
136
143
|
state.currentPhase = "complete";
|