@tangle-network/agent-app 0.45.27 → 0.45.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/{DesignCanvas-3RRPVPCP.js → DesignCanvas-RSWQCXM3.js} +2 -2
  2. package/dist/{DesignCanvasEditor-KAMMEX7E.js → DesignCanvasEditor-XZMGIST7.js} +4 -4
  3. package/dist/assistant/index.d.ts +63 -39
  4. package/dist/assistant/index.js +23 -5
  5. package/dist/assistant/index.js.map +1 -1
  6. package/dist/chat-react/index.d.ts +25 -2
  7. package/dist/chat-react/index.js.map +1 -1
  8. package/dist/chat-routes/index.js +1 -1
  9. package/dist/{chunk-ODYE4A7L.js → chunk-3I27SDU3.js} +142 -58
  10. package/dist/chunk-3I27SDU3.js.map +1 -0
  11. package/dist/chunk-C3CAYGGQ.js +18 -0
  12. package/dist/chunk-C3CAYGGQ.js.map +1 -0
  13. package/dist/chunk-EMLGWEHV.js +33 -0
  14. package/dist/chunk-EMLGWEHV.js.map +1 -0
  15. package/dist/{chunk-XEA5EG6P.js → chunk-EPZYYWCT.js} +8 -30
  16. package/dist/chunk-EPZYYWCT.js.map +1 -0
  17. package/dist/{chunk-NC2V63EL.js → chunk-FNM7JYME.js} +88 -21
  18. package/dist/chunk-FNM7JYME.js.map +1 -0
  19. package/dist/{chunk-WD2B6HGY.js → chunk-K4RA5T3A.js} +11 -32
  20. package/dist/chunk-K4RA5T3A.js.map +1 -0
  21. package/dist/{chunk-4QVG4WJQ.js → chunk-OYTMILRK.js} +10 -3
  22. package/dist/chunk-OYTMILRK.js.map +1 -0
  23. package/dist/{chunk-OYGM5WLD.js → chunk-PURHUAOR.js} +52 -15
  24. package/dist/chunk-PURHUAOR.js.map +1 -0
  25. package/dist/{chunk-AWPK7ZDS.js → chunk-QGOU3VXH.js} +3 -3
  26. package/dist/{chunk-RT5RKAFX.js → chunk-QGSVF6DZ.js} +8 -18
  27. package/dist/chunk-QGSVF6DZ.js.map +1 -0
  28. package/dist/{chunk-OTLEYHOG.js → chunk-RRH23CGS.js} +24 -4
  29. package/dist/chunk-RRH23CGS.js.map +1 -0
  30. package/dist/{chunk-N3G3FSM4.js → chunk-YXA3U4JZ.js} +9 -47
  31. package/dist/chunk-YXA3U4JZ.js.map +1 -0
  32. package/dist/design-canvas-react/engine.d.ts +15 -1
  33. package/dist/design-canvas-react/engine.js +6 -2
  34. package/dist/design-canvas-react/index.d.ts +13 -1
  35. package/dist/design-canvas-react/index.js +13 -8
  36. package/dist/design-canvas-react/index.js.map +1 -1
  37. package/dist/design-canvas-react/lazy.js +1 -1
  38. package/dist/legibility/cli.js +14 -8
  39. package/dist/legibility/cli.js.map +1 -1
  40. package/dist/legibility/index.js +2 -1
  41. package/dist/peer-floors/cli.js +14 -8
  42. package/dist/peer-floors/cli.js.map +1 -1
  43. package/dist/preflight/cli.js +4 -2
  44. package/dist/preflight/cli.js.map +1 -1
  45. package/dist/{run-tEsZUhAf.d.ts → run-D80hoLKh.d.ts} +1 -1
  46. package/dist/sandbox/index.d.ts +11 -1
  47. package/dist/sandbox/index.js +1 -1
  48. package/dist/signoff/cli.d.ts +2 -2
  49. package/dist/signoff/cli.js +6 -18
  50. package/dist/signoff/cli.js.map +1 -1
  51. package/dist/signoff/index.d.ts +4 -420
  52. package/dist/signoff/index.js +3 -55
  53. package/dist/signoff/proof-cli.js +11 -6
  54. package/dist/signoff/proof-cli.js.map +1 -1
  55. package/dist/signoff/proof.d.ts +70 -271
  56. package/dist/signoff/proof.js +3 -73
  57. package/dist/signoff/proof.js.map +1 -1
  58. package/dist/theme-contract/cli.js +6 -2
  59. package/dist/theme-contract/cli.js.map +1 -1
  60. package/dist/theme-contract/index.js +2 -1
  61. package/dist/{types-U7Nz-txa.d.ts → types-Bd6uW9vQ.d.ts} +1 -1
  62. package/dist/web-react/index.d.ts +51 -44
  63. package/dist/web-react/index.js +1 -1
  64. package/package.json +14 -3
  65. package/dist/chunk-4QVG4WJQ.js.map +0 -1
  66. package/dist/chunk-N3G3FSM4.js.map +0 -1
  67. package/dist/chunk-NC2V63EL.js.map +0 -1
  68. package/dist/chunk-ODYE4A7L.js.map +0 -1
  69. package/dist/chunk-OTLEYHOG.js.map +0 -1
  70. package/dist/chunk-OYGM5WLD.js.map +0 -1
  71. package/dist/chunk-RT5RKAFX.js.map +0 -1
  72. package/dist/chunk-WD2B6HGY.js.map +0 -1
  73. package/dist/chunk-XEA5EG6P.js.map +0 -1
  74. /package/dist/{DesignCanvas-3RRPVPCP.js.map → DesignCanvas-RSWQCXM3.js.map} +0 -0
  75. /package/dist/{DesignCanvasEditor-KAMMEX7E.js.map → DesignCanvasEditor-XZMGIST7.js.map} +0 -0
  76. /package/dist/{chunk-AWPK7ZDS.js.map → chunk-QGOU3VXH.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/design-canvas-react/components/CanvasInsertPanel.tsx","../../src/design-canvas-react/components/LayersPanel.tsx","../../src/design-canvas-react/components/layer-tree.ts"],"sourcesContent":["/**\n * The human \"add element to the canvas\" side panel — a reusable left rail any\n * canvas consumer can mount so a person (not just the agent) can put images,\n * shapes, and text on the canvas. Designed for the `renderSidePanel` slot of\n * {@link DesignCanvasProps}; pass `renderSidePanel={(ctx) => <CanvasInsertPanel … />}`.\n *\n * Endpoints are per-app, so every I/O path is a callback — no product route is\n * hardcoded:\n * - `onUploadImage(file) => Promise<url>` — optional. When provided, the host\n * stores the file and returns the src to insert. The url MUST be http(s) or\n * a rooted `/api/` path (enforced by `assertSceneMediaSrc` before insertion);\n * a `data:` url is rejected, matching the scene model's media boundary.\n * - `loadGenerations?()` — optional provider for \"already generated in this\n * workspace\" images; omit to hide the tab.\n * - `templates?` — optional template set; defaults to {@link DEFAULT_INSERT_TEMPLATES}.\n *\n * Insertion goes through `onInsert`, which the host wires to its\n * `onApplyOperations` pipeline (server-validated, undoable) — the same path\n * every other edit takes.\n *\n * Tokens/icons follow the canvas convention: CSS-var design tokens and inline\n * SVG glyphs, no icon-library or Tailwind-semantic-token dependency.\n */\n\nimport { useEffect, useRef, useState, type ReactElement } from 'react'\nimport type { SceneOperation } from '../../design-canvas/operations'\nimport { assertSceneMediaSrc } from '../../design-canvas/model'\nimport {\n DEFAULT_INSERT_TEMPLATES,\n buildInsertImageOp,\n type InsertPageGeometry,\n type InsertTemplate,\n} from '../insert-builders'\nimport { ImageGlyph, ShapesGlyph } from './glyphs'\n\n/** An already-generated image the panel can offer for one-click insertion. */\nexport interface InsertGeneration {\n id: string\n /** The image url to insert. Must satisfy the scene media boundary\n * (http(s) or rooted `/api/`); rejected otherwise at insert time. */\n url: string\n /** Optional prompt/label shown as the tile's title. */\n label?: string\n}\n\nexport interface CanvasInsertPanelProps {\n canWrite: boolean\n /** The active page new elements are added to. */\n page: InsertPageGeometry\n /** Submit operations through the host's apply pipeline. */\n onInsert(operations: SceneOperation[]): Promise<unknown>\n /** Store an uploaded file and return its src (http(s) or rooted `/api/`). */\n onUploadImage?(file: File): Promise<string>\n /** Optional provider for the Generations tab; omit to hide it. */\n loadGenerations?(): Promise<InsertGeneration[]>\n /** Drop-in templates; defaults to the built-in starter set. */\n templates?: readonly InsertTemplate[]\n /** Accepted upload mime types. Default: PNG/JPEG/GIF/WebP. */\n accept?: string\n className?: string\n}\n\ntype Tab = 'uploads' | 'templates' | 'generations'\n\nconst DEFAULT_ACCEPT = 'image/png,image/jpeg,image/gif,image/webp'\n\n/** Read an image url's natural dimensions in the browser; resolves `{0,0}` when\n * Image is unavailable (SSR/tests), the load fails, or the probe times out —\n * builders fall back to the size cap, so a failed probe still inserts a sensibly\n * sized element rather than blocking insertion. */\nconst PROBE_TIMEOUT_MS = 4000\n\nfunction probeImageSize(url: string): Promise<{ width: number; height: number }> {\n return new Promise((resolve) => {\n if (typeof Image === 'undefined') return resolve({ width: 0, height: 0 })\n let settled = false\n const finish = (size: { width: number; height: number }) => {\n if (settled) return\n settled = true\n resolve(size)\n }\n const img = new Image()\n img.onload = () => finish({ width: img.naturalWidth, height: img.naturalHeight })\n img.onerror = () => finish({ width: 0, height: 0 })\n // A URL that never loads (or an environment that never fires load/error)\n // must not hang the insert forever.\n setTimeout(() => finish({ width: 0, height: 0 }), PROBE_TIMEOUT_MS)\n img.src = url\n })\n}\n\nfunction UploadGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 9l5-5 5 5M12 4v12\" />\n </svg>\n )\n}\n\nfunction SparkleGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M12 3v3m0 12v3M3 12h3m12 0h3M5.6 5.6l2.1 2.1m8.6 8.6 2.1 2.1m0-12.8-2.1 2.1M7.7 16.3l-2.1 2.1\" />\n </svg>\n )\n}\n\nfunction SpinnerGlyph({ className }: { className?: string }) {\n return (\n <svg className={`${className ?? ''} animate-spin`} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" aria-hidden>\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\" strokeLinecap=\"round\" />\n </svg>\n )\n}\n\n/** Probe-page geometry for deriving a template's primary element kind. The\n * result drives only the tile preview, so the exact dimensions don't matter. */\nconst PREVIEW_PROBE_PAGE: InsertPageGeometry = { pageId: 'preview', width: 1000, height: 1000 }\n\n/** The visual a template tile shows. Derived from the first element the\n * template's (pure) `build` produces, so custom templates render correctly\n * too — not just the built-in set. */\ntype TemplateShape = 'heading' | 'body' | 'rect' | 'ellipse' | 'other'\n\n/** A text element is treated as a heading (shown as \"T\") when it reads as a\n * title — bold or set in a large face; otherwise body copy (shown as \"¶\"). */\nconst HEADING_FONT_SIZE = 32\n\nfunction templateShape(tpl: InsertTemplate): TemplateShape {\n try {\n const ops = tpl.build(PREVIEW_PROBE_PAGE)\n const added = ops.find((op) => op.type === 'add_element')\n if (!added || added.type !== 'add_element') return 'other'\n const el = added.element\n if (el.kind === 'rect') return 'rect'\n if (el.kind === 'ellipse') return 'ellipse'\n if (el.kind === 'text') {\n const isHeading = el.fontStyle.includes('bold') || el.fontSize >= HEADING_FONT_SIZE\n return isHeading ? 'heading' : 'body'\n }\n return 'other'\n } catch {\n // A throwing build can't preview; fall through to the neutral shape glyph.\n return 'other'\n }\n}\n\n/** A small visual standing in for what the template inserts, so tiles are\n * distinguishable at a glance instead of four identical text chips. */\nfunction TemplatePreview({ shape }: { shape: TemplateShape }) {\n if (shape === 'rect') {\n return <span className=\"block h-6 w-9 rounded-sm bg-[var(--brand-primary)]\" aria-hidden />\n }\n if (shape === 'ellipse') {\n return <span className=\"block h-7 w-7 rounded-full bg-[var(--brand-primary)]\" aria-hidden />\n }\n // Text tiles show a glyph: a bold \"T\" for a heading, a paragraph mark \"¶\"\n // for body copy.\n if (shape === 'heading' || shape === 'body') {\n return (\n <span\n className={`block leading-none text-[var(--text-primary)] ${\n shape === 'heading' ? 'text-2xl font-bold' : 'text-lg'\n }`}\n aria-hidden\n >\n {shape === 'heading' ? 'T' : '¶'}\n </span>\n )\n }\n return <ShapesGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n}\n\nexport function CanvasInsertPanel({\n canWrite,\n page,\n onInsert,\n onUploadImage,\n loadGenerations,\n templates = DEFAULT_INSERT_TEMPLATES,\n accept = DEFAULT_ACCEPT,\n className,\n}: CanvasInsertPanelProps) {\n const [tab, setTab] = useState<Tab>(() => (templates.length > 0 ? 'templates' : 'uploads'))\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState('')\n const [dragOver, setDragOver] = useState(false)\n const [generations, setGenerations] = useState<InsertGeneration[]>([])\n const [generationsLoaded, setGenerationsLoaded] = useState(false)\n const fileInputRef = useRef<HTMLInputElement | null>(null)\n\n useEffect(() => {\n if (tab === 'uploads' && !onUploadImage) {\n setTab(templates.length > 0 ? 'templates' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'templates' && templates.length === 0) {\n setTab(onUploadImage ? 'uploads' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'generations' && !loadGenerations) {\n setTab(templates.length > 0 ? 'templates' : onUploadImage ? 'uploads' : 'templates')\n }\n }, [tab, templates.length, onUploadImage, loadGenerations])\n\n useEffect(() => {\n if (tab !== 'generations' || generationsLoaded || !loadGenerations) return\n let cancelled = false\n void (async () => {\n try {\n const rows = await loadGenerations()\n if (!cancelled) setGenerations(rows.filter((g) => g.url))\n } catch (err) {\n if (!cancelled) setError(err instanceof Error ? err.message : 'Could not load generations')\n } finally {\n if (!cancelled) setGenerationsLoaded(true)\n }\n })()\n return () => {\n cancelled = true\n }\n }, [tab, generationsLoaded, loadGenerations])\n\n async function insertImageFromUrl(url: string) {\n // Validate the media boundary BEFORE probing — a data: or sandbox-local url\n // throws here (surfaced as an inline error) without a wasted image load.\n assertSceneMediaSrc(url, 'image src')\n const natural = await probeImageSize(url)\n await onInsert([buildInsertImageOp(url, natural, page)])\n }\n\n async function handleFiles(files: FileList | File[]) {\n if (!canWrite || busy) return\n if (!onUploadImage) return\n const list = Array.from(files).filter((f) => f.type.startsWith('image/'))\n if (list.length === 0) {\n setError('Only image files can be added to the canvas')\n return\n }\n setBusy(true)\n setError('')\n try {\n for (const file of list) {\n const url = await onUploadImage(file)\n await insertImageFromUrl(url)\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Upload failed')\n } finally {\n setBusy(false)\n }\n }\n\n async function runInsert(build: () => SceneOperation[] | Promise<SceneOperation[]>) {\n if (!canWrite || busy) return\n setBusy(true)\n setError('')\n try {\n await onInsert(await build())\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Insert failed')\n } finally {\n setBusy(false)\n }\n }\n\n const tabs: Array<{ id: Tab; label: string; icon: (p: { className?: string }) => ReactElement; show: boolean }> = [\n { id: 'uploads', label: 'Uploads', icon: ImageGlyph, show: !!onUploadImage },\n { id: 'templates', label: 'Templates', icon: ShapesGlyph, show: templates.length > 0 },\n { id: 'generations', label: 'Generations', icon: SparkleGlyph, show: !!loadGenerations },\n ]\n const visibleTabs = tabs.filter((t) => t.show)\n\n return (\n <div className={`flex h-full min-h-0 flex-col bg-[var(--bg-input)] text-[var(--text-primary)] ${className ?? ''}`}>\n <div className=\"flex shrink-0 border-b border-[var(--border-default)]\">\n {visibleTabs.map(({ id, label, icon: Icon }) => (\n <button\n key={id}\n type=\"button\"\n onClick={() => setTab(id)}\n // These tabs sit flush against the top and side edges of a panel\n // that clips its overflow, so the floor's outward ring is cut off on\n // three sides and reads as a stray underline. The negative offset\n // draws the same ring inside the tab; width and colour still come\n // from the tokens.\n className={`flex flex-1 items-center justify-center gap-1.5 px-2 py-2.5 text-xs font-medium transition-colors focus-visible:[outline-offset:-2px] ${\n tab === id\n ? 'border-b-2 border-[var(--brand-primary)] text-[var(--text-primary)]'\n : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'\n }`}\n >\n <Icon className=\"h-3.5 w-3.5\" />\n {label}\n </button>\n ))}\n </div>\n\n {!canWrite ? (\n <div className=\"flex flex-1 items-center justify-center p-4 text-center text-sm text-[var(--text-muted)]\">\n You have view-only access to this design.\n </div>\n ) : (\n <div className=\"min-h-0 flex-1 overflow-y-auto p-3\">\n {tab === 'uploads' && (\n <div className=\"flex flex-col gap-3\">\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => fileInputRef.current?.click()}\n onDragOver={(e) => {\n e.preventDefault()\n setDragOver(true)\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(e) => {\n e.preventDefault()\n setDragOver(false)\n if (e.dataTransfer?.files?.length) void handleFiles(e.dataTransfer.files)\n }}\n className={`flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors disabled:opacity-50 ${\n dragOver\n ? 'border-[var(--brand-primary)] bg-[var(--brand-primary)]/5'\n : 'border-[var(--border-default)] hover:border-[var(--brand-primary)]/40'\n }`}\n >\n {busy ? (\n <SpinnerGlyph className=\"h-6 w-6 text-[var(--brand-primary)]\" />\n ) : (\n <UploadGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n )}\n <span className=\"text-sm font-medium text-[var(--text-primary)]\">\n {busy ? 'Uploading…' : 'Drop an image or click to upload'}\n </span>\n <span className=\"text-xs text-[var(--text-muted)]\">PNG, JPEG, GIF, or WebP</span>\n </button>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n multiple\n className=\"hidden\"\n onChange={(e) => {\n if (e.target.files?.length) void handleFiles(e.target.files)\n e.target.value = ''\n }}\n />\n </div>\n )}\n\n {tab === 'templates' && (\n <div className=\"grid grid-cols-2 gap-2\">\n {templates.map((tpl) => (\n <button\n key={tpl.id}\n type=\"button\"\n disabled={busy}\n onClick={() => void runInsert(() => tpl.build(page))}\n className=\"flex h-20 flex-col items-center justify-center gap-1.5 rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[var(--brand-primary)]/40 disabled:opacity-50\"\n >\n <span className=\"flex h-7 items-center justify-center\">\n <TemplatePreview shape={templateShape(tpl)} />\n </span>\n <span>{tpl.label}</span>\n </button>\n ))}\n </div>\n )}\n\n {tab === 'generations' && (\n <div className=\"flex flex-col gap-2\">\n {!generationsLoaded ? (\n <div className=\"flex items-center justify-center py-8 text-[var(--text-muted)]\">\n <SpinnerGlyph className=\"h-5 w-5\" />\n </div>\n ) : generations.length === 0 ? (\n <p className=\"px-1 py-6 text-center text-sm text-[var(--text-muted)]\">\n No generated images yet. Ask the agent to generate one.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-2\">\n {generations.map((gen) => (\n <button\n key={gen.id}\n type=\"button\"\n disabled={busy}\n onClick={() => {\n if (busy || !canWrite) return\n setBusy(true)\n setError('')\n insertImageFromUrl(gen.url)\n .catch((err) => setError(err instanceof Error ? err.message : 'Insert failed'))\n .finally(() => setBusy(false))\n }}\n title={gen.label}\n className=\"group relative aspect-square overflow-hidden rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] transition-colors hover:border-[var(--brand-primary)] disabled:opacity-50\"\n >\n {/* eslint-disable-next-line jsx-a11y/img-redundant-alt */}\n <img src={gen.url} alt={gen.label || 'Generated image'} className=\"h-full w-full object-cover\" />\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n\n {error ? <p className=\"mt-3 text-xs leading-5 text-[var(--text-danger,#dc2626)]\">{error}</p> : null}\n </div>\n )}\n </div>\n )\n}\n","/**\n * Layers panel — reverse-z list of the active page's elements. Highest z-index\n * is at the top. Groups show their children indented beneath them. Clicking\n * selects an element; meta-click adds to selection. Double-clicking the name\n * opens an inline rename input. Eye and lock icons toggle visibility/locked.\n * Rows can be drag-reordered within the list (siblings only; cross-group reorder\n * is a pending integrator concern). Capped at LAYERS_PANEL_ROW_LIMIT rows.\n */\n\nimport { useCallback, useMemo, useRef, useState } from 'react'\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\nimport { flattenLayerTree, LAYERS_PANEL_ROW_LIMIT } from './layer-tree'\nimport {\n EllipseGlyph,\n EyeGlyph,\n EyeOffGlyph,\n GroupGlyph,\n ImageGlyph,\n LineGlyph,\n LockGlyph,\n RectGlyph,\n SlotGlyph,\n TextGlyph,\n UnlockGlyph,\n VideoGlyph,\n} from './glyphs'\nimport type { SceneElementKind } from '../../design-canvas/model'\n\nfunction KindIcon({ kind, className }: { kind: SceneElementKind; className?: string }) {\n switch (kind) {\n case 'rect': return <RectGlyph className={className} />\n case 'ellipse': return <EllipseGlyph className={className} />\n case 'line': return <LineGlyph className={className} />\n case 'text': return <TextGlyph className={className} />\n case 'image': return <ImageGlyph className={className} />\n case 'video': return <VideoGlyph className={className} />\n case 'group': return <GroupGlyph className={className} />\n }\n}\n\nexport interface LayersPanelProps {\n page: ScenePage\n selectedElementIds: string[]\n canWrite: boolean\n /** Emit a set_attrs command for the given element. */\n onSetAttrs(elementId: string, attrs: Partial<Pick<SceneElement, 'name' | 'visible' | 'locked'>>): void\n /** Emit a reorder_element command. */\n onReorder(elementId: string, toIndex: number): void\n onSelect(elementId: string, additive: boolean): void\n}\n\nconst INDENT_PX = 16\n\nexport function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder, onSelect }: LayersPanelProps) {\n const rows = useMemo(() => flattenLayerTree(page), [page])\n const [renamingId, setRenamingId] = useState<string | null>(null)\n const [renameValue, setRenameValue] = useState('')\n\n // Drag-reorder state: we track the dragged row's ownerIndex and the target\n // index within the SAME owner. Cross-owner reorder is not supported here.\n const dragRowRef = useRef<{ elementId: string; ownerIndex: number; ownerLength: number } | null>(null)\n const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)\n\n function startRename(element: SceneElement) {\n setRenamingId(element.id)\n setRenameValue(element.name)\n }\n\n function commitRename(elementId: string) {\n const name = renameValue.trim()\n if (name.length > 0) onSetAttrs(elementId, { name })\n setRenamingId(null)\n }\n\n const visible = rows.length > LAYERS_PANEL_ROW_LIMIT ? rows.slice(0, LAYERS_PANEL_ROW_LIMIT) : rows\n\n return (\n <div className=\"flex h-full flex-col overflow-hidden text-[var(--text-primary)]\">\n <div className=\"shrink-0 border-b border-[var(--border-default)] px-3 py-1.5 text-[11px] font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n Layers\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {visible.map((row) => {\n const { element } = row\n const isSelected = selectedElementIds.includes(element.id)\n const isRenaming = renamingId === element.id\n\n return (\n <div\n key={element.id}\n data-layer-row={element.id}\n draggable={canWrite}\n onDragStart={() => {\n dragRowRef.current = {\n elementId: element.id,\n ownerIndex: row.ownerIndex,\n ownerLength: row.ownerLength,\n }\n }}\n onDragOver={(event) => {\n if (!dragRowRef.current) return\n // Only allow reorder within the same owner (same parentGroupId + depth)\n event.preventDefault()\n setDragOverIndex(row.ownerIndex)\n }}\n onDragLeave={() => setDragOverIndex(null)}\n onDrop={() => {\n const drag = dragRowRef.current\n if (!drag) return\n if (drag.elementId !== element.id) {\n onReorder(drag.elementId, row.ownerIndex)\n }\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n onDragEnd={() => {\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n className={[\n 'group flex items-center gap-1.5 py-1 pr-2 text-[13px] transition-colors',\n isSelected ? 'bg-[var(--brand-primary)]/15 text-[var(--text-primary)]' : 'hover:bg-[var(--border-default)]/40 text-[var(--text-secondary)]',\n dragOverIndex === row.ownerIndex ? 'border-t border-[var(--brand-primary)]' : '',\n ].join(' ')}\n style={{ paddingLeft: 8 + row.depth * INDENT_PX }}\n >\n {isRenaming ? (\n <input\n autoFocus\n value={renameValue}\n onChange={(event) => setRenameValue(event.target.value)}\n onBlur={() => commitRename(element.id)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') commitRename(element.id)\n if (event.key === 'Escape') setRenamingId(null)\n event.stopPropagation()\n }}\n className=\"min-w-0 flex-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0 text-[13px] text-[var(--text-primary)] focus:border-[var(--brand-primary)]\"\n />\n ) : (\n // Selection is a real button (sibling of the eye/lock buttons) so\n // the row is not an interactive control nesting interactive\n // controls (axe: nested-interactive).\n <button\n type=\"button\"\n onClick={(event) => onSelect(element.id, event.metaKey || event.ctrlKey)}\n onDoubleClick={() => {\n if (canWrite) startRename(element)\n }}\n title={element.name}\n className=\"flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 bg-transparent text-left\"\n >\n <KindIcon kind={element.kind} className=\"h-3.5 w-3.5 shrink-0 opacity-60\" />\n {element.slot ? <SlotGlyph className=\"h-3 w-3 shrink-0 text-[var(--brand-primary)]\" /> : null}\n <span className=\"min-w-0 flex-1 truncate\">{element.name}</span>\n </button>\n )}\n\n {/* Visibility toggle */}\n <button\n type=\"button\"\n aria-label={element.visible ? 'Hide element' : 'Show element'}\n aria-pressed={!element.visible}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { visible: !element.visible })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.visible ? 'opacity-0' : 'opacity-100',\n ].join(' ')}\n >\n {element.visible\n ? <EyeGlyph className=\"h-3.5 w-3.5\" />\n : <EyeOffGlyph className=\"h-3.5 w-3.5 text-[var(--text-muted)]\" />}\n </button>\n\n {/* Lock toggle */}\n <button\n type=\"button\"\n aria-label={element.locked ? 'Unlock element' : 'Lock element'}\n aria-pressed={!!element.locked}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { locked: !element.locked })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.locked ? 'opacity-100' : 'opacity-0',\n ].join(' ')}\n >\n {element.locked\n ? <LockGlyph className=\"h-3.5 w-3.5 text-[var(--text-warning)]\" />\n : <UnlockGlyph className=\"h-3.5 w-3.5\" />}\n </button>\n </div>\n )\n })}\n\n {rows.length > LAYERS_PANEL_ROW_LIMIT ? (\n <div className=\"px-3 py-2 text-[11px] text-[var(--text-muted)]\">\n +{rows.length - LAYERS_PANEL_ROW_LIMIT} more elements — select a group to scope the list\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n","/**\n * Pure layer-tree helpers: flatten a page's element array into an ordered list\n * for the LayersPanel, preserving group nesting. Extracted so render order,\n * depth, and selection math can be unit-tested independently of React.\n */\n\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\n\n/** Describe a layer row with its element, depth, grouping, ownership, and parent group details */\nexport interface LayerRow {\n element: SceneElement\n depth: number\n /** True when this row is a group whose children follow it in the list. */\n isGroup: boolean\n /** Index within its owner (page.elements or group.children). */\n ownerIndex: number\n /** Length of the owner array — needed for z-order bound math. */\n ownerLength: number\n /** Id of the containing group, or null for page-root elements. */\n parentGroupId: string | null\n}\n\n/**\n * Flatten `page.elements` into display order for the layers panel.\n *\n * Z-order is bottom→top in the element array; the layers panel shows\n * top→bottom (highest z-index first). A group row precedes its children\n * (the group is \"above\" its children in the panel, reflecting that the group\n * node itself paints last in Konva when empty; children paint inside it).\n *\n * Depth increases by 1 for each group level, starting at 0 for page-root\n * elements. Groups are expanded; collapsed state is a view concern the caller\n * tracks separately.\n */\nexport function flattenLayerTree(page: ScenePage): LayerRow[] {\n const rows: LayerRow[] = []\n\n function visit(elements: SceneElement[], depth: number, parentGroupId: string | null) {\n // Reverse so highest z-index appears at the top of the panel.\n for (let i = elements.length - 1; i >= 0; i -= 1) {\n const element = elements[i]!\n rows.push({\n element,\n depth,\n isGroup: element.kind === 'group',\n ownerIndex: i,\n ownerLength: elements.length,\n parentGroupId,\n })\n if (element.kind === 'group') {\n visit(element.children, depth + 1, element.id)\n }\n }\n }\n\n visit(page.elements, 0, null)\n return rows\n}\n\n/** The maximum number of rows the layers panel renders before truncating. */\nexport const LAYERS_PANEL_ROW_LIMIT = 500\n"],"mappings":"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","names":["useRef","useState","jsx","jsxs","useState","useRef"]}
1
+ {"version":3,"sources":["../../src/design-canvas-react/components/CanvasInsertPanel.tsx","../../src/design-canvas-react/components/LayersPanel.tsx","../../src/design-canvas-react/components/layer-tree.ts"],"sourcesContent":["/**\n * The human \"add element to the canvas\" side panel — a reusable left rail any\n * canvas consumer can mount so a person (not just the agent) can put images,\n * shapes, and text on the canvas. Designed for the `renderSidePanel` slot of\n * {@link DesignCanvasProps}; pass `renderSidePanel={(ctx) => <CanvasInsertPanel … />}`.\n *\n * Endpoints are per-app, so every I/O path is a callback — no product route is\n * hardcoded:\n * - `onUploadImage(file) => Promise<url>` — optional. When provided, the host\n * stores the file and returns the src to insert. The url MUST be http(s) or\n * a rooted `/api/` path (enforced by `assertSceneMediaSrc` before insertion);\n * a `data:` url is rejected, matching the scene model's media boundary.\n * - `loadGenerations?()` — optional provider for \"already generated in this\n * workspace\" images; omit to hide the tab.\n * - `templates?` — optional template set; defaults to {@link DEFAULT_INSERT_TEMPLATES}.\n *\n * Insertion goes through `onInsert`, which the host wires to its\n * `onApplyOperations` pipeline (server-validated, undoable) — the same path\n * every other edit takes.\n *\n * Tokens/icons follow the canvas convention: CSS-var design tokens and inline\n * SVG glyphs, no icon-library or Tailwind-semantic-token dependency.\n */\n\nimport { useEffect, useRef, useState, type ReactElement } from 'react'\nimport type { SceneOperation } from '../../design-canvas/operations'\nimport { assertSceneMediaSrc } from '../../design-canvas/model'\nimport {\n DEFAULT_INSERT_TEMPLATES,\n buildInsertImageOp,\n type InsertPageGeometry,\n type InsertTemplate,\n} from '../insert-builders'\nimport { ImageGlyph, ShapesGlyph } from './glyphs'\n\n/** An already-generated image the panel can offer for one-click insertion. */\nexport interface InsertGeneration {\n id: string\n /** The image url to insert. Must satisfy the scene media boundary\n * (http(s) or rooted `/api/`); rejected otherwise at insert time. */\n url: string\n /** Optional prompt/label shown as the tile's title. */\n label?: string\n}\n\nexport interface CanvasInsertPanelProps {\n canWrite: boolean\n /** The active page new elements are added to. */\n page: InsertPageGeometry\n /** Submit operations through the host's apply pipeline. */\n onInsert(operations: SceneOperation[]): Promise<unknown>\n /** Store an uploaded file and return its src (http(s) or rooted `/api/`). */\n onUploadImage?(file: File): Promise<string>\n /** Optional provider for the Generations tab; omit to hide it. */\n loadGenerations?(): Promise<InsertGeneration[]>\n /** Drop-in templates; defaults to the built-in starter set. */\n templates?: readonly InsertTemplate[]\n /** Accepted upload mime types. Default: PNG/JPEG/GIF/WebP. */\n accept?: string\n className?: string\n}\n\ntype Tab = 'uploads' | 'templates' | 'generations'\n\nconst DEFAULT_ACCEPT = 'image/png,image/jpeg,image/gif,image/webp'\n\n/** Read an image url's natural dimensions in the browser; resolves `{0,0}` when\n * Image is unavailable (SSR/tests), the load fails, or the probe times out —\n * builders fall back to the size cap, so a failed probe still inserts a sensibly\n * sized element rather than blocking insertion. */\nconst PROBE_TIMEOUT_MS = 4000\n\nfunction probeImageSize(url: string): Promise<{ width: number; height: number }> {\n return new Promise((resolve) => {\n if (typeof Image === 'undefined') return resolve({ width: 0, height: 0 })\n let settled = false\n const finish = (size: { width: number; height: number }) => {\n if (settled) return\n settled = true\n resolve(size)\n }\n const img = new Image()\n img.onload = () => finish({ width: img.naturalWidth, height: img.naturalHeight })\n img.onerror = () => finish({ width: 0, height: 0 })\n // A URL that never loads (or an environment that never fires load/error)\n // must not hang the insert forever.\n setTimeout(() => finish({ width: 0, height: 0 }), PROBE_TIMEOUT_MS)\n img.src = url\n })\n}\n\nfunction UploadGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 9l5-5 5 5M12 4v12\" />\n </svg>\n )\n}\n\nfunction SparkleGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M12 3v3m0 12v3M3 12h3m12 0h3M5.6 5.6l2.1 2.1m8.6 8.6 2.1 2.1m0-12.8-2.1 2.1M7.7 16.3l-2.1 2.1\" />\n </svg>\n )\n}\n\nfunction SpinnerGlyph({ className }: { className?: string }) {\n return (\n <svg className={`${className ?? ''} animate-spin`} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" aria-hidden>\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\" strokeLinecap=\"round\" />\n </svg>\n )\n}\n\n/** Probe-page geometry for deriving a template's primary element kind. The\n * result drives only the tile preview, so the exact dimensions don't matter. */\nconst PREVIEW_PROBE_PAGE: InsertPageGeometry = { pageId: 'preview', width: 1000, height: 1000 }\n\n/** The visual a template tile shows. Derived from the first element the\n * template's (pure) `build` produces, so custom templates render correctly\n * too — not just the built-in set. */\ntype TemplateShape = 'heading' | 'body' | 'rect' | 'ellipse' | 'other'\n\n/** A text element is treated as a heading (shown as \"T\") when it reads as a\n * title — bold or set in a large face; otherwise body copy (shown as \"¶\"). */\nconst HEADING_FONT_SIZE = 32\n\nfunction templateShape(tpl: InsertTemplate): TemplateShape {\n try {\n const ops = tpl.build(PREVIEW_PROBE_PAGE)\n const added = ops.find((op) => op.type === 'add_element')\n if (!added || added.type !== 'add_element') return 'other'\n const el = added.element\n if (el.kind === 'rect') return 'rect'\n if (el.kind === 'ellipse') return 'ellipse'\n if (el.kind === 'text') {\n const isHeading = el.fontStyle.includes('bold') || el.fontSize >= HEADING_FONT_SIZE\n return isHeading ? 'heading' : 'body'\n }\n return 'other'\n } catch {\n // A throwing build can't preview; fall through to the neutral shape glyph.\n return 'other'\n }\n}\n\n/** A small visual standing in for what the template inserts, so tiles are\n * distinguishable at a glance instead of four identical text chips. */\nfunction TemplatePreview({ shape }: { shape: TemplateShape }) {\n if (shape === 'rect') {\n return <span className=\"block h-6 w-9 rounded-sm bg-[var(--brand-primary)]\" aria-hidden />\n }\n if (shape === 'ellipse') {\n return <span className=\"block h-7 w-7 rounded-full bg-[var(--brand-primary)]\" aria-hidden />\n }\n // Text tiles show a glyph: a bold \"T\" for a heading, a paragraph mark \"¶\"\n // for body copy.\n if (shape === 'heading' || shape === 'body') {\n return (\n <span\n className={`block leading-none text-[var(--text-primary)] ${\n shape === 'heading' ? 'text-2xl font-bold' : 'text-lg'\n }`}\n aria-hidden\n >\n {shape === 'heading' ? 'T' : '¶'}\n </span>\n )\n }\n return <ShapesGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n}\n\nexport function CanvasInsertPanel({\n canWrite,\n page,\n onInsert,\n onUploadImage,\n loadGenerations,\n templates = DEFAULT_INSERT_TEMPLATES,\n accept = DEFAULT_ACCEPT,\n className,\n}: CanvasInsertPanelProps) {\n const [tab, setTab] = useState<Tab>(() => (templates.length > 0 ? 'templates' : 'uploads'))\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState('')\n const [dragOver, setDragOver] = useState(false)\n const [generations, setGenerations] = useState<InsertGeneration[]>([])\n const [generationsLoaded, setGenerationsLoaded] = useState(false)\n const fileInputRef = useRef<HTMLInputElement | null>(null)\n\n useEffect(() => {\n if (tab === 'uploads' && !onUploadImage) {\n setTab(templates.length > 0 ? 'templates' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'templates' && templates.length === 0) {\n setTab(onUploadImage ? 'uploads' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'generations' && !loadGenerations) {\n setTab(templates.length > 0 ? 'templates' : onUploadImage ? 'uploads' : 'templates')\n }\n }, [tab, templates.length, onUploadImage, loadGenerations])\n\n useEffect(() => {\n if (tab !== 'generations' || generationsLoaded || !loadGenerations) return\n let cancelled = false\n void (async () => {\n try {\n const rows = await loadGenerations()\n if (!cancelled) setGenerations(rows.filter((g) => g.url))\n } catch (err) {\n if (!cancelled) setError(err instanceof Error ? err.message : 'Could not load generations')\n } finally {\n if (!cancelled) setGenerationsLoaded(true)\n }\n })()\n return () => {\n cancelled = true\n }\n }, [tab, generationsLoaded, loadGenerations])\n\n async function insertImageFromUrl(url: string) {\n // Validate the media boundary BEFORE probing — a data: or sandbox-local url\n // throws here (surfaced as an inline error) without a wasted image load.\n assertSceneMediaSrc(url, 'image src')\n const natural = await probeImageSize(url)\n await onInsert([buildInsertImageOp(url, natural, page)])\n }\n\n async function handleFiles(files: FileList | File[]) {\n if (!canWrite || busy) return\n if (!onUploadImage) return\n const list = Array.from(files).filter((f) => f.type.startsWith('image/'))\n if (list.length === 0) {\n setError('Only image files can be added to the canvas')\n return\n }\n setBusy(true)\n setError('')\n try {\n for (const file of list) {\n const url = await onUploadImage(file)\n await insertImageFromUrl(url)\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Upload failed')\n } finally {\n setBusy(false)\n }\n }\n\n async function runInsert(build: () => SceneOperation[] | Promise<SceneOperation[]>) {\n if (!canWrite || busy) return\n setBusy(true)\n setError('')\n try {\n await onInsert(await build())\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Insert failed')\n } finally {\n setBusy(false)\n }\n }\n\n const tabs: Array<{ id: Tab; label: string; icon: (p: { className?: string }) => ReactElement; show: boolean }> = [\n { id: 'uploads', label: 'Uploads', icon: ImageGlyph, show: !!onUploadImage },\n { id: 'templates', label: 'Templates', icon: ShapesGlyph, show: templates.length > 0 },\n { id: 'generations', label: 'Generations', icon: SparkleGlyph, show: !!loadGenerations },\n ]\n const visibleTabs = tabs.filter((t) => t.show)\n\n return (\n <div className={`flex h-full min-h-0 flex-col bg-[var(--bg-input)] text-[var(--text-primary)] ${className ?? ''}`}>\n <div className=\"flex shrink-0 border-b border-[var(--border-default)]\">\n {visibleTabs.map(({ id, label, icon: Icon }) => (\n <button\n key={id}\n type=\"button\"\n onClick={() => setTab(id)}\n // These tabs sit flush against the top and side edges of a panel\n // that clips its overflow, so the floor's outward ring is cut off on\n // three sides and reads as a stray underline. The negative offset\n // draws the same ring inside the tab; width and colour still come\n // from the tokens.\n className={`flex flex-1 items-center justify-center gap-1.5 px-2 py-2.5 text-xs font-medium transition-colors focus-visible:[outline-offset:-2px] ${\n tab === id\n ? 'border-b-2 border-[var(--brand-primary)] text-[var(--text-primary)]'\n : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'\n }`}\n >\n <Icon className=\"h-3.5 w-3.5\" />\n {label}\n </button>\n ))}\n </div>\n\n {!canWrite ? (\n <div className=\"flex flex-1 items-center justify-center p-4 text-center text-sm text-[var(--text-muted)]\">\n You have view-only access to this design.\n </div>\n ) : (\n <div className=\"min-h-0 flex-1 overflow-y-auto p-3\">\n {tab === 'uploads' && (\n <div className=\"flex flex-col gap-3\">\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => fileInputRef.current?.click()}\n onDragOver={(e) => {\n e.preventDefault()\n setDragOver(true)\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(e) => {\n e.preventDefault()\n setDragOver(false)\n if (e.dataTransfer?.files?.length) void handleFiles(e.dataTransfer.files)\n }}\n className={`flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors disabled:opacity-50 ${\n dragOver\n ? 'border-[var(--brand-primary)] bg-[color-mix(in_srgb,var(--brand-primary)_5%,transparent)]'\n : 'border-[var(--border-default)] hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)]'\n }`}\n >\n {busy ? (\n <SpinnerGlyph className=\"h-6 w-6 text-[var(--brand-primary)]\" />\n ) : (\n <UploadGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n )}\n <span className=\"text-sm font-medium text-[var(--text-primary)]\">\n {busy ? 'Uploading…' : 'Drop an image or click to upload'}\n </span>\n <span className=\"text-xs text-[var(--text-muted)]\">PNG, JPEG, GIF, or WebP</span>\n </button>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n multiple\n className=\"hidden\"\n onChange={(e) => {\n if (e.target.files?.length) void handleFiles(e.target.files)\n e.target.value = ''\n }}\n />\n </div>\n )}\n\n {tab === 'templates' && (\n <div className=\"grid grid-cols-2 gap-2\">\n {templates.map((tpl) => (\n <button\n key={tpl.id}\n type=\"button\"\n disabled={busy}\n onClick={() => void runInsert(() => tpl.build(page))}\n className=\"flex h-20 flex-col items-center justify-center gap-1.5 rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)] disabled:opacity-50\"\n >\n <span className=\"flex h-7 items-center justify-center\">\n <TemplatePreview shape={templateShape(tpl)} />\n </span>\n <span>{tpl.label}</span>\n </button>\n ))}\n </div>\n )}\n\n {tab === 'generations' && (\n <div className=\"flex flex-col gap-2\">\n {!generationsLoaded ? (\n <div className=\"flex items-center justify-center py-8 text-[var(--text-muted)]\">\n <SpinnerGlyph className=\"h-5 w-5\" />\n </div>\n ) : generations.length === 0 ? (\n <p className=\"px-1 py-6 text-center text-sm text-[var(--text-muted)]\">\n No generated images yet. Ask the agent to generate one.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-2\">\n {generations.map((gen) => (\n <button\n key={gen.id}\n type=\"button\"\n disabled={busy}\n onClick={() => {\n if (busy || !canWrite) return\n setBusy(true)\n setError('')\n insertImageFromUrl(gen.url)\n .catch((err) => setError(err instanceof Error ? err.message : 'Insert failed'))\n .finally(() => setBusy(false))\n }}\n title={gen.label}\n className=\"group relative aspect-square overflow-hidden rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] transition-colors hover:border-[var(--brand-primary)] disabled:opacity-50\"\n >\n {/* eslint-disable-next-line jsx-a11y/img-redundant-alt */}\n <img src={gen.url} alt={gen.label || 'Generated image'} className=\"h-full w-full object-cover\" />\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n\n {error ? <p className=\"mt-3 text-xs leading-5 text-[var(--text-danger,#dc2626)]\">{error}</p> : null}\n </div>\n )}\n </div>\n )\n}\n","/**\n * Layers panel — reverse-z list of the active page's elements. Highest z-index\n * is at the top. Groups show their children indented beneath them. Clicking\n * selects an element; meta-click adds to selection. Double-clicking the name\n * opens an inline rename input. Eye and lock icons toggle visibility/locked.\n * Rows can be drag-reordered within the list (siblings only; cross-group reorder\n * is a pending integrator concern). Capped at LAYERS_PANEL_ROW_LIMIT rows.\n */\n\nimport { useCallback, useMemo, useRef, useState } from 'react'\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\nimport { flattenLayerTree, LAYERS_PANEL_ROW_LIMIT } from './layer-tree'\nimport {\n EllipseGlyph,\n EyeGlyph,\n EyeOffGlyph,\n GroupGlyph,\n ImageGlyph,\n LineGlyph,\n LockGlyph,\n RectGlyph,\n SlotGlyph,\n TextGlyph,\n UnlockGlyph,\n VideoGlyph,\n} from './glyphs'\nimport type { SceneElementKind } from '../../design-canvas/model'\n\nfunction KindIcon({ kind, className }: { kind: SceneElementKind; className?: string }) {\n switch (kind) {\n case 'rect': return <RectGlyph className={className} />\n case 'ellipse': return <EllipseGlyph className={className} />\n case 'line': return <LineGlyph className={className} />\n case 'text': return <TextGlyph className={className} />\n case 'image': return <ImageGlyph className={className} />\n case 'video': return <VideoGlyph className={className} />\n case 'group': return <GroupGlyph className={className} />\n }\n}\n\nexport interface LayersPanelProps {\n page: ScenePage\n selectedElementIds: string[]\n canWrite: boolean\n /** Emit a set_attrs command for the given element. */\n onSetAttrs(elementId: string, attrs: Partial<Pick<SceneElement, 'name' | 'visible' | 'locked'>>): void\n /** Emit a reorder_element command. */\n onReorder(elementId: string, toIndex: number): void\n onSelect(elementId: string, additive: boolean): void\n}\n\nconst INDENT_PX = 16\n\nexport function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder, onSelect }: LayersPanelProps) {\n const rows = useMemo(() => flattenLayerTree(page), [page])\n const [renamingId, setRenamingId] = useState<string | null>(null)\n const [renameValue, setRenameValue] = useState('')\n\n // Drag-reorder state: we track the dragged row's ownerIndex and the target\n // index within the SAME owner. Cross-owner reorder is not supported here.\n const dragRowRef = useRef<{ elementId: string; ownerIndex: number; ownerLength: number } | null>(null)\n const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)\n\n function startRename(element: SceneElement) {\n setRenamingId(element.id)\n setRenameValue(element.name)\n }\n\n function commitRename(elementId: string) {\n const name = renameValue.trim()\n if (name.length > 0) onSetAttrs(elementId, { name })\n setRenamingId(null)\n }\n\n const visible = rows.length > LAYERS_PANEL_ROW_LIMIT ? rows.slice(0, LAYERS_PANEL_ROW_LIMIT) : rows\n\n return (\n <div className=\"flex h-full flex-col overflow-hidden text-[var(--text-primary)]\">\n <div className=\"shrink-0 border-b border-[var(--border-default)] px-3 py-1.5 text-[11px] font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n Layers\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {visible.length === 0 ? (\n <div className=\"px-3 py-2 text-[11px] text-[var(--text-muted)]\">No layers yet</div>\n ) : null}\n {visible.map((row) => {\n const { element } = row\n const isSelected = selectedElementIds.includes(element.id)\n const isRenaming = renamingId === element.id\n\n return (\n <div\n key={element.id}\n data-layer-row={element.id}\n draggable={canWrite}\n onDragStart={() => {\n dragRowRef.current = {\n elementId: element.id,\n ownerIndex: row.ownerIndex,\n ownerLength: row.ownerLength,\n }\n }}\n onDragOver={(event) => {\n if (!dragRowRef.current) return\n // Only allow reorder within the same owner (same parentGroupId + depth)\n event.preventDefault()\n setDragOverIndex(row.ownerIndex)\n }}\n onDragLeave={() => setDragOverIndex(null)}\n onDrop={() => {\n const drag = dragRowRef.current\n if (!drag) return\n if (drag.elementId !== element.id) {\n onReorder(drag.elementId, row.ownerIndex)\n }\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n onDragEnd={() => {\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n className={[\n 'group flex items-center gap-1.5 py-1 pr-2 text-[13px] transition-colors',\n isSelected ? 'bg-[color-mix(in_srgb,var(--brand-primary)_15%,transparent)] text-[var(--text-primary)]' : 'hover:bg-[color-mix(in_srgb,var(--border-default)_40%,transparent)] text-[var(--text-secondary)]',\n dragOverIndex === row.ownerIndex ? 'border-t border-[var(--brand-primary)]' : '',\n ].join(' ')}\n style={{ paddingLeft: 8 + row.depth * INDENT_PX }}\n >\n {isRenaming ? (\n <input\n autoFocus\n value={renameValue}\n onChange={(event) => setRenameValue(event.target.value)}\n onBlur={() => commitRename(element.id)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') commitRename(element.id)\n if (event.key === 'Escape') setRenamingId(null)\n event.stopPropagation()\n }}\n className=\"min-w-0 flex-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0 text-[13px] text-[var(--text-primary)] focus:border-[var(--brand-primary)]\"\n />\n ) : (\n // Selection is a real button (sibling of the eye/lock buttons) so\n // the row is not an interactive control nesting interactive\n // controls (axe: nested-interactive).\n <button\n type=\"button\"\n onClick={(event) => onSelect(element.id, event.metaKey || event.ctrlKey)}\n onDoubleClick={() => {\n if (canWrite) startRename(element)\n }}\n title={element.name}\n className=\"flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 bg-transparent text-left\"\n >\n <KindIcon kind={element.kind} className=\"h-3.5 w-3.5 shrink-0 opacity-60\" />\n {element.slot ? <SlotGlyph className=\"h-3 w-3 shrink-0 text-[var(--brand-primary)]\" /> : null}\n <span className=\"min-w-0 flex-1 truncate\">{element.name}</span>\n </button>\n )}\n\n {/* Visibility toggle */}\n <button\n type=\"button\"\n aria-label={element.visible ? 'Hide element' : 'Show element'}\n aria-pressed={!element.visible}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { visible: !element.visible })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.visible ? 'opacity-0' : 'opacity-100',\n ].join(' ')}\n >\n {element.visible\n ? <EyeGlyph className=\"h-3.5 w-3.5\" />\n : <EyeOffGlyph className=\"h-3.5 w-3.5 text-[var(--text-muted)]\" />}\n </button>\n\n {/* Lock toggle */}\n <button\n type=\"button\"\n aria-label={element.locked ? 'Unlock element' : 'Lock element'}\n aria-pressed={!!element.locked}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { locked: !element.locked })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.locked ? 'opacity-100' : 'opacity-0',\n ].join(' ')}\n >\n {element.locked\n ? <LockGlyph className=\"h-3.5 w-3.5 text-[var(--text-warning)]\" />\n : <UnlockGlyph className=\"h-3.5 w-3.5\" />}\n </button>\n </div>\n )\n })}\n\n {rows.length > LAYERS_PANEL_ROW_LIMIT ? (\n <div className=\"px-3 py-2 text-[11px] text-[var(--text-muted)]\">\n +{rows.length - LAYERS_PANEL_ROW_LIMIT} more elements — select a group to scope the list\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n","/**\n * Pure layer-tree helpers: flatten a page's element array into an ordered list\n * for the LayersPanel, preserving group nesting. Extracted so render order,\n * depth, and selection math can be unit-tested independently of React.\n */\n\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\n\n/** Describe a layer row with its element, depth, grouping, ownership, and parent group details */\nexport interface LayerRow {\n element: SceneElement\n depth: number\n /** True when this row is a group whose children follow it in the list. */\n isGroup: boolean\n /** Index within its owner (page.elements or group.children). */\n ownerIndex: number\n /** Length of the owner array — needed for z-order bound math. */\n ownerLength: number\n /** Id of the containing group, or null for page-root elements. */\n parentGroupId: string | null\n}\n\n/**\n * Flatten `page.elements` into display order for the layers panel.\n *\n * Z-order is bottom→top in the element array; the layers panel shows\n * top→bottom (highest z-index first). A group row precedes its children\n * (the group is \"above\" its children in the panel, reflecting that the group\n * node itself paints last in Konva when empty; children paint inside it).\n *\n * Depth increases by 1 for each group level, starting at 0 for page-root\n * elements. Groups are expanded; collapsed state is a view concern the caller\n * tracks separately.\n */\nexport function flattenLayerTree(page: ScenePage): LayerRow[] {\n const rows: LayerRow[] = []\n\n function visit(elements: SceneElement[], depth: number, parentGroupId: string | null) {\n // Reverse so highest z-index appears at the top of the panel.\n for (let i = elements.length - 1; i >= 0; i -= 1) {\n const element = elements[i]!\n rows.push({\n element,\n depth,\n isGroup: element.kind === 'group',\n ownerIndex: i,\n ownerLength: elements.length,\n parentGroupId,\n })\n if (element.kind === 'group') {\n visit(element.children, depth + 1, element.id)\n }\n }\n }\n\n visit(page.elements, 0, null)\n return rows\n}\n\n/** The maximum number of rows the layers panel renders before truncating. */\nexport const LAYERS_PANEL_ROW_LIMIT = 500\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAS,WAAW,QAAQ,gBAAmC;AAsEzD,cAqLI,YArLJ;AA9BN,IAAM,iBAAiB;AAMvB,IAAM,mBAAmB;AAEzB,SAAS,eAAe,KAAyD;AAC/E,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI,OAAO,UAAU,YAAa,QAAO,QAAQ,EAAE,OAAO,GAAG,QAAQ,EAAE,CAAC;AACxE,QAAI,UAAU;AACd,UAAM,SAAS,CAAC,SAA4C;AAC1D,UAAI,QAAS;AACb,gBAAU;AACV,cAAQ,IAAI;AAAA,IACd;AACA,UAAM,MAAM,IAAI,MAAM;AACtB,QAAI,SAAS,MAAM,OAAO,EAAE,OAAO,IAAI,cAAc,QAAQ,IAAI,cAAc,CAAC;AAChF,QAAI,UAAU,MAAM,OAAO,EAAE,OAAO,GAAG,QAAQ,EAAE,CAAC;AAGlD,eAAW,MAAM,OAAO,EAAE,OAAO,GAAG,QAAQ,EAAE,CAAC,GAAG,gBAAgB;AAClE,QAAI,MAAM;AAAA,EACZ,CAAC;AACH;AAEA,SAAS,YAAY,EAAE,UAAU,GAA2B;AAC1D,SACE,oBAAC,SAAI,WAAsB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAW,MACvJ,8BAAC,UAAK,GAAE,iEAAgE,GAC1E;AAEJ;AAEA,SAAS,aAAa,EAAE,UAAU,GAA2B;AAC3D,SACE,oBAAC,SAAI,WAAsB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAW,MACvJ,8BAAC,UAAK,GAAE,iGAAgG,GAC1G;AAEJ;AAEA,SAAS,aAAa,EAAE,UAAU,GAA2B;AAC3D,SACE,oBAAC,SAAI,WAAW,GAAG,aAAa,EAAE,iBAAiB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAW,MAClI,8BAAC,UAAK,GAAE,4BAA2B,eAAc,SAAQ,GAC3D;AAEJ;AAIA,IAAM,qBAAyC,EAAE,QAAQ,WAAW,OAAO,KAAM,QAAQ,IAAK;AAS9F,IAAM,oBAAoB;AAE1B,SAAS,cAAc,KAAoC;AACzD,MAAI;AACF,UAAM,MAAM,IAAI,MAAM,kBAAkB;AACxC,UAAM,QAAQ,IAAI,KAAK,CAAC,OAAO,GAAG,SAAS,aAAa;AACxD,QAAI,CAAC,SAAS,MAAM,SAAS,cAAe,QAAO;AACnD,UAAM,KAAK,MAAM;AACjB,QAAI,GAAG,SAAS,OAAQ,QAAO;AAC/B,QAAI,GAAG,SAAS,UAAW,QAAO;AAClC,QAAI,GAAG,SAAS,QAAQ;AACtB,YAAM,YAAY,GAAG,UAAU,SAAS,MAAM,KAAK,GAAG,YAAY;AAClE,aAAO,YAAY,YAAY;AAAA,IACjC;AACA,WAAO;AAAA,EACT,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAIA,SAAS,gBAAgB,EAAE,MAAM,GAA6B;AAC5D,MAAI,UAAU,QAAQ;AACpB,WAAO,oBAAC,UAAK,WAAU,sDAAqD,eAAW,MAAC;AAAA,EAC1F;AACA,MAAI,UAAU,WAAW;AACvB,WAAO,oBAAC,UAAK,WAAU,wDAAuD,eAAW,MAAC;AAAA,EAC5F;AAGA,MAAI,UAAU,aAAa,UAAU,QAAQ;AAC3C,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,iDACT,UAAU,YAAY,uBAAuB,SAC/C;AAAA,QACA,eAAW;AAAA,QAEV,oBAAU,YAAY,MAAM;AAAA;AAAA,IAC/B;AAAA,EAEJ;AACA,SAAO,oBAAC,eAAY,WAAU,oCAAmC;AACnE;AAEO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,SAAS;AAAA,EACT;AACF,GAA2B;AACzB,QAAM,CAAC,KAAK,MAAM,IAAI,SAAc,MAAO,UAAU,SAAS,IAAI,cAAc,SAAU;AAC1F,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,aAAa,cAAc,IAAI,SAA6B,CAAC,CAAC;AACrE,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAS,KAAK;AAChE,QAAM,eAAe,OAAgC,IAAI;AAEzD,YAAU,MAAM;AACd,QAAI,QAAQ,aAAa,CAAC,eAAe;AACvC,aAAO,UAAU,SAAS,IAAI,cAAc,kBAAkB,gBAAgB,SAAS;AAAA,IACzF;AACA,QAAI,QAAQ,eAAe,UAAU,WAAW,GAAG;AACjD,aAAO,gBAAgB,YAAY,kBAAkB,gBAAgB,SAAS;AAAA,IAChF;AACA,QAAI,QAAQ,iBAAiB,CAAC,iBAAiB;AAC7C,aAAO,UAAU,SAAS,IAAI,cAAc,gBAAgB,YAAY,WAAW;AAAA,IACrF;AAAA,EACF,GAAG,CAAC,KAAK,UAAU,QAAQ,eAAe,eAAe,CAAC;AAE1D,YAAU,MAAM;AACd,QAAI,QAAQ,iBAAiB,qBAAqB,CAAC,gBAAiB;AACpE,QAAI,YAAY;AAChB,UAAM,YAAY;AAChB,UAAI;AACF,cAAM,OAAO,MAAM,gBAAgB;AACnC,YAAI,CAAC,UAAW,gBAAe,KAAK,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;AAAA,MAC1D,SAAS,KAAK;AACZ,YAAI,CAAC,UAAW,UAAS,eAAe,QAAQ,IAAI,UAAU,4BAA4B;AAAA,MAC5F,UAAE;AACA,YAAI,CAAC,UAAW,sBAAqB,IAAI;AAAA,MAC3C;AAAA,IACF,GAAG;AACH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,KAAK,mBAAmB,eAAe,CAAC;AAE5C,iBAAe,mBAAmB,KAAa;AAG7C,wBAAoB,KAAK,WAAW;AACpC,UAAM,UAAU,MAAM,eAAe,GAAG;AACxC,UAAM,SAAS,CAAC,mBAAmB,KAAK,SAAS,IAAI,CAAC,CAAC;AAAA,EACzD;AAEA,iBAAe,YAAY,OAA0B;AACnD,QAAI,CAAC,YAAY,KAAM;AACvB,QAAI,CAAC,cAAe;AACpB,UAAM,OAAO,MAAM,KAAK,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,WAAW,QAAQ,CAAC;AACxE,QAAI,KAAK,WAAW,GAAG;AACrB,eAAS,6CAA6C;AACtD;AAAA,IACF;AACA,YAAQ,IAAI;AACZ,aAAS,EAAE;AACX,QAAI;AACF,iBAAW,QAAQ,MAAM;AACvB,cAAM,MAAM,MAAM,cAAc,IAAI;AACpC,cAAM,mBAAmB,GAAG;AAAA,MAC9B;AAAA,IACF,SAAS,KAAK;AACZ,eAAS,eAAe,QAAQ,IAAI,UAAU,eAAe;AAAA,IAC/D,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAEA,iBAAe,UAAU,OAA2D;AAClF,QAAI,CAAC,YAAY,KAAM;AACvB,YAAQ,IAAI;AACZ,aAAS,EAAE;AACX,QAAI;AACF,YAAM,SAAS,MAAM,MAAM,CAAC;AAAA,IAC9B,SAAS,KAAK;AACZ,eAAS,eAAe,QAAQ,IAAI,UAAU,eAAe;AAAA,IAC/D,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAEA,QAAM,OAA4G;AAAA,IAChH,EAAE,IAAI,WAAW,OAAO,WAAW,MAAM,YAAY,MAAM,CAAC,CAAC,cAAc;AAAA,IAC3E,EAAE,IAAI,aAAa,OAAO,aAAa,MAAM,aAAa,MAAM,UAAU,SAAS,EAAE;AAAA,IACrF,EAAE,IAAI,eAAe,OAAO,eAAe,MAAM,cAAc,MAAM,CAAC,CAAC,gBAAgB;AAAA,EACzF;AACA,QAAM,cAAc,KAAK,OAAO,CAAC,MAAM,EAAE,IAAI;AAE7C,SACE,qBAAC,SAAI,WAAW,gFAAgF,aAAa,EAAE,IAC7G;AAAA,wBAAC,SAAI,WAAU,yDACZ,sBAAY,IAAI,CAAC,EAAE,IAAI,OAAO,MAAM,KAAK,MACxC;AAAA,MAAC;AAAA;AAAA,QAEC,MAAK;AAAA,QACL,SAAS,MAAM,OAAO,EAAE;AAAA,QAMxB,WAAW,yIACT,QAAQ,KACJ,wEACA,2DACN;AAAA,QAEA;AAAA,8BAAC,QAAK,WAAU,eAAc;AAAA,UAC7B;AAAA;AAAA;AAAA,MAfI;AAAA,IAgBP,CACD,GACH;AAAA,IAEC,CAAC,WACA,oBAAC,SAAI,WAAU,4FAA2F,uDAE1G,IAEA,qBAAC,SAAI,WAAU,sCACZ;AAAA,cAAQ,aACP,qBAAC,SAAI,WAAU,uBACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,UAAU;AAAA,YACV,SAAS,MAAM,aAAa,SAAS,MAAM;AAAA,YAC3C,YAAY,CAAC,MAAM;AACjB,gBAAE,eAAe;AACjB,0BAAY,IAAI;AAAA,YAClB;AAAA,YACA,aAAa,MAAM,YAAY,KAAK;AAAA,YACpC,QAAQ,CAAC,MAAM;AACb,gBAAE,eAAe;AACjB,0BAAY,KAAK;AACjB,kBAAI,EAAE,cAAc,OAAO,OAAQ,MAAK,YAAY,EAAE,aAAa,KAAK;AAAA,YAC1E;AAAA,YACA,WAAW,iJACT,WACI,8FACA,uGACN;AAAA,YAEC;AAAA,qBACC,oBAAC,gBAAa,WAAU,uCAAsC,IAE9D,oBAAC,eAAY,WAAU,oCAAmC;AAAA,cAE5D,oBAAC,UAAK,WAAU,kDACb,iBAAO,oBAAe,oCACzB;AAAA,cACA,oBAAC,UAAK,WAAU,oCAAmC,qCAAuB;AAAA;AAAA;AAAA,QAC5E;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL;AAAA,YACA,UAAQ;AAAA,YACR,WAAU;AAAA,YACV,UAAU,CAAC,MAAM;AACf,kBAAI,EAAE,OAAO,OAAO,OAAQ,MAAK,YAAY,EAAE,OAAO,KAAK;AAC3D,gBAAE,OAAO,QAAQ;AAAA,YACnB;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MAGD,QAAQ,eACP,oBAAC,SAAI,WAAU,0BACZ,oBAAU,IAAI,CAAC,QACd;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,UAAU;AAAA,UACV,SAAS,MAAM,KAAK,UAAU,MAAM,IAAI,MAAM,IAAI,CAAC;AAAA,UACnD,WAAU;AAAA,UAEV;AAAA,gCAAC,UAAK,WAAU,wCACd,8BAAC,mBAAgB,OAAO,cAAc,GAAG,GAAG,GAC9C;AAAA,YACA,oBAAC,UAAM,cAAI,OAAM;AAAA;AAAA;AAAA,QATZ,IAAI;AAAA,MAUX,CACD,GACH;AAAA,MAGD,QAAQ,iBACP,oBAAC,SAAI,WAAU,uBACZ,WAAC,oBACA,oBAAC,SAAI,WAAU,kEACb,8BAAC,gBAAa,WAAU,WAAU,GACpC,IACE,YAAY,WAAW,IACzB,oBAAC,OAAE,WAAU,0DAAyD,qEAEtE,IAEA,oBAAC,SAAI,WAAU,0BACZ,sBAAY,IAAI,CAAC,QAChB;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,UAAU;AAAA,UACV,SAAS,MAAM;AACb,gBAAI,QAAQ,CAAC,SAAU;AACvB,oBAAQ,IAAI;AACZ,qBAAS,EAAE;AACX,+BAAmB,IAAI,GAAG,EACvB,MAAM,CAAC,QAAQ,SAAS,eAAe,QAAQ,IAAI,UAAU,eAAe,CAAC,EAC7E,QAAQ,MAAM,QAAQ,KAAK,CAAC;AAAA,UACjC;AAAA,UACA,OAAO,IAAI;AAAA,UACX,WAAU;AAAA,UAGV,8BAAC,SAAI,KAAK,IAAI,KAAK,KAAK,IAAI,SAAS,mBAAmB,WAAU,8BAA6B;AAAA;AAAA,QAf1F,IAAI;AAAA,MAgBX,CACD,GACH,GAEJ;AAAA,MAGD,QAAQ,oBAAC,OAAE,WAAU,4DAA4D,iBAAM,IAAO;AAAA,OACjG;AAAA,KAEJ;AAEJ;;;AChZA,SAAsB,SAAS,UAAAA,SAAQ,YAAAC,iBAAgB;;;ACyBhD,SAAS,iBAAiB,MAA6B;AAC5D,QAAM,OAAmB,CAAC;AAE1B,WAAS,MAAM,UAA0B,OAAe,eAA8B;AAEpF,aAAS,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;AAChD,YAAM,UAAU,SAAS,CAAC;AAC1B,WAAK,KAAK;AAAA,QACR;AAAA,QACA;AAAA,QACA,SAAS,QAAQ,SAAS;AAAA,QAC1B,YAAY;AAAA,QACZ,aAAa,SAAS;AAAA,QACtB;AAAA,MACF,CAAC;AACD,UAAI,QAAQ,SAAS,SAAS;AAC5B,cAAM,QAAQ,UAAU,QAAQ,GAAG,QAAQ,EAAE;AAAA,MAC/C;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,UAAU,GAAG,IAAI;AAC5B,SAAO;AACT;AAGO,IAAM,yBAAyB;;;AD9Bd,gBAAAC,MAqHR,QAAAC,aArHQ;AAFxB,SAAS,SAAS,EAAE,MAAM,UAAU,GAAmD;AACrF,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAQ,aAAO,gBAAAD,KAAC,aAAU,WAAsB;AAAA,IACrD,KAAK;AAAW,aAAO,gBAAAA,KAAC,gBAAa,WAAsB;AAAA,IAC3D,KAAK;AAAQ,aAAO,gBAAAA,KAAC,aAAU,WAAsB;AAAA,IACrD,KAAK;AAAQ,aAAO,gBAAAA,KAAC,aAAU,WAAsB;AAAA,IACrD,KAAK;AAAS,aAAO,gBAAAA,KAAC,cAAW,WAAsB;AAAA,IACvD,KAAK;AAAS,aAAO,gBAAAA,KAAC,cAAW,WAAsB;AAAA,IACvD,KAAK;AAAS,aAAO,gBAAAA,KAAC,cAAW,WAAsB;AAAA,EACzD;AACF;AAaA,IAAM,YAAY;AAEX,SAAS,YAAY,EAAE,MAAM,oBAAoB,UAAU,YAAY,WAAW,SAAS,GAAqB;AACrH,QAAM,OAAO,QAAQ,MAAM,iBAAiB,IAAI,GAAG,CAAC,IAAI,CAAC;AACzD,QAAM,CAAC,YAAY,aAAa,IAAIE,UAAwB,IAAI;AAChE,QAAM,CAAC,aAAa,cAAc,IAAIA,UAAS,EAAE;AAIjD,QAAM,aAAaC,QAA8E,IAAI;AACrG,QAAM,CAAC,eAAe,gBAAgB,IAAID,UAAwB,IAAI;AAEtE,WAAS,YAAY,SAAuB;AAC1C,kBAAc,QAAQ,EAAE;AACxB,mBAAe,QAAQ,IAAI;AAAA,EAC7B;AAEA,WAAS,aAAa,WAAmB;AACvC,UAAM,OAAO,YAAY,KAAK;AAC9B,QAAI,KAAK,SAAS,EAAG,YAAW,WAAW,EAAE,KAAK,CAAC;AACnD,kBAAc,IAAI;AAAA,EACpB;AAEA,QAAM,UAAU,KAAK,SAAS,yBAAyB,KAAK,MAAM,GAAG,sBAAsB,IAAI;AAE/F,SACE,gBAAAD,MAAC,SAAI,WAAU,mEACb;AAAA,oBAAAD,KAAC,SAAI,WAAU,yIAAwI,oBAEvJ;AAAA,IAEA,gBAAAC,MAAC,SAAI,WAAU,kCACZ;AAAA,cAAQ,WAAW,IAClB,gBAAAD,KAAC,SAAI,WAAU,kDAAiD,2BAAa,IAC3E;AAAA,MACH,QAAQ,IAAI,CAAC,QAAQ;AACpB,cAAM,EAAE,QAAQ,IAAI;AACpB,cAAM,aAAa,mBAAmB,SAAS,QAAQ,EAAE;AACzD,cAAM,aAAa,eAAe,QAAQ;AAE1C,eACE,gBAAAC;AAAA,UAAC;AAAA;AAAA,YAEC,kBAAgB,QAAQ;AAAA,YACxB,WAAW;AAAA,YACX,aAAa,MAAM;AACjB,yBAAW,UAAU;AAAA,gBACnB,WAAW,QAAQ;AAAA,gBACnB,YAAY,IAAI;AAAA,gBAChB,aAAa,IAAI;AAAA,cACnB;AAAA,YACF;AAAA,YACA,YAAY,CAAC,UAAU;AACrB,kBAAI,CAAC,WAAW,QAAS;AAEzB,oBAAM,eAAe;AACrB,+BAAiB,IAAI,UAAU;AAAA,YACjC;AAAA,YACA,aAAa,MAAM,iBAAiB,IAAI;AAAA,YACxC,QAAQ,MAAM;AACZ,oBAAM,OAAO,WAAW;AACxB,kBAAI,CAAC,KAAM;AACX,kBAAI,KAAK,cAAc,QAAQ,IAAI;AACjC,0BAAU,KAAK,WAAW,IAAI,UAAU;AAAA,cAC1C;AACA,yBAAW,UAAU;AACrB,+BAAiB,IAAI;AAAA,YACvB;AAAA,YACA,WAAW,MAAM;AACf,yBAAW,UAAU;AACrB,+BAAiB,IAAI;AAAA,YACvB;AAAA,YACA,WAAW;AAAA,cACT;AAAA,cACA,aAAa,4FAA4F;AAAA,cACzG,kBAAkB,IAAI,aAAa,2CAA2C;AAAA,YAChF,EAAE,KAAK,GAAG;AAAA,YACV,OAAO,EAAE,aAAa,IAAI,IAAI,QAAQ,UAAU;AAAA,YAE/C;AAAA,2BACC,gBAAAD;AAAA,gBAAC;AAAA;AAAA,kBACC,WAAS;AAAA,kBACT,OAAO;AAAA,kBACP,UAAU,CAAC,UAAU,eAAe,MAAM,OAAO,KAAK;AAAA,kBACtD,QAAQ,MAAM,aAAa,QAAQ,EAAE;AAAA,kBACrC,WAAW,CAAC,UAAU;AACpB,wBAAI,MAAM,QAAQ,QAAS,cAAa,QAAQ,EAAE;AAClD,wBAAI,MAAM,QAAQ,SAAU,eAAc,IAAI;AAC9C,0BAAM,gBAAgB;AAAA,kBACxB;AAAA,kBACA,WAAU;AAAA;AAAA,cACZ;AAAA;AAAA;AAAA;AAAA,gBAKA,gBAAAC;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAS,CAAC,UAAU,SAAS,QAAQ,IAAI,MAAM,WAAW,MAAM,OAAO;AAAA,oBACvE,eAAe,MAAM;AACnB,0BAAI,SAAU,aAAY,OAAO;AAAA,oBACnC;AAAA,oBACA,OAAO,QAAQ;AAAA,oBACf,WAAU;AAAA,oBAEV;AAAA,sCAAAD,KAAC,YAAS,MAAM,QAAQ,MAAM,WAAU,mCAAkC;AAAA,sBACzE,QAAQ,OAAO,gBAAAA,KAAC,aAAU,WAAU,gDAA+C,IAAK;AAAA,sBACzF,gBAAAA,KAAC,UAAK,WAAU,2BAA2B,kBAAQ,MAAK;AAAA;AAAA;AAAA,gBAC1D;AAAA;AAAA,cAIF,gBAAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,cAAY,QAAQ,UAAU,iBAAiB;AAAA,kBAC/C,gBAAc,CAAC,QAAQ;AAAA,kBACvB,SAAS,CAAC,UAAU;AAClB,0BAAM,gBAAgB;AACtB,wBAAI,SAAU,YAAW,QAAQ,IAAI,EAAE,SAAS,CAAC,QAAQ,QAAQ,CAAC;AAAA,kBACpE;AAAA,kBACA,UAAU,CAAC;AAAA,kBACX,WAAW;AAAA,oBACT;AAAA,oBACA,QAAQ,UAAU,cAAc;AAAA,kBAClC,EAAE,KAAK,GAAG;AAAA,kBAET,kBAAQ,UACL,gBAAAA,KAAC,YAAS,WAAU,eAAc,IAClC,gBAAAA,KAAC,eAAY,WAAU,wCAAuC;AAAA;AAAA,cACpE;AAAA,cAGA,gBAAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,cAAY,QAAQ,SAAS,mBAAmB;AAAA,kBAChD,gBAAc,CAAC,CAAC,QAAQ;AAAA,kBACxB,SAAS,CAAC,UAAU;AAClB,0BAAM,gBAAgB;AACtB,wBAAI,SAAU,YAAW,QAAQ,IAAI,EAAE,QAAQ,CAAC,QAAQ,OAAO,CAAC;AAAA,kBAClE;AAAA,kBACA,UAAU,CAAC;AAAA,kBACX,WAAW;AAAA,oBACT;AAAA,oBACA,QAAQ,SAAS,gBAAgB;AAAA,kBACnC,EAAE,KAAK,GAAG;AAAA,kBAET,kBAAQ,SACL,gBAAAA,KAAC,aAAU,WAAU,0CAAyC,IAC9D,gBAAAA,KAAC,eAAY,WAAU,eAAc;AAAA;AAAA,cAC3C;AAAA;AAAA;AAAA,UA3GK,QAAQ;AAAA,QA4Gf;AAAA,MAEJ,CAAC;AAAA,MAEA,KAAK,SAAS,yBACb,gBAAAC,MAAC,SAAI,WAAU,kDAAiD;AAAA;AAAA,QAC5D,KAAK,SAAS;AAAA,QAAuB;AAAA,SACzC,IACE;AAAA,OACN;AAAA,KACF;AAEJ;","names":["useRef","useState","jsx","jsxs","useState","useRef"]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DesignCanvasChromeLazy,
3
3
  DesignCanvasLazy
4
- } from "../chunk-AWPK7ZDS.js";
4
+ } from "../chunk-QGOU3VXH.js";
5
5
  export {
6
6
  DesignCanvasChromeLazy,
7
7
  DesignCanvasLazy
@@ -4,7 +4,11 @@ import {
4
4
  checkLegibility,
5
5
  formatLegibilityReport,
6
6
  legibilityReportToJson
7
- } from "../chunk-XEA5EG6P.js";
7
+ } from "../chunk-EPZYYWCT.js";
8
+ import "../chunk-EMLGWEHV.js";
9
+ import {
10
+ invokedAsScript
11
+ } from "../chunk-C3CAYGGQ.js";
8
12
 
9
13
  // src/legibility/cli.ts
10
14
  import { existsSync } from "fs";
@@ -130,14 +134,16 @@ async function main() {
130
134
  `);
131
135
  return report.ok ? 0 : 1;
132
136
  }
133
- try {
134
- process.exit(await main());
135
- } catch (error) {
136
- const message = error instanceof Error ? error.message : String(error);
137
- process.stderr.write(`agent-app-legibility-check: ${message}
137
+ if (invokedAsScript(import.meta.url, process.argv[1])) {
138
+ try {
139
+ process.exit(await main());
140
+ } catch (error) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ process.stderr.write(`agent-app-legibility-check: ${message}
138
143
  `);
139
- if (error instanceof LegibilityUsageError) process.stderr.write(`${USAGE}
144
+ if (error instanceof LegibilityUsageError) process.stderr.write(`${USAGE}
140
145
  `);
141
- process.exit(2);
146
+ process.exit(2);
147
+ }
142
148
  }
143
149
  //# sourceMappingURL=cli.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/legibility/cli.ts","../../src/legibility/cli-args.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `agent-app-legibility-check` — fail a product's CI when a screen leaves its\n * reader guessing.\n *\n * Runs in the PRODUCT's repo, over the product's own source, next to typecheck:\n *\n * \"scripts\": {\n * \"legibility\": \"agent-app-legibility-check --src src --routes src/routes.ts --nav src/components/workspace-sidebar.tsx\"\n * }\n *\n * Flags (repeatable where a list makes sense):\n * --src <dir> source directory to scan (required unless the config supplies it)\n * --routes <file> react-router route config, for the unreachable-capability check\n * --nav <file> a file whose path literals are navigation entries\n * --ignore <substr> skip paths containing this substring\n * --skip <check> turn one check off wholesale (prefer a per-line suppression)\n * --config <file> a legibility.config.mjs default-exporting the config\n * (default: ./legibility.config.mjs when present)\n * --json print the report as JSON instead of text\n * --list-suppressions print every honoured suppression with its reason\n *\n * Exit 0 with no findings, 1 with findings, 2 on a usage or config error — the\n * same contract as `agent-app-preflight` and `agent-app-peer-check`.\n */\n\nimport { existsSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { pathToFileURL } from 'node:url'\n\nimport { DEFAULT_CONFIG_FILE, LegibilityUsageError, mergeConfig, parseArgs, USAGE } from './cli-args'\nimport { checkLegibility } from './index'\nimport { formatLegibilityReport, legibilityReportToJson } from './report'\nimport type { LegibilityConfig } from './types'\n\nasync function loadConfigFile(path: string | undefined): Promise<Partial<LegibilityConfig>> {\n const explicit = path !== undefined\n const resolved = resolve(process.cwd(), path ?? DEFAULT_CONFIG_FILE)\n if (!existsSync(resolved)) {\n if (explicit) throw new LegibilityUsageError(`no config at ${resolved}`)\n return {}\n }\n const module: unknown = await import(pathToFileURL(resolved).href)\n const exported = (module as { default?: unknown; config?: unknown }).default ?? (module as { config?: unknown }).config\n const value = typeof exported === 'function' ? await (exported as () => Promise<unknown>)() : exported\n if (value === null || typeof value !== 'object') {\n throw new LegibilityUsageError(`${resolved} must default-export a legibility config object`)\n }\n return value as Partial<LegibilityConfig>\n}\n\nasync function main(): Promise<number> {\n const args = parseArgs(process.argv.slice(2))\n if (args.help) {\n process.stdout.write(`${USAGE}\\n`)\n return 0\n }\n\n const config = mergeConfig(await loadConfigFile(args.config), args)\n if (config.srcDirs.length === 0) {\n throw new LegibilityUsageError('at least one --src <dir> is required (or srcDirs in the config file)')\n }\n\n const report = checkLegibility(config)\n const text = args.json\n ? legibilityReportToJson(report)\n : formatLegibilityReport(report, { listSuppressions: args.listSuppressions })\n\n if (report.ok) process.stdout.write(`${text}\\n`)\n else process.stderr.write(`${text}\\n`)\n return report.ok ? 0 : 1\n}\n\ntry {\n process.exit(await main())\n} catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n process.stderr.write(`agent-app-legibility-check: ${message}\\n`)\n if (error instanceof LegibilityUsageError) process.stderr.write(`${USAGE}\\n`)\n process.exit(2)\n}\n","/**\n * The bin's argument and config layer, split from `cli.ts` so it can be tested\n * without executing a `main()` that calls `process.exit`.\n *\n * Errors are thrown as {@link LegibilityUsageError} rather than exiting here:\n * exiting is the bin's job, and a parser that exits cannot be exercised.\n */\n\nimport { LEGIBILITY_CHECKS, type LegibilityCheckId, type LegibilityConfig } from './types'\n\nexport const DEFAULT_CONFIG_FILE = 'legibility.config.mjs'\n\n/** A usage or config problem — exit code 2, never a finding. */\nexport class LegibilityUsageError extends Error {}\n\nexport interface ParsedArgs {\n srcDirs: string[]\n navFiles: string[]\n ignorePaths: string[]\n skip: LegibilityCheckId[]\n routes?: string\n config?: string\n json: boolean\n listSuppressions: boolean\n help: boolean\n}\n\nexport const USAGE = [\n 'Usage: agent-app-legibility-check --src <dir> [--src <dir>…] [--routes <file>] [--nav <file>…]',\n ' [--ignore <substr>…] [--skip <check>…] [--config <file>]',\n ' [--json] [--list-suppressions]',\n `Checks: ${LEGIBILITY_CHECKS.join(', ')}`,\n].join('\\n')\n\nexport function parseArgs(argv: readonly string[]): ParsedArgs {\n const out: ParsedArgs = {\n srcDirs: [],\n navFiles: [],\n ignorePaths: [],\n skip: [],\n json: false,\n listSuppressions: false,\n help: false,\n }\n for (let i = 0; i < argv.length; i++) {\n const flag = argv[i]\n const take = (): string => {\n const value = argv[++i]\n if (value === undefined) throw new LegibilityUsageError(`${flag} needs a value`)\n return value\n }\n switch (flag) {\n case '--src':\n out.srcDirs.push(take())\n break\n case '--routes':\n out.routes = take()\n break\n case '--nav':\n out.navFiles.push(take())\n break\n case '--ignore':\n out.ignorePaths.push(take())\n break\n case '--skip': {\n const check = take()\n if (!LEGIBILITY_CHECKS.includes(check as LegibilityCheckId)) {\n throw new LegibilityUsageError(`--skip ${check} is not a check. Known: ${LEGIBILITY_CHECKS.join(', ')}`)\n }\n out.skip.push(check as LegibilityCheckId)\n break\n }\n case '--config':\n out.config = take()\n break\n case '--json':\n out.json = true\n break\n case '--list-suppressions':\n out.listSuppressions = true\n break\n case '-h':\n case '--help':\n out.help = true\n break\n default:\n throw new LegibilityUsageError(`unknown argument: ${flag}`)\n }\n }\n return out\n}\n\n/**\n * Merge a config file with the flags. Flags WIN, so a one-off CI invocation can\n * narrow a run without editing the committed config.\n */\nexport function mergeConfig(fromFile: Partial<LegibilityConfig>, args: ParsedArgs): LegibilityConfig {\n const checks: Partial<Record<LegibilityCheckId, boolean>> = { ...fromFile.checks }\n for (const check of args.skip) checks[check] = false\n\n const reachability = {\n ...fromFile.reachability,\n ...(args.routes ? { routeConfigFile: args.routes } : {}),\n ...(args.navFiles.length > 0 ? { navFiles: args.navFiles } : {}),\n }\n\n return {\n ...fromFile,\n srcDirs: args.srcDirs.length > 0 ? args.srcDirs : (fromFile.srcDirs ?? []),\n ignorePaths: [...(fromFile.ignorePaths ?? []), ...args.ignorePaths],\n ...(Object.keys(checks).length > 0 ? { checks } : {}),\n ...(reachability.routeConfigFile || reachability.routePaths ? { reachability } : {}),\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/legibility/cli.ts","../../src/legibility/cli-args.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `agent-app-legibility-check` — fail a product's CI when a screen leaves its\n * reader guessing.\n *\n * Runs in the PRODUCT's repo, over the product's own source, next to typecheck:\n *\n * \"scripts\": {\n * \"legibility\": \"agent-app-legibility-check --src src --routes src/routes.ts --nav src/components/workspace-sidebar.tsx\"\n * }\n *\n * Flags (repeatable where a list makes sense):\n * --src <dir> source directory to scan (required unless the config supplies it)\n * --routes <file> react-router route config, for the unreachable-capability check\n * --nav <file> a file whose path literals are navigation entries\n * --ignore <substr> skip paths containing this substring\n * --skip <check> turn one check off wholesale (prefer a per-line suppression)\n * --config <file> a legibility.config.mjs default-exporting the config\n * (default: ./legibility.config.mjs when present)\n * --json print the report as JSON instead of text\n * --list-suppressions print every honoured suppression with its reason\n *\n * Exit 0 with no findings, 1 with findings, 2 on a usage or config error — the\n * same contract as `agent-app-preflight` and `agent-app-peer-check`.\n */\n\nimport { existsSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { pathToFileURL } from 'node:url'\n\nimport { invokedAsScript } from '../signoff/invoked-as-script'\nimport { DEFAULT_CONFIG_FILE, LegibilityUsageError, mergeConfig, parseArgs, USAGE } from './cli-args'\nimport { checkLegibility } from './index'\nimport { formatLegibilityReport, legibilityReportToJson } from './report'\nimport type { LegibilityConfig } from './types'\n\nasync function loadConfigFile(path: string | undefined): Promise<Partial<LegibilityConfig>> {\n const explicit = path !== undefined\n const resolved = resolve(process.cwd(), path ?? DEFAULT_CONFIG_FILE)\n if (!existsSync(resolved)) {\n if (explicit) throw new LegibilityUsageError(`no config at ${resolved}`)\n return {}\n }\n const module: unknown = await import(pathToFileURL(resolved).href)\n const exported = (module as { default?: unknown; config?: unknown }).default ?? (module as { config?: unknown }).config\n const value = typeof exported === 'function' ? await (exported as () => Promise<unknown>)() : exported\n if (value === null || typeof value !== 'object') {\n throw new LegibilityUsageError(`${resolved} must default-export a legibility config object`)\n }\n return value as Partial<LegibilityConfig>\n}\n\nasync function main(): Promise<number> {\n const args = parseArgs(process.argv.slice(2))\n if (args.help) {\n process.stdout.write(`${USAGE}\\n`)\n return 0\n }\n\n const config = mergeConfig(await loadConfigFile(args.config), args)\n if (config.srcDirs.length === 0) {\n throw new LegibilityUsageError('at least one --src <dir> is required (or srcDirs in the config file)')\n }\n\n const report = checkLegibility(config)\n const text = args.json\n ? legibilityReportToJson(report)\n : formatLegibilityReport(report, { listSuppressions: args.listSuppressions })\n\n if (report.ok) process.stdout.write(`${text}\\n`)\n else process.stderr.write(`${text}\\n`)\n return report.ok ? 0 : 1\n}\n\n/* c8 ignore start — process wiring, exercised by the bin itself */\nif (invokedAsScript(import.meta.url, process.argv[1])) {\n try {\n process.exit(await main())\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n process.stderr.write(`agent-app-legibility-check: ${message}\\n`)\n if (error instanceof LegibilityUsageError) process.stderr.write(`${USAGE}\\n`)\n process.exit(2)\n }\n}\n/* c8 ignore stop */\n","/**\n * The bin's argument and config layer, split from `cli.ts` so it can be tested\n * without executing a `main()` that calls `process.exit`.\n *\n * Errors are thrown as {@link LegibilityUsageError} rather than exiting here:\n * exiting is the bin's job, and a parser that exits cannot be exercised.\n */\n\nimport { LEGIBILITY_CHECKS, type LegibilityCheckId, type LegibilityConfig } from './types'\n\nexport const DEFAULT_CONFIG_FILE = 'legibility.config.mjs'\n\n/** A usage or config problem — exit code 2, never a finding. */\nexport class LegibilityUsageError extends Error {}\n\nexport interface ParsedArgs {\n srcDirs: string[]\n navFiles: string[]\n ignorePaths: string[]\n skip: LegibilityCheckId[]\n routes?: string\n config?: string\n json: boolean\n listSuppressions: boolean\n help: boolean\n}\n\nexport const USAGE = [\n 'Usage: agent-app-legibility-check --src <dir> [--src <dir>…] [--routes <file>] [--nav <file>…]',\n ' [--ignore <substr>…] [--skip <check>…] [--config <file>]',\n ' [--json] [--list-suppressions]',\n `Checks: ${LEGIBILITY_CHECKS.join(', ')}`,\n].join('\\n')\n\nexport function parseArgs(argv: readonly string[]): ParsedArgs {\n const out: ParsedArgs = {\n srcDirs: [],\n navFiles: [],\n ignorePaths: [],\n skip: [],\n json: false,\n listSuppressions: false,\n help: false,\n }\n for (let i = 0; i < argv.length; i++) {\n const flag = argv[i]\n const take = (): string => {\n const value = argv[++i]\n if (value === undefined) throw new LegibilityUsageError(`${flag} needs a value`)\n return value\n }\n switch (flag) {\n case '--src':\n out.srcDirs.push(take())\n break\n case '--routes':\n out.routes = take()\n break\n case '--nav':\n out.navFiles.push(take())\n break\n case '--ignore':\n out.ignorePaths.push(take())\n break\n case '--skip': {\n const check = take()\n if (!LEGIBILITY_CHECKS.includes(check as LegibilityCheckId)) {\n throw new LegibilityUsageError(`--skip ${check} is not a check. Known: ${LEGIBILITY_CHECKS.join(', ')}`)\n }\n out.skip.push(check as LegibilityCheckId)\n break\n }\n case '--config':\n out.config = take()\n break\n case '--json':\n out.json = true\n break\n case '--list-suppressions':\n out.listSuppressions = true\n break\n case '-h':\n case '--help':\n out.help = true\n break\n default:\n throw new LegibilityUsageError(`unknown argument: ${flag}`)\n }\n }\n return out\n}\n\n/**\n * Merge a config file with the flags. Flags WIN, so a one-off CI invocation can\n * narrow a run without editing the committed config.\n */\nexport function mergeConfig(fromFile: Partial<LegibilityConfig>, args: ParsedArgs): LegibilityConfig {\n const checks: Partial<Record<LegibilityCheckId, boolean>> = { ...fromFile.checks }\n for (const check of args.skip) checks[check] = false\n\n const reachability = {\n ...fromFile.reachability,\n ...(args.routes ? { routeConfigFile: args.routes } : {}),\n ...(args.navFiles.length > 0 ? { navFiles: args.navFiles } : {}),\n }\n\n return {\n ...fromFile,\n srcDirs: args.srcDirs.length > 0 ? args.srcDirs : (fromFile.srcDirs ?? []),\n ignorePaths: [...(fromFile.ignorePaths ?? []), ...args.ignorePaths],\n ...(Object.keys(checks).length > 0 ? { checks } : {}),\n ...(reachability.routeConfigFile || reachability.routePaths ? { reachability } : {}),\n }\n}\n"],"mappings":"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","names":[]}
@@ -13,7 +13,8 @@ import {
13
13
  scanSource,
14
14
  scanSources,
15
15
  staticSegments
16
- } from "../chunk-XEA5EG6P.js";
16
+ } from "../chunk-EPZYYWCT.js";
17
+ import "../chunk-EMLGWEHV.js";
17
18
  export {
18
19
  COPY_CALLS,
19
20
  COPY_KEYS,
@@ -1,19 +1,25 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ invokedAsScript
4
+ } from "../chunk-C3CAYGGQ.js";
2
5
  import {
3
6
  checkPeerFloors,
4
7
  formatPeerFloorReport
5
8
  } from "../chunk-S24VVLKV.js";
6
9
 
7
10
  // src/peer-floors/cli.ts
8
- var appDir = process.argv[2] ?? process.cwd();
9
- try {
10
- const report = checkPeerFloors({ appDir });
11
- process.stdout.write(`${formatPeerFloorReport(report)}
11
+ function main() {
12
+ const appDir = process.argv[2] ?? process.cwd();
13
+ try {
14
+ const report = checkPeerFloors({ appDir });
15
+ process.stdout.write(`${formatPeerFloorReport(report)}
12
16
  `);
13
- process.exit(report.ok ? 0 : 1);
14
- } catch (err) {
15
- process.stderr.write(`agent-app-peer-check failed: ${err instanceof Error ? err.message : String(err)}
17
+ process.exit(report.ok ? 0 : 1);
18
+ } catch (err) {
19
+ process.stderr.write(`agent-app-peer-check failed: ${err instanceof Error ? err.message : String(err)}
16
20
  `);
17
- process.exit(1);
21
+ process.exit(1);
22
+ }
18
23
  }
24
+ if (invokedAsScript(import.meta.url, process.argv[1])) main();
19
25
  //# sourceMappingURL=cli.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/peer-floors/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `agent-app-peer-check` — fail a consumer's CI when its installed tree sits\n * below a peer floor this package declares.\n *\n * Runs in the CONSUMER's repo, over the consumer's own `node_modules`, because\n * the floors only mean anything against a real install. Add it next to\n * typecheck:\n *\n * \"scripts\": { \"peer-check\": \"agent-app-peer-check\" }\n *\n * Exits 1 on any violation, 0 otherwise. Takes no configuration — the floors\n * come from the installed shell manifest, so the check cannot go stale when the\n * shell adds an engine.\n */\nimport { checkPeerFloors, formatPeerFloorReport } from './check'\n\nconst appDir = process.argv[2] ?? process.cwd()\n\ntry {\n const report = checkPeerFloors({ appDir })\n process.stdout.write(`${formatPeerFloorReport(report)}\\n`)\n process.exit(report.ok ? 0 : 1)\n} catch (err) {\n process.stderr.write(`agent-app-peer-check failed: ${err instanceof Error ? err.message : String(err)}\\n`)\n process.exit(1)\n}\n"],"mappings":";;;;;;;AAiBA,IAAM,SAAS,QAAQ,KAAK,CAAC,KAAK,QAAQ,IAAI;AAE9C,IAAI;AACF,QAAM,SAAS,gBAAgB,EAAE,OAAO,CAAC;AACzC,UAAQ,OAAO,MAAM,GAAG,sBAAsB,MAAM,CAAC;AAAA,CAAI;AACzD,UAAQ,KAAK,OAAO,KAAK,IAAI,CAAC;AAChC,SAAS,KAAK;AACZ,UAAQ,OAAO,MAAM,gCAAgC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AACzG,UAAQ,KAAK,CAAC;AAChB;","names":[]}
1
+ {"version":3,"sources":["../../src/peer-floors/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `agent-app-peer-check` — fail a consumer's CI when its installed tree sits\n * below a peer floor this package declares.\n *\n * Runs in the CONSUMER's repo, over the consumer's own `node_modules`, because\n * the floors only mean anything against a real install. Add it next to\n * typecheck:\n *\n * \"scripts\": { \"peer-check\": \"agent-app-peer-check\" }\n *\n * Exits 1 on any violation, 0 otherwise. Takes no configuration — the floors\n * come from the installed shell manifest, so the check cannot go stale when the\n * shell adds an engine.\n */\nimport { checkPeerFloors, formatPeerFloorReport } from './check'\nimport { invokedAsScript } from '../signoff/invoked-as-script'\n\nfunction main(): void {\n const appDir = process.argv[2] ?? process.cwd()\n try {\n const report = checkPeerFloors({ appDir })\n process.stdout.write(`${formatPeerFloorReport(report)}\\n`)\n process.exit(report.ok ? 0 : 1)\n } catch (err) {\n process.stderr.write(`agent-app-peer-check failed: ${err instanceof Error ? err.message : String(err)}\\n`)\n process.exit(1)\n }\n}\n\n/* c8 ignore start — process wiring, exercised by the bin itself */\nif (invokedAsScript(import.meta.url, process.argv[1])) main()\n/* c8 ignore stop */\n"],"mappings":";;;;;;;;;;AAkBA,SAAS,OAAa;AACpB,QAAM,SAAS,QAAQ,KAAK,CAAC,KAAK,QAAQ,IAAI;AAC9C,MAAI;AACF,UAAM,SAAS,gBAAgB,EAAE,OAAO,CAAC;AACzC,YAAQ,OAAO,MAAM,GAAG,sBAAsB,MAAM,CAAC;AAAA,CAAI;AACzD,YAAQ,KAAK,OAAO,KAAK,IAAI,CAAC;AAAA,EAChC,SAAS,KAAK;AACZ,YAAQ,OAAO,MAAM,gCAAgC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AACzG,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAGA,IAAI,gBAAgB,YAAY,KAAK,QAAQ,KAAK,CAAC,CAAC,EAAG,MAAK;","names":[]}
@@ -3,6 +3,9 @@ import {
3
3
  formatPreflightReport,
4
4
  runPreflight
5
5
  } from "../chunk-5FAKBTCK.js";
6
+ import {
7
+ invokedAsScript
8
+ } from "../chunk-C3CAYGGQ.js";
6
9
 
7
10
  // src/preflight/cli.ts
8
11
  import { pathToFileURL } from "url";
@@ -34,8 +37,7 @@ async function runPreflightCli(argv) {
34
37
  console.log(formatPreflightReport(report));
35
38
  return report.ok ? 0 : 1;
36
39
  }
37
- var invokedDirectly = process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href;
38
- if (invokedDirectly) {
40
+ if (invokedAsScript(import.meta.url, process.argv[1])) {
39
41
  runPreflightCli(process.argv.slice(2)).then((code) => {
40
42
  process.exit(code);
41
43
  }).catch((err) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/preflight/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `agent-app-preflight` — the deploy-time probe runner.\n *\n * Loads a product's `preflight.config.mjs` (default export: `PreflightProbe[]`),\n * runs every probe, prints the operator table, and exits non-zero when a\n * critical probe fails so a deploy step can gate on it.\n *\n * Run it where the REAL secrets are — after `wrangler secret put …`, not in CI.\n * CI has no secrets, so a probe there proves nothing.\n *\n * agent-app-preflight # ./preflight.config.mjs\n * agent-app-preflight ops/probes.mjs # explicit path\n *\n * This bin existed only as a promise in the module docs until 0.44.0; at least\n * one product had hand-rolled a byte-identical `scripts/preflight.mjs` around\n * `runPreflight` + `formatPreflightReport` to fill the gap.\n */\nimport { pathToFileURL } from 'node:url'\nimport { resolve } from 'node:path'\nimport { existsSync } from 'node:fs'\nimport { formatPreflightReport, runPreflight, type PreflightProbe } from './index'\n\nconst DEFAULT_CONFIG = 'preflight.config.mjs'\n\n/**\n * Load the probe list from a config module. Fail loud on every miss — a\n * preflight that silently probes nothing and exits 0 is worse than no\n * preflight, because it reports the deploy as verified.\n */\nexport async function loadProbes(configPath: string): Promise<PreflightProbe[]> {\n const abs = resolve(process.cwd(), configPath)\n if (!existsSync(abs)) {\n throw new Error(\n `agent-app-preflight: no config at \"${abs}\". Create one that default-exports a PreflightProbe[], or pass a path.`,\n )\n }\n const mod: unknown = await import(pathToFileURL(abs).href)\n const probes = (mod as { default?: unknown }).default\n if (!Array.isArray(probes)) {\n throw new Error(\n `agent-app-preflight: \"${abs}\" must default-export an array of probes; got ${typeof probes}.`,\n )\n }\n if (probes.length === 0) {\n throw new Error(`agent-app-preflight: \"${abs}\" exported an empty probe list — nothing would be verified.`)\n }\n return probes as PreflightProbe[]\n}\n\n/** Run the probes named by `argv` and resolve with the process exit code. */\nexport async function runPreflightCli(argv: readonly string[]): Promise<number> {\n const probes = await loadProbes(argv[0] ?? DEFAULT_CONFIG)\n const report = await runPreflight(probes)\n console.log(formatPreflightReport(report))\n return report.ok ? 0 : 1\n}\n\n/* c8 ignore start — process wiring, exercised by the bin itself */\nconst invokedDirectly =\n process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href\n\nif (invokedDirectly) {\n runPreflightCli(process.argv.slice(2))\n .then((code) => {\n process.exit(code)\n })\n .catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : String(err))\n process.exit(1)\n })\n}\n/* c8 ignore stop */\n"],"mappings":";;;;;;;AAkBA,SAAS,qBAAqB;AAC9B,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAG3B,IAAM,iBAAiB;AAOvB,eAAsB,WAAW,YAA+C;AAC9E,QAAM,MAAM,QAAQ,QAAQ,IAAI,GAAG,UAAU;AAC7C,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,UAAM,IAAI;AAAA,MACR,sCAAsC,GAAG;AAAA,IAC3C;AAAA,EACF;AACA,QAAM,MAAe,MAAM,OAAO,cAAc,GAAG,EAAE;AACrD,QAAM,SAAU,IAA8B;AAC9C,MAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,UAAM,IAAI;AAAA,MACR,yBAAyB,GAAG,iDAAiD,OAAO,MAAM;AAAA,IAC5F;AAAA,EACF;AACA,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,MAAM,yBAAyB,GAAG,kEAA6D;AAAA,EAC3G;AACA,SAAO;AACT;AAGA,eAAsB,gBAAgB,MAA0C;AAC9E,QAAM,SAAS,MAAM,WAAW,KAAK,CAAC,KAAK,cAAc;AACzD,QAAM,SAAS,MAAM,aAAa,MAAM;AACxC,UAAQ,IAAI,sBAAsB,MAAM,CAAC;AACzC,SAAO,OAAO,KAAK,IAAI;AACzB;AAGA,IAAM,kBACJ,QAAQ,KAAK,CAAC,MAAM,UAAa,YAAY,QAAQ,cAAc,QAAQ,KAAK,CAAC,CAAC,EAAE;AAEtF,IAAI,iBAAiB;AACnB,kBAAgB,QAAQ,KAAK,MAAM,CAAC,CAAC,EAClC,KAAK,CAAC,SAAS;AACd,YAAQ,KAAK,IAAI;AAAA,EACnB,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,YAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAC9D,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACL;","names":[]}
1
+ {"version":3,"sources":["../../src/preflight/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `agent-app-preflight` — the deploy-time probe runner.\n *\n * Loads a product's `preflight.config.mjs` (default export: `PreflightProbe[]`),\n * runs every probe, prints the operator table, and exits non-zero when a\n * critical probe fails so a deploy step can gate on it.\n *\n * Run it where the REAL secrets are — after `wrangler secret put …`, not in CI.\n * CI has no secrets, so a probe there proves nothing.\n *\n * agent-app-preflight # ./preflight.config.mjs\n * agent-app-preflight ops/probes.mjs # explicit path\n *\n * This bin existed only as a promise in the module docs until 0.44.0; at least\n * one product had hand-rolled a byte-identical `scripts/preflight.mjs` around\n * `runPreflight` + `formatPreflightReport` to fill the gap.\n */\nimport { pathToFileURL } from 'node:url'\nimport { resolve } from 'node:path'\nimport { existsSync } from 'node:fs'\nimport { formatPreflightReport, runPreflight, type PreflightProbe } from './index'\nimport { invokedAsScript } from '../signoff/invoked-as-script'\n\nconst DEFAULT_CONFIG = 'preflight.config.mjs'\n\n/**\n * Load the probe list from a config module. Fail loud on every miss — a\n * preflight that silently probes nothing and exits 0 is worse than no\n * preflight, because it reports the deploy as verified.\n */\nexport async function loadProbes(configPath: string): Promise<PreflightProbe[]> {\n const abs = resolve(process.cwd(), configPath)\n if (!existsSync(abs)) {\n throw new Error(\n `agent-app-preflight: no config at \"${abs}\". Create one that default-exports a PreflightProbe[], or pass a path.`,\n )\n }\n const mod: unknown = await import(pathToFileURL(abs).href)\n const probes = (mod as { default?: unknown }).default\n if (!Array.isArray(probes)) {\n throw new Error(\n `agent-app-preflight: \"${abs}\" must default-export an array of probes; got ${typeof probes}.`,\n )\n }\n if (probes.length === 0) {\n throw new Error(`agent-app-preflight: \"${abs}\" exported an empty probe list — nothing would be verified.`)\n }\n return probes as PreflightProbe[]\n}\n\n/** Run the probes named by `argv` and resolve with the process exit code. */\nexport async function runPreflightCli(argv: readonly string[]): Promise<number> {\n const probes = await loadProbes(argv[0] ?? DEFAULT_CONFIG)\n const report = await runPreflight(probes)\n console.log(formatPreflightReport(report))\n return report.ok ? 0 : 1\n}\n\n/* c8 ignore start — process wiring, exercised by the bin itself */\nif (invokedAsScript(import.meta.url, process.argv[1])) {\n runPreflightCli(process.argv.slice(2))\n .then((code) => {\n process.exit(code)\n })\n .catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : String(err))\n process.exit(1)\n })\n}\n/* c8 ignore stop */\n"],"mappings":";;;;;;;;;;AAkBA,SAAS,qBAAqB;AAC9B,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAI3B,IAAM,iBAAiB;AAOvB,eAAsB,WAAW,YAA+C;AAC9E,QAAM,MAAM,QAAQ,QAAQ,IAAI,GAAG,UAAU;AAC7C,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,UAAM,IAAI;AAAA,MACR,sCAAsC,GAAG;AAAA,IAC3C;AAAA,EACF;AACA,QAAM,MAAe,MAAM,OAAO,cAAc,GAAG,EAAE;AACrD,QAAM,SAAU,IAA8B;AAC9C,MAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,UAAM,IAAI;AAAA,MACR,yBAAyB,GAAG,iDAAiD,OAAO,MAAM;AAAA,IAC5F;AAAA,EACF;AACA,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,MAAM,yBAAyB,GAAG,kEAA6D;AAAA,EAC3G;AACA,SAAO;AACT;AAGA,eAAsB,gBAAgB,MAA0C;AAC9E,QAAM,SAAS,MAAM,WAAW,KAAK,CAAC,KAAK,cAAc;AACzD,QAAM,SAAS,MAAM,aAAa,MAAM;AACxC,UAAQ,IAAI,sBAAsB,MAAM,CAAC;AACzC,SAAO,OAAO,KAAK,IAAI;AACzB;AAGA,IAAI,gBAAgB,YAAY,KAAK,QAAQ,KAAK,CAAC,CAAC,GAAG;AACrD,kBAAgB,QAAQ,KAAK,MAAM,CAAC,CAAC,EAClC,KAAK,CAAC,SAAS;AACd,YAAQ,KAAK,IAAI;AAAA,EACnB,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,YAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAC9D,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACL;","names":[]}
@@ -1,4 +1,4 @@
1
- import { S as SignoffSource, a as SignoffEvent, b as SignoffReport } from './types-U7Nz-txa.js';
1
+ import { S as SignoffSource, a as SignoffEvent, b as SignoffReport } from './types-Bd6uW9vQ.js';
2
2
 
3
3
  /**
4
4
  * The sign-off run: reproduce a clean CI environment, then go further.
@@ -1104,6 +1104,16 @@ interface LivenessProbeConfig {
1104
1104
  sidecarProcessPattern?: (harness: Harness) => string;
1105
1105
  execTimeoutMs?: number;
1106
1106
  psTimeoutMs?: number;
1107
+ /**
1108
+ * Reuse a successful liveness result for this many milliseconds for the
1109
+ * same box id. Defaults to 5 seconds; set to 0 to probe on every reuse.
1110
+ *
1111
+ * Only the exec/sidecar probe is cached. Runtime readiness, egress policy,
1112
+ * deferred-file materialization, and bootstrap still run on every reuse.
1113
+ * A box that dies during this window is surfaced by the next dispatch and
1114
+ * is probed again after the TTL; the cache never triggers box deletion.
1115
+ */
1116
+ cacheTtlMs?: number;
1107
1117
  }
1108
1118
  /** Define options for composing a user profile including prompts, files, servers, and name */
1109
1119
  interface ProfileComposeOptions {
@@ -1201,7 +1211,7 @@ interface SandboxRuntimeConfig {
1201
1211
  declare const DEFAULT_SANDBOX_RESOURCES: SandboxResourceConfig;
1202
1212
  /** Resolve a synchronous sandbox client from provided runtime configuration credentials */
1203
1213
  declare function getClient(shell: SandboxRuntimeConfig): Sandbox;
1204
- /** Reset the client cache to clear stored data and force fresh retrieval */
1214
+ /** Reset the process-local sandbox client and liveness-verification caches. */
1205
1215
  declare function resetClientCache(): void;
1206
1216
  /** Describe an application tool with its name, unique key, and description */
1207
1217
  interface AppToolDescriptor {
@@ -89,7 +89,7 @@ import {
89
89
  workspaceSandboxRecoveryMessage,
90
90
  workspaceSandboxRecoveryRecommendedActions,
91
91
  writeProfileFilesToBox
92
- } from "../chunk-OTLEYHOG.js";
92
+ } from "../chunk-RRH23CGS.js";
93
93
  import "../chunk-TH7L265V.js";
94
94
  import "../chunk-LWSJK546.js";
95
95
  import "../chunk-P5PIAQVS.js";
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { R as RunSignoffOptions } from '../run-tEsZUhAf.js';
3
- import '../types-U7Nz-txa.js';
2
+ import { R as RunSignoffOptions } from '../run-D80hoLKh.js';
3
+ import '../types-Bd6uW9vQ.js';
4
4
 
5
5
  interface ParsedArgs extends RunSignoffOptions {
6
6
  readonly jsonPath?: string;
@@ -1,28 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ formatSignoffLine,
3
4
  formatSignoffReport,
4
5
  runSignoff
5
- } from "../chunk-WD2B6HGY.js";
6
+ } from "../chunk-K4RA5T3A.js";
7
+ import {
8
+ invokedAsScript
9
+ } from "../chunk-C3CAYGGQ.js";
6
10
 
7
11
  // src/signoff/cli.ts
8
12
  import { mkdirSync, writeFileSync } from "fs";
9
13
  import { dirname, resolve } from "path";
10
-
11
- // src/signoff/invoked-as-script.ts
12
- import { realpathSync } from "fs";
13
- import { pathToFileURL } from "url";
14
- function invokedAsScript(moduleUrl, entry) {
15
- if (entry === void 0 || entry.length === 0) return false;
16
- let resolved;
17
- try {
18
- resolved = realpathSync(entry);
19
- } catch {
20
- return false;
21
- }
22
- return moduleUrl === pathToFileURL(resolved).href;
23
- }
24
-
25
- // src/signoff/cli.ts
26
14
  function usage() {
27
15
  return [
28
16
  "Usage: agent-app-signoff [repoDir] [options]",
@@ -148,7 +136,7 @@ async function runSignoffCli(argv) {
148
136
  writeFileSync(abs, `${JSON.stringify(report, null, 2)}
149
137
  `);
150
138
  }
151
- process.stdout.write(`${formatSignoffReport(report)}
139
+ process.stdout.write(`${args.quiet ? formatSignoffLine(report) : formatSignoffReport(report)}
152
140
  `);
153
141
  return report.ok ? 0 : 1;
154
142
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/signoff/cli.ts","../../src/signoff/invoked-as-script.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `agent-app-signoff` — run the local sign-off gate and print the proof.\n *\n * Exits 0 only when every declared step passed in a pristine tree. Exit 1 is a\n * real failure; exit 2 is a usage or config error, so a script can tell \"your\n * code is broken\" from \"your gate is misconfigured\".\n *\n * agent-app-signoff # this repo, working tree, default config\n * agent-app-signoff --source head # exactly the commit that would merge\n * agent-app-signoff --seed 12345 # reproduce a previous run's orders\n * agent-app-signoff --keep-going # run every step, full picture\n * agent-app-signoff --json proof.json # machine-readable record\n */\nimport { mkdirSync, writeFileSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { invokedAsScript } from './invoked-as-script'\nimport { formatSignoffReport } from './report'\nimport { runSignoff, type RunSignoffOptions } from './run'\nimport type { SignoffEvent, SignoffSource } from './types'\n\ninterface ParsedArgs extends RunSignoffOptions {\n readonly jsonPath?: string\n readonly quiet?: boolean\n}\n\nfunction usage(): string {\n return [\n 'Usage: agent-app-signoff [repoDir] [options]',\n '',\n ' --source head|working-tree bytes to verify (default working-tree)',\n ' --config <path> config module (default signoff.config.mjs)',\n ' --seed <n> base seed; reproduces a previous run',\n ' --shuffle-runs <n> override every shuffled step\\'s run count',\n ' --max-parallel <n> cap concurrent steps',\n ' --cache-dir <path> clean trees + pristine stores live here',\n ' --keep-going run every step even after one fails',\n ' --keep-workspace leave the clean tree on disk to inspect',\n ' --json <path> write the machine-readable report',\n ' --quiet verdict only; no per-step progress',\n ].join('\\n')\n}\n\nfunction fail(message: string): never {\n process.stderr.write(`agent-app-signoff: ${message}\\n\\n${usage()}\\n`)\n process.exit(2)\n}\n\nfunction positiveInt(raw: string, flag: string): number {\n const value = Number.parseInt(raw, 10)\n if (!Number.isFinite(value) || value < 0) fail(`${flag} needs a non-negative integer, got \"${raw}\"`)\n return value\n}\n\nexport function parseArgs(argv: readonly string[]): ParsedArgs {\n const parsed: {\n repoDir?: string\n configPath?: string\n source?: SignoffSource\n seed?: number\n shuffleRuns?: number\n maxParallel?: number\n cacheDir?: string\n keepGoing?: boolean\n keepWorkspace?: boolean\n jsonPath?: string\n quiet?: boolean\n } = {}\n\n for (let index = 0; index < argv.length; index += 1) {\n const flag = argv[index] as string\n const take = (): string => {\n const value = argv[index + 1]\n if (value === undefined) fail(`${flag} needs a value`)\n index += 1\n return value\n }\n switch (flag) {\n case '--source': {\n const value = take()\n if (value !== 'head' && value !== 'working-tree') fail(`--source must be head or working-tree, got \"${value}\"`)\n parsed.source = value\n break\n }\n case '--config': parsed.configPath = take(); break\n case '--seed': parsed.seed = positiveInt(take(), '--seed'); break\n case '--shuffle-runs': parsed.shuffleRuns = positiveInt(take(), '--shuffle-runs'); break\n case '--max-parallel': parsed.maxParallel = positiveInt(take(), '--max-parallel'); break\n case '--cache-dir': parsed.cacheDir = take(); break\n case '--keep-going': parsed.keepGoing = true; break\n case '--keep-workspace': parsed.keepWorkspace = true; break\n case '--json': parsed.jsonPath = take(); break\n case '--quiet': parsed.quiet = true; break\n case '-h':\n case '--help':\n process.stdout.write(`${usage()}\\n`)\n process.exit(0)\n break\n default:\n if (flag.startsWith('-')) fail(`unknown option: ${flag}`)\n if (parsed.repoDir !== undefined) fail(`unexpected second directory: ${flag}`)\n parsed.repoDir = flag\n }\n }\n return parsed\n}\n\nfunction progressWriter(): (event: SignoffEvent) => void {\n return (event) => {\n switch (event.kind) {\n case 'tree':\n process.stderr.write(`· clean tree at ${event.path} (${event.head.slice(0, 12)}${event.dirty ? ' + working tree' : ''})\\n`)\n break\n case 'store':\n process.stderr.write(`· store ${event.cacheHit ? 'warm' : 'COLD'} ${event.cacheKey.slice(0, 16)}\\n`)\n break\n case 'install-start':\n process.stderr.write(`· ${event.command}\\n`)\n break\n case 'install-end':\n process.stderr.write(`· install exit ${event.exitCode} in ${event.durationMs}ms\\n`)\n break\n case 'step-start':\n process.stderr.write(`· start ${event.name}${event.seed === null ? '' : ` (seed ${event.seed})`}\\n`)\n break\n case 'step-end':\n process.stderr.write(`· ${event.status === 'passed' ? 'ok' : event.status} ${event.name} ${event.durationMs}ms\\n`)\n break\n }\n }\n}\n\nexport async function runSignoffCli(argv: readonly string[]): Promise<number> {\n const args = parseArgs(argv)\n const report = await runSignoff({ ...args, onEvent: args.quiet ? undefined : progressWriter() })\n\n if (args.jsonPath !== undefined) {\n const abs = resolve(args.jsonPath)\n mkdirSync(dirname(abs), { recursive: true })\n writeFileSync(abs, `${JSON.stringify(report, null, 2)}\\n`)\n }\n\n process.stdout.write(`${formatSignoffReport(report)}\\n`)\n return report.ok ? 0 : 1\n}\n\n/* c8 ignore start — process wiring, exercised by the bin itself */\nif (invokedAsScript(import.meta.url, process.argv[1])) {\n runSignoffCli(process.argv.slice(2))\n .then((code) => process.exit(code))\n .catch((err: unknown) => {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`)\n process.exit(2)\n })\n}\n/* c8 ignore stop */\n","import { realpathSync } from 'node:fs'\nimport { pathToFileURL } from 'node:url'\n\n/**\n * Is this module the script the process was started with?\n *\n * The obvious spelling — `import.meta.url === pathToFileURL(process.argv[1]).href`\n * — is wrong for every packaged bin, and wrong in the silent direction: the\n * module decides it was imported rather than executed, so the CLI does nothing\n * and exits 0.\n *\n * The two sides are not the same kind of path. `import.meta.url` is the module\n * URL Node resolved, and Node resolves a module's realpath unless started with\n * `--preserve-symlinks`. `process.argv[1]` is the path as SPELLED on the\n * command line. A package manager's bin shim spells it through symlinked\n * directories:\n *\n * argv[1] …/node_modules/.bin/../@scope/pkg/dist/cli.js\n * import.meta.url …/node_modules/.pnpm/pkg@1.2.3/node_modules/@scope/pkg/dist/cli.js\n *\n * Same file, two strings, and a `===` between them is false.\n *\n * So the entry path is resolved the same way Node resolved the module before\n * the comparison. A missing or unresolvable entry is not this function's\n * business to explain — it is simply not this module, which is the honest\n * answer and keeps an importer from executing a CLI.\n */\nexport function invokedAsScript(moduleUrl: string, entry: string | undefined): boolean {\n if (entry === undefined || entry.length === 0) return false\n let resolved: string\n try {\n resolved = realpathSync(entry)\n } catch {\n return false\n }\n return moduleUrl === pathToFileURL(resolved).href\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/signoff/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `agent-app-signoff` — run the local sign-off gate and print the proof.\n *\n * Exits 0 only when every declared step passed in a pristine tree. Exit 1 is a\n * real failure; exit 2 is a usage or config error, so a script can tell \"your\n * code is broken\" from \"your gate is misconfigured\".\n *\n * agent-app-signoff # this repo, working tree, default config\n * agent-app-signoff --source head # exactly the commit that would merge\n * agent-app-signoff --seed 12345 # reproduce a previous run's orders\n * agent-app-signoff --keep-going # run every step, full picture\n * agent-app-signoff --json proof.json # machine-readable record\n */\nimport { mkdirSync, writeFileSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { invokedAsScript } from './invoked-as-script'\nimport { formatSignoffLine, formatSignoffReport } from './report'\nimport { runSignoff, type RunSignoffOptions } from './run'\nimport type { SignoffEvent, SignoffSource } from './types'\n\ninterface ParsedArgs extends RunSignoffOptions {\n readonly jsonPath?: string\n readonly quiet?: boolean\n}\n\nfunction usage(): string {\n return [\n 'Usage: agent-app-signoff [repoDir] [options]',\n '',\n ' --source head|working-tree bytes to verify (default working-tree)',\n ' --config <path> config module (default signoff.config.mjs)',\n ' --seed <n> base seed; reproduces a previous run',\n ' --shuffle-runs <n> override every shuffled step\\'s run count',\n ' --max-parallel <n> cap concurrent steps',\n ' --cache-dir <path> clean trees + pristine stores live here',\n ' --keep-going run every step even after one fails',\n ' --keep-workspace leave the clean tree on disk to inspect',\n ' --json <path> write the machine-readable report',\n ' --quiet verdict only; no per-step progress',\n ].join('\\n')\n}\n\nfunction fail(message: string): never {\n process.stderr.write(`agent-app-signoff: ${message}\\n\\n${usage()}\\n`)\n process.exit(2)\n}\n\nfunction positiveInt(raw: string, flag: string): number {\n const value = Number.parseInt(raw, 10)\n if (!Number.isFinite(value) || value < 0) fail(`${flag} needs a non-negative integer, got \"${raw}\"`)\n return value\n}\n\nexport function parseArgs(argv: readonly string[]): ParsedArgs {\n const parsed: {\n repoDir?: string\n configPath?: string\n source?: SignoffSource\n seed?: number\n shuffleRuns?: number\n maxParallel?: number\n cacheDir?: string\n keepGoing?: boolean\n keepWorkspace?: boolean\n jsonPath?: string\n quiet?: boolean\n } = {}\n\n for (let index = 0; index < argv.length; index += 1) {\n const flag = argv[index] as string\n const take = (): string => {\n const value = argv[index + 1]\n if (value === undefined) fail(`${flag} needs a value`)\n index += 1\n return value\n }\n switch (flag) {\n case '--source': {\n const value = take()\n if (value !== 'head' && value !== 'working-tree') fail(`--source must be head or working-tree, got \"${value}\"`)\n parsed.source = value\n break\n }\n case '--config': parsed.configPath = take(); break\n case '--seed': parsed.seed = positiveInt(take(), '--seed'); break\n case '--shuffle-runs': parsed.shuffleRuns = positiveInt(take(), '--shuffle-runs'); break\n case '--max-parallel': parsed.maxParallel = positiveInt(take(), '--max-parallel'); break\n case '--cache-dir': parsed.cacheDir = take(); break\n case '--keep-going': parsed.keepGoing = true; break\n case '--keep-workspace': parsed.keepWorkspace = true; break\n case '--json': parsed.jsonPath = take(); break\n case '--quiet': parsed.quiet = true; break\n case '-h':\n case '--help':\n process.stdout.write(`${usage()}\\n`)\n process.exit(0)\n break\n default:\n if (flag.startsWith('-')) fail(`unknown option: ${flag}`)\n if (parsed.repoDir !== undefined) fail(`unexpected second directory: ${flag}`)\n parsed.repoDir = flag\n }\n }\n return parsed\n}\n\nfunction progressWriter(): (event: SignoffEvent) => void {\n return (event) => {\n switch (event.kind) {\n case 'tree':\n process.stderr.write(`· clean tree at ${event.path} (${event.head.slice(0, 12)}${event.dirty ? ' + working tree' : ''})\\n`)\n break\n case 'store':\n process.stderr.write(`· store ${event.cacheHit ? 'warm' : 'COLD'} ${event.cacheKey.slice(0, 16)}\\n`)\n break\n case 'install-start':\n process.stderr.write(`· ${event.command}\\n`)\n break\n case 'install-end':\n process.stderr.write(`· install exit ${event.exitCode} in ${event.durationMs}ms\\n`)\n break\n case 'step-start':\n process.stderr.write(`· start ${event.name}${event.seed === null ? '' : ` (seed ${event.seed})`}\\n`)\n break\n case 'step-end':\n process.stderr.write(`· ${event.status === 'passed' ? 'ok' : event.status} ${event.name} ${event.durationMs}ms\\n`)\n break\n }\n }\n}\n\nexport async function runSignoffCli(argv: readonly string[]): Promise<number> {\n const args = parseArgs(argv)\n const report = await runSignoff({ ...args, onEvent: args.quiet ? undefined : progressWriter() })\n\n if (args.jsonPath !== undefined) {\n const abs = resolve(args.jsonPath)\n mkdirSync(dirname(abs), { recursive: true })\n writeFileSync(abs, `${JSON.stringify(report, null, 2)}\\n`)\n }\n\n // `--quiet` is documented as \"verdict only\", and CLAUDE.md names the one-line\n // `signoff PASS <sha> …` summary as an accepted PR proof. Printing the full\n // report here contradicted both, and left the only producer of that summary\n // with no caller.\n process.stdout.write(`${args.quiet ? formatSignoffLine(report) : formatSignoffReport(report)}\\n`)\n return report.ok ? 0 : 1\n}\n\n/* c8 ignore start — process wiring, exercised by the bin itself */\nif (invokedAsScript(import.meta.url, process.argv[1])) {\n runSignoffCli(process.argv.slice(2))\n .then((code) => process.exit(code))\n .catch((err: unknown) => {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`)\n process.exit(2)\n })\n}\n/* c8 ignore stop */\n"],"mappings":"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","names":[]}