@playcraft/cli 0.1.70 → 0.1.72
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/README.md +20 -1
- package/dist/cli-root-help.js +1 -0
- package/dist/commands/references.js +3 -0
- package/dist/commands/skills.js +68 -4
- package/dist/commands/tools-batch.js +2 -0
- package/dist/project-skills/discovery.js +248 -0
- package/dist/project-skills/messages.js +80 -0
- package/dist/project-skills/remote-activation.js +500 -0
- package/dist/project-skills/remote-client.js +117 -0
- package/dist/project-skills/remote-commands.js +87 -0
- package/dist/project-skills/remote-prepare.js +223 -0
- package/dist/project-skills/search-command.js +47 -0
- package/dist/project-skills/search.js +96 -0
- package/dist/recording/project-digest.js +1 -1
- package/dist/references/messages.js +2 -0
- package/dist/references/protocol.js +39 -12
- package/dist/workspace-runtime/batch/freeze.js +94 -24
- package/dist/workspace-runtime/batch/records.js +12 -6
- package/dist/workspace-runtime/batch/routes.js +31 -11
- package/dist/workspace-runtime/batch/source-candidate.js +29 -16
- package/dist/workspace-runtime/batch/source-capture.js +251 -20
- package/dist/workspace-runtime/codex/app-server.js +30 -1
- package/dist/workspace-runtime/codex/model-broker.js +93 -6
- package/dist/workspace-runtime/codex/remix-references-broker.js +208 -67
- package/dist/workspace-runtime/codex/skill-discovery-broker.js +174 -0
- package/dist/workspace-runtime/serve.js +5 -0
- package/dist/workspace-runtime/server/directory-access.js +10 -0
- package/dist/workspace-runtime/server/dispatch.js +121 -28
- package/dist/workspace-runtime/server/http.js +345 -117
- package/dist/workspace-runtime/server/operation-record.js +3 -1
- package/dist/workspace-runtime/workspaces/composer-context.js +92 -41
- package/dist/workspace-runtime/workspaces/files.js +59 -23
- package/dist/workspace-runtime/workspaces/remix-references.js +162 -53
- package/dist/workspace-runtime/workspaces/skill-activation-recovery.js +54 -0
- package/dist/workspace-runtime/workspaces/skill-activation-requests.js +512 -0
- package/dist/workspace-runtime/workspaces/skills.js +13 -176
- package/dist/workspace-runtime/workspaces/starter-template.js +74 -0
- package/package.json +4 -3
- package/project-template/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
- package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
- package/project-templates/empty/gitignore +1 -0
- package/project-templates/phaser/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
- package/project-templates/threejs/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
|
@@ -6,11 +6,11 @@ import { workspaceRoot } from '../workspaces/registry.js';
|
|
|
6
6
|
import { gitRead } from './git.js';
|
|
7
7
|
import { captureIndependentSource } from './source-capture.js';
|
|
8
8
|
import { filesystemObjectStore, readSnapshot, serializeFreeze, sourceCacheDir, writeSnapshot, } from './records.js';
|
|
9
|
-
import { assertSourceIdentity, readSourceCandidate, writeSourceCandidate } from './source-candidate.js';
|
|
9
|
+
import { assertSourceIdentity, readSourceCandidate, writeSourceCandidate, } from './source-candidate.js';
|
|
10
10
|
import { BatchSourceFreezeError, sourceFreezeError } from './source-failure.js';
|
|
11
11
|
import { publishSourceCandidate } from './source-publish.js';
|
|
12
12
|
import { clearDirectoryFreeze, withDirectoryFreeze } from '../server/directory-access.js';
|
|
13
|
-
import { adoptPublicationBinding, publicationBinding, publicationGitOptions } from './publication.js';
|
|
13
|
+
import { adoptPublicationBinding, publicationBinding, publicationGitOptions, } from './publication.js';
|
|
14
14
|
async function usagePercent(path) {
|
|
15
15
|
const disk = await statfs(path);
|
|
16
16
|
if (disk.blocks <= 0)
|
|
@@ -23,7 +23,7 @@ async function stopManagedWriters(state, workspaceId, directoryName) {
|
|
|
23
23
|
for (const write of state.journal.listManagedWrites()) {
|
|
24
24
|
if (write.stoppedAt || write.workspaceId !== scope)
|
|
25
25
|
continue;
|
|
26
|
-
if (write.pid === undefined || !await stopBackgroundWriter(write.pid))
|
|
26
|
+
if (write.pid === undefined || !(await stopBackgroundWriter(write.pid)))
|
|
27
27
|
continue;
|
|
28
28
|
await state.journal.putManagedWrite({ ...write, stoppedAt: new Date().toISOString() });
|
|
29
29
|
}
|
|
@@ -42,8 +42,11 @@ export async function freezeBatchSource(state, body, options) {
|
|
|
42
42
|
await clearDirectoryFreeze(state, request.batchId);
|
|
43
43
|
return batchSourceFreezeResultSchema.parse({ status: 'frozen', snapshot: frozen });
|
|
44
44
|
}
|
|
45
|
-
return withDirectoryFreeze(state, {
|
|
46
|
-
|
|
45
|
+
return withDirectoryFreeze(state, {
|
|
46
|
+
workspaceId: request.workspaceId,
|
|
47
|
+
directoryName: request.sourceDirectoryName,
|
|
48
|
+
batchId: request.batchId,
|
|
49
|
+
}, () => serializeFreeze(async () => {
|
|
47
50
|
let candidate;
|
|
48
51
|
let source;
|
|
49
52
|
try {
|
|
@@ -56,7 +59,12 @@ export async function freezeBatchSource(state, body, options) {
|
|
|
56
59
|
return batchSourceFreezeResultSchema.parse({ status: 'frozen', snapshot: existing });
|
|
57
60
|
}
|
|
58
61
|
if (!pending && 'resumeOnly' in request && request.resumeOnly === true) {
|
|
59
|
-
throw await sourceFreezeError(state, request.batchId, {
|
|
62
|
+
throw await sourceFreezeError(state, request.batchId, {
|
|
63
|
+
code: 'batch_source_candidate_lost',
|
|
64
|
+
stage: 'capture',
|
|
65
|
+
retryable: false,
|
|
66
|
+
outcome: 'unknown',
|
|
67
|
+
});
|
|
60
68
|
}
|
|
61
69
|
if (pending)
|
|
62
70
|
assertSourceIdentity(state, request, pending.snapshot, pending);
|
|
@@ -70,26 +78,40 @@ export async function freezeBatchSource(state, body, options) {
|
|
|
70
78
|
if (pending)
|
|
71
79
|
candidate = pending;
|
|
72
80
|
else {
|
|
73
|
-
const percent = Math.min(100, Math.max(0, options?.usagePercent ?? await usagePercent(source)));
|
|
81
|
+
const percent = Math.min(100, Math.max(0, options?.usagePercent ?? (await usagePercent(source))));
|
|
74
82
|
if (workspaceDiskAdmission(percent) === 'paused')
|
|
75
83
|
throw new Error('disk_admission_paused');
|
|
76
84
|
await stopManagedWriters(state, request.workspaceId, request.sourceDirectoryName);
|
|
77
85
|
const cache = sourceCacheDir(state, request.batchId);
|
|
78
86
|
await rm(cache, { recursive: true, force: true });
|
|
79
|
-
const captured = await captureIndependentSource(source, cache
|
|
87
|
+
const captured = await captureIndependentSource(source, cache, request.starterTemplate, {
|
|
88
|
+
batchId: request.batchId,
|
|
89
|
+
workspaceId: request.workspaceId,
|
|
90
|
+
directoryName: request.sourceDirectoryName,
|
|
91
|
+
});
|
|
80
92
|
// private
|
|
81
93
|
await writeFile(`${cache}.digest`, captured.digest, { mode: 0o600 });
|
|
82
94
|
const snapshot = {
|
|
83
|
-
batchId: request.batchId,
|
|
84
|
-
|
|
95
|
+
batchId: request.batchId,
|
|
96
|
+
commitHash: captured.commitHash,
|
|
97
|
+
digest: captured.digest,
|
|
98
|
+
sourceWorkspaceId: request.workspaceId,
|
|
99
|
+
sourceDirectoryName: request.sourceDirectoryName,
|
|
85
100
|
frozenAt: new Date((await gitRead(cache, ['show', '-s', '--format=%cI', 'HEAD'])).trim()).toISOString(),
|
|
86
|
-
...(request.persist === 'remote_ref'
|
|
87
|
-
|
|
101
|
+
...(request.persist === 'remote_ref'
|
|
102
|
+
? { remoteRef: batchSourceRemoteRef(request.batchId) }
|
|
103
|
+
: {
|
|
104
|
+
artifactKey: `workspaces/${request.workspaceId}/batches/${request.batchId}/source/${captured.digest}.json.gz`,
|
|
105
|
+
}),
|
|
88
106
|
};
|
|
89
107
|
candidate = {
|
|
90
|
-
version: 1,
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
version: 1,
|
|
109
|
+
persist: request.persist,
|
|
110
|
+
repositoryUrl: binding?.repositoryUrl ?? undefined,
|
|
111
|
+
projectId: binding?.projectId,
|
|
112
|
+
branchName: request.sourceDirectoryName === 'basic'
|
|
113
|
+
? (binding?.branchName ?? undefined)
|
|
114
|
+
: (binding?.directories?.find((directory) => directory.directoryName === request.sourceDirectoryName)?.branchName ?? undefined),
|
|
93
115
|
snapshot,
|
|
94
116
|
};
|
|
95
117
|
await writeSourceCandidate(state, candidate);
|
|
@@ -98,12 +120,44 @@ export async function freezeBatchSource(state, body, options) {
|
|
|
98
120
|
catch (error) {
|
|
99
121
|
if (error instanceof BatchSourceFreezeError)
|
|
100
122
|
throw error;
|
|
101
|
-
const known = new Set([
|
|
102
|
-
'
|
|
103
|
-
|
|
123
|
+
const known = new Set([
|
|
124
|
+
'disk_admission_paused',
|
|
125
|
+
'disk_usage_unavailable',
|
|
126
|
+
'batch_source_changed',
|
|
127
|
+
'batch_source_path_invalid',
|
|
128
|
+
'workspace_not_registered',
|
|
129
|
+
'batch_source_identity_mismatch',
|
|
130
|
+
'batch_source_snapshot_mismatch',
|
|
131
|
+
'batch_source_candidate_lost',
|
|
132
|
+
'starter_template_unavailable',
|
|
133
|
+
'starter_template_invalid',
|
|
134
|
+
'starter_template_path_conflict',
|
|
135
|
+
'batch_source_ignored_files',
|
|
136
|
+
]);
|
|
137
|
+
const code = error instanceof Error && known.has(error.message)
|
|
138
|
+
? error.message
|
|
139
|
+
: 'batch_source_capture_failed';
|
|
104
140
|
throw await sourceFreezeError(state, request.batchId, {
|
|
105
|
-
code,
|
|
106
|
-
|
|
141
|
+
code,
|
|
142
|
+
stage: 'capture',
|
|
143
|
+
retryable: !(code === 'batch_source_changed' &&
|
|
144
|
+
request.starterTemplate &&
|
|
145
|
+
'snapshot' in request.starterTemplate) &&
|
|
146
|
+
![
|
|
147
|
+
'batch_source_path_invalid',
|
|
148
|
+
'batch_source_identity_mismatch',
|
|
149
|
+
'batch_source_snapshot_mismatch',
|
|
150
|
+
'batch_source_candidate_lost',
|
|
151
|
+
'starter_template_unavailable',
|
|
152
|
+
'starter_template_invalid',
|
|
153
|
+
'starter_template_path_conflict',
|
|
154
|
+
'batch_source_ignored_files',
|
|
155
|
+
].includes(code),
|
|
156
|
+
outcome: code === 'batch_source_identity_mismatch' ||
|
|
157
|
+
code === 'batch_source_snapshot_mismatch' ||
|
|
158
|
+
code === 'batch_source_candidate_lost'
|
|
159
|
+
? 'unknown'
|
|
160
|
+
: 'not_published',
|
|
107
161
|
}, error);
|
|
108
162
|
}
|
|
109
163
|
await publishSourceCandidate(state, candidate, source, options?.objectStore ?? filesystemObjectStore(state), request.publication);
|
|
@@ -113,14 +167,30 @@ export async function freezeBatchSource(state, body, options) {
|
|
|
113
167
|
await writeSnapshot(state, candidate.snapshot);
|
|
114
168
|
}
|
|
115
169
|
catch (error) {
|
|
116
|
-
throw await sourceFreezeError(state, request.batchId, {
|
|
170
|
+
throw await sourceFreezeError(state, request.batchId, {
|
|
171
|
+
code: 'batch_source_verification_failed',
|
|
172
|
+
stage: 'verify',
|
|
173
|
+
retryable: true,
|
|
174
|
+
outcome: 'unknown',
|
|
175
|
+
}, error);
|
|
117
176
|
}
|
|
118
|
-
return batchSourceFreezeResultSchema.parse({
|
|
177
|
+
return batchSourceFreezeResultSchema.parse({
|
|
178
|
+
status: 'frozen',
|
|
179
|
+
snapshot: candidate.snapshot,
|
|
180
|
+
});
|
|
119
181
|
}), () => stopManagedWriters(state, request.workspaceId, request.sourceDirectoryName), {
|
|
120
|
-
signal: options?.signal,
|
|
182
|
+
signal: options?.signal,
|
|
183
|
+
deadline: 'deadline' in request && typeof request.deadline === 'string'
|
|
184
|
+
? request.deadline
|
|
185
|
+
: undefined,
|
|
121
186
|
}).catch(async (error) => {
|
|
122
187
|
if (error instanceof Error && error.message === 'directory_access_cancelled')
|
|
123
|
-
throw await sourceFreezeError(state, request.batchId, {
|
|
188
|
+
throw await sourceFreezeError(state, request.batchId, {
|
|
189
|
+
code: 'source_freeze_timeout',
|
|
190
|
+
stage: 'capture',
|
|
191
|
+
retryable: false,
|
|
192
|
+
outcome: 'not_published',
|
|
193
|
+
});
|
|
124
194
|
throw error;
|
|
125
195
|
});
|
|
126
196
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { batchSourceSnapshotRefSchema, sandboxBatchMemberStatusSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
4
|
+
import { batchSourceSnapshotRefSchema, normalizeLegacyBatchSourceSnapshot, sandboxBatchMemberStatusSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
5
5
|
import { releaseBatchDirectory } from '../server/directory-access.js';
|
|
6
6
|
const runs = new Map();
|
|
7
7
|
let freezeChain = Promise.resolve();
|
|
@@ -29,7 +29,7 @@ export function sourceCacheDir(state, batchId) {
|
|
|
29
29
|
return join(state.config.stateDir, 'batch-source-cache', batchId);
|
|
30
30
|
}
|
|
31
31
|
export function objectFile(state, key) {
|
|
32
|
-
if (key.split('/').some(part => !part || part === '.' || part === '..'))
|
|
32
|
+
if (key.split('/').some((part) => !part || part === '.' || part === '..'))
|
|
33
33
|
throw new Error('batch_object_key_invalid');
|
|
34
34
|
return join(state.config.stateDir, 'batch-objects', key);
|
|
35
35
|
}
|
|
@@ -53,7 +53,9 @@ async function readJson(path) {
|
|
|
53
53
|
}
|
|
54
54
|
export async function readSnapshot(state, batchId) {
|
|
55
55
|
const value = await readJson(snapshotFile(state, batchId));
|
|
56
|
-
return value
|
|
56
|
+
return value
|
|
57
|
+
? batchSourceSnapshotRefSchema.parse(normalizeLegacyBatchSourceSnapshot(value))
|
|
58
|
+
: undefined;
|
|
57
59
|
}
|
|
58
60
|
export async function writeSnapshot(state, snapshot) {
|
|
59
61
|
const parsed = batchSourceSnapshotRefSchema.parse(snapshot);
|
|
@@ -70,7 +72,7 @@ export async function writeMember(state, record) {
|
|
|
70
72
|
}
|
|
71
73
|
export async function listMembers(state) {
|
|
72
74
|
const directory = join(state.config.stateDir, 'batch-members');
|
|
73
|
-
const names = await readdir(directory).catch(error => {
|
|
75
|
+
const names = await readdir(directory).catch((error) => {
|
|
74
76
|
if (error.code === 'ENOENT')
|
|
75
77
|
return [];
|
|
76
78
|
throw error;
|
|
@@ -111,7 +113,11 @@ export function memberStatus(record) {
|
|
|
111
113
|
}
|
|
112
114
|
export function filesystemObjectStore(state) {
|
|
113
115
|
return {
|
|
114
|
-
async put(key, body) {
|
|
115
|
-
|
|
116
|
+
async put(key, body) {
|
|
117
|
+
await writeAtomic(objectFile(state, key), body);
|
|
118
|
+
},
|
|
119
|
+
async get(key) {
|
|
120
|
+
return readFile(objectFile(state, key));
|
|
121
|
+
},
|
|
116
122
|
};
|
|
117
123
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { batchSourceFreezeResultSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
1
|
+
import { batchSourceFreezeResultSchema, WORKSPACE_REMIX_REFERENCE_BODY_MAX_BYTES, } from '@playcraft/common/cloud-workspace/runtime';
|
|
2
2
|
import { stopBatchMember, readBatchMemberStatus } from './control.js';
|
|
3
3
|
import { deliverBatchMember } from './deliver.js';
|
|
4
4
|
import { freezeBatchSource } from './freeze.js';
|
|
@@ -6,17 +6,28 @@ import { generateBatchMember } from './generate.js';
|
|
|
6
6
|
import { prepareBatchMember } from './prepare.js';
|
|
7
7
|
import { BatchSourceFreezeError } from './source-failure.js';
|
|
8
8
|
const CLIENT_ERRORS = new Set([
|
|
9
|
-
'occupancy_required',
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
9
|
+
'occupancy_required',
|
|
10
|
+
'invalid_batch_request',
|
|
11
|
+
'batch_branch_invalid',
|
|
12
|
+
'stale_owner',
|
|
13
|
+
'disk_admission_paused',
|
|
14
|
+
'batch_source_push_failed',
|
|
15
|
+
'batch_source_upload_failed',
|
|
16
|
+
'batch_source_changed',
|
|
17
|
+
'batch_source_path_invalid',
|
|
18
|
+
'batch_command_conflict',
|
|
19
|
+
'batch_snapshot_mismatch',
|
|
20
|
+
'batch_commit_mismatch',
|
|
21
|
+
'workspace_not_registered',
|
|
22
|
+
'disk_usage_unavailable',
|
|
23
|
+
'batch_object_key_invalid',
|
|
13
24
|
]);
|
|
14
|
-
async function readBody(request) {
|
|
25
|
+
async function readBody(request, maxBytes = 1024 * 1024) {
|
|
15
26
|
const chunks = [];
|
|
16
27
|
let size = 0;
|
|
17
28
|
for await (const chunk of request) {
|
|
18
29
|
size += chunk.length;
|
|
19
|
-
if (size >
|
|
30
|
+
if (size > maxBytes)
|
|
20
31
|
throw new Error('request_body_too_large');
|
|
21
32
|
chunks.push(Buffer.from(chunk));
|
|
22
33
|
}
|
|
@@ -30,7 +41,9 @@ export async function dispatchBatchRoute(state, request, url) {
|
|
|
30
41
|
const member = /^\/runtime\/batch-members\/([^/]+)(?:\/(stop))?$/.exec(path);
|
|
31
42
|
if (method === 'GET' && member && !member[2]) {
|
|
32
43
|
const status = await readBatchMemberStatus(state, decodeURIComponent(member[1]));
|
|
33
|
-
return status
|
|
44
|
+
return status
|
|
45
|
+
? { status: 200, body: status }
|
|
46
|
+
: { status: 404, body: { error: 'batch_member_not_found' } };
|
|
34
47
|
}
|
|
35
48
|
if (method !== 'POST')
|
|
36
49
|
return undefined;
|
|
@@ -40,7 +53,7 @@ export async function dispatchBatchRoute(state, request, url) {
|
|
|
40
53
|
const disconnected = () => controller.abort();
|
|
41
54
|
request.socket.once('close', disconnected);
|
|
42
55
|
try {
|
|
43
|
-
const result = batchSourceFreezeResultSchema.parse(await freezeBatchSource(state, await readBody(request), { signal: controller.signal }));
|
|
56
|
+
const result = batchSourceFreezeResultSchema.parse(await freezeBatchSource(state, await readBody(request, WORKSPACE_REMIX_REFERENCE_BODY_MAX_BYTES), { signal: controller.signal }));
|
|
44
57
|
return { status: 200, body: result };
|
|
45
58
|
}
|
|
46
59
|
finally {
|
|
@@ -54,7 +67,10 @@ export async function dispatchBatchRoute(state, request, url) {
|
|
|
54
67
|
if (path === '/runtime/batch-members/deliver')
|
|
55
68
|
return { status: 200, body: await deliverBatchMember(state, await readBody(request)) };
|
|
56
69
|
if (member?.[2] === 'stop')
|
|
57
|
-
return {
|
|
70
|
+
return {
|
|
71
|
+
status: 200,
|
|
72
|
+
body: await stopBatchMember(state, decodeURIComponent(member[1]), await readBody(request)),
|
|
73
|
+
};
|
|
58
74
|
return undefined;
|
|
59
75
|
}
|
|
60
76
|
catch (error) {
|
|
@@ -63,7 +79,11 @@ export async function dispatchBatchRoute(state, request, url) {
|
|
|
63
79
|
const code = error instanceof Error && CLIENT_ERRORS.has(error.message) ? error.message : undefined;
|
|
64
80
|
if (!code)
|
|
65
81
|
throw error;
|
|
66
|
-
const status = code === 'occupancy_required' ||
|
|
82
|
+
const status = code === 'occupancy_required' ||
|
|
83
|
+
code === 'invalid_batch_request' ||
|
|
84
|
+
code === 'batch_branch_invalid'
|
|
85
|
+
? 400
|
|
86
|
+
: 409;
|
|
67
87
|
return { status, body: { error: code } };
|
|
68
88
|
}
|
|
69
89
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { link, mkdir, open, readFile, unlink } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { batchSourceSnapshotRefSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
4
|
+
import { batchSourceSnapshotRefSchema, normalizeLegacyBatchSourceSnapshot, } from '@playcraft/common/cloud-workspace/runtime';
|
|
5
5
|
import { publicationBinding } from './publication.js';
|
|
6
6
|
export function sourceCandidateFile(state, batchId) {
|
|
7
7
|
return join(state.config.stateDir, 'batch-source-candidates', `${batchId}.json`);
|
|
@@ -23,12 +23,16 @@ export async function readSourceCandidate(state, batchId) {
|
|
|
23
23
|
catch {
|
|
24
24
|
throw new Error('batch_source_snapshot_mismatch');
|
|
25
25
|
}
|
|
26
|
-
if (!stored ||
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
if (!stored ||
|
|
27
|
+
stored.version !== 1 ||
|
|
28
|
+
!['remote_ref', 'artifact'].includes(stored.persist) ||
|
|
29
|
+
(stored.repositoryUrl !== undefined && typeof stored.repositoryUrl !== 'string') ||
|
|
30
|
+
(stored.projectId !== undefined &&
|
|
31
|
+
stored.projectId !== null &&
|
|
32
|
+
typeof stored.projectId !== 'string') ||
|
|
33
|
+
(stored.branchName !== undefined && typeof stored.branchName !== 'string'))
|
|
30
34
|
throw new Error('batch_source_snapshot_mismatch');
|
|
31
|
-
const parsed = batchSourceSnapshotRefSchema.safeParse(stored.snapshot);
|
|
35
|
+
const parsed = batchSourceSnapshotRefSchema.safeParse(normalizeLegacyBatchSourceSnapshot(stored.snapshot));
|
|
32
36
|
if (!parsed.success)
|
|
33
37
|
throw new Error('batch_source_snapshot_mismatch');
|
|
34
38
|
const snapshot = parsed.data;
|
|
@@ -74,16 +78,25 @@ export async function writeSourceCandidate(state, candidate) {
|
|
|
74
78
|
}
|
|
75
79
|
export function assertSourceIdentity(state, request, snapshot, candidate) {
|
|
76
80
|
const binding = publicationBinding(state, request.workspaceId, request.sourceDirectoryName, request.publication);
|
|
77
|
-
const branchName = request.sourceDirectoryName === 'basic'
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
const branchName = request.sourceDirectoryName === 'basic'
|
|
82
|
+
? binding?.branchName
|
|
83
|
+
: binding?.directories?.find((directory) => directory.directoryName === request.sourceDirectoryName)?.branchName;
|
|
84
|
+
if (snapshot.batchId !== request.batchId ||
|
|
85
|
+
snapshot.sourceWorkspaceId !== request.workspaceId ||
|
|
86
|
+
snapshot.sourceDirectoryName !== request.sourceDirectoryName ||
|
|
87
|
+
(request.persist === 'remote_ref'
|
|
88
|
+
? !snapshot.remoteRef || Boolean(snapshot.artifactKey)
|
|
89
|
+
: !snapshot.artifactKey || Boolean(snapshot.remoteRef)) ||
|
|
90
|
+
(candidate &&
|
|
91
|
+
(candidate.persist !== request.persist ||
|
|
92
|
+
candidate.repositoryUrl !== (binding?.repositoryUrl ?? undefined) ||
|
|
93
|
+
candidate.projectId !== binding?.projectId ||
|
|
94
|
+
candidate.branchName !== (branchName ?? undefined) ||
|
|
95
|
+
candidate.snapshot.commitHash !== snapshot.commitHash ||
|
|
96
|
+
candidate.snapshot.digest !== snapshot.digest ||
|
|
97
|
+
candidate.snapshot.frozenAt !== snapshot.frozenAt ||
|
|
98
|
+
candidate.snapshot.remoteRef !== snapshot.remoteRef ||
|
|
99
|
+
candidate.snapshot.artifactKey !== snapshot.artifactKey))) {
|
|
87
100
|
throw new Error('batch_source_identity_mismatch');
|
|
88
101
|
}
|
|
89
102
|
}
|