@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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
|
|
4
|
+
import { computeFacetPaths, type FacetRuntimePaths } from "../../shared/config/paths";
|
|
5
|
+
import { FACET_SCHEMA_VERSION } from "../../shared/contracts/envelope";
|
|
6
|
+
import { CURRENT_STORAGE_VERSION } from "../../shared/storage-version";
|
|
7
|
+
import {
|
|
8
|
+
isLockStale,
|
|
9
|
+
readLockMetadata,
|
|
10
|
+
type LockMetadata,
|
|
11
|
+
} from "../../service/lifecycle/process-lock";
|
|
12
|
+
import { isPidAlive } from "../../shared/util/process";
|
|
13
|
+
import { probeNetnsSupport, type NetnsProbe } from "../../validation/sandbox/netns";
|
|
14
|
+
import { resolveShellBinary } from "../../validation/tier1/launcher";
|
|
15
|
+
import type { DoctorResult } from "../../shared/contracts/commands/results";
|
|
16
|
+
|
|
17
|
+
export const DOCTOR_PROBE_NAMES = [
|
|
18
|
+
"bun",
|
|
19
|
+
"chrome-headless-shell",
|
|
20
|
+
"netns-userns",
|
|
21
|
+
"database",
|
|
22
|
+
"token-permissions",
|
|
23
|
+
"evidence-permissions",
|
|
24
|
+
"service-lock",
|
|
25
|
+
] as const;
|
|
26
|
+
export type DoctorProbeName = (typeof DOCTOR_PROBE_NAMES)[number];
|
|
27
|
+
export type DoctorProbeResult = DoctorResult["probes"][number];
|
|
28
|
+
|
|
29
|
+
type DoctorFs = {
|
|
30
|
+
exists: (path: string) => boolean;
|
|
31
|
+
stat: (path: string) => { mode: number };
|
|
32
|
+
};
|
|
33
|
+
type DoctorDb = { quickCheck: string; version: number | null };
|
|
34
|
+
export interface DoctorOptions {
|
|
35
|
+
readonly argv?: readonly string[];
|
|
36
|
+
readonly bunVersion?: string;
|
|
37
|
+
readonly paths?: FacetRuntimePaths;
|
|
38
|
+
readonly shellBinary?: string | null;
|
|
39
|
+
readonly netns?: NetnsProbe;
|
|
40
|
+
readonly fs?: DoctorFs;
|
|
41
|
+
readonly databaseReader?: (path: string) => DoctorDb;
|
|
42
|
+
readonly lockReader?: (path: string) => LockMetadata | null;
|
|
43
|
+
readonly pidAlive?: (pid: number) => boolean;
|
|
44
|
+
readonly lockStale?: (metadata: LockMetadata) => boolean;
|
|
45
|
+
readonly statusCheck?: (metadata: LockMetadata) => boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const BUN_VERSION = "1.4.0";
|
|
49
|
+
const shellFix = "bunx --bun puppeteer browsers install chrome-headless-shell@151.0.7922.77";
|
|
50
|
+
const netnsFix =
|
|
51
|
+
"sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 && unshare --map-current-user --user --net -- /bin/true";
|
|
52
|
+
|
|
53
|
+
function defaultDbReader(path: string): DoctorDb {
|
|
54
|
+
const db = new Database(path, { readonly: true, strict: true });
|
|
55
|
+
try {
|
|
56
|
+
const quickCheck = String(
|
|
57
|
+
(db.query("PRAGMA quick_check").get() as { quick_check?: string })?.quick_check,
|
|
58
|
+
);
|
|
59
|
+
const row = db.query("SELECT MAX(version) AS version FROM schema_migrations").get() as {
|
|
60
|
+
version?: number | null;
|
|
61
|
+
};
|
|
62
|
+
return { quickCheck, version: row.version ?? null };
|
|
63
|
+
} finally {
|
|
64
|
+
db.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function modeOf(fs: DoctorFs, path: string): number | null {
|
|
69
|
+
try {
|
|
70
|
+
return fs.stat(path).mode & 0o777;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function probe(
|
|
77
|
+
name: DoctorProbeName,
|
|
78
|
+
status: "pass" | "fail",
|
|
79
|
+
summary: string,
|
|
80
|
+
fixCommand: string | null,
|
|
81
|
+
details: Record<string, string | number | boolean | null> = {},
|
|
82
|
+
): DoctorProbeResult {
|
|
83
|
+
return { name, status, summary, fixCommand, details };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function invocationPrefix(argv: readonly string[]): string {
|
|
87
|
+
const sourceEntrypoint = argv[1];
|
|
88
|
+
return sourceEntrypoint?.endsWith("/src/cli/main.ts") === true
|
|
89
|
+
? `bun ${sourceEntrypoint}`
|
|
90
|
+
: "facet";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function runDoctor(options: DoctorOptions = {}): DoctorResult {
|
|
94
|
+
const paths = options.paths ?? computeFacetPaths();
|
|
95
|
+
const fs = options.fs ?? { exists: existsSync, stat: statSync };
|
|
96
|
+
const databaseReader = options.databaseReader ?? defaultDbReader;
|
|
97
|
+
const lockReader = options.lockReader ?? readLockMetadata;
|
|
98
|
+
const pidAlive = options.pidAlive ?? isPidAlive;
|
|
99
|
+
const lockStale = options.lockStale ?? isLockStale;
|
|
100
|
+
const shellBinary =
|
|
101
|
+
options.shellBinary === undefined ? resolveShellBinary() : options.shellBinary;
|
|
102
|
+
const netns = options.netns ?? probeNetnsSupport();
|
|
103
|
+
const probes: DoctorProbeResult[] = [];
|
|
104
|
+
const bunVersion = options.bunVersion ?? Bun.version;
|
|
105
|
+
const restartFix = `${invocationPrefix(options.argv ?? process.argv)} status --start`;
|
|
106
|
+
|
|
107
|
+
probes.push(
|
|
108
|
+
bunVersion === BUN_VERSION
|
|
109
|
+
? probe("bun", "pass", bunVersion, null, { version: bunVersion, expected: BUN_VERSION })
|
|
110
|
+
: probe(
|
|
111
|
+
"bun",
|
|
112
|
+
"fail",
|
|
113
|
+
`${bunVersion}, expected ${BUN_VERSION}`,
|
|
114
|
+
`curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}"`,
|
|
115
|
+
{
|
|
116
|
+
version: bunVersion,
|
|
117
|
+
expected: BUN_VERSION,
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
probes.push(
|
|
122
|
+
shellBinary === null
|
|
123
|
+
? probe("chrome-headless-shell", "fail", "pinned shell not found", shellFix, { found: false })
|
|
124
|
+
: probe("chrome-headless-shell", "pass", shellBinary, null, {
|
|
125
|
+
found: true,
|
|
126
|
+
path: shellBinary,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
probes.push(
|
|
130
|
+
netns.available
|
|
131
|
+
? probe("netns-userns", "pass", "available", null, { available: true })
|
|
132
|
+
: probe("netns-userns", "fail", netns.reason ?? "unavailable", netnsFix, {
|
|
133
|
+
available: false,
|
|
134
|
+
reason: netns.reason,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (!fs.exists(paths.database)) {
|
|
139
|
+
probes.push(probe("database", "fail", "database missing", restartFix, { present: false }));
|
|
140
|
+
} else {
|
|
141
|
+
try {
|
|
142
|
+
const db = databaseReader(paths.database);
|
|
143
|
+
if (db.quickCheck !== "ok") {
|
|
144
|
+
probes.push(
|
|
145
|
+
probe("database", "fail", `quick_check ${db.quickCheck}`, restartFix, {
|
|
146
|
+
quickCheck: db.quickCheck,
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
} else if (db.version !== CURRENT_STORAGE_VERSION) {
|
|
150
|
+
probes.push(
|
|
151
|
+
probe(
|
|
152
|
+
"database",
|
|
153
|
+
"fail",
|
|
154
|
+
`schema v${db.version ?? "unknown"}, expected v${CURRENT_STORAGE_VERSION}`,
|
|
155
|
+
restartFix,
|
|
156
|
+
{
|
|
157
|
+
version: db.version,
|
|
158
|
+
expected: CURRENT_STORAGE_VERSION,
|
|
159
|
+
},
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
probes.push(
|
|
164
|
+
probe("database", "pass", `schema v${db.version}`, null, {
|
|
165
|
+
quickCheck: db.quickCheck,
|
|
166
|
+
version: db.version,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
probes.push(
|
|
172
|
+
probe(
|
|
173
|
+
"database",
|
|
174
|
+
"fail",
|
|
175
|
+
error instanceof Error ? error.message : String(error),
|
|
176
|
+
restartFix,
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const installToken = paths.token.replace(/promote\.token$/, "install.token");
|
|
183
|
+
const tokenPaths = [installToken, paths.token];
|
|
184
|
+
const tokenModes = tokenPaths.map((path) => ({ path, mode: modeOf(fs, path) }));
|
|
185
|
+
const tokenBad = tokenModes.find(({ mode }) => mode !== null && mode !== 0o600);
|
|
186
|
+
const installMode = modeOf(fs, installToken);
|
|
187
|
+
probes.push(
|
|
188
|
+
tokenBad
|
|
189
|
+
? probe(
|
|
190
|
+
"token-permissions",
|
|
191
|
+
"fail",
|
|
192
|
+
`${tokenBad.path} mode ${tokenBad.mode?.toString(8)}`,
|
|
193
|
+
'chmod 600 "$FACET_HOME/secrets/install.token" "$FACET_HOME/secrets/promote.token"',
|
|
194
|
+
{ path: tokenBad.path, mode: tokenBad.mode },
|
|
195
|
+
)
|
|
196
|
+
: installMode === null
|
|
197
|
+
? probe("token-permissions", "fail", `${installToken} missing`, restartFix, {
|
|
198
|
+
path: installToken,
|
|
199
|
+
present: false,
|
|
200
|
+
})
|
|
201
|
+
: probe("token-permissions", "pass", "token files mode 600", null, { mode: 0o600 }),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const evidenceMode = modeOf(fs, paths.evidence);
|
|
205
|
+
probes.push(
|
|
206
|
+
evidenceMode === 0o700
|
|
207
|
+
? probe("evidence-permissions", "pass", "mode 700", null, { mode: evidenceMode })
|
|
208
|
+
: probe(
|
|
209
|
+
"evidence-permissions",
|
|
210
|
+
"fail",
|
|
211
|
+
evidenceMode === null ? "evidence root missing" : `mode ${evidenceMode.toString(8)}`,
|
|
212
|
+
`mkdir -p "$FACET_HOME/evidence" && chmod 700 "$FACET_HOME/evidence"`,
|
|
213
|
+
{ mode: evidenceMode },
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const lock = lockReader(paths.lock);
|
|
218
|
+
if (lock === null) {
|
|
219
|
+
probes.push(probe("service-lock", "pass", "dormant · no lock", null, { state: "dormant" }));
|
|
220
|
+
} else if (lockStale(lock) || !pidAlive(lock.pid)) {
|
|
221
|
+
probes.push(
|
|
222
|
+
probe("service-lock", "fail", "stale or dead lock", restartFix, {
|
|
223
|
+
pid: lock.pid,
|
|
224
|
+
stale: true,
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
} else if (lock.contractVersion !== FACET_SCHEMA_VERSION) {
|
|
228
|
+
probes.push(
|
|
229
|
+
probe("service-lock", "fail", `cross-version lock ${lock.contractVersion}`, restartFix, {
|
|
230
|
+
contractVersion: lock.contractVersion,
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
} else if (options.statusCheck !== undefined && !options.statusCheck(lock)) {
|
|
234
|
+
probes.push(
|
|
235
|
+
probe("service-lock", "fail", "live service status request failed", restartFix, {
|
|
236
|
+
pid: lock.pid,
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
probes.push(
|
|
241
|
+
probe("service-lock", "pass", `active · pid ${lock.pid}`, null, {
|
|
242
|
+
pid: lock.pid,
|
|
243
|
+
state: "active",
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { command: "doctor", allPassed: probes.every((item) => item.status === "pass"), probes };
|
|
249
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `facet export` — fetch source or retained render bytes and write the
|
|
3
|
+
* artifact plus its disclosure sidecar locally.
|
|
4
|
+
*
|
|
5
|
+
* Output paths and overwrite policy stay outside the wire request so the
|
|
6
|
+
* service cannot be used as a general filesystem writer.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
import type { ExportRequest } from "../../shared/contracts/commands/requests";
|
|
14
|
+
import { ExportRequestSchema } from "../../shared/contracts/commands/requests";
|
|
15
|
+
import type { ExportResult } from "../../shared/contracts/commands/results";
|
|
16
|
+
import { extensionForExport, sidecarFilenameForArtifact } from "../../shared/export";
|
|
17
|
+
import { FacetError } from "../../shared/errors/facet-error";
|
|
18
|
+
import { generateRequestId } from "../../shared/util/time";
|
|
19
|
+
|
|
20
|
+
export interface ExportPaths {
|
|
21
|
+
readonly artifactPath: string;
|
|
22
|
+
readonly sidecarPath: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildExportRequest(
|
|
26
|
+
args: Readonly<Record<string, string | boolean>>,
|
|
27
|
+
): ExportRequest {
|
|
28
|
+
const artifactId = args["artifact-id"];
|
|
29
|
+
if (typeof artifactId !== "string" || artifactId.length === 0) {
|
|
30
|
+
throw new FacetError("invalid_request", "export: artifact id is required", {
|
|
31
|
+
retryable: false,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const revision = args["revision"];
|
|
35
|
+
return ExportRequestSchema.parse({
|
|
36
|
+
command: "export",
|
|
37
|
+
requestId: generateRequestId(),
|
|
38
|
+
artifactId,
|
|
39
|
+
...(typeof revision === "string" ? { revisionSha: revision } : {}),
|
|
40
|
+
format: typeof args["format"] === "string" ? args["format"] : "source",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { extensionForExport } from "../../shared/export";
|
|
45
|
+
export { sidecarFilenameForArtifact } from "../../shared/export";
|
|
46
|
+
|
|
47
|
+
function sanitizeDerivedNamePart(value: string): string {
|
|
48
|
+
return value
|
|
49
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
50
|
+
.replace(/\.\.+/g, "-")
|
|
51
|
+
.replace(/^[.-]+|[.-]+$/g, "")
|
|
52
|
+
.slice(0, 80)
|
|
53
|
+
.replace(/^[.-]+|[.-]+$/g, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function derivedSlug(result: ExportResult): string {
|
|
57
|
+
return (
|
|
58
|
+
sanitizeDerivedNamePart(result.sidecar.slug) ||
|
|
59
|
+
sanitizeDerivedNamePart(result.sidecar.artifactId) ||
|
|
60
|
+
"artifact"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function removeIfExists(path: string): void {
|
|
65
|
+
try {
|
|
66
|
+
unlinkSync(path);
|
|
67
|
+
} catch {
|
|
68
|
+
// Best-effort cleanup must not replace the original write failure.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolveExportPaths(
|
|
73
|
+
result: ExportResult,
|
|
74
|
+
outFlag: string | undefined,
|
|
75
|
+
cwd: string,
|
|
76
|
+
): ExportPaths {
|
|
77
|
+
const extension = extensionForExport(
|
|
78
|
+
result.format,
|
|
79
|
+
result.sidecar.artifactType,
|
|
80
|
+
result.sidecar.renderFormat,
|
|
81
|
+
);
|
|
82
|
+
const artifactPath =
|
|
83
|
+
outFlag === undefined
|
|
84
|
+
? resolve(cwd, `${derivedSlug(result)}-${result.sidecar.revisionSha.slice(0, 7)}${extension}`)
|
|
85
|
+
: resolve(cwd, outFlag);
|
|
86
|
+
return { artifactPath, sidecarPath: sidecarFilenameForArtifact(artifactPath) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function writeExportFiles(
|
|
90
|
+
result: ExportResult,
|
|
91
|
+
paths: ExportPaths,
|
|
92
|
+
force: boolean,
|
|
93
|
+
renameFile: typeof renameSync = renameSync,
|
|
94
|
+
): void {
|
|
95
|
+
const artifactExistedBeforeWrite = existsSync(paths.artifactPath);
|
|
96
|
+
const sidecarExistedBeforeWrite = existsSync(paths.sidecarPath);
|
|
97
|
+
if (!force) {
|
|
98
|
+
const existing = [
|
|
99
|
+
artifactExistedBeforeWrite ? paths.artifactPath : null,
|
|
100
|
+
sidecarExistedBeforeWrite ? paths.sidecarPath : null,
|
|
101
|
+
].filter((path): path is string => path !== null);
|
|
102
|
+
if (existing.length > 0) {
|
|
103
|
+
throw new FacetError(
|
|
104
|
+
"invalid_request",
|
|
105
|
+
`export output already exists: ${existing.join(", ")} (pass --force to replace)`,
|
|
106
|
+
{ retryable: false, details: { reason: "output_exists" } },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
mkdirSync(dirname(paths.artifactPath), { recursive: true });
|
|
112
|
+
mkdirSync(dirname(paths.sidecarPath), { recursive: true });
|
|
113
|
+
for (const target of [paths.artifactPath, paths.sidecarPath]) {
|
|
114
|
+
if (existsSync(target) && statSync(target).isDirectory()) {
|
|
115
|
+
throw new Error(`export output is a directory: ${target}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const artifactTempPath = join(
|
|
120
|
+
dirname(paths.artifactPath),
|
|
121
|
+
`.${basename(paths.artifactPath)}.${randomUUID()}.tmp`,
|
|
122
|
+
);
|
|
123
|
+
const sidecarTempPath = join(
|
|
124
|
+
dirname(paths.sidecarPath),
|
|
125
|
+
`.${basename(paths.sidecarPath)}.${randomUUID()}.tmp`,
|
|
126
|
+
);
|
|
127
|
+
const artifactBackupPath = artifactExistedBeforeWrite
|
|
128
|
+
? join(dirname(paths.artifactPath), `.${basename(paths.artifactPath)}.${randomUUID()}.bak`)
|
|
129
|
+
: null;
|
|
130
|
+
const sidecarBackupPath = sidecarExistedBeforeWrite
|
|
131
|
+
? join(dirname(paths.sidecarPath), `.${basename(paths.sidecarPath)}.${randomUUID()}.bak`)
|
|
132
|
+
: null;
|
|
133
|
+
let artifactRenamed = false;
|
|
134
|
+
let sidecarRenamed = false;
|
|
135
|
+
|
|
136
|
+
const restoreTarget = (target: string, backup: string | null, wasRenamed: boolean): void => {
|
|
137
|
+
try {
|
|
138
|
+
if (backup !== null) {
|
|
139
|
+
if (existsSync(target)) removeIfExists(target);
|
|
140
|
+
if (existsSync(backup)) renameSync(backup, target);
|
|
141
|
+
} else if (wasRenamed) {
|
|
142
|
+
removeIfExists(target);
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Best-effort rollback must not mask the original rename failure.
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
writeFileSync(artifactTempPath, Buffer.from(result.bytes, "base64"), {
|
|
151
|
+
flag: "wx",
|
|
152
|
+
mode: 0o600,
|
|
153
|
+
});
|
|
154
|
+
writeFileSync(sidecarTempPath, `${JSON.stringify(result.sidecar, null, 2)}\n`, {
|
|
155
|
+
encoding: "utf8",
|
|
156
|
+
flag: "wx",
|
|
157
|
+
mode: 0o600,
|
|
158
|
+
});
|
|
159
|
+
if (artifactBackupPath !== null) renameFile(paths.artifactPath, artifactBackupPath);
|
|
160
|
+
if (sidecarBackupPath !== null) renameFile(paths.sidecarPath, sidecarBackupPath);
|
|
161
|
+
renameFile(artifactTempPath, paths.artifactPath);
|
|
162
|
+
artifactRenamed = true;
|
|
163
|
+
renameFile(sidecarTempPath, paths.sidecarPath);
|
|
164
|
+
sidecarRenamed = true;
|
|
165
|
+
if (artifactBackupPath !== null) removeIfExists(artifactBackupPath);
|
|
166
|
+
if (sidecarBackupPath !== null) removeIfExists(sidecarBackupPath);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
for (const tempPath of [artifactTempPath, sidecarTempPath]) {
|
|
169
|
+
try {
|
|
170
|
+
unlinkSync(tempPath);
|
|
171
|
+
} catch {
|
|
172
|
+
// Preserve the write or rename failure as the useful error at the CLI boundary.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
restoreTarget(paths.sidecarPath, sidecarBackupPath, sidecarRenamed);
|
|
176
|
+
restoreTarget(paths.artifactPath, artifactBackupPath, artifactRenamed);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `facet instantiate` — create a new artifact from a named template.
|
|
3
|
+
*
|
|
4
|
+
* Input-validation errors throw `FacetError("invalid_request", ...)`
|
|
5
|
+
* so the envelope preserves the typed `invalid_request` code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { generateRequestId } from "../../shared/util/time";
|
|
9
|
+
import { FacetError } from "../../shared/errors/facet-error";
|
|
10
|
+
import type { InstantiateRequest } from "../../shared/contracts/commands/requests";
|
|
11
|
+
|
|
12
|
+
export function buildInstantiateRequest(
|
|
13
|
+
args: Readonly<Record<string, string | boolean>>,
|
|
14
|
+
): InstantiateRequest {
|
|
15
|
+
const name = args["name"];
|
|
16
|
+
const newSlug = args["new-slug"];
|
|
17
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
18
|
+
throw new FacetError("invalid_request", "--name is required for instantiate", {
|
|
19
|
+
retryable: false,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (typeof newSlug !== "string" || newSlug.length === 0) {
|
|
23
|
+
throw new FacetError("invalid_request", "--new-slug is required for instantiate", {
|
|
24
|
+
retryable: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const projectId = args["project-id"];
|
|
28
|
+
return {
|
|
29
|
+
command: "instantiate",
|
|
30
|
+
requestId: generateRequestId(),
|
|
31
|
+
name,
|
|
32
|
+
newSlug,
|
|
33
|
+
...(typeof projectId === "string" ? { projectId } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `facet list` — list artifacts for a project.
|
|
3
|
+
*
|
|
4
|
+
* Supports optional `--slug-prefix` and `--limit` filters. The
|
|
5
|
+
* service applies its own `MAX_LIST_LIMIT` cap, so the CLI does
|
|
6
|
+
* NOT clamp — a too-large limit surfaces as a typed `invalid_request`
|
|
7
|
+
* from the service and the envelope carries the typed code.
|
|
8
|
+
*
|
|
9
|
+
* Input-validation errors throw `FacetError("invalid_request", ...)`
|
|
10
|
+
* so the envelope preserves the typed `invalid_request` code; the
|
|
11
|
+
* main catch recognises a pre-existing FacetError and passes its
|
|
12
|
+
* body through unchanged.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { generateRequestId } from "../../shared/util/time";
|
|
16
|
+
import { FacetError } from "../../shared/errors/facet-error";
|
|
17
|
+
import { MAX_LIST_LIMIT } from "../../shared/config/limits";
|
|
18
|
+
import type { ListRequest } from "../../shared/contracts/commands/requests";
|
|
19
|
+
|
|
20
|
+
export function buildListRequest(args: Readonly<Record<string, string | boolean>>): ListRequest {
|
|
21
|
+
const projectId = args["project-id"];
|
|
22
|
+
if (typeof projectId !== "string" || projectId.length === 0) {
|
|
23
|
+
throw new FacetError("invalid_request", "--project-id is required for list", {
|
|
24
|
+
retryable: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const slugPrefix = args["slug-prefix"];
|
|
28
|
+
const limit = args["limit"];
|
|
29
|
+
const limitNumber = typeof limit === "string" ? Number(limit) : undefined;
|
|
30
|
+
if (
|
|
31
|
+
limitNumber !== undefined &&
|
|
32
|
+
(!Number.isSafeInteger(limitNumber) || limitNumber < 1 || limitNumber > MAX_LIST_LIMIT)
|
|
33
|
+
) {
|
|
34
|
+
throw new FacetError(
|
|
35
|
+
"invalid_request",
|
|
36
|
+
`--limit must be an integer between 1 and ${MAX_LIST_LIMIT}`,
|
|
37
|
+
{
|
|
38
|
+
retryable: false,
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
command: "list",
|
|
44
|
+
requestId: generateRequestId(),
|
|
45
|
+
projectId,
|
|
46
|
+
...(typeof slugPrefix === "string" ? { slugPrefix } : {}),
|
|
47
|
+
...(limitNumber !== undefined ? { limit: limitNumber } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `facet open` — issue a gallery lease + frame URL for a revision.
|
|
3
|
+
*
|
|
4
|
+
* The frame URL does NOT carry the lease id; the lease is delivered
|
|
5
|
+
* out-of-band in the response body. The CLI prints the envelope
|
|
6
|
+
* verbatim and lets the adapter carry the lease to its own
|
|
7
|
+
* transport.
|
|
8
|
+
*
|
|
9
|
+
* Input-validation errors throw `FacetError("invalid_request", ...)`
|
|
10
|
+
* so the envelope preserves the typed `invalid_request` code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { generateRequestId } from "../../shared/util/time";
|
|
14
|
+
import { FacetError } from "../../shared/errors/facet-error";
|
|
15
|
+
import type { OpenRequest } from "../../shared/contracts/commands/requests";
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
export type OpenLauncher = (url: string) => void | Promise<void>;
|
|
19
|
+
|
|
20
|
+
export interface DisplayOpenResult {
|
|
21
|
+
readonly frameUrl: string;
|
|
22
|
+
readonly installToken?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function launchWithXdgOpen(url: string): void {
|
|
26
|
+
const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
27
|
+
child.unref();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function launchDisplay(
|
|
31
|
+
result: DisplayOpenResult,
|
|
32
|
+
launcher: OpenLauncher = launchWithXdgOpen,
|
|
33
|
+
): Promise<boolean> {
|
|
34
|
+
if (result.installToken !== undefined && result.frameUrl.includes(result.installToken)) {
|
|
35
|
+
throw new FacetError("invalid_envelope", "Display URL contains the install token", {
|
|
36
|
+
retryable: false,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const parsed = new URL(result.frameUrl);
|
|
40
|
+
if (parsed.hostname !== "127.0.0.1") {
|
|
41
|
+
throw new FacetError("invalid_envelope", "Display URL must be loopback-only", {
|
|
42
|
+
retryable: false,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await launcher(result.frameUrl);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildOpenRequest(args: Readonly<Record<string, string | boolean>>): OpenRequest {
|
|
54
|
+
const artifactId = args["artifact-id"];
|
|
55
|
+
const revisionSha = args["revision-sha"];
|
|
56
|
+
if (typeof artifactId !== "string" || artifactId.length === 0) {
|
|
57
|
+
throw new FacetError("invalid_request", "--artifact-id is required for open", {
|
|
58
|
+
retryable: false,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
revisionSha !== undefined &&
|
|
63
|
+
(typeof revisionSha !== "string" || !/^[a-f0-9]{64}$/.test(revisionSha))
|
|
64
|
+
) {
|
|
65
|
+
throw new FacetError("invalid_request", "--revision-sha must be a 64-char hex sha256", {
|
|
66
|
+
retryable: false,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
command: "open",
|
|
71
|
+
requestId: generateRequestId(),
|
|
72
|
+
artifactId,
|
|
73
|
+
...(revisionSha === undefined ? {} : { revisionSha }),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `facet pin` — pin or unpin a revision.
|
|
3
|
+
*
|
|
4
|
+
* `--pinned true|false` is parsed as a boolean; anything else is a
|
|
5
|
+
* usage error before the wire.
|
|
6
|
+
*
|
|
7
|
+
* Input-validation errors throw `FacetError("invalid_request", ...)`
|
|
8
|
+
* so the envelope preserves the typed `invalid_request` code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { generateRequestId } from "../../shared/util/time";
|
|
12
|
+
import { FacetError } from "../../shared/errors/facet-error";
|
|
13
|
+
import type { PinRequest } from "../../shared/contracts/commands/requests";
|
|
14
|
+
|
|
15
|
+
function parsePinned(raw: string | boolean | undefined): boolean {
|
|
16
|
+
if (typeof raw === "boolean") return raw;
|
|
17
|
+
if (typeof raw === "string") {
|
|
18
|
+
if (raw === "true") return true;
|
|
19
|
+
if (raw === "false") return false;
|
|
20
|
+
}
|
|
21
|
+
throw new FacetError("invalid_request", "--pinned must be 'true' or 'false'", {
|
|
22
|
+
retryable: false,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildPinRequest(args: Readonly<Record<string, string | boolean>>): PinRequest {
|
|
27
|
+
const revisionId = args["revision-id"];
|
|
28
|
+
if (typeof revisionId !== "string" || revisionId.length === 0) {
|
|
29
|
+
throw new FacetError("invalid_request", "--revision-id is required for pin", {
|
|
30
|
+
retryable: false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
command: "pin",
|
|
35
|
+
requestId: generateRequestId(),
|
|
36
|
+
revisionId,
|
|
37
|
+
pinned: parsePinned(args["pinned"]),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `facet promote` — promote a revision to a named template.
|
|
3
|
+
*
|
|
4
|
+
* Promote requires the operator token (enforced by the service at
|
|
5
|
+
* the router level — install-bearer callers receive 403). The CLI
|
|
6
|
+
* does NOT inject the operator token; out-of-band provisioning is
|
|
7
|
+
* the only path to a working promote, by design (ADR 0001 D4).
|
|
8
|
+
*
|
|
9
|
+
* Input-validation errors throw `FacetError("invalid_request", ...)`
|
|
10
|
+
* so the envelope preserves the typed `invalid_request` code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { generateRequestId } from "../../shared/util/time";
|
|
14
|
+
import { FacetError } from "../../shared/errors/facet-error";
|
|
15
|
+
import type { PromoteRequest } from "../../shared/contracts/commands/requests";
|
|
16
|
+
|
|
17
|
+
export function buildPromoteRequest(
|
|
18
|
+
args: Readonly<Record<string, string | boolean>>,
|
|
19
|
+
): PromoteRequest {
|
|
20
|
+
const revisionId = args["revision-id"];
|
|
21
|
+
const name = args["name"];
|
|
22
|
+
const promotedBy = args["promoted-by"];
|
|
23
|
+
if (typeof revisionId !== "string" || revisionId.length === 0) {
|
|
24
|
+
throw new FacetError("invalid_request", "--revision-id is required for promote", {
|
|
25
|
+
retryable: false,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
29
|
+
throw new FacetError("invalid_request", "--name is required for promote", {
|
|
30
|
+
retryable: false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (typeof promotedBy !== "string" || promotedBy.length === 0) {
|
|
34
|
+
throw new FacetError("invalid_request", "--promoted-by is required for promote", {
|
|
35
|
+
retryable: false,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const artifactId = args["artifact-id"];
|
|
39
|
+
const description = args["description"];
|
|
40
|
+
return {
|
|
41
|
+
command: "promote",
|
|
42
|
+
requestId: generateRequestId(),
|
|
43
|
+
revisionId,
|
|
44
|
+
name,
|
|
45
|
+
promotedBy,
|
|
46
|
+
...(typeof artifactId === "string" ? { artifactId } : {}),
|
|
47
|
+
...(typeof description === "string" ? { description } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|