@runinfra/cli 0.1.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/LICENSE +48 -0
- package/README.md +458 -0
- package/bin/runinfra.mjs +46 -0
- package/dist/args.js +142 -0
- package/dist/artifact-name.js +62 -0
- package/dist/auth-api.js +177 -0
- package/dist/browser.js +70 -0
- package/dist/catalog-api.js +89 -0
- package/dist/checksum.js +28 -0
- package/dist/context.js +52 -0
- package/dist/credentials.js +136 -0
- package/dist/disk.js +33 -0
- package/dist/downloader.js +265 -0
- package/dist/endpoints.js +58 -0
- package/dist/env.js +1 -0
- package/dist/errors.js +54 -0
- package/dist/help.js +47 -0
- package/dist/http.js +204 -0
- package/dist/lease.js +91 -0
- package/dist/login.js +207 -0
- package/dist/logout.js +17 -0
- package/dist/loopback.js +142 -0
- package/dist/main.js +70 -0
- package/dist/output.js +58 -0
- package/dist/paths.js +20 -0
- package/dist/pkce.js +24 -0
- package/dist/progress.js +63 -0
- package/dist/prompt.js +46 -0
- package/dist/pull.js +236 -0
- package/dist/range-plan.js +112 -0
- package/dist/redact.js +8 -0
- package/dist/scopes.js +2 -0
- package/dist/sidecar.js +108 -0
- package/dist/timers.js +13 -0
- package/dist/version.js +14 -0
- package/dist/whoami.js +28 -0
- package/package.json +43 -0
package/dist/progress.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const MIN_ELAPSED_MS_FOR_RATE = 1_000;
|
|
2
|
+
export function readProgress(snapshot) {
|
|
3
|
+
const total = Math.max(0, snapshot.totalBytes);
|
|
4
|
+
const completed = Math.min(Math.max(0, snapshot.completedBytes), total || Number.MAX_SAFE_INTEGER);
|
|
5
|
+
const percent = total > 0 ? (completed / total) * 100 : 0;
|
|
6
|
+
if (snapshot.elapsedMs < MIN_ELAPSED_MS_FOR_RATE || snapshot.sessionBytes <= 0) {
|
|
7
|
+
return { percent, bytesPerSecond: 0, etaSeconds: null };
|
|
8
|
+
}
|
|
9
|
+
const bytesPerSecond = snapshot.sessionBytes / (snapshot.elapsedMs / 1000);
|
|
10
|
+
const remaining = Math.max(0, total - completed);
|
|
11
|
+
return {
|
|
12
|
+
percent,
|
|
13
|
+
bytesPerSecond,
|
|
14
|
+
etaSeconds: bytesPerSecond > 0 ? Math.ceil(remaining / bytesPerSecond) : null,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const UNITS = ["B", "KB", "MB", "GB", "TB"];
|
|
18
|
+
export function formatBytes(bytes) {
|
|
19
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
20
|
+
return "0 B";
|
|
21
|
+
let value = bytes;
|
|
22
|
+
let unit = 0;
|
|
23
|
+
while (value >= 1024 && unit < UNITS.length - 1) {
|
|
24
|
+
value /= 1024;
|
|
25
|
+
unit += 1;
|
|
26
|
+
}
|
|
27
|
+
const decimals = unit === 0 ? 0 : value >= 100 ? 0 : value >= 10 ? 1 : 2;
|
|
28
|
+
return `${value.toFixed(decimals)} ${UNITS[unit] ?? "B"}`;
|
|
29
|
+
}
|
|
30
|
+
export function formatDuration(seconds) {
|
|
31
|
+
if (!Number.isFinite(seconds) || seconds < 0)
|
|
32
|
+
return "unknown";
|
|
33
|
+
const whole = Math.round(seconds);
|
|
34
|
+
if (whole < 60)
|
|
35
|
+
return `${whole}s`;
|
|
36
|
+
const minutes = Math.floor(whole / 60);
|
|
37
|
+
if (minutes < 60)
|
|
38
|
+
return `${minutes}m ${String(whole % 60).padStart(2, "0")}s`;
|
|
39
|
+
const hours = Math.floor(minutes / 60);
|
|
40
|
+
return `${hours}h ${String(minutes % 60).padStart(2, "0")}m`;
|
|
41
|
+
}
|
|
42
|
+
export function formatRate(bytesPerSecond) {
|
|
43
|
+
if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0)
|
|
44
|
+
return "--";
|
|
45
|
+
return `${formatBytes(bytesPerSecond)}/s`;
|
|
46
|
+
}
|
|
47
|
+
const BAR_WIDTH = 24;
|
|
48
|
+
export function renderBar(percent) {
|
|
49
|
+
const clamped = Math.min(100, Math.max(0, percent));
|
|
50
|
+
const filled = Math.round((clamped / 100) * BAR_WIDTH);
|
|
51
|
+
return `[${"#".repeat(filled)}${"-".repeat(BAR_WIDTH - filled)}]`;
|
|
52
|
+
}
|
|
53
|
+
export function renderProgress(label, snapshot, reading = readProgress(snapshot)) {
|
|
54
|
+
const eta = reading.etaSeconds === null ? "--" : formatDuration(reading.etaSeconds);
|
|
55
|
+
return [
|
|
56
|
+
label,
|
|
57
|
+
renderBar(reading.percent),
|
|
58
|
+
`${reading.percent.toFixed(1)}%`,
|
|
59
|
+
`${formatBytes(snapshot.completedBytes)} / ${formatBytes(snapshot.totalBytes)}`,
|
|
60
|
+
formatRate(reading.bytesPerSecond),
|
|
61
|
+
`ETA ${eta}`,
|
|
62
|
+
].join(" ");
|
|
63
|
+
}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
export function promptLine(question, streams = {}) {
|
|
3
|
+
const input = streams.input ?? process.stdin;
|
|
4
|
+
const output = streams.output ?? process.stderr;
|
|
5
|
+
let settled = false;
|
|
6
|
+
let close = null;
|
|
7
|
+
let finishRef = null;
|
|
8
|
+
const answer = new Promise((resolve) => {
|
|
9
|
+
const finish = (value) => {
|
|
10
|
+
if (settled)
|
|
11
|
+
return;
|
|
12
|
+
settled = true;
|
|
13
|
+
close?.();
|
|
14
|
+
resolve(value);
|
|
15
|
+
};
|
|
16
|
+
finishRef = finish;
|
|
17
|
+
let rl;
|
|
18
|
+
try {
|
|
19
|
+
rl = createInterface({
|
|
20
|
+
input,
|
|
21
|
+
output,
|
|
22
|
+
terminal: input.isTTY === true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
finish(null);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
close = () => {
|
|
30
|
+
rl.removeAllListeners();
|
|
31
|
+
rl.close();
|
|
32
|
+
if (input === process.stdin)
|
|
33
|
+
process.stdin.pause();
|
|
34
|
+
};
|
|
35
|
+
rl.on("line", (line) => finish(line.trim()));
|
|
36
|
+
rl.on("close", () => finish(null));
|
|
37
|
+
rl.on("error", () => finish(null));
|
|
38
|
+
output.write(`${question}`);
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
answer,
|
|
42
|
+
cancel() {
|
|
43
|
+
finishRef?.(null);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
package/dist/pull.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { open, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
3
|
+
import { artifactFileName, partFileName, sidecarFileName } from "./artifact-name.js";
|
|
4
|
+
import { probeArtifact, requestLease } from "./catalog-api.js";
|
|
5
|
+
import { judgeChecksum, sha256File } from "./checksum.js";
|
|
6
|
+
import { requireCredentials } from "./context.js";
|
|
7
|
+
import { assertFreeSpace } from "./disk.js";
|
|
8
|
+
import { runDownload } from "./downloader.js";
|
|
9
|
+
import { CliError } from "./errors.js";
|
|
10
|
+
import { formatBytes, formatDuration } from "./progress.js";
|
|
11
|
+
import { clampConcurrency, normalizeRanges, totalBytes, } from "./range-plan.js";
|
|
12
|
+
import { describeMismatch, parseSidecar, serializeSidecar, sidecarMismatch, SIDECAR_FORMAT, } from "./sidecar.js";
|
|
13
|
+
export async function pull(context, options, signal) {
|
|
14
|
+
const { output } = context;
|
|
15
|
+
const { credentials, endpoints } = await requireCredentials(context);
|
|
16
|
+
const concurrency = clampConcurrency(options.concurrency);
|
|
17
|
+
let lease = await requestLease(endpoints, credentials.apiKey, options.slug, signal);
|
|
18
|
+
let probe = await probeArtifact(lease.url, signal);
|
|
19
|
+
if (!probe.ok && probe.reason === "lease_expired") {
|
|
20
|
+
lease = await requestLease(endpoints, credentials.apiKey, options.slug, signal);
|
|
21
|
+
probe = await probeArtifact(lease.url, signal);
|
|
22
|
+
}
|
|
23
|
+
if (!probe.ok) {
|
|
24
|
+
throw new CliError("artifact_not_ready", `The stored files for ${options.slug} could not be read.`, "Try again shortly. If it keeps failing, contact support.");
|
|
25
|
+
}
|
|
26
|
+
const artifact = probe.probe;
|
|
27
|
+
const directory = resolveDestination(options.out);
|
|
28
|
+
await ensureDirectory(directory);
|
|
29
|
+
const fileName = artifactFileName({
|
|
30
|
+
url: lease.url,
|
|
31
|
+
slug: options.slug,
|
|
32
|
+
packageVersion: lease.packageVersion,
|
|
33
|
+
});
|
|
34
|
+
const finalPath = join(directory, fileName);
|
|
35
|
+
const partPath = join(directory, partFileName(fileName));
|
|
36
|
+
const sidecarPath = join(directory, sidecarFileName(fileName));
|
|
37
|
+
const existing = await statOrNull(finalPath);
|
|
38
|
+
if (existing !== null) {
|
|
39
|
+
if (existing.size === artifact.sizeBytes) {
|
|
40
|
+
output.info(`${fileName} is already downloaded.`);
|
|
41
|
+
output.result(finalPath);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
throw new CliError("storage_unwritable", `${finalPath} already exists and is a different size (${formatBytes(existing.size)} on disk, ${formatBytes(artifact.sizeBytes)} published).`, "Move or delete that file, or pass --out to a different directory.");
|
|
45
|
+
}
|
|
46
|
+
const sidecar = await resolveSidecar({
|
|
47
|
+
context,
|
|
48
|
+
slug: options.slug,
|
|
49
|
+
sidecarPath,
|
|
50
|
+
partPath,
|
|
51
|
+
packageVersion: lease.packageVersion,
|
|
52
|
+
checksumSha256: lease.checksumSha256,
|
|
53
|
+
sizeBytes: artifact.sizeBytes,
|
|
54
|
+
etag: artifact.etag,
|
|
55
|
+
});
|
|
56
|
+
const usableRanges = normalizeRanges(sidecar.committedRanges, artifact.sizeBytes) ?? [];
|
|
57
|
+
const alreadyOnDisk = totalBytes(usableRanges);
|
|
58
|
+
const remaining = Math.max(0, artifact.sizeBytes - alreadyOnDisk);
|
|
59
|
+
await assertFreeSpace(directory, remaining);
|
|
60
|
+
if (alreadyOnDisk > 0) {
|
|
61
|
+
output.info(`Resuming ${fileName}: ${formatBytes(alreadyOnDisk)} of ${formatBytes(artifact.sizeBytes)} already on disk.`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
output.info(`Downloading ${fileName} (${formatBytes(artifact.sizeBytes)}) into ${directory}`);
|
|
65
|
+
}
|
|
66
|
+
const handle = await openPartFile(partPath, artifact.sizeBytes);
|
|
67
|
+
const startedAtMs = Date.now();
|
|
68
|
+
let sessionBytes = 0;
|
|
69
|
+
try {
|
|
70
|
+
const result = await runDownload({
|
|
71
|
+
output,
|
|
72
|
+
endpoints,
|
|
73
|
+
apiKey: credentials.apiKey,
|
|
74
|
+
slug: options.slug,
|
|
75
|
+
label: fileName,
|
|
76
|
+
lease,
|
|
77
|
+
sidecar,
|
|
78
|
+
handle,
|
|
79
|
+
concurrency,
|
|
80
|
+
rangesSupported: artifact.rangesSupported,
|
|
81
|
+
signal,
|
|
82
|
+
onCommit: async (committed) => {
|
|
83
|
+
sidecar.committedRanges = committed;
|
|
84
|
+
sidecar.updatedAt = new Date().toISOString();
|
|
85
|
+
await writeSidecar(sidecarPath, sidecar);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
sessionBytes = result.sessionBytes;
|
|
89
|
+
await handle.sync();
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await handle.close().catch(() => undefined);
|
|
93
|
+
}
|
|
94
|
+
const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAtMs) / 1000));
|
|
95
|
+
output.info(`Transferred ${formatBytes(sessionBytes)} in ${formatDuration(elapsedSeconds)}.`);
|
|
96
|
+
if (sidecar.checksumSha256 !== null) {
|
|
97
|
+
let lastTickMs = 0;
|
|
98
|
+
const actual = await sha256File(partPath, (hashed) => {
|
|
99
|
+
const nowMs = Date.now();
|
|
100
|
+
if (nowMs - lastTickMs < 500)
|
|
101
|
+
return;
|
|
102
|
+
lastTickMs = nowMs;
|
|
103
|
+
const percent = artifact.sizeBytes > 0 ? (hashed / artifact.sizeBytes) * 100 : 0;
|
|
104
|
+
output.status(` Verifying checksum ${percent.toFixed(1)}%`);
|
|
105
|
+
}, signal);
|
|
106
|
+
output.endStatus();
|
|
107
|
+
const verdict = judgeChecksum(sidecar.checksumSha256, actual);
|
|
108
|
+
if (verdict === "mismatch") {
|
|
109
|
+
throw new CliError("checksum_mismatch", `The downloaded bytes do not match the published checksum for ${options.slug}.`, `The partial file was left at ${partPath}. Delete it and run the command again.`);
|
|
110
|
+
}
|
|
111
|
+
if (verdict === "match") {
|
|
112
|
+
output.detail("Checksum verified.");
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
output.warn("The published checksum could not be read, so the file is UNVERIFIED.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
output.warn("This package publishes no checksum, so the file is UNVERIFIED.");
|
|
120
|
+
}
|
|
121
|
+
await rename(partPath, finalPath);
|
|
122
|
+
await rm(sidecarPath, { force: true });
|
|
123
|
+
output.info(`Saved ${fileName}.`);
|
|
124
|
+
output.result(finalPath);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
function resolveDestination(out) {
|
|
128
|
+
if (out === null)
|
|
129
|
+
return process.cwd();
|
|
130
|
+
return isAbsolute(out) ? resolvePath(out) : resolvePath(process.cwd(), out);
|
|
131
|
+
}
|
|
132
|
+
async function ensureDirectory(directory) {
|
|
133
|
+
try {
|
|
134
|
+
await mkdir(directory, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
throw new CliError("storage_unwritable", `Could not create ${directory}: ${error.message}`, "Pass --out to a directory you can write to.");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function statOrNull(path) {
|
|
141
|
+
try {
|
|
142
|
+
const stats = await stat(path);
|
|
143
|
+
return { size: stats.size };
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error.code === "ENOENT")
|
|
147
|
+
return null;
|
|
148
|
+
throw new CliError("storage_unwritable", `Could not read ${path}: ${error.message}`, null);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function resolveSidecar(input) {
|
|
152
|
+
const fresh = {
|
|
153
|
+
format: SIDECAR_FORMAT,
|
|
154
|
+
slug: input.slug,
|
|
155
|
+
packageVersion: input.packageVersion,
|
|
156
|
+
sizeBytes: input.sizeBytes,
|
|
157
|
+
checksumSha256: input.checksumSha256,
|
|
158
|
+
etag: input.etag,
|
|
159
|
+
committedRanges: [],
|
|
160
|
+
updatedAt: new Date().toISOString(),
|
|
161
|
+
};
|
|
162
|
+
const raw = await readFileOrNull(input.sidecarPath);
|
|
163
|
+
const existing = raw === null ? null : parseSidecar(raw);
|
|
164
|
+
const partExists = (await statOrNull(input.partPath)) !== null;
|
|
165
|
+
if (existing === null) {
|
|
166
|
+
if (partExists) {
|
|
167
|
+
input.context.output.warn("The resume record for this download is missing or unreadable, so it starts over.");
|
|
168
|
+
}
|
|
169
|
+
await discardPartial(input.partPath, input.sidecarPath);
|
|
170
|
+
return fresh;
|
|
171
|
+
}
|
|
172
|
+
const mismatch = sidecarMismatch(existing, {
|
|
173
|
+
slug: input.slug,
|
|
174
|
+
packageVersion: input.packageVersion,
|
|
175
|
+
sizeBytes: input.sizeBytes,
|
|
176
|
+
etag: input.etag,
|
|
177
|
+
checksumSha256: input.checksumSha256,
|
|
178
|
+
});
|
|
179
|
+
if (mismatch !== null) {
|
|
180
|
+
input.context.output.warn(`Starting over because ${describeMismatch(mismatch)}. The partial file was discarded so two versions are never mixed.`);
|
|
181
|
+
await discardPartial(input.partPath, input.sidecarPath);
|
|
182
|
+
return fresh;
|
|
183
|
+
}
|
|
184
|
+
if (!partExists) {
|
|
185
|
+
await discardPartial(input.partPath, input.sidecarPath);
|
|
186
|
+
return fresh;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
...existing,
|
|
190
|
+
checksumSha256: existing.checksumSha256 ?? input.checksumSha256,
|
|
191
|
+
etag: existing.etag ?? input.etag,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function readFileOrNull(path) {
|
|
195
|
+
try {
|
|
196
|
+
const handle = await open(path, "r");
|
|
197
|
+
try {
|
|
198
|
+
return await handle.readFile("utf8");
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
await handle.close();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error.code === "ENOENT")
|
|
206
|
+
return null;
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function discardPartial(partPath, sidecarPath) {
|
|
211
|
+
await rm(partPath, { force: true });
|
|
212
|
+
await rm(sidecarPath, { force: true });
|
|
213
|
+
}
|
|
214
|
+
async function openPartFile(partPath, sizeBytes) {
|
|
215
|
+
try {
|
|
216
|
+
let handle;
|
|
217
|
+
try {
|
|
218
|
+
handle = await open(partPath, "r+");
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (error.code !== "ENOENT")
|
|
222
|
+
throw error;
|
|
223
|
+
handle = await open(partPath, "w+");
|
|
224
|
+
}
|
|
225
|
+
await handle.truncate(sizeBytes);
|
|
226
|
+
return handle;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
throw new CliError("storage_unwritable", `Could not open ${partPath} for writing: ${error.message}`, "Check permissions and free space on the destination, then try again.");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function writeSidecar(path, sidecar) {
|
|
233
|
+
const temp = `${path}.tmp`;
|
|
234
|
+
await writeFile(temp, serializeSidecar(sidecar), "utf8");
|
|
235
|
+
await rename(temp, path);
|
|
236
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export const DEFAULT_CONCURRENCY = 8;
|
|
2
|
+
export const MIN_CONCURRENCY = 1;
|
|
3
|
+
export const MAX_CONCURRENCY = 16;
|
|
4
|
+
export const MIN_CHUNK_BYTES = 1024 * 1024;
|
|
5
|
+
export const TARGET_CHUNK_BYTES = 32 * 1024 * 1024;
|
|
6
|
+
export function clampConcurrency(requested) {
|
|
7
|
+
if (requested === null || !Number.isFinite(requested))
|
|
8
|
+
return DEFAULT_CONCURRENCY;
|
|
9
|
+
const rounded = Math.trunc(requested);
|
|
10
|
+
if (rounded < MIN_CONCURRENCY)
|
|
11
|
+
return MIN_CONCURRENCY;
|
|
12
|
+
if (rounded > MAX_CONCURRENCY)
|
|
13
|
+
return MAX_CONCURRENCY;
|
|
14
|
+
return rounded;
|
|
15
|
+
}
|
|
16
|
+
export function rangeLength(range) {
|
|
17
|
+
return range.endInclusive - range.start + 1;
|
|
18
|
+
}
|
|
19
|
+
export function totalBytes(ranges) {
|
|
20
|
+
let sum = 0;
|
|
21
|
+
for (const range of ranges)
|
|
22
|
+
sum += rangeLength(range);
|
|
23
|
+
return sum;
|
|
24
|
+
}
|
|
25
|
+
export function chunkSizeFor(sizeBytes, concurrency) {
|
|
26
|
+
if (sizeBytes <= 0)
|
|
27
|
+
return MIN_CHUNK_BYTES;
|
|
28
|
+
const perWorker = Math.ceil(sizeBytes / Math.max(1, concurrency));
|
|
29
|
+
return Math.max(MIN_CHUNK_BYTES, Math.min(TARGET_CHUNK_BYTES, perWorker));
|
|
30
|
+
}
|
|
31
|
+
export function planChunks(sizeBytes, chunkBytes) {
|
|
32
|
+
if (sizeBytes <= 0)
|
|
33
|
+
return [];
|
|
34
|
+
const size = Math.max(1, Math.trunc(chunkBytes));
|
|
35
|
+
const chunks = [];
|
|
36
|
+
for (let start = 0; start < sizeBytes; start += size) {
|
|
37
|
+
chunks.push({
|
|
38
|
+
start,
|
|
39
|
+
endInclusive: Math.min(start + size, sizeBytes) - 1,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return chunks;
|
|
43
|
+
}
|
|
44
|
+
export function normalizeRanges(ranges, sizeBytes) {
|
|
45
|
+
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 0)
|
|
46
|
+
return null;
|
|
47
|
+
const sorted = [];
|
|
48
|
+
for (const range of ranges) {
|
|
49
|
+
if (!Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.endInclusive)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (range.start < 0 || range.endInclusive < range.start)
|
|
53
|
+
return null;
|
|
54
|
+
if (range.endInclusive > sizeBytes - 1)
|
|
55
|
+
return null;
|
|
56
|
+
sorted.push({ start: range.start, endInclusive: range.endInclusive });
|
|
57
|
+
}
|
|
58
|
+
sorted.sort((a, b) => a.start - b.start);
|
|
59
|
+
const merged = [];
|
|
60
|
+
for (const range of sorted) {
|
|
61
|
+
const last = merged[merged.length - 1];
|
|
62
|
+
if (last && range.start <= last.endInclusive + 1) {
|
|
63
|
+
last.endInclusive = Math.max(last.endInclusive, range.endInclusive);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
merged.push(range);
|
|
67
|
+
}
|
|
68
|
+
return merged;
|
|
69
|
+
}
|
|
70
|
+
export function missingRanges(sizeBytes, committed) {
|
|
71
|
+
if (sizeBytes <= 0)
|
|
72
|
+
return [];
|
|
73
|
+
const gaps = [];
|
|
74
|
+
let cursor = 0;
|
|
75
|
+
for (const range of committed) {
|
|
76
|
+
if (range.start > cursor) {
|
|
77
|
+
gaps.push({ start: cursor, endInclusive: range.start - 1 });
|
|
78
|
+
}
|
|
79
|
+
cursor = Math.max(cursor, range.endInclusive + 1);
|
|
80
|
+
}
|
|
81
|
+
if (cursor <= sizeBytes - 1) {
|
|
82
|
+
gaps.push({ start: cursor, endInclusive: sizeBytes - 1 });
|
|
83
|
+
}
|
|
84
|
+
return gaps;
|
|
85
|
+
}
|
|
86
|
+
export function splitRanges(ranges, chunkBytes) {
|
|
87
|
+
const size = Math.max(1, Math.trunc(chunkBytes));
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const range of ranges) {
|
|
90
|
+
for (let start = range.start; start <= range.endInclusive; start += size) {
|
|
91
|
+
out.push({
|
|
92
|
+
start,
|
|
93
|
+
endInclusive: Math.min(start + size - 1, range.endInclusive),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
export function planResume(input) {
|
|
100
|
+
const concurrency = clampConcurrency(input.concurrency);
|
|
101
|
+
const normalized = normalizeRanges(input.committed, input.sizeBytes);
|
|
102
|
+
const committed = normalized ?? [];
|
|
103
|
+
const gaps = missingRanges(input.sizeBytes, committed);
|
|
104
|
+
const chunkBytes = chunkSizeFor(input.sizeBytes, concurrency);
|
|
105
|
+
const completedBytes = totalBytes(committed);
|
|
106
|
+
return {
|
|
107
|
+
chunks: splitRanges(gaps, chunkBytes),
|
|
108
|
+
completedBytes,
|
|
109
|
+
remainingBytes: Math.max(0, input.sizeBytes - completedBytes),
|
|
110
|
+
restarted: normalized === null,
|
|
111
|
+
};
|
|
112
|
+
}
|
package/dist/redact.js
ADDED
package/dist/scopes.js
ADDED
package/dist/sidecar.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export const SIDECAR_FORMAT = 1;
|
|
2
|
+
export function serializeSidecar(sidecar) {
|
|
3
|
+
return `${JSON.stringify(sidecar, null, 2)}\n`;
|
|
4
|
+
}
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function readRanges(value) {
|
|
9
|
+
if (!Array.isArray(value))
|
|
10
|
+
return null;
|
|
11
|
+
const ranges = [];
|
|
12
|
+
for (const entry of value) {
|
|
13
|
+
if (!isRecord(entry))
|
|
14
|
+
return null;
|
|
15
|
+
const { start, endInclusive } = entry;
|
|
16
|
+
if (typeof start !== "number" || typeof endInclusive !== "number")
|
|
17
|
+
return null;
|
|
18
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(endInclusive))
|
|
19
|
+
return null;
|
|
20
|
+
ranges.push({ start, endInclusive });
|
|
21
|
+
}
|
|
22
|
+
return ranges;
|
|
23
|
+
}
|
|
24
|
+
export function parseSidecar(raw) {
|
|
25
|
+
let value;
|
|
26
|
+
try {
|
|
27
|
+
value = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (!isRecord(value))
|
|
33
|
+
return null;
|
|
34
|
+
if (value.format !== SIDECAR_FORMAT)
|
|
35
|
+
return null;
|
|
36
|
+
const { slug, packageVersion, sizeBytes, checksumSha256, etag, updatedAt } = value;
|
|
37
|
+
if (typeof slug !== "string" || slug.length === 0)
|
|
38
|
+
return null;
|
|
39
|
+
if (typeof packageVersion !== "number" || !Number.isSafeInteger(packageVersion))
|
|
40
|
+
return null;
|
|
41
|
+
if (typeof sizeBytes !== "number" || !Number.isSafeInteger(sizeBytes) || sizeBytes < 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (checksumSha256 !== null && typeof checksumSha256 !== "string")
|
|
45
|
+
return null;
|
|
46
|
+
if (etag !== null && typeof etag !== "string")
|
|
47
|
+
return null;
|
|
48
|
+
if (typeof updatedAt !== "string")
|
|
49
|
+
return null;
|
|
50
|
+
const committedRanges = readRanges(value.committedRanges);
|
|
51
|
+
if (committedRanges === null)
|
|
52
|
+
return null;
|
|
53
|
+
return {
|
|
54
|
+
format: SIDECAR_FORMAT,
|
|
55
|
+
slug,
|
|
56
|
+
packageVersion,
|
|
57
|
+
sizeBytes,
|
|
58
|
+
checksumSha256,
|
|
59
|
+
etag,
|
|
60
|
+
committedRanges,
|
|
61
|
+
updatedAt,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function normalizeEtag(etag) {
|
|
65
|
+
if (etag === null)
|
|
66
|
+
return null;
|
|
67
|
+
const trimmed = etag.trim().replace(/^W\//iu, "");
|
|
68
|
+
const unquoted = trimmed.replace(/^"([\s\S]*)"$/u, "$1");
|
|
69
|
+
return unquoted.length > 0 ? unquoted : null;
|
|
70
|
+
}
|
|
71
|
+
export function sidecarMismatch(sidecar, remote) {
|
|
72
|
+
if (sidecar.slug !== remote.slug)
|
|
73
|
+
return "slug";
|
|
74
|
+
if (sidecar.packageVersion !== remote.packageVersion)
|
|
75
|
+
return "version";
|
|
76
|
+
if (sidecar.sizeBytes !== remote.sizeBytes)
|
|
77
|
+
return "size";
|
|
78
|
+
const stored = normalizeEtag(sidecar.etag);
|
|
79
|
+
const current = normalizeEtag(remote.etag);
|
|
80
|
+
if (stored !== null && current !== null && stored !== current)
|
|
81
|
+
return "etag";
|
|
82
|
+
if (sidecar.checksumSha256 !== null &&
|
|
83
|
+
remote.checksumSha256 !== null &&
|
|
84
|
+
sidecar.checksumSha256.toLowerCase() !== remote.checksumSha256.toLowerCase()) {
|
|
85
|
+
return "checksum";
|
|
86
|
+
}
|
|
87
|
+
const identityProven = stored !== null && current !== null;
|
|
88
|
+
const verifiableLater = sidecar.checksumSha256 !== null || remote.checksumSha256 !== null;
|
|
89
|
+
if (!identityProven && !verifiableLater)
|
|
90
|
+
return "unverifiable";
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
export function describeMismatch(mismatch) {
|
|
94
|
+
switch (mismatch) {
|
|
95
|
+
case "slug":
|
|
96
|
+
return "the partial file belongs to a different package";
|
|
97
|
+
case "version":
|
|
98
|
+
return "a newer version of this package was published";
|
|
99
|
+
case "size":
|
|
100
|
+
return "the published artifact changed size";
|
|
101
|
+
case "etag":
|
|
102
|
+
return "the published artifact was replaced";
|
|
103
|
+
case "checksum":
|
|
104
|
+
return "the published checksum for this artifact changed";
|
|
105
|
+
case "unverifiable":
|
|
106
|
+
return "the partial file cannot be proved to belong to the artifact being served, and this package publishes no checksum that would catch it later";
|
|
107
|
+
}
|
|
108
|
+
}
|
package/dist/timers.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function sleep(ms) {
|
|
2
|
+
return new Promise((resolve) => {
|
|
3
|
+
setTimeout(resolve, ms);
|
|
4
|
+
});
|
|
5
|
+
}
|
|
6
|
+
export function unrefTimer(timer) {
|
|
7
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
8
|
+
const candidate = timer.unref;
|
|
9
|
+
if (typeof candidate === "function") {
|
|
10
|
+
candidate.call(timer);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const CLI_NAME = "runinfra";
|
|
2
|
+
export const CLI_PACKAGE = "@runinfra/cli";
|
|
3
|
+
export const CLI_VERSION = "0.1.0";
|
|
4
|
+
export const CLI_CLIENT_ID = "runinfra-cli";
|
|
5
|
+
export const MINIMUM_NODE_MAJOR = 20;
|
|
6
|
+
export function isSupportedNodeVersion(version) {
|
|
7
|
+
const major = Number.parseInt(version.split(".")[0] ?? "", 10);
|
|
8
|
+
if (!Number.isFinite(major))
|
|
9
|
+
return false;
|
|
10
|
+
return major >= MINIMUM_NODE_MAJOR;
|
|
11
|
+
}
|
|
12
|
+
export function userAgent(nodeVersion = process.versions.node, platform = process.platform) {
|
|
13
|
+
return `${CLI_PACKAGE}/${CLI_VERSION} (node/${nodeVersion}; ${platform})`;
|
|
14
|
+
}
|
package/dist/whoami.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { redact } from "./redact.js";
|
|
2
|
+
import { credentialsExpired } from "./credentials.js";
|
|
3
|
+
import { formatDuration } from "./progress.js";
|
|
4
|
+
import { requireCredentials } from "./context.js";
|
|
5
|
+
export async function whoami(context) {
|
|
6
|
+
const { credentials } = await requireCredentials(context);
|
|
7
|
+
const expiresAtMs = Date.parse(credentials.expiresAt);
|
|
8
|
+
const remainingSeconds = Math.max(0, Math.round((expiresAtMs - Date.now()) / 1000));
|
|
9
|
+
const rows = [
|
|
10
|
+
["Workspace", credentials.workspaceId],
|
|
11
|
+
["Key", `${credentials.keyPrefix} (${redact(credentials.apiKey)})`],
|
|
12
|
+
["Grants", credentials.scope.length > 0 ? credentials.scope : "none"],
|
|
13
|
+
["Endpoint", credentials.apiBase],
|
|
14
|
+
["Connected", credentials.createdAt],
|
|
15
|
+
[
|
|
16
|
+
"Expires",
|
|
17
|
+
credentialsExpired(credentials)
|
|
18
|
+
? `${credentials.expiresAt} (expired)`
|
|
19
|
+
: `${credentials.expiresAt} (in ${formatDuration(remainingSeconds)})`,
|
|
20
|
+
],
|
|
21
|
+
];
|
|
22
|
+
const width = Math.max(...rows.map(([label]) => label.length));
|
|
23
|
+
for (const [label, value] of rows) {
|
|
24
|
+
context.output.result(`${label.padEnd(width)} ${value}`);
|
|
25
|
+
}
|
|
26
|
+
context.output.info("This is what this machine has stored. Access may have been revoked from Settings, API keys since it was granted.");
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@runinfra/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "RunInfra CLI: browser-approved sign-in and resumable downloads for optimized model packages",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"homepage": "https://runinfra.ai/optimized-models",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://runinfra.ai/contact"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"runinfra",
|
|
12
|
+
"cli",
|
|
13
|
+
"oauth",
|
|
14
|
+
"pkce",
|
|
15
|
+
"model",
|
|
16
|
+
"download",
|
|
17
|
+
"resumable"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": {
|
|
21
|
+
"runinfra": "./bin/runinfra.mjs"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "node ../node_modules/typescript/bin/tsc -p tsconfig.json",
|
|
29
|
+
"prepack": "node ../node_modules/typescript/bin/tsc -p tsconfig.json",
|
|
30
|
+
"test": "node ../node_modules/vitest/vitest.mjs run cli/src --root .."
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin",
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"package.json"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"sideEffects": false
|
|
43
|
+
}
|