blok0 0.1.0 → 0.1.2
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/dist/api/index.d.ts +46 -0
- package/dist/api/index.js +147 -0
- package/dist/ast/index.d.ts +31 -0
- package/dist/ast/index.js +324 -0
- package/dist/auth/constants.d.ts +11 -0
- package/dist/auth/constants.js +155 -0
- package/dist/auth/index.d.ts +61 -0
- package/dist/auth/index.js +168 -0
- package/dist/auth/server.d.ts +55 -0
- package/dist/auth/server.js +236 -0
- package/dist/blocks/index.d.ts +56 -0
- package/dist/blocks/index.js +189 -0
- package/dist/handlers/add-block.d.ts +7 -0
- package/dist/handlers/add-block.js +142 -0
- package/dist/handlers/login.d.ts +8 -0
- package/dist/handlers/login.js +124 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +187 -7
- package/dist/registry/index.d.ts +75 -0
- package/dist/registry/index.js +231 -0
- package/package.json +33 -25
- package/src/api/index.ts +177 -0
- package/src/ast/index.ts +368 -0
- package/src/auth/constants.ts +155 -0
- package/src/auth/index.ts +154 -0
- package/src/auth/server.ts +240 -0
- package/src/blocks/index.ts +186 -0
- package/src/handlers/add-block.ts +132 -0
- package/src/handlers/login.ts +130 -0
- package/src/index.ts +212 -51
- package/src/registry/index.ts +244 -0
- package/test-ast.js +150 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as keytar from 'keytar';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
|
|
6
|
+
const SERVICE_NAME = 'blok0';
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), '.blok0');
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
9
|
+
|
|
10
|
+
export interface AuthConfig {
|
|
11
|
+
apiEndpoint?: string;
|
|
12
|
+
refreshToken?: string;
|
|
13
|
+
tokenExpiry?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AuthCallback {
|
|
17
|
+
token: string;
|
|
18
|
+
expires_in?: number;
|
|
19
|
+
refresh_token?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AuthServerOptions {
|
|
23
|
+
port?: number;
|
|
24
|
+
timeout?: number;
|
|
25
|
+
state?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TokenResponse {
|
|
29
|
+
access_token: string;
|
|
30
|
+
refresh_token?: string;
|
|
31
|
+
expires_in?: number;
|
|
32
|
+
token_type?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Ensure config directory exists
|
|
37
|
+
*/
|
|
38
|
+
function ensureConfigDir(): void {
|
|
39
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
40
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load auth configuration from file
|
|
46
|
+
*/
|
|
47
|
+
export function loadAuthConfig(): AuthConfig {
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
50
|
+
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
51
|
+
return JSON.parse(data);
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn('Failed to load auth config:', error);
|
|
55
|
+
}
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Save auth configuration to file
|
|
61
|
+
*/
|
|
62
|
+
export function saveAuthConfig(config: AuthConfig): void {
|
|
63
|
+
ensureConfigDir();
|
|
64
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Store access token securely
|
|
69
|
+
*/
|
|
70
|
+
export async function storeAccessToken(token: string): Promise<void> {
|
|
71
|
+
try {
|
|
72
|
+
await keytar.setPassword(SERVICE_NAME, 'access_token', token);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Failed to store access token:', error);
|
|
75
|
+
throw new Error('Unable to securely store access token');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get stored access token
|
|
81
|
+
*/
|
|
82
|
+
export async function getAccessToken(): Promise<string | null> {
|
|
83
|
+
try {
|
|
84
|
+
return await keytar.getPassword(SERVICE_NAME, 'access_token');
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Failed to retrieve access token:', error);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Store refresh token securely
|
|
93
|
+
*/
|
|
94
|
+
export async function storeRefreshToken(token: string): Promise<void> {
|
|
95
|
+
try {
|
|
96
|
+
await keytar.setPassword(SERVICE_NAME, 'refresh_token', token);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Failed to store refresh token:', error);
|
|
99
|
+
throw new Error('Unable to securely store refresh token');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get stored refresh token
|
|
105
|
+
*/
|
|
106
|
+
export async function getRefreshToken(): Promise<string | null> {
|
|
107
|
+
try {
|
|
108
|
+
return await keytar.getPassword(SERVICE_NAME, 'refresh_token');
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Failed to retrieve refresh token:', error);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clear all stored credentials
|
|
117
|
+
*/
|
|
118
|
+
export async function clearCredentials(): Promise<void> {
|
|
119
|
+
try {
|
|
120
|
+
await keytar.deletePassword(SERVICE_NAME, 'access_token');
|
|
121
|
+
await keytar.deletePassword(SERVICE_NAME, 'refresh_token');
|
|
122
|
+
|
|
123
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
124
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Failed to clear credentials:', error);
|
|
128
|
+
throw new Error('Unable to clear stored credentials');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if user is authenticated
|
|
134
|
+
*/
|
|
135
|
+
export async function isAuthenticated(): Promise<boolean> {
|
|
136
|
+
const token = await getAccessToken();
|
|
137
|
+
return token !== null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validate token expiry (basic check)
|
|
142
|
+
*/
|
|
143
|
+
export function isTokenExpired(expiry?: number): boolean {
|
|
144
|
+
if (!expiry) return false;
|
|
145
|
+
return Date.now() >= expiry;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get authorization header for API requests
|
|
150
|
+
*/
|
|
151
|
+
export async function getAuthHeader(): Promise<string | null> {
|
|
152
|
+
const token = await getAccessToken();
|
|
153
|
+
return token ? `Bearer ${token}` : null;
|
|
154
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
import { AuthCallback, AuthServerOptions } from './index';
|
|
5
|
+
import {
|
|
6
|
+
AUTH_BASE_URL,
|
|
7
|
+
AUTHORIZE_ENDPOINT,
|
|
8
|
+
DEFAULT_TIMEOUT,
|
|
9
|
+
PORT_RANGE,
|
|
10
|
+
CALLBACK_PATH,
|
|
11
|
+
SUCCESS_HTML,
|
|
12
|
+
ERROR_HTML,
|
|
13
|
+
TIMEOUT_HTML
|
|
14
|
+
} from './constants';
|
|
15
|
+
|
|
16
|
+
export class AuthServer extends EventEmitter {
|
|
17
|
+
private server?: http.Server;
|
|
18
|
+
private port?: number;
|
|
19
|
+
private state: string;
|
|
20
|
+
private timeoutId?: NodeJS.Timeout;
|
|
21
|
+
private resolveCallback?: (value: AuthCallback) => void;
|
|
22
|
+
private rejectCallback?: (error: Error) => void;
|
|
23
|
+
|
|
24
|
+
constructor(options: AuthServerOptions = {}) {
|
|
25
|
+
super();
|
|
26
|
+
this.state = options.state || this.generateState();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate a random state parameter for CSRF protection
|
|
31
|
+
*/
|
|
32
|
+
private generateState(): string {
|
|
33
|
+
return randomBytes(32).toString('hex');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Find an available port in the configured range
|
|
38
|
+
*/
|
|
39
|
+
private async findAvailablePort(): Promise<number> {
|
|
40
|
+
const { min, max } = PORT_RANGE;
|
|
41
|
+
|
|
42
|
+
for (let port = min; port <= max; port++) {
|
|
43
|
+
if (await this.isPortAvailable(port)) {
|
|
44
|
+
return port;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error('No available ports found in range');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a port is available
|
|
53
|
+
*/
|
|
54
|
+
private async isPortAvailable(port: number): Promise<boolean> {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const testServer = http.createServer();
|
|
57
|
+
testServer.listen(port, '127.0.0.1', () => {
|
|
58
|
+
testServer.close(() => resolve(true));
|
|
59
|
+
});
|
|
60
|
+
testServer.on('error', () => resolve(false));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Handle incoming HTTP requests
|
|
66
|
+
*/
|
|
67
|
+
private handleRequest = (req: http.IncomingMessage, res: http.ServerResponse): void => {
|
|
68
|
+
try {
|
|
69
|
+
const fullUrl = `http://localhost:${this.port}${req.url}`;
|
|
70
|
+
const parsedUrl = new URL(fullUrl);
|
|
71
|
+
const pathname = parsedUrl.pathname;
|
|
72
|
+
|
|
73
|
+
// Handle callback endpoint
|
|
74
|
+
if (pathname === CALLBACK_PATH && req.method === 'GET') {
|
|
75
|
+
this.handleCallback(parsedUrl, res);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Handle any other request with a 404
|
|
80
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
81
|
+
res.end('Not found');
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error parsing request URL:', error);
|
|
84
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
85
|
+
res.end('Bad request');
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handle the OAuth callback
|
|
91
|
+
*/
|
|
92
|
+
private handleCallback(parsedUrl: URL, res: http.ServerResponse): void {
|
|
93
|
+
const searchParams = parsedUrl.searchParams;
|
|
94
|
+
const state = searchParams.get('state');
|
|
95
|
+
const token = searchParams.get('token');
|
|
96
|
+
const error = searchParams.get('error');
|
|
97
|
+
|
|
98
|
+
// Validate state parameter
|
|
99
|
+
if (!state || state !== this.state) {
|
|
100
|
+
console.error('State parameter mismatch - possible CSRF attack');
|
|
101
|
+
console.error(`Expected: ${this.state}, Received: ${state}`);
|
|
102
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
103
|
+
res.end(ERROR_HTML);
|
|
104
|
+
this.emit('error', new Error('Invalid state parameter'));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle error from authorization server
|
|
109
|
+
if (error) {
|
|
110
|
+
console.error('Authorization error:', error);
|
|
111
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
112
|
+
res.end(ERROR_HTML);
|
|
113
|
+
this.emit('error', new Error(`Authorization failed: ${error}`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle successful authorization
|
|
118
|
+
if (token) {
|
|
119
|
+
const authCallback: AuthCallback = { token };
|
|
120
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
121
|
+
res.end(SUCCESS_HTML);
|
|
122
|
+
this.emit('success', authCallback);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle missing token
|
|
127
|
+
console.error('No token received in callback');
|
|
128
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
129
|
+
res.end(ERROR_HTML);
|
|
130
|
+
this.emit('error', new Error('No token received'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Initialize the server by finding an available port
|
|
135
|
+
*/
|
|
136
|
+
async initialize(): Promise<void> {
|
|
137
|
+
if (!this.port) {
|
|
138
|
+
this.port = await this.findAvailablePort();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Start the authentication server
|
|
144
|
+
*/
|
|
145
|
+
async start(): Promise<AuthCallback> {
|
|
146
|
+
return new Promise(async (resolve, reject) => {
|
|
147
|
+
this.resolveCallback = resolve;
|
|
148
|
+
this.rejectCallback = reject;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Initialize (find port) if not already done
|
|
152
|
+
await this.initialize();
|
|
153
|
+
|
|
154
|
+
// Create server
|
|
155
|
+
this.server = http.createServer(this.handleRequest);
|
|
156
|
+
|
|
157
|
+
// Set up event listeners
|
|
158
|
+
this.on('success', (callback: AuthCallback) => {
|
|
159
|
+
this.cleanup();
|
|
160
|
+
this.resolveCallback!(callback);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.on('error', (error: Error) => {
|
|
164
|
+
this.cleanup();
|
|
165
|
+
this.rejectCallback!(error);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Set timeout
|
|
169
|
+
this.timeoutId = setTimeout(() => {
|
|
170
|
+
this.handleTimeout();
|
|
171
|
+
}, DEFAULT_TIMEOUT);
|
|
172
|
+
|
|
173
|
+
// Start server
|
|
174
|
+
await new Promise<void>((resolveServer, rejectServer) => {
|
|
175
|
+
this.server!.listen(this.port, '127.0.0.1', () => {
|
|
176
|
+
console.log(`🔐 Authentication server started on http://localhost:${this.port}`);
|
|
177
|
+
resolveServer();
|
|
178
|
+
});
|
|
179
|
+
this.server!.on('error', rejectServer);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.cleanup();
|
|
184
|
+
reject(error);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Handle authentication timeout
|
|
191
|
+
*/
|
|
192
|
+
private handleTimeout(): void {
|
|
193
|
+
console.log('⏱️ Authentication timed out');
|
|
194
|
+
|
|
195
|
+
// Send timeout page to any open browser windows
|
|
196
|
+
if (this.server) {
|
|
197
|
+
// Note: In a real implementation, we'd track active connections
|
|
198
|
+
// For now, we'll just emit the error
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.emit('error', new Error('Authentication timed out'));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get the authorization URL to open in browser
|
|
206
|
+
*/
|
|
207
|
+
getAuthorizationUrl(): string {
|
|
208
|
+
const redirectUri = `http://localhost:${this.port}${CALLBACK_PATH}`;
|
|
209
|
+
const params = new URLSearchParams({
|
|
210
|
+
redirect_uri: redirectUri,
|
|
211
|
+
state: this.state,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return `${AUTH_BASE_URL}${AUTHORIZE_ENDPOINT}?${params.toString()}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Clean up server resources
|
|
219
|
+
*/
|
|
220
|
+
private cleanup(): void {
|
|
221
|
+
if (this.timeoutId) {
|
|
222
|
+
clearTimeout(this.timeoutId);
|
|
223
|
+
this.timeoutId = undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (this.server) {
|
|
227
|
+
this.server.close();
|
|
228
|
+
this.server = undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.removeAllListeners();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Stop the server
|
|
236
|
+
*/
|
|
237
|
+
stop(): void {
|
|
238
|
+
this.cleanup();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { BlockEntry } from '../registry';
|
|
4
|
+
import { CodeFile } from '../api';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert slug to PascalCase identifier
|
|
8
|
+
*/
|
|
9
|
+
export function slugToIdentifier(slug: string): string {
|
|
10
|
+
return slug
|
|
11
|
+
.split('-')
|
|
12
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
13
|
+
.join('');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert slug to folder name (direct mapping)
|
|
18
|
+
*/
|
|
19
|
+
export function slugToFolderName(slug: string): string {
|
|
20
|
+
return slug;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate that a directory contains a valid block
|
|
25
|
+
*/
|
|
26
|
+
export function validateBlockDirectory(dirPath: string): { valid: boolean; errors: string[] } {
|
|
27
|
+
const errors: string[] = [];
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(dirPath)) {
|
|
30
|
+
errors.push('Block directory does not exist');
|
|
31
|
+
return { valid: false, errors };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const configPath = path.join(dirPath, 'config.ts');
|
|
35
|
+
if (!fs.existsSync(configPath)) {
|
|
36
|
+
errors.push('config.ts file is missing');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check if config.ts exports a valid block configuration
|
|
40
|
+
if (fs.existsSync(configPath)) {
|
|
41
|
+
try {
|
|
42
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
43
|
+
|
|
44
|
+
// Basic validation - check for required exports
|
|
45
|
+
if (!configContent.includes('export') || !configContent.includes('Block')) {
|
|
46
|
+
errors.push('config.ts does not appear to export a valid block configuration');
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
errors.push(`Failed to read config.ts: ${(error as Error).message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { valid: errors.length === 0, errors };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create block directory structure and write files
|
|
58
|
+
*/
|
|
59
|
+
export function createBlockDirectory(
|
|
60
|
+
baseDir: string,
|
|
61
|
+
slug: string,
|
|
62
|
+
files: CodeFile[]
|
|
63
|
+
): { dir: string; configPath: string; componentPath: string } {
|
|
64
|
+
const blockDir = path.join(baseDir, slugToFolderName(slug));
|
|
65
|
+
|
|
66
|
+
// Check if directory already exists
|
|
67
|
+
if (fs.existsSync(blockDir)) {
|
|
68
|
+
throw new Error(`Block directory already exists: ${blockDir}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create directory
|
|
72
|
+
fs.mkdirSync(blockDir, { recursive: true });
|
|
73
|
+
|
|
74
|
+
let configPath = '';
|
|
75
|
+
let componentPath = '';
|
|
76
|
+
|
|
77
|
+
// Write files
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
const filePath = path.join(blockDir, file.name);
|
|
80
|
+
fs.writeFileSync(filePath, file.content);
|
|
81
|
+
|
|
82
|
+
if (file.name === 'config.ts') {
|
|
83
|
+
configPath = filePath;
|
|
84
|
+
} else if (file.name === 'Component.tsx') {
|
|
85
|
+
componentPath = filePath;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!configPath) {
|
|
90
|
+
throw new Error('config.ts file was not found in downloaded files');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!componentPath) {
|
|
94
|
+
throw new Error('Component.tsx file was not found in downloaded files');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { dir: blockDir, configPath, componentPath };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Remove block directory
|
|
102
|
+
*/
|
|
103
|
+
export function removeBlockDirectory(dirPath: string): void {
|
|
104
|
+
if (fs.existsSync(dirPath)) {
|
|
105
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create block entry from metadata and file paths
|
|
111
|
+
*/
|
|
112
|
+
export function createBlockEntry(
|
|
113
|
+
metadata: { id: number; name: string; slug: string; sourceUrl: string },
|
|
114
|
+
dir: string,
|
|
115
|
+
configPath: string,
|
|
116
|
+
componentPath: string,
|
|
117
|
+
checksums: { [filename: string]: string }
|
|
118
|
+
): BlockEntry {
|
|
119
|
+
return {
|
|
120
|
+
id: metadata.id,
|
|
121
|
+
name: metadata.name,
|
|
122
|
+
slug: metadata.slug,
|
|
123
|
+
dir,
|
|
124
|
+
configPath,
|
|
125
|
+
componentPath,
|
|
126
|
+
source: {
|
|
127
|
+
url: metadata.sourceUrl,
|
|
128
|
+
id: metadata.id,
|
|
129
|
+
fetchedAt: new Date().toISOString()
|
|
130
|
+
},
|
|
131
|
+
checksums
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get all block directories in src/blocks
|
|
137
|
+
*/
|
|
138
|
+
export function getBlockDirectories(blocksDir: string = 'src/blocks'): string[] {
|
|
139
|
+
const fullBlocksDir = path.join(process.cwd(), blocksDir);
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(fullBlocksDir)) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const entries = fs.readdirSync(fullBlocksDir, { withFileTypes: true });
|
|
146
|
+
return entries
|
|
147
|
+
.filter(entry => entry.isDirectory())
|
|
148
|
+
.map(entry => path.join(fullBlocksDir, entry.name));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Find blocks by scanning filesystem (fallback when registry is unavailable)
|
|
153
|
+
*/
|
|
154
|
+
export function discoverBlocksFromFilesystem(): Array<{ slug: string; dir: string; hasConfig: boolean }> {
|
|
155
|
+
const blocksDir = path.join(process.cwd(), 'src/blocks');
|
|
156
|
+
|
|
157
|
+
if (!fs.existsSync(blocksDir)) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const blockDirs = getBlockDirectories();
|
|
162
|
+
const blocks: Array<{ slug: string; dir: string; hasConfig: boolean }> = [];
|
|
163
|
+
|
|
164
|
+
for (const dir of blockDirs) {
|
|
165
|
+
const slug = path.basename(dir);
|
|
166
|
+
const configPath = path.join(dir, 'config.ts');
|
|
167
|
+
const hasConfig = fs.existsSync(configPath);
|
|
168
|
+
|
|
169
|
+
blocks.push({ slug, dir, hasConfig });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return blocks;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Ensure src/blocks directory exists
|
|
177
|
+
*/
|
|
178
|
+
export function ensureBlocksDirectory(): string {
|
|
179
|
+
const blocksDir = path.join(process.cwd(), 'src/blocks');
|
|
180
|
+
|
|
181
|
+
if (!fs.existsSync(blocksDir)) {
|
|
182
|
+
fs.mkdirSync(blocksDir, { recursive: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return blocksDir;
|
|
186
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { isAuthenticated } from '../auth';
|
|
3
|
+
import { apiClient } from '../api';
|
|
4
|
+
import { isBlockRegistered, addBlockToRegistry, calculateDirectoryChecksums } from '../registry';
|
|
5
|
+
import {
|
|
6
|
+
ensureBlocksDirectory,
|
|
7
|
+
createBlockDirectory,
|
|
8
|
+
createBlockEntry,
|
|
9
|
+
slugToIdentifier,
|
|
10
|
+
validateBlockDirectory
|
|
11
|
+
} from '../blocks';
|
|
12
|
+
import { updatePageCollectionConfig, updateRenderBlocksComponent, findPagesCollection, findRenderBlocksComponent } from '../ast';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle add block command
|
|
16
|
+
*/
|
|
17
|
+
export async function handleAddBlock(blockUrl: string, options: { force?: boolean; dryRun?: boolean } = {}): Promise<void> {
|
|
18
|
+
console.log('📦 Adding Blok0 Block');
|
|
19
|
+
console.log('====================');
|
|
20
|
+
console.log('');
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Step 1: Authentication check
|
|
24
|
+
console.log('🔐 Checking authentication...');
|
|
25
|
+
const authenticated = await isAuthenticated();
|
|
26
|
+
if (!authenticated) {
|
|
27
|
+
console.error('❌ You are not logged in. Please run `blok0 login` first.');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Step 2: Fetch block data from API
|
|
32
|
+
console.log(`📡 Fetching block from: ${blockUrl}`);
|
|
33
|
+
const { metadata, files } = await apiClient.fetchBlockData(blockUrl);
|
|
34
|
+
console.log(`✅ Found block: "${metadata.name}" (${metadata.slug})`);
|
|
35
|
+
|
|
36
|
+
// Step 3: Check if block is already registered
|
|
37
|
+
if (isBlockRegistered(metadata.slug)) {
|
|
38
|
+
if (!options.force) {
|
|
39
|
+
console.error(`❌ Block "${metadata.slug}" is already installed. Use --force to reinstall.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.log('⚠️ Block already exists, reinstalling...');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (options.dryRun) {
|
|
46
|
+
console.log('🔍 Dry run mode - would perform the following actions:');
|
|
47
|
+
console.log(` - Create directory: src/blocks/${metadata.slug}`);
|
|
48
|
+
console.log(` - Download ${files.length} files`);
|
|
49
|
+
console.log(' - Update Payload config');
|
|
50
|
+
console.log(' - Update RenderBlocks component');
|
|
51
|
+
console.log(' - Register block in blok0-registry.json');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Step 4: Ensure blocks directory exists
|
|
56
|
+
const blocksDir = ensureBlocksDirectory();
|
|
57
|
+
|
|
58
|
+
// Step 5: Create block directory and files
|
|
59
|
+
console.log('📁 Creating block directory and files...');
|
|
60
|
+
const { dir, configPath, componentPath } = createBlockDirectory(blocksDir, metadata.slug, files);
|
|
61
|
+
console.log(`✅ Created block directory: ${path.relative(process.cwd(), dir)}`);
|
|
62
|
+
|
|
63
|
+
// Step 6: Validate created block
|
|
64
|
+
const validation = validateBlockDirectory(dir);
|
|
65
|
+
if (!validation.valid) {
|
|
66
|
+
console.error('❌ Block validation failed:');
|
|
67
|
+
validation.errors.forEach(error => console.error(` - ${error}`));
|
|
68
|
+
// Cleanup on failure
|
|
69
|
+
require('fs').rmSync(dir, { recursive: true, force: true });
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Step 7: Calculate checksums
|
|
74
|
+
const checksums = calculateDirectoryChecksums(dir);
|
|
75
|
+
|
|
76
|
+
// Step 8: Create registry entry
|
|
77
|
+
const blockEntry = createBlockEntry(
|
|
78
|
+
{
|
|
79
|
+
id: metadata.id,
|
|
80
|
+
name: metadata.name,
|
|
81
|
+
slug: metadata.slug,
|
|
82
|
+
sourceUrl: blockUrl
|
|
83
|
+
},
|
|
84
|
+
dir,
|
|
85
|
+
configPath,
|
|
86
|
+
componentPath,
|
|
87
|
+
checksums
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Step 9: Update Pages collection (AST manipulation)
|
|
91
|
+
const pagesCollectionPath = findPagesCollection();
|
|
92
|
+
if (pagesCollectionPath) {
|
|
93
|
+
console.log('🔧 Updating Pages collection...');
|
|
94
|
+
const blockIdentifier = slugToIdentifier(metadata.slug);
|
|
95
|
+
const relativeConfigPath = `@/blocks/${metadata.slug}/config`;
|
|
96
|
+
|
|
97
|
+
updatePageCollectionConfig(pagesCollectionPath, relativeConfigPath, blockIdentifier);
|
|
98
|
+
console.log(`✅ Added ${blockIdentifier} to Pages collection`);
|
|
99
|
+
} else {
|
|
100
|
+
console.warn('⚠️ Could not find Pages collection file. You may need to manually add the block to your collections.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Step 10: Update RenderBlocks component (AST manipulation)
|
|
104
|
+
const renderBlocksPath = findRenderBlocksComponent();
|
|
105
|
+
if (renderBlocksPath) {
|
|
106
|
+
console.log('🔧 Updating RenderBlocks component...');
|
|
107
|
+
const relativeComponentPath = `./${metadata.slug}/Component`;
|
|
108
|
+
|
|
109
|
+
updateRenderBlocksComponent(renderBlocksPath, metadata.slug, relativeComponentPath);
|
|
110
|
+
console.log(`✅ Added ${metadata.slug} component to RenderBlocks`);
|
|
111
|
+
} else {
|
|
112
|
+
console.warn('⚠️ Could not find RenderBlocks component. You may need to manually add the block component.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Step 11: Register block in registry
|
|
116
|
+
console.log('📝 Registering block...');
|
|
117
|
+
addBlockToRegistry(blockEntry);
|
|
118
|
+
console.log('✅ Block registered successfully');
|
|
119
|
+
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log('🎉 Block installation complete!');
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log('Next steps:');
|
|
124
|
+
console.log('1. Review the installed files in src/blocks/' + metadata.slug);
|
|
125
|
+
console.log('2. Test your application to ensure the block works correctly');
|
|
126
|
+
console.log('3. Commit the changes to your repository');
|
|
127
|
+
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('❌ Failed to add block:', (error as Error).message);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|