braeburn 1.0.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/LICENSE +21 -0
- package/dist/commands/config.d.ts +11 -0
- package/dist/commands/config.js +81 -0
- package/dist/commands/log.d.ts +6 -0
- package/dist/commands/log.js +35 -0
- package/dist/commands/update.d.ts +8 -0
- package/dist/commands/update.js +110 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +150 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.js +44 -0
- package/dist/logo.d.ts +1 -0
- package/dist/logo.js +14 -0
- package/dist/runner.d.ts +21 -0
- package/dist/runner.js +34 -0
- package/dist/steps/cleanup.d.ts +3 -0
- package/dist/steps/cleanup.js +13 -0
- package/dist/steps/dotnet.d.ts +3 -0
- package/dist/steps/dotnet.js +14 -0
- package/dist/steps/homebrew.d.ts +3 -0
- package/dist/steps/homebrew.js +13 -0
- package/dist/steps/index.d.ts +27 -0
- package/dist/steps/index.js +24 -0
- package/dist/steps/macos.d.ts +3 -0
- package/dist/steps/macos.js +32 -0
- package/dist/steps/mas.d.ts +3 -0
- package/dist/steps/mas.js +14 -0
- package/dist/steps/npm.d.ts +3 -0
- package/dist/steps/npm.js +14 -0
- package/dist/steps/nvm.d.ts +3 -0
- package/dist/steps/nvm.js +20 -0
- package/dist/steps/ohmyzsh.d.ts +3 -0
- package/dist/steps/ohmyzsh.js +17 -0
- package/dist/steps/pip.d.ts +3 -0
- package/dist/steps/pip.js +17 -0
- package/dist/steps/pyenv.d.ts +3 -0
- package/dist/steps/pyenv.js +29 -0
- package/dist/ui/currentStep.d.ts +10 -0
- package/dist/ui/currentStep.js +16 -0
- package/dist/ui/header.d.ts +11 -0
- package/dist/ui/header.js +61 -0
- package/dist/ui/outputBox.d.ts +2 -0
- package/dist/ui/outputBox.js +29 -0
- package/dist/ui/prompt.d.ts +3 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/screen.d.ts +3 -0
- package/dist/ui/screen.js +54 -0
- package/dist/ui/state.d.ts +27 -0
- package/dist/ui/state.js +13 -0
- package/dist/ui/versionReport.d.ts +3 -0
- package/dist/ui/versionReport.js +25 -0
- package/package.json +32 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { checkCommandExists, runStep } from "./index.js";
|
|
2
|
+
const homebrewStep = {
|
|
3
|
+
id: "homebrew",
|
|
4
|
+
name: "Homebrew",
|
|
5
|
+
description: "Update Homebrew itself and upgrade all installed formulae",
|
|
6
|
+
async checkIsAvailable() {
|
|
7
|
+
return checkCommandExists("brew");
|
|
8
|
+
},
|
|
9
|
+
async run(context) {
|
|
10
|
+
await runStep("brew update && brew upgrade", context);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
export default homebrewStep;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type OutputLineCallback } from "../runner.js";
|
|
2
|
+
import type { StepLogWriter } from "../logger.js";
|
|
3
|
+
export type StepRunContext = {
|
|
4
|
+
onOutputLine: OutputLineCallback;
|
|
5
|
+
logWriter: StepLogWriter;
|
|
6
|
+
};
|
|
7
|
+
export type Step = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
brewPackageToInstall?: string;
|
|
12
|
+
checkIsAvailable: () => Promise<boolean>;
|
|
13
|
+
run: (context: StepRunContext) => Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
export { default as homebrewStep } from "./homebrew.js";
|
|
16
|
+
export { default as masStep } from "./mas.js";
|
|
17
|
+
export { default as ohmyzshStep } from "./ohmyzsh.js";
|
|
18
|
+
export { default as npmStep } from "./npm.js";
|
|
19
|
+
export { default as pipStep } from "./pip.js";
|
|
20
|
+
export { default as pyenvStep } from "./pyenv.js";
|
|
21
|
+
export { default as nvmStep } from "./nvm.js";
|
|
22
|
+
export { default as dotnetStep } from "./dotnet.js";
|
|
23
|
+
export { default as macosStep } from "./macos.js";
|
|
24
|
+
export { default as cleanupStep } from "./cleanup.js";
|
|
25
|
+
export declare function checkCommandExists(command: string): Promise<boolean>;
|
|
26
|
+
export declare function checkPathExists(filePath: string): Promise<boolean>;
|
|
27
|
+
export declare function runStep(shellCommand: string, context: StepRunContext): Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { doesShellCommandSucceed, runShellCommand, } from "../runner.js";
|
|
2
|
+
export { default as homebrewStep } from "./homebrew.js";
|
|
3
|
+
export { default as masStep } from "./mas.js";
|
|
4
|
+
export { default as ohmyzshStep } from "./ohmyzsh.js";
|
|
5
|
+
export { default as npmStep } from "./npm.js";
|
|
6
|
+
export { default as pipStep } from "./pip.js";
|
|
7
|
+
export { default as pyenvStep } from "./pyenv.js";
|
|
8
|
+
export { default as nvmStep } from "./nvm.js";
|
|
9
|
+
export { default as dotnetStep } from "./dotnet.js";
|
|
10
|
+
export { default as macosStep } from "./macos.js";
|
|
11
|
+
export { default as cleanupStep } from "./cleanup.js";
|
|
12
|
+
export async function checkCommandExists(command) {
|
|
13
|
+
return doesShellCommandSucceed({ shellCommand: `command -v ${command}` });
|
|
14
|
+
}
|
|
15
|
+
export async function checkPathExists(filePath) {
|
|
16
|
+
return doesShellCommandSucceed({ shellCommand: `test -e "${filePath}"` });
|
|
17
|
+
}
|
|
18
|
+
export async function runStep(shellCommand, context) {
|
|
19
|
+
await runShellCommand({
|
|
20
|
+
shellCommand,
|
|
21
|
+
onOutputLine: context.onOutputLine,
|
|
22
|
+
logWriter: context.logWriter,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { runStep, } from "./index.js";
|
|
2
|
+
import { captureShellCommandOutput } from "../runner.js";
|
|
3
|
+
// softwareupdate always exists on macOS — no availability check needed
|
|
4
|
+
const macosStep = {
|
|
5
|
+
id: "macos",
|
|
6
|
+
name: "macOS",
|
|
7
|
+
description: "Check for and optionally install macOS system software updates",
|
|
8
|
+
async checkIsAvailable() {
|
|
9
|
+
return true;
|
|
10
|
+
},
|
|
11
|
+
async run(context) {
|
|
12
|
+
const updateListOutput = await captureShellCommandOutput({
|
|
13
|
+
shellCommand: "softwareupdate -l 2>&1",
|
|
14
|
+
});
|
|
15
|
+
context.logWriter(updateListOutput);
|
|
16
|
+
const noUpdatesAvailable = updateListOutput.includes("No new software available");
|
|
17
|
+
if (noUpdatesAvailable) {
|
|
18
|
+
context.onOutputLine({
|
|
19
|
+
text: "macOS is already up to date.",
|
|
20
|
+
isError: false,
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
context.onOutputLine({ text: updateListOutput, isError: false });
|
|
25
|
+
context.onOutputLine({
|
|
26
|
+
text: "Updates found — installing now...",
|
|
27
|
+
isError: false,
|
|
28
|
+
});
|
|
29
|
+
await runStep("softwareupdate -ia --verbose", context);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export default macosStep;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { checkCommandExists, runStep } from "./index.js";
|
|
2
|
+
const masStep = {
|
|
3
|
+
id: "mas",
|
|
4
|
+
name: "Mac App Store",
|
|
5
|
+
description: "Upgrade all Mac App Store apps via the mas CLI tool",
|
|
6
|
+
brewPackageToInstall: "mas",
|
|
7
|
+
async checkIsAvailable() {
|
|
8
|
+
return checkCommandExists("mas");
|
|
9
|
+
},
|
|
10
|
+
async run(context) {
|
|
11
|
+
await runStep("mas upgrade", context);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export default masStep;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { checkCommandExists, runStep } from "./index.js";
|
|
2
|
+
const npmStep = {
|
|
3
|
+
id: "npm",
|
|
4
|
+
name: "npm",
|
|
5
|
+
description: "Update all globally installed npm packages",
|
|
6
|
+
// No brewPackageToInstall — npm comes bundled with Node.js
|
|
7
|
+
async checkIsAvailable() {
|
|
8
|
+
return checkCommandExists("npm");
|
|
9
|
+
},
|
|
10
|
+
async run(context) {
|
|
11
|
+
await runStep("npm update -g", context);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export default npmStep;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { checkPathExists, runStep } from "./index.js";
|
|
4
|
+
const NVM_DIRECTORY = join(homedir(), ".nvm");
|
|
5
|
+
// nvm is a shell function sourced from nvm.sh — it cannot be invoked as a
|
|
6
|
+
// standalone binary, so we source it explicitly inside each bash invocation.
|
|
7
|
+
const NVM_SOURCE_PREFIX = `export NVM_DIR="${NVM_DIRECTORY}" && source "$NVM_DIR/nvm.sh"`;
|
|
8
|
+
const nvmStep = {
|
|
9
|
+
id: "nvm",
|
|
10
|
+
name: "Node.js (nvm)",
|
|
11
|
+
description: "Install the latest Node.js via nvm, migrating packages from the current version",
|
|
12
|
+
// No brewPackageToInstall — nvm is installed via a curl script, not Homebrew
|
|
13
|
+
async checkIsAvailable() {
|
|
14
|
+
return checkPathExists(NVM_DIRECTORY);
|
|
15
|
+
},
|
|
16
|
+
async run(context) {
|
|
17
|
+
await runStep(`${NVM_SOURCE_PREFIX} && nvm install node --reinstall-packages-from=node`, context);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export default nvmStep;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { checkPathExists, runStep } from "./index.js";
|
|
4
|
+
const OH_MY_ZSH_UPGRADE_SCRIPT_PATH = join(homedir(), ".oh-my-zsh", "tools", "upgrade.sh");
|
|
5
|
+
const ohmyzshStep = {
|
|
6
|
+
id: "ohmyzsh",
|
|
7
|
+
name: "Oh My Zsh",
|
|
8
|
+
description: "Update Oh My Zsh to the latest version",
|
|
9
|
+
// No brewPackageToInstall — Oh My Zsh is installed via a curl script, not Homebrew
|
|
10
|
+
async checkIsAvailable() {
|
|
11
|
+
return checkPathExists(OH_MY_ZSH_UPGRADE_SCRIPT_PATH);
|
|
12
|
+
},
|
|
13
|
+
async run(context) {
|
|
14
|
+
await runStep(`zsh "${OH_MY_ZSH_UPGRADE_SCRIPT_PATH}"`, context);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export default ohmyzshStep;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { checkCommandExists, runStep } from "./index.js";
|
|
2
|
+
// Updating all global pip3 packages can occasionally break tools that depend
|
|
3
|
+
// on specific versions. The warning is surfaced in the UI before running.
|
|
4
|
+
const PIP_UPDATE_ALL_OUTDATED_SHELL_COMMAND = "pip3 list --outdated --format=columns | tail -n +3 | awk '{print $1}' | xargs -n1 pip3 install -U";
|
|
5
|
+
const pipStep = {
|
|
6
|
+
id: "pip",
|
|
7
|
+
name: "pip3",
|
|
8
|
+
description: "Update all globally installed pip3 packages",
|
|
9
|
+
// No brewPackageToInstall — pip3 comes with Python
|
|
10
|
+
async checkIsAvailable() {
|
|
11
|
+
return checkCommandExists("pip3");
|
|
12
|
+
},
|
|
13
|
+
async run(context) {
|
|
14
|
+
await runStep(PIP_UPDATE_ALL_OUTDATED_SHELL_COMMAND, context);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export default pipStep;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { checkCommandExists, runStep, } from "./index.js";
|
|
2
|
+
import { captureShellCommandOutput } from "../runner.js";
|
|
3
|
+
// Dynamically finds the latest stable Python 3.x.y release known to pyenv,
|
|
4
|
+
// excluding pre-releases (alpha, beta, release candidates, dev builds).
|
|
5
|
+
const FIND_LATEST_STABLE_PYTHON_SHELL_COMMAND = "pyenv install -l | grep -E '^\\s+3\\.[0-9]+\\.[0-9]+$' | grep -vE 'dev|a[0-9]|b[0-9]|rc[0-9]' | tail -1 | tr -d ' '";
|
|
6
|
+
const pyenvStep = {
|
|
7
|
+
id: "pyenv",
|
|
8
|
+
name: "pyenv",
|
|
9
|
+
description: "Upgrade pyenv via Homebrew and install the latest Python 3.x",
|
|
10
|
+
brewPackageToInstall: "pyenv",
|
|
11
|
+
async checkIsAvailable() {
|
|
12
|
+
return checkCommandExists("pyenv");
|
|
13
|
+
},
|
|
14
|
+
async run(context) {
|
|
15
|
+
await runStep("brew upgrade pyenv", context);
|
|
16
|
+
const latestPythonVersion = await captureShellCommandOutput({
|
|
17
|
+
shellCommand: FIND_LATEST_STABLE_PYTHON_SHELL_COMMAND,
|
|
18
|
+
});
|
|
19
|
+
if (!latestPythonVersion) {
|
|
20
|
+
context.onOutputLine({
|
|
21
|
+
text: "Could not determine latest Python version — skipping pyenv install.",
|
|
22
|
+
isError: true,
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
await runStep(`pyenv install --skip-existing ${latestPythonVersion}`, context);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
export default pyenvStep;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Step } from "../steps/index.js";
|
|
2
|
+
import type { StepPhase } from "./state.js";
|
|
3
|
+
type ActiveStepOptions = {
|
|
4
|
+
step: Step;
|
|
5
|
+
stepNumber: number;
|
|
6
|
+
totalSteps: number;
|
|
7
|
+
phase: StepPhase;
|
|
8
|
+
};
|
|
9
|
+
export declare function buildActiveStepLines(options: ActiveStepOptions): string[];
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export function buildActiveStepLines(options) {
|
|
3
|
+
const { step, stepNumber, totalSteps, phase } = options;
|
|
4
|
+
const isRunning = phase === "running" || phase === "installing";
|
|
5
|
+
const lines = [
|
|
6
|
+
chalk.dim(` ${"─".repeat(3)} Step ${stepNumber}/${totalSteps} `) +
|
|
7
|
+
chalk.bold.white(step.name) +
|
|
8
|
+
chalk.dim(` ${"─".repeat(20)}`),
|
|
9
|
+
` ${chalk.dim("·")} ${chalk.dim.italic(step.description)}`,
|
|
10
|
+
];
|
|
11
|
+
if (isRunning) {
|
|
12
|
+
const label = phase === "installing" ? "Installing..." : "Running...";
|
|
13
|
+
lines.push(` ${chalk.blue("▶")} ${label}`);
|
|
14
|
+
}
|
|
15
|
+
return lines;
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Step } from "../steps/index.js";
|
|
2
|
+
import type { StepPhase, CompletedStepRecord } from "./state.js";
|
|
3
|
+
type BuildHeaderOptions = {
|
|
4
|
+
steps: Step[];
|
|
5
|
+
version: string;
|
|
6
|
+
currentStepIndex: number;
|
|
7
|
+
currentPhase: StepPhase;
|
|
8
|
+
completedStepRecords: CompletedStepRecord[];
|
|
9
|
+
};
|
|
10
|
+
export declare function buildHeaderLines(options: BuildHeaderOptions): string[];
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { LOGO_ART } from "../logo.js";
|
|
3
|
+
const LOGO_COLUMN_WIDTH = 32;
|
|
4
|
+
const LOGO_SEPARATOR = " ";
|
|
5
|
+
function stepTrackerIcon(phase) {
|
|
6
|
+
if (phase === "complete")
|
|
7
|
+
return chalk.green("✓ ");
|
|
8
|
+
if (phase === "failed")
|
|
9
|
+
return chalk.red("✗ ");
|
|
10
|
+
if (phase === "skipped" || phase === "not-available")
|
|
11
|
+
return chalk.dim("– ");
|
|
12
|
+
if (phase === "running" ||
|
|
13
|
+
phase === "installing" ||
|
|
14
|
+
phase === "prompting-to-run" ||
|
|
15
|
+
phase === "prompting-to-install" ||
|
|
16
|
+
phase === "checking-availability")
|
|
17
|
+
return chalk.cyan("→ ");
|
|
18
|
+
return chalk.dim("· ");
|
|
19
|
+
}
|
|
20
|
+
function isActivePhase(phase) {
|
|
21
|
+
return (phase === "running" ||
|
|
22
|
+
phase === "installing" ||
|
|
23
|
+
phase === "prompting-to-run" ||
|
|
24
|
+
phase === "prompting-to-install" ||
|
|
25
|
+
phase === "checking-availability");
|
|
26
|
+
}
|
|
27
|
+
function deriveAllStepPhases(steps, currentStepIndex, currentPhase, completedStepRecords) {
|
|
28
|
+
return steps.map((_, index) => {
|
|
29
|
+
if (index < completedStepRecords.length)
|
|
30
|
+
return completedStepRecords[index].phase;
|
|
31
|
+
if (index === currentStepIndex)
|
|
32
|
+
return currentPhase;
|
|
33
|
+
return "pending";
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function buildHeaderLines(options) {
|
|
37
|
+
const { steps, version, currentStepIndex, currentPhase, completedStepRecords } = options;
|
|
38
|
+
const phases = deriveAllStepPhases(steps, currentStepIndex, currentPhase, completedStepRecords);
|
|
39
|
+
const logoLines = LOGO_ART.split("\n");
|
|
40
|
+
const rightColumnLines = [
|
|
41
|
+
`${chalk.bold.white("braeburn")} ${chalk.dim("v" + version)}`,
|
|
42
|
+
chalk.dim("macOS system updater"),
|
|
43
|
+
"",
|
|
44
|
+
...steps.map((step, index) => {
|
|
45
|
+
const icon = stepTrackerIcon(phases[index]);
|
|
46
|
+
const name = isActivePhase(phases[index]) ? chalk.white(step.name) : chalk.dim(step.name);
|
|
47
|
+
return `${icon}${name}`;
|
|
48
|
+
}),
|
|
49
|
+
];
|
|
50
|
+
const totalLines = Math.max(logoLines.length, rightColumnLines.length);
|
|
51
|
+
const result = [];
|
|
52
|
+
for (let i = 0; i < totalLines; i++) {
|
|
53
|
+
// Pad the raw logo line to a fixed width before applying colour, so the
|
|
54
|
+
// ANSI escape codes don't affect visual alignment.
|
|
55
|
+
const rawLogoLine = (logoLines[i] ?? "").padEnd(LOGO_COLUMN_WIDTH);
|
|
56
|
+
const logoColumn = chalk.yellow(rawLogoLine);
|
|
57
|
+
const rightColumn = rightColumnLines[i] ?? "";
|
|
58
|
+
result.push(`${logoColumn}${LOGO_SEPARATOR}${rightColumn}`);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
const INDENT = " ";
|
|
3
|
+
const HEADER_LINES_APPROXIMATE = 18;
|
|
4
|
+
const OUTPUT_BOX_CHROME_LINES = 3;
|
|
5
|
+
const MINIMUM_VISIBLE_LINES = 5;
|
|
6
|
+
function maxVisibleLines() {
|
|
7
|
+
const rows = process.stdout.rows ?? 40;
|
|
8
|
+
const available = rows - HEADER_LINES_APPROXIMATE - OUTPUT_BOX_CHROME_LINES;
|
|
9
|
+
return Math.max(MINIMUM_VISIBLE_LINES, available);
|
|
10
|
+
}
|
|
11
|
+
function boxWidth() {
|
|
12
|
+
return Math.min(process.stdout.columns ?? 80, 120) - INDENT.length * 2;
|
|
13
|
+
}
|
|
14
|
+
export function buildOutputBoxLines(lines, stepName) {
|
|
15
|
+
const visibleLines = lines.slice(-maxVisibleLines());
|
|
16
|
+
const width = boxWidth();
|
|
17
|
+
const headerLabel = `─ ${stepName} output `;
|
|
18
|
+
const topDashes = "─".repeat(Math.max(0, width - headerLabel.length - 2));
|
|
19
|
+
const topBorder = chalk.dim(`${INDENT}┌${headerLabel}${topDashes}┐`);
|
|
20
|
+
const bottomBorder = chalk.dim(`${INDENT}└${"─".repeat(width - 2)}┘`);
|
|
21
|
+
const result = [topBorder];
|
|
22
|
+
for (const line of visibleLines) {
|
|
23
|
+
const truncated = line.text.slice(0, width - 4);
|
|
24
|
+
const colored = line.isError ? chalk.yellow(truncated) : chalk.dim(truncated);
|
|
25
|
+
result.push(`${INDENT}${chalk.dim("│")} ${colored}`);
|
|
26
|
+
}
|
|
27
|
+
result.push(bottomBorder);
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
export function captureYesNo() {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
readline.emitKeypressEvents(process.stdin);
|
|
6
|
+
if (process.stdin.isTTY) {
|
|
7
|
+
process.stdin.setRawMode(true);
|
|
8
|
+
}
|
|
9
|
+
const handleKeypress = (character, key) => {
|
|
10
|
+
if (key?.ctrl && key?.name === "c") {
|
|
11
|
+
process.stdout.write("\x1b[?25h\n");
|
|
12
|
+
process.exit(130);
|
|
13
|
+
}
|
|
14
|
+
const isConfirm = character === "y" || character === "Y" || key?.name === "return";
|
|
15
|
+
const isDecline = character === "n" || character === "N";
|
|
16
|
+
if (!isConfirm && !isDecline)
|
|
17
|
+
return;
|
|
18
|
+
process.stdin.removeListener("keypress", handleKeypress);
|
|
19
|
+
if (process.stdin.isTTY) {
|
|
20
|
+
process.stdin.setRawMode(false);
|
|
21
|
+
}
|
|
22
|
+
process.stdin.pause();
|
|
23
|
+
resolve(isConfirm);
|
|
24
|
+
};
|
|
25
|
+
process.stdin.on("keypress", handleKeypress);
|
|
26
|
+
process.stdin.resume();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function buildPromptLines(prompt) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
if (prompt.warning) {
|
|
32
|
+
lines.push(` ${chalk.yellow("⚠")} ${chalk.yellow(prompt.warning)}`);
|
|
33
|
+
lines.push("");
|
|
34
|
+
}
|
|
35
|
+
lines.push(` ${chalk.cyan("?")} ${prompt.question} ${chalk.dim("[Y/n]")}`);
|
|
36
|
+
return lines;
|
|
37
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { buildHeaderLines } from "./header.js";
|
|
2
|
+
import { buildActiveStepLines } from "./currentStep.js";
|
|
3
|
+
import { buildOutputBoxLines } from "./outputBox.js";
|
|
4
|
+
import { buildPromptLines } from "./prompt.js";
|
|
5
|
+
import { buildVersionReportLines } from "./versionReport.js";
|
|
6
|
+
let previousLineCount = 0;
|
|
7
|
+
export function renderScreen(content) {
|
|
8
|
+
if (previousLineCount > 0) {
|
|
9
|
+
process.stdout.write(`\x1b[${previousLineCount}A\x1b[J`);
|
|
10
|
+
}
|
|
11
|
+
process.stdout.write(content);
|
|
12
|
+
previousLineCount = (content.match(/\n/g) ?? []).length;
|
|
13
|
+
}
|
|
14
|
+
export function buildScreen(state) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
lines.push(...buildHeaderLines({
|
|
17
|
+
steps: state.steps,
|
|
18
|
+
version: state.version,
|
|
19
|
+
currentStepIndex: state.currentStepIndex,
|
|
20
|
+
currentPhase: state.currentPhase,
|
|
21
|
+
completedStepRecords: state.completedStepRecords,
|
|
22
|
+
}));
|
|
23
|
+
lines.push("");
|
|
24
|
+
if (state.isFinished) {
|
|
25
|
+
if (state.versionReport) {
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push(...buildVersionReportLines(state.versionReport));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const currentStep = state.steps[state.currentStepIndex];
|
|
32
|
+
if (currentStep) {
|
|
33
|
+
lines.push("");
|
|
34
|
+
lines.push(...buildActiveStepLines({
|
|
35
|
+
step: currentStep,
|
|
36
|
+
stepNumber: state.currentStepIndex + 1,
|
|
37
|
+
totalSteps: state.steps.length,
|
|
38
|
+
phase: state.currentPhase,
|
|
39
|
+
}));
|
|
40
|
+
const isShowingOutput = (state.currentPhase === "running" || state.currentPhase === "installing") &&
|
|
41
|
+
state.currentOutputLines.length > 0;
|
|
42
|
+
if (isShowingOutput) {
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(...buildOutputBoxLines(state.currentOutputLines, currentStep.name));
|
|
45
|
+
}
|
|
46
|
+
if (state.currentPrompt) {
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push(...buildPromptLines(state.currentPrompt));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
lines.push("");
|
|
53
|
+
return lines.join("\n") + "\n";
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Step } from "../steps/index.js";
|
|
2
|
+
import type { CommandOutputLine } from "../runner.js";
|
|
3
|
+
export type StepPhase = "pending" | "checking-availability" | "prompting-to-install" | "installing" | "prompting-to-run" | "running" | "complete" | "failed" | "skipped" | "not-available";
|
|
4
|
+
export type CompletedStepRecord = {
|
|
5
|
+
phase: StepPhase;
|
|
6
|
+
summaryNote?: string;
|
|
7
|
+
};
|
|
8
|
+
export type CurrentPrompt = {
|
|
9
|
+
question: string;
|
|
10
|
+
warning?: string;
|
|
11
|
+
};
|
|
12
|
+
export type ResolvedVersion = {
|
|
13
|
+
label: string;
|
|
14
|
+
value: string;
|
|
15
|
+
};
|
|
16
|
+
export type AppState = {
|
|
17
|
+
steps: Step[];
|
|
18
|
+
version: string;
|
|
19
|
+
currentStepIndex: number;
|
|
20
|
+
currentPhase: StepPhase;
|
|
21
|
+
completedStepRecords: CompletedStepRecord[];
|
|
22
|
+
currentOutputLines: CommandOutputLine[];
|
|
23
|
+
currentPrompt: CurrentPrompt | undefined;
|
|
24
|
+
isFinished: boolean;
|
|
25
|
+
versionReport: ResolvedVersion[] | undefined;
|
|
26
|
+
};
|
|
27
|
+
export declare function createInitialAppState(steps: Step[], version: string): AppState;
|
package/dist/ui/state.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function createInitialAppState(steps, version) {
|
|
2
|
+
return {
|
|
3
|
+
steps,
|
|
4
|
+
version,
|
|
5
|
+
currentStepIndex: 0,
|
|
6
|
+
currentPhase: "checking-availability",
|
|
7
|
+
completedStepRecords: [],
|
|
8
|
+
currentOutputLines: [],
|
|
9
|
+
currentPrompt: undefined,
|
|
10
|
+
isFinished: false,
|
|
11
|
+
versionReport: undefined,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { captureShellCommandOutput } from "../runner.js";
|
|
3
|
+
const VERSION_ENTRIES = [
|
|
4
|
+
{ label: "macOS", shellCommand: "sw_vers -productVersion" },
|
|
5
|
+
{ label: "Homebrew", shellCommand: "brew --version | head -n1" },
|
|
6
|
+
{ label: "Node", shellCommand: "node -v 2>/dev/null" },
|
|
7
|
+
{ label: "NPM", shellCommand: "npm -v 2>/dev/null" },
|
|
8
|
+
{ label: "Python", shellCommand: "python3 --version 2>/dev/null" },
|
|
9
|
+
{ label: "pip3", shellCommand: "pip3 --version 2>/dev/null | cut -d' ' -f1-2" },
|
|
10
|
+
{ label: "Zsh", shellCommand: "zsh --version 2>/dev/null" },
|
|
11
|
+
];
|
|
12
|
+
export async function collectVersions() {
|
|
13
|
+
return Promise.all(VERSION_ENTRIES.map(async ({ label, shellCommand }) => {
|
|
14
|
+
const value = await captureShellCommandOutput({ shellCommand }).catch(() => "");
|
|
15
|
+
return { label, value: value || "not installed" };
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
export function buildVersionReportLines(versions) {
|
|
19
|
+
return [
|
|
20
|
+
chalk.dim(" ─── Versions ─────────────────────────"),
|
|
21
|
+
...versions.map(({ label, value }) => ` ${chalk.dim("·")} ${chalk.bold(label + ":")} ${chalk.dim(value)}`),
|
|
22
|
+
"",
|
|
23
|
+
` ${chalk.green.bold("✓")} ${chalk.bold("All done!")}`,
|
|
24
|
+
];
|
|
25
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "braeburn",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "macOS system updater CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"braeburn": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsx src/index.ts",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^5.3.0",
|
|
23
|
+
"commander": "^12.1.0",
|
|
24
|
+
"execa": "^9.3.0",
|
|
25
|
+
"smol-toml": "^1.6.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"tsx": "^4.7.0",
|
|
30
|
+
"typescript": "^5.5.0"
|
|
31
|
+
}
|
|
32
|
+
}
|