@playcraft/cli 0.1.56-beta → 0.1.57-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/assets/client.js +6 -4
- package/dist/commands/tools-3d.js +15 -0
- package/dist/commands/tools-generation.js +3 -2
- package/dist/utils/agent-api-client.js +31 -8
- package/dist/utils/agent-generation-provenance.js +45 -0
- package/dist/utils/generation-workspace-scope.js +50 -0
- package/dist/utils/image-generation-retry.js +7 -0
- package/dist/utils/tool-operation-journal.js +34 -4
- package/dist/workspace-runtime/batch/prepare.js +9 -52
- package/dist/workspace-runtime/codex/delivery-tools.js +7 -5
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +80 -0
- package/dist/workspace-runtime/codex/launcher-client.js +18 -0
- package/dist/workspace-runtime/codex/model-broker.js +23 -4
- package/dist/workspace-runtime/serve.js +8 -30
- package/dist/workspace-runtime/workspaces/dependencies.js +24 -10
- package/dist/workspace-runtime/workspaces/platform-export.js +185 -0
- package/package.json +3 -3
- package/project-template-v2/.playcraft/skills.lock.json +15 -8
- package/templates/preview-runtime/v1/README.md +4 -0
- package/templates/preview-runtime/v1/manifest.json +6 -6
- package/templates/preview-runtime/v1/runtime.mjs +368 -22
- package/templates/preview-runtime/v1/types/adapters.d.ts +4 -1
- package/templates/preview-runtime/v1/types/preview.d.ts +1 -0
- package/templates/preview-runtime/v1/types/protocol.d.ts +19 -0
- package/templates/preview-runtime/v1/types/runtime.d.ts +2 -1
|
@@ -6,6 +6,7 @@ import { basename, dirname } from 'node:path';
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { workspaceContextFromRoot } from '../workspaces/registry.js';
|
|
8
8
|
import { PLATFORM_ROUTES, modelResponsesPath, platformEndpoint } from '@playcraft/common/cloud-workspace/runtime';
|
|
9
|
+
import { WorkspaceGenerationProvenanceBroker } from './generation-provenance-broker.js';
|
|
9
10
|
const credentialSchema = z.object({
|
|
10
11
|
token: z.string().min(1), expiresAt: z.string().datetime(),
|
|
11
12
|
configId: z.string().uuid(), model: z.string().min(1).max(128),
|
|
@@ -25,8 +26,10 @@ export class WorkspaceModelBroker {
|
|
|
25
26
|
pending = new Map();
|
|
26
27
|
requests = new Set();
|
|
27
28
|
sessionRequests = new Map();
|
|
29
|
+
generation;
|
|
28
30
|
constructor(state) {
|
|
29
31
|
this.state = state;
|
|
32
|
+
this.generation = new WorkspaceGenerationProvenanceBroker(state);
|
|
30
33
|
}
|
|
31
34
|
async start() {
|
|
32
35
|
if (this.server)
|
|
@@ -67,8 +70,10 @@ export class WorkspaceModelBroker {
|
|
|
67
70
|
}
|
|
68
71
|
// Resolve current model metadata and permission at every native turn boundary.
|
|
69
72
|
const credential = await this.credential(entry, true);
|
|
73
|
+
const boundaryId = sessionId && clientRequestId ? await this.generation.prepare() : undefined;
|
|
70
74
|
if (!route || route.epoch !== this.state.runtimeEpoch) {
|
|
71
|
-
route = { workspaceId, epoch: this.state.runtimeEpoch, capability: randomBytes(32).toString('base64url')
|
|
75
|
+
route = { workspaceId, epoch: this.state.runtimeEpoch, capability: randomBytes(32).toString('base64url'),
|
|
76
|
+
generationCapability: randomBytes(32).toString('base64url') };
|
|
72
77
|
this.routes.set(routeKey, route);
|
|
73
78
|
}
|
|
74
79
|
// Each session pins its prepared model; shared credential renewal cannot change an active turn.
|
|
@@ -77,12 +82,16 @@ export class WorkspaceModelBroker {
|
|
|
77
82
|
if (!this.valid(entry) || this.routes.get(routeKey) !== route)
|
|
78
83
|
throw new Error('model_access_denied');
|
|
79
84
|
route.selected = { entry, model: credential.model,
|
|
80
|
-
...(sessionId && clientRequestId ? { feedback: { sessionId, clientRequestId } } : {}) };
|
|
85
|
+
...(sessionId && clientRequestId ? { feedback: { sessionId, clientRequestId, cwd, boundaryId } } : {}) };
|
|
81
86
|
},
|
|
82
87
|
model: credential.model,
|
|
83
88
|
modelConfigId: credential.configId,
|
|
84
89
|
modelProvider: 'playcraft_workspace',
|
|
85
90
|
config: {
|
|
91
|
+
...(sessionId ? { 'shell_environment_policy.set': {
|
|
92
|
+
PLAYCRAFT_AGENT_GENERATION_ENDPOINT: `${this.baseUrl}/${routeKey}/generation-provenance`,
|
|
93
|
+
PLAYCRAFT_AGENT_GENERATION_CAPABILITY: route.generationCapability,
|
|
94
|
+
} } : {}),
|
|
86
95
|
'model_providers.playcraft_workspace': {
|
|
87
96
|
name: 'PlayCraft Workspace', base_url: `${this.baseUrl}/${routeKey}`,
|
|
88
97
|
wire_api: 'responses', requires_openai_auth: false,
|
|
@@ -142,18 +151,28 @@ export class WorkspaceModelBroker {
|
|
|
142
151
|
const cancel = () => abort.abort();
|
|
143
152
|
response.once('close', cancel);
|
|
144
153
|
try {
|
|
145
|
-
const match = /^\/([0-9a-f-]{36}(?:\/[0-9a-f]{32})?)\/responses$/.exec(request.url ?? '');
|
|
154
|
+
const match = /^\/([0-9a-f-]{36}(?:\/[0-9a-f]{32})?)\/(responses|generation-provenance)$/.exec(request.url ?? '');
|
|
146
155
|
const route = match ? this.routes.get(match[1]) : undefined;
|
|
147
156
|
const selected = route?.selected;
|
|
148
157
|
const entry = selected?.entry;
|
|
149
158
|
const presented = Buffer.from(request.headers.authorization ?? '');
|
|
150
|
-
const
|
|
159
|
+
const generationRequest = match?.[2] === 'generation-provenance';
|
|
160
|
+
const expected = Buffer.from(route ? `Bearer ${generationRequest ? route.generationCapability : route.capability}` : '');
|
|
151
161
|
if (request.method !== 'POST' || !entry || !expected.length || presented.length !== expected.length
|
|
152
162
|
|| !timingSafeEqual(presented, expected) || !this.valid(entry)) {
|
|
153
163
|
response.writeHead(403);
|
|
154
164
|
response.end();
|
|
155
165
|
return;
|
|
156
166
|
}
|
|
167
|
+
if (generationRequest) {
|
|
168
|
+
if (!selected?.feedback) {
|
|
169
|
+
response.writeHead(403);
|
|
170
|
+
response.end();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await this.generation.forward(request, response, selected.feedback, () => this.valid(entry) && route?.selected === selected);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
157
176
|
activeRoute = route;
|
|
158
177
|
const active = this.sessionRequests.get(route) ?? new Set();
|
|
159
178
|
active.add(abort);
|
|
@@ -2,7 +2,6 @@ import { closeCliCredentials } from './workspaces/cli-credentials.js';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir, readdir } from 'node:fs/promises';
|
|
4
4
|
import { createServer } from 'node:http';
|
|
5
|
-
import { createPlatformAdapter } from '@playcraft/build';
|
|
6
5
|
import { INCOMPATIBLE_EXIT_CODE, negotiateProtocol, transitionOperation, directoryMutationReceiptSchema, productionSessionAttemptId, } from '@playcraft/common/cloud-workspace/runtime';
|
|
7
6
|
import { RuntimeJournal } from './persistence/journal.js';
|
|
8
7
|
import { createCodexAppServerAdapter } from './codex/app-server.js';
|
|
@@ -32,7 +31,7 @@ import { revisionScope } from './workspaces/registry.js';
|
|
|
32
31
|
import { observeWorkspaceArtifacts } from './workspaces/artifact-events.js';
|
|
33
32
|
import { loadWorkspaceMcpConfig } from './codex/mcp-config.js';
|
|
34
33
|
import { startSourceEventUploader, stopSourceEventUploader } from './server/source-uploader.js';
|
|
35
|
-
import {
|
|
34
|
+
import { platformExportTarget, readPlatformExport, startPlatformExport } from './workspaces/platform-export.js';
|
|
36
35
|
export async function startWorkspaceRuntime(config, options = {}) {
|
|
37
36
|
process.env.PLAYCRAFT_SKIP_UPDATE_CHECK = '1';
|
|
38
37
|
const negotiated = negotiateProtocol(config);
|
|
@@ -355,6 +354,7 @@ function closeServer(server) {
|
|
|
355
354
|
function formatHost(host) {
|
|
356
355
|
return host.includes(':') ? `[${host}]` : host;
|
|
357
356
|
}
|
|
357
|
+
const UUID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
358
358
|
function deliveryToolHandlers(state) {
|
|
359
359
|
const contextFor = async (input) => {
|
|
360
360
|
if (input.workspaceId)
|
|
@@ -368,37 +368,15 @@ function deliveryToolHandlers(state) {
|
|
|
368
368
|
const { workspaceId, directoryName } = await contextFor(input);
|
|
369
369
|
if (directoryName === 'root')
|
|
370
370
|
throw new Error('invalid_workspace_directory');
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const operation = await submitTrustedOperation(state, {
|
|
374
|
-
kind: 'delivery.build',
|
|
375
|
-
workspaceId,
|
|
376
|
-
directoryName,
|
|
377
|
-
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
378
|
-
clientRequestId: `${input.clientRequestId.slice(0, 80)}:${channel}`,
|
|
379
|
-
payload: {
|
|
380
|
-
channel,
|
|
381
|
-
format: input.format ?? createPlatformAdapter({ platform: channel, format: 'html' }).getDefaultFormat(),
|
|
382
|
-
actor: 'agent',
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
let record;
|
|
386
|
-
for (let attempt = 0; attempt < 40 && !record; attempt++) {
|
|
387
|
-
record = await new DeliveryStore(state.config.stateDir, workspaceId).read(operation.operationId);
|
|
388
|
-
if (!record)
|
|
389
|
-
await new Promise(resolve => setTimeout(resolve, 25));
|
|
390
|
-
}
|
|
391
|
-
builds.push({
|
|
392
|
-
buildId: operation.operationId,
|
|
393
|
-
channel,
|
|
394
|
-
status: record?.status ?? operation.status,
|
|
395
|
-
...(record?.queuePosition ? { queuePosition: record.queuePosition } : {}),
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
return { builds };
|
|
371
|
+
// Exports go through the platform; a standalone workspace becomes a project first.
|
|
372
|
+
return startPlatformExport(state, { ...input, workspaceId, directoryName });
|
|
399
373
|
},
|
|
400
374
|
async get(input) {
|
|
401
375
|
const { workspaceId, directoryName } = await contextFor(input);
|
|
376
|
+
const sandboxBuild = input.buildId !== undefined && UUID.test(input.buildId);
|
|
377
|
+
if (!sandboxBuild && await platformExportTarget(state, { workspaceId, directoryName })) {
|
|
378
|
+
return readPlatformExport(state, { ...input, workspaceId, directoryName });
|
|
379
|
+
}
|
|
402
380
|
const store = new DeliveryStore(state.config.stateDir, workspaceId);
|
|
403
381
|
const record = input.buildId
|
|
404
382
|
? await store.read(input.buildId)
|
|
@@ -15,16 +15,21 @@ const SHARE_WAIT_MS = 60_000;
|
|
|
15
15
|
* prompts, and writes plain lines to the log. Package scripts run: the workspace is the user's own
|
|
16
16
|
* sandbox and the install runs as the development user.
|
|
17
17
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
export function dependencyInstallCommand(state, options = {}) {
|
|
19
|
+
const store = process.env.PLAYCRAFT_PNPM_STORE ?? (process.platform === 'linux' ? '/var/cache/playcraft/pnpm-store' : join(state.config.stateDir, 'pnpm-store'));
|
|
20
|
+
const quotedStore = `'${store.replaceAll("'", "'\\''")}'`;
|
|
21
|
+
return `pnpm install --${options.frozenLockfile ? 'frozen' : 'no-frozen'}-lockfile --store-dir ${quotedStore} --reporter=append-only --config.confirm-modules-purge=false`;
|
|
22
|
+
}
|
|
23
|
+
/** pnpm owns installation; other package managers' lockfiles remain untouched. */
|
|
20
24
|
export async function planInstall(root) {
|
|
21
25
|
const manifest = await readOptional(join(root, 'package.json'));
|
|
22
26
|
if (manifest === undefined)
|
|
23
27
|
return { kind: 'skip' };
|
|
24
|
-
const lock =
|
|
28
|
+
const lock = await readOptional(join(root, 'pnpm-lock.yaml'));
|
|
25
29
|
return {
|
|
26
30
|
kind: 'install',
|
|
27
|
-
lockHash: createHash('sha256').update(manifest).update('\0').update(lock).digest('hex'),
|
|
31
|
+
lockHash: createHash('sha256').update(manifest).update('\0').update(lock ?? '').digest('hex'),
|
|
32
|
+
hasLockfile: lock !== undefined,
|
|
28
33
|
};
|
|
29
34
|
}
|
|
30
35
|
const running = new WeakMap();
|
|
@@ -108,7 +113,16 @@ export async function recoverInterruptedInstalls(journal) {
|
|
|
108
113
|
}
|
|
109
114
|
}
|
|
110
115
|
async function installOnce(state, workspaceId, root) {
|
|
111
|
-
|
|
116
|
+
let plan;
|
|
117
|
+
try {
|
|
118
|
+
plan = await planInstall(root);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
await state.journal.putWorkspaceDependencies({
|
|
122
|
+
workspaceId, status: 'failed', error: 'dependency_inputs_unreadable', finishedAt: new Date().toISOString(),
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
112
126
|
const current = state.journal.getWorkspaceDependencies(workspaceId);
|
|
113
127
|
if (plan.kind === 'skip') {
|
|
114
128
|
if (current?.status !== 'none') {
|
|
@@ -174,9 +188,7 @@ async function installOnce(state, workspaceId, root) {
|
|
|
174
188
|
*/
|
|
175
189
|
async function runInstall(state, workspaceId, root) {
|
|
176
190
|
// Tests replace the install; a real registry is never reached from them.
|
|
177
|
-
const
|
|
178
|
-
const quotedStore = `'${store.replaceAll("'", "'\\''")}'`;
|
|
179
|
-
const planned = process.env.PLAYCRAFT_DEPENDENCY_INSTALL_COMMAND || INSTALL_COMMAND.replace('/var/cache/playcraft/pnpm-store', quotedStore);
|
|
191
|
+
const planned = process.env.PLAYCRAFT_DEPENDENCY_INSTALL_COMMAND || dependencyInstallCommand(state);
|
|
180
192
|
const logDir = join(state.config.stateDir, 'dependency-logs');
|
|
181
193
|
await mkdir(logDir, { recursive: true, mode: 0o700 });
|
|
182
194
|
const logPath = join(logDir, `${workspaceId}.log`);
|
|
@@ -265,8 +277,10 @@ async function readOptional(path) {
|
|
|
265
277
|
try {
|
|
266
278
|
return await readFile(path, 'utf8');
|
|
267
279
|
}
|
|
268
|
-
catch {
|
|
269
|
-
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (error.code === 'ENOENT')
|
|
282
|
+
return undefined;
|
|
283
|
+
throw error;
|
|
270
284
|
}
|
|
271
285
|
}
|
|
272
286
|
async function isDirectory(path) {
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { buildStageAt } from '@playcraft/common/build-progress';
|
|
7
|
+
import { PLATFORM_ROUTES, platformEndpoint } from '@playcraft/common/cloud-workspace/runtime';
|
|
8
|
+
import { submitTrustedOperation } from '../server/submit-operation.js';
|
|
9
|
+
import { businessEndpoint } from '../../utils/environment-endpoints.js';
|
|
10
|
+
import { ensureCliCredentials } from './cli-credentials.js';
|
|
11
|
+
import { readWorkspaceGitStatus } from './git.js';
|
|
12
|
+
import { sessionWorkspaceRoot } from './registry.js';
|
|
13
|
+
const run = promisify(execFile);
|
|
14
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
15
|
+
/** Validation findings beyond this are summarized by count; the Agent fixes the first ones first. */
|
|
16
|
+
const MAX_FINDINGS = 30;
|
|
17
|
+
/** A tool error the Agent sees as its reason; `code` is stable, the detail explains it. */
|
|
18
|
+
export class PlatformExportError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code, detail) {
|
|
21
|
+
super(detail ? `${code}: ${detail}` : code);
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The project and branch a workspace directory exports, or undefined when it has no project. */
|
|
26
|
+
export async function platformExportTarget(state, scope) {
|
|
27
|
+
const binding = state.journal.getWorkspace(scope.workspaceId);
|
|
28
|
+
if (!binding?.projectId)
|
|
29
|
+
return undefined;
|
|
30
|
+
const root = await sessionWorkspaceRoot(state, scope.workspaceId, scope.directoryName);
|
|
31
|
+
return { projectId: binding.projectId, root };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A standalone workspace has no remote to export from. The platform gives it a project (as its
|
|
35
|
+
* first save does) and registers it again; the registration reaches this runtime before the reply.
|
|
36
|
+
*/
|
|
37
|
+
async function promoteStandalone(state, scope) {
|
|
38
|
+
if (state.journal.getWorkspace(scope.workspaceId)?.targetKind !== 'standalone' || scope.directoryName !== 'basic')
|
|
39
|
+
return;
|
|
40
|
+
const config = state.config;
|
|
41
|
+
const response = await fetch(platformEndpoint(config.publicBaseUrl, PLATFORM_ROUTES.workspacePromotions), {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
redirect: 'error',
|
|
44
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
45
|
+
headers: { authorization: `Bearer ${config.controlToken}`, 'content-type': 'application/json', 'playcraft-env': config.environment },
|
|
46
|
+
body: JSON.stringify({ runtimeId: config.runtimeId, generation: config.generation, runtimeEpoch: state.runtimeEpoch,
|
|
47
|
+
workspaceId: scope.workspaceId }),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const body = await response.json().catch(() => undefined);
|
|
51
|
+
throw new PlatformExportError('workspace_promotion_failed', typeof body?.reason === 'string' ? body.reason : String(response.status));
|
|
52
|
+
}
|
|
53
|
+
await response.body?.cancel().catch(() => undefined);
|
|
54
|
+
}
|
|
55
|
+
const credentialSchema = z.object({ apiUrl: z.string().url(), token: z.string().min(1) });
|
|
56
|
+
async function request(state, path, init) {
|
|
57
|
+
await ensureCliCredentials(state).catch(() => { throw new PlatformExportError('cli_credentials_unavailable'); });
|
|
58
|
+
const credential = credentialSchema.parse(JSON.parse(await readFile(join(state.config.workspaceRoot, '.playcraft.json'), 'utf8')));
|
|
59
|
+
const response = await fetch(businessEndpoint(credential.apiUrl, path), {
|
|
60
|
+
method: init.method,
|
|
61
|
+
redirect: 'error',
|
|
62
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
63
|
+
headers: {
|
|
64
|
+
'X-Sandbox-Token': credential.token,
|
|
65
|
+
'playcraft-env': state.config.environment,
|
|
66
|
+
...(init.body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
67
|
+
},
|
|
68
|
+
...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
|
|
69
|
+
});
|
|
70
|
+
const text = await response.text();
|
|
71
|
+
let body;
|
|
72
|
+
try {
|
|
73
|
+
body = JSON.parse(text);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
body = undefined;
|
|
77
|
+
}
|
|
78
|
+
const error = body && typeof body === 'object' && 'error' in body ? body.error : undefined;
|
|
79
|
+
if (!response.ok || (typeof error === 'string' && !('id' in body))) {
|
|
80
|
+
const message = body && typeof body === 'object' && 'message' in body ? body.message : error;
|
|
81
|
+
throw new PlatformExportError('platform_export_rejected', `${response.status} ${typeof message === 'string' ? message : Array.isArray(message) ? message.join('; ') : text.slice(0, 300)}`);
|
|
82
|
+
}
|
|
83
|
+
return body;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Saves the directory (commit + push), then exports that commit for the channels through the
|
|
87
|
+
* platform, as the delivery panel's Export does. Each channel repeats its latest export of the
|
|
88
|
+
* branch, or uses the defaults without one. A standalone workspace first becomes a project.
|
|
89
|
+
*/
|
|
90
|
+
export async function startPlatformExport(state, input) {
|
|
91
|
+
await promoteStandalone(state, input);
|
|
92
|
+
const target = await platformExportTarget(state, input);
|
|
93
|
+
if (!target)
|
|
94
|
+
throw new PlatformExportError('platform_export_requires_project');
|
|
95
|
+
const saved = await submitTrustedOperation(state, {
|
|
96
|
+
kind: 'workspace.save',
|
|
97
|
+
workspaceId: input.workspaceId,
|
|
98
|
+
directoryName: input.directoryName,
|
|
99
|
+
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
100
|
+
clientRequestId: `${input.clientRequestId.slice(0, 80)}:export-save`,
|
|
101
|
+
payload: {},
|
|
102
|
+
});
|
|
103
|
+
if (saved.status !== 'completed')
|
|
104
|
+
throw new PlatformExportError('workspace_save_failed', saved.error);
|
|
105
|
+
const status = await readWorkspaceGitStatus(target.root);
|
|
106
|
+
// A save skips the push during a merge; with an upstream, `ahead` says the push did not land.
|
|
107
|
+
if (status.merging || (status.hasUpstream && status.ahead > 0))
|
|
108
|
+
throw new PlatformExportError('workspace_not_pushed');
|
|
109
|
+
if (status.branch === 'HEAD')
|
|
110
|
+
throw new PlatformExportError('workspace_detached_head');
|
|
111
|
+
const commit = (await run('git', ['-C', target.root, 'rev-parse', 'HEAD'])).stdout.trim();
|
|
112
|
+
const result = await request(state, '/api/builds/agent-exports', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: {
|
|
115
|
+
projectId: target.projectId,
|
|
116
|
+
branchName: status.branch,
|
|
117
|
+
commitHash: commit,
|
|
118
|
+
platforms: input.channels,
|
|
119
|
+
...(input.format ? { format: input.format } : {}),
|
|
120
|
+
clientRequestId: input.clientRequestId,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
commit: commit.slice(0, 7),
|
|
125
|
+
branch: status.branch,
|
|
126
|
+
builds: result.builds.map((build) => ({
|
|
127
|
+
buildId: build.id,
|
|
128
|
+
channel: build.platform,
|
|
129
|
+
status: build.status,
|
|
130
|
+
...(build.format ? { format: build.format } : {}),
|
|
131
|
+
...(build.error ? { error: build.error } : {}),
|
|
132
|
+
...(result.options?.[build.platform] ? { options: result.options[build.platform] } : {}),
|
|
133
|
+
})),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** One platform export as the Agent reads it: status, the recorded failure and the checks. */
|
|
137
|
+
export async function readPlatformExport(state, input) {
|
|
138
|
+
const target = await platformExportTarget(state, input);
|
|
139
|
+
if (!target)
|
|
140
|
+
throw new PlatformExportError('platform_export_requires_project');
|
|
141
|
+
let buildId = input.buildId;
|
|
142
|
+
if (!buildId) {
|
|
143
|
+
const { branch } = await readWorkspaceGitStatus(target.root);
|
|
144
|
+
const query = new URLSearchParams({ projectId: target.projectId, branchName: branch, limit: '1',
|
|
145
|
+
...(input.channel ? { platform: input.channel } : {}) });
|
|
146
|
+
const list = await request(state, `/api/agent/tools/list-builds?${query}`, { method: 'GET' });
|
|
147
|
+
buildId = list.items[0]?.id;
|
|
148
|
+
if (!buildId)
|
|
149
|
+
throw new PlatformExportError('delivery_build_not_found');
|
|
150
|
+
}
|
|
151
|
+
const build = await request(state, `/api/agent/tools/get-build?${new URLSearchParams({ id: buildId })}`, { method: 'GET' });
|
|
152
|
+
const running = build.status === 'queued' || build.status === 'running';
|
|
153
|
+
const findings = (build.validationWarnings ?? []).map((warning) => ({
|
|
154
|
+
code: warning.code,
|
|
155
|
+
severity: warning.severity,
|
|
156
|
+
message: warning.message,
|
|
157
|
+
...(typeof warning.details?.status === 'string' ? { status: warning.details.status } : {}),
|
|
158
|
+
...(typeof warning.details?.recommendation === 'string' ? { recommendation: warning.details.recommendation } : {}),
|
|
159
|
+
...(typeof warning.details?.file === 'string' ? { file: warning.details.file } : {}),
|
|
160
|
+
...(typeof warning.details?.line === 'number' ? { line: warning.details.line } : {}),
|
|
161
|
+
}));
|
|
162
|
+
return {
|
|
163
|
+
build: {
|
|
164
|
+
buildId: build.id,
|
|
165
|
+
channel: build.platform,
|
|
166
|
+
format: build.format,
|
|
167
|
+
status: build.status,
|
|
168
|
+
...(running ? { stage: buildStageAt(build.progress).stage } : {}),
|
|
169
|
+
...(build.commitHash ? { commit: build.commitHash } : {}),
|
|
170
|
+
...(build.error ? { error: build.error } : {}),
|
|
171
|
+
...(build.previewUrl ? { previewUrl: build.previewUrl } : {}),
|
|
172
|
+
...(build.downloadUrl ? { downloadUrl: build.downloadUrl } : {}),
|
|
173
|
+
...(typeof build.outputSize === 'number' ? { sizeBytes: build.outputSize } : {}),
|
|
174
|
+
...(typeof build.sizeLimit === 'number' ? { sizeLimitBytes: build.sizeLimit } : {}),
|
|
175
|
+
},
|
|
176
|
+
...(build.artifacts?.failure ? { failure: build.artifacts.failure } : {}),
|
|
177
|
+
...(!running && build.validationPassed !== null && build.validationPassed !== undefined ? {
|
|
178
|
+
validation: {
|
|
179
|
+
passed: build.validationPassed,
|
|
180
|
+
findings: findings.slice(0, MAX_FINDINGS),
|
|
181
|
+
...(findings.length > MAX_FINDINGS ? { omitted: findings.length - MAX_FINDINGS } : {}),
|
|
182
|
+
},
|
|
183
|
+
} : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57-beta",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"@gltf-transform/core": "^4.3.0",
|
|
53
53
|
"@gltf-transform/extensions": "^4.3.0",
|
|
54
54
|
"@gltf-transform/functions": "^4.3.0",
|
|
55
|
-
"@playcraft/build": "0.1.
|
|
56
|
-
"@playcraft/common": "0.1.
|
|
55
|
+
"@playcraft/build": "0.1.57-beta",
|
|
56
|
+
"@playcraft/common": "0.1.57-beta",
|
|
57
57
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
58
58
|
"chokidar": "^4.0.3",
|
|
59
59
|
"commander": "^13.1.0",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"skillRef": "platform/playcraft-asset-management",
|
|
13
|
-
"version": "1.
|
|
14
|
-
"artifactHash": "
|
|
13
|
+
"version": "1.1.0",
|
|
14
|
+
"artifactHash": "0b5408792edfeb7b852817bd76893b95738a0e161407f0b48c4b50161797ba3c",
|
|
15
15
|
"surface": "portal-agent",
|
|
16
16
|
"mountPath": ".playcraft/skills/platform__playcraft-asset-management"
|
|
17
17
|
},
|
|
@@ -108,8 +108,8 @@
|
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
"skillRef": "remix/playcraft-audio-generation",
|
|
111
|
-
"version": "1.
|
|
112
|
-
"artifactHash": "
|
|
111
|
+
"version": "1.1.1",
|
|
112
|
+
"artifactHash": "27661b1b52e7981a0b9cf5531f535fcf10928ea290348fcb469e35ad2082b437",
|
|
113
113
|
"surface": "sandbox-agent",
|
|
114
114
|
"mountPath": ".playcraft/skills/remix__playcraft-audio-generation"
|
|
115
115
|
},
|
|
@@ -122,15 +122,15 @@
|
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
"skillRef": "remix/playcraft-image-generation",
|
|
125
|
-
"version": "1.
|
|
126
|
-
"artifactHash": "
|
|
125
|
+
"version": "1.1.2",
|
|
126
|
+
"artifactHash": "a006c96700f8dbfe4c34a31d0e1646620ca6148d319e59e7466d627fea1ca65e",
|
|
127
127
|
"surface": "sandbox-agent",
|
|
128
128
|
"mountPath": ".playcraft/skills/remix__playcraft-image-generation"
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
131
|
"skillRef": "remix/playcraft-image-processing",
|
|
132
|
-
"version": "1.
|
|
133
|
-
"artifactHash": "
|
|
132
|
+
"version": "1.1.1",
|
|
133
|
+
"artifactHash": "903f4efc14e2bb57403856edeca43146904ec64811cfd107b6ec76bb451842fd",
|
|
134
134
|
"surface": "sandbox-agent",
|
|
135
135
|
"mountPath": ".playcraft/skills/remix__playcraft-image-processing"
|
|
136
136
|
},
|
|
@@ -147,6 +147,13 @@
|
|
|
147
147
|
"artifactHash": "e2a59c61ef5b094a821fbbab3b0f65f906bfc0dfda6f3d8c0b33a6f460dd0963",
|
|
148
148
|
"surface": "sandbox-agent",
|
|
149
149
|
"mountPath": ".playcraft/skills/remix__playcraft-remix-workflow"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"skillRef": "remix/playcraft-workspace-assets",
|
|
153
|
+
"version": "1.0.0",
|
|
154
|
+
"artifactHash": "f098949b043a229e561d92a34c3a65b44ffbd83d1758ef30ac09d2d0d5ea2570",
|
|
155
|
+
"surface": "sandbox-agent",
|
|
156
|
+
"mountPath": ".playcraft/skills/remix__playcraft-workspace-assets"
|
|
150
157
|
}
|
|
151
158
|
]
|
|
152
159
|
}
|
|
@@ -22,6 +22,10 @@ Prepare all required images before changing visible objects, call `assertCurrent
|
|
|
22
22
|
|
|
23
23
|
`ResourceLoadError` is exported from `runtime.mjs`. Live adapters let this error reach the runtime, which retains the last known installation and reports a recoverable, field-level resource failure. Do not catch an arbitrary update exception and claim successful fallback. During initial `createGame`, catch only this load error to start with an embedded default image or empty background; the runtime still reports the saved image as unapplied. A later patch of that field retries it; unrelated parameter updates can continue. `resourceErrors` and file-save receipts are independent. Save logical paths and let the original production asset registry include the file when building delivery output.
|
|
24
24
|
|
|
25
|
+
The endpoint advertises `canRefreshImages: true` for explicit same-path content refresh. The host sends registered live-image fields and their expected file digests through a separate resource sequence; raw parameters and draft revisions remain unchanged. In `apply`, honor `context.fields` even when a logical path equals its previous value: request that field through `loadImage` and install the returned image after `assertCurrent()`. A local path-equality cache must not hide this explicit update. The runtime prepares new images without blocking unrelated live parameters, checks the expected content digest, rebases against the latest permitted configuration and records installed digests only after the real render acknowledgement. A matching installed digest is a no-op; a failed or superseded preparation leaves the old image available and can be retried. Older endpoints without this optional capability require the existing rebuild route.
|
|
26
|
+
|
|
27
|
+
Content generations are independent per field. If an opening image and board image are being prepared together, a newer board-only refresh cancels only the old board read; the still-current opening image must finish and apply while preserving the newer board. Each application uses only the prepared fields that remain current and the latest permitted configuration. A preparation failure in a still-current batch retains the old images and marks all its unapplied fields as recoverable, so an explicit retry cannot silently forget a prepared sibling.
|
|
28
|
+
|
|
25
29
|
## Preview build
|
|
26
30
|
|
|
27
31
|
The engineering project must install a compatible Vite as a development dependency. Set a separate package script:
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"protocolVersion": 1,
|
|
5
5
|
"files": {
|
|
6
6
|
"build-preview.mjs": "2bd512675d829cc07ceddcb58365a6f74d36ff93690f8d72f4bbaa1d45511c91",
|
|
7
|
-
"README.md": "
|
|
7
|
+
"README.md": "3e35ca2ce6eab84ba8688b400aee8ee5e69458466a13f00e459fd630575d9cbd",
|
|
8
8
|
"release-guard.mjs": "0c33d1559c68844f3760c3e754ee36ca70dc27151ef6f7ad62f29f95239679cc",
|
|
9
9
|
"runtime.d.mts": "a366552166a4a93103306997058951416740eaf6d6f3d0a34854386b4f1e02f5",
|
|
10
10
|
"runtime.d.ts": "a366552166a4a93103306997058951416740eaf6d6f3d0a34854386b4f1e02f5",
|
|
11
|
-
"runtime.mjs": "
|
|
12
|
-
"types/adapters.d.ts": "
|
|
13
|
-
"types/preview.d.ts": "
|
|
14
|
-
"types/protocol.d.ts": "
|
|
15
|
-
"types/runtime.d.ts": "
|
|
11
|
+
"runtime.mjs": "700c6cb0859ef4b4390d7a1dae41f7e8fa7be7c8dad90b5b48a7e7ee5f90a6a1",
|
|
12
|
+
"types/adapters.d.ts": "f5e70c4bb9043de1c590eada14266f613286360c809faf26680b0bbbd1cc7b2c",
|
|
13
|
+
"types/preview.d.ts": "db2e7eb1fa3406ca53fc7c3a4352da061c05af714ead978111ab5118de58db32",
|
|
14
|
+
"types/protocol.d.ts": "b298d1d35d6e261bbf6a0500f9029f38cad1532bb46475566180b3b92833c507",
|
|
15
|
+
"types/runtime.d.ts": "f52d08d7b53ff84394381f9d1741a624142f75fac468abf9a5029e98fe172ca4"
|
|
16
16
|
}
|
|
17
17
|
}
|