@playcraft/cli 0.0.55 → 0.0.57

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 (170) hide show
  1. package/README.md +18 -0
  2. package/REMIX.md +115 -19
  3. package/dist/atom-tree/fusion-hints.js +8 -3
  4. package/dist/atom-tree/fusion-intent.js +62 -0
  5. package/dist/atom-tree/selection.js +3 -1
  6. package/dist/commands/create.js +299 -85
  7. package/dist/commands/image.js +79 -69
  8. package/dist/commands/levels.js +29 -0
  9. package/dist/commands/patterns.js +75 -0
  10. package/dist/commands/remix.js +93 -0
  11. package/dist/commands/skills.js +48 -17
  12. package/dist/index.js +9 -5
  13. package/dist/remix/clone-source.js +208 -0
  14. package/dist/remix/clone.js +256 -0
  15. package/dist/remix/local-git.js +56 -0
  16. package/dist/remix/pull.js +64 -0
  17. package/dist/remix/push.js +70 -0
  18. package/dist/scaffold/templates.js +1 -1
  19. package/dist/utils/agent-api-client.js +10 -3
  20. package/dist/utils/remix-image-attachments.js +4 -32
  21. package/dist/utils/sandbox-path.js +20 -0
  22. package/package.json +3 -3
  23. package/project-template/.claude/agents/creative-director.md +114 -0
  24. package/project-template/.claude/agents/developer.md +5 -2
  25. package/project-template/.claude/agents/game-designer.md +41 -20
  26. package/project-template/.claude/agents/refs/README.md +8 -0
  27. package/project-template/.claude/agents/refs/creative-director-guide.md +35 -0
  28. package/project-template/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  29. package/project-template/.claude/agents/refs/creative-discovery-guide.md +76 -0
  30. package/project-template/.claude/agents/refs/creative-north-star-examples.md +82 -0
  31. package/project-template/.claude/agents/refs/developer-dev-handoff.md +1 -1
  32. package/project-template/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  33. package/project-template/.claude/agents/refs/game-designer-gameplay-sufficiency.md +4 -4
  34. package/project-template/.claude/agents/reviewer.md +11 -0
  35. package/project-template/.claude/hooks/README.md +12 -12
  36. package/project-template/.claude/hooks/block-developer-preflight.mjs +49 -13
  37. package/project-template/.claude/hooks/block-ring1-tasklist.mjs +1 -1
  38. package/project-template/.claude/hooks/user-prompt.mjs +3 -13
  39. package/project-template/.claude/hooks/validate-workflow-stop.mjs +209 -12
  40. package/project-template/.claude/settings.json +2 -2
  41. package/project-template/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  42. package/project-template/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -1
  43. package/project-template/.claude/snapshot.mjs +0 -1
  44. package/project-template/.codebuddy/settings.json +1 -1
  45. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  46. package/project-template/CLAUDE.md +9 -9
  47. package/project-template/docs/project-state.md +1 -1
  48. package/project-template/docs/team/agent-conduct.md +18 -18
  49. package/project-template/docs/team/collaboration.md +19 -7
  50. package/project-template/docs/team/workflow-changelog.md +15 -0
  51. package/project-template/fusion-patterns/catalog.json +77 -0
  52. package/project-template/package.json +2 -0
  53. package/project-template/skills/_shared/scripts/create-playable.mjs +31 -11
  54. package/project-template/skills/_shared/scripts/dev-start.mjs +3 -3
  55. package/project-template/skills/_shared/scripts/disclosure-config.mjs +65 -39
  56. package/project-template/skills/_shared/scripts/harness-smoke.mjs +3 -1
  57. package/project-template/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  58. package/project-template/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  59. package/project-template/skills/_shared/scripts/slice-set.mjs +29 -2
  60. package/project-template/skills/_shared/scripts/state-get.mjs +124 -7
  61. package/project-template/skills/_shared/scripts/state-store.mjs +25 -2
  62. package/project-template/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  63. package/project-template/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  64. package/project-template/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  65. package/project-template/skills/playable-record/scripts/record-playable.mjs +2 -1
  66. package/project-template/skills/playable-record/scripts/verify-contract.mjs +4 -4
  67. package/project-template/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  68. package/project-template/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  69. package/project-template/skills/playable-record/scripts/verify-slice.mjs +2 -2
  70. package/project-template/templates/design-brief.template.md +34 -0
  71. package/project-template/templates/design-methodology.md +1 -2
  72. package/project-template/templates/design.template.md +65 -0
  73. package/project-template/templates/developer-log.template.md +12 -1
  74. package/project-template/templates/five-axis-framework.md +12 -0
  75. package/project-template/templates/game-designer-creative-process.template.md +99 -0
  76. package/project-template/templates/level-sketch.template.json +56 -0
  77. package/project-template/templates/north-star.template.md +43 -0
  78. package/project-template/templates/project-state.template.md +1 -1
  79. package/project-template/vite.config.ts +4 -5
  80. package/project-templates/phaser/.claude/agents/creative-director.md +114 -0
  81. package/project-templates/phaser/.claude/agents/game-designer.md +41 -20
  82. package/project-templates/phaser/.claude/agents/refs/creative-director-guide.md +35 -0
  83. package/project-templates/phaser/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  84. package/project-templates/phaser/.claude/agents/refs/creative-discovery-guide.md +76 -0
  85. package/project-templates/phaser/.claude/agents/refs/creative-north-star-examples.md +82 -0
  86. package/project-templates/phaser/.claude/agents/refs/developer-dev-handoff.md +1 -1
  87. package/project-templates/phaser/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  88. package/project-templates/phaser/.claude/agents/reviewer.md +11 -0
  89. package/project-templates/phaser/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  90. package/project-templates/phaser/.claude/hooks/user-prompt.mjs +17 -18
  91. package/project-templates/phaser/.claude/hooks/validate-workflow-stop.mjs +209 -12
  92. package/project-templates/phaser/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  93. package/project-templates/phaser/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  94. package/project-templates/phaser/CLAUDE.md +88 -54
  95. package/project-templates/phaser/docs/DESIGN.md +25 -0
  96. package/project-templates/phaser/docs/project-state.md +1 -1
  97. package/project-templates/phaser/docs/team/collaboration.md +19 -7
  98. package/project-templates/phaser/docs/team/workflow-changelog.md +14 -0
  99. package/project-templates/phaser/fusion-patterns/catalog.json +77 -0
  100. package/project-templates/phaser/package.json +3 -1
  101. package/project-templates/phaser/skills/_shared/scripts/dev-start.mjs +3 -3
  102. package/project-templates/phaser/skills/_shared/scripts/disclosure-config.mjs +65 -39
  103. package/project-templates/phaser/skills/_shared/scripts/harness-smoke.mjs +3 -1
  104. package/project-templates/phaser/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  105. package/project-templates/phaser/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  106. package/project-templates/phaser/skills/_shared/scripts/state-get.mjs +124 -7
  107. package/project-templates/phaser/skills/_shared/scripts/state-store.mjs +25 -2
  108. package/project-templates/phaser/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  109. package/project-templates/phaser/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  110. package/project-templates/phaser/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  111. package/project-templates/phaser/skills/playable-record/scripts/record-playable.mjs +2 -1
  112. package/project-templates/phaser/skills/playable-record/scripts/verify-contract.mjs +4 -4
  113. package/project-templates/phaser/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  114. package/project-templates/phaser/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  115. package/project-templates/phaser/skills/playable-record/scripts/verify-slice.mjs +2 -2
  116. package/project-templates/phaser/templates/design-brief.template.md +42 -11
  117. package/project-templates/phaser/templates/design-methodology.md +1 -2
  118. package/project-templates/phaser/templates/design.template.md +77 -0
  119. package/project-templates/phaser/templates/five-axis-framework.md +50 -1
  120. package/project-templates/phaser/templates/game-designer-creative-process.template.md +99 -0
  121. package/project-templates/phaser/templates/level-sketch.template.json +56 -0
  122. package/project-templates/phaser/templates/north-star.template.md +43 -0
  123. package/project-templates/phaser/templates/project-state.template.md +1 -1
  124. package/project-templates/phaser/vite.config.ts +4 -5
  125. package/project-templates/threejs/.claude/agents/creative-director.md +114 -0
  126. package/project-templates/threejs/.claude/agents/game-designer.md +41 -20
  127. package/project-templates/threejs/.claude/agents/refs/creative-director-guide.md +35 -0
  128. package/project-templates/threejs/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  129. package/project-templates/threejs/.claude/agents/refs/creative-discovery-guide.md +76 -0
  130. package/project-templates/threejs/.claude/agents/refs/creative-north-star-examples.md +82 -0
  131. package/project-templates/threejs/.claude/agents/refs/developer-dev-handoff.md +1 -1
  132. package/project-templates/threejs/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  133. package/project-templates/threejs/.claude/agents/reviewer.md +11 -0
  134. package/project-templates/threejs/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  135. package/project-templates/threejs/.claude/hooks/user-prompt.mjs +17 -18
  136. package/project-templates/threejs/.claude/hooks/validate-workflow-stop.mjs +209 -12
  137. package/project-templates/threejs/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  138. package/project-templates/threejs/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  139. package/project-templates/threejs/CLAUDE.md +88 -54
  140. package/project-templates/threejs/docs/DESIGN.md +25 -0
  141. package/project-templates/threejs/docs/project-state.md +1 -1
  142. package/project-templates/threejs/docs/team/collaboration.md +19 -7
  143. package/project-templates/threejs/docs/team/workflow-changelog.md +14 -0
  144. package/project-templates/threejs/fusion-patterns/catalog.json +77 -0
  145. package/project-templates/threejs/package.json +3 -1
  146. package/project-templates/threejs/skills/_shared/scripts/dev-start.mjs +3 -3
  147. package/project-templates/threejs/skills/_shared/scripts/disclosure-config.mjs +65 -39
  148. package/project-templates/threejs/skills/_shared/scripts/harness-smoke.mjs +3 -1
  149. package/project-templates/threejs/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  150. package/project-templates/threejs/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  151. package/project-templates/threejs/skills/_shared/scripts/state-get.mjs +124 -7
  152. package/project-templates/threejs/skills/_shared/scripts/state-store.mjs +25 -2
  153. package/project-templates/threejs/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  154. package/project-templates/threejs/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  155. package/project-templates/threejs/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  156. package/project-templates/threejs/skills/playable-record/scripts/record-playable.mjs +2 -1
  157. package/project-templates/threejs/skills/playable-record/scripts/verify-contract.mjs +4 -4
  158. package/project-templates/threejs/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  159. package/project-templates/threejs/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  160. package/project-templates/threejs/skills/playable-record/scripts/verify-slice.mjs +2 -2
  161. package/project-templates/threejs/templates/design-brief.template.md +42 -11
  162. package/project-templates/threejs/templates/design-methodology.md +1 -2
  163. package/project-templates/threejs/templates/design.template.md +77 -0
  164. package/project-templates/threejs/templates/five-axis-framework.md +50 -1
  165. package/project-templates/threejs/templates/game-designer-creative-process.template.md +99 -0
  166. package/project-templates/threejs/templates/level-sketch.template.json +56 -0
  167. package/project-templates/threejs/templates/north-star.template.md +43 -0
  168. package/project-templates/threejs/templates/project-state.template.md +1 -1
  169. package/project-templates/threejs/vite.config.ts +4 -5
  170. package/project-template/docs/DESIGN.md +0 -65
@@ -5,6 +5,9 @@ import { readFileSync } from 'node:fs';
5
5
  import { PlayCraftApiError, PlayCraftHttpClient, isPlayCraftApiError } from '../utils/playcraft-http-client.js';
6
6
  import { IMAGE_OPTION_HELP, buildReferenceImagesPrompt, readImageAttachments, uploadImageAttachments, } from '../utils/remix-image-attachments.js';
7
7
  import { initTemplateCommand } from '../remix/init-template.js';
8
+ import { remixClone, gitCredentialCommand, isRemoteCloneSource } from '../remix/clone.js';
9
+ import { remixPush } from '../remix/push.js';
10
+ import { remixPull } from '../remix/pull.js';
8
11
  // Register autocomplete prompt
9
12
  inquirer.registerPrompt('autocomplete', autocomplete);
10
13
  const JSON_OUTPUT_HELP = '输出一条 JSON (JavaScript Object Notation, JavaScript 对象表示法) 结果';
@@ -1266,4 +1269,94 @@ export function registerRemixCommands(program) {
1266
1269
  .option('--token <token>', PAT_HELP)
1267
1270
  .option('--json', JSON_OUTPUT_HELP)
1268
1271
  .action((opts) => remixRun({ ...opts, images: opts.image, timeout: Math.max(1, Number(opts.timeout) || 1800) }));
1272
+ remix.command('clone')
1273
+ .description('经 PlayCraft Git 代理克隆 Remix 源码到本地(支持粘贴详情页 / Editor / 工蜂 URL)')
1274
+ .argument('[source]', 'Remix 详情页、Editor 地址,或工蜂/Git 分支页 URL')
1275
+ .argument('[dir]', '本地目标目录(默认 <projectId>-<branch>)')
1276
+ .option('--project-id <id>', '项目 ID(未传 URL 时必填)')
1277
+ .option('--branch <name>', 'Remix 分支名(未传 URL 或工蜂地址无 /tree 时必填)')
1278
+ .option('--install', 'clone 后执行 npm install')
1279
+ .option('--dev', 'install 后执行 npm run dev')
1280
+ .option('--url <url>', SERVICE_ADDRESS_HELP)
1281
+ .option('--token <token>', PAT_HELP)
1282
+ .option('--json', JSON_OUTPUT_HELP)
1283
+ .action(async (sourceArg, dirArg, opts) => {
1284
+ try {
1285
+ let source = sourceArg ? String(sourceArg) : undefined;
1286
+ let dir = dirArg ? String(dirArg) : undefined;
1287
+ // `clone ./outdir --project-id … --branch …` → first arg is dir
1288
+ if (source && !isRemoteCloneSource(source)) {
1289
+ dir = source;
1290
+ source = undefined;
1291
+ }
1292
+ await remixClone({
1293
+ source,
1294
+ dir,
1295
+ projectId: opts.projectId ? String(opts.projectId) : undefined,
1296
+ branch: opts.branch ? String(opts.branch) : undefined,
1297
+ install: Boolean(opts.install || opts.dev),
1298
+ dev: Boolean(opts.dev),
1299
+ url: opts.url,
1300
+ token: opts.token,
1301
+ json: Boolean(opts.json),
1302
+ });
1303
+ }
1304
+ catch (error) {
1305
+ handleCommandError('remix.clone', opts, undefined, error);
1306
+ }
1307
+ });
1308
+ remix
1309
+ .command('push')
1310
+ .description('经 PlayCraft Git 代理推送当前 Remix(注入已登录 PAT,避免 Windows Git Credential Manager 弹窗)')
1311
+ .argument('[git-args...]', '透传给 git push 的参数(可选)')
1312
+ .option('--dir <path>', '仓库目录(默认当前目录)')
1313
+ .option('--url <url>', SERVICE_ADDRESS_HELP)
1314
+ .option('--token <token>', PAT_HELP)
1315
+ .option('--json', JSON_OUTPUT_HELP)
1316
+ .action(async (gitArgs, opts) => {
1317
+ try {
1318
+ await remixPush({
1319
+ dir: opts.dir ? String(opts.dir) : undefined,
1320
+ url: opts.url,
1321
+ token: opts.token,
1322
+ json: Boolean(opts.json),
1323
+ gitArgs: Array.isArray(gitArgs) ? gitArgs.map(String) : [],
1324
+ });
1325
+ }
1326
+ catch (error) {
1327
+ handleCommandError('remix.push', opts, undefined, error);
1328
+ }
1329
+ });
1330
+ remix
1331
+ .command('pull')
1332
+ .description('经 PlayCraft Git 代理拉取当前 Remix(注入已登录 PAT,避免 Windows Git Credential Manager 弹窗)')
1333
+ .argument('[git-args...]', '透传给 git pull 的参数(可选)')
1334
+ .option('--dir <path>', '仓库目录(默认当前目录)')
1335
+ .option('--url <url>', SERVICE_ADDRESS_HELP)
1336
+ .option('--token <token>', PAT_HELP)
1337
+ .option('--json', JSON_OUTPUT_HELP)
1338
+ .action(async (gitArgs, opts) => {
1339
+ try {
1340
+ await remixPull({
1341
+ dir: opts.dir ? String(opts.dir) : undefined,
1342
+ url: opts.url,
1343
+ token: opts.token,
1344
+ json: Boolean(opts.json),
1345
+ gitArgs: Array.isArray(gitArgs) ? gitArgs.map(String) : [],
1346
+ });
1347
+ }
1348
+ catch (error) {
1349
+ handleCommandError('remix.pull', opts, undefined, error);
1350
+ }
1351
+ });
1352
+ }
1353
+ /** Register `playcraft git-credential`(clone 写入的 helper;拉/推请用 remix pull / push). */
1354
+ export function registerGitCredentialCommand(program) {
1355
+ program
1356
+ .command('git-credential')
1357
+ .description('Git credential helper(一般勿手动调用;请用 playcraft remix pull / push)')
1358
+ .argument('[action]', 'get | store | erase', 'get')
1359
+ .action((action) => {
1360
+ gitCredentialCommand(action);
1361
+ });
1269
1362
  }
@@ -21,6 +21,7 @@
21
21
  import * as fs from 'node:fs';
22
22
  import * as path from 'node:path';
23
23
  import { getFusionHintsForGameplayAtoms, } from '../atom-tree/fusion-hints.js';
24
+ import { detectFusionIntent, fusionCompositeScoreBoost, fusionSingleMechanicPenalty, isFusionCompositeManifest, } from '../atom-tree/fusion-intent.js';
24
25
  import { collectPinnedDependencies, loadSkillManifestIndex, parseAtomTreeJson, parseSelectedAtomRefsFromAtomTreeProject, isUninitializedAtomTreeSkeleton, resolveSkillsDirs, validateAtomTreeSelectionProject, } from '../atom-tree/selection.js';
25
26
  import { buildAtomTreeSkeleton } from '../atom-tree/build-skeleton.js';
26
27
  import { validateAsrCoverageProject } from '../asr/validate-asr-coverage.js';
@@ -83,6 +84,10 @@ const INTENT_TAG_ALIASES = {
83
84
  mahjong: ['tile', 'tiles', '牌面'],
84
85
  麻将: ['tile', 'tiles', 'mahjong', '牌面'],
85
86
  solitaire: ['solitaire', 'klondike', 'card'],
87
+ 消除: ['elimination', 'match-3', 'match3', 'match'],
88
+ 箭头: ['arrow', 'path', 'slide-out'],
89
+ fusion: ['融合', 'hybrid', 'combine'],
90
+ 融合: ['fusion', 'hybrid', 'combine', 'match-3', 'arrow'],
86
91
  };
87
92
  const ANCHOR_MEDIA_ROLE_LABELS = {
88
93
  tileTextureSet: { label: '牌面/瓦片贴图组', layer: '视觉资产层' },
@@ -250,7 +255,7 @@ function getCategoryLayer(category, atomId) {
250
255
  return '玩法层';
251
256
  }
252
257
  // ─── Manifest 索引器 ─────────────────────────────────────────────────────────
253
- function loadSkillIndex(skillsDirs) {
258
+ export function loadSkillIndex(skillsDirs) {
254
259
  const entries = [];
255
260
  const seen = new Set();
256
261
  for (const dir of skillsDirs) {
@@ -281,7 +286,10 @@ function loadSkillIndex(skillsDirs) {
281
286
  // ─── 推荐算法 ────────────────────────────────────────────────────────────────
282
287
  function getGameplayType(manifest) {
283
288
  const gen = manifest['generation'];
284
- return gen?.gameplayType;
289
+ if (gen?.gameplayType)
290
+ return gen.gameplayType;
291
+ const pgs = manifest['pgs'];
292
+ return pgs?.gameplayType;
285
293
  }
286
294
  function isEngineManifest(manifest) {
287
295
  return manifest.category === 'engine' || manifest.bundleType === 'aicomponent';
@@ -315,7 +323,7 @@ function pinAtomIdToBareName(pinAtomId) {
315
323
  /**
316
324
  * 仅按 intent 打分(discovery 相位)。不假设目标引擎。
317
325
  */
318
- export function scoreSkillIntentOnly(entry, intentTags) {
326
+ export function scoreSkillIntentOnly(entry, intentTags, rawIntent = '') {
319
327
  if (intentTags.length === 0)
320
328
  return 0;
321
329
  const { manifest } = entry;
@@ -333,6 +341,8 @@ export function scoreSkillIntentOnly(entry, intentTags) {
333
341
  }
334
342
  if (manifest.category === 'engine')
335
343
  score += 1;
344
+ score += fusionCompositeScoreBoost(manifest, intentTags, rawIntent);
345
+ score += fusionSingleMechanicPenalty(manifest, intentTags, rawIntent);
336
346
  return score;
337
347
  }
338
348
  /**
@@ -392,7 +402,7 @@ export function inferSuggestedEngines(scored, intentTags, indexMap) {
392
402
  * 计算一个 skill 对给定 engine + intent 的匹配分数。
393
403
  * 得分越高,越应该被推荐。
394
404
  */
395
- export function scoreSkill(entry, engine, intentTags) {
405
+ export function scoreSkill(entry, engine, intentTags, rawIntent = '') {
396
406
  const { manifest } = entry;
397
407
  let score = 0;
398
408
  // 1. 引擎精确匹配:renderBackend 或 engineVariants 中有指定引擎
@@ -418,6 +428,8 @@ export function scoreSkill(entry, engine, intentTags) {
418
428
  });
419
429
  if (importsEngine)
420
430
  score += 5;
431
+ score += fusionCompositeScoreBoost(manifest, intentTags, rawIntent);
432
+ score += fusionSingleMechanicPenalty(manifest, intentTags, rawIntent);
421
433
  return score;
422
434
  }
423
435
  /**
@@ -452,9 +464,12 @@ function collectDependencies(atomId, index, engine, visited = new Set(), depth =
452
464
  }
453
465
  else {
454
466
  // 无 variant,使用顶层 imports
467
+ // 只对 requires 做递归传递依赖展开;compatibleWith 只记录直接引用
455
468
  for (const imp of entry.manifest.imports ?? []) {
456
- if (imp.pinAtomId && !visited.has(imp.pinAtomId)) {
457
- deps.push(imp.pinAtomId);
469
+ if (!imp.pinAtomId || visited.has(imp.pinAtomId))
470
+ continue;
471
+ deps.push(imp.pinAtomId);
472
+ if (imp.edgeKind === 'requires') {
458
473
  deps.push(...collectDependencies(imp.pinAtomId, index, engine, visited, depth + 1));
459
474
  }
460
475
  }
@@ -563,9 +578,10 @@ function buildIntentOnlyMatchResult(entries, intentTags, suggestedEngines, index
563
578
  /**
564
579
  * 仅 --intent:先推断引擎,再对首选引擎做完整 match(一轮输出全部内容)。
565
580
  */
566
- export function buildAutoMatchResult(entries, intentTags) {
581
+ export function buildAutoMatchResult(entries, intentTags, rawIntent = '') {
582
+ const intentBlob = rawIntent || intentTags.join(' ');
567
583
  const scored = entries
568
- .map((entry) => ({ entry, score: scoreSkillIntentOnly(entry, intentTags) }))
584
+ .map((entry) => ({ entry, score: scoreSkillIntentOnly(entry, intentTags, intentBlob) }))
569
585
  .filter(({ score }) => score >= SCORE_THRESHOLD_DISCOVERY);
570
586
  const indexMap = new Map(entries.map((e) => [e.atomId, e]));
571
587
  const suggestedEngines = inferSuggestedEngines(scored, intentTags, indexMap);
@@ -573,7 +589,7 @@ export function buildAutoMatchResult(entries, intentTags) {
573
589
  if (!engine) {
574
590
  return buildIntentOnlyMatchResult(entries, intentTags, suggestedEngines, indexMap);
575
591
  }
576
- const full = buildMatchResult(entries, engine, intentTags);
592
+ const full = buildMatchResult(entries, engine, intentTags, intentBlob);
577
593
  const gap = suggestedEngines.length > 1 ? suggestedEngines[0].score - suggestedEngines[1].score : 99;
578
594
  return {
579
595
  ...full,
@@ -582,8 +598,20 @@ export function buildAutoMatchResult(entries, intentTags) {
582
598
  engineAmbiguous: suggestedEngines.length > 1 && gap < 5,
583
599
  };
584
600
  }
585
- function selectMatchSeedAtomIds(scored, engine, intentTags) {
601
+ function selectMatchSeedAtomIds(scored, engine, intentTags, rawIntent = '') {
586
602
  const seeds = new Set();
603
+ const fusionIntent = detectFusionIntent(intentTags, rawIntent);
604
+ if (fusionIntent) {
605
+ const composites = scored.filter(({ entry, score }) => isGameplayManifest(entry.manifest) &&
606
+ isFusionCompositeManifest(entry.manifest) &&
607
+ score >= SCORE_THRESHOLD_FULL &&
608
+ passesGameplayIntentFilter(entry, intentTags));
609
+ if (composites.length > 0) {
610
+ for (const { entry } of composites)
611
+ seeds.add(entry.atomId);
612
+ return seeds;
613
+ }
614
+ }
587
615
  for (const { entry, score } of scored) {
588
616
  const { manifest } = entry;
589
617
  if (isEngineManifest(manifest) && pinAtomIdToEngine(entry.atomId, manifest) === engine) {
@@ -674,19 +702,20 @@ function buildItemReason(entry, engine, intentTags, score, via, dependencyReason
674
702
  return `标签:${matchedTags.join(', ')}`;
675
703
  return score !== null ? `score=${score}` : '推荐';
676
704
  }
677
- export function buildMatchResult(entries, engine, intentTags) {
705
+ export function buildMatchResult(entries, engine, intentTags, rawIntent = '') {
706
+ const intentBlob = rawIntent || intentTags.join(' ');
678
707
  const indexMap = new Map(entries.map((e) => [e.atomId, e]));
679
708
  const scoreById = new Map();
680
709
  const expandedIntent = expandIntentTags(intentTags);
681
710
  const scored = entries
682
711
  .map((entry) => {
683
- const score = scoreSkill(entry, engine, expandedIntent);
712
+ const score = scoreSkill(entry, engine, expandedIntent, intentBlob);
684
713
  scoreById.set(entry.atomId, score);
685
714
  return { entry, score };
686
715
  })
687
716
  .filter(({ score }) => score >= SCORE_THRESHOLD_FULL)
688
717
  .sort((a, b) => b.score - a.score);
689
- const seedIds = selectMatchSeedAtomIds(scored, engine, expandedIntent);
718
+ const seedIds = selectMatchSeedAtomIds(scored, engine, expandedIntent, intentBlob);
690
719
  const requiredIds = expandSeedClosure(seedIds, indexMap, engine);
691
720
  const seedIdSet = seedIds;
692
721
  const dependencyReasonMap = buildDependencyReasonMap(requiredIds, indexMap, engine);
@@ -1475,9 +1504,10 @@ export function registerSkillsCommands(program) {
1475
1504
  ' 示例:playcraft skills match --engine phaser --intent "match3,grid" --json');
1476
1505
  process.exit(1);
1477
1506
  }
1507
+ const intentBlob = opts.intent;
1478
1508
  const result = opts.engine
1479
- ? buildMatchResult(entries, opts.engine, intentTags)
1480
- : buildAutoMatchResult(entries, intentTags);
1509
+ ? buildMatchResult(entries, opts.engine, intentTags, intentBlob)
1510
+ : buildAutoMatchResult(entries, intentTags, intentBlob);
1481
1511
  if (opts.json) {
1482
1512
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
1483
1513
  return;
@@ -1909,9 +1939,10 @@ export function registerSkillsCommands(program) {
1909
1939
  process.exit(1);
1910
1940
  }
1911
1941
  }
1942
+ const intentBlob = opts.intent;
1912
1943
  const result = opts.engine
1913
- ? buildMatchResult(entries, opts.engine, intentTags)
1914
- : buildAutoMatchResult(entries, intentTags);
1944
+ ? buildMatchResult(entries, opts.engine, intentTags, intentBlob)
1945
+ : buildAutoMatchResult(entries, intentTags, intentBlob);
1915
1946
  if (!result.engine) {
1916
1947
  console.log('无法从 intent 推断运行引擎。请指定:--engine phaser 或 --engine threejs');
1917
1948
  console.log(`示例:playcraft skills init-atom-tree --intent "${intentTags.join(',')}" --engine phaser --force`);
package/dist/index.js CHANGED
@@ -21,8 +21,10 @@ import { registerImageCommands } from './commands/image.js';
21
21
  import { registerAudioCommands } from './commands/audio.js';
22
22
  import { registerPrefabCommands } from './commands/prefab.js';
23
23
  import { registerRecommendCommands } from './commands/recommend.js';
24
- import { registerRemixCommands } from './commands/remix.js';
24
+ import { registerRemixCommands, registerGitCredentialCommand } from './commands/remix.js';
25
25
  import { registerSkillsCommands } from './commands/skills.js';
26
+ import { registerLevelsCommands } from './commands/levels.js';
27
+ import { registerPatternsCommands } from './commands/patterns.js';
26
28
  import { registerPradCommands } from './commands/prad.js';
27
29
  import { register3DCommands } from './commands/3d.js';
28
30
  import { CLI_ROOT_DESCRIPTION, getCliTopicsHelpText, registerRootProgramHelp, } from './cli-root-help.js';
@@ -30,10 +32,7 @@ const __filename = fileURLToPath(import.meta.url);
30
32
  const __dirname = dirname(__filename);
31
33
  const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
32
34
  const program = new Command();
33
- program
34
- .name('playcraft')
35
- .description(CLI_ROOT_DESCRIPTION)
36
- .version(packageJson.version);
35
+ program.name('playcraft').description(CLI_ROOT_DESCRIPTION).version(packageJson.version);
37
36
  registerRootProgramHelp(program);
38
37
  // 版本检查(非阻塞,后台执行)
39
38
  // 只在非 upgrade 命令时检查,避免重复提示
@@ -45,6 +44,8 @@ program
45
44
  .command('create [project-name]')
46
45
  .description('从模板创建新的可玩广告项目')
47
46
  .option('-d, --dir <path>', '在指定目录下创建项目', process.cwd())
47
+ .option('--port <number>', '本地 Agent 端口(非交互模式默认 2468)', '2468')
48
+ .option('--non-interactive', '跳过交互式问答,使用命令行参数或默认值')
48
49
  .option('-t, --template <id>', '模板: agent | phaser-playable | three-playable', 'agent')
49
50
  .option('--here', '在当前目录(或 --dir 指定目录)直接初始化,不创建子目录')
50
51
  .option('-f, --force', '非交互模式:目标目录非空时仍覆盖同名文件')
@@ -359,8 +360,11 @@ registerPrefabCommands(program);
359
360
  registerRecommendCommands(program);
360
361
  // Remix workflow (playcraft remix <command>)
361
362
  registerRemixCommands(program);
363
+ registerGitCredentialCommand(program);
362
364
  // 开发时 Skill 发现与脚手架 (playcraft skills <command>)
363
365
  registerSkillsCommands(program);
366
+ registerLevelsCommands(program);
367
+ registerPatternsCommands(program);
364
368
  registerPradCommands(program);
365
369
  // 3D model utilities (playcraft 3d <command>)
366
370
  register3DCommands(program);
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Parse sources accepted by `playcraft remix clone`:
3
+ * - PlayCraft remix detail: /projects/:id/remixes/:branch
4
+ * - PlayCraft remix editor: /editor/remix/:id?branch=
5
+ * - Gongfeng / GitLab / GitHub branch tree or bare repo URL
6
+ */
7
+ /** 工蜂内网 / 外网域名互为别名(详情页可能展示任一)。 */
8
+ export const GONGFENG_HOST_ALIASES = ['git.woa.com', 'git.tencent.com'];
9
+ const GIT_HOST_HINTS = [
10
+ ...GONGFENG_HOST_ALIASES,
11
+ 'github.com',
12
+ 'gitlab.com',
13
+ 'gitee.com',
14
+ ];
15
+ function looksLikeHttpUrl(value) {
16
+ return /^https?:\/\//i.test(value.trim());
17
+ }
18
+ /** Canonical host for Gongfeng alias matching (woa ↔ tencent). */
19
+ export function canonicalizeGitHost(hostname) {
20
+ const host = hostname.trim().toLowerCase();
21
+ if (GONGFENG_HOST_ALIASES.includes(host)) {
22
+ return 'git.woa.com';
23
+ }
24
+ return host;
25
+ }
26
+ function isGongfengHost(hostname) {
27
+ const host = hostname.trim().toLowerCase();
28
+ return GONGFENG_HOST_ALIASES.includes(host);
29
+ }
30
+ function decodePathSegment(segment) {
31
+ try {
32
+ return decodeURIComponent(segment);
33
+ }
34
+ catch {
35
+ return segment;
36
+ }
37
+ }
38
+ /** Strip credentials and trailing .git / slash for comparison. Gongfeng hosts are aliased. */
39
+ export function normalizeGitRepositoryUrl(raw) {
40
+ let url = raw.trim();
41
+ // git@host:group/repo.git → https://host/group/repo
42
+ const scp = /^git@([^:]+):(.+)$/.exec(url);
43
+ if (scp) {
44
+ url = `https://${scp[1]}/${scp[2]}`;
45
+ }
46
+ try {
47
+ const u = new URL(url);
48
+ u.hash = '';
49
+ u.search = '';
50
+ u.username = '';
51
+ u.password = '';
52
+ u.hostname = canonicalizeGitHost(u.hostname);
53
+ let pathname = u.pathname.replace(/\/+$/, '');
54
+ pathname = pathname.replace(/\.git$/i, '');
55
+ return `${u.protocol}//${u.host}${pathname}`.toLowerCase();
56
+ }
57
+ catch {
58
+ return url
59
+ .replace(/\.git$/i, '')
60
+ .replace(/\/+$/, '')
61
+ .toLowerCase();
62
+ }
63
+ }
64
+ /**
65
+ * From a host tree/blob URL, extract repo base + optional branch.
66
+ * Supports:
67
+ * .../tree/<branch>
68
+ * .../-/tree/<branch> (GitLab)
69
+ * .../blob/<branch>/...
70
+ */
71
+ export function parseGitHostTreeUrl(raw) {
72
+ let input = raw.trim();
73
+ if (!looksLikeHttpUrl(input) && !input.startsWith('git@'))
74
+ return null;
75
+ // scp-style
76
+ if (input.startsWith('git@')) {
77
+ const scp = /^git@([^:]+):(.+)$/.exec(input);
78
+ if (!scp)
79
+ return null;
80
+ input = `https://${scp[1]}/${scp[2]}`;
81
+ }
82
+ let u;
83
+ try {
84
+ u = new URL(input);
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ const host = u.hostname.toLowerCase();
90
+ const isKnownHost = isGongfengHost(host) ||
91
+ GIT_HOST_HINTS.some((h) => host === h || host.endsWith(`.${h}`)) ||
92
+ host.includes('git.');
93
+ // PlayCraft portal hosts are handled elsewhere
94
+ if (!isKnownHost && !u.pathname.includes('/tree/') && !u.pathname.includes('/-/tree/')) {
95
+ // Still allow any https URL that looks like *.git
96
+ if (!u.pathname.endsWith('.git') && !input.endsWith('.git')) {
97
+ return null;
98
+ }
99
+ }
100
+ const path = u.pathname;
101
+ const gitlabTree = /\/-\/tree\/([^/]+)(?:\/|$)/.exec(path);
102
+ const plainTree = /\/tree\/([^/]+)(?:\/|$)/.exec(path);
103
+ const blob = /\/blob\/([^/]+)(?:\/|$)/.exec(path);
104
+ const match = gitlabTree || plainTree || blob;
105
+ let repositoryPath = path;
106
+ let branch;
107
+ if (match) {
108
+ branch = decodePathSegment(match[1]);
109
+ repositoryPath = path.slice(0, match.index);
110
+ }
111
+ repositoryPath = repositoryPath.replace(/\.git$/i, '').replace(/\/+$/, '');
112
+ if (!repositoryPath || repositoryPath === '/')
113
+ return null;
114
+ const repositoryUrl = `${u.protocol}//${u.host}${repositoryPath}`;
115
+ return { repositoryUrl, branch };
116
+ }
117
+ export function parsePlaycraftRemixUrl(raw) {
118
+ if (!looksLikeHttpUrl(raw))
119
+ return null;
120
+ let u;
121
+ try {
122
+ u = new URL(raw.trim());
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ // /projects/:projectId/remixes/:branch
128
+ const detail = /^\/projects\/([^/]+)\/remixes\/([^/]+)\/?$/.exec(u.pathname);
129
+ if (detail) {
130
+ const projectId = decodePathSegment(detail[1]);
131
+ const branch = decodePathSegment(detail[2]);
132
+ if (!projectId || !branch)
133
+ return null;
134
+ return {
135
+ kind: 'playcraft-detail',
136
+ projectId,
137
+ branch,
138
+ origin: u.origin,
139
+ };
140
+ }
141
+ // /editor/remix/:projectId?branch=
142
+ const editor = /^\/editor\/remix\/([^/]+)\/?$/.exec(u.pathname);
143
+ if (editor) {
144
+ const projectId = decodePathSegment(editor[1]);
145
+ const branchRaw = u.searchParams.get('branch') || u.searchParams.get('branchId');
146
+ if (!projectId || !branchRaw?.trim())
147
+ return null;
148
+ return {
149
+ kind: 'playcraft-editor',
150
+ projectId,
151
+ branch: decodePathSegment(branchRaw.trim()),
152
+ origin: u.origin,
153
+ };
154
+ }
155
+ return null;
156
+ }
157
+ /**
158
+ * Resolve clone source from a pasted URL or explicit flags.
159
+ * Throws with a user-facing Chinese message on conflict / missing fields.
160
+ */
161
+ export function resolveRemixCloneTarget(input) {
162
+ const source = input.source?.trim();
163
+ const flagProjectId = input.projectId?.trim();
164
+ const flagBranch = input.branch?.trim();
165
+ const sourceLooksRemote = Boolean(source) && (looksLikeHttpUrl(source) || source.startsWith('git@'));
166
+ if (source && sourceLooksRemote) {
167
+ const playcraft = parsePlaycraftRemixUrl(source);
168
+ if (playcraft && (playcraft.kind === 'playcraft-detail' || playcraft.kind === 'playcraft-editor')) {
169
+ if (flagProjectId && flagProjectId !== playcraft.projectId) {
170
+ throw new Error('URL 中的项目 ID 与 --project-id 不一致。');
171
+ }
172
+ if (flagBranch && flagBranch !== playcraft.branch) {
173
+ throw new Error('URL 中的分支与 --branch 不一致。');
174
+ }
175
+ return {
176
+ kind: playcraft.kind,
177
+ projectId: playcraft.projectId,
178
+ branch: playcraft.branch,
179
+ origin: playcraft.origin,
180
+ };
181
+ }
182
+ const gitHost = parseGitHostTreeUrl(source);
183
+ if (gitHost) {
184
+ const branch = flagBranch || gitHost.branch;
185
+ if (!branch) {
186
+ throw new Error('工蜂/Git 地址未包含分支,请在 URL 使用 /tree/<branch>,或额外传入 --branch。');
187
+ }
188
+ return {
189
+ kind: 'git-host',
190
+ repositoryUrl: gitHost.repositoryUrl,
191
+ branch,
192
+ };
193
+ }
194
+ throw new Error('无法识别该 URL。请粘贴 Remix 详情页、Remix Editor 地址,或工蜂分支页(…/tree/<branch>)。');
195
+ }
196
+ if (flagProjectId && flagBranch) {
197
+ return { kind: 'flags', projectId: flagProjectId, branch: flagBranch };
198
+ }
199
+ throw new Error('请传入 Remix 详情/编辑器 URL,或工蜂 Git 地址;也可使用 --project-id 与 --branch。');
200
+ }
201
+ export function isRemoteCloneSource(value) {
202
+ const v = value?.trim();
203
+ return Boolean(v && (looksLikeHttpUrl(v) || v.startsWith('git@')));
204
+ }
205
+ /** @deprecated use isRemoteCloneSource */
206
+ export function isHttpCloneSource(value) {
207
+ return isRemoteCloneSource(value);
208
+ }