@siteoshq/cli 1.5.0 → 1.7.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 +24 -1
- package/dist/cli.js +701 -76
- package/dist/cli.js.map +1 -1
- package/package.json +22 -19
package/dist/cli.js
CHANGED
|
@@ -484,7 +484,8 @@ var siteOSServiceAudienceSchema = z.enum([
|
|
|
484
484
|
"siteos-forms",
|
|
485
485
|
"siteos-pulse",
|
|
486
486
|
"siteos-search",
|
|
487
|
-
"siteos-seo"
|
|
487
|
+
"siteos-seo",
|
|
488
|
+
"siteos-analytics"
|
|
488
489
|
]);
|
|
489
490
|
var siteOSServiceScopeSchema = z.string().min(1).max(160).regex(/^[a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*){2,}$/);
|
|
490
491
|
var scopeNamespaceByAudience = {
|
|
@@ -495,7 +496,8 @@ var scopeNamespaceByAudience = {
|
|
|
495
496
|
"siteos-forms": "forms",
|
|
496
497
|
"siteos-pulse": "pulse",
|
|
497
498
|
"siteos-search": "search",
|
|
498
|
-
"siteos-seo": "seo"
|
|
499
|
+
"siteos-seo": "seo",
|
|
500
|
+
"siteos-analytics": "analytics"
|
|
499
501
|
};
|
|
500
502
|
function matchingServiceScopes(value, context) {
|
|
501
503
|
const prefix = `${scopeNamespaceByAudience[value.audience]}:`;
|
|
@@ -1445,7 +1447,8 @@ var PROJECT_SERVICES = [
|
|
|
1445
1447
|
"forms",
|
|
1446
1448
|
"search",
|
|
1447
1449
|
"trace",
|
|
1448
|
-
"seo"
|
|
1450
|
+
"seo",
|
|
1451
|
+
"analytics"
|
|
1449
1452
|
];
|
|
1450
1453
|
var ProjectSchema = z4.object({
|
|
1451
1454
|
id: z4.string().min(1),
|
|
@@ -1499,7 +1502,7 @@ var OverviewSchema = z4.object({
|
|
|
1499
1502
|
});
|
|
1500
1503
|
function createProjectApi(input) {
|
|
1501
1504
|
const origin = resolveSiteOSAuthBaseUrl(input.env);
|
|
1502
|
-
async function request(
|
|
1505
|
+
async function request(path32, schema, body, method) {
|
|
1503
1506
|
const scope = body === void 0 ? "projects:workspace:read" : "projects:workspace:write";
|
|
1504
1507
|
const grant = await input.grants.acquire({
|
|
1505
1508
|
audience: "siteos-projects",
|
|
@@ -1513,7 +1516,7 @@ function createProjectApi(input) {
|
|
|
1513
1516
|
message: "SiteOS API access is unavailable."
|
|
1514
1517
|
});
|
|
1515
1518
|
const response = await input.fetchImpl(
|
|
1516
|
-
`${origin}/api/projects/v1/projects${
|
|
1519
|
+
`${origin}/api/projects/v1/projects${path32}`,
|
|
1517
1520
|
{
|
|
1518
1521
|
method: method ?? (body === void 0 ? "GET" : "POST"),
|
|
1519
1522
|
headers: {
|
|
@@ -1709,14 +1712,14 @@ async function commonServiceContext(options, service, environmentSlug) {
|
|
|
1709
1712
|
const binding = attachment.environments.find(
|
|
1710
1713
|
(item) => item.environmentId === context.environment.id
|
|
1711
1714
|
);
|
|
1712
|
-
if (["pulse", "cookie", "seo"].includes(service) && !binding)
|
|
1715
|
+
if (["pulse", "cookie", "seo", "analytics"].includes(service) && !binding)
|
|
1713
1716
|
throw new SiteOSAuthApiError({
|
|
1714
1717
|
code: "PROJECT_ENVIRONMENT_NOT_CONFIGURED",
|
|
1715
1718
|
message: `Set up ${service} in ${context.environment.name} with \`siteos project connect ${service}\`.`
|
|
1716
1719
|
});
|
|
1717
1720
|
return {
|
|
1718
1721
|
...context,
|
|
1719
|
-
resourceId: ["pulse", "cookie", "seo"].includes(service) ? binding.resourceId : attachment.resourceId,
|
|
1722
|
+
resourceId: ["pulse", "cookie", "seo", "analytics"].includes(service) ? binding.resourceId : attachment.resourceId,
|
|
1720
1723
|
environmentBinding: binding
|
|
1721
1724
|
};
|
|
1722
1725
|
}
|
|
@@ -9229,13 +9232,13 @@ Usage:
|
|
|
9229
9232
|
siteos project use <id-or-slug> [--json]
|
|
9230
9233
|
siteos project update [--name <name>] [--slug <slug>] [--url <production-url>] [--json]
|
|
9231
9234
|
siteos project status [--json]
|
|
9232
|
-
siteos project connect <pulse|cookie|forms|search|trace|seo> [--resource <id>] [--json]
|
|
9235
|
+
siteos project connect <pulse|cookie|forms|search|trace|seo|analytics> [--resource <id>] [--json]
|
|
9233
9236
|
siteos project environment list [--json]
|
|
9234
9237
|
siteos project environment create --name <name> --slug <slug> [--url <url>] [--json]
|
|
9235
9238
|
siteos project environment use <slug> [--json]
|
|
9236
9239
|
siteos project environment update <slug> [--name <name>] [--url <url>] [--json]
|
|
9237
|
-
siteos project environment resources <pulse|cookie|forms|search|trace|seo> [--json]
|
|
9238
|
-
siteos project environment connect <pulse|cookie|forms|search|trace|seo> --environment <slug> [--resource <id>] [--json]
|
|
9240
|
+
siteos project environment resources <pulse|cookie|forms|search|trace|seo|analytics> [--json]
|
|
9241
|
+
siteos project environment connect <pulse|cookie|forms|search|trace|seo|analytics> --environment <slug> [--resource <id>] [--json]
|
|
9239
9242
|
|
|
9240
9243
|
Select a Project once per repository. Service commands use its configured resources.
|
|
9241
9244
|
Connect creates a draft workspace, or explicitly attaches an existing resource.
|
|
@@ -9333,7 +9336,7 @@ Run \`siteos project status\` to inspect its services.`;
|
|
|
9333
9336
|
const service = parsed.positionals[0];
|
|
9334
9337
|
if (!PROJECT_SERVICES.includes(service))
|
|
9335
9338
|
throw new Error(
|
|
9336
|
-
"Choose pulse, cookie, forms, search, trace or
|
|
9339
|
+
"Choose pulse, cookie, forms, search, trace, seo or analytics. Organization connections use `siteos integrations`. "
|
|
9337
9340
|
);
|
|
9338
9341
|
overview = await api.connect(
|
|
9339
9342
|
overview.project.id,
|
|
@@ -9389,7 +9392,9 @@ async function runEnvironmentCommand3(options) {
|
|
|
9389
9392
|
throw new Error("Environment create requires --name and --slug.");
|
|
9390
9393
|
const service = positionals[0];
|
|
9391
9394
|
if ((action === "connect" || action === "resources") && !PROJECT_SERVICES.includes(service))
|
|
9392
|
-
throw new Error(
|
|
9395
|
+
throw new Error(
|
|
9396
|
+
"Choose pulse, cookie, forms, search, trace, seo or analytics."
|
|
9397
|
+
);
|
|
9393
9398
|
if (action === "connect" && !values.environment)
|
|
9394
9399
|
throw new Error("Choose a Project environment with --environment <slug>.");
|
|
9395
9400
|
const context = await readCommonProject(
|
|
@@ -9836,11 +9841,619 @@ async function runServiceCommand(service, options) {
|
|
|
9836
9841
|
}
|
|
9837
9842
|
}
|
|
9838
9843
|
|
|
9839
|
-
// src/services/
|
|
9840
|
-
import {
|
|
9844
|
+
// src/services/analytics-command.ts
|
|
9845
|
+
import { readFile as readFile17, stat as stat2 } from "fs/promises";
|
|
9841
9846
|
import path29 from "path";
|
|
9842
9847
|
import { parseArgs as parseArgs4 } from "util";
|
|
9843
9848
|
import { z as z16 } from "zod";
|
|
9849
|
+
var ANALYTICS_HELP = `Set up website Analytics in the selected Project environment.
|
|
9850
|
+
|
|
9851
|
+
Usage:
|
|
9852
|
+
siteos analytics status [--environment <slug>] [--json]
|
|
9853
|
+
siteos analytics installation [--environment <slug>] [--json]
|
|
9854
|
+
siteos analytics report [--days <1|7|28>] [--event <name>] [--country <ISO|unknown>] [--campaign <id>] [--environment <slug>] [--json]
|
|
9855
|
+
siteos analytics realtime [--country <ISO|unknown>] [--campaign <id>] [--environment <slug>] [--json]
|
|
9856
|
+
siteos analytics settings show [--environment <slug>] [--json]
|
|
9857
|
+
siteos analytics settings set --revision <number> [--enabled <true|false>] [--cookie-events <true|false>] [--minimal-realtime <true|false>] [--environment <slug>] [--json]
|
|
9858
|
+
siteos analytics events list [--environment <slug>] [--json]
|
|
9859
|
+
siteos analytics events create --file <event.json> [--environment <slug>] [--json]
|
|
9860
|
+
siteos analytics events snippet <name> [--environment <slug>] [--json]
|
|
9861
|
+
siteos analytics campaigns list [--environment <slug>] [--json]
|
|
9862
|
+
siteos analytics campaigns create --file <campaign.json> [--environment <slug>] [--json]
|
|
9863
|
+
siteos analytics goals list [--environment <slug>] [--json]
|
|
9864
|
+
siteos analytics goals create --file <goal.json> [--environment <slug>] [--json]
|
|
9865
|
+
siteos analytics goals archive <id> [--environment <slug>] [--json]
|
|
9866
|
+
siteos analytics funnels list [--environment <slug>] [--json]
|
|
9867
|
+
siteos analytics funnels create --file <funnel.json> [--environment <slug>] [--json]
|
|
9868
|
+
siteos analytics funnels archive <id> [--environment <slug>] [--json]
|
|
9869
|
+
siteos analytics monitoring prepare [--environment <slug>] [--json]
|
|
9870
|
+
|
|
9871
|
+
Connect first: siteos project connect analytics. Cookie and Trace are optional.
|
|
9872
|
+
Use project environment use <slug> to select an environment; no resource fallback is allowed.
|
|
9873
|
+
Analytics starts independently. Consent control is configured and published in Cookie.
|
|
9874
|
+
An external CMP can gate its loader explicitly; privacy opt-outs still apply.
|
|
9875
|
+
Settings require an owner/admin, the current revision and at least one change.
|
|
9876
|
+
Catalog files are bounded JSON using the documented event/campaign/goal/funnel contracts.
|
|
9877
|
+
Monitoring preparation creates a Trace draft only; publication remains a separate operation.
|
|
9878
|
+
Analytics is Unlimited during early access. No synthetic events are sent by these commands.`;
|
|
9879
|
+
var EventInput = z16.object({
|
|
9880
|
+
name: z16.string().regex(/^[a-z][a-z0-9_]{0,63}$/u).refine((v) => v !== "pageview" && !v.startsWith("cookie_")),
|
|
9881
|
+
label: z16.string().trim().min(1).max(80),
|
|
9882
|
+
properties: z16.record(
|
|
9883
|
+
z16.string().regex(/^[a-z][a-z0-9_]{0,39}$/u),
|
|
9884
|
+
z16.array(
|
|
9885
|
+
z16.string().min(1).max(80).regex(/^[a-zA-Z0-9 _./:-]+$/u)
|
|
9886
|
+
).min(1).max(20)
|
|
9887
|
+
).refine((v) => Object.keys(v).length <= 8)
|
|
9888
|
+
}).strict();
|
|
9889
|
+
var Resource = z16.object({
|
|
9890
|
+
id: z16.string(),
|
|
9891
|
+
organizationId: z16.string(),
|
|
9892
|
+
name: z16.string(),
|
|
9893
|
+
slug: z16.string(),
|
|
9894
|
+
origin: z16.string().url(),
|
|
9895
|
+
publicKey: z16.string(),
|
|
9896
|
+
revision: z16.number().int().positive(),
|
|
9897
|
+
enabled: z16.boolean(),
|
|
9898
|
+
cookieEvents: z16.boolean(),
|
|
9899
|
+
minimalRealtime: z16.boolean()
|
|
9900
|
+
});
|
|
9901
|
+
var readKeys = [
|
|
9902
|
+
"days",
|
|
9903
|
+
"from",
|
|
9904
|
+
"to",
|
|
9905
|
+
"totals",
|
|
9906
|
+
"trend",
|
|
9907
|
+
"pages",
|
|
9908
|
+
"sources",
|
|
9909
|
+
"countries",
|
|
9910
|
+
"countryDetectionConfigured",
|
|
9911
|
+
"countryFilter",
|
|
9912
|
+
"campaignFilter",
|
|
9913
|
+
"eventCounts",
|
|
9914
|
+
"recent",
|
|
9915
|
+
"lastReceivedAt",
|
|
9916
|
+
"campaigns",
|
|
9917
|
+
"goals",
|
|
9918
|
+
"funnels",
|
|
9919
|
+
"convertedVisits"
|
|
9920
|
+
];
|
|
9921
|
+
var liveKeys = [
|
|
9922
|
+
"at",
|
|
9923
|
+
"from",
|
|
9924
|
+
"activeSince",
|
|
9925
|
+
"collectionEnabled",
|
|
9926
|
+
"countryFilter",
|
|
9927
|
+
"campaignFilter",
|
|
9928
|
+
"activeVisitors",
|
|
9929
|
+
"visitors",
|
|
9930
|
+
"pageviews",
|
|
9931
|
+
"events",
|
|
9932
|
+
"trend",
|
|
9933
|
+
"pages",
|
|
9934
|
+
"sources",
|
|
9935
|
+
"countries",
|
|
9936
|
+
"eventCounts",
|
|
9937
|
+
"minimal"
|
|
9938
|
+
];
|
|
9939
|
+
var pick = (value, keys) => Object.fromEntries(
|
|
9940
|
+
keys.filter((key) => Object.hasOwn(value, key)).map((key) => [key, value[key]])
|
|
9941
|
+
);
|
|
9942
|
+
async function runAnalyticsCommand(options) {
|
|
9943
|
+
if (!options.args.length || options.args.some((v) => ["--help", "-h"].includes(v)))
|
|
9944
|
+
return { exitCode: 0, stdout: ANALYTICS_HELP };
|
|
9945
|
+
const json = options.args.includes("--json");
|
|
9946
|
+
try {
|
|
9947
|
+
const { positionals, values } = parseArgs4({
|
|
9948
|
+
args: options.args,
|
|
9949
|
+
strict: true,
|
|
9950
|
+
allowPositionals: true,
|
|
9951
|
+
options: {
|
|
9952
|
+
json: { type: "boolean" },
|
|
9953
|
+
environment: { type: "string" },
|
|
9954
|
+
days: { type: "string" },
|
|
9955
|
+
event: { type: "string" },
|
|
9956
|
+
country: { type: "string" },
|
|
9957
|
+
campaign: { type: "string" },
|
|
9958
|
+
revision: { type: "string" },
|
|
9959
|
+
enabled: { type: "string" },
|
|
9960
|
+
"cookie-events": { type: "string" },
|
|
9961
|
+
"minimal-realtime": { type: "string" },
|
|
9962
|
+
file: { type: "string" }
|
|
9963
|
+
}
|
|
9964
|
+
});
|
|
9965
|
+
const route = positionals.slice(0, 2).join(" "), action = positionals[0];
|
|
9966
|
+
const operations = {
|
|
9967
|
+
status: { args: 1, flags: [] },
|
|
9968
|
+
installation: { args: 1, flags: [] },
|
|
9969
|
+
report: { args: 1, flags: ["days", "event", "country", "campaign"] },
|
|
9970
|
+
realtime: { args: 1, flags: ["country", "campaign"] },
|
|
9971
|
+
"settings show": { args: 2, flags: [] },
|
|
9972
|
+
"settings set": {
|
|
9973
|
+
args: 2,
|
|
9974
|
+
flags: ["revision", "enabled", "cookie-events", "minimal-realtime"],
|
|
9975
|
+
scope: "analytics:workspace:write"
|
|
9976
|
+
},
|
|
9977
|
+
"events list": { args: 2, flags: [] },
|
|
9978
|
+
"events snippet": { args: 3, flags: [] },
|
|
9979
|
+
"events create": {
|
|
9980
|
+
args: 2,
|
|
9981
|
+
flags: ["file"],
|
|
9982
|
+
scope: "analytics:events:write"
|
|
9983
|
+
},
|
|
9984
|
+
"campaigns list": { args: 2, flags: [] },
|
|
9985
|
+
"campaigns create": {
|
|
9986
|
+
args: 2,
|
|
9987
|
+
flags: ["file"],
|
|
9988
|
+
scope: "analytics:reports:write"
|
|
9989
|
+
},
|
|
9990
|
+
"goals list": { args: 2, flags: [] },
|
|
9991
|
+
"goals create": {
|
|
9992
|
+
args: 2,
|
|
9993
|
+
flags: ["file"],
|
|
9994
|
+
scope: "analytics:reports:write"
|
|
9995
|
+
},
|
|
9996
|
+
"goals archive": { args: 3, flags: [], scope: "analytics:reports:write" },
|
|
9997
|
+
"funnels list": { args: 2, flags: [] },
|
|
9998
|
+
"funnels create": {
|
|
9999
|
+
args: 2,
|
|
10000
|
+
flags: ["file"],
|
|
10001
|
+
scope: "analytics:reports:write"
|
|
10002
|
+
},
|
|
10003
|
+
"funnels archive": {
|
|
10004
|
+
args: 3,
|
|
10005
|
+
flags: [],
|
|
10006
|
+
scope: "analytics:reports:write"
|
|
10007
|
+
},
|
|
10008
|
+
"monitoring prepare": {
|
|
10009
|
+
args: 2,
|
|
10010
|
+
flags: [],
|
|
10011
|
+
scope: "analytics:monitoring:write"
|
|
10012
|
+
}
|
|
10013
|
+
};
|
|
10014
|
+
const operation = operations[route];
|
|
10015
|
+
if (!operation || positionals.length !== operation.args || Object.keys(values).some(
|
|
10016
|
+
(key) => !["json", "environment", ...operation.flags].includes(key)
|
|
10017
|
+
))
|
|
10018
|
+
throw new Error(
|
|
10019
|
+
"Invalid Analytics operation or flags. Run `siteos analytics --help`."
|
|
10020
|
+
);
|
|
10021
|
+
if (values.days && !["1", "7", "28"].includes(values.days))
|
|
10022
|
+
throw new Error("Use --days 1, 7 or 28.");
|
|
10023
|
+
if (values.country && !/^(?:[A-Z]{2}|unknown)$/u.test(values.country))
|
|
10024
|
+
throw new Error("Use an uppercase ISO country code or unknown.");
|
|
10025
|
+
if (values.campaign) z16.string().uuid().parse(values.campaign);
|
|
10026
|
+
const setting = route === "settings set", creating = positionals[1] === "create", archive = positionals[1] === "archive";
|
|
10027
|
+
let body;
|
|
10028
|
+
if (setting) {
|
|
10029
|
+
if (!/^[1-9]\d{0,8}$/u.test(values.revision ?? ""))
|
|
10030
|
+
throw new Error(
|
|
10031
|
+
"Read settings first and supply the current positive --revision."
|
|
10032
|
+
);
|
|
10033
|
+
body = { revision: Number(values.revision) };
|
|
10034
|
+
for (const [flag, key] of [
|
|
10035
|
+
["enabled", "enabled"],
|
|
10036
|
+
["cookie-events", "cookieEvents"],
|
|
10037
|
+
["minimal-realtime", "minimalRealtime"]
|
|
10038
|
+
]) {
|
|
10039
|
+
if (values[flag] === void 0) continue;
|
|
10040
|
+
if (!["true", "false"].includes(values[flag]))
|
|
10041
|
+
throw new Error(`Use --${flag} true|false.`);
|
|
10042
|
+
body[key] = values[flag] === "true";
|
|
10043
|
+
}
|
|
10044
|
+
if (Object.keys(body).length < 2)
|
|
10045
|
+
throw new Error("Provide at least one setting to change.");
|
|
10046
|
+
} else if (creating) {
|
|
10047
|
+
if (!values.file)
|
|
10048
|
+
throw new Error("Creation requires --file with a JSON definition.");
|
|
10049
|
+
const filename = path29.resolve(options.cwd ?? process.cwd(), values.file);
|
|
10050
|
+
if ((await stat2(filename)).size > 16384)
|
|
10051
|
+
throw new Error("The definition must not exceed 16 KiB.");
|
|
10052
|
+
const text = await readFile17(filename, "utf8");
|
|
10053
|
+
if (Buffer.byteLength(text) > 16384)
|
|
10054
|
+
throw new Error("The definition must not exceed 16 KiB.");
|
|
10055
|
+
let definition;
|
|
10056
|
+
try {
|
|
10057
|
+
definition = JSON.parse(text);
|
|
10058
|
+
} catch {
|
|
10059
|
+
throw new Error("The definition must contain valid JSON.");
|
|
10060
|
+
}
|
|
10061
|
+
body = z16.record(z16.unknown()).parse(definition);
|
|
10062
|
+
if (action === "events") body = EventInput.parse(body);
|
|
10063
|
+
} else if (archive) body = { id: z16.string().uuid().parse(positionals[2]) };
|
|
10064
|
+
const context = await commonServiceContext(
|
|
10065
|
+
options,
|
|
10066
|
+
"analytics",
|
|
10067
|
+
values.environment
|
|
10068
|
+
);
|
|
10069
|
+
if (!context)
|
|
10070
|
+
throw new Error(
|
|
10071
|
+
"Select a SiteOS Project with `siteos project use` first."
|
|
10072
|
+
);
|
|
10073
|
+
const runtime = commonProjectRuntime(options), scope = operation.scope ?? "analytics:workspace:read";
|
|
10074
|
+
const grant = await runtime.grants.acquire({
|
|
10075
|
+
audience: "siteos-analytics",
|
|
10076
|
+
scopes: [scope]
|
|
10077
|
+
});
|
|
10078
|
+
if (grant.grant.audience !== "siteos-analytics" || grant.grant.scopes.length !== 1 || grant.grant.scopes[0] !== scope || grant.grant.organizationId !== context.overview.project.organizationId)
|
|
10079
|
+
throw new SiteOSAuthApiError({
|
|
10080
|
+
code: "AUTH_INVALID_RESPONSE",
|
|
10081
|
+
message: "The Analytics grant does not match this Project and operation."
|
|
10082
|
+
});
|
|
10083
|
+
const query = new URLSearchParams();
|
|
10084
|
+
for (const key of ["days", "event", "country", "campaign"])
|
|
10085
|
+
if (values[key]) query.set(key, values[key]);
|
|
10086
|
+
const suffix = action === "realtime" ? `/realtime?${query}` : setting ? "/settings" : creating || archive ? `/${action}` : route === "monitoring prepare" ? "/monitoring" : `?${query}`;
|
|
10087
|
+
if (!options.fetchImpl)
|
|
10088
|
+
throw new Error("SiteOS API access is unavailable.");
|
|
10089
|
+
const response = await options.fetchImpl(
|
|
10090
|
+
`${runtime.api.origin}/api/analytics/v1/resources/${encodeURIComponent(context.resourceId)}${suffix}`,
|
|
10091
|
+
{
|
|
10092
|
+
method: setting ? "PATCH" : archive ? "DELETE" : operation.scope ? "POST" : "GET",
|
|
10093
|
+
headers: {
|
|
10094
|
+
Accept: "application/json",
|
|
10095
|
+
Authorization: `Bearer ${grant.accessToken}`,
|
|
10096
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
10097
|
+
},
|
|
10098
|
+
...body ? { body: JSON.stringify(body) } : {},
|
|
10099
|
+
signal: AbortSignal.timeout(3e4)
|
|
10100
|
+
}
|
|
10101
|
+
);
|
|
10102
|
+
const value = await response.json();
|
|
10103
|
+
if (!response.ok) {
|
|
10104
|
+
const error = z16.object({
|
|
10105
|
+
error: z16.object({ code: z16.string(), message: z16.string().max(500) })
|
|
10106
|
+
}).safeParse(value);
|
|
10107
|
+
throw new SiteOSAuthApiError({
|
|
10108
|
+
code: error.success ? error.data.error.code : "ANALYTICS_REQUEST_FAILED",
|
|
10109
|
+
message: error.success ? error.data.error.message : "The Analytics request failed.",
|
|
10110
|
+
status: response.status
|
|
10111
|
+
});
|
|
10112
|
+
}
|
|
10113
|
+
const record = z16.object({
|
|
10114
|
+
contractVersion: z16.literal(1),
|
|
10115
|
+
resourceId: z16.literal(context.resourceId)
|
|
10116
|
+
}).passthrough().parse(value);
|
|
10117
|
+
let output;
|
|
10118
|
+
if (setting) {
|
|
10119
|
+
const saved = Resource.parse(record);
|
|
10120
|
+
if (saved.id !== context.resourceId || saved.organizationId !== context.overview.project.organizationId || saved.revision !== Number(values.revision) + 1 || Object.entries(body).some(
|
|
10121
|
+
([key, val]) => key !== "revision" && saved[key] !== val
|
|
10122
|
+
))
|
|
10123
|
+
throw new Error("The saved settings do not match this change.");
|
|
10124
|
+
output = saved;
|
|
10125
|
+
} else if (creating) {
|
|
10126
|
+
if (action === "events") {
|
|
10127
|
+
output = EventInput.parse(
|
|
10128
|
+
pick(record, ["name", "label", "properties"])
|
|
10129
|
+
);
|
|
10130
|
+
if (JSON.stringify(output) !== JSON.stringify(body))
|
|
10131
|
+
throw new Error("The saved event does not match this definition.");
|
|
10132
|
+
} else
|
|
10133
|
+
output = {
|
|
10134
|
+
id: z16.string().uuid().parse(record.id),
|
|
10135
|
+
...pick(
|
|
10136
|
+
record,
|
|
10137
|
+
action === "campaigns" ? ["label", "source", "medium", "campaign"] : action === "goals" ? ["label", "match"] : ["label", "steps"]
|
|
10138
|
+
)
|
|
10139
|
+
};
|
|
10140
|
+
} else if (archive)
|
|
10141
|
+
output = { archived: z16.literal(true).parse(record.archived) };
|
|
10142
|
+
else if (route === "monitoring prepare")
|
|
10143
|
+
output = { state: z16.literal("prepared").parse(record.state) };
|
|
10144
|
+
else if (action === "realtime") output = pick(record, liveKeys);
|
|
10145
|
+
else {
|
|
10146
|
+
const resource = Resource.parse(record.resource);
|
|
10147
|
+
if (resource.id !== context.resourceId || resource.organizationId !== context.overview.project.organizationId)
|
|
10148
|
+
throw new Error(
|
|
10149
|
+
"The Analytics response belongs to a different resource."
|
|
10150
|
+
);
|
|
10151
|
+
if (action === "status")
|
|
10152
|
+
output = {
|
|
10153
|
+
resource,
|
|
10154
|
+
lastReceivedAt: record.lastReceivedAt,
|
|
10155
|
+
consentSource: z16.object({
|
|
10156
|
+
consentSource: z16.enum([
|
|
10157
|
+
"cookie",
|
|
10158
|
+
"external",
|
|
10159
|
+
"waiting_for_cookie",
|
|
10160
|
+
"not_required"
|
|
10161
|
+
])
|
|
10162
|
+
}).parse(record.configuration).consentSource
|
|
10163
|
+
};
|
|
10164
|
+
else if (route === "settings show") output = resource;
|
|
10165
|
+
else if (action === "installation")
|
|
10166
|
+
output = {
|
|
10167
|
+
resource,
|
|
10168
|
+
installation: z16.string().parse(record.installation),
|
|
10169
|
+
configuration: record.configuration
|
|
10170
|
+
};
|
|
10171
|
+
else if (action === "events") {
|
|
10172
|
+
const definitions = z16.array(EventInput).parse(record.definitions);
|
|
10173
|
+
if (route === "events snippet") {
|
|
10174
|
+
const definition = definitions.find((d) => d.name === positionals[2]);
|
|
10175
|
+
if (!definition)
|
|
10176
|
+
throw new Error(
|
|
10177
|
+
"Create the event definition before requesting its snippet."
|
|
10178
|
+
);
|
|
10179
|
+
output = {
|
|
10180
|
+
name: definition.name,
|
|
10181
|
+
javascript: `window.SiteOSAnalytics?.track(${JSON.stringify(definition.name)}, ${JSON.stringify(Object.fromEntries(Object.entries(definition.properties).map(([key, values2]) => [key, values2[0]])))});`,
|
|
10182
|
+
guidance: "Trigger only after the business action succeeds; substitute declared categorical values."
|
|
10183
|
+
};
|
|
10184
|
+
} else output = { definitions, eventCounts: record.eventCounts };
|
|
10185
|
+
} else
|
|
10186
|
+
output = action === "report" ? pick(record, readKeys) : { [action]: z16.array(z16.unknown()).parse(record[action]) };
|
|
10187
|
+
}
|
|
10188
|
+
return { exitCode: 0, stdout: JSON.stringify(output, null, 2) };
|
|
10189
|
+
} catch (cause) {
|
|
10190
|
+
const error = cause instanceof SiteOSAuthApiError ? { code: cause.code, message: cause.message } : {
|
|
10191
|
+
code: "ANALYTICS_COMMAND_FAILED",
|
|
10192
|
+
message: cause instanceof z16.ZodError ? "The Analytics input or response does not match the supported contract." : cause instanceof Error ? cause.message : "The Analytics command failed."
|
|
10193
|
+
};
|
|
10194
|
+
return {
|
|
10195
|
+
exitCode: cause instanceof SiteOSAuthApiError ? 1 : 2,
|
|
10196
|
+
...json ? { stdout: JSON.stringify({ error }) } : { stderr: error.message }
|
|
10197
|
+
};
|
|
10198
|
+
}
|
|
10199
|
+
}
|
|
10200
|
+
|
|
10201
|
+
// src/services/seo-command.ts
|
|
10202
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
10203
|
+
import path31 from "path";
|
|
10204
|
+
import { parseArgs as parseArgs6 } from "util";
|
|
10205
|
+
import { z as z18 } from "zod";
|
|
10206
|
+
|
|
10207
|
+
// src/services/seo-performance-command.ts
|
|
10208
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
10209
|
+
import { writeFile as writeFile8 } from "fs/promises";
|
|
10210
|
+
import path30 from "path";
|
|
10211
|
+
import { setTimeout as setTimeout2 } from "timers/promises";
|
|
10212
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
10213
|
+
import { z as z17 } from "zod";
|
|
10214
|
+
var PERFORMANCE_HELP = `
|
|
10215
|
+
siteos seo performance run --audit <id> --url <url> [--url <url>...] [--device <mobile|desktop>] [--idempotency-key <key>] [--environment <slug>] [--json]
|
|
10216
|
+
siteos seo performance list [--device <mobile|desktop>] [--environment <slug>] [--json]
|
|
10217
|
+
siteos seo performance show <id> [--url <url>] [--environment <slug>] [--json]
|
|
10218
|
+
siteos seo performance wait <id> [--timeout <seconds>] [--environment <slug>] [--json]
|
|
10219
|
+
siteos seo performance cancel <id> [--environment <slug>] [--json]
|
|
10220
|
+
siteos seo performance history --url <url> [--device <mobile|desktop>] [--environment <slug>] [--json]
|
|
10221
|
+
siteos seo performance export <id> --format <csv|json> --output <new-file> [--environment <slug>] [--json]
|
|
10222
|
+
|
|
10223
|
+
Performance checks use up to 10 explicitly selected URLs from a saved full HTML audit.
|
|
10224
|
+
Mobile is the default for a new run. Scores are Lighthouse lab data, not real-user Core Web Vitals.
|
|
10225
|
+
Reuse the returned idempotency key after an uncertain run response. Wait defaults to 120 seconds (maximum 900).
|
|
10226
|
+
`;
|
|
10227
|
+
async function runSeoPerformanceCommand(options) {
|
|
10228
|
+
let idempotencyKey;
|
|
10229
|
+
try {
|
|
10230
|
+
const { values, positionals } = parseArgs5({
|
|
10231
|
+
args: options.args.slice(1),
|
|
10232
|
+
strict: true,
|
|
10233
|
+
allowPositionals: true,
|
|
10234
|
+
options: {
|
|
10235
|
+
json: { type: "boolean" },
|
|
10236
|
+
environment: { type: "string" },
|
|
10237
|
+
audit: { type: "string" },
|
|
10238
|
+
url: { type: "string", multiple: true },
|
|
10239
|
+
device: { type: "string" },
|
|
10240
|
+
"idempotency-key": { type: "string" },
|
|
10241
|
+
timeout: { type: "string" },
|
|
10242
|
+
format: { type: "string" },
|
|
10243
|
+
output: { type: "string" }
|
|
10244
|
+
}
|
|
10245
|
+
});
|
|
10246
|
+
const action = positionals[0] ?? "";
|
|
10247
|
+
const id = positionals[1];
|
|
10248
|
+
const operations = {
|
|
10249
|
+
run: { args: 1, flags: ["audit", "url", "device", "idempotency-key"] },
|
|
10250
|
+
list: { args: 1, flags: ["device"] },
|
|
10251
|
+
show: { args: 2, flags: ["url"] },
|
|
10252
|
+
wait: { args: 2, flags: ["timeout"] },
|
|
10253
|
+
cancel: { args: 2, flags: [] },
|
|
10254
|
+
history: { args: 1, flags: ["url", "device"] },
|
|
10255
|
+
export: { args: 2, flags: ["format", "output"] }
|
|
10256
|
+
};
|
|
10257
|
+
const operation = operations[action];
|
|
10258
|
+
if (!operation || positionals.length !== operation.args || Object.keys(values).some(
|
|
10259
|
+
(key) => !["environment", "json", ...operation.flags].includes(key)
|
|
10260
|
+
))
|
|
10261
|
+
throw new Error(
|
|
10262
|
+
"Invalid performance operation or flags. Run siteos seo --help."
|
|
10263
|
+
);
|
|
10264
|
+
if (values.device && !["mobile", "desktop"].includes(values.device))
|
|
10265
|
+
throw new Error("Use --device mobile or desktop.");
|
|
10266
|
+
if (action === "run" && (!values.audit || !values.url?.length || values.url.length > 10))
|
|
10267
|
+
throw new Error(
|
|
10268
|
+
"Choose a source --audit and between 1 and 10 --url values."
|
|
10269
|
+
);
|
|
10270
|
+
if (action === "history" && values.url?.length !== 1 || action === "show" && values.url && values.url.length !== 1)
|
|
10271
|
+
throw new Error("Use one --url for page evidence or history.");
|
|
10272
|
+
for (const value of values.url ?? []) {
|
|
10273
|
+
const url = new URL(value);
|
|
10274
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.port || value.length > 2048)
|
|
10275
|
+
throw new Error("Use eligible public HTTP(S) URLs.");
|
|
10276
|
+
}
|
|
10277
|
+
const timeout = Number(values.timeout ?? 120);
|
|
10278
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 900)
|
|
10279
|
+
throw new Error("Use a wait timeout between 1 and 900 seconds.");
|
|
10280
|
+
if (action === "export" && (!values.output || !["csv", "json"].includes(values.format ?? "")))
|
|
10281
|
+
throw new Error(
|
|
10282
|
+
"Export requires --format csv|json and --output for a new file."
|
|
10283
|
+
);
|
|
10284
|
+
if (action === "run") {
|
|
10285
|
+
idempotencyKey = values["idempotency-key"] ?? randomUUID6();
|
|
10286
|
+
if (!/^[a-zA-Z0-9_-]{16,100}$/u.test(idempotencyKey))
|
|
10287
|
+
throw new Error(
|
|
10288
|
+
"Use an idempotency key of 16 to 100 letters, digits, underscores or hyphens."
|
|
10289
|
+
);
|
|
10290
|
+
}
|
|
10291
|
+
const context = await commonServiceContext(
|
|
10292
|
+
options,
|
|
10293
|
+
"seo",
|
|
10294
|
+
values.environment
|
|
10295
|
+
);
|
|
10296
|
+
if (!context)
|
|
10297
|
+
throw new Error("Select a SiteOS Project with siteos project use first.");
|
|
10298
|
+
const runtime = commonProjectRuntime(options);
|
|
10299
|
+
const writing = ["run", "cancel"].includes(action);
|
|
10300
|
+
const scope = writing ? "seo:audits:write" : "seo:workspace:read";
|
|
10301
|
+
const batchSchema = z17.object({
|
|
10302
|
+
id: z17.string(),
|
|
10303
|
+
resourceId: z17.literal(context.resourceId),
|
|
10304
|
+
organizationId: z17.literal(context.overview.project.organizationId),
|
|
10305
|
+
sourceAuditId: z17.string(),
|
|
10306
|
+
state: z17.enum([
|
|
10307
|
+
"queued",
|
|
10308
|
+
"running",
|
|
10309
|
+
"completed",
|
|
10310
|
+
"partial",
|
|
10311
|
+
"failed",
|
|
10312
|
+
"cancelled"
|
|
10313
|
+
]),
|
|
10314
|
+
device: z17.enum(["mobile", "desktop"]),
|
|
10315
|
+
urls: z17.array(z17.string()).min(1).max(10)
|
|
10316
|
+
}).passthrough();
|
|
10317
|
+
const query = new URLSearchParams();
|
|
10318
|
+
if (values.device) query.set("device", values.device);
|
|
10319
|
+
if (!writing && values.url?.[0]) query.set("pageUrl", values.url[0]);
|
|
10320
|
+
const suffix = id ? `/${encodeURIComponent(id)}${action === "cancel" ? "/cancel" : action === "export" ? "/export" : ""}` : "";
|
|
10321
|
+
if (action === "export") query.set("format", values.format);
|
|
10322
|
+
const deadline = Date.now() + timeout * 1e3;
|
|
10323
|
+
while (true) {
|
|
10324
|
+
const grant = await runtime.grants.acquire({
|
|
10325
|
+
audience: "siteos-seo",
|
|
10326
|
+
scopes: [scope]
|
|
10327
|
+
});
|
|
10328
|
+
if (grant.grant.audience !== "siteos-seo" || grant.grant.organizationId !== context.overview.project.organizationId || grant.grant.scopes.length !== 1 || grant.grant.scopes[0] !== scope)
|
|
10329
|
+
throw new Error(
|
|
10330
|
+
"The SEO grant does not match this Project and operation."
|
|
10331
|
+
);
|
|
10332
|
+
if (!options.fetchImpl)
|
|
10333
|
+
throw new Error("SiteOS API access is unavailable.");
|
|
10334
|
+
const response = await options.fetchImpl(
|
|
10335
|
+
`${runtime.api.origin}/api/seo/v1/resources/${encodeURIComponent(context.resourceId)}/browser-checks${suffix}?${query}`,
|
|
10336
|
+
{
|
|
10337
|
+
method: writing ? "POST" : "GET",
|
|
10338
|
+
headers: {
|
|
10339
|
+
Accept: "application/json",
|
|
10340
|
+
Authorization: `Bearer ${grant.accessToken}`,
|
|
10341
|
+
...action === "run" ? { "Content-Type": "application/json" } : {}
|
|
10342
|
+
},
|
|
10343
|
+
...action === "run" ? {
|
|
10344
|
+
body: JSON.stringify({
|
|
10345
|
+
auditId: values.audit,
|
|
10346
|
+
urls: values.url,
|
|
10347
|
+
device: values.device ?? "mobile",
|
|
10348
|
+
idempotencyKey
|
|
10349
|
+
})
|
|
10350
|
+
} : {},
|
|
10351
|
+
signal: AbortSignal.timeout(3e4)
|
|
10352
|
+
}
|
|
10353
|
+
);
|
|
10354
|
+
if (action === "export" && response.ok) {
|
|
10355
|
+
if (!(response instanceof Response) || response.headers.get("X-SEO-Browser-Id") !== id || !response.headers.get("Content-Type")?.startsWith(
|
|
10356
|
+
values.format === "csv" ? "text/csv" : "application/json"
|
|
10357
|
+
))
|
|
10358
|
+
throw new Error(
|
|
10359
|
+
"The export response does not match the selected check."
|
|
10360
|
+
);
|
|
10361
|
+
const text = await response.text();
|
|
10362
|
+
if (values.format === "json") {
|
|
10363
|
+
const parsed = z17.object({
|
|
10364
|
+
contractVersion: z17.literal(1),
|
|
10365
|
+
batch: batchSchema,
|
|
10366
|
+
pages: z17.array(z17.unknown())
|
|
10367
|
+
}).parse(JSON.parse(text));
|
|
10368
|
+
if (parsed.batch.id !== id)
|
|
10369
|
+
throw new Error(
|
|
10370
|
+
"The export response does not match the selected check."
|
|
10371
|
+
);
|
|
10372
|
+
}
|
|
10373
|
+
const output = path30.resolve(
|
|
10374
|
+
options.cwd ?? process.cwd(),
|
|
10375
|
+
values.output
|
|
10376
|
+
);
|
|
10377
|
+
await writeFile8(output, text, { flag: "wx", mode: 384 });
|
|
10378
|
+
return {
|
|
10379
|
+
exitCode: 0,
|
|
10380
|
+
stdout: JSON.stringify(
|
|
10381
|
+
{ batchId: id, output, format: values.format },
|
|
10382
|
+
null,
|
|
10383
|
+
2
|
|
10384
|
+
)
|
|
10385
|
+
};
|
|
10386
|
+
}
|
|
10387
|
+
const data = await response.json();
|
|
10388
|
+
if (!response.ok) {
|
|
10389
|
+
const error = z17.object({
|
|
10390
|
+
error: z17.object({ code: z17.string(), message: z17.string().max(500) })
|
|
10391
|
+
}).safeParse(data);
|
|
10392
|
+
throw new SiteOSAuthApiError({
|
|
10393
|
+
code: error.success ? error.data.error.code : "SEO_REQUEST_FAILED",
|
|
10394
|
+
message: error.success ? error.data.error.message : "The performance request failed.",
|
|
10395
|
+
status: response.status
|
|
10396
|
+
});
|
|
10397
|
+
}
|
|
10398
|
+
const record = z17.object({ contractVersion: z17.literal(1) }).passthrough().parse(data);
|
|
10399
|
+
if (action === "cancel") z17.literal(true).parse(record.cancelled);
|
|
10400
|
+
else if (action === "run") {
|
|
10401
|
+
const accepted = batchSchema.parse(record.batch);
|
|
10402
|
+
const normalize = (url) => {
|
|
10403
|
+
const value = new URL(url);
|
|
10404
|
+
value.hash = "";
|
|
10405
|
+
return value.href;
|
|
10406
|
+
};
|
|
10407
|
+
if (accepted.sourceAuditId !== values.audit || accepted.device !== (values.device ?? "mobile") || JSON.stringify([...new Set(accepted.urls.map(normalize))].sort()) !== JSON.stringify([...new Set(values.url.map(normalize))].sort()))
|
|
10408
|
+
throw new Error(
|
|
10409
|
+
"The queued check does not match the requested source, device and URLs."
|
|
10410
|
+
);
|
|
10411
|
+
} else {
|
|
10412
|
+
z17.literal(context.resourceId).parse(record.resourceId);
|
|
10413
|
+
z17.array(batchSchema).parse(record.batches);
|
|
10414
|
+
const selected = batchSchema.nullable().parse(record.batch);
|
|
10415
|
+
if (id && selected?.id !== id)
|
|
10416
|
+
throw new Error(
|
|
10417
|
+
"The response does not match the selected performance check."
|
|
10418
|
+
);
|
|
10419
|
+
}
|
|
10420
|
+
const batch = record.batch ? batchSchema.parse(record.batch) : null;
|
|
10421
|
+
if (action !== "wait" || !batch || !["queued", "running"].includes(batch.state))
|
|
10422
|
+
return {
|
|
10423
|
+
exitCode: 0,
|
|
10424
|
+
stdout: JSON.stringify(
|
|
10425
|
+
{ ...record, ...idempotencyKey ? { idempotencyKey } : {} },
|
|
10426
|
+
null,
|
|
10427
|
+
2
|
|
10428
|
+
)
|
|
10429
|
+
};
|
|
10430
|
+
if (Date.now() >= deadline)
|
|
10431
|
+
return {
|
|
10432
|
+
exitCode: 3,
|
|
10433
|
+
stdout: JSON.stringify({ ...record, timedOut: true }, null, 2)
|
|
10434
|
+
};
|
|
10435
|
+
await setTimeout2(Math.min(3e3, Math.max(0, deadline - Date.now())));
|
|
10436
|
+
}
|
|
10437
|
+
} catch (cause) {
|
|
10438
|
+
const error = {
|
|
10439
|
+
code: cause instanceof SiteOSAuthApiError ? cause.code : "SEO_COMMAND_FAILED",
|
|
10440
|
+
message: cause instanceof z17.ZodError ? "The SEO service returned an invalid response." : cause instanceof Error ? cause.message : "The performance command failed."
|
|
10441
|
+
};
|
|
10442
|
+
return {
|
|
10443
|
+
exitCode: cause instanceof SiteOSAuthApiError ? 1 : 2,
|
|
10444
|
+
...options.args.includes("--json") ? {
|
|
10445
|
+
stdout: JSON.stringify({
|
|
10446
|
+
error,
|
|
10447
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
10448
|
+
})
|
|
10449
|
+
} : {
|
|
10450
|
+
stderr: `${error.message}${idempotencyKey ? ` Retry key: ${idempotencyKey}` : ""}`
|
|
10451
|
+
}
|
|
10452
|
+
};
|
|
10453
|
+
}
|
|
10454
|
+
}
|
|
10455
|
+
|
|
10456
|
+
// src/services/seo-command.ts
|
|
9844
10457
|
var SEO_HELP = `Audit public HTML in the selected Project environment.
|
|
9845
10458
|
|
|
9846
10459
|
Usage:
|
|
@@ -9861,19 +10474,23 @@ Usage:
|
|
|
9861
10474
|
siteos seo notifications destinations [--environment <slug>] [--json]
|
|
9862
10475
|
siteos seo notifications set --enabled <true|false> [--destination <candidate-id>] --severity <error|warning> --failures <true|false> --revision <number> [--environment <slug>] [--json]
|
|
9863
10476
|
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]
|
|
10477
|
+
${PERFORMANCE_HELP.trim().split("\n\n")[0]}
|
|
9864
10478
|
|
|
9865
10479
|
Schedule and notification writes require an owner/admin and the saved revision (initially 0).
|
|
9866
10480
|
Export writes all matching rows to a new file; existing files are never overwritten.
|
|
9867
10481
|
Runs are queued. Read audit show until terminal; an accepted run is not a completed check.
|
|
9868
10482
|
Recheck accepts a URL observed in the source audit. Cross-page rules require a full audit.
|
|
9869
10483
|
Read the current disposition revision before ignore/restore; use 0 if no decision exists.
|
|
9870
|
-
Setup: siteos project connect seo. No crawl runs during setup
|
|
10484
|
+
Setup: siteos project connect seo. No crawl runs during setup.
|
|
10485
|
+
${PERFORMANCE_HELP.trim().split("\n\n").slice(1).join("\n\n")}`;
|
|
9871
10486
|
async function runSeoCommand(options) {
|
|
9872
10487
|
if (!options.args.length || options.args.some((arg) => ["--help", "-h"].includes(arg)))
|
|
9873
10488
|
return { exitCode: 0, stdout: SEO_HELP };
|
|
10489
|
+
if (options.args[0] === "performance")
|
|
10490
|
+
return runSeoPerformanceCommand(options);
|
|
9874
10491
|
const json = options.args.includes("--json");
|
|
9875
10492
|
try {
|
|
9876
|
-
const { positionals, values } =
|
|
10493
|
+
const { positionals, values } = parseArgs6({
|
|
9877
10494
|
args: options.args,
|
|
9878
10495
|
strict: true,
|
|
9879
10496
|
allowPositionals: true,
|
|
@@ -10086,18 +10703,18 @@ async function runSeoCommand(options) {
|
|
|
10086
10703
|
);
|
|
10087
10704
|
const text = await response.text();
|
|
10088
10705
|
if (values.format === "json")
|
|
10089
|
-
|
|
10090
|
-
contractVersion:
|
|
10091
|
-
audit:
|
|
10092
|
-
id:
|
|
10093
|
-
resourceId:
|
|
10706
|
+
z18.object({
|
|
10707
|
+
contractVersion: z18.literal(1),
|
|
10708
|
+
audit: z18.object({
|
|
10709
|
+
id: z18.literal(values.audit),
|
|
10710
|
+
resourceId: z18.literal(context.resourceId)
|
|
10094
10711
|
}),
|
|
10095
|
-
kind:
|
|
10096
|
-
totalRows:
|
|
10097
|
-
rows:
|
|
10712
|
+
kind: z18.literal(values.kind),
|
|
10713
|
+
totalRows: z18.literal(rows),
|
|
10714
|
+
rows: z18.array(z18.unknown()).length(rows)
|
|
10098
10715
|
}).parse(JSON.parse(text));
|
|
10099
|
-
const output =
|
|
10100
|
-
await
|
|
10716
|
+
const output = path31.resolve(options.cwd ?? process.cwd(), values.output);
|
|
10717
|
+
await writeFile9(output, text, { flag: "wx", mode: 384 });
|
|
10101
10718
|
return {
|
|
10102
10719
|
exitCode: 0,
|
|
10103
10720
|
stdout: JSON.stringify(
|
|
@@ -10115,8 +10732,8 @@ async function runSeoCommand(options) {
|
|
|
10115
10732
|
}
|
|
10116
10733
|
const data = await response.json();
|
|
10117
10734
|
if (!response.ok) {
|
|
10118
|
-
const result =
|
|
10119
|
-
error:
|
|
10735
|
+
const result = z18.object({
|
|
10736
|
+
error: z18.object({ code: z18.string(), message: z18.string().max(500) })
|
|
10120
10737
|
}).safeParse(data);
|
|
10121
10738
|
throw new SiteOSAuthApiError({
|
|
10122
10739
|
code: result.success ? result.data.error.code : "SEO_REQUEST_FAILED",
|
|
@@ -10124,34 +10741,34 @@ async function runSeoCommand(options) {
|
|
|
10124
10741
|
status: response.status
|
|
10125
10742
|
});
|
|
10126
10743
|
}
|
|
10127
|
-
const record =
|
|
10744
|
+
const record = z18.object({ contractVersion: z18.literal(1) }).passthrough().parse(data);
|
|
10128
10745
|
if (automation) {
|
|
10129
|
-
|
|
10130
|
-
const schedule =
|
|
10131
|
-
enabled:
|
|
10132
|
-
weekday:
|
|
10133
|
-
time:
|
|
10134
|
-
timeZone:
|
|
10135
|
-
revision:
|
|
10136
|
-
nextRunAt:
|
|
10746
|
+
z18.literal(context.resourceId).parse(record.resourceId);
|
|
10747
|
+
const schedule = z18.object({
|
|
10748
|
+
enabled: z18.boolean(),
|
|
10749
|
+
weekday: z18.number().int().min(1).max(7),
|
|
10750
|
+
time: z18.string(),
|
|
10751
|
+
timeZone: z18.string(),
|
|
10752
|
+
revision: z18.number().int().min(0),
|
|
10753
|
+
nextRunAt: z18.string().nullable()
|
|
10137
10754
|
});
|
|
10138
|
-
const notificationRoute =
|
|
10139
|
-
enabled:
|
|
10140
|
-
minimumSeverity:
|
|
10141
|
-
includeFailures:
|
|
10142
|
-
revision:
|
|
10143
|
-
destinationId:
|
|
10755
|
+
const notificationRoute = z18.object({
|
|
10756
|
+
enabled: z18.boolean(),
|
|
10757
|
+
minimumSeverity: z18.enum(["error", "warning"]),
|
|
10758
|
+
includeFailures: z18.boolean(),
|
|
10759
|
+
revision: z18.number().int().min(0),
|
|
10760
|
+
destinationId: z18.string().nullable()
|
|
10144
10761
|
});
|
|
10145
10762
|
if (retryNotification) {
|
|
10146
|
-
|
|
10147
|
-
|
|
10763
|
+
z18.literal(true).parse(record.retryQueued);
|
|
10764
|
+
z18.literal(positionals[2]).parse(record.notificationId);
|
|
10148
10765
|
} else if (route === "notifications destinations")
|
|
10149
|
-
|
|
10150
|
-
candidates:
|
|
10151
|
-
|
|
10152
|
-
candidateId:
|
|
10153
|
-
label:
|
|
10154
|
-
availability:
|
|
10766
|
+
z18.object({
|
|
10767
|
+
candidates: z18.array(
|
|
10768
|
+
z18.object({
|
|
10769
|
+
candidateId: z18.string(),
|
|
10770
|
+
label: z18.string(),
|
|
10771
|
+
availability: z18.literal("available")
|
|
10155
10772
|
})
|
|
10156
10773
|
)
|
|
10157
10774
|
}).parse(record);
|
|
@@ -10164,46 +10781,46 @@ async function runSeoCommand(options) {
|
|
|
10164
10781
|
notificationRoute.parse(record.route);
|
|
10165
10782
|
}
|
|
10166
10783
|
} else if (!writing) {
|
|
10167
|
-
const validated =
|
|
10168
|
-
resource:
|
|
10169
|
-
id:
|
|
10170
|
-
organizationId:
|
|
10784
|
+
const validated = z18.object({
|
|
10785
|
+
resource: z18.object({
|
|
10786
|
+
id: z18.literal(context.resourceId),
|
|
10787
|
+
organizationId: z18.literal(context.overview.project.organizationId)
|
|
10171
10788
|
}),
|
|
10172
|
-
audits:
|
|
10173
|
-
audit:
|
|
10174
|
-
id:
|
|
10175
|
-
resourceId:
|
|
10789
|
+
audits: z18.array(z18.object({ id: z18.string() }).passthrough()),
|
|
10790
|
+
audit: z18.object({
|
|
10791
|
+
id: z18.string(),
|
|
10792
|
+
resourceId: z18.literal(context.resourceId)
|
|
10176
10793
|
}).passthrough().nullable(),
|
|
10177
|
-
pages:
|
|
10178
|
-
issues:
|
|
10179
|
-
changes:
|
|
10180
|
-
totalChanges:
|
|
10181
|
-
dispositions:
|
|
10794
|
+
pages: z18.array(z18.unknown()),
|
|
10795
|
+
issues: z18.array(z18.unknown()),
|
|
10796
|
+
changes: z18.array(z18.unknown()),
|
|
10797
|
+
totalChanges: z18.number(),
|
|
10798
|
+
dispositions: z18.array(z18.unknown())
|
|
10182
10799
|
}).passthrough().parse(record);
|
|
10183
10800
|
const selected = query.get("audit");
|
|
10184
10801
|
if (selected && validated.audit?.id !== selected)
|
|
10185
10802
|
throw new Error("The SEO response does not match the requested audit.");
|
|
10186
10803
|
} else if (record.audit)
|
|
10187
|
-
|
|
10188
|
-
id:
|
|
10189
|
-
resourceId:
|
|
10190
|
-
organizationId:
|
|
10191
|
-
state:
|
|
10804
|
+
z18.object({
|
|
10805
|
+
id: z18.string(),
|
|
10806
|
+
resourceId: z18.literal(context.resourceId),
|
|
10807
|
+
organizationId: z18.literal(context.overview.project.organizationId),
|
|
10808
|
+
state: z18.literal("queued")
|
|
10192
10809
|
}).parse(record.audit);
|
|
10193
|
-
else if (route === "audit cancel")
|
|
10810
|
+
else if (route === "audit cancel") z18.literal(true).parse(record.cancelled);
|
|
10194
10811
|
else if (action === "issue")
|
|
10195
|
-
|
|
10196
|
-
url:
|
|
10197
|
-
ruleId:
|
|
10198
|
-
ignored:
|
|
10199
|
-
revision:
|
|
10812
|
+
z18.object({
|
|
10813
|
+
url: z18.literal(values.url),
|
|
10814
|
+
ruleId: z18.literal(values.rule),
|
|
10815
|
+
ignored: z18.literal(positionals[1] === "ignore"),
|
|
10816
|
+
revision: z18.literal(Number(values.revision) + 1)
|
|
10200
10817
|
}).parse(record.disposition);
|
|
10201
10818
|
else throw new Error("The SEO service returned an invalid response.");
|
|
10202
10819
|
return { exitCode: 0, stdout: JSON.stringify(record, null, 2) };
|
|
10203
10820
|
} catch (cause) {
|
|
10204
10821
|
const error = {
|
|
10205
10822
|
code: cause instanceof SiteOSAuthApiError ? cause.code : "SEO_COMMAND_FAILED",
|
|
10206
|
-
message: cause instanceof
|
|
10823
|
+
message: cause instanceof z18.ZodError ? "The SEO service returned an invalid response." : cause instanceof Error ? cause.message : "The SEO command failed."
|
|
10207
10824
|
};
|
|
10208
10825
|
return {
|
|
10209
10826
|
exitCode: cause instanceof SiteOSAuthApiError ? 1 : 2,
|
|
@@ -10245,6 +10862,12 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
10245
10862
|
}
|
|
10246
10863
|
function rootCommandRegistry() {
|
|
10247
10864
|
return createCommandRegistry({
|
|
10865
|
+
analytics: (args2) => runAnalyticsCommand({
|
|
10866
|
+
args: args2,
|
|
10867
|
+
cwd: process.cwd(),
|
|
10868
|
+
env: process.env,
|
|
10869
|
+
fetchImpl: globalThis.fetch
|
|
10870
|
+
}),
|
|
10248
10871
|
seo: (args2) => runSeoCommand({
|
|
10249
10872
|
args: args2,
|
|
10250
10873
|
cwd: process.cwd(),
|
|
@@ -10358,6 +10981,7 @@ Usage:
|
|
|
10358
10981
|
siteos project --help
|
|
10359
10982
|
siteos cookie --help
|
|
10360
10983
|
siteos trace --help
|
|
10984
|
+
siteos analytics --help
|
|
10361
10985
|
siteos seo --help
|
|
10362
10986
|
siteos integrations --help
|
|
10363
10987
|
siteos pulse --help
|
|
@@ -10370,6 +10994,7 @@ Commands:
|
|
|
10370
10994
|
project Select one Project and configure its services and environments.
|
|
10371
10995
|
cookie Configure, publish, and inspect the Project\u2019s cookie banner.
|
|
10372
10996
|
trace Configure analytics observation and inspect evidence.
|
|
10997
|
+
analytics Configure website Analytics, events and realtime reports.
|
|
10373
10998
|
seo Audit HTML, inspect changes and verify fixes.
|
|
10374
10999
|
integrations Manage Organization connections and destinations.
|
|
10375
11000
|
pulse Manage monitoring checks, tests, and deployments.
|