@playdrop/playdrop-cli 0.3.8-build.2 → 0.3.9

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 (106) hide show
  1. package/config/client-meta.json +5 -5
  2. package/dist/apps/build.js +45 -39
  3. package/dist/catalogue-utils.js +23 -18
  4. package/dist/catalogue.d.ts +0 -2
  5. package/dist/catalogue.js +54 -41
  6. package/dist/clientInfo.js +16 -2
  7. package/dist/commands/browse.js +66 -23
  8. package/dist/commands/capture.js +3 -2
  9. package/dist/commands/create.js +49 -46
  10. package/dist/commands/createRemixContent.js +29 -44
  11. package/dist/commands/creations.js +51 -13
  12. package/dist/commands/devServer.d.ts +1 -1
  13. package/dist/commands/devShared.js +1 -1
  14. package/dist/commands/generation.js +91 -74
  15. package/dist/commands/gettingStarted.js +1 -1
  16. package/dist/commands/search.js +29 -1
  17. package/dist/commands/upload-content.d.ts +70 -0
  18. package/dist/commands/upload-content.js +627 -0
  19. package/dist/commands/upload-graph.d.ts +23 -0
  20. package/dist/commands/upload-graph.js +108 -0
  21. package/dist/commands/upload.js +264 -543
  22. package/dist/http.d.ts +1 -1
  23. package/dist/playwright.d.ts +12 -4
  24. package/dist/proxyFetch.js +3 -2
  25. package/node_modules/@playdrop/ai-client/dist/index.d.ts.map +1 -1
  26. package/node_modules/@playdrop/ai-client/dist/index.js +74 -54
  27. package/node_modules/@playdrop/api-client/dist/client.d.ts +34 -14
  28. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  29. package/node_modules/@playdrop/api-client/dist/client.js +6 -8
  30. package/node_modules/@playdrop/api-client/dist/core/errors.js +11 -11
  31. package/node_modules/@playdrop/api-client/dist/core/request.d.ts +2 -0
  32. package/node_modules/@playdrop/api-client/dist/core/request.d.ts.map +1 -1
  33. package/node_modules/@playdrop/api-client/dist/core/request.js +10 -3
  34. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +12 -10
  35. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  36. package/node_modules/@playdrop/api-client/dist/domains/admin.js +33 -30
  37. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +8 -1
  38. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  39. package/node_modules/@playdrop/api-client/dist/domains/apps.js +140 -124
  40. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts +9 -5
  41. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts.map +1 -1
  42. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.js +151 -88
  43. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts +4 -1
  44. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts.map +1 -1
  45. package/node_modules/@playdrop/api-client/dist/domains/assets.js +153 -112
  46. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts +3 -1
  47. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts.map +1 -1
  48. package/node_modules/@playdrop/api-client/dist/domains/auth.js +21 -0
  49. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts +6 -2
  50. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts.map +1 -1
  51. package/node_modules/@playdrop/api-client/dist/domains/me.js +13 -2
  52. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +1 -0
  53. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  54. package/node_modules/@playdrop/api-client/dist/domains/payments.js +10 -0
  55. package/node_modules/@playdrop/api-client/dist/index.d.ts +65 -24
  56. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  57. package/node_modules/@playdrop/api-client/dist/index.js +38 -13
  58. package/node_modules/@playdrop/boxel-core/dist/src/entity-cleaner.js +2 -0
  59. package/node_modules/@playdrop/boxel-core/dist/src/entity-cleaner.js.map +1 -1
  60. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/textured-builder.js +1 -1
  61. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/textured-builder.js.map +1 -1
  62. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/voxel-builder.js +1 -1
  63. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/voxel-builder.js.map +1 -1
  64. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/builder.js +95 -75
  65. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/builder.js.map +1 -1
  66. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/scanner.d.ts +2 -3
  67. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/scanner.js.map +1 -1
  68. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/textures/face-map.js +4 -4
  69. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/textures/face-map.js.map +1 -1
  70. package/node_modules/@playdrop/boxel-core/dist/src/palette_tools.d.ts +2 -2
  71. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/slice.d.ts +5 -5
  72. package/node_modules/@playdrop/boxel-core/dist/src/transforms/voxels/textured-to-voxel.d.ts +3 -3
  73. package/node_modules/@playdrop/boxel-core/dist/src/types.d.ts +25 -25
  74. package/node_modules/@playdrop/boxel-core/dist/src/validation.js +2 -1
  75. package/node_modules/@playdrop/boxel-core/dist/src/validation.js.map +1 -1
  76. package/node_modules/@playdrop/boxel-three/dist/src/exporters/glb.js +5 -0
  77. package/node_modules/@playdrop/boxel-three/package.json +3 -3
  78. package/node_modules/@playdrop/config/client-meta.json +5 -5
  79. package/node_modules/@playdrop/config/dist/src/index.js +6 -6
  80. package/node_modules/@playdrop/config/dist/test/validateClientEnvironment.test.js +2 -2
  81. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  82. package/node_modules/@playdrop/types/dist/api.d.ts +33 -8
  83. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  84. package/node_modules/@playdrop/types/dist/api.js +16 -8
  85. package/node_modules/@playdrop/types/dist/asset-pack.d.ts +107 -11
  86. package/node_modules/@playdrop/types/dist/asset-pack.d.ts.map +1 -1
  87. package/node_modules/@playdrop/types/dist/asset-pack.js +2 -0
  88. package/node_modules/@playdrop/types/dist/asset.d.ts +15 -0
  89. package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
  90. package/node_modules/@playdrop/types/dist/asset.js +1 -0
  91. package/node_modules/@playdrop/types/dist/ecs.d.ts.map +1 -1
  92. package/node_modules/@playdrop/types/dist/ecs.js +10 -6
  93. package/node_modules/@playdrop/types/dist/entity.d.ts +5 -10
  94. package/node_modules/@playdrop/types/dist/entity.d.ts.map +1 -1
  95. package/node_modules/@playdrop/types/dist/entity.js +40 -23
  96. package/node_modules/@playdrop/types/dist/graph.d.ts.map +1 -1
  97. package/node_modules/@playdrop/types/dist/graph.js +13 -5
  98. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  99. package/node_modules/@playdrop/types/dist/version.js +7 -3
  100. package/node_modules/@playdrop/vox-three/dist/src/vox.d.ts +1 -0
  101. package/node_modules/@playdrop/vox-three/dist/src/vox.js +15 -6
  102. package/node_modules/@playdrop/vox-three/dist/src/vox.js.map +1 -1
  103. package/node_modules/@playdrop/vox-three/dist/test/vox.test.js +16 -0
  104. package/node_modules/@playdrop/vox-three/dist/test/vox.test.js.map +1 -1
  105. package/node_modules/@playdrop/vox-three/package.json +3 -3
  106. package/package.json +1 -1
@@ -1,16 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.upload = upload;
4
- const node_fs_1 = require("node:fs");
5
- const node_path_1 = require("node:path");
6
4
  const types_1 = require("@playdrop/types");
7
- const http_1 = require("../http");
8
- const commandContext_1 = require("../commandContext");
9
- const uploadLog_1 = require("../uploadLog");
10
5
  const apps_1 = require("../apps");
6
+ const commandContext_1 = require("../commandContext");
7
+ const http_1 = require("../http");
11
8
  const taskSelection_1 = require("../taskSelection");
12
9
  const taskUtils_1 = require("../taskUtils");
13
- const model_artifacts_1 = require("../assets/model-artifacts");
10
+ const uploadLog_1 = require("../uploadLog");
11
+ const upload_content_1 = require("./upload-content");
12
+ const upload_graph_1 = require("./upload-graph");
14
13
  const DEFAULT_PORTAL_BASE = 'https://www.playdrop.ai';
15
14
  const APP_TYPE_TO_SLUG = {
16
15
  GAME: 'game',
@@ -18,39 +17,20 @@ const APP_TYPE_TO_SLUG = {
18
17
  TOOL: 'tool',
19
18
  TEMPLATE: 'template',
20
19
  };
21
- const EXTENSION_TO_FORMAT = {
22
- '.png': 'PNG',
23
- '.jpg': 'JPEG',
24
- '.jpeg': 'JPEG',
25
- '.webp': 'WEBP',
26
- '.gif': 'GIF',
27
- '.aseprite': 'ASEPRITE',
28
- '.mp4': 'MP4',
29
- '.webm': 'WEBM',
30
- '.mp3': 'MP3',
31
- '.wav': 'WAV',
32
- '.ogg': 'OGG',
33
- '.vox': 'VOX',
34
- '.gltf': 'GLTF',
35
- '.glb': 'GLB',
36
- };
37
- const EXTENSION_TO_MIME = {
38
- '.png': 'image/png',
39
- '.jpg': 'image/jpeg',
40
- '.jpeg': 'image/jpeg',
41
- '.webp': 'image/webp',
42
- '.gif': 'image/gif',
43
- '.aseprite': 'application/octet-stream',
44
- '.mp4': 'video/mp4',
45
- '.webm': 'video/webm',
46
- '.mp3': 'audio/mpeg',
47
- '.wav': 'audio/wav',
48
- '.ogg': 'audio/ogg',
49
- '.vox': 'application/octet-stream',
50
- '.gltf': 'model/gltf+json',
51
- '.glb': 'model/gltf-binary',
52
- '.json': 'application/json',
53
- };
20
+ function buildApiUnavailableMessage(apiBase) {
21
+ if (apiBase && typeof apiBase === 'string') {
22
+ return `Could not reach the Playdrop API at ${apiBase}. Check your internet connection, then ensure the server is running before retrying.`;
23
+ }
24
+ return 'Could not reach the Playdrop API. Check your internet connection, then ensure the server is running before retrying.';
25
+ }
26
+ function isConnectionRefusedError(error) {
27
+ const networkCode = error?.cause?.code;
28
+ if (error instanceof TypeError || networkCode === 'ECONNREFUSED') {
29
+ return true;
30
+ }
31
+ const aggregateErrors = error?.cause?.errors;
32
+ return Array.isArray(aggregateErrors) && aggregateErrors.some((entry) => entry?.code === 'ECONNREFUSED');
33
+ }
54
34
  async function fetchCurrentUserInfo(client, apiBase) {
55
35
  try {
56
36
  const data = await client.me();
@@ -70,16 +50,8 @@ async function fetchCurrentUserInfo(client, apiBase) {
70
50
  if (error instanceof types_1.ApiError) {
71
51
  return { username: null, role: null };
72
52
  }
73
- const message = apiBase && typeof apiBase === 'string'
74
- ? `Could not reach the Playdrop API at ${apiBase}. Check your internet connection, then ensure the server is running before retrying.`
75
- : 'Could not reach the Playdrop API. Check your internet connection, then ensure the server is running before retrying.';
76
- const networkCode = error?.cause?.code;
77
- const aggregateErrors = error?.cause?.errors;
78
- if (error instanceof TypeError || networkCode === 'ECONNREFUSED') {
79
- throw new Error(message);
80
- }
81
- if (Array.isArray(aggregateErrors) && aggregateErrors.some((entry) => entry?.code === 'ECONNREFUSED')) {
82
- throw new Error(message);
53
+ if (isConnectionRefusedError(error)) {
54
+ throw new Error(buildApiUnavailableMessage(apiBase));
83
55
  }
84
56
  throw error;
85
57
  }
@@ -132,548 +104,297 @@ function appendOverviewLink(entry, task, portalBase, creator) {
132
104
  }
133
105
  entry.detail = entry.detail ? `${entry.detail} | view: ${url}` : `view: ${url}`;
134
106
  }
135
- function createFileFromPath(filePath) {
136
- const extension = (0, node_path_1.extname)(filePath).toLowerCase();
137
- const mimeType = EXTENSION_TO_MIME[extension] ?? 'application/octet-stream';
138
- const data = (0, node_fs_1.readFileSync)(filePath);
139
- return new File([data], (0, node_path_1.basename)(filePath), { type: mimeType });
107
+ function buildTaskEntityId(task, creatorUsername) {
108
+ return task.kind === 'embedded-asset'
109
+ ? `${creatorUsername}/${task.appName}:${task.name}`
110
+ : `${creatorUsername}/${task.name}`;
140
111
  }
141
- function isPngSignature(buffer) {
142
- if (buffer.length < 8) {
143
- return false;
144
- }
145
- return (buffer[0] === 0x89
146
- && buffer[1] === 0x50
147
- && buffer[2] === 0x4e
148
- && buffer[3] === 0x47
149
- && buffer[4] === 0x0d
150
- && buffer[5] === 0x0a
151
- && buffer[6] === 0x1a
152
- && buffer[7] === 0x0a);
153
- }
154
- function isMp4Signature(buffer) {
155
- if (buffer.length < 12) {
156
- return false;
157
- }
158
- return buffer.toString('ascii', 4, 8) === 'ftyp';
159
- }
160
- function createListingFileFromPath(filePath, expectedMime) {
161
- const data = (0, node_fs_1.readFileSync)(filePath);
162
- if (expectedMime === 'image/png' && !isPngSignature(data)) {
163
- throw new Error(`Invalid listing file signature for ${filePath}. Expected image/png.`);
164
- }
165
- if (expectedMime === 'video/mp4' && !isMp4Signature(data)) {
166
- throw new Error(`Invalid listing file signature for ${filePath}. Expected video/mp4.`);
167
- }
168
- return new File([data], (0, node_path_1.basename)(filePath), { type: expectedMime });
169
- }
170
- function resolveAssetFormat(task) {
171
- if (typeof task.format === 'string' && task.format.trim().length > 0) {
172
- return task.format.trim().toUpperCase();
173
- }
174
- if (task.category === 'MODEL_3D') {
175
- const primaryFile = task.filePaths.primary;
176
- if (typeof primaryFile === 'string') {
177
- const normalizedPrimary = primaryFile.trim().toLowerCase();
178
- if (normalizedPrimary.endsWith('.boxel.json')) {
179
- return 'PLAYDROP_BOXEL_JSON';
180
- }
181
- if (normalizedPrimary.endsWith('.vox')) {
182
- return 'VOX';
183
- }
184
- const primaryExtension = (0, node_path_1.extname)(normalizedPrimary);
185
- if (primaryExtension === '.glb') {
186
- return 'GLB';
187
- }
188
- if (primaryExtension === '.gltf') {
189
- return 'GLTF';
190
- }
191
- }
192
- const boxelFile = task.filePaths.boxel;
193
- if (typeof boxelFile === 'string' && boxelFile.trim().toLowerCase().endsWith('.boxel.json')) {
194
- return 'PLAYDROP_BOXEL_JSON';
195
- }
196
- const meshFile = task.filePaths.mesh;
197
- if (typeof meshFile === 'string') {
198
- const meshExtension = (0, node_path_1.extname)(meshFile).toLowerCase();
199
- if (meshExtension === '.glb') {
200
- return 'GLB';
201
- }
202
- if (meshExtension === '.gltf') {
203
- return 'GLTF';
204
- }
205
- }
206
- throw new Error(`Asset "${task.name}" is missing files.primary or uses an unsupported MODEL_3D primary extension.`);
207
- }
208
- const filePaths = Object.values(task.filePaths);
209
- if (filePaths.length === 0) {
210
- throw new Error(`Asset "${task.name}" has no files.`);
211
- }
212
- if (filePaths.length > 1) {
213
- throw new Error(`Asset "${task.name}" is missing format and has multiple files. Set "format" explicitly in catalogue.json.`);
214
- }
215
- const extension = (0, node_path_1.extname)(filePaths[0]).toLowerCase();
216
- const inferred = EXTENSION_TO_FORMAT[extension];
217
- if (!inferred) {
218
- throw new Error(`Asset "${task.name}" uses unsupported file extension "${extension}". Set "format" explicitly.`);
219
- }
220
- return inferred;
112
+ function pushLoggedEntry(results, entry) {
113
+ results.push(entry);
114
+ console.log((0, uploadLog_1.formatTaskLogLine)(entry));
221
115
  }
222
- function validateAssetSubcategoryCompatibility(task, format) {
223
- const category = typeof task.category === 'string' ? task.category.trim().toUpperCase() : '';
224
- const subcategory = typeof task.subcategory === 'string' ? task.subcategory.trim().toLowerCase() : '';
225
- const definition = types_1.ASSET_SUBCATEGORY_DEFINITIONS_BY_KEY[`${category}:${subcategory}`];
226
- if (!definition) {
227
- throw new Error(`Asset "${task.name}" uses unsupported subcategory "${task.subcategory}" for category "${task.category}".`);
116
+ function buildUploadErrorDetail(error) {
117
+ const message = typeof error?.message === 'string' && error.message.trim()
118
+ ? error.message.trim()
119
+ : 'upload failed';
120
+ if (/status\s+413/i.test(message)) {
121
+ return 'upload failed: asset bundle too large (HTTP 413). Reduce the size of your app files and try again.';
228
122
  }
229
- if (!definition.formats.includes(format)) {
230
- throw new Error(`Asset "${task.name}" uses format "${format}" not allowed for subcategory "${subcategory}".`);
123
+ if (!message.startsWith('upload failed')) {
124
+ return `upload failed: ${message}`;
231
125
  }
232
- (0, model_artifacts_1.assertGeneratedModel3DAssetRoles)(task, format);
233
- return subcategory;
126
+ return message;
234
127
  }
235
- async function uploadAssetTask(client, task, sourceAppVersionId, creatorUsername) {
236
- const format = resolveAssetFormat(task);
237
- const subcategory = validateAssetSubcategoryCompatibility(task, format);
238
- await (0, model_artifacts_1.prepareModel3DAssetArtifacts)(task);
239
- const visibility = task.visibility;
240
- const files = Object.entries(task.filePaths).map(([role, filePath]) => ({
241
- file: createFileFromPath(filePath),
242
- fieldName: role,
243
- filename: (0, node_path_1.basename)(filePath),
244
- }));
245
- const targetCreatorUsername = typeof creatorUsername === 'string' ? creatorUsername.trim() : '';
246
- if (!targetCreatorUsername) {
247
- throw new Error(`Asset "${task.name}" upload is missing creator username.`);
248
- }
249
- const response = await client.createAssetVersion(targetCreatorUsername, task.name, {
250
- displayName: task.kind === 'asset' ? task.displayName : undefined,
251
- category: task.category,
252
- subcategory,
253
- format,
254
- remixRef: task.kind === 'asset' ? task.remix : undefined,
255
- visibility: visibility,
256
- sourceKind: sourceAppVersionId ? 'APP_EMBEDDED' : 'UPLOAD',
257
- sourceAppVersionId,
258
- shopListed: task.shopListed,
259
- shopPriceCredits: task.shopPriceCredits,
260
- files,
261
- });
262
- const uploadedCreatorUsername = response.asset.creatorUsername;
263
- const name = response.asset.name;
264
- const revision = response.version.revision;
265
- const versionNodeId = typeof response.versionNodeId === 'string' ? response.versionNodeId.trim() : '';
266
- if (typeof uploadedCreatorUsername !== 'string' || uploadedCreatorUsername.trim().length === 0) {
267
- throw new Error(`Asset "${task.name}" upload did not return creatorUsername.`);
268
- }
269
- if (!versionNodeId) {
270
- throw new Error(`Asset "${task.name}" upload did not return versionNodeId.`);
271
- }
128
+ function buildTaskErrorEntry(task, entityId, error) {
272
129
  return {
273
- creatorUsername: uploadedCreatorUsername,
274
- name,
275
- revision,
276
- ref: `asset:${uploadedCreatorUsername}/${name}@r${revision}`,
277
- versionNodeId,
130
+ action: 'upload',
131
+ status: 'error',
132
+ entityType: task.kind,
133
+ entityId,
134
+ catalogue: task.cataloguePath,
135
+ detail: buildUploadErrorDetail(error),
278
136
  };
279
137
  }
280
- function parseAssetRef(raw) {
281
- const trimmed = raw.trim();
282
- const canonicalMatch = /^asset:([^/]+)\/([^@]+)@r(\d+)$/i.exec(trimmed);
283
- if (canonicalMatch) {
284
- return {
285
- creatorUsername: canonicalMatch[1],
286
- name: canonicalMatch[2],
287
- revision: Number.parseInt(canonicalMatch[3], 10),
288
- };
289
- }
290
- const plainMatch = /^([^/]+)\/([^@]+)@r(\d+)$/i.exec(trimmed);
291
- if (plainMatch) {
292
- return {
293
- creatorUsername: plainMatch[1],
294
- name: plainMatch[2],
295
- revision: Number.parseInt(plainMatch[3], 10),
296
- };
297
- }
298
- return null;
138
+ function buildPackPlanningFailureEntry(task, defaultCreator, currentUserRole, message) {
139
+ const creator = (0, upload_content_1.getTaskCreatorResult)(task, defaultCreator, currentUserRole);
140
+ return {
141
+ action: 'upload',
142
+ status: 'error',
143
+ entityType: task.kind,
144
+ entityId: buildTaskEntityId(task, creator.taskCreator),
145
+ catalogue: task.cataloguePath,
146
+ detail: `upload failed: ${message}`,
147
+ };
299
148
  }
300
- async function resolveAssetReference(client, rawRef, fallbackCreator, uploadedAssets) {
301
- const trimmed = rawRef.trim();
302
- if (!trimmed) {
303
- throw new Error('Asset pack has an empty asset reference.');
304
- }
305
- const parsed = parseAssetRef(trimmed);
306
- if (parsed) {
307
- return `asset:${parsed.creatorUsername}/${parsed.name}@r${parsed.revision}`;
308
- }
309
- if (trimmed.includes('@')) {
310
- throw new Error(`Invalid asset ref "${trimmed}". Use "asset:creator/name@rN" format.`);
311
- }
312
- const slashIndex = trimmed.indexOf('/');
313
- const creatorUsername = slashIndex > 0 ? trimmed.slice(0, slashIndex) : fallbackCreator;
314
- const name = slashIndex > 0 ? trimmed.slice(slashIndex + 1) : trimmed;
315
- if (!creatorUsername || !name) {
316
- throw new Error(`Invalid asset reference "${trimmed}".`);
317
- }
318
- const key = `${creatorUsername}/${name}`;
319
- const uploaded = uploadedAssets.get(key);
320
- if (uploaded) {
321
- return uploaded.ref;
322
- }
323
- const detail = await client.fetchAssetBySlug(creatorUsername, name);
324
- const revision = detail?.asset?.currentVersion?.revision;
325
- if (typeof revision !== 'number' || !Number.isInteger(revision) || revision <= 0) {
326
- throw new Error(`Asset "${creatorUsername}/${name}" has no current version to reference from asset pack.`);
327
- }
328
- return `asset:${creatorUsername}/${name}@r${revision}`;
149
+ function appendTaskRelations(graphState, fromNodeId, relations, contextLabel) {
150
+ (relations ?? []).forEach((relation, index) => {
151
+ if (relation.type === 'REMIXES') {
152
+ throw new Error(`${contextLabel} relations[${index}] must use remix instead of REMIXES.`);
153
+ }
154
+ (0, upload_graph_1.appendPendingRelation)(graphState, fromNodeId, relation.type, relation.to, `${contextLabel} relations[${index}]`);
155
+ });
329
156
  }
330
- async function uploadAssetPackTask(client, task, creatorUsername, uploadedAssets, targetCreatorUsername) {
331
- const assetRefs = [];
332
- for (const ref of task.assets) {
333
- assetRefs.push(await resolveAssetReference(client, ref, creatorUsername, uploadedAssets));
334
- }
335
- const mutationCreatorUsername = typeof targetCreatorUsername === 'string' && targetCreatorUsername.trim().length > 0
336
- ? targetCreatorUsername.trim()
337
- : creatorUsername.trim();
338
- if (!mutationCreatorUsername) {
339
- throw new Error(`Asset pack "${task.name}@${task.version}" upload is missing creator username.`);
340
- }
341
- const response = await client.uploadAssetPackVersion(mutationCreatorUsername, task.name, {
342
- request: {
343
- version: task.version,
344
- remixRef: task.remix,
345
- visibility: task.visibility,
346
- hostingMode: task.hostingMode,
347
- externalUrl: task.externalUrl,
348
- downloadUrl: task.downloadUrl,
349
- releaseNotes: task.releaseNotes,
350
- assets: assetRefs.map((assetRef) => ({ assetRef })),
351
- },
352
- icon: task.listing?.iconPath ? createListingFileFromPath(task.listing.iconPath, 'image/png') : undefined,
353
- heroPortrait: task.listing?.heroPortraitPath ? createListingFileFromPath(task.listing.heroPortraitPath, 'image/png') : undefined,
354
- heroLandscape: task.listing?.heroLandscapePath ? createListingFileFromPath(task.listing.heroLandscapePath, 'image/png') : undefined,
355
- screenshotsPortrait: task.listing?.screenshotPortraitPaths?.map((filePath) => createListingFileFromPath(filePath, 'image/png')),
356
- screenshotsLandscape: task.listing?.screenshotLandscapePaths?.map((filePath) => createListingFileFromPath(filePath, 'image/png')),
357
- videosPortrait: task.listing?.videoPortraitPaths?.map((filePath) => createListingFileFromPath(filePath, 'video/mp4')),
358
- videosLandscape: task.listing?.videoLandscapePaths?.map((filePath) => createListingFileFromPath(filePath, 'video/mp4')),
157
+ async function uploadAppTask(state, task, taskCreator, options) {
158
+ const { upload } = await (0, apps_1.runAppPipeline)(state.client, task, {
159
+ skipEcs: options?.skipEcs,
160
+ creatorUsername: taskCreator,
359
161
  });
360
- const versionNodeId = typeof response.versionNodeId === 'string' ? response.versionNodeId.trim() : '';
361
- if (!versionNodeId) {
362
- throw new Error(`Asset pack "${task.name}@${task.version}" upload did not return versionNodeId.`);
363
- }
364
- return {
365
- assetRefs,
366
- versionNodeId,
367
- ref: `pack:${creatorUsername}/${task.name}@${task.version}`,
162
+ if (!upload.versionCreated || !upload.version) {
163
+ throw new Error(`App "${task.name}" upload did not return a created version.`);
164
+ }
165
+ if (typeof upload.versionId !== 'number') {
166
+ throw new Error(`App "${task.name}" upload did not return version ID.`);
167
+ }
168
+ if (typeof upload.versionNodeId !== 'string' || upload.versionNodeId.trim().length === 0) {
169
+ throw new Error(`App "${task.name}" upload did not return versionNodeId.`);
170
+ }
171
+ const appRef = `app:${taskCreator}/${task.name}@${upload.version}`;
172
+ const uploadedApp = {
173
+ creatorUsername: taskCreator,
174
+ name: task.name,
175
+ version: upload.version,
176
+ versionId: upload.versionId,
177
+ versionNodeId: upload.versionNodeId,
178
+ ref: appRef,
368
179
  };
369
- }
370
- const RELATION_TYPES = new Set([
371
- 'USES',
372
- 'CONTAINS',
373
- 'REMIXES',
374
- ]);
375
- function buildEmptyGraphState() {
376
- return {
377
- canonicalNodeByRef: new Map(),
378
- localAppNodeByName: new Map(),
379
- localAssetNodeByName: new Map(),
380
- localPackNodeByNameVersion: new Map(),
381
- ambiguousAppNames: new Set(),
382
- ambiguousAssetNames: new Set(),
383
- ambiguousPackNameVersions: new Set(),
384
- pendingRelations: [],
180
+ state.uploadedAppsByName.set(task.name, uploadedApp);
181
+ (0, upload_graph_1.registerCanonicalNode)(state.graphState, appRef, uploadedApp.versionNodeId);
182
+ (0, upload_graph_1.registerLocalRef)(state.graphState.localAppNodeByName, state.graphState.ambiguousAppNames, task.name, uploadedApp.versionNodeId);
183
+ task.uses.assets.forEach((toRef, index) => {
184
+ (0, upload_graph_1.appendPendingRelation)(state.graphState, uploadedApp.versionNodeId, 'USES', toRef, `[${task.cataloguePath}] app "${task.name}" uses.assets[${index}]`);
185
+ });
186
+ task.uses.packs.forEach((toRef, index) => {
187
+ (0, upload_graph_1.appendPendingRelation)(state.graphState, uploadedApp.versionNodeId, 'USES', toRef, `[${task.cataloguePath}] app "${task.name}" uses.packs[${index}]`);
188
+ });
189
+ appendTaskRelations(state.graphState, uploadedApp.versionNodeId, task.graph.relations, `[${task.cataloguePath}] app "${task.name}"`);
190
+ const entry = {
191
+ action: 'upload',
192
+ status: 'success',
193
+ entityType: 'app',
194
+ entityId: buildTaskEntityId(task, taskCreator),
195
+ catalogue: task.cataloguePath,
196
+ detail: `version ${upload.version} created`,
385
197
  };
198
+ appendOverviewLink(entry, task, state.portalBase, taskCreator);
199
+ return entry;
386
200
  }
387
- function registerLocalRef(map, ambiguous, key, nodeId) {
388
- const normalized = key.trim();
389
- if (!normalized) {
390
- return;
391
- }
392
- const existing = map.get(normalized);
393
- if (!existing) {
394
- map.set(normalized, nodeId);
395
- return;
396
- }
397
- if (existing !== nodeId) {
398
- ambiguous.add(normalized);
201
+ async function uploadStandaloneAssetTask(state, task, taskCreator) {
202
+ const ownedPack = state.packPlanning.ownedPackByAssetKey.get((0, upload_content_1.buildAssetKey)(taskCreator, task.name));
203
+ if (ownedPack) {
204
+ return {
205
+ action: 'upload',
206
+ status: 'success',
207
+ entityType: 'asset',
208
+ entityId: buildTaskEntityId(task, taskCreator),
209
+ catalogue: task.cataloguePath,
210
+ detail: `staged with asset pack ${ownedPack.packTask.name}@${ownedPack.packTask.version}`,
211
+ };
399
212
  }
213
+ const uploaded = await (0, upload_content_1.uploadAssetTask)(state.client, task, undefined, taskCreator);
214
+ state.uploadedAssetsByKey.set(`${uploaded.creatorUsername}/${uploaded.name}`, uploaded);
215
+ (0, upload_graph_1.registerCanonicalNode)(state.graphState, uploaded.ref, uploaded.versionNodeId);
216
+ (0, upload_graph_1.registerLocalRef)(state.graphState.localAssetNodeByName, state.graphState.ambiguousAssetNames, task.name, uploaded.versionNodeId);
217
+ appendTaskRelations(state.graphState, uploaded.versionNodeId, task.relations, `[${task.cataloguePath}] asset "${task.name}"`);
218
+ const entry = {
219
+ action: 'upload',
220
+ status: 'success',
221
+ entityType: 'asset',
222
+ entityId: buildTaskEntityId(task, taskCreator),
223
+ catalogue: task.cataloguePath,
224
+ detail: uploaded.ref,
225
+ };
226
+ appendOverviewLink(entry, task, state.portalBase, uploaded.creatorUsername);
227
+ return entry;
400
228
  }
401
- function registerCanonicalNode(state, ref, nodeId) {
402
- const normalized = ref.trim();
403
- if (!normalized) {
404
- return;
405
- }
406
- state.canonicalNodeByRef.set(normalized, nodeId);
229
+ async function uploadEmbeddedAssetTask(state, task, taskCreator) {
230
+ const sourceApp = state.uploadedAppsByName.get(task.appName);
231
+ if (!sourceApp) {
232
+ throw new Error(`Embedded asset "${task.name}" references app "${task.appName}" that was not uploaded in this run.`);
233
+ }
234
+ const uploaded = await (0, upload_content_1.uploadAssetTask)(state.client, task, sourceApp.versionId, sourceApp.creatorUsername);
235
+ state.uploadedAssetsByKey.set(`${uploaded.creatorUsername}/${uploaded.name}`, uploaded);
236
+ (0, upload_graph_1.registerCanonicalNode)(state.graphState, uploaded.ref, uploaded.versionNodeId);
237
+ (0, upload_graph_1.registerLocalRef)(state.graphState.localAssetNodeByName, state.graphState.ambiguousAssetNames, task.name, uploaded.versionNodeId);
238
+ const entry = {
239
+ action: 'upload',
240
+ status: 'success',
241
+ entityType: 'embedded-asset',
242
+ entityId: buildTaskEntityId(task, taskCreator),
243
+ catalogue: task.cataloguePath,
244
+ detail: `${uploaded.ref} (source app version ${sourceApp.versionId})`,
245
+ };
246
+ appendOverviewLink(entry, task, state.portalBase, uploaded.creatorUsername);
247
+ return entry;
407
248
  }
408
- function appendPendingRelation(state, fromNodeId, type, toRef, context) {
409
- state.pendingRelations.push({
410
- fromNodeId,
411
- type,
412
- toRef,
413
- context,
249
+ function collectPackLocalAssetTasks(state, task, taskCreator) {
250
+ const packKey = (0, upload_content_1.buildPackKey)(taskCreator, task.name, task.version);
251
+ const packPlan = state.packPlanning.packPlansByKey.get(packKey);
252
+ if (!packPlan) {
253
+ throw new Error(`Asset pack "${task.name}@${task.version}" is missing an upload plan.`);
254
+ }
255
+ const localAssetTasks = packPlan.ownedAssetKeys.map((assetKey) => {
256
+ const ownedTask = state.sortedTasks.find((candidate) => candidate.kind === 'asset' && (0, upload_content_1.buildAssetKey)(taskCreator, candidate.name) === assetKey);
257
+ if (!ownedTask || ownedTask.kind !== 'asset') {
258
+ throw new Error(`Asset pack "${task.name}@${task.version}" is missing local asset task "${assetKey}".`);
259
+ }
260
+ return ownedTask;
414
261
  });
262
+ return { packKey, packPlan, localAssetTasks };
415
263
  }
416
- function parseRelationType(raw, context) {
417
- const normalized = raw.trim().toUpperCase();
418
- if (!RELATION_TYPES.has(normalized)) {
419
- throw new Error(`${context}: relation type "${raw}" is not supported.`);
420
- }
421
- return normalized;
422
- }
423
- function resolveLocalRefNodeId(map, ambiguous, key, context, kindLabel) {
424
- if (ambiguous.has(key)) {
425
- throw new Error(`${context}: local ${kindLabel} ref "${key}" is ambiguous. Use a canonical ref.`);
426
- }
427
- const nodeId = map.get(key);
428
- if (!nodeId) {
429
- throw new Error(`${context}: local ${kindLabel} ref "${key}" was not created in this upload run.`);
430
- }
431
- return nodeId;
432
- }
433
- function resolveRelationTargetNodeId(state, rawRef, context) {
434
- const ref = rawRef.trim();
435
- if (!ref) {
436
- throw new Error(`${context}: relation target ref is empty.`);
437
- }
438
- if (ref.startsWith('@app/')) {
439
- const name = ref.slice('@app/'.length).trim();
440
- if (!name) {
441
- throw new Error(`${context}: invalid local app ref "${ref}". Use "@app/<name>".`);
264
+ function registerUploadedLocalPackAssets(state, task, taskCreator, packPlan, localAssetTasks, uploadedPack) {
265
+ const localTaskByUploadKey = new Map();
266
+ for (const localAssetTask of localAssetTasks) {
267
+ const uploadKey = packPlan.uploadKeyByAssetKey.get((0, upload_content_1.buildAssetKey)(taskCreator, localAssetTask.name));
268
+ if (!uploadKey) {
269
+ throw new Error(`Asset pack "${task.name}@${task.version}" is missing upload key for local asset "${localAssetTask.name}".`);
442
270
  }
443
- return resolveLocalRefNodeId(state.localAppNodeByName, state.ambiguousAppNames, name, context, 'app');
271
+ localTaskByUploadKey.set(uploadKey, localAssetTask);
444
272
  }
445
- if (ref.startsWith('@asset/')) {
446
- const name = ref.slice('@asset/'.length).trim();
447
- if (!name) {
448
- throw new Error(`${context}: invalid local asset ref "${ref}". Use "@asset/<name>".`);
273
+ for (const uploadedLocalAsset of uploadedPack.uploadedLocalAssets) {
274
+ const localTask = localTaskByUploadKey.get(uploadedLocalAsset.uploadKey);
275
+ if (!localTask) {
276
+ continue;
449
277
  }
450
- return resolveLocalRefNodeId(state.localAssetNodeByName, state.ambiguousAssetNames, name, context, 'asset');
278
+ const uploadedLocalRef = {
279
+ creatorUsername: uploadedLocalAsset.creatorUsername,
280
+ name: uploadedLocalAsset.name,
281
+ revision: uploadedLocalAsset.revision,
282
+ ref: `asset:${uploadedLocalAsset.creatorUsername}/${uploadedLocalAsset.name}@r${uploadedLocalAsset.revision}`,
283
+ versionNodeId: uploadedLocalAsset.versionNodeId,
284
+ };
285
+ state.uploadedAssetsByKey.set((0, upload_content_1.buildAssetKey)(uploadedLocalAsset.creatorUsername, uploadedLocalAsset.name), uploadedLocalRef);
286
+ (0, upload_graph_1.registerCanonicalNode)(state.graphState, uploadedLocalRef.ref, uploadedLocalRef.versionNodeId);
287
+ (0, upload_graph_1.registerLocalRef)(state.graphState.localAssetNodeByName, state.graphState.ambiguousAssetNames, localTask.name, uploadedLocalRef.versionNodeId);
288
+ appendTaskRelations(state.graphState, uploadedLocalRef.versionNodeId, localTask.relations, `[${localTask.cataloguePath}] asset "${localTask.name}"`);
451
289
  }
452
- if (ref.startsWith('@pack/')) {
453
- const target = ref.slice('@pack/'.length).trim();
454
- const index = target.lastIndexOf('@');
455
- if (index <= 0 || index === target.length - 1) {
456
- throw new Error(`${context}: invalid local pack ref "${ref}". Use "@pack/<name>@<semver>".`);
457
- }
458
- return resolveLocalRefNodeId(state.localPackNodeByNameVersion, state.ambiguousPackNameVersions, target, context, 'pack');
290
+ }
291
+ async function uploadPackTask(state, task, taskCreator) {
292
+ const { packPlan, localAssetTasks } = collectPackLocalAssetTasks(state, task, taskCreator);
293
+ const mutationTargetCreator = typeof task.username === 'string' && task.username.trim().length > 0
294
+ ? taskCreator
295
+ : undefined;
296
+ const uploadedPack = await (0, upload_content_1.uploadAssetPackTask)(state.client, task, taskCreator, state.uploadedAssetsByKey, localAssetTasks, packPlan.uploadKeyByAssetKey, mutationTargetCreator);
297
+ (0, upload_graph_1.registerCanonicalNode)(state.graphState, uploadedPack.ref, uploadedPack.versionNodeId);
298
+ (0, upload_graph_1.registerLocalRef)(state.graphState.localPackNodeByNameVersion, state.graphState.ambiguousPackNameVersions, `${task.name}@${task.version}`, uploadedPack.versionNodeId);
299
+ registerUploadedLocalPackAssets(state, task, taskCreator, packPlan, localAssetTasks, uploadedPack);
300
+ appendTaskRelations(state.graphState, uploadedPack.versionNodeId, task.relations, `[${task.cataloguePath}] assetPack "${task.name}@${task.version}"`);
301
+ const entry = {
302
+ action: 'upload',
303
+ status: 'success',
304
+ entityType: 'asset-pack',
305
+ entityId: buildTaskEntityId(task, taskCreator),
306
+ catalogue: task.cataloguePath,
307
+ detail: `version ${task.version} created (${uploadedPack.assetRefs.length} assets)`,
308
+ };
309
+ appendOverviewLink(entry, task, state.portalBase, taskCreator);
310
+ return entry;
311
+ }
312
+ async function processSingleUploadTask(state, task, taskCreator, options) {
313
+ if (task.kind === 'app') {
314
+ return await uploadAppTask(state, task, taskCreator, options);
459
315
  }
460
- const canonicalPattern = /^(app|asset|pack):[^/]+\/[^@]+@.+$/i;
461
- if (!canonicalPattern.test(ref)) {
462
- throw new Error(`${context}: invalid relation target ref "${ref}". Use canonical refs (app:/asset:/pack:) or local refs (@app/@asset/@pack).`);
316
+ if (task.kind === 'asset') {
317
+ return await uploadStandaloneAssetTask(state, task, taskCreator);
463
318
  }
464
- const nodeId = state.canonicalNodeByRef.get(ref);
465
- if (!nodeId) {
466
- throw new Error(`${context}: canonical ref "${ref}" was not resolved in this upload run.`);
319
+ if (task.kind === 'embedded-asset') {
320
+ return await uploadEmbeddedAssetTask(state, task, taskCreator);
467
321
  }
468
- return nodeId;
322
+ if (task.kind === 'asset-pack') {
323
+ return await uploadPackTask(state, task, taskCreator);
324
+ }
325
+ throw new Error(`Unsupported task kind "${task.kind}".`);
469
326
  }
470
- async function applyGraphOperations(client, state) {
471
- if (state.pendingRelations.length > 0) {
472
- const relations = state.pendingRelations.map((row) => ({
473
- fromNodeId: row.fromNodeId,
474
- toNodeId: resolveRelationTargetNodeId(state, row.toRef, row.context),
475
- type: parseRelationType(row.type, row.context),
476
- }));
477
- await client.batchUpsertGraphRelations({ relations });
327
+ async function flushGraphState(client, graphState, results) {
328
+ try {
329
+ await (0, upload_graph_1.applyGraphOperations)(client, graphState);
330
+ }
331
+ catch (error) {
332
+ const message = typeof error?.message === 'string' && error.message.trim().length > 0
333
+ ? error.message.trim()
334
+ : 'graph batch upload failed';
335
+ const entry = {
336
+ action: 'upload',
337
+ status: 'error',
338
+ entityType: 'app',
339
+ entityId: 'graph',
340
+ catalogue: 'graph:batch',
341
+ detail: `upload failed: ${message}`,
342
+ };
343
+ pushLoggedEntry(results, entry);
344
+ process.exitCode = process.exitCode || 1;
478
345
  }
479
346
  }
480
347
  async function processUploadTasks(client, tasks, owner, ownerUsername, currentUserRole, webBase, options) {
481
348
  const portalBase = webBase ? normalizePortalBase(webBase) : null;
482
349
  const defaultCreator = normalizeCreatorUsername(ownerUsername) ?? owner;
483
- const uploadedAppsByName = new Map();
484
- const uploadedAssetsByKey = new Map();
485
- const graphState = buildEmptyGraphState();
486
350
  const results = [];
487
351
  let aborted = false;
488
- for (const task of (0, taskUtils_1.sortTasks)(tasks)) {
489
- let taskOwner = defaultCreator;
490
- let taskOwnerUsername = defaultCreator;
491
- const requestedTaskOwner = 'username' in task && typeof task.username === 'string'
492
- ? String(task.username).trim()
493
- : '';
494
- const creatorTargetError = requestedTaskOwner && currentUserRole !== 'ADMIN'
495
- ? `Task "${task.name}" sets username="${requestedTaskOwner}", but only admins can target another creator.`
496
- : null;
497
- if (requestedTaskOwner && !creatorTargetError) {
498
- taskOwner = requestedTaskOwner;
499
- taskOwnerUsername = requestedTaskOwner;
500
- console.log(`[Admin] Publishing ${task.name} as user: ${requestedTaskOwner}`);
352
+ const sortedTasks = (0, taskUtils_1.sortTasks)(tasks);
353
+ const packPlanning = (0, upload_content_1.buildAssetPackUploadPlans)(sortedTasks, defaultCreator, currentUserRole);
354
+ if (!packPlanning.ok) {
355
+ const entry = buildPackPlanningFailureEntry(packPlanning.task, defaultCreator, currentUserRole, packPlanning.message);
356
+ pushLoggedEntry(results, entry);
357
+ process.exitCode = process.exitCode || 1;
358
+ return results;
359
+ }
360
+ const state = {
361
+ client,
362
+ sortedTasks,
363
+ defaultCreator,
364
+ currentUserRole,
365
+ portalBase,
366
+ uploadedAppsByName: new Map(),
367
+ uploadedAssetsByKey: new Map(),
368
+ graphState: (0, upload_graph_1.buildEmptyGraphState)(),
369
+ packPlanning,
370
+ };
371
+ for (const task of sortedTasks) {
372
+ const creatorResult = (0, upload_content_1.getTaskCreatorResult)(task, defaultCreator, currentUserRole);
373
+ if (creatorResult.requestedTaskOwner && !creatorResult.creatorTargetError) {
374
+ console.log(`[Admin] Publishing ${task.name} as user: ${creatorResult.taskCreator}`);
501
375
  }
502
- const entityId = task.kind === 'embedded-asset' ? `${taskOwner}/${task.appName}:${task.name}` : `${taskOwner}/${task.name}`;
503
- const taskCreator = normalizeCreatorUsername(taskOwnerUsername) ?? taskOwner;
376
+ const entityId = buildTaskEntityId(task, creatorResult.taskCreator);
377
+ const taskCreator = creatorResult.taskCreator;
504
378
  try {
505
- if (creatorTargetError) {
506
- throw new Error(creatorTargetError);
507
- }
508
- if (task.kind === 'app') {
509
- const { upload } = await (0, apps_1.runAppPipeline)(client, task, {
510
- skipEcs: options?.skipEcs,
511
- creatorUsername: taskCreator,
512
- });
513
- if (!upload.versionCreated || !upload.version) {
514
- throw new Error(`App "${task.name}" upload did not return a created version.`);
515
- }
516
- if (typeof upload.versionId !== 'number') {
517
- throw new Error(`App "${task.name}" upload did not return version ID.`);
518
- }
519
- if (typeof upload.versionNodeId !== 'string' || upload.versionNodeId.trim().length === 0) {
520
- throw new Error(`App "${task.name}" upload did not return versionNodeId.`);
521
- }
522
- const appRef = `app:${taskCreator}/${task.name}@${upload.version}`;
523
- const uploadedApp = {
524
- creatorUsername: taskCreator,
525
- name: task.name,
526
- version: upload.version,
527
- versionId: upload.versionId,
528
- versionNodeId: upload.versionNodeId,
529
- ref: appRef,
530
- };
531
- uploadedAppsByName.set(task.name, uploadedApp);
532
- registerCanonicalNode(graphState, appRef, uploadedApp.versionNodeId);
533
- registerLocalRef(graphState.localAppNodeByName, graphState.ambiguousAppNames, task.name, uploadedApp.versionNodeId);
534
- task.uses.assets.forEach((toRef, index) => {
535
- appendPendingRelation(graphState, uploadedApp.versionNodeId, 'USES', toRef, `[${task.cataloguePath}] app "${task.name}" uses.assets[${index}]`);
536
- });
537
- task.uses.packs.forEach((toRef, index) => {
538
- appendPendingRelation(graphState, uploadedApp.versionNodeId, 'USES', toRef, `[${task.cataloguePath}] app "${task.name}" uses.packs[${index}]`);
539
- });
540
- task.graph.relations.forEach((relation, index) => {
541
- if (relation.type === 'REMIXES') {
542
- throw new Error(`[${task.cataloguePath}] app "${task.name}" relations[${index}] must use remix instead of REMIXES.`);
543
- }
544
- appendPendingRelation(graphState, uploadedApp.versionNodeId, relation.type, relation.to, `[${task.cataloguePath}] app "${task.name}" relations[${index}]`);
545
- });
546
- const entry = {
547
- action: 'upload',
548
- status: 'success',
549
- entityType: 'app',
550
- entityId,
551
- catalogue: task.cataloguePath,
552
- detail: `version ${upload.version} created`,
553
- };
554
- appendOverviewLink(entry, task, portalBase, taskCreator);
555
- results.push(entry);
556
- console.log((0, uploadLog_1.formatTaskLogLine)(entry));
557
- }
558
- else if (task.kind === 'asset') {
559
- const uploaded = await uploadAssetTask(client, task, undefined, taskCreator);
560
- uploadedAssetsByKey.set(`${uploaded.creatorUsername}/${uploaded.name}`, uploaded);
561
- registerCanonicalNode(graphState, uploaded.ref, uploaded.versionNodeId);
562
- registerLocalRef(graphState.localAssetNodeByName, graphState.ambiguousAssetNames, task.name, uploaded.versionNodeId);
563
- (task.relations ?? []).forEach((relation, index) => {
564
- if (relation.type === 'REMIXES') {
565
- throw new Error(`[${task.cataloguePath}] asset "${task.name}" relations[${index}] must use remix instead of REMIXES.`);
566
- }
567
- appendPendingRelation(graphState, uploaded.versionNodeId, relation.type, relation.to, `[${task.cataloguePath}] asset "${task.name}" relations[${index}]`);
568
- });
569
- const entry = {
570
- action: 'upload',
571
- status: 'success',
572
- entityType: 'asset',
573
- entityId,
574
- catalogue: task.cataloguePath,
575
- detail: uploaded.ref,
576
- };
577
- appendOverviewLink(entry, task, portalBase, uploaded.creatorUsername);
578
- results.push(entry);
579
- console.log((0, uploadLog_1.formatTaskLogLine)(entry));
580
- }
581
- else if (task.kind === 'embedded-asset') {
582
- const sourceApp = uploadedAppsByName.get(task.appName);
583
- if (!sourceApp) {
584
- throw new Error(`Embedded asset "${task.name}" references app "${task.appName}" that was not uploaded in this run.`);
585
- }
586
- const uploaded = await uploadAssetTask(client, task, sourceApp.versionId, sourceApp.creatorUsername);
587
- uploadedAssetsByKey.set(`${uploaded.creatorUsername}/${uploaded.name}`, uploaded);
588
- registerCanonicalNode(graphState, uploaded.ref, uploaded.versionNodeId);
589
- registerLocalRef(graphState.localAssetNodeByName, graphState.ambiguousAssetNames, task.name, uploaded.versionNodeId);
590
- const entry = {
591
- action: 'upload',
592
- status: 'success',
593
- entityType: 'embedded-asset',
594
- entityId,
595
- catalogue: task.cataloguePath,
596
- detail: `${uploaded.ref} (source app version ${sourceApp.versionId})`,
597
- };
598
- appendOverviewLink(entry, task, portalBase, uploaded.creatorUsername);
599
- results.push(entry);
600
- console.log((0, uploadLog_1.formatTaskLogLine)(entry));
601
- }
602
- else if (task.kind === 'asset-pack') {
603
- const mutationTargetCreator = typeof task.username === 'string' && task.username.trim().length > 0
604
- ? taskCreator
605
- : undefined;
606
- const uploadedPack = await uploadAssetPackTask(client, task, taskCreator, uploadedAssetsByKey, mutationTargetCreator);
607
- registerCanonicalNode(graphState, uploadedPack.ref, uploadedPack.versionNodeId);
608
- registerLocalRef(graphState.localPackNodeByNameVersion, graphState.ambiguousPackNameVersions, `${task.name}@${task.version}`, uploadedPack.versionNodeId);
609
- (task.relations ?? []).forEach((relation, index) => {
610
- if (relation.type === 'REMIXES') {
611
- throw new Error(`[${task.cataloguePath}] assetPack "${task.name}@${task.version}" relations[${index}] must use remix instead of REMIXES.`);
612
- }
613
- appendPendingRelation(graphState, uploadedPack.versionNodeId, relation.type, relation.to, `[${task.cataloguePath}] assetPack "${task.name}@${task.version}" relations[${index}]`);
614
- });
615
- const entry = {
616
- action: 'upload',
617
- status: 'success',
618
- entityType: 'asset-pack',
619
- entityId,
620
- catalogue: task.cataloguePath,
621
- detail: `version ${task.version} created (${uploadedPack.assetRefs.length} assets)`,
622
- };
623
- appendOverviewLink(entry, task, portalBase, taskCreator);
624
- results.push(entry);
625
- console.log((0, uploadLog_1.formatTaskLogLine)(entry));
626
- }
627
- else {
628
- throw new Error(`Unsupported task kind "${task.kind}".`);
379
+ if (creatorResult.creatorTargetError) {
380
+ throw new Error(creatorResult.creatorTargetError);
629
381
  }
382
+ const entry = await processSingleUploadTask(state, task, taskCreator, options);
383
+ pushLoggedEntry(results, entry);
630
384
  }
631
385
  catch (error) {
632
386
  if (error instanceof http_1.CLIUnsupportedClientError) {
633
387
  throw error;
634
388
  }
635
- let message = typeof error?.message === 'string' && error.message.trim() ? error.message.trim() : 'upload failed';
636
- if (/status\s+413/i.test(message)) {
637
- message = 'upload failed: asset bundle too large (HTTP 413). Reduce the size of your app files and try again.';
638
- }
639
- else if (!message.startsWith('upload failed')) {
640
- message = `upload failed: ${message}`;
641
- }
642
- const entry = {
643
- action: 'upload',
644
- status: 'error',
645
- entityType: task.kind,
646
- entityId,
647
- catalogue: task.cataloguePath,
648
- detail: message,
649
- };
650
- results.push(entry);
651
- console.log((0, uploadLog_1.formatTaskLogLine)(entry));
389
+ const entry = buildTaskErrorEntry(task, entityId, error);
390
+ pushLoggedEntry(results, entry);
652
391
  process.exitCode = process.exitCode || 1;
653
392
  aborted = true;
654
393
  break;
655
394
  }
656
395
  }
657
396
  if (!aborted) {
658
- try {
659
- await applyGraphOperations(client, graphState);
660
- }
661
- catch (error) {
662
- const message = typeof error?.message === 'string' && error.message.trim().length > 0
663
- ? error.message.trim()
664
- : 'graph batch upload failed';
665
- const entry = {
666
- action: 'upload',
667
- status: 'error',
668
- entityType: 'app',
669
- entityId: 'graph',
670
- catalogue: 'graph:batch',
671
- detail: `upload failed: ${message}`,
672
- };
673
- results.push(entry);
674
- console.log((0, uploadLog_1.formatTaskLogLine)(entry));
675
- process.exitCode = process.exitCode || 1;
676
- }
397
+ await flushGraphState(client, state.graphState, results);
677
398
  }
678
399
  return results;
679
400
  }