cueloop 0.1.0-alpha.67 → 0.1.0-alpha.69
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/package.json +5 -5
- package/src/main.ts +16 -9
- package/src/session-commands.ts +9 -1
- package/src/update-command.test.ts +163 -0
- package/src/update-command.ts +177 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cueloop",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.69",
|
|
4
4
|
"description": "Terminal review surface for coding agents: annotate plans and diffs, return a verdict, send structured feedback back",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"build:binary": "bun build --compile --external cpu-features ./src/main.ts --outfile dist/cueloop"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@cueloop/adapters": "0.1.0-alpha.
|
|
34
|
-
"@cueloop/client": "0.1.0-alpha.
|
|
35
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
36
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
33
|
+
"@cueloop/adapters": "0.1.0-alpha.69",
|
|
34
|
+
"@cueloop/client": "0.1.0-alpha.69",
|
|
35
|
+
"@cueloop/daemon": "0.1.0-alpha.69",
|
|
36
|
+
"@cueloop/schema": "0.1.0-alpha.69",
|
|
37
37
|
"valibot": "^1.4.2"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
package/src/main.ts
CHANGED
|
@@ -114,6 +114,7 @@ const commandHandlers: CommandHandlers = {
|
|
|
114
114
|
wake: async (rest) => (await import("./wake-command")).wakeCommand(rest),
|
|
115
115
|
actions: async (rest) => (await import("./actions-command")).actionsCommand(rest),
|
|
116
116
|
refine: async (rest) => (await import("./refine-command")).refineCommand(rest),
|
|
117
|
+
update: async (rest) => (await import("./update-command")).updateCommand(rest),
|
|
117
118
|
review: (rest) => reviewEntry(rest),
|
|
118
119
|
"review-post": async (rest) => (await import("./pr")).reviewPostCommand(rest),
|
|
119
120
|
dev: async () => (await import("./dev-command")).devCommand(),
|
|
@@ -202,26 +203,30 @@ async function replyCommand(argv: string[]): Promise<number> {
|
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
/**
|
|
205
|
-
* `cueloop prototype <file.
|
|
206
|
-
*
|
|
207
|
-
* latest
|
|
206
|
+
* `cueloop prototype <file.md>` creates a review of a component design doc
|
|
207
|
+
* (API / Composition / Callstack); an `.html` file instead creates the opt-in
|
|
208
|
+
* experimental pixel mockup. A selector or `--open`/`--latest` (or a bare call
|
|
209
|
+
* with no file) opens the latest pending prototype review instead.
|
|
208
210
|
*/
|
|
209
211
|
async function prototypeCommand(argv: string[]): Promise<number> {
|
|
210
212
|
const parsed = parseArgs(argv);
|
|
211
213
|
const selector = openSelector(parsed);
|
|
212
214
|
const wantsOpen = "open" in parsed.flags || "latest" in parsed.flags;
|
|
215
|
+
// a markdown design doc is the default; an .html entry is the opt-in pixel mockup
|
|
216
|
+
const isHtmlPrototype = selector?.endsWith(".html") ?? false;
|
|
217
|
+
const isMarkdownPrototype = selector?.endsWith(".md") || selector?.endsWith(".markdown") || false;
|
|
213
218
|
const looksLikeFile =
|
|
214
|
-
selector !== undefined && !isSessionId(selector) &&
|
|
219
|
+
selector !== undefined && !isSessionId(selector) && (isHtmlPrototype || isMarkdownPrototype);
|
|
215
220
|
|
|
216
221
|
if (wantsOpen || !looksLikeFile)
|
|
217
222
|
return openReviewOfKind(isPrototypeReview, "prototype", selector);
|
|
218
223
|
|
|
219
224
|
const path = resolve(selector);
|
|
220
|
-
const
|
|
225
|
+
const content = await Bun.file(path)
|
|
221
226
|
.text()
|
|
222
227
|
.catch(() => undefined);
|
|
223
228
|
|
|
224
|
-
if (
|
|
229
|
+
if (content === undefined) {
|
|
225
230
|
console.error(`prototype: cannot read ${path}`);
|
|
226
231
|
|
|
227
232
|
return 1;
|
|
@@ -229,8 +234,9 @@ async function prototypeCommand(argv: string[]): Promise<number> {
|
|
|
229
234
|
const client = await DaemonClient.connect({ autostart: true });
|
|
230
235
|
const review = await openReview(client, {
|
|
231
236
|
type: "prototype",
|
|
232
|
-
content
|
|
233
|
-
prototypePath
|
|
237
|
+
content,
|
|
238
|
+
// only an HTML entry carries a prototypePath, the signal for the experimental pixel mode
|
|
239
|
+
prototypePath: isHtmlPrototype ? path : undefined,
|
|
234
240
|
title: basename(path),
|
|
235
241
|
});
|
|
236
242
|
|
|
@@ -325,7 +331,7 @@ function printHelp(): void {
|
|
|
325
331
|
" cueloop diff [id|title] review your working tree (untracked files included);",
|
|
326
332
|
" with a clean tree, open the latest pending diff review",
|
|
327
333
|
" cueloop review <pr> review a pull request (--no-tui prints the session)",
|
|
328
|
-
" cueloop prototype <file.
|
|
334
|
+
" cueloop prototype <file.md> review a component design doc (or open the latest by id/title)",
|
|
329
335
|
"",
|
|
330
336
|
"share:",
|
|
331
337
|
" cueloop serve [session-id] share over ssh: observers are read-only,",
|
|
@@ -344,6 +350,7 @@ function printHelp(): void {
|
|
|
344
350
|
" cueloop session <primitive> [flags] script the daemon (create|get|list|wait|annotate|remove|cut|restore|curate|set-viewed|navigate|branch|switch|label|fork|name-self|events|resolve|submit-revision)",
|
|
345
351
|
" cueloop actions list list the quick-action vocabulary (for annotate --action)",
|
|
346
352
|
" cueloop refine mine past reviews into a markdown report + writeback proposals",
|
|
353
|
+
" cueloop update [--dry-run] update the installed cueloop binary (--dry-run reports the target only)",
|
|
347
354
|
" cueloop wake <id> [--harness codex --thread <id>] resume the agent with the verdict (spawn detached)",
|
|
348
355
|
" cueloop review-post <id> <pr> post a resolved session's verdict back to the PR",
|
|
349
356
|
" cueloop daemon run the daemon in the foreground",
|
package/src/session-commands.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { DaemonClient } from "@cueloop/daemon/client";
|
|
|
16
16
|
import { openHerdrPaneForReview } from "@cueloop/daemon/herdr-pane";
|
|
17
17
|
import { openReview, verdictResponse } from "@cueloop/daemon/review";
|
|
18
18
|
import { loadConfig, quickActionBody, resolveQuickAction } from "@cueloop/client/config";
|
|
19
|
+
import { slashItemsFrom } from "@cueloop/client/slash-palette";
|
|
19
20
|
import { parseArgs, stringFlag } from "./args";
|
|
20
21
|
|
|
21
22
|
async function readStdin(): Promise<string> {
|
|
@@ -342,8 +343,15 @@ async function sessionResolveCommand({
|
|
|
342
343
|
}: SessionContext): Promise<number> {
|
|
343
344
|
const id = required(positional[1], "session id");
|
|
344
345
|
const kind = v.parse(VerdictKindSchema, required(stringFlag(flags, "verdict"), "--verdict"));
|
|
346
|
+
// expand /name quick-action references in TUI-authored comments, so a CLI resolve sends the agent
|
|
347
|
+
// the action bodies too, not the bare references
|
|
348
|
+
const session = await client.sessionGet(id);
|
|
349
|
+
const actions = loadConfig({ repoRoot: session.workspace.repoRoot }).actions;
|
|
350
|
+
const actionBodies = Object.fromEntries(
|
|
351
|
+
slashItemsFrom(actions).map((item) => [item.name, item.body]),
|
|
352
|
+
);
|
|
345
353
|
|
|
346
|
-
out(await client.sessionResolve(id, kind, stringFlag(flags, "summary") ?? ""));
|
|
354
|
+
out(await client.sessionResolve(id, kind, stringFlag(flags, "summary") ?? "", actionBodies));
|
|
347
355
|
|
|
348
356
|
return 0;
|
|
349
357
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/** update install-dir resolution and the update flow over injected deps: never the Bun `/$bunfs` path, an up-to-date short-circuit, and a restart notice on success. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
4
|
+
import { resolveInstallDir, runUpdate, type UpdateDeps } from "./update-command";
|
|
5
|
+
|
|
6
|
+
function depsSpy(overrides: Partial<UpdateDeps> = {}): UpdateDeps & { lines: string[] } {
|
|
7
|
+
const lines: string[] = [];
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
currentVersion: "0.1.0-alpha.68",
|
|
11
|
+
installDir: () => "/Users/dev/.local/bin",
|
|
12
|
+
fetchLatestVersion: mock(async () => "0.1.0-alpha.68"),
|
|
13
|
+
runInstaller: mock(async () => 0),
|
|
14
|
+
out: (message: string) => void lines.push(message),
|
|
15
|
+
lines,
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("resolveInstallDir", () => {
|
|
21
|
+
test("rejects the Bun single-file virtual path and falls back to the per-user bin dir", () => {
|
|
22
|
+
// Arrange - a compiled binary that mistakenly reports its bunfs path
|
|
23
|
+
const env: NodeJS.ProcessEnv = { HOME: "/Users/dev" };
|
|
24
|
+
|
|
25
|
+
// Act
|
|
26
|
+
const dir = resolveInstallDir("/$bunfs/root/cueloop", env);
|
|
27
|
+
|
|
28
|
+
// Assert
|
|
29
|
+
expect(dir).toBe("/Users/dev/.local/bin");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("uses the real on-disk directory of the invoked binary", () => {
|
|
33
|
+
// Arrange
|
|
34
|
+
const env: NodeJS.ProcessEnv = { HOME: "/Users/dev" };
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
const dir = resolveInstallDir("/usr/local/bin/cueloop", env);
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(dir).toBe("/usr/local/bin");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("falls back to the per-user bin dir when not running as the compiled cueloop", () => {
|
|
44
|
+
// Arrange - execPath is the bun runtime, not cueloop
|
|
45
|
+
const env: NodeJS.ProcessEnv = { HOME: "/Users/dev" };
|
|
46
|
+
|
|
47
|
+
// Act
|
|
48
|
+
const dir = resolveInstallDir("/Users/dev/.bun/bin/bun", env);
|
|
49
|
+
|
|
50
|
+
// Assert
|
|
51
|
+
expect(dir).toBe("/Users/dev/.local/bin");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("an explicit CUELOOP_INSTALL_DIR overrides everything", () => {
|
|
55
|
+
// Arrange
|
|
56
|
+
const env: NodeJS.ProcessEnv = { HOME: "/Users/dev", CUELOOP_INSTALL_DIR: "/opt/bin" };
|
|
57
|
+
|
|
58
|
+
// Act
|
|
59
|
+
const dir = resolveInstallDir("/$bunfs/root/cueloop", env);
|
|
60
|
+
|
|
61
|
+
// Assert
|
|
62
|
+
expect(dir).toBe("/opt/bin");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe(runUpdate, () => {
|
|
67
|
+
test("--dry-run reports the target without any network or install work", async () => {
|
|
68
|
+
// Arrange
|
|
69
|
+
const deps = depsSpy();
|
|
70
|
+
|
|
71
|
+
// Act
|
|
72
|
+
const code = await runUpdate(deps, true);
|
|
73
|
+
|
|
74
|
+
// Assert
|
|
75
|
+
expect(code).toBe(0);
|
|
76
|
+
expect(deps.lines).toEqual([
|
|
77
|
+
"Current version: 0.1.0-alpha.68",
|
|
78
|
+
"cueloop would update in /Users/dev/.local/bin",
|
|
79
|
+
]);
|
|
80
|
+
expect(deps.fetchLatestVersion).not.toHaveBeenCalled();
|
|
81
|
+
expect(deps.runInstaller).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("reports up to date and skips the installer when no newer version exists", async () => {
|
|
85
|
+
// Arrange - latest matches the current version
|
|
86
|
+
const deps = depsSpy({ fetchLatestVersion: mock(async () => "0.1.0-alpha.68") });
|
|
87
|
+
|
|
88
|
+
// Act
|
|
89
|
+
const code = await runUpdate(deps, false);
|
|
90
|
+
|
|
91
|
+
// Assert
|
|
92
|
+
expect(code).toBe(0);
|
|
93
|
+
expect(deps.lines).toContain("cueloop is up to date (0.1.0-alpha.68)");
|
|
94
|
+
expect(deps.runInstaller).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("does not downgrade when the newest release is older than the current build", async () => {
|
|
98
|
+
// Arrange - a dev build ahead of the published release
|
|
99
|
+
const deps = depsSpy({
|
|
100
|
+
currentVersion: "0.1.0-alpha.69",
|
|
101
|
+
fetchLatestVersion: mock(async () => "0.1.0-alpha.68"),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Act
|
|
105
|
+
const code = await runUpdate(deps, false);
|
|
106
|
+
|
|
107
|
+
// Assert
|
|
108
|
+
expect(code).toBe(0);
|
|
109
|
+
expect(deps.runInstaller).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("runs the installer and prints a restart notice when a newer version exists", async () => {
|
|
113
|
+
// Arrange
|
|
114
|
+
const deps = depsSpy({ fetchLatestVersion: mock(async () => "0.1.0-alpha.99") });
|
|
115
|
+
|
|
116
|
+
// Act
|
|
117
|
+
const code = await runUpdate(deps, false);
|
|
118
|
+
|
|
119
|
+
// Assert
|
|
120
|
+
expect(code).toBe(0);
|
|
121
|
+
expect(deps.runInstaller).toHaveBeenCalledWith("/Users/dev/.local/bin");
|
|
122
|
+
expect(deps.lines).toContain("Update ran successfully! Please restart cueloop.");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("proceeds with the update when the latest version cannot be determined", async () => {
|
|
126
|
+
// Arrange - releases API unreachable
|
|
127
|
+
const deps = depsSpy({ fetchLatestVersion: mock(async () => undefined) });
|
|
128
|
+
|
|
129
|
+
// Act
|
|
130
|
+
await runUpdate(deps, false);
|
|
131
|
+
|
|
132
|
+
// Assert
|
|
133
|
+
expect(deps.runInstaller).toHaveBeenCalledTimes(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("does not print the restart notice when the installer fails", async () => {
|
|
137
|
+
// Arrange
|
|
138
|
+
const deps = depsSpy({
|
|
139
|
+
fetchLatestVersion: mock(async () => "0.1.0-alpha.99"),
|
|
140
|
+
runInstaller: mock(async () => 1),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Act
|
|
144
|
+
const code = await runUpdate(deps, false);
|
|
145
|
+
|
|
146
|
+
// Assert
|
|
147
|
+
expect(code).toBe(1);
|
|
148
|
+
expect(deps.lines).not.toContain("Update ran successfully! Please restart cueloop.");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("rejects a relative install dir before doing any work", async () => {
|
|
152
|
+
// Arrange
|
|
153
|
+
const deps = depsSpy({ installDir: () => "relative/dir" });
|
|
154
|
+
|
|
155
|
+
// Act
|
|
156
|
+
const code = await runUpdate(deps, false);
|
|
157
|
+
|
|
158
|
+
// Assert
|
|
159
|
+
expect(code).toBe(1);
|
|
160
|
+
expect(deps.lines[0]).toContain("must be an absolute path");
|
|
161
|
+
expect(deps.runInstaller).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
import { CLI_VERSION } from "./version";
|
|
4
|
+
|
|
5
|
+
const INSTALL_URL = "https://cueloop.dev/install.sh";
|
|
6
|
+
const RELEASES_URL = "https://api.github.com/repos/mmurakaru/cueloop/releases?per_page=100";
|
|
7
|
+
const RELEASE_TAG_PREFIX = "cueloop@";
|
|
8
|
+
|
|
9
|
+
const InstallerScriptSchema = v.pipe(
|
|
10
|
+
v.string(),
|
|
11
|
+
v.minLength(1),
|
|
12
|
+
v.check(
|
|
13
|
+
(script) => script.includes("CUELOOP_INSTALL_DIR"),
|
|
14
|
+
"installer is not a cueloop installer",
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const ReleasesSchema = v.array(v.object({ tag_name: v.nullish(v.string()) }));
|
|
19
|
+
|
|
20
|
+
function userBinInstallDir(env: NodeJS.ProcessEnv): string | undefined {
|
|
21
|
+
if (env.HOME === undefined) return undefined;
|
|
22
|
+
|
|
23
|
+
return `${env.HOME}/.local/bin`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The directory that holds the running binary, or undefined when we are not the
|
|
28
|
+
* compiled cueloop. Reads `execPath` (the real on-disk executable), not
|
|
29
|
+
* `argv[1]`: a Bun single-file executable reports its virtual filesystem path
|
|
30
|
+
* (`/$bunfs/root/cueloop`) in `argv[1]`, and installing there fails with a
|
|
31
|
+
* read-only filesystem error. The `/$bunfs` guard is belt-and-braces.
|
|
32
|
+
*/
|
|
33
|
+
function invokedBinaryInstallDir(execPath: string): string | undefined {
|
|
34
|
+
if (basename(execPath) !== "cueloop") return undefined;
|
|
35
|
+
|
|
36
|
+
const dir = dirname(isAbsolute(execPath) ? execPath : resolve(execPath));
|
|
37
|
+
|
|
38
|
+
if (dir === "/$bunfs" || dir.startsWith("/$bunfs/")) return undefined;
|
|
39
|
+
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Where an update should land: an explicit override, else the binary's own directory, else the per-user bin dir. */
|
|
44
|
+
export function resolveInstallDir(execPath: string, env: NodeJS.ProcessEnv): string | undefined {
|
|
45
|
+
return env.CUELOOP_INSTALL_DIR || invokedBinaryInstallDir(execPath) || userBinInstallDir(env);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The newest published cueloop version, or undefined when the releases API is unreachable or empty. */
|
|
49
|
+
async function fetchLatestVersion(): Promise<string | undefined> {
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(RELEASES_URL, { headers: { "user-agent": "cueloop-update" } });
|
|
52
|
+
|
|
53
|
+
if (!response.ok) return undefined;
|
|
54
|
+
const parsed = v.safeParse(ReleasesSchema, await response.json());
|
|
55
|
+
|
|
56
|
+
if (!parsed.success) return undefined;
|
|
57
|
+
|
|
58
|
+
for (const release of parsed.output) {
|
|
59
|
+
if (release.tag_name?.startsWith(RELEASE_TAG_PREFIX)) {
|
|
60
|
+
return release.tag_name.slice(RELEASE_TAG_PREFIX.length);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined;
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True when `latest` is strictly newer than `current`; unparseable versions fall through to attempting the update. */
|
|
71
|
+
function isNewer(latest: string, current: string): boolean {
|
|
72
|
+
try {
|
|
73
|
+
return Bun.semver.order(latest, current) === 1;
|
|
74
|
+
} catch {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Fetch the published installer and run it, targeting `targetInstallDir`; returns the installer's exit code. */
|
|
80
|
+
async function runInstaller(targetInstallDir: string): Promise<number> {
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch(INSTALL_URL);
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
console.error(`cueloop update: failed to fetch installer (${response.status})`);
|
|
86
|
+
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
const parsed = v.safeParse(InstallerScriptSchema, await response.text());
|
|
90
|
+
|
|
91
|
+
if (!parsed.success) {
|
|
92
|
+
console.error("cueloop update: fetched installer failed validation");
|
|
93
|
+
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
const child = Bun.spawn(["sh"], {
|
|
97
|
+
stdin: "pipe",
|
|
98
|
+
stdout: "inherit",
|
|
99
|
+
stderr: "inherit",
|
|
100
|
+
// an update replaces the binary in place; it never edits the user's shell rc
|
|
101
|
+
env: { ...process.env, CUELOOP_INSTALL_DIR: targetInstallDir, CUELOOP_NO_MODIFY_PATH: "1" },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
child.stdin.write(parsed.output);
|
|
105
|
+
child.stdin.end();
|
|
106
|
+
|
|
107
|
+
return child.exited;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`cueloop update: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface UpdateDeps {
|
|
116
|
+
currentVersion: string;
|
|
117
|
+
installDir: () => string | undefined;
|
|
118
|
+
fetchLatestVersion: () => Promise<string | undefined>;
|
|
119
|
+
runInstaller: (targetInstallDir: string) => Promise<number>;
|
|
120
|
+
out: (message: string) => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The update flow, over injectable dependencies: resolve the target directory,
|
|
125
|
+
* report the current version, and either report that we are up to date or re-run
|
|
126
|
+
* the installer. `--dry-run` resolves and reports the target without any network
|
|
127
|
+
* work, which is how the compiled-binary path resolution gets tested.
|
|
128
|
+
*/
|
|
129
|
+
export async function runUpdate(deps: UpdateDeps, dryRun: boolean): Promise<number> {
|
|
130
|
+
const targetInstallDir = deps.installDir();
|
|
131
|
+
|
|
132
|
+
if (targetInstallDir === undefined) {
|
|
133
|
+
deps.out("cueloop update: HOME is not set and the current binary path is unknown");
|
|
134
|
+
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
if (!targetInstallDir.startsWith("/")) {
|
|
138
|
+
deps.out("cueloop update: CUELOOP_INSTALL_DIR must be an absolute path");
|
|
139
|
+
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
deps.out(`Current version: ${deps.currentVersion}`);
|
|
143
|
+
|
|
144
|
+
if (dryRun) {
|
|
145
|
+
deps.out(`cueloop would update in ${targetInstallDir}`);
|
|
146
|
+
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
deps.out("Checking for updates to latest version...");
|
|
150
|
+
const latest = await deps.fetchLatestVersion();
|
|
151
|
+
|
|
152
|
+
if (latest !== undefined && !isNewer(latest, deps.currentVersion)) {
|
|
153
|
+
deps.out(`cueloop is up to date (${deps.currentVersion})`);
|
|
154
|
+
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
deps.out(`updating cueloop in ${targetInstallDir}...`);
|
|
158
|
+
const exitCode = await deps.runInstaller(targetInstallDir);
|
|
159
|
+
|
|
160
|
+
if (exitCode === 0) deps.out("Update ran successfully! Please restart cueloop.");
|
|
161
|
+
|
|
162
|
+
return exitCode;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** `cueloop update [--dry-run]` - update the installed cueloop binary in place. */
|
|
166
|
+
export async function updateCommand(argv: string[] = []): Promise<number> {
|
|
167
|
+
return runUpdate(
|
|
168
|
+
{
|
|
169
|
+
currentVersion: CLI_VERSION,
|
|
170
|
+
installDir: () => resolveInstallDir(process.execPath, process.env),
|
|
171
|
+
fetchLatestVersion,
|
|
172
|
+
runInstaller,
|
|
173
|
+
out: (message) => console.error(message),
|
|
174
|
+
},
|
|
175
|
+
argv.includes("--dry-run"),
|
|
176
|
+
);
|
|
177
|
+
}
|