@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.
- package/dist/commands/tools-batch.js +1 -3
- package/dist/workspace-runtime/{production → batch}/content-changes.js +6 -12
- package/dist/workspace-runtime/batch/deliver.js +5 -0
- package/dist/workspace-runtime/batch/freeze.js +61 -62
- package/dist/workspace-runtime/batch/generate.js +24 -3
- package/dist/workspace-runtime/batch/occupancy.js +1 -1
- package/dist/workspace-runtime/batch/prepare.js +2 -0
- package/dist/workspace-runtime/batch/routes.js +3 -0
- package/dist/workspace-runtime/batch/source-candidate.js +86 -0
- package/dist/workspace-runtime/batch/source-failure.js +43 -0
- package/dist/workspace-runtime/batch/source-publish.js +132 -0
- package/dist/workspace-runtime/server/dispatch.js +17 -8
- package/dist/workspace-runtime/server/http.js +10 -24
- package/dist/workspace-runtime/server/terminal-host.js +2 -2
- package/dist/workspace-runtime/verify-artifact.js +59 -0
- package/dist/workspace-runtime/workspaces/cli-credentials.js +6 -2
- package/dist/workspace-runtime/workspaces/directories.js +10 -20
- package/dist/workspace-runtime/workspaces/directory-maintenance.js +3 -3
- package/package.json +3 -3
- package/dist/workspace-runtime/production/contracts.js +0 -19
- package/dist/workspace-runtime/production/controller.js +0 -228
- package/dist/workspace-runtime/production/dependencies.js +0 -60
- package/dist/workspace-runtime/production/native.js +0 -239
- package/dist/workspace-runtime/production/processes.js +0 -102
- package/dist/workspace-runtime/production/save-command.js +0 -35
- package/dist/workspace-runtime/production/source-archive.js +0 -137
- package/dist/workspace-runtime/production/source-copy.js +0 -92
- package/dist/workspace-runtime/production/stages.js +0 -354
- 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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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 {
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
await
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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 {
|
|
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
|
|
14
|
-
if (await
|
|
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.
|
|
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": "
|
|
55
|
-
"@playcraft/common": "
|
|
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();
|