braeburn 1.0.1 → 1.2.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/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +144 -0
- package/dist/commands/update.js +10 -3
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/index.js +6 -1
- package/dist/ui/prompt.d.ts +2 -1
- package/dist/ui/prompt.js +9 -3
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { writeConfig, PROTECTED_STEP_IDS } from "../config.js";
|
|
4
|
+
import { LOGO_ART } from "../logo.js";
|
|
5
|
+
// Module-level render state (scoped to this screen, separate from update screen)
|
|
6
|
+
let prevLines = 0;
|
|
7
|
+
function render(content) {
|
|
8
|
+
if (prevLines > 0) {
|
|
9
|
+
process.stdout.write(`\x1b[${prevLines}A\x1b[J`);
|
|
10
|
+
}
|
|
11
|
+
process.stdout.write(content);
|
|
12
|
+
prevLines = (content.match(/\n/g) ?? []).length;
|
|
13
|
+
}
|
|
14
|
+
function buildLoadingScreen() {
|
|
15
|
+
const lines = [
|
|
16
|
+
chalk.yellow(LOGO_ART),
|
|
17
|
+
"",
|
|
18
|
+
` ${chalk.bold.white("Welcome to braeburn!")}`,
|
|
19
|
+
"",
|
|
20
|
+
` ${chalk.dim("Checking which tools are installed\u2026")}`,
|
|
21
|
+
"",
|
|
22
|
+
];
|
|
23
|
+
return lines.join("\n") + "\n";
|
|
24
|
+
}
|
|
25
|
+
function buildSetupScreen(items, cursorIndex) {
|
|
26
|
+
const lines = [
|
|
27
|
+
chalk.yellow(LOGO_ART),
|
|
28
|
+
"",
|
|
29
|
+
` ${chalk.bold.white("Welcome to braeburn!")}`,
|
|
30
|
+
"",
|
|
31
|
+
` Select the update tools you\u2019d like to enable. For anything that isn\u2019t`,
|
|
32
|
+
` installed yet, braeburn will offer to set it up via Homebrew when you run it.`,
|
|
33
|
+
"",
|
|
34
|
+
` ${chalk.dim("\u2191\u2193 navigate Space toggle Return confirm")}`,
|
|
35
|
+
"",
|
|
36
|
+
];
|
|
37
|
+
for (let i = 0; i < items.length; i++) {
|
|
38
|
+
const item = items[i];
|
|
39
|
+
const isCursor = i === cursorIndex;
|
|
40
|
+
const cursor = isCursor ? chalk.cyan("\u203a") : " ";
|
|
41
|
+
const checkbox = item.selected ? chalk.green("\u25cf") : chalk.dim("\u25cb");
|
|
42
|
+
const namePadded = item.step.name.padEnd(18);
|
|
43
|
+
const name = isCursor ? chalk.bold.white(namePadded) : chalk.white(namePadded);
|
|
44
|
+
let status;
|
|
45
|
+
if (item.isProtected) {
|
|
46
|
+
status = chalk.dim("required");
|
|
47
|
+
}
|
|
48
|
+
else if (item.isAvailable) {
|
|
49
|
+
status = chalk.green("installed");
|
|
50
|
+
}
|
|
51
|
+
else if (item.step.brewPackageToInstall) {
|
|
52
|
+
status = chalk.yellow(`not installed`) + chalk.dim(` \u2192 will offer to install via Homebrew`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
status = chalk.dim("not installed");
|
|
56
|
+
}
|
|
57
|
+
lines.push(` ${cursor} ${checkbox} ${name} ${status}`);
|
|
58
|
+
if (isCursor) {
|
|
59
|
+
lines.push(` ${chalk.dim(item.step.description)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const enabledCount = items.filter((i) => i.selected).length;
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push(` ${chalk.dim(`${enabledCount} of ${items.length} tools selected`)}`);
|
|
65
|
+
lines.push("");
|
|
66
|
+
return lines.join("\n") + "\n";
|
|
67
|
+
}
|
|
68
|
+
export async function runSetupCommand(allSteps) {
|
|
69
|
+
// Hide cursor; restore on exit
|
|
70
|
+
process.stdout.write("\x1b[?25l");
|
|
71
|
+
process.on("exit", () => process.stdout.write("\x1b[?25h"));
|
|
72
|
+
process.on("SIGINT", () => {
|
|
73
|
+
process.stdout.write("\x1b[?25h\n");
|
|
74
|
+
process.exit(130);
|
|
75
|
+
});
|
|
76
|
+
// Show loading screen while we check availability in parallel
|
|
77
|
+
render(buildLoadingScreen());
|
|
78
|
+
const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
|
|
79
|
+
const items = allSteps.map((step, i) => ({
|
|
80
|
+
step,
|
|
81
|
+
selected: true, // all enabled by default — user opts out
|
|
82
|
+
isProtected: PROTECTED_STEP_IDS.has(step.id),
|
|
83
|
+
isAvailable: availabilityResults[i],
|
|
84
|
+
}));
|
|
85
|
+
let cursorIndex = 0;
|
|
86
|
+
render(buildSetupScreen(items, cursorIndex));
|
|
87
|
+
// Interactive selection loop
|
|
88
|
+
await new Promise((resolve) => {
|
|
89
|
+
readline.emitKeypressEvents(process.stdin);
|
|
90
|
+
if (process.stdin.isTTY)
|
|
91
|
+
process.stdin.setRawMode(true);
|
|
92
|
+
const handleKeypress = (_char, key) => {
|
|
93
|
+
if (key?.ctrl && key?.name === "c") {
|
|
94
|
+
process.stdout.write("\x1b[?25h\n");
|
|
95
|
+
process.exit(130);
|
|
96
|
+
}
|
|
97
|
+
if (key?.name === "up" || key?.name === "k") {
|
|
98
|
+
cursorIndex = Math.max(0, cursorIndex - 1);
|
|
99
|
+
render(buildSetupScreen(items, cursorIndex));
|
|
100
|
+
}
|
|
101
|
+
else if (key?.name === "down" || key?.name === "j") {
|
|
102
|
+
cursorIndex = Math.min(items.length - 1, cursorIndex + 1);
|
|
103
|
+
render(buildSetupScreen(items, cursorIndex));
|
|
104
|
+
}
|
|
105
|
+
else if (key?.name === "space") {
|
|
106
|
+
const item = items[cursorIndex];
|
|
107
|
+
if (!item.isProtected) {
|
|
108
|
+
item.selected = !item.selected;
|
|
109
|
+
render(buildSetupScreen(items, cursorIndex));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (key?.name === "return") {
|
|
113
|
+
process.stdin.removeListener("keypress", handleKeypress);
|
|
114
|
+
if (process.stdin.isTTY)
|
|
115
|
+
process.stdin.setRawMode(false);
|
|
116
|
+
process.stdin.pause();
|
|
117
|
+
resolve();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
process.stdin.on("keypress", handleKeypress);
|
|
121
|
+
process.stdin.resume();
|
|
122
|
+
});
|
|
123
|
+
// Restore cursor
|
|
124
|
+
process.stdout.write("\x1b[?25h");
|
|
125
|
+
// Persist choices — only write explicit false entries (keeps file minimal, matches
|
|
126
|
+
// the opt-out convention used everywhere else in the codebase)
|
|
127
|
+
const stepsConfig = {};
|
|
128
|
+
for (const item of items) {
|
|
129
|
+
if (!item.isProtected && !item.selected) {
|
|
130
|
+
stepsConfig[item.step.id] = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await writeConfig({ steps: stepsConfig });
|
|
134
|
+
// Clear the setup screen and print a brief confirmation before the update starts
|
|
135
|
+
if (prevLines > 0) {
|
|
136
|
+
process.stdout.write(`\x1b[${prevLines}A\x1b[J`);
|
|
137
|
+
}
|
|
138
|
+
process.stdout.write(chalk.yellow(LOGO_ART) + "\n");
|
|
139
|
+
process.stdout.write("\n");
|
|
140
|
+
process.stdout.write(` ${chalk.green("\u2713")} Setup complete! Starting your first update\u2026\n`);
|
|
141
|
+
process.stdout.write("\n");
|
|
142
|
+
// Small pause so the confirmation is readable before the update screen takes over
|
|
143
|
+
await new Promise((res) => setTimeout(res, 800));
|
|
144
|
+
}
|
package/dist/commands/update.js
CHANGED
|
@@ -5,7 +5,8 @@ import { captureYesNo } from "../ui/prompt.js";
|
|
|
5
5
|
import { createInitialAppState } from "../ui/state.js";
|
|
6
6
|
import { buildScreen, renderScreen } from "../ui/screen.js";
|
|
7
7
|
export async function runUpdateCommand(options) {
|
|
8
|
-
const { steps,
|
|
8
|
+
const { steps, version } = options;
|
|
9
|
+
let autoYes = options.autoYes;
|
|
9
10
|
const state = createInitialAppState(steps, version);
|
|
10
11
|
process.stdout.write("\x1b[?25l");
|
|
11
12
|
process.on("exit", () => process.stdout.write("\x1b[?25h"));
|
|
@@ -34,7 +35,10 @@ export async function runUpdateCommand(options) {
|
|
|
34
35
|
question: `Install ${step.name} via Homebrew? (brew install ${step.brewPackageToInstall})`,
|
|
35
36
|
};
|
|
36
37
|
renderScreen(buildScreen(state));
|
|
37
|
-
const
|
|
38
|
+
const installAnswer = autoYes ? "yes" : await captureYesNo();
|
|
39
|
+
if (installAnswer === "force")
|
|
40
|
+
autoYes = true;
|
|
41
|
+
const shouldInstall = installAnswer !== "no";
|
|
38
42
|
state.currentPrompt = undefined;
|
|
39
43
|
if (!shouldInstall) {
|
|
40
44
|
state.currentPhase = "skipped";
|
|
@@ -68,7 +72,10 @@ export async function runUpdateCommand(options) {
|
|
|
68
72
|
state.currentPhase = "prompting-to-run";
|
|
69
73
|
state.currentPrompt = { question: `Run ${step.name} update?`, warning: pipWarning };
|
|
70
74
|
renderScreen(buildScreen(state));
|
|
71
|
-
const
|
|
75
|
+
const runAnswer = autoYes ? "yes" : await captureYesNo();
|
|
76
|
+
if (runAnswer === "force")
|
|
77
|
+
autoYes = true;
|
|
78
|
+
const shouldRun = runAnswer !== "no";
|
|
72
79
|
state.currentPrompt = undefined;
|
|
73
80
|
if (!shouldRun) {
|
|
74
81
|
state.currentPhase = "skipped";
|
package/dist/config.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type BraeburnConfig = {
|
|
|
4
4
|
steps: Record<string, boolean>;
|
|
5
5
|
};
|
|
6
6
|
export declare function resolveConfigPath(): Promise<string>;
|
|
7
|
+
export declare function configFileExists(): Promise<boolean>;
|
|
7
8
|
export declare function readConfig(): Promise<BraeburnConfig>;
|
|
8
9
|
export declare function writeConfig(config: BraeburnConfig): Promise<void>;
|
|
9
10
|
export declare function isStepEnabled(config: BraeburnConfig, stepId: string): boolean;
|
package/dist/config.js
CHANGED
|
@@ -21,6 +21,10 @@ export async function resolveConfigPath() {
|
|
|
21
21
|
}
|
|
22
22
|
return join(homedir(), ".braeburn", "config");
|
|
23
23
|
}
|
|
24
|
+
export async function configFileExists() {
|
|
25
|
+
const configPath = await resolveConfigPath();
|
|
26
|
+
return pathExists(configPath);
|
|
27
|
+
}
|
|
24
28
|
export async function readConfig() {
|
|
25
29
|
const configPath = await resolveConfigPath();
|
|
26
30
|
try {
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,8 @@ import { homebrewStep, masStep, ohmyzshStep, npmStep, pipStep, pyenvStep, nvmSte
|
|
|
7
7
|
import { runUpdateCommand } from "./commands/update.js";
|
|
8
8
|
import { runLogCommand, runLogListCommand } from "./commands/log.js";
|
|
9
9
|
import { runConfigCommand, runConfigUpdateCommand } from "./commands/config.js";
|
|
10
|
-
import {
|
|
10
|
+
import { runSetupCommand } from "./commands/setup.js";
|
|
11
|
+
import { readConfig, isStepEnabled, PROTECTED_STEP_IDS, configFileExists } from "./config.js";
|
|
11
12
|
const ALL_STEPS = [
|
|
12
13
|
homebrewStep,
|
|
13
14
|
masStep,
|
|
@@ -58,6 +59,10 @@ Examples:
|
|
|
58
59
|
`)
|
|
59
60
|
.action(async (stepArguments, options) => {
|
|
60
61
|
const autoYes = options.yes === true || options.force === true;
|
|
62
|
+
// First-run: if no config file exists yet, show the setup wizard.
|
|
63
|
+
if (!(await configFileExists())) {
|
|
64
|
+
await runSetupCommand(ALL_STEPS);
|
|
65
|
+
}
|
|
61
66
|
let stepsToRun = stepArguments.length === 0
|
|
62
67
|
? ALL_STEPS
|
|
63
68
|
: resolveStepsByIds(stepArguments);
|
package/dist/ui/prompt.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { CurrentPrompt } from "./state.js";
|
|
2
|
-
export
|
|
2
|
+
export type YesNoForceAnswer = "yes" | "no" | "force";
|
|
3
|
+
export declare function captureYesNo(): Promise<YesNoForceAnswer>;
|
|
3
4
|
export declare function buildPromptLines(prompt: CurrentPrompt): string[];
|
package/dist/ui/prompt.js
CHANGED
|
@@ -13,14 +13,20 @@ 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
|
-
|
|
16
|
+
const isForce = character === "f" || character === "F";
|
|
17
|
+
if (!isConfirm && !isDecline && !isForce)
|
|
17
18
|
return;
|
|
18
19
|
process.stdin.removeListener("keypress", handleKeypress);
|
|
19
20
|
if (process.stdin.isTTY) {
|
|
20
21
|
process.stdin.setRawMode(false);
|
|
21
22
|
}
|
|
22
23
|
process.stdin.pause();
|
|
23
|
-
|
|
24
|
+
if (isForce)
|
|
25
|
+
resolve("force");
|
|
26
|
+
else if (isConfirm)
|
|
27
|
+
resolve("yes");
|
|
28
|
+
else
|
|
29
|
+
resolve("no");
|
|
24
30
|
};
|
|
25
31
|
process.stdin.on("keypress", handleKeypress);
|
|
26
32
|
process.stdin.resume();
|
|
@@ -32,6 +38,6 @@ export function buildPromptLines(prompt) {
|
|
|
32
38
|
lines.push(` ${chalk.yellow("⚠")} ${chalk.yellow(prompt.warning)}`);
|
|
33
39
|
lines.push("");
|
|
34
40
|
}
|
|
35
|
-
lines.push(` ${chalk.cyan("?")} ${prompt.question} ${chalk.dim("[Y/n]")}`);
|
|
41
|
+
lines.push(` ${chalk.cyan("?")} ${prompt.question} ${chalk.dim("[Y/n/f]")}`);
|
|
36
42
|
return lines;
|
|
37
43
|
}
|