blok0 0.1.0 → 0.1.1
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/detectors.ts +22 -22
- package/src/handlers/add-block.ts +132 -0
- package/src/handlers/generate.ts +62 -62
- 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
- package/tsconfig.json +16 -16
|
@@ -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
|
+
}
|
package/src/detectors.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
|
|
4
|
-
export function checkEmptyDirectory(): boolean {
|
|
5
|
-
const cwd = process.cwd();
|
|
6
|
-
|
|
7
|
-
const pkgPath = join(cwd, 'package.json');
|
|
8
|
-
const configJs = join(cwd, 'payload.config.js');
|
|
9
|
-
const configTs = join(cwd, 'payload.config.ts');
|
|
10
|
-
|
|
11
|
-
if (existsSync(pkgPath)) {
|
|
12
|
-
console.error('Error: package.json already exists. Please run in an empty directory.');
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (existsSync(configJs) || existsSync(configTs)) {
|
|
17
|
-
console.error('Error: Payload config file already exists. Please run in an empty directory.');
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
export function checkEmptyDirectory(): boolean {
|
|
5
|
+
const cwd = process.cwd();
|
|
6
|
+
|
|
7
|
+
const pkgPath = join(cwd, 'package.json');
|
|
8
|
+
const configJs = join(cwd, 'payload.config.js');
|
|
9
|
+
const configTs = join(cwd, 'payload.config.ts');
|
|
10
|
+
|
|
11
|
+
if (existsSync(pkgPath)) {
|
|
12
|
+
console.error('Error: package.json already exists. Please run in an empty directory.');
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (existsSync(configJs) || existsSync(configTs)) {
|
|
17
|
+
console.error('Error: Payload config file already exists. Please run in an empty directory.');
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return true;
|
|
22
|
+
}
|