@axpo/cli 1.0.0

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/index.js ADDED
@@ -0,0 +1,2586 @@
1
+ import * as esbuild from 'esbuild';
2
+ import * as path3 from 'path';
3
+ import path3__default from 'path';
4
+ import * as fs3 from 'fs';
5
+ import fs3__default, { createReadStream } from 'fs';
6
+ import * as readline from 'readline';
7
+ import JSZip from 'jszip';
8
+ import chokidar from 'chokidar';
9
+ import color from 'picocolors';
10
+ import logUpdate from 'log-update';
11
+ import { createSpinner } from 'nanospinner';
12
+ import boxen from 'boxen';
13
+ import { fileURLToPath, pathToFileURL } from 'url';
14
+ import * as http from 'http';
15
+ import * as os from 'os';
16
+ import { WebSocketServer, WebSocket } from 'ws';
17
+ import qr from 'qrcode-terminal';
18
+ import axios from 'axios';
19
+ import FormData from 'form-data';
20
+ import { confirm, input, select, password } from '@inquirer/prompts';
21
+ import { execSync, spawn } from 'child_process';
22
+
23
+ // src/build.ts
24
+ var CONFIG_NAMES = [
25
+ "axpo.config.ts",
26
+ "axpo.config.mjs",
27
+ "axpo.config.js",
28
+ "axpo.config.cjs"
29
+ ];
30
+ function findConfigFile() {
31
+ for (const name of CONFIG_NAMES) {
32
+ const fullPath = path3__default.resolve(process.cwd(), name);
33
+ if (fs3__default.existsSync(fullPath)) return fullPath;
34
+ }
35
+ return null;
36
+ }
37
+ function loadPluginJson() {
38
+ const pluginJsonPath = path3__default.resolve(process.cwd(), "plugin.json");
39
+ if (!fs3__default.existsSync(pluginJsonPath)) {
40
+ throw new Error("plugin.json not found in project root");
41
+ }
42
+ try {
43
+ const pluginJson = JSON.parse(fs3__default.readFileSync(pluginJsonPath, "utf-8"));
44
+ if (!pluginJson.id) throw new Error('plugin.json: "id" is required');
45
+ if (!pluginJson.name) throw new Error('plugin.json: "name" is required');
46
+ if (!pluginJson.version) throw new Error('plugin.json: "version" is required');
47
+ if (!pluginJson.author?.name) throw new Error('plugin.json: "author.name" is required');
48
+ if (!pluginJson.author?.email) throw new Error('plugin.json: "author.email" is required');
49
+ return pluginJson;
50
+ } catch (error) {
51
+ if (error.code === "ENOENT") throw new Error("plugin.json not found");
52
+ throw new Error(`Failed to parse plugin.json: ${error.message}`);
53
+ }
54
+ }
55
+ async function loadConfig(configPath) {
56
+ let finalPath = null;
57
+ if (configPath) {
58
+ finalPath = path3__default.resolve(process.cwd(), configPath);
59
+ if (!fs3__default.existsSync(finalPath)) {
60
+ throw new Error(`Config file not found: ${configPath}`);
61
+ }
62
+ } else {
63
+ finalPath = findConfigFile();
64
+ if (!finalPath) {
65
+ throw new Error(
66
+ "Config file not found. Supported formats:\n" + CONFIG_NAMES.map((n) => ` - ${n}`).join("\n")
67
+ );
68
+ }
69
+ }
70
+ try {
71
+ const ext = path3__default.extname(finalPath);
72
+ if (ext === ".cjs") {
73
+ const { createRequire } = await import('module');
74
+ const require2 = createRequire(import.meta.url);
75
+ const mod2 = require2(finalPath);
76
+ return mod2.default || mod2;
77
+ }
78
+ const url = `${pathToFileURL(finalPath).href}?t=${Date.now()}`;
79
+ const mod = await import(url);
80
+ return mod.default || {};
81
+ } catch (error) {
82
+ if (error.code === "ERR_MODULE_NOT_FOUND") {
83
+ throw new Error(`Failed to load config: Module not found
84
+ ${error.message}`);
85
+ }
86
+ if (error.message?.includes("SyntaxError")) {
87
+ throw new Error(`Syntax error in config file: ${path3__default.basename(finalPath)}
88
+ ${error.message}`);
89
+ }
90
+ throw new Error(`Failed to load config: ${error.message}`);
91
+ }
92
+ }
93
+ function validateConfig(config, pluginJson) {
94
+ if (!/^[a-z][a-z0-9_]*(\.[a-z0-9_]+)+$/i.test(pluginJson.id)) {
95
+ throw new Error("Invalid plugin ID format in plugin.json. Use format like: com.example.plugin");
96
+ }
97
+ if (!config.buildOptions) {
98
+ throw new Error("buildOptions is required in axpo.config");
99
+ }
100
+ if (!config.buildOptions.entryPoints) {
101
+ throw new Error("buildOptions.entryPoints is required in axpo.config");
102
+ }
103
+ if (!config.buildOptions.outfile && !config.buildOptions.outdir) {
104
+ throw new Error("buildOptions.outfile or buildOptions.outdir is required in axpo.config");
105
+ }
106
+ if (config.dev && typeof config.dev !== "object") {
107
+ throw new Error("dev must be an object");
108
+ }
109
+ if (config.dev?.port && (typeof config.dev.port !== "number" || config.dev.port < 1 || config.dev.port > 65535)) {
110
+ throw new Error("dev.port must be a valid port number (1-65535)");
111
+ }
112
+ }
113
+ function getOutDir(config) {
114
+ if (config.buildOptions?.outdir) return config.buildOptions.outdir;
115
+ if (config.buildOptions?.outfile) return path3__default.dirname(config.buildOptions.outfile);
116
+ throw new Error("No output directory found in buildOptions");
117
+ }
118
+ var MINIMUM_API_REQUIREMENTS = {
119
+ "editorFile": 958,
120
+ "editorfile": 958
121
+ // Support both cases
122
+ };
123
+ var CodeAnalyzer = class {
124
+ constructor(srcDir = "src", pluginMinVersion) {
125
+ this.srcDir = srcDir;
126
+ this.pluginMinVersion = pluginMinVersion;
127
+ }
128
+ removeComments(code) {
129
+ return code.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
130
+ }
131
+ analyzeAPIUsage() {
132
+ const apis = /* @__PURE__ */ new Set();
133
+ const warnings = [];
134
+ this.scanDirectory(this.srcDir, (content, filePath) => {
135
+ const lines = content.split("\n");
136
+ const requirePattern = /acode\.require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
137
+ lines.forEach((line, index) => {
138
+ const cleanLine = this.removeComments(line);
139
+ requirePattern.lastIndex = 0;
140
+ let match;
141
+ while ((match = requirePattern.exec(cleanLine)) !== null) {
142
+ const apiName = match[1];
143
+ apis.add(apiName);
144
+ const requiredVersion = MINIMUM_API_REQUIREMENTS[apiName] || MINIMUM_API_REQUIREMENTS[apiName.toLowerCase()];
145
+ if (requiredVersion) {
146
+ let severity = "info";
147
+ if (this.pluginMinVersion !== void 0) {
148
+ if (requiredVersion > this.pluginMinVersion) {
149
+ severity = "error";
150
+ } else {
151
+ severity = "info";
152
+ }
153
+ } else {
154
+ severity = "warn";
155
+ }
156
+ warnings.push({
157
+ api: apiName,
158
+ requiredVersion,
159
+ file: path3.relative(process.cwd(), filePath),
160
+ line: index + 1,
161
+ severity
162
+ });
163
+ }
164
+ }
165
+ });
166
+ });
167
+ return {
168
+ apis,
169
+ warnings,
170
+ hasErrors: warnings.some((w) => w.severity === "error"),
171
+ hasWarnings: warnings.some((w) => w.severity === "warn")
172
+ };
173
+ }
174
+ scanDirectory(dir, callback) {
175
+ if (!fs3.existsSync(dir)) return;
176
+ fs3.readdirSync(dir).forEach((file) => {
177
+ const filePath = path3.join(dir, file);
178
+ const stat = fs3.statSync(filePath);
179
+ if (stat.isDirectory()) {
180
+ if (!["node_modules", "build", "dist", ".git"].includes(file)) {
181
+ this.scanDirectory(filePath, callback);
182
+ }
183
+ } else if (/\.(ts|js|tsx|jsx)$/.test(file)) {
184
+ callback(fs3.readFileSync(filePath, "utf-8"), filePath);
185
+ }
186
+ });
187
+ }
188
+ };
189
+
190
+ // src/dev-server.ts
191
+ var MAX_CLIENTS = 5;
192
+ var VIEWPORT_HEIGHT = 15;
193
+ var MAX_LOGS = 200;
194
+ var IP_CHECK_INTERVAL = 5e3;
195
+ var AxpoDevServer = class {
196
+ constructor(options) {
197
+ this.server = null;
198
+ this.wss = null;
199
+ this.clients = /* @__PURE__ */ new Map();
200
+ this.logs = /* @__PURE__ */ new Map();
201
+ this.logOrder = [];
202
+ this.buildVersion = 0;
203
+ this.statusSection = {
204
+ buildStatus: "idle"
205
+ };
206
+ this.qrCode = "";
207
+ this.localIP = "";
208
+ this.renderScheduled = false;
209
+ this.terminalWidth = 80;
210
+ this.availableClientIds = [1, 2, 3, 4, 5];
211
+ this.clientIdMap = /* @__PURE__ */ new Map();
212
+ this.scrollOffset = 0;
213
+ this.lastRenderedLines = 0;
214
+ this.isFirstRender = true;
215
+ this.buildSessionWarnings = {
216
+ apiWarnings: /* @__PURE__ */ new Set(),
217
+ versionWarnings: /* @__PURE__ */ new Set(),
218
+ buildErrors: /* @__PURE__ */ new Set(),
219
+ lastBuildHash: ""
220
+ };
221
+ this.sessionWarnings = /* @__PURE__ */ new Map();
222
+ this.detectedAPIs = /* @__PURE__ */ new Set();
223
+ // NEW: IP monitoring
224
+ this.ipCheckInterval = null;
225
+ this.lastKnownIP = "";
226
+ this.port = options.port;
227
+ this.config = options.config;
228
+ this.pluginJson = options.pluginJson;
229
+ this.localIP = this.getLocalIP();
230
+ this.lastKnownIP = this.localIP;
231
+ this.qrCode = this.generateQRCode(`axpo://${this.localIP}:${this.port}`);
232
+ this.updateTerminalDimensions();
233
+ if (process.stdout.isTTY) {
234
+ process.stdout.on("resize", () => {
235
+ this.handleTerminalResize();
236
+ });
237
+ }
238
+ this.startIPMonitoring();
239
+ }
240
+ // NEW: Start monitoring IP address changes
241
+ startIPMonitoring() {
242
+ this.ipCheckInterval = setInterval(() => {
243
+ this.checkAndUpdateIP();
244
+ }, IP_CHECK_INTERVAL);
245
+ }
246
+ // NEW: Stop IP monitoring
247
+ stopIPMonitoring() {
248
+ if (this.ipCheckInterval) {
249
+ clearInterval(this.ipCheckInterval);
250
+ this.ipCheckInterval = null;
251
+ }
252
+ }
253
+ // NEW: Check if IP has changed and update accordingly
254
+ checkAndUpdateIP() {
255
+ const currentIP = this.getLocalIP();
256
+ if (currentIP !== this.lastKnownIP) {
257
+ const oldIP = this.lastKnownIP;
258
+ this.lastKnownIP = currentIP;
259
+ this.localIP = currentIP;
260
+ this.qrCode = this.generateQRCode(`axpo://${this.localIP}:${this.port}`);
261
+ this.addLog("info", `\u{1F504} Network changed: ${oldIP} \u2192 ${currentIP}`);
262
+ this.notifyClientsIPChange(currentIP);
263
+ this.renderUI();
264
+ }
265
+ }
266
+ // NEW: Notify clients about IP change
267
+ notifyClientsIPChange(newIP) {
268
+ this.broadcast({
269
+ type: "ip_changed",
270
+ newIP,
271
+ newURL: `axpo://${newIP}:${this.port}`,
272
+ timestamp: Date.now()
273
+ });
274
+ }
275
+ generateContentHash(content) {
276
+ let hash = 0;
277
+ for (let i = 0; i < content.length; i++) {
278
+ const char = content.charCodeAt(i);
279
+ hash = (hash << 5) - hash + char;
280
+ hash = hash & hash;
281
+ }
282
+ return hash.toString(36);
283
+ }
284
+ checkVersionCompatibility(clientId, deviceInfo) {
285
+ const client = this.clients.get(clientId);
286
+ if (!client) return;
287
+ const freshPluginJson = this.getFreshPluginJson();
288
+ if (!this.sessionWarnings.has(clientId)) {
289
+ this.sessionWarnings.set(clientId, {
290
+ minVersionCodeWarning: false,
291
+ apiWarnings: /* @__PURE__ */ new Set()
292
+ });
293
+ }
294
+ const warnings = this.sessionWarnings.get(clientId);
295
+ if (freshPluginJson.minVersionCode && !warnings.minVersionCodeWarning) {
296
+ if (deviceInfo.versionCode < freshPluginJson.minVersionCode) {
297
+ this.addLog(
298
+ "warn",
299
+ `\u26A0\uFE0F Version mismatch: Plugin requires minVersionCode ${freshPluginJson.minVersionCode}, but client has ${deviceInfo.versionCode}. Plugin may not work correctly.`,
300
+ client.clientNumber
301
+ );
302
+ warnings.minVersionCodeWarning = true;
303
+ }
304
+ }
305
+ this.detectedAPIs.forEach((apiName) => {
306
+ if (!warnings.apiWarnings.has(apiName)) {
307
+ const requiredVersion = MINIMUM_API_REQUIREMENTS[apiName];
308
+ if (requiredVersion && deviceInfo.versionCode < requiredVersion) {
309
+ this.addLog(
310
+ "warn",
311
+ `\u26A0\uFE0F API '${apiName}' requires versionCode ${requiredVersion}, but client has ${deviceInfo.versionCode}. This API may not be available.`,
312
+ client.clientNumber
313
+ );
314
+ warnings.apiWarnings.add(apiName);
315
+ }
316
+ }
317
+ });
318
+ }
319
+ analyzeAndReportAPIs(analysis) {
320
+ if (!analysis || !analysis.warnings) return;
321
+ const { warnings, apis } = analysis;
322
+ const freshPluginJson = this.getFreshPluginJson();
323
+ this.detectedAPIs = apis;
324
+ const warningsHash = this.generateContentHash(
325
+ JSON.stringify(warnings.map((w) => `${w.api}:${w.severity}`))
326
+ );
327
+ if (this.buildSessionWarnings.lastBuildHash === warningsHash) {
328
+ return;
329
+ }
330
+ this.buildSessionWarnings.lastBuildHash = warningsHash;
331
+ if (apis.size > 0 && this.buildSessionWarnings.apiWarnings.size === 0) {
332
+ const apiList = Array.from(apis).join(", ");
333
+ this.addLog("info", `\u{1F4E6} Detected ${apis.size} API(s): ${apiList}`);
334
+ }
335
+ const errors = warnings.filter((w) => w.severity === "error");
336
+ const warns = warnings.filter((w) => w.severity === "warn");
337
+ errors.forEach((warning) => {
338
+ const key = `${warning.api}:${warning.severity}`;
339
+ if (!this.buildSessionWarnings.buildErrors.has(key)) {
340
+ this.addLog(
341
+ "error",
342
+ `\u274C API '${warning.api}' requires versionCode ${warning.requiredVersion}, but plugin minVersionCode is ${freshPluginJson.minVersionCode || "not set"}. Update plugin.json!`
343
+ );
344
+ this.buildSessionWarnings.buildErrors.add(key);
345
+ }
346
+ });
347
+ warns.forEach((warning) => {
348
+ const key = `${warning.api}:${warning.severity}`;
349
+ if (!this.buildSessionWarnings.versionWarnings.has(key)) {
350
+ this.addLog(
351
+ "warn",
352
+ `\u26A0\uFE0F API '${warning.api}' requires versionCode ${warning.requiredVersion}. Consider adding "minVersionCode": ${warning.requiredVersion} to plugin.json`
353
+ );
354
+ this.buildSessionWarnings.versionWarnings.add(key);
355
+ }
356
+ });
357
+ Array.from(apis).forEach((api) => {
358
+ this.buildSessionWarnings.apiWarnings.add(api);
359
+ });
360
+ }
361
+ // Get fresh plugin.json from disk
362
+ getFreshPluginJson() {
363
+ try {
364
+ const pluginJsonPath = path3.resolve(process.cwd(), "plugin.json");
365
+ if (fs3.existsSync(pluginJsonPath)) {
366
+ const content = fs3.readFileSync(pluginJsonPath, "utf-8");
367
+ return JSON.parse(content);
368
+ }
369
+ } catch (error) {
370
+ console.warn("Failed to read fresh plugin.json:", error);
371
+ }
372
+ return this.pluginJson;
373
+ }
374
+ handleTerminalResize() {
375
+ const oldWidth = this.terminalWidth;
376
+ this.updateTerminalDimensions();
377
+ if (Math.abs(this.terminalWidth - oldWidth) > 10) {
378
+ this.qrCode = this.generateQRCode(`axpo://${this.localIP}:${this.port}`);
379
+ }
380
+ const totalLines = this.calculateTotalLines();
381
+ const maxScroll = Math.max(0, totalLines - VIEWPORT_HEIGHT);
382
+ if (this.scrollOffset > maxScroll) {
383
+ this.scrollOffset = maxScroll;
384
+ }
385
+ logUpdate.clear();
386
+ logUpdate.done();
387
+ this.isFirstRender = true;
388
+ this.renderUI();
389
+ }
390
+ updateTerminalDimensions() {
391
+ if (process.stdout.isTTY) {
392
+ this.terminalWidth = Math.max(60, (process.stdout.columns || 80) - 2);
393
+ }
394
+ }
395
+ getNextClientId() {
396
+ return this.availableClientIds.shift() || 0;
397
+ }
398
+ releaseClientId(clientId) {
399
+ const clientNumber = this.clientIdMap.get(clientId);
400
+ if (!clientNumber) return;
401
+ this.clientIdMap.delete(clientId);
402
+ const remainingClients = Array.from(this.clients.values()).filter((c) => c.id !== clientId).sort((a, b) => a.connectedAt.getTime() - b.connectedAt.getTime());
403
+ this.availableClientIds = [1, 2, 3, 4, 5];
404
+ this.clientIdMap.clear();
405
+ remainingClients.forEach((client, index) => {
406
+ const newNumber = index + 1;
407
+ const oldNumber = client.clientNumber;
408
+ client.clientNumber = newNumber;
409
+ this.clientIdMap.set(client.id, newNumber);
410
+ if (oldNumber !== newNumber) {
411
+ this.updateLogsClientNumber(oldNumber, newNumber);
412
+ }
413
+ });
414
+ this.availableClientIds = [1, 2, 3, 4, 5].filter(
415
+ (id) => !Array.from(this.clientIdMap.values()).includes(id)
416
+ );
417
+ }
418
+ updateLogsClientNumber(oldNumber, newNumber) {
419
+ this.logs.forEach((entry, key) => {
420
+ if (entry.clientNumber === oldNumber) {
421
+ entry.clientNumber = newNumber;
422
+ const newKey = key.replace(`:${oldNumber}:`, `:${newNumber}:`);
423
+ if (newKey !== key) {
424
+ this.logs.delete(key);
425
+ this.logs.set(newKey, entry);
426
+ const index = this.logOrder.indexOf(key);
427
+ if (index !== -1) {
428
+ this.logOrder[index] = newKey;
429
+ }
430
+ }
431
+ }
432
+ });
433
+ }
434
+ truncateText(text, maxLength = this.terminalWidth) {
435
+ const plainText = text.replace(/\x1b\[[0-9;]*m/g, "");
436
+ if (plainText.length <= maxLength) {
437
+ return text;
438
+ }
439
+ let visibleLength = 0;
440
+ let truncatedIndex = 0;
441
+ for (let i = 0; i < text.length; i++) {
442
+ if (text[i] === "\x1B") {
443
+ const endIndex = text.indexOf("m", i);
444
+ if (endIndex !== -1) {
445
+ i = endIndex;
446
+ continue;
447
+ }
448
+ }
449
+ visibleLength++;
450
+ truncatedIndex = i + 1;
451
+ if (visibleLength >= maxLength - 3) {
452
+ break;
453
+ }
454
+ }
455
+ return text.substring(0, truncatedIndex) + color.dim("...");
456
+ }
457
+ scrollUp() {
458
+ const totalLines = this.calculateTotalLines();
459
+ const maxScroll = Math.max(0, totalLines - VIEWPORT_HEIGHT);
460
+ if (this.scrollOffset < maxScroll) {
461
+ this.scrollOffset++;
462
+ this.renderUI();
463
+ }
464
+ }
465
+ scrollDown() {
466
+ if (this.scrollOffset > 0) {
467
+ this.scrollOffset--;
468
+ this.renderUI();
469
+ }
470
+ }
471
+ scrollToBottom() {
472
+ if (this.scrollOffset !== 0) {
473
+ this.scrollOffset = 0;
474
+ this.renderUI();
475
+ }
476
+ }
477
+ scrollToTop() {
478
+ const totalLines = this.calculateTotalLines();
479
+ const maxScroll = Math.max(0, totalLines - VIEWPORT_HEIGHT);
480
+ if (this.scrollOffset !== maxScroll) {
481
+ this.scrollOffset = maxScroll;
482
+ this.renderUI();
483
+ }
484
+ }
485
+ async start() {
486
+ return new Promise((resolve5, reject) => {
487
+ this.server = http.createServer((req, res) => this.handleRequest(req, res));
488
+ this.wss = new WebSocketServer({ server: this.server });
489
+ this.wss.on("connection", (ws, req) => this.handleConnection(ws, req));
490
+ this.server.listen(this.port, () => {
491
+ this.renderUI();
492
+ resolve5();
493
+ });
494
+ this.server.on("error", (error) => {
495
+ if (error.code === "EADDRINUSE") {
496
+ console.error(color.red(`
497
+ \u2717 Port ${this.port} is already in use`));
498
+ } else {
499
+ console.error(color.red("Server error:"), error);
500
+ }
501
+ reject(error);
502
+ });
503
+ });
504
+ }
505
+ async stop() {
506
+ try {
507
+ this.stopIPMonitoring();
508
+ this.clients.forEach((client) => {
509
+ try {
510
+ if (client.ws.readyState === WebSocket.OPEN) {
511
+ client.ws.close();
512
+ }
513
+ } catch (error) {
514
+ }
515
+ });
516
+ this.clients.clear();
517
+ if (this.wss) {
518
+ await new Promise((resolve5) => {
519
+ this.wss.close(() => resolve5());
520
+ });
521
+ this.wss = null;
522
+ }
523
+ if (this.server) {
524
+ await new Promise((resolve5) => {
525
+ this.server.close(() => {
526
+ this.server = null;
527
+ resolve5();
528
+ });
529
+ });
530
+ }
531
+ } catch (error) {
532
+ }
533
+ }
534
+ scheduleRender() {
535
+ if (this.renderScheduled) return;
536
+ this.renderScheduled = true;
537
+ setImmediate(() => {
538
+ this.renderScheduled = false;
539
+ this.renderUI();
540
+ });
541
+ }
542
+ renderUI() {
543
+ const output = [];
544
+ const freshPluginJson = this.getFreshPluginJson();
545
+ output.push("");
546
+ output.push(color.bold(color.white(` \u{1F493} Axpo Dev Server`)));
547
+ output.push("");
548
+ output.push(this.truncateText(color.cyan(` ${freshPluginJson.name} \xB7 ${color.gray(`v${freshPluginJson.version}`)}`)));
549
+ const qrLines = this.qrCode.split("\n");
550
+ qrLines.forEach((line) => output.push(line));
551
+ output.push("");
552
+ output.push(this.truncateText(color.bold(color.white(`\u203A Axpo waiting on ${color.cyan(`axpo://${this.localIP}:${this.port}`)}`))));
553
+ output.push(this.truncateText(color.bold(color.white(`\u203A Scan the QR code above with Axpo Dev (Acode Plugin)`))));
554
+ output.push("");
555
+ const clientText = this.clients.size === MAX_CLIENTS ? color.yellow(`${this.clients.size}/${MAX_CLIENTS} (MAX)`) : `${this.clients.size}/${MAX_CLIENTS}`;
556
+ output.push(this.truncateText(color.dim(` Clients: ${clientText}`)));
557
+ output.push("");
558
+ output.push(color.bold(color.white("\u203A r=Reload \u2502 c=Clear \u2502 q=Quit \u2502 \u2191/\u2193=Scroll \u2502 Home/End")));
559
+ output.push("");
560
+ output.push(this.truncateText(color.dim("Logs below. Use arrow keys to scroll.")));
561
+ output.push("");
562
+ if (this.statusSection.buildStatus !== "idle") {
563
+ const timeStr = this.statusSection.buildTimestamp ? new Date(this.statusSection.buildTimestamp).toLocaleTimeString() : (/* @__PURE__ */ new Date()).toLocaleTimeString();
564
+ if (this.statusSection.buildStatus === "building") {
565
+ output.push(this.truncateText(`${color.dim(timeStr)} ${color.yellow("\u22EF")} Building...`));
566
+ } else if (this.statusSection.buildStatus === "success") {
567
+ const time = this.statusSection.buildTime || 0;
568
+ try {
569
+ const outDir = getOutDir(this.config);
570
+ const mainJsPath = path3.join(outDir, "main.js");
571
+ const size = fs3.existsSync(mainJsPath) ? fs3.statSync(mainJsPath).size : 0;
572
+ const sizeKB = (size / 1024).toFixed(2);
573
+ output.push(this.truncateText(`${color.dim(timeStr)} ${color.green("\u2713")} Build complete in ${time}ms \xB7 ${outDir}/main.js (${sizeKB} KB)`));
574
+ } catch (error) {
575
+ output.push(this.truncateText(`${color.dim(timeStr)} ${color.green("\u2713")} Build complete in ${time}ms`));
576
+ }
577
+ } else if (this.statusSection.buildStatus === "failed") {
578
+ const errorCount = this.statusSection.errorCount || 1;
579
+ const errorText = errorCount === 1 ? "error" : "errors";
580
+ output.push(this.truncateText(`${color.dim(timeStr)} ${color.red("\u2717")} Build failed with ${errorCount} ${errorText}`));
581
+ }
582
+ }
583
+ if (this.logOrder.length > 0) {
584
+ const allLines = [];
585
+ this.logOrder.forEach((key, logIndex) => {
586
+ const entry = this.logs.get(key);
587
+ if (!entry) return;
588
+ const lines = this.getLogLines(entry);
589
+ lines.forEach((line, lineIndex) => {
590
+ allLines.push({
591
+ line,
592
+ logIndex,
593
+ lineIndex,
594
+ totalLinesInLog: lines.length
595
+ });
596
+ });
597
+ });
598
+ const totalLines = allLines.length;
599
+ const viewportEnd = totalLines - this.scrollOffset;
600
+ const viewportStart = Math.max(0, viewportEnd - VIEWPORT_HEIGHT);
601
+ const visibleLines = allLines.slice(viewportStart, viewportEnd);
602
+ const fullyHiddenAbove = /* @__PURE__ */ new Set();
603
+ const fullyHiddenBelow = /* @__PURE__ */ new Set();
604
+ const partiallyVisibleLogs = /* @__PURE__ */ new Set();
605
+ visibleLines.forEach((lineInfo) => {
606
+ partiallyVisibleLogs.add(lineInfo.logIndex);
607
+ });
608
+ for (let i = 0; i < viewportStart; i++) {
609
+ const lineInfo = allLines[i];
610
+ if (!partiallyVisibleLogs.has(lineInfo.logIndex)) {
611
+ fullyHiddenAbove.add(lineInfo.logIndex);
612
+ }
613
+ }
614
+ for (let i = viewportEnd; i < totalLines; i++) {
615
+ const lineInfo = allLines[i];
616
+ if (!partiallyVisibleLogs.has(lineInfo.logIndex)) {
617
+ fullyHiddenBelow.add(lineInfo.logIndex);
618
+ }
619
+ }
620
+ if (fullyHiddenAbove.size > 0) {
621
+ const count = fullyHiddenAbove.size;
622
+ output.push(color.dim(` \u2191 ${count} older log${count > 1 ? "s" : ""} hidden (total: ${this.logOrder.length}) \xB7 press \u2191`));
623
+ } else if (viewportStart > 0) {
624
+ const firstVisibleLog = visibleLines[0];
625
+ if (firstVisibleLog && firstVisibleLog.lineIndex > 0) {
626
+ output.push(color.dim(` \u2191 scroll up for more \xB7 press \u2191`));
627
+ }
628
+ }
629
+ visibleLines.forEach((lineInfo, idx) => {
630
+ let line = lineInfo.line;
631
+ if (idx === 0 && lineInfo.lineIndex > 0) {
632
+ const plainLine = line.replace(/\x1b\[[0-9;]*m/g, "");
633
+ if (!plainLine.trim().startsWith("...")) {
634
+ const contentStart = line.search(/[^\x1b\[0-9;m]/);
635
+ if (contentStart !== -1) {
636
+ const prefix = line.substring(0, contentStart);
637
+ const content = line.substring(contentStart);
638
+ line = prefix + color.dim("...") + content;
639
+ } else {
640
+ line = color.dim("...") + line;
641
+ }
642
+ }
643
+ }
644
+ if (idx === visibleLines.length - 1) {
645
+ const remainingLines = lineInfo.totalLinesInLog - lineInfo.lineIndex - 1;
646
+ if (remainingLines > 0) {
647
+ line = line + color.dim("...");
648
+ }
649
+ }
650
+ output.push(this.truncateText(line));
651
+ });
652
+ if (fullyHiddenBelow.size > 0) {
653
+ const count = fullyHiddenBelow.size;
654
+ output.push(color.dim(` \u2193 ${count} newer log${count > 1 ? "s" : ""} hidden \xB7 press \u2193`));
655
+ } else if (viewportEnd < totalLines) {
656
+ const lastVisibleLog = visibleLines[visibleLines.length - 1];
657
+ if (lastVisibleLog) {
658
+ const lastLogTotalLines = lastVisibleLog.totalLinesInLog;
659
+ if (lastVisibleLog.lineIndex < lastLogTotalLines - 1) {
660
+ output.push(color.dim(` \u2193 scroll down for more \xB7 press \u2193`));
661
+ }
662
+ }
663
+ }
664
+ if (totalLines > VIEWPORT_HEIGHT) {
665
+ const scrollPercent = Math.round((totalLines - viewportEnd) / (totalLines - VIEWPORT_HEIGHT) * 100);
666
+ output.push(color.dim(` Viewing ${viewportStart + 1}-${viewportEnd} of ${totalLines} lines (${scrollPercent}% from bottom)`));
667
+ }
668
+ }
669
+ if (this.statusSection.shutdownStatus) {
670
+ const timeStr = (/* @__PURE__ */ new Date()).toLocaleTimeString();
671
+ if (this.statusSection.shutdownStatus === "stopping") {
672
+ output.push(this.truncateText(`${color.dim(timeStr)} ${color.yellow("\u22EF")} Stopping dev server...`));
673
+ } else if (this.statusSection.shutdownStatus === "stopped") {
674
+ output.push(this.truncateText(`${color.dim(timeStr)} ${color.green("\u2713")} Server stopped successfully`));
675
+ } else if (this.statusSection.shutdownStatus === "error") {
676
+ output.push(this.truncateText(`${color.dim(timeStr)} ${color.red("\u2717")} Error stopping server`));
677
+ }
678
+ }
679
+ output.push("");
680
+ const newContent = output.join("\n");
681
+ try {
682
+ logUpdate(newContent);
683
+ this.lastRenderedLines = output.length;
684
+ this.isFirstRender = false;
685
+ } catch (error) {
686
+ console.log(newContent);
687
+ }
688
+ }
689
+ wrapText(text, maxWidth) {
690
+ const plainText = text.replace(/\x1b\[[0-9;]*m/g, "");
691
+ if (plainText.length <= maxWidth) {
692
+ return [text];
693
+ }
694
+ const lines = [];
695
+ let remaining = text;
696
+ while (remaining.length > 0) {
697
+ if (remaining.length <= maxWidth) {
698
+ lines.push(remaining);
699
+ break;
700
+ }
701
+ const chunk = remaining.substring(0, maxWidth);
702
+ const lastSpace = chunk.lastIndexOf(" ");
703
+ if (lastSpace > maxWidth * 0.7) {
704
+ lines.push(remaining.substring(0, lastSpace));
705
+ remaining = " " + remaining.substring(lastSpace + 1);
706
+ } else {
707
+ lines.push(chunk);
708
+ remaining = " " + remaining.substring(maxWidth);
709
+ }
710
+ }
711
+ return lines;
712
+ }
713
+ calculateTotalLines() {
714
+ let totalLines = 0;
715
+ this.logOrder.forEach((key) => {
716
+ const entry = this.logs.get(key);
717
+ if (!entry) return;
718
+ const lines = this.getLogLines(entry);
719
+ totalLines += lines.length;
720
+ });
721
+ return totalLines;
722
+ }
723
+ getLogLines(entry) {
724
+ const entryTimeStr = new Date(entry.timestamp).toLocaleTimeString();
725
+ const clientSuffix = entry.clientNumber ? ` \u203A ${color.cyan(`[${entry.clientNumber}]`)}` : "";
726
+ const levelIndicator = entry.level === "error" ? color.red("\u2717 ERROR") : entry.level === "warn" ? color.yellow("\u26A0 WARN") : entry.level === "info" ? color.gray("\u203A INFO") : entry.level === "device" ? color.gray("\u203A DEVICE") : color.blue("\u203A LOG");
727
+ const countSuffix = entry.count > 1 ? ` ${color.dim(`[${entry.count}\xD7]`)}` : "";
728
+ const logLine = `${color.dim(entryTimeStr)} ${levelIndicator}${clientSuffix} ${entry.message}${countSuffix}`;
729
+ return this.wrapText(logLine, this.terminalWidth);
730
+ }
731
+ setBuildStatus(status, time, error, errorCount) {
732
+ this.statusSection.buildStatus = status;
733
+ this.statusSection.buildTime = time;
734
+ this.statusSection.errorCount = errorCount;
735
+ this.statusSection.buildTimestamp = Date.now();
736
+ if (status === "failed" && error) {
737
+ const errors = error.split("\n").filter((e) => e.trim());
738
+ errors.forEach((err) => {
739
+ this.addLog("error", err);
740
+ });
741
+ }
742
+ this.scrollToBottom();
743
+ this.renderUI();
744
+ }
745
+ setShutdownStatus(status) {
746
+ this.statusSection.shutdownStatus = status;
747
+ this.renderUI();
748
+ }
749
+ addLog(level, message, clientNumber) {
750
+ if (!message?.trim()) return;
751
+ message = message.trim();
752
+ const filters = [
753
+ "Starting initial build",
754
+ "Reloading clients",
755
+ "Build failed with"
756
+ ];
757
+ if (filters.some((filter) => message.includes(filter))) return;
758
+ const key = clientNumber ? `${level}:${clientNumber}:${message}` : `${level}:${message}`;
759
+ if (this.logs.has(key)) {
760
+ const existing = this.logs.get(key);
761
+ existing.count++;
762
+ existing.timestamp = Date.now();
763
+ const currentIndex = this.logOrder.indexOf(key);
764
+ if (currentIndex !== -1) {
765
+ this.logOrder.splice(currentIndex, 1);
766
+ }
767
+ this.logOrder.push(key);
768
+ } else {
769
+ const entry = {
770
+ level,
771
+ message,
772
+ timestamp: Date.now(),
773
+ count: 1,
774
+ clientNumber
775
+ };
776
+ this.logs.set(key, entry);
777
+ this.logOrder.push(key);
778
+ if (this.logOrder.length > MAX_LOGS) {
779
+ const removedKey = this.logOrder.shift();
780
+ this.logs.delete(removedKey);
781
+ if (this.scrollOffset > 0) {
782
+ this.scrollOffset = Math.max(0, this.scrollOffset - 1);
783
+ }
784
+ }
785
+ }
786
+ if (this.scrollOffset === 0) {
787
+ this.scheduleRender();
788
+ }
789
+ }
790
+ addDeviceInfoLog(deviceInfo, clientNumber) {
791
+ const key = `device:${clientNumber}:${deviceInfo.deviceModel}`;
792
+ const deviceDetails = [
793
+ `\u{1F4F1} ${deviceInfo.deviceModel}`,
794
+ deviceInfo.deviceVendor && deviceInfo.deviceVendor !== "Unknown" ? deviceInfo.deviceVendor : null,
795
+ deviceInfo.os,
796
+ `Acode ${deviceInfo.version} (${deviceInfo.versionCode})`,
797
+ `${deviceInfo.screenWidth}\xD7${deviceInfo.screenHeight}`,
798
+ deviceInfo.ram ? `${deviceInfo.ram}GB RAM` : null,
799
+ deviceInfo.cpuCores ? `${deviceInfo.cpuCores} cores` : null
800
+ ].filter(Boolean).join(" \xB7 ");
801
+ const entry = {
802
+ level: "device",
803
+ message: deviceDetails,
804
+ // ✅ Use formatted string instead of separate deviceInfo
805
+ timestamp: Date.now(),
806
+ count: 1,
807
+ clientNumber
808
+ // ✅ REMOVE: deviceInfo field
809
+ };
810
+ this.logs.set(key, entry);
811
+ this.logOrder.push(key);
812
+ if (this.logOrder.length > MAX_LOGS) {
813
+ const removedKey = this.logOrder.shift();
814
+ this.logs.delete(removedKey);
815
+ if (this.scrollOffset > 0) {
816
+ this.scrollOffset = Math.max(0, this.scrollOffset - 1);
817
+ }
818
+ }
819
+ if (this.scrollOffset === 0) {
820
+ this.scheduleRender();
821
+ }
822
+ }
823
+ clearLogs() {
824
+ this.logs.clear();
825
+ this.logOrder = [];
826
+ this.scrollOffset = 0;
827
+ this.renderUI();
828
+ }
829
+ notifyReload() {
830
+ this.buildVersion++;
831
+ this.broadcast({ type: "reload", buildVersion: this.buildVersion, timestamp: Date.now() });
832
+ }
833
+ handleRequest(req, res) {
834
+ const url = req.url || "/";
835
+ if (req.method === "OPTIONS") {
836
+ res.writeHead(200);
837
+ res.end();
838
+ return;
839
+ }
840
+ if (url === "/" || url === "/manifest") {
841
+ this.serveManifest(res);
842
+ } else if (url === "/plugin.json") {
843
+ this.serveFile("plugin.json", res);
844
+ } else if (url === "/status") {
845
+ this.serveStatus(res);
846
+ } else {
847
+ const fileName = url.substring(1);
848
+ try {
849
+ const buildDir = getOutDir(this.config);
850
+ const buildFilePath = path3.join(buildDir, fileName);
851
+ if (fs3.existsSync(buildFilePath) && fs3.statSync(buildFilePath).isFile()) {
852
+ try {
853
+ const content = fs3.readFileSync(buildFilePath);
854
+ const ext = path3.extname(buildFilePath);
855
+ const contentType = this.getContentType(ext);
856
+ res.writeHead(200, {
857
+ "Content-Type": contentType,
858
+ "Content-Length": content.length.toString(),
859
+ "Access-Control-Allow-Origin": "*"
860
+ });
861
+ res.end(content);
862
+ return;
863
+ } catch (error) {
864
+ this.serve500(res, error);
865
+ return;
866
+ }
867
+ }
868
+ } catch (error) {
869
+ }
870
+ if (fs3.existsSync(fileName) && fs3.statSync(fileName).isFile()) {
871
+ this.serveFile(fileName, res);
872
+ } else {
873
+ this.serve404(res);
874
+ }
875
+ }
876
+ }
877
+ serveManifest(res) {
878
+ try {
879
+ const freshPluginJson = this.getFreshPluginJson();
880
+ const manifest = {
881
+ ...freshPluginJson,
882
+ protocol: "axpo",
883
+ buildVersion: this.buildVersion,
884
+ endpoints: {
885
+ plugin: "/plugin.json",
886
+ main: "/main.js",
887
+ ws: `ws://${this.localIP}:${this.port}`
888
+ },
889
+ files: this.getServedFiles()
890
+ };
891
+ res.writeHead(200, {
892
+ "Content-Type": "application/json",
893
+ "Access-Control-Allow-Origin": "*",
894
+ "Cache-Control": "no-cache, no-store, must-revalidate",
895
+ "Pragma": "no-cache",
896
+ "Expires": "0"
897
+ });
898
+ res.end(JSON.stringify(manifest, null, 2));
899
+ } catch (error) {
900
+ console.error("Error serving manifest:", error);
901
+ const manifest = {
902
+ ...this.pluginJson,
903
+ protocol: "axpo",
904
+ buildVersion: this.buildVersion,
905
+ endpoints: {
906
+ plugin: "/plugin.json",
907
+ main: "/main.js",
908
+ ws: `ws://${this.localIP}:${this.port}`
909
+ },
910
+ files: this.getServedFiles()
911
+ };
912
+ res.writeHead(200, {
913
+ "Content-Type": "application/json",
914
+ "Access-Control-Allow-Origin": "*",
915
+ "Cache-Control": "no-cache, no-store, must-revalidate"
916
+ });
917
+ res.end(JSON.stringify(manifest, null, 2));
918
+ }
919
+ }
920
+ serveStatus(res) {
921
+ const status = {
922
+ running: true,
923
+ buildVersion: this.buildVersion,
924
+ clients: this.clients.size,
925
+ maxClients: MAX_CLIENTS,
926
+ uptime: process.uptime(),
927
+ logs: this.logs.size,
928
+ buildStatus: this.statusSection.buildStatus,
929
+ currentIP: this.localIP,
930
+ url: `axpo://${this.localIP}:${this.port}`
931
+ };
932
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
933
+ res.end(JSON.stringify(status, null, 2));
934
+ }
935
+ serveFile(filePath, res) {
936
+ const fullPath = path3.resolve(process.cwd(), filePath);
937
+ if (!fullPath.startsWith(process.cwd())) {
938
+ this.serve403(res);
939
+ return;
940
+ }
941
+ if (!fs3.existsSync(fullPath)) {
942
+ this.serve404(res);
943
+ return;
944
+ }
945
+ try {
946
+ const content = fs3.readFileSync(fullPath);
947
+ const ext = path3.extname(fullPath);
948
+ const contentType = this.getContentType(ext);
949
+ res.writeHead(200, {
950
+ "Content-Type": contentType,
951
+ "Content-Length": content.length.toString(),
952
+ "Access-Control-Allow-Origin": "*"
953
+ });
954
+ res.end(content);
955
+ } catch (error) {
956
+ this.serve500(res, error);
957
+ }
958
+ }
959
+ getContentType(ext) {
960
+ const types = {
961
+ ".html": "text/html",
962
+ ".js": "application/javascript",
963
+ ".json": "application/json",
964
+ ".css": "text/css",
965
+ ".png": "image/png",
966
+ ".jpg": "image/jpeg",
967
+ ".jpeg": "image/jpeg",
968
+ ".gif": "image/gif",
969
+ ".svg": "image/svg+xml",
970
+ ".ico": "image/x-icon"
971
+ };
972
+ return types[ext] || "application/octet-stream";
973
+ }
974
+ getServedFiles() {
975
+ const files = [];
976
+ if (fs3.existsSync("plugin.json")) files.push("plugin.json");
977
+ try {
978
+ const buildDir = getOutDir(this.config);
979
+ if (fs3.existsSync(buildDir)) {
980
+ this.addFilesFromBuildDir(buildDir, files);
981
+ }
982
+ } catch (error) {
983
+ }
984
+ this.config.files?.forEach((file) => {
985
+ if (fs3.existsSync(file)) {
986
+ const stat = fs3.statSync(file);
987
+ if (stat.isFile()) {
988
+ if (!files.includes(file)) {
989
+ files.push(file);
990
+ }
991
+ } else if (stat.isDirectory()) {
992
+ this.addFilesFromFolder(file, file, files);
993
+ }
994
+ }
995
+ });
996
+ this.pluginJson.files?.forEach((file) => {
997
+ if (fs3.existsSync(file) && !files.includes(file)) {
998
+ const stat = fs3.statSync(file);
999
+ if (stat.isFile()) {
1000
+ files.push(file);
1001
+ } else if (stat.isDirectory()) {
1002
+ this.addFilesFromFolder(file, file, files);
1003
+ }
1004
+ }
1005
+ });
1006
+ return [...new Set(files)];
1007
+ }
1008
+ addFilesFromBuildDir(buildDir, files) {
1009
+ if (!fs3.existsSync(buildDir)) return;
1010
+ const entries = fs3.readdirSync(buildDir);
1011
+ entries.forEach((entry) => {
1012
+ const fullPath = path3.join(buildDir, entry);
1013
+ const stat = fs3.statSync(fullPath);
1014
+ if (stat.isDirectory()) {
1015
+ this.addFilesFromBuildDir(fullPath, files);
1016
+ } else {
1017
+ const fileName = path3.basename(fullPath);
1018
+ if (!files.includes(fileName)) {
1019
+ files.push(fileName);
1020
+ }
1021
+ }
1022
+ });
1023
+ }
1024
+ addFilesFromFolder(baseDir, dir, files) {
1025
+ if (!fs3.existsSync(dir)) return;
1026
+ const entries = fs3.readdirSync(dir);
1027
+ entries.forEach((entry) => {
1028
+ const fullPath = path3.join(dir, entry);
1029
+ const stat = fs3.statSync(fullPath);
1030
+ if (stat.isDirectory()) {
1031
+ this.addFilesFromFolder(baseDir, fullPath, files);
1032
+ } else {
1033
+ const relativePath = path3.relative(process.cwd(), fullPath);
1034
+ if (!files.includes(relativePath)) {
1035
+ files.push(relativePath);
1036
+ }
1037
+ }
1038
+ });
1039
+ }
1040
+ handleConnection(ws, req) {
1041
+ if (this.clients.size >= MAX_CLIENTS) {
1042
+ try {
1043
+ ws.close(1008, `Maximum ${MAX_CLIENTS} clients allowed`);
1044
+ } catch (error) {
1045
+ }
1046
+ return;
1047
+ }
1048
+ const clientId = this.generateClientId();
1049
+ const clientNumber = this.getNextClientId();
1050
+ if (clientNumber === 0) {
1051
+ ws.close(1008, "No available client IDs");
1052
+ return;
1053
+ }
1054
+ const freshPluginJson = this.getFreshPluginJson();
1055
+ const client = {
1056
+ ws,
1057
+ id: clientId,
1058
+ clientNumber,
1059
+ connectedAt: /* @__PURE__ */ new Date(),
1060
+ deviceInfo: void 0
1061
+ };
1062
+ this.clients.set(clientId, client);
1063
+ this.clientIdMap.set(clientId, clientNumber);
1064
+ this.scheduleRender();
1065
+ this.sendToClient(ws, {
1066
+ type: "connected",
1067
+ clientId,
1068
+ clientNumber,
1069
+ buildVersion: this.buildVersion,
1070
+ plugin: {
1071
+ name: freshPluginJson.name,
1072
+ id: freshPluginJson.id,
1073
+ version: freshPluginJson.version
1074
+ }
1075
+ });
1076
+ ws.on("message", (data) => {
1077
+ try {
1078
+ const message = JSON.parse(data.toString());
1079
+ this.handleClientMessage(clientId, message);
1080
+ } catch (error) {
1081
+ console.error("Failed to parse message:", error);
1082
+ }
1083
+ });
1084
+ ws.on("close", () => {
1085
+ const disconnectedNumber = this.clientIdMap.get(clientId);
1086
+ const disconnectedClient = this.clients.get(clientId);
1087
+ this.clients.delete(clientId);
1088
+ this.sessionWarnings.delete(clientId);
1089
+ this.releaseClientId(clientId);
1090
+ if (disconnectedNumber) {
1091
+ let disconnectMsg = `Disconnected`;
1092
+ if (disconnectedClient?.deviceInfo) {
1093
+ disconnectMsg += ` (${disconnectedClient.deviceInfo.deviceModel})`;
1094
+ }
1095
+ this.addLog("info", disconnectMsg, disconnectedNumber);
1096
+ this.scrollToBottom();
1097
+ }
1098
+ this.scheduleRender();
1099
+ });
1100
+ ws.on("error", (error) => {
1101
+ const disconnectedNumber = this.clientIdMap.get(clientId);
1102
+ this.clients.delete(clientId);
1103
+ this.sessionWarnings.delete(clientId);
1104
+ this.releaseClientId(clientId);
1105
+ if (disconnectedNumber) {
1106
+ this.addLog("warn", `Error: ${error.message}`, disconnectedNumber);
1107
+ this.scrollToBottom();
1108
+ }
1109
+ this.scheduleRender();
1110
+ });
1111
+ }
1112
+ handleClientMessage(clientId, message) {
1113
+ const client = this.clients.get(clientId);
1114
+ if (message.type === "ping") {
1115
+ if (client) this.sendToClient(client.ws, { type: "pong" });
1116
+ } else if (message.type === "device_info") {
1117
+ if (client && message.deviceInfo) {
1118
+ client.deviceInfo = message.deviceInfo;
1119
+ const deviceModel = message.deviceInfo.deviceModel || "Unknown Device";
1120
+ const acodeVersion = message.deviceInfo.versionCode || "Unknown";
1121
+ this.addLog(
1122
+ "info",
1123
+ `Connected (${deviceModel} \xB7 Acode ${acodeVersion})`,
1124
+ client.clientNumber
1125
+ );
1126
+ this.addDeviceInfoLog(message.deviceInfo, client.clientNumber);
1127
+ this.checkVersionCompatibility(clientId, message.deviceInfo);
1128
+ this.scheduleRender();
1129
+ }
1130
+ } else if (message.type === "log") {
1131
+ const { level, message: logMsg } = message;
1132
+ if (logMsg?.trim() && client) {
1133
+ this.addLog(level || "log", logMsg.trim(), client.clientNumber);
1134
+ }
1135
+ }
1136
+ }
1137
+ sendToClient(ws, message) {
1138
+ if (ws.readyState === WebSocket.OPEN) {
1139
+ ws.send(JSON.stringify(message));
1140
+ }
1141
+ }
1142
+ broadcast(message) {
1143
+ const data = JSON.stringify(message);
1144
+ this.clients.forEach((client) => {
1145
+ if (client.ws.readyState === WebSocket.OPEN) {
1146
+ client.ws.send(data);
1147
+ }
1148
+ });
1149
+ }
1150
+ generateClientId() {
1151
+ return `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1152
+ }
1153
+ getLocalIP() {
1154
+ try {
1155
+ const nets = os.networkInterfaces();
1156
+ const results = [];
1157
+ for (const name of Object.keys(nets)) {
1158
+ const netArray = nets[name];
1159
+ if (!netArray) continue;
1160
+ for (const net of netArray) {
1161
+ const familyV4Value = typeof net.family === "string" ? "IPv4" : 4;
1162
+ if (net.family === familyV4Value && !net.internal) {
1163
+ results.push(net.address);
1164
+ }
1165
+ }
1166
+ }
1167
+ const localNetwork = results.find((ip) => ip.startsWith("192.168."));
1168
+ if (localNetwork) return localNetwork;
1169
+ const corporateNetwork = results.find((ip) => ip.startsWith("10."));
1170
+ if (corporateNetwork) return corporateNetwork;
1171
+ const privateNetwork = results.find((ip) => {
1172
+ const parts = ip.split(".");
1173
+ if (parts[0] === "172") {
1174
+ const second = parseInt(parts[1]);
1175
+ return second >= 16 && second <= 31;
1176
+ }
1177
+ return false;
1178
+ });
1179
+ if (privateNetwork) return privateNetwork;
1180
+ if (results.length > 0) return results[0];
1181
+ } catch (error) {
1182
+ }
1183
+ return "127.0.0.1";
1184
+ }
1185
+ generateQRCode(text) {
1186
+ try {
1187
+ const chunks = [];
1188
+ const originalWrite = process.stdout.write.bind(process.stdout);
1189
+ process.stdout.write = (chunk) => {
1190
+ chunks.push(chunk.toString());
1191
+ return true;
1192
+ };
1193
+ try {
1194
+ qr.generate(text, { small: true });
1195
+ } finally {
1196
+ process.stdout.write = originalWrite;
1197
+ }
1198
+ const capturedOutput = chunks.join("");
1199
+ if (capturedOutput.trim()) {
1200
+ return capturedOutput.split("\n").map((line) => ` ${line}`).join("\n");
1201
+ }
1202
+ return this.getFallbackBox();
1203
+ } catch (error) {
1204
+ return this.getFallbackBox();
1205
+ }
1206
+ }
1207
+ getFallbackBox() {
1208
+ return color.gray(`${color.cyan("npm i qrcode-terminal")}`);
1209
+ }
1210
+ serve404(res) {
1211
+ res.writeHead(404, { "Content-Type": "text/plain" });
1212
+ res.end("404 Not Found");
1213
+ }
1214
+ serve403(res) {
1215
+ res.writeHead(403, { "Content-Type": "text/plain" });
1216
+ res.end("403 Forbidden");
1217
+ }
1218
+ serve500(res, error) {
1219
+ res.writeHead(500, { "Content-Type": "text/plain" });
1220
+ res.end(`500 Internal Server Error
1221
+
1222
+ ${error.message}`);
1223
+ }
1224
+ };
1225
+ var CACHE_DIR = path3.join(os.homedir(), ".cache", "axpo");
1226
+ var CACHE_FILE = path3.join(CACHE_DIR, "version-check.json");
1227
+ var CHECK_INTERVAL = 12 * 60 * 60 * 1e3;
1228
+ var NPM_PACKAGE = "@axpo/cli";
1229
+ function ensureCacheDir() {
1230
+ try {
1231
+ if (!fs3.existsSync(CACHE_DIR)) {
1232
+ fs3.mkdirSync(CACHE_DIR, { recursive: true, mode: 493 });
1233
+ }
1234
+ } catch {
1235
+ }
1236
+ }
1237
+ function getCurrentVersion() {
1238
+ try {
1239
+ const possiblePaths = [
1240
+ path3.join(import.meta.dirname, "../package.json"),
1241
+ path3.join(process.cwd(), "node_modules/@axpo/cli/package.json")
1242
+ ];
1243
+ for (const p of possiblePaths) {
1244
+ if (fs3.existsSync(p)) {
1245
+ const pkg = JSON.parse(fs3.readFileSync(p, "utf-8"));
1246
+ if (pkg.name === NPM_PACKAGE && pkg.version) {
1247
+ return pkg.version;
1248
+ }
1249
+ }
1250
+ }
1251
+ } catch {
1252
+ }
1253
+ return "1.0.0";
1254
+ }
1255
+ function readCache() {
1256
+ try {
1257
+ if (!fs3.existsSync(CACHE_FILE)) return null;
1258
+ const cache = JSON.parse(fs3.readFileSync(CACHE_FILE, "utf-8"));
1259
+ if (!cache.lastCheck || !cache.latestVersion || !cache.nextCheck) return null;
1260
+ if (cache.lastCheck < 0 || cache.nextCheck < cache.lastCheck) return null;
1261
+ return cache;
1262
+ } catch {
1263
+ return null;
1264
+ }
1265
+ }
1266
+ function writeCache(cache) {
1267
+ try {
1268
+ ensureCacheDir();
1269
+ fs3.writeFileSync(CACHE_FILE, JSON.stringify(cache), { encoding: "utf-8", mode: 420 });
1270
+ } catch {
1271
+ }
1272
+ }
1273
+ function shouldCheckVersion() {
1274
+ const cache = readCache();
1275
+ if (!cache) return { shouldCheck: true };
1276
+ if (Date.now() >= cache.nextCheck) return { shouldCheck: true };
1277
+ return { shouldCheck: false, cachedLatest: cache.latestVersion };
1278
+ }
1279
+ function compareVersions(v1, v2) {
1280
+ const parts1 = v1.split(".").map(Number);
1281
+ const parts2 = v2.split(".").map(Number);
1282
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
1283
+ const diff = (parts1[i] || 0) - (parts2[i] || 0);
1284
+ if (diff !== 0) return diff < 0 ? -1 : 1;
1285
+ }
1286
+ return 0;
1287
+ }
1288
+ function useStaleCacheFallback(currentVersion) {
1289
+ const cache = readCache();
1290
+ if (cache?.latestVersion && compareVersions(currentVersion, cache.latestVersion) < 0) {
1291
+ return { isOutdated: true, currentVersion, latestVersion: cache.latestVersion };
1292
+ }
1293
+ return null;
1294
+ }
1295
+ async function checkAxpoVersion() {
1296
+ const currentVersion = getCurrentVersion();
1297
+ const cacheCheck = shouldCheckVersion();
1298
+ if (!cacheCheck.shouldCheck && cacheCheck.cachedLatest) {
1299
+ if (compareVersions(currentVersion, cacheCheck.cachedLatest) < 0) {
1300
+ return { isOutdated: true, currentVersion, latestVersion: cacheCheck.cachedLatest };
1301
+ }
1302
+ return null;
1303
+ }
1304
+ try {
1305
+ const response = await fetch(
1306
+ `https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE)}/latest`,
1307
+ { headers: { Accept: "application/json" }, signal: AbortSignal.timeout(3e3) }
1308
+ );
1309
+ if (!response.ok) return useStaleCacheFallback(currentVersion);
1310
+ const data = await response.json();
1311
+ const latestVersion = data.version;
1312
+ if (!latestVersion) return useStaleCacheFallback(currentVersion);
1313
+ const now = Date.now();
1314
+ writeCache({ lastCheck: now, latestVersion, nextCheck: now + CHECK_INTERVAL });
1315
+ if (compareVersions(currentVersion, latestVersion) < 0) {
1316
+ return { isOutdated: true, currentVersion, latestVersion };
1317
+ }
1318
+ return null;
1319
+ } catch {
1320
+ return useStaleCacheFallback(currentVersion);
1321
+ }
1322
+ }
1323
+ function displayVersionWarning(result) {
1324
+ if (!result.isOutdated || !result.latestVersion) return;
1325
+ const message = [
1326
+ color.dim(`Current: ${result.currentVersion}`),
1327
+ color.green(`Latest: ${result.latestVersion}`),
1328
+ "",
1329
+ color.cyan(`npm install ${NPM_PACKAGE}@latest`),
1330
+ color.dim("to update in your project")
1331
+ ].join("\n");
1332
+ console.log(boxen(message, {
1333
+ title: "Update Available",
1334
+ titleAlignment: "center",
1335
+ padding: 1,
1336
+ margin: 0,
1337
+ borderStyle: "round",
1338
+ borderColor: "yellow",
1339
+ backgroundColor: "#1a1a1a",
1340
+ textAlignment: "center",
1341
+ width: (process.stdout.columns || 80) - 4
1342
+ }));
1343
+ }
1344
+ var API_BASE = process.env.AXPO_API_BASE || "https://acode.app/api";
1345
+ var CREDENTIALS_DIR = path3.join(os.homedir(), ".axpo");
1346
+ var CREDENTIALS_PATH = path3.join(CREDENTIALS_DIR, "credentials.json");
1347
+ var REQUEST_TIMEOUT = 3e4;
1348
+ var UPLOAD_TIMEOUT = 12e4;
1349
+ async function readPluginJsonFromZip(zipPath) {
1350
+ try {
1351
+ const zipData = fs3.readFileSync(zipPath);
1352
+ const zip = await JSZip.loadAsync(zipData);
1353
+ const pluginJsonFile = zip.file("plugin.json");
1354
+ if (!pluginJsonFile) {
1355
+ throw new Error("plugin.json not found in zip file");
1356
+ }
1357
+ const content = await pluginJsonFile.async("text");
1358
+ const pluginJson = JSON.parse(content);
1359
+ if (!pluginJson.id) throw new Error('plugin.json: "id" is required');
1360
+ if (!pluginJson.name) throw new Error('plugin.json: "name" is required');
1361
+ if (!pluginJson.version) throw new Error('plugin.json: "version" is required');
1362
+ if (!pluginJson.author?.name) throw new Error('plugin.json: "author.name" is required');
1363
+ if (!pluginJson.author?.email) throw new Error('plugin.json: "author.email" is required');
1364
+ return pluginJson;
1365
+ } catch (error) {
1366
+ if (error instanceof SyntaxError) {
1367
+ throw new Error("plugin.json contains invalid JSON");
1368
+ }
1369
+ if (error.message.includes("plugin.json")) {
1370
+ throw error;
1371
+ }
1372
+ throw new Error(`Failed to read zip file: ${error.message}`);
1373
+ }
1374
+ }
1375
+ function getZipSize(zipPath) {
1376
+ try {
1377
+ const stats = fs3.statSync(zipPath);
1378
+ return (stats.size / (1024 * 1024)).toFixed(2);
1379
+ } catch (error) {
1380
+ return "0.00";
1381
+ }
1382
+ }
1383
+ function ensureCredentialsDir() {
1384
+ try {
1385
+ if (!fs3.existsSync(CREDENTIALS_DIR)) {
1386
+ fs3.mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 448 });
1387
+ }
1388
+ } catch (error) {
1389
+ throw new Error(`Failed to create credentials directory: ${error.message}`);
1390
+ }
1391
+ }
1392
+ function saveCredentials(credentials) {
1393
+ try {
1394
+ ensureCredentialsDir();
1395
+ const content = JSON.stringify(credentials, null, 2);
1396
+ fs3.writeFileSync(CREDENTIALS_PATH, content, {
1397
+ encoding: "utf-8",
1398
+ mode: 384
1399
+ });
1400
+ } catch (error) {
1401
+ console.warn(color.yellow("\u26A0 Failed to save credentials"));
1402
+ }
1403
+ }
1404
+ function loadCredentials() {
1405
+ try {
1406
+ if (fs3.existsSync(CREDENTIALS_PATH)) {
1407
+ const content = fs3.readFileSync(CREDENTIALS_PATH, "utf-8");
1408
+ return JSON.parse(content);
1409
+ }
1410
+ } catch (error) {
1411
+ }
1412
+ return null;
1413
+ }
1414
+ function clearCredentials() {
1415
+ try {
1416
+ if (fs3.existsSync(CREDENTIALS_PATH)) {
1417
+ fs3.unlinkSync(CREDENTIALS_PATH);
1418
+ }
1419
+ } catch (error) {
1420
+ }
1421
+ }
1422
+ function parseApiError(error) {
1423
+ if (error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT") {
1424
+ return "Cannot connect to Acode registry. Please check your internet connection.";
1425
+ }
1426
+ const apiMessage = error.response?.data?.message;
1427
+ if (apiMessage) return apiMessage;
1428
+ const status = error.response?.status;
1429
+ if (status) return `API error (${status}): ${error.message}`;
1430
+ return error.message || "Unknown error";
1431
+ }
1432
+ async function checkPluginExists(pluginId) {
1433
+ const spinner = createSpinner("Checking plugin registry...").start();
1434
+ try {
1435
+ const response = await axios.get(`${API_BASE}/plugin/${pluginId}`, {
1436
+ timeout: REQUEST_TIMEOUT,
1437
+ validateStatus: (status) => status === 200 || status === 404
1438
+ });
1439
+ spinner.stop();
1440
+ if (response.status === 404) {
1441
+ return { exists: false };
1442
+ }
1443
+ return {
1444
+ exists: true,
1445
+ plugin: {
1446
+ id: response.data.id,
1447
+ name: response.data.name,
1448
+ version: response.data.version,
1449
+ downloads: response.data.downloads || 0,
1450
+ lastUpdated: response.data.updatedAt || response.data.createdAt
1451
+ }
1452
+ };
1453
+ } catch (error) {
1454
+ spinner.error({ text: "Failed to check plugin registry" });
1455
+ throw new Error(parseApiError(error));
1456
+ }
1457
+ }
1458
+ async function login(providedToken) {
1459
+ if (providedToken) {
1460
+ const spinner = createSpinner("Validating token...").start();
1461
+ try {
1462
+ await axios.get(`${API_BASE}/user/profile`, {
1463
+ headers: { "Cookie": `token=${providedToken}` },
1464
+ timeout: REQUEST_TIMEOUT
1465
+ });
1466
+ spinner.success({ text: "Token validated" });
1467
+ return providedToken;
1468
+ } catch (error) {
1469
+ spinner.error({ text: "Invalid token" });
1470
+ throw new Error("Provided token is invalid or expired");
1471
+ }
1472
+ }
1473
+ let credentials = loadCredentials();
1474
+ let attemptCount = 0;
1475
+ const maxAttempts = 3;
1476
+ while (attemptCount < maxAttempts) {
1477
+ attemptCount++;
1478
+ if (!credentials) {
1479
+ console.log("");
1480
+ console.log(color.cyan("Acode Account Login"));
1481
+ console.log(color.dim("Credentials will be saved in ~/.axpo/credentials.json"));
1482
+ console.log("");
1483
+ const email = await input({
1484
+ message: "Email:",
1485
+ validate: (val) => val.trim() ? true : "Email is required"
1486
+ });
1487
+ const pwd = await password({
1488
+ message: "Password:",
1489
+ mask: "*",
1490
+ validate: (val) => val ? true : "Password is required"
1491
+ });
1492
+ credentials = { email, password: pwd };
1493
+ }
1494
+ const spinner = createSpinner("Logging in...").start();
1495
+ try {
1496
+ const response = await axios.post(
1497
+ `${API_BASE}/login`,
1498
+ {
1499
+ email: credentials.email,
1500
+ password: credentials.password
1501
+ },
1502
+ {
1503
+ timeout: REQUEST_TIMEOUT,
1504
+ headers: { "Content-Type": "application/json" }
1505
+ }
1506
+ );
1507
+ const setCookieHeader = response.headers["set-cookie"];
1508
+ if (!setCookieHeader || setCookieHeader.length === 0) {
1509
+ throw new Error("No token received");
1510
+ }
1511
+ const token = setCookieHeader[0].split(";")[0].split("=")[1];
1512
+ if (!token) {
1513
+ throw new Error("Failed to extract token");
1514
+ }
1515
+ spinner.success({ text: "Login successful!" });
1516
+ console.log("");
1517
+ saveCredentials(credentials);
1518
+ return token;
1519
+ } catch (error) {
1520
+ const errorMessage = parseApiError(error);
1521
+ spinner.error({ text: errorMessage });
1522
+ credentials = null;
1523
+ clearCredentials();
1524
+ if (attemptCount < maxAttempts) {
1525
+ console.log("");
1526
+ const retry = await confirm({
1527
+ message: `Try again? (${maxAttempts - attemptCount} attempt${maxAttempts - attemptCount > 1 ? "s" : ""} left)`,
1528
+ default: true
1529
+ });
1530
+ if (!retry) {
1531
+ console.log(color.dim("Login cancelled"));
1532
+ console.log("");
1533
+ return null;
1534
+ }
1535
+ }
1536
+ }
1537
+ }
1538
+ console.log("");
1539
+ console.log(color.red("\u2717 Maximum login attempts reached"));
1540
+ console.log("");
1541
+ return null;
1542
+ }
1543
+ async function uploadPlugin(token, zipPath, isUpdate, pluginJson, zipSizeMB) {
1544
+ const actionText = isUpdate ? "Updating plugin" : "Publishing plugin";
1545
+ const spinner = createSpinner(actionText + "...").start();
1546
+ try {
1547
+ const form = new FormData();
1548
+ form.append("plugin", createReadStream(zipPath));
1549
+ const startTime = Date.now();
1550
+ const method = isUpdate ? "put" : "post";
1551
+ let lastProgress = 0;
1552
+ await axios({
1553
+ method,
1554
+ url: `${API_BASE}/plugin`,
1555
+ data: form,
1556
+ headers: {
1557
+ ...form.getHeaders(),
1558
+ "Cookie": `token=${token}`
1559
+ },
1560
+ timeout: UPLOAD_TIMEOUT,
1561
+ maxContentLength: Infinity,
1562
+ maxBodyLength: Infinity,
1563
+ onUploadProgress: (progressEvent) => {
1564
+ if (progressEvent.total) {
1565
+ const percentage = Math.round(progressEvent.loaded * 100 / progressEvent.total);
1566
+ if (percentage - lastProgress >= 10) {
1567
+ lastProgress = percentage;
1568
+ spinner.update({ text: `${actionText}... ${percentage}%` });
1569
+ }
1570
+ }
1571
+ }
1572
+ });
1573
+ const duration = Date.now() - startTime;
1574
+ spinner.success({ text: `${isUpdate ? "Update" : "Publish"} successful!` });
1575
+ console.log("");
1576
+ console.log(color.dim(" Plugin Details:"));
1577
+ console.log(color.dim(` \u2022 Name: ${pluginJson.name}`));
1578
+ console.log(color.dim(` \u2022 Version: ${pluginJson.version}`));
1579
+ console.log(color.dim(` \u2022 ID: ${pluginJson.id}`));
1580
+ console.log(color.dim(` \u2022 Size: ${zipSizeMB} MB`));
1581
+ console.log(color.dim(` \u2022 Upload time: ${(duration / 1e3).toFixed(1)}s`));
1582
+ console.log("");
1583
+ console.log(color.cyan(` \u{1F517} https://acode.app/plugin/${pluginJson.id}`));
1584
+ console.log("");
1585
+ } catch (error) {
1586
+ const errorMessage = parseApiError(error);
1587
+ spinner.error({ text: "Upload failed" });
1588
+ throw new Error(errorMessage);
1589
+ }
1590
+ }
1591
+ async function publish(options = {}) {
1592
+ try {
1593
+ console.log("");
1594
+ console.log(color.bold(color.white("\u{1F4E6} Publishing to Acode Plugin Registry")));
1595
+ console.log("");
1596
+ const envToken = process.env.AXPO_TOKEN;
1597
+ const token = options.token || envToken;
1598
+ let config;
1599
+ try {
1600
+ config = await loadConfig();
1601
+ } catch (error) {
1602
+ config = {};
1603
+ }
1604
+ let zipFilename = options.zipFilename || config.zipFilename || "Plugin.zip";
1605
+ if (!zipFilename.endsWith(".zip")) {
1606
+ zipFilename += ".zip";
1607
+ }
1608
+ const zipPath = path3.resolve(process.cwd(), zipFilename);
1609
+ if (!fs3.existsSync(zipPath)) {
1610
+ console.error(color.red(`\u2717 Zip file not found: ${zipFilename}`));
1611
+ console.log("");
1612
+ console.log(color.dim(" Run one of these commands first:"));
1613
+ console.log(color.cyan(" \u2022 axpo build --zip"));
1614
+ console.log(color.cyan(" \u2022 axpo zip"));
1615
+ console.log("");
1616
+ process.exit(1);
1617
+ }
1618
+ const spinner = createSpinner("Reading plugin.json from zip...").start();
1619
+ let pluginJson;
1620
+ try {
1621
+ pluginJson = await readPluginJsonFromZip(zipPath);
1622
+ spinner.success({ text: "Plugin.json loaded from zip" });
1623
+ } catch (error) {
1624
+ spinner.error({ text: "Failed to read plugin.json from zip" });
1625
+ console.log("");
1626
+ console.error(color.red(`\u2717 ${error.message}`));
1627
+ console.log("");
1628
+ console.log(color.dim(" Make sure your zip contains plugin.json in the root"));
1629
+ console.log("");
1630
+ process.exit(1);
1631
+ }
1632
+ const zipSizeMB = getZipSize(zipPath);
1633
+ let pluginCheck;
1634
+ try {
1635
+ pluginCheck = await checkPluginExists(pluginJson.id);
1636
+ } catch (error) {
1637
+ console.error(color.red(`\u2717 ${error.message}`));
1638
+ console.log("");
1639
+ process.exit(1);
1640
+ }
1641
+ const isUpdate = pluginCheck.exists;
1642
+ console.log("");
1643
+ console.log(color.dim("Plugin Information (from zip):"));
1644
+ console.log(color.dim(`\u2022 Name: ${pluginJson.name}`));
1645
+ console.log(color.dim(`\u2022 ID: ${pluginJson.id}`));
1646
+ console.log(color.dim(`\u2022 Version: ${pluginJson.version}`));
1647
+ console.log(color.dim(`\u2022 Author: ${pluginJson.author.name} <${pluginJson.author.email}>`));
1648
+ console.log(color.dim(`\u2022 Zip: ${zipFilename} (${zipSizeMB} MB)`));
1649
+ if (isUpdate && pluginCheck.plugin) {
1650
+ console.log("");
1651
+ console.log(color.yellow("\u26A0 Plugin already exists in registry"));
1652
+ console.log(color.dim(` Current version: ${pluginCheck.plugin.version}`));
1653
+ console.log(color.dim(` Downloads: ${pluginCheck.plugin.downloads}`));
1654
+ console.log(color.dim(` New version: ${pluginJson.version}`));
1655
+ if (pluginCheck.plugin.lastUpdated) {
1656
+ console.log(color.dim(` Last updated: ${new Date(pluginCheck.plugin.lastUpdated).toLocaleString()}`));
1657
+ }
1658
+ }
1659
+ console.log("");
1660
+ if (!options.force) {
1661
+ const confirmMessage = isUpdate ? `Update plugin to version ${pluginJson.version}?` : `Publish ${pluginJson.name} to Acode registry?`;
1662
+ const proceed = await confirm({
1663
+ message: confirmMessage,
1664
+ default: false
1665
+ });
1666
+ if (!proceed) {
1667
+ console.log(color.dim("Publish cancelled"));
1668
+ console.log("");
1669
+ return;
1670
+ }
1671
+ console.log("");
1672
+ }
1673
+ const authToken = await login(token);
1674
+ if (!authToken) {
1675
+ console.error(color.red("\u2717 Publish failed: Authentication required"));
1676
+ console.log("");
1677
+ process.exit(1);
1678
+ }
1679
+ await uploadPlugin(authToken, zipPath, isUpdate, pluginJson, zipSizeMB);
1680
+ } catch (error) {
1681
+ console.error(color.red(`\u2717 ${error.message}`));
1682
+ console.log("");
1683
+ if (process.env.AXPO_DEBUG) {
1684
+ console.error(error);
1685
+ }
1686
+ process.exit(1);
1687
+ }
1688
+ }
1689
+ async function logout() {
1690
+ try {
1691
+ const credentialsExist = fs3.existsSync(CREDENTIALS_PATH);
1692
+ if (!credentialsExist) {
1693
+ console.log("");
1694
+ console.log(color.dim("No saved credentials found"));
1695
+ console.log("");
1696
+ return;
1697
+ }
1698
+ clearCredentials();
1699
+ console.log("");
1700
+ console.log(color.green("\u2713 Logged out successfully"));
1701
+ console.log(color.dim(" Credentials cleared from ~/.axpo/credentials.json"));
1702
+ console.log("");
1703
+ } catch (error) {
1704
+ console.error(color.red(`\u2717 Logout failed: ${error.message}`));
1705
+ console.log("");
1706
+ process.exit(1);
1707
+ }
1708
+ }
1709
+
1710
+ // src/build.ts
1711
+ var devServerInstance = null;
1712
+ var versionCheckDone = false;
1713
+ var logLevelWarningShown = false;
1714
+ function createAxpoTransformPlugin(pluginJson) {
1715
+ return {
1716
+ name: "axpo-transform",
1717
+ setup(build3) {
1718
+ build3.onLoad({ filter: /src[\/\\]main\.(ts|js)$/ }, async (args) => {
1719
+ let contents = await fs3.promises.readFile(args.path, "utf8");
1720
+ const hasExportDefault = /export\s+default\s+/.test(contents);
1721
+ if (!hasExportDefault) {
1722
+ return {
1723
+ contents: "",
1724
+ errors: [{
1725
+ text: `Missing 'export default' in main file`,
1726
+ location: { file: args.path, line: 1, column: 0, length: 0, lineText: "" }
1727
+ }]
1728
+ };
1729
+ }
1730
+ if (!contents.includes("__createAxpoPlugin")) {
1731
+ if (contents.includes('from "@axpo/sdk"') || contents.includes("from '@axpo/sdk'")) {
1732
+ contents = contents.replace(
1733
+ /import\s*{([^}]+)}\s*from\s*['"]@axpo\/sdk['"]/,
1734
+ (m, imports) => {
1735
+ if (!imports.includes("__createAxpoPlugin")) {
1736
+ return `import { ${imports.trim()}, __createAxpoPlugin } from '@axpo/sdk'`;
1737
+ }
1738
+ return m;
1739
+ }
1740
+ );
1741
+ } else {
1742
+ contents = `import { __createAxpoPlugin } from '@axpo/sdk';
1743
+ ${contents}`;
1744
+ }
1745
+ }
1746
+ let transformed = contents;
1747
+ const pluginVarName = "__axpoPluginExport";
1748
+ transformed = transformed.replace(/export\s+default\s+/, `const ${pluginVarName} = `);
1749
+ transformed += `
1750
+
1751
+ __createAxpoPlugin('${pluginJson.id}', ${pluginVarName});
1752
+ `;
1753
+ return {
1754
+ contents: transformed,
1755
+ loader: args.path.endsWith(".ts") ? "ts" : "js"
1756
+ };
1757
+ });
1758
+ }
1759
+ };
1760
+ }
1761
+ function createPluginMetaPlugin(pluginJson) {
1762
+ return {
1763
+ name: "plugin-meta",
1764
+ setup(build3) {
1765
+ build3.onResolve({ filter: /^\.\.?\/axpo\.config(?:\.ts|\.js)?$/ }, (args) => {
1766
+ return { path: args.path, namespace: "plugin-meta" };
1767
+ });
1768
+ build3.onLoad({ filter: /.*/, namespace: "plugin-meta" }, () => {
1769
+ return {
1770
+ contents: `export default ${JSON.stringify({
1771
+ plugin: {
1772
+ id: pluginJson.id,
1773
+ name: pluginJson.name,
1774
+ version: pluginJson.version,
1775
+ author: pluginJson.author,
1776
+ description: pluginJson.description || "",
1777
+ icon: pluginJson.icon || "icon.png",
1778
+ license: pluginJson.license || "MIT"
1779
+ }
1780
+ }, null, 2)};`,
1781
+ loader: "js"
1782
+ };
1783
+ });
1784
+ }
1785
+ };
1786
+ }
1787
+ function displayLogLevelWarning() {
1788
+ const message = [
1789
+ color.bold(color.yellow("Configuration Warning")),
1790
+ "",
1791
+ color.dim("buildOptions.logLevel cannot be changed."),
1792
+ color.dim('It must remain "silent" for dev server compatibility.'),
1793
+ "",
1794
+ color.cyan('Please remove "logLevel" from your buildOptions.')
1795
+ ].join("\n");
1796
+ console.log(boxen(message, {
1797
+ title: "Warning",
1798
+ titleAlignment: "center",
1799
+ padding: 1,
1800
+ margin: 0,
1801
+ borderStyle: "single",
1802
+ borderColor: "yellow",
1803
+ backgroundColor: "#1a1a1a",
1804
+ textAlignment: "center",
1805
+ width: (process.stdout.columns || 80) - 4
1806
+ }));
1807
+ }
1808
+ async function showStartupWarnings(config) {
1809
+ if (versionCheckDone) return;
1810
+ const versionCheck = await checkAxpoVersion();
1811
+ if (versionCheck) displayVersionWarning(versionCheck);
1812
+ if ("logLevel" in (config.buildOptions || {}) && !logLevelWarningShown) {
1813
+ displayLogLevelWarning();
1814
+ logLevelWarningShown = true;
1815
+ }
1816
+ versionCheckDone = true;
1817
+ }
1818
+ function prepareBuildOptions(userOptions, pluginJson) {
1819
+ const cleanOptions = { ...userOptions };
1820
+ delete cleanOptions.logLevel;
1821
+ return {
1822
+ ...cleanOptions,
1823
+ plugins: [
1824
+ createAxpoTransformPlugin(pluginJson),
1825
+ createPluginMetaPlugin(pluginJson),
1826
+ ...cleanOptions.plugins || []
1827
+ ],
1828
+ logLevel: "silent",
1829
+ metafile: true,
1830
+ format: cleanOptions.format || "iife"
1831
+ };
1832
+ }
1833
+ function getBuildOutputFiles(outDir) {
1834
+ const files = [];
1835
+ if (!fs3.existsSync(outDir)) return files;
1836
+ function scanDir(dir) {
1837
+ fs3.readdirSync(dir).forEach((entry) => {
1838
+ const fullPath = path3.join(dir, entry);
1839
+ const stat = fs3.statSync(fullPath);
1840
+ if (stat.isDirectory()) {
1841
+ scanDir(fullPath);
1842
+ } else {
1843
+ files.push({
1844
+ path: path3.relative(process.cwd(), fullPath),
1845
+ size: stat.size
1846
+ });
1847
+ }
1848
+ });
1849
+ }
1850
+ scanDir(outDir);
1851
+ return files.sort((a, b) => a.path.localeCompare(b.path));
1852
+ }
1853
+ function analyzeAndShowWarnings(pluginJson) {
1854
+ try {
1855
+ const analyzer = new CodeAnalyzer("src", pluginJson.minVersionCode);
1856
+ const analysis = analyzer.analyzeAPIUsage();
1857
+ if (analysis.apis.size > 0) {
1858
+ console.log(color.cyan("\u{1F4CA} API Usage Analysis:"));
1859
+ console.log(color.dim(` Found ${analysis.apis.size} API(s): ${Array.from(analysis.apis).join(", ")}`));
1860
+ if (analysis.hasErrors) {
1861
+ console.log(color.red("\n\u274C Critical Issues:"));
1862
+ analysis.warnings.filter((w) => w.severity === "error").forEach((w) => {
1863
+ console.log(color.red(` \u2022 ${w.api} requires versionCode ${w.requiredVersion}, but plugin minVersionCode is ${pluginJson.minVersionCode || "not set"}`));
1864
+ console.log(color.dim(` ${w.file}:${w.line}`));
1865
+ });
1866
+ }
1867
+ if (analysis.hasWarnings) {
1868
+ console.log(color.yellow("\n\u26A0\uFE0F Warnings:"));
1869
+ analysis.warnings.filter((w) => w.severity === "warn").forEach((w) => {
1870
+ console.log(color.yellow(` \u2022 ${w.api} requires versionCode ${w.requiredVersion}. Consider adding "minVersionCode": ${w.requiredVersion} to plugin.json`));
1871
+ console.log(color.dim(` ${w.file}:${w.line}`));
1872
+ });
1873
+ }
1874
+ if (analysis.hasErrors || analysis.hasWarnings) {
1875
+ const maxRequired = Math.max(...analysis.warnings.map((w) => w.requiredVersion));
1876
+ console.log(color.yellow("\n\u{1F4A1} Recommendation:"));
1877
+ if (!pluginJson.minVersionCode) {
1878
+ console.log(color.yellow(` \u2022 Add "minVersionCode": ${maxRequired} to plugin.json`));
1879
+ } else if (pluginJson.minVersionCode < maxRequired) {
1880
+ console.log(color.yellow(` \u2022 Update "minVersionCode" from ${pluginJson.minVersionCode} to ${maxRequired} in plugin.json`));
1881
+ }
1882
+ }
1883
+ console.log("");
1884
+ }
1885
+ } catch (error) {
1886
+ console.warn(color.yellow("\u26A0 API analysis warning:"), error.message);
1887
+ console.log("");
1888
+ }
1889
+ }
1890
+ async function build2(options = {}) {
1891
+ const config = await loadConfig();
1892
+ const pluginJson = loadPluginJson();
1893
+ validateConfig(config, pluginJson);
1894
+ const startTime = Date.now();
1895
+ const isStandalone = !devServerInstance;
1896
+ if (isStandalone) {
1897
+ await showStartupWarnings(config);
1898
+ analyzeAndShowWarnings(pluginJson);
1899
+ }
1900
+ const spinner = isStandalone ? createSpinner("Building...").start() : null;
1901
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
1902
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
1903
+ try {
1904
+ const outDir = getOutDir(config);
1905
+ const resolvedOutDir = path3.resolve(process.cwd(), outDir);
1906
+ if (!fs3.existsSync(resolvedOutDir)) {
1907
+ fs3.mkdirSync(resolvedOutDir, { recursive: true });
1908
+ }
1909
+ if (isStandalone) {
1910
+ process.stdout.write = () => true;
1911
+ process.stderr.write = () => true;
1912
+ }
1913
+ const result = await esbuild.build(prepareBuildOptions(config.buildOptions, pluginJson));
1914
+ if (isStandalone) {
1915
+ process.stdout.write = originalStdoutWrite;
1916
+ process.stderr.write = originalStderrWrite;
1917
+ }
1918
+ if (result.errors.length > 0) {
1919
+ const errorMessages = result.errors.map((error) => {
1920
+ let msg = error.text;
1921
+ if (error.location) {
1922
+ msg += ` (${error.location.file}:${error.location.line}:${error.location.column})`;
1923
+ }
1924
+ return msg;
1925
+ }).join("\n");
1926
+ if (devServerInstance) {
1927
+ devServerInstance.setBuildStatus("failed", void 0, errorMessages, result.errors.length);
1928
+ if (result.warnings.length > 0) {
1929
+ result.warnings.forEach((warning) => {
1930
+ const msg = warning.location ? `${warning.text} (${warning.location.file}:${warning.location.line})` : warning.text;
1931
+ devServerInstance.addLog("warn", msg);
1932
+ });
1933
+ }
1934
+ } else {
1935
+ spinner?.error({ text: `Build failed with ${result.errors.length} error(s)` });
1936
+ console.log("");
1937
+ result.errors.forEach((error) => {
1938
+ console.log(color.red(` \u2022 ${error.text}`));
1939
+ if (error.location) {
1940
+ console.log(color.dim(` ${error.location.file}:${error.location.line}:${error.location.column}`));
1941
+ }
1942
+ });
1943
+ if (result.warnings.length > 0) {
1944
+ console.log("");
1945
+ result.warnings.forEach((warning) => {
1946
+ const msg = warning.location ? `${warning.text} (${warning.location.file}:${warning.location.line})` : warning.text;
1947
+ console.warn(color.yellow(` \u26A0 ${msg}`));
1948
+ });
1949
+ }
1950
+ console.log("");
1951
+ }
1952
+ return false;
1953
+ }
1954
+ if (devServerInstance) {
1955
+ try {
1956
+ const analysis = new CodeAnalyzer("src", pluginJson.minVersionCode).analyzeAPIUsage();
1957
+ devServerInstance.analyzeAndReportAPIs({
1958
+ warnings: analysis.warnings,
1959
+ apis: analysis.apis,
1960
+ hasErrors: analysis.hasErrors,
1961
+ hasWarnings: analysis.hasWarnings
1962
+ });
1963
+ } catch (error) {
1964
+ devServerInstance.addLog("warn", `API analyzer error: ${error.message}`);
1965
+ }
1966
+ }
1967
+ const duration = Date.now() - startTime;
1968
+ if (devServerInstance) {
1969
+ devServerInstance.setBuildStatus("success", duration);
1970
+ if (result.warnings.length > 0) {
1971
+ result.warnings.forEach((warning) => {
1972
+ const msg = warning.location ? `${warning.text} (${warning.location.file}:${warning.location.line})` : warning.text;
1973
+ devServerInstance.addLog("warn", msg);
1974
+ });
1975
+ }
1976
+ } else {
1977
+ spinner?.success({ text: `Build complete in ${duration}ms` });
1978
+ getBuildOutputFiles(resolvedOutDir).forEach((file) => {
1979
+ console.log(color.dim(` ${file.path} (${(file.size / 1024).toFixed(2)} KB)`));
1980
+ });
1981
+ if (result.warnings.length > 0) {
1982
+ console.log("");
1983
+ result.warnings.forEach((warning) => {
1984
+ const msg = warning.location ? `${warning.text} (${warning.location.file}:${warning.location.line})` : warning.text;
1985
+ console.warn(color.yellow(` \u26A0 ${msg}`));
1986
+ });
1987
+ }
1988
+ console.log("");
1989
+ }
1990
+ if (options.zip) await createZip(options.zipFilename);
1991
+ return true;
1992
+ } catch (error) {
1993
+ if (isStandalone && process.stdout.write !== originalStdoutWrite) {
1994
+ process.stdout.write = originalStdoutWrite;
1995
+ process.stderr.write = originalStderrWrite;
1996
+ }
1997
+ const errorMsg = error.message || String(error);
1998
+ devServerInstance ? devServerInstance.setBuildStatus("failed", void 0, errorMsg, 1) : spinner?.error({ text: `Build failed: ${errorMsg}` });
1999
+ if (!devServerInstance) console.log("");
2000
+ return false;
2001
+ }
2002
+ }
2003
+ async function dev() {
2004
+ const config = await loadConfig();
2005
+ const pluginJson = loadPluginJson();
2006
+ validateConfig(config, pluginJson);
2007
+ await showStartupWarnings(config);
2008
+ const devConfig = config.dev || {};
2009
+ const port = devConfig.port || 3e3;
2010
+ const devServer = new AxpoDevServer({ port, config, pluginJson });
2011
+ devServerInstance = devServer;
2012
+ let isShuttingDown = false;
2013
+ const gracefulShutdown = async () => {
2014
+ if (isShuttingDown) return;
2015
+ isShuttingDown = true;
2016
+ logUpdate.done();
2017
+ console.log("");
2018
+ const spinner = createSpinner("Stopping dev server...").start();
2019
+ try {
2020
+ if (watcher) await watcher.close();
2021
+ await devServer.stop();
2022
+ spinner.success({ text: "Server stopped successfully" });
2023
+ } catch (error) {
2024
+ spinner.error({ text: "Error stopping server" });
2025
+ }
2026
+ console.log("");
2027
+ process.exit(0);
2028
+ };
2029
+ let watcher = null;
2030
+ process.on("SIGINT", gracefulShutdown);
2031
+ process.on("SIGTERM", gracefulShutdown);
2032
+ await devServer.start();
2033
+ devServer.setBuildStatus("building");
2034
+ await build2();
2035
+ watcher = chokidar.watch([
2036
+ "src/**/*",
2037
+ "axpo.config.{ts,mjs,js,cjs}",
2038
+ // ✅ Support all config formats
2039
+ "plugin.json"
2040
+ ], {
2041
+ ignored: [/(^|[\/\\])\../, "**/node_modules/**", "**/build/**", "**/*.zip"],
2042
+ persistent: true,
2043
+ ignoreInitial: true,
2044
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
2045
+ });
2046
+ let isBuilding = false;
2047
+ let buildQueued = false;
2048
+ let debounceTimer = null;
2049
+ const doBuild = async () => {
2050
+ if (isBuilding) {
2051
+ buildQueued = true;
2052
+ return;
2053
+ }
2054
+ isBuilding = true;
2055
+ try {
2056
+ devServer.setBuildStatus("building");
2057
+ if (await build2()) devServer.notifyReload();
2058
+ } catch (error) {
2059
+ }
2060
+ isBuilding = false;
2061
+ if (buildQueued) {
2062
+ buildQueued = false;
2063
+ setTimeout(doBuild, 100);
2064
+ }
2065
+ };
2066
+ const handleFileChange = () => {
2067
+ if (debounceTimer) clearTimeout(debounceTimer);
2068
+ debounceTimer = setTimeout(doBuild, 50);
2069
+ };
2070
+ watcher.on("change", handleFileChange);
2071
+ watcher.on("add", handleFileChange);
2072
+ watcher.on("unlink", handleFileChange);
2073
+ setupKeyboardHandler(devServer, gracefulShutdown);
2074
+ process.stdin.resume();
2075
+ }
2076
+ function setupKeyboardHandler(devServer, gracefulShutdown) {
2077
+ if (process.stdin.isTTY) {
2078
+ readline.emitKeypressEvents(process.stdin);
2079
+ process.stdin.setRawMode(true);
2080
+ }
2081
+ process.stdin.on("keypress", async (str, key) => {
2082
+ if (!key) return;
2083
+ if (key.ctrl && key.name === "c") return await gracefulShutdown();
2084
+ switch (key.name) {
2085
+ case "r":
2086
+ devServer.addLog("info", "Manual reload");
2087
+ devServer.notifyReload();
2088
+ break;
2089
+ case "c":
2090
+ devServer.clearLogs();
2091
+ break;
2092
+ case "q":
2093
+ await gracefulShutdown();
2094
+ break;
2095
+ case "up":
2096
+ devServer.scrollUp();
2097
+ break;
2098
+ case "down":
2099
+ devServer.scrollDown();
2100
+ break;
2101
+ case "home":
2102
+ devServer.scrollToTop();
2103
+ break;
2104
+ case "end":
2105
+ devServer.scrollToBottom();
2106
+ break;
2107
+ }
2108
+ });
2109
+ }
2110
+ async function createZip(commandZipFilename) {
2111
+ const config = await loadConfig();
2112
+ const pluginJson = loadPluginJson();
2113
+ validateConfig(config, pluginJson);
2114
+ let zipFilename = commandZipFilename || config.zipFilename || "Plugin.zip";
2115
+ if (!zipFilename.endsWith(".zip")) zipFilename += ".zip";
2116
+ const spinner = createSpinner(`Creating ${zipFilename}...`).start();
2117
+ const startTime = Date.now();
2118
+ try {
2119
+ const zip = new JSZip();
2120
+ const buildDir = getOutDir(config);
2121
+ if (!fs3.existsSync(buildDir)) {
2122
+ spinner.error({ text: `Build folder not found: ${buildDir}` });
2123
+ console.log("");
2124
+ throw new Error(`Build folder not found: ${buildDir}
2125
+ Run "axpo build" first.`);
2126
+ }
2127
+ let filesAdded = 0;
2128
+ zip.file("plugin.json", JSON.stringify(pluginJson, null, 2));
2129
+ filesAdded++;
2130
+ const addFolder = (folderPath, zipRoot = "") => {
2131
+ if (!fs3.existsSync(folderPath)) return;
2132
+ fs3.readdirSync(folderPath).forEach((file) => {
2133
+ const filePath = path3.join(folderPath, file);
2134
+ try {
2135
+ const stat = fs3.statSync(filePath);
2136
+ if (stat.isDirectory()) {
2137
+ const newZipPath = zipRoot ? path3.join(zipRoot, file) : file;
2138
+ zip.folder(newZipPath);
2139
+ addFolder(filePath, newZipPath);
2140
+ } else if (stat.isFile()) {
2141
+ zip.file(zipRoot ? path3.join(zipRoot, file) : file, fs3.readFileSync(filePath));
2142
+ filesAdded++;
2143
+ }
2144
+ } catch (error) {
2145
+ }
2146
+ });
2147
+ };
2148
+ addFolder(buildDir);
2149
+ const allFiles = /* @__PURE__ */ new Set();
2150
+ config.files?.forEach((f) => allFiles.add(f));
2151
+ pluginJson.files?.forEach((f) => allFiles.add(f));
2152
+ allFiles.forEach((file) => {
2153
+ const filePath = path3.resolve(process.cwd(), file);
2154
+ if (fs3.existsSync(filePath)) {
2155
+ const stat = fs3.statSync(filePath);
2156
+ if (stat.isFile()) {
2157
+ zip.file(file, fs3.readFileSync(filePath));
2158
+ filesAdded++;
2159
+ } else if (stat.isDirectory()) {
2160
+ addFolder(filePath, file);
2161
+ }
2162
+ }
2163
+ });
2164
+ const content = await zip.generateAsync({
2165
+ type: "nodebuffer",
2166
+ compression: "DEFLATE",
2167
+ compressionOptions: { level: 9 }
2168
+ });
2169
+ fs3.writeFileSync(zipFilename, content);
2170
+ const duration = Date.now() - startTime;
2171
+ const sizeKB = (fs3.statSync(zipFilename).size / 1024).toFixed(2);
2172
+ spinner.success({ text: `${zipFilename} created in ${duration}ms (${sizeKB} KB, ${filesAdded} files)` });
2173
+ console.log("");
2174
+ if (devServerInstance) {
2175
+ devServerInstance.addLog("info", `${zipFilename} created in ${duration}ms (${sizeKB} KB, ${filesAdded} files)`);
2176
+ }
2177
+ } catch (error) {
2178
+ if (!error.message?.includes("Build folder not found")) {
2179
+ spinner.error({ text: `Zip creation failed: ${error.message}` });
2180
+ console.log("");
2181
+ }
2182
+ if (devServerInstance) {
2183
+ devServerInstance.addLog("error", `Zip creation failed: ${error.message}`);
2184
+ }
2185
+ }
2186
+ }
2187
+ async function clean() {
2188
+ const spinner = createSpinner("Cleaning build artifacts...").start();
2189
+ try {
2190
+ const config = await loadConfig();
2191
+ const buildDir = getOutDir(config);
2192
+ const itemsToClean = [];
2193
+ if (fs3.existsSync(buildDir)) {
2194
+ fs3.rmSync(buildDir, { recursive: true, force: true });
2195
+ itemsToClean.push(buildDir);
2196
+ }
2197
+ let zipFilename = config.zipFilename || "Plugin.zip";
2198
+ if (!zipFilename.endsWith(".zip")) zipFilename += ".zip";
2199
+ if (fs3.existsSync(zipFilename)) {
2200
+ fs3.unlinkSync(zipFilename);
2201
+ itemsToClean.push(zipFilename);
2202
+ }
2203
+ if (zipFilename !== "Plugin.zip" && fs3.existsSync("Plugin.zip")) {
2204
+ fs3.unlinkSync("Plugin.zip");
2205
+ itemsToClean.push("Plugin.zip");
2206
+ }
2207
+ spinner.success({
2208
+ text: itemsToClean.length > 0 ? `Clean complete - Removed: ${itemsToClean.join(", ")}` : "Already clean"
2209
+ });
2210
+ console.log("");
2211
+ } catch (error) {
2212
+ spinner.error({ text: `Clean failed: ${error.message}` });
2213
+ console.log("");
2214
+ process.exit(1);
2215
+ }
2216
+ }
2217
+ var __filename2 = fileURLToPath(import.meta.url);
2218
+ var __dirname2 = path3.dirname(__filename2);
2219
+ var TEMPLATES_DIR = path3.resolve(__dirname2, "../templates");
2220
+ var PM_INSTALL_CMD = {
2221
+ npm: ["install"],
2222
+ yarn: [],
2223
+ pnpm: ["install"],
2224
+ bun: ["install"]
2225
+ };
2226
+ function isTermux() {
2227
+ return process.env.TERMUX_VERSION !== void 0 || fs3.existsSync("/data/data/com.termux") || process.env.PREFIX?.includes("com.termux") === true;
2228
+ }
2229
+ function getGitUserInfo() {
2230
+ try {
2231
+ const name = execSync("git config user.name", { encoding: "utf-8" }).trim();
2232
+ const email = execSync("git config user.email", { encoding: "utf-8" }).trim();
2233
+ return { name, email };
2234
+ } catch {
2235
+ return { name: "", email: "" };
2236
+ }
2237
+ }
2238
+ function detectPackageManagers() {
2239
+ const managers = ["npm", "yarn", "pnpm", "bun"];
2240
+ return managers.filter((mgr) => {
2241
+ try {
2242
+ execSync(`${mgr} --version`, { stdio: "ignore" });
2243
+ return true;
2244
+ } catch {
2245
+ return false;
2246
+ }
2247
+ });
2248
+ }
2249
+ function runCommand(cmd, args, cwd) {
2250
+ return new Promise((resolve5, reject) => {
2251
+ const child = spawn(cmd, args, {
2252
+ cwd,
2253
+ stdio: "ignore",
2254
+ shell: false
2255
+ });
2256
+ child.on("close", (code) => {
2257
+ if (code === 0) resolve5();
2258
+ else reject(new Error(`Command failed: ${cmd} ${args.join(" ")}`));
2259
+ });
2260
+ });
2261
+ }
2262
+ function copyDirectory(src, dest) {
2263
+ if (!fs3.existsSync(dest)) {
2264
+ fs3.mkdirSync(dest, { recursive: true });
2265
+ }
2266
+ const entries = fs3.readdirSync(src, { withFileTypes: true });
2267
+ for (const entry of entries) {
2268
+ const srcPath = path3.join(src, entry.name);
2269
+ const destPath = path3.join(dest, entry.name);
2270
+ if (entry.isDirectory()) {
2271
+ copyDirectory(srcPath, destPath);
2272
+ } else {
2273
+ fs3.copyFileSync(srcPath, destPath);
2274
+ }
2275
+ }
2276
+ }
2277
+ function getCurrentDate() {
2278
+ const now = /* @__PURE__ */ new Date();
2279
+ const day = String(now.getDate()).padStart(2, "0");
2280
+ const month = String(now.getMonth() + 1).padStart(2, "0");
2281
+ const year = now.getFullYear();
2282
+ return `${day}/${month}/${year}`;
2283
+ }
2284
+ function replacePlaceholders(dir, data) {
2285
+ const walk = (currentDir) => {
2286
+ for (const file of fs3.readdirSync(currentDir)) {
2287
+ const fullPath = path3.join(currentDir, file);
2288
+ if (fs3.statSync(fullPath).isDirectory()) {
2289
+ walk(fullPath);
2290
+ } else {
2291
+ if (/\.(ts|js|json|md|html|xml|css|txt)$/.test(file)) {
2292
+ let content = fs3.readFileSync(fullPath, "utf8");
2293
+ content = content.replace(/\{\{projectName\}\}/g, data.projectName).replace(/\{\{pluginId\}\}/g, data.pluginId).replace(/\{\{pluginName\}\}/g, data.pluginName).replace(/\{\{description\}\}/g, data.description).replace(/\{\{authorName\}\}/g, data.authorName).replace(/\{\{email\}\}/g, data.email).replace(/\{\{github\}\}/g, data.github || "").replace(/\{\{date\}\}/g, data.date);
2294
+ if (data.github) {
2295
+ content = content.replace(/\{\{#if github\}\}(.*?)\{\{\/if\}\}/gs, "$1");
2296
+ } else {
2297
+ content = content.replace(/\{\{#if github\}\}.*?\{\{\/if\}\}/gs, "");
2298
+ }
2299
+ fs3.writeFileSync(fullPath, content, "utf8");
2300
+ }
2301
+ }
2302
+ }
2303
+ };
2304
+ walk(dir);
2305
+ }
2306
+ async function promptAxpoDevInstallation() {
2307
+ try {
2308
+ console.log("");
2309
+ console.log(color.bold(color.cyan("\u{1F50C} Axpo Dev Plugin")));
2310
+ console.log(color.dim("To test your plugin during development, you need the"));
2311
+ console.log(color.dim('"Axpo Dev" plugin installed in Acode.'));
2312
+ console.log("");
2313
+ const install = await confirm({
2314
+ message: "Would you like to install Axpo Dev plugin in Acode now?",
2315
+ default: true
2316
+ });
2317
+ if (install) {
2318
+ console.log("");
2319
+ console.log(color.cyan("\u{1F4F1} Opening Acode to install Axpo Dev plugin..."));
2320
+ execSync('am start -a android.intent.action.VIEW -d "acode://plugin/install/axpo.dev"', {
2321
+ stdio: "ignore"
2322
+ });
2323
+ console.log(color.green("\u2713 Plugin installation started in Acode"));
2324
+ console.log(color.dim(" Please complete the installation in Acode app"));
2325
+ console.log("");
2326
+ } else {
2327
+ console.log("");
2328
+ console.log(color.dim(" You can install it later from:"));
2329
+ console.log(color.cyan(' \u2022 Acode \u2192 Plugins \u2192 Search "Axpo Dev"'));
2330
+ console.log("");
2331
+ }
2332
+ } catch (error) {
2333
+ }
2334
+ }
2335
+ async function handleDependencyInstallation(projectDir) {
2336
+ const managers = detectPackageManagers();
2337
+ if (!managers.includes("npm")) {
2338
+ console.error(color.red("\u274C npm is required but not found on your system."));
2339
+ process.exit(1);
2340
+ }
2341
+ let selectedManager = "npm";
2342
+ if (managers.length > 1) {
2343
+ selectedManager = await select({
2344
+ message: "Choose a package manager:",
2345
+ choices: managers.map((manager) => ({
2346
+ name: manager,
2347
+ value: manager
2348
+ }))
2349
+ });
2350
+ } else {
2351
+ console.log(color.gray(`Using ${selectedManager} to install dependencies...`));
2352
+ }
2353
+ const spinner = createSpinner(`Installing dependencies with ${selectedManager}...`).start();
2354
+ try {
2355
+ const isWindows = process.platform === "win32";
2356
+ const command = isWindows ? `${selectedManager}.cmd` : selectedManager;
2357
+ await runCommand(command, PM_INSTALL_CMD[selectedManager], projectDir);
2358
+ spinner.success({ text: `Dependencies installed via ${selectedManager}` });
2359
+ return selectedManager;
2360
+ } catch (err) {
2361
+ spinner.error({ text: "Installation failed. Please try manually." });
2362
+ if (err instanceof Error) {
2363
+ console.error(color.dim(err.message));
2364
+ }
2365
+ return selectedManager;
2366
+ }
2367
+ }
2368
+ async function init(options = {}) {
2369
+ try {
2370
+ console.log(color.bold(color.cyan("\n\u{1F680} Create Acode Plugin\n")));
2371
+ if (!fs3.existsSync(TEMPLATES_DIR)) {
2372
+ throw new Error(
2373
+ `Templates directory not found. Please ensure axpo is installed correctly.
2374
+ Expected: ${TEMPLATES_DIR}`
2375
+ );
2376
+ }
2377
+ const gitInfo = getGitUserInfo();
2378
+ let projectName = options.name || "my-acode-plugin";
2379
+ let targetPath;
2380
+ if (options.current) {
2381
+ projectName = path3.basename(process.cwd());
2382
+ targetPath = process.cwd();
2383
+ const files = fs3.readdirSync(targetPath).filter((file) => !file.startsWith("."));
2384
+ if (files.length > 0 && !options.yes) {
2385
+ const proceed = await confirm({
2386
+ message: "Current directory is not empty. Continue anyway?",
2387
+ default: false
2388
+ });
2389
+ if (!proceed) {
2390
+ console.log(color.dim("Cancelled."));
2391
+ return;
2392
+ }
2393
+ }
2394
+ } else {
2395
+ targetPath = path3.resolve(process.cwd(), projectName);
2396
+ }
2397
+ let pluginName = "My Plugin";
2398
+ let pluginId = "com.example.plugin";
2399
+ let description = "My awesome Acode plugin";
2400
+ let authorName = gitInfo.name || "Your Name";
2401
+ let email = gitInfo.email || "your@email.com";
2402
+ let github = "";
2403
+ let template = "typescript";
2404
+ if (!options.yes) {
2405
+ if (!options.current && !options.name) {
2406
+ projectName = await input({
2407
+ message: "Project name (folder name):",
2408
+ default: "my-acode-plugin",
2409
+ validate: (input3) => {
2410
+ if (!input3.trim()) return "Project name is required";
2411
+ const targetDir = path3.resolve(process.cwd(), input3);
2412
+ const exists = fs3.existsSync(targetDir);
2413
+ if (exists) {
2414
+ return `Directory "${input3}" already exists. Please choose a different name.`;
2415
+ }
2416
+ return true;
2417
+ }
2418
+ });
2419
+ targetPath = path3.resolve(process.cwd(), projectName);
2420
+ }
2421
+ pluginName = await input({
2422
+ message: "Plugin name (display name):",
2423
+ default: "My Plugin",
2424
+ validate: (input3) => {
2425
+ if (!input3.trim()) return "Plugin name is required";
2426
+ return true;
2427
+ }
2428
+ });
2429
+ pluginId = await input({
2430
+ message: "Plugin ID:",
2431
+ default: "com.example.plugin",
2432
+ validate: async (input3) => {
2433
+ if (!input3) return "Plugin ID cannot be empty.";
2434
+ const valid = /^[a-z][a-z0-9_]*(\.[a-z0-9_]+)+$/i.test(input3);
2435
+ if (!valid) return "Invalid format. Use format like com.example.plugin.";
2436
+ try {
2437
+ const res = await fetch(`https://acode.app/api/plugin/${input3}`);
2438
+ const contentType = res.headers.get("content-type");
2439
+ if (contentType && contentType.includes("text/html")) {
2440
+ return true;
2441
+ }
2442
+ const data = await res.json();
2443
+ if (!res.ok && data.error === "Not found") return true;
2444
+ if (data && data.id) {
2445
+ return `Plugin ID already used by ${color.cyan(color.bold(data.author))} in ${color.cyan(color.bold(data.name))} plugin`;
2446
+ }
2447
+ return true;
2448
+ } catch (error) {
2449
+ console.log(color.yellow("\n\u26A0 Could not verify plugin ID availability (network error)"));
2450
+ console.log(color.dim("Continuing without validation...\n"));
2451
+ return true;
2452
+ }
2453
+ }
2454
+ });
2455
+ description = await input({
2456
+ message: "Description:",
2457
+ default: "My awesome Acode plugin"
2458
+ });
2459
+ authorName = await input({
2460
+ message: "Author name:",
2461
+ default: gitInfo.name || "Your Name",
2462
+ validate: (input3) => input3.trim() ? true : "Author name is required"
2463
+ });
2464
+ email = await input({
2465
+ message: "Author email:",
2466
+ default: gitInfo.email || "your@email.com",
2467
+ validate: (input3) => {
2468
+ if (!input3.trim()) return "Email is required";
2469
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input3)) {
2470
+ return "Please enter a valid email address";
2471
+ }
2472
+ return true;
2473
+ }
2474
+ });
2475
+ github = await input({
2476
+ message: "GitHub username (optional):",
2477
+ default: "",
2478
+ validate: async (input3) => {
2479
+ if (!input3 || !input3.trim()) return true;
2480
+ if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/i.test(input3)) {
2481
+ return "Invalid GitHub username format";
2482
+ }
2483
+ try {
2484
+ const res = await fetch(`https://api.github.com/users/${input3}`);
2485
+ if (!res.ok) {
2486
+ if (res.status === 404) {
2487
+ return `GitHub user ${color.red(color.bold(input3))} not found`;
2488
+ }
2489
+ return `Error checking GitHub username: ${res.statusText}`;
2490
+ }
2491
+ const data = await res.json();
2492
+ if (data && data.login) {
2493
+ return true;
2494
+ }
2495
+ return "Could not verify GitHub username";
2496
+ } catch (error) {
2497
+ console.log(color.yellow("\n\u26A0 Could not verify GitHub username (network error)"));
2498
+ console.log(color.dim("Continuing without validation...\n"));
2499
+ return true;
2500
+ }
2501
+ }
2502
+ });
2503
+ template = await select({
2504
+ message: "Choose a template:",
2505
+ choices: [
2506
+ { name: "TypeScript (Recommended)", value: "typescript" },
2507
+ { name: "JavaScript", value: "javascript" }
2508
+ ],
2509
+ default: "typescript"
2510
+ });
2511
+ }
2512
+ const templatePath = path3.join(TEMPLATES_DIR, template);
2513
+ if (!fs3.existsSync(templatePath)) {
2514
+ throw new Error(`Template not found: ${template}`);
2515
+ }
2516
+ if (fs3.existsSync(targetPath) && !options.current && (options.name || options.yes)) {
2517
+ throw new Error(`Directory "${projectName}" already exists. Please choose a different name or remove the existing directory.`);
2518
+ }
2519
+ const spinner = createSpinner("Creating project files...").start();
2520
+ try {
2521
+ if (!fs3.existsSync(targetPath)) {
2522
+ fs3.mkdirSync(targetPath, { recursive: true });
2523
+ }
2524
+ copyDirectory(templatePath, targetPath);
2525
+ replacePlaceholders(targetPath, {
2526
+ projectName,
2527
+ pluginName,
2528
+ pluginId,
2529
+ description,
2530
+ authorName,
2531
+ email,
2532
+ github,
2533
+ date: getCurrentDate()
2534
+ });
2535
+ spinner.success({ text: "Project files created" });
2536
+ } catch (err) {
2537
+ spinner.error({ text: "Failed to create project files" });
2538
+ throw err;
2539
+ }
2540
+ let shouldInstall = options.yes;
2541
+ let packageManager = "npm";
2542
+ if (!options.yes) {
2543
+ shouldInstall = await confirm({
2544
+ message: "Install dependencies now?",
2545
+ default: true
2546
+ });
2547
+ }
2548
+ if (shouldInstall) {
2549
+ packageManager = await handleDependencyInstallation(targetPath);
2550
+ }
2551
+ console.log("");
2552
+ console.log(color.green("\u2713 Plugin project created successfully!"));
2553
+ console.log("");
2554
+ console.log(color.bold("Next steps:"));
2555
+ if (!options.current) {
2556
+ console.log(color.cyan(` cd ${projectName}`));
2557
+ }
2558
+ if (!shouldInstall) {
2559
+ console.log(color.cyan(` ${packageManager} install`) + color.dim(" # Install dependencies"));
2560
+ }
2561
+ const runCmd = packageManager === "npm" ? "npm run" : packageManager;
2562
+ console.log(color.cyan(` ${runCmd} dev`) + color.dim(" # Start development server"));
2563
+ console.log(color.cyan(` ${runCmd} build`) + color.dim(" # Build for production"));
2564
+ console.log("");
2565
+ console.log(color.dim("Then scan the QR code with Axpo Dev plugin in Acode"));
2566
+ console.log("");
2567
+ if (isTermux()) {
2568
+ await promptAxpoDevInstallation();
2569
+ }
2570
+ } catch (error) {
2571
+ if (error.name === "ExitPromptError") {
2572
+ console.log("");
2573
+ console.log(color.bold(color.red("\u2716 Operation cancelled.")));
2574
+ console.log("");
2575
+ process.exit(0);
2576
+ }
2577
+ console.error(color.red(`
2578
+ \u2717 ${error.message}
2579
+ `));
2580
+ process.exit(1);
2581
+ }
2582
+ }
2583
+
2584
+ export { build2 as build, checkAxpoVersion, clean, createZip, dev, displayVersionWarning, getOutDir, init, loadConfig, loadPluginJson, logout, publish, validateConfig };
2585
+ //# sourceMappingURL=index.js.map
2586
+ //# sourceMappingURL=index.js.map