@playcraft/cli 0.1.66 → 0.1.68
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-program.js +82 -0
- package/dist/commands/audio-source.js +109 -0
- package/dist/commands/audio.js +13 -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 +257 -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/asset-library-import.js +27 -7
- 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 +80 -0
- 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/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 +459 -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/git.js +7 -0
- 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 +9 -4
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +92 -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 +92 -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 +92 -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 +92 -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
|
@@ -2,22 +2,27 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
3
|
import { lstat, mkdir, open, readFile, realpath, link, unlink } from 'node:fs/promises';
|
|
4
4
|
import { extname, join, posix, resolve, sep } from 'node:path';
|
|
5
|
-
import { WORKSPACE_ARTIFACT_MAX_BYTES, isWorkspaceArtifactOutputPath, workspaceRelativePathSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
5
|
+
import { WORKSPACE_ARTIFACT_MAX_BYTES, isWorkspaceArtifactOutputPath, workspaceRelativePathSchema, isSoundDocumentPath } from '@playcraft/common/cloud-workspace/runtime';
|
|
6
6
|
import { openSharedFile } from './shared-write.js';
|
|
7
|
+
import { audioArtifactResources } from './audio-assets/artifact-package.js';
|
|
7
8
|
const formats = {
|
|
8
9
|
'.png': ['image', 'image/png'], '.jpg': ['image', 'image/jpeg'], '.jpeg': ['image', 'image/jpeg'],
|
|
9
10
|
'.webp': ['image', 'image/webp'], '.gif': ['image', 'image/gif'], '.avif': ['image', 'image/avif'], '.svg': ['image', 'image/svg+xml'],
|
|
10
11
|
'.hdr': ['image', 'image/vnd.radiance'], '.exr': ['image', 'image/x-exr'],
|
|
11
12
|
'.ktx': ['image', 'image/ktx'], '.ktx2': ['image', 'image/ktx2'], '.dds': ['image', 'image/vnd-ms.dds'],
|
|
12
13
|
'.mp4': ['video', 'video/mp4'], '.webm': ['video', 'video/webm'], '.mov': ['video', 'video/quicktime'],
|
|
13
|
-
'.mp3': ['audio', 'audio/mpeg'], '.wav': ['audio', 'audio/wav'], '.ogg': ['audio', 'audio/ogg'], '.m4a': ['audio', 'audio/mp4'], '.flac': ['audio', 'audio/flac'],
|
|
14
|
+
'.mp3': ['audio', 'audio/mpeg'], '.wav': ['audio', 'audio/wav'], '.ogg': ['audio', 'audio/ogg'], '.opus': ['audio', 'audio/ogg'], '.m4a': ['audio', 'audio/mp4'], '.flac': ['audio', 'audio/flac'],
|
|
14
15
|
'.glb': ['model', 'model/gltf-binary'], '.gltf': ['model', 'model/gltf+json'], '.obj': ['model', 'text/plain'], '.fbx': ['model', 'application/octet-stream'], '.stl': ['model', 'model/stl'], '.usdz': ['model', 'model/vnd.usdz+zip'],
|
|
15
16
|
'.md': ['document', 'text/markdown'], '.txt': ['document', 'text/plain'], '.pdf': ['document', 'application/pdf'],
|
|
16
17
|
'.html': ['document', 'text/html'], '.csv': ['document', 'text/csv'], '.docx': ['document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
|
17
18
|
'.pptx': ['document', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'], '.xlsx': ['document', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
|
18
19
|
'.zip': ['file', 'application/zip'],
|
|
19
20
|
};
|
|
20
|
-
export function artifactFormat(path) {
|
|
21
|
+
export function artifactFormat(path) {
|
|
22
|
+
if (isSoundDocumentPath(path))
|
|
23
|
+
return ['audio', 'application/json'];
|
|
24
|
+
return formats[extname(path).toLowerCase()];
|
|
25
|
+
}
|
|
21
26
|
export function artifactMime(path) { return artifactFormat(path)?.[1] ?? 'application/octet-stream'; }
|
|
22
27
|
/** Reject every redirected path segment, including links that happen to remain in the workspace. */
|
|
23
28
|
export async function artifactPath(root, path) {
|
|
@@ -152,6 +157,8 @@ async function binaryModelDocument(root, path) {
|
|
|
152
157
|
}
|
|
153
158
|
/** Only explicit model references become members; neighboring names never imply a relationship. */
|
|
154
159
|
export async function artifactResources(root, path) {
|
|
160
|
+
if (isSoundDocumentPath(path))
|
|
161
|
+
return audioArtifactResources(root, path);
|
|
155
162
|
const resources = new Set();
|
|
156
163
|
const add = (from, uri) => { const result = resourcePath(from, uri); if (result)
|
|
157
164
|
resources.add(result); };
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
3
|
import { lstat, mkdir, open, readdir, rename, unlink } from 'node:fs/promises';
|
|
4
|
-
import { basename, join } from 'node:path';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
5
|
import sharp from 'sharp';
|
|
6
|
-
import { WORKSPACE_ARTIFACT_OUTPUT_DIRECTORY, isWorkspaceArtifactOutputPath, isWorkspaceArtifactOutputVersion, workspaceArtifactContentQuerySchema, workspaceArtifactListSchema, workspaceModelBuildSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
6
|
+
import { WORKSPACE_ARTIFACT_OUTPUT_DIRECTORY, isWorkspaceArtifactOutputPath, isWorkspaceArtifactOutputVersion, workspaceArtifactContentQuerySchema, workspaceArtifactListSchema, isSoundDocumentPath, workspaceModelBuildSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
7
7
|
import { artifactBundleDigest, artifactDirectory, artifactFormat, artifactPath, artifactResources, snapshotArtifactBundle, snapshotArtifactFile } from './artifact-files.js';
|
|
8
8
|
import { ARTIFACT_RECEIPTS_DIRECTORY, readArtifactReceipt } from './artifact-receipts.js';
|
|
9
9
|
import { ensureSharedDirectory } from './shared-write.js';
|
|
10
10
|
import { validateWorkspacePlayable } from '../previews/finalize.js';
|
|
11
11
|
import { PREVIEW_HTML_MAX_BYTES, WORKSPACE_PLAYABLE_OUTPUT_PATH } from '@playcraft/common/cloud-workspace/runtime';
|
|
12
12
|
import { applyWorkspaceModelBuildStates, sameWorkspaceModelBuild } from './model-build-inputs.js';
|
|
13
|
+
import { audioArtifactDetails, validateAudioArtifactVersion } from './audio-assets/artifact-package.js';
|
|
14
|
+
import { readAudioSource } from './audio-assets/storage.js';
|
|
13
15
|
const serial = new Map();
|
|
14
16
|
function digest(text) { return createHash('sha256').update(text).digest('hex'); }
|
|
15
|
-
function artifactId(workspaceId, path) {
|
|
17
|
+
function artifactId(workspaceId, path) {
|
|
18
|
+
const audioKey = /^dist\/audio\/([a-f0-9]{64})\/[a-f0-9]{64}\/[a-f0-9]{64}\/sound\.(?:pcaudio|pcscore)\.json$/.exec(path)?.[1];
|
|
19
|
+
return `artifact_${digest(`${workspaceId}\0${audioKey ? `audio-source:${audioKey}` : path}`).slice(0, 40)}`;
|
|
20
|
+
}
|
|
16
21
|
function outputArtifacts(artifacts) {
|
|
17
22
|
return artifacts.flatMap(artifact => {
|
|
18
23
|
const versions = artifact.versions.filter(isWorkspaceArtifactOutputVersion);
|
|
@@ -152,10 +157,34 @@ export class WorkspaceArtifactStore {
|
|
|
152
157
|
catch { /* An incomplete receipt stays pending; the next scan retries its original bytes. */ }
|
|
153
158
|
}
|
|
154
159
|
const candidates = await discoverPaths(root);
|
|
155
|
-
//
|
|
156
|
-
|
|
160
|
+
// Compound sounds and models claim their explicitly listed resources before standalone media.
|
|
161
|
+
const priority = (path) => isSoundDocumentPath(path) ? 2 : Number(artifactFormat(path)?.[0] === 'model');
|
|
162
|
+
candidates.sort((a, b) => priority(b) - priority(a) || a.localeCompare(b));
|
|
157
163
|
const visible = outputArtifacts(catalog.artifacts);
|
|
158
164
|
const members = new Set(visible.flatMap(artifact => artifact.versions.flatMap(version => version.files.filter(file => file.role !== 'primary').map(file => file.path))));
|
|
165
|
+
const audioMembers = new Set();
|
|
166
|
+
const currentAudio = new Set();
|
|
167
|
+
for (const path of candidates.filter(isSoundDocumentPath)) {
|
|
168
|
+
try {
|
|
169
|
+
const details = await audioArtifactDetails(root, path);
|
|
170
|
+
for (const member of details.resources) {
|
|
171
|
+
members.add(member);
|
|
172
|
+
audioMembers.add(member);
|
|
173
|
+
}
|
|
174
|
+
const source = await readAudioSource(root, details.sourcePath).catch(() => undefined);
|
|
175
|
+
if (source !== undefined && digest(source) === details.sourceContentHash)
|
|
176
|
+
currentAudio.add(path);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// An incomplete sound package stays visible as ordinary files, never as a standalone audio fallback.
|
|
180
|
+
for (const candidate of candidates.filter(value => dirname(value) === dirname(path))) {
|
|
181
|
+
members.add(candidate);
|
|
182
|
+
if (candidate !== path)
|
|
183
|
+
audioMembers.add(candidate);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
candidates.sort((a, b) => priority(b) - priority(a) || Number(currentAudio.has(a)) - Number(currentAudio.has(b)) || a.localeCompare(b));
|
|
159
188
|
for (const path of candidates) {
|
|
160
189
|
if (members.has(path))
|
|
161
190
|
continue;
|
|
@@ -165,7 +194,9 @@ export class WorkspaceArtifactStore {
|
|
|
165
194
|
continue;
|
|
166
195
|
try {
|
|
167
196
|
const current = visible.find(artifact => artifact.id === artifactId(this.scopeId, path));
|
|
168
|
-
const version =
|
|
197
|
+
const version = isSoundDocumentPath(path)
|
|
198
|
+
? current?.versions.find(entry => entry.path === path && entry.id === catalog.observations[path]?.versionId) ?? current?.versions.find(entry => entry.path === path)
|
|
199
|
+
: current?.versions.find(entry => entry.id === current.currentVersionId);
|
|
169
200
|
const knownWrite = sourcePaths.includes(path) && Boolean(source.operationId) && version?.source.operationId !== source.operationId;
|
|
170
201
|
const observation = catalog.observations[path];
|
|
171
202
|
if (!knownWrite && version && (path !== WORKSPACE_PLAYABLE_OUTPUT_PATH || current?.kind === 'playable')
|
|
@@ -229,6 +260,8 @@ export class WorkspaceArtifactStore {
|
|
|
229
260
|
}
|
|
230
261
|
catalog.artifacts = catalog.artifacts.filter(artifact => {
|
|
231
262
|
const current = artifact.versions.find(version => version.id === artifact.currentVersionId);
|
|
263
|
+
if (audioMembers.has(current.path))
|
|
264
|
+
return false;
|
|
232
265
|
return !isWorkspaceArtifactOutputVersion(current) || current.source.kind !== 'discovered' || !members.has(current.path);
|
|
233
266
|
});
|
|
234
267
|
catalog.artifacts.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id));
|
|
@@ -238,6 +271,10 @@ export class WorkspaceArtifactStore {
|
|
|
238
271
|
return { list, changed: original !== JSON.stringify(list.artifacts) };
|
|
239
272
|
}
|
|
240
273
|
async versionKind(objects, version) {
|
|
274
|
+
if (isSoundDocumentPath(version.path)) {
|
|
275
|
+
await validateAudioArtifactVersion(objects, version.path, version.files);
|
|
276
|
+
return 'audio';
|
|
277
|
+
}
|
|
241
278
|
if (version.path !== WORKSPACE_PLAYABLE_OUTPUT_PATH)
|
|
242
279
|
return artifactFormat(version.path)[0];
|
|
243
280
|
if (version.files.length !== 1)
|
|
@@ -260,11 +297,15 @@ export class WorkspaceArtifactStore {
|
|
|
260
297
|
}
|
|
261
298
|
async add(catalog, version, kind, objects) {
|
|
262
299
|
const id = artifactId(this.scopeId, version.path);
|
|
300
|
+
const audioName = kind === 'audio' && isSoundDocumentPath(version.path)
|
|
301
|
+
? (await validateAudioArtifactVersion(objects, version.path, version.files)).name : undefined;
|
|
263
302
|
let artifact = catalog.artifacts.find(item => item.id === id);
|
|
264
303
|
if (!artifact) {
|
|
265
304
|
artifact = { id, kind, name: basename(version.path), currentVersionId: version.id, updatedAt: version.createdAt, versions: [] };
|
|
266
305
|
catalog.artifacts.push(artifact);
|
|
267
306
|
}
|
|
307
|
+
if (audioName)
|
|
308
|
+
artifact.name = audioName;
|
|
268
309
|
if (kind === 'playable' && artifact.kind !== 'playable') {
|
|
269
310
|
const valid = await Promise.all(artifact.versions.map(async (item) => {
|
|
270
311
|
try {
|
|
@@ -6,6 +6,8 @@ import { BINARY_WORKSPACE_FILE_MAX_BYTES, WORKSPACE_LIBRARY_ATTACHMENT_MAX_BYTES
|
|
|
6
6
|
import { observeWorkspaceBinaryFile, writeWorkspaceBinaryFile } from './binary-write.js';
|
|
7
7
|
import { shareWorkspaceAssetDirectory } from './asset-publication-access.js';
|
|
8
8
|
import { ensureSharedDirectory } from './shared-write.js';
|
|
9
|
+
import { isEditableAudioLibraryAsset, prepareAudioLibraryImport } from './audio-assets/library-import.js';
|
|
10
|
+
import { readAudioArtifactPackage } from './audio-assets/artifact-package.js';
|
|
9
11
|
const RESERVED = new Set(['.git', 'node_modules', '.playcraft-temp', '.tmp']);
|
|
10
12
|
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
11
13
|
const missing = (error) => error?.code === 'ENOENT';
|
|
@@ -158,6 +160,9 @@ async function readTransaction(root, transaction) {
|
|
|
158
160
|
|| !/^[a-f0-9]{64}$/.test(value.incomingHash))
|
|
159
161
|
throw new AssetLibraryImportError('invalid_library_import');
|
|
160
162
|
validPath(value.target);
|
|
163
|
+
if (value.audioPackagePaths && (value.audioPackagePaths.length !== 5 || new Set(value.audioPackagePaths).size !== 5
|
|
164
|
+
|| value.audioPackagePaths.some(path => !/^dist\/audio\/[a-f0-9]{64}\/[a-f0-9]{64}\/[a-f0-9]{64}\/(?:sound\.(?:pcaudio|pcscore)\.json|audition\.wav|runtime\.mjs|sound\.mjs|manifest\.json)$/.test(path))))
|
|
165
|
+
throw new AssetLibraryImportError('invalid_library_import');
|
|
161
166
|
if ((value.operationId !== undefined || value.assetId !== undefined || value.paths !== undefined)
|
|
162
167
|
&& (typeof value.operationId !== 'string' || !value.operationId || value.operationId.length > 128
|
|
163
168
|
|| typeof value.assetId !== 'string' || !value.assetId || value.assetId.length > 128
|
|
@@ -203,8 +208,17 @@ export async function recoverInterruptedLibraryImports(root) {
|
|
|
203
208
|
throw new AssetLibraryImportError('invalid_library_import_path');
|
|
204
209
|
if (current?.isDirectory() && !current.isSymbolicLink() && !incoming
|
|
205
210
|
&& await directorySnapshot(target) === manifest.incomingHash) {
|
|
211
|
+
if (manifest.audioPackagePaths) {
|
|
212
|
+
const primary = manifest.audioPackagePaths.find(path => /\/sound\.(?:pcaudio|pcscore)\.json$/.test(path));
|
|
213
|
+
if (!primary)
|
|
214
|
+
throw new AssetLibraryImportError('invalid_library_import');
|
|
215
|
+
const packaged = await readAudioArtifactPackage(rootReal, primary);
|
|
216
|
+
if (!packaged.sourcePath.startsWith(`${manifest.target}/`) || packaged.files.size !== 5
|
|
217
|
+
|| [...packaged.files.keys()].some(path => !manifest.audioPackagePaths.includes(path)))
|
|
218
|
+
throw new AssetLibraryImportError('invalid_library_import');
|
|
219
|
+
}
|
|
206
220
|
if (manifest.operationId && manifest.assetId && manifest.paths)
|
|
207
|
-
result.published.push({ operationId: manifest.operationId, assetId: manifest.assetId, paths: manifest.paths });
|
|
221
|
+
result.published.push({ operationId: manifest.operationId, assetId: manifest.assetId, paths: [...manifest.paths, ...(manifest.audioPackagePaths ?? [])] });
|
|
208
222
|
await removeTransaction(rootReal, transaction);
|
|
209
223
|
result.recovered.push(manifest.target);
|
|
210
224
|
continue;
|
|
@@ -236,7 +250,7 @@ export async function recoverInterruptedLibraryImports(root) {
|
|
|
236
250
|
}
|
|
237
251
|
return result;
|
|
238
252
|
}
|
|
239
|
-
async function publishBundle(root, target, asset, replace, assertCurrent, operationId) {
|
|
253
|
+
async function publishBundle(root, target, asset, replace, assertCurrent, operationId, audioPackagePaths) {
|
|
240
254
|
const parent = dirname(target) === '.' ? '' : dirname(target);
|
|
241
255
|
await createPublicDirectory(root, parent);
|
|
242
256
|
const previous = await statTarget(root, target);
|
|
@@ -256,6 +270,7 @@ async function publishBundle(root, target, asset, replace, assertCurrent, operat
|
|
|
256
270
|
await writeWorkspaceBinaryFile(staged, { path: file.path, contentBase64: file.content, expectedContentHash: null });
|
|
257
271
|
}
|
|
258
272
|
const manifest = { version: 1, target, phase: 'prepared', originalHash: originalHash ?? null,
|
|
273
|
+
...(audioPackagePaths ? { audioPackagePaths } : {}),
|
|
259
274
|
incomingHash: await directorySnapshot(staged), ...(operationId ? { operationId, assetId: asset.assetId,
|
|
260
275
|
paths: asset.files.map(file => `${target}/${file.path}`) } : {}) };
|
|
261
276
|
await saveTransaction(root, transaction, manifest);
|
|
@@ -364,9 +379,13 @@ export async function importWorkspaceLibraryAssets(root, selection, snapshot, as
|
|
|
364
379
|
}
|
|
365
380
|
}
|
|
366
381
|
if (asset.kind === 'bundle') {
|
|
367
|
-
const
|
|
368
|
-
|
|
382
|
+
const audio = isEditableAudioLibraryAsset(asset) ? await prepareAudioLibraryImport(rootReal, target, asset, assertCurrent) : undefined;
|
|
383
|
+
const incoming = audio?.asset ?? asset;
|
|
384
|
+
const paths = incoming.files.map(file => validPath(`${target}/${file.path}`));
|
|
385
|
+
publishedTransaction = await publishBundle(rootReal, target, incoming, payload.collision === 'replace', assertCurrent, options.operationId, audio?.packagePaths);
|
|
369
386
|
result.paths.push(...paths);
|
|
387
|
+
if (audio)
|
|
388
|
+
result.paths.push(...audio.packagePaths);
|
|
370
389
|
}
|
|
371
390
|
else {
|
|
372
391
|
const observed = existing ? await observeWorkspaceBinaryFile(rootReal, target) : undefined;
|
|
@@ -378,9 +397,10 @@ export async function importWorkspaceLibraryAssets(root, selection, snapshot, as
|
|
|
378
397
|
}
|
|
379
398
|
catch (error) {
|
|
380
399
|
const code = error instanceof AssetLibraryImportError ? error.message
|
|
381
|
-
: error
|
|
382
|
-
:
|
|
383
|
-
: '
|
|
400
|
+
: error instanceof Error && ['invalid_audio_library_package', 'audio_library_engine_unavailable', 'audio_library_source_unverified', 'audio_representation_conflict'].includes(error.message) ? error.message
|
|
401
|
+
: error?.code === 'content_conflict' ? 'content_conflict'
|
|
402
|
+
: ['EACCES', 'EPERM'].includes(String(error?.code)) ? 'workspace_permission_denied'
|
|
403
|
+
: 'asset_library_import_failed';
|
|
384
404
|
result.failures.push({ assetId, error: code });
|
|
385
405
|
}
|
|
386
406
|
processed.add(assetId);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { posix } from 'node:path';
|
|
4
|
+
import { parseSoundDocument, soundSourceRenderHash, audioRepresentationSetHash, canonicalAudioJson, isSoundDocumentPath, workspaceRelativePathSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
5
|
+
import { audioProjectPath, audioError, digest } from './storage.js';
|
|
6
|
+
const hash = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
7
|
+
async function bytes(root, path, maximum) {
|
|
8
|
+
const file = await open(await audioProjectPath(root, path), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
9
|
+
try {
|
|
10
|
+
const before = await file.stat();
|
|
11
|
+
if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
|
|
12
|
+
throw audioError('invalid_audio_package', 400);
|
|
13
|
+
const result = await file.readFile(), after = await file.stat();
|
|
14
|
+
if (result.length !== before.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
|
|
15
|
+
throw audioError('invalid_audio_package', 400);
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
await file.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** A source alone is editable; only a fully verified manifest can identify a delivered sound bundle. */
|
|
23
|
+
export async function audioArtifactResources(root, primaryPath) {
|
|
24
|
+
return (await audioArtifactDetails(root, primaryPath)).resources;
|
|
25
|
+
}
|
|
26
|
+
export async function audioArtifactDetails(root, primaryPath) {
|
|
27
|
+
return inspectPackage(primaryPath, (path, maximum) => bytes(root, path, maximum));
|
|
28
|
+
}
|
|
29
|
+
/** Build snapshots retain the exact verified bytes, avoiding a second unverified file read. */
|
|
30
|
+
export async function readAudioArtifactPackage(root, primaryPath) {
|
|
31
|
+
const files = new Map();
|
|
32
|
+
const details = await inspectPackage(primaryPath, async (path, maximum) => {
|
|
33
|
+
const data = await bytes(root, path, maximum);
|
|
34
|
+
files.set(path, data);
|
|
35
|
+
return data;
|
|
36
|
+
});
|
|
37
|
+
return { ...details, files };
|
|
38
|
+
}
|
|
39
|
+
export async function validateAudioArtifactVersion(objects, primaryPath, files) {
|
|
40
|
+
if (files.length !== 5 || new Set(files.map(file => file.path)).size !== files.length ||
|
|
41
|
+
files.filter(file => file.role === 'primary').length !== 1 || !files.some(file => file.path === primaryPath && file.role === 'primary'))
|
|
42
|
+
throw audioError('invalid_audio_package', 400);
|
|
43
|
+
return inspectPackage(primaryPath, async (path, maximum) => {
|
|
44
|
+
const record = files.find(file => file.path === path);
|
|
45
|
+
if (!record || !hash(record.sha256))
|
|
46
|
+
throw audioError('invalid_audio_package', 400);
|
|
47
|
+
const result = await bytes(objects, record.sha256, maximum);
|
|
48
|
+
if (result.length !== record.size || digest(result) !== record.sha256)
|
|
49
|
+
throw audioError('invalid_audio_package', 400);
|
|
50
|
+
return result;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function inspectPackage(primaryPath, read) {
|
|
54
|
+
const sourceBytes = await read(primaryPath, 65536), document = parseSoundDocument(sourceBytes);
|
|
55
|
+
const sourceName = document.format === 'playcraft.score' ? 'sound.pcscore.json' : 'sound.pcaudio.json';
|
|
56
|
+
const directory = posix.dirname(primaryPath), manifestPath = `${directory}/manifest.json`;
|
|
57
|
+
const manifestBytes = await read(manifestPath, 65536);
|
|
58
|
+
let manifest;
|
|
59
|
+
try {
|
|
60
|
+
manifest = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(manifestBytes));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw audioError('invalid_audio_package', 400);
|
|
64
|
+
}
|
|
65
|
+
const allowed = ['version', 'sourcePath', 'sourceContentHash', 'sourceRenderHash', 'engine', 'rendererBuildHash', 'chromiumVersion',
|
|
66
|
+
'renderKey', 'pcmContentHash', 'files', 'representationSetHash'];
|
|
67
|
+
if (!manifest || Array.isArray(manifest) || Object.keys(manifest).length !== allowed.length || Object.keys(manifest).some(key => !allowed.includes(key)) ||
|
|
68
|
+
manifest.version !== 1 || !hash(manifest.sourceContentHash) || manifest.sourceContentHash !== digest(sourceBytes) ||
|
|
69
|
+
manifest.sourceRenderHash !== soundSourceRenderHash(document) || canonicalAudioJson(manifest.engine) !== canonicalAudioJson(document.engine) ||
|
|
70
|
+
!hash(manifest.rendererBuildHash) || !hash(manifest.renderKey) || !hash(manifest.pcmContentHash) ||
|
|
71
|
+
typeof manifest.chromiumVersion !== 'string' || !manifest.chromiumVersion || manifest.chromiumVersion.length > 128 ||
|
|
72
|
+
typeof manifest.sourcePath !== 'string' || !isSoundDocumentPath(manifest.sourcePath) ||
|
|
73
|
+
!manifest.sourcePath.endsWith(document.format === 'playcraft.score' ? '.pcscore.json' : '.pcaudio.json') ||
|
|
74
|
+
!workspaceRelativePathSchema.safeParse(manifest.sourcePath).success || manifest.sourcePath.split('/').some(part => ['.git', '.playcraft', 'node_modules'].includes(part)) ||
|
|
75
|
+
!hash(manifest.representationSetHash) || !Array.isArray(manifest.files) || manifest.files.length !== 4)
|
|
76
|
+
throw audioError('invalid_audio_package', 400);
|
|
77
|
+
if (digest(manifestBytes) !== digest(`${canonicalAudioJson(manifest)}\n`))
|
|
78
|
+
throw audioError('invalid_audio_package', 400);
|
|
79
|
+
const { representationSetHash, ...payload } = manifest;
|
|
80
|
+
if (audioRepresentationSetHash(payload) !== representationSetHash ||
|
|
81
|
+
directory !== `dist/audio/${digest(manifest.sourcePath)}/${manifest.sourceContentHash}/${representationSetHash}`)
|
|
82
|
+
throw audioError('invalid_audio_package', 400);
|
|
83
|
+
const expected = new Map([[sourceName, ['application/json', 'editable']], ['audition.wav', ['audio/wav', 'audition']],
|
|
84
|
+
['runtime.mjs', ['text/javascript', 'runtime']], ['sound.mjs', ['text/javascript', 'delivery']]]);
|
|
85
|
+
const resources = new Set([manifestPath]);
|
|
86
|
+
for (const file of manifest.files) {
|
|
87
|
+
if (!file || Array.isArray(file) || Object.keys(file).sort().join(',') !== 'contentHash,mimeType,path,role,size' ||
|
|
88
|
+
typeof file.path !== 'string' || !expected.has(file.path) || !hash(file.contentHash) || !Number.isSafeInteger(file.size) ||
|
|
89
|
+
file.size < 1 || file.size > (document.format === 'playcraft.score' ? 6 * 1024 * 1024 : 2 * 1024 * 1024))
|
|
90
|
+
throw audioError('invalid_audio_package', 400);
|
|
91
|
+
const [mimeType, role] = expected.get(file.path);
|
|
92
|
+
expected.delete(file.path);
|
|
93
|
+
if (file.mimeType !== mimeType || file.role !== role)
|
|
94
|
+
throw audioError('invalid_audio_package', 400);
|
|
95
|
+
const path = `${directory}/${file.path}`, member = await read(path, file.size);
|
|
96
|
+
if (member.length !== file.size || digest(member) !== file.contentHash)
|
|
97
|
+
throw audioError('invalid_audio_package', 400);
|
|
98
|
+
if (path !== primaryPath)
|
|
99
|
+
resources.add(path);
|
|
100
|
+
}
|
|
101
|
+
if (expected.size || !primaryPath.endsWith('/' + sourceName))
|
|
102
|
+
throw audioError('invalid_audio_package', 400);
|
|
103
|
+
return { resources: [...resources].sort(), name: document.name, sourcePath: manifest.sourcePath, sourceContentHash: manifest.sourceContentHash };
|
|
104
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open } from 'node:fs/promises';
|
|
3
|
+
import { workspaceAudioBindingSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
4
|
+
import { readAudioArtifactPackage } from './artifact-package.js';
|
|
5
|
+
import { audioProjectPath, digest } from './storage.js';
|
|
6
|
+
const maximumBindingBytes = 16_384;
|
|
7
|
+
/** Observe the tracked public binding without rewriting the source, binding, or private history. */
|
|
8
|
+
export async function readAudioBindingUsage(root, sourcePath, sourceContentHash, history, tracked) {
|
|
9
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_.-]{0,63}$/.test(tracked.eventName))
|
|
10
|
+
return undefined;
|
|
11
|
+
const bindingPath = `source/audio-bindings/${tracked.eventName}.json`;
|
|
12
|
+
const location = { status: 'integration_required', bindingPath, eventName: tracked.eventName };
|
|
13
|
+
const invalid = { ...location, bindingState: 'invalid' };
|
|
14
|
+
if (tracked.bindingPath !== bindingPath)
|
|
15
|
+
return invalid;
|
|
16
|
+
try {
|
|
17
|
+
const path = await audioProjectPath(root, tracked.bindingPath);
|
|
18
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
19
|
+
let bytes;
|
|
20
|
+
try {
|
|
21
|
+
const before = await file.stat();
|
|
22
|
+
if (!before.isFile() || before.nlink !== 1 || before.size < 1 || before.size > maximumBindingBytes)
|
|
23
|
+
return invalid;
|
|
24
|
+
// Recheck the path after opening, before reading any bytes through the retained descriptor.
|
|
25
|
+
const named = await lstat(await audioProjectPath(root, tracked.bindingPath));
|
|
26
|
+
if (named.dev !== before.dev || named.ino !== before.ino)
|
|
27
|
+
return invalid;
|
|
28
|
+
const buffer = Buffer.alloc(before.size + 1);
|
|
29
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0), after = await file.stat();
|
|
30
|
+
if (bytesRead !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs)
|
|
31
|
+
return invalid;
|
|
32
|
+
bytes = buffer.subarray(0, bytesRead);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await file.close();
|
|
36
|
+
}
|
|
37
|
+
const parsed = workspaceAudioBindingSchema.safeParse(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)));
|
|
38
|
+
if (!parsed.success || parsed.data.eventName !== tracked.eventName)
|
|
39
|
+
return invalid;
|
|
40
|
+
const binding = parsed.data;
|
|
41
|
+
if (binding.sourcePath !== sourcePath)
|
|
42
|
+
return { ...location, bindingState: 'different_source' };
|
|
43
|
+
if (!history.some(version => version.revision === binding.revision && version.contentHash === binding.sourceContentHash))
|
|
44
|
+
return invalid;
|
|
45
|
+
const directory = `dist/audio/${digest(sourcePath)}/${binding.sourceContentHash}/${binding.representationSetHash}`;
|
|
46
|
+
if (binding.modulePath !== `${directory}/sound.mjs`)
|
|
47
|
+
return invalid;
|
|
48
|
+
const bundle = await readAudioArtifactPackage(root, `${directory}/sound.${sourcePath.endsWith('.pcscore.json') ? 'pcscore' : 'pcaudio'}.json`);
|
|
49
|
+
const audition = bundle.files.get(`${directory}/audition.wav`);
|
|
50
|
+
if (bundle.sourcePath !== sourcePath || bundle.sourceContentHash !== binding.sourceContentHash
|
|
51
|
+
|| !audition || digest(audition) !== binding.auditionContentHash)
|
|
52
|
+
return invalid;
|
|
53
|
+
return { ...location, bindingState: binding.sourceContentHash === sourceContentHash ? 'current' : 'different_version',
|
|
54
|
+
sourceContentHash: binding.sourceContentHash, revision: binding.revision, modulePath: binding.modulePath,
|
|
55
|
+
representationSetHash: binding.representationSetHash,
|
|
56
|
+
...(binding.previousBinding ? { previousContentHash: binding.previousBinding } : {}) };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Only an absent binding is missing; broken package members are invalid bindings.
|
|
60
|
+
if (error.code === 'ENOENT') {
|
|
61
|
+
try {
|
|
62
|
+
await lstat(await audioProjectPath(root, tracked.bindingPath));
|
|
63
|
+
}
|
|
64
|
+
catch (missing) {
|
|
65
|
+
if (missing.code === 'ENOENT')
|
|
66
|
+
return { ...location, bindingState: 'missing' };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return invalid;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { lstat, readdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { canonicalAudioJson } from '@playcraft/common/cloud-workspace/runtime';
|
|
4
|
+
import { readAudioArtifactPackage } from './artifact-package.js';
|
|
5
|
+
import { audioError, audioProjectPath, digest, readAudioSource } from './storage.js';
|
|
6
|
+
import { ensureSharedDirectory, writeSharedFile } from '../shared-write.js';
|
|
7
|
+
import { readAudioProgramBuildInputs } from './program-build-inputs.js';
|
|
8
|
+
const hashPart = /^[a-f0-9]{64}$/;
|
|
9
|
+
const MAX_PACKAGES = 256;
|
|
10
|
+
const MAX_PACKAGE_BYTES = 64 * 1024 * 1024;
|
|
11
|
+
async function packagePaths(root) {
|
|
12
|
+
const paths = [];
|
|
13
|
+
const visit = async (path, depth) => {
|
|
14
|
+
const absolute = await audioProjectPath(root, path);
|
|
15
|
+
const stat = await lstat(absolute).catch(error => { if (error.code === 'ENOENT')
|
|
16
|
+
return undefined; throw error; });
|
|
17
|
+
if (!stat?.isDirectory() || stat.isSymbolicLink())
|
|
18
|
+
return;
|
|
19
|
+
if (depth === 3) {
|
|
20
|
+
if (paths.length >= MAX_PACKAGES)
|
|
21
|
+
throw audioError('audio_build_budget_exceeded', 422);
|
|
22
|
+
const names = await readdir(absolute);
|
|
23
|
+
for (const name of ['sound.pcaudio.json', 'sound.pcscore.json'])
|
|
24
|
+
if (names.includes(name)) {
|
|
25
|
+
if (paths.length >= MAX_PACKAGES)
|
|
26
|
+
throw audioError('audio_build_budget_exceeded', 422);
|
|
27
|
+
paths.push(`${path}/${name}`);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const entry of (await readdir(absolute, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name)))
|
|
32
|
+
if (entry.isDirectory() && !entry.isSymbolicLink() && hashPart.test(entry.name))
|
|
33
|
+
await visit(`${path}/${entry.name}`, depth + 1);
|
|
34
|
+
};
|
|
35
|
+
await visit('dist/audio', 0);
|
|
36
|
+
return paths;
|
|
37
|
+
}
|
|
38
|
+
/** Only validated immutable sound packages join source inputs; unrelated dist outputs stay excluded. */
|
|
39
|
+
export async function readAudioBuildInputs(root, destination) {
|
|
40
|
+
const packages = [];
|
|
41
|
+
let totalBytes = 0;
|
|
42
|
+
for (const primaryPath of await packagePaths(root)) {
|
|
43
|
+
let bundle;
|
|
44
|
+
try {
|
|
45
|
+
bundle = await readAudioArtifactPackage(root, primaryPath);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const sourceText = await readAudioSource(root, bundle.sourcePath);
|
|
51
|
+
const files = [...bundle.files].map(([path, bytes]) => ({ path, contentHash: digest(bytes) })).sort((a, b) => a.path.localeCompare(b.path));
|
|
52
|
+
totalBytes += [...bundle.files.values()].reduce((total, bytes) => total + bytes.length, 0) + (sourceText === undefined ? 0 : Buffer.byteLength(sourceText));
|
|
53
|
+
if (totalBytes > MAX_PACKAGE_BYTES)
|
|
54
|
+
throw audioError('audio_build_budget_exceeded', 422);
|
|
55
|
+
if (destination) {
|
|
56
|
+
for (const [path, bytes] of bundle.files) {
|
|
57
|
+
const target = await audioProjectPath(destination, path, true);
|
|
58
|
+
await ensureSharedDirectory(destination, dirname(target));
|
|
59
|
+
await writeSharedFile(destination, target, bytes);
|
|
60
|
+
}
|
|
61
|
+
if (sourceText !== undefined) {
|
|
62
|
+
const target = await audioProjectPath(destination, bundle.sourcePath, true);
|
|
63
|
+
await ensureSharedDirectory(destination, dirname(target));
|
|
64
|
+
await writeSharedFile(destination, target, sourceText);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
packages.push({ primaryPath, sourcePath: bundle.sourcePath, sourceContentHash: sourceText === undefined ? null : digest(sourceText), files });
|
|
68
|
+
}
|
|
69
|
+
const programs = await readAudioProgramBuildInputs(root, destination);
|
|
70
|
+
// Existing projects retain their original snapshot identity when no state programs are present.
|
|
71
|
+
return { version: 1, inputHash: digest(canonicalAudioJson(programs.packages.length ? { packages, programs } : packages)), packages,
|
|
72
|
+
...(programs.packages.length ? { programs } : {}) };
|
|
73
|
+
}
|
|
74
|
+
export async function verifyAudioBuildInputs(root, snapshot) {
|
|
75
|
+
if ((await readAudioBuildInputs(root)).inputHash !== snapshot.inputHash)
|
|
76
|
+
throw audioError('audio_build_input_changed');
|
|
77
|
+
}
|
|
78
|
+
export function markAudioBuildInputs(bytes, snapshot) {
|
|
79
|
+
return snapshot.packages.length || snapshot.programs?.packages.length ? Buffer.concat([Buffer.from(`<!--playcraft-audio-inputs:v1:${snapshot.inputHash}-->\n`), bytes]) : bytes;
|
|
80
|
+
}
|