@drip-apex/apex-cli 0.6.3 → 0.8.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/AGENTS.md +20 -3
- package/README.md +55 -2
- package/dist/cli.js +1525 -345
- package/package.json +1 -1
- package/skills/apex-cli/SKILL.md +11 -2
package/dist/cli.js
CHANGED
|
@@ -6158,16 +6158,16 @@ __export(ora_exports, {
|
|
|
6158
6158
|
function ora(options) {
|
|
6159
6159
|
return new Ora(options);
|
|
6160
6160
|
}
|
|
6161
|
-
async function oraPromise(
|
|
6162
|
-
const actionIsFunction = typeof
|
|
6163
|
-
const actionIsPromise = typeof
|
|
6161
|
+
async function oraPromise(action74, options) {
|
|
6162
|
+
const actionIsFunction = typeof action74 === "function";
|
|
6163
|
+
const actionIsPromise = typeof action74.then === "function";
|
|
6164
6164
|
if (!actionIsFunction && !actionIsPromise) {
|
|
6165
6165
|
throw new TypeError("Parameter `action` must be a Function or a Promise");
|
|
6166
6166
|
}
|
|
6167
6167
|
const { successText, failText } = typeof options === "object" ? options : { successText: void 0, failText: void 0 };
|
|
6168
6168
|
const spinner = ora(options).start();
|
|
6169
6169
|
try {
|
|
6170
|
-
const promise = actionIsFunction ?
|
|
6170
|
+
const promise = actionIsFunction ? action74(spinner) : action74;
|
|
6171
6171
|
const result = await promise;
|
|
6172
6172
|
spinner.succeed(
|
|
6173
6173
|
successText === void 0 ? void 0 : typeof successText === "string" ? successText : successText(result)
|
|
@@ -7112,7 +7112,7 @@ var import_node_os3 = __toESM(require("node:os"));
|
|
|
7112
7112
|
// package.json
|
|
7113
7113
|
var package_default = {
|
|
7114
7114
|
name: "@drip-apex/apex-cli",
|
|
7115
|
-
version: "0.
|
|
7115
|
+
version: "0.8.0",
|
|
7116
7116
|
license: "MIT",
|
|
7117
7117
|
description: "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
|
|
7118
7118
|
private: false,
|
|
@@ -7310,6 +7310,15 @@ function classifyApiError(status2, details) {
|
|
|
7310
7310
|
function confirmationRequired(command, message, retry) {
|
|
7311
7311
|
return fail(command, message, EXIT_CODES.confirmationRequired, { retry });
|
|
7312
7312
|
}
|
|
7313
|
+
var liveSpinners = /* @__PURE__ */ new Set();
|
|
7314
|
+
var exitFallback;
|
|
7315
|
+
function drainAndExit(code) {
|
|
7316
|
+
for (const spinner of liveSpinners) spinner.stop();
|
|
7317
|
+
process.exitCode = code;
|
|
7318
|
+
if (exitFallback) clearTimeout(exitFallback);
|
|
7319
|
+
exitFallback = setTimeout(() => process.exit(code), 500);
|
|
7320
|
+
exitFallback.unref();
|
|
7321
|
+
}
|
|
7313
7322
|
async function createSpinner(message) {
|
|
7314
7323
|
if (jsonMode || !process.stderr.isTTY) {
|
|
7315
7324
|
return {
|
|
@@ -7328,7 +7337,20 @@ async function createSpinner(message) {
|
|
|
7328
7337
|
};
|
|
7329
7338
|
}
|
|
7330
7339
|
const { default: ora2 } = await Promise.resolve().then(() => (init_ora(), ora_exports));
|
|
7331
|
-
|
|
7340
|
+
const spinner = ora2(message);
|
|
7341
|
+
const start = spinner.start.bind(spinner);
|
|
7342
|
+
spinner.start = (...args) => {
|
|
7343
|
+
liveSpinners.add(spinner);
|
|
7344
|
+
return start(...args);
|
|
7345
|
+
};
|
|
7346
|
+
for (const method of ["stop", "succeed", "fail"]) {
|
|
7347
|
+
const original = spinner[method].bind(spinner);
|
|
7348
|
+
spinner[method] = (...args) => {
|
|
7349
|
+
liveSpinners.delete(spinner);
|
|
7350
|
+
return original(...args);
|
|
7351
|
+
};
|
|
7352
|
+
}
|
|
7353
|
+
return spinner.start();
|
|
7332
7354
|
}
|
|
7333
7355
|
async function maybeCheckForUpdate() {
|
|
7334
7356
|
if (!process.stderr.isTTY || process.env.APEX_NO_UPDATE_CHECK === "1" || CLI_VERSION === "unknown")
|
|
@@ -7350,7 +7372,8 @@ async function maybeCheckForUpdate() {
|
|
|
7350
7372
|
async function handleCliError(err, command) {
|
|
7351
7373
|
if (err && typeof err === "object" && "exitCode" in err && err.exitCode === 0) {
|
|
7352
7374
|
flushFallbackEnvelope();
|
|
7353
|
-
|
|
7375
|
+
drainAndExit(0);
|
|
7376
|
+
return;
|
|
7354
7377
|
}
|
|
7355
7378
|
const commanderExitCode = err && typeof err === "object" && "exitCode" in err && typeof err.exitCode === "number" ? err.exitCode : null;
|
|
7356
7379
|
const cliError = err instanceof CliError ? err : new CliError(
|
|
@@ -7371,7 +7394,7 @@ async function handleCliError(err, command) {
|
|
|
7371
7394
|
`);
|
|
7372
7395
|
}
|
|
7373
7396
|
}
|
|
7374
|
-
|
|
7397
|
+
drainAndExit(cliError.exitCode);
|
|
7375
7398
|
}
|
|
7376
7399
|
|
|
7377
7400
|
// ../api-client/src/credentials.ts
|
|
@@ -7754,6 +7777,15 @@ function upsertExperimentBySlug(creds, slug, body5) {
|
|
|
7754
7777
|
function createExperiment(creds, body5) {
|
|
7755
7778
|
return request(creds, "POST", "/api/v1/experiments", body5);
|
|
7756
7779
|
}
|
|
7780
|
+
function updateExperiment(creds, id, body5) {
|
|
7781
|
+
return request(creds, "PATCH", `/api/v1/experiments/${encodeURIComponent(id)}`, body5);
|
|
7782
|
+
}
|
|
7783
|
+
function attachExperimentIdea(creds, id, body5) {
|
|
7784
|
+
return request(creds, "POST", `/api/v1/experiments/${encodeURIComponent(id)}/idea`, body5);
|
|
7785
|
+
}
|
|
7786
|
+
function detachExperimentIdea(creds, id) {
|
|
7787
|
+
return request(creds, "DELETE", `/api/v1/experiments/${encodeURIComponent(id)}/idea`);
|
|
7788
|
+
}
|
|
7757
7789
|
function pauseExperiment(creds, id) {
|
|
7758
7790
|
return request(creds, "POST", `/api/v1/experiments/${id}/pause`);
|
|
7759
7791
|
}
|
|
@@ -7781,7 +7813,13 @@ function createExperimentScreenshots(creds, id, body5) {
|
|
|
7781
7813
|
}
|
|
7782
7814
|
async function getScreenshotJob(creds, jobId, timeoutMs) {
|
|
7783
7815
|
await testScreenshotDelay?.(timeoutMs);
|
|
7784
|
-
return request(
|
|
7816
|
+
return request(
|
|
7817
|
+
creds,
|
|
7818
|
+
"GET",
|
|
7819
|
+
`/api/v1/screenshots/${encodeURIComponent(jobId)}`,
|
|
7820
|
+
void 0,
|
|
7821
|
+
timeoutMs
|
|
7822
|
+
);
|
|
7785
7823
|
}
|
|
7786
7824
|
function planExperimentFilesystem(creds, id, body5) {
|
|
7787
7825
|
return request(
|
|
@@ -8122,8 +8160,8 @@ function requestTargetingResource(creds, method, kind5, id, body5, limit, cursor
|
|
|
8122
8160
|
const path24 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
|
|
8123
8161
|
return request(creds, method, path24, body5);
|
|
8124
8162
|
}
|
|
8125
|
-
function managedPath(kind5, id,
|
|
8126
|
-
return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${
|
|
8163
|
+
function managedPath(kind5, id, action74) {
|
|
8164
|
+
return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action74 ? `/${action74}` : ""}`;
|
|
8127
8165
|
}
|
|
8128
8166
|
function listManagedDeliveries(creds, kind5, limit) {
|
|
8129
8167
|
const q = limit ? `?limit=${limit}` : "";
|
|
@@ -8141,8 +8179,8 @@ function createManagedDelivery(creds, kind5, body5) {
|
|
|
8141
8179
|
function updateManagedDelivery(creds, kind5, id, body5) {
|
|
8142
8180
|
return request(creds, "PATCH", managedPath(kind5, id), body5);
|
|
8143
8181
|
}
|
|
8144
|
-
function transitionManagedDelivery(creds, kind5, id,
|
|
8145
|
-
return request(creds, "POST", managedPath(kind5, id,
|
|
8182
|
+
function transitionManagedDelivery(creds, kind5, id, action74) {
|
|
8183
|
+
return request(creds, "POST", managedPath(kind5, id, action74));
|
|
8146
8184
|
}
|
|
8147
8185
|
function listConfigResources(creds, family, opts) {
|
|
8148
8186
|
const params = new URLSearchParams();
|
|
@@ -8301,6 +8339,28 @@ function restoreBacklogInsight(creds, id) {
|
|
|
8301
8339
|
function deleteInsight(creds, kind5, id) {
|
|
8302
8340
|
return request(creds, "DELETE", insightPath(kind5, id));
|
|
8303
8341
|
}
|
|
8342
|
+
function listBacklogFields(creds, includeArchived) {
|
|
8343
|
+
const query = includeArchived ? "?include_archived=true" : "";
|
|
8344
|
+
return request(creds, "GET", `/api/v1/backlog/fields${query}`);
|
|
8345
|
+
}
|
|
8346
|
+
function createBacklogField(creds, body5) {
|
|
8347
|
+
return request(creds, "POST", "/api/v1/backlog/fields", body5);
|
|
8348
|
+
}
|
|
8349
|
+
function updateBacklogField(creds, id, body5) {
|
|
8350
|
+
return request(
|
|
8351
|
+
creds,
|
|
8352
|
+
"PATCH",
|
|
8353
|
+
`/api/v1/backlog/fields/${encodeURIComponent(id)}`,
|
|
8354
|
+
body5
|
|
8355
|
+
);
|
|
8356
|
+
}
|
|
8357
|
+
function archiveBacklogField(creds, id) {
|
|
8358
|
+
return request(
|
|
8359
|
+
creds,
|
|
8360
|
+
"DELETE",
|
|
8361
|
+
`/api/v1/backlog/fields/${encodeURIComponent(id)}`
|
|
8362
|
+
);
|
|
8363
|
+
}
|
|
8304
8364
|
var listShops = (c) => userRequest(c, "GET", "/api/v1/shops");
|
|
8305
8365
|
var getShop = (c, id) => userRequest(c, "GET", `/api/v1/shops/${id}`);
|
|
8306
8366
|
var createShop = (c, body5) => userRequest(c, "POST", "/api/v1/shops", body5);
|
|
@@ -8835,6 +8895,43 @@ var import_node_crypto4 = __toESM(require("node:crypto"));
|
|
|
8835
8895
|
var import_node_fs7 = __toESM(require("node:fs"));
|
|
8836
8896
|
var import_node_path5 = __toESM(require("node:path"));
|
|
8837
8897
|
|
|
8898
|
+
// src/lib/qa-vocabulary.ts
|
|
8899
|
+
var checklistStatuses = [
|
|
8900
|
+
"pending",
|
|
8901
|
+
"pass",
|
|
8902
|
+
"warning",
|
|
8903
|
+
"fail",
|
|
8904
|
+
"accepted",
|
|
8905
|
+
"skipped"
|
|
8906
|
+
];
|
|
8907
|
+
var evidenceStatuses = [
|
|
8908
|
+
"captured",
|
|
8909
|
+
"passed",
|
|
8910
|
+
"warning",
|
|
8911
|
+
"failed",
|
|
8912
|
+
"pending",
|
|
8913
|
+
"not_run"
|
|
8914
|
+
];
|
|
8915
|
+
var evidenceKinds = [
|
|
8916
|
+
"screenshot",
|
|
8917
|
+
"goal",
|
|
8918
|
+
"summary",
|
|
8919
|
+
"launch_capture",
|
|
8920
|
+
"selector_audit",
|
|
8921
|
+
"console_network",
|
|
8922
|
+
"qa_audit",
|
|
8923
|
+
"readiness"
|
|
8924
|
+
];
|
|
8925
|
+
|
|
8926
|
+
// src/lib/dashboard-url.ts
|
|
8927
|
+
function buildDashboardUrl(baseUrl, shopId, experimentId) {
|
|
8928
|
+
if (!shopId) return null;
|
|
8929
|
+
const url = new URL("/api/shops/switch", baseUrl);
|
|
8930
|
+
url.searchParams.set("shopId", shopId);
|
|
8931
|
+
url.searchParams.set("redirect", "/experiments/" + experimentId);
|
|
8932
|
+
return url.toString();
|
|
8933
|
+
}
|
|
8934
|
+
|
|
8838
8935
|
// ../api-client/src/qa-preview-url.ts
|
|
8839
8936
|
var FORCE_ALIAS_PARAM_NAMES = ["drip_experiment_id", "drip_experiment", "drip_variation_id", "drip_variation"];
|
|
8840
8937
|
function applyQaPreviewParams(url, params) {
|
|
@@ -8872,6 +8969,112 @@ function buildQaPreviewUrl2(storeUrl, experimentId, variationId) {
|
|
|
8872
8969
|
return url;
|
|
8873
8970
|
}
|
|
8874
8971
|
|
|
8972
|
+
// src/lib/test-key.ts
|
|
8973
|
+
var TEST_KEY_RE = /^([A-Z0-9]{2,8})-(\d+)$/;
|
|
8974
|
+
function isTestKeyReference(value) {
|
|
8975
|
+
return value === value.toUpperCase() && TEST_KEY_RE.test(value.toUpperCase());
|
|
8976
|
+
}
|
|
8977
|
+
|
|
8978
|
+
// src/lib/effective-page-targeting.ts
|
|
8979
|
+
var ALL_PAGES_LABEL = "All pages";
|
|
8980
|
+
var CHECKOUT_LABEL = "Checkout";
|
|
8981
|
+
var UNKNOWN_PAGE_NAME = "Saved Page";
|
|
8982
|
+
function isRecord(value) {
|
|
8983
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
8984
|
+
}
|
|
8985
|
+
function arrayOf(value) {
|
|
8986
|
+
if (Array.isArray(value)) return value;
|
|
8987
|
+
if (value === void 0 || value === null) return [];
|
|
8988
|
+
return [value];
|
|
8989
|
+
}
|
|
8990
|
+
function ruleKey(rule) {
|
|
8991
|
+
if (typeof rule === "string") return JSON.stringify(["include", "simple", rule.trim(), ""]);
|
|
8992
|
+
if (!isRecord(rule)) return "";
|
|
8993
|
+
const candidate = rule;
|
|
8994
|
+
return JSON.stringify([
|
|
8995
|
+
candidate.include === false ? "exclude" : "include",
|
|
8996
|
+
candidate.type ?? "simple",
|
|
8997
|
+
typeof candidate.pattern === "string" ? candidate.pattern : "",
|
|
8998
|
+
candidate.matchType ?? candidate.match_type ?? ""
|
|
8999
|
+
]);
|
|
9000
|
+
}
|
|
9001
|
+
function authoredUrlRules(raw) {
|
|
9002
|
+
if (Array.isArray(raw.authorUrl)) return raw.authorUrl;
|
|
9003
|
+
const url = [...arrayOf(raw.url), ...arrayOf(raw.urls), ...arrayOf(raw.rules)];
|
|
9004
|
+
if (url.length === 0) return [];
|
|
9005
|
+
const derived = /* @__PURE__ */ new Set();
|
|
9006
|
+
for (const candidate of arrayOf(raw.pageRules)) {
|
|
9007
|
+
if (!isRecord(candidate)) continue;
|
|
9008
|
+
for (const rule of [...arrayOf(candidate.includeRules), ...arrayOf(candidate.excludeRules)]) {
|
|
9009
|
+
derived.add(ruleKey(rule));
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
9012
|
+
return url.filter((rule) => !derived.has(ruleKey(rule)));
|
|
9013
|
+
}
|
|
9014
|
+
function pageEntries(raw) {
|
|
9015
|
+
const byId = /* @__PURE__ */ new Map();
|
|
9016
|
+
for (const candidate of arrayOf(raw.pageRules)) {
|
|
9017
|
+
if (!isRecord(candidate) || typeof candidate.id !== "string" || !candidate.id) continue;
|
|
9018
|
+
byId.set(candidate.id, {
|
|
9019
|
+
name: typeof candidate.name === "string" && candidate.name.trim() ? candidate.name.trim() : null,
|
|
9020
|
+
include: candidate.include !== false
|
|
9021
|
+
});
|
|
9022
|
+
}
|
|
9023
|
+
for (const candidate of arrayOf(raw.pages)) {
|
|
9024
|
+
const id = typeof candidate === "string" ? candidate : isRecord(candidate) && typeof candidate.id === "string" ? candidate.id : "";
|
|
9025
|
+
if (!id) continue;
|
|
9026
|
+
byId.set(id, {
|
|
9027
|
+
name: byId.get(id)?.name ?? null,
|
|
9028
|
+
include: isRecord(candidate) && candidate.include === false ? false : true
|
|
9029
|
+
});
|
|
9030
|
+
}
|
|
9031
|
+
return [...byId.values()];
|
|
9032
|
+
}
|
|
9033
|
+
function urlRuleClause(count) {
|
|
9034
|
+
return count === 1 ? "1 URL rule" : `${count} URL rules`;
|
|
9035
|
+
}
|
|
9036
|
+
function effectivePageTargetingLabel(targeting) {
|
|
9037
|
+
const raw = isRecord(targeting) ? targeting : {};
|
|
9038
|
+
if (raw.surface === "checkout" || isRecord(raw.checkoutBlock) && raw.checkoutBlock.enabled === true) {
|
|
9039
|
+
return CHECKOUT_LABEL;
|
|
9040
|
+
}
|
|
9041
|
+
const pages = pageEntries(raw);
|
|
9042
|
+
const rules = authoredUrlRules(raw);
|
|
9043
|
+
const pagesLabel = pages.map((page) => {
|
|
9044
|
+
const readable = page.name ?? UNKNOWN_PAGE_NAME;
|
|
9045
|
+
return page.include ? readable : `Exclude ${readable}`;
|
|
9046
|
+
}).join(", ");
|
|
9047
|
+
if (pages.length && rules.length) return `${pagesLabel} and ${urlRuleClause(rules.length)}`;
|
|
9048
|
+
if (pages.length) return pagesLabel;
|
|
9049
|
+
if (rules.length) return `${urlRuleClause(rules.length)} (no saved Page)`;
|
|
9050
|
+
return ALL_PAGES_LABEL;
|
|
9051
|
+
}
|
|
9052
|
+
|
|
9053
|
+
// ../api-client/src/idea-reference.ts
|
|
9054
|
+
var IDEA_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
9055
|
+
var IDEA_KEY = /^[A-Za-z0-9]{2,8}-\d+$/;
|
|
9056
|
+
function classifyIdeaReference(reference) {
|
|
9057
|
+
const trimmed = reference.trim();
|
|
9058
|
+
if (IDEA_UUID.test(trimmed)) return { kind: "uuid", value: trimmed };
|
|
9059
|
+
if (IDEA_KEY.test(trimmed))
|
|
9060
|
+
return { kind: "key", value: trimmed.toUpperCase() };
|
|
9061
|
+
return { kind: "invalid", value: trimmed };
|
|
9062
|
+
}
|
|
9063
|
+
|
|
9064
|
+
// ../api-client/src/experiment-create.ts
|
|
9065
|
+
function deriveStoreUrl(patterns) {
|
|
9066
|
+
for (const pattern of patterns) {
|
|
9067
|
+
const trimmed = pattern.trim();
|
|
9068
|
+
if (trimmed.includes("*")) continue;
|
|
9069
|
+
try {
|
|
9070
|
+
const url = new URL(trimmed);
|
|
9071
|
+
if (url.protocol === "http:" || url.protocol === "https:") return trimmed;
|
|
9072
|
+
} catch {
|
|
9073
|
+
}
|
|
9074
|
+
}
|
|
9075
|
+
return null;
|
|
9076
|
+
}
|
|
9077
|
+
|
|
8875
9078
|
// src/lib/asset-upload.ts
|
|
8876
9079
|
var import_node_crypto3 = __toESM(require("node:crypto"));
|
|
8877
9080
|
var import_node_fs6 = __toESM(require("node:fs"));
|
|
@@ -8951,6 +9154,7 @@ function createAssetReferenceRewriter(options) {
|
|
|
8951
9154
|
let pending = uploadsByHash.get(hash);
|
|
8952
9155
|
if (!pending) {
|
|
8953
9156
|
pending = options.upload(file).then((asset) => {
|
|
9157
|
+
if (asset === null) return null;
|
|
8954
9158
|
if (!asset.id) throw new Error(`Asset upload for ${file.name} returned no ID.`);
|
|
8955
9159
|
const url = deliveryUrl(options.baseUrl, asset);
|
|
8956
9160
|
options.onUpload?.({ name: file.name, url, source });
|
|
@@ -8979,7 +9183,7 @@ function createAssetReferenceRewriter(options) {
|
|
|
8979
9183
|
const hash = contentHash(bytes);
|
|
8980
9184
|
const name = `inline-${hash.slice(0, 12)}${extensionForMimeType(mimeType)}`;
|
|
8981
9185
|
const url = await uploadOnce({ name, bytes, type: mimeType }, "inline");
|
|
8982
|
-
rewritten = rewritten.split(uri).join(url);
|
|
9186
|
+
if (url !== null) rewritten = rewritten.split(uri).join(url);
|
|
8983
9187
|
}
|
|
8984
9188
|
const localAssetUrl = async (reference) => {
|
|
8985
9189
|
if (reference.startsWith("/") || reference.startsWith("data:") || reference.includes("://")) {
|
|
@@ -9066,14 +9270,24 @@ function formatDate(iso) {
|
|
|
9066
9270
|
year: "numeric"
|
|
9067
9271
|
});
|
|
9068
9272
|
}
|
|
9069
|
-
function printExperiment(exp) {
|
|
9273
|
+
function printExperiment(exp, dashboardUrl = null) {
|
|
9070
9274
|
console.log();
|
|
9071
9275
|
console.log(source_default.bold(exp.name));
|
|
9276
|
+
if (dashboardUrl) console.log(` Dashboard: ${dashboardUrl}`);
|
|
9072
9277
|
console.log(` ID: ${source_default.dim(exp.id)}`);
|
|
9073
9278
|
if (exp.slug) console.log(` Slug: ${source_default.cyan(exp.slug)}`);
|
|
9074
9279
|
console.log(` Status: ${statusBadge(exp.status)}`);
|
|
9280
|
+
console.log(` Classification: ${exp.counting_type}`);
|
|
9075
9281
|
console.log(` Traffic: ${(exp.traffic_allocation * 100).toFixed(0)}%`);
|
|
9282
|
+
if (exp.targeting !== void 0) {
|
|
9283
|
+
console.log(` Targeting: ${effectivePageTargetingLabel(exp.targeting)}`);
|
|
9284
|
+
}
|
|
9076
9285
|
console.log(` Created: ${formatDate(exp.created_at)}`);
|
|
9286
|
+
if (exp.linked_idea) {
|
|
9287
|
+
const label = exp.linked_idea.key ? `${exp.linked_idea.key}: ` : "";
|
|
9288
|
+
const stage = exp.linked_idea.stage ? ` (${exp.linked_idea.stage})` : "";
|
|
9289
|
+
console.log(` Idea: ${label}${exp.linked_idea.title}${stage}`);
|
|
9290
|
+
}
|
|
9077
9291
|
if (exp.variations && exp.variations.length > 0) {
|
|
9078
9292
|
console.log(` Variations:`);
|
|
9079
9293
|
for (const v of exp.variations) {
|
|
@@ -9122,6 +9336,21 @@ function printExperimentResults(results2, currency) {
|
|
|
9122
9336
|
console.log(` Conversions: ${String(totals.primaryConversions ?? 0)}`);
|
|
9123
9337
|
console.log(` Revenue: ${formatRevenue(totals.revenue, currency)}`);
|
|
9124
9338
|
console.log(` Outcome: ${statisticsStateLabel(presentedOutcome)}${outcomeProvenance}`);
|
|
9339
|
+
const resultsWeekdays = results2.results_weekdays;
|
|
9340
|
+
if (resultsWeekdays && Array.isArray(resultsWeekdays.days)) {
|
|
9341
|
+
const labels = {
|
|
9342
|
+
monday: "Mon",
|
|
9343
|
+
tuesday: "Tue",
|
|
9344
|
+
wednesday: "Wed",
|
|
9345
|
+
thursday: "Thu",
|
|
9346
|
+
friday: "Fri",
|
|
9347
|
+
saturday: "Sat",
|
|
9348
|
+
sunday: "Sun"
|
|
9349
|
+
};
|
|
9350
|
+
console.log(
|
|
9351
|
+
` Judged on: ${resultsWeekdays.days.map((day) => labels[String(day)] ?? day).join(", ")} (${String(resultsWeekdays.timezone)})`
|
|
9352
|
+
);
|
|
9353
|
+
}
|
|
9125
9354
|
console.log(` Statistics: ${statisticsStateLabel(statisticsState)}`);
|
|
9126
9355
|
if (decision.treatmentName) {
|
|
9127
9356
|
console.log(` Treatment: ${String(decision.treatmentName)}`);
|
|
@@ -9275,21 +9504,26 @@ async function rewriteExperimentFilesystemAssets(input) {
|
|
|
9275
9504
|
baseUrl: input.baseUrl,
|
|
9276
9505
|
enabled: input.enabled,
|
|
9277
9506
|
upload: async (file) => {
|
|
9507
|
+
const hash = import_node_crypto4.default.createHash("sha256").update(file.bytes).digest("hex");
|
|
9508
|
+
const cached = input.assets[hash];
|
|
9509
|
+
if (cached) return cached;
|
|
9510
|
+
if (input.upload === null) return null;
|
|
9278
9511
|
const response = await input.upload(input.credentials, [file]);
|
|
9279
9512
|
const asset = response.data?.assets?.[0];
|
|
9280
9513
|
if (!response.ok || !asset?.id) {
|
|
9281
9514
|
throw new Error(response.error ?? `Failed to upload ${file.name}.`);
|
|
9282
9515
|
}
|
|
9283
|
-
|
|
9516
|
+
const uploaded = {
|
|
9284
9517
|
id: asset.id,
|
|
9285
|
-
url: typeof asset.url === "string" ? asset.url :
|
|
9286
|
-
deliveryUrl: typeof asset.deliveryUrl === "string" ? asset.deliveryUrl : void 0
|
|
9287
|
-
delivery_url: typeof asset.delivery_url === "string" ? asset.delivery_url : void 0
|
|
9518
|
+
url: typeof asset.url === "string" ? asset.url : `${new URL(input.baseUrl).origin}/api/assets/${encodeURIComponent(asset.id)}/file`,
|
|
9519
|
+
deliveryUrl: typeof asset.deliveryUrl === "string" ? asset.deliveryUrl : typeof asset.delivery_url === "string" ? asset.delivery_url : void 0
|
|
9288
9520
|
};
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
`
|
|
9521
|
+
input.assets[hash] = uploaded;
|
|
9522
|
+
process.stderr.write(
|
|
9523
|
+
`Uploaded asset ${file.name} -> ${uploaded.deliveryUrl ?? uploaded.url}
|
|
9524
|
+
`
|
|
9525
|
+
);
|
|
9526
|
+
return uploaded;
|
|
9293
9527
|
}
|
|
9294
9528
|
});
|
|
9295
9529
|
return Promise.all(
|
|
@@ -9335,6 +9569,7 @@ function printFilesystemPlan(plan) {
|
|
|
9335
9569
|
}
|
|
9336
9570
|
console.log();
|
|
9337
9571
|
console.log(`QA invalidated: ${plan.invalidatesQa ? source_default.yellow("yes") : "no"}`);
|
|
9572
|
+
console.log(`Acknowledgment: ${plan.invalidatesQa ? "--accept-qa-invalidation" : "none"}`);
|
|
9338
9573
|
console.log(`Precompile: ${plan.precompileRequired ? "yes" : "no"}`);
|
|
9339
9574
|
}
|
|
9340
9575
|
function blockingIssueSummary(issues) {
|
|
@@ -9343,11 +9578,21 @@ function blockingIssueSummary(issues) {
|
|
|
9343
9578
|
return `Experiment filesystem plan has errors. ${issues[0].message}${remaining ? ` and ${remaining} more` : ""}`;
|
|
9344
9579
|
}
|
|
9345
9580
|
async function getQaPreviewUrls(creds, experimentId) {
|
|
9581
|
+
const fallbackDashboardUrl = buildDashboardUrl(
|
|
9582
|
+
creds.baseUrl,
|
|
9583
|
+
creds.resolvedContext?.shopId,
|
|
9584
|
+
experimentId
|
|
9585
|
+
);
|
|
9346
9586
|
try {
|
|
9347
9587
|
const res = await getExperiment(creds, experimentId);
|
|
9588
|
+
const dashboardUrl = buildDashboardUrl(
|
|
9589
|
+
creds.baseUrl,
|
|
9590
|
+
res.data?.shop_id ?? creds.resolvedContext?.shopId,
|
|
9591
|
+
experimentId
|
|
9592
|
+
);
|
|
9348
9593
|
const storeUrl = res.data?.store_url;
|
|
9349
9594
|
if (!res.ok || !res.data || !storeUrl) {
|
|
9350
|
-
return { previewUrls: [], unavailable: true };
|
|
9595
|
+
return { previewUrls: [], unavailable: true, dashboardUrl };
|
|
9351
9596
|
}
|
|
9352
9597
|
const experiment = res.data;
|
|
9353
9598
|
const previewUrls = (experiment.variations ?? []).filter((variation) => !variation.is_control).map((variation) => ({
|
|
@@ -9355,9 +9600,9 @@ async function getQaPreviewUrls(creds, experimentId) {
|
|
|
9355
9600
|
variationName: variation.name,
|
|
9356
9601
|
url: buildQaPreviewUrl2(storeUrl, experiment.id, variation.id).toString()
|
|
9357
9602
|
}));
|
|
9358
|
-
return { previewUrls, unavailable: false };
|
|
9603
|
+
return { previewUrls, unavailable: false, dashboardUrl };
|
|
9359
9604
|
} catch {
|
|
9360
|
-
return { previewUrls: [], unavailable: true };
|
|
9605
|
+
return { previewUrls: [], unavailable: true, dashboardUrl: fallbackDashboardUrl };
|
|
9361
9606
|
}
|
|
9362
9607
|
}
|
|
9363
9608
|
function printQaPreviewUrls(experimentId, result) {
|
|
@@ -9430,8 +9675,29 @@ function parseEvidenceInput(value) {
|
|
|
9430
9675
|
}
|
|
9431
9676
|
return parsed;
|
|
9432
9677
|
}
|
|
9433
|
-
function
|
|
9434
|
-
return
|
|
9678
|
+
function qaChoice(flags, description, choices) {
|
|
9679
|
+
return new Option(flags, description).choices([...choices]).argParser((value) => value);
|
|
9680
|
+
}
|
|
9681
|
+
function validateQaChoice(command, option, value, allowed2) {
|
|
9682
|
+
if (value !== void 0 && !allowed2.includes(value)) {
|
|
9683
|
+
fail(command, `Invalid ${option}: ${value}`, EXIT_CODES.validation, {
|
|
9684
|
+
option,
|
|
9685
|
+
allowedValues: [...allowed2]
|
|
9686
|
+
});
|
|
9687
|
+
}
|
|
9688
|
+
}
|
|
9689
|
+
function qaSummary(run3, changed) {
|
|
9690
|
+
return {
|
|
9691
|
+
run: {
|
|
9692
|
+
id: run3.id,
|
|
9693
|
+
status: run3.status,
|
|
9694
|
+
summary: run3.summary,
|
|
9695
|
+
approvedAt: run3.approvedAt,
|
|
9696
|
+
updatedAt: run3.updatedAt
|
|
9697
|
+
},
|
|
9698
|
+
changed,
|
|
9699
|
+
validationErrors: []
|
|
9700
|
+
};
|
|
9435
9701
|
}
|
|
9436
9702
|
function tryReadFilesystemSession(folder) {
|
|
9437
9703
|
try {
|
|
@@ -9455,13 +9721,15 @@ async function resolveExperimentForFilesystemOrId(value) {
|
|
|
9455
9721
|
return { experimentId: exp.id, folder: null, label: exp.name };
|
|
9456
9722
|
}
|
|
9457
9723
|
async function refreshExperimentFilesystemFolderIfNeeded(creds, folder, experimentId) {
|
|
9458
|
-
if (!folder) return;
|
|
9724
|
+
if (!folder) return false;
|
|
9459
9725
|
try {
|
|
9460
9726
|
const refreshed = await getExperimentFilesystem(creds, experimentId);
|
|
9461
9727
|
if (refreshed.ok && refreshed.data) {
|
|
9728
|
+
const assets = tryReadFilesystemSession(folder)?.assets;
|
|
9462
9729
|
writeExperimentFilesystemFiles(folder, refreshed.data.files);
|
|
9463
9730
|
writeFilesystemSession(folder, {
|
|
9464
9731
|
schemaVersion: 1,
|
|
9732
|
+
...assets ? { assets } : {},
|
|
9465
9733
|
baseUrl: creds.baseUrl,
|
|
9466
9734
|
experimentId,
|
|
9467
9735
|
pulledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -9469,11 +9737,13 @@ async function refreshExperimentFilesystemFolderIfNeeded(creds, folder, experime
|
|
|
9469
9737
|
refreshed.data.files.map((file) => [file.path, { sha256: file.sha256 }])
|
|
9470
9738
|
)
|
|
9471
9739
|
});
|
|
9740
|
+
return true;
|
|
9472
9741
|
}
|
|
9473
9742
|
} catch {
|
|
9474
9743
|
}
|
|
9744
|
+
return false;
|
|
9475
9745
|
}
|
|
9476
|
-
async function refreshServerOwnedFilesIfNeeded(creds, folder, experimentId) {
|
|
9746
|
+
async function refreshServerOwnedFilesIfNeeded(creds, folder, experimentId, updateManifest = true) {
|
|
9477
9747
|
if (!folder) return;
|
|
9478
9748
|
try {
|
|
9479
9749
|
const refreshed = await getExperimentFilesystem(creds, experimentId);
|
|
@@ -9484,23 +9754,25 @@ async function refreshServerOwnedFilesIfNeeded(creds, folder, experimentId) {
|
|
|
9484
9754
|
if (serverOwned.length === 0) return;
|
|
9485
9755
|
const session = readFilesystemSession(folder);
|
|
9486
9756
|
writeExperimentFilesystemFiles(folder, serverOwned);
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9757
|
+
if (updateManifest)
|
|
9758
|
+
writeFilesystemSession(folder, {
|
|
9759
|
+
...session,
|
|
9760
|
+
files: {
|
|
9761
|
+
...session.files,
|
|
9762
|
+
...Object.fromEntries(serverOwned.map((file) => [file.path, { sha256: file.sha256 }]))
|
|
9763
|
+
}
|
|
9764
|
+
});
|
|
9494
9765
|
} catch {
|
|
9495
9766
|
}
|
|
9496
9767
|
}
|
|
9768
|
+
var EXPERIMENT_REFERENCE_DESCRIPTION = "Experiment ID, slug, or test key (for example BIO-085)";
|
|
9497
9769
|
async function resolveExperiment(idOrSlug) {
|
|
9498
9770
|
const creds = requireCredentials();
|
|
9499
|
-
if (/^[0-9a-f-]{36}$/i.test(idOrSlug)) {
|
|
9771
|
+
if (/^[0-9a-f-]{36}$/i.test(idOrSlug) || /^[1-9][0-9]{9}$/.test(idOrSlug) || isTestKeyReference(idOrSlug)) {
|
|
9500
9772
|
const res2 = await getExperiment(creds, idOrSlug);
|
|
9501
9773
|
if (res2.ok) return res2.data;
|
|
9502
9774
|
throw new CliError(
|
|
9503
|
-
res2.error ?? `Experiment "${idOrSlug}" not found`,
|
|
9775
|
+
res2.status === 404 && isTestKeyReference(idOrSlug) ? `Experiment "${idOrSlug}" not found in selected shop ${creds.resolvedContext?.shopId ?? "(API key shop)"}. Test keys resolve within the selected shop; use --shop <shop-id> to change it.` : res2.error ?? `Experiment "${idOrSlug}" not found`,
|
|
9504
9776
|
classifyStatus(res2.status),
|
|
9505
9777
|
res2.errorDetails
|
|
9506
9778
|
);
|
|
@@ -9554,16 +9826,59 @@ var list = new Command("list").alias("ls").description("List experiments (archiv
|
|
|
9554
9826
|
}
|
|
9555
9827
|
});
|
|
9556
9828
|
});
|
|
9557
|
-
var get = new Command("get").description("Get experiment details by ID or slug").argument("<id-or-slug>",
|
|
9829
|
+
var get = new Command("get").description("Get experiment details by ID or slug").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).action(async function action4(idOrSlug) {
|
|
9558
9830
|
const spinner = await createSpinner("Fetching experiment...");
|
|
9559
9831
|
const exp = await resolveExperiment(idOrSlug);
|
|
9560
9832
|
spinner?.stop();
|
|
9561
9833
|
if (!exp) {
|
|
9562
9834
|
fail(this, `Experiment "${idOrSlug}" not found.`, EXIT_CODES.validation);
|
|
9563
9835
|
}
|
|
9564
|
-
|
|
9836
|
+
const creds = requireCredentials();
|
|
9837
|
+
const dashboardUrl = buildDashboardUrl(
|
|
9838
|
+
creds.baseUrl,
|
|
9839
|
+
exp.shop_id ?? creds.resolvedContext?.shopId,
|
|
9840
|
+
exp.id
|
|
9841
|
+
);
|
|
9842
|
+
output(
|
|
9843
|
+
this,
|
|
9844
|
+
{ experiment: exp, dashboard_url: dashboardUrl },
|
|
9845
|
+
() => printExperiment(exp, dashboardUrl)
|
|
9846
|
+
);
|
|
9565
9847
|
});
|
|
9566
|
-
var
|
|
9848
|
+
var attachIdea = new Command("attach-idea").description("Attach an experiment to an idea").argument("<experiment>", EXPERIMENT_REFERENCE_DESCRIPTION).requiredOption("--idea <key>", "Idea UUID or Apex backlog key").option("--follow-up", "Attach as a follow-up test").action(async function action5(experimentRef, opts) {
|
|
9849
|
+
const experiment = await resolveExperiment(experimentRef);
|
|
9850
|
+
if (!experiment) fail(this, `Experiment "${experimentRef}" not found.`, EXIT_CODES.validation);
|
|
9851
|
+
const classified = classifyIdeaReference(opts.idea);
|
|
9852
|
+
if (classified.kind === "invalid") {
|
|
9853
|
+
fail(this, "--idea must be a UUID or Apex backlog key", EXIT_CODES.validation);
|
|
9854
|
+
}
|
|
9855
|
+
const res = await attachExperimentIdea(requireCredentials(), experiment.id, {
|
|
9856
|
+
...classified.kind === "uuid" ? { backlog_item_id: classified.value } : { idea_key: classified.value },
|
|
9857
|
+
...opts.followUp ? { as_follow_up: true } : {}
|
|
9858
|
+
});
|
|
9859
|
+
if (!res.ok)
|
|
9860
|
+
fail(
|
|
9861
|
+
this,
|
|
9862
|
+
res.error ?? "Failed to attach idea",
|
|
9863
|
+
classifyStatus(res.status),
|
|
9864
|
+
res.errorDetails
|
|
9865
|
+
);
|
|
9866
|
+
output(this, res.data, () => printSuccess("Idea attached."));
|
|
9867
|
+
});
|
|
9868
|
+
var detachIdea = new Command("detach-idea").description("Detach an experiment from its idea").argument("<experiment>", EXPERIMENT_REFERENCE_DESCRIPTION).action(async function action6(experimentRef) {
|
|
9869
|
+
const experiment = await resolveExperiment(experimentRef);
|
|
9870
|
+
if (!experiment) fail(this, `Experiment "${experimentRef}" not found.`, EXIT_CODES.validation);
|
|
9871
|
+
const res = await detachExperimentIdea(requireCredentials(), experiment.id);
|
|
9872
|
+
if (!res.ok)
|
|
9873
|
+
fail(
|
|
9874
|
+
this,
|
|
9875
|
+
res.error ?? "Failed to detach idea",
|
|
9876
|
+
classifyStatus(res.status),
|
|
9877
|
+
res.errorDetails
|
|
9878
|
+
);
|
|
9879
|
+
output(this, res.data, () => printSuccess("Idea detached."));
|
|
9880
|
+
});
|
|
9881
|
+
var results = new Command("results").description("Read experiment results and timeseries by ID or slug").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--currency <code>", "Currency code for revenue formatting").action(async function action7(idOrSlug, opts) {
|
|
9567
9882
|
const creds = requireCredentials();
|
|
9568
9883
|
const spinner = await createSpinner("Fetching experiment results...");
|
|
9569
9884
|
try {
|
|
@@ -9594,6 +9909,62 @@ var results = new Command("results").description("Read experiment results and ti
|
|
|
9594
9909
|
fail(this, error instanceof Error ? error.message : String(error));
|
|
9595
9910
|
}
|
|
9596
9911
|
});
|
|
9912
|
+
var DAY_NAMES = {
|
|
9913
|
+
mon: "monday",
|
|
9914
|
+
monday: "monday",
|
|
9915
|
+
tue: "tuesday",
|
|
9916
|
+
tuesday: "tuesday",
|
|
9917
|
+
wed: "wednesday",
|
|
9918
|
+
wednesday: "wednesday",
|
|
9919
|
+
thu: "thursday",
|
|
9920
|
+
thursday: "thursday",
|
|
9921
|
+
fri: "friday",
|
|
9922
|
+
friday: "friday",
|
|
9923
|
+
sat: "saturday",
|
|
9924
|
+
saturday: "saturday",
|
|
9925
|
+
sun: "sunday",
|
|
9926
|
+
sunday: "sunday"
|
|
9927
|
+
};
|
|
9928
|
+
var countingTypes = ["standard", "setup_test", "aa_test", "non_program"];
|
|
9929
|
+
var update = new Command("update").description("Update an experiment").addOption(
|
|
9930
|
+
new Option("--counting-type <type>", "Draft experiment classification").choices(countingTypes)
|
|
9931
|
+
).argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--results-weekdays <days>", "Comma-separated weekdays").option("--results-timezone <timezone>", "IANA timezone for the results window").option("--clear-results-weekdays", "Clear the results weekday window").action(async function action8(reference, opts) {
|
|
9932
|
+
if (opts.clearResultsWeekdays && (opts.resultsWeekdays || opts.resultsTimezone)) {
|
|
9933
|
+
fail(
|
|
9934
|
+
this,
|
|
9935
|
+
"--clear-results-weekdays cannot be combined with window options",
|
|
9936
|
+
EXIT_CODES.validation
|
|
9937
|
+
);
|
|
9938
|
+
}
|
|
9939
|
+
const hasWindowOption = opts.resultsWeekdays !== void 0 || opts.resultsTimezone !== void 0;
|
|
9940
|
+
if (!opts.clearResultsWeekdays && (!opts.countingType && !hasWindowOption || hasWindowOption && (!opts.resultsWeekdays || !opts.resultsTimezone))) {
|
|
9941
|
+
fail(
|
|
9942
|
+
this,
|
|
9943
|
+
"--results-weekdays and --results-timezone are required together",
|
|
9944
|
+
EXIT_CODES.validation
|
|
9945
|
+
);
|
|
9946
|
+
}
|
|
9947
|
+
const days = opts.resultsWeekdays?.split(",").map((day) => DAY_NAMES[day.trim().toLowerCase()]);
|
|
9948
|
+
if (days?.some((day) => !day)) fail(this, "Invalid weekday token", EXIT_CODES.validation);
|
|
9949
|
+
const exp = await resolveExperiment(reference);
|
|
9950
|
+
if (!exp) fail(this, `Experiment "${reference}" not found.`, EXIT_CODES.validation);
|
|
9951
|
+
const results_weekdays = opts.clearResultsWeekdays ? null : { days, timezone: opts.resultsTimezone };
|
|
9952
|
+
const hasWindow = opts.clearResultsWeekdays || opts.resultsWeekdays !== void 0;
|
|
9953
|
+
const res = await updateExperiment(requireCredentials(), exp.id, {
|
|
9954
|
+
...hasWindow ? { results_weekdays } : {},
|
|
9955
|
+
...opts.countingType !== void 0 ? { counting_type: opts.countingType } : {}
|
|
9956
|
+
});
|
|
9957
|
+
if (!res.ok) fail(this, res.error ?? "Failed to update experiment", classifyStatus(res.status));
|
|
9958
|
+
const stored = res.data?.results_weekdays ?? null;
|
|
9959
|
+
output(this, res.data, () => {
|
|
9960
|
+
if (opts.countingType !== void 0)
|
|
9961
|
+
console.log(`Classification: ${res.data.counting_type}`);
|
|
9962
|
+
if (hasWindow)
|
|
9963
|
+
console.log(
|
|
9964
|
+
stored?.days?.length ? `Judged on: ${stored.days.map((day) => day.slice(0, 3).replace(/^./, (c) => c.toUpperCase())).join(", ")} (${stored.timezone})` : "Results weekday window cleared: judged on all days."
|
|
9965
|
+
);
|
|
9966
|
+
});
|
|
9967
|
+
});
|
|
9597
9968
|
function collectCreateOption(value, previous) {
|
|
9598
9969
|
return [...previous, value];
|
|
9599
9970
|
}
|
|
@@ -9610,30 +9981,238 @@ function parseHttpDestination(value) {
|
|
|
9610
9981
|
}
|
|
9611
9982
|
return trimmed;
|
|
9612
9983
|
}
|
|
9613
|
-
function
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
9621
|
-
|
|
9984
|
+
function readCreateJson(command, reference) {
|
|
9985
|
+
const file = reference.startsWith("@") ? reference.slice(1) : reference;
|
|
9986
|
+
try {
|
|
9987
|
+
const parsed = JSON.parse(import_node_fs7.default.readFileSync(import_node_path5.default.resolve(process.cwd(), file), "utf8"));
|
|
9988
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
9989
|
+
throw new Error("expected an object");
|
|
9990
|
+
return parsed;
|
|
9991
|
+
} catch (error) {
|
|
9992
|
+
fail(
|
|
9993
|
+
command,
|
|
9994
|
+
`Invalid audience JSON ${reference}: ${error instanceof Error ? error.message : String(error)}`,
|
|
9995
|
+
EXIT_CODES.validation
|
|
9996
|
+
);
|
|
9997
|
+
}
|
|
9998
|
+
}
|
|
9999
|
+
var GOAL_TYPES = ["click", "pageview", "custom", "revenue"];
|
|
10000
|
+
var GOAL_MEASUREMENTS = ["conversion", "value"];
|
|
10001
|
+
var GOAL_VALUE_UNITS = ["currency", "number", "duration_seconds", "percent"];
|
|
10002
|
+
var INLINE_GOAL_FIELDS = [
|
|
10003
|
+
"name",
|
|
10004
|
+
"type",
|
|
10005
|
+
"measurement",
|
|
10006
|
+
"value_property",
|
|
10007
|
+
"value_unit",
|
|
10008
|
+
"selector",
|
|
10009
|
+
"text_match",
|
|
10010
|
+
"url_pattern",
|
|
10011
|
+
"page_id",
|
|
10012
|
+
"event_name",
|
|
10013
|
+
"count_once"
|
|
10014
|
+
];
|
|
10015
|
+
var GOAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
10016
|
+
var GOAL_EVENT_NAME = /^[a-zA-Z0-9_.:-]{1,120}$/;
|
|
10017
|
+
var GOAL_VALUE_PROPERTY = /^[A-Za-z0-9_.-]{1,120}$/;
|
|
10018
|
+
function normalizeNewGoalKey(key) {
|
|
10019
|
+
const normalized = key.trim().toLowerCase().replace(/-/g, "_");
|
|
10020
|
+
return normalized === "page" ? "page_id" : normalized;
|
|
10021
|
+
}
|
|
10022
|
+
function validateNewGoal(command, value, body5) {
|
|
10023
|
+
const invalid = (reason) => fail(command, `Invalid --goal "new:${value}"; ${reason}`, EXIT_CODES.validation);
|
|
10024
|
+
const text = (field) => {
|
|
10025
|
+
const raw = body5[field];
|
|
10026
|
+
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
10027
|
+
};
|
|
10028
|
+
for (const key of Object.keys(body5)) {
|
|
10029
|
+
if (INLINE_GOAL_FIELDS.includes(key)) continue;
|
|
10030
|
+
invalid(
|
|
10031
|
+
key === "capture" || key === "filter" ? `${key} is not expressible inline; create the goal with \`apex goals create\` and pass its id` : `unknown field "${key}"; inline fields are ${INLINE_GOAL_FIELDS.join(", ")}`
|
|
10032
|
+
);
|
|
10033
|
+
}
|
|
10034
|
+
const type = text("type");
|
|
10035
|
+
const name = text("name");
|
|
10036
|
+
if (!type || !name) invalid("type and name are required");
|
|
10037
|
+
if (!GOAL_TYPES.includes(type)) invalid(`type must be one of ${GOAL_TYPES.join(", ")}`);
|
|
10038
|
+
if (name.length > 255) invalid("name must be 255 characters or fewer");
|
|
10039
|
+
const measurement = text("measurement") ?? "conversion";
|
|
10040
|
+
if (!GOAL_MEASUREMENTS.includes(measurement))
|
|
10041
|
+
invalid(`measurement must be one of ${GOAL_MEASUREMENTS.join(", ")}`);
|
|
10042
|
+
if (body5.count_once !== void 0) {
|
|
10043
|
+
const flag = String(body5.count_once).toLowerCase();
|
|
10044
|
+
if (flag !== "true" && flag !== "false") invalid("count-once must be true or false");
|
|
10045
|
+
body5.count_once = flag === "true";
|
|
10046
|
+
}
|
|
10047
|
+
if (type === "click" && !text("selector") && !text("text_match"))
|
|
10048
|
+
invalid("click goals need selector=<css> or text-match=<text>");
|
|
10049
|
+
if (type === "pageview" && !text("page_id") && !text("url_pattern"))
|
|
10050
|
+
invalid("pageview goals need page=<page-id> or url-pattern=<pattern>");
|
|
10051
|
+
if (type === "custom" && !text("event_name")) invalid("custom goals need event-name=<name>");
|
|
10052
|
+
const eventName = text("event_name");
|
|
10053
|
+
if (eventName && !GOAL_EVENT_NAME.test(eventName))
|
|
10054
|
+
invalid("event-name must be 1-120 letters, numbers, dots, colons, underscores, or hyphens");
|
|
10055
|
+
if ((type === "custom" || type === "revenue") && text("page_id"))
|
|
10056
|
+
invalid(`page is not supported for ${type} goals`);
|
|
10057
|
+
const pageId = text("page_id");
|
|
10058
|
+
if (pageId && !GOAL_UUID.test(pageId))
|
|
10059
|
+
invalid("page must be a page UUID (list them with `apex pages list`)");
|
|
10060
|
+
const valueProperty = text("value_property");
|
|
10061
|
+
const valueUnit = text("value_unit");
|
|
10062
|
+
if (measurement !== "value" && (valueProperty || valueUnit))
|
|
10063
|
+
invalid("value-property and value-unit need measurement=value");
|
|
10064
|
+
if (type === "revenue" && valueProperty)
|
|
10065
|
+
invalid("revenue goals read the order revenue; value-property is not supported");
|
|
10066
|
+
if (valueProperty && !GOAL_VALUE_PROPERTY.test(valueProperty))
|
|
10067
|
+
invalid("value-property must be 1-120 letters, numbers, dots, underscores, or hyphens");
|
|
10068
|
+
if (valueUnit && !GOAL_VALUE_UNITS.includes(valueUnit))
|
|
10069
|
+
invalid(`value-unit must be one of ${GOAL_VALUE_UNITS.join(", ")}`);
|
|
10070
|
+
}
|
|
10071
|
+
function parseNewGoal(command, value) {
|
|
10072
|
+
const body5 = {};
|
|
10073
|
+
for (const field of value.split(",")) {
|
|
10074
|
+
const separator = field.indexOf("=");
|
|
10075
|
+
if (separator <= 0 || separator === field.length - 1) {
|
|
10076
|
+
fail(
|
|
10077
|
+
command,
|
|
10078
|
+
`Invalid --goal "new:${value}"; use new:type=<type>,selector=<css>,name=<text>`,
|
|
10079
|
+
EXIT_CODES.validation
|
|
10080
|
+
);
|
|
9622
10081
|
}
|
|
10082
|
+
body5[normalizeNewGoalKey(field.slice(0, separator))] = field.slice(separator + 1).trim();
|
|
9623
10083
|
}
|
|
9624
|
-
|
|
10084
|
+
validateNewGoal(command, value, body5);
|
|
10085
|
+
return body5;
|
|
9625
10086
|
}
|
|
9626
|
-
|
|
10087
|
+
async function resolveNewGoalPages(command, creds, goals, interactive) {
|
|
10088
|
+
const pending = goals.filter((goal) => goal.body.type === "click" && !goal.body.page_id);
|
|
10089
|
+
if (pending.length === 0) return;
|
|
10090
|
+
if (!interactive) {
|
|
10091
|
+
fail(
|
|
10092
|
+
command,
|
|
10093
|
+
`Invalid --goal "${pending[0].flagValue}"; click goals are page-scoped in v1 \u2014 add page=<page-id> (see \`apex pages list\`)`,
|
|
10094
|
+
EXIT_CODES.validation
|
|
10095
|
+
);
|
|
10096
|
+
}
|
|
10097
|
+
const listed = await requestTargetingResource(creds, "GET", "pages", void 0, void 0, 100);
|
|
10098
|
+
if (!listed.ok || !listed.data)
|
|
10099
|
+
fail(command, listed.error ?? "Failed to list pages", classifyStatus(listed.status));
|
|
10100
|
+
const rows = listed.data.data;
|
|
10101
|
+
if (rows.length === 0)
|
|
10102
|
+
fail(
|
|
10103
|
+
command,
|
|
10104
|
+
"Click goals are page-scoped in v1 and this shop has no pages yet; create one with `apex pages create`.",
|
|
10105
|
+
EXIT_CODES.validation
|
|
10106
|
+
);
|
|
10107
|
+
printChoices("Pages (enter id/number)", rows);
|
|
10108
|
+
for (const goal of pending) {
|
|
10109
|
+
const answer = await askCreate(`Page for click goal "${String(goal.body.name)}": `);
|
|
10110
|
+
if (!answer)
|
|
10111
|
+
fail(
|
|
10112
|
+
command,
|
|
10113
|
+
`Invalid --goal "${goal.flagValue}"; click goals need a page`,
|
|
10114
|
+
EXIT_CODES.validation
|
|
10115
|
+
);
|
|
10116
|
+
const chosen = /^\d+$/.test(answer) && rows[Number(answer) - 1] ? rows[Number(answer) - 1].id : answer;
|
|
10117
|
+
if (!GOAL_UUID.test(chosen))
|
|
10118
|
+
fail(
|
|
10119
|
+
command,
|
|
10120
|
+
`Invalid page "${chosen}"; pick a listed number or a page UUID`,
|
|
10121
|
+
EXIT_CODES.validation
|
|
10122
|
+
);
|
|
10123
|
+
goal.body.page_id = chosen;
|
|
10124
|
+
}
|
|
10125
|
+
}
|
|
10126
|
+
async function askCreate(question) {
|
|
10127
|
+
const readline3 = await import("node:readline");
|
|
10128
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
|
|
10129
|
+
return new Promise(
|
|
10130
|
+
(resolve2) => rl.question(question, (answer) => {
|
|
10131
|
+
rl.close();
|
|
10132
|
+
resolve2(answer.trim());
|
|
10133
|
+
})
|
|
10134
|
+
);
|
|
10135
|
+
}
|
|
10136
|
+
function printChoices(kind5, rows) {
|
|
10137
|
+
process.stderr.write(
|
|
10138
|
+
`${kind5}:
|
|
10139
|
+
${rows.map((row, index) => ` ${index + 1}. ${row.name} (${row.id})`).join("\n")}
|
|
10140
|
+
`
|
|
10141
|
+
);
|
|
10142
|
+
}
|
|
10143
|
+
var create = new Command("create").description("Create a new experiment").addOption(
|
|
10144
|
+
new Option("--counting-type <type>", "Experiment classification").choices(countingTypes).default("standard")
|
|
10145
|
+
).option("--name <name>", "Experiment name").option("--slug <slug>", "URL-safe slug for GitOps").option("--description <desc>", "Description").option("--type <type>", "Experiment type: mutation or redirect", "mutation").option(
|
|
9627
10146
|
"--source-url <pattern>",
|
|
9628
10147
|
"Positive source URL rule (repeatable; accepts exact URLs, paths, or wildcards)",
|
|
9629
10148
|
collectCreateOption,
|
|
9630
10149
|
[]
|
|
10150
|
+
).option(
|
|
10151
|
+
"--page-url <pattern>",
|
|
10152
|
+
"Page URL targeting pattern (repeatable)",
|
|
10153
|
+
collectCreateOption,
|
|
10154
|
+
[]
|
|
10155
|
+
).option(
|
|
10156
|
+
"--audience <segment-id|new:@file.json>",
|
|
10157
|
+
"Audience segment (repeatable)",
|
|
10158
|
+
collectCreateOption,
|
|
10159
|
+
[]
|
|
10160
|
+
).option(
|
|
10161
|
+
"--goal <goal-id|new:fields>",
|
|
10162
|
+
"Goal id or new:type=...,selector=...,name=... (repeatable)",
|
|
10163
|
+
collectCreateOption,
|
|
10164
|
+
[]
|
|
9631
10165
|
).option(
|
|
9632
10166
|
"--destination <url>",
|
|
9633
10167
|
"Redirect treatment destination (repeatable; creates one treatment per URL)",
|
|
9634
10168
|
collectCreateOption,
|
|
9635
10169
|
[]
|
|
9636
|
-
).option("--traffic <pct>", "Traffic allocation (0-100)", "100").option("--
|
|
10170
|
+
).option("--traffic <pct>", "Traffic allocation (0-100)", "100").option("--idea <id>", "Idea UUID or Apex backlog key").option("--backlog-item <id>", "Alias for --idea").option("--preview", "Print a QA preview URL for the first treatment").option("--yes", "Do not prompt for missing values").action(async function action9(opts) {
|
|
10171
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !opts.yes);
|
|
10172
|
+
if (!opts.name && interactive) opts.name = await askCreate("Experiment name: ");
|
|
10173
|
+
if (!opts.name?.trim()) fail(this, "Missing required value: --name", EXIT_CODES.validation);
|
|
10174
|
+
if (opts.pageUrl.length === 0 && opts.sourceUrl.length === 0 && interactive) {
|
|
10175
|
+
const answer = await askCreate("Page URL (--page-url): ");
|
|
10176
|
+
if (answer) opts.pageUrl.push(answer);
|
|
10177
|
+
}
|
|
10178
|
+
const creds = requireCredentials();
|
|
10179
|
+
if (opts.audience.length === 0 && interactive) {
|
|
10180
|
+
const listed = await requestTargetingResource(
|
|
10181
|
+
creds,
|
|
10182
|
+
"GET",
|
|
10183
|
+
"segments",
|
|
10184
|
+
void 0,
|
|
10185
|
+
void 0,
|
|
10186
|
+
100
|
|
10187
|
+
);
|
|
10188
|
+
if (!listed.ok || !listed.data)
|
|
10189
|
+
fail(this, listed.error ?? "Failed to list segments", classifyStatus(listed.status));
|
|
10190
|
+
const rows = listed.data.data;
|
|
10191
|
+
printChoices("Audiences (enter id/number, or none)", rows);
|
|
10192
|
+
const answer = await askCreate("Audience: ");
|
|
10193
|
+
if (answer && answer.toLowerCase() !== "none")
|
|
10194
|
+
opts.audience.push(
|
|
10195
|
+
/^\d+$/.test(answer) && rows[Number(answer) - 1] ? rows[Number(answer) - 1].id : answer
|
|
10196
|
+
);
|
|
10197
|
+
}
|
|
10198
|
+
if (opts.goal.length === 0 && interactive) {
|
|
10199
|
+
const listed = await listGoals(creds, { limit: 100 });
|
|
10200
|
+
if (!listed.ok || !listed.data)
|
|
10201
|
+
fail(this, listed.error ?? "Failed to list goals", classifyStatus(listed.status));
|
|
10202
|
+
const rows = listed.data.data;
|
|
10203
|
+
printChoices("Goals (enter ids/numbers separated by commas, or none)", rows);
|
|
10204
|
+
const answer = await askCreate("Goals: ");
|
|
10205
|
+
if (answer && answer.toLowerCase() !== "none")
|
|
10206
|
+
opts.goal.push(
|
|
10207
|
+
...answer.split(",").map((part) => part.trim()).filter(Boolean).map(
|
|
10208
|
+
(part) => /^\d+$/.test(part) && rows[Number(part) - 1] ? rows[Number(part) - 1].id : part
|
|
10209
|
+
)
|
|
10210
|
+
);
|
|
10211
|
+
}
|
|
10212
|
+
if (this.getOptionValueSource("traffic") === "default" && interactive) {
|
|
10213
|
+
const answer = await askCreate("Traffic percentage [100]: ");
|
|
10214
|
+
if (answer) opts.traffic = answer;
|
|
10215
|
+
}
|
|
9637
10216
|
if (opts.type !== "mutation" && opts.type !== "redirect") {
|
|
9638
10217
|
fail(this, "--type must be mutation or redirect", EXIT_CODES.validation);
|
|
9639
10218
|
}
|
|
@@ -9641,9 +10220,9 @@ var create = new Command("create").description("Create a new experiment").requir
|
|
|
9641
10220
|
if (!Number.isFinite(traffic) || traffic < 0 || traffic > 100) {
|
|
9642
10221
|
fail(this, "--traffic must be a number from 0 to 100", EXIT_CODES.validation);
|
|
9643
10222
|
}
|
|
9644
|
-
const sourceRules = opts.sourceUrl.map((value) => value.trim());
|
|
10223
|
+
const sourceRules = [...opts.sourceUrl, ...opts.pageUrl].map((value) => value.trim());
|
|
9645
10224
|
if (sourceRules.some((value) => value.length === 0)) {
|
|
9646
|
-
fail(this, "--source-url cannot be empty", EXIT_CODES.validation);
|
|
10225
|
+
fail(this, "--source-url/--page-url cannot be empty", EXIT_CODES.validation);
|
|
9647
10226
|
}
|
|
9648
10227
|
if (opts.type !== "redirect" && opts.destination.length > 0) {
|
|
9649
10228
|
fail(this, "--destination is only valid with --type redirect", EXIT_CODES.validation);
|
|
@@ -9663,94 +10242,180 @@ var create = new Command("create").description("Create a new experiment").requir
|
|
|
9663
10242
|
);
|
|
9664
10243
|
}
|
|
9665
10244
|
const destinations = opts.destination.map(parseHttpDestination);
|
|
10245
|
+
const previewStoreUrl = deriveStoreUrl(sourceRules);
|
|
10246
|
+
if (opts.preview && !previewStoreUrl) {
|
|
10247
|
+
fail(
|
|
10248
|
+
this,
|
|
10249
|
+
"--preview needs an absolute page URL: pass --page-url https://store.example/path (wildcard and path-only rules cannot seed the preview link), or create the experiment without --preview and run `apex preview <id> --store-url <url>`",
|
|
10250
|
+
EXIT_CODES.validation
|
|
10251
|
+
);
|
|
10252
|
+
}
|
|
9666
10253
|
const armWeight = destinations.length > 0 ? 1 / (destinations.length + 1) : null;
|
|
9667
|
-
const creds = requireCredentials();
|
|
9668
10254
|
let backlogItemId;
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
page.error ?? `Unable to resolve backlog item "${ref}"`,
|
|
9685
|
-
classifyStatus(page.status),
|
|
9686
|
-
page.errorDetails
|
|
9687
|
-
);
|
|
9688
|
-
}
|
|
9689
|
-
const match2 = page.data.data.find((item) => {
|
|
9690
|
-
const metadata = item.metadata && typeof item.metadata === "object" && !Array.isArray(item.metadata) ? item.metadata : {};
|
|
9691
|
-
return typeof metadata.backlog_key === "string" && metadata.backlog_key.toUpperCase() === ref.toUpperCase();
|
|
9692
|
-
});
|
|
9693
|
-
resolvedId = match2?.id;
|
|
9694
|
-
cursor = page.data.pagination.has_more ? page.data.pagination.next_cursor ?? void 0 : void 0;
|
|
9695
|
-
} while (!resolvedId && cursor);
|
|
9696
|
-
const backlog = resolvedId ? { ok: true, status: 200, data: { id: resolvedId } } : await getInsight(creds, "backlog", ref);
|
|
9697
|
-
if (!backlog.ok || !backlog.data?.id) {
|
|
9698
|
-
fail(
|
|
9699
|
-
this,
|
|
9700
|
-
backlog.error ?? `Backlog item "${ref}" not found`,
|
|
9701
|
-
classifyStatus(backlog.status),
|
|
9702
|
-
backlog.errorDetails
|
|
9703
|
-
);
|
|
9704
|
-
}
|
|
9705
|
-
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
9706
|
-
backlog.data.id
|
|
9707
|
-
)) {
|
|
9708
|
-
fail(this, `Backlog item "${ref}" not found`, EXIT_CODES.validation);
|
|
9709
|
-
}
|
|
9710
|
-
backlogItemId = backlog.data.id;
|
|
10255
|
+
let ideaKey;
|
|
10256
|
+
if (opts.idea !== void 0 && opts.backlogItem !== void 0 && opts.idea.trim().toUpperCase() !== opts.backlogItem.trim().toUpperCase()) {
|
|
10257
|
+
fail(
|
|
10258
|
+
this,
|
|
10259
|
+
"--idea and --backlog-item name the same idea; pass only one",
|
|
10260
|
+
EXIT_CODES.validation
|
|
10261
|
+
);
|
|
10262
|
+
}
|
|
10263
|
+
const ideaReference = opts.idea ?? opts.backlogItem;
|
|
10264
|
+
if (ideaReference !== void 0) {
|
|
10265
|
+
const classified = classifyIdeaReference(ideaReference);
|
|
10266
|
+
if (classified.kind === "uuid") {
|
|
10267
|
+
backlogItemId = classified.value;
|
|
10268
|
+
} else if (classified.kind === "key") {
|
|
10269
|
+
ideaKey = classified.value;
|
|
9711
10270
|
} else {
|
|
9712
|
-
fail(this, "--
|
|
10271
|
+
fail(this, "--idea must be a UUID or Apex backlog key", EXIT_CODES.validation);
|
|
9713
10272
|
}
|
|
9714
10273
|
}
|
|
10274
|
+
const newGoals = /* @__PURE__ */ new Map();
|
|
10275
|
+
for (const [index, goal] of opts.goal.entries()) {
|
|
10276
|
+
if (goal.startsWith("new:")) newGoals.set(index, parseNewGoal(this, goal.slice(4)));
|
|
10277
|
+
}
|
|
10278
|
+
await resolveNewGoalPages(
|
|
10279
|
+
this,
|
|
10280
|
+
creds,
|
|
10281
|
+
[...newGoals].map(([index, body5]) => ({ flagValue: opts.goal[index], body: body5 })),
|
|
10282
|
+
interactive
|
|
10283
|
+
);
|
|
10284
|
+
const createdSegments = [];
|
|
10285
|
+
const createdGoals = [];
|
|
10286
|
+
const segmentIds = [];
|
|
10287
|
+
const goalIds = [];
|
|
10288
|
+
const partial = () => [
|
|
10289
|
+
...createdSegments.map((item) => `segment ${item.id}`),
|
|
10290
|
+
...createdGoals.map((item) => `goal ${item.id}`)
|
|
10291
|
+
];
|
|
10292
|
+
for (const audience of opts.audience) {
|
|
10293
|
+
if (!audience.startsWith("new:")) {
|
|
10294
|
+
segmentIds.push(audience);
|
|
10295
|
+
continue;
|
|
10296
|
+
}
|
|
10297
|
+
const reference = audience.slice(4);
|
|
10298
|
+
if (!reference.startsWith("@"))
|
|
10299
|
+
fail(
|
|
10300
|
+
this,
|
|
10301
|
+
`Invalid --audience "${audience}"; new audiences use new:@file.json`,
|
|
10302
|
+
EXIT_CODES.validation
|
|
10303
|
+
);
|
|
10304
|
+
const response = await requestTargetingResource(
|
|
10305
|
+
creds,
|
|
10306
|
+
"POST",
|
|
10307
|
+
"segments",
|
|
10308
|
+
void 0,
|
|
10309
|
+
readCreateJson(this, reference)
|
|
10310
|
+
);
|
|
10311
|
+
if (!response.ok || !response.data)
|
|
10312
|
+
fail(
|
|
10313
|
+
this,
|
|
10314
|
+
`${response.error ?? "Failed to create audience"}${partial().length ? `; already created: ${partial().join(", ")}` : ""}`,
|
|
10315
|
+
classifyStatus(response.status)
|
|
10316
|
+
);
|
|
10317
|
+
const segment = response.data;
|
|
10318
|
+
createdSegments.push(segment);
|
|
10319
|
+
segmentIds.push(segment.id);
|
|
10320
|
+
}
|
|
10321
|
+
for (const [index, goal] of opts.goal.entries()) {
|
|
10322
|
+
const body5 = newGoals.get(index);
|
|
10323
|
+
if (!body5) {
|
|
10324
|
+
goalIds.push(goal);
|
|
10325
|
+
continue;
|
|
10326
|
+
}
|
|
10327
|
+
const response = await createGoal(creds, body5);
|
|
10328
|
+
if (!response.ok || !response.data)
|
|
10329
|
+
fail(
|
|
10330
|
+
this,
|
|
10331
|
+
`${response.error ?? "Failed to create goal"}${partial().length ? `; already created: ${partial().join(", ")}` : ""}`,
|
|
10332
|
+
classifyStatus(response.status)
|
|
10333
|
+
);
|
|
10334
|
+
createdGoals.push(response.data);
|
|
10335
|
+
goalIds.push(response.data.id);
|
|
10336
|
+
}
|
|
9715
10337
|
const spinner = await createSpinner("Creating experiment...");
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
10338
|
+
let res;
|
|
10339
|
+
if (segmentIds.length === 0 && goalIds.length === 0) {
|
|
10340
|
+
res = await createExperiment(creds, {
|
|
10341
|
+
name: opts.name,
|
|
10342
|
+
slug: opts.slug ?? null,
|
|
10343
|
+
description: opts.description ?? null,
|
|
10344
|
+
delivery_type: opts.type,
|
|
10345
|
+
counting_type: opts.countingType,
|
|
10346
|
+
traffic_allocation: traffic / 100,
|
|
10347
|
+
...backlogItemId ? { backlog_item_id: backlogItemId } : {},
|
|
10348
|
+
...ideaKey ? { idea_key: ideaKey } : {},
|
|
10349
|
+
...sourceRules.length > 0 ? {
|
|
10350
|
+
store_url: previewStoreUrl,
|
|
10351
|
+
targeting: {
|
|
10352
|
+
url: sourceRules.map((pattern) => ({
|
|
10353
|
+
type: "simple",
|
|
10354
|
+
pattern,
|
|
10355
|
+
include: true
|
|
10356
|
+
}))
|
|
10357
|
+
}
|
|
10358
|
+
} : {},
|
|
10359
|
+
...armWeight !== null ? {
|
|
10360
|
+
variations: [
|
|
10361
|
+
{
|
|
10362
|
+
name: "Control",
|
|
10363
|
+
weight: armWeight,
|
|
10364
|
+
is_control: true
|
|
10365
|
+
},
|
|
10366
|
+
...destinations.map((redirectUrl, index) => ({
|
|
10367
|
+
name: destinations.length === 1 ? "Treatment" : `Treatment ${index + 1}`,
|
|
10368
|
+
weight: armWeight,
|
|
10369
|
+
is_control: false,
|
|
10370
|
+
redirect_url: redirectUrl
|
|
10371
|
+
}))
|
|
10372
|
+
]
|
|
10373
|
+
} : {}
|
|
10374
|
+
});
|
|
10375
|
+
} else {
|
|
10376
|
+
res = await createExperiment(creds, {
|
|
10377
|
+
name: opts.name,
|
|
10378
|
+
slug: opts.slug ?? null,
|
|
10379
|
+
description: opts.description ?? null,
|
|
10380
|
+
delivery_type: opts.type,
|
|
10381
|
+
counting_type: opts.countingType,
|
|
10382
|
+
traffic_allocation: traffic / 100,
|
|
10383
|
+
...backlogItemId ? { backlog_item_id: backlogItemId } : {},
|
|
10384
|
+
...ideaKey ? { idea_key: ideaKey } : {},
|
|
10385
|
+
...sourceRules.length > 0 ? {
|
|
10386
|
+
store_url: previewStoreUrl,
|
|
10387
|
+
targeting: {
|
|
10388
|
+
url: sourceRules.map((pattern) => ({
|
|
10389
|
+
type: "simple",
|
|
10390
|
+
pattern,
|
|
10391
|
+
include: true
|
|
10392
|
+
})),
|
|
10393
|
+
...segmentIds.length ? { segments: segmentIds.map((id) => ({ id, include: true })) } : {}
|
|
10394
|
+
}
|
|
10395
|
+
} : segmentIds.length ? { targeting: { segments: segmentIds.map((id) => ({ id, include: true })) } } : {},
|
|
10396
|
+
...goalIds.length ? { goal_ids: goalIds, primary_goal_id: goalIds[0] } : {},
|
|
10397
|
+
...armWeight !== null ? {
|
|
10398
|
+
variations: [
|
|
10399
|
+
{
|
|
10400
|
+
name: "Control",
|
|
10401
|
+
weight: armWeight,
|
|
10402
|
+
is_control: true
|
|
10403
|
+
},
|
|
10404
|
+
...destinations.map((redirectUrl, index) => ({
|
|
10405
|
+
name: destinations.length === 1 ? "Treatment" : `Treatment ${index + 1}`,
|
|
10406
|
+
weight: armWeight,
|
|
10407
|
+
is_control: false,
|
|
10408
|
+
redirect_url: redirectUrl
|
|
10409
|
+
}))
|
|
10410
|
+
]
|
|
10411
|
+
} : {}
|
|
10412
|
+
});
|
|
10413
|
+
}
|
|
9749
10414
|
if (!res.ok || !res.data) {
|
|
9750
10415
|
spinner?.fail("Failed to create experiment");
|
|
9751
10416
|
fail(
|
|
9752
10417
|
this,
|
|
9753
|
-
res.error ?? "Failed to create experiment",
|
|
10418
|
+
`${res.error ?? "Failed to create experiment"}${partial().length ? `; already created: ${partial().join(", ")}` : ""}`,
|
|
9754
10419
|
classifyStatus(res.status),
|
|
9755
10420
|
res.errorDetails
|
|
9756
10421
|
);
|
|
@@ -9759,13 +10424,49 @@ var create = new Command("create").description("Create a new experiment").requir
|
|
|
9759
10424
|
spinner?.succeed(
|
|
9760
10425
|
outcome === "created" ? "Experiment created" : `Already linked \u2014 returning existing experiment ${res.data.test_key ?? res.data.slug ?? res.data.id}`
|
|
9761
10426
|
);
|
|
10427
|
+
let previewUrl = null;
|
|
10428
|
+
let previewNote = null;
|
|
10429
|
+
if (opts.preview) {
|
|
10430
|
+
const variation = res.data.variations?.find((item) => !item.is_control);
|
|
10431
|
+
const storeUrl = res.data.store_url ?? previewStoreUrl;
|
|
10432
|
+
if (storeUrl && variation) {
|
|
10433
|
+
previewUrl = buildQaPreviewUrl2(storeUrl, res.data.id, variation.id).toString();
|
|
10434
|
+
} else {
|
|
10435
|
+
previewNote = `No preview link: the experiment has no ${variation ? "store URL" : "treatment variation"} yet. Add one, then run \`apex preview ${res.data.id}\`.`;
|
|
10436
|
+
}
|
|
10437
|
+
}
|
|
9762
10438
|
output(
|
|
9763
10439
|
this,
|
|
9764
|
-
{
|
|
9765
|
-
|
|
10440
|
+
{
|
|
10441
|
+
outcome,
|
|
10442
|
+
backlog_item_id: backlogItemId ?? null,
|
|
10443
|
+
idea_key: ideaKey ?? null,
|
|
10444
|
+
experiment: res.data,
|
|
10445
|
+
segments: createdSegments,
|
|
10446
|
+
goals: createdGoals,
|
|
10447
|
+
dashboard_url: buildDashboardUrl(
|
|
10448
|
+
creds.baseUrl,
|
|
10449
|
+
res.data?.shop_id ?? creds.resolvedContext?.shopId,
|
|
10450
|
+
res.data.id
|
|
10451
|
+
),
|
|
10452
|
+
preview_url: previewUrl,
|
|
10453
|
+
preview_note: previewNote
|
|
10454
|
+
},
|
|
10455
|
+
() => {
|
|
10456
|
+
printExperiment(
|
|
10457
|
+
res.data,
|
|
10458
|
+
buildDashboardUrl(
|
|
10459
|
+
creds.baseUrl,
|
|
10460
|
+
res.data?.shop_id ?? creds.resolvedContext?.shopId,
|
|
10461
|
+
res.data.id
|
|
10462
|
+
)
|
|
10463
|
+
);
|
|
10464
|
+
if (previewUrl) console.log(`Preview: ${previewUrl}`);
|
|
10465
|
+
if (previewNote) console.log(previewNote);
|
|
10466
|
+
}
|
|
9766
10467
|
);
|
|
9767
10468
|
});
|
|
9768
|
-
var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>",
|
|
10469
|
+
var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).action(async function action10(idOrSlug) {
|
|
9769
10470
|
const creds = requireCredentials();
|
|
9770
10471
|
const spinner = await createSpinner("Pausing experiment...");
|
|
9771
10472
|
const exp = await resolveExperiment(idOrSlug);
|
|
@@ -9787,7 +10488,7 @@ var pause = new Command("pause").description("Pause a running experiment").argum
|
|
|
9787
10488
|
spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("paused")}`);
|
|
9788
10489
|
});
|
|
9789
10490
|
});
|
|
9790
|
-
var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>",
|
|
10491
|
+
var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).action(async function action11(idOrSlug) {
|
|
9791
10492
|
const creds = requireCredentials();
|
|
9792
10493
|
const spinner = await createSpinner("Completing experiment...");
|
|
9793
10494
|
const exp = await resolveExperiment(idOrSlug);
|
|
@@ -9809,7 +10510,7 @@ var complete = new Command("complete").description("Complete/stop an experiment"
|
|
|
9809
10510
|
spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("completed")}`);
|
|
9810
10511
|
});
|
|
9811
10512
|
});
|
|
9812
|
-
var del = new Command("delete").alias("rm").description("Delete an experiment").argument("<id-or-slug>",
|
|
10513
|
+
var del = new Command("delete").alias("rm").description("Delete an experiment").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("-f, --force", "Skip confirmation").action(async function action12(idOrSlug, opts) {
|
|
9813
10514
|
const creds = requireCredentials();
|
|
9814
10515
|
const exp = await resolveExperiment(idOrSlug);
|
|
9815
10516
|
if (!exp) {
|
|
@@ -9854,7 +10555,7 @@ var del = new Command("delete").alias("rm").description("Delete an experiment").
|
|
|
9854
10555
|
() => spinner.succeed(`Experiment "${exp.name}" deleted.`)
|
|
9855
10556
|
);
|
|
9856
10557
|
});
|
|
9857
|
-
var pullFilesystem = new Command("pull").description("Pull an experiment as an editable filesystem").argument("<id-or-slug>",
|
|
10558
|
+
var pullFilesystem = new Command("pull").description("Pull an experiment as an editable filesystem").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--out <path>", "Target folder").action(async function action13(idOrSlug, opts) {
|
|
9858
10559
|
const creds = requireCredentials();
|
|
9859
10560
|
const spinner = await createSpinner("Pulling experiment filesystem...");
|
|
9860
10561
|
try {
|
|
@@ -9895,12 +10596,20 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
|
|
|
9895
10596
|
throw error;
|
|
9896
10597
|
}
|
|
9897
10598
|
});
|
|
9898
|
-
var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function
|
|
10599
|
+
var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action14(folderArg) {
|
|
9899
10600
|
const creds = requireCredentials();
|
|
9900
10601
|
try {
|
|
9901
10602
|
const folder = folderPath(folderArg);
|
|
9902
10603
|
const session = readFilesystemSession(folder);
|
|
9903
|
-
const files =
|
|
10604
|
+
const files = await rewriteExperimentFilesystemAssets({
|
|
10605
|
+
files: listExperimentFilesystemFiles(folder, session),
|
|
10606
|
+
folder,
|
|
10607
|
+
baseUrl: creds.baseUrl,
|
|
10608
|
+
enabled: true,
|
|
10609
|
+
upload: null,
|
|
10610
|
+
assets: session.assets ?? {},
|
|
10611
|
+
credentials: creds
|
|
10612
|
+
});
|
|
9904
10613
|
const res = await planExperimentFilesystem(creds, session.experimentId, { files });
|
|
9905
10614
|
if (!res.ok || !res.data) {
|
|
9906
10615
|
fail(
|
|
@@ -9920,7 +10629,12 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
|
|
|
9920
10629
|
await refreshServerOwnedFilesIfNeeded(creds, folder, session.experimentId);
|
|
9921
10630
|
output(
|
|
9922
10631
|
this,
|
|
9923
|
-
{
|
|
10632
|
+
{
|
|
10633
|
+
folder,
|
|
10634
|
+
experimentId: session.experimentId,
|
|
10635
|
+
plan: res.data,
|
|
10636
|
+
requiredAcknowledgment: res.data.invalidatesQa ? "accept-qa-invalidation" : null
|
|
10637
|
+
},
|
|
9924
10638
|
() => printFilesystemPlan(res.data)
|
|
9925
10639
|
);
|
|
9926
10640
|
} catch (error) {
|
|
@@ -9928,19 +10642,27 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
|
|
|
9928
10642
|
fail(this, error instanceof Error ? error.message : String(error));
|
|
9929
10643
|
}
|
|
9930
10644
|
});
|
|
9931
|
-
var applyFilesystem = new Command("apply").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option("-f, --force", "
|
|
10645
|
+
var applyFilesystem = new Command("apply").option("--sync", "Refresh local authored files with the stored server bytes").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option("--accept-qa-invalidation", "Acknowledge that applying changes invalidates QA approval").addOption(new Option("-f, --force", "Deprecated alias for --accept-qa-invalidation").hideHelp()).option("--no-asset-upload", "Keep inline and local image references unchanged").action(async function action15(folderArg, opts) {
|
|
9932
10646
|
const creds = requireCredentials();
|
|
9933
10647
|
try {
|
|
9934
10648
|
const folder = folderPath(folderArg);
|
|
9935
10649
|
const session = readFilesystemSession(folder);
|
|
10650
|
+
if (opts.force)
|
|
10651
|
+
process.stderr.write("--force is deprecated; use --accept-qa-invalidation instead.\n");
|
|
10652
|
+
const assets = session.assets ?? {};
|
|
9936
10653
|
const files = await rewriteExperimentFilesystemAssets({
|
|
9937
10654
|
files: listExperimentFilesystemFiles(folder, session),
|
|
9938
10655
|
folder,
|
|
9939
10656
|
baseUrl: creds.baseUrl,
|
|
9940
10657
|
enabled: opts.assetUpload !== false,
|
|
9941
10658
|
upload: uploadAssets,
|
|
10659
|
+
assets,
|
|
9942
10660
|
credentials: creds
|
|
9943
10661
|
});
|
|
10662
|
+
if (Object.keys(assets).length > 0) {
|
|
10663
|
+
session.assets = assets;
|
|
10664
|
+
writeFilesystemSession(folder, session);
|
|
10665
|
+
}
|
|
9944
10666
|
const planRes = await planExperimentFilesystem(creds, session.experimentId, { files });
|
|
9945
10667
|
if (!planRes.ok || !planRes.data) {
|
|
9946
10668
|
fail(
|
|
@@ -9960,30 +10682,43 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
|
|
|
9960
10682
|
}
|
|
9961
10683
|
fail(this, blockingIssueSummary(planRes.data.blockingIssues), EXIT_CODES.validation, {
|
|
9962
10684
|
errors: planRes.data.errors,
|
|
9963
|
-
plan: planRes.data
|
|
10685
|
+
plan: planRes.data,
|
|
10686
|
+
requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null
|
|
9964
10687
|
});
|
|
9965
10688
|
}
|
|
9966
10689
|
if (planRes.data.changes.length === 0) {
|
|
10690
|
+
const synced2 = opts.sync ? await refreshExperimentFilesystemFolderIfNeeded(creds, folder, session.experimentId) : false;
|
|
10691
|
+
if (!synced2)
|
|
10692
|
+
await refreshServerOwnedFilesIfNeeded(creds, folder, session.experimentId, true);
|
|
10693
|
+
const { dashboardUrl: dashboardUrl2 } = await getQaPreviewUrls(creds, session.experimentId);
|
|
9967
10694
|
output(
|
|
9968
10695
|
this,
|
|
9969
10696
|
{
|
|
9970
10697
|
folder,
|
|
9971
10698
|
experimentId: session.experimentId,
|
|
9972
10699
|
applied: false,
|
|
10700
|
+
synced: synced2,
|
|
10701
|
+
dashboardUrl: dashboardUrl2,
|
|
9973
10702
|
plan: planRes.data,
|
|
10703
|
+
requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null,
|
|
9974
10704
|
message: "No changes."
|
|
9975
10705
|
},
|
|
9976
|
-
() =>
|
|
10706
|
+
() => {
|
|
10707
|
+
printFilesystemPlan(planRes.data);
|
|
10708
|
+
console.log(
|
|
10709
|
+
synced2 ? "Local files were synced." : opts.sync ? "Local file sync failed; retry --sync or apex experiments pull." : "Local authored files were left unchanged; use --sync or apex experiments pull to refresh."
|
|
10710
|
+
);
|
|
10711
|
+
}
|
|
9977
10712
|
);
|
|
9978
10713
|
return;
|
|
9979
10714
|
}
|
|
9980
|
-
if (planRes.data.invalidatesQa && !opts.force) {
|
|
10715
|
+
if (planRes.data.invalidatesQa && !opts.force && !opts.acceptQaInvalidation) {
|
|
9981
10716
|
const ok = await confirmApply("This apply invalidates QA approval. Continue?");
|
|
9982
10717
|
if (ok === null) {
|
|
9983
10718
|
confirmationRequired(
|
|
9984
10719
|
this,
|
|
9985
10720
|
"Applying these changes invalidates QA approval and requires confirmation.",
|
|
9986
|
-
`apex experiments apply ${folderArg} --
|
|
10721
|
+
`apex experiments apply ${folderArg} --accept-qa-invalidation`
|
|
9987
10722
|
);
|
|
9988
10723
|
}
|
|
9989
10724
|
if (!ok) {
|
|
@@ -10001,9 +10736,29 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
|
|
|
10001
10736
|
});
|
|
10002
10737
|
}
|
|
10003
10738
|
spinner?.succeed("Applied experiment filesystem");
|
|
10004
|
-
await refreshExperimentFilesystemFolderIfNeeded(creds, folder, session.experimentId);
|
|
10739
|
+
const synced = opts.sync ? await refreshExperimentFilesystemFolderIfNeeded(creds, folder, session.experimentId) : false;
|
|
10740
|
+
if (!synced) {
|
|
10741
|
+
await refreshServerOwnedFilesIfNeeded(
|
|
10742
|
+
creds,
|
|
10743
|
+
folder,
|
|
10744
|
+
session.experimentId,
|
|
10745
|
+
!res.data?.receipt?.files
|
|
10746
|
+
);
|
|
10747
|
+
if (res.data?.receipt?.files) {
|
|
10748
|
+
writeFilesystemSession(folder, {
|
|
10749
|
+
...session,
|
|
10750
|
+
files: {
|
|
10751
|
+
...session.files,
|
|
10752
|
+
...Object.fromEntries(
|
|
10753
|
+
res.data.receipt.files.map((file) => [file.path, { sha256: file.sha256 }])
|
|
10754
|
+
)
|
|
10755
|
+
}
|
|
10756
|
+
});
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
const syncMessage = synced ? "Local files were synced." : opts.sync ? "Local file sync failed; retry --sync or apex experiments pull." : "Local authored files were left unchanged; use --sync or apex experiments pull to refresh." + (res.data?.receipt?.files ? "" : " No receipt file digests; authored-file manifest entries left unchanged.");
|
|
10005
10760
|
const previewResult = await getQaPreviewUrls(creds, session.experimentId);
|
|
10006
|
-
const { previewUrls } = previewResult;
|
|
10761
|
+
const { previewUrls, dashboardUrl } = previewResult;
|
|
10007
10762
|
output(
|
|
10008
10763
|
this,
|
|
10009
10764
|
{
|
|
@@ -10011,12 +10766,37 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
|
|
|
10011
10766
|
experimentId: session.experimentId,
|
|
10012
10767
|
applied: true,
|
|
10013
10768
|
plan: planRes.data,
|
|
10769
|
+
requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null,
|
|
10014
10770
|
result: res.data,
|
|
10771
|
+
synced,
|
|
10772
|
+
dashboardUrl,
|
|
10773
|
+
receipt: res.data?.receipt ?? null,
|
|
10015
10774
|
previewUrls
|
|
10016
10775
|
},
|
|
10017
10776
|
() => {
|
|
10018
10777
|
printFilesystemPlan(planRes.data);
|
|
10778
|
+
const receipt = res.data?.receipt;
|
|
10779
|
+
if (receipt) {
|
|
10780
|
+
console.log("\nReceipt");
|
|
10781
|
+
if (dashboardUrl) console.log(` Dashboard: ${dashboardUrl}`);
|
|
10782
|
+
console.log(` Status: ${receipt.status}`);
|
|
10783
|
+
console.log(` Classification: ${receipt.delivery_type} / ${receipt.counting_type}`);
|
|
10784
|
+
console.log(` Traffic: ${(receipt.traffic_allocation * 100).toFixed(0)}%`);
|
|
10785
|
+
console.log(
|
|
10786
|
+
` Schedule: ${receipt.schedule.scheduled_start_at ?? "none"} \u2192 ${receipt.schedule.scheduled_end_at ?? "none"}`
|
|
10787
|
+
);
|
|
10788
|
+
console.log(` Goals: ${receipt.goal_count}`);
|
|
10789
|
+
console.log(
|
|
10790
|
+
` QA: ${receipt.qa ? `${receipt.qa.status} (${receipt.qa.run_id}), approved: ${receipt.qa.approved_at ?? "none"}` : "none"}`
|
|
10791
|
+
);
|
|
10792
|
+
for (const variation of receipt.variations) {
|
|
10793
|
+
console.log(
|
|
10794
|
+
` ${variation.name} (${variation.slug}): ${Object.entries(variation.digests).map(([key, value]) => `${key}=${value ?? "null"}`).join(" ")}`
|
|
10795
|
+
);
|
|
10796
|
+
}
|
|
10797
|
+
}
|
|
10019
10798
|
printQaPreviewUrls(session.experimentId, previewResult);
|
|
10799
|
+
console.log(syncMessage);
|
|
10020
10800
|
}
|
|
10021
10801
|
);
|
|
10022
10802
|
} catch (error) {
|
|
@@ -10024,7 +10804,11 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
|
|
|
10024
10804
|
fail(this, error instanceof Error ? error.message : String(error));
|
|
10025
10805
|
}
|
|
10026
10806
|
});
|
|
10027
|
-
var qaStatus = new Command("status").description("Show or create the latest experiment QA run").argument(
|
|
10807
|
+
var qaStatus = new Command("status").description("Show or create the latest experiment QA run").argument(
|
|
10808
|
+
"[folder-or-id]",
|
|
10809
|
+
"Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
|
|
10810
|
+
"."
|
|
10811
|
+
).option("--json", "Print the QA run in the standard JSON envelope").action(async function action16(target) {
|
|
10028
10812
|
const creds = requireCredentials();
|
|
10029
10813
|
const spinner = await createSpinner("Fetching QA run...");
|
|
10030
10814
|
try {
|
|
@@ -10048,15 +10832,17 @@ var qaStatus = new Command("status").description("Show or create the latest expe
|
|
|
10048
10832
|
throw error;
|
|
10049
10833
|
}
|
|
10050
10834
|
});
|
|
10051
|
-
var qaCheck = new Command("check").description("Update one QA checklist item with status, notes, and evidence").argument(
|
|
10835
|
+
var qaCheck = new Command("check").description("Update one QA checklist item with status, notes, and evidence").argument(
|
|
10836
|
+
"[folder-or-id]",
|
|
10837
|
+
"Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
|
|
10838
|
+
"."
|
|
10839
|
+
).requiredOption("--item <id>", "Checklist item id").addOption(
|
|
10840
|
+
qaChoice("--status <status>", "Checklist status", checklistStatuses).makeOptionMandatory()
|
|
10841
|
+
).option("--notes <text>", "Checklist notes").option("--evidence <json-or-path>", "Evidence JSON object or @path/to/evidence.json").option("--summary", "Print a compact run and changed values").option("--json", "Print raw QA run JSON").action(async function action17(target, opts) {
|
|
10842
|
+
validateQaChoice(this, "--status", opts.status, checklistStatuses);
|
|
10052
10843
|
const creds = requireCredentials();
|
|
10053
10844
|
const spinner = await createSpinner("Updating QA checklist...");
|
|
10054
10845
|
try {
|
|
10055
|
-
if (!isQaCheckStatus(opts.status)) {
|
|
10056
|
-
throw new Error(
|
|
10057
|
-
"Invalid QA status. Use pending, pass, warning, fail, accepted, or skipped."
|
|
10058
|
-
);
|
|
10059
|
-
}
|
|
10060
10846
|
const evidence = parseEvidenceInput(opts.evidence);
|
|
10061
10847
|
const resolved = await resolveExperimentForFilesystemOrId(target);
|
|
10062
10848
|
const current = await getExperimentQaRun(creds, resolved.experimentId);
|
|
@@ -10082,16 +10868,23 @@ var qaCheck = new Command("check").description("Update one QA checklist item wit
|
|
|
10082
10868
|
resolved.folder,
|
|
10083
10869
|
resolved.experimentId
|
|
10084
10870
|
);
|
|
10085
|
-
|
|
10871
|
+
const data = opts.summary ? qaSummary(run3, run3.checklist.find((item) => item.id === opts.item) ?? null) : { run: run3 };
|
|
10872
|
+
output(this, data, () => {
|
|
10086
10873
|
spinner.succeed("Updated QA checklist");
|
|
10087
|
-
|
|
10874
|
+
if (opts.summary) process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
10875
|
+
`);
|
|
10876
|
+
else printQaRun(run3);
|
|
10088
10877
|
});
|
|
10089
10878
|
} catch (error) {
|
|
10090
10879
|
spinner.fail("Failed to update QA checklist");
|
|
10091
10880
|
throw error;
|
|
10092
10881
|
}
|
|
10093
10882
|
});
|
|
10094
|
-
var qaNotes = new Command("notes").description("Update QA run notes or acceptance criteria summary").argument(
|
|
10883
|
+
var qaNotes = new Command("notes").description("Update QA run notes or acceptance criteria summary").argument(
|
|
10884
|
+
"[folder-or-id]",
|
|
10885
|
+
"Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
|
|
10886
|
+
"."
|
|
10887
|
+
).option("--notes <text>", "QA run notes").option("--acceptance <text>", "Acceptance criteria summary").option("--summary", "Print a compact run and changed values").option("--json", "Print raw QA run JSON").action(async function action18(target, opts) {
|
|
10095
10888
|
const creds = requireCredentials();
|
|
10096
10889
|
const spinner = await createSpinner("Updating QA notes...");
|
|
10097
10890
|
try {
|
|
@@ -10103,9 +10896,10 @@ var qaNotes = new Command("notes").description("Update QA run notes or acceptanc
|
|
|
10103
10896
|
if (!current.ok || !current.data) {
|
|
10104
10897
|
throw new Error(current.error ?? "Failed to fetch QA run");
|
|
10105
10898
|
}
|
|
10106
|
-
const body5 =
|
|
10899
|
+
const body5 = {
|
|
10107
10900
|
runId: current.data.run.id,
|
|
10108
|
-
|
|
10901
|
+
...opts.notes !== void 0 ? { runNotes: opts.notes } : {},
|
|
10902
|
+
...opts.acceptance !== void 0 ? { acceptanceCriteriaSummary: opts.acceptance } : {}
|
|
10109
10903
|
};
|
|
10110
10904
|
const res = await updateExperimentQaRun(creds, resolved.experimentId, body5);
|
|
10111
10905
|
if (!res.ok || !res.data)
|
|
@@ -10120,20 +10914,32 @@ var qaNotes = new Command("notes").description("Update QA run notes or acceptanc
|
|
|
10120
10914
|
resolved.folder,
|
|
10121
10915
|
resolved.experimentId
|
|
10122
10916
|
);
|
|
10123
|
-
|
|
10917
|
+
const data = opts.summary ? qaSummary(run3, {
|
|
10918
|
+
notes: run3.notes,
|
|
10919
|
+
acceptanceCriteriaSummary: run3.acceptanceCriteriaSummary
|
|
10920
|
+
}) : { run: run3 };
|
|
10921
|
+
output(this, data, () => {
|
|
10124
10922
|
spinner.succeed("Updated QA notes");
|
|
10125
|
-
|
|
10923
|
+
if (opts.summary) process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
10924
|
+
`);
|
|
10925
|
+
else printQaRun(run3);
|
|
10126
10926
|
});
|
|
10127
10927
|
} catch (error) {
|
|
10128
10928
|
spinner.fail("Failed to update QA notes");
|
|
10129
10929
|
throw error;
|
|
10130
10930
|
}
|
|
10131
10931
|
});
|
|
10132
|
-
var qaApprove = new Command("approve").description("
|
|
10932
|
+
var qaApprove = new Command("approve").description("Approve QA and move the experiment to ready_to_launch; qa reopen returns it to QA").argument(
|
|
10933
|
+
"[folder-or-id]",
|
|
10934
|
+
"Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
|
|
10935
|
+
"."
|
|
10936
|
+
).action(async function action19(target) {
|
|
10133
10937
|
const creds = requireCredentials();
|
|
10134
10938
|
const spinner = await createSpinner("Approving QA...");
|
|
10135
10939
|
try {
|
|
10136
10940
|
const resolved = await resolveExperimentForFilesystemOrId(target);
|
|
10941
|
+
const before = await resolveExperiment(resolved.experimentId);
|
|
10942
|
+
if (!before) fail(this, "Experiment not found.", EXIT_CODES.validation);
|
|
10137
10943
|
const res = await approveExperimentQa(creds, resolved.experimentId);
|
|
10138
10944
|
if (!res.ok)
|
|
10139
10945
|
throw new CliError(
|
|
@@ -10146,17 +10952,35 @@ var qaApprove = new Command("approve").description("Mark QA done after the check
|
|
|
10146
10952
|
resolved.folder,
|
|
10147
10953
|
resolved.experimentId
|
|
10148
10954
|
);
|
|
10955
|
+
if (!res.data?.status) fail(this, "QA approval response is missing experiment status.");
|
|
10956
|
+
const transition = {
|
|
10957
|
+
from: before.status,
|
|
10958
|
+
to: res.data.status,
|
|
10959
|
+
reopen: `apex experiments qa reopen ${resolved.experimentId}`
|
|
10960
|
+
};
|
|
10149
10961
|
output(
|
|
10150
10962
|
this,
|
|
10151
|
-
{
|
|
10152
|
-
|
|
10963
|
+
{
|
|
10964
|
+
experimentId: resolved.experimentId,
|
|
10965
|
+
qaStatus: "done",
|
|
10966
|
+
experiment_status_before: before.status,
|
|
10967
|
+
experiment_status_after: res.data.status,
|
|
10968
|
+
transition
|
|
10969
|
+
},
|
|
10970
|
+
() => spinner.succeed(
|
|
10971
|
+
`QA approved: ${transition.from} \u2192 ${transition.to}; reopen: ${transition.reopen}`
|
|
10972
|
+
)
|
|
10153
10973
|
);
|
|
10154
10974
|
} catch (error) {
|
|
10155
10975
|
spinner.fail("Failed to approve QA");
|
|
10156
10976
|
throw error;
|
|
10157
10977
|
}
|
|
10158
10978
|
});
|
|
10159
|
-
var qaReopen = new Command("reopen").description("Reopen QA and clear scheduled launch state").argument(
|
|
10979
|
+
var qaReopen = new Command("reopen").description("Reopen QA and clear scheduled launch state").argument(
|
|
10980
|
+
"[folder-or-id]",
|
|
10981
|
+
"Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
|
|
10982
|
+
"."
|
|
10983
|
+
).action(async function action20(target) {
|
|
10160
10984
|
const creds = requireCredentials();
|
|
10161
10985
|
const spinner = await createSpinner("Reopening QA...");
|
|
10162
10986
|
try {
|
|
@@ -10214,7 +11038,13 @@ function resolveEvidenceVariation(experiment, ref) {
|
|
|
10214
11038
|
}
|
|
10215
11039
|
throw new Error(`Unknown --variation value: ${ref}`);
|
|
10216
11040
|
}
|
|
10217
|
-
var qaEvidence = new Command("evidence").description("Attach screenshot jobs or upload typed QA evidence").argument(
|
|
11041
|
+
var qaEvidence = new Command("evidence").description("Attach screenshot jobs or upload typed QA evidence").argument(
|
|
11042
|
+
"[folder-or-id]",
|
|
11043
|
+
"Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
|
|
11044
|
+
"."
|
|
11045
|
+
).option("--job <jobId>", "Attach a terminal screenshot job").option("--file <path>", "Upload an evidence image; repeat for more files", collectFile, []).option("--label <text>", "Evidence label").option("--device <device>", "desktop, mobile, tablet, or a raw device value").option("--variation <ref>", "Variation ID, slug, or name").addOption(qaChoice("--status <status>", "Evidence status", evidenceStatuses)).addOption(qaChoice("--kind <kind>", "Evidence kind", evidenceKinds).default("screenshot")).option("--records <json-or-@path>", "Typed evidence record array").option("--summary", "Print a compact run and newly added evidence").option("--json", "Print the QA run in the standard JSON envelope").action(async function action21(target, opts) {
|
|
11046
|
+
validateQaChoice(this, "--status", opts.status, evidenceStatuses);
|
|
11047
|
+
validateQaChoice(this, "--kind", opts.kind, evidenceKinds);
|
|
10218
11048
|
const modes = Number(Boolean(opts.job)) + Number(opts.file.length > 0) + Number(Boolean(opts.records));
|
|
10219
11049
|
if (modes !== 1) {
|
|
10220
11050
|
fail(this, "Provide exactly one of --job, --file, or --records.", EXIT_CODES.validation);
|
|
@@ -10223,6 +11053,17 @@ var qaEvidence = new Command("evidence").description("Attach screenshot jobs or
|
|
|
10223
11053
|
const spinner = await createSpinner("Writing QA evidence...");
|
|
10224
11054
|
try {
|
|
10225
11055
|
const resolved = await resolveExperimentForFilesystemOrId(target);
|
|
11056
|
+
const previousIds = /* @__PURE__ */ new Set();
|
|
11057
|
+
if (opts.summary) {
|
|
11058
|
+
const current = await getExperimentQaRun(creds, resolved.experimentId);
|
|
11059
|
+
if (!current.ok || !current.data)
|
|
11060
|
+
throw new CliError(
|
|
11061
|
+
current.error ?? "Failed to fetch QA run",
|
|
11062
|
+
classifyStatus(current.status),
|
|
11063
|
+
current.errorDetails
|
|
11064
|
+
);
|
|
11065
|
+
for (const record2 of current.data.run.evidenceRecords) previousIds.add(record2.id);
|
|
11066
|
+
}
|
|
10226
11067
|
let response;
|
|
10227
11068
|
if (opts.job) {
|
|
10228
11069
|
response = await attachExperimentQaEvidence(creds, resolved.experimentId, {
|
|
@@ -10263,7 +11104,16 @@ var qaEvidence = new Command("evidence").description("Attach screenshot jobs or
|
|
|
10263
11104
|
);
|
|
10264
11105
|
}
|
|
10265
11106
|
spinner.stop();
|
|
10266
|
-
|
|
11107
|
+
const run3 = response.data.run;
|
|
11108
|
+
const data = opts.summary ? qaSummary(
|
|
11109
|
+
run3,
|
|
11110
|
+
run3.evidenceRecords.filter((record2) => !previousIds.has(record2.id))
|
|
11111
|
+
) : { run: run3 };
|
|
11112
|
+
output(this, data, () => {
|
|
11113
|
+
if (opts.summary) process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
11114
|
+
`);
|
|
11115
|
+
else printQaRun(run3);
|
|
11116
|
+
});
|
|
10267
11117
|
} catch (error) {
|
|
10268
11118
|
spinner.fail("Failed to write QA evidence");
|
|
10269
11119
|
throw error;
|
|
@@ -10279,102 +11129,255 @@ var SCREENSHOT_DEVICES = [
|
|
|
10279
11129
|
];
|
|
10280
11130
|
var safeFilenamePart = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "screenshot";
|
|
10281
11131
|
var commaList = (value) => value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
11132
|
+
function shellQuote(value) {
|
|
11133
|
+
if (value.length > 0 && /^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
|
|
11134
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
11135
|
+
}
|
|
11136
|
+
function isTransientResumeStatus(status2) {
|
|
11137
|
+
return status2 === 429 || status2 !== void 0 && status2 >= 500;
|
|
11138
|
+
}
|
|
11139
|
+
function screenshotTimeoutMessage(job, experimentRef, out, seconds, flags = {}) {
|
|
11140
|
+
const progress = job.progress ? ` Last progress: ${job.progress.captured}/${job.progress.total} captured.` : "";
|
|
11141
|
+
const carried = [
|
|
11142
|
+
flags.attachQa === false ? " --no-attach-qa" : "",
|
|
11143
|
+
flags.labelPairs === false ? " --no-label-pairs" : "",
|
|
11144
|
+
flags.json ? " --json" : ""
|
|
11145
|
+
].join("");
|
|
11146
|
+
const resume = `apex experiments screenshots ${shellQuote(experimentRef)} --job ${shellQuote(job.job_id)} --out ${shellQuote(out)}${carried}`;
|
|
11147
|
+
return `Screenshot job ${job.job_id} timed out after ${seconds}s.${progress} Resume with: ${resume}`;
|
|
11148
|
+
}
|
|
10282
11149
|
var screenshots = new Command("screenshots").description(
|
|
10283
11150
|
"Force-assign every capture, including control, to its variation server-side for deterministic screenshots"
|
|
10284
|
-
).argument("<experimentRef>",
|
|
11151
|
+
).argument("<experimentRef>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--url <target>", "Target URL override (not supported by the current v1 API)").option("--devices <list>", `Comma-separated devices (default: ${SCREENSHOT_DEVICES.join(",")})`).option(
|
|
10285
11152
|
"--variations <list>",
|
|
10286
11153
|
"Comma-separated treatment IDs, slugs, or names; control is always included and force-assigned"
|
|
10287
|
-
).option("--out <dir>", "Download directory", ".").option("--json", "Print the completed raw job payload with signed URLs").option("--no-attach-qa", "Do not attach the completed job to the QA run").option(
|
|
11154
|
+
).option("--out <dir>", "Download directory", ".").option("--job <id>", "Resume an existing screenshot job instead of creating one").option("--json", "Print the completed raw job payload with signed URLs").option("--no-attach-qa", "Do not attach the completed job to the QA run").option("--no-label-pairs", "Keep legacy screenshot filenames without before/after labels").option(
|
|
10288
11155
|
"--timeout <seconds>",
|
|
10289
11156
|
"Maximum wait for completion; finished results are always downloaded",
|
|
10290
11157
|
"300"
|
|
10291
|
-
).action(async function
|
|
11158
|
+
).action(async function action22(experimentRef, opts) {
|
|
10292
11159
|
if (opts.url)
|
|
10293
11160
|
fail(this, "--url is not supported by the current screenshot v1 API.", EXIT_CODES.validation);
|
|
10294
11161
|
const timeoutSeconds = Number(opts.timeout);
|
|
11162
|
+
const explicitJson = process.argv.includes("--json");
|
|
10295
11163
|
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)
|
|
10296
11164
|
fail(this, "--timeout must be a positive number of seconds.", EXIT_CODES.validation);
|
|
10297
11165
|
const experiment = await resolveExperiment(experimentRef);
|
|
10298
11166
|
if (!experiment?.variations?.length)
|
|
10299
11167
|
fail(this, "Experiment has no variations to capture.", EXIT_CODES.validation);
|
|
10300
|
-
const
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
);
|
|
10310
|
-
|
|
10311
|
-
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
for (const ref of requested) {
|
|
10316
|
-
const exactMatch = experiment.variations.find(
|
|
10317
|
-
(candidate) => ref === candidate.id || ref === candidate.slug
|
|
10318
|
-
);
|
|
10319
|
-
const nameMatches = exactMatch ? [] : experiment.variations.filter(
|
|
10320
|
-
(candidate) => ref.toLowerCase() === candidate.name.toLowerCase()
|
|
10321
|
-
);
|
|
10322
|
-
if (nameMatches.length > 1) {
|
|
10323
|
-
const matches = nameMatches.map(
|
|
10324
|
-
(candidate) => `${candidate.name} (ID: ${candidate.id}${candidate.slug ? `, slug: ${candidate.slug}` : ""})`
|
|
11168
|
+
const creds = requireCredentials();
|
|
11169
|
+
let devices;
|
|
11170
|
+
let variationIds;
|
|
11171
|
+
let job;
|
|
11172
|
+
let deadlineStart = null;
|
|
11173
|
+
if (opts.job) {
|
|
11174
|
+
const ignoredFlags = [
|
|
11175
|
+
opts.devices ? "--devices" : null,
|
|
11176
|
+
opts.variations ? "--variations" : null
|
|
11177
|
+
].filter((flag) => flag !== null);
|
|
11178
|
+
if (ignoredFlags.length)
|
|
11179
|
+
console.error(
|
|
11180
|
+
source_default.yellow(
|
|
11181
|
+
`Warning: ${ignoredFlags.join(" and ")} ${ignoredFlags.length > 1 ? "are" : "is"} ignored with --job; screenshot job ${opts.job} keeps its stored devices and variations.`
|
|
11182
|
+
)
|
|
10325
11183
|
);
|
|
11184
|
+
deadlineStart = screenshotNow();
|
|
11185
|
+
const resumeDeadline = deadlineStart + timeoutSeconds * 1e3;
|
|
11186
|
+
let existing;
|
|
11187
|
+
let resumeDelayMs = cliTestSetting("API") === "1" ? 1 : 1e3;
|
|
11188
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
11189
|
+
try {
|
|
11190
|
+
existing = await getScreenshotJob(
|
|
11191
|
+
creds,
|
|
11192
|
+
opts.job,
|
|
11193
|
+
Math.max(1, resumeDeadline - screenshotNow())
|
|
11194
|
+
);
|
|
11195
|
+
} catch (error) {
|
|
11196
|
+
if (attempt > 3 || screenshotNow() >= resumeDeadline) throw error;
|
|
11197
|
+
await new Promise(
|
|
11198
|
+
(resolve2) => setTimeout(
|
|
11199
|
+
resolve2,
|
|
11200
|
+
Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
|
|
11201
|
+
)
|
|
11202
|
+
);
|
|
11203
|
+
resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
|
|
11204
|
+
continue;
|
|
11205
|
+
}
|
|
11206
|
+
if (!existing.ok && isTransientResumeStatus(existing.status) && attempt <= 3 && screenshotNow() < resumeDeadline) {
|
|
11207
|
+
await new Promise(
|
|
11208
|
+
(resolve2) => setTimeout(
|
|
11209
|
+
resolve2,
|
|
11210
|
+
Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
|
|
11211
|
+
)
|
|
11212
|
+
);
|
|
11213
|
+
resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
|
|
11214
|
+
continue;
|
|
11215
|
+
}
|
|
11216
|
+
break;
|
|
11217
|
+
}
|
|
11218
|
+
if (!existing || !existing.ok || !existing.data)
|
|
10326
11219
|
fail(
|
|
10327
11220
|
this,
|
|
10328
|
-
|
|
11221
|
+
existing?.error ?? "Failed to resume screenshot job",
|
|
11222
|
+
classifyStatus(existing?.status),
|
|
11223
|
+
existing?.errorDetails
|
|
11224
|
+
);
|
|
11225
|
+
job = existing.data;
|
|
11226
|
+
if (!job.experiment_id)
|
|
11227
|
+
fail(
|
|
11228
|
+
this,
|
|
11229
|
+
`Screenshot job ${job.job_id} names no experiment; refusing to resume it under ${experimentRef} (${experiment.id}).`,
|
|
10329
11230
|
EXIT_CODES.validation
|
|
10330
11231
|
);
|
|
11232
|
+
if (job.experiment_id !== experiment.id)
|
|
11233
|
+
fail(
|
|
11234
|
+
this,
|
|
11235
|
+
`Screenshot job ${job.job_id} belongs to experiment ${job.experiment_id}, not ${experimentRef} (${experiment.id}).`,
|
|
11236
|
+
EXIT_CODES.validation
|
|
11237
|
+
);
|
|
11238
|
+
const storedDevices = job.devices ?? [];
|
|
11239
|
+
for (const device of storedDevices.filter((device2) => !SCREENSHOT_DEVICES.includes(device2)))
|
|
11240
|
+
console.error(
|
|
11241
|
+
source_default.yellow(
|
|
11242
|
+
`Warning: screenshot job ${job.job_id} names device ${device}, which this CLI does not know; skipping it.`
|
|
11243
|
+
)
|
|
11244
|
+
);
|
|
11245
|
+
devices = storedDevices.filter((device) => SCREENSHOT_DEVICES.includes(device));
|
|
11246
|
+
if (!devices.length)
|
|
11247
|
+
fail(
|
|
11248
|
+
this,
|
|
11249
|
+
storedDevices.length ? `Screenshot job ${job.job_id} names no device this CLI knows (${storedDevices.join(", ")}).` : `Screenshot job ${job.job_id} names no devices; it cannot be resumed.`,
|
|
11250
|
+
EXIT_CODES.validation
|
|
11251
|
+
);
|
|
11252
|
+
if (Array.isArray(job.results))
|
|
11253
|
+
fail(
|
|
11254
|
+
this,
|
|
11255
|
+
`Screenshot job ${job.job_id} stores its results in a legacy shape this CLI cannot download; create a new job instead of resuming it.`,
|
|
11256
|
+
EXIT_CODES.validation
|
|
11257
|
+
);
|
|
11258
|
+
const knownVariationIds = new Set(experiment.variations.map((variation) => variation.id));
|
|
11259
|
+
const storedVariationIds = job.variation_ids ?? [];
|
|
11260
|
+
for (const id of storedVariationIds.filter((id2) => !knownVariationIds.has(id2)))
|
|
11261
|
+
console.error(
|
|
11262
|
+
source_default.yellow(
|
|
11263
|
+
`Warning: screenshot job ${job.job_id} names variation ${id}, which is not part of this experiment; skipping it.`
|
|
11264
|
+
)
|
|
11265
|
+
);
|
|
11266
|
+
variationIds = storedVariationIds.filter((id) => knownVariationIds.has(id));
|
|
11267
|
+
if (!variationIds.length)
|
|
11268
|
+
fail(
|
|
11269
|
+
this,
|
|
11270
|
+
`Screenshot job ${job.job_id} names no variation that belongs to experiment ${experimentRef}.`,
|
|
11271
|
+
EXIT_CODES.validation
|
|
11272
|
+
);
|
|
11273
|
+
} else {
|
|
11274
|
+
devices = opts.devices ? commaList(opts.devices) : SCREENSHOT_DEVICES;
|
|
11275
|
+
if (!devices.length)
|
|
11276
|
+
fail(this, "--devices must contain at least one device.", EXIT_CODES.validation);
|
|
11277
|
+
const unknownDevices = devices.filter((device) => !SCREENSHOT_DEVICES.includes(device));
|
|
11278
|
+
if (unknownDevices.length)
|
|
11279
|
+
fail(
|
|
11280
|
+
this,
|
|
11281
|
+
`Unknown screenshot device(s): ${unknownDevices.join(", ")}`,
|
|
11282
|
+
EXIT_CODES.validation
|
|
11283
|
+
);
|
|
11284
|
+
const requested = opts.variations ? commaList(opts.variations) : [];
|
|
11285
|
+
const control = experiment.variations.find((variation) => variation.is_control);
|
|
11286
|
+
if (!control) fail(this, "Experiment has no control variation.", EXIT_CODES.validation);
|
|
11287
|
+
const matchedById = /* @__PURE__ */ new Map();
|
|
11288
|
+
const unmatched = [];
|
|
11289
|
+
for (const ref of requested) {
|
|
11290
|
+
const exactMatch = experiment.variations.find(
|
|
11291
|
+
(candidate) => ref === candidate.id || ref === candidate.slug
|
|
11292
|
+
);
|
|
11293
|
+
const nameMatches = exactMatch ? [] : experiment.variations.filter(
|
|
11294
|
+
(candidate) => ref.toLowerCase() === candidate.name.toLowerCase()
|
|
11295
|
+
);
|
|
11296
|
+
if (nameMatches.length > 1) {
|
|
11297
|
+
const matches = nameMatches.map(
|
|
11298
|
+
(candidate) => `${candidate.name} (ID: ${candidate.id}${candidate.slug ? `, slug: ${candidate.slug}` : ""})`
|
|
11299
|
+
);
|
|
11300
|
+
fail(
|
|
11301
|
+
this,
|
|
11302
|
+
`Ambiguous --variations value "${ref}" matches: ${matches.join("; ")}. Use a variation ID or slug.`,
|
|
11303
|
+
EXIT_CODES.validation
|
|
11304
|
+
);
|
|
11305
|
+
}
|
|
11306
|
+
const variation = exactMatch ?? nameMatches[0];
|
|
11307
|
+
if (variation) matchedById.set(variation.id, variation);
|
|
11308
|
+
else unmatched.push(ref);
|
|
10331
11309
|
}
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
created.errorDetails
|
|
10351
|
-
);
|
|
10352
|
-
let job = created.data;
|
|
10353
|
-
const deadline = screenshotNow() + timeoutSeconds * 1e3;
|
|
11310
|
+
if (unmatched.length)
|
|
11311
|
+
fail(this, `Unknown --variations value(s): ${unmatched.join(", ")}`, EXIT_CODES.validation);
|
|
11312
|
+
const selected2 = requested.length ? [...matchedById.values()] : experiment.variations;
|
|
11313
|
+
variationIds = [.../* @__PURE__ */ new Set([control.id, ...selected2.map((variation) => variation.id)])];
|
|
11314
|
+
const created = await createExperimentScreenshots(creds, experiment.id, {
|
|
11315
|
+
devices,
|
|
11316
|
+
variation_ids: variationIds
|
|
11317
|
+
});
|
|
11318
|
+
if (!created.ok || !created.data)
|
|
11319
|
+
fail(
|
|
11320
|
+
this,
|
|
11321
|
+
created.error ?? "Failed to create screenshot job",
|
|
11322
|
+
classifyStatus(created.status),
|
|
11323
|
+
created.errorDetails
|
|
11324
|
+
);
|
|
11325
|
+
job = created.data;
|
|
11326
|
+
}
|
|
11327
|
+
const deadline = (deadlineStart ?? screenshotNow()) + timeoutSeconds * 1e3;
|
|
10354
11328
|
let delayMs = cliTestSetting("API") === "1" ? 1 : 1e3;
|
|
10355
11329
|
let consecutiveTransientFailures = 0;
|
|
10356
11330
|
while (job.status === "queued" || job.status === "processing") {
|
|
10357
11331
|
if (screenshotNow() >= deadline)
|
|
10358
|
-
fail(
|
|
11332
|
+
fail(
|
|
11333
|
+
this,
|
|
11334
|
+
screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
|
|
11335
|
+
labelPairs: opts.labelPairs,
|
|
11336
|
+
json: explicitJson,
|
|
11337
|
+
attachQa: opts.attachQa
|
|
11338
|
+
})
|
|
11339
|
+
);
|
|
10359
11340
|
await new Promise(
|
|
10360
11341
|
(resolve2) => setTimeout(resolve2, Math.min(delayMs, Math.max(0, deadline - screenshotNow())))
|
|
10361
11342
|
);
|
|
10362
11343
|
if (screenshotNow() >= deadline)
|
|
10363
|
-
fail(
|
|
11344
|
+
fail(
|
|
11345
|
+
this,
|
|
11346
|
+
screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
|
|
11347
|
+
labelPairs: opts.labelPairs,
|
|
11348
|
+
json: explicitJson,
|
|
11349
|
+
attachQa: opts.attachQa
|
|
11350
|
+
})
|
|
11351
|
+
);
|
|
10364
11352
|
let polled;
|
|
10365
11353
|
try {
|
|
10366
11354
|
polled = await getScreenshotJob(creds, job.job_id, Math.max(1, deadline - screenshotNow()));
|
|
10367
11355
|
} catch (error) {
|
|
10368
11356
|
if (screenshotNow() >= deadline)
|
|
10369
|
-
fail(
|
|
11357
|
+
fail(
|
|
11358
|
+
this,
|
|
11359
|
+
screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
|
|
11360
|
+
labelPairs: opts.labelPairs,
|
|
11361
|
+
json: explicitJson,
|
|
11362
|
+
attachQa: opts.attachQa
|
|
11363
|
+
})
|
|
11364
|
+
);
|
|
10370
11365
|
consecutiveTransientFailures += 1;
|
|
10371
11366
|
if (consecutiveTransientFailures > 3) throw error;
|
|
10372
11367
|
delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
|
|
10373
11368
|
continue;
|
|
10374
11369
|
}
|
|
10375
11370
|
const polledToTerminalState = polled.ok && polled.data && polled.data.status !== "queued" && polled.data.status !== "processing";
|
|
11371
|
+
if (polled.ok && polled.data) job = polled.data;
|
|
10376
11372
|
if (!polledToTerminalState && screenshotNow() >= deadline) {
|
|
10377
|
-
fail(
|
|
11373
|
+
fail(
|
|
11374
|
+
this,
|
|
11375
|
+
screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
|
|
11376
|
+
labelPairs: opts.labelPairs,
|
|
11377
|
+
json: explicitJson,
|
|
11378
|
+
attachQa: opts.attachQa
|
|
11379
|
+
})
|
|
11380
|
+
);
|
|
10378
11381
|
}
|
|
10379
11382
|
if (!polled.ok || !polled.data) {
|
|
10380
11383
|
const transient = polled.status === 429 || polled.status >= 500;
|
|
@@ -10390,7 +11393,6 @@ var screenshots = new Command("screenshots").description(
|
|
|
10390
11393
|
continue;
|
|
10391
11394
|
}
|
|
10392
11395
|
consecutiveTransientFailures = 0;
|
|
10393
|
-
job = polled.data;
|
|
10394
11396
|
delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
|
|
10395
11397
|
}
|
|
10396
11398
|
if (opts.attachQa !== false) {
|
|
@@ -10421,16 +11423,32 @@ var screenshots = new Command("screenshots").description(
|
|
|
10421
11423
|
const rows = [];
|
|
10422
11424
|
const errors = [];
|
|
10423
11425
|
const usedFilenames = /* @__PURE__ */ new Set();
|
|
11426
|
+
const hasControlVariation = experiment.variations.some((item) => item.is_control);
|
|
10424
11427
|
const downloads = [];
|
|
11428
|
+
if (Array.isArray(job.results))
|
|
11429
|
+
fail(
|
|
11430
|
+
this,
|
|
11431
|
+
`Screenshot job ${job.job_id} stores its results in a legacy shape this CLI cannot download; create a new job instead of resuming it.`,
|
|
11432
|
+
EXIT_CODES.validation
|
|
11433
|
+
);
|
|
10425
11434
|
for (const variationId of variationIds) {
|
|
10426
11435
|
const variation = experiment.variations.find((item) => item.id === variationId);
|
|
11436
|
+
if (!variation) {
|
|
11437
|
+
console.error(
|
|
11438
|
+
source_default.yellow(
|
|
11439
|
+
`Warning: variation ${variationId} is not part of this experiment; skipping it.`
|
|
11440
|
+
)
|
|
11441
|
+
);
|
|
11442
|
+
continue;
|
|
11443
|
+
}
|
|
10427
11444
|
for (const device of devices) {
|
|
10428
11445
|
const url = job.results?.[variationId]?.[device];
|
|
10429
11446
|
if (!url) {
|
|
10430
11447
|
downloads.push({ variation, device, url, filename: "" });
|
|
10431
11448
|
continue;
|
|
10432
11449
|
}
|
|
10433
|
-
const
|
|
11450
|
+
const pairPrefix = opts.labelPairs === false || !hasControlVariation ? "" : variation.is_control ? "before-" : "after-";
|
|
11451
|
+
const baseFilename = `${pairPrefix}${safeFilenamePart(experiment.slug ?? experiment.name)}-${safeFilenamePart(variation.slug ?? variation.name)}-${safeFilenamePart(device)}`;
|
|
10434
11452
|
let filename = `${baseFilename}.png`;
|
|
10435
11453
|
if (usedFilenames.has(filename)) {
|
|
10436
11454
|
const suffix = variation.id.replace(/[^a-z0-9]/gi, "").slice(-8).toLowerCase();
|
|
@@ -10487,7 +11505,7 @@ var screenshots = new Command("screenshots").description(
|
|
|
10487
11505
|
printTable(["VARIATION", "DEVICE", "FILE"], rows, [24, 20, 48]);
|
|
10488
11506
|
});
|
|
10489
11507
|
});
|
|
10490
|
-
var experimentsCommand = new Command("experiments").alias("exp").description("Manage experiments").addCommand(list).addCommand(get).addCommand(results).addCommand(create).addCommand(pause).addCommand(complete).addCommand(del).addCommand(pullFilesystem).addCommand(planFilesystem).addCommand(applyFilesystem).addCommand(screenshots).addCommand(qa).addHelpText(
|
|
11508
|
+
var experimentsCommand = new Command("experiments").alias("exp").description("Manage experiments").addCommand(list).addCommand(get).addCommand(results).addCommand(update).addCommand(create).addCommand(attachIdea).addCommand(detachIdea).addCommand(pause).addCommand(complete).addCommand(del).addCommand(pullFilesystem).addCommand(planFilesystem).addCommand(applyFilesystem).addCommand(screenshots).addCommand(qa).addHelpText(
|
|
10491
11509
|
"after",
|
|
10492
11510
|
`
|
|
10493
11511
|
Examples:
|
|
@@ -10581,10 +11599,10 @@ async function importConfigModule(absPath) {
|
|
|
10581
11599
|
}
|
|
10582
11600
|
throw new Error(`Unsupported config extension "${ext}". Use .drip.config.ts or .drip.config.js.`);
|
|
10583
11601
|
}
|
|
10584
|
-
function
|
|
11602
|
+
function isRecord2(value) {
|
|
10585
11603
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
10586
11604
|
}
|
|
10587
|
-
var pushCommand = new Command("push").description("Push experiment definitions from config file to the API (idempotent by slug)").option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").option("--no-asset-upload", "Keep inline and local image references unchanged").action(async function
|
|
11605
|
+
var pushCommand = new Command("push").description("Push experiment definitions from config file to the API (idempotent by slug)").option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").option("--no-asset-upload", "Keep inline and local image references unchanged").action(async function action23(opts) {
|
|
10588
11606
|
const creds = requireCredentials();
|
|
10589
11607
|
const configPath = opts.config ?? findConfigPath();
|
|
10590
11608
|
if (!configPath) {
|
|
@@ -10599,12 +11617,12 @@ var pushCommand = new Command("push").description("Push experiment definitions f
|
|
|
10599
11617
|
let experiments;
|
|
10600
11618
|
try {
|
|
10601
11619
|
const mod = await importConfigModule(absoluteConfigPath);
|
|
10602
|
-
const exported =
|
|
11620
|
+
const exported = isRecord2(mod) && "default" in mod ? mod.default : mod;
|
|
10603
11621
|
if (Array.isArray(exported)) {
|
|
10604
11622
|
experiments = exported;
|
|
10605
|
-
} else if (
|
|
11623
|
+
} else if (isRecord2(exported) && Array.isArray(exported.experiments)) {
|
|
10606
11624
|
experiments = exported.experiments;
|
|
10607
|
-
} else if (
|
|
11625
|
+
} else if (isRecord2(exported) && typeof exported.slug === "string" && Array.isArray(exported.variations)) {
|
|
10608
11626
|
experiments = [exported];
|
|
10609
11627
|
} else {
|
|
10610
11628
|
spinner?.fail("Invalid config format");
|
|
@@ -11028,7 +12046,7 @@ async function createSession(folder) {
|
|
|
11028
12046
|
var harnessCommand = new Command("harness").description(
|
|
11029
12047
|
"Develop AB Test Harness projects through Apex live QA sessions"
|
|
11030
12048
|
);
|
|
11031
|
-
harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function
|
|
12049
|
+
harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action24(folderArg, opts) {
|
|
11032
12050
|
const folder = resolveFolder(folderArg);
|
|
11033
12051
|
const targetPath = apexConfigPath(folder);
|
|
11034
12052
|
if (import_node_fs9.default.existsSync(targetPath) && !opts.force) {
|
|
@@ -11048,7 +12066,7 @@ harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment
|
|
|
11048
12066
|
() => printSuccess(`Created ${targetPath}`)
|
|
11049
12067
|
);
|
|
11050
12068
|
});
|
|
11051
|
-
harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment folder").option("--no-open", "Print preview URL instead of opening it").action(async function
|
|
12069
|
+
harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment folder").option("--no-open", "Print preview URL instead of opening it").action(async function action25(folderArg, opts) {
|
|
11052
12070
|
const folder = resolveFolder(folderArg);
|
|
11053
12071
|
const created = await createSession(folder);
|
|
11054
12072
|
await runAbHarness(["build", folder], folder);
|
|
@@ -11115,7 +12133,7 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
|
|
|
11115
12133
|
process.exitCode = 0;
|
|
11116
12134
|
});
|
|
11117
12135
|
});
|
|
11118
|
-
harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function
|
|
12136
|
+
harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action26(folderArg) {
|
|
11119
12137
|
const folder = resolveFolder(folderArg);
|
|
11120
12138
|
const created = await createSession(folder);
|
|
11121
12139
|
await runAbHarness(["verify", folder], folder);
|
|
@@ -11490,7 +12508,7 @@ Examples:
|
|
|
11490
12508
|
apex workspace publish ./workspace
|
|
11491
12509
|
`
|
|
11492
12510
|
);
|
|
11493
|
-
operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment ID").requiredOption("--variation <id-or-slug>", "Variation ID, slug, or name").option("--out <path>", "Target folder").action(async function
|
|
12511
|
+
operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment ID").requiredOption("--variation <id-or-slug>", "Variation ID, slug, or name").option("--out <path>", "Target folder").action(async function action27(experimentId, opts) {
|
|
11494
12512
|
try {
|
|
11495
12513
|
const creds = requireCredentials();
|
|
11496
12514
|
const expRes = await getExperiment(creds, experimentId);
|
|
@@ -11540,7 +12558,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
|
|
|
11540
12558
|
throw error;
|
|
11541
12559
|
}
|
|
11542
12560
|
});
|
|
11543
|
-
operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function
|
|
12561
|
+
operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action28(folderArg) {
|
|
11544
12562
|
try {
|
|
11545
12563
|
const folder = folderPath2(folderArg);
|
|
11546
12564
|
const session = readSession(folder);
|
|
@@ -11572,7 +12590,7 @@ operatorCommand.command("status").argument("[folder]", "Operator workspace folde
|
|
|
11572
12590
|
fail(this, error instanceof Error ? error.message : String(error));
|
|
11573
12591
|
}
|
|
11574
12592
|
});
|
|
11575
|
-
operatorCommand.command("dev").argument("[folder]", "Operator workspace folder", ".").option("--no-open", "Print preview URL instead of opening it").option("--once", "Upload one preview version and exit").action(async function
|
|
12593
|
+
operatorCommand.command("dev").argument("[folder]", "Operator workspace folder", ".").option("--no-open", "Print preview URL instead of opening it").option("--once", "Upload one preview version and exit").action(async function action29(folderArg, opts) {
|
|
11576
12594
|
try {
|
|
11577
12595
|
const folder = folderPath2(folderArg);
|
|
11578
12596
|
const session = readSession(folder);
|
|
@@ -11651,7 +12669,7 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
|
|
|
11651
12669
|
throw error;
|
|
11652
12670
|
}
|
|
11653
12671
|
});
|
|
11654
|
-
operatorCommand.command("publish").argument("[folder]", "Operator workspace folder", ".").option("--allow-goal-writes", "Allow metrics.json to create or update shared shop-level goals").action(async function
|
|
12672
|
+
operatorCommand.command("publish").argument("[folder]", "Operator workspace folder", ".").option("--allow-goal-writes", "Allow metrics.json to create or update shared shop-level goals").action(async function action30(folderArg, opts) {
|
|
11655
12673
|
try {
|
|
11656
12674
|
const result = await runPublish(folderArg, opts);
|
|
11657
12675
|
const build = result.build;
|
|
@@ -11695,7 +12713,7 @@ function briefLine(item) {
|
|
|
11695
12713
|
].filter(Boolean);
|
|
11696
12714
|
return parts.join("\n");
|
|
11697
12715
|
}
|
|
11698
|
-
var approved = new Command("approved").description("List approved roadmap ideas ready to build in Apex").option("-n, --limit <n>", "Max results", "20").option("--cursor <id>", "Pagination cursor").option("-q, --query <text>", "Search title, hypothesis, brief, and notes").option("--unbuilt", "Only show approved ideas without a linked experiment").option("--built", "Only show approved ideas with a linked experiment").option("--json", "Print raw JSON").option("--brief", "Print each item with hypothesis and target URL").action(async function
|
|
12716
|
+
var approved = new Command("approved").description("List approved roadmap ideas ready to build in Apex").option("-n, --limit <n>", "Max results", "20").option("--cursor <id>", "Pagination cursor").option("-q, --query <text>", "Search title, hypothesis, brief, and notes").option("--unbuilt", "Only show approved ideas without a linked experiment").option("--built", "Only show approved ideas with a linked experiment").option("--json", "Print raw JSON").option("--brief", "Print each item with hypothesis and target URL").action(async function action31(opts) {
|
|
11699
12717
|
const creds = requireCredentials();
|
|
11700
12718
|
const limit = parseInt(opts.limit, 10);
|
|
11701
12719
|
const hasExperiment = opts.unbuilt ? false : opts.built ? true : void 0;
|
|
@@ -11749,7 +12767,7 @@ More results: --cursor ${res.data.pagination.next_cursor}`));
|
|
|
11749
12767
|
}
|
|
11750
12768
|
);
|
|
11751
12769
|
});
|
|
11752
|
-
var handoff = new Command("handoff").description("Get the full build brief for an approved roadmap item").argument("<id>", "Roadmap/backlog item ID").option("--out <path>", "Write handoff JSON to a file").addOption(new Option("-o, --output <path>", "Legacy alias for --out").hideHelp()).option("--instructions", "Print only the build instructions").action(async function
|
|
12770
|
+
var handoff = new Command("handoff").description("Get the full build brief for an approved roadmap item").argument("<id>", "Roadmap/backlog item ID").option("--out <path>", "Write handoff JSON to a file").addOption(new Option("-o, --output <path>", "Legacy alias for --out").hideHelp()).option("--instructions", "Print only the build instructions").action(async function action32(id, opts) {
|
|
11753
12771
|
if (opts.output !== void 0) {
|
|
11754
12772
|
process.stderr.write(
|
|
11755
12773
|
`Warning: --output is deprecated for apex roadmap handoff; use --out instead. --output will be removed in v${LEGACY_OUTPUT_REMOVAL_VERSION}.
|
|
@@ -11968,7 +12986,7 @@ trackingCommand.command("qa").description("Print the QA checklist for a tracking
|
|
|
11968
12986
|
console.log(`${index + 1}. ${check}`);
|
|
11969
12987
|
});
|
|
11970
12988
|
});
|
|
11971
|
-
trackingCommand.command("verify").description("Verify live tracking events for the current shop").option("--minutes <n>", "Lookback window in minutes", "120").option("--limit <n>", "Recent events to include", "20").option("--experiment <id>", "Filter to one experiment ID").option("--event-type <type>", "Filter to one event type; repeat with comma-separated values").action(async function
|
|
12989
|
+
trackingCommand.command("verify").description("Verify live tracking events for the current shop").option("--minutes <n>", "Lookback window in minutes", "120").option("--limit <n>", "Recent events to include", "20").option("--experiment <id>", "Filter to one experiment ID").option("--event-type <type>", "Filter to one event type; repeat with comma-separated values").action(async function action33(opts) {
|
|
11972
12990
|
const creds = requireCredentials();
|
|
11973
12991
|
const res = await verifyTracking(creds, {
|
|
11974
12992
|
minutes: Number.parseInt(opts.minutes, 10),
|
|
@@ -12225,7 +13243,7 @@ function printGoal(goal) {
|
|
|
12225
13243
|
console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
|
|
12226
13244
|
console.log();
|
|
12227
13245
|
}
|
|
12228
|
-
var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function
|
|
13246
|
+
var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action34(opts) {
|
|
12229
13247
|
const creds = requireCredentials();
|
|
12230
13248
|
const res = await listGoals(creds, { limit: Number.parseInt(opts.limit, 10) });
|
|
12231
13249
|
if (!res.ok || !res.data) {
|
|
@@ -12249,7 +13267,7 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
|
|
|
12249
13267
|
);
|
|
12250
13268
|
});
|
|
12251
13269
|
});
|
|
12252
|
-
var create2 = new Command("create").description("Create a goal").requiredOption("--name <name>", "Goal name").requiredOption("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value", "conversion").option("--value-property <property>", "Numeric event property for value goals").option("--value-unit <unit>", "currency|number|duration_seconds|percent").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function
|
|
13270
|
+
var create2 = new Command("create").description("Create a goal").requiredOption("--name <name>", "Goal name").requiredOption("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value", "conversion").option("--value-property <property>", "Numeric event property for value goals").option("--value-unit <unit>", "currency|number|duration_seconds|percent").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function action35(opts) {
|
|
12253
13271
|
const creds = requireCredentials();
|
|
12254
13272
|
const body5 = cleanBody({
|
|
12255
13273
|
name: opts.name,
|
|
@@ -12275,10 +13293,10 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
|
|
|
12275
13293
|
}
|
|
12276
13294
|
output(this, { goal: res.data }, () => printGoal(res.data));
|
|
12277
13295
|
});
|
|
12278
|
-
var
|
|
13296
|
+
var update2 = new Command("update").description("Update a goal").argument("<id>", "Goal ID").option("--name <name>", "Goal name").option("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value").option(
|
|
12279
13297
|
"--value-property <property>",
|
|
12280
13298
|
"Numeric event property for value goals; pass null to clear"
|
|
12281
|
-
).option("--value-unit <unit>", "currency|number|duration_seconds|percent; pass null to clear").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--no-count-once", "Do not count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function
|
|
13299
|
+
).option("--value-unit <unit>", "currency|number|duration_seconds|percent; pass null to clear").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--no-count-once", "Do not count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function action36(id, opts) {
|
|
12282
13300
|
const creds = requireCredentials();
|
|
12283
13301
|
const body5 = cleanBody({
|
|
12284
13302
|
name: opts.name,
|
|
@@ -12310,7 +13328,7 @@ var update = new Command("update").description("Update a goal").argument("<id>",
|
|
|
12310
13328
|
var test = new Command("test").description("Dry run a goal URL pattern against traffic").option(
|
|
12311
13329
|
"--pattern <regex>",
|
|
12312
13330
|
"URL regex pattern to test (matched against the full URL, as the SDK matches pageview goals)"
|
|
12313
|
-
).option("--goal <id>", "Existing goal ID whose URL pattern should be tested").option("--days <n>", "Traffic lookback days", "14").action(async function
|
|
13331
|
+
).option("--goal <id>", "Existing goal ID whose URL pattern should be tested").option("--days <n>", "Traffic lookback days", "14").action(async function action37(opts) {
|
|
12314
13332
|
if (!opts.pattern && !opts.goal)
|
|
12315
13333
|
fail(this, "Provide --pattern or --goal.", EXIT_CODES.validation);
|
|
12316
13334
|
if (opts.pattern && opts.goal)
|
|
@@ -12392,7 +13410,7 @@ var test = new Command("test").description("Dry run a goal URL pattern against t
|
|
|
12392
13410
|
}
|
|
12393
13411
|
);
|
|
12394
13412
|
});
|
|
12395
|
-
var suggest = new Command("suggest").description("Suggest goals and funnels from analytics").option("--create", "Create selected goals after confirmation").option("--yes", "Skip prompts when used with create").action(async function
|
|
13413
|
+
var suggest = new Command("suggest").description("Suggest goals and funnels from analytics").option("--create", "Create selected goals after confirmation").option("--yes", "Skip prompts when used with create").action(async function action38(opts) {
|
|
12396
13414
|
const creds = requireCredentials();
|
|
12397
13415
|
const res = await suggestGoals(creds);
|
|
12398
13416
|
if (!res.ok || !res.data)
|
|
@@ -12440,7 +13458,7 @@ var suggest = new Command("suggest").description("Suggest goals and funnels from
|
|
|
12440
13458
|
);
|
|
12441
13459
|
}
|
|
12442
13460
|
});
|
|
12443
|
-
var goalsCommand = new Command("goals").description("Manage conversion and revenue goals").addCommand(list2).addCommand(create2).addCommand(
|
|
13461
|
+
var goalsCommand = new Command("goals").description("Manage conversion and revenue goals").addCommand(list2).addCommand(create2).addCommand(update2).addCommand(test).addCommand(suggest).addHelpText(
|
|
12444
13462
|
"after",
|
|
12445
13463
|
`
|
|
12446
13464
|
Examples:
|
|
@@ -12482,7 +13500,7 @@ function printFunnel(funnel) {
|
|
|
12482
13500
|
console.log(` ${step.order}. ${step.label} \u2014 ${step.url_pattern}`);
|
|
12483
13501
|
console.log();
|
|
12484
13502
|
}
|
|
12485
|
-
var list3 = new Command("list").alias("ls").description("List funnels").action(async function
|
|
13503
|
+
var list3 = new Command("list").alias("ls").description("List funnels").action(async function action39() {
|
|
12486
13504
|
const res = await listFunnels(requireCredentials());
|
|
12487
13505
|
if (!res.ok || !res.data)
|
|
12488
13506
|
fail(
|
|
@@ -12501,7 +13519,7 @@ var list3 = new Command("list").alias("ls").description("List funnels").action(a
|
|
|
12501
13519
|
)
|
|
12502
13520
|
);
|
|
12503
13521
|
});
|
|
12504
|
-
var create3 = new Command("create").description("Create a funnel").requiredOption("--name <name>", "Funnel name").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(async function
|
|
13522
|
+
var create3 = new Command("create").description("Create a funnel").requiredOption("--name <name>", "Funnel name").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(async function action40(opts) {
|
|
12505
13523
|
const res = await createFunnel(requireCredentials(), {
|
|
12506
13524
|
name: opts.name,
|
|
12507
13525
|
steps: readSteps(this, opts.stepsFile)
|
|
@@ -12515,7 +13533,7 @@ var create3 = new Command("create").description("Create a funnel").requiredOptio
|
|
|
12515
13533
|
);
|
|
12516
13534
|
output(this, { funnel: res.data }, () => printFunnel(res.data));
|
|
12517
13535
|
});
|
|
12518
|
-
var
|
|
13536
|
+
var update3 = new Command("update").description("Replace a funnel's steps").argument("<id>", "Funnel ID").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(async function action41(id, opts) {
|
|
12519
13537
|
const res = await updateFunnel(requireCredentials(), id, {
|
|
12520
13538
|
steps: readSteps(this, opts.stepsFile)
|
|
12521
13539
|
});
|
|
@@ -12528,7 +13546,7 @@ var update2 = new Command("update").description("Replace a funnel's steps").argu
|
|
|
12528
13546
|
);
|
|
12529
13547
|
output(this, { funnel: res.data }, () => printFunnel(res.data));
|
|
12530
13548
|
});
|
|
12531
|
-
var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function
|
|
13549
|
+
var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action42(id) {
|
|
12532
13550
|
const res = await deleteFunnel(requireCredentials(), id);
|
|
12533
13551
|
if (!res.ok)
|
|
12534
13552
|
fail(
|
|
@@ -12539,7 +13557,7 @@ var del2 = new Command("delete").description("Delete a funnel").argument("<id>",
|
|
|
12539
13557
|
);
|
|
12540
13558
|
output(this, { deleted: true, id }, () => console.log(`Deleted funnel ${id}`));
|
|
12541
13559
|
});
|
|
12542
|
-
var run = new Command("run").description("Run funnel analysis").argument("<id>", "Funnel ID").option("--days <n>", "Analysis window", "30").action(async function
|
|
13560
|
+
var run = new Command("run").description("Run funnel analysis").argument("<id>", "Funnel ID").option("--days <n>", "Analysis window", "30").action(async function action43(id, opts) {
|
|
12543
13561
|
if (!/^[1-9]\d*$/.test(opts.days))
|
|
12544
13562
|
fail(this, "--days must be a positive integer", EXIT_CODES.validation);
|
|
12545
13563
|
const days = Number.parseInt(opts.days, 10);
|
|
@@ -12563,11 +13581,11 @@ var run = new Command("run").description("Run funnel analysis").argument("<id>",
|
|
|
12563
13581
|
)
|
|
12564
13582
|
);
|
|
12565
13583
|
});
|
|
12566
|
-
var validate2 = new Command("validate").description("Validate funnel steps without writing").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(function
|
|
13584
|
+
var validate2 = new Command("validate").description("Validate funnel steps without writing").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(function action44(opts) {
|
|
12567
13585
|
const steps = readSteps(this, opts.stepsFile);
|
|
12568
13586
|
output(this, { valid: true, steps }, () => console.log(`Valid funnel steps: ${steps.length}`));
|
|
12569
13587
|
});
|
|
12570
|
-
var funnelsCommand = new Command("funnels").description("Manage analytics funnels").addCommand(list3).addCommand(create3).addCommand(
|
|
13588
|
+
var funnelsCommand = new Command("funnels").description("Manage analytics funnels").addCommand(list3).addCommand(create3).addCommand(update3).addCommand(del2).addCommand(run).addCommand(validate2);
|
|
12571
13589
|
|
|
12572
13590
|
// src/commands/environments.ts
|
|
12573
13591
|
init_source();
|
|
@@ -12624,11 +13642,11 @@ async function load2(command) {
|
|
|
12624
13642
|
}
|
|
12625
13643
|
return result.data;
|
|
12626
13644
|
}
|
|
12627
|
-
var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function
|
|
13645
|
+
var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action45() {
|
|
12628
13646
|
const { environments } = await load2(this);
|
|
12629
13647
|
output(this, { environments }, () => printEnvironments(environments.items));
|
|
12630
13648
|
});
|
|
12631
|
-
var setDomain = new Command("set-domain").description("Map a domain to a runtime environment").argument("<domain>", "Domain to map").requiredOption("--environment <env>", "Environment key").action(async function
|
|
13649
|
+
var setDomain = new Command("set-domain").description("Map a domain to a runtime environment").argument("<domain>", "Domain to map").requiredOption("--environment <env>", "Environment key").action(async function action46(domain, opts) {
|
|
12632
13650
|
const normalizedDomain = normalizeEnvironmentDomain(domain);
|
|
12633
13651
|
if (!normalizedDomain) {
|
|
12634
13652
|
fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
|
|
@@ -12675,7 +13693,7 @@ var setDomain = new Command("set-domain").description("Map a domain to a runtime
|
|
|
12675
13693
|
() => printEnvironments(result.data.environments.items)
|
|
12676
13694
|
);
|
|
12677
13695
|
});
|
|
12678
|
-
var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function
|
|
13696
|
+
var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action47(domain) {
|
|
12679
13697
|
const normalizedDomain = normalizeEnvironmentDomain(domain);
|
|
12680
13698
|
if (!normalizedDomain) {
|
|
12681
13699
|
fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
|
|
@@ -12739,11 +13757,11 @@ function printConsent(data) {
|
|
|
12739
13757
|
`Split: ${data.prerequisites.showVariationsRule && data.prerequisites.startTrackingRule ? "canonical" : "off"}`
|
|
12740
13758
|
);
|
|
12741
13759
|
}
|
|
12742
|
-
var get2 = new Command("get").description("Show consent settings").action(async function
|
|
13760
|
+
var get2 = new Command("get").description("Show consent settings").action(async function action48() {
|
|
12743
13761
|
const data = await load3(this);
|
|
12744
13762
|
output(this, data, () => printConsent(data));
|
|
12745
13763
|
});
|
|
12746
|
-
var set = new Command("set").description("Enable and configure consent gating").requiredOption("--provider <provider>", "Consent provider").requiredOption("--group <group>", "Required consent group").option("--getter <path>", "Custom consent getter path").action(async function
|
|
13764
|
+
var set = new Command("set").description("Enable and configure consent gating").requiredOption("--provider <provider>", "Consent provider").requiredOption("--group <group>", "Required consent group").option("--getter <path>", "Custom consent getter path").action(async function action49(opts) {
|
|
12747
13765
|
const consentMode = {
|
|
12748
13766
|
enabled: true,
|
|
12749
13767
|
provider: opts.provider,
|
|
@@ -12764,7 +13782,7 @@ var set = new Command("set").description("Enable and configure consent gating").
|
|
|
12764
13782
|
warn(this, result.data.precompile);
|
|
12765
13783
|
output(this, result.data, () => printConsent(result.data));
|
|
12766
13784
|
});
|
|
12767
|
-
var off = new Command("off").description("Disable consent gating").action(async function
|
|
13785
|
+
var off = new Command("off").description("Disable consent gating").action(async function action50() {
|
|
12768
13786
|
const result = await updateConsent(requireCredentials(), { consentMode: { enabled: false } });
|
|
12769
13787
|
if (!result.ok || !result.data)
|
|
12770
13788
|
fail(
|
|
@@ -13078,8 +14096,8 @@ function score2(path24, patterns) {
|
|
|
13078
14096
|
0
|
|
13079
14097
|
);
|
|
13080
14098
|
}
|
|
13081
|
-
function best(
|
|
13082
|
-
const ranked =
|
|
14099
|
+
function best(fields3, patterns, numeric2 = false) {
|
|
14100
|
+
const ranked = fields3.filter(
|
|
13083
14101
|
(field) => !numeric2 || field.numericType === true || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
|
|
13084
14102
|
).map((field) => ({ field, score: score2(field.path.toLowerCase(), patterns) })).filter((item) => item.score > 0).sort((a, b) => b.score - a.score || (b.field.magnitude ?? 0) - (a.field.magnitude ?? 0));
|
|
13085
14103
|
if (!ranked.length) return [];
|
|
@@ -13104,15 +14122,15 @@ function proposeTriggers(report) {
|
|
|
13104
14122
|
continue;
|
|
13105
14123
|
const descriptors = candidate.fields ?? candidate.paths;
|
|
13106
14124
|
const payload = candidate.properties ?? candidate.payload ?? candidate;
|
|
13107
|
-
const
|
|
14125
|
+
const fields3 = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
|
|
13108
14126
|
const revenues = best(
|
|
13109
|
-
|
|
14127
|
+
fields3,
|
|
13110
14128
|
[/revenue/, /total.*amount|amount.*total/, /grand.*total|total/, /value|amount/],
|
|
13111
14129
|
true
|
|
13112
14130
|
);
|
|
13113
|
-
const orderIds = best(
|
|
13114
|
-
const currencies = best(
|
|
13115
|
-
const items = best(
|
|
14131
|
+
const orderIds = best(fields3, [/order.*id|id.*order/, /transaction.*id/, /(^|\.)id$/]);
|
|
14132
|
+
const currencies = best(fields3, [/currency/, /(^|\.)cur(rency)?$/]);
|
|
14133
|
+
const items = best(fields3, [/(^|\.)((line_?)?items|products)$/]);
|
|
13116
14134
|
if (!revenues.length || !orderIds.length || !currencies.length) continue;
|
|
13117
14135
|
for (const revenue of revenues)
|
|
13118
14136
|
for (const orderId of orderIds)
|
|
@@ -13662,11 +14680,11 @@ async function patchWithRetry(command, build) {
|
|
|
13662
14680
|
warn2(command, result.data.precompile);
|
|
13663
14681
|
return result.data;
|
|
13664
14682
|
}
|
|
13665
|
-
var list5 = new Command("list").alias("ls").description("List install domains").action(async function
|
|
14683
|
+
var list5 = new Command("list").alias("ls").description("List install domains").action(async function action51() {
|
|
13666
14684
|
const data = await load4(this);
|
|
13667
14685
|
output(this, data, () => printDomains(data));
|
|
13668
14686
|
});
|
|
13669
|
-
var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function
|
|
14687
|
+
var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action52(domain) {
|
|
13670
14688
|
const added = domain.map(normalizeDomain);
|
|
13671
14689
|
const data = await patchWithRetry(
|
|
13672
14690
|
this,
|
|
@@ -13674,7 +14692,7 @@ var add = new Command("add").description("Add install domains").argument("<domai
|
|
|
13674
14692
|
);
|
|
13675
14693
|
output(this, data, () => printDomains(data));
|
|
13676
14694
|
});
|
|
13677
|
-
var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function
|
|
14695
|
+
var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action53(domain) {
|
|
13678
14696
|
const removeSet = new Set(domain.map(normalizeDomain));
|
|
13679
14697
|
const data = await patchWithRetry(
|
|
13680
14698
|
this,
|
|
@@ -13682,7 +14700,7 @@ var remove = new Command("remove").description("Remove install domains").argumen
|
|
|
13682
14700
|
);
|
|
13683
14701
|
output(this, data, () => printDomains(data));
|
|
13684
14702
|
});
|
|
13685
|
-
var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function
|
|
14703
|
+
var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action54(domain) {
|
|
13686
14704
|
const replacement = domain.map(normalizeDomain);
|
|
13687
14705
|
const data = await patchWithRetry(this, () => replacement);
|
|
13688
14706
|
output(this, data, () => printDomains(data));
|
|
@@ -13774,14 +14792,14 @@ async function patchWithRetry2(command, build) {
|
|
|
13774
14792
|
);
|
|
13775
14793
|
return result.data;
|
|
13776
14794
|
}
|
|
13777
|
-
var get4 = new Command("get").description("Show cross-domain handover settings").action(async function
|
|
14795
|
+
var get4 = new Command("get").description("Show cross-domain handover settings").action(async function action55() {
|
|
13778
14796
|
const data = await load5(this);
|
|
13779
14797
|
output(this, data, () => printSettings(data.externalTracking));
|
|
13780
14798
|
});
|
|
13781
14799
|
var set4 = new Command("set").description("Enable cross-domain identity handover").requiredOption("--domain <domain...>", "Bound handover domain(s)").option("--param <param>", "Handover query parameter").option(
|
|
13782
14800
|
"--signed",
|
|
13783
14801
|
"Require signed handover tokens (the default; unsigning is a platform-support action)"
|
|
13784
|
-
).action(async function
|
|
14802
|
+
).action(async function action56(opts) {
|
|
13785
14803
|
const domains = opts.domain.map(normalizeDomain2);
|
|
13786
14804
|
if (domains.some((domain) => !domain))
|
|
13787
14805
|
fail(
|
|
@@ -13801,14 +14819,14 @@ var set4 = new Command("set").description("Enable cross-domain identity handover
|
|
|
13801
14819
|
}));
|
|
13802
14820
|
output(this, data, () => printSettings(data.externalTracking));
|
|
13803
14821
|
});
|
|
13804
|
-
var off2 = new Command("off").description("Disable cross-domain handover").action(async function
|
|
14822
|
+
var off2 = new Command("off").description("Disable cross-domain handover").action(async function action57() {
|
|
13805
14823
|
const data = await patchWithRetry2(this, (current) => ({
|
|
13806
14824
|
externalTracking: { domains: [], signed: true },
|
|
13807
14825
|
ifRevision: current.revision
|
|
13808
14826
|
}));
|
|
13809
14827
|
output(this, data, () => printSettings(data.externalTracking));
|
|
13810
14828
|
});
|
|
13811
|
-
var qa2 = new Command("qa").description("Check static link/form handover parameter carriage").requiredOption("--from <url>", "Page URL to inspect").requiredOption("--to-domain <domain>", "Target domain that should carry the handover parameter").action(async function
|
|
14829
|
+
var qa2 = new Command("qa").description("Check static link/form handover parameter carriage").requiredOption("--from <url>", "Page URL to inspect").requiredOption("--to-domain <domain>", "Target domain that should carry the handover parameter").action(async function action58(opts) {
|
|
13812
14830
|
const config = await load5(this);
|
|
13813
14831
|
const target = normalizeDomain2(opts.toDomain);
|
|
13814
14832
|
if (!target) fail(this, "--to-domain must be a valid hostname", EXIT_CODES.validation);
|
|
@@ -14851,7 +15869,7 @@ var doctorCommand = new Command("doctor").description("Check shop configuration
|
|
|
14851
15869
|
).option("--check <id>", "Run one check (for example purchase_capture)").option(
|
|
14852
15870
|
"--worker <url>",
|
|
14853
15871
|
`Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`
|
|
14854
|
-
).action(async function
|
|
15872
|
+
).action(async function action59(options) {
|
|
14855
15873
|
const data = await runDoctor(options);
|
|
14856
15874
|
if (options.check && data.checks.length === 0)
|
|
14857
15875
|
fail(this, `Unknown doctor check: ${options.check}`, EXIT_CODES.validation);
|
|
@@ -14873,7 +15891,7 @@ function printDoctorReport(checks) {
|
|
|
14873
15891
|
var consentRateCommand = new Command("consent-rate").description("Estimate analytics consent acceptance from visitor series").option(
|
|
14874
15892
|
"--since <date>",
|
|
14875
15893
|
"Consent gating start date (YYYY-MM-DD); splits the baseline and gated windows"
|
|
14876
|
-
).action(async function
|
|
15894
|
+
).action(async function action60(opts) {
|
|
14877
15895
|
if (opts.since !== void 0 && parseDay(opts.since) === null)
|
|
14878
15896
|
fail(this, "--since must be a calendar date in YYYY-MM-DD form", EXIT_CODES.validation);
|
|
14879
15897
|
const result = await getVisitorSeries(requireCredentials(), { days: VISITOR_SERIES_DAYS });
|
|
@@ -14933,7 +15951,7 @@ function analyticsSubcommand(name, description, defaultDays) {
|
|
|
14933
15951
|
command.option("--planning-days <n>", "Planning window in days", "180");
|
|
14934
15952
|
command.option("--limit <n>", "Maximum page rows", "50");
|
|
14935
15953
|
}
|
|
14936
|
-
return command.action(async function
|
|
15954
|
+
return command.action(async function action74(opts) {
|
|
14937
15955
|
const result = await getAnalytics(requireCredentials(), name, {
|
|
14938
15956
|
days: positiveInteger(this, opts.days, "--days"),
|
|
14939
15957
|
planningDays: positiveInteger(this, opts.planningDays, "--planning-days"),
|
|
@@ -14953,7 +15971,7 @@ function urlsCommand() {
|
|
|
14953
15971
|
return new Command("urls").description("Show storefront URL inventory from pageview analytics").option("-d, --days <n>", "Analytics window in days", "14").option("--host <host>", "Filter inventory to one host").option("--contains <substring>", "Server-side path substring filter").option(
|
|
14954
15972
|
"--match <regex>",
|
|
14955
15973
|
"Client-side regex filter; exhausts inventory pages before filtering"
|
|
14956
|
-
).option("--limit <n>", "Maximum rows to print", "50").action(async function
|
|
15974
|
+
).option("--limit <n>", "Maximum rows to print", "50").action(async function action74(opts) {
|
|
14957
15975
|
const creds = requireCredentials();
|
|
14958
15976
|
const days = positiveInteger(this, opts.days, "--days") ?? 14;
|
|
14959
15977
|
const displayLimit = Math.min(200, positiveInteger(this, opts.limit, "--limit") ?? 50);
|
|
@@ -15147,10 +16165,10 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
|
|
|
15147
16165
|
const chunks = [];
|
|
15148
16166
|
let bytes = 0;
|
|
15149
16167
|
let settled = false;
|
|
15150
|
-
const settle = (
|
|
16168
|
+
const settle = (action74) => {
|
|
15151
16169
|
if (settled) return;
|
|
15152
16170
|
settled = true;
|
|
15153
|
-
|
|
16171
|
+
action74();
|
|
15154
16172
|
};
|
|
15155
16173
|
stream.on("data", (chunk) => {
|
|
15156
16174
|
if (settled) return;
|
|
@@ -15221,11 +16239,11 @@ function pinnedFetch(url, addresses, limits) {
|
|
|
15221
16239
|
else callback(null, addresses[0], (0, import_node_net.isIP)(addresses[0]));
|
|
15222
16240
|
};
|
|
15223
16241
|
let settled = false;
|
|
15224
|
-
const settle = (
|
|
16242
|
+
const settle = (action74) => {
|
|
15225
16243
|
if (settled) return;
|
|
15226
16244
|
settled = true;
|
|
15227
16245
|
clearTimeout(deadline);
|
|
15228
|
-
|
|
16246
|
+
action74();
|
|
15229
16247
|
};
|
|
15230
16248
|
const req = send3(
|
|
15231
16249
|
url,
|
|
@@ -15560,7 +16578,7 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
|
|
|
15560
16578
|
var installCommand = new Command("install").description("Check Apex storefront installation").command("check").description("Verify snippet, served config, and recent tracking for the current shop").option("--url <url>", "Check one explicit bound storefront URL").option("--shop-id <id>", "Apex shop ID to verify (falls back to DRIP_SHOP_ID)").option(
|
|
15561
16579
|
"--worker-url <url>",
|
|
15562
16580
|
"Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)"
|
|
15563
|
-
).option("--environment <key>", "Environment key to request from the worker", "production").action(async function
|
|
16581
|
+
).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action61(opts) {
|
|
15564
16582
|
const creds = requireCredentials();
|
|
15565
16583
|
const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
|
|
15566
16584
|
if (!shopId)
|
|
@@ -15860,11 +16878,11 @@ function write(name) {
|
|
|
15860
16878
|
});
|
|
15861
16879
|
return c;
|
|
15862
16880
|
}
|
|
15863
|
-
function lifecycle(
|
|
15864
|
-
return new Command(
|
|
15865
|
-
const r = await transitionManagedDelivery(requireCredentials(), kind, id,
|
|
16881
|
+
function lifecycle(action74) {
|
|
16882
|
+
return new Command(action74).description(`${action74[0].toUpperCase()}${action74.slice(1)} a flag`).argument("<id>").action(async function(id) {
|
|
16883
|
+
const r = await transitionManagedDelivery(requireCredentials(), kind, id, action74);
|
|
15866
16884
|
if (!r.ok || !r.data)
|
|
15867
|
-
fail(this, r.error ?? `Failed to ${
|
|
16885
|
+
fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
|
|
15868
16886
|
output(this, { flag: r.data }, () => detail(r.data));
|
|
15869
16887
|
});
|
|
15870
16888
|
}
|
|
@@ -15902,13 +16920,74 @@ function parse(command, value) {
|
|
|
15902
16920
|
);
|
|
15903
16921
|
}
|
|
15904
16922
|
}
|
|
15905
|
-
function
|
|
16923
|
+
function collect2(value, previous) {
|
|
16924
|
+
return [...previous, value];
|
|
16925
|
+
}
|
|
16926
|
+
function customFieldsRecord(value) {
|
|
16927
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
16928
|
+
}
|
|
16929
|
+
function typedFieldValue(definition, raw) {
|
|
16930
|
+
if (raw === "null") return null;
|
|
16931
|
+
if (raw.trim() === "") return null;
|
|
16932
|
+
switch (definition?.type) {
|
|
16933
|
+
case "number": {
|
|
16934
|
+
const parsed = Number(raw.trim());
|
|
16935
|
+
return Number.isFinite(parsed) ? parsed : raw;
|
|
16936
|
+
}
|
|
16937
|
+
case "boolean": {
|
|
16938
|
+
const trimmed = raw.trim().toLowerCase();
|
|
16939
|
+
if (["true", "yes", "y", "1"].includes(trimmed)) return true;
|
|
16940
|
+
if (["false", "no", "n", "0"].includes(trimmed)) return false;
|
|
16941
|
+
return raw;
|
|
16942
|
+
}
|
|
16943
|
+
default:
|
|
16944
|
+
return raw;
|
|
16945
|
+
}
|
|
16946
|
+
}
|
|
16947
|
+
async function customFieldsFromPairs(command, pairs) {
|
|
16948
|
+
if (pairs.length === 0) return void 0;
|
|
16949
|
+
const parsedPairs = pairs.map((pair) => {
|
|
16950
|
+
const separator = pair.indexOf("=");
|
|
16951
|
+
if (separator <= 0)
|
|
16952
|
+
fail(command, `Invalid --field "${pair}": use key=value.`, EXIT_CODES.validation);
|
|
16953
|
+
return { key: pair.slice(0, separator), raw: pair.slice(separator + 1) };
|
|
16954
|
+
});
|
|
16955
|
+
const fields3 = await listBacklogFields(requireCredentials());
|
|
16956
|
+
if (!fields3.ok || !fields3.data) {
|
|
16957
|
+
process.stderr.write(
|
|
16958
|
+
`Could not read the shop's field definitions (${fields3.error ?? `HTTP ${fields3.status}`}). Sending every --field value as text, so a number or yes/no field will answer 422. Add the backlog:read scope to this key to type them.
|
|
16959
|
+
`
|
|
16960
|
+
);
|
|
16961
|
+
}
|
|
16962
|
+
const byKey = new Map(
|
|
16963
|
+
(fields3.ok && fields3.data ? fields3.data.data : []).map((field) => [field.key, field])
|
|
16964
|
+
);
|
|
16965
|
+
const values = {};
|
|
16966
|
+
for (const { key, raw } of parsedPairs) values[key] = typedFieldValue(byKey.get(key), raw);
|
|
16967
|
+
return values;
|
|
16968
|
+
}
|
|
16969
|
+
function detail2(x, labels) {
|
|
15906
16970
|
console.log();
|
|
15907
16971
|
console.log(source_default.bold(String(x.title ?? x.label ?? x.key ?? x.id)));
|
|
15908
16972
|
console.log(` ID: ${source_default.dim(x.id)}`);
|
|
15909
16973
|
if (x.status) console.log(` State: ${x.status}`);
|
|
16974
|
+
const custom = customFieldsRecord(x.custom_fields);
|
|
16975
|
+
if (Object.keys(custom).length > 0) {
|
|
16976
|
+
console.log();
|
|
16977
|
+
for (const [key, value] of Object.entries(custom)) {
|
|
16978
|
+
console.log(` ${labels.get(key) ?? key}: ${value === null ? "-" : String(value)}`);
|
|
16979
|
+
}
|
|
16980
|
+
}
|
|
15910
16981
|
console.log();
|
|
15911
16982
|
}
|
|
16983
|
+
async function fieldLabels(command, item) {
|
|
16984
|
+
if (shouldUseJson(command)) return /* @__PURE__ */ new Map();
|
|
16985
|
+
if (Object.keys(customFieldsRecord(item.custom_fields)).length === 0) return /* @__PURE__ */ new Map();
|
|
16986
|
+
const fields3 = await listBacklogFields(requireCredentials());
|
|
16987
|
+
return new Map(
|
|
16988
|
+
(fields3.ok && fields3.data ? fields3.data.data : []).map((field) => [field.key, field.label])
|
|
16989
|
+
);
|
|
16990
|
+
}
|
|
15912
16991
|
var list7 = new Command("list").alias("ls").description("List backlog").option("-n, --limit <n>", "Max results", "50").option("--cursor <cursor>", "Continue from cursor").option("--include-archived", "Include archived backlog items").action(async function(o) {
|
|
15913
16992
|
const r = await listInsights(
|
|
15914
16993
|
requireCredentials(),
|
|
@@ -15937,27 +17016,35 @@ var get6 = new Command("get").description("Get a backlog item").argument("<id>",
|
|
|
15937
17016
|
const r = await getInsight(requireCredentials(), kind2, id);
|
|
15938
17017
|
if (!r.ok || !r.data)
|
|
15939
17018
|
fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
|
|
15940
|
-
|
|
17019
|
+
const labels = await fieldLabels(this, r.data);
|
|
17020
|
+
output(this, { backlog_item: r.data }, () => detail2(r.data, labels));
|
|
15941
17021
|
});
|
|
15942
|
-
function write2(
|
|
15943
|
-
const c = new Command(
|
|
15944
|
-
`${
|
|
17022
|
+
function write2(action74) {
|
|
17023
|
+
const c = new Command(action74).description(
|
|
17024
|
+
`${action74 === "create" ? "Create" : "Update"} a ${singular}`
|
|
15945
17025
|
);
|
|
15946
|
-
if (
|
|
15947
|
-
c.argument("[json]", "Inline JSON, @file, or file path").option(
|
|
15948
|
-
"--
|
|
15949
|
-
"
|
|
17026
|
+
if (action74 === "update") c.argument("<id>", `${singular} ID`);
|
|
17027
|
+
c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").option(
|
|
17028
|
+
"--field <key=value>",
|
|
17029
|
+
"Set one of the shop's own fields; repeatable. Use key=null to clear it.",
|
|
17030
|
+
collect2,
|
|
17031
|
+
[]
|
|
15950
17032
|
);
|
|
15951
17033
|
c.action(async function(...args) {
|
|
15952
17034
|
const o = this.opts();
|
|
15953
|
-
const id =
|
|
15954
|
-
const raw = o.file ? `@${o.file}` : String(args[
|
|
17035
|
+
const id = action74 === "update" ? String(args[0]) : void 0;
|
|
17036
|
+
const raw = o.file ? `@${o.file}` : String(args[action74 === "update" ? 1 : 0] ?? "") || void 0;
|
|
15955
17037
|
const body5 = parse(this, raw);
|
|
17038
|
+
const customFields = await customFieldsFromPairs(this, o.field);
|
|
17039
|
+
if (customFields) {
|
|
17040
|
+
body5.custom_fields = { ...customFieldsRecord(body5.custom_fields), ...customFields };
|
|
17041
|
+
}
|
|
15956
17042
|
if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
|
|
15957
|
-
const r =
|
|
17043
|
+
const r = action74 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
|
|
15958
17044
|
if (!r.ok || !r.data)
|
|
15959
|
-
fail(this, r.error ?? `Failed to ${
|
|
15960
|
-
|
|
17045
|
+
fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
|
|
17046
|
+
const labels = await fieldLabels(this, r.data);
|
|
17047
|
+
output(this, { backlog_item: r.data }, () => detail2(r.data, labels));
|
|
15961
17048
|
});
|
|
15962
17049
|
return c;
|
|
15963
17050
|
}
|
|
@@ -16002,7 +17089,99 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
|
|
|
16002
17089
|
fail(this, r.error ?? "Failed to delete", classifyStatus(r.status), r.errorDetails);
|
|
16003
17090
|
output(this, { deleted: true, id }, () => console.log(`Deleted ${id}`));
|
|
16004
17091
|
});
|
|
16005
|
-
|
|
17092
|
+
function fieldDetail(field) {
|
|
17093
|
+
console.log();
|
|
17094
|
+
console.log(source_default.bold(field.label));
|
|
17095
|
+
console.log(` ID: ${source_default.dim(field.id)}`);
|
|
17096
|
+
console.log(` Key: ${field.key}`);
|
|
17097
|
+
console.log(` Type: ${field.type}`);
|
|
17098
|
+
if (field.options.length > 0) console.log(` Choices: ${field.options.join(", ")}`);
|
|
17099
|
+
console.log(` Required: ${field.required ? "yes" : "no"}`);
|
|
17100
|
+
console.log(` Visible to clients: ${field.client_visible ? "yes" : "no"}`);
|
|
17101
|
+
if (field.archived_at) console.log(` Archived: ${field.archived_at}`);
|
|
17102
|
+
console.log();
|
|
17103
|
+
}
|
|
17104
|
+
function boolOption(command, flag, value) {
|
|
17105
|
+
if (value === void 0) return void 0;
|
|
17106
|
+
const trimmed = value.trim().toLowerCase();
|
|
17107
|
+
if (["true", "yes", "y", "1"].includes(trimmed)) return true;
|
|
17108
|
+
if (["false", "no", "n", "0"].includes(trimmed)) return false;
|
|
17109
|
+
fail(command, `Invalid ${flag} "${value}": use true or false.`, EXIT_CODES.validation);
|
|
17110
|
+
}
|
|
17111
|
+
var fieldsList = new Command("list").alias("ls").description("List the shop's idea fields").option("--include-archived", "Include archived fields").action(async function(o) {
|
|
17112
|
+
const r = await listBacklogFields(requireCredentials(), o.includeArchived);
|
|
17113
|
+
if (!r.ok || !r.data)
|
|
17114
|
+
fail(this, r.error ?? "Failed to list fields", classifyStatus(r.status), r.errorDetails);
|
|
17115
|
+
output(
|
|
17116
|
+
this,
|
|
17117
|
+
{ fields: r.data.data },
|
|
17118
|
+
() => printTable(
|
|
17119
|
+
["KEY", "LABEL", "TYPE", "REQUIRED", "CLIENT"],
|
|
17120
|
+
r.data.data.map((field) => [
|
|
17121
|
+
field.key,
|
|
17122
|
+
field.label,
|
|
17123
|
+
field.type,
|
|
17124
|
+
field.required ? "yes" : "no",
|
|
17125
|
+
field.client_visible ? "yes" : "no"
|
|
17126
|
+
]),
|
|
17127
|
+
[24, 28, 14, 10, 8]
|
|
17128
|
+
)
|
|
17129
|
+
);
|
|
17130
|
+
});
|
|
17131
|
+
var fieldsCreate = new Command("create").description("Add an idea field").requiredOption("--label <text>", "What the field is called").requiredOption(
|
|
17132
|
+
"--type <type>",
|
|
17133
|
+
"short_text, long_text, number, boolean, single_choice, date, url or person"
|
|
17134
|
+
).option("--key <key>", "Field key; derived from the label when omitted").option(
|
|
17135
|
+
"--option <value>",
|
|
17136
|
+
"A choice for a single_choice field; repeatable",
|
|
17137
|
+
collect2,
|
|
17138
|
+
[]
|
|
17139
|
+
).option("--required <true|false>", "Every idea must carry a value").option("--client-visible <true|false>", "Share the value with the client").action(async function(o) {
|
|
17140
|
+
const required = boolOption(this, "--required", o.required);
|
|
17141
|
+
const clientVisible = boolOption(this, "--client-visible", o.clientVisible);
|
|
17142
|
+
const r = await createBacklogField(requireCredentials(), {
|
|
17143
|
+
label: o.label,
|
|
17144
|
+
type: o.type,
|
|
17145
|
+
...o.key ? { key: o.key } : {},
|
|
17146
|
+
...o.option.length > 0 ? { options: o.option } : {},
|
|
17147
|
+
...required === void 0 ? {} : { required },
|
|
17148
|
+
...clientVisible === void 0 ? {} : { client_visible: clientVisible }
|
|
17149
|
+
});
|
|
17150
|
+
if (!r.ok || !r.data)
|
|
17151
|
+
fail(this, r.error ?? "Failed to create field", classifyStatus(r.status), r.errorDetails);
|
|
17152
|
+
output(this, { field: r.data }, () => fieldDetail(r.data));
|
|
17153
|
+
});
|
|
17154
|
+
var fieldsUpdate = new Command("update").description("Update an idea field").argument("<id>", "field ID").option("--label <text>", "What the field is called").option(
|
|
17155
|
+
"--option <value>",
|
|
17156
|
+
"Replace the choices of a single_choice field; repeatable",
|
|
17157
|
+
collect2,
|
|
17158
|
+
[]
|
|
17159
|
+
).option("--required <true|false>", "Every idea must carry a value").option("--client-visible <true|false>", "Share the value with the client").option("--archived <true|false>", "Archive or restore the field").action(async function(id, o) {
|
|
17160
|
+
const required = boolOption(this, "--required", o.required);
|
|
17161
|
+
const clientVisible = boolOption(this, "--client-visible", o.clientVisible);
|
|
17162
|
+
const archived = boolOption(this, "--archived", o.archived);
|
|
17163
|
+
const body5 = {
|
|
17164
|
+
...o.label ? { label: o.label } : {},
|
|
17165
|
+
...o.option.length > 0 ? { options: o.option } : {},
|
|
17166
|
+
...required === void 0 ? {} : { required },
|
|
17167
|
+
...clientVisible === void 0 ? {} : { client_visible: clientVisible },
|
|
17168
|
+
...archived === void 0 ? {} : { archived }
|
|
17169
|
+
};
|
|
17170
|
+
if (!Object.keys(body5).length)
|
|
17171
|
+
fail(this, "Provide at least one field option to change.", EXIT_CODES.validation);
|
|
17172
|
+
const r = await updateBacklogField(requireCredentials(), id, body5);
|
|
17173
|
+
if (!r.ok || !r.data)
|
|
17174
|
+
fail(this, r.error ?? "Failed to update field", classifyStatus(r.status), r.errorDetails);
|
|
17175
|
+
output(this, { field: r.data }, () => fieldDetail(r.data));
|
|
17176
|
+
});
|
|
17177
|
+
var fieldsArchive = new Command("archive").description("Archive an idea field; values already on ideas are kept").argument("<id>", "field ID").action(async function(id) {
|
|
17178
|
+
const r = await archiveBacklogField(requireCredentials(), id);
|
|
17179
|
+
if (!r.ok || !r.data)
|
|
17180
|
+
fail(this, r.error ?? "Failed to archive field", classifyStatus(r.status), r.errorDetails);
|
|
17181
|
+
output(this, { field: r.data }, () => fieldDetail(r.data));
|
|
17182
|
+
});
|
|
17183
|
+
var fields2 = new Command("fields").description("Manage the shop's own idea fields").addCommand(fieldsList).addCommand(fieldsCreate).addCommand(fieldsUpdate).addCommand(fieldsArchive);
|
|
17184
|
+
var backlogCommand = new Command("backlog").description("Manage backlog").addCommand(list7).addCommand(get6).addCommand(write2("create")).addCommand(write2("update")).addCommand(archive).addCommand(restore).addCommand(remove2).addCommand(fields2);
|
|
16006
17185
|
|
|
16007
17186
|
// src/commands/signals.ts
|
|
16008
17187
|
var import_node_fs19 = __toESM(require("node:fs"));
|
|
@@ -16064,24 +17243,24 @@ var get7 = new Command("get").description("Get a signal").argument("<id>", "sign
|
|
|
16064
17243
|
fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
|
|
16065
17244
|
output(this, { signal: r.data }, () => detail3(r.data));
|
|
16066
17245
|
});
|
|
16067
|
-
function write3(
|
|
16068
|
-
const c = new Command(
|
|
16069
|
-
`${
|
|
17246
|
+
function write3(action74) {
|
|
17247
|
+
const c = new Command(action74).description(
|
|
17248
|
+
`${action74 === "create" ? "Create" : "Update"} a ${singular2}`
|
|
16070
17249
|
);
|
|
16071
|
-
if (
|
|
17250
|
+
if (action74 === "update") c.argument("<id>", `${singular2} ID`);
|
|
16072
17251
|
c.argument("[json]", "Inline JSON, @file, or file path").option(
|
|
16073
17252
|
"--file <path>",
|
|
16074
17253
|
"JSON body file"
|
|
16075
17254
|
);
|
|
16076
17255
|
c.action(async function(...args) {
|
|
16077
17256
|
const o = this.opts();
|
|
16078
|
-
const id =
|
|
16079
|
-
const raw = o.file ? `@${o.file}` : String(args[
|
|
17257
|
+
const id = action74 === "update" ? String(args[0]) : void 0;
|
|
17258
|
+
const raw = o.file ? `@${o.file}` : String(args[action74 === "update" ? 1 : 0] ?? "") || void 0;
|
|
16080
17259
|
const body5 = parse2(this, raw);
|
|
16081
17260
|
if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
|
|
16082
|
-
const r =
|
|
17261
|
+
const r = action74 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
|
|
16083
17262
|
if (!r.ok || !r.data)
|
|
16084
|
-
fail(this, r.error ?? `Failed to ${
|
|
17263
|
+
fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
|
|
16085
17264
|
output(this, { signal: r.data }, () => detail3(r.data));
|
|
16086
17265
|
});
|
|
16087
17266
|
return c;
|
|
@@ -16173,11 +17352,11 @@ function write4(name) {
|
|
|
16173
17352
|
});
|
|
16174
17353
|
return c;
|
|
16175
17354
|
}
|
|
16176
|
-
function lifecycle2(
|
|
16177
|
-
return new Command(
|
|
16178
|
-
const r = await transitionManagedDelivery(requireCredentials(), kind4, id,
|
|
17355
|
+
function lifecycle2(action74) {
|
|
17356
|
+
return new Command(action74).description(`${action74[0].toUpperCase()}${action74.slice(1)} a personalization`).argument("<id>").action(async function(id) {
|
|
17357
|
+
const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action74);
|
|
16179
17358
|
if (!r.ok || !r.data)
|
|
16180
|
-
fail(this, r.error ?? `Failed to ${
|
|
17359
|
+
fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
|
|
16181
17360
|
output(this, { personalization: r.data }, () => detail4(r.data));
|
|
16182
17361
|
});
|
|
16183
17362
|
}
|
|
@@ -16259,21 +17438,21 @@ function targetingResourceCommand(kind5) {
|
|
|
16259
17438
|
);
|
|
16260
17439
|
})
|
|
16261
17440
|
);
|
|
16262
|
-
for (const
|
|
16263
|
-
const command = new Command(
|
|
16264
|
-
`${
|
|
17441
|
+
for (const action74 of ["create", "update"]) {
|
|
17442
|
+
const command = new Command(action74).description(
|
|
17443
|
+
`${action74 === "create" ? "Create" : "Update"} a ${singular3}`
|
|
16265
17444
|
);
|
|
16266
|
-
if (
|
|
17445
|
+
if (action74 === "update") command.argument("<id>");
|
|
16267
17446
|
command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
|
|
16268
17447
|
const opts = this.opts();
|
|
16269
|
-
const id =
|
|
16270
|
-
const value = opts.file ? `@${opts.file}` : String(args[
|
|
17448
|
+
const id = action74 === "update" ? String(args[0]) : void 0;
|
|
17449
|
+
const value = opts.file ? `@${opts.file}` : String(args[action74 === "update" ? 1 : 0] ?? "") || void 0;
|
|
16271
17450
|
const requestBody = body3(this, value);
|
|
16272
|
-
if (
|
|
17451
|
+
if (action74 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
|
|
16273
17452
|
fail(this, `Updating a ${singular3} requires at least one field.`, EXIT_CODES.validation);
|
|
16274
17453
|
const response = await requestTargetingResource(
|
|
16275
17454
|
requireCredentials(),
|
|
16276
|
-
|
|
17455
|
+
action74 === "create" ? "POST" : "PATCH",
|
|
16277
17456
|
kind5,
|
|
16278
17457
|
id,
|
|
16279
17458
|
requestBody
|
|
@@ -16281,7 +17460,7 @@ function targetingResourceCommand(kind5) {
|
|
|
16281
17460
|
if (!response.ok || !response.data)
|
|
16282
17461
|
fail(
|
|
16283
17462
|
this,
|
|
16284
|
-
response.error ?? `Failed to ${
|
|
17463
|
+
response.error ?? `Failed to ${action74}`,
|
|
16285
17464
|
classifyStatus(response.status),
|
|
16286
17465
|
response.errorDetails
|
|
16287
17466
|
);
|
|
@@ -16290,7 +17469,7 @@ function targetingResourceCommand(kind5) {
|
|
|
16290
17469
|
{
|
|
16291
17470
|
[kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
|
|
16292
17471
|
},
|
|
16293
|
-
() => console.log(source_default.green(`${
|
|
17472
|
+
() => console.log(source_default.green(`${action74}d ${singular3}`))
|
|
16294
17473
|
);
|
|
16295
17474
|
});
|
|
16296
17475
|
root.addCommand(command);
|
|
@@ -16364,7 +17543,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
|
|
|
16364
17543
|
// src/commands/brand.ts
|
|
16365
17544
|
init_source();
|
|
16366
17545
|
var NO_PROFILE_MESSAGE = "No brand profile yet \u2014 run onboarding or a site check";
|
|
16367
|
-
var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function
|
|
17546
|
+
var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action62() {
|
|
16368
17547
|
const creds = requireCredentials();
|
|
16369
17548
|
const res = await getBrandContext(creds);
|
|
16370
17549
|
if (res.status === 404) {
|
|
@@ -16397,7 +17576,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
|
|
|
16397
17576
|
|
|
16398
17577
|
// src/commands/targeting.ts
|
|
16399
17578
|
init_source();
|
|
16400
|
-
var test2 = new Command("test").description("Evaluate experiment URL targeting for a page").argument("<url>", "URL to evaluate").option("--status <status>", "Comma-separated statuses to include, or all", "all").action(async function
|
|
17579
|
+
var test2 = new Command("test").description("Evaluate experiment URL targeting for a page").argument("<url>", "URL to evaluate").option("--status <status>", "Comma-separated statuses to include, or all", "all").action(async function action63(url, opts) {
|
|
16401
17580
|
const creds = requireCredentials();
|
|
16402
17581
|
const res = await testTargeting(creds, url, { status: opts.status });
|
|
16403
17582
|
if (!res.ok || !res.data) {
|
|
@@ -16482,7 +17661,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
|
|
|
16482
17661
|
"--type <type>",
|
|
16483
17662
|
"full_handover|design|development|qa|config|bug|help (internal types require a DRIP account)",
|
|
16484
17663
|
"qa"
|
|
16485
|
-
).option("--loom <url>", "Loom URL with reproduction or QA context").option("--experiment <id>", "Experiment ID").option("--sub-class <text>", "SOS sub-classification").option("--description <text-or-@file>", "Description text or @path").argument("[description]", "Description text").action(async function
|
|
17664
|
+
).option("--loom <url>", "Loom URL with reproduction or QA context").option("--experiment <id>", "Experiment ID").option("--sub-class <text>", "SOS sub-classification").option("--description <text-or-@file>", "Description text or @path").argument("[description]", "Description text").action(async function action64(descriptionArg, opts) {
|
|
16486
17665
|
if (!SOS_TYPES.has(opts.type)) {
|
|
16487
17666
|
fail(
|
|
16488
17667
|
this,
|
|
@@ -16509,7 +17688,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
|
|
|
16509
17688
|
}
|
|
16510
17689
|
output(this, res.data, () => printCreatedTicket(res.data));
|
|
16511
17690
|
});
|
|
16512
|
-
var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function
|
|
17691
|
+
var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action65() {
|
|
16513
17692
|
const creds = requireCredentials();
|
|
16514
17693
|
const res = await listMySos(creds);
|
|
16515
17694
|
if (!res.ok || !res.data) {
|
|
@@ -16658,7 +17837,7 @@ var create5 = new Command("create").description("Create a shop").argument("[json
|
|
|
16658
17837
|
const r = await createShop(requireUserCredentials(), payload);
|
|
16659
17838
|
done(this, r, "create shop");
|
|
16660
17839
|
});
|
|
16661
|
-
var
|
|
17840
|
+
var update4 = new Command("update").description("Update a shop").argument("<id>").argument("[json]", "Inline JSON, @file, or file path").option("--holdout-pct <percent>", "Platform-operator global holdout percent (0-10)").option("--holdout-enabled", "Enable the platform-operator global holdout v1 setting").option("--holdout-disabled", "Disable the platform-operator global holdout v1 setting").option("--confirm-tier", "Confirm a commercially sensitive tier change").action(async function(id, raw, o) {
|
|
16662
17841
|
const b = raw === void 0 ? {} : body4(this, raw);
|
|
16663
17842
|
if (o.holdoutEnabled && o.holdoutDisabled)
|
|
16664
17843
|
fail(this, "Use only one of --holdout-enabled or --holdout-disabled", EXIT_CODES.validation);
|
|
@@ -16791,7 +17970,7 @@ shopware.addCommand(
|
|
|
16791
17970
|
);
|
|
16792
17971
|
})
|
|
16793
17972
|
);
|
|
16794
|
-
var shopsCommand = new Command("shops").description("Manage organization shops").addCommand(list12).addCommand(get9).addCommand(create5).addCommand(
|
|
17973
|
+
var shopsCommand = new Command("shops").description("Manage organization shops").addCommand(list12).addCommand(get9).addCommand(create5).addCommand(update4).addCommand(doctorCommand).addCommand(
|
|
16795
17974
|
settings("auto-stop", "Read or update auto-stop defaults", { on: "enable", off: "disable" })
|
|
16796
17975
|
).addCommand(settings("testing-defaults", "Read or update testing defaults")).addCommand(settings("guardian", "Read or update guardian defaults", { on: "on", off: "off" })).addCommand(profile).addCommand(shopware);
|
|
16797
17976
|
|
|
@@ -16799,6 +17978,7 @@ var shopsCommand = new Command("shops").description("Manage organization shops")
|
|
|
16799
17978
|
function optionSchema(option) {
|
|
16800
17979
|
return {
|
|
16801
17980
|
flags: option.flags,
|
|
17981
|
+
...option.argChoices ? { choices: option.argChoices } : {},
|
|
16802
17982
|
description: option.description,
|
|
16803
17983
|
required: option.required,
|
|
16804
17984
|
optional: option.optional,
|
|
@@ -16824,7 +18004,7 @@ function commandSchema(command) {
|
|
|
16824
18004
|
};
|
|
16825
18005
|
}
|
|
16826
18006
|
function createSchemaCommand(program3) {
|
|
16827
|
-
return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function
|
|
18007
|
+
return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action74() {
|
|
16828
18008
|
const schema = commandSchema(program3);
|
|
16829
18009
|
output(this, schema, () => {
|
|
16830
18010
|
process.stdout.write(`${JSON.stringify(schema, null, 2)}
|
|
@@ -17239,7 +18419,7 @@ ${event.event || "(unnamed event)"} [${event.dlName}] \u2014 ${event.count} obse
|
|
|
17239
18419
|
return `${lines.join("\n")}
|
|
17240
18420
|
`;
|
|
17241
18421
|
}
|
|
17242
|
-
var discoverCommand = new Command("discover").description("Read back the tracking schema observed for the current shop").option("--minutes <n>", "Observation window in minutes").option("--gtm <url-or-id>", "Mine a public page or GTM container locally").option("--gtm-url <url>", "Fetch an explicit GTM container URL, including first-party proxies").option("--create-dimension <path>", "Create a custom dimension from an observed dataLayer path").option("--event <name>", "Select the observed event when a path appears on multiple events").option("--key <signal-key>", "Override the dimension signal key").option("--label <text>", "Override the dimension label").action(async function
|
|
18422
|
+
var discoverCommand = new Command("discover").description("Read back the tracking schema observed for the current shop").option("--minutes <n>", "Observation window in minutes").option("--gtm <url-or-id>", "Mine a public page or GTM container locally").option("--gtm-url <url>", "Fetch an explicit GTM container URL, including first-party proxies").option("--create-dimension <path>", "Create a custom dimension from an observed dataLayer path").option("--event <name>", "Select the observed event when a path appears on multiple events").option("--key <signal-key>", "Override the dimension signal key").option("--label <text>", "Override the dimension label").action(async function action66(opts) {
|
|
17243
18423
|
if (opts.gtm && opts.gtmUrl)
|
|
17244
18424
|
fail(this, "Use only one of --gtm or --gtm-url", EXIT_CODES.validation);
|
|
17245
18425
|
if (opts.createDimension && (opts.gtm || opts.gtmUrl)) {
|
|
@@ -17442,7 +18622,7 @@ function installSkill(paths, targetRoot) {
|
|
|
17442
18622
|
import_node_fs24.default.copyFileSync(paths.agents, import_node_path22.default.join(target, "AGENTS.md"));
|
|
17443
18623
|
return target;
|
|
17444
18624
|
}
|
|
17445
|
-
var skillsCommand = new Command("skills").description("Print or install the Apex CLI agent skill files").option("--install", "Copy the Apex CLI skill into ./.claude/skills/apex-cli").option("--target <dir>", "Install target skills directory", ".claude/skills").option("--json", "Print JSON").action(function
|
|
18625
|
+
var skillsCommand = new Command("skills").description("Print or install the Apex CLI agent skill files").option("--install", "Copy the Apex CLI skill into ./.claude/skills/apex-cli").option("--target <dir>", "Install target skills directory", ".claude/skills").option("--json", "Print JSON").action(function action67(opts) {
|
|
17446
18626
|
let paths;
|
|
17447
18627
|
try {
|
|
17448
18628
|
paths = skillPaths();
|
|
@@ -18066,7 +19246,7 @@ function selectedEvents(opts) {
|
|
|
18066
19246
|
}
|
|
18067
19247
|
var list14 = new Command("list").alias("ls").description(
|
|
18068
19248
|
"List webhooks. JSON includes availableEvents; secrets are never returned after create."
|
|
18069
|
-
).action(async function
|
|
19249
|
+
).action(async function action68() {
|
|
18070
19250
|
const data = await loadWebhookIndex(this);
|
|
18071
19251
|
output(this, data, () => printWebhooks(data.data));
|
|
18072
19252
|
});
|
|
@@ -18075,7 +19255,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
|
|
|
18075
19255
|
"Webhook event to subscribe to; repeat for multiple events",
|
|
18076
19256
|
(value, previous = []) => [...previous, value],
|
|
18077
19257
|
[]
|
|
18078
|
-
).action(async function
|
|
19258
|
+
).action(async function action69(opts) {
|
|
18079
19259
|
const events = selectedEvents(opts);
|
|
18080
19260
|
if (events.length === 0) fail(this, "At least one --event is required", EXIT_CODES.validation);
|
|
18081
19261
|
const index = await loadWebhookIndex(this);
|
|
@@ -18107,7 +19287,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
|
|
|
18107
19287
|
}
|
|
18108
19288
|
);
|
|
18109
19289
|
});
|
|
18110
|
-
var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function
|
|
19290
|
+
var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action70(id) {
|
|
18111
19291
|
requireShopSecretKey(this);
|
|
18112
19292
|
const result = handleResult(
|
|
18113
19293
|
this,
|
|
@@ -18116,7 +19296,7 @@ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").
|
|
|
18116
19296
|
);
|
|
18117
19297
|
output(this, result, () => console.log(source_default.green(`Deleted webhook ${id}`)));
|
|
18118
19298
|
});
|
|
18119
|
-
var deliveries = new Command("deliveries").description("List webhook deliveries including failure and disabled state").argument("<id>", "Webhook id").option("--limit <n>", "Maximum deliveries to show", "20").action(async function
|
|
19299
|
+
var deliveries = new Command("deliveries").description("List webhook deliveries including failure and disabled state").argument("<id>", "Webhook id").option("--limit <n>", "Maximum deliveries to show", "20").action(async function action71(id, opts) {
|
|
18120
19300
|
const limit = Number.parseInt(opts.limit, 10);
|
|
18121
19301
|
if (!Number.isInteger(limit) || limit < 1)
|
|
18122
19302
|
fail(this, "--limit must be a positive integer", EXIT_CODES.validation);
|
|
@@ -18128,12 +19308,12 @@ var deliveries = new Command("deliveries").description("List webhook deliveries
|
|
|
18128
19308
|
);
|
|
18129
19309
|
output(this, result, () => printDeliveries(result.data, result.webhook));
|
|
18130
19310
|
});
|
|
18131
|
-
var
|
|
19311
|
+
var update5 = new Command("update").description("Update a webhook URL, events, or active state").argument("<id>", "Webhook id").option("--url <url>", "HTTPS callback URL").option(
|
|
18132
19312
|
"--event <event>",
|
|
18133
19313
|
"Replacement event; repeat for multiple events",
|
|
18134
19314
|
(value, previous = []) => [...previous, value],
|
|
18135
19315
|
[]
|
|
18136
|
-
).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function
|
|
19316
|
+
).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action72(id, opts) {
|
|
18137
19317
|
if (opts.active && opts.inactive)
|
|
18138
19318
|
fail(this, "Use only one of --active or --inactive", EXIT_CODES.validation);
|
|
18139
19319
|
const events = selectedEvents(opts);
|
|
@@ -18166,7 +19346,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
|
|
|
18166
19346
|
);
|
|
18167
19347
|
output(this, { webhook: result }, () => printWebhooks([result]));
|
|
18168
19348
|
});
|
|
18169
|
-
var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function
|
|
19349
|
+
var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action73(id) {
|
|
18170
19350
|
requireShopSecretKey(this);
|
|
18171
19351
|
const result = handleResult(
|
|
18172
19352
|
this,
|
|
@@ -18181,7 +19361,7 @@ var test3 = new Command("test").description("Send a signed test event to a webho
|
|
|
18181
19361
|
)
|
|
18182
19362
|
);
|
|
18183
19363
|
});
|
|
18184
|
-
var webhooksCommand = new Command("webhooks").description("Manage shop webhooks").addCommand(list14).addCommand(create7).addCommand(
|
|
19364
|
+
var webhooksCommand = new Command("webhooks").description("Manage shop webhooks").addCommand(list14).addCommand(create7).addCommand(update5).addCommand(remove4).addCommand(deliveries).addCommand(test3);
|
|
18185
19365
|
|
|
18186
19366
|
// src/commands/simulate.ts
|
|
18187
19367
|
init_source();
|