@astralkit/cli 2.0.1 → 2.1.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
@@ -1,6 +1,6 @@
1
1
  # @astralkit/cli
2
2
 
3
- Install and manage AstralKit UI components in your project.
3
+ Install and manage AstralKit UI components, boosters, and workflows in your project.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -11,10 +11,26 @@ npx @astralkit/cli init
11
11
  # Add a component
12
12
  npx @astralkit/cli add button-sizes
13
13
 
14
+ # Add a booster (feature module)
15
+ npx @astralkit/cli add stripe-payments --booster
16
+
17
+ # Scaffold a workflow (complete app)
18
+ npx @astralkit/cli add saas-starter --workflow
19
+
14
20
  # Search for components
15
21
  npx @astralkit/cli search "pricing table"
16
22
  ```
17
23
 
24
+ ## Content Types
25
+
26
+ AstralKit distributes three types of content:
27
+
28
+ | Type | Description | Example |
29
+ |------|-------------|---------|
30
+ | **Components** | Single UI elements with framework variants | Button, Card, Data Table |
31
+ | **Boosters** | Drop-in feature modules (core logic + framework adapters) | Stripe Payments, Auth System |
32
+ | **Workflows** | Complete application scaffolds | SaaS Starter, CRM, Admin Dashboard |
33
+
18
34
  ## Commands
19
35
 
20
36
  ### `astralkit init`
@@ -27,9 +43,11 @@ astralkit init --framework=react
27
43
  astralkit init --dir=src/components/ui
28
44
  ```
29
45
 
30
- ### `astralkit add <component>`
46
+ ### `astralkit add <name>`
47
+
48
+ Add a component, booster, or workflow to your project.
31
49
 
32
- Add a component to your project. Fetches code from AstralKit and writes it to your components directory.
50
+ #### Adding Components
33
51
 
34
52
  ```bash
35
53
  astralkit add avatar-variants
@@ -37,17 +55,51 @@ astralkit add pricing-table --framework=react
37
55
  astralkit add data-table --overwrite
38
56
  ```
39
57
 
40
- Options:
58
+ #### Adding Boosters
59
+
60
+ Boosters install framework-agnostic core files (API routes, webhooks, utilities) and framework-specific adapter files (UI components, hooks).
61
+
62
+ ```bash
63
+ astralkit add stripe-payments --booster
64
+ astralkit add auth-system --booster --framework=nextjs
65
+ astralkit add stripe-payments --booster --overwrite
66
+ ```
67
+
68
+ Core files are written to `src/lib/<booster-slug>/` and adapter files to `src/components/<booster-slug>/` by default. Boosters may specify custom install paths.
69
+
70
+ #### Adding Workflows
71
+
72
+ Workflows scaffold a complete application. By default, files are created in a new `./<slug>/` directory.
73
+
74
+ ```bash
75
+ astralkit add saas-starter --workflow
76
+ astralkit add crm --workflow --framework=nextjs
77
+ ```
78
+
79
+ Use `--overlay` to merge workflow files into your current project instead of creating a new directory:
80
+
81
+ ```bash
82
+ astralkit add saas-starter --workflow --overlay
83
+ astralkit add saas-starter --workflow --overlay --overwrite
84
+ ```
85
+
86
+ #### Options
87
+
41
88
  - `-f, --framework` — Framework version (html, react, vue, nextjs, angular, svelte, astro)
42
89
  - `--dir` — Override output directory
43
90
  - `--overwrite` — Overwrite existing files
91
+ - `--booster` — Install a booster (feature module)
92
+ - `--workflow` — Scaffold a workflow (complete app template)
93
+ - `--overlay` — Merge workflow into current project (workflow only)
44
94
 
45
95
  ### `astralkit list`
46
96
 
47
- List all available components.
97
+ List available content.
48
98
 
49
99
  ```bash
50
- astralkit list
100
+ astralkit list # List components
101
+ astralkit list --boosters # List boosters
102
+ astralkit list --workflows # List workflows
51
103
  astralkit list --category=forms
52
104
  astralkit list --framework=react
53
105
  astralkit list --pro
@@ -56,11 +108,13 @@ astralkit list --free
56
108
 
57
109
  ### `astralkit search <query>`
58
110
 
59
- Search for components by name or description.
111
+ Search by name or description.
60
112
 
61
113
  ```bash
62
114
  astralkit search "data table"
63
115
  astralkit search "pricing" --framework=react
116
+ astralkit search "stripe" --boosters
117
+ astralkit search "crm" --workflows
64
118
  ```
65
119
 
66
120
  ### `astralkit diff <component>`
@@ -82,7 +136,7 @@ astralkit update button-sizes --force
82
136
 
83
137
  ### `astralkit login`
84
138
 
85
- Authenticate with AstralKit to access premium components. Opens your browser for secure login.
139
+ Authenticate with AstralKit to access premium content. Opens your browser for secure login.
86
140
 
87
141
  ```bash
88
142
  astralkit login
@@ -96,9 +150,9 @@ Remove stored authentication.
96
150
 
97
151
  Show current authentication status and subscription plan. Verifies your token with the server.
98
152
 
99
- ## Premium Components
153
+ ## Premium Content
100
154
 
101
- Some components require a Pro subscription. When you try to add a pro component without authentication:
155
+ Some components, boosters, and workflows require a Pro subscription. When you try to add pro content without authentication:
102
156
 
103
157
  ```bash
104
158
  $ astralkit add pro-data-table
@@ -106,7 +160,7 @@ $ astralkit add pro-data-table
106
160
  Authentication required. Run `astralkit login` to authenticate.
107
161
  ```
108
162
 
109
- After logging in, pro components install normally:
163
+ After logging in, pro content installs normally:
110
164
 
111
165
  ```bash
112
166
  $ astralkit login
@@ -114,8 +168,22 @@ $ astralkit login
114
168
  Email: you@example.com
115
169
  Plan: pro
116
170
 
117
- $ astralkit add pro-data-table
118
- ✔ Added Pro Data Table (react)
171
+ $ astralkit add stripe-payments --booster
172
+ ✔ Added booster Stripe Payments (nextjs)
173
+ Core files → src/lib/stripe-payments/
174
+ lib/stripe.ts
175
+ api/webhooks/stripe/route.ts
176
+ Adapter files → src/components/stripe-payments/
177
+ components/checkout-button.tsx
178
+ hooks/use-subscription.ts
179
+
180
+ Install required dependencies:
181
+ npm install stripe @stripe/stripe-js
182
+
183
+ Add these environment variables to your .env:
184
+ STRIPE_SECRET_KEY=
185
+ STRIPE_PUBLISHABLE_KEY=
186
+ STRIPE_WEBHOOK_SECRET=
119
187
  ```
120
188
 
121
189
  ## Configuration
@@ -147,6 +215,7 @@ export ASTRALKIT_TOKEN="your-api-token"
147
215
 
148
216
  # Commands automatically use the env var
149
217
  astralkit add button-sizes
218
+ astralkit add stripe-payments --booster
150
219
  astralkit whoami
151
220
  ```
152
221
 
@@ -189,7 +258,7 @@ If you see timeout or connection errors, check your internet connection. The CLI
189
258
  ## Requirements
190
259
 
191
260
  - Node.js 18+
192
- - An AstralKit account for premium components
261
+ - An AstralKit account for premium content
193
262
 
194
263
  ## Links
195
264
 
package/bin/index.js CHANGED
@@ -18,7 +18,7 @@ const program = new Command();
18
18
 
19
19
  program
20
20
  .name('astralkit')
21
- .description('AstralKit CLI — Install and manage UI components')
21
+ .description('AstralKit CLI — Install and manage UI components, boosters, and workflows')
22
22
  .version(pkg.version);
23
23
 
24
24
  // ─── init ────────────────────────────────────────────
@@ -31,11 +31,14 @@ program
31
31
 
32
32
  // ─── add ─────────────────────────────────────────────
33
33
  program
34
- .command('add <component>')
35
- .description('Add a component to your project')
34
+ .command('add <name>')
35
+ .description('Add a component, booster, or workflow to your project')
36
36
  .option('-f, --framework <framework>', 'Framework version (html, react, vue, nextjs)')
37
37
  .option('--dir <directory>', 'Override output directory')
38
38
  .option('--overwrite', 'Overwrite existing files', false)
39
+ .option('--booster', 'Install a booster (feature module)')
40
+ .option('--workflow', 'Scaffold a workflow (complete app template)')
41
+ .option('--overlay', 'Merge workflow into current project (workflow only)')
39
42
  .action(addCommand);
40
43
 
41
44
  // ─── login ───────────────────────────────────────────
@@ -57,18 +60,22 @@ program
57
60
  // ─── list ────────────────────────────────────────────
58
61
  program
59
62
  .command('list')
60
- .description('List available components')
63
+ .description('List available components, boosters, or workflows')
61
64
  .option('-c, --category <category>', 'Filter by category')
62
65
  .option('--framework <framework>', 'Filter by supported framework')
63
- .option('--pro', 'Show only pro components', false)
64
- .option('--free', 'Show only free components', false)
66
+ .option('--pro', 'Show only pro items', false)
67
+ .option('--free', 'Show only free items', false)
68
+ .option('--boosters', 'List boosters instead of components')
69
+ .option('--workflows', 'List workflows instead of components')
65
70
  .action(listCommand);
66
71
 
67
72
  // ─── search ──────────────────────────────────────────
68
73
  program
69
74
  .command('search <query>')
70
- .description('Search for components')
75
+ .description('Search for components, boosters, or workflows')
71
76
  .option('--framework <framework>', 'Filter by supported framework')
77
+ .option('--boosters', 'Search boosters only')
78
+ .option('--workflows', 'Search workflows only')
72
79
  .action(searchCommand);
73
80
 
74
81
  // ─── diff ────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astralkit/cli",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "AstralKit CLI — Install and manage UI components in your project",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -2,22 +2,35 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const chalk = require('chalk');
4
4
  const ora = require('ora');
5
- const { getComponent } = require('../lib/api');
5
+ const { getComponent, getBooster, getWorkflow } = require('../lib/api');
6
6
  const { getProjectConfig, detectFramework } = require('../lib/config');
7
7
  const { validateSlug, validateFramework, isSafePath } = require('../lib/validators');
8
8
 
9
- async function addCommand(componentSlug, options) {
10
- // Validate inputs
9
+ /**
10
+ * Route to the correct add handler based on flags.
11
+ */
12
+ async function addCommand(nameOrSlug, options) {
13
+ if (options.booster) {
14
+ return addBooster(nameOrSlug, options);
15
+ }
16
+ if (options.workflow) {
17
+ return addWorkflow(nameOrSlug, options);
18
+ }
19
+ return addComponent(nameOrSlug, options);
20
+ }
21
+
22
+ /**
23
+ * Add a single UI component (existing behavior).
24
+ */
25
+ async function addComponent(componentSlug, options) {
11
26
  if (!validateSlug(componentSlug)) process.exit(1);
12
- if (!validateFramework(options.framework)) process.exit(1);
27
+ if (options.framework && !validateFramework(options.framework)) process.exit(1);
13
28
 
14
- // Resolve framework
15
29
  const config = getProjectConfig();
16
30
  const framework = options.framework || config?.framework || detectFramework();
17
31
  const componentsDir = options.dir || config?.componentsDir || 'src/components/ui';
18
32
  const targetDir = path.resolve(process.cwd(), componentsDir, componentSlug);
19
33
 
20
- // Check for existing files
21
34
  if (fs.existsSync(targetDir) && !options.overwrite) {
22
35
  console.log(chalk.yellow(`Component already exists at ${componentsDir}/${componentSlug}/`));
23
36
  console.log(chalk.gray('Use --overwrite to replace existing files.'));
@@ -37,10 +50,8 @@ async function addCommand(componentSlug, options) {
37
50
  return;
38
51
  }
39
52
 
40
- // Create component directory
41
53
  fs.mkdirSync(targetDir, { recursive: true });
42
54
 
43
- // Write each file (with path traversal protection)
44
55
  for (const file of data.files) {
45
56
  if (!isSafePath(targetDir, file.path)) {
46
57
  console.log(chalk.red(` Skipping suspicious file path: ${file.path}`));
@@ -56,20 +67,17 @@ async function addCommand(componentSlug, options) {
56
67
 
57
68
  spinner.succeed(`Added ${chalk.cyan(data.name || componentSlug)} (${framework})`);
58
69
 
59
- // Show files written
60
70
  console.log(chalk.gray(` ${componentsDir}/${componentSlug}/`));
61
71
  for (const file of data.files) {
62
72
  console.log(chalk.gray(` ${file.path}`));
63
73
  }
64
74
 
65
- // Show dependencies to install
66
75
  if (data.dependencies?.length) {
67
76
  console.log();
68
77
  console.log(chalk.yellow('Install required dependencies:'));
69
78
  console.log(chalk.cyan(` npm install ${data.dependencies.join(' ')}`));
70
79
  }
71
80
 
72
- // Pro badge
73
81
  if (data.is_pro) {
74
82
  console.log();
75
83
  console.log(chalk.magenta('Pro component — thank you for your subscription!'));
@@ -77,28 +85,289 @@ async function addCommand(componentSlug, options) {
77
85
 
78
86
  } catch (err) {
79
87
  spinner.fail('Failed to add component.');
88
+ handleAddError(err, componentSlug, framework);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Add a booster (multi-file feature module with core + adapter).
94
+ */
95
+ async function addBooster(slug, options) {
96
+ if (!validateSlug(slug)) process.exit(1);
97
+ if (options.framework && !validateFramework(options.framework)) process.exit(1);
98
+
99
+ const config = getProjectConfig();
100
+ const framework = options.framework || config?.framework || detectFramework();
101
+
102
+ const spinner = ora(`Fetching booster ${slug} (${framework})...`).start();
103
+
104
+ try {
105
+ const data = await getBooster(slug, framework);
106
+
107
+ const coreFiles = data.core_files || [];
108
+ const adapterFiles = data.adapter_files || [];
109
+
110
+ if (coreFiles.length === 0 && adapterFiles.length === 0) {
111
+ spinner.fail(`No files available for booster "${slug}" (${framework}).`);
112
+ if (data.frameworks?.length) {
113
+ console.log(chalk.gray(`Available frameworks: ${data.frameworks.join(', ')}`));
114
+ }
115
+ return;
116
+ }
117
+
118
+ // Resolve install paths
119
+ const corePath = options.dir || data.install_paths?.core || `src/lib/${slug}`;
120
+ const adapterPath = data.install_paths?.adapter || `src/components/${slug}`;
121
+ const coreDir = path.resolve(process.cwd(), corePath);
122
+ const adapterDir = path.resolve(process.cwd(), adapterPath);
123
+
124
+ // Check for existing files
125
+ if (!options.overwrite) {
126
+ const conflicts = [];
127
+ for (const file of coreFiles) {
128
+ const fp = path.resolve(coreDir, file.path);
129
+ if (fs.existsSync(fp)) conflicts.push(`${corePath}/${file.path}`);
130
+ }
131
+ for (const file of adapterFiles) {
132
+ const fp = path.resolve(adapterDir, file.path);
133
+ if (fs.existsSync(fp)) conflicts.push(`${adapterPath}/${file.path}`);
134
+ }
135
+ if (conflicts.length > 0) {
136
+ spinner.fail('Some files already exist.');
137
+ console.log(chalk.yellow('Conflicting files:'));
138
+ for (const c of conflicts) {
139
+ console.log(chalk.gray(` ${c}`));
140
+ }
141
+ console.log(chalk.gray('Use --overwrite to replace existing files.'));
142
+ return;
143
+ }
144
+ }
145
+
146
+ // Write core files
147
+ let coreCount = 0;
148
+ for (const file of coreFiles) {
149
+ if (!isSafePath(coreDir, file.path)) {
150
+ console.log(chalk.red(` Skipping suspicious file path: ${file.path}`));
151
+ continue;
152
+ }
153
+ const filePath = path.resolve(coreDir, file.path);
154
+ const fileDir = path.dirname(filePath);
155
+ if (!fs.existsSync(fileDir)) {
156
+ fs.mkdirSync(fileDir, { recursive: true });
157
+ }
158
+ fs.writeFileSync(filePath, file.content);
159
+ coreCount++;
160
+ }
161
+
162
+ // Write adapter files
163
+ let adapterCount = 0;
164
+ for (const file of adapterFiles) {
165
+ if (!isSafePath(adapterDir, file.path)) {
166
+ console.log(chalk.red(` Skipping suspicious file path: ${file.path}`));
167
+ continue;
168
+ }
169
+ const filePath = path.resolve(adapterDir, file.path);
170
+ const fileDir = path.dirname(filePath);
171
+ if (!fs.existsSync(fileDir)) {
172
+ fs.mkdirSync(fileDir, { recursive: true });
173
+ }
174
+ fs.writeFileSync(filePath, file.content);
175
+ adapterCount++;
176
+ }
177
+
178
+ spinner.succeed(`Added booster ${chalk.cyan(data.name || slug)} (${framework})`);
179
+
180
+ // Show files written
181
+ if (coreCount > 0) {
182
+ console.log(chalk.gray(` Core files → ${corePath}/`));
183
+ for (const file of coreFiles) {
184
+ console.log(chalk.gray(` ${file.path}`));
185
+ }
186
+ }
187
+ if (adapterCount > 0) {
188
+ console.log(chalk.gray(` Adapter files → ${adapterPath}/`));
189
+ for (const file of adapterFiles) {
190
+ console.log(chalk.gray(` ${file.path}`));
191
+ }
192
+ }
193
+
194
+ // Dependencies
195
+ if (data.dependencies?.npm?.length) {
196
+ console.log();
197
+ console.log(chalk.yellow('Install required dependencies:'));
198
+ console.log(chalk.cyan(` npm install ${data.dependencies.npm.join(' ')}`));
199
+ }
200
+
201
+ // Environment variables
202
+ if (data.dependencies?.env?.length) {
203
+ console.log();
204
+ console.log(chalk.yellow('Add these environment variables to your .env:'));
205
+ for (const envVar of data.dependencies.env) {
206
+ console.log(chalk.gray(` ${envVar}=`));
207
+ }
208
+ }
209
+
210
+ // Post-install instructions
211
+ if (data.post_install) {
212
+ console.log();
213
+ console.log(chalk.white.bold('Setup instructions:'));
214
+ console.log(chalk.gray(data.post_install));
215
+ }
80
216
 
81
- if (err.code === 'AUTH_REQUIRED') {
82
- console.log(chalk.yellow(err.message));
83
- } else if (err.code === 'PRO_REQUIRED') {
84
- console.log(chalk.yellow(err.message));
217
+ if (data.is_pro) {
218
+ console.log();
219
+ console.log(chalk.magenta('Pro booster thank you for your subscription!'));
220
+ }
221
+
222
+ } catch (err) {
223
+ spinner.fail('Failed to add booster.');
224
+ handleAddError(err, slug, framework);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Add a workflow (complete application scaffold).
230
+ */
231
+ async function addWorkflow(slug, options) {
232
+ if (!validateSlug(slug)) process.exit(1);
233
+ if (options.framework && !validateFramework(options.framework)) process.exit(1);
234
+
235
+ const config = getProjectConfig();
236
+ const framework = options.framework || config?.framework || detectFramework();
237
+
238
+ const spinner = ora(`Fetching workflow ${slug} (${framework})...`).start();
239
+
240
+ try {
241
+ const data = await getWorkflow(slug, framework);
242
+
243
+ if (!data.files || data.files.length === 0) {
244
+ spinner.fail(`No files available for workflow "${slug}" (${framework}).`);
245
+ if (data.frameworks?.length) {
246
+ console.log(chalk.gray(`Available frameworks: ${data.frameworks.join(', ')}`));
247
+ }
248
+ return;
249
+ }
250
+
251
+ // Determine target directory
252
+ let targetDir;
253
+ if (options.overlay) {
254
+ // Overlay mode: write into current project
255
+ targetDir = path.resolve(process.cwd());
85
256
  } else {
86
- // Try to parse API error for framework suggestions
87
- try {
88
- const body = JSON.parse(err.message.replace(/^API error \d+: /, ''));
89
- if (body.supported_frameworks) {
90
- console.log(chalk.yellow(`No ${framework} code available for "${componentSlug}".`));
91
- console.log(chalk.gray(`Available frameworks: ${body.supported_frameworks.join(', ')}`));
92
- console.log(chalk.gray(`Try: astralkit add ${componentSlug} --framework=${body.supported_frameworks[0]}`));
93
- process.exit(1);
257
+ // Default: create a new subdirectory
258
+ const dirName = options.dir || slug;
259
+ targetDir = path.resolve(process.cwd(), dirName);
260
+ }
261
+
262
+ // Check for conflicts in overlay mode
263
+ if (options.overlay && !options.overwrite) {
264
+ const conflicts = [];
265
+ for (const file of data.files) {
266
+ const fp = path.resolve(targetDir, file.path);
267
+ if (fs.existsSync(fp)) conflicts.push(file.path);
268
+ }
269
+ if (conflicts.length > 0) {
270
+ spinner.fail(`${conflicts.length} file(s) would be overwritten.`);
271
+ console.log(chalk.yellow('Conflicting files:'));
272
+ for (const c of conflicts.slice(0, 20)) {
273
+ console.log(chalk.gray(` ${c}`));
274
+ }
275
+ if (conflicts.length > 20) {
276
+ console.log(chalk.gray(` ... and ${conflicts.length - 20} more`));
94
277
  }
95
- } catch {
96
- // Not a JSON parse error, show raw message
278
+ console.log(chalk.gray('Use --overwrite to replace existing files.'));
279
+ return;
280
+ }
281
+ }
282
+
283
+ // Check if target directory exists (non-overlay)
284
+ if (!options.overlay && fs.existsSync(targetDir) && !options.overwrite) {
285
+ spinner.fail(`Directory "${slug}/" already exists.`);
286
+ console.log(chalk.gray('Use --overwrite to replace, or --dir to specify a different name.'));
287
+ return;
288
+ }
289
+
290
+ // Write files
291
+ let fileCount = 0;
292
+ for (const file of data.files) {
293
+ if (!isSafePath(targetDir, file.path)) {
294
+ console.log(chalk.red(` Skipping suspicious file path: ${file.path}`));
295
+ continue;
296
+ }
297
+ const filePath = path.resolve(targetDir, file.path);
298
+ const fileDir = path.dirname(filePath);
299
+ if (!fs.existsSync(fileDir)) {
300
+ fs.mkdirSync(fileDir, { recursive: true });
301
+ }
302
+ fs.writeFileSync(filePath, file.content);
303
+ fileCount++;
304
+ }
305
+
306
+ const targetLabel = options.overlay ? './' : `${slug}/`;
307
+ spinner.succeed(`Scaffolded workflow ${chalk.cyan(data.name || slug)} (${framework}) → ${targetLabel}`);
308
+
309
+ console.log(chalk.gray(` ${fileCount} files written to ${targetLabel}`));
310
+
311
+ // Dependencies
312
+ if (data.dependencies?.npm?.length) {
313
+ console.log();
314
+ console.log(chalk.yellow('Install dependencies:'));
315
+ if (options.overlay) {
316
+ console.log(chalk.cyan(` npm install ${data.dependencies.npm.join(' ')}`));
317
+ } else {
318
+ console.log(chalk.cyan(` cd ${slug} && npm install`));
319
+ }
320
+ }
321
+
322
+ // Environment variables
323
+ if (data.dependencies?.env?.length) {
324
+ console.log();
325
+ console.log(chalk.yellow('Add these environment variables to your .env:'));
326
+ for (const envVar of data.dependencies.env) {
327
+ console.log(chalk.gray(` ${envVar}=`));
328
+ }
329
+ }
330
+
331
+ // Post-install instructions
332
+ if (data.post_install) {
333
+ console.log();
334
+ console.log(chalk.white.bold('Setup instructions:'));
335
+ console.log(chalk.gray(data.post_install));
336
+ }
337
+
338
+ if (data.is_pro) {
339
+ console.log();
340
+ console.log(chalk.magenta('Pro workflow — thank you for your subscription!'));
341
+ }
342
+
343
+ } catch (err) {
344
+ spinner.fail('Failed to add workflow.');
345
+ handleAddError(err, slug, framework);
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Shared error handler for add commands.
351
+ */
352
+ function handleAddError(err, slug, framework) {
353
+ if (err.code === 'AUTH_REQUIRED') {
354
+ console.log(chalk.yellow(err.message));
355
+ } else if (err.code === 'PRO_REQUIRED') {
356
+ console.log(chalk.yellow(err.message));
357
+ } else {
358
+ try {
359
+ const body = JSON.parse(err.message.replace(/^API error \d+: /, ''));
360
+ if (body.supported_frameworks) {
361
+ console.log(chalk.yellow(`No ${framework} version available for "${slug}".`));
362
+ console.log(chalk.gray(`Available frameworks: ${body.supported_frameworks.join(', ')}`));
363
+ process.exit(1);
97
364
  }
98
- console.error(chalk.red(err.message));
365
+ } catch {
366
+ // Not a JSON parse error
99
367
  }
100
- process.exit(1);
368
+ console.error(chalk.red(err.message));
101
369
  }
370
+ process.exit(1);
102
371
  }
103
372
 
104
373
  module.exports = { addCommand };
@@ -1,14 +1,29 @@
1
1
  const chalk = require('chalk');
2
2
  const ora = require('ora');
3
- const { listComponents } = require('../lib/api');
3
+ const { listComponents, listBoosters, listWorkflows } = require('../lib/api');
4
4
  const { validateFramework } = require('../lib/validators');
5
5
 
6
+ /**
7
+ * Route to the correct list handler based on flags.
8
+ */
6
9
  async function listCommand(options) {
7
- // Validate --framework if provided
8
10
  if (options.framework && !validateFramework(options.framework)) {
9
11
  process.exit(1);
10
12
  }
11
13
 
14
+ if (options.boosters) {
15
+ return listBoostersCommand(options);
16
+ }
17
+ if (options.workflows) {
18
+ return listWorkflowsCommand(options);
19
+ }
20
+ return listComponentsCommand(options);
21
+ }
22
+
23
+ /**
24
+ * List UI components (existing behavior).
25
+ */
26
+ async function listComponentsCommand(options) {
12
27
  const spinner = ora('Fetching components...').start();
13
28
 
14
29
  try {
@@ -26,7 +41,6 @@ async function listCommand(options) {
26
41
  return;
27
42
  }
28
43
 
29
- // Group by category
30
44
  const grouped = {};
31
45
  for (const comp of data.components) {
32
46
  const cat = comp.category || 'Uncategorized';
@@ -61,4 +75,112 @@ async function listCommand(options) {
61
75
  }
62
76
  }
63
77
 
78
+ /**
79
+ * List available boosters.
80
+ */
81
+ async function listBoostersCommand(options) {
82
+ const spinner = ora('Fetching boosters...').start();
83
+
84
+ try {
85
+ const data = await listBoosters({
86
+ framework: options.framework,
87
+ pro: options.pro,
88
+ free: options.free,
89
+ });
90
+
91
+ spinner.stop();
92
+
93
+ if (!data.boosters || data.boosters.length === 0) {
94
+ console.log(chalk.yellow('No boosters found.'));
95
+ return;
96
+ }
97
+
98
+ const grouped = {};
99
+ for (const booster of data.boosters) {
100
+ const cat = booster.category || 'Uncategorized';
101
+ if (!grouped[cat]) grouped[cat] = [];
102
+ grouped[cat].push(booster);
103
+ }
104
+
105
+ console.log(chalk.blue.bold(`AstralKit Boosters (${data.boosters.length} total)`));
106
+ console.log();
107
+
108
+ for (const [category, boosters] of Object.entries(grouped)) {
109
+ console.log(chalk.white.bold(` ${category}`));
110
+ for (const b of boosters) {
111
+ const proTag = b.is_pro ? chalk.magenta(' PRO') : '';
112
+ const frameworks = b.frameworks?.length
113
+ ? chalk.gray(` [${b.frameworks.join(', ')}]`)
114
+ : '';
115
+ console.log(` ${chalk.cyan(b.slug)}${proTag}${frameworks}`);
116
+ if (b.description) {
117
+ console.log(` ${chalk.gray(b.description)}`);
118
+ }
119
+ }
120
+ console.log();
121
+ }
122
+
123
+ console.log(chalk.gray('Run'), chalk.cyan('astralkit add --booster <name>'), chalk.gray('to install.'));
124
+
125
+ } catch (err) {
126
+ spinner.fail('Failed to list boosters.');
127
+ console.error(chalk.red(err.message));
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * List available workflows.
134
+ */
135
+ async function listWorkflowsCommand(options) {
136
+ const spinner = ora('Fetching workflows...').start();
137
+
138
+ try {
139
+ const data = await listWorkflows({
140
+ framework: options.framework,
141
+ pro: options.pro,
142
+ free: options.free,
143
+ });
144
+
145
+ spinner.stop();
146
+
147
+ if (!data.workflows || data.workflows.length === 0) {
148
+ console.log(chalk.yellow('No workflows found.'));
149
+ return;
150
+ }
151
+
152
+ const grouped = {};
153
+ for (const workflow of data.workflows) {
154
+ const cat = workflow.category || 'Uncategorized';
155
+ if (!grouped[cat]) grouped[cat] = [];
156
+ grouped[cat].push(workflow);
157
+ }
158
+
159
+ console.log(chalk.blue.bold(`AstralKit Workflows (${data.workflows.length} total)`));
160
+ console.log();
161
+
162
+ for (const [category, workflows] of Object.entries(grouped)) {
163
+ console.log(chalk.white.bold(` ${category}`));
164
+ for (const w of workflows) {
165
+ const proTag = w.is_pro ? chalk.magenta(' PRO') : '';
166
+ const frameworks = w.frameworks?.length
167
+ ? chalk.gray(` [${w.frameworks.join(', ')}]`)
168
+ : '';
169
+ console.log(` ${chalk.cyan(w.slug)}${proTag}${frameworks}`);
170
+ if (w.description) {
171
+ console.log(` ${chalk.gray(w.description)}`);
172
+ }
173
+ }
174
+ console.log();
175
+ }
176
+
177
+ console.log(chalk.gray('Run'), chalk.cyan('astralkit add --workflow <name>'), chalk.gray('to scaffold.'));
178
+
179
+ } catch (err) {
180
+ spinner.fail('Failed to list workflows.');
181
+ console.error(chalk.red(err.message));
182
+ process.exit(1);
183
+ }
184
+ }
185
+
64
186
  module.exports = { listCommand };
@@ -1,14 +1,29 @@
1
1
  const chalk = require('chalk');
2
2
  const ora = require('ora');
3
- const { listComponents } = require('../lib/api');
3
+ const { listComponents, listBoosters, listWorkflows } = require('../lib/api');
4
4
  const { validateFramework } = require('../lib/validators');
5
5
 
6
+ /**
7
+ * Route to the correct search handler based on flags.
8
+ */
6
9
  async function searchCommand(query, options) {
7
- // Validate --framework if provided
8
10
  if (options.framework && !validateFramework(options.framework)) {
9
11
  process.exit(1);
10
12
  }
11
13
 
14
+ if (options.boosters) {
15
+ return searchBoostersCommand(query, options);
16
+ }
17
+ if (options.workflows) {
18
+ return searchWorkflowsCommand(query, options);
19
+ }
20
+ return searchComponentsCommand(query, options);
21
+ }
22
+
23
+ /**
24
+ * Search UI components (existing behavior).
25
+ */
26
+ async function searchComponentsCommand(query, options) {
12
27
  const spinner = ora(`Searching for "${query}"...`).start();
13
28
 
14
29
  try {
@@ -50,4 +65,92 @@ async function searchCommand(query, options) {
50
65
  }
51
66
  }
52
67
 
68
+ /**
69
+ * Search boosters.
70
+ */
71
+ async function searchBoostersCommand(query, options) {
72
+ const spinner = ora(`Searching boosters for "${query}"...`).start();
73
+
74
+ try {
75
+ const data = await listBoosters({
76
+ search: query,
77
+ framework: options.framework,
78
+ });
79
+
80
+ spinner.stop();
81
+
82
+ if (!data.boosters || data.boosters.length === 0) {
83
+ console.log(chalk.yellow(`No boosters found for "${query}".`));
84
+ return;
85
+ }
86
+
87
+ console.log(chalk.blue.bold(`Found ${data.boosters.length} booster(s):`));
88
+ console.log();
89
+
90
+ for (const b of data.boosters) {
91
+ const proTag = b.is_pro ? chalk.magenta(' PRO') : '';
92
+ const cat = b.category ? chalk.gray(` (${b.category})`) : '';
93
+ console.log(` ${chalk.cyan(b.slug)}${proTag}${cat}`);
94
+ if (b.description) {
95
+ console.log(` ${chalk.gray(b.description.length > 80 ? b.description.slice(0, 80) + '...' : b.description)}`);
96
+ }
97
+ if (b.frameworks?.length) {
98
+ console.log(` ${chalk.gray(`Frameworks: ${b.frameworks.join(', ')}`)}`);
99
+ }
100
+ console.log();
101
+ }
102
+
103
+ console.log(chalk.gray('Run'), chalk.cyan('astralkit add --booster <name>'), chalk.gray('to install.'));
104
+
105
+ } catch (err) {
106
+ spinner.fail('Search failed.');
107
+ console.error(chalk.red(err.message));
108
+ process.exit(1);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Search workflows.
114
+ */
115
+ async function searchWorkflowsCommand(query, options) {
116
+ const spinner = ora(`Searching workflows for "${query}"...`).start();
117
+
118
+ try {
119
+ const data = await listWorkflows({
120
+ search: query,
121
+ framework: options.framework,
122
+ });
123
+
124
+ spinner.stop();
125
+
126
+ if (!data.workflows || data.workflows.length === 0) {
127
+ console.log(chalk.yellow(`No workflows found for "${query}".`));
128
+ return;
129
+ }
130
+
131
+ console.log(chalk.blue.bold(`Found ${data.workflows.length} workflow(s):`));
132
+ console.log();
133
+
134
+ for (const w of data.workflows) {
135
+ const proTag = w.is_pro ? chalk.magenta(' PRO') : '';
136
+ const cat = w.category ? chalk.gray(` (${w.category})`) : '';
137
+ console.log(` ${chalk.cyan(w.slug)}${proTag}${cat}`);
138
+ if (w.description) {
139
+ console.log(` ${chalk.gray(w.description.length > 80 ? w.description.slice(0, 80) + '...' : w.description)}`);
140
+ }
141
+ if (w.frameworks?.length) {
142
+ console.log(` ${chalk.gray(`Frameworks: ${w.frameworks.join(', ')}`)}`);
143
+ }
144
+ console.log();
145
+ }
146
+
147
+ console.log(chalk.gray('Run'), chalk.cyan('astralkit add --workflow <name>'), chalk.gray('to scaffold.'));
148
+
149
+ } catch (err) {
150
+ spinner.fail('Search failed.');
151
+ console.error(chalk.red(err.message));
152
+ process.exit(1);
153
+ }
154
+ }
155
+
53
156
  module.exports = { searchCommand };
package/src/lib/api.js CHANGED
@@ -136,4 +136,56 @@ async function verifyAuth() {
136
136
  return response.json();
137
137
  }
138
138
 
139
- module.exports = { apiRequest, listComponents, getComponent, verifyAuth, API_BASE, CLI_VERSION };
139
+ /**
140
+ * List boosters with optional filters.
141
+ */
142
+ async function listBoosters({ framework, search, pro, free } = {}) {
143
+ const params = new URLSearchParams();
144
+ if (framework) params.set('framework', framework);
145
+ if (pro) params.set('pro', 'true');
146
+ if (free) params.set('free', 'true');
147
+ if (search) params.set('search', search);
148
+
149
+ const response = await apiRequest(`/api/cli/boosters?${params}`);
150
+ return response.json();
151
+ }
152
+
153
+ /**
154
+ * Get a single booster's details + files for installation.
155
+ */
156
+ async function getBooster(slug, framework = 'nextjs') {
157
+ const response = await apiRequest(
158
+ `/api/cli/boosters/${encodeURIComponent(slug)}?framework=${encodeURIComponent(framework)}`
159
+ );
160
+ return response.json();
161
+ }
162
+
163
+ /**
164
+ * List workflows with optional filters.
165
+ */
166
+ async function listWorkflows({ framework, search, pro, free } = {}) {
167
+ const params = new URLSearchParams();
168
+ if (framework) params.set('framework', framework);
169
+ if (pro) params.set('pro', 'true');
170
+ if (free) params.set('free', 'true');
171
+ if (search) params.set('search', search);
172
+
173
+ const response = await apiRequest(`/api/cli/workflows?${params}`);
174
+ return response.json();
175
+ }
176
+
177
+ /**
178
+ * Get a single workflow's details + files for scaffolding.
179
+ */
180
+ async function getWorkflow(slug, framework = 'nextjs') {
181
+ const response = await apiRequest(
182
+ `/api/cli/workflows/${encodeURIComponent(slug)}?framework=${encodeURIComponent(framework)}`
183
+ );
184
+ return response.json();
185
+ }
186
+
187
+ module.exports = {
188
+ apiRequest, listComponents, getComponent, verifyAuth,
189
+ listBoosters, getBooster, listWorkflows, getWorkflow,
190
+ API_BASE, CLI_VERSION,
191
+ };