@helix3/helix-cli 0.1.13-helix3.79 → 0.1.13-helix3.82
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 +24 -1
- package/dist/index.js +263 -0
- package/dist/index.js.map +1 -1
- package/dist/lib.d.ts +182 -0
- package/dist/lib.js +234 -6
- package/dist/lib.js.map +1 -1
- package/dist/product.d.ts +84 -0
- package/dist/product.js +285 -0
- package/dist/product.js.map +1 -0
- package/dist/source-archive.d.ts +91 -0
- package/dist/source-archive.js +296 -0
- package/dist/source-archive.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
|
|
|
41
41
|
| `helix init <dir>` | Scaffold a minimal Three.js world (`helix.json` + `index.html` + `main.js`). |
|
|
42
42
|
| `helix install [--update]` | Resolve a world's `systems`/`abilities` pins (a v0.2 **or v0.3** manifest) → materialize the modules, the `three` import map, and `helix.runtime.ts`. |
|
|
43
43
|
| `helix validate [dir]` | Validate a bundle locally — the exact rules the server enforces. |
|
|
44
|
-
| `helix publish [dir] [--thumbnail <file>]` | Validate → resolve/create the world → upload files → finalize → set the cover image → print the play URL. |
|
|
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
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. |
|
|
@@ -71,6 +71,29 @@ The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
|
|
|
71
71
|
5. `POST /api/v1/instant-worlds/:id/builds/:buildId/finalize` → byte-exact verify, activate, publish.
|
|
72
72
|
6. `GET /api/v1/instant-worlds/:slug` → the play URL.
|
|
73
73
|
|
|
74
|
+
### Opt-in source upload
|
|
75
|
+
|
|
76
|
+
A published world ships only its **built bundle**, so there is nothing on the server to edit later.
|
|
77
|
+
`--upload-source` adds a second, separate upload that fixes that:
|
|
78
|
+
|
|
79
|
+
7. `POST /api/v1/instant-worlds/:id/source` → a presigned PUT into a **private** bucket (this call
|
|
80
|
+
also adopts the world into a project, which is the key its future workspace hangs from).
|
|
81
|
+
8. `helix-source.tar.gz` PUT with `cache-control: no-store`.
|
|
82
|
+
9. `POST /api/v1/instant-worlds/:id/source/finalize` → the server verifies the stored byte count
|
|
83
|
+
equals the declared one, then records the version.
|
|
84
|
+
|
|
85
|
+
- **Off by default.** Source is never sent unless you pass the flag. Not a prompt — an interactive
|
|
86
|
+
confirm would break CI and agent-driven publishes.
|
|
87
|
+
- **What goes in**: the project directory, honouring `.gitignore`, always excluding `node_modules`,
|
|
88
|
+
`.git`, `dist` and `.vite` — the same exclude set the website's own workspace checkpoints use.
|
|
89
|
+
- **Which directory**: `[dir]` is the BUILT bundle, so the source defaults to its parent. Override
|
|
90
|
+
with `--source-dir <path>`.
|
|
91
|
+
- **Never part of the build.** The bundle's content-type allowlist rejects archives, raw `.ts` is
|
|
92
|
+
excluded from a bundle by design, the bundle size budget is the playable budget, and every build
|
|
93
|
+
file lands on a public CDN path with a year-long immutable cache. Source uses its own private
|
|
94
|
+
channel for exactly that reason.
|
|
95
|
+
- **Limit**: 128 MiB compressed, checked locally before anything is uploaded.
|
|
96
|
+
|
|
74
97
|
The programmatic surface (`publishWorld`, `checkBundle`, `whoAmI`, …) is exported for other CLI
|
|
75
98
|
modules. Agent integrations should instruct or delegate to the `helix` commands above rather than
|
|
76
99
|
reimplementing operational behavior in a transport package.
|
package/dist/index.js
CHANGED
|
@@ -308,6 +308,11 @@ program
|
|
|
308
308
|
.option('--skip-subpath-check', 'skip the pre-publish sub-path load check (not recommended: it is the only gate that serves the bundle the way production does)')
|
|
309
309
|
.option('--description <text>', `catalog description, max ${lib_1.WORLD_DESCRIPTION_MAX} chars (pass "" to clear). helix.json cannot carry this — the manifest schema forbids unknown fields`)
|
|
310
310
|
.option('--description-file <path>', 'read the catalog description from a file')
|
|
311
|
+
.option('--upload-source', "Also upload this project's source so you can edit this world from the HELIX website later. " +
|
|
312
|
+
'Off by default — your source is not sent unless you pass this.')
|
|
313
|
+
.option('--source-dir <path>', "project directory to archive for --upload-source (default: the bundle directory's parent, " +
|
|
314
|
+
'since [directory] points at the BUILT bundle). node_modules, .git, dist and .vite are always excluded, ' +
|
|
315
|
+
'as is anything your .gitignore excludes')
|
|
311
316
|
.action(async (directory, opts) => {
|
|
312
317
|
// Pure input checks first — bad flags should fail before auth, the network, or any upload.
|
|
313
318
|
if (opts.description !== undefined && opts.descriptionFile) {
|
|
@@ -325,6 +330,9 @@ program
|
|
|
325
330
|
if (description !== undefined && description.length > lib_1.WORLD_DESCRIPTION_MAX) {
|
|
326
331
|
fail(`✖ description is ${description.length} chars; the limit is ${lib_1.WORLD_DESCRIPTION_MAX}. Nothing was published.`);
|
|
327
332
|
}
|
|
333
|
+
if (opts.sourceDir && !opts.uploadSource) {
|
|
334
|
+
fail('✖ --source-dir only means something with --upload-source. Source is never uploaded unless you ask for it.');
|
|
335
|
+
}
|
|
328
336
|
const creds = requireAuth();
|
|
329
337
|
const mismatch = (0, config_1.credsEnvMismatchWarning)(creds.apiUrl);
|
|
330
338
|
if (mismatch)
|
|
@@ -353,6 +361,8 @@ program
|
|
|
353
361
|
const result = await (0, lib_1.publishWorld)((0, node_path_1.resolve)(directory), creds, (msg) => console.log(msg), {
|
|
354
362
|
thumbnail: opts.thumbnail ? (0, node_path_1.resolve)(opts.thumbnail) : undefined,
|
|
355
363
|
description,
|
|
364
|
+
uploadSource: opts.uploadSource,
|
|
365
|
+
sourceDir: opts.sourceDir ? (0, node_path_1.resolve)(opts.sourceDir) : undefined,
|
|
356
366
|
});
|
|
357
367
|
console.log(`✔ Published "${result.title}" build ${result.buildNumber}`);
|
|
358
368
|
if (result.playUrl)
|
|
@@ -363,6 +373,10 @@ program
|
|
|
363
373
|
}
|
|
364
374
|
if (result.thumbnailWarning)
|
|
365
375
|
console.warn(` • ${result.thumbnailWarning}`);
|
|
376
|
+
// Spec §9.3: one line about source, every publish, as a FACT — not a
|
|
377
|
+
// warning and never repeated. It is what tells a creator that editing on
|
|
378
|
+
// the website is a thing, and that it did not happen behind their back.
|
|
379
|
+
console.log(` ${(0, lib_1.sourceOutcomeLine)(result.source, creds.apiUrl)}`);
|
|
366
380
|
}
|
|
367
381
|
catch (err) {
|
|
368
382
|
fail(err instanceof lib_1.ApiError
|
|
@@ -774,6 +788,255 @@ achievement
|
|
|
774
788
|
fail(err instanceof lib_1.ApiError ? err.render() : `✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
775
789
|
}
|
|
776
790
|
});
|
|
791
|
+
// --- helix product ---------------------------------------------------------
|
|
792
|
+
// The registry `Helix.purchase` sells from: a world can only sell what its
|
|
793
|
+
// creator registered here, and registering was API/website-only until these
|
|
794
|
+
// commands. ORDER, the INVERSE of achievements: publish the world FIRST — the
|
|
795
|
+
// creator registry lists Published worlds only, and nobody can buy inside a
|
|
796
|
+
// world they cannot enter. Worlds are named by SLUG, products by their key.
|
|
797
|
+
const product = program
|
|
798
|
+
.command('product')
|
|
799
|
+
.description("Register and manage a world's in-world products (what Helix.purchase sells). Publish the world FIRST — unlike achievements, a product only counts once the world is Published");
|
|
800
|
+
function renderProduct(p, indent = ' ') {
|
|
801
|
+
// An authored-grants product's stored type is server-DERIVED — the effect list is the truth,
|
|
802
|
+
// so the "your world applies it" hint only fits legacy consumables.
|
|
803
|
+
const lines = [
|
|
804
|
+
`${indent}price: ${p.priceLix} LIX${p.isFree ? ' — free "Claim"' : ''}`,
|
|
805
|
+
`${indent}type: ${p.type}${p.type === 'consumable' && !p.grants ? ' — your world applies the effect on a Granted result' : ''}`,
|
|
806
|
+
];
|
|
807
|
+
// The key is how world code names it; without one the uuid is the only handle.
|
|
808
|
+
lines.push(`${indent}buy as: ${p.key ? `pkey:${p.key}` : `${p.id} (no key — register with --key next time)`}`);
|
|
809
|
+
if (p.grants) {
|
|
810
|
+
const effects = p.grants.map((g) => g.kind === 'item'
|
|
811
|
+
? `item ${g.itemId}${(g.quantity ?? 1) > 1 ? ` ×${g.quantity}` : ''}`
|
|
812
|
+
: g.kind === 'pass'
|
|
813
|
+
? `pass ${g.passKey ?? p.key ?? '(product key)'}${g.durationSeconds ? ` for ${g.durationSeconds}s` : ''}`
|
|
814
|
+
: `currency ${g.code} +${g.amount}`);
|
|
815
|
+
lines.push(`${indent}grants: ${effects.length ? effects.join(', ') : 'nothing — a tip jar'}`);
|
|
816
|
+
}
|
|
817
|
+
else if (p.itemId) {
|
|
818
|
+
lines.push(`${indent}grants: item ${p.itemId}`);
|
|
819
|
+
}
|
|
820
|
+
if (p.maxPerUser !== null)
|
|
821
|
+
lines.push(`${indent}max/user: ${p.maxPerUser}`);
|
|
822
|
+
if (!p.active)
|
|
823
|
+
lines.push(`${indent}active: no — off sale, existing owners keep theirs`);
|
|
824
|
+
lines.push(`${indent}id: ${p.id}`);
|
|
825
|
+
return lines;
|
|
826
|
+
}
|
|
827
|
+
// The effect list arrives as one JSON string. Only "is it an array" is checked
|
|
828
|
+
// here — every shape rule belongs to the vocabulary, which reports them together.
|
|
829
|
+
function parseGrantsFlag(value) {
|
|
830
|
+
let parsed;
|
|
831
|
+
try {
|
|
832
|
+
parsed = JSON.parse(value);
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
836
|
+
fail(`✖ --grants must be a JSON array of effects — that is not valid JSON (${detail})`);
|
|
837
|
+
}
|
|
838
|
+
if (!Array.isArray(parsed)) {
|
|
839
|
+
fail('✖ --grants must be a JSON array of effects, e.g. \'[{"kind":"currency","code":"coin","amount":500}]\' (several = a bundle, [] = a tip jar)');
|
|
840
|
+
}
|
|
841
|
+
return parsed;
|
|
842
|
+
}
|
|
843
|
+
product
|
|
844
|
+
.command('register')
|
|
845
|
+
.description('Register a purchasable product on one of your PUBLISHED worlds — one round trip, live as soon as it returns')
|
|
846
|
+
.argument('<world-slug>', 'the world that sells it')
|
|
847
|
+
.requiredOption('--title <title>', 'name the purchase popup shows the player (max 120 chars)')
|
|
848
|
+
.requiredOption('--price <lix>', 'price in LIX; 0 registers a free "Claim" (a confirmation, no spend)')
|
|
849
|
+
.option('--type <type>', `what the purchase does: ${lib_1.WORLD_PRODUCT_TYPES.join(' | ')} — required unless you author --grants`)
|
|
850
|
+
.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} ` +
|
|
851
|
+
"(e.g. '[{\"kind\":\"currency\",\"code\":\"coin\",\"amount\":500}]'; [] = a tip jar). Fixed at registration")
|
|
852
|
+
.option('--key <key>', 'stable slug world code buys as `pkey:<key>` (max 64 chars, lowercase a-z0-9_-); it cannot be renamed later')
|
|
853
|
+
.option('--description <text>', 'what the player is buying')
|
|
854
|
+
.option('--item-id <uuid>', 'the universal item handed over — REQUIRED for item_grant and non_consumable, refused for consumable')
|
|
855
|
+
.option('--max-per-user <n>', 'cap on how many one player may own (whole number >= 1)')
|
|
856
|
+
.option('--dry-run', 'validate everything locally and print what WOULD be sent; no network, works logged out')
|
|
857
|
+
.option('--json', 'machine-readable output')
|
|
858
|
+
.action(async (worldSlug, opts) => {
|
|
859
|
+
const registration = {
|
|
860
|
+
worldSlug,
|
|
861
|
+
key: opts.key,
|
|
862
|
+
title: opts.title,
|
|
863
|
+
description: opts.description,
|
|
864
|
+
priceLix: Number(opts.price),
|
|
865
|
+
type: opts.type,
|
|
866
|
+
itemId: opts.itemId,
|
|
867
|
+
grants: opts.grants !== undefined ? parseGrantsFlag(opts.grants) : undefined,
|
|
868
|
+
maxPerUser: opts.maxPerUser !== undefined ? parseWholeNumber(opts.maxPerUser, '--max-per-user') : undefined,
|
|
869
|
+
};
|
|
870
|
+
// Stops before ANY network call, so it works logged out — the point is to
|
|
871
|
+
// check the vocabulary and the type/item coherence, not the account.
|
|
872
|
+
if (opts.dryRun) {
|
|
873
|
+
try {
|
|
874
|
+
const plan = (0, lib_1.planProductRegistration)(registration);
|
|
875
|
+
if (opts.json) {
|
|
876
|
+
console.log(JSON.stringify({ dryRun: true, worldSlug, fields: plan.fields, warnings: plan.warnings }, null, 2));
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
console.log('✔ Dry run — nothing sent.');
|
|
880
|
+
console.log(` POST /api/v1/iwp/products (worldId of "${worldSlug}")`);
|
|
881
|
+
console.log(` body: ${JSON.stringify(plan.fields, null, 2).replace(/\n/g, '\n ')}`);
|
|
882
|
+
for (const warning of plan.warnings)
|
|
883
|
+
console.log(` • ${warning}`);
|
|
884
|
+
if (!(0, lib_1.authStatus)().loggedIn)
|
|
885
|
+
console.log(' • not logged in — run `helix login` before the real registration');
|
|
886
|
+
}
|
|
887
|
+
catch (err) {
|
|
888
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
889
|
+
}
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const creds = requireAuth();
|
|
893
|
+
const mismatch = (0, config_1.credsEnvMismatchWarning)(creds.apiUrl);
|
|
894
|
+
if (mismatch)
|
|
895
|
+
fail(`✖ Refusing to register:\n${mismatch}`);
|
|
896
|
+
try {
|
|
897
|
+
const { product: created, warnings } = await (0, lib_1.registerProduct)(registration, creds, {
|
|
898
|
+
onProgress: (msg) => !opts.json && console.log(msg),
|
|
899
|
+
});
|
|
900
|
+
if (opts.json) {
|
|
901
|
+
console.log(JSON.stringify({ product: created, warnings }, null, 2));
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
console.log(`✔ Registered "${created.title}" on ${worldSlug}`);
|
|
905
|
+
for (const line of renderProduct(created))
|
|
906
|
+
console.log(line);
|
|
907
|
+
for (const warning of warnings)
|
|
908
|
+
console.log(` ⚠ ${warning}`);
|
|
909
|
+
}
|
|
910
|
+
catch (err) {
|
|
911
|
+
fail(err instanceof lib_1.ApiError ? err.render() : `✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
product
|
|
915
|
+
.command('list')
|
|
916
|
+
.description("List a world's registered products, or — with no slug — every PUBLISHED world of yours and what it sells")
|
|
917
|
+
.argument('[world-slug]', 'the world that sells them; omit for all your published worlds')
|
|
918
|
+
.option('--json', 'machine-readable output')
|
|
919
|
+
.action(async (worldSlug, opts) => {
|
|
920
|
+
const creds = requireAuth();
|
|
921
|
+
try {
|
|
922
|
+
if (!worldSlug) {
|
|
923
|
+
const worlds = await (0, lib_1.listProductWorlds)(creds);
|
|
924
|
+
if (opts.json) {
|
|
925
|
+
console.log(JSON.stringify(worlds, null, 2));
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (worlds.length === 0) {
|
|
929
|
+
console.log('You have no published worlds — `helix publish` one first; a product on a draft world is unbuyable.');
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
for (const world of worlds) {
|
|
933
|
+
console.log(`\n${world.slug} — "${world.title}" (${world.products.length} product${world.products.length === 1 ? '' : 's'})`);
|
|
934
|
+
for (const row of world.products) {
|
|
935
|
+
console.log(` ${row.key ?? row.id} — "${row.title}" · ${row.priceLix} LIX · ${row.type}${row.active ? '' : ' [INACTIVE]'}`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const rows = await (0, lib_1.listProducts)(worldSlug, creds);
|
|
941
|
+
if (opts.json) {
|
|
942
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (rows.length === 0) {
|
|
946
|
+
console.log(`No products registered on "${worldSlug}" yet — register one with: helix product register ${worldSlug} --title … --type … --price …`);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
console.log(`${rows.length} product${rows.length === 1 ? '' : 's'} on ${worldSlug}:`);
|
|
950
|
+
for (const row of rows) {
|
|
951
|
+
console.log(`\n ${row.key ?? row.id} — "${row.title}"${row.active ? '' : ' [INACTIVE]'}`);
|
|
952
|
+
console.log(` ${row.priceLix} LIX · ${row.type}${row.itemId ? ` · grants ${row.itemId}` : ''}${row.maxPerUser !== null ? ` · max ${row.maxPerUser}/user` : ''}`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
catch (err) {
|
|
956
|
+
fail(err instanceof lib_1.ApiError ? err.render() : `✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
product
|
|
960
|
+
.command('update')
|
|
961
|
+
.description('Patch a registered product. The key and the type are fixed at registration — world code buys the key, and changing what a purchase hands over would re-label every copy already sold')
|
|
962
|
+
.argument('<world-slug>', 'the world that sells it')
|
|
963
|
+
.argument('<product>', 'the product key (or its uuid, if you registered it without a key)')
|
|
964
|
+
.option('--title <title>', 'new title')
|
|
965
|
+
.option('--description <text>', 'new description')
|
|
966
|
+
.option('--price <lix>', 'new price in LIX; 0 makes it a free "Claim"')
|
|
967
|
+
.option('--max-per-user <n>', 'new per-player ownership cap (whole number >= 1)')
|
|
968
|
+
.option('--active <true|false>', 'false takes it off sale — existing owners keep theirs, nobody new can buy it')
|
|
969
|
+
.option('--dry-run', 'validate the patch locally and print what WOULD be sent; no network, works logged out')
|
|
970
|
+
.option('--json', 'machine-readable output')
|
|
971
|
+
.action(async (worldSlug, ref, opts) => {
|
|
972
|
+
const patch = {
|
|
973
|
+
title: opts.title,
|
|
974
|
+
description: opts.description,
|
|
975
|
+
priceLix: opts.price !== undefined ? Number(opts.price) : undefined,
|
|
976
|
+
maxPerUser: opts.maxPerUser !== undefined ? parseWholeNumber(opts.maxPerUser, '--max-per-user') : undefined,
|
|
977
|
+
active: opts.active !== undefined ? parseBooleanFlag(opts.active, '--active') : undefined,
|
|
978
|
+
};
|
|
979
|
+
if (opts.dryRun) {
|
|
980
|
+
try {
|
|
981
|
+
const body = (0, lib_1.buildProductUpdate)(patch);
|
|
982
|
+
if (opts.json) {
|
|
983
|
+
console.log(JSON.stringify({ dryRun: true, worldSlug, product: ref, patch: body }, null, 2));
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
console.log('✔ Dry run — nothing sent.');
|
|
987
|
+
console.log(` PATCH /api/v1/iwp/products/<id of "${ref}" on ${worldSlug}>`);
|
|
988
|
+
console.log(` body: ${JSON.stringify(body, null, 2).replace(/\n/g, '\n ')}`);
|
|
989
|
+
}
|
|
990
|
+
catch (err) {
|
|
991
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
992
|
+
}
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const creds = requireAuth();
|
|
996
|
+
const mismatch = (0, config_1.credsEnvMismatchWarning)(creds.apiUrl);
|
|
997
|
+
if (mismatch)
|
|
998
|
+
fail(`✖ Refusing to update:\n${mismatch}`);
|
|
999
|
+
try {
|
|
1000
|
+
const updated = await (0, lib_1.updateProduct)(worldSlug, ref, patch, creds);
|
|
1001
|
+
if (opts.json) {
|
|
1002
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
console.log(`✔ Updated "${updated.title}" on ${worldSlug}`);
|
|
1006
|
+
for (const line of renderProduct(updated))
|
|
1007
|
+
console.log(line);
|
|
1008
|
+
}
|
|
1009
|
+
catch (err) {
|
|
1010
|
+
fail(err instanceof lib_1.ApiError ? err.render() : `✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
product
|
|
1014
|
+
.command('set-active')
|
|
1015
|
+
.description('Put a product on sale or take it off. The row stays either way, so past purchases and their grants keep resolving')
|
|
1016
|
+
.argument('<world-slug>', 'the world that sells it')
|
|
1017
|
+
.argument('<product>', 'the product key (or its uuid)')
|
|
1018
|
+
.argument('<true|false>', 'true puts it on sale, false takes it off')
|
|
1019
|
+
.option('--json', 'machine-readable output')
|
|
1020
|
+
.action(async (worldSlug, ref, value, opts) => {
|
|
1021
|
+
const active = parseBooleanFlag(value, '<true|false>');
|
|
1022
|
+
const creds = requireAuth();
|
|
1023
|
+
const mismatch = (0, config_1.credsEnvMismatchWarning)(creds.apiUrl);
|
|
1024
|
+
if (mismatch)
|
|
1025
|
+
fail(`✖ Refusing to change what a world sells:\n${mismatch}`);
|
|
1026
|
+
try {
|
|
1027
|
+
const updated = await (0, lib_1.setProductActive)(worldSlug, ref, active, creds);
|
|
1028
|
+
if (opts.json) {
|
|
1029
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
console.log(`✔ "${updated.title}" is ${updated.active ? 'ON SALE' : 'off sale'} on ${worldSlug}`);
|
|
1033
|
+
for (const line of renderProduct(updated))
|
|
1034
|
+
console.log(line);
|
|
1035
|
+
}
|
|
1036
|
+
catch (err) {
|
|
1037
|
+
fail(err instanceof lib_1.ApiError ? err.render() : `✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
777
1040
|
// --- helix assets ----------------------------------------------------------
|
|
778
1041
|
// Operational asset work lives here. MCP surfaces may point agents at these
|
|
779
1042
|
// commands, but they must not reimplement Vault, Dreamer, download, or receipt
|