@embeddables/cli 0.5.1 → 0.6.1

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.
@@ -77,7 +77,7 @@ All component types are derived from `src/types-builder.ts`. The relevant typing
77
77
  **Key Base Properties** (all components have these):
78
78
 
79
79
  - `id: string` - Unique identifier - this must be unique across the entire Embeddable JSON. Always snake_case.
80
- - `key: string` - Unique identifier, used in React as key prop - unique in general but duplicate keys can be used in certain cases (e.g. two `email` fields, each hidden by conditions). Always snake_case.
80
+ - `key: string` - Unique identifier, used in React as key prop - unique in general but duplicate keys can be used in certain cases (e.g. two `email` fields, each hidden by conditions). Always snake_case. **Keys must not start with a digit** (invalid in JS/JSON). When deriving a key from text that would start with a number (e.g. "1 to 2 weeks" → "1_to_2_weeks"), use a semantic prefix instead: e.g. `range_1_to_2_weeks`, `option_1_to_2_weeks`, or the component key like `delivery_time_1_to_2_weeks`.
81
81
  - `type: ComponentType` - Component type
82
82
  - `tags?: string[]` - Used for CSS styling
83
83
  - `parent_id?: string` - For nested components
@@ -98,7 +98,7 @@ All component types are derived from `src/types-builder.ts`. The relevant typing
98
98
  - `outputs_onchange`
99
99
  - etc.
100
100
  - `OptionSelector`
101
- - `buttons`
101
+ - `buttons` - each button has a `key` (and optional `text`, `description`, etc.). **Button keys must not start with a digit.** If the option label would slug to a key starting with a number (e.g. "1 to 2 weeks" → "1_to_2_weeks"), use a prefix such as the component key (e.g. `delivery_range_1_to_2_weeks`) or a short semantic prefix (e.g. `range_1_to_2_weeks`, `option_1_to_2_weeks`).
102
102
  - `multiple` - whether to allow multi-select
103
103
  - `dropdown`
104
104
  - `checkbox` - whether to add a (square/round) visual checkbox in each button (the checkbox will be added for you, no need to add it manually)
package/dist/cli.js CHANGED
@@ -30,9 +30,10 @@ program
30
30
  });
31
31
  program
32
32
  .command('build')
33
- .requiredOption('-i, --id <id>', 'Embeddable ID')
33
+ .option('-i, --id <id>', 'Embeddable ID (will prompt if not provided)')
34
34
  .option('-p, --pages <glob>', 'Pages glob')
35
35
  .option('-o, --out <path>', 'Output json path')
36
+ .option('--fix', 'Apply lint fixes (duplicate IDs, keys/IDs starting with a number)')
36
37
  .option('--pageKeyFrom <mode>', 'filename|export', 'filename')
37
38
  .action(async (opts) => {
38
39
  await runBuild(opts);
@@ -42,6 +43,7 @@ program
42
43
  .option('-i, --id <id>', 'Embeddable ID (will prompt if not provided)')
43
44
  .option('-p, --pages <glob>', 'Pages glob')
44
45
  .option('-o, --out <path>', 'Output json path')
46
+ .option('--fix', 'Apply lint fixes (duplicate IDs, keys/IDs starting with a number)')
45
47
  .option('-L, --local', 'Use local engine (http://localhost:8787)')
46
48
  .option('-e, --engine <url>', 'Engine origin', 'https://engine.embeddables.com')
47
49
  .option('--port <n>', 'Dev proxy port', '3000')
@@ -1 +1 @@
1
- {"version":3,"file":"branch.d.ts","sourceRoot":"","sources":["../../src/commands/branch.ts"],"names":[],"mappings":"AAKA,wBAAsB,SAAS,CAAC,IAAI,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,iBAgDpD"}
1
+ {"version":3,"file":"branch.d.ts","sourceRoot":"","sources":["../../src/commands/branch.ts"],"names":[],"mappings":"AAKA,wBAAsB,SAAS,CAAC,IAAI,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,iBAoDpD"}
@@ -41,6 +41,10 @@ export async function runBranch(opts) {
41
41
  else {
42
42
  console.log(pc.cyan(`Switching to branch: ${selectedBranch.name}...`));
43
43
  console.log('');
44
- await runPull({ id: embeddableId, branch: selectedBranch.id });
44
+ await runPull({
45
+ id: embeddableId,
46
+ branch: selectedBranch.id,
47
+ branchName: selectedBranch.name,
48
+ });
45
49
  }
46
50
  }
@@ -1,7 +1,8 @@
1
1
  export declare function runBuild(opts: {
2
- id: string;
2
+ id?: string;
3
3
  pages?: string;
4
4
  out?: string;
5
+ fix?: boolean;
5
6
  pageKeyFrom: 'filename' | 'export';
6
7
  }): Promise<void>;
7
8
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAIA,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,iBAmBA"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAKA,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,iBA6BA"}
@@ -1,8 +1,18 @@
1
1
  import path from 'node:path';
2
2
  import { compileAllPages } from '../compiler/index.js';
3
3
  import { formatError } from '../compiler/errors.js';
4
+ import { promptForLocalEmbeddable } from '../prompts/index.js';
4
5
  export async function runBuild(opts) {
5
- const embeddableId = opts.id;
6
+ let embeddableId = opts.id;
7
+ if (!embeddableId) {
8
+ const selected = await promptForLocalEmbeddable({
9
+ message: 'Select an embeddable to build:',
10
+ });
11
+ if (!selected) {
12
+ process.exit(1);
13
+ }
14
+ embeddableId = selected;
15
+ }
6
16
  const pagesGlob = opts.pages || `embeddables/${embeddableId}/pages/**/*.page.tsx`;
7
17
  const outPath = opts.out || path.join('embeddables', embeddableId, '.generated', 'embeddable.json');
8
18
  const stylesDir = path.join('embeddables', embeddableId, 'styles');
@@ -13,6 +23,7 @@ export async function runBuild(opts) {
13
23
  pageKeyFrom: opts.pageKeyFrom,
14
24
  stylesDir,
15
25
  embeddableId,
26
+ fixLint: opts.fix ? true : 'prompt',
16
27
  });
17
28
  }
18
29
  catch (e) {
@@ -2,6 +2,7 @@ export declare function runDev(opts: {
2
2
  id?: string;
3
3
  pages?: string;
4
4
  out?: string;
5
+ fix?: boolean;
5
6
  local?: boolean;
6
7
  engine: string;
7
8
  port: string;
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAmHA,wBAAsB,MAAM,CAAC,IAAI,EAAE;IACjC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,iBA4GA"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAmHA,wBAAsB,MAAM,CAAC,IAAI,EAAE;IACjC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,iBA+GA"}
@@ -108,6 +108,7 @@ export async function runDev(opts) {
108
108
  const globalComponentsGlob = `embeddables/${embeddableId}/global-components/**/*.location.tsx`;
109
109
  const computedFieldsGlob = `embeddables/${embeddableId}/computed-fields/**/*.js`;
110
110
  const actionsGlob = `embeddables/${embeddableId}/actions/**/*.js`;
111
+ const fixLint = opts.fix ? true : 'prompt';
111
112
  // Initial build
112
113
  try {
113
114
  await compileAllPages({
@@ -117,6 +118,7 @@ export async function runDev(opts) {
117
118
  stylesDir,
118
119
  embeddableId,
119
120
  configPath,
121
+ fixLint,
120
122
  });
121
123
  }
122
124
  catch (e) {
@@ -158,6 +160,7 @@ export async function runDev(opts) {
158
160
  stylesDir,
159
161
  embeddableId,
160
162
  configPath,
163
+ fixLint,
161
164
  });
162
165
  proxy.broadcastReload();
163
166
  }
@@ -1,7 +1,9 @@
1
- export declare function runPull(opts: {
1
+ export type RunPullOptions = {
2
2
  id?: string;
3
3
  out?: string;
4
4
  branch?: string;
5
+ branchName?: string;
5
6
  fix?: boolean;
6
- }): Promise<void>;
7
+ };
8
+ export declare function runPull(opts: RunPullOptions): Promise<void>;
7
9
  //# sourceMappingURL=pull.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AAUA,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,iBAwNhG"}
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AAyEA,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,CAAC,EAAE,OAAO,CAAA;CACd,CAAA;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,cAAc,iBAkPjD"}
@@ -6,6 +6,69 @@ import { reverseCompile } from '../compiler/reverse.js';
6
6
  import { getAccessToken, isLoggedIn } from '../auth/index.js';
7
7
  import { getProjectId, writeProjectConfig } from '../config/index.js';
8
8
  import { promptForProject, promptForEmbeddable, fetchEmbeddableMetadata } from '../prompts/index.js';
9
+ /** Slug for branch name/id for use in filenames (e.g. "my branch" -> "my_branch"). */
10
+ function slugForBranch(nameOrId) {
11
+ return String(nameOrId).replace(/[^a-zA-Z0-9_.-]/g, '_').replace(/_+/g, '_') || 'main';
12
+ }
13
+ /** Versioned embeddable filename: embeddable-{branchSlug}@{version}.json */
14
+ function getVersionedBasename(version, branchSlug) {
15
+ const v = typeof version === 'number' ? version : String(version);
16
+ return `embeddable-${branchSlug}@${v}.json`;
17
+ }
18
+ /** Read current _branch_id and _branch_name from config (set when on a branch via `embeddables branch`). */
19
+ function getCurrentBranchFromConfig(embeddableId) {
20
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
21
+ if (!fs.existsSync(configPath))
22
+ return null;
23
+ try {
24
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
25
+ const branchId = config._branch_id;
26
+ if (typeof branchId !== 'string' || !branchId)
27
+ return null;
28
+ const branchName = config._branch_name;
29
+ return { branchId, branchName: typeof branchName === 'string' ? branchName : undefined };
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ /** Write _version, _branch_id, _branch_name to config.json. Called early and in finally so it always runs. */
36
+ function writePullMetadataToConfig(embeddableId, version, branch, branchName) {
37
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
38
+ let config = {};
39
+ try {
40
+ if (fs.existsSync(configPath)) {
41
+ try {
42
+ const content = fs.readFileSync(configPath, 'utf8');
43
+ config = JSON.parse(content);
44
+ }
45
+ catch {
46
+ /* use empty config */
47
+ }
48
+ }
49
+ if (version != null) {
50
+ const n = typeof version === 'number' ? version : parseInt(String(version), 10);
51
+ if (!isNaN(n))
52
+ config._version = n;
53
+ }
54
+ if (branch) {
55
+ config._branch_id = branch;
56
+ if (branchName)
57
+ config._branch_name = branchName;
58
+ else
59
+ delete config._branch_name;
60
+ }
61
+ else {
62
+ delete config._branch_id;
63
+ delete config._branch_name;
64
+ }
65
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
66
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
67
+ }
68
+ catch (err) {
69
+ console.warn(pc.yellow(`Could not write config.json: ${err instanceof Error ? err.message : err}`));
70
+ }
71
+ }
9
72
  export async function runPull(opts) {
10
73
  let embeddableId = opts.id;
11
74
  // If no ID provided, try to get it interactively
@@ -34,7 +97,7 @@ export async function runPull(opts) {
34
97
  project_id: projectId,
35
98
  project_name: selectedProject.title || undefined,
36
99
  });
37
- console.log(pc.green(`✓ Saved project to embeddables.json`));
100
+ console.log(pc.green(`✓ Wrote project config to embeddables.json`));
38
101
  console.log('');
39
102
  }
40
103
  console.log(pc.cyan('Fetching embeddables from project...'));
@@ -47,12 +110,23 @@ export async function runPull(opts) {
47
110
  embeddableId = selected;
48
111
  console.log('');
49
112
  }
113
+ // Stay on current branch when no --branch: use config's _branch_id if set
114
+ const currentFromConfig = opts.branch == null ? getCurrentBranchFromConfig(embeddableId) : null;
115
+ const effectiveBranch = opts.branch ?? currentFromConfig?.branchId;
116
+ const effectiveBranchName = opts.branchName ?? currentFromConfig?.branchName;
50
117
  let url = `https://engine.embeddables.com/${embeddableId}?version=latest`;
51
- if (opts.branch) {
52
- url += `&embeddable_branch=${opts.branch}`;
118
+ if (effectiveBranch) {
119
+ url += `&embeddable_branch=${effectiveBranch}`;
53
120
  }
54
121
  const outPath = opts.out || path.join('embeddables', embeddableId, '.generated', 'embeddable.json');
122
+ const branchLabel = effectiveBranch
123
+ ? effectiveBranchName
124
+ ? `${effectiveBranchName} (${effectiveBranch})`
125
+ : effectiveBranch
126
+ : 'main';
127
+ console.log(pc.cyan(`Pulling branch: ${pc.bold(branchLabel)}`));
55
128
  console.log(`Fetching embeddable from ${url}...`);
129
+ let pullVersion;
56
130
  try {
57
131
  // Add authentication header if available
58
132
  const accessToken = getAccessToken();
@@ -75,24 +149,28 @@ export async function runPull(opts) {
75
149
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
76
150
  const flowJson = JSON.stringify(flow, null, 2);
77
151
  fs.writeFileSync(outPath, flowJson, 'utf8');
78
- console.log(pc.cyan(`✓ Saved embeddable JSON to ${outPath}`));
79
- // Also save version-specific file (e.g. embeddable-v135.json) when version is present in response
80
- const version = data.version ?? data.embeddable_version ?? flow.version;
152
+ console.log(pc.cyan(`✓ Wrote embeddable JSON to ${outPath}`));
153
+ // Also save version-specific file (e.g. embeddable-main@135.json or embeddable-my_branch@135.json)
154
+ pullVersion =
155
+ data.version ?? data.embeddable_version ?? flow.version;
156
+ const version = pullVersion;
157
+ const branchSlug = slugForBranch(effectiveBranchName ?? effectiveBranch ?? 'main');
81
158
  if (version != null) {
82
159
  const versionStr = typeof version === 'string' ? version : String(version);
83
- const versionLabel = versionStr.startsWith('v') ? versionStr : `v${versionStr}`;
84
- const versionedBasename = `embeddable-${versionLabel}.json`;
160
+ const versionedBasename = getVersionedBasename(versionStr, branchSlug);
85
161
  const versionedPath = path.join(path.dirname(outPath), versionedBasename);
86
162
  fs.writeFileSync(versionedPath, flowJson, 'utf8');
87
- console.log(pc.cyan(`✓ Saved embeddable JSON to ${versionedPath}`));
163
+ console.log(pc.cyan(`✓ Wrote versioned embeddable JSON to ${versionedPath}`));
88
164
  }
165
+ // Persist _version and _branch_id in config.json immediately so they survive reverseCompile
166
+ writePullMetadataToConfig(embeddableId, version, effectiveBranch, effectiveBranchName);
89
167
  // Fetch and save flow metadata from DB (title, archived, created_at)
90
168
  const metadata = await fetchEmbeddableMetadata(embeddableId);
91
169
  if (metadata) {
92
170
  const metadataPath = path.join('embeddables', embeddableId, 'metadata.json');
93
171
  fs.mkdirSync(path.dirname(metadataPath), { recursive: true });
94
172
  fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + '\n', 'utf8');
95
- console.log(pc.cyan(`✓ Saved flow metadata to ${metadataPath}`));
173
+ console.log(pc.cyan(`✓ Wrote flow metadata to ${metadataPath}`));
96
174
  }
97
175
  // Clear existing pages, styles, computed-fields, actions, and global-components before generating new ones
98
176
  const pagesDir = path.join('embeddables', embeddableId, 'pages');
@@ -137,9 +215,16 @@ export async function runPull(opts) {
137
215
  }
138
216
  console.log(`${pc.gray(`Cleared ${existingGlobalComponents.length} existing global component(s)`)}`);
139
217
  }
140
- // Run reverse compiler
218
+ // Run reverse compiler (pass pullMetadata so config.json gets _version and _branch_id even on fix retry)
219
+ const versionNum = version != null && !isNaN(Number(version)) ? Number(version) : undefined;
220
+ const pullMetadata = {
221
+ version: versionNum,
222
+ branchId: effectiveBranch,
223
+ branchName: effectiveBranchName,
224
+ };
225
+ let usedFix = opts.fix;
141
226
  try {
142
- await reverseCompile(flow, embeddableId, { fix: opts.fix });
227
+ await reverseCompile(flow, embeddableId, { fix: opts.fix, pullMetadata });
143
228
  }
144
229
  catch (compileError) {
145
230
  // If fix mode wasn't already enabled, offer to retry with fix mode
@@ -159,10 +244,11 @@ export async function runPull(opts) {
159
244
  },
160
245
  });
161
246
  if (response.fix) {
247
+ usedFix = true;
162
248
  console.log('');
163
249
  console.log(pc.cyan('Retrying with auto-fix enabled...'));
164
250
  console.log('');
165
- await reverseCompile(flow, embeddableId, { fix: true });
251
+ await reverseCompile(flow, embeddableId, { fix: true, pullMetadata });
166
252
  }
167
253
  else {
168
254
  process.exit(1);
@@ -172,22 +258,17 @@ export async function runPull(opts) {
172
258
  throw compileError;
173
259
  }
174
260
  }
175
- // Store version number in config.json so save knows the base version
176
- if (version != null) {
177
- const versionNumber = typeof version === 'number' ? version : parseInt(String(version), 10);
178
- if (!isNaN(versionNumber)) {
179
- const configPath = path.join('embeddables', embeddableId, 'config.json');
180
- if (fs.existsSync(configPath)) {
181
- try {
182
- const configContent = fs.readFileSync(configPath, 'utf8');
183
- const config = JSON.parse(configContent);
184
- config._version = versionNumber;
185
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
186
- }
187
- catch {
188
- // Ignore errors updating config.json - versioned files are a fallback
189
- }
190
- }
261
+ // Persist mutated flow when fixes were applied (e.g. parent_key -> parent_id)
262
+ if (usedFix) {
263
+ const flowJson = JSON.stringify(flow, null, 2);
264
+ fs.writeFileSync(outPath, flowJson, 'utf8');
265
+ console.log(pc.cyan(`✓ Wrote embeddable JSON to ${outPath} (with fixes applied)`));
266
+ if (version != null) {
267
+ const versionStr = typeof version === 'string' ? version : String(version);
268
+ const versionedBasename = getVersionedBasename(versionStr, branchSlug);
269
+ const versionedPath = path.join(path.dirname(outPath), versionedBasename);
270
+ fs.writeFileSync(versionedPath, flowJson, 'utf8');
271
+ console.log(pc.cyan(`✓ Wrote versioned embeddable JSON to ${versionedPath} (with fixes applied)`));
191
272
  }
192
273
  }
193
274
  }
@@ -195,4 +276,10 @@ export async function runPull(opts) {
195
276
  console.error('Error pulling embeddable:', error);
196
277
  process.exit(1);
197
278
  }
279
+ finally {
280
+ // Always persist _version and _branch_id once we've fetched (so config is correct even if reverseCompile failed or was retried with fix)
281
+ if (embeddableId != null && pullVersion !== undefined) {
282
+ writePullMetadataToConfig(embeddableId, pullVersion, effectiveBranch, effectiveBranchName);
283
+ }
284
+ }
198
285
  }
@@ -1 +1 @@
1
- {"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"AAoJA,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAClC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,iBAoBA"}
1
+ {"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"AA+OA,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAClC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,iBAoBA"}
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import pc from 'picocolors';
4
4
  import prompts from 'prompts';
5
- import { getAccessToken, isLoggedIn } from '../auth/index.js';
5
+ import { getAccessToken, getAuthenticatedSupabaseClient, isLoggedIn } from '../auth/index.js';
6
6
  import { getProjectId, writeProjectConfig } from '../config/index.js';
7
7
  import { compileAllPages } from '../compiler/index.js';
8
8
  import { formatError } from '../compiler/errors.js';
@@ -69,6 +69,45 @@ function getVersionFromConfig(embeddableId) {
69
69
  }
70
70
  return null;
71
71
  }
72
+ /**
73
+ * Read `_branch_id` from config.json for the given embeddable (set when on a branch via `embeddables branch`).
74
+ */
75
+ function getBranchFromConfig(embeddableId) {
76
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
77
+ if (!fs.existsSync(configPath)) {
78
+ return null;
79
+ }
80
+ try {
81
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
82
+ if (typeof config._branch_id === 'string' && config._branch_id) {
83
+ return config._branch_id;
84
+ }
85
+ }
86
+ catch {
87
+ // Ignore parse errors
88
+ }
89
+ return null;
90
+ }
91
+ /** Slug for branch name/id for versioned filenames (e.g. "my branch" -> "my_branch"). */
92
+ function slugForBranch(nameOrId) {
93
+ return String(nameOrId).replace(/[^a-zA-Z0-9_.-]/g, '_').replace(/_+/g, '_') || 'main';
94
+ }
95
+ /** Get branch slug from config (_branch_name preferred, else _branch_id, else main). */
96
+ function getBranchSlugFromConfig(embeddableId) {
97
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
98
+ if (!fs.existsSync(configPath))
99
+ return 'main';
100
+ try {
101
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
102
+ const name = config._branch_name ?? config._branch_id;
103
+ if (typeof name === 'string' && name)
104
+ return slugForBranch(name);
105
+ }
106
+ catch {
107
+ /* ignore */
108
+ }
109
+ return 'main';
110
+ }
72
111
  /**
73
112
  * Update `_version` in config.json for the given embeddable.
74
113
  */
@@ -87,7 +126,7 @@ function setVersionInConfig(embeddableId, version) {
87
126
  }
88
127
  }
89
128
  /**
90
- * Scan the .generated/ directory for versioned files (embeddable-v*.json)
129
+ * Scan the .generated/ directory for versioned files (embeddable-v*.json or embeddable-*@*.json)
91
130
  * and return the highest version number found.
92
131
  */
93
132
  function getLatestVersionFromFiles(generatedDir) {
@@ -95,10 +134,11 @@ function getLatestVersionFromFiles(generatedDir) {
95
134
  return null;
96
135
  }
97
136
  const files = fs.readdirSync(generatedDir);
98
- const versionPattern = /^embeddable-v(\d+)\.json$/;
137
+ const legacyPattern = /^embeddable-v(\d+)\.json$/;
138
+ const branchPattern = /^embeddable-[^@]+@(\d+)\.json$/;
99
139
  let maxVersion = null;
100
140
  for (const file of files) {
101
- const match = file.match(versionPattern);
141
+ const match = file.match(legacyPattern) ?? file.match(branchPattern);
102
142
  if (match) {
103
143
  const version = parseInt(match[1], 10);
104
144
  if (maxVersion === null || version > maxVersion) {
@@ -108,6 +148,41 @@ function getLatestVersionFromFiles(generatedDir) {
108
148
  }
109
149
  return maxVersion;
110
150
  }
151
+ /**
152
+ * Fetch active drafts from other users on the same version (status=DRAFT, not saved/discarded).
153
+ * Used to warn before saving that others may have unsaved edits.
154
+ */
155
+ async function fetchOtherUsersDrafts(supabase, flowId, versionNumber, branchId, currentUserId) {
156
+ try {
157
+ let query = supabase
158
+ .from('flow_versions')
159
+ .select('id, author_id, author_name')
160
+ .eq('flow_id', flowId)
161
+ .eq('status', 'DRAFT')
162
+ .eq('version_number', versionNumber)
163
+ .not('draft_saved', 'is', true)
164
+ .not('draft_discarded', 'is', true)
165
+ .neq('author_id', currentUserId);
166
+ if (branchId === null) {
167
+ query = query.is('branch_id', null);
168
+ }
169
+ else {
170
+ query = query.eq('branch_id', branchId);
171
+ }
172
+ const { data, error } = await query;
173
+ if (error) {
174
+ return [];
175
+ }
176
+ return (data || []).map((row) => ({
177
+ id: row.id,
178
+ author_id: row.author_id,
179
+ author_name: row.author_name ?? null,
180
+ }));
181
+ }
182
+ catch {
183
+ return [];
184
+ }
185
+ }
111
186
  export async function runSave(opts) {
112
187
  try {
113
188
  await runSaveInner(opts);
@@ -158,6 +233,8 @@ async function runSaveInner(opts) {
158
233
  embeddableId = selected;
159
234
  console.log('');
160
235
  }
236
+ // Resolve branch: explicit -b flag wins, otherwise use current branch from config (set by `embeddables branch`)
237
+ const effectiveBranch = opts.branch ?? getBranchFromConfig(embeddableId) ?? undefined;
161
238
  // 4. Get project ID (from config or interactive prompt)
162
239
  let projectId = getProjectId();
163
240
  if (!projectId) {
@@ -242,6 +319,39 @@ async function runSaveInner(opts) {
242
319
  }
243
320
  fromVersionNumber = detectedVersion;
244
321
  }
322
+ // 7b. Check for other users' drafts on this version; warn and optionally abort
323
+ const supabase = await getAuthenticatedSupabaseClient();
324
+ if (supabase) {
325
+ const { data: { user }, } = await supabase.auth.getUser();
326
+ const currentUserId = user?.id;
327
+ if (currentUserId) {
328
+ const branchIdForDrafts = effectiveBranch ?? null;
329
+ const otherDrafts = await fetchOtherUsersDrafts(supabase, embeddableId, fromVersionNumber, branchIdForDrafts, currentUserId);
330
+ if (otherDrafts.length > 0) {
331
+ const names = otherDrafts
332
+ .map((d) => d.author_name?.trim() || d.author_id || 'Someone')
333
+ .filter((n, i, a) => a.indexOf(n) === i);
334
+ const namesText = names.length === 1 ? names[0] : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
335
+ console.log('');
336
+ console.warn(pc.yellow(`⚠ ${namesText} ${names.length === 1 ? 'has' : 'have'} unsaved edits on version ${fromVersionNumber}. Saving may cause conflicts.`));
337
+ const { proceed } = await prompts({
338
+ type: 'confirm',
339
+ name: 'proceed',
340
+ message: 'Save anyway?',
341
+ initial: false,
342
+ }, {
343
+ onCancel: () => {
344
+ process.exit(1);
345
+ },
346
+ });
347
+ if (!proceed) {
348
+ console.log(pc.gray('Save cancelled.'));
349
+ process.exit(0);
350
+ }
351
+ console.log('');
352
+ }
353
+ }
354
+ }
245
355
  // 8. POST to save-version API
246
356
  console.log(pc.cyan(`Saving embeddable (based on v${fromVersionNumber})...`));
247
357
  const body = {
@@ -253,8 +363,8 @@ async function runSaveInner(opts) {
253
363
  if (opts.label) {
254
364
  body.label = opts.label;
255
365
  }
256
- if (opts.branch) {
257
- body.branchId = opts.branch;
366
+ if (effectiveBranch) {
367
+ body.branchId = effectiveBranch;
258
368
  }
259
369
  const url = `${WEB_APP_BASE_URL}/api/embeddables/save-version`;
260
370
  const headers = {
@@ -340,7 +450,8 @@ async function runSaveInner(opts) {
340
450
  const { newVersionNumber } = forceResult.data;
341
451
  console.log(pc.green(`✓ Saved as version ${newVersionNumber}`));
342
452
  setVersionInConfig(embeddableId, newVersionNumber);
343
- const versionedPath = path.join(generatedDir, `embeddable-v${newVersionNumber}.json`);
453
+ const branchSlug = getBranchSlugFromConfig(embeddableId);
454
+ const versionedPath = path.join(generatedDir, `embeddable-${branchSlug}@${newVersionNumber}.json`);
344
455
  fs.mkdirSync(generatedDir, { recursive: true });
345
456
  fs.writeFileSync(versionedPath, jsonContent, 'utf8');
346
457
  console.log(pc.cyan(`✓ Saved version file to ${versionedPath}`));
@@ -358,8 +469,9 @@ async function runSaveInner(opts) {
358
469
  console.log(pc.green(`✓ Saved as version ${newVersionNumber}`));
359
470
  // Update _version in config.json so future saves know the base version
360
471
  setVersionInConfig(embeddableId, newVersionNumber);
361
- // Also save the versioned file to .generated/ as a snapshot
362
- const versionedPath = path.join(generatedDir, `embeddable-v${newVersionNumber}.json`);
472
+ // Also save the versioned file to .generated/ as a snapshot (embeddable-{branch}@{version}.json)
473
+ const branchSlug = getBranchSlugFromConfig(embeddableId);
474
+ const versionedPath = path.join(generatedDir, `embeddable-${branchSlug}@${newVersionNumber}.json`);
363
475
  fs.mkdirSync(generatedDir, { recursive: true });
364
476
  fs.writeFileSync(versionedPath, jsonContent, 'utf8');
365
477
  console.log(pc.cyan(`✓ Saved version file to ${versionedPath}`));
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Keys and IDs must not start with a digit (invalid in JS/JSON).
3
+ * This helper produces a safe key by prefixing when needed.
4
+ */
5
+ /** Returns a key safe for use (prefix applied if key starts with a digit). */
6
+ export declare function normalizeKeyIfStartsWithDigit(key: string | null, prefix: string): string | null;
7
+ export declare function keyOrIdStartsWithDigit(value: string | null | undefined): boolean;
8
+ //# sourceMappingURL=numericLeadingKeys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numericLeadingKeys.d.ts","sourceRoot":"","sources":["../../../src/compiler/helpers/numericLeadingKeys.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,8EAA8E;AAC9E,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI/F;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAIhF"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Keys and IDs must not start with a digit (invalid in JS/JSON).
3
+ * This helper produces a safe key by prefixing when needed.
4
+ */
5
+ /** Returns a key safe for use (prefix applied if key starts with a digit). */
6
+ export function normalizeKeyIfStartsWithDigit(key, prefix) {
7
+ if (key === null || typeof key !== 'string' || key === '')
8
+ return key;
9
+ if (!/^\d/.test(key))
10
+ return key;
11
+ return `${prefix}_${key}`;
12
+ }
13
+ export function keyOrIdStartsWithDigit(value) {
14
+ if (value === null || value === undefined || typeof value !== 'string' || value === '')
15
+ return false;
16
+ return /^\d/.test(value);
17
+ }
@@ -12,5 +12,7 @@ export declare function compileAllPages(opts: {
12
12
  stylesDir?: string;
13
13
  embeddableId?: string;
14
14
  configPath?: string;
15
+ /** When true, apply lint fixes (duplicate IDs, keys/IDs starting with a digit). When 'prompt', ask user to fix. */
16
+ fixLint?: true | 'prompt';
15
17
  }): Promise<void>;
16
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/compiler/index.ts"],"names":[],"mappings":"AA+CA;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CA2DlF;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,iBAiXA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/compiler/index.ts"],"names":[],"mappings":"AAmDA;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CA2DlF;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mHAAmH;IACnH,OAAO,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAA;CAC1B,iBA4cA"}