@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/README.md +67 -2
- package/dist/cli.js +786 -27
- package/dist/cli.js.map +1 -1
- package/package.json +24 -19
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(
|
|
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${
|
|
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
|
-
}).
|
|
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
|
-
}).
|
|
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" ? "
|
|
4714
|
-
error.status === 409 && error.code === "PROJECT_FORM_SUBMISSION_EXISTS" ? "The submission idempotency key was already used.
|
|
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
|
|
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
|
|
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.
|