@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.88
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/CHANGELOG.md +132 -0
- package/LICENSE +1 -1
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/deploy.js +40 -40
- package/dist/commands/flatten.js +191 -0
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +42 -27
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/agents/adaptive-router.js +330 -0
- package/dist/core/agents/query-decomposer.js +297 -0
- package/dist/core/agents/registry.js +3 -3
- package/dist/core/approvals/shortcut-resolver.js +98 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/ask-user/question.js +92 -0
- package/dist/core/audit/audit-trail.js +275 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-open-browser.js +4 -4
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash/redirect.js +281 -0
- package/dist/core/bash-classifier.js +436 -40
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/checkpoints/shadow-git.js +670 -0
- package/dist/core/citations/parser.js +109 -0
- package/dist/core/classifier/yolo-classifier.js +88 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/anvil-fanout.js +25 -25
- package/dist/core/consensus/diff-capture.js +121 -12
- package/dist/core/consensus/rubric.js +21 -21
- package/dist/core/context/builder.js +6 -6
- package/dist/core/context/compaction-events.js +8 -8
- package/dist/core/context/compaction.js +31 -31
- package/dist/core/context/index.js +15 -8
- package/dist/core/context/invariants.js +51 -51
- package/dist/core/context/markdown-loader.js +28 -10
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/context/pugiignore.js +41 -41
- package/dist/core/context/repo-skeleton.js +37 -37
- package/dist/core/context/tool-eviction.js +55 -0
- package/dist/core/context/watcher.js +32 -32
- package/dist/core/context/working-set.js +23 -23
- package/dist/core/coordinator/agent-tools.js +77 -0
- package/dist/core/coordinator/agent-toolset.js +65 -0
- package/dist/core/coordinator/fsm.js +73 -0
- package/dist/core/coordinator/mode-fsm.js +70 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/credentials.js +12 -12
- package/dist/core/cron/scheduler.js +138 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +93 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/engine-live.js +46 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/apply-patch-layer-e.js +189 -0
- package/dist/core/edits/dispatch.js +293 -7
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +3 -1
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-a-apply.js +15 -15
- package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
- package/dist/core/edits/layer-b-apply.js +9 -9
- package/dist/core/edits/layer-c-apply.js +6 -6
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/marker-parser.js +12 -12
- package/dist/core/edits/security-gate.js +27 -27
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +151 -26
- package/dist/core/engine/auto-compact.js +179 -0
- package/dist/core/engine/budgets.js +186 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/intensity.js +158 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +1295 -227
- package/dist/core/engine/prompts.js +134 -16
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1295 -59
- package/dist/core/evaluation/golden-dataset.js +293 -0
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/flatten/flatten-repo.js +439 -0
- package/dist/core/format/osc8-link.js +28 -0
- package/dist/core/hook-chains.js +392 -0
- package/dist/core/hooks/citation-verify-hook.js +138 -0
- package/dist/core/hooks/citation-verify.js +112 -0
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/image/renderer.js +71 -0
- package/dist/core/init/detector.js +582 -0
- package/dist/core/init/template-renderer.js +242 -0
- package/dist/core/jobs/registry.js +18 -18
- package/dist/core/ledger/results-tsv.js +142 -0
- package/dist/core/log-discipline/stdout-redirect.js +51 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/symbol-tools.js +372 -0
- package/dist/core/mcp/client.js +97 -28
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +39 -17
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/mcp/trust.js +10 -10
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/passive-extract.js +130 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory/secret-scanner.js +304 -0
- package/dist/core/memory-sync/queue.js +170 -0
- package/dist/core/metrics/extract.js +113 -0
- package/dist/core/modes/roo-modes.js +68 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +287 -5
- package/dist/core/permission.js +82 -22
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/bash-parser.js +371 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/constrained-edit.js +91 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/network-egress.js +137 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/plan-mode/ui-state.js +51 -0
- package/dist/core/plans/plan-artifact.js +721 -0
- package/dist/core/policy-limits/etag-store.js +122 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/prompt-cache/client-cache.js +99 -0
- package/dist/core/prompts/assembly.js +29 -0
- package/dist/core/prompts/registry.js +364 -0
- package/dist/core/pugi-md/cc-compat-rules.js +735 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/python/uv-installer.js +270 -0
- package/dist/core/python/uv-resolver.js +83 -0
- package/dist/core/rate-limit/narrator.js +146 -0
- package/dist/core/recipes/cli-types.js +20 -0
- package/dist/core/recipes/loader.js +103 -0
- package/dist/core/recipes/runner.js +345 -0
- package/dist/core/recipes/schema.js +587 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/ask.js +37 -37
- package/dist/core/repl/cancellation.js +26 -26
- package/dist/core/repl/cap-warning.js +4 -4
- package/dist/core/repl/clipboard-read.js +11 -11
- package/dist/core/repl/dispatch-fsm.js +12 -12
- package/dist/core/repl/history-search.js +15 -15
- package/dist/core/repl/history.js +28 -18
- package/dist/core/repl/kill-ring.js +5 -5
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/privacy-banner.js +22 -22
- package/dist/core/repl/session.js +2157 -214
- package/dist/core/repl/slash-commands.js +533 -40
- package/dist/core/repl/store/index.js +1 -1
- package/dist/core/repl/store/jsonl-log.js +22 -22
- package/dist/core/repl/store/lockfile.js +10 -10
- package/dist/core/repl/store/session-store.js +136 -107
- package/dist/core/repl/store/types.js +15 -15
- package/dist/core/repl/store/uuid-v7.js +12 -12
- package/dist/core/repl/workspace-context.js +43 -21
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/page-rank.js +105 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/retry-budget/retry-cap.js +74 -0
- package/dist/core/routing/lead-worker.js +43 -0
- package/dist/core/routing/pre-flight-estimator.js +108 -0
- package/dist/core/runs/run-tree.js +103 -0
- package/dist/core/security/injection-scanner.js +367 -0
- package/dist/core/security/output-filter.js +418 -0
- package/dist/core/session/env-file.js +105 -0
- package/dist/core/session/section-budgets.js +140 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +298 -5
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/skills/loader.js +22 -22
- package/dist/core/skills/sources.js +27 -27
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/statusline.js +99 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +132 -43
- package/dist/core/subagents/index.js +19 -6
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/tool-schema/compressor.js +89 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/trust.js +2 -2
- package/dist/core/tui/thinking-block.js +64 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/watch-markers/marker-watcher.js +133 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +36 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4203 -493
- package/dist/runtime/commands/agents.js +30 -30
- package/dist/runtime/commands/budget.js +5 -5
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/config.js +73 -39
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +244 -13
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +579 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +582 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/privacy.js +17 -17
- package/dist/runtime/commands/recipe.js +325 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +68 -53
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/roster.js +14 -14
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/skills.js +31 -31
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +54 -22
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/sigint-guard.js +272 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -0
- package/dist/skills/bundled/batch.js +617 -0
- package/dist/skills/bundled/index.js +45 -0
- package/dist/skills/bundled/loop.js +358 -0
- package/dist/skills/bundled/remember.js +383 -0
- package/dist/skills/bundled/simplify.js +289 -0
- package/dist/skills/bundled/skillify.js +373 -0
- package/dist/skills/bundled/stuck.js +558 -0
- package/dist/skills/bundled/verify.js +439 -0
- package/dist/testing/vcr.js +486 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +288 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +624 -46
- package/dist/tools/brief.js +224 -0
- package/dist/tools/enter-worktree.js +250 -0
- package/dist/tools/exit-worktree.js +147 -0
- package/dist/tools/file-tools.js +161 -44
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +85 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/sleep.js +99 -0
- package/dist/tools/synthetic-output.js +133 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/verify-plan-execution.js +295 -0
- package/dist/tools/web-fetch-injection-scanner.js +207 -0
- package/dist/tools/web-fetch.js +195 -10
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +11 -1
- package/dist/tui/ask-modal.js +14 -14
- package/dist/tui/ask-user-question-chips.js +257 -0
- package/dist/tui/ask-user-question-prompt.js +203 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +85 -11
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/device-flow.js +2 -2
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +247 -32
- package/dist/tui/login-picker.js +3 -3
- package/dist/tui/markdown-render.js +6 -6
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +332 -54
- package/dist/tui/repl-splash-art.js +16 -16
- package/dist/tui/repl-splash-mascot.js +48 -24
- package/dist/tui/repl-splash.js +22 -22
- package/dist/tui/repl.js +124 -44
- package/dist/tui/slash-palette.js +6 -6
- package/dist/tui/splash.js +2 -2
- package/dist/tui/status-bar.js +109 -31
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +53 -4
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/dist/tui/workspace-context.js +2 -2
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +25 -7
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas + minimal YAML parser for recipes (backlog #62).
|
|
3
|
+
*
|
|
4
|
+
* The recipe surface is deliberately tiny — top-level scalars, a flow
|
|
5
|
+
* map of params, a block sequence of steps. Pulling in `js-yaml` for
|
|
6
|
+
* this would be ~70 KB of installed weight; the hand-rolled parser
|
|
7
|
+
* below covers every supported shape in ~150 lines and refuses
|
|
8
|
+
* anything richer so the operator's recipe stays inside the schema.
|
|
9
|
+
*
|
|
10
|
+
* Format spec:
|
|
11
|
+
*
|
|
12
|
+
* schema: 1
|
|
13
|
+
* name: deploy-staging
|
|
14
|
+
* description: Build, smoke-test, deploy to staging
|
|
15
|
+
* params:
|
|
16
|
+
* branch: { type: string, default: main, required: false }
|
|
17
|
+
* skipTests: { type: boolean, default: false }
|
|
18
|
+
* steps:
|
|
19
|
+
* - name: build
|
|
20
|
+
* run: pnpm build
|
|
21
|
+
* - name: test
|
|
22
|
+
* run: pnpm test
|
|
23
|
+
* when: "!params.skipTests"
|
|
24
|
+
* - name: deploy
|
|
25
|
+
* run: ./scripts/deploy.sh ${params.branch} staging
|
|
26
|
+
*/
|
|
27
|
+
import { z } from 'zod';
|
|
28
|
+
const RECIPE_ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
29
|
+
/**
|
|
30
|
+
* Zod schema for the parsed-but-not-yet-augmented recipe. Files are
|
|
31
|
+
* parsed by `parseRecipeYaml` first, then validated by this schema, then
|
|
32
|
+
* augmented with `id`, `source`, `origin` by the loader.
|
|
33
|
+
*
|
|
34
|
+
* The `passthrough` policy is `strict` — unknown keys raise a clear
|
|
35
|
+
* error so a typo cannot silently no-op the way YAML's permissive
|
|
36
|
+
* shape usually allows.
|
|
37
|
+
*/
|
|
38
|
+
export const recipeFileSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
schema: z.literal(1),
|
|
41
|
+
name: z.string().min(1).optional(),
|
|
42
|
+
description: z.string().min(1).optional(),
|
|
43
|
+
params: z
|
|
44
|
+
.record(z.string(), z
|
|
45
|
+
.object({
|
|
46
|
+
type: z.enum(['string', 'boolean', 'number']),
|
|
47
|
+
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
48
|
+
required: z.boolean().optional(),
|
|
49
|
+
description: z.string().optional(),
|
|
50
|
+
})
|
|
51
|
+
.strict())
|
|
52
|
+
.optional(),
|
|
53
|
+
steps: z
|
|
54
|
+
.array(z
|
|
55
|
+
.object({
|
|
56
|
+
name: z.string().min(1),
|
|
57
|
+
run: z.string().min(1),
|
|
58
|
+
when: z.string().min(1).optional(),
|
|
59
|
+
continueOnError: z.boolean().optional(),
|
|
60
|
+
})
|
|
61
|
+
.strict())
|
|
62
|
+
.min(1),
|
|
63
|
+
})
|
|
64
|
+
.strict();
|
|
65
|
+
/**
|
|
66
|
+
* Validate a recipe id derived from its filename. Recipe ids are
|
|
67
|
+
* the canonical key — the loader uses them for dedup and CLI args
|
|
68
|
+
* use them verbatim, so they need to be predictable.
|
|
69
|
+
*/
|
|
70
|
+
export function isValidRecipeId(id) {
|
|
71
|
+
return RECIPE_ID_RE.test(id);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse + validate a recipe file body. Returns the structured Recipe
|
|
75
|
+
* on success or a single-line operator-readable error on failure.
|
|
76
|
+
*/
|
|
77
|
+
export function parseRecipeFile(body, ctx) {
|
|
78
|
+
if (!isValidRecipeId(ctx.id)) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: `recipe id '${ctx.id}' must match [a-z][a-z0-9-]* (lowercase, kebab-case, leading letter)`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
let raw;
|
|
85
|
+
try {
|
|
86
|
+
raw = parseRecipeYaml(body);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
return { ok: false, error: `parse error: ${message}` };
|
|
91
|
+
}
|
|
92
|
+
const validated = recipeFileSchema.safeParse(raw);
|
|
93
|
+
if (!validated.success) {
|
|
94
|
+
const firstIssue = validated.error.issues[0];
|
|
95
|
+
const path = firstIssue?.path.join('.') || '<root>';
|
|
96
|
+
const message = firstIssue?.message ?? 'unknown schema error';
|
|
97
|
+
return { ok: false, error: `schema error at ${path}: ${message}` };
|
|
98
|
+
}
|
|
99
|
+
const recipe = augment(validated.data, ctx);
|
|
100
|
+
// Validate when-expressions at load time so a typo fails fast.
|
|
101
|
+
for (const step of recipe.steps) {
|
|
102
|
+
if (step.when !== undefined) {
|
|
103
|
+
const check = compileWhenExpression(step.when, recipe.params);
|
|
104
|
+
if (!check.ok) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: `step '${step.name}': when-clause invalid: ${check.error}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { ok: true, recipe };
|
|
113
|
+
}
|
|
114
|
+
function augment(data, ctx) {
|
|
115
|
+
const params = [];
|
|
116
|
+
const paramRecord = data.params ?? {};
|
|
117
|
+
for (const [name, spec] of Object.entries(paramRecord)) {
|
|
118
|
+
const param = {
|
|
119
|
+
name,
|
|
120
|
+
type: spec.type,
|
|
121
|
+
required: spec.required ?? false,
|
|
122
|
+
...(spec.default !== undefined ? { default: spec.default } : {}),
|
|
123
|
+
...(spec.description !== undefined ? { description: spec.description } : {}),
|
|
124
|
+
};
|
|
125
|
+
params.push(param);
|
|
126
|
+
}
|
|
127
|
+
const steps = data.steps.map((s) => ({
|
|
128
|
+
name: s.name,
|
|
129
|
+
run: s.run,
|
|
130
|
+
continueOnError: s.continueOnError ?? false,
|
|
131
|
+
...(s.when !== undefined ? { when: s.when } : {}),
|
|
132
|
+
}));
|
|
133
|
+
return {
|
|
134
|
+
schema: 1,
|
|
135
|
+
id: ctx.id,
|
|
136
|
+
name: data.name ?? ctx.id,
|
|
137
|
+
...(data.description !== undefined ? { description: data.description } : {}),
|
|
138
|
+
source: ctx.source,
|
|
139
|
+
origin: ctx.origin,
|
|
140
|
+
params,
|
|
141
|
+
steps,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Minimal YAML parser tuned for the recipe schema. Public for direct
|
|
146
|
+
* test coverage. Supports:
|
|
147
|
+
*
|
|
148
|
+
* - Top-level scalars: `key: value`.
|
|
149
|
+
* - Flow mapping: `key: { a: b, c: d }`.
|
|
150
|
+
* - Block sequence under `steps:` with `- key: value` continuations.
|
|
151
|
+
* - Block mapping under `params:` with nested flow mapping per param.
|
|
152
|
+
* - Quoted strings: single and double quotes.
|
|
153
|
+
* - Numbers and booleans: `42`, `true`, `false`.
|
|
154
|
+
* - Comments after `#` (only when preceded by whitespace, so `pnpm test #x` works).
|
|
155
|
+
*
|
|
156
|
+
* Anything else raises with a clear `line N` pointer.
|
|
157
|
+
*/
|
|
158
|
+
export function parseRecipeYaml(body) {
|
|
159
|
+
const lines = body.split('\n').map((line) => line.replace(/\r$/, ''));
|
|
160
|
+
const stripped = lines.map(stripComment);
|
|
161
|
+
const root = {};
|
|
162
|
+
let i = 0;
|
|
163
|
+
while (i < stripped.length) {
|
|
164
|
+
const line = stripped[i] ?? '';
|
|
165
|
+
if (line.trim() === '') {
|
|
166
|
+
i += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (/^\s/.test(line)) {
|
|
170
|
+
throw new Error(`unexpected indentation at line ${i + 1}: '${line}'`);
|
|
171
|
+
}
|
|
172
|
+
const match = /^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/.exec(line);
|
|
173
|
+
if (!match) {
|
|
174
|
+
throw new Error(`cannot parse top-level line ${i + 1}: '${line}'`);
|
|
175
|
+
}
|
|
176
|
+
const key = match[1];
|
|
177
|
+
const tail = (match[2] ?? '').trim();
|
|
178
|
+
if (tail === '') {
|
|
179
|
+
const block = collectIndentedBlock(stripped, i + 1);
|
|
180
|
+
i = block.nextIndex;
|
|
181
|
+
if (key === 'params') {
|
|
182
|
+
root[key] = parseParamsBlock(block.lines, block.startLine);
|
|
183
|
+
}
|
|
184
|
+
else if (key === 'steps') {
|
|
185
|
+
root[key] = parseStepsBlock(block.lines, block.startLine);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
throw new Error(`block value at line ${i + 1} is only supported for 'params' / 'steps' (got '${key}')`);
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
root[key] = parseScalar(tail, i + 1);
|
|
193
|
+
i += 1;
|
|
194
|
+
}
|
|
195
|
+
return root;
|
|
196
|
+
}
|
|
197
|
+
function stripComment(line) {
|
|
198
|
+
let inSingle = false;
|
|
199
|
+
let inDouble = false;
|
|
200
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
201
|
+
const ch = line[i];
|
|
202
|
+
if (ch === "'" && !inDouble)
|
|
203
|
+
inSingle = !inSingle;
|
|
204
|
+
else if (ch === '"' && !inSingle)
|
|
205
|
+
inDouble = !inDouble;
|
|
206
|
+
else if (ch === '#' && !inSingle && !inDouble) {
|
|
207
|
+
const prev = line[i - 1];
|
|
208
|
+
if (prev === undefined || /\s/.test(prev)) {
|
|
209
|
+
return line.slice(0, i).replace(/\s+$/, '');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return line;
|
|
214
|
+
}
|
|
215
|
+
function collectIndentedBlock(lines, start) {
|
|
216
|
+
const block = [];
|
|
217
|
+
let i = start;
|
|
218
|
+
while (i < lines.length) {
|
|
219
|
+
const line = lines[i] ?? '';
|
|
220
|
+
if (line.trim() === '') {
|
|
221
|
+
block.push(line);
|
|
222
|
+
i += 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!/^\s/.test(line))
|
|
226
|
+
break;
|
|
227
|
+
block.push(line);
|
|
228
|
+
i += 1;
|
|
229
|
+
}
|
|
230
|
+
return { lines: block, startLine: start, nextIndex: i };
|
|
231
|
+
}
|
|
232
|
+
function parseParamsBlock(block, startLine) {
|
|
233
|
+
const out = {};
|
|
234
|
+
for (let i = 0; i < block.length; i += 1) {
|
|
235
|
+
const line = block[i] ?? '';
|
|
236
|
+
const trimmed = line.trim();
|
|
237
|
+
if (trimmed === '')
|
|
238
|
+
continue;
|
|
239
|
+
const match = /^\s+([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/.exec(line);
|
|
240
|
+
if (!match) {
|
|
241
|
+
throw new Error(`params: cannot parse line ${startLine + i + 1}: '${line}'`);
|
|
242
|
+
}
|
|
243
|
+
const key = match[1];
|
|
244
|
+
const tail = (match[2] ?? '').trim();
|
|
245
|
+
if (tail === '' || !tail.startsWith('{')) {
|
|
246
|
+
throw new Error(`params: each entry must be a flow mapping at line ${startLine + i + 1}: '${line}'`);
|
|
247
|
+
}
|
|
248
|
+
out[key] = parseFlowMapping(tail, startLine + i + 1);
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
function parseStepsBlock(block, startLine) {
|
|
253
|
+
const steps = [];
|
|
254
|
+
let current = null;
|
|
255
|
+
for (let i = 0; i < block.length; i += 1) {
|
|
256
|
+
const line = block[i] ?? '';
|
|
257
|
+
if (line.trim() === '')
|
|
258
|
+
continue;
|
|
259
|
+
const dashMatch = /^(\s+)-\s+(.*)$/.exec(line);
|
|
260
|
+
if (dashMatch) {
|
|
261
|
+
if (current !== null)
|
|
262
|
+
steps.push(current);
|
|
263
|
+
current = {};
|
|
264
|
+
const tail = (dashMatch[2] ?? '').trim();
|
|
265
|
+
const fieldMatch = /^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/.exec(tail);
|
|
266
|
+
if (!fieldMatch) {
|
|
267
|
+
throw new Error(`steps: dash entry must start with a field at line ${startLine + i + 1}: '${line}'`);
|
|
268
|
+
}
|
|
269
|
+
current[fieldMatch[1]] = parseScalar((fieldMatch[2] ?? '').trim(), startLine + i + 1);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const fieldMatch = /^\s+([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/.exec(line);
|
|
273
|
+
if (fieldMatch && current !== null) {
|
|
274
|
+
current[fieldMatch[1]] = parseScalar((fieldMatch[2] ?? '').trim(), startLine + i + 1);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
throw new Error(`steps: cannot parse line ${startLine + i + 1}: '${line}'`);
|
|
278
|
+
}
|
|
279
|
+
if (current !== null)
|
|
280
|
+
steps.push(current);
|
|
281
|
+
return steps;
|
|
282
|
+
}
|
|
283
|
+
function parseFlowMapping(text, line) {
|
|
284
|
+
if (!text.startsWith('{') || !text.endsWith('}')) {
|
|
285
|
+
throw new Error(`flow mapping must start with '{' and end with '}' at line ${line}: '${text}'`);
|
|
286
|
+
}
|
|
287
|
+
const inner = text.slice(1, -1).trim();
|
|
288
|
+
const out = {};
|
|
289
|
+
if (inner === '')
|
|
290
|
+
return out;
|
|
291
|
+
const parts = splitFlow(inner);
|
|
292
|
+
for (const part of parts) {
|
|
293
|
+
const colon = part.indexOf(':');
|
|
294
|
+
if (colon < 0) {
|
|
295
|
+
throw new Error(`flow mapping entry needs 'key: value' at line ${line}: '${part}'`);
|
|
296
|
+
}
|
|
297
|
+
const key = part.slice(0, colon).trim();
|
|
298
|
+
const value = part.slice(colon + 1).trim();
|
|
299
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(key)) {
|
|
300
|
+
throw new Error(`flow mapping key must match [a-zA-Z_][a-zA-Z0-9_-]* at line ${line}: '${key}'`);
|
|
301
|
+
}
|
|
302
|
+
out[key] = parseScalar(value, line);
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
function splitFlow(text) {
|
|
307
|
+
const out = [];
|
|
308
|
+
let depth = 0;
|
|
309
|
+
let inSingle = false;
|
|
310
|
+
let inDouble = false;
|
|
311
|
+
let buf = '';
|
|
312
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
313
|
+
const ch = text[i];
|
|
314
|
+
if (ch === "'" && !inDouble) {
|
|
315
|
+
inSingle = !inSingle;
|
|
316
|
+
buf += ch;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (ch === '"' && !inSingle) {
|
|
320
|
+
inDouble = !inDouble;
|
|
321
|
+
buf += ch;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (!inSingle && !inDouble) {
|
|
325
|
+
if (ch === '{' || ch === '[')
|
|
326
|
+
depth += 1;
|
|
327
|
+
else if (ch === '}' || ch === ']')
|
|
328
|
+
depth -= 1;
|
|
329
|
+
if (ch === ',' && depth === 0) {
|
|
330
|
+
out.push(buf);
|
|
331
|
+
buf = '';
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
buf += ch;
|
|
336
|
+
}
|
|
337
|
+
if (buf.trim() !== '')
|
|
338
|
+
out.push(buf);
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
function parseScalar(raw, line) {
|
|
342
|
+
if (raw === '')
|
|
343
|
+
return '';
|
|
344
|
+
const first = raw[0];
|
|
345
|
+
if (first === '"' || first === "'") {
|
|
346
|
+
const last = raw[raw.length - 1];
|
|
347
|
+
if (last !== first) {
|
|
348
|
+
throw new Error(`unterminated quoted string at line ${line}: ${raw}`);
|
|
349
|
+
}
|
|
350
|
+
return raw.slice(1, -1);
|
|
351
|
+
}
|
|
352
|
+
if (raw === 'true')
|
|
353
|
+
return true;
|
|
354
|
+
if (raw === 'false')
|
|
355
|
+
return false;
|
|
356
|
+
if (/^-?\d+$/.test(raw))
|
|
357
|
+
return Number.parseInt(raw, 10);
|
|
358
|
+
if (/^-?\d+\.\d+$/.test(raw))
|
|
359
|
+
return Number.parseFloat(raw);
|
|
360
|
+
return raw;
|
|
361
|
+
}
|
|
362
|
+
export function compileWhenExpression(source, declaredParams) {
|
|
363
|
+
const declared = new Set(declaredParams.map((p) => p.name));
|
|
364
|
+
const tokens = tokenizeWhen(source);
|
|
365
|
+
if (!tokens.ok)
|
|
366
|
+
return { ok: false, error: tokens.error };
|
|
367
|
+
const parser = new WhenParser(tokens.tokens, declared);
|
|
368
|
+
let ast;
|
|
369
|
+
try {
|
|
370
|
+
ast = parser.parseExpression();
|
|
371
|
+
parser.expectEnd();
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
375
|
+
return { ok: false, error: message };
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
ok: true,
|
|
379
|
+
evaluate: (params) => evaluateWhen(ast, params),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function tokenizeWhen(source) {
|
|
383
|
+
const tokens = [];
|
|
384
|
+
let i = 0;
|
|
385
|
+
while (i < source.length) {
|
|
386
|
+
const ch = source[i] ?? '';
|
|
387
|
+
if (/\s/.test(ch)) {
|
|
388
|
+
i += 1;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (ch === '(') {
|
|
392
|
+
tokens.push({ kind: 'LPAREN' });
|
|
393
|
+
i += 1;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (ch === ')') {
|
|
397
|
+
tokens.push({ kind: 'RPAREN' });
|
|
398
|
+
i += 1;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (ch === '!' && source[i + 1] === '=' && source[i + 2] === '=') {
|
|
402
|
+
tokens.push({ kind: 'NEQ' });
|
|
403
|
+
i += 3;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (ch === '=' && source[i + 1] === '=' && source[i + 2] === '=') {
|
|
407
|
+
tokens.push({ kind: 'EQ' });
|
|
408
|
+
i += 3;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (ch === '!') {
|
|
412
|
+
tokens.push({ kind: 'BANG' });
|
|
413
|
+
i += 1;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (ch === '&' && source[i + 1] === '&') {
|
|
417
|
+
tokens.push({ kind: 'AND' });
|
|
418
|
+
i += 2;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (ch === '|' && source[i + 1] === '|') {
|
|
422
|
+
tokens.push({ kind: 'OR' });
|
|
423
|
+
i += 2;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (ch === "'" || ch === '"') {
|
|
427
|
+
const close = source.indexOf(ch, i + 1);
|
|
428
|
+
if (close < 0) {
|
|
429
|
+
return { ok: false, error: `unterminated string starting at offset ${i}` };
|
|
430
|
+
}
|
|
431
|
+
tokens.push({ kind: 'STRING', value: source.slice(i + 1, close) });
|
|
432
|
+
i = close + 1;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (/[0-9-]/.test(ch)) {
|
|
436
|
+
const start = i;
|
|
437
|
+
if (ch === '-')
|
|
438
|
+
i += 1;
|
|
439
|
+
while (i < source.length && /[0-9.]/.test(source[i] ?? ''))
|
|
440
|
+
i += 1;
|
|
441
|
+
const num = Number.parseFloat(source.slice(start, i));
|
|
442
|
+
if (!Number.isFinite(num)) {
|
|
443
|
+
return { ok: false, error: `bad number at offset ${start}` };
|
|
444
|
+
}
|
|
445
|
+
tokens.push({ kind: 'NUMBER', value: num });
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (/[a-zA-Z_]/.test(ch)) {
|
|
449
|
+
const start = i;
|
|
450
|
+
while (i < source.length && /[a-zA-Z0-9_.]/.test(source[i] ?? ''))
|
|
451
|
+
i += 1;
|
|
452
|
+
const word = source.slice(start, i);
|
|
453
|
+
if (word === 'true' || word === 'false') {
|
|
454
|
+
tokens.push({ kind: 'BOOL', value: word === 'true' });
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const refMatch = /^params\.([a-zA-Z_][a-zA-Z0-9_]*)$/.exec(word);
|
|
458
|
+
if (!refMatch) {
|
|
459
|
+
return {
|
|
460
|
+
ok: false,
|
|
461
|
+
error: `unknown identifier '${word}' (only params.<name> is allowed)`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
tokens.push({ kind: 'REF', name: refMatch[1] });
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
return { ok: false, error: `unexpected character '${ch}' at offset ${i}` };
|
|
468
|
+
}
|
|
469
|
+
return { ok: true, tokens };
|
|
470
|
+
}
|
|
471
|
+
class WhenParser {
|
|
472
|
+
tokens;
|
|
473
|
+
declared;
|
|
474
|
+
pos = 0;
|
|
475
|
+
constructor(tokens, declared) {
|
|
476
|
+
this.tokens = tokens;
|
|
477
|
+
this.declared = declared;
|
|
478
|
+
}
|
|
479
|
+
parseExpression() {
|
|
480
|
+
return this.parseOr();
|
|
481
|
+
}
|
|
482
|
+
expectEnd() {
|
|
483
|
+
if (this.pos !== this.tokens.length) {
|
|
484
|
+
throw new Error(`unexpected token at position ${this.pos}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
parseOr() {
|
|
488
|
+
let left = this.parseAnd();
|
|
489
|
+
while (this.peek()?.kind === 'OR') {
|
|
490
|
+
this.pos += 1;
|
|
491
|
+
const right = this.parseAnd();
|
|
492
|
+
left = { kind: 'OR', left, right };
|
|
493
|
+
}
|
|
494
|
+
return left;
|
|
495
|
+
}
|
|
496
|
+
parseAnd() {
|
|
497
|
+
let left = this.parseUnary();
|
|
498
|
+
while (this.peek()?.kind === 'AND') {
|
|
499
|
+
this.pos += 1;
|
|
500
|
+
const right = this.parseUnary();
|
|
501
|
+
left = { kind: 'AND', left, right };
|
|
502
|
+
}
|
|
503
|
+
return left;
|
|
504
|
+
}
|
|
505
|
+
parseUnary() {
|
|
506
|
+
if (this.peek()?.kind === 'BANG') {
|
|
507
|
+
this.pos += 1;
|
|
508
|
+
const child = this.parseUnary();
|
|
509
|
+
return { kind: 'NOT', child };
|
|
510
|
+
}
|
|
511
|
+
return this.parseAtom();
|
|
512
|
+
}
|
|
513
|
+
parseAtom() {
|
|
514
|
+
const tok = this.peek();
|
|
515
|
+
if (!tok)
|
|
516
|
+
throw new Error('unexpected end of expression');
|
|
517
|
+
if (tok.kind === 'LPAREN') {
|
|
518
|
+
this.pos += 1;
|
|
519
|
+
const inner = this.parseOr();
|
|
520
|
+
const close = this.peek();
|
|
521
|
+
if (close?.kind !== 'RPAREN')
|
|
522
|
+
throw new Error('missing closing paren');
|
|
523
|
+
this.pos += 1;
|
|
524
|
+
return inner;
|
|
525
|
+
}
|
|
526
|
+
if (tok.kind === 'REF') {
|
|
527
|
+
this.pos += 1;
|
|
528
|
+
this.assertDeclared(tok.name);
|
|
529
|
+
const next = this.peek();
|
|
530
|
+
if (next?.kind === 'EQ' || next?.kind === 'NEQ') {
|
|
531
|
+
const opKind = next.kind;
|
|
532
|
+
this.pos += 1;
|
|
533
|
+
const lit = this.peek();
|
|
534
|
+
if (!lit ||
|
|
535
|
+
(lit.kind !== 'STRING' && lit.kind !== 'BOOL' && lit.kind !== 'NUMBER')) {
|
|
536
|
+
throw new Error(`expected literal after ${opKind} for params.${tok.name}`);
|
|
537
|
+
}
|
|
538
|
+
this.pos += 1;
|
|
539
|
+
const literal = lit.kind === 'STRING'
|
|
540
|
+
? lit.value
|
|
541
|
+
: lit.kind === 'BOOL'
|
|
542
|
+
? lit.value
|
|
543
|
+
: lit.value;
|
|
544
|
+
return { kind: opKind, ref: tok.name, literal };
|
|
545
|
+
}
|
|
546
|
+
return { kind: 'REF', ref: tok.name };
|
|
547
|
+
}
|
|
548
|
+
throw new Error(`unexpected token '${tok.kind}'`);
|
|
549
|
+
}
|
|
550
|
+
peek() {
|
|
551
|
+
return this.tokens[this.pos];
|
|
552
|
+
}
|
|
553
|
+
assertDeclared(name) {
|
|
554
|
+
if (!this.declared.has(name)) {
|
|
555
|
+
throw new Error(`undeclared param 'params.${name}' (declare it in params:)`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function evaluateWhen(node, params) {
|
|
560
|
+
switch (node.kind) {
|
|
561
|
+
case 'OR':
|
|
562
|
+
return evaluateWhen(node.left, params) || evaluateWhen(node.right, params);
|
|
563
|
+
case 'AND':
|
|
564
|
+
return evaluateWhen(node.left, params) && evaluateWhen(node.right, params);
|
|
565
|
+
case 'NOT':
|
|
566
|
+
return !evaluateWhen(node.child, params);
|
|
567
|
+
case 'EQ': {
|
|
568
|
+
const value = params[node.ref];
|
|
569
|
+
return value === node.literal;
|
|
570
|
+
}
|
|
571
|
+
case 'NEQ': {
|
|
572
|
+
const value = params[node.ref];
|
|
573
|
+
return value !== node.literal;
|
|
574
|
+
}
|
|
575
|
+
case 'REF': {
|
|
576
|
+
const value = params[node.ref];
|
|
577
|
+
if (typeof value === 'boolean')
|
|
578
|
+
return value;
|
|
579
|
+
if (typeof value === 'number')
|
|
580
|
+
return value !== 0;
|
|
581
|
+
if (typeof value === 'string')
|
|
582
|
+
return value !== '';
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
//# sourceMappingURL=schema.js.map
|