@lotics/cli 0.185.0 → 0.186.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/dist/src/cli.js +174 -10
- package/dist/src/client.d.ts +1 -0
- package/dist/src/client.js +1032 -891
- package/docs/cli_reference.md +1 -1
- package/package.json +1 -1
- package/dist/src/invocation.js +0 -142
package/dist/src/cli.js
CHANGED
|
@@ -44182,6 +44182,66 @@ function transportErrorMessage(status, parsed) {
|
|
|
44182
44182
|
return status < 500 || authored ? jsonMessage : gatewayErrorMessage(status);
|
|
44183
44183
|
}
|
|
44184
44184
|
|
|
44185
|
+
// ../shared/src/multipart_parts.ts
|
|
44186
|
+
var DEFAULT_MAX_RETRIES = 5;
|
|
44187
|
+
var MAX_RETRY_DELAY_MS = 15e3;
|
|
44188
|
+
var TIMEOUT_BASE_MS = 3e4;
|
|
44189
|
+
var TIMEOUT_PER_MB_MS = 15e3;
|
|
44190
|
+
function partTimeoutMs(partSizeBytes) {
|
|
44191
|
+
return TIMEOUT_BASE_MS + Math.ceil(partSizeBytes / (1024 * 1024)) * TIMEOUT_PER_MB_MS;
|
|
44192
|
+
}
|
|
44193
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
44194
|
+
var PermanentPartError = class extends Error {
|
|
44195
|
+
};
|
|
44196
|
+
function isRetryableStatus(status) {
|
|
44197
|
+
return status >= 500 || status === 408 || status === 429;
|
|
44198
|
+
}
|
|
44199
|
+
function partErrorFor(partNumber, status) {
|
|
44200
|
+
const message2 = `part ${partNumber} rejected by storage (${status})`;
|
|
44201
|
+
return isRetryableStatus(status) ? new Error(message2) : new PermanentPartError(message2);
|
|
44202
|
+
}
|
|
44203
|
+
async function uploadParts(options) {
|
|
44204
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
44205
|
+
const concurrency = Math.max(1, options.concurrency ?? 1);
|
|
44206
|
+
const done = [...options.alreadyUploaded ?? []];
|
|
44207
|
+
const finished = new Set(done.map((part) => part.part_number));
|
|
44208
|
+
const queue = options.parts.filter((part) => !finished.has(part.part_number));
|
|
44209
|
+
const uploadOne = async (part) => {
|
|
44210
|
+
const bytes = options.partBytes(part);
|
|
44211
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
44212
|
+
options.signal?.throwIfAborted();
|
|
44213
|
+
try {
|
|
44214
|
+
const timeout = AbortSignal.timeout(partTimeoutMs(bytes));
|
|
44215
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
44216
|
+
const etag = await options.putPart(part, await options.readPart(part), signal);
|
|
44217
|
+
if (!etag) {
|
|
44218
|
+
throw new PermanentPartError(`part ${part.part_number} stored without an ETag`);
|
|
44219
|
+
}
|
|
44220
|
+
return { part_number: part.part_number, etag };
|
|
44221
|
+
} catch (error52) {
|
|
44222
|
+
if (options.signal?.aborted) throw error52;
|
|
44223
|
+
if (error52 instanceof PermanentPartError) throw error52;
|
|
44224
|
+
if (attempt >= maxRetries) {
|
|
44225
|
+
options.onExhausted?.({ partNumber: part.part_number, attempts: attempt, error: error52 });
|
|
44226
|
+
throw error52;
|
|
44227
|
+
}
|
|
44228
|
+
options.onRetry?.({ partNumber: part.part_number, attempt, error: error52 });
|
|
44229
|
+
const base = 1e3 * 2 ** (attempt - 1);
|
|
44230
|
+
await sleep(Math.min(base + Math.random() * base * 0.5, MAX_RETRY_DELAY_MS));
|
|
44231
|
+
}
|
|
44232
|
+
}
|
|
44233
|
+
};
|
|
44234
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
44235
|
+
for (let next = queue.shift(); next !== void 0; next = queue.shift()) {
|
|
44236
|
+
const part = await uploadOne(next);
|
|
44237
|
+
done.push(part);
|
|
44238
|
+
options.onPartUploaded?.(part);
|
|
44239
|
+
}
|
|
44240
|
+
});
|
|
44241
|
+
await Promise.all(workers);
|
|
44242
|
+
return done.sort((a, b) => a.part_number - b.part_number);
|
|
44243
|
+
}
|
|
44244
|
+
|
|
44185
44245
|
// src/client.ts
|
|
44186
44246
|
import crypto from "node:crypto";
|
|
44187
44247
|
import fs2 from "node:fs";
|
|
@@ -44301,6 +44361,7 @@ function getMimeType(filename) {
|
|
|
44301
44361
|
const ext = path2.extname(filename).toLowerCase();
|
|
44302
44362
|
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
44303
44363
|
}
|
|
44364
|
+
var MULTIPART_THRESHOLD_BYTES = 8 * 1024 * 1024;
|
|
44304
44365
|
var API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai";
|
|
44305
44366
|
var WEB_APP_URL = "https://lotics.ai";
|
|
44306
44367
|
async function fetchOfficialStarters() {
|
|
@@ -45077,15 +45138,118 @@ var LoticsClient = class {
|
|
|
45077
45138
|
);
|
|
45078
45139
|
}
|
|
45079
45140
|
async uploadFiles(filePaths, options) {
|
|
45080
|
-
const
|
|
45141
|
+
const small = [];
|
|
45142
|
+
const large = [];
|
|
45081
45143
|
for (let i2 = 0; i2 < filePaths.length; i2++) {
|
|
45082
45144
|
const absolutePath = path2.resolve(filePaths[i2]);
|
|
45083
|
-
|
|
45084
|
-
|
|
45085
|
-
|
|
45145
|
+
const filename = options?.filenames?.[i2] ?? path2.basename(absolutePath);
|
|
45146
|
+
const { size: size2 } = await fs2.promises.stat(absolutePath);
|
|
45147
|
+
if (size2 >= MULTIPART_THRESHOLD_BYTES) {
|
|
45148
|
+
large.push({ absolutePath, filename, size: size2 });
|
|
45149
|
+
} else {
|
|
45150
|
+
small.push({ bytes: await fs2.promises.readFile(absolutePath), filename });
|
|
45151
|
+
}
|
|
45152
|
+
}
|
|
45153
|
+
const files = [];
|
|
45154
|
+
const errors2 = [];
|
|
45155
|
+
if (small.length > 0) {
|
|
45156
|
+
const result = await this.uploadFileBytes(small);
|
|
45157
|
+
files.push(...result.files);
|
|
45158
|
+
errors2.push(...result.errors);
|
|
45159
|
+
}
|
|
45160
|
+
for (const item of large) {
|
|
45161
|
+
try {
|
|
45162
|
+
files.push(await this.uploadLargeFile(item));
|
|
45163
|
+
} catch (error52) {
|
|
45164
|
+
errors2.push({
|
|
45165
|
+
filename: item.filename,
|
|
45166
|
+
error: error52 instanceof Error ? error52.message : String(error52)
|
|
45167
|
+
});
|
|
45168
|
+
}
|
|
45169
|
+
}
|
|
45170
|
+
return { files, errors: errors2 };
|
|
45171
|
+
}
|
|
45172
|
+
/**
|
|
45173
|
+
* Store one file by handing its bytes straight to object storage.
|
|
45174
|
+
*
|
|
45175
|
+
* The API mints presigned part URLs and registers the row; the bytes go
|
|
45176
|
+
* directly from disk to the bucket and never enter the API process. That is
|
|
45177
|
+
* what makes the ceiling "what a workspace may store" rather than "what one
|
|
45178
|
+
* request may cost the server" — the reason `POST /v1/files` and the ticketed
|
|
45179
|
+
* upload are both capped far lower, and the reason this exists at all.
|
|
45180
|
+
*
|
|
45181
|
+
* Parts are read one at a time rather than streamed as a request body: a part
|
|
45182
|
+
* is bounded (the server picks 5–10 MiB), a `fetch` body needs a length the
|
|
45183
|
+
* bucket will check anyway, and reading a slice keeps peak memory at one part
|
|
45184
|
+
* instead of one file.
|
|
45185
|
+
*
|
|
45186
|
+
* The PUT loop itself — retry, backoff, part timeout, ordering — is
|
|
45187
|
+
* `@lotics/shared/multipart_parts`, shared with the browser client so the
|
|
45188
|
+
* hardening cannot exist on one side only.
|
|
45189
|
+
*/
|
|
45190
|
+
async uploadLargeFile(input) {
|
|
45191
|
+
const mimeType = getMimeType(input.filename);
|
|
45192
|
+
const init = await this.request("POST", "/v1/files/multipart/init", {
|
|
45193
|
+
filename: input.filename,
|
|
45194
|
+
mime_type: mimeType,
|
|
45195
|
+
file_size: input.size
|
|
45196
|
+
});
|
|
45197
|
+
const partLength = (part) => Math.min(init.part_size, input.size - (part.part_number - 1) * init.part_size);
|
|
45198
|
+
const handle = await fs2.promises.open(input.absolutePath, "r");
|
|
45199
|
+
let uploaded;
|
|
45200
|
+
try {
|
|
45201
|
+
uploaded = await uploadParts({
|
|
45202
|
+
parts: init.parts,
|
|
45203
|
+
partBytes: partLength,
|
|
45204
|
+
// Sequential. Peak memory is then one part rather than one file, which
|
|
45205
|
+
// is the property that lets this run against a 2 GiB upload at all;
|
|
45206
|
+
// raising it trades that for wall-clock, and no upload has asked yet.
|
|
45207
|
+
concurrency: 1,
|
|
45208
|
+
putPart: async (part, body, signal) => {
|
|
45209
|
+
const response = await fetch(part.url, { method: "PUT", body, signal });
|
|
45210
|
+
if (!response.ok) throw partErrorFor(part.part_number, response.status);
|
|
45211
|
+
return response.headers.get("etag") ?? "";
|
|
45212
|
+
},
|
|
45213
|
+
readPart: async (part) => {
|
|
45214
|
+
const offset = (part.part_number - 1) * init.part_size;
|
|
45215
|
+
const length2 = partLength(part);
|
|
45216
|
+
const buffer = Buffer.alloc(length2);
|
|
45217
|
+
let filled = 0;
|
|
45218
|
+
while (filled < length2) {
|
|
45219
|
+
const { bytesRead } = await handle.read(buffer, filled, length2 - filled, offset + filled);
|
|
45220
|
+
if (bytesRead === 0) {
|
|
45221
|
+
throw new Error(
|
|
45222
|
+
`${input.filename} ended after ${offset + filled} bytes, short of the ${input.size} it reported \u2014 it changed while uploading`
|
|
45223
|
+
);
|
|
45224
|
+
}
|
|
45225
|
+
filled += bytesRead;
|
|
45226
|
+
}
|
|
45227
|
+
return buffer;
|
|
45228
|
+
}
|
|
45229
|
+
});
|
|
45230
|
+
} catch (error52) {
|
|
45231
|
+
await this.request("POST", "/v1/files/multipart/abort", {
|
|
45232
|
+
upload_id: init.upload_id,
|
|
45233
|
+
file_storage_key: init.file_storage_key
|
|
45234
|
+
}).catch(() => {
|
|
45086
45235
|
});
|
|
45236
|
+
throw error52;
|
|
45237
|
+
} finally {
|
|
45238
|
+
await handle.close();
|
|
45239
|
+
}
|
|
45240
|
+
const result = await this.request("POST", "/v1/files/multipart/complete", {
|
|
45241
|
+
file_id: init.file_id,
|
|
45242
|
+
upload_id: init.upload_id,
|
|
45243
|
+
file_storage_key: init.file_storage_key,
|
|
45244
|
+
filename: input.filename,
|
|
45245
|
+
mime_type: mimeType,
|
|
45246
|
+
parts: uploaded
|
|
45247
|
+
});
|
|
45248
|
+
const stored = result.files[0];
|
|
45249
|
+
if (!stored) {
|
|
45250
|
+
throw new Error(result.errors[0]?.error ?? "upload completed but stored no file");
|
|
45087
45251
|
}
|
|
45088
|
-
return
|
|
45252
|
+
return stored;
|
|
45089
45253
|
}
|
|
45090
45254
|
/**
|
|
45091
45255
|
* Store files from bytes the caller already holds — the path for a caller that
|
|
@@ -45711,7 +45875,7 @@ function resultSideEffects(result) {
|
|
|
45711
45875
|
}
|
|
45712
45876
|
|
|
45713
45877
|
// src/version.ts
|
|
45714
|
-
var VERSION = "0.
|
|
45878
|
+
var VERSION = "0.186.0";
|
|
45715
45879
|
|
|
45716
45880
|
// src/timezone.ts
|
|
45717
45881
|
function machineTimezone() {
|
|
@@ -103240,7 +103404,7 @@ import { readFileSync, writeFileSync, existsSync as existsSync2, mkdtempSync, rm
|
|
|
103240
103404
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
103241
103405
|
import { join as join2, dirname, resolve, extname, basename } from "node:path";
|
|
103242
103406
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
103243
|
-
import { setTimeout as
|
|
103407
|
+
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
103244
103408
|
var HERE = dirname(fileURLToPath2(import.meta.url));
|
|
103245
103409
|
function fail3(msg) {
|
|
103246
103410
|
console.error(msg);
|
|
@@ -103372,7 +103536,7 @@ async function runPreviewCommand(filePath, flags) {
|
|
|
103372
103536
|
const p = parseInt(readFileSync(portFile, "utf8").split("\n")[0], 10);
|
|
103373
103537
|
if (p) cdpPort = p;
|
|
103374
103538
|
}
|
|
103375
|
-
if (!cdpPort) await
|
|
103539
|
+
if (!cdpPort) await sleep2(100);
|
|
103376
103540
|
}
|
|
103377
103541
|
if (!cdpPort) throw new Error("Chrome did not expose a debugging port (launch failed?).");
|
|
103378
103542
|
let target;
|
|
@@ -103382,7 +103546,7 @@ async function runPreviewCommand(filePath, flags) {
|
|
|
103382
103546
|
target = list2.find((t) => t.type === "page");
|
|
103383
103547
|
} catch {
|
|
103384
103548
|
}
|
|
103385
|
-
if (!target?.webSocketDebuggerUrl) await
|
|
103549
|
+
if (!target?.webSocketDebuggerUrl) await sleep2(100);
|
|
103386
103550
|
}
|
|
103387
103551
|
if (!target?.webSocketDebuggerUrl) throw new Error("No Chrome page target available.");
|
|
103388
103552
|
const cdp = await cdpConnect(target.webSocketDebuggerUrl);
|
|
@@ -103404,7 +103568,7 @@ async function runPreviewCommand(filePath, flags) {
|
|
|
103404
103568
|
if (v.warnings?.length) warnings.push(...v.warnings);
|
|
103405
103569
|
break;
|
|
103406
103570
|
}
|
|
103407
|
-
await
|
|
103571
|
+
await sleep2(75);
|
|
103408
103572
|
}
|
|
103409
103573
|
if (!done) throw new Error("Render timed out (page never signaled completion).");
|
|
103410
103574
|
if (err2) throw new Error(`Render engine error: ${err2}`);
|
package/dist/src/client.d.ts
CHANGED
|
@@ -968,6 +968,7 @@ export declare class LoticsClient {
|
|
|
968
968
|
uploadFiles(filePaths: string[], options?: {
|
|
969
969
|
filenames?: string[];
|
|
970
970
|
}): Promise<FileUploadResult>;
|
|
971
|
+
private uploadLargeFile;
|
|
971
972
|
/**
|
|
972
973
|
* Store files from bytes the caller already holds — the path for a caller that
|
|
973
974
|
* never had them on disk (an email attachment decoded in memory, a generated
|