@playcraft/cli 0.1.67 → 0.1.69
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/asset-library/provenance.js +169 -0
- package/dist/commands/audio-program.js +82 -0
- package/dist/commands/audio.js +2 -0
- package/dist/commands/library-messages.js +27 -9
- package/dist/commands/library.js +62 -10
- package/dist/model-adaptation/project-template.js +56 -3
- package/dist/model-adaptation/project.js +15 -1
- package/dist/workspace-runtime/batch/freeze.js +15 -2
- package/dist/workspace-runtime/batch/git.js +16 -4
- package/dist/workspace-runtime/batch/publication.js +96 -0
- package/dist/workspace-runtime/batch/source-candidate.js +2 -1
- package/dist/workspace-runtime/batch/source-publish.js +26 -15
- package/dist/workspace-runtime/persistence/sqlite-worker.js +2 -1
- package/dist/workspace-runtime/serve.js +50 -18
- package/dist/workspace-runtime/server/audio-assets.js +49 -3
- package/dist/workspace-runtime/server/dispatch.js +42 -9
- package/dist/workspace-runtime/server/http.js +12 -2
- package/dist/workspace-runtime/verify-artifact.js +2 -1
- package/dist/workspace-runtime/workspaces/archive.js +28 -3
- package/dist/workspace-runtime/workspaces/asset-library-import.js +60 -9
- package/dist/workspace-runtime/workspaces/asset-provenance-verifier.js +36 -0
- package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +6 -2
- package/dist/workspace-runtime/workspaces/audio-assets/library-import.js +149 -0
- package/dist/workspace-runtime/workspaces/audio-assets/library-package.js +26 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-artifact-audit.js +90 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-browser.js +194 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-build-inputs.js +122 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-engine.js +39 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-package.js +289 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-variant-build.js +217 -0
- package/dist/workspace-runtime/workspaces/audio-assets/references.js +107 -0
- package/dist/workspace-runtime/workspaces/audio-assets/service.js +133 -1
- package/dist/workspace-runtime/workspaces/git.js +7 -0
- package/dist/workspace-runtime/workspaces/registry.js +3 -5
- package/dist/workspace-runtime/workspaces/remote-attach-marker.js +55 -0
- package/package.json +6 -3
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +20 -2
- package/project-template/.claude/skills/playcraft-workspace-assets/SKILL.md +15 -4
- package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +20 -2
- package/project-template-v2/.claude/skills/playcraft-workspace-assets/SKILL.md +15 -4
- package/project-template-v2/.playcraft/skills.lock.json +4 -4
- package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +20 -2
- package/project-templates/phaser/.claude/skills/playcraft-workspace-assets/SKILL.md +15 -4
- package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +20 -2
- package/project-templates/threejs/.claude/skills/playcraft-workspace-assets/SKILL.md +15 -4
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open, opendir, realpath } from 'node:fs/promises';
|
|
3
|
+
import { posix, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { init, parse } from 'es-module-lexer';
|
|
5
|
+
import { audioError, audioProjectPath, digest } from './storage.js';
|
|
6
|
+
import { writeSharedFile } from '../shared-write.js';
|
|
7
|
+
import { auditAudioProgramArtifact } from './program-artifact-audit.js';
|
|
8
|
+
import { assertAudioProgramBuildClone, readAudioProgramBuildInputs, verifyAudioProgramBuildInputs } from './program-build-inputs.js';
|
|
9
|
+
const SKIP = new Set(['dist', 'node_modules', '.git', '.playcraft', '.playcraft-delivery', '.turbo', 'coverage']);
|
|
10
|
+
const SOURCE = /\.(?:[cm]?[jt]s|[jt]sx)$/i;
|
|
11
|
+
const MAX_SOURCE_BYTES = 16 * 1024 * 1024, MAX_SOURCE_FILE_BYTES = 2 * 1024 * 1024, MAX_FILES = 2048, MAX_ENTRIES = 8192;
|
|
12
|
+
async function sourceFiles(root) {
|
|
13
|
+
const files = [], unresolved = [];
|
|
14
|
+
let count = 0;
|
|
15
|
+
const visit = async (directory, depth) => {
|
|
16
|
+
if (depth > 32)
|
|
17
|
+
throw audioError('audio_build_budget_exceeded', 422);
|
|
18
|
+
const handle = await opendir(directory ? await audioProjectPath(root, directory) : await realpath(root), { bufferSize: 32 });
|
|
19
|
+
for await (const entry of handle) {
|
|
20
|
+
if (++count > MAX_ENTRIES)
|
|
21
|
+
throw audioError('audio_build_budget_exceeded', 422);
|
|
22
|
+
if (SKIP.has(entry.name))
|
|
23
|
+
continue;
|
|
24
|
+
const path = directory ? `${directory}/${entry.name}` : entry.name;
|
|
25
|
+
if (entry.isSymbolicLink()) {
|
|
26
|
+
unresolved.push({ file: path, offset: 0, reason: 'symlink_source_skipped' });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (entry.isDirectory())
|
|
30
|
+
await visit(path, depth + 1);
|
|
31
|
+
else if (entry.isFile() && SOURCE.test(entry.name)) {
|
|
32
|
+
if (files.length >= MAX_FILES)
|
|
33
|
+
throw audioError('audio_build_budget_exceeded', 422);
|
|
34
|
+
files.push(path);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
await visit('', 0);
|
|
39
|
+
return { files: files.sort(), unresolved };
|
|
40
|
+
}
|
|
41
|
+
async function stableBytes(root, path, maximum) {
|
|
42
|
+
const handle = await open(await audioProjectPath(root, path), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
43
|
+
try {
|
|
44
|
+
const before = await handle.stat();
|
|
45
|
+
if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
|
|
46
|
+
throw audioError('audio_build_budget_exceeded', 422);
|
|
47
|
+
const bytes = await handle.readFile(), after = await handle.stat();
|
|
48
|
+
if (bytes.length !== before.size || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
|
|
49
|
+
throw audioError('audio_build_input_changed');
|
|
50
|
+
return bytes;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await handle.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function importedPath(file, specifier) {
|
|
57
|
+
if (specifier.includes('\\') || specifier.includes('\0'))
|
|
58
|
+
return undefined;
|
|
59
|
+
if (specifier.startsWith('/'))
|
|
60
|
+
return posix.normalize(specifier.slice(1));
|
|
61
|
+
if (!specifier.startsWith('.'))
|
|
62
|
+
return undefined;
|
|
63
|
+
const path = posix.normalize(posix.join(posix.dirname(file), specifier));
|
|
64
|
+
return path === '..' || path.startsWith('../') ? undefined : path;
|
|
65
|
+
}
|
|
66
|
+
/** Only lexer-confirmed literals in ordinary clone source are changed; immutable packages are never rewritten. */
|
|
67
|
+
export async function rewriteAudioProgramVariantImports(input) {
|
|
68
|
+
if (!['runtime', 'prerender'].includes(input.variant))
|
|
69
|
+
throw audioError('invalid_audio_variant', 400);
|
|
70
|
+
await assertAudioProgramBuildClone(input.sourceRoot, input.cloneRoot);
|
|
71
|
+
await verifyAudioProgramBuildInputs(input.sourceRoot, input.snapshot);
|
|
72
|
+
await verifyAudioProgramBuildInputs(input.cloneRoot, input.snapshot);
|
|
73
|
+
const routes = new Map();
|
|
74
|
+
for (const bundle of input.snapshot.packages)
|
|
75
|
+
for (const path of [bundle.runtimeModulePath, bundle.prerenderModulePath])
|
|
76
|
+
routes.set(path, input.variant === 'runtime' ? bundle.runtimeModulePath : bundle.prerenderModulePath);
|
|
77
|
+
const sources = await sourceFiles(input.cloneRoot), result = {
|
|
78
|
+
variant: input.variant, rewrites: [], unresolved: sources.unresolved, coverage: 'literal-project-relative-imports',
|
|
79
|
+
};
|
|
80
|
+
const changes = [];
|
|
81
|
+
let total = 0;
|
|
82
|
+
await init;
|
|
83
|
+
for (const file of sources.files) {
|
|
84
|
+
const bytes = await stableBytes(input.cloneRoot, file, MAX_SOURCE_FILE_BYTES);
|
|
85
|
+
if ((total += bytes.length) > MAX_SOURCE_BYTES)
|
|
86
|
+
throw audioError('audio_build_budget_exceeded', 422);
|
|
87
|
+
let text;
|
|
88
|
+
try {
|
|
89
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw audioError('audio_program_source_invalid', 400);
|
|
93
|
+
}
|
|
94
|
+
let imports;
|
|
95
|
+
try {
|
|
96
|
+
[imports] = parse(text, file);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
result.unresolved.push({ file, offset: 0, reason: 'module_lexer_parse_failed' });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const replacements = [];
|
|
103
|
+
for (const item of imports) {
|
|
104
|
+
if (item.d === -2)
|
|
105
|
+
continue;
|
|
106
|
+
if (item.n === undefined) {
|
|
107
|
+
if (item.d >= 0)
|
|
108
|
+
result.unresolved.push({ file, offset: item.s, reason: text.slice(item.s, item.e).includes('audio/programs/') ? 'computed_program_import' : 'computed_import_unverified' });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const boundary = item.n.search(/[?#]/), plain = boundary < 0 ? item.n : item.n.slice(0, boundary);
|
|
112
|
+
const resolved = importedPath(file, plain), selected = resolved && routes.get(resolved);
|
|
113
|
+
if (!selected) {
|
|
114
|
+
if (plain.includes('audio/programs/'))
|
|
115
|
+
result.unresolved.push({ file, offset: item.s, reason: 'program_import_path_unresolved' });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (boundary >= 0) {
|
|
119
|
+
result.unresolved.push({ file, offset: item.s, reason: 'program_import_query_unsupported' });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
let replacement = item.n.startsWith('/') ? `/${selected}` : posix.relative(posix.dirname(file), selected);
|
|
123
|
+
if (!item.n.startsWith('/') && !replacement.startsWith('.'))
|
|
124
|
+
replacement = './' + replacement;
|
|
125
|
+
if (selected === resolved && item.n === replacement)
|
|
126
|
+
continue;
|
|
127
|
+
const start = item.d === -1 ? item.s - 1 : item.s, end = item.d === -1 ? item.e + 1 : item.e;
|
|
128
|
+
if (!['"', "'"].includes(text[start]) || text[end - 1] !== text[start]) {
|
|
129
|
+
result.unresolved.push({ file, offset: item.s, reason: 'non_string_program_import' });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
replacements.push({ start, end, value: JSON.stringify(replacement), from: item.n, to: replacement });
|
|
133
|
+
}
|
|
134
|
+
if (!replacements.length)
|
|
135
|
+
continue;
|
|
136
|
+
let rewritten = text;
|
|
137
|
+
for (const item of [...replacements].sort((a, b) => b.start - a.start))
|
|
138
|
+
rewritten = rewritten.slice(0, item.start) + item.value + rewritten.slice(item.end);
|
|
139
|
+
changes.push({ file, bytes: Buffer.from(rewritten), beforeHash: digest(bytes), rows: replacements.map(({ from, to }) => ({ from, to })) });
|
|
140
|
+
}
|
|
141
|
+
// Validate the full plan before touching any clone source, then compare each inode's actual bytes.
|
|
142
|
+
for (const change of changes)
|
|
143
|
+
if (digest(await stableBytes(input.cloneRoot, change.file, MAX_SOURCE_FILE_BYTES)) !== change.beforeHash)
|
|
144
|
+
throw audioError('audio_build_input_changed');
|
|
145
|
+
for (const change of changes) {
|
|
146
|
+
await writeSharedFile(input.cloneRoot, await audioProjectPath(input.cloneRoot, change.file), change.bytes);
|
|
147
|
+
for (const row of change.rows)
|
|
148
|
+
result.rewrites.push({ file: change.file, ...row, beforeHash: change.beforeHash, afterHash: digest(change.bytes) });
|
|
149
|
+
}
|
|
150
|
+
await verifyAudioProgramBuildInputs(input.sourceRoot, input.snapshot);
|
|
151
|
+
await verifyAudioProgramBuildInputs(input.cloneRoot, input.snapshot);
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
/** The callback must return the actual final channel artifact, including its real container format. */
|
|
155
|
+
export async function measureAudioProgramVariants(input) {
|
|
156
|
+
const { capabilities } = input;
|
|
157
|
+
if (typeof capabilities.webAudio !== 'boolean' || typeof capabilities.runtimeSynthesis !== 'boolean'
|
|
158
|
+
|| !Number.isSafeInteger(capabilities.maxBytes) || capabilities.maxBytes < 1 || capabilities.maxBytes > 64 * 1024 * 1024)
|
|
159
|
+
throw audioError('invalid_audio_channel_capability', 400);
|
|
160
|
+
if (!capabilities.webAudio)
|
|
161
|
+
throw audioError('audio_delivery_unavailable', 422);
|
|
162
|
+
const checkAbort = () => { if (input.signal?.aborted)
|
|
163
|
+
throw audioError('audio_build_cancelled', 409); };
|
|
164
|
+
checkAbort();
|
|
165
|
+
const snapshot = input.snapshot ?? await readAudioProgramBuildInputs(input.root);
|
|
166
|
+
await verifyAudioProgramBuildInputs(input.root, snapshot);
|
|
167
|
+
const modes = capabilities.runtimeSynthesis ? ['runtime', 'prerender'] : ['prerender'];
|
|
168
|
+
const candidates = [], candidateRoots = new Set();
|
|
169
|
+
for (const variant of modes) {
|
|
170
|
+
checkAbort();
|
|
171
|
+
await verifyAudioProgramBuildInputs(input.root, snapshot);
|
|
172
|
+
try {
|
|
173
|
+
const prepared = await input.prepare(variant);
|
|
174
|
+
await assertAudioProgramBuildClone(input.root, prepared.projectRoot);
|
|
175
|
+
await assertAudioProgramBuildClone(input.root, prepared.artifactRoot);
|
|
176
|
+
const roots = await Promise.all([realpath(prepared.projectRoot), realpath(prepared.artifactRoot)]);
|
|
177
|
+
if ([...candidateRoots].some(old => roots.some(root => root === old || root.startsWith(old + sep) || old.startsWith(root + sep))))
|
|
178
|
+
throw audioError('invalid_audio_build_clone', 400);
|
|
179
|
+
for (const root of roots)
|
|
180
|
+
candidateRoots.add(root);
|
|
181
|
+
const copied = await readAudioProgramBuildInputs(input.root, prepared.projectRoot);
|
|
182
|
+
if (copied.inputHash !== snapshot.inputHash)
|
|
183
|
+
throw audioError('audio_build_input_changed');
|
|
184
|
+
const rewrite = await rewriteAudioProgramVariantImports({ sourceRoot: input.root, cloneRoot: prepared.projectRoot, snapshot, variant });
|
|
185
|
+
if (variant === 'prerender' && rewrite.unresolved.length > 0)
|
|
186
|
+
throw Object.assign(audioError('audio_program_import_unverified', 422), { details: rewrite.unresolved });
|
|
187
|
+
checkAbort();
|
|
188
|
+
const outputPath = await input.build({ ...prepared, variant, rewrite, signal: input.signal });
|
|
189
|
+
checkAbort();
|
|
190
|
+
await verifyAudioProgramBuildInputs(prepared.projectRoot, snapshot);
|
|
191
|
+
const artifactRoot = await realpath(prepared.artifactRoot), absolute = await realpath(outputPath), rel = relative(artifactRoot, absolute);
|
|
192
|
+
if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || resolve(artifactRoot, rel) !== absolute)
|
|
193
|
+
throw audioError('invalid_audio_build_artifact', 400);
|
|
194
|
+
const stat = await lstat(outputPath);
|
|
195
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
196
|
+
throw audioError('invalid_audio_build_artifact', 400);
|
|
197
|
+
const bytes = await stableBytes(artifactRoot, rel.split(sep).join('/'), 64 * 1024 * 1024);
|
|
198
|
+
if (!bytes.length)
|
|
199
|
+
throw audioError('invalid_audio_build_artifact', 400);
|
|
200
|
+
const artifactAudit = auditAudioProgramArtifact(bytes, outputPath, variant);
|
|
201
|
+
candidates.push({ variant, status: 'completed', ...prepared, outputPath, outputSize: bytes.length, outputHash: digest(bytes), rewrite, artifactAudit });
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (input.signal?.aborted)
|
|
205
|
+
throw audioError('audio_build_cancelled', 409);
|
|
206
|
+
candidates.push({ variant, status: 'failed', error: error.code ?? error.message,
|
|
207
|
+
...(error.details === undefined ? {} : { details: error.details }) });
|
|
208
|
+
}
|
|
209
|
+
await verifyAudioProgramBuildInputs(input.root, snapshot);
|
|
210
|
+
}
|
|
211
|
+
const valid = candidates.filter((candidate) => candidate.status === 'completed' && candidate.outputSize <= capabilities.maxBytes);
|
|
212
|
+
valid.sort((a, b) => a.outputSize - b.outputSize || Number(a.variant === 'runtime') - Number(b.variant === 'runtime'));
|
|
213
|
+
if (!valid.length)
|
|
214
|
+
throw Object.assign(audioError('audio_delivery_unavailable', 422), { details: { channel: capabilities.channel, candidates } });
|
|
215
|
+
return { version: 1, inputHash: snapshot.inputHash, channel: capabilities.channel, capabilities,
|
|
216
|
+
candidates, selected: valid[0], measuredFinalArtifacts: true };
|
|
217
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open, opendir } from 'node:fs/promises';
|
|
3
|
+
import { workspaceAudioBindingSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
4
|
+
import { audioProjectPath, audioError, digest } from './storage.js';
|
|
5
|
+
import { readAudioArtifactPackage } from './artifact-package.js';
|
|
6
|
+
export async function readAudioReferenceFile(root, path) {
|
|
7
|
+
const target = await audioProjectPath(root, path), file = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK)
|
|
8
|
+
.catch(error => { if (error.code === 'ENOENT')
|
|
9
|
+
return undefined; throw error; });
|
|
10
|
+
if (!file)
|
|
11
|
+
return undefined;
|
|
12
|
+
try {
|
|
13
|
+
const before = await file.stat();
|
|
14
|
+
if (!before.isFile() || before.nlink !== 1 || before.size < 1 || before.size > 16_384)
|
|
15
|
+
throw audioError('invalid_audio_binding', 400);
|
|
16
|
+
const bytes = await file.readFile(), after = await file.stat(), named = await lstat(await audioProjectPath(root, path));
|
|
17
|
+
if (bytes.length !== before.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || named.ino !== before.ino || named.dev !== before.dev)
|
|
18
|
+
throw audioError('audio_binding_conflict');
|
|
19
|
+
return bytes;
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await file.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function verifyAudioReference(root, sourcePath, history, bytes) {
|
|
26
|
+
const binding = workspaceAudioBindingSchema.parse(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)));
|
|
27
|
+
if (binding.sourcePath !== sourcePath || !history.some(version => version.revision === binding.revision && version.contentHash === binding.sourceContentHash))
|
|
28
|
+
throw audioError('invalid_audio_binding', 400);
|
|
29
|
+
const directory = `dist/audio/${digest(sourcePath)}/${binding.sourceContentHash}/${binding.representationSetHash}`;
|
|
30
|
+
if (binding.modulePath !== `${directory}/sound.mjs`)
|
|
31
|
+
throw audioError('invalid_audio_binding', 400);
|
|
32
|
+
const bundle = await readAudioArtifactPackage(root, `${directory}/sound.${sourcePath.endsWith('.pcscore.json') ? 'pcscore' : 'pcaudio'}.json`);
|
|
33
|
+
if (bundle.sourcePath !== sourcePath || bundle.sourceContentHash !== binding.sourceContentHash
|
|
34
|
+
|| digest(bundle.files.get(`${directory}/audition.wav`)) !== binding.auditionContentHash)
|
|
35
|
+
throw audioError('invalid_audio_binding', 400);
|
|
36
|
+
return binding;
|
|
37
|
+
}
|
|
38
|
+
export async function readAudioReferences(root, sourcePath, current, history, loadPrevious) {
|
|
39
|
+
const directory = await audioProjectPath(root, 'source/audio-bindings');
|
|
40
|
+
const handle = await opendir(directory).catch(error => { if (error.code === 'ENOENT')
|
|
41
|
+
return undefined; throw error; });
|
|
42
|
+
const names = [];
|
|
43
|
+
let entries = 0, truncated = false;
|
|
44
|
+
if (handle)
|
|
45
|
+
for await (const entry of handle) {
|
|
46
|
+
if (++entries > 1024) {
|
|
47
|
+
truncated = true;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
if (entry.isFile() && /^[a-zA-Z][a-zA-Z0-9_.-]{0,63}\.json$/.test(entry.name))
|
|
51
|
+
names.push(entry.name);
|
|
52
|
+
if (names.length > 128) {
|
|
53
|
+
truncated = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
names.sort();
|
|
58
|
+
const deadline = Date.now() + 5_000;
|
|
59
|
+
const references = [];
|
|
60
|
+
const version = (binding) => ({ revision: binding.revision, sourceContentHash: binding.sourceContentHash,
|
|
61
|
+
modulePath: binding.modulePath, representationSetHash: binding.representationSetHash });
|
|
62
|
+
for (const name of names.slice(0, 128)) {
|
|
63
|
+
if (Date.now() > deadline) {
|
|
64
|
+
truncated = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
const bindingPath = `source/audio-bindings/${name}`, bytes = await readAudioReferenceFile(root, bindingPath);
|
|
68
|
+
if (!bytes)
|
|
69
|
+
continue;
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = JSON.parse(bytes.toString('utf8'));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (raw?.sourcePath !== sourcePath || raw?.eventName !== name.slice(0, -5))
|
|
78
|
+
continue;
|
|
79
|
+
const entry = { eventName: raw.eventName, bindingPath, bindingContentHash: digest(bytes), state: 'invalid', history: [] };
|
|
80
|
+
try {
|
|
81
|
+
const binding = await verifyAudioReference(root, sourcePath, history, bytes);
|
|
82
|
+
entry.version = version(binding);
|
|
83
|
+
entry.state = binding.sourceContentHash === current.contentHash ? 'current' : 'different_version';
|
|
84
|
+
let previous = binding.previousBinding;
|
|
85
|
+
const seen = new Set([entry.bindingContentHash]);
|
|
86
|
+
while (previous && entry.history.length < 20 && !seen.has(previous)) {
|
|
87
|
+
if (Date.now() > deadline) {
|
|
88
|
+
truncated = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
seen.add(previous);
|
|
92
|
+
const old = await loadPrevious(previous);
|
|
93
|
+
if (!old || digest(old) !== previous)
|
|
94
|
+
break;
|
|
95
|
+
const checked = await verifyAudioReference(root, sourcePath, history, old);
|
|
96
|
+
if (checked.eventName !== binding.eventName)
|
|
97
|
+
break;
|
|
98
|
+
entry.history.push({ ...version(checked), bindingContentHash: previous });
|
|
99
|
+
previous = checked.previousBinding;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch { /* An invalid record remains visible, without an unverified package or restore target. */ }
|
|
103
|
+
references.push(entry);
|
|
104
|
+
}
|
|
105
|
+
return { version: 1, target: { kind: 'workspace-file', path: sourcePath }, sourceRevision: current.revision, sourceContentHash: current.contentHash,
|
|
106
|
+
references, scanLimit: 128, truncated, dynamicReferences: 'unverified' };
|
|
107
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { parseSoundDocument, serializeSoundDocument, soundSourceRenderHash, soundDurationSeconds, isSoundDocumentPath, audioRenderKey, canonicalAudioJson, assertAudioProtection, assertScoreProtection, } from '@playcraft/common/cloud-workspace/runtime';
|
|
2
|
+
import { parseSoundDocument, serializeSoundDocument, soundSourceRenderHash, soundDurationSeconds, isSoundDocumentPath, audioRenderKey, canonicalAudioJson, assertAudioProtection, assertScoreProtection, audioReferenceActionSchema, audioReferenceActionReceiptSchema, workspaceAudioBindingSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
3
3
|
import { writeSharedFile } from '../shared-write.js';
|
|
4
4
|
import { AudioStore, audioError, audioProjectPath, readAudioSource, digest } from './storage.js';
|
|
5
5
|
import { publishAudioRepresentations } from './representations.js';
|
|
6
6
|
import { audioArtifactResources } from './artifact-package.js';
|
|
7
|
+
import { readAudioArtifactPackage } from './artifact-package.js';
|
|
8
|
+
import { createAudioLibrarySnapshot } from './library-package.js';
|
|
9
|
+
import { readImportedAudioLibrarySource } from './library-import.js';
|
|
10
|
+
import { readAudioReferences, readAudioReferenceFile, verifyAudioReference } from './references.js';
|
|
11
|
+
import { writeWorkspaceBinaryFile } from '../binary-write.js';
|
|
7
12
|
import { readAudioBindingUsage } from './binding-usage.js';
|
|
8
13
|
const safeId = (id) => typeof id === 'string' && /^[a-zA-Z0-9_-]{1,128}$/.test(id);
|
|
9
14
|
const targetId = (target) => target.kind === 'workspace-file' ? `file:${target.path}` : `candidate:${target.taskId}:${target.candidateId}`;
|
|
@@ -13,6 +18,7 @@ const publicState = async (record, projectRoot) => {
|
|
|
13
18
|
const usage = record.target.kind === 'workspace-file' && record.usage
|
|
14
19
|
? await readAudioBindingUsage(projectRoot, record.target.path, current.contentHash, record.history, record.usage) : undefined;
|
|
15
20
|
return { version: 1, target: record.target, ...current, history: record.history, ...(usage ? { usage } : {}),
|
|
21
|
+
...(record.libraryOrigin ? { libraryOrigin: record.libraryOrigin } : {}),
|
|
16
22
|
...(record.representation ? { representation: record.representation } : {}),
|
|
17
23
|
...(record.adoptionReceipt ? { adoptionReceipt: record.adoptionReceipt } : {}) };
|
|
18
24
|
};
|
|
@@ -68,6 +74,20 @@ export class WorkspaceAudioAssets {
|
|
|
68
74
|
this.checkFormatPath(target.path, version.document);
|
|
69
75
|
record = { ...record, target, current: version, representation: undefined, adoptionReceipt: undefined,
|
|
70
76
|
history: [...(record?.history ?? []), summary(version)] };
|
|
77
|
+
try {
|
|
78
|
+
const imported = await readImportedAudioLibrarySource(this.projectRoot, target.path, version.contentHash);
|
|
79
|
+
if (imported) {
|
|
80
|
+
record.libraryOrigin = imported.origin;
|
|
81
|
+
if (imported.current) {
|
|
82
|
+
await this.store.bytes('auditions', imported.current.audition.contentHash, imported.current.wav);
|
|
83
|
+
record.current = { ...version, renderState: 'ready', audition: imported.current.audition };
|
|
84
|
+
record.representation = imported.current.representation;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
record.current = { ...version, renderState: 'failed', renderError: error instanceof Error && /^audio_library_/.test(error.message) ? error.message : 'audio_library_dependency_invalid' };
|
|
90
|
+
}
|
|
71
91
|
await this.saveRecord(record);
|
|
72
92
|
}
|
|
73
93
|
}
|
|
@@ -78,6 +98,118 @@ export class WorkspaceAudioAssets {
|
|
|
78
98
|
return record;
|
|
79
99
|
}
|
|
80
100
|
read(target) { return this.exclusive(async () => publicState(await this.observe(target), this.projectRoot)); }
|
|
101
|
+
references(target) {
|
|
102
|
+
return this.exclusive(async () => {
|
|
103
|
+
if (target.kind !== 'workspace-file')
|
|
104
|
+
throw audioError('invalid_audio_target', 400);
|
|
105
|
+
const record = await this.observe(target);
|
|
106
|
+
return readAudioReferences(this.projectRoot, target.path, record.current, record.history, hash => this.previousBinding(hash));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async previousBinding(hash) {
|
|
110
|
+
try {
|
|
111
|
+
return await readFile(this.store.blobPath('bindings', hash));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code === 'ENOENT')
|
|
115
|
+
return undefined;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
referenceAction(raw) {
|
|
120
|
+
return this.exclusive(async () => {
|
|
121
|
+
const input = audioReferenceActionSchema.parse(raw), payloadHash = digest(canonicalAudioJson(input));
|
|
122
|
+
let intent = await this.store.read('reference-requests', input.requestId);
|
|
123
|
+
if (intent && intent.payloadHash !== payloadHash)
|
|
124
|
+
throw audioError('audio_request_conflict');
|
|
125
|
+
if (intent?.status === 'complete')
|
|
126
|
+
return audioReferenceActionReceiptSchema.parse(intent.result);
|
|
127
|
+
const record = await this.observe(input.target), bindingPath = `source/audio-bindings/${input.eventName}.json`;
|
|
128
|
+
const current = await readAudioReferenceFile(this.projectRoot, bindingPath), currentHash = current ? digest(current) : null;
|
|
129
|
+
const alreadyWritten = intent?.status === 'pending' && currentHash === digest(Buffer.from(intent.entry, 'base64'));
|
|
130
|
+
if (!alreadyWritten) {
|
|
131
|
+
this.expected(record, { action: 'bind', requestId: input.requestId, target: input.target,
|
|
132
|
+
expectedRevision: input.expectedRevision, expectedContentHash: input.expectedContentHash });
|
|
133
|
+
if (currentHash !== input.expectedBindingContentHash)
|
|
134
|
+
throw audioError('audio_binding_conflict');
|
|
135
|
+
}
|
|
136
|
+
if (!intent) {
|
|
137
|
+
let bytes;
|
|
138
|
+
if (input.action === 'replace') {
|
|
139
|
+
if (record.current.renderState !== 'ready' || !record.current.audition || !record.representation
|
|
140
|
+
|| record.representation.sourceContentHash !== record.current.contentHash)
|
|
141
|
+
throw audioError('audio_audition_unavailable', 409);
|
|
142
|
+
const entry = workspaceAudioBindingSchema.parse({ version: 1, operationId: input.requestId, eventName: input.eventName,
|
|
143
|
+
sourcePath: input.target.path, sourceContentHash: record.current.contentHash, revision: record.current.revision,
|
|
144
|
+
auditionContentHash: record.current.audition.contentHash, modulePath: record.representation.modulePath,
|
|
145
|
+
representationSetHash: record.representation.representationSetHash, integrationStatus: 'integration_required', previousBinding: currentHash });
|
|
146
|
+
bytes = Buffer.from(`${canonicalAudioJson(entry)}\n`);
|
|
147
|
+
await verifyAudioReference(this.projectRoot, input.target.path, record.history, bytes);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const snapshot = await readAudioReferences(this.projectRoot, input.target.path, record.current, record.history, hash => this.previousBinding(hash));
|
|
151
|
+
const tracked = snapshot.references.find(reference => reference.eventName === input.eventName);
|
|
152
|
+
if (!tracked?.history.some(version => version.bindingContentHash === input.previousBindingHash))
|
|
153
|
+
throw audioError('invalid_audio_binding', 400);
|
|
154
|
+
const previous = await this.previousBinding(input.previousBindingHash);
|
|
155
|
+
if (!previous || digest(previous) !== input.previousBindingHash)
|
|
156
|
+
throw audioError('invalid_audio_binding', 400);
|
|
157
|
+
await verifyAudioReference(this.projectRoot, input.target.path, record.history, previous);
|
|
158
|
+
bytes = Buffer.from(previous);
|
|
159
|
+
}
|
|
160
|
+
if (current)
|
|
161
|
+
await this.store.bytes('bindings', currentHash, current);
|
|
162
|
+
intent = { payloadHash, status: 'pending', beforeHash: currentHash, entry: bytes.toString('base64') };
|
|
163
|
+
await this.store.write('reference-requests', input.requestId, intent);
|
|
164
|
+
}
|
|
165
|
+
const bytes = Buffer.from(intent.entry, 'base64'), binding = await verifyAudioReference(this.projectRoot, input.target.path, record.history, bytes);
|
|
166
|
+
if (binding.eventName !== input.eventName)
|
|
167
|
+
throw audioError('invalid_audio_binding', 400);
|
|
168
|
+
if (!alreadyWritten) {
|
|
169
|
+
try {
|
|
170
|
+
await writeWorkspaceBinaryFile(this.projectRoot, { path: bindingPath, contentBase64: intent.entry, expectedContentHash: intent.beforeHash });
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error.code === 'content_conflict')
|
|
174
|
+
throw audioError('audio_binding_conflict');
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
record.usage = { status: 'integration_required', bindingPath, eventName: input.eventName, sourceContentHash: binding.sourceContentHash,
|
|
179
|
+
...(binding.previousBinding ? { previousContentHash: binding.previousBinding } : {}) };
|
|
180
|
+
await this.saveRecord(record);
|
|
181
|
+
await this.options.onWrite?.(input.target.path, record.current, [bindingPath], input.requestId);
|
|
182
|
+
const result = audioReferenceActionReceiptSchema.parse({ version: 1, requestId: input.requestId, action: input.action, target: input.target,
|
|
183
|
+
eventName: input.eventName, bindingPath, bindingContentHash: digest(bytes), sourceContentHash: binding.sourceContentHash,
|
|
184
|
+
revision: binding.revision, modulePath: binding.modulePath, representationSetHash: binding.representationSetHash, integrationStatus: 'integration_required' });
|
|
185
|
+
await this.store.write('reference-requests', input.requestId, { ...intent, status: 'complete', result });
|
|
186
|
+
return result;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
librarySnapshot(target, revision, contentHash) {
|
|
190
|
+
return this.exclusive(async () => {
|
|
191
|
+
const record = await this.observe(target), version = record.current;
|
|
192
|
+
if (version.revision !== revision || version.contentHash !== contentHash)
|
|
193
|
+
throw audioError('audio_revision_conflict');
|
|
194
|
+
if (version.renderState !== 'ready' || !version.audition)
|
|
195
|
+
throw audioError('audio_audition_unavailable', 404);
|
|
196
|
+
const sourcePath = target.kind === 'workspace-file' ? target.path
|
|
197
|
+
: `source/audio/${target.candidateId}.${version.document.format === 'playcraft.score' ? 'pcscore' : 'pcaudio'}.json`;
|
|
198
|
+
if (record.representation?.sourceContentHash === contentHash) {
|
|
199
|
+
const entry = `${record.representation.modulePath.slice(0, -'sound.mjs'.length)}sound.${version.document.format === 'playcraft.score' ? 'pcscore' : 'pcaudio'}.json`;
|
|
200
|
+
const packaged = await readAudioArtifactPackage(this.projectRoot, entry);
|
|
201
|
+
const files = [...packaged.files].map(([path, bytes]) => ({ path: path.slice(path.lastIndexOf('/') + 1),
|
|
202
|
+
mimeType: path.endsWith('.wav') ? 'audio/wav' : path.endsWith('.mjs') ? 'text/javascript' : 'application/json',
|
|
203
|
+
size: bytes.length, contentHash: digest(bytes), content: bytes.toString('base64') }));
|
|
204
|
+
return { version: 1, target, revision, sourcePath, files };
|
|
205
|
+
}
|
|
206
|
+
const wav = await readFile(this.store.blobPath('auditions', version.audition.contentHash));
|
|
207
|
+
if (digest(wav) !== version.audition.contentHash || wav.length !== version.audition.size)
|
|
208
|
+
throw audioError('audio_audition_unavailable', 404);
|
|
209
|
+
return createAudioLibrarySnapshot({ target, revision, sourcePath, document: version.document, sourceText: version.sourceText,
|
|
210
|
+
contentHash, sourceRenderHash: version.sourceRenderHash, audition: version.audition, wav });
|
|
211
|
+
});
|
|
212
|
+
}
|
|
81
213
|
async content(target, revision, representation = 'audition') {
|
|
82
214
|
const record = await this.observe(target), version = revision === undefined || revision === record.current.revision ? record.current :
|
|
83
215
|
await this.store.read('versions', `${targetId(target)}:${revision}`);
|
|
@@ -357,7 +357,14 @@ export async function saveWorkspaceGit(root, input) {
|
|
|
357
357
|
// user (or an agent) finishes the merge with an explicit commit; saves resume after it.
|
|
358
358
|
if ((await mergeState(rootReal)).merging)
|
|
359
359
|
return { pushed: false, withheld: [] };
|
|
360
|
+
const { readAudioBuildInputs, verifyAudioBuildInputs } = await import('./audio-assets/build-inputs.js');
|
|
361
|
+
const audio = await readAudioBuildInputs(rootReal);
|
|
360
362
|
await git(rootReal, ['add', '-A'], 60000);
|
|
363
|
+
// dist is normally ignored. Save only verified immutable audio closures, never unrelated outputs.
|
|
364
|
+
const audioPaths = [...new Set([...audio.packages, ...(audio.programs?.packages ?? [])].flatMap(bundle => bundle.files.map(file => file.path)))];
|
|
365
|
+
for (let index = 0; index < audioPaths.length; index += 64)
|
|
366
|
+
await git(rootReal, ['--literal-pathspecs', 'add', '-f', '--', ...audioPaths.slice(index, index + 64)], 60000);
|
|
367
|
+
await verifyAudioBuildInputs(rootReal, audio);
|
|
361
368
|
const staged = (await git(rootReal, ['diff', '--cached', '--name-only', '-z'], 60000)).split('\0').filter(Boolean);
|
|
362
369
|
const withheld = staged.filter(isCredentialFile);
|
|
363
370
|
await unstage(rootReal, withheld);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { lstat, mkdir, realpath, rm
|
|
2
|
+
import { lstat, mkdir, realpath, rm } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join, relative } from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { isLocalWorkspaceGitAuthMode, sameWorkspaceRegistration, workspaceRegistrationSchema, batchSourceSnapshotSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
@@ -10,6 +10,7 @@ import { ensureWorkspaceSkills, readWorkspaceSkills } from './skills.js';
|
|
|
10
10
|
import { loadWorkspaceSkillArtifact, workspacePlatformSkillAccess } from './skill-artifact-loader.js';
|
|
11
11
|
import { writeWorkspaceCliContext } from './cli-context.js';
|
|
12
12
|
import { attachWorkspaceRemote } from './git.js';
|
|
13
|
+
import { markRemoteAttachPending } from './remote-attach-marker.js';
|
|
13
14
|
import { ensureWorkspaceArtifactInstructions } from './instructions.js';
|
|
14
15
|
import { repairWorkspaceShares } from './workspace-shares.js';
|
|
15
16
|
export { WorkspaceContextError } from './context-error.js';
|
|
@@ -183,10 +184,7 @@ export async function registerWorkspace(state, input, leaseFence, preparation, r
|
|
|
183
184
|
&& (needsRemote || remoteUpgrade || await lstat(attachMarker).then(() => true, () => false))) {
|
|
184
185
|
if (preparation?.mode !== 'clone' || !state.gitCredentials)
|
|
185
186
|
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
186
|
-
|
|
187
|
-
await mkdir(join(state.config.stateDir, 'workspace-remote-attach'), { recursive: true, mode: 0o700 });
|
|
188
|
-
// private
|
|
189
|
-
await writeFile(attachMarker, JSON.stringify({ repositoryUrl: binding.repositoryUrl, branchName: binding.branchName }), { mode: 0o600 });
|
|
187
|
+
await markRemoteAttachPending(state, binding.workspaceId, binding.repositoryUrl, binding.branchName);
|
|
190
188
|
// The credential helper only serves a workspace the journal knows to be bound.
|
|
191
189
|
await state.journal.putWorkspace({ ...binding, directories });
|
|
192
190
|
try {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { link, mkdir, open, readFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
function markerPath(state, workspaceId) {
|
|
5
|
+
return join(state.config.stateDir, 'workspace-remote-attach', `${workspaceId}.json`);
|
|
6
|
+
}
|
|
7
|
+
/** Caller holds the workspace journal lock. Never replace a different unfinished attach. */
|
|
8
|
+
export async function markRemoteAttachPending(state, workspaceId, repositoryUrl, branchName) {
|
|
9
|
+
const path = markerPath(state, workspaceId);
|
|
10
|
+
const directory = join(state.config.stateDir, 'workspace-remote-attach');
|
|
11
|
+
// private: the attach marker is controller state outside the shared workspace tree.
|
|
12
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
13
|
+
const expected = { repositoryUrl, branchName };
|
|
14
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
15
|
+
// private: only the controller reads an unfinished attach back.
|
|
16
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
17
|
+
try {
|
|
18
|
+
await handle.writeFile(JSON.stringify(expected));
|
|
19
|
+
await handle.sync();
|
|
20
|
+
await handle.close();
|
|
21
|
+
try {
|
|
22
|
+
await link(temporary, path);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== 'EEXIST')
|
|
26
|
+
throw error;
|
|
27
|
+
const previous = JSON.parse(await readFile(path, 'utf8'));
|
|
28
|
+
if (previous.repositoryUrl !== repositoryUrl || previous.branchName !== branchName)
|
|
29
|
+
throw new Error('batch_source_identity_mismatch');
|
|
30
|
+
}
|
|
31
|
+
const folder = await open(directory, 'r');
|
|
32
|
+
try {
|
|
33
|
+
await folder.sync();
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await folder.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await handle.close();
|
|
41
|
+
await unlink(temporary).catch(() => undefined);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Caller holds the workspace journal lock to serialize with attach and marker removal. */
|
|
45
|
+
export async function remoteAttachPending(state, workspaceId) {
|
|
46
|
+
try {
|
|
47
|
+
await readFile(markerPath(state, workspaceId));
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === 'ENOENT')
|
|
52
|
+
return false;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.69",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"verify:parameter-skill": "node scripts/generate-parameter-skill.mjs --check && node --test scripts/generate-parameter-skill.test.mjs",
|
|
45
45
|
"verify:level-editor-skill": "node scripts/generate-level-editor-skill.mjs --check",
|
|
46
46
|
"verify:asset-skills": "node scripts/generate-asset-skills.mjs --check && node --test scripts/generate-asset-skills.test.mjs",
|
|
47
|
+
"generate:audio-program-companion": "node scripts/generate-audio-program-companion.mjs",
|
|
48
|
+
"verify:audio-program-companion": "node scripts/generate-audio-program-companion.mjs --check",
|
|
47
49
|
"verify:agent-stop": "node scripts/verify-agent-stop.mjs",
|
|
48
50
|
"verify-sprite-overlay": "node scripts/verify-sprite-overlay.mjs",
|
|
49
51
|
"link": "node scripts/bump-local.js && pnpm build && npm link",
|
|
@@ -54,8 +56,8 @@
|
|
|
54
56
|
"@gltf-transform/core": "^4.3.0",
|
|
55
57
|
"@gltf-transform/extensions": "^4.3.0",
|
|
56
58
|
"@gltf-transform/functions": "^4.3.0",
|
|
57
|
-
"@playcraft/build": "0.1.
|
|
58
|
-
"@playcraft/common": "0.1.
|
|
59
|
+
"@playcraft/build": "0.1.69",
|
|
60
|
+
"@playcraft/common": "0.1.69",
|
|
59
61
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
60
62
|
"chokidar": "^4.0.3",
|
|
61
63
|
"commander": "^13.1.0",
|
|
@@ -64,6 +66,7 @@
|
|
|
64
66
|
"dotenv": "^17.2.3",
|
|
65
67
|
"drizzle-orm": "^0.45.1",
|
|
66
68
|
"earcut": "3.2.4",
|
|
69
|
+
"es-module-lexer": "1.7.0",
|
|
67
70
|
"execa": "^9.6.1",
|
|
68
71
|
"express": "^5.2.1",
|
|
69
72
|
"fluent-ffmpeg": "^2.1.3",
|