@mexty/cli 1.0.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.
Files changed (49) hide show
  1. package/README.md +427 -0
  2. package/dist/commands/create.d.ts +7 -0
  3. package/dist/commands/create.d.ts.map +1 -0
  4. package/dist/commands/create.js +80 -0
  5. package/dist/commands/create.js.map +1 -0
  6. package/dist/commands/delete.d.ts +2 -0
  7. package/dist/commands/delete.d.ts.map +1 -0
  8. package/dist/commands/delete.js +54 -0
  9. package/dist/commands/delete.js.map +1 -0
  10. package/dist/commands/fork.d.ts +2 -0
  11. package/dist/commands/fork.d.ts.map +1 -0
  12. package/dist/commands/fork.js +52 -0
  13. package/dist/commands/fork.js.map +1 -0
  14. package/dist/commands/login.d.ts +2 -0
  15. package/dist/commands/login.d.ts.map +1 -0
  16. package/dist/commands/login.js +12 -0
  17. package/dist/commands/login.js.map +1 -0
  18. package/dist/commands/publish.d.ts +2 -0
  19. package/dist/commands/publish.d.ts.map +1 -0
  20. package/dist/commands/publish.js +139 -0
  21. package/dist/commands/publish.js.map +1 -0
  22. package/dist/commands/sync.d.ts +2 -0
  23. package/dist/commands/sync.d.ts.map +1 -0
  24. package/dist/commands/sync.js +140 -0
  25. package/dist/commands/sync.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +60 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/utils/api.d.ts +55 -0
  31. package/dist/utils/api.d.ts.map +1 -0
  32. package/dist/utils/api.js +68 -0
  33. package/dist/utils/api.js.map +1 -0
  34. package/dist/utils/git.d.ts +41 -0
  35. package/dist/utils/git.d.ts.map +1 -0
  36. package/dist/utils/git.js +171 -0
  37. package/dist/utils/git.js.map +1 -0
  38. package/package.json +39 -0
  39. package/src/commands/create.ts +97 -0
  40. package/src/commands/delete.ts +63 -0
  41. package/src/commands/fork.ts +58 -0
  42. package/src/commands/login.ts +104 -0
  43. package/src/commands/publish.ts +159 -0
  44. package/src/commands/sync.ts +284 -0
  45. package/src/index.ts +84 -0
  46. package/src/utils/api.ts +240 -0
  47. package/src/utils/auth.ts +21 -0
  48. package/src/utils/git.ts +194 -0
  49. package/tsconfig.json +24 -0
@@ -0,0 +1,159 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { apiClient } from '../utils/api';
5
+ import { GitManager } from '../utils/git';
6
+ import { createInterface } from 'readline';
7
+ import { syncCommand } from './sync';
8
+ import { requireAuthentication, getAuthenticatedUser } from '../utils/auth';
9
+
10
+ // Simple confirmation function
11
+ async function confirm(question: string): Promise<boolean> {
12
+ return new Promise((resolve) => {
13
+ const rl = createInterface({
14
+ input: process.stdin,
15
+ output: process.stdout
16
+ });
17
+
18
+ rl.question(`${question} (y/N): `, (answer) => {
19
+ rl.close();
20
+ resolve(answer.toLowerCase().trim() === 'y' || answer.toLowerCase().trim() === 'yes');
21
+ });
22
+ });
23
+ }
24
+
25
+ // Extract block ID from package.json or git URL
26
+ async function findBlockId(): Promise<string | null> {
27
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
28
+
29
+ if (fs.existsSync(packageJsonPath)) {
30
+ try {
31
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
32
+
33
+ // Look for block ID in package.json name or description
34
+ if (packageJson.name && packageJson.name.startsWith('block-')) {
35
+ return packageJson.name.replace('block-', '');
36
+ }
37
+
38
+ // Look in description for block ID pattern
39
+ if (packageJson.description) {
40
+ const match = packageJson.description.match(/block[:\s]+([a-f0-9]{24})/i);
41
+ if (match) {
42
+ return match[1];
43
+ }
44
+ }
45
+ } catch (error) {
46
+ console.warn(chalk.yellow('⚠️ Could not parse package.json'));
47
+ }
48
+ }
49
+
50
+ // Try to extract from git remote URL
51
+ try {
52
+ const gitManager = new GitManager();
53
+ const remoteUrl = await gitManager.getRemoteUrl();
54
+
55
+ if (remoteUrl) {
56
+ const match = remoteUrl.match(/block-([a-f0-9]{24})/);
57
+ if (match) {
58
+ return match[1];
59
+ }
60
+ }
61
+ } catch (error) {
62
+ // Ignore git errors
63
+ }
64
+
65
+ return null;
66
+ }
67
+
68
+ export async function publishCommand(): Promise<void> {
69
+ try {
70
+ // Check authentication first
71
+ requireAuthentication();
72
+
73
+ const user = getAuthenticatedUser();
74
+ console.log(chalk.blue('🚀 Publishing block...'));
75
+ console.log(chalk.gray(` User: ${user?.fullName || user?.email || 'Unknown'}`));
76
+
77
+ // Check if we're in a git repository
78
+ const gitManager = new GitManager();
79
+ const isGitRepo = await gitManager.isGitRepository();
80
+
81
+ if (!isGitRepo) {
82
+ console.error(chalk.red('❌ Not a git repository. Please run this command from a block repository.'));
83
+ process.exit(1);
84
+ }
85
+
86
+ // Get repository information
87
+ const repoInfo = await gitManager.getRepositoryInfo();
88
+ console.log(chalk.gray(` Current branch: ${repoInfo.branch}`));
89
+ console.log(chalk.gray(` Remote URL: ${repoInfo.remoteUrl}`));
90
+
91
+ // Find block ID
92
+ const blockId = await findBlockId();
93
+ if (!blockId) {
94
+ console.error(chalk.red('❌ Could not determine block ID from repository.'));
95
+ console.error(chalk.yellow(' Make sure you are in a block repository created with mexty'));
96
+ process.exit(1);
97
+ }
98
+
99
+ console.log(chalk.gray(` Block ID: ${blockId}`));
100
+
101
+ // Check for uncommitted changes
102
+ if (repoInfo.hasChanges) {
103
+ console.log(chalk.yellow('⚠️ You have uncommitted changes.'));
104
+ console.log(chalk.gray(' Please commit your changes before publishing:'));
105
+ console.log(chalk.gray(' git add . && git commit -m "Your commit message"'));
106
+
107
+ const proceed = await confirm('Do you want to continue anyway?');
108
+ if (!proceed) {
109
+ console.log(chalk.yellow('🚫 Publishing cancelled.'));
110
+ return;
111
+ }
112
+ }
113
+
114
+ // Ask user to push changes
115
+ console.log(chalk.blue('\n📤 Push your changes to GitHub:'));
116
+ console.log(chalk.gray(` git push origin ${repoInfo.branch}`));
117
+
118
+ const pushed = await confirm('Have you pushed your changes to GitHub?');
119
+ if (!pushed) {
120
+ console.log(chalk.yellow('🚫 Please push your changes first and then run publish again.'));
121
+ return;
122
+ }
123
+
124
+ // Trigger save and bundle
125
+ console.log(chalk.yellow('📡 Triggering build and bundle process...'));
126
+
127
+ try {
128
+ const result = await apiClient.saveAndBundle({ blockId });
129
+
130
+ console.log(chalk.green('✅ Block published successfully!'));
131
+ console.log(chalk.gray(` Bundle Path: ${result.bundlePath}`));
132
+ console.log(chalk.gray(` Federation URL: ${result.federationUrl}`));
133
+
134
+ if (result.message) {
135
+ console.log(chalk.blue(` ${result.message}`));
136
+ }
137
+
138
+ // Automatically sync registry after successful publish
139
+ console.log(chalk.blue('\n🔄 Auto-syncing registry...'));
140
+ try {
141
+ await syncCommand();
142
+ console.log(chalk.green('✅ Registry synced! Your block is now available as a named component.'));
143
+ } catch (syncError: any) {
144
+ console.warn(chalk.yellow(`⚠️ Registry sync failed: ${syncError.message}`));
145
+ console.log(chalk.gray(' You can manually sync later with: mexty sync'));
146
+ // Don't fail the publish if sync fails - it's not critical
147
+ }
148
+
149
+ } catch (buildError: any) {
150
+ console.error(chalk.red(`❌ Build failed: ${buildError.message}`));
151
+ console.log(chalk.yellow(' Check the server logs for more details.'));
152
+ process.exit(1);
153
+ }
154
+
155
+ } catch (error: any) {
156
+ console.error(chalk.red(`❌ Failed to publish block: ${error.message}`));
157
+ process.exit(1);
158
+ }
159
+ }
@@ -0,0 +1,284 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { apiClient } from '../utils/api';
5
+
6
+ interface BlockRegistryEntry {
7
+ blockId: string;
8
+ componentName: string;
9
+ author?: string; // Username of the author
10
+ title: string;
11
+ description: string;
12
+ version?: string;
13
+ tags?: string[];
14
+ lastUpdated: string;
15
+ }
16
+
17
+ interface BlockRegistry {
18
+ [componentName: string]: BlockRegistryEntry;
19
+ }
20
+
21
+ interface AuthorNamespaceRegistry {
22
+ [author: string]: {
23
+ [componentName: string]: BlockRegistryEntry;
24
+ };
25
+ }
26
+
27
+ export async function syncCommand(): Promise<void> {
28
+ try {
29
+ console.log(chalk.blue('🔄 Syncing block registry...'));
30
+
31
+ // Fetch registry from server
32
+ console.log(chalk.yellow('📡 Fetching registry from server...'));
33
+ const response = await fetch('http://localhost:3001/api/blocks/registry');
34
+
35
+ if (!response.ok) {
36
+ throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
37
+ }
38
+
39
+ const data = await response.json();
40
+ const registry: BlockRegistry = data.registry;
41
+ const authorRegistry: AuthorNamespaceRegistry = data.authorRegistry;
42
+ const meta = data.meta;
43
+
44
+ console.log(chalk.green(`✅ Registry fetched successfully!`));
45
+ console.log(chalk.gray(` Total blocks: ${meta.totalBlocks}`));
46
+ console.log(chalk.gray(` Total components: ${meta.totalComponents}`));
47
+ console.log(chalk.gray(` Total authors: ${meta.totalAuthors || 0}`));
48
+ console.log(chalk.gray(` Last updated: ${meta.lastUpdated}`));
49
+
50
+ if (Object.keys(registry).length === 0) {
51
+ console.log(chalk.yellow('⚠️ No components found in registry.'));
52
+ console.log(chalk.gray(' Make sure you have built some blocks first using "mexty publish"'));
53
+ return;
54
+ }
55
+
56
+ // Display available components
57
+ console.log(chalk.blue('\n📋 Available components (global namespace):'));
58
+ for (const [componentName, entry] of Object.entries(registry)) {
59
+ console.log(chalk.green(` ${componentName}${entry.author ? chalk.gray(` (by ${entry.author})`) : ''}`));
60
+ console.log(chalk.gray(` Block ID: ${entry.blockId}`));
61
+ console.log(chalk.gray(` Title: ${entry.title}`));
62
+ console.log(chalk.gray(` Description: ${entry.description.substring(0, 60)}${entry.description.length > 60 ? '...' : ''}`));
63
+ console.log(chalk.gray(` Tags: ${entry.tags?.join(', ') || 'none'}`));
64
+ console.log('');
65
+ }
66
+
67
+ // Display author namespaces
68
+ if (Object.keys(authorRegistry).length > 0) {
69
+ console.log(chalk.blue('\n👤 Author namespaces:'));
70
+ for (const [author, components] of Object.entries(authorRegistry)) {
71
+ console.log(chalk.cyan(` @${author}:`));
72
+ for (const [componentName, entry] of Object.entries(components)) {
73
+ console.log(chalk.green(` ${componentName}`));
74
+ console.log(chalk.gray(` Block ID: ${entry.blockId}`));
75
+ console.log(chalk.gray(` Title: ${entry.title}`));
76
+ }
77
+ console.log('');
78
+ }
79
+ }
80
+
81
+ // Generate named exports file and author entry files
82
+ const mextBlockPath = findMextBlockPath();
83
+ if (mextBlockPath) {
84
+ console.log(chalk.blue('🔧 Updating mext-block exports...'));
85
+ await generateNamedExports(mextBlockPath, registry, authorRegistry);
86
+ await generateAuthorEntryFiles(mextBlockPath, authorRegistry);
87
+ console.log(chalk.green(`✅ Exports updated in ${mextBlockPath}`));
88
+ console.log(chalk.gray(` Generated ${Object.keys(authorRegistry).length} author entry files`));
89
+ } else {
90
+ console.log(chalk.yellow('⚠️ mext-block package not found locally.'));
91
+ console.log(chalk.gray(' Named exports file not generated.'));
92
+ }
93
+
94
+ console.log(chalk.green('\n🎉 Registry sync completed!'));
95
+
96
+ } catch (error: any) {
97
+ console.error(chalk.red(`❌ Failed to sync registry: ${error.message}`));
98
+ process.exit(1);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Find the mext-block package path
104
+ */
105
+ function findMextBlockPath(): string | null {
106
+ // Look for mext-block in common locations
107
+ const possiblePaths = [
108
+ path.join(process.cwd(), '..', 'mext-block'),
109
+ path.join(process.cwd(), 'mext-block'),
110
+ path.join(process.cwd(), '..', '..', 'mext-block'),
111
+ ];
112
+
113
+ for (const possiblePath of possiblePaths) {
114
+ if (fs.existsSync(path.join(possiblePath, 'package.json'))) {
115
+ try {
116
+ const packageJson = JSON.parse(fs.readFileSync(path.join(possiblePath, 'package.json'), 'utf8'));
117
+ if (packageJson.name === '@mexty/block' || packageJson.name === 'mextblock') {
118
+ return possiblePath;
119
+ }
120
+ } catch (error) {
121
+ // Ignore invalid package.json files
122
+ }
123
+ }
124
+ }
125
+
126
+ return null;
127
+ }
128
+
129
+ /**
130
+ * Generate named exports file for the mext-block package
131
+ */
132
+ async function generateNamedExports(mextBlockPath: string, registry: BlockRegistry, authorRegistry: AuthorNamespaceRegistry): Promise<void> {
133
+ const namedExportsPath = path.join(mextBlockPath, 'src', 'namedExports.ts');
134
+
135
+ // Generate the file content
136
+ const content = `// Auto-generated file - DO NOT EDIT MANUALLY
137
+ // Generated on: ${new Date().toISOString()}
138
+ // Total components: ${Object.keys(registry).length}
139
+ // Total authors: ${Object.keys(authorRegistry).length}
140
+
141
+ import { createNamedBlock } from './components/NamedBlock';
142
+ import { createAuthorBlock } from './components/AuthorBlock';
143
+
144
+ // ===== GLOBAL NAMESPACE COMPONENTS =====
145
+ ${Object.entries(registry).map(([componentName, entry]) =>
146
+ `// ${entry.title}${entry.author ? ` (by ${entry.author})` : ''}
147
+ // Block ID: ${entry.blockId}
148
+ // Description: ${entry.description}
149
+ // Tags: ${entry.tags?.join(', ') || 'none'}
150
+ export const ${componentName} = createNamedBlock('${componentName}');`
151
+ ).join('\n\n')}
152
+
153
+ // Export all global components as an object for convenience
154
+ export const NamedComponents = {
155
+ ${Object.keys(registry).map(componentName => ` ${componentName},`).join('\n')}
156
+ };
157
+
158
+ // Note: Author-specific components are now available via direct imports:
159
+ // import { ComponentName } from '@mexty/block/authorname'
160
+ // Available authors: ${Object.keys(authorRegistry).join(', ')}
161
+
162
+ // Registry metadata
163
+ export const registryMetadata = {
164
+ totalComponents: ${Object.keys(registry).length},
165
+ totalAuthors: ${Object.keys(authorRegistry).length},
166
+ lastGenerated: '${new Date().toISOString()}',
167
+ components: {
168
+ ${Object.entries(registry).map(([componentName, entry]) =>
169
+ ` ${componentName}: {
170
+ blockId: '${entry.blockId}',
171
+ title: '${entry.title}',
172
+ description: '${entry.description.replace(/'/g, "\\'")}',
173
+ author: '${entry.author || ''}',
174
+ tags: [${entry.tags?.map(tag => `'${tag}'`).join(', ') || ''}],
175
+ lastUpdated: '${entry.lastUpdated}'
176
+ },`
177
+ ).join('\n')}
178
+ },
179
+ authors: {
180
+ ${Object.entries(authorRegistry).map(([author, components]) =>
181
+ ` ${author}: {
182
+ ${Object.entries(components).map(([componentName, entry]) =>
183
+ ` ${componentName}: {
184
+ blockId: '${entry.blockId}',
185
+ title: '${entry.title}',
186
+ description: '${entry.description.replace(/'/g, "\\'")}',
187
+ tags: [${entry.tags?.map(tag => `'${tag}'`).join(', ') || ''}],
188
+ lastUpdated: '${entry.lastUpdated}'
189
+ },`
190
+ ).join('\n')}
191
+ },`
192
+ ).join('\n')}
193
+ }
194
+ };
195
+ `;
196
+
197
+ // Ensure directory exists
198
+ const srcDir = path.dirname(namedExportsPath);
199
+ if (!fs.existsSync(srcDir)) {
200
+ fs.mkdirSync(srcDir, { recursive: true });
201
+ }
202
+
203
+ // Write the file
204
+ fs.writeFileSync(namedExportsPath, content, 'utf8');
205
+
206
+ // Update the main index.ts to include these exports
207
+ const indexPath = path.join(mextBlockPath, 'src', 'index.ts');
208
+ if (fs.existsSync(indexPath)) {
209
+ let indexContent = fs.readFileSync(indexPath, 'utf8');
210
+
211
+ // Add export for named exports if not already present
212
+ if (!indexContent.includes('export * from \'./namedExports\'')) {
213
+ indexContent += '\n// Auto-generated named exports\nexport * from \'./namedExports\';\n';
214
+ fs.writeFileSync(indexPath, indexContent, 'utf8');
215
+ }
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Generate author-specific entry files for direct imports
221
+ */
222
+ async function generateAuthorEntryFiles(mextBlockPath: string, authorRegistry: AuthorNamespaceRegistry): Promise<void> {
223
+ const authorsDir = path.join(mextBlockPath, 'src', 'authors');
224
+
225
+ // Clean up existing author files
226
+ if (fs.existsSync(authorsDir)) {
227
+ fs.rmSync(authorsDir, { recursive: true, force: true });
228
+ }
229
+
230
+ // Create authors directory
231
+ fs.mkdirSync(authorsDir, { recursive: true });
232
+
233
+ // Generate an entry file for each author
234
+ for (const [author, components] of Object.entries(authorRegistry)) {
235
+ const authorDir = path.join(authorsDir, author);
236
+ fs.mkdirSync(authorDir, { recursive: true });
237
+
238
+ const authorEntryPath = path.join(authorDir, 'index.ts');
239
+
240
+ // Generate the author's entry file content
241
+ const content = `// Auto-generated author entry file for ${author}
242
+ // Generated on: ${new Date().toISOString()}
243
+ // Total components: ${Object.keys(components).length}
244
+
245
+ import { createAuthorBlock } from '../../components/AuthorBlock';
246
+
247
+ ${Object.entries(components).map(([componentName, entry]) =>
248
+ `// ${entry.title}
249
+ // Block ID: ${entry.blockId}
250
+ // Description: ${entry.description}
251
+ // Tags: ${entry.tags?.join(', ') || 'none'}
252
+ export const ${componentName} = createAuthorBlock('${author}', '${componentName}');`
253
+ ).join('\n\n')}
254
+
255
+ // Export all components as default for convenience
256
+ export default {
257
+ ${Object.keys(components).map(componentName => ` ${componentName},`).join('\n')}
258
+ };
259
+
260
+ // Author metadata
261
+ export const authorMetadata = {
262
+ author: '${author}',
263
+ totalComponents: ${Object.keys(components).length},
264
+ lastGenerated: '${new Date().toISOString()}',
265
+ components: {
266
+ ${Object.entries(components).map(([componentName, entry]) =>
267
+ ` ${componentName}: {
268
+ blockId: '${entry.blockId}',
269
+ title: '${entry.title}',
270
+ description: '${entry.description.replace(/'/g, "\\'")}',
271
+ tags: [${entry.tags?.map(tag => `'${tag}'`).join(', ') || ''}],
272
+ lastUpdated: '${entry.lastUpdated}'
273
+ },`
274
+ ).join('\n')}
275
+ }
276
+ };
277
+ `;
278
+
279
+ // Write the author entry file
280
+ fs.writeFileSync(authorEntryPath, content, 'utf8');
281
+
282
+ console.log(chalk.gray(` Created entry file for ${author} (${Object.keys(components).length} components)`));
283
+ }
284
+ }
package/src/index.ts ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { loginCommand } from './commands/login';
6
+ import { createCommand } from './commands/create';
7
+ import { forkCommand } from './commands/fork';
8
+ import { deleteCommand } from './commands/delete';
9
+ import { publishCommand } from './commands/publish';
10
+ import { syncCommand } from './commands/sync';
11
+ import { apiClient } from './utils/api';
12
+
13
+ const program = new Command();
14
+
15
+ // CLI Configuration
16
+ program
17
+ .name('mexty')
18
+ .description('MEXT CLI for managing React microfrontend blocks and components')
19
+ .version('1.0.0');
20
+
21
+ // Add commands
22
+ program
23
+ .command('login')
24
+ .description('Authenticate with MEXT')
25
+ .action(loginCommand);
26
+
27
+ program
28
+ .command('logout')
29
+ .description('Logout from MEXT')
30
+ .action(async () => {
31
+ try {
32
+ if (!apiClient.isAuthenticated()) {
33
+ console.log(chalk.yellow('⚠️ You are not logged in'));
34
+ return;
35
+ }
36
+
37
+ await apiClient.logout();
38
+ console.log(chalk.green('✅ Logged out successfully'));
39
+ } catch (error: any) {
40
+ console.error(chalk.red(`❌ Logout failed: ${error.message}`));
41
+ }
42
+ });
43
+
44
+ program
45
+ .command('create <name>')
46
+ .description('Create a new React microfrontend block')
47
+ .option('-d, --description <description>', 'Block description')
48
+ .option('-t, --type <type>', 'Block type', 'custom')
49
+ .action(createCommand);
50
+
51
+ program
52
+ .command('fork <blockId>')
53
+ .description('Fork an existing block and clone its repository')
54
+ .action(forkCommand);
55
+
56
+ program
57
+ .command('delete <blockId>')
58
+ .description('Delete a block (requires ownership)')
59
+ .action(deleteCommand);
60
+
61
+ program
62
+ .command('publish')
63
+ .description('Publish current block with automatic bundling')
64
+ .action(publishCommand);
65
+
66
+ program
67
+ .command('sync')
68
+ .description('Sync block registry and update typed exports')
69
+ .action(syncCommand);
70
+
71
+ // Error handling
72
+ program.on('command:*', () => {
73
+ console.error(chalk.red(`Invalid command: ${program.args.join(' ')}`));
74
+ console.log(chalk.yellow('See --help for a list of available commands.'));
75
+ process.exit(1);
76
+ });
77
+
78
+ // Parse arguments
79
+ program.parse(process.argv);
80
+
81
+ // Show help if no command provided
82
+ if (!process.argv.slice(2).length) {
83
+ program.outputHelp();
84
+ }