@canonry/canonry 7.12.0 → 7.14.0

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.
Files changed (34) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +14 -7
  2. package/assets/assets/AuditHistoryPanel-gAkqWz9z.js +1 -0
  3. package/assets/assets/{BacklinksPage-CbRIZhu5.js → BacklinksPage-C67DtHo8.js} +1 -1
  4. package/assets/assets/{HistoryPage-Dfq1vK9k.js → HistoryPage-BQuW6Z6m.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-DcH0zYNN.js → MeasurementPropertyPage-B1XS6DR0.js} +1 -1
  6. package/assets/assets/ProjectPage-CV_7bOA7.js +8 -0
  7. package/assets/assets/{RunsPage-aOeJ60pX.js → RunsPage-CJYihZYC.js} +1 -1
  8. package/assets/assets/SettingsPage-Dd3J2DQE.js +1 -0
  9. package/assets/assets/{SiteHealthSection-CUOt7jeo.js → SiteHealthSection-iktyuzt2.js} +3 -3
  10. package/assets/assets/{TrafficPage-C0fVYizx.js → TrafficPage-CWjK7F3j.js} +2 -2
  11. package/assets/assets/{TrafficSourceDetailPage-LMhXbFlA.js → TrafficSourceDetailPage-D4_AXQNE.js} +1 -1
  12. package/assets/assets/{extract-error-message-5aHy0fsK.js → extract-error-message-Cxefvthz.js} +1 -1
  13. package/assets/assets/{index-BJh8cX5E.js → index-BbmiGIM_.js} +27 -27
  14. package/assets/assets/index-aBS1f2Bj.css +1 -0
  15. package/assets/assets/{react-sigma_core.esm.min-ldX35rMq.js → react-sigma_core.esm.min-DDEPa7AV.js} +1 -1
  16. package/assets/assets/{v2-overview-adapter-CaO9vY-l.js → v2-overview-adapter-CLxlMdb2.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-BTHEFJCI.js → chunk-4LQ4HKVK.js} +5 -5
  19. package/dist/{chunk-AU7OUDWR.js → chunk-A25FO3Z5.js} +64 -13
  20. package/dist/{chunk-GQMSJEMZ.js → chunk-EZRBFH77.js} +233 -54
  21. package/dist/{chunk-DXI2FTSW.js → chunk-KXHPQMBP.js} +93 -31
  22. package/dist/{chunk-EPSHIH63.js → chunk-LUQ5G5OO.js} +3 -3
  23. package/dist/{chunk-7I64KEGM.js → chunk-WHVEDCHY.js} +2 -2
  24. package/dist/{chunk-SM6ZZAPK.js → chunk-ZHXOR2LJ.js} +2 -2
  25. package/dist/cli.js +1300 -1244
  26. package/dist/{demo-server-WJUA4EIG.js → demo-server-SIATTXXS.js} +4 -4
  27. package/dist/index.js +6 -6
  28. package/dist/{intelligence-service-DJRH4Z3D.js → intelligence-service-UEEVETII.js} +2 -2
  29. package/dist/mcp.js +4 -4
  30. package/package.json +13 -13
  31. package/assets/assets/AuditHistoryPanel-Bpkuj-iF.js +0 -1
  32. package/assets/assets/ProjectPage-enF4twWt.js +0 -8
  33. package/assets/assets/SettingsPage-nA0_ImoQ.js +0 -1
  34. package/assets/assets/index-BNeiohKS.css +0 -1
package/dist/cli.js CHANGED
@@ -33,11 +33,11 @@ import {
33
33
  trackCliCommandFinished,
34
34
  trackEvent,
35
35
  waitForServerRuntimeStartup
36
- } from "./chunk-DXI2FTSW.js";
36
+ } from "./chunk-KXHPQMBP.js";
37
37
  import {
38
38
  autoSyncSkills,
39
39
  formatAutoSyncNotice
40
- } from "./chunk-EPSHIH63.js";
40
+ } from "./chunk-LUQ5G5OO.js";
41
41
  import {
42
42
  configExists,
43
43
  createApiClient,
@@ -54,13 +54,13 @@ import {
54
54
  saveConfig,
55
55
  saveConfigPatch,
56
56
  shouldPrintServeSkillsNudge
57
- } from "./chunk-SM6ZZAPK.js";
57
+ } from "./chunk-ZHXOR2LJ.js";
58
58
  import {
59
59
  DEFAULT_DEMO_TRUSTED_PROXIES,
60
60
  trustedProxiesProblem,
61
61
  trustedProxyProblem
62
62
  } from "./chunk-RB2F4D3Z.js";
63
- import "./chunk-7I64KEGM.js";
63
+ import "./chunk-WHVEDCHY.js";
64
64
  import {
65
65
  CliError,
66
66
  EXIT_SYSTEM_ERROR,
@@ -72,7 +72,7 @@ import {
72
72
  runMeasurementDraftAction,
73
73
  systemError,
74
74
  usageError
75
- } from "./chunk-BTHEFJCI.js";
75
+ } from "./chunk-4LQ4HKVK.js";
76
76
  import {
77
77
  CLOUDFLARE_WORKER_BINDINGS,
78
78
  CLOUDFLARE_WORKER_GENERATED_MARKER,
@@ -104,7 +104,7 @@ import {
104
104
  rawEventSamples,
105
105
  readMarketCompetitorNames,
106
106
  runs
107
- } from "./chunk-GQMSJEMZ.js";
107
+ } from "./chunk-EZRBFH77.js";
108
108
  import {
109
109
  AdsDeliverySnapshotStatuses,
110
110
  AdsHistoricalCampaignRollupStatuses,
@@ -234,7 +234,7 @@ import {
234
234
  visibilityCompareSelectionSchema,
235
235
  visibilityReportRequestSchema,
236
236
  winnabilityClassSchema
237
- } from "./chunk-AU7OUDWR.js";
237
+ } from "./chunk-A25FO3Z5.js";
238
238
 
239
239
  // src/cli.ts
240
240
  import { pathToFileURL } from "url";
@@ -784,7 +784,7 @@ function parseDemoTrustedProxies(values) {
784
784
  async function demoCommand(options) {
785
785
  const { host, port } = parseDemoListenOptions(options);
786
786
  const trustProxy = parseDemoTrustedProxies(options.trustProxy);
787
- const { createDemoServer } = await import("./demo-server-WJUA4EIG.js");
787
+ const { createDemoServer } = await import("./demo-server-SIATTXXS.js");
788
788
  const app = await createDemoServer({ trustProxy });
789
789
  let stopping = false;
790
790
  const stop = () => {
@@ -1198,7 +1198,7 @@ No DB writes performed. Re-run without --dry-run to apply.`);
1198
1198
  }
1199
1199
  }
1200
1200
  async function backfillInsightsCommand(project2, opts) {
1201
- const { IntelligenceService } = await import("./intelligence-service-DJRH4Z3D.js");
1201
+ const { IntelligenceService } = await import("./intelligence-service-UEEVETII.js");
1202
1202
  const config = loadConfig();
1203
1203
  const db = createClient(config.database);
1204
1204
  migrate(db);
@@ -13444,1432 +13444,1485 @@ async function clearResults(project2, opts) {
13444
13444
  }
13445
13445
  }
13446
13446
 
13447
- // src/commands/status.ts
13447
+ // src/commands/run.ts
13448
13448
  function getClient21() {
13449
13449
  return createApiClient();
13450
13450
  }
13451
- async function showStatus(project2, format) {
13451
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
13452
+ function shellArg(value) {
13453
+ return /^[\w.-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
13454
+ }
13455
+ function providerAccountStreakLine(provider, streak) {
13456
+ return `${provider} (${streak.code}): failed on its account in each of its last ${streak.consecutiveRuns} runs; called again after ${streak.retryAfter}`;
13457
+ }
13458
+ function providersThatKeepFailing(count) {
13459
+ return count === 1 ? "a provider that keeps failing on its account" : `${count} providers that keep failing on their accounts`;
13460
+ }
13461
+ function providerAccountFixLine(project2) {
13462
+ return `Fix the key, access or billing in the provider's console. Saving a new key, model or endpoint (canonry settings provider <name>) retries at once; canonry run ${shellArg(project2)} --force calls every provider now.`;
13463
+ }
13464
+ function admissionDecisionKey(admission) {
13465
+ return JSON.stringify([
13466
+ admission.refused,
13467
+ admission.retryAfter,
13468
+ Object.entries(admission.providers).sort(([left], [right]) => left.localeCompare(right)).map(([provider, streak]) => [provider, streak.code, streak.consecutiveRuns, streak.since, streak.latestRunId, streak.retryAfter])
13469
+ ]);
13470
+ }
13471
+ function admissionScopeLines(admission, label, showAllowed) {
13472
+ const held = Object.entries(admission.providers);
13473
+ if (held.length === 0) return showAllowed ? [`${label}: no providers held back.`] : [];
13474
+ const retry = label === "Manual sweep" ? `it can retry after ${admission.retryAfter}` : `scheduled sweeps skip their slots until ${admission.retryAfter}`;
13475
+ return [
13476
+ admission.refused ? `${label}: refused (PROVIDERS_FAILING). Every provider it would call keeps failing on its account; ${retry}.` : `${label}: skips ${providersThatKeepFailing(held.length)}; the rest run.`,
13477
+ ...held.map(([provider, streak]) => ` ${providerAccountStreakLine(provider, streak)}`)
13478
+ ];
13479
+ }
13480
+ function runAdmissionLines(project2, admission, manualAdmission) {
13481
+ if (!admission) return [];
13482
+ const differs = manualAdmission !== void 0 && admissionDecisionKey(admission) !== admissionDecisionKey(manualAdmission);
13483
+ const lines = differs ? [...admissionScopeLines(admission, "Scheduled sweep", true), ...admissionScopeLines(manualAdmission, "Manual sweep", true)] : admissionScopeLines(admission, "Next sweep", false);
13484
+ return lines.length > 0 ? [...lines, providerAccountFixLine(project2)] : [];
13485
+ }
13486
+ function printSkippedProviders(project2, skipped) {
13487
+ const held = Object.entries(skipped ?? {});
13488
+ if (held.length === 0) return;
13489
+ console.log(`
13490
+ Not calling ${providersThatKeepFailing(held.length)}:`);
13491
+ for (const [provider, streak] of held) console.log(` ${providerAccountStreakLine(provider, streak)}`);
13492
+ console.log(providerAccountFixLine(project2));
13493
+ }
13494
+ function throwIfWaitedRunFailed(waited) {
13495
+ const failed = waited.filter((r) => r.status === RunStatuses.failed);
13496
+ if (failed.length === 0) return;
13497
+ const reason = (r) => r.error ? formatRunErrorOneLine(r.error) : null;
13498
+ const label = (r) => [r.project, r.location ? `(${r.location})` : null, `run ${r.runId}`].filter(Boolean).join(" ");
13499
+ const message = waited.length === 1 ? `Run ${failed[0].runId} failed${reason(failed[0]) ? `: ${reason(failed[0])}` : ""}` : `${failed.length} of ${waited.length} runs failed: ${failed.map((r) => `${label(r)}${reason(r) ? `: ${reason(r)}` : ""}`).join("; ")}`;
13500
+ throw new CliError({
13501
+ code: "RUN_FAILED",
13502
+ message,
13503
+ exitCode: EXIT_SYSTEM_ERROR,
13504
+ details: {
13505
+ waitedRunCount: waited.length,
13506
+ failedRuns: failed.map((r) => ({
13507
+ runId: r.runId,
13508
+ ...r.project ? { project: r.project } : {},
13509
+ ...r.location ? { location: r.location } : {},
13510
+ error: reason(r)
13511
+ }))
13512
+ }
13513
+ });
13514
+ }
13515
+ async function triggerRun(project2, opts) {
13452
13516
  const client = getClient21();
13453
- const projectData = await client.getProject(project2);
13454
- const latest = await getLatestRunSummary(client, project2);
13455
- if (isMachineFormat(format)) {
13456
- let runs2 = [];
13457
- try {
13458
- runs2 = await client.listRuns(project2);
13459
- } catch {
13517
+ const body = {};
13518
+ if (opts?.provider) {
13519
+ const providerInputs = opts.provider.split(",").map((s) => s.trim()).filter(Boolean);
13520
+ const resolved = providerInputs.flatMap((p) => resolveProviderInput(p));
13521
+ body.providers = resolved.length > 0 ? resolved : providerInputs;
13522
+ }
13523
+ if (opts?.queries?.length) {
13524
+ body.queries = opts.queries;
13525
+ }
13526
+ if (opts?.groups?.length || opts?.targets?.length) {
13527
+ body.measurementScope = {
13528
+ ...opts.groups?.length ? { groups: opts.groups } : {},
13529
+ ...opts.targets?.length ? { targets: opts.targets } : {}
13530
+ };
13531
+ }
13532
+ if (opts?.location) {
13533
+ body.location = opts.location;
13534
+ }
13535
+ if (opts?.allLocations) {
13536
+ body.allLocations = true;
13537
+ }
13538
+ if (opts?.noLocation) {
13539
+ body.noLocation = true;
13540
+ }
13541
+ if (opts?.probe) {
13542
+ body.trigger = "probe";
13543
+ }
13544
+ if (opts?.dispatchMode) {
13545
+ body.dispatchMode = opts.dispatchMode;
13546
+ }
13547
+ if (opts?.force) {
13548
+ body.force = true;
13549
+ }
13550
+ const response = await client.triggerRun(project2, body);
13551
+ if (Array.isArray(response)) {
13552
+ const locationRuns = response;
13553
+ if (isMachineFormat(opts?.format)) {
13554
+ if (opts?.wait) {
13555
+ const finals = await Promise.all(
13556
+ locationRuns.map(async (r) => !r.id || r.status === "conflict" ? null : pollRun2(client, r.id, false))
13557
+ );
13558
+ console.log(JSON.stringify(locationRuns.map((r, i) => finals[i] ? { ...r, ...finals[i] } : r), null, 2));
13559
+ throwIfWaitedRunFailed(locationRuns.flatMap((r, i) => {
13560
+ const final = finals[i];
13561
+ return final ? [{ runId: final.id, status: final.status, project: project2, location: r.location ?? null, error: final.error }] : [];
13562
+ }));
13563
+ } else {
13564
+ console.log(JSON.stringify(locationRuns, null, 2));
13565
+ }
13566
+ return;
13567
+ }
13568
+ console.log(`Triggered ${locationRuns.length} location sweep(s) \u2014 ${locationRuns.length}\xD7 API calls:
13569
+ `);
13570
+ console.log(" LOCATION RUN ID STATUS");
13571
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
13572
+ for (const r of locationRuns) {
13573
+ const loc = (r.location ?? "(unknown)").padEnd(15);
13574
+ const id = (r.id ?? "(conflict)").padEnd(36);
13575
+ console.log(` ${loc} ${id} ${r.status}`);
13576
+ }
13577
+ printSkippedProviders(project2, locationRuns.find((r) => r.skippedProviders)?.skippedProviders);
13578
+ if (opts?.wait) {
13579
+ const pending = locationRuns.filter((r) => r.id && r.status !== "conflict" && !TERMINAL_STATUSES.has(r.status));
13580
+ const errors = /* @__PURE__ */ new Map();
13581
+ if (pending.length > 0) {
13582
+ process.stderr.write(`Waiting for ${pending.length} run(s)`);
13583
+ const batchWaits = /* @__PURE__ */ new Map();
13584
+ await Promise.all(
13585
+ pending.map(async (r) => {
13586
+ const final = await pollRun2(client, r.id);
13587
+ r.status = final.status;
13588
+ errors.set(r.id, final.error);
13589
+ const waiting = batchWaitLine(final);
13590
+ if (waiting) batchWaits.set(r.id, waiting);
13591
+ })
13592
+ );
13593
+ process.stderr.write("\n");
13594
+ console.log("\nFinal statuses:");
13595
+ for (const r of locationRuns) {
13596
+ const loc = (r.location ?? "(unknown)").padEnd(15);
13597
+ console.log(` ${loc} ${r.status}`);
13598
+ }
13599
+ if (batchWaits.size > 0) console.log("");
13600
+ for (const r of locationRuns) {
13601
+ const waiting = batchWaits.get(r.id);
13602
+ if (waiting) console.log(` ${r.location ?? "(unknown)"}: ${waiting}`);
13603
+ }
13604
+ }
13605
+ throwIfWaitedRunFailed(locationRuns.filter((r) => r.id && r.status !== "conflict").map((r) => ({ runId: r.id, status: r.status, project: project2, location: r.location ?? null, error: errors.get(r.id) })));
13460
13606
  }
13461
- console.log(JSON.stringify({
13462
- project: projectData,
13463
- runs: runs2,
13464
- latestRun: latest.run,
13465
- totalRuns: latest.totalRuns
13466
- }, null, 2));
13467
13607
  return;
13468
13608
  }
13469
- console.log(`Status: ${projectData.displayName ?? projectData.name} (${projectData.name})
13470
- `);
13471
- console.log(` Domain: ${projectData.canonicalDomain}`);
13472
- console.log(` Country: ${projectData.country}`);
13473
- console.log(` Language: ${projectData.language}`);
13474
- if (latest.run) {
13475
- console.log(`
13476
- Latest run:`);
13477
- console.log(` ID: ${latest.run.id}`);
13478
- console.log(` Status: ${latest.run.status}`);
13479
- console.log(` Created: ${latest.run.createdAt}`);
13480
- if (latest.run.finishedAt) {
13481
- console.log(` Finished: ${latest.run.finishedAt}`);
13609
+ const run = response;
13610
+ if (opts?.wait && run.id && !TERMINAL_STATUSES.has(run.status)) {
13611
+ const showProgress = !isMachineFormat(opts?.format);
13612
+ if (showProgress) process.stderr.write(`Run ${run.id} started`);
13613
+ const result = await pollRun2(client, run.id, showProgress);
13614
+ if (isMachineFormat(opts?.format)) {
13615
+ console.log(JSON.stringify(result, null, 2));
13616
+ } else {
13617
+ process.stderr.write("\n");
13618
+ printRunDetail(result);
13619
+ const waiting = batchWaitLine(result);
13620
+ if (waiting) console.log(`
13621
+ ${waiting}`);
13482
13622
  }
13483
- console.log(`
13484
- Total runs: ${latest.totalRuns}`);
13485
- } else {
13486
- const projectArg = /^[\w.-]+$/.test(project2) ? project2 : `'${project2.replaceAll("'", `'\\''`)}'`;
13487
- console.log(`
13488
- No runs yet. Capture Page Health with "canonry technical-aeo run ${projectArg}", or run an AI Visibility sweep after adding a provider.`);
13623
+ throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project: project2, error: result.error }]);
13624
+ return;
13489
13625
  }
13490
- }
13491
- async function getLatestRunSummary(client, project2) {
13492
- try {
13493
- return await client.getLatestRun(project2);
13494
- } catch (err) {
13495
- if (!isEndpointMissing(err)) throw err;
13496
- const runs2 = await client.listRuns(project2);
13497
- if (runs2.length === 0) {
13498
- return { totalRuns: 0, run: null };
13626
+ if (opts?.wait && (TERMINAL_STATUSES.has(run.status) || !run.id)) {
13627
+ const result = run.id ? await client.getRun(run.id) : run;
13628
+ if (isMachineFormat(opts?.format)) {
13629
+ console.log(JSON.stringify(result, null, 2));
13630
+ } else {
13631
+ printRunDetail(result);
13499
13632
  }
13500
- const latestRun = runs2.reduce(
13501
- (current, candidate) => candidate.createdAt > current.createdAt ? candidate : current
13502
- );
13503
- return {
13504
- totalRuns: runs2.length,
13505
- run: latestRun
13506
- };
13633
+ throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project: project2, error: result.error }]);
13634
+ return;
13507
13635
  }
13508
- }
13509
-
13510
- // src/cli-commands/operator.ts
13511
- var RESULTS_EXPORT_USAGE = "canonry results export <project> [--format json|csv] [--since <ISO>] [--until <ISO>] [--include-probes] [--output <path>|-]";
13512
- function parseResultsExportFormat(value) {
13513
- if (value === void 0 || value === "json") return "json";
13514
- if (value === "csv") return "csv";
13515
- throw usageError(`Error: --format must be "json" or "csv"
13516
-
13517
- Usage: ${RESULTS_EXPORT_USAGE}`);
13518
- }
13519
- var RESULTS_CLEAR_USAGE = "canonry results clear <project> [--run <id> ...] [--research-run <id> ...] [--confirm] [--format json|jsonl]";
13520
- function parseHistoryPageOption(input, key, usage) {
13521
- const value = getString(input.values, key);
13522
- if (value === void 0) return void 0;
13523
- if (!/^\d+$/.test(value)) {
13524
- throw usageError(`Error: --${key} must be a ${key === "limit" ? "whole number from 1 to 500" : "nonnegative integer"}
13525
- Usage: ${usage}`);
13636
+ if (isMachineFormat(opts?.format)) {
13637
+ console.log(JSON.stringify(run, null, 2));
13638
+ return;
13526
13639
  }
13527
- const parsed = Number(value);
13528
- const valid = Number.isSafeInteger(parsed) && (key === "limit" ? parsed >= 1 && parsed <= 500 : parsed >= 0);
13529
- if (!valid) {
13530
- throw usageError(`Error: --${key} must be a ${key === "limit" ? "whole number from 1 to 500" : "nonnegative integer"}
13531
- Usage: ${usage}`);
13640
+ console.log(`Run created: ${run.id}`);
13641
+ console.log(` Kind: ${run.kind}`);
13642
+ console.log(` Status: ${run.status}`);
13643
+ if (opts?.provider) {
13644
+ console.log(` Provider: ${opts.provider}`);
13532
13645
  }
13533
- return parsed;
13646
+ printSkippedProviders(project2, run.skippedProviders);
13534
13647
  }
13535
- var OPERATOR_CLI_COMMANDS = [
13536
- {
13537
- path: ["results", "clear"],
13538
- usage: RESULTS_CLEAR_USAGE,
13539
- options: { run: multiStringOption(), "research-run": multiStringOption(), confirm: { type: "boolean", default: false } },
13540
- run: async (input) => {
13541
- await clearResults(requireProject(input, "results.clear", RESULTS_CLEAR_USAGE), {
13542
- runIds: getStringArray(input.values, "run") ?? [],
13543
- researchRunIds: getStringArray(input.values, "research-run") ?? [],
13544
- confirm: getBoolean(input.values, "confirm"),
13545
- format: input.format
13546
- });
13648
+ async function triggerRunAll(opts) {
13649
+ const client = getClient21();
13650
+ const projects2 = await client.listProjects();
13651
+ if (projects2.length === 0) {
13652
+ if (isMachineFormat(opts?.format)) {
13653
+ console.log("[]");
13654
+ } else {
13655
+ console.log("No projects found.");
13547
13656
  }
13548
- },
13549
- {
13550
- path: ["results", "export"],
13551
- usage: RESULTS_EXPORT_USAGE,
13552
- options: {
13553
- since: stringOption(),
13554
- until: stringOption(),
13555
- "include-probes": { type: "boolean", default: false },
13556
- output: { type: "string", short: "o" }
13557
- },
13558
- run: async (input) => {
13559
- const project2 = requireProject(input, "results export", RESULTS_EXPORT_USAGE);
13560
- await exportResults(project2, {
13561
- format: parseResultsExportFormat(getString(input.values, "format")),
13562
- since: getString(input.values, "since"),
13563
- until: getString(input.values, "until"),
13564
- includeProbes: getBoolean(input.values, "include-probes"),
13565
- output: getString(input.values, "output")
13566
- });
13657
+ return;
13658
+ }
13659
+ const baseBody = {};
13660
+ if (opts?.provider) {
13661
+ const providerInputs = opts.provider.split(",").map((s) => s.trim()).filter(Boolean);
13662
+ const resolved = providerInputs.flatMap((p) => resolveProviderInput(p));
13663
+ baseBody.providers = resolved.length > 0 ? resolved : providerInputs;
13664
+ }
13665
+ if (opts?.allLocations) {
13666
+ baseBody.allLocations = true;
13667
+ }
13668
+ if (opts?.noLocation) {
13669
+ baseBody.noLocation = true;
13670
+ }
13671
+ if (opts?.dispatchMode) {
13672
+ baseBody.dispatchMode = opts.dispatchMode;
13673
+ }
13674
+ if (opts?.force) {
13675
+ baseBody.force = true;
13676
+ }
13677
+ const results = [];
13678
+ for (const p of projects2) {
13679
+ const body = { ...baseBody };
13680
+ if (body.allLocations && p.locations.length === 0) {
13681
+ delete body.allLocations;
13567
13682
  }
13568
- },
13569
- {
13570
- path: ["status"],
13571
- usage: "canonry status <project> [--format json]",
13572
- run: async (input) => {
13573
- const project2 = requireProject(input, "status", "canonry status <project> [--format json]");
13574
- await showStatus(project2, input.format);
13683
+ try {
13684
+ const response = await client.triggerRun(p.name, body);
13685
+ const dispatched = Array.isArray(response) ? response : [response];
13686
+ for (const r of dispatched) {
13687
+ results.push({
13688
+ project: p.name,
13689
+ runId: r.id,
13690
+ status: r.status,
13691
+ location: r.location ?? null
13692
+ });
13693
+ }
13694
+ } catch (err) {
13695
+ const msg = describeError(err);
13696
+ results.push({ project: p.name, runId: "", status: "error", location: null, error: msg });
13575
13697
  }
13576
- },
13577
- {
13578
- path: ["evidence"],
13579
- usage: "canonry evidence <project> [--format json]",
13580
- run: async (input) => {
13581
- const project2 = requireProject(input, "evidence", "canonry evidence <project> [--format json]");
13582
- await showEvidence(project2, input.format);
13698
+ }
13699
+ const errors = /* @__PURE__ */ new Map();
13700
+ const batchWaits = /* @__PURE__ */ new Map();
13701
+ if (opts?.wait) {
13702
+ const pending = results.filter((r) => r.runId && !TERMINAL_STATUSES.has(r.status));
13703
+ if (pending.length > 0) {
13704
+ const showProgress = !isMachineFormat(opts?.format);
13705
+ if (showProgress) process.stderr.write(`Waiting for ${pending.length} run(s)`);
13706
+ await Promise.all(pending.map(async (r) => {
13707
+ const final = await pollRun2(client, r.runId, showProgress);
13708
+ r.status = final.status;
13709
+ errors.set(r.runId, final.error);
13710
+ const waiting = batchWaitLine(final);
13711
+ if (waiting) batchWaits.set(r.runId, waiting);
13712
+ }));
13713
+ if (showProgress) process.stderr.write("\n");
13583
13714
  }
13584
- },
13585
- {
13586
- path: ["history"],
13587
- usage: "canonry history <project> [--limit <1..500>] [--offset <n>] [--since <ISO>] [--action <action>] [--actor <actor>] [--entity-type <type>] [--format json|jsonl]\n canonry history --all [same filters]",
13588
- options: {
13589
- all: { type: "boolean", default: false },
13590
- limit: stringOption(),
13591
- offset: stringOption(),
13592
- since: stringOption(),
13593
- action: stringOption(),
13594
- actor: stringOption(),
13595
- "entity-type": stringOption()
13596
- },
13597
- run: async (input) => {
13598
- const usage = "canonry history <project> [--limit <1..500>] [--offset <n>] [--since <ISO>] [--action <action>] [--actor <actor>] [--entity-type <type>] [--format json|jsonl]\n canonry history --all [same filters]";
13599
- const project2 = getBoolean(input.values, "all") ? void 0 : requireProject(input, "history", usage);
13600
- await showHistory(project2, input.format, {
13601
- limit: parseHistoryPageOption(input, "limit", usage),
13602
- offset: parseHistoryPageOption(input, "offset", usage),
13603
- since: getString(input.values, "since"),
13604
- action: getString(input.values, "action"),
13605
- actor: getString(input.values, "actor"),
13606
- entityType: getString(input.values, "entity-type")
13607
- });
13715
+ }
13716
+ const waited = results.filter((r) => r.runId).map((r) => ({ runId: r.runId, status: r.status, project: r.project, location: r.location, error: errors.get(r.runId) }));
13717
+ if (isMachineFormat(opts?.format)) {
13718
+ console.log(JSON.stringify(results, null, 2));
13719
+ if (opts?.wait) throwIfWaitedRunFailed(waited);
13720
+ return;
13721
+ }
13722
+ const showLocationColumn = results.some((r) => r.location !== null);
13723
+ const projectCount = new Set(results.map((r) => r.project)).size;
13724
+ console.log(`Triggered ${results.length} run(s) across ${projectCount} project(s):
13725
+ `);
13726
+ if (showLocationColumn) {
13727
+ console.log(" PROJECT LOCATION RUN ID STATUS");
13728
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
13729
+ for (const r of results) {
13730
+ const proj = r.project.padEnd(31);
13731
+ const loc = (r.location ?? "\u2014").padEnd(15);
13732
+ const id = (r.runId || "(failed)").padEnd(36);
13733
+ console.log(` ${proj} ${loc} ${id} ${r.status}`);
13608
13734
  }
13609
- },
13610
- {
13611
- path: ["export"],
13612
- usage: "canonry export <project> [--include-results] [--format json]",
13613
- options: {
13614
- "include-results": { type: "boolean", default: false }
13615
- },
13616
- run: async (input) => {
13617
- const project2 = requireProject(input, "export", "canonry export <project> [--include-results] [--format json]");
13618
- await exportProject(project2, {
13619
- includeResults: getBoolean(input.values, "include-results"),
13620
- format: input.format
13621
- });
13735
+ } else {
13736
+ console.log(" PROJECT RUN ID STATUS");
13737
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
13738
+ for (const r of results) {
13739
+ const proj = r.project.padEnd(31);
13740
+ const id = (r.runId || "(failed)").padEnd(36);
13741
+ console.log(` ${proj} ${id} ${r.status}`);
13622
13742
  }
13623
- },
13624
- {
13625
- path: ["analytics"],
13626
- usage: "canonry analytics <project> [--feature metrics|gaps|sources] [--window 7d|30d|90d|all] [--format json]",
13627
- options: {
13628
- feature: stringOption(),
13629
- window: stringOption()
13630
- },
13631
- run: async (input) => {
13632
- const project2 = requireProject(input, "analytics", "canonry analytics <project> [--feature metrics|gaps|sources] [--window 7d|30d|90d|all] [--format json]");
13633
- await showAnalytics(project2, {
13634
- feature: getString(input.values, "feature"),
13635
- window: getString(input.values, "window"),
13636
- format: input.format
13743
+ }
13744
+ if (batchWaits.size > 0) console.log("");
13745
+ for (const r of results) {
13746
+ const waiting = batchWaits.get(r.runId);
13747
+ if (waiting) console.log(` ${r.location ? `${r.project} (${r.location})` : r.project}: ${waiting}`);
13748
+ }
13749
+ if (opts?.wait) throwIfWaitedRunFailed(waited);
13750
+ }
13751
+ async function cancelRun(project2, runId, format) {
13752
+ const client = getClient21();
13753
+ let targetId = runId;
13754
+ if (!targetId) {
13755
+ const runs2 = await client.listRuns(project2);
13756
+ const activeRuns = runs2.filter((r) => r.status === "queued" || r.status === "running");
13757
+ if (activeRuns.length > 1) {
13758
+ const candidates = activeRuns.map(({ id, kind, status }) => ({ id, kind, status }));
13759
+ throw new CliError({
13760
+ code: "MULTIPLE_ACTIVE_RUNS",
13761
+ message: `Multiple active runs found for project "${project2}". Specify a run ID.`,
13762
+ displayMessage: `Error: Multiple active runs found for project "${project2}". Specify a run ID.
13763
+ ` + candidates.map((r) => ` ${r.id} ${r.kind} ${r.status}`).join("\n") + `
13764
+ To cancel by ID: canonry run cancel ${project2} <run-id>`,
13765
+ details: {
13766
+ project: project2,
13767
+ activeRuns: candidates,
13768
+ suggestedCommands: [`canonry run cancel ${project2} <run-id>`]
13769
+ }
13637
13770
  });
13638
13771
  }
13639
- },
13640
- {
13641
- path: ["sources"],
13642
- usage: SOURCES_USAGE,
13643
- options: {
13644
- rank: { type: "boolean", default: false },
13645
- "by-provider": { type: "boolean", default: false },
13646
- limit: stringOption(),
13647
- window: stringOption(),
13648
- "query-class": stringOption(),
13649
- "run-id": stringOption(),
13650
- "include-by-query": stringOption()
13651
- },
13652
- run: async (input) => {
13653
- const project2 = requireProject(input, "sources", SOURCES_USAGE);
13654
- await showSources(project2, {
13655
- rank: getBoolean(input.values, "rank"),
13656
- byProvider: getBoolean(input.values, "by-provider"),
13657
- limit: parseIntegerOption(input, "limit", {
13658
- message: "--limit must be an integer",
13659
- usage: SOURCES_USAGE,
13660
- command: "sources"
13661
- }),
13662
- window: getString(input.values, "window"),
13663
- runId: getString(input.values, "run-id"),
13664
- queryClass: parseSourcesQueryClass(getString(input.values, "query-class")),
13665
- includeByQuery: parseSourcesIncludeByQuery(getString(input.values, "include-by-query")),
13666
- format: input.format
13772
+ const active = activeRuns[0];
13773
+ if (!active) {
13774
+ throw new CliError({
13775
+ code: "NO_ACTIVE_RUN",
13776
+ message: `No active run found for project "${project2}"`,
13777
+ displayMessage: `Error: canonry run cancel "${project2}" \u2014 no active run found (status must be queued or running).
13778
+ Check run status : canonry status ${project2}
13779
+ To cancel by ID : canonry run cancel ${project2} <run-id>`,
13780
+ details: {
13781
+ project: project2,
13782
+ allowedStatuses: ["queued", "running"],
13783
+ suggestedCommands: [
13784
+ `canonry status ${project2}`,
13785
+ `canonry run cancel ${project2} <run-id>`
13786
+ ]
13787
+ }
13667
13788
  });
13668
13789
  }
13669
- },
13670
- {
13671
- path: ["apply"],
13672
- usage: "canonry apply <file...> [--format json]",
13673
- run: async (input) => {
13674
- if (input.positionals.length === 0) {
13675
- throw usageError("Error: at least one file path is required\nUsage: canonry apply <file...> [--format json]", {
13676
- message: "at least one file path is required",
13677
- details: {
13678
- command: "apply",
13679
- usage: "canonry apply <file...> [--format json]"
13680
- }
13681
- });
13682
- }
13683
- await applyConfigs(input.positionals, input.format);
13684
- }
13790
+ targetId = active.id;
13685
13791
  }
13686
- ];
13687
-
13688
- // src/commands/project.ts
13689
- function getClient22() {
13690
- return createApiClient();
13792
+ const result = await client.cancelRun(targetId);
13793
+ if (isMachineFormat(format)) {
13794
+ console.log(JSON.stringify(result, null, 2));
13795
+ return;
13796
+ }
13797
+ console.log(`Run ${result.id} cancelled.`);
13691
13798
  }
13692
- async function createProject(name, opts) {
13693
- const client = getClient22();
13694
- const result = await client.putProject(name, {
13695
- displayName: opts.displayName,
13696
- canonicalDomain: opts.domain,
13697
- ownedDomains: opts.ownedDomains ?? [],
13698
- aliases: normalizeProjectAliases(opts.displayName, opts.aliases ?? []),
13699
- ...opts.qualifiedAliases?.length ? { qualifiedAliases: opts.qualifiedAliases } : {},
13700
- country: opts.country,
13701
- language: opts.language,
13702
- providers: opts.providers ?? [],
13703
- providerModels: opts.providerModels ?? {},
13704
- ...opts.providerDispatchModes && Object.keys(opts.providerDispatchModes).length > 0 ? { providerDispatchModes: opts.providerDispatchModes } : {},
13705
- // Sent only when given: this PUT also re-runs over an existing project,
13706
- // and an omitted budget keeps the stored one.
13707
- ...opts.siteAuditMaxPages !== void 0 ? { siteAuditMaxPages: opts.siteAuditMaxPages } : {}
13708
- });
13709
- warnDroppedDispatchModes(opts.providerDispatchModes ?? {}, result);
13710
- if (isMachineFormat(opts.format)) {
13711
- console.log(JSON.stringify(result, null, 2));
13799
+ async function showRun(id, format) {
13800
+ const client = getClient21();
13801
+ const run = await client.getRun(id);
13802
+ if (isMachineFormat(format)) {
13803
+ console.log(JSON.stringify(run, null, 2));
13712
13804
  return;
13713
13805
  }
13714
- console.log(`Project created: ${result.name} (${result.id})`);
13715
- }
13716
- function warnDroppedDispatchModes(sent, result) {
13717
- const kept = result.providerDispatchModes ?? {};
13718
- const dropped = Object.keys(sent).filter((provider) => !(provider in kept));
13719
- if (dropped.length > 0) {
13720
- console.error(`Warning: Dropped dispatch mode for engine(s) the project does not measure: ${dropped.join(", ")}`);
13721
- }
13806
+ printRunDetail(run);
13722
13807
  }
13723
- async function listProjects(format) {
13724
- const client = getClient22();
13725
- const projects2 = await client.listProjects();
13726
- if (format === "json") {
13727
- console.log(JSON.stringify(projects2, null, 2));
13808
+ async function listRuns(project2, opts) {
13809
+ const client = getClient21();
13810
+ const runs2 = await client.listRuns(project2, opts?.limit, opts?.kind, opts?.status, opts?.excludeKind, opts?.excludeKinds);
13811
+ if (opts?.format === "json") {
13812
+ console.log(JSON.stringify(runs2, null, 2));
13728
13813
  return;
13729
- }
13730
- if (format === "jsonl") {
13731
- emitJsonl(projects2);
13814
+ } else if (opts?.format === "jsonl") {
13815
+ emitJsonl(runs2.map((run) => ({ project: project2, ...run })));
13732
13816
  return;
13733
13817
  }
13734
- if (projects2.length === 0) {
13735
- console.log("No projects found.");
13818
+ if (runs2.length === 0) {
13819
+ console.log(`No runs found for "${project2}".`);
13736
13820
  return;
13737
13821
  }
13738
- console.log("Projects:\n");
13739
- const nameWidth = Math.max(4, ...projects2.map((p) => p.name.length));
13740
- const domainLabel = (p) => {
13741
- const extra = Math.max(0, effectiveDomains(p).length - 1);
13742
- return extra > 0 ? `${p.canonicalDomain} (+${extra})` : p.canonicalDomain;
13743
- };
13744
- const domainWidth = Math.max(6, ...projects2.map((p) => domainLabel(p).length));
13745
- console.log(
13746
- ` ${"NAME".padEnd(nameWidth)} ${"DOMAIN".padEnd(domainWidth)} COUNTRY LANGUAGE`
13747
- );
13748
- console.log(` ${"\u2500".repeat(nameWidth)} ${"\u2500".repeat(domainWidth)} \u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
13749
- for (const p of projects2) {
13822
+ console.log(`Runs for "${project2}" (${runs2.length}):
13823
+ `);
13824
+ console.log(" ID STATUS KIND TRIGGER CREATED");
13825
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
13826
+ for (const run of runs2) {
13750
13827
  console.log(
13751
- ` ${p.name.padEnd(nameWidth)} ${domainLabel(p).padEnd(domainWidth)} ${p.country.padEnd(7)} ${p.language}`
13828
+ ` ${run.id} ${run.status.padEnd(10)} ${run.kind.padEnd(18)} ${run.trigger.padEnd(9)} ${run.createdAt}`
13752
13829
  );
13753
13830
  }
13754
13831
  }
13755
- function storedQualifiedAliases(project2) {
13756
- return project2.qualifiedAliases ?? [];
13757
- }
13758
- async function showProject(name, format) {
13759
- const client = getClient22();
13760
- const project2 = await client.getProject(name);
13761
- if (isMachineFormat(format)) {
13762
- console.log(JSON.stringify(project2, null, 2));
13763
- return;
13764
- }
13765
- console.log(`Project: ${project2.displayName ?? project2.name}
13766
- `);
13767
- console.log(` Name: ${project2.name}`);
13768
- console.log(` ID: ${project2.id}`);
13769
- console.log(` Domain: ${project2.canonicalDomain}`);
13770
- const secondaryDomains = effectiveDomains(project2).slice(1);
13771
- if (secondaryDomains.length > 0) {
13772
- console.log(` Owned domains: ${secondaryDomains.join(", ")}`);
13832
+ var POLL_TIMEOUT_MS3 = 10 * 60 * 1e3;
13833
+ async function pollRun2(client, runId, showProgress = true) {
13834
+ const deadline = Date.now() + POLL_TIMEOUT_MS3;
13835
+ for (; ; ) {
13836
+ await new Promise((r) => setTimeout(r, 2e3));
13837
+ if (Date.now() > deadline) {
13838
+ throw new Error(`Timed out waiting for run ${runId} after ${POLL_TIMEOUT_MS3 / 1e3}s`);
13839
+ }
13840
+ const run = await client.getRun(runId);
13841
+ if (showProgress) process.stderr.write(".");
13842
+ if (TERMINAL_STATUSES.has(run.status) || outstandingBatches(run).length > 0) {
13843
+ return run;
13844
+ }
13773
13845
  }
13774
- if (project2.aliases && project2.aliases.length > 0) {
13775
- console.log(` Aliases: ${project2.aliases.join(", ")}`);
13846
+ }
13847
+ function outstandingBatches(run) {
13848
+ if (TERMINAL_STATUSES.has(run.status)) return [];
13849
+ return (run.providerBatches ?? []).filter((batch) => OUTSTANDING_PROVIDER_BATCH_STATUSES.includes(batch.status));
13850
+ }
13851
+ function batchWaitLine(run) {
13852
+ const outstanding = outstandingBatches(run);
13853
+ if (outstanding.length === 0) return null;
13854
+ const batches = outstanding.map((batch) => `${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`);
13855
+ return `Waiting on provider batch(es): ${batches.join("; ")}; check with canonry run show ${run.id}`;
13856
+ }
13857
+ function printRunDetail(run) {
13858
+ console.log(`Run: ${run.id}`);
13859
+ console.log(` Status: ${run.status}`);
13860
+ console.log(` Kind: ${run.kind}`);
13861
+ if (run.trigger) console.log(` Trigger: ${run.trigger}`);
13862
+ if (run.startedAt) console.log(` Started: ${run.startedAt}`);
13863
+ if (run.finishedAt) console.log(` Finished: ${run.finishedAt}`);
13864
+ if (run.createdAt) console.log(` Created: ${run.createdAt}`);
13865
+ const skipped = Object.entries(run.skippedProviders ?? {});
13866
+ for (const [provider, streak] of skipped) console.log(` Skipped: ${providerAccountStreakLine(provider, streak)}`);
13867
+ if (run.error) {
13868
+ if (run.error.message) console.log(` Error: ${run.error.message}`);
13869
+ if (run.error.providers) {
13870
+ for (const [provider, detail] of Object.entries(run.error.providers)) {
13871
+ if (detail.skipped && run.skippedProviders?.[provider]) continue;
13872
+ console.log(` Error (${provider}): ${detail.message}`);
13873
+ }
13874
+ }
13776
13875
  }
13777
- const qualifiedAliases = storedQualifiedAliases(project2);
13778
- if (qualifiedAliases.length > 0) {
13779
- console.log(` Qualified: ${qualifiedAliases.join(", ")} (sentiment)`);
13876
+ const batched = Object.keys(run.dispatchModes ?? {}).sort();
13877
+ if (batched.length > 0) console.log(` Dispatch: ${batched.map((provider) => `${provider}=batch`).join(", ")} (other providers sync)`);
13878
+ if (run.providerBatches && run.providerBatches.length > 0) {
13879
+ console.log("\n Provider batches:");
13880
+ for (const batch of run.providerBatches) console.log(` ${providerBatchLine(batch)}`);
13780
13881
  }
13781
- console.log(` Country: ${project2.country}`);
13782
- console.log(` Language: ${project2.language}`);
13783
- console.log(` Config source: ${project2.configSource}`);
13784
- console.log(` Config revision: ${project2.configRevision}`);
13785
- console.log(` Providers: ${(project2.providers ?? []).length > 0 ? project2.providers.join(", ") : "all configured"}`);
13786
- const providerModels = Object.entries(project2.providerModels ?? {});
13787
- console.log(` Model overrides: ${providerModels.length > 0 ? providerModels.map(([provider, model]) => `${provider}=${model}`).join(", ") : "(none; instance settings inherited)"}`);
13788
- const dispatchModes = Object.entries(project2.providerDispatchModes ?? {}).sort(([left], [right]) => left.localeCompare(right));
13789
- console.log(` Dispatch modes: ${dispatchModes.length > 0 ? `${dispatchModes.map(([provider, mode]) => `${provider}=${mode}`).join(", ")} (scheduled sweeps; others sync)` : "(none; every provider runs sync)"}`);
13790
- console.log(` Negative reviews: ${resolveNegativeReviewMaxStars(project2.negativeReviewMaxStars)} stars or fewer${project2.negativeReviewMaxStars == null ? " (default)" : ""}`);
13791
- if (project2.siteAuditMaxPages !== void 0) {
13792
- console.log(` Site Health page budget: ${formatSiteAuditPageBudget(project2.siteAuditMaxPages)}`);
13882
+ if (run.usage && run.usage.length > 0) printUsageTable(run.usage);
13883
+ if (run.snapshots && run.snapshots.length > 0) {
13884
+ console.log(`
13885
+ Snapshots: ${run.snapshots.length} (cell = [citation][mention]; C=cited c=not, M=mentioned m=not, \u2013=no data)`);
13886
+ for (const s of run.snapshots) {
13887
+ const citationGlyph = s.citationState === CitationStates.cited ? "C" : "c";
13888
+ const mentionGlyph = typeof s.answerMentioned === "boolean" ? s.answerMentioned ? "M" : "m" : "\u2013";
13889
+ const modelLabel = s.model ? ` (${s.model})` : "";
13890
+ console.log(` [${citationGlyph}${mentionGlyph}] ${s.provider}${modelLabel} ${s.query}`);
13891
+ }
13793
13892
  }
13794
- if (project2.competitorAutoAliases !== void 0) {
13795
- console.log(` Competitor auto aliases: ${formatCompetitorAutoAliasMode(project2.competitorAutoAliases)}`);
13893
+ }
13894
+ function providerBatchLine(batch) {
13895
+ const recorded = `${batch.recordedCount} of ${batch.requestCount} answers recorded`;
13896
+ const withError = (text) => batch.error ? `${text} (${batch.error})` : text;
13897
+ switch (batch.status) {
13898
+ case ProviderBatchStatuses.submitting:
13899
+ return `submitting provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests`;
13900
+ // The two outstanding statuses: the run stays `running` until they clear.
13901
+ case ProviderBatchStatuses.submitted:
13902
+ case ProviderBatchStatuses.ended:
13903
+ return `waiting on provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`;
13904
+ case ProviderBatchStatuses.ingested:
13905
+ return withError(`provider batch ${batch.provider}: ingested \u2014 ${recorded}`);
13906
+ case ProviderBatchStatuses.cancelled:
13907
+ return withError(`provider batch ${batch.provider}: cancelled \u2014 ${recorded}; the rest are missing and can be filled`);
13908
+ case ProviderBatchStatuses.failed:
13909
+ return withError(`provider batch ${batch.provider}: rejected by the provider \u2014 its ${batch.requestCount} answers ran sync instead`);
13910
+ case ProviderBatchStatuses.unknown:
13911
+ return withError(`provider batch ${batch.provider}: submit outcome unknown \u2014 never resubmitted; its ${batch.requestCount} answers stay missing`);
13796
13912
  }
13797
- console.log(` Tags: ${project2.tags.length > 0 ? project2.tags.join(", ") : "(none)"}`);
13798
- const labelEntries = Object.entries(project2.labels);
13799
- console.log(` Labels: ${labelEntries.length > 0 ? labelEntries.map(([k, v]) => `${k}=${v}`).join(", ") : "(none)"}`);
13800
- if (project2.createdAt) console.log(` Created: ${project2.createdAt}`);
13801
- if (project2.updatedAt) console.log(` Updated: ${project2.updatedAt}`);
13802
13913
  }
13803
- function formatCompetitorAutoAliasMode(mode) {
13804
- if (mode === "apply") return "apply (names detected after each sweep are stored and counted)";
13805
- if (mode === "off") return "off (no detection after sweeps)";
13806
- return "preview (detected after each sweep and logged; nothing stored)";
13914
+ function printUsageTable(rows) {
13915
+ const cost = (row) => row.estimatedCostMicros === null ? "unpriced" : `${formatMicros(row.estimatedCostMicros, "USD", { fractionDigits: 4, showTinyAsLessThan: true })}${row.unpricedAnswers > 0 ? ` (+${row.unpricedAnswers} unpriced)` : ""}`;
13916
+ const table = rows.map((row) => [
13917
+ row.provider,
13918
+ row.pricingTier,
13919
+ row.answers.toLocaleString("en-US"),
13920
+ row.inputTokens.toLocaleString("en-US"),
13921
+ row.cachedInputTokens.toLocaleString("en-US"),
13922
+ row.cacheWriteTokens.toLocaleString("en-US"),
13923
+ row.outputTokens.toLocaleString("en-US"),
13924
+ row.searchCount.toLocaleString("en-US"),
13925
+ cost(row)
13926
+ ]);
13927
+ const header = ["PROVIDER", "TIER", "ANSWERS", "INPUT", "CACHED", "CACHE WRITE", "OUTPUT", "SEARCHES", "EST. COST"];
13928
+ const widths = header.map((title, index) => Math.max(title.length, ...table.map((cells) => cells[index].length)));
13929
+ const line = (cells) => ` ${cells.map((cell2, index) => cell2.padEnd(widths[index])).join(" ").trimEnd()}`;
13930
+ console.log("\n Usage (answers with recorded usage; cost is an estimate):");
13931
+ console.log(line(header));
13932
+ for (const cells of table) console.log(line(cells));
13807
13933
  }
13808
- async function updateProjectSettings(name, opts) {
13809
- const client = getClient22();
13810
- const project2 = await client.getProject(name);
13811
- let ownedDomains = opts.ownedDomains ?? project2.ownedDomains ?? [];
13812
- if (opts.addOwnedDomain) {
13813
- const toAdd = opts.addOwnedDomain.filter((d) => !ownedDomains.includes(d));
13814
- ownedDomains = [...ownedDomains, ...toAdd];
13934
+ var FILL_POLL_INTERVAL_MS = 3e3;
13935
+ var FILL_POLL_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
13936
+ function missingSummary(completeness) {
13937
+ const parts = Object.entries(completeness.missingByProvider).sort(([left], [right]) => left.localeCompare(right)).map(([provider, count]) => `${provider} ${count}`);
13938
+ return parts.length ? parts.join(", ") : "none";
13939
+ }
13940
+ function printCompleteness(completeness) {
13941
+ console.log(`Run: ${completeness.runId}`);
13942
+ console.log(` Status: ${completeness.status}`);
13943
+ if (!completeness.planned) {
13944
+ console.log(" Not a measurement-plan run: there are no expected answers to count.");
13945
+ return;
13815
13946
  }
13816
- if (opts.removeOwnedDomain) {
13817
- const toRemove = new Set(opts.removeOwnedDomain);
13818
- ownedDomains = ownedDomains.filter((d) => !toRemove.has(d));
13947
+ if (!completeness.readable) {
13948
+ console.log(" Answers: unknown (the run's manifest cannot be read)");
13949
+ } else {
13950
+ console.log(` Answers: ${completeness.executed}/${completeness.expected}`);
13951
+ console.log(` Missing: ${completeness.missing} (${missingSummary(completeness)})`);
13819
13952
  }
13820
- const nextDisplayName = opts.displayName ?? project2.displayName ?? project2.name;
13821
- let aliases = opts.aliases ?? project2.aliases ?? [];
13822
- if (opts.addAlias) {
13823
- const existingKeys = new Set(aliases.map((a) => a.toLowerCase()));
13824
- const toAdd = opts.addAlias.filter((a) => !existingKeys.has(a.toLowerCase()));
13825
- aliases = [...aliases, ...toAdd];
13953
+ if (completeness.refusal) console.log(` Fillable: no. ${completeness.refusal.message}`);
13954
+ else console.log(` Fillable: ${completeness.fillable ? "yes" : "not right now (a sweep or another fill is running)"}`);
13955
+ const fill = completeness.latestFill;
13956
+ if (fill) {
13957
+ console.log(` Last fill: ${fill.id} ${fill.status}, ${fill.filled}/${fill.expected} recorded${fill.error ? `. ${fill.error}` : ""}`);
13826
13958
  }
13827
- if (opts.removeAlias) {
13828
- const toRemove = new Set(opts.removeAlias.map((a) => a.toLowerCase()));
13829
- aliases = aliases.filter((a) => !toRemove.has(a.toLowerCase()));
13959
+ }
13960
+ async function showRunCompleteness(runId, format) {
13961
+ const completeness = await getClient21().getRunCompleteness(runId);
13962
+ if (isMachineFormat(format)) {
13963
+ console.log(JSON.stringify(completeness, null, 2));
13964
+ return;
13830
13965
  }
13831
- const nextAliases = normalizeProjectAliases(nextDisplayName, aliases);
13832
- const touchesQualified = (opts.addQualifiedAlias?.length ?? 0) > 0 || (opts.removeQualifiedAlias?.length ?? 0) > 0;
13833
- let qualifiedAliases = resolveProjectQualifiedAliases(
13834
- { displayName: nextDisplayName, aliases: nextAliases },
13835
- storedQualifiedAliases(project2)
13836
- ).value;
13837
- if (opts.addQualifiedAlias) {
13838
- const existingKeys = new Set(qualifiedAliases.map((alias) => alias.toLowerCase()));
13839
- for (const alias of opts.addQualifiedAlias) {
13840
- if (existingKeys.has(alias.toLowerCase())) continue;
13841
- existingKeys.add(alias.toLowerCase());
13842
- qualifiedAliases = [...qualifiedAliases, alias];
13843
- }
13844
- }
13845
- if (opts.removeQualifiedAlias) {
13846
- const toRemove = new Set(opts.removeQualifiedAlias.map((alias) => alias.toLowerCase()));
13847
- qualifiedAliases = qualifiedAliases.filter((alias) => !toRemove.has(alias.toLowerCase()));
13848
- }
13849
- const providerModels = { ...project2.providerModels ?? {}, ...opts.providerModels ?? {} };
13850
- for (const provider of opts.clearProviderModels ?? []) delete providerModels[provider];
13851
- const nextProviders = opts.providers ?? project2.providers ?? [];
13852
- if (nextProviders.length > 0) {
13853
- const requested = Object.keys(opts.providerModels ?? {}).filter((p) => !nextProviders.includes(p));
13854
- if (requested.length > 0) {
13855
- throw usageError(
13856
- `Error: --provider-model set for ${requested.join(", ")}, which ${requested.length > 1 ? "are" : "is"} not a selected engine for "${name}"`,
13857
- {
13858
- message: "provider-model set for a provider the project does not run",
13859
- details: { command: "project.update", project: name, providers: nextProviders, unselected: requested }
13860
- }
13861
- );
13862
- }
13863
- }
13864
- const touchesDispatch = Object.keys(opts.dispatchModes ?? {}).length > 0 || (opts.clearDispatchModes ?? []).length > 0;
13865
- const providerDispatchModes = { ...project2.providerDispatchModes ?? {}, ...opts.dispatchModes ?? {} };
13866
- for (const provider of opts.clearDispatchModes ?? []) delete providerDispatchModes[provider];
13867
- const result = await client.putProject(name, {
13868
- displayName: nextDisplayName,
13869
- canonicalDomain: opts.domain ?? project2.canonicalDomain,
13870
- ownedDomains,
13871
- aliases: nextAliases,
13872
- ...touchesQualified ? { qualifiedAliases } : {},
13873
- country: opts.country ?? project2.country,
13874
- language: opts.language ?? project2.language,
13875
- tags: project2.tags,
13876
- labels: project2.labels,
13877
- providers: opts.providers ?? project2.providers,
13878
- providerModels,
13879
- ...touchesDispatch ? { providerDispatchModes } : {},
13880
- locations: project2.locations,
13881
- defaultLocation: project2.defaultLocation,
13882
- autoExtractBacklinks: project2.autoExtractBacklinks,
13883
- // Sent only when asked for: the server keeps the stored threshold when the
13884
- // field is absent, so a stale read cannot overwrite a newer setting.
13885
- ...opts.negativeReviewMaxStars !== void 0 ? { negativeReviewMaxStars: opts.negativeReviewMaxStars } : {},
13886
- // Same for the page budget: never echoed from the read above, so an edit
13887
- // that does not touch it cannot overwrite a newer one or send a stray null.
13888
- ...opts.siteAuditMaxPages !== void 0 ? { siteAuditMaxPages: opts.siteAuditMaxPages } : {},
13889
- // Same for the detection mode.
13890
- ...opts.competitorAutoAliases !== void 0 ? { competitorAutoAliases: opts.competitorAutoAliases } : {}
13966
+ printCompleteness(completeness);
13967
+ }
13968
+ async function fillRun(runId, opts = {}) {
13969
+ const client = getClient21();
13970
+ const response = await client.fillRun(runId, {
13971
+ ...opts.providers?.length ? { providers: opts.providers } : {},
13972
+ ...opts.dryRun ? { dryRun: true } : {}
13891
13973
  });
13892
- warnDroppedDispatchModes(touchesDispatch ? providerDispatchModes : project2.providerDispatchModes ?? {}, result);
13893
- if (isMachineFormat(opts.format)) {
13894
- console.log(JSON.stringify(result, null, 2));
13974
+ if (!opts.wait || response.outcome !== "queued" || !response.fill) {
13975
+ if (isMachineFormat(opts.format)) {
13976
+ console.log(JSON.stringify(response, null, 2));
13977
+ return;
13978
+ }
13979
+ if (response.outcome === "already-complete") console.log(`Run ${runId} is already complete: nothing to fill.`);
13980
+ else if (response.outcome === "dry-run") console.log("Dry run: nothing was queued.");
13981
+ else if (response.fill) console.log(`Fill ${response.fill.id} queued: ${response.fill.expected} missing answer(s) (${missingSummary(response.completeness)}).`);
13982
+ printCompleteness(response.completeness);
13895
13983
  return;
13896
13984
  }
13897
- console.log(`Project updated: ${result.name}`);
13898
- const dropped = Object.keys(providerModels).filter((provider) => !(provider in result.providerModels));
13899
- if (dropped.length > 0) {
13900
- console.log(` Dropped model override for deselected engine(s): ${dropped.join(", ")}`);
13985
+ const fillId = response.fill.id;
13986
+ const deadline = Date.now() + FILL_POLL_TIMEOUT_MS;
13987
+ let completeness = response.completeness;
13988
+ if (!isMachineFormat(opts.format)) {
13989
+ process.stderr.write(`Filling ${response.fill.expected} missing answer(s) in run ${runId}`);
13901
13990
  }
13902
- }
13903
- async function deleteProject(name, opts) {
13904
- const client = getClient22();
13905
- const isJson = isMachineFormat(opts?.format);
13906
- if (opts?.dryRun) {
13907
- const preview = await client.previewProjectDelete(name);
13908
- if (isJson) {
13909
- console.log(JSON.stringify({ dryRun: true, ...preview }, null, 2));
13910
- return;
13991
+ for (; ; ) {
13992
+ await new Promise((resolve) => setTimeout(resolve, FILL_POLL_INTERVAL_MS));
13993
+ if (Date.now() > deadline) {
13994
+ throw new CliError({
13995
+ code: "RUN_FILL_INCOMPLETE",
13996
+ message: `Timed out waiting for fill ${fillId}; it may still be running. Check with: canonry run completeness ${runId}`,
13997
+ exitCode: EXIT_SYSTEM_ERROR,
13998
+ details: { runId, fillId }
13999
+ });
13911
14000
  }
13912
- const { cascadeRows: cr, detachedRows: dr } = preview;
13913
- console.log(`Project delete preview for "${name}":`);
13914
- console.log(` Cascade-deletes:`);
13915
- console.log(` queries: ${cr.queries}`);
13916
- console.log(` competitors: ${cr.competitors}`);
13917
- console.log(` runs: ${cr.runs}`);
13918
- console.log(` snapshots: ${cr.snapshots}`);
13919
- console.log(` insights: ${cr.insights}`);
13920
- console.log(` Detached (project_id set to NULL):`);
13921
- console.log(` audit_log: ${dr.auditLog}`);
13922
- console.log(``);
13923
- console.log(`No DB writes performed. Re-run without --dry-run to delete.`);
13924
- return;
14001
+ completeness = await client.getRunCompleteness(runId);
14002
+ if (!isMachineFormat(opts.format)) process.stderr.write(".");
14003
+ const latest = completeness.latestFill;
14004
+ const working = latest !== null && (latest.status === "queued" || latest.status === "running");
14005
+ if (completeness.status === "completed" || !working) break;
13925
14006
  }
13926
- await client.deleteProject(name);
13927
- if (isJson) {
13928
- console.log(JSON.stringify({ name, deleted: true }, null, 2));
13929
- return;
14007
+ if (!isMachineFormat(opts.format)) process.stderr.write("\n");
14008
+ if (isMachineFormat(opts.format)) console.log(JSON.stringify(completeness, null, 2));
14009
+ else printCompleteness(completeness);
14010
+ if (completeness.status !== "completed") {
14011
+ throw new CliError({
14012
+ code: "RUN_FILL_INCOMPLETE",
14013
+ message: `Run ${runId} is still ${completeness.status}: ${completeness.missing} answer(s) missing (${missingSummary(completeness)}).`,
14014
+ exitCode: EXIT_SYSTEM_ERROR,
14015
+ details: { runId, fillId, missing: completeness.missing, missingByProvider: completeness.missingByProvider }
14016
+ });
13930
14017
  }
13931
- console.log(`Project deleted: ${name}`);
13932
14018
  }
13933
- async function addLocation(project2, opts) {
13934
- const client = getClient22();
13935
- const location = await client.addLocation(project2, {
13936
- label: opts.label,
13937
- city: opts.city,
13938
- region: opts.region,
13939
- country: opts.country,
13940
- timezone: opts.timezone
13941
- });
13942
- if (isMachineFormat(opts.format)) {
13943
- console.log(JSON.stringify(location, null, 2));
13944
- return;
13945
- }
13946
- console.log(`Location added: ${opts.label} (${opts.city}, ${opts.region}, ${opts.country})`);
14019
+
14020
+ // src/commands/status.ts
14021
+ function getClient22() {
14022
+ return createApiClient();
13947
14023
  }
13948
- async function listLocations(project2, format) {
14024
+ async function showStatus(project2, format) {
13949
14025
  const client = getClient22();
13950
- const result = await client.listLocations(project2);
13951
- if (format === "json") {
13952
- console.log(JSON.stringify(result, null, 2));
13953
- return;
13954
- }
13955
- if (format === "jsonl") {
13956
- emitJsonl(result.locations.map((loc) => ({
13957
- project: project2,
13958
- isDefault: loc.label === result.defaultLocation,
13959
- ...loc
13960
- })));
13961
- return;
13962
- }
13963
- if (result.locations.length === 0) {
13964
- console.log(`No locations configured for "${project2}".`);
14026
+ const projectData = await client.getProject(project2);
14027
+ const latest = await getLatestRunSummary(client, project2);
14028
+ if (isMachineFormat(format)) {
14029
+ let runs2 = [];
14030
+ try {
14031
+ runs2 = await client.listRuns(project2);
14032
+ } catch {
14033
+ }
14034
+ console.log(JSON.stringify({
14035
+ project: projectData,
14036
+ runs: runs2,
14037
+ latestRun: latest.run,
14038
+ totalRuns: latest.totalRuns,
14039
+ // Absent from a server that predates it.
14040
+ ...latest.admission ? { admission: latest.admission } : {},
14041
+ ...latest.manualAdmission ? { manualAdmission: latest.manualAdmission } : {}
14042
+ }, null, 2));
13965
14043
  return;
13966
14044
  }
13967
- console.log(`Locations for "${project2}" (${result.locations.length}):
14045
+ console.log(`Status: ${projectData.displayName ?? projectData.name} (${projectData.name})
13968
14046
  `);
13969
- console.log(" LABEL CITY REGION COUNTRY DEFAULT");
13970
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500");
13971
- for (const loc of result.locations) {
13972
- const isDefault = loc.label === result.defaultLocation ? " *" : "";
13973
- console.log(
13974
- ` ${loc.label.padEnd(15)} ${loc.city.padEnd(19)} ${loc.region.padEnd(19)} ${loc.country.padEnd(7)}${isDefault}`
13975
- );
13976
- }
13977
- if (result.defaultLocation) {
14047
+ console.log(` Domain: ${projectData.canonicalDomain}`);
14048
+ console.log(` Country: ${projectData.country}`);
14049
+ console.log(` Language: ${projectData.language}`);
14050
+ if (latest.run) {
13978
14051
  console.log(`
13979
- Default: ${result.defaultLocation}`);
13980
- }
13981
- }
13982
- async function removeLocation(project2, label, format) {
13983
- const client = getClient22();
13984
- await client.removeLocation(project2, label);
13985
- if (isMachineFormat(format)) {
13986
- console.log(JSON.stringify({ project: project2, label, removed: true }, null, 2));
13987
- return;
14052
+ Latest run:`);
14053
+ console.log(` ID: ${latest.run.id}`);
14054
+ console.log(` Status: ${latest.run.status}`);
14055
+ console.log(` Created: ${latest.run.createdAt}`);
14056
+ if (latest.run.finishedAt) {
14057
+ console.log(` Finished: ${latest.run.finishedAt}`);
14058
+ }
14059
+ console.log(`
14060
+ Total runs: ${latest.totalRuns}`);
14061
+ } else {
14062
+ const projectArg = /^[\w.-]+$/.test(project2) ? project2 : `'${project2.replaceAll("'", `'\\''`)}'`;
14063
+ console.log(`
14064
+ No runs yet. Capture Page Health with "canonry technical-aeo run ${projectArg}", or run an AI Visibility sweep after adding a provider.`);
13988
14065
  }
13989
- console.log(`Location removed: ${label}`);
14066
+ const admission = runAdmissionLines(project2, latest.admission, latest.manualAdmission);
14067
+ if (admission.length > 0) console.log(`
14068
+ ${admission.map((line) => ` ${line}`).join("\n")}`);
13990
14069
  }
13991
- async function setDefaultLocation(project2, label, format) {
13992
- const client = getClient22();
13993
- const result = await client.setDefaultLocation(project2, label);
13994
- if (isMachineFormat(format)) {
13995
- console.log(JSON.stringify({ project: project2, ...result }, null, 2));
13996
- return;
14070
+ async function getLatestRunSummary(client, project2) {
14071
+ try {
14072
+ return await client.getLatestRun(project2);
14073
+ } catch (err) {
14074
+ if (!isEndpointMissing(err)) throw err;
14075
+ const runs2 = await client.listRuns(project2);
14076
+ if (runs2.length === 0) {
14077
+ return { totalRuns: 0, run: null };
14078
+ }
14079
+ const latestRun = runs2.reduce(
14080
+ (current, candidate) => candidate.createdAt > current.createdAt ? candidate : current
14081
+ );
14082
+ return {
14083
+ totalRuns: runs2.length,
14084
+ run: latestRun
14085
+ };
13997
14086
  }
13998
- console.log(`Default location set to: ${label}`);
13999
14087
  }
14000
14088
 
14001
- // src/cli-commands/project.ts
14002
- var PROJECT_CLI_COMMANDS = [
14003
- {
14004
- path: ["project", "create"],
14005
- usage: "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--dispatch-mode provider=sync|batch...] [--site-audit-max-pages <1-50000|full>] [--format json]",
14006
- help: "Create a project. Pass --domain for the public site to scan. Provider credentials are not required for Page Health.",
14007
- options: {
14008
- domain: { type: "string", short: "d" },
14009
- "owned-domain": multiStringOption(),
14010
- alias: multiStringOption(),
14011
- "qualified-alias": multiStringOption(),
14012
- country: stringOption(),
14013
- language: stringOption(),
14014
- "display-name": stringOption(),
14015
- provider: multiStringOption(),
14016
- "provider-model": multiStringOption(),
14017
- "dispatch-mode": multiStringOption(),
14018
- "site-audit-max-pages": stringOption()
14019
- },
14089
+ // src/cli-commands/operator.ts
14090
+ var RESULTS_EXPORT_USAGE = "canonry results export <project> [--format json|csv] [--since <ISO>] [--until <ISO>] [--include-probes] [--output <path>|-]";
14091
+ function parseResultsExportFormat(value) {
14092
+ if (value === void 0 || value === "json") return "json";
14093
+ if (value === "csv") return "csv";
14094
+ throw usageError(`Error: --format must be "json" or "csv"
14095
+
14096
+ Usage: ${RESULTS_EXPORT_USAGE}`);
14097
+ }
14098
+ var RESULTS_CLEAR_USAGE = "canonry results clear <project> [--run <id> ...] [--research-run <id> ...] [--confirm] [--format json|jsonl]";
14099
+ function parseHistoryPageOption(input, key, usage) {
14100
+ const value = getString(input.values, key);
14101
+ if (value === void 0) return void 0;
14102
+ if (!/^\d+$/.test(value)) {
14103
+ throw usageError(`Error: --${key} must be a ${key === "limit" ? "whole number from 1 to 500" : "nonnegative integer"}
14104
+ Usage: ${usage}`);
14105
+ }
14106
+ const parsed = Number(value);
14107
+ const valid = Number.isSafeInteger(parsed) && (key === "limit" ? parsed >= 1 && parsed <= 500 : parsed >= 0);
14108
+ if (!valid) {
14109
+ throw usageError(`Error: --${key} must be a ${key === "limit" ? "whole number from 1 to 500" : "nonnegative integer"}
14110
+ Usage: ${usage}`);
14111
+ }
14112
+ return parsed;
14113
+ }
14114
+ var OPERATOR_CLI_COMMANDS = [
14115
+ {
14116
+ path: ["results", "clear"],
14117
+ usage: RESULTS_CLEAR_USAGE,
14118
+ options: { run: multiStringOption(), "research-run": multiStringOption(), confirm: { type: "boolean", default: false } },
14020
14119
  run: async (input) => {
14021
- const name = requireProject(
14022
- input,
14023
- "project.create",
14024
- "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--dispatch-mode provider=sync|batch...] [--site-audit-max-pages <1-50000|full>] [--format json]"
14025
- );
14026
- await createProject(name, {
14027
- domain: getString(input.values, "domain") ?? name,
14028
- ownedDomains: getStringArray(input.values, "owned-domain") ?? [],
14029
- aliases: getStringArray(input.values, "alias") ?? [],
14030
- qualifiedAliases: getStringArray(input.values, "qualified-alias") ?? [],
14031
- country: getString(input.values, "country") ?? "US",
14032
- language: getString(input.values, "language") ?? "en",
14033
- displayName: getString(input.values, "display-name") ?? name,
14034
- providers: getStringArray(input.values, "provider") ?? [],
14035
- providerModels: parseProviderModelAssignments(getStringArray(input.values, "provider-model")),
14036
- providerDispatchModes: parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode")),
14037
- siteAuditMaxPages: parseSiteAuditMaxPages(getString(input.values, "site-audit-max-pages"), "project.create"),
14120
+ await clearResults(requireProject(input, "results.clear", RESULTS_CLEAR_USAGE), {
14121
+ runIds: getStringArray(input.values, "run") ?? [],
14122
+ researchRunIds: getStringArray(input.values, "research-run") ?? [],
14123
+ confirm: getBoolean(input.values, "confirm"),
14038
14124
  format: input.format
14039
14125
  });
14040
14126
  }
14041
14127
  },
14042
14128
  {
14043
- path: ["project", "update"],
14044
- usage: "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--add-qualified-alias <name>...] [--remove-qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--all-providers] [--provider-model provider=model...] [--clear-provider-model <provider>...] [--dispatch-mode provider=sync|batch...] [--clear-dispatch-mode <provider>...] [--negative-review-max-stars <1-4|default>] [--site-audit-max-pages <1-50000|full>] [--competitor-auto-aliases <off|preview|apply>] [--format json]",
14129
+ path: ["results", "export"],
14130
+ usage: RESULTS_EXPORT_USAGE,
14045
14131
  options: {
14046
- domain: { type: "string", short: "d" },
14047
- "owned-domain": multiStringOption(),
14048
- "add-domain": multiStringOption(),
14049
- "remove-domain": multiStringOption(),
14050
- alias: multiStringOption(),
14051
- "add-alias": multiStringOption(),
14052
- "remove-alias": multiStringOption(),
14053
- "add-qualified-alias": multiStringOption(),
14054
- "remove-qualified-alias": multiStringOption(),
14055
- country: stringOption(),
14056
- language: stringOption(),
14057
- "display-name": stringOption(),
14058
- provider: multiStringOption(),
14059
- "all-providers": { type: "boolean" },
14060
- "provider-model": multiStringOption(),
14061
- "clear-provider-model": multiStringOption(),
14062
- "dispatch-mode": multiStringOption(),
14063
- "clear-dispatch-mode": multiStringOption(),
14064
- "negative-review-max-stars": stringOption(),
14065
- "site-audit-max-pages": stringOption(),
14066
- "competitor-auto-aliases": stringOption()
14132
+ since: stringOption(),
14133
+ until: stringOption(),
14134
+ "include-probes": { type: "boolean", default: false },
14135
+ output: { type: "string", short: "o" }
14067
14136
  },
14068
14137
  run: async (input) => {
14069
- const name = requireProject(
14070
- input,
14071
- "project.update",
14072
- "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--add-qualified-alias <name>...] [--remove-qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--all-providers] [--provider-model provider=model...] [--clear-provider-model <provider>...] [--dispatch-mode provider=sync|batch...] [--clear-dispatch-mode <provider>...] [--negative-review-max-stars <1-4|default>] [--site-audit-max-pages <1-50000|full>] [--competitor-auto-aliases <off|preview|apply>] [--format json]"
14073
- );
14074
- const providers = getStringArray(input.values, "provider");
14075
- const allProviders = getBoolean(input.values, "all-providers");
14076
- if (allProviders && providers?.length) throw usageError("Error: --all-providers conflicts with --provider");
14077
- const providerModels = parseProviderModelAssignments(getStringArray(input.values, "provider-model"));
14078
- const clearProviderModels = getStringArray(input.values, "clear-provider-model") ?? [];
14079
- for (const provider of clearProviderModels) {
14080
- if (provider in providerModels) throw usageError(`Error: --provider-model and --clear-provider-model conflict for ${provider}`);
14081
- }
14082
- const dispatchModes = parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode"));
14083
- const clearDispatchModes = getStringArray(input.values, "clear-dispatch-mode") ?? [];
14084
- for (const provider of clearDispatchModes) {
14085
- if (provider in dispatchModes) throw usageError(`Error: --dispatch-mode and --clear-dispatch-mode conflict for ${provider}`);
14086
- }
14087
- await updateProjectSettings(name, {
14088
- displayName: getString(input.values, "display-name"),
14089
- domain: getString(input.values, "domain"),
14090
- ownedDomains: getStringArray(input.values, "owned-domain"),
14091
- addOwnedDomain: getStringArray(input.values, "add-domain"),
14092
- removeOwnedDomain: getStringArray(input.values, "remove-domain"),
14093
- aliases: getStringArray(input.values, "alias"),
14094
- addAlias: getStringArray(input.values, "add-alias"),
14095
- removeAlias: getStringArray(input.values, "remove-alias"),
14096
- addQualifiedAlias: getStringArray(input.values, "add-qualified-alias"),
14097
- removeQualifiedAlias: getStringArray(input.values, "remove-qualified-alias"),
14098
- country: getString(input.values, "country"),
14099
- language: getString(input.values, "language"),
14100
- providers: allProviders ? [] : providers,
14101
- providerModels,
14102
- clearProviderModels,
14103
- dispatchModes,
14104
- clearDispatchModes,
14105
- negativeReviewMaxStars: parseNegativeReviewMaxStars(getString(input.values, "negative-review-max-stars")),
14106
- siteAuditMaxPages: parseSiteAuditMaxPages(getString(input.values, "site-audit-max-pages"), "project.update"),
14107
- competitorAutoAliases: parseCompetitorAutoAliasMode(getString(input.values, "competitor-auto-aliases")),
14108
- format: input.format
14138
+ const project2 = requireProject(input, "results export", RESULTS_EXPORT_USAGE);
14139
+ await exportResults(project2, {
14140
+ format: parseResultsExportFormat(getString(input.values, "format")),
14141
+ since: getString(input.values, "since"),
14142
+ until: getString(input.values, "until"),
14143
+ includeProbes: getBoolean(input.values, "include-probes"),
14144
+ output: getString(input.values, "output")
14109
14145
  });
14110
14146
  }
14111
14147
  },
14112
14148
  {
14113
- path: ["project", "list"],
14114
- usage: "canonry project list [--format json]",
14115
- allowPositionals: false,
14149
+ path: ["status"],
14150
+ usage: "canonry status <project> [--format json]",
14116
14151
  run: async (input) => {
14117
- await listProjects(input.format);
14152
+ const project2 = requireProject(input, "status", "canonry status <project> [--format json]");
14153
+ await showStatus(project2, input.format);
14118
14154
  }
14119
14155
  },
14120
14156
  {
14121
- path: ["project", "show"],
14122
- usage: "canonry project show <name> [--format json]",
14157
+ path: ["evidence"],
14158
+ usage: "canonry evidence <project> [--format json]",
14123
14159
  run: async (input) => {
14124
- const name = requireProject(input, "project.show", "canonry project show <name>");
14125
- await showProject(name, input.format);
14160
+ const project2 = requireProject(input, "evidence", "canonry evidence <project> [--format json]");
14161
+ await showEvidence(project2, input.format);
14126
14162
  }
14127
14163
  },
14128
14164
  {
14129
- path: ["project", "delete"],
14130
- usage: "canonry project delete <name> [--dry-run] [--format json]",
14131
- supportsDryRun: true,
14165
+ path: ["history"],
14166
+ usage: "canonry history <project> [--limit <1..500>] [--offset <n>] [--since <ISO>] [--action <action>] [--actor <actor>] [--entity-type <type>] [--format json|jsonl]\n canonry history --all [same filters]",
14167
+ options: {
14168
+ all: { type: "boolean", default: false },
14169
+ limit: stringOption(),
14170
+ offset: stringOption(),
14171
+ since: stringOption(),
14172
+ action: stringOption(),
14173
+ actor: stringOption(),
14174
+ "entity-type": stringOption()
14175
+ },
14132
14176
  run: async (input) => {
14133
- const name = requireProject(input, "project.delete", "canonry project delete <name> [--dry-run] [--format json]");
14134
- await deleteProject(name, { dryRun: input.dryRun, format: input.format });
14177
+ const usage = "canonry history <project> [--limit <1..500>] [--offset <n>] [--since <ISO>] [--action <action>] [--actor <actor>] [--entity-type <type>] [--format json|jsonl]\n canonry history --all [same filters]";
14178
+ const project2 = getBoolean(input.values, "all") ? void 0 : requireProject(input, "history", usage);
14179
+ await showHistory(project2, input.format, {
14180
+ limit: parseHistoryPageOption(input, "limit", usage),
14181
+ offset: parseHistoryPageOption(input, "offset", usage),
14182
+ since: getString(input.values, "since"),
14183
+ action: getString(input.values, "action"),
14184
+ actor: getString(input.values, "actor"),
14185
+ entityType: getString(input.values, "entity-type")
14186
+ });
14135
14187
  }
14136
14188
  },
14137
14189
  {
14138
- path: ["project", "add-location"],
14139
- usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]",
14190
+ path: ["export"],
14191
+ usage: "canonry export <project> [--include-results] [--format json]",
14140
14192
  options: {
14141
- label: stringOption(),
14142
- city: stringOption(),
14143
- region: stringOption(),
14144
- country: stringOption(),
14145
- timezone: stringOption()
14193
+ "include-results": { type: "boolean", default: false }
14146
14194
  },
14147
14195
  run: async (input) => {
14148
- const name = requireProject(
14149
- input,
14150
- "project.add-location",
14151
- "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]"
14152
- );
14153
- const label = getString(input.values, "label");
14154
- const city = getString(input.values, "city");
14155
- const region = getString(input.values, "region");
14156
- const country = getString(input.values, "country");
14157
- if (!label || !city || !region || !country) {
14158
- throw usageError("Error: --label, --city, --region, and --country are all required", {
14159
- message: "location label, city, region, and country are required",
14160
- details: {
14161
- command: "project.add-location",
14162
- usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]",
14163
- required: ["label", "city", "region", "country"]
14164
- }
14165
- });
14166
- }
14167
- await addLocation(name, {
14168
- label,
14169
- city,
14170
- region,
14171
- country,
14172
- timezone: getString(input.values, "timezone"),
14196
+ const project2 = requireProject(input, "export", "canonry export <project> [--include-results] [--format json]");
14197
+ await exportProject(project2, {
14198
+ includeResults: getBoolean(input.values, "include-results"),
14173
14199
  format: input.format
14174
14200
  });
14175
14201
  }
14176
14202
  },
14177
14203
  {
14178
- path: ["project", "locations"],
14179
- usage: "canonry project locations <name> [--format json]",
14180
- run: async (input) => {
14181
- const name = requireProject(input, "project.locations", "canonry project locations <name> [--format json]");
14182
- await listLocations(name, input.format);
14183
- }
14184
- },
14185
- {
14186
- path: ["project", "remove-location"],
14187
- usage: "canonry project remove-location <name> <label> [--format json]",
14204
+ path: ["analytics"],
14205
+ usage: "canonry analytics <project> [--feature metrics|gaps|sources] [--window 7d|30d|90d|all] [--format json]",
14206
+ options: {
14207
+ feature: stringOption(),
14208
+ window: stringOption()
14209
+ },
14188
14210
  run: async (input) => {
14189
- const name = requireProject(input, "project.remove-location", "canonry project remove-location <name> <label> [--format json]");
14190
- const label = requirePositional(input, 1, {
14191
- command: "project.remove-location",
14192
- usage: "canonry project remove-location <name> <label> [--format json]",
14193
- message: "project name and location label are required"
14211
+ const project2 = requireProject(input, "analytics", "canonry analytics <project> [--feature metrics|gaps|sources] [--window 7d|30d|90d|all] [--format json]");
14212
+ await showAnalytics(project2, {
14213
+ feature: getString(input.values, "feature"),
14214
+ window: getString(input.values, "window"),
14215
+ format: input.format
14194
14216
  });
14195
- await removeLocation(name, label, input.format);
14196
14217
  }
14197
14218
  },
14198
14219
  {
14199
- path: ["project", "set-default-location"],
14200
- usage: "canonry project set-default-location <name> <label> [--format json]",
14201
- run: async (input) => {
14202
- const name = requireProject(input, "project.set-default-location", "canonry project set-default-location <name> <label> [--format json]");
14203
- const label = requirePositional(input, 1, {
14204
- command: "project.set-default-location",
14205
- usage: "canonry project set-default-location <name> <label> [--format json]",
14206
- message: "project name and location label are required"
14207
- });
14208
- await setDefaultLocation(name, label, input.format);
14209
- }
14220
+ path: ["sources"],
14221
+ usage: SOURCES_USAGE,
14222
+ options: {
14223
+ rank: { type: "boolean", default: false },
14224
+ "by-provider": { type: "boolean", default: false },
14225
+ limit: stringOption(),
14226
+ window: stringOption(),
14227
+ "query-class": stringOption(),
14228
+ "run-id": stringOption(),
14229
+ "include-by-query": stringOption()
14230
+ },
14231
+ run: async (input) => {
14232
+ const project2 = requireProject(input, "sources", SOURCES_USAGE);
14233
+ await showSources(project2, {
14234
+ rank: getBoolean(input.values, "rank"),
14235
+ byProvider: getBoolean(input.values, "by-provider"),
14236
+ limit: parseIntegerOption(input, "limit", {
14237
+ message: "--limit must be an integer",
14238
+ usage: SOURCES_USAGE,
14239
+ command: "sources"
14240
+ }),
14241
+ window: getString(input.values, "window"),
14242
+ runId: getString(input.values, "run-id"),
14243
+ queryClass: parseSourcesQueryClass(getString(input.values, "query-class")),
14244
+ includeByQuery: parseSourcesIncludeByQuery(getString(input.values, "include-by-query")),
14245
+ format: input.format
14246
+ });
14247
+ }
14210
14248
  },
14211
14249
  {
14212
- path: ["project"],
14213
- usage: "canonry project <create|update|list|show|delete|add-location|locations|remove-location|set-default-location> [args]",
14250
+ path: ["apply"],
14251
+ usage: "canonry apply <file...> [--format json]",
14214
14252
  run: async (input) => {
14215
- unknownSubcommand(input.positionals[0], {
14216
- command: "project",
14217
- usage: "canonry project <create|update|list|show|delete|add-location|locations|remove-location|set-default-location> [args]",
14218
- available: ["create", "update", "list", "show", "delete", "add-location", "locations", "remove-location", "set-default-location"]
14219
- });
14253
+ if (input.positionals.length === 0) {
14254
+ throw usageError("Error: at least one file path is required\nUsage: canonry apply <file...> [--format json]", {
14255
+ message: "at least one file path is required",
14256
+ details: {
14257
+ command: "apply",
14258
+ usage: "canonry apply <file...> [--format json]"
14259
+ }
14260
+ });
14261
+ }
14262
+ await applyConfigs(input.positionals, input.format);
14220
14263
  }
14221
14264
  }
14222
14265
  ];
14223
- function parseDispatchModeAssignments(assignments) {
14224
- const result = {};
14225
- for (const assignment of assignments ?? []) {
14226
- const separator = assignment.indexOf("=");
14227
- const provider = separator === -1 ? "" : assignment.slice(0, separator).trim();
14228
- const mode = providerDispatchModeSchema.safeParse(separator === -1 ? "" : assignment.slice(separator + 1).trim().toLowerCase());
14229
- if (!provider || !mode.success) throw usageError(`Error: --dispatch-mode must use provider=sync or provider=batch (received ${assignment})`);
14230
- if (provider in result) throw usageError(`Error: duplicate --dispatch-mode assignment for ${provider}`);
14231
- result[provider] = mode.data;
14232
- }
14233
- return result;
14266
+
14267
+ // src/commands/project.ts
14268
+ function getClient23() {
14269
+ return createApiClient();
14234
14270
  }
14235
- function parseProviderModelAssignments(assignments) {
14236
- const result = {};
14237
- for (const assignment of assignments ?? []) {
14238
- const separator = assignment.indexOf("=");
14239
- const provider = separator === -1 ? "" : assignment.slice(0, separator).trim();
14240
- const model = separator === -1 ? "" : assignment.slice(separator + 1).trim();
14241
- if (!provider || !model) throw usageError(`Error: --provider-model must use provider=model (received ${assignment})`);
14242
- if (provider in result) throw usageError(`Error: duplicate --provider-model assignment for ${provider}`);
14243
- result[provider] = model;
14271
+ async function createProject(name, opts) {
14272
+ const client = getClient23();
14273
+ const result = await client.putProject(name, {
14274
+ displayName: opts.displayName,
14275
+ canonicalDomain: opts.domain,
14276
+ ownedDomains: opts.ownedDomains ?? [],
14277
+ aliases: normalizeProjectAliases(opts.displayName, opts.aliases ?? []),
14278
+ ...opts.qualifiedAliases?.length ? { qualifiedAliases: opts.qualifiedAliases } : {},
14279
+ country: opts.country,
14280
+ language: opts.language,
14281
+ providers: opts.providers ?? [],
14282
+ providerModels: opts.providerModels ?? {},
14283
+ ...opts.providerDispatchModes && Object.keys(opts.providerDispatchModes).length > 0 ? { providerDispatchModes: opts.providerDispatchModes } : {},
14284
+ // Sent only when given: this PUT also re-runs over an existing project,
14285
+ // and an omitted budget keeps the stored one.
14286
+ ...opts.siteAuditMaxPages !== void 0 ? { siteAuditMaxPages: opts.siteAuditMaxPages } : {}
14287
+ });
14288
+ warnDroppedDispatchModes(opts.providerDispatchModes ?? {}, result);
14289
+ if (isMachineFormat(opts.format)) {
14290
+ console.log(JSON.stringify(result, null, 2));
14291
+ return;
14244
14292
  }
14245
- return result;
14293
+ console.log(`Project created: ${result.name} (${result.id})`);
14246
14294
  }
14247
- function parseNegativeReviewMaxStars(raw) {
14248
- if (raw === void 0) return void 0;
14249
- if (raw === "default") return null;
14250
- const parsed = gbpNegativeReviewMaxStarsSchema.safeParse(Number(raw));
14251
- if (!parsed.success) {
14252
- throw usageError('Error: --negative-review-max-stars must be 1, 2, 3, 4, or "default" (3)', {
14253
- message: '--negative-review-max-stars must be 1-4 or "default"',
14254
- details: { command: "project.update", value: raw }
14255
- });
14295
+ function warnDroppedDispatchModes(sent, result) {
14296
+ const kept = result.providerDispatchModes ?? {};
14297
+ const dropped = Object.keys(sent).filter((provider) => !(provider in kept));
14298
+ if (dropped.length > 0) {
14299
+ console.error(`Warning: Dropped dispatch mode for engine(s) the project does not measure: ${dropped.join(", ")}`);
14256
14300
  }
14257
- return parsed.data;
14258
14301
  }
14259
- function parseSiteAuditMaxPages(raw, command) {
14260
- if (raw === void 0) return void 0;
14261
- const value = raw.trim();
14262
- if (value.toLowerCase() === "full") return null;
14263
- const parsed = /^\d+$/.test(value) ? siteAuditPageBudgetSchema.safeParse(Number(value)) : void 0;
14264
- if (!parsed?.success) {
14265
- throw usageError(`Error: --site-audit-max-pages must be a whole number of pages from 1 to ${SITE_AUDIT_MAX_PAGE_LIMIT}, or "full" for the full site`, {
14266
- message: `--site-audit-max-pages must be 1-${SITE_AUDIT_MAX_PAGE_LIMIT} or "full"`,
14267
- details: { command, option: "site-audit-max-pages", value: raw }
14268
- });
14302
+ async function listProjects(format) {
14303
+ const client = getClient23();
14304
+ const projects2 = await client.listProjects();
14305
+ if (format === "json") {
14306
+ console.log(JSON.stringify(projects2, null, 2));
14307
+ return;
14269
14308
  }
14270
- return parsed.data;
14271
- }
14272
- function parseCompetitorAutoAliasMode(raw) {
14273
- if (raw === void 0) return void 0;
14274
- const parsed = competitorAutoAliasModeSchema.safeParse(raw.trim().toLowerCase());
14275
- if (!parsed.success) {
14276
- throw usageError("Error: --competitor-auto-aliases must be off, preview or apply", {
14277
- message: "--competitor-auto-aliases must be off, preview or apply",
14278
- details: { command: "project.update", option: "competitor-auto-aliases", value: raw }
14279
- });
14309
+ if (format === "jsonl") {
14310
+ emitJsonl(projects2);
14311
+ return;
14280
14312
  }
14281
- return parsed.data;
14282
- }
14283
-
14284
- // src/commands/organic-evidence.ts
14285
- async function showOrganicEvidence(project2, opts) {
14286
- const data = await createApiClient().getOrganicEvidence(project2, opts.period);
14287
- if (isMachineFormat(opts.format)) {
14288
- console.log(JSON.stringify(data, null, 2));
14313
+ if (projects2.length === 0) {
14314
+ console.log("No projects found.");
14289
14315
  return;
14290
14316
  }
14291
- printOrganicEvidence(project2, data);
14317
+ console.log("Projects:\n");
14318
+ const nameWidth = Math.max(4, ...projects2.map((p) => p.name.length));
14319
+ const domainLabel = (p) => {
14320
+ const extra = Math.max(0, effectiveDomains(p).length - 1);
14321
+ return extra > 0 ? `${p.canonicalDomain} (+${extra})` : p.canonicalDomain;
14322
+ };
14323
+ const domainWidth = Math.max(6, ...projects2.map((p) => domainLabel(p).length));
14324
+ console.log(
14325
+ ` ${"NAME".padEnd(nameWidth)} ${"DOMAIN".padEnd(domainWidth)} COUNTRY LANGUAGE`
14326
+ );
14327
+ console.log(` ${"\u2500".repeat(nameWidth)} ${"\u2500".repeat(domainWidth)} \u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
14328
+ for (const p of projects2) {
14329
+ console.log(
14330
+ ` ${p.name.padEnd(nameWidth)} ${domainLabel(p).padEnd(domainWidth)} ${p.country.padEnd(7)} ${p.language}`
14331
+ );
14332
+ }
14292
14333
  }
14293
- function formatCounts(clicks, impressions) {
14294
- return `${clicks} clicks / ${impressions} impressions`;
14334
+ function storedQualifiedAliases(project2) {
14335
+ return project2.qualifiedAliases ?? [];
14295
14336
  }
14296
- function printOrganicEvidence(project2, data) {
14297
- const asOf = data.asOfDate ? ` through ${data.asOfDate}` : "";
14298
- console.log(`Organic evidence: ${project2} (${data.periodDays} days${asOf})`);
14299
- console.log(`Coverage: GSC ${data.coverage.gsc ? "yes" : "no"} \xB7 GA4 ${data.coverage.ga4 ? "yes" : "no"} \xB7 server ${data.coverage.server ? "yes" : "no"} \xB7 AI visibility ${data.coverage.visibility ? "yes" : "no"}`);
14300
- if (data.gsc) {
14301
- console.log(`Google Search: ${formatCounts(data.gsc.propertyTotals.clicks, data.gsc.propertyTotals.impressions)} (named non-brand: ${formatCounts(data.gsc.namedNonBrand.clicks, data.gsc.namedNonBrand.impressions)})`);
14337
+ async function showProject(name, format) {
14338
+ const client = getClient23();
14339
+ const project2 = await client.getProject(name);
14340
+ if (isMachineFormat(format)) {
14341
+ console.log(JSON.stringify(project2, null, 2));
14342
+ return;
14302
14343
  }
14303
- if (data.ga4) console.log(`GA4 organic: ${data.ga4.organicSessions} sessions`);
14304
- if (data.gaAiReferrals) console.log(`GA4 AI referrals: ${data.gaAiReferrals.organicSessions} organic, ${data.gaAiReferrals.paidSessions} paid sessions`);
14305
- if (data.server) {
14306
- const s = data.server;
14307
- const fetchTotal = s.userFetchHits.verified + s.userFetchHits.claimedUnverified + s.userFetchHits.unknownAiLike;
14308
- console.log(`Server AI: ${s.crawlerHits.verified} verified + ${s.crawlerHits.claimedUnverified} claimed + ${s.crawlerHits.unknownAiLike} heuristic crawls; ${fetchTotal} user-agent fetches (${s.userFetchHits.verified} verified, ${s.userFetchHits.claimedUnverified} claimed, ${s.userFetchHits.unknownAiLike} heuristic); ${s.referralSessions.organic} organic referrals, ${s.referralSessions.paid} paid, ${s.referralSessions.unknown} unclassified`);
14344
+ console.log(`Project: ${project2.displayName ?? project2.name}
14345
+ `);
14346
+ console.log(` Name: ${project2.name}`);
14347
+ console.log(` ID: ${project2.id}`);
14348
+ console.log(` Domain: ${project2.canonicalDomain}`);
14349
+ const secondaryDomains = effectiveDomains(project2).slice(1);
14350
+ if (secondaryDomains.length > 0) {
14351
+ console.log(` Owned domains: ${secondaryDomains.join(", ")}`);
14309
14352
  }
14310
- if (data.visibility) {
14311
- const v = data.visibility;
14312
- console.log(`Latest AI visibility: ${v.mentionedPairs}/${v.answerPairs} mentioned, ${v.citedPairs}/${v.answerPairs} cited (${Math.floor(v.ageDays)}d old)`);
14353
+ if (project2.aliases && project2.aliases.length > 0) {
14354
+ console.log(` Aliases: ${project2.aliases.join(", ")}`);
14313
14355
  }
14314
- if (data.findings.length > 0) {
14315
- console.log("\nFindings:");
14316
- for (const finding of data.findings) console.log(`- ${finding.title}: ${finding.detail}`);
14356
+ const qualifiedAliases = storedQualifiedAliases(project2);
14357
+ if (qualifiedAliases.length > 0) {
14358
+ console.log(` Qualified: ${qualifiedAliases.join(", ")} (sentiment)`);
14317
14359
  }
14318
- if (data.limitations.length > 0) {
14319
- console.log("\nLimitations:");
14320
- for (const limitation of data.limitations) console.log(`- ${limitation.detail}`);
14360
+ console.log(` Country: ${project2.country}`);
14361
+ console.log(` Language: ${project2.language}`);
14362
+ console.log(` Config source: ${project2.configSource}`);
14363
+ console.log(` Config revision: ${project2.configRevision}`);
14364
+ console.log(` Providers: ${(project2.providers ?? []).length > 0 ? project2.providers.join(", ") : "all configured"}`);
14365
+ const providerModels = Object.entries(project2.providerModels ?? {});
14366
+ console.log(` Model overrides: ${providerModels.length > 0 ? providerModels.map(([provider, model]) => `${provider}=${model}`).join(", ") : "(none; instance settings inherited)"}`);
14367
+ const dispatchModes = Object.entries(project2.providerDispatchModes ?? {}).sort(([left], [right]) => left.localeCompare(right));
14368
+ console.log(` Dispatch modes: ${dispatchModes.length > 0 ? `${dispatchModes.map(([provider, mode]) => `${provider}=${mode}`).join(", ")} (scheduled sweeps; others sync)` : "(none; every provider runs sync)"}`);
14369
+ console.log(` Negative reviews: ${resolveNegativeReviewMaxStars(project2.negativeReviewMaxStars)} stars or fewer${project2.negativeReviewMaxStars == null ? " (default)" : ""}`);
14370
+ if (project2.siteAuditMaxPages !== void 0) {
14371
+ console.log(` Site Health page budget: ${formatSiteAuditPageBudget(project2.siteAuditMaxPages)}`);
14321
14372
  }
14322
- }
14323
-
14324
- // src/cli-commands/organic-evidence.ts
14325
- var USAGE3 = "canonry organic-evidence <project> [--period 60|90] [--format json|jsonl]";
14326
- function parsePeriod(value) {
14327
- if (value === void 0) return void 0;
14328
- if (value === "60" || value === "90") return Number(value);
14329
- throw usageError(`Error: --period must be 60 or 90
14330
-
14331
- Usage: ${USAGE3}`);
14332
- }
14333
- var ORGANIC_EVIDENCE_CLI_COMMANDS = [
14334
- {
14335
- path: ["organic-evidence"],
14336
- usage: USAGE3,
14337
- options: { period: stringOption() },
14338
- run: async (input) => {
14339
- const project2 = requireProject(input, "organic-evidence", USAGE3);
14340
- await showOrganicEvidence(project2, {
14341
- period: parsePeriod(getString(input.values, "period")),
14342
- format: input.format
14343
- });
14344
- }
14373
+ if (project2.competitorAutoAliases !== void 0) {
14374
+ console.log(` Competitor auto aliases: ${formatCompetitorAutoAliasMode(project2.competitorAutoAliases)}`);
14345
14375
  }
14346
- ];
14347
-
14348
- // src/commands/run.ts
14349
- function getClient23() {
14350
- return createApiClient();
14376
+ console.log(` Tags: ${project2.tags.length > 0 ? project2.tags.join(", ") : "(none)"}`);
14377
+ const labelEntries = Object.entries(project2.labels);
14378
+ console.log(` Labels: ${labelEntries.length > 0 ? labelEntries.map(([k, v]) => `${k}=${v}`).join(", ") : "(none)"}`);
14379
+ if (project2.createdAt) console.log(` Created: ${project2.createdAt}`);
14380
+ if (project2.updatedAt) console.log(` Updated: ${project2.updatedAt}`);
14351
14381
  }
14352
- var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
14353
- function throwIfWaitedRunFailed(waited) {
14354
- const failed = waited.filter((r) => r.status === RunStatuses.failed);
14355
- if (failed.length === 0) return;
14356
- const reason = (r) => r.error ? formatRunErrorOneLine(r.error) : null;
14357
- const label = (r) => [r.project, r.location ? `(${r.location})` : null, `run ${r.runId}`].filter(Boolean).join(" ");
14358
- const message = waited.length === 1 ? `Run ${failed[0].runId} failed${reason(failed[0]) ? `: ${reason(failed[0])}` : ""}` : `${failed.length} of ${waited.length} runs failed: ${failed.map((r) => `${label(r)}${reason(r) ? `: ${reason(r)}` : ""}`).join("; ")}`;
14359
- throw new CliError({
14360
- code: "RUN_FAILED",
14361
- message,
14362
- exitCode: EXIT_SYSTEM_ERROR,
14363
- details: {
14364
- waitedRunCount: waited.length,
14365
- failedRuns: failed.map((r) => ({
14366
- runId: r.runId,
14367
- ...r.project ? { project: r.project } : {},
14368
- ...r.location ? { location: r.location } : {},
14369
- error: reason(r)
14370
- }))
14371
- }
14372
- });
14382
+ function formatCompetitorAutoAliasMode(mode) {
14383
+ if (mode === "apply") return "apply (names detected after each sweep are stored and counted)";
14384
+ if (mode === "off") return "off (no detection after sweeps)";
14385
+ return "preview (detected after each sweep and logged; nothing stored)";
14373
14386
  }
14374
- async function triggerRun(project2, opts) {
14387
+ async function updateProjectSettings(name, opts) {
14375
14388
  const client = getClient23();
14376
- const body = {};
14377
- if (opts?.provider) {
14378
- const providerInputs = opts.provider.split(",").map((s) => s.trim()).filter(Boolean);
14379
- const resolved = providerInputs.flatMap((p) => resolveProviderInput(p));
14380
- body.providers = resolved.length > 0 ? resolved : providerInputs;
14389
+ const project2 = await client.getProject(name);
14390
+ let ownedDomains = opts.ownedDomains ?? project2.ownedDomains ?? [];
14391
+ if (opts.addOwnedDomain) {
14392
+ const toAdd = opts.addOwnedDomain.filter((d) => !ownedDomains.includes(d));
14393
+ ownedDomains = [...ownedDomains, ...toAdd];
14381
14394
  }
14382
- if (opts?.queries?.length) {
14383
- body.queries = opts.queries;
14395
+ if (opts.removeOwnedDomain) {
14396
+ const toRemove = new Set(opts.removeOwnedDomain);
14397
+ ownedDomains = ownedDomains.filter((d) => !toRemove.has(d));
14384
14398
  }
14385
- if (opts?.groups?.length || opts?.targets?.length) {
14386
- body.measurementScope = {
14387
- ...opts.groups?.length ? { groups: opts.groups } : {},
14388
- ...opts.targets?.length ? { targets: opts.targets } : {}
14389
- };
14399
+ const nextDisplayName = opts.displayName ?? project2.displayName ?? project2.name;
14400
+ let aliases = opts.aliases ?? project2.aliases ?? [];
14401
+ if (opts.addAlias) {
14402
+ const existingKeys = new Set(aliases.map((a) => a.toLowerCase()));
14403
+ const toAdd = opts.addAlias.filter((a) => !existingKeys.has(a.toLowerCase()));
14404
+ aliases = [...aliases, ...toAdd];
14390
14405
  }
14391
- if (opts?.location) {
14392
- body.location = opts.location;
14406
+ if (opts.removeAlias) {
14407
+ const toRemove = new Set(opts.removeAlias.map((a) => a.toLowerCase()));
14408
+ aliases = aliases.filter((a) => !toRemove.has(a.toLowerCase()));
14393
14409
  }
14394
- if (opts?.allLocations) {
14395
- body.allLocations = true;
14410
+ const nextAliases = normalizeProjectAliases(nextDisplayName, aliases);
14411
+ const touchesQualified = (opts.addQualifiedAlias?.length ?? 0) > 0 || (opts.removeQualifiedAlias?.length ?? 0) > 0;
14412
+ let qualifiedAliases = resolveProjectQualifiedAliases(
14413
+ { displayName: nextDisplayName, aliases: nextAliases },
14414
+ storedQualifiedAliases(project2)
14415
+ ).value;
14416
+ if (opts.addQualifiedAlias) {
14417
+ const existingKeys = new Set(qualifiedAliases.map((alias) => alias.toLowerCase()));
14418
+ for (const alias of opts.addQualifiedAlias) {
14419
+ if (existingKeys.has(alias.toLowerCase())) continue;
14420
+ existingKeys.add(alias.toLowerCase());
14421
+ qualifiedAliases = [...qualifiedAliases, alias];
14422
+ }
14396
14423
  }
14397
- if (opts?.noLocation) {
14398
- body.noLocation = true;
14424
+ if (opts.removeQualifiedAlias) {
14425
+ const toRemove = new Set(opts.removeQualifiedAlias.map((alias) => alias.toLowerCase()));
14426
+ qualifiedAliases = qualifiedAliases.filter((alias) => !toRemove.has(alias.toLowerCase()));
14399
14427
  }
14400
- if (opts?.probe) {
14401
- body.trigger = "probe";
14428
+ const providerModels = { ...project2.providerModels ?? {}, ...opts.providerModels ?? {} };
14429
+ for (const provider of opts.clearProviderModels ?? []) delete providerModels[provider];
14430
+ const nextProviders = opts.providers ?? project2.providers ?? [];
14431
+ if (nextProviders.length > 0) {
14432
+ const requested = Object.keys(opts.providerModels ?? {}).filter((p) => !nextProviders.includes(p));
14433
+ if (requested.length > 0) {
14434
+ throw usageError(
14435
+ `Error: --provider-model set for ${requested.join(", ")}, which ${requested.length > 1 ? "are" : "is"} not a selected engine for "${name}"`,
14436
+ {
14437
+ message: "provider-model set for a provider the project does not run",
14438
+ details: { command: "project.update", project: name, providers: nextProviders, unselected: requested }
14439
+ }
14440
+ );
14441
+ }
14402
14442
  }
14403
- if (opts?.dispatchMode) {
14404
- body.dispatchMode = opts.dispatchMode;
14443
+ const touchesDispatch = Object.keys(opts.dispatchModes ?? {}).length > 0 || (opts.clearDispatchModes ?? []).length > 0;
14444
+ const providerDispatchModes = { ...project2.providerDispatchModes ?? {}, ...opts.dispatchModes ?? {} };
14445
+ for (const provider of opts.clearDispatchModes ?? []) delete providerDispatchModes[provider];
14446
+ const result = await client.putProject(name, {
14447
+ displayName: nextDisplayName,
14448
+ canonicalDomain: opts.domain ?? project2.canonicalDomain,
14449
+ ownedDomains,
14450
+ aliases: nextAliases,
14451
+ ...touchesQualified ? { qualifiedAliases } : {},
14452
+ country: opts.country ?? project2.country,
14453
+ language: opts.language ?? project2.language,
14454
+ tags: project2.tags,
14455
+ labels: project2.labels,
14456
+ providers: opts.providers ?? project2.providers,
14457
+ providerModels,
14458
+ ...touchesDispatch ? { providerDispatchModes } : {},
14459
+ locations: project2.locations,
14460
+ defaultLocation: project2.defaultLocation,
14461
+ autoExtractBacklinks: project2.autoExtractBacklinks,
14462
+ // Sent only when asked for: the server keeps the stored threshold when the
14463
+ // field is absent, so a stale read cannot overwrite a newer setting.
14464
+ ...opts.negativeReviewMaxStars !== void 0 ? { negativeReviewMaxStars: opts.negativeReviewMaxStars } : {},
14465
+ // Same for the page budget: never echoed from the read above, so an edit
14466
+ // that does not touch it cannot overwrite a newer one or send a stray null.
14467
+ ...opts.siteAuditMaxPages !== void 0 ? { siteAuditMaxPages: opts.siteAuditMaxPages } : {},
14468
+ // Same for the detection mode.
14469
+ ...opts.competitorAutoAliases !== void 0 ? { competitorAutoAliases: opts.competitorAutoAliases } : {}
14470
+ });
14471
+ warnDroppedDispatchModes(touchesDispatch ? providerDispatchModes : project2.providerDispatchModes ?? {}, result);
14472
+ if (isMachineFormat(opts.format)) {
14473
+ console.log(JSON.stringify(result, null, 2));
14474
+ return;
14405
14475
  }
14406
- if (opts?.force) {
14407
- body.force = true;
14476
+ console.log(`Project updated: ${result.name}`);
14477
+ const dropped = Object.keys(providerModels).filter((provider) => !(provider in result.providerModels));
14478
+ if (dropped.length > 0) {
14479
+ console.log(` Dropped model override for deselected engine(s): ${dropped.join(", ")}`);
14408
14480
  }
14409
- const response = await client.triggerRun(project2, body);
14410
- if (Array.isArray(response)) {
14411
- const locationRuns = response;
14412
- if (isMachineFormat(opts?.format)) {
14413
- if (opts?.wait) {
14414
- const finals = await Promise.all(
14415
- locationRuns.map(async (r) => !r.id || r.status === "conflict" ? null : pollRun2(client, r.id, false))
14416
- );
14417
- console.log(JSON.stringify(locationRuns.map((r, i) => finals[i] ? { ...r, ...finals[i] } : r), null, 2));
14418
- throwIfWaitedRunFailed(locationRuns.flatMap((r, i) => {
14419
- const final = finals[i];
14420
- return final ? [{ runId: final.id, status: final.status, project: project2, location: r.location ?? null, error: final.error }] : [];
14421
- }));
14422
- } else {
14423
- console.log(JSON.stringify(locationRuns, null, 2));
14424
- }
14481
+ }
14482
+ async function deleteProject(name, opts) {
14483
+ const client = getClient23();
14484
+ const isJson = isMachineFormat(opts?.format);
14485
+ if (opts?.dryRun) {
14486
+ const preview = await client.previewProjectDelete(name);
14487
+ if (isJson) {
14488
+ console.log(JSON.stringify({ dryRun: true, ...preview }, null, 2));
14425
14489
  return;
14426
14490
  }
14427
- console.log(`Triggered ${locationRuns.length} location sweep(s) \u2014 ${locationRuns.length}\xD7 API calls:
14428
- `);
14429
- console.log(" LOCATION RUN ID STATUS");
14430
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
14431
- for (const r of locationRuns) {
14432
- const loc = (r.location ?? "(unknown)").padEnd(15);
14433
- const id = (r.id ?? "(conflict)").padEnd(36);
14434
- console.log(` ${loc} ${id} ${r.status}`);
14435
- }
14436
- if (opts?.wait) {
14437
- const pending = locationRuns.filter((r) => r.id && r.status !== "conflict" && !TERMINAL_STATUSES.has(r.status));
14438
- const errors = /* @__PURE__ */ new Map();
14439
- if (pending.length > 0) {
14440
- process.stderr.write(`Waiting for ${pending.length} run(s)`);
14441
- const batchWaits = /* @__PURE__ */ new Map();
14442
- await Promise.all(
14443
- pending.map(async (r) => {
14444
- const final = await pollRun2(client, r.id);
14445
- r.status = final.status;
14446
- errors.set(r.id, final.error);
14447
- const waiting = batchWaitLine(final);
14448
- if (waiting) batchWaits.set(r.id, waiting);
14449
- })
14450
- );
14451
- process.stderr.write("\n");
14452
- console.log("\nFinal statuses:");
14453
- for (const r of locationRuns) {
14454
- const loc = (r.location ?? "(unknown)").padEnd(15);
14455
- console.log(` ${loc} ${r.status}`);
14456
- }
14457
- if (batchWaits.size > 0) console.log("");
14458
- for (const r of locationRuns) {
14459
- const waiting = batchWaits.get(r.id);
14460
- if (waiting) console.log(` ${r.location ?? "(unknown)"}: ${waiting}`);
14461
- }
14462
- }
14463
- throwIfWaitedRunFailed(locationRuns.filter((r) => r.id && r.status !== "conflict").map((r) => ({ runId: r.id, status: r.status, project: project2, location: r.location ?? null, error: errors.get(r.id) })));
14464
- }
14491
+ const { cascadeRows: cr, detachedRows: dr } = preview;
14492
+ console.log(`Project delete preview for "${name}":`);
14493
+ console.log(` Cascade-deletes:`);
14494
+ console.log(` queries: ${cr.queries}`);
14495
+ console.log(` competitors: ${cr.competitors}`);
14496
+ console.log(` runs: ${cr.runs}`);
14497
+ console.log(` snapshots: ${cr.snapshots}`);
14498
+ console.log(` insights: ${cr.insights}`);
14499
+ console.log(` Detached (project_id set to NULL):`);
14500
+ console.log(` audit_log: ${dr.auditLog}`);
14501
+ console.log(``);
14502
+ console.log(`No DB writes performed. Re-run without --dry-run to delete.`);
14465
14503
  return;
14466
14504
  }
14467
- const run = response;
14468
- if (opts?.wait && run.id && !TERMINAL_STATUSES.has(run.status)) {
14469
- const showProgress = !isMachineFormat(opts?.format);
14470
- if (showProgress) process.stderr.write(`Run ${run.id} started`);
14471
- const result = await pollRun2(client, run.id, showProgress);
14472
- if (isMachineFormat(opts?.format)) {
14473
- console.log(JSON.stringify(result, null, 2));
14474
- } else {
14475
- process.stderr.write("\n");
14476
- printRunDetail(result);
14477
- const waiting = batchWaitLine(result);
14478
- if (waiting) console.log(`
14479
- ${waiting}`);
14480
- }
14481
- throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project: project2, error: result.error }]);
14505
+ await client.deleteProject(name);
14506
+ if (isJson) {
14507
+ console.log(JSON.stringify({ name, deleted: true }, null, 2));
14482
14508
  return;
14483
14509
  }
14484
- if (opts?.wait && (TERMINAL_STATUSES.has(run.status) || !run.id)) {
14485
- const result = run.id ? await client.getRun(run.id) : run;
14486
- if (isMachineFormat(opts?.format)) {
14487
- console.log(JSON.stringify(result, null, 2));
14488
- } else {
14489
- printRunDetail(result);
14490
- }
14491
- throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project: project2, error: result.error }]);
14510
+ console.log(`Project deleted: ${name}`);
14511
+ }
14512
+ async function addLocation(project2, opts) {
14513
+ const client = getClient23();
14514
+ const location = await client.addLocation(project2, {
14515
+ label: opts.label,
14516
+ city: opts.city,
14517
+ region: opts.region,
14518
+ country: opts.country,
14519
+ timezone: opts.timezone
14520
+ });
14521
+ if (isMachineFormat(opts.format)) {
14522
+ console.log(JSON.stringify(location, null, 2));
14492
14523
  return;
14493
14524
  }
14494
- if (isMachineFormat(opts?.format)) {
14495
- console.log(JSON.stringify(run, null, 2));
14525
+ console.log(`Location added: ${opts.label} (${opts.city}, ${opts.region}, ${opts.country})`);
14526
+ }
14527
+ async function listLocations(project2, format) {
14528
+ const client = getClient23();
14529
+ const result = await client.listLocations(project2);
14530
+ if (format === "json") {
14531
+ console.log(JSON.stringify(result, null, 2));
14496
14532
  return;
14497
14533
  }
14498
- console.log(`Run created: ${run.id}`);
14499
- console.log(` Kind: ${run.kind}`);
14500
- console.log(` Status: ${run.status}`);
14501
- if (opts?.provider) {
14502
- console.log(` Provider: ${opts.provider}`);
14503
- }
14504
- }
14505
- async function triggerRunAll(opts) {
14506
- const client = getClient23();
14507
- const projects2 = await client.listProjects();
14508
- if (projects2.length === 0) {
14509
- if (isMachineFormat(opts?.format)) {
14510
- console.log("[]");
14511
- } else {
14512
- console.log("No projects found.");
14513
- }
14534
+ if (format === "jsonl") {
14535
+ emitJsonl(result.locations.map((loc) => ({
14536
+ project: project2,
14537
+ isDefault: loc.label === result.defaultLocation,
14538
+ ...loc
14539
+ })));
14514
14540
  return;
14515
14541
  }
14516
- const baseBody = {};
14517
- if (opts?.provider) {
14518
- const providerInputs = opts.provider.split(",").map((s) => s.trim()).filter(Boolean);
14519
- const resolved = providerInputs.flatMap((p) => resolveProviderInput(p));
14520
- baseBody.providers = resolved.length > 0 ? resolved : providerInputs;
14521
- }
14522
- if (opts?.allLocations) {
14523
- baseBody.allLocations = true;
14524
- }
14525
- if (opts?.noLocation) {
14526
- baseBody.noLocation = true;
14527
- }
14528
- if (opts?.dispatchMode) {
14529
- baseBody.dispatchMode = opts.dispatchMode;
14530
- }
14531
- if (opts?.force) {
14532
- baseBody.force = true;
14533
- }
14534
- const results = [];
14535
- for (const p of projects2) {
14536
- const body = { ...baseBody };
14537
- if (body.allLocations && p.locations.length === 0) {
14538
- delete body.allLocations;
14539
- }
14540
- try {
14541
- const response = await client.triggerRun(p.name, body);
14542
- const dispatched = Array.isArray(response) ? response : [response];
14543
- for (const r of dispatched) {
14544
- results.push({
14545
- project: p.name,
14546
- runId: r.id,
14547
- status: r.status,
14548
- location: r.location ?? null
14549
- });
14550
- }
14551
- } catch (err) {
14552
- const msg = describeError(err);
14553
- results.push({ project: p.name, runId: "", status: "error", location: null, error: msg });
14554
- }
14555
- }
14556
- const errors = /* @__PURE__ */ new Map();
14557
- const batchWaits = /* @__PURE__ */ new Map();
14558
- if (opts?.wait) {
14559
- const pending = results.filter((r) => r.runId && !TERMINAL_STATUSES.has(r.status));
14560
- if (pending.length > 0) {
14561
- const showProgress = !isMachineFormat(opts?.format);
14562
- if (showProgress) process.stderr.write(`Waiting for ${pending.length} run(s)`);
14563
- await Promise.all(pending.map(async (r) => {
14564
- const final = await pollRun2(client, r.runId, showProgress);
14565
- r.status = final.status;
14566
- errors.set(r.runId, final.error);
14567
- const waiting = batchWaitLine(final);
14568
- if (waiting) batchWaits.set(r.runId, waiting);
14569
- }));
14570
- if (showProgress) process.stderr.write("\n");
14571
- }
14572
- }
14573
- const waited = results.filter((r) => r.runId).map((r) => ({ runId: r.runId, status: r.status, project: r.project, location: r.location, error: errors.get(r.runId) }));
14574
- if (isMachineFormat(opts?.format)) {
14575
- console.log(JSON.stringify(results, null, 2));
14576
- if (opts?.wait) throwIfWaitedRunFailed(waited);
14542
+ if (result.locations.length === 0) {
14543
+ console.log(`No locations configured for "${project2}".`);
14577
14544
  return;
14578
14545
  }
14579
- const showLocationColumn = results.some((r) => r.location !== null);
14580
- const projectCount = new Set(results.map((r) => r.project)).size;
14581
- console.log(`Triggered ${results.length} run(s) across ${projectCount} project(s):
14546
+ console.log(`Locations for "${project2}" (${result.locations.length}):
14582
14547
  `);
14583
- if (showLocationColumn) {
14584
- console.log(" PROJECT LOCATION RUN ID STATUS");
14585
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
14586
- for (const r of results) {
14587
- const proj = r.project.padEnd(31);
14588
- const loc = (r.location ?? "\u2014").padEnd(15);
14589
- const id = (r.runId || "(failed)").padEnd(36);
14590
- console.log(` ${proj} ${loc} ${id} ${r.status}`);
14591
- }
14592
- } else {
14593
- console.log(" PROJECT RUN ID STATUS");
14594
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
14595
- for (const r of results) {
14596
- const proj = r.project.padEnd(31);
14597
- const id = (r.runId || "(failed)").padEnd(36);
14598
- console.log(` ${proj} ${id} ${r.status}`);
14599
- }
14548
+ console.log(" LABEL CITY REGION COUNTRY DEFAULT");
14549
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500");
14550
+ for (const loc of result.locations) {
14551
+ const isDefault = loc.label === result.defaultLocation ? " *" : "";
14552
+ console.log(
14553
+ ` ${loc.label.padEnd(15)} ${loc.city.padEnd(19)} ${loc.region.padEnd(19)} ${loc.country.padEnd(7)}${isDefault}`
14554
+ );
14600
14555
  }
14601
- if (batchWaits.size > 0) console.log("");
14602
- for (const r of results) {
14603
- const waiting = batchWaits.get(r.runId);
14604
- if (waiting) console.log(` ${r.location ? `${r.project} (${r.location})` : r.project}: ${waiting}`);
14556
+ if (result.defaultLocation) {
14557
+ console.log(`
14558
+ Default: ${result.defaultLocation}`);
14605
14559
  }
14606
- if (opts?.wait) throwIfWaitedRunFailed(waited);
14607
14560
  }
14608
- async function cancelRun(project2, runId, format) {
14561
+ async function removeLocation(project2, label, format) {
14609
14562
  const client = getClient23();
14610
- let targetId = runId;
14611
- if (!targetId) {
14612
- const runs2 = await client.listRuns(project2);
14613
- const activeRuns = runs2.filter((r) => r.status === "queued" || r.status === "running");
14614
- if (activeRuns.length > 1) {
14615
- const candidates = activeRuns.map(({ id, kind, status }) => ({ id, kind, status }));
14616
- throw new CliError({
14617
- code: "MULTIPLE_ACTIVE_RUNS",
14618
- message: `Multiple active runs found for project "${project2}". Specify a run ID.`,
14619
- displayMessage: `Error: Multiple active runs found for project "${project2}". Specify a run ID.
14620
- ` + candidates.map((r) => ` ${r.id} ${r.kind} ${r.status}`).join("\n") + `
14621
- To cancel by ID: canonry run cancel ${project2} <run-id>`,
14622
- details: {
14623
- project: project2,
14624
- activeRuns: candidates,
14625
- suggestedCommands: [`canonry run cancel ${project2} <run-id>`]
14626
- }
14627
- });
14628
- }
14629
- const active = activeRuns[0];
14630
- if (!active) {
14631
- throw new CliError({
14632
- code: "NO_ACTIVE_RUN",
14633
- message: `No active run found for project "${project2}"`,
14634
- displayMessage: `Error: canonry run cancel "${project2}" \u2014 no active run found (status must be queued or running).
14635
- Check run status : canonry status ${project2}
14636
- To cancel by ID : canonry run cancel ${project2} <run-id>`,
14637
- details: {
14638
- project: project2,
14639
- allowedStatuses: ["queued", "running"],
14640
- suggestedCommands: [
14641
- `canonry status ${project2}`,
14642
- `canonry run cancel ${project2} <run-id>`
14643
- ]
14644
- }
14645
- });
14646
- }
14647
- targetId = active.id;
14648
- }
14649
- const result = await client.cancelRun(targetId);
14563
+ await client.removeLocation(project2, label);
14650
14564
  if (isMachineFormat(format)) {
14651
- console.log(JSON.stringify(result, null, 2));
14565
+ console.log(JSON.stringify({ project: project2, label, removed: true }, null, 2));
14652
14566
  return;
14653
14567
  }
14654
- console.log(`Run ${result.id} cancelled.`);
14568
+ console.log(`Location removed: ${label}`);
14655
14569
  }
14656
- async function showRun(id, format) {
14570
+ async function setDefaultLocation(project2, label, format) {
14657
14571
  const client = getClient23();
14658
- const run = await client.getRun(id);
14572
+ const result = await client.setDefaultLocation(project2, label);
14659
14573
  if (isMachineFormat(format)) {
14660
- console.log(JSON.stringify(run, null, 2));
14661
- return;
14662
- }
14663
- printRunDetail(run);
14664
- }
14665
- async function listRuns(project2, opts) {
14666
- const client = getClient23();
14667
- const runs2 = await client.listRuns(project2, opts?.limit, opts?.kind, opts?.status, opts?.excludeKind, opts?.excludeKinds);
14668
- if (opts?.format === "json") {
14669
- console.log(JSON.stringify(runs2, null, 2));
14670
- return;
14671
- } else if (opts?.format === "jsonl") {
14672
- emitJsonl(runs2.map((run) => ({ project: project2, ...run })));
14673
- return;
14674
- }
14675
- if (runs2.length === 0) {
14676
- console.log(`No runs found for "${project2}".`);
14574
+ console.log(JSON.stringify({ project: project2, ...result }, null, 2));
14677
14575
  return;
14678
14576
  }
14679
- console.log(`Runs for "${project2}" (${runs2.length}):
14680
- `);
14681
- console.log(" ID STATUS KIND TRIGGER CREATED");
14682
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
14683
- for (const run of runs2) {
14684
- console.log(
14685
- ` ${run.id} ${run.status.padEnd(10)} ${run.kind.padEnd(18)} ${run.trigger.padEnd(9)} ${run.createdAt}`
14686
- );
14687
- }
14577
+ console.log(`Default location set to: ${label}`);
14688
14578
  }
14689
- var POLL_TIMEOUT_MS3 = 10 * 60 * 1e3;
14690
- async function pollRun2(client, runId, showProgress = true) {
14691
- const deadline = Date.now() + POLL_TIMEOUT_MS3;
14692
- for (; ; ) {
14693
- await new Promise((r) => setTimeout(r, 2e3));
14694
- if (Date.now() > deadline) {
14695
- throw new Error(`Timed out waiting for run ${runId} after ${POLL_TIMEOUT_MS3 / 1e3}s`);
14579
+
14580
+ // src/cli-commands/project.ts
14581
+ var PROJECT_CLI_COMMANDS = [
14582
+ {
14583
+ path: ["project", "create"],
14584
+ usage: "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--dispatch-mode provider=sync|batch...] [--site-audit-max-pages <1-50000|full>] [--format json]",
14585
+ help: "Create a project. Pass --domain for the public site to scan. Provider credentials are not required for Page Health.",
14586
+ options: {
14587
+ domain: { type: "string", short: "d" },
14588
+ "owned-domain": multiStringOption(),
14589
+ alias: multiStringOption(),
14590
+ "qualified-alias": multiStringOption(),
14591
+ country: stringOption(),
14592
+ language: stringOption(),
14593
+ "display-name": stringOption(),
14594
+ provider: multiStringOption(),
14595
+ "provider-model": multiStringOption(),
14596
+ "dispatch-mode": multiStringOption(),
14597
+ "site-audit-max-pages": stringOption()
14598
+ },
14599
+ run: async (input) => {
14600
+ const name = requireProject(
14601
+ input,
14602
+ "project.create",
14603
+ "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--dispatch-mode provider=sync|batch...] [--site-audit-max-pages <1-50000|full>] [--format json]"
14604
+ );
14605
+ await createProject(name, {
14606
+ domain: getString(input.values, "domain") ?? name,
14607
+ ownedDomains: getStringArray(input.values, "owned-domain") ?? [],
14608
+ aliases: getStringArray(input.values, "alias") ?? [],
14609
+ qualifiedAliases: getStringArray(input.values, "qualified-alias") ?? [],
14610
+ country: getString(input.values, "country") ?? "US",
14611
+ language: getString(input.values, "language") ?? "en",
14612
+ displayName: getString(input.values, "display-name") ?? name,
14613
+ providers: getStringArray(input.values, "provider") ?? [],
14614
+ providerModels: parseProviderModelAssignments(getStringArray(input.values, "provider-model")),
14615
+ providerDispatchModes: parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode")),
14616
+ siteAuditMaxPages: parseSiteAuditMaxPages(getString(input.values, "site-audit-max-pages"), "project.create"),
14617
+ format: input.format
14618
+ });
14696
14619
  }
14697
- const run = await client.getRun(runId);
14698
- if (showProgress) process.stderr.write(".");
14699
- if (TERMINAL_STATUSES.has(run.status) || outstandingBatches(run).length > 0) {
14700
- return run;
14620
+ },
14621
+ {
14622
+ path: ["project", "update"],
14623
+ usage: "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--add-qualified-alias <name>...] [--remove-qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--all-providers] [--provider-model provider=model...] [--clear-provider-model <provider>...] [--dispatch-mode provider=sync|batch...] [--clear-dispatch-mode <provider>...] [--negative-review-max-stars <1-4|default>] [--site-audit-max-pages <1-50000|full>] [--competitor-auto-aliases <off|preview|apply>] [--format json]",
14624
+ options: {
14625
+ domain: { type: "string", short: "d" },
14626
+ "owned-domain": multiStringOption(),
14627
+ "add-domain": multiStringOption(),
14628
+ "remove-domain": multiStringOption(),
14629
+ alias: multiStringOption(),
14630
+ "add-alias": multiStringOption(),
14631
+ "remove-alias": multiStringOption(),
14632
+ "add-qualified-alias": multiStringOption(),
14633
+ "remove-qualified-alias": multiStringOption(),
14634
+ country: stringOption(),
14635
+ language: stringOption(),
14636
+ "display-name": stringOption(),
14637
+ provider: multiStringOption(),
14638
+ "all-providers": { type: "boolean" },
14639
+ "provider-model": multiStringOption(),
14640
+ "clear-provider-model": multiStringOption(),
14641
+ "dispatch-mode": multiStringOption(),
14642
+ "clear-dispatch-mode": multiStringOption(),
14643
+ "negative-review-max-stars": stringOption(),
14644
+ "site-audit-max-pages": stringOption(),
14645
+ "competitor-auto-aliases": stringOption()
14646
+ },
14647
+ run: async (input) => {
14648
+ const name = requireProject(
14649
+ input,
14650
+ "project.update",
14651
+ "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--add-qualified-alias <name>...] [--remove-qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--all-providers] [--provider-model provider=model...] [--clear-provider-model <provider>...] [--dispatch-mode provider=sync|batch...] [--clear-dispatch-mode <provider>...] [--negative-review-max-stars <1-4|default>] [--site-audit-max-pages <1-50000|full>] [--competitor-auto-aliases <off|preview|apply>] [--format json]"
14652
+ );
14653
+ const providers = getStringArray(input.values, "provider");
14654
+ const allProviders = getBoolean(input.values, "all-providers");
14655
+ if (allProviders && providers?.length) throw usageError("Error: --all-providers conflicts with --provider");
14656
+ const providerModels = parseProviderModelAssignments(getStringArray(input.values, "provider-model"));
14657
+ const clearProviderModels = getStringArray(input.values, "clear-provider-model") ?? [];
14658
+ for (const provider of clearProviderModels) {
14659
+ if (provider in providerModels) throw usageError(`Error: --provider-model and --clear-provider-model conflict for ${provider}`);
14660
+ }
14661
+ const dispatchModes = parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode"));
14662
+ const clearDispatchModes = getStringArray(input.values, "clear-dispatch-mode") ?? [];
14663
+ for (const provider of clearDispatchModes) {
14664
+ if (provider in dispatchModes) throw usageError(`Error: --dispatch-mode and --clear-dispatch-mode conflict for ${provider}`);
14665
+ }
14666
+ await updateProjectSettings(name, {
14667
+ displayName: getString(input.values, "display-name"),
14668
+ domain: getString(input.values, "domain"),
14669
+ ownedDomains: getStringArray(input.values, "owned-domain"),
14670
+ addOwnedDomain: getStringArray(input.values, "add-domain"),
14671
+ removeOwnedDomain: getStringArray(input.values, "remove-domain"),
14672
+ aliases: getStringArray(input.values, "alias"),
14673
+ addAlias: getStringArray(input.values, "add-alias"),
14674
+ removeAlias: getStringArray(input.values, "remove-alias"),
14675
+ addQualifiedAlias: getStringArray(input.values, "add-qualified-alias"),
14676
+ removeQualifiedAlias: getStringArray(input.values, "remove-qualified-alias"),
14677
+ country: getString(input.values, "country"),
14678
+ language: getString(input.values, "language"),
14679
+ providers: allProviders ? [] : providers,
14680
+ providerModels,
14681
+ clearProviderModels,
14682
+ dispatchModes,
14683
+ clearDispatchModes,
14684
+ negativeReviewMaxStars: parseNegativeReviewMaxStars(getString(input.values, "negative-review-max-stars")),
14685
+ siteAuditMaxPages: parseSiteAuditMaxPages(getString(input.values, "site-audit-max-pages"), "project.update"),
14686
+ competitorAutoAliases: parseCompetitorAutoAliasMode(getString(input.values, "competitor-auto-aliases")),
14687
+ format: input.format
14688
+ });
14701
14689
  }
14702
- }
14703
- }
14704
- function outstandingBatches(run) {
14705
- if (TERMINAL_STATUSES.has(run.status)) return [];
14706
- return (run.providerBatches ?? []).filter((batch) => OUTSTANDING_PROVIDER_BATCH_STATUSES.includes(batch.status));
14707
- }
14708
- function batchWaitLine(run) {
14709
- const outstanding = outstandingBatches(run);
14710
- if (outstanding.length === 0) return null;
14711
- const batches = outstanding.map((batch) => `${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`);
14712
- return `Waiting on provider batch(es): ${batches.join("; ")}; check with canonry run show ${run.id}`;
14713
- }
14714
- function printRunDetail(run) {
14715
- console.log(`Run: ${run.id}`);
14716
- console.log(` Status: ${run.status}`);
14717
- console.log(` Kind: ${run.kind}`);
14718
- if (run.trigger) console.log(` Trigger: ${run.trigger}`);
14719
- if (run.startedAt) console.log(` Started: ${run.startedAt}`);
14720
- if (run.finishedAt) console.log(` Finished: ${run.finishedAt}`);
14721
- if (run.createdAt) console.log(` Created: ${run.createdAt}`);
14722
- if (run.error) {
14723
- if (run.error.message) console.log(` Error: ${run.error.message}`);
14724
- if (run.error.providers) {
14725
- for (const [provider, detail] of Object.entries(run.error.providers)) {
14726
- console.log(` Error (${provider}): ${detail.message}`);
14690
+ },
14691
+ {
14692
+ path: ["project", "list"],
14693
+ usage: "canonry project list [--format json]",
14694
+ allowPositionals: false,
14695
+ run: async (input) => {
14696
+ await listProjects(input.format);
14697
+ }
14698
+ },
14699
+ {
14700
+ path: ["project", "show"],
14701
+ usage: "canonry project show <name> [--format json]",
14702
+ run: async (input) => {
14703
+ const name = requireProject(input, "project.show", "canonry project show <name>");
14704
+ await showProject(name, input.format);
14705
+ }
14706
+ },
14707
+ {
14708
+ path: ["project", "delete"],
14709
+ usage: "canonry project delete <name> [--dry-run] [--format json]",
14710
+ supportsDryRun: true,
14711
+ run: async (input) => {
14712
+ const name = requireProject(input, "project.delete", "canonry project delete <name> [--dry-run] [--format json]");
14713
+ await deleteProject(name, { dryRun: input.dryRun, format: input.format });
14714
+ }
14715
+ },
14716
+ {
14717
+ path: ["project", "add-location"],
14718
+ usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]",
14719
+ options: {
14720
+ label: stringOption(),
14721
+ city: stringOption(),
14722
+ region: stringOption(),
14723
+ country: stringOption(),
14724
+ timezone: stringOption()
14725
+ },
14726
+ run: async (input) => {
14727
+ const name = requireProject(
14728
+ input,
14729
+ "project.add-location",
14730
+ "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]"
14731
+ );
14732
+ const label = getString(input.values, "label");
14733
+ const city = getString(input.values, "city");
14734
+ const region = getString(input.values, "region");
14735
+ const country = getString(input.values, "country");
14736
+ if (!label || !city || !region || !country) {
14737
+ throw usageError("Error: --label, --city, --region, and --country are all required", {
14738
+ message: "location label, city, region, and country are required",
14739
+ details: {
14740
+ command: "project.add-location",
14741
+ usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]",
14742
+ required: ["label", "city", "region", "country"]
14743
+ }
14744
+ });
14727
14745
  }
14746
+ await addLocation(name, {
14747
+ label,
14748
+ city,
14749
+ region,
14750
+ country,
14751
+ timezone: getString(input.values, "timezone"),
14752
+ format: input.format
14753
+ });
14754
+ }
14755
+ },
14756
+ {
14757
+ path: ["project", "locations"],
14758
+ usage: "canonry project locations <name> [--format json]",
14759
+ run: async (input) => {
14760
+ const name = requireProject(input, "project.locations", "canonry project locations <name> [--format json]");
14761
+ await listLocations(name, input.format);
14762
+ }
14763
+ },
14764
+ {
14765
+ path: ["project", "remove-location"],
14766
+ usage: "canonry project remove-location <name> <label> [--format json]",
14767
+ run: async (input) => {
14768
+ const name = requireProject(input, "project.remove-location", "canonry project remove-location <name> <label> [--format json]");
14769
+ const label = requirePositional(input, 1, {
14770
+ command: "project.remove-location",
14771
+ usage: "canonry project remove-location <name> <label> [--format json]",
14772
+ message: "project name and location label are required"
14773
+ });
14774
+ await removeLocation(name, label, input.format);
14775
+ }
14776
+ },
14777
+ {
14778
+ path: ["project", "set-default-location"],
14779
+ usage: "canonry project set-default-location <name> <label> [--format json]",
14780
+ run: async (input) => {
14781
+ const name = requireProject(input, "project.set-default-location", "canonry project set-default-location <name> <label> [--format json]");
14782
+ const label = requirePositional(input, 1, {
14783
+ command: "project.set-default-location",
14784
+ usage: "canonry project set-default-location <name> <label> [--format json]",
14785
+ message: "project name and location label are required"
14786
+ });
14787
+ await setDefaultLocation(name, label, input.format);
14788
+ }
14789
+ },
14790
+ {
14791
+ path: ["project"],
14792
+ usage: "canonry project <create|update|list|show|delete|add-location|locations|remove-location|set-default-location> [args]",
14793
+ run: async (input) => {
14794
+ unknownSubcommand(input.positionals[0], {
14795
+ command: "project",
14796
+ usage: "canonry project <create|update|list|show|delete|add-location|locations|remove-location|set-default-location> [args]",
14797
+ available: ["create", "update", "list", "show", "delete", "add-location", "locations", "remove-location", "set-default-location"]
14798
+ });
14728
14799
  }
14729
14800
  }
14730
- const batched = Object.keys(run.dispatchModes ?? {}).sort();
14731
- if (batched.length > 0) console.log(` Dispatch: ${batched.map((provider) => `${provider}=batch`).join(", ")} (other providers sync)`);
14732
- if (run.providerBatches && run.providerBatches.length > 0) {
14733
- console.log("\n Provider batches:");
14734
- for (const batch of run.providerBatches) console.log(` ${providerBatchLine(batch)}`);
14801
+ ];
14802
+ function parseDispatchModeAssignments(assignments) {
14803
+ const result = {};
14804
+ for (const assignment of assignments ?? []) {
14805
+ const separator = assignment.indexOf("=");
14806
+ const provider = separator === -1 ? "" : assignment.slice(0, separator).trim();
14807
+ const mode = providerDispatchModeSchema.safeParse(separator === -1 ? "" : assignment.slice(separator + 1).trim().toLowerCase());
14808
+ if (!provider || !mode.success) throw usageError(`Error: --dispatch-mode must use provider=sync or provider=batch (received ${assignment})`);
14809
+ if (provider in result) throw usageError(`Error: duplicate --dispatch-mode assignment for ${provider}`);
14810
+ result[provider] = mode.data;
14735
14811
  }
14736
- if (run.usage && run.usage.length > 0) printUsageTable(run.usage);
14737
- if (run.snapshots && run.snapshots.length > 0) {
14738
- console.log(`
14739
- Snapshots: ${run.snapshots.length} (cell = [citation][mention]; C=cited c=not, M=mentioned m=not, \u2013=no data)`);
14740
- for (const s of run.snapshots) {
14741
- const citationGlyph = s.citationState === CitationStates.cited ? "C" : "c";
14742
- const mentionGlyph = typeof s.answerMentioned === "boolean" ? s.answerMentioned ? "M" : "m" : "\u2013";
14743
- const modelLabel = s.model ? ` (${s.model})` : "";
14744
- console.log(` [${citationGlyph}${mentionGlyph}] ${s.provider}${modelLabel} ${s.query}`);
14745
- }
14812
+ return result;
14813
+ }
14814
+ function parseProviderModelAssignments(assignments) {
14815
+ const result = {};
14816
+ for (const assignment of assignments ?? []) {
14817
+ const separator = assignment.indexOf("=");
14818
+ const provider = separator === -1 ? "" : assignment.slice(0, separator).trim();
14819
+ const model = separator === -1 ? "" : assignment.slice(separator + 1).trim();
14820
+ if (!provider || !model) throw usageError(`Error: --provider-model must use provider=model (received ${assignment})`);
14821
+ if (provider in result) throw usageError(`Error: duplicate --provider-model assignment for ${provider}`);
14822
+ result[provider] = model;
14746
14823
  }
14824
+ return result;
14747
14825
  }
14748
- function providerBatchLine(batch) {
14749
- const recorded = `${batch.recordedCount} of ${batch.requestCount} answers recorded`;
14750
- const withError = (text) => batch.error ? `${text} (${batch.error})` : text;
14751
- switch (batch.status) {
14752
- case ProviderBatchStatuses.submitting:
14753
- return `submitting provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests`;
14754
- // The two outstanding statuses: the run stays `running` until they clear.
14755
- case ProviderBatchStatuses.submitted:
14756
- case ProviderBatchStatuses.ended:
14757
- return `waiting on provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`;
14758
- case ProviderBatchStatuses.ingested:
14759
- return withError(`provider batch ${batch.provider}: ingested \u2014 ${recorded}`);
14760
- case ProviderBatchStatuses.cancelled:
14761
- return withError(`provider batch ${batch.provider}: cancelled \u2014 ${recorded}; the rest are missing and can be filled`);
14762
- case ProviderBatchStatuses.failed:
14763
- return withError(`provider batch ${batch.provider}: rejected by the provider \u2014 its ${batch.requestCount} answers ran sync instead`);
14764
- case ProviderBatchStatuses.unknown:
14765
- return withError(`provider batch ${batch.provider}: submit outcome unknown \u2014 never resubmitted; its ${batch.requestCount} answers stay missing`);
14826
+ function parseNegativeReviewMaxStars(raw) {
14827
+ if (raw === void 0) return void 0;
14828
+ if (raw === "default") return null;
14829
+ const parsed = gbpNegativeReviewMaxStarsSchema.safeParse(Number(raw));
14830
+ if (!parsed.success) {
14831
+ throw usageError('Error: --negative-review-max-stars must be 1, 2, 3, 4, or "default" (3)', {
14832
+ message: '--negative-review-max-stars must be 1-4 or "default"',
14833
+ details: { command: "project.update", value: raw }
14834
+ });
14766
14835
  }
14836
+ return parsed.data;
14767
14837
  }
14768
- function printUsageTable(rows) {
14769
- const cost = (row) => row.estimatedCostMicros === null ? "unpriced" : `${formatMicros(row.estimatedCostMicros, "USD", { fractionDigits: 4, showTinyAsLessThan: true })}${row.unpricedAnswers > 0 ? ` (+${row.unpricedAnswers} unpriced)` : ""}`;
14770
- const table = rows.map((row) => [
14771
- row.provider,
14772
- row.pricingTier,
14773
- row.answers.toLocaleString("en-US"),
14774
- row.inputTokens.toLocaleString("en-US"),
14775
- row.cachedInputTokens.toLocaleString("en-US"),
14776
- row.cacheWriteTokens.toLocaleString("en-US"),
14777
- row.outputTokens.toLocaleString("en-US"),
14778
- row.searchCount.toLocaleString("en-US"),
14779
- cost(row)
14780
- ]);
14781
- const header = ["PROVIDER", "TIER", "ANSWERS", "INPUT", "CACHED", "CACHE WRITE", "OUTPUT", "SEARCHES", "EST. COST"];
14782
- const widths = header.map((title, index) => Math.max(title.length, ...table.map((cells) => cells[index].length)));
14783
- const line = (cells) => ` ${cells.map((cell2, index) => cell2.padEnd(widths[index])).join(" ").trimEnd()}`;
14784
- console.log("\n Usage (answers with recorded usage; cost is an estimate):");
14785
- console.log(line(header));
14786
- for (const cells of table) console.log(line(cells));
14838
+ function parseSiteAuditMaxPages(raw, command) {
14839
+ if (raw === void 0) return void 0;
14840
+ const value = raw.trim();
14841
+ if (value.toLowerCase() === "full") return null;
14842
+ const parsed = /^\d+$/.test(value) ? siteAuditPageBudgetSchema.safeParse(Number(value)) : void 0;
14843
+ if (!parsed?.success) {
14844
+ throw usageError(`Error: --site-audit-max-pages must be a whole number of pages from 1 to ${SITE_AUDIT_MAX_PAGE_LIMIT}, or "full" for the full site`, {
14845
+ message: `--site-audit-max-pages must be 1-${SITE_AUDIT_MAX_PAGE_LIMIT} or "full"`,
14846
+ details: { command, option: "site-audit-max-pages", value: raw }
14847
+ });
14848
+ }
14849
+ return parsed.data;
14787
14850
  }
14788
- var FILL_POLL_INTERVAL_MS = 3e3;
14789
- var FILL_POLL_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
14790
- function missingSummary(completeness) {
14791
- const parts = Object.entries(completeness.missingByProvider).sort(([left], [right]) => left.localeCompare(right)).map(([provider, count]) => `${provider} ${count}`);
14792
- return parts.length ? parts.join(", ") : "none";
14851
+ function parseCompetitorAutoAliasMode(raw) {
14852
+ if (raw === void 0) return void 0;
14853
+ const parsed = competitorAutoAliasModeSchema.safeParse(raw.trim().toLowerCase());
14854
+ if (!parsed.success) {
14855
+ throw usageError("Error: --competitor-auto-aliases must be off, preview or apply", {
14856
+ message: "--competitor-auto-aliases must be off, preview or apply",
14857
+ details: { command: "project.update", option: "competitor-auto-aliases", value: raw }
14858
+ });
14859
+ }
14860
+ return parsed.data;
14793
14861
  }
14794
- function printCompleteness(completeness) {
14795
- console.log(`Run: ${completeness.runId}`);
14796
- console.log(` Status: ${completeness.status}`);
14797
- if (!completeness.planned) {
14798
- console.log(" Not a measurement-plan run: there are no expected answers to count.");
14862
+
14863
+ // src/commands/organic-evidence.ts
14864
+ async function showOrganicEvidence(project2, opts) {
14865
+ const data = await createApiClient().getOrganicEvidence(project2, opts.period);
14866
+ if (isMachineFormat(opts.format)) {
14867
+ console.log(JSON.stringify(data, null, 2));
14799
14868
  return;
14800
14869
  }
14801
- if (!completeness.readable) {
14802
- console.log(" Answers: unknown (the run's manifest cannot be read)");
14803
- } else {
14804
- console.log(` Answers: ${completeness.executed}/${completeness.expected}`);
14805
- console.log(` Missing: ${completeness.missing} (${missingSummary(completeness)})`);
14870
+ printOrganicEvidence(project2, data);
14871
+ }
14872
+ function formatCounts(clicks, impressions) {
14873
+ return `${clicks} clicks / ${impressions} impressions`;
14874
+ }
14875
+ function printOrganicEvidence(project2, data) {
14876
+ const asOf = data.asOfDate ? ` through ${data.asOfDate}` : "";
14877
+ console.log(`Organic evidence: ${project2} (${data.periodDays} days${asOf})`);
14878
+ console.log(`Coverage: GSC ${data.coverage.gsc ? "yes" : "no"} \xB7 GA4 ${data.coverage.ga4 ? "yes" : "no"} \xB7 server ${data.coverage.server ? "yes" : "no"} \xB7 AI visibility ${data.coverage.visibility ? "yes" : "no"}`);
14879
+ if (data.gsc) {
14880
+ console.log(`Google Search: ${formatCounts(data.gsc.propertyTotals.clicks, data.gsc.propertyTotals.impressions)} (named non-brand: ${formatCounts(data.gsc.namedNonBrand.clicks, data.gsc.namedNonBrand.impressions)})`);
14806
14881
  }
14807
- if (completeness.refusal) console.log(` Fillable: no. ${completeness.refusal.message}`);
14808
- else console.log(` Fillable: ${completeness.fillable ? "yes" : "not right now (a sweep or another fill is running)"}`);
14809
- const fill = completeness.latestFill;
14810
- if (fill) {
14811
- console.log(` Last fill: ${fill.id} ${fill.status}, ${fill.filled}/${fill.expected} recorded${fill.error ? `. ${fill.error}` : ""}`);
14882
+ if (data.ga4) console.log(`GA4 organic: ${data.ga4.organicSessions} sessions`);
14883
+ if (data.gaAiReferrals) console.log(`GA4 AI referrals: ${data.gaAiReferrals.organicSessions} organic, ${data.gaAiReferrals.paidSessions} paid sessions`);
14884
+ if (data.server) {
14885
+ const s = data.server;
14886
+ const fetchTotal = s.userFetchHits.verified + s.userFetchHits.claimedUnverified + s.userFetchHits.unknownAiLike;
14887
+ console.log(`Server AI: ${s.crawlerHits.verified} verified + ${s.crawlerHits.claimedUnverified} claimed + ${s.crawlerHits.unknownAiLike} heuristic crawls; ${fetchTotal} user-agent fetches (${s.userFetchHits.verified} verified, ${s.userFetchHits.claimedUnverified} claimed, ${s.userFetchHits.unknownAiLike} heuristic); ${s.referralSessions.organic} organic referrals, ${s.referralSessions.paid} paid, ${s.referralSessions.unknown} unclassified`);
14812
14888
  }
14813
- }
14814
- async function showRunCompleteness(runId, format) {
14815
- const completeness = await getClient23().getRunCompleteness(runId);
14816
- if (isMachineFormat(format)) {
14817
- console.log(JSON.stringify(completeness, null, 2));
14818
- return;
14889
+ if (data.visibility) {
14890
+ const v = data.visibility;
14891
+ console.log(`Latest AI visibility: ${v.mentionedPairs}/${v.answerPairs} mentioned, ${v.citedPairs}/${v.answerPairs} cited (${Math.floor(v.ageDays)}d old)`);
14819
14892
  }
14820
- printCompleteness(completeness);
14821
- }
14822
- async function fillRun(runId, opts = {}) {
14823
- const client = getClient23();
14824
- const response = await client.fillRun(runId, {
14825
- ...opts.providers?.length ? { providers: opts.providers } : {},
14826
- ...opts.dryRun ? { dryRun: true } : {}
14827
- });
14828
- if (!opts.wait || response.outcome !== "queued" || !response.fill) {
14829
- if (isMachineFormat(opts.format)) {
14830
- console.log(JSON.stringify(response, null, 2));
14831
- return;
14832
- }
14833
- if (response.outcome === "already-complete") console.log(`Run ${runId} is already complete: nothing to fill.`);
14834
- else if (response.outcome === "dry-run") console.log("Dry run: nothing was queued.");
14835
- else if (response.fill) console.log(`Fill ${response.fill.id} queued: ${response.fill.expected} missing answer(s) (${missingSummary(response.completeness)}).`);
14836
- printCompleteness(response.completeness);
14837
- return;
14893
+ if (data.findings.length > 0) {
14894
+ console.log("\nFindings:");
14895
+ for (const finding of data.findings) console.log(`- ${finding.title}: ${finding.detail}`);
14838
14896
  }
14839
- const fillId = response.fill.id;
14840
- const deadline = Date.now() + FILL_POLL_TIMEOUT_MS;
14841
- let completeness = response.completeness;
14842
- if (!isMachineFormat(opts.format)) {
14843
- process.stderr.write(`Filling ${response.fill.expected} missing answer(s) in run ${runId}`);
14897
+ if (data.limitations.length > 0) {
14898
+ console.log("\nLimitations:");
14899
+ for (const limitation of data.limitations) console.log(`- ${limitation.detail}`);
14844
14900
  }
14845
- for (; ; ) {
14846
- await new Promise((resolve) => setTimeout(resolve, FILL_POLL_INTERVAL_MS));
14847
- if (Date.now() > deadline) {
14848
- throw new CliError({
14849
- code: "RUN_FILL_INCOMPLETE",
14850
- message: `Timed out waiting for fill ${fillId}; it may still be running. Check with: canonry run completeness ${runId}`,
14851
- exitCode: EXIT_SYSTEM_ERROR,
14852
- details: { runId, fillId }
14901
+ }
14902
+
14903
+ // src/cli-commands/organic-evidence.ts
14904
+ var USAGE3 = "canonry organic-evidence <project> [--period 60|90] [--format json|jsonl]";
14905
+ function parsePeriod(value) {
14906
+ if (value === void 0) return void 0;
14907
+ if (value === "60" || value === "90") return Number(value);
14908
+ throw usageError(`Error: --period must be 60 or 90
14909
+
14910
+ Usage: ${USAGE3}`);
14911
+ }
14912
+ var ORGANIC_EVIDENCE_CLI_COMMANDS = [
14913
+ {
14914
+ path: ["organic-evidence"],
14915
+ usage: USAGE3,
14916
+ options: { period: stringOption() },
14917
+ run: async (input) => {
14918
+ const project2 = requireProject(input, "organic-evidence", USAGE3);
14919
+ await showOrganicEvidence(project2, {
14920
+ period: parsePeriod(getString(input.values, "period")),
14921
+ format: input.format
14853
14922
  });
14854
14923
  }
14855
- completeness = await client.getRunCompleteness(runId);
14856
- if (!isMachineFormat(opts.format)) process.stderr.write(".");
14857
- const latest = completeness.latestFill;
14858
- const working = latest !== null && (latest.status === "queued" || latest.status === "running");
14859
- if (completeness.status === "completed" || !working) break;
14860
- }
14861
- if (!isMachineFormat(opts.format)) process.stderr.write("\n");
14862
- if (isMachineFormat(opts.format)) console.log(JSON.stringify(completeness, null, 2));
14863
- else printCompleteness(completeness);
14864
- if (completeness.status !== "completed") {
14865
- throw new CliError({
14866
- code: "RUN_FILL_INCOMPLETE",
14867
- message: `Run ${runId} is still ${completeness.status}: ${completeness.missing} answer(s) missing (${missingSummary(completeness)}).`,
14868
- exitCode: EXIT_SYSTEM_ERROR,
14869
- details: { runId, fillId, missing: completeness.missing, missingByProvider: completeness.missingByProvider }
14870
- });
14871
14924
  }
14872
- }
14925
+ ];
14873
14926
 
14874
14927
  // src/cli-commands/run.ts
14875
14928
  var RUN_TRIGGER_USAGE = "canonry run trigger <project> [--group <key>]... [--target <key>]... [--provider <name>] [--query <q>...] [--location <label>] [--all-locations] [--no-location] [--probe] [--dispatch-mode sync|batch] [--force] [--wait] [--format json]";
@@ -16364,6 +16417,9 @@ function renderHuman2(overview) {
16364
16417
  } else {
16365
16418
  console.log("\n No runs yet.");
16366
16419
  }
16420
+ const admission = runAdmissionLines(meta.name, latestRun.admission, latestRun.manualAdmission);
16421
+ if (admission.length > 0) console.log(`
16422
+ ${admission.map((line) => ` ${line}`).join("\n")}`);
16367
16423
  console.log("\nScores:");
16368
16424
  printScore("Mention ", scores.mention);
16369
16425
  printScore("Visibility ", scores.visibility);