@helix3/helix-cli 0.1.13-helix3.85 → 0.1.13-helix3.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,7 +44,8 @@ The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
44
44
  | `helix publish [dir] [--thumbnail <file>] [--upload-source] [--source-dir <path>]` | Validate → resolve/create the world → upload files → finalize → set the cover image → print the play URL. `--upload-source` additionally sends the project's source through a separate private channel so the world can be edited on the website later — **off by default**. |
45
45
  | `helix list` | List your worlds. |
46
46
  | `helix item list-slots [--json]` | Print the Character-Creator vocabulary a wearable declares: the cosmetic slot tags, and the genders. |
47
- | `helix item publish <mesh.glb> --title <t> [--kind <k>] [--slot <tag>] [--gender <g>] [--price-lix <n> \| --personal] [--thumbnail <f>] [--dry-run]` | Publish a universal item (wearable / avatar / home item) from one `.glb`; the server verifies the mesh inline. |
47
+ | `helix item publish <mesh.glb> --title <t> [--collectible-supply <n>] [initial distribution flags] [--dry-run\|--quote]` | Publish a Standard item by default, or one fixed-supply Collectible. The item definition is separate from acquisition routes. |
48
+ | `helix item distribution create\|list\|update\|disable …` | Manage Marketplace/World Claim or Buy routes without republishing the item or paying another publish fee. |
48
49
  | `helix preview-video <slug> <youtube-url>` | Set or clear a published world's YouTube preview. |
49
50
  | `helix thumbnail set <slug> <file>` | Upload a thumbnail or preview image through the CLI-owned media path. |
50
51
  | `helix assets search\|get\|versions\|track\|install\|update` | Search the typed Vault with explainable ranking; inspect/manage annotations and immutable versions; install by durable ID with verified SHA-256 receipts. Material installs default to lean runtime KTX2 maps; use `--material-renditions source` for source PNGs or `all` for both families. |
@@ -239,19 +240,63 @@ would filter nothing.
239
240
 
240
241
  ## Item publish flow
241
242
 
242
- `helix item publish` is a different shape from a world publish: one mesh, one round trip, an
243
- inline verdict which is what an editor (Helix Studio) needs when an artist clicks Publish.
243
+ `helix item publish` creates an item definition. It defaults to **Standard**: unlimited issuance,
244
+ no public serial, and no resale. `--collectible-supply N` is the only edition selector: it fixes a
245
+ positive supply, assigns public serials, makes instances Marketplace-resellable, and reserves
246
+ serial `#1` for the creator. There are no separate tradable, serial, giftable, personal, or
247
+ discoverability switches.
244
248
 
245
249
  1. Local validation, before anything leaves the machine: the `--kind`, the Character-Creator
246
250
  `--slot` and `--gender` (**both required** for a wearable, exact-match), the title/slug/tag
247
251
  limits, and the GLB container magic. `--dry-run` stops here and prints exactly what would be
248
252
  sent.
249
- 2. `POST /api/v1/universal-items/upload` `multipart/form-data` with a `mesh` file part, an
253
+ 2. `--quote` asks the server for the definition publish fee, the creator tier's included free-
254
+ Collectible units, excess issuance authorization cost, creator serial `#1`, price floor and
255
+ resale policy. It uploads nothing and charges nothing.
256
+ 3. `POST /api/v1/universal-items/upload` — `multipart/form-data` with a `mesh` file part, an
250
257
  optional `thumbnail` file part, and the metadata as ONE JSON string in `payload`.
251
- 3. The server verifies the mesh **inline** (triangles, texture edges, materials, and — for a
258
+ 4. The server verifies the mesh **inline** (triangles, texture edges, materials, and — for a
252
259
  wearable — whether it is a skinned garment or a rigid socketed accessory) and returns the
253
260
  created item together with the verification verdict and warnings.
254
261
 
262
+ Price, world, schedule and limits live on a separate distribution. Price `0` means a shell-
263
+ confirmed **Claim**; a positive price means **Buy** and must meet the server-owned floor. A world
264
+ route also has a stable key, which world code passes to
265
+ `Helix.marketplace.purchaseDistributionKey(key)`. `--max-per-player unlimited` explicitly clears a
266
+ cap; free Collectibles default to one per player when omitted. Distribution changes never republish
267
+ the definition.
268
+
269
+ ```bash
270
+ # Standard, free Marketplace Claim
271
+ helix item publish ./postcard.glb --title "Harbor Postcard"
272
+ helix item distribution create <item-id> --channel marketplace --price-lix 0
273
+
274
+ # Standard, paid World Buy
275
+ helix item distribution create <item-id> --channel world --world fishing-cove \
276
+ --key fishing_rod --price-lix 250 --max-per-player unlimited
277
+
278
+ # Collectible, free World Claim (creator receives #1 from the 500 fixed units)
279
+ helix item publish ./trophy.glb --title "Season One Trophy" --collectible-supply 500 \
280
+ --distribution-channel world --distribution-price-lix 0 --world tournament \
281
+ --distribution-key season_one --max-per-player 1 --quote
282
+ # Review the quote, then run the same command without --quote to publish.
283
+
284
+ # Collectible, paid Marketplace Buy
285
+ helix item publish ./bluefin.glb --title "Legendary Bluefin" --collectible-supply 10000
286
+ helix item distribution create <item-id> --channel marketplace --price-lix 500 --max-claims 9000
287
+
288
+ helix item distribution update <distribution-id> --price-lix 600 --ends-at 2026-12-01T00:00:00Z
289
+ helix item distribution disable <distribution-id>
290
+ ```
291
+
292
+ Collectible resale uses Marketplace escrow. The creator royalty is fixed at 5%; the launch seller
293
+ platform fee is 10% for Free and 5% for Plus (future Gold 2.5%, Diamond 0%). The buyer pays no
294
+ surcharge, creator self-sale omits a redundant royalty leg, and self-purchase is prohibited.
295
+ Every acquisition has one 24-hour relisting cooldown. Basic requires email; Verified means a unique
296
+ verified mobile plus good standing and may resell immediately subject to that cooldown. Identity Verified
297
+ is reserved for later high-risk capability, and tooling accepts future Business Verified. Payout holds are
298
+ separate: 3 days base, risk-extendable up to 15 days.
299
+
255
300
  The slot table in `src/item.ts` MIRRORS `helix-backend-api → src/universal-items/cc-wearable-slot.ts`,
256
301
  and the gender list mirrors `src/universal-items/cc-gender.ts` the same way. They are a local fast
257
302
  path so a typo costs no upload; the server remains the authority and its 400 lists every valid
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Canonical universal-item distribution vocabulary.
3
+ *
4
+ * An item definition says what the item is. A distribution is one creator-
5
+ * authorized primary acquisition route. Keeping this builder network-free
6
+ * lets `--dry-run` exercise exactly the payload the real command sends.
7
+ */
8
+ export declare const ITEM_EDITION_MODES: readonly ["standard", "collectible"];
9
+ export type ItemEditionMode = (typeof ITEM_EDITION_MODES)[number];
10
+ export declare const ITEM_DISTRIBUTION_CHANNELS: readonly ["marketplace", "world"];
11
+ export type ItemDistributionChannel = (typeof ITEM_DISTRIBUTION_CHANNELS)[number];
12
+ export declare const DISTRIBUTION_KEY_PATTERN: RegExp;
13
+ export type ItemDistributionInput = {
14
+ itemId?: string;
15
+ channel: ItemDistributionChannel;
16
+ priceLix: number;
17
+ worldId?: string;
18
+ worldSlug?: string;
19
+ key?: string;
20
+ maxPerPlayer?: number | null;
21
+ maxClaims?: number | null;
22
+ startsAt?: string | null;
23
+ endsAt?: string | null;
24
+ };
25
+ export type ItemDistributionFields = {
26
+ itemId?: string;
27
+ channel: ItemDistributionChannel;
28
+ priceLix: number;
29
+ worldId?: string;
30
+ /** Offline plan only; the CLI resolves it to worldId before a network write. */
31
+ worldSlug?: string;
32
+ key?: string;
33
+ maxPerPlayer?: number | null;
34
+ maxClaims?: number | null;
35
+ startsAt?: string | null;
36
+ endsAt?: string | null;
37
+ };
38
+ export type ItemDistributionUpdateInput = {
39
+ priceLix?: number;
40
+ maxPerPlayer?: number | null;
41
+ maxClaims?: number | null;
42
+ startsAt?: string | null;
43
+ endsAt?: string | null;
44
+ };
45
+ export type ItemDistributionPatch = ItemDistributionUpdateInput;
46
+ export declare function buildItemDistribution(input: ItemDistributionInput): ItemDistributionFields;
47
+ export declare function buildItemDistributionUpdate(input: ItemDistributionUpdateInput): ItemDistributionPatch;
48
+ export declare function editionFields(collectibleSupply?: number): {
49
+ editionMode: ItemEditionMode;
50
+ collectibleSupply?: number;
51
+ };
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ /**
3
+ * Canonical universal-item distribution vocabulary.
4
+ *
5
+ * An item definition says what the item is. A distribution is one creator-
6
+ * authorized primary acquisition route. Keeping this builder network-free
7
+ * lets `--dry-run` exercise exactly the payload the real command sends.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.DISTRIBUTION_KEY_PATTERN = exports.ITEM_DISTRIBUTION_CHANNELS = exports.ITEM_EDITION_MODES = void 0;
11
+ exports.buildItemDistribution = buildItemDistribution;
12
+ exports.buildItemDistributionUpdate = buildItemDistributionUpdate;
13
+ exports.editionFields = editionFields;
14
+ exports.ITEM_EDITION_MODES = ['standard', 'collectible'];
15
+ exports.ITEM_DISTRIBUTION_CHANNELS = ['marketplace', 'world'];
16
+ exports.DISTRIBUTION_KEY_PATTERN = /^[a-z0-9](?:[a-z0-9_-]{0,62}[a-z0-9])?$/;
17
+ const positiveInt = (name, value) => {
18
+ if (value === undefined || value === null)
19
+ return;
20
+ if (!Number.isInteger(value) || value < 1) {
21
+ throw new Error(`${name} must be a whole number >= 1, or unlimited`);
22
+ }
23
+ };
24
+ const price = (value) => {
25
+ if (!Number.isFinite(value) || value < 0) {
26
+ throw new Error(`--price-lix must be a number >= 0 LIX (got ${value})`);
27
+ }
28
+ return value;
29
+ };
30
+ const timestamp = (name, value) => {
31
+ if (value === undefined || value === null)
32
+ return value;
33
+ const parsed = new Date(value);
34
+ if (!Number.isFinite(parsed.valueOf())) {
35
+ throw new Error(`${name} must be an ISO-8601 timestamp (got "${value}")`);
36
+ }
37
+ return parsed.toISOString();
38
+ };
39
+ function buildItemDistribution(input) {
40
+ if (!exports.ITEM_DISTRIBUTION_CHANNELS.includes(input.channel)) {
41
+ throw new Error(`--channel must be marketplace or world (got "${input.channel}")`);
42
+ }
43
+ const priceLix = price(input.priceLix);
44
+ positiveInt('--max-per-player', input.maxPerPlayer);
45
+ positiveInt('--max-claims', input.maxClaims);
46
+ const startsAt = timestamp('--starts-at', input.startsAt);
47
+ const endsAt = timestamp('--ends-at', input.endsAt);
48
+ if (startsAt && endsAt && new Date(startsAt) >= new Date(endsAt)) {
49
+ throw new Error('--ends-at must be later than --starts-at');
50
+ }
51
+ const worldId = input.worldId?.trim();
52
+ const worldSlug = input.worldSlug?.trim();
53
+ const key = input.key?.trim();
54
+ if (input.channel === 'world') {
55
+ if (!worldId && !worldSlug) {
56
+ throw new Error('a world distribution requires --world <slug>');
57
+ }
58
+ if (!key)
59
+ throw new Error('a world distribution requires --distribution-key <key>');
60
+ if (!exports.DISTRIBUTION_KEY_PATTERN.test(key)) {
61
+ throw new Error(`--distribution-key "${key}" must use lowercase letters, digits, _ or - (1-64 characters)`);
62
+ }
63
+ }
64
+ else if (worldId || input.worldSlug || key) {
65
+ throw new Error('a marketplace distribution cannot carry --world or --distribution-key');
66
+ }
67
+ return {
68
+ ...(input.itemId ? { itemId: input.itemId } : {}),
69
+ channel: input.channel,
70
+ priceLix,
71
+ ...(worldId ? { worldId } : {}),
72
+ ...(!worldId && worldSlug ? { worldSlug } : {}),
73
+ ...(key ? { key } : {}),
74
+ ...(input.maxPerPlayer !== undefined ? { maxPerPlayer: input.maxPerPlayer } : {}),
75
+ ...(input.maxClaims !== undefined ? { maxClaims: input.maxClaims } : {}),
76
+ ...(startsAt !== undefined ? { startsAt } : {}),
77
+ ...(endsAt !== undefined ? { endsAt } : {}),
78
+ };
79
+ }
80
+ function buildItemDistributionUpdate(input) {
81
+ if (input.priceLix === undefined && input.maxPerPlayer === undefined &&
82
+ input.maxClaims === undefined && input.startsAt === undefined && input.endsAt === undefined) {
83
+ throw new Error('no distribution changes supplied');
84
+ }
85
+ if (input.priceLix !== undefined)
86
+ price(input.priceLix);
87
+ positiveInt('--max-per-player', input.maxPerPlayer);
88
+ positiveInt('--max-claims', input.maxClaims);
89
+ const startsAt = timestamp('--starts-at', input.startsAt);
90
+ const endsAt = timestamp('--ends-at', input.endsAt);
91
+ if (startsAt && endsAt && new Date(startsAt) >= new Date(endsAt)) {
92
+ throw new Error('--ends-at must be later than --starts-at');
93
+ }
94
+ return {
95
+ ...(input.priceLix !== undefined ? { priceLix: input.priceLix } : {}),
96
+ ...(input.maxPerPlayer !== undefined ? { maxPerPlayer: input.maxPerPlayer } : {}),
97
+ ...(input.maxClaims !== undefined ? { maxClaims: input.maxClaims } : {}),
98
+ ...(startsAt !== undefined ? { startsAt } : {}),
99
+ ...(endsAt !== undefined ? { endsAt } : {}),
100
+ };
101
+ }
102
+ function editionFields(collectibleSupply) {
103
+ if (collectibleSupply === undefined)
104
+ return { editionMode: 'standard' };
105
+ if (!Number.isInteger(collectibleSupply) || collectibleSupply < 1) {
106
+ throw new Error('--collectible-supply must be a whole number >= 1');
107
+ }
108
+ return { editionMode: 'collectible', collectibleSupply };
109
+ }
110
+ //# sourceMappingURL=distribution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distribution.js","sourceRoot":"","sources":["../src/distribution.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -524,6 +524,56 @@ function parsePriceLix(value) {
524
524
  fail(`✖ --price-lix must be a number, got '${value}'`);
525
525
  return price;
526
526
  }
527
+ function parsePositiveInt(value, flag) {
528
+ const parsed = Number(value);
529
+ if (!Number.isInteger(parsed) || parsed < 1)
530
+ fail(`✖ ${flag} must be a whole number >= 1`);
531
+ return parsed;
532
+ }
533
+ function parseLimit(value, flag) {
534
+ if (value.trim().toLowerCase() === 'unlimited')
535
+ return null;
536
+ return parsePositiveInt(value, flag);
537
+ }
538
+ function renderItemQuote(q) {
539
+ const lines = [
540
+ ` edition: ${q.editionMode}${q.collectibleSupply !== null ? ` · fixed supply ${q.collectibleSupply} · creator serial #${q.creatorSerial ?? 1}` : ' · unlimited · no serials'}`,
541
+ ` publish total: ${q.publishFeeLix} LIX`,
542
+ ` review fee: ${q.reviewFeeLix} LIX · normally non-refundable`,
543
+ ` compliance bond: ${q.complianceBondLix} LIX · returned after approval + ${q.complianceBondReleaseDays} days in good standing`,
544
+ ];
545
+ if (q.editionMode === 'collectible') {
546
+ lines.push(` free Collectible allowance: ${q.includedCollectibleUnits} included · ${q.chargeableCollectibleUnits} additionally authorized`, ` issuance authorization: ${q.issuanceAuthorizationFeeLix} LIX`, ` resale: ${q.creatorRoyaltyPercent}% creator royalty · seller fee ${q.sellerPlatformFeePercentByTier.free}% Free / ${q.sellerPlatformFeePercentByTier.plus}% Plus (future ${q.sellerPlatformFeePercentByTier.gold}% Gold / ${q.sellerPlatformFeePercentByTier.diamond}% Diamond)`, ` buyer surcharge: ${q.buyerSurchargePercent}% · self-purchase prohibited · ${q.resaleCooldownHours}h relisting cooldown`, ` resale eligibility: Basic email; Verified unique mobile + good standing (immediate); Identity Verified reserved for later high-risk capability; Business Verified supported`, ` payout hold: ${q.payoutHoldBaseDays} days base · risk-extension up to ${q.payoutHoldMaxDays} days`);
547
+ }
548
+ if (q.distribution) {
549
+ lines.push(` initial ${q.distribution.channel}: ${q.distribution.priceLix === 0 ? 'Claim (free)' : `Buy at ${q.distribution.priceLix} LIX`} · floor ${q.distribution.priceFloorLix} LIX`);
550
+ }
551
+ lines.push(` total due now: ${q.totalDueLix} LIX`);
552
+ return lines;
553
+ }
554
+ function initialDistributionFromFlags(opts) {
555
+ const any = opts.distributionChannel !== undefined || opts.distributionPriceLix !== undefined ||
556
+ opts.world !== undefined || opts.distributionKey !== undefined || opts.maxPerPlayer !== undefined ||
557
+ opts.maxClaims !== undefined || opts.startsAt !== undefined || opts.endsAt !== undefined;
558
+ if (!any)
559
+ return undefined;
560
+ if (!opts.distributionChannel)
561
+ fail('✖ initial distribution flags require --distribution-channel');
562
+ if (opts.distributionPriceLix === undefined)
563
+ fail('✖ an initial distribution requires --distribution-price-lix');
564
+ const input = {
565
+ channel: opts.distributionChannel,
566
+ priceLix: parsePriceLix(opts.distributionPriceLix),
567
+ worldSlug: opts.world,
568
+ key: opts.distributionKey,
569
+ maxPerPlayer: opts.maxPerPlayer !== undefined ? parseLimit(opts.maxPerPlayer, '--max-per-player') : undefined,
570
+ maxClaims: opts.maxClaims !== undefined ? parseLimit(opts.maxClaims, '--max-claims') : undefined,
571
+ startsAt: opts.startsAt,
572
+ endsAt: opts.endsAt,
573
+ };
574
+ (0, lib_1.buildItemDistribution)(input);
575
+ return input;
576
+ }
527
577
  item
528
578
  .command('publish')
529
579
  .description('Publish a universal item from a local .glb — the server verifies the mesh inline and returns the verdict in the same round trip')
@@ -541,11 +591,19 @@ item
541
591
  .option('--slug <slug>', 'url slug (default: derived from the title)')
542
592
  .option('--description <text>', 'item description')
543
593
  .option('--thumbnail <file>', 'thumbnail image (png/jpg/webp); rendered from the mesh when omitted, required for an avatar')
544
- .option('--price-lix <n>', 'list on the marketplace at this LIX price (charges a non-refundable publish fee)')
545
- .option('--personal', 'keep it private and free instead of listing it (no publish fee)')
594
+ .option('--collectible-supply <n>', 'select Collectible edition with this fixed positive supply; omitted means Standard')
595
+ .option('--distribution-channel <channel>', 'create one initial route: marketplace or world')
596
+ .option('--distribution-price-lix <n>', 'initial route price; 0 is Claim, positive is Buy')
597
+ .option('--world <slug>', 'world that owns an initial world route')
598
+ .option('--distribution-key <key>', 'stable key world code passes to purchaseDistributionKey')
599
+ .option('--max-per-player <n|unlimited>', 'initial route per-player cap; free Collectibles default to 1 server-side')
600
+ .option('--max-claims <n|unlimited>', 'initial route-wide issuance cap')
601
+ .option('--starts-at <iso>', 'initial route activation time (ISO-8601)')
602
+ .option('--ends-at <iso>', 'initial route end time (ISO-8601)')
546
603
  .option('--tags <a,b,c>', 'comma-separated search keywords')
547
604
  .option('--allow-oversize', 'relax the mesh size/polycount ceilings for a deliberately heavy asset')
548
605
  .option('--dry-run', 'validate everything locally and print what WOULD be sent; no upload, no fee')
606
+ .option('--quote', 'request the server-owned publish/issuance quote; no upload or fee')
549
607
  .option('--json', 'machine-readable output')
550
608
  .action(async (mesh, opts) => {
551
609
  const publishOptions = {
@@ -557,11 +615,15 @@ item
557
615
  genders: opts.gender !== undefined ? (0, lib_1.parseGenders)(opts.gender) : undefined,
558
616
  slug: opts.slug,
559
617
  description: opts.description,
560
- priceLix: opts.priceLix !== undefined ? parsePriceLix(opts.priceLix) : null,
561
- keepPersonal: opts.personal === true,
618
+ collectibleSupply: opts.collectibleSupply !== undefined
619
+ ? parsePositiveInt(opts.collectibleSupply, '--collectible-supply')
620
+ : undefined,
621
+ initialDistribution: initialDistributionFromFlags(opts),
562
622
  tags: opts.tags !== undefined ? (0, lib_1.parseTags)(opts.tags) : undefined,
563
623
  allowOversize: opts.allowOversize === true,
564
624
  };
625
+ if (opts.dryRun && opts.quote)
626
+ fail('✖ choose --dry-run (offline) or --quote (server-owned fees), not both');
565
627
  // --dry-run stops before ANY network call, so it works logged out — the
566
628
  // point is to check the mesh and the metadata, not the account.
567
629
  if (opts.dryRun) {
@@ -594,6 +656,23 @@ item
594
656
  }
595
657
  return;
596
658
  }
659
+ if (opts.quote) {
660
+ const creds = requireAuth();
661
+ try {
662
+ const quote = await (0, lib_1.quoteItemPublish)(publishOptions, creds);
663
+ if (opts.json)
664
+ console.log(JSON.stringify({ quote }, null, 2));
665
+ else {
666
+ console.log('✔ Publish quote — nothing uploaded or charged.');
667
+ for (const line of renderItemQuote(quote))
668
+ console.log(line);
669
+ }
670
+ }
671
+ catch (err) {
672
+ fail(err instanceof lib_1.ApiError ? err.render() : `✖ ${err instanceof Error ? err.message : String(err)}`);
673
+ }
674
+ return;
675
+ }
597
676
  const creds = requireAuth();
598
677
  const mismatch = (0, config_1.credsEnvMismatchWarning)(creds.apiUrl);
599
678
  if (mismatch)
@@ -614,9 +693,11 @@ item
614
693
  if (result.genders.length)
615
694
  console.log(` gender: ${result.genders.join(', ')}`);
616
695
  console.log(` status: ${result.status}`);
617
- console.log(result.priceLix === null
618
- ? ' price: (personal not listed)'
619
- : ` price: ${result.priceLix} LIX (publish fee ${result.publishFeeLix} LIX)`);
696
+ console.log(` edition: ${result.editionMode}${result.collectibleSupply !== null ? ` · supply ${result.collectibleSupply} · creator serial #${result.creatorSerial ?? 1}` : ''}`);
697
+ console.log(` publish fee: ${result.publishFeeLix} LIX`);
698
+ if (result.initialDistribution) {
699
+ console.log(` route: ${result.initialDistribution.channel} · ${result.initialDistribution.priceLix === 0 ? 'Claim' : `Buy ${result.initialDistribution.priceLix} LIX`}`);
700
+ }
620
701
  console.log(` id: ${result.id}`);
621
702
  for (const warning of result.verification.warnings)
622
703
  console.log(` ⚠ ${warning}`);
@@ -627,6 +708,145 @@ item
627
708
  : `✖ ${err instanceof Error ? err.message : String(err)}`);
628
709
  }
629
710
  });
711
+ const itemDistribution = item
712
+ .command('distribution')
713
+ .description('Create and manage primary Claim/Buy routes for one published item definition');
714
+ const renderDistribution = (d) => [
715
+ `${d.id} · ${d.channel}${d.worldSlug ? `:${d.worldSlug}/${d.key}` : ''}`,
716
+ `${d.priceLix === 0 ? 'Claim' : `Buy ${d.priceLix} LIX`}${d.active ? '' : ' · DISABLED'}`,
717
+ `max/player ${d.maxPerPlayer ?? 'unlimited'} · max claims ${d.maxClaims ?? 'unlimited'}`,
718
+ ].join(' · ');
719
+ itemDistribution
720
+ .command('create')
721
+ .description('Create one creator-authorized primary acquisition route; no second publish fee')
722
+ .argument('<item-id>', 'published universal item definition id')
723
+ .requiredOption('--channel <marketplace|world>', 'route channel')
724
+ .requiredOption('--price-lix <n>', '0 is Claim; positive is Buy and must meet the server-owned floor')
725
+ .option('--world <slug>', 'required for a world route')
726
+ .option('--key <key>', 'stable world route key')
727
+ .option('--max-per-player <n|unlimited>', 'per-player cap')
728
+ .option('--max-claims <n|unlimited>', 'distribution-wide cap')
729
+ .option('--starts-at <iso>', 'activation time')
730
+ .option('--ends-at <iso>', 'end time')
731
+ .option('--dry-run', 'validate and print the request; no network')
732
+ .option('--json', 'machine-readable output')
733
+ .action(async (itemId, opts) => {
734
+ const input = {
735
+ itemId,
736
+ channel: opts.channel,
737
+ priceLix: parsePriceLix(opts.priceLix),
738
+ worldSlug: opts.world,
739
+ key: opts.key,
740
+ maxPerPlayer: opts.maxPerPlayer !== undefined ? parseLimit(opts.maxPerPlayer, '--max-per-player') : undefined,
741
+ maxClaims: opts.maxClaims !== undefined ? parseLimit(opts.maxClaims, '--max-claims') : undefined,
742
+ startsAt: opts.startsAt,
743
+ endsAt: opts.endsAt,
744
+ };
745
+ try {
746
+ const fields = (0, lib_1.buildItemDistribution)(input);
747
+ if (opts.dryRun) {
748
+ const out = { dryRun: true, endpoint: '/api/v1/item-distributions', fields };
749
+ if (opts.json)
750
+ console.log(JSON.stringify(out, null, 2));
751
+ else
752
+ console.log(`✔ Dry run — nothing sent.\n POST ${out.endpoint}\n ${JSON.stringify(fields)}`);
753
+ return;
754
+ }
755
+ const created = await (0, lib_1.createItemDistribution)(input, requireAuth());
756
+ if (opts.json)
757
+ console.log(JSON.stringify(created, null, 2));
758
+ else
759
+ console.log(`✔ Created ${created.priceLix === 0 ? 'Claim' : 'Buy'} route.\n ${renderDistribution(created)}`);
760
+ }
761
+ catch (err) {
762
+ fail(err instanceof lib_1.ApiError ? err.render() : `✖ ${err instanceof Error ? err.message : String(err)}`);
763
+ }
764
+ });
765
+ itemDistribution
766
+ .command('list')
767
+ .description('List every active or disabled primary route drawing from an item')
768
+ .argument('<item-id>')
769
+ .option('--json', 'machine-readable output')
770
+ .action(async (itemId, opts) => {
771
+ try {
772
+ const rows = await (0, lib_1.listItemDistributions)(itemId, requireAuth());
773
+ if (opts.json)
774
+ console.log(JSON.stringify({ distributions: rows }, null, 2));
775
+ else if (!rows.length)
776
+ console.log(`No distributions exist for ${itemId}.`);
777
+ else
778
+ console.log(rows.map(renderDistribution).join('\n'));
779
+ }
780
+ catch (err) {
781
+ fail(err instanceof lib_1.ApiError ? err.render() : `✖ ${err instanceof Error ? err.message : String(err)}`);
782
+ }
783
+ });
784
+ const clearableTimestamp = (value) => value?.toLowerCase() === 'none' ? null : value;
785
+ itemDistribution
786
+ .command('update')
787
+ .description('Reprice or change schedule/caps. Item, channel, world and key remain fixed')
788
+ .argument('<distribution-id>')
789
+ .option('--price-lix <n>')
790
+ .option('--max-per-player <n|unlimited>')
791
+ .option('--max-claims <n|unlimited>')
792
+ .option('--starts-at <iso|none>', 'replace or clear the activation time')
793
+ .option('--ends-at <iso|none>', 'replace or clear the end time')
794
+ .option('--dry-run', 'validate and print the patch; no network')
795
+ .option('--json', 'machine-readable output')
796
+ .action(async (id, opts) => {
797
+ const input = {
798
+ priceLix: opts.priceLix !== undefined ? parsePriceLix(opts.priceLix) : undefined,
799
+ maxPerPlayer: opts.maxPerPlayer !== undefined ? parseLimit(opts.maxPerPlayer, '--max-per-player') : undefined,
800
+ maxClaims: opts.maxClaims !== undefined ? parseLimit(opts.maxClaims, '--max-claims') : undefined,
801
+ startsAt: clearableTimestamp(opts.startsAt),
802
+ endsAt: clearableTimestamp(opts.endsAt),
803
+ };
804
+ try {
805
+ const patch = (0, lib_1.buildItemDistributionUpdate)(input);
806
+ if (opts.dryRun) {
807
+ const out = { dryRun: true, endpoint: `/api/v1/item-distributions/${id}`, patch };
808
+ if (opts.json)
809
+ console.log(JSON.stringify(out, null, 2));
810
+ else
811
+ console.log(`✔ Dry run — nothing sent.\n PATCH ${out.endpoint}\n ${JSON.stringify(patch)}`);
812
+ return;
813
+ }
814
+ const updated = await (0, lib_1.updateItemDistribution)(id, input, requireAuth());
815
+ if (opts.json)
816
+ console.log(JSON.stringify(updated, null, 2));
817
+ else
818
+ console.log(`✔ Updated distribution.\n ${renderDistribution(updated)}`);
819
+ }
820
+ catch (err) {
821
+ fail(err instanceof lib_1.ApiError ? err.render() : `✖ ${err instanceof Error ? err.message : String(err)}`);
822
+ }
823
+ });
824
+ itemDistribution
825
+ .command('disable')
826
+ .description('Disable an acquisition route without deleting its history or republishing the item')
827
+ .argument('<distribution-id>')
828
+ .option('--dry-run', 'print the request; no network')
829
+ .option('--json', 'machine-readable output')
830
+ .action(async (id, opts) => {
831
+ if (opts.dryRun) {
832
+ const out = { dryRun: true, endpoint: `/api/v1/item-distributions/${id}/disable` };
833
+ if (opts.json)
834
+ console.log(JSON.stringify(out, null, 2));
835
+ else
836
+ console.log(`✔ Dry run — nothing sent.\n POST ${out.endpoint}`);
837
+ return;
838
+ }
839
+ try {
840
+ const disabled = await (0, lib_1.disableItemDistribution)(id, requireAuth());
841
+ if (opts.json)
842
+ console.log(JSON.stringify(disabled, null, 2));
843
+ else
844
+ console.log(`✔ Disabled distribution.\n ${renderDistribution(disabled)}`);
845
+ }
846
+ catch (err) {
847
+ fail(err instanceof lib_1.ApiError ? err.render() : `✖ ${err instanceof Error ? err.message : String(err)}`);
848
+ }
849
+ });
630
850
  // --- helix achievement -----------------------------------------------------
631
851
  // A world's achievement registry lives OUTSIDE its bundle — helix.json cannot
632
852
  // declare one and publish cannot check that a key exists, so a rule awarding an
@@ -847,7 +1067,7 @@ achievement
847
1067
  // they cannot enter. Worlds are named by SLUG, products by their key.
848
1068
  const product = program
849
1069
  .command('product')
850
- .description("Register and manage a world's in-world products (what Helix.purchase sells). Registration works on a Draft (`helix world create` first); buying needs the world Published");
1070
+ .description("Register world-local consumables, passes and currencies. Universal items use `helix item distribution` instead");
851
1071
  function renderProduct(p, indent = ' ') {
852
1072
  // An authored-grants product's stored type is server-DERIVED — the effect list is the truth,
853
1073
  // so the "your world applies it" hint only fits legacy consumables.
@@ -897,12 +1117,11 @@ product
897
1117
  .argument('<world-slug>', 'the world that sells it')
898
1118
  .requiredOption('--title <title>', 'name the purchase popup shows the player (max 120 chars)')
899
1119
  .requiredOption('--price <lix>', 'price in LIX; 0 registers a free "Claim" (a confirmation, no spend)')
900
- .option('--type <type>', `what the purchase does: ${lib_1.WORLD_PRODUCT_TYPES.join(' | ')} — required unless you author --grants`)
901
- .option('--grants <json>', `JSON array of what the purchase hands over, instead of --type/--item-id: item | pass | currency, max ${lib_1.MAX_GRANT_EFFECTS} ` +
1120
+ .option('--type <type>', 'legacy world-local effect type: consumable (required unless you author --grants)')
1121
+ .option('--grants <json>', `JSON array of world-local entitlements instead of --type: pass | currency, max ${lib_1.MAX_GRANT_EFFECTS} ` +
902
1122
  "(e.g. '[{\"kind\":\"currency\",\"code\":\"coin\",\"amount\":500}]'; [] = a tip jar). Fixed at registration")
903
1123
  .option('--key <key>', 'stable slug world code buys as `pkey:<key>` (max 64 chars, lowercase a-z0-9_-); it cannot be renamed later')
904
1124
  .option('--description <text>', 'what the player is buying')
905
- .option('--item-id <uuid>', 'the universal item handed over — REQUIRED for item_grant and non_consumable, refused for consumable')
906
1125
  .option('--max-per-user <n>', 'cap on how many one player may own (whole number >= 1)')
907
1126
  .option('--dry-run', 'validate everything locally and print what WOULD be sent; no network, works logged out')
908
1127
  .option('--json', 'machine-readable output')
@@ -914,10 +1133,15 @@ product
914
1133
  description: opts.description,
915
1134
  priceLix: Number(opts.price),
916
1135
  type: opts.type,
917
- itemId: opts.itemId,
918
1136
  grants: opts.grants !== undefined ? parseGrantsFlag(opts.grants) : undefined,
919
1137
  maxPerUser: opts.maxPerUser !== undefined ? parseWholeNumber(opts.maxPerUser, '--max-per-user') : undefined,
920
1138
  };
1139
+ if (registration.type !== undefined && registration.type !== 'consumable') {
1140
+ fail('✖ World Products do not mint universal items. Use `helix item distribution create`; legacy --type accepts only consumable.');
1141
+ }
1142
+ if (registration.grants?.some((grant) => grant.kind === 'item')) {
1143
+ fail('✖ World Product grants accept pass/currency only. Publish the item and create an ItemDistribution instead.');
1144
+ }
921
1145
  // Stops before ANY network call, so it works logged out — the point is to
922
1146
  // check the vocabulary and the type/item coherence, not the account.
923
1147
  if (opts.dryRun) {