prized 0.18.0-rc.5 → 0.18.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/bin/prized.js +4 -2
- package/lib/install.js +85 -7
- package/lib/report.js +29 -0
- package/package.json +1 -1
package/bin/prized.js
CHANGED
|
@@ -7,19 +7,21 @@ import { spawnSync } from 'node:child_process';
|
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
8
8
|
import { ensureCli } from '../lib/install.js';
|
|
9
9
|
import { isReleaseVersion } from '../lib/release.js';
|
|
10
|
+
import { reporter } from '../lib/report.js';
|
|
10
11
|
|
|
11
12
|
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
13
|
+
const report = reporter(process.stderr);
|
|
12
14
|
|
|
13
15
|
try {
|
|
14
16
|
if (!isReleaseVersion(version)) {
|
|
15
17
|
throw new Error(`this is a development copy (${version}); run a published version: npx prized`);
|
|
16
18
|
}
|
|
17
|
-
const bin = await ensureCli(version, { log:
|
|
19
|
+
const bin = await ensureCli(version, { log: report.log, progress: report.progress });
|
|
18
20
|
const args = process.argv.slice(2);
|
|
19
21
|
const run = spawnSync(bin, args.length > 0 ? args : ['start'], { stdio: 'inherit' });
|
|
20
22
|
if (run.error) throw run.error;
|
|
21
23
|
process.exit(run.status ?? 1);
|
|
22
24
|
} catch (err) {
|
|
23
|
-
|
|
25
|
+
report.fail(err instanceof Error ? err.message : String(err));
|
|
24
26
|
process.exit(1);
|
|
25
27
|
}
|
package/lib/install.js
CHANGED
|
@@ -22,6 +22,9 @@ export async function ensureCli(version, opts = {}) {
|
|
|
22
22
|
root = cacheRoot(),
|
|
23
23
|
fetchImpl = fetch,
|
|
24
24
|
log = () => {},
|
|
25
|
+
progress = () => {},
|
|
26
|
+
stallMs = STALL_MS,
|
|
27
|
+
attempts = ATTEMPTS,
|
|
25
28
|
} = opts;
|
|
26
29
|
const dir = join(root, version);
|
|
27
30
|
const bin = join(dir, binaryName(platform));
|
|
@@ -30,10 +33,11 @@ export async function ensureCli(version, opts = {}) {
|
|
|
30
33
|
const asset = assetFor(version, platform, arch);
|
|
31
34
|
const from = `${base}/cli/v${version}`;
|
|
32
35
|
log(`Downloading Prized ${version}…`);
|
|
36
|
+
const fetchOpts = { fetchImpl, stallMs, attempts, log };
|
|
33
37
|
const [sums, sig, archive] = await Promise.all([
|
|
34
|
-
get(
|
|
35
|
-
get(
|
|
36
|
-
get(
|
|
38
|
+
get(`${from}/SHA256SUMS`, fetchOpts),
|
|
39
|
+
get(`${from}/SHA256SUMS.minisig`, fetchOpts),
|
|
40
|
+
get(`${from}/${asset}`, { ...fetchOpts, progress }),
|
|
37
41
|
]);
|
|
38
42
|
const trusted = verifyMinisign(sums, sig, pubkey);
|
|
39
43
|
if (trusted !== `prized ${version}`) {
|
|
@@ -104,9 +108,83 @@ export function sumFor(sums, name) {
|
|
|
104
108
|
return null;
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
/** No bytes for this long, connecting included, and a download is dropped. */
|
|
112
|
+
const STALL_MS = 30_000;
|
|
113
|
+
/** Tries per file: a stall, a dropped connection or a 5xx is tried again. */
|
|
114
|
+
const ATTEMPTS = 3;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The body at url. A connection can stop moving without closing (a Wi-Fi
|
|
118
|
+
* roam, a middlebox) and fetch has no timeout of its own: without one the
|
|
119
|
+
* first `npx prized` sat on "Downloading…" for good. Each try is dropped
|
|
120
|
+
* once no bytes arrived for stallMs; a stall, a network error or a 5xx is
|
|
121
|
+
* tried again, anything else (a 404) is the answer.
|
|
122
|
+
*/
|
|
123
|
+
async function get(url, { fetchImpl, stallMs, attempts, log, progress = () => {} }) {
|
|
124
|
+
for (let attempt = 1; ; attempt++) {
|
|
125
|
+
try {
|
|
126
|
+
return await getOnce(url, fetchImpl, stallMs, progress);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (!err.retry || attempt >= attempts) {
|
|
129
|
+
if (err.retry) err.message += ` (tried ${attempts} times; check your connection and run npx prized again)`;
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
log(`${err.message}; trying again…`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function getOnce(url, fetchImpl, stallMs, progress) {
|
|
138
|
+
const ac = new AbortController();
|
|
139
|
+
const stalled = new Error(`the download of ${url.slice(url.lastIndexOf('/') + 1)} stalled (nothing for ${Math.round(stallMs / 1000)} s)`);
|
|
140
|
+
stalled.retry = true;
|
|
141
|
+
let timer;
|
|
142
|
+
const arm = () => {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
timer = setTimeout(() => ac.abort(stalled), stallMs);
|
|
145
|
+
};
|
|
146
|
+
const failed = (err) => {
|
|
147
|
+
if (ac.signal.aborted) return stalled;
|
|
148
|
+
const e = new Error(`${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
149
|
+
e.retry = true;
|
|
150
|
+
return e;
|
|
151
|
+
};
|
|
152
|
+
arm();
|
|
153
|
+
try {
|
|
154
|
+
let res;
|
|
155
|
+
try {
|
|
156
|
+
res = await fetchImpl(url, { redirect: 'follow', signal: ac.signal });
|
|
157
|
+
} catch (err) {
|
|
158
|
+
throw failed(err);
|
|
159
|
+
}
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
// Unread, the answer's body can hold its connection through the
|
|
162
|
+
// retries that follow.
|
|
163
|
+
await res.body?.cancel().catch(() => {});
|
|
164
|
+
const e = new Error(`${url} answered ${res.status}`);
|
|
165
|
+
e.retry = res.status >= 500;
|
|
166
|
+
throw e;
|
|
167
|
+
}
|
|
168
|
+
const total = Number(res.headers?.get?.('content-length')) || 0;
|
|
169
|
+
const reader = res.body.getReader();
|
|
170
|
+
const chunks = [];
|
|
171
|
+
let got = 0;
|
|
172
|
+
for (;;) {
|
|
173
|
+
let step;
|
|
174
|
+
try {
|
|
175
|
+
step = await reader.read();
|
|
176
|
+
} catch (err) {
|
|
177
|
+
throw failed(err);
|
|
178
|
+
}
|
|
179
|
+
if (step.done) break;
|
|
180
|
+
chunks.push(step.value);
|
|
181
|
+
got += step.value.length;
|
|
182
|
+
arm();
|
|
183
|
+
progress(got, total);
|
|
184
|
+
}
|
|
185
|
+
return Buffer.concat(chunks);
|
|
186
|
+
} finally {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
}
|
|
111
189
|
}
|
|
112
190
|
|
package/lib/report.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// What `npx prized` prints while it fetches the CLI, on stderr: a line per
|
|
2
|
+
// event, and on a terminal the archive's progress rewriting one line under
|
|
3
|
+
// "Downloading…" so a slow download does not read as stuck. Any line that
|
|
4
|
+
// follows a progress line in flight (a retry notice, the final error)
|
|
5
|
+
// clears it first rather than trailing the percentage.
|
|
6
|
+
|
|
7
|
+
/** A reporter over out (a stream with write and isTTY). */
|
|
8
|
+
export function reporter(out) {
|
|
9
|
+
let shown = -1;
|
|
10
|
+
const midLine = () => shown >= 0 && shown < 100;
|
|
11
|
+
const clear = () => (midLine() ? '\r\x1b[2K' : '');
|
|
12
|
+
return {
|
|
13
|
+
log(line) {
|
|
14
|
+
out.write(`${clear()}${line}\n`);
|
|
15
|
+
if (midLine()) shown = -1;
|
|
16
|
+
},
|
|
17
|
+
progress(got, total) {
|
|
18
|
+
if (!out.isTTY || !total) return;
|
|
19
|
+
const pct = Math.min(100, Math.floor((got * 100) / total));
|
|
20
|
+
if (pct === shown) return;
|
|
21
|
+
shown = pct;
|
|
22
|
+
out.write(`\r\x1b[2K ${pct}% of ${(total / 1e6).toFixed(1)} MB${pct >= 100 ? '\n' : ''}`);
|
|
23
|
+
},
|
|
24
|
+
fail(message) {
|
|
25
|
+
out.write(`${clear()}prized: ${message}\n`);
|
|
26
|
+
shown = -1;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prized",
|
|
3
|
-
"version": "0.18.0
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Prized: a cloud dev box for your coding agents. npx prized signs you up, starts your box and sets up this machine.",
|
|
5
5
|
"homepage": "https://prized.dev",
|
|
6
6
|
"license": "UNLICENSED",
|