@legionworks/facet 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +23 -0
- package/README.md +158 -0
- package/dist/gallery/chunk-4g8rem85.css +656 -0
- package/dist/gallery/chunk-c6s9k9ty.js +5970 -0
- package/dist/gallery/frame/artifact.css +8424 -0
- package/dist/gallery/frame/chunks/abnfDiagram-N423BO3Z-jj74szbm.js +155 -0
- package/dist/gallery/frame/chunks/architecture-TIHT7OUA-cvdjn7dt.js +10 -0
- package/dist/gallery/frame/chunks/architectureDiagram-T3A2C74G-n4z77eg6.js +8675 -0
- package/dist/gallery/frame/chunks/blockDiagram-VBNYF7ZC-7xsqzrd1.js +3529 -0
- package/dist/gallery/frame/chunks/c4Diagram-5PPSVZJV-z2zjs7wx.js +2413 -0
- package/dist/gallery/frame/chunks/classDiagram-JCYQIIEL-ve8rngqp.js +47 -0
- package/dist/gallery/frame/chunks/classDiagram-v2-OCEON4UE-8cfaxb8z.js +47 -0
- package/dist/gallery/frame/chunks/cose-bilkent-JH36ORCC-sn92qexx.js +4822 -0
- package/dist/gallery/frame/chunks/cynefin-VYW2F7L2-tqjv913d.js +10 -0
- package/dist/gallery/frame/chunks/cynefinDiagram-MW4NZA55-c1c0dj12.js +500 -0
- package/dist/gallery/frame/chunks/dagre-VZM6K2ZE-7tyg4yt2.js +476 -0
- package/dist/gallery/frame/chunks/diagram-7IWD3JNH-y4eng9sc.js +526 -0
- package/dist/gallery/frame/chunks/diagram-B4RE2ZJO-rn3p7j5h.js +673 -0
- package/dist/gallery/frame/chunks/diagram-LBJQPF4R-w4a2cm9q.js +255 -0
- package/dist/gallery/frame/chunks/diagram-Q27KOJAE-aqc2v7wy.js +575 -0
- package/dist/gallery/frame/chunks/diagram-UB23O5K3-xncy8nyd.js +355 -0
- package/dist/gallery/frame/chunks/ebnfDiagram-BXEA7PRR-acwgxz16.js +175 -0
- package/dist/gallery/frame/chunks/erDiagram-JOGREHBK-mzp5nswt.js +1332 -0
- package/dist/gallery/frame/chunks/eventmodeling-45OFAUF4-sg07qr0c.js +10 -0
- package/dist/gallery/frame/chunks/flowDiagram-UKHOOZJN-2e5ygs44.js +29 -0
- package/dist/gallery/frame/chunks/ganttDiagram-PKOTCBZU-p2hhh7hr.js +2620 -0
- package/dist/gallery/frame/chunks/gitGraph-TEB2WS4Q-j6kw0naa.js +10 -0
- package/dist/gallery/frame/chunks/gitGraphDiagram-DS77QQ5N-4z9z8wjm.js +1347 -0
- package/dist/gallery/frame/chunks/info-DKCQHKI2-mpt80jd3.js +10 -0
- package/dist/gallery/frame/chunks/infoDiagram-6WML65LV-5rqm3mgg.js +65 -0
- package/dist/gallery/frame/chunks/ishikawaDiagram-WSZJBQD7-b51z96jy.js +975 -0
- package/dist/gallery/frame/chunks/journeyDiagram-NVQOT4AX-9jbfp0pk.js +1272 -0
- package/dist/gallery/frame/chunks/kanban-definition-27J2QSJJ-4x0vx4se.js +1116 -0
- package/dist/gallery/frame/chunks/katex-xfgk95tv.js +14034 -0
- package/dist/gallery/frame/chunks/markdown-1s5vwkf2.js +9 -0
- package/dist/gallery/frame/chunks/markdown-2h6bwsms.js +3239 -0
- package/dist/gallery/frame/chunks/markdown-2x113yh8.js +107 -0
- package/dist/gallery/frame/chunks/markdown-30b7rwmp.js +1943 -0
- package/dist/gallery/frame/chunks/markdown-356bfj4g.js +2031 -0
- package/dist/gallery/frame/chunks/markdown-44n9ea69.js +2406 -0
- package/dist/gallery/frame/chunks/markdown-56enrf2r.js +39 -0
- package/dist/gallery/frame/chunks/markdown-5avymm8t.js +725 -0
- package/dist/gallery/frame/chunks/markdown-6r4svars.js +33 -0
- package/dist/gallery/frame/chunks/markdown-6zz8efx2.js +24 -0
- package/dist/gallery/frame/chunks/markdown-7jp6prnb.js +19 -0
- package/dist/gallery/frame/chunks/markdown-809k7q41.js +55 -0
- package/dist/gallery/frame/chunks/markdown-82xmzhzd.js +1076 -0
- package/dist/gallery/frame/chunks/markdown-92n37gd4.js +7627 -0
- package/dist/gallery/frame/chunks/markdown-ag2sssf8.js +36 -0
- package/dist/gallery/frame/chunks/markdown-b4s7ntq2.js +19 -0
- package/dist/gallery/frame/chunks/markdown-c9k0fvp1.js +100 -0
- package/dist/gallery/frame/chunks/markdown-ceefymn8.js +125 -0
- package/dist/gallery/frame/chunks/markdown-d1wfbs70.js +243 -0
- package/dist/gallery/frame/chunks/markdown-dy5sgjhc.js +11022 -0
- package/dist/gallery/frame/chunks/markdown-dz3z5w30.js +30868 -0
- package/dist/gallery/frame/chunks/markdown-eg6ngz7y.js +30340 -0
- package/dist/gallery/frame/chunks/markdown-ff5wvb43.js +1149 -0
- package/dist/gallery/frame/chunks/markdown-ffg06nfz.js +86 -0
- package/dist/gallery/frame/chunks/markdown-fvfncadf.js +472 -0
- package/dist/gallery/frame/chunks/markdown-gvyszqpv.js +19 -0
- package/dist/gallery/frame/chunks/markdown-gx1p0ew0.js +88 -0
- package/dist/gallery/frame/chunks/markdown-gxye5frz.js +65 -0
- package/dist/gallery/frame/chunks/markdown-h7pzv34e.js +105 -0
- package/dist/gallery/frame/chunks/markdown-hab789t9.js +47 -0
- package/dist/gallery/frame/chunks/markdown-hjp6sj8t.js +125 -0
- package/dist/gallery/frame/chunks/markdown-jka64qgp.js +1993 -0
- package/dist/gallery/frame/chunks/markdown-k8xkn8mw.js +56 -0
- package/dist/gallery/frame/chunks/markdown-m7frq8x8.js +112 -0
- package/dist/gallery/frame/chunks/markdown-p740v4qm.js +36 -0
- package/dist/gallery/frame/chunks/markdown-pxfeh231.js +57 -0
- package/dist/gallery/frame/chunks/markdown-pzgd8z3s.js +87 -0
- package/dist/gallery/frame/chunks/markdown-qx3cngz2.js +1673 -0
- package/dist/gallery/frame/chunks/markdown-rawhfvvc.js +85 -0
- package/dist/gallery/frame/chunks/markdown-rd8w0ng9.js +49 -0
- package/dist/gallery/frame/chunks/markdown-rm2yxfjj.js +2773 -0
- package/dist/gallery/frame/chunks/markdown-s1jatrnk.js +993 -0
- package/dist/gallery/frame/chunks/markdown-sgfg5vvq.js +59 -0
- package/dist/gallery/frame/chunks/markdown-t4ethv9d.js +409 -0
- package/dist/gallery/frame/chunks/markdown-tnmabqvz.js +36 -0
- package/dist/gallery/frame/chunks/markdown-tvdpppw8.js +5292 -0
- package/dist/gallery/frame/chunks/markdown-v0x27twr.js +22 -0
- package/dist/gallery/frame/chunks/markdown-v14td68x.js +36 -0
- package/dist/gallery/frame/chunks/markdown-w6sgd54z.js +36 -0
- package/dist/gallery/frame/chunks/markdown-xw8dsk8b.js +2125 -0
- package/dist/gallery/frame/chunks/markdown-y18mve73.js +85 -0
- package/dist/gallery/frame/chunks/markdown-yengsms5.js +427 -0
- package/dist/gallery/frame/chunks/markdown-z1wc0v0d.js +1826 -0
- package/dist/gallery/frame/chunks/markdown-zazx9h4n.js +329 -0
- package/dist/gallery/frame/chunks/mermaid-de7p4v0x.js +30 -0
- package/dist/gallery/frame/chunks/mindmap-definition-FAOFIHXS-ra44cbem.js +1216 -0
- package/dist/gallery/frame/chunks/packet-7NZHBO7P-xppm5913.js +10 -0
- package/dist/gallery/frame/chunks/pegDiagram-VL7TDLO6-a2a4wg03.js +163 -0
- package/dist/gallery/frame/chunks/pie-RZYD4A2V-hnbxk152.js +10 -0
- package/dist/gallery/frame/chunks/pieDiagram-7S7Q4E2Y-f1vxjtmx.js +311 -0
- package/dist/gallery/frame/chunks/quadrantDiagram-CIZ2JOQS-ywafa6kh.js +1390 -0
- package/dist/gallery/frame/chunks/radar-I7S5WNFK-ewqd27se.js +10 -0
- package/dist/gallery/frame/chunks/railroad-3IZDKUUU-5hy4bdx3.js +10 -0
- package/dist/gallery/frame/chunks/railroad-abnf-AHOZXSZD-bh5sxz0d.js +10 -0
- package/dist/gallery/frame/chunks/railroad-ebnf-EBAXGLYW-6b9c3zeh.js +10 -0
- package/dist/gallery/frame/chunks/railroad-peg-LSFZ7HO6-avc8v39s.js +10 -0
- package/dist/gallery/frame/chunks/railroadDiagram-AXF67PYL-qc61wax4.js +131 -0
- package/dist/gallery/frame/chunks/requirementDiagram-LRYGKXZP-9bs4786c.js +1295 -0
- package/dist/gallery/frame/chunks/sankeyDiagram-W5VNT64P-awt1em8b.js +1339 -0
- package/dist/gallery/frame/chunks/sequenceDiagram-SI44F4Z6-qaf9mz3t.js +4324 -0
- package/dist/gallery/frame/chunks/sizeCapture-X5ZJPWSS-9fq685xd.js +64 -0
- package/dist/gallery/frame/chunks/stateDiagram-OKZ733FA-vx7a7hh0.js +456 -0
- package/dist/gallery/frame/chunks/stateDiagram-v2-UEYNNEHI-x4m6ws5t.js +46 -0
- package/dist/gallery/frame/chunks/swimlanes-SLNWSIFB-e4mxm1gh.js +8287 -0
- package/dist/gallery/frame/chunks/swimlanesDiagram-ULZ7WXOC-0h93e2qq.js +42 -0
- package/dist/gallery/frame/chunks/timeline-definition-Z64GVDOM-wp0bhbzg.js +1549 -0
- package/dist/gallery/frame/chunks/treeView-QDETBFTQ-hcx7n0vt.js +10 -0
- package/dist/gallery/frame/chunks/treemap-6X3UGDF4-av02skdw.js +10 -0
- package/dist/gallery/frame/chunks/vennDiagram-T6HMQDX7-pmvk1p7n.js +2566 -0
- package/dist/gallery/frame/chunks/wardley-OPB4EBWU-njsp6e40.js +10 -0
- package/dist/gallery/frame/chunks/wardleyDiagram-T6FBY63Y-eggmnv4z.js +979 -0
- package/dist/gallery/frame/chunks/xychartDiagram-ELKLHX3M-q40tnpe5.js +1954 -0
- package/dist/gallery/frame/frame.css +183 -0
- package/dist/gallery/frame/runtime/chart.js +42744 -0
- package/dist/gallery/frame/runtime/html.js +14 -0
- package/dist/gallery/frame/runtime/markdown.js +1403 -0
- package/dist/gallery/frame/runtime/mermaid.js +32 -0
- package/dist/gallery/frame/runtime/svg.js +14 -0
- package/dist/gallery/frame/runtime/tsx.js +52 -0
- package/dist/gallery/index.html +104 -0
- package/docs/reference/cli.md +163 -0
- package/docs/reference/export.md +106 -0
- package/docs/reference/html.md +215 -0
- package/docs/reference/http.md +85 -0
- package/docs/reference/mcp.md +78 -0
- package/docs/reference/security.md +119 -0
- package/docs/reference/storage.md +93 -0
- package/docs/reference/tsx.md +101 -0
- package/docs/reference/validation.md +212 -0
- package/package.json +89 -0
- package/scripts/build-gallery.ts +127 -0
- package/scripts/launch-netns.sh +67 -0
- package/skills/facet/SKILL.md +100 -0
- package/src/cli/client.ts +311 -0
- package/src/cli/commands/create.ts +49 -0
- package/src/cli/commands/doctor.ts +249 -0
- package/src/cli/commands/export.ts +179 -0
- package/src/cli/commands/instantiate.ts +35 -0
- package/src/cli/commands/list.ts +49 -0
- package/src/cli/commands/open.ts +75 -0
- package/src/cli/commands/pin.ts +39 -0
- package/src/cli/commands/promote.ts +49 -0
- package/src/cli/commands/publish.ts +122 -0
- package/src/cli/commands/read-back.ts +56 -0
- package/src/cli/commands/status.ts +114 -0
- package/src/cli/commands/watch.ts +146 -0
- package/src/cli/main.ts +522 -0
- package/src/cli/output.ts +106 -0
- package/src/cli/parser.ts +376 -0
- package/src/cli/presenter.ts +212 -0
- package/src/cli/service-metadata.ts +134 -0
- package/src/cli/spawn-service.ts +326 -0
- package/src/css.d.ts +4 -0
- package/src/gallery-web/app.ts +1581 -0
- package/src/gallery-web/export-menu.ts +121 -0
- package/src/gallery-web/export.ts +112 -0
- package/src/gallery-web/favicon.ts +54 -0
- package/src/gallery-web/frame/entries/chart.ts +6 -0
- package/src/gallery-web/frame/entries/html.ts +6 -0
- package/src/gallery-web/frame/entries/markdown.ts +6 -0
- package/src/gallery-web/frame/entries/mermaid.ts +6 -0
- package/src/gallery-web/frame/entries/svg.ts +6 -0
- package/src/gallery-web/frame/entries/tsx.ts +6 -0
- package/src/gallery-web/frame/frame-payload.ts +91 -0
- package/src/gallery-web/frame/renderer-validation.ts +13 -0
- package/src/gallery-web/frame/renderers/chart.ts +182 -0
- package/src/gallery-web/frame/renderers/dompurify-shim.ts +112 -0
- package/src/gallery-web/frame/renderers/html.ts +48 -0
- package/src/gallery-web/frame/renderers/markdown.ts +81 -0
- package/src/gallery-web/frame/renderers/mermaid.ts +120 -0
- package/src/gallery-web/frame/renderers/registry.ts +260 -0
- package/src/gallery-web/frame/renderers/svg.ts +393 -0
- package/src/gallery-web/frame/renderers/tsx.ts +61 -0
- package/src/gallery-web/frame/runtime.ts +501 -0
- package/src/gallery-web/frame/styles/artifact.css +8424 -0
- package/src/gallery-web/frame/styles/frame.css +183 -0
- package/src/gallery-web/frame/styles/html-source.css +23 -0
- package/src/gallery-web/frame/view-box.ts +33 -0
- package/src/gallery-web/frame-html.ts +72 -0
- package/src/gallery-web/index.html +104 -0
- package/src/gallery-web/session.ts +107 -0
- package/src/gallery-web/sse-client.ts +212 -0
- package/src/gallery-web/styles/app.css +420 -0
- package/src/gallery-web/styles/tokens.css +112 -0
- package/src/gallery-web/styles/verdict.css +186 -0
- package/src/gallery-web/swap.ts +64 -0
- package/src/gallery-web/theme.ts +40 -0
- package/src/gallery-web/view-state.ts +93 -0
- package/src/harness-adapters/claude-code/README.md +10 -0
- package/src/harness-adapters/claude-code/facet.sh +4 -0
- package/src/harness-adapters/codex/README.md +10 -0
- package/src/harness-adapters/codex/facet.sh +4 -0
- package/src/harness-adapters/mcp/README.md +13 -0
- package/src/harness-adapters/mcp/cli-bridge.ts +216 -0
- package/src/harness-adapters/mcp/main.ts +15 -0
- package/src/harness-adapters/mcp/server.ts +164 -0
- package/src/harness-adapters/mcp/tool-schemas.ts +53 -0
- package/src/harness-adapters/opencode/README.md +10 -0
- package/src/harness-adapters/opencode/facet.sh +4 -0
- package/src/runtime/compiled-entrypoints.ts +69 -0
- package/src/service/dispatcher.ts +717 -0
- package/src/service/export.ts +218 -0
- package/src/service/http-utils.ts +55 -0
- package/src/service/lexical/expectations.ts +210 -0
- package/src/service/lifecycle/idle-controller.ts +125 -0
- package/src/service/lifecycle/orphan-cleanup.ts +128 -0
- package/src/service/lifecycle/process-lock.ts +235 -0
- package/src/service/main.ts +136 -0
- package/src/service/process.ts +240 -0
- package/src/service/router-guards.ts +239 -0
- package/src/service/router.ts +627 -0
- package/src/service/security/auth.ts +168 -0
- package/src/service/security/host-origin.ts +138 -0
- package/src/service/security/http-guards.ts +13 -0
- package/src/service/security/leases.ts +182 -0
- package/src/service/security/token-store.ts +138 -0
- package/src/service/server.ts +356 -0
- package/src/service/store/database.ts +61 -0
- package/src/service/store/evidence-retention.ts +234 -0
- package/src/service/store/migrations.ts +167 -0
- package/src/service/store/repository-lifecycle.ts +121 -0
- package/src/service/store/repository.ts +732 -0
- package/src/service/store/schema.ts +218 -0
- package/src/service/stored-verdict.ts +97 -0
- package/src/service/stream.ts +301 -0
- package/src/service/verdict-enrichment.ts +45 -0
- package/src/shared/build/frame-bundle-plugins.ts +61 -0
- package/src/shared/build-mode.ts +28 -0
- package/src/shared/config/evidence-read.ts +98 -0
- package/src/shared/config/limits.ts +56 -0
- package/src/shared/config/paths.ts +96 -0
- package/src/shared/contracts/artifact-types.ts +2 -0
- package/src/shared/contracts/artifact.ts +98 -0
- package/src/shared/contracts/commands/_shared.ts +59 -0
- package/src/shared/contracts/commands/guards.ts +86 -0
- package/src/shared/contracts/commands/index.ts +204 -0
- package/src/shared/contracts/commands/names.ts +35 -0
- package/src/shared/contracts/commands/requests.ts +149 -0
- package/src/shared/contracts/commands/results.ts +200 -0
- package/src/shared/contracts/envelope.ts +140 -0
- package/src/shared/contracts/events.ts +41 -0
- package/src/shared/contracts/observed-counts.ts +19 -0
- package/src/shared/contracts/renderers.ts +6 -0
- package/src/shared/contracts/validation.ts +296 -0
- package/src/shared/errors/facet-error.ts +127 -0
- package/src/shared/errors/store-error.ts +68 -0
- package/src/shared/evidence-image.ts +37 -0
- package/src/shared/export.ts +31 -0
- package/src/shared/html/policy.ts +108 -0
- package/src/shared/html/style-vocabulary.ts +352 -0
- package/src/shared/logging/logger.ts +108 -0
- package/src/shared/logging/redact.ts +49 -0
- package/src/shared/security/frozen-csp.ts +13 -0
- package/src/shared/storage-version.ts +2 -0
- package/src/shared/tsx/execution.ts +29 -0
- package/src/shared/tsx/import-policy.ts +94 -0
- package/src/shared/util/dir-permissions.ts +94 -0
- package/src/shared/util/mermaid-nodes.ts +230 -0
- package/src/shared/util/process.ts +67 -0
- package/src/shared/util/time.ts +21 -0
- package/src/shared/version.ts +3 -0
- package/src/validation/sandbox/limits.ts +41 -0
- package/src/validation/sandbox/netns.ts +177 -0
- package/src/validation/tier0/chart.ts +177 -0
- package/src/validation/tier0/dom-shim.ts +54 -0
- package/src/validation/tier0/html.ts +408 -0
- package/src/validation/tier0/markdown.ts +285 -0
- package/src/validation/tier0/mermaid.ts +98 -0
- package/src/validation/tier0/runner.ts +504 -0
- package/src/validation/tier0/svg.ts +321 -0
- package/src/validation/tier0/tsx/allowlist-resolver.ts +77 -0
- package/src/validation/tier0/tsx/ast-policy-capabilities.ts +238 -0
- package/src/validation/tier0/tsx/ast-policy-shared.ts +71 -0
- package/src/validation/tier0/tsx/ast-policy.ts +181 -0
- package/src/validation/tier0/tsx/compiler.ts +156 -0
- package/src/validation/tier0/worker-dispatch.ts +211 -0
- package/src/validation/tier0/worker-entry.ts +96 -0
- package/src/validation/tier0/worker-input.ts +75 -0
- package/src/validation/tier1/browser-process.ts +147 -0
- package/src/validation/tier1/cdp-pipe.ts +281 -0
- package/src/validation/tier1/entries/chart.ts +5 -0
- package/src/validation/tier1/entries/html.ts +6 -0
- package/src/validation/tier1/entries/markdown.ts +5 -0
- package/src/validation/tier1/entries/mermaid.ts +5 -0
- package/src/validation/tier1/entries/svg.ts +5 -0
- package/src/validation/tier1/entries/tsx.ts +5 -0
- package/src/validation/tier1/frame-target.ts +162 -0
- package/src/validation/tier1/harness-entry.ts +261 -0
- package/src/validation/tier1/harness.ts +215 -0
- package/src/validation/tier1/isolated-probe.ts +94 -0
- package/src/validation/tier1/launcher.ts +177 -0
- package/src/validation/tier1/limits.ts +80 -0
- package/src/validation/tier1/nonce.ts +17 -0
- package/src/validation/tier1/protocol-probe.ts +519 -0
- package/src/validation/tier1/runner.ts +1122 -0
- package/src/validation/tier1/verdict.ts +246 -0
- package/src/validation/tier1/webp.ts +69 -0
- package/templates/README.md +88 -0
- package/templates/bar-compare.vl.json +63 -0
- package/templates/capacity-report.tsx +102 -0
- package/templates/decision-record.md +24 -0
- package/templates/deployment-state.mmd +40 -0
- package/templates/exemplar.md +91 -0
- package/templates/facet-story.tsx +799 -0
- package/templates/fleet-dashboard.html +141 -0
- package/templates/html-release-ledger.html +91 -0
- package/templates/html-status-report.html +114 -0
- package/templates/incident-console.tsx +147 -0
- package/templates/legion-boundaries.mmd +23 -0
- package/templates/legion-flow.mmd +15 -0
- package/templates/legion-sequence.mmd +14 -0
- package/templates/legion-state.mmd +7 -0
- package/templates/metric-card.svg +29 -0
- package/templates/observability-map.svg +101 -0
- package/templates/pipeline-audit.md +108 -0
- package/templates/release-metrics.vl.json +98 -0
- package/templates/service-topology.mmd +47 -0
- package/templates/status-report.md +32 -0
- package/templates/system-map.svg +53 -0
- package/templates/timeseries.vl.json +71 -0
- package/templates/tsx-interactive-counter.tsx +19 -0
- package/templates/tsx-status-report.tsx +31 -0
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entrypoint.
|
|
5
|
+
*
|
|
6
|
+
* `runCli(argv, io)` is the testable core: it parses argv, decides
|
|
7
|
+
* whether the kill switch is active, lazily spawns the service on
|
|
8
|
+
* first call, builds a strict `CommandRequest` for the verb, sends
|
|
9
|
+
* it through the loopback HTTP client, and prints the response
|
|
10
|
+
* envelope to stdout. All diagnostics go to stderr.
|
|
11
|
+
*
|
|
12
|
+
* `main()` is a thin wrapper that calls `runCli(process.argv.slice(2),
|
|
13
|
+
* { stdin, stdout, stderr, env: process.env })` and then
|
|
14
|
+
* `process.exit(code)`. The split keeps tests hermetic — every test
|
|
15
|
+
* can pass its own io bag without ever spawning a real subprocess
|
|
16
|
+
* from the test runner.
|
|
17
|
+
*
|
|
18
|
+
* stdout is reserved for the versioned JSON envelope. `--help` and
|
|
19
|
+
* `--version` honor `--format text|json`; verb calls always print
|
|
20
|
+
* a JSON envelope. The kill switch `FACET=off` is a clean no-op
|
|
21
|
+
* (exit 0, no service spawned, no envelope on stdout).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Readable } from "node:stream";
|
|
25
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
26
|
+
|
|
27
|
+
import { FACET_SCHEMA_VERSION, okEnvelope, type FacetEnvelope } from "../shared/contracts/envelope";
|
|
28
|
+
import type { CommandRequest, CommandResult, CommandName } from "../shared/contracts/commands";
|
|
29
|
+
import { FacetError } from "../shared/errors/facet-error";
|
|
30
|
+
import { FACET_VERSION } from "../shared/version";
|
|
31
|
+
import { isCompiledEntrypointArg, isCompiledRuntime } from "../shared/build-mode";
|
|
32
|
+
|
|
33
|
+
import { parseArgs, renderHelp, type ParsedCommand } from "./parser";
|
|
34
|
+
import { buildVersionEnvelope, buildUsageError, printEnvelope, EXIT_CODES } from "./output";
|
|
35
|
+
import { ensureService, type ServiceHooks } from "./spawn-service";
|
|
36
|
+
import { FacetClient } from "./client";
|
|
37
|
+
|
|
38
|
+
import { buildCreateRequest } from "./commands/create";
|
|
39
|
+
import { buildListRequest } from "./commands/list";
|
|
40
|
+
import { buildOpenRequest, launchDisplay } from "./commands/open";
|
|
41
|
+
import { buildPinRequest } from "./commands/pin";
|
|
42
|
+
import { buildPromoteRequest } from "./commands/promote";
|
|
43
|
+
import { buildInstantiateRequest } from "./commands/instantiate";
|
|
44
|
+
import { buildReadBackRequest } from "./commands/read-back";
|
|
45
|
+
import { buildStatusRequest } from "./commands/status";
|
|
46
|
+
import { collectFacetStatus } from "./commands/status";
|
|
47
|
+
import { computeFacetPaths, legacyXdgEvidenceRoot } from "../shared/config/paths";
|
|
48
|
+
import { readInstallToken, readLiveMetadata } from "./service-metadata";
|
|
49
|
+
import { generateRequestId } from "../shared/util/time";
|
|
50
|
+
import { buildPublishRequest, resolveSourceBytes } from "./commands/publish";
|
|
51
|
+
import { buildExportRequest, resolveExportPaths, writeExportFiles } from "./commands/export";
|
|
52
|
+
import { presentEnvelope, presenterCaps, shouldPresentPretty } from "./presenter";
|
|
53
|
+
import { runDoctor } from "./commands/doctor";
|
|
54
|
+
import { watchPublishFile } from "./commands/watch";
|
|
55
|
+
|
|
56
|
+
export interface CliIo {
|
|
57
|
+
/** A standard Readable stream of bytes — the CLI reads source bytes for `publish` from here. */
|
|
58
|
+
readonly stdin: ReadableStream<Uint8Array> | Readable;
|
|
59
|
+
readonly stdout: { write(chunk: string | Uint8Array): boolean; readonly isTTY?: boolean };
|
|
60
|
+
readonly stderr: { write(chunk: string | Uint8Array): boolean };
|
|
61
|
+
readonly env: NodeJS.ProcessEnv;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function writeEnvelope(
|
|
65
|
+
io: CliIo,
|
|
66
|
+
parsed: ParsedCommand,
|
|
67
|
+
envelope: FacetEnvelope<unknown>,
|
|
68
|
+
): void {
|
|
69
|
+
const routing = {
|
|
70
|
+
isTTY: io.stdout.isTTY === true,
|
|
71
|
+
jsonFlag: parsed.kind === "verb" && (parsed.jsonFlag || parsed.format === "json"),
|
|
72
|
+
env: io.env,
|
|
73
|
+
};
|
|
74
|
+
if (shouldPresentPretty(routing)) {
|
|
75
|
+
for (const line of presentEnvelope(envelope, presenterCaps(routing)))
|
|
76
|
+
io.stdout.write(`${line}\n`);
|
|
77
|
+
} else {
|
|
78
|
+
printEnvelope(io.stdout, envelope);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Test-only side-channels. The production entrypoint never passes
|
|
84
|
+
* these; the test suite uses them to count real spawns and to
|
|
85
|
+
* disable the in-process inflight wait map.
|
|
86
|
+
*/
|
|
87
|
+
export interface CliTestHooks {
|
|
88
|
+
readonly onServiceSpawn?: () => void;
|
|
89
|
+
readonly bypassInflight?: boolean;
|
|
90
|
+
readonly openLauncher?: (url: string) => void | Promise<void>;
|
|
91
|
+
readonly watchSignal?: AbortSignal;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface CliExit {
|
|
95
|
+
readonly code: number;
|
|
96
|
+
/** The pid the CLI spawned the service under, when applicable (null when reusing a live service). */
|
|
97
|
+
readonly spawnedPid: number | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Read all bytes from a ReadableStream or a Node Readable. Used by
|
|
102
|
+
* `publish --file -` and by tests that pass a fixed byte payload.
|
|
103
|
+
*/
|
|
104
|
+
async function readAllStdin(stdin: CliIo["stdin"]): Promise<Uint8Array> {
|
|
105
|
+
// Node Readable path: collect chunks via async iterator.
|
|
106
|
+
if (typeof (stdin as Readable)[Symbol.asyncIterator] === "function") {
|
|
107
|
+
const chunks: Buffer[] = [];
|
|
108
|
+
for await (const chunk of stdin as AsyncIterable<Buffer | Uint8Array>) {
|
|
109
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
110
|
+
}
|
|
111
|
+
return new Uint8Array(Buffer.concat(chunks));
|
|
112
|
+
}
|
|
113
|
+
// Web ReadableStream path.
|
|
114
|
+
const stream = stdin as ReadableStream<Uint8Array>;
|
|
115
|
+
const reader = stream.getReader();
|
|
116
|
+
const chunks: Uint8Array[] = [];
|
|
117
|
+
while (true) {
|
|
118
|
+
const { value, done } = await reader.read();
|
|
119
|
+
if (done) break;
|
|
120
|
+
if (value !== undefined) chunks.push(value);
|
|
121
|
+
}
|
|
122
|
+
let total = 0;
|
|
123
|
+
for (const c of chunks) total += c.length;
|
|
124
|
+
const out = new Uint8Array(total);
|
|
125
|
+
let offset = 0;
|
|
126
|
+
for (const c of chunks) {
|
|
127
|
+
out.set(c, offset);
|
|
128
|
+
offset += c.length;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Dispatch a parsed verb to its builder, send the request, and
|
|
135
|
+
* return the response envelope. Throws on builder-side usage errors
|
|
136
|
+
* (caught by the caller and wrapped in a typed envelope).
|
|
137
|
+
*/
|
|
138
|
+
async function executeVerb(
|
|
139
|
+
verb: Extract<ParsedCommand, { kind: "verb" }> & { verb: CommandName },
|
|
140
|
+
args: Readonly<Record<string, string | boolean>>,
|
|
141
|
+
stdinBytes: Uint8Array,
|
|
142
|
+
resolved: { baseUrl: string; installToken: string; promoteToken?: string },
|
|
143
|
+
openLauncher?: (url: string) => void | Promise<void>,
|
|
144
|
+
cwd = process.cwd(),
|
|
145
|
+
promoteToken?: string,
|
|
146
|
+
sourceBytesOverride?: Uint8Array,
|
|
147
|
+
): Promise<FacetEnvelope<unknown>> {
|
|
148
|
+
const client = new FacetClient({
|
|
149
|
+
baseUrl: resolved.baseUrl,
|
|
150
|
+
installToken: resolved.installToken,
|
|
151
|
+
...(promoteToken === undefined ? {} : { promoteToken }),
|
|
152
|
+
});
|
|
153
|
+
let request: CommandRequest;
|
|
154
|
+
switch (verb.verb) {
|
|
155
|
+
case "create":
|
|
156
|
+
request = buildCreateRequest(args);
|
|
157
|
+
break;
|
|
158
|
+
case "publish": {
|
|
159
|
+
const bytes =
|
|
160
|
+
sourceBytesOverride ??
|
|
161
|
+
resolveSourceBytes({
|
|
162
|
+
fileFlag: typeof args["file"] === "string" ? args["file"] : undefined,
|
|
163
|
+
stdinBytes,
|
|
164
|
+
});
|
|
165
|
+
request = buildPublishRequest(args, bytes);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case "list":
|
|
169
|
+
request = buildListRequest(args);
|
|
170
|
+
break;
|
|
171
|
+
case "readBack":
|
|
172
|
+
request = buildReadBackRequest(args);
|
|
173
|
+
break;
|
|
174
|
+
case "status":
|
|
175
|
+
request = buildStatusRequest(args);
|
|
176
|
+
break;
|
|
177
|
+
case "open":
|
|
178
|
+
request = buildOpenRequest(args);
|
|
179
|
+
break;
|
|
180
|
+
case "promote":
|
|
181
|
+
request = buildPromoteRequest(args);
|
|
182
|
+
break;
|
|
183
|
+
case "instantiate":
|
|
184
|
+
request = buildInstantiateRequest(args);
|
|
185
|
+
break;
|
|
186
|
+
case "pin":
|
|
187
|
+
request = buildPinRequest(args);
|
|
188
|
+
break;
|
|
189
|
+
case "export":
|
|
190
|
+
request = buildExportRequest(args);
|
|
191
|
+
break;
|
|
192
|
+
default: {
|
|
193
|
+
const exhaustive: never = verb.verb;
|
|
194
|
+
void exhaustive;
|
|
195
|
+
throw new FacetError("invalid_request", `Unhandled verb`, { retryable: false });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const response = await client.sendCommand(request);
|
|
199
|
+
if (verb.verb === "export" && response.ok) {
|
|
200
|
+
const result = response.data as Extract<CommandResult, { command: "export" }>;
|
|
201
|
+
const paths = resolveExportPaths(
|
|
202
|
+
result,
|
|
203
|
+
typeof args.out === "string" ? args.out : undefined,
|
|
204
|
+
cwd,
|
|
205
|
+
);
|
|
206
|
+
try {
|
|
207
|
+
writeExportFiles(result, paths, args.force === true);
|
|
208
|
+
} catch (cause) {
|
|
209
|
+
if (cause instanceof FacetError) throw cause;
|
|
210
|
+
throw new FacetError(
|
|
211
|
+
"output_unwritable",
|
|
212
|
+
`Cannot write export output: ${paths.artifactPath}`,
|
|
213
|
+
{
|
|
214
|
+
retryable: false,
|
|
215
|
+
cause,
|
|
216
|
+
details: { out: paths.artifactPath },
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (args["include-bytes"] !== true) {
|
|
221
|
+
const { bytes: _bytes, ...projected } = result;
|
|
222
|
+
void _bytes;
|
|
223
|
+
return {
|
|
224
|
+
...response,
|
|
225
|
+
data: { ...projected, paths, byteCount: Buffer.from(result.bytes, "base64").byteLength },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (verb.verb === "open" && response.ok) {
|
|
230
|
+
const openData = response.data as Extract<CommandResult, { command: "open" }>;
|
|
231
|
+
const launched =
|
|
232
|
+
args["no-launch"] === true
|
|
233
|
+
? false
|
|
234
|
+
: await launchDisplay(
|
|
235
|
+
{ frameUrl: openData.frameUrl, installToken: resolved.installToken },
|
|
236
|
+
openLauncher,
|
|
237
|
+
);
|
|
238
|
+
return { ...response, data: { ...openData, launched } };
|
|
239
|
+
}
|
|
240
|
+
return response;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Top-level CLI driver. Returns the exit code + the pid the CLI
|
|
245
|
+
* spawned (for test assertions); never calls `process.exit` so
|
|
246
|
+
* tests can run end-to-end without a real subprocess.
|
|
247
|
+
*
|
|
248
|
+
* The third arg is test-only and lets the concurrency test count
|
|
249
|
+
* real spawns + disable the in-process inflight wait map.
|
|
250
|
+
*/
|
|
251
|
+
export async function runCli(
|
|
252
|
+
argv: readonly string[],
|
|
253
|
+
io: CliIo,
|
|
254
|
+
testHooks: CliTestHooks = {},
|
|
255
|
+
): Promise<CliExit> {
|
|
256
|
+
// 1. Kill switch: FACET=off is a clean no-op.
|
|
257
|
+
if ((io.env.FACET ?? "").toLowerCase() === "off") {
|
|
258
|
+
return { code: EXIT_CODES.OK, spawnedPid: null };
|
|
259
|
+
}
|
|
260
|
+
const parsed = parseArgs(argv);
|
|
261
|
+
|
|
262
|
+
// 2. Meta commands: --help, --version.
|
|
263
|
+
if (parsed.kind === "help") {
|
|
264
|
+
io.stdout.write(renderHelp(parsed.verb));
|
|
265
|
+
io.stdout.write("\n");
|
|
266
|
+
return { code: EXIT_CODES.OK, spawnedPid: null };
|
|
267
|
+
}
|
|
268
|
+
if (parsed.kind === "version") {
|
|
269
|
+
if (parsed.format === "json") {
|
|
270
|
+
printEnvelope(io.stdout, buildVersionEnvelope(FACET_VERSION, FACET_SCHEMA_VERSION));
|
|
271
|
+
} else {
|
|
272
|
+
io.stdout.write(`facet ${FACET_VERSION} (${FACET_SCHEMA_VERSION})\n`);
|
|
273
|
+
}
|
|
274
|
+
return { code: EXIT_CODES.OK, spawnedPid: null };
|
|
275
|
+
}
|
|
276
|
+
if (parsed.kind === "usage") {
|
|
277
|
+
const env1 = buildUsageError(parsed.message, {
|
|
278
|
+
reason: "usage_error",
|
|
279
|
+
...parsed.details,
|
|
280
|
+
});
|
|
281
|
+
printEnvelope(io.stdout, env1);
|
|
282
|
+
return { code: EXIT_CODES.USAGE, spawnedPid: null };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (parsed.kind === "verb" && parsed.verb === "doctor") {
|
|
286
|
+
const result = runDoctor({
|
|
287
|
+
paths: computeFacetPaths(
|
|
288
|
+
io.env.FACET_HOME === undefined ? {} : { facetHome: io.env.FACET_HOME },
|
|
289
|
+
),
|
|
290
|
+
});
|
|
291
|
+
writeEnvelope(io, parsed, {
|
|
292
|
+
schemaVersion: FACET_SCHEMA_VERSION,
|
|
293
|
+
requestId: generateRequestId(),
|
|
294
|
+
ok: true,
|
|
295
|
+
data: result,
|
|
296
|
+
});
|
|
297
|
+
return { code: result.allPassed ? EXIT_CODES.OK : EXIT_CODES.DOCTOR_FAILED, spawnedPid: null };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (
|
|
301
|
+
parsed.kind === "verb" &&
|
|
302
|
+
parsed.verb === "status" &&
|
|
303
|
+
parsed.args["artifact-id"] === undefined
|
|
304
|
+
) {
|
|
305
|
+
const pathsEnv = io.env.FACET_HOME === undefined ? {} : { facetHome: io.env.FACET_HOME };
|
|
306
|
+
const paths = computeFacetPaths(pathsEnv);
|
|
307
|
+
const legacyEvidenceRoot = legacyXdgEvidenceRoot(pathsEnv);
|
|
308
|
+
try {
|
|
309
|
+
let spawnedPid: number | null = null;
|
|
310
|
+
let resolved: { baseUrl: string; installToken: string } | null = null;
|
|
311
|
+
if (parsed.args.start === true) {
|
|
312
|
+
const started = await ensureService({ env: io.env }, testHooks as ServiceHooks);
|
|
313
|
+
spawnedPid = started.metadata.pid;
|
|
314
|
+
resolved = { baseUrl: started.baseUrl, installToken: started.installToken };
|
|
315
|
+
} else {
|
|
316
|
+
const metadata = readLiveMetadata(paths);
|
|
317
|
+
if (metadata !== null) {
|
|
318
|
+
resolved = {
|
|
319
|
+
baseUrl: `http://127.0.0.1:${metadata.port}`,
|
|
320
|
+
installToken: readInstallToken(paths),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (resolved !== null) {
|
|
325
|
+
const response = await new FacetClient(resolved).sendCommand(buildStatusRequest({}));
|
|
326
|
+
writeEnvelope(io, parsed, response);
|
|
327
|
+
return { code: EXIT_CODES.OK, spawnedPid };
|
|
328
|
+
}
|
|
329
|
+
const data = {
|
|
330
|
+
command: "status" as const,
|
|
331
|
+
...collectFacetStatus(paths, undefined, legacyEvidenceRoot),
|
|
332
|
+
};
|
|
333
|
+
writeEnvelope(io, parsed, okEnvelope(generateRequestId(), data));
|
|
334
|
+
return { code: EXIT_CODES.OK, spawnedPid };
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const facet = error instanceof FacetError ? error : FacetError.from(error);
|
|
337
|
+
const env1: FacetEnvelope<never> = {
|
|
338
|
+
schemaVersion: FACET_SCHEMA_VERSION,
|
|
339
|
+
requestId: generateRequestId(),
|
|
340
|
+
ok: false,
|
|
341
|
+
error: facet.toBody(),
|
|
342
|
+
};
|
|
343
|
+
writeEnvelope(io, parsed, env1);
|
|
344
|
+
return { code: EXIT_CODES.OK, spawnedPid: null };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (parsed.kind === "verb" && parsed.verb === "publish" && parsed.args.watch === true) {
|
|
349
|
+
if (typeof parsed.args.file !== "string" || parsed.args.file === "-") {
|
|
350
|
+
const env1 = buildUsageError("publish --watch requires --file <path>", {
|
|
351
|
+
reason: "watch_requires_file",
|
|
352
|
+
});
|
|
353
|
+
printEnvelope(io.stdout, env1);
|
|
354
|
+
return { code: EXIT_CODES.USAGE, spawnedPid: null };
|
|
355
|
+
}
|
|
356
|
+
if (!existsSync(parsed.args.file)) {
|
|
357
|
+
const env1 = buildUsageError(`publish --watch file does not exist: ${parsed.args.file}`, {
|
|
358
|
+
reason: "watch_file_missing",
|
|
359
|
+
});
|
|
360
|
+
printEnvelope(io.stdout, env1);
|
|
361
|
+
return { code: EXIT_CODES.USAGE, spawnedPid: null };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 3. Read stdin up front (cheap when empty; needed for `publish -`).
|
|
366
|
+
const stdinBytes =
|
|
367
|
+
parsed.kind === "verb" && parsed.verb === "publish" && parsed.args.watch === true
|
|
368
|
+
? new Uint8Array()
|
|
369
|
+
: await readAllStdin(io.stdin);
|
|
370
|
+
|
|
371
|
+
// 4. Lazy-spawn the service and resolve its baseUrl + install token.
|
|
372
|
+
let resolved: {
|
|
373
|
+
baseUrl: string;
|
|
374
|
+
installToken: string;
|
|
375
|
+
promoteToken?: string;
|
|
376
|
+
metadata: { pid: number; startTime: number; port: number; contractVersion: string };
|
|
377
|
+
};
|
|
378
|
+
let promoteToken: string | undefined;
|
|
379
|
+
try {
|
|
380
|
+
const r = await ensureService({ env: io.env }, testHooks as ServiceHooks);
|
|
381
|
+
const paths = computeFacetPaths(
|
|
382
|
+
io.env.FACET_HOME === undefined ? {} : { facetHome: io.env.FACET_HOME },
|
|
383
|
+
);
|
|
384
|
+
promoteToken =
|
|
385
|
+
io.env.FACET_PROMOTE_TOKEN ??
|
|
386
|
+
(existsSync(paths.token) ? readFileSync(paths.token, "utf8").trim() : undefined);
|
|
387
|
+
resolved = {
|
|
388
|
+
baseUrl: r.baseUrl,
|
|
389
|
+
installToken: r.installToken,
|
|
390
|
+
metadata: r.metadata,
|
|
391
|
+
...(promoteToken === undefined || promoteToken.length === 0 ? {} : { promoteToken }),
|
|
392
|
+
};
|
|
393
|
+
} catch (error) {
|
|
394
|
+
// Any `FacetError` produces a well-formed envelope on stdout;
|
|
395
|
+
// adapters branch on the envelope, so exit 0. A non-FacetError
|
|
396
|
+
// (unexpected runtime failure) is mapped to a generic internal
|
|
397
|
+
// envelope and exits INTERNAL so shell pipelines can see it.
|
|
398
|
+
const facet = error instanceof FacetError ? error : FacetError.from(error);
|
|
399
|
+
const env1: FacetEnvelope<never> = {
|
|
400
|
+
schemaVersion: FACET_SCHEMA_VERSION,
|
|
401
|
+
requestId: `req-${crypto.randomUUID()}`,
|
|
402
|
+
ok: false,
|
|
403
|
+
error: facet.toBody(),
|
|
404
|
+
};
|
|
405
|
+
printEnvelope(io.stdout, env1);
|
|
406
|
+
if (error instanceof FacetError) {
|
|
407
|
+
return { code: EXIT_CODES.OK, spawnedPid: null };
|
|
408
|
+
}
|
|
409
|
+
return { code: EXIT_CODES.INTERNAL, spawnedPid: null };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// 5. Dispatch the verb.
|
|
413
|
+
try {
|
|
414
|
+
const serviceVerb = parsed as Extract<ParsedCommand, { kind: "verb" }> & { verb: CommandName };
|
|
415
|
+
if (serviceVerb.verb === "publish" && parsed.args.watch === true) {
|
|
416
|
+
const watchArgs = { ...parsed.args };
|
|
417
|
+
delete watchArgs.watch;
|
|
418
|
+
const watchExit = await watchPublishFile({
|
|
419
|
+
filePath: String(parsed.args.file),
|
|
420
|
+
...(testHooks.watchSignal === undefined ? {} : { signal: testHooks.watchSignal }),
|
|
421
|
+
publish: async (bytes) => {
|
|
422
|
+
const request = buildPublishRequest(watchArgs, bytes);
|
|
423
|
+
return new FacetClient({
|
|
424
|
+
baseUrl: resolved.baseUrl,
|
|
425
|
+
installToken: resolved.installToken,
|
|
426
|
+
...(promoteToken === undefined ? {} : { promoteToken }),
|
|
427
|
+
}).sendCommand(request);
|
|
428
|
+
},
|
|
429
|
+
emit: (envelope) => writeEnvelope(io, parsed, envelope),
|
|
430
|
+
diagnostic: (message) => {
|
|
431
|
+
io.stderr.write(`${message}\n`);
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
return { code: watchExit.code, spawnedPid: resolved.metadata.pid };
|
|
435
|
+
}
|
|
436
|
+
const response = await executeVerb(
|
|
437
|
+
serviceVerb,
|
|
438
|
+
parsed.args,
|
|
439
|
+
stdinBytes,
|
|
440
|
+
{
|
|
441
|
+
baseUrl: resolved.baseUrl,
|
|
442
|
+
installToken: resolved.installToken,
|
|
443
|
+
},
|
|
444
|
+
testHooks.openLauncher,
|
|
445
|
+
process.cwd(),
|
|
446
|
+
promoteToken,
|
|
447
|
+
);
|
|
448
|
+
writeEnvelope(io, parsed, response);
|
|
449
|
+
return { code: EXIT_CODES.OK, spawnedPid: resolved.metadata.pid };
|
|
450
|
+
} catch (error) {
|
|
451
|
+
// Any `FacetError` (typed validation, connection failure, contract
|
|
452
|
+
// mismatch) yields a well-formed envelope on stdout — adapters
|
|
453
|
+
// branch on the envelope, so the exit code stays 0. Non-FacetError
|
|
454
|
+
// throws (e.g. unexpected runtime errors) are mapped to a generic
|
|
455
|
+
// internal envelope and exit INTERNAL so shell pipelines see a
|
|
456
|
+
// distinct nonzero code.
|
|
457
|
+
const facet = error instanceof FacetError ? error : FacetError.from(error);
|
|
458
|
+
const env1: FacetEnvelope<CommandResult> = {
|
|
459
|
+
schemaVersion: FACET_SCHEMA_VERSION,
|
|
460
|
+
requestId: `req-${crypto.randomUUID()}`,
|
|
461
|
+
ok: false,
|
|
462
|
+
error: facet.toBody(),
|
|
463
|
+
};
|
|
464
|
+
writeEnvelope(io, parsed, env1);
|
|
465
|
+
if (error instanceof FacetError) {
|
|
466
|
+
return { code: EXIT_CODES.OK, spawnedPid: resolved.metadata.pid };
|
|
467
|
+
}
|
|
468
|
+
return { code: EXIT_CODES.INTERNAL, spawnedPid: resolved.metadata.pid };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Real entrypoint. Compiled hidden roles are resolved lazily so source CLI
|
|
474
|
+
* commands never evaluate validation's Tier 1 runtime graph.
|
|
475
|
+
*/
|
|
476
|
+
async function main(): Promise<void> {
|
|
477
|
+
const argv = process.argv.slice(2);
|
|
478
|
+
if (isCompiledRuntime() && isCompiledEntrypointArg(argv)) {
|
|
479
|
+
const { dispatchCompiledEntrypoint } = await import("../runtime/compiled-entrypoints");
|
|
480
|
+
const compiledExit = await dispatchCompiledEntrypoint(argv);
|
|
481
|
+
if (compiledExit !== null) {
|
|
482
|
+
process.exit(compiledExit);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const watchController = argv.includes("--watch") ? new AbortController() : undefined;
|
|
487
|
+
const onSigint = () => watchController?.abort();
|
|
488
|
+
if (watchController !== undefined) process.once("SIGINT", onSigint);
|
|
489
|
+
const exit = await runCli(
|
|
490
|
+
argv,
|
|
491
|
+
{
|
|
492
|
+
stdin: process.stdin as unknown as ReadableStream<Uint8Array>,
|
|
493
|
+
stdout: {
|
|
494
|
+
get isTTY() {
|
|
495
|
+
return process.stdout.isTTY === true;
|
|
496
|
+
},
|
|
497
|
+
write(chunk) {
|
|
498
|
+
process.stdout.write(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
|
|
499
|
+
return true;
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
stderr: {
|
|
503
|
+
write(chunk) {
|
|
504
|
+
process.stderr.write(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
|
|
505
|
+
return true;
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
env: process.env,
|
|
509
|
+
},
|
|
510
|
+
watchController === undefined ? {} : { watchSignal: watchController.signal },
|
|
511
|
+
);
|
|
512
|
+
if (watchController !== undefined) process.off("SIGINT", onSigint);
|
|
513
|
+
process.exit(exit.code);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Re-export `okEnvelope` for the test that builds expected envelopes
|
|
517
|
+
// alongside the wire output. Pure re-export — no behavior change.
|
|
518
|
+
export { okEnvelope };
|
|
519
|
+
|
|
520
|
+
if (import.meta.main) {
|
|
521
|
+
void main();
|
|
522
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output adapter.
|
|
3
|
+
*
|
|
4
|
+
* I/O surface:
|
|
5
|
+
* - stdout: `--help` text (default format), `--version` text, and
|
|
6
|
+
* every envelope (success OR typed error). The envelope is the
|
|
7
|
+
* product contract — adapters parse stdout as JSON and branch on
|
|
8
|
+
* the typed `ok` + `error.code` shape.
|
|
9
|
+
* - stderr: service-side structured JSON logs + any CLI-side
|
|
10
|
+
* diagnostic the caller might want to capture (the current
|
|
11
|
+
* `runCli` path is silent on stderr; future surface-level
|
|
12
|
+
* warnings go here).
|
|
13
|
+
*
|
|
14
|
+
* Exit-code policy:
|
|
15
|
+
* The envelope is the contract. Any CLI path that can produce a
|
|
16
|
+
* well-formed `FacetEnvelope` exits 0 — adapters branch on the
|
|
17
|
+
* envelope, not the exit code. Nonzero codes are reserved for
|
|
18
|
+
* paths that cannot produce a typed envelope:
|
|
19
|
+
*
|
|
20
|
+
* - pre-parse usage error (unknown verb, bad flag, missing arg)
|
|
21
|
+
* → USAGE (64)
|
|
22
|
+
* - unhandled non-FacetError throw (a real bug) → INTERNAL (70)
|
|
23
|
+
*
|
|
24
|
+
* Rule of thumb: malformed invocation the CLI can't turn into a
|
|
25
|
+
* typed envelope → nonzero; any well-formed envelope (incl.
|
|
26
|
+
* `ok: false` with a typed `error.code`) → exit 0.
|
|
27
|
+
*
|
|
28
|
+
* DATA / TEMPFAIL codes from earlier drafts were removed: with the
|
|
29
|
+
* envelope-first policy they are unreachable. Every `FacetError`
|
|
30
|
+
* thrown by the spawn path or a per-verb builder is wrapped in a
|
|
31
|
+
* typed envelope and exits 0.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { errEnvelope, okEnvelope, type FacetEnvelope } from "../shared/contracts/envelope";
|
|
35
|
+
import { generateRequestId } from "../shared/util/time";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Exit code table — single source of truth. Every CLI caller reads
|
|
39
|
+
* its exit code from this map so the `--help` text and the runtime
|
|
40
|
+
* dispatch can never disagree.
|
|
41
|
+
*/
|
|
42
|
+
export const EXIT_CODES = {
|
|
43
|
+
/** Well-formed envelope on stdout (success OR typed error). */
|
|
44
|
+
OK: 0,
|
|
45
|
+
/** Doctor completed and found one or more failing environment probes. */
|
|
46
|
+
DOCTOR_FAILED: 1,
|
|
47
|
+
/**
|
|
48
|
+
* Pre-parse usage error — unknown verb, bad flag, missing
|
|
49
|
+
* required argument, or a malformed invocation the CLI cannot
|
|
50
|
+
* turn into a typed envelope. Distinct from typed error
|
|
51
|
+
* envelopes (which also exit 0).
|
|
52
|
+
*/
|
|
53
|
+
USAGE: 64,
|
|
54
|
+
/**
|
|
55
|
+
* Unhandled non-FacetError throw (a real bug). The CLI
|
|
56
|
+
* surfaced an internal envelope on stdout; the nonzero exit
|
|
57
|
+
* signals "shell pipeline should treat this as a crash".
|
|
58
|
+
*/
|
|
59
|
+
INTERNAL: 70,
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
export interface CliWriter {
|
|
63
|
+
write(chunk: string | Uint8Array): boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Print the envelope to stdout as a single JSON line. Newline
|
|
68
|
+
* appended so shell pipelines see a complete record per call.
|
|
69
|
+
*/
|
|
70
|
+
export function printEnvelope(writer: CliWriter, envelope: FacetEnvelope<unknown>): void {
|
|
71
|
+
writer.write(`${JSON.stringify(envelope)}\n`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build an OK envelope carrying a version + contractVersion. Used by
|
|
76
|
+
* `--version --json`; the text form of `--version` writes a plain
|
|
77
|
+
* string to stdout instead.
|
|
78
|
+
*/
|
|
79
|
+
export function buildVersionEnvelope(
|
|
80
|
+
version: string,
|
|
81
|
+
contractVersion: string,
|
|
82
|
+
): FacetEnvelope<{
|
|
83
|
+
version: string;
|
|
84
|
+
contractVersion: string;
|
|
85
|
+
}> {
|
|
86
|
+
return okEnvelope(generateRequestId(), { version, contractVersion });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build a typed error envelope for a pre-parse CLI-side failure.
|
|
91
|
+
* Used by the parser when it cannot turn the invocation into a
|
|
92
|
+
* typed envelope (unknown verb, bad flag, missing required value).
|
|
93
|
+
* The CLI still exits USAGE (64) here because the user-supplied
|
|
94
|
+
* argv is what triggered the error, not the service.
|
|
95
|
+
*/
|
|
96
|
+
export function buildUsageError(
|
|
97
|
+
message: string,
|
|
98
|
+
details?: Record<string, string | number | boolean | null>,
|
|
99
|
+
): FacetEnvelope<never> {
|
|
100
|
+
return errEnvelope(generateRequestId(), {
|
|
101
|
+
code: "invalid_request",
|
|
102
|
+
message,
|
|
103
|
+
retryable: false,
|
|
104
|
+
...(details !== undefined ? { details } : {}),
|
|
105
|
+
});
|
|
106
|
+
}
|