@embeddables/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,21 +11,29 @@ A CLI for authoring and managing Embeddables locally using TypeScript/TSX.
11
11
 
12
12
  ## Installation
13
13
 
14
+ Install the CLI globally:
15
+
14
16
  ```bash
15
- npm install @embeddables/cli
17
+ npm install -g @embeddables/cli
16
18
  ```
17
19
 
18
20
  ## Quick Start
19
21
 
20
22
  ```bash
23
+ # Initialize a new project (will ask for your Embeddables project ID)
24
+ embeddables init
25
+
26
+ # Install dependencies
27
+ npm install
28
+
21
29
  # Login to your Embeddables account
22
- npx embeddables login
30
+ embeddables login
23
31
 
24
- # Pull an existing embeddable
25
- npx embeddables pull --id <embeddable-id>
32
+ # Pull an embeddable (shows a list to choose from)
33
+ embeddables pull
26
34
 
27
35
  # Start dev server with hot reload
28
- npx embeddables dev --id <embeddable-id>
36
+ embeddables dev
29
37
  ```
30
38
 
31
39
  This creates the following structure in your repo:
@@ -43,20 +51,42 @@ embeddables/
43
51
 
44
52
  ## Commands
45
53
 
54
+ ### `embeddables init`
55
+ Initialize a new Embeddables project. Creates `package.json`, `embeddables.json`, `.gitignore`, and an `embeddables/` directory.
56
+
57
+ If you're logged in, you'll see an interactive list of your projects to choose from. Otherwise, you can enter a project ID manually or skip.
58
+
59
+ Options:
60
+ - `--name <name>`: Project name (prompts if not provided)
61
+ - `--project-id <id>`: Embeddables project ID (shows list if logged in)
62
+ - `-y, --yes`: Skip prompts and use defaults
63
+
64
+ The project ID is stored in `embeddables.json` and enables interactive embeddable selection when running `embeddables pull`.
65
+
46
66
  ### `embeddables login`
47
67
  Authenticate with your Embeddables account.
48
68
 
49
69
  ### `embeddables logout`
50
70
  Clear stored authentication.
51
71
 
52
- ### `embeddables pull --id <id>`
72
+ ### `embeddables pull`
53
73
  Fetch an embeddable from the cloud and reverse-compile it into local TSX files.
54
74
 
75
+ If you're logged in and run `embeddables pull` without arguments:
76
+ 1. If no project is configured, you'll be prompted to select one (saved to `embeddables.json`)
77
+ 2. Then you'll see an interactive list of embeddables to choose from
78
+
55
79
  Options:
56
- - `--id <id>` (required): Embeddable ID to pull
80
+ - `--id <id>`: Embeddable ID to pull (skips interactive selection)
57
81
  - `--branch <branch_id>`: Pull a specific branch version
58
82
  - `--fix`: Remove components with missing required props instead of erroring
59
83
 
84
+ ### `embeddables branch`
85
+ Switch to a different branch of a local embeddable. Shows an interactive list of branches to choose from.
86
+
87
+ Options:
88
+ - `--id <id>`: Embeddable ID (will prompt from local embeddables if not provided)
89
+
60
90
  ### `embeddables build --id <id>`
61
91
  Compile TSX pages into the canonical JSON format.
62
92
 
@@ -35,7 +35,7 @@ export declare function getSupabaseClient(): SupabaseClient;
35
35
  /**
36
36
  * Get authenticated Supabase client (with stored session)
37
37
  */
38
- export declare function getAuthenticatedSupabaseClient(): SupabaseClient | null;
38
+ export declare function getAuthenticatedSupabaseClient(): Promise<SupabaseClient | null>;
39
39
  /**
40
40
  * Get access token for authenticated requests
41
41
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAGpE,eAAO,MAAM,YAAY,6CAA6C,CAAA;AACtE,eAAO,MAAM,iBAAiB,mDAAmD,CAAA;AAEjF,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAKD;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,UAAU,GAAG,IAAI,CAUlD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAGxD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAcpC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,CAWlD;AAED;;GAEG;AACH,wBAAgB,8BAA8B,IAAI,cAAc,GAAG,IAAI,CActE;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAM9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAGpE,eAAO,MAAM,YAAY,6CAA6C,CAAA;AACtE,eAAO,MAAM,iBAAiB,mDAAmD,CAAA;AAEjF,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAMD;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,UAAU,GAAG,IAAI,CAUlD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAGxD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAcpC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,CAWlD;AAED;;GAEG;AACH,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAcrF;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAM9C"}
@@ -1,10 +1,12 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { createClient } from '@supabase/supabase-js';
4
5
  // TODO: Move to environment variables
5
6
  export const SUPABASE_URL = 'https://ierxexdtyashuotcsjyo.supabase.co'; // Replace with actual Supabase URL
6
7
  export const SUPABASE_ANON_KEY = 'sb_publishable_Vt8-msWNtn2hhOY3AA6RsQ_ueTx79Vo'; // Replace with actual Supabase anon key
7
- const AUTH_DIR = path.join(process.cwd(), '.auth');
8
+ // Store auth globally in user's home directory
9
+ const AUTH_DIR = path.join(os.homedir(), '.embeddables');
8
10
  const AUTH_FILE = path.join(AUTH_DIR, 'auth.json');
9
11
  /**
10
12
  * Get the path to the auth config file
@@ -75,14 +77,14 @@ export function getSupabaseClient() {
75
77
  /**
76
78
  * Get authenticated Supabase client (with stored session)
77
79
  */
78
- export function getAuthenticatedSupabaseClient() {
80
+ export async function getAuthenticatedSupabaseClient() {
79
81
  const config = readAuthConfig();
80
82
  if (!config || !isLoggedIn()) {
81
83
  return null;
82
84
  }
83
85
  const client = getSupabaseClient();
84
86
  // Set the session manually
85
- client.auth.setSession({
87
+ await client.auth.setSession({
86
88
  access_token: config.access_token,
87
89
  refresh_token: config.refresh_token,
88
90
  });
package/dist/cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { Command } from 'commander';
2
2
  import pc from 'picocolors';
3
+ import { runBranch } from './commands/branch.js';
3
4
  import { runBuild } from './commands/build.js';
4
5
  import { runBuildWorkbench } from './commands/build-workbench.js';
5
6
  import { runDev } from './commands/dev.js';
7
+ import { runInit } from './commands/init.js';
6
8
  import { runLogin } from './commands/login.js';
7
9
  import { runLogout } from './commands/logout.js';
8
10
  import { runPull } from './commands/pull.js';
@@ -17,6 +19,15 @@ console.error = (...args) => {
17
19
  };
18
20
  const program = new Command();
19
21
  program.name('embeddables').description('Embeddables CLI').version('0.1.0');
22
+ program
23
+ .command('init')
24
+ .description('Initialize a new Embeddables project')
25
+ .option('--name <name>', 'Project name')
26
+ .option('--project-id <id>', 'Embeddables project ID')
27
+ .option('-y, --yes', 'Skip prompts and use defaults')
28
+ .action(async (opts) => {
29
+ await runInit({ name: opts.name, projectId: opts.projectId, yes: opts.yes });
30
+ });
20
31
  program
21
32
  .command('build')
22
33
  .requiredOption('--id <id>', 'Embeddable ID')
@@ -57,13 +68,21 @@ program
57
68
  });
58
69
  program
59
70
  .command('pull')
60
- .requiredOption('--id <id>', 'Embeddable ID to pull')
71
+ .description('Pull an embeddable from the cloud')
72
+ .option('--id <id>', 'Embeddable ID to pull (interactive selection if not provided)')
61
73
  .option('--out <path>', 'Output json path')
62
74
  .option('--branch <branch_id>', 'Embeddable branch ID')
63
75
  .option('--fix', 'Fix by removing components missing required props (warn instead of error)')
64
76
  .action(async (opts) => {
65
77
  await runPull(opts);
66
78
  });
79
+ program
80
+ .command('branch')
81
+ .description('Switch to a different branch of an embeddable')
82
+ .option('--id <id>', 'Embeddable ID (will prompt if not provided)')
83
+ .action(async (opts) => {
84
+ await runBranch(opts);
85
+ });
67
86
  program
68
87
  .command('build-workbench')
69
88
  .description('Build Workbench for CDN deployment')
@@ -0,0 +1,4 @@
1
+ export declare function runBranch(opts: {
2
+ id?: string;
3
+ }): Promise<void>;
4
+ //# sourceMappingURL=branch.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,46 @@
1
+ import pc from 'picocolors';
2
+ import { isLoggedIn } from '../auth/index.js';
3
+ import { runPull } from './pull.js';
4
+ import { promptForLocalEmbeddable, fetchBranches, promptForBranch } from '../prompts/index.js';
5
+ export async function runBranch(opts) {
6
+ // Check login status
7
+ if (!isLoggedIn()) {
8
+ console.error(pc.red('Not logged in.'));
9
+ console.log(pc.gray('Run "embeddables login" first.'));
10
+ process.exit(1);
11
+ }
12
+ // Get embeddable ID
13
+ let embeddableId = opts.id;
14
+ if (!embeddableId) {
15
+ const selected = await promptForLocalEmbeddable();
16
+ if (!selected) {
17
+ process.exit(1);
18
+ }
19
+ embeddableId = selected;
20
+ }
21
+ console.log('');
22
+ console.log(pc.cyan('Fetching branches...'));
23
+ // Fetch branches for this embeddable
24
+ const branches = await fetchBranches(embeddableId);
25
+ if (branches.length === 0) {
26
+ console.log(pc.yellow('No branches found for this embeddable.'));
27
+ console.log(pc.gray('Branches are created in the Embeddables Builder.'));
28
+ return;
29
+ }
30
+ console.log('');
31
+ // Prompt for branch selection
32
+ const selectedBranch = await promptForBranch(branches);
33
+ console.log('');
34
+ // Pull the selected branch
35
+ if (selectedBranch === null) {
36
+ // User selected "main" - pull without branch
37
+ console.log(pc.cyan('Switching to main (latest published version)...'));
38
+ console.log('');
39
+ await runPull({ id: embeddableId });
40
+ }
41
+ else {
42
+ console.log(pc.cyan(`Switching to branch: ${selectedBranch.name}...`));
43
+ console.log('');
44
+ await runPull({ id: embeddableId, branch: selectedBranch.id });
45
+ }
46
+ }
@@ -17,12 +17,12 @@ async function discoverEmbeddables() {
17
17
  if (!entry.isDirectory())
18
18
  continue;
19
19
  const id = entry.name;
20
- const configPath = path.join(embeddablesDir, id, 'config.json');
20
+ const metadataPath = path.join(embeddablesDir, id, 'metadata.json');
21
21
  let title = null;
22
- if (fs.existsSync(configPath)) {
22
+ if (fs.existsSync(metadataPath)) {
23
23
  try {
24
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
25
- title = config.title || null;
24
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
25
+ title = metadata.title || null;
26
26
  }
27
27
  catch {
28
28
  // Ignore JSON parse errors
@@ -0,0 +1,6 @@
1
+ export declare function runInit(opts: {
2
+ name?: string;
3
+ projectId?: string;
4
+ yes?: boolean;
5
+ }): Promise<void>;
6
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAiBA,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,iBAsMvF"}
@@ -0,0 +1,188 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import pc from 'picocolors';
4
+ import prompts from 'prompts';
5
+ import { writeProjectConfig, readProjectConfig } from '../config/index.js';
6
+ import { isLoggedIn } from '../auth/index.js';
7
+ import { promptForProject } from '../prompts/index.js';
8
+ export async function runInit(opts) {
9
+ const cwd = process.cwd();
10
+ const packageJsonPath = path.join(cwd, 'package.json');
11
+ const gitignorePath = path.join(cwd, '.gitignore');
12
+ const embeddablesDir = path.join(cwd, 'embeddables');
13
+ console.log('');
14
+ console.log(pc.bold(pc.cyan(' Embeddables Project Setup')));
15
+ console.log('');
16
+ // Check if already initialized
17
+ let existingPackageJson = null;
18
+ if (fs.existsSync(packageJsonPath)) {
19
+ try {
20
+ existingPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
21
+ }
22
+ catch {
23
+ // Invalid JSON, we'll overwrite
24
+ }
25
+ }
26
+ const existingConfig = readProjectConfig();
27
+ // Step 1: Get Embeddables project (this determines the default name)
28
+ let projectId = opts.projectId;
29
+ let selectedProjectTitle;
30
+ if (!projectId && !opts.yes) {
31
+ if (existingConfig?.project_id) {
32
+ console.log(pc.gray(` Using existing project ID: ${existingConfig.project_id}`));
33
+ projectId = existingConfig.project_id;
34
+ selectedProjectTitle = existingConfig.project_name || undefined;
35
+ }
36
+ else if (isLoggedIn()) {
37
+ // Fetch and show project list
38
+ console.log(pc.gray(' Fetching projects...'));
39
+ const selectedProject = await promptForProject({
40
+ allowSkip: true,
41
+ message: 'Select your Embeddables project:',
42
+ });
43
+ if (selectedProject) {
44
+ projectId = selectedProject.id;
45
+ selectedProjectTitle = selectedProject.title || undefined;
46
+ }
47
+ }
48
+ else {
49
+ // Not logged in - offer manual entry or skip
50
+ console.log(pc.gray(' (Login with "embeddables login" to select from your projects)'));
51
+ const response = await prompts({
52
+ type: 'text',
53
+ name: 'projectId',
54
+ message: 'Embeddables project ID (optional):',
55
+ hint: 'e.g., proj_abc123 - leave empty to skip',
56
+ }, {
57
+ onCancel: () => {
58
+ console.log(pc.gray('\n Cancelled.'));
59
+ process.exit(0);
60
+ },
61
+ });
62
+ projectId = response.projectId || undefined;
63
+ }
64
+ }
65
+ // Step 2: Get project name (default to selected project title, or existing, or directory name)
66
+ let projectName = opts.name;
67
+ if (!projectName) {
68
+ if (existingPackageJson?.name) {
69
+ projectName = existingPackageJson.name;
70
+ console.log(pc.gray(` Using existing project name: ${projectName}`));
71
+ }
72
+ else if (opts.yes) {
73
+ // Use selected project title or directory name as default
74
+ projectName = selectedProjectTitle || path.basename(cwd);
75
+ }
76
+ else {
77
+ // Default to selected project title, then directory name
78
+ const defaultName = selectedProjectTitle || path.basename(cwd);
79
+ const response = await prompts({
80
+ type: 'text',
81
+ name: 'name',
82
+ message: 'Project name:',
83
+ initial: defaultName,
84
+ }, {
85
+ onCancel: () => {
86
+ console.log(pc.gray('\n Cancelled.'));
87
+ process.exit(0);
88
+ },
89
+ });
90
+ projectName = response.name;
91
+ }
92
+ }
93
+ if (!projectName) {
94
+ console.error('Project name is required.');
95
+ process.exit(1);
96
+ }
97
+ // Create or update package.json
98
+ const packageJson = existingPackageJson || {
99
+ name: projectName,
100
+ version: '1.0.0',
101
+ type: 'module',
102
+ };
103
+ // Ensure name is set
104
+ if (!packageJson.name) {
105
+ packageJson.name = projectName;
106
+ }
107
+ // Ensure type is module
108
+ if (!packageJson.type) {
109
+ packageJson.type = 'module';
110
+ }
111
+ // Add @embeddables/cli as a dependency
112
+ if (!packageJson.dependencies) {
113
+ packageJson.dependencies = {};
114
+ }
115
+ if (!packageJson.dependencies['@embeddables/cli']) {
116
+ packageJson.dependencies['@embeddables/cli'] = '^0.1.0';
117
+ }
118
+ // Write package.json
119
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
120
+ if (existingPackageJson) {
121
+ console.log(pc.green(' ✓ Updated package.json'));
122
+ }
123
+ else {
124
+ console.log(pc.green(' ✓ Created package.json'));
125
+ }
126
+ // Write embeddables.json config
127
+ if (projectId) {
128
+ writeProjectConfig({
129
+ project_id: projectId,
130
+ project_name: selectedProjectTitle || projectName,
131
+ });
132
+ console.log(pc.green(' ✓ Created embeddables.json'));
133
+ }
134
+ // Create or update .gitignore
135
+ const gitignoreEntries = ['node_modules/', '**/.generated/', '.DS_Store'];
136
+ let existingGitignore = '';
137
+ if (fs.existsSync(gitignorePath)) {
138
+ existingGitignore = fs.readFileSync(gitignorePath, 'utf8');
139
+ }
140
+ const newEntries = gitignoreEntries.filter((entry) => !existingGitignore.includes(entry.replace('/', '')));
141
+ if (newEntries.length > 0) {
142
+ const separator = existingGitignore && !existingGitignore.endsWith('\n') ? '\n' : '';
143
+ const embeddablesSection = existingGitignore
144
+ ? `${separator}\n# Embeddables\n${newEntries.join('\n')}\n`
145
+ : `# Embeddables\n${gitignoreEntries.join('\n')}\n`;
146
+ fs.writeFileSync(gitignorePath, existingGitignore + embeddablesSection, 'utf8');
147
+ if (existingGitignore) {
148
+ console.log(pc.green(' ✓ Updated .gitignore'));
149
+ }
150
+ else {
151
+ console.log(pc.green(' ✓ Created .gitignore'));
152
+ }
153
+ }
154
+ else {
155
+ console.log(pc.gray(' ✓ .gitignore already configured'));
156
+ }
157
+ // Create embeddables directory
158
+ if (!fs.existsSync(embeddablesDir)) {
159
+ fs.mkdirSync(embeddablesDir, { recursive: true });
160
+ console.log(pc.green(' ✓ Created embeddables/ directory'));
161
+ }
162
+ else {
163
+ console.log(pc.gray(' ✓ embeddables/ directory exists'));
164
+ }
165
+ // Print next steps
166
+ console.log('');
167
+ console.log(pc.bold(' Next steps:'));
168
+ console.log('');
169
+ console.log(pc.cyan(' 1. Install dependencies:'));
170
+ console.log(pc.white(' npm install'));
171
+ console.log('');
172
+ console.log(pc.cyan(' 2. Login to Embeddables:'));
173
+ console.log(pc.white(' npx embeddables login'));
174
+ console.log('');
175
+ if (projectId) {
176
+ console.log(pc.cyan(' 3. Pull an embeddable:'));
177
+ console.log(pc.white(' npx embeddables pull'));
178
+ console.log(pc.gray(' (will show a list of embeddables in your project)'));
179
+ }
180
+ else {
181
+ console.log(pc.cyan(' 3. Pull an existing embeddable:'));
182
+ console.log(pc.white(' npx embeddables pull --id <embeddable-id>'));
183
+ }
184
+ console.log('');
185
+ console.log(pc.cyan(' 4. Start developing:'));
186
+ console.log(pc.white(' npx embeddables dev'));
187
+ console.log('');
188
+ }
@@ -1,5 +1,5 @@
1
1
  export declare function runPull(opts: {
2
- id: string;
2
+ id?: string;
3
3
  out?: string;
4
4
  branch?: string;
5
5
  fix?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AAOA,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,iBA0G/F"}
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,iBAmMhG"}
@@ -1,10 +1,50 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import pc from 'picocolors';
4
+ import prompts from 'prompts';
4
5
  import { reverseCompile } from '../compiler/reverse.js';
5
- import { getAccessToken } from '../auth/index.js';
6
+ import { getAccessToken, isLoggedIn } from '../auth/index.js';
7
+ import { getProjectId, writeProjectConfig } from '../config/index.js';
8
+ import { promptForProject, promptForEmbeddable, fetchEmbeddableMetadata } from '../prompts/index.js';
6
9
  export async function runPull(opts) {
7
- const embeddableId = opts.id;
10
+ let embeddableId = opts.id;
11
+ // If no ID provided, try to get it interactively
12
+ if (!embeddableId) {
13
+ if (!isLoggedIn()) {
14
+ console.error(pc.red('No embeddable ID provided and not logged in.'));
15
+ console.log('');
16
+ console.log(pc.gray('Either:'));
17
+ console.log(pc.gray(' 1. Use --id <embeddable-id> to specify an embeddable'));
18
+ console.log(pc.gray(' 2. Run "embeddables login" first for interactive selection'));
19
+ process.exit(1);
20
+ }
21
+ let projectId = getProjectId();
22
+ // If no project ID configured, prompt for project selection
23
+ if (!projectId) {
24
+ console.log(pc.cyan('Fetching projects...'));
25
+ const selectedProject = await promptForProject();
26
+ if (!selectedProject) {
27
+ process.exit(1);
28
+ }
29
+ projectId = selectedProject.id;
30
+ // Save the selected project to config
31
+ writeProjectConfig({
32
+ project_id: projectId,
33
+ project_name: selectedProject.title || undefined,
34
+ });
35
+ console.log(pc.green(`✓ Saved project to embeddables.json`));
36
+ console.log('');
37
+ }
38
+ console.log(pc.cyan('Fetching embeddables from project...'));
39
+ const selected = await promptForEmbeddable(projectId, {
40
+ message: 'Select an embeddable to pull:',
41
+ });
42
+ if (!selected) {
43
+ process.exit(1);
44
+ }
45
+ embeddableId = selected;
46
+ console.log('');
47
+ }
8
48
  let url = `https://engine.embeddables.com/${embeddableId}?version=latest`;
9
49
  if (opts.branch) {
10
50
  url += `&embeddable_branch=${opts.branch}`;
@@ -44,6 +84,14 @@ export async function runPull(opts) {
44
84
  fs.writeFileSync(versionedPath, flowJson, 'utf8');
45
85
  console.log(pc.cyan(`✓ Saved embeddable JSON to ${versionedPath}`));
46
86
  }
87
+ // Fetch and save flow metadata from DB (title, archived, created_at)
88
+ const metadata = await fetchEmbeddableMetadata(embeddableId);
89
+ if (metadata) {
90
+ const metadataPath = path.join('embeddables', embeddableId, 'metadata.json');
91
+ fs.mkdirSync(path.dirname(metadataPath), { recursive: true });
92
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + '\n', 'utf8');
93
+ console.log(pc.cyan(`✓ Saved flow metadata to ${metadataPath}`));
94
+ }
47
95
  // Clear existing pages, styles, computed-fields, actions, and global-components before generating new ones
48
96
  const pagesDir = path.join('embeddables', embeddableId, 'pages');
49
97
  const stylesDir = path.join('embeddables', embeddableId, 'styles');
@@ -88,7 +136,40 @@ export async function runPull(opts) {
88
136
  console.log(`${pc.gray(`Cleared ${existingGlobalComponents.length} existing global component(s)`)}`);
89
137
  }
90
138
  // Run reverse compiler
91
- await reverseCompile(flow, embeddableId, { fix: opts.fix });
139
+ try {
140
+ await reverseCompile(flow, embeddableId, { fix: opts.fix });
141
+ }
142
+ catch (compileError) {
143
+ // If fix mode wasn't already enabled, offer to retry with fix mode
144
+ if (!opts.fix && compileError instanceof Error) {
145
+ console.log('');
146
+ console.error(pc.red('Error during reverse compile:'));
147
+ console.error(pc.yellow(` ${compileError.message}`));
148
+ console.log('');
149
+ const response = await prompts({
150
+ type: 'confirm',
151
+ name: 'fix',
152
+ message: 'Would you like to retry with auto-fix enabled? (removes problematic components)',
153
+ initial: true,
154
+ }, {
155
+ onCancel: () => {
156
+ process.exit(1);
157
+ },
158
+ });
159
+ if (response.fix) {
160
+ console.log('');
161
+ console.log(pc.cyan('Retrying with auto-fix enabled...'));
162
+ console.log('');
163
+ await reverseCompile(flow, embeddableId, { fix: true });
164
+ }
165
+ else {
166
+ process.exit(1);
167
+ }
168
+ }
169
+ else {
170
+ throw compileError;
171
+ }
172
+ }
92
173
  }
93
174
  catch (error) {
94
175
  console.error('Error pulling embeddable:', error);
@@ -0,0 +1,21 @@
1
+ export interface ProjectConfig {
2
+ project_id?: string;
3
+ project_name?: string;
4
+ }
5
+ /**
6
+ * Get the path to the project config file
7
+ */
8
+ export declare function getConfigFilePath(): string;
9
+ /**
10
+ * Read project config from file
11
+ */
12
+ export declare function readProjectConfig(): ProjectConfig | null;
13
+ /**
14
+ * Write project config to file
15
+ */
16
+ export declare function writeProjectConfig(config: ProjectConfig): void;
17
+ /**
18
+ * Get the project ID from config
19
+ */
20
+ export declare function getProjectId(): string | null;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAID;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,aAAa,GAAG,IAAI,CAWxD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAQ9D;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAG5C"}
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const CONFIG_FILE = 'embeddables.json';
4
+ /**
5
+ * Get the path to the project config file
6
+ */
7
+ export function getConfigFilePath() {
8
+ return path.join(process.cwd(), CONFIG_FILE);
9
+ }
10
+ /**
11
+ * Read project config from file
12
+ */
13
+ export function readProjectConfig() {
14
+ const configPath = getConfigFilePath();
15
+ try {
16
+ if (!fs.existsSync(configPath)) {
17
+ return null;
18
+ }
19
+ const content = fs.readFileSync(configPath, 'utf8');
20
+ return JSON.parse(content);
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ /**
27
+ * Write project config to file
28
+ */
29
+ export function writeProjectConfig(config) {
30
+ const configPath = getConfigFilePath();
31
+ // Merge with existing config
32
+ const existing = readProjectConfig() || {};
33
+ const merged = { ...existing, ...config };
34
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
35
+ }
36
+ /**
37
+ * Get the project ID from config
38
+ */
39
+ export function getProjectId() {
40
+ const config = readProjectConfig();
41
+ return config?.project_id || null;
42
+ }
@@ -0,0 +1,6 @@
1
+ export declare const BranchStatus: {
2
+ readonly ACTIVE: "ACTIVE";
3
+ readonly MERGED: "MERGED";
4
+ };
5
+ export type BranchStatus = (typeof BranchStatus)[keyof typeof BranchStatus];
6
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY;;;CAGf,CAAA;AAEV,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAA"}
@@ -0,0 +1,4 @@
1
+ export const BranchStatus = {
2
+ ACTIVE: 'ACTIVE',
3
+ MERGED: 'MERGED',
4
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Format a date string or Date object as YYYY-MM-DD.
3
+ */
4
+ export declare function formatDate(date: string | Date): string;
5
+ //# sourceMappingURL=dates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dates.d.ts","sourceRoot":"","sources":["../../src/helpers/dates.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGtD"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Format a date string or Date object as YYYY-MM-DD.
3
+ */
4
+ export function formatDate(date) {
5
+ const d = typeof date === 'string' ? new Date(date) : date;
6
+ return d.toISOString().slice(0, 10);
7
+ }
@@ -0,0 +1,20 @@
1
+ import { BranchStatus } from '../constants.js';
2
+ export interface BranchInfo {
3
+ id: string;
4
+ name: string;
5
+ status: BranchStatus;
6
+ created_at: string;
7
+ origin_version: number | null;
8
+ origin_branch: string | null;
9
+ }
10
+ /**
11
+ * Fetch all branches for an embeddable from Supabase
12
+ */
13
+ export declare function fetchBranches(flowId: string): Promise<BranchInfo[]>;
14
+ /**
15
+ * Prompt the user to select a branch
16
+ * Returns null if user selects "main" or cancels
17
+ * The "main" option is always included at the top
18
+ */
19
+ export declare function promptForBranch(branches: BranchInfo[]): Promise<BranchInfo | null>;
20
+ //# sourceMappingURL=branches.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branches.d.ts","sourceRoot":"","sources":["../../src/prompts/branches.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAG9C,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,YAAY,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CA8BzE;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA8DxF"}
@@ -0,0 +1,89 @@
1
+ import pc from 'picocolors';
2
+ import prompts from 'prompts';
3
+ import { getAuthenticatedSupabaseClient } from '../auth/index.js';
4
+ import { BranchStatus } from '../constants.js';
5
+ import { formatDate } from '../helpers/dates.js';
6
+ /**
7
+ * Fetch all branches for an embeddable from Supabase
8
+ */
9
+ export async function fetchBranches(flowId) {
10
+ const supabase = await getAuthenticatedSupabaseClient();
11
+ if (!supabase) {
12
+ return [];
13
+ }
14
+ try {
15
+ const { data, error } = await supabase
16
+ .from('branches')
17
+ .select('id, name, status, created_at, origin_version, origin_branch')
18
+ .eq('flow_id', flowId)
19
+ .order('created_at', { ascending: false });
20
+ if (error) {
21
+ console.warn(pc.yellow(`Could not fetch branches: ${error.message}`));
22
+ return [];
23
+ }
24
+ return (data || []).map((row) => ({
25
+ id: row.id,
26
+ name: row.name,
27
+ status: row.status,
28
+ created_at: row.created_at,
29
+ origin_version: row.origin_version,
30
+ origin_branch: row.origin_branch,
31
+ }));
32
+ }
33
+ catch (err) {
34
+ console.warn(pc.yellow(`Could not fetch branches: ${err}`));
35
+ return [];
36
+ }
37
+ }
38
+ /**
39
+ * Prompt the user to select a branch
40
+ * Returns null if user selects "main" or cancels
41
+ * The "main" option is always included at the top
42
+ */
43
+ export async function promptForBranch(branches) {
44
+ // Separate active and merged branches
45
+ const activeBranches = branches.filter((b) => b.status === BranchStatus.ACTIVE);
46
+ const mergedBranches = branches.filter((b) => b.status === BranchStatus.MERGED);
47
+ const choices = [];
48
+ // Add "main" option first
49
+ choices.push({
50
+ title: pc.bold('main'),
51
+ value: 'main',
52
+ description: 'Latest published version',
53
+ });
54
+ // Add active branches
55
+ for (const branch of activeBranches) {
56
+ const date = formatDate(branch.created_at);
57
+ choices.push({
58
+ title: branch.name,
59
+ value: branch.id,
60
+ description: `Active · Created ${date}`,
61
+ });
62
+ }
63
+ // Add merged branches (dimmed)
64
+ for (const branch of mergedBranches) {
65
+ const date = formatDate(branch.created_at);
66
+ choices.push({
67
+ title: pc.dim(branch.name),
68
+ value: branch.id,
69
+ description: pc.dim(`Merged · Created ${date}`),
70
+ });
71
+ }
72
+ const response = await prompts({
73
+ type: 'autocomplete',
74
+ name: 'branchId',
75
+ message: 'Select a branch:',
76
+ choices,
77
+ suggest: (input, choices) => Promise.resolve(choices.filter((c) => c.value === 'main' ||
78
+ (c.title?.toLowerCase().includes(input.toLowerCase()) ?? false) ||
79
+ String(c.value).toLowerCase().includes(input.toLowerCase()))),
80
+ }, {
81
+ onCancel: () => {
82
+ process.exit(0);
83
+ },
84
+ });
85
+ if (!response.branchId || response.branchId === 'main') {
86
+ return null;
87
+ }
88
+ return branches.find((b) => b.id === response.branchId) || null;
89
+ }
@@ -0,0 +1,41 @@
1
+ export interface EmbeddableInfo {
2
+ id: string;
3
+ title: string | null;
4
+ }
5
+ export interface EmbeddableMetadata {
6
+ title: string | null;
7
+ archived: boolean;
8
+ created_at: string | null;
9
+ }
10
+ export interface LocalEmbeddable {
11
+ id: string;
12
+ title: string | null;
13
+ }
14
+ export interface PromptForEmbeddableOptions {
15
+ /** Custom message for the prompt */
16
+ message?: string;
17
+ }
18
+ /**
19
+ * Fetch all embeddables for a project from Supabase
20
+ */
21
+ export declare function fetchProjectEmbeddables(projectId: string): Promise<EmbeddableInfo[]>;
22
+ /**
23
+ * Fetch metadata for a single embeddable from the flows table.
24
+ * Returns title, archived, and created_at from the DB (not from the version-specific embeddable.json).
25
+ */
26
+ export declare function fetchEmbeddableMetadata(embeddableId: string): Promise<EmbeddableMetadata | null>;
27
+ /**
28
+ * Prompt the user to select an embeddable from a project
29
+ * Returns null if no embeddables found or user cancels
30
+ */
31
+ export declare function promptForEmbeddable(projectId: string, options?: PromptForEmbeddableOptions): Promise<string | null>;
32
+ /**
33
+ * Discover embeddables in the local embeddables/ directory
34
+ */
35
+ export declare function discoverLocalEmbeddables(): Promise<LocalEmbeddable[]>;
36
+ /**
37
+ * Prompt the user to select a local embeddable
38
+ * Returns null if no embeddables found or user cancels
39
+ */
40
+ export declare function promptForLocalEmbeddable(options?: PromptForEmbeddableOptions): Promise<string | null>;
41
+ //# sourceMappingURL=embeddables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embeddables.d.ts","sourceRoot":"","sources":["../../src/prompts/embeddables.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,MAAM,WAAW,0BAA0B;IACzC,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CA0B1F;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA2BpC;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuCxB;AAED;;GAEG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAqC3E;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuCxB"}
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import pc from 'picocolors';
4
+ import prompts from 'prompts';
5
+ import { getAuthenticatedSupabaseClient } from '../auth/index.js';
6
+ /**
7
+ * Fetch all embeddables for a project from Supabase
8
+ */
9
+ export async function fetchProjectEmbeddables(projectId) {
10
+ const supabase = await getAuthenticatedSupabaseClient();
11
+ if (!supabase) {
12
+ return [];
13
+ }
14
+ try {
15
+ const { data, error } = await supabase
16
+ .from('flows')
17
+ .select('id, title')
18
+ .eq('project_id', projectId)
19
+ .order('title', { ascending: true });
20
+ if (error) {
21
+ console.warn(pc.yellow(`Could not fetch embeddables: ${error.message}`));
22
+ return [];
23
+ }
24
+ return (data || []).map((row) => ({
25
+ id: row.id,
26
+ title: row.title || null,
27
+ }));
28
+ }
29
+ catch (err) {
30
+ console.warn(pc.yellow(`Could not fetch embeddables: ${err}`));
31
+ return [];
32
+ }
33
+ }
34
+ /**
35
+ * Fetch metadata for a single embeddable from the flows table.
36
+ * Returns title, archived, and created_at from the DB (not from the version-specific embeddable.json).
37
+ */
38
+ export async function fetchEmbeddableMetadata(embeddableId) {
39
+ const supabase = await getAuthenticatedSupabaseClient();
40
+ if (!supabase) {
41
+ return null;
42
+ }
43
+ try {
44
+ const { data, error } = await supabase
45
+ .from('flows')
46
+ .select('title, archived, created_at')
47
+ .eq('id', embeddableId)
48
+ .single();
49
+ if (error) {
50
+ console.warn(pc.yellow(`Could not fetch embeddable metadata: ${error.message}`));
51
+ return null;
52
+ }
53
+ return {
54
+ title: data.title || null,
55
+ archived: data.archived ?? false,
56
+ created_at: data.created_at || null,
57
+ };
58
+ }
59
+ catch (err) {
60
+ console.warn(pc.yellow(`Could not fetch embeddable metadata: ${err}`));
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * Prompt the user to select an embeddable from a project
66
+ * Returns null if no embeddables found or user cancels
67
+ */
68
+ export async function promptForEmbeddable(projectId, options = {}) {
69
+ const { message = 'Select an embeddable:' } = options;
70
+ const embeddables = await fetchProjectEmbeddables(projectId);
71
+ if (embeddables.length === 0) {
72
+ console.log(pc.yellow('No embeddables found in this project.'));
73
+ return null;
74
+ }
75
+ const choices = embeddables.map((e) => ({
76
+ title: e.title || e.id,
77
+ description: e.id,
78
+ value: e.id,
79
+ }));
80
+ const response = await prompts({
81
+ type: 'autocomplete',
82
+ name: 'id',
83
+ message,
84
+ choices,
85
+ suggest: (input, choices) => Promise.resolve(choices.filter((c) => (c.title?.toLowerCase().includes(input.toLowerCase()) ?? false) ||
86
+ String(c.value).toLowerCase().includes(input.toLowerCase()))),
87
+ }, {
88
+ onCancel: () => {
89
+ process.exit(0);
90
+ },
91
+ });
92
+ return response.id || null;
93
+ }
94
+ /**
95
+ * Discover embeddables in the local embeddables/ directory
96
+ */
97
+ export async function discoverLocalEmbeddables() {
98
+ const embeddablesDir = 'embeddables';
99
+ if (!fs.existsSync(embeddablesDir)) {
100
+ return [];
101
+ }
102
+ const entries = fs.readdirSync(embeddablesDir, { withFileTypes: true });
103
+ const embeddables = [];
104
+ for (const entry of entries) {
105
+ if (!entry.isDirectory())
106
+ continue;
107
+ const id = entry.name;
108
+ const metadataPath = path.join(embeddablesDir, id, 'metadata.json');
109
+ let title = null;
110
+ if (fs.existsSync(metadataPath)) {
111
+ try {
112
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
113
+ title = metadata.title || null;
114
+ }
115
+ catch {
116
+ // Ignore JSON parse errors
117
+ }
118
+ }
119
+ embeddables.push({ id, title });
120
+ }
121
+ // Sort by title first, then by id
122
+ return embeddables.sort((a, b) => {
123
+ if (a.title && !b.title)
124
+ return -1;
125
+ if (!a.title && b.title)
126
+ return 1;
127
+ const aLabel = a.title || a.id;
128
+ const bLabel = b.title || b.id;
129
+ return aLabel.localeCompare(bLabel);
130
+ });
131
+ }
132
+ /**
133
+ * Prompt the user to select a local embeddable
134
+ * Returns null if no embeddables found or user cancels
135
+ */
136
+ export async function promptForLocalEmbeddable(options = {}) {
137
+ const { message = 'Select an embeddable:' } = options;
138
+ const embeddables = await discoverLocalEmbeddables();
139
+ if (embeddables.length === 0) {
140
+ console.error(pc.red('No embeddables found in the embeddables/ directory.'));
141
+ console.log(pc.gray('Run `embeddables pull` to pull an embeddable first.'));
142
+ return null;
143
+ }
144
+ const choices = embeddables.map((e) => ({
145
+ title: e.title ? `${e.title} (${e.id})` : e.id,
146
+ value: e.id,
147
+ }));
148
+ const response = await prompts({
149
+ type: 'autocomplete',
150
+ name: 'id',
151
+ message,
152
+ choices,
153
+ suggest: (input, choices) => Promise.resolve(choices.filter((c) => (c.title?.toLowerCase().includes(input.toLowerCase()) ?? false) ||
154
+ String(c.value).toLowerCase().includes(input.toLowerCase()))),
155
+ }, {
156
+ onCancel: () => {
157
+ process.exit(0);
158
+ },
159
+ });
160
+ return response.id || null;
161
+ }
@@ -0,0 +1,7 @@
1
+ export { fetchProjects, promptForProject } from './projects.js';
2
+ export type { ProjectInfo, PromptForProjectOptions } from './projects.js';
3
+ export { fetchProjectEmbeddables, fetchEmbeddableMetadata, promptForEmbeddable, discoverLocalEmbeddables, promptForLocalEmbeddable, } from './embeddables.js';
4
+ export type { EmbeddableInfo, EmbeddableMetadata, LocalEmbeddable, PromptForEmbeddableOptions, } from './embeddables.js';
5
+ export { fetchBranches, promptForBranch } from './branches.js';
6
+ export type { BranchInfo } from './branches.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,4 @@
1
+ // Centralized prompts for CLI commands
2
+ export { fetchProjects, promptForProject } from './projects.js';
3
+ export { fetchProjectEmbeddables, fetchEmbeddableMetadata, promptForEmbeddable, discoverLocalEmbeddables, promptForLocalEmbeddable, } from './embeddables.js';
4
+ export { fetchBranches, promptForBranch } from './branches.js';
@@ -0,0 +1,21 @@
1
+ export interface ProjectInfo {
2
+ id: string;
3
+ title: string | null;
4
+ org_title: string | null;
5
+ }
6
+ export interface PromptForProjectOptions {
7
+ /** Include a "Skip for now" option (default: false) */
8
+ allowSkip?: boolean;
9
+ /** Custom message for the prompt (default: "Select a project:") */
10
+ message?: string;
11
+ }
12
+ /**
13
+ * Fetch all non-archived projects from Supabase
14
+ */
15
+ export declare function fetchProjects(): Promise<ProjectInfo[]>;
16
+ /**
17
+ * Prompt the user to select a project from the list
18
+ * Returns null if no projects found, user cancels, or user selects "skip"
19
+ */
20
+ export declare function promptForProject(options?: PromptForProjectOptions): Promise<ProjectInfo | null>;
21
+ //# sourceMappingURL=projects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/prompts/projects.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,uDAAuD;IACvD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAuC5D;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAqD7B"}
@@ -0,0 +1,84 @@
1
+ import pc from 'picocolors';
2
+ import prompts from 'prompts';
3
+ import { getAuthenticatedSupabaseClient } from '../auth/index.js';
4
+ /**
5
+ * Fetch all non-archived projects from Supabase
6
+ */
7
+ export async function fetchProjects() {
8
+ const supabase = await getAuthenticatedSupabaseClient();
9
+ if (!supabase) {
10
+ return [];
11
+ }
12
+ try {
13
+ const { data, error } = await supabase
14
+ .from('projects')
15
+ .select(`
16
+ id,
17
+ title,
18
+ group_id,
19
+ groups (
20
+ title
21
+ )
22
+ `)
23
+ .not('archived', 'is', 'true')
24
+ .order('title', { ascending: true });
25
+ if (error) {
26
+ console.warn(pc.yellow(`Could not fetch projects: ${error.message}`));
27
+ return [];
28
+ }
29
+ return (data || []).map((row) => ({
30
+ id: row.id,
31
+ title: row.title || null,
32
+ org_title:
33
+ // For some reason, I can't get Supabase typings to return this as an object
34
+ // (not an array of objects) so I have to convert it manually
35
+ row.groups?.title || null,
36
+ }));
37
+ }
38
+ catch (err) {
39
+ console.warn(pc.yellow(`Could not fetch projects: ${err}`));
40
+ return [];
41
+ }
42
+ }
43
+ /**
44
+ * Prompt the user to select a project from the list
45
+ * Returns null if no projects found, user cancels, or user selects "skip"
46
+ */
47
+ export async function promptForProject(options = {}) {
48
+ const { allowSkip = false, message = 'Select a project:' } = options;
49
+ const projects = await fetchProjects();
50
+ if (projects.length === 0) {
51
+ console.log(pc.yellow('No projects found.'));
52
+ return null;
53
+ }
54
+ const choices = projects.map((p) => ({
55
+ title: p.title || p.id,
56
+ description: p.org_title ? `${p.org_title} (${p.id})` : p.id,
57
+ value: p.id,
58
+ }));
59
+ if (allowSkip) {
60
+ choices.push({
61
+ title: pc.dim('Skip for now'),
62
+ description: 'You can set this later',
63
+ value: '__skip__',
64
+ });
65
+ }
66
+ const response = await prompts({
67
+ type: 'autocomplete',
68
+ name: 'id',
69
+ message,
70
+ choices,
71
+ suggest: (input, choices) => Promise.resolve(choices.filter((c) => c.value === '__skip__' ||
72
+ (c.title?.toLowerCase().includes(input.toLowerCase()) ?? false) ||
73
+ String(c.value).toLowerCase().includes(input.toLowerCase()) ||
74
+ c.description?.toLowerCase().includes(input.toLowerCase()))),
75
+ }, {
76
+ onCancel: () => {
77
+ process.exit(0);
78
+ },
79
+ });
80
+ if (!response.id || response.id === '__skip__') {
81
+ return null;
82
+ }
83
+ return projects.find((p) => p.id === response.id) || null;
84
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@embeddables/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "embeddables": "./bin/embeddables.mjs"
@@ -47,19 +47,22 @@
47
47
  "@babel/parser": "^7.26.0",
48
48
  "@babel/traverse": "^7.26.0",
49
49
  "@supabase/supabase-js": "^2.39.0",
50
+ "@tailwindcss/postcss": "^4.1.18",
51
+ "autoprefixer": "^10.4.23",
50
52
  "chokidar": "^3.6.0",
51
53
  "commander": "^12.1.0",
52
54
  "cssjson": "^2.1.3",
55
+ "esbuild": "^0.25.0",
53
56
  "express": "^4.19.2",
54
57
  "fast-glob": "^3.3.2",
55
58
  "http-proxy-middleware": "^3.0.3",
56
59
  "picocolors": "^1.1.0",
60
+ "postcss": "^8.5.6",
57
61
  "prompts": "^2.4.2",
58
62
  "react": "^19.2.3",
59
63
  "react-dom": "^19.2.3"
60
64
  },
61
65
  "devDependencies": {
62
- "@tailwindcss/postcss": "^4.1.18",
63
66
  "@types/babel__generator": "^7.27.0",
64
67
  "@types/babel__traverse": "^7.28.0",
65
68
  "@types/express": "^4.17.21",
@@ -67,9 +70,6 @@
67
70
  "@types/prompts": "^2.4.9",
68
71
  "@types/react": "^19.2.8",
69
72
  "@vitest/coverage-v8": "^2.1.8",
70
- "autoprefixer": "^10.4.23",
71
- "esbuild": "^0.25.0",
72
- "postcss": "^8.5.6",
73
73
  "prettier": "^3.8.1",
74
74
  "tailwindcss": "^4.1.18",
75
75
  "tsx": "^4.19.2",