@nextcommerce/campaigns-os 1.34.1 → 1.37.2
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 +42 -13
- package/CHANGELOG.md +117 -0
- package/README.md +48 -21
- package/contracts/agent-relevant-change-policy.v1.json +1 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
- package/contracts/commerce-surface-catalog.json +31 -29
- package/contracts/fixtures/progress/observation.v0.json +89 -0
- package/contracts/release-ledger.json +425 -0
- package/contracts/supported-surface.json +20 -5
- package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
- package/demo/apollo-v0/NOTICE.txt +51 -0
- package/demo/apollo-v0/assets/css/demo.css +2 -0
- package/demo/apollo-v0/assets/css/landing/tokens.css +35 -0
- package/demo/apollo-v0/assets/css/next-core.css +16124 -0
- package/demo/apollo-v0/assets/images/1x1_1.svg +19 -0
- package/demo/apollo-v0/assets/images/1x1_2.svg +19 -0
- package/demo/apollo-v0/assets/images/affirm-logo.svg +24 -0
- package/demo/apollo-v0/assets/images/apple-pay-logo.svg +4 -0
- package/demo/apollo-v0/assets/images/bancontact-logo.svg +1 -0
- package/demo/apollo-v0/assets/images/cc-visa.svg +28 -0
- package/demo/apollo-v0/assets/images/cc_amex.svg +20 -0
- package/demo/apollo-v0/assets/images/cc_discover.svg +21 -0
- package/demo/apollo-v0/assets/images/cc_master.svg +22 -0
- package/demo/apollo-v0/assets/images/credit-card-flags.svg +38 -0
- package/demo/apollo-v0/assets/images/demo-inline-40788e52a7c75b78.svg +1 -0
- package/demo/apollo-v0/assets/images/demo-inline-686e73c8840a0f0a.svg +1 -0
- package/demo/apollo-v0/assets/images/google-pay-logo.svg +7 -0
- package/demo/apollo-v0/assets/images/guarantee-badge.png +0 -0
- package/demo/apollo-v0/assets/images/icon-dollar.svg +5 -0
- package/demo/apollo-v0/assets/images/icon-guarantee.svg +5 -0
- package/demo/apollo-v0/assets/images/icon-shipping.svg +5 -0
- package/demo/apollo-v0/assets/images/icons8-lock-24_1icons8-lock-24.png +0 -0
- package/demo/apollo-v0/assets/images/ideal-logo.svg +30 -0
- package/demo/apollo-v0/assets/images/klarna-logo.svg +9 -0
- package/demo/apollo-v0/assets/images/landing/_shared/16x9.svg +19 -0
- package/demo/apollo-v0/assets/images/landing/_shared/1x1_1.svg +19 -0
- package/demo/apollo-v0/assets/images/landing/_shared/4x3.svg +19 -0
- package/demo/apollo-v0/assets/images/landing/_shared/arrow-right.svg +6 -0
- package/demo/apollo-v0/assets/images/landing/_shared/cta-guarantee-icon.png +0 -0
- package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-icon-1.svg +8 -0
- package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-icon-2.svg +10 -0
- package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-icon-3.svg +13 -0
- package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-icon-4.svg +11 -0
- package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-quote.svg +6 -0
- package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-verified.svg +10 -0
- package/demo/apollo-v0/assets/images/landing/bottomcta-1/check-bullet.svg +5 -0
- package/demo/apollo-v0/assets/images/landing/faq-1/faq-chevron.svg +6 -0
- package/demo/apollo-v0/assets/images/landing/footer-1/footer-logo.png +0 -0
- package/demo/apollo-v0/assets/images/landing/guarantee-1/guarantee-1-badge.svg +8 -0
- package/demo/apollo-v0/assets/images/landing/hero-1/icon-check.svg +5 -0
- package/demo/apollo-v0/assets/images/landing/hero-1/icon-star.svg +6 -0
- package/demo/apollo-v0/assets/images/landing/hero-1/icon-verified.svg +6 -0
- package/demo/apollo-v0/assets/images/landing/icons-5/icon-batteries.svg +7 -0
- package/demo/apollo-v0/assets/images/landing/icons-5/icon-cuff-checking.svg +6 -0
- package/demo/apollo-v0/assets/images/landing/icons-5/icon-dual-user.svg +5 -0
- package/demo/apollo-v0/assets/images/landing/icons-5/icon-fda-cleared.svg +5 -0
- package/demo/apollo-v0/assets/images/landing/icons-5/icon-heartbeat.svg +17 -0
- package/demo/apollo-v0/assets/images/landing/icons-5/icon-lcd-display.svg +10 -0
- package/demo/apollo-v0/assets/images/landing/icons-5/icon-movement-error.svg +7 -0
- package/demo/apollo-v0/assets/images/landing/icons-5/icon-portable.png +0 -0
- package/demo/apollo-v0/assets/images/landing/icons-5/icon-reading-memory.svg +6 -0
- package/demo/apollo-v0/assets/images/landing/icons-5/icon-wrist-comfort.svg +5 -0
- package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-card-1.jpg +0 -0
- package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-card-2.jpg +0 -0
- package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-card-3.jpg +0 -0
- package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-card-4.jpg +0 -0
- package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-1.png +0 -0
- package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-2.png +0 -0
- package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-3.png +0 -0
- package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-4.png +0 -0
- package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-5.png +0 -0
- package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-6.png +0 -0
- package/demo/apollo-v0/assets/images/landing/nav-1/flag-us.png +0 -0
- package/demo/apollo-v0/assets/images/landing/reviews-3/star-card.svg +3 -0
- package/demo/apollo-v0/assets/images/landing/reviews-3/star-lg.svg +3 -0
- package/demo/apollo-v0/assets/images/landing/reviews-3/star-sm.svg +3 -0
- package/demo/apollo-v0/assets/images/landing/reviews-3/verified.svg +10 -0
- package/demo/apollo-v0/assets/images/landing/testimonials-2/testimonials-2-reactions.svg +9 -0
- package/demo/apollo-v0/assets/images/landing/testimonials-2/testimonials-2-stars.svg +6 -0
- package/demo/apollo-v0/assets/images/link-logo.svg +1 -0
- package/demo/apollo-v0/assets/images/next-dark.svg +8 -0
- package/demo/apollo-v0/assets/images/paypal-logo.svg +5 -0
- package/demo/apollo-v0/assets/images/paypal-txt.svg +8 -0
- package/demo/apollo-v0/assets/images/paypal.svg +22 -0
- package/demo/apollo-v0/assets/images/sepa-logo.svg +275 -0
- package/demo/apollo-v0/assets/images/twint-logo.svg +1 -0
- package/demo/apollo-v0/assets/images/united-states-flag-icon.webp +0 -0
- package/demo/apollo-v0/assets/images/upsell-payment-logos.svg +38 -0
- package/demo/apollo-v0/assets/images/usps.png +0 -0
- package/demo/apollo-v0/checkout/index.html +1370 -0
- package/demo/apollo-v0/landing/index.html +1836 -0
- package/demo/apollo-v0/provenance.json +342 -0
- package/demo/apollo-v0/receipt/index.html +292 -0
- package/demo/apollo-v0/upsell-bundle-stepper/index.html +401 -0
- package/docs/activation-and-evidence.md +37 -0
- package/docs/build-packet.md +107 -0
- package/docs/demo-preview.md +63 -0
- package/docs/diagnostics.md +55 -0
- package/docs/orientation-contract-reference.md +2 -1
- package/docs/progress-snapshots.md +142 -0
- package/docs/runtime-readiness.md +1 -1
- package/docs/supported-surface.md +13 -1
- package/package.json +13 -3
- package/schemas/campaign-runtime-build-context.v0.schema.json +42 -0
- package/schemas/campaigns-os-progress-snapshot.v0.schema.json +370 -0
- package/skills/next-campaigns-build/SKILL.md +17 -1
- package/skills/next-campaigns-os/SKILL.md +19 -3
- package/skills/next-campaigns-os-setup/SKILL.md +17 -1
- package/skills/next-campaigns-polish/SKILL.md +18 -2
- package/skills/next-campaigns-qa/SKILL.md +24 -8
- package/skills.json +5 -5
- package/src/cli.mjs +74 -4
- package/src/consent.mjs +2 -2
- package/src/demo-artifact.mjs +97 -0
- package/src/demo.mjs +85 -0
- package/src/diagnostic.mjs +101 -0
- package/src/install-mode.mjs +13 -1
- package/src/progress-node.mjs +177 -0
- package/src/progress.mjs +133 -0
package/src/cli.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { withHtmlScanSnapshot, readHtmlScanText, htmlScanDigest } from "./html-scan.mjs";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { createDemo, demoArguments } from "./demo.mjs";
|
|
3
4
|
import { execFileSync } from "node:child_process";
|
|
4
5
|
import {
|
|
5
6
|
accessSync,
|
|
@@ -22,6 +23,8 @@ import { homedir } from "node:os";
|
|
|
22
23
|
import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
23
24
|
import { fileURLToPath } from "node:url";
|
|
24
25
|
import { shellToken } from "./shell-token.mjs";
|
|
26
|
+
import { diagnosticExport, diagnosticTextLines } from "./diagnostic.mjs";
|
|
27
|
+
import { observeProgress, PROGRESS_OBSERVATION } from "./progress-node.mjs";
|
|
25
28
|
import { describeSdkIgnoredMetaTags, isSdkIgnoredMetaTag } from "./sdk-meta-tags.mjs";
|
|
26
29
|
import { HIDDEN_EAGER_MEDIA_ACTIONS, requiredActionText, substitutePacket } from "./gate-actions.mjs";
|
|
27
30
|
import { ORDER_PATH_DEPTH_DRIFT_CODE, orderPathDepthDriftText, orderPathDepthReconcileAction, orderPathDepthsDisagree, parseOrderPathDepthFlag } from "./proof-policy.mjs";
|
|
@@ -444,6 +447,7 @@ const HELP = `Campaigns OS toolkit
|
|
|
444
447
|
|
|
445
448
|
Usage:
|
|
446
449
|
campaigns-os help
|
|
450
|
+
campaigns-os demo --target <new-directory> # offline inert sample; open landing/index.html; no campaign evidence
|
|
447
451
|
campaigns-os start (--spec <json> | --map-id <id>) --source <html-dir> --target <page-kit-dir> --template-family <family>
|
|
448
452
|
[--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
|
|
449
453
|
[--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
|
|
@@ -472,8 +476,9 @@ Usage:
|
|
|
472
476
|
campaigns-os validate-assembly-report --report <json> [--json]
|
|
473
477
|
campaigns-os install-skills [--platform <claude|codex|agents|all>] [--target <skills-dir>] [--dry-run] [--json]
|
|
474
478
|
campaigns-os tooling status [--platform <claude|codex|agents|all>] [--target <skills-dir>] [--json] # install-mode (checkout or pinned package), git, and skill freshness preflight
|
|
479
|
+
campaigns-os tooling diagnose [--packet <packet>] [--platform <claude|codex|agents|all>] [--json] # read-only redacted support summary
|
|
475
480
|
campaigns-os install-agent-context --target <page-kit-dir> [--dry-run]
|
|
476
|
-
campaigns-os next --packet <json> [--json] # self-decide next stage; returns gates[] + next_actions[] (exact commands) alongside the prompt
|
|
481
|
+
campaigns-os next --packet <json> [--no-write] [--no-remit] [--proxy-base <url>] [--json] # self-decide next stage; returns gates[] + next_actions[] (exact commands) alongside the prompt
|
|
477
482
|
campaigns-os next setup --packet <json> [--context <json>] [--report <json>] [--json]
|
|
478
483
|
campaigns-os next build --packet <json> [--context <json>] [--report <json>] [--json]
|
|
479
484
|
campaigns-os next polish --packet <json> --report <json> [--json]
|
|
@@ -598,6 +603,24 @@ export async function main(argv) {
|
|
|
598
603
|
if (args._[0] === "campaigns-os") args._.shift();
|
|
599
604
|
const command = args._[0] || "help";
|
|
600
605
|
|
|
606
|
+
// An offline sample must not recover sessions or emit lifecycle evidence.
|
|
607
|
+
if (command === "demo") {
|
|
608
|
+
// Validate raw tokens here: parsing loses duplicate flags. The private
|
|
609
|
+
// dispatcher then rechecks the parsed shape and extracts the target.
|
|
610
|
+
demoArguments(args, argv);
|
|
611
|
+
await dispatch(command, args);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Diagnostic export is an inspection, including when a run is active or
|
|
616
|
+
// stale. Bypass session sweeping, ambient resolution, and lifecycle capture
|
|
617
|
+
// so no closeout/remit or journal write can occur before the projection.
|
|
618
|
+
if (command === "tooling" && args._[1] === "diagnose") {
|
|
619
|
+
const result = toolingDiagnose(args);
|
|
620
|
+
console.log(args.json ? JSON.stringify(result, null, 2) : diagnosticTextLines(result).join("\n"));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
601
624
|
// Ambient run session (Tier 3): when `run start` is active, every command
|
|
602
625
|
// shares its run_id WITHOUT --run-id. Explicit --run-id still wins. Resolved
|
|
603
626
|
// ONCE here and threaded through dispatch + persistence so the run_id a
|
|
@@ -1010,6 +1033,17 @@ const PREPARE_MODES = Object.freeze({
|
|
|
1010
1033
|
});
|
|
1011
1034
|
|
|
1012
1035
|
async function dispatch(command, args, recorder = NOOP_RECORDER, ambient = null, sessionHolder = null) {
|
|
1036
|
+
if (command === "demo") {
|
|
1037
|
+
// main() already validated raw argv before entering this private function.
|
|
1038
|
+
const target = demoArguments(args);
|
|
1039
|
+
if (target === null) {
|
|
1040
|
+
console.log("campaigns-os demo --target <new-directory>\nOffline Apollo sample only. Open the printed landing/index.html file. Start a real campaign in a separate new Page Kit folder; preserve your sample edits.");
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const result = createDemo(target);
|
|
1044
|
+
console.log(`Offline sample only; no campaign evidence.\nOpen: ${result.index}\nStart a real campaign in a separate new Page Kit folder. Preserve sample edits; demo is never converted automatically.`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1013
1047
|
if (command === "help" || (args.help && command !== "qa")) {
|
|
1014
1048
|
console.log(HELP);
|
|
1015
1049
|
return;
|
|
@@ -1171,6 +1205,7 @@ async function dispatch(command, args, recorder = NOOP_RECORDER, ambient = null,
|
|
|
1171
1205
|
// explicit stage (`next build`, `next polish`, etc.) is unchanged.
|
|
1172
1206
|
const stage = args._[1] || null;
|
|
1173
1207
|
const result = nextStage(stage, args, ambient);
|
|
1208
|
+
await observeProgress(args, result, { packageVersion: packageVersion(), resolveKey: resolveCampaignsApiKeySource });
|
|
1174
1209
|
writeResult(result, args, result.ok ? 0 : 2);
|
|
1175
1210
|
printNextTinyPrompt(result, args);
|
|
1176
1211
|
return;
|
|
@@ -1182,7 +1217,14 @@ async function dispatch(command, args, recorder = NOOP_RECORDER, ambient = null,
|
|
|
1182
1217
|
// command a QA run prints has to agree with the run_id this session will
|
|
1183
1218
|
// later close and remit under.
|
|
1184
1219
|
const result = await runQaCli(args, { ambient });
|
|
1185
|
-
if (args._[1] === "run" && result?.verdict
|
|
1220
|
+
if (args._[1] === "run" && result?.verdict && recordQaStageOutcome(args, result)) {
|
|
1221
|
+
// Observe committed QA before the existing closeout; this cannot close
|
|
1222
|
+
// a run or change the QA disposition. Reuse the canonical picker.
|
|
1223
|
+
try {
|
|
1224
|
+
const continuation = nextStage(null, { ...args, "no-write": true }, null);
|
|
1225
|
+
await observeProgress(args, continuation, { qaResult: result, packageVersion: packageVersion(), resolveKey: resolveCampaignsApiKeySource });
|
|
1226
|
+
} catch { /* optional progress never changes lifecycle closeout */ }
|
|
1227
|
+
}
|
|
1186
1228
|
if (sessionHolder) sessionHolder.qaResult = result;
|
|
1187
1229
|
return;
|
|
1188
1230
|
}
|
|
@@ -1327,7 +1369,10 @@ async function resolveSpecPath(args, opts = {}) {
|
|
|
1327
1369
|
const spec = await fetchSpecByMapId(mapId, { proxyBase, fetchImpl: opts.fetchImpl });
|
|
1328
1370
|
mkdirSync(cacheDir, { recursive: true });
|
|
1329
1371
|
writeFileSync(cachePath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1330
|
-
return { specPath: cachePath, source: "remote", mapId, proxyBase
|
|
1372
|
+
return { specPath: cachePath, source: "remote", mapId, proxyBase,
|
|
1373
|
+
savedMapRevision: { map_id: mapId, hash: spec.spec_identity?.spec_hash || spec.spec_hash || null,
|
|
1374
|
+
algorithm: "map-store-v1", local_spec_material_hash: specMaterialHash(spec) },
|
|
1375
|
+
};
|
|
1331
1376
|
}
|
|
1332
1377
|
throw new Error(
|
|
1333
1378
|
"Either --spec <path> or --map-id <id> is required. " +
|
|
@@ -2478,6 +2523,7 @@ function prepareBuild(args, options = {}) {
|
|
|
2478
2523
|
: null,
|
|
2479
2524
|
map_id: specInput?.mapId || null,
|
|
2480
2525
|
proxy_base: specInput?.proxyBase || null,
|
|
2526
|
+
saved_map_revision: specInput?.savedMapRevision || null,
|
|
2481
2527
|
source_root: portable(sourceRoot),
|
|
2482
2528
|
target_repo: portable(targetRepo),
|
|
2483
2529
|
template_family: explicitTemplateFamily || null,
|
|
@@ -9500,6 +9546,10 @@ export function nextStage(stage, args, ambient = null) {
|
|
|
9500
9546
|
runRecordCloseout = null;
|
|
9501
9547
|
}
|
|
9502
9548
|
const finalize = (result) => {
|
|
9549
|
+
Object.defineProperty(result, PROGRESS_OBSERVATION, { value: {
|
|
9550
|
+
workspace: { packet, packetPath, targetRepo, contextPath, reportPath },
|
|
9551
|
+
context: readJsonIfExists(contextPath), report, doctor,
|
|
9552
|
+
} });
|
|
9503
9553
|
if (divergences.length) result.divergences = divergences;
|
|
9504
9554
|
result.gates = buildNextGates({ doctor, report, themeGate, polishGate, prepareBuildGate, packetPath });
|
|
9505
9555
|
result.next_actions = buildNextActions({ result, packetPath, packet, themeGate, polishGate, polishCheckpointGate, prepareBuildGate, ambient, runRecordCloseout, purchaseProof, brandContract: doctor.derived?.brand_contract || null, context: readJsonIfExists(contextPath), targetRepo });
|
|
@@ -10917,7 +10967,9 @@ function toolingCommand(args) {
|
|
|
10917
10967
|
} else if (install.mode !== "checkout" && cli.global_binary.status === "found_other_install") {
|
|
10918
10968
|
// An npx cache is ephemeral: never tell the operator to put its .bin on
|
|
10919
10969
|
// PATH. The pinned npx form is what makes the inspected copy run.
|
|
10920
|
-
warnings.push(install.mode === "
|
|
10970
|
+
warnings.push(install.mode === "global"
|
|
10971
|
+
? `The campaigns-os on PATH is a different install; use \`${cli.invocation_prefix} <command>\` to run the global copy inspected here.`
|
|
10972
|
+
: install.mode === "npx_cache"
|
|
10921
10973
|
? `The campaigns-os on PATH (${cli.global_binary.path}) is a different install from the one inspected here (${install.location}); bare commands would run that other copy. Use \`${cli.invocation_prefix} <command>\` so the pinned copy runs.`
|
|
10922
10974
|
: cli.bin_dir
|
|
10923
10975
|
? `The campaigns-os on PATH (${cli.global_binary.path}) is a different install from the one inspected here (${cli.local_bin}); bare commands would run that other copy. Run export PATH="${cli.bin_dir}:$PATH" to put this install first.`
|
|
@@ -10948,6 +11000,24 @@ function toolingCommand(args) {
|
|
|
10948
11000
|
};
|
|
10949
11001
|
}
|
|
10950
11002
|
|
|
11003
|
+
export function toolingDiagnose(args, { runTooling = toolingCommand, runDoctor = doctorCommand } = {}) {
|
|
11004
|
+
let tooling = null;
|
|
11005
|
+
let doctor = null;
|
|
11006
|
+
let inspectionFailed = false;
|
|
11007
|
+
const platform = args.platform || "all";
|
|
11008
|
+
// Inputs are used only by local producers, never echoed, and mutation flags
|
|
11009
|
+
// are not forwarded. Even an exception's message may contain a secret path.
|
|
11010
|
+
try {
|
|
11011
|
+
tooling = runTooling({ _: ["tooling", "status"], platform, ...(typeof args.target === "string" ? { target: args.target } : {}) });
|
|
11012
|
+
} catch { /* unavailable, no raw producer exception in a support export */ }
|
|
11013
|
+
if (args.packet !== undefined) {
|
|
11014
|
+
try {
|
|
11015
|
+
doctor = runDoctor({ packet: args.packet, "no-write": true, ...(typeof args.context === "string" ? { context: args.context } : {}), ...(typeof args.report === "string" ? { report: args.report } : {}) });
|
|
11016
|
+
} catch { inspectionFailed = true; }
|
|
11017
|
+
}
|
|
11018
|
+
return diagnosticExport({ tooling, doctor, platform, inspectionFailed });
|
|
11019
|
+
}
|
|
11020
|
+
|
|
10951
11021
|
function localCliStatus(pkg, install = { mode: "checkout", pinned: null }) {
|
|
10952
11022
|
const resolved = resolveInvocation(ROOT, pkg, install);
|
|
10953
11023
|
return {
|
package/src/consent.mjs
CHANGED
|
@@ -298,7 +298,7 @@ let defaultOnAnnounced = false;
|
|
|
298
298
|
export function announceDefaultOnTelemetry(endpoint, { write = (line) => process.stderr.write(line) } = {}) {
|
|
299
299
|
if (defaultOnAnnounced) return false;
|
|
300
300
|
defaultOnAnnounced = true;
|
|
301
|
-
write(`[campaigns-os] Run telemetry is ON by default: anonymized run records are sent to ${endpoint || CANONICAL_REMIT_SCOPE} to improve templates, tooling, and guidance. Disable with \`campaigns-os telemetry off\` or CAMPAIGNS_OS_TELEMETRY=off.\n`);
|
|
301
|
+
write(`[campaigns-os] Run telemetry is ON by default: anonymized run records and minimal stage observations are sent to ${endpoint || CANONICAL_REMIT_SCOPE} to improve templates, tooling, and guidance; stage observations support Workspace progress. Disable with \`campaigns-os telemetry off\` or CAMPAIGNS_OS_TELEMETRY=off.\n`);
|
|
302
302
|
return true;
|
|
303
303
|
}
|
|
304
304
|
|
|
@@ -338,7 +338,7 @@ export async function promptAndPersistConsent({
|
|
|
338
338
|
if (!isTTY) return { state: "off", source: "default", resolved: false, prompted: false };
|
|
339
339
|
|
|
340
340
|
const answer = await ask(
|
|
341
|
-
"Campaigns OS can send build telemetry to Next Commerce to improve templates, tools, and guidance. Share telemetry from this machine? [Y/n] (change any time): ",
|
|
341
|
+
"Campaigns OS can send build telemetry and minimal stage observations to Next Commerce to improve templates, tools, and guidance; stage observations support Workspace progress. Share telemetry from this machine? [Y/n] (change any time): ",
|
|
342
342
|
);
|
|
343
343
|
const normalized = String(answer || "").trim().toLowerCase();
|
|
344
344
|
const state = (normalized === "" || ["y", "yes", "1", "true", "on"].includes(normalized)) ? "on" : "off";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Validator for the one reviewed bundled projection; never a general sanitizer.
|
|
2
|
+
import {readFileSync,readdirSync,lstatSync} from 'node:fs';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {join,posix} from 'node:path';
|
|
5
|
+
import {parse,parseFragment} from 'parse5';
|
|
6
|
+
export const DEMO_ROUTES=['landing','checkout','upsell-bundle-stepper','receipt'];
|
|
7
|
+
export const DEMO_CSP="default-src 'none'; img-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; form-action 'none'; base-uri 'none'; connect-src 'none'; frame-src 'none'; object-src 'none'";
|
|
8
|
+
const digest=bytes=>`sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
9
|
+
const allowedPath=name=>/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_.-]+)*\.(?:html|css|svg|png|jpe?g|webp|gif|woff2?|txt|json)$/.test(name)&&!name.split('/').some(part=>part==='.'||part==='..');
|
|
10
|
+
const image=name=>/\.(?:svg|png|jpe?g|webp|gif)$/.test(name);
|
|
11
|
+
const inactiveTags=new Set(['script','iframe','frame','frameset','object','embed','base','form','noscript','template','foreignobject','animate','animatetransform','animatemotion','set','audio','video']);
|
|
12
|
+
const resourceAttrs=new Set(['href','xlink:href','src','srcset','poster','data','action','formaction','ping','background','manifest','profile','codebase','archive','dynsrc','lowsrc','srcdoc']);
|
|
13
|
+
function localReference(value,from,files,{fragment=false}={}) {
|
|
14
|
+
if(fragment&&/^#[A-Za-z_][A-Za-z0-9_.:-]*$/.test(value))return null;
|
|
15
|
+
if(typeof value!=='string'||!value||/[\\%?#\s\u0000-\u001f]/.test(value)||value.startsWith('/')||/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value))throw Error('demo.resource_invalid');
|
|
16
|
+
const target=posix.normalize(posix.join(posix.dirname(from),value));
|
|
17
|
+
if(!allowedPath(target)||!files.has(target))throw Error('demo.resource_missing');
|
|
18
|
+
return target;
|
|
19
|
+
}
|
|
20
|
+
function cssText(text,from,files) {
|
|
21
|
+
// Decode CSS escapes before inspecting resource constructs, including escaped
|
|
22
|
+
// identifiers. This projection has no imports, image-set or active behavior.
|
|
23
|
+
const decoded=text.replace(/\\([0-9a-f]{1,6})(?:\r\n|[\t\n\r\f ])?/gi,(_,hex)=>String.fromCodePoint(Math.min(parseInt(hex,16)||0xfffd,0x10ffff))).replace(/\\([^\n\r\f])/g,'$1').replace(/\/\*[\s\S]*?\*\//g,'');
|
|
24
|
+
if(/@import\b|\b(?:image-set|expression|element)\s*\(|-moz-binding|behavior\s*:/i.test(decoded))throw Error('demo.css_active');
|
|
25
|
+
for(const match of decoded.matchAll(/url\s*\(\s*([^)]*)\)/gi)) {
|
|
26
|
+
let value=match[1].trim();if(/^(['"])[\s\S]*\1$/.test(value))value=value.slice(1,-1);
|
|
27
|
+
const target=localReference(value,from,files,{fragment:true});
|
|
28
|
+
if(target&&!image(target)&&!/\.woff2?$/.test(target))throw Error('demo.css_resource_invalid');
|
|
29
|
+
}
|
|
30
|
+
if((decoded.match(/url\s*\(/gi)||[]).length!==[...decoded.matchAll(/url\s*\(\s*([^)]*)\)/gi)].length)throw Error('demo.css_invalid');
|
|
31
|
+
}
|
|
32
|
+
function markup(text,from,files,{svg=false}={}) {
|
|
33
|
+
if(svg&&(/<!doctype/i.test(text)||/<\?(?!xml(?:\s|\?))/i.test(text)))throw Error('demo.svg_document_active');
|
|
34
|
+
const document=svg?parseFragment(text):parse(text);let head=null,banner=false,nav=0,csp=0;
|
|
35
|
+
function visit(node) {
|
|
36
|
+
const tag=node.tagName?.toLowerCase();const attrs=Object.fromEntries((node.attrs||[]).map(attr=>[attr.prefix?`${attr.prefix}:${attr.name}`:attr.name,attr.value]));
|
|
37
|
+
if(inactiveTags.has(tag)||node.content)throw Error('demo.markup_active');
|
|
38
|
+
if(tag==='head')head=node;
|
|
39
|
+
if(attrs.role==='button'&&attrs['aria-disabled']!=='true')throw Error('demo.control_active');
|
|
40
|
+
if(tag==='aside'&&attrs['data-demo-banner']==='true')banner=true;
|
|
41
|
+
if(['button','input','select','textarea'].includes(tag)&&!Object.hasOwn(attrs,'disabled'))throw Error('demo.control_active');
|
|
42
|
+
for(const [name,value] of Object.entries(attrs)) {
|
|
43
|
+
if(/^on/i.test(name)||/^data-next-|^os-checkout-|^next-/.test(name))throw Error('demo.attribute_active');
|
|
44
|
+
if(name==='style')cssText(value,from,files);
|
|
45
|
+
if(!resourceAttrs.has(name))continue;
|
|
46
|
+
let target;
|
|
47
|
+
if(tag==='a'&&name==='href'&&attrs['data-demo-nav']==='true') {
|
|
48
|
+
target=localReference(value,from,files);if(!DEMO_ROUTES.some(route=>target===`${route}/index.html`))throw Error('demo.navigation_invalid');nav++;
|
|
49
|
+
}else if(tag==='link'&&name==='href'&&attrs.rel==='stylesheet') {
|
|
50
|
+
target=localReference(value,from,files);if(!target.endsWith('.css'))throw Error('demo.stylesheet_invalid');
|
|
51
|
+
}else if(tag==='img'&&name==='src') {
|
|
52
|
+
target=localReference(value,from,files);if(!image(target))throw Error('demo.image_invalid');
|
|
53
|
+
}else if(['use','image'].includes(tag)&&['href','xlink:href'].includes(name)) {
|
|
54
|
+
target=localReference(value,from,files,{fragment:true});if(target&&!image(target))throw Error('demo.svg_reference_invalid');
|
|
55
|
+
}else throw Error('demo.resource_attribute_invalid');
|
|
56
|
+
}
|
|
57
|
+
if(tag==='meta'&&Object.hasOwn(attrs,'http-equiv')) {
|
|
58
|
+
if(attrs['http-equiv'].toLowerCase()!=='content-security-policy'||attrs.content!==DEMO_CSP)throw Error('demo.policy_invalid');csp++;
|
|
59
|
+
}
|
|
60
|
+
if(tag==='style')cssText((node.childNodes||[]).map(child=>child.value||'').join(''),from,files);
|
|
61
|
+
for(const child of node.childNodes||[])visit(child);
|
|
62
|
+
}
|
|
63
|
+
visit(document);
|
|
64
|
+
if(!svg) {
|
|
65
|
+
const first=(head?.childNodes||[]).find(node=>node.tagName);
|
|
66
|
+
if(csp!==1||first?.tagName!=='meta'||!banner||nav!==4)throw Error('demo.shell_invalid');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function validateDemoArtifact(root) {
|
|
70
|
+
const files=new Map();let total=0;
|
|
71
|
+
function collect(dir,prefix='') {
|
|
72
|
+
for(const name of readdirSync(dir).sort()) {
|
|
73
|
+
const path=join(dir,name),stat=lstatSync(path),relative=prefix?`${prefix}/${name}`:name;
|
|
74
|
+
if(stat.isSymbolicLink())throw Error('demo.symlink_invalid');
|
|
75
|
+
if(stat.isDirectory()){if(!/^[A-Za-z0-9_-]+$/.test(name))throw Error('demo.path_invalid');collect(path,relative);continue;}
|
|
76
|
+
if(!stat.isFile()||!allowedPath(relative)||stat.size>2097152||(total+=stat.size)>8388608||files.size>=256)throw Error('demo.file_invalid');
|
|
77
|
+
files.set(relative,readFileSync(path));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
collect(root);
|
|
81
|
+
const provenance=JSON.parse(files.get('provenance.json')?.toString()||'null');
|
|
82
|
+
if(provenance?.schema_version!=='campaigns-os-inert-demo/v0'||provenance.template_commit!=='11352c30c596db258679fd3a552b906086b11bb9'||provenance.page_kit_version!=='0.2.0'||provenance.tailwind_version!=='3.4.17'||JSON.stringify(provenance.routes)!==JSON.stringify(DEMO_ROUTES))throw Error('demo.provenance_invalid');
|
|
83
|
+
const expected=provenance.output_hashes;
|
|
84
|
+
const provenanceFields=['schema_version','template_commit','page_kit_version','page_kit_commit','tailwind_version','routes','typography','input_hashes','toolchain_lock_hash','output_hashes'];
|
|
85
|
+
if(Object.keys(provenance).length!==provenanceFields.length||Object.keys(provenance).some(name=>!provenanceFields.includes(name))||provenance.page_kit_commit!=='eed0679b1d02ef5cbe7f21964165c4202724d91d'||provenance.typography!=='system-ui, sans-serif'||!/^sha256:[0-9a-f]{64}$/.test(provenance.toolchain_lock_hash)||!provenance.input_hashes||Object.keys(provenance.input_hashes).length>256||Object.entries(provenance.input_hashes).some(([name,value])=>!/^(?:src\/explore|rendered|_data)\/[A-Za-z0-9_./-]+$/.test(name)||name.split('/').includes('..')||!/^sha256:[0-9a-f]{64}$/.test(value)))throw Error('demo.provenance_invalid');
|
|
86
|
+
if(!expected||Object.keys(expected).length!==files.size-1||Object.keys(expected).some(name=>!files.has(name)||name==='provenance.json'))throw Error('demo.manifest_invalid');
|
|
87
|
+
for(const [name,bytes] of files) {
|
|
88
|
+
if(name!=='provenance.json'&&expected[name]!==digest(bytes))throw Error('demo.digest_mismatch');
|
|
89
|
+
if(name.endsWith('.html')){if(!DEMO_ROUTES.some(route=>name===`${route}/index.html`))throw Error('demo.route_invalid');markup(bytes.toString(),name,files);}
|
|
90
|
+
if(name.endsWith('.svg'))markup(bytes.toString(),name,files,{svg:true});
|
|
91
|
+
if(name.endsWith('.css'))cssText(bytes.toString(),name,files);
|
|
92
|
+
if(name.endsWith('.json')&&name!=='provenance.json')throw Error('demo.json_invalid');
|
|
93
|
+
}
|
|
94
|
+
for(const route of DEMO_ROUTES)if(!files.has(`${route}/index.html`))throw Error('demo.route_missing');
|
|
95
|
+
if(!files.has('NOTICE.txt'))throw Error('demo.notice_missing');
|
|
96
|
+
return {files,provenance,totalBytes:total};
|
|
97
|
+
}
|
package/src/demo.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { mkdirSync, openSync, fstatSync, lstatSync, writeFileSync, closeSync, unlinkSync, rmdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import { resolve, dirname, basename, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { validateDemoArtifact } from "./demo-artifact.mjs";
|
|
5
|
+
|
|
6
|
+
const BUNDLE = fileURLToPath(new URL("../demo/apollo-v0", import.meta.url));
|
|
7
|
+
const TARGET_CHANGED_MESSAGE = "demo.target_changed: destination changed during copy; inspect and preserve its files, then retry with a different new directory";
|
|
8
|
+
const same = (left, right) => left.dev === right.dev && left.ino === right.ino;
|
|
9
|
+
function owns(path, stat) {
|
|
10
|
+
try { const current = lstatSync(path); return !current.isSymbolicLink() && same(current, stat); } catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveDemoTarget(target, { resolveParent = realpathSync } = {}) {
|
|
14
|
+
if (typeof target !== "string" || !target.trim()) throw Error("demo.target_required: use --target <new-directory>");
|
|
15
|
+
const destination = resolve(target), name = basename(destination);
|
|
16
|
+
if (!name) throw Error("demo.target_invalid: choose a new directory below an existing parent");
|
|
17
|
+
return join(resolveParent(dirname(destination)), name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Exclusive creation only. Failure cleanup removes precisely the entries this
|
|
21
|
+
// invocation created, and never recursively removes a caller's directory.
|
|
22
|
+
export function createDemo(target, { bundle = BUNDLE, writeBytes = writeFileSync } = {}) {
|
|
23
|
+
if (typeof target !== "string" || !target.trim()) throw Error("demo.target_required: use --target <new-directory>");
|
|
24
|
+
const artifact = validateDemoArtifact(bundle);
|
|
25
|
+
// Resolve the existing parent once; mkdir remains exclusive at the final
|
|
26
|
+
// component, so an existing directory, file or symlink cannot be overwritten.
|
|
27
|
+
const root = resolveDemoTarget(target);
|
|
28
|
+
const directories = [], files = [];
|
|
29
|
+
function directory(path) {
|
|
30
|
+
mkdirSync(path);
|
|
31
|
+
const stat = lstatSync(path);
|
|
32
|
+
directories.push({ path, stat });
|
|
33
|
+
return stat;
|
|
34
|
+
}
|
|
35
|
+
let rootStat;
|
|
36
|
+
try { rootStat = directory(root); } catch (error) {
|
|
37
|
+
if (error.code === "EEXIST") throw Error("demo.target_exists: choose a new directory; demo never merges or overwrites");
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
const created = new Map([[root, rootStat]]);
|
|
41
|
+
try {
|
|
42
|
+
for (const [name, bytes] of artifact.files) {
|
|
43
|
+
const path = join(root, name);
|
|
44
|
+
const parts = name.split("/"); parts.pop();
|
|
45
|
+
let current = root;
|
|
46
|
+
for (const part of parts) {
|
|
47
|
+
if (!owns(current, created.get(current))) throw Error(TARGET_CHANGED_MESSAGE);
|
|
48
|
+
current = join(current, part);
|
|
49
|
+
if (!created.has(current)) created.set(current, directory(current));
|
|
50
|
+
}
|
|
51
|
+
if (!owns(current, created.get(current))) throw Error(TARGET_CHANGED_MESSAGE);
|
|
52
|
+
const fd = openSync(path, "wx");
|
|
53
|
+
try {
|
|
54
|
+
files.push({ path, stat: fstatSync(fd) });
|
|
55
|
+
writeBytes(fd, bytes);
|
|
56
|
+
} finally { closeSync(fd); }
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
for (const entry of files.reverse()) if (owns(entry.path, entry.stat)) { try { unlinkSync(entry.path); } catch {} }
|
|
60
|
+
for (const entry of directories.reverse()) if (owns(entry.path, entry.stat)) { try { rmdirSync(entry.path); } catch {} }
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
return { index: join(root, "landing", "index.html"), files: artifact.files.size };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function demoArguments(args, argv = null) {
|
|
67
|
+
if (argv) {
|
|
68
|
+
const tokens = argv[0] === "campaigns-os" ? argv.slice(1) : argv;
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
for (let index = 1; index < tokens.length; index++) {
|
|
71
|
+
const flag = tokens[index];
|
|
72
|
+
if (!["--target", "--help"].includes(flag) || seen.has(flag)) throw Error("demo.unsupported_arguments: only one --target and --help are supported");
|
|
73
|
+
seen.add(flag);
|
|
74
|
+
if (flag === "--target") {
|
|
75
|
+
if (!tokens[index + 1] || tokens[index + 1].startsWith("--")) throw Error("demo.target_required: use --target <new-directory>");
|
|
76
|
+
index++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const unsupported = Object.keys(args).filter(name => !["_", "target", "help"].includes(name));
|
|
81
|
+
if (unsupported.length || args._.length !== 1 || (args.help !== undefined && args.help !== true)) throw Error("demo.unsupported_arguments: supported usage is demo --target <new-directory> or demo --help; no-write and dry-run are unsupported");
|
|
82
|
+
if (args.help) return null;
|
|
83
|
+
if (typeof args.target !== "string" || !args.target.trim()) throw Error("demo.target_required: use --target <new-directory>");
|
|
84
|
+
return args.target;
|
|
85
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// A projection, never a scrubber: source objects and free text never enter
|
|
2
|
+
// the export. Unknown producer values have fixed, non-actionable markers.
|
|
3
|
+
const MODES = new Set(["checkout", "node_modules", "global", "npx_cache", "package_directory"]);
|
|
4
|
+
const PLATFORMS = new Set(["claude", "codex", "agents", "all"]);
|
|
5
|
+
const STAGES = new Set(["prepare-build", "doctor-blocked", "setup", "build", "polish", "deploy", "qa", "done"]);
|
|
6
|
+
const STATUSES = new Set(["ready", "attention_required", "ready_with_warnings", "ready_with_waivers", "blocked"]);
|
|
7
|
+
const REASONS = new Set([
|
|
8
|
+
"assembly.template_lock", "campaign.allowed_domains_confirmed", "deploy.preview_url",
|
|
9
|
+
"page_kit.scaffold_required", "spec.page_url_html_extension", "spec.route_collision",
|
|
10
|
+
"scope.runtime_qa_blocked", "scope.partial_build", "content_residue.needs_merchant_input",
|
|
11
|
+
"page_kit.store_profile.mismatch", "page_kit.store_profile.demo_residue",
|
|
12
|
+
"page_kit.sdk_version.expected_observed_mismatch", "page_kit.sdk_version.repo_newer",
|
|
13
|
+
"page_kit.sdk_version.conflicting_declarations", "polish.evidence_missing",
|
|
14
|
+
"polish.assembly_source_package_stale", "polish.source_package_stale", "polish.stale",
|
|
15
|
+
"polish.hidden_eager_media.capture_missing", "polish.hidden_eager_media.capture_malformed",
|
|
16
|
+
"theme_gate.starter_palette_only", "built_output.campaign_identity",
|
|
17
|
+
"built_output.upsell_selector_scope", "built_output.sdk_markup.swap_with_add_to_cart",
|
|
18
|
+
"built_output.sdk_markup.checkout_not_form", "built_output.sdk_markup.wrong_field_name",
|
|
19
|
+
"built_output.sdk_markup.missing_selector_id_match",
|
|
20
|
+
]);
|
|
21
|
+
const ACTIONS = new Set([
|
|
22
|
+
"repair_target", "align_store_profile", "align_sdk_version", "repair_waiver", "waive_checkpoint",
|
|
23
|
+
"rerun_build", "run_polish", "theme_generate", "apply_brand_layer", "waive_theme",
|
|
24
|
+
"polish.hidden_eager_media.capture", "polish.hidden_eager_media.install_browser",
|
|
25
|
+
"polish.hidden_eager_media.repair", "polish.hidden_eager_media.repair_authority",
|
|
26
|
+
"polish.hidden_eager_media.local_proof_rebuild", "polish.hidden_eager_media.waive",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const RECOVERY = Object.freeze({
|
|
30
|
+
skills: { owner: "operator", action_id: "install-skills", input_needed: "Selected agent profile", instruction: "Refresh the bundled skills for that profile and restart the agent." },
|
|
31
|
+
pin: { owner: "operator", action_id: "tooling.status", input_needed: "Reviewed toolkit version and lockfile", instruction: "Compare the installed package with the reviewed version; registry currency is not checked." },
|
|
32
|
+
update: { owner: "operator", action_id: "tooling.status", input_needed: "Reviewed upstream revision", instruction: "Review and update the toolkit checkout using the existing worktree workflow." },
|
|
33
|
+
doctor: { owner: "workflow_owner", action_id: "doctor", input_needed: "Local packet inputs and detailed doctor findings", instruction: "Inspect doctor locally and follow its existing owner, required inputs, and recovery actions." },
|
|
34
|
+
unsupported: { owner: "toolkit_maintainer", action_id: "tooling.diagnose", input_needed: "Producer vocabulary supported by this diagnostic version", instruction: "Review unsupported producer values locally; this export gives them no authority." },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export function diagnosticExport({ tooling = null, doctor = null, platform = "all", inspectionFailed = false } = {}) {
|
|
38
|
+
const reasonIds = new Set();
|
|
39
|
+
const actionIds = new Set();
|
|
40
|
+
const recovery = [];
|
|
41
|
+
const enumValue = (value, allowed) => {
|
|
42
|
+
if (allowed.has(value)) return value;
|
|
43
|
+
reasonIds.add("diagnostic.unsupported_value");
|
|
44
|
+
return "unknown";
|
|
45
|
+
};
|
|
46
|
+
const safePlatform = enumValue(platform, PLATFORMS);
|
|
47
|
+
const mode = tooling ? enumValue(tooling.install?.mode, MODES) : "unknown";
|
|
48
|
+
const version = typeof tooling?.package?.version === "string" && /^\d{1,6}\.\d{1,6}\.\d{1,6}$/.test(tooling.package.version)
|
|
49
|
+
? tooling.package.version : null;
|
|
50
|
+
const toolingStatus = tooling ? enumValue(tooling.status, STATUSES) : "unavailable";
|
|
51
|
+
if (!tooling) reasonIds.add("diagnostic.tooling_unavailable");
|
|
52
|
+
const doctorStatus = doctor ? enumValue(doctor.status, STATUSES) : inspectionFailed ? "unavailable" : "not_requested";
|
|
53
|
+
const stage = doctor ? enumValue(doctor.next?.stage, STAGES) : "unknown";
|
|
54
|
+
let freshness = "unknown";
|
|
55
|
+
if (tooling?.install?.pinned?.commit && /^[0-9a-f]{7,40}$/.test(tooling.install.pinned.commit)) freshness = "pinned";
|
|
56
|
+
else if (mode === "checkout" && tooling?.git?.status === "ok" && Number.isInteger(tooling.git.behind) && tooling.git.behind >= 0) {
|
|
57
|
+
freshness = tooling.git.behind > 0 ? "local_ref_behind" : "local_ref_current";
|
|
58
|
+
}
|
|
59
|
+
if (tooling) {
|
|
60
|
+
if (typeof tooling.skills?.ok !== "boolean") {
|
|
61
|
+
reasonIds.add("diagnostic.unsupported_value");
|
|
62
|
+
} else if (tooling.skills.ok === false) {
|
|
63
|
+
reasonIds.add("tooling.skills_stale"); actionIds.add("install-skills"); recovery.push(RECOVERY.skills);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (freshness === "unknown") { reasonIds.add("tooling.freshness_unknown"); recovery.push(RECOVERY.pin); }
|
|
67
|
+
if (freshness === "local_ref_behind") { reasonIds.add("tooling.checkout_behind"); recovery.push(RECOVERY.update); }
|
|
68
|
+
for (const issue of [...(Array.isArray(doctor?.errors) ? doctor.errors : []), ...(Array.isArray(doctor?.warnings) ? doctor.warnings : [])]) {
|
|
69
|
+
reasonIds.add(REASONS.has(issue?.code) ? issue.code : "diagnostic.unsupported_reason");
|
|
70
|
+
}
|
|
71
|
+
for (const gate of Array.isArray(doctor?.derived?.checkpoint_gates) ? doctor.derived.checkpoint_gates : []) {
|
|
72
|
+
for (const action of Array.isArray(gate?.required_actions) ? gate.required_actions : []) {
|
|
73
|
+
actionIds.add(ACTIONS.has(action?.id) ? action.id : "diagnostic.unsupported_action");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if ((doctor && doctorStatus !== "ready") || inspectionFailed) {
|
|
77
|
+
if (inspectionFailed) reasonIds.add("diagnostic.inspection_unavailable");
|
|
78
|
+
actionIds.add("doctor"); recovery.push(RECOVERY.doctor);
|
|
79
|
+
}
|
|
80
|
+
if ([...reasonIds, ...actionIds].some((id) => id.startsWith("diagnostic.unsupported_"))) recovery.push(RECOVERY.unsupported);
|
|
81
|
+
return {
|
|
82
|
+
schema_version: "campaigns-os-diagnostic/v0",
|
|
83
|
+
version, install_mode: mode, platform: safePlatform,
|
|
84
|
+
tooling_status: toolingStatus, doctor_status: doctorStatus, stage,
|
|
85
|
+
freshness: { toolkit: freshness, inspection: doctor ? "observed" : inspectionFailed ? "unavailable" : "not_requested" },
|
|
86
|
+
reason_ids: [...reasonIds].sort(), action_ids: [...actionIds].sort(), recovery,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function diagnosticTextLines(result) {
|
|
91
|
+
return [
|
|
92
|
+
"Campaigns OS diagnostic (redacted)",
|
|
93
|
+
`Version: ${result.version || "unknown"}; install: ${result.install_mode}; platform: ${result.platform}`,
|
|
94
|
+
`Tooling: ${result.tooling_status}; doctor: ${result.doctor_status}; next stage: ${result.stage}`,
|
|
95
|
+
`Freshness: ${result.freshness.toolkit}; local inspection: ${result.freshness.inspection}`,
|
|
96
|
+
`Reasons: ${result.reason_ids.join(", ") || "none"}`,
|
|
97
|
+
`Actions: ${result.action_ids.join(", ") || "none"}`,
|
|
98
|
+
...result.recovery.map((item) => `Recovery (${item.owner}, ${item.action_id}): ${item.instruction} Input needed: ${item.input_needed}.`),
|
|
99
|
+
"This summary contains no campaign content or paths. Inspect detailed recovery locally. QA publication recovery uses the retained verdict with qa publish; never repeat an order to publish evidence.",
|
|
100
|
+
];
|
|
101
|
+
}
|
package/src/install-mode.mjs
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
import { accessSync, constants as fsConstants, existsSync, readFileSync, realpathSync } from "node:fs";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { basename, delimiter, dirname, join, relative, resolve, sep } from "node:path";
|
|
8
|
+
import { shellToken } from "./shell-token.mjs";
|
|
8
9
|
|
|
9
10
|
const PACKAGE_INSTALL_MODE_LABELS = Object.freeze({
|
|
10
11
|
checkout: "git checkout",
|
|
11
12
|
npx_cache: "package install (npx cache)",
|
|
12
13
|
node_modules: "package install (node_modules)",
|
|
13
14
|
package_directory: "package install",
|
|
15
|
+
global: "global package install",
|
|
14
16
|
});
|
|
15
17
|
|
|
16
18
|
const PUBLIC_GIT_SOURCE = "github:NextCommerceCo/campaigns-os";
|
|
@@ -111,9 +113,18 @@ export function localInstallStatus(root, pkg = {}) {
|
|
|
111
113
|
const segments = root.split(sep);
|
|
112
114
|
const npxIndex = segments.indexOf("_npx");
|
|
113
115
|
const inNpxCache = npxIndex >= 0 && /^[0-9a-f]{8,}$/i.test(segments[npxIndex + 1] || "") && segments[npxIndex + 2] === "node_modules";
|
|
116
|
+
const modules = enclosingNodeModules(root);
|
|
117
|
+
// npm's global layout is prefix/lib/node_modules on POSIX and
|
|
118
|
+
// prefix/node_modules on Windows. Verify the prefix's own bin points to
|
|
119
|
+
// this package, independently of PATH, which may select another install.
|
|
120
|
+
const globalPrefix = modules && (process.platform === "win32" ? dirname(modules) : basename(dirname(modules)) === "lib" ? dirname(dirname(modules)) : null);
|
|
121
|
+
const globalBin = globalPrefix ? join(globalPrefix, process.platform === "win32" ? "campaigns-os.cmd" : "bin/campaigns-os") : null;
|
|
122
|
+
const ownBin = resolve(root, typeof pkg.bin === "object" ? pkg.bin["campaigns-os"] || "bin/campaigns-os.mjs" : "bin/campaigns-os.mjs");
|
|
123
|
+
const globalInstall = globalBin && existsSync(globalBin) && executableTargetPath(globalBin) === realpathOrSelf(ownBin);
|
|
114
124
|
const mode = inNpxCache
|
|
115
125
|
? "npx_cache"
|
|
116
|
-
:
|
|
126
|
+
: globalInstall ? "global"
|
|
127
|
+
: modules
|
|
117
128
|
? "node_modules"
|
|
118
129
|
: "package_directory";
|
|
119
130
|
const pinned = derivePackagePin(root, pkg);
|
|
@@ -227,6 +238,7 @@ export function resolveInvocation(root, pkg = {}, install = localInstallStatus(r
|
|
|
227
238
|
if (install.mode === "checkout") prefix = "npm run campaigns-os --";
|
|
228
239
|
else if (install.mode === "npx_cache") prefix = install.pinned?.spec ? `npx --yes ${install.pinned.spec}` : "campaigns-os";
|
|
229
240
|
else if (install.mode === "node_modules") prefix = "npx campaigns-os";
|
|
241
|
+
else if (install.mode === "global") prefix = matches ? "campaigns-os" : `node ${shellToken(localBin)}`;
|
|
230
242
|
else prefix = "campaigns-os";
|
|
231
243
|
return {
|
|
232
244
|
install,
|