braeburn 1.4.0 → 1.4.1
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 +3 -1
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.js +187 -24
- package/dist/index.js +21 -4
- package/dist/ui/screen.js +1 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,9 @@ braeburn homebrew npm # run specific steps only
|
|
|
24
24
|
|---|---|
|
|
25
25
|
| `braeburn [steps...] [-y]` | Run update steps (default) |
|
|
26
26
|
| `braeburn log [step]` | View the latest output log for a step |
|
|
27
|
-
| `braeburn config` |
|
|
27
|
+
| `braeburn config` | Show config subcommand help |
|
|
28
|
+
| `braeburn config list` | Print current configuration |
|
|
29
|
+
| `braeburn config update` | Open interactive configuration editor |
|
|
28
30
|
| `braeburn config update --no-<step>` | Disable a step |
|
|
29
31
|
| `braeburn config update --<step>` | Re-enable a step |
|
|
30
32
|
|
|
@@ -2,6 +2,7 @@ import { type BraeburnConfig } from "../config.js";
|
|
|
2
2
|
import type { Step } from "../steps/index.js";
|
|
3
3
|
type RunConfigCommandOptions = {
|
|
4
4
|
allSteps: Step[];
|
|
5
|
+
outputMode: "interactive" | "non-interactive";
|
|
5
6
|
};
|
|
6
7
|
type DesiredState = "enable" | "disable";
|
|
7
8
|
type RunConfigUpdateCommandOptions = {
|
|
@@ -20,4 +21,10 @@ type ConfigUpdateResult = {
|
|
|
20
21
|
};
|
|
21
22
|
export declare function applyConfigUpdates(config: BraeburnConfig, settingUpdates: Record<string, DesiredState>): ConfigUpdateResult;
|
|
22
23
|
export declare function runConfigUpdateCommand(options: RunConfigUpdateCommandOptions): Promise<void>;
|
|
24
|
+
type BuildConfigTableOutputOptions = {
|
|
25
|
+
config: BraeburnConfig;
|
|
26
|
+
configPath: string;
|
|
27
|
+
allSteps: Step[];
|
|
28
|
+
};
|
|
29
|
+
export declare function buildConfigTableOutput(options: BuildConfigTableOutputOptions): string;
|
|
23
30
|
export {};
|
package/dist/commands/config.js
CHANGED
|
@@ -1,33 +1,22 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
1
2
|
import chalk from "chalk";
|
|
2
3
|
import { readConfig, writeConfig, resolveConfigPath, isSettingEnabled, isStepEnabled, isLogoEnabled, applySettingToConfig, PROTECTED_STEP_IDS, DEFAULT_OFF_STEP_IDS, } from "../config.js";
|
|
4
|
+
import { createScreenRenderer } from "../ui/screen.js";
|
|
5
|
+
import { hideCursorDuringExecution } from "../ui/terminal.js";
|
|
3
6
|
export async function runConfigCommand(options) {
|
|
4
|
-
const { allSteps } = options;
|
|
7
|
+
const { allSteps, outputMode } = options;
|
|
5
8
|
const config = await readConfig();
|
|
6
9
|
const configPath = await resolveConfigPath();
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
process.stdout.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
for (const step of allSteps) {
|
|
16
|
-
const isProtected = PROTECTED_STEP_IDS.has(step.id);
|
|
17
|
-
const enabled = isStepEnabled(config, step.id);
|
|
18
|
-
let statusText;
|
|
19
|
-
if (isProtected) {
|
|
20
|
-
statusText = chalk.dim("always enabled");
|
|
21
|
-
}
|
|
22
|
-
else if (enabled) {
|
|
23
|
-
statusText = chalk.green("enabled");
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
statusText = chalk.red("disabled");
|
|
27
|
-
}
|
|
28
|
-
process.stdout.write(`${step.id.padEnd(STEP_COL)}${statusText}\n`);
|
|
10
|
+
if (outputMode === "non-interactive") {
|
|
11
|
+
process.stdout.write(buildConfigTableOutput({ config, configPath, allSteps }));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
15
|
+
process.stderr.write("Interactive mode requires a TTY. Showing non-interactive output.\n\n");
|
|
16
|
+
process.stdout.write(buildConfigTableOutput({ config, configPath, allSteps }));
|
|
17
|
+
return;
|
|
29
18
|
}
|
|
30
|
-
|
|
19
|
+
await runInteractiveConfigView({ config, configPath, allSteps });
|
|
31
20
|
}
|
|
32
21
|
export function applyConfigUpdates(config, settingUpdates) {
|
|
33
22
|
let updatedConfig = config;
|
|
@@ -74,6 +63,180 @@ export async function runConfigUpdateCommand(options) {
|
|
|
74
63
|
const configPath = await resolveConfigPath();
|
|
75
64
|
process.stdout.write(`\nConfig saved to ${chalk.dim(configPath)}\n`);
|
|
76
65
|
}
|
|
66
|
+
export function buildConfigTableOutput(options) {
|
|
67
|
+
const { config, configPath, allSteps } = options;
|
|
68
|
+
const lines = [];
|
|
69
|
+
const stepColumnWidth = 12;
|
|
70
|
+
const divider = "─".repeat(stepColumnWidth + 16);
|
|
71
|
+
lines.push(`Config: ${chalk.dim(configPath)}`);
|
|
72
|
+
lines.push("");
|
|
73
|
+
lines.push(`${"Step".padEnd(stepColumnWidth)}Status`);
|
|
74
|
+
lines.push(divider);
|
|
75
|
+
const logoEnabled = isLogoEnabled(config);
|
|
76
|
+
lines.push(`${"logo".padEnd(stepColumnWidth)}${logoEnabled ? chalk.green("enabled") : chalk.red("disabled")}`);
|
|
77
|
+
lines.push(divider);
|
|
78
|
+
for (const step of allSteps) {
|
|
79
|
+
const isProtected = PROTECTED_STEP_IDS.has(step.id);
|
|
80
|
+
const enabled = isStepEnabled(config, step.id);
|
|
81
|
+
let statusText;
|
|
82
|
+
if (isProtected) {
|
|
83
|
+
statusText = chalk.dim("always enabled");
|
|
84
|
+
}
|
|
85
|
+
else if (enabled) {
|
|
86
|
+
statusText = chalk.green("enabled");
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
statusText = chalk.red("disabled");
|
|
90
|
+
}
|
|
91
|
+
lines.push(`${step.id.padEnd(stepColumnWidth)}${statusText}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push("");
|
|
94
|
+
return lines.join("\n") + "\n";
|
|
95
|
+
}
|
|
96
|
+
function buildConfigViewItems(config, allSteps) {
|
|
97
|
+
const viewItems = [
|
|
98
|
+
{
|
|
99
|
+
id: "logo",
|
|
100
|
+
label: "logo",
|
|
101
|
+
description: "Show the braeburn logo in command output",
|
|
102
|
+
protection: "configurable",
|
|
103
|
+
selection: isLogoEnabled(config) ? "enabled" : "disabled",
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
for (const step of allSteps) {
|
|
107
|
+
viewItems.push({
|
|
108
|
+
id: step.id,
|
|
109
|
+
label: step.id,
|
|
110
|
+
description: step.description,
|
|
111
|
+
protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
|
|
112
|
+
selection: isStepEnabled(config, step.id) ? "enabled" : "disabled",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return viewItems;
|
|
116
|
+
}
|
|
117
|
+
function buildInteractiveConfigScreen(options) {
|
|
118
|
+
const { configPath, items, cursorIndex } = options;
|
|
119
|
+
const lines = [];
|
|
120
|
+
lines.push(`Config: ${chalk.dim(configPath)}`);
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(` ${chalk.dim("↑↓ navigate Space toggle Return save q quit")}`);
|
|
123
|
+
lines.push("");
|
|
124
|
+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
125
|
+
const item = items[itemIndex];
|
|
126
|
+
const isCursor = itemIndex === cursorIndex;
|
|
127
|
+
const cursor = isCursor ? chalk.cyan("›") : " ";
|
|
128
|
+
const marker = item.selection === "enabled" ? chalk.green("●") : chalk.dim("○");
|
|
129
|
+
const label = isCursor ? chalk.bold.white(item.label.padEnd(12)) : chalk.white(item.label.padEnd(12));
|
|
130
|
+
let status;
|
|
131
|
+
if (item.protection === "protected") {
|
|
132
|
+
status = chalk.dim("always enabled");
|
|
133
|
+
}
|
|
134
|
+
else if (item.selection === "enabled") {
|
|
135
|
+
status = chalk.green("enabled");
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
status = chalk.red("disabled");
|
|
139
|
+
}
|
|
140
|
+
lines.push(` ${cursor} ${marker} ${label} ${status}`);
|
|
141
|
+
if (isCursor) {
|
|
142
|
+
lines.push(` ${chalk.dim(item.description)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const enabledCount = items.filter((item) => item.selection === "enabled").length;
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push(` ${chalk.dim(`${enabledCount} of ${items.length} settings enabled`)}`);
|
|
148
|
+
lines.push("");
|
|
149
|
+
return lines.join("\n") + "\n";
|
|
150
|
+
}
|
|
151
|
+
async function runInteractiveConfigView(options) {
|
|
152
|
+
const { config, configPath, allSteps } = options;
|
|
153
|
+
const render = createScreenRenderer();
|
|
154
|
+
const restoreCursor = hideCursorDuringExecution({ screenBuffer: "alternate" });
|
|
155
|
+
const items = buildConfigViewItems(config, allSteps);
|
|
156
|
+
let cursorIndex = 0;
|
|
157
|
+
let completionOutput = "";
|
|
158
|
+
try {
|
|
159
|
+
render(buildInteractiveConfigScreen({ configPath, items, cursorIndex }));
|
|
160
|
+
const interactionResult = await new Promise((resolve) => {
|
|
161
|
+
readline.emitKeypressEvents(process.stdin);
|
|
162
|
+
if (process.stdin.isTTY) {
|
|
163
|
+
process.stdin.setRawMode(true);
|
|
164
|
+
}
|
|
165
|
+
const completeInteraction = (result) => {
|
|
166
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
167
|
+
if (process.stdin.isTTY) {
|
|
168
|
+
process.stdin.setRawMode(false);
|
|
169
|
+
}
|
|
170
|
+
process.stdin.pause();
|
|
171
|
+
resolve(result);
|
|
172
|
+
};
|
|
173
|
+
const onKeypress = (_char, key) => {
|
|
174
|
+
if (key.ctrl && key.name === "c") {
|
|
175
|
+
process.exit(130);
|
|
176
|
+
}
|
|
177
|
+
if (key.name === "up" || key.name === "k") {
|
|
178
|
+
cursorIndex = Math.max(0, cursorIndex - 1);
|
|
179
|
+
render(buildInteractiveConfigScreen({ configPath, items, cursorIndex }));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (key.name === "down" || key.name === "j") {
|
|
183
|
+
cursorIndex = Math.min(items.length - 1, cursorIndex + 1);
|
|
184
|
+
render(buildInteractiveConfigScreen({ configPath, items, cursorIndex }));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (key.name === "space") {
|
|
188
|
+
const selectedItem = items[cursorIndex];
|
|
189
|
+
if (selectedItem.protection === "configurable") {
|
|
190
|
+
selectedItem.selection = selectedItem.selection === "enabled" ? "disabled" : "enabled";
|
|
191
|
+
render(buildInteractiveConfigScreen({ configPath, items, cursorIndex }));
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (key.name === "return") {
|
|
196
|
+
completeInteraction("save");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (key.name === "q" || key.name === "escape") {
|
|
200
|
+
completeInteraction("cancel");
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
process.stdin.on("keypress", onKeypress);
|
|
204
|
+
process.stdin.resume();
|
|
205
|
+
});
|
|
206
|
+
if (interactionResult === "cancel") {
|
|
207
|
+
completionOutput = "No changes saved.\n";
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const settingUpdates = {};
|
|
211
|
+
for (const item of items) {
|
|
212
|
+
if (item.protection === "protected") {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
settingUpdates[item.id] = item.selection === "enabled" ? "enable" : "disable";
|
|
216
|
+
}
|
|
217
|
+
const { updatedConfig, changes } = applyConfigUpdates(config, settingUpdates);
|
|
218
|
+
if (changes.length === 0) {
|
|
219
|
+
completionOutput = "No changes — already set as requested.\n";
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
await writeCleanConfig(updatedConfig);
|
|
223
|
+
const outputLines = [];
|
|
224
|
+
for (const { label, from, to } of changes) {
|
|
225
|
+
const fromLabel = from === "enable" ? chalk.green("enabled") : chalk.red("disabled");
|
|
226
|
+
const toLabel = to === "enable" ? chalk.green("enabled") : chalk.red("disabled");
|
|
227
|
+
outputLines.push(` ${label.padEnd(12)} ${fromLabel} → ${toLabel}`);
|
|
228
|
+
}
|
|
229
|
+
outputLines.push("");
|
|
230
|
+
outputLines.push(`Config saved to ${chalk.dim(configPath)}`);
|
|
231
|
+
completionOutput = `${outputLines.join("\n")}\n`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
restoreCursor();
|
|
237
|
+
}
|
|
238
|
+
process.stdout.write(completionOutput);
|
|
239
|
+
}
|
|
77
240
|
async function writeCleanConfig(config) {
|
|
78
241
|
const cleaned = { steps: {} };
|
|
79
242
|
for (const [stepId, value] of Object.entries(config.steps)) {
|
package/dist/index.js
CHANGED
|
@@ -103,15 +103,25 @@ Examples:
|
|
|
103
103
|
const configCommand = program
|
|
104
104
|
.command("config")
|
|
105
105
|
.description("View or edit braeburn configuration")
|
|
106
|
+
.action(() => {
|
|
107
|
+
configCommand.outputHelp();
|
|
108
|
+
});
|
|
109
|
+
configCommand
|
|
110
|
+
.command("list")
|
|
111
|
+
.description("Print current configuration")
|
|
106
112
|
.action(async () => {
|
|
107
|
-
await runConfigCommand({
|
|
113
|
+
await runConfigCommand({
|
|
114
|
+
allSteps: ALL_STEPS,
|
|
115
|
+
outputMode: "non-interactive",
|
|
116
|
+
});
|
|
108
117
|
});
|
|
109
118
|
const configurableSteps = ALL_STEPS.filter((step) => !PROTECTED_STEP_IDS.has(step.id));
|
|
110
119
|
const configUpdateCommand = configCommand
|
|
111
120
|
.command("update")
|
|
112
|
-
.description("
|
|
121
|
+
.description("Edit configuration (interactive by default, flags for direct updates)")
|
|
113
122
|
.addHelpText("after", `
|
|
114
123
|
Examples:
|
|
124
|
+
braeburn config update Open interactive config editor
|
|
115
125
|
braeburn config update --no-logo Hide the logo
|
|
116
126
|
braeburn config update --no-ohmyzsh Disable Oh My Zsh updates
|
|
117
127
|
braeburn config update --no-pip --no-nvm Disable pip and nvm updates
|
|
@@ -123,7 +133,7 @@ for (const step of configurableSteps) {
|
|
|
123
133
|
configUpdateCommand.option(`--no-${step.id}`, `Disable ${step.name} updates`);
|
|
124
134
|
configUpdateCommand.option(`--${step.id}`, `Enable ${step.name} updates`);
|
|
125
135
|
}
|
|
126
|
-
configUpdateCommand.action(function () {
|
|
136
|
+
configUpdateCommand.action(async function () {
|
|
127
137
|
// Commander defaults --no-* to true, so we use getOptionValueSource to detect explicit CLI flags.
|
|
128
138
|
const settingUpdates = {};
|
|
129
139
|
for (const step of configurableSteps) {
|
|
@@ -136,7 +146,14 @@ configUpdateCommand.action(function () {
|
|
|
136
146
|
if (logoSource === "cli") {
|
|
137
147
|
settingUpdates["logo"] = configUpdateCommand.opts().logo ? "enable" : "disable";
|
|
138
148
|
}
|
|
139
|
-
|
|
149
|
+
if (Object.keys(settingUpdates).length === 0) {
|
|
150
|
+
await runConfigCommand({
|
|
151
|
+
allSteps: ALL_STEPS,
|
|
152
|
+
outputMode: "interactive",
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await runConfigUpdateCommand({ settingUpdates, allSteps: ALL_STEPS });
|
|
140
157
|
});
|
|
141
158
|
function resolveStepsByIds(stepIds) {
|
|
142
159
|
const resolvedSteps = [];
|
package/dist/ui/screen.js
CHANGED
|
@@ -4,13 +4,8 @@ import { buildOutputBoxLines } from "./outputBox.js";
|
|
|
4
4
|
import { buildPromptLines } from "./prompt.js";
|
|
5
5
|
import { buildVersionReportLines } from "./versionReport.js";
|
|
6
6
|
export function createScreenRenderer(output = process.stdout) {
|
|
7
|
-
let hasAnchor = false;
|
|
8
7
|
return (content) => {
|
|
9
|
-
|
|
10
|
-
output.write("\x1b7");
|
|
11
|
-
hasAnchor = true;
|
|
12
|
-
}
|
|
13
|
-
output.write("\x1b8\x1b[J");
|
|
8
|
+
output.write("\x1b[H\x1b[2J");
|
|
14
9
|
output.write(content);
|
|
15
10
|
};
|
|
16
11
|
}
|