@wrangled/cli 0.3.149 → 0.3.151

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 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
- const cause = error.cause?.message ?? error.message;
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
@@ -156,6 +156,7 @@ For "how is the funnel doing?", "which campaigns are working?" and "where does t
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
158
  - \`${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
+ - 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.
159
160
  - 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.
160
161
  - \`${cli} metrics query --metric <name> [--by <dimension>]...\` gives any of the standard numbers by name, split and filtered your way, such as \`--metric spend --metric cost_per_lead --by channel --where channel!=affiliate --sort=-spend\`. It sums the parts before dividing, keeps currencies apart, credits leads and sales by channel or campaign to the first touch (\`--attribution last_touch\` for the last), and prints an empty number, never 0, for a source that is not connected. An unknown name fails with the list of real ones. Use it before writing SQL for a number these reports do not split the way you need.
161
162
  - \`${cli} metrics search "<everyday words>"\` finds the name \`metrics query\` takes from your own words, such as \`"show rate"\`, \`"cost per booked call"\` or \`"first time buyers"\`: each match with what it means, which of the client's sources feed it and whether they are connected, and for a split like channel or campaign up to five of the client's own values. \`${cli} metrics list\` prints every number and split. The client's own confirmed labels and numbers show beside the catalog's, marked "(custom)". Search before guessing a name.
@@ -452,6 +453,7 @@ A scheduled agent's run acts as its skill's agent, the way the Facebook ads agen
452
453
  ${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
453
454
  ${cli} logout [--client <name>] delete the default saved login, or one client's
454
455
  ${cli} whoami show the current workspace, library access, and which client's key is in use and why
456
+ ${cli} version this CLI's version, the commit the API runs, and the latest CLI it ships (also --version); needs no login
455
457
  ${cli} knowledge search "<question>" [--limit <n>] [--kind <kind>] [--hook-kind <v> ...] short, cited snippets from the know-how libraries (keys with library access)
456
458
  ${cli} knowledge tag <entry-id> --hook-kind <v> --confidence <c> tag a library entry with the marketing grammar (or --file tags.json)
457
459
  ${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,8 +5,9 @@ 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
+ import { campaignLayout, money, moneyLayout } from "./report-columns.js";
10
11
  import { renderDataMap, renderQuestions } from "./map.js";
11
12
  import { apiSetupPath, parseAuthFlag, parseQueryFlags, setupText, submittedText, testText, tryText, waitingText, } from "./api-setup.js";
12
13
  import { answerRow, batches, ENRICH_EMAIL_USAGE, parsePeople, parseSteps } from "./enrich.js";
@@ -28,6 +29,7 @@ import { AGENT_ENV, HARNESS_COMMAND_ENV, RUNNER_TOKEN_ENV, runLoop, runOnce } fr
28
29
  import { npmRegistry } from "./runner/self-update.js";
29
30
  import { SKILL_NAME_PATTERN } from "./skill-zip.js";
30
31
  import { installSkillFolder, operatorConnection, packSkill, parseSkillRef, readSkillFolder, unpackSkill, } from "./skills.js";
32
+ import { ownVersion } from "./version-check.js";
31
33
  import { versionAtLeast } from "./version.js";
32
34
  const DEFAULT_WAIT_SECONDS = 300;
33
35
  const META_AD_PAUSE_USAGE = 'meta ad pause <ad-id> --idempotency-key <key> --reason "<why>" [--name "<expected>"] [--run <run-id>]';
@@ -113,6 +115,25 @@ const commands = {
113
115
  console.log(`\n${clientReason(choice)}`);
114
116
  },
115
117
  },
118
+ version: {
119
+ usage: "version this CLI's version, and the API's: the commit it runs and the latest CLI it ships (also --version)",
120
+ async run(args, options) {
121
+ if (args.length > 0)
122
+ throw new CliError("Usage: version", exitCodes.usage);
123
+ const url = await chosenApiUrl(options);
124
+ const api = await fetchApiVersion(url).then(({ api }) => ({ url, commit: api.commit, cliLatest: api.cliLatest }), (error) => {
125
+ if (!(error instanceof CliError))
126
+ throw error;
127
+ console.error(`error: ${error.message}`);
128
+ return null;
129
+ });
130
+ if (options.format === "json")
131
+ return printJson({ cli: ownVersion(), api });
132
+ console.log(`CLI ${ownVersion()}`);
133
+ if (api)
134
+ console.log(`API ${api.url} commit ${api.commit ?? "unknown"}, latest CLI ${api.cliLatest}`);
135
+ },
136
+ },
116
137
  "knowledge search": {
117
138
  usage: 'knowledge search ["<question>"] [--limit <n>] [--kind message|transcript|document|note] [--library <slug>] [--angle <v>] [--hook-kind <v>] [--ad-format <v>] [--awareness <v>]',
118
139
  async run(args, options) {
@@ -242,7 +263,7 @@ const commands = {
242
263
  const { funnel } = await api(await resolveConnection(options), `/api/v1/reports/funnel${query}`);
243
264
  if (options.format === "json")
244
265
  return printJson({ funnel });
245
- printReport(`Funnel by ${funnel.grain}`, funnel, ["period", "currency"], FUNNEL_NUMBERS);
266
+ printReport(`Funnel by ${funnel.grain}`, funnel, ["period", "currency"], { numbers: FUNNEL_NUMBERS, before: [], after: [] });
246
267
  printLeftOut(funnel.left_out, "sales, revenue_net and roas");
247
268
  },
248
269
  },
@@ -480,7 +501,7 @@ const commands = {
480
501
  const { money } = await api(await resolveConnection(options), `/api/v1/reports/money${query}`);
481
502
  if (options.format === "json")
482
503
  return printJson({ money });
483
- printReport(`Money by ${money.grain}`, money, ["period", "currency"], MONEY_NUMBERS);
504
+ printReport(`Money by ${money.grain}`, money, ["period", "currency"], moneyLayout(money));
484
505
  if (money.by_source.length > 0)
485
506
  console.log(`\nMoney in by payment source:\n${renderTable(["source", "currency", "gross", "refunds", "net", "orders"], money.by_source)}`);
486
507
  if (money.store_orders_in_stripe?.length) {
@@ -552,7 +573,7 @@ const commands = {
552
573
  if (options.format === "json")
553
574
  return printJson({ campaignResults });
554
575
  const title = campaignResults.channel === null ? "Campaigns, best first" : `${campaignResults.channel} campaigns, best first`;
555
- printReport(title, campaignResults, ["channel", "campaign_id", "campaign_name", "currency"], CAMPAIGN_NUMBERS);
576
+ printReport(title, campaignResults, ["channel", "campaign_id", "campaign_name", "currency"], campaignLayout(campaignResults));
556
577
  printLeftOut(campaignResults.left_out, "the totals (the rows keep it)");
557
578
  },
558
579
  },
@@ -4032,8 +4053,7 @@ async function withRunner(options, work) {
4032
4053
  await work({
4033
4054
  apiUrl: resolveApiUrl(options.apiUrl, null),
4034
4055
  token,
4035
- // main.js sits in dist/ (main.ts in src/), one below the package's package.json.
4036
- cliVersion: JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version,
4056
+ cliVersion: ownVersion(),
4037
4057
  harnessCommand: process.env[HARNESS_COMMAND_ENV] || undefined,
4038
4058
  openRouterKey: process.env[OPENROUTER_KEY_ENV] || undefined,
4039
4059
  maxSeconds: options.maxSeconds,
@@ -4103,27 +4123,6 @@ function historyText(history) {
4103
4123
  return main + catchingUp.join("");
4104
4124
  }
4105
4125
  const FUNNEL_NUMBERS = ["spend", "leads", "booked_calls", "calls_held", "sales_count", "revenue_net", "new_buyers", "cost_per_lead", "cost_per_booked_call", "cost_per_sale", "cost_per_new_buyer", "roas"];
4106
- const MONEY_NUMBERS = ["gross", "refunds", "net", "orders", "ad_spend", "affiliate_payouts", "left_after_ad_costs", "roas"];
4107
- const CAMPAIGN_NUMBERS = [
4108
- "spend",
4109
- "leads_first_touch",
4110
- "leads_last_touch",
4111
- "booked_calls_first_touch",
4112
- "sales_first_touch",
4113
- "revenue_first_touch",
4114
- "revenue_last_touch",
4115
- "cost_per_lead_first_touch",
4116
- "roas_first_touch",
4117
- "roas_last_touch",
4118
- ];
4119
- function money(value, currency) {
4120
- try {
4121
- return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(value);
4122
- }
4123
- catch {
4124
- return `${value.toFixed(2)} ${currency}`;
4125
- }
4126
- }
4127
4126
  function newIdeasLine({ target, proposedToday, stalled, costPerResult: cpr }, ideasPerDay) {
4128
4127
  const split = `${target} of ${ideasPerDay}${proposedToday > 0 ? `, ${proposedToday} already proposed` : ""}`;
4129
4128
  if (!cpr)
@@ -4578,13 +4577,19 @@ function printLeftOut(leftOut, figures) {
4578
4577
  const rows = leftOut.flatMap((l) => l.amounts.map((a) => ({ fact: l.fact_id, currency: a.currency, revenue: a.revenue, sales: a.sales, rule: l.summary })));
4579
4578
  console.log(`\nLeft out of ${figures} by confirmed leave-out rules:\n${renderTable(["fact", "currency", "revenue", "sales", "rule"], rows)}`);
4580
4579
  }
4581
- function printReport(title, report, labels, numbers) {
4580
+ function printReport(title, report, labels, { numbers, before, after }) {
4581
+ if (before.length > 0)
4582
+ console.log(`${before.join("\n")}\n`);
4582
4583
  if (report.rows.length === 0)
4583
- return console.log(`No numbers from ${report.from} to ${report.to}.`);
4584
- console.log(`${title}, ${report.from} to ${report.to}:`);
4585
- console.log(renderTable([...labels, ...numbers], report.rows));
4586
- console.log("\nTotals:");
4587
- console.log(renderTable(["currency", ...numbers], report.totals));
4584
+ console.log(`No numbers from ${report.from} to ${report.to}.`);
4585
+ else {
4586
+ console.log(`${title}, ${report.from} to ${report.to}:`);
4587
+ console.log(renderTable([...labels, ...numbers], report.rows));
4588
+ console.log("\nTotals:");
4589
+ console.log(renderTable(["currency", ...numbers], report.totals));
4590
+ }
4591
+ for (const line of after)
4592
+ console.log(line);
4588
4593
  }
4589
4594
  const agentPath = (options) => `/api/v1/agents/${encodeURIComponent(options.agent)}`;
4590
4595
  const FACTS_PATH = "/api/v1/facts";
@@ -5227,11 +5232,14 @@ the runner starts instead of Claude Code or Codex.
5227
5232
 
5228
5233
  Run \`${brand.slug} docs\` for the full guide.
5229
5234
  `;
5230
- /** A bare --questions (map's switch) as an empty value, since funnels draft gives --questions a value. */
5231
- const bareQuestions = (argv) => argv.map((arg, i) => (arg === "--questions" && (i === argv.length - 1 || argv[i + 1].startsWith("-")) ? "--questions=" : arg));
5235
+ /**
5236
+ * A bare --questions (map's switch) or --version (the version command, with no command before it) as an empty value,
5237
+ * since funnels draft gives --questions a value, and funnels show and dashboards rollback --version.
5238
+ */
5239
+ const bareSwitches = (argv) => argv.map((arg, i) => ((arg === "--questions" || arg === "--version") && (i === argv.length - 1 || argv[i + 1].startsWith("-")) ? `${arg}=` : arg));
5232
5240
  function parseOptions(argv) {
5233
5241
  const { values, positionals } = parseArgs({
5234
- args: bareQuestions(argv),
5242
+ args: bareSwitches(argv),
5235
5243
  allowPositionals: true,
5236
5244
  options: {
5237
5245
  format: { type: "string", default: "table" },
@@ -6996,7 +7004,7 @@ async function newerCliHint() {
6996
7004
  // 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.
6997
7005
  if (process.env.NODE_ENV === "test" && !process.env.npm_config_registry)
6998
7006
  return undefined;
6999
- const version = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version;
7007
+ const version = ownVersion();
7000
7008
  const latest = await npmRegistry.latest(3_000).catch(() => undefined);
7001
7009
  if (latest === undefined || versionAtLeast(version, latest))
7002
7010
  return undefined;
@@ -7004,6 +7012,8 @@ async function newerCliHint() {
7004
7012
  }
7005
7013
  async function main(argv) {
7006
7014
  const { positionals, options, help: wantsHelp } = parseOptions(argv);
7015
+ if (positionals.length === 0 && options.version === "" && !wantsHelp)
7016
+ return commands.version.run([], options);
7007
7017
  if (wantsHelp || positionals.length === 0) {
7008
7018
  process.stdout.write(help);
7009
7019
  return;
@@ -0,0 +1,57 @@
1
+ import { brand } from "./brand.js";
2
+ export const MONEY_NUMBERS = ["gross", "refunds", "net", "orders", "ad_spend", "affiliate_payouts", "left_after_ad_costs", "roas"];
3
+ export const CAMPAIGN_NUMBERS = [
4
+ "spend",
5
+ "leads_first_touch",
6
+ "leads_last_touch",
7
+ "booked_calls_first_touch",
8
+ "sales_first_touch",
9
+ "revenue_first_touch",
10
+ "revenue_last_touch",
11
+ "cost_per_lead_first_touch",
12
+ "roas_first_touch",
13
+ "roas_last_touch",
14
+ ];
15
+ const MONEY_IN_NUMBERS = new Set(["gross", "refunds", "net", "orders", "left_after_ad_costs", "roas"]);
16
+ const TRIPLE_WHALE_NUMBERS = ["orders_triple_whale", "revenue_triple_whale", "roas_triple_whale"];
17
+ const NO_CRM_SALES = "no payment tool, store or CRM sales are connected";
18
+ export function money(value, currency) {
19
+ try {
20
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(value);
21
+ }
22
+ catch {
23
+ return `${value.toFixed(2)} ${currency}`;
24
+ }
25
+ }
26
+ /** A server older than this CLI sends no sales_from; its money in is daily_sales'. */
27
+ export function moneyLayout({ sales_from: from }) {
28
+ if (from === undefined || from.source === "daily_sales")
29
+ return { numbers: MONEY_NUMBERS, before: [], after: [] };
30
+ if (from.source === null)
31
+ return { numbers: MONEY_NUMBERS.filter((n) => !MONEY_IN_NUMBERS.has(n)), before: [from.why], after: [] };
32
+ return { numbers: MONEY_NUMBERS, before: [], after: [`\nMoney in is Triple Whale's ${from.detail}, because ${NO_CRM_SALES}.`] };
33
+ }
34
+ const blankEverywhere = (column, rows) => rows.every((row) => row[column] === null || row[column] === undefined);
35
+ /**
36
+ * Without daily_sales, the first- and last-touch columns blank in every row and total are left out, and with Triple
37
+ * Whale its own orders, revenue and return on ad spend are added.
38
+ */
39
+ export function campaignLayout(report) {
40
+ const from = report.sales_from;
41
+ if (from === undefined || from.source === "daily_sales")
42
+ return { numbers: CAMPAIGN_NUMBERS, before: [], after: [] };
43
+ const all = [...report.rows, ...report.totals];
44
+ const touches = CAMPAIGN_NUMBERS.filter((n) => n === "spend" || !blankEverywhere(n, all));
45
+ if (from.source === null)
46
+ return { numbers: touches, before: [from.why], after: [] };
47
+ const others = (report.triple_whale_other_campaigns ?? []).map(({ currency, orders, revenue }) => `Triple Whale credits ${orders} more ${orders === 1 ? "order" : "orders"}${revenue === null || currency === null ? "" : ` (${money(revenue, currency)})`} to Facebook ads that aren't in the ad accounts ${brand.name} is connected to, so they aren't in the rows above.`);
48
+ return {
49
+ numbers: [...touches, ...TRIPLE_WHALE_NUMBERS],
50
+ before: [],
51
+ after: [
52
+ `\nOrders and revenue are what Triple Whale credits to each Facebook campaign (${from.detail}), because ${NO_CRM_SALES}. ` +
53
+ "Triple Whale can credit one order to more than one ad, so these can add up to more than the store's own orders.",
54
+ ...others,
55
+ ],
56
+ };
57
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.149",
3
+ "version": "0.3.151",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {