disk 0.8.17 → 0.8.19

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/dist/cli.js DELETED
@@ -1,1450 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // bin/cli.ts
4
- import { createRequire } from "module";
5
- import { Command, Option } from "commander";
6
-
7
- // src/client.ts
8
- import createClientDefault from "openapi-fetch";
9
-
10
- // src/s3xml.ts
11
- import { XMLParser } from "fast-xml-parser";
12
- var parser = new XMLParser({
13
- isArray: (name) => name === "Contents" || name === "CommonPrefixes",
14
- parseTagValue: false,
15
- trimValues: true,
16
- ignoreAttributes: true
17
- });
18
- function parseXml(xml) {
19
- if (!xml.trim()) return {};
20
- return parser.parse(xml);
21
- }
22
-
23
- // src/errors.ts
24
- var ArchilError = class extends Error {
25
- /** HTTP status code associated with the failure. */
26
- status;
27
- /** Machine-readable error code (e.g. an S3 code like "NoSuchKey"), if known. */
28
- code;
29
- constructor(message, status, code) {
30
- super(message);
31
- this.name = "ArchilError";
32
- this.status = status;
33
- this.code = code;
34
- }
35
- };
36
- var ArchilApiError = class extends ArchilError {
37
- constructor(message, status, code) {
38
- super(message, status, code);
39
- this.name = "ArchilApiError";
40
- }
41
- };
42
- var ArchilS3Error = class extends ArchilError {
43
- /** S3 request id, if the gateway returned one. */
44
- requestId;
45
- /** Raw response body (the XML document), for debugging. */
46
- raw;
47
- constructor(opts) {
48
- const detail = opts.message ?? opts.statusText ?? "";
49
- const codePart = opts.code ? ` ${opts.code}` : "";
50
- super(
51
- `S3 ${opts.operation} failed: ${opts.statusCode}${codePart}${detail ? ` \u2014 ${detail}` : ""}`,
52
- opts.statusCode,
53
- opts.code
54
- );
55
- this.name = "ArchilS3Error";
56
- this.requestId = opts.requestId;
57
- this.raw = opts.raw;
58
- }
59
- };
60
- function tagString(obj, tag) {
61
- const value = obj[tag];
62
- return value === void 0 || value === null ? void 0 : String(value);
63
- }
64
- function parseS3Error(operation, statusCode, statusText, body) {
65
- let err = {};
66
- try {
67
- err = parseXml(body).Error ?? {};
68
- } catch {
69
- err = {};
70
- }
71
- return new ArchilS3Error({
72
- operation,
73
- statusCode,
74
- statusText,
75
- code: tagString(err, "Code"),
76
- message: tagString(err, "Message"),
77
- requestId: tagString(err, "RequestId"),
78
- raw: body
79
- });
80
- }
81
-
82
- // src/regions.ts
83
- var REGION_URLS = {
84
- "aws-us-east-1": "https://control.green.us-east-1.aws.prod.archil.com",
85
- "aws-us-west-2": "https://control.green.us-west-2.aws.prod.archil.com",
86
- "aws-eu-west-1": "https://control.green.eu-west-1.aws.prod.archil.com",
87
- "gcp-us-central1": "https://control.blue.us-central1.gcp.prod.archil.com"
88
- };
89
- function resolveBaseUrl(region) {
90
- const url = REGION_URLS[region];
91
- if (!url) {
92
- throw new Error(
93
- `Unknown region "${region}". Valid regions: ${Object.keys(REGION_URLS).join(", ")}`
94
- );
95
- }
96
- return url;
97
- }
98
- function deriveS3BaseUrl(controlBaseUrl) {
99
- try {
100
- const u = new URL(controlBaseUrl);
101
- u.hostname = u.hostname.replace(/^control\./, "s3.");
102
- return u.toString().replace(/\/$/, "");
103
- } catch {
104
- return void 0;
105
- }
106
- }
107
-
108
- // src/version.ts
109
- var VERSION = true ? "0.8.17" : "0.0.0-dev";
110
- var USER_AGENT = `archil-js/${VERSION}`;
111
-
112
- // src/client.ts
113
- var createClient = typeof createClientDefault === "function" ? createClientDefault : createClientDefault.default;
114
- function createApiClient(opts) {
115
- const baseUrl = opts.baseUrl ?? resolveBaseUrl(opts.region);
116
- return createClient({
117
- baseUrl,
118
- headers: {
119
- Authorization: `key-${opts.apiKey.replace(/^key-/, "")}`,
120
- // Identifies the JS SDK (and its version) to the control plane. Honored
121
- // by Node's fetch; browsers treat User-Agent as a forbidden header and
122
- // drop it, which is fine — the SDK's primary use is server-side.
123
- "User-Agent": USER_AGENT
124
- }
125
- });
126
- }
127
- async function unwrap(promise) {
128
- const { data: body, error, response } = await promise;
129
- if (error || !body) {
130
- const errBody = error;
131
- throw new ArchilApiError(
132
- errBody?.error ?? `API request failed with status ${response.status}`,
133
- response.status
134
- );
135
- }
136
- if (!body.success) {
137
- throw new ArchilApiError(
138
- body.error ?? "Unknown API error",
139
- response.status
140
- );
141
- }
142
- return body.data;
143
- }
144
- async function unwrapEmpty(promise) {
145
- const { data: body, error, response } = await promise;
146
- if (error || !body) {
147
- const errBody = error;
148
- throw new ArchilApiError(
149
- errBody?.error ?? `API request failed with status ${response.status}`,
150
- response.status
151
- );
152
- }
153
- if (!body.success) {
154
- throw new ArchilApiError(
155
- body.error ?? "Unknown API error",
156
- response.status
157
- );
158
- }
159
- }
160
-
161
- // src/disk.ts
162
- var Disk = class {
163
- id;
164
- name;
165
- organization;
166
- status;
167
- provider;
168
- region;
169
- createdAt;
170
- fsHandlerStatus;
171
- lastAccessed;
172
- dataSize;
173
- monthlyUsage;
174
- mounts;
175
- metrics;
176
- connectedClients;
177
- authorizedUsers;
178
- allowedIps;
179
- /** @internal */
180
- _client;
181
- /** @internal */
182
- _archilRegion;
183
- /** Base URL for the S3-compatible API (port 9000 ingress). Empty if unset. */
184
- _s3BaseUrl;
185
- /** Lazily-constructed multipart namespace (see {@link multipart}). @internal */
186
- _multipart;
187
- /** @internal */
188
- constructor(data, client, archilRegion, s3BaseUrl) {
189
- this.id = data.id;
190
- this.name = data.name;
191
- this.organization = data.organization;
192
- this.status = data.status;
193
- this.provider = data.provider;
194
- this.region = data.region;
195
- this.createdAt = data.createdAt;
196
- this.fsHandlerStatus = data.fsHandlerStatus;
197
- this.lastAccessed = data.lastAccessed;
198
- this.dataSize = data.dataSize;
199
- this.monthlyUsage = data.monthlyUsage;
200
- this.mounts = data.mounts;
201
- this.metrics = data.metrics;
202
- this.connectedClients = data.connectedClients;
203
- this.authorizedUsers = data.authorizedUsers;
204
- this.allowedIps = data.allowedIps;
205
- this._client = client;
206
- this._archilRegion = archilRegion;
207
- this._s3BaseUrl = s3BaseUrl ?? "";
208
- }
209
- toJSON() {
210
- return {
211
- id: this.id,
212
- name: this.name,
213
- organization: this.organization,
214
- status: this.status,
215
- provider: this.provider,
216
- region: this.region,
217
- createdAt: this.createdAt,
218
- fsHandlerStatus: this.fsHandlerStatus,
219
- lastAccessed: this.lastAccessed,
220
- dataSize: this.dataSize,
221
- monthlyUsage: this.monthlyUsage,
222
- mounts: this.mounts,
223
- metrics: this.metrics,
224
- connectedClients: this.connectedClients,
225
- authorizedUsers: this.authorizedUsers,
226
- allowedIps: this.allowedIps
227
- };
228
- }
229
- async addUser(user) {
230
- return unwrap(
231
- this._client.POST("/api/disks/{id}/users", {
232
- params: { path: { id: this.id } },
233
- body: user
234
- })
235
- );
236
- }
237
- async removeUser(userType, identifier) {
238
- await unwrapEmpty(
239
- this._client.DELETE("/api/disks/{id}/users/{userType}", {
240
- params: {
241
- path: { id: this.id, userType },
242
- query: { identifier }
243
- }
244
- })
245
- );
246
- }
247
- async createToken(nickname) {
248
- const user = await unwrap(
249
- this._client.POST("/api/disks/{id}/users", {
250
- params: { path: { id: this.id } },
251
- body: { type: "token", nickname }
252
- })
253
- );
254
- if (!user.token || !user.identifier) {
255
- throw new Error("Server did not return a generated token");
256
- }
257
- return user;
258
- }
259
- async removeTokenUser(identifier) {
260
- await this.removeUser("token", identifier);
261
- }
262
- async getAllowedIPs() {
263
- const data = await unwrap(
264
- this._client.GET("/api/disks/{id}/allowed-ips", {
265
- params: { path: { id: this.id } }
266
- })
267
- );
268
- return data.allowedIps;
269
- }
270
- async setAllowedIPs(allowedIps) {
271
- const data = await unwrap(
272
- this._client.PUT("/api/disks/{id}/allowed-ips", {
273
- params: { path: { id: this.id } },
274
- body: { allowedIps }
275
- })
276
- );
277
- return data.allowedIps;
278
- }
279
- async addAllowedIP(ip) {
280
- const current = await this.getAllowedIPs();
281
- if (current.includes(ip)) return current;
282
- return this.setAllowedIPs([...current, ip]);
283
- }
284
- async removeAllowedIP(ip) {
285
- const current = await this.getAllowedIPs();
286
- return this.setAllowedIPs(current.filter((i) => i !== ip));
287
- }
288
- async delete() {
289
- await unwrapEmpty(
290
- this._client.DELETE("/api/disks/{id}", {
291
- params: { path: { id: this.id } }
292
- })
293
- );
294
- }
295
- /**
296
- * Execute a command in a container with this disk mounted.
297
- * Blocks until the command completes and returns stdout, stderr, and exit code.
298
- */
299
- async exec(command) {
300
- return unwrap(
301
- this._client.POST("/api/disks/{id}/exec", {
302
- params: { path: { id: this.id } },
303
- body: { command }
304
- })
305
- );
306
- }
307
- /**
308
- * Constant-time parallel grep across files on this disk. Listing and
309
- * matching are fanned out across ephemeral exec containers; the request
310
- * finishes within the supplied time budget regardless of dataset size.
311
- *
312
- * The returned `stoppedReason` says whether the search ran to completion
313
- * or short-circuited on `maxResults` / `maxDurationSeconds`. When
314
- * stopping early, the matches returned are a sample (whichever workers
315
- * reported first), not the lexicographically first N.
316
- */
317
- async grep(opts) {
318
- return unwrap(
319
- this._client.POST("/api/disks/{id}/grep", {
320
- params: { path: { id: this.id } },
321
- body: {
322
- directory: opts.directory,
323
- pattern: opts.pattern,
324
- recursive: opts.recursive ?? false,
325
- maxDurationSeconds: opts.maxDurationSeconds ?? 30,
326
- concurrency: opts.concurrency ?? 50,
327
- maxResults: opts.maxResults ?? 1e3
328
- }
329
- })
330
- );
331
- }
332
- /**
333
- * Create a signed, time-limited URL that lets anyone download a single file
334
- * from this disk without authentication. The returned URL embeds a
335
- * cryptographically signed token carrying the disk, the file's key, and an
336
- * expiry — share it directly; no API key is needed to redeem it.
337
- *
338
- * @param key Path to the file on the disk (e.g. "reports/2026-01/data.pdf").
339
- * @param opts `expiresIn` sets the URL lifetime in seconds (any positive
340
- * integer, max 604800 = 7 days). Defaults to 24h.
341
- */
342
- async share(key, opts = {}) {
343
- const body = { key };
344
- if (opts.expiresIn !== void 0) body.expiresIn = opts.expiresIn;
345
- const call = this._client.POST;
346
- return unwrap(call(`/api/disks/${this.id}/share`, { body }));
347
- }
348
- /**
349
- * Read an object from the disk via the S3-compatible GetObject API and return
350
- * its full contents as bytes. Throws `ArchilS3Error` if the object does not
351
- * exist (status 404, code "NoSuchKey") or the request is rejected; use
352
- * `headObject`/`objectExists` to check existence without throwing.
353
- *
354
- * @param key Path on the disk (e.g. "reports/2026-01/data.json")
355
- */
356
- async getObject(key) {
357
- const resp = await this._s3Request("GET", key);
358
- if (!resp.ok) {
359
- throw parseS3Error("GetObject", resp.status, resp.statusText, decodeText(resp.body));
360
- }
361
- return resp.body;
362
- }
363
- /**
364
- * Fetch an object's metadata (size, etag, content type, last-modified) without
365
- * downloading its contents, via the S3-compatible HeadObject API. Returns
366
- * `null` if the object does not exist.
367
- *
368
- * @param key Path on the disk (e.g. "reports/2026-01/data.json")
369
- */
370
- async headObject(key) {
371
- const resp = await this._s3Request("HEAD", key);
372
- if (resp.status === 404) return null;
373
- if (!resp.ok) {
374
- throw parseS3Error("HeadObject", resp.status, resp.statusText, decodeText(resp.body));
375
- }
376
- const lastModified = resp.headers.get("last-modified");
377
- return {
378
- size: Number(resp.headers.get("content-length") ?? 0),
379
- etag: resp.headers.get("etag") ?? void 0,
380
- contentType: resp.headers.get("content-type") ?? void 0,
381
- lastModified: lastModified ? new Date(lastModified) : void 0
382
- };
383
- }
384
- /** Whether an object exists on the disk (a HeadObject that maps 404 → false). */
385
- async objectExists(key) {
386
- return await this.headObject(key) !== null;
387
- }
388
- /**
389
- * Write an object to the disk via the S3-compatible API. Handles any size:
390
- * bodies at or below `multipartThreshold` (defaults to `partSize`, i.e.
391
- * 16 MiB) go through a single PutObject request; larger bodies are uploaded as
392
- * a multipart upload — split into `partSize` parts, uploaded with bounded
393
- * `concurrency` (default 4), and assembled. A failed part aborts the upload so
394
- * nothing is left half-staged. For manual control over the multipart
395
- * lifecycle, use the {@link multipart} namespace.
396
- *
397
- * Faster than exec for large files — no container overhead, no command-length
398
- * limits. Returns the entity tag the server assigned (a multipart upload's tag
399
- * is S3's `md5(concat(partMd5s))-N` form rather than a plain MD5).
400
- *
401
- * @param key Path on the disk (e.g. "reports/2026-01/data.json")
402
- * @param body Contents as a string, Uint8Array/Buffer, or ArrayBuffer
403
- * @param options Either a content-type string, or {@link PutObjectOptions}
404
- * (`contentType`, `multipartThreshold`, `partSize`,
405
- * `concurrency`). Content type defaults to
406
- * "application/octet-stream".
407
- */
408
- async putObject(key, body, options) {
409
- const opts = typeof options === "string" ? { contentType: options } : options ?? {};
410
- const contentType = opts.contentType ?? "application/octet-stream";
411
- const partSize = Math.max(opts.partSize ?? DEFAULT_PART_SIZE, MIN_PART_SIZE);
412
- const threshold = opts.multipartThreshold ?? partSize;
413
- const bytes = toBytes(body);
414
- if (bytes.length <= threshold) {
415
- const resp = await this._s3Request("PUT", key, { body, contentType });
416
- if (!resp.ok) {
417
- throw parseS3Error("PutObject", resp.status, resp.statusText, decodeText(resp.body));
418
- }
419
- return { etag: resp.headers.get("etag") ?? void 0 };
420
- }
421
- return this._putMultipart(
422
- key,
423
- bytes,
424
- contentType,
425
- partSize,
426
- Math.max(1, opts.concurrency ?? DEFAULT_UPLOAD_CONCURRENCY)
427
- );
428
- }
429
- /**
430
- * Upload a large body through the multipart lifecycle: split into `partSize`
431
- * parts, upload them with bounded concurrency, then complete — aborting the
432
- * upload if any part fails so nothing is left half-staged. @internal
433
- */
434
- async _putMultipart(key, bytes, contentType, partSize, concurrency) {
435
- const effectivePartSize = effectiveUploadPartSize(bytes.length, partSize);
436
- const mp = this.multipart;
437
- const upload = await mp.create(key, contentType);
438
- try {
439
- const partCount = Math.ceil(bytes.length / effectivePartSize);
440
- const parts = new Array(partCount);
441
- let next = 0;
442
- const worker = async () => {
443
- for (; ; ) {
444
- const index = next++;
445
- if (index >= partCount) return;
446
- const start = index * effectivePartSize;
447
- const slice = bytes.subarray(start, Math.min(start + effectivePartSize, bytes.length));
448
- parts[index] = await mp.uploadPart(key, upload.uploadId, index + 1, slice);
449
- }
450
- };
451
- await Promise.all(Array.from({ length: Math.min(concurrency, partCount) }, worker));
452
- const done = await mp.complete(key, upload.uploadId, parts);
453
- return { etag: done.etag };
454
- } catch (err) {
455
- await mp.abort(key, upload.uploadId).catch(() => {
456
- });
457
- throw err;
458
- }
459
- }
460
- /**
461
- * Append bytes to an object via the S3-compatible PutObject append extension
462
- * (`?append=true`). If the object already exists the bytes are appended to it;
463
- * if it doesn't, it is created. Returns the entity tag of the full object
464
- * after the append.
465
- *
466
- * Each call may append at most 1 MiB — the server rejects a larger body with
467
- * `EntityTooLarge`. To grow an object past that, append in chunks (or use
468
- * {@link putObject} for a one-shot large write).
469
- *
470
- * Unlike most operations this is NOT auto-retried on a transient error:
471
- * append isn't idempotent, so retrying a succeeded-but-unacknowledged append
472
- * would duplicate the bytes. On a transient failure, re-append yourself only
473
- * after confirming the object's size.
474
- *
475
- * @param key Path on the disk (e.g. "logs/app.log")
476
- * @param body Bytes to append (string, Uint8Array/Buffer, or ArrayBuffer)
477
- * @param contentType MIME type, applied only when the object is newly created.
478
- */
479
- async appendObject(key, body, contentType = "application/octet-stream") {
480
- const resp = await this._s3Request("PUT", key, {
481
- body,
482
- contentType,
483
- query: { append: "true" },
484
- retry: false
485
- });
486
- if (!resp.ok) {
487
- throw parseS3Error("AppendObject", resp.status, resp.statusText, decodeText(resp.body));
488
- }
489
- return { etag: resp.headers.get("etag") ?? void 0 };
490
- }
491
- /**
492
- * Delete an object from the disk via the S3-compatible DeleteObject API.
493
- * Idempotent: deleting a key that doesn't exist resolves successfully, per
494
- * S3 semantics.
495
- *
496
- * @param key Path on the disk (e.g. "project/dist/server.cjs")
497
- */
498
- async deleteObject(key) {
499
- const resp = await this._s3Request("DELETE", key);
500
- if (!resp.ok && resp.status !== 404) {
501
- throw parseS3Error("DeleteObject", resp.status, resp.statusText, decodeText(resp.body));
502
- }
503
- }
504
- /**
505
- * List objects on the disk via the S3-compatible ListObjectsV2 API. By
506
- * default this follows continuation tokens until the listing is exhausted and
507
- * returns every matching key. Use `limit` to cap the total, `singlePage` for a
508
- * single request, or {@link listObjectsPages} to stream pages without loading
509
- * everything into memory.
510
- *
511
- * @param prefix Only return keys beginning with this prefix (omit for all).
512
- * @param opts Listing and pagination options.
513
- */
514
- async listObjects(prefix, opts = {}) {
515
- if (opts.singlePage) {
516
- return this._listObjectsPage(prefix, opts);
517
- }
518
- const objects = [];
519
- const commonPrefixes = [];
520
- const seenPrefixes = /* @__PURE__ */ new Set();
521
- let echoedPrefix;
522
- let truncated = false;
523
- outer: for await (const page of this.listObjectsPages(prefix, opts)) {
524
- echoedPrefix = page.prefix;
525
- for (const cp of page.commonPrefixes) {
526
- if (!seenPrefixes.has(cp)) {
527
- seenPrefixes.add(cp);
528
- commonPrefixes.push(cp);
529
- }
530
- }
531
- for (const obj of page.objects) {
532
- if (opts.limit !== void 0 && objects.length >= opts.limit) {
533
- truncated = true;
534
- break outer;
535
- }
536
- objects.push(obj);
537
- }
538
- }
539
- return { objects, commonPrefixes, isTruncated: truncated, keyCount: objects.length, prefix: echoedPrefix };
540
- }
541
- /**
542
- * Yield ListObjectsV2 pages lazily, following continuation tokens. A
543
- * memory-friendly way to process a large listing without materializing it:
544
- *
545
- * ```ts
546
- * for await (const page of disk.listObjectsPages("logs/")) {
547
- * for (const obj of page.objects) process(obj);
548
- * }
549
- * ```
550
- *
551
- * @param prefix Only return keys beginning with this prefix (omit for all).
552
- * @param opts Listing options (`limit` / `singlePage` are ignored here —
553
- * control your own loop).
554
- */
555
- async *listObjectsPages(prefix, opts = {}) {
556
- const seenTokens = /* @__PURE__ */ new Set();
557
- let continuationToken = opts.continuationToken;
558
- for (; ; ) {
559
- const page = await this._listObjectsPage(prefix, { ...opts, continuationToken });
560
- yield page;
561
- const next = page.isTruncated ? page.nextContinuationToken : void 0;
562
- if (!next || seenTokens.has(next)) break;
563
- seenTokens.add(next);
564
- continuationToken = next;
565
- }
566
- }
567
- /** Fetch a single ListObjectsV2 page. @internal */
568
- async _listObjectsPage(prefix, opts) {
569
- const query = { "list-type": 2 };
570
- if (prefix !== void 0) query.prefix = prefix;
571
- if (!opts.recursive) query.delimiter = "/";
572
- if (opts.continuationToken !== void 0) query["continuation-token"] = opts.continuationToken;
573
- if (opts.startAfter !== void 0) query["start-after"] = opts.startAfter;
574
- const resp = await this._s3Request("GET", "", { query });
575
- if (!resp.ok) {
576
- throw parseS3Error("ListObjectsV2", resp.status, resp.statusText, decodeText(resp.body));
577
- }
578
- return parseListObjectsResult(decodeText(resp.body));
579
- }
580
- /**
581
- * Delete up to many objects in a single S3-compatible DeleteObjects request.
582
- * Unlike {@link deleteObject}, failures are reported per key rather than
583
- * thrown: the result's `deleted` lists the keys that were removed and
584
- * `errors` lists the ones that weren't (with the server's code/message). A
585
- * key that didn't exist still counts as deleted, per S3 semantics.
586
- *
587
- * The server caps a single request at 1000 keys; this method transparently
588
- * splits larger inputs into 1000-key batches and merges the results.
589
- *
590
- * @param keys Object keys to delete.
591
- * @param opts `quiet` suppresses the per-key success list server-side.
592
- */
593
- async deleteObjects(keys2, opts = {}) {
594
- const deleted = [];
595
- const errors = [];
596
- for (let i = 0; i < keys2.length; i += MAX_DELETE_OBJECTS_PER_REQUEST) {
597
- const batch = keys2.slice(i, i + MAX_DELETE_OBJECTS_PER_REQUEST);
598
- const xml = buildDeleteObjectsXml(batch, opts.quiet ?? false);
599
- const resp = await this._s3Request("POST", "", {
600
- query: { delete: "" },
601
- body: xml,
602
- contentType: "application/xml"
603
- });
604
- if (!resp.ok) {
605
- throw parseS3Error("DeleteObjects", resp.status, resp.statusText, decodeText(resp.body));
606
- }
607
- const parsed = parseDeleteObjectsResult(decodeText(resp.body));
608
- deleted.push(...parsed.deleted);
609
- errors.push(...parsed.errors);
610
- }
611
- return { deleted, errors };
612
- }
613
- /**
614
- * The advanced, opt-in multipart-upload API. Drive the raw lifecycle
615
- * yourself — `create` → `uploadPart` → `complete` (or `abort`), plus
616
- * `listParts` / `listUploads`. Most callers don't need this: {@link putObject}
617
- * runs the whole lifecycle automatically for large bodies. Reach for it only
618
- * when you need manual control (e.g. uploading parts from separate processes),
619
- * and note you then own part-size, memory, and concurrency management.
620
- */
621
- get multipart() {
622
- return this._multipart ??= new DiskMultipart(this.id, this._s3Request.bind(this));
623
- }
624
- /**
625
- * Send a single request to the disk's S3-compatible endpoint. This reuses the
626
- * control-plane client purely for its credential and transport — the same
627
- * `Authorization` header is sent and verified by the same code server-side —
628
- * pointed at the S3 host. Returns the response status and fully-buffered body
629
- * so callers can inspect both regardless of the verb used.
630
- *
631
- * The S3 routes (`/{diskId}/{key}` for objects, `/{diskId}` for the bucket)
632
- * are not part of the typed control-plane API, so the path and per-request
633
- * options are passed untyped. An empty `key` targets the bucket itself (used
634
- * by listObjects).
635
- *
636
- * @internal
637
- */
638
- async _s3Request(method, key, opts = {}) {
639
- if (!this._s3BaseUrl) {
640
- throw new Error(
641
- "S3 base URL not configured. Pass s3BaseUrl to new Archil({...}) or set ARCHIL_S3_BASE_URL."
642
- );
643
- }
644
- const call = this._client[method];
645
- const trimmedKey = key.replace(/^\//, "");
646
- const encodedKey = trimmedKey.split("/").map(encodeURIComponent).join("/");
647
- const path = encodedKey ? `/${this.id}/${encodedKey}` : `/${this.id}`;
648
- const init = {
649
- baseUrl: this._s3BaseUrl,
650
- parseAs: "stream",
651
- ...opts.query ? { params: { query: opts.query } } : {},
652
- // fetch accepts string / Uint8Array / ArrayBuffer bodies directly; pass it
653
- // through unchanged (no Node Buffer in the data path). Bodies are fully
654
- // buffered (never streams), so re-sending one on a retry is safe.
655
- ...opts.body !== void 0 ? { body: opts.body, bodySerializer: (b) => b } : {},
656
- ...opts.contentType ? { headers: { "Content-Type": opts.contentType } } : {}
657
- };
658
- const hasBody = method === "GET" || method === "POST";
659
- const maxRetries = opts.retry === false ? 0 : MAX_S3_RETRIES;
660
- for (let attempt = 0; ; attempt++) {
661
- let error;
662
- let response;
663
- try {
664
- ({ error, response } = await call(path, init));
665
- } catch (transportError) {
666
- if (attempt < maxRetries) {
667
- await sleep(s3RetryDelayMs(attempt));
668
- continue;
669
- }
670
- throw transportError;
671
- }
672
- if (!response.ok) {
673
- if (isTransientS3Status(response.status) && attempt < maxRetries) {
674
- await response.body?.cancel().catch(() => {
675
- });
676
- await sleep(s3RetryDelayMs(attempt));
677
- continue;
678
- }
679
- const message = typeof error === "string" ? error : error ? JSON.stringify(error) : "";
680
- return {
681
- ok: false,
682
- status: response.status,
683
- statusText: response.statusText,
684
- headers: response.headers,
685
- body: new TextEncoder().encode(message)
686
- };
687
- }
688
- const bytes = hasBody ? new Uint8Array(await response.arrayBuffer()) : new Uint8Array(0);
689
- return {
690
- ok: true,
691
- status: response.status,
692
- statusText: response.statusText,
693
- headers: response.headers,
694
- body: bytes
695
- };
696
- }
697
- }
698
- /**
699
- * Connect to this disk's data plane via the native ArchilClient.
700
- *
701
- * Requires the native module to be available (platform-specific .node binary).
702
- */
703
- async mount(opts) {
704
- let ArchilClient;
705
- try {
706
- const nativeSpecifier = "@archildata/native";
707
- const native = await import(nativeSpecifier);
708
- ArchilClient = native.ArchilClient ?? native.default?.ArchilClient;
709
- if (!ArchilClient) {
710
- throw new Error("@archildata/native did not export ArchilClient");
711
- }
712
- } catch {
713
- throw new Error(
714
- "Native client not available. Install @archildata/native (platform-specific binary) or use ArchilClient.connect() directly. mount() is not supported in browsers."
715
- );
716
- }
717
- return ArchilClient.connect({
718
- region: this._archilRegion,
719
- diskName: `${this.organization}/${this.name}`,
720
- authToken: opts?.authToken,
721
- logLevel: opts?.logLevel,
722
- serverAddress: opts?.serverAddress,
723
- insecure: opts?.insecure
724
- });
725
- }
726
- };
727
- var DiskMultipart = class {
728
- /** @internal */
729
- constructor(diskId, s3Request) {
730
- this.diskId = diskId;
731
- this.s3Request = s3Request;
732
- }
733
- diskId;
734
- s3Request;
735
- /**
736
- * Start a multipart upload (CreateMultipartUpload) and return its `uploadId`.
737
- *
738
- * @param key Path on the disk the finished object will live at.
739
- * @param contentType MIME type to store the object with.
740
- */
741
- async create(key, contentType) {
742
- const resp = await this.s3Request("POST", key, { query: { uploads: "" }, contentType });
743
- if (!resp.ok) {
744
- throw parseS3Error("CreateMultipartUpload", resp.status, resp.statusText, decodeText(resp.body));
745
- }
746
- const root = parseXml(decodeText(resp.body)).InitiateMultipartUploadResult ?? {};
747
- const uploadId = optionalString(root.UploadId);
748
- if (!uploadId) {
749
- throw new ArchilS3Error({
750
- operation: "CreateMultipartUpload",
751
- statusCode: resp.status,
752
- message: "response did not contain an UploadId",
753
- raw: decodeText(resp.body)
754
- });
755
- }
756
- return {
757
- uploadId,
758
- key: optionalString(root.Key) ?? key,
759
- bucket: optionalString(root.Bucket) ?? this.diskId
760
- };
761
- }
762
- /**
763
- * Upload one part (UploadPart) and return its entity tag, which you must
764
- * collect (with its part number) and pass to {@link complete}. Every part
765
- * except the last must be at least 5 MiB.
766
- *
767
- * @param key The upload's object key.
768
- * @param uploadId The id from {@link create}.
769
- * @param partNumber 1-based part number (1..=10000).
770
- * @param body Part contents as a string, Uint8Array/Buffer, or ArrayBuffer.
771
- */
772
- async uploadPart(key, uploadId, partNumber, body) {
773
- const resp = await this.s3Request("PUT", key, { query: { uploadId, partNumber }, body });
774
- if (!resp.ok) {
775
- throw parseS3Error("UploadPart", resp.status, resp.statusText, decodeText(resp.body));
776
- }
777
- return { partNumber, etag: resp.headers.get("etag") ?? "" };
778
- }
779
- /**
780
- * Finish a multipart upload (CompleteMultipartUpload), assembling the listed
781
- * parts into one object. Parts are sorted by part number before submission
782
- * (the server requires strictly-increasing order).
783
- *
784
- * Unlike the other operations this is NOT auto-retried on a transient error:
785
- * the gateway isn't idempotent for completion, so a retry after a
786
- * successful-but-unacknowledged complete would return a spurious NoSuchUpload.
787
- * On a transient failure, re-drive completion yourself only after confirming
788
- * the object isn't already present.
789
- *
790
- * @param key The upload's object key.
791
- * @param uploadId The id from {@link create}.
792
- * @param parts The `{ partNumber, etag }` pairs from {@link uploadPart}.
793
- */
794
- async complete(key, uploadId, parts) {
795
- const sorted = [...parts].sort((a, b) => a.partNumber - b.partNumber);
796
- const xml = buildCompleteMultipartUploadXml(sorted);
797
- const resp = await this.s3Request("POST", key, {
798
- query: { uploadId },
799
- body: xml,
800
- contentType: "application/xml",
801
- retry: false
802
- });
803
- if (!resp.ok) {
804
- throw parseS3Error("CompleteMultipartUpload", resp.status, resp.statusText, decodeText(resp.body));
805
- }
806
- const root = parseXml(decodeText(resp.body)).CompleteMultipartUploadResult ?? {};
807
- return {
808
- etag: optionalString(root.ETag),
809
- location: optionalString(root.Location),
810
- bucket: optionalString(root.Bucket),
811
- key: optionalString(root.Key)
812
- };
813
- }
814
- /**
815
- * Abort a multipart upload (AbortMultipartUpload), discarding every staged
816
- * part. Idempotent against an upload that's already gone (404 / NoSuchUpload
817
- * resolves successfully).
818
- *
819
- * @param key The upload's object key.
820
- * @param uploadId The id from {@link create}.
821
- */
822
- async abort(key, uploadId) {
823
- const resp = await this.s3Request("DELETE", key, { query: { uploadId } });
824
- if (!resp.ok && resp.status !== 404) {
825
- throw parseS3Error("AbortMultipartUpload", resp.status, resp.statusText, decodeText(resp.body));
826
- }
827
- }
828
- /**
829
- * List the parts already uploaded for an in-progress upload (ListParts).
830
- * Returns a single page; follow `nextPartNumberMarker` (when `isTruncated`)
831
- * to page through the rest.
832
- *
833
- * @param key The upload's object key.
834
- * @param uploadId The id from {@link create}.
835
- * @param opts `maxParts` / `partNumberMarker` pagination controls.
836
- */
837
- async listParts(key, uploadId, opts = {}) {
838
- const query = { uploadId };
839
- if (opts.maxParts !== void 0) query["max-parts"] = opts.maxParts;
840
- if (opts.partNumberMarker !== void 0) query["part-number-marker"] = opts.partNumberMarker;
841
- const resp = await this.s3Request("GET", key, { query });
842
- if (!resp.ok) {
843
- throw parseS3Error("ListParts", resp.status, resp.statusText, decodeText(resp.body));
844
- }
845
- return parseListPartsResult(decodeText(resp.body));
846
- }
847
- /**
848
- * List in-progress multipart uploads on the disk (ListMultipartUploads).
849
- * Returns a single page; follow `nextKeyMarker` / `nextUploadIdMarker` (when
850
- * `isTruncated`) for the rest.
851
- *
852
- * @param opts Prefix/delimiter filter and pagination markers.
853
- */
854
- async listUploads(opts = {}) {
855
- const query = { uploads: "" };
856
- if (opts.prefix !== void 0) query.prefix = opts.prefix;
857
- if (opts.delimiter !== void 0) query.delimiter = opts.delimiter;
858
- if (opts.keyMarker !== void 0) query["key-marker"] = opts.keyMarker;
859
- if (opts.uploadIdMarker !== void 0) query["upload-id-marker"] = opts.uploadIdMarker;
860
- if (opts.maxUploads !== void 0) query["max-uploads"] = opts.maxUploads;
861
- const resp = await this.s3Request("GET", "", { query });
862
- if (!resp.ok) {
863
- throw parseS3Error("ListMultipartUploads", resp.status, resp.statusText, decodeText(resp.body));
864
- }
865
- return parseListMultipartUploadsResult(decodeText(resp.body));
866
- }
867
- };
868
- var MAX_DELETE_OBJECTS_PER_REQUEST = 1e3;
869
- var MAX_S3_RETRIES = 3;
870
- var S3_RETRY_BASE_MS = 100;
871
- var S3_RETRY_CAP_MS = 2e3;
872
- function isTransientS3Status(status) {
873
- return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
874
- }
875
- function s3RetryDelayMs(attempt) {
876
- const ceiling = Math.min(S3_RETRY_CAP_MS, S3_RETRY_BASE_MS * 2 ** attempt);
877
- return Math.random() * ceiling;
878
- }
879
- function sleep(ms) {
880
- return new Promise((resolve) => setTimeout(resolve, ms));
881
- }
882
- var MIN_PART_SIZE = 5 * 1024 * 1024;
883
- var DEFAULT_PART_SIZE = 16 * 1024 * 1024;
884
- var DEFAULT_UPLOAD_CONCURRENCY = 4;
885
- var MAX_PARTS_PER_UPLOAD = 1e4;
886
- function effectiveUploadPartSize(totalBytes, requestedPartSize) {
887
- if (Math.ceil(totalBytes / requestedPartSize) <= MAX_PARTS_PER_UPLOAD) {
888
- return requestedPartSize;
889
- }
890
- const MiB = 1024 * 1024;
891
- const needed = Math.ceil(totalBytes / MAX_PARTS_PER_UPLOAD);
892
- return Math.ceil(needed / MiB) * MiB;
893
- }
894
- function toBytes(body) {
895
- if (typeof body === "string") return new TextEncoder().encode(body);
896
- if (body instanceof Uint8Array) return body;
897
- return new Uint8Array(body);
898
- }
899
- function escapeXml(value) {
900
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
901
- }
902
- function buildDeleteObjectsXml(keys2, quiet) {
903
- const objects = keys2.map((k) => `<Object><Key>${escapeXml(k)}</Key></Object>`).join("");
904
- const quietTag = quiet ? "<Quiet>true</Quiet>" : "";
905
- return `<?xml version="1.0" encoding="UTF-8"?><Delete>${objects}${quietTag}</Delete>`;
906
- }
907
- function buildCompleteMultipartUploadXml(parts) {
908
- const body = parts.map(
909
- (p) => `<Part><PartNumber>${p.partNumber}</PartNumber><ETag>${escapeXml(p.etag)}</ETag></Part>`
910
- ).join("");
911
- return `<?xml version="1.0" encoding="UTF-8"?><CompleteMultipartUpload>${body}</CompleteMultipartUpload>`;
912
- }
913
- function asArray(value) {
914
- if (value === void 0 || value === null) return [];
915
- return Array.isArray(value) ? value : [value];
916
- }
917
- function optionalNumber(value) {
918
- return value === void 0 || value === null ? void 0 : Number(value);
919
- }
920
- function optionalString(value) {
921
- return value === void 0 || value === null ? void 0 : String(value);
922
- }
923
- function optionalDate(value) {
924
- return value === void 0 || value === null ? void 0 : new Date(String(value));
925
- }
926
- function decodeText(bytes) {
927
- return new TextDecoder().decode(bytes);
928
- }
929
- function parseListObjectsResult(xml) {
930
- const root = parseXml(xml).ListBucketResult ?? {};
931
- const contents = root.Contents ?? [];
932
- const objects = contents.map((c) => ({
933
- key: String(c.Key ?? ""),
934
- size: Number(c.Size ?? 0),
935
- etag: optionalString(c.ETag),
936
- lastModified: optionalDate(c.LastModified)
937
- }));
938
- const commonPrefixes = (root.CommonPrefixes ?? []).map((cp) => optionalString(cp.Prefix)).filter((p) => p !== void 0);
939
- return {
940
- objects,
941
- commonPrefixes,
942
- isTruncated: root.IsTruncated === "true" || root.IsTruncated === true,
943
- nextContinuationToken: optionalString(root.NextContinuationToken),
944
- keyCount: root.KeyCount !== void 0 ? Number(root.KeyCount) : objects.length,
945
- prefix: optionalString(root.Prefix)
946
- };
947
- }
948
- function xmlIsTruncated(value) {
949
- return value === "true" || value === true;
950
- }
951
- function parseDeleteObjectsResult(xml) {
952
- const root = parseXml(xml).DeleteResult ?? {};
953
- const deleted = asArray(root.Deleted).map((d) => optionalString(d.Key)).filter((k) => k !== void 0);
954
- const errors = asArray(root.Error).map((e) => ({
955
- key: String(e.Key ?? ""),
956
- code: optionalString(e.Code),
957
- message: optionalString(e.Message)
958
- }));
959
- return { deleted, errors };
960
- }
961
- function parseListPartsResult(xml) {
962
- const root = parseXml(xml).ListPartsResult ?? {};
963
- const parts = asArray(root.Part).map((p) => ({
964
- partNumber: Number(p.PartNumber ?? 0),
965
- etag: optionalString(p.ETag),
966
- size: Number(p.Size ?? 0),
967
- lastModified: optionalDate(p.LastModified)
968
- }));
969
- return {
970
- bucket: optionalString(root.Bucket),
971
- key: optionalString(root.Key),
972
- uploadId: optionalString(root.UploadId),
973
- parts,
974
- isTruncated: xmlIsTruncated(root.IsTruncated),
975
- partNumberMarker: Number(root.PartNumberMarker ?? 0),
976
- nextPartNumberMarker: optionalNumber(root.NextPartNumberMarker),
977
- maxParts: Number(root.MaxParts ?? parts.length)
978
- };
979
- }
980
- function parseListMultipartUploadsResult(xml) {
981
- const root = parseXml(xml).ListMultipartUploadsResult ?? {};
982
- const uploads = asArray(root.Upload).map((u) => ({
983
- key: String(u.Key ?? ""),
984
- uploadId: String(u.UploadId ?? ""),
985
- initiated: optionalDate(u.Initiated)
986
- }));
987
- const commonPrefixes = asArray(root.CommonPrefixes).map((cp) => optionalString(cp.Prefix)).filter((p) => p !== void 0);
988
- return {
989
- bucket: optionalString(root.Bucket),
990
- uploads,
991
- commonPrefixes,
992
- isTruncated: xmlIsTruncated(root.IsTruncated),
993
- keyMarker: optionalString(root.KeyMarker),
994
- uploadIdMarker: optionalString(root.UploadIdMarker),
995
- nextKeyMarker: optionalString(root.NextKeyMarker),
996
- nextUploadIdMarker: optionalString(root.NextUploadIdMarker),
997
- prefix: optionalString(root.Prefix),
998
- delimiter: optionalString(root.Delimiter),
999
- maxUploads: optionalNumber(root.MaxUploads)
1000
- };
1001
- }
1002
-
1003
- // src/disks.ts
1004
- var Disks = class {
1005
- /** @internal */
1006
- _client;
1007
- /** @internal */
1008
- _region;
1009
- /** @internal */
1010
- _s3BaseUrl;
1011
- /** @internal */
1012
- constructor(client, region, s3BaseUrl) {
1013
- this._client = client;
1014
- this._region = region;
1015
- this._s3BaseUrl = s3BaseUrl;
1016
- }
1017
- async list(opts) {
1018
- const data = await unwrap(
1019
- this._client.GET("/api/disks", {
1020
- params: { query: { limit: opts?.limit, cursor: opts?.cursor, name: opts?.name } }
1021
- })
1022
- );
1023
- return data.map(
1024
- (d) => new Disk(d, this._client, this._region, this._s3BaseUrl)
1025
- );
1026
- }
1027
- async get(id) {
1028
- const data = await unwrap(
1029
- this._client.GET("/api/disks/{id}", {
1030
- params: { path: { id } }
1031
- })
1032
- );
1033
- return new Disk(data, this._client, this._region, this._s3BaseUrl);
1034
- }
1035
- /**
1036
- * Create a new disk with an auto-generated mount token.
1037
- *
1038
- * Returns the Disk, the one-time token (save it — it cannot be retrieved
1039
- * again), and the token identifier for later management.
1040
- */
1041
- async create(req) {
1042
- const created = await unwrap(
1043
- this._client.POST("/api/disks", { body: req })
1044
- );
1045
- const resp = created;
1046
- if (!resp.diskId) {
1047
- throw new Error("API returned success but no diskId");
1048
- }
1049
- const authorizedUsers = resp.authorizedUsers ?? [];
1050
- const tokenUser = authorizedUsers.find((u) => u.token);
1051
- const disk = await this.get(resp.diskId);
1052
- return {
1053
- disk,
1054
- token: tokenUser?.token ?? null,
1055
- tokenIdentifier: tokenUser?.identifier ?? null,
1056
- authorizedUsers
1057
- };
1058
- }
1059
- };
1060
-
1061
- // src/tokens.ts
1062
- var Tokens = class {
1063
- /** @internal */
1064
- _client;
1065
- /** @internal */
1066
- constructor(client) {
1067
- this._client = client;
1068
- }
1069
- async list(opts) {
1070
- const data = await unwrap(
1071
- this._client.GET("/api/tokens", {
1072
- params: { query: { limit: opts?.limit, cursor: opts?.cursor } }
1073
- })
1074
- );
1075
- return data.tokens ?? [];
1076
- }
1077
- async create(req) {
1078
- const data = await unwrap(
1079
- this._client.POST("/api/tokens", { body: req })
1080
- );
1081
- return data;
1082
- }
1083
- async delete(id) {
1084
- await unwrapEmpty(
1085
- this._client.DELETE("/api/tokens/{id}", {
1086
- params: { path: { id } }
1087
- })
1088
- );
1089
- }
1090
- };
1091
-
1092
- // src/archil.ts
1093
- function envVar(name) {
1094
- return typeof process !== "undefined" && process.env ? process.env[name] : void 0;
1095
- }
1096
- function isExecMountSpec(m) {
1097
- return typeof m === "object" && m !== null && "disk" in m;
1098
- }
1099
- function diskIdFromMount(m) {
1100
- return typeof m === "string" ? m : m.id;
1101
- }
1102
- var Archil = class {
1103
- disks;
1104
- tokens;
1105
- /** @internal */
1106
- _client;
1107
- constructor(opts = {}) {
1108
- const apiKey = opts.apiKey ?? envVar("ARCHIL_API_KEY");
1109
- const region = opts.region ?? envVar("ARCHIL_REGION");
1110
- if (!apiKey) {
1111
- throw new Error("Missing API key: pass apiKey in options or set ARCHIL_API_KEY environment variable");
1112
- }
1113
- if (!region) {
1114
- throw new Error("Missing region: pass region in options or set ARCHIL_REGION environment variable");
1115
- }
1116
- const controlBaseUrl = opts.baseUrl ?? resolveBaseUrl(region);
1117
- const client = createApiClient({
1118
- apiKey,
1119
- region,
1120
- baseUrl: controlBaseUrl
1121
- });
1122
- const s3BaseUrl = opts.s3BaseUrl ?? envVar("ARCHIL_S3_BASE_URL") ?? deriveS3BaseUrl(controlBaseUrl);
1123
- this._client = client;
1124
- this.disks = new Disks(client, region, s3BaseUrl);
1125
- this.tokens = new Tokens(client);
1126
- }
1127
- /**
1128
- * Run a command in a container with multiple disks mounted simultaneously,
1129
- * each at its own relative path under `/mnt/archil`. Blocks until the
1130
- * command completes and returns its stdout, stderr, exit code, and timing.
1131
- */
1132
- async exec(opts) {
1133
- const disks = {};
1134
- for (const [relPath, mount] of Object.entries(opts.disks)) {
1135
- if (isExecMountSpec(mount)) {
1136
- const entry = {
1137
- disk: diskIdFromMount(mount.disk),
1138
- readOnly: mount.readOnly ?? false
1139
- };
1140
- if (mount.subdirectory !== void 0) entry.subdirectory = mount.subdirectory;
1141
- disks[relPath] = entry;
1142
- } else {
1143
- disks[relPath] = diskIdFromMount(mount);
1144
- }
1145
- }
1146
- return unwrap(
1147
- this._client.POST("/api/exec", {
1148
- body: { disks, command: opts.command }
1149
- })
1150
- );
1151
- }
1152
- };
1153
-
1154
- // src/index.ts
1155
- var _options;
1156
- var _instance;
1157
- function configure(options) {
1158
- _options = options;
1159
- _instance = void 0;
1160
- }
1161
- function archil() {
1162
- if (!_instance) {
1163
- _instance = new Archil(_options);
1164
- }
1165
- return _instance;
1166
- }
1167
- function createDisk(req) {
1168
- return archil().disks.create(req);
1169
- }
1170
- function listDisks(opts) {
1171
- return archil().disks.list(opts);
1172
- }
1173
- function getDisk(id) {
1174
- return archil().disks.get(id);
1175
- }
1176
- function listApiKeys(opts) {
1177
- return archil().tokens.list(opts);
1178
- }
1179
- function createApiKey(req) {
1180
- return archil().tokens.create(req);
1181
- }
1182
- function deleteApiKey(id) {
1183
- return archil().tokens.delete(id);
1184
- }
1185
-
1186
- // bin/cli.ts
1187
- var pkg = createRequire(import.meta.url)("../package.json");
1188
- var program = new Command();
1189
- program.name("disk").description("Manage Archil disks from the command line").version(pkg.version).addOption(new Option("-k, --api-key <key>", "Archil API key").env("ARCHIL_API_KEY")).addOption(new Option("-r, --region <region>", "Archil region").env("ARCHIL_REGION")).addOption(new Option("--base-url <url>", "Override control-plane base URL")).hook("preAction", () => {
1190
- const opts = program.opts();
1191
- try {
1192
- configure({ apiKey: opts.apiKey, region: opts.region, baseUrl: opts.baseUrl });
1193
- } catch (err) {
1194
- fail(err);
1195
- }
1196
- });
1197
- function fail(err) {
1198
- if (err instanceof ArchilApiError) {
1199
- console.error(`Error (${err.status}): ${err.message}`);
1200
- } else {
1201
- console.error(err instanceof Error ? err.message : String(err));
1202
- }
1203
- process.exit(1);
1204
- }
1205
- function isDiskId(s) {
1206
- return /^(dsk-|fs-)[0-9a-fA-F]+$/.test(s);
1207
- }
1208
- async function resolveDisk(idOrName) {
1209
- if (isDiskId(idOrName)) {
1210
- return getDisk(idOrName);
1211
- }
1212
- const matches = await listDisks({ name: idOrName });
1213
- if (matches.length === 0) {
1214
- throw new ArchilApiError(`No disk found with name '${idOrName}'`, 404);
1215
- }
1216
- if (matches.length > 1) {
1217
- throw new ArchilApiError(
1218
- `Multiple disks match name '${idOrName}' \u2014 pass a disk id (dsk-...) instead`,
1219
- 400
1220
- );
1221
- }
1222
- return matches[0];
1223
- }
1224
- function formatBytes(n) {
1225
- if (n === void 0 || n === null) return void 0;
1226
- if (n < 1024) return `${n} B`;
1227
- const units = ["KB", "MB", "GB", "TB", "PB"];
1228
- let v = n / 1024;
1229
- let i = 0;
1230
- while (v >= 1024 && i < units.length - 1) {
1231
- v /= 1024;
1232
- i++;
1233
- }
1234
- return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${units[i]}`;
1235
- }
1236
- function renderTable(rows, headers) {
1237
- const all = headers ? [headers, ...rows] : rows;
1238
- if (all.length === 0) return "";
1239
- const cols = Math.max(...all.map((r) => r.length));
1240
- const widths = [];
1241
- for (let i = 0; i < cols; i++) {
1242
- widths[i] = Math.max(...all.map((r) => (r[i] ?? "").length));
1243
- }
1244
- const bar = (l, m, r) => l + widths.map((w) => "\u2500".repeat(w + 2)).join(m) + r;
1245
- const line = (r) => "\u2502 " + widths.map((w, i) => (r[i] ?? "").padEnd(w)).join(" \u2502 ") + " \u2502";
1246
- const out = [bar("\u256D", "\u252C", "\u256E")];
1247
- if (headers) {
1248
- out.push(line(headers));
1249
- out.push(bar("\u251C", "\u253C", "\u2524"));
1250
- }
1251
- for (const r of rows) out.push(line(r));
1252
- out.push(bar("\u2570", "\u2534", "\u256F"));
1253
- return out.join("\n");
1254
- }
1255
- function section(title, body) {
1256
- console.log("");
1257
- console.log(title);
1258
- console.log(body);
1259
- }
1260
- function printDisk(d) {
1261
- console.log(`${d.organization}/${d.name} (${d.id})`);
1262
- const kv = [
1263
- ["status", d.status],
1264
- ["provider", d.provider],
1265
- ["region", d.region],
1266
- ["created", d.createdAt],
1267
- ["last accessed", d.lastAccessed],
1268
- ["data size", formatBytes(d.dataSize)],
1269
- ["monthly usage", d.monthlyUsage]
1270
- ];
1271
- const visible = kv.filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => [k, String(v)]);
1272
- console.log("");
1273
- console.log(renderTable(visible));
1274
- if (d.mounts && d.mounts.length > 0) {
1275
- const rows = d.mounts.map((m) => [
1276
- m.type ?? "?",
1277
- m.name ?? m.path ?? "",
1278
- m.accessMode ?? ""
1279
- ]);
1280
- section("Mounts", renderTable(rows, ["type", "location", "mode"]));
1281
- }
1282
- if (d.authorizedUsers && d.authorizedUsers.length > 0) {
1283
- const rows = d.authorizedUsers.map((u) => {
1284
- if (u.type === "token") {
1285
- return ["token", u.nickname ?? "(no name)", u.tokenSuffix ? `-${u.tokenSuffix}` : ""];
1286
- }
1287
- return [u.type ?? "?", u.identifier ?? u.principal ?? "", ""];
1288
- });
1289
- section("Authorized users", renderTable(rows, ["type", "name / identifier", "suffix"]));
1290
- }
1291
- if (d.connectedClients && d.connectedClients.length > 0) {
1292
- const rows = d.connectedClients.map((c) => [
1293
- c.id ?? "",
1294
- c.ipAddress ?? "",
1295
- c.connectedAt ?? ""
1296
- ]);
1297
- section("Connected clients", renderTable(rows, ["id", "ip", "connected at"]));
1298
- }
1299
- }
1300
- program.command("list").description("List disks in the current region").option("--limit <n>", "Maximum number of disks to return", (v) => parseInt(v, 10)).option("-o, --output <format>", "Output format: table | json", "table").action(async (opts) => {
1301
- try {
1302
- const disks = await listDisks({ limit: opts.limit });
1303
- if (opts.output === "json") {
1304
- console.log(JSON.stringify(disks, null, 2));
1305
- return;
1306
- }
1307
- if (disks.length === 0) {
1308
- console.log("No disks found.");
1309
- return;
1310
- }
1311
- const rows = disks.map((d) => [d.id, `${d.organization}/${d.name}`, d.status]);
1312
- console.log(renderTable(rows, ["id", "name", "status"]));
1313
- } catch (err) {
1314
- fail(err);
1315
- }
1316
- });
1317
- program.command("get <id|name>").description("Show details for a disk (accepts a disk id or name)").option("-o, --output <format>", "Output format: table | json", "table").action(async (idOrName, opts) => {
1318
- try {
1319
- const d = await resolveDisk(idOrName);
1320
- if (opts.output === "json") {
1321
- console.log(JSON.stringify(d, null, 2));
1322
- } else {
1323
- printDisk(d);
1324
- }
1325
- } catch (err) {
1326
- fail(err);
1327
- }
1328
- });
1329
- program.command("create <name>").description("Create a new disk").action(async (name) => {
1330
- try {
1331
- const result = await createDisk({ name });
1332
- console.log(`Created disk ${result.disk.organization}/${result.disk.name}`);
1333
- console.log(` id: ${result.disk.id}`);
1334
- console.log(` status: ${result.disk.status}`);
1335
- if (result.token) {
1336
- console.log("");
1337
- console.log("Disk token (save this \u2014 it cannot be retrieved again):");
1338
- console.log(` ${result.token}`);
1339
- if (result.tokenIdentifier) {
1340
- console.log(` identifier: ${result.tokenIdentifier}`);
1341
- }
1342
- }
1343
- } catch (err) {
1344
- fail(err);
1345
- }
1346
- });
1347
- program.command("delete <id|name>").description("Delete a disk (accepts a disk id or name)").action(async (idOrName) => {
1348
- try {
1349
- const d = await resolveDisk(idOrName);
1350
- await d.delete();
1351
- console.log(`Deleted ${d.organization}/${d.name} (${d.id})`);
1352
- } catch (err) {
1353
- fail(err);
1354
- }
1355
- });
1356
- program.command("exec <id|name> <command...>").description("Run a command in a container with the disk mounted, return stdout/stderr/exit code (accepts a disk id or name)").action(async (idOrName, cmd) => {
1357
- try {
1358
- const d = await resolveDisk(idOrName);
1359
- const result = await d.exec(cmd.join(" "));
1360
- if (result.stdout) process.stdout.write(result.stdout);
1361
- if (result.stderr) process.stderr.write(result.stderr);
1362
- process.exit(result.exitCode);
1363
- } catch (err) {
1364
- fail(err);
1365
- }
1366
- });
1367
- program.command("grep <pattern> <target>").description(
1368
- "Constant-time parallel server-side grep across a disk. <target> is <id|name>[/<path>] \u2014 the path after the first '/' is the directory to search (defaults to the disk root). Accepts a disk id or name."
1369
- ).option("--no-recursive", "Search only the given directory, not its subdirectories (recursive is the default)").option("--max-duration <seconds>", "Wall-clock deadline for the whole search", (v) => parseInt(v, 10)).option("--concurrency <n>", "Max parallel grep workers (clamped to fleet capacity)", (v) => parseInt(v, 10)).option("--max-results <n>", "Stop once this many matches are collected", (v) => parseInt(v, 10)).option("-o, --output <format>", "Output format: text | json", "text").action(
1370
- async (pattern, target, opts) => {
1371
- try {
1372
- const slash = target.indexOf("/");
1373
- const idOrName = slash === -1 ? target : target.slice(0, slash);
1374
- const directory = slash === -1 ? "" : target.slice(slash + 1);
1375
- const d = await resolveDisk(idOrName);
1376
- const res = await d.grep({
1377
- directory,
1378
- pattern,
1379
- recursive: opts.recursive !== false,
1380
- maxDurationSeconds: opts.maxDuration,
1381
- concurrency: opts.concurrency,
1382
- maxResults: opts.maxResults
1383
- });
1384
- if (opts.output === "json") {
1385
- console.log(JSON.stringify(res, null, 2));
1386
- } else {
1387
- for (const m of res.matches) {
1388
- console.log(`${m.file}:${m.line}:${m.text}`);
1389
- }
1390
- console.error(
1391
- `${res.matches.length} match${res.matches.length === 1 ? "" : "es"} \xB7 ${res.filesScanned} files scanned \xB7 ${res.containersDispatched} containers \xB7 ${res.durationMs}ms \xB7 ${res.stoppedReason}`
1392
- );
1393
- if (res.stoppedReason !== "completed") {
1394
- console.error(
1395
- `warning: search did not complete (${res.stoppedReason}); results may be partial`
1396
- );
1397
- }
1398
- }
1399
- process.exit(res.matches.length > 0 ? 0 : 1);
1400
- } catch (err) {
1401
- fail(err);
1402
- }
1403
- }
1404
- );
1405
- var keys = program.command("api-keys").description("Manage Archil API keys (account-level credentials)");
1406
- keys.command("list").description("List API keys").option("--limit <n>", "Maximum number of keys to return", (v) => parseInt(v, 10)).action(async (opts) => {
1407
- try {
1408
- const ks = await listApiKeys({ limit: opts.limit });
1409
- if (ks.length === 0) {
1410
- console.log("No API keys found.");
1411
- return;
1412
- }
1413
- for (const k of ks) {
1414
- console.log(`${k.id ?? "-"} ${k.name ?? "-"} ${k.createdAt ?? ""}`);
1415
- }
1416
- } catch (err) {
1417
- fail(err);
1418
- }
1419
- });
1420
- keys.command("create <name>").description("Create a new API key (the key value is shown once)").option("--description <description>", "Optional description").action(async (name, opts) => {
1421
- try {
1422
- const k = await createApiKey({ name, description: opts.description });
1423
- console.log(`Created API key ${name} (${k.id ?? "?"})`);
1424
- if (k.token) {
1425
- console.log("");
1426
- console.log("API key value (save this \u2014 it cannot be retrieved again):");
1427
- console.log(` ${k.token}`);
1428
- }
1429
- } catch (err) {
1430
- fail(err);
1431
- }
1432
- });
1433
- keys.command("delete <id>").description("Delete an API key").action(async (id) => {
1434
- try {
1435
- await deleteApiKey(id);
1436
- console.log(`Deleted API key ${id}`);
1437
- } catch (err) {
1438
- fail(err);
1439
- }
1440
- });
1441
- function rewriteSugar(argv) {
1442
- const known = /* @__PURE__ */ new Set(["list", "get", "create", "delete", "exec", "grep", "api-keys", "help", "--help", "-h", "--version", "-V"]);
1443
- const head = argv[2];
1444
- const next = argv[3];
1445
- if (head && next === "exec" && !known.has(head) && !head.startsWith("-")) {
1446
- return [...argv.slice(0, 2), "exec", head, ...argv.slice(4)];
1447
- }
1448
- return argv;
1449
- }
1450
- program.parseAsync(rewriteSugar(process.argv));