@wrangled/cli 0.3.150 → 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
@@ -453,6 +453,7 @@ A scheduled agent's run acts as its skill's agent, the way the Facebook ads agen
453
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
454
454
  ${cli} logout [--client <name>] delete the default saved login, or one client's
455
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
456
457
  ${cli} knowledge search "<question>" [--limit <n>] [--kind <kind>] [--hook-kind <v> ...] short, cited snippets from the know-how libraries (keys with library access)
457
458
  ${cli} knowledge tag <entry-id> --hook-kind <v> --confidence <c> tag a library entry with the marketing grammar (or --file tags.json)
458
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,7 +5,7 @@ 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";
@@ -29,6 +29,7 @@ import { AGENT_ENV, HARNESS_COMMAND_ENV, RUNNER_TOKEN_ENV, runLoop, runOnce } fr
29
29
  import { npmRegistry } from "./runner/self-update.js";
30
30
  import { SKILL_NAME_PATTERN } from "./skill-zip.js";
31
31
  import { installSkillFolder, operatorConnection, packSkill, parseSkillRef, readSkillFolder, unpackSkill, } from "./skills.js";
32
+ import { ownVersion } from "./version-check.js";
32
33
  import { versionAtLeast } from "./version.js";
33
34
  const DEFAULT_WAIT_SECONDS = 300;
34
35
  const META_AD_PAUSE_USAGE = 'meta ad pause <ad-id> --idempotency-key <key> --reason "<why>" [--name "<expected>"] [--run <run-id>]';
@@ -114,6 +115,25 @@ const commands = {
114
115
  console.log(`\n${clientReason(choice)}`);
115
116
  },
116
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
+ },
117
137
  "knowledge search": {
118
138
  usage: 'knowledge search ["<question>"] [--limit <n>] [--kind message|transcript|document|note] [--library <slug>] [--angle <v>] [--hook-kind <v>] [--ad-format <v>] [--awareness <v>]',
119
139
  async run(args, options) {
@@ -4033,8 +4053,7 @@ async function withRunner(options, work) {
4033
4053
  await work({
4034
4054
  apiUrl: resolveApiUrl(options.apiUrl, null),
4035
4055
  token,
4036
- // main.js sits in dist/ (main.ts in src/), one below the package's package.json.
4037
- cliVersion: JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version,
4056
+ cliVersion: ownVersion(),
4038
4057
  harnessCommand: process.env[HARNESS_COMMAND_ENV] || undefined,
4039
4058
  openRouterKey: process.env[OPENROUTER_KEY_ENV] || undefined,
4040
4059
  maxSeconds: options.maxSeconds,
@@ -5213,11 +5232,14 @@ the runner starts instead of Claude Code or Codex.
5213
5232
 
5214
5233
  Run \`${brand.slug} docs\` for the full guide.
5215
5234
  `;
5216
- /** A bare --questions (map's switch) as an empty value, since funnels draft gives --questions a value. */
5217
- 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));
5218
5240
  function parseOptions(argv) {
5219
5241
  const { values, positionals } = parseArgs({
5220
- args: bareQuestions(argv),
5242
+ args: bareSwitches(argv),
5221
5243
  allowPositionals: true,
5222
5244
  options: {
5223
5245
  format: { type: "string", default: "table" },
@@ -6982,7 +7004,7 @@ async function newerCliHint() {
6982
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.
6983
7005
  if (process.env.NODE_ENV === "test" && !process.env.npm_config_registry)
6984
7006
  return undefined;
6985
- const version = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version;
7007
+ const version = ownVersion();
6986
7008
  const latest = await npmRegistry.latest(3_000).catch(() => undefined);
6987
7009
  if (latest === undefined || versionAtLeast(version, latest))
6988
7010
  return undefined;
@@ -6990,6 +7012,8 @@ async function newerCliHint() {
6990
7012
  }
6991
7013
  async function main(argv) {
6992
7014
  const { positionals, options, help: wantsHelp } = parseOptions(argv);
7015
+ if (positionals.length === 0 && options.version === "" && !wantsHelp)
7016
+ return commands.version.run([], options);
6993
7017
  if (wantsHelp || positionals.length === 0) {
6994
7018
  process.stdout.write(help);
6995
7019
  return;
@@ -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.150",
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": {