@playcraft/cli 0.1.72 → 0.1.73

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.
@@ -109,6 +109,9 @@ export class RuntimeJournal {
109
109
  getWorkspace(workspaceId) {
110
110
  return this.snapshot.workspaces[workspaceId];
111
111
  }
112
+ listWorkspaces() {
113
+ return Object.values(this.snapshot.workspaces);
114
+ }
112
115
  async putWorkspace(binding) {
113
116
  const parsed = workspaceRegistrationSchema.parse(binding);
114
117
  await this.run('upsertRecord', { bucket: 'workspaces', key: parsed.workspaceId, body: parsed });
@@ -33,7 +33,7 @@ import { revisionScope } from './workspaces/registry.js';
33
33
  import { observeWorkspaceArtifacts } from './workspaces/artifact-events.js';
34
34
  import { loadWorkspaceMcpConfig } from './codex/mcp-config.js';
35
35
  import { startSourceEventUploader, stopSourceEventUploader } from './server/source-uploader.js';
36
- import { platformExportTarget, readPlatformExport, startPlatformExport } from './workspaces/platform-export.js';
36
+ import { platformExportTarget, readPlatformExport, startPlatformExport, syncWorkspaceBindings } from './workspaces/platform-export.js';
37
37
  import { scheduledTaskProposalHandler } from './workspaces/scheduled-task-proposals.js';
38
38
  import { executionContextToolHandler } from './workspaces/execution-context.js';
39
39
  import { registerLevelEditorRequest, startLevelEditorRequestQueue, stopLevelEditorRequestQueue } from './workspaces/level-editor-requests.js';
@@ -352,6 +352,8 @@ export async function startWorkspaceRuntime(config, options = {}) {
352
352
  const port = await listenOn(server, config.listen.host, config.listen.port);
353
353
  startLevelEditorRequestQueue(state);
354
354
  startSkillActivationRequestQueue(state);
355
+ // After listening: the platform registers a missed binding through this server before it replies.
356
+ void syncWorkspaceBindings(state);
355
357
  return {
356
358
  config: state.config,
357
359
  state,
@@ -42,7 +42,7 @@ import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
42
42
  import { WorkspaceArtifactStore } from '../workspaces/artifacts.js';
43
43
  import { OperationLogStore } from '../workspaces/operation-log.js';
44
44
  import { acceptOperationCore, observeLevelEditorTransaction } from './accept-operation.js';
45
- import { workspaceArtifactContentQuerySchema } from '@playcraft/common/cloud-workspace/runtime';
45
+ import { workspaceArtifactContentQuerySchema, workspaceBindingIdentity } from '@playcraft/common/cloud-workspace/runtime';
46
46
  import { searchWorkspaceFiles } from '../workspaces/files-search.js';
47
47
  import { serveWorkspaceArchive } from '../workspaces/archive.js';
48
48
  import { verifyRuntimeAssetProvenance } from '../workspaces/asset-provenance-verifier.js';
@@ -251,6 +251,8 @@ async function dispatchRuntimeRequest(state, request, response) {
251
251
  ...(state.native?.startPrediction ? { predictionVersion: 1 } : {}),
252
252
  sourceReadToken: state.journal.sourceReadToken(),
253
253
  workspaceMode: state.config.workspaceMode,
254
+ // Runtime compares these with Business and registers again a binding this sandbox missed.
255
+ workspaceBindings: state.journal.listWorkspaces().map(workspaceBindingIdentity),
254
256
  executionGroup: {
255
257
  replaced: state.journal.executionGroup().replaced,
256
258
  stopConfirmed: state.journal.executionGroup().stopConfirmed,
@@ -31,12 +31,11 @@ export async function platformExportTarget(state, scope) {
31
31
  return { projectId: binding.projectId, root };
32
32
  }
33
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.
34
+ * Asks the platform about this workspace's binding. `promote` gives a standalone workspace a
35
+ * project (as its first save does); both register the binding Business holds again when this
36
+ * runtime lacks it. The registration reaches this runtime before the reply.
36
37
  */
37
- async function promoteStandalone(state, scope) {
38
- if (state.journal.getWorkspace(scope.workspaceId)?.targetKind !== 'standalone' || scope.directoryName !== 'basic')
39
- return;
38
+ async function requestBinding(state, workspaceId, intent) {
40
39
  const config = state.config;
41
40
  const response = await fetch(platformEndpoint(config.publicBaseUrl, PLATFORM_ROUTES.workspacePromotions), {
42
41
  method: 'POST',
@@ -44,7 +43,7 @@ async function promoteStandalone(state, scope) {
44
43
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
45
44
  headers: { authorization: `Bearer ${config.controlToken}`, 'content-type': 'application/json', 'playcraft-env': config.environment },
46
45
  body: JSON.stringify({ runtimeId: config.runtimeId, generation: config.generation, runtimeEpoch: state.runtimeEpoch,
47
- workspaceId: scope.workspaceId }),
46
+ workspaceId, ...(intent === 'sync' ? { intent } : {}) }),
48
47
  });
49
48
  if (!response.ok) {
50
49
  const body = await response.json().catch(() => undefined);
@@ -52,6 +51,28 @@ async function promoteStandalone(state, scope) {
52
51
  }
53
52
  await response.body?.cancel().catch(() => undefined);
54
53
  }
54
+ /**
55
+ * Basic needs a project to export from. Without one in the journal, the workspace is either still
56
+ * standalone or holds a binding whose registration never arrived; the platform settles both.
57
+ */
58
+ async function ensureProjectBinding(state, scope) {
59
+ if (scope.directoryName !== 'basic' || state.journal.getWorkspace(scope.workspaceId)?.projectId)
60
+ return;
61
+ await requestBinding(state, scope.workspaceId, 'promote');
62
+ }
63
+ /**
64
+ * On start, asks that each binding this runtime holds still match Business, so one whose
65
+ * registration never arrived is registered again before anything needs it. Best effort.
66
+ */
67
+ export async function syncWorkspaceBindings(state) {
68
+ if (state.config.workspaceMode !== 'multi' || !state.config.publicBaseUrl || !state.config.controlToken)
69
+ return;
70
+ for (const binding of state.journal.listWorkspaces()) {
71
+ await requestBinding(state, binding.workspaceId, 'sync').catch((error) => {
72
+ console.warn('[workspace-runtime] binding sync failed:', error instanceof Error ? error.message : error);
73
+ });
74
+ }
75
+ }
55
76
  const credentialSchema = z.object({ apiUrl: z.string().url(), token: z.string().min(1) });
56
77
  async function request(state, path, init) {
57
78
  await ensureCliCredentials(state).catch(() => { throw new PlatformExportError('cli_credentials_unavailable'); });
@@ -86,13 +107,16 @@ async function request(state, path, init) {
86
107
  * Saves the directory (commit + push), then exports that commit for the channels through the
87
108
  * platform, as the delivery panel's Export does. Each channel uses that panel's fixed HTML or
88
109
  * ZIP choice. Other options repeat the channel's latest export of the branch, or stay empty
89
- * without one. A standalone workspace first becomes a project.
110
+ * without one. A standalone workspace first becomes a project, and a binding this runtime missed
111
+ * is registered again.
90
112
  */
91
113
  export async function startPlatformExport(state, input) {
92
- await promoteStandalone(state, input);
114
+ await ensureProjectBinding(state, input);
93
115
  const target = await platformExportTarget(state, input);
116
+ // The platform replied that the workspace has a project, but this runtime still holds none.
94
117
  if (!target)
95
- throw new PlatformExportError('platform_export_requires_project');
118
+ throw new PlatformExportError(input.directoryName === 'basic'
119
+ ? 'workspace_registration_not_delivered' : 'platform_export_requires_project');
96
120
  const saved = await submitTrustedOperation(state, {
97
121
  kind: 'workspace.save',
98
122
  workspaceId: input.workspaceId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.1.72",
3
+ "version": "0.1.73",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -57,8 +57,8 @@
57
57
  "@gltf-transform/core": "^4.3.0",
58
58
  "@gltf-transform/extensions": "^4.3.0",
59
59
  "@gltf-transform/functions": "^4.3.0",
60
- "@playcraft/build": "0.1.72",
61
- "@playcraft/common": "0.1.72",
60
+ "@playcraft/build": "0.1.73",
61
+ "@playcraft/common": "0.1.73",
62
62
  "better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
63
63
  "chokidar": "^4.0.3",
64
64
  "commander": "^13.1.0",