cueloop 0.1.0-alpha.68 → 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 -11
- package/src/session-commands.ts +9 -1
- package/src/update-command.test.ts +163 -0
- package/src/update-command.ts +118 -23
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,7 +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 () => (await import("./update-command")).updateCommand(),
|
|
117
|
+
update: async (rest) => (await import("./update-command")).updateCommand(rest),
|
|
118
118
|
review: (rest) => reviewEntry(rest),
|
|
119
119
|
"review-post": async (rest) => (await import("./pr")).reviewPostCommand(rest),
|
|
120
120
|
dev: async () => (await import("./dev-command")).devCommand(),
|
|
@@ -203,26 +203,30 @@ async function replyCommand(argv: string[]): Promise<number> {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
|
-
* `cueloop prototype <file.
|
|
207
|
-
*
|
|
208
|
-
* 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.
|
|
209
210
|
*/
|
|
210
211
|
async function prototypeCommand(argv: string[]): Promise<number> {
|
|
211
212
|
const parsed = parseArgs(argv);
|
|
212
213
|
const selector = openSelector(parsed);
|
|
213
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;
|
|
214
218
|
const looksLikeFile =
|
|
215
|
-
selector !== undefined && !isSessionId(selector) &&
|
|
219
|
+
selector !== undefined && !isSessionId(selector) && (isHtmlPrototype || isMarkdownPrototype);
|
|
216
220
|
|
|
217
221
|
if (wantsOpen || !looksLikeFile)
|
|
218
222
|
return openReviewOfKind(isPrototypeReview, "prototype", selector);
|
|
219
223
|
|
|
220
224
|
const path = resolve(selector);
|
|
221
|
-
const
|
|
225
|
+
const content = await Bun.file(path)
|
|
222
226
|
.text()
|
|
223
227
|
.catch(() => undefined);
|
|
224
228
|
|
|
225
|
-
if (
|
|
229
|
+
if (content === undefined) {
|
|
226
230
|
console.error(`prototype: cannot read ${path}`);
|
|
227
231
|
|
|
228
232
|
return 1;
|
|
@@ -230,8 +234,9 @@ async function prototypeCommand(argv: string[]): Promise<number> {
|
|
|
230
234
|
const client = await DaemonClient.connect({ autostart: true });
|
|
231
235
|
const review = await openReview(client, {
|
|
232
236
|
type: "prototype",
|
|
233
|
-
content
|
|
234
|
-
prototypePath
|
|
237
|
+
content,
|
|
238
|
+
// only an HTML entry carries a prototypePath, the signal for the experimental pixel mode
|
|
239
|
+
prototypePath: isHtmlPrototype ? path : undefined,
|
|
235
240
|
title: basename(path),
|
|
236
241
|
});
|
|
237
242
|
|
|
@@ -326,7 +331,7 @@ function printHelp(): void {
|
|
|
326
331
|
" cueloop diff [id|title] review your working tree (untracked files included);",
|
|
327
332
|
" with a clean tree, open the latest pending diff review",
|
|
328
333
|
" cueloop review <pr> review a pull request (--no-tui prints the session)",
|
|
329
|
-
" cueloop prototype <file.
|
|
334
|
+
" cueloop prototype <file.md> review a component design doc (or open the latest by id/title)",
|
|
330
335
|
"",
|
|
331
336
|
"share:",
|
|
332
337
|
" cueloop serve [session-id] share over ssh: observers are read-only,",
|
|
@@ -345,7 +350,7 @@ function printHelp(): void {
|
|
|
345
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)",
|
|
346
351
|
" cueloop actions list list the quick-action vocabulary (for annotate --action)",
|
|
347
352
|
" cueloop refine mine past reviews into a markdown report + writeback proposals",
|
|
348
|
-
" cueloop update
|
|
353
|
+
" cueloop update [--dry-run] update the installed cueloop binary (--dry-run reports the target only)",
|
|
349
354
|
" cueloop wake <id> [--harness codex --thread <id>] resume the agent with the verdict (spawn detached)",
|
|
350
355
|
" cueloop review-post <id> <pr> post a resolved session's verdict back to the PR",
|
|
351
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
|
+
});
|
package/src/update-command.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
2
2
|
import * as v from "valibot";
|
|
3
|
+
import { CLI_VERSION } from "./version";
|
|
3
4
|
|
|
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
|
+
|
|
5
9
|
const InstallerScriptSchema = v.pipe(
|
|
6
10
|
v.string(),
|
|
7
11
|
v.minLength(1),
|
|
@@ -11,44 +15,70 @@ const InstallerScriptSchema = v.pipe(
|
|
|
11
15
|
),
|
|
12
16
|
);
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
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;
|
|
16
22
|
|
|
17
|
-
return `${
|
|
23
|
+
return `${env.HOME}/.local/bin`;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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;
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
}
|
|
36
|
+
const dir = dirname(isAbsolute(execPath) ? execPath : resolve(execPath));
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
if (dir === "/$bunfs" || dir.startsWith("/$bunfs/")) return undefined;
|
|
39
|
+
|
|
40
|
+
return dir;
|
|
29
41
|
}
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
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);
|
|
33
46
|
}
|
|
34
47
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
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());
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
console.error("cueloop update: HOME is not set and the current binary path is unknown");
|
|
56
|
+
if (!parsed.success) return undefined;
|
|
41
57
|
|
|
42
|
-
|
|
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;
|
|
43
67
|
}
|
|
44
|
-
|
|
45
|
-
console.error("cueloop update: CUELOOP_INSTALL_DIR must be an absolute path");
|
|
68
|
+
}
|
|
46
69
|
|
|
47
|
-
|
|
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;
|
|
48
76
|
}
|
|
77
|
+
}
|
|
49
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> {
|
|
50
81
|
try {
|
|
51
|
-
console.error(`updating cueloop in ${targetInstallDir}...`);
|
|
52
82
|
const response = await fetch(INSTALL_URL);
|
|
53
83
|
|
|
54
84
|
if (!response.ok) {
|
|
@@ -67,7 +97,8 @@ export async function updateCommand(): Promise<number> {
|
|
|
67
97
|
stdin: "pipe",
|
|
68
98
|
stdout: "inherit",
|
|
69
99
|
stderr: "inherit",
|
|
70
|
-
|
|
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" },
|
|
71
102
|
});
|
|
72
103
|
|
|
73
104
|
child.stdin.write(parsed.output);
|
|
@@ -80,3 +111,67 @@ export async function updateCommand(): Promise<number> {
|
|
|
80
111
|
return 1;
|
|
81
112
|
}
|
|
82
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
|
+
}
|