@siteoshq/cli 1.1.1 → 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/dist/cli.js CHANGED
@@ -1,4 +1,438 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/services/cookie-verification.ts
13
+ var cookie_verification_exports = {};
14
+ __export(cookie_verification_exports, {
15
+ VerificationInstallation: () => VerificationInstallation,
16
+ assessCookieObservations: () => assessCookieObservations,
17
+ verificationTarget: () => verificationTarget,
18
+ verifyCookieWebsite: () => verifyCookieWebsite
19
+ });
20
+ import { z as z14 } from "zod";
21
+ function verificationTarget(websiteUrl, candidate) {
22
+ const website = new URL(websiteUrl);
23
+ const target = new URL(candidate ?? websiteUrl, website);
24
+ if (target.origin !== website.origin || target.username || target.password || target.hash || !["https:", "http:"].includes(target.protocol))
25
+ throw new Error(
26
+ "Verify a URL on the selected Project environment's origin, without credentials or fragments."
27
+ );
28
+ return target.toString();
29
+ }
30
+ function matchesName(value, names) {
31
+ return Boolean(
32
+ names && (names.exact.includes(value) || names.prefixes.some((prefix) => value.startsWith(prefix)))
33
+ );
34
+ }
35
+ function assessCookieObservations(input) {
36
+ const issues = [];
37
+ const unknownOrigins = /* @__PURE__ */ new Set();
38
+ const unknownStorage = /* @__PURE__ */ new Set();
39
+ const services = input.envelope.config.services;
40
+ for (const request of input.requests) {
41
+ const matches = services.filter(
42
+ (service) => service.lifecycle && [
43
+ ...service.lifecycle.scriptOrigins,
44
+ ...service.lifecycle.iframeOrigins,
45
+ ...service.lifecycle.pixelOrigins
46
+ ].some((url) => new URL(url).origin === request.origin)
47
+ );
48
+ const state = input.observations.find(
49
+ (item) => item.scenario === request.scenario
50
+ )?.state;
51
+ if (matches.length) {
52
+ const allowed = matches.some(
53
+ (service) => state?.services.find((item) => item.key === service.key)?.allowed
54
+ );
55
+ const advancedGoogle = input.envelope.config.integrations.googleConsentMode === "advanced" && matches.every(
56
+ (service) => ["google-analytics", "google-ads"].includes(service.key)
57
+ );
58
+ if (!allowed && !advancedGoogle)
59
+ issues.push({
60
+ code: "request_without_permission",
61
+ scenario: request.scenario,
62
+ detail: request.origin
63
+ });
64
+ } else if (request.origin !== input.firstPartyOrigin && !input.deliveryOrigins.includes(request.origin))
65
+ unknownOrigins.add(request.origin);
66
+ }
67
+ for (const observation of input.observations) {
68
+ for (const service of services) {
69
+ const allowed = observation.state.services.find(
70
+ (item) => item.key === service.key
71
+ )?.allowed;
72
+ const stored = observation.cookies.some(
73
+ (name) => matchesName(name, service.lifecycle?.firstPartyCookies)
74
+ ) || observation.localStorage.some(
75
+ (name) => matchesName(name, service.lifecycle?.localStorage)
76
+ );
77
+ if (!allowed && stored)
78
+ issues.push({
79
+ code: "storage_without_permission",
80
+ scenario: observation.scenario,
81
+ detail: service.key
82
+ });
83
+ }
84
+ for (const name of observation.cookies) {
85
+ if (name !== "siteos_consent" && !services.some(
86
+ (service) => matchesName(name, service.lifecycle?.firstPartyCookies)
87
+ ))
88
+ unknownStorage.add(`cookie:${name}`);
89
+ }
90
+ for (const name of observation.localStorage) {
91
+ if (!name.startsWith("siteos-cookie:") && !services.some(
92
+ (service) => matchesName(name, service.lifecycle?.localStorage)
93
+ ))
94
+ unknownStorage.add(`localStorage:${name}`);
95
+ }
96
+ }
97
+ return {
98
+ issues,
99
+ unknownOrigins: [...unknownOrigins].sort(),
100
+ unknownStorage: [...unknownStorage].sort()
101
+ };
102
+ }
103
+ async function verifyCookieWebsite(input) {
104
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
105
+ const issues = [];
106
+ const requests = [];
107
+ const observations = [];
108
+ let envelope = null;
109
+ let scenario = "first-visit";
110
+ let overflow = false;
111
+ const installation = input.installation;
112
+ const browser = await (input.launch ?? (async () => {
113
+ const playwright = await import("playwright");
114
+ try {
115
+ return await playwright[input.browserName].launch({ headless: true });
116
+ } catch {
117
+ throw new Error(
118
+ `The ${input.browserName} browser is unavailable. Run npx playwright@1.61.1 install ${input.browserName}, then retry.`
119
+ );
120
+ }
121
+ }))();
122
+ const readState = async (page) => State.parse(
123
+ await page.evaluate(
124
+ () => window.SiteOSCookie.getDebugState()
125
+ )
126
+ );
127
+ const waitForRuntime = async (page) => {
128
+ await page.waitForFunction(
129
+ () => Boolean(
130
+ window.SiteOSCookie?.getDebugState().resolved
131
+ ),
132
+ void 0,
133
+ { timeout: 15e3 }
134
+ );
135
+ };
136
+ try {
137
+ for (const gpc of [false, true]) {
138
+ const context = await browser.newContext({
139
+ serviceWorkers: "block",
140
+ ...gpc ? { extraHTTPHeaders: { "Sec-GPC": "1" } } : {}
141
+ });
142
+ if (gpc)
143
+ await context.addInitScript(
144
+ () => Object.defineProperty(navigator, "globalPrivacyControl", {
145
+ get: () => true
146
+ })
147
+ );
148
+ const page = await context.newPage();
149
+ page.setDefaultTimeout(15e3);
150
+ page.setDefaultNavigationTimeout(25e3);
151
+ const pending = /* @__PURE__ */ new Set();
152
+ page.on("request", (request) => {
153
+ if (!/^https?:/u.test(request.url())) return;
154
+ if (requests.length >= 2e3) {
155
+ overflow = true;
156
+ return;
157
+ }
158
+ requests.push({
159
+ origin: new URL(request.url()).origin,
160
+ type: request.resourceType(),
161
+ scenario
162
+ });
163
+ });
164
+ page.on("response", (response) => {
165
+ const expectedUrl = new URL(installation.delivery.configUrl);
166
+ const responseUrl = new URL(response.url());
167
+ if (responseUrl.origin !== expectedUrl.origin || responseUrl.pathname !== expectedUrl.pathname)
168
+ return;
169
+ const responseScenario = scenario;
170
+ const reading = (async () => {
171
+ let timeout;
172
+ try {
173
+ const raw = await Promise.race([
174
+ response.body(),
175
+ new Promise((_, reject) => {
176
+ timeout = setTimeout(() => reject(new Error("timeout")), 5e3);
177
+ })
178
+ ]);
179
+ if (raw.byteLength > 512 * 1024) throw new Error("size");
180
+ const parsed = Envelope.parse(JSON.parse(raw.toString()));
181
+ if (parsed.publicKey !== installation.publicKey || parsed.revision !== installation.currentPublishedRevision)
182
+ throw new Error("revision");
183
+ envelope = parsed;
184
+ } catch {
185
+ issues.push({
186
+ code: "configuration_mismatch",
187
+ scenario: responseScenario,
188
+ detail: "Configuration is unavailable, invalid, or differs from the published revision."
189
+ });
190
+ } finally {
191
+ clearTimeout(timeout);
192
+ }
193
+ })();
194
+ pending.add(reading);
195
+ void reading.finally(() => pending.delete(reading));
196
+ });
197
+ scenario = gpc ? "gpc-first-visit" : "first-visit";
198
+ try {
199
+ await page.goto(input.url, { waitUntil: "domcontentloaded" });
200
+ if (new URL(page.url()).origin !== new URL(input.url).origin)
201
+ throw new Error("The website redirected to another origin.");
202
+ await waitForRuntime(page);
203
+ await Promise.all([...pending]);
204
+ const capture = async () => {
205
+ await page.waitForTimeout(1500);
206
+ if (new URL(page.url()).origin !== new URL(input.url).origin)
207
+ throw new Error("The website navigated to another origin.");
208
+ await waitForRuntime(page);
209
+ const state = await readState(page);
210
+ if (state.preview || state.resolved?.publicKey !== installation.publicKey || state.resolved?.revision !== installation.currentPublishedRevision)
211
+ issues.push({
212
+ code: "runtime_mismatch",
213
+ scenario,
214
+ detail: "The loaded runtime is a preview or uses another publication."
215
+ });
216
+ if (Number(state.runtimeVersion.split(".")[0]) < 11)
217
+ issues.push({
218
+ code: "runtime_outdated",
219
+ scenario,
220
+ detail: state.runtimeVersion
221
+ });
222
+ observations.push({
223
+ scenario,
224
+ state,
225
+ cookies: [
226
+ ...new Set(
227
+ (await context.cookies()).map((cookie) => cookie.name)
228
+ )
229
+ ].sort(),
230
+ localStorage: await page.evaluate(
231
+ () => Object.keys(localStorage).sort()
232
+ )
233
+ });
234
+ };
235
+ await capture();
236
+ if (gpc) {
237
+ const state = observations.at(-1).state;
238
+ if (!state.consent.globalPrivacyControl || !state.consent.privacyChoices.saleOrShareOptOut || !state.consent.privacyChoices.targetedAdvertisingOptOut)
239
+ issues.push({
240
+ code: "gpc_not_applied",
241
+ scenario,
242
+ detail: "GPC must be visible and both privacy opt-outs applied."
243
+ });
244
+ } else {
245
+ for (const action of [
246
+ "reject",
247
+ "accept",
248
+ "granular",
249
+ "withdraw"
250
+ ]) {
251
+ scenario = action;
252
+ try {
253
+ await page.evaluate(async (choice) => {
254
+ const api = window.SiteOSCookie;
255
+ if (choice === "accept") await api.acceptAll();
256
+ else if (choice === "granular")
257
+ await api.updateConsent(
258
+ api.getConsent().categories.filter((key) => key !== "necessary").slice(0, 1)
259
+ );
260
+ else await api.rejectAll();
261
+ }, action);
262
+ } catch (error) {
263
+ if (!(error instanceof Error) || !/Execution context was destroyed/u.test(error.message))
264
+ throw error;
265
+ }
266
+ await capture();
267
+ const state = observations.at(-1).state;
268
+ if ((action === "reject" || action === "withdraw") && state.consent.categories.some((key) => key !== "necessary"))
269
+ issues.push({
270
+ code: "refusal_not_applied",
271
+ scenario,
272
+ detail: "Optional categories remain granted."
273
+ });
274
+ }
275
+ scenario = "returning-after-refusal";
276
+ await page.reload({ waitUntil: "domcontentloaded" });
277
+ await capture();
278
+ if (observations.at(-1).state.consent.categories.some((key) => key !== "necessary"))
279
+ issues.push({
280
+ code: "refusal_not_persisted",
281
+ scenario,
282
+ detail: "Refusal was not preserved across reload."
283
+ });
284
+ }
285
+ } catch {
286
+ issues.push({
287
+ code: "scenario_incomplete",
288
+ scenario,
289
+ detail: "The page or Cookie runtime did not complete this scenario within the time limit."
290
+ });
291
+ } finally {
292
+ await context.close();
293
+ await Promise.all([...pending]);
294
+ }
295
+ }
296
+ } finally {
297
+ await browser.close();
298
+ }
299
+ const assessment = envelope ? assessCookieObservations({
300
+ envelope,
301
+ observations,
302
+ requests,
303
+ firstPartyOrigin: new URL(input.url).origin,
304
+ deliveryOrigins: Object.values(installation.delivery).filter((url) => typeof url === "string").map((url) => new URL(url).origin)
305
+ }) : { issues: [], unknownOrigins: [], unknownStorage: [] };
306
+ issues.push(...assessment.issues);
307
+ if (!envelope)
308
+ issues.push({
309
+ code: "configuration_not_observed",
310
+ scenario: "all",
311
+ detail: "No matching public configuration response was observed."
312
+ });
313
+ if (overflow)
314
+ issues.push({
315
+ code: "request_limit",
316
+ scenario: "all",
317
+ detail: "The request limit was reached; this run is incomplete."
318
+ });
319
+ const config = envelope;
320
+ const status = issues.length ? "failed" : assessment.unknownOrigins.length || assessment.unknownStorage.length || config?.config.integrations.googleConsentMode === "advanced" ? "needs-review" : "passed";
321
+ return {
322
+ schemaVersion: 1,
323
+ status,
324
+ startedAt,
325
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
326
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString(),
327
+ target: {
328
+ origin: new URL(input.url).origin,
329
+ path: new URL(input.url).pathname
330
+ },
331
+ browser: input.browserName,
332
+ publicKey: installation.publicKey,
333
+ revision: installation.currentPublishedRevision,
334
+ region: config?.trustedSignals?.region ?? null,
335
+ issues: [
336
+ ...new Map(
337
+ issues.map((issue) => [JSON.stringify(issue), issue])
338
+ ).values()
339
+ ],
340
+ unknownOrigins: assessment.unknownOrigins,
341
+ unknownStorage: assessment.unknownStorage,
342
+ observations,
343
+ requestCounts: [
344
+ ...new Set(requests.map((item) => `${item.scenario} ${item.origin}`))
345
+ ].map((key) => ({
346
+ scenario: key.split(" ")[0],
347
+ origin: key.split(" ")[1],
348
+ count: requests.filter(
349
+ (item) => `${item.scenario} ${item.origin}` === key
350
+ ).length
351
+ })),
352
+ limits: [
353
+ "One route, fresh local browser, 1.5-second observation per scenario, at most 2000 requests.",
354
+ "Consent actions use the public runtime API. Visual, keyboard and GTM Tag Assistant acceptance remain separate.",
355
+ "Unknown origins/storage need classification. First-party or server-side tracking and delayed/interaction-only resources require a source audit.",
356
+ "Geography is this browser's actual Edge location. Use regions resolve for simulations; do not spoof production geography.",
357
+ "No cookie values, request query strings, bodies, credentials or visitor identifiers are included. A pass covers only the observed scope, not legal compliance."
358
+ ]
359
+ };
360
+ }
361
+ var Names, Service, Envelope, State, VerificationInstallation;
362
+ var init_cookie_verification = __esm({
363
+ "src/services/cookie-verification.ts"() {
364
+ "use strict";
365
+ Names = z14.object({
366
+ exact: z14.array(z14.string()),
367
+ prefixes: z14.array(z14.string())
368
+ });
369
+ Service = z14.object({
370
+ key: z14.string(),
371
+ purposeKey: z14.string(),
372
+ lifecycle: z14.object({
373
+ scriptOrigins: z14.array(z14.string()),
374
+ iframeOrigins: z14.array(z14.string()),
375
+ pixelOrigins: z14.array(z14.string()),
376
+ firstPartyCookies: Names,
377
+ localStorage: Names
378
+ }).optional()
379
+ });
380
+ Envelope = z14.object({
381
+ publicKey: z14.string(),
382
+ revision: z14.number(),
383
+ ruleKey: z14.string(),
384
+ profileKey: z14.string(),
385
+ trustedSignals: z14.object({
386
+ region: z14.object({
387
+ countryCode: z14.string().nullable(),
388
+ subdivisionCode: z14.string().nullable()
389
+ }).optional()
390
+ }).optional(),
391
+ config: z14.object({
392
+ services: z14.array(Service),
393
+ categories: z14.array(z14.object({ key: z14.string(), required: z14.boolean() })),
394
+ integrations: z14.object({ googleConsentMode: z14.string() })
395
+ })
396
+ });
397
+ State = z14.object({
398
+ runtimeVersion: z14.string(),
399
+ preview: z14.boolean(),
400
+ resolved: z14.object({
401
+ publicKey: z14.string(),
402
+ revision: z14.number(),
403
+ ruleKey: z14.string(),
404
+ profileKey: z14.string()
405
+ }).nullable(),
406
+ consent: z14.object({
407
+ categories: z14.array(z14.string()),
408
+ decision: z14.string(),
409
+ globalPrivacyControl: z14.boolean(),
410
+ privacyChoices: z14.object({
411
+ saleOrShareOptOut: z14.boolean(),
412
+ targetedAdvertisingOptOut: z14.boolean()
413
+ })
414
+ }),
415
+ services: z14.array(
416
+ z14.object({
417
+ key: z14.string(),
418
+ allowed: z14.boolean(),
419
+ footprintPresent: z14.boolean()
420
+ })
421
+ )
422
+ });
423
+ VerificationInstallation = z14.object({
424
+ publicKey: z14.string(),
425
+ currentPublishedRevision: z14.number().int().positive(),
426
+ delivery: z14.object({
427
+ runtimeUrl: z14.string().url(),
428
+ configUrl: z14.string().url(),
429
+ analyticsUrl: z14.string().url().nullable(),
430
+ receiptsUrl: z14.string().url(),
431
+ handshakeUrl: z14.string().url()
432
+ })
433
+ });
434
+ }
435
+ });
2
436
 
3
437
  // src/health-check.ts
4
438
  import path10 from "path";
@@ -49,7 +483,8 @@ var siteOSServiceAudienceSchema = z.enum([
49
483
  "siteos-cookie",
50
484
  "siteos-forms",
51
485
  "siteos-pulse",
52
- "siteos-search"
486
+ "siteos-search",
487
+ "siteos-seo"
53
488
  ]);
54
489
  var siteOSServiceScopeSchema = z.string().min(1).max(160).regex(/^[a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*){2,}$/);
55
490
  var scopeNamespaceByAudience = {
@@ -59,7 +494,8 @@ var scopeNamespaceByAudience = {
59
494
  "siteos-cookie": "cookie",
60
495
  "siteos-forms": "forms",
61
496
  "siteos-pulse": "pulse",
62
- "siteos-search": "search"
497
+ "siteos-search": "search",
498
+ "siteos-seo": "seo"
63
499
  };
64
500
  function matchingServiceScopes(value, context) {
65
501
  const prefix = `${scopeNamespaceByAudience[value.audience]}:`;
@@ -1008,7 +1444,8 @@ var PROJECT_SERVICES = [
1008
1444
  "cookie",
1009
1445
  "forms",
1010
1446
  "search",
1011
- "trace"
1447
+ "trace",
1448
+ "seo"
1012
1449
  ];
1013
1450
  var ProjectSchema = z4.object({
1014
1451
  id: z4.string().min(1),
@@ -1062,7 +1499,7 @@ var OverviewSchema = z4.object({
1062
1499
  });
1063
1500
  function createProjectApi(input) {
1064
1501
  const origin = resolveSiteOSAuthBaseUrl(input.env);
1065
- async function request(path29, schema, body, method) {
1502
+ async function request(path30, schema, body, method) {
1066
1503
  const scope = body === void 0 ? "projects:workspace:read" : "projects:workspace:write";
1067
1504
  const grant = await input.grants.acquire({
1068
1505
  audience: "siteos-projects",
@@ -1076,7 +1513,7 @@ function createProjectApi(input) {
1076
1513
  message: "SiteOS API access is unavailable."
1077
1514
  });
1078
1515
  const response = await input.fetchImpl(
1079
- `${origin}/api/projects/v1/projects${path29}`,
1516
+ `${origin}/api/projects/v1/projects${path30}`,
1080
1517
  {
1081
1518
  method: method ?? (body === void 0 ? "GET" : "POST"),
1082
1519
  headers: {
@@ -1272,14 +1709,14 @@ async function commonServiceContext(options, service, environmentSlug) {
1272
1709
  const binding = attachment.environments.find(
1273
1710
  (item) => item.environmentId === context.environment.id
1274
1711
  );
1275
- if (["pulse", "cookie"].includes(service) && !binding)
1712
+ if (["pulse", "cookie", "seo"].includes(service) && !binding)
1276
1713
  throw new SiteOSAuthApiError({
1277
1714
  code: "PROJECT_ENVIRONMENT_NOT_CONFIGURED",
1278
1715
  message: `Set up ${service} in ${context.environment.name} with \`siteos project connect ${service}\`.`
1279
1716
  });
1280
1717
  return {
1281
1718
  ...context,
1282
- resourceId: ["pulse", "cookie"].includes(service) ? binding.resourceId : attachment.resourceId,
1719
+ resourceId: ["pulse", "cookie", "seo"].includes(service) ? binding.resourceId : attachment.resourceId,
1283
1720
  environmentBinding: binding
1284
1721
  };
1285
1722
  }
@@ -8318,13 +8755,13 @@ Usage:
8318
8755
  siteos project use <id-or-slug> [--json]
8319
8756
  siteos project update [--name <name>] [--slug <slug>] [--url <production-url>] [--json]
8320
8757
  siteos project status [--json]
8321
- siteos project connect <pulse|cookie|forms|search|trace> [--resource <id>] [--json]
8758
+ siteos project connect <pulse|cookie|forms|search|trace|seo> [--resource <id>] [--json]
8322
8759
  siteos project environment list [--json]
8323
8760
  siteos project environment create --name <name> --slug <slug> [--url <url>] [--json]
8324
8761
  siteos project environment use <slug> [--json]
8325
8762
  siteos project environment update <slug> [--name <name>] [--url <url>] [--json]
8326
- siteos project environment resources <pulse|cookie|forms|search|trace> [--json]
8327
- 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]
8328
8765
 
8329
8766
  Select a Project once per repository. Service commands use its configured resources.
8330
8767
  Connect creates a draft workspace, or explicitly attaches an existing resource.
@@ -8422,7 +8859,7 @@ Run \`siteos project status\` to inspect its services.`;
8422
8859
  const service = parsed.positionals[0];
8423
8860
  if (!PROJECT_SERVICES.includes(service))
8424
8861
  throw new Error(
8425
- "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`. "
8426
8863
  );
8427
8864
  overview = await api.connect(
8428
8865
  overview.project.id,
@@ -8478,7 +8915,7 @@ async function runEnvironmentCommand3(options) {
8478
8915
  throw new Error("Environment create requires --name and --slug.");
8479
8916
  const service = positionals[0];
8480
8917
  if ((action === "connect" || action === "resources") && !PROJECT_SERVICES.includes(service))
8481
- throw new Error("Choose pulse, cookie, forms, search or trace.");
8918
+ throw new Error("Choose pulse, cookie, forms, search, trace or seo.");
8482
8919
  if (action === "connect" && !values.environment)
8483
8920
  throw new Error("Choose a Project environment with --environment <slug>.");
8484
8921
  const context = await readCommonProject(
@@ -8563,13 +9000,18 @@ async function runEnvironmentCommand3(options) {
8563
9000
  import { readFile as readFile16, stat } from "fs/promises";
8564
9001
  import path28 from "path";
8565
9002
  import { parseArgs as parseArgs3 } from "util";
8566
- import { z as z14 } from "zod";
9003
+ import { z as z15 } from "zod";
8567
9004
  var SERVICE_HELP = {
8568
9005
  cookie: `Manage Cookie for the selected SiteOS Project.
8569
9006
 
8570
9007
  Usage:
8571
9008
  siteos cookie status [--json]
8572
9009
  siteos cookie installation [--json]
9010
+ siteos cookie schema [--json]
9011
+ siteos cookie validate --input <draft.json> [--json]
9012
+ siteos cookie regions resolve [--country <ISO>] [--subdivision <code>] [--source <draft|published>] [--json]
9013
+ siteos cookie verify [--url <same-origin-url>] [--browser <chromium|webkit>] [--json]
9014
+ siteos cookie restore --input <restore.json> [--json]
8573
9015
  siteos cookie draft get [--json]
8574
9016
  siteos cookie draft save --input <draft.json> [--json]
8575
9017
  siteos cookie publish --input <publication.json> [--json]
@@ -8618,7 +9060,12 @@ async function runServiceCommand(service, options) {
8618
9060
  "range-days": { type: "string" },
8619
9061
  query: { type: "string" },
8620
9062
  cursor: { type: "string" },
8621
- channel: { type: "string" }
9063
+ channel: { type: "string" },
9064
+ country: { type: "string" },
9065
+ subdivision: { type: "string" },
9066
+ source: { type: "string" },
9067
+ url: { type: "string" },
9068
+ browser: { type: "string" }
8622
9069
  }
8623
9070
  });
8624
9071
  const [action, subaction] = positionals;
@@ -8627,6 +9074,11 @@ async function runServiceCommand(service, options) {
8627
9074
  cookie: {
8628
9075
  status: [],
8629
9076
  installation: [],
9077
+ schema: [],
9078
+ validate: ["input"],
9079
+ "regions resolve": ["country", "subdivision", "source"],
9080
+ verify: ["url", "browser"],
9081
+ restore: ["input"],
8630
9082
  "draft get": [],
8631
9083
  "draft save": ["input"],
8632
9084
  publish: ["input"],
@@ -8706,8 +9158,8 @@ async function runServiceCommand(service, options) {
8706
9158
  );
8707
9159
  const result2 = await response.json();
8708
9160
  if (!response.ok) {
8709
- const error = z14.object({
8710
- error: z14.object({ code: z14.string(), message: z14.string().max(500) })
9161
+ const error = z15.object({
9162
+ error: z15.object({ code: z15.string(), message: z15.string().max(500) })
8711
9163
  }).safeParse(result2);
8712
9164
  throw new SiteOSAuthApiError({
8713
9165
  code: error.success ? error.data.error.code : "SERVICE_REQUEST_FAILED",
@@ -8731,14 +9183,60 @@ async function runServiceCommand(service, options) {
8731
9183
  if (service === "cookie") {
8732
9184
  if (action === "status" && positionals.length === 1)
8733
9185
  result = await request(site);
8734
- else if (action === "installation")
9186
+ else if (action === "schema") result = await request(`${site}/schema`);
9187
+ else if (action === "validate") {
9188
+ result = await request(
9189
+ `${site}/validate`,
9190
+ "POST",
9191
+ await inputFile(),
9192
+ "cookie:workspace:read"
9193
+ );
9194
+ const validation = z15.object({
9195
+ valid: z15.boolean(),
9196
+ draftVersionMatches: z15.boolean().optional()
9197
+ }).parse(result);
9198
+ if (!validation.valid || validation.draftVersionMatches === false)
9199
+ return { exitCode: 2, stdout: JSON.stringify(result, null, 2) };
9200
+ } else if (action === "restore")
9201
+ result = await request(`${site}/restore`, "POST", await inputFile());
9202
+ else if (action === "regions") {
9203
+ const query = new URLSearchParams();
9204
+ for (const key of ["country", "subdivision", "source"])
9205
+ if (values[key]) query.set(key, values[key]);
9206
+ result = await request(`${site}/regions?${query}`);
9207
+ } else if (action === "verify") {
9208
+ const {
9209
+ VerificationInstallation: VerificationInstallation2,
9210
+ verificationTarget: verificationTarget2,
9211
+ verifyCookieWebsite: verifyCookieWebsite2
9212
+ } = await Promise.resolve().then(() => (init_cookie_verification(), cookie_verification_exports));
9213
+ const browserName = values.browser ?? "chromium";
9214
+ if (browserName !== "chromium" && browserName !== "webkit")
9215
+ throw new Error("Choose chromium or webkit.");
9216
+ if (!common.environment.url)
9217
+ throw new Error(
9218
+ "Set the selected Project environment URL before verification."
9219
+ );
9220
+ const url = verificationTarget2(common.environment.url, values.url);
9221
+ result = await verifyCookieWebsite2({
9222
+ installation: VerificationInstallation2.parse(
9223
+ await request(`${site}/installation`)
9224
+ ),
9225
+ url,
9226
+ browserName
9227
+ });
9228
+ return {
9229
+ exitCode: result.status === "passed" ? 0 : 1,
9230
+ stdout: JSON.stringify(result, null, 2)
9231
+ };
9232
+ } else if (action === "installation")
8735
9233
  result = await request(`${site}/installation`);
8736
9234
  else if (action === "draft" && subaction === "get" && positionals.length === 2) {
8737
- const response = z14.object({
8738
- name: z14.string(),
8739
- hostname: z14.string(),
8740
- draftVersion: z14.number(),
8741
- draft: z14.unknown()
9235
+ const response = z15.object({
9236
+ name: z15.string(),
9237
+ hostname: z15.string(),
9238
+ draftVersion: z15.number(),
9239
+ draft: z15.unknown()
8742
9240
  }).parse(await request(site));
8743
9241
  result = {
8744
9242
  name: response.name,
@@ -8770,9 +9268,9 @@ async function runServiceCommand(service, options) {
8770
9268
  else if (action === "environments" && positionals.length === 1)
8771
9269
  result = await request(`${site}/environments`);
8772
9270
  else if (action === "report" || action === "installation" || action === "tracking-plan") {
8773
- const response = z14.object({
8774
- environments: z14.array(
8775
- z14.object({ id: z14.string(), slug: z14.string() })
9271
+ const response = z15.object({
9272
+ environments: z15.array(
9273
+ z15.object({ id: z15.string(), slug: z15.string() })
8776
9274
  )
8777
9275
  }).parse(await request(`${site}/environments`));
8778
9276
  const environment = common.overview.project.environments.find(
@@ -8797,11 +9295,11 @@ async function runServiceCommand(service, options) {
8797
9295
  );
8798
9296
  const base = `/environments/${encodeURIComponent(matches[0].id)}`;
8799
9297
  if (action === "report" && positionals.length === 1) {
8800
- const detail = z14.object({
8801
- site: z14.object({
8802
- id: z14.literal(common.resourceId),
8803
- environments: z14.array(
8804
- z14.object({ id: z14.string(), evidence: z14.unknown() })
9298
+ const detail = z15.object({
9299
+ site: z15.object({
9300
+ id: z15.literal(common.resourceId),
9301
+ environments: z15.array(
9302
+ z15.object({ id: z15.string(), evidence: z15.unknown() })
8805
9303
  )
8806
9304
  })
8807
9305
  }).parse(await request(site));
@@ -8859,7 +9357,383 @@ async function runServiceCommand(service, options) {
8859
9357
  } catch (cause) {
8860
9358
  return {
8861
9359
  exitCode: cause instanceof SiteOSAuthApiError ? 1 : 2,
8862
- stderr: cause instanceof SiteOSAuthApiError || !(cause instanceof z14.ZodError) && cause instanceof Error ? cause.message : "The service returned an invalid response."
9360
+ stderr: cause instanceof SiteOSAuthApiError || !(cause instanceof z15.ZodError) && cause instanceof Error ? cause.message : "The service returned an invalid response."
9361
+ };
9362
+ }
9363
+ }
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 }
8863
9737
  };
8864
9738
  }
8865
9739
  }
@@ -8897,6 +9771,12 @@ if (!command || command === "--help" || command === "-h") {
8897
9771
  }
8898
9772
  function rootCommandRegistry() {
8899
9773
  return createCommandRegistry({
9774
+ seo: (args2) => runSeoCommand({
9775
+ args: args2,
9776
+ cwd: process.cwd(),
9777
+ env: process.env,
9778
+ fetchImpl: globalThis.fetch
9779
+ }),
8900
9780
  "health-check": runHealthCheck,
8901
9781
  project: (args2) => runProjectCommand4({
8902
9782
  args: args2,
@@ -9004,6 +9884,7 @@ Usage:
9004
9884
  siteos project --help
9005
9885
  siteos cookie --help
9006
9886
  siteos trace --help
9887
+ siteos seo --help
9007
9888
  siteos integrations --help
9008
9889
  siteos pulse --help
9009
9890
  siteos search --help
@@ -9015,6 +9896,7 @@ Commands:
9015
9896
  project Select one Project and configure its services and environments.
9016
9897
  cookie Configure, publish, and inspect the Project\u2019s cookie banner.
9017
9898
  trace Configure analytics observation and inspect evidence.
9899
+ seo Audit HTML, inspect changes and verify fixes.
9018
9900
  integrations Manage Organization connections and destinations.
9019
9901
  pulse Manage monitoring checks, tests, and deployments.
9020
9902
  search Run SiteOS search operations for a project environment.