@siteoshq/cli 1.2.0 → 1.4.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
@@ -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
  }
@@ -2933,6 +2936,8 @@ function isCliExitCode(value) {
2933
2936
  import { readFile as readFile5 } from "fs/promises";
2934
2937
  import path12 from "path";
2935
2938
  import { z as z10 } from "zod";
2939
+ import { Ajv } from "ajv";
2940
+ import addFormats from "ajv-formats";
2936
2941
 
2937
2942
  // src/siteos-forms-api.ts
2938
2943
  import { z as z9 } from "zod";
@@ -2968,12 +2973,82 @@ var formDefinitionSyncResponseSchema = z9.object({
2968
2973
  success: z9.literal(true),
2969
2974
  version: z9.number().int().positive()
2970
2975
  }).strict();
2976
+ var definitionSchema = z9.object({
2977
+ id: opaqueIdentifierSchema,
2978
+ environmentId: opaqueIdentifierSchema,
2979
+ formKey: opaqueIdentifierSchema,
2980
+ name: z9.string(),
2981
+ sourcePagePath: z9.string().nullable(),
2982
+ activeVersion: z9.number().int().positive(),
2983
+ activeVersionId: opaqueIdentifierSchema,
2984
+ status: z9.enum(["active", "inactive"]),
2985
+ submissionCount: z9.number().int().nonnegative(),
2986
+ lastSubmittedAt: z9.string().datetime({ offset: true }).nullable(),
2987
+ createdAt: z9.string().datetime({ offset: true }),
2988
+ updatedAt: z9.string().datetime({ offset: true })
2989
+ });
2990
+ var definitionListSchema = z9.object({
2991
+ definitions: z9.array(definitionSchema)
2992
+ });
2993
+ var definitionContextSchema = z9.object({
2994
+ definition: definitionSchema,
2995
+ revision: z9.number().int().positive(),
2996
+ canManage: z9.boolean(),
2997
+ project: z9.object({ id: opaqueIdentifierSchema, name: z9.string() }),
2998
+ environment: z9.object({
2999
+ id: opaqueIdentifierSchema,
3000
+ name: z9.string(),
3001
+ slug: z9.string()
3002
+ })
3003
+ });
3004
+ var definitionChangeResponseSchema = z9.object({
3005
+ deleted: z9.boolean(),
3006
+ formId: opaqueIdentifierSchema
3007
+ });
2971
3008
  var formSubmissionResponseSchema = z9.object({
2972
3009
  formId: opaqueIdentifierSchema,
2973
3010
  receivedAt: z9.string().datetime({ offset: true }),
2974
3011
  submissionId: opaqueIdentifierSchema,
2975
3012
  success: z9.literal(true)
2976
3013
  }).strict();
3014
+ var formsSubmissionStatusSchema = z9.enum([
3015
+ "new",
3016
+ "read",
3017
+ "archived",
3018
+ "spam"
3019
+ ]);
3020
+ var submissionSummarySchema = z9.object({
3021
+ id: opaqueIdentifierSchema,
3022
+ formId: opaqueIdentifierSchema,
3023
+ normalized: z9.record(
3024
+ z9.union([z9.string(), z9.number(), z9.boolean(), z9.null()])
3025
+ ),
3026
+ status: formsSubmissionStatusSchema,
3027
+ submittedAt: z9.string().datetime({ offset: true })
3028
+ });
3029
+ var submissionDetailSchema = submissionSummarySchema.extend({
3030
+ payload: z9.record(z9.unknown()),
3031
+ fields: z9.array(
3032
+ z9.object({
3033
+ key: z9.string(),
3034
+ label: z9.string().optional(),
3035
+ kind: z9.string().optional(),
3036
+ displayRole: z9.enum(["primary", "secondary"]).optional(),
3037
+ multiple: z9.boolean().optional(),
3038
+ options: z9.array(z9.object({ value: z9.string(), label: z9.string() })).optional()
3039
+ })
3040
+ ),
3041
+ version: z9.number().int().positive(),
3042
+ versionId: opaqueIdentifierSchema
3043
+ });
3044
+ var submissionPageSchema = z9.object({
3045
+ submissions: z9.array(submissionSummarySchema),
3046
+ total: z9.number().int().nonnegative(),
3047
+ nextCursor: z9.string().nullable()
3048
+ });
3049
+ var submissionDetailResponseSchema = z9.object({
3050
+ submission: submissionDetailSchema
3051
+ });
2977
3052
  var formsCredentialMetadataSchema = z9.object({
2978
3053
  createdAt: z9.string().datetime({ offset: true }),
2979
3054
  id: z9.string().trim().min(1).max(255),
@@ -2990,6 +3065,9 @@ var formsCredentialExchangeSchema = formsCredentialMetadataSchema.extend({
2990
3065
  token: z9.string().regex(/^pfs_[A-Za-z0-9_-]{22}$/)
2991
3066
  });
2992
3067
  var formsErrorCodeSchema = z9.enum([
3068
+ "AUTHENTICATION_REQUIRED",
3069
+ "FORBIDDEN",
3070
+ "CONFLICT",
2993
3071
  "INVALID_REQUEST",
2994
3072
  "INVALID_INPUT",
2995
3073
  "VALIDATION_FAILED",
@@ -2997,6 +3075,8 @@ var formsErrorCodeSchema = z9.enum([
2997
3075
  "UNAUTHORIZED",
2998
3076
  "IDENTITY_REQUIRED",
2999
3077
  "NOT_ALLOWED",
3078
+ "FORM_ARCHIVED",
3079
+ "FORM_DELETED",
3000
3080
  "NOT_FOUND",
3001
3081
  "PROJECT_NOT_FOUND",
3002
3082
  "PROJECT_CONFLICT",
@@ -3009,6 +3089,9 @@ var formsErrorCodeSchema = z9.enum([
3009
3089
  "INTERNAL_ERROR"
3010
3090
  ]);
3011
3091
  var safeFormsErrorMessages = {
3092
+ AUTHENTICATION_REQUIRED: "Sign in to SiteOS before reading Forms submissions.",
3093
+ FORBIDDEN: "Forms management access is denied.",
3094
+ CONFLICT: "The resource changed or this idempotency key belongs to different data. Read the current state before retrying.",
3012
3095
  INVALID_REQUEST: "The Forms request is invalid.",
3013
3096
  INVALID_INPUT: "Invalid input data.",
3014
3097
  VALIDATION_FAILED: "Validation failed.",
@@ -3016,6 +3099,8 @@ var safeFormsErrorMessages = {
3016
3099
  UNAUTHORIZED: "Forms management authorization is invalid.",
3017
3100
  IDENTITY_REQUIRED: "Forms browser identity is required.",
3018
3101
  NOT_ALLOWED: "Forms management access is denied.",
3102
+ FORM_ARCHIVED: "This form is archived. Restore it explicitly before syncing or sending new submissions.",
3103
+ FORM_DELETED: "This form was permanently deleted. Use a new form key for a new form.",
3019
3104
  NOT_FOUND: "The requested resource was not found.",
3020
3105
  PROJECT_NOT_FOUND: "The Forms Project was not found.",
3021
3106
  PROJECT_CONFLICT: "The Forms Project already exists.",
@@ -3050,7 +3135,59 @@ function resolveSiteOSFormsBaseUrl(env = process.env) {
3050
3135
  }
3051
3136
  function createSiteOSFormsApiClient(options) {
3052
3137
  const apiBaseUrl = normalizeFormsOrigin(options.apiBaseUrl);
3138
+ const inboxPath = (input) => `/api/v1/forms/environments/${encodeURIComponent(input.environmentId)}/forms/${encodeURIComponent(input.formId)}/submissions`;
3139
+ const definitionPath = (input) => `/api/v1/forms/environments/${encodeURIComponent(input.environmentId)}/forms/${encodeURIComponent(input.formId)}`;
3053
3140
  return {
3141
+ listDefinitions: (input) => requestJson2({
3142
+ apiBaseUrl,
3143
+ fetchImpl: options.fetchImpl,
3144
+ headers: managementHeaders(input),
3145
+ method: "GET",
3146
+ path: `/api/v1/forms/environments/${encodeURIComponent(input.environmentId)}/definitions?status=${input.status}`,
3147
+ responseSchema: definitionListSchema
3148
+ }),
3149
+ readDefinition: (input) => requestJson2({
3150
+ apiBaseUrl,
3151
+ fetchImpl: options.fetchImpl,
3152
+ headers: managementHeaders(input),
3153
+ method: "GET",
3154
+ path: definitionPath(input),
3155
+ responseSchema: definitionContextSchema
3156
+ }),
3157
+ changeDefinition: (input) => requestJson2({
3158
+ apiBaseUrl,
3159
+ fetchImpl: options.fetchImpl,
3160
+ headers: managementHeaders(input),
3161
+ method: "PATCH",
3162
+ path: definitionPath(input),
3163
+ body: input.change,
3164
+ responseSchema: definitionChangeResponseSchema
3165
+ }),
3166
+ listSubmissions: (input) => requestJson2({
3167
+ apiBaseUrl,
3168
+ fetchImpl: options.fetchImpl,
3169
+ headers: managementHeaders(input),
3170
+ method: "GET",
3171
+ path: `${inboxPath(input)}?${input.filters}`,
3172
+ responseSchema: submissionPageSchema
3173
+ }),
3174
+ readSubmission: (input) => requestJson2({
3175
+ apiBaseUrl,
3176
+ fetchImpl: options.fetchImpl,
3177
+ headers: managementHeaders(input),
3178
+ method: "GET",
3179
+ path: `${inboxPath(input)}/${encodeURIComponent(input.submissionId)}`,
3180
+ responseSchema: submissionDetailResponseSchema
3181
+ }),
3182
+ updateSubmissionStatus: (input) => requestJson2({
3183
+ apiBaseUrl,
3184
+ fetchImpl: options.fetchImpl,
3185
+ headers: managementHeaders(input),
3186
+ method: "PATCH",
3187
+ path: `${inboxPath(input)}/${encodeURIComponent(input.submissionId)}`,
3188
+ body: { status: input.status, expectedStatus: input.expectedStatus },
3189
+ responseSchema: submissionDetailResponseSchema
3190
+ }),
3054
3191
  createProject: (input) => requestJson2({
3055
3192
  apiBaseUrl,
3056
3193
  body: { name: input.name, slug: input.slug },
@@ -3404,25 +3541,40 @@ var SLUG_FLAG = "--slug";
3404
3541
  var CREDENTIAL_FLAG = "--credential";
3405
3542
  var REPLACE_FLAG = "--replace";
3406
3543
  var normalizedFieldOptionSchema = z10.object({
3407
- label: z10.string().trim().min(1),
3408
- value: z10.string().trim().min(1)
3544
+ label: z10.string().trim().min(1).max(255),
3545
+ value: z10.string().trim().min(1).max(255)
3409
3546
  });
3410
3547
  var normalizedFieldSchema = z10.object({
3411
- key: z10.string().trim().min(1),
3412
- kind: z10.string().trim().min(1).optional(),
3413
- label: z10.string().trim().min(1).optional(),
3548
+ key: z10.string().trim().min(1).max(255),
3549
+ kind: z10.string().trim().min(1).max(64).optional(),
3550
+ label: z10.string().trim().min(1).max(255).optional(),
3414
3551
  displayRole: z10.enum(["primary", "secondary"]).optional(),
3415
3552
  multiple: z10.boolean().optional(),
3416
- options: z10.array(normalizedFieldOptionSchema).optional()
3417
- }).passthrough();
3553
+ options: z10.array(normalizedFieldOptionSchema).max(500).optional()
3554
+ }).strict();
3418
3555
  var definitionSyncInputSchema = z10.object({
3419
- formKey: z10.string().trim().min(1),
3420
- name: z10.string().trim().min(1),
3421
- normalizedFieldsJson: z10.array(normalizedFieldSchema).min(1),
3556
+ formKey: z10.string().trim().min(1).max(255),
3557
+ name: z10.string().trim().min(1).max(255),
3558
+ normalizedFieldsJson: z10.array(normalizedFieldSchema).min(1).max(500),
3422
3559
  schemaJson: z10.record(z10.unknown()),
3423
- sourceExportId: z10.string().trim().min(1).optional(),
3424
- sourcePagePath: z10.string().trim().min(1).optional()
3425
- }).passthrough().superRefine((definition, context) => {
3560
+ sourceExportId: z10.string().trim().min(1).max(255).optional(),
3561
+ sourcePagePath: z10.string().trim().min(1).max(2048).optional()
3562
+ }).strict().superRefine((definition, context) => {
3563
+ const ajv = new Ajv({
3564
+ allErrors: true,
3565
+ removeAdditional: false,
3566
+ strict: true
3567
+ });
3568
+ addFormats.default(ajv);
3569
+ try {
3570
+ ajv.compile(definition.schemaJson);
3571
+ } catch {
3572
+ context.addIssue({
3573
+ code: z10.ZodIssueCode.custom,
3574
+ message: "schemaJson must be an executable JSON Schema supported by SiteOS Forms.",
3575
+ path: ["schemaJson"]
3576
+ });
3577
+ }
3426
3578
  const fields = definition.normalizedFieldsJson;
3427
3579
  const duplicateKeys = fields.map((field) => field.key).filter((key, index, keys) => keys.indexOf(key) !== index);
3428
3580
  for (const key of new Set(duplicateKeys)) {
@@ -3503,6 +3655,11 @@ var FORMS_HELP = `Usage:
3503
3655
  siteos forms environment create --slug <slug> --name <name> [--json]
3504
3656
  siteos forms definition sync --environment <slug> --input <path> [--json]
3505
3657
  siteos forms definition sync --environment <slug> --manifest <path> [--json]
3658
+ siteos forms definition list --environment <slug> [--status <active|inactive|all>] [--json]
3659
+ siteos forms definition read --environment <slug> --form <form-id> [--json]
3660
+ siteos forms definition archive --environment <slug> --form <form-id> --expected-revision <revision> [--json]
3661
+ siteos forms definition restore --environment <slug> --form <form-id> --expected-revision <revision> [--json]
3662
+ siteos forms definition delete --environment <slug> --form <form-id> [--apply --confirm <form-key> --expected-revision <revision> --expected-submissions <count>] [--json]
3506
3663
  siteos forms definition check --input <path> [--json]
3507
3664
  siteos forms definition check --manifest <path> [--json]
3508
3665
  siteos forms credential list --environment <slug> [--json]
@@ -3510,6 +3667,9 @@ var FORMS_HELP = `Usage:
3510
3667
  siteos forms credential rotate --environment <slug> --install [--name <name>] [--json]
3511
3668
  siteos forms credential revoke --environment <slug> --credential <credential-id> [--json]
3512
3669
  siteos forms credentials issue --environment <slug> [--name <name>] [--json]
3670
+ siteos forms submissions list --environment <slug> --form <form-id> [--query <text>] [--status <status>] [--from <ISO>] [--to <ISO>] [--limit <1-100>] [--cursor <cursor>] [--json]
3671
+ siteos forms submissions read --environment <slug> --form <form-id> --submission <id> [--json]
3672
+ siteos forms submissions status --environment <slug> --form <form-id> --submission <id> --status <new|read|archived|spam> --expected-status <status> [--json]
3513
3673
  siteos forms submit --input <path> [--json]
3514
3674
 
3515
3675
  Manage SiteOS Forms Environments, definitions, credentials, and submission smoke tests.`;
@@ -3531,6 +3691,8 @@ async function runFormsCommand(options) {
3531
3691
  };
3532
3692
  }
3533
3693
  }
3694
+ if (args2[0] === "submissions")
3695
+ return runSubmissionsCommand({ ...options, args: args2.slice(1) });
3534
3696
  if (args2[0] === "definition") {
3535
3697
  return runDefinitionCommand({
3536
3698
  ...options,
@@ -3574,6 +3736,96 @@ async function runFormsCommand(options) {
3574
3736
  }
3575
3737
  return usageError2("Unknown SiteOS forms command.");
3576
3738
  }
3739
+ async function runSubmissionsCommand(options) {
3740
+ const action = options.args[0];
3741
+ if (!action || !["list", "read", "status"].includes(action))
3742
+ return usageError2("Unknown Forms submissions command.");
3743
+ const parsed = parseFlags(options.args.slice(1), {
3744
+ allowed: /* @__PURE__ */ new Set([
3745
+ "--environment",
3746
+ "--form",
3747
+ "--json",
3748
+ ...action === "list" ? ["--query", "--status", "--from", "--to", "--limit", "--cursor"] : [
3749
+ "--submission",
3750
+ ...action === "status" ? ["--status", "--expected-status"] : []
3751
+ ]
3752
+ ]),
3753
+ boolean: /* @__PURE__ */ new Set(["--json"]),
3754
+ rejectDuplicates: true
3755
+ });
3756
+ if (!parsed.ok) return usageError2(parsed.error);
3757
+ if (parsed.positionals.length)
3758
+ return usageError2("Unexpected positional arguments.");
3759
+ const value = (key) => readStringFlag(parsed.flags, `--${key}`);
3760
+ const environmentSlug = value("environment");
3761
+ const formId = value("form");
3762
+ const submissionId = value("submission");
3763
+ if (!environmentSlug || !isValidEnvironmentSlug(environmentSlug) || !formId || formId.length > 255)
3764
+ return usageError2("Provide a valid --environment and --form.");
3765
+ if (action !== "list" && (!submissionId || submissionId.length > 255))
3766
+ return usageError2("Provide --submission.");
3767
+ const status = formsSubmissionStatusSchema.safeParse(value("status"));
3768
+ const expected = formsSubmissionStatusSchema.safeParse(
3769
+ value("expected-status")
3770
+ );
3771
+ if (value("status") && !status.success || action === "status" && (!status.success || !expected.success))
3772
+ return usageError2(
3773
+ "Use new, read, archived or spam for --status and --expected-status."
3774
+ );
3775
+ const filters = new URLSearchParams();
3776
+ for (const key of ["query", "status", "from", "to", "limit", "cursor"]) {
3777
+ const item = value(key);
3778
+ if (item) filters.set(key, item);
3779
+ }
3780
+ const limit = Number(value("limit") ?? 50);
3781
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
3782
+ return usageError2("--limit must be between 1 and 100.");
3783
+ if ((value("query")?.length ?? 0) > 200 || (value("cursor")?.length ?? 0) > 2048)
3784
+ return usageError2("The query or cursor is too long.");
3785
+ for (const key of ["from", "to"])
3786
+ if (value(key) && !z10.string().datetime({ offset: true }).safeParse(value(key)).success)
3787
+ return usageError2(
3788
+ `--${key} must be an ISO timestamp including a timezone.`
3789
+ );
3790
+ if (value("from") && value("to") && Date.parse(value("from")) >= Date.parse(value("to")))
3791
+ return usageError2("--from must be earlier than --to (exclusive).");
3792
+ const context = await loadFormsManagementContext(
3793
+ options,
3794
+ "forms:workspace:write"
3795
+ );
3796
+ if (!context.ok) return context.error;
3797
+ try {
3798
+ const { environments } = await context.client.listEnvironments({
3799
+ grant: context.grant,
3800
+ projectId: context.project.id
3801
+ });
3802
+ requireEnvironmentProjectConvergence(environments, context.project.id);
3803
+ const environment = environments.find(
3804
+ (item) => item.slug === environmentSlug
3805
+ );
3806
+ if (!environment)
3807
+ return usageError2(
3808
+ "The selected Forms Environment does not exist. Run forms environment list."
3809
+ );
3810
+ const input = {
3811
+ grant: context.grant,
3812
+ environmentId: environment.id,
3813
+ formId
3814
+ };
3815
+ const response = action === "list" ? await context.client.listSubmissions({ ...input, filters }) : action === "read" ? await context.client.readSubmission({
3816
+ ...input,
3817
+ submissionId
3818
+ }) : await context.client.updateSubmissionStatus({
3819
+ ...input,
3820
+ submissionId,
3821
+ status: status.data,
3822
+ expectedStatus: expected.data
3823
+ });
3824
+ return { exitCode: 0, stdout: stringifySafeJson(response) };
3825
+ } catch (error) {
3826
+ return formatApiError(error);
3827
+ }
3828
+ }
3577
3829
  async function runProjectCommand(options) {
3578
3830
  const action = options.args[0];
3579
3831
  if (!action || !["create", "list", "status", "use"].includes(action)) {
@@ -4014,6 +4266,10 @@ async function requireFormsProjectReference(cwd) {
4014
4266
  return reference;
4015
4267
  }
4016
4268
  async function runDefinitionCommand(options) {
4269
+ if (["list", "read", "archive", "restore", "delete"].includes(
4270
+ options.args[0] ?? ""
4271
+ ))
4272
+ return runDefinitionLifecycle(options);
4017
4273
  if (options.args[0] === "sync") {
4018
4274
  return runDefinitionSync({
4019
4275
  ...options,
@@ -4028,6 +4284,125 @@ async function runDefinitionCommand(options) {
4028
4284
  }
4029
4285
  return usageError2("Unknown SiteOS forms definition command.");
4030
4286
  }
4287
+ async function runDefinitionLifecycle(options) {
4288
+ const action = options.args[0];
4289
+ const parsed = parseFlags(options.args.slice(1), {
4290
+ allowed: /* @__PURE__ */ new Set([
4291
+ "--environment",
4292
+ "--json",
4293
+ ...action === "list" ? ["--status"] : ["--form"],
4294
+ ...["archive", "restore", "delete"].includes(action) ? ["--expected-revision"] : [],
4295
+ ...action === "delete" ? ["--apply", "--confirm", "--expected-submissions"] : []
4296
+ ]),
4297
+ boolean: /* @__PURE__ */ new Set(["--json", "--apply"]),
4298
+ rejectDuplicates: true
4299
+ });
4300
+ if (!parsed.ok) return usageError2(parsed.error);
4301
+ if (parsed.positionals.length)
4302
+ return usageError2("Unexpected positional arguments.");
4303
+ const value = (key) => readStringFlag(parsed.flags, `--${key}`);
4304
+ const environmentSlug = value("environment");
4305
+ const formId = value("form");
4306
+ if (!environmentSlug || !isValidEnvironmentSlug(environmentSlug) || action !== "list" && (!formId || formId.length > 255))
4307
+ return usageError2("Provide a valid --environment and --form.");
4308
+ const status = value("status") ?? "active";
4309
+ if (!["active", "inactive", "all"].includes(status))
4310
+ return usageError2("Use active, inactive or all for --status.");
4311
+ const apply = parsed.flags.get("--apply") === true;
4312
+ const revision = Number(value("expected-revision"));
4313
+ const count = Number(value("expected-submissions"));
4314
+ const confirmKey = value("confirm");
4315
+ if ((action === "archive" || action === "restore" || apply) && (!Number.isSafeInteger(revision) || revision < 1))
4316
+ return usageError2(
4317
+ "Read the form first, then provide its --expected-revision."
4318
+ );
4319
+ if (apply && (!confirmKey || confirmKey.length > 255 || !Number.isSafeInteger(count) || count < 0))
4320
+ return usageError2(
4321
+ "Deletion requires --confirm with the exact form key and --expected-submissions from the preview."
4322
+ );
4323
+ if (action === "delete" && !apply && ["confirm", "expected-revision", "expected-submissions"].some(
4324
+ (key) => value(key) !== void 0
4325
+ ))
4326
+ return usageError2(
4327
+ "Delete is a read-only preview. Add --apply with all confirmation values to delete."
4328
+ );
4329
+ const context = await loadFormsManagementContext(
4330
+ options,
4331
+ "forms:workspace:write"
4332
+ );
4333
+ if (!context.ok) return context.error;
4334
+ try {
4335
+ const { environments } = await context.client.listEnvironments({
4336
+ grant: context.grant,
4337
+ projectId: context.project.id
4338
+ });
4339
+ requireEnvironmentProjectConvergence(environments, context.project.id);
4340
+ const environment = environments.find(
4341
+ (item) => item.slug === environmentSlug
4342
+ );
4343
+ if (!environment)
4344
+ return usageError2(
4345
+ "The selected Forms Environment does not exist. Run forms environment list."
4346
+ );
4347
+ const input = {
4348
+ grant: context.grant,
4349
+ environmentId: environment.id,
4350
+ formId: formId ?? ""
4351
+ };
4352
+ if (action === "list")
4353
+ return {
4354
+ exitCode: 0,
4355
+ stdout: stringifySafeJson(
4356
+ await context.client.listDefinitions({
4357
+ ...input,
4358
+ status
4359
+ })
4360
+ )
4361
+ };
4362
+ if (action === "read" || action === "delete" && !apply) {
4363
+ const preview = await context.client.readDefinition(input);
4364
+ if (preview.project.id !== context.project.id || preview.environment.id !== environment.id || preview.definition.id !== formId || preview.definition.environmentId !== environment.id)
4365
+ throw new SiteOSFormsApiError({
4366
+ code: "INVALID_RESPONSE",
4367
+ message: "The form does not match the selected Project and Environment."
4368
+ });
4369
+ return {
4370
+ exitCode: 0,
4371
+ stdout: stringifySafeJson(
4372
+ action === "read" ? preview : {
4373
+ ...preview,
4374
+ action: "delete",
4375
+ applied: false,
4376
+ warning: "Permanently removes this form, all versions and all submitted answers. This cannot be undone. The form key remains reserved.",
4377
+ confirmation: {
4378
+ confirmKey: preview.definition.formKey,
4379
+ expectedRevision: preview.revision,
4380
+ expectedSubmissionCount: preview.definition.submissionCount
4381
+ }
4382
+ }
4383
+ )
4384
+ };
4385
+ }
4386
+ const response = await context.client.changeDefinition({
4387
+ ...input,
4388
+ change: action === "delete" ? {
4389
+ action,
4390
+ confirmKey,
4391
+ expectedRevision: revision,
4392
+ expectedSubmissionCount: count
4393
+ } : {
4394
+ action,
4395
+ expectedRevision: revision
4396
+ }
4397
+ });
4398
+ return {
4399
+ exitCode: 0,
4400
+ stdout: stringifySafeJson({ ...response, action, applied: true })
4401
+ };
4402
+ } catch (error) {
4403
+ return formatApiError(error);
4404
+ }
4405
+ }
4031
4406
  async function runDefinitionSync(options) {
4032
4407
  const parsed = parseDefinitionSourceFlags(options.args, {
4033
4408
  environmentRequired: true,
@@ -4710,8 +5085,8 @@ function formatApiError(error) {
4710
5085
  error.status ? `Status: ${error.status}` : void 0,
4711
5086
  `Code: ${error.code}`,
4712
5087
  error.message,
4713
- error.status === 404 && error.code === "NOT_FOUND" ? "The form was not found. Run definition sync before testing submissions." : void 0,
4714
- error.status === 409 && error.code === "PROJECT_FORM_SUBMISSION_EXISTS" ? "The submission idempotency key was already used. Retry with a new idempotency key." : void 0
5088
+ error.status === 404 && error.code === "NOT_FOUND" ? "Verify the selected Project, Environment and form ID. The server must support this command." : void 0,
5089
+ error.status === 409 && error.code === "PROJECT_FORM_SUBMISSION_EXISTS" ? "The submission idempotency key was already used. Keep it for an unchanged retry and check the existing receipt before sending again." : void 0
4715
5090
  ].filter((line) => Boolean(line));
4716
5091
  return {
4717
5092
  exitCode: 1,
@@ -8752,13 +9127,13 @@ Usage:
8752
9127
  siteos project use <id-or-slug> [--json]
8753
9128
  siteos project update [--name <name>] [--slug <slug>] [--url <production-url>] [--json]
8754
9129
  siteos project status [--json]
8755
- siteos project connect <pulse|cookie|forms|search|trace> [--resource <id>] [--json]
9130
+ siteos project connect <pulse|cookie|forms|search|trace|seo> [--resource <id>] [--json]
8756
9131
  siteos project environment list [--json]
8757
9132
  siteos project environment create --name <name> --slug <slug> [--url <url>] [--json]
8758
9133
  siteos project environment use <slug> [--json]
8759
9134
  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]
9135
+ siteos project environment resources <pulse|cookie|forms|search|trace|seo> [--json]
9136
+ siteos project environment connect <pulse|cookie|forms|search|trace|seo> --environment <slug> [--resource <id>] [--json]
8762
9137
 
8763
9138
  Select a Project once per repository. Service commands use its configured resources.
8764
9139
  Connect creates a draft workspace, or explicitly attaches an existing resource.
@@ -8856,7 +9231,7 @@ Run \`siteos project status\` to inspect its services.`;
8856
9231
  const service = parsed.positionals[0];
8857
9232
  if (!PROJECT_SERVICES.includes(service))
8858
9233
  throw new Error(
8859
- "Choose pulse, cookie, forms, search or trace. Organization connections use `siteos integrations`. "
9234
+ "Choose pulse, cookie, forms, search, trace or seo. Organization connections use `siteos integrations`. "
8860
9235
  );
8861
9236
  overview = await api.connect(
8862
9237
  overview.project.id,
@@ -8912,7 +9287,7 @@ async function runEnvironmentCommand3(options) {
8912
9287
  throw new Error("Environment create requires --name and --slug.");
8913
9288
  const service = positionals[0];
8914
9289
  if ((action === "connect" || action === "resources") && !PROJECT_SERVICES.includes(service))
8915
- throw new Error("Choose pulse, cookie, forms, search or trace.");
9290
+ throw new Error("Choose pulse, cookie, forms, search, trace or seo.");
8916
9291
  if (action === "connect" && !values.environment)
8917
9292
  throw new Error("Choose a Project environment with --environment <slug>.");
8918
9293
  const context = await readCommonProject(
@@ -9359,6 +9734,382 @@ async function runServiceCommand(service, options) {
9359
9734
  }
9360
9735
  }
9361
9736
 
9737
+ // src/services/seo-command.ts
9738
+ import { writeFile as writeFile8 } from "fs/promises";
9739
+ import path29 from "path";
9740
+ import { parseArgs as parseArgs4 } from "util";
9741
+ import { z as z16 } from "zod";
9742
+ var SEO_HELP = `Audit public HTML in the selected Project environment.
9743
+
9744
+ Usage:
9745
+ siteos seo status [--environment <slug>] [--json]
9746
+ siteos seo audit run [--environment <slug>] [--json]
9747
+ siteos seo audit list [--environment <slug>] [--json]
9748
+ siteos seo audit show <id> [--environment <slug>] [--json]
9749
+ siteos seo audit cancel <id> [--environment <slug>] [--json]
9750
+ siteos seo pages [--audit <id>] [--query <text>] [--url <url>] [--page <number>] [--environment <slug>] [--json]
9751
+ siteos seo issues [--audit <id>] [--rule <id>] [--page <number>] [--environment <slug>] [--json]
9752
+ siteos seo changes [--audit <id>] [--state <new|reopened|still_present|resolved|not_rechecked>] [--page <number>] [--environment <slug>] [--json]
9753
+ siteos seo recheck --audit <id> --url <url> [--environment <slug>] [--json]
9754
+ siteos seo issue <ignore|restore> --audit <id> --url <url> --rule <id> --reason <text> --revision <number> [--environment <slug>] [--json]
9755
+ siteos seo schedule show [--environment <slug>] [--json]
9756
+ siteos seo schedule set --enabled <true|false> --weekday <1-7> --time <HH:mm> --timezone <IANA> --revision <number> [--environment <slug>] [--json]
9757
+ siteos seo notifications retry <notification-id> [--environment <slug>] [--json]
9758
+ siteos seo notifications show [--environment <slug>] [--json]
9759
+ siteos seo notifications destinations [--environment <slug>] [--json]
9760
+ siteos seo notifications set --enabled <true|false> [--destination <candidate-id>] --severity <error|warning> --failures <true|false> --revision <number> [--environment <slug>] [--json]
9761
+ 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]
9762
+
9763
+ Schedule and notification writes require an owner/admin and the saved revision (initially 0).
9764
+ Export writes all matching rows to a new file; existing files are never overwritten.
9765
+ Runs are queued. Read audit show until terminal; an accepted run is not a completed check.
9766
+ Recheck accepts a URL observed in the source audit. Cross-page rules require a full audit.
9767
+ Read the current disposition revision before ignore/restore; use 0 if no decision exists.
9768
+ Setup: siteos project connect seo. No crawl runs during setup.`;
9769
+ async function runSeoCommand(options) {
9770
+ if (!options.args.length || options.args.some((arg) => ["--help", "-h"].includes(arg)))
9771
+ return { exitCode: 0, stdout: SEO_HELP };
9772
+ const json = options.args.includes("--json");
9773
+ try {
9774
+ const { positionals, values } = parseArgs4({
9775
+ args: options.args,
9776
+ strict: true,
9777
+ allowPositionals: true,
9778
+ options: {
9779
+ json: { type: "boolean" },
9780
+ environment: { type: "string" },
9781
+ audit: { type: "string" },
9782
+ url: { type: "string" },
9783
+ rule: { type: "string" },
9784
+ reason: { type: "string" },
9785
+ revision: { type: "string" },
9786
+ query: { type: "string" },
9787
+ page: { type: "string" },
9788
+ state: { type: "string" },
9789
+ enabled: { type: "string" },
9790
+ weekday: { type: "string" },
9791
+ time: { type: "string" },
9792
+ timezone: { type: "string" },
9793
+ destination: { type: "string" },
9794
+ severity: { type: "string" },
9795
+ failures: { type: "string" },
9796
+ kind: { type: "string" },
9797
+ format: { type: "string" },
9798
+ output: { type: "string" }
9799
+ }
9800
+ });
9801
+ const route = positionals.slice(0, 2).join(" ");
9802
+ const action = positionals[0];
9803
+ const operations = {
9804
+ status: { flags: [], args: 1 },
9805
+ "schedule show": { flags: [], args: 2 },
9806
+ "schedule set": {
9807
+ flags: ["enabled", "weekday", "time", "timezone", "revision"],
9808
+ args: 2
9809
+ },
9810
+ "notifications retry": { flags: [], args: 3 },
9811
+ "notifications show": { flags: [], args: 2 },
9812
+ "notifications destinations": { flags: [], args: 2 },
9813
+ "notifications set": {
9814
+ flags: ["enabled", "destination", "severity", "failures", "revision"],
9815
+ args: 2
9816
+ },
9817
+ export: {
9818
+ flags: [
9819
+ "audit",
9820
+ "kind",
9821
+ "format",
9822
+ "output",
9823
+ "query",
9824
+ "rule",
9825
+ "severity",
9826
+ "state"
9827
+ ],
9828
+ args: 1
9829
+ },
9830
+ "audit run": { flags: [], args: 2 },
9831
+ "audit list": { flags: [], args: 2 },
9832
+ "audit show": { flags: [], args: 3 },
9833
+ "audit cancel": { flags: [], args: 3 },
9834
+ pages: { flags: ["audit", "url", "query", "page"], args: 1 },
9835
+ issues: { flags: ["audit", "rule", "page"], args: 1 },
9836
+ changes: { flags: ["audit", "state", "page"], args: 1 },
9837
+ recheck: { flags: ["audit", "url"], args: 1 },
9838
+ "issue ignore": {
9839
+ flags: ["audit", "url", "rule", "reason", "revision"],
9840
+ args: 2
9841
+ },
9842
+ "issue restore": {
9843
+ flags: ["audit", "url", "rule", "reason", "revision"],
9844
+ args: 2
9845
+ }
9846
+ };
9847
+ const operation = operations[route];
9848
+ if (!operation || positionals.length !== operation.args || Object.keys(values).some(
9849
+ (key) => !["json", "environment", ...operation.flags].includes(key)
9850
+ ))
9851
+ throw new Error(
9852
+ "Invalid SEO operation or flags. Run `siteos seo --help`."
9853
+ );
9854
+ if (action === "recheck" && (!values.audit || !values.url))
9855
+ throw new Error("Recheck requires --audit and --url.");
9856
+ if (action === "issue" && ["audit", "url", "rule", "reason", "revision"].some(
9857
+ (key) => !values[key]
9858
+ ))
9859
+ throw new Error(
9860
+ "Issue decisions require --audit, --url, --rule, --reason and --revision."
9861
+ );
9862
+ if (values.page && !/^[1-9]\d{0,5}$/u.test(values.page))
9863
+ throw new Error("Use a positive page number.");
9864
+ if (values.revision && !/^\d{1,9}$/u.test(values.revision))
9865
+ throw new Error("Use a non-negative revision.");
9866
+ if (values.reason && (values.reason.trim().length < 3 || values.reason.length > 500))
9867
+ throw new Error("Use a reason between 3 and 500 characters.");
9868
+ const pageStateFilter = action === "export" && values.kind !== "changes";
9869
+ if (values.state && !(pageStateFilter ? ["analyzed", "unavailable", "excluded", "non_html"] : ["new", "reopened", "still_present", "resolved", "not_rechecked"]).includes(values.state))
9870
+ throw new Error("Use a documented page or change state for this export.");
9871
+ const automation = ["schedule", "notifications"].includes(action ?? "");
9872
+ const setting = automation && positionals[1] === "set";
9873
+ if (setting && (!values.revision || !["true", "false"].includes(values.enabled ?? "")))
9874
+ throw new Error("Settings require --enabled true|false and --revision.");
9875
+ if (route === "schedule set") {
9876
+ if (!/^[1-7]$/u.test(values.weekday ?? "") || !/^([01]\d|2[0-3]):[0-5]\d$/u.test(values.time ?? "") || !values.timezone)
9877
+ throw new Error("Use weekday 1\u20137, HH:mm and an IANA time zone.");
9878
+ try {
9879
+ new Intl.DateTimeFormat("en", { timeZone: values.timezone });
9880
+ } catch {
9881
+ throw new Error("Use a valid IANA time zone.");
9882
+ }
9883
+ }
9884
+ if (route === "notifications set" && (!["error", "warning"].includes(values.severity ?? "") || !["true", "false"].includes(values.failures ?? "") || values.enabled === "true" && !values.destination))
9885
+ throw new Error(
9886
+ "Notifications require --severity, --failures and an available --destination when enabled."
9887
+ );
9888
+ if (action === "export" && (!values.audit || !["pages", "issues", "changes"].includes(values.kind ?? "") || !["csv", "json"].includes(values.format ?? "") || !values.output))
9889
+ throw new Error(
9890
+ "Export requires --audit, --kind, --format and --output."
9891
+ );
9892
+ if (values.severity && !["error", "warning", "notice"].includes(values.severity))
9893
+ throw new Error("Use a documented severity.");
9894
+ const context = await commonServiceContext(
9895
+ options,
9896
+ "seo",
9897
+ values.environment
9898
+ );
9899
+ if (!context)
9900
+ throw new Error(
9901
+ "Select a SiteOS Project with `siteos project use` first."
9902
+ );
9903
+ const runtime = commonProjectRuntime(options);
9904
+ const retryNotification = route === "notifications retry";
9905
+ const writing = retryNotification || setting || [
9906
+ "audit run",
9907
+ "audit cancel",
9908
+ "recheck",
9909
+ "issue ignore",
9910
+ "issue restore"
9911
+ ].includes(route);
9912
+ 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";
9913
+ const grant = await runtime.grants.acquire({
9914
+ audience: "siteos-seo",
9915
+ scopes: [scope]
9916
+ });
9917
+ if (grant.grant.audience !== "siteos-seo" || grant.grant.scopes.length !== 1 || grant.grant.scopes[0] !== scope || grant.grant.organizationId !== context.overview.project.organizationId)
9918
+ throw new SiteOSAuthApiError({
9919
+ code: "AUTH_INVALID_RESPONSE",
9920
+ message: "The SEO grant does not match this Project and operation."
9921
+ });
9922
+ const query = new URLSearchParams();
9923
+ for (const [flag, name] of [
9924
+ ["audit", "audit"],
9925
+ ["rule", "rule"],
9926
+ ["query", "q"],
9927
+ ["url", "pageUrl"],
9928
+ ["page", "page"],
9929
+ ["state", "change"],
9930
+ ["kind", "kind"],
9931
+ ["format", "format"],
9932
+ ["severity", "severity"]
9933
+ ])
9934
+ if (values[flag] && !writing) query.set(name, values[flag]);
9935
+ if (pageStateFilter && values.state) {
9936
+ query.delete("change");
9937
+ query.set("state", values.state);
9938
+ }
9939
+ if (route === "audit show") query.set("audit", positionals[2]);
9940
+ 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}`;
9941
+ const body = retryNotification ? { notificationId: positionals[2] } : route === "schedule set" ? {
9942
+ enabled: values.enabled === "true",
9943
+ weekday: Number(values.weekday),
9944
+ time: values.time,
9945
+ timeZone: values.timezone,
9946
+ expectedRevision: Number(values.revision)
9947
+ } : route === "notifications set" ? {
9948
+ enabled: values.enabled === "true",
9949
+ candidateId: values.destination ?? null,
9950
+ minimumSeverity: values.severity,
9951
+ includeFailures: values.failures === "true",
9952
+ expectedRevision: Number(values.revision)
9953
+ } : action === "recheck" ? { auditId: values.audit, urls: [values.url] } : action === "issue" ? {
9954
+ auditId: values.audit,
9955
+ url: values.url,
9956
+ ruleId: values.rule,
9957
+ reason: values.reason,
9958
+ expectedRevision: Number(values.revision),
9959
+ ignored: positionals[1] === "ignore"
9960
+ } : void 0;
9961
+ if (!options.fetchImpl)
9962
+ throw new Error("SiteOS API access is unavailable.");
9963
+ const response = await options.fetchImpl(
9964
+ `${runtime.api.origin}/api/seo/v1/resources/${encodeURIComponent(context.resourceId)}${suffix}`,
9965
+ {
9966
+ method: setting ? "PATCH" : writing ? "POST" : "GET",
9967
+ headers: {
9968
+ Accept: "application/json",
9969
+ Authorization: `Bearer ${grant.accessToken}`,
9970
+ ...body ? { "Content-Type": "application/json" } : {}
9971
+ },
9972
+ ...body ? { body: JSON.stringify(body) } : {},
9973
+ signal: AbortSignal.timeout(3e4)
9974
+ }
9975
+ );
9976
+ if (action === "export" && response.ok) {
9977
+ if (!(response instanceof Response))
9978
+ throw new Error("The API transport does not support file exports.");
9979
+ const mime = values.format === "csv" ? "text/csv" : "application/json";
9980
+ const rows = Number(response.headers.get("X-SEO-Export-Rows"));
9981
+ 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)
9982
+ throw new Error(
9983
+ "The export response does not match the requested audit."
9984
+ );
9985
+ const text = await response.text();
9986
+ if (values.format === "json")
9987
+ z16.object({
9988
+ contractVersion: z16.literal(1),
9989
+ audit: z16.object({
9990
+ id: z16.literal(values.audit),
9991
+ resourceId: z16.literal(context.resourceId)
9992
+ }),
9993
+ kind: z16.literal(values.kind),
9994
+ totalRows: z16.literal(rows),
9995
+ rows: z16.array(z16.unknown()).length(rows)
9996
+ }).parse(JSON.parse(text));
9997
+ const output = path29.resolve(options.cwd ?? process.cwd(), values.output);
9998
+ await writeFile8(output, text, { flag: "wx", mode: 384 });
9999
+ return {
10000
+ exitCode: 0,
10001
+ stdout: JSON.stringify(
10002
+ {
10003
+ auditId: values.audit,
10004
+ kind: values.kind,
10005
+ format: values.format,
10006
+ rows,
10007
+ output
10008
+ },
10009
+ null,
10010
+ 2
10011
+ )
10012
+ };
10013
+ }
10014
+ const data = await response.json();
10015
+ if (!response.ok) {
10016
+ const result = z16.object({
10017
+ error: z16.object({ code: z16.string(), message: z16.string().max(500) })
10018
+ }).safeParse(data);
10019
+ throw new SiteOSAuthApiError({
10020
+ code: result.success ? result.data.error.code : "SEO_REQUEST_FAILED",
10021
+ message: result.success ? result.data.error.message : "The SEO request failed.",
10022
+ status: response.status
10023
+ });
10024
+ }
10025
+ const record = z16.object({ contractVersion: z16.literal(1) }).passthrough().parse(data);
10026
+ if (automation) {
10027
+ z16.literal(context.resourceId).parse(record.resourceId);
10028
+ const schedule = z16.object({
10029
+ enabled: z16.boolean(),
10030
+ weekday: z16.number().int().min(1).max(7),
10031
+ time: z16.string(),
10032
+ timeZone: z16.string(),
10033
+ revision: z16.number().int().min(0),
10034
+ nextRunAt: z16.string().nullable()
10035
+ });
10036
+ const notificationRoute = z16.object({
10037
+ enabled: z16.boolean(),
10038
+ minimumSeverity: z16.enum(["error", "warning"]),
10039
+ includeFailures: z16.boolean(),
10040
+ revision: z16.number().int().min(0),
10041
+ destinationId: z16.string().nullable()
10042
+ });
10043
+ if (retryNotification) {
10044
+ z16.literal(true).parse(record.retryQueued);
10045
+ z16.literal(positionals[2]).parse(record.notificationId);
10046
+ } else if (route === "notifications destinations")
10047
+ z16.object({
10048
+ candidates: z16.array(
10049
+ z16.object({
10050
+ candidateId: z16.string(),
10051
+ label: z16.string(),
10052
+ availability: z16.literal("available")
10053
+ })
10054
+ )
10055
+ }).parse(record);
10056
+ else if (setting) {
10057
+ const value = action === "schedule" ? schedule.parse(record.schedule) : notificationRoute.parse(record.route);
10058
+ if (value.revision !== Number(values.revision) + 1 || value.enabled !== (values.enabled === "true"))
10059
+ throw new Error("The saved settings do not match this change.");
10060
+ } else {
10061
+ schedule.parse(record.schedule);
10062
+ notificationRoute.parse(record.route);
10063
+ }
10064
+ } else if (!writing) {
10065
+ const validated = z16.object({
10066
+ resource: z16.object({
10067
+ id: z16.literal(context.resourceId),
10068
+ organizationId: z16.literal(context.overview.project.organizationId)
10069
+ }),
10070
+ audits: z16.array(z16.object({ id: z16.string() }).passthrough()),
10071
+ audit: z16.object({
10072
+ id: z16.string(),
10073
+ resourceId: z16.literal(context.resourceId)
10074
+ }).passthrough().nullable(),
10075
+ pages: z16.array(z16.unknown()),
10076
+ issues: z16.array(z16.unknown()),
10077
+ changes: z16.array(z16.unknown()),
10078
+ totalChanges: z16.number(),
10079
+ dispositions: z16.array(z16.unknown())
10080
+ }).passthrough().parse(record);
10081
+ const selected = query.get("audit");
10082
+ if (selected && validated.audit?.id !== selected)
10083
+ throw new Error("The SEO response does not match the requested audit.");
10084
+ } else if (record.audit)
10085
+ z16.object({
10086
+ id: z16.string(),
10087
+ resourceId: z16.literal(context.resourceId),
10088
+ organizationId: z16.literal(context.overview.project.organizationId),
10089
+ state: z16.literal("queued")
10090
+ }).parse(record.audit);
10091
+ else if (route === "audit cancel") z16.literal(true).parse(record.cancelled);
10092
+ else if (action === "issue")
10093
+ z16.object({
10094
+ url: z16.literal(values.url),
10095
+ ruleId: z16.literal(values.rule),
10096
+ ignored: z16.literal(positionals[1] === "ignore"),
10097
+ revision: z16.literal(Number(values.revision) + 1)
10098
+ }).parse(record.disposition);
10099
+ else throw new Error("The SEO service returned an invalid response.");
10100
+ return { exitCode: 0, stdout: JSON.stringify(record, null, 2) };
10101
+ } catch (cause) {
10102
+ const error = {
10103
+ code: cause instanceof SiteOSAuthApiError ? cause.code : "SEO_COMMAND_FAILED",
10104
+ message: cause instanceof z16.ZodError ? "The SEO service returned an invalid response." : cause instanceof Error ? cause.message : "The SEO command failed."
10105
+ };
10106
+ return {
10107
+ exitCode: cause instanceof SiteOSAuthApiError ? 1 : 2,
10108
+ ...json ? { stdout: JSON.stringify({ error }) } : { stderr: error.message }
10109
+ };
10110
+ }
10111
+ }
10112
+
9362
10113
  // src/cli.ts
9363
10114
  var [command, ...args] = process.argv.slice(2);
9364
10115
  var HELP_FLAGS5 = /* @__PURE__ */ new Set(["--help", "-h"]);
@@ -9392,6 +10143,12 @@ if (!command || command === "--help" || command === "-h") {
9392
10143
  }
9393
10144
  function rootCommandRegistry() {
9394
10145
  return createCommandRegistry({
10146
+ seo: (args2) => runSeoCommand({
10147
+ args: args2,
10148
+ cwd: process.cwd(),
10149
+ env: process.env,
10150
+ fetchImpl: globalThis.fetch
10151
+ }),
9395
10152
  "health-check": runHealthCheck,
9396
10153
  project: (args2) => runProjectCommand4({
9397
10154
  args: args2,
@@ -9499,6 +10256,7 @@ Usage:
9499
10256
  siteos project --help
9500
10257
  siteos cookie --help
9501
10258
  siteos trace --help
10259
+ siteos seo --help
9502
10260
  siteos integrations --help
9503
10261
  siteos pulse --help
9504
10262
  siteos search --help
@@ -9510,6 +10268,7 @@ Commands:
9510
10268
  project Select one Project and configure its services and environments.
9511
10269
  cookie Configure, publish, and inspect the Project\u2019s cookie banner.
9512
10270
  trace Configure analytics observation and inspect evidence.
10271
+ seo Audit HTML, inspect changes and verify fixes.
9513
10272
  integrations Manage Organization connections and destinations.
9514
10273
  pulse Manage monitoring checks, tests, and deployments.
9515
10274
  search Run SiteOS search operations for a project environment.