@playcraft/cli 0.1.42 → 0.1.44-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.
Files changed (390) hide show
  1. package/README.md +62 -3
  2. package/SKILLS_REGISTRY.md +149 -0
  3. package/dist/agent/local-backend.js +1 -1
  4. package/dist/auth/account-activation.js +157 -0
  5. package/dist/auth/messages.js +30 -0
  6. package/dist/cli-root-help.js +2 -0
  7. package/dist/commands/build-all.js +5 -19
  8. package/dist/commands/build-temp.js +26 -0
  9. package/dist/commands/build.js +15 -28
  10. package/dist/commands/create.js +26 -33
  11. package/dist/commands/image.js +2 -6
  12. package/dist/commands/login.js +84 -42
  13. package/dist/commands/platform-skills.generated.js +19 -0
  14. package/dist/commands/playable.js +53 -0
  15. package/dist/commands/skills.js +4 -0
  16. package/dist/commands/tools-3d.js +11 -0
  17. package/dist/commands/tools-batch.js +105 -0
  18. package/dist/commands/tools-generation.js +69 -137
  19. package/dist/commands/tools-utils.js +0 -12
  20. package/dist/commands/tools.js +2 -2
  21. package/dist/commands/video.js +122 -0
  22. package/dist/commands/workspace-runtime.js +54 -0
  23. package/dist/commands/workspace.js +40 -0
  24. package/dist/config.js +59 -4
  25. package/dist/index.js +40 -3
  26. package/dist/project-skills/builtin-import.js +225 -0
  27. package/dist/project-skills/commands.js +77 -0
  28. package/dist/project-skills/lifecycle.js +28 -0
  29. package/dist/project-skills/local-run.js +112 -0
  30. package/dist/project-skills/local-store.js +221 -0
  31. package/dist/project-skills/messages.js +80 -0
  32. package/dist/project-skills/reconcile.js +62 -0
  33. package/dist/project-skills/remote-cache.js +51 -0
  34. package/dist/project-skills/validation.js +19 -0
  35. package/dist/recording/messages.js +40 -0
  36. package/dist/recording/project-digest.js +85 -0
  37. package/dist/recording/recorder.js +293 -0
  38. package/dist/recording/scenario.js +50 -0
  39. package/dist/remix/clone.js +2 -0
  40. package/dist/remix/init-template.js +2 -0
  41. package/dist/remix/pull.js +2 -0
  42. package/dist/remix/push.js +21 -1
  43. package/dist/skill-registry/client.js +345 -0
  44. package/dist/skill-registry/commands.js +180 -0
  45. package/dist/skill-registry/evaluation-client.js +156 -0
  46. package/dist/skill-registry/evaluation-commands.js +174 -0
  47. package/dist/skill-registry/messages.js +177 -0
  48. package/dist/skill-registry/package.js +275 -0
  49. package/dist/skill-registry/platform-package.js +100 -0
  50. package/dist/utils/agent-api-client.js +210 -30
  51. package/dist/utils/environment-endpoints.js +17 -0
  52. package/dist/utils/error-diagnostics.js +20 -0
  53. package/dist/utils/playcraft-http-client.js +6 -4
  54. package/dist/utils/tool-operation-journal.js +124 -0
  55. package/dist/utils/video-evidence-git.js +81 -0
  56. package/dist/video-replication/client.js +234 -0
  57. package/dist/video-replication/comparison-evidence.js +149 -0
  58. package/dist/video-replication/comparison-files.js +57 -0
  59. package/dist/video-replication/comparison-log.js +72 -0
  60. package/dist/video-replication/comparison-messages.js +104 -0
  61. package/dist/video-replication/comparison-output.js +63 -0
  62. package/dist/video-replication/comparison-preflight.js +66 -0
  63. package/dist/video-replication/comparison.js +230 -0
  64. package/dist/video-replication/diagnostics.js +41 -0
  65. package/dist/video-replication/evidence.js +165 -0
  66. package/dist/video-replication/media.js +112 -0
  67. package/dist/video-replication/messages.js +97 -0
  68. package/dist/workspace/messages.js +67 -0
  69. package/dist/workspace/transfer-client.js +233 -0
  70. package/dist/workspace/transfer-errors.js +48 -0
  71. package/dist/workspace-runtime/batch/control.js +80 -0
  72. package/dist/workspace-runtime/batch/deliver.js +150 -0
  73. package/dist/workspace-runtime/batch/freeze.js +102 -0
  74. package/dist/workspace-runtime/batch/generate.js +205 -0
  75. package/dist/workspace-runtime/batch/git.js +40 -0
  76. package/dist/workspace-runtime/batch/occupancy.js +8 -0
  77. package/dist/workspace-runtime/batch/policy.js +70 -0
  78. package/dist/workspace-runtime/batch/prepare.js +202 -0
  79. package/dist/workspace-runtime/batch/processes.js +72 -0
  80. package/dist/workspace-runtime/batch/records.js +113 -0
  81. package/dist/workspace-runtime/batch/routes.js +58 -0
  82. package/dist/workspace-runtime/batch/source-capture.js +159 -0
  83. package/dist/workspace-runtime/codex/app-server.js +1040 -0
  84. package/dist/workspace-runtime/codex/compact.js +46 -0
  85. package/dist/workspace-runtime/codex/config-toml.js +107 -0
  86. package/dist/workspace-runtime/codex/jsonrpc-stdio.js +145 -0
  87. package/dist/workspace-runtime/codex/launcher-client.js +177 -0
  88. package/dist/workspace-runtime/codex/loopback.js +146 -0
  89. package/dist/workspace-runtime/codex/mcp-config.js +39 -0
  90. package/dist/workspace-runtime/codex/model-broker.js +263 -0
  91. package/dist/workspace-runtime/codex/native-adapter.js +9 -0
  92. package/dist/workspace-runtime/codex/permissions.js +79 -0
  93. package/dist/workspace-runtime/codex/thread-image.js +38 -0
  94. package/dist/workspace-runtime/codex/ui-instructions.js +4 -0
  95. package/dist/workspace-runtime/codex/ui-tools.js +186 -0
  96. package/dist/workspace-runtime/codex/work-mode.js +15 -0
  97. package/dist/workspace-runtime/main.js +10 -0
  98. package/dist/workspace-runtime/persistence/journal.js +365 -0
  99. package/dist/workspace-runtime/persistence/native-subagents.js +162 -0
  100. package/dist/workspace-runtime/persistence/sqlite-client.js +77 -0
  101. package/dist/workspace-runtime/persistence/sqlite-worker.js +1161 -0
  102. package/dist/workspace-runtime/persistence/stopped-execution.js +77 -0
  103. package/dist/workspace-runtime/previews/output.js +36 -0
  104. package/dist/workspace-runtime/previews/publish.js +262 -0
  105. package/dist/workspace-runtime/previews/single-file.js +43 -0
  106. package/dist/workspace-runtime/processes/managed-writes.js +69 -0
  107. package/dist/workspace-runtime/production/content-changes.js +36 -0
  108. package/dist/workspace-runtime/production/contracts.js +19 -0
  109. package/dist/workspace-runtime/production/controller.js +228 -0
  110. package/dist/workspace-runtime/production/dependencies.js +60 -0
  111. package/dist/workspace-runtime/production/native.js +239 -0
  112. package/dist/workspace-runtime/production/processes.js +102 -0
  113. package/dist/workspace-runtime/production/save-command.js +35 -0
  114. package/dist/workspace-runtime/production/source-archive.js +137 -0
  115. package/dist/workspace-runtime/production/source-copy.js +92 -0
  116. package/dist/workspace-runtime/production/stages.js +354 -0
  117. package/dist/workspace-runtime/production/storage.js +75 -0
  118. package/dist/workspace-runtime/serve.js +283 -0
  119. package/dist/workspace-runtime/server/auth.js +21 -0
  120. package/dist/workspace-runtime/server/connectivity.js +209 -0
  121. package/dist/workspace-runtime/server/desktop-host.js +429 -0
  122. package/dist/workspace-runtime/server/desktop-preview-config.js +81 -0
  123. package/dist/workspace-runtime/server/desktop-preview-service.js +157 -0
  124. package/dist/workspace-runtime/server/desktop-preview-static.js +53 -0
  125. package/dist/workspace-runtime/server/desktop-preview.js +189 -0
  126. package/dist/workspace-runtime/server/dispatch.js +1339 -0
  127. package/dist/workspace-runtime/server/endpoints.js +21 -0
  128. package/dist/workspace-runtime/server/execution-group.js +90 -0
  129. package/dist/workspace-runtime/server/http.js +1297 -0
  130. package/dist/workspace-runtime/server/lock.js +53 -0
  131. package/dist/workspace-runtime/server/rfb-watch-gate.js +168 -0
  132. package/dist/workspace-runtime/server/session-restore.js +84 -0
  133. package/dist/workspace-runtime/server/source-uploader.js +138 -0
  134. package/dist/workspace-runtime/server/terminal-host.js +366 -0
  135. package/dist/workspace-runtime/server/types.js +1 -0
  136. package/dist/workspace-runtime/server/workspace-transfer.js +168 -0
  137. package/dist/workspace-runtime/workspaces/archive.js +207 -0
  138. package/dist/workspace-runtime/workspaces/artifact-events.js +55 -0
  139. package/dist/workspace-runtime/workspaces/artifact-files.js +199 -0
  140. package/dist/workspace-runtime/workspaces/artifact-receipts.js +89 -0
  141. package/dist/workspace-runtime/workspaces/artifacts.js +282 -0
  142. package/dist/workspace-runtime/workspaces/asset-library-import.js +366 -0
  143. package/dist/workspace-runtime/workspaces/attachment-content.js +73 -0
  144. package/dist/workspace-runtime/workspaces/attachments.js +185 -0
  145. package/dist/workspace-runtime/workspaces/batch-commit.js +142 -0
  146. package/dist/workspace-runtime/workspaces/binary-content.js +65 -0
  147. package/dist/workspace-runtime/workspaces/binary-write.js +112 -0
  148. package/dist/workspace-runtime/workspaces/cli-context.js +23 -0
  149. package/dist/workspace-runtime/workspaces/cli-credentials.js +111 -0
  150. package/dist/workspace-runtime/workspaces/composer-context.js +147 -0
  151. package/dist/workspace-runtime/workspaces/context-error.js +2 -0
  152. package/dist/workspace-runtime/workspaces/dependencies.js +264 -0
  153. package/dist/workspace-runtime/workspaces/developer-command.js +64 -0
  154. package/dist/workspace-runtime/workspaces/directories.js +137 -0
  155. package/dist/workspace-runtime/workspaces/directory-access.js +37 -0
  156. package/dist/workspace-runtime/workspaces/directory-attachments.js +245 -0
  157. package/dist/workspace-runtime/workspaces/directory-create.js +57 -0
  158. package/dist/workspace-runtime/workspaces/directory-maintenance.js +111 -0
  159. package/dist/workspace-runtime/workspaces/directory-stats.js +49 -0
  160. package/dist/workspace-runtime/workspaces/disk-snapshot.js +31 -0
  161. package/dist/workspace-runtime/workspaces/empty-template.js +42 -0
  162. package/dist/workspace-runtime/workspaces/file-content.js +48 -0
  163. package/dist/workspace-runtime/workspaces/files-search.js +116 -0
  164. package/dist/workspace-runtime/workspaces/files.js +150 -0
  165. package/dist/workspace-runtime/workspaces/git-credential-broker.js +312 -0
  166. package/dist/workspace-runtime/workspaces/git-credential-helper.js +68 -0
  167. package/dist/workspace-runtime/workspaces/git-direct-store.js +155 -0
  168. package/dist/workspace-runtime/workspaces/git.js +670 -0
  169. package/dist/workspace-runtime/workspaces/instructions.js +56 -0
  170. package/dist/workspace-runtime/workspaces/json5-edit.js +170 -0
  171. package/dist/workspace-runtime/workspaces/library-attachments.js +268 -0
  172. package/dist/workspace-runtime/workspaces/optional-platform-skills.js +14 -0
  173. package/dist/workspace-runtime/workspaces/parameters.js +272 -0
  174. package/dist/workspace-runtime/workspaces/prepare.js +278 -0
  175. package/dist/workspace-runtime/workspaces/registry.js +251 -0
  176. package/dist/workspace-runtime/workspaces/remix-references.js +301 -0
  177. package/dist/workspace-runtime/workspaces/revisions.js +31 -0
  178. package/dist/workspace-runtime/workspaces/session-image.js +104 -0
  179. package/dist/workspace-runtime/workspaces/shared-git.js +40 -0
  180. package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +95 -0
  181. package/dist/workspace-runtime/workspaces/skills.js +191 -0
  182. package/dist/workspace-runtime/workspaces/transfer-quota.js +69 -0
  183. package/dist/workspace-runtime/workspaces/transfer.js +471 -0
  184. package/dist/workspace-runtime/workspaces/turn-changes.js +75 -0
  185. package/dist/workspace-runtime/workspaces/video-attachments.js +49 -0
  186. package/package.json +15 -5
  187. package/project-template/.claude/agents/refs/ta-image-generation-detail.md +123 -67
  188. package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +63 -135
  189. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +33 -42
  190. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +20 -18
  191. package/project-template/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +7 -6
  192. package/project-template/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +39 -39
  193. package/project-template/.claude/skills/playcraft-replica-validation/SKILL.md +25 -0
  194. package/project-template/.claude/skills/playcraft-replica-validation/references/contracts.md +79 -0
  195. package/project-template/.claude/skills/playcraft-video-understanding/SKILL.md +29 -0
  196. package/project-template/.claude/skills/playcraft-video-understanding/references/contracts.md +37 -0
  197. package/project-template-v2/.claude/agents/artist.md +82 -0
  198. package/project-template-v2/.claude/agents/developer.md +153 -0
  199. package/project-template-v2/.claude/agents/game-designer.md +264 -0
  200. package/project-template-v2/.claude/agents/refs/artist-art-style-catalog.md +533 -0
  201. package/project-template-v2/.claude/agents/refs/artist-color-audio-recipes.md +153 -0
  202. package/project-template-v2/.claude/agents/refs/artist-dimension-axis.md +27 -0
  203. package/project-template-v2/.claude/agents/refs/artist-master-composite-recipes.md +208 -0
  204. package/project-template-v2/.claude/agents/refs/atom-skill-library.md +81 -0
  205. package/project-template-v2/.claude/agents/refs/developer-impl-cookbook.md +432 -0
  206. package/project-template-v2/.claude/agents/refs/framework-5-component-filter.md +252 -0
  207. package/project-template-v2/.claude/agents/refs/framework-game-feel-juice.md +266 -0
  208. package/project-template-v2/.claude/agents/refs/framework-mda.md +147 -0
  209. package/project-template-v2/.claude/agents/refs/game-designer-gameplay-sufficiency.md +123 -0
  210. package/project-template-v2/.claude/agents/refs/ta-3d-flip-recipe.md +88 -0
  211. package/project-template-v2/.claude/agents/refs/ta-atlas-deliverable-standard.md +67 -0
  212. package/project-template-v2/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
  213. package/project-template-v2/.claude/agents/refs/ta-image-generation-detail.md +300 -0
  214. package/project-template-v2/.claude/agents/refs/ta-image-ops-reference.md +495 -0
  215. package/project-template-v2/.claude/agents/refs/ta-pipeline-cookbook.md +1141 -0
  216. package/project-template-v2/.claude/agents/refs/ta-tools-reference.md +111 -0
  217. package/project-template-v2/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
  218. package/project-template-v2/.claude/agents/refs/threejs-cannon-pitfalls.md +412 -0
  219. package/project-template-v2/.claude/agents/reviewer.md +75 -0
  220. package/project-template-v2/.claude/agents/technical-artist.md +86 -0
  221. package/project-template-v2/.claude/hooks/snapshot-milestone.mjs +243 -0
  222. package/project-template-v2/.claude/hooks/user-prompt.mjs +133 -0
  223. package/project-template-v2/.claude/settings.json +33 -0
  224. package/project-template-v2/.claude/skills/brainstorming/SKILL.md +161 -0
  225. package/project-template-v2/.claude/skills/brainstorming/scripts/frame-template.html +270 -0
  226. package/project-template-v2/.claude/skills/brainstorming/scripts/helper.js +177 -0
  227. package/project-template-v2/.claude/skills/brainstorming/scripts/server.cjs +354 -0
  228. package/project-template-v2/.claude/skills/brainstorming/scripts/start-server.sh +148 -0
  229. package/project-template-v2/.claude/skills/brainstorming/scripts/stop-server.sh +56 -0
  230. package/project-template-v2/.claude/skills/brainstorming/scripts/wait-for-selection.sh +62 -0
  231. package/project-template-v2/.claude/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  232. package/project-template-v2/.claude/skills/brainstorming/visual-companion.md +309 -0
  233. package/project-template-v2/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
  234. package/project-template-v2/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
  235. package/project-template-v2/.claude/skills/playcraft-asset-state-sheet/SKILL.md +205 -0
  236. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
  237. package/project-template-v2/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
  238. package/project-template-v2/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
  239. package/project-template-v2/.claude/skills/playcraft-image-generation/SKILL.md +298 -0
  240. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
  241. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
  242. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +324 -0
  243. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
  244. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
  245. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
  246. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
  247. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
  248. package/project-template-v2/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
  249. package/project-template-v2/.claude/skills/playcraft-image-processing/SKILL.md +219 -0
  250. package/project-template-v2/.claude/skills/playcraft-masking/SKILL.md +373 -0
  251. package/project-template-v2/.claude/skills/playcraft-playable-optimization/SKILL.md +161 -0
  252. package/project-template-v2/.claude/skills/playcraft-research/SKILL.md +215 -0
  253. package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +382 -0
  254. package/project-template-v2/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
  255. package/project-template-v2/.claude/skills/playcraft-sprite-remix/SKILL.md +158 -0
  256. package/project-template-v2/.claude/skills/playcraft-sprite-sheet/SKILL.md +100 -0
  257. package/project-template-v2/.claude/skills/playcraft-storyboard/SKILL.md +167 -0
  258. package/project-template-v2/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
  259. package/project-template-v2/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
  260. package/project-template-v2/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
  261. package/project-template-v2/.claude/skills/playwright-cli/SKILL.md +390 -0
  262. package/project-template-v2/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
  263. package/project-template-v2/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
  264. package/project-template-v2/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
  265. package/project-template-v2/.claude/skills/playwright-cli/references/running-code.md +240 -0
  266. package/project-template-v2/.claude/skills/playwright-cli/references/session-management.md +226 -0
  267. package/project-template-v2/.claude/skills/playwright-cli/references/spec-driven-testing.md +308 -0
  268. package/project-template-v2/.claude/skills/playwright-cli/references/storage-state.md +275 -0
  269. package/project-template-v2/.claude/skills/playwright-cli/references/test-generation.md +134 -0
  270. package/project-template-v2/.claude/skills/playwright-cli/references/tracing.md +142 -0
  271. package/project-template-v2/.claude/skills/playwright-cli/references/video-recording.md +153 -0
  272. package/project-template-v2/.claude/skills/session-analyzer/SKILL.md +386 -0
  273. package/project-template-v2/.claude/skills/session-analyzer/scripts/execution-breakdown.mjs +182 -0
  274. package/project-template-v2/.claude/skills/session-analyzer/scripts/find-turns.mjs +72 -0
  275. package/project-template-v2/.claude/skills/session-analyzer/scripts/heavy-output.mjs +121 -0
  276. package/project-template-v2/.claude/skills/session-analyzer/scripts/resolve-session.mjs +102 -0
  277. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-stats.mjs +127 -0
  278. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-tool-timeline.mjs +106 -0
  279. package/project-template-v2/.claude/skills/session-analyzer/scripts/time-gaps.mjs +128 -0
  280. package/project-template-v2/.claude/skills/session-analyzer/scripts/turn-timeline.mjs +67 -0
  281. package/project-template-v2/.claude/snapshot.mjs +263 -0
  282. package/project-template-v2/.playcraft/skills.lock.json +152 -0
  283. package/project-template-v2/CLAUDE.md +146 -0
  284. package/project-template-v2/assets/audio/bgm/.gitkeep +0 -0
  285. package/project-template-v2/assets/audio/sfx/.gitkeep +0 -0
  286. package/project-template-v2/assets/bundles/.gitkeep +0 -0
  287. package/project-template-v2/assets/images/bg/.gitkeep +0 -0
  288. package/project-template-v2/assets/images/reference/.gitkeep +0 -0
  289. package/project-template-v2/assets/images/storyboard/.gitkeep +0 -0
  290. package/project-template-v2/assets/images/tiles/.gitkeep +0 -0
  291. package/project-template-v2/assets/images/ui/.gitkeep +0 -0
  292. package/project-template-v2/assets/images/vfx/.gitkeep +0 -0
  293. package/project-template-v2/assets/models/.gitkeep +0 -0
  294. package/project-template-v2/docs/harness/iteration-1/context-flow.md +254 -0
  295. package/project-template-v2/docs/harness/iteration-1/generate-flow.md +91 -0
  296. package/project-template-v2/docs/harness/iteration-1/ideate-flow.md +214 -0
  297. package/project-template-v2/docs/harness/iteration-1/optimize-flow.md +75 -0
  298. package/project-template-v2/docs/harness/iteration-1/wrapup-flow.md +63 -0
  299. package/project-template-v2/docs/harness/iteration-2/context-flow.md +223 -0
  300. package/project-template-v2/docs/harness/iteration-2/generate-flow.md +129 -0
  301. package/project-template-v2/docs/harness/iteration-2/ideate-flow.md +267 -0
  302. package/project-template-v2/docs/harness/iteration-2/optimize-flow.md +164 -0
  303. package/project-template-v2/docs/harness/iteration-2/wrapup-flow.md +115 -0
  304. package/project-template-v2/docs/harness/orchestrator-flow.md +364 -0
  305. package/project-template-v2/docs/project-state.json +60 -0
  306. package/project-template-v2/docs/project-state.md +72 -0
  307. package/project-template-v2/docs/standards/README.md +225 -0
  308. package/project-template-v2/docs/standards/agent-behavior-standards.md +174 -0
  309. package/project-template-v2/docs/standards/artifacts/design-brief.md +19 -0
  310. package/project-template-v2/docs/standards/artifacts/design.md +22 -0
  311. package/project-template-v2/docs/standards/artifacts/game-code.md +41 -0
  312. package/project-template-v2/docs/standards/artifacts/todo-list.md +41 -0
  313. package/project-template-v2/docs/standards/iter1-agent-behavior-standards.md +343 -0
  314. package/project-template-v2/game/index.ts +18 -0
  315. package/project-template-v2/globals.d.ts +51 -0
  316. package/project-template-v2/index.css +34 -0
  317. package/project-template-v2/index.html +18 -0
  318. package/project-template-v2/main.ts +9 -0
  319. package/project-template-v2/package.json +46 -0
  320. package/project-template-v2/skills/_shared/scripts/dispatch-clear.mjs +31 -0
  321. package/project-template-v2/skills/_shared/scripts/dispatch-set.mjs +86 -0
  322. package/project-template-v2/skills/_shared/scripts/dod-check.mjs +153 -0
  323. package/project-template-v2/skills/_shared/scripts/handoff-append.mjs +70 -0
  324. package/project-template-v2/skills/_shared/scripts/lib/validator-artifacts.mjs +91 -0
  325. package/project-template-v2/skills/_shared/scripts/pipeline/dod-config.mjs +131 -0
  326. package/project-template-v2/skills/_shared/scripts/pipeline/index.mjs +80 -0
  327. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-1-core.mjs +90 -0
  328. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-2-wrap.mjs +93 -0
  329. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-3-visual.mjs +23 -0
  330. package/project-template-v2/skills/_shared/scripts/render-project-state.mjs +230 -0
  331. package/project-template-v2/skills/_shared/scripts/state-advance-stage.mjs +58 -0
  332. package/project-template-v2/skills/_shared/scripts/state-get.mjs +339 -0
  333. package/project-template-v2/skills/_shared/scripts/state-handoff.mjs +39 -0
  334. package/project-template-v2/skills/_shared/scripts/state-set.mjs +94 -0
  335. package/project-template-v2/skills/_shared/scripts/state-store.mjs +783 -0
  336. package/project-template-v2/skills/_shared/scripts/todo-add.mjs +88 -0
  337. package/project-template-v2/skills/_shared/scripts/todo-get.mjs +130 -0
  338. package/project-template-v2/skills/_shared/scripts/todo-remove.mjs +33 -0
  339. package/project-template-v2/skills/_shared/scripts/todo-set.mjs +47 -0
  340. package/project-template-v2/skills/_shared/scripts/verify-asset-code-sync.mjs +268 -0
  341. package/project-template-v2/skills/_shared/scripts/verify-env.mjs +285 -0
  342. package/project-template-v2/skills/_shared/scripts/verify-placeholders.mjs +161 -0
  343. package/project-template-v2/skills/playable-autoplay/SKILL.md +176 -0
  344. package/project-template-v2/skills/playable-autoplay/agents/openai.yaml +4 -0
  345. package/project-template-v2/skills/playable-debug/SKILL.md +116 -0
  346. package/project-template-v2/skills/playable-debug/agents/openai.yaml +4 -0
  347. package/project-template-v2/skills/playable-debug/references/debug-config.md +40 -0
  348. package/project-template-v2/skills/playable-record/SKILL.md +140 -0
  349. package/project-template-v2/skills/playable-record/scripts/lib/dev-server.mjs +104 -0
  350. package/project-template-v2/skills/playable-record/scripts/lib/record-audio-bridge.js +141 -0
  351. package/project-template-v2/skills/playable-record/scripts/record-playable.mjs +425 -0
  352. package/project-template-v2/skills/playable-record/scripts/verify-contract.mjs +261 -0
  353. package/project-template-v2/skills/playable-record/scripts/verify-firstwin.mjs +398 -0
  354. package/project-template-v2/skills/playable-record/scripts/verify-fusion.mjs +322 -0
  355. package/project-template-v2/skills/playable-record/scripts/verify-lifecycle.mjs +105 -0
  356. package/project-template-v2/skills/playable-record/scripts/verify-vlm-video.mjs +230 -0
  357. package/project-template-v2/skills/playable-validate/SKILL.md +61 -0
  358. package/project-template-v2/skills/playable-validate/validation-rules.md +14 -0
  359. package/project-template-v2/skills/playable-verify-ui/SKILL.md +63 -0
  360. package/project-template-v2/skills/playable-verify-ui/scripts/lib/__init__.py +1 -0
  361. package/project-template-v2/skills/playable-verify-ui/scripts/lib/config_expr.js +218 -0
  362. package/project-template-v2/skills/playable-verify-ui/scripts/lib/node_utils.js +51 -0
  363. package/project-template-v2/skills/playable-verify-ui/scripts/lib/profile_loader.py +59 -0
  364. package/project-template-v2/skills/playable-verify-ui/scripts/lib/report.py +106 -0
  365. package/project-template-v2/skills/playable-verify-ui/scripts/lib/runtime_contract.js +116 -0
  366. package/project-template-v2/skills/playable-verify-ui/scripts/verify_fetch_antipatterns.py +59 -0
  367. package/project-template-v2/skills/playable-verify-ui/scripts/verify_hardcoded_layout.js +64 -0
  368. package/project-template-v2/skills/playable-verify-ui/scripts/verify_runtime_contract.js +58 -0
  369. package/project-template-v2/skills/playable-verify-ui/scripts/verify_text_overlap.py +333 -0
  370. package/project-template-v2/ta-workspace/scripts/.gitkeep +0 -0
  371. package/project-template-v2/tsconfig.json +20 -0
  372. package/project-template-v2/vite.config.ts +27 -0
  373. package/project-templates/batch-locks/phaser.yaml +922 -0
  374. package/project-templates/batch-locks/threejs.yaml +978 -0
  375. package/project-templates/empty/AGENTS.md +41 -0
  376. package/project-templates/empty/gitignore +33 -0
  377. package/project-templates/phaser/.claude/agents/refs/ta-image-generation-detail.md +59 -59
  378. package/project-templates/phaser/.claude/skills/playcraft-image-generation/SKILL.md +63 -135
  379. package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +33 -42
  380. package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +20 -18
  381. package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +7 -6
  382. package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +39 -39
  383. package/project-templates/threejs/.claude/agents/refs/ta-image-generation-detail.md +59 -59
  384. package/project-templates/threejs/.claude/skills/playcraft-image-generation/SKILL.md +63 -135
  385. package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +33 -42
  386. package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +20 -18
  387. package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +7 -6
  388. package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +39 -39
  389. package/dist/commands/tools-image-context.js +0 -29
  390. package/dist/utils/image-asset-context.js +0 -228
@@ -0,0 +1,233 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createReadStream, createWriteStream, existsSync } from 'node:fs';
3
+ import { mkdir, mkdtemp, readFile, rm, stat, statfs, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join, resolve } from 'node:path';
6
+ import { Readable, Transform } from 'node:stream';
7
+ import { pipeline } from 'node:stream/promises';
8
+ import { RESOURCE_GRANT_ACTIONS, workspaceSessionScopeSchema } from '@playcraft/common/cloud-workspace/runtime';
9
+ import { PlayCraftHttpClient } from '../utils/playcraft-http-client.js';
10
+ import { createWorkspacePushTransfer, inspectWorkspaceCommit, RAW_ATTRIBUTES, runTransferGit, transferGit } from '../workspace-runtime/workspaces/transfer.js';
11
+ import { workspaceMessage as message } from './messages.js';
12
+ import { normalizeWorkspaceTransferError, transferByteCount, workspaceTransferFailure } from './transfer-errors.js';
13
+ const META_FILE = '.playcraft-workspace.json';
14
+ export function workspaceServiceUrl(baseUrl, path) {
15
+ const base = new URL(baseUrl);
16
+ if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password || base.search || base.hash)
17
+ throw new Error(message('invalidTarget'));
18
+ return `${base.toString().replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
19
+ }
20
+ async function request(client, path, init = {}) {
21
+ const headers = new Headers(init.headers);
22
+ headers.set('Authorization', `Bearer ${client.token}`);
23
+ headers.set('X-Request-Id', client.requestId);
24
+ let response;
25
+ try {
26
+ response = await fetch(workspaceServiceUrl(client.baseUrl, path), {
27
+ ...init, headers, redirect: 'error', signal: AbortSignal.timeout(15 * 60_000),
28
+ });
29
+ }
30
+ catch (error) {
31
+ throw normalizeWorkspaceTransferError(error);
32
+ }
33
+ if (response.ok)
34
+ return response;
35
+ const body = await response.json().catch(() => ({}));
36
+ const value = body && typeof body === 'object' ? body : {};
37
+ const nested = value.error && typeof value.error === 'object' ? value.error : {};
38
+ const code = String(nested.code ?? value.reason ?? value.code ?? value.message ?? value.error ?? response.status);
39
+ const rawDetails = nested.details ?? value.details;
40
+ const details = rawDetails && typeof rawDetails === 'object' ? rawDetails : undefined;
41
+ if (response.status === 401)
42
+ throw new Error(`${message('login')} ${client.baseUrl}`);
43
+ if (code.startsWith('workspace_transfer_') || code === 'invalid_workspace_transfer')
44
+ throw workspaceTransferFailure(code, details);
45
+ if (response.status === 413)
46
+ throw workspaceTransferFailure('workspace_transfer_proxy_too_large');
47
+ if (response.status === 408 || response.status === 504)
48
+ throw workspaceTransferFailure('workspace_transfer_timeout');
49
+ throw workspaceTransferFailure(code, details);
50
+ }
51
+ async function json(client, path, body) {
52
+ const response = await request(client, path, body === undefined ? {} : {
53
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
54
+ });
55
+ const value = await response.json();
56
+ return (value?.result ?? value);
57
+ }
58
+ async function ensureAccess(client, workspaceId, directoryName) {
59
+ const workspace = await json(client, `/api/cloud-workspaces/${workspaceId}`);
60
+ const scope = workspace.projectId
61
+ ? `/api/projects/${encodeURIComponent(workspace.projectId)}/cloud-workspaces/${workspaceId}`
62
+ : `/api/cloud-workspaces/${workspaceId}`;
63
+ let grant = await json(client, `${scope}/runtime-grants`, {
64
+ clientRequestId: `workspace-cli-${randomUUID()}`, actions: RESOURCE_GRANT_ACTIONS,
65
+ });
66
+ for (let attempt = 0; grant.grantStatus === 'pending' && grant.grantRequestId && attempt < 60; attempt++) {
67
+ await new Promise(resolve => setTimeout(resolve, 500));
68
+ grant = await json(client, `${scope}/runtime-grants/${encodeURIComponent(grant.grantRequestId)}`);
69
+ }
70
+ if (grant.grantStatus !== 'ready')
71
+ throw new Error(message('notReady'));
72
+ let operation = await json(client, `/runtime/workspaces/${workspaceId}/ensure`, { clientRequestId: randomUUID(), directoryName: directoryName === 'root' ? 'basic' : directoryName });
73
+ for (let attempt = 0; operation.operationId && ['queued', 'dispatched', 'accepted'].includes(operation.status ?? '') && attempt < 60; attempt++) {
74
+ await new Promise(resolve => setTimeout(resolve, 500));
75
+ operation = await json(client, `/runtime/operations/${encodeURIComponent(operation.operationId)}`);
76
+ }
77
+ if (operation.status !== 'completed')
78
+ throw new Error(message('notReady'));
79
+ }
80
+ async function localDiskSpace(path, required) {
81
+ const disk = await statfs(path);
82
+ const available = disk.bavail * disk.bsize;
83
+ if (required > available)
84
+ throw workspaceTransferFailure('workspace_transfer_disk_full', { actual: required, limit: available });
85
+ return available;
86
+ }
87
+ async function downloadBundle(response, target, temporary) {
88
+ if (!response.body)
89
+ throw new Error(message('failed'));
90
+ try {
91
+ const available = await localDiskSpace(temporary, Number(response.headers.get('content-length') ?? 0));
92
+ let bytes = 0;
93
+ const guard = new Transform({ transform(chunk, _encoding, callback) {
94
+ bytes += chunk.length;
95
+ callback(bytes > available ? workspaceTransferFailure('workspace_transfer_disk_full', { actual: bytes, limit: available }) : null, chunk);
96
+ } });
97
+ await pipeline(Readable.fromWeb(response.body), guard, createWriteStream(target, { flags: 'wx', mode: 0o600 }));
98
+ const declared = response.headers.get('content-length');
99
+ if (declared && Number(declared) !== bytes)
100
+ throw workspaceTransferFailure('invalid_workspace_transfer');
101
+ }
102
+ finally {
103
+ await response.body.cancel().catch(() => undefined);
104
+ }
105
+ }
106
+ export async function workspaceClone(options) {
107
+ const workspaceId = options.workspaceId.trim(), directoryName = options.directory ?? 'basic';
108
+ if (!/^[a-f0-9-]{36}$/i.test(workspaceId) || !workspaceSessionScopeSchema.safeParse(directoryName).success)
109
+ throw new Error(message('invalidTarget'));
110
+ const target = resolve(options.dir?.trim() || `workspace-${workspaceId}-${directoryName}`);
111
+ if (existsSync(target))
112
+ throw new Error(message('directoryExists'));
113
+ const client = new PlayCraftHttpClient(options);
114
+ await ensureAccess(client, workspaceId, directoryName);
115
+ const temporary = await mkdtemp(join(tmpdir(), 'playcraft-clone-'));
116
+ let created = false;
117
+ try {
118
+ const response = await request(client, `/runtime/workspaces/${workspaceId}/git-bundle?directoryName=${encodeURIComponent(directoryName)}`);
119
+ const tree = response.headers.get('x-playcraft-workspace-tree');
120
+ if (!tree || !/^[a-f0-9]{40,64}$/.test(tree)) {
121
+ await response.body?.cancel();
122
+ throw new Error(message('failed'));
123
+ }
124
+ const bundlePath = join(temporary, 'workspace.bundle');
125
+ await downloadBundle(response, bundlePath, temporary);
126
+ await mkdir(target);
127
+ created = true;
128
+ const contentBytes = Number(response.headers.get('x-playcraft-workspace-content-bytes') ?? 0);
129
+ if (Number.isSafeInteger(contentBytes) && contentBytes > 0)
130
+ await localDiskSpace(target, contentBytes * 2);
131
+ await runTransferGit(temporary, ['clone', '--no-checkout', '--', bundlePath, target]);
132
+ await writeFile(join(target, '.git', 'info', 'attributes'), RAW_ATTRIBUTES);
133
+ await runTransferGit(target, ['checkout', '--force', 'HEAD']);
134
+ if (transferGit(target, ['rev-parse', 'HEAD^{tree}']) !== tree)
135
+ throw new Error(message('failed'));
136
+ transferGit(target, ['config', '--unset-all', 'remote.origin.url']);
137
+ const meta = { schemaVersion: 'playcraft.workspace-checkout.v1', workspaceId, directoryName,
138
+ baseUrl: client.baseUrl, tree };
139
+ await writeFile(join(target, META_FILE), `${JSON.stringify(meta, null, 2)}\n`);
140
+ const excludePath = join(target, '.git', 'info', 'exclude');
141
+ const exclude = await readFile(excludePath, 'utf8').catch(() => '');
142
+ await writeFile(excludePath, `${exclude}\n/${META_FILE}\n`);
143
+ if (options.json)
144
+ console.log(JSON.stringify({ ok: true, command: 'workspace.clone', data: { ...meta, dir: target } }));
145
+ else
146
+ console.log(`${message('cloned')} ${target}`);
147
+ }
148
+ catch (error) {
149
+ if (created)
150
+ await rm(target, { recursive: true, force: true });
151
+ throw normalizeWorkspaceTransferError(error);
152
+ }
153
+ finally {
154
+ await rm(temporary, { recursive: true, force: true });
155
+ }
156
+ }
157
+ export async function workspacePush(options = {}) {
158
+ const root = resolve(options.dir?.trim() || process.cwd());
159
+ let meta;
160
+ try {
161
+ meta = JSON.parse(await readFile(join(root, META_FILE), 'utf8'));
162
+ if (meta.schemaVersion !== 'playcraft.workspace-checkout.v1' || !/^[a-f0-9-]{36}$/i.test(meta.workspaceId) ||
163
+ !workspaceSessionScopeSchema.safeParse(meta.directoryName).success || !/^[a-f0-9]{40,64}$/.test(meta.tree))
164
+ throw new Error();
165
+ workspaceServiceUrl(meta.baseUrl, '/');
166
+ }
167
+ catch {
168
+ throw new Error(message('metadataMissing'));
169
+ }
170
+ if (transferGit(root, ['status', '--porcelain']).length > 0)
171
+ throw new Error(message('cleanRequired'));
172
+ const client = new PlayCraftHttpClient({ url: options.url || meta.baseUrl, token: options.token });
173
+ if (workspaceServiceUrl(client.baseUrl, '/') !== workspaceServiceUrl(meta.baseUrl, '/'))
174
+ throw new Error(message('invalidTarget'));
175
+ await ensureAccess(client, meta.workspaceId, meta.directoryName);
176
+ const targetTree = transferGit(root, ['rev-parse', 'HEAD^{tree}']);
177
+ const stats = await inspectWorkspaceCommit(root, targetTree).catch(error => { throw normalizeWorkspaceTransferError(error); });
178
+ const path = `/runtime/workspaces/${meta.workspaceId}/git-bundle?directoryName=${encodeURIComponent(meta.directoryName)}`;
179
+ const preflightResponse = await request(client, `${path}&preflight=1&contentBytes=${stats.bytes}&fileCount=${stats.files}&expectedTree=${meta.tree}&targetTree=${targetTree}`);
180
+ if (!preflightResponse.headers.get('content-type')?.includes('application/json')) {
181
+ await preflightResponse.body?.cancel();
182
+ throw workspaceTransferFailure('workspace_transfer_upgrade_required');
183
+ }
184
+ const preflight = await preflightResponse.json();
185
+ if (preflight.protocolVersion !== 2)
186
+ throw workspaceTransferFailure('workspace_transfer_upgrade_required');
187
+ if (preflight.alreadyApplied && preflight.tree === targetTree) {
188
+ meta.tree = targetTree;
189
+ await writeFile(join(root, META_FILE), `${JSON.stringify(meta, null, 2)}\n`);
190
+ if (options.json)
191
+ console.log(JSON.stringify({ ok: true, command: 'workspace.push', data: { tree: targetTree, changedPaths: [] } }));
192
+ else
193
+ console.log(message('pushed'));
194
+ return;
195
+ }
196
+ if (!options.json)
197
+ console.error(message('preflight', { files: stats.files, bytes: transferByteCount(stats.bytes) }));
198
+ let snapshot;
199
+ try {
200
+ snapshot = await createWorkspacePushTransfer(root, meta.tree);
201
+ if (snapshot.tree !== targetTree)
202
+ throw workspaceTransferFailure('workspace_transfer_conflict');
203
+ const bundleBytes = (await stat(snapshot.bundlePath)).size;
204
+ if (!options.json)
205
+ console.error(message('upload', { bytes: transferByteCount(bundleBytes) }));
206
+ const stream = createReadStream(snapshot.bundlePath);
207
+ let response;
208
+ try {
209
+ response = await request(client, path, {
210
+ method: 'POST', headers: { 'content-type': 'application/octet-stream', 'content-length': String(bundleBytes),
211
+ 'x-playcraft-workspace-tree': meta.tree }, body: stream, duplex: 'half',
212
+ });
213
+ }
214
+ finally {
215
+ stream.destroy();
216
+ }
217
+ const result = await response.json();
218
+ if (!/^[a-f0-9]{40,64}$/.test(result.tree))
219
+ throw new Error(message('failed'));
220
+ meta.tree = result.tree;
221
+ await writeFile(join(root, META_FILE), `${JSON.stringify(meta, null, 2)}\n`);
222
+ if (options.json)
223
+ console.log(JSON.stringify({ ok: true, command: 'workspace.push', data: result }));
224
+ else
225
+ console.log(message('pushed'));
226
+ }
227
+ catch (error) {
228
+ throw normalizeWorkspaceTransferError(error);
229
+ }
230
+ finally {
231
+ await snapshot?.cleanup();
232
+ }
233
+ }
@@ -0,0 +1,48 @@
1
+ import { WorkspaceTransferError } from '../workspace-runtime/workspaces/transfer.js';
2
+ import { workspaceMessage as message } from './messages.js';
3
+ export class WorkspaceCliTransferError extends Error {
4
+ code;
5
+ details;
6
+ constructor(code, text, details) {
7
+ super(text);
8
+ this.code = code;
9
+ this.details = details;
10
+ }
11
+ }
12
+ export function transferByteCount(value) {
13
+ return typeof value === 'number' && Number.isFinite(value)
14
+ ? message('byteCount', { value: value.toLocaleString('en-US') }) : message('byteCount', { value: '—' });
15
+ }
16
+ export function workspaceTransferFailure(code, details) {
17
+ const key = code === 'workspace_transfer_quota_exceeded' ? 'quotaExceeded'
18
+ : code === 'workspace_transfer_disk_full' ? typeof details?.actual === 'number' && typeof details.limit === 'number' ? 'diskFull' : 'diskFullUnknown'
19
+ : code === 'workspace_transfer_storage_unavailable' ? 'diskUnavailable'
20
+ : code === 'workspace_transfer_proxy_too_large' ? 'proxyTooLarge'
21
+ : code === 'workspace_transfer_too_large' ? 'tooLarge'
22
+ : code.includes('timeout') ? 'timeout'
23
+ : code === 'workspace_transfer_network' ? 'network'
24
+ : code === 'workspace_transfer_conflict' ? 'conflict'
25
+ : code === 'workspace_transfer_busy' ? 'busy'
26
+ : code === 'workspace_transfer_git_missing' ? 'gitMissing'
27
+ : code === 'workspace_transfer_base_missing' ? 'baseMissing'
28
+ : code.startsWith('workspace_transfer_git_') ? 'gitFailed'
29
+ : code === 'invalid_workspace_transfer' || code === 'workspace_transfer_symlink' ? 'invalidTransfer'
30
+ : code === 'workspace_transfer_upgrade_required' ? 'upgradeRequired' : 'failed';
31
+ const text = message(key, { actual: transferByteCount(details?.actual), limit: transferByteCount(details?.limit) });
32
+ return new WorkspaceCliTransferError(code, key === 'failed' ? `${text}: ${code}` : text, details);
33
+ }
34
+ export function normalizeWorkspaceTransferError(error) {
35
+ if (error instanceof WorkspaceCliTransferError)
36
+ return error;
37
+ if (error instanceof WorkspaceTransferError)
38
+ return workspaceTransferFailure(error.code, error.details);
39
+ const value = (error && typeof error === 'object' ? error : {});
40
+ if (value.code === 'ENOSPC')
41
+ return workspaceTransferFailure('workspace_transfer_disk_full');
42
+ if (/Timeout|Abort/.test(value.name ?? '') || /TIMEOUT/.test(value.cause?.code ?? ''))
43
+ return workspaceTransferFailure('workspace_transfer_timeout');
44
+ if (/UND_ERR_SOCKET|ECONNRESET|ECONNREFUSED|ENOTFOUND|EPIPE/.test(value.cause?.code ?? value.code ?? '') ||
45
+ error instanceof TypeError && /fetch|network|terminated/i.test(error.message))
46
+ return workspaceTransferFailure('workspace_transfer_network');
47
+ return error instanceof Error ? error : new Error(String(error));
48
+ }
@@ -0,0 +1,80 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { sandboxBatchMemberStopSchema } from '@playcraft/common/cloud-workspace/runtime';
3
+ import { groupExited, stopProcessGroup } from './processes.js';
4
+ import { memberStatus, readMember, serializeRun, writeMember, writeStopMarker } from './records.js';
5
+ async function prove(record) {
6
+ let stopped = true;
7
+ for (const process of record.processes) {
8
+ if (process.stopped || groupExited(process.pid))
9
+ process.stopped = true;
10
+ else {
11
+ process.stopped = await stopProcessGroup(process);
12
+ if (!process.stopped)
13
+ stopped = false;
14
+ }
15
+ }
16
+ return stopped;
17
+ }
18
+ function sessionBusy(state, record) {
19
+ const session = record.sessionId ? state.journal.getSession(record.sessionId) : undefined;
20
+ if (!session)
21
+ return false;
22
+ return session.turnBlock === 'uncertain' || session.turnStatus === 'starting' || session.turnStatus === 'running';
23
+ }
24
+ /** Read-only besides reconciling process proof. Does not start work or advance a phase. */
25
+ export async function readBatchMemberStatus(state, runId) {
26
+ const record = await readMember(state, runId);
27
+ if (!record)
28
+ return undefined;
29
+ const processes = record.processes.map(process => ({ ...process, stopped: process.stopped || groupExited(process.pid) }));
30
+ const live = processes.some(process => !process.stopped);
31
+ let next = { ...record, processes };
32
+ if (live && (record.writersStopped || record.state === 'completed' || record.state === 'stopped' || record.state === 'needs_user' || record.state === 'failed')) {
33
+ next = { ...next, writersStopped: false, state: record.stopRequested ? 'stopping' : 'unknown', holding: true };
34
+ }
35
+ else if (!live && record.state === 'running') {
36
+ const turnLive = sessionBusy(state, record);
37
+ next = { ...next, writersStopped: !turnLive, state: record.stopRequested ? 'stopped' : turnLive ? 'running' : 'unknown', holding: turnLive || !record.stopRequested };
38
+ }
39
+ if (JSON.stringify(next) !== JSON.stringify(record))
40
+ next = await writeMember(state, next);
41
+ return memberStatus(next);
42
+ }
43
+ export async function stopBatchMember(state, runId, body) {
44
+ const parsed = sandboxBatchMemberStopSchema.safeParse({ ...(body && typeof body === 'object' ? body : {}), runId });
45
+ if (!parsed.success)
46
+ throw new Error('invalid_batch_request');
47
+ const command = parsed.data;
48
+ return serializeRun(runId, async () => {
49
+ const existing = await readMember(state, runId);
50
+ if (existing && command.ownerFence < existing.ownerFence)
51
+ throw new Error('stale_owner');
52
+ if (existing?.state === 'completed' && existing.phase === 'deliver' && existing.evidence && existing.writersStopped)
53
+ return memberStatus(existing);
54
+ await writeStopMarker(state, runId, command.ownerFence);
55
+ const session = existing?.sessionId ? state.journal.getSession(existing.sessionId) : undefined;
56
+ if (session?.threadId && existing?.sessionId) {
57
+ await state.native?.interrupt?.({ sessionId: existing.sessionId, threadId: session.threadId }).catch(() => undefined);
58
+ const deadline = Date.now() + 2_000;
59
+ while (sessionBusy(state, existing) && Date.now() < deadline)
60
+ await delay(15);
61
+ }
62
+ const record = existing ?? {
63
+ runId, ownerFence: command.ownerFence, occupancyId: '00000000-0000-0000-0000-000000000000', workspaceId: '00000000-0000-0000-0000-000000000000',
64
+ directoryName: 'basic', phase: 'prepare', state: 'stopping', writersStopped: false, holding: true, stopRequested: true, processes: [],
65
+ updatedAt: new Date().toISOString(),
66
+ };
67
+ const writersStopped = await prove(record) && !sessionBusy(state, record);
68
+ const saved = await writeMember(state, {
69
+ ...record, ownerFence: command.ownerFence, stopRequested: true, state: writersStopped ? 'stopped' : 'stopping',
70
+ writersStopped, holding: !writersStopped,
71
+ });
72
+ return memberStatus(saved);
73
+ });
74
+ }
75
+ export async function rememberProcess(state, runId, process) {
76
+ const record = await readMember(state, runId);
77
+ if (!record)
78
+ return;
79
+ await writeMember(state, { ...record, processes: [...record.processes.filter(item => item.pid !== process.pid), process] });
80
+ }
@@ -0,0 +1,150 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { lstat, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { hashOperationPayload, sandboxBatchMemberDeliverSchema } from '@playcraft/common/cloud-workspace/runtime';
5
+ import { findExternalLocalReferences } from '../previews/single-file.js';
6
+ import { workspaceRoot } from '../workspaces/registry.js';
7
+ import { gitRead, gitWrite } from './git.js';
8
+ import { groupExited, runProcessGroup, stopProcessGroup } from './processes.js';
9
+ import { filesystemObjectStore, memberStatus, readMember, readStopMarker, serializeRun, writeMember, } from './records.js';
10
+ function buildCommand() {
11
+ return process.env.PLAYCRAFT_BATCH_BUILD_COMMAND
12
+ ?? 'playcraft build --mode full --platform playcraft --format html --output dist/index.html .';
13
+ }
14
+ async function saveExact(root) {
15
+ await gitWrite(root, ['add', '-A', '.']);
16
+ const privatePaths = await gitRead(root, ['ls-files', '--', '.playcraft.json', 'playcraft.config.json', ':(glob)**/.playcraft.json', ':(glob)**/playcraft.config.json']);
17
+ if (privatePaths.trim())
18
+ throw new Error('batch_private_path');
19
+ const clean = await gitWrite(root, ['diff', '--cached', '--quiet']).then(() => true, () => false);
20
+ if (!clean)
21
+ await gitWrite(root, ['commit', '-m', 'Save batch member']);
22
+ return (await gitRead(root, ['rev-parse', 'HEAD'])).trim();
23
+ }
24
+ async function prove(record) {
25
+ let stopped = true;
26
+ for (const process of record.processes) {
27
+ if (!(process.stopped || groupExited(process.pid))) {
28
+ process.stopped = await stopProcessGroup(process);
29
+ if (!process.stopped)
30
+ stopped = false;
31
+ }
32
+ else
33
+ process.stopped = true;
34
+ }
35
+ return stopped;
36
+ }
37
+ export async function deliverBatchMember(state, body, options) {
38
+ if (!body || typeof body !== 'object' || !('occupancyId' in body) || !body.occupancyId)
39
+ throw new Error('occupancy_required');
40
+ const parsed = sandboxBatchMemberDeliverSchema.safeParse(body);
41
+ if (!parsed.success)
42
+ throw new Error('invalid_batch_request');
43
+ const command = parsed.data;
44
+ const hash = hashOperationPayload(command);
45
+ return serializeRun(command.runId, async () => {
46
+ const existing = await readMember(state, command.runId);
47
+ if (existing && command.ownerFence < existing.ownerFence)
48
+ throw new Error('stale_owner');
49
+ if (existing?.state === 'completed' && existing.phase === 'deliver' && existing.evidence && existing.commandHash?.deliver === hash && existing.ownerFence === command.ownerFence) {
50
+ return memberStatus(existing);
51
+ }
52
+ if (await readStopMarker(state, command.runId) && existing?.state === 'completed' && existing.evidence)
53
+ return memberStatus(existing);
54
+ if (await readStopMarker(state, command.runId))
55
+ return memberStatus(await markStopped(state, command, existing));
56
+ if (existing && existing.ownerFence === command.ownerFence && existing.commandHash?.deliver && existing.commandHash.deliver !== hash && existing.state === 'completed') {
57
+ throw new Error('batch_command_conflict');
58
+ }
59
+ const root = await workspaceRoot(state, command.workspaceId, command.directoryName);
60
+ let record = await writeMember(state, {
61
+ runId: command.runId, ownerFence: command.ownerFence, occupancyId: command.occupancyId, workspaceId: command.workspaceId,
62
+ directoryName: command.directoryName, phase: 'deliver', state: 'running', writersStopped: false, holding: true, stopRequested: false,
63
+ processes: existing?.processes ?? [], sessionId: existing?.sessionId, operationId: existing?.operationId, commandHash: { ...existing?.commandHash, deliver: hash },
64
+ snapshotDigest: existing?.snapshotDigest, branchName: existing?.branchName, updatedAt: new Date().toISOString(),
65
+ });
66
+ const buildRoot = join(state.config.stateDir, 'batch-builds', `${command.runId}-${randomUUID()}`);
67
+ try {
68
+ const savedCommit = command.fromCommit ?? await saveExact(root);
69
+ if (command.fromCommit)
70
+ await gitRead(root, ['cat-file', '-e', `${command.fromCommit}^{commit}`]);
71
+ if (await readStopMarker(state, command.runId))
72
+ return memberStatus(await markStopped(state, command, record));
73
+ let pushedCommit;
74
+ if (command.pushRemote) {
75
+ const branch = existing?.branchName ?? (await gitRead(root, ['symbolic-ref', '--short', 'HEAD'])).trim();
76
+ try {
77
+ await gitWrite(root, ['push', '--porcelain', 'origin', `${savedCommit}:refs/heads/${branch}`]);
78
+ }
79
+ catch {
80
+ throw new Error('batch_push_failed');
81
+ }
82
+ const remote = (await gitRead(root, ['ls-remote', 'origin', `refs/heads/${branch}`])).slice(0, savedCommit.length);
83
+ if (remote !== savedCommit)
84
+ throw new Error('batch_push_failed');
85
+ pushedCommit = savedCommit;
86
+ }
87
+ await mkdir(join(buildRoot, '..'), { recursive: true });
88
+ await gitWrite(join(buildRoot, '..'), ['clone', '--local', '--no-hardlinks', '--no-checkout', '--', root, buildRoot]);
89
+ await gitWrite(buildRoot, ['checkout', '--detach', savedCommit]);
90
+ const built = await runProcessGroup(buildRoot, buildCommand(), 'build', async (process) => {
91
+ record = await writeMember(state, { ...record, processes: [...record.processes, process] });
92
+ });
93
+ record = await writeMember(state, { ...record, processes: record.processes.map(process => process.pid === built.process.pid ? built.process : process) });
94
+ if (await readStopMarker(state, command.runId))
95
+ return memberStatus(await markStopped(state, command, record));
96
+ if (built.code !== 0)
97
+ throw new Error('batch_build_failed');
98
+ const htmlPath = join(buildRoot, 'dist', 'index.html');
99
+ if (!(await lstat(htmlPath).catch(() => undefined))?.isFile())
100
+ throw new Error('batch_build_failed');
101
+ const entries = await readdir(join(buildRoot, 'dist'));
102
+ if (!entries.includes('index.html'))
103
+ throw new Error('batch_build_failed');
104
+ const bodyBytes = await readFile(htmlPath);
105
+ if (findExternalLocalReferences(bodyBytes.toString()).length)
106
+ throw new Error('batch_verify_failed');
107
+ const buildDigest = createHash('sha256').update(bodyBytes).digest('hex');
108
+ const previewRef = `workspaces/${command.workspaceId}/batches/members/${command.runId}/${buildDigest}.html`;
109
+ const store = options?.objectStore ?? filesystemObjectStore(state);
110
+ await store.put(previewRef, bodyBytes);
111
+ const published = await store.get(previewRef);
112
+ if (createHash('sha256').update(published).digest('hex') !== buildDigest)
113
+ throw new Error('batch_verify_failed');
114
+ await mkdir(join(root, 'dist'), { recursive: true });
115
+ await writeFile(join(root, 'dist', 'index.html'), published);
116
+ if (createHash('sha256').update(await readFile(join(root, 'dist', 'index.html'))).digest('hex') !== buildDigest)
117
+ throw new Error('batch_verify_failed');
118
+ const writersStopped = await prove(record);
119
+ if (!writersStopped)
120
+ return memberStatus(await writeMember(state, { ...record, state: 'stopping', writersStopped: false, holding: true, headCommit: savedCommit }));
121
+ if (await readStopMarker(state, command.runId))
122
+ return memberStatus(await markStopped(state, command, { ...record, headCommit: savedCommit }));
123
+ record = await writeMember(state, {
124
+ ...record, state: 'completed', writersStopped: true, holding: false, headCommit: savedCommit, error: undefined,
125
+ evidence: { savedCommit, ...(pushedCommit ? { pushedCommit } : {}), buildDigest, verified: true, previewRef },
126
+ });
127
+ return memberStatus(record);
128
+ }
129
+ catch (error) {
130
+ const code = error instanceof Error && /^[a-z0-9_]+$/.test(error.message) ? error.message : 'batch_deliver_failed';
131
+ const writersStopped = await prove(record);
132
+ record = await writeMember(state, { ...record, state: writersStopped ? 'failed' : 'stopping', writersStopped, holding: !writersStopped, error: code });
133
+ return memberStatus(record);
134
+ }
135
+ finally {
136
+ await rm(buildRoot, { recursive: true, force: true });
137
+ }
138
+ });
139
+ }
140
+ async function markStopped(state, command, existing) {
141
+ if (existing)
142
+ await prove(existing);
143
+ return writeMember(state, {
144
+ runId: command.runId, ownerFence: command.ownerFence, occupancyId: existing?.occupancyId ?? command.occupancyId,
145
+ workspaceId: command.workspaceId, directoryName: command.directoryName, phase: 'deliver', state: 'stopped', writersStopped: true,
146
+ holding: false, stopRequested: true, processes: existing?.processes ?? [], sessionId: existing?.sessionId, operationId: existing?.operationId,
147
+ headCommit: existing?.headCommit, evidence: existing?.evidence, commandHash: existing?.commandHash, snapshotDigest: existing?.snapshotDigest,
148
+ branchName: existing?.branchName, updatedAt: new Date().toISOString(),
149
+ });
150
+ }
@@ -0,0 +1,102 @@
1
+ import { lstat, rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { statfs } from 'node:fs/promises';
4
+ import { batchSourceFreezeResultSchema, batchSourceRemoteRef, sandboxBatchSourceFreezeRequestSchema, workspaceDiskAdmission, } from '@playcraft/common/cloud-workspace/runtime';
5
+ import { stopBackgroundWriter } from '../processes/managed-writes.js';
6
+ import { workspaceRoot } from '../workspaces/registry.js';
7
+ import { copyCredentialHelpers, gitRead, gitWrite } from './git.js';
8
+ import { archiveSource, captureIndependentSource } from './source-capture.js';
9
+ import { filesystemObjectStore, readSnapshot, serializeFreeze, sourceCacheDir, writeSnapshot, } from './records.js';
10
+ async function usagePercent(path) {
11
+ const disk = await statfs(path);
12
+ if (disk.blocks <= 0)
13
+ throw new Error('disk_usage_unavailable');
14
+ return ((disk.blocks - disk.bfree) / disk.blocks) * 100;
15
+ }
16
+ /** Dependency installs are keyed `workspaceId:directoryName`. A bare workspace id is not this directory. */
17
+ async function stopManagedWriters(state, workspaceId, directoryName) {
18
+ const scope = `${workspaceId}:${directoryName}`;
19
+ for (const write of state.journal.listManagedWrites()) {
20
+ if (write.stoppedAt || write.workspaceId !== scope)
21
+ continue;
22
+ if (write.pid !== undefined) {
23
+ const stopped = await stopBackgroundWriter(write.pid);
24
+ if (!stopped)
25
+ continue;
26
+ }
27
+ await state.journal.putManagedWrite({ ...write, stoppedAt: new Date().toISOString() });
28
+ }
29
+ }
30
+ async function pushHiddenRef(cache, sourceRoot, remote, commit, ref) {
31
+ await copyCredentialHelpers(sourceRoot, cache);
32
+ try {
33
+ await gitWrite(cache, ['push', remote, `${commit}:${ref}`]);
34
+ }
35
+ catch {
36
+ throw new Error('batch_source_push_failed');
37
+ }
38
+ const remoteHash = (await gitRead(cache, ['ls-remote', remote, ref]).catch(() => '')).slice(0, commit.length);
39
+ if (remoteHash !== commit)
40
+ throw new Error('batch_source_push_failed');
41
+ }
42
+ export async function freezeBatchSource(state, body, options) {
43
+ const parsed = sandboxBatchSourceFreezeRequestSchema.safeParse(body);
44
+ if (!parsed.success)
45
+ throw new Error('invalid_batch_request');
46
+ const request = parsed.data;
47
+ return serializeFreeze(async () => {
48
+ const existing = await readSnapshot(state, request.batchId);
49
+ if (existing)
50
+ return batchSourceFreezeResultSchema.parse({ status: 'frozen', snapshot: existing });
51
+ const source = await workspaceRoot(state, request.workspaceId, request.sourceDirectoryName);
52
+ const percent = Math.min(100, Math.max(0, options?.usagePercent ?? await usagePercent(source)));
53
+ if (workspaceDiskAdmission(percent) === 'paused')
54
+ throw new Error('disk_admission_paused');
55
+ await stopManagedWriters(state, request.workspaceId, request.sourceDirectoryName);
56
+ const cache = sourceCacheDir(state, request.batchId);
57
+ await rm(cache, { recursive: true, force: true });
58
+ const captured = await captureIndependentSource(source, cache);
59
+ await writeFile(`${cache}.digest`, captured.digest, { mode: 0o600 });
60
+ const binding = state.journal.getWorkspace(request.workspaceId);
61
+ const frozenAt = new Date((await gitRead(cache, ['show', '-s', '--format=%cI', 'HEAD'])).trim()).toISOString();
62
+ const snapshot = {
63
+ batchId: request.batchId,
64
+ commitHash: captured.commitHash,
65
+ digest: captured.digest,
66
+ sourceWorkspaceId: request.workspaceId,
67
+ sourceDirectoryName: request.sourceDirectoryName,
68
+ frozenAt,
69
+ };
70
+ try {
71
+ if (request.persist === 'remote_ref') {
72
+ if (!binding?.repositoryUrl)
73
+ throw new Error('batch_source_push_failed');
74
+ const ref = batchSourceRemoteRef(request.batchId);
75
+ await pushHiddenRef(cache, source, binding.repositoryUrl, captured.commitHash, ref);
76
+ snapshot.remoteRef = ref;
77
+ }
78
+ else {
79
+ const key = `workspaces/${request.workspaceId}/batches/${request.batchId}/source/${captured.digest}.json.gz`;
80
+ const store = options?.objectStore ?? filesystemObjectStore(state);
81
+ try {
82
+ await store.put(key, await archiveSource(cache, captured));
83
+ }
84
+ catch (error) {
85
+ if (error instanceof Error && error.message === 'batch_object_key_invalid')
86
+ throw error;
87
+ throw new Error('batch_source_upload_failed');
88
+ }
89
+ snapshot.artifactKey = key;
90
+ }
91
+ }
92
+ catch (error) {
93
+ await rm(cache, { recursive: true, force: true });
94
+ await rm(`${cache}.digest`, { force: true });
95
+ throw error;
96
+ }
97
+ if (!await lstat(join(cache, '.git')).catch(() => undefined))
98
+ throw new Error('batch_source_capture_failed');
99
+ await writeSnapshot(state, snapshot);
100
+ return batchSourceFreezeResultSchema.parse({ status: 'frozen', snapshot });
101
+ });
102
+ }