@whatalo/cli-kit 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.cjs ADDED
@@ -0,0 +1,1055 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ CONFIG_FILE_NAME: () => CONFIG_FILE_NAME,
34
+ POLL_STATUS: () => POLL_STATUS,
35
+ WhataloApiClient: () => WhataloApiClient,
36
+ WhataloAuthError: () => WhataloAuthError,
37
+ WhataloConfigError: () => WhataloConfigError,
38
+ WhataloNetworkError: () => WhataloNetworkError,
39
+ WhataloValidationError: () => WhataloValidationError,
40
+ banner: () => banner,
41
+ checkSdkCompatibility: () => checkSdkCompatibility,
42
+ clearSession: () => clearSession,
43
+ code: () => code,
44
+ createSpinner: () => createSpinner,
45
+ createTunnel: () => createTunnel,
46
+ ensureCloudflared: () => ensureCloudflared,
47
+ error: () => error,
48
+ failMissingNonTTYFlags: () => failMissingNonTTYFlags,
49
+ getSession: () => getSession,
50
+ getSessionDir: () => getSessionDir,
51
+ getUpgradeCommand: () => getUpgradeCommand,
52
+ handleCliError: () => handleCliError,
53
+ info: () => info,
54
+ isNewerVersion: () => isNewerVersion,
55
+ isSessionValid: () => isSessionValid,
56
+ link: () => link,
57
+ parseEnvFile: () => parseEnvFile,
58
+ pollForToken: () => pollForToken,
59
+ readConfig: () => readConfig,
60
+ refreshAccessToken: () => refreshAccessToken,
61
+ renderInfoPanel: () => renderInfoPanel,
62
+ renderTable: () => renderTable,
63
+ renderTasks: () => renderTasks,
64
+ requestDeviceCode: () => requestDeviceCode,
65
+ saveSession: () => saveSession,
66
+ scheduleVersionCheck: () => scheduleVersionCheck,
67
+ success: () => success,
68
+ table: () => table,
69
+ updateEnvFile: () => updateEnvFile,
70
+ warn: () => warn,
71
+ withErrorHandler: () => withErrorHandler,
72
+ writeConfig: () => writeConfig
73
+ });
74
+ module.exports = __toCommonJS(src_exports);
75
+
76
+ // src/session/store.ts
77
+ var import_promises = __toESM(require("fs/promises"), 1);
78
+ var import_node_os = __toESM(require("os"), 1);
79
+ var import_node_path = __toESM(require("path"), 1);
80
+ function getSessionDir() {
81
+ return import_node_path.default.join(import_node_os.default.homedir(), ".whatalo");
82
+ }
83
+ function getSessionPath() {
84
+ return import_node_path.default.join(getSessionDir(), "session.json");
85
+ }
86
+ async function saveSession(session) {
87
+ const dir = getSessionDir();
88
+ const filePath = getSessionPath();
89
+ await import_promises.default.mkdir(dir, { recursive: true, mode: 448 });
90
+ await import_promises.default.writeFile(filePath, JSON.stringify(session, null, 2), {
91
+ encoding: "utf-8",
92
+ mode: 384
93
+ });
94
+ await import_promises.default.chmod(filePath, 384);
95
+ }
96
+ async function getSession() {
97
+ try {
98
+ const raw = await import_promises.default.readFile(getSessionPath(), { encoding: "utf-8" });
99
+ return JSON.parse(raw);
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ async function clearSession() {
105
+ try {
106
+ await import_promises.default.unlink(getSessionPath());
107
+ } catch (err) {
108
+ const error2 = err;
109
+ if (error2.code !== "ENOENT") throw err;
110
+ }
111
+ }
112
+ function isSessionValid(session) {
113
+ const expiresAt = new Date(session.expiresAt).getTime();
114
+ const now = Date.now();
115
+ const SKEW_BUFFER_MS = 6e4;
116
+ return expiresAt - SKEW_BUFFER_MS > now;
117
+ }
118
+
119
+ // src/session/types.ts
120
+ var POLL_STATUS = {
121
+ PENDING: "pending",
122
+ AUTHORIZED: "authorized",
123
+ EXPIRED: "expired",
124
+ DENIED: "denied"
125
+ };
126
+
127
+ // src/session/device-flow.ts
128
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
129
+ async function requestDeviceCode(portalUrl) {
130
+ const res = await fetch(`${portalUrl}/api/auth/device-code`, {
131
+ method: "POST",
132
+ headers: { "Content-Type": "application/json" },
133
+ body: JSON.stringify({ clientId: "whatalo-cli" }),
134
+ signal: AbortSignal.timeout(3e4)
135
+ });
136
+ if (!res.ok) {
137
+ const body = await res.text().catch(() => "Unknown error");
138
+ throw new Error(`Failed to request device code (${res.status}): ${body}`);
139
+ }
140
+ return await res.json();
141
+ }
142
+ async function* pollForToken(portalUrl, deviceCode, initialInterval) {
143
+ let interval = Math.max(initialInterval, 5);
144
+ while (true) {
145
+ await sleep(interval * 1e3);
146
+ const res = await fetch(`${portalUrl}/api/auth/device-token`, {
147
+ method: "POST",
148
+ headers: { "Content-Type": "application/json" },
149
+ body: JSON.stringify({
150
+ deviceCode,
151
+ grantType: "urn:ietf:params:oauth:grant-type:device_code"
152
+ }),
153
+ signal: AbortSignal.timeout(3e4)
154
+ });
155
+ if (res.ok) {
156
+ const data = await res.json();
157
+ yield { status: POLL_STATUS.AUTHORIZED, token: data };
158
+ return;
159
+ }
160
+ let errorCode = "unknown";
161
+ try {
162
+ const errorBody = await res.json();
163
+ errorCode = errorBody.error ?? "unknown";
164
+ } catch {
165
+ yield { status: POLL_STATUS.EXPIRED };
166
+ return;
167
+ }
168
+ switch (errorCode) {
169
+ case "authorization_pending":
170
+ yield { status: POLL_STATUS.PENDING };
171
+ break;
172
+ case "slow_down":
173
+ interval += 5;
174
+ yield { status: POLL_STATUS.PENDING };
175
+ break;
176
+ case "expired_token":
177
+ yield { status: POLL_STATUS.EXPIRED };
178
+ return;
179
+ case "access_denied":
180
+ yield { status: POLL_STATUS.DENIED };
181
+ return;
182
+ default:
183
+ yield { status: POLL_STATUS.EXPIRED };
184
+ return;
185
+ }
186
+ }
187
+ }
188
+ async function refreshAccessToken(portalUrl, refreshToken) {
189
+ const res = await fetch(`${portalUrl}/api/auth/refresh`, {
190
+ method: "POST",
191
+ headers: { "Content-Type": "application/json" },
192
+ body: JSON.stringify({
193
+ refreshToken,
194
+ grantType: "refresh_token"
195
+ }),
196
+ signal: AbortSignal.timeout(3e4)
197
+ });
198
+ if (!res.ok) {
199
+ const body = await res.text().catch(() => "Unknown error");
200
+ throw new Error(`Failed to refresh token (${res.status}): ${body}`);
201
+ }
202
+ return await res.json();
203
+ }
204
+
205
+ // src/output/format.ts
206
+ var import_chalk = __toESM(require("chalk"), 1);
207
+ function banner(title, version) {
208
+ console.log();
209
+ console.log(` ${import_chalk.default.bold.cyan(title)} ${import_chalk.default.dim(`v${version}`)}`);
210
+ console.log();
211
+ }
212
+ function success(message) {
213
+ console.log(` ${import_chalk.default.green("\u2713")} ${message}`);
214
+ }
215
+ function error(message) {
216
+ console.log(` ${import_chalk.default.red("\u2717")} ${message}`);
217
+ }
218
+ function warn(message) {
219
+ console.log(` ${import_chalk.default.yellow("\u26A0")} ${message}`);
220
+ }
221
+ function info(message) {
222
+ console.log(` ${import_chalk.default.blue("\u2139")} ${message}`);
223
+ }
224
+ function link(url) {
225
+ return import_chalk.default.underline.cyan(url);
226
+ }
227
+ function code(text) {
228
+ return import_chalk.default.dim("`") + import_chalk.default.bold(text) + import_chalk.default.dim("`");
229
+ }
230
+ function table(headers, rows) {
231
+ if (headers.length === 0) return;
232
+ const widths = headers.map(
233
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
234
+ );
235
+ const headerLine = headers.map((h, i) => h.padEnd(widths[i] ?? h.length)).join(" ");
236
+ console.log(` ${import_chalk.default.bold(headerLine)}`);
237
+ const separator = widths.map((w) => "\u2500".repeat(w)).join(" ");
238
+ console.log(` ${separator}`);
239
+ for (const row of rows) {
240
+ const line = row.map((cell, i) => cell.padEnd(widths[i] ?? cell.length)).join(" ");
241
+ console.log(` ${line}`);
242
+ }
243
+ }
244
+ var STATUS_ICONS = {
245
+ pending: import_chalk.default.dim("\u25CB"),
246
+ running: import_chalk.default.cyan("\u25C9"),
247
+ success: import_chalk.default.green("\u2713"),
248
+ error: import_chalk.default.red("\u2717"),
249
+ warning: import_chalk.default.yellow("\u26A0"),
250
+ skipped: import_chalk.default.dim("\u2013")
251
+ };
252
+ function renderTasks(tasks) {
253
+ for (const task of tasks) {
254
+ const icon = STATUS_ICONS[task.status];
255
+ const label = task.status === "error" ? import_chalk.default.red(task.label) : task.status === "warning" ? import_chalk.default.yellow(task.label) : task.status === "skipped" ? import_chalk.default.dim(task.label) : task.label;
256
+ console.log(` ${icon} ${label}`);
257
+ if (task.detail) {
258
+ console.log(` ${import_chalk.default.dim(task.detail)}`);
259
+ }
260
+ }
261
+ }
262
+ function renderInfoPanel(title, sections) {
263
+ console.log();
264
+ console.log(` ${import_chalk.default.bold(title)}`);
265
+ console.log(` ${"\u2550".repeat(title.length)}`);
266
+ for (const section of sections) {
267
+ console.log();
268
+ console.log(` ${import_chalk.default.bold.dim(section.heading)}`);
269
+ const maxKeyLen = Math.max(...section.rows.map((r) => r.key.length));
270
+ for (const row of section.rows) {
271
+ console.log(
272
+ ` ${import_chalk.default.dim(row.key.padEnd(maxKeyLen))} ${row.value}`
273
+ );
274
+ }
275
+ }
276
+ console.log();
277
+ }
278
+ function renderTable(options) {
279
+ table(options.headers, options.rows);
280
+ }
281
+
282
+ // src/output/spinner.ts
283
+ var import_chalk2 = __toESM(require("chalk"), 1);
284
+ var SPINNER_FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
285
+ var FRAME_INTERVAL_MS = 100;
286
+ function createSpinner(message) {
287
+ let frameIndex = 0;
288
+ let stopped = false;
289
+ const timer = setInterval(() => {
290
+ if (stopped) return;
291
+ const frame = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length];
292
+ process.stdout.write(`\x1B[2K\r ${import_chalk2.default.magenta(frame)} ${message}`);
293
+ frameIndex++;
294
+ }, FRAME_INTERVAL_MS);
295
+ const firstFrame = SPINNER_FRAMES[0];
296
+ process.stdout.write(` ${import_chalk2.default.magenta(firstFrame)} ${message}`);
297
+ return {
298
+ stop(finalMessage) {
299
+ if (stopped) return;
300
+ stopped = true;
301
+ clearInterval(timer);
302
+ process.stdout.write(`\x1B[2K\r`);
303
+ if (finalMessage) {
304
+ console.log(` ${import_chalk2.default.green("\u2713")} ${finalMessage}`);
305
+ }
306
+ }
307
+ };
308
+ }
309
+
310
+ // src/output/errors.ts
311
+ var WhataloAuthError = class extends Error {
312
+ constructor(message = "Authentication required") {
313
+ super(message);
314
+ this.name = "WhataloAuthError";
315
+ }
316
+ };
317
+ var WhataloConfigError = class extends Error {
318
+ suggestion;
319
+ constructor(message, suggestion) {
320
+ super(message);
321
+ this.name = "WhataloConfigError";
322
+ this.suggestion = suggestion;
323
+ }
324
+ };
325
+ var WhataloNetworkError = class extends Error {
326
+ statusCode;
327
+ constructor(message, statusCode) {
328
+ super(message);
329
+ this.name = "WhataloNetworkError";
330
+ this.statusCode = statusCode;
331
+ }
332
+ };
333
+ var WhataloValidationError = class extends Error {
334
+ field;
335
+ constructor(message, field) {
336
+ super(message);
337
+ this.name = "WhataloValidationError";
338
+ this.field = field;
339
+ }
340
+ };
341
+
342
+ // src/output/error-handler.ts
343
+ function withErrorHandler(fn) {
344
+ return async (...args) => {
345
+ try {
346
+ await fn(...args);
347
+ } catch (err) {
348
+ handleCliError(err);
349
+ }
350
+ };
351
+ }
352
+ function handleCliError(error2) {
353
+ if (error2 instanceof WhataloAuthError) {
354
+ error(error2.message);
355
+ info("Run `whatalo login` to re-authenticate.");
356
+ process.exit(1);
357
+ }
358
+ if (error2 instanceof WhataloConfigError) {
359
+ error(error2.message);
360
+ if (error2.suggestion) {
361
+ info(`Fix: ${error2.suggestion}`);
362
+ }
363
+ process.exit(2);
364
+ }
365
+ if (error2 instanceof WhataloNetworkError) {
366
+ error("Could not connect to Whatalo API.");
367
+ if (error2.statusCode === 429) {
368
+ warn("Rate limit reached. Wait a moment and try again.");
369
+ } else {
370
+ info("Check your internet connection and try again.");
371
+ }
372
+ if (error2.message && error2.message !== "Could not connect to Whatalo API.") {
373
+ info(`Details: ${error2.message}`);
374
+ }
375
+ process.exit(1);
376
+ }
377
+ if (error2 instanceof WhataloValidationError) {
378
+ error(error2.message);
379
+ if (error2.field) {
380
+ info(`Field: ${error2.field}`);
381
+ }
382
+ process.exit(1);
383
+ }
384
+ error("An unexpected error occurred.");
385
+ if (error2 instanceof Error) {
386
+ info(`Details: ${error2.message}`);
387
+ }
388
+ info(
389
+ "If this persists, run `whatalo info --json` and report the issue."
390
+ );
391
+ process.exit(1);
392
+ }
393
+
394
+ // src/output/non-tty.ts
395
+ function failMissingNonTTYFlags(requiredFlags, options) {
396
+ if (process.stdout.isTTY) return;
397
+ const missing = requiredFlags.filter((flag) => !options[flag]);
398
+ if (missing.length > 0) {
399
+ throw new WhataloValidationError(
400
+ `${missing.map((f) => `--${f}`).join(", ")} required in non-interactive environments (CI/CD).`,
401
+ missing[0]
402
+ );
403
+ }
404
+ }
405
+
406
+ // src/http/client.ts
407
+ var import_node_fs = require("fs");
408
+ var import_node_path2 = require("path");
409
+ var import_node_url = require("url");
410
+ var import_meta = {};
411
+ var DEFAULT_TIMEOUT_MS = 3e4;
412
+ var MAX_RETRIES = 3;
413
+ var BASE_BACKOFF_MS = 1e3;
414
+ function getCliVersion() {
415
+ try {
416
+ const __dirname = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
417
+ const pkg = JSON.parse(
418
+ (0, import_node_fs.readFileSync)((0, import_node_path2.join)(__dirname, "..", "package.json"), "utf-8")
419
+ );
420
+ return pkg.version ?? "unknown";
421
+ } catch {
422
+ return "unknown";
423
+ }
424
+ }
425
+ function buildUserAgent() {
426
+ const cliVersion = getCliVersion();
427
+ const nodeVersion = process.version;
428
+ const platform = process.platform;
429
+ const arch = process.arch;
430
+ return `whatalo-cli/${cliVersion} node/${nodeVersion} ${platform}/${arch}`;
431
+ }
432
+ var WhataloApiClient = class {
433
+ options;
434
+ timeout;
435
+ userAgent;
436
+ constructor(options) {
437
+ this.options = options;
438
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
439
+ this.userAgent = buildUserAgent();
440
+ }
441
+ /** Sends an authenticated GET request and returns the parsed JSON body */
442
+ async get(path6) {
443
+ return this.request("GET", path6);
444
+ }
445
+ /** Sends an authenticated POST request with an optional JSON body */
446
+ async post(path6, body) {
447
+ return this.request("POST", path6, body);
448
+ }
449
+ /** Sends an authenticated PATCH request with an optional JSON body */
450
+ async patch(path6, body) {
451
+ return this.request("PATCH", path6, body);
452
+ }
453
+ /** Sends an authenticated DELETE request */
454
+ async delete(path6) {
455
+ return this.request("DELETE", path6);
456
+ }
457
+ /**
458
+ * Core request method with automatic token refresh on 401,
459
+ * retry with exponential backoff on 5xx, and rate limit handling.
460
+ */
461
+ async request(method, path6, body) {
462
+ const session = await this.options.getSession();
463
+ if (!session) {
464
+ throw new WhataloAuthError("Not logged in. Run `whatalo login` first.");
465
+ }
466
+ const doFetch = async (token) => {
467
+ const url = `${this.options.portalUrl}${path6}`;
468
+ return fetch(url, {
469
+ method,
470
+ headers: {
471
+ "Content-Type": "application/json",
472
+ Authorization: `Bearer ${token}`,
473
+ "User-Agent": this.userAgent
474
+ },
475
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
476
+ signal: AbortSignal.timeout(this.timeout)
477
+ });
478
+ };
479
+ let res;
480
+ try {
481
+ res = await this.executeWithRetry(doFetch, session.accessToken);
482
+ } catch (err) {
483
+ if (err instanceof WhataloAuthError || err instanceof WhataloNetworkError) {
484
+ throw err;
485
+ }
486
+ const message = err instanceof Error ? err.message : "Unknown error";
487
+ throw new WhataloNetworkError(message);
488
+ }
489
+ if (res.status === 401) {
490
+ try {
491
+ const refreshed = await this.options.refreshSession();
492
+ res = await this.executeWithRetry(doFetch, refreshed.accessToken);
493
+ } catch (refreshErr) {
494
+ if (refreshErr instanceof WhataloNetworkError) throw refreshErr;
495
+ throw new WhataloAuthError(
496
+ "Session expired. Run `whatalo login` to re-authenticate."
497
+ );
498
+ }
499
+ if (res.status === 401) {
500
+ throw new WhataloAuthError(
501
+ "Session expired. Run `whatalo login` to re-authenticate."
502
+ );
503
+ }
504
+ }
505
+ if (!res.ok) {
506
+ const errorBody = await res.text().catch(() => "Unknown error");
507
+ throw new WhataloNetworkError(
508
+ `API error ${res.status}: ${errorBody}`,
509
+ res.status
510
+ );
511
+ }
512
+ return await res.json();
513
+ }
514
+ /**
515
+ * Executes a fetch with retry on 5xx and rate-limit (429) handling.
516
+ * Uses exponential backoff: 1s, 2s, 4s.
517
+ */
518
+ async executeWithRetry(doFetch, token) {
519
+ let lastResponse;
520
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
521
+ try {
522
+ lastResponse = await doFetch(token);
523
+ } catch (err) {
524
+ if (attempt === MAX_RETRIES - 1) {
525
+ const message = err instanceof Error ? err.message : "Network error";
526
+ throw new WhataloNetworkError(message);
527
+ }
528
+ await this.sleep(BASE_BACKOFF_MS * Math.pow(2, attempt));
529
+ continue;
530
+ }
531
+ if (lastResponse.status === 429) {
532
+ const retryAfter = lastResponse.headers.get("Retry-After");
533
+ const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : BASE_BACKOFF_MS * Math.pow(2, attempt);
534
+ if (attempt === MAX_RETRIES - 1) {
535
+ throw new WhataloNetworkError("Rate limit exceeded", 429);
536
+ }
537
+ await this.sleep(Math.min(waitMs, 3e4));
538
+ continue;
539
+ }
540
+ if (lastResponse.status >= 500 && attempt < MAX_RETRIES - 1) {
541
+ await this.sleep(BASE_BACKOFF_MS * Math.pow(2, attempt));
542
+ continue;
543
+ }
544
+ return lastResponse;
545
+ }
546
+ return lastResponse;
547
+ }
548
+ sleep(ms) {
549
+ return new Promise((resolve) => setTimeout(resolve, ms));
550
+ }
551
+ };
552
+
553
+ // src/config/toml.ts
554
+ var import_promises2 = require("fs/promises");
555
+ var import_node_path3 = __toESM(require("path"), 1);
556
+ var import_toml = __toESM(require("@iarna/toml"), 1);
557
+
558
+ // src/config/types.ts
559
+ var REQUIRED_FIELDS = [
560
+ "plugin.name",
561
+ "plugin.plugin_id",
562
+ "plugin.slug",
563
+ "build.dev_command",
564
+ "build.build_command",
565
+ "build.output_dir",
566
+ "dev.port"
567
+ ];
568
+ var CONFIG_FILE_NAME = "whatalo.app.toml";
569
+
570
+ // src/config/toml.ts
571
+ async function readConfig(dir) {
572
+ const filePath = import_node_path3.default.join(dir, CONFIG_FILE_NAME);
573
+ let raw;
574
+ try {
575
+ raw = await (0, import_promises2.readFile)(filePath, "utf-8");
576
+ } catch {
577
+ throw new Error(
578
+ `Could not find ${CONFIG_FILE_NAME} in "${dir}". Run \`whatalo init\` to set up your project.`
579
+ );
580
+ }
581
+ let parsed;
582
+ try {
583
+ parsed = import_toml.default.parse(raw);
584
+ } catch (err) {
585
+ const message = err instanceof Error ? err.message : String(err);
586
+ throw new Error(
587
+ `Failed to parse ${CONFIG_FILE_NAME}: ${message}. Check for syntax errors.`
588
+ );
589
+ }
590
+ validateRequiredFields(parsed);
591
+ return parsed;
592
+ }
593
+ async function writeConfig(dir, config) {
594
+ const filePath = import_node_path3.default.join(dir, CONFIG_FILE_NAME);
595
+ const content = import_toml.default.stringify(config);
596
+ await (0, import_promises2.writeFile)(filePath, content, "utf-8");
597
+ }
598
+ function validateRequiredFields(obj) {
599
+ for (const fieldPath of REQUIRED_FIELDS) {
600
+ const parts = fieldPath.split(".");
601
+ let current = obj;
602
+ for (const part of parts) {
603
+ if (current === null || current === void 0 || typeof current !== "object") {
604
+ throw new Error(
605
+ `Missing \`${fieldPath}\` in ${CONFIG_FILE_NAME}. Run \`whatalo init\` to set up your project.`
606
+ );
607
+ }
608
+ current = current[part];
609
+ }
610
+ if (current === null || current === void 0 || current === "") {
611
+ throw new Error(
612
+ `Missing \`${fieldPath}\` in ${CONFIG_FILE_NAME}. Run \`whatalo init\` to set up your project.`
613
+ );
614
+ }
615
+ }
616
+ }
617
+
618
+ // src/config/env-file.ts
619
+ var import_promises3 = require("fs/promises");
620
+ function parseEnvFile(content) {
621
+ const lines = content.split("\n");
622
+ return lines.map((raw) => {
623
+ const trimmed = raw.trim();
624
+ if (trimmed === "" || trimmed.startsWith("#")) {
625
+ return { raw };
626
+ }
627
+ const eqIndex = trimmed.indexOf("=");
628
+ if (eqIndex === -1) {
629
+ return { raw };
630
+ }
631
+ const key = trimmed.slice(0, eqIndex).trim();
632
+ let value = trimmed.slice(eqIndex + 1).trim();
633
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
634
+ value = value.slice(1, -1);
635
+ }
636
+ return { raw, key, value };
637
+ });
638
+ }
639
+ async function updateEnvFile(filePath, vars) {
640
+ let existingContent = "";
641
+ try {
642
+ existingContent = await (0, import_promises3.readFile)(filePath, "utf-8");
643
+ } catch {
644
+ }
645
+ const entries = existingContent ? parseEnvFile(existingContent) : [];
646
+ const updatedKeys = /* @__PURE__ */ new Set();
647
+ const updatedLines = entries.map((entry) => {
648
+ if (entry.key && entry.key in vars) {
649
+ updatedKeys.add(entry.key);
650
+ return `${entry.key}=${vars[entry.key]}`;
651
+ }
652
+ return entry.raw;
653
+ });
654
+ const newVars = Object.entries(vars).filter(
655
+ ([key]) => !updatedKeys.has(key)
656
+ );
657
+ if (newVars.length > 0) {
658
+ if (updatedLines.length > 0 && updatedLines[updatedLines.length - 1] !== "") {
659
+ updatedLines.push("");
660
+ }
661
+ updatedLines.push("# Whatalo Plugin Variables");
662
+ for (const [key, value] of newVars) {
663
+ updatedLines.push(`${key}=${value}`);
664
+ }
665
+ }
666
+ const output = updatedLines.join("\n");
667
+ await (0, import_promises3.writeFile)(filePath, output.endsWith("\n") ? output : output + "\n", "utf-8");
668
+ }
669
+
670
+ // src/tunnel/cloudflared.ts
671
+ var import_node_child_process = require("child_process");
672
+ var import_node_fs2 = require("fs");
673
+ var import_promises4 = require("fs/promises");
674
+ var import_node_path4 = __toESM(require("path"), 1);
675
+ var import_node_os2 = __toESM(require("os"), 1);
676
+ var import_node_https = __toESM(require("https"), 1);
677
+ var TUNNEL_START_TIMEOUT_MS = 15e3;
678
+ var TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
679
+ function getBinDir() {
680
+ return import_node_path4.default.join(import_node_os2.default.homedir(), ".whatalo", "bin");
681
+ }
682
+ function getManagedBinaryPath() {
683
+ return import_node_path4.default.join(getBinDir(), "cloudflared");
684
+ }
685
+ function findOnSystemPath() {
686
+ const cmd = process.platform === "win32" ? "where" : "which";
687
+ const result = (0, import_node_child_process.spawnSync)(cmd, ["cloudflared"], { encoding: "utf-8" });
688
+ if (result.status === 0 && result.stdout.trim()) {
689
+ return result.stdout.trim().split("\n")[0] ?? null;
690
+ }
691
+ return null;
692
+ }
693
+ async function ensureCloudflared() {
694
+ const systemPath = findOnSystemPath();
695
+ if (systemPath) return systemPath;
696
+ const managedPath = getManagedBinaryPath();
697
+ if ((0, import_node_fs2.existsSync)(managedPath)) return managedPath;
698
+ return downloadCloudflared(managedPath);
699
+ }
700
+ function resolvePlatformInfo() {
701
+ const platform = process.platform;
702
+ const arch = process.arch === "x64" ? "amd64" : process.arch;
703
+ if (platform === "darwin") {
704
+ return { os: "darwin", arch, ext: "tgz" };
705
+ }
706
+ if (platform === "linux") {
707
+ return { os: "linux", arch, ext: "binary" };
708
+ }
709
+ throw new Error(
710
+ `Unsupported platform: ${platform}. Install cloudflared manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/`
711
+ );
712
+ }
713
+ function buildDownloadUrl(osName, arch, ext) {
714
+ const base = "https://github.com/cloudflare/cloudflared/releases/latest/download";
715
+ if (ext === "tgz") {
716
+ return `${base}/cloudflared-${osName}-${arch}.tgz`;
717
+ }
718
+ return `${base}/cloudflared-${osName}-${arch}`;
719
+ }
720
+ async function downloadFile(url, dest) {
721
+ return new Promise((resolve, reject) => {
722
+ const follow = (targetUrl) => {
723
+ import_node_https.default.get(targetUrl, (res) => {
724
+ if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
725
+ follow(res.headers.location);
726
+ return;
727
+ }
728
+ if (res.statusCode !== 200) {
729
+ reject(
730
+ new Error(
731
+ `Download failed with HTTP ${res.statusCode}: ${targetUrl}`
732
+ )
733
+ );
734
+ return;
735
+ }
736
+ const file = (0, import_node_fs2.createWriteStream)(dest);
737
+ res.pipe(file);
738
+ file.on("finish", () => file.close(() => resolve()));
739
+ file.on("error", (err) => {
740
+ file.close();
741
+ reject(err);
742
+ });
743
+ }).on("error", reject);
744
+ };
745
+ follow(url);
746
+ });
747
+ }
748
+ async function downloadCloudflared(targetPath) {
749
+ const { os: osName, arch, ext } = resolvePlatformInfo();
750
+ const downloadUrl = buildDownloadUrl(osName, arch, ext);
751
+ (0, import_node_fs2.mkdirSync)(import_node_path4.default.dirname(targetPath), { recursive: true });
752
+ info(`Downloading cloudflared for ${osName}/${arch}\u2026`);
753
+ info(`Source: ${downloadUrl}`);
754
+ const tmpPath = `${targetPath}.tmp`;
755
+ try {
756
+ await downloadFile(downloadUrl, tmpPath);
757
+ if (ext === "tgz") {
758
+ const tarResult = (0, import_node_child_process.spawnSync)(
759
+ "tar",
760
+ ["xzf", tmpPath, "-C", import_node_path4.default.dirname(targetPath), "cloudflared"],
761
+ { encoding: "utf-8" }
762
+ );
763
+ if (tarResult.status !== 0) {
764
+ throw new Error(
765
+ `tar extraction failed: ${tarResult.stderr || (tarResult.error?.message ?? "unknown error")}`
766
+ );
767
+ }
768
+ await (0, import_promises4.unlink)(tmpPath).catch(() => void 0);
769
+ } else {
770
+ await (0, import_promises4.rename)(tmpPath, targetPath);
771
+ }
772
+ if (!(0, import_node_fs2.existsSync)(targetPath)) {
773
+ throw new Error(
774
+ "Binary extraction completed but cloudflared was not found at the expected path."
775
+ );
776
+ }
777
+ (0, import_node_fs2.chmodSync)(targetPath, 493);
778
+ } catch (err) {
779
+ await (0, import_promises4.unlink)(tmpPath).catch(() => void 0);
780
+ throw new Error(
781
+ `Could not download cloudflared. Install manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/
782
+ Original error: ${err.message}`
783
+ );
784
+ }
785
+ info(`cloudflared saved to ${targetPath}`);
786
+ return targetPath;
787
+ }
788
+ async function createTunnel(options) {
789
+ const { localPort, protocol = "http" } = options;
790
+ const binaryPath = await ensureCloudflared();
791
+ return new Promise((resolve, reject) => {
792
+ const child = (0, import_node_child_process.spawn)(
793
+ binaryPath,
794
+ [
795
+ "tunnel",
796
+ "--url",
797
+ `${protocol}://localhost:${localPort}`,
798
+ "--no-autoupdate"
799
+ ],
800
+ {
801
+ // Inherit environment so cloudflared can read system certificates
802
+ env: { ...process.env },
803
+ // stdout flows to the terminal; stderr is captured for URL extraction
804
+ stdio: ["ignore", "inherit", "pipe"]
805
+ }
806
+ );
807
+ let urlFound = false;
808
+ let lineBuffer = "";
809
+ const timeout = setTimeout(() => {
810
+ if (!urlFound) {
811
+ child.kill("SIGTERM");
812
+ reject(
813
+ new Error(
814
+ `Tunnel failed to start. Check if port ${localPort} is accessible.`
815
+ )
816
+ );
817
+ }
818
+ }, TUNNEL_START_TIMEOUT_MS);
819
+ child.stderr?.on("data", (chunk) => {
820
+ lineBuffer += chunk.toString("utf-8");
821
+ const lines = lineBuffer.split("\n");
822
+ lineBuffer = lines.pop() ?? "";
823
+ for (const line of lines) {
824
+ const match = TUNNEL_URL_REGEX.exec(line);
825
+ if (match && !urlFound) {
826
+ urlFound = true;
827
+ clearTimeout(timeout);
828
+ const tunnelUrl = match[0];
829
+ const kill = () => new Promise((res) => {
830
+ if (child.exitCode !== null) {
831
+ res();
832
+ return;
833
+ }
834
+ child.once("exit", () => res());
835
+ child.kill("SIGTERM");
836
+ });
837
+ resolve({ url: tunnelUrl, process: child, kill });
838
+ }
839
+ }
840
+ });
841
+ child.on("error", (err) => {
842
+ clearTimeout(timeout);
843
+ if (!urlFound) {
844
+ reject(
845
+ new Error(
846
+ `Tunnel failed to start. Check if port ${localPort} is accessible.
847
+ Original error: ${err.message}`
848
+ )
849
+ );
850
+ }
851
+ });
852
+ child.on("exit", (code2) => {
853
+ clearTimeout(timeout);
854
+ if (!urlFound) {
855
+ reject(
856
+ new Error(
857
+ code2 != null && code2 !== 0 ? `Could not extract tunnel URL. Try with \`--tunnel-url\` flag.` : `Tunnel failed to start. Check if port ${localPort} is accessible.`
858
+ )
859
+ );
860
+ }
861
+ });
862
+ });
863
+ }
864
+
865
+ // src/version/check.ts
866
+ var import_promises5 = require("fs/promises");
867
+ var import_node_path5 = __toESM(require("path"), 1);
868
+ var import_chalk3 = __toESM(require("chalk"), 1);
869
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
870
+ var REGISTRY_TIMEOUT_MS = 3e3;
871
+ function getCachePath() {
872
+ return import_node_path5.default.join(getSessionDir(), "version-check.json");
873
+ }
874
+ async function readCache() {
875
+ try {
876
+ const raw = await (0, import_promises5.readFile)(getCachePath(), "utf-8");
877
+ const cache = JSON.parse(raw);
878
+ const lastCheck = new Date(cache.lastCheck).getTime();
879
+ if (Date.now() - lastCheck < CHECK_INTERVAL_MS) {
880
+ return cache;
881
+ }
882
+ return null;
883
+ } catch {
884
+ return null;
885
+ }
886
+ }
887
+ async function writeCache(cache) {
888
+ try {
889
+ const dir = getSessionDir();
890
+ await (0, import_promises5.mkdir)(dir, { recursive: true, mode: 448 });
891
+ await (0, import_promises5.writeFile)(getCachePath(), JSON.stringify(cache, null, 2), "utf-8");
892
+ } catch {
893
+ }
894
+ }
895
+ async function fetchLatestVersion(packageName) {
896
+ try {
897
+ const res = await fetch(
898
+ `https://registry.npmjs.org/${packageName}/latest`,
899
+ { signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) }
900
+ );
901
+ if (!res.ok) return null;
902
+ const data = await res.json();
903
+ return data.version ?? null;
904
+ } catch {
905
+ return null;
906
+ }
907
+ }
908
+ function isNewerVersion(current, latest) {
909
+ const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
910
+ const c = parse(current);
911
+ const l = parse(latest);
912
+ for (let i = 0; i < 3; i++) {
913
+ if ((l[i] ?? 0) > (c[i] ?? 0)) return true;
914
+ if ((l[i] ?? 0) < (c[i] ?? 0)) return false;
915
+ }
916
+ return false;
917
+ }
918
+ function getUpgradeCommand() {
919
+ const pm = detectPackageManager();
920
+ switch (pm) {
921
+ case "pnpm":
922
+ return "pnpm add -g @whatalo/cli";
923
+ case "yarn":
924
+ return "yarn global add @whatalo/cli";
925
+ case "npm":
926
+ return "npm install -g @whatalo/cli";
927
+ }
928
+ }
929
+ function detectPackageManager() {
930
+ const userAgent = process.env.npm_config_user_agent ?? "";
931
+ if (userAgent.includes("pnpm")) return "pnpm";
932
+ if (userAgent.includes("yarn")) return "yarn";
933
+ if (userAgent.includes("npm")) return "npm";
934
+ return "pnpm";
935
+ }
936
+ function scheduleVersionCheck(currentVersion) {
937
+ const checkPromise = (async () => {
938
+ const cached = await readCache();
939
+ if (cached && cached.currentVersion === currentVersion) {
940
+ return cached.latestVersion !== currentVersion && isNewerVersion(currentVersion, cached.latestVersion) ? cached.latestVersion : null;
941
+ }
942
+ const latestVersion = await fetchLatestVersion("@whatalo/cli");
943
+ if (!latestVersion) return null;
944
+ await writeCache({
945
+ lastCheck: (/* @__PURE__ */ new Date()).toISOString(),
946
+ latestVersion,
947
+ currentVersion
948
+ });
949
+ return isNewerVersion(currentVersion, latestVersion) ? latestVersion : null;
950
+ })();
951
+ process.on("exit", () => {
952
+ checkPromise.then((latestVersion) => {
953
+ if (!latestVersion) return;
954
+ const upgradeCmd = getUpgradeCommand();
955
+ const box = [
956
+ "",
957
+ import_chalk3.default.yellow("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"),
958
+ import_chalk3.default.yellow("\u2502 \u2502"),
959
+ import_chalk3.default.yellow("\u2502") + ` Update available: ${import_chalk3.default.dim(currentVersion)} ${import_chalk3.default.yellow("\u2192")} ${import_chalk3.default.green(latestVersion)} ` + import_chalk3.default.yellow("\u2502"),
960
+ import_chalk3.default.yellow("\u2502") + ` Run: ${import_chalk3.default.cyan(upgradeCmd)} ` + import_chalk3.default.yellow("\u2502"),
961
+ import_chalk3.default.yellow("\u2502 \u2502"),
962
+ import_chalk3.default.yellow("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"),
963
+ ""
964
+ ];
965
+ for (const line of box) {
966
+ process.stderr.write(line + "\n");
967
+ }
968
+ }).catch(() => {
969
+ });
970
+ });
971
+ }
972
+
973
+ // src/version/compatibility.ts
974
+ var import_node_fs3 = require("fs");
975
+ var import_node_path6 = __toESM(require("path"), 1);
976
+ function checkSdkCompatibility(cliVersion, projectDir) {
977
+ const sdkVersion = getSdkVersion(projectDir);
978
+ if (!sdkVersion) return null;
979
+ const cliMajor = parseMajor(cliVersion);
980
+ const sdkMajor = parseMajor(sdkVersion);
981
+ if (cliMajor !== sdkMajor) {
982
+ return `SDK version ${sdkVersion} may not be compatible with CLI ${cliVersion}. Run: pnpm update @whatalo/app-sdk`;
983
+ }
984
+ const cliMinor = parseMinor(cliVersion);
985
+ const sdkMinor = parseMinor(sdkVersion);
986
+ if (cliMinor - sdkMinor > 1) {
987
+ return `SDK version ${sdkVersion} is behind CLI ${cliVersion}. Run: pnpm update @whatalo/app-sdk`;
988
+ }
989
+ return null;
990
+ }
991
+ function getSdkVersion(projectDir) {
992
+ try {
993
+ const pkgPath = import_node_path6.default.join(
994
+ projectDir,
995
+ "node_modules",
996
+ "@whatalo",
997
+ "app-sdk",
998
+ "package.json"
999
+ );
1000
+ const raw = (0, import_node_fs3.readFileSync)(pkgPath, "utf-8");
1001
+ const pkg = JSON.parse(raw);
1002
+ return pkg.version ?? null;
1003
+ } catch {
1004
+ return null;
1005
+ }
1006
+ }
1007
+ function parseMajor(version) {
1008
+ return parseInt(version.replace(/^v/, "").split(".")[0] ?? "0", 10);
1009
+ }
1010
+ function parseMinor(version) {
1011
+ return parseInt(version.replace(/^v/, "").split(".")[1] ?? "0", 10);
1012
+ }
1013
+ // Annotate the CommonJS export names for ESM import in node:
1014
+ 0 && (module.exports = {
1015
+ CONFIG_FILE_NAME,
1016
+ POLL_STATUS,
1017
+ WhataloApiClient,
1018
+ WhataloAuthError,
1019
+ WhataloConfigError,
1020
+ WhataloNetworkError,
1021
+ WhataloValidationError,
1022
+ banner,
1023
+ checkSdkCompatibility,
1024
+ clearSession,
1025
+ code,
1026
+ createSpinner,
1027
+ createTunnel,
1028
+ ensureCloudflared,
1029
+ error,
1030
+ failMissingNonTTYFlags,
1031
+ getSession,
1032
+ getSessionDir,
1033
+ getUpgradeCommand,
1034
+ handleCliError,
1035
+ info,
1036
+ isNewerVersion,
1037
+ isSessionValid,
1038
+ link,
1039
+ parseEnvFile,
1040
+ pollForToken,
1041
+ readConfig,
1042
+ refreshAccessToken,
1043
+ renderInfoPanel,
1044
+ renderTable,
1045
+ renderTasks,
1046
+ requestDeviceCode,
1047
+ saveSession,
1048
+ scheduleVersionCheck,
1049
+ success,
1050
+ table,
1051
+ updateEnvFile,
1052
+ warn,
1053
+ withErrorHandler,
1054
+ writeConfig
1055
+ });