@clickraft/cli 0.14.0 → 0.15.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/CHANGELOG.md +7 -4
- package/README.md +19 -0
- package/dist/cli.js +322 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.js +322 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
|
-
import { dirname as
|
|
5
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
8
|
// ../sdk/dist/index.js
|
|
@@ -151,10 +151,10 @@ function parseProductCreateRequest(input) {
|
|
|
151
151
|
{ retryable: false, details: parsed.error.issues }
|
|
152
152
|
);
|
|
153
153
|
}
|
|
154
|
-
const
|
|
154
|
+
const request3 = parsed.data;
|
|
155
155
|
const seen = /* @__PURE__ */ new Set();
|
|
156
156
|
const duplicates = /* @__PURE__ */ new Set();
|
|
157
|
-
for (const img of
|
|
157
|
+
for (const img of request3.images) {
|
|
158
158
|
if (seen.has(img.assetId)) duplicates.add(img.assetId);
|
|
159
159
|
seen.add(img.assetId);
|
|
160
160
|
}
|
|
@@ -165,7 +165,7 @@ function parseProductCreateRequest(input) {
|
|
|
165
165
|
{ retryable: false }
|
|
166
166
|
);
|
|
167
167
|
}
|
|
168
|
-
const bytes = Buffer.byteLength(JSON.stringify(
|
|
168
|
+
const bytes = Buffer.byteLength(JSON.stringify(request3), "utf8");
|
|
169
169
|
if (bytes > MAX_PRODUCT_CREATE_BODY_BYTES) {
|
|
170
170
|
throw new ApiError(
|
|
171
171
|
"INPUT_INVALID_FORMAT",
|
|
@@ -173,7 +173,7 @@ function parseProductCreateRequest(input) {
|
|
|
173
173
|
{ retryable: false }
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
|
-
return
|
|
176
|
+
return request3;
|
|
177
177
|
}
|
|
178
178
|
var TokenRowSchema = z4.object({
|
|
179
179
|
id: z4.string().uuid(),
|
|
@@ -284,6 +284,34 @@ var GenerationResultSchema = z6.object({
|
|
|
284
284
|
startedAt: z6.string().nullable(),
|
|
285
285
|
completedAt: z6.string().nullable()
|
|
286
286
|
}).strict();
|
|
287
|
+
var GenerationListItemSchema = z6.object({
|
|
288
|
+
jobId: z6.string().uuid(),
|
|
289
|
+
status: GenerationStatusSchema,
|
|
290
|
+
modelSlug: z6.string().nullable(),
|
|
291
|
+
contentType: z6.enum(["image", "video", "audio"]),
|
|
292
|
+
prompt: z6.string().nullable(),
|
|
293
|
+
resultUrl: z6.string().nullable(),
|
|
294
|
+
thumbnailUrl: z6.string().nullable(),
|
|
295
|
+
errorCode: z6.string().nullable(),
|
|
296
|
+
errorMessage: z6.string().nullable(),
|
|
297
|
+
creditsCharged: z6.number().int().nonnegative(),
|
|
298
|
+
creditsRefunded: z6.boolean(),
|
|
299
|
+
createdAt: z6.string(),
|
|
300
|
+
completedAt: z6.string().nullable()
|
|
301
|
+
}).strict();
|
|
302
|
+
var GenerationListResponseSchema = z6.object({
|
|
303
|
+
jobs: z6.array(GenerationListItemSchema),
|
|
304
|
+
cursor: z6.string().nullable(),
|
|
305
|
+
hasMore: z6.boolean()
|
|
306
|
+
}).strict();
|
|
307
|
+
var GenerateEstimateResponseSchema = z6.object({
|
|
308
|
+
modelSlug: z6.string(),
|
|
309
|
+
creditCost: z6.number().int().nonnegative(),
|
|
310
|
+
estimatedSeconds: z6.number().int().nonnegative().nullable(),
|
|
311
|
+
balance: z6.number().nullable(),
|
|
312
|
+
affordable: z6.boolean(),
|
|
313
|
+
blockedReason: z6.enum(["insufficient_credits", "feature_disabled", "subscription_required", "past_due"]).nullable()
|
|
314
|
+
}).strict();
|
|
287
315
|
var MAX_UPLOAD_SIZE_BYTES = 15 * 1024 * 1024;
|
|
288
316
|
var UploadRequestSchema = z7.object({
|
|
289
317
|
filename: z7.string().min(1).max(255),
|
|
@@ -536,10 +564,10 @@ async function sleep(delayMs, signal) {
|
|
|
536
564
|
if (signal?.aborted) {
|
|
537
565
|
throw signalToAbortError(signal);
|
|
538
566
|
}
|
|
539
|
-
await new Promise((
|
|
567
|
+
await new Promise((resolve2, reject) => {
|
|
540
568
|
const timer = setTimeout(() => {
|
|
541
569
|
signal?.removeEventListener("abort", onAbort);
|
|
542
|
-
|
|
570
|
+
resolve2();
|
|
543
571
|
}, delayMs);
|
|
544
572
|
const onAbort = () => {
|
|
545
573
|
clearTimeout(timer);
|
|
@@ -1087,7 +1115,8 @@ async function createGeneration(params) {
|
|
|
1087
1115
|
path: "/jobs",
|
|
1088
1116
|
body: params.body,
|
|
1089
1117
|
responseSchema: params.createSchema,
|
|
1090
|
-
signal: params.signal
|
|
1118
|
+
signal: params.signal,
|
|
1119
|
+
idempotencyKey: params.idempotencyKey
|
|
1091
1120
|
});
|
|
1092
1121
|
if (!params.wait) {
|
|
1093
1122
|
return params.projectCreated(created, params.modelSlug);
|
|
@@ -2188,8 +2217,7 @@ async function generateWait(opts) {
|
|
|
2188
2217
|
|
|
2189
2218
|
// src/commands/generate/create.ts
|
|
2190
2219
|
var PREFLIGHT_PLACEHOLDER_URL = "https://placeholder.invalid/pending-upload";
|
|
2191
|
-
async function
|
|
2192
|
-
const client = opts.client ?? await buildGenerateClient(opts);
|
|
2220
|
+
async function prepareCreateBody(opts, client) {
|
|
2193
2221
|
const preflightRefs = (opts.referenceImages ?? []).map(
|
|
2194
2222
|
(v) => isUrlLike(v) ? v : PREFLIGHT_PLACEHOLDER_URL
|
|
2195
2223
|
);
|
|
@@ -2205,7 +2233,11 @@ async function generateCreate(opts) {
|
|
|
2205
2233
|
client,
|
|
2206
2234
|
signal: opts.signal
|
|
2207
2235
|
}))[0];
|
|
2208
|
-
|
|
2236
|
+
return buildRequestBody({ ...opts, referenceImages, startFrame });
|
|
2237
|
+
}
|
|
2238
|
+
async function generateCreate(opts) {
|
|
2239
|
+
const client = opts.client ?? await buildGenerateClient(opts);
|
|
2240
|
+
const body = await prepareCreateBody(opts, client);
|
|
2209
2241
|
return createGeneration({
|
|
2210
2242
|
client,
|
|
2211
2243
|
body,
|
|
@@ -2302,6 +2334,87 @@ function projectCreateToResult(response, modelSlug) {
|
|
|
2302
2334
|
};
|
|
2303
2335
|
}
|
|
2304
2336
|
|
|
2337
|
+
// src/commands/generate/download.ts
|
|
2338
|
+
import { createWriteStream } from "fs";
|
|
2339
|
+
import { mkdir as mkdir2, rename as rename2, rm as rm2, stat as stat3 } from "fs/promises";
|
|
2340
|
+
import { basename as basename2, dirname as dirname2, extname, join as join2, resolve, sep } from "path";
|
|
2341
|
+
import { Readable } from "stream";
|
|
2342
|
+
import { pipeline } from "stream/promises";
|
|
2343
|
+
import { request } from "undici";
|
|
2344
|
+
var DOWNLOAD_TIMEOUT_MS = 12e4;
|
|
2345
|
+
async function isDirectory(path) {
|
|
2346
|
+
try {
|
|
2347
|
+
return (await stat3(path)).isDirectory();
|
|
2348
|
+
} catch {
|
|
2349
|
+
return false;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
function extensionOf(url) {
|
|
2353
|
+
const ext = extname(new URL(url).pathname);
|
|
2354
|
+
return /^\.[a-z0-9]{1,5}$/i.test(ext) ? ext.toLowerCase() : "";
|
|
2355
|
+
}
|
|
2356
|
+
async function resolveOutputPath(output, result) {
|
|
2357
|
+
if (!result.resultUrl) {
|
|
2358
|
+
throw new ApiError("INPUT_INVALID_FORMAT", "The job has no result to save.", {
|
|
2359
|
+
retryable: false
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
const asDir = output.endsWith("/") || output.endsWith(sep) || await isDirectory(output);
|
|
2363
|
+
return asDir ? resolve(output, `${result.jobId}${extensionOf(result.resultUrl)}`) : resolve(output);
|
|
2364
|
+
}
|
|
2365
|
+
async function downloadResult(result, output, options = {}) {
|
|
2366
|
+
if (result.status !== "completed" || !result.resultUrl) {
|
|
2367
|
+
throw new ApiError(
|
|
2368
|
+
"INPUT_INVALID_FORMAT",
|
|
2369
|
+
`Job ${result.jobId} is ${result.status}; only a completed job can be saved.`,
|
|
2370
|
+
{ retryable: false }
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
const url = result.resultUrl;
|
|
2374
|
+
if (!url.startsWith("https://") && !url.startsWith("http://")) {
|
|
2375
|
+
throw new ApiError("INPUT_INVALID_FORMAT", `Refusing to download a non-HTTP URL: ${url}`, {
|
|
2376
|
+
retryable: false
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
const target = await resolveOutputPath(output, result);
|
|
2380
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
2381
|
+
const partial = join2(dirname2(target), `.${basename2(target)}.part`);
|
|
2382
|
+
const signals = [AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)];
|
|
2383
|
+
if (options.signal) signals.push(options.signal);
|
|
2384
|
+
const res = await request(url, {
|
|
2385
|
+
method: "GET",
|
|
2386
|
+
signal: AbortSignal.any(signals),
|
|
2387
|
+
dispatcher: options.dispatcher
|
|
2388
|
+
});
|
|
2389
|
+
if (res.statusCode !== 200) {
|
|
2390
|
+
await res.body.dump();
|
|
2391
|
+
throw new ApiError("NETWORK_ERROR", `Could not download the result (HTTP ${res.statusCode}).`, {
|
|
2392
|
+
retryable: res.statusCode >= 500
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
try {
|
|
2396
|
+
await pipeline(Readable.from(res.body), createWriteStream(partial));
|
|
2397
|
+
await rename2(partial, target);
|
|
2398
|
+
} catch (err) {
|
|
2399
|
+
await rm2(partial, { force: true });
|
|
2400
|
+
throw err;
|
|
2401
|
+
}
|
|
2402
|
+
return target;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
// src/commands/generate/estimate.ts
|
|
2406
|
+
async function generateEstimate(opts) {
|
|
2407
|
+
const client = opts.client ?? await buildGenerateClient(opts);
|
|
2408
|
+
const body = await prepareCreateBody(opts, client);
|
|
2409
|
+
return client.request({
|
|
2410
|
+
method: "POST",
|
|
2411
|
+
path: "/jobs/estimate",
|
|
2412
|
+
body,
|
|
2413
|
+
responseSchema: GenerateEstimateResponseSchema,
|
|
2414
|
+
signal: opts.signal
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2305
2418
|
// src/commands/generate/get.ts
|
|
2306
2419
|
async function generateGet(opts) {
|
|
2307
2420
|
const client = opts.client ?? await buildGenerateClient(opts);
|
|
@@ -2314,6 +2427,23 @@ async function generateGet(opts) {
|
|
|
2314
2427
|
});
|
|
2315
2428
|
}
|
|
2316
2429
|
|
|
2430
|
+
// src/commands/generate/list.ts
|
|
2431
|
+
async function generateList(opts) {
|
|
2432
|
+
const client = opts.client ?? await buildGenerateClient(opts);
|
|
2433
|
+
return client.request({
|
|
2434
|
+
method: "GET",
|
|
2435
|
+
path: "/jobs",
|
|
2436
|
+
query: {
|
|
2437
|
+
limit: opts.limit,
|
|
2438
|
+
status: opts.status,
|
|
2439
|
+
contentType: opts.contentType,
|
|
2440
|
+
cursor: opts.cursor
|
|
2441
|
+
},
|
|
2442
|
+
responseSchema: GenerationListResponseSchema,
|
|
2443
|
+
signal: opts.signal
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2317
2447
|
// src/cli/args.ts
|
|
2318
2448
|
var ArgError = class extends Error {
|
|
2319
2449
|
constructor(message) {
|
|
@@ -2500,7 +2630,7 @@ function validateProductSpecs(specs) {
|
|
|
2500
2630
|
|
|
2501
2631
|
// src/auth/device-flow.ts
|
|
2502
2632
|
import { setTimeout as delay } from "timers/promises";
|
|
2503
|
-
import { request } from "undici";
|
|
2633
|
+
import { request as request2 } from "undici";
|
|
2504
2634
|
var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
2505
2635
|
var FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
|
|
2506
2636
|
async function runDeviceFlow(opts) {
|
|
@@ -2615,7 +2745,7 @@ async function attemptDeviceAuthorization(opts, scope) {
|
|
|
2615
2745
|
}
|
|
2616
2746
|
let resp;
|
|
2617
2747
|
try {
|
|
2618
|
-
resp = await
|
|
2748
|
+
resp = await request2(`${opts.apiBaseUrl}/oauth/device-authorization`, {
|
|
2619
2749
|
method: "POST",
|
|
2620
2750
|
headers,
|
|
2621
2751
|
body,
|
|
@@ -2697,7 +2827,7 @@ async function pollToken(args) {
|
|
|
2697
2827
|
}
|
|
2698
2828
|
let resp;
|
|
2699
2829
|
try {
|
|
2700
|
-
resp = await
|
|
2830
|
+
resp = await request2(`${args.apiBaseUrl}/oauth/token`, {
|
|
2701
2831
|
method: "POST",
|
|
2702
2832
|
headers,
|
|
2703
2833
|
body,
|
|
@@ -4338,6 +4468,8 @@ function renderRootHelp(version) {
|
|
|
4338
4468
|
" generate create Submit a generation job and wait for completion",
|
|
4339
4469
|
" generate wait Wait for an existing job to reach a terminal state",
|
|
4340
4470
|
" generate get Fetch the current state of a job",
|
|
4471
|
+
" generate list List your recent generations, newest first",
|
|
4472
|
+
" generate estimate Price a generation without running it",
|
|
4341
4473
|
" jobs wait Alias of `generate wait`",
|
|
4342
4474
|
" workflow create Create a new empty workflow",
|
|
4343
4475
|
" workflow list List workflows in your organization",
|
|
@@ -4609,6 +4741,8 @@ function renderCommandHelp(command) {
|
|
|
4609
4741
|
" create Submit a generation job (waits for completion by default)",
|
|
4610
4742
|
" wait Long-poll an existing job until terminal",
|
|
4611
4743
|
" get Fetch the current state of a job (no polling)",
|
|
4744
|
+
" list List your recent generations, newest first",
|
|
4745
|
+
" estimate Price a generation without running it (same flags as create)",
|
|
4612
4746
|
""
|
|
4613
4747
|
].join("\n");
|
|
4614
4748
|
case "generate create":
|
|
@@ -4637,6 +4771,8 @@ function renderCommandHelp(command) {
|
|
|
4637
4771
|
" --node-id <id> Attach to a specific node",
|
|
4638
4772
|
" --no-wait, --async Return immediately with the jobId",
|
|
4639
4773
|
" --timeout <seconds> Override the 120s default wait",
|
|
4774
|
+
" --output <path> Save the completed result to a file, or into a",
|
|
4775
|
+
" directory as <jobId>.<ext>",
|
|
4640
4776
|
" --profile <name> Credentials profile name",
|
|
4641
4777
|
" --api-base-url <url> Override the API base URL",
|
|
4642
4778
|
" --token <token> Override the access token",
|
|
@@ -4653,6 +4789,42 @@ function renderCommandHelp(command) {
|
|
|
4653
4789
|
"",
|
|
4654
4790
|
"Options:",
|
|
4655
4791
|
" --timeout <seconds> Overall wait deadline (default: 120)",
|
|
4792
|
+
" --output <path> Save the completed result (file, or directory \u2192 <jobId>.<ext>)",
|
|
4793
|
+
" --profile <name>",
|
|
4794
|
+
" --api-base-url <url>",
|
|
4795
|
+
" --token <token>",
|
|
4796
|
+
" --json",
|
|
4797
|
+
""
|
|
4798
|
+
].join("\n");
|
|
4799
|
+
case "generate list":
|
|
4800
|
+
return [
|
|
4801
|
+
"Usage: clickraft generate list [options]",
|
|
4802
|
+
"",
|
|
4803
|
+
"Lists your recent generations in the organization, newest first.",
|
|
4804
|
+
"",
|
|
4805
|
+
"Options:",
|
|
4806
|
+
" --limit <n> Results per page (1-50, default: 20)",
|
|
4807
|
+
" --status <status> queued, processing, uploading, completed, failed, cancelled",
|
|
4808
|
+
" --content-type <type> image, video, or audio",
|
|
4809
|
+
" --cursor <token> Pagination cursor from a previous response",
|
|
4810
|
+
" --profile <name>",
|
|
4811
|
+
" --api-base-url <url>",
|
|
4812
|
+
" --token <token>",
|
|
4813
|
+
" --json",
|
|
4814
|
+
""
|
|
4815
|
+
].join("\n");
|
|
4816
|
+
case "generate estimate":
|
|
4817
|
+
return [
|
|
4818
|
+
"Usage: clickraft generate estimate [prompt] --model-slug <slug> [options]",
|
|
4819
|
+
"",
|
|
4820
|
+
"Prices exactly what `generate create` with the same flags would charge, and",
|
|
4821
|
+
"says whether the account can run it now. Nothing is created or charged.",
|
|
4822
|
+
"Accepts the same generation flags as `generate create` (--prompt,",
|
|
4823
|
+
"--model-slug, --reference-image, --start-frame, --aspect-ratio, --resolution,",
|
|
4824
|
+
"--duration-seconds, --quality, --brand-model, --product, --workflow-id,",
|
|
4825
|
+
"--node-id). Local reference paths are uploaded first, as with create.",
|
|
4826
|
+
"",
|
|
4827
|
+
"Options:",
|
|
4656
4828
|
" --profile <name>",
|
|
4657
4829
|
" --api-base-url <url>",
|
|
4658
4830
|
" --token <token>",
|
|
@@ -4666,6 +4838,7 @@ function renderCommandHelp(command) {
|
|
|
4666
4838
|
"Fetches the current state of a generation job without polling.",
|
|
4667
4839
|
"",
|
|
4668
4840
|
"Options:",
|
|
4841
|
+
" --output <path> Save the completed result (file, or directory \u2192 <jobId>.<ext>)",
|
|
4669
4842
|
" --profile <name>",
|
|
4670
4843
|
" --api-base-url <url>",
|
|
4671
4844
|
" --token <token>",
|
|
@@ -4797,6 +4970,8 @@ function matchNewCommand(argv) {
|
|
|
4797
4970
|
if (sub === "create") return { kind: "cmd", path: "generate.create", verbTokenCount: 2 };
|
|
4798
4971
|
if (sub === "wait") return { kind: "cmd", path: "generate.wait", verbTokenCount: 2 };
|
|
4799
4972
|
if (sub === "get") return { kind: "cmd", path: "generate.get", verbTokenCount: 2 };
|
|
4973
|
+
if (sub === "list") return { kind: "cmd", path: "generate.list", verbTokenCount: 2 };
|
|
4974
|
+
if (sub === "estimate") return { kind: "cmd", path: "generate.estimate", verbTokenCount: 2 };
|
|
4800
4975
|
if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "generate" };
|
|
4801
4976
|
return { kind: "unknown-sub", verb: "generate", sub };
|
|
4802
4977
|
}
|
|
@@ -4889,6 +5064,10 @@ async function runNewCommand(newCmd, options, stdout, stderr) {
|
|
|
4889
5064
|
return await runGenerateWait(argvAfterVerb, ctx, rc);
|
|
4890
5065
|
case "generate.get":
|
|
4891
5066
|
return await runGenerateGet(argvAfterVerb, ctx, rc);
|
|
5067
|
+
case "generate.list":
|
|
5068
|
+
return await runGenerateList(argvAfterVerb, ctx, rc);
|
|
5069
|
+
case "generate.estimate":
|
|
5070
|
+
return await runGenerateEstimate(argvAfterVerb, ctx, rc);
|
|
4892
5071
|
case "jobs.wait":
|
|
4893
5072
|
return await runJobsWait(argvAfterVerb, ctx, rc);
|
|
4894
5073
|
case "upload":
|
|
@@ -4954,7 +5133,8 @@ async function runGenerateCreate(argv, ctx, rc) {
|
|
|
4954
5133
|
"quality",
|
|
4955
5134
|
"workflow-id",
|
|
4956
5135
|
"node-id",
|
|
4957
|
-
"timeout"
|
|
5136
|
+
"timeout",
|
|
5137
|
+
"output"
|
|
4958
5138
|
],
|
|
4959
5139
|
boolean: [...COMMON_RICH_BOOL_FLAGS, "wait", "async"],
|
|
4960
5140
|
array: ["reference-image", "brand-model", "product"]
|
|
@@ -4987,12 +5167,12 @@ async function runGenerateCreate(argv, ctx, rc) {
|
|
|
4987
5167
|
token: parsed.string.token,
|
|
4988
5168
|
signal: rc.signal
|
|
4989
5169
|
});
|
|
4990
|
-
emitJob(result, ctx, rc);
|
|
5170
|
+
await emitJob(result, ctx, rc, parsed.string.output);
|
|
4991
5171
|
return exitCodeForResult(result);
|
|
4992
5172
|
}
|
|
4993
5173
|
async function runGenerateWait(argv, ctx, rc) {
|
|
4994
5174
|
const parsed = parseArgs(argv, {
|
|
4995
|
-
string: [...COMMON_RICH_STRING_FLAGS, "timeout"],
|
|
5175
|
+
string: [...COMMON_RICH_STRING_FLAGS, "timeout", "output"],
|
|
4996
5176
|
boolean: [...COMMON_RICH_BOOL_FLAGS]
|
|
4997
5177
|
});
|
|
4998
5178
|
const jobId = parsed.positional[0];
|
|
@@ -5005,12 +5185,12 @@ async function runGenerateWait(argv, ctx, rc) {
|
|
|
5005
5185
|
token: parsed.string.token,
|
|
5006
5186
|
signal: rc.signal
|
|
5007
5187
|
});
|
|
5008
|
-
emitJob(result, ctx, rc);
|
|
5188
|
+
await emitJob(result, ctx, rc, parsed.string.output);
|
|
5009
5189
|
return exitCodeForResult(result);
|
|
5010
5190
|
}
|
|
5011
5191
|
async function runGenerateGet(argv, ctx, rc) {
|
|
5012
5192
|
const parsed = parseArgs(argv, {
|
|
5013
|
-
string: [...COMMON_RICH_STRING_FLAGS],
|
|
5193
|
+
string: [...COMMON_RICH_STRING_FLAGS, "output"],
|
|
5014
5194
|
boolean: [...COMMON_RICH_BOOL_FLAGS]
|
|
5015
5195
|
});
|
|
5016
5196
|
const jobId = parsed.positional[0];
|
|
@@ -5022,12 +5202,84 @@ async function runGenerateGet(argv, ctx, rc) {
|
|
|
5022
5202
|
token: parsed.string.token,
|
|
5023
5203
|
signal: rc.signal
|
|
5024
5204
|
});
|
|
5025
|
-
emitJob(result, ctx, rc);
|
|
5205
|
+
await emitJob(result, ctx, rc, parsed.string.output);
|
|
5026
5206
|
return exitCodeForResult(result);
|
|
5027
5207
|
}
|
|
5208
|
+
async function runGenerateList(argv, ctx, rc) {
|
|
5209
|
+
const parsed = parseArgs(argv, {
|
|
5210
|
+
string: [...COMMON_RICH_STRING_FLAGS, "limit", "status", "content-type", "cursor"],
|
|
5211
|
+
boolean: [...COMMON_RICH_BOOL_FLAGS]
|
|
5212
|
+
});
|
|
5213
|
+
const page = await generateList({
|
|
5214
|
+
limit: optionalInt(parsed.string.limit, "limit"),
|
|
5215
|
+
status: parsed.string.status,
|
|
5216
|
+
contentType: parsed.string["content-type"],
|
|
5217
|
+
cursor: parsed.string.cursor,
|
|
5218
|
+
profile: parsed.string.profile,
|
|
5219
|
+
apiBaseUrl: parsed.string["api-base-url"],
|
|
5220
|
+
token: parsed.string.token,
|
|
5221
|
+
signal: rc.signal
|
|
5222
|
+
});
|
|
5223
|
+
if (rc.jsonMode) {
|
|
5224
|
+
rc.stdout.write(JSON.stringify(wrapSuccess(page, ctx)) + "\n");
|
|
5225
|
+
return 0;
|
|
5226
|
+
}
|
|
5227
|
+
rc.stdout.write(renderGenerationList(page) + "\n");
|
|
5228
|
+
return 0;
|
|
5229
|
+
}
|
|
5230
|
+
async function runGenerateEstimate(argv, ctx, rc) {
|
|
5231
|
+
const parsed = parseArgs(argv, {
|
|
5232
|
+
string: [
|
|
5233
|
+
...COMMON_RICH_STRING_FLAGS,
|
|
5234
|
+
"prompt",
|
|
5235
|
+
"model-slug",
|
|
5236
|
+
"start-frame",
|
|
5237
|
+
"aspect-ratio",
|
|
5238
|
+
"resolution",
|
|
5239
|
+
"duration-seconds",
|
|
5240
|
+
"quality",
|
|
5241
|
+
"workflow-id",
|
|
5242
|
+
"node-id"
|
|
5243
|
+
],
|
|
5244
|
+
boolean: [...COMMON_RICH_BOOL_FLAGS],
|
|
5245
|
+
array: ["reference-image", "brand-model", "product"]
|
|
5246
|
+
});
|
|
5247
|
+
const modelSlug = parsed.string["model-slug"];
|
|
5248
|
+
if (!modelSlug) throw new ArgError("--model-slug is required.");
|
|
5249
|
+
const rawBrandModels = parsed.array["brand-model"] ?? [];
|
|
5250
|
+
const brandModels = rawBrandModels.length > 0 ? rawBrandModels.map(parseBrandModelSpec) : void 0;
|
|
5251
|
+
if (brandModels) validateBrandModelSpecs(brandModels);
|
|
5252
|
+
const rawProducts = parsed.array["product"] ?? [];
|
|
5253
|
+
const products = rawProducts.length > 0 ? rawProducts.map(parseProductSpec) : void 0;
|
|
5254
|
+
if (products) validateProductSpecs(products);
|
|
5255
|
+
const estimate = await generateEstimate({
|
|
5256
|
+
prompt: parsed.string.prompt ?? parsed.positional[0],
|
|
5257
|
+
modelSlug,
|
|
5258
|
+
referenceImages: parsed.array["reference-image"],
|
|
5259
|
+
startFrame: parsed.string["start-frame"],
|
|
5260
|
+
aspectRatio: parsed.string["aspect-ratio"],
|
|
5261
|
+
resolution: parsed.string.resolution,
|
|
5262
|
+
durationSeconds: optionalInt(parsed.string["duration-seconds"], "duration-seconds"),
|
|
5263
|
+
quality: parsed.string.quality,
|
|
5264
|
+
brandModels,
|
|
5265
|
+
products,
|
|
5266
|
+
workflowId: parsed.string["workflow-id"],
|
|
5267
|
+
nodeId: parsed.string["node-id"],
|
|
5268
|
+
profile: parsed.string.profile,
|
|
5269
|
+
apiBaseUrl: parsed.string["api-base-url"],
|
|
5270
|
+
token: parsed.string.token,
|
|
5271
|
+
signal: rc.signal
|
|
5272
|
+
});
|
|
5273
|
+
if (rc.jsonMode) {
|
|
5274
|
+
rc.stdout.write(JSON.stringify(wrapSuccess(estimate, ctx)) + "\n");
|
|
5275
|
+
return 0;
|
|
5276
|
+
}
|
|
5277
|
+
rc.stdout.write(renderEstimate(estimate) + "\n");
|
|
5278
|
+
return 0;
|
|
5279
|
+
}
|
|
5028
5280
|
async function runJobsWait(argv, ctx, rc) {
|
|
5029
5281
|
const parsed = parseArgs(argv, {
|
|
5030
|
-
string: [...COMMON_RICH_STRING_FLAGS, "timeout"],
|
|
5282
|
+
string: [...COMMON_RICH_STRING_FLAGS, "timeout", "output"],
|
|
5031
5283
|
boolean: [...COMMON_RICH_BOOL_FLAGS]
|
|
5032
5284
|
});
|
|
5033
5285
|
const jobId = parsed.positional[0];
|
|
@@ -5040,7 +5292,7 @@ async function runJobsWait(argv, ctx, rc) {
|
|
|
5040
5292
|
token: parsed.string.token,
|
|
5041
5293
|
signal: rc.signal
|
|
5042
5294
|
});
|
|
5043
|
-
emitJob(result, ctx, rc);
|
|
5295
|
+
await emitJob(result, ctx, rc, parsed.string.output);
|
|
5044
5296
|
return exitCodeForResult(result);
|
|
5045
5297
|
}
|
|
5046
5298
|
async function runUploadCmd(argv, ctx, rc) {
|
|
@@ -5061,15 +5313,27 @@ async function runUploadCmd(argv, ctx, rc) {
|
|
|
5061
5313
|
emitUpload(result, ctx, rc);
|
|
5062
5314
|
return 0;
|
|
5063
5315
|
}
|
|
5064
|
-
function emitJob(result, ctx, rc) {
|
|
5316
|
+
async function emitJob(result, ctx, rc, output) {
|
|
5065
5317
|
if (rc.signal?.aborted) {
|
|
5066
5318
|
throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
|
|
5067
5319
|
}
|
|
5320
|
+
let savedPath;
|
|
5321
|
+
if (output !== void 0) {
|
|
5322
|
+
if (result.status === "completed" && result.resultUrl) {
|
|
5323
|
+
savedPath = await downloadResult(result, output, { signal: rc.signal });
|
|
5324
|
+
} else {
|
|
5325
|
+
rc.stderr.write(`Not saved: job ${result.jobId} is ${result.status}.
|
|
5326
|
+
`);
|
|
5327
|
+
}
|
|
5328
|
+
}
|
|
5068
5329
|
if (rc.jsonMode) {
|
|
5069
|
-
|
|
5330
|
+
const data = savedPath ? { ...result, savedPath } : result;
|
|
5331
|
+
rc.stdout.write(JSON.stringify(wrapSuccess(data, ctx)) + "\n");
|
|
5070
5332
|
return;
|
|
5071
5333
|
}
|
|
5072
5334
|
rc.stdout.write(renderGenerationResult(result) + "\n");
|
|
5335
|
+
if (savedPath) rc.stdout.write(`Saved: ${savedPath}
|
|
5336
|
+
`);
|
|
5073
5337
|
}
|
|
5074
5338
|
function emitUpload(result, ctx, rc) {
|
|
5075
5339
|
if (rc.signal?.aborted) {
|
|
@@ -5092,6 +5356,38 @@ function renderGenerationResult(r) {
|
|
|
5092
5356
|
}
|
|
5093
5357
|
return lines.join("\n");
|
|
5094
5358
|
}
|
|
5359
|
+
function renderGenerationList(page) {
|
|
5360
|
+
if (page.jobs.length === 0) return "No generations.";
|
|
5361
|
+
const headers = ["JOB ID", "STATUS", "MODEL", "CREATED", "CREDITS", "PROMPT"];
|
|
5362
|
+
const PROMPT_WIDTH = 48;
|
|
5363
|
+
const data = page.jobs.map((job) => {
|
|
5364
|
+
const prompt = (job.prompt ?? "\u2014").replace(/\s+/g, " ");
|
|
5365
|
+
return [
|
|
5366
|
+
job.jobId,
|
|
5367
|
+
job.status,
|
|
5368
|
+
job.modelSlug ?? "\u2014",
|
|
5369
|
+
job.createdAt.slice(0, 16),
|
|
5370
|
+
String(job.creditsCharged),
|
|
5371
|
+
prompt.length > PROMPT_WIDTH ? `${prompt.slice(0, PROMPT_WIDTH - 1)}\u2026` : prompt
|
|
5372
|
+
];
|
|
5373
|
+
});
|
|
5374
|
+
const widths = headers.map(
|
|
5375
|
+
(header, i) => Math.max(header.length, ...data.map((cells) => cells[i].length))
|
|
5376
|
+
);
|
|
5377
|
+
const padRow = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
|
|
5378
|
+
const lines = [padRow(headers), ...data.map(padRow)];
|
|
5379
|
+
if (page.hasMore && page.cursor) lines.push("", `More: --cursor ${page.cursor}`);
|
|
5380
|
+
return lines.join("\n");
|
|
5381
|
+
}
|
|
5382
|
+
function renderEstimate(e) {
|
|
5383
|
+
const lines = [`Model: ${e.modelSlug}`, `Estimated cost: ${e.creditCost} credits`];
|
|
5384
|
+
if (e.estimatedSeconds !== null) lines.push(`Typical duration: ~${e.estimatedSeconds}s`);
|
|
5385
|
+
if (e.balance !== null) lines.push(`Balance: ${e.balance} credits`);
|
|
5386
|
+
lines.push(
|
|
5387
|
+
e.affordable ? "Can run now: yes" : `Can run now: no (${e.blockedReason?.replace(/_/g, " ") ?? "blocked"})`
|
|
5388
|
+
);
|
|
5389
|
+
return lines.join("\n");
|
|
5390
|
+
}
|
|
5095
5391
|
function renderUploadResult(r) {
|
|
5096
5392
|
return [
|
|
5097
5393
|
`Uploaded: ${r.publicUrl}`,
|
|
@@ -5622,8 +5918,8 @@ async function readJsonFile(filePath) {
|
|
|
5622
5918
|
|
|
5623
5919
|
// src/index.ts
|
|
5624
5920
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
5625
|
-
var __dirname2 =
|
|
5626
|
-
var pkg = JSON.parse(readFileSync(
|
|
5921
|
+
var __dirname2 = dirname3(__filename2);
|
|
5922
|
+
var pkg = JSON.parse(readFileSync(join3(__dirname2, "..", "package.json"), "utf8"));
|
|
5627
5923
|
async function run(args, signal) {
|
|
5628
5924
|
return dispatch({
|
|
5629
5925
|
argv: args,
|