braeburn 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/commands/update.js +47 -0
- package/dist/config.js +1 -1
- package/dist/index.js +2 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.js +33 -0
- package/dist/steps/braeburn.d.ts +3 -0
- package/dist/steps/braeburn.js +14 -0
- package/dist/steps/catalog.js +3 -1
- package/dist/steps/index.d.ts +1 -0
- package/dist/steps/index.js +1 -0
- package/dist/ui/currentStep.js +3 -1
- package/dist/ui/header.js +3 -1
- package/dist/ui/prompt.d.ts +1 -1
- package/dist/ui/prompt.js +5 -2
- package/dist/update/engine.d.ts +1 -1
- package/dist/update/engine.js +20 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ Steps are grouped by system capability. For new setups, braeburn uses a conserva
|
|
|
42
42
|
| `mas` | Apps & Packages | off | `mas` |
|
|
43
43
|
| `macos` | Apps & Packages | off | — |
|
|
44
44
|
| `npm` | CLI Tools | on | `npm` |
|
|
45
|
+
| `braeburn` | CLI Tools | on | `npm` |
|
|
45
46
|
| `pip` | CLI Tools | on | `pip3` |
|
|
46
47
|
| `dotnet` | CLI Tools | on | `dotnet` |
|
|
47
48
|
| `ohmyzsh` | Shell | off | `~/.oh-my-zsh` |
|
package/dist/commands/update.js
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
1
2
|
import { collectVersions } from "../update/versionCollector.js";
|
|
2
3
|
import { captureYesNo } from "../ui/prompt.js";
|
|
3
4
|
import { buildScreen, buildScreenWithAnimationFrame, createScreenRenderer } from "../ui/screen.js";
|
|
4
5
|
import { hideCursorDuringExecution } from "../ui/terminal.js";
|
|
5
6
|
import { runUpdateEngine } from "../update/engine.js";
|
|
7
|
+
import { cancelActiveShellCommand } from "../runner.js";
|
|
8
|
+
function shouldCaptureRuntimeAbortKey(state) {
|
|
9
|
+
return state?.currentPhase === "running" || state?.currentPhase === "installing";
|
|
10
|
+
}
|
|
6
11
|
export async function runUpdateCommand(options) {
|
|
7
12
|
const renderScreen = createScreenRenderer();
|
|
8
13
|
const restoreCursor = hideCursorDuringExecution({ screenBuffer: "alternate" });
|
|
9
14
|
let finalScreen = "";
|
|
10
15
|
let latestState = undefined;
|
|
16
|
+
let runtimeAbortKeyCaptureEnabled = false;
|
|
11
17
|
let animationFrameIndex = 0;
|
|
18
|
+
const handleRuntimeKeypress = (typedCharacter, key) => {
|
|
19
|
+
if (key?.ctrl && key?.name === "c") {
|
|
20
|
+
process.stdout.write("\x1b[?25h\n");
|
|
21
|
+
process.exit(130);
|
|
22
|
+
}
|
|
23
|
+
const isQuitRequest = typedCharacter === "q" || typedCharacter === "Q";
|
|
24
|
+
if (!isQuitRequest) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
cancelActiveShellCommand();
|
|
28
|
+
};
|
|
29
|
+
const enableRuntimeAbortKeyCapture = () => {
|
|
30
|
+
if (runtimeAbortKeyCaptureEnabled) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
readline.emitKeypressEvents(process.stdin);
|
|
34
|
+
process.stdin.on("keypress", handleRuntimeKeypress);
|
|
35
|
+
if (process.stdin.isTTY) {
|
|
36
|
+
process.stdin.setRawMode(true);
|
|
37
|
+
}
|
|
38
|
+
process.stdin.resume();
|
|
39
|
+
runtimeAbortKeyCaptureEnabled = true;
|
|
40
|
+
};
|
|
41
|
+
const disableRuntimeAbortKeyCapture = () => {
|
|
42
|
+
if (!runtimeAbortKeyCaptureEnabled) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
process.stdin.removeListener("keypress", handleRuntimeKeypress);
|
|
46
|
+
if (process.stdin.isTTY) {
|
|
47
|
+
process.stdin.setRawMode(false);
|
|
48
|
+
}
|
|
49
|
+
process.stdin.pause();
|
|
50
|
+
runtimeAbortKeyCaptureEnabled = false;
|
|
51
|
+
};
|
|
12
52
|
const animationTimer = setInterval(() => {
|
|
13
53
|
if (!latestState) {
|
|
14
54
|
return;
|
|
@@ -32,12 +72,19 @@ export async function runUpdateCommand(options) {
|
|
|
32
72
|
collectVersions,
|
|
33
73
|
onStateChanged: (state) => {
|
|
34
74
|
latestState = state;
|
|
75
|
+
if (shouldCaptureRuntimeAbortKey(state)) {
|
|
76
|
+
enableRuntimeAbortKeyCapture();
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
disableRuntimeAbortKeyCapture();
|
|
80
|
+
}
|
|
35
81
|
renderScreen(buildScreenWithAnimationFrame(state, animationFrameIndex));
|
|
36
82
|
},
|
|
37
83
|
});
|
|
38
84
|
finalScreen = buildScreen(finalState);
|
|
39
85
|
}
|
|
40
86
|
finally {
|
|
87
|
+
disableRuntimeAbortKeyCapture();
|
|
41
88
|
clearInterval(animationTimer);
|
|
42
89
|
restoreCursor();
|
|
43
90
|
}
|
package/dist/config.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { parse, stringify } from "smol-toml";
|
|
5
5
|
export const PROTECTED_STEP_IDS = new Set(["homebrew"]);
|
|
6
6
|
export const LEGACY_DEFAULT_OFF_STEP_IDS = new Set(["nvm", "pyenv"]);
|
|
7
|
-
export const CONSERVATIVE_DEFAULT_ON_STEP_IDS = new Set(["homebrew", "npm", "pip", "dotnet"]);
|
|
7
|
+
export const CONSERVATIVE_DEFAULT_ON_STEP_IDS = new Set(["homebrew", "npm", "braeburn", "pip", "dotnet"]);
|
|
8
8
|
const EMPTY_CONFIG = { steps: {} };
|
|
9
9
|
const LOGO_SETTING_ID = "logo";
|
|
10
10
|
const DEFAULTS_PROFILE_SETTING_ID = "defaultsProfile";
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ Step descriptions:
|
|
|
39
39
|
|
|
40
40
|
System / CLI Tools:
|
|
41
41
|
npm Update global npm packages (requires: npm)
|
|
42
|
+
braeburn Update braeburn CLI itself (requires: npm)
|
|
42
43
|
pip Update global pip3 packages (requires: pip3) ⚠ may be fragile
|
|
43
44
|
dotnet Update .NET global tools (requires: dotnet)
|
|
44
45
|
|
|
@@ -84,6 +85,7 @@ program
|
|
|
84
85
|
.option("--mas", "Show latest Mac App Store log")
|
|
85
86
|
.option("--ohmyzsh", "Show latest Oh My Zsh log")
|
|
86
87
|
.option("--npm", "Show latest npm log")
|
|
88
|
+
.option("--braeburn", "Show latest braeburn log")
|
|
87
89
|
.option("--pip", "Show latest pip3 log")
|
|
88
90
|
.option("--pyenv", "Show latest pyenv log")
|
|
89
91
|
.option("--nvm", "Show latest nvm log")
|
package/dist/runner.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ type RunCommandOptions = {
|
|
|
10
10
|
onOutputLine: OutputLineCallback;
|
|
11
11
|
logWriter: StepLogWriter;
|
|
12
12
|
};
|
|
13
|
+
export declare class ShellCommandCanceledError extends Error {
|
|
14
|
+
readonly shellCommand: string;
|
|
15
|
+
readonly originalError: unknown;
|
|
16
|
+
constructor(shellCommand: string, originalError: unknown);
|
|
17
|
+
}
|
|
18
|
+
export declare function cancelActiveShellCommand(): boolean;
|
|
13
19
|
export declare function runShellCommand(options: RunCommandOptions): Promise<void>;
|
|
14
20
|
type CheckCommandOptions = {
|
|
15
21
|
shellCommand: string;
|
package/dist/runner.js
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
2
|
const FAILURE_OUTPUT_TAIL_LINE_LIMIT = 20;
|
|
3
|
+
let activeShellCommandSubprocess;
|
|
4
|
+
const userCanceledSubprocesses = new WeakSet();
|
|
5
|
+
export class ShellCommandCanceledError extends Error {
|
|
6
|
+
shellCommand;
|
|
7
|
+
originalError;
|
|
8
|
+
constructor(shellCommand, originalError) {
|
|
9
|
+
super(`Command canceled by user: ${shellCommand}`);
|
|
10
|
+
this.name = "ShellCommandCanceledError";
|
|
11
|
+
this.shellCommand = shellCommand;
|
|
12
|
+
this.originalError = originalError;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function cancelActiveShellCommand() {
|
|
16
|
+
if (!activeShellCommandSubprocess) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
userCanceledSubprocesses.add(activeShellCommandSubprocess);
|
|
20
|
+
activeShellCommandSubprocess.kill("SIGTERM");
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
3
23
|
function splitNonEmptyLines(text) {
|
|
4
24
|
if (!text) {
|
|
5
25
|
return [];
|
|
@@ -45,6 +65,7 @@ export async function runShellCommand(options) {
|
|
|
45
65
|
all: true,
|
|
46
66
|
reject: true,
|
|
47
67
|
});
|
|
68
|
+
activeShellCommandSubprocess = subprocess;
|
|
48
69
|
subprocess.stdout?.on("data", (chunk) => {
|
|
49
70
|
const lines = String(chunk).split(/\r?\n|\r/).filter(Boolean);
|
|
50
71
|
for (const line of lines) {
|
|
@@ -63,12 +84,24 @@ export async function runShellCommand(options) {
|
|
|
63
84
|
await subprocess;
|
|
64
85
|
}
|
|
65
86
|
catch (error) {
|
|
87
|
+
const commandWasCanceledByUser = userCanceledSubprocesses.has(subprocess);
|
|
66
88
|
const failureSummaryLines = buildFailureSummaryLines(options.shellCommand, error);
|
|
89
|
+
if (commandWasCanceledByUser) {
|
|
90
|
+
failureSummaryLines.push("[braeburn] Command canceled by user input (q).");
|
|
91
|
+
}
|
|
67
92
|
for (const line of failureSummaryLines) {
|
|
68
93
|
await options.logWriter(line);
|
|
69
94
|
}
|
|
95
|
+
if (commandWasCanceledByUser) {
|
|
96
|
+
throw new ShellCommandCanceledError(options.shellCommand, error);
|
|
97
|
+
}
|
|
70
98
|
throw error;
|
|
71
99
|
}
|
|
100
|
+
finally {
|
|
101
|
+
if (activeShellCommandSubprocess === subprocess) {
|
|
102
|
+
activeShellCommandSubprocess = undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
72
105
|
}
|
|
73
106
|
export async function doesShellCommandSucceed(options) {
|
|
74
107
|
const result = await execa("bash", ["-c", options.shellCommand], {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { checkCommandExists } from "./runtime.js";
|
|
2
|
+
const braeburnStep = {
|
|
3
|
+
id: "braeburn",
|
|
4
|
+
name: "braeburn",
|
|
5
|
+
categoryId: "cli-tools",
|
|
6
|
+
description: "Update the braeburn CLI to the latest npm release",
|
|
7
|
+
async checkIsAvailable() {
|
|
8
|
+
return checkCommandExists("npm");
|
|
9
|
+
},
|
|
10
|
+
async run(context) {
|
|
11
|
+
await context.runStep("npm install -g braeburn@latest");
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export default braeburnStep;
|
package/dist/steps/catalog.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pyenvStep, nvmStep, homebrewStep, masStep, ohmyzshStep, npmStep, pipStep, dotnetStep, macosStep, cleanupStep, } from "./index.js";
|
|
1
|
+
import { pyenvStep, nvmStep, homebrewStep, masStep, ohmyzshStep, npmStep, pipStep, dotnetStep, macosStep, cleanupStep, braeburnStep, } from "./index.js";
|
|
2
2
|
const STEP_BY_ID = {
|
|
3
3
|
pyenv: pyenvStep,
|
|
4
4
|
nvm: nvmStep,
|
|
@@ -10,6 +10,7 @@ const STEP_BY_ID = {
|
|
|
10
10
|
dotnet: dotnetStep,
|
|
11
11
|
ohmyzsh: ohmyzshStep,
|
|
12
12
|
cleanup: cleanupStep,
|
|
13
|
+
braeburn: braeburnStep,
|
|
13
14
|
};
|
|
14
15
|
const STEP_EXECUTION_ORDER = [
|
|
15
16
|
"pyenv",
|
|
@@ -18,6 +19,7 @@ const STEP_EXECUTION_ORDER = [
|
|
|
18
19
|
"mas",
|
|
19
20
|
"macos",
|
|
20
21
|
"npm",
|
|
22
|
+
"braeburn",
|
|
21
23
|
"pip",
|
|
22
24
|
"dotnet",
|
|
23
25
|
"ohmyzsh",
|
package/dist/steps/index.d.ts
CHANGED
|
@@ -14,3 +14,4 @@ export { default as nvmStep } from "./nvm.js";
|
|
|
14
14
|
export { default as dotnetStep } from "./dotnet.js";
|
|
15
15
|
export { default as macosStep } from "./macos.js";
|
|
16
16
|
export { default as cleanupStep } from "./cleanup.js";
|
|
17
|
+
export { default as braeburnStep } from "./braeburn.js";
|
package/dist/steps/index.js
CHANGED
|
@@ -11,3 +11,4 @@ export { default as nvmStep } from "./nvm.js";
|
|
|
11
11
|
export { default as dotnetStep } from "./dotnet.js";
|
|
12
12
|
export { default as macosStep } from "./macos.js";
|
|
13
13
|
export { default as cleanupStep } from "./cleanup.js";
|
|
14
|
+
export { default as braeburnStep } from "./braeburn.js";
|
package/dist/ui/currentStep.js
CHANGED
|
@@ -11,7 +11,9 @@ export function buildActiveStepLines(options) {
|
|
|
11
11
|
` ${chalk.dim("·")} ${chalk.dim.italic(step.description)}`,
|
|
12
12
|
];
|
|
13
13
|
if (isRunning) {
|
|
14
|
-
const label = phase === "installing"
|
|
14
|
+
const label = phase === "installing"
|
|
15
|
+
? "Installing... press q to end this step"
|
|
16
|
+
: "Running... press q to end this step";
|
|
15
17
|
lines.push(` ${chalk.blue(getActivityIndicatorFrame(activityFrameIndex))} ${label}`);
|
|
16
18
|
}
|
|
17
19
|
return lines;
|
package/dist/ui/header.js
CHANGED
|
@@ -21,7 +21,9 @@ export function stepTrackerIcon(phase, activityFrameIndex = 0) {
|
|
|
21
21
|
return chalk.green("✓ ");
|
|
22
22
|
if (phase === "failed")
|
|
23
23
|
return chalk.red("✗ ");
|
|
24
|
-
if (phase === "skipped"
|
|
24
|
+
if (phase === "skipped")
|
|
25
|
+
return chalk.yellow("↷ ");
|
|
26
|
+
if (phase === "not-available")
|
|
25
27
|
return chalk.dim("– ");
|
|
26
28
|
if (phase === "running" || phase === "installing")
|
|
27
29
|
return chalk.cyan(`${getActivityIndicatorFrame(activityFrameIndex)} `);
|
package/dist/ui/prompt.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { CurrentPrompt } from "./state.js";
|
|
2
|
-
export type YesNoForceAnswer = "yes" | "no" | "force";
|
|
2
|
+
export type YesNoForceAnswer = "yes" | "no" | "skip" | "force";
|
|
3
3
|
export declare function captureYesNo(): Promise<YesNoForceAnswer>;
|
|
4
4
|
export declare function buildPromptLines(prompt: CurrentPrompt): string[];
|
package/dist/ui/prompt.js
CHANGED
|
@@ -13,8 +13,9 @@ export function captureYesNo() {
|
|
|
13
13
|
}
|
|
14
14
|
const isConfirm = character === "y" || character === "Y" || key?.name === "return";
|
|
15
15
|
const isDecline = character === "n" || character === "N";
|
|
16
|
+
const isSkip = character === "s" || character === "S";
|
|
16
17
|
const isForce = character === "f" || character === "F";
|
|
17
|
-
if (!isConfirm && !isDecline && !isForce)
|
|
18
|
+
if (!isConfirm && !isDecline && !isSkip && !isForce)
|
|
18
19
|
return;
|
|
19
20
|
process.stdin.removeListener("keypress", handleKeypress);
|
|
20
21
|
if (process.stdin.isTTY) {
|
|
@@ -23,6 +24,8 @@ export function captureYesNo() {
|
|
|
23
24
|
process.stdin.pause();
|
|
24
25
|
if (isForce)
|
|
25
26
|
resolve("force");
|
|
27
|
+
else if (isSkip)
|
|
28
|
+
resolve("skip");
|
|
26
29
|
else if (isConfirm)
|
|
27
30
|
resolve("yes");
|
|
28
31
|
else
|
|
@@ -38,6 +41,6 @@ export function buildPromptLines(prompt) {
|
|
|
38
41
|
lines.push(` ${chalk.yellow("⚠")} ${chalk.yellow(prompt.warning)}`);
|
|
39
42
|
lines.push("");
|
|
40
43
|
}
|
|
41
|
-
lines.push(` ${chalk.cyan("?")} ${prompt.question} ${chalk.dim("[Y/n/f]")}`);
|
|
44
|
+
lines.push(` ${chalk.cyan("?")} ${prompt.question} ${chalk.dim("[Y/n/s/f]")}`);
|
|
42
45
|
return lines;
|
|
43
46
|
}
|
package/dist/update/engine.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { runShellCommand } from "../runner.js";
|
|
|
3
3
|
import { createDefaultStepRunContext, type Step } from "../steps/index.js";
|
|
4
4
|
import { type LogoVisibility, type ResolvedVersion, type UpdateState } from "./state.js";
|
|
5
5
|
export type PromptMode = "interactive" | "auto-accept";
|
|
6
|
-
export type ConfirmationAnswer = "yes" | "no" | "force";
|
|
6
|
+
export type ConfirmationAnswer = "yes" | "no" | "skip" | "force";
|
|
7
7
|
type UpdateEngineDependencies = {
|
|
8
8
|
createLogWriter: (stepId: string) => Promise<StepLogWriter>;
|
|
9
9
|
runCommand: typeof runShellCommand;
|
package/dist/update/engine.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createLogWriterForStep } from "../logger.js";
|
|
2
|
-
import { runShellCommand } from "../runner.js";
|
|
2
|
+
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";
|
|
@@ -15,7 +15,7 @@ async function resolvePrompt(promptMode, askForConfirmation) {
|
|
|
15
15
|
return { promptMode: "auto-accept", decision: "run" };
|
|
16
16
|
}
|
|
17
17
|
const answer = await askForConfirmation();
|
|
18
|
-
if (answer === "no") {
|
|
18
|
+
if (answer === "no" || answer === "skip") {
|
|
19
19
|
return { promptMode: "interactive", decision: "skip" };
|
|
20
20
|
}
|
|
21
21
|
if (answer === "force") {
|
|
@@ -26,6 +26,9 @@ async function resolvePrompt(promptMode, askForConfirmation) {
|
|
|
26
26
|
function reportState(state, onStateChanged) {
|
|
27
27
|
onStateChanged(state);
|
|
28
28
|
}
|
|
29
|
+
function wasStepCanceledByUser(error) {
|
|
30
|
+
return error instanceof ShellCommandCanceledError;
|
|
31
|
+
}
|
|
29
32
|
export async function runUpdateEngine(options) {
|
|
30
33
|
const dependencies = resolveDependencies(options.dependencies);
|
|
31
34
|
const state = createInitialUpdateState(toDisplaySteps(options.steps), options.version, options.logoVisibility);
|
|
@@ -75,10 +78,16 @@ export async function runUpdateEngine(options) {
|
|
|
75
78
|
logWriter: installLogWriter,
|
|
76
79
|
});
|
|
77
80
|
}
|
|
78
|
-
catch {
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (wasStepCanceledByUser(error)) {
|
|
83
|
+
promptMode = "interactive";
|
|
84
|
+
}
|
|
79
85
|
state.currentPhase = "failed";
|
|
80
86
|
reportState(state, options.onStateChanged);
|
|
81
|
-
state.completedStepRecords.push({
|
|
87
|
+
state.completedStepRecords.push({
|
|
88
|
+
phase: "failed",
|
|
89
|
+
summaryNote: wasStepCanceledByUser(error) ? "canceled by user" : "install failed",
|
|
90
|
+
});
|
|
82
91
|
continue;
|
|
83
92
|
}
|
|
84
93
|
}
|
|
@@ -109,11 +118,17 @@ export async function runUpdateEngine(options) {
|
|
|
109
118
|
state.completedStepRecords.push({ phase: "complete", summaryNote: "updated" });
|
|
110
119
|
}
|
|
111
120
|
catch (error) {
|
|
121
|
+
if (wasStepCanceledByUser(error)) {
|
|
122
|
+
promptMode = "interactive";
|
|
123
|
+
}
|
|
112
124
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
113
125
|
state.currentPhase = "failed";
|
|
114
126
|
state.currentOutputLines = [];
|
|
115
127
|
reportState(state, options.onStateChanged);
|
|
116
|
-
state.completedStepRecords.push({
|
|
128
|
+
state.completedStepRecords.push({
|
|
129
|
+
phase: "failed",
|
|
130
|
+
summaryNote: wasStepCanceledByUser(error) ? "canceled by user" : errorMessage,
|
|
131
|
+
});
|
|
117
132
|
}
|
|
118
133
|
}
|
|
119
134
|
state.runCompletion = "finished";
|