@embeddables/cli 0.4.4 → 0.5.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.
@@ -0,0 +1,26 @@
1
+ We need to build the pages from the attached images. Please create a plan and highlight any decisions that need to be made on implementation / things you're unsure about, especially when it comes to deciding how to build something out of the available components and settings.
2
+
3
+ Embeddable ID: <EMBEDDABLE_ID>
4
+
5
+ Important: Always read the Embeddables CLI context before starting (e.g. @.cursor/rules/embeddables-cli.md or @.claudefiles/embeddables-cli.md depending on your editor).
6
+
7
+ [KEEP / REMOVE]
8
+ Use the designs from the images but use the content from the attached document instead, and build out the pages based on that content. Therefore, when using the designs from the images, for each page just find the most relevant design for the page's content and design it based on that. However, don't be confined to the provided designs - just find the closest design and modify it to fit the content required.
9
+
10
+ Notes:
11
+
12
+ 1. The page is screenshotted on a standard MacBook.
13
+ 2. Ignore any floating widgets in the corners that would be installed separately (e.g. Intercom).
14
+ 3. Add a max width, with the content centered so that on desktop it's in the middle and mobile it's full width.
15
+ 4. Make sure the header and footer go in global components, including any brand logo, progress bar, back button, global links etc.
16
+ 5. Whenever you need to insert an image (e.g. a logo, an icon, a larger image) use one of the following (and please be consistent with your choice out of those options within a particular component or section):
17
+ a. `emojiIcon` with an emoji when there is an appropriate one (can be used in `imageUrl` in CustomButton, or `imageUrl` inside OptionSelector buttons)
18
+ b. placeholder image: https://placehold.co/600x400?text=My+Text+To+Display if there is text in the image (can't be emojis), otherwise https://placehold.co/600x400 (but with the correct size specified in the URL in either case; can be used in `src` in MediaImage, `imageUrl` in CustomButton, or `imageUrl` inside OptionSelector buttons)
19
+
20
+ At the end of your work, include a short summary covering:
21
+
22
+ - Decisions made – Any implementation choices where requirements, design, or instructions were not fully explicit.
23
+ - Assumptions – Anything you assumed about behavior, structure, content, or flow.
24
+ - Uncertainties – Anything that may need clarification or confirmation instead of being silently decided.
25
+
26
+ Keep it concise and focused on reasoning, not a restatement of the instructions.
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import pc from 'picocolors';
3
3
  import { runBranch } from './commands/branch.js';
4
4
  import { runBuild } from './commands/build.js';
5
5
  import { runDev } from './commands/dev.js';
6
+ import { runExperimentsConnect } from './commands/experiments-connect.js';
6
7
  import { runInit } from './commands/init.js';
7
8
  import { runLogin } from './commands/login.js';
8
9
  import { runLogout } from './commands/logout.js';
@@ -99,4 +100,18 @@ program
99
100
  .action(async (opts) => {
100
101
  await runBranch(opts);
101
102
  });
103
+ const experiments = program.command('experiments').description('Manage embeddable experiments');
104
+ experiments
105
+ .command('connect')
106
+ .description('Connect an experiment to an embeddable')
107
+ .option('-i, --id <id>', 'Embeddable ID (will prompt if not provided)')
108
+ .option('--experiment-id <id>', 'Experiment ID (will prompt to choose from project if not provided)')
109
+ .option('--experiment-key <key>', 'Experiment key (required if --experiment-id is set)')
110
+ .action(async (opts) => {
111
+ await runExperimentsConnect({
112
+ id: opts.id,
113
+ experimentId: opts.experimentId,
114
+ experimentKey: opts.experimentKey,
115
+ });
116
+ });
102
117
  await program.parseAsync(process.argv);
@@ -0,0 +1,6 @@
1
+ export declare function runExperimentsConnect(opts: {
2
+ id?: string;
3
+ experimentId?: string;
4
+ experimentKey?: string;
5
+ }): Promise<void>;
6
+ //# sourceMappingURL=experiments-connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiments-connect.d.ts","sourceRoot":"","sources":["../../src/commands/experiments-connect.ts"],"names":[],"mappings":"AAYA,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,iBAcA"}
@@ -0,0 +1,114 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { isLoggedIn } from '../auth/index.js';
5
+ import { getProjectId, writeProjectConfig } from '../config/index.js';
6
+ import { promptForProject, promptForLocalEmbeddable, promptForExperiment, } from '../prompts/index.js';
7
+ export async function runExperimentsConnect(opts) {
8
+ try {
9
+ await runExperimentsConnectInner(opts);
10
+ }
11
+ catch (error) {
12
+ if (error instanceof Error) {
13
+ console.error(pc.red(`Connect failed: ${error.message}`));
14
+ }
15
+ else {
16
+ console.error(pc.red('Connect failed with an unexpected error.'));
17
+ }
18
+ process.exit(1);
19
+ if (error instanceof Error && error.message === 'exit') {
20
+ throw error;
21
+ }
22
+ }
23
+ }
24
+ async function runExperimentsConnectInner(opts) {
25
+ // 1. Check login (needed for fetching experiments from Supabase)
26
+ if (!isLoggedIn()) {
27
+ console.error(pc.red('Not logged in.'));
28
+ console.log(pc.gray('Run "embeddables login" first.'));
29
+ process.exit(1);
30
+ }
31
+ // 2. Get project ID (needed for experiment list)
32
+ let projectId = getProjectId();
33
+ if (!projectId) {
34
+ console.log(pc.cyan('No project configured. Fetching projects...'));
35
+ const selectedProject = await promptForProject();
36
+ if (!selectedProject) {
37
+ process.exit(1);
38
+ }
39
+ projectId = selectedProject.id;
40
+ writeProjectConfig({
41
+ org_id: selectedProject.org_id || undefined,
42
+ org_title: selectedProject.org_title || undefined,
43
+ project_id: projectId,
44
+ project_name: selectedProject.title || undefined,
45
+ });
46
+ console.log(pc.green('✓ Saved project to embeddables.json'));
47
+ console.log('');
48
+ }
49
+ // 4. Get embeddable ID
50
+ let embeddableId = opts.id;
51
+ if (!embeddableId) {
52
+ const selected = await promptForLocalEmbeddable({
53
+ message: 'Select an embeddable to connect the experiment to:',
54
+ });
55
+ if (!selected) {
56
+ process.exit(1);
57
+ }
58
+ embeddableId = selected;
59
+ console.log('');
60
+ }
61
+ // 5. Get experiment_id and experiment_key (from opts or interactive prompt)
62
+ let experimentId = opts.experimentId;
63
+ let experimentKey = opts.experimentKey;
64
+ if (experimentId && !experimentKey) {
65
+ console.error(pc.red('When using --experiment-id, --experiment-key is also required.'));
66
+ process.exit(1);
67
+ }
68
+ if (experimentKey && !experimentId) {
69
+ console.error(pc.red('When using --experiment-key, --experiment-id is also required.'));
70
+ process.exit(1);
71
+ }
72
+ if (!experimentId || !experimentKey) {
73
+ console.log(pc.cyan('Fetching experiments from project...'));
74
+ const selected = await promptForExperiment(projectId, {
75
+ message: 'Select an experiment to connect:',
76
+ excludeConnectedTo: embeddableId,
77
+ });
78
+ if (!selected) {
79
+ process.exit(1);
80
+ }
81
+ experimentId = selected.experiment_id;
82
+ experimentKey = selected.experiment_key;
83
+ console.log('');
84
+ }
85
+ // 6. Read config.json
86
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
87
+ if (!fs.existsSync(configPath)) {
88
+ console.error(pc.red(`No config.json found at ${configPath}`));
89
+ console.log(pc.gray('Run "embeddables pull" or "embeddables init" first.'));
90
+ process.exit(1);
91
+ }
92
+ let config;
93
+ try {
94
+ const content = fs.readFileSync(configPath, 'utf8');
95
+ config = JSON.parse(content);
96
+ }
97
+ catch {
98
+ console.error(pc.red('Failed to parse config.json.'));
99
+ process.exit(1);
100
+ }
101
+ // 7. Append to connected_experiments
102
+ const connectedExperiments = Array.isArray(config.connected_experiments) ? [...config.connected_experiments] : [];
103
+ const alreadyConnected = connectedExperiments.some((e) => e.experiment_id === experimentId && e.experiment_key === experimentKey);
104
+ if (alreadyConnected) {
105
+ console.log(pc.yellow(`Experiment "${experimentKey}" is already connected to this embeddable.`));
106
+ return;
107
+ }
108
+ connectedExperiments.push({ experiment_id: experimentId, experiment_key: experimentKey });
109
+ config.connected_experiments = connectedExperiments;
110
+ // 8. Write the modified config back
111
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
112
+ console.log(pc.green(`✓ Connected experiment "${experimentKey}" to embeddable (updated config.json)`));
113
+ console.log(pc.gray('Run "embeddables save" to persist to the cloud.'));
114
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAuHA,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,iBAuMxE"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAuHA,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,iBAyOxE"}
@@ -193,13 +193,48 @@ export async function runInit(opts) {
193
193
  else {
194
194
  console.log(pc.gray(' ✓ embeddables/ directory exists'));
195
195
  }
196
- // Copy .cursor/ folder with Cursor rules
196
+ // Inject .cursor/ and .claudefiles/ from source .prompts/embeddables-cli.md (or copy if pre-built dirs exist)
197
197
  const packageRoot = path.resolve(__dirname, '..', '..');
198
+ const promptsSource = path.join(packageRoot, '.prompts', 'embeddables-cli.md');
198
199
  const sourceCursorDir = path.join(packageRoot, '.cursor');
199
- const targetCursorDir = path.join(cwd, '.cursor');
200
- if (fs.existsSync(sourceCursorDir)) {
201
- copyDirSync(sourceCursorDir, targetCursorDir);
200
+ const sourceClaudefilesDir = path.join(packageRoot, '.claudefiles');
201
+ if (fs.existsSync(promptsSource)) {
202
+ const content = fs.readFileSync(promptsSource, 'utf8');
203
+ const cursorFrontmatter = `---
204
+ globs:
205
+ alwaysApply: true
206
+ ---
207
+
208
+ `;
209
+ const claudeIntro = `# Embeddables project (Claude Code)
210
+
211
+ This is an Embeddables project managed by the Embeddables CLI. The CLI transforms Embeddable JSON into a local file structure for development, then compiles it back to JSON for saving.
212
+
213
+ For full context when editing embeddables (pages, components, styles, actions, computed fields), see:
214
+
215
+ - **.claudefiles/embeddables-cli.md** — Embeddables CLI structure, types, file layout, and conventions.
216
+
217
+ All TypeScript types are in \`.types/\` (generated by \`embeddables init\`). Use \`embeddables dev\` to run the dev server and \`embeddables pull\` / \`embeddables save\` to sync with the project.
218
+ `;
219
+ const cursorRulesDir = path.join(cwd, '.cursor', 'rules');
220
+ const claudefilesDir = path.join(cwd, '.claudefiles');
221
+ fs.mkdirSync(cursorRulesDir, { recursive: true });
222
+ fs.mkdirSync(claudefilesDir, { recursive: true });
223
+ fs.writeFileSync(path.join(cursorRulesDir, 'embeddables-cli.md'), cursorFrontmatter + content, 'utf8');
224
+ fs.writeFileSync(path.join(claudefilesDir, 'CLAUDE.md'), claudeIntro, 'utf8');
225
+ fs.writeFileSync(path.join(claudefilesDir, 'embeddables-cli.md'), content, 'utf8');
202
226
  console.log(pc.green(' ✓ Injected .cursor/ rules'));
227
+ console.log(pc.green(' ✓ Injected .claudefiles/'));
228
+ }
229
+ else if (fs.existsSync(sourceCursorDir) || fs.existsSync(sourceClaudefilesDir)) {
230
+ if (fs.existsSync(sourceCursorDir)) {
231
+ copyDirSync(sourceCursorDir, path.join(cwd, '.cursor'));
232
+ console.log(pc.green(' ✓ Injected .cursor/ rules'));
233
+ }
234
+ if (fs.existsSync(sourceClaudefilesDir)) {
235
+ copyDirSync(sourceClaudefilesDir, path.join(cwd, '.claudefiles'));
236
+ console.log(pc.green(' ✓ Injected .claudefiles/'));
237
+ }
203
238
  }
204
239
  // Create tsconfig.json for editor support (JSX, type checking)
205
240
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
@@ -0,0 +1,28 @@
1
+ export interface ExperimentInfo {
2
+ id: string;
3
+ experiment_id: string;
4
+ experiment_key: string;
5
+ name: string | null;
6
+ status: string;
7
+ flow_id: string;
8
+ flow_ids: string[] | null;
9
+ }
10
+ export interface PromptForExperimentOptions {
11
+ /** Custom message for the prompt */
12
+ message?: string;
13
+ /** Filter out experiments already connected to this embeddable ID */
14
+ excludeConnectedTo?: string;
15
+ }
16
+ /**
17
+ * Fetch experiments for a project from Supabase (using experiments_extended view)
18
+ */
19
+ export declare function fetchProjectExperiments(projectId: string): Promise<ExperimentInfo[]>;
20
+ /**
21
+ * Prompt the user to select an experiment from the project
22
+ * Returns null if no experiments found or user cancels
23
+ */
24
+ export declare function promptForExperiment(projectId: string, options?: PromptForExperimentOptions): Promise<{
25
+ experiment_id: string;
26
+ experiment_key: string;
27
+ } | null>;
28
+ //# sourceMappingURL=experiments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiments.d.ts","sourceRoot":"","sources":["../../src/prompts/experiments.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,0BAA0B;IACzC,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CA+B1F;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA4DnE"}
@@ -0,0 +1,86 @@
1
+ import pc from 'picocolors';
2
+ import prompts from 'prompts';
3
+ import { getAuthenticatedSupabaseClient } from '../auth/index.js';
4
+ /**
5
+ * Fetch experiments for a project from Supabase (using experiments_extended view)
6
+ */
7
+ export async function fetchProjectExperiments(projectId) {
8
+ const supabase = await getAuthenticatedSupabaseClient();
9
+ if (!supabase) {
10
+ return [];
11
+ }
12
+ try {
13
+ const { data, error } = await supabase
14
+ .from('experiments_extended')
15
+ .select('id, experiment_id, experiment_key, name, status, flow_id, flow_ids')
16
+ .eq('project_id', projectId)
17
+ .order('name', { ascending: true, nullsFirst: false });
18
+ if (error) {
19
+ console.warn(pc.yellow(`Could not fetch experiments: ${error.message}`));
20
+ return [];
21
+ }
22
+ return (data || []).map((row) => ({
23
+ id: row.id,
24
+ experiment_id: row.experiment_id,
25
+ experiment_key: row.experiment_key,
26
+ name: row.name || null,
27
+ status: row.status || 'draft',
28
+ flow_id: row.flow_id,
29
+ flow_ids: row.flow_ids || null,
30
+ }));
31
+ }
32
+ catch (err) {
33
+ console.warn(pc.yellow(`Could not fetch experiments: ${err}`));
34
+ return [];
35
+ }
36
+ }
37
+ /**
38
+ * Prompt the user to select an experiment from the project
39
+ * Returns null if no experiments found or user cancels
40
+ */
41
+ export async function promptForExperiment(projectId, options = {}) {
42
+ const { message = 'Select an experiment to connect:', excludeConnectedTo } = options;
43
+ const experiments = await fetchProjectExperiments(projectId);
44
+ let filtered = experiments;
45
+ if (excludeConnectedTo) {
46
+ filtered = experiments.filter((e) => {
47
+ const connected = e.flow_ids ?? [e.flow_id];
48
+ return !connected.includes(excludeConnectedTo);
49
+ });
50
+ }
51
+ if (filtered.length === 0) {
52
+ if (excludeConnectedTo && experiments.length > 0) {
53
+ console.log(pc.yellow('All experiments in this project are already connected to this embeddable.'));
54
+ }
55
+ else {
56
+ console.log(pc.yellow('No experiments found in this project.'));
57
+ }
58
+ return null;
59
+ }
60
+ const choices = filtered.map((e) => ({
61
+ title: e.name || e.experiment_key,
62
+ description: `${e.experiment_key} (${e.experiment_id})`,
63
+ value: e,
64
+ }));
65
+ const response = await prompts({
66
+ type: 'autocomplete',
67
+ name: 'experiment',
68
+ message,
69
+ choices,
70
+ suggest: (input, choices) => Promise.resolve(choices.filter((c) => (c.title?.toLowerCase().includes(input.toLowerCase()) ?? false) ||
71
+ String(c.value.experiment_key).toLowerCase().includes(input.toLowerCase()) ||
72
+ String(c.value.experiment_id).toLowerCase().includes(input.toLowerCase()))),
73
+ }, {
74
+ onCancel: () => {
75
+ process.exit(0);
76
+ },
77
+ });
78
+ const selected = response.experiment;
79
+ if (!selected) {
80
+ return null;
81
+ }
82
+ return {
83
+ experiment_id: selected.experiment_id,
84
+ experiment_key: selected.experiment_key,
85
+ };
86
+ }
@@ -4,4 +4,6 @@ export { fetchProjectEmbeddables, fetchEmbeddableMetadata, promptForEmbeddable,
4
4
  export type { EmbeddableInfo, EmbeddableMetadata, LocalEmbeddable, PromptForEmbeddableOptions, } from './embeddables.js';
5
5
  export { fetchBranches, promptForBranch } from './branches.js';
6
6
  export type { BranchInfo } from './branches.js';
7
+ export { fetchProjectExperiments, promptForExperiment, } from './experiments.js';
8
+ export type { ExperimentInfo, PromptForExperimentOptions } from './experiments.js';
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAC/D,YAAY,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAEzE,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,0BAA0B,GAC3B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAC/D,YAAY,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAEzE,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,0BAA0B,GAC3B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C,OAAO,EACL,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,cAAc,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAA"}
@@ -2,3 +2,4 @@
2
2
  export { fetchProjects, promptForProject } from './projects.js';
3
3
  export { fetchProjectEmbeddables, fetchEmbeddableMetadata, promptForEmbeddable, discoverLocalEmbeddables, promptForLocalEmbeddable, } from './embeddables.js';
4
4
  export { fetchBranches, promptForBranch } from './branches.js';
5
+ export { fetchProjectExperiments, promptForExperiment, } from './experiments.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@embeddables/cli",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "embeddables": "./bin/embeddables.mjs"
@@ -24,7 +24,7 @@
24
24
  "files": [
25
25
  "bin",
26
26
  "dist",
27
- ".cursor",
27
+ ".prompts",
28
28
  "README.md"
29
29
  ],
30
30
  "engines": {
@@ -43,7 +43,8 @@
43
43
  "test:watch": "vitest --watch",
44
44
  "test:coverage": "vitest --coverage",
45
45
  "test:verbose": "vitest --reporter verbose",
46
- "prepack": "node scripts/readme-swap.cjs prepack",
46
+ "build:prompts": "node scripts/build-prompts.cjs",
47
+ "prepack": "node scripts/readme-swap.cjs prepack && node scripts/build-prompts.cjs",
47
48
  "postpack": "node scripts/readme-swap.cjs postpack"
48
49
  },
49
50
  "dependencies": {
File without changes