@playcraft/cli 0.1.66 → 0.1.67
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/dist/assets/client.js +4 -2
- package/dist/assets/messages.js +2 -0
- package/dist/audio-source/messages.js +35 -0
- package/dist/commands/assets.js +1 -1
- package/dist/commands/audio-source.js +109 -0
- package/dist/commands/audio.js +11 -4
- package/dist/commands/build.js +4 -2
- package/dist/commands/tools-context.js +46 -0
- package/dist/commands/tools.js +2 -0
- package/dist/project-skills/builtin-import.js +69 -22
- package/dist/project-skills/commands.js +11 -2
- package/dist/project-skills/lifecycle.js +1 -1
- package/dist/project-skills/local-store.js +90 -31
- package/dist/project-skills/messages.js +16 -0
- package/dist/project-skills/migrate-lock.js +166 -0
- package/dist/project-skills/remote-cache.js +36 -5
- package/dist/utils/agent-api-client.js +18 -1
- package/dist/utils/agent-generation-provenance.js +8 -2
- package/dist/utils/audio-export.js +249 -0
- package/dist/utils/audio-processor.js +22 -99
- package/dist/utils/execution-context-bridge.js +28 -0
- package/dist/workspace-runtime/batch/control.js +13 -2
- package/dist/workspace-runtime/batch/generate.js +43 -2
- package/dist/workspace-runtime/batch/prepare.js +4 -0
- package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
- package/dist/workspace-runtime/batch/source-capture.js +6 -8
- package/dist/workspace-runtime/codex/app-server.js +26 -2
- package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
- package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
- package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
- package/dist/workspace-runtime/codex/model-broker.js +15 -2
- package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
- package/dist/workspace-runtime/previews/finalize.js +1 -1
- package/dist/workspace-runtime/previews/output.js +4 -1
- package/dist/workspace-runtime/previews/publish.js +5 -2
- package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
- package/dist/workspace-runtime/serve.js +6 -0
- package/dist/workspace-runtime/server/accept-operation.js +85 -13
- package/dist/workspace-runtime/server/audio-assets.js +211 -0
- package/dist/workspace-runtime/server/audio-export.js +136 -0
- package/dist/workspace-runtime/server/directory-access.js +222 -67
- package/dist/workspace-runtime/server/dispatch.js +252 -29
- package/dist/workspace-runtime/server/http.js +97 -3
- package/dist/workspace-runtime/server/operation-record.js +1 -1
- package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
- package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
- package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
- package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
- package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +76 -0
- package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
- package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
- package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
- package/dist/workspace-runtime/workspaces/audio-assets/service.js +327 -0
- package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
- package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
- package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
- package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
- package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
- package/dist/workspace-runtime/workspaces/files.js +3 -5
- package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
- package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
- package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
- package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
- package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
- package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
- package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
- package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
- package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
- package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
- package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
- package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
- package/dist/workspace-runtime/workspaces/skills.js +48 -13
- package/package.json +6 -4
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
- package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
- package/project-template/vite.config.ts +1 -1
- package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
- package/project-template-v2/.playcraft/skills.lock.json +9 -2
- package/project-template-v2/vite.config.ts +1 -1
- package/project-templates/empty/AGENTS.md +2 -0
- package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
- package/project-templates/phaser/vite.config.ts +1 -1
- package/project-templates/playable-template-phaser/vite.config.ts +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
- package/project-templates/threejs/vite.config.ts +1 -1
- package/templates/preview-runtime/v1/README.md +12 -0
- package/templates/preview-runtime/v1/build-preview.mjs +17 -8
- package/templates/preview-runtime/v1/manifest.json +9 -6
- package/templates/preview-runtime/v1/release-guard.mjs +1 -1
- package/templates/preview-runtime/v1/runtime.d.mts +1 -3
- package/templates/preview-runtime/v1/runtime.d.ts +1 -3
- package/templates/preview-runtime/v1/runtime.mjs +1142 -257
- package/templates/preview-runtime/v1/types/host.d.ts +29 -0
- package/templates/preview-runtime/v1/types/index.d.ts +6 -0
- package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
|
@@ -27,12 +27,34 @@ async function change(directory, action) {
|
|
|
27
27
|
await rm(lease);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
async function
|
|
30
|
+
async function snapshotBytes(directory) {
|
|
31
|
+
const declaration = await readFile(join(directory, PROJECT_SKILLS_DECLARATION_PATH))
|
|
32
|
+
.catch((error) => { if (error.code === 'ENOENT')
|
|
33
|
+
return null; throw error; });
|
|
34
|
+
return { declaration, lock: await readProjectSkillsLockFile(directory) };
|
|
35
|
+
}
|
|
36
|
+
async function writeSnapshot(directory, snapshot, expected) {
|
|
31
37
|
validateProjectSkillsSnapshot(snapshot.declaration, snapshot.lock);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
const staged = [];
|
|
39
|
+
try {
|
|
40
|
+
for (const [file, value] of [[PROJECT_SKILLS_DECLARATION_PATH, snapshot.declaration], [PROJECT_SKILLS_LOCK_PATH, snapshot.lock]]) {
|
|
41
|
+
const target = join(directory, file), temporary = `${target}.${randomUUID()}.tmp`;
|
|
42
|
+
staged.push({ target, temporary });
|
|
43
|
+
await writeFile(temporary, `${canonicalProjectSkillsJson(value)}\n`, { flag: 'wx', mode: 0o644 });
|
|
44
|
+
}
|
|
45
|
+
if (expected) {
|
|
46
|
+
const current = await snapshotBytes(directory);
|
|
47
|
+
if ((current.declaration === null) !== (expected.declaration === null)
|
|
48
|
+
|| current.declaration && expected.declaration && !current.declaration.equals(expected.declaration)
|
|
49
|
+
|| current.lock !== expected.lock)
|
|
50
|
+
throw new Error('skills_revision_conflict');
|
|
51
|
+
}
|
|
52
|
+
for (const { temporary, target } of staged)
|
|
53
|
+
await rename(temporary, target);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
for (const { temporary } of staged)
|
|
57
|
+
await rm(temporary, { force: true });
|
|
36
58
|
}
|
|
37
59
|
}
|
|
38
60
|
/** Initializes an unmanaged checkout with one complete source and release snapshot under the existing writer lease. */
|
|
@@ -55,7 +77,7 @@ export async function initializeLocalProjectSkills(directory, cache, options) {
|
|
|
55
77
|
let configurationWriteStarted = false;
|
|
56
78
|
try {
|
|
57
79
|
await options.prepareSources?.();
|
|
58
|
-
const snapshot = buildLocalProjectSkillsSnapshot(await inspectProjectSkills(directory));
|
|
80
|
+
const snapshot = buildLocalProjectSkillsSnapshot(await inspectProjectSkills(directory), undefined, { lockVersion: options.platform?.lock.lockVersion ?? 3 });
|
|
59
81
|
for (const entry of snapshot.declaration.entries)
|
|
60
82
|
if (entry.source.kind === 'repository' && options.enabledRoots.has(entry.source.root))
|
|
61
83
|
entry.enabled = true;
|
|
@@ -64,7 +86,9 @@ export async function initializeLocalProjectSkills(directory, cache, options) {
|
|
|
64
86
|
if (platform.declaration.entries.some(entry => entry.source.kind !== 'registry'))
|
|
65
87
|
throw new Error('workspace_skills_invalid');
|
|
66
88
|
snapshot.declaration.entries.push(...platform.declaration.entries);
|
|
67
|
-
snapshot.lock.
|
|
89
|
+
if (snapshot.lock.lockVersion !== platform.lock.lockVersion)
|
|
90
|
+
throw new Error('workspace_skills_lock_version_mismatch');
|
|
91
|
+
snapshot.lock = { ...snapshot.lock, entries: [...snapshot.lock.entries, ...platform.lock.entries] };
|
|
68
92
|
}
|
|
69
93
|
snapshot.lock.declarationHash = declarationDigest(snapshot.declaration);
|
|
70
94
|
validateProjectSkillsSnapshot(snapshot.declaration, snapshot.lock);
|
|
@@ -73,13 +97,16 @@ export async function initializeLocalProjectSkills(directory, cache, options) {
|
|
|
73
97
|
for (const artifact of [...snapshot.artifacts, ...(options.platform?.artifacts ?? [])]) {
|
|
74
98
|
if (createHash('sha256').update(artifact.archive).digest('hex') !== artifact.artifactHash)
|
|
75
99
|
throw new Error('workspace_skills_digest_mismatch');
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
const contentHash = 'contentHash' in artifact ? artifact.contentHash : undefined;
|
|
101
|
+
for (const name of [`${artifact.artifactHash}.tar.gz`, ...(contentHash ? [`content-${contentHash}.tar.gz`] : [])]) {
|
|
102
|
+
const target = join(cache, name), temporary = `${target}.${randomUUID()}.tmp`;
|
|
103
|
+
try {
|
|
104
|
+
await writeFile(temporary, artifact.archive, { flag: 'wx', mode: 0o600 });
|
|
105
|
+
await rename(temporary, target);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await rm(temporary, { force: true });
|
|
109
|
+
}
|
|
83
110
|
}
|
|
84
111
|
}
|
|
85
112
|
configurationWriteStarted = true;
|
|
@@ -107,8 +134,9 @@ export async function initializeLocalProjectSkills(directory, cache, options) {
|
|
|
107
134
|
}
|
|
108
135
|
});
|
|
109
136
|
}
|
|
110
|
-
export async function syncLocalProjectSkills(directory, cache = projectSkillsCache()) {
|
|
137
|
+
export async function syncLocalProjectSkills(directory, cache = projectSkillsCache(), options = {}) {
|
|
111
138
|
return change(directory, async () => {
|
|
139
|
+
const original = await snapshotBytes(directory);
|
|
112
140
|
let previous;
|
|
113
141
|
try {
|
|
114
142
|
previous = await readProjectSkillsLockFile(directory) ?? undefined;
|
|
@@ -117,7 +145,7 @@ export async function syncLocalProjectSkills(directory, cache = projectSkillsCac
|
|
|
117
145
|
if (error.code !== 'ENOENT')
|
|
118
146
|
throw error;
|
|
119
147
|
}
|
|
120
|
-
const snapshot = buildLocalProjectSkillsSnapshot(await inspectProjectSkills(directory), previous);
|
|
148
|
+
const snapshot = buildLocalProjectSkillsSnapshot(await inspectProjectSkills(directory), previous, options);
|
|
121
149
|
await mkdir(cache, { recursive: true, mode: 0o700 });
|
|
122
150
|
for (const artifact of snapshot.artifacts) {
|
|
123
151
|
const target = join(cache, `${artifact.artifactHash}.tar.gz`);
|
|
@@ -125,18 +153,19 @@ export async function syncLocalProjectSkills(directory, cache = projectSkillsCac
|
|
|
125
153
|
await writeFile(temporary, artifact.archive, { flag: 'wx', mode: 0o600 });
|
|
126
154
|
await rename(temporary, target);
|
|
127
155
|
}
|
|
128
|
-
await writeSnapshot(directory, snapshot);
|
|
156
|
+
await writeSnapshot(directory, snapshot, original);
|
|
129
157
|
return { declaration: snapshot.declaration, lock: snapshot.lock };
|
|
130
158
|
});
|
|
131
159
|
}
|
|
132
160
|
export async function enableLocalProjectSkill(directory, entryKey, enabled) {
|
|
133
161
|
return change(directory, async () => {
|
|
162
|
+
const original = await snapshotBytes(directory);
|
|
134
163
|
const previous = await readLocalProjectSkillSnapshot(directory);
|
|
135
164
|
const next = setProjectSkillEnabled(previous.declaration, previous.lock, entryKey, enabled);
|
|
136
165
|
// Disabled required dependencies are shown immediately, before changing either file.
|
|
137
166
|
for (const surface of ['portal-agent', 'sandbox-agent', 'build-time'])
|
|
138
167
|
effectiveProjectSkills(next.declaration, next.lock, surface);
|
|
139
|
-
await writeSnapshot(directory, next);
|
|
168
|
+
await writeSnapshot(directory, next, original);
|
|
140
169
|
return next;
|
|
141
170
|
});
|
|
142
171
|
}
|
|
@@ -150,27 +179,54 @@ export async function installFrozenProjectSkills(directory, cache = projectSkill
|
|
|
150
179
|
for (const entry of effective) {
|
|
151
180
|
const cached = join(cache, `${entry.revision.artifactHash}.tar.gz`);
|
|
152
181
|
let archive;
|
|
182
|
+
let contentAddressed = false;
|
|
153
183
|
try {
|
|
154
184
|
archive = await readFile(cached);
|
|
155
185
|
}
|
|
156
186
|
catch (error) {
|
|
157
|
-
if (error.code !== 'ENOENT'
|
|
187
|
+
if (error.code !== 'ENOENT')
|
|
158
188
|
throw error;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
if (snapshot.lock.lockVersion === 4) {
|
|
190
|
+
try {
|
|
191
|
+
archive = await readFile(join(cache, `content-${entry.revision.contentHash}.tar.gz`));
|
|
192
|
+
contentAddressed = true;
|
|
193
|
+
}
|
|
194
|
+
catch (contentError) {
|
|
195
|
+
if (contentError.code !== 'ENOENT')
|
|
196
|
+
throw contentError;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!contentAddressed) {
|
|
200
|
+
if (entry.revision.kind !== 'source')
|
|
201
|
+
throw error;
|
|
202
|
+
const declaration = snapshot.declaration.entries.find(item => item.entryKey === entry.entryKey);
|
|
203
|
+
const source = await readProjectSkillSource(directory, declaration);
|
|
204
|
+
if (source.sourceTreeHash !== entry.sourceTreeHash)
|
|
205
|
+
throw new Error(`Source digest mismatch: ${entry.entryKey}`);
|
|
206
|
+
if (snapshot.lock.lockVersion === 4) {
|
|
207
|
+
if (source.sourceTreeHash !== entry.revision.contentHash)
|
|
208
|
+
throw new Error(`Source digest mismatch: ${entry.entryKey}`);
|
|
209
|
+
for (const file of source.files) {
|
|
210
|
+
const target = join(generation, entry.entryKey, file.path);
|
|
211
|
+
await mkdir(join(target, '..'), { recursive: true });
|
|
212
|
+
await writeFile(target, file.content, { flag: 'wx', mode: 0o444 });
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
archive = createCanonicalBundleArchive(source.files.map(file => ({ path: file.path, data: Buffer.from(file.content) })));
|
|
217
|
+
if (createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
|
|
218
|
+
throw new Error(`Artifact digest mismatch: ${entry.entryKey}`);
|
|
219
|
+
await writeFile(cached, archive, { flag: 'wx', mode: 0o600 }).catch(error => { if (error.code !== 'EEXIST')
|
|
220
|
+
throw error; });
|
|
221
|
+
}
|
|
168
222
|
}
|
|
169
|
-
if (createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
|
|
223
|
+
if (!contentAddressed && createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
|
|
170
224
|
throw new Error(`Artifact digest mismatch: ${entry.entryKey}`);
|
|
171
225
|
const extracted = extractBundleArchive(archive);
|
|
172
226
|
if (extracted.issues.length)
|
|
173
227
|
throw new Error(JSON.stringify(extracted.issues));
|
|
228
|
+
if (snapshot.lock.lockVersion === 4 && sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== entry.revision.contentHash)
|
|
229
|
+
throw new Error(`Content digest mismatch: ${entry.entryKey}`);
|
|
174
230
|
if (entry.revision.kind === 'source' && sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== entry.sourceTreeHash)
|
|
175
231
|
throw new Error(`Source digest mismatch: ${entry.entryKey}`);
|
|
176
232
|
for (const file of extracted.files) {
|
|
@@ -193,15 +249,18 @@ export async function explainLocalProjectSkills(directory) {
|
|
|
193
249
|
export async function adoptServerProjectSkillSnapshot(directory, expected, response, checkpoint) {
|
|
194
250
|
const next = validateProjectSkillsSnapshot(response.declaration, response.lock);
|
|
195
251
|
const withoutIdentity = (snapshot) => ({
|
|
252
|
+
lockVersion: snapshot.lock.lockVersion,
|
|
196
253
|
declaration: { ...snapshot.declaration, entries: snapshot.declaration.entries.map(entry => ({ ...entry,
|
|
197
254
|
source: entry.source.kind === 'repository' ? { kind: entry.source.kind, root: entry.source.root } : entry.source })) },
|
|
198
255
|
entries: snapshot.lock.entries.map(entry => ({ entryKey: entry.entryKey, sourceTreeHash: entry.sourceTreeHash,
|
|
199
256
|
dependencies: entry.dependencies,
|
|
200
|
-
revision: entry.revision.kind === 'source' ? {
|
|
257
|
+
revision: { ...entry.revision, ...(entry.revision.kind === 'source' ? { skillRef: undefined } : {}),
|
|
258
|
+
...(snapshot.lock.lockVersion === 4 ? { artifactHash: undefined } : {}) } })),
|
|
201
259
|
});
|
|
202
260
|
if (canonicalProjectSkillsJson(withoutIdentity(expected)) !== canonicalProjectSkillsJson(withoutIdentity(next)))
|
|
203
261
|
throw new Error(t('reconcileChanged'));
|
|
204
262
|
return change(directory, async () => {
|
|
263
|
+
const original = await snapshotBytes(directory);
|
|
205
264
|
const current = await readLocalProjectSkillSnapshot(directory);
|
|
206
265
|
if (canonicalProjectSkillsJson(current) !== canonicalProjectSkillsJson(expected))
|
|
207
266
|
throw new Error(t('reconcileChanged'));
|
|
@@ -215,7 +274,7 @@ export async function adoptServerProjectSkillSnapshot(directory, expected, respo
|
|
|
215
274
|
checkpoint();
|
|
216
275
|
const changed = canonicalProjectSkillsJson(current) !== canonicalProjectSkillsJson(next);
|
|
217
276
|
if (changed)
|
|
218
|
-
await writeSnapshot(directory, next);
|
|
277
|
+
await writeSnapshot(directory, next, original);
|
|
219
278
|
return { changed };
|
|
220
279
|
});
|
|
221
280
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
const messages = {
|
|
2
2
|
en: {
|
|
3
|
+
migrateLockV4: 'Upgrade a v3 lock using verified original artifacts and retain a backup',
|
|
4
|
+
migrationMissingArchive: 'Cannot upgrade: the exact locked artifact is missing',
|
|
5
|
+
migrationInvalidArchive: 'Cannot upgrade: the locked artifact is invalid or changed',
|
|
6
|
+
migrationSourceChanged: 'Cannot upgrade: the locked source files changed',
|
|
7
|
+
migrationConcurrentChanged: 'Concurrent lock change detected; original file preserved for recovery',
|
|
8
|
+
lockVersion: 'Lock format for a new project: 3 or 4 (existing locks require explicit migration)',
|
|
9
|
+
invalidLockVersion: 'Lock format must be 3 or 4',
|
|
10
|
+
migrateLockRequired: 'The existing lock cannot be converted by sync; use skills project migrate-lock-v4.',
|
|
3
11
|
importBuiltins: 'Import published platform skills in Cloud Workspace; preserve exact existing revisions',
|
|
4
12
|
platformRequired: 'Use Cloud Workspace’s Skills panel to import published platform skills. Local template import is unavailable.',
|
|
5
13
|
expectedDeclarationHash: 'Expected declaration digest from the current skill lock',
|
|
@@ -37,6 +45,14 @@ const messages = {
|
|
|
37
45
|
changed: 'Source files have changed since the saved revision',
|
|
38
46
|
},
|
|
39
47
|
zh: {
|
|
48
|
+
migrateLockV4: '使用已核验的原始技能包升级 v3 锁并保留备份',
|
|
49
|
+
migrationMissingArchive: '无法升级:缺少锁定的原始技能包',
|
|
50
|
+
migrationInvalidArchive: '无法升级:锁定的技能包无效或已更改',
|
|
51
|
+
migrationSourceChanged: '无法升级:锁定的技能源文件已更改',
|
|
52
|
+
migrationConcurrentChanged: '检测到技能锁并发变更,原文件已保留供恢复',
|
|
53
|
+
lockVersion: '新项目的技能锁格式:3 或 4(已有锁需要显式迁移)',
|
|
54
|
+
invalidLockVersion: '技能锁格式必须是 3 或 4',
|
|
55
|
+
migrateLockRequired: '不能通过 sync 转换已有技能锁,请使用 skills project migrate-lock-v4。',
|
|
40
56
|
importBuiltins: '在云工作区导入已发布平台技能,保留已有精确修订',
|
|
41
57
|
platformRequired: '请在云工作区的技能面板导入已发布平台技能;本地模板导入已停用。',
|
|
42
58
|
expectedDeclarationHash: '当前技能锁中的声明摘要',
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { link, lstat, mkdir, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { canonicalProjectSkillsJson, dependencyDigest, extractBundleArchive, readProjectSkillSource, readProjectSkillsLockFile, sourceTreeDigest, validateProjectSkillsSnapshot, PROJECT_SKILLS_DECLARATION_PATH, PROJECT_SKILLS_LOCK_PATH, } from '@playcraft/common/project-skills';
|
|
6
|
+
import { projectSkillsCache } from './local-store.js';
|
|
7
|
+
import { projectSkillsMessage as t } from './messages.js';
|
|
8
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
9
|
+
const MAX_ARCHIVE = 80 * 1024 * 1024;
|
|
10
|
+
async function readExactArchive(path, expectedHash) {
|
|
11
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK).catch(error => {
|
|
12
|
+
if (error.code === 'ENOENT')
|
|
13
|
+
return undefined;
|
|
14
|
+
throw error;
|
|
15
|
+
});
|
|
16
|
+
if (!handle)
|
|
17
|
+
return undefined;
|
|
18
|
+
try {
|
|
19
|
+
const stat = await handle.stat();
|
|
20
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > MAX_ARCHIVE)
|
|
21
|
+
throw new Error(t('migrationInvalidArchive'));
|
|
22
|
+
const bytes = await handle.readFile();
|
|
23
|
+
if (bytes.length !== stat.size || sha256(bytes) !== expectedHash)
|
|
24
|
+
throw new Error(t('migrationInvalidArchive'));
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await handle.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Upgrade only from verified v3 bytes. No release is replaced based on name and version alone. */
|
|
32
|
+
export async function migrateProjectSkillsLockV4(directory, cache = projectSkillsCache()) {
|
|
33
|
+
const management = join(directory, '.playcraft');
|
|
34
|
+
const managementStat = await lstat(management);
|
|
35
|
+
if (!managementStat.isDirectory() || managementStat.isSymbolicLink())
|
|
36
|
+
throw new Error('workspace_skills_invalid');
|
|
37
|
+
const lease = join(management, 'skills-write.lock');
|
|
38
|
+
await writeFile(lease, `${process.pid}\n`, { flag: 'wx', mode: 0o600 });
|
|
39
|
+
let temporary;
|
|
40
|
+
try {
|
|
41
|
+
const declarationPath = join(directory, PROJECT_SKILLS_DECLARATION_PATH);
|
|
42
|
+
const declarationStat = await lstat(declarationPath);
|
|
43
|
+
if (!declarationStat.isFile() || declarationStat.isSymbolicLink() || declarationStat.size > 1024 * 1024)
|
|
44
|
+
throw new Error('workspace_skills_invalid');
|
|
45
|
+
const declarationBytes = await readFile(declarationPath);
|
|
46
|
+
const lockPath = join(directory, PROJECT_SKILLS_LOCK_PATH);
|
|
47
|
+
const lockStat = await lstat(lockPath);
|
|
48
|
+
if (!lockStat.isFile() || lockStat.isSymbolicLink() || lockStat.nlink !== 1 || lockStat.size > 1024 * 1024)
|
|
49
|
+
throw new Error('workspace_skills_invalid');
|
|
50
|
+
const lockMode = lockStat.mode & 0o777;
|
|
51
|
+
const lockBytes = await readProjectSkillsLockFile(directory);
|
|
52
|
+
const { declaration, lock } = validateProjectSkillsSnapshot(declarationBytes.toString('utf8'), lockBytes);
|
|
53
|
+
if (lock.lockVersion === 4)
|
|
54
|
+
return { changed: false, lockVersion: 4 };
|
|
55
|
+
const artifactsDir = join(management, 'skill-artifacts');
|
|
56
|
+
await mkdir(artifactsDir, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST')
|
|
57
|
+
throw error; });
|
|
58
|
+
const artifactsStat = await lstat(artifactsDir);
|
|
59
|
+
if (!artifactsStat.isDirectory() || artifactsStat.isSymbolicLink())
|
|
60
|
+
throw new Error('workspace_skills_invalid');
|
|
61
|
+
const entries = [];
|
|
62
|
+
for (const entry of lock.entries) {
|
|
63
|
+
let contentHash;
|
|
64
|
+
if (entry.revision.kind === 'source') {
|
|
65
|
+
const declared = declaration.entries.find(item => item.entryKey === entry.entryKey);
|
|
66
|
+
const source = await readProjectSkillSource(directory, declared);
|
|
67
|
+
if (source.sourceTreeHash !== entry.sourceTreeHash)
|
|
68
|
+
throw new Error(`${t('migrationSourceChanged')}: ${entry.entryKey}`);
|
|
69
|
+
contentHash = source.sourceTreeHash;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const name = `${entry.revision.artifactHash}.tar.gz`;
|
|
73
|
+
const archive = await readExactArchive(join(artifactsDir, name), entry.revision.artifactHash)
|
|
74
|
+
?? await readExactArchive(join(cache, name), entry.revision.artifactHash);
|
|
75
|
+
if (!archive)
|
|
76
|
+
throw new Error(`${t('migrationMissingArchive')}: ${entry.entryKey}`);
|
|
77
|
+
const extracted = extractBundleArchive(archive);
|
|
78
|
+
if (extracted.issues.length || !extracted.files.some(file => file.path === 'SKILL.md'))
|
|
79
|
+
throw new Error(t('migrationInvalidArchive'));
|
|
80
|
+
contentHash = sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data })));
|
|
81
|
+
}
|
|
82
|
+
entries.push({ ...entry, revision: { ...entry.revision, contentHash } });
|
|
83
|
+
}
|
|
84
|
+
for (const entry of entries)
|
|
85
|
+
entry.dependencyLockHash = dependencyDigest(entries, entry.dependencies);
|
|
86
|
+
const next = validateProjectSkillsSnapshot(declaration, { ...lock, lockVersion: 4, entries }).lock;
|
|
87
|
+
const old = Buffer.from(lockBytes);
|
|
88
|
+
const backup = join(artifactsDir, `skills.lock.v3-${sha256(old)}.json`);
|
|
89
|
+
try {
|
|
90
|
+
await writeFile(backup, old, { flag: 'wx', mode: 0o600 });
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.code !== 'EEXIST')
|
|
94
|
+
throw error;
|
|
95
|
+
const saved = await readExactArchive(backup, sha256(old));
|
|
96
|
+
if (!saved || !saved.equals(old))
|
|
97
|
+
throw new Error('skills_revision_conflict');
|
|
98
|
+
}
|
|
99
|
+
temporary = `${lockPath}.${randomUUID()}.tmp`;
|
|
100
|
+
const staged = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, lockMode);
|
|
101
|
+
try {
|
|
102
|
+
await staged.writeFile(`${canonicalProjectSkillsJson(next)}\n`);
|
|
103
|
+
// The process umask may remove group write access on creation.
|
|
104
|
+
await staged.chmod(lockMode);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await staged.close();
|
|
108
|
+
}
|
|
109
|
+
for (const declared of declaration.entries) {
|
|
110
|
+
if (declared.source.kind !== 'repository')
|
|
111
|
+
continue;
|
|
112
|
+
const source = await readProjectSkillSource(directory, declared);
|
|
113
|
+
if (source.sourceTreeHash !== lock.entries.find(item => item.entryKey === declared.entryKey)?.sourceTreeHash) {
|
|
114
|
+
throw new Error(`${t('migrationSourceChanged')}: ${declared.entryKey}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!(await readFile(declarationPath)).equals(declarationBytes) || await readProjectSkillsLockFile(directory) !== lockBytes) {
|
|
118
|
+
throw new Error('skills_revision_conflict');
|
|
119
|
+
}
|
|
120
|
+
const target = lockPath;
|
|
121
|
+
const recovery = join(artifactsDir, `skills.lock.v3-recovery-${randomUUID()}.json`);
|
|
122
|
+
// Reserve a unique destination; a cross-device rename fails before the original is moved.
|
|
123
|
+
await writeFile(recovery, '', { flag: 'wx', mode: 0o600 });
|
|
124
|
+
try {
|
|
125
|
+
await rename(target, recovery);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
await rm(recovery, { force: true });
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
let published = false;
|
|
132
|
+
try {
|
|
133
|
+
// A noncooperative writer may have changed the inode after our previous comparison.
|
|
134
|
+
const moved = await readExactArchive(recovery, sha256(old));
|
|
135
|
+
const movedStat = await lstat(recovery);
|
|
136
|
+
if (!moved?.equals(old) || (movedStat.mode & 0o777) !== lockMode)
|
|
137
|
+
throw new Error('skills_revision_conflict');
|
|
138
|
+
// Linking is atomic and cannot overwrite a concurrently created target.
|
|
139
|
+
await link(temporary, target);
|
|
140
|
+
published = true;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
// Restore only into an absent target; always keep the moved inode for recovery.
|
|
144
|
+
try {
|
|
145
|
+
const saved = await lstat(recovery);
|
|
146
|
+
if (saved.isFile() && !saved.isSymbolicLink())
|
|
147
|
+
await link(recovery, target);
|
|
148
|
+
}
|
|
149
|
+
catch (restoreError) {
|
|
150
|
+
if (restoreError.code !== 'EEXIST')
|
|
151
|
+
throw restoreError;
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`${t('migrationConcurrentChanged')}: ${recovery}`, { cause: error });
|
|
154
|
+
}
|
|
155
|
+
if (published)
|
|
156
|
+
await rm(temporary, { force: true });
|
|
157
|
+
temporary = undefined;
|
|
158
|
+
// Keep the moved inode: a process that opened the old file may still write to it later.
|
|
159
|
+
return { changed: true, lockVersion: 4, backup, recovery };
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
if (temporary)
|
|
163
|
+
await rm(temporary, { force: true });
|
|
164
|
+
await rm(lease, { force: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir, writeFile, rename, readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { extractBundleArchive, sourceTreeDigest } from '@playcraft/common/project-skills';
|
|
4
5
|
import { PlayCraftHttpClient } from '../utils/playcraft-http-client.js';
|
|
5
6
|
import { readRemixLocalMeta } from '../remix/local-git.js';
|
|
6
7
|
import { readLocalProjectSkillSnapshot, projectSkillsCache } from './local-store.js';
|
|
@@ -15,15 +16,39 @@ export async function downloadLocalProjectSkillArtifacts(directory, cache = proj
|
|
|
15
16
|
let downloaded = 0;
|
|
16
17
|
for (const locked of snapshot.lock.entries) {
|
|
17
18
|
const file = join(cache, `${locked.revision.artifactHash}.tar.gz`);
|
|
19
|
+
const contentHash = snapshot.lock.lockVersion === 4 ? locked.revision.contentHash : undefined;
|
|
18
20
|
try {
|
|
19
|
-
|
|
21
|
+
const bytes = await readFile(file);
|
|
22
|
+
if (createHash('sha256').update(bytes).digest('hex') === locked.revision.artifactHash) {
|
|
23
|
+
if (contentHash) {
|
|
24
|
+
const extracted = extractBundleArchive(bytes);
|
|
25
|
+
if (extracted.issues.length || sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== contentHash)
|
|
26
|
+
throw new Error(t('downloadMismatch'));
|
|
27
|
+
}
|
|
20
28
|
continue;
|
|
29
|
+
}
|
|
21
30
|
}
|
|
22
31
|
catch (error) {
|
|
23
32
|
if (error.code !== 'ENOENT')
|
|
24
33
|
throw error;
|
|
25
34
|
}
|
|
26
|
-
const
|
|
35
|
+
const contentFile = contentHash ? join(cache, `content-${contentHash}.tar.gz`) : undefined;
|
|
36
|
+
if (contentFile) {
|
|
37
|
+
try {
|
|
38
|
+
const extracted = extractBundleArchive(await readFile(contentFile));
|
|
39
|
+
if (!extracted.issues.length && sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) === contentHash)
|
|
40
|
+
continue;
|
|
41
|
+
throw new Error(t('downloadMismatch'));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code !== 'ENOENT')
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const authorized = view.entries?.find(entry => entry.entryKey === locked.entryKey && (contentHash
|
|
49
|
+
? entry.skillRef === locked.revision.skillRef && entry.revisionKind === locked.revision.kind &&
|
|
50
|
+
(locked.revision.kind === 'release' ? entry.version === locked.revision.version : entry.sourceRevision === locked.revision.sourceRevision)
|
|
51
|
+
: entry.artifactHash === locked.revision.artifactHash));
|
|
27
52
|
if (!authorized)
|
|
28
53
|
throw new Error(t('downloadMismatch'));
|
|
29
54
|
const response = await fetch(client.url(`/projects/${encodeURIComponent(meta.projectId)}/skills/versions/${authorized.versionId}/archive?${query}`), {
|
|
@@ -40,11 +65,17 @@ export async function downloadLocalProjectSkillArtifacts(directory, cache = proj
|
|
|
40
65
|
parts.push(Buffer.from(chunk));
|
|
41
66
|
}
|
|
42
67
|
const archive = Buffer.concat(parts);
|
|
43
|
-
if (createHash('sha256').update(archive).digest('hex') !==
|
|
68
|
+
if (!/^[a-f0-9]{64}$/.test(authorized.artifactHash) || createHash('sha256').update(archive).digest('hex') !== authorized.artifactHash)
|
|
44
69
|
throw new Error(t('downloadMismatch'));
|
|
45
|
-
|
|
70
|
+
if (contentHash) {
|
|
71
|
+
const extracted = extractBundleArchive(archive);
|
|
72
|
+
if (extracted.issues.length || sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== contentHash)
|
|
73
|
+
throw new Error(t('downloadMismatch'));
|
|
74
|
+
}
|
|
75
|
+
const target = contentFile ?? file;
|
|
76
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
46
77
|
await writeFile(temporary, archive, { mode: 0o600, flag: 'wx' });
|
|
47
|
-
await rename(temporary,
|
|
78
|
+
await rename(temporary, target);
|
|
48
79
|
downloaded++;
|
|
49
80
|
}
|
|
50
81
|
return { downloaded };
|
|
@@ -11,7 +11,7 @@ import { DEFAULT_PLAYCRAFT_CLOUD_BASE } from '../commands/login.js';
|
|
|
11
11
|
import { AGENT_TOOL_OPERATION_PROTOCOL, AGENT_TOOL_OPERATION_HEADERS, AGENT_TOOL_OPERATION_CAPABILITIES_PATH, } from '@playcraft/common/agent-tools';
|
|
12
12
|
import { beginToolOperation, findToolOperation } from './tool-operation-journal.js';
|
|
13
13
|
import { businessEndpoint } from './environment-endpoints.js';
|
|
14
|
-
import { PLATFORM_ROUTES } from '@playcraft/common/cloud-workspace/runtime';
|
|
14
|
+
import { PLATFORM_ROUTES, workspaceExecutionContextQuerySchema, workspaceExecutionContextReadSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
15
15
|
import { cloudRuntimeCredentialHint, isWorkspaceGenerationPath, withGenerationWorkspaceScope } from './generation-workspace-scope.js';
|
|
16
16
|
import { agentGenerationProvenanceHeaders } from './agent-generation-provenance.js';
|
|
17
17
|
import { redactDiagnosticText } from './error-diagnostics.js';
|
|
@@ -245,6 +245,23 @@ export class AgentApiClient {
|
|
|
245
245
|
}
|
|
246
246
|
return res.json();
|
|
247
247
|
}
|
|
248
|
+
/** Context reads are fresh authenticated queries and never durable mutation receipts. */
|
|
249
|
+
async readWorkspaceExecutionContext(scope, request, signal = AbortSignal.timeout(15_000)) {
|
|
250
|
+
const query = workspaceExecutionContextQuerySchema.parse(request);
|
|
251
|
+
const response = await fetch(this.toolsRequestUrl('/execution-context'), {
|
|
252
|
+
method: 'POST', redirect: 'error', signal,
|
|
253
|
+
headers: { ...this.authHeaders(), 'content-type': 'application/json' },
|
|
254
|
+
body: JSON.stringify({ workspaceId: scope.workspaceId, directoryName: scope.directoryName, request: query }),
|
|
255
|
+
});
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
const text = await response.text().catch(() => '');
|
|
258
|
+
const pending = pendingAccountActivationError(response.status, parseErrorPayload(text));
|
|
259
|
+
if (pending)
|
|
260
|
+
throw pending;
|
|
261
|
+
throw new Error(`execution_context_read_failed:${response.status}: ${responseErrorReason(text)}`);
|
|
262
|
+
}
|
|
263
|
+
return workspaceExecutionContextReadSchema.parse(await response.json());
|
|
264
|
+
}
|
|
248
265
|
/** Stream authenticated tool content to a new file; credentials never follow redirects. */
|
|
249
266
|
async download(path, destination, options) {
|
|
250
267
|
const url = this.toolsRequestUrl(path);
|
|
@@ -13,7 +13,13 @@ export async function agentGenerationProvenanceHeaders(path, body, requestId, cl
|
|
|
13
13
|
return {};
|
|
14
14
|
}
|
|
15
15
|
const assetCreate = normalized === '/workspace-assets' && payload?.action === 'create';
|
|
16
|
-
|
|
16
|
+
const audioWrite = normalized === '/workspace-audio';
|
|
17
|
+
const audioRequest = payload?.request;
|
|
18
|
+
const audioRequestId = audioRequest && typeof audioRequest === 'object' && !Array.isArray(audioRequest)
|
|
19
|
+
? audioRequest.requestId : undefined;
|
|
20
|
+
if (audioWrite && (typeof audioRequestId !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(audioRequestId)))
|
|
21
|
+
return {};
|
|
22
|
+
if (!isWorkspaceGenerationPath(normalized) && !assetCreate && !audioWrite && normalized !== '/create-batch')
|
|
17
23
|
return {};
|
|
18
24
|
const endpoint = process.env.PLAYCRAFT_AGENT_GENERATION_ENDPOINT;
|
|
19
25
|
const capability = process.env.PLAYCRAFT_AGENT_GENERATION_CAPABILITY;
|
|
@@ -29,7 +35,7 @@ export async function agentGenerationProvenanceHeaders(path, body, requestId, cl
|
|
|
29
35
|
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000),
|
|
30
36
|
headers: { 'content-type': 'application/json', authorization: `Bearer ${capability}` },
|
|
31
37
|
body: JSON.stringify({ toolPath: `/agent/tools${normalized}`,
|
|
32
|
-
toolRequestId: assetCreate ? payload.clientRequestId : requestId, payloadHash: hashOperationPayload(payload) }),
|
|
38
|
+
toolRequestId: audioWrite ? audioRequestId : assetCreate ? payload.clientRequestId : requestId, payloadHash: hashOperationPayload(payload) }),
|
|
33
39
|
});
|
|
34
40
|
if (!response.ok) {
|
|
35
41
|
await response.body?.cancel();
|