@playcraft/cli 0.1.45-beta → 0.1.47-beta

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 (29) hide show
  1. package/dist/commands/tools-batch.js +1 -3
  2. package/dist/workspace-runtime/{production → batch}/content-changes.js +6 -12
  3. package/dist/workspace-runtime/batch/deliver.js +5 -0
  4. package/dist/workspace-runtime/batch/freeze.js +61 -62
  5. package/dist/workspace-runtime/batch/generate.js +24 -3
  6. package/dist/workspace-runtime/batch/occupancy.js +1 -1
  7. package/dist/workspace-runtime/batch/prepare.js +2 -0
  8. package/dist/workspace-runtime/batch/routes.js +3 -0
  9. package/dist/workspace-runtime/batch/source-candidate.js +86 -0
  10. package/dist/workspace-runtime/batch/source-failure.js +43 -0
  11. package/dist/workspace-runtime/batch/source-publish.js +132 -0
  12. package/dist/workspace-runtime/server/dispatch.js +17 -8
  13. package/dist/workspace-runtime/server/http.js +10 -24
  14. package/dist/workspace-runtime/server/terminal-host.js +2 -2
  15. package/dist/workspace-runtime/verify-artifact.js +59 -0
  16. package/dist/workspace-runtime/workspaces/cli-credentials.js +6 -2
  17. package/dist/workspace-runtime/workspaces/directories.js +10 -20
  18. package/dist/workspace-runtime/workspaces/directory-maintenance.js +3 -3
  19. package/package.json +3 -3
  20. package/dist/workspace-runtime/production/contracts.js +0 -19
  21. package/dist/workspace-runtime/production/controller.js +0 -228
  22. package/dist/workspace-runtime/production/dependencies.js +0 -60
  23. package/dist/workspace-runtime/production/native.js +0 -239
  24. package/dist/workspace-runtime/production/processes.js +0 -102
  25. package/dist/workspace-runtime/production/save-command.js +0 -35
  26. package/dist/workspace-runtime/production/source-archive.js +0 -137
  27. package/dist/workspace-runtime/production/source-copy.js +0 -92
  28. package/dist/workspace-runtime/production/stages.js +0 -354
  29. package/dist/workspace-runtime/production/storage.js +0 -75
@@ -5,7 +5,7 @@ import { shareWorkspaceGit } from '../workspaces/shared-git.js';
5
5
  import { join } from 'node:path';
6
6
  import { z } from 'zod';
7
7
  import { fileWritePayloadSchema, fileBinaryWritePayloadSchema, directoryCreatePayloadSchema, gitCommitPayloadSchema, gitPathsPayloadSchema, gitPushPayloadSchema, gitPullPayloadSchema, gitMergeAbortPayloadSchema, parameterPatchPayloadSchema, productionSessionAttemptId, reasoningEffortSchema, workspacePermissionModeSchema, workspaceComposerSelectionSchema, workspaceReviewPayloadSchema, workspaceCompactPayloadSchema, workspaceSteerPayloadSchema, workspaceApprovalResponseSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, recoverUncertainOperation, workspaceSavePayloadSchema, workspaceSkillsUpdatePayloadSchema, workspaceRemixReferenceImportPayloadSchema, } from '@playcraft/common/cloud-workspace/runtime';
8
- import { closeCliCredentials } from '../workspaces/cli-credentials.js';
8
+ import { closeCliCredentials, ensureCliCredentials } from '../workspaces/cli-credentials.js';
9
9
  import { artifactFormat } from '../workspaces/artifact-files.js';
10
10
  import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
11
11
  import { resolveWorkspaceAttachments } from '../workspaces/attachments.js';
@@ -27,7 +27,7 @@ import { patchWorkspaceParameters, readWorkspaceParameters } from '../workspaces
27
27
  import { previewFailureCode, previewWorkspaceRoot, runPreviewPublish, trackPreviewPublish, } from '../previews/publish.js';
28
28
  import { spawnBackgroundWriter, stopBackgroundWriter, } from '../processes/managed-writes.js';
29
29
  import { wakeSourceEventUploader } from './source-uploader.js';
30
- import { productionDirectoryBusy } from '../production/controller.js';
30
+ import { batchDirectoryHeld } from '../batch/occupancy.js';
31
31
  import { BinaryWorkspaceWriteError, observeWorkspaceBinaryFile, writeWorkspaceBinaryFile } from '../workspaces/binary-write.js';
32
32
  import { importWorkspaceLibraryAssets } from '../workspaces/asset-library-import.js';
33
33
  import { createWorkspaceDirectory, WorkspaceDirectoryCreateError } from '../workspaces/directory-create.js';
@@ -174,7 +174,7 @@ async function executeRemixReferenceImport(state, scopeKey, operation, payload)
174
174
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_remix_reference' }));
175
175
  if (workspaceSessions(state, operation.workspaceId, operation.directoryName).some(sessionTurnBusy))
176
176
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_already_active' }));
177
- if (await productionDirectoryBusy(state, operation.workspaceId, operation.directoryName))
177
+ if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
178
178
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'batch_workspace_readonly' }));
179
179
  try {
180
180
  const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName);
@@ -374,6 +374,15 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
374
374
  clientRequestId: operation.clientRequestId, sourceDirectoryName: binding.directoryName ?? 'basic',
375
375
  ...(typeof body.teamId === 'string' ? { teamId: body.teamId } : {}) })}`
376
376
  : '';
377
+ if (state.config.workspaceMode === 'multi') {
378
+ try {
379
+ // The control service has adopted this epoch before dispatching the turn.
380
+ await ensureCliCredentials(state);
381
+ }
382
+ catch {
383
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'cli_credentials_unavailable' }));
384
+ }
385
+ }
377
386
  // Taken before the native turn exists, so nothing the agent writes can precede it.
378
387
  await beginTurnChanges(state, sessionId, {
379
388
  root, workspaceId: operation.workspaceId, directoryName: binding.directoryName ?? 'basic',
@@ -589,7 +598,7 @@ async function executeCompact(state, scopeKey, operation, payload) {
589
598
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'session_workspace_mismatch' }));
590
599
  if (sessionTurnBusy(binding))
591
600
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_already_active' }));
592
- if (await productionDirectoryBusy(state, operation.workspaceId, operation.directoryName))
601
+ if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
593
602
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'batch_workspace_readonly' }));
594
603
  const adapter = native(state);
595
604
  if (!adapter.startCompact)
@@ -644,7 +653,7 @@ async function executeSteer(state, scopeKey, operation, payload) {
644
653
  if (state.journal.listOperations().some(({ operation: other }) => other.kind === 'session.steer'
645
654
  && other.sessionId === sessionId && other.expectedTurnId === parsed.data.expectedTurnId && other.status === 'uncertain'))
646
655
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'steer_receipt_uncertain' }));
647
- if (await productionDirectoryBusy(state, operation.workspaceId, operation.directoryName))
656
+ if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
648
657
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'batch_workspace_readonly' }));
649
658
  const adapter = native(state);
650
659
  if (!adapter.steer)
@@ -753,7 +762,7 @@ async function executeDirectoryCreate(state, scopeKey, operation, payload) {
753
762
  if (!parsed.success)
754
763
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_workspace_path' }));
755
764
  try {
756
- if (await productionDirectoryBusy(state, operation.workspaceId, operation.directoryName))
765
+ if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
757
766
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'batch_workspace_readonly' }));
758
767
  const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName);
759
768
  const created = await createWorkspaceDirectory(root, parsed.data);
@@ -781,7 +790,7 @@ async function executeAssetLibraryImport(state, scopeKey, operation, payload) {
781
790
  const assertCurrent = async () => {
782
791
  if ((operation.leaseFence ?? 0) < state.acceptedLeaseFence || state.journal.destroyed())
783
792
  throw new Error('stale_lease');
784
- if (await productionDirectoryBusy(state, operation.workspaceId, operation.directoryName))
793
+ if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
785
794
  throw new Error('batch_workspace_readonly');
786
795
  };
787
796
  await assertCurrent();
@@ -824,7 +833,7 @@ async function executeFileBinaryWrite(state, scopeKey, operation, payload) {
824
833
  return state.journal.exclusive(`file:${scope ?? 'single'}:${input.path}`, async () => {
825
834
  try {
826
835
  // Recheck after waiting for the workspace/file locks.
827
- if (await productionDirectoryBusy(state, operation.workspaceId, operation.directoryName))
836
+ if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
828
837
  throw Object.assign(new Error('batch_workspace_readonly'), { code: 'batch_workspace_readonly' });
829
838
  const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName);
830
839
  const observed = await observeWorkspaceBinaryFile(root, input.path);
@@ -24,7 +24,7 @@ import { listWorkspaceRemixReferences, RemixReferenceError } from '../workspaces
24
24
  import { loadWorkspaceSkillArtifact } from '../workspaces/skill-artifact-loader.js';
25
25
  import { join } from 'node:path';
26
26
  import { terminalHost } from './terminal-host.js';
27
- import { productionCapability, productionDirectoryBusy, executeSandboxProduction, sandboxProductionReceipt } from '../production/controller.js';
27
+ import { batchDirectoryHeld } from '../batch/occupancy.js';
28
28
  import { dispatchBatchRoute } from '../batch/routes.js';
29
29
  import { cleanupWorkspaceDirectories, syncWorkspaceDirectories } from '../workspaces/directory-maintenance.js';
30
30
  import { registerWorkspaceDirectory, restoreWorkspaceDirectory } from '../workspaces/directories.js';
@@ -56,21 +56,6 @@ export async function handleRuntimeRequest(state, request, response) {
56
56
  writeJson(response, batch.status, batch.body);
57
57
  return;
58
58
  }
59
- if (request.method === 'GET' && url.pathname === '/runtime/production/capability') {
60
- writeJson(response, 200, await productionCapability(state));
61
- return;
62
- }
63
- if (request.method === 'POST' && ['/runtime/production/stages', '/runtime/production/stop', '/runtime/production/reconcile'].includes(url.pathname)) {
64
- writeJson(response, 200, await executeSandboxProduction(state, await readJson(request)));
65
- return;
66
- }
67
- if (request.method === 'GET' && url.pathname.startsWith('/runtime/production/stages/')) {
68
- writeJson(response, 200, await sandboxProductionReceipt(state, decodeURIComponent(url.pathname.slice('/runtime/production/stages/'.length)), {
69
- workspaceId: url.searchParams.get('workspaceId') ?? '', directoryName, taskId: url.searchParams.get('taskId') ?? '',
70
- attemptId: url.searchParams.get('attemptId') ?? '', owner: url.searchParams.get('owner') ?? '', fence: Number(url.searchParams.get('fence')),
71
- }));
72
- return;
73
- }
74
59
  if (request.method === 'GET' && url.pathname === '/runtime/health') {
75
60
  writeJson(response, 200, { status: 'ok', handshake: handshake(state) });
76
61
  return;
@@ -108,6 +93,7 @@ export async function handleRuntimeRequest(state, request, response) {
108
93
  permissionsVersion: 1,
109
94
  composerContextVersion: 1,
110
95
  remixReferencesVersion: 1,
96
+ batchSourceFreezeRevision: 2,
111
97
  ...(state.native?.workspaceUi ? { workspaceUiVersion: 1 } : {}),
112
98
  ...(state.native?.startReview && state.native?.steer ? { turnControlVersion: 1 } : {}),
113
99
  ...(state.native?.startCompact ? { compactionVersion: 1 } : {}),
@@ -143,7 +129,7 @@ export async function handleRuntimeRequest(state, request, response) {
143
129
  }
144
130
  try {
145
131
  const restored = await state.journal.exclusive(`directory:${scope.data.workspaceId}:${directoryName}`, async () => {
146
- if (await productionDirectoryBusy(state, scope.data.workspaceId, directoryName))
132
+ if (await batchDirectoryHeld(state, scope.data.workspaceId, directoryName))
147
133
  throw new SessionRestoreError('batch_workspace_readonly');
148
134
  return state.journal.exclusive(`session:${scope.data.sessionId}`, () => state.journal.exclusive(`workspace:${revisionScope(state, scope.data.workspaceId, directoryName) ?? 'single'}`, () => restoreNativeSession(state, scope.data, parsed.data)));
149
135
  });
@@ -277,7 +263,7 @@ export async function handleRuntimeRequest(state, request, response) {
277
263
  importReceipts: await listWorkspaceLibraryImportReceipts(root, sessionId) });
278
264
  }
279
265
  else {
280
- if (await productionDirectoryBusy(state, workspaceId, directoryName)) {
266
+ if (await batchDirectoryHeld(state, workspaceId, directoryName)) {
281
267
  writeJson(response, 409, { error: 'batch_workspace_readonly' });
282
268
  return;
283
269
  }
@@ -303,7 +289,7 @@ export async function handleRuntimeRequest(state, request, response) {
303
289
  if (writeDestroyed(state, response))
304
290
  return;
305
291
  const workspaceId = url.searchParams.get('workspaceId') ?? '';
306
- if (await productionDirectoryBusy(state, workspaceId, directoryName)) {
292
+ if (await batchDirectoryHeld(state, workspaceId, directoryName)) {
307
293
  writeJson(response, 409, { error: 'batch_workspace_readonly' });
308
294
  return;
309
295
  }
@@ -337,7 +323,7 @@ export async function handleRuntimeRequest(state, request, response) {
337
323
  if (writeDestroyed(state, response))
338
324
  return;
339
325
  const workspaceId = url.searchParams.get('workspaceId') ?? '';
340
- if (await productionDirectoryBusy(state, workspaceId, directoryName)) {
326
+ if (await batchDirectoryHeld(state, workspaceId, directoryName)) {
341
327
  writeJson(response, 409, { error: 'batch_workspace_readonly' });
342
328
  return;
343
329
  }
@@ -354,7 +340,7 @@ export async function handleRuntimeRequest(state, request, response) {
354
340
  if (writeDestroyed(state, response))
355
341
  return;
356
342
  const workspaceId = url.searchParams.get('workspaceId') ?? '';
357
- if (await productionDirectoryBusy(state, workspaceId, directoryName)) {
343
+ if (await batchDirectoryHeld(state, workspaceId, directoryName)) {
358
344
  writeJson(response, 409, { error: 'batch_workspace_readonly' });
359
345
  return;
360
346
  }
@@ -767,7 +753,7 @@ async function acceptOperation(state, request, response) {
767
753
  await sessionWorkspaceRoot(state, parsed.data.workspaceId, parsed.data.directoryName);
768
754
  const mutation = isWorkspaceMutation(parsed.data.kind);
769
755
  const occupiesDirectory = mutation || parsed.data.kind === 'session.compact';
770
- if (occupiesDirectory && await productionDirectoryBusy(state, parsed.data.workspaceId, parsed.data.directoryName)) {
756
+ if (occupiesDirectory && await batchDirectoryHeld(state, parsed.data.workspaceId, parsed.data.directoryName)) {
771
757
  writeJson(response, 409, { error: 'batch_workspace_readonly' });
772
758
  return;
773
759
  }
@@ -909,7 +895,7 @@ async function acceptOperation(state, request, response) {
909
895
  writeJson(response, executed.status === 'failed' ? 409 : 202, executed);
910
896
  });
911
897
  const acceptBound = async () => {
912
- if (occupiesDirectory && await productionDirectoryBusy(state, parsed.data.workspaceId, parsed.data.directoryName)) {
898
+ if (occupiesDirectory && await batchDirectoryHeld(state, parsed.data.workspaceId, parsed.data.directoryName)) {
913
899
  writeJson(response, 409, { error: 'batch_workspace_readonly' });
914
900
  return;
915
901
  }
@@ -929,7 +915,7 @@ async function acceptOperation(state, request, response) {
929
915
  else
930
916
  await accept();
931
917
  };
932
- // Production admission and maintenance use this same lock before checking durable occupancy.
918
+ // Directory maintenance uses this same lock before checking durable occupancy.
933
919
  if (occupiesDirectory && state.config.workspaceMode === 'multi')
934
920
  await state.journal.exclusive(`directory:${parsed.data.workspaceId}:${parsed.data.directoryName ?? 'basic'}`, acceptBound);
935
921
  else
@@ -1,4 +1,4 @@
1
- import { productionDirectoryBusy } from '../production/controller.js';
1
+ import { batchDirectoryHeld } from '../batch/occupancy.js';
2
2
  import { createRequire } from 'node:module';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { lstat } from 'node:fs/promises';
@@ -67,7 +67,7 @@ export class TerminalHost {
67
67
  rejectUpgrade(socket, 404, { error: 'terminal_not_found' });
68
68
  return;
69
69
  }
70
- if (await productionDirectoryBusy(this.state, terminal.workspaceId, terminal.directoryName)) {
70
+ if (await batchDirectoryHeld(this.state, terminal.workspaceId, terminal.directoryName)) {
71
71
  rejectUpgrade(socket, 409, { error: 'batch_workspace_readonly' });
72
72
  return;
73
73
  }
@@ -0,0 +1,59 @@
1
+ import assert from 'node:assert/strict';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { readFile, mkdtemp, rm } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { batchSourceRemoteRef, parseSandboxRuntimeConfig } from '@playcraft/common/cloud-workspace/runtime';
9
+ import { createLoopbackNativeAdapter } from './codex/loopback.js';
10
+ import { startWorkspaceRuntime } from './serve.js';
11
+ async function dependencyVersion(name, entry) {
12
+ let directory = dirname(createRequire(import.meta.url).resolve(entry));
13
+ for (;;) {
14
+ const metadata = await readFile(join(directory, 'package.json'), 'utf8').then(JSON.parse).catch(() => undefined);
15
+ if (metadata?.name === name)
16
+ return metadata.version;
17
+ const parent = dirname(directory);
18
+ if (parent === directory)
19
+ throw new Error(`package_metadata_missing:${name}`);
20
+ directory = parent;
21
+ }
22
+ }
23
+ /** Starts only an isolated temporary runtime; never reads or changes an existing workspace. */
24
+ export async function verifyRuntimeArtifact(expectedVersion) {
25
+ assert.match(expectedVersion, /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/);
26
+ const metadata = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
27
+ assert.equal(metadata.version, expectedVersion, 'command package version differs from the release target');
28
+ assert.equal(await dependencyVersion('@playcraft/common', '@playcraft/common/cloud-workspace/runtime'), expectedVersion);
29
+ assert.equal(await dependencyVersion('@playcraft/build', '@playcraft/build'), expectedVersion);
30
+ const batchId = randomUUID();
31
+ assert.equal(batchSourceRemoteRef(batchId), `refs/tags/playcraft/batch/${batchId}/source`, 'installed shared library lacks standard tag references for source snapshots');
32
+ const directory = await mkdtemp(join(tmpdir(), 'playcraft-artifact-verify-'));
33
+ let runtime;
34
+ try {
35
+ const config = parseSandboxRuntimeConfig({
36
+ protocolMajor: 1, protocolMinor: 0, environment: 'artifact-verification',
37
+ runtimeId: randomUUID(), userId: 'artifact-verification', generation: 1, runtimeEpoch: 0,
38
+ workspaceMode: 'multi', workspaceRoot: join(directory, 'workspaces'), stateDir: join(directory, 'state'),
39
+ publicBaseUrl: 'http://127.0.0.1:9', controlToken: randomUUID(), cliVersion: expectedVersion,
40
+ listen: { host: '127.0.0.1', port: 0 },
41
+ });
42
+ runtime = await startWorkspaceRuntime(config, { nativeAdapter: createLoopbackNativeAdapter() });
43
+ const response = await fetch(`${runtime.url}/runtime/identity`, {
44
+ headers: { authorization: `Bearer ${config.controlToken}` }, signal: AbortSignal.timeout(10_000),
45
+ });
46
+ assert.equal(response.status, 200, 'built runtime identity is unavailable');
47
+ const identity = await response.json();
48
+ assert.equal(identity.runtimeId, config.runtimeId);
49
+ assert.equal(identity.batchSourceFreezeRevision, 2, 'built runtime lacks the required batch source freeze implementation');
50
+ }
51
+ finally {
52
+ await runtime?.close();
53
+ await rm(directory, { recursive: true, force: true });
54
+ }
55
+ }
56
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
57
+ await verifyRuntimeArtifact(process.argv[2] ?? '');
58
+ console.log('Runtime artifact verified: batchSourceFreezeRevision=2');
59
+ }
@@ -24,7 +24,7 @@ export async function ensureCliCredentials(state) {
24
24
  entry = { closed: false, expiresAt: 0, epoch: -1 };
25
25
  entries.set(state, entry);
26
26
  }
27
- if (entry.closed)
27
+ if (entry.closed || state.journal.destroyed())
28
28
  throw new Error('cli_credentials_unavailable');
29
29
  if (entry.epoch === state.runtimeEpoch && entry.expiresAt > Date.now() + 60_000)
30
30
  return;
@@ -33,6 +33,9 @@ export async function ensureCliCredentials(state) {
33
33
  entry.pending = undefined;
34
34
  });
35
35
  await entry.pending;
36
+ if (entry.closed || state.journal.destroyed() || entry.epoch !== state.runtimeEpoch
37
+ || entry.expiresAt <= Date.now() + 60_000)
38
+ throw new Error('cli_credentials_unavailable');
36
39
  }
37
40
  async function renew(state, entry) {
38
41
  const config = state.config;
@@ -92,7 +95,8 @@ async function renew(state, entry) {
92
95
  if (!entry.closed && !state.journal.destroyed()) {
93
96
  if (entry.timer)
94
97
  clearTimeout(entry.timer);
95
- const wait = entry.expiresAt > Date.now() + 60_000 ? entry.expiresAt - Date.now() - 59_000 : 30_000;
98
+ const wait = entry.epoch === state.runtimeEpoch && entry.expiresAt > Date.now() + 60_000
99
+ ? entry.expiresAt - Date.now() - 59_000 : 30_000;
96
100
  entry.timer = setTimeout(() => void ensureCliCredentials(state).catch(() => undefined), wait);
97
101
  entry.timer.unref();
98
102
  }
@@ -12,16 +12,13 @@ import { revisionScope } from './registry.js';
12
12
  import { ensureWorkspaceSkills } from './skills.js';
13
13
  import { checkoutBatchCommit, freezeBatchCommit } from './batch-commit.js';
14
14
  import { ensureWorkspaceArtifactInstructions } from './instructions.js';
15
- import { checkoutProductionSourceArchive } from '../production/source-archive.js';
16
15
  import { shareWorkspaceRepository } from './git.js';
17
16
  const execute = promisify(execFile);
18
17
  /** Trusted Runtime registration restores immutable input; it never follows basic's current branch head. */
19
- export async function registerWorkspaceDirectory(state, input, leaseFence, sourceDirectoryName, sourceArchive) {
18
+ export async function registerWorkspaceDirectory(state, input, leaseFence, sourceDirectoryName) {
20
19
  const directory = workspaceDirectoryRegistrationSchema.parse(input);
21
20
  if (directory.directoryName === 'basic')
22
21
  throw new WorkspaceContextError('workspace_directory_binding_conflict');
23
- if (sourceArchive && (sourceArchive.snapshot.sourceWorkspaceId !== directory.workspaceId || sourceArchive.snapshot.commitHash !== directory.sourceCommit))
24
- throw new WorkspaceContextError('workspace_directory_binding_conflict');
25
22
  return state.journal.exclusive(`directory:${directory.workspaceId}:${directory.directoryName}`, async () => {
26
23
  if (state.journal.destroyed())
27
24
  throw new WorkspaceContextError('runtime_destroyed');
@@ -51,22 +48,15 @@ export async function registerWorkspaceDirectory(state, input, leaseFence, sourc
51
48
  throw new WorkspaceContextError('workspace_directory_source_commit_required');
52
49
  const staging = join(parent, `.directory-${randomUUID()}`);
53
50
  try {
54
- if (sourceArchive) {
55
- if (!sourceArchive.body)
56
- throw new WorkspaceContextError('production_source_archive_required');
57
- await checkoutProductionSourceArchive({ snapshot: sourceArchive.snapshot, body: sourceArchive.body, destination: staging });
58
- }
59
- else {
60
- const source = await workspaceRoot(state, directory.workspaceId, sourceDirectoryName ?? 'basic');
61
- await git(source, 'cat-file', '-e', `${directory.sourceCommit}^{commit}`);
62
- if (sourceDirectoryName !== undefined) {
63
- const frozen = await freezeBatchCommit({ source: { workspaceId: directory.workspaceId, root: source },
64
- commitHash: directory.sourceCommit, snapshotId: `directory-${directory.remixId}` });
65
- await checkoutBatchCommit({ source: { workspaceId: directory.workspaceId, root: source }, frozen, destination: staging });
66
- }
67
- else
68
- await execute('git', ['-c', 'core.hooksPath=/dev/null', 'clone', '--local', '--no-hardlinks', '--no-checkout', '--', source, staging], { timeout: 90_000, maxBuffer: 1024 * 1024 });
51
+ const source = await workspaceRoot(state, directory.workspaceId, sourceDirectoryName ?? 'basic');
52
+ await git(source, 'cat-file', '-e', `${directory.sourceCommit}^{commit}`);
53
+ if (sourceDirectoryName !== undefined) {
54
+ const frozen = await freezeBatchCommit({ source: { workspaceId: directory.workspaceId, root: source },
55
+ commitHash: directory.sourceCommit, snapshotId: `directory-${directory.remixId}` });
56
+ await checkoutBatchCommit({ source: { workspaceId: directory.workspaceId, root: source }, frozen, destination: staging });
69
57
  }
58
+ else
59
+ await execute('git', ['-c', 'core.hooksPath=/dev/null', 'clone', '--local', '--no-hardlinks', '--no-checkout', '--', source, staging], { timeout: 90_000, maxBuffer: 1024 * 1024 });
70
60
  await git(staging, 'checkout', '-b', directory.branchName, directory.sourceCommit);
71
61
  await git(staging, 'remote', 'set-url', 'origin', binding.repositoryUrl);
72
62
  await git(staging, 'config', 'user.name', 'PlayCraft');
@@ -90,7 +80,7 @@ export async function registerWorkspaceDirectory(state, input, leaseFence, sourc
90
80
  const latest = state.journal.getWorkspace(directory.workspaceId) ?? binding;
91
81
  await state.journal.putWorkspace({ ...latest, directories: [...(latest.directories ?? []).filter(item => item.directoryName !== directory.directoryName), { ...directory, status: 'ready' }] });
92
82
  });
93
- if (state.gitCredentials && !sourceArchive)
83
+ if (state.gitCredentials)
94
84
  await state.gitCredentials.configure(state.journal.getWorkspace(directory.workspaceId) ?? binding, await state.gitCredentials.prepareDirectory(directory.workspaceId, directory.directoryName), directory.directoryName);
95
85
  return directory;
96
86
  });
@@ -4,14 +4,14 @@ import { join } from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { promisify } from 'node:util';
6
6
  import { workspaceDirectoryNameSchema } from '@playcraft/common/cloud-workspace/runtime';
7
- import { productionDirectoryBusy } from '../production/controller.js';
7
+ import { batchDirectoryHeld } from '../batch/occupancy.js';
8
8
  import { workspaceRoot, workspaceSessions } from './registry.js';
9
9
  const execute = promisify(execFile);
10
10
  const git = async (state, root, ...args) => (await execute('git', ['-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', '-C', root, ...args], {
11
11
  timeout: 30_000, maxBuffer: 1024 * 1024, env: { PATH: process.env.PATH, HOME: state.config.stateDir, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0' },
12
12
  })).stdout.trim();
13
- export async function workspaceDirectoryOccupied(state, workspaceId, directoryName, excludeAttemptId, ignoreSourceCaptures = false) {
14
- if (await productionDirectoryBusy(state, workspaceId, directoryName, excludeAttemptId, ignoreSourceCaptures))
13
+ export async function workspaceDirectoryOccupied(state, workspaceId, directoryName) {
14
+ if (await batchDirectoryHeld(state, workspaceId, directoryName))
15
15
  return true;
16
16
  if (state.journal.listOperations().some(({ operation }) => operation.workspaceId === workspaceId
17
17
  && (operation.directoryName ?? 'basic') === directoryName && ['queued', 'dispatched', 'accepted', 'uncertain'].includes(operation.status)))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.1.45-beta",
3
+ "version": "0.1.47-beta",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -51,8 +51,8 @@
51
51
  "@gltf-transform/core": "^4.3.0",
52
52
  "@gltf-transform/extensions": "^4.3.0",
53
53
  "@gltf-transform/functions": "^4.3.0",
54
- "@playcraft/build": "^0.1.45-beta",
55
- "@playcraft/common": "^0.1.45-beta",
54
+ "@playcraft/build": "0.1.47-beta",
55
+ "@playcraft/common": "0.1.47-beta",
56
56
  "better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
57
57
  "chokidar": "^4.0.3",
58
58
  "commander": "^13.1.0",
@@ -1,19 +0,0 @@
1
- import { z } from 'zod';
2
- import { workspacePreparationSchema } from '@playcraft/common/cloud-workspace/runtime';
3
- export const productionStageNames = ['capture-source', 'prepare', 'generate', 'freeze', 'save', 'build', 'preview'];
4
- export const productionExecutionRequestSchema = z.object({
5
- action: z.enum(['stage', 'stop', 'reconcile']),
6
- task: z.object({ taskId: z.string().uuid(), attemptId: z.string().uuid(), batchId: z.string().uuid(), workspaceId: z.string().uuid(),
7
- environment: z.string().min(1), userId: z.string().min(1), payload: z.record(z.unknown()), }).passthrough(),
8
- lease: z.object({ taskId: z.string().uuid(), attemptId: z.string().uuid(), owner: z.string().min(1), fence: z.number().int().positive() }).strict(),
9
- directoryName: z.string().regex(/^(?:basic|remix-[a-z0-9][a-z0-9-]{3,63})$/).default('basic'), generation: z.number().int().positive(), runtimeEpoch: z.number().int().nonnegative(),
10
- callbackUrl: z.string().url(), runnerToken: z.string().min(1), payload: z.record(z.unknown()),
11
- stage: z.enum(productionStageNames).optional(),
12
- stages: z.array(z.object({ stage: z.enum(productionStageNames), requestId: z.string(), status: z.string(),
13
- result: z.record(z.unknown()).optional(), payload: z.record(z.unknown()).optional() }).passthrough()).optional(),
14
- source: z.object({ workspaceId: z.string().uuid(), generation: z.number().int().positive(), runtimeId: z.string().min(1) }).optional(),
15
- model: z.object({ endpoint: z.string().url(), token: z.string().min(1), model: z.string().min(1) }).optional(),
16
- git: z.object({ repositoryUrl: z.string().min(1), branchName: z.string().min(1), username: z.string().optional(), password: z.string().optional() }).optional(),
17
- repairSnapshot: z.unknown().optional(), repairSource: z.unknown().optional(), repairCommitHash: z.string().optional(),
18
- repairPreparation: z.custom(value => workspacePreparationSchema.safeParse(value).success).optional(),
19
- }).strict();