braeburn 1.3.0 → 1.4.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.
Files changed (51) hide show
  1. package/dist/commands/setup.d.ts +9 -1
  2. package/dist/commands/setup.js +66 -55
  3. package/dist/commands/update.d.ts +2 -2
  4. package/dist/commands/update.js +22 -101
  5. package/dist/index.js +1 -13
  6. package/dist/runner.js +2 -2
  7. package/dist/steps/catalog.d.ts +2 -0
  8. package/dist/steps/catalog.js +13 -0
  9. package/dist/steps/cleanup.d.ts +1 -1
  10. package/dist/steps/cleanup.js +1 -1
  11. package/dist/steps/dotnet.d.ts +1 -1
  12. package/dist/steps/dotnet.js +1 -1
  13. package/dist/steps/homebrew.d.ts +1 -1
  14. package/dist/steps/homebrew.js +1 -1
  15. package/dist/steps/index.d.ts +2 -25
  16. package/dist/steps/index.js +1 -23
  17. package/dist/steps/macos.d.ts +1 -1
  18. package/dist/steps/mas.d.ts +1 -1
  19. package/dist/steps/mas.js +1 -1
  20. package/dist/steps/npm.d.ts +1 -1
  21. package/dist/steps/npm.js +1 -1
  22. package/dist/steps/nvm.d.ts +1 -1
  23. package/dist/steps/nvm.js +1 -1
  24. package/dist/steps/ohmyzsh.d.ts +1 -1
  25. package/dist/steps/ohmyzsh.js +1 -1
  26. package/dist/steps/pip.d.ts +1 -1
  27. package/dist/steps/pip.js +1 -1
  28. package/dist/steps/pyenv.d.ts +1 -1
  29. package/dist/steps/pyenv.js +1 -1
  30. package/dist/steps/runtime.d.ts +7 -0
  31. package/dist/steps/runtime.js +23 -0
  32. package/dist/steps/types.d.ts +21 -0
  33. package/dist/steps/types.js +1 -0
  34. package/dist/ui/currentStep.d.ts +2 -3
  35. package/dist/ui/header.d.ts +3 -4
  36. package/dist/ui/screen.js +5 -4
  37. package/dist/ui/state.d.ts +1 -30
  38. package/dist/ui/state.js +1 -14
  39. package/dist/ui/terminal.d.ts +1 -0
  40. package/dist/ui/terminal.js +14 -3
  41. package/dist/ui/versionReport.d.ts +0 -1
  42. package/dist/ui/versionReport.js +0 -16
  43. package/dist/update/displayStep.d.ts +4 -0
  44. package/dist/update/displayStep.js +11 -0
  45. package/dist/update/engine.d.ts +23 -0
  46. package/dist/update/engine.js +126 -0
  47. package/dist/update/state.d.ts +38 -0
  48. package/dist/update/state.js +15 -0
  49. package/dist/update/versionCollector.d.ts +2 -0
  50. package/dist/update/versionCollector.js +16 -0
  51. package/package.json +1 -1
@@ -1,9 +1,17 @@
1
1
  import type { Step } from "../steps/index.js";
2
+ import type { StepStage } from "../update/state.js";
2
3
  export type SelectionState = "selected" | "deselected";
3
4
  export type ProtectionStatus = "protected" | "configurable";
4
5
  export type AvailabilityStatus = "available" | "unavailable";
6
+ export type SetupStepView = {
7
+ id: string;
8
+ name: string;
9
+ description: string;
10
+ stage: StepStage;
11
+ brewPackageToInstall?: string;
12
+ };
5
13
  export type SelectableStep = {
6
- step: Step;
14
+ step: SetupStepView;
7
15
  selection: SelectionState;
8
16
  protection: ProtectionStatus;
9
17
  availability: AvailabilityStatus;
@@ -73,64 +73,75 @@ export function buildSetupScreen(items, cursorIndex) {
73
73
  }
74
74
  export async function runSetupCommand(allSteps) {
75
75
  const render = createScreenRenderer();
76
- hideCursorDuringExecution();
77
- render(buildLoadingScreen());
78
- const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
79
- const items = allSteps.map((step, stepIndex) => ({
80
- step,
81
- selection: PROTECTED_STEP_IDS.has(step.id) || step.stage === "tools" ? "selected" : "deselected",
82
- protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
83
- availability: availabilityResults[stepIndex] ? "available" : "unavailable",
84
- }));
85
- let cursorIndex = 0;
86
- render(buildSetupScreen(items, cursorIndex));
87
- await new Promise((resolve) => {
88
- readline.emitKeypressEvents(process.stdin);
89
- if (process.stdin.isTTY)
90
- process.stdin.setRawMode(true);
91
- const handleKeypress = (_char, key) => {
92
- if (key?.ctrl && key?.name === "c") {
93
- process.exit(130);
94
- }
95
- if (key?.name === "up" || key?.name === "k") {
96
- cursorIndex = Math.max(0, cursorIndex - 1);
97
- render(buildSetupScreen(items, cursorIndex));
98
- }
99
- else if (key?.name === "down" || key?.name === "j") {
100
- cursorIndex = Math.min(items.length - 1, cursorIndex + 1);
101
- render(buildSetupScreen(items, cursorIndex));
102
- }
103
- else if (key?.name === "space") {
104
- const item = items[cursorIndex];
105
- if (item.protection === "configurable") {
106
- item.selection = item.selection === "selected" ? "deselected" : "selected";
76
+ const restoreCursor = hideCursorDuringExecution({ screenBuffer: "alternate" });
77
+ try {
78
+ render(buildLoadingScreen());
79
+ const availabilityResults = await Promise.all(allSteps.map((step) => step.checkIsAvailable()));
80
+ const items = allSteps.map((step, stepIndex) => ({
81
+ step: {
82
+ id: step.id,
83
+ name: step.name,
84
+ description: step.description,
85
+ stage: step.stage,
86
+ brewPackageToInstall: step.brewPackageToInstall,
87
+ },
88
+ selection: PROTECTED_STEP_IDS.has(step.id) || step.stage === "tools" ? "selected" : "deselected",
89
+ protection: PROTECTED_STEP_IDS.has(step.id) ? "protected" : "configurable",
90
+ availability: availabilityResults[stepIndex] ? "available" : "unavailable",
91
+ }));
92
+ let cursorIndex = 0;
93
+ render(buildSetupScreen(items, cursorIndex));
94
+ await new Promise((resolve) => {
95
+ readline.emitKeypressEvents(process.stdin);
96
+ if (process.stdin.isTTY)
97
+ process.stdin.setRawMode(true);
98
+ const handleKeypress = (_char, key) => {
99
+ if (key?.ctrl && key?.name === "c") {
100
+ process.exit(130);
101
+ }
102
+ if (key?.name === "up" || key?.name === "k") {
103
+ cursorIndex = Math.max(0, cursorIndex - 1);
107
104
  render(buildSetupScreen(items, cursorIndex));
108
105
  }
106
+ else if (key?.name === "down" || key?.name === "j") {
107
+ cursorIndex = Math.min(items.length - 1, cursorIndex + 1);
108
+ render(buildSetupScreen(items, cursorIndex));
109
+ }
110
+ else if (key?.name === "space") {
111
+ const item = items[cursorIndex];
112
+ if (item.protection === "configurable") {
113
+ item.selection = item.selection === "selected" ? "deselected" : "selected";
114
+ render(buildSetupScreen(items, cursorIndex));
115
+ }
116
+ }
117
+ else if (key?.name === "return") {
118
+ process.stdin.removeListener("keypress", handleKeypress);
119
+ if (process.stdin.isTTY)
120
+ process.stdin.setRawMode(false);
121
+ process.stdin.pause();
122
+ resolve();
123
+ }
124
+ };
125
+ process.stdin.on("keypress", handleKeypress);
126
+ process.stdin.resume();
127
+ });
128
+ const stepsConfig = {};
129
+ for (const item of items) {
130
+ if (item.protection === "configurable" && item.selection === "deselected") {
131
+ stepsConfig[item.step.id] = false;
109
132
  }
110
- else if (key?.name === "return") {
111
- process.stdin.removeListener("keypress", handleKeypress);
112
- if (process.stdin.isTTY)
113
- process.stdin.setRawMode(false);
114
- process.stdin.pause();
115
- resolve();
116
- }
117
- };
118
- process.stdin.on("keypress", handleKeypress);
119
- process.stdin.resume();
120
- });
121
- const stepsConfig = {};
122
- for (const item of items) {
123
- if (item.protection === "configurable" && item.selection === "deselected") {
124
- stepsConfig[item.step.id] = false;
125
133
  }
134
+ await writeConfig({ steps: stepsConfig });
135
+ const confirmationLines = [
136
+ chalk.yellow(LOGO_ART),
137
+ "",
138
+ ` ${chalk.green("\u2713")} Setup complete! Starting your first update\u2026`,
139
+ "",
140
+ ];
141
+ render(confirmationLines.join("\n") + "\n");
142
+ await new Promise((resolve) => setTimeout(resolve, 800));
143
+ }
144
+ finally {
145
+ restoreCursor();
126
146
  }
127
- await writeConfig({ steps: stepsConfig });
128
- const confirmationLines = [
129
- chalk.yellow(LOGO_ART),
130
- "",
131
- ` ${chalk.green("\u2713")} Setup complete! Starting your first update\u2026`,
132
- "",
133
- ];
134
- render(confirmationLines.join("\n") + "\n");
135
- await new Promise((resolve) => setTimeout(resolve, 800));
136
147
  }
@@ -1,6 +1,6 @@
1
+ import { type PromptMode } from "../update/engine.js";
2
+ import type { LogoVisibility } from "../update/state.js";
1
3
  import type { Step } from "../steps/index.js";
2
- type PromptMode = "interactive" | "auto-accept";
3
- type LogoVisibility = "visible" | "hidden";
4
4
  type RunUpdateCommandOptions = {
5
5
  steps: Step[];
6
6
  promptMode: PromptMode;
@@ -1,109 +1,30 @@
1
- import { runShellCommand } from "../runner.js";
2
- import { createLogWriterForStep } from "../logger.js";
3
- import { collectVersions } from "../ui/versionReport.js";
1
+ import { collectVersions } from "../update/versionCollector.js";
4
2
  import { captureYesNo } from "../ui/prompt.js";
5
- import { createInitialAppState } from "../ui/state.js";
6
3
  import { buildScreen, createScreenRenderer } from "../ui/screen.js";
7
4
  import { hideCursorDuringExecution } from "../ui/terminal.js";
8
- import { createDefaultStepRunContext } from "../steps/index.js";
5
+ import { runUpdateEngine } from "../update/engine.js";
9
6
  export async function runUpdateCommand(options) {
10
- const { steps, version } = options;
11
- let autoAccept = options.promptMode === "auto-accept";
12
- const state = createInitialAppState(steps, version, options.logoVisibility);
13
7
  const renderScreen = createScreenRenderer();
14
- hideCursorDuringExecution();
15
- renderScreen(buildScreen(state));
16
- for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
17
- const step = steps[stepIndex];
18
- state.currentStepIndex = stepIndex;
19
- state.currentPhase = "checking-availability";
20
- state.currentOutputLines = [];
21
- state.currentPrompt = undefined;
22
- renderScreen(buildScreen(state));
23
- const isAvailable = await step.checkIsAvailable();
24
- if (!isAvailable && !step.brewPackageToInstall) {
25
- state.currentPhase = "not-available";
26
- renderScreen(buildScreen(state));
27
- state.completedStepRecords.push({ phase: "not-available", summaryNote: "not installed" });
28
- continue;
29
- }
30
- if (!isAvailable && step.brewPackageToInstall) {
31
- state.currentPhase = "prompting-to-install";
32
- state.currentPrompt = {
33
- question: `Install ${step.name} via Homebrew? (brew install ${step.brewPackageToInstall})`,
34
- };
35
- renderScreen(buildScreen(state));
36
- const installAnswer = autoAccept ? "yes" : await captureYesNo();
37
- if (installAnswer === "force")
38
- autoAccept = true;
39
- const shouldInstall = installAnswer !== "no";
40
- state.currentPrompt = undefined;
41
- if (!shouldInstall) {
42
- state.currentPhase = "skipped";
8
+ const restoreCursor = hideCursorDuringExecution({ screenBuffer: "alternate" });
9
+ let finalScreen = "";
10
+ try {
11
+ const finalState = await runUpdateEngine({
12
+ steps: options.steps,
13
+ promptMode: options.promptMode,
14
+ version: options.version,
15
+ logoVisibility: options.logoVisibility,
16
+ askForConfirmation: captureYesNo,
17
+ collectVersions,
18
+ onStateChanged: (state) => {
43
19
  renderScreen(buildScreen(state));
44
- state.completedStepRecords.push({ phase: "skipped" });
45
- continue;
46
- }
47
- state.currentPhase = "installing";
48
- renderScreen(buildScreen(state));
49
- try {
50
- const installLogWriter = await createLogWriterForStep(`${step.id}-install`);
51
- await runShellCommand({
52
- shellCommand: `brew install ${step.brewPackageToInstall}`,
53
- onOutputLine: (line) => {
54
- state.currentOutputLines.push(line);
55
- renderScreen(buildScreen(state));
56
- },
57
- logWriter: installLogWriter,
58
- });
59
- }
60
- catch {
61
- state.currentPhase = "failed";
62
- renderScreen(buildScreen(state));
63
- state.completedStepRecords.push({ phase: "failed", summaryNote: "install failed" });
64
- continue;
65
- }
66
- }
67
- state.currentPhase = "prompting-to-run";
68
- state.currentPrompt = { question: `Run ${step.name} update?`, warning: step.warning };
69
- renderScreen(buildScreen(state));
70
- const runAnswer = autoAccept ? "yes" : await captureYesNo();
71
- if (runAnswer === "force")
72
- autoAccept = true;
73
- const shouldRun = runAnswer !== "no";
74
- state.currentPrompt = undefined;
75
- if (!shouldRun) {
76
- state.currentPhase = "skipped";
77
- renderScreen(buildScreen(state));
78
- state.completedStepRecords.push({ phase: "skipped" });
79
- continue;
80
- }
81
- state.currentPhase = "running";
82
- state.currentOutputLines = [];
83
- renderScreen(buildScreen(state));
84
- const stepLogWriter = await createLogWriterForStep(step.id);
85
- try {
86
- await step.run(createDefaultStepRunContext((line) => {
87
- state.currentOutputLines.push(line);
88
- renderScreen(buildScreen(state));
89
- }, stepLogWriter));
90
- state.currentPhase = "complete";
91
- state.currentOutputLines = [];
92
- renderScreen(buildScreen(state));
93
- state.completedStepRecords.push({ phase: "complete", summaryNote: "updated" });
94
- }
95
- catch (error) {
96
- const errorMessage = error instanceof Error ? error.message : String(error);
97
- state.currentPhase = "failed";
98
- state.currentOutputLines = [];
99
- renderScreen(buildScreen(state));
100
- state.completedStepRecords.push({ phase: "failed", summaryNote: errorMessage });
101
- }
20
+ },
21
+ });
22
+ finalScreen = buildScreen(finalState);
23
+ }
24
+ finally {
25
+ restoreCursor();
26
+ }
27
+ if (finalScreen) {
28
+ process.stdout.write(finalScreen);
102
29
  }
103
- state.runCompletion = "finished";
104
- state.currentOutputLines = [];
105
- state.currentPrompt = undefined;
106
- renderScreen(buildScreen(state));
107
- state.versionReport = await collectVersions();
108
- renderScreen(buildScreen(state));
109
30
  }
package/dist/index.js CHANGED
@@ -3,24 +3,12 @@ import { Command } from "commander";
3
3
  import { createRequire } from "node:module";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { dirname, join } from "node:path";
6
- import { homebrewStep, masStep, ohmyzshStep, npmStep, pipStep, pyenvStep, nvmStep, dotnetStep, macosStep, cleanupStep, } from "./steps/index.js";
6
+ import { ALL_STEPS } from "./steps/catalog.js";
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
10
  import { runSetupCommand } from "./commands/setup.js";
11
11
  import { readConfig, isStepEnabled, isLogoEnabled, PROTECTED_STEP_IDS, configFileExists } from "./config.js";
12
- const ALL_STEPS = [
13
- pyenvStep,
14
- nvmStep,
15
- homebrewStep,
16
- masStep,
17
- ohmyzshStep,
18
- npmStep,
19
- pipStep,
20
- dotnetStep,
21
- macosStep,
22
- cleanupStep,
23
- ];
24
12
  const STEP_IDS_BY_NAME = new Map(ALL_STEPS.map((step) => [step.id, step]));
25
13
  const requireFromThis = createRequire(import.meta.url);
26
14
  const packageJson = requireFromThis(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"));
package/dist/runner.js CHANGED
@@ -5,14 +5,14 @@ export async function runShellCommand(options) {
5
5
  reject: true,
6
6
  });
7
7
  subprocess.stdout?.on("data", (chunk) => {
8
- const lines = String(chunk).split("\n").filter(Boolean);
8
+ const lines = String(chunk).split(/\r?\n|\r/).filter(Boolean);
9
9
  for (const line of lines) {
10
10
  options.onOutputLine({ text: line, source: "stdout" });
11
11
  options.logWriter(line);
12
12
  }
13
13
  });
14
14
  subprocess.stderr?.on("data", (chunk) => {
15
- const lines = String(chunk).split("\n").filter(Boolean);
15
+ const lines = String(chunk).split(/\r?\n|\r/).filter(Boolean);
16
16
  for (const line of lines) {
17
17
  options.onOutputLine({ text: line, source: "stderr" });
18
18
  options.logWriter(line);
@@ -0,0 +1,2 @@
1
+ import { type Step } from "./index.js";
2
+ export declare const ALL_STEPS: Step[];
@@ -0,0 +1,13 @@
1
+ import { pyenvStep, nvmStep, homebrewStep, masStep, ohmyzshStep, npmStep, pipStep, dotnetStep, macosStep, cleanupStep, } from "./index.js";
2
+ export const ALL_STEPS = [
3
+ pyenvStep,
4
+ nvmStep,
5
+ homebrewStep,
6
+ masStep,
7
+ ohmyzshStep,
8
+ npmStep,
9
+ pipStep,
10
+ dotnetStep,
11
+ macosStep,
12
+ cleanupStep,
13
+ ];
@@ -1,3 +1,3 @@
1
- import { type Step } from "./index.js";
1
+ import type { Step } from "./types.js";
2
2
  declare const cleanupStep: Step;
3
3
  export default cleanupStep;
@@ -1,4 +1,4 @@
1
- import { checkCommandExists } from "./index.js";
1
+ import { checkCommandExists } from "./runtime.js";
2
2
  const cleanupStep = {
3
3
  id: "cleanup",
4
4
  name: "Cleanup",
@@ -1,3 +1,3 @@
1
- import { type Step } from "./index.js";
1
+ import type { Step } from "./types.js";
2
2
  declare const dotnetStep: Step;
3
3
  export default dotnetStep;
@@ -1,4 +1,4 @@
1
- import { checkCommandExists } from "./index.js";
1
+ import { checkCommandExists } from "./runtime.js";
2
2
  const dotnetStep = {
3
3
  id: "dotnet",
4
4
  name: ".NET",
@@ -1,3 +1,3 @@
1
- import { type Step } from "./index.js";
1
+ import type { Step } from "./types.js";
2
2
  declare const homebrewStep: Step;
3
3
  export default homebrewStep;
@@ -1,4 +1,4 @@
1
- import { checkCommandExists } from "./index.js";
1
+ import { checkCommandExists } from "./runtime.js";
2
2
  const homebrewStep = {
3
3
  id: "homebrew",
4
4
  name: "Homebrew",
@@ -1,24 +1,5 @@
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
- runStep: (shellCommand: string) => Promise<void>;
7
- captureOutput: (options: {
8
- shellCommand: string;
9
- }) => Promise<string>;
10
- };
11
- export type StepStage = "runtime" | "tools";
12
- export type Step = {
13
- id: string;
14
- name: string;
15
- description: string;
16
- stage: StepStage;
17
- warning?: string;
18
- brewPackageToInstall?: string;
19
- checkIsAvailable: () => Promise<boolean>;
20
- run: (context: StepRunContext) => Promise<void>;
21
- };
1
+ export type { StepRunContext, StepStage, Step, } from "./types.js";
2
+ export { checkCommandExists, checkPathExists, runStep, createDefaultStepRunContext, } from "./runtime.js";
22
3
  export { default as homebrewStep } from "./homebrew.js";
23
4
  export { default as masStep } from "./mas.js";
24
5
  export { default as ohmyzshStep } from "./ohmyzsh.js";
@@ -29,7 +10,3 @@ export { default as nvmStep } from "./nvm.js";
29
10
  export { default as dotnetStep } from "./dotnet.js";
30
11
  export { default as macosStep } from "./macos.js";
31
12
  export { default as cleanupStep } from "./cleanup.js";
32
- export declare function checkCommandExists(command: string): Promise<boolean>;
33
- export declare function checkPathExists(filePath: string): Promise<boolean>;
34
- export declare function runStep(shellCommand: string, context: StepRunContext): Promise<void>;
35
- export declare function createDefaultStepRunContext(onOutputLine: OutputLineCallback, logWriter: StepLogWriter): StepRunContext;
@@ -1,4 +1,4 @@
1
- import { doesShellCommandSucceed, runShellCommand, captureShellCommandOutput, } from "../runner.js";
1
+ export { checkCommandExists, checkPathExists, runStep, createDefaultStepRunContext, } from "./runtime.js";
2
2
  export { default as homebrewStep } from "./homebrew.js";
3
3
  export { default as masStep } from "./mas.js";
4
4
  export { default as ohmyzshStep } from "./ohmyzsh.js";
@@ -9,25 +9,3 @@ export { default as nvmStep } from "./nvm.js";
9
9
  export { default as dotnetStep } from "./dotnet.js";
10
10
  export { default as macosStep } from "./macos.js";
11
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
- }
25
- export function createDefaultStepRunContext(onOutputLine, logWriter) {
26
- const context = {
27
- onOutputLine,
28
- logWriter,
29
- runStep: (shellCommand) => runStep(shellCommand, context),
30
- captureOutput: captureShellCommandOutput,
31
- };
32
- return context;
33
- }
@@ -1,3 +1,3 @@
1
- import { type Step } from "./index.js";
1
+ import { type Step } from "./types.js";
2
2
  declare const macosStep: Step;
3
3
  export default macosStep;
@@ -1,3 +1,3 @@
1
- import { type Step } from "./index.js";
1
+ import type { Step } from "./types.js";
2
2
  declare const masStep: Step;
3
3
  export default masStep;
package/dist/steps/mas.js CHANGED
@@ -1,4 +1,4 @@
1
- import { checkCommandExists } from "./index.js";
1
+ import { checkCommandExists } from "./runtime.js";
2
2
  const masStep = {
3
3
  id: "mas",
4
4
  name: "Mac App Store",
@@ -1,3 +1,3 @@
1
- import { type Step } from "./index.js";
1
+ import type { Step } from "./types.js";
2
2
  declare const npmStep: Step;
3
3
  export default npmStep;
package/dist/steps/npm.js CHANGED
@@ -1,4 +1,4 @@
1
- import { checkCommandExists } from "./index.js";
1
+ import { checkCommandExists } from "./runtime.js";
2
2
  const npmStep = {
3
3
  id: "npm",
4
4
  name: "npm",
@@ -1,3 +1,3 @@
1
- import { type Step } from "./index.js";
1
+ import type { Step } from "./types.js";
2
2
  declare const nvmStep: Step;
3
3
  export default nvmStep;
package/dist/steps/nvm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
- import { checkPathExists } from "./index.js";
3
+ import { checkPathExists } from "./runtime.js";
4
4
  const NVM_DIRECTORY = join(homedir(), ".nvm");
5
5
  // nvm is a shell function sourced from nvm.sh — it cannot be invoked as a
6
6
  // standalone binary, so we source it explicitly inside each bash invocation.
@@ -1,3 +1,3 @@
1
- import { type Step } from "./index.js";
1
+ import type { Step } from "./types.js";
2
2
  declare const ohmyzshStep: Step;
3
3
  export default ohmyzshStep;
@@ -1,6 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
- import { checkPathExists } from "./index.js";
3
+ import { checkPathExists } from "./runtime.js";
4
4
  const OH_MY_ZSH_UPGRADE_SCRIPT_PATH = join(homedir(), ".oh-my-zsh", "tools", "upgrade.sh");
5
5
  const ohmyzshStep = {
6
6
  id: "ohmyzsh",
@@ -1,3 +1,3 @@
1
- import { type Step } from "./index.js";
1
+ import type { Step } from "./types.js";
2
2
  declare const pipStep: Step;
3
3
  export default pipStep;
package/dist/steps/pip.js CHANGED
@@ -1,4 +1,4 @@
1
- import { checkCommandExists } from "./index.js";
1
+ import { checkCommandExists } from "./runtime.js";
2
2
  const PIP_UPDATE_ALL_OUTDATED_SHELL_COMMAND = "pip3 list --outdated --format=columns | tail -n +3 | awk '{print $1}' | xargs -n1 pip3 install -U";
3
3
  const pipStep = {
4
4
  id: "pip",
@@ -1,3 +1,3 @@
1
- import { type Step } from "./index.js";
1
+ import type { Step } from "./types.js";
2
2
  declare const pyenvStep: Step;
3
3
  export default pyenvStep;
@@ -1,4 +1,4 @@
1
- import { checkCommandExists, } from "./index.js";
1
+ import { checkCommandExists, } from "./runtime.js";
2
2
  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 ' '";
3
3
  const pyenvStep = {
4
4
  id: "pyenv",
@@ -0,0 +1,7 @@
1
+ import { type OutputLineCallback } from "../runner.js";
2
+ import type { StepLogWriter } from "../logger.js";
3
+ import type { StepRunContext } from "./types.js";
4
+ export declare function checkCommandExists(command: string): Promise<boolean>;
5
+ export declare function checkPathExists(filePath: string): Promise<boolean>;
6
+ export declare function runStep(shellCommand: string, context: StepRunContext): Promise<void>;
7
+ export declare function createDefaultStepRunContext(onOutputLine: OutputLineCallback, logWriter: StepLogWriter): StepRunContext;
@@ -0,0 +1,23 @@
1
+ import { doesShellCommandSucceed, runShellCommand, captureShellCommandOutput, } from "../runner.js";
2
+ export async function checkCommandExists(command) {
3
+ return doesShellCommandSucceed({ shellCommand: `command -v ${command}` });
4
+ }
5
+ export async function checkPathExists(filePath) {
6
+ return doesShellCommandSucceed({ shellCommand: `test -e "${filePath}"` });
7
+ }
8
+ export async function runStep(shellCommand, context) {
9
+ await runShellCommand({
10
+ shellCommand,
11
+ onOutputLine: context.onOutputLine,
12
+ logWriter: context.logWriter,
13
+ });
14
+ }
15
+ export function createDefaultStepRunContext(onOutputLine, logWriter) {
16
+ const context = {
17
+ onOutputLine,
18
+ logWriter,
19
+ runStep: (shellCommand) => runStep(shellCommand, context),
20
+ captureOutput: captureShellCommandOutput,
21
+ };
22
+ return context;
23
+ }
@@ -0,0 +1,21 @@
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
+ runStep: (shellCommand: string) => Promise<void>;
7
+ captureOutput: (options: {
8
+ shellCommand: string;
9
+ }) => Promise<string>;
10
+ };
11
+ export type StepStage = "runtime" | "tools";
12
+ export type Step = {
13
+ id: string;
14
+ name: string;
15
+ description: string;
16
+ stage: StepStage;
17
+ warning?: string;
18
+ brewPackageToInstall?: string;
19
+ checkIsAvailable: () => Promise<boolean>;
20
+ run: (context: StepRunContext) => Promise<void>;
21
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,7 +1,6 @@
1
- import type { Step } from "../steps/index.js";
2
- import type { StepPhase } from "./state.js";
1
+ import type { DisplayStep, StepPhase } from "./state.js";
3
2
  type ActiveStepOptions = {
4
- step: Step;
3
+ step: DisplayStep;
5
4
  stepNumber: number;
6
5
  totalSteps: number;
7
6
  phase: StepPhase;
@@ -1,13 +1,12 @@
1
- import type { Step } from "../steps/index.js";
2
- import type { StepPhase, CompletedStepRecord, LogoVisibility } from "./state.js";
1
+ import type { DisplayStep, StepPhase, CompletedStepRecord, LogoVisibility } from "./state.js";
3
2
  import type { TerminalDimensions } from "./outputBox.js";
4
3
  type LogoLayout = "side-by-side" | "stacked" | "none";
5
4
  export declare function determineLogoLayout(logoLines: string[], dimensions?: TerminalDimensions): LogoLayout;
6
5
  export declare function stepTrackerIcon(phase: StepPhase): string;
7
6
  export declare function isActivePhase(phase: StepPhase): boolean;
8
- export declare function deriveAllStepPhases(steps: Step[], currentStepIndex: number, currentPhase: StepPhase, completedStepRecords: CompletedStepRecord[]): StepPhase[];
7
+ export declare function deriveAllStepPhases(steps: DisplayStep[], currentStepIndex: number, currentPhase: StepPhase, completedStepRecords: CompletedStepRecord[]): StepPhase[];
9
8
  type BuildHeaderOptions = {
10
- steps: Step[];
9
+ steps: DisplayStep[];
11
10
  version: string;
12
11
  logoVisibility: LogoVisibility;
13
12
  currentStepIndex: number;
package/dist/ui/screen.js CHANGED
@@ -4,13 +4,14 @@ 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 previousLineCount = 0;
7
+ let hasAnchor = false;
8
8
  return (content) => {
9
- if (previousLineCount > 0) {
10
- output.write(`\x1b[${previousLineCount}A\x1b[J`);
9
+ if (!hasAnchor) {
10
+ output.write("\x1b7");
11
+ hasAnchor = true;
11
12
  }
13
+ output.write("\x1b8\x1b[J");
12
14
  output.write(content);
13
- previousLineCount = (content.match(/\n/g) ?? []).length;
14
15
  };
15
16
  }
16
17
  export function buildScreen(state, terminalDimensions) {
@@ -1,30 +1 @@
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 LogoVisibility = "visible" | "hidden";
17
- export type RunCompletion = "in-progress" | "finished";
18
- export type AppState = {
19
- steps: Step[];
20
- version: string;
21
- logoVisibility: LogoVisibility;
22
- currentStepIndex: number;
23
- currentPhase: StepPhase;
24
- completedStepRecords: CompletedStepRecord[];
25
- currentOutputLines: CommandOutputLine[];
26
- currentPrompt: CurrentPrompt | undefined;
27
- runCompletion: RunCompletion;
28
- versionReport: ResolvedVersion[] | undefined;
29
- };
30
- export declare function createInitialAppState(steps: Step[], version: string, logoVisibility: LogoVisibility): AppState;
1
+ export { type StepStage, type DisplayStep, type StepPhase, type CompletedStepRecord, type CurrentPrompt, type ResolvedVersion, type LogoVisibility, type RunCompletion, type UpdateState, type AppState, createInitialUpdateState, createInitialAppState, } from "../update/state.js";
package/dist/ui/state.js CHANGED
@@ -1,14 +1 @@
1
- export function createInitialAppState(steps, version, logoVisibility) {
2
- return {
3
- steps,
4
- version,
5
- logoVisibility,
6
- currentStepIndex: 0,
7
- currentPhase: "checking-availability",
8
- completedStepRecords: [],
9
- currentOutputLines: [],
10
- currentPrompt: undefined,
11
- runCompletion: "in-progress",
12
- versionReport: undefined,
13
- };
14
- }
1
+ export { createInitialUpdateState, createInitialAppState, } from "../update/state.js";
@@ -1,5 +1,6 @@
1
1
  type TerminalOptions = {
2
2
  output?: NodeJS.WritableStream;
3
+ screenBuffer?: "main" | "alternate";
3
4
  };
4
5
  type CursorCleanup = () => void;
5
6
  export declare function hideCursorDuringExecution(options?: TerminalOptions): CursorCleanup;
@@ -1,9 +1,20 @@
1
1
  export function hideCursorDuringExecution(options = {}) {
2
2
  const output = options.output ?? process.stdout;
3
+ const screenBuffer = options.screenBuffer ?? "main";
4
+ if (screenBuffer === "alternate") {
5
+ output.write("\x1b[?1049h");
6
+ }
3
7
  output.write("\x1b[?25l");
4
- const restoreOnExit = () => output.write("\x1b[?25h");
8
+ const restoreTerminal = () => {
9
+ output.write("\x1b[?25h");
10
+ if (screenBuffer === "alternate") {
11
+ output.write("\x1b[?1049l");
12
+ }
13
+ };
14
+ const restoreOnExit = () => restoreTerminal();
5
15
  const restoreAndExitOnInterrupt = () => {
6
- output.write("\x1b[?25h\n");
16
+ restoreTerminal();
17
+ output.write("\n");
7
18
  process.exit(130);
8
19
  };
9
20
  process.on("exit", restoreOnExit);
@@ -11,6 +22,6 @@ export function hideCursorDuringExecution(options = {}) {
11
22
  return () => {
12
23
  process.removeListener("exit", restoreOnExit);
13
24
  process.removeListener("SIGINT", restoreAndExitOnInterrupt);
14
- output.write("\x1b[?25h");
25
+ restoreTerminal();
15
26
  };
16
27
  }
@@ -1,3 +1,2 @@
1
1
  import type { ResolvedVersion } from "./state.js";
2
- export declare function collectVersions(): Promise<ResolvedVersion[]>;
3
2
  export declare function buildVersionReportLines(versions: ResolvedVersion[]): string[];
@@ -1,20 +1,4 @@
1
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
2
  export function buildVersionReportLines(versions) {
19
3
  return [
20
4
  chalk.dim(" ─── Versions ─────────────────────────"),
@@ -0,0 +1,4 @@
1
+ import type { Step } from "../steps/index.js";
2
+ import type { DisplayStep } from "./state.js";
3
+ export declare function toDisplayStep(step: Step): DisplayStep;
4
+ export declare function toDisplaySteps(steps: Step[]): DisplayStep[];
@@ -0,0 +1,11 @@
1
+ export function toDisplayStep(step) {
2
+ return {
3
+ id: step.id,
4
+ name: step.name,
5
+ description: step.description,
6
+ stage: step.stage,
7
+ };
8
+ }
9
+ export function toDisplaySteps(steps) {
10
+ return steps.map(toDisplayStep);
11
+ }
@@ -0,0 +1,23 @@
1
+ import { type StepLogWriter } from "../logger.js";
2
+ import { runShellCommand } from "../runner.js";
3
+ import { createDefaultStepRunContext, type Step } from "../steps/index.js";
4
+ import { type LogoVisibility, type ResolvedVersion, type UpdateState } from "./state.js";
5
+ export type PromptMode = "interactive" | "auto-accept";
6
+ export type ConfirmationAnswer = "yes" | "no" | "force";
7
+ type UpdateEngineDependencies = {
8
+ createLogWriter: (stepId: string) => Promise<StepLogWriter>;
9
+ runCommand: typeof runShellCommand;
10
+ createStepRunContext: typeof createDefaultStepRunContext;
11
+ };
12
+ type RunUpdateEngineOptions = {
13
+ steps: Step[];
14
+ promptMode: PromptMode;
15
+ version: string;
16
+ logoVisibility: LogoVisibility;
17
+ askForConfirmation: () => Promise<ConfirmationAnswer>;
18
+ collectVersions: () => Promise<ResolvedVersion[]>;
19
+ onStateChanged: (state: UpdateState) => void;
20
+ dependencies?: Partial<UpdateEngineDependencies>;
21
+ };
22
+ export declare function runUpdateEngine(options: RunUpdateEngineOptions): Promise<UpdateState>;
23
+ export {};
@@ -0,0 +1,126 @@
1
+ import { createLogWriterForStep } from "../logger.js";
2
+ import { runShellCommand } from "../runner.js";
3
+ import { createDefaultStepRunContext } from "../steps/index.js";
4
+ import { toDisplaySteps } from "./displayStep.js";
5
+ import { createInitialUpdateState, } from "./state.js";
6
+ function resolveDependencies(dependencyOverrides) {
7
+ return {
8
+ createLogWriter: dependencyOverrides?.createLogWriter ?? createLogWriterForStep,
9
+ runCommand: dependencyOverrides?.runCommand ?? runShellCommand,
10
+ createStepRunContext: dependencyOverrides?.createStepRunContext ?? createDefaultStepRunContext,
11
+ };
12
+ }
13
+ async function resolvePrompt(promptMode, askForConfirmation) {
14
+ if (promptMode === "auto-accept") {
15
+ return { promptMode: "auto-accept", decision: "run" };
16
+ }
17
+ const answer = await askForConfirmation();
18
+ if (answer === "no") {
19
+ return { promptMode: "interactive", decision: "skip" };
20
+ }
21
+ if (answer === "force") {
22
+ return { promptMode: "auto-accept", decision: "run" };
23
+ }
24
+ return { promptMode: "interactive", decision: "run" };
25
+ }
26
+ function reportState(state, onStateChanged) {
27
+ onStateChanged(state);
28
+ }
29
+ export async function runUpdateEngine(options) {
30
+ const dependencies = resolveDependencies(options.dependencies);
31
+ const state = createInitialUpdateState(toDisplaySteps(options.steps), options.version, options.logoVisibility);
32
+ let promptMode = options.promptMode;
33
+ reportState(state, options.onStateChanged);
34
+ for (let stepIndex = 0; stepIndex < options.steps.length; stepIndex++) {
35
+ const step = options.steps[stepIndex];
36
+ state.currentStepIndex = stepIndex;
37
+ state.currentPhase = "checking-availability";
38
+ state.currentOutputLines = [];
39
+ state.currentPrompt = undefined;
40
+ reportState(state, options.onStateChanged);
41
+ const availabilityStatus = (await step.checkIsAvailable())
42
+ ? "available"
43
+ : "unavailable";
44
+ if (availabilityStatus === "unavailable" && !step.brewPackageToInstall) {
45
+ state.currentPhase = "not-available";
46
+ reportState(state, options.onStateChanged);
47
+ state.completedStepRecords.push({ phase: "not-available", summaryNote: "not installed" });
48
+ continue;
49
+ }
50
+ if (availabilityStatus === "unavailable" && step.brewPackageToInstall) {
51
+ state.currentPhase = "prompting-to-install";
52
+ state.currentPrompt = {
53
+ question: `Install ${step.name} via Homebrew? (brew install ${step.brewPackageToInstall})`,
54
+ };
55
+ reportState(state, options.onStateChanged);
56
+ const installPrompt = await resolvePrompt(promptMode, options.askForConfirmation);
57
+ promptMode = installPrompt.promptMode;
58
+ state.currentPrompt = undefined;
59
+ if (installPrompt.decision === "skip") {
60
+ state.currentPhase = "skipped";
61
+ reportState(state, options.onStateChanged);
62
+ state.completedStepRecords.push({ phase: "skipped" });
63
+ continue;
64
+ }
65
+ state.currentPhase = "installing";
66
+ reportState(state, options.onStateChanged);
67
+ try {
68
+ const installLogWriter = await dependencies.createLogWriter(`${step.id}-install`);
69
+ await dependencies.runCommand({
70
+ shellCommand: `brew install ${step.brewPackageToInstall}`,
71
+ onOutputLine: (line) => {
72
+ state.currentOutputLines.push(line);
73
+ reportState(state, options.onStateChanged);
74
+ },
75
+ logWriter: installLogWriter,
76
+ });
77
+ }
78
+ catch {
79
+ state.currentPhase = "failed";
80
+ reportState(state, options.onStateChanged);
81
+ state.completedStepRecords.push({ phase: "failed", summaryNote: "install failed" });
82
+ continue;
83
+ }
84
+ }
85
+ state.currentPhase = "prompting-to-run";
86
+ state.currentPrompt = { question: `Run ${step.name} update?`, warning: step.warning };
87
+ reportState(state, options.onStateChanged);
88
+ const runPrompt = await resolvePrompt(promptMode, options.askForConfirmation);
89
+ promptMode = runPrompt.promptMode;
90
+ state.currentPrompt = undefined;
91
+ if (runPrompt.decision === "skip") {
92
+ state.currentPhase = "skipped";
93
+ reportState(state, options.onStateChanged);
94
+ state.completedStepRecords.push({ phase: "skipped" });
95
+ continue;
96
+ }
97
+ state.currentPhase = "running";
98
+ state.currentOutputLines = [];
99
+ reportState(state, options.onStateChanged);
100
+ const stepLogWriter = await dependencies.createLogWriter(step.id);
101
+ try {
102
+ await step.run(dependencies.createStepRunContext((line) => {
103
+ state.currentOutputLines.push(line);
104
+ reportState(state, options.onStateChanged);
105
+ }, stepLogWriter));
106
+ state.currentPhase = "complete";
107
+ state.currentOutputLines = [];
108
+ reportState(state, options.onStateChanged);
109
+ state.completedStepRecords.push({ phase: "complete", summaryNote: "updated" });
110
+ }
111
+ catch (error) {
112
+ const errorMessage = error instanceof Error ? error.message : String(error);
113
+ state.currentPhase = "failed";
114
+ state.currentOutputLines = [];
115
+ reportState(state, options.onStateChanged);
116
+ state.completedStepRecords.push({ phase: "failed", summaryNote: errorMessage });
117
+ }
118
+ }
119
+ state.runCompletion = "finished";
120
+ state.currentOutputLines = [];
121
+ state.currentPrompt = undefined;
122
+ reportState(state, options.onStateChanged);
123
+ state.versionReport = await options.collectVersions();
124
+ reportState(state, options.onStateChanged);
125
+ return state;
126
+ }
@@ -0,0 +1,38 @@
1
+ import type { CommandOutputLine } from "../runner.js";
2
+ export type StepStage = "runtime" | "tools";
3
+ export type DisplayStep = {
4
+ id: string;
5
+ name: string;
6
+ description: string;
7
+ stage: StepStage;
8
+ };
9
+ export type StepPhase = "pending" | "checking-availability" | "prompting-to-install" | "installing" | "prompting-to-run" | "running" | "complete" | "failed" | "skipped" | "not-available";
10
+ export type CompletedStepRecord = {
11
+ phase: StepPhase;
12
+ summaryNote?: string;
13
+ };
14
+ export type CurrentPrompt = {
15
+ question: string;
16
+ warning?: string;
17
+ };
18
+ export type ResolvedVersion = {
19
+ label: string;
20
+ value: string;
21
+ };
22
+ export type LogoVisibility = "visible" | "hidden";
23
+ export type RunCompletion = "in-progress" | "finished";
24
+ export type UpdateState = {
25
+ steps: DisplayStep[];
26
+ version: string;
27
+ logoVisibility: LogoVisibility;
28
+ currentStepIndex: number;
29
+ currentPhase: StepPhase;
30
+ completedStepRecords: CompletedStepRecord[];
31
+ currentOutputLines: CommandOutputLine[];
32
+ currentPrompt: CurrentPrompt | undefined;
33
+ runCompletion: RunCompletion;
34
+ versionReport: ResolvedVersion[] | undefined;
35
+ };
36
+ export type AppState = UpdateState;
37
+ export declare function createInitialUpdateState(steps: DisplayStep[], version: string, logoVisibility: LogoVisibility): UpdateState;
38
+ export declare const createInitialAppState: typeof createInitialUpdateState;
@@ -0,0 +1,15 @@
1
+ export function createInitialUpdateState(steps, version, logoVisibility) {
2
+ return {
3
+ steps,
4
+ version,
5
+ logoVisibility,
6
+ currentStepIndex: 0,
7
+ currentPhase: "checking-availability",
8
+ completedStepRecords: [],
9
+ currentOutputLines: [],
10
+ currentPrompt: undefined,
11
+ runCompletion: "in-progress",
12
+ versionReport: undefined,
13
+ };
14
+ }
15
+ export const createInitialAppState = createInitialUpdateState;
@@ -0,0 +1,2 @@
1
+ import type { ResolvedVersion } from "./state.js";
2
+ export declare function collectVersions(): Promise<ResolvedVersion[]>;
@@ -0,0 +1,16 @@
1
+ import { captureShellCommandOutput } from "../runner.js";
2
+ const VERSION_ENTRIES = [
3
+ { label: "macOS", shellCommand: "sw_vers -productVersion" },
4
+ { label: "Homebrew", shellCommand: "brew --version | head -n1" },
5
+ { label: "Node", shellCommand: "node -v 2>/dev/null" },
6
+ { label: "NPM", shellCommand: "npm -v 2>/dev/null" },
7
+ { label: "Python", shellCommand: "python3 --version 2>/dev/null" },
8
+ { label: "pip3", shellCommand: "pip3 --version 2>/dev/null | cut -d' ' -f1-2" },
9
+ { label: "Zsh", shellCommand: "zsh --version 2>/dev/null" },
10
+ ];
11
+ export async function collectVersions() {
12
+ return Promise.all(VERSION_ENTRIES.map(async ({ label, shellCommand }) => {
13
+ const value = await captureShellCommandOutput({ shellCommand }).catch(() => "");
14
+ return { label, value: value || "not installed" };
15
+ }));
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braeburn",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "macOS system updater CLI",
5
5
  "type": "module",
6
6
  "bin": {