@qodeca/xezar 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +810 -0
- package/dist/agent-config/account-identity.d.ts +44 -0
- package/dist/agent-config/account-identity.js +128 -0
- package/dist/agent-config/account-identity.js.map +1 -0
- package/dist/agent-config/catalog.d.ts +71 -0
- package/dist/agent-config/catalog.js +223 -0
- package/dist/agent-config/catalog.js.map +1 -0
- package/dist/agent-config/files.d.ts +46 -0
- package/dist/agent-config/files.js +119 -0
- package/dist/agent-config/files.js.map +1 -0
- package/dist/agent-config/model-settings/claude.d.ts +2 -0
- package/dist/agent-config/model-settings/claude.js +11 -0
- package/dist/agent-config/model-settings/claude.js.map +1 -0
- package/dist/agent-config/model-settings/codex.d.ts +2 -0
- package/dist/agent-config/model-settings/codex.js +16 -0
- package/dist/agent-config/model-settings/codex.js.map +1 -0
- package/dist/agent-config/model-settings/opencode.d.ts +2 -0
- package/dist/agent-config/model-settings/opencode.js +8 -0
- package/dist/agent-config/model-settings/opencode.js.map +1 -0
- package/dist/agent-config/model-settings/pi.d.ts +12 -0
- package/dist/agent-config/model-settings/pi.js +18 -0
- package/dist/agent-config/model-settings/pi.js.map +1 -0
- package/dist/agent-config/model-settings/shared.d.ts +9 -0
- package/dist/agent-config/model-settings/shared.js +93 -0
- package/dist/agent-config/model-settings/shared.js.map +1 -0
- package/dist/agent-config/model-settings/types.d.ts +9 -0
- package/dist/agent-config/model-settings/types.js +2 -0
- package/dist/agent-config/model-settings/types.js.map +1 -0
- package/dist/agent-config/models.d.ts +9 -0
- package/dist/agent-config/models.js +32 -0
- package/dist/agent-config/models.js.map +1 -0
- package/dist/agent-config/seed.d.ts +7 -0
- package/dist/agent-config/seed.js +90 -0
- package/dist/agent-config/seed.js.map +1 -0
- package/dist/agent-config/service.d.ts +44 -0
- package/dist/agent-config/service.js +71 -0
- package/dist/agent-config/service.js.map +1 -0
- package/dist/agent-config/validate.d.ts +21 -0
- package/dist/agent-config/validate.js +76 -0
- package/dist/agent-config/validate.js.map +1 -0
- package/dist/automations/coordinator.d.ts +26 -0
- package/dist/automations/coordinator.js +66 -0
- package/dist/automations/coordinator.js.map +1 -0
- package/dist/automations/github-poller.d.ts +74 -0
- package/dist/automations/github-poller.js +234 -0
- package/dist/automations/github-poller.js.map +1 -0
- package/dist/automations/scheduler.d.ts +42 -0
- package/dist/automations/scheduler.js +192 -0
- package/dist/automations/scheduler.js.map +1 -0
- package/dist/automations/store.d.ts +65 -0
- package/dist/automations/store.js +298 -0
- package/dist/automations/store.js.map +1 -0
- package/dist/automations/task-template.d.ts +18 -0
- package/dist/automations/task-template.js +95 -0
- package/dist/automations/task-template.js.map +1 -0
- package/dist/automations/types.d.ts +255 -0
- package/dist/automations/types.js +159 -0
- package/dist/automations/types.js.map +1 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.js +242 -0
- package/dist/config.js.map +1 -0
- package/dist/contract/agent-config.d.ts +153 -0
- package/dist/contract/agent-profiles.d.ts +347 -0
- package/dist/contract/automations.d.ts +935 -0
- package/dist/contract/events.d.ts +104 -0
- package/dist/contract/github.d.ts +597 -0
- package/dist/contract/health.d.ts +102 -0
- package/dist/contract/index.d.ts +16 -0
- package/dist/contract/index.js +1953 -0
- package/dist/contract/projects.d.ts +210 -0
- package/dist/contract/repo.d.ts +234 -0
- package/dist/contract/runs.d.ts +1527 -0
- package/dist/contract/skills.d.ts +234 -0
- package/dist/contract/workflows.d.ts +218 -0
- package/dist/contract/workspace.d.ts +579 -0
- package/dist/core/agent-env.d.ts +31 -0
- package/dist/core/agent-env.js +371 -0
- package/dist/core/agent-env.js.map +1 -0
- package/dist/core/agent-model-policy.d.ts +10 -0
- package/dist/core/agent-model-policy.js +36 -0
- package/dist/core/agent-model-policy.js.map +1 -0
- package/dist/core/agent-profiles.d.ts +62 -0
- package/dist/core/agent-profiles.js +90 -0
- package/dist/core/agent-profiles.js.map +1 -0
- package/dist/core/agent-runner.d.ts +211 -0
- package/dist/core/agent-runner.js +68 -0
- package/dist/core/agent-runner.js.map +1 -0
- package/dist/core/ask.d.ts +115 -0
- package/dist/core/ask.js +256 -0
- package/dist/core/ask.js.map +1 -0
- package/dist/core/backend-detect.d.ts +16 -0
- package/dist/core/backend-detect.js +155 -0
- package/dist/core/backend-detect.js.map +1 -0
- package/dist/core/claude-cli-runner.d.ts +65 -0
- package/dist/core/claude-cli-runner.js +479 -0
- package/dist/core/claude-cli-runner.js.map +1 -0
- package/dist/core/claude-model-catalog.d.ts +24 -0
- package/dist/core/claude-model-catalog.js +208 -0
- package/dist/core/claude-model-catalog.js.map +1 -0
- package/dist/core/claude-ui-mapper.d.ts +79 -0
- package/dist/core/claude-ui-mapper.js +597 -0
- package/dist/core/claude-ui-mapper.js.map +1 -0
- package/dist/core/codex-app-server-runner.d.ts +41 -0
- package/dist/core/codex-app-server-runner.js +584 -0
- package/dist/core/codex-app-server-runner.js.map +1 -0
- package/dist/core/codex-app-server-transport.d.ts +35 -0
- package/dist/core/codex-app-server-transport.js +150 -0
- package/dist/core/codex-app-server-transport.js.map +1 -0
- package/dist/core/codex-model-catalog.d.ts +10 -0
- package/dist/core/codex-model-catalog.js +110 -0
- package/dist/core/codex-model-catalog.js.map +1 -0
- package/dist/core/codex-ui-mapper.d.ts +109 -0
- package/dist/core/codex-ui-mapper.js +764 -0
- package/dist/core/codex-ui-mapper.js.map +1 -0
- package/dist/core/model-identity.d.ts +125 -0
- package/dist/core/model-identity.js +132 -0
- package/dist/core/model-identity.js.map +1 -0
- package/dist/core/model-presets.d.ts +21 -0
- package/dist/core/model-presets.js +91 -0
- package/dist/core/model-presets.js.map +1 -0
- package/dist/core/ndjson.d.ts +6 -0
- package/dist/core/ndjson.js +23 -0
- package/dist/core/ndjson.js.map +1 -0
- package/dist/core/opencode-model-catalog.d.ts +32 -0
- package/dist/core/opencode-model-catalog.js +153 -0
- package/dist/core/opencode-model-catalog.js.map +1 -0
- package/dist/core/opencode-server-runner.d.ts +32 -0
- package/dist/core/opencode-server-runner.js +535 -0
- package/dist/core/opencode-server-runner.js.map +1 -0
- package/dist/core/opencode-ui-mapper.d.ts +111 -0
- package/dist/core/opencode-ui-mapper.js +668 -0
- package/dist/core/opencode-ui-mapper.js.map +1 -0
- package/dist/core/pi-runner.d.ts +24 -0
- package/dist/core/pi-runner.js +349 -0
- package/dist/core/pi-runner.js.map +1 -0
- package/dist/core/pi-ui-mapper.d.ts +34 -0
- package/dist/core/pi-ui-mapper.js +270 -0
- package/dist/core/pi-ui-mapper.js.map +1 -0
- package/dist/core/process-usage.d.ts +66 -0
- package/dist/core/process-usage.js +193 -0
- package/dist/core/process-usage.js.map +1 -0
- package/dist/core/provider-auth.d.ts +154 -0
- package/dist/core/provider-auth.js +509 -0
- package/dist/core/provider-auth.js.map +1 -0
- package/dist/core/provider-availability.d.ts +3 -0
- package/dist/core/provider-availability.js +14 -0
- package/dist/core/provider-availability.js.map +1 -0
- package/dist/core/runner-factory.d.ts +8 -0
- package/dist/core/runner-factory.js +25 -0
- package/dist/core/runner-factory.js.map +1 -0
- package/dist/core/runner-model-catalog.d.ts +28 -0
- package/dist/core/runner-model-catalog.js +62 -0
- package/dist/core/runner-model-catalog.js.map +1 -0
- package/dist/core/secret-redaction.d.ts +37 -0
- package/dist/core/secret-redaction.js +115 -0
- package/dist/core/secret-redaction.js.map +1 -0
- package/dist/core/shell-env.d.ts +28 -0
- package/dist/core/shell-env.js +56 -0
- package/dist/core/shell-env.js.map +1 -0
- package/dist/core/tool-display.d.ts +31 -0
- package/dist/core/tool-display.js +179 -0
- package/dist/core/tool-display.js.map +1 -0
- package/dist/core/ui-events.d.ts +332 -0
- package/dist/core/ui-events.js +22 -0
- package/dist/core/ui-events.js.map +1 -0
- package/dist/core/usage-limit.d.ts +42 -0
- package/dist/core/usage-limit.js +209 -0
- package/dist/core/usage-limit.js.map +1 -0
- package/dist/core/usage.d.ts +11 -0
- package/dist/core/usage.js +15 -0
- package/dist/core/usage.js.map +1 -0
- package/dist/core/v1-text-coalescer.d.ts +36 -0
- package/dist/core/v1-text-coalescer.js +72 -0
- package/dist/core/v1-text-coalescer.js.map +1 -0
- package/dist/git-diff-base.d.ts +85 -0
- package/dist/git-diff-base.js +180 -0
- package/dist/git-diff-base.js.map +1 -0
- package/dist/git-refs.d.ts +11 -0
- package/dist/git-refs.js +14 -0
- package/dist/git-refs.js.map +1 -0
- package/dist/git-worktree.d.ts +161 -0
- package/dist/git-worktree.js +508 -0
- package/dist/git-worktree.js.map +1 -0
- package/dist/handoff.d.ts +62 -0
- package/dist/handoff.js +154 -0
- package/dist/handoff.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +684 -0
- package/dist/index.js.map +1 -0
- package/dist/install-as-command.d.ts +40 -0
- package/dist/install-as-command.js +52 -0
- package/dist/install-as-command.js.map +1 -0
- package/dist/pack-check.d.ts +17 -0
- package/dist/pack-check.js +27 -0
- package/dist/pack-check.js.map +1 -0
- package/dist/paths.d.ts +140 -0
- package/dist/paths.js +192 -0
- package/dist/paths.js.map +1 -0
- package/dist/planner.d.ts +27 -0
- package/dist/planner.js +268 -0
- package/dist/planner.js.map +1 -0
- package/dist/release/manifests.d.ts +78 -0
- package/dist/release/manifests.js +57 -0
- package/dist/release/manifests.js.map +1 -0
- package/dist/release/snapshot.d.ts +67 -0
- package/dist/release/snapshot.js +97 -0
- package/dist/release/snapshot.js.map +1 -0
- package/dist/release/stable.d.ts +33 -0
- package/dist/release/stable.js +50 -0
- package/dist/release/stable.js.map +1 -0
- package/dist/runs/agent-tmpdir.d.ts +41 -0
- package/dist/runs/agent-tmpdir.js +183 -0
- package/dist/runs/agent-tmpdir.js.map +1 -0
- package/dist/runs/arm-repo-handle.d.ts +17 -0
- package/dist/runs/arm-repo-handle.js +24 -0
- package/dist/runs/arm-repo-handle.js.map +1 -0
- package/dist/runs/auto-name.d.ts +75 -0
- package/dist/runs/auto-name.js +179 -0
- package/dist/runs/auto-name.js.map +1 -0
- package/dist/runs/event-history.d.ts +50 -0
- package/dist/runs/event-history.js +613 -0
- package/dist/runs/event-history.js.map +1 -0
- package/dist/runs/retention.d.ts +60 -0
- package/dist/runs/retention.js +86 -0
- package/dist/runs/retention.js.map +1 -0
- package/dist/runs/review-gate.d.ts +18 -0
- package/dist/runs/review-gate.js +21 -0
- package/dist/runs/review-gate.js.map +1 -0
- package/dist/runs/run-index.d.ts +19 -0
- package/dist/runs/run-index.js +43 -0
- package/dist/runs/run-index.js.map +1 -0
- package/dist/runs/store.d.ts +490 -0
- package/dist/runs/store.js +1283 -0
- package/dist/runs/store.js.map +1 -0
- package/dist/runs/task-markers.d.ts +28 -0
- package/dist/runs/task-markers.js +74 -0
- package/dist/runs/task-markers.js.map +1 -0
- package/dist/runs/task-refs.d.ts +25 -0
- package/dist/runs/task-refs.js +74 -0
- package/dist/runs/task-refs.js.map +1 -0
- package/dist/runs/ui-event-sink.d.ts +80 -0
- package/dist/runs/ui-event-sink.js +209 -0
- package/dist/runs/ui-event-sink.js.map +1 -0
- package/dist/server/app-type.d.ts +21 -0
- package/dist/server/app-type.js +2 -0
- package/dist/server/app-type.js.map +1 -0
- package/dist/server/capabilities.d.ts +70 -0
- package/dist/server/capabilities.js +152 -0
- package/dist/server/capabilities.js.map +1 -0
- package/dist/server/checkout.d.ts +128 -0
- package/dist/server/checkout.js +296 -0
- package/dist/server/checkout.js.map +1 -0
- package/dist/server/forge/github.d.ts +535 -0
- package/dist/server/forge/github.js +2544 -0
- package/dist/server/forge/github.js.map +1 -0
- package/dist/server/forge/index.d.ts +40 -0
- package/dist/server/forge/index.js +72 -0
- package/dist/server/forge/index.js.map +1 -0
- package/dist/server/forge/types.d.ts +267 -0
- package/dist/server/forge/types.js +2 -0
- package/dist/server/forge/types.js.map +1 -0
- package/dist/server/fs-browse.d.ts +105 -0
- package/dist/server/fs-browse.js +182 -0
- package/dist/server/fs-browse.js.map +1 -0
- package/dist/server/git-changes.d.ts +213 -0
- package/dist/server/git-changes.js +555 -0
- package/dist/server/git-changes.js.map +1 -0
- package/dist/server/git.d.ts +28 -0
- package/dist/server/git.js +112 -0
- package/dist/server/git.js.map +1 -0
- package/dist/server/github.d.ts +9 -0
- package/dist/server/github.js +8 -0
- package/dist/server/github.js.map +1 -0
- package/dist/server/launch-key.d.ts +7 -0
- package/dist/server/launch-key.js +33 -0
- package/dist/server/launch-key.js.map +1 -0
- package/dist/server/open-in-app.d.ts +66 -0
- package/dist/server/open-in-app.js +211 -0
- package/dist/server/open-in-app.js.map +1 -0
- package/dist/server/open-in-terminal.d.ts +70 -0
- package/dist/server/open-in-terminal.js +179 -0
- package/dist/server/open-in-terminal.js.map +1 -0
- package/dist/server/pr.d.ts +7 -0
- package/dist/server/pr.js +7 -0
- package/dist/server/pr.js.map +1 -0
- package/dist/server/project-context.d.ts +120 -0
- package/dist/server/project-context.js +192 -0
- package/dist/server/project-context.js.map +1 -0
- package/dist/server/provider-action-gate.d.ts +8 -0
- package/dist/server/provider-action-gate.js +56 -0
- package/dist/server/provider-action-gate.js.map +1 -0
- package/dist/server/provider-auth-runtime.d.ts +21 -0
- package/dist/server/provider-auth-runtime.js +66 -0
- package/dist/server/provider-auth-runtime.js.map +1 -0
- package/dist/server/server.d.ts +13578 -0
- package/dist/server/server.js +5284 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/static-ui.d.ts +49 -0
- package/dist/server/static-ui.js +92 -0
- package/dist/server/static-ui.js.map +1 -0
- package/dist/server/validators.d.ts +97 -0
- package/dist/server/validators.js +86 -0
- package/dist/server/validators.js.map +1 -0
- package/dist/server/ws.d.ts +83 -0
- package/dist/server/ws.js +216 -0
- package/dist/server/ws.js.map +1 -0
- package/dist/server/wsl.d.ts +25 -0
- package/dist/server/wsl.js +92 -0
- package/dist/server/wsl.js.map +1 -0
- package/dist/server-install/engine.d.ts +50 -0
- package/dist/server-install/engine.js +324 -0
- package/dist/server-install/engine.js.map +1 -0
- package/dist/server-install/platforms/macosx-ngrok.d.ts +6 -0
- package/dist/server-install/platforms/macosx-ngrok.js +348 -0
- package/dist/server-install/platforms/macosx-ngrok.js.map +1 -0
- package/dist/server-install/platforms/ubuntu-vps.d.ts +63 -0
- package/dist/server-install/platforms/ubuntu-vps.js +997 -0
- package/dist/server-install/platforms/ubuntu-vps.js.map +1 -0
- package/dist/server-install/state.d.ts +52 -0
- package/dist/server-install/state.js +182 -0
- package/dist/server-install/state.js.map +1 -0
- package/dist/server-install/steps.d.ts +109 -0
- package/dist/server-install/steps.js +334 -0
- package/dist/server-install/steps.js.map +1 -0
- package/dist/server-install/strategies.d.ts +4 -0
- package/dist/server-install/strategies.js +20 -0
- package/dist/server-install/strategies.js.map +1 -0
- package/dist/server-install/types.d.ts +287 -0
- package/dist/server-install/types.js +121 -0
- package/dist/server-install/types.js.map +1 -0
- package/dist/server-install/ui.d.ts +42 -0
- package/dist/server-install/ui.js +130 -0
- package/dist/server-install/ui.js.map +1 -0
- package/dist/skills-banner.d.ts +22 -0
- package/dist/skills-banner.js +41 -0
- package/dist/skills-banner.js.map +1 -0
- package/dist/skills-remote.d.ts +84 -0
- package/dist/skills-remote.js +468 -0
- package/dist/skills-remote.js.map +1 -0
- package/dist/skills-update.d.ts +88 -0
- package/dist/skills-update.js +392 -0
- package/dist/skills-update.js.map +1 -0
- package/dist/skills.d.ts +82 -0
- package/dist/skills.js +251 -0
- package/dist/skills.js.map +1 -0
- package/dist/todos.d.ts +48 -0
- package/dist/todos.js +201 -0
- package/dist/todos.js.map +1 -0
- package/dist/ui-state.d.ts +26 -0
- package/dist/ui-state.js +38 -0
- package/dist/ui-state.js.map +1 -0
- package/dist/update-check.d.ts +12 -0
- package/dist/update-check.js +41 -0
- package/dist/update-check.js.map +1 -0
- package/dist/workflows/load.d.ts +15 -0
- package/dist/workflows/load.js +56 -0
- package/dist/workflows/load.js.map +1 -0
- package/dist/workflows/run.d.ts +803 -0
- package/dist/workflows/run.js +3483 -0
- package/dist/workflows/run.js.map +1 -0
- package/dist/workflows/types.d.ts +145 -0
- package/dist/workflows/types.js +194 -0
- package/dist/workflows/types.js.map +1 -0
- package/dist/workspace/agent-accounts.d.ts +153 -0
- package/dist/workspace/agent-accounts.js +304 -0
- package/dist/workspace/agent-accounts.js.map +1 -0
- package/dist/workspace/agent-profiles.d.ts +78 -0
- package/dist/workspace/agent-profiles.js +115 -0
- package/dist/workspace/agent-profiles.js.map +1 -0
- package/dist/workspace/config.d.ts +169 -0
- package/dist/workspace/config.js +379 -0
- package/dist/workspace/config.js.map +1 -0
- package/dist/workspace/migrations.d.ts +45 -0
- package/dist/workspace/migrations.js +147 -0
- package/dist/workspace/migrations.js.map +1 -0
- package/dist/workspace/projects-cli.d.ts +25 -0
- package/dist/workspace/projects-cli.js +172 -0
- package/dist/workspace/projects-cli.js.map +1 -0
- package/dist/workspace/projects.d.ts +109 -0
- package/dist/workspace/projects.js +250 -0
- package/dist/workspace/projects.js.map +1 -0
- package/dist/workspace/semaphore.d.ts +179 -0
- package/dist/workspace/semaphore.js +195 -0
- package/dist/workspace/semaphore.js.map +1 -0
- package/dist/workspace/ui-state.d.ts +34 -0
- package/dist/workspace/ui-state.js +53 -0
- package/dist/workspace/ui-state.js.map +1 -0
- package/package.json +85 -0
- package/scripts/check-pack.mjs +46 -0
- package/scripts/inline-contract.mjs +112 -0
- package/scripts/install-as-command.mjs +120 -0
- package/scripts/mock-claude.mjs +524 -0
- package/scripts/mock-pi-rpc.mjs +83 -0
- package/scripts/sync-readme.mjs +20 -0
- package/scripts/test-process-usage.mjs +47 -0
- package/web/dist/assets/alert-dialog-CGskMvgc.js +1 -0
- package/web/dist/assets/arrow-down-G8adPhbu.js +1 -0
- package/web/dist/assets/arrow-left-BJsIW6A0.js +1 -0
- package/web/dist/assets/bundle-mjs-BT31bpU6.js +1 -0
- package/web/dist/assets/centered-state-B_V_Ai2h.js +43 -0
- package/web/dist/assets/chevron-right-CFzn-von.js +1 -0
- package/web/dist/assets/chunk-BO2N2NFS-DE6qKn3r.js +131 -0
- package/web/dist/assets/collapsible-B_8uYwHb.js +1 -0
- package/web/dist/assets/commit-list-C4Dv6vUL.js +1 -0
- package/web/dist/assets/compare-variants-SR2zZfZq.js +1 -0
- package/web/dist/assets/core-BCsw8oQw.js +12 -0
- package/web/dist/assets/css-BsVw1vtW.js +1 -0
- package/web/dist/assets/diff-CbozNUJq.js +3 -0
- package/web/dist/assets/diff-stat-btAmKFJr.js +1 -0
- package/web/dist/assets/diff-view-BpFTcG3R.js +6 -0
- package/web/dist/assets/diff-woXpYk--.js +1 -0
- package/web/dist/assets/dropdown-menu-Z80KMInL.js +1 -0
- package/web/dist/assets/editable-title-wS5NWwob.js +1 -0
- package/web/dist/assets/ellipsis-vertical-Bmi2u2TG.js +1 -0
- package/web/dist/assets/engine-javascript-FQvzRocB.js +141 -0
- package/web/dist/assets/file-DKoYWRsv.js +1 -0
- package/web/dist/assets/folder-u01HScRT.js +1 -0
- package/web/dist/assets/git-toolbar-DZlZjTGz.js +1 -0
- package/web/dist/assets/github-CFURo9kf.js +1 -0
- package/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/web/dist/assets/highlighted-body-OFNGDK62-BL-MfFgl.js +1 -0
- package/web/dist/assets/highlighter-C8DK1_sG.js +3 -0
- package/web/dist/assets/html-BY76lXLH.js +1 -0
- package/web/dist/assets/image-preview-BR3iIIhf.js +1 -0
- package/web/dist/assets/index-BcI5hug3.css +2 -0
- package/web/dist/assets/index-DlC81_GI.js +7 -0
- package/web/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/web/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/web/dist/assets/javascript-BgS3c2Ky.js +1 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/web/dist/assets/json-qhed-kSA.js +1 -0
- package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/web/dist/assets/jsx-Bz0zcwM4.js +1 -0
- package/web/dist/assets/lib-BQXq3kEf.js +1 -0
- package/web/dist/assets/lib-BxQXEXDF.js +1 -0
- package/web/dist/assets/markdown-5FuCZDA4.js +2 -0
- package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/web/dist/assets/mermaid-GHXKKRXX-C4jxqUUb.js +1 -0
- package/web/dist/assets/new-task-form-BydHBbef.js +1 -0
- package/web/dist/assets/pill-vNzaaYxj.js +1 -0
- package/web/dist/assets/project-router-BbCzrHKs.js +1 -0
- package/web/dist/assets/prompt-templates-B7Fs2b1S.js +15 -0
- package/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/web/dist/assets/react-runtime-CCIEwYL0.js +9 -0
- package/web/dist/assets/refresh-cw-DGliez3E.js +1 -0
- package/web/dist/assets/repo-git-CJFDMauC.js +1 -0
- package/web/dist/assets/rolldown-runtime-QTnfLwEv.js +1 -0
- package/web/dist/assets/run-diff-Bq9vkSNK.js +3 -0
- package/web/dist/assets/run-header-Cd8mSKBG.js +1 -0
- package/web/dist/assets/rust-Cfkwpbl8.js +1 -0
- package/web/dist/assets/search-x-69n_672Y.js +1 -0
- package/web/dist/assets/shellscript-CLZ0U2zV.js +1 -0
- package/web/dist/assets/skill-empty-hint-DTu0LzaJ.js +1 -0
- package/web/dist/assets/skills-Cs-auniL.js +1 -0
- package/web/dist/assets/skills-D4P-Hgey.js +1 -0
- package/web/dist/assets/sparkles-B8agSxBZ.js +1 -0
- package/web/dist/assets/sql-BsFa4tDR.js +1 -0
- package/web/dist/assets/square-terminal-Cf4Og6ct.js +1 -0
- package/web/dist/assets/tab-link-Cp2-CJS8.js +1 -0
- package/web/dist/assets/task-changes-DYgNXOyY.js +1 -0
- package/web/dist/assets/task-commits-BRpZSz1G.js +1 -0
- package/web/dist/assets/task-files-pKOwSyLh.js +2 -0
- package/web/dist/assets/task-thread-Bfyqqy-t.js +9 -0
- package/web/dist/assets/textarea--cpYshee.js +1 -0
- package/web/dist/assets/thread-loading-GyH2Atwo.js +1 -0
- package/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/web/dist/assets/trash-2--ZtrzUx3.js +1 -0
- package/web/dist/assets/triangle-alert-D5tJwRD2.js +1 -0
- package/web/dist/assets/tsx-udAQXfEw.js +1 -0
- package/web/dist/assets/typescript-bsJCZSQ-.js +1 -0
- package/web/dist/assets/upload-SnwVxKOA.js +1 -0
- package/web/dist/assets/use-desktop-C31lOFUD.js +1 -0
- package/web/dist/assets/use-submit-shortcut-JY8M_MPL.js +1 -0
- package/web/dist/assets/utils-Bb8ohcr3.js +64 -0
- package/web/dist/assets/workflows-BmOehP-A.js +11 -0
- package/web/dist/assets/yaml-rwi0_p6S.js +1 -0
- package/web/dist/assets/zoomable-image-0G3IBXWC.js +1 -0
- package/web/dist/index.html +80 -0
- package/web/dist/xezar.svg +14 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server capabilities reported by `/api/health` — the UI hides what the server
|
|
3
|
+
* says isn't there, and the matching endpoints refuse as defense in depth.
|
|
4
|
+
*
|
|
5
|
+
* `localHandoff` (cockpit-ui redesign spec §"Deployment modes — local vs
|
|
6
|
+
* hosted"): the default deployment is `npx @qodeca/xezar` on localhost, where
|
|
7
|
+
* handing a session off to a local terminal/editor makes sense. On a VPS/remote
|
|
8
|
+
* box it doesn't: `XEZ_REMOTE=1` (or binding a non-loopback host) switches to
|
|
9
|
+
* hosted mode — the UI hides every local-machine affordance and the open-in-*
|
|
10
|
+
* endpoints 409.
|
|
11
|
+
*
|
|
12
|
+
* `followups` (spec 007, #444, #471): the global follow-up inbox is **opt-in**
|
|
13
|
+
* via `XEZ_FOLLOWUPS=1` and off by default. Off, agents are never told to write
|
|
14
|
+
* `todos.json` (they get `HANDOFF_ONLY_INSTRUCTIONS` and an empty
|
|
15
|
+
* `XEZ_TODOS_FILE`), the Inbox nav item is gone and the inbox endpoints refuse.
|
|
16
|
+
* The per-task handoff journal is independent and runs either way.
|
|
17
|
+
*
|
|
18
|
+
* `singleProject` (spec 2026-07-21-xez-single-project): the workspace is
|
|
19
|
+
* deliberately constrained to the launch project when `XEZ_SINGLE_PROJECT=1`.
|
|
20
|
+
* Like the other opt-in capabilities, activation is strict: no other spelling
|
|
21
|
+
* enables it.
|
|
22
|
+
*
|
|
23
|
+
* `automations` (spec 2026-07-25-github-automations, #801): GitHub automations
|
|
24
|
+
* are **opt-in** via `XEZ_AUTOMATIONS=1` and off by default. Off, the
|
|
25
|
+
* `Automations` nav item is gone everywhere it is rendered, the
|
|
26
|
+
* `/automations*` endpoints refuse, and the workspace scheduler never polls
|
|
27
|
+
* GitHub — the flag removes the behavior, not only the UI. Activation is
|
|
28
|
+
* strict, like the two capabilities above. Nothing on disk is touched:
|
|
29
|
+
* definitions, receipts and high-watermarks survive the flag being off, so
|
|
30
|
+
* unsetting it and restarting restores the feature wholesale.
|
|
31
|
+
*
|
|
32
|
+
* Usage presentation: token counts and monetary cost stay visible by default.
|
|
33
|
+
* `XEZ_HIDE_TOKEN_USAGE=1` and `XEZ_HIDE_COST=1` hide them independently;
|
|
34
|
+
* legacy `XEZ_HIDE_TOKEN_METRICS=1` remains the master hide-all switch. None
|
|
35
|
+
* changes collection, persistence, events, or API run records.
|
|
36
|
+
*/
|
|
37
|
+
import type { Capabilities } from '../contract/index.js';
|
|
38
|
+
/** Canonical hostname of an authority: port and IPv6 brackets removed, IPv6
|
|
39
|
+
* expanded, zone id and FQDN trailing dot dropped, lowercased.
|
|
40
|
+
* `[::1]:4321` → `0:0:0:0:0:0:0:1`, `localhost.:4321` → `localhost`.
|
|
41
|
+
*
|
|
42
|
+
* Strict by construction: an authority that does not parse as one of the three
|
|
43
|
+
* legal shapes returns `''`, which no allowlist matches. Being lax here is a
|
|
44
|
+
* security bug, not a convenience — a parser that merely *finds* a loopback
|
|
45
|
+
* name inside the header would accept `[::1]@evil.com` or `127.0.0.1:evil.com`. */
|
|
46
|
+
export declare function normalizeHostname(host: string): string;
|
|
47
|
+
/** True for bind hosts that only the local machine can reach. Undefined = the
|
|
48
|
+
* default bind (127.0.0.1), hence trusted — this is a *configuration* value we
|
|
49
|
+
* chose, not a request header. Do NOT use this on attacker-controlled input;
|
|
50
|
+
* use `isLoopbackHostHeader`, which fails closed on a missing host. */
|
|
51
|
+
export declare function isLoopbackHost(host: string | undefined): boolean;
|
|
52
|
+
/** True for a request's `Host`/`Origin` hostname when it names this machine.
|
|
53
|
+
* The untrusted-input twin of `isLoopbackHost`: a missing or unparseable host
|
|
54
|
+
* is **untrusted** (false), because absent is not the same as "we defaulted to
|
|
55
|
+
* loopback" once the value arrives over the wire (#426). */
|
|
56
|
+
export declare function isLoopbackHostHeader(host: string | null | undefined): boolean;
|
|
57
|
+
/** `XEZ_REMOTE=1` or a non-loopback bind host ⇒ hosted mode (no local handoff).
|
|
58
|
+
* `XEZ_FOLLOWUPS=1` ⇒ the follow-up inbox exists (#471).
|
|
59
|
+
* `XEZ_AUTOMATIONS=1` ⇒ GitHub automations exist (#801).
|
|
60
|
+
*
|
|
61
|
+
* Read per request — cheap, and tests/ops can flip `XEZ_REMOTE` live. `followups` is honest
|
|
62
|
+
* per request too, but flipping it ON at runtime is only half a switch: the per-dataDir
|
|
63
|
+
* todos watch (step 2.3) is created by an SSE subscription, so connections opened while the
|
|
64
|
+
* flag was off never subscribed and get no live inbox updates until they reconnect. Hence
|
|
65
|
+
* the UI's "set XEZ_FOLLOWUPS=1 and restart xezar" wording — treat it as a boot-time flag.
|
|
66
|
+
*
|
|
67
|
+
* `automations` carries the same caveat and for the same reason: the workspace scheduler is
|
|
68
|
+
* started once, on the server's `listening` event, so flipping the flag on afterwards gates
|
|
69
|
+
* the routes open without ever starting the poller. Boot-time flag, same wording. */
|
|
70
|
+
export declare function resolveCapabilities(env?: NodeJS.ProcessEnv, bindHost?: string): Capabilities;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server capabilities reported by `/api/health` — the UI hides what the server
|
|
3
|
+
* says isn't there, and the matching endpoints refuse as defense in depth.
|
|
4
|
+
*
|
|
5
|
+
* `localHandoff` (cockpit-ui redesign spec §"Deployment modes — local vs
|
|
6
|
+
* hosted"): the default deployment is `npx @qodeca/xezar` on localhost, where
|
|
7
|
+
* handing a session off to a local terminal/editor makes sense. On a VPS/remote
|
|
8
|
+
* box it doesn't: `XEZ_REMOTE=1` (or binding a non-loopback host) switches to
|
|
9
|
+
* hosted mode — the UI hides every local-machine affordance and the open-in-*
|
|
10
|
+
* endpoints 409.
|
|
11
|
+
*
|
|
12
|
+
* `followups` (spec 007, #444, #471): the global follow-up inbox is **opt-in**
|
|
13
|
+
* via `XEZ_FOLLOWUPS=1` and off by default. Off, agents are never told to write
|
|
14
|
+
* `todos.json` (they get `HANDOFF_ONLY_INSTRUCTIONS` and an empty
|
|
15
|
+
* `XEZ_TODOS_FILE`), the Inbox nav item is gone and the inbox endpoints refuse.
|
|
16
|
+
* The per-task handoff journal is independent and runs either way.
|
|
17
|
+
*
|
|
18
|
+
* `singleProject` (spec 2026-07-21-xez-single-project): the workspace is
|
|
19
|
+
* deliberately constrained to the launch project when `XEZ_SINGLE_PROJECT=1`.
|
|
20
|
+
* Like the other opt-in capabilities, activation is strict: no other spelling
|
|
21
|
+
* enables it.
|
|
22
|
+
*
|
|
23
|
+
* `automations` (spec 2026-07-25-github-automations, #801): GitHub automations
|
|
24
|
+
* are **opt-in** via `XEZ_AUTOMATIONS=1` and off by default. Off, the
|
|
25
|
+
* `Automations` nav item is gone everywhere it is rendered, the
|
|
26
|
+
* `/automations*` endpoints refuse, and the workspace scheduler never polls
|
|
27
|
+
* GitHub — the flag removes the behavior, not only the UI. Activation is
|
|
28
|
+
* strict, like the two capabilities above. Nothing on disk is touched:
|
|
29
|
+
* definitions, receipts and high-watermarks survive the flag being off, so
|
|
30
|
+
* unsetting it and restarting restores the feature wholesale.
|
|
31
|
+
*
|
|
32
|
+
* Usage presentation: token counts and monetary cost stay visible by default.
|
|
33
|
+
* `XEZ_HIDE_TOKEN_USAGE=1` and `XEZ_HIDE_COST=1` hide them independently;
|
|
34
|
+
* legacy `XEZ_HIDE_TOKEN_METRICS=1` remains the master hide-all switch. None
|
|
35
|
+
* changes collection, persistence, events, or API run records.
|
|
36
|
+
*/
|
|
37
|
+
import { followupsEnabled } from '../handoff.js';
|
|
38
|
+
/** Every IPv4 address in 127.0.0.0/8, anchored. Anchoring is load-bearing: a
|
|
39
|
+
* `startsWith('127.')` test also matches attacker-controlled *hostnames* like
|
|
40
|
+
* `127.0.0.1.evil.com`, which is exactly the DNS-rebinding bypass #426 is
|
|
41
|
+
* about — see `isLoopbackHostHeader`. */
|
|
42
|
+
const LOOPBACK_V4 = /^127(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
|
|
43
|
+
/** The IPv6 loopback in the canonical form `expandIpv6` emits. */
|
|
44
|
+
const LOOPBACK_V6 = '0:0:0:0:0:0:0:1';
|
|
45
|
+
/** Expand an IPv6 literal to all 8 groups, lowercase and without leading zeros
|
|
46
|
+
* (`::1` → `0:0:0:0:0:0:0:1`), or `null` when it is not a valid literal.
|
|
47
|
+
*
|
|
48
|
+
* Canonicalizing rather than merely *recognizing* matters: `Host` carries
|
|
49
|
+
* whatever spelling the client sent, but `new URL().hostname` compresses
|
|
50
|
+
* (`[0:0:0:0:0:0:0:1]` → `[::1]`), so the two only compare equal on a common
|
|
51
|
+
* form. */
|
|
52
|
+
function expandIpv6(h) {
|
|
53
|
+
const halves = h.split('::');
|
|
54
|
+
if (halves.length > 2)
|
|
55
|
+
return null;
|
|
56
|
+
let groups;
|
|
57
|
+
if (halves.length === 2) {
|
|
58
|
+
const head = halves[0] ? halves[0].split(':') : [];
|
|
59
|
+
const tail = halves[1] ? halves[1].split(':') : [];
|
|
60
|
+
if (head.length + tail.length > 7)
|
|
61
|
+
return null;
|
|
62
|
+
groups = [...head, ...Array(8 - head.length - tail.length).fill('0'), ...tail];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
groups = h.split(':');
|
|
66
|
+
}
|
|
67
|
+
if (groups.length !== 8 || !groups.every((g) => /^[0-9a-f]{1,4}$/.test(g)))
|
|
68
|
+
return null;
|
|
69
|
+
return groups.map((g) => parseInt(g, 16).toString(16)).join(':');
|
|
70
|
+
}
|
|
71
|
+
/** Canonical hostname of an authority: port and IPv6 brackets removed, IPv6
|
|
72
|
+
* expanded, zone id and FQDN trailing dot dropped, lowercased.
|
|
73
|
+
* `[::1]:4321` → `0:0:0:0:0:0:0:1`, `localhost.:4321` → `localhost`.
|
|
74
|
+
*
|
|
75
|
+
* Strict by construction: an authority that does not parse as one of the three
|
|
76
|
+
* legal shapes returns `''`, which no allowlist matches. Being lax here is a
|
|
77
|
+
* security bug, not a convenience — a parser that merely *finds* a loopback
|
|
78
|
+
* name inside the header would accept `[::1]@evil.com` or `127.0.0.1:evil.com`. */
|
|
79
|
+
export function normalizeHostname(host) {
|
|
80
|
+
// 1. Bracketed IPv6, optionally with a port: `[::1]`, `[::1%25eth0]:4321`.
|
|
81
|
+
// The end anchor is load-bearing — without it `[::1]@evil.com` → `::1`.
|
|
82
|
+
const bracketed = host.match(/^\[([^\]]+)\](?::\d+)?$/);
|
|
83
|
+
if (bracketed)
|
|
84
|
+
return expandIpv6(stripZone(bracketed[1].toLowerCase())) ?? '';
|
|
85
|
+
// 2. A bare IPv6 literal cannot carry a port (no brackets ⇒ every colon
|
|
86
|
+
// belongs to the address), so anything with >1 colon is taken whole.
|
|
87
|
+
if (host.split(':').length > 2)
|
|
88
|
+
return expandIpv6(stripZone(host.toLowerCase())) ?? '';
|
|
89
|
+
// 3. `name` or `name:port`, where the port must actually be digits.
|
|
90
|
+
const plain = host.match(/^([^:[\]]*)(?::(\d+))?$/);
|
|
91
|
+
if (plain)
|
|
92
|
+
return plain[1].toLowerCase().replace(/\.$/, '');
|
|
93
|
+
return ''; // unparseable ⇒ matches no allowlist
|
|
94
|
+
}
|
|
95
|
+
/** Drop an IPv6 zone id (`fe80::1%eth0`), anchored to the end so a `%` inside a
|
|
96
|
+
* registrable hostname can never truncate it down to a loopback name. */
|
|
97
|
+
function stripZone(h) {
|
|
98
|
+
return h.replace(/%[a-z0-9._~-]+$/, '');
|
|
99
|
+
}
|
|
100
|
+
/** True when the hostname names this machine and nothing else. Expects the
|
|
101
|
+
* canonical output of `normalizeHostname`. */
|
|
102
|
+
function isLoopbackName(hostname) {
|
|
103
|
+
return hostname === 'localhost' || LOOPBACK_V4.test(hostname) || hostname === LOOPBACK_V6;
|
|
104
|
+
}
|
|
105
|
+
/** True for bind hosts that only the local machine can reach. Undefined = the
|
|
106
|
+
* default bind (127.0.0.1), hence trusted — this is a *configuration* value we
|
|
107
|
+
* chose, not a request header. Do NOT use this on attacker-controlled input;
|
|
108
|
+
* use `isLoopbackHostHeader`, which fails closed on a missing host. */
|
|
109
|
+
export function isLoopbackHost(host) {
|
|
110
|
+
if (!host)
|
|
111
|
+
return true;
|
|
112
|
+
return isLoopbackName(normalizeHostname(host));
|
|
113
|
+
}
|
|
114
|
+
/** True for a request's `Host`/`Origin` hostname when it names this machine.
|
|
115
|
+
* The untrusted-input twin of `isLoopbackHost`: a missing or unparseable host
|
|
116
|
+
* is **untrusted** (false), because absent is not the same as "we defaulted to
|
|
117
|
+
* loopback" once the value arrives over the wire (#426). */
|
|
118
|
+
export function isLoopbackHostHeader(host) {
|
|
119
|
+
if (!host)
|
|
120
|
+
return false;
|
|
121
|
+
return isLoopbackName(normalizeHostname(host));
|
|
122
|
+
}
|
|
123
|
+
/** `XEZ_REMOTE=1` or a non-loopback bind host ⇒ hosted mode (no local handoff).
|
|
124
|
+
* `XEZ_FOLLOWUPS=1` ⇒ the follow-up inbox exists (#471).
|
|
125
|
+
* `XEZ_AUTOMATIONS=1` ⇒ GitHub automations exist (#801).
|
|
126
|
+
*
|
|
127
|
+
* Read per request — cheap, and tests/ops can flip `XEZ_REMOTE` live. `followups` is honest
|
|
128
|
+
* per request too, but flipping it ON at runtime is only half a switch: the per-dataDir
|
|
129
|
+
* todos watch (step 2.3) is created by an SSE subscription, so connections opened while the
|
|
130
|
+
* flag was off never subscribed and get no live inbox updates until they reconnect. Hence
|
|
131
|
+
* the UI's "set XEZ_FOLLOWUPS=1 and restart xezar" wording — treat it as a boot-time flag.
|
|
132
|
+
*
|
|
133
|
+
* `automations` carries the same caveat and for the same reason: the workspace scheduler is
|
|
134
|
+
* started once, on the server's `listening` event, so flipping the flag on afterwards gates
|
|
135
|
+
* the routes open without ever starting the poller. Boot-time flag, same wording. */
|
|
136
|
+
export function resolveCapabilities(env = process.env, bindHost) {
|
|
137
|
+
const hideAllUsage = env.XEZ_HIDE_TOKEN_METRICS === '1';
|
|
138
|
+
const tokenUsageMetrics = !hideAllUsage && env.XEZ_HIDE_TOKEN_USAGE !== '1';
|
|
139
|
+
const costMetrics = !hideAllUsage && env.XEZ_HIDE_COST !== '1';
|
|
140
|
+
return {
|
|
141
|
+
localHandoff: env.XEZ_REMOTE !== '1' && isLoopbackHost(bindHost),
|
|
142
|
+
// Deliberately not re-derived here: RunManager enforces the same predicate,
|
|
143
|
+
// and two spellings of "is the inbox on" would eventually disagree.
|
|
144
|
+
followups: followupsEnabled(env),
|
|
145
|
+
singleProject: env.XEZ_SINGLE_PROJECT === '1',
|
|
146
|
+
automations: env.XEZ_AUTOMATIONS === '1',
|
|
147
|
+
tokenMetrics: tokenUsageMetrics && costMetrics,
|
|
148
|
+
tokenUsageMetrics,
|
|
149
|
+
costMetrics,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=capabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../../src/server/capabilities.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** One `checkout-progress` SSE payload (workspace-level event, step 2.8's bus).
|
|
2
|
+
* `checkoutId` is echoed from the request so a cockpit only renders its own
|
|
3
|
+
* clone — two tabs cloning at once share the one workspace stream. */
|
|
4
|
+
export interface CheckoutProgressEvent {
|
|
5
|
+
checkoutId?: string;
|
|
6
|
+
/** The target folder name, so a payload is readable without the id too. */
|
|
7
|
+
name: string;
|
|
8
|
+
phase: 'cloning' | 'done' | 'error';
|
|
9
|
+
/** One line of `git clone` progress (present on `cloning`). */
|
|
10
|
+
line?: string;
|
|
11
|
+
/** Human-readable failure (present on `error`). */
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
export type CheckoutFailure = {
|
|
15
|
+
ok: false;
|
|
16
|
+
status: 400 | 409 | 500;
|
|
17
|
+
error: string;
|
|
18
|
+
}
|
|
19
|
+
/** `gh` is missing or unauthenticated — the spec's `{ error, reason }`
|
|
20
|
+
* degradation, mirroring the GitHub pane's contract. */
|
|
21
|
+
| {
|
|
22
|
+
ok: false;
|
|
23
|
+
status: 503;
|
|
24
|
+
error: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
};
|
|
27
|
+
export type CheckoutResult = {
|
|
28
|
+
ok: true;
|
|
29
|
+
target: string;
|
|
30
|
+
name: string;
|
|
31
|
+
} | CheckoutFailure;
|
|
32
|
+
/** A GitHub repo reference the clone flow accepts. */
|
|
33
|
+
export interface RepoRef {
|
|
34
|
+
owner: string;
|
|
35
|
+
repo: string;
|
|
36
|
+
/** What `gh repo clone` is handed — always the normalized `owner/repo`, so a
|
|
37
|
+
* URL spelling can never smuggle flags or a different host past `gh`. */
|
|
38
|
+
slug: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse `owner/repo`, `https://github.com/owner/repo(.git)`, `git@github.com:owner/repo.git`
|
|
42
|
+
* or `github.com/owner/repo` into a normalized ref. `null` when it is not a
|
|
43
|
+
* GitHub repo reference — the route answers 400 rather than handing an
|
|
44
|
+
* arbitrary string to `gh`.
|
|
45
|
+
*
|
|
46
|
+
* Only github.com: `gh repo clone` would happily take an enterprise host, but
|
|
47
|
+
* this flow's contract (and its `gh`-availability degradation) is GitHub's, and
|
|
48
|
+
* silently accepting `evil.example/owner/repo` would make the "which host am I
|
|
49
|
+
* cloning from" question unanswerable from the dialog.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseRepoRef(input: string): RepoRef | null;
|
|
52
|
+
/**
|
|
53
|
+
* Validate the target folder name (the dialog lets the user edit it, and it
|
|
54
|
+
* defaults to the repo name).
|
|
55
|
+
*
|
|
56
|
+
* A name is one path segment and nothing else. `.` / `..` and anything with a
|
|
57
|
+
* separator are rejected outright rather than sanitized: a silently rewritten
|
|
58
|
+
* name would clone somewhere other than the path the dialog previewed, which
|
|
59
|
+
* is the one thing a checkout target must never do. A leading dot is refused
|
|
60
|
+
* too — a project named `.ssh` under the checkout root is not a project.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isValidCheckoutName(name: string): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Delete a partially-cloned checkout — the ONE destructive path in this module.
|
|
65
|
+
*
|
|
66
|
+
* Called only after `mkdir(target)` (non-recursive, so it succeeded only
|
|
67
|
+
* because the directory did not exist and WE created it). Even with that proof
|
|
68
|
+
* in hand, every one of these must hold or nothing is deleted:
|
|
69
|
+
*
|
|
70
|
+
* - `target` is still a real directory and NOT a symlink (`lstat`, not `stat`):
|
|
71
|
+
* between the mkdir and the failure, the directory could have been swapped
|
|
72
|
+
* for a link pointing anywhere.
|
|
73
|
+
* - `projectsDir` and `target` both resolve (`realpath`), and the resolved
|
|
74
|
+
* target's PARENT is exactly the resolved checkout root. That is strict
|
|
75
|
+
* containment (nothing outside the root) *and* a depth limit (a direct child
|
|
76
|
+
* only, never the root itself, never a nested path).
|
|
77
|
+
*
|
|
78
|
+
* Any surprise — a vanished path, an unresolvable root, a `rm` that fails —
|
|
79
|
+
* leaves the directory alone. Leaving a stray folder is a nuisance; deleting
|
|
80
|
+
* the wrong one is unrecoverable, so every ambiguous case resolves toward "do
|
|
81
|
+
* nothing".
|
|
82
|
+
*/
|
|
83
|
+
export declare function cleanupCheckout(projectsDir: string, target: string): Promise<boolean>;
|
|
84
|
+
/** Injected so the tests can drive a fake clone without a network or a `gh`
|
|
85
|
+
* binary. `dir` already exists (we created it); the runner clones INTO it. */
|
|
86
|
+
export type CloneRunner = (ref: RepoRef, dir: string, onLine: (line: string) => void, signal: AbortSignal | undefined) => Promise<{
|
|
87
|
+
ok: true;
|
|
88
|
+
} | {
|
|
89
|
+
ok: false;
|
|
90
|
+
error: string;
|
|
91
|
+
notFound?: boolean;
|
|
92
|
+
}>;
|
|
93
|
+
/**
|
|
94
|
+
* `gh repo clone <owner/repo> <dir> -- --progress`.
|
|
95
|
+
*
|
|
96
|
+
* `spawn`, not `execFile`, because the whole point of this route is that the
|
|
97
|
+
* dialog sees progress while it happens: `git clone --progress` writes its
|
|
98
|
+
* counters to stderr, and each line becomes a `checkout-progress` event.
|
|
99
|
+
* (`--progress` is needed explicitly — git suppresses it when stderr is not a
|
|
100
|
+
* TTY, which it never is here.)
|
|
101
|
+
*/
|
|
102
|
+
export declare const ghCloneRunner: CloneRunner;
|
|
103
|
+
/** `XEZ_DRY_RUN=1` — a fake clone so the dialog (and the tests) can exercise
|
|
104
|
+
* the whole flow offline: a few progress lines and a plausible repo on disk.
|
|
105
|
+
* It writes only INSIDE the directory the caller already created. */
|
|
106
|
+
export declare const dryRunCloneRunner: CloneRunner;
|
|
107
|
+
export interface CheckoutOptions {
|
|
108
|
+
/** Raw user input: `owner/repo` or a GitHub URL. */
|
|
109
|
+
url: string;
|
|
110
|
+
/** Target folder name; defaults to the repo name. */
|
|
111
|
+
name?: string | undefined;
|
|
112
|
+
/** The checkout root, ALREADY `~`-expanded by the caller. */
|
|
113
|
+
projectsDir: string;
|
|
114
|
+
onProgress: (event: CheckoutProgressEvent) => void;
|
|
115
|
+
checkoutId?: string | undefined;
|
|
116
|
+
signal?: AbortSignal | undefined;
|
|
117
|
+
/** Test seam; defaults to `gh` (or the dry-run fake under `XEZ_DRY_RUN=1`). */
|
|
118
|
+
run?: CloneRunner;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Clone a GitHub repo into `<projectsDir>/<name>`, streaming progress.
|
|
122
|
+
*
|
|
123
|
+
* Answers only when the clone has finished (the spec's "long-running: answers
|
|
124
|
+
* when the clone finishes"); the dialog's liveness comes from `onProgress`.
|
|
125
|
+
* On any failure the partially-written directory is removed and NOTHING is
|
|
126
|
+
* registered — registration is the caller's job, and only on `ok: true`.
|
|
127
|
+
*/
|
|
128
|
+
export declare function checkoutRepo(opts: CheckoutOptions): Promise<CheckoutResult>;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { lstat, mkdir, realpath, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* `POST /api/projects/checkout` — the "Add project → Clone from GitHub" flow
|
|
6
|
+
* (spec 2026-07-20-multi-project-workspace, step 4.3).
|
|
7
|
+
*
|
|
8
|
+
* The route itself (server.ts) owns the registry write; this module owns the
|
|
9
|
+
* only two things that are genuinely dangerous about cloning on the operator's
|
|
10
|
+
* behalf:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Where the clone lands.** The target is always `<projectsDir>/<name>`
|
|
13
|
+
* with `name` a single, boring path segment. It is never a nested path,
|
|
14
|
+
* never `..`, never absolute — the checkout root is a root, not a starting
|
|
15
|
+
* point for traversal.
|
|
16
|
+
* 2. **What cleanup may delete.** A failed clone must not leave a half-written
|
|
17
|
+
* directory behind (a later attempt would trip over it, and registering it
|
|
18
|
+
* would put a broken project in the sidebar). But "rm -rf the target" is a
|
|
19
|
+
* destructive path reached by a *network failure*, so it is guarded as
|
|
20
|
+
* tightly as `fs-browse.ts` guards containment — see `cleanupCheckout`.
|
|
21
|
+
*
|
|
22
|
+
* The load-bearing trick for (2) is that the target directory is created HERE,
|
|
23
|
+
* with a non-recursive `mkdir`, before `gh` ever runs. That single syscall is
|
|
24
|
+
* both the atomic existence check (EEXIST ⇒ 409, and we never touched what was
|
|
25
|
+
* there) and the proof of ownership that authorizes cleanup: we only ever
|
|
26
|
+
* delete a directory this operation is known to have created.
|
|
27
|
+
*/
|
|
28
|
+
/** How long a clone may run before it is killed. Long enough for a large repo
|
|
29
|
+
* on a slow link, short enough that a hung `gh` (an auth prompt that will
|
|
30
|
+
* never be answered — `gh` is non-interactive here, but a proxy can still
|
|
31
|
+
* stall) does not pin a request forever. */
|
|
32
|
+
const CLONE_TIMEOUT_MS = 10 * 60_000;
|
|
33
|
+
/** Progress lines kept for the error message. `git clone` is chatty and the
|
|
34
|
+
* useful part of a failure is always at the end. */
|
|
35
|
+
const ERROR_TAIL_LINES = 6;
|
|
36
|
+
/** `owner` and `repo` as GitHub itself allows them: alphanumerics, `-`, `_`,
|
|
37
|
+
* `.`. Deliberately strict — this string becomes a `gh` argv entry and half of
|
|
38
|
+
* a filesystem path, and every character outside this set is someone trying
|
|
39
|
+
* something. */
|
|
40
|
+
const NAME_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
41
|
+
/**
|
|
42
|
+
* Parse `owner/repo`, `https://github.com/owner/repo(.git)`, `git@github.com:owner/repo.git`
|
|
43
|
+
* or `github.com/owner/repo` into a normalized ref. `null` when it is not a
|
|
44
|
+
* GitHub repo reference — the route answers 400 rather than handing an
|
|
45
|
+
* arbitrary string to `gh`.
|
|
46
|
+
*
|
|
47
|
+
* Only github.com: `gh repo clone` would happily take an enterprise host, but
|
|
48
|
+
* this flow's contract (and its `gh`-availability degradation) is GitHub's, and
|
|
49
|
+
* silently accepting `evil.example/owner/repo` would make the "which host am I
|
|
50
|
+
* cloning from" question unanswerable from the dialog.
|
|
51
|
+
*/
|
|
52
|
+
export function parseRepoRef(input) {
|
|
53
|
+
const trimmed = input.trim();
|
|
54
|
+
if (trimmed === '' || trimmed.length > 512)
|
|
55
|
+
return null;
|
|
56
|
+
// Strip the scheme/host spellings down to `owner/repo`, then validate ONE
|
|
57
|
+
// shape. Doing it the other way round (a regex per spelling) is how a fourth
|
|
58
|
+
// spelling eventually gets a weaker check than the other three.
|
|
59
|
+
let path = trimmed;
|
|
60
|
+
const ssh = /^(?:ssh:\/\/)?git@github\.com[:/](.+)$/.exec(path);
|
|
61
|
+
if (ssh?.[1])
|
|
62
|
+
path = ssh[1];
|
|
63
|
+
else {
|
|
64
|
+
const https = /^(?:https?:\/\/)?(?:www\.)?github\.com\/(.+)$/.exec(path);
|
|
65
|
+
if (https?.[1])
|
|
66
|
+
path = https[1];
|
|
67
|
+
}
|
|
68
|
+
path = path.replace(/\/+$/, '').replace(/\.git$/, '');
|
|
69
|
+
const parts = path.split('/');
|
|
70
|
+
if (parts.length !== 2)
|
|
71
|
+
return null;
|
|
72
|
+
const [owner, repo] = parts;
|
|
73
|
+
if (!owner || !repo || !NAME_SEGMENT.test(owner) || !NAME_SEGMENT.test(repo))
|
|
74
|
+
return null;
|
|
75
|
+
return { owner, repo, slug: `${owner}/${repo}` };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Validate the target folder name (the dialog lets the user edit it, and it
|
|
79
|
+
* defaults to the repo name).
|
|
80
|
+
*
|
|
81
|
+
* A name is one path segment and nothing else. `.` / `..` and anything with a
|
|
82
|
+
* separator are rejected outright rather than sanitized: a silently rewritten
|
|
83
|
+
* name would clone somewhere other than the path the dialog previewed, which
|
|
84
|
+
* is the one thing a checkout target must never do. A leading dot is refused
|
|
85
|
+
* too — a project named `.ssh` under the checkout root is not a project.
|
|
86
|
+
*/
|
|
87
|
+
export function isValidCheckoutName(name) {
|
|
88
|
+
return name.length <= 128 && NAME_SEGMENT.test(name) && !name.includes('/') && !name.includes('\\');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Delete a partially-cloned checkout — the ONE destructive path in this module.
|
|
92
|
+
*
|
|
93
|
+
* Called only after `mkdir(target)` (non-recursive, so it succeeded only
|
|
94
|
+
* because the directory did not exist and WE created it). Even with that proof
|
|
95
|
+
* in hand, every one of these must hold or nothing is deleted:
|
|
96
|
+
*
|
|
97
|
+
* - `target` is still a real directory and NOT a symlink (`lstat`, not `stat`):
|
|
98
|
+
* between the mkdir and the failure, the directory could have been swapped
|
|
99
|
+
* for a link pointing anywhere.
|
|
100
|
+
* - `projectsDir` and `target` both resolve (`realpath`), and the resolved
|
|
101
|
+
* target's PARENT is exactly the resolved checkout root. That is strict
|
|
102
|
+
* containment (nothing outside the root) *and* a depth limit (a direct child
|
|
103
|
+
* only, never the root itself, never a nested path).
|
|
104
|
+
*
|
|
105
|
+
* Any surprise — a vanished path, an unresolvable root, a `rm` that fails —
|
|
106
|
+
* leaves the directory alone. Leaving a stray folder is a nuisance; deleting
|
|
107
|
+
* the wrong one is unrecoverable, so every ambiguous case resolves toward "do
|
|
108
|
+
* nothing".
|
|
109
|
+
*/
|
|
110
|
+
export async function cleanupCheckout(projectsDir, target) {
|
|
111
|
+
let realRoot;
|
|
112
|
+
let realTarget;
|
|
113
|
+
try {
|
|
114
|
+
const info = await lstat(target);
|
|
115
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
116
|
+
return false;
|
|
117
|
+
realRoot = await realpath(projectsDir);
|
|
118
|
+
realTarget = await realpath(target);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false; // gone, or unresolvable — either way, not ours to remove
|
|
122
|
+
}
|
|
123
|
+
if (realTarget === realRoot)
|
|
124
|
+
return false;
|
|
125
|
+
if (dirname(realTarget) !== realRoot)
|
|
126
|
+
return false;
|
|
127
|
+
try {
|
|
128
|
+
await rm(realTarget, { recursive: true, force: true });
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* `gh repo clone <owner/repo> <dir> -- --progress`.
|
|
137
|
+
*
|
|
138
|
+
* `spawn`, not `execFile`, because the whole point of this route is that the
|
|
139
|
+
* dialog sees progress while it happens: `git clone --progress` writes its
|
|
140
|
+
* counters to stderr, and each line becomes a `checkout-progress` event.
|
|
141
|
+
* (`--progress` is needed explicitly — git suppresses it when stderr is not a
|
|
142
|
+
* TTY, which it never is here.)
|
|
143
|
+
*/
|
|
144
|
+
export const ghCloneRunner = (ref, dir, onLine, signal) => new Promise((resolvePromise) => {
|
|
145
|
+
const child = spawn('gh', ['repo', 'clone', ref.slug, dir, '--', '--progress'], {
|
|
146
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
147
|
+
timeout: CLONE_TIMEOUT_MS,
|
|
148
|
+
// No inherited stdin and `GH_PROMPT_DISABLED`: an unauthenticated `gh`
|
|
149
|
+
// must fail with a message the dialog can show, not block on a prompt
|
|
150
|
+
// nobody can see.
|
|
151
|
+
env: { ...process.env, GH_PROMPT_DISABLED: '1', GIT_TERMINAL_PROMPT: '0' },
|
|
152
|
+
});
|
|
153
|
+
const tail = [];
|
|
154
|
+
let settled = false;
|
|
155
|
+
const finish = (result) => {
|
|
156
|
+
if (settled)
|
|
157
|
+
return;
|
|
158
|
+
settled = true;
|
|
159
|
+
resolvePromise(result);
|
|
160
|
+
};
|
|
161
|
+
// The client hung up (dialog closed, tab gone). Kill the clone rather than
|
|
162
|
+
// let it keep writing into a directory nobody is waiting for — the caller
|
|
163
|
+
// then takes the failure path, which cleans up.
|
|
164
|
+
const onAbort = () => {
|
|
165
|
+
child.kill('SIGTERM');
|
|
166
|
+
finish({ ok: false, error: 'checkout cancelled' });
|
|
167
|
+
};
|
|
168
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
169
|
+
// git's progress is carriage-return-separated, not newline-separated —
|
|
170
|
+
// splitting on `\n` alone would buffer the whole "Receiving objects" phase
|
|
171
|
+
// into one line delivered at the end, which is exactly the silent spinner
|
|
172
|
+
// this stream exists to avoid.
|
|
173
|
+
let pending = '';
|
|
174
|
+
const consume = (chunk) => {
|
|
175
|
+
pending += chunk;
|
|
176
|
+
const parts = pending.split(/\r\n|\r|\n/);
|
|
177
|
+
pending = parts.pop() ?? '';
|
|
178
|
+
for (const part of parts) {
|
|
179
|
+
const line = part.trim();
|
|
180
|
+
if (line === '')
|
|
181
|
+
continue;
|
|
182
|
+
tail.push(line);
|
|
183
|
+
if (tail.length > ERROR_TAIL_LINES)
|
|
184
|
+
tail.shift();
|
|
185
|
+
onLine(line);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
child.stderr?.setEncoding('utf8');
|
|
189
|
+
child.stderr?.on('data', consume);
|
|
190
|
+
child.stdout?.setEncoding('utf8');
|
|
191
|
+
child.stdout?.on('data', consume);
|
|
192
|
+
child.on('error', (err) => {
|
|
193
|
+
signal?.removeEventListener('abort', onAbort);
|
|
194
|
+
// ENOENT is the `gh`-not-installed case, which the route degrades on
|
|
195
|
+
// rather than reports as a clone failure.
|
|
196
|
+
const notFound = err.code === 'ENOENT';
|
|
197
|
+
finish({ ok: false, error: err.message, notFound });
|
|
198
|
+
});
|
|
199
|
+
child.on('close', (code) => {
|
|
200
|
+
signal?.removeEventListener('abort', onAbort);
|
|
201
|
+
if (code === 0)
|
|
202
|
+
return finish({ ok: true });
|
|
203
|
+
// The tail of gh/git's own output IS the error message — `gh` writes
|
|
204
|
+
// "could not find repository", "authentication required" and the network
|
|
205
|
+
// errors itself, and paraphrasing them would only lose detail.
|
|
206
|
+
const detail = tail.join('\n').trim();
|
|
207
|
+
finish({ ok: false, error: detail === '' ? `gh repo clone exited with code ${code}` : detail });
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
/** `XEZ_DRY_RUN=1` — a fake clone so the dialog (and the tests) can exercise
|
|
211
|
+
* the whole flow offline: a few progress lines and a plausible repo on disk.
|
|
212
|
+
* It writes only INSIDE the directory the caller already created. */
|
|
213
|
+
export const dryRunCloneRunner = async (ref, dir, onLine) => {
|
|
214
|
+
onLine(`Cloning into '${dir}'...`);
|
|
215
|
+
onLine('remote: Enumerating objects: 3, done.');
|
|
216
|
+
// A `.git` directory so the registered project probes as a git repo the way
|
|
217
|
+
// a real clone would — the point of the dry run is the same shape, not the
|
|
218
|
+
// same bytes.
|
|
219
|
+
await mkdir(join(dir, '.git'), { recursive: true });
|
|
220
|
+
await writeFile(join(dir, 'README.md'), `# ${ref.repo}\n\n(XEZ_DRY_RUN=1 fake clone)\n`, 'utf8');
|
|
221
|
+
onLine('Receiving objects: 100% (3/3), done.');
|
|
222
|
+
return { ok: true };
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Clone a GitHub repo into `<projectsDir>/<name>`, streaming progress.
|
|
226
|
+
*
|
|
227
|
+
* Answers only when the clone has finished (the spec's "long-running: answers
|
|
228
|
+
* when the clone finishes"); the dialog's liveness comes from `onProgress`.
|
|
229
|
+
* On any failure the partially-written directory is removed and NOTHING is
|
|
230
|
+
* registered — registration is the caller's job, and only on `ok: true`.
|
|
231
|
+
*/
|
|
232
|
+
export async function checkoutRepo(opts) {
|
|
233
|
+
const ref = parseRepoRef(opts.url);
|
|
234
|
+
if (!ref) {
|
|
235
|
+
return { ok: false, status: 400, error: `not a GitHub repository: ${opts.url.trim().slice(0, 200)}` };
|
|
236
|
+
}
|
|
237
|
+
const name = (opts.name ?? '').trim() === '' ? ref.repo : (opts.name ?? '').trim();
|
|
238
|
+
if (!isValidCheckoutName(name)) {
|
|
239
|
+
return { ok: false, status: 400, error: `not a valid folder name: ${name.slice(0, 200)}` };
|
|
240
|
+
}
|
|
241
|
+
if (!opts.projectsDir.startsWith('/')) {
|
|
242
|
+
return { ok: false, status: 500, error: `checkout root is not an absolute path: ${opts.projectsDir}` };
|
|
243
|
+
}
|
|
244
|
+
const root = resolve(opts.projectsDir);
|
|
245
|
+
const target = join(root, name);
|
|
246
|
+
// The checkout root is created on demand — a fresh install has never had one,
|
|
247
|
+
// and failing "clone" because a directory the user never asked about is
|
|
248
|
+
// missing would be an odd first experience. Its writability is validated on
|
|
249
|
+
// `PUT /api/workspace/config`; a failure here is reported as one.
|
|
250
|
+
try {
|
|
251
|
+
await mkdir(root, { recursive: true });
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
return { ok: false, status: 500, error: `checkout root is not writable: ${errText(err)}` };
|
|
255
|
+
}
|
|
256
|
+
// THE ownership token (see the module docstring): non-recursive, so it
|
|
257
|
+
// succeeds only when it created the directory itself. EEXIST is the spec's
|
|
258
|
+
// 409 — and note nothing has touched the existing directory to learn that.
|
|
259
|
+
try {
|
|
260
|
+
await mkdir(target);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
if (err.code === 'EEXIST') {
|
|
264
|
+
return { ok: false, status: 409, error: `folder already exists: ${target}` };
|
|
265
|
+
}
|
|
266
|
+
return { ok: false, status: 500, error: `could not create ${target}: ${errText(err)}` };
|
|
267
|
+
}
|
|
268
|
+
const emit = (event) => opts.onProgress({ ...event, name, ...(opts.checkoutId ? { checkoutId: opts.checkoutId } : {}) });
|
|
269
|
+
const run = opts.run ?? (process.env.XEZ_DRY_RUN === '1' ? dryRunCloneRunner : ghCloneRunner);
|
|
270
|
+
let outcome;
|
|
271
|
+
try {
|
|
272
|
+
outcome = await run(ref, target, (line) => emit({ phase: 'cloning', line }), opts.signal);
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
// A runner that throws is still a failed clone — same cleanup, same shape.
|
|
276
|
+
outcome = { ok: false, error: errText(err) };
|
|
277
|
+
}
|
|
278
|
+
if (!outcome.ok) {
|
|
279
|
+
// Cleanup FIRST, then answer: the dialog's "try again" must not race a
|
|
280
|
+
// directory that is still on disk (it would get the 409 instead).
|
|
281
|
+
await cleanupCheckout(root, target);
|
|
282
|
+
if (outcome.notFound) {
|
|
283
|
+
const reason = 'gh CLI not found — install it and run `gh auth login`';
|
|
284
|
+
emit({ phase: 'error', error: reason });
|
|
285
|
+
return { ok: false, status: 503, error: reason, reason };
|
|
286
|
+
}
|
|
287
|
+
emit({ phase: 'error', error: outcome.error });
|
|
288
|
+
return { ok: false, status: 500, error: outcome.error };
|
|
289
|
+
}
|
|
290
|
+
emit({ phase: 'done' });
|
|
291
|
+
return { ok: true, target, name };
|
|
292
|
+
}
|
|
293
|
+
function errText(err) {
|
|
294
|
+
return err instanceof Error ? err.message : String(err);
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=checkout.js.map
|