@drip-apex/apex-cli 0.6.2 → 0.6.3

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 CHANGED
@@ -70,3 +70,31 @@ the first writes: `apex experiments create`, `apex goals create`,
70
70
  - Edit experiment files, variation code, metrics, and targeting only through supported paths.
71
71
  - Do not bypass the variation artifact persistence path with direct API writes.
72
72
  - Keep generated evidence outside `qa.json`; attach it through QA commands.
73
+
74
+
75
+ ## Project code
76
+
77
+ ```sh
78
+ apex project-code get --shop example.com --json
79
+ apex project-code diff --file project-code.json --shop example.com --json
80
+ apex project-code set --file project-code.json --shop example.com --backup previous-code.json --json
81
+ ```
82
+
83
+ The input is a partial projectCode object, for example `{ "css": "body { color: red; }" }`.
84
+ Supported fields are `enabled` (boolean) and `css`, `helperJs`, and `js` (strings or null).
85
+ Each code field permits at most 20,000 characters. Omitted fields remain unchanged.
86
+ Null or blank code clears that field. Unknown fields and empty objects are rejected.
87
+
88
+ GET returns complete code, shop identity, and an opaque version. DIFF returns effective
89
+ before/after values and changed fields without writing. SET creates a new backup file
90
+ with complete previous code, identity, capture time, and the exact version before PATCH.
91
+ It never replaces an existing backup. Backup failures prevent the write. An effective
92
+ no-op returns `unchanged`, without a PATCH or backup file.
93
+
94
+ The backed-up version becomes expectedVersion unchanged. A conflict returns exit 5,
95
+ keeps the backup, and does not retry. Read the latest state and preview it before a new
96
+ attempt with a new backup path. Authentication uses exit 2; invalid input uses exit 3.
97
+ A successful write returns `saved`, its diff, backup path, current version, writtenVersion,
98
+ and precompile result. A failed precompile is a saved-write warning. All warnings remain
99
+ in JSON and are also written to stderr. User keys resolve domains or pin UUID context;
100
+ shop keys must match the requested identity before any backup or write.
package/README.md CHANGED
@@ -109,6 +109,10 @@ including every subcommand, argument, option, and alias.
109
109
  ### Experiments and delivery
110
110
 
111
111
  - `apex consent` — manage consent gating.
112
+ - `apex project-code` — inspect and safely change shop project code.
113
+ - `apex project-code get` — read complete code and its opaque version.
114
+ - `apex project-code diff` — preview a partial JSON change without writing.
115
+ - `apex project-code set` — create a backup before a version-checked write.
112
116
  - `apex triggers` — manage data-layer triggers and temporary setup capture.
113
117
  - `apex triggers get --shop <domain-or-id>` — read the identified shop's runtime triggers.
114
118
  - `apex triggers set` — replace the shop's runtime triggers from a JSON file.
@@ -228,3 +232,31 @@ Read the CLI reference on the docs site:
228
232
  - https://docs.drip-apex.com/cli/commands-output
229
233
 
230
234
  Maintainers: see the [CLI release runbook](../../docs/cli-releases.md).
235
+
236
+
237
+ ## Project code
238
+
239
+ ```sh
240
+ apex project-code get --shop example.com --json
241
+ apex project-code diff --file project-code.json --shop example.com --json
242
+ apex project-code set --file project-code.json --shop example.com --backup previous-code.json --json
243
+ ```
244
+
245
+ The input is a partial projectCode object, for example `{ "css": "body { color: red; }" }`.
246
+ Supported fields are `enabled` (boolean) and `css`, `helperJs`, and `js` (strings or null).
247
+ Each code field permits at most 20,000 characters. Omitted fields remain unchanged.
248
+ Null or blank code clears that field. Unknown fields and empty objects are rejected.
249
+
250
+ GET returns complete code, shop identity, and an opaque version. DIFF returns effective
251
+ before/after values and changed fields without writing. SET creates a new backup file
252
+ with complete previous code, identity, capture time, and the exact version before PATCH.
253
+ It never replaces an existing backup. Backup failures prevent the write. An effective
254
+ no-op returns `unchanged`, without a PATCH or backup file.
255
+
256
+ The backed-up version becomes expectedVersion unchanged. A conflict returns exit 5,
257
+ keeps the backup, and does not retry. Read the latest state and preview it before a new
258
+ attempt with a new backup path. Authentication uses exit 2; invalid input uses exit 3.
259
+ A successful write returns `saved`, its diff, backup path, current version, writtenVersion,
260
+ and precompile result. A failed precompile is a saved-write warning. All warnings remain
261
+ in JSON and are also written to stderr. User keys resolve domains or pin UUID context;
262
+ shop keys must match the requested identity before any backup or write.
package/dist/cli.js CHANGED
@@ -6988,14 +6988,14 @@ var init_open = __esm({
6988
6988
  }
6989
6989
  const subprocess = import_node_child_process5.default.spawn(command, cliArguments, childProcessOptions);
6990
6990
  if (options.wait) {
6991
- return new Promise((resolve, reject) => {
6991
+ return new Promise((resolve2, reject) => {
6992
6992
  subprocess.once("error", reject);
6993
6993
  subprocess.once("close", (exitCode) => {
6994
6994
  if (!options.allowNonzeroExitCode && exitCode > 0) {
6995
6995
  reject(new Error(`Exited with code ${exitCode}`));
6996
6996
  return;
6997
6997
  }
6998
- resolve(subprocess);
6998
+ resolve2(subprocess);
6999
6999
  });
7000
7000
  });
7001
7001
  }
@@ -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.6.2",
7115
+ version: "0.6.3",
7116
7116
  license: "MIT",
7117
7117
  description: "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
7118
7118
  private: false,
@@ -8369,9 +8369,9 @@ function resolveSelection(tree, input) {
8369
8369
  function ask(question) {
8370
8370
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stderr });
8371
8371
  return new Promise(
8372
- (resolve) => rl.question(question, (answer) => {
8372
+ (resolve2) => rl.question(question, (answer) => {
8373
8373
  rl.close();
8374
- resolve(answer.trim());
8374
+ resolve2(answer.trim());
8375
8375
  })
8376
8376
  );
8377
8377
  }
@@ -8449,7 +8449,7 @@ function prompt(question, hide = false) {
8449
8449
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
8450
8450
  throw new Error("Interactive input is unavailable in non-TTY mode.");
8451
8451
  }
8452
- return new Promise((resolve) => {
8452
+ return new Promise((resolve2) => {
8453
8453
  const rl = import_node_readline2.default.createInterface({
8454
8454
  input: process.stdin,
8455
8455
  output: process.stderr
@@ -8469,13 +8469,13 @@ function prompt(question, hide = false) {
8469
8469
  stdout.write = originalWrite;
8470
8470
  console.error();
8471
8471
  rl.close();
8472
- resolve(answer.trim());
8472
+ resolve2(answer.trim());
8473
8473
  });
8474
8474
  masking = true;
8475
8475
  } else {
8476
8476
  rl.question(question, (answer) => {
8477
8477
  rl.close();
8478
- resolve(answer.trim());
8478
+ resolve2(answer.trim());
8479
8479
  });
8480
8480
  }
8481
8481
  });
@@ -8501,7 +8501,7 @@ function parseFormBody(body5) {
8501
8501
  async function waitForBrowserLogin(baseUrl, userMode = false) {
8502
8502
  const state = import_node_crypto2.default.randomBytes(24).toString("base64url");
8503
8503
  const clientName = import_node_os5.default.hostname() || "Local machine";
8504
- return new Promise((resolve, reject) => {
8504
+ return new Promise((resolve2, reject) => {
8505
8505
  let settled = false;
8506
8506
  let timeout = null;
8507
8507
  const finish = (err, apiKey) => {
@@ -8513,7 +8513,7 @@ async function waitForBrowserLogin(baseUrl, userMode = false) {
8513
8513
  } catch {
8514
8514
  }
8515
8515
  if (err) reject(err);
8516
- else resolve(apiKey ?? "");
8516
+ else resolve2(apiKey ?? "");
8517
8517
  };
8518
8518
  const server = import_node_http.default.createServer((req, res) => {
8519
8519
  if (req.method === "OPTIONS") {
@@ -9414,8 +9414,8 @@ async function confirmApply(message) {
9414
9414
  }
9415
9415
  const readline3 = await import("node:readline");
9416
9416
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
9417
- const answer = await new Promise((resolve) => {
9418
- rl.question(source_default.yellow(`${message} [y/N] `), resolve);
9417
+ const answer = await new Promise((resolve2) => {
9418
+ rl.question(source_default.yellow(`${message} [y/N] `), resolve2);
9419
9419
  });
9420
9420
  rl.close();
9421
9421
  return answer.toLowerCase() === "y";
@@ -9828,8 +9828,8 @@ var del = new Command("delete").alias("rm").description("Delete an experiment").
9828
9828
  input: process.stdin,
9829
9829
  output: process.stderr
9830
9830
  });
9831
- const answer = await new Promise((resolve) => {
9832
- rl.question(source_default.yellow(`Delete experiment "${exp.name}" (${exp.id})? [y/N] `), resolve);
9831
+ const answer = await new Promise((resolve2) => {
9832
+ rl.question(source_default.yellow(`Delete experiment "${exp.name}" (${exp.id})? [y/N] `), resolve2);
9833
9833
  });
9834
9834
  rl.close();
9835
9835
  if (answer.toLowerCase() !== "y") {
@@ -10357,7 +10357,7 @@ var screenshots = new Command("screenshots").description(
10357
10357
  if (screenshotNow() >= deadline)
10358
10358
  fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10359
10359
  await new Promise(
10360
- (resolve) => setTimeout(resolve, Math.min(delayMs, Math.max(0, deadline - screenshotNow())))
10360
+ (resolve2) => setTimeout(resolve2, Math.min(delayMs, Math.max(0, deadline - screenshotNow())))
10361
10361
  );
10362
10362
  if (screenshotNow() >= deadline)
10363
10363
  fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
@@ -10938,11 +10938,11 @@ function cleanupResetJs(experiment) {
10938
10938
  }
10939
10939
  function runAbHarness(args, cwd) {
10940
10940
  const bin = process.env.AB_HARNESS_BIN || "ab-harness";
10941
- return new Promise((resolve, reject) => {
10941
+ return new Promise((resolve2, reject) => {
10942
10942
  const child = (0, import_node_child_process6.spawn)(bin, args, { cwd, stdio: "inherit" });
10943
10943
  child.on("error", reject);
10944
10944
  child.on("exit", (code) => {
10945
- if (code === 0) resolve();
10945
+ if (code === 0) resolve2();
10946
10946
  else reject(new Error(`${bin} ${args.join(" ")} exited with ${code}`));
10947
10947
  });
10948
10948
  });
@@ -12416,7 +12416,7 @@ var suggest = new Command("suggest").description("Suggest goals and funnels from
12416
12416
  const readline3 = await import("node:readline");
12417
12417
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
12418
12418
  const answer = await new Promise(
12419
- (resolve) => rl.question(source_default.yellow("Create this goal? [y/N] "), resolve)
12419
+ (resolve2) => rl.question(source_default.yellow("Create this goal? [y/N] "), resolve2)
12420
12420
  );
12421
12421
  rl.close();
12422
12422
  if (answer.toLowerCase() !== "y") continue;
@@ -12778,9 +12778,219 @@ var off = new Command("off").description("Disable consent gating").action(async
12778
12778
  });
12779
12779
  var consentCommand = new Command("consent").description("Manage consent gating").addCommand(get2).addCommand(set).addCommand(off);
12780
12780
 
12781
+ // src/commands/project-code.ts
12782
+ var import_node_fs15 = require("node:fs");
12783
+ var import_node_path13 = require("node:path");
12784
+
12785
+ // src/commands/runtime-settings-shop.ts
12786
+ var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
12787
+ async function identifyRuntimeSettingsShop(command, requested) {
12788
+ let credentials = requireCredentials();
12789
+ if (credentials.credentialKind === "user") {
12790
+ let shopId = requested;
12791
+ if (!uuidPattern.test(requested)) {
12792
+ const context = await getAccountContext({
12793
+ ...credentials,
12794
+ resolvedContext: { ...credentials.resolvedContext, shopId: void 0 }
12795
+ });
12796
+ if (!context.ok || !context.data)
12797
+ fail(command, context.error ?? "Shop discovery failed", classifyStatus(context.status));
12798
+ const match2 = context.data.data.orgs.flatMap((org) => org.shops).find((shop) => shop.domain.toLowerCase() === requested.toLowerCase());
12799
+ if (!match2)
12800
+ fail(command, "--shop domain is not available to this user", EXIT_CODES.validation);
12801
+ shopId = match2.id;
12802
+ }
12803
+ credentials = { ...credentials, resolvedContext: { ...credentials.resolvedContext, shopId } };
12804
+ }
12805
+ const response = await getInstallDomains(credentials);
12806
+ if (!response.ok || !response.data)
12807
+ fail(command, response.error ?? "Shop identity unavailable", classifyStatus(response.status));
12808
+ const { shop_id, domain } = response.data;
12809
+ if (credentials.credentialKind === "user" && shop_id.toLowerCase() !== credentials.resolvedContext?.shopId?.toLowerCase()) {
12810
+ fail(
12811
+ command,
12812
+ "Shop identity does not match the requested user-key context",
12813
+ EXIT_CODES.validation
12814
+ );
12815
+ }
12816
+ if (requested.toLowerCase() !== shop_id.toLowerCase() && requested.toLowerCase() !== domain.toLowerCase()) {
12817
+ fail(command, "--shop does not match the authenticated shop", EXIT_CODES.validation);
12818
+ }
12819
+ setEnvelopeContext({ shop_id, credential: credentials.credentialKind ?? "shop" });
12820
+ return { shop_id, domain, credentials };
12821
+ }
12822
+
12823
+ // src/commands/project-code.ts
12824
+ var fields = ["enabled", "css", "helperJs", "js"];
12825
+ function readPatch(command, file) {
12826
+ let input;
12827
+ try {
12828
+ input = JSON.parse((0, import_node_fs15.readFileSync)(file, "utf8"));
12829
+ } catch (error) {
12830
+ fail(command, `Cannot read project code JSON: ${String(error)}`, EXIT_CODES.validation);
12831
+ }
12832
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).length === 0) {
12833
+ fail(command, "Project code must be a nonempty partial object", EXIT_CODES.validation);
12834
+ }
12835
+ for (const [key, value] of Object.entries(input)) {
12836
+ if (!fields.includes(key))
12837
+ fail(command, `Unknown project code field: ${key}`, EXIT_CODES.validation);
12838
+ if (key === "enabled" ? typeof value !== "boolean" : value !== null && (typeof value !== "string" || value.length > 2e4)) {
12839
+ fail(
12840
+ command,
12841
+ `${key} must be ${key === "enabled" ? "a boolean" : "null or a string of at most 20,000 characters"}`,
12842
+ EXIT_CODES.validation
12843
+ );
12844
+ }
12845
+ }
12846
+ return input;
12847
+ }
12848
+ async function readCurrent(command, shop) {
12849
+ const response = await getRuntimeSettings(shop.credentials);
12850
+ if (!response.ok || !response.data)
12851
+ fail(
12852
+ command,
12853
+ response.error ?? "Runtime settings read failed",
12854
+ classifyStatus(response.status)
12855
+ );
12856
+ const data = response.data;
12857
+ if (data.shop_id !== shop.shop_id)
12858
+ fail(command, "Runtime settings returned a different shop", EXIT_CODES.validation);
12859
+ const projectCode = data.runtime_settings?.projectCode;
12860
+ if (!projectCode || typeof projectCode.enabled !== "boolean" || [projectCode.css, projectCode.helperJs, projectCode.js].some(
12861
+ (code) => typeof code !== "string"
12862
+ ) || !(data.version === null || typeof data.version === "string" && data.version.length > 0)) {
12863
+ fail(
12864
+ command,
12865
+ "Runtime settings must include complete project code and an opaque version",
12866
+ EXIT_CODES.general
12867
+ );
12868
+ }
12869
+ return { projectCode, version: data.version };
12870
+ }
12871
+ function effectiveDiff(before, patch) {
12872
+ const after = { ...before };
12873
+ if (patch.enabled !== void 0) after.enabled = patch.enabled;
12874
+ for (const key of ["css", "helperJs", "js"]) {
12875
+ if (patch[key] !== void 0) after[key] = patch[key]?.trim() || "";
12876
+ }
12877
+ const changedFields = fields.filter((field) => before[field] !== after[field]);
12878
+ const changes = Object.fromEntries(
12879
+ changedFields.map((field) => [field, { before: before[field], after: after[field] }])
12880
+ );
12881
+ return { changedFields, changes, before, after };
12882
+ }
12883
+ var projectCodeCommand = new Command("project-code").description(
12884
+ "Inspect, preview, and back up shop project code"
12885
+ );
12886
+ projectCodeCommand.addCommand(
12887
+ new Command("get").description("Read complete project code and its opaque version").requiredOption("--shop <domain-or-id>", "Shop domain or UUID").action(async (options, command) => {
12888
+ const { credentials: _credentials, ...identity } = await identifyRuntimeSettingsShop(
12889
+ command,
12890
+ options.shop
12891
+ );
12892
+ const current = await readCurrent(command, { ...identity, credentials: _credentials });
12893
+ output(
12894
+ command,
12895
+ { ...identity, ...current },
12896
+ () => process.stdout.write(`${JSON.stringify({ ...identity, ...current }, null, 2)}
12897
+ `)
12898
+ );
12899
+ })
12900
+ );
12901
+ for (const mode of ["diff", "set"]) {
12902
+ const command = new Command(mode).description(
12903
+ mode === "diff" ? "Preview an effective partial project code change without writing" : "Back up prior code and apply a version-checked partial change"
12904
+ ).requiredOption("--file <path>", "Partial projectCode JSON file").requiredOption("--shop <domain-or-id>", "Shop domain or UUID");
12905
+ if (mode === "set")
12906
+ command.requiredOption(
12907
+ "--backup <path>",
12908
+ "New backup file; an existing file is never replaced"
12909
+ );
12910
+ command.action(async (options, actionCommand) => {
12911
+ const patch = readPatch(actionCommand, options.file);
12912
+ const shop = await identifyRuntimeSettingsShop(actionCommand, options.shop);
12913
+ const current = await readCurrent(actionCommand, shop);
12914
+ const { credentials: _credentials, ...identity } = shop;
12915
+ const diff = effectiveDiff(current.projectCode, patch);
12916
+ if (mode === "diff" || diff.changedFields.length === 0) {
12917
+ const result2 = {
12918
+ ...identity,
12919
+ status: diff.changedFields.length ? "changed" : "unchanged",
12920
+ version: current.version,
12921
+ diff
12922
+ };
12923
+ output(
12924
+ actionCommand,
12925
+ result2,
12926
+ () => process.stdout.write(`${JSON.stringify(result2, null, 2)}
12927
+ `)
12928
+ );
12929
+ return;
12930
+ }
12931
+ const backupPath = (0, import_node_path13.resolve)(options.backup);
12932
+ try {
12933
+ (0, import_node_fs15.writeFileSync)(
12934
+ backupPath,
12935
+ `${JSON.stringify({ ...identity, ...current, capturedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
12936
+ `,
12937
+ { flag: "wx", mode: 384 }
12938
+ );
12939
+ } catch (error) {
12940
+ fail(
12941
+ actionCommand,
12942
+ `Backup failed; project code was not sent: ${String(error)}`,
12943
+ EXIT_CODES.general
12944
+ );
12945
+ }
12946
+ const response = await updateRuntimeSettings(shop.credentials, {
12947
+ projectCode: patch,
12948
+ expectedVersion: current.version
12949
+ });
12950
+ if (!response.ok || !response.data) {
12951
+ fail(
12952
+ actionCommand,
12953
+ response.error ?? "Project code update failed",
12954
+ classifyStatus(response.status),
12955
+ { ...response.errorDetails, backupPath }
12956
+ );
12957
+ }
12958
+ const data = response.data;
12959
+ const warnings = [...data.warnings ?? []];
12960
+ if (data.precompile?.status === "warning" || data.precompile?.status === "error") {
12961
+ warnings.push(
12962
+ `Project code saved; publication ${data.precompile.status}: ${data.precompile.summary}`
12963
+ );
12964
+ }
12965
+ if (data.shop_id !== identity.shop_id)
12966
+ warnings.push(
12967
+ "The write response has a different shop identity; inspect the saved state before another write."
12968
+ );
12969
+ for (const warning of warnings) process.stderr.write(`WARNING: ${warning}
12970
+ `);
12971
+ const result = {
12972
+ ...identity,
12973
+ status: "saved",
12974
+ diff,
12975
+ backupPath,
12976
+ version: data.version,
12977
+ writtenVersion: data.writtenVersion,
12978
+ precompile: data.precompile,
12979
+ warnings
12980
+ };
12981
+ output(
12982
+ actionCommand,
12983
+ result,
12984
+ () => process.stdout.write(`${JSON.stringify(result, null, 2)}
12985
+ `)
12986
+ );
12987
+ });
12988
+ projectCodeCommand.addCommand(command);
12989
+ }
12990
+
12781
12991
  // src/commands/triggers.ts
12782
- var import_node_fs15 = __toESM(require("node:fs"));
12783
- var import_node_path13 = __toESM(require("node:path"));
12992
+ var import_node_fs16 = __toESM(require("node:fs"));
12993
+ var import_node_path14 = __toESM(require("node:path"));
12784
12994
 
12785
12995
  // src/trigger-proposal.ts
12786
12996
  function runtimePath(path24) {
@@ -12868,8 +13078,8 @@ function score2(path24, patterns) {
12868
13078
  0
12869
13079
  );
12870
13080
  }
12871
- function best(fields, patterns, numeric2 = false) {
12872
- const ranked = fields.filter(
13081
+ function best(fields2, patterns, numeric2 = false) {
13082
+ const ranked = fields2.filter(
12873
13083
  (field) => !numeric2 || field.numericType === true || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
12874
13084
  ).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));
12875
13085
  if (!ranked.length) return [];
@@ -12894,15 +13104,15 @@ function proposeTriggers(report) {
12894
13104
  continue;
12895
13105
  const descriptors = candidate.fields ?? candidate.paths;
12896
13106
  const payload = candidate.properties ?? candidate.payload ?? candidate;
12897
- const fields = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
13107
+ const fields2 = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
12898
13108
  const revenues = best(
12899
- fields,
13109
+ fields2,
12900
13110
  [/revenue/, /total.*amount|amount.*total/, /grand.*total|total/, /value|amount/],
12901
13111
  true
12902
13112
  );
12903
- const orderIds = best(fields, [/order.*id|id.*order/, /transaction.*id/, /(^|\.)id$/]);
12904
- const currencies = best(fields, [/currency/, /(^|\.)cur(rency)?$/]);
12905
- const items = best(fields, [/(^|\.)((line_?)?items|products)$/]);
13113
+ const orderIds = best(fields2, [/order.*id|id.*order/, /transaction.*id/, /(^|\.)id$/]);
13114
+ const currencies = best(fields2, [/currency/, /(^|\.)cur(rency)?$/]);
13115
+ const items = best(fields2, [/(^|\.)((line_?)?items|products)$/]);
12906
13116
  if (!revenues.length || !orderIds.length || !currencies.length) continue;
12907
13117
  for (const revenue of revenues)
12908
13118
  for (const orderId of orderIds)
@@ -13197,10 +13407,10 @@ function writeWarnings(result) {
13197
13407
  `);
13198
13408
  }
13199
13409
  }
13200
- var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
13410
+ var uuidPattern2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
13201
13411
  async function identify(command, requestedShop) {
13202
13412
  let credentials = requireCredentials();
13203
- if (credentials.credentialKind === "user" && !uuidPattern.test(requestedShop)) {
13413
+ if (credentials.credentialKind === "user" && !uuidPattern2.test(requestedShop)) {
13204
13414
  const discoveryCredentials = {
13205
13415
  ...credentials,
13206
13416
  resolvedContext: { ...credentials.resolvedContext, shopId: void 0 }
@@ -13273,7 +13483,7 @@ var get3 = new Command("get").description("Get data layer triggers").requiredOpt
13273
13483
  var set2 = new Command("set").requiredOption("--file <json>", "JSON trigger array").requiredOption("--shop <domain-or-id>", "Shop domain or Apex shop ID").action(async function(opts) {
13274
13484
  let triggers;
13275
13485
  try {
13276
- triggers = strictTriggers(JSON.parse(import_node_fs15.default.readFileSync(import_node_path13.default.resolve(opts.file), "utf8")));
13486
+ triggers = strictTriggers(JSON.parse(import_node_fs16.default.readFileSync(import_node_path14.default.resolve(opts.file), "utf8")));
13277
13487
  } catch (error) {
13278
13488
  fail(
13279
13489
  this,
@@ -13300,7 +13510,7 @@ var propose = new Command("propose").requiredOption("--from-file <u2-report.json
13300
13510
  let proposals;
13301
13511
  let report;
13302
13512
  try {
13303
- report = JSON.parse(import_node_fs15.default.readFileSync(import_node_path13.default.resolve(opts.fromFile), "utf8"));
13513
+ report = JSON.parse(import_node_fs16.default.readFileSync(import_node_path14.default.resolve(opts.fromFile), "utf8"));
13304
13514
  proposals = proposeTriggers(report);
13305
13515
  } catch (error) {
13306
13516
  fail(this, error instanceof Error ? error.message : String(error), 2);
@@ -14932,7 +15142,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14932
15142
  `Response from ${hostname} uses unsupported content encoding "${encoding}"`
14933
15143
  )
14934
15144
  );
14935
- return new Promise((resolve, reject) => {
15145
+ return new Promise((resolve2, reject) => {
14936
15146
  const stream = decoderFor(encoding);
14937
15147
  const chunks = [];
14938
15148
  let bytes = 0;
@@ -14952,7 +15162,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14952
15162
  }
14953
15163
  chunks.push(chunk.subarray(0, limits.maxBytes - bytes));
14954
15164
  stream.destroy();
14955
- settle(() => resolve({ body: Buffer.concat(chunks), truncated: true }));
15165
+ settle(() => resolve2({ body: Buffer.concat(chunks), truncated: true }));
14956
15166
  return;
14957
15167
  }
14958
15168
  chunks.push(chunk);
@@ -14962,7 +15172,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14962
15172
  "error",
14963
15173
  (error) => settle(() => {
14964
15174
  if (limits.onExceed === "truncate" && inputTruncated && chunks.length > 0)
14965
- resolve({ body: Buffer.concat(chunks), truncated: true });
15175
+ resolve2({ body: Buffer.concat(chunks), truncated: true });
14966
15176
  else
14967
15177
  reject(
14968
15178
  new BodyDecodeError(
@@ -14973,7 +15183,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14973
15183
  );
14974
15184
  stream.on(
14975
15185
  "end",
14976
- () => settle(() => resolve({ body: Buffer.concat(chunks), truncated: false }))
15186
+ () => settle(() => resolve2({ body: Buffer.concat(chunks), truncated: false }))
14977
15187
  );
14978
15188
  stream.end(raw);
14979
15189
  });
@@ -15000,7 +15210,7 @@ function responseFromIncoming(res, body5, decoded) {
15000
15210
  });
15001
15211
  }
15002
15212
  function pinnedFetch(url, addresses, limits) {
15003
- return new Promise((resolve, reject) => {
15213
+ return new Promise((resolve2, reject) => {
15004
15214
  const send3 = url.protocol === "https:" ? import_node_https.request : import_node_http2.request;
15005
15215
  const pinnedLookup = (_hostname, options, callback) => {
15006
15216
  if (options?.all)
@@ -15047,7 +15257,7 @@ function pinnedFetch(url, addresses, limits) {
15047
15257
  chunks.push(chunk.subarray(0, limits.maxBytes - bytes));
15048
15258
  req.destroy();
15049
15259
  settle(
15050
- () => finalizeIncoming(url, res, Buffer.concat(chunks), true, limits).then(resolve, reject)
15260
+ () => finalizeIncoming(url, res, Buffer.concat(chunks), true, limits).then(resolve2, reject)
15051
15261
  );
15052
15262
  return;
15053
15263
  }
@@ -15062,7 +15272,7 @@ function pinnedFetch(url, addresses, limits) {
15062
15272
  reject(new Error(`No HTTP status from ${url.hostname}`));
15063
15273
  return;
15064
15274
  }
15065
- finalizeIncoming(url, res, Buffer.concat(chunks), false, limits).then(resolve, reject);
15275
+ finalizeIncoming(url, res, Buffer.concat(chunks), false, limits).then(resolve2, reject);
15066
15276
  })
15067
15277
  );
15068
15278
  }
@@ -15494,8 +15704,8 @@ var installCommand = new Command("install").description("Check Apex storefront i
15494
15704
 
15495
15705
  // src/commands/flags.ts
15496
15706
  init_source();
15497
- var import_node_fs16 = __toESM(require("node:fs"));
15498
- var import_node_path14 = __toESM(require("node:path"));
15707
+ var import_node_fs17 = __toESM(require("node:fs"));
15708
+ var import_node_path15 = __toESM(require("node:path"));
15499
15709
 
15500
15710
  // src/edge-status.ts
15501
15711
  function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 360) {
@@ -15537,7 +15747,7 @@ function body(command, value, opts) {
15537
15747
  let parsed = {};
15538
15748
  if (value) {
15539
15749
  const source = value.startsWith("@") ? value.slice(1) : value;
15540
- const text = value.startsWith("@") || import_node_fs16.default.existsSync(source) ? import_node_fs16.default.readFileSync(import_node_path14.default.resolve(process.cwd(), source), "utf8") : value;
15750
+ const text = value.startsWith("@") || import_node_fs17.default.existsSync(source) ? import_node_fs17.default.readFileSync(import_node_path15.default.resolve(process.cwd(), source), "utf8") : value;
15541
15751
  try {
15542
15752
  parsed = JSON.parse(text);
15543
15753
  } catch (e) {
@@ -15670,8 +15880,8 @@ Examples:
15670
15880
  );
15671
15881
 
15672
15882
  // src/commands/backlog.ts
15673
- var import_node_fs17 = __toESM(require("node:fs"));
15674
- var import_node_path15 = __toESM(require("node:path"));
15883
+ var import_node_fs18 = __toESM(require("node:fs"));
15884
+ var import_node_path16 = __toESM(require("node:path"));
15675
15885
  init_source();
15676
15886
  var kind2 = "backlog";
15677
15887
  var singular = "backlog item";
@@ -15679,7 +15889,7 @@ function parse(command, value) {
15679
15889
  if (!value) return {};
15680
15890
  try {
15681
15891
  const source = value.startsWith("@") ? value.slice(1) : value;
15682
- const text = value.startsWith("@") || import_node_fs17.default.existsSync(source) ? import_node_fs17.default.readFileSync(import_node_path15.default.resolve(process.cwd(), source), "utf8") : value;
15892
+ const text = value.startsWith("@") || import_node_fs18.default.existsSync(source) ? import_node_fs18.default.readFileSync(import_node_path16.default.resolve(process.cwd(), source), "utf8") : value;
15683
15893
  const body5 = JSON.parse(text);
15684
15894
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
15685
15895
  throw new Error("body must be an object");
@@ -15779,7 +15989,7 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
15779
15989
  const readline3 = await import("node:readline");
15780
15990
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
15781
15991
  const answer = await new Promise(
15782
- (resolve) => rl.question(source_default.yellow(`Delete backlog item ${id}? [y/N] `), resolve)
15992
+ (resolve2) => rl.question(source_default.yellow(`Delete backlog item ${id}? [y/N] `), resolve2)
15783
15993
  );
15784
15994
  rl.close();
15785
15995
  if (answer.toLowerCase() !== "y") {
@@ -15795,8 +16005,8 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
15795
16005
  var backlogCommand = new Command("backlog").description("Manage backlog").addCommand(list7).addCommand(get6).addCommand(write2("create")).addCommand(write2("update")).addCommand(archive).addCommand(restore).addCommand(remove2);
15796
16006
 
15797
16007
  // src/commands/signals.ts
15798
- var import_node_fs18 = __toESM(require("node:fs"));
15799
- var import_node_path16 = __toESM(require("node:path"));
16008
+ var import_node_fs19 = __toESM(require("node:fs"));
16009
+ var import_node_path17 = __toESM(require("node:path"));
15800
16010
  init_source();
15801
16011
  var kind3 = "signals";
15802
16012
  var singular2 = "signal";
@@ -15804,7 +16014,7 @@ function parse2(command, value) {
15804
16014
  if (!value) return {};
15805
16015
  try {
15806
16016
  const source = value.startsWith("@") ? value.slice(1) : value;
15807
- const text = value.startsWith("@") || import_node_fs18.default.existsSync(source) ? import_node_fs18.default.readFileSync(import_node_path16.default.resolve(process.cwd(), source), "utf8") : value;
16017
+ const text = value.startsWith("@") || import_node_fs19.default.existsSync(source) ? import_node_fs19.default.readFileSync(import_node_path17.default.resolve(process.cwd(), source), "utf8") : value;
15808
16018
  const body5 = JSON.parse(text);
15809
16019
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
15810
16020
  throw new Error("body must be an object");
@@ -15886,14 +16096,14 @@ var signalsCommand = new Command("signals").description("Manage signals").addCom
15886
16096
 
15887
16097
  // src/commands/personalizations.ts
15888
16098
  init_source();
15889
- var import_node_fs19 = __toESM(require("node:fs"));
15890
- var import_node_path17 = __toESM(require("node:path"));
16099
+ var import_node_fs20 = __toESM(require("node:fs"));
16100
+ var import_node_path18 = __toESM(require("node:path"));
15891
16101
  var kind4 = "personalizations";
15892
16102
  function body2(command, value, opts) {
15893
16103
  let parsed = {};
15894
16104
  if (value) {
15895
16105
  const source = value.startsWith("@") ? value.slice(1) : value;
15896
- const text = value.startsWith("@") || import_node_fs19.default.existsSync(source) ? import_node_fs19.default.readFileSync(import_node_path17.default.resolve(process.cwd(), source), "utf8") : value;
16106
+ const text = value.startsWith("@") || import_node_fs20.default.existsSync(source) ? import_node_fs20.default.readFileSync(import_node_path18.default.resolve(process.cwd(), source), "utf8") : value;
15897
16107
  try {
15898
16108
  parsed = JSON.parse(text);
15899
16109
  } catch (e) {
@@ -15975,13 +16185,13 @@ var personalizationsCommand = new Command("personalizations").description("Manag
15975
16185
 
15976
16186
  // src/commands/targeting-resources.ts
15977
16187
  init_source();
15978
- var import_node_fs20 = __toESM(require("node:fs"));
15979
- var import_node_path18 = __toESM(require("node:path"));
16188
+ var import_node_fs21 = __toESM(require("node:fs"));
16189
+ var import_node_path19 = __toESM(require("node:path"));
15980
16190
  function body3(command, value) {
15981
16191
  if (!value) return {};
15982
16192
  const source = value.startsWith("@") ? value.slice(1) : value;
15983
16193
  try {
15984
- const text = value.startsWith("@") || import_node_fs20.default.existsSync(source) ? import_node_fs20.default.readFileSync(import_node_path18.default.resolve(process.cwd(), source), "utf8") : value;
16194
+ const text = value.startsWith("@") || import_node_fs21.default.existsSync(source) ? import_node_fs21.default.readFileSync(import_node_path19.default.resolve(process.cwd(), source), "utf8") : value;
15985
16195
  return JSON.parse(text);
15986
16196
  } catch (error) {
15987
16197
  fail(
@@ -16101,8 +16311,8 @@ function targetingResourceCommand(kind5) {
16101
16311
  input: process.stdin,
16102
16312
  output: process.stderr
16103
16313
  });
16104
- const answer = await new Promise((resolve) => {
16105
- rl.question(source_default.yellow(`Delete exclusion group "${id}"? [y/N] `), resolve);
16314
+ const answer = await new Promise((resolve2) => {
16315
+ rl.question(source_default.yellow(`Delete exclusion group "${id}"? [y/N] `), resolve2);
16106
16316
  });
16107
16317
  rl.close();
16108
16318
  if (answer.toLowerCase() !== "y") {
@@ -16232,8 +16442,8 @@ var targetingCommand = new Command("targeting").description("Test experiment tar
16232
16442
 
16233
16443
  // src/commands/sos.ts
16234
16444
  init_source();
16235
- var import_node_fs21 = __toESM(require("node:fs"));
16236
- var import_node_path19 = __toESM(require("node:path"));
16445
+ var import_node_fs22 = __toESM(require("node:fs"));
16446
+ var import_node_path20 = __toESM(require("node:path"));
16237
16447
  var SOS_TYPES = /* @__PURE__ */ new Set([
16238
16448
  "full_handover",
16239
16449
  "design",
@@ -16246,7 +16456,7 @@ var SOS_TYPES = /* @__PURE__ */ new Set([
16246
16456
  function readText(value) {
16247
16457
  if (!value) return void 0;
16248
16458
  if (!value.startsWith("@")) return value;
16249
- return import_node_fs21.default.readFileSync(import_node_path19.default.resolve(process.cwd(), value.slice(1)), "utf8");
16459
+ return import_node_fs22.default.readFileSync(import_node_path20.default.resolve(process.cwd(), value.slice(1)), "utf8");
16250
16460
  }
16251
16461
  function printTicket(ticket, reused) {
16252
16462
  console.log();
@@ -16380,13 +16590,13 @@ var markRead = new Command("mark-read").option("--channel <key>", "Mark one chan
16380
16590
  var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(list11).addCommand(send2).addCommand(markRead);
16381
16591
 
16382
16592
  // src/commands/shops.ts
16383
- var import_node_fs22 = __toESM(require("node:fs"));
16384
- var import_node_path20 = __toESM(require("node:path"));
16593
+ var import_node_fs23 = __toESM(require("node:fs"));
16594
+ var import_node_path21 = __toESM(require("node:path"));
16385
16595
  function body4(command, raw) {
16386
16596
  if (!raw) return {};
16387
16597
  try {
16388
16598
  const source = raw.startsWith("@") ? raw.slice(1) : raw;
16389
- const text = raw.startsWith("@") || import_node_fs22.default.existsSync(source) ? import_node_fs22.default.readFileSync(import_node_path20.default.resolve(source), "utf8") : raw;
16599
+ const text = raw.startsWith("@") || import_node_fs23.default.existsSync(source) ? import_node_fs23.default.readFileSync(import_node_path21.default.resolve(source), "utf8") : raw;
16390
16600
  const value = JSON.parse(text);
16391
16601
  if (!value || typeof value !== "object" || Array.isArray(value))
16392
16602
  throw new Error("body must be an object");
@@ -17199,16 +17409,16 @@ var discoverCommand = new Command("discover").description("Read back the trackin
17199
17409
  });
17200
17410
 
17201
17411
  // src/commands/skills.ts
17202
- var import_node_fs23 = __toESM(require("node:fs"));
17203
- var import_node_path21 = __toESM(require("node:path"));
17412
+ var import_node_fs24 = __toESM(require("node:fs"));
17413
+ var import_node_path22 = __toESM(require("node:path"));
17204
17414
  function packageRoot() {
17205
17415
  const candidates = [
17206
- import_node_path21.default.resolve(__dirname, ".."),
17207
- import_node_path21.default.resolve(import_node_path21.default.dirname(process.argv[1] ?? process.cwd()), ".."),
17208
- import_node_path21.default.resolve(process.cwd(), "packages/cli")
17416
+ import_node_path22.default.resolve(__dirname, ".."),
17417
+ import_node_path22.default.resolve(import_node_path22.default.dirname(process.argv[1] ?? process.cwd()), ".."),
17418
+ import_node_path22.default.resolve(process.cwd(), "packages/cli")
17209
17419
  ];
17210
17420
  for (const candidate of candidates) {
17211
- if (import_node_fs23.default.existsSync(import_node_path21.default.join(candidate, "AGENTS.md"))) return candidate;
17421
+ if (import_node_fs24.default.existsSync(import_node_path22.default.join(candidate, "AGENTS.md"))) return candidate;
17212
17422
  }
17213
17423
  throw new Error(
17214
17424
  `Apex CLI package files not found. Looked for AGENTS.md in: ${candidates.join(", ")}`
@@ -17217,19 +17427,19 @@ function packageRoot() {
17217
17427
  function skillPaths() {
17218
17428
  const root = packageRoot();
17219
17429
  return {
17220
- agents: import_node_path21.default.join(root, "AGENTS.md"),
17221
- skill: import_node_path21.default.join(root, "skills", "apex-cli", "SKILL.md")
17430
+ agents: import_node_path22.default.join(root, "AGENTS.md"),
17431
+ skill: import_node_path22.default.join(root, "skills", "apex-cli", "SKILL.md")
17222
17432
  };
17223
17433
  }
17224
17434
  function installSkill(paths, targetRoot) {
17225
- const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs23.default.existsSync(filePath));
17435
+ const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs24.default.existsSync(filePath));
17226
17436
  if (missing.length > 0) {
17227
17437
  throw new Error(`Apex CLI package files not found. Missing: ${missing.join(", ")}`);
17228
17438
  }
17229
- const target = import_node_path21.default.resolve(targetRoot, "apex-cli");
17230
- import_node_fs23.default.mkdirSync(target, { recursive: true });
17231
- import_node_fs23.default.copyFileSync(paths.skill, import_node_path21.default.join(target, "SKILL.md"));
17232
- import_node_fs23.default.copyFileSync(paths.agents, import_node_path21.default.join(target, "AGENTS.md"));
17439
+ const target = import_node_path22.default.resolve(targetRoot, "apex-cli");
17440
+ import_node_fs24.default.mkdirSync(target, { recursive: true });
17441
+ import_node_fs24.default.copyFileSync(paths.skill, import_node_path22.default.join(target, "SKILL.md"));
17442
+ import_node_fs24.default.copyFileSync(paths.agents, import_node_path22.default.join(target, "AGENTS.md"));
17233
17443
  return target;
17234
17444
  }
17235
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 action64(opts) {
@@ -17329,8 +17539,8 @@ async function confirmArchive(command, shopId, force) {
17329
17539
  }
17330
17540
  const readline3 = await import("node:readline");
17331
17541
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
17332
- const answer = await new Promise((resolve) => {
17333
- rl.question("Archive this shop? Apex delivery will be disabled. [y/N] ", resolve);
17542
+ const answer = await new Promise((resolve2) => {
17543
+ rl.question("Archive this shop? Apex delivery will be disabled. [y/N] ", resolve2);
17334
17544
  });
17335
17545
  rl.close();
17336
17546
  if (answer.toLowerCase() !== "y") {
@@ -17384,7 +17594,7 @@ async function confirm(c, force, label, retry) {
17384
17594
  const readline3 = await import("node:readline");
17385
17595
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
17386
17596
  const answer = await new Promise(
17387
- (resolve) => rl.question(source_default.yellow(`${label}? [y/N] `), resolve)
17597
+ (resolve2) => rl.question(source_default.yellow(`${label}? [y/N] `), resolve2)
17388
17598
  );
17389
17599
  rl.close();
17390
17600
  if (answer.toLowerCase() !== "y") {
@@ -17975,10 +18185,10 @@ var webhooksCommand = new Command("webhooks").description("Manage shop webhooks"
17975
18185
 
17976
18186
  // src/commands/simulate.ts
17977
18187
  init_source();
17978
- var import_node_fs24 = __toESM(require("node:fs"));
17979
- var import_node_path22 = __toESM(require("node:path"));
18188
+ var import_node_fs25 = __toESM(require("node:fs"));
18189
+ var import_node_path23 = __toESM(require("node:path"));
17980
18190
  function readJson2(file) {
17981
- const text = file === "-" ? import_node_fs24.default.readFileSync(0, "utf8") : import_node_fs24.default.readFileSync(import_node_path22.default.resolve(file), "utf8");
18191
+ const text = file === "-" ? import_node_fs25.default.readFileSync(0, "utf8") : import_node_fs25.default.readFileSync(import_node_path23.default.resolve(file), "utf8");
17982
18192
  try {
17983
18193
  return JSON.parse(text);
17984
18194
  } catch (error) {
@@ -18100,8 +18310,8 @@ var simulateCommand = new Command("simulate").description("Simulate data-layer t
18100
18310
  });
18101
18311
 
18102
18312
  // src/commands/config-resources.ts
18103
- var import_node_fs25 = __toESM(require("node:fs"));
18104
- var import_node_path23 = __toESM(require("node:path"));
18313
+ var import_node_fs26 = __toESM(require("node:fs"));
18314
+ var import_node_path24 = __toESM(require("node:path"));
18105
18315
  function show(resource) {
18106
18316
  process.stdout.write(`${JSON.stringify(resource, null, 2)}
18107
18317
  `);
@@ -18127,8 +18337,8 @@ async function confirmAssetDelete(id) {
18127
18337
  input: process.stdin,
18128
18338
  output: process.stderr
18129
18339
  });
18130
- const answer = await new Promise((resolve) => {
18131
- rl.question(`Delete asset "${id}"? This cannot be undone. [y/N] `, resolve);
18340
+ const answer = await new Promise((resolve2) => {
18341
+ rl.question(`Delete asset "${id}"? This cannot be undone. [y/N] `, resolve2);
18132
18342
  });
18133
18343
  rl.close();
18134
18344
  return answer.trim().toLowerCase() === "y";
@@ -18174,12 +18384,12 @@ function readFamily(family, label) {
18174
18384
  }
18175
18385
  var assetReads = readFamily("assets", "assets");
18176
18386
  var upload = new Command("upload").description("Upload an asset").argument("<file>").action(async function(file) {
18177
- const absolute = import_node_path23.default.resolve(process.cwd(), file);
18178
- if (!import_node_fs25.default.existsSync(absolute)) fail(this, `File not found: ${file}`, 3);
18179
- const fileStat = import_node_fs25.default.statSync(absolute);
18387
+ const absolute = import_node_path24.default.resolve(process.cwd(), file);
18388
+ if (!import_node_fs26.default.existsSync(absolute)) fail(this, `File not found: ${file}`, 3);
18389
+ const fileStat = import_node_fs26.default.statSync(absolute);
18180
18390
  if (!fileStat.isFile()) fail(this, `File not found: ${file}`, 3);
18181
18391
  if (fileStat.size > MAX_ASSET_SIZE_BYTES) fail(this, `${file} is larger than 10 MB`, 3);
18182
- const mimeType = ASSET_MIME_TYPES[import_node_path23.default.extname(absolute).toLowerCase()];
18392
+ const mimeType = ASSET_MIME_TYPES[import_node_path24.default.extname(absolute).toLowerCase()];
18183
18393
  if (!mimeType)
18184
18394
  fail(
18185
18395
  this,
@@ -18188,8 +18398,8 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
18188
18398
  );
18189
18399
  const r = await uploadAssets(requireCredentials(), [
18190
18400
  {
18191
- name: import_node_path23.default.basename(absolute),
18192
- bytes: import_node_fs25.default.readFileSync(absolute),
18401
+ name: import_node_path24.default.basename(absolute),
18402
+ bytes: import_node_fs26.default.readFileSync(absolute),
18193
18403
  type: mimeType
18194
18404
  }
18195
18405
  ]);
@@ -18279,6 +18489,7 @@ program2.addCommand(funnelsCommand);
18279
18489
  program2.addCommand(environmentsCommand);
18280
18490
  program2.addCommand(consentCommand);
18281
18491
  program2.addCommand(triggersCommand);
18492
+ program2.addCommand(projectCodeCommand);
18282
18493
  program2.addCommand(domainsCommand);
18283
18494
  program2.addCommand(handoverCommand);
18284
18495
  program2.addCommand(analyticsCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drip-apex/apex-cli",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "license": "MIT",
5
5
  "description": "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
6
6
  "private": false,
@@ -63,3 +63,31 @@ Platform administrators can archive a mistakenly created shop with
63
63
  `apex admin shops restore <shopId>`. Archive refuses a shop with running tests;
64
64
  stop those tests rather than retrying or overriding the refusal. Restore leaves
65
65
  delivery controlled by the runtime kill switch.
66
+
67
+
68
+ ## Project code
69
+
70
+ ```sh
71
+ apex project-code get --shop example.com --json
72
+ apex project-code diff --file project-code.json --shop example.com --json
73
+ apex project-code set --file project-code.json --shop example.com --backup previous-code.json --json
74
+ ```
75
+
76
+ The input is a partial projectCode object, for example `{ "css": "body { color: red; }" }`.
77
+ Supported fields are `enabled` (boolean) and `css`, `helperJs`, and `js` (strings or null).
78
+ Each code field permits at most 20,000 characters. Omitted fields remain unchanged.
79
+ Null or blank code clears that field. Unknown fields and empty objects are rejected.
80
+
81
+ GET returns complete code, shop identity, and an opaque version. DIFF returns effective
82
+ before/after values and changed fields without writing. SET creates a new backup file
83
+ with complete previous code, identity, capture time, and the exact version before PATCH.
84
+ It never replaces an existing backup. Backup failures prevent the write. An effective
85
+ no-op returns `unchanged`, without a PATCH or backup file.
86
+
87
+ The backed-up version becomes expectedVersion unchanged. A conflict returns exit 5,
88
+ keeps the backup, and does not retry. Read the latest state and preview it before a new
89
+ attempt with a new backup path. Authentication uses exit 2; invalid input uses exit 3.
90
+ A successful write returns `saved`, its diff, backup path, current version, writtenVersion,
91
+ and precompile result. A failed precompile is a saved-write warning. All warnings remain
92
+ in JSON and are also written to stderr. User keys resolve domains or pin UUID context;
93
+ shop keys must match the requested identity before any backup or write.