@wrangled/cli 0.3.150 → 0.3.152
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/api.js +30 -2
- package/dist/guide.js +2 -0
- package/dist/main.js +56 -11
- package/dist/unattributed-calls.js +17 -0
- package/dist/version-check.js +28 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { brand, envPrefix } from "./brand.js";
|
|
5
5
|
import { RUN_ROOT_ENV, writeFileAtomic } from "./files.js";
|
|
6
|
+
import { noteLatestCli } from "./version-check.js";
|
|
6
7
|
export const exitCodes = { ok: 0, error: 1, usage: 2, auth: 3, timedOut: 4, interrupted: 130 };
|
|
7
8
|
export class CliError extends Error {
|
|
8
9
|
exitCode;
|
|
@@ -335,9 +336,9 @@ async function request(connection, path, init) {
|
|
|
335
336
|
});
|
|
336
337
|
}
|
|
337
338
|
catch (error) {
|
|
338
|
-
|
|
339
|
-
throw new CliError(`Cannot reach ${connection.apiUrl}: ${cause}`, exitCodes.error);
|
|
339
|
+
throw cannotReach(connection.apiUrl, error);
|
|
340
340
|
}
|
|
341
|
+
noteLatestCli(response);
|
|
341
342
|
if (response.status === 401) {
|
|
342
343
|
throw new CliError(connection.rejected ?? `The API key was rejected by ${connection.apiUrl}. Run \`${brand.slug} login\` with a valid key.`, exitCodes.auth);
|
|
343
344
|
}
|
|
@@ -347,6 +348,33 @@ async function request(connection, path, init) {
|
|
|
347
348
|
}
|
|
348
349
|
return response;
|
|
349
350
|
}
|
|
351
|
+
/** The build the API at `apiUrl` runs; it needs no key. */
|
|
352
|
+
export async function fetchApiVersion(apiUrl) {
|
|
353
|
+
let response;
|
|
354
|
+
let body;
|
|
355
|
+
try {
|
|
356
|
+
response = await fetch(`${apiUrl}/api/v1/version`, { signal: AbortSignal.timeout(VERSION_TIMEOUT_MS) });
|
|
357
|
+
noteLatestCli(response);
|
|
358
|
+
if (!response.ok)
|
|
359
|
+
throw new CliError(`HTTP ${response.status} from /api/v1/version at ${apiUrl}`, exitCodes.error);
|
|
360
|
+
body = await response.json().catch(() => undefined);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
if (error instanceof CliError)
|
|
364
|
+
throw error;
|
|
365
|
+
throw cannotReach(apiUrl, error);
|
|
366
|
+
}
|
|
367
|
+
const api = body?.api;
|
|
368
|
+
if (typeof api?.cliLatest !== "string" || (api.commit !== null && typeof api.commit !== "string")) {
|
|
369
|
+
throw new CliError(`/api/v1/version at ${apiUrl} did not answer with a version.`, exitCodes.error);
|
|
370
|
+
}
|
|
371
|
+
return { api: { commit: api.commit, cliLatest: api.cliLatest } };
|
|
372
|
+
}
|
|
373
|
+
const VERSION_TIMEOUT_MS = 5_000;
|
|
374
|
+
function cannotReach(apiUrl, error) {
|
|
375
|
+
const cause = error.cause;
|
|
376
|
+
return new CliError(`Cannot reach ${apiUrl}: ${cause?.message || cause?.code || error.message}`, exitCodes.error);
|
|
377
|
+
}
|
|
350
378
|
function parseErrorBody(text) {
|
|
351
379
|
try {
|
|
352
380
|
return JSON.parse(text).error;
|
package/dist/guide.js
CHANGED
|
@@ -155,6 +155,7 @@ For "how is the funnel doing?", "which campaigns are working?" and "where does t
|
|
|
155
155
|
|
|
156
156
|
- \`${cli} funnel [--grain day|week|month]\` prints ad spend, leads, booked calls, calls held, sales and net revenue per day, week (starting Monday) or month, with cost per lead, per booked call and per sale and ROAS, then totals for the whole range.
|
|
157
157
|
- \`${cli} campaigns [--channel <channel>] [--limit <n>]\` prints each campaign's spend, leads, booked calls, sales and revenue, best first (most first-touch revenue, then most spend), then totals over every matching campaign. Each lead and sale is credited to the person's first touch and last touch side by side. \`--limit\` is 1 to 200 (default 50); \`--channel\` is meta, google, affiliate, a utm_source, unknown or recorded_in_crm.
|
|
158
|
+
- \`${cli} calls unattributed [--attribution first_touch|last_touch] [--limit <n>]\` answers "why do these booked calls have no ad?": it counts the booked calls in the range (by day booked), how many were credited to a Facebook ad and how many were not, as \`ad_results_daily\` credits them (to the first touch by default), splits the calls with no ad by why (\`no_person\`: the booking's contact has no email or phone to match a person; \`meta_without_ad\`: a Facebook visit whose link named no single ad; \`not_meta\`: another channel; \`visit_after_call\`: the visits are dated after the booking; \`last_visit_only\`: only a latest visit, no first; \`no_visit\`: no tagged visit at all), then lists those calls newest first with the booking's contact id, the person id and the touch's channel, campaign, ad and landing page. With and without add up to every booked call. \`--limit\` is 1 to 1000 (default 200); the counts always cover every call. The rows are in the \`booked_call_credits\` view; join \`touches\` on \`person_id\` to see each visit's tags.
|
|
158
159
|
- \`${cli} money [--grain day|week|month]\` prints money in (gross, refunds, net, orders), ad spend, affiliate payouts, what is left after ad costs and ROAS per period, then totals for the whole range, money in per payment source (stripe, ghl) and the biggest refund days. Refunds come off on the day they were made, not the day of the sale. \`left_after_ad_costs\` is net less ad spend and affiliate payouts; it is not profit. Its \`roas\` is net over ad spend alone, so it differs from \`${cli} funnel\`'s, which divides by all spend including affiliate payouts.
|
|
159
160
|
- When no payment tool, store or CRM sales are connected but Triple Whale is, \`${cli} money\` takes money in from Triple Whale's store totals and \`${cli} campaigns\` adds the orders, revenue and ROAS Triple Whale credits to each Facebook campaign (\`orders_triple_whale\`, \`revenue_triple_whale\`, \`roas_triple_whale\`), counted as \`${cli} winners\` counts them. Each says so under its table. Triple Whale can credit one order to several ads, so campaign orders can add up to more than the store's. With neither, each report says what is missing and leaves out the columns that would be blank.
|
|
160
161
|
- Money is never converted or added across currencies: there is one row per currency, and one total per currency. In \`${cli} funnel\`, leads and calls are counted for the whole period and repeat on every currency row, so never add them across its rows. In \`${cli} campaigns\`, each campaign's leads and calls sit on the row of the currency it spends in (or a blank-currency row when it has no spend), so they do add up across rows and totals. Ratios come from the summed numbers, not averages. An empty cell means the source is not connected, or a ratio would divide by 0.
|
|
@@ -453,6 +454,7 @@ A scheduled agent's run acts as its skill's agent, the way the Facebook ads agen
|
|
|
453
454
|
${cli} login [--client <name>] validate a key (prompted, piped, or ${envPrefix}_API_KEY) and save it, as the default or under a client's name
|
|
454
455
|
${cli} logout [--client <name>] delete the default saved login, or one client's
|
|
455
456
|
${cli} whoami show the current workspace, library access, and which client's key is in use and why
|
|
457
|
+
${cli} version this CLI's version, the commit the API runs, and the latest CLI it ships (also --version); needs no login
|
|
456
458
|
${cli} knowledge search "<question>" [--limit <n>] [--kind <kind>] [--hook-kind <v> ...] short, cited snippets from the know-how libraries (keys with library access)
|
|
457
459
|
${cli} knowledge tag <entry-id> --hook-kind <v> --confidence <c> tag a library entry with the marketing grammar (or --file tags.json)
|
|
458
460
|
${cli} guide what this workspace's data holds, how fresh it is, and what is missing: read it first
|
package/dist/main.js
CHANGED
|
@@ -5,10 +5,11 @@ import { mkdir, readFile, stat } from "node:fs/promises";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
|
-
import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, getSigned, IMPORT_LIMITS, DOCUMENT_LIMITS, META_IDS_MAX, PROFILE_MESSAGING_KINDS, OUTCOME_PARTS, FACET_WORDS, GOOGLE_ADS_IDS_MAX, IMPORT_NAME_PATTERN, AGENT_TABLE_LIMITS, AGENT_TABLE_NAME_PATTERN, putSigned, CLIENT_FILE, parseClientName, readSavedLogins, removeCredentials, resolveApiUrl, resolveConnection, resolveLogin, UnreadableCredentialsError, writeCredentials, OUTBOUND_PUSH_MAX, entryTagFlags, streamSigned, } from "./api.js";
|
|
8
|
+
import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, getSigned, IMPORT_LIMITS, DOCUMENT_LIMITS, META_IDS_MAX, PROFILE_MESSAGING_KINDS, OUTCOME_PARTS, FACET_WORDS, GOOGLE_ADS_IDS_MAX, IMPORT_NAME_PATTERN, AGENT_TABLE_LIMITS, AGENT_TABLE_NAME_PATTERN, putSigned, CLIENT_FILE, parseClientName, readSavedLogins, removeCredentials, resolveApiUrl, chosenApiUrl, fetchApiVersion, resolveConnection, resolveLogin, UnreadableCredentialsError, writeCredentials, OUTBOUND_PUSH_MAX, entryTagFlags, streamSigned, } from "./api.js";
|
|
9
9
|
import { brand, envPrefix } from "./brand.js";
|
|
10
10
|
import { campaignLayout, money, moneyLayout } from "./report-columns.js";
|
|
11
11
|
import { renderDataMap, renderQuestions } from "./map.js";
|
|
12
|
+
import { renderUnattributedCalls } from "./unattributed-calls.js";
|
|
12
13
|
import { apiSetupPath, parseAuthFlag, parseQueryFlags, setupText, submittedText, testText, tryText, waitingText, } from "./api-setup.js";
|
|
13
14
|
import { answerRow, batches, ENRICH_EMAIL_USAGE, parsePeople, parseSteps } from "./enrich.js";
|
|
14
15
|
import { sendCopies, sendStatusChanges } from "./meta-progress.js";
|
|
@@ -29,6 +30,7 @@ import { AGENT_ENV, HARNESS_COMMAND_ENV, RUNNER_TOKEN_ENV, runLoop, runOnce } fr
|
|
|
29
30
|
import { npmRegistry } from "./runner/self-update.js";
|
|
30
31
|
import { SKILL_NAME_PATTERN } from "./skill-zip.js";
|
|
31
32
|
import { installSkillFolder, operatorConnection, packSkill, parseSkillRef, readSkillFolder, unpackSkill, } from "./skills.js";
|
|
33
|
+
import { ownVersion } from "./version-check.js";
|
|
32
34
|
import { versionAtLeast } from "./version.js";
|
|
33
35
|
const DEFAULT_WAIT_SECONDS = 300;
|
|
34
36
|
const META_AD_PAUSE_USAGE = 'meta ad pause <ad-id> --idempotency-key <key> --reason "<why>" [--name "<expected>"] [--run <run-id>]';
|
|
@@ -114,6 +116,25 @@ const commands = {
|
|
|
114
116
|
console.log(`\n${clientReason(choice)}`);
|
|
115
117
|
},
|
|
116
118
|
},
|
|
119
|
+
version: {
|
|
120
|
+
usage: "version this CLI's version, and the API's: the commit it runs and the latest CLI it ships (also --version)",
|
|
121
|
+
async run(args, options) {
|
|
122
|
+
if (args.length > 0)
|
|
123
|
+
throw new CliError("Usage: version", exitCodes.usage);
|
|
124
|
+
const url = await chosenApiUrl(options);
|
|
125
|
+
const api = await fetchApiVersion(url).then(({ api }) => ({ url, commit: api.commit, cliLatest: api.cliLatest }), (error) => {
|
|
126
|
+
if (!(error instanceof CliError))
|
|
127
|
+
throw error;
|
|
128
|
+
console.error(`error: ${error.message}`);
|
|
129
|
+
return null;
|
|
130
|
+
});
|
|
131
|
+
if (options.format === "json")
|
|
132
|
+
return printJson({ cli: ownVersion(), api });
|
|
133
|
+
console.log(`CLI ${ownVersion()}`);
|
|
134
|
+
if (api)
|
|
135
|
+
console.log(`API ${api.url} commit ${api.commit ?? "unknown"}, latest CLI ${api.cliLatest}`);
|
|
136
|
+
},
|
|
137
|
+
},
|
|
117
138
|
"knowledge search": {
|
|
118
139
|
usage: 'knowledge search ["<question>"] [--limit <n>] [--kind message|transcript|document|note] [--library <slug>] [--angle <v>] [--hook-kind <v>] [--ad-format <v>] [--awareness <v>]',
|
|
119
140
|
async run(args, options) {
|
|
@@ -557,6 +578,21 @@ const commands = {
|
|
|
557
578
|
printLeftOut(campaignResults.left_out, "the totals (the rows keep it)");
|
|
558
579
|
},
|
|
559
580
|
},
|
|
581
|
+
"calls unattributed": {
|
|
582
|
+
usage: "calls unattributed [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] [--attribution first_touch|last_touch] [--limit <n>] (booked calls credited to no Facebook ad, and why each one has none)",
|
|
583
|
+
async run(args, options) {
|
|
584
|
+
if (args.length > 0)
|
|
585
|
+
throw new CliError("Usage: calls unattributed [--from <date>] [--to <date>] [--attribution first_touch|last_touch] [--limit <n>]", exitCodes.usage);
|
|
586
|
+
const touch = options.attribution === undefined ? undefined : CALL_TOUCHES.get(options.attribution);
|
|
587
|
+
if (options.attribution !== undefined && touch === undefined)
|
|
588
|
+
throw new CliError("--attribution is first_touch or last_touch.", exitCodes.usage);
|
|
589
|
+
const query = reportQuery(options, { touch, limit: options.limit });
|
|
590
|
+
const { unattributedCalls } = await api(await resolveConnection(options), `/api/v1/reports/unattributed-calls${query}`);
|
|
591
|
+
if (options.format === "json")
|
|
592
|
+
return printJson({ unattributedCalls });
|
|
593
|
+
process.stdout.write(renderUnattributedCalls(unattributedCalls));
|
|
594
|
+
},
|
|
595
|
+
},
|
|
560
596
|
"tools list": {
|
|
561
597
|
usage: "tools list",
|
|
562
598
|
async run(_args, options) {
|
|
@@ -4033,8 +4069,7 @@ async function withRunner(options, work) {
|
|
|
4033
4069
|
await work({
|
|
4034
4070
|
apiUrl: resolveApiUrl(options.apiUrl, null),
|
|
4035
4071
|
token,
|
|
4036
|
-
|
|
4037
|
-
cliVersion: JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version,
|
|
4072
|
+
cliVersion: ownVersion(),
|
|
4038
4073
|
harnessCommand: process.env[HARNESS_COMMAND_ENV] || undefined,
|
|
4039
4074
|
openRouterKey: process.env[OPENROUTER_KEY_ENV] || undefined,
|
|
4040
4075
|
maxSeconds: options.maxSeconds,
|
|
@@ -4430,6 +4465,10 @@ function amountOption(flag, value) {
|
|
|
4430
4465
|
throw new CliError(`${flag} must be a number above 0, such as 10000.`, exitCodes.usage);
|
|
4431
4466
|
return n;
|
|
4432
4467
|
}
|
|
4468
|
+
const CALL_TOUCHES = new Map([
|
|
4469
|
+
["first_touch", "first"],
|
|
4470
|
+
["last_touch", "last"],
|
|
4471
|
+
]);
|
|
4433
4472
|
function reportQuery(options, extra) {
|
|
4434
4473
|
const query = new URLSearchParams();
|
|
4435
4474
|
for (const [name, value] of Object.entries({ from: options.from, to: options.to, ...extra }))
|
|
@@ -5020,8 +5059,8 @@ Options:
|
|
|
5020
5059
|
--where <json | @file.json> tables delete-rows: {column: value} pairs a row must all match to be deleted
|
|
5021
5060
|
--mode append|replace tables import: add the file's rows (default) or replace every row the table has
|
|
5022
5061
|
--yes confirm intake delete, import remove, tables drop, content collections remove or remove-item, which delete saved data for good
|
|
5023
|
-
--from <YYYY-MM-DD> first day funnel, campaigns, money, metrics query, brief, pages results and messages results cover (default: 29 days before --to)
|
|
5024
|
-
--to <YYYY-MM-DD> last day funnel, campaigns, money, metrics query, brief, pages results and messages results cover (default: today in the workspace reporting time zone);
|
|
5062
|
+
--from <YYYY-MM-DD> first day funnel, campaigns, money, calls unattributed, metrics query, brief, pages results and messages results cover (default: 29 days before --to)
|
|
5063
|
+
--to <YYYY-MM-DD> last day funnel, campaigns, money, calls unattributed, metrics query, brief, pages results and messages results cover (default: today in the workspace reporting time zone);
|
|
5025
5064
|
for outbound push, where the leads go: instantly:<campaign-id>
|
|
5026
5065
|
--grain <grain> how funnel and money group days: day (default), week (starting Monday) or month
|
|
5027
5066
|
--month <YYYY-MM> the month tools usage covers, in the workspace reporting time zone (default: this month)
|
|
@@ -5031,7 +5070,8 @@ Options:
|
|
|
5031
5070
|
Facebook ad, ad set or ad name (ad, adset, creative), or by a confirmed custom label (metrics custom list); repeat for
|
|
5032
5071
|
several
|
|
5033
5072
|
--where <dim>=<v,v> metrics query and feeds create: only rows with one of these values; <dim>!=<v,v> keeps the rest; repeat for several
|
|
5034
|
-
--attribution first_touch|last_touch metrics query and feeds create: which ad touch gets the credit for leads and sales by channel or campaign
|
|
5073
|
+
--attribution first_touch|last_touch metrics query and feeds create: which ad touch gets the credit for leads and sales by channel or campaign;
|
|
5074
|
+
calls unattributed: which touch credits each booked call
|
|
5035
5075
|
--last-days <n> feeds create: how many days the sheet link covers, ending today and counting it (default 30, at most 400)
|
|
5036
5076
|
(default: first_touch)
|
|
5037
5077
|
--channel <channel> only this channel's campaigns (and, with brief, its ads): meta, google, affiliate, a utm_source, unknown or recorded_in_crm;
|
|
@@ -5157,7 +5197,7 @@ Options:
|
|
|
5157
5197
|
first; repeat for several
|
|
5158
5198
|
--order desc|asc content search's sort direction (default: desc, best or newest first)
|
|
5159
5199
|
--offset <n> api setup, api try: where in the guide or the answer the slice starts, in characters
|
|
5160
|
-
--limit <n> api setup, api try: how many characters of the guide (default 20,000) or the answer (default 65,536); how many results content search, creatives list, media list, campaigns, voice, ads tags, content tags,
|
|
5200
|
+
--limit <n> api setup, api try: how many characters of the guide (default 20,000) or the answer (default 65,536); how many results content search, creatives list, media list, campaigns, calls unattributed (default 200, at most 1000), voice, ads tags, content tags,
|
|
5161
5201
|
content to-check and seo page-two return, how many ads ads autotag tags and how many items content autotag
|
|
5162
5202
|
tags, or how many ads each ads tag-formats batch looks at (default 2000; seo page-two: 50)
|
|
5163
5203
|
--library <slug> with knowledge search, only this library: creative-cartel, exodus or meta-policy
|
|
@@ -5213,11 +5253,14 @@ the runner starts instead of Claude Code or Codex.
|
|
|
5213
5253
|
|
|
5214
5254
|
Run \`${brand.slug} docs\` for the full guide.
|
|
5215
5255
|
`;
|
|
5216
|
-
/**
|
|
5217
|
-
|
|
5256
|
+
/**
|
|
5257
|
+
* A bare --questions (map's switch) or --version (the version command, with no command before it) as an empty value,
|
|
5258
|
+
* since funnels draft gives --questions a value, and funnels show and dashboards rollback --version.
|
|
5259
|
+
*/
|
|
5260
|
+
const bareSwitches = (argv) => argv.map((arg, i) => ((arg === "--questions" || arg === "--version") && (i === argv.length - 1 || argv[i + 1].startsWith("-")) ? `${arg}=` : arg));
|
|
5218
5261
|
function parseOptions(argv) {
|
|
5219
5262
|
const { values, positionals } = parseArgs({
|
|
5220
|
-
args:
|
|
5263
|
+
args: bareSwitches(argv),
|
|
5221
5264
|
allowPositionals: true,
|
|
5222
5265
|
options: {
|
|
5223
5266
|
format: { type: "string", default: "table" },
|
|
@@ -6982,7 +7025,7 @@ async function newerCliHint() {
|
|
|
6982
7025
|
// Tests run the CLI with NODE_ENV=test; they ask a registry only when they name one, so npm's real version cannot change their output.
|
|
6983
7026
|
if (process.env.NODE_ENV === "test" && !process.env.npm_config_registry)
|
|
6984
7027
|
return undefined;
|
|
6985
|
-
const version =
|
|
7028
|
+
const version = ownVersion();
|
|
6986
7029
|
const latest = await npmRegistry.latest(3_000).catch(() => undefined);
|
|
6987
7030
|
if (latest === undefined || versionAtLeast(version, latest))
|
|
6988
7031
|
return undefined;
|
|
@@ -6990,6 +7033,8 @@ async function newerCliHint() {
|
|
|
6990
7033
|
}
|
|
6991
7034
|
async function main(argv) {
|
|
6992
7035
|
const { positionals, options, help: wantsHelp } = parseOptions(argv);
|
|
7036
|
+
if (positionals.length === 0 && options.version === "" && !wantsHelp)
|
|
7037
|
+
return commands.version.run([], options);
|
|
6993
7038
|
if (wantsHelp || positionals.length === 0) {
|
|
6994
7039
|
process.stdout.write(help);
|
|
6995
7040
|
return;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { renderTable } from "./table.js";
|
|
2
|
+
const CALL_COLUMNS = ["booked_on", "started_on", "credit", "record_source", "contact_id", "outcome", "channel", "campaign_id", "ad_id", "landing_page_key"];
|
|
3
|
+
export function renderUnattributedCalls(report) {
|
|
4
|
+
const range = `${report.from} to ${report.to}`;
|
|
5
|
+
if (report.booked_calls === null)
|
|
6
|
+
return `No booked-call source is connected, so there are no booked calls from ${range}.\n`;
|
|
7
|
+
const lines = [
|
|
8
|
+
`Booked calls ${range}, credited to the ${report.touch} touch: ${report.booked_calls} in all, ${report.with_ad} with a Facebook ad, ${report.without_ad} without one.`,
|
|
9
|
+
];
|
|
10
|
+
if (report.without_ad === 0)
|
|
11
|
+
return `${lines[0]}\n`;
|
|
12
|
+
lines.push("", "Why calls have no ad:", renderTable(["credit", "calls"], report.by_credit.filter((c) => c.calls > 0)));
|
|
13
|
+
lines.push("", "Calls with no ad, newest first:", renderTable(CALL_COLUMNS, report.calls));
|
|
14
|
+
if (report.truncated)
|
|
15
|
+
lines.push(`Showing the newest ${report.calls.length} of ${report.without_ad}; --limit shows up to 1000.`);
|
|
16
|
+
return `${lines.join("\n")}\n`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { brand } from "./brand.js";
|
|
3
|
+
import { VERSION_PATTERN, versionAtLeast } from "./version.js";
|
|
4
|
+
let own;
|
|
5
|
+
/** This CLI's version, from the package.json one folder above dist/ (and src/). */
|
|
6
|
+
export function ownVersion() {
|
|
7
|
+
own ??= JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
8
|
+
return own;
|
|
9
|
+
}
|
|
10
|
+
/** The response header that names the latest CLI version the API's build ships. */
|
|
11
|
+
export const CLI_LATEST_HEADER = `x-${brand.slug}-cli-latest`;
|
|
12
|
+
/** The line to warn with when `latest` is a newer version than `version`; nothing when it is not, or is no version at all. */
|
|
13
|
+
export function cliUpdateWarning(version, latest) {
|
|
14
|
+
if (!latest || !VERSION_PATTERN.test(latest) || !VERSION_PATTERN.test(version) || versionAtLeast(version, latest))
|
|
15
|
+
return undefined;
|
|
16
|
+
return `warning: this CLI is ${version} and the API's latest is ${latest}. Update: npm install -g ${brand.npmPackage}@latest`;
|
|
17
|
+
}
|
|
18
|
+
let warned = false;
|
|
19
|
+
/** Warns on stderr, once per process, when an API response says a newer CLI is out. */
|
|
20
|
+
export function noteLatestCli(response) {
|
|
21
|
+
if (warned)
|
|
22
|
+
return;
|
|
23
|
+
const warning = cliUpdateWarning(ownVersion(), response.headers.get(CLI_LATEST_HEADER));
|
|
24
|
+
if (warning === undefined)
|
|
25
|
+
return;
|
|
26
|
+
warned = true;
|
|
27
|
+
process.stderr.write(`${warning}\n`);
|
|
28
|
+
}
|