@astralkit/cli 2.0.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,79 @@
1
+ const chalk = require('chalk');
2
+ const inquirer = require('inquirer');
3
+ const { detectFramework, defaultConfig, writeProjectConfig, getProjectConfig, CONFIG_FILENAME } = require('../lib/config');
4
+ const { validateFramework } = require('../lib/validators');
5
+
6
+ async function initCommand(options) {
7
+ console.log(chalk.blue.bold('AstralKit — Project Setup'));
8
+ console.log();
9
+
10
+ // Validate --framework if provided
11
+ if (options.framework && !validateFramework(options.framework)) {
12
+ process.exit(1);
13
+ }
14
+
15
+ // Check if already initialized
16
+ const existing = getProjectConfig();
17
+ if (existing) {
18
+ console.log(chalk.yellow(`Found existing ${CONFIG_FILENAME} at ${existing._path}`));
19
+ const { overwrite } = await inquirer.prompt([{
20
+ type: 'confirm',
21
+ name: 'overwrite',
22
+ message: 'Overwrite existing config?',
23
+ default: false,
24
+ }]);
25
+ if (!overwrite) {
26
+ console.log(chalk.gray('Aborted.'));
27
+ return;
28
+ }
29
+ }
30
+
31
+ // Detect or ask for framework
32
+ let framework = options.framework;
33
+ if (!framework) {
34
+ const detected = detectFramework();
35
+ console.log(chalk.gray(`Detected framework: ${detected}`));
36
+
37
+ const { confirmed } = await inquirer.prompt([{
38
+ type: 'list',
39
+ name: 'confirmed',
40
+ message: 'Which framework are you using?',
41
+ choices: [
42
+ { name: `${detected} (detected)`, value: detected },
43
+ { name: 'HTML + Tailwind', value: 'html' },
44
+ { name: 'React', value: 'react' },
45
+ { name: 'Vue', value: 'vue' },
46
+ { name: 'Next.js', value: 'nextjs' },
47
+ { name: 'Angular', value: 'angular' },
48
+ { name: 'Svelte', value: 'svelte' },
49
+ { name: 'Astro', value: 'astro' },
50
+ ].filter((c, i, arr) => {
51
+ // Remove duplicate if detected matches a named option
52
+ if (i === 0) return true;
53
+ return c.value !== detected;
54
+ }),
55
+ default: detected,
56
+ }]);
57
+ framework = confirmed;
58
+ }
59
+
60
+ // Component directory
61
+ const componentDir = options.dir || 'src/components/ui';
62
+
63
+ // Write config
64
+ const config = defaultConfig(framework);
65
+ config.componentsDir = componentDir;
66
+ const configPath = writeProjectConfig(config);
67
+
68
+ console.log();
69
+ console.log(chalk.green('Created'), chalk.cyan(CONFIG_FILENAME));
70
+ console.log(chalk.gray(` Framework: ${framework}`));
71
+ console.log(chalk.gray(` Components: ${componentDir}/`));
72
+ console.log();
73
+ console.log(chalk.green('Next steps:'));
74
+ console.log(chalk.gray(' astralkit add button Add a component'));
75
+ console.log(chalk.gray(' astralkit list Browse components'));
76
+ console.log(chalk.gray(' astralkit login Unlock pro components'));
77
+ }
78
+
79
+ module.exports = { initCommand };
@@ -0,0 +1,64 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const { listComponents } = require('../lib/api');
4
+ const { validateFramework } = require('../lib/validators');
5
+
6
+ async function listCommand(options) {
7
+ // Validate --framework if provided
8
+ if (options.framework && !validateFramework(options.framework)) {
9
+ process.exit(1);
10
+ }
11
+
12
+ const spinner = ora('Fetching components...').start();
13
+
14
+ try {
15
+ const data = await listComponents({
16
+ category: options.category,
17
+ framework: options.framework,
18
+ pro: options.pro,
19
+ free: options.free,
20
+ });
21
+
22
+ spinner.stop();
23
+
24
+ if (!data.components || data.components.length === 0) {
25
+ console.log(chalk.yellow('No components found.'));
26
+ return;
27
+ }
28
+
29
+ // Group by category
30
+ const grouped = {};
31
+ for (const comp of data.components) {
32
+ const cat = comp.category || 'Uncategorized';
33
+ if (!grouped[cat]) grouped[cat] = [];
34
+ grouped[cat].push(comp);
35
+ }
36
+
37
+ console.log(chalk.blue.bold(`AstralKit Components (${data.components.length} total)`));
38
+ console.log();
39
+
40
+ for (const [category, components] of Object.entries(grouped)) {
41
+ console.log(chalk.white.bold(` ${category}`));
42
+ for (const comp of components) {
43
+ const proTag = comp.is_pro ? chalk.magenta(' PRO') : '';
44
+ const frameworks = comp.supported_frameworks?.length
45
+ ? chalk.gray(` [${comp.supported_frameworks.join(', ')}]`)
46
+ : '';
47
+ console.log(` ${chalk.cyan(comp.slug)}${proTag}${frameworks}`);
48
+ if (comp.short_description) {
49
+ console.log(` ${chalk.gray(comp.short_description)}`);
50
+ }
51
+ }
52
+ console.log();
53
+ }
54
+
55
+ console.log(chalk.gray('Run'), chalk.cyan('astralkit add <component>'), chalk.gray('to install.'));
56
+
57
+ } catch (err) {
58
+ spinner.fail('Failed to list components.');
59
+ console.error(chalk.red(err.message));
60
+ process.exit(1);
61
+ }
62
+ }
63
+
64
+ module.exports = { listCommand };
@@ -0,0 +1,157 @@
1
+ const http = require('http');
2
+ const crypto = require('crypto');
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const open = require('open');
6
+ const { getAuth, saveAuth } = require('../lib/auth');
7
+ const { API_BASE } = require('../lib/api');
8
+
9
+ const DEFAULT_CALLBACK_PORT = 9876;
10
+
11
+ async function loginCommand() {
12
+ console.log(chalk.blue.bold('AstralKit — Login'));
13
+ console.log();
14
+
15
+ // CI/CD shortcut: if ASTRALKIT_TOKEN is set, skip browser login
16
+ if (process.env.ASTRALKIT_TOKEN) {
17
+ const auth = getAuth();
18
+ if (auth.token) {
19
+ console.log(chalk.green('Authenticated via ASTRALKIT_TOKEN environment variable.'));
20
+ } else if (auth.expired) {
21
+ console.log(chalk.yellow('ASTRALKIT_TOKEN is expired. Please generate a new token.'));
22
+ process.exit(1);
23
+ } else {
24
+ console.log(chalk.red('ASTRALKIT_TOKEN is set but invalid.'));
25
+ process.exit(1);
26
+ }
27
+ return;
28
+ }
29
+
30
+ const callbackPort = parseInt(process.env.ASTRALKIT_LOGIN_PORT || String(DEFAULT_CALLBACK_PORT), 10);
31
+ if (isNaN(callbackPort) || callbackPort < 1024 || callbackPort > 65535) {
32
+ console.error(chalk.red('Invalid ASTRALKIT_LOGIN_PORT. Must be between 1024 and 65535.'));
33
+ process.exit(1);
34
+ }
35
+
36
+ // Generate CSRF state token
37
+ const state = crypto.randomBytes(32).toString('hex');
38
+
39
+ const spinner = ora('Waiting for browser authentication...').start();
40
+
41
+ try {
42
+ const token = await new Promise((resolve, reject) => {
43
+ const server = http.createServer((req, res) => {
44
+ const url = new URL(req.url, `http://localhost:${callbackPort}`);
45
+
46
+ // Restrict CORS to AstralKit domain only
47
+ const origin = req.headers['origin'] || '';
48
+ const allowedOrigin = API_BASE.startsWith('http://localhost')
49
+ ? origin // Allow localhost in development
50
+ : 'https://astralkit.com';
51
+
52
+ res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
53
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
54
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
55
+
56
+ // Handle CORS preflight
57
+ if (req.method === 'OPTIONS') {
58
+ res.writeHead(204);
59
+ res.end();
60
+ return;
61
+ }
62
+
63
+ if (url.pathname === '/callback' && req.method === 'POST') {
64
+ let body = '';
65
+
66
+ // Limit request body size (1MB max)
67
+ let bodySize = 0;
68
+ req.on('data', (chunk) => {
69
+ bodySize += chunk.length;
70
+ if (bodySize > 1_048_576) {
71
+ res.writeHead(413);
72
+ res.end(JSON.stringify({ error: 'Request body too large' }));
73
+ server.close();
74
+ reject(new Error('Received oversized callback payload.'));
75
+ return;
76
+ }
77
+ body += chunk;
78
+ });
79
+
80
+ req.on('end', () => {
81
+ try {
82
+ const data = JSON.parse(body);
83
+
84
+ // Verify CSRF state token
85
+ if (data.state !== state) {
86
+ res.writeHead(403, { 'Content-Type': 'application/json' });
87
+ res.end(JSON.stringify({ error: 'State mismatch' }));
88
+ server.close();
89
+ reject(new Error('Authentication failed — state mismatch. Please try again.'));
90
+ return;
91
+ }
92
+
93
+ const token = data.token;
94
+ const email = data.email;
95
+ const plan = data.plan;
96
+
97
+ if (token) {
98
+ res.writeHead(200, { 'Content-Type': 'application/json' });
99
+ res.end(JSON.stringify({ ok: true }));
100
+ server.close();
101
+ resolve({ token, email, plan });
102
+ } else {
103
+ res.writeHead(400, { 'Content-Type': 'application/json' });
104
+ res.end(JSON.stringify({ error: 'Missing token' }));
105
+ server.close();
106
+ reject(new Error('Authentication failed — no token received.'));
107
+ }
108
+ } catch {
109
+ res.writeHead(400, { 'Content-Type': 'application/json' });
110
+ res.end(JSON.stringify({ error: 'Invalid request body' }));
111
+ }
112
+ });
113
+ } else {
114
+ res.writeHead(404);
115
+ res.end();
116
+ }
117
+ });
118
+
119
+ server.listen(callbackPort, () => {
120
+ const loginUrl = `${API_BASE}/cli-auth?callback=${encodeURIComponent(`http://localhost:${callbackPort}/callback`)}&state=${state}`;
121
+ open(loginUrl);
122
+ });
123
+
124
+ // Timeout after 5 minutes
125
+ setTimeout(() => {
126
+ server.close();
127
+ reject(new Error('Authentication timed out. Please try again.'));
128
+ }, 5 * 60 * 1000);
129
+
130
+ server.on('error', (err) => {
131
+ if (err.code === 'EADDRINUSE') {
132
+ reject(new Error(
133
+ `Port ${callbackPort} is in use. ` +
134
+ `Close the conflicting process or set ASTRALKIT_LOGIN_PORT to a different port.`
135
+ ));
136
+ } else {
137
+ reject(err);
138
+ }
139
+ });
140
+ });
141
+
142
+ // Save credentials
143
+ saveAuth(token);
144
+ spinner.succeed('Authenticated successfully!');
145
+ console.log(chalk.gray(` Email: ${token.email || 'unknown'}`));
146
+ console.log(chalk.gray(` Plan: ${token.plan || 'free'}`));
147
+ console.log();
148
+ console.log(chalk.green('You can now install pro components:'));
149
+ console.log(chalk.cyan(' astralkit add pricing-table'));
150
+
151
+ } catch (err) {
152
+ spinner.fail(err.message);
153
+ process.exit(1);
154
+ }
155
+ }
156
+
157
+ module.exports = { loginCommand };
@@ -0,0 +1,53 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const { listComponents } = require('../lib/api');
4
+ const { validateFramework } = require('../lib/validators');
5
+
6
+ async function searchCommand(query, options) {
7
+ // Validate --framework if provided
8
+ if (options.framework && !validateFramework(options.framework)) {
9
+ process.exit(1);
10
+ }
11
+
12
+ const spinner = ora(`Searching for "${query}"...`).start();
13
+
14
+ try {
15
+ const data = await listComponents({
16
+ search: query,
17
+ framework: options.framework,
18
+ });
19
+
20
+ spinner.stop();
21
+
22
+ if (!data.components || data.components.length === 0) {
23
+ console.log(chalk.yellow(`No components found for "${query}".`));
24
+ return;
25
+ }
26
+
27
+ console.log(chalk.blue.bold(`Found ${data.components.length} component(s):`));
28
+ console.log();
29
+
30
+ for (const comp of data.components) {
31
+ const proTag = comp.is_pro ? chalk.magenta(' PRO') : '';
32
+ const cat = comp.category ? chalk.gray(` (${comp.category})`) : '';
33
+ console.log(` ${chalk.cyan(comp.slug)}${proTag}${cat}`);
34
+ if (comp.short_description || comp.description) {
35
+ const desc = comp.short_description || comp.description;
36
+ console.log(` ${chalk.gray(desc.length > 80 ? desc.slice(0, 80) + '...' : desc)}`);
37
+ }
38
+ if (comp.supported_frameworks?.length) {
39
+ console.log(` ${chalk.gray(`Frameworks: ${comp.supported_frameworks.join(', ')}`)}`);
40
+ }
41
+ console.log();
42
+ }
43
+
44
+ console.log(chalk.gray('Run'), chalk.cyan('astralkit add <component>'), chalk.gray('to install.'));
45
+
46
+ } catch (err) {
47
+ spinner.fail('Search failed.');
48
+ console.error(chalk.red(err.message));
49
+ process.exit(1);
50
+ }
51
+ }
52
+
53
+ module.exports = { searchCommand };
@@ -0,0 +1,123 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const inquirer = require('inquirer');
6
+ const { getComponent } = require('../lib/api');
7
+ const { getProjectConfig, detectFramework } = require('../lib/config');
8
+ const { validateSlug, validateFramework, isSafePath } = require('../lib/validators');
9
+
10
+ async function updateCommand(componentSlug, options) {
11
+ // Validate inputs
12
+ if (!validateSlug(componentSlug)) process.exit(1);
13
+ if (!validateFramework(options.framework)) process.exit(1);
14
+
15
+ const config = getProjectConfig();
16
+ const framework = options.framework || config?.framework || detectFramework();
17
+ const componentsDir = options.dir || config?.componentsDir || 'src/components/ui';
18
+ const targetDir = path.resolve(process.cwd(), componentsDir, componentSlug);
19
+
20
+ // Check if component exists locally
21
+ if (!fs.existsSync(targetDir)) {
22
+ console.log(chalk.yellow(`Component "${componentSlug}" not found locally.`));
23
+ console.log(chalk.gray(`Run ${chalk.cyan(`astralkit add ${componentSlug}`)} to install it first.`));
24
+ return;
25
+ }
26
+
27
+ const spinner = ora(`Fetching latest ${componentSlug} (${framework})...`).start();
28
+
29
+ try {
30
+ const data = await getComponent(componentSlug, framework);
31
+
32
+ if (!data.files || data.files.length === 0) {
33
+ spinner.fail(`No ${framework} code available for "${componentSlug}".`);
34
+ return;
35
+ }
36
+
37
+ spinner.stop();
38
+
39
+ // Check which files have changes
40
+ const changedFiles = [];
41
+ const newFiles = [];
42
+
43
+ for (const remoteFile of data.files) {
44
+ const localPath = path.join(targetDir, remoteFile.path);
45
+
46
+ if (!fs.existsSync(localPath)) {
47
+ newFiles.push(remoteFile);
48
+ continue;
49
+ }
50
+
51
+ const localContent = fs.readFileSync(localPath, 'utf8');
52
+ if (localContent !== remoteFile.content) {
53
+ changedFiles.push(remoteFile);
54
+ }
55
+ }
56
+
57
+ if (changedFiles.length === 0 && newFiles.length === 0) {
58
+ console.log(chalk.green('Already up to date.'));
59
+ return;
60
+ }
61
+
62
+ // Show what will change
63
+ console.log(chalk.blue.bold(`Update: ${data.name || componentSlug}`));
64
+ console.log();
65
+
66
+ if (changedFiles.length > 0) {
67
+ console.log(chalk.yellow('Modified files:'));
68
+ for (const f of changedFiles) {
69
+ console.log(chalk.yellow(` ~ ${f.path}`));
70
+ }
71
+ }
72
+ if (newFiles.length > 0) {
73
+ console.log(chalk.green('New files:'));
74
+ for (const f of newFiles) {
75
+ console.log(chalk.green(` + ${f.path}`));
76
+ }
77
+ }
78
+ console.log();
79
+
80
+ // Confirm unless --force
81
+ if (!options.force) {
82
+ const { confirm } = await inquirer.prompt([{
83
+ type: 'confirm',
84
+ name: 'confirm',
85
+ message: `Update ${changedFiles.length + newFiles.length} file(s)? This will overwrite local changes.`,
86
+ default: false,
87
+ }]);
88
+
89
+ if (!confirm) {
90
+ console.log(chalk.gray('Aborted.'));
91
+ return;
92
+ }
93
+ }
94
+
95
+ // Write updated files (with path traversal protection)
96
+ const updateSpinner = ora('Updating files...').start();
97
+
98
+ for (const file of [...changedFiles, ...newFiles]) {
99
+ if (!isSafePath(targetDir, file.path)) {
100
+ console.log(chalk.red(` Skipping suspicious file path: ${file.path}`));
101
+ continue;
102
+ }
103
+ const filePath = path.resolve(targetDir, file.path);
104
+ const fileDir = path.dirname(filePath);
105
+ if (!fs.existsSync(fileDir)) {
106
+ fs.mkdirSync(fileDir, { recursive: true });
107
+ }
108
+ fs.writeFileSync(filePath, file.content);
109
+ }
110
+
111
+ updateSpinner.succeed(`Updated ${chalk.cyan(data.name || componentSlug)}`);
112
+
113
+ for (const file of [...changedFiles, ...newFiles]) {
114
+ console.log(chalk.gray(` ${file.path}`));
115
+ }
116
+ } catch (err) {
117
+ spinner.fail('Update failed.');
118
+ console.error(chalk.red(err.message));
119
+ process.exit(1);
120
+ }
121
+ }
122
+
123
+ module.exports = { updateCommand };
package/src/lib/api.js ADDED
@@ -0,0 +1,139 @@
1
+ const path = require('path');
2
+ const { getAuth } = require('./auth');
3
+
4
+ const pkg = require(path.join(__dirname, '..', '..', 'package.json'));
5
+ const CLI_VERSION = pkg.version;
6
+
7
+ const DEFAULT_API_BASE = 'https://astralkit.com';
8
+ const REQUEST_TIMEOUT_MS = 30_000;
9
+
10
+ const API_BASE = (() => {
11
+ const override = process.env.ASTRALKIT_API_URL;
12
+ if (!override) return DEFAULT_API_BASE;
13
+
14
+ try {
15
+ const parsed = new URL(override);
16
+ if (parsed.protocol !== 'https:') {
17
+ console.warn(
18
+ '\x1b[33m⚠ ASTRALKIT_API_URL uses insecure protocol. ' +
19
+ 'Auth tokens will be sent in plaintext. Use HTTPS in production.\x1b[0m'
20
+ );
21
+ }
22
+ } catch {
23
+ console.error('\x1b[31m✖ Invalid ASTRALKIT_API_URL. Falling back to default.\x1b[0m');
24
+ return DEFAULT_API_BASE;
25
+ }
26
+
27
+ return override;
28
+ })();
29
+
30
+ /**
31
+ * Make an authenticated API request to AstralKit.
32
+ */
33
+ async function apiRequest(urlPath, options = {}) {
34
+ const url = `${API_BASE}${urlPath}`;
35
+ const auth = getAuth();
36
+
37
+ const headers = {
38
+ 'Accept': 'application/json',
39
+ 'User-Agent': `@astralkit/cli/${CLI_VERSION} node/${process.version}`,
40
+ 'X-AstralKit-CLI': CLI_VERSION,
41
+ ...options.headers,
42
+ };
43
+
44
+ if (auth.token) {
45
+ headers['Authorization'] = `Bearer ${auth.token}`;
46
+ }
47
+
48
+ // Request timeout
49
+ const controller = new AbortController();
50
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
51
+
52
+ let response;
53
+ try {
54
+ response = await fetch(url, {
55
+ ...options,
56
+ headers,
57
+ signal: controller.signal,
58
+ });
59
+ } catch (err) {
60
+ clearTimeout(timeout);
61
+ if (err.name === 'AbortError') {
62
+ const error = new Error('Request timed out. Check your network connection and try again.');
63
+ error.code = 'TIMEOUT';
64
+ throw error;
65
+ }
66
+ if (err.code === 'ENOTFOUND' || err.cause?.code === 'ENOTFOUND') {
67
+ const error = new Error('Could not reach astralkit.com. Check your internet connection.');
68
+ error.code = 'NETWORK_ERROR';
69
+ throw error;
70
+ }
71
+ if (err.code === 'ECONNREFUSED' || err.cause?.code === 'ECONNREFUSED') {
72
+ const error = new Error('Connection refused. The AstralKit API may be temporarily down.');
73
+ error.code = 'NETWORK_ERROR';
74
+ throw error;
75
+ }
76
+ throw err;
77
+ } finally {
78
+ clearTimeout(timeout);
79
+ }
80
+
81
+ if (response.status === 401) {
82
+ const message = auth.expired
83
+ ? 'Session expired. Run `astralkit login` to re-authenticate.'
84
+ : 'Authentication required. Run `astralkit login` to authenticate.';
85
+ const err = new Error(message);
86
+ err.code = 'AUTH_REQUIRED';
87
+ throw err;
88
+ }
89
+
90
+ if (response.status === 403) {
91
+ const err = new Error('Pro subscription required for this component. Visit https://astralkit.com/pricing');
92
+ err.code = 'PRO_REQUIRED';
93
+ throw err;
94
+ }
95
+
96
+ if (!response.ok) {
97
+ const body = await response.text().catch(() => '');
98
+ const err = new Error(`API error ${response.status}: ${body || response.statusText}`);
99
+ err.code = 'API_ERROR';
100
+ throw err;
101
+ }
102
+
103
+ return response;
104
+ }
105
+
106
+ /**
107
+ * List components with optional filters.
108
+ */
109
+ async function listComponents({ category, framework, pro, free, search } = {}) {
110
+ const params = new URLSearchParams();
111
+ if (category) params.set('category', category);
112
+ if (framework) params.set('framework', framework);
113
+ if (pro) params.set('pro', 'true');
114
+ if (free) params.set('free', 'true');
115
+ if (search) params.set('search', search);
116
+
117
+ const response = await apiRequest(`/api/cli/components?${params}`);
118
+ return response.json();
119
+ }
120
+
121
+ /**
122
+ * Get a single component's details + code files for installation.
123
+ */
124
+ async function getComponent(slug, framework = 'html') {
125
+ const response = await apiRequest(
126
+ `/api/cli/components/${encodeURIComponent(slug)}?framework=${encodeURIComponent(framework)}`
127
+ );
128
+ return response.json();
129
+ }
130
+
131
+ /**
132
+ * Verify current authentication with the server.
133
+ */
134
+ async function verifyAuth() {
135
+ const response = await apiRequest('/api/cli/whoami');
136
+ return response.json();
137
+ }
138
+
139
+ module.exports = { apiRequest, listComponents, getComponent, verifyAuth, API_BASE, CLI_VERSION };