@playcraft/cli 0.0.57 → 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 +34 -16
- 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/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 +2 -0
- package/dist/remix/init-template.js +2 -0
- package/dist/remix/pull.js +2 -0
- package/dist/remix/push.js +21 -1
- 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 +4 -1
- 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,364 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { lstat, mkdir, open, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
|
+
import { matchWorkspaceGitGatewayCloneUrl, workspaceGitAccessSchema, workspaceGitTransportSchema, workspacePreparationSchema, workspaceRegistrationSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
7
|
+
import { WorkspaceContextError } from './context-error.js';
|
|
8
|
+
/** The caller holds the Workspace lock. Only an absent, never-published checkout may be cloned. */
|
|
9
|
+
export async function prepareWorkspaceCheckout(state, binding, input, leaseFence) {
|
|
10
|
+
const preparation = workspacePreparationSchema.safeParse(input);
|
|
11
|
+
if (!preparation.success)
|
|
12
|
+
throw new WorkspaceContextError('invalid_workspace_preparation');
|
|
13
|
+
const source = parseCloneSource(binding.repositoryUrl);
|
|
14
|
+
const parent = await realpath(state.config.workspaceRoot);
|
|
15
|
+
const target = join(parent, binding.workspaceId);
|
|
16
|
+
const receiptDir = join(state.config.stateDir, 'workspace-preparations');
|
|
17
|
+
await mkdir(receiptDir, { recursive: true, mode: 0o700 });
|
|
18
|
+
const receiptPath = join(receiptDir, `${binding.workspaceId}.json`);
|
|
19
|
+
const previous = await readReceipt(receiptPath, binding);
|
|
20
|
+
if (previous?.status === 'published')
|
|
21
|
+
throw new WorkspaceContextError('workspace_recovery_required');
|
|
22
|
+
if (await exists(target))
|
|
23
|
+
throw new WorkspaceContextError('workspace_checkout_conflict');
|
|
24
|
+
const receipt = previous ?? {
|
|
25
|
+
binding,
|
|
26
|
+
stagingName: `.prepare-${binding.workspaceId}-${randomUUID()}`,
|
|
27
|
+
status: 'cloning',
|
|
28
|
+
};
|
|
29
|
+
const staging = join(parent, receipt.stagingName);
|
|
30
|
+
const assertCurrent = () => {
|
|
31
|
+
if (state.journal.destroyed())
|
|
32
|
+
throw new WorkspaceContextError('runtime_destroyed');
|
|
33
|
+
if (leaseFence < state.acceptedLeaseFence)
|
|
34
|
+
throw new WorkspaceContextError('stale_lease');
|
|
35
|
+
};
|
|
36
|
+
assertCurrent();
|
|
37
|
+
if (receipt.status === 'cloning') {
|
|
38
|
+
// Persist ownership before creating/removing a staging directory, so a crash can be retried.
|
|
39
|
+
await writeReceipt(receiptPath, receipt);
|
|
40
|
+
await rm(staging, { recursive: true, force: true });
|
|
41
|
+
await mkdir(staging, { mode: 0o700 });
|
|
42
|
+
const access = preparation.data.gitAccess;
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (access &&
|
|
45
|
+
(Date.parse(access.issuedAt) > now || Date.parse(access.expiresAt) <= now + 10_000))
|
|
46
|
+
throw new WorkspaceContextError('workspace_git_access_expired');
|
|
47
|
+
let gateway;
|
|
48
|
+
try {
|
|
49
|
+
gateway = access
|
|
50
|
+
? await mountGateway(state, binding.repositoryUrl, access, preparation.data.transport)
|
|
51
|
+
: undefined;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
await rm(staging, { recursive: true, force: true });
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
const deadline = Math.min(now + 90_000, access ? Date.parse(access.expiresAt) : Infinity);
|
|
58
|
+
const env = {
|
|
59
|
+
PATH: process.env.PATH,
|
|
60
|
+
HOME: state.config.stateDir,
|
|
61
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
62
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
63
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
64
|
+
GIT_ALLOW_PROTOCOL: 'http:https',
|
|
65
|
+
};
|
|
66
|
+
const cloneConfig = [];
|
|
67
|
+
if (gateway) {
|
|
68
|
+
// Rewrite only inside this process. The password is read from the mounted directory for the proxy URL.
|
|
69
|
+
env.GIT_CONFIG_COUNT = '1';
|
|
70
|
+
env.GIT_CONFIG_KEY_0 = `url.${gateway.cloneUrl}.insteadOf`;
|
|
71
|
+
env.GIT_CONFIG_VALUE_0 = binding.repositoryUrl;
|
|
72
|
+
env.PLAYCRAFT_GIT_HELPER = Buffer.from(GIT_GATEWAY_CREDENTIAL_HELPER).toString('base64');
|
|
73
|
+
env.PLAYCRAFT_GIT_CREDENTIAL_DIRECTORY = gateway.directory;
|
|
74
|
+
env.PLAYCRAFT_GIT_GATEWAY_CLONE_URL = gateway.cloneUrl;
|
|
75
|
+
env.PLAYCRAFT_GIT_CANONICAL_URL = binding.repositoryUrl;
|
|
76
|
+
cloneConfig.push(`credential.helper=!${shellQuote(process.execPath)} -e ${shellQuote('eval(Buffer.from(process.env.PLAYCRAFT_GIT_HELPER,"base64").toString())')}`, 'credential.useHttpPath=true');
|
|
77
|
+
}
|
|
78
|
+
else if (access) {
|
|
79
|
+
// Authentication exists only in the clone process environment; never in arguments or .git/config.
|
|
80
|
+
env.GIT_CONFIG_COUNT = '1';
|
|
81
|
+
env.GIT_CONFIG_KEY_0 = `http.${source.href}.extraHeader`;
|
|
82
|
+
env.GIT_CONFIG_VALUE_0 = `Authorization: Basic ${Buffer.from(`${access.username}:${access.password}`).toString('base64')}`;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await runGit(['check-ref-format', '--branch', binding.branchName], env, deadline);
|
|
86
|
+
await runGit([
|
|
87
|
+
'clone',
|
|
88
|
+
'--no-checkout',
|
|
89
|
+
'--single-branch',
|
|
90
|
+
'--branch',
|
|
91
|
+
binding.branchName,
|
|
92
|
+
'--',
|
|
93
|
+
binding.repositoryUrl,
|
|
94
|
+
staging,
|
|
95
|
+
], env, deadline, cloneConfig);
|
|
96
|
+
// Checkout never receives the temporary credential and never invokes repository hooks or global filters.
|
|
97
|
+
clearCloneSecrets(env);
|
|
98
|
+
await runGit(['-C', staging, 'checkout', '--force', binding.branchName], env, deadline);
|
|
99
|
+
assertCurrent();
|
|
100
|
+
receipt.status = 'ready';
|
|
101
|
+
await writeReceipt(receiptPath, receipt);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
await rm(staging, { recursive: true, force: true });
|
|
105
|
+
if (error instanceof WorkspaceContextError)
|
|
106
|
+
throw error;
|
|
107
|
+
throw new WorkspaceContextError('workspace_clone_failed');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const stat = await lstat(staging).catch(() => undefined);
|
|
111
|
+
if (!stat?.isDirectory() || stat.isSymbolicLink())
|
|
112
|
+
throw new WorkspaceContextError('workspace_recovery_required');
|
|
113
|
+
assertCurrent();
|
|
114
|
+
if (await exists(target))
|
|
115
|
+
throw new WorkspaceContextError('workspace_checkout_conflict');
|
|
116
|
+
await rename(staging, target);
|
|
117
|
+
receipt.status = 'published';
|
|
118
|
+
await writeReceipt(receiptPath, receipt);
|
|
119
|
+
}
|
|
120
|
+
function clearCloneSecrets(env) {
|
|
121
|
+
for (const key of [
|
|
122
|
+
'GIT_CONFIG_COUNT',
|
|
123
|
+
'GIT_CONFIG_KEY_0',
|
|
124
|
+
'GIT_CONFIG_VALUE_0',
|
|
125
|
+
'GIT_CONFIG_KEY_1',
|
|
126
|
+
'GIT_CONFIG_VALUE_1',
|
|
127
|
+
'PLAYCRAFT_GIT_HELPER',
|
|
128
|
+
'PLAYCRAFT_GIT_CREDENTIAL_DIRECTORY',
|
|
129
|
+
'PLAYCRAFT_GIT_GATEWAY_CLONE_URL',
|
|
130
|
+
'PLAYCRAFT_GIT_CANONICAL_URL',
|
|
131
|
+
]) {
|
|
132
|
+
delete env[key];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function shellQuote(value) {
|
|
136
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
137
|
+
}
|
|
138
|
+
/** Read-only open of the publisher's control directory. Credentials are not copied into the checkout. */
|
|
139
|
+
async function mountGateway(state, repositoryUrl, access, transport) {
|
|
140
|
+
if (!transport)
|
|
141
|
+
return undefined;
|
|
142
|
+
const directory = state.config.gitCredentialDirectory;
|
|
143
|
+
const matched = matchWorkspaceGitGatewayCloneUrl(transport.cloneUrl, state.config.gitGatewayOrigins, repositoryUrl);
|
|
144
|
+
if (!matched.ok || !directory)
|
|
145
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
146
|
+
const parsed = workspaceGitTransportSchema.parse(transport);
|
|
147
|
+
if (parsed.cloneUrl !== transport.cloneUrl) {
|
|
148
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
149
|
+
}
|
|
150
|
+
const entry = await readMountedEntry(directory, transport.cloneUrl, matched.requestId, repositoryUrl);
|
|
151
|
+
if (entry.username !== access.username ||
|
|
152
|
+
entry.issuedAt !== access.issuedAt ||
|
|
153
|
+
entry.expiresAt !== access.expiresAt ||
|
|
154
|
+
!sameSecret(entry.password, access.password)) {
|
|
155
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
156
|
+
}
|
|
157
|
+
return { directory, cloneUrl: transport.cloneUrl };
|
|
158
|
+
}
|
|
159
|
+
async function readMountedEntry(directory, cloneUrl, requestId, repositoryUrl) {
|
|
160
|
+
if (!isAbsolute(directory) || (await realpath(directory)) !== resolve(directory)) {
|
|
161
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
162
|
+
}
|
|
163
|
+
const directoryStat = await lstat(directory);
|
|
164
|
+
if (!directoryStat.isDirectory() ||
|
|
165
|
+
directoryStat.isSymbolicLink() ||
|
|
166
|
+
(directoryStat.mode & 0o077) !== 0 ||
|
|
167
|
+
(typeof process.getuid === 'function' && directoryStat.uid !== process.getuid())) {
|
|
168
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
169
|
+
}
|
|
170
|
+
const directoryHandle = await open(directory, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_DIRECTORY);
|
|
171
|
+
await directoryHandle.close();
|
|
172
|
+
const file = await open(join(directory, 'workspace-git-credentials.json'), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
173
|
+
try {
|
|
174
|
+
const stat = await file.stat();
|
|
175
|
+
if (!stat.isFile() || (stat.mode & 0o077) !== 0 || stat.size > 1024 * 1024) {
|
|
176
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
177
|
+
}
|
|
178
|
+
const document = JSON.parse(await file.readFile('utf8'));
|
|
179
|
+
if (blocked(document).has(requestId)) {
|
|
180
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
181
|
+
}
|
|
182
|
+
const entries = (document.entries ?? []).filter((entry) => entry.transport &&
|
|
183
|
+
typeof entry.transport === 'object' &&
|
|
184
|
+
entry.transport.cloneUrl === cloneUrl);
|
|
185
|
+
if (entries.length !== 1)
|
|
186
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
187
|
+
const entry = entries[0];
|
|
188
|
+
if (entry.repositoryUrl !== repositoryUrl ||
|
|
189
|
+
requestIdOf(entry.command) !== requestId ||
|
|
190
|
+
requestIdOf(entry.receipt) !== requestId) {
|
|
191
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
192
|
+
}
|
|
193
|
+
return workspaceGitAccessSchema.parse(entry.gitAccess);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
await file.close();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function requestIdOf(value) {
|
|
200
|
+
if (!value || typeof value !== 'object')
|
|
201
|
+
return undefined;
|
|
202
|
+
const id = value.requestId;
|
|
203
|
+
return typeof id === 'string' ? id : undefined;
|
|
204
|
+
}
|
|
205
|
+
function blocked(document) {
|
|
206
|
+
const ids = new Set();
|
|
207
|
+
for (const list of [document.retired, document.revocations]) {
|
|
208
|
+
if (list === undefined)
|
|
209
|
+
continue;
|
|
210
|
+
if (!Array.isArray(list))
|
|
211
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
212
|
+
for (const item of list) {
|
|
213
|
+
const id = requestIdOf(item);
|
|
214
|
+
if (id)
|
|
215
|
+
ids.add(id);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return ids;
|
|
219
|
+
}
|
|
220
|
+
function sameSecret(left, right) {
|
|
221
|
+
const a = Buffer.from(left);
|
|
222
|
+
const b = Buffer.from(right);
|
|
223
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
224
|
+
}
|
|
225
|
+
const GIT_GATEWAY_CREDENTIAL_HELPER = `
|
|
226
|
+
const fs = require('node:fs');
|
|
227
|
+
const path = require('node:path');
|
|
228
|
+
const action = process.argv[1];
|
|
229
|
+
if (action !== 'get') process.exit(0);
|
|
230
|
+
const chunks = [];
|
|
231
|
+
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
232
|
+
process.stdin.on('end', () => {
|
|
233
|
+
try {
|
|
234
|
+
const fields = {};
|
|
235
|
+
for (const line of Buffer.concat(chunks).toString('utf8').split('\\n')) {
|
|
236
|
+
if (!line) continue;
|
|
237
|
+
const index = line.indexOf('=');
|
|
238
|
+
if (index > 0) fields[line.slice(0, index)] = line.slice(index + 1);
|
|
239
|
+
}
|
|
240
|
+
const cloneUrl = new URL(process.env.PLAYCRAFT_GIT_GATEWAY_CLONE_URL);
|
|
241
|
+
const canonical = new URL(process.env.PLAYCRAFT_GIT_CANONICAL_URL);
|
|
242
|
+
if (!fields.path || !fields.protocol || !fields.host) process.exit(1);
|
|
243
|
+
const requested = new URL(fields.protocol + '://' + fields.host + '/' + String(fields.path).replace(/^\\/+/, ''));
|
|
244
|
+
if (requested.origin !== cloneUrl.origin || requested.pathname !== cloneUrl.pathname || requested.origin === canonical.origin) process.exit(1);
|
|
245
|
+
const requestId = cloneUrl.pathname.split('/').pop();
|
|
246
|
+
const directory = process.env.PLAYCRAFT_GIT_CREDENTIAL_DIRECTORY;
|
|
247
|
+
const flags = fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
|
|
248
|
+
const dir = fs.openSync(directory, flags | (fs.constants.O_DIRECTORY || 0));
|
|
249
|
+
try {
|
|
250
|
+
if (!fs.fstatSync(dir).isDirectory()) process.exit(1);
|
|
251
|
+
} finally { fs.closeSync(dir); }
|
|
252
|
+
const file = fs.openSync(path.join(directory, 'workspace-git-credentials.json'), flags);
|
|
253
|
+
try {
|
|
254
|
+
const stat = fs.fstatSync(file);
|
|
255
|
+
if (!stat.isFile() || (stat.mode & 0o077) !== 0 || stat.size > 1024 * 1024) process.exit(1);
|
|
256
|
+
const document = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
257
|
+
const blocked = [...(document.retired || []), ...(document.revocations || [])];
|
|
258
|
+
if (blocked.some((item) => item && item.requestId === requestId)) process.exit(1);
|
|
259
|
+
const entries = (document.entries || []).filter((entry) => entry && entry.transport && entry.transport.cloneUrl === process.env.PLAYCRAFT_GIT_GATEWAY_CLONE_URL && entry.repositoryUrl === process.env.PLAYCRAFT_GIT_CANONICAL_URL);
|
|
260
|
+
if (entries.length !== 1) process.exit(1);
|
|
261
|
+
const access = entries[0].gitAccess || {};
|
|
262
|
+
if (Date.parse(access.issuedAt) > Date.now() || Date.parse(access.expiresAt) <= Date.now() + 10000) process.exit(1);
|
|
263
|
+
if (!access.username || !access.password || /[\\r\\n\\0]/.test(access.username) || /[\\r\\n\\0]/.test(access.password)) process.exit(1);
|
|
264
|
+
process.stdout.write('username=' + access.username + '\\npassword=' + access.password + '\\n');
|
|
265
|
+
process.exit(0);
|
|
266
|
+
} finally { fs.closeSync(file); }
|
|
267
|
+
} catch { process.exit(1); }
|
|
268
|
+
});
|
|
269
|
+
`;
|
|
270
|
+
function parseCloneSource(value) {
|
|
271
|
+
try {
|
|
272
|
+
const source = new URL(value);
|
|
273
|
+
if ((source.protocol !== 'https:' &&
|
|
274
|
+
!(source.protocol === 'http:' &&
|
|
275
|
+
['127.0.0.1', '[::1]', 'localhost'].includes(source.hostname))) ||
|
|
276
|
+
source.username ||
|
|
277
|
+
source.password ||
|
|
278
|
+
source.search ||
|
|
279
|
+
source.hash)
|
|
280
|
+
throw new Error();
|
|
281
|
+
return source;
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
throw new WorkspaceContextError('workspace_clone_transport_unavailable');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function exists(path) {
|
|
288
|
+
try {
|
|
289
|
+
await lstat(path);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
if (error.code === 'ENOENT')
|
|
294
|
+
return false;
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function readReceipt(path, binding) {
|
|
299
|
+
try {
|
|
300
|
+
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
301
|
+
const parsed = workspaceRegistrationSchema.parse(value.binding);
|
|
302
|
+
if (JSON.stringify(parsed) !== JSON.stringify(binding) ||
|
|
303
|
+
!['cloning', 'ready', 'published'].includes(value.status) ||
|
|
304
|
+
!new RegExp(`^\\.prepare-${binding.workspaceId}-[0-9a-f-]{36}$`).test(value.stagingName))
|
|
305
|
+
throw new Error();
|
|
306
|
+
return { binding: parsed, stagingName: value.stagingName, status: value.status };
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
if (error.code === 'ENOENT')
|
|
310
|
+
return undefined;
|
|
311
|
+
throw new WorkspaceContextError('workspace_preparation_receipt_invalid');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function writeReceipt(path, value) {
|
|
315
|
+
const temp = `${path}.tmp`;
|
|
316
|
+
await writeFile(temp, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
317
|
+
await rename(temp, path);
|
|
318
|
+
}
|
|
319
|
+
function runGit(args, env, deadline, config = []) {
|
|
320
|
+
return new Promise((resolve, reject) => {
|
|
321
|
+
let timedOut = false;
|
|
322
|
+
const child = spawn('git', [
|
|
323
|
+
'-c',
|
|
324
|
+
'core.hooksPath=/dev/null',
|
|
325
|
+
'-c',
|
|
326
|
+
'credential.helper=',
|
|
327
|
+
...config.flatMap((item) => ['-c', item]),
|
|
328
|
+
'-c',
|
|
329
|
+
'http.followRedirects=false',
|
|
330
|
+
'-c',
|
|
331
|
+
'init.templateDir=',
|
|
332
|
+
...args,
|
|
333
|
+
], {
|
|
334
|
+
env,
|
|
335
|
+
detached: process.platform !== 'win32',
|
|
336
|
+
stdio: 'ignore',
|
|
337
|
+
});
|
|
338
|
+
const timer = setTimeout(() => {
|
|
339
|
+
timedOut = true;
|
|
340
|
+
try {
|
|
341
|
+
if (process.platform !== 'win32' && child.pid)
|
|
342
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
343
|
+
else
|
|
344
|
+
child.kill('SIGKILL');
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
/* The process may have already exited. */
|
|
348
|
+
}
|
|
349
|
+
}, Math.max(1, deadline - Date.now()));
|
|
350
|
+
child.once('error', () => {
|
|
351
|
+
clearTimeout(timer);
|
|
352
|
+
reject(new WorkspaceContextError('workspace_clone_failed'));
|
|
353
|
+
});
|
|
354
|
+
child.once('exit', (code) => {
|
|
355
|
+
clearTimeout(timer);
|
|
356
|
+
if (timedOut)
|
|
357
|
+
reject(new WorkspaceContextError('workspace_clone_timeout'));
|
|
358
|
+
else if (code === 0)
|
|
359
|
+
resolve();
|
|
360
|
+
else
|
|
361
|
+
reject(new WorkspaceContextError('workspace_clone_failed'));
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { workspaceRegistrationSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
6
|
+
import { prepareWorkspaceCheckout } from './prepare.js';
|
|
7
|
+
import { WorkspaceContextError } from './context-error.js';
|
|
8
|
+
export { WorkspaceContextError } from './context-error.js';
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
export function revisionScope(state, workspaceId) {
|
|
11
|
+
return state.config.workspaceMode === 'multi' ? workspaceId : undefined;
|
|
12
|
+
}
|
|
13
|
+
export function workspaceSessions(state, workspaceId) {
|
|
14
|
+
return state.journal
|
|
15
|
+
.listSessions()
|
|
16
|
+
.filter((item) => state.config.workspaceMode !== 'multi' || item.workspaceId === workspaceId);
|
|
17
|
+
}
|
|
18
|
+
export async function workspaceRoot(state, workspaceId) {
|
|
19
|
+
if (state.config.workspaceMode !== 'multi')
|
|
20
|
+
return state.config.workspaceRoot;
|
|
21
|
+
if (!workspaceId || !state.journal.getWorkspace(workspaceId))
|
|
22
|
+
throw new WorkspaceContextError('workspace_not_registered');
|
|
23
|
+
return checkedDirectory(state, workspaceId);
|
|
24
|
+
}
|
|
25
|
+
async function checkedDirectory(state, workspaceId) {
|
|
26
|
+
if (!/^[0-9a-f-]{36}$/i.test(workspaceId))
|
|
27
|
+
throw new WorkspaceContextError('invalid_workspace');
|
|
28
|
+
const parent = await realpath(state.config.workspaceRoot);
|
|
29
|
+
const target = join(parent, workspaceId);
|
|
30
|
+
try {
|
|
31
|
+
const stat = await lstat(target);
|
|
32
|
+
if (!stat.isDirectory() ||
|
|
33
|
+
stat.isSymbolicLink() ||
|
|
34
|
+
relative(parent, await realpath(target)) !== workspaceId) {
|
|
35
|
+
throw new WorkspaceContextError('invalid_workspace_directory');
|
|
36
|
+
}
|
|
37
|
+
const stateDir = await realpath(state.config.stateDir);
|
|
38
|
+
if (stateDir === target || stateDir.startsWith(`${target}/`))
|
|
39
|
+
throw new WorkspaceContextError('invalid_workspace_directory');
|
|
40
|
+
return target;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof WorkspaceContextError)
|
|
44
|
+
throw error;
|
|
45
|
+
throw new WorkspaceContextError('workspace_not_prepared');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Registers a fixed checkout; optional first creation is restricted to its trusted binding. */
|
|
49
|
+
export async function registerWorkspace(state, input, leaseFence, preparation) {
|
|
50
|
+
if (state.config.workspaceMode !== 'multi')
|
|
51
|
+
throw new WorkspaceContextError('multi_workspace_unavailable');
|
|
52
|
+
const parsed = workspaceRegistrationSchema.safeParse(input);
|
|
53
|
+
if (!parsed.success)
|
|
54
|
+
throw new WorkspaceContextError('invalid_workspace_registration');
|
|
55
|
+
const binding = parsed.data;
|
|
56
|
+
return state.journal.exclusive(`workspace:${binding.workspaceId}`, async () => {
|
|
57
|
+
if (state.journal.destroyed())
|
|
58
|
+
throw new WorkspaceContextError('runtime_destroyed');
|
|
59
|
+
if (leaseFence < state.acceptedLeaseFence)
|
|
60
|
+
throw new WorkspaceContextError('stale_lease');
|
|
61
|
+
state.acceptedLeaseFence = leaseFence;
|
|
62
|
+
await state.journal.setAcceptedLeaseFence(leaseFence);
|
|
63
|
+
const existing = state.journal.getWorkspace(binding.workspaceId);
|
|
64
|
+
if (existing && JSON.stringify(existing) !== JSON.stringify(binding))
|
|
65
|
+
throw new WorkspaceContextError('workspace_binding_conflict');
|
|
66
|
+
let root;
|
|
67
|
+
try {
|
|
68
|
+
root = await checkedDirectory(state, binding.workspaceId);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (existing && error instanceof WorkspaceContextError && error.message === 'workspace_not_prepared')
|
|
72
|
+
throw new WorkspaceContextError('workspace_recovery_required');
|
|
73
|
+
if (!preparation || !(error instanceof WorkspaceContextError) || error.message !== 'workspace_not_prepared')
|
|
74
|
+
throw error;
|
|
75
|
+
await prepareWorkspaceCheckout(state, binding, preparation, leaseFence);
|
|
76
|
+
root = await checkedDirectory(state, binding.workspaceId);
|
|
77
|
+
}
|
|
78
|
+
const git = async (...args) => (await exec('git', ['-C', root, ...args], {
|
|
79
|
+
timeout: 5000,
|
|
80
|
+
maxBuffer: 64 * 1024,
|
|
81
|
+
env: {
|
|
82
|
+
PATH: process.env.PATH,
|
|
83
|
+
HOME: process.env.HOME,
|
|
84
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
85
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
86
|
+
},
|
|
87
|
+
})).stdout.trim();
|
|
88
|
+
try {
|
|
89
|
+
if ((await realpath(await git('rev-parse', '--show-toplevel'))) !== root ||
|
|
90
|
+
(await git('remote', 'get-url', 'origin')) !== binding.repositoryUrl ||
|
|
91
|
+
(await git('symbolic-ref', '--short', 'HEAD')) !== binding.branchName) {
|
|
92
|
+
throw new WorkspaceContextError('workspace_checkout_mismatch');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof WorkspaceContextError)
|
|
97
|
+
throw error;
|
|
98
|
+
throw new WorkspaceContextError('workspace_not_prepared');
|
|
99
|
+
}
|
|
100
|
+
if (state.journal.destroyed())
|
|
101
|
+
throw new WorkspaceContextError('runtime_destroyed');
|
|
102
|
+
if (leaseFence < state.acceptedLeaseFence)
|
|
103
|
+
throw new WorkspaceContextError('stale_lease');
|
|
104
|
+
await state.journal.putWorkspace(binding);
|
|
105
|
+
return binding;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readWorkspaceFile } from './files.js';
|
|
2
|
+
import { revisionScope, workspaceRoot } from './registry.js';
|
|
3
|
+
/** Observe native/manual edits before checking an editor's conditional write. */
|
|
4
|
+
export async function withObservedFile(state, workspaceId, path, action) {
|
|
5
|
+
const scope = revisionScope(state, workspaceId);
|
|
6
|
+
return state.journal.exclusive(`file:${scope ?? 'single'}:${path}`, async () => {
|
|
7
|
+
const records = state.journal.fileRevisions(scope);
|
|
8
|
+
const previous = records[path];
|
|
9
|
+
let file;
|
|
10
|
+
try {
|
|
11
|
+
file = await readWorkspaceFile(await workspaceRoot(state, workspaceId), path, records);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code !== 'ENOENT')
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
const hash = file?.contentHash ?? '';
|
|
18
|
+
const revision = previous ? previous.revision + (previous.contentHash === hash ? 0 : 1) : 0;
|
|
19
|
+
if ((!previous && file) || (previous && previous.contentHash !== hash)) {
|
|
20
|
+
await state.journal.putFileRevision(path, { revision, contentHash: hash }, scope);
|
|
21
|
+
}
|
|
22
|
+
return action({ file: file ? { ...file, revision } : undefined, revision });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export async function readObservedFile(state, workspaceId, path) {
|
|
26
|
+
return withObservedFile(state, workspaceId, path, async ({ file }) => {
|
|
27
|
+
if (!file)
|
|
28
|
+
throw Object.assign(new Error('file_not_found'), { code: 'ENOENT' });
|
|
29
|
+
return file;
|
|
30
|
+
});
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.58",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"dist",
|
|
15
15
|
"templates",
|
|
16
16
|
"project-template",
|
|
17
|
+
"project-template-v2",
|
|
17
18
|
"engine-packs",
|
|
18
19
|
"project-templates",
|
|
19
20
|
"REMIX.md",
|
|
@@ -35,6 +36,8 @@
|
|
|
35
36
|
"test:agent:gate": "node scripts/run-agent-tests.mjs gate",
|
|
36
37
|
"test:agent:vlm": "node scripts/run-agent-tests.mjs vlm",
|
|
37
38
|
"test:agent:stop": "node scripts/run-agent-tests.mjs stop",
|
|
39
|
+
"generate:platform-skills": "node scripts/generate-platform-skills.mjs",
|
|
40
|
+
"verify:platform-skills": "node scripts/generate-platform-skills.mjs --check",
|
|
38
41
|
"verify:agent-stop": "node scripts/verify-agent-stop.mjs",
|
|
39
42
|
"verify-sprite-overlay": "node scripts/verify-sprite-overlay.mjs",
|
|
40
43
|
"link": "node scripts/bump-local.js && pnpm build && npm link",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: artist
|
|
3
|
+
description: 'Artist: produces scoped visual intent, composite references, and asset-state context when dispatched.'
|
|
4
|
+
tools:
|
|
5
|
+
- 'Read'
|
|
6
|
+
- 'Write'
|
|
7
|
+
- 'Grep'
|
|
8
|
+
- 'Glob'
|
|
9
|
+
- 'Skill'
|
|
10
|
+
- 'Bash'
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Artist — Role Contract
|
|
14
|
+
|
|
15
|
+
## Role Essence
|
|
16
|
+
|
|
17
|
+
You produce scoped visual direction and visual handoff context.
|
|
18
|
+
|
|
19
|
+
Your stable responsibility is clarifying what the playable should look and feel like through visual intent, MC (Master Composite, 主合成图) references, ASR (Asset State Reference, 资产状态参考) context, and artifact refs.
|
|
20
|
+
|
|
21
|
+
You are not the gameplay designer, runtime implementer, Technical Artist (TA, 技术美术), reviewer, or orchestrator.
|
|
22
|
+
|
|
23
|
+
## Orchestration Contract
|
|
24
|
+
|
|
25
|
+
You run inside a pipeline orchestration system. The orchestrator dispatches work to you and manages the overall flow — you only see and act on your assigned workticket.
|
|
26
|
+
|
|
27
|
+
**On start** — read your workticket:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm run state:get -- --json dispatch
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Key fields:
|
|
34
|
+
|
|
35
|
+
- `role` — must be `artist` for you to act; if it is empty or another role, stop and report no assigned workticket
|
|
36
|
+
- `goal` / `guidance` — what you are here to do and how
|
|
37
|
+
- `allowedFiles` — the only paths you may write (hard boundary)
|
|
38
|
+
- `forbiddenTasks` — things explicitly out of scope for this run
|
|
39
|
+
- `acceptance` — the conditions that define done; stop when these are met, no more
|
|
40
|
+
- `activeTodoId` — the todo or stage task assigned to you this run
|
|
41
|
+
|
|
42
|
+
**Before STOP** — record what you did:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm run handoff:append -- role=artist summary="<what you did in one sentence>" artifactRefs="<produced files>"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If the work failed or is blocked, include the blocker in `summary` and pass `blockers="<reason>"`.
|
|
49
|
+
The orchestrator reads your handoff and decides todo status and what comes next — do not call `todo:set`, route the next role, or promote yourself to orchestrator.
|
|
50
|
+
|
|
51
|
+
Additional permissions are granted per-run by the orchestrator in the flow notes. Do not use them unless explicitly listed there.
|
|
52
|
+
|
|
53
|
+
## Inputs
|
|
54
|
+
|
|
55
|
+
- `docs/design-brief.md` or `docs/style-exploration.md`
|
|
56
|
+
- existing visual references in scope
|
|
57
|
+
|
|
58
|
+
## Outputs
|
|
59
|
+
|
|
60
|
+
- style intent summary
|
|
61
|
+
- MC reference or storyboard note
|
|
62
|
+
- ASR context / state reference
|
|
63
|
+
- artifact refs that already exist or were explicitly produced
|
|
64
|
+
|
|
65
|
+
Keep outputs useful to the next role, not decorative.
|
|
66
|
+
|
|
67
|
+
## Domain References
|
|
68
|
+
|
|
69
|
+
Read the one matching the task; they hold the deep how-to, not just inspiration:
|
|
70
|
+
|
|
71
|
+
- `refs/artist-art-style-catalog.md` — per-style visual definitions; pick and ground a style direction.
|
|
72
|
+
- `refs/artist-master-composite-recipes.md` — MC prompt recipes and approved models per game type.
|
|
73
|
+
- `refs/artist-color-audio-recipes.md` — color schemes and audio mood arcs by genre.
|
|
74
|
+
- `refs/artist-dimension-axis.md` — what MC/ASR to deliver per dimension (flat 2D vs 3D).
|
|
75
|
+
|
|
76
|
+
## Forbidden Ownership
|
|
77
|
+
|
|
78
|
+
You must not:
|
|
79
|
+
|
|
80
|
+
- define gameplay mechanics
|
|
81
|
+
- implement runtime files
|
|
82
|
+
- write broad style essays unrelated to the task
|