onepatch 0.2.0 → 0.3.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/README.md +4 -0
- package/package.json +1 -1
- package/src/cli.ts +18 -1
- package/src/credentials.ts +15 -6
- package/src/update.test.ts +128 -0
- package/src/update.ts +187 -0
package/README.md
CHANGED
|
@@ -31,6 +31,10 @@ onepatch whoami
|
|
|
31
31
|
|
|
32
32
|
Pass `-` to read SQL or message text from stdin. `--json` prints raw MCP content blocks. `--api <url>` (or `ONEPATCH_API_URL`) targets a different deployment.
|
|
33
33
|
|
|
34
|
+
## Updates
|
|
35
|
+
|
|
36
|
+
The CLI keeps itself current. Once a day, in a detached background process, it asks the npm registry for the latest version; when one exists it reinstalls itself through whichever package manager owns the copy (bun or npm) and prints a one-line notice to stderr. The command you typed is never delayed and never fails because of update machinery. Set `ONEPATCH_NO_UPDATE=1` to disable it, or run `onepatch update` to update on demand. Running from a source checkout never auto-updates.
|
|
37
|
+
|
|
34
38
|
## Programmatic use
|
|
35
39
|
|
|
36
40
|
```ts
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { decodeJwtClaims, getValidAccessToken } from "./auth";
|
|
|
6
6
|
import { OnepatchClient } from "./client";
|
|
7
7
|
import { fetchBootstrap, resolveApiUrl } from "./config";
|
|
8
8
|
import { deleteCredentials, loadCredentials, saveCredentials } from "./credentials";
|
|
9
|
+
import { currentVersion, maybeAutoUpdate, runUpdate } from "./update";
|
|
9
10
|
import { pollForDeviceToken, startDeviceAuthorization } from "./workos";
|
|
10
11
|
|
|
11
12
|
const USAGE = `onepatch — OnePatch from the terminal
|
|
@@ -29,13 +30,16 @@ Usage:
|
|
|
29
30
|
onepatch incidents read <num> [--raw] Read one incident by number
|
|
30
31
|
|
|
31
32
|
onepatch tools List the server's MCP tools
|
|
33
|
+
onepatch update Update the CLI to the latest version now
|
|
32
34
|
|
|
33
35
|
Global flags:
|
|
34
36
|
--api <url> Deployment base URL (default: $ONEPATCH_API_URL or https://app.onepatch.dev)
|
|
35
37
|
--json Print raw MCP content blocks as JSON
|
|
38
|
+
--version Print the CLI version
|
|
36
39
|
-h, --help Show this help
|
|
37
40
|
|
|
38
|
-
Pass "-" for <sql> or <text> to read it from stdin
|
|
41
|
+
Pass "-" for <sql> or <text> to read it from stdin.
|
|
42
|
+
The CLI keeps itself current in the background; ONEPATCH_NO_UPDATE=1 disables that.`;
|
|
39
43
|
|
|
40
44
|
function fail(message: string): never {
|
|
41
45
|
console.error(message);
|
|
@@ -128,15 +132,28 @@ async function main(): Promise<void> {
|
|
|
128
132
|
"waiting-on": { type: "string" },
|
|
129
133
|
limit: { type: "string" },
|
|
130
134
|
raw: { type: "boolean", default: false },
|
|
135
|
+
version: { type: "boolean", default: false },
|
|
136
|
+
// Internal: `onepatch update --check` only refreshes the cached
|
|
137
|
+
// latest-version state; the background updater spawns it.
|
|
138
|
+
check: { type: "boolean", default: false },
|
|
131
139
|
},
|
|
132
140
|
});
|
|
133
141
|
|
|
142
|
+
if (flags.version) {
|
|
143
|
+
console.log(currentVersion());
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
134
147
|
const [noun, verb, ...rest] = positionals;
|
|
135
148
|
if (flags.help || !noun) {
|
|
136
149
|
console.log(USAGE);
|
|
137
150
|
return;
|
|
138
151
|
}
|
|
139
152
|
|
|
153
|
+
if (noun === "update") return await runUpdate({ checkOnly: flags.check });
|
|
154
|
+
// Every other command triggers the zero-cost background update pass.
|
|
155
|
+
maybeAutoUpdate();
|
|
156
|
+
|
|
140
157
|
const api = resolveApiUrl(flags.api);
|
|
141
158
|
|
|
142
159
|
if (noun === "login") return await login(api);
|
package/src/credentials.ts
CHANGED
|
@@ -16,11 +16,21 @@ export type Credentials = {
|
|
|
16
16
|
|
|
17
17
|
type CredentialsFile = Record<string, Credentials>;
|
|
18
18
|
|
|
19
|
-
export function
|
|
20
|
-
|
|
19
|
+
export function configDir(): string {
|
|
20
|
+
return (
|
|
21
21
|
process.env.ONEPATCH_CONFIG_DIR ??
|
|
22
|
-
join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "onepatch")
|
|
23
|
-
|
|
22
|
+
join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "onepatch")
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ensureConfigDir(): string {
|
|
27
|
+
const dir = configDir();
|
|
28
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function credentialsPath(): string {
|
|
33
|
+
return join(configDir(), "credentials.json");
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
function readFile(): CredentialsFile {
|
|
@@ -37,8 +47,7 @@ export function loadCredentials(api: string): Credentials | null {
|
|
|
37
47
|
|
|
38
48
|
export function saveCredentials(api: string, creds: Credentials): void {
|
|
39
49
|
const path = credentialsPath();
|
|
40
|
-
|
|
41
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
50
|
+
ensureConfigDir();
|
|
42
51
|
const all = readFile();
|
|
43
52
|
all[api] = creds;
|
|
44
53
|
writeFileSync(path, `${JSON.stringify(all, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
CHECK_INTERVAL_MS,
|
|
6
|
+
compareVersions,
|
|
7
|
+
decideUpdateAction,
|
|
8
|
+
detectInstallKind,
|
|
9
|
+
fetchLatestVersion,
|
|
10
|
+
INSTALL_RETRY_MS,
|
|
11
|
+
installCommand,
|
|
12
|
+
} from "./update";
|
|
13
|
+
|
|
14
|
+
describe("compareVersions", () => {
|
|
15
|
+
test("orders plain versions", () => {
|
|
16
|
+
expect(compareVersions("0.2.0", "0.3.0")).toBeLessThan(0);
|
|
17
|
+
expect(compareVersions("0.3.0", "0.2.9")).toBeGreaterThan(0);
|
|
18
|
+
expect(compareVersions("1.0.0", "0.99.99")).toBeGreaterThan(0);
|
|
19
|
+
expect(compareVersions("0.2.0", "0.2.0")).toBe(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("compares numerically, not lexically", () => {
|
|
23
|
+
expect(compareVersions("0.10.0", "0.9.0")).toBeGreaterThan(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("prerelease sorts below its release", () => {
|
|
27
|
+
expect(compareVersions("1.0.0-beta.1", "1.0.0")).toBeLessThan(0);
|
|
28
|
+
expect(compareVersions("1.0.0", "1.0.0-rc.2")).toBeGreaterThan(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("tolerates short versions", () => {
|
|
32
|
+
expect(compareVersions("1.0", "1.0.0")).toBe(0);
|
|
33
|
+
expect(compareVersions("1", "1.0.1")).toBeLessThan(0);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("decideUpdateAction", () => {
|
|
38
|
+
const now = 1_700_000_000_000;
|
|
39
|
+
|
|
40
|
+
test("empty state asks for a background check", () => {
|
|
41
|
+
expect(decideUpdateAction({}, "0.2.0", now)).toEqual({ kind: "check" });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("fresh state with no newer version does nothing", () => {
|
|
45
|
+
expect(decideUpdateAction({ checkedAt: now - 1000, latest: "0.2.0" }, "0.2.0", now)).toEqual({
|
|
46
|
+
kind: "none",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("stale state asks for a background check", () => {
|
|
51
|
+
expect(
|
|
52
|
+
decideUpdateAction({ checkedAt: now - CHECK_INTERVAL_MS - 1, latest: "0.2.0" }, "0.2.0", now),
|
|
53
|
+
).toEqual({ kind: "check" });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("known newer version installs", () => {
|
|
57
|
+
expect(decideUpdateAction({ checkedAt: now, latest: "0.3.0" }, "0.2.0", now)).toEqual({
|
|
58
|
+
kind: "install",
|
|
59
|
+
latest: "0.3.0",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("an in-flight install suppresses re-spawning", () => {
|
|
64
|
+
expect(
|
|
65
|
+
decideUpdateAction(
|
|
66
|
+
{ checkedAt: now, latest: "0.3.0", installStartedAt: now - 1000 },
|
|
67
|
+
"0.2.0",
|
|
68
|
+
now,
|
|
69
|
+
),
|
|
70
|
+
).toEqual({ kind: "none" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("a failed install retries after the guard window", () => {
|
|
74
|
+
expect(
|
|
75
|
+
decideUpdateAction(
|
|
76
|
+
{ checkedAt: now, latest: "0.3.0", installStartedAt: now - INSTALL_RETRY_MS - 1 },
|
|
77
|
+
"0.2.0",
|
|
78
|
+
now,
|
|
79
|
+
),
|
|
80
|
+
).toEqual({ kind: "install", latest: "0.3.0" });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a cached latest older than current never installs", () => {
|
|
84
|
+
expect(decideUpdateAction({ checkedAt: now, latest: "0.1.0" }, "0.2.0", now)).toEqual({
|
|
85
|
+
kind: "none",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("detectInstallKind", () => {
|
|
91
|
+
test("bun global install", () => {
|
|
92
|
+
expect(
|
|
93
|
+
detectInstallKind(join(homedir(), ".bun", "install", "global", "node_modules", "onepatch")),
|
|
94
|
+
).toBe("bun");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("npm global install", () => {
|
|
98
|
+
expect(detectInstallKind("/usr/local/lib/node_modules/onepatch")).toBe("npm");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("source checkout is not updatable", () => {
|
|
102
|
+
expect(detectInstallKind("/Users/someone/dev/onepatch-cli")).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("installCommand", () => {
|
|
107
|
+
test("pins the discovered version, not the latest tag", () => {
|
|
108
|
+
expect(installCommand("bun", "0.3.0")).toEqual(["bun", "add", "-g", "onepatch@0.3.0"]);
|
|
109
|
+
expect(installCommand("npm", "0.3.0")).toEqual(["npm", "install", "-g", "onepatch@0.3.0"]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("fetchLatestVersion", () => {
|
|
114
|
+
test("reads the version from the registry manifest", async () => {
|
|
115
|
+
const fake = (async () => Response.json({ version: "0.4.2" })) as unknown as typeof fetch;
|
|
116
|
+
expect(await fetchLatestVersion(fake)).toBe("0.4.2");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("rejects a manifest without a version", async () => {
|
|
120
|
+
const fake = (async () => Response.json({})) as unknown as typeof fetch;
|
|
121
|
+
await expect(fetchLatestVersion(fake)).rejects.toThrow("without a version");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("rejects a non-2xx answer", async () => {
|
|
125
|
+
const fake = (async () => new Response("nope", { status: 503 })) as unknown as typeof fetch;
|
|
126
|
+
await expect(fetchLatestVersion(fake)).rejects.toThrow("503");
|
|
127
|
+
});
|
|
128
|
+
});
|
package/src/update.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Self-update. The rule that shapes everything here: the command the user
|
|
2
|
+
// actually typed never waits on update machinery. Each invocation reads one
|
|
3
|
+
// small cached JSON file; a stale cache spawns a detached background process
|
|
4
|
+
// to refresh it from the npm registry, and a cache that already names a newer
|
|
5
|
+
// version spawns a detached reinstall via whichever package manager owns this
|
|
6
|
+
// copy. Either way the foreground command proceeds immediately, and the next
|
|
7
|
+
// invocation runs the new code.
|
|
8
|
+
import { readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join, sep } from "node:path";
|
|
11
|
+
import { configDir, ensureConfigDir } from "./credentials";
|
|
12
|
+
|
|
13
|
+
export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
14
|
+
// A spawned install normally lands in seconds; this only bounds how long a
|
|
15
|
+
// *failed* install suppresses retries (and dogpiles from parallel invocations).
|
|
16
|
+
export const INSTALL_RETRY_MS = 15 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
const packageRoot = (() => {
|
|
19
|
+
// The bin shim is a symlink into the installed package; resolve it so the
|
|
20
|
+
// path we inspect is where the package physically lives.
|
|
21
|
+
const raw = join(import.meta.dir, "..");
|
|
22
|
+
try {
|
|
23
|
+
return realpathSync(raw);
|
|
24
|
+
} catch {
|
|
25
|
+
return raw;
|
|
26
|
+
}
|
|
27
|
+
})();
|
|
28
|
+
|
|
29
|
+
export function currentVersion(): string {
|
|
30
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")) as {
|
|
31
|
+
version?: unknown;
|
|
32
|
+
};
|
|
33
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Plain x.y.z compare; a prerelease suffix sorts below its release.
|
|
37
|
+
export function compareVersions(a: string, b: string): number {
|
|
38
|
+
const parse = (v: string) => {
|
|
39
|
+
const [main = "", pre] = v.split("-", 2);
|
|
40
|
+
const nums = main.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
41
|
+
while (nums.length < 3) nums.push(0);
|
|
42
|
+
return { nums, pre };
|
|
43
|
+
};
|
|
44
|
+
const pa = parse(a);
|
|
45
|
+
const pb = parse(b);
|
|
46
|
+
for (let i = 0; i < 3; i++) {
|
|
47
|
+
const d = (pa.nums[i] ?? 0) - (pb.nums[i] ?? 0);
|
|
48
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
49
|
+
}
|
|
50
|
+
if (pa.pre === pb.pre) return 0;
|
|
51
|
+
if (pa.pre === undefined) return 1;
|
|
52
|
+
if (pb.pre === undefined) return -1;
|
|
53
|
+
return pa.pre < pb.pre ? -1 : 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type InstallKind = "bun" | "npm";
|
|
57
|
+
|
|
58
|
+
// Which package manager owns this copy — decided from where the package
|
|
59
|
+
// physically lives, because that's also the only place the answer is honest.
|
|
60
|
+
// null means a source checkout (dev), where auto-update must stay away.
|
|
61
|
+
export function detectInstallKind(root: string = packageRoot): InstallKind | null {
|
|
62
|
+
const bunInstall = process.env.BUN_INSTALL ?? join(homedir(), ".bun");
|
|
63
|
+
if (root.startsWith(bunInstall + sep) || root.split(sep).includes(".bun")) return "bun";
|
|
64
|
+
if (root.split(sep).includes("node_modules")) return "npm";
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function installCommand(kind: InstallKind, version: string): string[] {
|
|
69
|
+
return kind === "bun"
|
|
70
|
+
? ["bun", "add", "-g", `onepatch@${version}`]
|
|
71
|
+
: ["npm", "install", "-g", `onepatch@${version}`];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type UpdateState = {
|
|
75
|
+
checkedAt?: number;
|
|
76
|
+
latest?: string;
|
|
77
|
+
installStartedAt?: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function updateStatePath(): string {
|
|
81
|
+
return join(configDir(), "update-check.json");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readUpdateState(): UpdateState {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(updateStatePath(), "utf8")) as UpdateState;
|
|
87
|
+
} catch {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function writeUpdateState(state: UpdateState): void {
|
|
93
|
+
ensureConfigDir();
|
|
94
|
+
writeFileSync(updateStatePath(), `${JSON.stringify(state)}\n`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type UpdateAction =
|
|
98
|
+
| { kind: "install"; latest: string }
|
|
99
|
+
| { kind: "check" }
|
|
100
|
+
| { kind: "none" };
|
|
101
|
+
|
|
102
|
+
export function decideUpdateAction(state: UpdateState, current: string, now: number): UpdateAction {
|
|
103
|
+
if (state.latest && compareVersions(state.latest, current) > 0) {
|
|
104
|
+
if (state.installStartedAt !== undefined && now - state.installStartedAt < INSTALL_RETRY_MS) {
|
|
105
|
+
return { kind: "none" };
|
|
106
|
+
}
|
|
107
|
+
return { kind: "install", latest: state.latest };
|
|
108
|
+
}
|
|
109
|
+
if (state.checkedAt === undefined || now - state.checkedAt > CHECK_INTERVAL_MS) {
|
|
110
|
+
return { kind: "check" };
|
|
111
|
+
}
|
|
112
|
+
return { kind: "none" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function fetchLatestVersion(fetchImpl: typeof fetch = fetch): Promise<string> {
|
|
116
|
+
const res = await fetchImpl("https://registry.npmjs.org/onepatch/latest", {
|
|
117
|
+
headers: { accept: "application/json" },
|
|
118
|
+
signal: AbortSignal.timeout(10_000),
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok) throw new Error(`npm registry answered ${res.status} for onepatch@latest`);
|
|
121
|
+
const doc = (await res.json()) as { version?: unknown };
|
|
122
|
+
if (typeof doc.version !== "string") {
|
|
123
|
+
throw new Error("npm registry returned a manifest without a version");
|
|
124
|
+
}
|
|
125
|
+
return doc.version;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function spawnDetached(cmd: string[]): void {
|
|
129
|
+
Bun.spawn({ cmd, stdin: "ignore", stdout: "ignore", stderr: "ignore" }).unref();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Called on every ordinary invocation. Must never throw, never block, never
|
|
133
|
+
// touch the network in-process.
|
|
134
|
+
export function maybeAutoUpdate(): void {
|
|
135
|
+
try {
|
|
136
|
+
if (process.env.ONEPATCH_NO_UPDATE) return;
|
|
137
|
+
const kind = detectInstallKind();
|
|
138
|
+
if (kind === null) return;
|
|
139
|
+
const state = readUpdateState();
|
|
140
|
+
const action = decideUpdateAction(state, currentVersion(), Date.now());
|
|
141
|
+
if (action.kind === "install") {
|
|
142
|
+
// Stamp before spawning so parallel invocations don't dogpile.
|
|
143
|
+
writeUpdateState({ ...state, installStartedAt: Date.now() });
|
|
144
|
+
spawnDetached(installCommand(kind, action.latest));
|
|
145
|
+
console.error(
|
|
146
|
+
`onepatch ${currentVersion()} → ${action.latest} is installing in the background ` +
|
|
147
|
+
"(ONEPATCH_NO_UPDATE=1 disables this).",
|
|
148
|
+
);
|
|
149
|
+
} else if (action.kind === "check") {
|
|
150
|
+
// Refresh the cache off-process: `onepatch update --check` fetches the
|
|
151
|
+
// registry and writes the state file, costing this invocation nothing.
|
|
152
|
+
spawnDetached([process.execPath, join(packageRoot, "src", "cli.ts"), "update", "--check"]);
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Auto-update is strictly best-effort; the user's command always wins.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// The explicit `onepatch update` command (and its hidden `--check` cache-refresh
|
|
160
|
+
// mode used by the background spawn above).
|
|
161
|
+
export async function runUpdate(opts: { checkOnly: boolean }): Promise<void> {
|
|
162
|
+
const latest = await fetchLatestVersion();
|
|
163
|
+
const prior = readUpdateState();
|
|
164
|
+
writeUpdateState({ ...prior, checkedAt: Date.now(), latest });
|
|
165
|
+
if (opts.checkOnly) return;
|
|
166
|
+
|
|
167
|
+
const current = currentVersion();
|
|
168
|
+
if (compareVersions(latest, current) <= 0) {
|
|
169
|
+
console.log(`onepatch ${current} is up to date.`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const kind = detectInstallKind();
|
|
173
|
+
if (kind === null) {
|
|
174
|
+
console.log(
|
|
175
|
+
`onepatch ${latest} is available (you have ${current}), but this copy isn't a ` +
|
|
176
|
+
"global install — update your checkout with git instead.",
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const cmd = installCommand(kind, latest);
|
|
181
|
+
console.log(`Updating onepatch ${current} → ${latest} (${cmd.join(" ")})…`);
|
|
182
|
+
const proc = Bun.spawn({ cmd, stdin: "ignore", stdout: "inherit", stderr: "inherit" });
|
|
183
|
+
const code = await proc.exited;
|
|
184
|
+
if (code !== 0) throw new Error(`${cmd[0]} exited with code ${code}`);
|
|
185
|
+
writeUpdateState({ checkedAt: Date.now(), latest });
|
|
186
|
+
console.log(`onepatch ${latest} installed.`);
|
|
187
|
+
}
|