@funnelsgrove/cli 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,24 +7,25 @@ npm install -g @funnelsgrove/cli
7
7
  fgrove login
8
8
  ```
9
9
 
10
- Set the active project and funnel:
10
+ Sync a funnel into its own local folder:
11
11
 
12
12
  ```bash
13
- fgrove use --project claimbee --funnel claimbee-ios
14
- fgrove status
13
+ fgrove sync down --funnel claimbee-ios --dir ./claimbee-ios
14
+ cd ./claimbee-ios
15
15
  ```
16
16
 
17
17
  Common workflow:
18
18
 
19
19
  ```bash
20
- fgrove sync down --dir ./claimbee-ios
21
- cd ./claimbee-ios
20
+ fgrove status
22
21
  fgrove docs
23
22
  fgrove sync up --message 'Update funnel copy'
24
23
  fgrove publish --env preview
25
24
  ```
26
25
 
27
- Use `fgrove env pull --dir <local-dir>` to refresh only the ignored local `.env` file after project settings change, without replacing source files.
26
+ Inside a synced folder, `fgrove` reads `.funnelsgrove-sync.json` first, so you do not need to run `fgrove use` when switching between local funnel directories. Use `fgrove use` only when you want a global fallback context for commands outside a synced folder.
27
+
28
+ Use `fgrove env pull` from a synced folder to refresh only the ignored local `.env` file after project settings change, without replacing source files.
28
29
 
29
30
  GitHub sync workflow:
30
31
 
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import { type ActiveContext } from './authStore.js';
3
+ import { type SyncManifest } from './localSync.js';
4
+ type SyncTargetIdInput = {
5
+ explicitWorkspaceId?: string;
6
+ explicitFunnelId?: string;
7
+ manifest?: Pick<SyncManifest, 'workspaceId' | 'funnelId'> | null;
8
+ active?: Pick<ActiveContext, 'workspaceId' | 'funnelId'> | null;
9
+ defaultWorkspaceId?: string;
10
+ };
11
+ export declare function resolveSyncTargetIds(input: SyncTargetIdInput): {
12
+ workspaceId: string;
13
+ funnelId?: string;
14
+ };
2
15
  export {};
package/dist/cli.js CHANGED
@@ -35,6 +35,21 @@ const resolveTemplatePath = (slug) => path.resolve(__dirname, '..', '..', '..',
35
35
  const resolveTemplateDocsPath = () => path.resolve(__dirname, '..', TEMPLATE_DOCS_DIR);
36
36
  const DEFAULT_API_URL = 'https://api.funnelsgrove.com/trpc';
37
37
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
38
+ const PUBLISH_POLL_INTERVAL_MS = 2_000;
39
+ const PUBLISH_WAIT_TIMEOUT_MS = 30 * 60_000;
40
+ export function resolveSyncTargetIds(input) {
41
+ const workspaceId = input.explicitWorkspaceId ||
42
+ input.manifest?.workspaceId ||
43
+ input.active?.workspaceId ||
44
+ input.defaultWorkspaceId;
45
+ if (!workspaceId) {
46
+ throw new Error('No workspace found for this account.');
47
+ }
48
+ return {
49
+ workspaceId,
50
+ funnelId: input.explicitFunnelId || input.manifest?.funnelId || input.active?.funnelId,
51
+ };
52
+ }
38
53
  const getApiUrl = () => {
39
54
  const options = program.opts();
40
55
  return options.apiUrl || process.env.FUNNELSGROVE_API_URL || DEFAULT_API_URL;
@@ -59,6 +74,40 @@ const callApi = async (input) => {
59
74
  token: input.token,
60
75
  });
61
76
  };
77
+ const sleep = async (ms) => {
78
+ await new Promise((resolve) => {
79
+ setTimeout(resolve, ms);
80
+ });
81
+ };
82
+ const isTerminalDeploymentState = (state) => {
83
+ return ['ready', 'failed', 'expired', 'canceled'].includes(state.trim().toLowerCase());
84
+ };
85
+ const waitForPublishDeployment = async (input) => {
86
+ const deadline = Date.now() + PUBLISH_WAIT_TIMEOUT_MS;
87
+ while (Date.now() <= deadline) {
88
+ const detail = await callApi({
89
+ path: 'funnels.detail',
90
+ type: 'query',
91
+ token: input.token,
92
+ data: {
93
+ workspaceId: input.workspaceId,
94
+ funnelId: input.funnelId,
95
+ },
96
+ });
97
+ const deployment = detail.deployments.find((item) => item.id === input.deploymentId);
98
+ if (deployment && isTerminalDeploymentState(deployment.state)) {
99
+ if (deployment.state === 'failed') {
100
+ throw new Error(deployment.qa_summary || 'Publish deployment failed.');
101
+ }
102
+ if (deployment.state === 'expired' || deployment.state === 'canceled') {
103
+ throw new Error(`Publish deployment ${deployment.state}.`);
104
+ }
105
+ return deployment;
106
+ }
107
+ await sleep(PUBLISH_POLL_INTERVAL_MS);
108
+ }
109
+ throw new Error(`Timed out waiting for publish deployment ${input.deploymentId}.`);
110
+ };
62
111
  const readCodeFromStdin = async () => {
63
112
  const rl = createInterface({
64
113
  input: process.stdin,
@@ -182,18 +231,24 @@ const resolveSyncTarget = async (input) => {
182
231
  const sourceDir = path.resolve(process.cwd(), input.dir || '.');
183
232
  const manifest = await readSyncManifest(sourceDir);
184
233
  const active = await loadActiveContext(getConfigPath());
185
- const workspaceId = input.workspace
186
- ? await resolveWorkspaceId(input.token, input.workspace)
187
- : manifest?.workspaceId || active?.workspaceId || await resolveWorkspaceId(input.token);
188
- const funnelId = input.funnel
189
- ? await resolveFunnelId(input.token, workspaceId, input.funnel)
190
- : manifest?.funnelId || active?.funnelId;
191
- if (!funnelId) {
192
- throw new Error('No active funnel. Run `fgrove use --funnel <id-or-slug>` or pass `--funnel`.');
234
+ const explicitWorkspaceId = input.workspace ? await resolveWorkspaceId(input.token, input.workspace) : undefined;
235
+ const fallbackWorkspaceId = explicitWorkspaceId || manifest?.workspaceId || active?.workspaceId || await resolveWorkspaceId(input.token);
236
+ const explicitFunnelId = input.funnel
237
+ ? await resolveFunnelId(input.token, fallbackWorkspaceId, input.funnel)
238
+ : undefined;
239
+ const resolved = resolveSyncTargetIds({
240
+ explicitWorkspaceId,
241
+ explicitFunnelId,
242
+ manifest,
243
+ active,
244
+ defaultWorkspaceId: fallbackWorkspaceId,
245
+ });
246
+ if (!resolved.funnelId) {
247
+ throw new Error('No synced funnel found. Run from a synced funnel directory, pass `--funnel`, or set a fallback with `fgrove use --funnel <id-or-slug>`.');
193
248
  }
194
249
  return {
195
- workspaceId,
196
- funnelId,
250
+ workspaceId: resolved.workspaceId,
251
+ funnelId: resolved.funnelId,
197
252
  sourceDir,
198
253
  manifest,
199
254
  };
@@ -241,8 +296,8 @@ program
241
296
  .option('--config <path>', 'Auth config path', process.env.FUNNELSGROVE_CONFIG || getDefaultAuthConfigPath());
242
297
  addExamples(program, [
243
298
  'fgrove login',
244
- 'fgrove use --project claimbee --funnel claimbee-ios',
245
- 'fgrove sync down --dir ./claimbee-ios',
299
+ 'fgrove sync down --funnel claimbee-ios --dir ./claimbee-ios',
300
+ 'cd ./claimbee-ios && fgrove status',
246
301
  'fgrove publish --env preview',
247
302
  ]);
248
303
  addExamples(program
@@ -336,9 +391,9 @@ addExamples(program
336
391
  console.log(`user\t${me.user.email || me.user.id}`);
337
392
  console.log(`api\t${getApiUrl()}`);
338
393
  console.log(`config\t${getConfigPath()}`);
339
- console.log(`workspace\t${active?.workspaceName || active?.workspaceSlug || active?.workspaceId || me.workspace?.name || 'Not set'}`);
340
- console.log(`project\t${active?.projectName || active?.projectSlug || active?.projectId || 'Not set'}`);
341
- console.log(`funnel\t${active?.funnelName || active?.funnelSlug || active?.funnelId || 'Not set'}`);
394
+ console.log(`workspace\t${manifest?.workspaceId || active?.workspaceName || active?.workspaceSlug || active?.workspaceId || me.workspace?.name || 'Not set'}`);
395
+ console.log(`project\t${manifest ? 'Not set' : active?.projectName || active?.projectSlug || active?.projectId || 'Not set'}`);
396
+ console.log(`funnel\t${manifest?.funnelId || active?.funnelName || active?.funnelSlug || active?.funnelId || 'Not set'}`);
342
397
  if (manifest) {
343
398
  console.log(`localWorkspace\t${manifest.workspaceId}`);
344
399
  console.log(`localFunnel\t${manifest.funnelId}`);
@@ -405,7 +460,7 @@ addExamples(funnelsCommand
405
460
  console.log(`${result.funnel.id}\t${result.funnel.name}\t${result.funnel.slug}`);
406
461
  });
407
462
  const syncCommand = addExamples(program.command('sync').description('Sync funnel source'), [
408
- 'fgrove sync down --dir ./claimbee-ios',
463
+ 'fgrove sync down --funnel claimbee-ios --dir ./claimbee-ios',
409
464
  'fgrove sync up --message "Update copy"',
410
465
  ]);
411
466
  addExamples(syncCommand
@@ -414,7 +469,6 @@ addExamples(syncCommand
414
469
  .option('--workspace <id-or-slug-or-name>', 'Workspace id, slug, or name')
415
470
  .option('--funnel <id-or-slug>', 'Funnel id or slug')
416
471
  .requiredOption('--dir <path>', 'Local target directory'), [
417
- 'fgrove sync down --dir ./claimbee-ios',
418
472
  'fgrove sync down --funnel claimbee-ios --dir ./claimbee-ios',
419
473
  ])
420
474
  .action(async (options) => {
@@ -608,7 +662,13 @@ addExamples(program
608
662
  domains: publishEnv === 'production' && options.domain ? [options.domain] : undefined,
609
663
  },
610
664
  });
611
- console.log(`${result.deploymentUrl}\tv${result.publishedVersionSeq}\t${result.publishedVersionId}`);
665
+ const readyDeployment = await waitForPublishDeployment({
666
+ token,
667
+ workspaceId: target.workspaceId,
668
+ funnelId: target.funnelId,
669
+ deploymentId: result.deploymentId,
670
+ });
671
+ console.log(`${readyDeployment.deployment_url || result.deploymentUrl}\tv${result.publishedVersionSeq}\t${result.publishedVersionId}`);
612
672
  });
613
673
  const githubCommand = addExamples(program.command('github').description('Manage GitHub funnel sync'), [
614
674
  'fgrove github status --funnel claimbee-general',
@@ -785,7 +845,9 @@ addExamples(program
785
845
  console.log(' npm install');
786
846
  console.log(' npm run dev');
787
847
  });
788
- program.parseAsync().catch((error) => {
789
- console.error(error instanceof Error ? error.message : String(error));
790
- process.exitCode = 1;
791
- });
848
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
849
+ program.parseAsync().catch((error) => {
850
+ console.error(error instanceof Error ? error.message : String(error));
851
+ process.exitCode = 1;
852
+ });
853
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "FunnelsGrove command-line tools for editing, syncing, and publishing funnels",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,7 +7,9 @@ Flow is product logic. The source of truth is `src/config/funnel.manifest.ts`; r
7
7
  The manifest defines:
8
8
 
9
9
  - `viewport`: the designed shell size, usually `430 x 932`.
10
- - `assets`: preloaded image metadata used by runtime/build tooling.
10
+ - `assets`: image metadata used by runtime/build tooling. Funnel shells may use
11
+ this data to warm likely next-step images, but first-viewport images should
12
+ still use the framework's normal priority/preload mechanism.
11
13
  - `steps`: every routable step with `id`, `path`, `filePath`, `componentKey`, `type`, optional `kind`, and optional `assetIds`.
12
14
  - `edgesByStepId`: graph edges between steps.
13
15
  - `experiments`: optional variant routing.
@@ -48,12 +48,16 @@ Runtime config should come from `runtimePublicConfig` through a local `src/runti
48
48
 
49
49
  Subscription paywalls with on-page wallet buttons should share one `useStripeSubscriptionCheckoutSession(...)` instance across the manual card checkout and wallet checkout. Do not build funnel-local Stripe PaymentIntent or Express Checkout flows.
50
50
 
51
+ - Keep funnel-specific checkout orchestration local when it includes product layout, analytics callbacks, return URL branching, or email prompt copy. The shared UI boundary is `SharedStripeCheckoutV2Dialog`; do not add another shared wrapper that duplicates a funnel's paywall composition.
51
52
  - Use `SharedStripeCheckoutV2Dialog` for the manual/card checkout modal. Open it from the paywall CTA after the checkout session has an `activeClientSecret` and `stripePromise`.
52
53
  - Render real wallet slots on the paywall with `ApplePaySubscriptionCheckoutSlot` and `GooglePaySubscriptionCheckoutSlot`, usually above the manual/card CTA. Pass the same checkout session, selected plan amount, return URL, customer details, summary label, success handler, and any `beforeConfirm` logic used to finalize email or analytics metadata.
53
54
  - Use `usePlatformWalletPaymentMethods()` to decide which slots to render. Desktop web can show Apple Pay and Google Pay; iOS shows Apple Pay; Android shows Google Pay.
54
55
  - Track each slot's `onAvailabilityChange` result by the current `checkoutSession.intentKey`. When opening `SharedStripeCheckoutV2Dialog`, derive `initialWalletAvailable` from the current intent's confirmed wallet availability and pass it into the dialog so checkout can show the wallet tab immediately when a wallet is already known to be available.
55
56
  - Treat unknown wallet availability as `null`, not `false`. Only hide/fallback from a wallet after Stripe or the shared slot reports that wallet unavailable.
57
+ - Treat Checkout Session/API preparation failures as retryable checkout failures, not wallet unavailability. A failed `prepareWalletCheckout()` should leave the wallet slot in unknown availability and show the real clickable fallback placeholder after loading stops; only Stripe readiness/availability reports should mark a wallet unavailable.
56
58
  - Suspend paywall wallet slots while the shared checkout dialog or another blocking offer modal is open, so two Stripe Express Checkout elements do not compete for the same active checkout session.
59
+ - If checkout starts without a known customer email, first try to resolve it from a known `user_id` or `stripe_customer_id`. Only show an email prompt when there is no known identity to fetch from, or that lookup returns no email.
60
+ - Before publishing payment changes, run the shared wallet smoke helper from `@funnelsgrove/payments` against local and preview paywall URLs. The package script is `npm run smoke:wallet --workspace @funnelsgrove/payments -- --url <paywall-url>` and accepts selector overrides through CLI flags or `WALLET_SMOKE_*` env vars.
57
61
 
58
62
  ## Paywall Identity Handoff
59
63
 
@@ -50,6 +50,8 @@ Always pass runtime checkout config so published funnels, preview funnels, and l
50
50
 
51
51
  Checkout endpoints receive `user_id` and `customerEmail` when available. Stripe metadata and `client_reference_id` rely on these values for webhook reconciliation, analytics, and project user support links.
52
52
 
53
+ When a funnel sends a configured Stripe `providerPlanId`, public checkout APIs validate that price id against Stripe and fail with a client-visible error if it is missing. Do not silently replace configured price ids with generated `price_data`; dynamic price fallback is only for legacy/catalog entries that have no provider price id.
54
+
53
55
  ## Subscription APIs
54
56
 
55
57
  Use the runtime service methods instead of calling the endpoints directly: