@refract9/cli 0.0.0-stage → 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/README.md +9 -2
- package/dist/main.js +874 -0
- package/package.json +30 -4
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @refract9/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line client for the refract9 HTTP API. Requires Node.js 24.15 or newer.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @refract9/cli
|
|
7
|
+
refract9 help
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`REFRACT9_API_KEY` is required for API commands. `REFRACT9_API_URL` defaults to `http://localhost:3000`.
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/main.ts
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { completeMultipartUpload, createMultipartUpload, uploadPart } from "@vercel/blob/client";
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
var defaultApiUrl = "http://localhost:3000";
|
|
12
|
+
var missingKey = "Set REFRACT9_API_KEY to call the refract9 API.";
|
|
13
|
+
function readClientConfig(env) {
|
|
14
|
+
const apiKey = env.REFRACT9_API_KEY?.trim() ?? "";
|
|
15
|
+
if (apiKey.length === 0) return { ok: false, message: missingKey };
|
|
16
|
+
const configured = env.REFRACT9_API_URL?.trim() ?? "";
|
|
17
|
+
const apiUrl = (configured.length === 0 ? defaultApiUrl : configured).replace(/\/+$/, "");
|
|
18
|
+
return { ok: true, config: { apiKey, apiUrl } };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/help.ts
|
|
22
|
+
function helpText(topic) {
|
|
23
|
+
if (topic === "entry") return entryHelp;
|
|
24
|
+
if (topic === "file") return fileHelp;
|
|
25
|
+
return overviewHelp;
|
|
26
|
+
}
|
|
27
|
+
var overviewHelp = `refract9 \u2014 command-line client for the refract9 API
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
refract9 <command> [arguments]
|
|
31
|
+
refract9 help [topic]
|
|
32
|
+
refract9 <command> --help
|
|
33
|
+
|
|
34
|
+
Topics:
|
|
35
|
+
entry Create, update, delete, and list entries
|
|
36
|
+
file Upload and remove private entry files
|
|
37
|
+
|
|
38
|
+
Environment:
|
|
39
|
+
REFRACT9_API_KEY Bearer token. Required for every command except help.
|
|
40
|
+
REFRACT9_API_URL API origin. Default: http://localhost:3000
|
|
41
|
+
|
|
42
|
+
Commands:
|
|
43
|
+
entry create --manifest <json>
|
|
44
|
+
entry update <id> --manifest <json>
|
|
45
|
+
entry delete <id>
|
|
46
|
+
entry list
|
|
47
|
+
entry file put <id> --path <path> <file>
|
|
48
|
+
entry file rm <id> <path>
|
|
49
|
+
|
|
50
|
+
Run \`refract9 help entry\` or \`refract9 help file\` for details.
|
|
51
|
+
\`refract9 help file\` describes the 100 MB limit, retries, and resume.`;
|
|
52
|
+
var entryHelp = `Entry commands manage entry metadata.
|
|
53
|
+
|
|
54
|
+
Usage:
|
|
55
|
+
refract9 entry create --manifest <json>
|
|
56
|
+
refract9 entry update <id> --manifest <json>
|
|
57
|
+
refract9 entry delete <id>
|
|
58
|
+
refract9 entry list
|
|
59
|
+
|
|
60
|
+
--manifest <json> Manifest object as JSON. Extra fields are sent as given.
|
|
61
|
+
|
|
62
|
+
Create stores a new entry. Update replaces the manifest of one owned entry.
|
|
63
|
+
Delete removes that entry and its private files. List prints the owned entries,
|
|
64
|
+
newest first.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
refract9 entry create --manifest '{"type":"markdown","version":1,"path":"index.md"}'`;
|
|
68
|
+
var fileHelp = `File commands upload and remove private entry files.
|
|
69
|
+
|
|
70
|
+
Usage:
|
|
71
|
+
refract9 entry file put <id> --path <path> <file>
|
|
72
|
+
refract9 entry file rm <id> <path>
|
|
73
|
+
|
|
74
|
+
--path <path> Entry path stored with the file, such as notes/a.md.
|
|
75
|
+
<file> Local file to upload.
|
|
76
|
+
|
|
77
|
+
Limit:
|
|
78
|
+
A single file must be 100 MB (104857600 bytes) or smaller.
|
|
79
|
+
|
|
80
|
+
Retry:
|
|
81
|
+
Each token request, part upload, completion, registration, and file removal
|
|
82
|
+
tries 5 times. The wait is 200 ms and doubles after each failure.
|
|
83
|
+
Network errors and HTTP 408, 429, 500, 502, 503, and 504 are retried.
|
|
84
|
+
HTTP 400, 401, 403, and 404 stop immediately.
|
|
85
|
+
|
|
86
|
+
Resume:
|
|
87
|
+
entry file put uploads the file in 8 MB (8388608 bytes) parts.
|
|
88
|
+
The last part can be smaller. After each part, progress is written to
|
|
89
|
+
~/.refract9/uploads/<id>.json. The id is a hash of the API URL, the entry
|
|
90
|
+
id, the entry path, and the local file argument.
|
|
91
|
+
Run the same command to resume after the last completed part.
|
|
92
|
+
Registration is saved separately, so a finished upload is not sent again.
|
|
93
|
+
A file whose size or modification time changed starts a new upload.
|
|
94
|
+
Delete that progress file to discard a partial upload and start over.
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
REFRACT9_API_KEY=rf9_... refract9 entry file put entry-id --path notes/a.md ./note.md
|
|
98
|
+
REFRACT9_API_KEY=rf9_... refract9 entry file rm entry-id notes/a.md`;
|
|
99
|
+
|
|
100
|
+
// src/parse.ts
|
|
101
|
+
var usage = `Usage:
|
|
102
|
+
refract9 help
|
|
103
|
+
refract9 entry create --manifest <json>
|
|
104
|
+
refract9 entry update <id> --manifest <json>
|
|
105
|
+
refract9 entry delete <id>
|
|
106
|
+
refract9 entry list
|
|
107
|
+
refract9 entry file put <id> --path <path> <file>
|
|
108
|
+
refract9 entry file rm <id> <path>
|
|
109
|
+
Run \`refract9 help\` for the manual.`;
|
|
110
|
+
function fail(detail) {
|
|
111
|
+
return { ok: false, message: `${detail}
|
|
112
|
+
${usage}` };
|
|
113
|
+
}
|
|
114
|
+
function succeed(command) {
|
|
115
|
+
return { ok: true, command };
|
|
116
|
+
}
|
|
117
|
+
function helpTopic(argv) {
|
|
118
|
+
const asked = argv.length === 0 || argv[0] === "help" || argv.includes("--help") || argv.includes("-h");
|
|
119
|
+
if (!asked) return null;
|
|
120
|
+
const args = argv.filter((arg) => arg !== "--help" && arg !== "-h");
|
|
121
|
+
const rest = args[0] === "help" ? args.slice(1) : args;
|
|
122
|
+
if (rest.length === 0) return "overview";
|
|
123
|
+
if (rest[0] === "file" && rest.length === 1) return "file";
|
|
124
|
+
if (rest[0] !== "entry") return "unknown";
|
|
125
|
+
if (rest.length === 1) return "entry";
|
|
126
|
+
if (rest[1] === "file") {
|
|
127
|
+
if (rest.length === 2) return "file";
|
|
128
|
+
if (rest.length === 3 && (rest[2] === "put" || rest[2] === "rm")) return "file";
|
|
129
|
+
return "unknown";
|
|
130
|
+
}
|
|
131
|
+
if (rest.length === 2 && ["create", "update", "delete", "list"].includes(rest[1] ?? "")) {
|
|
132
|
+
return "entry";
|
|
133
|
+
}
|
|
134
|
+
return "unknown";
|
|
135
|
+
}
|
|
136
|
+
function scan(args, knownFlags) {
|
|
137
|
+
const flags = /* @__PURE__ */ new Map();
|
|
138
|
+
const positionals = [];
|
|
139
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
140
|
+
const token = args[index] ?? "";
|
|
141
|
+
if (token.startsWith("-")) {
|
|
142
|
+
if (!knownFlags.has(token)) return { ok: false, message: `Unexpected argument: ${token}.` };
|
|
143
|
+
const value = args[index + 1];
|
|
144
|
+
if (value === void 0 || value.startsWith("-")) {
|
|
145
|
+
return { ok: false, message: `Missing value for ${token}.` };
|
|
146
|
+
}
|
|
147
|
+
if (flags.has(token)) return { ok: false, message: `Duplicate ${token}.` };
|
|
148
|
+
flags.set(token, value);
|
|
149
|
+
index += 1;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
positionals.push(token);
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, flags, positionals };
|
|
155
|
+
}
|
|
156
|
+
function parsedManifest(raw) {
|
|
157
|
+
try {
|
|
158
|
+
return { ok: true, manifest: JSON.parse(raw) };
|
|
159
|
+
} catch {
|
|
160
|
+
return { ok: false, message: "Manifest must be JSON." };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function unexpected(positionals, count) {
|
|
164
|
+
const extra = positionals[count];
|
|
165
|
+
return extra === void 0 ? null : `Unexpected argument: ${extra}.`;
|
|
166
|
+
}
|
|
167
|
+
function parseList(args) {
|
|
168
|
+
const scanned = scan(args, /* @__PURE__ */ new Set());
|
|
169
|
+
if (!scanned.ok) return fail(scanned.message);
|
|
170
|
+
const extra = unexpected(scanned.positionals, 0);
|
|
171
|
+
if (extra) return fail(extra);
|
|
172
|
+
return succeed({ kind: "entry-list" });
|
|
173
|
+
}
|
|
174
|
+
function parseCreate(args) {
|
|
175
|
+
const scanned = scan(args, /* @__PURE__ */ new Set(["--manifest"]));
|
|
176
|
+
if (!scanned.ok) return fail(scanned.message);
|
|
177
|
+
const raw = scanned.flags.get("--manifest");
|
|
178
|
+
if (raw === void 0) return fail("Missing --manifest.");
|
|
179
|
+
const manifest = parsedManifest(raw);
|
|
180
|
+
if (!manifest.ok) return fail(manifest.message);
|
|
181
|
+
const extra = unexpected(scanned.positionals, 0);
|
|
182
|
+
if (extra) return fail(extra);
|
|
183
|
+
return succeed({ kind: "entry-create", manifest: manifest.manifest });
|
|
184
|
+
}
|
|
185
|
+
function parseUpdate(args) {
|
|
186
|
+
const scanned = scan(args, /* @__PURE__ */ new Set(["--manifest"]));
|
|
187
|
+
if (!scanned.ok) return fail(scanned.message);
|
|
188
|
+
const raw = scanned.flags.get("--manifest");
|
|
189
|
+
if (raw === void 0) return fail("Missing --manifest.");
|
|
190
|
+
const manifest = parsedManifest(raw);
|
|
191
|
+
if (!manifest.ok) return fail(manifest.message);
|
|
192
|
+
const id = scanned.positionals[0];
|
|
193
|
+
if (id === void 0) return fail("Missing entry id.");
|
|
194
|
+
const extra = unexpected(scanned.positionals, 1);
|
|
195
|
+
if (extra) return fail(extra);
|
|
196
|
+
return succeed({ kind: "entry-update", id, manifest: manifest.manifest });
|
|
197
|
+
}
|
|
198
|
+
function parseDelete(args) {
|
|
199
|
+
const scanned = scan(args, /* @__PURE__ */ new Set());
|
|
200
|
+
if (!scanned.ok) return fail(scanned.message);
|
|
201
|
+
const id = scanned.positionals[0];
|
|
202
|
+
if (id === void 0) return fail("Missing entry id.");
|
|
203
|
+
const extra = unexpected(scanned.positionals, 1);
|
|
204
|
+
if (extra) return fail(extra);
|
|
205
|
+
return succeed({ kind: "entry-delete", id });
|
|
206
|
+
}
|
|
207
|
+
function parseFilePut(args) {
|
|
208
|
+
const scanned = scan(args, /* @__PURE__ */ new Set(["--path"]));
|
|
209
|
+
if (!scanned.ok) return fail(scanned.message);
|
|
210
|
+
const path = scanned.flags.get("--path");
|
|
211
|
+
if (path === void 0) return fail("Missing --path.");
|
|
212
|
+
const id = scanned.positionals[0];
|
|
213
|
+
if (id === void 0) return fail("Missing entry id.");
|
|
214
|
+
const file = scanned.positionals[1];
|
|
215
|
+
if (file === void 0) return fail("Missing file.");
|
|
216
|
+
const extra = unexpected(scanned.positionals, 2);
|
|
217
|
+
if (extra) return fail(extra);
|
|
218
|
+
return succeed({ kind: "file-put", id, path, file });
|
|
219
|
+
}
|
|
220
|
+
function parseFileRm(args) {
|
|
221
|
+
const scanned = scan(args, /* @__PURE__ */ new Set());
|
|
222
|
+
if (!scanned.ok) return fail(scanned.message);
|
|
223
|
+
const id = scanned.positionals[0];
|
|
224
|
+
if (id === void 0) return fail("Missing entry id.");
|
|
225
|
+
const path = scanned.positionals[1];
|
|
226
|
+
if (path === void 0) return fail("Missing path.");
|
|
227
|
+
const extra = unexpected(scanned.positionals, 2);
|
|
228
|
+
if (extra) return fail(extra);
|
|
229
|
+
return succeed({ kind: "file-rm", id, path });
|
|
230
|
+
}
|
|
231
|
+
function parseFile(args) {
|
|
232
|
+
const action = args[0];
|
|
233
|
+
const rest = args.slice(1);
|
|
234
|
+
if (action === "put") return parseFilePut(rest);
|
|
235
|
+
if (action === "rm") return parseFileRm(rest);
|
|
236
|
+
return fail("Unknown command.");
|
|
237
|
+
}
|
|
238
|
+
function parseArgv(argv) {
|
|
239
|
+
const topic = helpTopic(argv);
|
|
240
|
+
if (topic === "overview" || topic === "entry" || topic === "file") {
|
|
241
|
+
const explicit = argv.length > 0 && (argv[0] === "help" || argv.includes("--help") || argv.includes("-h"));
|
|
242
|
+
const text = helpText(topic);
|
|
243
|
+
if (!explicit) return { ok: false, message: text };
|
|
244
|
+
return { ok: true, help: text };
|
|
245
|
+
}
|
|
246
|
+
if (topic === "unknown") return fail("Unknown help topic. Topics: entry, file.");
|
|
247
|
+
if (argv[0] !== "entry") return fail("Unknown command.");
|
|
248
|
+
const action = argv[1];
|
|
249
|
+
const args = argv.slice(2);
|
|
250
|
+
if (action === "list") return parseList(args);
|
|
251
|
+
if (action === "create") return parseCreate(args);
|
|
252
|
+
if (action === "update") return parseUpdate(args);
|
|
253
|
+
if (action === "delete") return parseDelete(args);
|
|
254
|
+
if (action === "file") return parseFile(args);
|
|
255
|
+
return fail("Unknown command.");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/plan.ts
|
|
259
|
+
function authHeader(apiKey) {
|
|
260
|
+
return { Authorization: `Bearer ${apiKey}` };
|
|
261
|
+
}
|
|
262
|
+
function jsonHeaders(apiKey) {
|
|
263
|
+
return {
|
|
264
|
+
Authorization: `Bearer ${apiKey}`,
|
|
265
|
+
"Content-Type": "application/json"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function entryUrl(apiUrl, id) {
|
|
269
|
+
const base = `${apiUrl}/api/v1/entries`;
|
|
270
|
+
return id === void 0 ? base : `${base}/${encodeURIComponent(id)}`;
|
|
271
|
+
}
|
|
272
|
+
function planCommand(config, command, nonce) {
|
|
273
|
+
switch (command.kind) {
|
|
274
|
+
case "entry-list":
|
|
275
|
+
return {
|
|
276
|
+
kind: "http",
|
|
277
|
+
request: {
|
|
278
|
+
method: "GET",
|
|
279
|
+
url: entryUrl(config.apiUrl),
|
|
280
|
+
headers: authHeader(config.apiKey)
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
case "entry-create":
|
|
284
|
+
return {
|
|
285
|
+
kind: "http",
|
|
286
|
+
request: {
|
|
287
|
+
method: "POST",
|
|
288
|
+
url: entryUrl(config.apiUrl),
|
|
289
|
+
headers: jsonHeaders(config.apiKey),
|
|
290
|
+
body: JSON.stringify({ manifest: command.manifest })
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
case "entry-update":
|
|
294
|
+
return {
|
|
295
|
+
kind: "http",
|
|
296
|
+
request: {
|
|
297
|
+
method: "PATCH",
|
|
298
|
+
url: entryUrl(config.apiUrl, command.id),
|
|
299
|
+
headers: jsonHeaders(config.apiKey),
|
|
300
|
+
body: JSON.stringify({ manifest: command.manifest })
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
case "entry-delete":
|
|
304
|
+
return {
|
|
305
|
+
kind: "http",
|
|
306
|
+
request: {
|
|
307
|
+
method: "DELETE",
|
|
308
|
+
url: entryUrl(config.apiUrl, command.id),
|
|
309
|
+
headers: authHeader(config.apiKey)
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
case "file-rm":
|
|
313
|
+
return {
|
|
314
|
+
kind: "http",
|
|
315
|
+
request: {
|
|
316
|
+
method: "DELETE",
|
|
317
|
+
url: `${entryUrl(config.apiUrl, command.id)}/files`,
|
|
318
|
+
headers: jsonHeaders(config.apiKey),
|
|
319
|
+
body: JSON.stringify({ path: command.path })
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
case "file-put": {
|
|
323
|
+
const pathname = `entries/${command.id}/${nonce}/${command.path}`;
|
|
324
|
+
return {
|
|
325
|
+
kind: "file-put",
|
|
326
|
+
pathname,
|
|
327
|
+
access: "private",
|
|
328
|
+
handleUploadUrl: `${entryUrl(config.apiUrl, command.id)}/blob`,
|
|
329
|
+
clientPayload: JSON.stringify({ path: command.path }),
|
|
330
|
+
headers: authHeader(config.apiKey),
|
|
331
|
+
register: {
|
|
332
|
+
method: "POST",
|
|
333
|
+
url: `${entryUrl(config.apiUrl, command.id)}/files`,
|
|
334
|
+
headers: jsonHeaders(config.apiKey),
|
|
335
|
+
body: JSON.stringify({ path: command.path, pathname })
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/transfer.ts
|
|
343
|
+
import { createHash } from "node:crypto";
|
|
344
|
+
import { join } from "node:path";
|
|
345
|
+
var maximumFileBytes = 100 * 1024 * 1024;
|
|
346
|
+
var partSizeBytes = 8 * 1024 * 1024;
|
|
347
|
+
var transferAttempts = 5;
|
|
348
|
+
var TransferFailure = class extends Error {
|
|
349
|
+
status;
|
|
350
|
+
attempts;
|
|
351
|
+
constructor(message, status, attempts = 1) {
|
|
352
|
+
super(message);
|
|
353
|
+
this.name = "TransferFailure";
|
|
354
|
+
this.status = status;
|
|
355
|
+
this.attempts = attempts;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
function fileSizeError(size) {
|
|
359
|
+
if (size <= maximumFileBytes) return null;
|
|
360
|
+
return `File is ${size} bytes. A single file must be 100 MB (${maximumFileBytes} bytes) or smaller.`;
|
|
361
|
+
}
|
|
362
|
+
function planParts(size, partSize = partSizeBytes) {
|
|
363
|
+
if (size === 0) return [{ partNumber: 1, start: 0, end: 0 }];
|
|
364
|
+
const parts = [];
|
|
365
|
+
let start = 0;
|
|
366
|
+
let partNumber = 1;
|
|
367
|
+
while (start < size) {
|
|
368
|
+
const end = Math.min(start + partSize, size);
|
|
369
|
+
parts.push({ partNumber, start, end });
|
|
370
|
+
start = end;
|
|
371
|
+
partNumber += 1;
|
|
372
|
+
}
|
|
373
|
+
return parts;
|
|
374
|
+
}
|
|
375
|
+
function retryDelayMs(attempt) {
|
|
376
|
+
return 200 * 2 ** (attempt - 1);
|
|
377
|
+
}
|
|
378
|
+
function checkpointId(input) {
|
|
379
|
+
return createHash("sha256").update(JSON.stringify([input.apiUrl, input.entryId, input.path, input.file])).digest("hex");
|
|
380
|
+
}
|
|
381
|
+
function checkpointFilePath(home, id) {
|
|
382
|
+
if (!/^[a-f0-9]{64}$/.test(id)) throw new Error("Invalid checkpoint id.");
|
|
383
|
+
return join(home, ".refract9", "uploads", `${id}.json`);
|
|
384
|
+
}
|
|
385
|
+
function parseCheckpoint(raw) {
|
|
386
|
+
let value;
|
|
387
|
+
try {
|
|
388
|
+
value = JSON.parse(raw);
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
if (!isCheckpoint(value)) return null;
|
|
393
|
+
return value;
|
|
394
|
+
}
|
|
395
|
+
async function putFile(config, command, io) {
|
|
396
|
+
const notes = [];
|
|
397
|
+
let progressPath = null;
|
|
398
|
+
try {
|
|
399
|
+
const info = await io.stat(command.file);
|
|
400
|
+
const sizeError = fileSizeError(info.size);
|
|
401
|
+
if (sizeError) return { exitCode: 1, stdout: "", stderr: withNewline(sizeError) };
|
|
402
|
+
const id = checkpointId({
|
|
403
|
+
apiUrl: config.apiUrl,
|
|
404
|
+
entryId: command.id,
|
|
405
|
+
path: command.path,
|
|
406
|
+
file: command.file
|
|
407
|
+
});
|
|
408
|
+
const save = async (checkpoint2) => {
|
|
409
|
+
await io.writeCheckpoint(id, `${JSON.stringify(checkpoint2, null, 2)}
|
|
410
|
+
`);
|
|
411
|
+
progressPath = io.checkpointPath(id);
|
|
412
|
+
};
|
|
413
|
+
let checkpoint = await loadCheckpoint(io, id, config, command, info, notes);
|
|
414
|
+
let sessionToken = null;
|
|
415
|
+
if (checkpoint) {
|
|
416
|
+
progressPath = io.checkpointPath(id);
|
|
417
|
+
} else {
|
|
418
|
+
const opened = await openUpload(io, config, command, info, notes, save);
|
|
419
|
+
checkpoint = opened.checkpoint;
|
|
420
|
+
sessionToken = opened.token;
|
|
421
|
+
}
|
|
422
|
+
const resumedRegistration = checkpoint.phase === "registering";
|
|
423
|
+
if (!resumedRegistration) {
|
|
424
|
+
const step = await uploadParts(io, config, command, checkpoint, sessionToken, notes, save);
|
|
425
|
+
if (step.done) return { exitCode: 0, stdout: step.stdout, stderr: noted(notes) };
|
|
426
|
+
checkpoint = step.checkpoint;
|
|
427
|
+
}
|
|
428
|
+
if (resumedRegistration) notes.push("Resuming file registration.");
|
|
429
|
+
const plan = filePlan(config, command, checkpoint.nonce);
|
|
430
|
+
const pathname = checkpoint.registerPathname ?? checkpoint.pathname;
|
|
431
|
+
const stdout = await registerFile(io, plan.register, command.path, pathname, (attempt) => {
|
|
432
|
+
notes.push(`Retrying file registration (attempt ${attempt + 1} of ${transferAttempts}).`);
|
|
433
|
+
});
|
|
434
|
+
await io.removeCheckpoint(id);
|
|
435
|
+
return { exitCode: 0, stdout, stderr: noted(notes) };
|
|
436
|
+
} catch (error) {
|
|
437
|
+
return failureResult(error, progressPath);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function sendWithRetry(io, request) {
|
|
441
|
+
const notes = [];
|
|
442
|
+
try {
|
|
443
|
+
const response = await withRetry(
|
|
444
|
+
io,
|
|
445
|
+
async () => {
|
|
446
|
+
const result = await io.fetch(request.url, {
|
|
447
|
+
method: request.method,
|
|
448
|
+
headers: request.headers,
|
|
449
|
+
...request.body === void 0 ? {} : { body: request.body }
|
|
450
|
+
});
|
|
451
|
+
if (result.status >= 400) {
|
|
452
|
+
const message = result.text.length > 0 ? result.text : `Request failed (${result.status}).`;
|
|
453
|
+
throw new TransferFailure(message, result.status);
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
},
|
|
457
|
+
(attempt) => {
|
|
458
|
+
notes.push(`Retrying request (attempt ${attempt + 1} of ${transferAttempts}).`);
|
|
459
|
+
}
|
|
460
|
+
);
|
|
461
|
+
return {
|
|
462
|
+
exitCode: 0,
|
|
463
|
+
stdout: response.text.length > 0 ? withNewline(response.text) : "",
|
|
464
|
+
stderr: noted(notes)
|
|
465
|
+
};
|
|
466
|
+
} catch (error) {
|
|
467
|
+
return failureResult(error, null);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async function loadCheckpoint(io, id, config, command, info, notes) {
|
|
471
|
+
const raw = await io.readCheckpoint(id);
|
|
472
|
+
if (raw === null) return null;
|
|
473
|
+
const parsed2 = parseCheckpoint(raw);
|
|
474
|
+
if (!parsed2) {
|
|
475
|
+
await io.removeCheckpoint(id);
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
if (!sameTarget(parsed2, config, command, info)) {
|
|
479
|
+
notes.push("Local file changed. Starting a new upload.");
|
|
480
|
+
await io.removeCheckpoint(id);
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
return parsed2;
|
|
484
|
+
}
|
|
485
|
+
async function openUpload(io, config, command, info, notes, save) {
|
|
486
|
+
const nonce = io.nonce();
|
|
487
|
+
const plan = filePlan(config, command, nonce);
|
|
488
|
+
const token = await issueToken(io, plan, (attempt) => {
|
|
489
|
+
notes.push(`Retrying upload (attempt ${attempt + 1} of ${transferAttempts}).`);
|
|
490
|
+
});
|
|
491
|
+
const opened = await withRetry(
|
|
492
|
+
io,
|
|
493
|
+
() => io.createUpload(plan.pathname, token),
|
|
494
|
+
(attempt) => {
|
|
495
|
+
notes.push(`Retrying upload (attempt ${attempt + 1} of ${transferAttempts}).`);
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
const checkpoint = {
|
|
499
|
+
version: 1,
|
|
500
|
+
apiUrl: config.apiUrl,
|
|
501
|
+
entryId: command.id,
|
|
502
|
+
path: command.path,
|
|
503
|
+
file: command.file,
|
|
504
|
+
size: info.size,
|
|
505
|
+
mtimeMs: info.mtimeMs,
|
|
506
|
+
nonce,
|
|
507
|
+
pathname: plan.pathname,
|
|
508
|
+
uploadId: opened.uploadId,
|
|
509
|
+
key: opened.key,
|
|
510
|
+
parts: [],
|
|
511
|
+
phase: "uploading",
|
|
512
|
+
registerPathname: null
|
|
513
|
+
};
|
|
514
|
+
await save(checkpoint);
|
|
515
|
+
return { checkpoint, token };
|
|
516
|
+
}
|
|
517
|
+
async function uploadParts(io, config, command, checkpoint, sessionToken, notes, save) {
|
|
518
|
+
const plan = filePlan(config, command, checkpoint.nonce);
|
|
519
|
+
const token = sessionToken ?? await issueToken(io, plan, (attempt) => {
|
|
520
|
+
notes.push(`Retrying upload (attempt ${attempt + 1} of ${transferAttempts}).`);
|
|
521
|
+
});
|
|
522
|
+
const planned = planParts(checkpoint.size);
|
|
523
|
+
const done = new Set(checkpoint.parts.map((part) => part.partNumber));
|
|
524
|
+
const pending = planned.filter((part) => !done.has(part.partNumber));
|
|
525
|
+
if (done.size > 0) {
|
|
526
|
+
const next = pending[0];
|
|
527
|
+
notes.push(
|
|
528
|
+
next ? `Resuming at part ${next.partNumber} of ${planned.length}.` : "Resuming upload completion."
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
let current = checkpoint;
|
|
532
|
+
for (const part of pending) {
|
|
533
|
+
const uploaded = await withRetry(
|
|
534
|
+
io,
|
|
535
|
+
async () => {
|
|
536
|
+
const body = await io.readRange(command.file, part.start, part.end);
|
|
537
|
+
return io.uploadPart({
|
|
538
|
+
pathname: current.pathname,
|
|
539
|
+
token,
|
|
540
|
+
key: current.key,
|
|
541
|
+
uploadId: current.uploadId,
|
|
542
|
+
partNumber: part.partNumber,
|
|
543
|
+
body
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
(attempt) => {
|
|
547
|
+
notes.push(
|
|
548
|
+
`Retrying part ${part.partNumber} (attempt ${attempt + 1} of ${transferAttempts}).`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
current = {
|
|
553
|
+
...current,
|
|
554
|
+
parts: [...current.parts, { partNumber: uploaded.partNumber, etag: uploaded.etag }]
|
|
555
|
+
};
|
|
556
|
+
await save(current);
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
const completed = await withRetry(
|
|
560
|
+
io,
|
|
561
|
+
() => io.completeUpload({
|
|
562
|
+
pathname: current.pathname,
|
|
563
|
+
token,
|
|
564
|
+
key: current.key,
|
|
565
|
+
uploadId: current.uploadId,
|
|
566
|
+
parts: [...current.parts].sort((left, right) => left.partNumber - right.partNumber)
|
|
567
|
+
}),
|
|
568
|
+
(attempt) => {
|
|
569
|
+
notes.push(`Retrying upload (attempt ${attempt + 1} of ${transferAttempts}).`);
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
current = { ...current, phase: "registering", registerPathname: completed.pathname };
|
|
573
|
+
await save(current);
|
|
574
|
+
return { done: false, checkpoint: current };
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (!isPermanent(error)) throw error;
|
|
577
|
+
try {
|
|
578
|
+
const stdout = await registerFile(
|
|
579
|
+
io,
|
|
580
|
+
plan.register,
|
|
581
|
+
command.path,
|
|
582
|
+
current.pathname,
|
|
583
|
+
() => {
|
|
584
|
+
}
|
|
585
|
+
);
|
|
586
|
+
await io.removeCheckpoint(
|
|
587
|
+
checkpointId({
|
|
588
|
+
apiUrl: config.apiUrl,
|
|
589
|
+
entryId: command.id,
|
|
590
|
+
path: command.path,
|
|
591
|
+
file: command.file
|
|
592
|
+
})
|
|
593
|
+
);
|
|
594
|
+
return { done: true, stdout };
|
|
595
|
+
} catch {
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async function issueToken(io, plan, onRetry) {
|
|
601
|
+
const body = JSON.stringify({
|
|
602
|
+
type: "blob.generate-client-token",
|
|
603
|
+
payload: {
|
|
604
|
+
pathname: plan.pathname,
|
|
605
|
+
clientPayload: plan.clientPayload,
|
|
606
|
+
multipart: true
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
const text = await withRetry(
|
|
610
|
+
io,
|
|
611
|
+
async () => {
|
|
612
|
+
const result = await io.fetch(plan.handleUploadUrl, {
|
|
613
|
+
method: "POST",
|
|
614
|
+
headers: { ...plan.headers, "Content-Type": "application/json" },
|
|
615
|
+
body
|
|
616
|
+
});
|
|
617
|
+
if (result.status >= 400) {
|
|
618
|
+
const message = result.text.length > 0 ? result.text : `Request failed (${result.status}).`;
|
|
619
|
+
throw new TransferFailure(message, result.status);
|
|
620
|
+
}
|
|
621
|
+
return clientToken(result.text);
|
|
622
|
+
},
|
|
623
|
+
onRetry
|
|
624
|
+
);
|
|
625
|
+
return text;
|
|
626
|
+
}
|
|
627
|
+
function clientToken(text) {
|
|
628
|
+
let parsed2;
|
|
629
|
+
try {
|
|
630
|
+
parsed2 = JSON.parse(text);
|
|
631
|
+
} catch {
|
|
632
|
+
throw new TransferFailure("Client token response was invalid.", 502);
|
|
633
|
+
}
|
|
634
|
+
if (!parsed2 || typeof parsed2 !== "object" || !("clientToken" in parsed2)) {
|
|
635
|
+
throw new TransferFailure("Client token response was invalid.", 502);
|
|
636
|
+
}
|
|
637
|
+
const token = parsed2.clientToken;
|
|
638
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
639
|
+
throw new TransferFailure("Client token response was invalid.", 502);
|
|
640
|
+
}
|
|
641
|
+
return token;
|
|
642
|
+
}
|
|
643
|
+
async function registerFile(io, request, path, pathname, onRetry) {
|
|
644
|
+
const response = await withRetry(
|
|
645
|
+
io,
|
|
646
|
+
async () => {
|
|
647
|
+
const result = await io.fetch(request.url, {
|
|
648
|
+
method: request.method,
|
|
649
|
+
headers: request.headers,
|
|
650
|
+
body: JSON.stringify({ path, pathname })
|
|
651
|
+
});
|
|
652
|
+
if (result.status >= 400) {
|
|
653
|
+
const message = result.text.length > 0 ? result.text : `Request failed (${result.status}).`;
|
|
654
|
+
throw new TransferFailure(message, result.status);
|
|
655
|
+
}
|
|
656
|
+
return result;
|
|
657
|
+
},
|
|
658
|
+
onRetry
|
|
659
|
+
);
|
|
660
|
+
return response.text.length > 0 ? withNewline(response.text) : "";
|
|
661
|
+
}
|
|
662
|
+
function filePlan(config, command, nonce) {
|
|
663
|
+
const plan = planCommand(config, { kind: "file-put", ...command }, nonce);
|
|
664
|
+
if (plan.kind !== "file-put") throw new Error("Command failed.");
|
|
665
|
+
return plan;
|
|
666
|
+
}
|
|
667
|
+
function sameTarget(checkpoint, config, command, info) {
|
|
668
|
+
return checkpoint.apiUrl === config.apiUrl && checkpoint.entryId === command.id && checkpoint.path === command.path && checkpoint.file === command.file && checkpoint.size === info.size && checkpoint.mtimeMs === info.mtimeMs;
|
|
669
|
+
}
|
|
670
|
+
function isRetryableStatus(status) {
|
|
671
|
+
return status === 408 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
672
|
+
}
|
|
673
|
+
function isPermanent(error) {
|
|
674
|
+
return error instanceof TransferFailure && error.status !== void 0 && !isRetryableStatus(error.status);
|
|
675
|
+
}
|
|
676
|
+
function canRetry(error) {
|
|
677
|
+
if (error instanceof TransferFailure) {
|
|
678
|
+
return error.status === void 0 || isRetryableStatus(error.status);
|
|
679
|
+
}
|
|
680
|
+
if (error instanceof Error && "code" in error) {
|
|
681
|
+
const code = String(error.code);
|
|
682
|
+
if (code === "ENOENT" || code === "EISDIR" || code === "EACCES") return false;
|
|
683
|
+
}
|
|
684
|
+
return error instanceof Error;
|
|
685
|
+
}
|
|
686
|
+
async function withRetry(io, run, onRetry) {
|
|
687
|
+
let last;
|
|
688
|
+
for (let attempt = 1; attempt <= transferAttempts; attempt += 1) {
|
|
689
|
+
try {
|
|
690
|
+
return await run();
|
|
691
|
+
} catch (error) {
|
|
692
|
+
last = error;
|
|
693
|
+
if (!canRetry(error) || attempt === transferAttempts) {
|
|
694
|
+
if (attempt > 1) {
|
|
695
|
+
const message = error instanceof Error ? error.message : "Command failed.";
|
|
696
|
+
const status = error instanceof TransferFailure ? error.status : void 0;
|
|
697
|
+
throw new TransferFailure(
|
|
698
|
+
`Failed after ${attempt} attempts: ${message}`,
|
|
699
|
+
status,
|
|
700
|
+
attempt
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
throw error;
|
|
704
|
+
}
|
|
705
|
+
onRetry(attempt);
|
|
706
|
+
await io.sleep(retryDelayMs(attempt));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
throw last;
|
|
710
|
+
}
|
|
711
|
+
function failureResult(error, progressPath) {
|
|
712
|
+
const message = error instanceof Error ? error.message : "Command failed.";
|
|
713
|
+
const text = withNewline(message);
|
|
714
|
+
if (progressPath === null) return { exitCode: 1, stdout: "", stderr: text };
|
|
715
|
+
return { exitCode: 1, stdout: "", stderr: `${text}Progress file: ${progressPath}
|
|
716
|
+
` };
|
|
717
|
+
}
|
|
718
|
+
function noted(notes) {
|
|
719
|
+
if (notes.length === 0) return "";
|
|
720
|
+
return `${notes.join("\n")}
|
|
721
|
+
`;
|
|
722
|
+
}
|
|
723
|
+
function withNewline(text) {
|
|
724
|
+
return text.endsWith("\n") ? text : `${text}
|
|
725
|
+
`;
|
|
726
|
+
}
|
|
727
|
+
function isCheckpoint(value) {
|
|
728
|
+
if (!value || typeof value !== "object") return false;
|
|
729
|
+
const checkpoint = value;
|
|
730
|
+
if (checkpoint.version !== 1) return false;
|
|
731
|
+
if (checkpoint.phase !== "uploading" && checkpoint.phase !== "registering") return false;
|
|
732
|
+
if (!Array.isArray(checkpoint.parts) || !checkpoint.parts.every(isCompletedPart)) return false;
|
|
733
|
+
const fields = [
|
|
734
|
+
"apiUrl",
|
|
735
|
+
"entryId",
|
|
736
|
+
"path",
|
|
737
|
+
"file",
|
|
738
|
+
"nonce",
|
|
739
|
+
"pathname",
|
|
740
|
+
"uploadId",
|
|
741
|
+
"key"
|
|
742
|
+
];
|
|
743
|
+
for (const field of fields) {
|
|
744
|
+
if (typeof checkpoint[field] !== "string" || checkpoint[field].length === 0) return false;
|
|
745
|
+
}
|
|
746
|
+
if (!Number.isFinite(checkpoint.size) || checkpoint.size < 0) return false;
|
|
747
|
+
if (!Number.isFinite(checkpoint.mtimeMs)) return false;
|
|
748
|
+
if (checkpoint.registerPathname !== null && typeof checkpoint.registerPathname !== "string") {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
if (checkpoint.phase === "registering" && (checkpoint.registerPathname === null || checkpoint.registerPathname.length === 0)) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
function isCompletedPart(value) {
|
|
757
|
+
if (!value || typeof value !== "object") return false;
|
|
758
|
+
const part = value;
|
|
759
|
+
return Number.isInteger(part.partNumber) && part.partNumber > 0 && typeof part.etag === "string" && part.etag.length > 0;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/run.ts
|
|
763
|
+
function withNewline2(text) {
|
|
764
|
+
return text.endsWith("\n") ? text : `${text}
|
|
765
|
+
`;
|
|
766
|
+
}
|
|
767
|
+
async function send(io, request) {
|
|
768
|
+
const response = await io.fetch(request.url, {
|
|
769
|
+
method: request.method,
|
|
770
|
+
headers: request.headers,
|
|
771
|
+
...request.body === void 0 ? {} : { body: request.body }
|
|
772
|
+
});
|
|
773
|
+
if (response.status >= 400) {
|
|
774
|
+
const stderr = response.text.length > 0 ? withNewline2(response.text) : `Request failed (${response.status}).
|
|
775
|
+
`;
|
|
776
|
+
return { exitCode: 1, stdout: "", stderr };
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
exitCode: 0,
|
|
780
|
+
stdout: response.text.length > 0 ? withNewline2(response.text) : "",
|
|
781
|
+
stderr: ""
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
async function runCommand(config, command, io) {
|
|
785
|
+
try {
|
|
786
|
+
if (command.kind === "file-put") return await putFile(config, command, io);
|
|
787
|
+
const plan = planCommand(config, command, "");
|
|
788
|
+
if (plan.kind !== "http") throw new Error("Command failed.");
|
|
789
|
+
if (command.kind === "file-rm") return await sendWithRetry(io, plan.request);
|
|
790
|
+
return await send(io, plan.request);
|
|
791
|
+
} catch (error) {
|
|
792
|
+
const message = error instanceof Error ? error.message : "Command failed.";
|
|
793
|
+
return { exitCode: 1, stdout: "", stderr: withNewline2(message) };
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/main.ts
|
|
798
|
+
var parsed = parseArgv(process.argv.slice(2));
|
|
799
|
+
if (!parsed.ok) {
|
|
800
|
+
process.stderr.write(`${parsed.message}
|
|
801
|
+
`);
|
|
802
|
+
process.exitCode = 1;
|
|
803
|
+
} else if ("help" in parsed) {
|
|
804
|
+
process.stdout.write(`${parsed.help}
|
|
805
|
+
`);
|
|
806
|
+
} else {
|
|
807
|
+
const config = readClientConfig(process.env);
|
|
808
|
+
if (!config.ok) {
|
|
809
|
+
process.stderr.write(`${config.message}
|
|
810
|
+
`);
|
|
811
|
+
process.exitCode = 1;
|
|
812
|
+
} else {
|
|
813
|
+
const home = homedir();
|
|
814
|
+
const io = {
|
|
815
|
+
nonce: () => randomBytes(16).toString("base64url"),
|
|
816
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
817
|
+
stat: async (path) => {
|
|
818
|
+
const info = await stat(path);
|
|
819
|
+
return { size: info.size, mtimeMs: info.mtimeMs };
|
|
820
|
+
},
|
|
821
|
+
readRange: async (path, start, end) => {
|
|
822
|
+
const handle = await open(path, "r");
|
|
823
|
+
try {
|
|
824
|
+
const length = end - start;
|
|
825
|
+
const buffer = Buffer.alloc(length);
|
|
826
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
827
|
+
if (bytesRead !== length) throw new TransferFailure("File changed while reading.", 400);
|
|
828
|
+
return buffer;
|
|
829
|
+
} finally {
|
|
830
|
+
await handle.close();
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
readCheckpoint: async (id) => {
|
|
834
|
+
try {
|
|
835
|
+
return await readFile(checkpointFilePath(home, id), "utf8");
|
|
836
|
+
} catch (error) {
|
|
837
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
writeCheckpoint: async (id, body) => {
|
|
842
|
+
const path = checkpointFilePath(home, id);
|
|
843
|
+
await mkdir(dirname(path), { recursive: true });
|
|
844
|
+
await writeFile(path, body);
|
|
845
|
+
},
|
|
846
|
+
removeCheckpoint: async (id) => {
|
|
847
|
+
await rm(checkpointFilePath(home, id), { force: true });
|
|
848
|
+
},
|
|
849
|
+
checkpointPath: (id) => checkpointFilePath(home, id),
|
|
850
|
+
createUpload: (pathname, token) => createMultipartUpload(pathname, { access: "private", token }),
|
|
851
|
+
uploadPart: (part) => uploadPart(part.pathname, Buffer.from(part.body), {
|
|
852
|
+
access: "private",
|
|
853
|
+
token: part.token,
|
|
854
|
+
key: part.key,
|
|
855
|
+
uploadId: part.uploadId,
|
|
856
|
+
partNumber: part.partNumber
|
|
857
|
+
}),
|
|
858
|
+
completeUpload: (upload) => completeMultipartUpload(upload.pathname, upload.parts, {
|
|
859
|
+
access: "private",
|
|
860
|
+
token: upload.token,
|
|
861
|
+
key: upload.key,
|
|
862
|
+
uploadId: upload.uploadId
|
|
863
|
+
}),
|
|
864
|
+
fetch: async (url, init) => {
|
|
865
|
+
const response = await fetch(url, init);
|
|
866
|
+
return { status: response.status, text: await response.text() };
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
const result = await runCommand(config.config, parsed.command, io);
|
|
870
|
+
if (result.stdout.length > 0) process.stdout.write(result.stdout);
|
|
871
|
+
if (result.stderr.length > 0) process.stderr.write(result.stderr);
|
|
872
|
+
process.exitCode = result.exitCode;
|
|
873
|
+
}
|
|
874
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@refract9/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command-line client for the refract9 HTTP API.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/mengtaoxin/refract9.git",
|
|
8
|
+
"directory": "apps/cli"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"refract9": "./dist/main.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/main.js"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prepack": "node build.mts"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@vercel/blob": "^2.8.2"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"esbuild": "^0.28.2"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=24.15.0"
|
|
31
|
+
}
|
|
32
|
+
}
|