@playcraft/cli 0.1.54 → 0.1.56-beta
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/README.md +83 -15
- package/SKILLS_REGISTRY.md +149 -0
- package/dist/agent/local-backend.js +1 -1
- package/dist/assets/client.js +157 -0
- package/dist/assets/messages.js +48 -0
- package/dist/auth/account-activation.js +157 -0
- package/dist/auth/messages.js +30 -0
- package/dist/cli-root-help.js +4 -0
- package/dist/commands/assets.js +123 -0
- package/dist/commands/build-all.js +5 -19
- package/dist/commands/build-temp.js +26 -0
- package/dist/commands/build.js +53 -28
- package/dist/commands/create.js +26 -33
- package/dist/commands/image-layer-messages.js +37 -0
- package/dist/commands/image-layer-tools.js +185 -0
- package/dist/commands/image.js +2 -73
- package/dist/commands/library-messages.js +46 -0
- package/dist/commands/library.js +130 -0
- package/dist/commands/login.js +84 -42
- package/dist/commands/platform-skills.generated.js +19 -0
- package/dist/commands/playable.js +53 -0
- package/dist/commands/preview-runtime.js +21 -0
- package/dist/commands/skills.js +4 -0
- package/dist/commands/tools-3d.js +11 -0
- package/dist/commands/tools-batch.js +113 -0
- package/dist/commands/tools-generation.js +6 -0
- package/dist/commands/tools.js +2 -0
- package/dist/commands/video.js +122 -0
- package/dist/commands/workspace-runtime.js +54 -0
- package/dist/commands/workspace.js +40 -0
- package/dist/config.js +59 -4
- package/dist/index.js +47 -3
- package/dist/preview-runtime/distribution.js +77 -0
- package/dist/project-skills/builtin-import.js +225 -0
- package/dist/project-skills/commands.js +77 -0
- package/dist/project-skills/lifecycle.js +28 -0
- package/dist/project-skills/local-run.js +112 -0
- package/dist/project-skills/local-store.js +221 -0
- package/dist/project-skills/messages.js +80 -0
- package/dist/project-skills/reconcile.js +62 -0
- package/dist/project-skills/remote-cache.js +51 -0
- package/dist/project-skills/validation.js +19 -0
- package/dist/recording/messages.js +40 -0
- package/dist/recording/project-digest.js +85 -0
- package/dist/recording/recorder.js +293 -0
- package/dist/recording/scenario.js +50 -0
- package/dist/remix/clone.js +2 -0
- package/dist/remix/init-template.js +2 -0
- package/dist/remix/pull.js +2 -0
- package/dist/remix/push.js +21 -1
- package/dist/skill-registry/client.js +345 -0
- package/dist/skill-registry/commands.js +180 -0
- package/dist/skill-registry/evaluation-client.js +156 -0
- package/dist/skill-registry/evaluation-commands.js +174 -0
- package/dist/skill-registry/messages.js +177 -0
- package/dist/skill-registry/package.js +275 -0
- package/dist/skill-registry/platform-package.js +100 -0
- package/dist/utils/agent-api-client.js +245 -30
- package/dist/utils/environment-endpoints.js +17 -0
- package/dist/utils/error-diagnostics.js +20 -0
- package/dist/utils/playcraft-http-client.js +6 -4
- package/dist/utils/tool-operation-journal.js +124 -0
- package/dist/utils/video-evidence-git.js +81 -0
- package/dist/video-replication/client.js +234 -0
- package/dist/video-replication/comparison-evidence.js +149 -0
- package/dist/video-replication/comparison-files.js +57 -0
- package/dist/video-replication/comparison-log.js +72 -0
- package/dist/video-replication/comparison-messages.js +104 -0
- package/dist/video-replication/comparison-output.js +63 -0
- package/dist/video-replication/comparison-preflight.js +66 -0
- package/dist/video-replication/comparison.js +230 -0
- package/dist/video-replication/diagnostics.js +41 -0
- package/dist/video-replication/evidence.js +165 -0
- package/dist/video-replication/media.js +112 -0
- package/dist/video-replication/messages.js +97 -0
- package/dist/workspace/messages.js +67 -0
- package/dist/workspace/transfer-client.js +237 -0
- package/dist/workspace/transfer-errors.js +48 -0
- package/dist/workspace-runtime/batch/content-changes.js +30 -0
- package/dist/workspace-runtime/batch/control.js +86 -0
- package/dist/workspace-runtime/batch/deliver.js +173 -0
- package/dist/workspace-runtime/batch/freeze.js +101 -0
- package/dist/workspace-runtime/batch/generate.js +249 -0
- package/dist/workspace-runtime/batch/git.js +40 -0
- package/dist/workspace-runtime/batch/occupancy.js +8 -0
- package/dist/workspace-runtime/batch/policy.js +70 -0
- package/dist/workspace-runtime/batch/prepare.js +317 -0
- package/dist/workspace-runtime/batch/processes.js +72 -0
- package/dist/workspace-runtime/batch/records.js +113 -0
- package/dist/workspace-runtime/batch/routes.js +61 -0
- package/dist/workspace-runtime/batch/source-candidate.js +86 -0
- package/dist/workspace-runtime/batch/source-capture.js +146 -0
- package/dist/workspace-runtime/batch/source-failure.js +43 -0
- package/dist/workspace-runtime/batch/source-publish.js +130 -0
- package/dist/workspace-runtime/codex/app-server.js +1065 -0
- package/dist/workspace-runtime/codex/compact.js +46 -0
- package/dist/workspace-runtime/codex/config-toml.js +107 -0
- package/dist/workspace-runtime/codex/delivery-tools.js +144 -0
- package/dist/workspace-runtime/codex/jsonrpc-stdio.js +145 -0
- package/dist/workspace-runtime/codex/launcher-client.js +177 -0
- package/dist/workspace-runtime/codex/loopback.js +146 -0
- package/dist/workspace-runtime/codex/mcp-config.js +39 -0
- package/dist/workspace-runtime/codex/model-broker.js +263 -0
- package/dist/workspace-runtime/codex/native-adapter.js +9 -0
- package/dist/workspace-runtime/codex/permissions.js +79 -0
- package/dist/workspace-runtime/codex/thread-image.js +38 -0
- package/dist/workspace-runtime/codex/tool-response.js +10 -0
- package/dist/workspace-runtime/codex/ui-instructions.js +4 -0
- package/dist/workspace-runtime/codex/ui-tools.js +203 -0
- package/dist/workspace-runtime/codex/work-mode.js +15 -0
- package/dist/workspace-runtime/main.js +10 -0
- package/dist/workspace-runtime/persistence/journal.js +365 -0
- package/dist/workspace-runtime/persistence/native-subagents.js +162 -0
- package/dist/workspace-runtime/persistence/sqlite-client.js +77 -0
- package/dist/workspace-runtime/persistence/sqlite-worker.js +1161 -0
- package/dist/workspace-runtime/persistence/stopped-execution.js +77 -0
- package/dist/workspace-runtime/previews/diagnostics.js +44 -0
- package/dist/workspace-runtime/previews/finalize.js +64 -0
- package/dist/workspace-runtime/previews/output.js +36 -0
- package/dist/workspace-runtime/previews/publish.js +306 -0
- package/dist/workspace-runtime/previews/runtime-preview.js +251 -0
- package/dist/workspace-runtime/previews/single-file.js +43 -0
- package/dist/workspace-runtime/processes/managed-writes.js +69 -0
- package/dist/workspace-runtime/serve.js +441 -0
- package/dist/workspace-runtime/server/accept-operation.js +61 -0
- package/dist/workspace-runtime/server/asset-transfers.js +63 -0
- package/dist/workspace-runtime/server/auth.js +21 -0
- package/dist/workspace-runtime/server/connectivity.js +211 -0
- package/dist/workspace-runtime/server/desktop-host.js +429 -0
- package/dist/workspace-runtime/server/desktop-preview-config.js +81 -0
- package/dist/workspace-runtime/server/desktop-preview-service.js +157 -0
- package/dist/workspace-runtime/server/desktop-preview-static.js +53 -0
- package/dist/workspace-runtime/server/desktop-preview.js +189 -0
- package/dist/workspace-runtime/server/dispatch.js +1655 -0
- package/dist/workspace-runtime/server/endpoints.js +21 -0
- package/dist/workspace-runtime/server/execution-group.js +90 -0
- package/dist/workspace-runtime/server/http.js +1363 -0
- package/dist/workspace-runtime/server/lock.js +53 -0
- package/dist/workspace-runtime/server/operation-record.js +33 -0
- package/dist/workspace-runtime/server/rfb-watch-gate.js +168 -0
- package/dist/workspace-runtime/server/session-restore.js +84 -0
- package/dist/workspace-runtime/server/source-uploader.js +138 -0
- package/dist/workspace-runtime/server/submit-operation.js +41 -0
- package/dist/workspace-runtime/server/terminal-host.js +366 -0
- package/dist/workspace-runtime/server/types.js +1 -0
- package/dist/workspace-runtime/server/workspace-transfer.js +168 -0
- package/dist/workspace-runtime/verify-artifact.js +59 -0
- package/dist/workspace-runtime/workspaces/append-only-log.js +62 -0
- package/dist/workspace-runtime/workspaces/archive.js +207 -0
- package/dist/workspace-runtime/workspaces/artifact-events.js +168 -0
- package/dist/workspace-runtime/workspaces/artifact-files.js +201 -0
- package/dist/workspace-runtime/workspaces/artifact-receipts.js +112 -0
- package/dist/workspace-runtime/workspaces/artifacts.js +380 -0
- package/dist/workspace-runtime/workspaces/asset-library-import.js +387 -0
- package/dist/workspace-runtime/workspaces/asset-publication-access.js +29 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-dependencies.js +81 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +438 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +389 -0
- package/dist/workspace-runtime/workspaces/attachment-content.js +73 -0
- package/dist/workspace-runtime/workspaces/attachments.js +185 -0
- package/dist/workspace-runtime/workspaces/batch-commit.js +142 -0
- package/dist/workspace-runtime/workspaces/binary-content.js +65 -0
- package/dist/workspace-runtime/workspaces/binary-write.js +117 -0
- package/dist/workspace-runtime/workspaces/cli-context.js +23 -0
- package/dist/workspace-runtime/workspaces/cli-credentials.js +115 -0
- package/dist/workspace-runtime/workspaces/composer-context.js +147 -0
- package/dist/workspace-runtime/workspaces/context-error.js +2 -0
- package/dist/workspace-runtime/workspaces/delivery-build.js +421 -0
- package/dist/workspace-runtime/workspaces/delivery-store.js +168 -0
- package/dist/workspace-runtime/workspaces/delivery.js +64 -0
- package/dist/workspace-runtime/workspaces/dependencies.js +279 -0
- package/dist/workspace-runtime/workspaces/developer-command.js +98 -0
- package/dist/workspace-runtime/workspaces/directories.js +135 -0
- package/dist/workspace-runtime/workspaces/directory-access.js +50 -0
- package/dist/workspace-runtime/workspaces/directory-attachments.js +245 -0
- package/dist/workspace-runtime/workspaces/directory-create.js +57 -0
- package/dist/workspace-runtime/workspaces/directory-maintenance.js +111 -0
- package/dist/workspace-runtime/workspaces/directory-mutations.js +238 -0
- package/dist/workspace-runtime/workspaces/directory-revisions.js +56 -0
- package/dist/workspace-runtime/workspaces/directory-stats.js +49 -0
- package/dist/workspace-runtime/workspaces/disk-snapshot.js +31 -0
- package/dist/workspace-runtime/workspaces/empty-template.js +43 -0
- package/dist/workspace-runtime/workspaces/file-content.js +48 -0
- package/dist/workspace-runtime/workspaces/files-search.js +116 -0
- package/dist/workspace-runtime/workspaces/files.js +150 -0
- package/dist/workspace-runtime/workspaces/git-credential-broker.js +312 -0
- package/dist/workspace-runtime/workspaces/git-credential-helper.js +68 -0
- package/dist/workspace-runtime/workspaces/git-direct-store.js +155 -0
- package/dist/workspace-runtime/workspaces/git.js +683 -0
- package/dist/workspace-runtime/workspaces/instructions.js +56 -0
- package/dist/workspace-runtime/workspaces/json5-edit.js +313 -0
- package/dist/workspace-runtime/workspaces/library-attachments.js +268 -0
- package/dist/workspace-runtime/workspaces/operation-log.js +23 -0
- package/dist/workspace-runtime/workspaces/optional-platform-skills.js +14 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-inputs.js +97 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +40 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-store.js +58 -0
- package/dist/workspace-runtime/workspaces/parameter-agent.js +284 -0
- package/dist/workspace-runtime/workspaces/parameter-assets.js +212 -0
- package/dist/workspace-runtime/workspaces/parameters.js +369 -0
- package/dist/workspace-runtime/workspaces/prepare.js +284 -0
- package/dist/workspace-runtime/workspaces/registry.js +251 -0
- package/dist/workspace-runtime/workspaces/remix-references.js +301 -0
- package/dist/workspace-runtime/workspaces/revisions.js +31 -0
- package/dist/workspace-runtime/workspaces/session-image.js +104 -0
- package/dist/workspace-runtime/workspaces/shared-git.js +40 -0
- package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +95 -0
- package/dist/workspace-runtime/workspaces/skills.js +191 -0
- package/dist/workspace-runtime/workspaces/theme-asset-index.js +267 -0
- package/dist/workspace-runtime/workspaces/transfer-quota.js +69 -0
- package/dist/workspace-runtime/workspaces/transfer.js +471 -0
- package/dist/workspace-runtime/workspaces/turn-changes.js +75 -0
- package/dist/workspace-runtime/workspaces/video-attachments.js +49 -0
- package/dist/workspace-runtime/workspaces/worktree-attachments.js +74 -0
- package/dist/workspace-runtime/workspaces/worktree-checkout.js +67 -0
- package/dist/workspace-runtime/workspaces/worktrees.js +73 -0
- package/package.json +16 -5
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +31 -4
- package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +36 -5
- package/project-template/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +1 -1
- package/project-template/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +1 -1
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +1 -1
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +1 -1
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +1 -1
- package/project-template/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +1 -1
- package/project-template/.claude/skills/playcraft-image-generation/reference/ta-image-generation-detail.md +300 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +1 -1
- package/project-template/.claude/skills/playcraft-image-processing/SKILL.md +50 -25
- package/project-template/.claude/skills/playcraft-replica-validation/SKILL.md +25 -0
- package/project-template/.claude/skills/playcraft-replica-validation/references/contracts.md +79 -0
- package/project-template/.claude/skills/playcraft-video-understanding/SKILL.md +29 -0
- package/project-template/.claude/skills/playcraft-video-understanding/references/contracts.md +37 -0
- package/project-template/.claude/skills/playcraft-workspace-assets/SKILL.md +53 -0
- package/project-template/.claude/skills/playcraft-workspace-assets/references/requests.json +32 -0
- package/project-template-v2/.claude/agents/artist.md +82 -0
- package/project-template-v2/.claude/agents/developer.md +153 -0
- package/project-template-v2/.claude/agents/game-designer.md +264 -0
- package/project-template-v2/.claude/agents/refs/artist-art-style-catalog.md +533 -0
- package/project-template-v2/.claude/agents/refs/artist-color-audio-recipes.md +153 -0
- package/project-template-v2/.claude/agents/refs/artist-dimension-axis.md +27 -0
- package/project-template-v2/.claude/agents/refs/artist-master-composite-recipes.md +208 -0
- package/project-template-v2/.claude/agents/refs/atom-skill-library.md +81 -0
- package/project-template-v2/.claude/agents/refs/developer-impl-cookbook.md +432 -0
- package/project-template-v2/.claude/agents/refs/framework-5-component-filter.md +252 -0
- package/project-template-v2/.claude/agents/refs/framework-game-feel-juice.md +266 -0
- package/project-template-v2/.claude/agents/refs/framework-mda.md +147 -0
- package/project-template-v2/.claude/agents/refs/game-designer-gameplay-sufficiency.md +123 -0
- package/project-template-v2/.claude/agents/refs/ta-3d-flip-recipe.md +88 -0
- package/project-template-v2/.claude/agents/refs/ta-atlas-deliverable-standard.md +67 -0
- package/project-template-v2/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
- package/project-template-v2/.claude/agents/refs/ta-image-generation-detail.md +300 -0
- package/project-template-v2/.claude/agents/refs/ta-image-ops-reference.md +495 -0
- package/project-template-v2/.claude/agents/refs/ta-pipeline-cookbook.md +1141 -0
- package/project-template-v2/.claude/agents/refs/ta-tools-reference.md +111 -0
- package/project-template-v2/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
- package/project-template-v2/.claude/agents/refs/threejs-cannon-pitfalls.md +412 -0
- package/project-template-v2/.claude/agents/reviewer.md +75 -0
- package/project-template-v2/.claude/agents/technical-artist.md +86 -0
- package/project-template-v2/.claude/hooks/snapshot-milestone.mjs +243 -0
- package/project-template-v2/.claude/hooks/user-prompt.mjs +133 -0
- package/project-template-v2/.claude/settings.json +33 -0
- package/project-template-v2/.claude/skills/brainstorming/SKILL.md +161 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/frame-template.html +270 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/helper.js +177 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/server.cjs +354 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/start-server.sh +148 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/stop-server.sh +56 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/wait-for-selection.sh +62 -0
- package/project-template-v2/.claude/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/project-template-v2/.claude/skills/brainstorming/visual-companion.md +309 -0
- package/project-template-v2/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
- package/project-template-v2/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
- package/project-template-v2/.claude/skills/playcraft-asset-state-sheet/SKILL.md +205 -0
- package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +307 -0
- package/project-template-v2/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
- package/project-template-v2/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/SKILL.md +401 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +333 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +95 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +136 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/ta-image-generation-detail.md +300 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
- package/project-template-v2/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
- package/project-template-v2/.claude/skills/playcraft-image-processing/SKILL.md +244 -0
- package/project-template-v2/.claude/skills/playcraft-masking/SKILL.md +373 -0
- package/project-template-v2/.claude/skills/playcraft-playable-optimization/SKILL.md +161 -0
- package/project-template-v2/.claude/skills/playcraft-research/SKILL.md +215 -0
- package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +382 -0
- package/project-template-v2/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
- package/project-template-v2/.claude/skills/playcraft-sprite-remix/SKILL.md +158 -0
- package/project-template-v2/.claude/skills/playcraft-sprite-sheet/SKILL.md +100 -0
- package/project-template-v2/.claude/skills/playcraft-storyboard/SKILL.md +167 -0
- package/project-template-v2/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
- package/project-template-v2/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
- package/project-template-v2/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
- package/project-template-v2/.claude/skills/playcraft-workspace-assets/SKILL.md +53 -0
- package/project-template-v2/.claude/skills/playcraft-workspace-assets/references/requests.json +32 -0
- package/project-template-v2/.claude/skills/playwright-cli/SKILL.md +390 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/running-code.md +240 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/session-management.md +226 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/spec-driven-testing.md +308 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/storage-state.md +275 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/test-generation.md +134 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/tracing.md +142 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/video-recording.md +153 -0
- package/project-template-v2/.claude/skills/session-analyzer/SKILL.md +386 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/execution-breakdown.mjs +182 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/find-turns.mjs +72 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/heavy-output.mjs +121 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/resolve-session.mjs +102 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-stats.mjs +127 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-tool-timeline.mjs +106 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/time-gaps.mjs +128 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/turn-timeline.mjs +67 -0
- package/project-template-v2/.claude/snapshot.mjs +263 -0
- package/project-template-v2/.playcraft/skills.lock.json +152 -0
- package/project-template-v2/CLAUDE.md +146 -0
- package/project-template-v2/assets/audio/bgm/.gitkeep +0 -0
- package/project-template-v2/assets/audio/sfx/.gitkeep +0 -0
- package/project-template-v2/assets/bundles/.gitkeep +0 -0
- package/project-template-v2/assets/images/bg/.gitkeep +0 -0
- package/project-template-v2/assets/images/reference/.gitkeep +0 -0
- package/project-template-v2/assets/images/storyboard/.gitkeep +0 -0
- package/project-template-v2/assets/images/tiles/.gitkeep +0 -0
- package/project-template-v2/assets/images/ui/.gitkeep +0 -0
- package/project-template-v2/assets/images/vfx/.gitkeep +0 -0
- package/project-template-v2/assets/models/.gitkeep +0 -0
- package/project-template-v2/docs/harness/iteration-1/context-flow.md +254 -0
- package/project-template-v2/docs/harness/iteration-1/generate-flow.md +91 -0
- package/project-template-v2/docs/harness/iteration-1/ideate-flow.md +214 -0
- package/project-template-v2/docs/harness/iteration-1/optimize-flow.md +75 -0
- package/project-template-v2/docs/harness/iteration-1/wrapup-flow.md +63 -0
- package/project-template-v2/docs/harness/iteration-2/context-flow.md +223 -0
- package/project-template-v2/docs/harness/iteration-2/generate-flow.md +129 -0
- package/project-template-v2/docs/harness/iteration-2/ideate-flow.md +267 -0
- package/project-template-v2/docs/harness/iteration-2/optimize-flow.md +164 -0
- package/project-template-v2/docs/harness/iteration-2/wrapup-flow.md +115 -0
- package/project-template-v2/docs/harness/orchestrator-flow.md +364 -0
- package/project-template-v2/docs/project-state.json +60 -0
- package/project-template-v2/docs/project-state.md +72 -0
- package/project-template-v2/docs/standards/README.md +225 -0
- package/project-template-v2/docs/standards/agent-behavior-standards.md +174 -0
- package/project-template-v2/docs/standards/artifacts/design-brief.md +19 -0
- package/project-template-v2/docs/standards/artifacts/design.md +22 -0
- package/project-template-v2/docs/standards/artifacts/game-code.md +41 -0
- package/project-template-v2/docs/standards/artifacts/todo-list.md +41 -0
- package/project-template-v2/docs/standards/iter1-agent-behavior-standards.md +343 -0
- package/project-template-v2/game/index.ts +18 -0
- package/project-template-v2/globals.d.ts +51 -0
- package/project-template-v2/index.css +34 -0
- package/project-template-v2/index.html +18 -0
- package/project-template-v2/main.ts +9 -0
- package/project-template-v2/package.json +46 -0
- package/project-template-v2/skills/_shared/scripts/dispatch-clear.mjs +31 -0
- package/project-template-v2/skills/_shared/scripts/dispatch-set.mjs +86 -0
- package/project-template-v2/skills/_shared/scripts/dod-check.mjs +153 -0
- package/project-template-v2/skills/_shared/scripts/handoff-append.mjs +70 -0
- package/project-template-v2/skills/_shared/scripts/lib/validator-artifacts.mjs +91 -0
- package/project-template-v2/skills/_shared/scripts/pipeline/dod-config.mjs +131 -0
- package/project-template-v2/skills/_shared/scripts/pipeline/index.mjs +80 -0
- package/project-template-v2/skills/_shared/scripts/pipeline/iteration-1-core.mjs +90 -0
- package/project-template-v2/skills/_shared/scripts/pipeline/iteration-2-wrap.mjs +93 -0
- package/project-template-v2/skills/_shared/scripts/pipeline/iteration-3-visual.mjs +23 -0
- package/project-template-v2/skills/_shared/scripts/render-project-state.mjs +230 -0
- package/project-template-v2/skills/_shared/scripts/state-advance-stage.mjs +58 -0
- package/project-template-v2/skills/_shared/scripts/state-get.mjs +339 -0
- package/project-template-v2/skills/_shared/scripts/state-handoff.mjs +39 -0
- package/project-template-v2/skills/_shared/scripts/state-set.mjs +94 -0
- package/project-template-v2/skills/_shared/scripts/state-store.mjs +783 -0
- package/project-template-v2/skills/_shared/scripts/todo-add.mjs +88 -0
- package/project-template-v2/skills/_shared/scripts/todo-get.mjs +130 -0
- package/project-template-v2/skills/_shared/scripts/todo-remove.mjs +33 -0
- package/project-template-v2/skills/_shared/scripts/todo-set.mjs +47 -0
- package/project-template-v2/skills/_shared/scripts/verify-asset-code-sync.mjs +268 -0
- package/project-template-v2/skills/_shared/scripts/verify-env.mjs +285 -0
- package/project-template-v2/skills/_shared/scripts/verify-placeholders.mjs +161 -0
- package/project-template-v2/skills/playable-autoplay/SKILL.md +176 -0
- package/project-template-v2/skills/playable-autoplay/agents/openai.yaml +4 -0
- package/project-template-v2/skills/playable-debug/SKILL.md +116 -0
- package/project-template-v2/skills/playable-debug/agents/openai.yaml +4 -0
- package/project-template-v2/skills/playable-debug/references/debug-config.md +40 -0
- package/project-template-v2/skills/playable-record/SKILL.md +140 -0
- package/project-template-v2/skills/playable-record/scripts/lib/dev-server.mjs +104 -0
- package/project-template-v2/skills/playable-record/scripts/lib/record-audio-bridge.js +141 -0
- package/project-template-v2/skills/playable-record/scripts/record-playable.mjs +425 -0
- package/project-template-v2/skills/playable-record/scripts/verify-contract.mjs +261 -0
- package/project-template-v2/skills/playable-record/scripts/verify-firstwin.mjs +398 -0
- package/project-template-v2/skills/playable-record/scripts/verify-fusion.mjs +322 -0
- package/project-template-v2/skills/playable-record/scripts/verify-lifecycle.mjs +105 -0
- package/project-template-v2/skills/playable-record/scripts/verify-vlm-video.mjs +230 -0
- package/project-template-v2/skills/playable-validate/SKILL.md +61 -0
- package/project-template-v2/skills/playable-validate/validation-rules.md +14 -0
- package/project-template-v2/skills/playable-verify-ui/SKILL.md +63 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/__init__.py +1 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/config_expr.js +218 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/node_utils.js +51 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/profile_loader.py +59 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/report.py +106 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/runtime_contract.js +116 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_fetch_antipatterns.py +59 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_hardcoded_layout.js +64 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_runtime_contract.js +58 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_text_overlap.py +333 -0
- package/project-template-v2/ta-workspace/scripts/.gitkeep +0 -0
- package/project-template-v2/tsconfig.json +20 -0
- package/project-template-v2/vite.config.ts +27 -0
- package/project-templates/batch-locks/phaser.yaml +922 -0
- package/project-templates/batch-locks/threejs.yaml +978 -0
- package/project-templates/empty/AGENTS.md +53 -0
- package/project-templates/empty/gitignore +33 -0
- package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +31 -4
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/SKILL.md +36 -5
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +1 -1
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +1 -1
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +1 -1
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +1 -1
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +1 -1
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +1 -1
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/ta-image-generation-detail.md +300 -0
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +1 -1
- package/project-templates/phaser/.claude/skills/playcraft-image-processing/SKILL.md +50 -25
- package/project-templates/phaser/.claude/skills/playcraft-workspace-assets/SKILL.md +53 -0
- package/project-templates/phaser/.claude/skills/playcraft-workspace-assets/references/requests.json +32 -0
- package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +31 -4
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/SKILL.md +36 -5
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/ta-image-generation-detail.md +300 -0
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-image-processing/SKILL.md +50 -25
- package/project-templates/threejs/.claude/skills/playcraft-workspace-assets/SKILL.md +53 -0
- package/project-templates/threejs/.claude/skills/playcraft-workspace-assets/references/requests.json +32 -0
- package/templates/preview-runtime/v1/README.md +79 -0
- package/templates/preview-runtime/v1/build-preview.mjs +1324 -0
- package/templates/preview-runtime/v1/manifest.json +17 -0
- package/templates/preview-runtime/v1/release-guard.mjs +87 -0
- package/templates/preview-runtime/v1/runtime.d.mts +3 -0
- package/templates/preview-runtime/v1/runtime.d.ts +3 -0
- package/templates/preview-runtime/v1/runtime.mjs +1312 -0
- package/templates/preview-runtime/v1/types/adapters.d.ts +28 -0
- package/templates/preview-runtime/v1/types/preview.d.ts +24 -0
- package/templates/preview-runtime/v1/types/protocol.d.ts +131 -0
- package/templates/preview-runtime/v1/types/runtime.d.ts +14 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { constants, createWriteStream } from 'node:fs';
|
|
4
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, open, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
7
|
+
import { Transform } from 'node:stream';
|
|
8
|
+
import { pipeline } from 'node:stream/promises';
|
|
9
|
+
import { isCredentialFile } from './git.js';
|
|
10
|
+
const EXCLUDED = new Set(['.git', 'node_modules', '.playcraft-workspace.json', '.playcraft.json']);
|
|
11
|
+
const GIT_TIMEOUT_MS = 15 * 60_000;
|
|
12
|
+
const GIT_OPTIONS = ['-c', 'core.hooksPath=/dev/null', '-c', 'commit.gpgsign=false', '-c', 'core.autocrlf=false',
|
|
13
|
+
'-c', 'user.name=PlayCraft Workspace', '-c', 'user.email=workspace@playcraft.local'];
|
|
14
|
+
const SNAPSHOT_MESSAGE = 'PlayCraft workspace snapshot v1';
|
|
15
|
+
export const RAW_ATTRIBUTES = '* -text -eol -working-tree-encoding -filter -ident\n';
|
|
16
|
+
export class WorkspaceTransferError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
details;
|
|
19
|
+
constructor(code, details) {
|
|
20
|
+
super(code);
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.details = details;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function gitEnvironment() {
|
|
26
|
+
return { PATH: process.env.PATH, HOME: tmpdir(), GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null',
|
|
27
|
+
GIT_TERMINAL_PROMPT: '0', GIT_ALLOW_PROTOCOL: 'file', GIT_AUTHOR_NAME: 'PlayCraft Workspace',
|
|
28
|
+
GIT_AUTHOR_EMAIL: 'workspace@playcraft.local', GIT_COMMITTER_NAME: 'PlayCraft Workspace',
|
|
29
|
+
GIT_COMMITTER_EMAIL: 'workspace@playcraft.local', GIT_AUTHOR_DATE: '2000-01-01T00:00:00Z',
|
|
30
|
+
GIT_COMMITTER_DATE: '2000-01-01T00:00:00Z' };
|
|
31
|
+
}
|
|
32
|
+
function excluded(path) {
|
|
33
|
+
const parts = path.split('/');
|
|
34
|
+
return parts.some(part => EXCLUDED.has(part.toLowerCase()) || /^\.playcraft-(?:credential|transfer)-.*\.tmp$/i.test(part)) ||
|
|
35
|
+
parts.some((part, index) => part === '.playcraft' && /^(?:workspace-context(?:\..*)?|video-replication)$/.test(parts[index + 1] ?? '')) ||
|
|
36
|
+
isCredentialFile(path);
|
|
37
|
+
}
|
|
38
|
+
export function transferGit(root, args) {
|
|
39
|
+
const result = spawnSync('git', [...GIT_OPTIONS, ...args], { cwd: root, encoding: 'utf8', env: gitEnvironment(), timeout: GIT_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024 });
|
|
40
|
+
if (result.status !== 0) {
|
|
41
|
+
const code = result.error?.code;
|
|
42
|
+
throw new WorkspaceTransferError(code === 'ETIMEDOUT' ? 'workspace_transfer_git_timeout'
|
|
43
|
+
: code === 'ENOENT' ? 'workspace_transfer_git_missing' : 'workspace_transfer_git_failed');
|
|
44
|
+
}
|
|
45
|
+
return result.stdout.trim();
|
|
46
|
+
}
|
|
47
|
+
/** Large object operations write directly to disk and remain cancellable. */
|
|
48
|
+
export async function runTransferGit(root, args, options = {}) {
|
|
49
|
+
options.signal?.throwIfAborted();
|
|
50
|
+
return new Promise((resolveResult, reject) => {
|
|
51
|
+
const child = spawn('git', [...GIT_OPTIONS, ...args], { cwd: root, env: gitEnvironment(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
52
|
+
let stdout = '', stderr = '', failure, timedOut = false;
|
|
53
|
+
const abort = () => { failure = options.signal?.reason ?? new Error('workspace_transfer_cancelled'); child.kill('SIGKILL'); };
|
|
54
|
+
const timeout = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, GIT_TIMEOUT_MS);
|
|
55
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
56
|
+
child.stdout.setEncoding('utf8');
|
|
57
|
+
child.stderr.setEncoding('utf8');
|
|
58
|
+
child.stdout.on('data', (chunk) => {
|
|
59
|
+
try {
|
|
60
|
+
if (options.onStdout)
|
|
61
|
+
options.onStdout(chunk);
|
|
62
|
+
else {
|
|
63
|
+
stdout += chunk;
|
|
64
|
+
if (stdout.length > 8 * 1024 * 1024)
|
|
65
|
+
throw new WorkspaceTransferError('workspace_transfer_git_output_too_large');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
failure = error;
|
|
70
|
+
child.kill('SIGKILL');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
child.stderr.on('data', (chunk) => { if (stderr.length < 4096)
|
|
74
|
+
stderr += chunk.slice(0, 4096 - stderr.length); });
|
|
75
|
+
child.once('error', error => {
|
|
76
|
+
failure = new WorkspaceTransferError(error.code === 'ENOENT'
|
|
77
|
+
? 'workspace_transfer_git_missing' : 'workspace_transfer_git_failed');
|
|
78
|
+
});
|
|
79
|
+
child.once('close', code => {
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
options.signal?.removeEventListener('abort', abort);
|
|
82
|
+
if (failure)
|
|
83
|
+
reject(failure);
|
|
84
|
+
else if (timedOut)
|
|
85
|
+
reject(new WorkspaceTransferError('workspace_transfer_git_timeout', { operation: args[0] }));
|
|
86
|
+
else if (code !== 0)
|
|
87
|
+
reject(new WorkspaceTransferError(/no space left|disk full/i.test(stderr)
|
|
88
|
+
? 'workspace_transfer_disk_full' : 'workspace_transfer_git_failed', { operation: args[0], exitCode: code }));
|
|
89
|
+
else
|
|
90
|
+
resolveResult(stdout.trim());
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function copySnapshot(source, target, signal, hashOnly = false) {
|
|
95
|
+
let bytes = 0, files = 0;
|
|
96
|
+
async function copy(directory, prefix) {
|
|
97
|
+
signal?.throwIfAborted();
|
|
98
|
+
const treeParts = [];
|
|
99
|
+
const directoryInfo = await lstat(directory);
|
|
100
|
+
const directoryReal = await realpath(directory);
|
|
101
|
+
if (!directoryInfo.isDirectory() || directoryInfo.isSymbolicLink() ||
|
|
102
|
+
(directoryReal !== source && !directoryReal.startsWith(`${source}${sep}`)))
|
|
103
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
104
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
105
|
+
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
106
|
+
if (excluded(path))
|
|
107
|
+
continue;
|
|
108
|
+
signal?.throwIfAborted();
|
|
109
|
+
const from = join(directory, entry.name), to = target ? join(target, path) : undefined;
|
|
110
|
+
const info = await lstat(from);
|
|
111
|
+
if (info.isSymbolicLink())
|
|
112
|
+
continue;
|
|
113
|
+
if (info.isDirectory()) {
|
|
114
|
+
if (to)
|
|
115
|
+
await mkdir(to, { recursive: true });
|
|
116
|
+
const child = await copy(from, path);
|
|
117
|
+
if (child && child !== EMPTY_TREE)
|
|
118
|
+
treeParts.push({ name: entry.name, directory: true,
|
|
119
|
+
value: Buffer.concat([Buffer.from(`40000 ${entry.name}\0`), Buffer.from(child, 'hex')]) });
|
|
120
|
+
}
|
|
121
|
+
else if (info.isFile()) {
|
|
122
|
+
bytes += info.size;
|
|
123
|
+
files++;
|
|
124
|
+
if (!(await realpath(from)).startsWith(`${source}${sep}`))
|
|
125
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
126
|
+
if (!to && !hashOnly)
|
|
127
|
+
continue;
|
|
128
|
+
const handle = await open(from, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
129
|
+
try {
|
|
130
|
+
const before = await handle.stat();
|
|
131
|
+
if (!before.isFile() || before.dev !== info.dev || before.ino !== info.ino || before.size !== info.size ||
|
|
132
|
+
before.mtimeMs !== info.mtimeMs || before.ctimeMs !== info.ctimeMs)
|
|
133
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
134
|
+
let size = 0;
|
|
135
|
+
const hash = hashOnly ? createHash('sha1').update(`blob ${before.size}\0`) : undefined;
|
|
136
|
+
if (to) {
|
|
137
|
+
const guard = new Transform({ transform(chunk, _encoding, callback) {
|
|
138
|
+
size += chunk.length;
|
|
139
|
+
callback(size > before.size ? new WorkspaceTransferError('workspace_transfer_conflict') : null, chunk);
|
|
140
|
+
} });
|
|
141
|
+
await pipeline(handle.createReadStream({ autoClose: false }), guard, createWriteStream(to, { flags: 'wx', mode: 0o600 }), { signal });
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
for await (const chunk of handle.createReadStream({ autoClose: false })) {
|
|
145
|
+
signal?.throwIfAborted();
|
|
146
|
+
size += chunk.length;
|
|
147
|
+
if (size > before.size)
|
|
148
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
149
|
+
hash.update(chunk);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const after = await handle.stat();
|
|
153
|
+
if (size !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs)
|
|
154
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
155
|
+
if (hash)
|
|
156
|
+
treeParts.push({ name: entry.name, directory: false,
|
|
157
|
+
value: Buffer.concat([Buffer.from(`${info.mode & 0o111 ? '100755' : '100644'} ${entry.name}\0`), hash.digest()]) });
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await handle.close();
|
|
161
|
+
}
|
|
162
|
+
if (to)
|
|
163
|
+
await chmod(to, info.mode & 0o111 ? 0o755 : 0o644);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const after = await lstat(directory);
|
|
167
|
+
if (after.dev !== directoryInfo.dev || after.ino !== directoryInfo.ino || after.mtimeMs !== directoryInfo.mtimeMs || after.ctimeMs !== directoryInfo.ctimeMs)
|
|
168
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
169
|
+
if (hashOnly) {
|
|
170
|
+
treeParts.sort((left, right) => Buffer.compare(Buffer.from(left.name + (left.directory ? '/' : '\0')), Buffer.from(right.name + (right.directory ? '/' : '\0'))));
|
|
171
|
+
const length = treeParts.reduce((total, part) => total + part.value.length, 0);
|
|
172
|
+
const hash = createHash('sha1').update(`tree ${length}\0`);
|
|
173
|
+
for (const part of treeParts)
|
|
174
|
+
hash.update(part.value);
|
|
175
|
+
return hash.digest('hex');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const tree = await copy(source, '');
|
|
179
|
+
return { bytes, files, ...(tree ? { tree } : {}) };
|
|
180
|
+
}
|
|
181
|
+
const EMPTY_TREE = createHash('sha1').update('tree 0\0').digest('hex');
|
|
182
|
+
export async function inspectWorkspaceTransfer(root, options = {}) {
|
|
183
|
+
return copySnapshot(await realpath(root), undefined, options.signal);
|
|
184
|
+
}
|
|
185
|
+
export async function inspectWorkspaceTree(root, options = {}) {
|
|
186
|
+
const snapshot = await copySnapshot(await realpath(root), undefined, options.signal, true);
|
|
187
|
+
return { bytes: snapshot.bytes, files: snapshot.files, tree: snapshot.tree };
|
|
188
|
+
}
|
|
189
|
+
async function matchesFile(root, path, expected) {
|
|
190
|
+
const target = await safeTarget(root, path);
|
|
191
|
+
const info = await lstat(target).catch(error => { if (['ENOENT', 'ENOTDIR'].includes(error.code))
|
|
192
|
+
return undefined; throw error; });
|
|
193
|
+
if (!expected)
|
|
194
|
+
return !info || info.isDirectory();
|
|
195
|
+
if (!info?.isFile() || Boolean(info.mode & 0o111) !== expected.executable)
|
|
196
|
+
return false;
|
|
197
|
+
const handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
198
|
+
try {
|
|
199
|
+
const stat = await handle.stat();
|
|
200
|
+
const hash = createHash(expected.sha.length === 64 ? 'sha256' : 'sha1');
|
|
201
|
+
hash.update(`blob ${stat.size}\0`);
|
|
202
|
+
for await (const chunk of handle.createReadStream({ autoClose: false }))
|
|
203
|
+
hash.update(chunk);
|
|
204
|
+
return hash.digest('hex') === expected.sha;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await handle.close();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function removeEmptyParents(root, target) {
|
|
211
|
+
let parent = dirname(target);
|
|
212
|
+
while (parent !== root) {
|
|
213
|
+
if ((await readdir(parent)).length > 0)
|
|
214
|
+
break;
|
|
215
|
+
await rmdir(parent);
|
|
216
|
+
parent = dirname(parent);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function installFile(root, path, source, executable) {
|
|
220
|
+
const target = await safeTarget(root, path);
|
|
221
|
+
const existing = await lstat(target).catch(error => { if (error.code === 'ENOENT')
|
|
222
|
+
return undefined; throw error; });
|
|
223
|
+
if (existing?.isDirectory())
|
|
224
|
+
await rmdir(target);
|
|
225
|
+
await mkdir(dirname(target), { recursive: true });
|
|
226
|
+
const stage = join(dirname(target), `.playcraft-transfer-${randomUUID()}.tmp`);
|
|
227
|
+
try {
|
|
228
|
+
await copyFile(source, stage);
|
|
229
|
+
await chmod(stage, executable ? 0o755 : 0o644);
|
|
230
|
+
await rename(stage, target);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
await rm(stage, { force: true }).catch(() => undefined);
|
|
234
|
+
await removeEmptyParents(root, target).catch(() => undefined);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
await rm(stage, { force: true }).catch(() => undefined);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** A fresh history contains only the current portable files, including uncommitted changes. */
|
|
242
|
+
export async function createWorkspaceTransfer(root, options = {}) {
|
|
243
|
+
const temporary = await mkdtemp(join(tmpdir(), 'playcraft-transfer-'));
|
|
244
|
+
const cleanup = () => rm(temporary, { recursive: true, force: true });
|
|
245
|
+
try {
|
|
246
|
+
const checkout = join(temporary, 'checkout');
|
|
247
|
+
await mkdir(checkout);
|
|
248
|
+
const stats = await copySnapshot(await realpath(root), checkout, options.signal);
|
|
249
|
+
await runTransferGit(checkout, ['init', '--initial-branch=main'], options);
|
|
250
|
+
// Portable snapshots preserve bytes even when project attributes request content conversion.
|
|
251
|
+
await writeFile(join(checkout, '.git', 'info', 'attributes'), RAW_ATTRIBUTES);
|
|
252
|
+
await runTransferGit(checkout, ['add', '--force', '--all'], options);
|
|
253
|
+
const tree = await runTransferGit(checkout, ['write-tree'], options);
|
|
254
|
+
const head = await canonicalCommit(checkout, tree, options.signal);
|
|
255
|
+
await runTransferGit(checkout, ['update-ref', 'refs/heads/main', head], options);
|
|
256
|
+
const bundlePath = join(temporary, 'workspace.bundle');
|
|
257
|
+
if (!options.skipBundle)
|
|
258
|
+
await runTransferGit(checkout, ['bundle', 'create', bundlePath, 'HEAD'], options);
|
|
259
|
+
return { bundlePath, tree, head, stats, cleanup };
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
await cleanup();
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function canonicalCommit(root, tree, signal) {
|
|
267
|
+
return runTransferGit(root, ['commit-tree', tree, '-m', SNAPSHOT_MESSAGE], { signal });
|
|
268
|
+
}
|
|
269
|
+
/** Only objects introduced since the last cloud snapshot are packed; local history stays local. */
|
|
270
|
+
export async function createWorkspacePushTransfer(root, expectedTree, options = {}) {
|
|
271
|
+
if (!/^[a-f0-9]{40,64}$/.test(expectedTree))
|
|
272
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
273
|
+
const temporary = await mkdtemp(join(tmpdir(), 'playcraft-push-'));
|
|
274
|
+
const cleanup = () => rm(temporary, { recursive: true, force: true });
|
|
275
|
+
try {
|
|
276
|
+
const checkout = join(temporary, 'checkout');
|
|
277
|
+
await runTransferGit(temporary, ['clone', '--shared', '--no-checkout', '--', await realpath(root), checkout], options);
|
|
278
|
+
const tree = await runTransferGit(checkout, ['rev-parse', '--verify', 'HEAD^{tree}'], options);
|
|
279
|
+
const stats = summarizeEntries(await treeEntries(checkout, tree, options.signal));
|
|
280
|
+
try {
|
|
281
|
+
await runTransferGit(checkout, ['cat-file', '-e', `${expectedTree}^{tree}`], options);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (error instanceof WorkspaceTransferError && error.code === 'workspace_transfer_git_failed')
|
|
285
|
+
throw new WorkspaceTransferError('workspace_transfer_base_missing');
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
const base = await canonicalCommit(checkout, expectedTree, options.signal);
|
|
289
|
+
const head = await runTransferGit(checkout, ['commit-tree', tree, '-p', base, '-m', SNAPSHOT_MESSAGE], options);
|
|
290
|
+
const transferRef = `refs/heads/playcraft-transfer-${tree}`;
|
|
291
|
+
await runTransferGit(checkout, ['update-ref', transferRef, head], options);
|
|
292
|
+
await runTransferGit(checkout, ['symbolic-ref', 'HEAD', transferRef], options);
|
|
293
|
+
const bundlePath = join(temporary, 'workspace.bundle');
|
|
294
|
+
await runTransferGit(checkout, ['bundle', 'create', bundlePath, 'HEAD', transferRef, `^${base}`], options);
|
|
295
|
+
return { bundlePath, tree, head, stats, cleanup };
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
await cleanup();
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function treeEntries(root, ref = 'HEAD', signal) {
|
|
303
|
+
const entries = [];
|
|
304
|
+
const normalized = new Set();
|
|
305
|
+
let pending = '';
|
|
306
|
+
const parse = (line) => {
|
|
307
|
+
const match = /^(100644|100755) blob ([a-f0-9]{40,64})\s+(\d+)\t(.+)$/s.exec(line);
|
|
308
|
+
if (!match)
|
|
309
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
310
|
+
const path = match[4];
|
|
311
|
+
if (path.length > 1024 || path.includes('\\') || path.startsWith('/') || /[\x00-\x1f\x7f]/.test(path) ||
|
|
312
|
+
path.split('/').some(part => !part || part === '.' || part === '..') || excluded(path))
|
|
313
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
314
|
+
const size = Number(match[3]);
|
|
315
|
+
if (!Number.isSafeInteger(size) || normalized.has(path.toLowerCase()))
|
|
316
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
317
|
+
normalized.add(path.toLowerCase());
|
|
318
|
+
entries.push({ path, sha: match[2], executable: match[1] === '100755', size });
|
|
319
|
+
};
|
|
320
|
+
await runTransferGit(root, ['ls-tree', '-r', '-z', '-l', ref], { signal, onStdout(chunk) {
|
|
321
|
+
pending += chunk;
|
|
322
|
+
let end;
|
|
323
|
+
while ((end = pending.indexOf('\0')) !== -1) {
|
|
324
|
+
parse(pending.slice(0, end));
|
|
325
|
+
pending = pending.slice(end + 1);
|
|
326
|
+
}
|
|
327
|
+
if (pending.length > 1200)
|
|
328
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
329
|
+
} });
|
|
330
|
+
if (pending)
|
|
331
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
332
|
+
return entries;
|
|
333
|
+
}
|
|
334
|
+
function summarizeEntries(entries) {
|
|
335
|
+
const bytes = entries.reduce((total, entry) => total + entry.size, 0);
|
|
336
|
+
if (!Number.isSafeInteger(bytes))
|
|
337
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
338
|
+
return { bytes, files: entries.length };
|
|
339
|
+
}
|
|
340
|
+
export async function inspectWorkspaceCommit(root, ref = 'HEAD', options = {}) {
|
|
341
|
+
const tree = await runTransferGit(root, ['rev-parse', '--verify', '--end-of-options', `${ref}^{tree}`], options);
|
|
342
|
+
return summarizeEntries(await treeEntries(root, tree, options.signal));
|
|
343
|
+
}
|
|
344
|
+
async function safeTarget(root, path) {
|
|
345
|
+
const target = resolve(root, path);
|
|
346
|
+
if (!target.startsWith(`${root}${sep}`))
|
|
347
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
348
|
+
let current = root;
|
|
349
|
+
for (const segment of path.split('/')) {
|
|
350
|
+
current = join(current, segment);
|
|
351
|
+
const info = await lstat(current).catch(error => { if (['ENOENT', 'ENOTDIR'].includes(error.code))
|
|
352
|
+
return undefined; throw error; });
|
|
353
|
+
if (info?.isSymbolicLink())
|
|
354
|
+
throw new WorkspaceTransferError('workspace_transfer_symlink');
|
|
355
|
+
}
|
|
356
|
+
return target;
|
|
357
|
+
}
|
|
358
|
+
/** Compare the entire checkout before applying local commits, so cloud edits are never overwritten. */
|
|
359
|
+
export async function applyWorkspaceTransfer(root, bundle, expectedTree, options = {}) {
|
|
360
|
+
if (!/^[a-f0-9]{40,64}$/.test(expectedTree))
|
|
361
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
362
|
+
options.signal?.throwIfAborted();
|
|
363
|
+
const rootReal = await realpath(root);
|
|
364
|
+
const temporary = await mkdtemp(join(tmpdir(), 'playcraft-import-'));
|
|
365
|
+
let current;
|
|
366
|
+
try {
|
|
367
|
+
const bundlePath = typeof bundle === 'string' ? resolve(bundle) : join(temporary, 'incoming.bundle');
|
|
368
|
+
const incoming = join(temporary, 'checkout');
|
|
369
|
+
if (typeof bundle !== 'string')
|
|
370
|
+
await writeFile(bundlePath, bundle, { mode: 0o600 });
|
|
371
|
+
const bundleInfo = await stat(bundlePath);
|
|
372
|
+
if (!bundleInfo.isFile() || !bundleInfo.size)
|
|
373
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
374
|
+
const advertised = await runTransferGit(temporary, ['bundle', 'list-heads', bundlePath], options);
|
|
375
|
+
const targets = advertised.split('\n').map(line => /^[a-f0-9]{40,64} refs\/heads\/playcraft-transfer-([a-f0-9]{40,64})$/.exec(line)?.[1]).filter(Boolean);
|
|
376
|
+
if (targets.length > 1)
|
|
377
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
378
|
+
const targetTree = targets[0];
|
|
379
|
+
current = await createWorkspaceTransfer(rootReal, { ...options, skipBundle: true });
|
|
380
|
+
// A lost response can be retried after the original prerequisite has left the cloud snapshot.
|
|
381
|
+
if (targetTree === current.tree) {
|
|
382
|
+
await options.validateSnapshot?.(current.stats, current.stats, bundleInfo.size);
|
|
383
|
+
return { tree: current.tree, head: current.head, changedPaths: [] };
|
|
384
|
+
}
|
|
385
|
+
const baseline = join(dirname(current.bundlePath), 'checkout');
|
|
386
|
+
await runTransferGit(temporary, ['clone', '--shared', '--no-checkout', '--', baseline, incoming], options);
|
|
387
|
+
await writeFile(join(incoming, '.git', 'info', 'attributes'), RAW_ATTRIBUTES);
|
|
388
|
+
try {
|
|
389
|
+
await runTransferGit(incoming, ['fetch', '--no-tags', '--', bundlePath, 'HEAD'], options);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
if (current.tree !== expectedTree)
|
|
393
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
394
|
+
if (error instanceof WorkspaceTransferError && error.code === 'workspace_transfer_git_failed')
|
|
395
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
const entries = await treeEntries(incoming, 'FETCH_HEAD', options.signal);
|
|
399
|
+
const tree = await runTransferGit(incoming, ['rev-parse', 'FETCH_HEAD^{tree}'], options);
|
|
400
|
+
const head = await runTransferGit(incoming, ['rev-parse', 'FETCH_HEAD'], options);
|
|
401
|
+
if (targetTree && targetTree !== tree)
|
|
402
|
+
throw new WorkspaceTransferError('invalid_workspace_transfer');
|
|
403
|
+
await options.validateSnapshot?.(summarizeEntries(entries), current.stats, bundleInfo.size);
|
|
404
|
+
if (current.tree === tree)
|
|
405
|
+
return { tree, head, changedPaths: [] };
|
|
406
|
+
if (current.tree !== expectedTree)
|
|
407
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
408
|
+
const previous = new Map((await treeEntries(baseline, 'HEAD', options.signal)).map(entry => [entry.path, entry]));
|
|
409
|
+
const next = new Map(entries.map(entry => [entry.path, entry]));
|
|
410
|
+
const changedPaths = [...new Set([...previous.keys(), ...next.keys()])]
|
|
411
|
+
.filter(path => previous.get(path)?.sha !== next.get(path)?.sha || previous.get(path)?.executable !== next.get(path)?.executable);
|
|
412
|
+
for (const path of changedPaths) {
|
|
413
|
+
const target = await safeTarget(rootReal, path);
|
|
414
|
+
const info = await lstat(target).catch(error => { if (['ENOENT', 'ENOTDIR'].includes(error.code))
|
|
415
|
+
return undefined; throw error; });
|
|
416
|
+
if (next.has(path) && info?.isDirectory()) {
|
|
417
|
+
const children = await readdir(target, { recursive: true });
|
|
418
|
+
for (const child of children) {
|
|
419
|
+
const childPath = `${path}/${child.split(sep).join('/')}`;
|
|
420
|
+
const childInfo = await lstat(join(target, child));
|
|
421
|
+
if (!childInfo.isDirectory() && (!previous.has(childPath) || next.has(childPath)))
|
|
422
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
await runTransferGit(incoming, ['checkout', '--force', '--detach', head], options);
|
|
427
|
+
const recheck = await copySnapshot(rootReal, undefined, options.signal, true);
|
|
428
|
+
if (recheck.tree !== expectedTree)
|
|
429
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
430
|
+
const applied = [];
|
|
431
|
+
try {
|
|
432
|
+
// Deleting files first also permits a file to become a directory, or the reverse.
|
|
433
|
+
for (const path of changedPaths.filter(path => !next.has(path)).sort((a, b) => b.length - a.length)) {
|
|
434
|
+
options.signal?.throwIfAborted();
|
|
435
|
+
if (!await matchesFile(rootReal, path, previous.get(path)))
|
|
436
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
437
|
+
await rm(await safeTarget(rootReal, path), { force: true });
|
|
438
|
+
applied.push(path);
|
|
439
|
+
await removeEmptyParents(rootReal, join(rootReal, path));
|
|
440
|
+
}
|
|
441
|
+
for (const path of changedPaths.filter(path => next.has(path))) {
|
|
442
|
+
options.signal?.throwIfAborted();
|
|
443
|
+
if (!await matchesFile(rootReal, path, previous.get(path)))
|
|
444
|
+
throw new WorkspaceTransferError('workspace_transfer_conflict');
|
|
445
|
+
const entry = next.get(path);
|
|
446
|
+
await installFile(rootReal, path, join(incoming, path), entry.executable);
|
|
447
|
+
applied.push(path);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
// Roll back only our writes; never replace another writer's intervening content.
|
|
452
|
+
for (const path of applied.reverse()) {
|
|
453
|
+
if (!await matchesFile(rootReal, path, next.get(path)))
|
|
454
|
+
throw new WorkspaceTransferError('workspace_transfer_rollback_conflict');
|
|
455
|
+
const old = previous.get(path);
|
|
456
|
+
if (old)
|
|
457
|
+
await installFile(rootReal, path, join(baseline, path), old.executable);
|
|
458
|
+
else {
|
|
459
|
+
await rm(await safeTarget(rootReal, path), { force: true });
|
|
460
|
+
await removeEmptyParents(rootReal, join(rootReal, path));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
return { tree, head, changedPaths };
|
|
466
|
+
}
|
|
467
|
+
finally {
|
|
468
|
+
await current?.cleanup();
|
|
469
|
+
await rm(temporary, { recursive: true, force: true });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { TURN_FILES_CHANGED_EVENT, } from '@playcraft/common/cloud-workspace/runtime';
|
|
2
|
+
import { diffWorkspaceTrees, retainTurnSnapshots, snapshotWorkspaceTree } from './git.js';
|
|
3
|
+
import { shareWorkspaceGit } from './shared-git.js';
|
|
4
|
+
const TURN_ENDS = new Set(['turn/completed', 'turn.completed', 'turn.interrupted', 'turn.failed']);
|
|
5
|
+
/**
|
|
6
|
+
* Snapshots taken when a session's turn starts, until that turn ends. Kept in memory only: a
|
|
7
|
+
* restart ends the native turn as well, and that turn then simply reports no changes.
|
|
8
|
+
*/
|
|
9
|
+
const started = new WeakMap();
|
|
10
|
+
function turnsOf(state) {
|
|
11
|
+
let turns = started.get(state);
|
|
12
|
+
if (!turns)
|
|
13
|
+
started.set(state, turns = new Map());
|
|
14
|
+
return turns;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Records the work tree before a turn is sent. A directory that cannot be snapshotted (no
|
|
18
|
+
* repository, Git failure) gets no change summary; the turn itself is never held back by it.
|
|
19
|
+
*/
|
|
20
|
+
export async function beginTurnChanges(state, sessionId, input) {
|
|
21
|
+
turnsOf(state).delete(sessionId);
|
|
22
|
+
if (input.directoryName === 'root')
|
|
23
|
+
return;
|
|
24
|
+
await shareWorkspaceGit(state, input.root).catch(() => undefined);
|
|
25
|
+
const before = await snapshotWorkspaceTree(input.root).catch(() => undefined);
|
|
26
|
+
if (before)
|
|
27
|
+
turnsOf(state).set(sessionId, { ...input, before });
|
|
28
|
+
}
|
|
29
|
+
/** A turn that never started has nothing to compare. */
|
|
30
|
+
export function abandonTurnChanges(state, sessionId) {
|
|
31
|
+
turnsOf(state).delete(sessionId);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* When a session's turn ends, compares the work tree with its start and records the files that
|
|
35
|
+
* changed for that session's conversation. Returns no event when nothing changed.
|
|
36
|
+
*/
|
|
37
|
+
export async function finishTurnChanges(state, event) {
|
|
38
|
+
const { threadId, turnId } = event;
|
|
39
|
+
if (!TURN_ENDS.has(event.method) || !threadId || !turnId)
|
|
40
|
+
return [];
|
|
41
|
+
const session = state.journal.listSessions().find((item) => item.threadId === threadId);
|
|
42
|
+
const start = session && turnsOf(state).get(session.sessionId);
|
|
43
|
+
if (!session || !start)
|
|
44
|
+
return [];
|
|
45
|
+
turnsOf(state).delete(session.sessionId);
|
|
46
|
+
try {
|
|
47
|
+
await shareWorkspaceGit(state, start.root);
|
|
48
|
+
const after = await snapshotWorkspaceTree(start.root);
|
|
49
|
+
const { files, truncated } = await diffWorkspaceTrees(start.root, start.before, after);
|
|
50
|
+
if (!files.length)
|
|
51
|
+
return [];
|
|
52
|
+
await retainTurnSnapshots(start.root, turnId, start.before, after);
|
|
53
|
+
const changes = { threadId, turnId, before: start.before, after, files, truncated };
|
|
54
|
+
return [await state.journal.allocateSourceEvent({
|
|
55
|
+
runtimeId: state.config.runtimeId,
|
|
56
|
+
generation: state.config.generation,
|
|
57
|
+
runtimeEpoch: state.runtimeEpoch,
|
|
58
|
+
type: TURN_FILES_CHANGED_EVENT,
|
|
59
|
+
payload: {
|
|
60
|
+
...changes,
|
|
61
|
+
sessionId: session.sessionId,
|
|
62
|
+
...(start.workspaceId ? { workspaceId: start.workspaceId } : {}),
|
|
63
|
+
...(start.directoryName ? { directoryName: start.directoryName } : {}),
|
|
64
|
+
},
|
|
65
|
+
occurredAt: new Date().toISOString(),
|
|
66
|
+
sessionId: session.sessionId,
|
|
67
|
+
workspaceId: start.workspaceId,
|
|
68
|
+
threadId,
|
|
69
|
+
turnId,
|
|
70
|
+
})];
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { WORKSPACE_VIDEO_ATTACHMENT_MAX_DURATION_MS, WORKSPACE_VIDEO_ATTACHMENT_MAX_BYTES } from '@playcraft/common/cloud-workspace/runtime';
|
|
4
|
+
/** Probe the opened descriptor so replacing a pathname cannot redirect the media reader. */
|
|
5
|
+
export async function probeAttachmentVideo(handle) {
|
|
6
|
+
const output = await new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(process.env.PLAYCRAFT_FFPROBE_PATH ?? 'ffprobe', [
|
|
8
|
+
'-v', 'error', '-protocol_whitelist', 'file,pipe', '-show_entries',
|
|
9
|
+
'format=format_name,duration:stream=codec_type,width,height', '-of', 'json',
|
|
10
|
+
process.platform === 'linux' ? '/proc/self/fd/3' : '/dev/fd/3',
|
|
11
|
+
], { stdio: ['ignore', 'pipe', 'ignore', handle.fd] });
|
|
12
|
+
let text = '';
|
|
13
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('invalid_attachment')); }, 15_000);
|
|
14
|
+
timer.unref();
|
|
15
|
+
child.stdout.on('data', (chunk) => {
|
|
16
|
+
text += chunk.toString('utf8');
|
|
17
|
+
if (text.length > 64 * 1024) {
|
|
18
|
+
child.kill('SIGKILL');
|
|
19
|
+
reject(new Error('invalid_attachment'));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
child.once('error', () => { clearTimeout(timer); reject(new Error('video_probe_unavailable')); });
|
|
23
|
+
child.once('close', code => { clearTimeout(timer); code === 0 ? resolve(text) : reject(new Error('invalid_attachment')); });
|
|
24
|
+
});
|
|
25
|
+
const result = JSON.parse(output);
|
|
26
|
+
const stream = result.streams?.find(item => item.codec_type === 'video');
|
|
27
|
+
const durationMs = Math.round(Number(result.format?.duration) * 1000);
|
|
28
|
+
const formats = result.format?.format_name?.split(',') ?? [];
|
|
29
|
+
const mimeType = formats.includes('webm') ? 'video/webm' : formats.includes('mp4') ? 'video/mp4' : formats.includes('mov') ? 'video/quicktime' : undefined;
|
|
30
|
+
if (!stream || !mimeType || !Number.isFinite(durationMs) || durationMs <= 0 || durationMs > WORKSPACE_VIDEO_ATTACHMENT_MAX_DURATION_MS
|
|
31
|
+
|| !Number.isSafeInteger(stream.width) || !Number.isSafeInteger(stream.height) || stream.width <= 0 || stream.height <= 0
|
|
32
|
+
|| stream.width > 16384 || stream.height > 16384)
|
|
33
|
+
throw new Error('invalid_attachment');
|
|
34
|
+
return { mimeType, durationMs, width: stream.width, height: stream.height };
|
|
35
|
+
}
|
|
36
|
+
export async function hashAttachment(handle) {
|
|
37
|
+
const digest = createHash('sha256');
|
|
38
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
39
|
+
let position = 0;
|
|
40
|
+
for (;;) {
|
|
41
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, position);
|
|
42
|
+
if (!bytesRead)
|
|
43
|
+
return digest.digest('hex');
|
|
44
|
+
digest.update(chunk.subarray(0, bytesRead));
|
|
45
|
+
position += bytesRead;
|
|
46
|
+
if (position > WORKSPACE_VIDEO_ATTACHMENT_MAX_BYTES)
|
|
47
|
+
throw new Error('invalid_attachment');
|
|
48
|
+
}
|
|
49
|
+
}
|