@bitmagic/cli 0.1.72-dev.0 → 0.1.73-dev.0

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
@@ -185,8 +185,8 @@ browser reloads itself on every change and the assets panel shows each generatio
185
185
 
186
186
  | Command | What it does |
187
187
  |---|---|
188
- | `bitmagic login [--env <env>]` | Device-code auth against Auth0; stores credentials per environment. |
189
- | `bitmagic subscribe [--no-wait]` | Start the Bitmagic Pro subscription the CLI requires; opens checkout and waits for it. |
188
+ | `bitmagic login [--env <env>] [--ref <campaign>]` | Device-code auth against Auth0; stores credentials per environment. `--ref` is set by a landing page's prompt and passed on to the Bitmagic Pro page for attribution. |
189
+ | `bitmagic subscribe [--no-wait] [--ref <campaign>]` | Start the Bitmagic Pro subscription the CLI requires; opens checkout and waits for it. |
190
190
  | `bitmagic usage [--days N]` | Where this account's sparks went, and what Pro allowance is left. |
191
191
  | `bitmagic allowance [--allow-sparks \| --block-sparks]` | The two Pro allowance windows, and whether your purchased sparks may fund work once they run out. `--allow-sparks` needs a real terminal — it authorises spending your money, so an agent cannot run it for you. |
192
192
  | `bitmagic logout [--env <env>]` | Remove stored credentials for an environment. |
@@ -56,6 +56,8 @@ export interface SubscribeOptions {
56
56
  noOpen?: boolean;
57
57
  /** Skip the wait entirely and return as soon as checkout is open. */
58
58
  noWait?: boolean;
59
+ /** The campaign that sent the creator (`--ref`), passed on to the storefront. See storefrontUrlFor. */
60
+ ref?: string;
59
61
  }
60
62
  export interface SubscribeResult {
61
63
  /** True when Pro was confirmed active before we returned. */
@@ -88,8 +90,15 @@ export interface SubscribeResult {
88
90
  * there is no session to resume it comes straight back and nothing is shown.
89
91
  * The portal treats a bare `cli=1` as the same request, so every CLI already
90
92
  * installed is fixed by the portal deploy alone — this only states it.
93
+ *
94
+ * `ref` names the paid campaign whose landing page handed the creator the
95
+ * prompt that ran `bitmagic login --ref <campaign>`. The storefront reports it
96
+ * to analytics (portal/src/campaign-ref.ts): the prompt is the one thing that
97
+ * reliably crosses from the landing page's browser to wherever this opens. Only
98
+ * a short slug is passed on; anything else is dropped rather than refused,
99
+ * because a marketing tag must never stand between a creator and signing in.
91
100
  */
92
- export declare function storefrontUrlFor(subscribeUrl: string, userId: string): string;
101
+ export declare function storefrontUrlFor(subscribeUrl: string, userId: string, ref?: string): string;
93
102
  /**
94
103
  * Explain that Pro is required, open the storefront, and wait for it to land.
95
104
  *
@@ -82,10 +82,19 @@ export function fetchWhoami(environment, token, deps) {
82
82
  * there is no session to resume it comes straight back and nothing is shown.
83
83
  * The portal treats a bare `cli=1` as the same request, so every CLI already
84
84
  * installed is fixed by the portal deploy alone — this only states it.
85
+ *
86
+ * `ref` names the paid campaign whose landing page handed the creator the
87
+ * prompt that ran `bitmagic login --ref <campaign>`. The storefront reports it
88
+ * to analytics (portal/src/campaign-ref.ts): the prompt is the one thing that
89
+ * reliably crosses from the landing page's browser to wherever this opens. Only
90
+ * a short slug is passed on; anything else is dropped rather than refused,
91
+ * because a marketing tag must never stand between a creator and signing in.
85
92
  */
86
- export function storefrontUrlFor(subscribeUrl, userId) {
93
+ export function storefrontUrlFor(subscribeUrl, userId, ref) {
87
94
  const separator = subscribeUrl.includes('?') ? '&' : '?';
88
- return `${subscribeUrl}${separator}cli=1&signin=1&u=${encodeURIComponent(userId)}`;
95
+ const slug = ref?.trim().toLowerCase();
96
+ const campaign = slug && /^[a-z0-9][a-z0-9-]{0,31}$/.test(slug) ? `&ref=${slug}` : '';
97
+ return `${subscribeUrl}${separator}cli=1&signin=1&u=${encodeURIComponent(userId)}${campaign}`;
89
98
  }
90
99
  /**
91
100
  * Explain that Pro is required, open the storefront, and wait for it to land.
@@ -98,7 +107,7 @@ export async function runSubscribeFlow(environment, token, me, output, options =
98
107
  // An api-server too old to serve a storefront still has to be able to sell a
99
108
  // subscription, so fall back to minting Checkout the way this flow used to.
100
109
  const url = me.subscribeUrl
101
- ? storefrontUrlFor(me.subscribeUrl, me.userId)
110
+ ? storefrontUrlFor(me.subscribeUrl, me.userId, options.ref)
102
111
  : (await apiPost(environment, '/api/cli/v1/pro/checkout', token, {}, { fetch: deps.fetch })).url;
103
112
  output.info('');
104
113
  output.info('The Bitmagic CLI needs Bitmagic Pro.');
@@ -126,7 +135,7 @@ export async function runSubscribeFlow(environment, token, me, output, options =
126
135
  // A transient failure mid-poll must not end the flow — the creator may
127
136
  // still be typing their card in. Only a definitive "yes" stops the loop.
128
137
  // This answers for a redeemed voucher too: `pro` is entitlement, not billing.
129
- let current = null;
138
+ let current;
130
139
  try {
131
140
  current = await fetchWhoami(environment, token, deps);
132
141
  }
@@ -1 +1 @@
1
- {"version":3,"file":"subscribe-flow.js","sourceRoot":"","sources":["../../src/auth/subscribe-flow.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"subscribe-flow.js","sourceRoot":"","sources":["../../src/auth/subscribe-flow.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts CHANGED
@@ -42,6 +42,10 @@ declare const rawCommands: {
42
42
  readonly type: "boolean";
43
43
  readonly description: "Print the result as JSON on stdout; all progress goes to stderr.";
44
44
  };
45
+ readonly ref: {
46
+ readonly type: "string";
47
+ readonly description: "The campaign that sent you, passed on to the Bitmagic Pro page. Set by a landing page's prompt.";
48
+ };
45
49
  }>;
46
50
  logout: CommandDef<{
47
51
  readonly env: {
@@ -94,6 +98,10 @@ declare const rawCommands: {
94
98
  readonly type: "boolean";
95
99
  readonly description: "Print the result as JSON on stdout; all progress goes to stderr.";
96
100
  };
101
+ readonly ref: {
102
+ readonly type: "string";
103
+ readonly description: "The campaign that sent you, passed on to the Bitmagic Pro page. Set by a landing page's prompt.";
104
+ };
97
105
  }>;
98
106
  usage: CommandDef<{
99
107
  readonly days: {
@@ -221,25 +221,6 @@ export interface MusicRunResult {
221
221
  file?: string;
222
222
  bytes?: number;
223
223
  }
224
- /**
225
- * `generate music` is the one `generate` lane with two outputs a creator may want separately: the
226
- * world.json asset (the default — what a game plays as its bed) and the MP3 file (`--out`, what a
227
- * video editor or `bitmagic trailer make` wants). `--no-world` keeps the asset out of the game
228
- * for the file-only case, so a trailer bed is not registered as a track the game never plays.
229
- *
230
- * Cannot go through {@link runAssetGeneration} because that always applies the patches and never
231
- * downloads; it shares {@link requestAssetWithJobRecord} so the request, the job record and the
232
- * "world.json is unchanged" failure contract are the same as every other lane's.
233
- */
234
- export declare function runMusicGeneration(options: {
235
- body: Record<string, unknown>;
236
- /** Save the MP3 here (relative to the current directory). */
237
- out?: string;
238
- /** Apply the asset to world.json. The default; `--no-world` turns it off. */
239
- world: boolean;
240
- json: boolean;
241
- deps?: GenerateRunDeps;
242
- }): Promise<MusicRunResult>;
243
224
  export interface SpeechRunResult {
244
225
  speechUrl: string;
245
226
  originalUrl: string;
@@ -250,18 +231,7 @@ export interface SpeechRunResult {
250
231
  file?: string;
251
232
  bytes?: number;
252
233
  }
253
- /**
254
- * `generate speech` has the same two-outputs shape as `generate music`, and for a sharper reason:
255
- * the world.json asset is a line an NPC speaks (`engine.playSound`), while the MP3 is the input
256
- * `tools/lipsync/make_line.py --audio` needs to cut mouth cues from. A creator rigging a talking
257
- * character wants the file and NOT the asset, so `--no-world` is a first-class path here rather
258
- * than an escape hatch.
259
- *
260
- * Cannot go through {@link runAssetGeneration} because that always applies the patches and never
261
- * downloads; it shares {@link requestAssetWithJobRecord} so the request, the job record and the
262
- * "world.json is unchanged" failure contract are the same as every other lane's.
263
- */
264
- export declare function runSpeechGeneration(options: {
234
+ export interface AudioRunOptions {
265
235
  body: Record<string, unknown>;
266
236
  /** Save the MP3 here (relative to the current directory). */
267
237
  out?: string;
@@ -269,7 +239,22 @@ export declare function runSpeechGeneration(options: {
269
239
  world: boolean;
270
240
  json: boolean;
271
241
  deps?: GenerateRunDeps;
272
- }): Promise<SpeechRunResult>;
242
+ }
243
+ /**
244
+ * `generate music` is the one `generate` lane with two outputs a creator may want separately: the
245
+ * world.json asset (the default — what a game plays as its bed) and the MP3 file (`--out`, what a
246
+ * video editor or `bitmagic trailer make` wants). `--no-world` keeps the asset out of the game
247
+ * for the file-only case, so a trailer bed is not registered as a track the game never plays.
248
+ */
249
+ export declare function runMusicGeneration(options: AudioRunOptions): Promise<MusicRunResult>;
250
+ /**
251
+ * `generate speech` has the same two-outputs shape as `generate music`, and for a sharper reason:
252
+ * the world.json asset is a line an NPC speaks (`engine.playSound`), while the MP3 is the input
253
+ * `tools/lipsync/make_line.py --audio` needs to cut mouth cues from. A creator rigging a talking
254
+ * character wants the file and NOT the asset, so `--no-world` is a first-class path here rather
255
+ * than an escape hatch.
256
+ */
257
+ export declare function runSpeechGeneration(options: AudioRunOptions): Promise<SpeechRunResult>;
273
258
  /**
274
259
  * Refuses `--voxel-grid` alongside `--mesh`, rather than ignoring it.
275
260
  *
@@ -24,7 +24,6 @@ import { smartObjectSpecSchema, formatZodIssues } from '@bitmagic/asset-core';
24
24
  import { DEFAULT_MASTER_GRID, generateAndInstallProp } from '../generate/prop.js';
25
25
  import { generateAndInstallVehicle } from '../generate/vehicle.js';
26
26
  import { generateAndInstallModel } from '../generate/model.js';
27
- import { downloadMusicFile } from '../generate/music.js';
28
27
  import { downloadAudioFile } from '../generate/audio-file.js';
29
28
  import { MUSIC_MAX_SECONDS, MUSIC_MIN_SECONDS, SPEECH_MAX_CHARS, SPEECH_VOICES, VIDEO_MAX_SECONDS, VIDEO_MIN_SECONDS, } from '@bitmagic/asset-core';
30
29
  import { generateAndInstallAnimationFromVideo, isLocomotionState, isVideoToMotionModel, } from '../generate/animation-from-video.js';
@@ -567,24 +566,24 @@ export async function requestAssetWithJobRecord(type, label, body, runDeps, onSu
567
566
  });
568
567
  }
569
568
  /**
570
- * `generate music` is the one `generate` lane with two outputs a creator may want separately: the
571
- * world.json asset (the default — what a game plays as its bed) and the MP3 file (`--out`, what a
572
- * video editor or `bitmagic trailer make` wants). `--no-world` keeps the asset out of the game
573
- * for the file-only case, so a trailer bed is not registered as a track the game never plays.
569
+ * The lane both audio generators share: request, apply (or deliberately not), download (or not).
574
570
  *
575
- * Cannot go through {@link runAssetGeneration} because that always applies the patches and never
576
- * downloads; it shares {@link requestAssetWithJobRecord} so the request, the job record and the
577
- * "world.json is unchanged" failure contract are the same as every other lane's.
571
+ * Neither can go through {@link runAssetGeneration} — that always applies the patches and never
572
+ * downloads — so both share {@link requestAssetWithJobRecord} instead, which is what keeps the
573
+ * request, the job record and the "world.json is unchanged" failure contract identical to every
574
+ * other lane's. Written once because the two differed only in nouns, and a message about a
575
+ * generation that was billed and then half-lost is the last thing a creator reads.
578
576
  */
579
- export async function runMusicGeneration(options) {
577
+ async function runAudioGeneration(lane, options) {
580
578
  const output = createOutput(options.json);
581
579
  const runDeps = options.deps ?? defaultDeps(options.json);
582
580
  const outPath = options.out === undefined ? undefined : path.resolve(options.out);
583
- const result = await requestAssetWithJobRecord('music', 'music', options.body, runDeps, async (outcome, worldPath, log) => {
584
- if (!outcome.music) {
585
- throw new CliError('The server reported the music as generated but sent no track URLs. world.json is unchanged.');
581
+ const shared = await requestAssetWithJobRecord(lane.type, lane.type, options.body, runDeps, async (outcome, worldPath, log) => {
582
+ const urls = lane.read(outcome);
583
+ if (!urls) {
584
+ throw new CliError(`The server reported the ${lane.noun} as generated but sent ${lane.missingUrls}. world.json is unchanged.`);
586
585
  }
587
- const { music } = outcome;
586
+ const generated = `Generated ${lane.noun} (${urls.durationSeconds.toFixed(1)}s): ${outcome.message}`;
588
587
  let patchesApplied = 0;
589
588
  if (options.world) {
590
589
  let applyResult;
@@ -592,120 +591,88 @@ export async function runMusicGeneration(options) {
592
591
  applyResult = applyPatchesToWorld(worldPath, outcome.patches);
593
592
  }
594
593
  catch (error) {
595
- throw decoratedError(error, (message) => `Generated music successfully, but could not write it into world.json: ${message} `
596
- + 'world.json is unchanged — the track was generated (and may have been billed) but is not '
597
- + `yet in your project. It is at ${music.originalUrl}.`);
594
+ throw decoratedError(error, (message) => `Generated ${lane.noun} successfully, but could not write it into world.json: ${message} `
595
+ + `world.json is unchanged — ${lane.output.toLowerCase()} was generated (and may have been billed) `
596
+ + `but is not yet in your project. It is at ${urls.originalUrl}.`);
598
597
  }
599
598
  patchesApplied = applyResult.applied;
600
- log(`Generated music (${music.durationSeconds.toFixed(1)}s): ${outcome.message}`);
599
+ log(generated);
601
600
  for (const line of applyResult.summary)
602
601
  runDeps.log(` - ${line}`);
603
602
  runDeps.log(worldUpdatedLine(patchesApplied));
604
603
  }
605
604
  else {
606
- log(`Generated music (${music.durationSeconds.toFixed(1)}s): ${outcome.message}`);
605
+ log(generated);
607
606
  runDeps.log('world.json is unchanged (--no-world).');
608
607
  }
609
608
  let bytes;
610
609
  if (outPath !== undefined) {
611
610
  try {
612
- bytes = await downloadMusicFile(music.originalUrl, outPath, runDeps.fetch);
611
+ bytes = await downloadAudioFile(urls.originalUrl, outPath, runDeps.fetch, lane.fileLabel);
613
612
  }
614
613
  catch (error) {
615
- // The asset is generated, billed, and (unless --no-world) already in world.json; only the
616
- // local copy is missing, and the message has to say so or the creator regenerates.
617
- throw decoratedError(error, (message) => `${message} The track was generated${options.world ? ' and is in world.json' : ''}; only saving `
614
+ // The asset is generated, billed, and (unless --no-world) already in world.json; only
615
+ // the local copy is missing, and the message has to say so or the creator regenerates.
616
+ throw decoratedError(error, (message) => `${message} ${lane.output} was generated${options.world ? ' and is in world.json' : ''}; only saving `
618
617
  + 'the local copy failed.');
619
618
  }
620
619
  runDeps.log(`Saved ${outPath} (${Math.round(bytes / 1024)} KB).`);
621
620
  }
622
621
  return {
623
- musicUrl: music.musicUrl,
624
- originalUrl: music.originalUrl,
625
- musicId: music.musicId,
626
- durationSeconds: music.durationSeconds,
622
+ ...urls,
627
623
  patchesApplied,
628
624
  ...(outPath !== undefined ? { file: outPath } : {}),
629
625
  ...(bytes !== undefined ? { bytes } : {}),
630
626
  };
631
627
  });
632
- output.result({ ok: true, kind: 'music', ...result });
628
+ const result = lane.rename(shared);
629
+ output.result({ ok: true, kind: lane.type, ...result });
633
630
  if (options.world) {
634
- runDeps.log(`Play it from game code with engine.playMusic('${result.musicId}').`);
631
+ runDeps.log(lane.playHint(shared.id));
635
632
  runDeps.log(DEV_VIEW_RELOADS);
636
633
  }
637
634
  return result;
638
635
  }
636
+ /**
637
+ * `generate music` is the one `generate` lane with two outputs a creator may want separately: the
638
+ * world.json asset (the default — what a game plays as its bed) and the MP3 file (`--out`, what a
639
+ * video editor or `bitmagic trailer make` wants). `--no-world` keeps the asset out of the game
640
+ * for the file-only case, so a trailer bed is not registered as a track the game never plays.
641
+ */
642
+ export function runMusicGeneration(options) {
643
+ return runAudioGeneration({
644
+ type: 'music',
645
+ noun: 'music',
646
+ output: 'The track',
647
+ missingUrls: 'no track URLs',
648
+ fileLabel: 'track',
649
+ read: ({ music }) => (music
650
+ ? { url: music.musicUrl, originalUrl: music.originalUrl, id: music.musicId, durationSeconds: music.durationSeconds }
651
+ : undefined),
652
+ rename: ({ url, id, ...rest }) => ({ ...rest, musicUrl: url, musicId: id }),
653
+ playHint: (musicId) => `Play it from game code with engine.playMusic('${musicId}').`,
654
+ }, options);
655
+ }
639
656
  /**
640
657
  * `generate speech` has the same two-outputs shape as `generate music`, and for a sharper reason:
641
658
  * the world.json asset is a line an NPC speaks (`engine.playSound`), while the MP3 is the input
642
659
  * `tools/lipsync/make_line.py --audio` needs to cut mouth cues from. A creator rigging a talking
643
660
  * character wants the file and NOT the asset, so `--no-world` is a first-class path here rather
644
661
  * than an escape hatch.
645
- *
646
- * Cannot go through {@link runAssetGeneration} because that always applies the patches and never
647
- * downloads; it shares {@link requestAssetWithJobRecord} so the request, the job record and the
648
- * "world.json is unchanged" failure contract are the same as every other lane's.
649
662
  */
650
- export async function runSpeechGeneration(options) {
651
- const output = createOutput(options.json);
652
- const runDeps = options.deps ?? defaultDeps(options.json);
653
- const outPath = options.out === undefined ? undefined : path.resolve(options.out);
654
- const result = await requestAssetWithJobRecord('speech', 'speech', options.body, runDeps, async (outcome, worldPath, log) => {
655
- if (!outcome.speech) {
656
- throw new CliError('The server reported the speech as generated but sent no audio URLs. world.json is unchanged.');
657
- }
658
- const { speech } = outcome;
659
- let patchesApplied = 0;
660
- if (options.world) {
661
- let applyResult;
662
- try {
663
- applyResult = applyPatchesToWorld(worldPath, outcome.patches);
664
- }
665
- catch (error) {
666
- throw decoratedError(error, (message) => `Generated speech successfully, but could not write it into world.json: ${message} `
667
- + 'world.json is unchanged — the line was generated (and may have been billed) but is not '
668
- + `yet in your project. It is at ${speech.originalUrl}.`);
669
- }
670
- patchesApplied = applyResult.applied;
671
- log(`Generated speech (${speech.durationSeconds.toFixed(1)}s): ${outcome.message}`);
672
- for (const line of applyResult.summary)
673
- runDeps.log(` - ${line}`);
674
- runDeps.log(worldUpdatedLine(patchesApplied));
675
- }
676
- else {
677
- log(`Generated speech (${speech.durationSeconds.toFixed(1)}s): ${outcome.message}`);
678
- runDeps.log('world.json is unchanged (--no-world).');
679
- }
680
- let bytes;
681
- if (outPath !== undefined) {
682
- try {
683
- bytes = await downloadAudioFile(speech.originalUrl, outPath, runDeps.fetch, 'line');
684
- }
685
- catch (error) {
686
- // The line is generated, billed, and (unless --no-world) already in world.json; only the
687
- // local copy is missing, and the message has to say so or the creator regenerates.
688
- throw decoratedError(error, (message) => `${message} The line was generated${options.world ? ' and is in world.json' : ''}; only saving `
689
- + 'the local copy failed.');
690
- }
691
- runDeps.log(`Saved ${outPath} (${Math.round(bytes / 1024)} KB).`);
692
- }
693
- return {
694
- speechUrl: speech.speechUrl,
695
- originalUrl: speech.originalUrl,
696
- speechId: speech.speechId,
697
- durationSeconds: speech.durationSeconds,
698
- patchesApplied,
699
- ...(outPath !== undefined ? { file: outPath } : {}),
700
- ...(bytes !== undefined ? { bytes } : {}),
701
- };
702
- });
703
- output.result({ ok: true, kind: 'speech', ...result });
704
- if (options.world) {
705
- runDeps.log(`Play it from game code with engine.playSound('${result.speechId}').`);
706
- runDeps.log(DEV_VIEW_RELOADS);
707
- }
708
- return result;
663
+ export function runSpeechGeneration(options) {
664
+ return runAudioGeneration({
665
+ type: 'speech',
666
+ noun: 'speech',
667
+ output: 'The line',
668
+ missingUrls: 'no audio URLs',
669
+ fileLabel: 'line',
670
+ read: ({ speech }) => (speech
671
+ ? { url: speech.speechUrl, originalUrl: speech.originalUrl, id: speech.speechId, durationSeconds: speech.durationSeconds }
672
+ : undefined),
673
+ rename: ({ url, id, ...rest }) => ({ ...rest, speechUrl: url, speechId: id }),
674
+ playHint: (speechId) => `Play it from game code with engine.playSound('${speechId}').`,
675
+ }, options);
709
676
  }
710
677
  /**
711
678
  * Refuses `--voxel-grid` alongside `--mesh`, rather than ignoring it.