@funnelsgrove/cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/apiClient.js CHANGED
@@ -46,6 +46,16 @@ export async function callTrpcProcedure(input) {
46
46
  ? `${baseUrl}?input=${encodeURIComponent(JSON.stringify(input.input))}`
47
47
  : baseUrl;
48
48
  const response = await fetchFn(url, init);
49
- const json = await response.json();
49
+ const responseText = await response.text();
50
+ let json;
51
+ try {
52
+ json = JSON.parse(responseText);
53
+ }
54
+ catch {
55
+ const contentType = response.headers.get('content-type') || 'unknown content type';
56
+ const snippet = responseText.replace(/\s+/g, ' ').trim().slice(0, 500);
57
+ const details = snippet ? `: ${snippet}` : '';
58
+ throw new Error(`FunnelsGrove API returned ${response.status} ${response.statusText || 'Unknown status'} with ${contentType}${details}`);
59
+ }
50
60
  return parseTrpcJsonResponse(json);
51
61
  }
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
2
3
  import { cp, mkdir, readFile, writeFile } from 'node:fs/promises';
3
4
  import { createInterface } from 'node:readline/promises';
4
5
  import path from 'node:path';
@@ -6,10 +7,22 @@ import { fileURLToPath } from 'node:url';
6
7
  import { Command } from 'commander';
7
8
  import { callTrpcProcedure } from './apiClient.js';
8
9
  import { clearActiveContext, getDefaultAuthConfigPath, loadActiveContext, loadAuthToken, saveActiveContext, saveAuthToken, } from './authStore.js';
9
- import { buildSyncManifest, collectSourceFiles, readSyncManifest, writeSourceFiles, writeSyncManifest, } from './localSync.js';
10
+ import { buildSyncManifest, chunkChangedSourceFiles, collectChangedSourceFiles, collectSourceFiles, readSyncManifest, writeSourceFiles, writeSyncManifest, } from './localSync.js';
10
11
  import { isKnownTemplate, KNOWN_TEMPLATE_SLUGS, reskinFunnel } from './reskin.js';
11
12
  import { syncTemplateDocs, TEMPLATE_DOCS_DIR } from './templateDocs.js';
12
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const readCliVersion = () => {
15
+ try {
16
+ const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
17
+ if (typeof packageJson.version === 'string' && packageJson.version.trim()) {
18
+ return packageJson.version;
19
+ }
20
+ }
21
+ catch {
22
+ // Fall through to the packaged fallback below.
23
+ }
24
+ return '0.1.3';
25
+ };
13
26
  const toKebabCase = (value) => value
14
27
  .trim()
15
28
  .replace(/([a-z])([A-Z])/g, '$1-$2')
@@ -205,7 +218,7 @@ const program = new Command();
205
218
  program
206
219
  .name('fgrove')
207
220
  .description('FunnelsGrove CLI for editing, syncing, and publishing funnels')
208
- .version('0.1.0')
221
+ .version(readCliVersion())
209
222
  .option('--api-url <url>', 'FunnelsGrove tRPC API URL', process.env.FUNNELSGROVE_API_URL || DEFAULT_API_URL)
210
223
  .option('--config <path>', 'Auth config path', process.env.FUNNELSGROVE_CONFIG || getDefaultAuthConfigPath());
211
224
  addExamples(program, [
@@ -434,24 +447,60 @@ addExamples(syncCommand
434
447
  funnel: options.funnel,
435
448
  dir: options.dir,
436
449
  });
437
- const files = await collectSourceFiles(target.sourceDir);
438
- const result = await callApi({
439
- path: 'funnels.importSource',
440
- type: 'mutation',
441
- token,
442
- data: {
443
- workspaceId: target.workspaceId,
444
- funnelId: target.funnelId,
445
- message: options.message,
446
- files,
447
- },
448
- });
450
+ const changes = target.manifest
451
+ ? await collectChangedSourceFiles(target.sourceDir, target.manifest)
452
+ : null;
453
+ if (changes && changes.files.length === 0 && changes.deletedPaths.length === 0) {
454
+ console.log('No local changes to sync.');
455
+ return;
456
+ }
457
+ let result = null;
458
+ let syncedFileCount = 0;
459
+ let deletedFileCount = 0;
460
+ if (changes) {
461
+ const batches = chunkChangedSourceFiles(changes);
462
+ for (const batch of batches) {
463
+ result = await callApi({
464
+ path: 'funnels.patchSource',
465
+ type: 'mutation',
466
+ token,
467
+ data: {
468
+ workspaceId: target.workspaceId,
469
+ funnelId: target.funnelId,
470
+ message: options.message,
471
+ files: batch.files,
472
+ deletedPaths: batch.deletedPaths,
473
+ },
474
+ });
475
+ syncedFileCount += result.syncedFiles.length;
476
+ deletedFileCount += result.deletedFiles?.length || 0;
477
+ }
478
+ }
479
+ else {
480
+ result = await callApi({
481
+ path: 'funnels.importSource',
482
+ type: 'mutation',
483
+ token,
484
+ data: {
485
+ workspaceId: target.workspaceId,
486
+ funnelId: target.funnelId,
487
+ message: options.message,
488
+ files: await collectSourceFiles(target.sourceDir),
489
+ },
490
+ });
491
+ syncedFileCount = result.syncedFiles.length;
492
+ deletedFileCount = result.deletedFiles?.length || 0;
493
+ }
494
+ if (!result) {
495
+ throw new Error('No sync result returned.');
496
+ }
449
497
  await writeSyncManifest(target.sourceDir, await buildSyncManifest(target.sourceDir, {
450
498
  workspaceId: target.workspaceId,
451
499
  funnelId: target.funnelId,
452
500
  draftVersionId: result.versionId,
453
501
  }));
454
- console.log(`Synced ${result.syncedFiles.length} files to draft v${result.versionSeq} (${result.versionId})`);
502
+ const deletedSummary = deletedFileCount > 0 ? ` and removed ${deletedFileCount} files` : '';
503
+ console.log(`Synced ${syncedFileCount} files${deletedSummary} to draft v${result.versionSeq} (${result.versionId})`);
455
504
  });
456
505
  addExamples(program
457
506
  .command('publish')
@@ -16,10 +16,22 @@ export type SourceFile = {
16
16
  content: string;
17
17
  contentType?: string;
18
18
  };
19
+ export type ChangedSourceFiles = {
20
+ currentManifest: SyncManifest;
21
+ deletedPaths: string[];
22
+ files: SourceFile[];
23
+ };
24
+ export type SourceFilePatchBatch = {
25
+ deletedPaths: string[];
26
+ files: SourceFile[];
27
+ };
28
+ export declare const DEFAULT_PATCH_BATCH_CONTENT_CHARS = 12000000;
19
29
  export declare function normalizeSyncPath(filePath: string): string;
20
30
  export declare function shouldSyncFile(filePath: string): boolean;
21
31
  export declare function buildSyncManifest(rootDir: string, input: SyncManifestInput): Promise<SyncManifest>;
22
32
  export declare function writeSyncManifest(rootDir: string, manifest: SyncManifest): Promise<void>;
23
33
  export declare function readSyncManifest(rootDir: string): Promise<SyncManifest | null>;
24
34
  export declare function collectSourceFiles(rootDir: string): Promise<SourceFile[]>;
35
+ export declare function collectChangedSourceFiles(rootDir: string, previousManifest: SyncManifest): Promise<ChangedSourceFiles>;
36
+ export declare function chunkChangedSourceFiles(changes: Pick<ChangedSourceFiles, 'deletedPaths' | 'files'>, maxContentChars?: number): SourceFilePatchBatch[];
25
37
  export declare function writeSourceFiles(rootDir: string, files: SourceFile[]): Promise<void>;
package/dist/localSync.js CHANGED
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  export const SYNC_MANIFEST_FILE = '.funnelsgrove-sync.json';
5
+ export const DEFAULT_PATCH_BATCH_CONTENT_CHARS = 12_000_000;
5
6
  const EXCLUDED_PATH_PARTS = new Set(['node_modules', '.next', 'out']);
6
7
  export function normalizeSyncPath(filePath) {
7
8
  const normalized = path.posix.normalize(filePath.replaceAll('\\', '/'));
@@ -76,7 +77,58 @@ export async function collectSourceFiles(rootDir) {
76
77
  funnelId: '',
77
78
  draftVersionId: '',
78
79
  });
79
- const files = await Promise.all(manifest.files.map(async (file) => {
80
+ return readSourceFiles(rootDir, manifest.files);
81
+ }
82
+ export async function collectChangedSourceFiles(rootDir, previousManifest) {
83
+ const currentManifest = await buildSyncManifest(rootDir, {
84
+ workspaceId: previousManifest.workspaceId,
85
+ funnelId: previousManifest.funnelId,
86
+ draftVersionId: previousManifest.draftVersionId,
87
+ });
88
+ const previousHashByPath = new Map(previousManifest.files.map((file) => [file.path, file.hash]));
89
+ const currentHashByPath = new Map(currentManifest.files.map((file) => [file.path, file.hash]));
90
+ const changedManifestFiles = currentManifest.files.filter((file) => previousHashByPath.get(file.path) !== file.hash);
91
+ const deletedPaths = previousManifest.files
92
+ .filter((file) => !currentHashByPath.has(file.path))
93
+ .map((file) => file.path)
94
+ .sort((left, right) => left.localeCompare(right));
95
+ return {
96
+ currentManifest,
97
+ deletedPaths,
98
+ files: await readSourceFiles(rootDir, changedManifestFiles),
99
+ };
100
+ }
101
+ export function chunkChangedSourceFiles(changes, maxContentChars = DEFAULT_PATCH_BATCH_CONTENT_CHARS) {
102
+ const batches = [];
103
+ let currentBatch = {
104
+ deletedPaths: changes.deletedPaths,
105
+ files: [],
106
+ };
107
+ let currentContentChars = 0;
108
+ const pushCurrentBatch = () => {
109
+ if (currentBatch.files.length === 0 && currentBatch.deletedPaths.length === 0) {
110
+ return;
111
+ }
112
+ batches.push(currentBatch);
113
+ currentBatch = {
114
+ deletedPaths: [],
115
+ files: [],
116
+ };
117
+ currentContentChars = 0;
118
+ };
119
+ for (const file of changes.files) {
120
+ if (currentBatch.files.length > 0 &&
121
+ currentContentChars + file.content.length > maxContentChars) {
122
+ pushCurrentBatch();
123
+ }
124
+ currentBatch.files.push(file);
125
+ currentContentChars += file.content.length;
126
+ }
127
+ pushCurrentBatch();
128
+ return batches;
129
+ }
130
+ async function readSourceFiles(rootDir, manifestFiles) {
131
+ const files = await Promise.all(manifestFiles.map(async (file) => {
80
132
  const absolutePath = path.join(rootDir, assertSafeSyncPath(file.path));
81
133
  const imageContentType = inferImageContentType(file.path);
82
134
  const buffer = await readFile(absolutePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "FunnelsGrove command-line tools for editing, syncing, and publishing funnels",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "access": "public"
17
17
  },
18
18
  "scripts": {
19
- "build": "rm -rf dist && tsc",
19
+ "build": "rm -rf dist && tsc && chmod +x dist/cli.js",
20
20
  "prepack": "npm run build",
21
21
  "test": "vitest run"
22
22
  },
@@ -0,0 +1,102 @@
1
+ # AGENTS.md
2
+
3
+ Behavioral guidelines to reduce common LLM coding mistakes. Merge these rules with project-specific instructions as needed.
4
+
5
+ **Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
6
+
7
+ ## Read Docs Before Editing
8
+
9
+ Before changing any funnel file, read the relevant topic docs in this folder and follow their contracts. Make the edit according to those docs first; if the requested change conflicts with the docs, stop and explain the conflict before editing.
10
+
11
+ ## 1. Think Before Coding
12
+
13
+ **Don't assume. Don't hide confusion. Surface tradeoffs.**
14
+
15
+ Before implementing:
16
+ - State your assumptions explicitly. If uncertain, ask.
17
+ - If multiple interpretations exist, present them. Do not pick silently.
18
+ - If a simpler approach exists, say so. Push back when warranted.
19
+ - If something is unclear, stop. Name what's confusing. Ask.
20
+
21
+ ## 2. Simplicity First
22
+
23
+ **Minimum code that solves the problem. Nothing speculative.**
24
+
25
+ - No features beyond what was asked.
26
+ - No abstractions for single-use code.
27
+ - No flexibility or configurability that was not requested.
28
+ - No error handling for impossible scenarios.
29
+ - If you write 200 lines and it could be 50, rewrite it.
30
+
31
+ Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
32
+
33
+ ## 3. Surgical Changes
34
+
35
+ **Touch only what you must. Clean up only your own mess.**
36
+
37
+ When editing existing code:
38
+ - Do not improve adjacent code, comments, or formatting.
39
+ - Do not refactor things that are not broken.
40
+ - Match existing style, even if you would do it differently.
41
+ - If you notice unrelated dead code, mention it. Do not delete it.
42
+
43
+ When your changes create orphans:
44
+ - Remove imports, variables, functions, and files that your changes made unused.
45
+ - Do not remove pre-existing dead code unless asked.
46
+
47
+ The test: every changed line should trace directly to the user's request.
48
+
49
+ ## 4. Goal-Driven Execution
50
+
51
+ **Define success criteria. Loop until verified.**
52
+
53
+ Transform tasks into verifiable goals:
54
+ - "Add validation" -> "Write tests for invalid inputs, then make them pass"
55
+ - "Fix the bug" -> "Write a test that reproduces it, then make it pass"
56
+ - "Refactor X" -> "Ensure tests pass before and after"
57
+
58
+ For multi-step tasks, state a brief plan:
59
+
60
+ ```text
61
+ 1. [Step] -> verify: [check]
62
+ 2. [Step] -> verify: [check]
63
+ 3. [Step] -> verify: [check]
64
+ ```
65
+
66
+ Strong success criteria let you loop independently. Weak criteria like "make it work" require constant clarification.
67
+
68
+ ---
69
+
70
+ **These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
71
+
72
+ ## Documentation Index
73
+
74
+ Use this folder as the local source-of-truth guide when editing a synced FunnelsGrove funnel.
75
+
76
+ Current funnel templates are small Next.js apps built on shared runtime modules:
77
+
78
+ - `@funnelsgrove/runtime` owns the funnel contract: routing, user state, content localization, builder preview patches, subscriptions, analytics helpers, runtime env, and theme variables.
79
+ - `@funnelsgrove/analytics` owns analytics tracking and A/B experiment tracking. It encapsulates the PostHog integration so funnel steps do not call PostHog directly.
80
+ - `@funnelsgrove/payments` owns checkout plans, discounts, Stripe sessions/intents, wallet slots, and shared checkout UI.
81
+ - The local funnel owns product decisions: step views, content files, editor fields, flow manifest, theme, checked-in assets, and billing plan ids.
82
+
83
+ Keep changes simple and local. Most edits should touch one step file plus its matching `content/` and `editor/` files, or one manifest/config file for flow, theme, or billing changes.
84
+
85
+ ## Funnel Workflow
86
+
87
+ 1. Run `fgrove status` and confirm the active project and funnel.
88
+ 2. Keep edits inside the synced funnel tree.
89
+ 3. Run the funnel's local checks before syncing.
90
+ 4. Run `fgrove sync up --message '<summary>'`.
91
+ 5. Run `fgrove publish --env preview --message '<summary>'` and verify the preview.
92
+ 6. Publish production only when explicitly requested.
93
+
94
+ - [Editing or Creating a Step](docs/editing-step.md)
95
+ - [Editing Flow](docs/editing-flow.md)
96
+ - [Editor and Content](docs/editor-and-content.md)
97
+ - [Payment Plans and Discounts](docs/payment-plans-and-discounts.md)
98
+ - [SDK API Endpoints](docs/sdk-api-endpoints.md)
99
+ - [Analytics](docs/analytics.md)
100
+ - [A/B Experiments](docs/ab-experiments.md)
101
+ - [Theme](docs/theme.md)
102
+ - [Publishing and Versioning](docs/publishing-and-versioning.md)
@@ -0,0 +1,3 @@
1
+ @AGENTS.md
2
+
3
+ Claude Code should use `AGENTS.md` as the shared agent guidance for this synced funnel. Merge those rules with the user's current request and any project-specific instructions.
@@ -1,10 +1,33 @@
1
1
  # A/B Experiments
2
2
 
3
- Experiments should isolate one decision and make results trustworthy.
4
-
5
- Principles:
6
- - Define the hypothesis before changing variants.
7
- - Keep assignment stable for a user during the experiment.
8
- - Change one major variable per experiment when possible.
9
- - Make variant names clear and durable.
10
- - Do not remove the control until results are reviewed and the rollout is approved.
3
+ Experiments live in `src/config/funnel.manifest.ts`. They are routing rules, not ad hoc conditionals inside step components.
4
+
5
+ ## Runtime Contract
6
+
7
+ An experiment attaches to a source step and maps variant keys to routable step ids:
8
+
9
+ ```ts
10
+ {
11
+ experimentId: 'paywall-ab',
12
+ stepId: 'paywall',
13
+ variants: [
14
+ { variantKey: 'control', routeToStepId: 'paywall' },
15
+ { variantKey: 'variant_b', routeToStepId: 'paywall-b' },
16
+ ],
17
+ }
18
+ ```
19
+
20
+ The flow controller resolves the experiment assignment through the shared analytics/runtime integration and routes to the matched variant. `@funnelsgrove/analytics` encapsulates api for experiment tracking. If the assignment is not ready on an experiment step, rendering is suspended to avoid flashing the control before the variant resolves.
21
+
22
+ ## Editor and Preview
23
+
24
+ When `?editor=true` is enabled, the editor panel can force runtime mode and select experiment variants. Builder preview can also navigate steps through the preview bridge. This lets agents verify each variant without changing the manifest.
25
+
26
+ ## Rules
27
+
28
+ - Keep `control` stable.
29
+ - Keep variant keys durable and readable.
30
+ - Test the attachment step and every variant route.
31
+ - Do not put unrelated changes in a variant; isolate one major decision.
32
+ - Do not remove a live variant until analysis and rollout are approved.
33
+ - If a variant changes paywall plans or checkout behavior, update `@funnelsgrove/analytics` event expectations too.
@@ -1,10 +1,58 @@
1
1
  # Analytics
2
2
 
3
- Analytics should describe meaningful user behavior, not implementation details.
4
-
5
- Principles:
6
- - Keep event names stable unless a migration is planned.
7
- - Track step views, key choices, checkout starts, purchases, and major errors.
8
- - Include only dimensions needed for analysis.
9
- - Do not send personal data or secrets.
10
- - When changing flow or offers, verify analytics still fires on the new path.
3
+ Analytics should describe meaningful user behavior. `@funnelsgrove/analytics` is the tracking boundary for funnel code: it encapsulates the analytics integration for event delivery and experiment metadata.
4
+
5
+ ## Standard Events
6
+
7
+ `FunnelFlow` tracks:
8
+
9
+ - `step_start` when a step becomes active.
10
+ - `step_end` when the user leaves a step, including the answer diff selected during that step.
11
+
12
+ `@funnelsgrove/analytics` maps canonical runtime event types to provider event names:
13
+
14
+ - `step_start` -> `step_started`.
15
+ - `step_end` -> `step_completed`.
16
+ - `funnel_start` -> `funnel_started`.
17
+
18
+ Experiment assignments are attached as feature flag properties when available. Preview runtime skips normal analytics delivery.
19
+
20
+ ## Custom Events
21
+
22
+ Use `publicAnalyticsSdk.track(...)` from `@funnelsgrove/analytics`:
23
+
24
+ ```ts
25
+ publicAnalyticsSdk.track({
26
+ eventType: 'checkout_started',
27
+ stepId: stepPaywallId,
28
+ stepName: stepPaywall.name || stepPaywall.title,
29
+ metadata: {
30
+ planId,
31
+ providerPlanId,
32
+ couponId,
33
+ amountCents,
34
+ environment,
35
+ },
36
+ });
37
+ ```
38
+
39
+ Flush before redirects or payment handoff when losing the page would drop the event:
40
+
41
+ ```ts
42
+ await publicAnalyticsSdk.flush().catch(() => 0);
43
+ ```
44
+
45
+ ## Experiment Tracking
46
+
47
+ Flow and experiment assignment are configured in `src/config/funnel.manifest.ts`, while tracking uses the analytics package. When an experiment is active, the runtime passes assignment metadata into `publicAnalyticsSdk.trackStepStarted(...)`, `trackStepCompleted(...)`, and custom events through feature flag properties.
48
+
49
+ Agents should only document and verify the analytics package surface: stable event names, step ids, selected answers, checkout metadata, and experiment metadata. Treat the underlying provider as an implementation detail.
50
+
51
+ ## Agent Rules
52
+
53
+ - Keep event names stable.
54
+ - Use `metadata`, `payload`, `context`, and `selected` for small analysis fields only.
55
+ - Do not send personal data, secrets, full payment objects, SDK keys, or raw Stripe responses.
56
+ - Use `@funnelsgrove/analytics` for custom tracking; do not call the provider SDK directly.
57
+ - When changing flow, verify step start/end still fires on the new path.
58
+ - When changing offers, verify checkout events include plan id, provider plan id, coupon id, amount, and mode.
@@ -1,10 +1,71 @@
1
1
  # Editing Flow
2
2
 
3
- The flow controls how users move between steps. Treat flow edits as product logic changes, not only UI changes.
4
-
5
- Principles:
6
- - Keep the happy path short and obvious.
7
- - Make every conditional branch easy to explain.
8
- - Preserve back/forward behavior when the funnel supports it.
9
- - Confirm that skipped steps do not leave required state missing.
10
- - Update analytics events when a branch, gate, or conversion path changes.
3
+ Flow is product logic. The source of truth is `src/config/funnel.manifest.ts`; runtime helpers in `@funnelsgrove/runtime` resolve paths, edges, entry points, experiments, and browser history.
4
+
5
+ ## Manifest Contract
6
+
7
+ The manifest defines:
8
+
9
+ - `viewport`: the designed shell size, usually `430 x 932`.
10
+ - `assets`: preloaded image metadata used by runtime/build tooling.
11
+ - `steps`: every routable step with `id`, `path`, `filePath`, `componentKey`, `type`, optional `kind`, and optional `assetIds`.
12
+ - `edgesByStepId`: graph edges between steps.
13
+ - `experiments`: optional variant routing.
14
+
15
+ Keep `steps[].id`, `path`, and answer keys stable unless the request is a migration.
16
+
17
+ ## Routing Rules
18
+
19
+ Sequential routes use:
20
+
21
+ ```ts
22
+ edgesByStepId: {
23
+ claim: [{ toStepId: 'profile' }],
24
+ }
25
+ ```
26
+
27
+ Conditional routes use `conditionId` values written by `goChoice(...)`. For yes/no choices the runtime expects ids like:
28
+
29
+ ```ts
30
+ eligibility: [
31
+ { toStepId: 'active-google-claim', conditionId: 'eligibility:yes' },
32
+ { toStepId: 'subscriptions', conditionId: 'eligibility:no' },
33
+ ]
34
+ ```
35
+
36
+ The flow controller resolves the next step from configured edges first, then falls back to the sequential order. It also keeps the URL in sync through `getPathForStep(...)` and browser history.
37
+
38
+ ## Step Navigation
39
+
40
+ Use the funnel context:
41
+
42
+ - `goNext()` for normal manifest progression.
43
+ - `goChoice('yes' | 'no')` when the current branch is represented by conditional edges.
44
+ - `goToStep(stepId)` for explicit jumps such as paywall success or manage-subscription return.
45
+
46
+ Do not hardcode route strings in step logic when `getPathForStep(...)` or context navigation is available.
47
+
48
+ ## Experiments
49
+
50
+ Experiments attach to a step and route to variant steps:
51
+
52
+ ```ts
53
+ {
54
+ experimentId: 'paywall-ab',
55
+ stepId: 'paywall',
56
+ variants: [
57
+ { variantKey: 'control', routeToStepId: 'paywall' },
58
+ { variantKey: 'variant_b', routeToStepId: 'paywall-b' },
59
+ ],
60
+ }
61
+ ```
62
+
63
+ The runtime resolves assignments through the shared analytics/runtime integration outside preview and uses editor overrides inside preview/editor mode. Keep the control variant stable and do not remove a running variant until analytics have been reviewed.
64
+
65
+ ## Checklist
66
+
67
+ - Add or update the manifest step.
68
+ - Register the component in `src/runtime/step-registry.ts`.
69
+ - Update `edgesByStepId`, `entryPoints`, and `assetIds` if needed.
70
+ - Make sure skipped steps do not own required answers.
71
+ - Verify the step before, the edited step, and the step after.
@@ -1,10 +1,66 @@
1
- # Editing a Step
1
+ # Editing or Creating a Step
2
2
 
3
- A step is one screen or decision point in the funnel. Keep step changes focused: copy, layout, input behavior, validation, and navigation for that screen.
3
+ A step is one screen or decision point. Keep its view, local state, validation, and step-specific CSS in the step file. Keep editable copy/images in a matching content file and builder fields in a matching editor file.
4
4
 
5
- Principles:
6
- - Preserve existing step ids and route keys unless the change is explicitly a migration.
7
- - Reuse existing UI components, tokens, and helper functions.
8
- - Keep step state local unless another step needs it.
9
- - Make button labels, validation messages, and analytics events match the changed user action.
10
- - Test the edited step directly and also test the step before and after it.
5
+ ## Source Files
6
+
7
+ For an existing step, start from `src/config/funnel.manifest.ts` and find:
8
+
9
+ - `steps[].id`: stable route/runtime id, such as `paywall`.
10
+ - `steps[].path`: URL path, such as `/paywall`.
11
+ - `steps[].filePath`: React view file, such as `src/steps/step-32-paywall.tsx`.
12
+ - `steps[].componentKey`: key registered in `src/runtime/step-registry.ts`.
13
+
14
+ Most steps also have:
15
+
16
+ - `src/steps/content/<step>.content.ts`: localized content using `LocalizedStepContent`.
17
+ - `src/steps/editor/<step>.editor.ts`: builder editor fields using `StepEditorSection`.
18
+ - `src/steps/step-content.registry.ts`: maps the runtime step id to those content/editor files.
19
+
20
+ ## View Pattern
21
+
22
+ Step files usually export:
23
+
24
+ - A stable `stepXId`.
25
+ - A `FunnelStepMeta` object with `id`, `type`, `title`, optional `kind`, and optional `actionBar`.
26
+ - A React component that reads content with `usePreviewStepLocalizedContent(...)` or the funnel's wrapper, such as `useClaimbeeStepContent(...)`.
27
+
28
+ Use `useFunnel()` for runtime state:
29
+
30
+ - `answers` / `attributes`: current funnel answers.
31
+ - `setAnswer(key, value)` or `setAttribute(key, value)`: store data another step needs.
32
+ - `goNext()`, `goToStep(stepId)`, or `goChoice('yes' | 'no')`: navigate through the manifest.
33
+ - `user`, `setUser`, `completeStep`: only when the step explicitly needs user/session behavior.
34
+
35
+ Do not bypass the flow controller with raw `window.location` for normal funnel navigation.
36
+
37
+ ## CSS Pattern
38
+
39
+ Keep one-off step styling at the bottom of the step file:
40
+
41
+ ```tsx
42
+ return (
43
+ <>
44
+ <section className='claimbee-step'>...</section>
45
+ <style>{stepStyles}</style>
46
+ </>
47
+ );
48
+
49
+ const stepStyles = `
50
+ .claimbee-step { ... }
51
+ `;
52
+ ```
53
+
54
+ Use shared CSS under `src/steps/styles/shared/` only for patterns reused by multiple steps. Do not move one-off Figma parity or paywall-specific styling into shared files.
55
+
56
+ ## Creating a Step
57
+
58
+ 1. Add the view file in `src/steps/`.
59
+ 2. Add `content/<step>.content.ts` with the exact content shape the view needs.
60
+ 3. Add `editor/<step>.editor.ts` exposing only fields that should be editable.
61
+ 4. Register the component in `src/runtime/step-registry.ts`.
62
+ 5. Add content/editor paths in `src/steps/step-content.registry.ts`.
63
+ 6. Add the step and route edge in `src/config/funnel.manifest.ts`.
64
+ 7. Add focused tests when changing routing, state, checkout, parsing, or non-trivial UI behavior.
65
+
66
+ Preserve existing ids unless the work is an explicit migration. A renamed step id changes routes, persisted answers, analytics, builder preview patches, and publish history.
@@ -1,10 +1,58 @@
1
1
  # Editor and Content
2
2
 
3
- Content should be editable without making the runtime fragile. Prefer existing content structures over hardcoded one-off strings.
4
-
5
- Principles:
6
- - Keep user-facing copy in the same content pattern the funnel already uses.
7
- - Use plain, direct copy for actions and error states.
8
- - Keep legal, pricing, and disclaimer text exact.
9
- - Do not add unused content fields.
10
- - If content is repeated across steps, centralize it only when that matches the current funnel pattern.
3
+ Editable step content is a runtime contract. The step view should read from typed content, not from scattered hardcoded strings, when the value is copy, image, legal text, pricing label, FAQ, testimonial, or builder-editable UI.
4
+
5
+ ## Content Files
6
+
7
+ Each step content file exports a typed `LocalizedStepContent<T>`:
8
+
9
+ ```ts
10
+ export const stepContent = {
11
+ defaultLocale: 'en',
12
+ locales: {
13
+ en: {
14
+ headline: ['People getting', 'free money', 'know one secret'],
15
+ artwork: { src: '/figma/example.png', alt: '' },
16
+ },
17
+ },
18
+ } as const satisfies LocalizedStepContent<StepLocaleContent>;
19
+ ```
20
+
21
+ The runtime validates that a default locale exists and resolves locale from funnel attributes (`browserLanguage`, `language`, `locale`) or browser locale. In preview, builder content patches are merged before the step renders.
22
+
23
+ Use runtime item types where possible:
24
+
25
+ - `StepImage` for images.
26
+ - `ChoiceItem`, `InfoItem`, `QuoteItem`, `QaItem`, `LinkItem`, `ReasonItem`, `StoreLinkItem`, `PlanPresentationItem` for lists.
27
+ - `CountryPricingProfile` for country-specific plan ordering/defaults.
28
+
29
+ ## Editor Files
30
+
31
+ Each editor file exports `readonly StepEditorSection[]`. A field's `path` must match the content object path:
32
+
33
+ ```ts
34
+ {
35
+ id: 'hero.image',
36
+ label: 'Hero image',
37
+ kind: 'image',
38
+ path: 'hero.image',
39
+ }
40
+ ```
41
+
42
+ Supported field kinds are `text`, `textLines`, `textarea`, `image`, `boolean`, `select`, `list`, and `pricingProfile`. List fields must use a preset such as `choiceItems`, `infoItems`, `quoteItems`, `qaItems`, or `linkItems`.
43
+
44
+ Only expose fields the builder should edit. Do not add spare content fields for possible future use.
45
+
46
+ ## Variables
47
+
48
+ For builder variables, set `supportsVariables: true` on text-capable fields only. Editor variables map `user.*` tokens to answer paths, then runtime interpolation replaces `{{ user.token }}` style values in supported fields. Universal variables also support simple `{user_id}` replacement at render time.
49
+
50
+ Keep variable use obvious. If a step depends on a value from an earlier step, make sure that earlier step writes a stable answer key.
51
+
52
+ ## Agent Rules
53
+
54
+ - Keep copy, images, FAQ, testimonials, legal strings, and checkout labels in content files.
55
+ - Keep layout decisions in the step file.
56
+ - Keep editor sections aligned with the content shape.
57
+ - Keep `src/steps/step-content.registry.ts` in sync when adding or renaming content/editor files.
58
+ - Use checked-in public assets (`/paywall/...`, `/figma/...`, etc.); do not leave temporary localhost asset URLs.
@@ -1,10 +1,53 @@
1
1
  # Payment Plans and Discounts
2
2
 
3
- Payment changes affect conversion, billing, and support. Keep them explicit and easy to review.
4
-
5
- Principles:
6
- - Treat plan ids, price ids, trial lengths, and discount codes as stable contracts.
7
- - Do not rename or remove a live plan unless the rollout calls for it.
8
- - Keep displayed price, billing period, trial text, and checkout payload in sync.
9
- - Test free trials, discounted offers, and default paid plans separately.
10
- - Update paywall copy and analytics when an offer changes.
3
+ Payments are handled through `@funnelsgrove/payments`. Funnel steps should use the shared payment module instead of building raw Stripe calls.
4
+
5
+ ## Plan Sources
6
+
7
+ Local plans live in `src/config/billing.plans.ts`. A plan catalog is keyed by stable funnel keys (`primary`, `secondary`, `tertiary`) and each plan includes:
8
+
9
+ - `projectPlanId`: FunnelsGrove project plan id.
10
+ - `providerPlanId`: Stripe price id.
11
+ - `title`, `priceLabel`, `perDayAmount`, `amountCents`.
12
+ - Optional `featuredTag`, `oldPriceLabel`, `checkoutSummaryLabel`, `billingInterval`, `billingIntervalCount`, `isDefault`.
13
+
14
+ ClaimBee-style funnels may keep separate test/live catalogs with `createRuntimeModeBillingPlanCatalog(...)` and choose the active catalog through `useRuntimeMode()` plus checkout mode resolution.
15
+
16
+ ## Runtime Resolution
17
+
18
+ Use the shared helpers:
19
+
20
+ - `buildConfigPaywallPlans(...)` for local catalogs.
21
+ - `useResolvedPaywallPlans(...)` when the funnel can load synced project Stripe plans.
22
+ - `getDefaultPlanId(...)`, `findPaywallPlan(...)`, and `getPaywallPlanSelectionValue(...)` for selection.
23
+ - `usePreviewStepPaywallPlans(stepId)` so builder paywall plan edits appear in preview.
24
+
25
+ If a catalog is only a mapping of `{ projectPlanId }`, the payment module can load remote project plans from `/sdk/public/payments/plans` and map them back to funnel keys.
26
+
27
+ ## Discounts
28
+
29
+ Discounts use `BillingDiscountList` or `BillingDiscountCatalog` and are resolved by `buildBillingDiscountCatalog(...)`. The current ClaimBee pattern has two stages:
30
+
31
+ - First stage: coupon id, percent, duration.
32
+ - Second stage: stronger coupon id, percent, duration, and previous percent.
33
+
34
+ Use `resolvePaywallDiscountState(...)`, `advancePaywallDiscountState(...)`, `activateSecondPaywallDiscount(...)`, `serializePaywallDiscountState(...)`, and `buildDiscountedPaywallPlans(...)`. Store discount state through runtime paywall state helpers (`readPaywallStateValue`, `updatePaywallStateValue`) so it is scoped by funnel id.
35
+
36
+ ## Checkout
37
+
38
+ Prefer shared Stripe surfaces:
39
+
40
+ - `useStripeSubscriptionCheckoutSession(...)` for subscription checkout sessions.
41
+ - `SharedStripeCheckoutV2Dialog` for card checkout UI.
42
+ - `ApplePaySubscriptionCheckoutSlot` and `GooglePaySubscriptionCheckoutSlot` for wallet buttons.
43
+ - `StripeExpressCheckoutButton` / `StripePlanSelector` for older paywall variants.
44
+
45
+ Runtime config should come from `runtimePublicConfig` through a local `src/runtime/checkout-runtime-config.ts`. Include `apiBaseUrl`, `funnelId`, and the SDK publishable key. Preview seed keys may resolve to the preview payment key and force test checkout mode.
46
+
47
+ ## Agent Rules
48
+
49
+ - Never change a live `projectPlanId`, `providerPlanId`, coupon id, or amount casually.
50
+ - Keep displayed price, discounted price, amount cents, coupon id, checkout payload, and analytics metadata in sync.
51
+ - Track checkout starts/completions with plan id, provider plan id, coupon id, amount, and environment.
52
+ - Keep legal, renewal, guarantee, support, and button labels in content files.
53
+ - Test test-mode and live-mode plan resolution separately when a payment change affects both.
@@ -1,10 +1,42 @@
1
1
  # Publishing and Versioning
2
2
 
3
- Syncing creates drafts. Publishing makes a draft available to users.
3
+ Local edits are not public until they are synced and published. Sync creates or patches a draft version. Publish deploys that draft.
4
4
 
5
- Principles:
6
- - Use clear sync and publish messages so versions are reviewable.
5
+ ## CLI Flow
6
+
7
+ ```bash
8
+ fgrove status
9
+ fgrove sync up --message '<summary>'
10
+ fgrove publish --env preview --message '<summary>'
11
+ ```
12
+
13
+ Only publish production when explicitly requested:
14
+
15
+ ```bash
16
+ fgrove publish --env production --domain <domain> --message '<summary>'
17
+ ```
18
+
19
+ Production publish requires a domain. Preview publish returns a deployment URL, version sequence, and version id.
20
+
21
+ ## Local Sync Contract
22
+
23
+ The CLI writes `.funnelsgrove-sync.json` into the local tree. Keep it there. It records workspace id, funnel id, and current draft version id so later `sync up` can patch only changed/deleted files.
24
+
25
+ Local-only files are excluded from upload:
26
+
27
+ - `.env`, `.env.local`, `.env.*` except `.env.example`.
28
+ - `.funnelsgrove-sync.json`.
29
+ - `node_modules`.
30
+ - `.next`.
31
+ - `out`.
32
+
33
+ Do not edit generated build output as the source of truth.
34
+
35
+ ## Agent Rules
36
+
37
+ - Run `fgrove status` before syncing.
38
+ - Use clear sync and publish messages.
39
+ - Sync before publishing.
7
40
  - Publish preview first and verify the returned URL.
8
- - Production publish requires an explicit production request and target domain.
9
- - Keep `.funnelsgrove-sync.json` in the local tree; it records the synced draft.
10
- - If preview verification fails, fix locally, sync up again, and publish a new preview.
41
+ - If preview verification fails, fix locally, run checks, sync up again, and publish a new preview.
42
+ - Do not production publish without explicit instruction and target domain.
@@ -1,10 +1,46 @@
1
1
  # SDK API Endpoints
2
2
 
3
- SDK calls should be thin, typed boundaries between the funnel and FunnelsGrove services.
4
-
5
- Principles:
6
- - Reuse existing SDK helpers instead of calling raw endpoints directly.
7
- - Keep request payloads minimal and typed.
8
- - Handle loading, retryable errors, and final failure states in the UI.
9
- - Avoid logging personal data, payment data, or secrets.
10
- - When an endpoint contract changes, update the caller, tests, and docs together.
3
+ Funnel code should call shared runtime/payment/analytics services, not raw endpoints, unless a new shared helper is being built.
4
+
5
+ ## Runtime Config
6
+
7
+ `@funnelsgrove/runtime` reads public config from env aliases and exposes `runtimePublicConfig`:
8
+
9
+ - SDK API base URL.
10
+ - funnel id and funnel version id.
11
+ - SDK publishable key.
12
+ - project id and analytics provider key.
13
+ - support/legal/app-store/deep-link metadata.
14
+
15
+ `buildMainApiUrl(...)` and `buildSdkHeaders(...)` add the correct SDK base URL and `x-sdk-publishable-key`. Preview frame runtime intentionally suppresses normal SDK calls.
16
+
17
+ ## User and State APIs
18
+
19
+ Use `apiService` from `@funnelsgrove/runtime`:
20
+
21
+ - `bootstrapSession(...)`: create/restore the visitor using local user id, URL `user_id`, attribution, email, or name.
22
+ - `updateUser(...)`: persist email/name/progress attributes.
23
+ - `uploadTempPhoto(...)`: upload or locally preview image captures.
24
+ - `getManageSubscriptions()` and `updateSubscription(...)`: subscription management by `user_id` or `stripe_customer_id`.
25
+ - `trackFunnelEvent(...)`, `trackStepStarted(...)`, `trackStepCompleted(...)`: legacy runtime event delivery.
26
+
27
+ The flow controller already bootstraps the user, persists answer attributes, records completed steps, and connects analytics identity through the shared analytics/runtime integration. Steps should only call `apiService.updateUser(...)` directly when they collect profile data such as checkout email.
28
+
29
+ ## Payment APIs
30
+
31
+ Use `@funnelsgrove/payments`:
32
+
33
+ - `getPaywallPlans(...)` / `useResolvedPaywallPlans(...)` for `/sdk/public/payments/plans`.
34
+ - `createStripePaymentIntent(...)` for payment intents.
35
+ - `createStripeSubscriptionCheckout(...)` for subscription Elements checkout.
36
+ - `createStripeCheckoutSession(...)` or `redirectToStripeCheckout(...)` for hosted checkout.
37
+
38
+ Always pass runtime checkout config so published funnels, preview funnels, and local builds resolve the correct funnel id and SDK key.
39
+
40
+ ## Rules
41
+
42
+ - Keep payloads minimal and typed.
43
+ - Do not log personal data, payment details, SDK keys, or Stripe secrets.
44
+ - Handle loading, unavailable config, API failure, and final failure states in UI.
45
+ - Use local fallbacks only where the shared service already supports them.
46
+ - If an endpoint contract changes, update the shared service, callers, tests, and these docs together.
@@ -1,10 +1,35 @@
1
1
  # Theme
2
2
 
3
- Theme changes should make the funnel feel consistent, not create one-off styling.
4
-
5
- Principles:
6
- - Use existing tokens for colors, spacing, typography, and radius.
7
- - Keep contrast readable across all important states.
8
- - Check mobile and desktop layouts after theme edits.
9
- - Avoid changing component internals for a theme-only request.
10
- - Update shared theme values when the same style appears in multiple places.
3
+ Theme is a runtime contract in `src/theme/theme.ts`. It is converted to CSS variables by `createThemeCssVariables(...)` from `@funnelsgrove/runtime`.
4
+
5
+ ## Theme Contract
6
+
7
+ The theme owns:
8
+
9
+ - Interface colors: `background`, `primary`, `primaryText`, `secondary`, `secondaryText`, `text`.
10
+ - Semantic colors: `button`, `buttonText`, `surface`, `border`, `warning`, `danger`.
11
+ - Derived colors: muted/success/warning/danger/neutral backgrounds, muted text, soft accents.
12
+ - Typography: font family, weights, sizes, line heights.
13
+ - Device shell: active preset, shell width, max width, padding, corner radius.
14
+ - Safe area offsets.
15
+
16
+ The generated CSS vars include `--color-bg`, `--color-primary`, `--color-secondary`, `--color-text`, `--font-family-base`, `--padding`, `--corner-radius`, and shell sizing vars.
17
+
18
+ ## Styling Rules
19
+
20
+ - Use CSS variables in step CSS and shared CSS.
21
+ - Use `src/app/globals.css` only for global reset, fonts, and body/root behavior.
22
+ - Use `src/steps/styles/shared/` for repeated step patterns.
23
+ - Keep one-off styles inside the step file at the bottom.
24
+ - Keep the designed shell stable unless the request is explicitly about device size.
25
+
26
+ ClaimBee-style funnels use a 430px mobile shell, Inter, and product colors through theme variables. If changing brand colors, update `theme.ts` first and only add local CSS overrides when the layout genuinely needs them.
27
+
28
+ ## Verification
29
+
30
+ After theme edits, check:
31
+
32
+ - Main happy path.
33
+ - Paywall and checkout states.
34
+ - Mobile shell and desktop centered preview.
35
+ - Contrast for primary buttons, secondary accents, disabled/loading states, and error text.
@@ -1,24 +0,0 @@
1
- # Funnel Editing Guide
2
-
3
- Use this folder as the local source-of-truth guide when editing a synced FunnelsGrove funnel.
4
-
5
- ## Workflow
6
-
7
- 1. Run `fgrove status` and confirm the active project and funnel.
8
- 2. Keep edits inside the synced funnel tree.
9
- 3. Run the funnel's local checks before syncing.
10
- 4. Run `fgrove sync up --message '<summary>'`.
11
- 5. Run `fgrove publish --env preview --message '<summary>'` and verify the preview.
12
- 6. Publish production only when explicitly requested.
13
-
14
- ## Topics
15
-
16
- - [Editing a Step](docs/editing-step.md)
17
- - [Editing Flow](docs/editing-flow.md)
18
- - [Editor and Content](docs/editor-and-content.md)
19
- - [Payment Plans and Discounts](docs/payment-plans-and-discounts.md)
20
- - [SDK API Endpoints](docs/sdk-api-endpoints.md)
21
- - [Analytics](docs/analytics.md)
22
- - [A/B Experiments](docs/ab-experiments.md)
23
- - [Theme](docs/theme.md)
24
- - [Publishing and Versioning](docs/publishing-and-versioning.md)