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