@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Blue Beacon Creative LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# @astralkit/cli
|
|
2
|
+
|
|
3
|
+
Install and manage AstralKit UI components in your project.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Initialize AstralKit in your project
|
|
9
|
+
npx @astralkit/cli init
|
|
10
|
+
|
|
11
|
+
# Add a component
|
|
12
|
+
npx @astralkit/cli add button-sizes
|
|
13
|
+
|
|
14
|
+
# Search for components
|
|
15
|
+
npx @astralkit/cli search "pricing table"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
### `astralkit init`
|
|
21
|
+
|
|
22
|
+
Initialize AstralKit in your project. Detects your framework and creates `astralkit.config.json`.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
astralkit init
|
|
26
|
+
astralkit init --framework=react
|
|
27
|
+
astralkit init --dir=src/components/ui
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### `astralkit add <component>`
|
|
31
|
+
|
|
32
|
+
Add a component to your project. Fetches code from AstralKit and writes it to your components directory.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
astralkit add avatar-variants
|
|
36
|
+
astralkit add pricing-table --framework=react
|
|
37
|
+
astralkit add data-table --overwrite
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
- `-f, --framework` — Framework version (html, react, vue, nextjs, angular, svelte, astro)
|
|
42
|
+
- `--dir` — Override output directory
|
|
43
|
+
- `--overwrite` — Overwrite existing files
|
|
44
|
+
|
|
45
|
+
### `astralkit list`
|
|
46
|
+
|
|
47
|
+
List all available components.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
astralkit list
|
|
51
|
+
astralkit list --category=forms
|
|
52
|
+
astralkit list --framework=react
|
|
53
|
+
astralkit list --pro
|
|
54
|
+
astralkit list --free
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `astralkit search <query>`
|
|
58
|
+
|
|
59
|
+
Search for components by name or description.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
astralkit search "data table"
|
|
63
|
+
astralkit search "pricing" --framework=react
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `astralkit diff <component>`
|
|
67
|
+
|
|
68
|
+
Show differences between your local component and the latest version.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
astralkit diff button-sizes
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `astralkit update <component>`
|
|
75
|
+
|
|
76
|
+
Update a component to the latest version from AstralKit.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
astralkit update button-sizes
|
|
80
|
+
astralkit update button-sizes --force
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `astralkit login`
|
|
84
|
+
|
|
85
|
+
Authenticate with AstralKit to access premium components. Opens your browser for secure login.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
astralkit login
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `astralkit logout`
|
|
92
|
+
|
|
93
|
+
Remove stored authentication.
|
|
94
|
+
|
|
95
|
+
### `astralkit whoami`
|
|
96
|
+
|
|
97
|
+
Show current authentication status and subscription plan. Verifies your token with the server.
|
|
98
|
+
|
|
99
|
+
## Premium Components
|
|
100
|
+
|
|
101
|
+
Some components require a Pro subscription. When you try to add a pro component without authentication:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
$ astralkit add pro-data-table
|
|
105
|
+
✖ Failed to add component.
|
|
106
|
+
Authentication required. Run `astralkit login` to authenticate.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
After logging in, pro components install normally:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
$ astralkit login
|
|
113
|
+
✔ Authenticated successfully!
|
|
114
|
+
Email: you@example.com
|
|
115
|
+
Plan: pro
|
|
116
|
+
|
|
117
|
+
$ astralkit add pro-data-table
|
|
118
|
+
✔ Added Pro Data Table (react)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Configuration
|
|
122
|
+
|
|
123
|
+
### `astralkit.config.json`
|
|
124
|
+
|
|
125
|
+
Created by `astralkit init` in your project root:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"$schema": "https://astralkit.com/schema/config.json",
|
|
130
|
+
"framework": "nextjs",
|
|
131
|
+
"componentsDir": "src/components/ui",
|
|
132
|
+
"tokensImport": "@astralkit/tokens/css"
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `~/.astralkit/config.json`
|
|
137
|
+
|
|
138
|
+
Stores your authentication token (created by `astralkit login`). This file is created with restricted permissions (owner read/write only).
|
|
139
|
+
|
|
140
|
+
## CI/CD Usage
|
|
141
|
+
|
|
142
|
+
For automated environments (CI/CD pipelines, Docker builds), set the `ASTRALKIT_TOKEN` environment variable instead of using browser login:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Set token from your AstralKit dashboard
|
|
146
|
+
export ASTRALKIT_TOKEN="your-api-token"
|
|
147
|
+
|
|
148
|
+
# Commands automatically use the env var
|
|
149
|
+
astralkit add button-sizes
|
|
150
|
+
astralkit whoami
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Environment Variables
|
|
154
|
+
|
|
155
|
+
| Variable | Description |
|
|
156
|
+
|----------|-------------|
|
|
157
|
+
| `ASTRALKIT_TOKEN` | API token for CI/CD (skips browser login) |
|
|
158
|
+
| `ASTRALKIT_API_URL` | Override API base URL (development only) |
|
|
159
|
+
| `ASTRALKIT_LOGIN_PORT` | Override callback port for login (default: 9876) |
|
|
160
|
+
|
|
161
|
+
## Security
|
|
162
|
+
|
|
163
|
+
- Authentication tokens are stored at `~/.astralkit/config.json` with `0600` permissions (owner read/write only)
|
|
164
|
+
- The config directory is created with `0700` permissions
|
|
165
|
+
- Browser login uses CSRF state tokens to prevent cross-site attacks
|
|
166
|
+
- CORS is restricted to `astralkit.com` during authentication
|
|
167
|
+
- All API requests use HTTPS with 30-second timeouts
|
|
168
|
+
- No tokens, secrets, or credentials are ever hardcoded in the package
|
|
169
|
+
|
|
170
|
+
## Troubleshooting
|
|
171
|
+
|
|
172
|
+
### Port 9876 is in use
|
|
173
|
+
|
|
174
|
+
If `astralkit login` fails because the port is busy:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Use a different port
|
|
178
|
+
ASTRALKIT_LOGIN_PORT=9877 astralkit login
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Token expired
|
|
182
|
+
|
|
183
|
+
If you see "Session expired", run `astralkit login` again to get a fresh token.
|
|
184
|
+
|
|
185
|
+
### Network errors
|
|
186
|
+
|
|
187
|
+
If you see timeout or connection errors, check your internet connection. The CLI requires access to `astralkit.com`.
|
|
188
|
+
|
|
189
|
+
## Requirements
|
|
190
|
+
|
|
191
|
+
- Node.js 18+
|
|
192
|
+
- An AstralKit account for premium components
|
|
193
|
+
|
|
194
|
+
## Links
|
|
195
|
+
|
|
196
|
+
- [AstralKit](https://astralkit.com)
|
|
197
|
+
- [Documentation](https://astralkit.com/docs)
|
|
198
|
+
- [Pricing](https://astralkit.com/pricing)
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
8
|
+
|
|
9
|
+
const { initCommand } = require('../src/commands/init');
|
|
10
|
+
const { addCommand } = require('../src/commands/add');
|
|
11
|
+
const { loginCommand } = require('../src/commands/login');
|
|
12
|
+
const { listCommand } = require('../src/commands/list');
|
|
13
|
+
const { searchCommand } = require('../src/commands/search');
|
|
14
|
+
const { diffCommand } = require('../src/commands/diff');
|
|
15
|
+
const { updateCommand } = require('../src/commands/update');
|
|
16
|
+
|
|
17
|
+
const program = new Command();
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.name('astralkit')
|
|
21
|
+
.description('AstralKit CLI — Install and manage UI components')
|
|
22
|
+
.version(pkg.version);
|
|
23
|
+
|
|
24
|
+
// ─── init ────────────────────────────────────────────
|
|
25
|
+
program
|
|
26
|
+
.command('init')
|
|
27
|
+
.description('Initialize AstralKit in your project')
|
|
28
|
+
.option('--framework <framework>', 'Framework to use (auto-detected if omitted)')
|
|
29
|
+
.option('--dir <directory>', 'Component output directory', 'src/components/ui')
|
|
30
|
+
.action(initCommand);
|
|
31
|
+
|
|
32
|
+
// ─── add ─────────────────────────────────────────────
|
|
33
|
+
program
|
|
34
|
+
.command('add <component>')
|
|
35
|
+
.description('Add a component to your project')
|
|
36
|
+
.option('-f, --framework <framework>', 'Framework version (html, react, vue, nextjs)')
|
|
37
|
+
.option('--dir <directory>', 'Override output directory')
|
|
38
|
+
.option('--overwrite', 'Overwrite existing files', false)
|
|
39
|
+
.action(addCommand);
|
|
40
|
+
|
|
41
|
+
// ─── login ───────────────────────────────────────────
|
|
42
|
+
program
|
|
43
|
+
.command('login')
|
|
44
|
+
.description('Authenticate with AstralKit for premium components')
|
|
45
|
+
.action(loginCommand);
|
|
46
|
+
|
|
47
|
+
// ─── logout ──────────────────────────────────────────
|
|
48
|
+
program
|
|
49
|
+
.command('logout')
|
|
50
|
+
.description('Remove stored authentication')
|
|
51
|
+
.action(async () => {
|
|
52
|
+
const { clearToken } = require('../src/lib/auth');
|
|
53
|
+
clearToken();
|
|
54
|
+
console.log(chalk.green('Logged out successfully.'));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─── list ────────────────────────────────────────────
|
|
58
|
+
program
|
|
59
|
+
.command('list')
|
|
60
|
+
.description('List available components')
|
|
61
|
+
.option('-c, --category <category>', 'Filter by category')
|
|
62
|
+
.option('--framework <framework>', 'Filter by supported framework')
|
|
63
|
+
.option('--pro', 'Show only pro components', false)
|
|
64
|
+
.option('--free', 'Show only free components', false)
|
|
65
|
+
.action(listCommand);
|
|
66
|
+
|
|
67
|
+
// ─── search ──────────────────────────────────────────
|
|
68
|
+
program
|
|
69
|
+
.command('search <query>')
|
|
70
|
+
.description('Search for components')
|
|
71
|
+
.option('--framework <framework>', 'Filter by supported framework')
|
|
72
|
+
.action(searchCommand);
|
|
73
|
+
|
|
74
|
+
// ─── diff ────────────────────────────────────────────
|
|
75
|
+
program
|
|
76
|
+
.command('diff <component>')
|
|
77
|
+
.description('Show differences between local and remote component')
|
|
78
|
+
.option('-f, --framework <framework>', 'Framework version')
|
|
79
|
+
.option('--dir <directory>', 'Override component directory')
|
|
80
|
+
.action(diffCommand);
|
|
81
|
+
|
|
82
|
+
// ─── update ──────────────────────────────────────────
|
|
83
|
+
program
|
|
84
|
+
.command('update <component>')
|
|
85
|
+
.description('Update a component to the latest version')
|
|
86
|
+
.option('-f, --framework <framework>', 'Framework version')
|
|
87
|
+
.option('--dir <directory>', 'Override component directory')
|
|
88
|
+
.option('--force', 'Skip confirmation prompt', false)
|
|
89
|
+
.action(updateCommand);
|
|
90
|
+
|
|
91
|
+
// ─── whoami ──────────────────────────────────────────
|
|
92
|
+
program
|
|
93
|
+
.command('whoami')
|
|
94
|
+
.description('Show current authentication status and subscription plan')
|
|
95
|
+
.action(async () => {
|
|
96
|
+
const { getAuth } = require('../src/lib/auth');
|
|
97
|
+
const ora = require('ora');
|
|
98
|
+
|
|
99
|
+
const auth = getAuth();
|
|
100
|
+
|
|
101
|
+
if (!auth.token) {
|
|
102
|
+
if (auth.expired) {
|
|
103
|
+
console.log(chalk.yellow('Session expired.'));
|
|
104
|
+
console.log(chalk.gray('Run'), chalk.cyan('astralkit login'), chalk.gray('to re-authenticate.'));
|
|
105
|
+
} else {
|
|
106
|
+
console.log(chalk.yellow('Not authenticated.'));
|
|
107
|
+
console.log(chalk.gray('Run'), chalk.cyan('astralkit login'), chalk.gray('to authenticate.'));
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Verify with server
|
|
113
|
+
const spinner = ora('Verifying authentication...').start();
|
|
114
|
+
try {
|
|
115
|
+
const { verifyAuth } = require('../src/lib/api');
|
|
116
|
+
const data = await verifyAuth();
|
|
117
|
+
spinner.stop();
|
|
118
|
+
|
|
119
|
+
console.log(chalk.green('Authenticated as:'), data.email || 'unknown');
|
|
120
|
+
console.log(chalk.gray('Plan:'), data.plan || 'free');
|
|
121
|
+
if (auth.source === 'env') {
|
|
122
|
+
console.log(chalk.gray('Source: ASTRALKIT_TOKEN environment variable'));
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
spinner.stop();
|
|
126
|
+
|
|
127
|
+
if (err.code === 'AUTH_REQUIRED') {
|
|
128
|
+
console.log(chalk.yellow('Token is invalid or expired.'));
|
|
129
|
+
console.log(chalk.gray('Run'), chalk.cyan('astralkit login'), chalk.gray('to re-authenticate.'));
|
|
130
|
+
} else {
|
|
131
|
+
// Offline fallback — show cached info
|
|
132
|
+
console.log(chalk.green('Authenticated as:'), auth.email || 'unknown');
|
|
133
|
+
console.log(chalk.gray('Plan:'), auth.plan || 'unknown', chalk.gray('(cached — unable to verify)'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@astralkit/cli",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "AstralKit CLI — Install and manage UI components in your project",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"astralkit": "bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"registry": "https://registry.npmjs.org"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"prepublishOnly": "node -e \"require('./package.json')\""
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"astralkit",
|
|
26
|
+
"ui",
|
|
27
|
+
"components",
|
|
28
|
+
"tailwind",
|
|
29
|
+
"cli",
|
|
30
|
+
"react",
|
|
31
|
+
"nextjs",
|
|
32
|
+
"vue",
|
|
33
|
+
"svelte"
|
|
34
|
+
],
|
|
35
|
+
"homepage": "https://astralkit.com",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/efpacc/Astral-new.git",
|
|
39
|
+
"directory": "packages/cli"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/efpacc/Astral-new/issues"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"commander": "^12.0.0",
|
|
46
|
+
"chalk": "^4.1.2",
|
|
47
|
+
"ora": "^5.4.1",
|
|
48
|
+
"inquirer": "^8.2.6",
|
|
49
|
+
"open": "^8.4.2"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const ora = require('ora');
|
|
5
|
+
const { getComponent } = require('../lib/api');
|
|
6
|
+
const { getProjectConfig, detectFramework } = require('../lib/config');
|
|
7
|
+
const { validateSlug, validateFramework, isSafePath } = require('../lib/validators');
|
|
8
|
+
|
|
9
|
+
async function addCommand(componentSlug, options) {
|
|
10
|
+
// Validate inputs
|
|
11
|
+
if (!validateSlug(componentSlug)) process.exit(1);
|
|
12
|
+
if (!validateFramework(options.framework)) process.exit(1);
|
|
13
|
+
|
|
14
|
+
// Resolve framework
|
|
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 for existing files
|
|
21
|
+
if (fs.existsSync(targetDir) && !options.overwrite) {
|
|
22
|
+
console.log(chalk.yellow(`Component already exists at ${componentsDir}/${componentSlug}/`));
|
|
23
|
+
console.log(chalk.gray('Use --overwrite to replace existing files.'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const spinner = ora(`Fetching ${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
|
+
if (data.supported_frameworks?.length) {
|
|
35
|
+
console.log(chalk.gray(`Available frameworks: ${data.supported_frameworks.join(', ')}`));
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create component directory
|
|
41
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
42
|
+
|
|
43
|
+
// Write each file (with path traversal protection)
|
|
44
|
+
for (const file of data.files) {
|
|
45
|
+
if (!isSafePath(targetDir, file.path)) {
|
|
46
|
+
console.log(chalk.red(` Skipping suspicious file path: ${file.path}`));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const filePath = path.resolve(targetDir, file.path);
|
|
50
|
+
const fileDir = path.dirname(filePath);
|
|
51
|
+
if (!fs.existsSync(fileDir)) {
|
|
52
|
+
fs.mkdirSync(fileDir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
fs.writeFileSync(filePath, file.content);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
spinner.succeed(`Added ${chalk.cyan(data.name || componentSlug)} (${framework})`);
|
|
58
|
+
|
|
59
|
+
// Show files written
|
|
60
|
+
console.log(chalk.gray(` ${componentsDir}/${componentSlug}/`));
|
|
61
|
+
for (const file of data.files) {
|
|
62
|
+
console.log(chalk.gray(` ${file.path}`));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Show dependencies to install
|
|
66
|
+
if (data.dependencies?.length) {
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(chalk.yellow('Install required dependencies:'));
|
|
69
|
+
console.log(chalk.cyan(` npm install ${data.dependencies.join(' ')}`));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Pro badge
|
|
73
|
+
if (data.is_pro) {
|
|
74
|
+
console.log();
|
|
75
|
+
console.log(chalk.magenta('Pro component — thank you for your subscription!'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
} catch (err) {
|
|
79
|
+
spinner.fail('Failed to add component.');
|
|
80
|
+
|
|
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));
|
|
85
|
+
} 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);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Not a JSON parse error, show raw message
|
|
97
|
+
}
|
|
98
|
+
console.error(chalk.red(err.message));
|
|
99
|
+
}
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { addCommand };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const ora = require('ora');
|
|
5
|
+
const { getComponent } = require('../lib/api');
|
|
6
|
+
const { getProjectConfig, detectFramework } = require('../lib/config');
|
|
7
|
+
const { validateSlug, validateFramework, isSafePath } = require('../lib/validators');
|
|
8
|
+
|
|
9
|
+
async function diffCommand(componentSlug, options) {
|
|
10
|
+
// Validate inputs
|
|
11
|
+
if (!validateSlug(componentSlug)) process.exit(1);
|
|
12
|
+
if (!validateFramework(options.framework)) process.exit(1);
|
|
13
|
+
|
|
14
|
+
const config = getProjectConfig();
|
|
15
|
+
const framework = options.framework || config?.framework || detectFramework();
|
|
16
|
+
const componentsDir = options.dir || config?.componentsDir || 'src/components/ui';
|
|
17
|
+
const targetDir = path.resolve(process.cwd(), componentsDir, componentSlug);
|
|
18
|
+
|
|
19
|
+
// Check if component exists locally
|
|
20
|
+
if (!fs.existsSync(targetDir)) {
|
|
21
|
+
console.log(chalk.yellow(`Component "${componentSlug}" not found locally at ${componentsDir}/${componentSlug}/`));
|
|
22
|
+
console.log(chalk.gray('Nothing to diff.'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const spinner = ora(`Fetching latest ${componentSlug} (${framework})...`).start();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const data = await getComponent(componentSlug, framework);
|
|
30
|
+
|
|
31
|
+
if (!data.files || data.files.length === 0) {
|
|
32
|
+
spinner.fail(`No ${framework} code available for "${componentSlug}" on remote.`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
spinner.stop();
|
|
37
|
+
|
|
38
|
+
console.log(chalk.blue.bold(`Diff: ${componentSlug} (${framework})`));
|
|
39
|
+
console.log();
|
|
40
|
+
|
|
41
|
+
let hasChanges = false;
|
|
42
|
+
|
|
43
|
+
for (const remoteFile of data.files) {
|
|
44
|
+
// Path traversal protection
|
|
45
|
+
if (!isSafePath(targetDir, remoteFile.path)) {
|
|
46
|
+
console.log(chalk.red(` Skipping suspicious file path: ${remoteFile.path}`));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const localPath = path.resolve(targetDir, remoteFile.path);
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(localPath)) {
|
|
53
|
+
console.log(chalk.green(`+ ${remoteFile.path}`), chalk.gray('(new file on remote)'));
|
|
54
|
+
hasChanges = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const localContent = fs.readFileSync(localPath, 'utf8');
|
|
59
|
+
const remoteContent = remoteFile.content;
|
|
60
|
+
|
|
61
|
+
if (localContent === remoteContent) {
|
|
62
|
+
console.log(chalk.gray(` ${remoteFile.path}`), chalk.green('(up to date)'));
|
|
63
|
+
} else {
|
|
64
|
+
hasChanges = true;
|
|
65
|
+
console.log(chalk.yellow(`~ ${remoteFile.path}`), chalk.yellow('(modified)'));
|
|
66
|
+
|
|
67
|
+
// Show simple line count diff
|
|
68
|
+
const localLines = localContent.split('\n').length;
|
|
69
|
+
const remoteLines = remoteContent.split('\n').length;
|
|
70
|
+
const diff = remoteLines - localLines;
|
|
71
|
+
if (diff > 0) {
|
|
72
|
+
console.log(chalk.gray(` Remote has ${diff} more line(s)`));
|
|
73
|
+
} else if (diff < 0) {
|
|
74
|
+
console.log(chalk.gray(` Local has ${Math.abs(diff)} more line(s)`));
|
|
75
|
+
} else {
|
|
76
|
+
console.log(chalk.gray(` Same line count, content differs`));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check for local files not on remote
|
|
82
|
+
if (fs.existsSync(targetDir)) {
|
|
83
|
+
const localFiles = fs.readdirSync(targetDir);
|
|
84
|
+
const remoteFilenames = new Set(data.files.map(f => f.path));
|
|
85
|
+
for (const localFile of localFiles) {
|
|
86
|
+
if (!remoteFilenames.has(localFile)) {
|
|
87
|
+
console.log(chalk.red(`- ${localFile}`), chalk.gray('(local only, not on remote)'));
|
|
88
|
+
hasChanges = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log();
|
|
94
|
+
if (hasChanges) {
|
|
95
|
+
console.log(chalk.yellow('Changes detected.'));
|
|
96
|
+
console.log(chalk.gray(`Run ${chalk.cyan(`astralkit update ${componentSlug}`)} to update.`));
|
|
97
|
+
} else {
|
|
98
|
+
console.log(chalk.green('All files are up to date.'));
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
spinner.fail('Diff failed.');
|
|
102
|
+
console.error(chalk.red(err.message));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { diffCommand };
|