@nextcommerce/campaigns-os 1.34.1 → 1.37.1

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.
Files changed (115) hide show
  1. package/AGENTS.md +42 -13
  2. package/CHANGELOG.md +87 -0
  3. package/README.md +39 -20
  4. package/contracts/agent-relevant-change-policy.v1.json +1 -0
  5. package/contracts/fixtures/progress/observation.v0.json +89 -0
  6. package/contracts/release-ledger.json +359 -0
  7. package/contracts/supported-surface.json +20 -5
  8. package/demo/apollo-v0/NOTICE.txt +51 -0
  9. package/demo/apollo-v0/assets/css/demo.css +2 -0
  10. package/demo/apollo-v0/assets/css/landing/tokens.css +35 -0
  11. package/demo/apollo-v0/assets/css/next-core.css +16124 -0
  12. package/demo/apollo-v0/assets/images/1x1_1.svg +19 -0
  13. package/demo/apollo-v0/assets/images/1x1_2.svg +19 -0
  14. package/demo/apollo-v0/assets/images/affirm-logo.svg +24 -0
  15. package/demo/apollo-v0/assets/images/apple-pay-logo.svg +4 -0
  16. package/demo/apollo-v0/assets/images/bancontact-logo.svg +1 -0
  17. package/demo/apollo-v0/assets/images/cc-visa.svg +28 -0
  18. package/demo/apollo-v0/assets/images/cc_amex.svg +20 -0
  19. package/demo/apollo-v0/assets/images/cc_discover.svg +21 -0
  20. package/demo/apollo-v0/assets/images/cc_master.svg +22 -0
  21. package/demo/apollo-v0/assets/images/credit-card-flags.svg +38 -0
  22. package/demo/apollo-v0/assets/images/demo-inline-40788e52a7c75b78.svg +1 -0
  23. package/demo/apollo-v0/assets/images/demo-inline-686e73c8840a0f0a.svg +1 -0
  24. package/demo/apollo-v0/assets/images/google-pay-logo.svg +7 -0
  25. package/demo/apollo-v0/assets/images/guarantee-badge.png +0 -0
  26. package/demo/apollo-v0/assets/images/icon-dollar.svg +5 -0
  27. package/demo/apollo-v0/assets/images/icon-guarantee.svg +5 -0
  28. package/demo/apollo-v0/assets/images/icon-shipping.svg +5 -0
  29. package/demo/apollo-v0/assets/images/icons8-lock-24_1icons8-lock-24.png +0 -0
  30. package/demo/apollo-v0/assets/images/ideal-logo.svg +30 -0
  31. package/demo/apollo-v0/assets/images/klarna-logo.svg +9 -0
  32. package/demo/apollo-v0/assets/images/landing/_shared/16x9.svg +19 -0
  33. package/demo/apollo-v0/assets/images/landing/_shared/1x1_1.svg +19 -0
  34. package/demo/apollo-v0/assets/images/landing/_shared/4x3.svg +19 -0
  35. package/demo/apollo-v0/assets/images/landing/_shared/arrow-right.svg +6 -0
  36. package/demo/apollo-v0/assets/images/landing/_shared/cta-guarantee-icon.png +0 -0
  37. package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-icon-1.svg +8 -0
  38. package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-icon-2.svg +10 -0
  39. package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-icon-3.svg +13 -0
  40. package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-icon-4.svg +11 -0
  41. package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-quote.svg +6 -0
  42. package/demo/apollo-v0/assets/images/landing/benefits-2/benefits-2-verified.svg +10 -0
  43. package/demo/apollo-v0/assets/images/landing/bottomcta-1/check-bullet.svg +5 -0
  44. package/demo/apollo-v0/assets/images/landing/faq-1/faq-chevron.svg +6 -0
  45. package/demo/apollo-v0/assets/images/landing/footer-1/footer-logo.png +0 -0
  46. package/demo/apollo-v0/assets/images/landing/guarantee-1/guarantee-1-badge.svg +8 -0
  47. package/demo/apollo-v0/assets/images/landing/hero-1/icon-check.svg +5 -0
  48. package/demo/apollo-v0/assets/images/landing/hero-1/icon-star.svg +6 -0
  49. package/demo/apollo-v0/assets/images/landing/hero-1/icon-verified.svg +6 -0
  50. package/demo/apollo-v0/assets/images/landing/icons-5/icon-batteries.svg +7 -0
  51. package/demo/apollo-v0/assets/images/landing/icons-5/icon-cuff-checking.svg +6 -0
  52. package/demo/apollo-v0/assets/images/landing/icons-5/icon-dual-user.svg +5 -0
  53. package/demo/apollo-v0/assets/images/landing/icons-5/icon-fda-cleared.svg +5 -0
  54. package/demo/apollo-v0/assets/images/landing/icons-5/icon-heartbeat.svg +17 -0
  55. package/demo/apollo-v0/assets/images/landing/icons-5/icon-lcd-display.svg +10 -0
  56. package/demo/apollo-v0/assets/images/landing/icons-5/icon-movement-error.svg +7 -0
  57. package/demo/apollo-v0/assets/images/landing/icons-5/icon-portable.png +0 -0
  58. package/demo/apollo-v0/assets/images/landing/icons-5/icon-reading-memory.svg +6 -0
  59. package/demo/apollo-v0/assets/images/landing/icons-5/icon-wrist-comfort.svg +5 -0
  60. package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-card-1.jpg +0 -0
  61. package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-card-2.jpg +0 -0
  62. package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-card-3.jpg +0 -0
  63. package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-card-4.jpg +0 -0
  64. package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-1.png +0 -0
  65. package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-2.png +0 -0
  66. package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-3.png +0 -0
  67. package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-4.png +0 -0
  68. package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-5.png +0 -0
  69. package/demo/apollo-v0/assets/images/landing/ingredients-3/ingredients-3-promise-6.png +0 -0
  70. package/demo/apollo-v0/assets/images/landing/nav-1/flag-us.png +0 -0
  71. package/demo/apollo-v0/assets/images/landing/reviews-3/star-card.svg +3 -0
  72. package/demo/apollo-v0/assets/images/landing/reviews-3/star-lg.svg +3 -0
  73. package/demo/apollo-v0/assets/images/landing/reviews-3/star-sm.svg +3 -0
  74. package/demo/apollo-v0/assets/images/landing/reviews-3/verified.svg +10 -0
  75. package/demo/apollo-v0/assets/images/landing/testimonials-2/testimonials-2-reactions.svg +9 -0
  76. package/demo/apollo-v0/assets/images/landing/testimonials-2/testimonials-2-stars.svg +6 -0
  77. package/demo/apollo-v0/assets/images/link-logo.svg +1 -0
  78. package/demo/apollo-v0/assets/images/next-dark.svg +8 -0
  79. package/demo/apollo-v0/assets/images/paypal-logo.svg +5 -0
  80. package/demo/apollo-v0/assets/images/paypal-txt.svg +8 -0
  81. package/demo/apollo-v0/assets/images/paypal.svg +22 -0
  82. package/demo/apollo-v0/assets/images/sepa-logo.svg +275 -0
  83. package/demo/apollo-v0/assets/images/twint-logo.svg +1 -0
  84. package/demo/apollo-v0/assets/images/united-states-flag-icon.webp +0 -0
  85. package/demo/apollo-v0/assets/images/upsell-payment-logos.svg +38 -0
  86. package/demo/apollo-v0/assets/images/usps.png +0 -0
  87. package/demo/apollo-v0/checkout/index.html +1370 -0
  88. package/demo/apollo-v0/landing/index.html +1836 -0
  89. package/demo/apollo-v0/provenance.json +342 -0
  90. package/demo/apollo-v0/receipt/index.html +292 -0
  91. package/demo/apollo-v0/upsell-bundle-stepper/index.html +401 -0
  92. package/docs/activation-and-evidence.md +37 -0
  93. package/docs/demo-preview.md +63 -0
  94. package/docs/diagnostics.md +55 -0
  95. package/docs/orientation-contract-reference.md +2 -1
  96. package/docs/progress-snapshots.md +142 -0
  97. package/docs/runtime-readiness.md +1 -1
  98. package/docs/supported-surface.md +13 -1
  99. package/package.json +13 -3
  100. package/schemas/campaign-runtime-build-context.v0.schema.json +42 -0
  101. package/schemas/campaigns-os-progress-snapshot.v0.schema.json +370 -0
  102. package/skills/next-campaigns-build/SKILL.md +17 -1
  103. package/skills/next-campaigns-os/SKILL.md +19 -3
  104. package/skills/next-campaigns-os-setup/SKILL.md +17 -1
  105. package/skills/next-campaigns-polish/SKILL.md +18 -2
  106. package/skills/next-campaigns-qa/SKILL.md +24 -8
  107. package/skills.json +5 -5
  108. package/src/cli.mjs +74 -4
  109. package/src/consent.mjs +2 -2
  110. package/src/demo-artifact.mjs +97 -0
  111. package/src/demo.mjs +85 -0
  112. package/src/diagnostic.mjs +101 -0
  113. package/src/install-mode.mjs +13 -1
  114. package/src/progress-node.mjs +177 -0
  115. package/src/progress.mjs +133 -0
@@ -1,11 +1,27 @@
1
1
  ---
2
2
  name: next-campaigns-os
3
- version: 1.0.16
3
+ version: 1.0.17
4
4
  description: Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth.
5
5
  ---
6
6
 
7
7
  # Campaigns OS
8
8
 
9
+ ## Installed toolkit commands
10
+
11
+ Run from the campaign folder with an exact project-local devDependency and
12
+ committed lockfile. Orient on reviewed source before installation; check release
13
+ provenance or pin the full reviewed Git SHA. Preflight with `npx campaigns-os
14
+ tooling status --platform <claude|codex>` and refresh bundled skills for the same
15
+ profile. Use the invocation printed by status and `next` to avoid PATH shadowing.
16
+
17
+ In the instructions below, bare `campaigns-os …` means `npx campaigns-os …`
18
+ from that campaign folder. Global-only users substitute the global copy's printed invocation for
19
+ each `npx campaigns-os` example; toolkit contributors translate to `npm run campaigns-os -- …` in
20
+ the toolkit checkout. Browser installation is `npx campaigns-os qa
21
+ install-browser`, not a campaign npm script. `tooling diagnose --packet <p>
22
+ --json` provides a redacted support export without changing retained evidence.
23
+
24
+
9
25
  Use this skill to orient a campaign build, run preflight, decide the next stage, and keep the lifecycle honest.
10
26
 
11
27
  ## Public Lifecycle Boundary
@@ -32,7 +48,7 @@ Workflow:
32
48
  7. If doctor's `next` block says `doctor-blocked` or `prepare-build` (it names the same stage `campaigns-os next` would), stop and resolve the named blockers.
33
49
  8. If doctor returns `build`, hand off with `campaigns-os next build --packet <packet>` and follow `next-campaigns-build`'s recommended **build → independent review → repair → verification** loop.
34
50
  9. After build, require polish and a preview deploy before QA. During Polish,
35
- install the package-owned browser once with `npm run qa:install-browser`,
51
+ install the package-owned browser once with `npx campaigns-os qa install-browser`,
36
52
  serve the current build, and run `campaigns-os polish capture --packet <p> --base-url <served-build-url>` before recording a terminal Polish status.
37
53
  The package-owned producer attaches `visual_review.page_load`; never
38
54
  hand-author it. Nonwaivable incomplete evidence blocks. A complete hidden
@@ -41,7 +57,7 @@ Workflow:
41
57
  Doctor/next report `ready_with_waivers`; QA
42
58
  retains each attributed exception as `ready_with_exceptions`, and one
43
59
  exception never suppresses another blocker.
44
- 10. Run the package-owned proof path in sequence: ensure `npm run qa:install-browser` has completed, run `campaigns-os qa resolve --packet <packet>`, then `campaigns-os qa run --packet <packet> --base-url <url> --browser --test-order common`.
60
+ 10. Run the package-owned proof path in sequence: ensure `npx campaigns-os qa install-browser` has completed, run `campaigns-os qa resolve --packet <packet>`, then `campaigns-os qa run --packet <packet> --base-url <url> --browser --test-order common`.
45
61
  11. Treat typed-card proof coverage as the control. Global test cards bypass the gateway and create no transactions, so no permission/approval is needed. `common` runs checkout, first-offer accept and decline, and a deduplicated shortest real receipt path when that adds coverage (at most four orders). `full` walks every actual terminal path in the selected checkout topology; cycles, missing routes, and reachable nonterminals block exhaustive proof before browser launch. The accidental-flood cap remains `6`, and an overflow names the exact explicit `--max-test-orders` raise. Localhost on any port is a Campaigns App Development domain for SDK QA with analytics suppressed; non-localhost preview/production origins still need SDK origin allowlist confirmation.
46
62
  12. Discuss launch only from recorded build, polish, deploy, browser QA, and test-order evidence, or from explicit blockers.
47
63
 
@@ -1,11 +1,27 @@
1
1
  ---
2
2
  name: next-campaigns-os-setup
3
- version: 2.0.0
3
+ version: 2.0.1
4
4
  description: Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly installed as next-campaigns-setup; renamed 2026-08 to stop colliding with the published NextCommerceCo/skills scaffolder of that name.
5
5
  ---
6
6
 
7
7
  # Next Campaigns OS Setup
8
8
 
9
+ ## Installed toolkit commands
10
+
11
+ Run from the campaign folder with an exact project-local devDependency and
12
+ committed lockfile. Orient on reviewed source before installation; check release
13
+ provenance or pin the full reviewed Git SHA. Preflight with `npx campaigns-os
14
+ tooling status --platform <claude|codex>` and refresh bundled skills for the same
15
+ profile. Use the invocation printed by status and `next` to avoid PATH shadowing.
16
+
17
+ In the instructions below, bare `campaigns-os …` means `npx campaigns-os …`
18
+ from that campaign folder. Global-only users substitute the global copy's printed invocation for
19
+ each `npx campaigns-os` example; toolkit contributors translate to `npm run campaigns-os -- …` in
20
+ the toolkit checkout. Browser installation is `npx campaigns-os qa
21
+ install-browser`, not a campaign npm script. `tooling diagnose --packet <p>
22
+ --json` provides a redacted support export without changing retained evidence.
23
+
24
+
9
25
  Use this skill when the Build Packet doctor says setup is required before assembly.
10
26
 
11
27
  Responsibilities:
@@ -1,11 +1,27 @@
1
1
  ---
2
2
  name: next-campaigns-polish
3
- version: 1.1.1
3
+ version: 1.1.2
4
4
  description: Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign.
5
5
  ---
6
6
 
7
7
  # Next Campaigns Polish
8
8
 
9
+ ## Installed toolkit commands
10
+
11
+ Run from the campaign folder with an exact project-local devDependency and
12
+ committed lockfile. Orient on reviewed source before installation; check release
13
+ provenance or pin the full reviewed Git SHA. Preflight with `npx campaigns-os
14
+ tooling status --platform <claude|codex>` and refresh bundled skills for the same
15
+ profile. Use the invocation printed by status and `next` to avoid PATH shadowing.
16
+
17
+ In the instructions below, bare `campaigns-os …` means `npx campaigns-os …`
18
+ from that campaign folder. Global-only users substitute the global copy's printed invocation for
19
+ each `npx campaigns-os` example; toolkit contributors translate to `npm run campaigns-os -- …` in
20
+ the toolkit checkout. Browser installation is `npx campaigns-os qa
21
+ install-browser`, not a campaign npm script. `tooling diagnose --packet <p>
22
+ --json` provides a redacted support export without changing retained evidence.
23
+
24
+
9
25
  Use this after build has produced a runnable page-kit campaign.
10
26
 
11
27
  Theme gate: `campaigns-os next polish` blocks when theme inspect found a
@@ -33,7 +49,7 @@ Responsibilities:
33
49
  - For exit-intent pops and promo-code inputs, polish the wrapper/copy states without breaking SDK coupon/voucher apply hooks or `cart.hasCoupon("CODE")` conditional labels.
34
50
  - If `report.theme` or `context.theme` exists, verify brand-theme load order after `next-core.css`, source-token parity for primary color/CTA/surface/text/font/radius when present, and SDK safety. When the brand layer is missing, stale, low-confidence, or unsafe to apply, record the first repair-loop defect or an explicit skipped reason.
35
51
  - Before recording a terminal Polish status, install the package-owned browser
36
- once with `npm run qa:install-browser`, serve the current build, and run
52
+ once with `npx campaigns-os qa install-browser`, serve the current build, and run
37
53
  `campaigns-os polish capture --packet <packet> --base-url <served-build-url>`.
38
54
  The package captures every mapped route at fixed desktop/mobile viewports and
39
55
  attaches `stages.polish.evidence.visual_review.page_load`. Never hand-author,
@@ -1,26 +1,42 @@
1
1
  ---
2
2
  name: next-campaigns-qa
3
- version: 1.3.1
3
+ version: 1.3.2
4
4
  description: Run spec-aware QA from a Campaign Map ID and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof.
5
5
  ---
6
6
 
7
7
  # Next Campaigns QA
8
8
 
9
+ ## Installed toolkit commands
10
+
11
+ Run from the campaign folder with an exact project-local devDependency and
12
+ committed lockfile. Orient on reviewed source before installation; check release
13
+ provenance or pin the full reviewed Git SHA. Preflight with `npx campaigns-os
14
+ tooling status --platform <claude|codex>` and refresh bundled skills for the same
15
+ profile. Use the invocation printed by status and `next` to avoid PATH shadowing.
16
+
17
+ In the instructions below, bare `campaigns-os …` means `npx campaigns-os …`
18
+ from that campaign folder. Global-only users substitute the global copy's printed invocation for
19
+ each `npx campaigns-os` example; toolkit contributors translate to `npm run campaigns-os -- …` in
20
+ the toolkit checkout. Browser installation is `npx campaigns-os qa
21
+ install-browser`, not a campaign npm script. `tooling diagnose --packet <p>
22
+ --json` provides a redacted support export without changing retained evidence.
23
+
24
+
9
25
  Use this after the campaign has a preview or production URL and the assembly report records build and polish status. The public v0 runner is Node/npm-based, with an owned Playwright browser pass:
10
26
 
11
27
  ```bash
12
- npm run qa:install-browser
13
- npm run campaigns-os -- qa resolve --packet campaign-runtime.build.json
14
- npm run campaigns-os -- qa run --packet campaign-runtime.build.json --base-url <preview-url>
28
+ npx campaigns-os qa install-browser
29
+ npx campaigns-os qa resolve --packet campaign-runtime.build.json
30
+ npx campaigns-os qa run --packet campaign-runtime.build.json --base-url <preview-url>
15
31
  # Fixture-driven migration parity proof. Publishes to the QA portal by default.
16
- npm run campaigns-os -- qa parity --fixture <parity-fixture.json> --scenario <scenario-id> --base-url <preview-url>
32
+ npx campaigns-os qa parity --fixture <parity-fixture.json> --scenario <scenario-id> --base-url <preview-url>
17
33
  # Browser QA + typed-card proof. Publishes to the QA portal by default and prints the portal link.
18
- npm run campaigns-os -- qa run --packet campaign-runtime.build.json --base-url <preview-url> --browser --test-order common
34
+ npx campaigns-os qa run --packet campaign-runtime.build.json --base-url <preview-url> --browser --test-order common
19
35
  # Offline / dev / CI only: keep the verdict local
20
- npm run campaigns-os -- qa run --packet campaign-runtime.build.json --base-url <preview-url> --browser --test-order common --no-post-verdict
36
+ npx campaigns-os qa run --packet campaign-runtime.build.json --base-url <preview-url> --browser --test-order common --no-post-verdict
21
37
  ```
22
38
 
23
- `npm run qa:install-browser` is part of the standard QA sequence. Run it once
39
+ `npx campaigns-os qa install-browser` is part of the standard QA sequence. Run it once
24
40
  after install/update before using `--browser` or `--test-order`; do not skip it
25
41
  unless the local Playwright browser binary is already installed.
26
42
 
package/skills.json CHANGED
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "id": "next-campaigns-os",
17
17
  "name": "Campaigns OS Lifecycle",
18
- "version": "1.0.16",
18
+ "version": "1.0.17",
19
19
  "path": "skills/next-campaigns-os/SKILL.md",
20
20
  "domain": "campaigns",
21
21
  "description": "Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth."
@@ -23,7 +23,7 @@
23
23
  {
24
24
  "id": "next-campaigns-os-setup",
25
25
  "name": "Campaigns OS Setup",
26
- "version": "2.0.0",
26
+ "version": "2.0.1",
27
27
  "path": "skills/next-campaigns-os-setup/SKILL.md",
28
28
  "domain": "campaigns",
29
29
  "description": "Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly next-campaigns-setup; renamed to release that name to the published NextCommerceCo/skills scaffolder."
@@ -31,7 +31,7 @@
31
31
  {
32
32
  "id": "next-campaigns-build",
33
33
  "name": "Campaign Build",
34
- "version": "1.0.2",
34
+ "version": "1.0.3",
35
35
  "path": "skills/next-campaigns-build/SKILL.md",
36
36
  "domain": "campaigns",
37
37
  "description": "Assemble a NEXT campaign from a doctor-cleared Build Packet, CampaignSpec/API values, prepared HTML/assets, page-kit, and starter-template contracts."
@@ -39,7 +39,7 @@
39
39
  {
40
40
  "id": "next-campaigns-polish",
41
41
  "name": "Campaign Polish",
42
- "version": "1.1.1",
42
+ "version": "1.1.2",
43
43
  "path": "skills/next-campaigns-polish/SKILL.md",
44
44
  "domain": "campaigns",
45
45
  "description": "Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign."
@@ -47,7 +47,7 @@
47
47
  {
48
48
  "id": "next-campaigns-qa",
49
49
  "name": "Campaign QA",
50
- "version": "1.3.1",
50
+ "version": "1.3.2",
51
51
  "path": "skills/next-campaigns-qa/SKILL.md",
52
52
  "domain": "campaigns",
53
53
  "description": "Run spec-aware QA from a Campaign Map ID and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof."
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) recordQaStageOutcome(args, result);
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 === "npx_cache"
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
+ }