@playdrop/playdrop-cli 0.7.2 → 0.7.4

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.upload = upload;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
+ const semver_1 = require("semver");
6
7
  const types_1 = require("@playdrop/types");
7
8
  const apps_1 = require("../apps");
8
9
  const commandContext_1 = require("../commandContext");
@@ -182,6 +183,522 @@ function normalizeCreatorUsername(username) {
182
183
  }
183
184
  return trimmed;
184
185
  }
186
+ function isNotFoundApiError(error) {
187
+ return error instanceof types_1.ApiError && error.status === 404;
188
+ }
189
+ function createRemoteDependencyCache() {
190
+ return {
191
+ assetSpecVersionExists: new Map(),
192
+ assetSpecSupportExists: new Map(),
193
+ assetVersionExists: new Map(),
194
+ packVersionExists: new Map(),
195
+ };
196
+ }
197
+ function createTaskExecutionId(task, creator, index) {
198
+ if (task.kind === 'asset-spec') {
199
+ return `${index}:${task.kind}:${creator}/${task.name}@${task.version}`;
200
+ }
201
+ if (task.kind === 'asset-pack') {
202
+ return `${index}:${task.kind}:${creator}/${task.name}@${task.version}`;
203
+ }
204
+ if (task.kind === 'owned-asset') {
205
+ return `${index}:${task.kind}:${creator}/${task.appName}:${task.name}`;
206
+ }
207
+ return `${index}:${task.kind}:${creator}/${task.name}`;
208
+ }
209
+ function buildTaskDetailEntry(task, entityId, status, detail) {
210
+ return {
211
+ action: 'upload',
212
+ status,
213
+ entityType: task.kind,
214
+ entityId,
215
+ catalogue: task.cataloguePath,
216
+ detail,
217
+ };
218
+ }
219
+ function buildTaskDirectErrorEntry(task, entityId, detail) {
220
+ return buildTaskDetailEntry(task, entityId, 'error', detail.startsWith('upload failed:') ? detail : `upload failed: ${detail}`);
221
+ }
222
+ function buildTaskBlockedEntry(task, entityId, detail) {
223
+ return buildTaskDetailEntry(task, entityId, 'blocked', detail);
224
+ }
225
+ function buildAssetSpecFamilyKey(creatorUsername, name) {
226
+ return `${creatorUsername}/${name}`;
227
+ }
228
+ function buildAssetSpecVersionKey(creatorUsername, name, version) {
229
+ return `${creatorUsername}/${name}@${version}`;
230
+ }
231
+ function pushIndexedNode(map, key, node) {
232
+ const existing = map.get(key);
233
+ if (existing) {
234
+ existing.push(node);
235
+ return;
236
+ }
237
+ map.set(key, [node]);
238
+ }
239
+ function pickSingleLocalDependency(candidates, ref) {
240
+ if (!candidates || candidates.length === 0) {
241
+ return { node: null, blockedDetail: null };
242
+ }
243
+ if (candidates.length > 1) {
244
+ return {
245
+ node: null,
246
+ blockedDetail: `local_dependency_ambiguous:${ref}`,
247
+ };
248
+ }
249
+ return { node: candidates[0], blockedDetail: null };
250
+ }
251
+ function pickCompatibleLocalAssetSpecTask(candidates, versionRange) {
252
+ if (!candidates || candidates.length === 0) {
253
+ return null;
254
+ }
255
+ const matching = candidates.filter((candidate) => (candidate.task.kind === 'asset-spec' && (0, semver_1.satisfies)(candidate.task.version, versionRange)));
256
+ if (matching.length === 0) {
257
+ return null;
258
+ }
259
+ matching.sort((left, right) => (0, semver_1.compare)(right.task.version, left.task.version));
260
+ return matching[0];
261
+ }
262
+ async function validateRemoteAssetSpecVersionExists(client, rawRef, cache) {
263
+ if (cache.assetSpecVersionExists.has(rawRef)) {
264
+ return cache.assetSpecVersionExists.get(rawRef);
265
+ }
266
+ const parsed = (0, types_1.parseAssetSpecVersionRef)(rawRef);
267
+ if (!parsed) {
268
+ cache.assetSpecVersionExists.set(rawRef, false);
269
+ return false;
270
+ }
271
+ try {
272
+ await client.fetchAssetSpecVersion(parsed.creatorUsername, parsed.name, parsed.version);
273
+ cache.assetSpecVersionExists.set(rawRef, true);
274
+ return true;
275
+ }
276
+ catch (error) {
277
+ if (isNotFoundApiError(error)) {
278
+ cache.assetSpecVersionExists.set(rawRef, false);
279
+ return false;
280
+ }
281
+ throw error;
282
+ }
283
+ }
284
+ async function validateRemoteAssetSpecSupportExists(client, familyRef, versionRange, cache) {
285
+ const cacheKey = `${familyRef}@@${versionRange}`;
286
+ if (cache.assetSpecSupportExists.has(cacheKey)) {
287
+ return cache.assetSpecSupportExists.get(cacheKey);
288
+ }
289
+ const parsed = (0, types_1.parseAssetSpecFamilyRef)(familyRef);
290
+ if (!parsed) {
291
+ cache.assetSpecSupportExists.set(cacheKey, false);
292
+ return false;
293
+ }
294
+ try {
295
+ const response = await client.listAssetSpecVersions(parsed.creatorUsername, parsed.name, { limit: 200, offset: 0 });
296
+ const exists = response.versions.some((version) => (0, semver_1.satisfies)(version.version, versionRange));
297
+ cache.assetSpecSupportExists.set(cacheKey, exists);
298
+ return exists;
299
+ }
300
+ catch (error) {
301
+ if (isNotFoundApiError(error)) {
302
+ cache.assetSpecSupportExists.set(cacheKey, false);
303
+ return false;
304
+ }
305
+ throw error;
306
+ }
307
+ }
308
+ async function validateRemoteAssetVersionExists(client, rawRef, cache) {
309
+ if (cache.assetVersionExists.has(rawRef)) {
310
+ return cache.assetVersionExists.get(rawRef);
311
+ }
312
+ const parsed = (0, types_1.parseContentVersionRef)(rawRef);
313
+ if (!parsed || parsed.kind !== 'asset') {
314
+ cache.assetVersionExists.set(rawRef, false);
315
+ return false;
316
+ }
317
+ try {
318
+ const detail = await client.fetchAssetBySlug(parsed.creatorUsername, parsed.name);
319
+ if (detail.asset.currentVersion?.revision === parsed.revision) {
320
+ cache.assetVersionExists.set(rawRef, true);
321
+ return true;
322
+ }
323
+ const versions = await client.listAssetVersions(parsed.creatorUsername, parsed.name, { limit: 200, offset: 0 });
324
+ const exists = versions.versions.some((version) => version.revision === parsed.revision);
325
+ cache.assetVersionExists.set(rawRef, exists);
326
+ return exists;
327
+ }
328
+ catch (error) {
329
+ if (isNotFoundApiError(error)) {
330
+ cache.assetVersionExists.set(rawRef, false);
331
+ return false;
332
+ }
333
+ throw error;
334
+ }
335
+ }
336
+ async function validateRemotePackVersionExists(client, rawRef, cache) {
337
+ if (cache.packVersionExists.has(rawRef)) {
338
+ return cache.packVersionExists.get(rawRef);
339
+ }
340
+ const parsed = (0, types_1.parseContentVersionRef)(rawRef);
341
+ if (!parsed || parsed.kind !== 'pack') {
342
+ cache.packVersionExists.set(rawRef, false);
343
+ return false;
344
+ }
345
+ try {
346
+ const detail = await client.fetchAssetPackBySlug(parsed.creatorUsername, parsed.name);
347
+ if (detail.pack.currentVersion?.version === parsed.version) {
348
+ cache.packVersionExists.set(rawRef, true);
349
+ return true;
350
+ }
351
+ const versions = await client.listAssetPackVersions(parsed.creatorUsername, parsed.name, { limit: 200, offset: 0 });
352
+ const exists = versions.versions.some((version) => version.version === parsed.version);
353
+ cache.packVersionExists.set(rawRef, exists);
354
+ return exists;
355
+ }
356
+ catch (error) {
357
+ if (isNotFoundApiError(error)) {
358
+ cache.packVersionExists.set(rawRef, false);
359
+ return false;
360
+ }
361
+ throw error;
362
+ }
363
+ }
364
+ function addLocalDependency(node, dependencyTaskId, ref) {
365
+ if (node.localDependencies.some((dependency) => dependency.taskId === dependencyTaskId)) {
366
+ return;
367
+ }
368
+ node.localDependencies.push({ taskId: dependencyTaskId, ref });
369
+ }
370
+ function normalizePackDependencyRef(rawRef) {
371
+ const parsed = (0, types_1.parseContentVersionRef)(rawRef);
372
+ if (!parsed || parsed.kind !== 'pack') {
373
+ return null;
374
+ }
375
+ return (0, types_1.formatContentVersionRef)(parsed);
376
+ }
377
+ function buildTagRegistryEntry(allowedTargetKinds) {
378
+ return {
379
+ allowedTargetKinds: normalizeSavedItemKinds(allowedTargetKinds),
380
+ tags: new Set(),
381
+ };
382
+ }
383
+ function mergeTagRegistryEntry(registry, groupSlug, entry, options) {
384
+ const existing = registry.get(groupSlug);
385
+ if (!existing) {
386
+ registry.set(groupSlug, {
387
+ allowedTargetKinds: [...entry.allowedTargetKinds],
388
+ tags: new Set(entry.tags),
389
+ });
390
+ return;
391
+ }
392
+ if (options.replaceAllowedTargetKinds) {
393
+ existing.allowedTargetKinds = [...entry.allowedTargetKinds];
394
+ }
395
+ entry.tags.forEach((tagSlug) => existing.tags.add(tagSlug));
396
+ }
397
+ async function buildRemoteTagRegistry(client) {
398
+ const registry = new Map();
399
+ let directoryOffset = 0;
400
+ while (true) {
401
+ const directory = await client.fetchTagDirectory({
402
+ limit: 200,
403
+ ...(directoryOffset > 0 ? { offset: directoryOffset } : {}),
404
+ tagLimit: 1,
405
+ });
406
+ for (const summary of directory.groups) {
407
+ const groupSlug = summary.group.slug;
408
+ const tagCount = Math.max(summary.tagCount, summary.tags.length);
409
+ const entry = buildTagRegistryEntry(summary.group.allowedTargetKinds);
410
+ let offset = 0;
411
+ while (offset < tagCount || (tagCount === 0 && offset === 0)) {
412
+ const response = await client.fetchTagGroup(groupSlug, {
413
+ sort: 'alpha',
414
+ limit: 200,
415
+ ...(offset > 0 ? { offset } : {}),
416
+ });
417
+ response.tags.forEach((tag) => entry.tags.add(tag.slug));
418
+ if (response.tags.length === 0 || response.tags.length < 200) {
419
+ break;
420
+ }
421
+ offset += response.tags.length;
422
+ }
423
+ mergeTagRegistryEntry(registry, groupSlug, entry, { replaceAllowedTargetKinds: true });
424
+ }
425
+ if (!directory.pagination?.hasMore) {
426
+ break;
427
+ }
428
+ directoryOffset += directory.groups.length;
429
+ }
430
+ return registry;
431
+ }
432
+ function buildLocalTagRegistry(groups) {
433
+ const registry = new Map();
434
+ for (const group of groups) {
435
+ const entry = buildTagRegistryEntry(group.allowedTargetKinds);
436
+ group.tags.forEach((tag) => entry.tags.add(tag.slug));
437
+ mergeTagRegistryEntry(registry, group.slug, entry, { replaceAllowedTargetKinds: true });
438
+ }
439
+ return registry;
440
+ }
441
+ async function buildUploadTagRegistry(client, localGroups) {
442
+ const registry = await buildRemoteTagRegistry(client);
443
+ const localRegistry = buildLocalTagRegistry(localGroups);
444
+ for (const [groupSlug, entry] of localRegistry.entries()) {
445
+ mergeTagRegistryEntry(registry, groupSlug, entry, { replaceAllowedTargetKinds: true });
446
+ }
447
+ return registry;
448
+ }
449
+ function validateTagRefsForTarget(tagRegistry, target) {
450
+ for (const rawTagRef of target.tags) {
451
+ let normalizedRef = '';
452
+ try {
453
+ normalizedRef = (0, types_1.normalizeTagRef)(rawTagRef);
454
+ }
455
+ catch {
456
+ return `invalid_tag_ref:${String(rawTagRef ?? '').trim() || String(rawTagRef ?? '')}`;
457
+ }
458
+ const [groupSlug, tagSlug] = normalizedRef.split('/');
459
+ if (!groupSlug || !tagSlug) {
460
+ return `invalid_tag_ref:${normalizedRef}`;
461
+ }
462
+ const registryEntry = tagRegistry.get(groupSlug);
463
+ if (!registryEntry || !registryEntry.tags.has(tagSlug)) {
464
+ return `unknown_tag_ref:${normalizedRef}`;
465
+ }
466
+ if (registryEntry.allowedTargetKinds.length > 0
467
+ && !registryEntry.allowedTargetKinds.includes(target.targetKind)) {
468
+ return `tag_not_allowed_for_target_kind:${normalizedRef}:${target.targetKind}`;
469
+ }
470
+ }
471
+ return null;
472
+ }
473
+ function collectTaskTagValidationTargets(task) {
474
+ if (task.kind === 'app') {
475
+ return [
476
+ { tags: task.tags, targetKind: 'APP' },
477
+ ...task.ownedAssets.map((ownedAsset) => ({
478
+ tags: ownedAsset.tags,
479
+ targetKind: 'ASSET',
480
+ })),
481
+ ];
482
+ }
483
+ if (task.kind === 'asset') {
484
+ return [{ tags: task.tags, targetKind: 'ASSET' }];
485
+ }
486
+ if (task.kind === 'owned-asset') {
487
+ return [{ tags: task.tags, targetKind: 'ASSET' }];
488
+ }
489
+ if (task.kind === 'asset-pack') {
490
+ return [
491
+ { tags: task.tags, targetKind: 'PACK' },
492
+ ...task.ownedAssets.map((ownedAsset) => ({
493
+ tags: ownedAsset.tags,
494
+ targetKind: 'ASSET',
495
+ })),
496
+ ];
497
+ }
498
+ return [];
499
+ }
500
+ function tasksRequireTagValidation(tasks) {
501
+ return tasks.some((task) => collectTaskTagValidationTargets(task).some((target) => target.tags.length > 0));
502
+ }
503
+ async function buildUploadPreflight(client, tasks, defaultCreator, currentUserRole, localTagGroups) {
504
+ const nodes = (0, taskUtils_1.sortTasks)(tasks).map((task, index) => {
505
+ const creatorResult = (0, upload_content_1.getTaskCreatorResult)(task, defaultCreator, currentUserRole);
506
+ const entityId = buildTaskEntityId(task, creatorResult.taskCreator);
507
+ const node = {
508
+ id: createTaskExecutionId(task, creatorResult.taskCreator, index),
509
+ task,
510
+ creator: creatorResult.taskCreator,
511
+ entityId,
512
+ localDependencies: [],
513
+ localAppAssetKeysByRef: new Map(),
514
+ localAppPackRefsByRef: new Map(),
515
+ };
516
+ return { node, creatorResult };
517
+ });
518
+ const initialEntries = [];
519
+ for (const { node, creatorResult } of nodes) {
520
+ if (creatorResult.creatorTargetError) {
521
+ initialEntries.push({
522
+ taskId: node.id,
523
+ entry: buildTaskDirectErrorEntry(node.task, node.entityId, creatorResult.creatorTargetError),
524
+ });
525
+ }
526
+ }
527
+ const assetSpecVersionTasksByKey = new Map();
528
+ const assetSpecFamilyTasksByKey = new Map();
529
+ const assetTasksByKey = new Map();
530
+ const packTasksByKey = new Map();
531
+ for (const { node } of nodes) {
532
+ if (node.task.kind === 'asset-spec') {
533
+ pushIndexedNode(assetSpecVersionTasksByKey, buildAssetSpecVersionKey(node.creator, node.task.name, node.task.version), node);
534
+ pushIndexedNode(assetSpecFamilyTasksByKey, buildAssetSpecFamilyKey(node.creator, node.task.name), node);
535
+ continue;
536
+ }
537
+ if (node.task.kind === 'asset') {
538
+ pushIndexedNode(assetTasksByKey, (0, upload_content_1.buildAssetKey)(node.creator, node.task.name), node);
539
+ continue;
540
+ }
541
+ if (node.task.kind === 'asset-pack') {
542
+ pushIndexedNode(packTasksByKey, (0, upload_content_1.buildPackKey)(node.creator, node.task.name, node.task.version), node);
543
+ }
544
+ }
545
+ const remoteCache = createRemoteDependencyCache();
546
+ const tagRegistry = tasksRequireTagValidation(tasks)
547
+ ? await buildUploadTagRegistry(client, localTagGroups)
548
+ : null;
549
+ const initialStatusByTaskId = new Map(initialEntries.map((entry) => [entry.taskId, entry.entry.status]));
550
+ for (const { node } of nodes) {
551
+ if (initialStatusByTaskId.has(node.id)) {
552
+ continue;
553
+ }
554
+ let blockedDetail = null;
555
+ if (!blockedDetail && tagRegistry) {
556
+ for (const target of collectTaskTagValidationTargets(node.task)) {
557
+ blockedDetail = validateTagRefsForTarget(tagRegistry, target);
558
+ if (blockedDetail) {
559
+ break;
560
+ }
561
+ }
562
+ }
563
+ if (!blockedDetail && node.task.kind === 'asset' && node.task.assetSpec) {
564
+ const parsedAssetSpec = (0, types_1.parseAssetSpecVersionRef)(node.task.assetSpec);
565
+ if (!parsedAssetSpec) {
566
+ blockedDetail = `dependency_invalid:${node.task.assetSpec}`;
567
+ }
568
+ else {
569
+ const assetSpecKey = buildAssetSpecVersionKey(parsedAssetSpec.creatorUsername, parsedAssetSpec.name, parsedAssetSpec.version);
570
+ const localDependency = pickSingleLocalDependency(assetSpecVersionTasksByKey.get(assetSpecKey), (0, types_1.formatAssetSpecVersionRef)(parsedAssetSpec));
571
+ if (localDependency.blockedDetail) {
572
+ blockedDetail = localDependency.blockedDetail;
573
+ }
574
+ else if (localDependency.node) {
575
+ addLocalDependency(node, localDependency.node.id, (0, types_1.formatAssetSpecVersionRef)(parsedAssetSpec));
576
+ }
577
+ else if (!(await validateRemoteAssetSpecVersionExists(client, (0, types_1.formatAssetSpecVersionRef)(parsedAssetSpec), remoteCache))) {
578
+ blockedDetail = `dependency_missing:${(0, types_1.formatAssetSpecVersionRef)(parsedAssetSpec)}`;
579
+ }
580
+ }
581
+ }
582
+ if (!blockedDetail && node.task.kind === 'app') {
583
+ for (const support of node.task.assetSpecSupport) {
584
+ const parsedFamily = (0, types_1.parseAssetSpecFamilyRef)(support.assetSpec);
585
+ if (!parsedFamily) {
586
+ blockedDetail = `dependency_invalid:${support.assetSpec}`;
587
+ break;
588
+ }
589
+ const familyKey = buildAssetSpecFamilyKey(parsedFamily.creatorUsername, parsedFamily.name);
590
+ const localDependency = pickCompatibleLocalAssetSpecTask(assetSpecFamilyTasksByKey.get(familyKey), support.versionRange);
591
+ if (localDependency) {
592
+ addLocalDependency(node, localDependency.id, `${support.assetSpec}@${support.versionRange}`);
593
+ continue;
594
+ }
595
+ const remoteExists = await validateRemoteAssetSpecSupportExists(client, support.assetSpec, support.versionRange, remoteCache);
596
+ if (!remoteExists) {
597
+ blockedDetail = `dependency_missing:${support.assetSpec}@${support.versionRange}`;
598
+ break;
599
+ }
600
+ }
601
+ }
602
+ if (!blockedDetail && node.task.kind === 'app') {
603
+ for (const assetDependency of node.task.uses.assets) {
604
+ const parsedAsset = (0, types_1.parseContentVersionRef)(assetDependency.ref);
605
+ if (!parsedAsset || parsedAsset.kind !== 'asset') {
606
+ blockedDetail = `dependency_invalid:${assetDependency.ref}`;
607
+ break;
608
+ }
609
+ const assetRef = (0, types_1.formatContentVersionRef)(parsedAsset);
610
+ const assetKey = (0, upload_content_1.buildAssetKey)(parsedAsset.creatorUsername, parsedAsset.name);
611
+ const localDependency = pickSingleLocalDependency(assetTasksByKey.get(assetKey), assetRef);
612
+ if (localDependency.blockedDetail) {
613
+ blockedDetail = localDependency.blockedDetail;
614
+ break;
615
+ }
616
+ if (localDependency.node) {
617
+ addLocalDependency(node, localDependency.node.id, assetRef);
618
+ node.localAppAssetKeysByRef.set(assetRef, (0, upload_content_1.buildAssetKey)(localDependency.node.creator, localDependency.node.task.name));
619
+ continue;
620
+ }
621
+ if (!(await validateRemoteAssetVersionExists(client, assetRef, remoteCache))) {
622
+ blockedDetail = `dependency_missing:${assetRef}`;
623
+ break;
624
+ }
625
+ }
626
+ }
627
+ if (!blockedDetail && node.task.kind === 'app') {
628
+ for (const rawPackRef of node.task.uses.packs) {
629
+ const normalizedPackRef = normalizePackDependencyRef(rawPackRef);
630
+ if (!normalizedPackRef) {
631
+ blockedDetail = `dependency_invalid:${rawPackRef.trim() || rawPackRef}`;
632
+ break;
633
+ }
634
+ const parsedPack = (0, types_1.parseContentVersionRef)(normalizedPackRef);
635
+ if (!parsedPack || parsedPack.kind !== 'pack') {
636
+ blockedDetail = `dependency_invalid:${normalizedPackRef}`;
637
+ break;
638
+ }
639
+ const localDependency = pickSingleLocalDependency(packTasksByKey.get((0, upload_content_1.buildPackKey)(parsedPack.creatorUsername, parsedPack.name, parsedPack.version)), normalizedPackRef);
640
+ if (localDependency.blockedDetail) {
641
+ blockedDetail = localDependency.blockedDetail;
642
+ break;
643
+ }
644
+ if (localDependency.node) {
645
+ addLocalDependency(node, localDependency.node.id, normalizedPackRef);
646
+ if (localDependency.node.task.kind === 'asset-pack') {
647
+ node.localAppPackRefsByRef.set(normalizedPackRef, `pack:${localDependency.node.creator}/${localDependency.node.task.name}@${localDependency.node.task.version}`);
648
+ }
649
+ continue;
650
+ }
651
+ if (!(await validateRemotePackVersionExists(client, normalizedPackRef, remoteCache))) {
652
+ blockedDetail = `dependency_missing:${normalizedPackRef}`;
653
+ break;
654
+ }
655
+ }
656
+ }
657
+ if (blockedDetail) {
658
+ initialEntries.push({
659
+ taskId: node.id,
660
+ entry: buildTaskBlockedEntry(node.task, node.entityId, blockedDetail),
661
+ });
662
+ }
663
+ }
664
+ return {
665
+ nodes: nodes.map((entry) => entry.node),
666
+ initialEntries,
667
+ };
668
+ }
669
+ function resolveAppTaskLocalDependencies(task, node, state) {
670
+ if (node.localAppAssetKeysByRef.size === 0 && node.localAppPackRefsByRef.size === 0) {
671
+ return task;
672
+ }
673
+ const usesAssets = task.uses.assets.map((dependency) => {
674
+ const assetKey = node.localAppAssetKeysByRef.get(dependency.ref);
675
+ if (!assetKey) {
676
+ return dependency;
677
+ }
678
+ const uploaded = state.uploadedAssetsByKey.get(assetKey);
679
+ if (!uploaded) {
680
+ throw new Error(`Local asset dependency "${dependency.ref}" did not produce an uploaded asset ref.`);
681
+ }
682
+ return {
683
+ ...dependency,
684
+ ref: uploaded.ref,
685
+ };
686
+ });
687
+ const usesPacks = task.uses.packs.map((rawRef) => {
688
+ const normalized = normalizePackDependencyRef(rawRef);
689
+ if (!normalized) {
690
+ return rawRef;
691
+ }
692
+ return node.localAppPackRefsByRef.get(normalized) ?? normalized;
693
+ });
694
+ return {
695
+ ...task,
696
+ uses: {
697
+ assets: usesAssets,
698
+ packs: usesPacks,
699
+ },
700
+ };
701
+ }
185
702
  function buildAppOverviewUrl(base, creator, task) {
186
703
  const typeSlug = (0, appUrls_1.getAppTypeSlug)(task.type ?? 'GAME');
187
704
  const encodedCreator = encodeURIComponent(creator);
@@ -233,6 +750,22 @@ function buildUploadErrorDetail(error) {
233
750
  if (error instanceof types_1.ApiError && error.code === 'tag_clear_confirmation_required') {
234
751
  return 'tag_clear_confirmation_required: This publish would remove existing live tags. Re-run with --clear-tags to confirm.';
235
752
  }
753
+ if (error instanceof types_1.ApiError) {
754
+ const ref = typeof error.details?.ref === 'string' ? error.details.ref.trim() : '';
755
+ const targetKind = typeof error.details?.targetKind === 'string' ? error.details.targetKind.trim() : '';
756
+ if (error.code === 'unknown_tag_ref' && ref) {
757
+ return `unknown_tag_ref:${ref}`;
758
+ }
759
+ if (error.code === 'invalid_tag_ref' && ref) {
760
+ return `invalid_tag_ref:${ref}`;
761
+ }
762
+ if (error.code === 'duplicate_tag_ref' && ref) {
763
+ return `duplicate_tag_ref:${ref}`;
764
+ }
765
+ if (error.code === 'tag_not_allowed_for_target_kind' && ref && targetKind) {
766
+ return `tag_not_allowed_for_target_kind:${ref}:${targetKind}`;
767
+ }
768
+ }
236
769
  const rawMessage = typeof error?.message === 'string' && error.message.trim()
237
770
  ? error.message.trim()
238
771
  : 'upload failed';
@@ -370,12 +903,12 @@ async function uploadStandaloneAssetTask(state, task, taskCreator, options) {
370
903
  appendOverviewLink(entry, task, state.portalBase, uploaded.creatorUsername);
371
904
  return entry;
372
905
  }
373
- async function uploadOwnedAssetTask(state, task, taskCreator) {
906
+ async function uploadOwnedAssetTask(state, task, taskCreator, options) {
374
907
  const sourceApp = state.uploadedAppsByName.get(task.appName);
375
908
  if (!sourceApp) {
376
909
  throw new Error(`Owned asset "${task.name}" references app "${task.appName}" that was not uploaded in this run.`);
377
910
  }
378
- const uploaded = await (0, upload_content_1.uploadAssetTask)(state.client, task, sourceApp.versionId, sourceApp.creatorUsername);
911
+ const uploaded = await (0, upload_content_1.uploadAssetTask)(state.client, task, sourceApp.versionId, sourceApp.creatorUsername, { clearTags: options?.clearTags });
379
912
  state.uploadedAssetsByKey.set(`${uploaded.creatorUsername}/${uploaded.name}`, uploaded);
380
913
  (0, upload_graph_1.registerCanonicalNode)(state.graphState, uploaded.ref, uploaded.versionNodeId);
381
914
  (0, upload_graph_1.registerLocalRef)(state.graphState.localAssetNodeByName, state.graphState.ambiguousAssetNames, task.name, uploaded.versionNodeId);
@@ -446,7 +979,10 @@ async function uploadPackTask(state, task, taskCreator, options) {
446
979
  appendOverviewLink(entry, task, state.portalBase, taskCreator);
447
980
  return entry;
448
981
  }
449
- async function processSingleUploadTask(state, task, taskCreator, options) {
982
+ async function processSingleUploadTask(state, node, taskCreator, options) {
983
+ const task = node.task.kind === 'app'
984
+ ? resolveAppTaskLocalDependencies(node.task, node, state)
985
+ : node.task;
450
986
  if (task.kind === 'app') {
451
987
  return await uploadAppTask(state, task, taskCreator, options);
452
988
  }
@@ -457,7 +993,7 @@ async function processSingleUploadTask(state, task, taskCreator, options) {
457
993
  return { entry: await uploadStandaloneAssetTask(state, task, taskCreator, options), warnings: [] };
458
994
  }
459
995
  if (task.kind === 'owned-asset') {
460
- return { entry: await uploadOwnedAssetTask(state, task, taskCreator), warnings: [] };
996
+ return { entry: await uploadOwnedAssetTask(state, task, taskCreator, options), warnings: [] };
461
997
  }
462
998
  if (task.kind === 'asset-pack') {
463
999
  return { entry: await uploadPackTask(state, task, taskCreator, options), warnings: [] };
@@ -484,17 +1020,21 @@ async function flushGraphState(client, graphState, results) {
484
1020
  process.exitCode = process.exitCode || 1;
485
1021
  }
486
1022
  }
487
- async function processUploadTasks(client, tasks, owner, ownerUsername, currentUserRole, currentUser, token, warnings, apiBase, webBase, options) {
1023
+ async function processUploadTasks(client, tasks, owner, ownerUsername, currentUserRole, localTagGroups, currentUser, token, warnings, apiBase, webBase, options) {
488
1024
  const portalBase = normalizePortalBase(webBase);
489
1025
  const defaultCreator = normalizeCreatorUsername(ownerUsername) ?? owner;
490
1026
  const results = [];
491
- let aborted = false;
492
- const sortedTasks = (0, taskUtils_1.sortTasks)(tasks);
1027
+ const preflight = await buildUploadPreflight(client, tasks, defaultCreator, currentUserRole, localTagGroups);
1028
+ const sortedTasks = preflight.nodes.map((node) => node.task);
493
1029
  const packPlanning = (0, upload_content_1.buildAssetPackUploadPlans)(sortedTasks, defaultCreator, currentUserRole);
1030
+ const taskStatusById = new Map(preflight.nodes.map((node) => [node.id, 'pending']));
1031
+ preflight.initialEntries.forEach(({ taskId, entry }) => {
1032
+ taskStatusById.set(taskId, entry.status);
1033
+ pushLoggedEntry(results, entry);
1034
+ });
494
1035
  if (!packPlanning.ok) {
495
1036
  const entry = buildPackPlanningFailureEntry(packPlanning.task, defaultCreator, currentUserRole, packPlanning.message);
496
1037
  pushLoggedEntry(results, entry);
497
- process.exitCode = process.exitCode || 1;
498
1038
  return results;
499
1039
  }
500
1040
  const state = {
@@ -511,45 +1051,85 @@ async function processUploadTasks(client, tasks, owner, ownerUsername, currentUs
511
1051
  graphState: (0, upload_graph_1.buildEmptyGraphState)(),
512
1052
  packPlanning,
513
1053
  };
514
- for (const task of sortedTasks) {
515
- const creatorResult = (0, upload_content_1.getTaskCreatorResult)(task, defaultCreator, currentUserRole);
516
- if (creatorResult.requestedTaskOwner && !creatorResult.creatorTargetError) {
517
- console.log(`[Admin] Publishing ${task.name} as user: ${creatorResult.taskCreator}`);
1054
+ while (true) {
1055
+ const pendingNodes = preflight.nodes.filter((node) => taskStatusById.get(node.id) === 'pending');
1056
+ if (pendingNodes.length === 0) {
1057
+ break;
518
1058
  }
519
- const entityId = buildTaskEntityId(task, creatorResult.taskCreator);
520
- const taskCreator = creatorResult.taskCreator;
521
- try {
522
- if (creatorResult.creatorTargetError) {
523
- throw new Error(creatorResult.creatorTargetError);
1059
+ let madeProgress = false;
1060
+ for (const node of preflight.nodes) {
1061
+ if (taskStatusById.get(node.id) !== 'pending') {
1062
+ continue;
524
1063
  }
525
- const outcome = await processSingleUploadTask(state, task, taskCreator, options);
526
- outcome.warnings.forEach((warning) => warnings.add(warning));
527
- pushLoggedEntry(results, outcome.entry);
528
- }
529
- catch (error) {
530
- if (error instanceof http_1.CLIUnsupportedClientError) {
531
- throw error;
1064
+ const failedDependency = node.localDependencies.find((dependency) => {
1065
+ const dependencyStatus = taskStatusById.get(dependency.taskId);
1066
+ return dependencyStatus === 'error' || dependencyStatus === 'blocked';
1067
+ });
1068
+ if (failedDependency) {
1069
+ const entry = buildTaskBlockedEntry(node.task, node.entityId, `local_dependency_failed:${failedDependency.ref}`);
1070
+ taskStatusById.set(node.id, entry.status);
1071
+ pushLoggedEntry(results, entry);
1072
+ madeProgress = true;
1073
+ continue;
532
1074
  }
533
- const entry = buildTaskErrorEntry(task, entityId, error);
534
- pushLoggedEntry(results, entry);
535
- process.exitCode = process.exitCode || 1;
536
- aborted = true;
1075
+ const waitingOnLocalDependency = node.localDependencies.some((dependency) => taskStatusById.get(dependency.taskId) === 'pending');
1076
+ if (waitingOnLocalDependency) {
1077
+ continue;
1078
+ }
1079
+ const creatorResult = (0, upload_content_1.getTaskCreatorResult)(node.task, defaultCreator, currentUserRole);
1080
+ if (creatorResult.requestedTaskOwner && !creatorResult.creatorTargetError) {
1081
+ console.log(`[Admin] Publishing ${node.task.name} as user: ${creatorResult.taskCreator}`);
1082
+ }
1083
+ try {
1084
+ const outcome = await processSingleUploadTask(state, node, node.creator, options);
1085
+ outcome.warnings.forEach((warning) => warnings.add(warning));
1086
+ taskStatusById.set(node.id, outcome.entry.status);
1087
+ pushLoggedEntry(results, outcome.entry);
1088
+ }
1089
+ catch (error) {
1090
+ if (error instanceof http_1.CLIUnsupportedClientError) {
1091
+ throw error;
1092
+ }
1093
+ const entry = buildTaskErrorEntry(node.task, node.entityId, error);
1094
+ taskStatusById.set(node.id, entry.status);
1095
+ pushLoggedEntry(results, entry);
1096
+ }
1097
+ madeProgress = true;
537
1098
  break;
538
1099
  }
1100
+ if (madeProgress) {
1101
+ continue;
1102
+ }
1103
+ for (const node of pendingNodes) {
1104
+ const entry = buildTaskDirectErrorEntry(node.task, node.entityId, 'dependency_cycle_detected');
1105
+ taskStatusById.set(node.id, entry.status);
1106
+ pushLoggedEntry(results, entry);
1107
+ }
539
1108
  }
540
- if (!aborted) {
541
- await flushGraphState(client, state.graphState, results);
542
- }
1109
+ await flushGraphState(client, state.graphState, results);
543
1110
  return results;
544
1111
  }
1112
+ function printResolvedUploadAccount(input) {
1113
+ const username = typeof input.username === 'string' && input.username.trim().length > 0
1114
+ ? input.username.trim()
1115
+ : 'unknown';
1116
+ console.log(`Resolved account: ${username} (${input.env})`);
1117
+ }
545
1118
  async function upload(pathOrName, options) {
546
1119
  const selection = (0, taskSelection_1.selectTasks)(pathOrName);
547
1120
  const workspaceRoot = resolveUploadWorkspaceRoot(pathOrName, selection.resolution);
548
- const ctx = await (0, commandContext_1.resolveAuthenticatedEnvironmentContext)('project publish', 'Publishing content', { workspacePath: workspaceRoot });
1121
+ const ctx = await (0, commandContext_1.resolveAuthenticatedEnvironmentContext)('project publish', 'Publishing content', {
1122
+ env: options?.env,
1123
+ workspacePath: workspaceRoot,
1124
+ });
549
1125
  if (!ctx) {
550
1126
  return;
551
1127
  }
552
1128
  const { client, env, envConfig } = ctx;
1129
+ printResolvedUploadAccount({
1130
+ env,
1131
+ username: ctx.account?.username,
1132
+ });
553
1133
  let userInfo = { username: null, role: null, user: null };
554
1134
  try {
555
1135
  userInfo = await fetchCurrentUserInfo(client, envConfig.apiBase);
@@ -600,7 +1180,7 @@ async function upload(pathOrName, options) {
600
1180
  taxonomyEntries.forEach((entry) => pushLoggedEntry(results, entry));
601
1181
  }
602
1182
  if (tasks.length > 0) {
603
- const uploadEntries = await processUploadTasks(client, tasks, owner, userInfo.username, userInfo.role, userInfo.user, ctx.token, warnings, envConfig.apiBase, envConfig.webBase, options);
1183
+ const uploadEntries = await processUploadTasks(client, tasks, owner, userInfo.username, userInfo.role, tagGroupLoad.groups, userInfo.user, ctx.token, warnings, envConfig.apiBase, envConfig.webBase, options);
604
1184
  results.push(...uploadEntries);
605
1185
  }
606
1186
  (0, uploadLog_1.printTaskSummary)(results, warnings, { action: 'upload', environment: env });