@wrangled/cli 0.3.46 → 0.3.48
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 +7 -0
- package/dist/main.js +169 -1
- package/dist/schedules.js +55 -0
- package/dist/secrets.js +35 -0
- package/package.json +1 -1
package/dist/guide.js
CHANGED
|
@@ -142,6 +142,10 @@ A webhook address lets a tool such as a form or webinar platform push events int
|
|
|
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
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
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.
|
|
148
|
+
|
|
145
149
|
## Landing pages
|
|
146
150
|
|
|
147
151
|
The warehouse's content library keeps each landing page's words in order, read about once a day: one \`landing_page\` item per page in \`content_items\`, its versions in \`content_versions\` (\`fields->'blocks'\` holds the headings, paragraphs, lists, buttons, image descriptions and form field labels), and the list of pages with when each was last read in \`content_pages\`. Pages come from Meta ad links and GA4 landing pages. Add any other page with \`${cli} content add-page <url>\`: it is read at once, and the output shows its key, whether it could be read (\`ok\`, \`not_found\`, \`blocked\` by robots.txt, \`failed\`, \`not_html\`), and its version. A link that redirects, such as a link shortener, is recorded under the page it leads to.
|
|
@@ -325,6 +329,9 @@ ${cli} import remove --table <name> --yes drop the table import_<name> and its
|
|
|
325
329
|
${cli} tools list list generation and research tools
|
|
326
330
|
${cli} tools run <tool> --input '<json>' [--idempotency-key <key>] [--wait <seconds>]
|
|
327
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
|
|
328
335
|
${cli} creatives list list creatives (--status, --kind, --limit)
|
|
329
336
|
${cli} creatives show <sku> show one creative with its spec and lineage
|
|
330
337
|
${cli} creatives download <sku | media-id | asset-path | asset-link> [--out <path>] [--open] save a creative's image or video
|
package/dist/main.js
CHANGED
|
@@ -7,6 +7,8 @@ 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 { scheduleCreateBody, scheduleIdFor, SCHEDULES_CREATE_USAGE } from "./schedules.js";
|
|
11
|
+
import { secretBody, secretProvider, SECRETS_SET_USAGE } from "./secrets.js";
|
|
10
12
|
import { guide, skill } from "./guide.js";
|
|
11
13
|
import { PULL_STATE_FILE, pullContent } from "./pull.js";
|
|
12
14
|
import { AGENTS_MD_MARKER, metaAdsAgentsMd, PLAYBOOK_CHOICES, playbooks } from "./playbooks.js";
|
|
@@ -367,6 +369,57 @@ const commands = {
|
|
|
367
369
|
console.log(renderTable(["key", "label", "value"], settings.fields.map((f) => ({ key: f.key, label: f.label, value: settings.values[f.key] ?? "" }))));
|
|
368
370
|
},
|
|
369
371
|
},
|
|
372
|
+
"secrets list": {
|
|
373
|
+
usage: "secrets list (which provider keys the client has; never the keys themselves)",
|
|
374
|
+
async run(args, options) {
|
|
375
|
+
if (args.length > 0)
|
|
376
|
+
throw new CliError("Usage: secrets list", exitCodes.usage);
|
|
377
|
+
const { secrets } = await api(await resolveConnection(options), SECRETS_PATH);
|
|
378
|
+
if (options.format === "json")
|
|
379
|
+
return printJson({ secrets });
|
|
380
|
+
const rows = secrets.map((s) => ({
|
|
381
|
+
provider: s.provider,
|
|
382
|
+
"key in use": secretSourceLabel(s),
|
|
383
|
+
fields: s.fields.join(", "),
|
|
384
|
+
saved: s.updatedAt ?? "",
|
|
385
|
+
"saved by": s.updatedBy ?? "",
|
|
386
|
+
"last used": s.lastUsedAt ?? (s.source === "workspace" && s.changedWith === "secrets" ? "never" : ""),
|
|
387
|
+
}));
|
|
388
|
+
console.log(renderTable(["provider", "key in use", "fields", "saved", "saved by", "last used"], rows));
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
"secrets set": {
|
|
392
|
+
usage: SECRETS_SET_USAGE,
|
|
393
|
+
async run(args, options) {
|
|
394
|
+
const provider = secretProvider(args);
|
|
395
|
+
const text = options.file
|
|
396
|
+
? await readFile(await inRunFolder(options.file), "utf8")
|
|
397
|
+
: process.stdin.isTTY
|
|
398
|
+
? await promptHidden(`Paste the ${provider} key: `)
|
|
399
|
+
: await readStdin();
|
|
400
|
+
const connection = await resolveConnection(options);
|
|
401
|
+
const { secret } = await api(connection, secretPath(provider), secretBody(text), "PUT");
|
|
402
|
+
if (options.format === "json")
|
|
403
|
+
return printJson({ secret });
|
|
404
|
+
console.log(`Saved the client's own ${secret.provider} key. Its ${secret.provider} runs now use it instead of the shared one.`);
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
"secrets remove": {
|
|
408
|
+
usage: "secrets remove <provider> (runs go back to the shared key)",
|
|
409
|
+
async run(args, options) {
|
|
410
|
+
if (args.length !== 1)
|
|
411
|
+
throw new CliError("Usage: secrets remove <provider>", exitCodes.usage);
|
|
412
|
+
const result = await api(await resolveConnection(options), secretPath(args[0]), undefined, "DELETE");
|
|
413
|
+
if (options.format === "json")
|
|
414
|
+
return printJson(result);
|
|
415
|
+
const { provider } = result.secret;
|
|
416
|
+
if (!result.removed)
|
|
417
|
+
return console.log(`The client had no ${provider} key of its own; nothing changed.`);
|
|
418
|
+
console.log(result.secret.source === "platform"
|
|
419
|
+
? `Removed the client's own ${provider} key. Its runs now use the shared key.`
|
|
420
|
+
: `Removed the client's own ${provider} key. No shared key is set, so its ${provider} runs fail until a key is saved.`);
|
|
421
|
+
},
|
|
422
|
+
},
|
|
370
423
|
"intake list": {
|
|
371
424
|
usage: "intake list",
|
|
372
425
|
async run(args, options) {
|
|
@@ -2000,6 +2053,72 @@ const commands = {
|
|
|
2000
2053
|
console.log(`Stopped trying ${dropped.skill} version ${dropped.version} on ${dropped.workspace}. Runs stay on version ${dropped.versionInUse}.`);
|
|
2001
2054
|
},
|
|
2002
2055
|
},
|
|
2056
|
+
"schedules list": {
|
|
2057
|
+
usage: "schedules list (the client's scheduled hands, and who set each one up)",
|
|
2058
|
+
async run(args, options) {
|
|
2059
|
+
if (args.length > 0)
|
|
2060
|
+
throw new CliError("Usage: schedules list", exitCodes.usage);
|
|
2061
|
+
const { schedules } = await api(await resolveConnection(options), SCHEDULES_PATH);
|
|
2062
|
+
if (options.format === "json")
|
|
2063
|
+
return printJson({ schedules });
|
|
2064
|
+
if (schedules.length === 0)
|
|
2065
|
+
return console.log("No scheduled hands yet.");
|
|
2066
|
+
const rows = schedules.map((w) => ({
|
|
2067
|
+
id: w.id,
|
|
2068
|
+
skill: w.skill,
|
|
2069
|
+
schedule: `${w.schedule} ${w.timeZone}`,
|
|
2070
|
+
runner: w.runner,
|
|
2071
|
+
enabled: w.enabled ? "yes" : "no",
|
|
2072
|
+
"next run": w.nextRunAt,
|
|
2073
|
+
"set up by": w.createdBy ?? "operator",
|
|
2074
|
+
}));
|
|
2075
|
+
console.log(renderTable(["id", "skill", "schedule", "runner", "enabled", "next run", "set up by"], rows));
|
|
2076
|
+
},
|
|
2077
|
+
},
|
|
2078
|
+
"schedules create": {
|
|
2079
|
+
usage: `${SCHEDULES_CREATE_USAGE} (a key that can manage settings)`,
|
|
2080
|
+
async run(args, options) {
|
|
2081
|
+
if (args.length > 0)
|
|
2082
|
+
throw new CliError(`Usage: ${SCHEDULES_CREATE_USAGE}`, exitCodes.usage);
|
|
2083
|
+
const file = options.schedule.promptFile;
|
|
2084
|
+
const body = scheduleCreateBody(options.schedule, file === undefined ? null : await readFile(await inRunFolder(file), "utf8"));
|
|
2085
|
+
const { schedule, created } = await api(await resolveConnection(options), SCHEDULES_PATH, body);
|
|
2086
|
+
if (options.format === "json")
|
|
2087
|
+
return printJson({ schedule, created });
|
|
2088
|
+
console.log(`${created ? "Set up" : "Changed"} the scheduled hand ${schedule.skill} (${schedule.id}): ${schedule.schedule} ${schedule.timeZone}, next run ${schedule.nextRunAt}.`);
|
|
2089
|
+
},
|
|
2090
|
+
},
|
|
2091
|
+
...Object.fromEntries([
|
|
2092
|
+
["pause", false, "Paused", "It does not run on its schedule until resumed."],
|
|
2093
|
+
["resume", true, "Resumed", "Its next run is"],
|
|
2094
|
+
].map(([verb, enabled, done, then]) => [
|
|
2095
|
+
`schedules ${verb}`,
|
|
2096
|
+
{
|
|
2097
|
+
usage: `schedules ${verb} <id | skill | name> (a key that can manage settings)`,
|
|
2098
|
+
async run(args, options) {
|
|
2099
|
+
if (args.length !== 1)
|
|
2100
|
+
throw new CliError(`Usage: schedules ${verb} <id | skill | name>`, exitCodes.usage);
|
|
2101
|
+
const connection = await resolveConnection(options);
|
|
2102
|
+
const id = await scheduleId(connection, args[0]);
|
|
2103
|
+
const { schedule } = await api(connection, schedulePath(id), { enabled }, "PATCH");
|
|
2104
|
+
if (options.format === "json")
|
|
2105
|
+
return printJson({ schedule });
|
|
2106
|
+
console.log(`${done} ${schedule.skill}. ${enabled ? `${then} ${schedule.nextRunAt}.` : then}`);
|
|
2107
|
+
},
|
|
2108
|
+
},
|
|
2109
|
+
])),
|
|
2110
|
+
"schedules delete": {
|
|
2111
|
+
usage: "schedules delete <id | skill | name> (a key that can manage settings; its past runs are kept)",
|
|
2112
|
+
async run(args, options) {
|
|
2113
|
+
if (args.length !== 1)
|
|
2114
|
+
throw new CliError("Usage: schedules delete <id | skill | name>", exitCodes.usage);
|
|
2115
|
+
const connection = await resolveConnection(options);
|
|
2116
|
+
const result = await api(connection, schedulePath(await scheduleId(connection, args[0])), undefined, "DELETE");
|
|
2117
|
+
if (options.format === "json")
|
|
2118
|
+
return printJson(result);
|
|
2119
|
+
console.log(`Deleted the scheduled hand ${result.deleted.skill}. It no longer runs; its past runs are kept.`);
|
|
2120
|
+
},
|
|
2121
|
+
},
|
|
2003
2122
|
"runner once": {
|
|
2004
2123
|
usage: `runner once [--run <id>] [--max-seconds <n>] (takes the runner token in ${RUNNER_TOKEN_ENV})`,
|
|
2005
2124
|
async run(args, options) {
|
|
@@ -2595,7 +2714,7 @@ Options:
|
|
|
2595
2714
|
--days <n> winners: the last n complete days, ending yesterday, instead of a week (at most 90); or give winners
|
|
2596
2715
|
--from and --to for any dates
|
|
2597
2716
|
--body, --html <text | @file> the words, or the full HTML, content create, capture and edit save
|
|
2598
|
-
--file <path> an image, video or audio file, up to 50 MB, content create, capture and edit save; repeat for several
|
|
2717
|
+
--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
|
|
2599
2718
|
--transcript <text | @file> what is said in the first video or audio file content capture saves, so none is paid for
|
|
2600
2719
|
--language <code> the spoken language of --transcript, such as en
|
|
2601
2720
|
--source <text> where the content content capture saves came from, in words
|
|
@@ -2653,6 +2772,16 @@ Options:
|
|
|
2653
2772
|
--kind email|text|page with content tags and content autotag, only that kind (content autotag takes a comma list)
|
|
2654
2773
|
--max-seconds <n> stop a runner's run after this many seconds, if that comes before its deadline
|
|
2655
2774
|
--workspace <slug> for worker promote and worker drop: the workspace whose hand they change
|
|
2775
|
+
--skill <name>[@<n>] schedules create: the library skill the hand runs (its latest version unless @n)
|
|
2776
|
+
--name <name> --prompt-file <path> schedules create: save the file (at most 64 KB) as this client's own skill
|
|
2777
|
+
sched-<client code>-<name> and run that instead; changed text adds a new version
|
|
2778
|
+
--cron "<5 fields>" schedules create: when it runs, like "0 9 * * 1" for Mondays at 9:00
|
|
2779
|
+
--time-zone <zone> schedules create: the time zone --cron is read in (default: the client's reporting time zone)
|
|
2780
|
+
--runner mac_mini|trigger schedules create: where it runs (trigger, the cloud, is refused until it is live)
|
|
2781
|
+
--harness claude|codex, --model <id>, --prompt <text>, --deadline-minutes <1-60> schedules create: the agent, model,
|
|
2782
|
+
run prompt and time limit of each run
|
|
2783
|
+
--ai-cap-cents, --tool-cap-cents <n> schedules create: what one run may spend on AI and on images and video, at most
|
|
2784
|
+
1000 and 2000 cents
|
|
2656
2785
|
-h, --help show this help
|
|
2657
2786
|
|
|
2658
2787
|
skill publish, skill list and skill install <name> use the skill library, which takes the operator token in
|
|
@@ -2773,6 +2902,16 @@ function parseOptions(argv) {
|
|
|
2773
2902
|
confidence: { type: "string" },
|
|
2774
2903
|
remove: { type: "boolean", default: false },
|
|
2775
2904
|
"dry-run": { type: "boolean", default: false },
|
|
2905
|
+
skill: { type: "string" },
|
|
2906
|
+
cron: { type: "string" },
|
|
2907
|
+
"time-zone": { type: "string" },
|
|
2908
|
+
runner: { type: "string" },
|
|
2909
|
+
model: { type: "string" },
|
|
2910
|
+
prompt: { type: "string" },
|
|
2911
|
+
"prompt-file": { type: "string" },
|
|
2912
|
+
"deadline-minutes": { type: "string" },
|
|
2913
|
+
"ai-cap-cents": { type: "string" },
|
|
2914
|
+
"tool-cap-cents": { type: "string" },
|
|
2776
2915
|
help: { type: "boolean", short: "h", default: false },
|
|
2777
2916
|
},
|
|
2778
2917
|
});
|
|
@@ -2885,6 +3024,20 @@ function parseOptions(argv) {
|
|
|
2885
3024
|
confidence: values.confidence,
|
|
2886
3025
|
remove: values.remove,
|
|
2887
3026
|
dryRun: values["dry-run"],
|
|
3027
|
+
schedule: {
|
|
3028
|
+
skill: values.skill,
|
|
3029
|
+
name: values.name,
|
|
3030
|
+
promptFile: values["prompt-file"],
|
|
3031
|
+
cron: values.cron,
|
|
3032
|
+
timeZone: values["time-zone"],
|
|
3033
|
+
runner: values.runner,
|
|
3034
|
+
harness: values.harness,
|
|
3035
|
+
model: values.model,
|
|
3036
|
+
prompt: values.prompt,
|
|
3037
|
+
deadlineMinutes: values["deadline-minutes"],
|
|
3038
|
+
aiCapCents: values["ai-cap-cents"],
|
|
3039
|
+
toolCapCents: values["tool-cap-cents"],
|
|
3040
|
+
},
|
|
2888
3041
|
},
|
|
2889
3042
|
};
|
|
2890
3043
|
}
|
|
@@ -2894,6 +3047,21 @@ async function changeCandidate(change, args, options) {
|
|
|
2894
3047
|
throw new CliError(`Usage: worker ${change} <skill> --workspace <slug>`, exitCodes.usage);
|
|
2895
3048
|
return api(await operatorConnection(options), `/api/operator/workers/${change}`, { workspace: options.workspace, skill: args[0] });
|
|
2896
3049
|
}
|
|
3050
|
+
const SECRETS_PATH = "/api/v1/secrets";
|
|
3051
|
+
const SCHEDULES_PATH = "/api/v1/schedules";
|
|
3052
|
+
const schedulePath = (id) => `${SCHEDULES_PATH}/${encodeURIComponent(id)}`;
|
|
3053
|
+
async function scheduleId(connection, ref) {
|
|
3054
|
+
const { schedules } = await api(connection, SCHEDULES_PATH);
|
|
3055
|
+
return scheduleIdFor(schedules, ref);
|
|
3056
|
+
}
|
|
3057
|
+
function secretPath(provider) {
|
|
3058
|
+
return `${SECRETS_PATH}/${encodeURIComponent(provider)}`;
|
|
3059
|
+
}
|
|
3060
|
+
function secretSourceLabel(secret) {
|
|
3061
|
+
if (secret.source === "workspace")
|
|
3062
|
+
return secret.changedWith === "meta_source" ? "the Meta source's" : "the client's own";
|
|
3063
|
+
return secret.source === "platform" ? "the shared key" : "none";
|
|
3064
|
+
}
|
|
2897
3065
|
function findCommand(positionals) {
|
|
2898
3066
|
const three = positionals.slice(0, 3).join(" ");
|
|
2899
3067
|
if (Object.hasOwn(commands, three))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CliError, exitCodes } from "./api.js";
|
|
2
|
+
export const SCHEDULES_CREATE_USAGE = 'schedules create (--skill <name>[@<version>] | --name <name> --prompt-file <path>) --cron "<5 fields>" [--time-zone <zone>] ' +
|
|
3
|
+
"[--runner mac_mini|trigger] [--harness claude|codex] [--model <id>] [--prompt <text>] [--deadline-minutes <n>] [--ai-cap-cents <n>] [--tool-cap-cents <n>]";
|
|
4
|
+
const usage = (problem) => new CliError(`${problem}\nUsage: ${SCHEDULES_CREATE_USAGE}`, exitCodes.usage);
|
|
5
|
+
function whole(flag, raw) {
|
|
6
|
+
if (raw === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (!/^\d+$/.test(raw))
|
|
9
|
+
throw usage(`--${flag} must be a whole number; got '${raw}'.`);
|
|
10
|
+
return Number(raw);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* What POST /api/v1/schedules takes for these flags. `promptFileText` is the text of --prompt-file, read by the caller.
|
|
14
|
+
* The server checks the values themselves; this only checks the flags fit together.
|
|
15
|
+
*/
|
|
16
|
+
export function scheduleCreateBody(flags, promptFileText) {
|
|
17
|
+
const fromFile = flags.name !== undefined || flags.promptFile !== undefined;
|
|
18
|
+
if ((flags.skill === undefined) === !fromFile)
|
|
19
|
+
throw usage("Give --skill, or --name with --prompt-file: one of the two.");
|
|
20
|
+
if (fromFile && (flags.name === undefined || flags.promptFile === undefined))
|
|
21
|
+
throw usage("--name and --prompt-file go together.");
|
|
22
|
+
if (flags.cron === undefined)
|
|
23
|
+
throw usage("--cron is required.");
|
|
24
|
+
const body = {
|
|
25
|
+
skill: flags.skill,
|
|
26
|
+
name: flags.name,
|
|
27
|
+
promptFile: fromFile ? (promptFileText ?? "") : undefined,
|
|
28
|
+
schedule: flags.cron,
|
|
29
|
+
timeZone: flags.timeZone,
|
|
30
|
+
runner: flags.runner,
|
|
31
|
+
harness: flags.harness,
|
|
32
|
+
model: flags.model,
|
|
33
|
+
prompt: flags.prompt,
|
|
34
|
+
deadlineMinutes: whole("deadline-minutes", flags.deadlineMinutes),
|
|
35
|
+
aiCapCents: whole("ai-cap-cents", flags.aiCapCents),
|
|
36
|
+
toolCapCents: whole("tool-cap-cents", flags.toolCapCents),
|
|
37
|
+
};
|
|
38
|
+
return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The id of the hand `ref` names: its id, its skill, or for a hand made from a prompt file (skill
|
|
42
|
+
* `sched-<workspace code>-<name>`), the name it was given, when exactly one hand has it.
|
|
43
|
+
*/
|
|
44
|
+
export function scheduleIdFor(schedules, ref) {
|
|
45
|
+
const exact = schedules.find((s) => s.id === ref.toLowerCase()) ?? schedules.find((s) => s.skill === ref);
|
|
46
|
+
if (exact)
|
|
47
|
+
return exact.id;
|
|
48
|
+
const named = schedules.filter((s) => /^sched-[a-z0-9]+-/.test(s.skill) && s.skill.replace(/^sched-[a-z0-9]+-/, "") === ref);
|
|
49
|
+
if (named.length === 1)
|
|
50
|
+
return named[0].id;
|
|
51
|
+
if (named.length > 1) {
|
|
52
|
+
throw new CliError(`More than one scheduled hand is named ${JSON.stringify(ref)} (${named.map((s) => s.skill).join(", ")}); give its id or full skill name.`, exitCodes.usage);
|
|
53
|
+
}
|
|
54
|
+
throw new CliError(`schedule_not_found: no scheduled hand with id, skill or name ${JSON.stringify(ref)} in this workspace`, exitCodes.error);
|
|
55
|
+
}
|
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
|
+
}
|