@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.
Files changed (101) hide show
  1. package/dist/assets/client.js +4 -2
  2. package/dist/assets/messages.js +2 -0
  3. package/dist/audio-source/messages.js +35 -0
  4. package/dist/commands/assets.js +1 -1
  5. package/dist/commands/audio-source.js +109 -0
  6. package/dist/commands/audio.js +11 -4
  7. package/dist/commands/build.js +4 -2
  8. package/dist/commands/tools-context.js +46 -0
  9. package/dist/commands/tools.js +2 -0
  10. package/dist/project-skills/builtin-import.js +69 -22
  11. package/dist/project-skills/commands.js +11 -2
  12. package/dist/project-skills/lifecycle.js +1 -1
  13. package/dist/project-skills/local-store.js +90 -31
  14. package/dist/project-skills/messages.js +16 -0
  15. package/dist/project-skills/migrate-lock.js +166 -0
  16. package/dist/project-skills/remote-cache.js +36 -5
  17. package/dist/utils/agent-api-client.js +18 -1
  18. package/dist/utils/agent-generation-provenance.js +8 -2
  19. package/dist/utils/audio-export.js +249 -0
  20. package/dist/utils/audio-processor.js +22 -99
  21. package/dist/utils/execution-context-bridge.js +28 -0
  22. package/dist/workspace-runtime/batch/control.js +13 -2
  23. package/dist/workspace-runtime/batch/generate.js +43 -2
  24. package/dist/workspace-runtime/batch/prepare.js +4 -0
  25. package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
  26. package/dist/workspace-runtime/batch/source-capture.js +6 -8
  27. package/dist/workspace-runtime/codex/app-server.js +26 -2
  28. package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
  29. package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
  30. package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
  31. package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
  32. package/dist/workspace-runtime/codex/model-broker.js +15 -2
  33. package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
  34. package/dist/workspace-runtime/previews/finalize.js +1 -1
  35. package/dist/workspace-runtime/previews/output.js +4 -1
  36. package/dist/workspace-runtime/previews/publish.js +5 -2
  37. package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
  38. package/dist/workspace-runtime/serve.js +6 -0
  39. package/dist/workspace-runtime/server/accept-operation.js +85 -13
  40. package/dist/workspace-runtime/server/audio-assets.js +211 -0
  41. package/dist/workspace-runtime/server/audio-export.js +136 -0
  42. package/dist/workspace-runtime/server/directory-access.js +222 -67
  43. package/dist/workspace-runtime/server/dispatch.js +252 -29
  44. package/dist/workspace-runtime/server/http.js +97 -3
  45. package/dist/workspace-runtime/server/operation-record.js +1 -1
  46. package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
  47. package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
  48. package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
  49. package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
  50. package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +76 -0
  51. package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
  52. package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
  53. package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
  54. package/dist/workspace-runtime/workspaces/audio-assets/service.js +327 -0
  55. package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
  56. package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
  57. package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
  58. package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
  59. package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
  60. package/dist/workspace-runtime/workspaces/files.js +3 -5
  61. package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
  62. package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
  63. package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
  64. package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
  65. package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
  66. package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
  67. package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
  68. package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
  69. package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
  70. package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
  71. package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
  72. package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
  73. package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
  74. package/dist/workspace-runtime/workspaces/skills.js +48 -13
  75. package/package.json +6 -4
  76. package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  77. package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
  78. package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
  79. package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
  80. package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
  81. package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
  82. package/project-template/vite.config.ts +1 -1
  83. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  84. package/project-template-v2/.playcraft/skills.lock.json +9 -2
  85. package/project-template-v2/vite.config.ts +1 -1
  86. package/project-templates/empty/AGENTS.md +2 -0
  87. package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  88. package/project-templates/phaser/vite.config.ts +1 -1
  89. package/project-templates/playable-template-phaser/vite.config.ts +1 -1
  90. package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  91. package/project-templates/threejs/vite.config.ts +1 -1
  92. package/templates/preview-runtime/v1/README.md +12 -0
  93. package/templates/preview-runtime/v1/build-preview.mjs +17 -8
  94. package/templates/preview-runtime/v1/manifest.json +9 -6
  95. package/templates/preview-runtime/v1/release-guard.mjs +1 -1
  96. package/templates/preview-runtime/v1/runtime.d.mts +1 -3
  97. package/templates/preview-runtime/v1/runtime.d.ts +1 -3
  98. package/templates/preview-runtime/v1/runtime.mjs +1142 -257
  99. package/templates/preview-runtime/v1/types/host.d.ts +29 -0
  100. package/templates/preview-runtime/v1/types/index.d.ts +6 -0
  101. package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
@@ -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;
@@ -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
+ }
@@ -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
+ }
@@ -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('素材工具:本地音频处理(ffmpeg,不调用 API)');
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('--bitrate <kbps>', '目标比特率(kbps),如 64', parseInt)
71
- .option('--sample-rate <hz>', '目标采样率(Hz),如 22050', parseInt)
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>', '目标体积(自动计算比特率),如 30KB、1.5MB')
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
  });
@@ -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
+ }
@@ -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
- if (archive.length > 80 * 1024 * 1024 || createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
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
- return { artifactHash: entry.revision.artifactHash, archive };
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 path = join(cache, `${artifact.artifactHash}.tar.gz`);
79
- try {
80
- await writeFile(path, artifact.archive, { flag: 'wx', mode: 0o600 });
81
- }
82
- catch (error) {
83
- if (error.code !== 'EEXIST' || !(await readFile(path)).equals(artifact.archive))
84
- throw error;
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 previous = validateProjectSkillsSnapshot(originalDeclaration.toString('utf8'), await readProjectSkillsLockFile(directory));
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.push(locked);
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 previous = validateProjectSkillsSnapshot(originalDeclaration.toString('utf8'), await readProjectSkillsLockFile(directory));
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 rename(item.to, item.from).catch(() => undefined);
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
- .action((options) => output(() => syncLocalProjectSkills(resolve(options.dir))));
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 === 3;
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)