@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
package/dist/assets/client.js
CHANGED
|
@@ -55,11 +55,11 @@ export function scopedAssetTask(scope, value) {
|
|
|
55
55
|
return task;
|
|
56
56
|
}
|
|
57
57
|
export async function readAssetTasks(client, scope) {
|
|
58
|
-
const value = assetGenerationTaskListSchema.parse(await client.get('/workspace-assets', { ...scope, action: 'list' }));
|
|
58
|
+
const value = assetGenerationTaskListSchema.parse(await client.get('/workspace-assets', { ...scope, audioProtocolVersion: '1', audioScoreProtocolVersion: '1', action: 'list' }));
|
|
59
59
|
return { tasks: value.tasks.map(task => scopedAssetTask(scope, task)) };
|
|
60
60
|
}
|
|
61
61
|
export async function readAssetTask(client, scope, taskId) {
|
|
62
|
-
const task = scopedAssetTask(scope, await client.get('/workspace-assets', { ...scope, action: 'get', taskId }));
|
|
62
|
+
const task = scopedAssetTask(scope, await client.get('/workspace-assets', { ...scope, audioProtocolVersion: '1', audioScoreProtocolVersion: '1', action: 'get', taskId }));
|
|
63
63
|
if (task.taskId !== taskId)
|
|
64
64
|
throw new Error(t('scopeMismatch'));
|
|
65
65
|
return task;
|
|
@@ -99,6 +99,8 @@ async function verifyFile(path, file) {
|
|
|
99
99
|
/** Stage and verify every member before publishing the new local output. */
|
|
100
100
|
export async function downloadAssetTask(client, scope, taskId, output, versionId) {
|
|
101
101
|
const task = await readAssetTask(client, scope, taskId);
|
|
102
|
+
if (task.audioResult)
|
|
103
|
+
throw new Error(t('audioSourceRequired'));
|
|
102
104
|
if (!['awaiting_adoption', 'saved'].includes(task.status) || !task.output)
|
|
103
105
|
throw new Error(t('outputUnavailable'));
|
|
104
106
|
let bundle = task.bundle, entry = task.output;
|
package/dist/assets/messages.js
CHANGED
|
@@ -22,6 +22,7 @@ const messages = {
|
|
|
22
22
|
invalidContent: 'Downloaded content does not match the task size or content digest.', failed: 'Asset operation failed',
|
|
23
23
|
timeoutReached: 'Waiting timed out. Use assets get or assets wait with the same task identifier; generation was not cancelled.',
|
|
24
24
|
requestTooLarge: 'The request document exceeds the permitted size.',
|
|
25
|
+
audioSourceRequired: 'This task contains editable sound candidates. Use playcraft audio source inspect/render/adopt with its task and candidate identifiers.',
|
|
25
26
|
},
|
|
26
27
|
zh: {
|
|
27
28
|
topic: '生成工作区素材,管理工作区面板中同一批任务', estimate: '估算生成费用,不创建任务', generate: '创建可恢复的素材生成任务',
|
|
@@ -40,6 +41,7 @@ const messages = {
|
|
|
40
41
|
invalidContent: '下载内容与任务记录的大小或内容摘要不一致。', failed: '素材操作失败',
|
|
41
42
|
timeoutReached: '等待已超时。请使用同一任务标识运行 assets get 或 assets wait,生成任务未取消。',
|
|
42
43
|
requestTooLarge: '请求文档超过允许大小。',
|
|
44
|
+
audioSourceRequired: '此任务包含可编辑声音候选。请使用 playcraft audio source inspect/render/adopt,并传入任务和候选标识。',
|
|
43
45
|
},
|
|
44
46
|
};
|
|
45
47
|
export function assetMessage(key) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const copy = {
|
|
2
|
+
en: { audioTopic: 'Process audio files and manage workspace sound and music assets', topic: 'Create and revise workspace sound effects and music', capabilities: 'Inspect available sound features',
|
|
3
|
+
inspect: 'Read the exact source, audition status and history', create: 'Generate short sound candidates in the workspace asset tasks',
|
|
4
|
+
edit: 'Save a source revision or a shape preserving simple adjustment', render: 'Render an exact source revision',
|
|
5
|
+
adopt: 'Save a private candidate and its complete representation bundle', rollback: 'Restore an earlier source revision',
|
|
6
|
+
bind: 'Record a version bound event handoff for game integration', scope: 'Workspace identifier; inferred from the project',
|
|
7
|
+
directory: 'Engineering directory; inferred from the project', path: 'Workspace source path ending in .pcaudio.json or .pcscore.json',
|
|
8
|
+
task: 'Existing generation task identifier', candidate: 'Candidate identifier', revision: 'Expected current source revision',
|
|
9
|
+
hash: 'Expected current source content digest', document: 'Validated sound source document', request: 'Generation request document',
|
|
10
|
+
requestId: 'Stable operation identity; reuse after an uncertain response', output: 'Destination workspace source path',
|
|
11
|
+
destinationRevision: 'Expected destination revision, or 0 when absent', destinationHash: 'Expected destination digest when replacing a source',
|
|
12
|
+
version: 'Historical source revision to restore', event: 'Explicit game event name; actual code integration is a separate verified step',
|
|
13
|
+
macro: 'Simple adjustment: volume, pitch, duration, brightness; music also supports tempo', value: 'Adjustment value: gain, semitones, seconds, cutoff multiplier or beats per minute',
|
|
14
|
+
lockedTracks: 'Comma separated locked music track identifiers', preserveMelody: 'Preserve pitched note identities, timing and melody',
|
|
15
|
+
selection: 'Frozen music selection document with trackIds, noteIds and rangeBeats', adjustment: 'Structured music adjustment document',
|
|
16
|
+
voices: 'Comma separated selected voice identifiers', invalid: 'Invalid sound command arguments.', failed: 'Sound operation failed',
|
|
17
|
+
locked: 'Comma separated locked voice identifiers from the selected context', preserve: 'Comma separated protected parameter targets from the selected context',
|
|
18
|
+
},
|
|
19
|
+
zh: { audioTopic: '处理音频文件,生成和续改工作区音效与音乐', topic: '生成和续改工作区音效与音乐', capabilities: '查看声音能力', inspect: '读取精确源、试听状态和历史',
|
|
20
|
+
create: '在工作区素材任务中生成短音效候选', edit: '保存源修订或保留曲线形状的简单调整', render: '渲染精确源版本',
|
|
21
|
+
adopt: '保存私有候选及完整表示包', rollback: '恢复历史源版本', bind: '记录固定声音版本的游戏事件交接',
|
|
22
|
+
scope: '工作区标识;从工程推断', directory: '工程目录;从工程推断', path: '以 .pcaudio.json 或 .pcscore.json 结尾的工程源路径',
|
|
23
|
+
task: '已有生成任务标识', candidate: '候选标识', revision: '预期当前源修订号', hash: '预期当前源内容摘要',
|
|
24
|
+
document: '声音源文档路径', request: '生成请求文档路径', requestId: '稳定操作标识;响应不确定时复用',
|
|
25
|
+
output: '目标工程源路径', destinationRevision: '预期目标修订;目标不存在时为 0', destinationHash: '替换目标时预期的内容摘要',
|
|
26
|
+
version: '要恢复的历史源修订', event: '明确的游戏事件名称;代码接入需要另外验证', macro: '简单调整:volume、pitch、duration、brightness;音乐另支持 tempo',
|
|
27
|
+
lockedTracks: '用逗号分隔的锁定音乐轨道标识', preserveMelody: '保留旋律音符身份、拍点与音高',
|
|
28
|
+
selection: '含轨道、音符与节拍范围的冻结选区文档', adjustment: '结构化音乐调整文档路径',
|
|
29
|
+
value: '调整值:音量、半音、秒或滤波倍数', voices: '用逗号分隔的选中声部标识', invalid: '声音命令参数无效。', failed: '声音操作失败',
|
|
30
|
+
locked: '用逗号分隔的锁定声部;来自所选上下文', preserve: '用逗号分隔的保护参数路径;来自所选上下文',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
export function audioSourceMessage(key) {
|
|
34
|
+
return copy[(process.env.PLAYCRAFT_LANGUAGE || process.env.LANG || 'en').startsWith('zh') ? 'zh' : 'en'][key];
|
|
35
|
+
}
|
package/dist/commands/assets.js
CHANGED
|
@@ -37,7 +37,7 @@ async function run(command, options, action) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
function write(client, body) {
|
|
40
|
-
return client.post(body.action === 'animation-draft' ? '/workspace-assets/animation-draft' : '/workspace-assets', workspaceAssetToolWriteSchema.parse(body));
|
|
40
|
+
return client.post(body.action === 'animation-draft' ? '/workspace-assets/animation-draft' : '/workspace-assets', workspaceAssetToolWriteSchema.parse({ ...body, audioProtocolVersion: 1, audioScoreProtocolVersion: 1 }));
|
|
41
41
|
}
|
|
42
42
|
export function registerWorkspaceAssetCommands(program) {
|
|
43
43
|
const assets = program.command('assets').description(t('topic'));
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { applyAudioAdjustment, applyScoreAdjustment, parseSoundDocument, workspaceAudioProtectionSchema, scoreProtectionSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
4
|
+
import { resolveAssetScope, readAssetRequest } from '../assets/client.js';
|
|
5
|
+
import { AgentApiClient } from '../utils/agent-api-client.js';
|
|
6
|
+
import { resolveSandboxPath } from '../utils/sandbox-path.js';
|
|
7
|
+
import { audioSourceMessage as t } from '../audio-source/messages.js';
|
|
8
|
+
function target(options) {
|
|
9
|
+
if (options.path && !options.task && !options.candidate)
|
|
10
|
+
return { kind: 'workspace-file', path: options.path };
|
|
11
|
+
if (options.task && options.candidate && !options.path)
|
|
12
|
+
return { kind: 'generation-candidate', taskId: options.task, candidateId: options.candidate };
|
|
13
|
+
throw new Error(t('invalid'));
|
|
14
|
+
}
|
|
15
|
+
function expected(options) {
|
|
16
|
+
const revision = Number(options.revision);
|
|
17
|
+
if (!Number.isSafeInteger(revision) || revision < 1 || !/^[a-f0-9]{64}$/.test(options.hash ?? ''))
|
|
18
|
+
throw new Error(t('invalid'));
|
|
19
|
+
return { expectedRevision: revision, expectedContentHash: options.hash };
|
|
20
|
+
}
|
|
21
|
+
function scoped(command) { return command.option('--workspace <id>', t('scope')).option('--directory <name>', t('directory')); }
|
|
22
|
+
function selected(command) { return scoped(command).option('--path <path>', t('path')).option('--task <id>', t('task')).option('--candidate <id>', t('candidate')); }
|
|
23
|
+
function writable(command) {
|
|
24
|
+
return selected(command).requiredOption('--revision <number>', t('revision')).requiredOption('--hash <digest>', t('hash'))
|
|
25
|
+
.requiredOption('--request-id <id>', t('requestId')).option('--locked-voices <ids>', t('locked')).option('--preserve-targets <targets>', t('preserve'))
|
|
26
|
+
.option('--locked-tracks <ids>', t('lockedTracks')).option('--preserve-melody', t('preserveMelody')).option('--selection <file>', t('selection'));
|
|
27
|
+
}
|
|
28
|
+
async function protection(options, score = false) {
|
|
29
|
+
const list = (value) => value?.split(',').map(item => item.trim()).filter(Boolean) ?? [];
|
|
30
|
+
if (score || options.lockedTracks !== undefined || options.preserveMelody !== undefined || options.selection) {
|
|
31
|
+
if (options.lockedVoices !== undefined || options.preserveTargets !== undefined)
|
|
32
|
+
throw new Error(t('invalid'));
|
|
33
|
+
return scoreProtectionSchema.parse({ lockedTrackIds: list(options.lockedTracks), preserveMelody: options.preserveMelody ?? false,
|
|
34
|
+
...(options.selection ? { selection: await readAssetRequest(options.selection) } : {}) });
|
|
35
|
+
}
|
|
36
|
+
return workspaceAudioProtectionSchema.parse({ lockedVoiceIds: list(options.lockedVoices), preserveTargets: list(options.preserveTargets) });
|
|
37
|
+
}
|
|
38
|
+
function output(action) {
|
|
39
|
+
return async (options) => {
|
|
40
|
+
try {
|
|
41
|
+
console.log(JSON.stringify(await action(options), null, 2));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error(`${t('failed')}: ${error instanceof Error ? error.message : String(error)}`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function action(options, request) {
|
|
50
|
+
return new AgentApiClient().post('/workspace-audio', { ...await resolveAssetScope(options), audioScoreProtocolVersion: 1, request });
|
|
51
|
+
}
|
|
52
|
+
export function registerAudioSourceCommands(audio) {
|
|
53
|
+
const source = audio.command('source').description(t('topic'));
|
|
54
|
+
scoped(source.command('capabilities').description(t('capabilities'))).action(output(async (options) => new AgentApiClient().get('/workspace-audio', { ...await resolveAssetScope(options), audioScoreProtocolVersion: '1', action: 'capabilities' }, { signal: AbortSignal.timeout(165_000) })));
|
|
55
|
+
selected(source.command('inspect').description(t('inspect'))).action(output(async (options) => {
|
|
56
|
+
const value = target(options);
|
|
57
|
+
return new AgentApiClient().get('/workspace-audio', { ...await resolveAssetScope(options), audioScoreProtocolVersion: '1', action: 'read',
|
|
58
|
+
...(value.kind === 'workspace-file' ? { path: value.path } : { taskId: value.taskId, candidateId: value.candidateId }) });
|
|
59
|
+
}));
|
|
60
|
+
scoped(source.command('create').description(t('create'))).requiredOption('--request <file>', t('request'))
|
|
61
|
+
.requiredOption('--request-id <id>', t('requestId'))
|
|
62
|
+
.action(output(async (options) => {
|
|
63
|
+
const request = await readAssetRequest(options.request);
|
|
64
|
+
if (!request || typeof request !== 'object' || Array.isArray(request))
|
|
65
|
+
throw new Error(t('invalid'));
|
|
66
|
+
return new AgentApiClient().post('/workspace-assets', { ...await resolveAssetScope(options), audioProtocolVersion: 1, audioScoreProtocolVersion: 1,
|
|
67
|
+
action: 'create', clientRequestId: options.requestId,
|
|
68
|
+
request: { ...request, kind: 'audio', representation: ('representation' in request ? request.representation : undefined) ?? 'synth-sfx' } });
|
|
69
|
+
}));
|
|
70
|
+
for (const name of ['edit', 'save'])
|
|
71
|
+
writable(source.command(name).description(t('edit')))
|
|
72
|
+
.option('--document <file>', t('document')).option('--macro <kind>', t('macro')).option('--value <number>', t('value')).option('--voices <ids>', t('voices'))
|
|
73
|
+
.option('--adjustment <file>', t('adjustment'))
|
|
74
|
+
.action(output(async (options) => {
|
|
75
|
+
let document;
|
|
76
|
+
if (options.document && !options.macro && !options.adjustment)
|
|
77
|
+
document = parseSoundDocument(await readFile(resolveSandboxPath(options.document)));
|
|
78
|
+
else if (!options.document && (!!options.macro !== !!options.adjustment)) {
|
|
79
|
+
const value = target(options), state = await new AgentApiClient().get('/workspace-audio', { ...await resolveAssetScope(options), audioScoreProtocolVersion: '1', action: 'read', ...(value.kind === 'workspace-file' ? { path: value.path } : { taskId: value.taskId, candidateId: value.candidateId }) });
|
|
80
|
+
if (state.revision !== expected(options).expectedRevision || state.contentHash !== options.hash)
|
|
81
|
+
throw new Error('audio_revision_conflict');
|
|
82
|
+
const policy = await protection(options, state.document.format === 'playcraft.score');
|
|
83
|
+
if (state.document.format === 'playcraft.score') {
|
|
84
|
+
const adjustment = options.adjustment ? await readAssetRequest(options.adjustment) : { kind: options.macro, value: Number(options.value) };
|
|
85
|
+
document = applyScoreAdjustment(state.document, adjustment, scoreProtectionSchema.parse(policy));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
if (options.adjustment || !['volume', 'pitch', 'duration', 'brightness'].includes(options.macro))
|
|
89
|
+
throw new Error(t('invalid'));
|
|
90
|
+
document = applyAudioAdjustment(state.document, { kind: options.macro, value: Number(options.value),
|
|
91
|
+
...(options.voices ? { voiceIds: options.voices.split(',') } : {}) }, policy);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else
|
|
95
|
+
throw new Error(t('invalid'));
|
|
96
|
+
return action(options, { action: 'save', requestId: options.requestId ?? randomUUID(), target: target(options), ...expected(options), document,
|
|
97
|
+
protection: await protection(options, document.format === 'playcraft.score') });
|
|
98
|
+
}));
|
|
99
|
+
writable(source.command('render').description(t('render'))).action(output(options => action(options, { action: 'render', requestId: options.requestId ?? randomUUID(), target: target(options), ...expected(options) })));
|
|
100
|
+
writable(source.command('adopt').description(t('adopt'))).requiredOption('--output <path>', t('output'))
|
|
101
|
+
.option('--destination-revision <number>', t('destinationRevision'), '0').option('--destination-hash <digest>', t('destinationHash'))
|
|
102
|
+
.action(output(options => action(options, { action: 'adopt', requestId: options.requestId ?? randomUUID(), target: target(options), ...expected(options),
|
|
103
|
+
path: options.output, expectedDestinationRevision: Number(options.destinationRevision), expectedDestinationContentHash: options.destinationHash ?? null })));
|
|
104
|
+
writable(source.command('rollback').description(t('rollback'))).requiredOption('--version <number>', t('version'))
|
|
105
|
+
.action(output(options => action(options, { action: 'rollback', requestId: options.requestId ?? randomUUID(), target: target(options), ...expected(options), versionRevision: Number(options.version) })));
|
|
106
|
+
for (const name of ['bind', 'integrate'])
|
|
107
|
+
writable(source.command(name).description(t('bind'))).requiredOption('--event <name>', t('event'))
|
|
108
|
+
.action(output(options => action(options, { action: 'bind', requestId: options.requestId ?? randomUUID(), target: target(options), ...expected(options), eventName: options.event })));
|
|
109
|
+
}
|
package/dist/commands/audio.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getAudioInfo, compressAudio, trimAudio, convertAudio, fadeAudio, concatAudio, normalizeAudio, loopAudio, mixAudio, } from '../utils/audio-processor.js';
|
|
2
|
+
import { registerAudioSourceCommands } from './audio-source.js';
|
|
3
|
+
import { audioSourceMessage } from '../audio-source/messages.js';
|
|
2
4
|
/**
|
|
3
5
|
* Parse and validate `audio mix --volumes` / `--offsets` against `--inputs`.
|
|
4
6
|
*/
|
|
@@ -45,7 +47,8 @@ function buildValidatedMixTracks(inputs, volumeStrs, offsetStrs) {
|
|
|
45
47
|
export function registerAudioCommands(program) {
|
|
46
48
|
const audio = program
|
|
47
49
|
.command('audio')
|
|
48
|
-
.description('
|
|
50
|
+
.description(audioSourceMessage('audioTopic'));
|
|
51
|
+
registerAudioSourceCommands(audio);
|
|
49
52
|
// ── audio-info ──────────────────────────────────────────────────────────
|
|
50
53
|
audio
|
|
51
54
|
.command('info')
|
|
@@ -67,15 +70,19 @@ export function registerAudioCommands(program) {
|
|
|
67
70
|
.description('压缩音频文件(降低比特率/采样率,转单声道,或指定目标体积)')
|
|
68
71
|
.requiredOption('--input <file>', '输入音频文件路径')
|
|
69
72
|
.requiredOption('--output <file>', '输出音频文件路径')
|
|
70
|
-
.option('--
|
|
71
|
-
.option('--
|
|
73
|
+
.option('--format <format>', '输出格式:mp3、ogg、opus、wav(默认由扩展名确定)')
|
|
74
|
+
.option('--bitrate <kbps>', '目标比特率(kbps),如 64', Number)
|
|
75
|
+
.option('--sample-rate <hz>', '目标采样率(Hz),如 22050', Number)
|
|
76
|
+
.option('--channels <count>', '输出声道数:1 或 2', Number)
|
|
72
77
|
.option('--mono', '转为单声道', false)
|
|
73
|
-
.option('--target-size <size>', '
|
|
78
|
+
.option('--target-size <size>', '输出体积上限(不能满足时失败),如 30KB、1.5MB')
|
|
74
79
|
.action(async (opts) => {
|
|
75
80
|
try {
|
|
76
81
|
await compressAudio(opts.input, opts.output, {
|
|
82
|
+
format: opts.format,
|
|
77
83
|
bitrate: opts.bitrate,
|
|
78
84
|
sampleRate: opts.sampleRate,
|
|
85
|
+
channels: opts.channels,
|
|
79
86
|
mono: opts.mono,
|
|
80
87
|
targetSize: opts.targetSize,
|
|
81
88
|
});
|
package/dist/commands/build.js
CHANGED
|
@@ -9,6 +9,7 @@ import inquirer from 'inquirer';
|
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { generatedWorkspace } from '../workspace-runtime/workspaces/artifact-receipts.js';
|
|
11
11
|
import { isolateWorkspaceForBuild } from '../workspace-runtime/workspaces/build-isolation.js';
|
|
12
|
+
import { markAudioBuildInputs, verifyAudioBuildInputs } from '../workspace-runtime/workspaces/audio-assets/build-inputs.js';
|
|
12
13
|
import { finalizeWorkspacePlayable, readWorkspacePlayable, workspacePlayableSignature } from '../workspace-runtime/previews/finalize.js';
|
|
13
14
|
import { AgentApiClient } from '../utils/agent-api-client.js';
|
|
14
15
|
import { WORKSPACE_PLAYABLE_OUTPUT_PATH } from '@playcraft/common/cloud-workspace/runtime';
|
|
@@ -302,13 +303,14 @@ export async function buildCommand(projectPath, options) {
|
|
|
302
303
|
try {
|
|
303
304
|
const signature = canonicalOutput ? await workspacePlayableSignature(workspace.root) : undefined;
|
|
304
305
|
const project = path.join(scratch, 'project'), output = path.join(scratch, 'output');
|
|
305
|
-
await isolateWorkspaceForBuild(workspace.root, project);
|
|
306
|
+
const audioInputs = await isolateWorkspaceForBuild(workspace.root, project);
|
|
306
307
|
await buildCommand(project, { ...options, workspaceArtifact: false, output });
|
|
308
|
+
await verifyAudioBuildInputs(workspace.root, audioInputs);
|
|
307
309
|
if (canonicalOutput) {
|
|
308
310
|
const entries = await fs.readdir(output, { withFileTypes: true });
|
|
309
311
|
if (entries.length !== 1 || !entries[0].isFile() || !entries[0].name.endsWith('.html'))
|
|
310
312
|
throw new Error('preview_not_single_file');
|
|
311
|
-
await finalizeWorkspacePlayable(workspace.root, await fs.readFile(path.join(output, entries[0].name)), 'playcraft build', { expectedSignature: signature });
|
|
313
|
+
await finalizeWorkspacePlayable(workspace.root, markAudioBuildInputs(await fs.readFile(path.join(output, entries[0].name)), audioInputs), 'playcraft build', { expectedSignature: signature });
|
|
312
314
|
console.log(path.join(workspace.root, WORKSPACE_PLAYABLE_OUTPUT_PATH));
|
|
313
315
|
try {
|
|
314
316
|
const { bytes } = await readWorkspacePlayable(workspace.root);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path';
|
|
2
|
+
import { workspaceDirectoryRegistrationSchema, workspaceExecutionContextQuerySchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
3
|
+
import { AgentApiClient } from '../utils/agent-api-client.js';
|
|
4
|
+
import { generatedWorkspace } from '../workspace-runtime/workspaces/artifact-receipts.js';
|
|
5
|
+
import { handleError } from './tools-utils.js';
|
|
6
|
+
import { readExecutionContextFromBridge } from '../utils/execution-context-bridge.js';
|
|
7
|
+
const messages = {
|
|
8
|
+
en: { description: 'Read selected current execution context from the platform',
|
|
9
|
+
fields: 'Comma-separated fields to read: user,team,workspace,clock',
|
|
10
|
+
revision: 'Previous revision of the same fields, only when their values remain visible',
|
|
11
|
+
scope: 'Run this command from the current cloud workspace basic or Remix engineering directory.' },
|
|
12
|
+
zh: { description: '按需读取平台解析的当前执行上下文',
|
|
13
|
+
fields: '需要读取的字段,以逗号分隔:user,team,workspace,clock',
|
|
14
|
+
revision: '相同字段的已有修订号,仅在对应结果仍可见时使用',
|
|
15
|
+
scope: '请在当前云工作区的 basic 或 Remix 工程目录执行此命令。' },
|
|
16
|
+
};
|
|
17
|
+
const language = () => /^zh/i.test(process.env.PLAYCRAFT_LANG ?? process.env.LANG ?? '') ? messages.zh : messages.en;
|
|
18
|
+
const scopeSchema = workspaceDirectoryRegistrationSchema.pick({ workspaceId: true, directoryName: true });
|
|
19
|
+
export async function readExecutionContext(request, client, cwd = process.cwd()) {
|
|
20
|
+
const query = workspaceExecutionContextQuerySchema.parse(request);
|
|
21
|
+
const mediated = await readExecutionContextFromBridge(query);
|
|
22
|
+
if (mediated)
|
|
23
|
+
return mediated;
|
|
24
|
+
const context = await generatedWorkspace(cwd);
|
|
25
|
+
const scope = scopeSchema.safeParse(context ? { workspaceId: context.workspaceId, directoryName: basename(context.root) } : undefined);
|
|
26
|
+
if (!context || !scope.success || basename(dirname(context.root)) !== context.workspaceId)
|
|
27
|
+
throw new Error(language().scope);
|
|
28
|
+
return (client ?? new AgentApiClient()).readWorkspaceExecutionContext(scope.data, query);
|
|
29
|
+
}
|
|
30
|
+
export function registerContextCommands(tools) {
|
|
31
|
+
const t = language();
|
|
32
|
+
tools.command('get-context').description(t.description)
|
|
33
|
+
.requiredOption('--fields <fields>', t.fields)
|
|
34
|
+
.option('--known-revision <revision>', t.revision)
|
|
35
|
+
.option('--json', 'JSON (JavaScript Object Notation, 数据交换格式)')
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
try {
|
|
38
|
+
const request = workspaceExecutionContextQuerySchema.parse({ fields: options.fields.split(',').map((field) => field.trim()),
|
|
39
|
+
...(options.knownRevision ? { knownRevision: options.knownRevision } : {}) });
|
|
40
|
+
console.log(JSON.stringify(await readExecutionContext(request), null, options.json ? undefined : 2));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
handleError(error);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
package/dist/commands/tools.js
CHANGED
|
@@ -5,6 +5,7 @@ import { register3DCommands } from './tools-3d.js';
|
|
|
5
5
|
import { registerDetectCommand } from './tools-detect.js';
|
|
6
6
|
import { registerVideoReviewCommand } from './tools-video-review.js';
|
|
7
7
|
import { registerImageContextCommand } from './tools-image-context.js';
|
|
8
|
+
import { registerContextCommands } from './tools-context.js';
|
|
8
9
|
export function registerToolsCommands(program) {
|
|
9
10
|
const tools = program
|
|
10
11
|
.command('tools')
|
|
@@ -16,4 +17,5 @@ export function registerToolsCommands(program) {
|
|
|
16
17
|
register3DCommands(tools);
|
|
17
18
|
registerDetectCommand(tools);
|
|
18
19
|
registerVideoReviewCommand(tools);
|
|
20
|
+
registerContextCommands(tools);
|
|
19
21
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
|
-
import { canonicalProjectSkillsJson, declarationDigest, dependencyDigest, extractBundleArchive, readProjectSkillSource, readProjectSkillsLockFile, validateProjectSkillsSnapshot, PROJECT_SKILLS_DECLARATION_PATH, PROJECT_SKILLS_LOCK_PATH, } from '@playcraft/common/project-skills';
|
|
4
|
+
import { canonicalProjectSkillsJson, declarationDigest, dependencyDigest, extractBundleArchive, sourceTreeDigest, readProjectSkillSource, readProjectSkillsLockFile, validateProjectSkillsSnapshot, PROJECT_SKILLS_DECLARATION_PATH, PROJECT_SKILLS_LOCK_PATH, } from '@playcraft/common/project-skills';
|
|
5
5
|
import { LEGACY_VIDEO_SOURCE_HASHES, WORKSPACE_BUILTIN_SKILL_NAMES } from '@playcraft/common/cloud-workspace/runtime';
|
|
6
6
|
import { projectSkillsCache } from './local-store.js';
|
|
7
7
|
export const IMPORTABLE_BUILTIN_SKILLS = WORKSPACE_BUILTIN_SKILL_NAMES;
|
|
@@ -20,14 +20,15 @@ async function safeDirectory(root, path) {
|
|
|
20
20
|
return current;
|
|
21
21
|
}
|
|
22
22
|
/** Selects published platform revisions once; neither directory contents nor names authorize a replacement. */
|
|
23
|
-
export async function selectPlatformSkillSnapshot(names, access) {
|
|
23
|
+
export async function selectPlatformSkillSnapshot(names, access, lockVersion = 4, existingLock = false) {
|
|
24
24
|
if (!names.length || new Set(names).size !== names.length
|
|
25
25
|
|| names.some(name => !IMPORTABLE_BUILTIN_SKILLS.includes(name)))
|
|
26
26
|
throw new Error('workspace_skills_builtin_unknown');
|
|
27
27
|
if (!access)
|
|
28
28
|
throw new Error('workspace_skills_platform_required');
|
|
29
29
|
const requested = names.map(platformSkillRef);
|
|
30
|
-
const response = await access.resolve(requested)
|
|
30
|
+
const response = await (lockVersion === 4 ? existingLock ? access.resolve(requested, 4, 4) : access.resolve(requested, 4)
|
|
31
|
+
: access.resolve(requested));
|
|
31
32
|
const snapshot = validateProjectSkillsSnapshot(response?.declaration, response?.lock);
|
|
32
33
|
if (snapshot.declaration.entries.length !== names.length || snapshot.lock.entries.length !== names.length)
|
|
33
34
|
throw new Error('workspace_skills_invalid');
|
|
@@ -41,16 +42,19 @@ export async function selectPlatformSkillSnapshot(names, access) {
|
|
|
41
42
|
}
|
|
42
43
|
const artifacts = await Promise.all(snapshot.lock.entries.map(async (entry) => {
|
|
43
44
|
const archive = await access.loadArtifact(entry);
|
|
44
|
-
|
|
45
|
+
const artifactHash = createHash('sha256').update(archive).digest('hex');
|
|
46
|
+
if (archive.length > 80 * 1024 * 1024 || (snapshot.lock.lockVersion === 3 && artifactHash !== entry.revision.artifactHash))
|
|
45
47
|
throw new Error('workspace_skills_digest_mismatch');
|
|
46
48
|
const extracted = extractBundleArchive(archive);
|
|
47
49
|
if (extracted.issues.length || !extracted.files.some(file => file.path === 'SKILL.md'))
|
|
48
50
|
throw new Error('workspace_skills_invalid_bundle');
|
|
49
|
-
|
|
51
|
+
if (snapshot.lock.lockVersion === 4 && sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== entry.revision.contentHash)
|
|
52
|
+
throw new Error('workspace_skills_digest_mismatch');
|
|
53
|
+
return { artifactHash, archive, contentHash: snapshot.lock.lockVersion === 4 ? entry.revision.contentHash : undefined };
|
|
50
54
|
}));
|
|
51
55
|
return { ...snapshot, artifacts };
|
|
52
56
|
}
|
|
53
|
-
async function commitSnapshot(directory, snapshot, originalDeclaration, staged) {
|
|
57
|
+
async function commitSnapshot(directory, snapshot, originalDeclaration, originalLock, staged) {
|
|
54
58
|
const declarationPath = join(directory, PROJECT_SKILLS_DECLARATION_PATH);
|
|
55
59
|
const pending = [];
|
|
56
60
|
for (const [path, value] of [[PROJECT_SKILLS_DECLARATION_PATH, snapshot.declaration], [PROJECT_SKILLS_LOCK_PATH, snapshot.lock]]) {
|
|
@@ -59,11 +63,25 @@ async function commitSnapshot(directory, snapshot, originalDeclaration, staged)
|
|
|
59
63
|
await writeFile(temporary, `${canonicalProjectSkillsJson(value)}\n`, { flag: 'wx', mode: 0o644 });
|
|
60
64
|
pending.push(temporary);
|
|
61
65
|
}
|
|
66
|
+
const [currentDeclaration, currentLock] = await Promise.all([
|
|
67
|
+
readFile(declarationPath).catch((error) => { if (error.code === 'ENOENT')
|
|
68
|
+
return null; throw error; }),
|
|
69
|
+
readProjectSkillsLockFile(directory),
|
|
70
|
+
]);
|
|
71
|
+
if (!currentDeclaration?.equals(originalDeclaration) || currentLock !== originalLock)
|
|
72
|
+
throw new Error('skills_revision_conflict');
|
|
62
73
|
await rename(pending[0], declarationPath);
|
|
63
74
|
try {
|
|
64
75
|
await rename(pending[1], join(directory, PROJECT_SKILLS_LOCK_PATH));
|
|
65
76
|
}
|
|
66
77
|
catch (error) {
|
|
78
|
+
const current = await readFile(declarationPath).catch((readError) => {
|
|
79
|
+
if (readError.code === 'ENOENT')
|
|
80
|
+
return null;
|
|
81
|
+
throw readError;
|
|
82
|
+
});
|
|
83
|
+
if (!current?.equals(Buffer.from(`${canonicalProjectSkillsJson(snapshot.declaration)}\n`)))
|
|
84
|
+
throw new Error('skills_revision_conflict');
|
|
67
85
|
// The old lock still exists. Restore its exact declaration before releasing the writer lease.
|
|
68
86
|
const restore = `${declarationPath}.${randomUUID()}.tmp`;
|
|
69
87
|
staged.push(restore);
|
|
@@ -75,13 +93,23 @@ async function commitSnapshot(directory, snapshot, originalDeclaration, staged)
|
|
|
75
93
|
async function cacheArtifacts(cache, selected) {
|
|
76
94
|
await mkdir(cache, { recursive: true, mode: 0o700 });
|
|
77
95
|
for (const artifact of selected.artifacts) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
for (const name of [`${artifact.artifactHash}.tar.gz`, ...(artifact.contentHash ? [`content-${artifact.contentHash}.tar.gz`] : [])]) {
|
|
97
|
+
const path = join(cache, name);
|
|
98
|
+
try {
|
|
99
|
+
await writeFile(path, artifact.archive, { flag: 'wx', mode: 0o600 });
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error.code !== 'EEXIST')
|
|
103
|
+
throw error;
|
|
104
|
+
const existing = await readFile(path);
|
|
105
|
+
if (name.startsWith('content-')) {
|
|
106
|
+
const extracted = extractBundleArchive(existing);
|
|
107
|
+
if (extracted.issues.length || sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== artifact.contentHash)
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
else if (!existing.equals(artifact.archive))
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
85
113
|
}
|
|
86
114
|
}
|
|
87
115
|
}
|
|
@@ -100,7 +128,8 @@ export async function importBuiltinProjectSkills(directory, options, cache = pro
|
|
|
100
128
|
if (!declarationStat.isFile() || declarationStat.isSymbolicLink() || declarationStat.size > 1024 * 1024)
|
|
101
129
|
throw new Error('workspace_skills_invalid');
|
|
102
130
|
const originalDeclaration = await readFile(declarationPath);
|
|
103
|
-
const
|
|
131
|
+
const originalLock = await readProjectSkillsLockFile(directory);
|
|
132
|
+
const previous = validateProjectSkillsSnapshot(originalDeclaration.toString('utf8'), originalLock);
|
|
104
133
|
if (options.expectedDeclarationHash !== previous.lock.declarationHash || options.expectedLockHash !== hash(previous.lock))
|
|
105
134
|
throw new Error('skills_revision_conflict');
|
|
106
135
|
const next = structuredClone(previous);
|
|
@@ -122,7 +151,9 @@ export async function importBuiltinProjectSkills(directory, options, cache = pro
|
|
|
122
151
|
}
|
|
123
152
|
const imported = [];
|
|
124
153
|
if (missing.length) {
|
|
125
|
-
const resolved = selected ?? await selectPlatformSkillSnapshot(missing, access);
|
|
154
|
+
const resolved = selected ?? await selectPlatformSkillSnapshot(missing, access, previous.lock.lockVersion, previous.lock.lockVersion === 4);
|
|
155
|
+
if (resolved.lock.lockVersion !== previous.lock.lockVersion)
|
|
156
|
+
throw new Error('workspace_skills_lock_version_mismatch');
|
|
126
157
|
await cacheArtifacts(cache, resolved);
|
|
127
158
|
for (const name of missing) {
|
|
128
159
|
const entry = resolved.declaration.entries.find(item => item.entryKey === name);
|
|
@@ -130,12 +161,12 @@ export async function importBuiltinProjectSkills(directory, options, cache = pro
|
|
|
130
161
|
if (!entry || !locked)
|
|
131
162
|
throw new Error('workspace_skills_invalid');
|
|
132
163
|
next.declaration.entries.push({ ...entry, enabled });
|
|
133
|
-
next.lock.entries.
|
|
164
|
+
next.lock = { ...next.lock, entries: [...next.lock.entries, locked] };
|
|
134
165
|
imported.push(name);
|
|
135
166
|
}
|
|
136
167
|
next.lock.declarationHash = declarationDigest(next.declaration);
|
|
137
168
|
validateProjectSkillsSnapshot(next.declaration, next.lock);
|
|
138
|
-
await commitSnapshot(directory, next, originalDeclaration, staged);
|
|
169
|
+
await commitSnapshot(directory, next, originalDeclaration, originalLock, staged);
|
|
139
170
|
}
|
|
140
171
|
return { ...next, imported, unchanged, lockHash: hash(next.lock) };
|
|
141
172
|
}
|
|
@@ -159,7 +190,8 @@ export async function migrateVideoProjectSkills(directory, options, cache, acces
|
|
|
159
190
|
if (!declarationStat.isFile() || declarationStat.isSymbolicLink() || declarationStat.size > 1024 * 1024)
|
|
160
191
|
throw new Error('workspace_skills_invalid');
|
|
161
192
|
const originalDeclaration = await readFile(declarationPath);
|
|
162
|
-
const
|
|
193
|
+
const originalLock = await readProjectSkillsLockFile(directory);
|
|
194
|
+
const previous = validateProjectSkillsSnapshot(originalDeclaration.toString('utf8'), originalLock);
|
|
163
195
|
const oldLockHash = hash(previous.lock);
|
|
164
196
|
if (options.expectedDeclarationHash !== previous.lock.declarationHash || options.expectedLockHash !== oldLockHash)
|
|
165
197
|
throw new Error('skills_revision_conflict');
|
|
@@ -180,7 +212,9 @@ export async function migrateVideoProjectSkills(directory, options, cache, acces
|
|
|
180
212
|
throw new Error('workspace_skills_builtin_conflict');
|
|
181
213
|
replacements.push({ entry, name });
|
|
182
214
|
}
|
|
183
|
-
const selected = await selectPlatformSkillSnapshot(replacements.map(item => item.name), access);
|
|
215
|
+
const selected = await selectPlatformSkillSnapshot(replacements.map(item => item.name), access, previous.lock.lockVersion, previous.lock.lockVersion === 4);
|
|
216
|
+
if (selected.lock.lockVersion !== previous.lock.lockVersion)
|
|
217
|
+
throw new Error('workspace_skills_lock_version_mismatch');
|
|
184
218
|
await cacheArtifacts(cache, selected);
|
|
185
219
|
const next = structuredClone(previous);
|
|
186
220
|
for (const { entry, name } of replacements) {
|
|
@@ -206,18 +240,31 @@ export async function migrateVideoProjectSkills(directory, options, cache, acces
|
|
|
206
240
|
for (const { entry } of replacements) {
|
|
207
241
|
if (entry.source.kind !== 'repository')
|
|
208
242
|
throw new Error('workspace_skills_invalid');
|
|
243
|
+
const expectedSource = previous.lock.entries.find(item => item.entryKey === entry.entryKey)?.sourceTreeHash;
|
|
244
|
+
if ((await readProjectSkillSource(directory, entry)).sourceTreeHash !== expectedSource)
|
|
245
|
+
throw new Error('workspace_skills_builtin_conflict');
|
|
209
246
|
const from = join(directory, entry.source.root), to = join(history, entry.entryKey);
|
|
210
247
|
await rename(from, to);
|
|
211
248
|
moved.push({ from, to });
|
|
249
|
+
const movedEntry = { ...entry, source: { ...entry.source, root: `.playcraft/skill-migrations/${oldLockHash}/${entry.entryKey}` } };
|
|
250
|
+
if ((await readProjectSkillSource(directory, movedEntry)).sourceTreeHash !== expectedSource)
|
|
251
|
+
throw new Error('workspace_skills_builtin_conflict');
|
|
212
252
|
}
|
|
213
|
-
await commitSnapshot(directory, next, originalDeclaration, staged);
|
|
253
|
+
await commitSnapshot(directory, next, originalDeclaration, originalLock, staged);
|
|
214
254
|
committed = true;
|
|
215
255
|
return { ...next, migrated: options.entryKeys, history: `.playcraft/skill-migrations/${oldLockHash}/migration.json`, lockHash: hash(next.lock) };
|
|
216
256
|
}
|
|
217
257
|
finally {
|
|
218
258
|
if (!committed)
|
|
219
|
-
for (const item of moved.reverse())
|
|
220
|
-
await
|
|
259
|
+
for (const item of moved.reverse()) {
|
|
260
|
+
const occupied = await lstat(item.from).then(() => true, (error) => {
|
|
261
|
+
if (error.code === 'ENOENT')
|
|
262
|
+
return false;
|
|
263
|
+
throw error;
|
|
264
|
+
});
|
|
265
|
+
if (!occupied)
|
|
266
|
+
await rename(item.to, item.from).catch(() => undefined);
|
|
267
|
+
}
|
|
221
268
|
for (const path of staged)
|
|
222
269
|
await rm(path, { force: true });
|
|
223
270
|
await rm(lease, { force: true });
|
|
@@ -8,6 +8,7 @@ import { runLocalProjectSkills, localProjectSkillUsage } from './local-run.js';
|
|
|
8
8
|
import { projectSkillsMessage as t } from './messages.js';
|
|
9
9
|
import { reconcileLocalProjectSkills } from './reconcile.js';
|
|
10
10
|
import { importBuiltinProjectSkills } from './builtin-import.js';
|
|
11
|
+
import { migrateProjectSkillsLockV4 } from './migrate-lock.js';
|
|
11
12
|
export async function inspectLocalProjectSkills(directory) {
|
|
12
13
|
const inventory = await inspectProjectSkills(directory);
|
|
13
14
|
// Report metadata only; never print complete skill bytes or project test data.
|
|
@@ -22,7 +23,7 @@ export function registerProjectSkillsCommands(skills) {
|
|
|
22
23
|
process.stdout.write(`${JSON.stringify(await action(), null, 2)}\n`);
|
|
23
24
|
}
|
|
24
25
|
catch (error) {
|
|
25
|
-
process.stderr.write(`${t('failed')}: ${error instanceof Error && error.message === 'workspace_skills_platform_required' ? t('platformRequired') : error instanceof Error ? error.message : String(error)}\n`);
|
|
26
|
+
process.stderr.write(`${t('failed')}: ${error instanceof Error && error.message === 'workspace_skills_platform_required' ? t('platformRequired') : error instanceof Error && 'code' in error && error.code === 'lock_migration_required' ? t('migrateLockRequired') : error instanceof Error ? error.message : String(error)}\n`);
|
|
26
27
|
process.exitCode = 1;
|
|
27
28
|
}
|
|
28
29
|
};
|
|
@@ -36,7 +37,15 @@ export function registerProjectSkillsCommands(skills) {
|
|
|
36
37
|
project.command('usage').description(t('usage')).option('--dir <path>', t('directory'), '.')
|
|
37
38
|
.action((options) => output(() => localProjectSkillUsage(resolve(options.dir))));
|
|
38
39
|
project.command('sync').description(t('sync')).option('--dir <path>', t('directory'), '.')
|
|
39
|
-
.
|
|
40
|
+
.option('--lock-version <3|4>', t('lockVersion'))
|
|
41
|
+
.action((options) => output(() => {
|
|
42
|
+
if (options.lockVersion !== undefined && options.lockVersion !== '3' && options.lockVersion !== '4')
|
|
43
|
+
throw new Error(t('invalidLockVersion'));
|
|
44
|
+
const lockVersion = options.lockVersion === undefined ? undefined : options.lockVersion === '4' ? 4 : 3;
|
|
45
|
+
return syncLocalProjectSkills(resolve(options.dir), undefined, { lockVersion });
|
|
46
|
+
}));
|
|
47
|
+
project.command('migrate-lock-v4').description(t('migrateLockV4')).option('--dir <path>', t('directory'), '.')
|
|
48
|
+
.action((options) => output(() => migrateProjectSkillsLockV4(resolve(options.dir))));
|
|
40
49
|
project.command('import-builtins <names...>').description(t('importBuiltins')).option('--dir <path>', t('directory'), '.')
|
|
41
50
|
.requiredOption('--expected-declaration-hash <hash>', t('expectedDeclarationHash'))
|
|
42
51
|
.requiredOption('--expected-lock-hash <hash>', t('expectedLockHash'))
|
|
@@ -6,7 +6,7 @@ import { projectSkillsMessage as t } from './messages.js';
|
|
|
6
6
|
/** Existing formats keep their legacy consumer; format 3 never silently downgrades. */
|
|
7
7
|
export async function prepareLocalSkillLifecycle(directory, action, cache) {
|
|
8
8
|
const raw = await readProjectSkillsLockFile(directory);
|
|
9
|
-
const managed = raw !== null && JSON.parse(raw).lockVersion
|
|
9
|
+
const managed = raw !== null && [3, 4].includes(JSON.parse(raw).lockVersion);
|
|
10
10
|
if (!managed && process.env.PROJECT_SKILLS_ADOPTION_ENABLED !== 'true')
|
|
11
11
|
return { managed: false };
|
|
12
12
|
if (raw !== null && !managed)
|