@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.
- package/README.md +21 -0
- package/REMIX.md +103 -19
- package/dist/atom-tree/fusion-hints.js +8 -3
- package/dist/atom-tree/fusion-intent.js +62 -0
- package/dist/atom-tree/selection.js +3 -1
- package/dist/commands/create.js +299 -85
- package/dist/commands/image.js +79 -69
- package/dist/commands/levels.js +29 -0
- package/dist/commands/patterns.js +75 -0
- package/dist/commands/remix.js +47 -0
- package/dist/commands/skills.js +48 -17
- package/dist/index.js +9 -5
- package/dist/remix/clone-source.js +208 -0
- package/dist/remix/clone.js +255 -0
- package/dist/scaffold/templates.js +1 -1
- package/dist/utils/agent-api-client.js +10 -3
- package/dist/utils/remix-image-attachments.js +4 -32
- package/dist/utils/sandbox-path.js +20 -0
- package/package.json +3 -3
- package/project-template/.claude/agents/creative-director.md +114 -0
- package/project-template/.claude/agents/developer.md +5 -2
- package/project-template/.claude/agents/game-designer.md +41 -20
- package/project-template/.claude/agents/refs/README.md +8 -0
- package/project-template/.claude/agents/refs/creative-director-guide.md +35 -0
- package/project-template/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
- package/project-template/.claude/agents/refs/creative-discovery-guide.md +76 -0
- package/project-template/.claude/agents/refs/creative-north-star-examples.md +82 -0
- package/project-template/.claude/agents/refs/developer-dev-handoff.md +1 -1
- package/project-template/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
- package/project-template/.claude/agents/refs/game-designer-gameplay-sufficiency.md +4 -4
- package/project-template/.claude/agents/reviewer.md +11 -0
- package/project-template/.claude/hooks/README.md +12 -12
- package/project-template/.claude/hooks/block-developer-preflight.mjs +49 -13
- package/project-template/.claude/hooks/block-ring1-tasklist.mjs +1 -1
- package/project-template/.claude/hooks/user-prompt.mjs +3 -13
- package/project-template/.claude/hooks/validate-workflow-stop.mjs +209 -12
- package/project-template/.claude/settings.json +2 -2
- package/project-template/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
- package/project-template/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -1
- package/project-template/.claude/snapshot.mjs +0 -1
- package/project-template/.codebuddy/settings.json +1 -1
- package/project-template/.cursor/rules/playcraft-orchestrator.mdc +1 -1
- package/project-template/CLAUDE.md +9 -9
- package/project-template/docs/project-state.md +1 -1
- package/project-template/docs/team/agent-conduct.md +18 -18
- package/project-template/docs/team/collaboration.md +19 -7
- package/project-template/docs/team/workflow-changelog.md +15 -0
- package/project-template/fusion-patterns/catalog.json +77 -0
- package/project-template/package.json +2 -0
- package/project-template/skills/_shared/scripts/create-playable.mjs +31 -11
- package/project-template/skills/_shared/scripts/dev-start.mjs +3 -3
- package/project-template/skills/_shared/scripts/disclosure-config.mjs +65 -39
- package/project-template/skills/_shared/scripts/harness-smoke.mjs +3 -1
- package/project-template/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
- package/project-template/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
- package/project-template/skills/_shared/scripts/slice-set.mjs +29 -2
- package/project-template/skills/_shared/scripts/state-get.mjs +124 -7
- package/project-template/skills/_shared/scripts/state-store.mjs +25 -2
- package/project-template/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
- package/project-template/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
- package/project-template/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
- package/project-template/skills/playable-record/scripts/record-playable.mjs +2 -1
- package/project-template/skills/playable-record/scripts/verify-contract.mjs +4 -4
- package/project-template/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
- package/project-template/skills/playable-record/scripts/verify-fusion.mjs +37 -2
- package/project-template/skills/playable-record/scripts/verify-slice.mjs +2 -2
- package/project-template/templates/design-brief.template.md +34 -0
- package/project-template/templates/design-methodology.md +1 -2
- package/project-template/templates/design.template.md +65 -0
- package/project-template/templates/developer-log.template.md +12 -1
- package/project-template/templates/five-axis-framework.md +12 -0
- package/project-template/templates/game-designer-creative-process.template.md +99 -0
- package/project-template/templates/level-sketch.template.json +56 -0
- package/project-template/templates/north-star.template.md +43 -0
- package/project-template/templates/project-state.template.md +1 -1
- package/project-template/vite.config.ts +4 -5
- package/project-templates/phaser/.claude/agents/creative-director.md +114 -0
- package/project-templates/phaser/.claude/agents/game-designer.md +41 -20
- package/project-templates/phaser/.claude/agents/refs/creative-director-guide.md +35 -0
- package/project-templates/phaser/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
- package/project-templates/phaser/.claude/agents/refs/creative-discovery-guide.md +76 -0
- package/project-templates/phaser/.claude/agents/refs/creative-north-star-examples.md +82 -0
- package/project-templates/phaser/.claude/agents/refs/developer-dev-handoff.md +1 -1
- package/project-templates/phaser/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
- package/project-templates/phaser/.claude/agents/reviewer.md +11 -0
- package/project-templates/phaser/.claude/hooks/block-ring1-tasklist.mjs +2 -2
- package/project-templates/phaser/.claude/hooks/user-prompt.mjs +17 -18
- package/project-templates/phaser/.claude/hooks/validate-workflow-stop.mjs +209 -12
- package/project-templates/phaser/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
- package/project-templates/phaser/.cursor/rules/playcraft-orchestrator.mdc +1 -1
- package/project-templates/phaser/CLAUDE.md +88 -54
- package/project-templates/phaser/docs/DESIGN.md +25 -0
- package/project-templates/phaser/docs/project-state.md +1 -1
- package/project-templates/phaser/docs/team/collaboration.md +19 -7
- package/project-templates/phaser/docs/team/workflow-changelog.md +14 -0
- package/project-templates/phaser/fusion-patterns/catalog.json +77 -0
- package/project-templates/phaser/package.json +3 -1
- package/project-templates/phaser/skills/_shared/scripts/dev-start.mjs +3 -3
- package/project-templates/phaser/skills/_shared/scripts/disclosure-config.mjs +65 -39
- package/project-templates/phaser/skills/_shared/scripts/harness-smoke.mjs +3 -1
- package/project-templates/phaser/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
- package/project-templates/phaser/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
- package/project-templates/phaser/skills/_shared/scripts/state-get.mjs +124 -7
- package/project-templates/phaser/skills/_shared/scripts/state-store.mjs +25 -2
- package/project-templates/phaser/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
- package/project-templates/phaser/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
- package/project-templates/phaser/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
- package/project-templates/phaser/skills/playable-record/scripts/record-playable.mjs +2 -1
- package/project-templates/phaser/skills/playable-record/scripts/verify-contract.mjs +4 -4
- package/project-templates/phaser/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
- package/project-templates/phaser/skills/playable-record/scripts/verify-fusion.mjs +37 -2
- package/project-templates/phaser/skills/playable-record/scripts/verify-slice.mjs +2 -2
- package/project-templates/phaser/templates/design-brief.template.md +42 -11
- package/project-templates/phaser/templates/design-methodology.md +1 -2
- package/project-templates/phaser/templates/design.template.md +77 -0
- package/project-templates/phaser/templates/five-axis-framework.md +50 -1
- package/project-templates/phaser/templates/game-designer-creative-process.template.md +99 -0
- package/project-templates/phaser/templates/level-sketch.template.json +56 -0
- package/project-templates/phaser/templates/north-star.template.md +43 -0
- package/project-templates/phaser/templates/project-state.template.md +1 -1
- package/project-templates/phaser/vite.config.ts +4 -5
- package/project-templates/threejs/.claude/agents/creative-director.md +114 -0
- package/project-templates/threejs/.claude/agents/game-designer.md +41 -20
- package/project-templates/threejs/.claude/agents/refs/creative-director-guide.md +35 -0
- package/project-templates/threejs/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
- package/project-templates/threejs/.claude/agents/refs/creative-discovery-guide.md +76 -0
- package/project-templates/threejs/.claude/agents/refs/creative-north-star-examples.md +82 -0
- package/project-templates/threejs/.claude/agents/refs/developer-dev-handoff.md +1 -1
- package/project-templates/threejs/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
- package/project-templates/threejs/.claude/agents/reviewer.md +11 -0
- package/project-templates/threejs/.claude/hooks/block-ring1-tasklist.mjs +2 -2
- package/project-templates/threejs/.claude/hooks/user-prompt.mjs +17 -18
- package/project-templates/threejs/.claude/hooks/validate-workflow-stop.mjs +209 -12
- package/project-templates/threejs/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
- package/project-templates/threejs/.cursor/rules/playcraft-orchestrator.mdc +1 -1
- package/project-templates/threejs/CLAUDE.md +88 -54
- package/project-templates/threejs/docs/DESIGN.md +25 -0
- package/project-templates/threejs/docs/project-state.md +1 -1
- package/project-templates/threejs/docs/team/collaboration.md +19 -7
- package/project-templates/threejs/docs/team/workflow-changelog.md +14 -0
- package/project-templates/threejs/fusion-patterns/catalog.json +77 -0
- package/project-templates/threejs/package.json +3 -1
- package/project-templates/threejs/skills/_shared/scripts/dev-start.mjs +3 -3
- package/project-templates/threejs/skills/_shared/scripts/disclosure-config.mjs +65 -39
- package/project-templates/threejs/skills/_shared/scripts/harness-smoke.mjs +3 -1
- package/project-templates/threejs/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
- package/project-templates/threejs/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
- package/project-templates/threejs/skills/_shared/scripts/state-get.mjs +124 -7
- package/project-templates/threejs/skills/_shared/scripts/state-store.mjs +25 -2
- package/project-templates/threejs/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
- package/project-templates/threejs/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
- package/project-templates/threejs/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
- package/project-templates/threejs/skills/playable-record/scripts/record-playable.mjs +2 -1
- package/project-templates/threejs/skills/playable-record/scripts/verify-contract.mjs +4 -4
- package/project-templates/threejs/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
- package/project-templates/threejs/skills/playable-record/scripts/verify-fusion.mjs +37 -2
- package/project-templates/threejs/skills/playable-record/scripts/verify-slice.mjs +2 -2
- package/project-templates/threejs/templates/design-brief.template.md +42 -11
- package/project-templates/threejs/templates/design-methodology.md +1 -2
- package/project-templates/threejs/templates/design.template.md +77 -0
- package/project-templates/threejs/templates/five-axis-framework.md +50 -1
- package/project-templates/threejs/templates/game-designer-creative-process.template.md +99 -0
- package/project-templates/threejs/templates/level-sketch.template.json +56 -0
- package/project-templates/threejs/templates/north-star.template.md +43 -0
- package/project-templates/threejs/templates/project-state.template.md +1 -1
- package/project-templates/threejs/vite.config.ts +4 -5
- package/project-template/docs/DESIGN.md +0 -65
package/dist/commands/image.js
CHANGED
|
@@ -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(
|
|
26
|
+
const st = statSync(resolved);
|
|
25
27
|
if (!st.isFile()) {
|
|
26
|
-
console.error(`Error: ${label} is not a regular file: ${
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
103
|
+
const resolvedDir = resolveSandboxPath(dir);
|
|
104
|
+
if (!existsSync(resolvedDir))
|
|
98
105
|
return [];
|
|
99
106
|
const reg = globPattern(pattern);
|
|
100
|
-
const files = readdirSync(
|
|
101
|
-
.filter((f) => reg.test(f) && statSync(join(
|
|
102
|
-
.map((f) => join(
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/dist/commands/remix.js
CHANGED
|
@@ -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
|
}
|