@siteoshq/cli 1.2.0 → 1.3.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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # SiteOS CLI
2
2
 
3
3
  `@siteoshq/cli` exposes one `siteos` binary for Auth, common Projects, Pulse, Cookie, Forms,
4
- Search, Trace and Integrations. This source prepares version 1.2.0; publishing is a separate release.
4
+ Search, Trace, SEO and Integrations. This source prepares version 1.3.0; publishing is a separate release.
5
5
  Node.js 22 or newer is required.
6
6
 
7
7
  ## Install and authenticate
@@ -69,7 +69,7 @@ siteos project environment connect forms --environment production --resource '<f
69
69
  Omit `--resource` to create a new local environment. `siteos project environment list` reports the
70
70
  common catalog and connections. Forms/Search/Trace operational `--environment` flags accept common
71
71
  slugs and resolve explicit native bindings; without the flag, commands use the selected environment.
72
- Pulse/Cookie use separate bound resources per environment. Missing bindings fail without a fallback.
72
+ Pulse/Cookie/SEO use separate bound resources per environment. Missing bindings fail without a fallback.
73
73
  Manage names and website URLs with `project update` and `project environment update <slug>`.
74
74
  Cookie/Trace published addresses remain unchanged until explicit republication.
75
75
 
@@ -136,6 +136,35 @@ snippet is unavailable until runtime publication succeeds. Integrations connecti
136
136
  Organization; provider authorization opens the shared browser flow, and notification destinations
137
137
  are connected explicitly by each service.
138
138
 
139
+ ### SEO (prepared for release)
140
+
141
+ Check `siteos seo --help` before use; install a matching release only after it is published. An
142
+ SEO-capable server returns SEO in its Project service catalog, which older strict CLI parsers may
143
+ reject. Coordinate the CLI update before that server rollout, then publish the matching plugin.
144
+
145
+ ```sh
146
+ siteos project connect seo --json
147
+ siteos seo status --environment production --json
148
+ siteos seo audit run --environment production --json
149
+ siteos seo audit list --json
150
+ siteos seo audit show '<audit-id>' --json
151
+ siteos seo pages --audit '<audit-id>' --json
152
+ siteos seo issues --audit '<audit-id>' --json
153
+ siteos seo changes --audit '<audit-id>' --state resolved --json
154
+ siteos seo recheck --audit '<audit-id>' --url https://example.com/pricing --json
155
+ ```
156
+
157
+ Setup performs no crawl. `audit run` queues a full HTML audit; `recheck` queues one previously
158
+ observed URL. Read the returned audit ID until a terminal state; a queued response is not a result.
159
+ `audit cancel '<audit-id>'` revokes further work. Pages and changes are paginated with `--page`.
160
+
161
+ `issue ignore` and `issue restore` require `--audit`, `--url`, `--rule`, `--reason` and the current
162
+ `--revision` (0 before the first decision). They use a distinct issue-write scope. Read back the
163
+ decision after success; a revision conflict requires refreshing it. Ignore never means resolved.
164
+ All SEO commands resolve the selected common Project and explicit environment binding. Current
165
+ Auth policy permits CLI service-grant issuance to Organization owners/admins. JSON uses contract
166
+ version 1 and does not include credentials. The focused plugin skill is `siteos-seo`.
167
+
139
168
  ## Local state and diagnostics
140
169
 
141
170
  ```sh
package/dist/cli.js CHANGED
@@ -483,7 +483,8 @@ var siteOSServiceAudienceSchema = z.enum([
483
483
  "siteos-cookie",
484
484
  "siteos-forms",
485
485
  "siteos-pulse",
486
- "siteos-search"
486
+ "siteos-search",
487
+ "siteos-seo"
487
488
  ]);
488
489
  var siteOSServiceScopeSchema = z.string().min(1).max(160).regex(/^[a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*){2,}$/);
489
490
  var scopeNamespaceByAudience = {
@@ -493,7 +494,8 @@ var scopeNamespaceByAudience = {
493
494
  "siteos-cookie": "cookie",
494
495
  "siteos-forms": "forms",
495
496
  "siteos-pulse": "pulse",
496
- "siteos-search": "search"
497
+ "siteos-search": "search",
498
+ "siteos-seo": "seo"
497
499
  };
498
500
  function matchingServiceScopes(value, context) {
499
501
  const prefix = `${scopeNamespaceByAudience[value.audience]}:`;
@@ -1442,7 +1444,8 @@ var PROJECT_SERVICES = [
1442
1444
  "cookie",
1443
1445
  "forms",
1444
1446
  "search",
1445
- "trace"
1447
+ "trace",
1448
+ "seo"
1446
1449
  ];
1447
1450
  var ProjectSchema = z4.object({
1448
1451
  id: z4.string().min(1),
@@ -1496,7 +1499,7 @@ var OverviewSchema = z4.object({
1496
1499
  });
1497
1500
  function createProjectApi(input) {
1498
1501
  const origin = resolveSiteOSAuthBaseUrl(input.env);
1499
- async function request(path29, schema, body, method) {
1502
+ async function request(path30, schema, body, method) {
1500
1503
  const scope = body === void 0 ? "projects:workspace:read" : "projects:workspace:write";
1501
1504
  const grant = await input.grants.acquire({
1502
1505
  audience: "siteos-projects",
@@ -1510,7 +1513,7 @@ function createProjectApi(input) {
1510
1513
  message: "SiteOS API access is unavailable."
1511
1514
  });
1512
1515
  const response = await input.fetchImpl(
1513
- `${origin}/api/projects/v1/projects${path29}`,
1516
+ `${origin}/api/projects/v1/projects${path30}`,
1514
1517
  {
1515
1518
  method: method ?? (body === void 0 ? "GET" : "POST"),
1516
1519
  headers: {
@@ -1706,14 +1709,14 @@ async function commonServiceContext(options, service, environmentSlug) {
1706
1709
  const binding = attachment.environments.find(
1707
1710
  (item) => item.environmentId === context.environment.id
1708
1711
  );
1709
- if (["pulse", "cookie"].includes(service) && !binding)
1712
+ if (["pulse", "cookie", "seo"].includes(service) && !binding)
1710
1713
  throw new SiteOSAuthApiError({
1711
1714
  code: "PROJECT_ENVIRONMENT_NOT_CONFIGURED",
1712
1715
  message: `Set up ${service} in ${context.environment.name} with \`siteos project connect ${service}\`.`
1713
1716
  });
1714
1717
  return {
1715
1718
  ...context,
1716
- resourceId: ["pulse", "cookie"].includes(service) ? binding.resourceId : attachment.resourceId,
1719
+ resourceId: ["pulse", "cookie", "seo"].includes(service) ? binding.resourceId : attachment.resourceId,
1717
1720
  environmentBinding: binding
1718
1721
  };
1719
1722
  }
@@ -8752,13 +8755,13 @@ Usage:
8752
8755
  siteos project use <id-or-slug> [--json]
8753
8756
  siteos project update [--name <name>] [--slug <slug>] [--url <production-url>] [--json]
8754
8757
  siteos project status [--json]
8755
- siteos project connect <pulse|cookie|forms|search|trace> [--resource <id>] [--json]
8758
+ siteos project connect <pulse|cookie|forms|search|trace|seo> [--resource <id>] [--json]
8756
8759
  siteos project environment list [--json]
8757
8760
  siteos project environment create --name <name> --slug <slug> [--url <url>] [--json]
8758
8761
  siteos project environment use <slug> [--json]
8759
8762
  siteos project environment update <slug> [--name <name>] [--url <url>] [--json]
8760
- siteos project environment resources <pulse|cookie|forms|search|trace> [--json]
8761
- siteos project environment connect <pulse|cookie|forms|search|trace> --environment <slug> [--resource <id>] [--json]
8763
+ siteos project environment resources <pulse|cookie|forms|search|trace|seo> [--json]
8764
+ siteos project environment connect <pulse|cookie|forms|search|trace|seo> --environment <slug> [--resource <id>] [--json]
8762
8765
 
8763
8766
  Select a Project once per repository. Service commands use its configured resources.
8764
8767
  Connect creates a draft workspace, or explicitly attaches an existing resource.
@@ -8856,7 +8859,7 @@ Run \`siteos project status\` to inspect its services.`;
8856
8859
  const service = parsed.positionals[0];
8857
8860
  if (!PROJECT_SERVICES.includes(service))
8858
8861
  throw new Error(
8859
- "Choose pulse, cookie, forms, search or trace. Organization connections use `siteos integrations`. "
8862
+ "Choose pulse, cookie, forms, search, trace or seo. Organization connections use `siteos integrations`. "
8860
8863
  );
8861
8864
  overview = await api.connect(
8862
8865
  overview.project.id,
@@ -8912,7 +8915,7 @@ async function runEnvironmentCommand3(options) {
8912
8915
  throw new Error("Environment create requires --name and --slug.");
8913
8916
  const service = positionals[0];
8914
8917
  if ((action === "connect" || action === "resources") && !PROJECT_SERVICES.includes(service))
8915
- throw new Error("Choose pulse, cookie, forms, search or trace.");
8918
+ throw new Error("Choose pulse, cookie, forms, search, trace or seo.");
8916
8919
  if (action === "connect" && !values.environment)
8917
8920
  throw new Error("Choose a Project environment with --environment <slug>.");
8918
8921
  const context = await readCommonProject(
@@ -9359,6 +9362,382 @@ async function runServiceCommand(service, options) {
9359
9362
  }
9360
9363
  }
9361
9364
 
9365
+ // src/services/seo-command.ts
9366
+ import { writeFile as writeFile8 } from "fs/promises";
9367
+ import path29 from "path";
9368
+ import { parseArgs as parseArgs4 } from "util";
9369
+ import { z as z16 } from "zod";
9370
+ var SEO_HELP = `Audit public HTML in the selected Project environment.
9371
+
9372
+ Usage:
9373
+ siteos seo status [--environment <slug>] [--json]
9374
+ siteos seo audit run [--environment <slug>] [--json]
9375
+ siteos seo audit list [--environment <slug>] [--json]
9376
+ siteos seo audit show <id> [--environment <slug>] [--json]
9377
+ siteos seo audit cancel <id> [--environment <slug>] [--json]
9378
+ siteos seo pages [--audit <id>] [--query <text>] [--url <url>] [--page <number>] [--environment <slug>] [--json]
9379
+ siteos seo issues [--audit <id>] [--rule <id>] [--page <number>] [--environment <slug>] [--json]
9380
+ siteos seo changes [--audit <id>] [--state <new|reopened|still_present|resolved|not_rechecked>] [--page <number>] [--environment <slug>] [--json]
9381
+ siteos seo recheck --audit <id> --url <url> [--environment <slug>] [--json]
9382
+ siteos seo issue <ignore|restore> --audit <id> --url <url> --rule <id> --reason <text> --revision <number> [--environment <slug>] [--json]
9383
+ siteos seo schedule show [--environment <slug>] [--json]
9384
+ siteos seo schedule set --enabled <true|false> --weekday <1-7> --time <HH:mm> --timezone <IANA> --revision <number> [--environment <slug>] [--json]
9385
+ siteos seo notifications retry <notification-id> [--environment <slug>] [--json]
9386
+ siteos seo notifications show [--environment <slug>] [--json]
9387
+ siteos seo notifications destinations [--environment <slug>] [--json]
9388
+ siteos seo notifications set --enabled <true|false> [--destination <candidate-id>] --severity <error|warning> --failures <true|false> --revision <number> [--environment <slug>] [--json]
9389
+ siteos seo export --audit <id> --kind <pages|issues|changes> --format <csv|json> --output <new-file> [--query <text>] [--rule <id>] [--severity <error|warning|notice>] [--state <page-or-change-state>] [--environment <slug>] [--json]
9390
+
9391
+ Schedule and notification writes require an owner/admin and the saved revision (initially 0).
9392
+ Export writes all matching rows to a new file; existing files are never overwritten.
9393
+ Runs are queued. Read audit show until terminal; an accepted run is not a completed check.
9394
+ Recheck accepts a URL observed in the source audit. Cross-page rules require a full audit.
9395
+ Read the current disposition revision before ignore/restore; use 0 if no decision exists.
9396
+ Setup: siteos project connect seo. No crawl runs during setup.`;
9397
+ async function runSeoCommand(options) {
9398
+ if (!options.args.length || options.args.some((arg) => ["--help", "-h"].includes(arg)))
9399
+ return { exitCode: 0, stdout: SEO_HELP };
9400
+ const json = options.args.includes("--json");
9401
+ try {
9402
+ const { positionals, values } = parseArgs4({
9403
+ args: options.args,
9404
+ strict: true,
9405
+ allowPositionals: true,
9406
+ options: {
9407
+ json: { type: "boolean" },
9408
+ environment: { type: "string" },
9409
+ audit: { type: "string" },
9410
+ url: { type: "string" },
9411
+ rule: { type: "string" },
9412
+ reason: { type: "string" },
9413
+ revision: { type: "string" },
9414
+ query: { type: "string" },
9415
+ page: { type: "string" },
9416
+ state: { type: "string" },
9417
+ enabled: { type: "string" },
9418
+ weekday: { type: "string" },
9419
+ time: { type: "string" },
9420
+ timezone: { type: "string" },
9421
+ destination: { type: "string" },
9422
+ severity: { type: "string" },
9423
+ failures: { type: "string" },
9424
+ kind: { type: "string" },
9425
+ format: { type: "string" },
9426
+ output: { type: "string" }
9427
+ }
9428
+ });
9429
+ const route = positionals.slice(0, 2).join(" ");
9430
+ const action = positionals[0];
9431
+ const operations = {
9432
+ status: { flags: [], args: 1 },
9433
+ "schedule show": { flags: [], args: 2 },
9434
+ "schedule set": {
9435
+ flags: ["enabled", "weekday", "time", "timezone", "revision"],
9436
+ args: 2
9437
+ },
9438
+ "notifications retry": { flags: [], args: 3 },
9439
+ "notifications show": { flags: [], args: 2 },
9440
+ "notifications destinations": { flags: [], args: 2 },
9441
+ "notifications set": {
9442
+ flags: ["enabled", "destination", "severity", "failures", "revision"],
9443
+ args: 2
9444
+ },
9445
+ export: {
9446
+ flags: [
9447
+ "audit",
9448
+ "kind",
9449
+ "format",
9450
+ "output",
9451
+ "query",
9452
+ "rule",
9453
+ "severity",
9454
+ "state"
9455
+ ],
9456
+ args: 1
9457
+ },
9458
+ "audit run": { flags: [], args: 2 },
9459
+ "audit list": { flags: [], args: 2 },
9460
+ "audit show": { flags: [], args: 3 },
9461
+ "audit cancel": { flags: [], args: 3 },
9462
+ pages: { flags: ["audit", "url", "query", "page"], args: 1 },
9463
+ issues: { flags: ["audit", "rule", "page"], args: 1 },
9464
+ changes: { flags: ["audit", "state", "page"], args: 1 },
9465
+ recheck: { flags: ["audit", "url"], args: 1 },
9466
+ "issue ignore": {
9467
+ flags: ["audit", "url", "rule", "reason", "revision"],
9468
+ args: 2
9469
+ },
9470
+ "issue restore": {
9471
+ flags: ["audit", "url", "rule", "reason", "revision"],
9472
+ args: 2
9473
+ }
9474
+ };
9475
+ const operation = operations[route];
9476
+ if (!operation || positionals.length !== operation.args || Object.keys(values).some(
9477
+ (key) => !["json", "environment", ...operation.flags].includes(key)
9478
+ ))
9479
+ throw new Error(
9480
+ "Invalid SEO operation or flags. Run `siteos seo --help`."
9481
+ );
9482
+ if (action === "recheck" && (!values.audit || !values.url))
9483
+ throw new Error("Recheck requires --audit and --url.");
9484
+ if (action === "issue" && ["audit", "url", "rule", "reason", "revision"].some(
9485
+ (key) => !values[key]
9486
+ ))
9487
+ throw new Error(
9488
+ "Issue decisions require --audit, --url, --rule, --reason and --revision."
9489
+ );
9490
+ if (values.page && !/^[1-9]\d{0,5}$/u.test(values.page))
9491
+ throw new Error("Use a positive page number.");
9492
+ if (values.revision && !/^\d{1,9}$/u.test(values.revision))
9493
+ throw new Error("Use a non-negative revision.");
9494
+ if (values.reason && (values.reason.trim().length < 3 || values.reason.length > 500))
9495
+ throw new Error("Use a reason between 3 and 500 characters.");
9496
+ const pageStateFilter = action === "export" && values.kind !== "changes";
9497
+ if (values.state && !(pageStateFilter ? ["analyzed", "unavailable", "excluded", "non_html"] : ["new", "reopened", "still_present", "resolved", "not_rechecked"]).includes(values.state))
9498
+ throw new Error("Use a documented page or change state for this export.");
9499
+ const automation = ["schedule", "notifications"].includes(action ?? "");
9500
+ const setting = automation && positionals[1] === "set";
9501
+ if (setting && (!values.revision || !["true", "false"].includes(values.enabled ?? "")))
9502
+ throw new Error("Settings require --enabled true|false and --revision.");
9503
+ if (route === "schedule set") {
9504
+ if (!/^[1-7]$/u.test(values.weekday ?? "") || !/^([01]\d|2[0-3]):[0-5]\d$/u.test(values.time ?? "") || !values.timezone)
9505
+ throw new Error("Use weekday 1\u20137, HH:mm and an IANA time zone.");
9506
+ try {
9507
+ new Intl.DateTimeFormat("en", { timeZone: values.timezone });
9508
+ } catch {
9509
+ throw new Error("Use a valid IANA time zone.");
9510
+ }
9511
+ }
9512
+ if (route === "notifications set" && (!["error", "warning"].includes(values.severity ?? "") || !["true", "false"].includes(values.failures ?? "") || values.enabled === "true" && !values.destination))
9513
+ throw new Error(
9514
+ "Notifications require --severity, --failures and an available --destination when enabled."
9515
+ );
9516
+ if (action === "export" && (!values.audit || !["pages", "issues", "changes"].includes(values.kind ?? "") || !["csv", "json"].includes(values.format ?? "") || !values.output))
9517
+ throw new Error(
9518
+ "Export requires --audit, --kind, --format and --output."
9519
+ );
9520
+ if (values.severity && !["error", "warning", "notice"].includes(values.severity))
9521
+ throw new Error("Use a documented severity.");
9522
+ const context = await commonServiceContext(
9523
+ options,
9524
+ "seo",
9525
+ values.environment
9526
+ );
9527
+ if (!context)
9528
+ throw new Error(
9529
+ "Select a SiteOS Project with `siteos project use` first."
9530
+ );
9531
+ const runtime = commonProjectRuntime(options);
9532
+ const retryNotification = route === "notifications retry";
9533
+ const writing = retryNotification || setting || [
9534
+ "audit run",
9535
+ "audit cancel",
9536
+ "recheck",
9537
+ "issue ignore",
9538
+ "issue restore"
9539
+ ].includes(route);
9540
+ const scope = retryNotification ? "seo:notifications:write" : setting ? action === "schedule" ? "seo:schedule:write" : "seo:notifications:write" : !writing ? "seo:workspace:read" : action === "issue" ? "seo:issues:write" : "seo:audits:write";
9541
+ const grant = await runtime.grants.acquire({
9542
+ audience: "siteos-seo",
9543
+ scopes: [scope]
9544
+ });
9545
+ if (grant.grant.audience !== "siteos-seo" || grant.grant.scopes.length !== 1 || grant.grant.scopes[0] !== scope || grant.grant.organizationId !== context.overview.project.organizationId)
9546
+ throw new SiteOSAuthApiError({
9547
+ code: "AUTH_INVALID_RESPONSE",
9548
+ message: "The SEO grant does not match this Project and operation."
9549
+ });
9550
+ const query = new URLSearchParams();
9551
+ for (const [flag, name] of [
9552
+ ["audit", "audit"],
9553
+ ["rule", "rule"],
9554
+ ["query", "q"],
9555
+ ["url", "pageUrl"],
9556
+ ["page", "page"],
9557
+ ["state", "change"],
9558
+ ["kind", "kind"],
9559
+ ["format", "format"],
9560
+ ["severity", "severity"]
9561
+ ])
9562
+ if (values[flag] && !writing) query.set(name, values[flag]);
9563
+ if (pageStateFilter && values.state) {
9564
+ query.delete("change");
9565
+ query.set("state", values.state);
9566
+ }
9567
+ if (route === "audit show") query.set("audit", positionals[2]);
9568
+ const suffix = retryNotification ? "/notification-retries" : automation ? setting ? `/${action}` : route === "notifications destinations" ? "/destinations" : "/automation" : action === "export" ? `/export?${query}` : route === "audit run" ? "/audits" : route === "audit cancel" ? `/audits/${encodeURIComponent(positionals[2])}/cancel` : action === "recheck" ? "/rechecks" : action === "issue" ? "/dispositions" : `?${query}`;
9569
+ const body = retryNotification ? { notificationId: positionals[2] } : route === "schedule set" ? {
9570
+ enabled: values.enabled === "true",
9571
+ weekday: Number(values.weekday),
9572
+ time: values.time,
9573
+ timeZone: values.timezone,
9574
+ expectedRevision: Number(values.revision)
9575
+ } : route === "notifications set" ? {
9576
+ enabled: values.enabled === "true",
9577
+ candidateId: values.destination ?? null,
9578
+ minimumSeverity: values.severity,
9579
+ includeFailures: values.failures === "true",
9580
+ expectedRevision: Number(values.revision)
9581
+ } : action === "recheck" ? { auditId: values.audit, urls: [values.url] } : action === "issue" ? {
9582
+ auditId: values.audit,
9583
+ url: values.url,
9584
+ ruleId: values.rule,
9585
+ reason: values.reason,
9586
+ expectedRevision: Number(values.revision),
9587
+ ignored: positionals[1] === "ignore"
9588
+ } : void 0;
9589
+ if (!options.fetchImpl)
9590
+ throw new Error("SiteOS API access is unavailable.");
9591
+ const response = await options.fetchImpl(
9592
+ `${runtime.api.origin}/api/seo/v1/resources/${encodeURIComponent(context.resourceId)}${suffix}`,
9593
+ {
9594
+ method: setting ? "PATCH" : writing ? "POST" : "GET",
9595
+ headers: {
9596
+ Accept: "application/json",
9597
+ Authorization: `Bearer ${grant.accessToken}`,
9598
+ ...body ? { "Content-Type": "application/json" } : {}
9599
+ },
9600
+ ...body ? { body: JSON.stringify(body) } : {},
9601
+ signal: AbortSignal.timeout(3e4)
9602
+ }
9603
+ );
9604
+ if (action === "export" && response.ok) {
9605
+ if (!(response instanceof Response))
9606
+ throw new Error("The API transport does not support file exports.");
9607
+ const mime = values.format === "csv" ? "text/csv" : "application/json";
9608
+ const rows = Number(response.headers.get("X-SEO-Export-Rows"));
9609
+ if (response.headers.get("X-SEO-Audit-Id") !== values.audit || !response.headers.get("Content-Type")?.startsWith(mime) || !response.headers.has("X-SEO-Export-Rows") || !Number.isSafeInteger(rows) || rows < 0)
9610
+ throw new Error(
9611
+ "The export response does not match the requested audit."
9612
+ );
9613
+ const text = await response.text();
9614
+ if (values.format === "json")
9615
+ z16.object({
9616
+ contractVersion: z16.literal(1),
9617
+ audit: z16.object({
9618
+ id: z16.literal(values.audit),
9619
+ resourceId: z16.literal(context.resourceId)
9620
+ }),
9621
+ kind: z16.literal(values.kind),
9622
+ totalRows: z16.literal(rows),
9623
+ rows: z16.array(z16.unknown()).length(rows)
9624
+ }).parse(JSON.parse(text));
9625
+ const output = path29.resolve(options.cwd ?? process.cwd(), values.output);
9626
+ await writeFile8(output, text, { flag: "wx", mode: 384 });
9627
+ return {
9628
+ exitCode: 0,
9629
+ stdout: JSON.stringify(
9630
+ {
9631
+ auditId: values.audit,
9632
+ kind: values.kind,
9633
+ format: values.format,
9634
+ rows,
9635
+ output
9636
+ },
9637
+ null,
9638
+ 2
9639
+ )
9640
+ };
9641
+ }
9642
+ const data = await response.json();
9643
+ if (!response.ok) {
9644
+ const result = z16.object({
9645
+ error: z16.object({ code: z16.string(), message: z16.string().max(500) })
9646
+ }).safeParse(data);
9647
+ throw new SiteOSAuthApiError({
9648
+ code: result.success ? result.data.error.code : "SEO_REQUEST_FAILED",
9649
+ message: result.success ? result.data.error.message : "The SEO request failed.",
9650
+ status: response.status
9651
+ });
9652
+ }
9653
+ const record = z16.object({ contractVersion: z16.literal(1) }).passthrough().parse(data);
9654
+ if (automation) {
9655
+ z16.literal(context.resourceId).parse(record.resourceId);
9656
+ const schedule = z16.object({
9657
+ enabled: z16.boolean(),
9658
+ weekday: z16.number().int().min(1).max(7),
9659
+ time: z16.string(),
9660
+ timeZone: z16.string(),
9661
+ revision: z16.number().int().min(0),
9662
+ nextRunAt: z16.string().nullable()
9663
+ });
9664
+ const notificationRoute = z16.object({
9665
+ enabled: z16.boolean(),
9666
+ minimumSeverity: z16.enum(["error", "warning"]),
9667
+ includeFailures: z16.boolean(),
9668
+ revision: z16.number().int().min(0),
9669
+ destinationId: z16.string().nullable()
9670
+ });
9671
+ if (retryNotification) {
9672
+ z16.literal(true).parse(record.retryQueued);
9673
+ z16.literal(positionals[2]).parse(record.notificationId);
9674
+ } else if (route === "notifications destinations")
9675
+ z16.object({
9676
+ candidates: z16.array(
9677
+ z16.object({
9678
+ candidateId: z16.string(),
9679
+ label: z16.string(),
9680
+ availability: z16.literal("available")
9681
+ })
9682
+ )
9683
+ }).parse(record);
9684
+ else if (setting) {
9685
+ const value = action === "schedule" ? schedule.parse(record.schedule) : notificationRoute.parse(record.route);
9686
+ if (value.revision !== Number(values.revision) + 1 || value.enabled !== (values.enabled === "true"))
9687
+ throw new Error("The saved settings do not match this change.");
9688
+ } else {
9689
+ schedule.parse(record.schedule);
9690
+ notificationRoute.parse(record.route);
9691
+ }
9692
+ } else if (!writing) {
9693
+ const validated = z16.object({
9694
+ resource: z16.object({
9695
+ id: z16.literal(context.resourceId),
9696
+ organizationId: z16.literal(context.overview.project.organizationId)
9697
+ }),
9698
+ audits: z16.array(z16.object({ id: z16.string() }).passthrough()),
9699
+ audit: z16.object({
9700
+ id: z16.string(),
9701
+ resourceId: z16.literal(context.resourceId)
9702
+ }).passthrough().nullable(),
9703
+ pages: z16.array(z16.unknown()),
9704
+ issues: z16.array(z16.unknown()),
9705
+ changes: z16.array(z16.unknown()),
9706
+ totalChanges: z16.number(),
9707
+ dispositions: z16.array(z16.unknown())
9708
+ }).passthrough().parse(record);
9709
+ const selected = query.get("audit");
9710
+ if (selected && validated.audit?.id !== selected)
9711
+ throw new Error("The SEO response does not match the requested audit.");
9712
+ } else if (record.audit)
9713
+ z16.object({
9714
+ id: z16.string(),
9715
+ resourceId: z16.literal(context.resourceId),
9716
+ organizationId: z16.literal(context.overview.project.organizationId),
9717
+ state: z16.literal("queued")
9718
+ }).parse(record.audit);
9719
+ else if (route === "audit cancel") z16.literal(true).parse(record.cancelled);
9720
+ else if (action === "issue")
9721
+ z16.object({
9722
+ url: z16.literal(values.url),
9723
+ ruleId: z16.literal(values.rule),
9724
+ ignored: z16.literal(positionals[1] === "ignore"),
9725
+ revision: z16.literal(Number(values.revision) + 1)
9726
+ }).parse(record.disposition);
9727
+ else throw new Error("The SEO service returned an invalid response.");
9728
+ return { exitCode: 0, stdout: JSON.stringify(record, null, 2) };
9729
+ } catch (cause) {
9730
+ const error = {
9731
+ code: cause instanceof SiteOSAuthApiError ? cause.code : "SEO_COMMAND_FAILED",
9732
+ message: cause instanceof z16.ZodError ? "The SEO service returned an invalid response." : cause instanceof Error ? cause.message : "The SEO command failed."
9733
+ };
9734
+ return {
9735
+ exitCode: cause instanceof SiteOSAuthApiError ? 1 : 2,
9736
+ ...json ? { stdout: JSON.stringify({ error }) } : { stderr: error.message }
9737
+ };
9738
+ }
9739
+ }
9740
+
9362
9741
  // src/cli.ts
9363
9742
  var [command, ...args] = process.argv.slice(2);
9364
9743
  var HELP_FLAGS5 = /* @__PURE__ */ new Set(["--help", "-h"]);
@@ -9392,6 +9771,12 @@ if (!command || command === "--help" || command === "-h") {
9392
9771
  }
9393
9772
  function rootCommandRegistry() {
9394
9773
  return createCommandRegistry({
9774
+ seo: (args2) => runSeoCommand({
9775
+ args: args2,
9776
+ cwd: process.cwd(),
9777
+ env: process.env,
9778
+ fetchImpl: globalThis.fetch
9779
+ }),
9395
9780
  "health-check": runHealthCheck,
9396
9781
  project: (args2) => runProjectCommand4({
9397
9782
  args: args2,
@@ -9499,6 +9884,7 @@ Usage:
9499
9884
  siteos project --help
9500
9885
  siteos cookie --help
9501
9886
  siteos trace --help
9887
+ siteos seo --help
9502
9888
  siteos integrations --help
9503
9889
  siteos pulse --help
9504
9890
  siteos search --help
@@ -9510,6 +9896,7 @@ Commands:
9510
9896
  project Select one Project and configure its services and environments.
9511
9897
  cookie Configure, publish, and inspect the Project\u2019s cookie banner.
9512
9898
  trace Configure analytics observation and inspect evidence.
9899
+ seo Audit HTML, inspect changes and verify fixes.
9513
9900
  integrations Manage Organization connections and destinations.
9514
9901
  pulse Manage monitoring checks, tests, and deployments.
9515
9902
  search Run SiteOS search operations for a project environment.