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.
@@ -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
+ }