@playcraft/cli 0.0.56 → 0.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -26
- package/REMIX.md +17 -5
- package/dist/cli-root-help.js +1 -0
- package/dist/commands/build-all.js +10 -9
- package/dist/commands/build.js +13 -12
- package/dist/commands/create.js +26 -33
- package/dist/commands/platform-skills.generated.js +19 -0
- package/dist/commands/remix.js +48 -2
- package/dist/commands/skills.js +2 -0
- package/dist/commands/tools-generation.js +1 -1
- package/dist/commands/workspace-runtime.js +54 -0
- package/dist/index.js +3 -1
- package/dist/project-skills/commands.js +72 -0
- package/dist/project-skills/lifecycle.js +28 -0
- package/dist/project-skills/local-run.js +112 -0
- package/dist/project-skills/local-store.js +149 -0
- package/dist/project-skills/messages.js +72 -0
- package/dist/project-skills/reconcile.js +62 -0
- package/dist/project-skills/remote-cache.js +51 -0
- package/dist/project-skills/validation.js +19 -0
- package/dist/remix/clone.js +4 -1
- package/dist/remix/init-template.js +2 -0
- package/dist/remix/local-git.js +56 -0
- package/dist/remix/pull.js +66 -0
- package/dist/remix/push.js +90 -0
- package/dist/utils/agent-api-client.js +54 -17
- package/dist/utils/tool-operation-journal.js +54 -0
- package/dist/workspace-runtime/codex/app-server.js +395 -0
- package/dist/workspace-runtime/codex/config-toml.js +25 -0
- package/dist/workspace-runtime/codex/jsonrpc-stdio.js +106 -0
- package/dist/workspace-runtime/codex/loopback.js +59 -0
- package/dist/workspace-runtime/codex/native-adapter.js +3 -0
- package/dist/workspace-runtime/main.js +10 -0
- package/dist/workspace-runtime/persistence/journal.js +350 -0
- package/dist/workspace-runtime/processes/managed-writes.js +69 -0
- package/dist/workspace-runtime/serve.js +111 -0
- package/dist/workspace-runtime/server/auth.js +21 -0
- package/dist/workspace-runtime/server/dispatch.js +861 -0
- package/dist/workspace-runtime/server/execution-group.js +88 -0
- package/dist/workspace-runtime/server/http.js +561 -0
- package/dist/workspace-runtime/server/lock.js +53 -0
- package/dist/workspace-runtime/server/types.js +1 -0
- package/dist/workspace-runtime/workspaces/context-error.js +2 -0
- package/dist/workspace-runtime/workspaces/file-snapshot.js +213 -0
- package/dist/workspace-runtime/workspaces/files.js +143 -0
- package/dist/workspace-runtime/workspaces/git.js +242 -0
- package/dist/workspace-runtime/workspaces/json5-edit.js +170 -0
- package/dist/workspace-runtime/workspaces/parameters.js +272 -0
- package/dist/workspace-runtime/workspaces/prepare.js +364 -0
- package/dist/workspace-runtime/workspaces/registry.js +107 -0
- package/dist/workspace-runtime/workspaces/revisions.js +31 -0
- package/package.json +6 -3
- package/project-template-v2/.claude/agents/artist.md +82 -0
- package/project-template-v2/.claude/agents/developer.md +153 -0
- package/project-template-v2/.claude/agents/game-designer.md +264 -0
- package/project-template-v2/.claude/agents/refs/artist-art-style-catalog.md +533 -0
- package/project-template-v2/.claude/agents/refs/artist-color-audio-recipes.md +153 -0
- package/project-template-v2/.claude/agents/refs/artist-dimension-axis.md +27 -0
- package/project-template-v2/.claude/agents/refs/artist-master-composite-recipes.md +208 -0
- package/project-template-v2/.claude/agents/refs/atom-skill-library.md +81 -0
- package/project-template-v2/.claude/agents/refs/developer-impl-cookbook.md +432 -0
- package/project-template-v2/.claude/agents/refs/framework-5-component-filter.md +252 -0
- package/project-template-v2/.claude/agents/refs/framework-game-feel-juice.md +266 -0
- package/project-template-v2/.claude/agents/refs/framework-mda.md +147 -0
- package/project-template-v2/.claude/agents/refs/game-designer-gameplay-sufficiency.md +123 -0
- package/project-template-v2/.claude/agents/refs/ta-3d-flip-recipe.md +88 -0
- package/project-template-v2/.claude/agents/refs/ta-atlas-deliverable-standard.md +67 -0
- package/project-template-v2/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
- package/project-template-v2/.claude/agents/refs/ta-image-generation-detail.md +300 -0
- package/project-template-v2/.claude/agents/refs/ta-image-ops-reference.md +495 -0
- package/project-template-v2/.claude/agents/refs/ta-pipeline-cookbook.md +1141 -0
- package/project-template-v2/.claude/agents/refs/ta-tools-reference.md +111 -0
- package/project-template-v2/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
- package/project-template-v2/.claude/agents/refs/threejs-cannon-pitfalls.md +412 -0
- package/project-template-v2/.claude/agents/reviewer.md +75 -0
- package/project-template-v2/.claude/agents/technical-artist.md +86 -0
- package/project-template-v2/.claude/hooks/snapshot-milestone.mjs +243 -0
- package/project-template-v2/.claude/hooks/user-prompt.mjs +133 -0
- package/project-template-v2/.claude/settings.json +33 -0
- package/project-template-v2/.claude/skills/brainstorming/SKILL.md +161 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/frame-template.html +270 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/helper.js +177 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/server.cjs +354 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/start-server.sh +148 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/stop-server.sh +56 -0
- package/project-template-v2/.claude/skills/brainstorming/scripts/wait-for-selection.sh +62 -0
- package/project-template-v2/.claude/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/project-template-v2/.claude/skills/brainstorming/visual-companion.md +309 -0
- package/project-template-v2/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
- package/project-template-v2/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
- package/project-template-v2/.claude/skills/playcraft-asset-state-sheet/SKILL.md +205 -0
- package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
- package/project-template-v2/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
- package/project-template-v2/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/SKILL.md +298 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +324 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
- package/project-template-v2/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
- package/project-template-v2/.claude/skills/playcraft-image-processing/SKILL.md +219 -0
- package/project-template-v2/.claude/skills/playcraft-masking/SKILL.md +373 -0
- package/project-template-v2/.claude/skills/playcraft-playable-optimization/SKILL.md +161 -0
- package/project-template-v2/.claude/skills/playcraft-research/SKILL.md +215 -0
- package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +382 -0
- package/project-template-v2/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
- package/project-template-v2/.claude/skills/playcraft-sprite-remix/SKILL.md +158 -0
- package/project-template-v2/.claude/skills/playcraft-sprite-sheet/SKILL.md +100 -0
- package/project-template-v2/.claude/skills/playcraft-storyboard/SKILL.md +167 -0
- package/project-template-v2/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
- package/project-template-v2/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
- package/project-template-v2/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
- package/project-template-v2/.claude/skills/playwright-cli/SKILL.md +390 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/running-code.md +240 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/session-management.md +226 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/spec-driven-testing.md +308 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/storage-state.md +275 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/test-generation.md +134 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/tracing.md +142 -0
- package/project-template-v2/.claude/skills/playwright-cli/references/video-recording.md +153 -0
- package/project-template-v2/.claude/skills/session-analyzer/SKILL.md +386 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/execution-breakdown.mjs +182 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/find-turns.mjs +72 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/heavy-output.mjs +121 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/resolve-session.mjs +102 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-stats.mjs +127 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-tool-timeline.mjs +106 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/time-gaps.mjs +128 -0
- package/project-template-v2/.claude/skills/session-analyzer/scripts/turn-timeline.mjs +67 -0
- package/project-template-v2/.claude/snapshot.mjs +263 -0
- package/project-template-v2/.playcraft/skills.lock.json +152 -0
- package/project-template-v2/CLAUDE.md +146 -0
- package/project-template-v2/assets/audio/bgm/.gitkeep +0 -0
- package/project-template-v2/assets/audio/sfx/.gitkeep +0 -0
- package/project-template-v2/assets/bundles/.gitkeep +0 -0
- package/project-template-v2/assets/images/bg/.gitkeep +0 -0
- package/project-template-v2/assets/images/reference/.gitkeep +0 -0
- package/project-template-v2/assets/images/storyboard/.gitkeep +0 -0
- package/project-template-v2/assets/images/tiles/.gitkeep +0 -0
- package/project-template-v2/assets/images/ui/.gitkeep +0 -0
- package/project-template-v2/assets/images/vfx/.gitkeep +0 -0
- package/project-template-v2/assets/models/.gitkeep +0 -0
- package/project-template-v2/docs/harness/iteration-1/context-flow.md +254 -0
- package/project-template-v2/docs/harness/iteration-1/generate-flow.md +91 -0
- package/project-template-v2/docs/harness/iteration-1/ideate-flow.md +214 -0
- package/project-template-v2/docs/harness/iteration-1/optimize-flow.md +75 -0
- package/project-template-v2/docs/harness/iteration-1/wrapup-flow.md +63 -0
- package/project-template-v2/docs/harness/iteration-2/context-flow.md +223 -0
- package/project-template-v2/docs/harness/iteration-2/generate-flow.md +129 -0
- package/project-template-v2/docs/harness/iteration-2/ideate-flow.md +267 -0
- package/project-template-v2/docs/harness/iteration-2/optimize-flow.md +164 -0
- package/project-template-v2/docs/harness/iteration-2/wrapup-flow.md +115 -0
- package/project-template-v2/docs/harness/orchestrator-flow.md +364 -0
- package/project-template-v2/docs/project-state.json +60 -0
- package/project-template-v2/docs/project-state.md +72 -0
- package/project-template-v2/docs/standards/README.md +225 -0
- package/project-template-v2/docs/standards/agent-behavior-standards.md +174 -0
- package/project-template-v2/docs/standards/artifacts/design-brief.md +19 -0
- package/project-template-v2/docs/standards/artifacts/design.md +22 -0
- package/project-template-v2/docs/standards/artifacts/game-code.md +41 -0
- package/project-template-v2/docs/standards/artifacts/todo-list.md +41 -0
- package/project-template-v2/docs/standards/iter1-agent-behavior-standards.md +343 -0
- package/project-template-v2/game/index.ts +18 -0
- package/project-template-v2/globals.d.ts +51 -0
- package/project-template-v2/index.css +34 -0
- package/project-template-v2/index.html +18 -0
- package/project-template-v2/main.ts +9 -0
- package/project-template-v2/package.json +46 -0
- package/project-template-v2/skills/_shared/scripts/dispatch-clear.mjs +31 -0
- package/project-template-v2/skills/_shared/scripts/dispatch-set.mjs +86 -0
- package/project-template-v2/skills/_shared/scripts/dod-check.mjs +153 -0
- package/project-template-v2/skills/_shared/scripts/handoff-append.mjs +70 -0
- package/project-template-v2/skills/_shared/scripts/lib/validator-artifacts.mjs +91 -0
- package/project-template-v2/skills/_shared/scripts/pipeline/dod-config.mjs +131 -0
- package/project-template-v2/skills/_shared/scripts/pipeline/index.mjs +80 -0
- package/project-template-v2/skills/_shared/scripts/pipeline/iteration-1-core.mjs +90 -0
- package/project-template-v2/skills/_shared/scripts/pipeline/iteration-2-wrap.mjs +93 -0
- package/project-template-v2/skills/_shared/scripts/pipeline/iteration-3-visual.mjs +23 -0
- package/project-template-v2/skills/_shared/scripts/render-project-state.mjs +230 -0
- package/project-template-v2/skills/_shared/scripts/state-advance-stage.mjs +58 -0
- package/project-template-v2/skills/_shared/scripts/state-get.mjs +339 -0
- package/project-template-v2/skills/_shared/scripts/state-handoff.mjs +39 -0
- package/project-template-v2/skills/_shared/scripts/state-set.mjs +94 -0
- package/project-template-v2/skills/_shared/scripts/state-store.mjs +783 -0
- package/project-template-v2/skills/_shared/scripts/todo-add.mjs +88 -0
- package/project-template-v2/skills/_shared/scripts/todo-get.mjs +130 -0
- package/project-template-v2/skills/_shared/scripts/todo-remove.mjs +33 -0
- package/project-template-v2/skills/_shared/scripts/todo-set.mjs +47 -0
- package/project-template-v2/skills/_shared/scripts/verify-asset-code-sync.mjs +268 -0
- package/project-template-v2/skills/_shared/scripts/verify-env.mjs +285 -0
- package/project-template-v2/skills/_shared/scripts/verify-placeholders.mjs +161 -0
- package/project-template-v2/skills/playable-autoplay/SKILL.md +176 -0
- package/project-template-v2/skills/playable-autoplay/agents/openai.yaml +4 -0
- package/project-template-v2/skills/playable-debug/SKILL.md +116 -0
- package/project-template-v2/skills/playable-debug/agents/openai.yaml +4 -0
- package/project-template-v2/skills/playable-debug/references/debug-config.md +40 -0
- package/project-template-v2/skills/playable-record/SKILL.md +140 -0
- package/project-template-v2/skills/playable-record/scripts/lib/dev-server.mjs +104 -0
- package/project-template-v2/skills/playable-record/scripts/lib/record-audio-bridge.js +141 -0
- package/project-template-v2/skills/playable-record/scripts/record-playable.mjs +425 -0
- package/project-template-v2/skills/playable-record/scripts/verify-contract.mjs +261 -0
- package/project-template-v2/skills/playable-record/scripts/verify-firstwin.mjs +398 -0
- package/project-template-v2/skills/playable-record/scripts/verify-fusion.mjs +322 -0
- package/project-template-v2/skills/playable-record/scripts/verify-lifecycle.mjs +105 -0
- package/project-template-v2/skills/playable-record/scripts/verify-vlm-video.mjs +230 -0
- package/project-template-v2/skills/playable-validate/SKILL.md +61 -0
- package/project-template-v2/skills/playable-validate/validation-rules.md +14 -0
- package/project-template-v2/skills/playable-verify-ui/SKILL.md +63 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/__init__.py +1 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/config_expr.js +218 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/node_utils.js +51 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/profile_loader.py +59 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/report.py +106 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/runtime_contract.js +116 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_fetch_antipatterns.py +59 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_hardcoded_layout.js +64 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_runtime_contract.js +58 -0
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_text_overlap.py +333 -0
- package/project-template-v2/ta-workspace/scripts/.gitkeep +0 -0
- package/project-template-v2/tsconfig.json +20 -0
- package/project-template-v2/vite.config.ts +27 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import JSON5 from 'json5';
|
|
2
|
+
export class ParameterEditError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code) {
|
|
5
|
+
super(code);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'ParameterEditError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function replaceJson5Path(source, fieldPath, value) {
|
|
11
|
+
const span = findJson5ValueSpan(source, fieldPath);
|
|
12
|
+
if (!span)
|
|
13
|
+
throw new ParameterEditError('parameter_field_missing');
|
|
14
|
+
const current = skipTrivia(source, span.start, span.end);
|
|
15
|
+
if (source[current] === '{' || source[current] === '[') {
|
|
16
|
+
throw new ParameterEditError('parameter_value_invalid');
|
|
17
|
+
}
|
|
18
|
+
return `${source.slice(0, span.start)}${formatJson5Value(value, source.slice(span.start, span.end))}${source.slice(span.end)}`;
|
|
19
|
+
}
|
|
20
|
+
export function findJson5ValueSpan(source, fieldPath) {
|
|
21
|
+
const start = skipTrivia(source, 0, source.length);
|
|
22
|
+
if (source[start] !== '{')
|
|
23
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
24
|
+
return findInObject(source, start, source.length, fieldPath, 0);
|
|
25
|
+
}
|
|
26
|
+
function findInObject(source, braceIndex, end, fieldPath, depth) {
|
|
27
|
+
let index = skipTrivia(source, braceIndex + 1, end);
|
|
28
|
+
while (index < end) {
|
|
29
|
+
if (source[index] === '}')
|
|
30
|
+
return undefined;
|
|
31
|
+
const key = parseKey(source, index, end);
|
|
32
|
+
index = skipTrivia(source, key.end, end);
|
|
33
|
+
if (source[index] !== ':')
|
|
34
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
35
|
+
const value = scanValue(source, index + 1, end);
|
|
36
|
+
if (key.name === fieldPath[depth]) {
|
|
37
|
+
if (depth === fieldPath.length - 1)
|
|
38
|
+
return value;
|
|
39
|
+
const nested = skipTrivia(source, value.start, end);
|
|
40
|
+
if (source[nested] !== '{')
|
|
41
|
+
throw new ParameterEditError('parameter_field_missing');
|
|
42
|
+
return findInObject(source, nested, value.end, fieldPath, depth + 1);
|
|
43
|
+
}
|
|
44
|
+
index = skipTrivia(source, value.end, end);
|
|
45
|
+
if (source[index] === ',')
|
|
46
|
+
index = skipTrivia(source, index + 1, end);
|
|
47
|
+
}
|
|
48
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
49
|
+
}
|
|
50
|
+
function parseKey(source, start, end) {
|
|
51
|
+
const index = skipTrivia(source, start, end);
|
|
52
|
+
const first = source[index];
|
|
53
|
+
if (first === '"' || first === "'") {
|
|
54
|
+
const close = stringEnd(source, index, end);
|
|
55
|
+
return { name: JSON5.parse(source.slice(index, close)), end: close };
|
|
56
|
+
}
|
|
57
|
+
if (!/[A-Za-z_$]/.test(first ?? ''))
|
|
58
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
59
|
+
let cursor = index + 1;
|
|
60
|
+
while (cursor < end && /[A-Za-z0-9_$]/.test(source[cursor]))
|
|
61
|
+
cursor += 1;
|
|
62
|
+
return { name: source.slice(index, cursor), end: cursor };
|
|
63
|
+
}
|
|
64
|
+
function scanValue(source, from, end) {
|
|
65
|
+
const start = skipTrivia(source, from, end);
|
|
66
|
+
const first = source[start];
|
|
67
|
+
if (first === '"' || first === "'")
|
|
68
|
+
return { start, end: stringEnd(source, start, end) };
|
|
69
|
+
if (first === '{' || first === '[')
|
|
70
|
+
return { start, end: balancedEnd(source, start, end) };
|
|
71
|
+
let cursor = start;
|
|
72
|
+
while (cursor < end) {
|
|
73
|
+
const current = source[cursor];
|
|
74
|
+
if (/\s/.test(current) || current === ',' || current === '}' || current === ']' || current === '/')
|
|
75
|
+
break;
|
|
76
|
+
cursor += 1;
|
|
77
|
+
}
|
|
78
|
+
if (cursor === start)
|
|
79
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
80
|
+
return { start, end: cursor };
|
|
81
|
+
}
|
|
82
|
+
function balancedEnd(source, start, end) {
|
|
83
|
+
const open = source[start];
|
|
84
|
+
const close = open === '{' ? '}' : ']';
|
|
85
|
+
let depth = 0;
|
|
86
|
+
let index = start;
|
|
87
|
+
while (index < end) {
|
|
88
|
+
const current = source[index];
|
|
89
|
+
if (current === '"' || current === "'") {
|
|
90
|
+
index = stringEnd(source, index, end);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (current === '/' && source[index + 1] === '/') {
|
|
94
|
+
index = skipTrivia(source, index, end);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (current === '/' && source[index + 1] === '*') {
|
|
98
|
+
index = skipTrivia(source, index, end);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (current === open)
|
|
102
|
+
depth += 1;
|
|
103
|
+
else if (current === close) {
|
|
104
|
+
depth -= 1;
|
|
105
|
+
if (depth === 0)
|
|
106
|
+
return index + 1;
|
|
107
|
+
}
|
|
108
|
+
index += 1;
|
|
109
|
+
}
|
|
110
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
111
|
+
}
|
|
112
|
+
function stringEnd(source, start, end) {
|
|
113
|
+
const quote = source[start];
|
|
114
|
+
let index = start + 1;
|
|
115
|
+
while (index < end) {
|
|
116
|
+
const current = source[index];
|
|
117
|
+
if (current === '\\') {
|
|
118
|
+
index += 2;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (current === quote)
|
|
122
|
+
return index + 1;
|
|
123
|
+
index += 1;
|
|
124
|
+
}
|
|
125
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
126
|
+
}
|
|
127
|
+
function skipTrivia(source, start, end) {
|
|
128
|
+
let index = start;
|
|
129
|
+
while (index < end) {
|
|
130
|
+
const current = source[index];
|
|
131
|
+
if (/\s/.test(current)) {
|
|
132
|
+
index += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (current === '/' && source[index + 1] === '/') {
|
|
136
|
+
index += 2;
|
|
137
|
+
while (index < end && source[index] !== '\n')
|
|
138
|
+
index += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (current === '/' && source[index + 1] === '*') {
|
|
142
|
+
const close = source.indexOf('*/', index + 2);
|
|
143
|
+
if (close < 0 || close >= end)
|
|
144
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
145
|
+
index = close + 2;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
return index;
|
|
151
|
+
}
|
|
152
|
+
function formatJson5Value(value, previous) {
|
|
153
|
+
if (typeof value === 'string') {
|
|
154
|
+
const trimmed = previous.trim();
|
|
155
|
+
if (trimmed.startsWith("'")) {
|
|
156
|
+
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`;
|
|
157
|
+
}
|
|
158
|
+
return JSON.stringify(value);
|
|
159
|
+
}
|
|
160
|
+
if (typeof value === 'boolean' || value === null)
|
|
161
|
+
return String(value);
|
|
162
|
+
const trimmed = previous.trim();
|
|
163
|
+
if (/^[-+]?0x[0-9a-fA-F]+$/.test(trimmed) && Number.isInteger(value)) {
|
|
164
|
+
const sign = value < 0 ? '-' : trimmed.startsWith('+') ? '+' : '';
|
|
165
|
+
const hex = Math.abs(value).toString(16);
|
|
166
|
+
const upper = /[A-F]/.test(trimmed) && !/[a-f]/.test(trimmed);
|
|
167
|
+
return `${sign}0x${upper ? hex.toUpperCase() : hex}`;
|
|
168
|
+
}
|
|
169
|
+
return Number.isInteger(value) ? String(value) : JSON.stringify(value);
|
|
170
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { readdir, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { relative, resolve, sep } from 'node:path';
|
|
3
|
+
import JSON5 from 'json5';
|
|
4
|
+
import { parameterDocumentSchema, parameterPatchPayloadSchema, workspaceRelativePathSchema, WORKSPACE_FILE_CONTENT_MAX, } from '@playcraft/common/cloud-workspace/runtime';
|
|
5
|
+
import { InvalidWorkspacePathError, writeWorkspaceFile, } from './files.js';
|
|
6
|
+
import { ParameterEditError, replaceJson5Path } from './json5-edit.js';
|
|
7
|
+
const MANIFEST_PATH = '.playcraft/workspace.json';
|
|
8
|
+
const DEFAULT_SCHEMA_PATH = 'src/theme/theme.schema.json5';
|
|
9
|
+
const THEME_INDEX_PATH = 'src/theme/index.ts';
|
|
10
|
+
const THEME_ROOT = 'src/theme';
|
|
11
|
+
export async function readWorkspaceParameters(root, revisions) {
|
|
12
|
+
const located = await locateParameterFiles(root);
|
|
13
|
+
if (located.errors.some((item) => item.reason === 'parameter_manifest_invalid')) {
|
|
14
|
+
return parameterDocumentSchema.parse({ status: 'invalid', errors: located.errors });
|
|
15
|
+
}
|
|
16
|
+
if (!located.schemaPath) {
|
|
17
|
+
return parameterDocumentSchema.parse({
|
|
18
|
+
status: 'missing',
|
|
19
|
+
...(located.errors.length > 0 ? { errors: located.errors } : {}),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const schemaFile = await readOptionalJson5(root, located.schemaPath);
|
|
23
|
+
if (schemaFile.status === 'missing') {
|
|
24
|
+
return parameterDocumentSchema.parse({
|
|
25
|
+
status: 'missing',
|
|
26
|
+
schemaPath: located.schemaPath,
|
|
27
|
+
themeId: located.themeId,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (schemaFile.status === 'invalid') {
|
|
31
|
+
return parameterDocumentSchema.parse({
|
|
32
|
+
status: 'invalid',
|
|
33
|
+
schemaPath: located.schemaPath,
|
|
34
|
+
themeId: located.themeId,
|
|
35
|
+
errors: [{ path: located.schemaPath, reason: schemaFile.reason }],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (!located.dataPath) {
|
|
39
|
+
return parameterDocumentSchema.parse({
|
|
40
|
+
status: 'schema_only',
|
|
41
|
+
schemaPath: located.schemaPath,
|
|
42
|
+
themeId: located.themeId,
|
|
43
|
+
schema: schemaFile.value,
|
|
44
|
+
schemaRevision: revisions[located.schemaPath]?.revision ?? 0,
|
|
45
|
+
...(located.errors.length > 0 ? { errors: located.errors } : {}),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const dataFile = await readOptionalJson5(root, located.dataPath);
|
|
49
|
+
if (dataFile.status === 'missing') {
|
|
50
|
+
return parameterDocumentSchema.parse({
|
|
51
|
+
status: 'schema_only',
|
|
52
|
+
schemaPath: located.schemaPath,
|
|
53
|
+
dataPath: located.dataPath,
|
|
54
|
+
themeId: located.themeId,
|
|
55
|
+
schema: schemaFile.value,
|
|
56
|
+
schemaRevision: revisions[located.schemaPath]?.revision ?? 0,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (dataFile.status === 'invalid') {
|
|
60
|
+
return parameterDocumentSchema.parse({
|
|
61
|
+
status: 'invalid',
|
|
62
|
+
schemaPath: located.schemaPath,
|
|
63
|
+
dataPath: located.dataPath,
|
|
64
|
+
themeId: located.themeId,
|
|
65
|
+
schema: schemaFile.value,
|
|
66
|
+
schemaRevision: revisions[located.schemaPath]?.revision ?? 0,
|
|
67
|
+
errors: [{ path: located.dataPath, reason: dataFile.reason }],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return parameterDocumentSchema.parse({
|
|
71
|
+
status: 'ready',
|
|
72
|
+
schemaPath: located.schemaPath,
|
|
73
|
+
dataPath: located.dataPath,
|
|
74
|
+
themeId: located.themeId,
|
|
75
|
+
schema: schemaFile.value,
|
|
76
|
+
data: dataFile.value,
|
|
77
|
+
schemaRevision: revisions[located.schemaPath]?.revision ?? 0,
|
|
78
|
+
dataRevision: revisions[located.dataPath]?.revision ?? 0,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export async function patchWorkspaceParameters(root, revisions, payload) {
|
|
82
|
+
const parsed = parameterPatchPayloadSchema.parse(payload);
|
|
83
|
+
const document = await readWorkspaceParameters(root, revisions);
|
|
84
|
+
if (document.status === 'missing' || document.status === 'schema_only' || !document.dataPath) {
|
|
85
|
+
throw new ParameterEditError('parameter_data_missing');
|
|
86
|
+
}
|
|
87
|
+
if (document.status === 'invalid')
|
|
88
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
89
|
+
const typeError = validateAgainstSchema(document.schema, parsed.fieldPath, parsed.value);
|
|
90
|
+
if (typeError)
|
|
91
|
+
throw new ParameterEditError(typeError);
|
|
92
|
+
const source = await readOptionalText(root, document.dataPath);
|
|
93
|
+
if (source === undefined)
|
|
94
|
+
throw new ParameterEditError('parameter_data_missing');
|
|
95
|
+
const next = replaceJson5Path(source, parsed.fieldPath, parsed.value);
|
|
96
|
+
JSON5.parse(next);
|
|
97
|
+
return writeWorkspaceFile(root, document.dataPath, next);
|
|
98
|
+
}
|
|
99
|
+
async function locateParameterFiles(root) {
|
|
100
|
+
const manifest = await readOptionalJson5(root, MANIFEST_PATH);
|
|
101
|
+
if (manifest.status === 'invalid') {
|
|
102
|
+
return { errors: [{ path: MANIFEST_PATH, reason: 'parameter_manifest_invalid' }] };
|
|
103
|
+
}
|
|
104
|
+
if (manifest.status === 'ok') {
|
|
105
|
+
const declared = declaredPaths(manifest.value);
|
|
106
|
+
if (!declared) {
|
|
107
|
+
return { errors: [{ path: MANIFEST_PATH, reason: 'parameter_manifest_invalid' }] };
|
|
108
|
+
}
|
|
109
|
+
if (declared.schemaPath || declared.dataPath)
|
|
110
|
+
return { ...declared, errors: [] };
|
|
111
|
+
if (declared.themeId) {
|
|
112
|
+
return {
|
|
113
|
+
schemaPath: DEFAULT_SCHEMA_PATH,
|
|
114
|
+
dataPath: `${THEME_ROOT}/${declared.themeId}/theme.data.json5`,
|
|
115
|
+
themeId: declared.themeId,
|
|
116
|
+
errors: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const schemaExists = (await readOptionalJson5(root, DEFAULT_SCHEMA_PATH)).status !== 'missing';
|
|
121
|
+
const theme = await readDeclaredTheme(root);
|
|
122
|
+
const dataPath = theme
|
|
123
|
+
? `${THEME_ROOT}/${theme}/theme.data.json5`
|
|
124
|
+
: await uniqueThemeDataPath(root);
|
|
125
|
+
if (!schemaExists) {
|
|
126
|
+
return { themeId: theme, errors: [] };
|
|
127
|
+
}
|
|
128
|
+
if (!theme && (await listThemeDataFiles(root)).length > 1) {
|
|
129
|
+
return {
|
|
130
|
+
schemaPath: DEFAULT_SCHEMA_PATH,
|
|
131
|
+
errors: [{ path: THEME_INDEX_PATH, reason: 'theme_data_ambiguous' }],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
schemaPath: DEFAULT_SCHEMA_PATH,
|
|
136
|
+
dataPath,
|
|
137
|
+
themeId: theme,
|
|
138
|
+
errors: [],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function declaredPaths(value) {
|
|
142
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
143
|
+
return undefined;
|
|
144
|
+
const parameters = value.parameters;
|
|
145
|
+
if (parameters === undefined)
|
|
146
|
+
return {};
|
|
147
|
+
if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters))
|
|
148
|
+
return undefined;
|
|
149
|
+
const record = parameters;
|
|
150
|
+
if (Object.keys(record).some((key) => !['schemaPath', 'dataPath', 'themeId'].includes(key))) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const schemaPath = record.schemaPath === undefined ? undefined : workspaceRelativePathSchema.safeParse(record.schemaPath);
|
|
154
|
+
const dataPath = record.dataPath === undefined ? undefined : workspaceRelativePathSchema.safeParse(record.dataPath);
|
|
155
|
+
const themeId = record.themeId === undefined
|
|
156
|
+
? undefined
|
|
157
|
+
: typeof record.themeId === 'string' && record.themeId.trim() && record.themeId.length <= 128
|
|
158
|
+
? record.themeId.trim()
|
|
159
|
+
: undefined;
|
|
160
|
+
if (record.schemaPath !== undefined && !schemaPath?.success)
|
|
161
|
+
return undefined;
|
|
162
|
+
if (record.dataPath !== undefined && !dataPath?.success)
|
|
163
|
+
return undefined;
|
|
164
|
+
if (record.themeId !== undefined && !themeId)
|
|
165
|
+
return undefined;
|
|
166
|
+
return {
|
|
167
|
+
schemaPath: schemaPath?.success ? schemaPath.data : undefined,
|
|
168
|
+
dataPath: dataPath?.success ? dataPath.data : undefined,
|
|
169
|
+
themeId,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async function readDeclaredTheme(root) {
|
|
173
|
+
const contents = await readOptionalText(root, THEME_INDEX_PATH);
|
|
174
|
+
if (!contents)
|
|
175
|
+
return undefined;
|
|
176
|
+
const text = contents.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
177
|
+
const match = text.match(/export\s+\{\s*default\s*\}\s+from\s+['"]\.\/([^'"]+)['"]/);
|
|
178
|
+
const themeId = match?.[1]?.trim();
|
|
179
|
+
return themeId && workspaceRelativePathSchema.safeParse(`${THEME_ROOT}/${themeId}/theme.data.json5`).success
|
|
180
|
+
? themeId
|
|
181
|
+
: undefined;
|
|
182
|
+
}
|
|
183
|
+
async function uniqueThemeDataPath(root) {
|
|
184
|
+
const files = await listThemeDataFiles(root);
|
|
185
|
+
return files.length === 1 ? files[0] : undefined;
|
|
186
|
+
}
|
|
187
|
+
async function listThemeDataFiles(root) {
|
|
188
|
+
try {
|
|
189
|
+
const rootReal = await realpath(root);
|
|
190
|
+
const themeRoot = resolve(rootReal, ...THEME_ROOT.split('/'));
|
|
191
|
+
const entries = await readdir(themeRoot, { withFileTypes: true });
|
|
192
|
+
const files = [];
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
195
|
+
continue;
|
|
196
|
+
const relativePath = `${THEME_ROOT}/${entry.name}/theme.data.json5`;
|
|
197
|
+
if ((await readOptionalText(root, relativePath)) !== undefined)
|
|
198
|
+
files.push(relativePath);
|
|
199
|
+
}
|
|
200
|
+
return files;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error.code === 'ENOENT')
|
|
204
|
+
return [];
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function readOptionalJson5(root, relativePath) {
|
|
209
|
+
const text = await readOptionalText(root, relativePath);
|
|
210
|
+
if (text === undefined)
|
|
211
|
+
return { status: 'missing' };
|
|
212
|
+
try {
|
|
213
|
+
return { status: 'ok', value: JSON5.parse(text) };
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return { status: 'invalid', reason: 'parameter_document_invalid' };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function readOptionalText(root, relativePath) {
|
|
220
|
+
const parsed = workspaceRelativePathSchema.safeParse(relativePath);
|
|
221
|
+
if (!parsed.success)
|
|
222
|
+
return undefined;
|
|
223
|
+
try {
|
|
224
|
+
const rootReal = await realpath(root);
|
|
225
|
+
const target = resolve(rootReal, ...parsed.data.split('/'));
|
|
226
|
+
const text = await readFile(target, 'utf8');
|
|
227
|
+
const existing = await realpath(target);
|
|
228
|
+
const rel = relative(rootReal, existing);
|
|
229
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || rel.startsWith('..')) {
|
|
230
|
+
throw new InvalidWorkspacePathError(relativePath);
|
|
231
|
+
}
|
|
232
|
+
if (Buffer.byteLength(text, 'utf8') > WORKSPACE_FILE_CONTENT_MAX) {
|
|
233
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
234
|
+
}
|
|
235
|
+
return text;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (error instanceof ParameterEditError || error instanceof InvalidWorkspacePathError)
|
|
239
|
+
throw error;
|
|
240
|
+
if (error.code === 'ENOENT')
|
|
241
|
+
return undefined;
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function validateAgainstSchema(schema, fieldPath, value) {
|
|
246
|
+
let node = schema;
|
|
247
|
+
for (const key of fieldPath) {
|
|
248
|
+
if (!node || typeof node !== 'object' || Array.isArray(node))
|
|
249
|
+
return 'parameter_value_invalid';
|
|
250
|
+
const properties = node.properties;
|
|
251
|
+
if (!properties || typeof properties !== 'object' || Array.isArray(properties))
|
|
252
|
+
return 'parameter_value_invalid';
|
|
253
|
+
node = properties[key];
|
|
254
|
+
if (node === undefined)
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
if (!node || typeof node !== 'object' || Array.isArray(node))
|
|
258
|
+
return undefined;
|
|
259
|
+
const type = node.type;
|
|
260
|
+
if (type === 'string')
|
|
261
|
+
return typeof value === 'string' ? undefined : 'parameter_value_invalid';
|
|
262
|
+
if (type === 'boolean')
|
|
263
|
+
return typeof value === 'boolean' ? undefined : 'parameter_value_invalid';
|
|
264
|
+
if (type === 'integer')
|
|
265
|
+
return Number.isInteger(value) ? undefined : 'parameter_value_invalid';
|
|
266
|
+
if (type === 'number') {
|
|
267
|
+
return typeof value === 'number' && Number.isFinite(value) ? undefined : 'parameter_value_invalid';
|
|
268
|
+
}
|
|
269
|
+
if (type === 'null')
|
|
270
|
+
return value === null ? undefined : 'parameter_value_invalid';
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|