@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,240 @@
1
+ import axios, { AxiosInstance, AxiosResponse } from 'axios';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+
7
+ export interface Block {
8
+ _id: string;
9
+ blockType: string;
10
+ title: string;
11
+ description: string;
12
+ gitUrl?: string;
13
+ courseId?: string;
14
+ courseName?: string;
15
+ allowedBrickTypes: string[];
16
+ allowedBlockTypes?: string[];
17
+ scope: ('library' | 'user-store' | 'published-store')[];
18
+ content: any[];
19
+ bundlePath?: string;
20
+ federationUrl?: string;
21
+ buildStatus?: 'pending' | 'building' | 'success' | 'failed';
22
+ buildError?: string;
23
+ lastBuilt?: Date;
24
+ createdAt: Date;
25
+ updatedAt: Date;
26
+ }
27
+
28
+ export interface CreateBlockRequest {
29
+ blockType: string;
30
+ title: string;
31
+ description: string;
32
+ gitUrl?: string;
33
+ courseId?: string;
34
+ courseName?: string;
35
+ allowedBrickTypes: string[];
36
+ allowedBlockTypes?: string[];
37
+ scope: string[];
38
+ content?: any[];
39
+ }
40
+
41
+ export interface ForkBlockRequest {
42
+ blockId: string;
43
+ title?: string;
44
+ description?: string;
45
+ }
46
+
47
+ export interface SaveAndBundleRequest {
48
+ blockId: string;
49
+ }
50
+
51
+ export interface AuthResponse {
52
+ success: boolean;
53
+ message: string;
54
+ token?: string;
55
+ user?: {
56
+ id: string;
57
+ email: string;
58
+ fullName?: string;
59
+ occupation?: string;
60
+ isProfileComplete: boolean;
61
+ };
62
+ }
63
+
64
+ export interface LoginRequest {
65
+ email: string;
66
+ otp: string;
67
+ }
68
+
69
+ class ApiClient {
70
+ private client: AxiosInstance;
71
+ private baseUrl: string;
72
+ private tokenPath: string;
73
+
74
+ constructor(baseUrl: string = 'http://localhost:3001') {
75
+ this.baseUrl = baseUrl;
76
+ this.tokenPath = path.join(os.homedir(), '.mext', 'auth.json');
77
+
78
+ this.client = axios.create({
79
+ baseURL: baseUrl,
80
+ timeout: 30000,
81
+ headers: {
82
+ 'Content-Type': 'application/json',
83
+ },
84
+ });
85
+
86
+ // Add request interceptor to include auth token
87
+ this.client.interceptors.request.use(
88
+ (config) => {
89
+ const token = this.getStoredToken();
90
+ if (token) {
91
+ config.headers.Authorization = `Bearer ${token}`;
92
+ }
93
+ return config;
94
+ },
95
+ (error) => Promise.reject(error)
96
+ );
97
+
98
+ // Add response interceptor for error handling
99
+ this.client.interceptors.response.use(
100
+ (response) => response,
101
+ (error) => {
102
+ if (error.response?.status === 401) {
103
+ console.error(chalk.red('Authentication required. Please login first: mexty login'));
104
+ this.clearStoredToken();
105
+ } else if (error.response) {
106
+ console.error(chalk.red(`API Error ${error.response.status}: ${error.response.data?.error || error.message}`));
107
+ } else if (error.request) {
108
+ console.error(chalk.red('Network Error: Could not reach MEXT server'));
109
+ console.error(chalk.yellow(`Make sure the server is running at ${this.baseUrl}`));
110
+ } else {
111
+ console.error(chalk.red(`Request Error: ${error.message}`));
112
+ }
113
+ throw error;
114
+ }
115
+ );
116
+ }
117
+
118
+ private getStoredToken(): string | null {
119
+ try {
120
+ if (fs.existsSync(this.tokenPath)) {
121
+ const authData = JSON.parse(fs.readFileSync(this.tokenPath, 'utf8'));
122
+ return authData.token || null;
123
+ }
124
+ } catch (error) {
125
+ // Ignore errors reading token file
126
+ }
127
+ return null;
128
+ }
129
+
130
+ private storeToken(token: string, user: any): void {
131
+ try {
132
+ const authDir = path.dirname(this.tokenPath);
133
+ if (!fs.existsSync(authDir)) {
134
+ fs.mkdirSync(authDir, { recursive: true });
135
+ }
136
+
137
+ const authData = {
138
+ token,
139
+ user,
140
+ timestamp: new Date().toISOString()
141
+ };
142
+
143
+ fs.writeFileSync(this.tokenPath, JSON.stringify(authData, null, 2));
144
+ } catch (error: any) {
145
+ console.warn(chalk.yellow(`Warning: Could not store auth token: ${error.message}`));
146
+ }
147
+ }
148
+
149
+ private clearStoredToken(): void {
150
+ try {
151
+ if (fs.existsSync(this.tokenPath)) {
152
+ fs.unlinkSync(this.tokenPath);
153
+ }
154
+ } catch (error) {
155
+ // Ignore errors clearing token file
156
+ }
157
+ }
158
+
159
+ public isAuthenticated(): boolean {
160
+ return this.getStoredToken() !== null;
161
+ }
162
+
163
+ public getStoredUser(): any {
164
+ try {
165
+ if (fs.existsSync(this.tokenPath)) {
166
+ const authData = JSON.parse(fs.readFileSync(this.tokenPath, 'utf8'));
167
+ return authData.user || null;
168
+ }
169
+ } catch (error) {
170
+ // Ignore errors reading user data
171
+ }
172
+ return null;
173
+ }
174
+
175
+ async createBlock(data: CreateBlockRequest): Promise<Block> {
176
+ const response: AxiosResponse<Block> = await this.client.post('/api/blocks', data);
177
+ return response.data;
178
+ }
179
+
180
+ async forkBlock(data: ForkBlockRequest): Promise<Block> {
181
+ const response: AxiosResponse<Block> = await this.client.post('/api/blocks/fork', data);
182
+ return response.data;
183
+ }
184
+
185
+ async deleteBlock(blockId: string): Promise<void> {
186
+ await this.client.delete(`/api/blocks/${blockId}`);
187
+ }
188
+
189
+ async getBlock(blockId: string): Promise<Block> {
190
+ const response: AxiosResponse<Block> = await this.client.get(`/api/blocks/${blockId}`);
191
+ return response.data;
192
+ }
193
+
194
+ async saveAndBundle(data: SaveAndBundleRequest): Promise<any> {
195
+ const response = await this.client.post('/api/blocks/save-and-bundle', data);
196
+ return response.data;
197
+ }
198
+
199
+ async healthCheck(): Promise<boolean> {
200
+ try {
201
+ await this.client.get('/api/health');
202
+ return true;
203
+ } catch (error) {
204
+ return false;
205
+ }
206
+ }
207
+
208
+ async requestOTP(email: string): Promise<AuthResponse> {
209
+ const response: AxiosResponse<AuthResponse> = await this.client.post('/api/auth/request-otp', { email });
210
+ return response.data;
211
+ }
212
+
213
+ async verifyOTP(email: string, otp: string): Promise<AuthResponse> {
214
+ const response: AxiosResponse<AuthResponse> = await this.client.post('/api/auth/verify-otp', { email, otp });
215
+ const data = response.data;
216
+
217
+ if (data.success && data.token && data.user) {
218
+ this.storeToken(data.token, data.user);
219
+ }
220
+
221
+ return data;
222
+ }
223
+
224
+ async logout(): Promise<void> {
225
+ try {
226
+ await this.client.post('/api/auth/logout');
227
+ } catch (error) {
228
+ // Ignore logout errors
229
+ } finally {
230
+ this.clearStoredToken();
231
+ }
232
+ }
233
+
234
+ setBaseUrl(url: string): void {
235
+ this.baseUrl = url;
236
+ this.client.defaults.baseURL = url;
237
+ }
238
+ }
239
+
240
+ export const apiClient = new ApiClient();
@@ -0,0 +1,21 @@
1
+ import chalk from 'chalk';
2
+ import { apiClient } from './api';
3
+
4
+ export function checkAuthentication(): boolean {
5
+ if (!apiClient.isAuthenticated()) {
6
+ console.error(chalk.red('❌ Authentication required'));
7
+ console.log(chalk.yellow(' Please login first: mexty login'));
8
+ return false;
9
+ }
10
+ return true;
11
+ }
12
+
13
+ export function getAuthenticatedUser(): any {
14
+ return apiClient.getStoredUser();
15
+ }
16
+
17
+ export function requireAuthentication(): void {
18
+ if (!checkAuthentication()) {
19
+ process.exit(1);
20
+ }
21
+ }
@@ -0,0 +1,194 @@
1
+ import simpleGit, { SimpleGit } from 'simple-git';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import chalk from 'chalk';
5
+
6
+ // Simple spinner implementation since ora v5 has import issues
7
+ class SimpleSpinner {
8
+ private message: string;
9
+ private interval: NodeJS.Timeout | null = null;
10
+ private frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
11
+ private currentFrame = 0;
12
+
13
+ constructor(message: string) {
14
+ this.message = message;
15
+ }
16
+
17
+ start(): this {
18
+ process.stdout.write(this.message);
19
+ this.interval = setInterval(() => {
20
+ process.stdout.write(`\r${this.frames[this.currentFrame]} ${this.message}`);
21
+ this.currentFrame = (this.currentFrame + 1) % this.frames.length;
22
+ }, 80);
23
+ return this;
24
+ }
25
+
26
+ succeed(message: string): void {
27
+ this.stop();
28
+ console.log(`\r✅ ${message}`);
29
+ }
30
+
31
+ fail(message: string): void {
32
+ this.stop();
33
+ console.log(`\r❌ ${message}`);
34
+ }
35
+
36
+ private stop(): void {
37
+ if (this.interval) {
38
+ clearInterval(this.interval);
39
+ this.interval = null;
40
+ }
41
+ process.stdout.write('\r');
42
+ }
43
+ }
44
+
45
+ function ora(message: string): SimpleSpinner {
46
+ return new SimpleSpinner(message);
47
+ }
48
+
49
+ export class GitManager {
50
+ private git: SimpleGit;
51
+
52
+ constructor(cwd?: string) {
53
+ this.git = simpleGit(cwd);
54
+ }
55
+
56
+ /**
57
+ * Clone a repository to a local directory
58
+ */
59
+ async cloneRepository(repoUrl: string, targetDir: string): Promise<void> {
60
+ const spinner = ora(`Cloning repository from ${repoUrl}...`).start();
61
+
62
+ try {
63
+ // Ensure target directory doesn't exist
64
+ if (fs.existsSync(targetDir)) {
65
+ spinner.fail(chalk.red(`Directory ${targetDir} already exists`));
66
+ throw new Error(`Directory ${targetDir} already exists`);
67
+ }
68
+
69
+ // Clone the repository
70
+ await this.git.clone(repoUrl, targetDir);
71
+
72
+ spinner.succeed(chalk.green(`Repository cloned to ${targetDir}`));
73
+ } catch (error: any) {
74
+ spinner.fail(chalk.red(`Failed to clone repository: ${error.message}`));
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Check if current directory is a Git repository
81
+ */
82
+ async isGitRepository(dir?: string): Promise<boolean> {
83
+ try {
84
+ const git = dir ? simpleGit(dir) : this.git;
85
+ await git.status();
86
+ return true;
87
+ } catch (error) {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Get the current repository's remote URL
94
+ */
95
+ async getRemoteUrl(dir?: string): Promise<string | null> {
96
+ try {
97
+ const git = dir ? simpleGit(dir) : this.git;
98
+ const remotes = await git.getRemotes(true);
99
+ const origin = remotes.find(remote => remote.name === 'origin');
100
+ return origin?.refs?.fetch || null;
101
+ } catch (error) {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Check if there are uncommitted changes
108
+ */
109
+ async hasUncommittedChanges(dir?: string): Promise<boolean> {
110
+ try {
111
+ const git = dir ? simpleGit(dir) : this.git;
112
+ const status = await git.status();
113
+ return !status.isClean();
114
+ } catch (error) {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Push current branch to remote
121
+ */
122
+ async pushToRemote(dir?: string): Promise<void> {
123
+ const spinner = ora('Pushing changes to remote repository...').start();
124
+
125
+ try {
126
+ const git = dir ? simpleGit(dir) : this.git;
127
+
128
+ // Get current branch
129
+ const status = await git.status();
130
+ const currentBranch = status.current;
131
+
132
+ if (!currentBranch) {
133
+ throw new Error('No current branch found');
134
+ }
135
+
136
+ // Push to remote
137
+ await git.push('origin', currentBranch);
138
+
139
+ spinner.succeed(chalk.green('Changes pushed to remote repository'));
140
+ } catch (error: any) {
141
+ spinner.fail(chalk.red(`Failed to push changes: ${error.message}`));
142
+ throw error;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Get repository information
148
+ */
149
+ async getRepositoryInfo(dir?: string): Promise<{
150
+ branch: string;
151
+ remoteUrl: string | null;
152
+ hasChanges: boolean;
153
+ }> {
154
+ const git = dir ? simpleGit(dir) : this.git;
155
+
156
+ const status = await git.status();
157
+ const remoteUrl = await this.getRemoteUrl(dir);
158
+
159
+ return {
160
+ branch: status.current || 'unknown',
161
+ remoteUrl,
162
+ hasChanges: !status.isClean()
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Extract repository name from URL
168
+ */
169
+ static extractRepoName(gitUrl: string): string {
170
+ // Handle both SSH and HTTPS URLs
171
+ const match = gitUrl.match(/\/([^\/]+?)(?:\.git)?$/);
172
+ if (match) {
173
+ return match[1];
174
+ }
175
+
176
+ // Fallback: use the last part of the URL
177
+ const parts = gitUrl.split('/');
178
+ return parts[parts.length - 1].replace('.git', '');
179
+ }
180
+
181
+ /**
182
+ * Validate Git URL format
183
+ */
184
+ static isValidGitUrl(url: string): boolean {
185
+ const patterns = [
186
+ /^https:\/\/github\.com\/[\w\-\.]+\/[\w\-\.]+(?:\.git)?$/,
187
+ /^git@github\.com:[\w\-\.]+\/[\w\-\.]+(?:\.git)?$/,
188
+ /^https:\/\/gitlab\.com\/[\w\-\.]+\/[\w\-\.]+(?:\.git)?$/,
189
+ /^git@gitlab\.com:[\w\-\.]+\/[\w\-\.]+(?:\.git)?$/
190
+ ];
191
+
192
+ return patterns.some(pattern => pattern.test(url));
193
+ }
194
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": [
18
+ "src/**/*"
19
+ ],
20
+ "exclude": [
21
+ "node_modules",
22
+ "dist"
23
+ ]
24
+ }