total-recall-brain 3.45.0 → 3.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/total-recall.mjs +4 -2
- package/frontend/dist/assets/index-BRYE1jmv.css +1 -0
- package/frontend/dist/assets/index-D1goIPjC.js +5598 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +5 -4
- package/frontend/src/App.tsx +183 -236
- package/frontend/src/api/auth-inventory.ts +62 -0
- package/frontend/src/api/memory.ts +13 -8
- package/frontend/src/api/plugin-credentials.ts +23 -0
- package/frontend/src/api/plugins.ts +37 -53
- package/frontend/src/api/workspace-drawers.ts +97 -0
- package/frontend/src/components/BrainSelector.tsx +9 -3
- package/frontend/src/components/plugins/PluginPanel.tsx +83 -42
- package/frontend/src/components/plugins/PluginSettings.tsx +10 -20
- package/frontend/src/components/plugins/PluginWorkspace.tsx +367 -0
- package/frontend/src/components/plugins/ScopedPluginCommands.tsx +142 -0
- package/frontend/src/components/plugins/plugin-frame.ts +44 -33
- package/frontend/src/components/plugins/scoped-plugin-operations.ts +28 -0
- package/frontend/src/components/workspace/DrawerChat.tsx +110 -0
- package/frontend/src/components/workspace/DrawerLayout.tsx +196 -0
- package/frontend/src/components/workspace/DrawerMarkup.tsx +49 -0
- package/frontend/src/components/workspace/PluginChunkLibrary.tsx +92 -0
- package/frontend/src/components/workspace/PluginHome.tsx +76 -0
- package/frontend/src/components/workspace/WorkspaceDrawers.tsx +96 -0
- package/frontend/src/components/workspace/WorkspacePage.tsx +95 -0
- package/frontend/src/components/workspace/usePluginDesign.ts +38 -0
- package/frontend/src/index.css +409 -136
- package/frontend/src/pages/HelpPage.tsx +15 -46
- package/frontend/src/pages/MemoryPage.tsx +19 -36
- package/frontend/src/pages/SettingsPage.tsx +1 -1
- package/metadata.plugin.schema.json +53 -227
- package/package.json +1 -1
- package/scaffold/.agent/skills/plugin/SKILL.md +13 -11
- package/scaffold/.agent/skills/plugin/evals/README.md +14 -21
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +15 -18
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +19 -24
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +12 -20
- package/scaffold/.agent/skills/plugin/evals/cases/04-home-collects-credential.mjs +35 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +8 -28
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +85 -47
- package/scaffold/.agent/skills/plugin/evals/run.mjs +17 -18
- package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +4 -4
- package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +2 -2
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +4 -4
- package/scaffold/.agent/skills/plugin/references/anti-patterns.md +2 -2
- package/scaffold/.agent/skills/plugin/references/generative-home.md +38 -0
- package/scaffold/.agent/skills/plugin/references/scaffold.md +5 -5
- package/scaffold/.agent/skills/plugin/references/surfaces.md +13 -4
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +75 -153
- package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/DESIGN.md +76 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +3 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +20 -15
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +13 -51
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/access-overview.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/agent-login.md +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/baseline-audit.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/credential-repository-binding.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/credential-store-maintenance.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/dashboard-password.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/dashboard-sessions.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/endpoint-probe.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/google-sign-in.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/mesh-machines.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/passkey-enrollment.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/registered-brains.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/scoped-access-keys.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/secret-inventory.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/signing-key-rotation.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/home.js +375 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/{ui/DESIGN.md → DESIGN.md} +32 -21
- package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +17 -1
- package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +10 -1
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +4 -3
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +11 -27
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/control-ledger.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/local-inventory-triage.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/pending-control.md +39 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/run-evidence.md +41 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/schedule-vs-run-evidence.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/source-coverage.md +41 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/{card.js → home.js} +32 -27
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/DESIGN.md +53 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/cli.mjs +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +40 -40
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/config.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/detect.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/gate.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/init.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/report.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/run.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/snapshot.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/code-quality-report.mjs +55 -23
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/home.mjs +214 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/DESIGN.md +146 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +51 -34
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/command.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/create.md +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/history.md +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/library.md +49 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/list.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/promote.md +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/read.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/remove.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/rollback.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/show.md +53 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/status.md +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/test-path.md +38 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/test.md +46 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/update.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/home.js +533 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/DESIGN.md +110 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +13 -10
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/benchmark.md +49 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/coverage.md +53 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/models.md +43 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/pull.md +41 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/status.md +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/home.js +353 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/DESIGN.md +31 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/README.md +25 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/cli.mjs +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/git/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +17 -27
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -6
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +24 -6
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +33 -7
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +29 -5
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/unmerged.mjs +182 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/change-comparison.md +27 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/commit-identity.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/create-worktree.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/registered-worktrees.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/remove-worktree.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/repository-ledger.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/unmerged-work.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/upstream-divergence.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/worktree-conflict-triage.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/worktree-terminal.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/home.js +273 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/DESIGN.md +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +4 -5
- package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +9 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +37 -39
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +5 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/active-device-dispatch.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/control-probe.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/control-server.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/credential-readiness.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/elected-leader.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/enrollment-key.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/file-receipt.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/fleet-execution.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/fleet-presence.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/interactive-ssh.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/local-enrollment.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/mesh-policy.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/network-atlas.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/node-roster.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/node-selection-proof.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/owner-observations.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/peer-path.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/pinned-leader.md +24 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/reachability-evidence-ladder.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/recorded-login.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/remote-execution.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/secret-grants.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/{card.js → home.js} +93 -22
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/DESIGN.md +86 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +19 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +43 -22
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/chat.mjs +80 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/adapter-docs-freshness.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/chat-handoff.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/fleet-maintenance.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/harness-inventory.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/included-access.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/included-usage-verification.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/independent-review.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/managed-execution.md +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/official-documentation.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/quota-recovery.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/review-routing-decision.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/route-work.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/routing-evidence.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/run-ledger.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/home.js +198 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/DESIGN.md +75 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +2 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +17 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +10 -44
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +5 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +3 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/ingest.mjs +23 -18
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +8 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +3 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +21 -47
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +17 -6
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/auto-refresh.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/bootstrap-pages.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/generate-wiki.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/generator-settings.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/host-probe.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/ingest-preview.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/link-hygiene.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/link-topology.md +19 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/passage-search.md +19 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/propagation-plan.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/provider-credentials.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/refresh-report.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/task-evidence.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/upstream-record.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/watch-upstream.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/wiki-atlas.md +19 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +261 -513
- package/scaffold/.agent/skills/total-recall/plugins/security/DESIGN.md +84 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/README.md +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -19
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +2 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +19 -52
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/device-evidence.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/evidence-desk.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/guided-device-review.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/host-and-network.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/https-headers.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/mesh-coverage.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/private-security-tools.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/single-origin-header-check.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/source-review.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/ssh-permission-plan.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/home.js +283 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/DESIGN.md +93 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +5 -3
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/conformance.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/registry.md +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/repair.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/scaffold.md +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/status.md +43 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/validate.md +34 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/validation-diagnostics.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/workspace-registry-preflight.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/home.js +348 -0
- package/scaffold/.agent/skills/total-recall/references/operations.md +2 -0
- package/src/cli/api.mjs +3 -3
- package/src/cli/brief.mjs +3 -0
- package/src/cli/config.mjs +16 -0
- package/src/cli/connect.mjs +2 -7
- package/src/cli/plugin/chunks.mjs +145 -0
- package/src/cli/plugin/create.mjs +24 -52
- package/src/cli/plugin/index.mjs +20 -9
- package/src/cli/setup.mjs +2 -9
- package/src/cli/start.mjs +1 -0
- package/src/cli/startup.mjs +18 -0
- package/src/core/app-deploy/design-tokens.mjs +14 -9
- package/src/core/app-deploy/ui-elements.mjs +1 -1
- package/src/core/brain-registry.mjs +5 -0
- package/src/core/brain-updates.mjs +5 -1
- package/src/core/daemon-control.mjs +5 -1
- package/src/core/dashboard-bundle.mjs +261 -0
- package/src/core/ensure-dependencies.mjs +19 -15
- package/src/core/memory-brain.mjs +30 -8
- package/src/core/plugin-activation.mjs +63 -20
- package/src/core/plugin-contracts/{card.mjs → manifest.mjs} +23 -57
- package/src/core/plugin-contracts/startup-check.mjs +49 -0
- package/src/core/plugin-contracts/ui.mjs +9 -3
- package/src/core/plugin-distribution.mjs +6 -6
- package/src/core/plugin-lifecycle.mjs +58 -0
- package/src/core/plugin-loader.mjs +9 -5
- package/src/core/plugin-runner.mjs +21 -10
- package/src/core/plugin-startup-checks.mjs +44 -0
- package/src/core/plugin-state.mjs +19 -5
- package/src/core/plugin-store.mjs +30 -11
- package/src/core/project-id.mjs +15 -5
- package/src/core/required-plugins.mjs +31 -6
- package/src/core/schema.mjs +2 -0
- package/src/core/ssss-operation-service.mjs +1 -0
- package/src/core/startup-health.mjs +28 -3
- package/src/core/testing/plugin-fixture.mjs +22 -21
- package/src/core/throttled-fetch.mjs +25 -18
- package/src/core/ui-chunk-choice.mjs +77 -0
- package/src/core/ui-chunk.registry.json +23 -0
- package/src/core/ui-chunks.mjs +434 -0
- package/src/core/vault-cache.mjs +58 -59
- package/src/core/workspace-compositions.mjs +151 -0
- package/src/core/workspace-drawers.mjs +79 -0
- package/src/server/api.mjs +4 -27
- package/src/server/index.mjs +28 -21
- package/src/server/memory-events.mjs +31 -0
- package/src/server/plugin-brain-targets.mjs +89 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-inventory.mjs +3 -2
- package/src/server/route-manifest.json +52 -4
- package/src/server/routes/_shared.mjs +7 -14
- package/src/server/routes/brains.mjs +29 -44
- package/src/server/routes/dashboard.mjs +26 -0
- package/src/server/routes/plugins.mjs +154 -45
- package/src/server/routes/ui-chunks.mjs +192 -0
- package/frontend/dist/assets/index-DACh1dhx.js +0 -5613
- package/frontend/dist/assets/index-DtwsDyFM.css +0 -1
- package/frontend/src/components/plugins/card/CardEngine.tsx +0 -108
- package/frontend/src/components/plugins/card/CardScenes.tsx +0 -103
- package/frontend/src/components/plugins/card/ExecutionPicker.tsx +0 -64
- package/frontend/src/components/plugins/card/args.ts +0 -28
- package/frontend/src/components/plugins/card/card.css +0 -32
- package/frontend/src/components/plugins/card/cli-line.ts +0 -13
- package/frontend/src/components/plugins/card/contract.ts +0 -91
- package/frontend/src/components/plugins/card/format.ts +0 -49
- package/frontend/src/components/plugins/card/runner-context.ts +0 -11
- package/frontend/src/components/plugins/card/runner.tsx +0 -43
- package/frontend/src/components/plugins/card/theme.ts +0 -35
- package/frontend/src/components/plugins/card/use-source.ts +0 -22
- package/frontend/src/components/plugins/card/widgets.tsx +0 -94
- package/frontend/src/components/plugins/workspace/WorkspaceHost.tsx +0 -23
- package/frontend/src/pages/PluginsPage.tsx +0 -1719
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +0 -47
- package/scaffold/.agent/skills/plugin/prompts/06-card.md +0 -46
- package/scaffold/.agent/skills/plugin/references/card.md +0 -90
- package/scaffold/.agent/skills/plugin/subagents/card-designer.md +0 -60
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +0 -67
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +0 -1179
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +0 -588
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +0 -575
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +0 -482
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/DESIGN.md +0 -100
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/card.js +0 -637
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/DESIGN.md +0 -217
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +0 -1276
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +0 -45
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +0 -1401
- package/scaffold/.agent/skills/total-recall/plugins/security/scripts/build-ui.mjs +0 -4
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +0 -41
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +0 -396
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/panel.js +0 -98
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/DESIGN.md +0 -15
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/card.js +0 -182
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dashboard bundle a source checkout serves, kept as new as its sources.
|
|
3
|
+
*
|
|
4
|
+
* The server used to build `frontend/dist` only when `index.html` was missing,
|
|
5
|
+
* and auto-pull rebuilt it only when a pull from origin changed `frontend/`.
|
|
6
|
+
* A local merge or commit refreshed nothing, so the served dashboard silently
|
|
7
|
+
* stayed on an old build: the plugin workspace drawers committed on
|
|
8
|
+
* 2026-11-08 never appeared at localhost:3000 because the bundle there was
|
|
9
|
+
* built on 2026-11-09 from code without them, and nothing ever looked again.
|
|
10
|
+
*
|
|
11
|
+
* Here "stale" has one definition, shared with the publish gate
|
|
12
|
+
* (`scripts/check-dist-freshness.mjs`): a source file newer than the newest
|
|
13
|
+
* bundle file. The server checks on start and on an interval, rebuilds in the
|
|
14
|
+
* background with the frontend's own build script, and keeps serving the
|
|
15
|
+
* current bundle until the new one is complete. It never installs
|
|
16
|
+
* dependencies: missing build tools are reported, not repaired behind the
|
|
17
|
+
* operator's back. An npm tarball gives every file the same mtime, so an
|
|
18
|
+
* installed package is never stale and never builds.
|
|
19
|
+
*/
|
|
20
|
+
import { spawn } from 'node:child_process';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist']);
|
|
26
|
+
|
|
27
|
+
/** Sources must be untouched this long before a rebuild, so a half-written edit is not built. */
|
|
28
|
+
export const QUIET_MS = 60_000;
|
|
29
|
+
export const CHECK_INTERVAL_MS = 5 * 60_000;
|
|
30
|
+
export const BUILD_TIMEOUT_MS = 10 * 60_000;
|
|
31
|
+
const OUTPUT_TAIL_BYTES = 4096;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Newest mtime under a file or directory, and the path carrying it.
|
|
35
|
+
* Missing paths contribute nothing rather than failing: vite.config.ts and
|
|
36
|
+
* friends are allowed not to exist.
|
|
37
|
+
*/
|
|
38
|
+
export async function newestUnder(target) {
|
|
39
|
+
let info;
|
|
40
|
+
try {
|
|
41
|
+
info = await stat(target);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (info.isFile()) return { path: target, mtimeMs: info.mtimeMs };
|
|
47
|
+
|
|
48
|
+
let best = null;
|
|
49
|
+
const entries = await readdir(target, { withFileTypes: true });
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (entry.isDirectory() && IGNORED_DIRS.has(entry.name)) continue;
|
|
52
|
+
const found = await newestUnder(path.join(target, entry.name));
|
|
53
|
+
if (found && (!best || found.mtimeMs > best.mtimeMs)) best = found;
|
|
54
|
+
}
|
|
55
|
+
return best;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether the built bundle can be trusted to represent the sources.
|
|
60
|
+
* Returns { ok, reason, bundle, source }; never throws, never prints, so the
|
|
61
|
+
* decision can be tested apart from how it is reported.
|
|
62
|
+
*/
|
|
63
|
+
export async function checkDistFreshness({ distDir, sourcePaths }) {
|
|
64
|
+
const bundle = await newestUnder(distDir);
|
|
65
|
+
if (!bundle) return { ok: false, reason: 'missing-bundle', bundle: null, source: null };
|
|
66
|
+
|
|
67
|
+
let source = null;
|
|
68
|
+
for (const candidate of sourcePaths) {
|
|
69
|
+
const found = await newestUnder(candidate);
|
|
70
|
+
if (found && (!source || found.mtimeMs > source.mtimeMs)) source = found;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (source && source.mtimeMs > bundle.mtimeMs) {
|
|
74
|
+
return { ok: false, reason: 'stale-bundle', bundle, source };
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, reason: 'current', bundle, source };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Every input whose change changes the built dashboard. */
|
|
80
|
+
export function dashboardTargets(root) {
|
|
81
|
+
const frontend = path.join(root, 'frontend');
|
|
82
|
+
let tsconfigs = [];
|
|
83
|
+
try {
|
|
84
|
+
tsconfigs = fs.readdirSync(frontend).filter(name => /^tsconfig.*\.json$/.test(name)).map(name => path.join(frontend, name));
|
|
85
|
+
} catch { /* no frontend directory: nothing to compare */ }
|
|
86
|
+
return {
|
|
87
|
+
distDir: path.join(frontend, 'dist'),
|
|
88
|
+
sourcePaths: [
|
|
89
|
+
path.join(frontend, 'src'),
|
|
90
|
+
path.join(frontend, 'public'),
|
|
91
|
+
path.join(frontend, 'index.html'),
|
|
92
|
+
path.join(frontend, 'vite.config.ts'),
|
|
93
|
+
path.join(frontend, 'package.json'),
|
|
94
|
+
...tsconfigs,
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The build script needs vite and the TypeScript compiler, hoisted to the root or local to the frontend. */
|
|
100
|
+
export function buildToolsPresent(root) {
|
|
101
|
+
const frontend = path.join(root, 'frontend');
|
|
102
|
+
const has = rel => [root, frontend].some(dir => fs.existsSync(path.join(dir, 'node_modules', rel)));
|
|
103
|
+
return fs.existsSync(path.join(frontend, 'package.json')) && has(path.join('vite', 'bin', 'vite.js')) && has(path.join('typescript', 'bin', 'tsc'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** npm installed beside the running node (Homebrew, nvm, system installs), else from PATH. */
|
|
107
|
+
function npmBinary() {
|
|
108
|
+
const beside = path.join(path.dirname(process.execPath), process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
109
|
+
return fs.existsSync(beside) ? beside : 'npm';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Run the frontend's own `build` script into `outDir`. npm appends the
|
|
114
|
+
* arguments after `--` to the script, so they reach `vite build`.
|
|
115
|
+
* @returns {Promise<{ code: number|null, output: string, timedOut: boolean }>}
|
|
116
|
+
*/
|
|
117
|
+
export function runFrontendBuild({ frontendDir, outDir, timeoutMs = BUILD_TIMEOUT_MS }) {
|
|
118
|
+
return new Promise(resolve => {
|
|
119
|
+
let output = '';
|
|
120
|
+
let timedOut = false;
|
|
121
|
+
const keep = chunk => { output = (output + chunk).slice(-OUTPUT_TAIL_BYTES); };
|
|
122
|
+
const env = { ...process.env, PATH: [path.dirname(process.execPath), process.env.PATH].filter(Boolean).join(path.delimiter) };
|
|
123
|
+
let child;
|
|
124
|
+
try {
|
|
125
|
+
child = spawn(npmBinary(), ['run', 'build', '--', '--outDir', outDir, '--emptyOutDir'], {
|
|
126
|
+
cwd: frontendDir, env, stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32', detached: process.platform !== 'win32',
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
resolve({ code: null, output: error.message, timedOut: false });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
child.stdout.on('data', keep);
|
|
133
|
+
child.stderr.on('data', keep);
|
|
134
|
+
const timer = setTimeout(() => {
|
|
135
|
+
timedOut = true;
|
|
136
|
+
// The build runs in its own process group: end tsc and vite with npm.
|
|
137
|
+
try { process.kill(-child.pid, 'SIGTERM'); } catch { child.kill('SIGTERM'); }
|
|
138
|
+
}, timeoutMs);
|
|
139
|
+
timer.unref?.();
|
|
140
|
+
child.on('error', error => { clearTimeout(timer); resolve({ code: null, output: `${output}\n${error.message}`.trim(), timedOut }); });
|
|
141
|
+
child.on('close', code => { clearTimeout(timer); resolve({ code, output, timedOut }); });
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function processAlive(pid) {
|
|
146
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
147
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Hold the build lock, or report who holds it. A lock left by a process that
|
|
152
|
+
* no longer exists is reclaimed once.
|
|
153
|
+
*/
|
|
154
|
+
function acquireLock(file) {
|
|
155
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
156
|
+
try {
|
|
157
|
+
const fd = fs.openSync(file, 'wx', 0o600);
|
|
158
|
+
fs.writeSync(fd, `${process.pid}\n`);
|
|
159
|
+
fs.closeSync(fd);
|
|
160
|
+
return { held: true, release: () => { try { if (Number(fs.readFileSync(file, 'utf8').trim()) === process.pid) fs.unlinkSync(file); } catch { /* already gone */ } } };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error.code !== 'EEXIST') throw error;
|
|
163
|
+
const owner = Number(String(fs.readFileSync(file, 'utf8')).trim());
|
|
164
|
+
if (processAlive(owner)) return { held: false, owner };
|
|
165
|
+
fs.rmSync(file, { force: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { held: false, owner: null };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Build the dashboard beside the served one and swap it in. The served bundle
|
|
173
|
+
* stays in place until the new one is complete; a failed build leaves it
|
|
174
|
+
* untouched.
|
|
175
|
+
* @returns {Promise<{ ok: boolean, status: string, reason?: string, durationMs?: number, output?: string }>}
|
|
176
|
+
*/
|
|
177
|
+
export async function rebuildDashboard({ root, run = runFrontendBuild, timeoutMs = BUILD_TIMEOUT_MS } = {}) {
|
|
178
|
+
const frontendDir = path.join(root, 'frontend');
|
|
179
|
+
const dist = path.join(frontendDir, 'dist');
|
|
180
|
+
const next = path.join(frontendDir, 'dist.next');
|
|
181
|
+
const prev = path.join(frontendDir, 'dist.prev');
|
|
182
|
+
const lock = acquireLock(path.join(frontendDir, '.dist-build.lock'));
|
|
183
|
+
if (!lock.held) return { ok: false, status: 'building-elsewhere', reason: `another process (pid ${lock.owner ?? 'unknown'}) is building the dashboard` };
|
|
184
|
+
const started = Date.now();
|
|
185
|
+
try {
|
|
186
|
+
fs.rmSync(next, { recursive: true, force: true });
|
|
187
|
+
const result = await run({ frontendDir, outDir: next, timeoutMs });
|
|
188
|
+
const built = result.code === 0 && !result.timedOut && fs.existsSync(path.join(next, 'index.html'));
|
|
189
|
+
if (!built) {
|
|
190
|
+
fs.rmSync(next, { recursive: true, force: true });
|
|
191
|
+
const reason = result.timedOut ? `build timed out after ${Math.round(timeoutMs / 1000)} s`
|
|
192
|
+
: result.code === 0 ? 'build finished without index.html' : `build exited ${result.code ?? 'without a status'}`;
|
|
193
|
+
return { ok: false, status: 'failed', reason, durationMs: Date.now() - started, output: result.output };
|
|
194
|
+
}
|
|
195
|
+
// Two renames: the served directory is absent only between them.
|
|
196
|
+
fs.rmSync(prev, { recursive: true, force: true });
|
|
197
|
+
const hadDist = fs.existsSync(dist);
|
|
198
|
+
if (hadDist) fs.renameSync(dist, prev);
|
|
199
|
+
try {
|
|
200
|
+
fs.renameSync(next, dist);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (hadDist) fs.renameSync(prev, dist);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
fs.rmSync(prev, { recursive: true, force: true });
|
|
206
|
+
return { ok: true, status: 'built', durationMs: Date.now() - started };
|
|
207
|
+
} finally {
|
|
208
|
+
lock.release();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check once and rebuild when needed. Returns the state to report and, when
|
|
214
|
+
* the sources are still being edited, how long to wait before checking again.
|
|
215
|
+
*/
|
|
216
|
+
export async function refreshDashboard({ root, now = Date.now, quietMs = QUIET_MS, rebuild = rebuildDashboard, toolsPresent = buildToolsPresent } = {}) {
|
|
217
|
+
const freshness = await checkDistFreshness(dashboardTargets(root));
|
|
218
|
+
if (freshness.ok) return { status: 'current', bundleBuiltAt: new Date(freshness.bundle.mtimeMs).toISOString() };
|
|
219
|
+
const state = freshness.reason === 'missing-bundle' ? 'missing' : 'stale';
|
|
220
|
+
if (!toolsPresent(root)) {
|
|
221
|
+
return { status: state, reason: `the build tools are not installed; run npm ci in ${root}`, source: freshness.source?.path ?? null };
|
|
222
|
+
}
|
|
223
|
+
if (freshness.source && now() - freshness.source.mtimeMs < quietMs) {
|
|
224
|
+
return { status: state, reason: 'sources are still changing', source: freshness.source.path, retryInMs: quietMs - (now() - freshness.source.mtimeMs) + 1000 };
|
|
225
|
+
}
|
|
226
|
+
const build = await rebuild({ root });
|
|
227
|
+
if (build.ok) return { status: 'current', rebuilt: true, durationMs: build.durationMs, was: state };
|
|
228
|
+
return { status: state, reason: build.reason, build };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Keep a serving checkout's bundle current: check now, after the sources go
|
|
233
|
+
* quiet, and every `intervalMs`. `report` receives each state change.
|
|
234
|
+
* @returns {{ state: () => object, check: () => Promise<object>, stop: () => void }}
|
|
235
|
+
*/
|
|
236
|
+
export function watchDashboardBundle({ root, report = () => {}, intervalMs = CHECK_INTERVAL_MS, refresh = refreshDashboard } = {}) {
|
|
237
|
+
let current = { status: 'unchecked' };
|
|
238
|
+
let running = null;
|
|
239
|
+
let retry = null;
|
|
240
|
+
let lastKey = '';
|
|
241
|
+
const check = () => {
|
|
242
|
+
if (running) return running;
|
|
243
|
+
running = (async () => {
|
|
244
|
+
try {
|
|
245
|
+
current = { ...await refresh({ root }), checkedAt: new Date().toISOString() };
|
|
246
|
+
} catch (error) {
|
|
247
|
+
current = { status: 'error', reason: error.message, checkedAt: new Date().toISOString() };
|
|
248
|
+
}
|
|
249
|
+
const key = `${current.status}|${current.reason ?? ''}|${current.rebuilt ?? ''}`;
|
|
250
|
+
if (key !== lastKey || current.rebuilt) report(current);
|
|
251
|
+
lastKey = key;
|
|
252
|
+
clearTimeout(retry);
|
|
253
|
+
if (current.retryInMs) { retry = setTimeout(check, current.retryInMs); retry.unref?.(); }
|
|
254
|
+
return current;
|
|
255
|
+
})().finally(() => { running = null; });
|
|
256
|
+
return running;
|
|
257
|
+
};
|
|
258
|
+
const interval = setInterval(check, intervalMs);
|
|
259
|
+
interval.unref?.();
|
|
260
|
+
return { state: () => current, check, stop: () => { clearInterval(interval); clearTimeout(retry); } };
|
|
261
|
+
}
|
|
@@ -22,25 +22,29 @@ const RUNNING_PACKAGE_ROOT = (() => {
|
|
|
22
22
|
})();
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* is
|
|
25
|
+
* The tree matches the lockfile when every locked package is on disk at its locked version
|
|
26
|
+
* (optional packages may be absent on this platform; workspace links are not copies; an absent
|
|
27
|
+
* peer is npm's call). It reads each package's own package.json, never npm's record of the tree
|
|
28
|
+
* (node_modules/.package-lock.json): `npm install --package-lock-only`, which the brain updater
|
|
29
|
+
* runs, rewrites that record to the new versions without installing them. On 2026-11-12 21 linked
|
|
30
|
+
* plugins held @ssss/cli 0.14.4 under a record and lock that both said 0.14.5, so every check that
|
|
31
|
+
* trusted the record, and npm itself, reported the stale tree as current.
|
|
28
32
|
*/
|
|
29
33
|
export function npmTreeMatchesLock(root) {
|
|
30
|
-
let lock
|
|
31
|
-
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} catch { return false; }
|
|
35
|
-
if (!lock || !installed) return false;
|
|
36
|
-
for (const [key, entry] of Object.entries(installed)) {
|
|
37
|
-
if (!key || entry.link) continue;
|
|
38
|
-
if (!lock[key] || lock[key].version !== entry.version) return false;
|
|
39
|
-
}
|
|
34
|
+
let lock;
|
|
35
|
+
try { lock = JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf8')).packages; }
|
|
36
|
+
catch { return false; }
|
|
37
|
+
if (!lock || !fs.existsSync(path.join(root, 'node_modules'))) return false;
|
|
40
38
|
for (const [key, entry] of Object.entries(lock)) {
|
|
41
39
|
if (!key.startsWith('node_modules/') && !key.includes('/node_modules/')) continue;
|
|
42
|
-
if (entry.optional || entry.link
|
|
43
|
-
|
|
40
|
+
if (entry.optional || entry.link) continue;
|
|
41
|
+
let version;
|
|
42
|
+
try { version = JSON.parse(fs.readFileSync(path.join(root, key, 'package.json'), 'utf8')).version; }
|
|
43
|
+
catch {
|
|
44
|
+
if (entry.peer) continue;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (entry.version && version !== entry.version) return false;
|
|
44
48
|
}
|
|
45
49
|
return true;
|
|
46
50
|
}
|
|
@@ -5,6 +5,7 @@ import { writeFileSecure } from './secure-file.mjs';
|
|
|
5
5
|
import { ensureProjectId } from './project-id.mjs';
|
|
6
6
|
|
|
7
7
|
function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); }
|
|
8
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
8
9
|
|
|
9
10
|
export const VAULT_CATEGORIES = Object.freeze([
|
|
10
11
|
'invariants',
|
|
@@ -18,17 +19,33 @@ export const VAULT_CATEGORIES = Object.freeze([
|
|
|
18
19
|
'lore',
|
|
19
20
|
]);
|
|
20
21
|
|
|
22
|
+
function readBrainConfig(file) {
|
|
23
|
+
let current;
|
|
24
|
+
try { current = JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === 'ENOENT') return {};
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
30
|
+
throw new Error('Existing brain identity must be an object');
|
|
31
|
+
}
|
|
32
|
+
return current;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Update connection settings without replacing the existing brain identity. */
|
|
36
|
+
export function writeBrainConnection(brainDir, fields) {
|
|
37
|
+
const file = path.join(brainDir, 'config', 'brain.json');
|
|
38
|
+
const current = readBrainConfig(file);
|
|
39
|
+
ensureDir(path.dirname(file));
|
|
40
|
+
writeFileSecure(file, JSON.stringify({ ...current, ...fields }, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
41
|
+
return file;
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
export function writeBrainIdentity(brainDir, { name, role = 'project', tags = [] } = {}) {
|
|
22
45
|
const configDir = path.join(brainDir, 'config');
|
|
23
46
|
ensureDir(configDir);
|
|
24
47
|
const brainJsonPath = path.join(configDir, 'brain.json');
|
|
25
|
-
|
|
26
|
-
if (fs.existsSync(brainJsonPath)) {
|
|
27
|
-
current = JSON.parse(fs.readFileSync(brainJsonPath, 'utf8'));
|
|
28
|
-
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
29
|
-
throw new Error('Existing brain identity must be an object');
|
|
30
|
-
}
|
|
31
|
-
}
|
|
48
|
+
const current = readBrainConfig(brainJsonPath);
|
|
32
49
|
const next = {
|
|
33
50
|
...current,
|
|
34
51
|
name: name || current.name || path.basename(path.resolve(brainDir, '../../..')),
|
|
@@ -54,12 +71,17 @@ export function registerProjectBrain(globalBrainDir, entry) {
|
|
|
54
71
|
|
|
55
72
|
const abs = path.resolve(entry.path);
|
|
56
73
|
const idx = list.findIndex((p) => path.resolve(p.path || '') === abs);
|
|
74
|
+
const existing = list[idx];
|
|
75
|
+
const candidateId = existing?.project_id;
|
|
76
|
+
const fallbackId = typeof candidateId === 'string' && UUID.test(candidateId) &&
|
|
77
|
+
existing.brainDir === entry.brainDir && list.filter(record => record.project_id === candidateId).length === 1
|
|
78
|
+
? candidateId : undefined;
|
|
57
79
|
const now = new Date().toISOString();
|
|
58
80
|
const row = {
|
|
59
81
|
name: entry.name,
|
|
60
82
|
path: abs,
|
|
61
83
|
brainDir: entry.brainDir,
|
|
62
|
-
project_id: ensureProjectId(entry.brainDir).project_id,
|
|
84
|
+
project_id: ensureProjectId(entry.brainDir, { fallbackId }).project_id,
|
|
63
85
|
full_brain: true,
|
|
64
86
|
layer: 'project',
|
|
65
87
|
tags: entry.tags || ['project-brain'],
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** Host activation policy; never pretends a provider or external daemon is healthy. */
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getPlugin, globalPluginsDir, projectPluginsDir, vaultForPluginsDir } from './plugin-loader.mjs';
|
|
4
|
+
import { patchPluginRecord, patchProjectActivationRecord, recordPath, emit, vaultFor, recompileAfterPluginMutation } from './plugin-store.mjs';
|
|
5
|
+
import { patchVfsDocument } from './ssss-operation-service.mjs';
|
|
6
|
+
import { pluginActivation, readActivationRecord, usesSharedPluginPolicy } from './plugin-state.mjs';
|
|
7
|
+
import { moveRepositoryPluginState, withRepositoryPluginLifecycle } from './plugin-lifecycle.mjs';
|
|
5
8
|
import { REQUIRED_INSTALLATION_PLUGIN_IDS } from './default-plugins.mjs';
|
|
6
9
|
export { pluginActivation };
|
|
7
10
|
|
|
@@ -14,38 +17,78 @@ export { pluginActivation };
|
|
|
14
17
|
* following the shared setting (`clear: true`).
|
|
15
18
|
*/
|
|
16
19
|
export async function setPluginActive(id, enabled, { projectRoot = process.cwd(), scope = 'project', clear = false } = {}) {
|
|
17
|
-
|
|
20
|
+
const onUse = enabled === 'on-use';
|
|
21
|
+
if (onUse && (scope !== 'project' || !projectRoot || projectPluginsDir(projectRoot) === globalPluginsDir() || clear)) {
|
|
22
|
+
throw new Error('On use is available for individual repository brains.');
|
|
23
|
+
}
|
|
24
|
+
if ((enabled === false || onUse) && REQUIRED_INSTALLATION_PLUGIN_IDS.includes(id)) {
|
|
18
25
|
throw new Error('Required plugin ' + id + ' cannot be disabled');
|
|
19
26
|
}
|
|
20
27
|
if (clear === true) {
|
|
21
28
|
const plugin = getPlugin(id, projectRoot);
|
|
22
29
|
if (!plugin?.valid) throw new Error(`Plugin ${id} is missing or invalid`);
|
|
23
|
-
await
|
|
24
|
-
|
|
30
|
+
await withRepositoryPluginLifecycle(projectRoot, id, async brain => {
|
|
31
|
+
if (!usesSharedPluginPolicy(plugin) || pluginActivation(plugin, { projectRoot: null }).enabled) {
|
|
32
|
+
moveRepositoryPluginState(brain, id, 'restore');
|
|
33
|
+
}
|
|
34
|
+
await patchProjectActivationRecord(id, projectRoot, null);
|
|
35
|
+
await emit(vaultForPluginsDir(projectPluginsDir(projectRoot)), id, 'activation-cleared', {});
|
|
36
|
+
});
|
|
25
37
|
const compilation = await recompileAfterPluginMutation(projectRoot, false);
|
|
26
38
|
return { pluginId: id, activation: pluginActivation(plugin, { projectRoot }), scope: 'shared', compilation };
|
|
27
39
|
}
|
|
28
|
-
if (
|
|
40
|
+
if (onUse) enabled = false;
|
|
41
|
+
if (typeof enabled !== 'boolean') throw new Error('Plugin activation requires a boolean or On use');
|
|
29
42
|
const plugin = getPlugin(id, projectRoot);
|
|
30
43
|
if (!plugin?.valid) throw new Error(`Plugin ${id} is missing or invalid`);
|
|
31
44
|
if (scope === 'shared') {
|
|
32
|
-
if (plugin
|
|
33
|
-
await patchPluginRecord(plugin, { runtime_enabled: enabled });
|
|
45
|
+
if (!usesSharedPluginPolicy(plugin)) throw new Error(`Plugin ${id} is a ${plugin.scope} copy without a shared setting to change`);
|
|
46
|
+
await patchPluginRecord(plugin, { runtime_enabled: enabled, runtime_on_use: false });
|
|
34
47
|
await emit(vaultFor(plugin), id, enabled ? 'activated' : 'deactivated', { enabled, scope: 'shared' });
|
|
35
48
|
const compilation = await recompileAfterPluginMutation(projectRoot, true);
|
|
36
49
|
return { pluginId: id, activation: pluginActivation(plugin, { projectRoot }), scope: 'shared', compilation };
|
|
37
50
|
}
|
|
38
51
|
// Repository scope. A repository-owned copy already is that repository's setting.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
const event = onUse ? 'activation-on-use' : enabled ? 'activated' : 'deactivated';
|
|
53
|
+
const shared = usesSharedPluginPolicy(plugin);
|
|
54
|
+
const resultScope = shared ? 'project' : plugin.scope;
|
|
55
|
+
const vaultRoot = shared ? vaultForPluginsDir(projectPluginsDir(projectRoot)) : vaultFor(plugin);
|
|
56
|
+
const cleanup = await withRepositoryPluginLifecycle(projectRoot, id, async brain => {
|
|
57
|
+
if (onUse && !brain) throw new Error('On use is available for individual repository brains.');
|
|
58
|
+
let result = enabled ? moveRepositoryPluginState(brain, id, 'restore') : undefined;
|
|
59
|
+
if (shared) await patchProjectActivationRecord(id, projectRoot, enabled, { manifest: plugin.manifest, onUse });
|
|
60
|
+
else await patchPluginRecord(plugin, { runtime_enabled: enabled, runtime_on_use: onUse });
|
|
61
|
+
if (!enabled && !onUse) {
|
|
62
|
+
try { result = moveRepositoryPluginState(brain, id, 'archive'); }
|
|
63
|
+
catch (error) {
|
|
64
|
+
result = { ok: false, state: 'pending', error: error.code ? `Could not archive repository plugin state (${error.code}).` : error.message };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
await emit(vaultRoot, id, event, { enabled, scope: resultScope, ...(result ? { cleanup: result.state } : {}) });
|
|
68
|
+
return result;
|
|
69
|
+
});
|
|
70
|
+
const compilation = await recompileAfterPluginMutation(projectRoot, false);
|
|
71
|
+
return { pluginId: id, activation: pluginActivation(plugin, { projectRoot }), scope: resultScope, compilation, ...(cleanup ? { cleanup } : {}) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Claim one armed project policy through the SSSS compare-and-set writer. */
|
|
75
|
+
export async function activatePluginOnUse(id, { projectRoot = process.cwd() } = {}) {
|
|
76
|
+
if (!projectRoot || projectPluginsDir(projectRoot) === globalPluginsDir()) return null;
|
|
77
|
+
const plugin = getPlugin(id, projectRoot);
|
|
78
|
+
if (!plugin?.valid || pluginActivation(plugin, { projectRoot }).mode !== 'on-use') return null;
|
|
79
|
+
const vaultRoot = usesSharedPluginPolicy(plugin) ? vaultForPluginsDir(projectPluginsDir(projectRoot)) : vaultFor(plugin);
|
|
80
|
+
const cleanup = await withRepositoryPluginLifecycle(projectRoot, id, async brain => {
|
|
81
|
+
if (!brain) return null;
|
|
82
|
+
const current = readActivationRecord(path.join(vaultRoot, recordPath(id)), id, { withHash: true });
|
|
83
|
+
if (current?.record.runtime_on_use !== true) return null;
|
|
84
|
+
const result = moveRepositoryPluginState(brain, id, 'restore');
|
|
85
|
+
await patchVfsDocument(recordPath(id), { runtime_enabled: true, runtime_on_use: false, timestamp: new Date().toISOString() }, {
|
|
86
|
+
vaultRoot, expectedBeforeHash: current.hash, intent: 'Activate plugin on deliberate use',
|
|
87
|
+
});
|
|
88
|
+
await emit(vaultRoot, id, 'activated-on-use', { enabled: true, scope: 'project', cleanup: result.state });
|
|
89
|
+
return result;
|
|
90
|
+
});
|
|
91
|
+
if (!cleanup) return null;
|
|
49
92
|
const compilation = await recompileAfterPluginMutation(projectRoot, false);
|
|
50
|
-
return { pluginId: id, activation: pluginActivation(plugin, { projectRoot }), scope: 'project', compilation };
|
|
93
|
+
return { pluginId: id, activation: pluginActivation(plugin, { projectRoot }), scope: 'project', compilation, cleanup };
|
|
51
94
|
}
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Functional card contract. A plugin's card face is its own bespoke Web
|
|
3
|
-
* Component, declared as a `ui.elements[]` entry of kind `card`. There is no
|
|
4
|
-
* template: no shared layout, no widget vocabulary, no allowlist of shapes.
|
|
5
|
-
* Core validates that the face exists and that the commands behind it are
|
|
6
|
-
* declared; it never dictates how the face is arranged.
|
|
7
|
-
*
|
|
8
|
-
* A plugin carries exactly one face and AS MANY secondary screens as it needs,
|
|
9
|
-
* each declared `kind: 'panel'`. Screens are the plugin's own design: it names
|
|
10
|
-
* them, orders them and decides what is on them. The host imposes no count and
|
|
11
|
-
* no screen set, and never derives a screen from the command list.
|
|
12
|
-
*/
|
|
1
|
+
/** Cross-field validation for plugin manifests and their command contracts. */
|
|
13
2
|
const plain = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
|
14
3
|
const COMMAND = /^[a-z][a-z0-9-]{0,63}$/;
|
|
15
4
|
const ARG_NAME = /^[a-z][a-zA-Z0-9]{0,63}$/;
|
|
@@ -19,11 +8,10 @@ export const COMMAND_EFFECTS = ['read', 'write', 'destructive', 'send'];
|
|
|
19
8
|
export const OUTPUT_KINDS = ['json', 'text'];
|
|
20
9
|
export const TIERS = ['core', 'official', 'personal', 'community'];
|
|
21
10
|
export const LICENSE_CLASSES = ['open', 'closed'];
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export const CARD_HEADERS = ['plain', 'ledger-rule', 'band', 'tab-strip', 'badge-left'];
|
|
11
|
+
const BRAND_FONTS = ['system', 'inter', 'ibm-plex', 'space-grotesk', 'jetbrains-mono', 'dm-sans', 'outfit', 'fraunces', 'newsreader', 'chivo-mono'];
|
|
12
|
+
const BRAND_DENSITIES = ['compact', 'comfortable', 'spacious'];
|
|
13
|
+
const BRAND_RADII = ['none', 'sm', 'md', 'lg', 'pill'];
|
|
14
|
+
const BRAND_HEADERS = ['plain', 'ledger-rule', 'band', 'tab-strip', 'badge-left'];
|
|
27
15
|
export const WORKSPACE_PERMISSIONS = ['microphone', 'camera', 'notifications'];
|
|
28
16
|
const HEX = /^#[0-9a-fA-F]{6}$/;
|
|
29
17
|
|
|
@@ -41,7 +29,7 @@ function validateArg(arg, where, errors) {
|
|
|
41
29
|
if (arg.secret === true && arg.type !== 'string') errors.push(`${where}: a secret stdin argument must be a string`);
|
|
42
30
|
}
|
|
43
31
|
|
|
44
|
-
export function
|
|
32
|
+
export function validateManifestContract(manifest) {
|
|
45
33
|
const errors = [];
|
|
46
34
|
const warnings = [];
|
|
47
35
|
const subs = manifest.cli?.subcommands;
|
|
@@ -55,6 +43,13 @@ export function validateCardContract(manifest) {
|
|
|
55
43
|
else names.add(sub.name);
|
|
56
44
|
if (sub.effect !== undefined && !COMMAND_EFFECTS.includes(sub.effect)) errors.push(`${where}.effect must be one of: ${COMMAND_EFFECTS.join(', ')}`);
|
|
57
45
|
if (sub.output !== undefined && !OUTPUT_KINDS.includes(sub.output)) errors.push(`${where}.output must be one of: ${OUTPUT_KINDS.join(', ')}`);
|
|
46
|
+
const terminalFlags = sub.terminal_only_args;
|
|
47
|
+
if (sub.terminal_only !== undefined && typeof sub.terminal_only !== 'boolean') errors.push(`${where}.terminal_only must be a boolean`);
|
|
48
|
+
if (terminalFlags !== undefined && (!Array.isArray(terminalFlags) ||
|
|
49
|
+
terminalFlags.some(flag => typeof flag !== 'string' || !FLAG.test(flag)) ||
|
|
50
|
+
new Set(terminalFlags).size !== terminalFlags.length)) {
|
|
51
|
+
errors.push(`${where}.terminal_only_args must contain unique long flags`);
|
|
52
|
+
}
|
|
58
53
|
// The runner refuses anything outside this range, so a manifest that
|
|
59
54
|
// declares it must fail validation rather than every call at run time.
|
|
60
55
|
if (sub.timeout_ms !== undefined && (!Number.isInteger(sub.timeout_ms) || sub.timeout_ms < 1 || sub.timeout_ms > 600000)) errors.push(`${where}.timeout_ms must be an integer from 1 to 600000`);
|
|
@@ -91,7 +86,7 @@ export function validateCardContract(manifest) {
|
|
|
91
86
|
if (!LICENSE_CLASSES.includes(manifest.distribution.license)) errors.push(`'distribution.license' must be one of: ${LICENSE_CLASSES.join(', ')}`);
|
|
92
87
|
}
|
|
93
88
|
}
|
|
94
|
-
|
|
89
|
+
validatePresentation(manifest, errors, warnings);
|
|
95
90
|
validateBranding(manifest.branding, errors, warnings);
|
|
96
91
|
validateWorkspace(manifest.workspace, errors);
|
|
97
92
|
if (Array.isArray(manifest.secrets)) {
|
|
@@ -99,30 +94,15 @@ export function validateCardContract(manifest) {
|
|
|
99
94
|
if (!plain(s) || typeof s.key !== 'string' || !/^[A-Z][A-Z0-9_]{0,63}$/.test(s.key)) errors.push(`secrets[${i}].key must be an uppercase identifier`);
|
|
100
95
|
});
|
|
101
96
|
}
|
|
102
|
-
|
|
103
|
-
warnings.push("card_face.metrics are fixed text and cannot reflect real state; declare a 'status' command instead");
|
|
104
|
-
}
|
|
105
|
-
return { errors, warnings, states: STATUS_STATES };
|
|
97
|
+
return { errors, warnings };
|
|
106
98
|
}
|
|
107
99
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
* a shared template is invalid.
|
|
112
|
-
*
|
|
113
|
-
* The *scaffold* emitted by `plugin create` deliberately contains no card: a
|
|
114
|
-
* face is authored by hand once the plugin's function is understood. A scaffold
|
|
115
|
-
* declares `scaffold: true`, so it installs while the author works and reports
|
|
116
|
-
* its missing face as a warning rather than an error. Any other plugin without
|
|
117
|
-
* a face is an error, because nothing else explains why it is missing.
|
|
118
|
-
*/
|
|
119
|
-
function validateCardFace(manifest, errors, warnings) {
|
|
120
|
-
if (manifest.card_layout !== undefined) {
|
|
121
|
-
errors.push("'card_layout' is not supported: a card face is bespoke owner code in ui.elements[], never a shared template");
|
|
100
|
+
function validatePresentation(manifest, errors, warnings) {
|
|
101
|
+
for (const field of ['card', 'card_face', 'card_layout']) {
|
|
102
|
+
if (manifest[field] !== undefined) errors.push(`'${field}' is not supported: declare a native ui.home instead`);
|
|
122
103
|
}
|
|
123
104
|
const elements = manifest.ui?.elements;
|
|
124
|
-
// A brick is distributed as capability code alone
|
|
125
|
-
// has no face; every other plugin still requires exactly one.
|
|
105
|
+
// A brick is distributed as capability code alone and has no UI.
|
|
126
106
|
if (manifest.brick === true) {
|
|
127
107
|
validateCapability(manifest, errors);
|
|
128
108
|
if (manifest.ui !== undefined && Array.isArray(elements) && elements.length > 0) {
|
|
@@ -131,23 +111,9 @@ function validateCardFace(manifest, errors, warnings) {
|
|
|
131
111
|
return;
|
|
132
112
|
}
|
|
133
113
|
if (manifest.capability !== undefined) validateCapability(manifest, errors);
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// shippable plugin, so the missing face is reported and stays visible.
|
|
138
|
-
if (manifest.scaffold === true) {
|
|
139
|
-
warnings.push("'ui.elements' declares no card face yet: author ui/card.js and ui/DESIGN.md by hand for this plugin's own function, then declare the face in ui.elements[]");
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
errors.push("'ui.elements' must declare the plugin's card face; every plugin has one, authored by hand");
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
// One face, and as many secondary screens as the plugin needs. Cardinality is
|
|
146
|
-
// checked for `card` alone, so panels may be declared in any number and on
|
|
147
|
-
// either side of the face.
|
|
148
|
-
const faces = elements.filter((element) => plain(element) && element.kind === 'card');
|
|
149
|
-
if (faces.length === 0) errors.push("'ui.elements' must declare an element of kind 'card'");
|
|
150
|
-
if (faces.length > 1) errors.push("'ui.elements' must declare exactly one card face; further screens use kind 'panel'");
|
|
114
|
+
if (manifest.ui?.home !== undefined) return;
|
|
115
|
+
if (manifest.scaffold === true) warnings.push("Scaffold needs an owner-authored 'ui.home' and DESIGN.md before shipping");
|
|
116
|
+
else errors.push("A plugin must declare a native 'ui.home'");
|
|
151
117
|
}
|
|
152
118
|
|
|
153
119
|
/**
|
|
@@ -188,7 +154,7 @@ function validateBranding(branding, errors, warnings) {
|
|
|
188
154
|
if (branding.icon !== undefined) {
|
|
189
155
|
warnings.push('branding.icon is deprecated: show the real logo with branding.logo instead of an emoji or generic icon');
|
|
190
156
|
}
|
|
191
|
-
const enums = { font:
|
|
157
|
+
const enums = { font: BRAND_FONTS, density: BRAND_DENSITIES, radius: BRAND_RADII, header: BRAND_HEADERS };
|
|
192
158
|
for (const [key, allowed] of Object.entries(enums)) {
|
|
193
159
|
const value = branding[key];
|
|
194
160
|
if (value === undefined || allowed.includes(value)) continue;
|