@playcraft/cli 0.0.55 → 0.0.56

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 (167) hide show
  1. package/README.md +21 -0
  2. package/REMIX.md +103 -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 +47 -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 +255 -0
  15. package/dist/scaffold/templates.js +1 -1
  16. package/dist/utils/agent-api-client.js +10 -3
  17. package/dist/utils/remix-image-attachments.js +4 -32
  18. package/dist/utils/sandbox-path.js +20 -0
  19. package/package.json +3 -3
  20. package/project-template/.claude/agents/creative-director.md +114 -0
  21. package/project-template/.claude/agents/developer.md +5 -2
  22. package/project-template/.claude/agents/game-designer.md +41 -20
  23. package/project-template/.claude/agents/refs/README.md +8 -0
  24. package/project-template/.claude/agents/refs/creative-director-guide.md +35 -0
  25. package/project-template/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  26. package/project-template/.claude/agents/refs/creative-discovery-guide.md +76 -0
  27. package/project-template/.claude/agents/refs/creative-north-star-examples.md +82 -0
  28. package/project-template/.claude/agents/refs/developer-dev-handoff.md +1 -1
  29. package/project-template/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  30. package/project-template/.claude/agents/refs/game-designer-gameplay-sufficiency.md +4 -4
  31. package/project-template/.claude/agents/reviewer.md +11 -0
  32. package/project-template/.claude/hooks/README.md +12 -12
  33. package/project-template/.claude/hooks/block-developer-preflight.mjs +49 -13
  34. package/project-template/.claude/hooks/block-ring1-tasklist.mjs +1 -1
  35. package/project-template/.claude/hooks/user-prompt.mjs +3 -13
  36. package/project-template/.claude/hooks/validate-workflow-stop.mjs +209 -12
  37. package/project-template/.claude/settings.json +2 -2
  38. package/project-template/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  39. package/project-template/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -1
  40. package/project-template/.claude/snapshot.mjs +0 -1
  41. package/project-template/.codebuddy/settings.json +1 -1
  42. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  43. package/project-template/CLAUDE.md +9 -9
  44. package/project-template/docs/project-state.md +1 -1
  45. package/project-template/docs/team/agent-conduct.md +18 -18
  46. package/project-template/docs/team/collaboration.md +19 -7
  47. package/project-template/docs/team/workflow-changelog.md +15 -0
  48. package/project-template/fusion-patterns/catalog.json +77 -0
  49. package/project-template/package.json +2 -0
  50. package/project-template/skills/_shared/scripts/create-playable.mjs +31 -11
  51. package/project-template/skills/_shared/scripts/dev-start.mjs +3 -3
  52. package/project-template/skills/_shared/scripts/disclosure-config.mjs +65 -39
  53. package/project-template/skills/_shared/scripts/harness-smoke.mjs +3 -1
  54. package/project-template/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  55. package/project-template/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  56. package/project-template/skills/_shared/scripts/slice-set.mjs +29 -2
  57. package/project-template/skills/_shared/scripts/state-get.mjs +124 -7
  58. package/project-template/skills/_shared/scripts/state-store.mjs +25 -2
  59. package/project-template/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  60. package/project-template/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  61. package/project-template/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  62. package/project-template/skills/playable-record/scripts/record-playable.mjs +2 -1
  63. package/project-template/skills/playable-record/scripts/verify-contract.mjs +4 -4
  64. package/project-template/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  65. package/project-template/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  66. package/project-template/skills/playable-record/scripts/verify-slice.mjs +2 -2
  67. package/project-template/templates/design-brief.template.md +34 -0
  68. package/project-template/templates/design-methodology.md +1 -2
  69. package/project-template/templates/design.template.md +65 -0
  70. package/project-template/templates/developer-log.template.md +12 -1
  71. package/project-template/templates/five-axis-framework.md +12 -0
  72. package/project-template/templates/game-designer-creative-process.template.md +99 -0
  73. package/project-template/templates/level-sketch.template.json +56 -0
  74. package/project-template/templates/north-star.template.md +43 -0
  75. package/project-template/templates/project-state.template.md +1 -1
  76. package/project-template/vite.config.ts +4 -5
  77. package/project-templates/phaser/.claude/agents/creative-director.md +114 -0
  78. package/project-templates/phaser/.claude/agents/game-designer.md +41 -20
  79. package/project-templates/phaser/.claude/agents/refs/creative-director-guide.md +35 -0
  80. package/project-templates/phaser/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  81. package/project-templates/phaser/.claude/agents/refs/creative-discovery-guide.md +76 -0
  82. package/project-templates/phaser/.claude/agents/refs/creative-north-star-examples.md +82 -0
  83. package/project-templates/phaser/.claude/agents/refs/developer-dev-handoff.md +1 -1
  84. package/project-templates/phaser/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  85. package/project-templates/phaser/.claude/agents/reviewer.md +11 -0
  86. package/project-templates/phaser/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  87. package/project-templates/phaser/.claude/hooks/user-prompt.mjs +17 -18
  88. package/project-templates/phaser/.claude/hooks/validate-workflow-stop.mjs +209 -12
  89. package/project-templates/phaser/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  90. package/project-templates/phaser/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  91. package/project-templates/phaser/CLAUDE.md +88 -54
  92. package/project-templates/phaser/docs/DESIGN.md +25 -0
  93. package/project-templates/phaser/docs/project-state.md +1 -1
  94. package/project-templates/phaser/docs/team/collaboration.md +19 -7
  95. package/project-templates/phaser/docs/team/workflow-changelog.md +14 -0
  96. package/project-templates/phaser/fusion-patterns/catalog.json +77 -0
  97. package/project-templates/phaser/package.json +3 -1
  98. package/project-templates/phaser/skills/_shared/scripts/dev-start.mjs +3 -3
  99. package/project-templates/phaser/skills/_shared/scripts/disclosure-config.mjs +65 -39
  100. package/project-templates/phaser/skills/_shared/scripts/harness-smoke.mjs +3 -1
  101. package/project-templates/phaser/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  102. package/project-templates/phaser/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  103. package/project-templates/phaser/skills/_shared/scripts/state-get.mjs +124 -7
  104. package/project-templates/phaser/skills/_shared/scripts/state-store.mjs +25 -2
  105. package/project-templates/phaser/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  106. package/project-templates/phaser/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  107. package/project-templates/phaser/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  108. package/project-templates/phaser/skills/playable-record/scripts/record-playable.mjs +2 -1
  109. package/project-templates/phaser/skills/playable-record/scripts/verify-contract.mjs +4 -4
  110. package/project-templates/phaser/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  111. package/project-templates/phaser/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  112. package/project-templates/phaser/skills/playable-record/scripts/verify-slice.mjs +2 -2
  113. package/project-templates/phaser/templates/design-brief.template.md +42 -11
  114. package/project-templates/phaser/templates/design-methodology.md +1 -2
  115. package/project-templates/phaser/templates/design.template.md +77 -0
  116. package/project-templates/phaser/templates/five-axis-framework.md +50 -1
  117. package/project-templates/phaser/templates/game-designer-creative-process.template.md +99 -0
  118. package/project-templates/phaser/templates/level-sketch.template.json +56 -0
  119. package/project-templates/phaser/templates/north-star.template.md +43 -0
  120. package/project-templates/phaser/templates/project-state.template.md +1 -1
  121. package/project-templates/phaser/vite.config.ts +4 -5
  122. package/project-templates/threejs/.claude/agents/creative-director.md +114 -0
  123. package/project-templates/threejs/.claude/agents/game-designer.md +41 -20
  124. package/project-templates/threejs/.claude/agents/refs/creative-director-guide.md +35 -0
  125. package/project-templates/threejs/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  126. package/project-templates/threejs/.claude/agents/refs/creative-discovery-guide.md +76 -0
  127. package/project-templates/threejs/.claude/agents/refs/creative-north-star-examples.md +82 -0
  128. package/project-templates/threejs/.claude/agents/refs/developer-dev-handoff.md +1 -1
  129. package/project-templates/threejs/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  130. package/project-templates/threejs/.claude/agents/reviewer.md +11 -0
  131. package/project-templates/threejs/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  132. package/project-templates/threejs/.claude/hooks/user-prompt.mjs +17 -18
  133. package/project-templates/threejs/.claude/hooks/validate-workflow-stop.mjs +209 -12
  134. package/project-templates/threejs/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  135. package/project-templates/threejs/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  136. package/project-templates/threejs/CLAUDE.md +88 -54
  137. package/project-templates/threejs/docs/DESIGN.md +25 -0
  138. package/project-templates/threejs/docs/project-state.md +1 -1
  139. package/project-templates/threejs/docs/team/collaboration.md +19 -7
  140. package/project-templates/threejs/docs/team/workflow-changelog.md +14 -0
  141. package/project-templates/threejs/fusion-patterns/catalog.json +77 -0
  142. package/project-templates/threejs/package.json +3 -1
  143. package/project-templates/threejs/skills/_shared/scripts/dev-start.mjs +3 -3
  144. package/project-templates/threejs/skills/_shared/scripts/disclosure-config.mjs +65 -39
  145. package/project-templates/threejs/skills/_shared/scripts/harness-smoke.mjs +3 -1
  146. package/project-templates/threejs/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  147. package/project-templates/threejs/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  148. package/project-templates/threejs/skills/_shared/scripts/state-get.mjs +124 -7
  149. package/project-templates/threejs/skills/_shared/scripts/state-store.mjs +25 -2
  150. package/project-templates/threejs/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  151. package/project-templates/threejs/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  152. package/project-templates/threejs/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  153. package/project-templates/threejs/skills/playable-record/scripts/record-playable.mjs +2 -1
  154. package/project-templates/threejs/skills/playable-record/scripts/verify-contract.mjs +4 -4
  155. package/project-templates/threejs/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  156. package/project-templates/threejs/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  157. package/project-templates/threejs/skills/playable-record/scripts/verify-slice.mjs +2 -2
  158. package/project-templates/threejs/templates/design-brief.template.md +42 -11
  159. package/project-templates/threejs/templates/design-methodology.md +1 -2
  160. package/project-templates/threejs/templates/design.template.md +77 -0
  161. package/project-templates/threejs/templates/five-axis-framework.md +50 -1
  162. package/project-templates/threejs/templates/game-designer-creative-process.template.md +99 -0
  163. package/project-templates/threejs/templates/level-sketch.template.json +56 -0
  164. package/project-templates/threejs/templates/north-star.template.md +43 -0
  165. package/project-templates/threejs/templates/project-state.template.md +1 -1
  166. package/project-templates/threejs/vite.config.ts +4 -5
  167. package/project-template/docs/DESIGN.md +0 -65
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import sharp from 'sharp';
5
5
  import yaml from 'js-yaml';
6
6
  import { AgentApiClient } from '../utils/agent-api-client.js';
7
+ import { resolveSandboxPath } from '../utils/sandbox-path.js';
7
8
  /** 单文件输入上限(字节),防止误处理巨型文件导致 OOM */
8
9
  const DEFAULT_MAX_INPUT_BYTES = 512 * 1024 * 1024;
9
10
  function getMaxInputBytes() {
@@ -20,23 +21,26 @@ function cliParseInt(value) {
20
21
  return Number.parseInt(value, 10);
21
22
  }
22
23
  function assertInputWithinLimit(filePath, label = 'input') {
24
+ const resolved = resolveSandboxPath(filePath);
23
25
  const max = getMaxInputBytes();
24
- const st = statSync(filePath);
26
+ const st = statSync(resolved);
25
27
  if (!st.isFile()) {
26
- console.error(`Error: ${label} is not a regular file: ${filePath}`);
28
+ console.error(`Error: ${label} is not a regular file: ${resolved}`);
27
29
  process.exit(1);
28
30
  }
29
31
  if (st.size > max) {
30
32
  console.error(`Error: ${label} file too large (${st.size} B > ${max} B). Raise PLAYCRAFT_IMAGE_MAX_INPUT_BYTES if needed.`);
31
33
  process.exit(1);
32
34
  }
35
+ return resolved;
33
36
  }
34
37
  function ensureDir(filePath) {
35
- mkdirSync(dirname(filePath), { recursive: true });
38
+ mkdirSync(dirname(resolveSandboxPath(filePath)), { recursive: true });
36
39
  }
37
40
  function writeOutput(path, buf) {
38
- ensureDir(path);
39
- writeFileSync(path, buf);
41
+ const resolved = resolveSandboxPath(path);
42
+ ensureDir(resolved);
43
+ writeFileSync(resolved, buf);
40
44
  }
41
45
  function sizeLabelBytes(byteLength) {
42
46
  const kb = byteLength / 1024;
@@ -55,12 +59,14 @@ function handleError(err) {
55
59
  * safeToFile 自动检测同路径场景:先写临时文件,成功后原子 rename 覆盖。
56
60
  */
57
61
  async function safeToFile(pipeline, outputPath, inputPath) {
58
- ensureDir(outputPath);
59
- if (inputPath && resolve(inputPath) === resolve(outputPath)) {
60
- const tmpPath = outputPath + '.__tmp__';
62
+ const resolvedOutput = resolveSandboxPath(outputPath);
63
+ const resolvedInput = inputPath ? resolveSandboxPath(inputPath) : undefined;
64
+ ensureDir(resolvedOutput);
65
+ if (resolvedInput && resolve(resolvedInput) === resolve(resolvedOutput)) {
66
+ const tmpPath = resolvedOutput + '.__tmp__';
61
67
  try {
62
68
  await pipeline.toFile(tmpPath);
63
- renameSync(tmpPath, outputPath);
69
+ renameSync(tmpPath, resolvedOutput);
64
70
  }
65
71
  catch (e) {
66
72
  try {
@@ -74,7 +80,7 @@ async function safeToFile(pipeline, outputPath, inputPath) {
74
80
  }
75
81
  }
76
82
  else {
77
- await pipeline.toFile(outputPath);
83
+ await pipeline.toFile(resolvedOutput);
78
84
  }
79
85
  }
80
86
  /**
@@ -94,12 +100,13 @@ function globPattern(pattern) {
94
100
  * pattern 只做文件名匹配(如 "*.png",不支持子目录 **)。
95
101
  */
96
102
  function collectFromDir(dir, pattern = '*.png', sort = 'name') {
97
- if (!existsSync(dir))
103
+ const resolvedDir = resolveSandboxPath(dir);
104
+ if (!existsSync(resolvedDir))
98
105
  return [];
99
106
  const reg = globPattern(pattern);
100
- const files = readdirSync(dir)
101
- .filter((f) => reg.test(f) && statSync(join(dir, f)).isFile())
102
- .map((f) => join(dir, f));
107
+ const files = readdirSync(resolvedDir)
108
+ .filter((f) => reg.test(f) && statSync(join(resolvedDir, f)).isFile())
109
+ .map((f) => join(resolvedDir, f));
103
110
  if (sort === 'name') {
104
111
  files.sort((a, b) => basename(a).localeCompare(basename(b), undefined, { numeric: true, sensitivity: 'base' }));
105
112
  }
@@ -116,14 +123,15 @@ function expandInputPaths(raw) {
116
123
  const parts = raw.split(',').map((p) => p.trim()).filter(Boolean);
117
124
  const result = [];
118
125
  for (const part of parts) {
119
- if (part.includes('*') || part.includes('?')) {
120
- const dir = dirname(part);
121
- const pat = basename(part);
126
+ const resolvedPart = resolveSandboxPath(part);
127
+ if (resolvedPart.includes('*') || resolvedPart.includes('?')) {
128
+ const dir = dirname(resolvedPart);
129
+ const pat = basename(resolvedPart);
122
130
  const matched = collectFromDir(dir === '.' ? process.cwd() : dir, pat, 'name');
123
131
  result.push(...matched);
124
132
  }
125
133
  else {
126
- result.push(part);
134
+ result.push(resolvedPart);
127
135
  }
128
136
  }
129
137
  return result;
@@ -639,6 +647,8 @@ const VFX_PRESETS = [
639
647
  * 在 batch 管线中执行单个步骤(复用 sharp 逻辑,无需启动子进程)。
640
648
  */
641
649
  async function runBatchStep(cmd, args, inputPath, outputPath) {
650
+ inputPath = resolveSandboxPath(inputPath);
651
+ outputPath = resolveSandboxPath(outputPath);
642
652
  const getArg = (flag) => {
643
653
  const idx = args.indexOf(flag);
644
654
  return idx !== -1 ? args[idx + 1] : undefined;
@@ -767,7 +777,7 @@ export function registerImageCommands(program) {
767
777
  .option('--json', '以 JSON 格式输出(默认人类可读格式)')
768
778
  .action(async (opts) => {
769
779
  try {
770
- assertInputWithinLimit(opts.input);
780
+ opts.input = assertInputWithinLimit(opts.input);
771
781
  const meta = await sharp(opts.input).metadata();
772
782
  const fileStat = statSync(opts.input);
773
783
  const channels = meta.channels ?? 0;
@@ -807,7 +817,7 @@ export function registerImageCommands(program) {
807
817
  .option('--fit <mode>', 'contain|cover|fill|inside|outside', 'cover')
808
818
  .action(async (opts) => {
809
819
  try {
810
- assertInputWithinLimit(opts.input);
820
+ opts.input = assertInputWithinLimit(opts.input);
811
821
  const meta = await sharp(opts.input).metadata();
812
822
  const w = meta.width ?? 64;
813
823
  const h = meta.height ?? 64;
@@ -827,7 +837,7 @@ export function registerImageCommands(program) {
827
837
  }
828
838
  const fit = parseResizeFitOption(opts.fit);
829
839
  await safeToFile(sharp(opts.input).resize(targetW, targetH, { fit }), opts.output, opts.input);
830
- const outSz = statSync(opts.output).size;
840
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
831
841
  console.log(`Resized ${w}x${h} → ${targetW ?? '?'}x${targetH ?? '?'} — ${opts.output} (${sizeLabelBytes(outSz)})`);
832
842
  }
833
843
  catch (e) {
@@ -845,9 +855,9 @@ export function registerImageCommands(program) {
845
855
  .requiredOption('--height <n>', '裁剪高度(像素)', cliParseInt)
846
856
  .action(async (opts) => {
847
857
  try {
848
- assertInputWithinLimit(opts.input);
858
+ opts.input = assertInputWithinLimit(opts.input);
849
859
  await safeToFile(sharp(opts.input).extract({ left: opts.x, top: opts.y, width: opts.width, height: opts.height }), opts.output, opts.input);
850
- const outSz = statSync(opts.output).size;
860
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
851
861
  console.log(`Cropped to ${opts.width}x${opts.height} at (${opts.x},${opts.y}) — ${opts.output} (${sizeLabelBytes(outSz)})`);
852
862
  }
853
863
  catch (e) {
@@ -863,9 +873,9 @@ export function registerImageCommands(program) {
863
873
  .option('--background <color>', '旋转后露出的背景色(CSS hex,如 #000000)', '#00000000')
864
874
  .action(async (opts) => {
865
875
  try {
866
- assertInputWithinLimit(opts.input);
876
+ opts.input = assertInputWithinLimit(opts.input);
867
877
  await safeToFile(sharp(opts.input).rotate(opts.angle, { background: opts.background }), opts.output, opts.input);
868
- const outSz = statSync(opts.output).size;
878
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
869
879
  console.log(`Rotated ${opts.angle}° — ${opts.output} (${sizeLabelBytes(outSz)})`);
870
880
  }
871
881
  catch (e) {
@@ -880,7 +890,7 @@ export function registerImageCommands(program) {
880
890
  .option('--direction <dir>', 'horizontal(左右)或 vertical(上下)', 'horizontal')
881
891
  .action(async (opts) => {
882
892
  try {
883
- assertInputWithinLimit(opts.input);
893
+ opts.input = assertInputWithinLimit(opts.input);
884
894
  let pipeline = sharp(opts.input);
885
895
  if (opts.direction === 'horizontal') {
886
896
  pipeline = pipeline.flop();
@@ -893,7 +903,7 @@ export function registerImageCommands(program) {
893
903
  process.exit(1);
894
904
  }
895
905
  await safeToFile(pipeline, opts.output, opts.input);
896
- const outSz = statSync(opts.output).size;
906
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
897
907
  console.log(`Flipped ${opts.direction} — ${opts.output} (${sizeLabelBytes(outSz)})`);
898
908
  }
899
909
  catch (e) {
@@ -913,13 +923,13 @@ export function registerImageCommands(program) {
913
923
  .option('--background <color>', '背景色(CSS hex)', '#00000000')
914
924
  .action(async (opts) => {
915
925
  try {
916
- assertInputWithinLimit(opts.input);
926
+ opts.input = assertInputWithinLimit(opts.input);
917
927
  const pad = opts.all !== undefined
918
928
  ? { top: opts.all, bottom: opts.all, left: opts.all, right: opts.all }
919
929
  : { top: opts.top, bottom: opts.bottom, left: opts.left, right: opts.right };
920
930
  await safeToFile(sharp(opts.input).extend({ ...pad, background: opts.background }), opts.output, opts.input);
921
- const meta = await sharp(opts.output).metadata();
922
- const outSz = statSync(opts.output).size;
931
+ const meta = await sharp(resolveSandboxPath(opts.output)).metadata();
932
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
923
933
  console.log(`Padded to ${meta.width}x${meta.height} — ${opts.output} (${sizeLabelBytes(outSz)})`);
924
934
  }
925
935
  catch (e) {
@@ -936,7 +946,7 @@ export function registerImageCommands(program) {
936
946
  .option('--lossless', '无损压缩(webp/avif 有效)')
937
947
  .action(async (opts) => {
938
948
  try {
939
- assertInputWithinLimit(opts.input);
949
+ opts.input = assertInputWithinLimit(opts.input);
940
950
  const origSize = statSync(opts.input).size;
941
951
  const fmt = (opts.format ?? opts.output.split('.').pop() ?? 'png').toLowerCase();
942
952
  let pipeline = sharp(opts.input);
@@ -957,7 +967,7 @@ export function registerImageCommands(program) {
957
967
  break;
958
968
  }
959
969
  await safeToFile(pipeline, opts.output, opts.input);
960
- const outSz = statSync(opts.output).size;
970
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
961
971
  const ratio = origSize > 0 ? ((1 - outSz / origSize) * 100).toFixed(1) : '0.0';
962
972
  console.log(`Converted to ${fmt} — ${opts.output} (${sizeLabelBytes(outSz)}, ${ratio}% reduction)`);
963
973
  }
@@ -972,9 +982,9 @@ export function registerImageCommands(program) {
972
982
  .requiredOption('--output <path>', '输出图片路径')
973
983
  .action(async (opts) => {
974
984
  try {
975
- assertInputWithinLimit(opts.input);
985
+ opts.input = assertInputWithinLimit(opts.input);
976
986
  await safeToFile(sharp(opts.input).grayscale(), opts.output, opts.input);
977
- const outSz = statSync(opts.output).size;
987
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
978
988
  console.log(`Grayscale — ${opts.output} (${sizeLabelBytes(outSz)})`);
979
989
  }
980
990
  catch (e) {
@@ -989,9 +999,9 @@ export function registerImageCommands(program) {
989
999
  .option('--sigma <n>', '模糊强度(0.3–1000,越大越模糊)', parseFloat, 3)
990
1000
  .action(async (opts) => {
991
1001
  try {
992
- assertInputWithinLimit(opts.input);
1002
+ opts.input = assertInputWithinLimit(opts.input);
993
1003
  await safeToFile(sharp(opts.input).blur(opts.sigma), opts.output, opts.input);
994
- const outSz = statSync(opts.output).size;
1004
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
995
1005
  console.log(`Blurred (sigma=${opts.sigma}) — ${opts.output} (${sizeLabelBytes(outSz)})`);
996
1006
  }
997
1007
  catch (e) {
@@ -1006,9 +1016,9 @@ export function registerImageCommands(program) {
1006
1016
  .requiredOption('--color <hex>', '叠加颜色(CSS hex,如 #ff0000)')
1007
1017
  .action(async (opts) => {
1008
1018
  try {
1009
- assertInputWithinLimit(opts.input);
1019
+ opts.input = assertInputWithinLimit(opts.input);
1010
1020
  await safeToFile(sharp(opts.input).tint(opts.color), opts.output, opts.input);
1011
- const outSz = statSync(opts.output).size;
1021
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
1012
1022
  console.log(`Tinted with ${opts.color} — ${opts.output} (${sizeLabelBytes(outSz)})`);
1013
1023
  }
1014
1024
  catch (e) {
@@ -1023,11 +1033,11 @@ export function registerImageCommands(program) {
1023
1033
  .option('--threshold <n>', '颜色容差(0-255,默认 10)', cliParseInt, 10)
1024
1034
  .action(async (opts) => {
1025
1035
  try {
1026
- assertInputWithinLimit(opts.input);
1036
+ opts.input = assertInputWithinLimit(opts.input);
1027
1037
  const before = await sharp(opts.input).metadata();
1028
1038
  await safeToFile(sharp(opts.input).trim({ threshold: opts.threshold }), opts.output, opts.input);
1029
- const after = await sharp(opts.output).metadata();
1030
- const outSz = statSync(opts.output).size;
1039
+ const after = await sharp(resolveSandboxPath(opts.output)).metadata();
1040
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
1031
1041
  console.log(`Trimmed ${before.width}x${before.height} → ${after.width}x${after.height} — ${opts.output} (${sizeLabelBytes(outSz)})`);
1032
1042
  }
1033
1043
  catch (e) {
@@ -1042,9 +1052,9 @@ export function registerImageCommands(program) {
1042
1052
  .option('--no-alpha', '不反转透明通道')
1043
1053
  .action(async (opts) => {
1044
1054
  try {
1045
- assertInputWithinLimit(opts.input);
1055
+ opts.input = assertInputWithinLimit(opts.input);
1046
1056
  await safeToFile(sharp(opts.input).negate({ alpha: opts.alpha !== false }), opts.output, opts.input);
1047
- const outSz = statSync(opts.output).size;
1057
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
1048
1058
  console.log(`Negated — ${opts.output} (${sizeLabelBytes(outSz)})`);
1049
1059
  }
1050
1060
  catch (e) {
@@ -1062,8 +1072,8 @@ export function registerImageCommands(program) {
1062
1072
  .option('--gravity <g>', '对齐方向(center|north|south|east|west...,--x/--y 优先)', 'center')
1063
1073
  .action(async (opts) => {
1064
1074
  try {
1065
- assertInputWithinLimit(opts.base, 'base');
1066
- assertInputWithinLimit(opts.overlay, 'overlay');
1075
+ opts.base = assertInputWithinLimit(opts.base, 'base');
1076
+ opts.overlay = assertInputWithinLimit(opts.overlay, 'overlay');
1067
1077
  const overlayOpts = { input: opts.overlay };
1068
1078
  if (opts.x !== undefined && opts.y !== undefined) {
1069
1079
  overlayOpts.left = opts.x;
@@ -1073,8 +1083,8 @@ export function registerImageCommands(program) {
1073
1083
  overlayOpts.gravity = opts.gravity;
1074
1084
  }
1075
1085
  await safeToFile(sharp(opts.base).composite([overlayOpts]), opts.output, opts.base);
1076
- const meta = await sharp(opts.output).metadata();
1077
- const outSz = statSync(opts.output).size;
1086
+ const meta = await sharp(resolveSandboxPath(opts.output)).metadata();
1087
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
1078
1088
  console.log(`Composited — ${meta.width}x${meta.height} — ${opts.output} (${sizeLabelBytes(outSz)})`);
1079
1089
  }
1080
1090
  catch (e) {
@@ -1089,14 +1099,14 @@ export function registerImageCommands(program) {
1089
1099
  .option('--pixel-size <n>', '像素块大小(默认 8)', cliParseInt, 8)
1090
1100
  .action(async (opts) => {
1091
1101
  try {
1092
- assertInputWithinLimit(opts.input);
1102
+ opts.input = assertInputWithinLimit(opts.input);
1093
1103
  const meta = await sharp(opts.input).metadata();
1094
1104
  const w = meta.width ?? 64;
1095
1105
  const h = meta.height ?? 64;
1096
1106
  const smallW = Math.max(1, Math.floor(w / opts.pixelSize));
1097
1107
  const smallH = Math.max(1, Math.floor(h / opts.pixelSize));
1098
1108
  await safeToFile(sharp(opts.input).resize(smallW, smallH, { kernel: 'nearest' }).resize(w, h, { kernel: 'nearest' }), opts.output, opts.input);
1099
- const outSz = statSync(opts.output).size;
1109
+ const outSz = statSync(resolveSandboxPath(opts.output)).size;
1100
1110
  console.log(`Pixelated (block=${opts.pixelSize}px) — ${opts.output} (${sizeLabelBytes(outSz)})`);
1101
1111
  }
1102
1112
  catch (e) {
@@ -1118,7 +1128,7 @@ export function registerImageCommands(program) {
1118
1128
  .option('--padding <n>', '帧间距(像素,默认 0)', cliParseInt, 0)
1119
1129
  .action(async (opts) => {
1120
1130
  try {
1121
- assertInputWithinLimit(opts.input);
1131
+ opts.input = assertInputWithinLimit(opts.input);
1122
1132
  let analysis;
1123
1133
  if (opts.autoDetect) {
1124
1134
  const imageBase64 = readFileSync(opts.input).toString('base64');
@@ -1185,7 +1195,7 @@ export function registerImageCommands(program) {
1185
1195
  try {
1186
1196
  let paths;
1187
1197
  if (opts.inputDir) {
1188
- paths = collectFromDir(opts.inputDir, opts.glob ?? '*.png', opts.sort ?? 'name');
1198
+ paths = collectFromDir(resolveSandboxPath(opts.inputDir), opts.glob ?? '*.png', opts.sort ?? 'name');
1189
1199
  }
1190
1200
  else if (opts.inputs) {
1191
1201
  paths = expandInputPaths(opts.inputs);
@@ -1199,7 +1209,7 @@ export function registerImageCommands(program) {
1199
1209
  process.exit(1);
1200
1210
  }
1201
1211
  for (let i = 0; i < paths.length; i++) {
1202
- assertInputWithinLimit(paths[i], `inputs[${i}]`);
1212
+ paths[i] = assertInputWithinLimit(paths[i], `inputs[${i}]`);
1203
1213
  }
1204
1214
  const firstMeta = await sharp(paths[0]).metadata();
1205
1215
  const cellW = opts.cellWidth ?? firstMeta.width ?? 64;
@@ -1290,7 +1300,7 @@ export function registerImageCommands(program) {
1290
1300
  const globPatterns = (opts.glob ?? '*.png,*.jpg,*.jpeg,*.webp').split(',').map((p) => p.trim());
1291
1301
  const allFiles = [];
1292
1302
  for (const pat of globPatterns) {
1293
- const matched = collectFromDir(opts.inputDir, pat, 'name');
1303
+ const matched = collectFromDir(resolveSandboxPath(opts.inputDir), pat, 'name');
1294
1304
  allFiles.push(...matched);
1295
1305
  }
1296
1306
  inputs = [...new Set(allFiles)];
@@ -1350,13 +1360,13 @@ export function registerImageCommands(program) {
1350
1360
  console.error('Error: --steps is empty or invalid');
1351
1361
  process.exit(1);
1352
1362
  }
1353
- mkdirSync(opts.outputDir, { recursive: true });
1363
+ mkdirSync(resolveSandboxPath(opts.outputDir), { recursive: true });
1354
1364
  let done = 0, skipped = 0, failed = 0;
1355
1365
  const total = inputs.length;
1356
1366
  const concurrency = Math.max(1, opts.parallel);
1357
1367
  const processOne = async (inputPath) => {
1358
1368
  const outName = basename(inputPath).replace(/\.[^.]+$/, '.png');
1359
- const outPath = join(opts.outputDir, outName);
1369
+ const outPath = join(resolveSandboxPath(opts.outputDir), outName);
1360
1370
  if (opts.skipExisting && existsSync(outPath)) {
1361
1371
  skipped++;
1362
1372
  process.stdout.write(`\r[${done + skipped + failed}/${total}] skipping ${outName} (exists)`);
@@ -1449,7 +1459,7 @@ export function registerImageCommands(program) {
1449
1459
  .option('--dry-run', '只打印将执行的操作,不实际处理')
1450
1460
  .action(async (opts) => {
1451
1461
  try {
1452
- const configRaw = readFileSync(opts.config, 'utf-8');
1462
+ const configRaw = readFileSync(resolveSandboxPath(opts.config), 'utf-8');
1453
1463
  const ext = extname(opts.config).toLowerCase();
1454
1464
  let config;
1455
1465
  if (ext === '.json') {
@@ -1617,7 +1627,7 @@ export function registerImageCommands(program) {
1617
1627
  .option('--no-refine', '[ai] 跳过 alpha 后处理(保留模型原始软边)')
1618
1628
  .action(async (opts) => {
1619
1629
  try {
1620
- assertInputWithinLimit(opts.input);
1630
+ opts.input = assertInputWithinLimit(opts.input);
1621
1631
  const method = opts.method ?? 'floodfill';
1622
1632
  if (method === 'floodfill') {
1623
1633
  const t0 = Date.now();
@@ -1689,7 +1699,7 @@ export function registerImageCommands(program) {
1689
1699
  console.error('Error: --input and --output are required');
1690
1700
  process.exit(1);
1691
1701
  }
1692
- if (!existsSync(opts.input)) {
1702
+ if (!existsSync(resolveSandboxPath(opts.input))) {
1693
1703
  console.error(`Error: input file not found: ${opts.input}`);
1694
1704
  process.exit(1);
1695
1705
  }
@@ -1706,11 +1716,11 @@ export function registerImageCommands(program) {
1706
1716
  console.error('Error: --preset flip requires --back <backside-image>');
1707
1717
  process.exit(1);
1708
1718
  }
1709
- if (!existsSync(opts.back)) {
1719
+ if (!existsSync(resolveSandboxPath(opts.back))) {
1710
1720
  console.error(`Error: back image not found: ${opts.back}`);
1711
1721
  process.exit(1);
1712
1722
  }
1713
- await buildFlipSpriteSheet(opts.input, opts.back, frames, columns, opts.output);
1723
+ await buildFlipSpriteSheet(resolveSandboxPath(opts.input), resolveSandboxPath(opts.back), frames, columns, resolveSandboxPath(opts.output));
1714
1724
  }
1715
1725
  else {
1716
1726
  const preset = opts.preset;
@@ -1718,10 +1728,10 @@ export function registerImageCommands(program) {
1718
1728
  console.error(`Error: unknown preset '${preset}'. Available: ${Object.keys(ANIMATE_PRESETS).join(', ')}, flip`);
1719
1729
  process.exit(1);
1720
1730
  }
1721
- await buildAnimationSpriteSheet(opts.input, preset, frames, columns, opts.output);
1731
+ await buildAnimationSpriteSheet(resolveSandboxPath(opts.input), preset, frames, columns, resolveSandboxPath(opts.output));
1722
1732
  }
1723
1733
  const elapsed = ((Date.now() - t0) / 1000).toFixed(2);
1724
- const outBuf = readFileSync(opts.output);
1734
+ const outBuf = readFileSync(resolveSandboxPath(opts.output));
1725
1735
  const meta = await sharp(opts.input).metadata();
1726
1736
  console.log(`Animation generated: ${opts.output} (${sizeLabel(outBuf)}, ${elapsed}s)`);
1727
1737
  console.log(` Preset: ${opts.back ? 'flip' : opts.preset} | Frames: ${frames} | Layout: ${columns}×${Math.ceil(frames / columns)} | Frame size: ${meta.width}×${meta.height}`);
@@ -1775,12 +1785,12 @@ export function registerImageCommands(program) {
1775
1785
  process.exit(1);
1776
1786
  }
1777
1787
  const scale = parseFloat(opts.scale);
1778
- const outDir = dirname(resolve(opts.output));
1788
+ const outDir = dirname(resolve(resolveSandboxPath(opts.output)));
1779
1789
  if (!existsSync(outDir))
1780
1790
  mkdirSync(outDir, { recursive: true });
1781
1791
  if (!opts.tint && scale === 1.0) {
1782
- copyFileSync(srcPath, opts.output);
1783
- const sz = sizeLabel(readFileSync(opts.output));
1792
+ copyFileSync(srcPath, resolveSandboxPath(opts.output));
1793
+ const sz = sizeLabel(readFileSync(resolveSandboxPath(opts.output)));
1784
1794
  console.log(`VFX copied: ${opts.output} (${sz})`);
1785
1795
  console.log(` ${preset.frames} frames, ${preset.columns} cols — ${preset.description}`);
1786
1796
  return;
@@ -1832,7 +1842,7 @@ export function registerImageCommands(program) {
1832
1842
  const style = opts.style;
1833
1843
  const buf = await generateTileBack(W, H, bg, border, style);
1834
1844
  ensureDir(opts.output);
1835
- writeFileSync(opts.output, buf);
1845
+ writeFileSync(resolveSandboxPath(opts.output), buf);
1836
1846
  console.log(`Tile back generated: ${opts.output} (${sizeLabel(buf)}, ${W}×${H}, style: ${style})`);
1837
1847
  }
1838
1848
  catch (e) {
@@ -1873,8 +1883,8 @@ export function registerImageCommands(program) {
1873
1883
  console.log(`Using image URL: ${input}`);
1874
1884
  }
1875
1885
  else {
1876
- assertInputWithinLimit(input);
1877
- payload.imageBase64 = readFileSync(input).toString('base64');
1886
+ const resolvedDecomposeInput = assertInputWithinLimit(input);
1887
+ payload.imageBase64 = readFileSync(resolvedDecomposeInput).toString('base64');
1878
1888
  console.log(`Uploading local image: ${input}`);
1879
1889
  }
1880
1890
  console.log(`Decomposing into ${payload.numLayers} layer(s) via 302 Qwen-Image-Layered (may take 1–2 min)…`);
@@ -1938,7 +1948,7 @@ export function registerImageCommands(program) {
1938
1948
  .option('--output-dir <dir>', '输出目录(默认 ./segment-output)', './segment-output')
1939
1949
  .action(async (opts) => {
1940
1950
  try {
1941
- assertInputWithinLimit(opts.input);
1951
+ opts.input = assertInputWithinLimit(opts.input);
1942
1952
  const imageBase64 = readFileSync(opts.input).toString('base64');
1943
1953
  // 构造 prompts 列表
1944
1954
  let prompts;
@@ -0,0 +1,29 @@
1
+ import { fileURLToPath, pathToFileURL } from 'node:url';
2
+ import { dirname, join } from 'node:path';
3
+ const cliRoot = dirname(fileURLToPath(import.meta.url));
4
+ async function loadValidator() {
5
+ const scriptPath = join(cliRoot, '../../project-template/skills/_shared/scripts/validate-level-sketch.mjs');
6
+ return import(pathToFileURL(scriptPath).href);
7
+ }
8
+ export function registerLevelsCommands(program) {
9
+ const levels = program.command('levels').description('Level sketch validation (executable design)');
10
+ levels
11
+ .command('validate')
12
+ .description('Validate docs/level-sketch.json — solvability + puzzle identity metrics')
13
+ .option('--sketch <path>', 'Path to level sketch JSON', 'docs/level-sketch.json')
14
+ .option('--json', 'Output JSON')
15
+ .action(async (opts) => {
16
+ const { validateLevelSketch } = await loadValidator();
17
+ const result = validateLevelSketch(process.cwd(), opts.sketch);
18
+ if (opts.json) {
19
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
20
+ }
21
+ else if (result.ok) {
22
+ process.stdout.write(`[levels validate] OK — paths=${result.metrics.pathCount} openingRemovable=${result.metrics.openingRemovableCount}\n`);
23
+ }
24
+ else {
25
+ process.stderr.write(`[levels validate] FAIL\n - ${result.problems.join('\n - ')}\n`);
26
+ }
27
+ process.exit(result.ok ? 0 : 2);
28
+ });
29
+ }
@@ -0,0 +1,75 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { expandIntentTags } from './skills.js';
5
+ function loadCatalog() {
6
+ const cliRoot = dirname(fileURLToPath(import.meta.url));
7
+ const catalogPath = join(cliRoot, '../../project-template/fusion-patterns/catalog.json');
8
+ if (!existsSync(catalogPath)) {
9
+ throw new Error(`fusion pattern catalog not found: ${catalogPath}`);
10
+ }
11
+ return JSON.parse(readFileSync(catalogPath, 'utf8'));
12
+ }
13
+ function scorePattern(pattern, intentTags) {
14
+ const expanded = expandIntentTags(intentTags);
15
+ let score = 0;
16
+ for (const tag of expanded) {
17
+ if (pattern.intentTags.some((t) => t.toLowerCase() === tag.toLowerCase()))
18
+ score += 3;
19
+ if (pattern.title.toLowerCase().includes(tag.toLowerCase()))
20
+ score += 1;
21
+ if (pattern.mechanics.toLowerCase().includes(tag.toLowerCase()))
22
+ score += 1;
23
+ }
24
+ const blob = intentTags.join(' ').toLowerCase();
25
+ if (pattern.intentTags.some((t) => blob.includes(t.toLowerCase())))
26
+ score += 2;
27
+ return score;
28
+ }
29
+ export function matchFusionPatterns(intent, limit = 3) {
30
+ const intentTags = intent
31
+ .split(/[,\s]+/)
32
+ .map((t) => t.trim())
33
+ .filter(Boolean);
34
+ const catalog = loadCatalog();
35
+ return catalog.patterns
36
+ .map((p) => ({ pattern: p, score: scorePattern(p, intentTags) }))
37
+ .filter(({ score }) => score > 0)
38
+ .sort((a, b) => b.score - a.score)
39
+ .slice(0, limit)
40
+ .map(({ pattern }) => pattern);
41
+ }
42
+ export function registerPatternsCommands(program) {
43
+ const patterns = program.command('patterns').description('Fusion pattern library (local RAG before web research)');
44
+ patterns
45
+ .command('match')
46
+ .description('Match fusion pattern cards from local catalog')
47
+ .requiredOption('--intent <text>', 'User intent keywords')
48
+ .option('--json', 'Output JSON')
49
+ .option('--limit <n>', 'Max patterns', '3')
50
+ .action((opts) => {
51
+ const items = matchFusionPatterns(opts.intent, parseInt(opts.limit, 10) || 3);
52
+ if (opts.json) {
53
+ process.stdout.write(`${JSON.stringify({ intent: opts.intent, items }, null, 2)}\n`);
54
+ return;
55
+ }
56
+ if (items.length === 0) {
57
+ console.log('No fusion patterns matched — fallback to playcraft tools research --focus gameplay');
58
+ return;
59
+ }
60
+ console.log(`\nFusion patterns (${items.length}):\n`);
61
+ for (const p of items) {
62
+ console.log(` [${p.id}] ${p.title}`);
63
+ console.log(` skill: ${p.skillRef}`);
64
+ console.log(` fantasy: ${p.playerFantasy}`);
65
+ console.log(` decision: ${p.decisionMoment}`);
66
+ if (p.recommendedProfile)
67
+ console.log(` profile: ${p.recommendedProfile}`);
68
+ if (p.depthHint)
69
+ console.log(` depthHint: ${p.depthHint}`);
70
+ if (p.antiPatternRef)
71
+ console.log(` avoid: ${p.antiPatternRef}`);
72
+ console.log();
73
+ }
74
+ });
75
+ }
@@ -5,6 +5,7 @@ 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';
8
9
  // Register autocomplete prompt
9
10
  inquirer.registerPrompt('autocomplete', autocomplete);
10
11
  const JSON_OUTPUT_HELP = '输出一条 JSON (JavaScript Object Notation, JavaScript 对象表示法) 结果';
@@ -1266,4 +1267,50 @@ export function registerRemixCommands(program) {
1266
1267
  .option('--token <token>', PAT_HELP)
1267
1268
  .option('--json', JSON_OUTPUT_HELP)
1268
1269
  .action((opts) => remixRun({ ...opts, images: opts.image, timeout: Math.max(1, Number(opts.timeout) || 1800) }));
1270
+ remix.command('clone')
1271
+ .description('经 PlayCraft Git 代理克隆 Remix 源码到本地(支持粘贴详情页 / Editor / 工蜂 URL)')
1272
+ .argument('[source]', 'Remix 详情页、Editor 地址,或工蜂/Git 分支页 URL')
1273
+ .argument('[dir]', '本地目标目录(默认 <projectId>-<branch>)')
1274
+ .option('--project-id <id>', '项目 ID(未传 URL 时必填)')
1275
+ .option('--branch <name>', 'Remix 分支名(未传 URL 或工蜂地址无 /tree 时必填)')
1276
+ .option('--install', 'clone 后执行 npm install')
1277
+ .option('--dev', 'install 后执行 npm run dev')
1278
+ .option('--url <url>', SERVICE_ADDRESS_HELP)
1279
+ .option('--token <token>', PAT_HELP)
1280
+ .option('--json', JSON_OUTPUT_HELP)
1281
+ .action(async (sourceArg, dirArg, opts) => {
1282
+ try {
1283
+ let source = sourceArg ? String(sourceArg) : undefined;
1284
+ let dir = dirArg ? String(dirArg) : undefined;
1285
+ // `clone ./outdir --project-id … --branch …` → first arg is dir
1286
+ if (source && !isRemoteCloneSource(source)) {
1287
+ dir = source;
1288
+ source = undefined;
1289
+ }
1290
+ await remixClone({
1291
+ source,
1292
+ dir,
1293
+ projectId: opts.projectId ? String(opts.projectId) : undefined,
1294
+ branch: opts.branch ? String(opts.branch) : undefined,
1295
+ install: Boolean(opts.install || opts.dev),
1296
+ dev: Boolean(opts.dev),
1297
+ url: opts.url,
1298
+ token: opts.token,
1299
+ json: Boolean(opts.json),
1300
+ });
1301
+ }
1302
+ catch (error) {
1303
+ handleCommandError('remix.clone', opts, undefined, error);
1304
+ }
1305
+ });
1306
+ }
1307
+ /** Register `playcraft git-credential` for local git pull/push after remix clone. */
1308
+ export function registerGitCredentialCommand(program) {
1309
+ program
1310
+ .command('git-credential')
1311
+ .description('Git credential helper:从登录配置提供 PlayCraft PAT')
1312
+ .argument('[action]', 'get | store | erase', 'get')
1313
+ .action((action) => {
1314
+ gitCredentialCommand(action);
1315
+ });
1269
1316
  }