@hellix-ai/cubes 0.1.0 → 0.1.2
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 +5 -3
- package/dist/cli.js +59 -31
- package/dist/cube.js +168 -0
- package/dist/sessions.js +5 -5
- package/dist/upgrade.js +105 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ cubes attach # pick a running one
|
|
|
12
12
|
cubes attach 7ff4 # or name one: an id prefix, or part of its title
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
No install: `npx @hellix-ai/cubes attach <session>`.
|
|
15
|
+
No install: `npx @hellix-ai/cubes attach <session>`. Later, `cubes upgrade` updates it in place.
|
|
16
16
|
|
|
17
17
|
## While attached
|
|
18
18
|
|
|
@@ -28,8 +28,9 @@ Every key goes to the session, exactly as if Claude Code were running locally, e
|
|
|
28
28
|
takes it back, the terminal says so and goes read-only; detach and attach again to reclaim it.
|
|
29
29
|
`cubes attach --view` watches without taking it.
|
|
30
30
|
|
|
31
|
-
A stopped or hibernated session is resumed before attaching (`--no-resume` to refuse)
|
|
32
|
-
connection reconnects on its own and replays
|
|
31
|
+
A stopped or hibernated session is resumed before attaching (`--no-resume` to refuse), with the
|
|
32
|
+
spinning Cubes mark on screen until it is up. A dropped connection reconnects on its own and replays
|
|
33
|
+
the screen.
|
|
33
34
|
|
|
34
35
|
Scrollback lives in the session's tmux: scroll with the mouse wheel or trackpad (tmux copy-mode), not
|
|
35
36
|
your terminal's own scrollback.
|
|
@@ -43,6 +44,7 @@ cubes whoami
|
|
|
43
44
|
cubes org [ALIAS]
|
|
44
45
|
cubes ls [--all] [--json]
|
|
45
46
|
cubes attach [SESSION] [--view] [--no-resume]
|
|
47
|
+
cubes upgrade [--check]
|
|
46
48
|
```
|
|
47
49
|
|
|
48
50
|
`--org ALIAS` works on any command. If you belong to several organizations, `cubes org` lists them and
|
package/dist/cli.js
CHANGED
|
@@ -11,10 +11,13 @@ import { makeApi, ApiError } from "./api.js";
|
|
|
11
11
|
import { attach, RESTORE_TERMINAL } from "./attach.js";
|
|
12
12
|
import { CliError, currentSession, discover, loginFromTokens, normalizeHost, orgsOf, pollDevice, revoke, startDevice, whoOf, } from "./auth.js";
|
|
13
13
|
import { readClipboardImage } from "./clipboard.js";
|
|
14
|
+
import { CubeSpinner } from "./cube.js";
|
|
14
15
|
import { clearLogin, configPath, loadLogin, saveLogin } from "./config.js";
|
|
15
16
|
import { parseArgs } from "./args.js";
|
|
16
17
|
import { formatTable, isUuid, listSessions, LIVE, resolveSession, stateOf } from "./sessions.js";
|
|
17
|
-
|
|
18
|
+
import { upgrade } from "./upgrade.js";
|
|
19
|
+
const PKG = createRequire(import.meta.url)("../package.json");
|
|
20
|
+
const VERSION = PKG.version;
|
|
18
21
|
const HELP = `cubes ${VERSION} - attach to your Cubes sessions from your own terminal
|
|
19
22
|
|
|
20
23
|
Usage:
|
|
@@ -24,6 +27,7 @@ Usage:
|
|
|
24
27
|
cubes org [ALIAS] list your orgs, or switch the default
|
|
25
28
|
cubes ls [--all] [--json] list sessions (--all includes spawned ones)
|
|
26
29
|
cubes attach [SESSION] [--view] [--no-resume] attach to a session
|
|
30
|
+
cubes upgrade [--check] install the newest cubes (--check only reports)
|
|
27
31
|
|
|
28
32
|
SESSION is an id, the start of one (4+ characters), or part of its title. Left out, you pick from
|
|
29
33
|
the running sessions. A stopped or hibernated session is resumed first unless --no-resume.
|
|
@@ -90,8 +94,14 @@ async function cmdLogin(a) {
|
|
|
90
94
|
process.stderr.write(`\nTo sign in to ${host}, open:\n\n ${url}\n\nand confirm the code ${start.user_code}\n\n`);
|
|
91
95
|
if (!a.flags.has("no-browser"))
|
|
92
96
|
openBrowser(url);
|
|
93
|
-
process.stderr
|
|
94
|
-
|
|
97
|
+
const spin = new CubeSpinner(process.stderr, "Waiting for you to approve it in the browser");
|
|
98
|
+
let tokens;
|
|
99
|
+
try {
|
|
100
|
+
tokens = await pollDevice(d, start);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
spin.stop();
|
|
104
|
+
}
|
|
95
105
|
const orgs = orgsOf(tokens.access_token);
|
|
96
106
|
let org = str(a.flags, "org");
|
|
97
107
|
if (org && orgs.length && !orgs.includes(org))
|
|
@@ -168,36 +178,38 @@ async function ensureRunning(api, row, resume) {
|
|
|
168
178
|
let s = row;
|
|
169
179
|
if (s.status === "running")
|
|
170
180
|
return s;
|
|
171
|
-
if (!LIVE.has(s.status))
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
181
|
+
if (!LIVE.has(s.status) && !resume)
|
|
182
|
+
throw new CliError(`that session is ${s.status}; leave out --no-resume to bring it back`);
|
|
183
|
+
const name = `"${s.title ?? s.id.slice(0, 8)}"`;
|
|
184
|
+
// The same cube the session's own screen spins while Claude boots, so the wait reads as one thing.
|
|
185
|
+
const spin = new CubeSpinner(process.stderr, `${LIVE.has(s.status) ? "Starting" : "Resuming"} ${name}`);
|
|
186
|
+
try {
|
|
187
|
+
if (!LIVE.has(s.status)) {
|
|
188
|
+
try {
|
|
189
|
+
await api.post(`/sessions/${id}/resume`);
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
if (!(e instanceof ApiError && e.status === 409 && /already running/.test(e.message)))
|
|
193
|
+
throw e;
|
|
194
|
+
}
|
|
177
195
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
196
|
+
const deadline = Date.now() + 5 * 60_000;
|
|
197
|
+
while (Date.now() < deadline) {
|
|
198
|
+
s = await api.get(`/sessions/${id}`);
|
|
199
|
+
if (s.status === "running")
|
|
200
|
+
return s;
|
|
201
|
+
if (!LIVE.has(s.status))
|
|
202
|
+
throw new CliError(`the session stopped while starting (${s.status})`);
|
|
203
|
+
if (s.provision_note) {
|
|
204
|
+
spin.update(`${s.provision_note}${s.provision_total ? ` (${s.provision_done ?? 0}/${s.provision_total})` : ""}`);
|
|
205
|
+
}
|
|
206
|
+
await sleep(1000);
|
|
181
207
|
}
|
|
208
|
+
throw new CliError("the session is taking too long to start - check it in the browser");
|
|
182
209
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
while (Date.now() < deadline) {
|
|
186
|
-
s = await api.get(`/sessions/${id}`);
|
|
187
|
-
if (s.status === "running")
|
|
188
|
-
return s;
|
|
189
|
-
if (!LIVE.has(s.status))
|
|
190
|
-
throw new CliError(`the session stopped while starting (${s.status})`);
|
|
191
|
-
const note = s.provision_note
|
|
192
|
-
? `${s.provision_note}${s.provision_total ? ` (${s.provision_done ?? 0}/${s.provision_total})` : ""}`
|
|
193
|
-
: "starting";
|
|
194
|
-
if (note !== lastNote) {
|
|
195
|
-
process.stderr.write(` ${note}…\n`);
|
|
196
|
-
lastNote = note;
|
|
197
|
-
}
|
|
198
|
-
await sleep(1000);
|
|
210
|
+
finally {
|
|
211
|
+
spin.stop();
|
|
199
212
|
}
|
|
200
|
-
throw new CliError("the session is taking too long to start - check it in the browser");
|
|
201
213
|
}
|
|
202
214
|
async function cmdAttach(a) {
|
|
203
215
|
const s = currentSession({ org: str(a.flags, "org") });
|
|
@@ -281,10 +293,26 @@ async function main(argv) {
|
|
|
281
293
|
return 0;
|
|
282
294
|
case "attach":
|
|
283
295
|
case "a": return cmdAttach(a);
|
|
296
|
+
case "upgrade":
|
|
297
|
+
case "update":
|
|
298
|
+
return upgrade({
|
|
299
|
+
pkg: PKG.name, current: VERSION, scriptPath: process.argv[1] ?? "",
|
|
300
|
+
log: (line) => process.stderr.write(`${line}\n`),
|
|
301
|
+
}, { check: a.flags.has("check") });
|
|
284
302
|
default: throw new CliError(`unknown command "${a.cmd}" - see \`cubes help\``);
|
|
285
303
|
}
|
|
286
304
|
}
|
|
287
|
-
|
|
305
|
+
/** Exit only once everything written has reached its destination. A write to a PIPE is asynchronous
|
|
306
|
+
* in Node, so a bare process.exit() after `cubes ls --json | jq` cut the JSON off mid-string. An empty
|
|
307
|
+
* write's callback fires after every write queued before it. */
|
|
308
|
+
function exitAfterFlush(code) {
|
|
309
|
+
let pending = 2;
|
|
310
|
+
const done = () => { if (--pending === 0)
|
|
311
|
+
process.exit(code); };
|
|
312
|
+
process.stdout.write("", done);
|
|
313
|
+
process.stderr.write("", done);
|
|
314
|
+
}
|
|
315
|
+
main(process.argv.slice(2)).then((code) => exitAfterFlush(code), (e) => {
|
|
288
316
|
process.stderr.write(`cubes: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
289
|
-
|
|
317
|
+
exitAfterFlush(e instanceof CliError ? e.exitCode : 1);
|
|
290
318
|
});
|
package/dist/cube.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The Cubes mark as a spinning ASCII cube, drawn in YOUR terminal while the CLI waits on something.
|
|
2
|
+
//
|
|
3
|
+
// It is the session boot animation (server/src/engine/interactiveAuth.ts, CUBE_SPIN_AWK) ported
|
|
4
|
+
// line for line: the same six faces sampled parametrically, rotated yaw-then-pitch with the pitch on
|
|
5
|
+
// the true isometric angle (so frame 0 is the app icon's pose), the same back-face culling, the same
|
|
6
|
+
// Lambert shading quantised to five steps of one indigo ramp and four ASCII densities, and the same
|
|
7
|
+
// 2π/40 turn so it loops without a stutter. So a resume looks like one continuous thing: this cube
|
|
8
|
+
// while the session comes back, then the session's own cube while Claude boots, then Claude.
|
|
9
|
+
//
|
|
10
|
+
// It is drawn INLINE, not on the alternate screen: what is above it (a sign-in code you have to type)
|
|
11
|
+
// stays where it is, and when the wait ends the region is erased and the cursor is back where it was.
|
|
12
|
+
const NFR = 40;
|
|
13
|
+
const CH = ["", "+", "*", "*", "%", "#"];
|
|
14
|
+
const CO = ["", "\x1b[38;5;61m", "\x1b[38;5;62m", "\x1b[38;5;63m", "\x1b[38;5;105m", "\x1b[38;5;147m"];
|
|
15
|
+
const CAPTION = "\x1b[38;5;147m";
|
|
16
|
+
const FACES = [
|
|
17
|
+
// outward normal, then the two tangents that sweep it
|
|
18
|
+
{ n: [0, 1, 0], a: [1, 0, 0], b: [0, 0, 1] },
|
|
19
|
+
{ n: [0, -1, 0], a: [1, 0, 0], b: [0, 0, 1] },
|
|
20
|
+
{ n: [1, 0, 0], a: [0, 1, 0], b: [0, 0, 1] },
|
|
21
|
+
{ n: [-1, 0, 0], a: [0, 1, 0], b: [0, 0, 1] },
|
|
22
|
+
{ n: [0, 0, 1], a: [1, 0, 0], b: [0, 1, 0] },
|
|
23
|
+
{ n: [0, 0, -1], a: [1, 0, 0], b: [0, 1, 0] },
|
|
24
|
+
];
|
|
25
|
+
const SP = Math.sin(0.61548);
|
|
26
|
+
const CP = Math.cos(0.61548);
|
|
27
|
+
const L = [-0.5, 0.72, 0.48];
|
|
28
|
+
/** The cube's size for a terminal of `cols` x `rows`: the boot window's caps, a little shorter so
|
|
29
|
+
* an inline cube does not take over the screen. Null when there is no room to draw one. */
|
|
30
|
+
export function cubeSize(cols, rows) {
|
|
31
|
+
let w = Math.min(cols - 4, 46);
|
|
32
|
+
let h = Math.min(rows - 6, 15);
|
|
33
|
+
if (h % 2 === 0)
|
|
34
|
+
h--;
|
|
35
|
+
if (w < 30 || h < 9)
|
|
36
|
+
return null;
|
|
37
|
+
return { w, h };
|
|
38
|
+
}
|
|
39
|
+
/** One frame: `h` rows of shade levels 0-5 (0 = empty). Frame numbers past 39 wrap seamlessly. */
|
|
40
|
+
export function cubeFrame(frame, w, h) {
|
|
41
|
+
const cx = (w - 1) / 2;
|
|
42
|
+
const cy = (h - 1) / 2;
|
|
43
|
+
const ys = cy / 1.634;
|
|
44
|
+
const xs = Math.min(2 * ys, cx / 1.415);
|
|
45
|
+
const th = 0.785398 + frame * (6.283185 / NFR);
|
|
46
|
+
const sq = Math.sin(th);
|
|
47
|
+
const cq = Math.cos(th);
|
|
48
|
+
const sc = Array.from({ length: h }, () => new Array(w).fill(0));
|
|
49
|
+
for (const f of FACES) {
|
|
50
|
+
const [nx, ny, nz] = f.n;
|
|
51
|
+
const mx = nx * cq - nz * sq;
|
|
52
|
+
const mz = nx * sq + nz * cq;
|
|
53
|
+
const qy = ny * CP - mz * SP;
|
|
54
|
+
const qz = ny * SP + mz * CP;
|
|
55
|
+
if (qz <= 0.001)
|
|
56
|
+
continue; // facing away
|
|
57
|
+
const bb = mx * L[0] + qy * L[1] + qz * L[2];
|
|
58
|
+
const lv = Math.min(5, Math.max(1, 1 + Math.floor(((bb + 0.55) / 1.5) * 5)));
|
|
59
|
+
for (let u = -1; u <= 1.0001; u += 0.035) {
|
|
60
|
+
for (let v = -1; v <= 1.0001; v += 0.035) {
|
|
61
|
+
const px = nx + u * f.a[0] + v * f.b[0];
|
|
62
|
+
const py = ny + u * f.a[1] + v * f.b[1];
|
|
63
|
+
const pz = nz + u * f.a[2] + v * f.b[2];
|
|
64
|
+
const rx = px * cq - pz * sq;
|
|
65
|
+
const rz = px * sq + pz * cq;
|
|
66
|
+
const ty = py * CP - rz * SP;
|
|
67
|
+
const sx = Math.floor(cx + xs * rx + 0.5);
|
|
68
|
+
const sy = Math.floor(cy - ys * ty + 0.5);
|
|
69
|
+
if (sx < 0 || sx >= w || sy < 0 || sy >= h)
|
|
70
|
+
continue;
|
|
71
|
+
sc[sy][sx] = lv;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return sc;
|
|
76
|
+
}
|
|
77
|
+
export function renderRow(levels, color) {
|
|
78
|
+
let out = "";
|
|
79
|
+
let cur = 0;
|
|
80
|
+
for (const s of levels) {
|
|
81
|
+
if (s === 0) {
|
|
82
|
+
if (cur && color)
|
|
83
|
+
out += "\x1b[0m";
|
|
84
|
+
cur = 0;
|
|
85
|
+
out += " ";
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
if (cur !== s && color)
|
|
89
|
+
out += CO[s];
|
|
90
|
+
cur = s;
|
|
91
|
+
out += CH[s];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return (color ? out + "\x1b[0m" : out).replace(/\s+$/, "");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A wait with the cube on it. Where it cannot draw (not a terminal, TERM=dumb, or too small), each
|
|
98
|
+
* caption is printed as an ordinary line instead, so a pipe or a CI log still says what is happening.
|
|
99
|
+
* NO_COLOR keeps the cube and drops the colour, which the four ASCII densities survive.
|
|
100
|
+
*/
|
|
101
|
+
export class CubeSpinner {
|
|
102
|
+
out;
|
|
103
|
+
timer = null;
|
|
104
|
+
frame = 0;
|
|
105
|
+
drawn = 0;
|
|
106
|
+
caption;
|
|
107
|
+
size;
|
|
108
|
+
color;
|
|
109
|
+
restore = () => this.out.write("\x1b[?25h");
|
|
110
|
+
// A Ctrl-C mid-wait: Node's default SIGINT exit runs no "exit" listeners, so it would leave the
|
|
111
|
+
// cube on screen and the cursor hidden. Clean up, then exit the way the default would have.
|
|
112
|
+
onSigint = () => { this.stop(); process.exit(130); };
|
|
113
|
+
constructor(out, caption, env = process.env) {
|
|
114
|
+
this.out = out;
|
|
115
|
+
this.caption = caption;
|
|
116
|
+
this.size = out.isTTY && env.TERM !== "dumb" ? cubeSize(out.columns ?? 80, out.rows ?? 24) : null;
|
|
117
|
+
this.color = !env.NO_COLOR;
|
|
118
|
+
if (!this.size) {
|
|
119
|
+
out.write(`${caption}…\n`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
out.write("\x1b[?25l");
|
|
123
|
+
process.once("exit", this.restore);
|
|
124
|
+
process.once("SIGINT", this.onSigint);
|
|
125
|
+
this.draw();
|
|
126
|
+
this.timer = setInterval(() => this.draw(), 60);
|
|
127
|
+
}
|
|
128
|
+
update(caption) {
|
|
129
|
+
if (caption === this.caption)
|
|
130
|
+
return;
|
|
131
|
+
this.caption = caption;
|
|
132
|
+
if (!this.size)
|
|
133
|
+
this.out.write(`${caption}…\n`);
|
|
134
|
+
}
|
|
135
|
+
/** Erase the cube, put the cursor back, and say how it ended (if anything). */
|
|
136
|
+
stop(final) {
|
|
137
|
+
if (this.timer)
|
|
138
|
+
clearInterval(this.timer);
|
|
139
|
+
this.timer = null;
|
|
140
|
+
if (this.size) {
|
|
141
|
+
if (this.drawn)
|
|
142
|
+
this.out.write(`\x1b[${this.drawn}A\r\x1b[J`);
|
|
143
|
+
this.restore();
|
|
144
|
+
process.removeListener("exit", this.restore);
|
|
145
|
+
process.removeListener("SIGINT", this.onSigint);
|
|
146
|
+
this.size = null;
|
|
147
|
+
}
|
|
148
|
+
if (final)
|
|
149
|
+
this.out.write(`${final}\n`);
|
|
150
|
+
}
|
|
151
|
+
draw() {
|
|
152
|
+
const { w, h } = this.size;
|
|
153
|
+
const rows = cubeFrame(this.frame, w, h);
|
|
154
|
+
const dots = ".".repeat((Math.floor(this.frame / 4) % 3) + 1);
|
|
155
|
+
// A caption that wraps would add a row this redraw does not know to move back over.
|
|
156
|
+
const room = Math.max(10, (this.out.columns ?? 80) - 6);
|
|
157
|
+
const text = this.caption.length > room ? this.caption.slice(0, room - 1) + "…" : this.caption;
|
|
158
|
+
const pad = " ".repeat(Math.max(0, Math.floor((w - text.length - 3) / 2) + 2));
|
|
159
|
+
let s = this.drawn ? `\x1b[${this.drawn}A` : "";
|
|
160
|
+
for (const r of rows)
|
|
161
|
+
s += `\r ${renderRow(r, this.color)}\x1b[K\n`;
|
|
162
|
+
const cap = `${text}${dots}`;
|
|
163
|
+
s += `\r\x1b[K\n\r${pad}${this.color ? CAPTION + cap + "\x1b[0m" : cap}\x1b[K\n`;
|
|
164
|
+
this.out.write(s);
|
|
165
|
+
this.drawn = rows.length + 2;
|
|
166
|
+
this.frame = (this.frame + 1) % NFR;
|
|
167
|
+
}
|
|
168
|
+
}
|
package/dist/sessions.js
CHANGED
|
@@ -65,19 +65,19 @@ function cut(s, w) {
|
|
|
65
65
|
return s.length <= w ? s : s.slice(0, w - 1) + "…";
|
|
66
66
|
}
|
|
67
67
|
export function formatTable(rows, width, now = Date.now()) {
|
|
68
|
-
const head = ["ID", "STATE", "AGE", "TITLE", "
|
|
68
|
+
const head = ["ID", "STATE", "AGE", "TITLE", "GROUP"];
|
|
69
69
|
const body = rows.map((s) => [
|
|
70
70
|
s.id.slice(0, 8),
|
|
71
71
|
stateOf(s),
|
|
72
72
|
age(s.last_attached_at ?? s.created_at, now),
|
|
73
73
|
s.title || "(untitled)",
|
|
74
|
-
s.
|
|
74
|
+
s.group_name ?? "",
|
|
75
75
|
]);
|
|
76
76
|
const w = (i) => Math.max(head[i].length, ...body.map((r) => r[i].length));
|
|
77
77
|
const fixed = [w(0), w(1), w(2)];
|
|
78
|
-
const
|
|
79
|
-
const titleW = Math.max(10, Math.min(w(3), width - fixed.reduce((a, b) => a + b, 0) -
|
|
78
|
+
const groupW = Math.min(w(4), 32);
|
|
79
|
+
const titleW = Math.max(10, Math.min(w(3), width - fixed.reduce((a, b) => a + b, 0) - groupW - 8));
|
|
80
80
|
const line = (r) => [r[0].padEnd(fixed[0]), r[1].padEnd(fixed[1]), r[2].padStart(fixed[2]),
|
|
81
|
-
cut(r[3], titleW).padEnd(titleW), cut(r[4],
|
|
81
|
+
cut(r[3], titleW).padEnd(titleW), cut(r[4], groupW)].join(" ").trimEnd();
|
|
82
82
|
return [line(head), ...body.map(line)].join("\n");
|
|
83
83
|
}
|
package/dist/upgrade.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// `cubes upgrade`: ask npm for the newest release and reinstall with whatever installed this copy.
|
|
2
|
+
//
|
|
3
|
+
// The install command has to match how the CLI got onto the machine: `npm install -g` over a pnpm or
|
|
4
|
+
// bun global install leaves the old copy first on PATH, and an npx run is a throwaway cache entry
|
|
5
|
+
// that `npx @hellix-ai/cubes@latest` already refreshes. The path of the running script says which.
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { CliError } from "./auth.js";
|
|
8
|
+
export const REGISTRY = "https://registry.npmjs.org";
|
|
9
|
+
/** -1 / 0 / 1 for plain x.y.z versions. A prerelease sorts below its release (0.2.0-rc < 0.2.0). */
|
|
10
|
+
export function compareVersions(a, b) {
|
|
11
|
+
const parse = (v) => {
|
|
12
|
+
const [core = "", pre] = v.trim().replace(/^v/, "").split("-", 2);
|
|
13
|
+
return { nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0), pre };
|
|
14
|
+
};
|
|
15
|
+
const x = parse(a);
|
|
16
|
+
const y = parse(b);
|
|
17
|
+
for (let i = 0; i < 3; i++) {
|
|
18
|
+
const d = (x.nums[i] ?? 0) - (y.nums[i] ?? 0);
|
|
19
|
+
if (d)
|
|
20
|
+
return d > 0 ? 1 : -1;
|
|
21
|
+
}
|
|
22
|
+
if (x.pre && !y.pre)
|
|
23
|
+
return -1;
|
|
24
|
+
if (!x.pre && y.pre)
|
|
25
|
+
return 1;
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
export function installerFor(scriptPath) {
|
|
29
|
+
const p = scriptPath.replace(/\\/g, "/");
|
|
30
|
+
if (p.includes("/_npx/"))
|
|
31
|
+
return "npx";
|
|
32
|
+
if (p.includes("/.bun/"))
|
|
33
|
+
return "bun";
|
|
34
|
+
if (/\/pnpm\/|\/\.pnpm\//.test(p))
|
|
35
|
+
return "pnpm";
|
|
36
|
+
if (/\/yarn\/global\//.test(p))
|
|
37
|
+
return "yarn";
|
|
38
|
+
return "npm";
|
|
39
|
+
}
|
|
40
|
+
export function installCommand(installer, spec) {
|
|
41
|
+
switch (installer) {
|
|
42
|
+
case "pnpm": return ["pnpm", "add", "-g", spec];
|
|
43
|
+
case "bun": return ["bun", "add", "-g", spec];
|
|
44
|
+
case "yarn": return ["yarn", "global", "add", spec];
|
|
45
|
+
default: return ["npm", "install", "-g", spec];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function latestVersion(pkg, fetchImpl = fetch) {
|
|
49
|
+
let res;
|
|
50
|
+
try {
|
|
51
|
+
res = await fetchImpl(`${REGISTRY}/${pkg.replace("/", "%2f")}/latest`, { signal: AbortSignal.timeout(10_000) });
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
throw new CliError(`cannot reach the npm registry: ${e.message}`);
|
|
55
|
+
}
|
|
56
|
+
if (!res.ok)
|
|
57
|
+
throw new CliError(`the npm registry answered HTTP ${res.status} for ${pkg}`);
|
|
58
|
+
const body = (await res.json());
|
|
59
|
+
if (typeof body.version !== "string")
|
|
60
|
+
throw new CliError("the npm registry did not say what the latest version is");
|
|
61
|
+
return body.version;
|
|
62
|
+
}
|
|
63
|
+
function runInherited(argv) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
// shell on Windows only, where the package managers are .cmd shims spawn cannot start directly.
|
|
66
|
+
const child = spawn(argv[0], argv.slice(1), { stdio: "inherit", shell: process.platform === "win32" });
|
|
67
|
+
child.on("error", () => resolve(127));
|
|
68
|
+
child.on("exit", (code) => resolve(code ?? 1));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Returns the process exit code. `check` only reports. */
|
|
72
|
+
export async function upgrade(d, opts = {}) {
|
|
73
|
+
const latest = await latestVersion(d.pkg, d.fetchImpl);
|
|
74
|
+
const cmp = compareVersions(d.current, latest);
|
|
75
|
+
if (cmp === 0) {
|
|
76
|
+
d.log(`cubes ${d.current} is the latest version.`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
if (cmp > 0) {
|
|
80
|
+
d.log(`cubes ${d.current} is newer than the latest published (${latest}); nothing to do.`);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
const installer = installerFor(d.scriptPath);
|
|
84
|
+
const spec = `${d.pkg}@${latest}`;
|
|
85
|
+
if (installer === "npx") {
|
|
86
|
+
d.log(`cubes ${latest} is available (this is ${d.current}). You are running it through npx, so ask for the new one:`);
|
|
87
|
+
d.log(` npx ${d.pkg}@latest <command>`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
const argv = installCommand(installer, spec);
|
|
91
|
+
if (opts.check) {
|
|
92
|
+
d.log(`cubes ${latest} is available (you have ${d.current}). Run \`cubes upgrade\`, or: ${argv.join(" ")}`);
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
d.log(`Upgrading cubes ${d.current} -> ${latest}: ${argv.join(" ")}`);
|
|
96
|
+
const code = await (d.run ?? runInherited)(argv);
|
|
97
|
+
if (code === 127)
|
|
98
|
+
throw new CliError(`could not run \`${argv[0]}\` - upgrade by hand: ${argv.join(" ")}`);
|
|
99
|
+
if (code !== 0) {
|
|
100
|
+
throw new CliError(`the upgrade failed (exit ${code}). If it was a permissions error, your global ${argv[0]} ` +
|
|
101
|
+
`directory is not writable by you - run it with the rights it needs: ${argv.join(" ")}`);
|
|
102
|
+
}
|
|
103
|
+
d.log(`cubes is now ${latest}.`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|