@playcraft/cli 0.1.48-beta → 0.1.50-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 (39) hide show
  1. package/dist/workspace-runtime/batch/deliver.js +105 -87
  2. package/dist/workspace-runtime/batch/prepare.js +169 -56
  3. package/dist/workspace-runtime/batch/source-capture.js +7 -20
  4. package/dist/workspace-runtime/batch/source-publish.js +2 -4
  5. package/dist/workspace-runtime/codex/app-server.js +19 -1
  6. package/dist/workspace-runtime/codex/delivery-tools.js +144 -0
  7. package/dist/workspace-runtime/codex/tool-response.js +10 -0
  8. package/dist/workspace-runtime/codex/ui-tools.js +22 -5
  9. package/dist/workspace-runtime/previews/diagnostics.js +44 -0
  10. package/dist/workspace-runtime/previews/publish.js +29 -8
  11. package/dist/workspace-runtime/previews/runtime-preview.js +177 -0
  12. package/dist/workspace-runtime/serve.js +162 -4
  13. package/dist/workspace-runtime/server/accept-operation.js +61 -0
  14. package/dist/workspace-runtime/server/asset-transfers.js +63 -0
  15. package/dist/workspace-runtime/server/dispatch.js +328 -44
  16. package/dist/workspace-runtime/server/http.js +193 -137
  17. package/dist/workspace-runtime/server/operation-record.js +33 -0
  18. package/dist/workspace-runtime/server/submit-operation.js +41 -0
  19. package/dist/workspace-runtime/workspaces/append-only-log.js +62 -0
  20. package/dist/workspace-runtime/workspaces/asset-transfer-dependencies.js +81 -0
  21. package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +434 -0
  22. package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +389 -0
  23. package/dist/workspace-runtime/workspaces/delivery-build.js +331 -0
  24. package/dist/workspace-runtime/workspaces/delivery-store.js +168 -0
  25. package/dist/workspace-runtime/workspaces/delivery.js +64 -0
  26. package/dist/workspace-runtime/workspaces/dependencies.js +15 -0
  27. package/dist/workspace-runtime/workspaces/developer-command.js +35 -1
  28. package/dist/workspace-runtime/workspaces/directories.js +9 -4
  29. package/dist/workspace-runtime/workspaces/directory-mutations.js +238 -0
  30. package/dist/workspace-runtime/workspaces/directory-revisions.js +56 -0
  31. package/dist/workspace-runtime/workspaces/disk-snapshot.js +3 -3
  32. package/dist/workspace-runtime/workspaces/json5-edit.js +144 -1
  33. package/dist/workspace-runtime/workspaces/operation-log.js +23 -0
  34. package/dist/workspace-runtime/workspaces/parameter-agent-inputs.js +87 -0
  35. package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +29 -0
  36. package/dist/workspace-runtime/workspaces/parameter-agent-store.js +58 -0
  37. package/dist/workspace-runtime/workspaces/parameter-agent.js +282 -0
  38. package/dist/workspace-runtime/workspaces/parameters.js +77 -54
  39. package/package.json +3 -3
@@ -1,26 +1,23 @@
1
- import { createHash, randomUUID } from 'node:crypto';
2
- import { lstat, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
1
+ import { constants } from 'node:fs';
2
+ import { lstat, open } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { hashOperationPayload, sandboxBatchMemberDeliverSchema } from '@playcraft/common/cloud-workspace/runtime';
5
- import { findExternalLocalReferences } from '../previews/single-file.js';
4
+ import { hashOperationPayload, sandboxBatchMemberDeliverSchema, } from '@playcraft/common/cloud-workspace/runtime';
6
5
  import { workspaceRoot } from '../workspaces/registry.js';
7
- import { requireBatchContentChanges } from './content-changes.js';
8
- import { gitRead, gitWrite } from './git.js';
9
6
  import { groupExited, runProcessGroup, stopProcessGroup } from './processes.js';
10
- import { filesystemObjectStore, memberStatus, readMember, readStopMarker, serializeRun, writeMember, } from './records.js';
7
+ import { memberStatus, readMember, readStopMarker, serializeRun, writeMember, } from './records.js';
8
+ /** Base build writes dist/index.html. Channel packaging stays on export playable (`--mode full`). */
11
9
  function buildCommand() {
12
- return process.env.PLAYCRAFT_BATCH_BUILD_COMMAND
13
- ?? 'playcraft build --mode full --platform playcraft --format html --output dist/index.html .';
10
+ return (process.env.PLAYCRAFT_BATCH_BUILD_COMMAND ?? 'CI=1 playcraft build --mode base --output dist .');
14
11
  }
15
- async function saveExact(root) {
16
- await gitWrite(root, ['add', '-A', '.']);
17
- const privatePaths = await gitRead(root, ['ls-files', '--', '.playcraft.json', 'playcraft.config.json', ':(glob)**/.playcraft.json', ':(glob)**/playcraft.config.json']);
18
- if (privatePaths.trim())
19
- throw new Error('batch_private_path');
20
- const clean = await gitWrite(root, ['diff', '--cached', '--quiet']).then(() => true, () => false);
21
- if (!clean)
22
- await gitWrite(root, ['commit', '-m', 'Save batch member']);
23
- return (await gitRead(root, ['rev-parse', 'HEAD'])).trim();
12
+ async function readBuiltHtml(root) {
13
+ const htmlPath = join(root, 'dist', 'index.html');
14
+ const info = await lstat(htmlPath).catch(() => undefined);
15
+ if (!info?.isFile() || info.isSymbolicLink())
16
+ throw new Error('batch_build_failed');
17
+ const handle = await open(htmlPath, constants.O_RDONLY | constants.O_NOFOLLOW).catch(() => {
18
+ throw new Error('batch_build_failed');
19
+ });
20
+ await handle.close();
24
21
  }
25
22
  async function prove(record) {
26
23
  let stopped = true;
@@ -35,8 +32,11 @@ async function prove(record) {
35
32
  }
36
33
  return stopped;
37
34
  }
38
- export async function deliverBatchMember(state, body, options) {
39
- if (!body || typeof body !== 'object' || !('occupancyId' in body) || !body.occupancyId)
35
+ export async function deliverBatchMember(state, body) {
36
+ if (!body ||
37
+ typeof body !== 'object' ||
38
+ !('occupancyId' in body) ||
39
+ !body.occupancyId)
40
40
  throw new Error('occupancy_required');
41
41
  const parsed = sandboxBatchMemberDeliverSchema.safeParse(body);
42
42
  if (!parsed.success)
@@ -47,109 +47,127 @@ export async function deliverBatchMember(state, body, options) {
47
47
  const existing = await readMember(state, command.runId);
48
48
  if (existing && command.ownerFence < existing.ownerFence)
49
49
  throw new Error('stale_owner');
50
- if (existing?.state === 'completed' && existing.phase === 'deliver' && existing.evidence && existing.commandHash?.deliver === hash && existing.ownerFence === command.ownerFence) {
50
+ if (existing?.state === 'completed' &&
51
+ existing.phase === 'deliver' &&
52
+ existing.evidence &&
53
+ existing.commandHash?.deliver === hash &&
54
+ existing.ownerFence === command.ownerFence) {
51
55
  return memberStatus(existing);
52
56
  }
53
- if (await readStopMarker(state, command.runId) && existing?.state === 'completed' && existing.evidence)
57
+ if ((await readStopMarker(state, command.runId)) &&
58
+ existing?.state === 'completed' &&
59
+ existing.evidence)
54
60
  return memberStatus(existing);
55
61
  if (await readStopMarker(state, command.runId))
56
62
  return memberStatus(await markStopped(state, command, existing));
57
- if (existing && existing.ownerFence === command.ownerFence && existing.commandHash?.deliver && existing.commandHash.deliver !== hash && existing.state === 'completed') {
63
+ if (existing &&
64
+ existing.ownerFence === command.ownerFence &&
65
+ existing.commandHash?.deliver &&
66
+ existing.commandHash.deliver !== hash &&
67
+ existing.state === 'completed') {
58
68
  throw new Error('batch_command_conflict');
59
69
  }
60
70
  const root = await workspaceRoot(state, command.workspaceId, command.directoryName);
61
71
  let record = await writeMember(state, {
62
- runId: command.runId, ownerFence: command.ownerFence, occupancyId: command.occupancyId, workspaceId: command.workspaceId,
63
- directoryName: command.directoryName, phase: 'deliver', state: 'running', writersStopped: false, holding: true, stopRequested: false,
64
- processes: existing?.processes ?? [], sessionId: existing?.sessionId, operationId: existing?.operationId, commandHash: { ...existing?.commandHash, deliver: hash },
65
- snapshotDigest: existing?.snapshotDigest, branchName: existing?.branchName, updatedAt: new Date().toISOString(),
66
- sourceCommit: existing?.sourceCommit ?? state.journal.getWorkspace(command.workspaceId)?.directories?.find(directory => directory.directoryName === command.directoryName)?.sourceCommit,
72
+ runId: command.runId,
73
+ ownerFence: command.ownerFence,
74
+ occupancyId: command.occupancyId,
75
+ workspaceId: command.workspaceId,
76
+ directoryName: command.directoryName,
77
+ phase: 'deliver',
78
+ state: 'running',
79
+ writersStopped: false,
80
+ holding: true,
81
+ stopRequested: false,
82
+ processes: existing?.processes ?? [],
83
+ sessionId: existing?.sessionId,
84
+ operationId: existing?.operationId,
85
+ commandHash: { ...existing?.commandHash, deliver: hash },
86
+ snapshotDigest: existing?.snapshotDigest,
87
+ branchName: existing?.branchName,
88
+ updatedAt: new Date().toISOString(),
89
+ sourceCommit: existing?.sourceCommit ??
90
+ state.journal
91
+ .getWorkspace(command.workspaceId)
92
+ ?.directories?.find((directory) => directory.directoryName === command.directoryName)
93
+ ?.sourceCommit,
67
94
  beforeTree: existing?.beforeTree,
68
95
  });
69
- const buildRoot = join(state.config.stateDir, 'batch-builds', `${command.runId}-${randomUUID()}`);
70
96
  try {
71
- const savedCommit = command.fromCommit ?? await saveExact(root);
72
- if (command.fromCommit)
73
- await gitRead(root, ['cat-file', '-e', `${command.fromCommit}^{commit}`]);
74
- await requireBatchContentChanges(root, record.sourceCommit ?? '', savedCommit);
75
97
  if (await readStopMarker(state, command.runId))
76
98
  return memberStatus(await markStopped(state, command, record));
77
- let pushedCommit;
78
- if (command.pushRemote) {
79
- const branch = existing?.branchName ?? (await gitRead(root, ['symbolic-ref', '--short', 'HEAD'])).trim();
80
- try {
81
- await gitWrite(root, ['push', '--porcelain', 'origin', `${savedCommit}:refs/heads/${branch}`]);
82
- }
83
- catch {
84
- throw new Error('batch_push_failed');
85
- }
86
- const remote = (await gitWrite(root, ['ls-remote', 'origin', `refs/heads/${branch}`])).slice(0, savedCommit.length);
87
- if (remote !== savedCommit)
88
- throw new Error('batch_push_failed');
89
- pushedCommit = savedCommit;
90
- }
91
- await mkdir(join(buildRoot, '..'), { recursive: true });
92
- await gitWrite(join(buildRoot, '..'), ['clone', '--local', '--no-hardlinks', '--no-checkout', '--', root, buildRoot]);
93
- await gitWrite(buildRoot, ['checkout', '--detach', savedCommit]);
94
- const built = await runProcessGroup(buildRoot, buildCommand(), 'build', async (process) => {
99
+ const built = await runProcessGroup(root, buildCommand(), 'build', async (process) => {
95
100
  record = await writeMember(state, { ...record, processes: [...record.processes, process] });
96
101
  });
97
- record = await writeMember(state, { ...record, processes: record.processes.map(process => process.pid === built.process.pid ? built.process : process) });
102
+ record = await writeMember(state, {
103
+ ...record,
104
+ processes: record.processes.map((process) => process.pid === built.process.pid ? built.process : process),
105
+ });
98
106
  if (await readStopMarker(state, command.runId))
99
107
  return memberStatus(await markStopped(state, command, record));
100
108
  if (built.code !== 0)
101
109
  throw new Error('batch_build_failed');
102
- const htmlPath = join(buildRoot, 'dist', 'index.html');
103
- if (!(await lstat(htmlPath).catch(() => undefined))?.isFile())
104
- throw new Error('batch_build_failed');
105
- const entries = await readdir(join(buildRoot, 'dist'));
106
- if (!entries.includes('index.html'))
107
- throw new Error('batch_build_failed');
108
- const bodyBytes = await readFile(htmlPath);
109
- if (findExternalLocalReferences(bodyBytes.toString()).length)
110
- throw new Error('batch_verify_failed');
111
- const buildDigest = createHash('sha256').update(bodyBytes).digest('hex');
112
- const previewRef = `workspaces/${command.workspaceId}/batches/members/${command.runId}/${buildDigest}.html`;
113
- const store = options?.objectStore ?? filesystemObjectStore(state);
114
- await store.put(previewRef, bodyBytes);
115
- const published = await store.get(previewRef);
116
- if (createHash('sha256').update(published).digest('hex') !== buildDigest)
117
- throw new Error('batch_verify_failed');
118
- await mkdir(join(root, 'dist'), { recursive: true });
119
- await writeFile(join(root, 'dist', 'index.html'), published);
120
- if (createHash('sha256').update(await readFile(join(root, 'dist', 'index.html'))).digest('hex') !== buildDigest)
121
- throw new Error('batch_verify_failed');
110
+ await readBuiltHtml(root);
122
111
  const writersStopped = await prove(record);
123
112
  if (!writersStopped)
124
- return memberStatus(await writeMember(state, { ...record, state: 'stopping', writersStopped: false, holding: true, headCommit: savedCommit }));
113
+ return memberStatus(await writeMember(state, {
114
+ ...record,
115
+ state: 'stopping',
116
+ writersStopped: false,
117
+ holding: true,
118
+ }));
125
119
  if (await readStopMarker(state, command.runId))
126
- return memberStatus(await markStopped(state, command, { ...record, headCommit: savedCommit }));
120
+ return memberStatus(await markStopped(state, command, record));
127
121
  record = await writeMember(state, {
128
- ...record, state: 'completed', writersStopped: true, holding: false, headCommit: savedCommit, error: undefined,
129
- evidence: { savedCommit, ...(pushedCommit ? { pushedCommit } : {}), buildDigest, verified: true, previewRef },
122
+ ...record,
123
+ state: 'completed',
124
+ writersStopped: true,
125
+ holding: false,
126
+ error: undefined,
127
+ evidence: { verified: true },
130
128
  });
131
129
  return memberStatus(record);
132
130
  }
133
131
  catch (error) {
134
- const code = error instanceof Error && /^[a-z0-9_]+$/.test(error.message) ? error.message : 'batch_deliver_failed';
132
+ const code = error instanceof Error && /^[a-z0-9_]+$/.test(error.message)
133
+ ? error.message
134
+ : 'batch_deliver_failed';
135
135
  const writersStopped = await prove(record);
136
- record = await writeMember(state, { ...record, state: writersStopped ? 'failed' : 'stopping', writersStopped, holding: !writersStopped, error: code });
136
+ record = await writeMember(state, {
137
+ ...record,
138
+ state: writersStopped ? 'failed' : 'stopping',
139
+ writersStopped,
140
+ holding: !writersStopped,
141
+ error: code,
142
+ });
137
143
  return memberStatus(record);
138
144
  }
139
- finally {
140
- await rm(buildRoot, { recursive: true, force: true });
141
- }
142
145
  });
143
146
  }
144
147
  async function markStopped(state, command, existing) {
145
148
  if (existing)
146
149
  await prove(existing);
147
150
  return writeMember(state, {
148
- runId: command.runId, ownerFence: command.ownerFence, occupancyId: existing?.occupancyId ?? command.occupancyId,
149
- workspaceId: command.workspaceId, directoryName: command.directoryName, phase: 'deliver', state: 'stopped', writersStopped: true,
150
- holding: false, stopRequested: true, processes: existing?.processes ?? [], sessionId: existing?.sessionId, operationId: existing?.operationId,
151
- headCommit: existing?.headCommit, evidence: existing?.evidence, commandHash: existing?.commandHash, snapshotDigest: existing?.snapshotDigest,
152
- branchName: existing?.branchName, updatedAt: new Date().toISOString(),
153
- sourceCommit: existing?.sourceCommit, beforeTree: existing?.beforeTree,
151
+ runId: command.runId,
152
+ ownerFence: command.ownerFence,
153
+ occupancyId: existing?.occupancyId ?? command.occupancyId,
154
+ workspaceId: command.workspaceId,
155
+ directoryName: command.directoryName,
156
+ phase: 'deliver',
157
+ state: 'stopped',
158
+ writersStopped: true,
159
+ holding: false,
160
+ stopRequested: true,
161
+ processes: existing?.processes ?? [],
162
+ sessionId: existing?.sessionId,
163
+ operationId: existing?.operationId,
164
+ headCommit: existing?.headCommit,
165
+ evidence: existing?.evidence,
166
+ commandHash: existing?.commandHash,
167
+ snapshotDigest: existing?.snapshotDigest,
168
+ branchName: existing?.branchName,
169
+ updatedAt: new Date().toISOString(),
170
+ sourceCommit: existing?.sourceCommit,
171
+ beforeTree: existing?.beforeTree,
154
172
  });
155
173
  }
@@ -6,14 +6,21 @@ import { sandboxBatchMemberPrepareSchema, } from '@playcraft/common/cloud-worksp
6
6
  import { registerWorkspaceDirectory } from '../workspaces/directories.js';
7
7
  import { workspaceContainerRoot } from '../workspaces/registry.js';
8
8
  import { copyCredentialHelpers, gitRead, gitWrite } from './git.js';
9
- import { digestFiles, readSourceFiles, restoreArchive } from './source-capture.js';
9
+ import { restoreArchive } from './source-capture.js';
10
10
  import { groupExited, runProcessGroup } from './processes.js';
11
11
  import { filesystemObjectStore, memberStatus, readMember, readSnapshot, readStopMarker, serializeRun, sourceCacheDir, writeMember, } from './records.js';
12
12
  const BRANCH = /^remix\/draft-[a-f0-9]{16}$/;
13
- const LOCKS = ['pnpm-lock.yaml', 'package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock', 'bun.lock', 'bun.lockb'];
13
+ const LOCKS = [
14
+ 'pnpm-lock.yaml',
15
+ 'package-lock.json',
16
+ 'npm-shrinkwrap.json',
17
+ 'yarn.lock',
18
+ 'bun.lock',
19
+ 'bun.lockb',
20
+ ];
14
21
  export function frozenInstallCommand() {
15
- return process.env.PLAYCRAFT_BATCH_INSTALL_COMMAND
16
- ?? 'pnpm install --frozen-lockfile --store-dir /var/cache/playcraft/pnpm-store --ignore-scripts';
22
+ return (process.env.PLAYCRAFT_BATCH_INSTALL_COMMAND ??
23
+ 'pnpm install --frozen-lockfile --store-dir /var/cache/playcraft/pnpm-store --ignore-scripts');
17
24
  }
18
25
  function hashCommand(value) {
19
26
  return createHash('sha256').update(JSON.stringify(value)).digest('hex');
@@ -21,10 +28,17 @@ function hashCommand(value) {
21
28
  async function materialize(state, snapshot, destination, store, sourceRoot) {
22
29
  const cache = sourceCacheDir(state, snapshot.batchId);
23
30
  const cachedDigest = await readFile(`${cache}.digest`, 'utf8').catch(() => '');
24
- if (cachedDigest.trim() === snapshot.digest && await lstat(join(cache, '.git')).catch(() => undefined)) {
25
- await gitWrite(dirnameOf(destination), ['clone', '--local', '--no-hardlinks', '--', cache, destination]);
26
- if (digestFiles(await readSourceFiles(destination)) !== snapshot.digest)
27
- throw new Error('batch_snapshot_mismatch');
31
+ if (cachedDigest.trim() === snapshot.digest &&
32
+ (await lstat(join(cache, '.git')).catch(() => undefined))) {
33
+ await gitWrite(dirnameOf(destination), [
34
+ 'clone',
35
+ '--local',
36
+ '--no-hardlinks',
37
+ '--',
38
+ cache,
39
+ destination,
40
+ ]);
41
+ await assertFrozenCommit(destination, snapshot.commitHash);
28
42
  return;
29
43
  }
30
44
  if (snapshot.remoteRef) {
@@ -41,34 +55,64 @@ async function materialize(state, snapshot, destination, store, sourceRoot) {
41
55
  throw new Error('batch_snapshot_mismatch');
42
56
  }
43
57
  await gitWrite(destination, ['checkout', '--detach', 'FETCH_HEAD']);
44
- if ((await gitRead(destination, ['rev-parse', 'HEAD'])).trim() !== snapshot.commitHash)
45
- throw new Error('batch_snapshot_mismatch');
46
- if (digestFiles(await readSourceFiles(destination)) !== snapshot.digest)
47
- throw new Error('batch_snapshot_mismatch');
58
+ await assertFrozenCommit(destination, snapshot.commitHash);
48
59
  return;
49
60
  }
50
61
  if (!snapshot.artifactKey)
51
62
  throw new Error('batch_snapshot_mismatch');
52
- const body = await store.get(snapshot.artifactKey).catch(() => { throw new Error('batch_snapshot_mismatch'); });
63
+ const body = await store.get(snapshot.artifactKey).catch(() => {
64
+ throw new Error('batch_snapshot_mismatch');
65
+ });
53
66
  await restoreArchive(body, destination, snapshot.digest);
54
67
  }
55
68
  function dirnameOf(path) {
56
69
  return join(path, '..');
57
70
  }
71
+ async function assertFrozenCommit(root, commit) {
72
+ if ((await gitRead(root, ['rev-parse', 'HEAD'])).trim() !== commit)
73
+ throw new Error('batch_snapshot_mismatch');
74
+ }
75
+ /** A bound workspace keeps origin aligned. A local workspace only needs the branch. */
58
76
  async function checkoutBranch(root, branchName, commit, repositoryUrl) {
59
77
  await gitWrite(root, ['checkout', '-B', branchName, commit]);
60
- const origin = await gitRead(root, ['remote', 'get-url', 'origin']).catch(() => '');
61
- if (!origin.trim())
62
- await gitWrite(root, ['remote', 'add', 'origin', repositoryUrl]);
63
- else if (origin.trim() !== repositoryUrl)
64
- await gitWrite(root, ['remote', 'set-url', 'origin', repositoryUrl]);
78
+ if (repositoryUrl) {
79
+ const origin = await gitRead(root, ['remote', 'get-url', 'origin']).catch(() => '');
80
+ if (!origin.trim())
81
+ await gitWrite(root, ['remote', 'add', 'origin', repositoryUrl]);
82
+ else if (origin.trim() !== repositoryUrl)
83
+ await gitWrite(root, ['remote', 'set-url', 'origin', repositoryUrl]);
84
+ }
85
+ else {
86
+ await gitWrite(root, ['remote', 'remove', 'origin']).catch(() => undefined);
87
+ }
65
88
  await gitWrite(root, ['config', 'user.name', 'PlayCraft']);
66
89
  await gitWrite(root, ['config', 'user.email', 'playcraft@localhost']);
67
90
  }
68
91
  async function resumeBranch(repositoryUrl, branchName, commit, destination, sourceRoot) {
69
- await gitWrite(dirnameOf(destination), ['clone', '--branch', branchName, '--single-branch', '--', repositoryUrl, destination]);
70
- await copyCredentialHelpers(sourceRoot, destination);
71
- if ((await gitRead(destination, ['rev-parse', 'HEAD'])).trim() !== commit && !(await gitRead(destination, ['cat-file', '-e', `${commit}^{commit}`]).then(() => true, () => false))) {
92
+ if (repositoryUrl) {
93
+ await gitWrite(dirnameOf(destination), [
94
+ 'clone',
95
+ '--branch',
96
+ branchName,
97
+ '--single-branch',
98
+ '--',
99
+ repositoryUrl,
100
+ destination,
101
+ ]);
102
+ await copyCredentialHelpers(sourceRoot, destination);
103
+ }
104
+ else {
105
+ await gitWrite(dirnameOf(destination), [
106
+ 'clone',
107
+ '--local',
108
+ '--no-hardlinks',
109
+ '--',
110
+ sourceRoot,
111
+ destination,
112
+ ]);
113
+ }
114
+ if ((await gitRead(destination, ['rev-parse', 'HEAD'])).trim() !== commit &&
115
+ !(await gitRead(destination, ['cat-file', '-e', `${commit}^{commit}`]).then(() => true, () => false))) {
72
116
  throw new Error('batch_commit_mismatch');
73
117
  }
74
118
  await gitWrite(destination, ['checkout', '-B', branchName, commit]);
@@ -102,17 +146,25 @@ export async function assertMemberDependencies(root) {
102
146
  if (present[0] !== 'pnpm-lock.yaml')
103
147
  throw new Error('dependency_lock_mismatch');
104
148
  const lock = await readFile(join(root, 'pnpm-lock.yaml'), 'utf8');
105
- if (names.some(name => !lock.includes(`${name}:`) && !lock.includes(`/${name}@`) && !lock.includes(`'${name}'`) && !lock.includes(`"${name}"`))) {
149
+ if (names.some((name) => !lock.includes(`${name}:`) &&
150
+ !lock.includes(`/${name}@`) &&
151
+ !lock.includes(`'${name}'`) &&
152
+ !lock.includes(`"${name}"`))) {
106
153
  throw new Error('dependency_lock_mismatch');
107
154
  }
108
155
  return 'install';
109
156
  }
110
157
  export async function prepareBatchMember(state, body, options) {
111
- if (!body || typeof body !== 'object' || !('occupancyId' in body) || !body.occupancyId)
158
+ if (!body ||
159
+ typeof body !== 'object' ||
160
+ !('occupancyId' in body) ||
161
+ !body.occupancyId)
112
162
  throw new Error('occupancy_required');
113
163
  const parsed = sandboxBatchMemberPrepareSchema.safeParse(body);
114
164
  if (!parsed.success)
115
- throw new Error(parsed.error.issues.some(issue => issue.path.includes('branchName')) ? 'batch_branch_invalid' : 'invalid_batch_request');
165
+ throw new Error(parsed.error.issues.some((issue) => issue.path.includes('branchName'))
166
+ ? 'batch_branch_invalid'
167
+ : 'invalid_batch_request');
116
168
  const command = parsed.data;
117
169
  if (!BRANCH.test(command.branchName))
118
170
  throw new Error('batch_branch_invalid');
@@ -124,81 +176,142 @@ export async function prepareBatchMember(state, body, options) {
124
176
  throw new Error('stale_owner');
125
177
  if (marker)
126
178
  return memberStatus(await sealStopped(state, command, existing));
127
- if (existing && existing.ownerFence === command.ownerFence && existing.commandHash?.prepare === hash && existing.state !== 'running' && existing.state !== 'failed') {
179
+ if (existing &&
180
+ existing.ownerFence === command.ownerFence &&
181
+ existing.commandHash?.prepare === hash &&
182
+ existing.state !== 'running' &&
183
+ existing.state !== 'failed') {
128
184
  return memberStatus(existing);
129
185
  }
130
- if (existing && existing.ownerFence === command.ownerFence && existing.commandHash?.prepare && existing.commandHash.prepare !== hash)
186
+ if (existing &&
187
+ existing.ownerFence === command.ownerFence &&
188
+ existing.commandHash?.prepare &&
189
+ existing.commandHash.prepare !== hash)
131
190
  throw new Error('batch_command_conflict');
132
- if (existing?.processes.some(process => process.role === 'install' && !process.stopped && !groupExited(process.pid)))
191
+ if (existing?.processes.some((process) => process.role === 'install' && !process.stopped && !groupExited(process.pid)))
133
192
  return memberStatus(existing);
134
193
  const snapshot = command.snapshot;
135
194
  if ((await readSnapshot(state, snapshot.batchId))?.digest === snapshot.digest) {
136
195
  /* The local snapshot record is an accelerator; the command snapshot remains authoritative. */
137
196
  }
138
197
  let record = await writeMember(state, {
139
- runId: command.runId, ownerFence: command.ownerFence, occupancyId: command.occupancyId,
140
- workspaceId: command.workspaceId, directoryName: command.directoryName, phase: 'prepare', state: 'running',
141
- writersStopped: false, holding: true, stopRequested: false, processes: existing?.processes ?? [],
142
- commandHash: { ...existing?.commandHash, prepare: hash }, snapshotDigest: snapshot.digest, branchName: command.branchName,
143
- sourceCommit: snapshot.commitHash, beforeTree: existing?.beforeTree,
198
+ runId: command.runId,
199
+ ownerFence: command.ownerFence,
200
+ occupancyId: command.occupancyId,
201
+ workspaceId: command.workspaceId,
202
+ directoryName: command.directoryName,
203
+ phase: 'prepare',
204
+ state: 'running',
205
+ writersStopped: false,
206
+ holding: true,
207
+ stopRequested: false,
208
+ processes: existing?.processes ?? [],
209
+ commandHash: { ...existing?.commandHash, prepare: hash },
210
+ snapshotDigest: snapshot.digest,
211
+ branchName: command.branchName,
212
+ sourceCommit: snapshot.commitHash,
213
+ beforeTree: existing?.beforeTree,
144
214
  updatedAt: new Date().toISOString(),
145
215
  });
146
- const parent = await workspaceContainerRoot(state, command.workspaceId);
147
- const root = join(parent, command.directoryName);
148
- const staging = join(parent, `.batch-member-${randomUUID()}`);
149
- const binding = state.journal.getWorkspace(command.workspaceId);
150
- if (!binding?.repositoryUrl)
151
- throw new Error('workspace_not_registered');
152
- const sourceRoot = join(parent, snapshot.sourceDirectoryName);
216
+ let staging;
153
217
  try {
154
- if (!await lstat(root).catch(() => undefined)) {
218
+ const parent = await workspaceContainerRoot(state, command.workspaceId);
219
+ const root = join(parent, command.directoryName);
220
+ staging = join(parent, `.batch-member-${randomUUID()}`);
221
+ const binding = state.journal.getWorkspace(command.workspaceId);
222
+ if (!binding)
223
+ throw new Error('workspace_not_registered');
224
+ const repositoryUrl = binding.repositoryUrl ?? undefined;
225
+ const sourceRoot = join(parent, snapshot.sourceDirectoryName);
226
+ if (!(await lstat(root).catch(() => undefined))) {
155
227
  await mkdir(parent, { recursive: true });
156
228
  if (command.resumeFromCommit)
157
- await resumeBranch(binding.repositoryUrl, command.branchName, command.resumeFromCommit, staging, sourceRoot);
229
+ await resumeBranch(repositoryUrl, command.branchName, command.resumeFromCommit, staging, sourceRoot);
158
230
  else
159
231
  await materialize(state, snapshot, staging, options?.objectStore ?? filesystemObjectStore(state), sourceRoot);
160
232
  const commit = command.resumeFromCommit ?? snapshot.commitHash;
161
- await checkoutBranch(staging, command.branchName, commit, binding.repositoryUrl);
233
+ await checkoutBranch(staging, command.branchName, commit, repositoryUrl);
162
234
  const { rename } = await import('node:fs/promises');
163
235
  await rename(staging, root);
164
236
  }
165
237
  await registerWorkspaceDirectory(state, {
166
- workspaceId: command.workspaceId, directoryName: command.directoryName, remixId: command.remixId,
167
- branchName: command.branchName, sourceCommit: command.resumeFromCommit ?? snapshot.commitHash,
238
+ workspaceId: command.workspaceId,
239
+ directoryName: command.directoryName,
240
+ remixId: command.remixId,
241
+ branchName: command.branchName,
242
+ sourceCommit: command.resumeFromCommit ?? snapshot.commitHash,
168
243
  }, state.acceptedLeaseFence);
169
244
  const plan = await assertMemberDependencies(root);
170
245
  if (plan === 'install') {
171
246
  const installed = await runProcessGroup(root, frozenInstallCommand(), 'install', async (process) => {
172
- record = await writeMember(state, { ...record, processes: [...record.processes, process] });
247
+ record = await writeMember(state, {
248
+ ...record,
249
+ processes: [...record.processes, process],
250
+ });
251
+ });
252
+ record = await writeMember(state, {
253
+ ...record,
254
+ processes: record.processes.map((process) => process.pid === installed.process.pid
255
+ ? { ...installed.process, role: 'install' }
256
+ : process),
173
257
  });
174
- record = await writeMember(state, { ...record, processes: record.processes.map(process => process.pid === installed.process.pid ? { ...installed.process, role: 'install' } : process) });
175
258
  if (await readStopMarker(state, command.runId))
176
259
  return memberStatus(await sealStopped(state, command, record));
177
260
  if (installed.code !== 0)
178
261
  throw new Error('batch_install_failed');
179
262
  }
180
263
  const head = (await gitRead(root, ['rev-parse', 'HEAD'])).trim();
181
- record = await writeMember(state, { ...record, state: 'completed', writersStopped: true, holding: true, headCommit: head, error: undefined });
264
+ record = await writeMember(state, {
265
+ ...record,
266
+ state: 'completed',
267
+ writersStopped: true,
268
+ holding: true,
269
+ headCommit: head,
270
+ error: undefined,
271
+ });
182
272
  return memberStatus(record);
183
273
  }
184
274
  catch (error) {
185
- const code = error instanceof Error && /^[a-z0-9_]+$/.test(error.message) ? error.message : 'batch_prepare_failed';
186
- record = await writeMember(state, { ...record, state: 'failed', writersStopped: true, holding: false, error: code });
275
+ const code = error instanceof Error && /^[a-z0-9_]+$/.test(error.message)
276
+ ? error.message
277
+ : 'batch_prepare_failed';
278
+ record = await writeMember(state, {
279
+ ...record,
280
+ state: 'failed',
281
+ writersStopped: true,
282
+ holding: false,
283
+ error: code,
284
+ });
187
285
  return memberStatus(record);
188
286
  }
189
287
  finally {
190
- await rm(staging, { recursive: true, force: true });
288
+ if (staging)
289
+ await rm(staging, { recursive: true, force: true });
191
290
  }
192
291
  });
193
292
  }
194
293
  async function sealStopped(state, command, existing) {
195
294
  return writeMember(state, {
196
- runId: command.runId, ownerFence: Math.max(command.ownerFence, existing?.ownerFence ?? 0), occupancyId: command.occupancyId,
197
- workspaceId: command.workspaceId, directoryName: command.directoryName, phase: existing?.phase ?? 'prepare',
198
- state: 'stopped', writersStopped: true, holding: false, stopRequested: true, processes: existing?.processes ?? [],
199
- sessionId: existing?.sessionId, operationId: existing?.operationId, headCommit: existing?.headCommit, evidence: existing?.evidence,
200
- commandHash: existing?.commandHash, snapshotDigest: existing?.snapshotDigest, branchName: existing?.branchName,
201
- sourceCommit: existing?.sourceCommit, beforeTree: existing?.beforeTree,
295
+ runId: command.runId,
296
+ ownerFence: Math.max(command.ownerFence, existing?.ownerFence ?? 0),
297
+ occupancyId: command.occupancyId,
298
+ workspaceId: command.workspaceId,
299
+ directoryName: command.directoryName,
300
+ phase: existing?.phase ?? 'prepare',
301
+ state: 'stopped',
302
+ writersStopped: true,
303
+ holding: false,
304
+ stopRequested: true,
305
+ processes: existing?.processes ?? [],
306
+ sessionId: existing?.sessionId,
307
+ operationId: existing?.operationId,
308
+ headCommit: existing?.headCommit,
309
+ evidence: existing?.evidence,
310
+ commandHash: existing?.commandHash,
311
+ snapshotDigest: existing?.snapshotDigest,
312
+ branchName: existing?.branchName,
313
+ sourceCommit: existing?.sourceCommit,
314
+ beforeTree: existing?.beforeTree,
202
315
  updatedAt: new Date().toISOString(),
203
316
  });
204
317
  }