@wrangled/cli 0.3.45 → 0.3.47
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/guide.js +12 -1
- package/dist/main.js +165 -20
- package/dist/secrets.js +35 -0
- package/package.json +1 -1
package/dist/guide.js
CHANGED
|
@@ -140,6 +140,11 @@ A webhook address lets a tool such as a form or webinar platform push events int
|
|
|
140
140
|
3. A run that is still going prints its id. Check it with \`${cli} tools get <run-id>\`. Image results list an \`asset\` link. Save the file with \`${cli} creatives download <asset-link>\`, or fetch the link with your API key as a bearer token; it redirects to a copy that works for 10 minutes.
|
|
141
141
|
4. Record a succeeded run in the ledger with its lineage: \`${cli} creatives create --run <run-id> --lineage '{"persona": "...", "painPoint": "...", "desiredOutcome": "...", "angle": "...", "sourceAd": {"metaAdId": "..."}}'\`. Each output gets a SKU such as \`DLC-000123\`. The SKU goes in the Meta ad name. Lineage is free-text notes; give the ad's marketing tags with \`--tags '<json>'\` or \`--tags @tags.json\`, the same fields as ad tags (see Ad tags), such as \`{"offer_key": "course", "landing_funnel_key": "webinar_to_course", "landing_step_key": "registration_page", "persona": "near_retirement", "angle": "story", "format": "image"}\`. Every output of the run gets them. Values come from \`${cli} grammar\`; persona and concept from this client's lists, and offer, funnel and step from its confirmed map (\`${cli} funnels show\`), where the step must be in that funnel and \`reply\` or \`no_link\` take no step. A value off them is refused with the ones to use. Recording the same run again returns its creatives as they are, tags included; \`${cli} creatives tag <sku> --file tags.json\` is how to change a creative's tags. Your tags read proposed until an Owner or Admin confirms them on the creative's page in Content; after that only a person changes them.
|
|
142
142
|
5. \`${cli} creatives list\` and \`${cli} creatives show <sku>\` read the ledger. \`${cli} creatives download <sku>\` saves a creative's image or video as \`<sku>.<ext>\` in the current folder; \`--out <path>\` names the file or a folder, and \`--open\` opens it. It also takes an asset path or link, such as a \`meta_ad_creatives\` \`image_path\`. \`--format json\` prints \`{ path, bytes, contentType }\`. Copy has no file; its text is in \`${cli} creatives show\`. For performance, query the \`creatives\` table or the \`creative_performance_daily\` view with \`${cli} warehouse query\`.
|
|
143
|
+
6. \`${cli} media list\` lists every image and video the tools made, newest first, whether or not it became a creative: its media id (\`<run id>:<index>\`), tool, prompt, cost and SKU, if any. Narrow it with \`--sku <sku>\`, \`--run <run-id>\` (one batch), \`--tool kie:nano-banana\`, \`--kind image|video\`, \`--since\` and \`--until\` (a day such as \`2026-10-08\`, which \`--until\` counts through its end in UTC, or an ISO time), and \`--limit <n>\` (default 50, at most 200). When more remain it prints the \`--cursor <media-id>\` that shows the next page. A run's cost is split evenly across its outputs and marked as an estimate when the provider did not report a charge. \`${cli} media get <media-id>\` shows the full input that made one, to reuse a good prompt; \`${cli} media download <media-id>\` saves its file, named by its SKU when it has one (\`${cli} creatives download\` takes a media id too). \`--format json\` prints \`{ media, nextCursor }\`.
|
|
144
|
+
|
|
145
|
+
### The client's own provider keys
|
|
146
|
+
|
|
147
|
+
A run bills the shared account unless the client has its own key for that provider. A key whose "Can manage settings" switch an admin turned on (Keys page) saves one: \`${cli} secrets set kie < key.txt\`, or \`--file <path>\`. Never type the key on the command line; it stays in the shell history, so the command refuses it. A key with several fields (genesis, vercel) is a JSON object, such as \`{"token": "...", "teamId": "..."}\`. \`${cli} secrets list\` shows which key each provider's runs use (the client's own, the shared one, or none), its field names, when and by whom it was saved, and when a run last read it, never the key itself. \`${cli} secrets remove kie\` puts runs back on the shared key. Meta's key is its source's, changed by connecting the Meta Ads source. A hand's run key can never save or remove keys.
|
|
143
148
|
|
|
144
149
|
## Landing pages
|
|
145
150
|
|
|
@@ -324,11 +329,17 @@ ${cli} import remove --table <name> --yes drop the table import_<name> and its
|
|
|
324
329
|
${cli} tools list list generation and research tools
|
|
325
330
|
${cli} tools run <tool> --input '<json>' [--idempotency-key <key>] [--wait <seconds>]
|
|
326
331
|
${cli} tools get <run-id> show a tool run, waiting for it to finish (--wait)
|
|
332
|
+
${cli} secrets list which provider keys the client has and which key runs use; never the keys themselves
|
|
333
|
+
${cli} secrets set <provider> [--file <path>] save the client's own provider key from stdin or a file (keys with "Can manage settings")
|
|
334
|
+
${cli} secrets remove <provider> remove it, so runs use the shared key again
|
|
327
335
|
${cli} creatives list list creatives (--status, --kind, --limit)
|
|
328
336
|
${cli} creatives show <sku> show one creative with its spec and lineage
|
|
329
|
-
${cli} creatives download <sku | asset-path | asset-link> [--out <path>] [--open] save a creative's image or video
|
|
337
|
+
${cli} creatives download <sku | media-id | asset-path | asset-link> [--out <path>] [--open] save a creative's image or video
|
|
330
338
|
${cli} creatives create --run <run-id> [--lineage '<json>' | @file.json] [--tags '<json>' | @file.json] record a run's output with a new SKU
|
|
331
339
|
${cli} creatives tag <sku> --file <tags.json> replace a creative's marketing tags, proposed until a person confirms them
|
|
340
|
+
${cli} media list list every made image and video (--sku, --run, --tool, --kind, --since, --until, --limit, --cursor)
|
|
341
|
+
${cli} media get <media-id> show the tool, model, full input and cost that made one
|
|
342
|
+
${cli} media download <media-id> [--out <path>] [--open] save a made image or video
|
|
332
343
|
${cli} content create --kind <kind> [--body ...] [--file <path>] [--note <why>] [--plan <plan-id>] save made content with a new SKU
|
|
333
344
|
${cli} content search [--text <words>] [--mode word|meaning] [--kind <kind>] [--platform <p>] [--origin <o>] [--from <date>] [--to <date>] [--sort <column>] [--order asc|desc] [--limit <n>] find content with its numbers
|
|
334
345
|
${cli} content pull <dir> [--kind <kind>] [--platform <p>] [--origin <o>] [--media] [--full] copy the library to a folder of Markdown files; rerun for changes
|
package/dist/main.js
CHANGED
|
@@ -7,6 +7,7 @@ import { parseArgs } from "node:util";
|
|
|
7
7
|
import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, getSigned, IMPORT_LIMITS, IMPORT_NAME_PATTERN, putSigned, CLIENT_FILE, parseClientName, readSavedLogins, removeCredentials, resolveApiUrl, resolveConnection, resolveLogin, UnreadableCredentialsError, writeCredentials, entryTagFlags, streamSigned, } from "./api.js";
|
|
8
8
|
import { brand, envPrefix } from "./brand.js";
|
|
9
9
|
import { firstSymlink, inRunFolder, writeFileAtomic } from "./files.js";
|
|
10
|
+
import { secretBody, secretProvider, SECRETS_SET_USAGE } from "./secrets.js";
|
|
10
11
|
import { guide, skill } from "./guide.js";
|
|
11
12
|
import { PULL_STATE_FILE, pullContent } from "./pull.js";
|
|
12
13
|
import { AGENTS_MD_MARKER, metaAdsAgentsMd, PLAYBOOK_CHOICES, playbooks } from "./playbooks.js";
|
|
@@ -367,6 +368,57 @@ const commands = {
|
|
|
367
368
|
console.log(renderTable(["key", "label", "value"], settings.fields.map((f) => ({ key: f.key, label: f.label, value: settings.values[f.key] ?? "" }))));
|
|
368
369
|
},
|
|
369
370
|
},
|
|
371
|
+
"secrets list": {
|
|
372
|
+
usage: "secrets list (which provider keys the client has; never the keys themselves)",
|
|
373
|
+
async run(args, options) {
|
|
374
|
+
if (args.length > 0)
|
|
375
|
+
throw new CliError("Usage: secrets list", exitCodes.usage);
|
|
376
|
+
const { secrets } = await api(await resolveConnection(options), SECRETS_PATH);
|
|
377
|
+
if (options.format === "json")
|
|
378
|
+
return printJson({ secrets });
|
|
379
|
+
const rows = secrets.map((s) => ({
|
|
380
|
+
provider: s.provider,
|
|
381
|
+
"key in use": secretSourceLabel(s),
|
|
382
|
+
fields: s.fields.join(", "),
|
|
383
|
+
saved: s.updatedAt ?? "",
|
|
384
|
+
"saved by": s.updatedBy ?? "",
|
|
385
|
+
"last used": s.lastUsedAt ?? (s.source === "workspace" && s.changedWith === "secrets" ? "never" : ""),
|
|
386
|
+
}));
|
|
387
|
+
console.log(renderTable(["provider", "key in use", "fields", "saved", "saved by", "last used"], rows));
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
"secrets set": {
|
|
391
|
+
usage: SECRETS_SET_USAGE,
|
|
392
|
+
async run(args, options) {
|
|
393
|
+
const provider = secretProvider(args);
|
|
394
|
+
const text = options.file
|
|
395
|
+
? await readFile(await inRunFolder(options.file), "utf8")
|
|
396
|
+
: process.stdin.isTTY
|
|
397
|
+
? await promptHidden(`Paste the ${provider} key: `)
|
|
398
|
+
: await readStdin();
|
|
399
|
+
const connection = await resolveConnection(options);
|
|
400
|
+
const { secret } = await api(connection, secretPath(provider), secretBody(text), "PUT");
|
|
401
|
+
if (options.format === "json")
|
|
402
|
+
return printJson({ secret });
|
|
403
|
+
console.log(`Saved the client's own ${secret.provider} key. Its ${secret.provider} runs now use it instead of the shared one.`);
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
"secrets remove": {
|
|
407
|
+
usage: "secrets remove <provider> (runs go back to the shared key)",
|
|
408
|
+
async run(args, options) {
|
|
409
|
+
if (args.length !== 1)
|
|
410
|
+
throw new CliError("Usage: secrets remove <provider>", exitCodes.usage);
|
|
411
|
+
const result = await api(await resolveConnection(options), secretPath(args[0]), undefined, "DELETE");
|
|
412
|
+
if (options.format === "json")
|
|
413
|
+
return printJson(result);
|
|
414
|
+
const { provider } = result.secret;
|
|
415
|
+
if (!result.removed)
|
|
416
|
+
return console.log(`The client had no ${provider} key of its own; nothing changed.`);
|
|
417
|
+
console.log(result.secret.source === "platform"
|
|
418
|
+
? `Removed the client's own ${provider} key. Its runs now use the shared key.`
|
|
419
|
+
: `Removed the client's own ${provider} key. No shared key is set, so its ${provider} runs fail until a key is saved.`);
|
|
420
|
+
},
|
|
421
|
+
},
|
|
370
422
|
"intake list": {
|
|
371
423
|
usage: "intake list",
|
|
372
424
|
async run(args, options) {
|
|
@@ -545,20 +597,11 @@ const commands = {
|
|
|
545
597
|
},
|
|
546
598
|
},
|
|
547
599
|
"creatives download": {
|
|
548
|
-
usage: "creatives download <sku | asset-path | asset-link> [--out <path>] [--open]",
|
|
600
|
+
usage: "creatives download <sku | media-id | asset-path | asset-link> [--out <path>] [--open]",
|
|
549
601
|
async run(args, options) {
|
|
550
602
|
if (args.length !== 1)
|
|
551
|
-
throw new CliError("Usage: creatives download <sku | asset-path | asset-link> [--out <path>] [--open]", exitCodes.usage);
|
|
552
|
-
|
|
553
|
-
const target = await downloadTarget(args[0], connection);
|
|
554
|
-
const { bytes, contentType } = await apiDownload(connection, `${ASSET_ROUTE}${target.assetPath.split("/").map(encodeURIComponent).join("/")}`);
|
|
555
|
-
const path = await savePath(options.out, extname(target.fileName) ? target.fileName : `${target.fileName}.${extensionFor(contentType)}`);
|
|
556
|
-
await saveFile(path, bytes);
|
|
557
|
-
if (options.open)
|
|
558
|
-
openFile(resolve(path));
|
|
559
|
-
if (options.format === "json")
|
|
560
|
-
return printJson({ path: resolve(path), bytes: bytes.byteLength, contentType });
|
|
561
|
-
console.log(`Saved ${target.label} to ${path} (${fileSize(bytes.byteLength)}).`);
|
|
603
|
+
throw new CliError("Usage: creatives download <sku | media-id | asset-path | asset-link> [--out <path>] [--open]", exitCodes.usage);
|
|
604
|
+
await download(args[0], options);
|
|
562
605
|
},
|
|
563
606
|
},
|
|
564
607
|
"creatives create": {
|
|
@@ -591,6 +634,48 @@ const commands = {
|
|
|
591
634
|
console.log(`Tagged ${creative.sku}. The tags read ${creative.tagsStatus} until a person confirms them in the app.`);
|
|
592
635
|
},
|
|
593
636
|
},
|
|
637
|
+
"media list": {
|
|
638
|
+
usage: "media list [--sku <sku>] [--run <tool-run-id>] [--tool <tool>] [--kind image|video] [--since <date>] [--until <date>] [--limit <n>] [--cursor <media-id>]",
|
|
639
|
+
async run(args, options) {
|
|
640
|
+
if (args.length > 0)
|
|
641
|
+
throw new CliError("Usage: media list [--sku <sku>] [--run <tool-run-id>] [--tool <tool>] [--kind image|video] [--since <date>] [--until <date>] [--limit <n>] [--cursor <media-id>]", exitCodes.usage);
|
|
642
|
+
const query = new URLSearchParams();
|
|
643
|
+
for (const name of ["sku", "run", "tool", "kind", "since", "until", "limit", "cursor"]) {
|
|
644
|
+
if (options[name])
|
|
645
|
+
query.set(name, options[name]);
|
|
646
|
+
}
|
|
647
|
+
const page = await api(await resolveConnection(options), `/api/v1/media${query.size ? `?${query}` : ""}`);
|
|
648
|
+
if (options.format === "json")
|
|
649
|
+
return printJson(page);
|
|
650
|
+
if (page.media.length === 0)
|
|
651
|
+
return console.log("No made images or videos match.");
|
|
652
|
+
const rows = page.media.map((m) => ({ ...m, prompt: excerpt(m.prompt ?? ""), cost: costLabel(m), sku: m.sku ?? "" }));
|
|
653
|
+
console.log(renderTable(["id", "tool", "prompt", "cost", "sku", "createdAt", "assetUrl"], rows));
|
|
654
|
+
if (page.nextCursor)
|
|
655
|
+
console.log(`\nMore: add --cursor ${page.nextCursor}`);
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
"media get": {
|
|
659
|
+
usage: "media get <media-id>",
|
|
660
|
+
async run(args, options) {
|
|
661
|
+
if (args.length !== 1)
|
|
662
|
+
throw new CliError("Usage: media get <media-id>", exitCodes.usage);
|
|
663
|
+
const { media } = await api(await resolveConnection(options), mediaPath(args[0]));
|
|
664
|
+
if (options.format === "json")
|
|
665
|
+
return printJson({ media });
|
|
666
|
+
const fields = { ...media, cost: costLabel(media) };
|
|
667
|
+
const shown = ["id", "tool", "model", "prompt", "input", "cost", "contentType", "sku", "creativeStatus", "assetUrl", "workerRunId", "createdAt"];
|
|
668
|
+
console.log(renderTable(["field", "value"], shown.filter((f) => fields[f] != null).map((f) => ({ field: f, value: fields[f] }))));
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
"media download": {
|
|
672
|
+
usage: "media download <media-id> [--out <path>] [--open]",
|
|
673
|
+
async run(args, options) {
|
|
674
|
+
if (args.length !== 1 || !parseMediaId(args[0]))
|
|
675
|
+
throw new CliError("Usage: media download <media-id> [--out <path>] [--open]", exitCodes.usage);
|
|
676
|
+
await download(args[0], options);
|
|
677
|
+
},
|
|
678
|
+
},
|
|
594
679
|
"content create": {
|
|
595
680
|
usage: "content create --kind <kind> [--name <name>] [--subject <s>] [--preview <s>] [--headline <s>] [--body <text> | @file] [--html @file] [--fields '<json>' | @file.json] [--file <path> ...] [--tool-run <id> ...] [--run <agent-run-id>] [--note <why>] [--plan <plan-id> [--role <role>]]",
|
|
596
681
|
async run(args, options) {
|
|
@@ -2539,11 +2624,11 @@ Options:
|
|
|
2539
2624
|
for warehouse schema, a table to describe in full; repeat for several
|
|
2540
2625
|
--summary warehouse schema: one short row per table (kind, source, column count, summary)
|
|
2541
2626
|
--search <words> warehouse schema: describe in full the tables whose name, description or columns mention a word
|
|
2542
|
-
--out <path> the file, or existing folder, creatives download and warehouse query save to; warehouse query
|
|
2627
|
+
--out <path> the file, or existing folder, creatives download, media download and warehouse query save to; warehouse query
|
|
2543
2628
|
writes CSV for a .csv path, otherwise JSON
|
|
2544
2629
|
--all with warehouse query --out, save the whole answer (up to 1,000,000 rows or 50 MiB) instead of
|
|
2545
2630
|
the first 1,000 rows or 5 MiB; .ndjson or .jsonl saves one JSON row per line
|
|
2546
|
-
--open open the file creatives download saved
|
|
2631
|
+
--open open the file creatives download or media download saved
|
|
2547
2632
|
--media with content pull, also save each item's files and HTML under media/ in the folder
|
|
2548
2633
|
--full with content pull, fetch every item, not only what changed since the last pull (brings new numbers)
|
|
2549
2634
|
--force content unpublish: take the page down even while delivering Meta ads link to it
|
|
@@ -2562,13 +2647,20 @@ Options:
|
|
|
2562
2647
|
--days <n> winners: the last n complete days, ending yesterday, instead of a week (at most 90); or give winners
|
|
2563
2648
|
--from and --to for any dates
|
|
2564
2649
|
--body, --html <text | @file> the words, or the full HTML, content create, capture and edit save
|
|
2565
|
-
--file <path> an image, video or audio file, up to 50 MB, content create, capture and edit save; repeat for several
|
|
2650
|
+
--file <path> an image, video or audio file, up to 50 MB, content create, capture and edit save; repeat for several; for secrets set, the file holding the key
|
|
2566
2651
|
--transcript <text | @file> what is said in the first video or audio file content capture saves, so none is paid for
|
|
2567
2652
|
--language <code> the spoken language of --transcript, such as en
|
|
2568
2653
|
--source <text> where the content content capture saves came from, in words
|
|
2569
2654
|
--url <url> the address of the content content capture saves
|
|
2570
2655
|
--tool-run <id> a succeeded tool run content create and edit link; repeat for several
|
|
2571
|
-
--run <id> the agent run making the content (content),
|
|
2656
|
+
--run <id> the agent run making the content (content), the tool run to record (creatives create), or the tool
|
|
2657
|
+
run whose outputs media list shows
|
|
2658
|
+
--sku <sku> with media list, only the output that became this creative
|
|
2659
|
+
--tool <tool> with media list, only this tool's outputs, such as kie:nano-banana
|
|
2660
|
+
--kind image|video with media list, only images or only videos
|
|
2661
|
+
--since, --until <YYYY-MM-DD | ISO time> with media list, made at or after --since and before --until (a day: through
|
|
2662
|
+
its end, in UTC)
|
|
2663
|
+
--cursor <media-id> with media list, the next page: the id it printed after "More"
|
|
2572
2664
|
--platform, --external-id where content status launched says it was published, and that platform's id for it
|
|
2573
2665
|
--object-type <type> what that platform's sync calls it (campaign_message or flow_message on klaviyo); needed where
|
|
2574
2666
|
the sync matches several kinds, so its next read joins the item
|
|
@@ -2600,9 +2692,9 @@ Options:
|
|
|
2600
2692
|
--from, --to <n> with content diff, the two versions to compare (default: the one before the current one, and the current one)
|
|
2601
2693
|
--sort <column> how content search sorts: recent (default) or a content_scorecard number, such as click_rate or roas
|
|
2602
2694
|
--order desc|asc content search's sort direction (default: desc, best or newest first)
|
|
2603
|
-
--limit <n> how many results content search, creatives list, campaigns, voice, ads tags, content tags
|
|
2604
|
-
return, how many ads ads autotag tags and how many items content autotag
|
|
2605
|
-
ads each ads tag-formats batch looks at (default 2000)
|
|
2695
|
+
--limit <n> how many results content search, creatives list, media list, campaigns, voice, ads tags, content tags
|
|
2696
|
+
and content to-check return, how many ads ads autotag tags and how many items content autotag
|
|
2697
|
+
tags, or how many ads each ads tag-formats batch looks at (default 2000)
|
|
2606
2698
|
--library <slug> with knowledge search, only this library: creative-cartel, exodus or meta-policy
|
|
2607
2699
|
--angle, --hook-kind, --ad-format, --awareness <value> with knowledge search, only library entries tagged that way, and
|
|
2608
2700
|
your own ads tagged the same way with their results; with knowledge tag, the tags to put on an entry;
|
|
@@ -2669,6 +2761,10 @@ function parseOptions(argv) {
|
|
|
2669
2761
|
yes: { type: "boolean", default: false },
|
|
2670
2762
|
from: { type: "string" },
|
|
2671
2763
|
since: { type: "string" },
|
|
2764
|
+
until: { type: "string" },
|
|
2765
|
+
sku: { type: "string" },
|
|
2766
|
+
tool: { type: "string" },
|
|
2767
|
+
cursor: { type: "string" },
|
|
2672
2768
|
to: { type: "string" },
|
|
2673
2769
|
grain: { type: "string" },
|
|
2674
2770
|
channel: { type: "string" },
|
|
@@ -2780,6 +2876,10 @@ function parseOptions(argv) {
|
|
|
2780
2876
|
yes: values.yes,
|
|
2781
2877
|
from: values.from,
|
|
2782
2878
|
since: values.since,
|
|
2879
|
+
until: values.until,
|
|
2880
|
+
sku: values.sku,
|
|
2881
|
+
tool: values.tool,
|
|
2882
|
+
cursor: values.cursor,
|
|
2783
2883
|
to: values.to,
|
|
2784
2884
|
grain: values.grain,
|
|
2785
2885
|
channel: values.channel,
|
|
@@ -2846,6 +2946,15 @@ async function changeCandidate(change, args, options) {
|
|
|
2846
2946
|
throw new CliError(`Usage: worker ${change} <skill> --workspace <slug>`, exitCodes.usage);
|
|
2847
2947
|
return api(await operatorConnection(options), `/api/operator/workers/${change}`, { workspace: options.workspace, skill: args[0] });
|
|
2848
2948
|
}
|
|
2949
|
+
const SECRETS_PATH = "/api/v1/secrets";
|
|
2950
|
+
function secretPath(provider) {
|
|
2951
|
+
return `${SECRETS_PATH}/${encodeURIComponent(provider)}`;
|
|
2952
|
+
}
|
|
2953
|
+
function secretSourceLabel(secret) {
|
|
2954
|
+
if (secret.source === "workspace")
|
|
2955
|
+
return secret.changedWith === "meta_source" ? "the Meta source's" : "the client's own";
|
|
2956
|
+
return secret.source === "platform" ? "the shared key" : "none";
|
|
2957
|
+
}
|
|
2849
2958
|
function findCommand(positionals) {
|
|
2850
2959
|
const three = positionals.slice(0, 3).join(" ");
|
|
2851
2960
|
if (Object.hasOwn(commands, three))
|
|
@@ -3391,8 +3500,44 @@ async function readImportFile(path) {
|
|
|
3391
3500
|
return readFile(path);
|
|
3392
3501
|
}
|
|
3393
3502
|
const ASSET_ROUTE = "/api/v1/assets/";
|
|
3394
|
-
/**
|
|
3503
|
+
/** Saves a creative's or a made output's file, as `creatives download` and `media download` do. */
|
|
3504
|
+
async function download(arg, options) {
|
|
3505
|
+
const connection = await resolveConnection(options);
|
|
3506
|
+
const target = await downloadTarget(arg, connection);
|
|
3507
|
+
const { bytes, contentType } = await apiDownload(connection, `${ASSET_ROUTE}${target.assetPath.split("/").map(encodeURIComponent).join("/")}`);
|
|
3508
|
+
const path = await savePath(options.out, extname(target.fileName) ? target.fileName : `${target.fileName}.${extensionFor(contentType)}`);
|
|
3509
|
+
await saveFile(path, bytes);
|
|
3510
|
+
if (options.open)
|
|
3511
|
+
openFile(resolve(path));
|
|
3512
|
+
if (options.format === "json")
|
|
3513
|
+
return printJson({ path: resolve(path), bytes: bytes.byteLength, contentType });
|
|
3514
|
+
console.log(`Saved ${target.label} to ${path} (${fileSize(bytes.byteLength)}).`);
|
|
3515
|
+
}
|
|
3516
|
+
const MEDIA_ID = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}):(0|[1-9]\d{0,3})$/i;
|
|
3517
|
+
function parseMediaId(raw) {
|
|
3518
|
+
const match = MEDIA_ID.exec(raw);
|
|
3519
|
+
return match ? { runId: match[1].toLowerCase(), index: Number(match[2]) } : null;
|
|
3520
|
+
}
|
|
3521
|
+
// A media id's colon is sent as it is, so the route reads the id whether or not the server decodes the path.
|
|
3522
|
+
function mediaPath(raw) {
|
|
3523
|
+
const id = parseMediaId(raw);
|
|
3524
|
+
if (!id)
|
|
3525
|
+
throw new CliError(`"${excerpt(raw)}" is not a media id; media list prints them as <run id>:<index>.`, exitCodes.usage);
|
|
3526
|
+
return `/api/v1/media/${id.runId}:${id.index}`;
|
|
3527
|
+
}
|
|
3528
|
+
function costLabel(media) {
|
|
3529
|
+
return `${media.costCents}¢${media.costIsEstimate ? " (estimate)" : ""}`;
|
|
3530
|
+
}
|
|
3531
|
+
/** What `creatives download` fetches, from a SKU, a media id, an asset path, or an asset link as `creatives show` prints it. */
|
|
3395
3532
|
async function downloadTarget(arg, connection) {
|
|
3533
|
+
const output = parseMediaId(arg);
|
|
3534
|
+
if (output) {
|
|
3535
|
+
const { media } = await api(connection, mediaPath(arg));
|
|
3536
|
+
if (!media.assetPath)
|
|
3537
|
+
throw new CliError(`${media.id} has no stored file to download.`, exitCodes.error);
|
|
3538
|
+
const name = media.sku ?? `${output.runId}-${output.index}`;
|
|
3539
|
+
return { label: media.sku ?? media.id, assetPath: media.assetPath, fileName: `${name}${extname(media.assetPath)}` };
|
|
3540
|
+
}
|
|
3396
3541
|
const routeAt = arg.indexOf(ASSET_ROUTE);
|
|
3397
3542
|
if (routeAt !== -1 || arg.includes("/")) {
|
|
3398
3543
|
const assetPath = routeAt === -1 ? arg : decodeLinkPath(arg.slice(routeAt + ASSET_ROUTE.length).replace(/[?#].*$/, ""));
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CliError, exitCodes } from "./api.js";
|
|
2
|
+
import { brand } from "./brand.js";
|
|
3
|
+
export const SECRETS_SET_USAGE = "secrets set <provider> [--file <path>] (the key on stdin or in the file; JSON {\"field\": \"...\"} for a key with several fields)";
|
|
4
|
+
/**
|
|
5
|
+
* The provider `secrets set` names. A second argument is refused without being echoed: it is most likely the key,
|
|
6
|
+
* which would then sit in the shell's history.
|
|
7
|
+
*/
|
|
8
|
+
export function secretProvider(args) {
|
|
9
|
+
if (args.length > 1) {
|
|
10
|
+
throw new CliError(`Never pass the key as an argument: it stays in your shell history. Pipe it on stdin (\`${brand.slug} secrets set <provider> < key.txt\`) or name a file with --file.`, exitCodes.usage);
|
|
11
|
+
}
|
|
12
|
+
if (args.length === 0)
|
|
13
|
+
throw new CliError(`Usage: ${SECRETS_SET_USAGE}`, exitCodes.usage);
|
|
14
|
+
return args[0];
|
|
15
|
+
}
|
|
16
|
+
/** A JSON object of strings is a multi-field key; anything else is the key itself. Error messages never repeat the text. */
|
|
17
|
+
export function secretBody(text) {
|
|
18
|
+
const trimmed = text.trim();
|
|
19
|
+
if (!trimmed)
|
|
20
|
+
throw new CliError("No key given. Pipe it on stdin or name a file with --file.", exitCodes.usage);
|
|
21
|
+
if (!trimmed.startsWith("{"))
|
|
22
|
+
return { value: trimmed };
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(trimmed);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new CliError('The key starts with "{" but is not valid JSON. A key with several fields is a JSON object, such as {"token": "...", "teamId": "..."}.', exitCodes.usage);
|
|
29
|
+
}
|
|
30
|
+
const fields = parsed;
|
|
31
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || Object.values(fields).some((v) => typeof v !== "string")) {
|
|
32
|
+
throw new CliError('A key with several fields is a JSON object of strings, such as {"token": "...", "teamId": "..."}.', exitCodes.usage);
|
|
33
|
+
}
|
|
34
|
+
return { fields: fields };
|
|
35
|
+
}
|