@wrangled/cli 0.3.59 → 0.3.61
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/articles.js +5 -5
- package/dist/guide.js +4 -3
- package/dist/main.js +42 -2
- package/dist/secrets.js +4 -3
- package/package.json +1 -1
package/dist/articles.js
CHANGED
|
@@ -32,11 +32,11 @@ export function pointImagesAtFiles(html, files) {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
/** The publishers `--to <publisher>:<site>` names, each taking --status; only Webflow takes --url-prefix. */
|
|
35
|
-
const SITE_PUBLISHERS = ["wordpress", "webflow"];
|
|
35
|
+
const SITE_PUBLISHERS = ["wordpress", "webflow", "strapi"];
|
|
36
36
|
/**
|
|
37
|
-
* The publisher and target `content publish` sends. `--to wordpress:<site
|
|
38
|
-
* themselves, with --status draft (the default) or publish, and for Webflow
|
|
39
|
-
* its target as --target JSON, given here parsed.
|
|
37
|
+
* The publisher and target `content publish` sends. `--to wordpress:<site>`, `--to webflow:<site>` and
|
|
38
|
+
* `--to strapi:<site>` fill the target themselves, with --status draft (the default) or publish, and for Webflow
|
|
39
|
+
* --url-prefix; any other publisher takes its target as --target JSON, given here parsed.
|
|
40
40
|
*/
|
|
41
41
|
export function publishDestination(to, { target, status, urlPrefix }) {
|
|
42
42
|
const publisher = SITE_PUBLISHERS.find((p) => to.startsWith(`${p}:`));
|
|
@@ -49,7 +49,7 @@ export function publishDestination(to, { target, status, urlPrefix }) {
|
|
|
49
49
|
return { publisher, target: { site, ...(status === undefined ? {} : { status }), ...(urlPrefix === undefined ? {} : { urlPrefix }) } };
|
|
50
50
|
}
|
|
51
51
|
if (status !== undefined)
|
|
52
|
-
throw new CliError("--status goes with --to wordpress:<site> or
|
|
52
|
+
throw new CliError("--status goes with --to wordpress:<site>, webflow:<site> or strapi:<site>.", exitCodes.usage);
|
|
53
53
|
if (target === undefined)
|
|
54
54
|
throw new CliError(`--to ${to} needs --target '<json>'.`, exitCodes.usage);
|
|
55
55
|
return { publisher: to, target };
|
package/dist/guide.js
CHANGED
|
@@ -93,7 +93,7 @@ To find content, such as "the 10 emails with the best click rate", "our 5 best v
|
|
|
93
93
|
|
|
94
94
|
- \`--text "<words>"\` finds words in the current version's subject, headline and body, or in a video's or call recording's transcript. Words are stemmed ("guarantee" finds "guaranteed"); use "quotes" for a phrase and -word to leave a word out.
|
|
95
95
|
- \`--mode meaning\` finds content that means something close to \`--text\`, even in other words: \`--text "saves time"\` also finds "done in five minutes". Results come nearest first, each with its distance (0 is the same meaning) and the closest passage as its words. It takes no \`--sort\` or \`--order\`, and finds only content embedded so far (each hour, new versions and transcripts are embedded).
|
|
96
|
-
- \`--kind\` (ad, email, sms, landing_page, video, social_post, article, conversation), \`--platform\` (meta, ghl, klaviyo, roezan, bayengage, activecampaign, mailchimp, hubspot, kit, instantly, kajabi, omnisend, google_ads, tiktok, microsoft_ads, pinterest, linkedin, web, wordpress, webflow, youtube, instagram, facebook_page, callrail, zoom, fathom, google_drive, ad_library, workspace, upload) and \`--origin\` (discovered or made) narrow it. \`--from\` and \`--to\` keep content live on those days: from its first day with numbers (or the day the library first saw it) to its last.
|
|
96
|
+
- \`--kind\` (ad, email, sms, landing_page, video, social_post, article, conversation), \`--platform\` (meta, ghl, klaviyo, roezan, bayengage, activecampaign, mailchimp, hubspot, kit, instantly, kajabi, omnisend, google_ads, tiktok, microsoft_ads, pinterest, linkedin, web, wordpress, webflow, strapi, youtube, instagram, facebook_page, callrail, zoom, fathom, google_drive, ad_library, workspace, upload) and \`--origin\` (discovered or made) narrow it. \`--from\` and \`--to\` keep content live on those days: from its first day with numbers (or the day the library first saw it) to its last.
|
|
97
97
|
- \`--sort\` is \`recent\` (the default, newest first) or any number in the \`content_scorecard\` view, such as \`click_rate\`, \`open_rate\`, \`roas\`, \`spend\` or their \`_30d\` twins for the last 30 days. Content with money in several currencies has a number per currency and sorts by its best. Content without that number comes last. \`--order asc\` turns it around; \`--limit\` is 1 to 50 (default 10).
|
|
98
98
|
- Each result has the item's id or SKU, its words, how many files it has, whether it has a transcript, and its scorecard numbers. \`${cli} content show <sku | item-id>\` prints one item in full: its words, how many versions it has, its transcript and every number; \`--format json\` adds every version, and links to its files.
|
|
99
99
|
- To add marketing that lives somewhere no source connects to, such as a sales video, a webinar recording, a script or an email from another tool, run \`${cli} content capture --kind <kind> --name <name>\` with \`--body\`, \`--file\`, \`--source\` (where it came from) and \`--url\`. It is kept as found, with no SKU, so it is never approved or published; \`--platform upload\` finds these. Videos and audio are transcribed on their own; give \`--transcript @file\` when you already have one, so none is paid for.
|
|
@@ -215,7 +215,7 @@ Save anything you make before it goes anywhere: an ad written without a tool, an
|
|
|
215
215
|
|
|
216
216
|
### Publishing
|
|
217
217
|
|
|
218
|
-
\`${cli} content publish <sku> --to <publisher> --target '<json>' --idempotency-key <key>\` publishes an approved item through one of ${brand.name}'s publishers, adding \`--plan <plan-id>\` when it is part of a launch plan and \`--run <run-id>\` from a hand's run. Only the approved version is published; an item edited after approval must be approved again. The server checks every rule first and refuses with each failure listed, recording nothing: one unfinished send per platform account, and at least 15 seconds between sends to one account. Otherwise it records exactly what it sends before sending it, then saves the platform's ids, marks the item launched, and moves the launch plan to live once all its items are out. For Meta without the \`--run\` of a hand in live mode, nothing is sent: the publication is saved as \`not_sent_practice\` with the full request. A send that may or may not have landed stays \`sending\` and is never retried; \`${cli} content reconcile <publication-id>\` asks the platform and finishes it. The same key returns the same publication. A page host is the publisher \`page_host:<host>\` with the target \`{"site":"...","path":"/..."}\`: it puts an approved landing page's HTML online, tags each link that leaves the page with the SKU, and is never held back by practice mode. \`${cli} content show <sku>\` lists the page's deployments with their ids. \`${cli} content unpublish <deployment-id>\` takes one live page down; the site's other pages stay up, and the deployment records when and by whom. It is refused while a synced Meta ad that is delivering links to the page, naming each ad, because a broken link in a running ad wastes money; \`--force\` takes it down anyway. Never roll a site back in Vercel's dashboard instead: the next deploy brings the rolled-away pages back. An article (\`${cli} content create --kind article --headline "<title>" --html @post.html --file hero.jpg\`) goes to the client's WordPress site with \`${cli} content publish <sku> --to wordpress:<site> --idempotency-key <key>\`, as a draft, or live with \`--status publish\`. Its images are uploaded once per site and each \`<img src>\` naming a \`--file\` points at the uploaded copy. The site's sign-in is saved first with \`${cli} secrets set wordpress:<site> --file wp.json\`, the file holding \`{"baseUrl": "https://...", "username": "...", "applicationPassword": "..."}\` (an application password made in WordPress under Users, Profile). For a client on Webflow, \`--to webflow:<site>\` puts it in the site's blog collection the same way, except that a picture inside the body must be a web address for now (the main picture, \`featured_image\`, can be a --file), adding \`--url-prefix https://<their domain>/blog\` so a live item's address is recorded; save the site first with \`${cli} secrets set webflow:<site> --file webflow.json\`, the file holding \`{"token": "...", "collectionId": "...", "fields": {"body": "post-body", "summary": "post-summary", "image": "main-image"}}\` (a site token with CMS read and write, the collection's id, and the slug of the collection's field each part goes into; the title and slug go into Webflow's own name and slug unless \`title\` or \`slug\` name others).
|
|
218
|
+
\`${cli} content publish <sku> --to <publisher> --target '<json>' --idempotency-key <key>\` publishes an approved item through one of ${brand.name}'s publishers, adding \`--plan <plan-id>\` when it is part of a launch plan and \`--run <run-id>\` from a hand's run. Only the approved version is published; an item edited after approval must be approved again. The server checks every rule first and refuses with each failure listed, recording nothing: one unfinished send per platform account, and at least 15 seconds between sends to one account. Otherwise it records exactly what it sends before sending it, then saves the platform's ids, marks the item launched, and moves the launch plan to live once all its items are out. For Meta without the \`--run\` of a hand in live mode, nothing is sent: the publication is saved as \`not_sent_practice\` with the full request. A send that may or may not have landed stays \`sending\` and is never retried; \`${cli} content reconcile <publication-id>\` asks the platform and finishes it. The same key returns the same publication. A page host is the publisher \`page_host:<host>\` with the target \`{"site":"...","path":"/..."}\`: it puts an approved landing page's HTML online, tags each link that leaves the page with the SKU, and is never held back by practice mode. \`${cli} content show <sku>\` lists the page's deployments with their ids. \`${cli} content unpublish <deployment-id>\` takes one live page down; the site's other pages stay up, and the deployment records when and by whom. It is refused while a synced Meta ad that is delivering links to the page, naming each ad, because a broken link in a running ad wastes money; \`--force\` takes it down anyway. Never roll a site back in Vercel's dashboard instead: the next deploy brings the rolled-away pages back. An article (\`${cli} content create --kind article --headline "<title>" --html @post.html --file hero.jpg\`) goes to the client's WordPress site with \`${cli} content publish <sku> --to wordpress:<site> --idempotency-key <key>\`, as a draft, or live with \`--status publish\`. Its images are uploaded once per site and each \`<img src>\` naming a \`--file\` points at the uploaded copy. The site's sign-in is saved first with \`${cli} secrets set wordpress:<site> --file wp.json\`, the file holding \`{"baseUrl": "https://...", "username": "...", "applicationPassword": "..."}\` (an application password made in WordPress under Users, Profile). For a client on Webflow, \`--to webflow:<site>\` puts it in the site's blog collection the same way, except that a picture inside the body must be a web address for now (the main picture, \`featured_image\`, can be a --file), adding \`--url-prefix https://<their domain>/blog\` so a live item's address is recorded; save the site first with \`${cli} secrets set webflow:<site> --file webflow.json\`, the file holding \`{"token": "...", "collectionId": "...", "fields": {"body": "post-body", "summary": "post-summary", "image": "main-image"}}\` (a site token with CMS read and write, the collection's id, and the slug of the collection's field each part goes into; the title and slug go into Webflow's own name and slug unless \`title\` or \`slug\` name others). For a client on Strapi (version 4 or 5), \`--to strapi:<site>\` makes an entry in the site's collection the same way, as a draft or published with \`--status publish\`, with its pictures uploaded; save the site first with \`${cli} secrets set strapi:<site> --file strapi.json\`, the file holding \`{"baseUrl": "https://...", "token": "...", "version": 5, "collection": "articles", "fields": {"body": "content", "summary": "description", "image": "cover"}, "bodyFormat": "markdown"}\` (an API token that can create entries and upload files, the collection's plural API id, the attribute each part goes into, with the title and slug in \`title\` and \`slug\` unless the map names others, and \`markdown\` for a Rich text (Markdown) body or \`html\` for a text one; Strapi 5's Blocks body is not supported yet).
|
|
219
219
|
|
|
220
220
|
The \`meta_ads\` publisher puts an approved ad on Facebook, always paused, in an ad set that already exists, \`--to meta_ads --target '{"accountId": "act_<id>", "adsetId": "<id>"}'\`, or in one made with \`${cli} meta adset publish\` (below), \`"adsetPublicationId": "<id>"\` in place of \`adsetId\`. \`"urlTags": "sku"\` (the default) adds the SKU tracking tags to the link; \`"urlTags": "none"\` sends the link exactly as written, Meta macros such as \`{{ad.name}}\` included. The ad needs pictures or a video (not both), body text and an https link in \`fields.link_urls\` (\`fields.cta\`, such as "Shop now", sets the button; \`fields.bodies\`, \`fields.titles\` and \`fields.descriptions\` add variations). The account, the ad set's campaign and the ad set must all be on the hand's publish list, which only an operator sets, and the ad set and campaign must be in the last Meta sync and not archived. A live send also needs a run of the Facebook ads hand, Meta numbers pulled under 12 hours ago, and room under the daily limit of new ads. The ad is named \`<SKU> | <name>\` and its creative's \`url_tags\` carry the SKU. In practice mode the same command stores the full request for review and sends nothing. Each publication's \`targetSnapshot\` names the campaign and ad set as they were at publish time.
|
|
221
221
|
|
|
@@ -404,7 +404,7 @@ ${cli} tools get <run-id> show a tool run, waiting for it to finish (-
|
|
|
404
404
|
${cli} tools usage [--month <YYYY-MM>] [--by tool|run] what tool runs cost in a month, per tool or per scheduled run, split by whose key paid
|
|
405
405
|
${cli} enrich email --file <people.csv | people.json> [--steps <tool,tool>] find one verified work email per person, cheapest provider first
|
|
406
406
|
${cli} secrets list which provider keys the client has and which key runs use; never the keys themselves
|
|
407
|
-
${cli} secrets set <provider> [--file <path>] save the client's own provider key from stdin or a file (keys with "Can manage settings"); wordpress:<site> saves a WordPress site's sign-in, webflow:<site> a Webflow site's token and field map
|
|
407
|
+
${cli} secrets set <provider> [--file <path>] save the client's own provider key from stdin or a file (keys with "Can manage settings"); wordpress:<site> saves a WordPress site's sign-in, webflow:<site> a Webflow site's token and field map, strapi:<site> a Strapi site's address, token and field map
|
|
408
408
|
${cli} secrets remove <provider> remove it, so runs use the shared key again
|
|
409
409
|
${cli} creatives list list creatives (--status, --kind, --limit)
|
|
410
410
|
${cli} creatives show <sku> show one creative with its spec and lineage
|
|
@@ -422,6 +422,7 @@ ${cli} content diff <sku | item-id> [--from <n>] [--to <n>] what changed betwe
|
|
|
422
422
|
${cli} content publish <sku> --to <publisher> --target '<json>' --idempotency-key <key> [--plan <id>] [--version <n>] [--run <run-id>] publish an approved item
|
|
423
423
|
${cli} content publish <sku> --to wordpress:<site> [--status publish] --idempotency-key <key> put an approved article on a WordPress site, as a draft unless --status publish
|
|
424
424
|
${cli} content publish <sku> --to webflow:<site> [--status publish] [--url-prefix <url>] --idempotency-key <key> put an approved article in a Webflow blog collection
|
|
425
|
+
${cli} content publish <sku> --to strapi:<site> [--status publish] --idempotency-key <key> put an approved article in a Strapi collection, as a draft unless --status publish
|
|
425
426
|
${cli} content reconcile <publication-id> check whether a publish left sending landed
|
|
426
427
|
${cli} content unpublish <deployment-id> [--force] take one deployed landing page down
|
|
427
428
|
${cli} meta adset publish --plan '<json>' --idempotency-key <key> [--run <run-id>] make a paused Meta ad set to add ads to
|
package/dist/main.js
CHANGED
|
@@ -26,7 +26,7 @@ const META_CHANGE_COLUMNS = ["id", "level", "object", "change", "status", "when"
|
|
|
26
26
|
const GOOGLE_ADS_CHANGE_COLUMNS = ["id", "account", "level", "object", "change", "status", "when", "by", "reason"];
|
|
27
27
|
const SERVER_WAIT_CAP = 50;
|
|
28
28
|
const VOICE_USAGE = "voice [--kind opener|objection|pain|desire|phrase] [--since <YYYY-MM-DD>] [--sort common|useful] [--limit <n>] [--json]";
|
|
29
|
-
const CONTENT_PUBLISH_USAGE = "content publish <sku | item-id> (--to <publisher> --target '<json>' | --to wordpress:<site> [--status draft|publish] | --to webflow:<site> [--status draft|publish] [--url-prefix <https://...>]) --idempotency-key <key> [--plan <plan-id>] [--version <n>] [--run <run-id>]";
|
|
29
|
+
const CONTENT_PUBLISH_USAGE = "content publish <sku | item-id> (--to <publisher> --target '<json>' | --to wordpress:<site> [--status draft|publish] | --to webflow:<site> [--status draft|publish] [--url-prefix <https://...>] | --to strapi:<site> [--status draft|publish]) --idempotency-key <key> [--plan <plan-id>] [--version <n>] [--run <run-id>]";
|
|
30
30
|
const META_MOVE_USAGE = 'meta ads move <ad-id> --to-adset <adset-id> --reason "<why>" [--keep-original] [--status active|paused] [--run <run-id>] [--idempotency-key <key>]';
|
|
31
31
|
const OUTBOUND_PUSH_USAGE = `outbound push --to instantly:<campaign-id> (--csv <file> | --table <import_ or agent_ table>) [--dry-run] [--max <n>] [--idempotency-key <key>]`;
|
|
32
32
|
const commands = {
|
|
@@ -456,6 +456,9 @@ const commands = {
|
|
|
456
456
|
if (secret.provider.startsWith("webflow:")) {
|
|
457
457
|
return console.log(`Saved the token and field map for the Webflow site ${secret.provider.slice("webflow:".length)}. content publish --to ${secret.provider} uses them.`);
|
|
458
458
|
}
|
|
459
|
+
if (secret.provider.startsWith("strapi:")) {
|
|
460
|
+
return console.log(`Saved the address, token and field map for the Strapi site ${secret.provider.slice("strapi:".length)}. content publish --to ${secret.provider} uses them.`);
|
|
461
|
+
}
|
|
459
462
|
console.log(`Saved the client's own ${secret.provider} key. Its ${secret.provider} runs now use it instead of the shared one.`);
|
|
460
463
|
},
|
|
461
464
|
},
|
|
@@ -2845,7 +2848,44 @@ function winnersText(report) {
|
|
|
2845
2848
|
(report.goalSentence ? `${report.goalSentence}\n` : "") +
|
|
2846
2849
|
`A ${words.primary.one} means ${report.saleMeans}. ${report.saleRule ?? FIRST_TOUCH_SALE_RULE}\n` +
|
|
2847
2850
|
winnersWindowText(winnersDatesTitle(report.window), week, words) +
|
|
2848
|
-
(week.from === last30Days.from && week.to === last30Days.to ? "" : winnersWindowText(`Last 30 days, ${last30Days.from} to ${last30Days.to}`, last30Days, words))
|
|
2851
|
+
(week.from === last30Days.from && week.to === last30Days.to ? "" : winnersWindowText(`Last 30 days, ${last30Days.from} to ${last30Days.to}`, last30Days, words)) +
|
|
2852
|
+
(report.followUp ? followUpText(report.followUp, winnersDatesTitle(report.window)) : ""));
|
|
2853
|
+
}
|
|
2854
|
+
/** A message's or sequence's tagged parts, "job: last_chance, hook: question". */
|
|
2855
|
+
const followUpTags = (tags) => tags.flatMap((t) => Object.entries(t.tags).flatMap(([field, value]) => (field !== "tag_reasons" && typeof value === "string" ? [`${field}: ${value}`] : []))).join(", ");
|
|
2856
|
+
/** A message's or sequence's line: its name, its numbers and tags, then why it is on the list. */
|
|
2857
|
+
function followUpLines(row) {
|
|
2858
|
+
const n = row.numbers;
|
|
2859
|
+
const name = "sequenceKey" in row ? `${row.name ?? `Workflow ${row.sequenceKey}`} (${counted(row.messages, "message")})` : [row.name ?? row.subject ?? (row.channel === "text" ? "Text message" : "Email"), row.itemId && `[${row.itemId}]`].filter(Boolean).join(" ");
|
|
2860
|
+
const engaged = "channel" in row && row.channel === "text" ? `${tenths(n.replied)} replied` : `${tenths(n.clicked)} clicked`;
|
|
2861
|
+
const sent = n.sent === null ? "no send count" : `${tenths(n.sent)} sent`;
|
|
2862
|
+
const tags = "tags" in row ? followUpTags(row.tags) : "";
|
|
2863
|
+
return `- ${name}: ${sent}, ${engaged}, ${counted(n.salesHelped, "sale")} helped, ${tenths(n.salesLastNudge)} last nudge${n.optedOut > 0 ? `, ${counted(n.optedOut, "opt-out")}` : ""}${tags ? `. Tags: ${tags}` : ""}\n ${row.pickedBecause}\n`;
|
|
2864
|
+
}
|
|
2865
|
+
function followUpWindowText(title, window) {
|
|
2866
|
+
const list = (heading, rows) => `\n${heading}\n${rows.length === 0 ? "- none\n" : rows.map(followUpLines).join("")}`;
|
|
2867
|
+
const claimed = window.toolClaimed.map((m) => {
|
|
2868
|
+
const revenue = m.toolClaimedRevenue.map((r) => (r.currency === null ? r.revenue.toFixed(2) : money(r.revenue, r.currency))).join(" and ") || "no revenue";
|
|
2869
|
+
const sends = m.toolSends === null ? "" : `, ${tenths(m.toolSends)} sent`;
|
|
2870
|
+
return `- ${m.name ?? m.subject ?? "Email"} [${m.itemId}] (${m.platform}): ${revenue} claimed${sends}\n ${m.pickedBecause}\n`;
|
|
2871
|
+
});
|
|
2872
|
+
return (`\n### ${title}\n` +
|
|
2873
|
+
list("Top emails", window.emails.top) +
|
|
2874
|
+
list("Bottom emails", window.emails.bottom) +
|
|
2875
|
+
list("Top texts", window.texts.top) +
|
|
2876
|
+
list("Bottom texts", window.texts.bottom) +
|
|
2877
|
+
list("Top sequences", window.sequences.top) +
|
|
2878
|
+
list("Bottom sequences", window.sequences.bottom) +
|
|
2879
|
+
(claimed.length > 0 ? `\nThe tool's own claims, not real sales\n${claimed.join("")}` : ""));
|
|
2880
|
+
}
|
|
2881
|
+
/** Follow-up emails and texts for the same windows as the ads; the last 30 days once when they are the dates asked for. */
|
|
2882
|
+
function followUpText(followUp, title) {
|
|
2883
|
+
if (followUp.state === "not_connected")
|
|
2884
|
+
return "\n## Follow-up emails and texts\n\nNo email or text tool is connected yet, so there are no follow-up messages to judge.\n";
|
|
2885
|
+
const { week, last30Days } = followUp;
|
|
2886
|
+
return (`\n## Follow-up emails and texts\n\nA sale is helped by a message the buyer clicked or replied to in the ${counted(followUp.creditDays, "day")} before buying. Under ${tenths(followUp.minSends)} sent, a message is judged on clicks or replies, never as a flop.\n` +
|
|
2887
|
+
followUpWindowText(title, week) +
|
|
2888
|
+
(week.from === last30Days.from && week.to === last30Days.to ? "" : followUpWindowText(`Last 30 days, ${last30Days.from} to ${last30Days.to}`, last30Days)));
|
|
2849
2889
|
}
|
|
2850
2890
|
function printWinnersGoals(goals, format) {
|
|
2851
2891
|
if (format === "json")
|
package/dist/secrets.js
CHANGED
|
@@ -14,8 +14,9 @@ export function secretProvider(args) {
|
|
|
14
14
|
return args[0];
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* A JSON object of strings is a multi-field key
|
|
18
|
-
* field map is; anything else is the key itself. Error messages never
|
|
17
|
+
* A JSON object of strings is a multi-field key. A field may also be a number, as a Strapi site's version is, or an
|
|
18
|
+
* object of strings, as a Webflow or Strapi site's field map is; anything else is the key itself. Error messages never
|
|
19
|
+
* repeat the text.
|
|
19
20
|
*/
|
|
20
21
|
export function secretBody(text) {
|
|
21
22
|
const trimmed = text.trim();
|
|
@@ -32,7 +33,7 @@ export function secretBody(text) {
|
|
|
32
33
|
}
|
|
33
34
|
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34
35
|
const fields = parsed;
|
|
35
|
-
const fieldOk = (v) => typeof v === "string" || (isObject(v) && Object.values(v).every((inner) => typeof inner === "string"));
|
|
36
|
+
const fieldOk = (v) => typeof v === "string" || typeof v === "number" || (isObject(v) && Object.values(v).every((inner) => typeof inner === "string"));
|
|
36
37
|
if (!isObject(parsed) || !Object.values(fields).every(fieldOk)) {
|
|
37
38
|
throw new CliError('A key with several fields is a JSON object of strings, such as {"token": "...", "teamId": "..."}.', exitCodes.usage);
|
|
38
39
|
}
|