@hapico/cli 0.0.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/bin/index.js +460 -0
- package/bun.lock +177 -0
- package/dist/index.js +460 -0
- package/index.ts +553 -0
- package/package.json +36 -0
- package/tsconfig.json +14 -0
package/index.ts
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import unzipper from "unzipper";
|
|
8
|
+
import ora, { Ora } from "ora";
|
|
9
|
+
import { WebSocket } from "ws";
|
|
10
|
+
import * as Babel from "@babel/standalone";
|
|
11
|
+
import QRCode from "qrcode-terminal";
|
|
12
|
+
import open from "open";
|
|
13
|
+
|
|
14
|
+
// Directory to store the token and project config
|
|
15
|
+
const CONFIG_DIR = path.join(
|
|
16
|
+
process.env.HOME || process.env.USERPROFILE || ".",
|
|
17
|
+
".hapico"
|
|
18
|
+
);
|
|
19
|
+
const TOKEN_FILE = path.join(CONFIG_DIR, "auth_token.json");
|
|
20
|
+
|
|
21
|
+
// Ensure config directory exists
|
|
22
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
23
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Function to save token
|
|
27
|
+
const saveToken = (token: string) => {
|
|
28
|
+
fs.writeFileSync(
|
|
29
|
+
TOKEN_FILE,
|
|
30
|
+
JSON.stringify({ access_token: token }, null, 2),
|
|
31
|
+
{
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Function to get stored token
|
|
38
|
+
const getStoredToken = (): string | null => {
|
|
39
|
+
if (fs.existsSync(TOKEN_FILE)) {
|
|
40
|
+
const data = fs.readFileSync(TOKEN_FILE, { encoding: "utf8" });
|
|
41
|
+
const json = JSON.parse(data);
|
|
42
|
+
return json.access_token || null;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Function to save project ID
|
|
48
|
+
const saveProjectId = (projectDir: string, id: string) => {
|
|
49
|
+
const configFile = path.join(projectDir, "hapico.config.json");
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
configFile,
|
|
52
|
+
JSON.stringify({ projectId: id }, null, 2),
|
|
53
|
+
{ encoding: "utf8" }
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Function to get stored project ID
|
|
58
|
+
const getStoredProjectId = (projectDir: string): string | null => {
|
|
59
|
+
const configFile = path.join(projectDir, "hapico.config.json");
|
|
60
|
+
if (fs.existsSync(configFile)) {
|
|
61
|
+
const data = fs.readFileSync(configFile, { encoding: "utf8" });
|
|
62
|
+
const json = JSON.parse(data);
|
|
63
|
+
return json.projectId || null;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Khởi tạo cache bằng Map để lưu trữ kết quả compile
|
|
69
|
+
const compileCache = new Map<string, string>();
|
|
70
|
+
|
|
71
|
+
interface FileContent {
|
|
72
|
+
path: string;
|
|
73
|
+
content: string;
|
|
74
|
+
es5?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ApiResponse {
|
|
78
|
+
data: {
|
|
79
|
+
files?: FileContent[];
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface RoomStateData {
|
|
84
|
+
[key: string]: any;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface WebSocketMessage {
|
|
88
|
+
type: string;
|
|
89
|
+
state?: RoomStateData;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const compileES5 = (code: string, filePath?: string) => {
|
|
93
|
+
if (filePath && !filePath.endsWith(".ts") && !filePath.endsWith(".tsx")) {
|
|
94
|
+
return code;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (compileCache.has(code)) {
|
|
98
|
+
return compileCache.get(code)!;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const result = Babel.transform(code, {
|
|
102
|
+
presets: [
|
|
103
|
+
["env", { targets: { esmodules: false } }],
|
|
104
|
+
"typescript",
|
|
105
|
+
"react",
|
|
106
|
+
],
|
|
107
|
+
filename: "file.tsx",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
compileCache.set(code, result.code || "");
|
|
111
|
+
return result.code;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
class FileManager {
|
|
118
|
+
private basePath: string;
|
|
119
|
+
private onFileChange?: (filePath: string, content: string) => void;
|
|
120
|
+
private watcher?: fs.FSWatcher;
|
|
121
|
+
|
|
122
|
+
constructor(
|
|
123
|
+
basePath: string,
|
|
124
|
+
onFileChange?: (filePath: string, content: string) => void
|
|
125
|
+
) {
|
|
126
|
+
this.basePath = basePath;
|
|
127
|
+
this.onFileChange = onFileChange;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public syncFiles(files: FileContent[]): void {
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
this.writeFile(file);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public listFiles(): FileContent[] {
|
|
137
|
+
const files: FileContent[] = [];
|
|
138
|
+
const traverseDirectory = (dir: string) => {
|
|
139
|
+
fs.readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
|
140
|
+
const fullPath = path.join(dir, entry.name);
|
|
141
|
+
if (entry.isDirectory()) {
|
|
142
|
+
traverseDirectory(fullPath);
|
|
143
|
+
} else if (entry.isFile()) {
|
|
144
|
+
const content = fs.readFileSync(fullPath, { encoding: "utf8" });
|
|
145
|
+
files.push({
|
|
146
|
+
path: fullPath.replace(this.basePath + path.sep, "./"),
|
|
147
|
+
content,
|
|
148
|
+
es5: compileES5(content, fullPath) ?? "",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
traverseDirectory(this.basePath);
|
|
154
|
+
return files;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private writeFile(file: FileContent): void {
|
|
158
|
+
try {
|
|
159
|
+
const fullPath = path.join(this.basePath, file.path);
|
|
160
|
+
const dir = path.dirname(fullPath);
|
|
161
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
162
|
+
|
|
163
|
+
let hasChanged = true;
|
|
164
|
+
if (fs.existsSync(fullPath)) {
|
|
165
|
+
try {
|
|
166
|
+
const existingContent = fs.readFileSync(fullPath, {
|
|
167
|
+
encoding: "utf8",
|
|
168
|
+
});
|
|
169
|
+
hasChanged = existingContent !== file.content;
|
|
170
|
+
} catch (readError) {
|
|
171
|
+
console.warn(
|
|
172
|
+
`Warning: Could not read existing file ${fullPath}, treating as changed:`,
|
|
173
|
+
readError
|
|
174
|
+
);
|
|
175
|
+
hasChanged = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (hasChanged) {
|
|
180
|
+
fs.writeFileSync(fullPath, file.content, { encoding: "utf8" });
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error(`Error processing file ${file.path}:`, error);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private startWatching(): void {
|
|
189
|
+
if (!this.onFileChange) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
this.watcher = fs.watch(
|
|
195
|
+
this.basePath,
|
|
196
|
+
{ recursive: true },
|
|
197
|
+
(eventType, filename) => {
|
|
198
|
+
if (filename && eventType === "change") {
|
|
199
|
+
const fullPath = path.join(this.basePath, filename);
|
|
200
|
+
try {
|
|
201
|
+
const stats = fs.statSync(fullPath);
|
|
202
|
+
if (stats.isFile()) {
|
|
203
|
+
const content = fs.readFileSync(fullPath, { encoding: "utf8" });
|
|
204
|
+
this.onFileChange!(fullPath, content);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.warn(`Error reading changed file ${fullPath}:`, error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
// console.error(`Error setting up file watcher for ${this.basePath}:`, error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public setOnFileChange(
|
|
218
|
+
callback: (filePath: string, content: string) => void
|
|
219
|
+
): void {
|
|
220
|
+
this.onFileChange = callback;
|
|
221
|
+
if (!this.watcher && (callback as any)) {
|
|
222
|
+
this.startWatching();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public stopWatching(): void {
|
|
227
|
+
if (this.watcher) {
|
|
228
|
+
this.watcher.close();
|
|
229
|
+
this.watcher = undefined;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
class RoomState {
|
|
235
|
+
private roomId: string;
|
|
236
|
+
private state: RoomStateData;
|
|
237
|
+
private isConnected: boolean;
|
|
238
|
+
private ws: WebSocket | null;
|
|
239
|
+
private reconnectTimeout: NodeJS.Timeout | null;
|
|
240
|
+
private reconnectAttempts: number;
|
|
241
|
+
|
|
242
|
+
public files: FileContent[] = [];
|
|
243
|
+
|
|
244
|
+
constructor(roomId: string) {
|
|
245
|
+
this.roomId = roomId;
|
|
246
|
+
this.state = {};
|
|
247
|
+
this.isConnected = false;
|
|
248
|
+
this.ws = null;
|
|
249
|
+
this.reconnectTimeout = null;
|
|
250
|
+
this.reconnectAttempts = 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
connect(onConnected?: () => void): void {
|
|
254
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
|
|
255
|
+
|
|
256
|
+
if (this.reconnectTimeout) {
|
|
257
|
+
clearTimeout(this.reconnectTimeout);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this.ws = new WebSocket(
|
|
261
|
+
`https://base.myworkbeast.com/ws?room=${this.roomId}`
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
this.ws.on("open", () => {
|
|
265
|
+
this.isConnected = true;
|
|
266
|
+
this.reconnectAttempts = 0;
|
|
267
|
+
onConnected?.();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
this.ws.on("close", () => {
|
|
271
|
+
this.isConnected = false;
|
|
272
|
+
|
|
273
|
+
this.reconnectAttempts++;
|
|
274
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
275
|
+
console.log(`Attempting to reconnect in ${delay / 1000}s...`);
|
|
276
|
+
|
|
277
|
+
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
this.ws.on("message", (data: Buffer) => {
|
|
281
|
+
try {
|
|
282
|
+
const message: WebSocketMessage = JSON.parse(data.toString());
|
|
283
|
+
if (message.type === "state" && message.state) {
|
|
284
|
+
this.state = message.state;
|
|
285
|
+
const view = this.state.view as Array<FileContent>;
|
|
286
|
+
this.files = view;
|
|
287
|
+
}
|
|
288
|
+
} catch (e) {
|
|
289
|
+
console.error("Error processing message:", e);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
this.ws.on("error", (err: Error) => {
|
|
294
|
+
console.error("WebSocket error:", err);
|
|
295
|
+
this.ws?.close();
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
updateState(key: string, value: any): void {
|
|
300
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
301
|
+
this.ws.send(
|
|
302
|
+
JSON.stringify({
|
|
303
|
+
type: "update",
|
|
304
|
+
state: { [key]: value },
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
} else {
|
|
308
|
+
console.log("Cannot update state: Not connected");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
disconnect(): void {
|
|
313
|
+
if (this.reconnectTimeout) {
|
|
314
|
+
clearTimeout(this.reconnectTimeout);
|
|
315
|
+
}
|
|
316
|
+
if (this.ws) {
|
|
317
|
+
this.ws.removeAllListeners("close");
|
|
318
|
+
this.ws.close();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
getState(): RoomStateData {
|
|
323
|
+
return this.state;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
getIsConnected(): boolean {
|
|
327
|
+
return this.isConnected;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
program.version("1.0.0").description("Hapico CLI for project management");
|
|
332
|
+
|
|
333
|
+
program
|
|
334
|
+
.command("clone <id>")
|
|
335
|
+
.description("Clone a project by ID")
|
|
336
|
+
.action(async (id: string) => {
|
|
337
|
+
const token = getStoredToken();
|
|
338
|
+
if (!token) {
|
|
339
|
+
console.error("You need to login first. Use 'hapico login' command.");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const projectDir: string = path.resolve(process.cwd(), id);
|
|
343
|
+
if (fs.existsSync(projectDir)) {
|
|
344
|
+
console.error(`Project directory "${id}" already exists.`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let files: FileContent[] = [];
|
|
349
|
+
const apiSpinner: Ora = ora("Fetching project data...").start();
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const response: ApiResponse = await axios.get(
|
|
353
|
+
`https://base.myworkbeast.com/api/views/${id}`
|
|
354
|
+
);
|
|
355
|
+
files = response.data.files || [];
|
|
356
|
+
apiSpinner.succeed("Project data fetched successfully!");
|
|
357
|
+
|
|
358
|
+
const templateSpinner: Ora = ora("Downloading template...").start();
|
|
359
|
+
const TEMPLATE_URL: string =
|
|
360
|
+
"https://files.hcm04.vstorage.vngcloud.vn/assets/template_zalominiapp_devmode.zip";
|
|
361
|
+
const templateResponse = await axios.get(TEMPLATE_URL, {
|
|
362
|
+
responseType: "arraybuffer",
|
|
363
|
+
});
|
|
364
|
+
templateSpinner.succeed("Template downloaded successfully!");
|
|
365
|
+
|
|
366
|
+
const outputDir: string = path.resolve(process.cwd(), id);
|
|
367
|
+
if (!fs.existsSync(outputDir)) {
|
|
368
|
+
fs.mkdirSync(outputDir);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const unzipSpinner: Ora = ora("Extracting template...").start();
|
|
372
|
+
await unzipper.Open.buffer(templateResponse.data).then((directory) =>
|
|
373
|
+
directory.extract({ path: outputDir })
|
|
374
|
+
);
|
|
375
|
+
unzipSpinner.succeed("Template extracted successfully!");
|
|
376
|
+
|
|
377
|
+
const macosxDir: string = path.join(process.cwd(), id, "__MACOSX");
|
|
378
|
+
if (fs.existsSync(macosxDir)) {
|
|
379
|
+
fs.rmSync(macosxDir, { recursive: true, force: true });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Save project ID to hapico.config.json
|
|
383
|
+
saveProjectId(outputDir, id);
|
|
384
|
+
|
|
385
|
+
console.log("Project cloned successfully!");
|
|
386
|
+
|
|
387
|
+
const saveSpinner: Ora = ora("Saving project files...").start();
|
|
388
|
+
files.forEach((file: FileContent) => {
|
|
389
|
+
const filePath: string = path.join(process.cwd(), id, "src", file.path);
|
|
390
|
+
const dir: string = path.dirname(filePath);
|
|
391
|
+
|
|
392
|
+
if (!fs.existsSync(dir)) {
|
|
393
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
fs.writeFileSync(filePath, file.content);
|
|
397
|
+
});
|
|
398
|
+
saveSpinner.succeed("Project files saved successfully!");
|
|
399
|
+
console.log(
|
|
400
|
+
`Run 'cd ${id} && npm install && hapico dev' to start the project.`
|
|
401
|
+
);
|
|
402
|
+
} catch (error: any) {
|
|
403
|
+
apiSpinner.fail(`Error cloning project: ${error.message}`);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
program
|
|
408
|
+
.command("dev")
|
|
409
|
+
.description("Start the project in development mode")
|
|
410
|
+
.action(() => {
|
|
411
|
+
const token = getStoredToken();
|
|
412
|
+
if (!token) {
|
|
413
|
+
console.error("You need to login first. Use 'hapico login' command.");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const devSpinner: Ora = ora(
|
|
417
|
+
"Starting the project in development mode..."
|
|
418
|
+
).start();
|
|
419
|
+
const pwd: string = process.cwd();
|
|
420
|
+
const srcDir: string = path.join(pwd, "src");
|
|
421
|
+
if (!fs.existsSync(srcDir)) {
|
|
422
|
+
devSpinner.fail(
|
|
423
|
+
"Source directory 'src' does not exist. Please clone a project first."
|
|
424
|
+
);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const projectId = getStoredProjectId(pwd);
|
|
428
|
+
if (!projectId) {
|
|
429
|
+
devSpinner.fail(
|
|
430
|
+
"Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
431
|
+
);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
console.log(`Connecting to WebSocket server for room: ${projectId}`);
|
|
435
|
+
const room = new RoomState(`view_${projectId}`);
|
|
436
|
+
|
|
437
|
+
room.connect(async () => {
|
|
438
|
+
devSpinner.succeed("Project started in development mode!");
|
|
439
|
+
|
|
440
|
+
const fileManager = new FileManager(srcDir);
|
|
441
|
+
|
|
442
|
+
const initialFiles = fileManager.listFiles();
|
|
443
|
+
room.updateState("view", initialFiles);
|
|
444
|
+
|
|
445
|
+
fileManager.setOnFileChange((filePath: string, content: string) => {
|
|
446
|
+
const es5 = compileES5(content, filePath);
|
|
447
|
+
console.log(`File changed: ${filePath?.replace(srcDir, ".")}`);
|
|
448
|
+
const updatedView = room.files.map((file) => {
|
|
449
|
+
if (path.join(srcDir, file.path) === filePath) {
|
|
450
|
+
return { ...file, content, es5 };
|
|
451
|
+
}
|
|
452
|
+
return file;
|
|
453
|
+
});
|
|
454
|
+
room.updateState("view", updatedView);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
QRCode.generate(
|
|
458
|
+
`https://zalo.me/s/3218692650896662017/player/${projectId}`,
|
|
459
|
+
{ small: true },
|
|
460
|
+
(qrcode: string) => {
|
|
461
|
+
console.log("Scan this QR code to connect to the project:");
|
|
462
|
+
console.log(qrcode);
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
program
|
|
469
|
+
.command("publish")
|
|
470
|
+
.description("Publish the project source code to the server")
|
|
471
|
+
.action(() => {
|
|
472
|
+
const token = getStoredToken();
|
|
473
|
+
if (!token) {
|
|
474
|
+
console.error("You need to login first. Use 'hapico login' command.");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const saveSpinner: Ora = ora("Saving project source code...").start();
|
|
478
|
+
const pwd = process.cwd();
|
|
479
|
+
const projectId = getStoredProjectId(pwd);
|
|
480
|
+
if (!projectId) {
|
|
481
|
+
saveSpinner.fail(
|
|
482
|
+
"Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
483
|
+
);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const srcDir: string = path.join(pwd, "src");
|
|
487
|
+
if (!fs.existsSync(srcDir)) {
|
|
488
|
+
saveSpinner.fail(
|
|
489
|
+
"Source directory 'src' does not exist. Please clone a project first."
|
|
490
|
+
);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const fileManager = new FileManager(srcDir);
|
|
494
|
+
const files = fileManager.listFiles();
|
|
495
|
+
const apiUrl = `https://base.myworkbeast.com/api/views/${projectId}`;
|
|
496
|
+
axios
|
|
497
|
+
.put(
|
|
498
|
+
apiUrl,
|
|
499
|
+
{
|
|
500
|
+
code: JSON.stringify({
|
|
501
|
+
files,
|
|
502
|
+
}),
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
headers: {
|
|
506
|
+
Authorization: `Bearer ${token}`,
|
|
507
|
+
"Content-Type": "application/json",
|
|
508
|
+
},
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
.then(() => {
|
|
512
|
+
saveSpinner.succeed("Project source code saved successfully!");
|
|
513
|
+
})
|
|
514
|
+
.catch((error) => {
|
|
515
|
+
saveSpinner.fail(`Error saving project: ${error.message}`);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
program
|
|
520
|
+
.command("login")
|
|
521
|
+
.description("Login to the system")
|
|
522
|
+
.action(() => {
|
|
523
|
+
const token = getStoredToken();
|
|
524
|
+
if (token) {
|
|
525
|
+
console.log("You are already logged in.");
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const readline = require("readline").createInterface({
|
|
529
|
+
input: process.stdin,
|
|
530
|
+
output: process.stdout,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
readline.question("Please enter your token: ", (inputToken: string) => {
|
|
534
|
+
saveToken(inputToken);
|
|
535
|
+
console.log("Login successful!");
|
|
536
|
+
readline.close();
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
program
|
|
541
|
+
.command("logout")
|
|
542
|
+
.description("Logout from the system")
|
|
543
|
+
.action(() => {
|
|
544
|
+
const token = getStoredToken();
|
|
545
|
+
if (!token) {
|
|
546
|
+
console.log("You are not logged in.");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
fs.unlinkSync(TOKEN_FILE);
|
|
550
|
+
console.log("Logout successful!");
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hapico/cli",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "A simple CLI tool for project management",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hapico": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc && cp -r dist/* ./bin/",
|
|
11
|
+
"start": "node dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"cli",
|
|
15
|
+
"hapico",
|
|
16
|
+
"project-management"
|
|
17
|
+
],
|
|
18
|
+
"author": "Your Name",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@babel/standalone": "^7.28.2",
|
|
22
|
+
"axios": "^1.11.0",
|
|
23
|
+
"commander": "^14.0.0",
|
|
24
|
+
"open": "^10.2.0",
|
|
25
|
+
"ora": "^8.2.0",
|
|
26
|
+
"qrcode-terminal": "^0.12.0",
|
|
27
|
+
"unzipper": "^0.12.3",
|
|
28
|
+
"ws": "^8.18.3"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/commander": "^2.12.5",
|
|
32
|
+
"@types/node": "^24.1.0",
|
|
33
|
+
"@types/unzipper": "^0.10.11",
|
|
34
|
+
"typescript": "^5.8.3"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2018",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["**/*.ts"],
|
|
13
|
+
"exclude": ["node_modules", "dist"]
|
|
14
|
+
}
|