@playcraft/cli 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/README.md +38 -3
  2. package/dist/agent/local-backend.js +1 -1
  3. package/dist/auth/account-activation.js +157 -0
  4. package/dist/auth/messages.js +30 -0
  5. package/dist/cli-root-help.js +2 -0
  6. package/dist/commands/build.js +9 -4
  7. package/dist/commands/create.js +26 -33
  8. package/dist/commands/image.js +2 -6
  9. package/dist/commands/login.js +84 -42
  10. package/dist/commands/platform-skills.generated.js +19 -0
  11. package/dist/commands/playable.js +53 -0
  12. package/dist/commands/skills.js +2 -0
  13. package/dist/commands/tools-3d.js +11 -0
  14. package/dist/commands/tools-batch.js +87 -0
  15. package/dist/commands/tools-generation.js +8 -4
  16. package/dist/commands/tools.js +2 -0
  17. package/dist/commands/video.js +111 -0
  18. package/dist/commands/workspace-runtime.js +54 -0
  19. package/dist/config.js +59 -4
  20. package/dist/index.js +34 -3
  21. package/dist/project-skills/builtin-import.js +225 -0
  22. package/dist/project-skills/commands.js +77 -0
  23. package/dist/project-skills/lifecycle.js +28 -0
  24. package/dist/project-skills/local-run.js +112 -0
  25. package/dist/project-skills/local-store.js +221 -0
  26. package/dist/project-skills/messages.js +80 -0
  27. package/dist/project-skills/reconcile.js +62 -0
  28. package/dist/project-skills/remote-cache.js +51 -0
  29. package/dist/project-skills/validation.js +19 -0
  30. package/dist/recording/messages.js +40 -0
  31. package/dist/recording/project-digest.js +85 -0
  32. package/dist/recording/recorder.js +293 -0
  33. package/dist/recording/scenario.js +50 -0
  34. package/dist/remix/clone.js +2 -0
  35. package/dist/remix/init-template.js +2 -0
  36. package/dist/remix/pull.js +2 -0
  37. package/dist/remix/push.js +21 -1
  38. package/dist/utils/agent-api-client.js +185 -30
  39. package/dist/utils/environment-endpoints.js +17 -0
  40. package/dist/utils/playcraft-http-client.js +6 -4
  41. package/dist/utils/tool-operation-journal.js +124 -0
  42. package/dist/utils/video-evidence-git.js +81 -0
  43. package/dist/video-replication/client.js +222 -0
  44. package/dist/video-replication/comparison-evidence.js +149 -0
  45. package/dist/video-replication/comparison-files.js +57 -0
  46. package/dist/video-replication/comparison-log.js +72 -0
  47. package/dist/video-replication/comparison-messages.js +104 -0
  48. package/dist/video-replication/comparison-output.js +63 -0
  49. package/dist/video-replication/comparison-preflight.js +66 -0
  50. package/dist/video-replication/comparison.js +216 -0
  51. package/dist/video-replication/evidence.js +165 -0
  52. package/dist/video-replication/media.js +111 -0
  53. package/dist/video-replication/messages.js +89 -0
  54. package/dist/workspace-runtime/codex/app-server.js +636 -0
  55. package/dist/workspace-runtime/codex/config-toml.js +107 -0
  56. package/dist/workspace-runtime/codex/jsonrpc-stdio.js +123 -0
  57. package/dist/workspace-runtime/codex/launcher-client.js +163 -0
  58. package/dist/workspace-runtime/codex/loopback.js +143 -0
  59. package/dist/workspace-runtime/codex/model-broker.js +258 -0
  60. package/dist/workspace-runtime/codex/native-adapter.js +9 -0
  61. package/dist/workspace-runtime/main.js +10 -0
  62. package/dist/workspace-runtime/persistence/journal.js +355 -0
  63. package/dist/workspace-runtime/persistence/native-subagents.js +162 -0
  64. package/dist/workspace-runtime/persistence/sqlite-client.js +77 -0
  65. package/dist/workspace-runtime/persistence/sqlite-worker.js +1057 -0
  66. package/dist/workspace-runtime/previews/publish.js +259 -0
  67. package/dist/workspace-runtime/previews/single-file.js +43 -0
  68. package/dist/workspace-runtime/processes/managed-writes.js +69 -0
  69. package/dist/workspace-runtime/production/contracts.js +17 -0
  70. package/dist/workspace-runtime/production/controller.js +207 -0
  71. package/dist/workspace-runtime/production/dependencies.js +60 -0
  72. package/dist/workspace-runtime/production/native.js +90 -0
  73. package/dist/workspace-runtime/production/processes.js +86 -0
  74. package/dist/workspace-runtime/production/save-command.js +24 -0
  75. package/dist/workspace-runtime/production/source-copy.js +91 -0
  76. package/dist/workspace-runtime/production/stages.js +202 -0
  77. package/dist/workspace-runtime/production/storage.js +32 -0
  78. package/dist/workspace-runtime/serve.js +202 -0
  79. package/dist/workspace-runtime/server/auth.js +21 -0
  80. package/dist/workspace-runtime/server/connectivity.js +201 -0
  81. package/dist/workspace-runtime/server/desktop-host.js +429 -0
  82. package/dist/workspace-runtime/server/desktop-preview-config.js +81 -0
  83. package/dist/workspace-runtime/server/desktop-preview-service.js +157 -0
  84. package/dist/workspace-runtime/server/desktop-preview-static.js +53 -0
  85. package/dist/workspace-runtime/server/desktop-preview.js +189 -0
  86. package/dist/workspace-runtime/server/dispatch.js +903 -0
  87. package/dist/workspace-runtime/server/endpoints.js +19 -0
  88. package/dist/workspace-runtime/server/execution-group.js +85 -0
  89. package/dist/workspace-runtime/server/http.js +984 -0
  90. package/dist/workspace-runtime/server/lock.js +53 -0
  91. package/dist/workspace-runtime/server/rfb-watch-gate.js +168 -0
  92. package/dist/workspace-runtime/server/terminal-host.js +294 -0
  93. package/dist/workspace-runtime/server/types.js +1 -0
  94. package/dist/workspace-runtime/workspaces/artifact-events.js +55 -0
  95. package/dist/workspace-runtime/workspaces/artifact-files.js +195 -0
  96. package/dist/workspace-runtime/workspaces/artifact-receipts.js +89 -0
  97. package/dist/workspace-runtime/workspaces/artifacts.js +260 -0
  98. package/dist/workspace-runtime/workspaces/attachment-content.js +111 -0
  99. package/dist/workspace-runtime/workspaces/attachments.js +175 -0
  100. package/dist/workspace-runtime/workspaces/batch-commit.js +142 -0
  101. package/dist/workspace-runtime/workspaces/cli-context.js +23 -0
  102. package/dist/workspace-runtime/workspaces/cli-credentials.js +111 -0
  103. package/dist/workspace-runtime/workspaces/context-error.js +2 -0
  104. package/dist/workspace-runtime/workspaces/dependencies.js +264 -0
  105. package/dist/workspace-runtime/workspaces/developer-command.js +64 -0
  106. package/dist/workspace-runtime/workspaces/directories.js +113 -0
  107. package/dist/workspace-runtime/workspaces/directory-access.js +29 -0
  108. package/dist/workspace-runtime/workspaces/directory-maintenance.js +111 -0
  109. package/dist/workspace-runtime/workspaces/directory-stats.js +49 -0
  110. package/dist/workspace-runtime/workspaces/disk-snapshot.js +31 -0
  111. package/dist/workspace-runtime/workspaces/empty-template.js +40 -0
  112. package/dist/workspace-runtime/workspaces/files.js +120 -0
  113. package/dist/workspace-runtime/workspaces/git-credential-broker.js +312 -0
  114. package/dist/workspace-runtime/workspaces/git-credential-helper.js +68 -0
  115. package/dist/workspace-runtime/workspaces/git-direct-store.js +155 -0
  116. package/dist/workspace-runtime/workspaces/git.js +481 -0
  117. package/dist/workspace-runtime/workspaces/json5-edit.js +170 -0
  118. package/dist/workspace-runtime/workspaces/parameters.js +272 -0
  119. package/dist/workspace-runtime/workspaces/prepare.js +264 -0
  120. package/dist/workspace-runtime/workspaces/registry.js +200 -0
  121. package/dist/workspace-runtime/workspaces/revisions.js +31 -0
  122. package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +91 -0
  123. package/dist/workspace-runtime/workspaces/skills.js +189 -0
  124. package/dist/workspace-runtime/workspaces/video-attachments.js +49 -0
  125. package/package.json +14 -5
  126. package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +23 -61
  127. package/project-template/.claude/skills/playcraft-replica-validation/SKILL.md +25 -0
  128. package/project-template/.claude/skills/playcraft-replica-validation/references/contracts.md +79 -0
  129. package/project-template/.claude/skills/playcraft-video-understanding/SKILL.md +29 -0
  130. package/project-template/.claude/skills/playcraft-video-understanding/references/contracts.md +37 -0
  131. package/project-template-v2/.claude/agents/artist.md +82 -0
  132. package/project-template-v2/.claude/agents/developer.md +153 -0
  133. package/project-template-v2/.claude/agents/game-designer.md +264 -0
  134. package/project-template-v2/.claude/agents/refs/artist-art-style-catalog.md +533 -0
  135. package/project-template-v2/.claude/agents/refs/artist-color-audio-recipes.md +153 -0
  136. package/project-template-v2/.claude/agents/refs/artist-dimension-axis.md +27 -0
  137. package/project-template-v2/.claude/agents/refs/artist-master-composite-recipes.md +208 -0
  138. package/project-template-v2/.claude/agents/refs/atom-skill-library.md +81 -0
  139. package/project-template-v2/.claude/agents/refs/developer-impl-cookbook.md +432 -0
  140. package/project-template-v2/.claude/agents/refs/framework-5-component-filter.md +252 -0
  141. package/project-template-v2/.claude/agents/refs/framework-game-feel-juice.md +266 -0
  142. package/project-template-v2/.claude/agents/refs/framework-mda.md +147 -0
  143. package/project-template-v2/.claude/agents/refs/game-designer-gameplay-sufficiency.md +123 -0
  144. package/project-template-v2/.claude/agents/refs/ta-3d-flip-recipe.md +88 -0
  145. package/project-template-v2/.claude/agents/refs/ta-atlas-deliverable-standard.md +67 -0
  146. package/project-template-v2/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
  147. package/project-template-v2/.claude/agents/refs/ta-image-generation-detail.md +300 -0
  148. package/project-template-v2/.claude/agents/refs/ta-image-ops-reference.md +495 -0
  149. package/project-template-v2/.claude/agents/refs/ta-pipeline-cookbook.md +1141 -0
  150. package/project-template-v2/.claude/agents/refs/ta-tools-reference.md +111 -0
  151. package/project-template-v2/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
  152. package/project-template-v2/.claude/agents/refs/threejs-cannon-pitfalls.md +412 -0
  153. package/project-template-v2/.claude/agents/reviewer.md +75 -0
  154. package/project-template-v2/.claude/agents/technical-artist.md +86 -0
  155. package/project-template-v2/.claude/hooks/snapshot-milestone.mjs +243 -0
  156. package/project-template-v2/.claude/hooks/user-prompt.mjs +133 -0
  157. package/project-template-v2/.claude/settings.json +33 -0
  158. package/project-template-v2/.claude/skills/brainstorming/SKILL.md +161 -0
  159. package/project-template-v2/.claude/skills/brainstorming/scripts/frame-template.html +270 -0
  160. package/project-template-v2/.claude/skills/brainstorming/scripts/helper.js +177 -0
  161. package/project-template-v2/.claude/skills/brainstorming/scripts/server.cjs +354 -0
  162. package/project-template-v2/.claude/skills/brainstorming/scripts/start-server.sh +148 -0
  163. package/project-template-v2/.claude/skills/brainstorming/scripts/stop-server.sh +56 -0
  164. package/project-template-v2/.claude/skills/brainstorming/scripts/wait-for-selection.sh +62 -0
  165. package/project-template-v2/.claude/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  166. package/project-template-v2/.claude/skills/brainstorming/visual-companion.md +309 -0
  167. package/project-template-v2/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
  168. package/project-template-v2/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
  169. package/project-template-v2/.claude/skills/playcraft-asset-state-sheet/SKILL.md +205 -0
  170. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
  171. package/project-template-v2/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
  172. package/project-template-v2/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
  173. package/project-template-v2/.claude/skills/playcraft-image-generation/SKILL.md +298 -0
  174. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
  175. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
  176. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +324 -0
  177. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
  178. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
  179. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
  180. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
  181. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
  182. package/project-template-v2/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
  183. package/project-template-v2/.claude/skills/playcraft-image-processing/SKILL.md +219 -0
  184. package/project-template-v2/.claude/skills/playcraft-masking/SKILL.md +373 -0
  185. package/project-template-v2/.claude/skills/playcraft-playable-optimization/SKILL.md +161 -0
  186. package/project-template-v2/.claude/skills/playcraft-research/SKILL.md +215 -0
  187. package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +382 -0
  188. package/project-template-v2/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
  189. package/project-template-v2/.claude/skills/playcraft-sprite-remix/SKILL.md +158 -0
  190. package/project-template-v2/.claude/skills/playcraft-sprite-sheet/SKILL.md +100 -0
  191. package/project-template-v2/.claude/skills/playcraft-storyboard/SKILL.md +167 -0
  192. package/project-template-v2/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
  193. package/project-template-v2/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
  194. package/project-template-v2/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
  195. package/project-template-v2/.claude/skills/playwright-cli/SKILL.md +390 -0
  196. package/project-template-v2/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
  197. package/project-template-v2/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
  198. package/project-template-v2/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
  199. package/project-template-v2/.claude/skills/playwright-cli/references/running-code.md +240 -0
  200. package/project-template-v2/.claude/skills/playwright-cli/references/session-management.md +226 -0
  201. package/project-template-v2/.claude/skills/playwright-cli/references/spec-driven-testing.md +308 -0
  202. package/project-template-v2/.claude/skills/playwright-cli/references/storage-state.md +275 -0
  203. package/project-template-v2/.claude/skills/playwright-cli/references/test-generation.md +134 -0
  204. package/project-template-v2/.claude/skills/playwright-cli/references/tracing.md +142 -0
  205. package/project-template-v2/.claude/skills/playwright-cli/references/video-recording.md +153 -0
  206. package/project-template-v2/.claude/skills/session-analyzer/SKILL.md +386 -0
  207. package/project-template-v2/.claude/skills/session-analyzer/scripts/execution-breakdown.mjs +182 -0
  208. package/project-template-v2/.claude/skills/session-analyzer/scripts/find-turns.mjs +72 -0
  209. package/project-template-v2/.claude/skills/session-analyzer/scripts/heavy-output.mjs +121 -0
  210. package/project-template-v2/.claude/skills/session-analyzer/scripts/resolve-session.mjs +102 -0
  211. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-stats.mjs +127 -0
  212. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-tool-timeline.mjs +106 -0
  213. package/project-template-v2/.claude/skills/session-analyzer/scripts/time-gaps.mjs +128 -0
  214. package/project-template-v2/.claude/skills/session-analyzer/scripts/turn-timeline.mjs +67 -0
  215. package/project-template-v2/.claude/snapshot.mjs +263 -0
  216. package/project-template-v2/.playcraft/skills.lock.json +152 -0
  217. package/project-template-v2/CLAUDE.md +146 -0
  218. package/project-template-v2/assets/audio/bgm/.gitkeep +0 -0
  219. package/project-template-v2/assets/audio/sfx/.gitkeep +0 -0
  220. package/project-template-v2/assets/bundles/.gitkeep +0 -0
  221. package/project-template-v2/assets/images/bg/.gitkeep +0 -0
  222. package/project-template-v2/assets/images/reference/.gitkeep +0 -0
  223. package/project-template-v2/assets/images/storyboard/.gitkeep +0 -0
  224. package/project-template-v2/assets/images/tiles/.gitkeep +0 -0
  225. package/project-template-v2/assets/images/ui/.gitkeep +0 -0
  226. package/project-template-v2/assets/images/vfx/.gitkeep +0 -0
  227. package/project-template-v2/assets/models/.gitkeep +0 -0
  228. package/project-template-v2/docs/harness/iteration-1/context-flow.md +254 -0
  229. package/project-template-v2/docs/harness/iteration-1/generate-flow.md +91 -0
  230. package/project-template-v2/docs/harness/iteration-1/ideate-flow.md +214 -0
  231. package/project-template-v2/docs/harness/iteration-1/optimize-flow.md +75 -0
  232. package/project-template-v2/docs/harness/iteration-1/wrapup-flow.md +63 -0
  233. package/project-template-v2/docs/harness/iteration-2/context-flow.md +223 -0
  234. package/project-template-v2/docs/harness/iteration-2/generate-flow.md +129 -0
  235. package/project-template-v2/docs/harness/iteration-2/ideate-flow.md +267 -0
  236. package/project-template-v2/docs/harness/iteration-2/optimize-flow.md +164 -0
  237. package/project-template-v2/docs/harness/iteration-2/wrapup-flow.md +115 -0
  238. package/project-template-v2/docs/harness/orchestrator-flow.md +364 -0
  239. package/project-template-v2/docs/project-state.json +60 -0
  240. package/project-template-v2/docs/project-state.md +72 -0
  241. package/project-template-v2/docs/standards/README.md +225 -0
  242. package/project-template-v2/docs/standards/agent-behavior-standards.md +174 -0
  243. package/project-template-v2/docs/standards/artifacts/design-brief.md +19 -0
  244. package/project-template-v2/docs/standards/artifacts/design.md +22 -0
  245. package/project-template-v2/docs/standards/artifacts/game-code.md +41 -0
  246. package/project-template-v2/docs/standards/artifacts/todo-list.md +41 -0
  247. package/project-template-v2/docs/standards/iter1-agent-behavior-standards.md +343 -0
  248. package/project-template-v2/game/index.ts +18 -0
  249. package/project-template-v2/globals.d.ts +51 -0
  250. package/project-template-v2/index.css +34 -0
  251. package/project-template-v2/index.html +18 -0
  252. package/project-template-v2/main.ts +9 -0
  253. package/project-template-v2/package.json +46 -0
  254. package/project-template-v2/skills/_shared/scripts/dispatch-clear.mjs +31 -0
  255. package/project-template-v2/skills/_shared/scripts/dispatch-set.mjs +86 -0
  256. package/project-template-v2/skills/_shared/scripts/dod-check.mjs +153 -0
  257. package/project-template-v2/skills/_shared/scripts/handoff-append.mjs +70 -0
  258. package/project-template-v2/skills/_shared/scripts/lib/validator-artifacts.mjs +91 -0
  259. package/project-template-v2/skills/_shared/scripts/pipeline/dod-config.mjs +131 -0
  260. package/project-template-v2/skills/_shared/scripts/pipeline/index.mjs +80 -0
  261. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-1-core.mjs +90 -0
  262. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-2-wrap.mjs +93 -0
  263. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-3-visual.mjs +23 -0
  264. package/project-template-v2/skills/_shared/scripts/render-project-state.mjs +230 -0
  265. package/project-template-v2/skills/_shared/scripts/state-advance-stage.mjs +58 -0
  266. package/project-template-v2/skills/_shared/scripts/state-get.mjs +339 -0
  267. package/project-template-v2/skills/_shared/scripts/state-handoff.mjs +39 -0
  268. package/project-template-v2/skills/_shared/scripts/state-set.mjs +94 -0
  269. package/project-template-v2/skills/_shared/scripts/state-store.mjs +783 -0
  270. package/project-template-v2/skills/_shared/scripts/todo-add.mjs +88 -0
  271. package/project-template-v2/skills/_shared/scripts/todo-get.mjs +130 -0
  272. package/project-template-v2/skills/_shared/scripts/todo-remove.mjs +33 -0
  273. package/project-template-v2/skills/_shared/scripts/todo-set.mjs +47 -0
  274. package/project-template-v2/skills/_shared/scripts/verify-asset-code-sync.mjs +268 -0
  275. package/project-template-v2/skills/_shared/scripts/verify-env.mjs +285 -0
  276. package/project-template-v2/skills/_shared/scripts/verify-placeholders.mjs +161 -0
  277. package/project-template-v2/skills/playable-autoplay/SKILL.md +176 -0
  278. package/project-template-v2/skills/playable-autoplay/agents/openai.yaml +4 -0
  279. package/project-template-v2/skills/playable-debug/SKILL.md +116 -0
  280. package/project-template-v2/skills/playable-debug/agents/openai.yaml +4 -0
  281. package/project-template-v2/skills/playable-debug/references/debug-config.md +40 -0
  282. package/project-template-v2/skills/playable-record/SKILL.md +140 -0
  283. package/project-template-v2/skills/playable-record/scripts/lib/dev-server.mjs +104 -0
  284. package/project-template-v2/skills/playable-record/scripts/lib/record-audio-bridge.js +141 -0
  285. package/project-template-v2/skills/playable-record/scripts/record-playable.mjs +425 -0
  286. package/project-template-v2/skills/playable-record/scripts/verify-contract.mjs +261 -0
  287. package/project-template-v2/skills/playable-record/scripts/verify-firstwin.mjs +398 -0
  288. package/project-template-v2/skills/playable-record/scripts/verify-fusion.mjs +322 -0
  289. package/project-template-v2/skills/playable-record/scripts/verify-lifecycle.mjs +105 -0
  290. package/project-template-v2/skills/playable-record/scripts/verify-vlm-video.mjs +230 -0
  291. package/project-template-v2/skills/playable-validate/SKILL.md +61 -0
  292. package/project-template-v2/skills/playable-validate/validation-rules.md +14 -0
  293. package/project-template-v2/skills/playable-verify-ui/SKILL.md +63 -0
  294. package/project-template-v2/skills/playable-verify-ui/scripts/lib/__init__.py +1 -0
  295. package/project-template-v2/skills/playable-verify-ui/scripts/lib/config_expr.js +218 -0
  296. package/project-template-v2/skills/playable-verify-ui/scripts/lib/node_utils.js +51 -0
  297. package/project-template-v2/skills/playable-verify-ui/scripts/lib/profile_loader.py +59 -0
  298. package/project-template-v2/skills/playable-verify-ui/scripts/lib/report.py +106 -0
  299. package/project-template-v2/skills/playable-verify-ui/scripts/lib/runtime_contract.js +116 -0
  300. package/project-template-v2/skills/playable-verify-ui/scripts/verify_fetch_antipatterns.py +59 -0
  301. package/project-template-v2/skills/playable-verify-ui/scripts/verify_hardcoded_layout.js +64 -0
  302. package/project-template-v2/skills/playable-verify-ui/scripts/verify_runtime_contract.js +58 -0
  303. package/project-template-v2/skills/playable-verify-ui/scripts/verify_text_overlap.py +333 -0
  304. package/project-template-v2/ta-workspace/scripts/.gitkeep +0 -0
  305. package/project-template-v2/tsconfig.json +20 -0
  306. package/project-template-v2/vite.config.ts +27 -0
  307. package/project-templates/batch-locks/phaser.yaml +922 -0
  308. package/project-templates/batch-locks/threejs.yaml +978 -0
  309. package/project-templates/empty/AGENTS.md +25 -0
  310. package/project-templates/empty/gitignore +33 -0
  311. package/project-templates/phaser/.claude/skills/playcraft-image-generation/SKILL.md +23 -61
  312. package/project-templates/threejs/.claude/skills/playcraft-image-generation/SKILL.md +23 -61
@@ -0,0 +1,90 @@
1
+ import { join } from 'node:path';
2
+ import { lstat, mkdir } from 'node:fs/promises';
3
+ import { createCodexAppServerAdapter } from '../codex/app-server.js';
4
+ import { verifyProductionLease } from './storage.js';
5
+ import { processBirth, stopProductionGroup } from './processes.js';
6
+ import { openDirectoryToDevelopment } from '../workspaces/directory-access.js';
7
+ export async function runProductionNative(state, input, root, record, track) {
8
+ if (!input.model)
9
+ throw new Error('production_model_configuration_required');
10
+ const temporaryRoot = join(root, '.playcraft-temp');
11
+ const existing = await lstat(temporaryRoot).catch(error => {
12
+ if (error.code === 'ENOENT')
13
+ return undefined;
14
+ throw error;
15
+ });
16
+ if (existing && (!existing.isDirectory() || existing.isSymbolicLink()))
17
+ throw new Error('production_source_path_invalid');
18
+ await mkdir(temporaryRoot, { recursive: true });
19
+ await openDirectoryToDevelopment(temporaryRoot, (await lstat(root)).gid);
20
+ const groups = [];
21
+ const adapter = createCodexAppServerAdapter({ config: { ...state.config, workspaceMode: 'single', workspaceRoot: root },
22
+ command: process.env.PLAYCRAFT_PRODUCTION_CODEX_COMMAND,
23
+ homeDir: process.env.PLAYCRAFT_LAUNCHER_SOCKET
24
+ ? `/var/lib/playcraft/codex/production-${input.task.attemptId}` : join(state.config.stateDir, 'production-native', input.task.attemptId),
25
+ model: input.model.model, processGroup: true,
26
+ fixedExecution: { cwd: root, approvalPolicy: 'never', sandboxPolicy: { type: 'workspaceWrite', writableRoots: [root],
27
+ networkAccess: true, excludeTmpdirEnvVar: true, excludeSlashTmp: true } },
28
+ fixedThreadConfig: { model: input.model.model, modelProvider: 'playcraft_production', config: {
29
+ approval_policy: 'never', sandbox_mode: 'workspace-write',
30
+ 'sandbox_workspace_write.writable_roots': [root],
31
+ 'sandbox_workspace_write.network_access': true,
32
+ 'sandbox_workspace_write.exclude_tmpdir_env_var': true,
33
+ 'sandbox_workspace_write.exclude_slash_tmp': true,
34
+ 'shell_environment_policy.set': { PLAYCRAFT_TOOL_REQUEST_DIR: join(temporaryRoot, 'tool-requests'), PLAYCRAFT_SKIP_UPDATE_CHECK: '1',
35
+ TMPDIR: temporaryRoot, npm_config_cache: join(temporaryRoot, 'npm-cache'), npm_config_store_dir: join(temporaryRoot, 'pnpm-store') },
36
+ 'model_providers.playcraft_production': { name: 'PlayCraft Production', base_url: input.model.endpoint,
37
+ wire_api: 'responses', requires_openai_auth: false, http_headers: { Authorization: `Bearer ${input.model.token}` } },
38
+ } },
39
+ onSkillProcessRestart: async (process) => {
40
+ const group = { pid: process.pid, birth: await processBirth(process.pid), stopped: false };
41
+ groups.push(group);
42
+ await record(group);
43
+ },
44
+ });
45
+ track(() => adapter.close());
46
+ const sessionId = `production-${input.task.attemptId}`;
47
+ let completion;
48
+ const done = new Promise(resolve => { completion = resolve; });
49
+ const unsubscribe = adapter.onCollected?.(event => {
50
+ if (event.kind === 'process_exit')
51
+ completion('unknown');
52
+ if (event.method === 'turn/completed') {
53
+ const value = event.params;
54
+ completion(value.turn?.status === 'completed' ? 'completed' : 'failed');
55
+ }
56
+ });
57
+ try {
58
+ await adapter.start();
59
+ const process = adapter.executionProcess?.();
60
+ if (!process)
61
+ throw new Error('production_process_not_started');
62
+ const group = { pid: process.pid, birth: await processBirth(process.pid), stopped: false };
63
+ groups.push(group);
64
+ await record(group);
65
+ const thread = await adapter.startThread({ sessionId, cwd: root });
66
+ const turn = await adapter.startTurn({ sessionId, threadId: thread.threadId, text: String(input.payload.prompt ?? ''),
67
+ clientRequestId: `${input.task.attemptId}:generate`, persistReservation: async () => undefined });
68
+ if (turn.outcome !== 'accepted')
69
+ throw new Error('production_native_start_unknown');
70
+ const timeout = setTimeout(() => completion('unknown'), 30 * 60_000);
71
+ let checking = false;
72
+ const leaseCheck = setInterval(() => {
73
+ if (checking)
74
+ return;
75
+ checking = true;
76
+ void verifyProductionLease(input).catch(async () => { completion('unknown'); await adapter.close?.().catch(() => undefined); }).finally(() => { checking = false; });
77
+ }, 5000);
78
+ const status = await done;
79
+ clearInterval(leaseCheck);
80
+ clearTimeout(timeout);
81
+ if (status !== 'completed')
82
+ throw new Error(status === 'failed' ? 'production_native_failed' : 'production_native_unknown');
83
+ }
84
+ finally {
85
+ unsubscribe?.();
86
+ await adapter.close?.().catch(() => undefined);
87
+ for (const group of groups)
88
+ await record({ ...group, stopped: await stopProductionGroup(group) });
89
+ }
90
+ }
@@ -0,0 +1,86 @@
1
+ import { spawn, execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { startLauncherProcess } from '../codex/launcher-client.js';
6
+ export async function processBirth(pid) {
7
+ if (process.platform !== 'linux') {
8
+ return promisify(execFile)('ps', ['-p', String(pid), '-o', 'lstart='], { timeout: 2000 }).then(value => value.stdout.trim() || undefined, () => undefined);
9
+ }
10
+ try {
11
+ const stat = await readFile(`/proc/${pid}/stat`, 'utf8');
12
+ return stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19];
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ }
18
+ export function groupExited(pid) {
19
+ try {
20
+ process.kill(-pid, 0);
21
+ return false;
22
+ }
23
+ catch (error) {
24
+ return error.code === 'ESRCH';
25
+ }
26
+ }
27
+ export async function stopProductionGroup(record) {
28
+ if (record.stopped || groupExited(record.pid))
29
+ return true;
30
+ if (record.birth && await processBirth(record.pid) !== record.birth)
31
+ return false;
32
+ for (const signal of ['SIGTERM', 'SIGKILL']) {
33
+ try {
34
+ process.kill(-record.pid, signal);
35
+ }
36
+ catch { /* Recheck whether the whole group exited. */ }
37
+ const deadline = Date.now() + 2000;
38
+ while (Date.now() < deadline) {
39
+ if (groupExited(record.pid))
40
+ return true;
41
+ await delay(25);
42
+ }
43
+ }
44
+ return groupExited(record.pid);
45
+ }
46
+ /** Only the save stage receives repository credentials; build and install receive none. */
47
+ export async function runProductionCommand(state, input) {
48
+ const launcher = !input.trusted ? process.env.PLAYCRAFT_LAUNCHER_SOCKET : undefined;
49
+ const launched = launcher ? await startLauncherProcess({ socketPath: launcher, processId: input.processId,
50
+ runtimeEpoch: state.runtimeEpoch, argv: ['/usr/bin/bash', '-lc', input.command], cwd: input.cwd, env: input.env ?? {} }) : undefined;
51
+ const local = launched ? undefined : spawn('bash', ['-lc', input.command], { cwd: input.cwd,
52
+ env: { PATH: process.env.PATH, HOME: state.config.stateDir, ...input.env }, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
53
+ const pid = launched?.pid ?? local?.pid;
54
+ if (!pid)
55
+ throw new Error('production_process_not_started');
56
+ const record = { pid, birth: await processBirth(pid), stopped: false };
57
+ try {
58
+ await input.record(record);
59
+ }
60
+ catch (error) {
61
+ await stopProductionGroup(record);
62
+ throw error;
63
+ }
64
+ let output = '';
65
+ const collect = (chunk) => { output = (output + String(chunk)).slice(-64 * 1024); };
66
+ (launched?.stdout ?? local.stdout).on('data', collect);
67
+ (launched?.stderr ?? local.stderr).on('data', collect);
68
+ launched?.stdin?.end();
69
+ let timedOut = false;
70
+ const code = await new Promise((resolve) => {
71
+ const timeout = setTimeout(() => {
72
+ timedOut = true;
73
+ void (launched ? launched.stop() : stopProductionGroup(record)).catch(() => undefined).finally(() => resolve(undefined));
74
+ }, input.timeoutMs);
75
+ const finish = (code) => { clearTimeout(timeout); resolve(code); };
76
+ if (launched)
77
+ launched.onExit(finish);
78
+ else {
79
+ local.once('error', () => finish(undefined));
80
+ local.once('close', finish);
81
+ }
82
+ });
83
+ const stopped = await stopProductionGroup(record);
84
+ await input.record({ ...record, stopped });
85
+ return { code: timedOut ? undefined : code, output, stopped };
86
+ }
@@ -0,0 +1,24 @@
1
+ export function productionSaveCommand() {
2
+ return `set -eu
3
+ status=0
4
+ remote=$(git -c protocol.version=0 -c core.hooksPath=/dev/null ls-remote --exit-code --heads "$PRODUCTION_REPOSITORY" "refs/heads/$PRODUCTION_BRANCH") || status=$?
5
+ if [ "$status" != 0 ] && [ "$status" != 2 ]; then exit "$status"; fi
6
+ if [ "\${PRODUCTION_EXPECTED_PARENT+x}" = x ]; then
7
+ actual=$(printf '%s' "$remote" | cut -f1)
8
+ test "$actual" = "$PRODUCTION_EXPECTED_PARENT" || { printf 'production_git_parent_changed\\n' >&2; exit 1; }
9
+ fi
10
+ if [ -n "$remote" ]; then
11
+ parent=$(printf '%s' "$remote" | cut -f1)
12
+ git -c protocol.version=0 -c core.hooksPath=/dev/null fetch --no-tags --no-recurse-submodules "$PRODUCTION_REPOSITORY" "refs/heads/$PRODUCTION_BRANCH"
13
+ test "$(git rev-parse FETCH_HEAD)" = "$parent"
14
+ git -c core.hooksPath=/dev/null -c core.fsmonitor=false reset --soft FETCH_HEAD
15
+ fi
16
+ git -c core.hooksPath=/dev/null -c core.fsmonitor=false add -A .
17
+ if [ -n "$(git ls-files -- .playcraft.json playcraft.config.json ':(glob)**/.playcraft.json' ':(glob)**/playcraft.config.json')" ]; then
18
+ printf 'production_git_private_path\\n' >&2
19
+ exit 1
20
+ fi
21
+ git diff --cached --quiet || git -c core.hooksPath=/dev/null commit -m "Save generated engineering"
22
+ git -c protocol.version=0 -c core.hooksPath=/dev/null -c remote.origin.url="$PRODUCTION_REPOSITORY" -c remote.origin.pushurl="$PRODUCTION_REPOSITORY" push --set-upstream origin HEAD:"refs/heads/$PRODUCTION_BRANCH"
23
+ git rev-parse HEAD`;
24
+ }
@@ -0,0 +1,91 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { constants } from 'node:fs';
4
+ import { lstat, mkdir, open, readdir, realpath, rename, rm, writeFile } from 'node:fs/promises';
5
+ import { dirname, isAbsolute, join, relative } from 'node:path';
6
+ import { promisify } from 'node:util';
7
+ import { excludeFromGit } from '../workspaces/dependencies.js';
8
+ const execute = promisify(execFile);
9
+ const privateNames = new Set(['.git', 'node_modules', '.ssh', '.aws', '.azure', '.gnupg', '.codex', '.npm', '.pnpm-store',
10
+ '.config', '.npmrc', '.yarnrc', '.yarnrc.yml', '.netrc', '.git-credentials', 'credentials.json', 'credentials', 'secrets',
11
+ 'id_rsa', 'id_ed25519', 'settings.local.json', '.playcraft.json', 'playcraft.config.json', '.playcraft-temp', '.playcraft-preview-out']);
12
+ const engineering = (path) => !path.split('/').some(name => privateNames.has(name)
13
+ || /^\.env(?:$|\.)/.test(name) || /\.(?:pem|key|p12|pfx)$/.test(name)) && !path.startsWith('.playcraft/skills/');
14
+ const inside = (root, path) => { const rel = relative(root, path); return rel !== '..' && !rel.startsWith('../') && !isAbsolute(rel); };
15
+ const git = async (root, ...args) => (await execute('git', ['-c', 'core.hooksPath=/dev/null', '-c', 'credential.helper=',
16
+ '-c', 'core.fsmonitor=false', '-C', root, ...args], { timeout: 90_000, maxBuffer: 64 * 1024 * 1024,
17
+ env: { PATH: process.env.PATH, HOME: dirname(root), GIT_OPTIONAL_LOCKS: '0', GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0' } })).stdout;
18
+ async function sourceFiles(root) {
19
+ const paths = new Set((await git(root, 'ls-files', '--cached', '--others', '--exclude-standard', '-z')).split('\0').filter(Boolean));
20
+ const artifacts = await readdir(join(root, '.playcraft/skill-artifacts')).catch(error => {
21
+ if (error.code === 'ENOENT')
22
+ return [];
23
+ throw error;
24
+ });
25
+ for (const name of artifacts)
26
+ if (/^[a-f0-9]{64}\.tar\.gz$/.test(name))
27
+ paths.add(`.playcraft/skill-artifacts/${name}`);
28
+ const files = new Map();
29
+ for (const path of [...paths].sort()) {
30
+ if (!engineering(path))
31
+ continue;
32
+ const full = join(root, path), stat = await lstat(full).catch(error => {
33
+ if (error.code === 'ENOENT')
34
+ return undefined;
35
+ throw error;
36
+ });
37
+ if (!stat)
38
+ continue;
39
+ if (!stat.isFile() || stat.isSymbolicLink() || !inside(root, await realpath(full)))
40
+ throw new Error('production_source_path_invalid');
41
+ const handle = await open(full, constants.O_RDONLY | constants.O_NOFOLLOW);
42
+ try {
43
+ files.set(path, { body: await handle.readFile(), mode: stat.mode & 0o777 });
44
+ }
45
+ finally {
46
+ await handle.close();
47
+ }
48
+ }
49
+ return files;
50
+ }
51
+ function digest(files) {
52
+ const hash = createHash('sha256');
53
+ for (const [path, file] of files)
54
+ hash.update(path).update('\0').update(String(file.mode)).update('\0').update(file.body).update('\0');
55
+ return hash.digest('hex');
56
+ }
57
+ /** Capture the selected working tree into a private branch; the source index, head and configuration are read only. */
58
+ export async function copyProductionSource(input) {
59
+ const source = await realpath(input.source);
60
+ if (await lstat(input.destination).catch(() => undefined))
61
+ throw new Error('production_source_destination_exists');
62
+ const sourceHead = await git(source, 'rev-parse', '--verify', 'HEAD').catch(() => '');
63
+ const files = await sourceFiles(source), sourceDigest = digest(files);
64
+ const staging = join(dirname(input.destination), `.production-source-${randomUUID()}`);
65
+ try {
66
+ await mkdir(staging);
67
+ // An independent root commit also excludes credentials that existed only in source history.
68
+ await git(staging, '-c', 'init.templateDir=', 'init', '--initial-branch', input.branchName);
69
+ for (const [path, file] of files) {
70
+ await mkdir(dirname(join(staging, path)), { recursive: true });
71
+ await writeFile(join(staging, path), file.body, { mode: file.mode, flag: 'wx' });
72
+ }
73
+ if (sourceHead !== await git(source, 'rev-parse', '--verify', 'HEAD').catch(() => '') || sourceDigest !== digest(await sourceFiles(source)))
74
+ throw new Error('production_source_changed');
75
+ await git(staging, 'remote', 'add', 'origin', input.repositoryUrl);
76
+ await git(staging, 'config', 'user.name', 'PlayCraft');
77
+ await git(staging, 'config', 'user.email', 'playcraft@localhost');
78
+ await excludeFromGit(staging);
79
+ await excludeFromGit(staging, '/.env*');
80
+ for (const entry of ['/.playcraft.json', '/playcraft.config.json', '/.playcraft/skill-artifacts/', '/.playcraft/skills/', '/.playcraft-temp/'])
81
+ await excludeFromGit(staging, entry);
82
+ await git(staging, 'add', '-A', '.');
83
+ await git(staging, 'commit', '--allow-empty', '-m', 'Capture batch source in independent remix');
84
+ const commitHash = (await git(staging, 'rev-parse', 'HEAD')).trim();
85
+ await rename(staging, input.destination);
86
+ return { commitHash, sourceDigest };
87
+ }
88
+ finally {
89
+ await rm(staging, { recursive: true, force: true });
90
+ }
91
+ }
@@ -0,0 +1,202 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { lstat, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
4
+ import { dirname, join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { batchBaselineSnapshotSchema } from '@playcraft/common/cloud-workspace/runtime';
7
+ import { checkoutBatchCommit, freezeBatchCommit } from '../workspaces/batch-commit.js';
8
+ import { grantWorkspaceTraversal, openDirectoryToDevelopment } from '../workspaces/directory-access.js';
9
+ import { registerWorkspaceDirectory } from '../workspaces/directories.js';
10
+ import { workspaceContainerRoot, workspaceRoot, workspaceSessions } from '../workspaces/registry.js';
11
+ import { excludeFromGit, settleDependencyInstalls } from '../workspaces/dependencies.js';
12
+ import { resolveProductionDependencies, verifyProductionDependencyLocks } from './dependencies.js';
13
+ import { productionSaveCommand } from './save-command.js';
14
+ import { productionOutputStore, verifyProductionLease } from './storage.js';
15
+ import { runProductionCommand } from './processes.js';
16
+ import { runProductionNative } from './native.js';
17
+ import { classifyPreviewOutput, findExternalLocalReferences } from '../previews/single-file.js';
18
+ import { copyProductionSource } from './source-copy.js';
19
+ import { workspaceDirectoryOccupied } from '../workspaces/directory-maintenance.js';
20
+ import { ensureWorkspaceSkills } from '../workspaces/skills.js';
21
+ const execute = promisify(execFile);
22
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
23
+ const hash = (body) => createHash('sha256').update(body).digest('hex');
24
+ const record = (value) => value && typeof value === 'object' ? value : {};
25
+ export async function executeProductionStage(state, input, processRecord, track) {
26
+ await verifyProductionLease(input);
27
+ const store = productionOutputStore(input);
28
+ const stage = input.stage;
29
+ const workspaceId = input.task.workspaceId;
30
+ const resultId = String(input.payload.resultVersionId ?? input.task.payload.resultVersionId ?? input.task.attemptId);
31
+ const sourceDirectoryName = String(input.payload.sourceDirectoryName ?? input.task.payload.sourceDirectoryName ?? 'basic');
32
+ if (input.task.payload.sourceDirectoryName !== undefined && input.payload.sourceDirectoryName !== undefined
33
+ && input.task.payload.sourceDirectoryName !== input.payload.sourceDirectoryName)
34
+ throw new Error('production_source_directory_mismatch');
35
+ const previous = (name) => input.stages?.find(value => value.stage === name && value.status === 'completed')?.result;
36
+ const run = async (cwd, command, env, trusted = false) => {
37
+ const outcome = await runProductionCommand(state, { processId: `production-${input.task.attemptId}-${stage}`, cwd, command, env, trusted,
38
+ timeoutMs: 10 * 60_000, record: processRecord });
39
+ if (!outcome.stopped || outcome.code === undefined)
40
+ throw new Error('production_process_unknown');
41
+ if (outcome.code !== 0)
42
+ throw new Error(`production_${stage}_failed`);
43
+ return outcome.output;
44
+ };
45
+ const git = async (root, ...args) => (await execute('git', ['-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', '-C', root, ...args], {
46
+ timeout: 20_000, maxBuffer: 1024 * 1024, env: { PATH: process.env.PATH, HOME: state.config.stateDir, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1' },
47
+ })).stdout.trim();
48
+ if (stage === 'capture-source') {
49
+ if (input.directoryName === 'basic' || input.directoryName === sourceDirectoryName)
50
+ throw new Error('production_source_directory_mismatch');
51
+ const authority = record(input.payload.workspace);
52
+ if (authority.directoryName !== input.directoryName || authority.workspaceId !== workspaceId
53
+ || typeof authority.remixId !== 'string' || typeof authority.branchName !== 'string' || typeof authority.repositoryUrl !== 'string')
54
+ throw new Error('production_source_directory_mismatch');
55
+ const { remixId, branchName, repositoryUrl } = authority;
56
+ await settleDependencyInstalls(state);
57
+ return state.journal.exclusive(`directory:${workspaceId}:${sourceDirectoryName}`, async () => {
58
+ if (await workspaceDirectoryOccupied(state, workspaceId, sourceDirectoryName)
59
+ || workspaceSessions(state, workspaceId, sourceDirectoryName).some(session => session.turnBlock === 'uncertain' || ['starting', 'running', 'waiting_for_user'].includes(session.turnStatus ?? '')))
60
+ throw new Error('production_source_busy');
61
+ const source = await workspaceRoot(state, workspaceId, sourceDirectoryName);
62
+ const container = await workspaceContainerRoot(state, workspaceId);
63
+ const destination = join(container, input.directoryName);
64
+ const developmentGroup = (await lstat(state.config.workspaceRoot)).gid;
65
+ const snapshot = await copyProductionSource({ source, destination, branchName, repositoryUrl });
66
+ await ensureWorkspaceSkills(destination, join(state.config.stateDir, 'skill-artifacts'));
67
+ await openDirectoryToDevelopment(destination, developmentGroup);
68
+ await grantWorkspaceTraversal(container, developmentGroup);
69
+ const binding = state.journal.getWorkspace(workspaceId);
70
+ await state.journal.putWorkspace({ ...binding, directories: [...(binding.directories ?? []).filter(value => value.directoryName !== input.directoryName),
71
+ { workspaceId, directoryName: input.directoryName, remixId, branchName, sourceCommit: snapshot.commitHash, status: 'ready' }] });
72
+ return { sourceSnapshot: snapshot, commitHash: snapshot.commitHash };
73
+ });
74
+ }
75
+ if (stage === 'prepare' && input.directoryName !== 'basic' && (input.payload.baseline ?? input.task.payload.baseline)) {
76
+ const baseline = record(input.payload.baseline ?? input.task.payload.baseline);
77
+ const authority = record(input.payload.workspace);
78
+ const sourceCommit = baseline.commitHash;
79
+ if (typeof sourceCommit !== 'string')
80
+ throw new Error('production_approved_commit_required');
81
+ const directory = { workspaceId, directoryName: input.directoryName,
82
+ remixId: String(authority.remixId), branchName: String(authority.branchName), sourceCommit };
83
+ await registerWorkspaceDirectory(state, directory, state.acceptedLeaseFence, sourceDirectoryName);
84
+ }
85
+ const root = await workspaceRoot(state, workspaceId, input.directoryName);
86
+ if (stage === 'prepare') {
87
+ await settleDependencyInstalls(state);
88
+ if (!(await lstat(join(root, 'package.json')).catch(() => undefined)))
89
+ return { prepared: true, directoryName: input.directoryName, emptySource: true };
90
+ const policy = await resolveProductionDependencies(root).catch(error => {
91
+ if (!(input.payload.baseline ?? input.task.payload.baseline) && error instanceof Error
92
+ && error.message === 'production_dependency_lock_required')
93
+ return undefined;
94
+ throw error;
95
+ });
96
+ if (!policy)
97
+ return { prepared: true, directoryName: input.directoryName, dependencyRepairRequired: true };
98
+ await run(root, process.env.PLAYCRAFT_PRODUCTION_INSTALL_COMMAND ?? 'pnpm install --frozen-lockfile --store-dir /var/cache/playcraft/pnpm-store --ignore-scripts');
99
+ await verifyProductionDependencyLocks(root, policy);
100
+ return { prepared: true, directoryName: input.directoryName };
101
+ }
102
+ if (stage === 'generate') {
103
+ await runProductionNative(state, input, root, processRecord, track);
104
+ await resolveProductionDependencies(root);
105
+ return { generated: true };
106
+ }
107
+ if (stage === 'save') {
108
+ if (!input.git)
109
+ throw new Error('production_git_scope_required');
110
+ const binding = state.journal.getWorkspace(workspaceId);
111
+ const branch = input.directoryName === 'basic' ? binding.branchName : binding.directories?.find(value => value.directoryName === input.directoryName)?.branchName;
112
+ if (input.git.branchName !== branch || await git(root, 'symbolic-ref', '--short', 'HEAD') !== branch)
113
+ throw new Error('production_git_binding_mismatch');
114
+ await excludeFromGit(root);
115
+ await excludeFromGit(root, '/.env*');
116
+ await excludeFromGit(root, '/.playcraft-temp/');
117
+ await excludeFromGit(root, '/.playcraft-preview-out/');
118
+ await git(root, 'config', 'user.name', 'PlayCraft');
119
+ await git(root, 'config', 'user.email', 'playcraft@localhost');
120
+ const env = { PRODUCTION_REPOSITORY: input.git.repositoryUrl, PRODUCTION_BRANCH: input.git.branchName,
121
+ GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1' };
122
+ if (input.git.username && input.git.password)
123
+ Object.assign(env, { GIT_CONFIG_COUNT: '1',
124
+ GIT_CONFIG_KEY_0: `http.${input.git.repositoryUrl}.extraHeader`,
125
+ GIT_CONFIG_VALUE_0: `Authorization: Basic ${Buffer.from(`${input.git.username}:${input.git.password}`).toString('base64')}` });
126
+ if (typeof input.payload.expectedParent === 'string')
127
+ env.PRODUCTION_EXPECTED_PARENT = input.payload.expectedParent;
128
+ await run(root, productionSaveCommand(), env, true);
129
+ const commitHash = await git(root, 'rev-parse', 'HEAD');
130
+ const snapshot = await freezeBatchCommit({ source: { workspaceId, root }, commitHash, snapshotId: `${input.task.attemptId}-save` });
131
+ return { snapshot, saved: { attemptId: input.task.attemptId, resultVersionId: resultId, workspaceId, sourceDigest: snapshot.checksum, commitHash, pushedCommitHash: commitHash } };
132
+ }
133
+ if (stage === 'freeze') {
134
+ const saved = record(previous('save')?.saved);
135
+ if (typeof saved.commitHash !== 'string')
136
+ throw new Error('production_saved_commit_required');
137
+ const snapshot = await freezeBatchCommit({ source: { workspaceId, root }, commitHash: saved.commitHash, snapshotId: `${input.task.attemptId}-freeze` });
138
+ const baseline = batchBaselineSnapshotSchema.parse({ batchId: input.task.batchId, projectId: String(input.payload.projectId ?? input.task.payload.projectId),
139
+ snapshotId: snapshot.snapshotId, sourceWorkspaceId: snapshot.sourceWorkspaceId, commitHash: snapshot.commitHash, checksum: snapshot.checksum,
140
+ sourceVersion: snapshot.sourceVersion, engine: String(input.payload.engine ?? input.task.payload.engine), skillLockDigest: snapshot.skillLockDigest, frozenAt: snapshot.frozenAt });
141
+ return { snapshot, baseline };
142
+ }
143
+ if (stage === 'build') {
144
+ const saved = record(input.payload.saved ?? previous('save')?.saved);
145
+ const snapshot = (previous('freeze')?.snapshot ?? previous('save')?.snapshot);
146
+ if (!snapshot || snapshot.commitHash !== saved.commitHash || snapshot.checksum !== saved.sourceDigest)
147
+ throw new Error('production_saved_snapshot_mismatch');
148
+ const destination = join(root, '.playcraft-temp', `production-${input.task.attemptId}`);
149
+ await excludeFromGit(root, '/.playcraft-temp/');
150
+ await rm(destination, { recursive: true, force: true });
151
+ try {
152
+ await checkoutBatchCommit({ source: { workspaceId, root }, frozen: snapshot, destination });
153
+ await openDirectoryToDevelopment(destination, (await lstat(state.config.workspaceRoot)).gid);
154
+ const policy = await resolveProductionDependencies(destination);
155
+ await run(destination, process.env.PLAYCRAFT_PRODUCTION_INSTALL_COMMAND ?? 'pnpm install --frozen-lockfile --store-dir /var/cache/playcraft/pnpm-store --ignore-scripts');
156
+ await verifyProductionDependencyLocks(destination, policy);
157
+ const output = join(destination, '.production-html');
158
+ const command = process.env.PLAYCRAFT_PRODUCTION_BUILD_COMMAND ?? 'playcraft build --mode full --platform playcraft --format html';
159
+ await run(destination, `${command} --output ${quote(output)} .`);
160
+ await verifyProductionDependencyLocks(destination, policy);
161
+ const entries = await readdir(output, { withFileTypes: true });
162
+ const single = classifyPreviewOutput(entries.map(entry => ({ name: entry.name, kind: entry.isFile() ? 'file' : entry.isDirectory() ? 'dir' : 'symlink' })));
163
+ if (!single.ok)
164
+ throw new Error('production_preview_not_single_file');
165
+ const path = join(output, single.entry);
166
+ if (!(await lstat(path)).isFile())
167
+ throw new Error('production_preview_invalid');
168
+ const body = await readFile(path);
169
+ if (findExternalLocalReferences(body.toString()).length)
170
+ throw new Error('production_preview_external_reference');
171
+ const outputDigest = hash(body), outputKey = `workspaces/${workspaceId}/production/${input.task.attemptId}/${outputDigest}.html`;
172
+ await store.put(outputKey, body);
173
+ return { outputKey, outputDigest, build: { attemptId: input.task.attemptId, resultVersionId: resultId, workspaceId,
174
+ sourceDigest: snapshot.checksum, commitHash: snapshot.commitHash, buildId: `${input.task.attemptId}-build`, succeeded: true } };
175
+ }
176
+ finally {
177
+ await rm(destination, { recursive: true, force: true });
178
+ }
179
+ }
180
+ if (stage === 'preview') {
181
+ const built = previous('build');
182
+ if (typeof built?.outputKey !== 'string' || typeof built.outputDigest !== 'string')
183
+ throw new Error('production_build_required');
184
+ const body = await store.get(built.outputKey);
185
+ if (hash(body) !== built.outputDigest)
186
+ throw new Error('production_preview_digest_mismatch');
187
+ const previewPath = join(root, '.playcraft-temp', `${input.task.attemptId}.html`);
188
+ await mkdir(dirname(previewPath), { recursive: true });
189
+ await writeFile(previewPath, body);
190
+ try {
191
+ const browser = process.env.PLAYCRAFT_PRODUCTION_PREVIEW_COMMAND ?? 'chromium --headless --no-sandbox --disable-gpu --virtual-time-budget=1000 --dump-dom';
192
+ const opened = await run(root, `${browser} ${quote(`file://${previewPath}`)}`);
193
+ if (!/<html[\s>]/i.test(opened))
194
+ throw new Error('production_preview_not_opened');
195
+ return { opened: true, outputDigest: built.outputDigest };
196
+ }
197
+ finally {
198
+ await rm(previewPath, { force: true });
199
+ }
200
+ }
201
+ throw new Error('invalid_production_stage');
202
+ }
@@ -0,0 +1,32 @@
1
+ const taskUrl = (input) => `${input.callbackUrl.replace(/\/+$/, '')}/internal/cloud-production/tasks/${input.task.taskId}`;
2
+ /** Build output travels through the Runtime that leased the task; engineering files travel through Git. */
3
+ export function productionOutputStore(input) {
4
+ const object = (key) => `${taskUrl(input)}/objects/${encodeURIComponent(key)}`;
5
+ const headers = { authorization: `Bearer ${input.runnerToken}`, 'x-playcraft-object-client': 'cli' };
6
+ return {
7
+ async get(key) {
8
+ const response = await fetch(object(key), { headers, redirect: 'error', signal: AbortSignal.timeout(60_000) });
9
+ if (response.status === 404)
10
+ throw new Error('object_missing');
11
+ if (!response.ok)
12
+ throw new Error('object_get_failed');
13
+ return Buffer.from(await response.arrayBuffer());
14
+ },
15
+ async put(key, body) {
16
+ const response = await fetch(object(key), { method: 'PUT', body: new Uint8Array(body), redirect: 'error', signal: AbortSignal.timeout(60_000),
17
+ headers: { ...headers, 'content-type': 'application/octet-stream' } });
18
+ if (!response.ok)
19
+ throw new Error(response.status === 409 ? 'object_conflict' : 'object_put_failed');
20
+ },
21
+ };
22
+ }
23
+ export async function verifyProductionLease(input) {
24
+ const response = await fetch(`${taskUrl(input)}/runner`, {
25
+ headers: { authorization: `Bearer ${input.runnerToken}` }, redirect: 'error', signal: AbortSignal.timeout(10_000),
26
+ });
27
+ if (!response.ok)
28
+ throw new Error('production_lease_unavailable');
29
+ const { task } = await response.json();
30
+ if (!task || task.taskId !== input.task.taskId || task.attemptId !== input.task.attemptId || task.leaseOwner !== input.lease.owner || task.leaseFence !== input.lease.fence || task.status !== 'running' || task.cancelRequested)
31
+ throw new Error('production_lease_unavailable');
32
+ }