@playcraft/cli 0.0.56 → 0.0.58

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 (228) hide show
  1. package/README.md +41 -26
  2. package/REMIX.md +17 -5
  3. package/dist/cli-root-help.js +1 -0
  4. package/dist/commands/build-all.js +10 -9
  5. package/dist/commands/build.js +13 -12
  6. package/dist/commands/create.js +26 -33
  7. package/dist/commands/platform-skills.generated.js +19 -0
  8. package/dist/commands/remix.js +48 -2
  9. package/dist/commands/skills.js +2 -0
  10. package/dist/commands/tools-generation.js +1 -1
  11. package/dist/commands/workspace-runtime.js +54 -0
  12. package/dist/index.js +3 -1
  13. package/dist/project-skills/commands.js +72 -0
  14. package/dist/project-skills/lifecycle.js +28 -0
  15. package/dist/project-skills/local-run.js +112 -0
  16. package/dist/project-skills/local-store.js +149 -0
  17. package/dist/project-skills/messages.js +72 -0
  18. package/dist/project-skills/reconcile.js +62 -0
  19. package/dist/project-skills/remote-cache.js +51 -0
  20. package/dist/project-skills/validation.js +19 -0
  21. package/dist/remix/clone.js +4 -1
  22. package/dist/remix/init-template.js +2 -0
  23. package/dist/remix/local-git.js +56 -0
  24. package/dist/remix/pull.js +66 -0
  25. package/dist/remix/push.js +90 -0
  26. package/dist/utils/agent-api-client.js +54 -17
  27. package/dist/utils/tool-operation-journal.js +54 -0
  28. package/dist/workspace-runtime/codex/app-server.js +395 -0
  29. package/dist/workspace-runtime/codex/config-toml.js +25 -0
  30. package/dist/workspace-runtime/codex/jsonrpc-stdio.js +106 -0
  31. package/dist/workspace-runtime/codex/loopback.js +59 -0
  32. package/dist/workspace-runtime/codex/native-adapter.js +3 -0
  33. package/dist/workspace-runtime/main.js +10 -0
  34. package/dist/workspace-runtime/persistence/journal.js +350 -0
  35. package/dist/workspace-runtime/processes/managed-writes.js +69 -0
  36. package/dist/workspace-runtime/serve.js +111 -0
  37. package/dist/workspace-runtime/server/auth.js +21 -0
  38. package/dist/workspace-runtime/server/dispatch.js +861 -0
  39. package/dist/workspace-runtime/server/execution-group.js +88 -0
  40. package/dist/workspace-runtime/server/http.js +561 -0
  41. package/dist/workspace-runtime/server/lock.js +53 -0
  42. package/dist/workspace-runtime/server/types.js +1 -0
  43. package/dist/workspace-runtime/workspaces/context-error.js +2 -0
  44. package/dist/workspace-runtime/workspaces/file-snapshot.js +213 -0
  45. package/dist/workspace-runtime/workspaces/files.js +143 -0
  46. package/dist/workspace-runtime/workspaces/git.js +242 -0
  47. package/dist/workspace-runtime/workspaces/json5-edit.js +170 -0
  48. package/dist/workspace-runtime/workspaces/parameters.js +272 -0
  49. package/dist/workspace-runtime/workspaces/prepare.js +364 -0
  50. package/dist/workspace-runtime/workspaces/registry.js +107 -0
  51. package/dist/workspace-runtime/workspaces/revisions.js +31 -0
  52. package/package.json +6 -3
  53. package/project-template-v2/.claude/agents/artist.md +82 -0
  54. package/project-template-v2/.claude/agents/developer.md +153 -0
  55. package/project-template-v2/.claude/agents/game-designer.md +264 -0
  56. package/project-template-v2/.claude/agents/refs/artist-art-style-catalog.md +533 -0
  57. package/project-template-v2/.claude/agents/refs/artist-color-audio-recipes.md +153 -0
  58. package/project-template-v2/.claude/agents/refs/artist-dimension-axis.md +27 -0
  59. package/project-template-v2/.claude/agents/refs/artist-master-composite-recipes.md +208 -0
  60. package/project-template-v2/.claude/agents/refs/atom-skill-library.md +81 -0
  61. package/project-template-v2/.claude/agents/refs/developer-impl-cookbook.md +432 -0
  62. package/project-template-v2/.claude/agents/refs/framework-5-component-filter.md +252 -0
  63. package/project-template-v2/.claude/agents/refs/framework-game-feel-juice.md +266 -0
  64. package/project-template-v2/.claude/agents/refs/framework-mda.md +147 -0
  65. package/project-template-v2/.claude/agents/refs/game-designer-gameplay-sufficiency.md +123 -0
  66. package/project-template-v2/.claude/agents/refs/ta-3d-flip-recipe.md +88 -0
  67. package/project-template-v2/.claude/agents/refs/ta-atlas-deliverable-standard.md +67 -0
  68. package/project-template-v2/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
  69. package/project-template-v2/.claude/agents/refs/ta-image-generation-detail.md +300 -0
  70. package/project-template-v2/.claude/agents/refs/ta-image-ops-reference.md +495 -0
  71. package/project-template-v2/.claude/agents/refs/ta-pipeline-cookbook.md +1141 -0
  72. package/project-template-v2/.claude/agents/refs/ta-tools-reference.md +111 -0
  73. package/project-template-v2/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
  74. package/project-template-v2/.claude/agents/refs/threejs-cannon-pitfalls.md +412 -0
  75. package/project-template-v2/.claude/agents/reviewer.md +75 -0
  76. package/project-template-v2/.claude/agents/technical-artist.md +86 -0
  77. package/project-template-v2/.claude/hooks/snapshot-milestone.mjs +243 -0
  78. package/project-template-v2/.claude/hooks/user-prompt.mjs +133 -0
  79. package/project-template-v2/.claude/settings.json +33 -0
  80. package/project-template-v2/.claude/skills/brainstorming/SKILL.md +161 -0
  81. package/project-template-v2/.claude/skills/brainstorming/scripts/frame-template.html +270 -0
  82. package/project-template-v2/.claude/skills/brainstorming/scripts/helper.js +177 -0
  83. package/project-template-v2/.claude/skills/brainstorming/scripts/server.cjs +354 -0
  84. package/project-template-v2/.claude/skills/brainstorming/scripts/start-server.sh +148 -0
  85. package/project-template-v2/.claude/skills/brainstorming/scripts/stop-server.sh +56 -0
  86. package/project-template-v2/.claude/skills/brainstorming/scripts/wait-for-selection.sh +62 -0
  87. package/project-template-v2/.claude/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  88. package/project-template-v2/.claude/skills/brainstorming/visual-companion.md +309 -0
  89. package/project-template-v2/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
  90. package/project-template-v2/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
  91. package/project-template-v2/.claude/skills/playcraft-asset-state-sheet/SKILL.md +205 -0
  92. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
  93. package/project-template-v2/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
  94. package/project-template-v2/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
  95. package/project-template-v2/.claude/skills/playcraft-image-generation/SKILL.md +298 -0
  96. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
  97. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
  98. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +324 -0
  99. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
  100. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
  101. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
  102. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
  103. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
  104. package/project-template-v2/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
  105. package/project-template-v2/.claude/skills/playcraft-image-processing/SKILL.md +219 -0
  106. package/project-template-v2/.claude/skills/playcraft-masking/SKILL.md +373 -0
  107. package/project-template-v2/.claude/skills/playcraft-playable-optimization/SKILL.md +161 -0
  108. package/project-template-v2/.claude/skills/playcraft-research/SKILL.md +215 -0
  109. package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +382 -0
  110. package/project-template-v2/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
  111. package/project-template-v2/.claude/skills/playcraft-sprite-remix/SKILL.md +158 -0
  112. package/project-template-v2/.claude/skills/playcraft-sprite-sheet/SKILL.md +100 -0
  113. package/project-template-v2/.claude/skills/playcraft-storyboard/SKILL.md +167 -0
  114. package/project-template-v2/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
  115. package/project-template-v2/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
  116. package/project-template-v2/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
  117. package/project-template-v2/.claude/skills/playwright-cli/SKILL.md +390 -0
  118. package/project-template-v2/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
  119. package/project-template-v2/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
  120. package/project-template-v2/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
  121. package/project-template-v2/.claude/skills/playwright-cli/references/running-code.md +240 -0
  122. package/project-template-v2/.claude/skills/playwright-cli/references/session-management.md +226 -0
  123. package/project-template-v2/.claude/skills/playwright-cli/references/spec-driven-testing.md +308 -0
  124. package/project-template-v2/.claude/skills/playwright-cli/references/storage-state.md +275 -0
  125. package/project-template-v2/.claude/skills/playwright-cli/references/test-generation.md +134 -0
  126. package/project-template-v2/.claude/skills/playwright-cli/references/tracing.md +142 -0
  127. package/project-template-v2/.claude/skills/playwright-cli/references/video-recording.md +153 -0
  128. package/project-template-v2/.claude/skills/session-analyzer/SKILL.md +386 -0
  129. package/project-template-v2/.claude/skills/session-analyzer/scripts/execution-breakdown.mjs +182 -0
  130. package/project-template-v2/.claude/skills/session-analyzer/scripts/find-turns.mjs +72 -0
  131. package/project-template-v2/.claude/skills/session-analyzer/scripts/heavy-output.mjs +121 -0
  132. package/project-template-v2/.claude/skills/session-analyzer/scripts/resolve-session.mjs +102 -0
  133. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-stats.mjs +127 -0
  134. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-tool-timeline.mjs +106 -0
  135. package/project-template-v2/.claude/skills/session-analyzer/scripts/time-gaps.mjs +128 -0
  136. package/project-template-v2/.claude/skills/session-analyzer/scripts/turn-timeline.mjs +67 -0
  137. package/project-template-v2/.claude/snapshot.mjs +263 -0
  138. package/project-template-v2/.playcraft/skills.lock.json +152 -0
  139. package/project-template-v2/CLAUDE.md +146 -0
  140. package/project-template-v2/assets/audio/bgm/.gitkeep +0 -0
  141. package/project-template-v2/assets/audio/sfx/.gitkeep +0 -0
  142. package/project-template-v2/assets/bundles/.gitkeep +0 -0
  143. package/project-template-v2/assets/images/bg/.gitkeep +0 -0
  144. package/project-template-v2/assets/images/reference/.gitkeep +0 -0
  145. package/project-template-v2/assets/images/storyboard/.gitkeep +0 -0
  146. package/project-template-v2/assets/images/tiles/.gitkeep +0 -0
  147. package/project-template-v2/assets/images/ui/.gitkeep +0 -0
  148. package/project-template-v2/assets/images/vfx/.gitkeep +0 -0
  149. package/project-template-v2/assets/models/.gitkeep +0 -0
  150. package/project-template-v2/docs/harness/iteration-1/context-flow.md +254 -0
  151. package/project-template-v2/docs/harness/iteration-1/generate-flow.md +91 -0
  152. package/project-template-v2/docs/harness/iteration-1/ideate-flow.md +214 -0
  153. package/project-template-v2/docs/harness/iteration-1/optimize-flow.md +75 -0
  154. package/project-template-v2/docs/harness/iteration-1/wrapup-flow.md +63 -0
  155. package/project-template-v2/docs/harness/iteration-2/context-flow.md +223 -0
  156. package/project-template-v2/docs/harness/iteration-2/generate-flow.md +129 -0
  157. package/project-template-v2/docs/harness/iteration-2/ideate-flow.md +267 -0
  158. package/project-template-v2/docs/harness/iteration-2/optimize-flow.md +164 -0
  159. package/project-template-v2/docs/harness/iteration-2/wrapup-flow.md +115 -0
  160. package/project-template-v2/docs/harness/orchestrator-flow.md +364 -0
  161. package/project-template-v2/docs/project-state.json +60 -0
  162. package/project-template-v2/docs/project-state.md +72 -0
  163. package/project-template-v2/docs/standards/README.md +225 -0
  164. package/project-template-v2/docs/standards/agent-behavior-standards.md +174 -0
  165. package/project-template-v2/docs/standards/artifacts/design-brief.md +19 -0
  166. package/project-template-v2/docs/standards/artifacts/design.md +22 -0
  167. package/project-template-v2/docs/standards/artifacts/game-code.md +41 -0
  168. package/project-template-v2/docs/standards/artifacts/todo-list.md +41 -0
  169. package/project-template-v2/docs/standards/iter1-agent-behavior-standards.md +343 -0
  170. package/project-template-v2/game/index.ts +18 -0
  171. package/project-template-v2/globals.d.ts +51 -0
  172. package/project-template-v2/index.css +34 -0
  173. package/project-template-v2/index.html +18 -0
  174. package/project-template-v2/main.ts +9 -0
  175. package/project-template-v2/package.json +46 -0
  176. package/project-template-v2/skills/_shared/scripts/dispatch-clear.mjs +31 -0
  177. package/project-template-v2/skills/_shared/scripts/dispatch-set.mjs +86 -0
  178. package/project-template-v2/skills/_shared/scripts/dod-check.mjs +153 -0
  179. package/project-template-v2/skills/_shared/scripts/handoff-append.mjs +70 -0
  180. package/project-template-v2/skills/_shared/scripts/lib/validator-artifacts.mjs +91 -0
  181. package/project-template-v2/skills/_shared/scripts/pipeline/dod-config.mjs +131 -0
  182. package/project-template-v2/skills/_shared/scripts/pipeline/index.mjs +80 -0
  183. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-1-core.mjs +90 -0
  184. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-2-wrap.mjs +93 -0
  185. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-3-visual.mjs +23 -0
  186. package/project-template-v2/skills/_shared/scripts/render-project-state.mjs +230 -0
  187. package/project-template-v2/skills/_shared/scripts/state-advance-stage.mjs +58 -0
  188. package/project-template-v2/skills/_shared/scripts/state-get.mjs +339 -0
  189. package/project-template-v2/skills/_shared/scripts/state-handoff.mjs +39 -0
  190. package/project-template-v2/skills/_shared/scripts/state-set.mjs +94 -0
  191. package/project-template-v2/skills/_shared/scripts/state-store.mjs +783 -0
  192. package/project-template-v2/skills/_shared/scripts/todo-add.mjs +88 -0
  193. package/project-template-v2/skills/_shared/scripts/todo-get.mjs +130 -0
  194. package/project-template-v2/skills/_shared/scripts/todo-remove.mjs +33 -0
  195. package/project-template-v2/skills/_shared/scripts/todo-set.mjs +47 -0
  196. package/project-template-v2/skills/_shared/scripts/verify-asset-code-sync.mjs +268 -0
  197. package/project-template-v2/skills/_shared/scripts/verify-env.mjs +285 -0
  198. package/project-template-v2/skills/_shared/scripts/verify-placeholders.mjs +161 -0
  199. package/project-template-v2/skills/playable-autoplay/SKILL.md +176 -0
  200. package/project-template-v2/skills/playable-autoplay/agents/openai.yaml +4 -0
  201. package/project-template-v2/skills/playable-debug/SKILL.md +116 -0
  202. package/project-template-v2/skills/playable-debug/agents/openai.yaml +4 -0
  203. package/project-template-v2/skills/playable-debug/references/debug-config.md +40 -0
  204. package/project-template-v2/skills/playable-record/SKILL.md +140 -0
  205. package/project-template-v2/skills/playable-record/scripts/lib/dev-server.mjs +104 -0
  206. package/project-template-v2/skills/playable-record/scripts/lib/record-audio-bridge.js +141 -0
  207. package/project-template-v2/skills/playable-record/scripts/record-playable.mjs +425 -0
  208. package/project-template-v2/skills/playable-record/scripts/verify-contract.mjs +261 -0
  209. package/project-template-v2/skills/playable-record/scripts/verify-firstwin.mjs +398 -0
  210. package/project-template-v2/skills/playable-record/scripts/verify-fusion.mjs +322 -0
  211. package/project-template-v2/skills/playable-record/scripts/verify-lifecycle.mjs +105 -0
  212. package/project-template-v2/skills/playable-record/scripts/verify-vlm-video.mjs +230 -0
  213. package/project-template-v2/skills/playable-validate/SKILL.md +61 -0
  214. package/project-template-v2/skills/playable-validate/validation-rules.md +14 -0
  215. package/project-template-v2/skills/playable-verify-ui/SKILL.md +63 -0
  216. package/project-template-v2/skills/playable-verify-ui/scripts/lib/__init__.py +1 -0
  217. package/project-template-v2/skills/playable-verify-ui/scripts/lib/config_expr.js +218 -0
  218. package/project-template-v2/skills/playable-verify-ui/scripts/lib/node_utils.js +51 -0
  219. package/project-template-v2/skills/playable-verify-ui/scripts/lib/profile_loader.py +59 -0
  220. package/project-template-v2/skills/playable-verify-ui/scripts/lib/report.py +106 -0
  221. package/project-template-v2/skills/playable-verify-ui/scripts/lib/runtime_contract.js +116 -0
  222. package/project-template-v2/skills/playable-verify-ui/scripts/verify_fetch_antipatterns.py +59 -0
  223. package/project-template-v2/skills/playable-verify-ui/scripts/verify_hardcoded_layout.js +64 -0
  224. package/project-template-v2/skills/playable-verify-ui/scripts/verify_runtime_contract.js +58 -0
  225. package/project-template-v2/skills/playable-verify-ui/scripts/verify_text_overlap.py +333 -0
  226. package/project-template-v2/ta-workspace/scripts/.gitkeep +0 -0
  227. package/project-template-v2/tsconfig.json +20 -0
  228. package/project-template-v2/vite.config.ts +27 -0
@@ -0,0 +1,72 @@
1
+ import { verifyLocalProjectSkills } from './validation.js';
2
+ export { verifyLocalProjectSkills } from './validation.js';
3
+ import { downloadLocalProjectSkillArtifacts } from './remote-cache.js';
4
+ import { resolve } from 'node:path';
5
+ import { inspectProjectSkills, } from '@playcraft/common/project-skills';
6
+ import { syncLocalProjectSkills, enableLocalProjectSkill, installFrozenProjectSkills, explainLocalProjectSkills } from './local-store.js';
7
+ import { runLocalProjectSkills, localProjectSkillUsage } from './local-run.js';
8
+ import { projectSkillsMessage as t } from './messages.js';
9
+ import { reconcileLocalProjectSkills } from './reconcile.js';
10
+ export async function inspectLocalProjectSkills(directory) {
11
+ const inventory = await inspectProjectSkills(directory);
12
+ // Report metadata only; never print complete skill bytes or project test data.
13
+ return { declaration: inventory.declaration, discovered: inventory.discovered, issues: inventory.issues,
14
+ sources: inventory.sources.map((source) => ({ entryKey: source.entryKey, root: source.root,
15
+ sourceTreeHash: source.sourceTreeHash, files: source.files.map((file) => file.path) })) };
16
+ }
17
+ export function registerProjectSkillsCommands(skills) {
18
+ const project = skills.command('project').description(t('description'));
19
+ const output = async (action) => {
20
+ try {
21
+ process.stdout.write(`${JSON.stringify(await action(), null, 2)}\n`);
22
+ }
23
+ catch (error) {
24
+ process.stderr.write(`${t('failed')}: ${error instanceof Error ? error.message : String(error)}\n`);
25
+ process.exitCode = 1;
26
+ }
27
+ };
28
+ project.command('run <prompt>').description(t('run')).requiredOption('--model <provider/model>', t('model'))
29
+ .option('--dir <path>', t('directory'), '.').action((prompt, options) => output(async () => {
30
+ const result = await runLocalProjectSkills(resolve(options.dir), { prompt, model: options.model });
31
+ if (result.exitCode !== 0)
32
+ process.exitCode = result.exitCode;
33
+ return result;
34
+ }));
35
+ project.command('usage').description(t('usage')).option('--dir <path>', t('directory'), '.')
36
+ .action((options) => output(() => localProjectSkillUsage(resolve(options.dir))));
37
+ project.command('sync').description(t('sync')).option('--dir <path>', t('directory'), '.')
38
+ .action((options) => output(() => syncLocalProjectSkills(resolve(options.dir))));
39
+ project.command('reconcile').description(t('reconcile')).option('--dir <path>', t('directory'), '.')
40
+ .action((options) => output(() => reconcileLocalProjectSkills(resolve(options.dir), { write: true })));
41
+ project.command('install').description(t('install')).requiredOption('--frozen', t('frozen')).option('--dir <path>', t('directory'), '.')
42
+ .option('--online', t('online')).action((options) => output(async () => {
43
+ const directory = resolve(options.dir);
44
+ if (options.online)
45
+ await downloadLocalProjectSkillArtifacts(directory);
46
+ return installFrozenProjectSkills(directory);
47
+ }));
48
+ for (const enabled of [true, false])
49
+ project.command(enabled ? 'enable <entryKey>' : 'disable <entryKey>')
50
+ .description(t(enabled ? 'enable' : 'disable')).option('--dir <path>', t('directory'), '.')
51
+ .action((entryKey, options) => output(() => enableLocalProjectSkill(resolve(options.dir), entryKey, enabled)));
52
+ project.command('explain').description(t('explain')).option('--dir <path>', t('directory'), '.')
53
+ .action((options) => output(() => explainLocalProjectSkills(resolve(options.dir))));
54
+ project.command('inspect').description(t('inspect')).option('--dir <path>', t('directory'), '.')
55
+ .action(async (options) => {
56
+ const result = await inspectLocalProjectSkills(resolve(options.dir));
57
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
58
+ if (result.issues.length)
59
+ process.exitCode = 1;
60
+ });
61
+ project.command('verify').description(t('verify')).option('--dir <path>', t('directory'), '.')
62
+ .option('--json', t('json')).action(async (options) => {
63
+ try {
64
+ const result = await verifyLocalProjectSkills(resolve(options.dir));
65
+ process.stdout.write(`${options.json ? JSON.stringify(result) : t('verified')}\n`);
66
+ }
67
+ catch (error) {
68
+ process.stderr.write(`${t('failed')}: ${error instanceof Error ? error.message : String(error)}\n`);
69
+ process.exitCode = 1;
70
+ }
71
+ });
72
+ }
@@ -0,0 +1,28 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readProjectSkillsLockFile } from '@playcraft/common/project-skills';
3
+ import { syncLocalProjectSkills, readLocalProjectSkillSnapshot, installFrozenProjectSkills } from './local-store.js';
4
+ import { verifyLocalProjectSkills } from './validation.js';
5
+ import { projectSkillsMessage as t } from './messages.js';
6
+ /** Existing formats keep their legacy consumer; format 3 never silently downgrades. */
7
+ export async function prepareLocalSkillLifecycle(directory, action, cache) {
8
+ const raw = await readProjectSkillsLockFile(directory);
9
+ const managed = raw !== null && JSON.parse(raw).lockVersion === 3;
10
+ if (!managed && process.env.PROJECT_SKILLS_ADOPTION_ENABLED !== 'true')
11
+ return { managed: false };
12
+ if (raw !== null && !managed)
13
+ return { managed: false, legacy: true };
14
+ if (action === 'create' || action === 'push' || !managed)
15
+ await syncLocalProjectSkills(directory, cache);
16
+ await verifyLocalProjectSkills(directory);
17
+ const snapshot = await readLocalProjectSkillSnapshot(directory);
18
+ if (action === 'push') {
19
+ const paths = ['.playcraft/skills.json', '.playcraft/skills.lock.json', ...snapshot.declaration.entries.flatMap(entry => entry.source.kind === 'repository' ? [entry.source.root, ...(entry.projectFiles ?? [])] : [])];
20
+ const status = spawnSync('git', ['status', '--porcelain', '--', ...paths], { cwd: directory, encoding: 'utf8' });
21
+ if (status.status !== 0 || status.stdout.trim())
22
+ throw new Error(t('commitRequired'));
23
+ }
24
+ // Source-only checkouts work offline. Registry packages remain exact and require an authorized cache fill.
25
+ if (!snapshot.lock.entries.some(entry => entry.revision.kind === 'release'))
26
+ await installFrozenProjectSkills(directory, cache);
27
+ return { managed: true, declarationHash: snapshot.lock.declarationHash };
28
+ }
@@ -0,0 +1,112 @@
1
+ import { spawn, execFileSync } from 'node:child_process';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { createRequire } from 'node:module';
4
+ import { readFile, writeFile, mkdir, readdir, realpath } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+ import { createInterface } from 'node:readline';
8
+ import { effectiveProjectSkills, readManagedSkillEvidence } from '@playcraft/common/project-skills';
9
+ import { installFrozenProjectSkills, readLocalProjectSkillSnapshot, projectSkillsCache } from './local-store.js';
10
+ import { verifyLocalProjectSkills } from './validation.js';
11
+ import { projectSkillsMessage as t } from './messages.js';
12
+ const require = createRequire(import.meta.url);
13
+ export async function localProjectSkillUsage(directory, cache = projectSkillsCache()) {
14
+ const root = join(cache, 'usage', createHash('sha256').update(await realpath(directory)).digest('hex'));
15
+ let files;
16
+ try {
17
+ files = await readdir(root);
18
+ }
19
+ catch (error) {
20
+ if (error.code === 'ENOENT')
21
+ return [];
22
+ throw error;
23
+ }
24
+ return Promise.all(files.filter(file => /^[a-f0-9-]+\.json$/.test(file)).sort().map(async (file) => JSON.parse(await readFile(join(root, file), 'utf8'))));
25
+ }
26
+ /** Local evidence is explicitly client-reported; it never acts as a server evaluation or access grant. */
27
+ export async function runLocalProjectSkills(directory, options, cache = projectSkillsCache()) {
28
+ if (execFileSync('opencode', ['--version'], { encoding: 'utf8', timeout: 10000 }).trim() !== '1.18.31')
29
+ throw new Error(t('runner'));
30
+ const cwd = await realpath(directory);
31
+ await verifyLocalProjectSkills(cwd);
32
+ const snapshot = await readLocalProjectSkillSnapshot(cwd);
33
+ const installed = await installFrozenProjectSkills(cwd, cache);
34
+ const runId = randomUUID(), runtimeRoot = join(installed.generation, '.runtime');
35
+ const home = join(runtimeRoot, 'home');
36
+ await mkdir(home, { recursive: true, mode: 0o700 });
37
+ const entries = await Promise.all(effectiveProjectSkills(snapshot.declaration, snapshot.lock, 'sandbox-agent').map(async (entry) => {
38
+ const bodyFile = join(installed.generation, entry.entryKey, 'SKILL.md');
39
+ return { name: entry.entryKey, skillRef: entry.revision.skillRef, revisionId: entry.revision.kind === 'source' ? entry.revision.sourceRevision : entry.revision.version,
40
+ artifactHash: entry.revision.artifactHash, bodyFile, bodyHash: createHash('sha256').update(await readFile(bodyFile)).digest('hex'), description: entry.revision.skillRef };
41
+ }));
42
+ await writeFile(join(runtimeRoot, 'manifest.json'), JSON.stringify({ schemaVersion: 1, runtimeId: runId, lockId: snapshot.lock.declarationHash, entries }), { mode: 0o400 });
43
+ await writeFile(join(runtimeRoot, 'opencode.json'), JSON.stringify({ model: options.model,
44
+ plugin: [pathToFileURL(require.resolve('@playcraft/common/project-skills/runtime-plugin')).href] }), { mode: 0o400 });
45
+ const usageRoot = join(cache, 'usage', createHash('sha256').update(cwd).digest('hex'));
46
+ await mkdir(usageRoot, { recursive: true, mode: 0o700 });
47
+ const evidence = new Map();
48
+ const report = { schemaVersion: 1, runId, declarationHash: snapshot.lock.declarationHash, provenance: 'local-client-reported',
49
+ runnerVersion: '1.18.31', model: options.model, startedAt: new Date().toISOString(), status: 'running', collectionStatus: 'collecting', loaded: [] };
50
+ const reportPath = join(usageRoot, `${runId}.json`);
51
+ await writeFile(reportPath, JSON.stringify(report), { mode: 0o600 });
52
+ const environment = { ...process.env, HOME: home, XDG_CONFIG_HOME: join(home, '.config'), XDG_DATA_HOME: join(home, '.local/share'), XDG_STATE_HOME: join(home, '.local/state'), XDG_CACHE_HOME: join(home, '.cache'),
53
+ OPENCODE_CONFIG: join(runtimeRoot, 'opencode.json'), OPENCODE_CONFIG_DIR: join(runtimeRoot, 'config'),
54
+ OPENCODE_DISABLE_PROJECT_CONFIG: 'true', OPENCODE_DISABLE_CLAUDE_CODE: 'true', OPENCODE_DISABLE_DEFAULT_PLUGINS: 'true', OPENCODE_DISABLE_AUTOUPDATE: 'true',
55
+ PLAYCRAFT_SKILL_RUNTIME_FILE: join(runtimeRoot, 'manifest.json') };
56
+ delete environment.PLAYCRAFT_SKILL_RUNTIME_ROOT;
57
+ delete environment.OPENCODE_CONFIG_CONTENT;
58
+ let child;
59
+ let cancelled = false, killTimer;
60
+ const cancel = () => {
61
+ cancelled = true;
62
+ child?.kill('SIGTERM');
63
+ killTimer ??= setTimeout(() => child?.kill('SIGKILL'), 5000);
64
+ killTimer.unref();
65
+ };
66
+ process.once('SIGINT', cancel);
67
+ process.once('SIGTERM', cancel);
68
+ try {
69
+ child = spawn('opencode', ['run', '--format', 'json', '--model', options.model, '--', options.prompt], { cwd, env: environment, stdio: ['ignore', 'pipe', 'inherit'] });
70
+ // Resolve spawn failures so an early error cannot become an unhandled rejection while reading stdout.
71
+ const finished = new Promise(resolve => {
72
+ child.once('error', error => resolve({ code: 1, error }));
73
+ child.once('close', code => resolve({ code: code ?? 1 }));
74
+ });
75
+ const lines = createInterface({ input: child.stdout });
76
+ let partial = false;
77
+ for await (const line of lines) {
78
+ try {
79
+ const event = JSON.parse(line);
80
+ const parsed = readManagedSkillEvidence(event);
81
+ if (parsed?.runtimeId === runId && parsed.lockId === report.declarationHash && entries.some(entry => entry.revisionId === parsed.skillVersionId && entry.artifactHash === parsed.artifactHash && entry.bodyHash === parsed.bodyHash)) {
82
+ evidence.set(parsed.dedupeKey, parsed);
83
+ report.loaded = [...evidence.values()];
84
+ await writeFile(reportPath, JSON.stringify(report), { mode: 0o600 });
85
+ }
86
+ if (event.type === 'text' && typeof event.part?.text === 'string')
87
+ process.stderr.write(event.part.text);
88
+ }
89
+ catch {
90
+ partial = true;
91
+ }
92
+ }
93
+ const result = await finished;
94
+ if (result.error)
95
+ throw result.error;
96
+ const code = cancelled ? 130 : result.code;
97
+ report.status = cancelled ? 'cancelled' : code === 0 ? 'completed' : 'failed';
98
+ report.collectionStatus = code === 0 && !partial ? 'complete' : 'partial';
99
+ return { ...report, reportPath, exitCode: code };
100
+ }
101
+ finally {
102
+ if (killTimer)
103
+ clearTimeout(killTimer);
104
+ process.removeListener('SIGINT', cancel);
105
+ process.removeListener('SIGTERM', cancel);
106
+ if (report.status === 'running') {
107
+ report.status = 'failed';
108
+ report.collectionStatus = 'partial';
109
+ }
110
+ await writeFile(reportPath, JSON.stringify(report), { mode: 0o600 });
111
+ }
112
+ }
@@ -0,0 +1,149 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, writeFile, lstat } from 'node:fs/promises';
3
+ import { join, resolve } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { projectSkillsMessage as t } from './messages.js';
6
+ import { buildLocalProjectSkillsSnapshot, canonicalProjectSkillsJson, extractBundleArchive, createCanonicalBundleArchive, readProjectSkillSource, sourceTreeDigest, readProjectSkillsLockFile, inspectProjectSkills, setProjectSkillEnabled, validateProjectSkillsSnapshot, effectiveProjectSkills, PROJECT_SKILLS_DECLARATION_PATH, PROJECT_SKILLS_LOCK_PATH, } from '@playcraft/common/project-skills';
7
+ export const projectSkillsCache = () => process.env.PLAYCRAFT_SKILLS_CACHE_DIR
8
+ ? resolve(process.env.PLAYCRAFT_SKILLS_CACHE_DIR) : join(homedir(), '.playcraft', 'skill-artifacts');
9
+ export async function readLocalProjectSkillSnapshot(directory) {
10
+ const inventory = await inspectProjectSkills(directory);
11
+ if (inventory.issues.length)
12
+ throw new Error(JSON.stringify(inventory.issues));
13
+ return validateProjectSkillsSnapshot(inventory.declaration, await readProjectSkillsLockFile(directory));
14
+ }
15
+ /** Serializes local writers; an interrupted pair is diagnosed by the declaration digest. */
16
+ async function change(directory, action) {
17
+ const management = join(directory, '.playcraft');
18
+ await mkdir(management, { recursive: true });
19
+ if ((await lstat(management)).isSymbolicLink())
20
+ throw new Error('Linked project management directory');
21
+ const lease = join(management, 'skills-write.lock');
22
+ await writeFile(lease, `${process.pid}\n`, { flag: 'wx', mode: 0o600 });
23
+ try {
24
+ return await action();
25
+ }
26
+ finally {
27
+ await rm(lease);
28
+ }
29
+ }
30
+ async function writeSnapshot(directory, snapshot) {
31
+ validateProjectSkillsSnapshot(snapshot.declaration, snapshot.lock);
32
+ for (const [file, value] of [[PROJECT_SKILLS_DECLARATION_PATH, snapshot.declaration], [PROJECT_SKILLS_LOCK_PATH, snapshot.lock]]) {
33
+ const target = join(directory, file), temporary = `${target}.${randomUUID()}.tmp`;
34
+ await writeFile(temporary, `${canonicalProjectSkillsJson(value)}\n`, { flag: 'wx', mode: 0o644 });
35
+ await rename(temporary, target);
36
+ }
37
+ }
38
+ export async function syncLocalProjectSkills(directory, cache = projectSkillsCache()) {
39
+ return change(directory, async () => {
40
+ let previous;
41
+ try {
42
+ previous = await readProjectSkillsLockFile(directory) ?? undefined;
43
+ }
44
+ catch (error) {
45
+ if (error.code !== 'ENOENT')
46
+ throw error;
47
+ }
48
+ const snapshot = buildLocalProjectSkillsSnapshot(await inspectProjectSkills(directory), previous);
49
+ await mkdir(cache, { recursive: true, mode: 0o700 });
50
+ for (const artifact of snapshot.artifacts) {
51
+ const target = join(cache, `${artifact.artifactHash}.tar.gz`);
52
+ const temporary = `${target}.${randomUUID()}.tmp`;
53
+ await writeFile(temporary, artifact.archive, { flag: 'wx', mode: 0o600 });
54
+ await rename(temporary, target);
55
+ }
56
+ await writeSnapshot(directory, snapshot);
57
+ return { declaration: snapshot.declaration, lock: snapshot.lock };
58
+ });
59
+ }
60
+ export async function enableLocalProjectSkill(directory, entryKey, enabled) {
61
+ return change(directory, async () => {
62
+ const previous = await readLocalProjectSkillSnapshot(directory);
63
+ const next = setProjectSkillEnabled(previous.declaration, previous.lock, entryKey, enabled);
64
+ // Disabled required dependencies are shown immediately, before changing either file.
65
+ for (const surface of ['portal-agent', 'sandbox-agent', 'build-time'])
66
+ effectiveProjectSkills(next.declaration, next.lock, surface);
67
+ await writeSnapshot(directory, next);
68
+ return next;
69
+ });
70
+ }
71
+ /** Restores exact cached bytes to a new generation. Never writes source directories. */
72
+ export async function installFrozenProjectSkills(directory, cache = projectSkillsCache()) {
73
+ const snapshot = await readLocalProjectSkillSnapshot(directory);
74
+ const generation = join(cache, 'runs', randomUUID());
75
+ const effective = effectiveProjectSkills(snapshot.declaration, snapshot.lock, 'sandbox-agent');
76
+ try {
77
+ await mkdir(generation, { recursive: true, mode: 0o700 });
78
+ for (const entry of effective) {
79
+ const cached = join(cache, `${entry.revision.artifactHash}.tar.gz`);
80
+ let archive;
81
+ try {
82
+ archive = await readFile(cached);
83
+ }
84
+ catch (error) {
85
+ if (error.code !== 'ENOENT' || entry.revision.kind !== 'source')
86
+ throw error;
87
+ const declaration = snapshot.declaration.entries.find(item => item.entryKey === entry.entryKey);
88
+ const source = await readProjectSkillSource(directory, declaration);
89
+ if (source.sourceTreeHash !== entry.sourceTreeHash)
90
+ throw new Error(`Source digest mismatch: ${entry.entryKey}`);
91
+ archive = createCanonicalBundleArchive(source.files.map(file => ({ path: file.path, data: Buffer.from(file.content) })));
92
+ if (createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
93
+ throw new Error(`Artifact digest mismatch: ${entry.entryKey}`);
94
+ await writeFile(cached, archive, { flag: 'wx', mode: 0o600 }).catch(error => { if (error.code !== 'EEXIST')
95
+ throw error; });
96
+ }
97
+ if (createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
98
+ throw new Error(`Artifact digest mismatch: ${entry.entryKey}`);
99
+ const extracted = extractBundleArchive(archive);
100
+ if (extracted.issues.length)
101
+ throw new Error(JSON.stringify(extracted.issues));
102
+ if (entry.revision.kind === 'source' && sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== entry.sourceTreeHash)
103
+ throw new Error(`Source digest mismatch: ${entry.entryKey}`);
104
+ for (const file of extracted.files) {
105
+ const target = join(generation, entry.entryKey, file.path);
106
+ await mkdir(join(target, '..'), { recursive: true });
107
+ await writeFile(target, file.data, { flag: 'wx', mode: 0o444 });
108
+ }
109
+ }
110
+ return { generation, entries: effective.length, declarationHash: snapshot.lock.declarationHash };
111
+ }
112
+ catch (error) {
113
+ await rm(generation, { recursive: true, force: true });
114
+ throw error;
115
+ }
116
+ }
117
+ export async function explainLocalProjectSkills(directory) {
118
+ const { declaration, lock } = await readLocalProjectSkillSnapshot(directory);
119
+ return declaration.entries.map((entry) => ({ ...entry, locked: lock.entries.find((locked) => locked.entryKey === entry.entryKey), reason: entry.enabled ? 'enabled_in_project' : 'disabled_in_project' }));
120
+ }
121
+ export async function adoptServerProjectSkillSnapshot(directory, expected, response, checkpoint) {
122
+ const next = validateProjectSkillsSnapshot(response.declaration, response.lock);
123
+ const withoutIdentity = (snapshot) => ({
124
+ declaration: { ...snapshot.declaration, entries: snapshot.declaration.entries.map(entry => ({ ...entry,
125
+ source: entry.source.kind === 'repository' ? { kind: entry.source.kind, root: entry.source.root } : entry.source })) },
126
+ entries: snapshot.lock.entries.map(entry => ({ entryKey: entry.entryKey, sourceTreeHash: entry.sourceTreeHash,
127
+ dependencies: entry.dependencies,
128
+ revision: entry.revision.kind === 'source' ? { ...entry.revision, skillRef: undefined } : entry.revision })),
129
+ });
130
+ if (canonicalProjectSkillsJson(withoutIdentity(expected)) !== canonicalProjectSkillsJson(withoutIdentity(next)))
131
+ throw new Error(t('reconcileChanged'));
132
+ return change(directory, async () => {
133
+ const current = await readLocalProjectSkillSnapshot(directory);
134
+ if (canonicalProjectSkillsJson(current) !== canonicalProjectSkillsJson(expected))
135
+ throw new Error(t('reconcileChanged'));
136
+ for (const entry of current.declaration.entries) {
137
+ if (entry.source.kind !== 'repository')
138
+ continue;
139
+ const source = await readProjectSkillSource(directory, entry);
140
+ if (source.sourceTreeHash !== current.lock.entries.find(item => item.entryKey === entry.entryKey)?.sourceTreeHash)
141
+ throw new Error(t('reconcileChanged'));
142
+ }
143
+ checkpoint();
144
+ const changed = canonicalProjectSkillsJson(current) !== canonicalProjectSkillsJson(next);
145
+ if (changed)
146
+ await writeSnapshot(directory, next);
147
+ return { changed };
148
+ });
149
+ }
@@ -0,0 +1,72 @@
1
+ const messages = {
2
+ en: {
3
+ reconcile: 'Register the pushed commit and adopt verified platform skill identities',
4
+ reconcileChanged: 'The branch, source bytes, or settings changed. Sync, commit and push the current project before reconciling.',
5
+ reconcilePending: 'Indexing continues on the server. Retry reconcile later. Operation:',
6
+ reconcileFailed: 'Skill indexing failed. Inspect the project adoption operation and retry it.',
7
+ reconcileNext: 'Skills are registered. Run skills project reconcile to adopt the verified identities locally.',
8
+ reconcileCommit: 'Verified skill identities saved. Commit the declaration and lock, then run remix push.',
9
+ reconcileUnchanged: 'Local skills match the registered platform revisions.',
10
+ pushIndexFailed: 'Git push succeeded, but skill indexing did not finish. Run skills project reconcile to retry.',
11
+ unregistered: 'New skill sources are not in the project lock. Run skills project sync before starting a task.',
12
+ online: 'Download missing exact artifacts using this Remix’s platform authorization',
13
+ downloadMismatch: 'The server has no authorized artifact matching this local lock. Sync the project first.',
14
+ commitRequired: 'Skill source or lock changes are ready. Commit them, then run remix push again.',
15
+ run: 'Run a task with the frozen project skills in OpenCode 1.18.31',
16
+ usage: 'Show locally reported skill deliveries (no chat upload)',
17
+ model: 'OpenCode model identifier; credentials come from the local environment',
18
+ runner: 'Managed skills require OpenCode 1.18.31.',
19
+ sync: 'Save source revisions and cache their exact artifacts offline',
20
+ install: 'Restore exactly the saved lock from the local cache',
21
+ frozen: 'Require exact locked artifacts',
22
+ enable: 'Enable a project skill for the next run',
23
+ disable: 'Disable a project skill for the next run',
24
+ explain: 'Explain configured revisions, sources and enablement',
25
+ description: 'Inspect and validate this project’s managed skills',
26
+ inspect: 'Inspect source directories and declarations without changing files',
27
+ verify: 'Validate the saved lock and local source bytes',
28
+ directory: 'Project directory',
29
+ json: 'Print structured output',
30
+ verified: 'Local skill sources and configuration match the lock. Server authorization and runner verification are separate checks.',
31
+ failed: 'Skill verification failed',
32
+ missing: 'The project does not have a managed declaration and lock yet.',
33
+ changed: 'Source files have changed since the saved revision',
34
+ },
35
+ zh: {
36
+ reconcile: '登记已推送的提交,并采用平台核验后的技能身份',
37
+ reconcileChanged: '分支、源文件或配置发生变化,请同步、提交并推送当前项目后再对账。',
38
+ reconcilePending: '服务端仍在登记,请稍后重新运行 reconcile。操作编号:',
39
+ reconcileFailed: '技能登记失败,请检查项目接管操作并重试。',
40
+ reconcileNext: '技能源已登记,请运行 skills project reconcile 在本地采用已核验的身份。',
41
+ reconcileCommit: '已保存核验后的技能身份,请提交声明和锁文件,然后运行 remix push。',
42
+ reconcileUnchanged: '本地技能与平台登记的修订一致。',
43
+ pushIndexFailed: 'Git 推送成功,但技能登记未完成。请运行 skills project reconcile 重试。',
44
+ unregistered: '发现尚未登记到项目锁的技能源,请先运行 skills project sync 再开始任务。',
45
+ online: '使用此 Remix 的平台权限下载缺失的精确制品',
46
+ downloadMismatch: '服务端没有与此本地锁匹配的授权制品,请先同步项目。',
47
+ commitRequired: '技能源或锁文件已更新,请提交这些修改后再次运行 remix push。',
48
+ run: '使用固定项目技能和 OpenCode 1.18.31 运行任务',
49
+ usage: '查看本地自报的技能加载记录(不上传聊天)',
50
+ model: 'OpenCode 模型标识;凭据使用本地环境',
51
+ runner: '受控技能需要 OpenCode 1.18.31。',
52
+ sync: '保存源修订并离线缓存精确制品',
53
+ install: '按锁从本地缓存还原精确制品',
54
+ frozen: '要求精确锁定制品',
55
+ enable: '在下次运行中启用项目技能',
56
+ disable: '在下次运行中停用项目技能',
57
+ explain: '解释配置的修订、来源与启停状态',
58
+ description: '检查和验证当前项目的受控技能',
59
+ inspect: '检查技能源目录和声明,不修改文件',
60
+ verify: '验证已保存的锁和本地源文件',
61
+ directory: '项目目录',
62
+ json: '输出结构化结果',
63
+ verified: '本地技能源和配置与锁一致。服务端授权和运行器验证仍需单独检查。',
64
+ failed: '技能验证失败',
65
+ missing: '此项目还没有受控技能声明和锁。',
66
+ changed: '源文件与已保存的修订不一致',
67
+ },
68
+ };
69
+ export function projectSkillsMessage(key) {
70
+ const locale = process.env.PLAYCRAFT_LANGUAGE || process.env.LANG || 'en';
71
+ return messages[locale.startsWith('zh') ? 'zh' : 'en'][key];
72
+ }
@@ -0,0 +1,62 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { PlayCraftHttpClient } from '../utils/playcraft-http-client.js';
3
+ import { readRemixLocalMeta } from '../remix/local-git.js';
4
+ import { readLocalProjectSkillSnapshot, adoptServerProjectSkillSnapshot } from './local-store.js';
5
+ import { projectSkillsMessage as t } from './messages.js';
6
+ /** Register the saved commit, then adopt only identity changes for the same local bytes and settings. */
7
+ export async function reconcileLocalProjectSkills(directory, options = {}) {
8
+ const metadata = readRemixLocalMeta(directory);
9
+ const git = (args) => {
10
+ const result = spawnSync('git', args, { cwd: directory, encoding: 'utf8' });
11
+ if (result.status !== 0)
12
+ throw new Error(t('reconcileChanged'));
13
+ return result.stdout.trim();
14
+ };
15
+ if (git(['branch', '--show-current']) !== metadata.branch)
16
+ throw new Error(t('reconcileChanged'));
17
+ const commit = git(['rev-parse', 'HEAD']);
18
+ const snapshot = await readLocalProjectSkillSnapshot(directory);
19
+ const paths = [
20
+ '.playcraft/skills.json',
21
+ '.playcraft/skills.lock.json',
22
+ ...snapshot.declaration.entries.flatMap((entry) => entry.source.kind === 'repository' ? [entry.source.root, ...(entry.projectFiles ?? [])] : []),
23
+ ];
24
+ if (git(['status', '--porcelain', '--', ...paths]))
25
+ throw new Error(t('commitRequired'));
26
+ const client = new PlayCraftHttpClient(options);
27
+ const base = `/projects/${encodeURIComponent(metadata.projectId)}/skills`;
28
+ const query = new URLSearchParams({ branch: metadata.branch });
29
+ const request = () => ({ signal: AbortSignal.timeout(30_000), redirect: 'error' });
30
+ const view = await client.get(`${base}/config?${query}`, undefined, request());
31
+ let operation = await client.post(`${base}/index`, { branch: metadata.branch, commit, expectedRevisionId: view.configuration?.id ?? null }, request());
32
+ const deadline = Date.now() + 15 * 60_000;
33
+ while (!['completed', 'failed', 'blocked', 'cancelled'].includes(operation.phase)) {
34
+ if (Date.now() > deadline)
35
+ throw new Error(`${t('reconcilePending')} ${operation.id}`);
36
+ await new Promise((resolve) => setTimeout(resolve, 1000));
37
+ operation = await client.get(`${base}/adoptions/${operation.id}?${query}`, undefined, request());
38
+ }
39
+ if (operation.phase !== 'completed')
40
+ throw new Error(operation.issues?.[0]?.detail || t('reconcileFailed'));
41
+ const updated = await client.get(`${base}/config?${query}`, undefined, request());
42
+ if (!updated.configuration || updated.configuration.id !== operation.configRevisionId)
43
+ throw new Error(t('reconcileChanged'));
44
+ if (!options.write)
45
+ return {
46
+ operationId: operation.id,
47
+ configRevisionId: operation.configRevisionId,
48
+ commit,
49
+ message: t('reconcileNext'),
50
+ };
51
+ const result = await adoptServerProjectSkillSnapshot(directory, snapshot, updated.configuration, () => {
52
+ if (git(['rev-parse', 'HEAD']) !== commit || git(['status', '--porcelain', '--', ...paths]))
53
+ throw new Error(t('reconcileChanged'));
54
+ });
55
+ return {
56
+ ...result,
57
+ operationId: operation.id,
58
+ configRevisionId: operation.configRevisionId,
59
+ commit,
60
+ message: result.changed ? t('reconcileCommit') : t('reconcileUnchanged'),
61
+ };
62
+ }
@@ -0,0 +1,51 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, writeFile, rename, readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { PlayCraftHttpClient } from '../utils/playcraft-http-client.js';
5
+ import { readRemixLocalMeta } from '../remix/local-git.js';
6
+ import { readLocalProjectSkillSnapshot, projectSkillsCache } from './local-store.js';
7
+ import { projectSkillsMessage as t } from './messages.js';
8
+ /** Only exact project-authorized revisions; identifiers inside Git never grant download access. */
9
+ export async function downloadLocalProjectSkillArtifacts(directory, cache = projectSkillsCache()) {
10
+ const meta = readRemixLocalMeta(directory), client = new PlayCraftHttpClient();
11
+ const snapshot = await readLocalProjectSkillSnapshot(directory);
12
+ const query = new URLSearchParams({ branch: meta.branch });
13
+ const view = await client.get(`/projects/${encodeURIComponent(meta.projectId)}/skills/config?${query}`);
14
+ await mkdir(cache, { recursive: true, mode: 0o700 });
15
+ let downloaded = 0;
16
+ for (const locked of snapshot.lock.entries) {
17
+ const file = join(cache, `${locked.revision.artifactHash}.tar.gz`);
18
+ try {
19
+ if (createHash('sha256').update(await readFile(file)).digest('hex') === locked.revision.artifactHash)
20
+ continue;
21
+ }
22
+ catch (error) {
23
+ if (error.code !== 'ENOENT')
24
+ throw error;
25
+ }
26
+ const authorized = view.entries?.find(entry => entry.entryKey === locked.entryKey && entry.artifactHash === locked.revision.artifactHash);
27
+ if (!authorized)
28
+ throw new Error(t('downloadMismatch'));
29
+ const response = await fetch(client.url(`/projects/${encodeURIComponent(meta.projectId)}/skills/versions/${authorized.versionId}/archive?${query}`), {
30
+ headers: { Authorization: `Bearer ${client.token}`, 'X-Request-Id': client.requestId }, redirect: 'error', signal: AbortSignal.timeout(60000),
31
+ });
32
+ if (!response.ok || !response.body)
33
+ throw new Error(`${t('downloadMismatch')} (${response.status})`);
34
+ const parts = [];
35
+ let size = 0;
36
+ for await (const chunk of response.body) {
37
+ size += chunk.byteLength;
38
+ if (size > 80 * 1024 * 1024)
39
+ throw new Error(t('downloadMismatch'));
40
+ parts.push(Buffer.from(chunk));
41
+ }
42
+ const archive = Buffer.concat(parts);
43
+ if (createHash('sha256').update(archive).digest('hex') !== locked.revision.artifactHash)
44
+ throw new Error(t('downloadMismatch'));
45
+ const temporary = `${file}.${randomUUID()}.tmp`;
46
+ await writeFile(temporary, archive, { mode: 0o600, flag: 'wx' });
47
+ await rename(temporary, file);
48
+ downloaded++;
49
+ }
50
+ return { downloaded };
51
+ }
@@ -0,0 +1,19 @@
1
+ import { readProjectSkillSource, inspectProjectSkills } from '@playcraft/common/project-skills';
2
+ import { readLocalProjectSkillSnapshot } from './local-store.js';
3
+ import { projectSkillsMessage as t } from './messages.js';
4
+ export async function verifyLocalProjectSkills(directory) {
5
+ const snapshot = await readLocalProjectSkillSnapshot(directory);
6
+ const inventory = await inspectProjectSkills(directory);
7
+ if (inventory.sources.length !== snapshot.declaration.entries.filter(entry => entry.source.kind === 'repository').length) {
8
+ throw new Error(t('unregistered'));
9
+ }
10
+ for (const entry of snapshot.declaration.entries) {
11
+ if (entry.source.kind !== 'repository')
12
+ continue;
13
+ const actual = await readProjectSkillSource(directory, entry);
14
+ const expected = snapshot.lock.entries.find((locked) => locked.entryKey === entry.entryKey);
15
+ if (actual.sourceTreeHash !== expected?.sourceTreeHash)
16
+ throw new Error(`${t('changed')}: ${entry.entryKey}`);
17
+ }
18
+ return { valid: true, declarationHash: snapshot.lock.declarationHash, entries: snapshot.lock.entries.length, scope: 'local-source-and-config' };
19
+ }
@@ -1,3 +1,4 @@
1
+ import { prepareLocalSkillLifecycle } from '../project-skills/lifecycle.js';
1
2
  import { spawnSync } from 'node:child_process';
2
3
  import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
3
4
  import path from 'node:path';
@@ -151,6 +152,7 @@ export async function remixClone(options) {
151
152
  };
152
153
  writePlaycraftMeta(targetDir, meta);
153
154
  configureRepoCredentialHelper(targetDir);
155
+ await prepareLocalSkillLifecycle(targetDir, 'checkout');
154
156
  let projectType;
155
157
  try {
156
158
  const project = await client.get(`/projects/${encodeURIComponent(projectId)}`);
@@ -200,7 +202,8 @@ export async function remixClone(options) {
200
202
  else {
201
203
  console.log(' playcraft start --project <project-id> # PlayCanvas / 本地 Agent 预览');
202
204
  }
203
- console.log(' git pull / git push # 经 PlayCraft Git 代理(需已 login)');
205
+ console.log(' playcraft remix pull # 拉取远端(沙箱 Save 后)');
206
+ console.log(' playcraft remix push # 推回 Remix(Windows 勿直接 git push/pull)');
204
207
  }
205
208
  export { isRemoteCloneSource, resolveRemixCloneTarget };
206
209
  /**
@@ -1,3 +1,4 @@
1
+ import { prepareLocalSkillLifecycle } from '../project-skills/lifecycle.js';
1
2
  import * as path from 'node:path';
2
3
  import { applyEnginePackToProject, bakeFullTemplate, KNOWN_ADAPTERS, readAdapterFromAtomTree, readEngineMarker, resolveCliPackageRoot, } from '../scaffold/compose.js';
3
4
  import { autoLinkSkillsFromAtomTree } from '../commands/skills.js';
@@ -68,6 +69,7 @@ export async function initTemplateCommand(opts) {
68
69
  console.log(` ⚠ auto-link skipped: ${err.message}`);
69
70
  }
70
71
  }
72
+ await prepareLocalSkillLifecycle(projectDir, 'create');
71
73
  console.log('\n下一步:');
72
74
  console.log(' npm install');
73
75
  console.log(' 阅读 .claude/skills/<atom>/SKILL.md(已自动 link)并填 logs/developer-log.md § Skill Preflight');