@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.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/bin/index.js +138 -0
- package/package.json +51 -0
- package/src/commands/add.js +104 -0
- package/src/commands/diff.js +107 -0
- package/src/commands/init.js +79 -0
- package/src/commands/list.js +64 -0
- package/src/commands/login.js +157 -0
- package/src/commands/search.js +53 -0
- package/src/commands/update.js +123 -0
- package/src/lib/api.js +139 -0
- package/src/lib/auth.js +119 -0
- package/src/lib/config.js +116 -0
- package/src/lib/validators.js +64 -0
|
@@ -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 };
|