@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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// src/tunnel/cloudflared.ts
|
|
2
|
+
import { spawnSync, spawn } from "child_process";
|
|
3
|
+
import { existsSync, chmodSync, mkdirSync, createWriteStream } from "fs";
|
|
4
|
+
import { unlink, rename } from "fs/promises";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import https from "https";
|
|
8
|
+
|
|
9
|
+
// src/output/format.ts
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
function info(message) {
|
|
12
|
+
console.log(` ${chalk.blue("\u2139")} ${message}`);
|
|
13
|
+
}
|
|
14
|
+
var STATUS_ICONS = {
|
|
15
|
+
pending: chalk.dim("\u25CB"),
|
|
16
|
+
running: chalk.cyan("\u25C9"),
|
|
17
|
+
success: chalk.green("\u2713"),
|
|
18
|
+
error: chalk.red("\u2717"),
|
|
19
|
+
warning: chalk.yellow("\u26A0"),
|
|
20
|
+
skipped: chalk.dim("\u2013")
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/tunnel/cloudflared.ts
|
|
24
|
+
var TUNNEL_START_TIMEOUT_MS = 15e3;
|
|
25
|
+
var TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
26
|
+
function getBinDir() {
|
|
27
|
+
return path.join(os.homedir(), ".whatalo", "bin");
|
|
28
|
+
}
|
|
29
|
+
function getManagedBinaryPath() {
|
|
30
|
+
return path.join(getBinDir(), "cloudflared");
|
|
31
|
+
}
|
|
32
|
+
function findOnSystemPath() {
|
|
33
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
34
|
+
const result = spawnSync(cmd, ["cloudflared"], { encoding: "utf-8" });
|
|
35
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
36
|
+
return result.stdout.trim().split("\n")[0] ?? null;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
async function ensureCloudflared() {
|
|
41
|
+
const systemPath = findOnSystemPath();
|
|
42
|
+
if (systemPath) return systemPath;
|
|
43
|
+
const managedPath = getManagedBinaryPath();
|
|
44
|
+
if (existsSync(managedPath)) return managedPath;
|
|
45
|
+
return downloadCloudflared(managedPath);
|
|
46
|
+
}
|
|
47
|
+
function resolvePlatformInfo() {
|
|
48
|
+
const platform = process.platform;
|
|
49
|
+
const arch = process.arch === "x64" ? "amd64" : process.arch;
|
|
50
|
+
if (platform === "darwin") {
|
|
51
|
+
return { os: "darwin", arch, ext: "tgz" };
|
|
52
|
+
}
|
|
53
|
+
if (platform === "linux") {
|
|
54
|
+
return { os: "linux", arch, ext: "binary" };
|
|
55
|
+
}
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Unsupported platform: ${platform}. Install cloudflared manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
function buildDownloadUrl(osName, arch, ext) {
|
|
61
|
+
const base = "https://github.com/cloudflare/cloudflared/releases/latest/download";
|
|
62
|
+
if (ext === "tgz") {
|
|
63
|
+
return `${base}/cloudflared-${osName}-${arch}.tgz`;
|
|
64
|
+
}
|
|
65
|
+
return `${base}/cloudflared-${osName}-${arch}`;
|
|
66
|
+
}
|
|
67
|
+
async function downloadFile(url, dest) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const follow = (targetUrl) => {
|
|
70
|
+
https.get(targetUrl, (res) => {
|
|
71
|
+
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
|
|
72
|
+
follow(res.headers.location);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (res.statusCode !== 200) {
|
|
76
|
+
reject(
|
|
77
|
+
new Error(
|
|
78
|
+
`Download failed with HTTP ${res.statusCode}: ${targetUrl}`
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const file = createWriteStream(dest);
|
|
84
|
+
res.pipe(file);
|
|
85
|
+
file.on("finish", () => file.close(() => resolve()));
|
|
86
|
+
file.on("error", (err) => {
|
|
87
|
+
file.close();
|
|
88
|
+
reject(err);
|
|
89
|
+
});
|
|
90
|
+
}).on("error", reject);
|
|
91
|
+
};
|
|
92
|
+
follow(url);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async function downloadCloudflared(targetPath) {
|
|
96
|
+
const { os: osName, arch, ext } = resolvePlatformInfo();
|
|
97
|
+
const downloadUrl = buildDownloadUrl(osName, arch, ext);
|
|
98
|
+
mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
99
|
+
info(`Downloading cloudflared for ${osName}/${arch}\u2026`);
|
|
100
|
+
info(`Source: ${downloadUrl}`);
|
|
101
|
+
const tmpPath = `${targetPath}.tmp`;
|
|
102
|
+
try {
|
|
103
|
+
await downloadFile(downloadUrl, tmpPath);
|
|
104
|
+
if (ext === "tgz") {
|
|
105
|
+
const tarResult = spawnSync(
|
|
106
|
+
"tar",
|
|
107
|
+
["xzf", tmpPath, "-C", path.dirname(targetPath), "cloudflared"],
|
|
108
|
+
{ encoding: "utf-8" }
|
|
109
|
+
);
|
|
110
|
+
if (tarResult.status !== 0) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`tar extraction failed: ${tarResult.stderr || (tarResult.error?.message ?? "unknown error")}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
await unlink(tmpPath).catch(() => void 0);
|
|
116
|
+
} else {
|
|
117
|
+
await rename(tmpPath, targetPath);
|
|
118
|
+
}
|
|
119
|
+
if (!existsSync(targetPath)) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"Binary extraction completed but cloudflared was not found at the expected path."
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
chmodSync(targetPath, 493);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
await unlink(tmpPath).catch(() => void 0);
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Could not download cloudflared. Install manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/
|
|
129
|
+
Original error: ${err.message}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
info(`cloudflared saved to ${targetPath}`);
|
|
133
|
+
return targetPath;
|
|
134
|
+
}
|
|
135
|
+
async function createTunnel(options) {
|
|
136
|
+
const { localPort, protocol = "http" } = options;
|
|
137
|
+
const binaryPath = await ensureCloudflared();
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const child = spawn(
|
|
140
|
+
binaryPath,
|
|
141
|
+
[
|
|
142
|
+
"tunnel",
|
|
143
|
+
"--url",
|
|
144
|
+
`${protocol}://localhost:${localPort}`,
|
|
145
|
+
"--no-autoupdate"
|
|
146
|
+
],
|
|
147
|
+
{
|
|
148
|
+
// Inherit environment so cloudflared can read system certificates
|
|
149
|
+
env: { ...process.env },
|
|
150
|
+
// stdout flows to the terminal; stderr is captured for URL extraction
|
|
151
|
+
stdio: ["ignore", "inherit", "pipe"]
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
let urlFound = false;
|
|
155
|
+
let lineBuffer = "";
|
|
156
|
+
const timeout = setTimeout(() => {
|
|
157
|
+
if (!urlFound) {
|
|
158
|
+
child.kill("SIGTERM");
|
|
159
|
+
reject(
|
|
160
|
+
new Error(
|
|
161
|
+
`Tunnel failed to start. Check if port ${localPort} is accessible.`
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}, TUNNEL_START_TIMEOUT_MS);
|
|
166
|
+
child.stderr?.on("data", (chunk) => {
|
|
167
|
+
lineBuffer += chunk.toString("utf-8");
|
|
168
|
+
const lines = lineBuffer.split("\n");
|
|
169
|
+
lineBuffer = lines.pop() ?? "";
|
|
170
|
+
for (const line of lines) {
|
|
171
|
+
const match = TUNNEL_URL_REGEX.exec(line);
|
|
172
|
+
if (match && !urlFound) {
|
|
173
|
+
urlFound = true;
|
|
174
|
+
clearTimeout(timeout);
|
|
175
|
+
const tunnelUrl = match[0];
|
|
176
|
+
const kill = () => new Promise((res) => {
|
|
177
|
+
if (child.exitCode !== null) {
|
|
178
|
+
res();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
child.once("exit", () => res());
|
|
182
|
+
child.kill("SIGTERM");
|
|
183
|
+
});
|
|
184
|
+
resolve({ url: tunnelUrl, process: child, kill });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
child.on("error", (err) => {
|
|
189
|
+
clearTimeout(timeout);
|
|
190
|
+
if (!urlFound) {
|
|
191
|
+
reject(
|
|
192
|
+
new Error(
|
|
193
|
+
`Tunnel failed to start. Check if port ${localPort} is accessible.
|
|
194
|
+
Original error: ${err.message}`
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
child.on("exit", (code) => {
|
|
200
|
+
clearTimeout(timeout);
|
|
201
|
+
if (!urlFound) {
|
|
202
|
+
reject(
|
|
203
|
+
new Error(
|
|
204
|
+
code != null && code !== 0 ? `Could not extract tunnel URL. Try with \`--tunnel-url\` flag.` : `Tunnel failed to start. Check if port ${localPort} is accessible.`
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
export {
|
|
212
|
+
createTunnel,
|
|
213
|
+
ensureCloudflared
|
|
214
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a persisted developer authentication session.
|
|
3
|
+
* Stored at ~/.whatalo/session.json
|
|
4
|
+
*/
|
|
5
|
+
interface WhataloSession {
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
/** ISO 8601 expiry timestamp */
|
|
9
|
+
expiresAt: string;
|
|
10
|
+
/** developer_profiles.id (UUID) */
|
|
11
|
+
developerId: string;
|
|
12
|
+
email: string;
|
|
13
|
+
/** URL of the Developer Portal that issued the tokens */
|
|
14
|
+
portalUrl: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Response from the device authorization endpoint.
|
|
18
|
+
* Returned when initiating the Device Authorization Grant (RFC 8628).
|
|
19
|
+
*/
|
|
20
|
+
interface DeviceCodeResponse {
|
|
21
|
+
deviceCode: string;
|
|
22
|
+
/** Short user-facing code to enter at the verification URI */
|
|
23
|
+
userCode: string;
|
|
24
|
+
verificationUri: string;
|
|
25
|
+
/** Lifetime of the device code in seconds */
|
|
26
|
+
expiresIn: number;
|
|
27
|
+
/** Minimum polling interval in seconds (never poll faster than this) */
|
|
28
|
+
interval: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Token payload returned by the authorization server after successful device grant.
|
|
32
|
+
*/
|
|
33
|
+
interface TokenResponse {
|
|
34
|
+
accessToken: string;
|
|
35
|
+
refreshToken: string;
|
|
36
|
+
/** Lifetime of the access token in seconds */
|
|
37
|
+
expiresIn: number;
|
|
38
|
+
/** developer_profiles.id (UUID) */
|
|
39
|
+
developerId: string;
|
|
40
|
+
email: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Discriminated union for each poll attempt during the Device Authorization Grant.
|
|
44
|
+
*/
|
|
45
|
+
declare const POLL_STATUS: {
|
|
46
|
+
readonly PENDING: "pending";
|
|
47
|
+
readonly AUTHORIZED: "authorized";
|
|
48
|
+
readonly EXPIRED: "expired";
|
|
49
|
+
readonly DENIED: "denied";
|
|
50
|
+
};
|
|
51
|
+
type PollStatus = (typeof POLL_STATUS)[keyof typeof POLL_STATUS];
|
|
52
|
+
type PollResult = {
|
|
53
|
+
status: typeof POLL_STATUS.PENDING;
|
|
54
|
+
} | {
|
|
55
|
+
status: typeof POLL_STATUS.AUTHORIZED;
|
|
56
|
+
token: TokenResponse;
|
|
57
|
+
} | {
|
|
58
|
+
status: typeof POLL_STATUS.EXPIRED;
|
|
59
|
+
} | {
|
|
60
|
+
status: typeof POLL_STATUS.DENIED;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export { type DeviceCodeResponse as D, type PollResult as P, type TokenResponse as T, type WhataloSession as W, type PollStatus as a, POLL_STATUS as b };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a persisted developer authentication session.
|
|
3
|
+
* Stored at ~/.whatalo/session.json
|
|
4
|
+
*/
|
|
5
|
+
interface WhataloSession {
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
/** ISO 8601 expiry timestamp */
|
|
9
|
+
expiresAt: string;
|
|
10
|
+
/** developer_profiles.id (UUID) */
|
|
11
|
+
developerId: string;
|
|
12
|
+
email: string;
|
|
13
|
+
/** URL of the Developer Portal that issued the tokens */
|
|
14
|
+
portalUrl: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Response from the device authorization endpoint.
|
|
18
|
+
* Returned when initiating the Device Authorization Grant (RFC 8628).
|
|
19
|
+
*/
|
|
20
|
+
interface DeviceCodeResponse {
|
|
21
|
+
deviceCode: string;
|
|
22
|
+
/** Short user-facing code to enter at the verification URI */
|
|
23
|
+
userCode: string;
|
|
24
|
+
verificationUri: string;
|
|
25
|
+
/** Lifetime of the device code in seconds */
|
|
26
|
+
expiresIn: number;
|
|
27
|
+
/** Minimum polling interval in seconds (never poll faster than this) */
|
|
28
|
+
interval: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Token payload returned by the authorization server after successful device grant.
|
|
32
|
+
*/
|
|
33
|
+
interface TokenResponse {
|
|
34
|
+
accessToken: string;
|
|
35
|
+
refreshToken: string;
|
|
36
|
+
/** Lifetime of the access token in seconds */
|
|
37
|
+
expiresIn: number;
|
|
38
|
+
/** developer_profiles.id (UUID) */
|
|
39
|
+
developerId: string;
|
|
40
|
+
email: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Discriminated union for each poll attempt during the Device Authorization Grant.
|
|
44
|
+
*/
|
|
45
|
+
declare const POLL_STATUS: {
|
|
46
|
+
readonly PENDING: "pending";
|
|
47
|
+
readonly AUTHORIZED: "authorized";
|
|
48
|
+
readonly EXPIRED: "expired";
|
|
49
|
+
readonly DENIED: "denied";
|
|
50
|
+
};
|
|
51
|
+
type PollStatus = (typeof POLL_STATUS)[keyof typeof POLL_STATUS];
|
|
52
|
+
type PollResult = {
|
|
53
|
+
status: typeof POLL_STATUS.PENDING;
|
|
54
|
+
} | {
|
|
55
|
+
status: typeof POLL_STATUS.AUTHORIZED;
|
|
56
|
+
token: TokenResponse;
|
|
57
|
+
} | {
|
|
58
|
+
status: typeof POLL_STATUS.EXPIRED;
|
|
59
|
+
} | {
|
|
60
|
+
status: typeof POLL_STATUS.DENIED;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export { type DeviceCodeResponse as D, type PollResult as P, type TokenResponse as T, type WhataloSession as W, type PollStatus as a, POLL_STATUS as b };
|
|
@@ -0,0 +1,204 @@
|
|
|
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/version/index.ts
|
|
31
|
+
var version_exports = {};
|
|
32
|
+
__export(version_exports, {
|
|
33
|
+
checkSdkCompatibility: () => checkSdkCompatibility,
|
|
34
|
+
getUpgradeCommand: () => getUpgradeCommand,
|
|
35
|
+
isNewerVersion: () => isNewerVersion,
|
|
36
|
+
scheduleVersionCheck: () => scheduleVersionCheck
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(version_exports);
|
|
39
|
+
|
|
40
|
+
// src/version/check.ts
|
|
41
|
+
var import_promises2 = require("fs/promises");
|
|
42
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
43
|
+
var import_chalk = __toESM(require("chalk"), 1);
|
|
44
|
+
|
|
45
|
+
// src/session/store.ts
|
|
46
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
47
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
48
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
49
|
+
function getSessionDir() {
|
|
50
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".whatalo");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/version/check.ts
|
|
54
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
55
|
+
var REGISTRY_TIMEOUT_MS = 3e3;
|
|
56
|
+
function getCachePath() {
|
|
57
|
+
return import_node_path2.default.join(getSessionDir(), "version-check.json");
|
|
58
|
+
}
|
|
59
|
+
async function readCache() {
|
|
60
|
+
try {
|
|
61
|
+
const raw = await (0, import_promises2.readFile)(getCachePath(), "utf-8");
|
|
62
|
+
const cache = JSON.parse(raw);
|
|
63
|
+
const lastCheck = new Date(cache.lastCheck).getTime();
|
|
64
|
+
if (Date.now() - lastCheck < CHECK_INTERVAL_MS) {
|
|
65
|
+
return cache;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function writeCache(cache) {
|
|
73
|
+
try {
|
|
74
|
+
const dir = getSessionDir();
|
|
75
|
+
await (0, import_promises2.mkdir)(dir, { recursive: true, mode: 448 });
|
|
76
|
+
await (0, import_promises2.writeFile)(getCachePath(), JSON.stringify(cache, null, 2), "utf-8");
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function fetchLatestVersion(packageName) {
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(
|
|
83
|
+
`https://registry.npmjs.org/${packageName}/latest`,
|
|
84
|
+
{ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) }
|
|
85
|
+
);
|
|
86
|
+
if (!res.ok) return null;
|
|
87
|
+
const data = await res.json();
|
|
88
|
+
return data.version ?? null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function isNewerVersion(current, latest) {
|
|
94
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
|
|
95
|
+
const c = parse(current);
|
|
96
|
+
const l = parse(latest);
|
|
97
|
+
for (let i = 0; i < 3; i++) {
|
|
98
|
+
if ((l[i] ?? 0) > (c[i] ?? 0)) return true;
|
|
99
|
+
if ((l[i] ?? 0) < (c[i] ?? 0)) return false;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
function getUpgradeCommand() {
|
|
104
|
+
const pm = detectPackageManager();
|
|
105
|
+
switch (pm) {
|
|
106
|
+
case "pnpm":
|
|
107
|
+
return "pnpm add -g @whatalo/cli";
|
|
108
|
+
case "yarn":
|
|
109
|
+
return "yarn global add @whatalo/cli";
|
|
110
|
+
case "npm":
|
|
111
|
+
return "npm install -g @whatalo/cli";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function detectPackageManager() {
|
|
115
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
116
|
+
if (userAgent.includes("pnpm")) return "pnpm";
|
|
117
|
+
if (userAgent.includes("yarn")) return "yarn";
|
|
118
|
+
if (userAgent.includes("npm")) return "npm";
|
|
119
|
+
return "pnpm";
|
|
120
|
+
}
|
|
121
|
+
function scheduleVersionCheck(currentVersion) {
|
|
122
|
+
const checkPromise = (async () => {
|
|
123
|
+
const cached = await readCache();
|
|
124
|
+
if (cached && cached.currentVersion === currentVersion) {
|
|
125
|
+
return cached.latestVersion !== currentVersion && isNewerVersion(currentVersion, cached.latestVersion) ? cached.latestVersion : null;
|
|
126
|
+
}
|
|
127
|
+
const latestVersion = await fetchLatestVersion("@whatalo/cli");
|
|
128
|
+
if (!latestVersion) return null;
|
|
129
|
+
await writeCache({
|
|
130
|
+
lastCheck: (/* @__PURE__ */ new Date()).toISOString(),
|
|
131
|
+
latestVersion,
|
|
132
|
+
currentVersion
|
|
133
|
+
});
|
|
134
|
+
return isNewerVersion(currentVersion, latestVersion) ? latestVersion : null;
|
|
135
|
+
})();
|
|
136
|
+
process.on("exit", () => {
|
|
137
|
+
checkPromise.then((latestVersion) => {
|
|
138
|
+
if (!latestVersion) return;
|
|
139
|
+
const upgradeCmd = getUpgradeCommand();
|
|
140
|
+
const box = [
|
|
141
|
+
"",
|
|
142
|
+
import_chalk.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"),
|
|
143
|
+
import_chalk.default.yellow("\u2502 \u2502"),
|
|
144
|
+
import_chalk.default.yellow("\u2502") + ` Update available: ${import_chalk.default.dim(currentVersion)} ${import_chalk.default.yellow("\u2192")} ${import_chalk.default.green(latestVersion)} ` + import_chalk.default.yellow("\u2502"),
|
|
145
|
+
import_chalk.default.yellow("\u2502") + ` Run: ${import_chalk.default.cyan(upgradeCmd)} ` + import_chalk.default.yellow("\u2502"),
|
|
146
|
+
import_chalk.default.yellow("\u2502 \u2502"),
|
|
147
|
+
import_chalk.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"),
|
|
148
|
+
""
|
|
149
|
+
];
|
|
150
|
+
for (const line of box) {
|
|
151
|
+
process.stderr.write(line + "\n");
|
|
152
|
+
}
|
|
153
|
+
}).catch(() => {
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/version/compatibility.ts
|
|
159
|
+
var import_node_fs = require("fs");
|
|
160
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
161
|
+
function checkSdkCompatibility(cliVersion, projectDir) {
|
|
162
|
+
const sdkVersion = getSdkVersion(projectDir);
|
|
163
|
+
if (!sdkVersion) return null;
|
|
164
|
+
const cliMajor = parseMajor(cliVersion);
|
|
165
|
+
const sdkMajor = parseMajor(sdkVersion);
|
|
166
|
+
if (cliMajor !== sdkMajor) {
|
|
167
|
+
return `SDK version ${sdkVersion} may not be compatible with CLI ${cliVersion}. Run: pnpm update @whatalo/app-sdk`;
|
|
168
|
+
}
|
|
169
|
+
const cliMinor = parseMinor(cliVersion);
|
|
170
|
+
const sdkMinor = parseMinor(sdkVersion);
|
|
171
|
+
if (cliMinor - sdkMinor > 1) {
|
|
172
|
+
return `SDK version ${sdkVersion} is behind CLI ${cliVersion}. Run: pnpm update @whatalo/app-sdk`;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
function getSdkVersion(projectDir) {
|
|
177
|
+
try {
|
|
178
|
+
const pkgPath = import_node_path3.default.join(
|
|
179
|
+
projectDir,
|
|
180
|
+
"node_modules",
|
|
181
|
+
"@whatalo",
|
|
182
|
+
"app-sdk",
|
|
183
|
+
"package.json"
|
|
184
|
+
);
|
|
185
|
+
const raw = (0, import_node_fs.readFileSync)(pkgPath, "utf-8");
|
|
186
|
+
const pkg = JSON.parse(raw);
|
|
187
|
+
return pkg.version ?? null;
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function parseMajor(version) {
|
|
193
|
+
return parseInt(version.replace(/^v/, "").split(".")[0] ?? "0", 10);
|
|
194
|
+
}
|
|
195
|
+
function parseMinor(version) {
|
|
196
|
+
return parseInt(version.replace(/^v/, "").split(".")[1] ?? "0", 10);
|
|
197
|
+
}
|
|
198
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
199
|
+
0 && (module.exports = {
|
|
200
|
+
checkSdkCompatibility,
|
|
201
|
+
getUpgradeCommand,
|
|
202
|
+
isNewerVersion,
|
|
203
|
+
scheduleVersionCheck
|
|
204
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-blocking CLI version check against the npm registry.
|
|
3
|
+
* Displays an update notice AFTER the command completes, never blocks execution.
|
|
4
|
+
*
|
|
5
|
+
* Cached to ~/.whatalo/version-check.json with a 24-hour TTL.
|
|
6
|
+
*/
|
|
7
|
+
interface VersionCheckResult {
|
|
8
|
+
updateAvailable: boolean;
|
|
9
|
+
currentVersion: string;
|
|
10
|
+
latestVersion: string;
|
|
11
|
+
upgradeCommand: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compares two semver version strings.
|
|
15
|
+
* Returns true if `latest` is newer than `current`.
|
|
16
|
+
*/
|
|
17
|
+
declare function isNewerVersion(current: string, latest: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Detects how the CLI was installed and returns the correct upgrade command.
|
|
20
|
+
*/
|
|
21
|
+
declare function getUpgradeCommand(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Fire-and-forget version check. Displays an update notice AFTER the command
|
|
24
|
+
* completes by registering a process.on("exit") handler.
|
|
25
|
+
*
|
|
26
|
+
* NEVER blocks command execution. Errors are silently ignored.
|
|
27
|
+
*/
|
|
28
|
+
declare function scheduleVersionCheck(currentVersion: string): void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* SDK version compatibility checker.
|
|
32
|
+
* Compares the installed @whatalo/app-sdk version against the CLI version
|
|
33
|
+
* and returns a warning if they are potentially incompatible.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Checks if the installed @whatalo/app-sdk version is compatible with the CLI.
|
|
37
|
+
* Returns a warning message or null if compatible.
|
|
38
|
+
*/
|
|
39
|
+
declare function checkSdkCompatibility(cliVersion: string, projectDir: string): string | null;
|
|
40
|
+
|
|
41
|
+
export { type VersionCheckResult, checkSdkCompatibility, getUpgradeCommand, isNewerVersion, scheduleVersionCheck };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-blocking CLI version check against the npm registry.
|
|
3
|
+
* Displays an update notice AFTER the command completes, never blocks execution.
|
|
4
|
+
*
|
|
5
|
+
* Cached to ~/.whatalo/version-check.json with a 24-hour TTL.
|
|
6
|
+
*/
|
|
7
|
+
interface VersionCheckResult {
|
|
8
|
+
updateAvailable: boolean;
|
|
9
|
+
currentVersion: string;
|
|
10
|
+
latestVersion: string;
|
|
11
|
+
upgradeCommand: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compares two semver version strings.
|
|
15
|
+
* Returns true if `latest` is newer than `current`.
|
|
16
|
+
*/
|
|
17
|
+
declare function isNewerVersion(current: string, latest: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Detects how the CLI was installed and returns the correct upgrade command.
|
|
20
|
+
*/
|
|
21
|
+
declare function getUpgradeCommand(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Fire-and-forget version check. Displays an update notice AFTER the command
|
|
24
|
+
* completes by registering a process.on("exit") handler.
|
|
25
|
+
*
|
|
26
|
+
* NEVER blocks command execution. Errors are silently ignored.
|
|
27
|
+
*/
|
|
28
|
+
declare function scheduleVersionCheck(currentVersion: string): void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* SDK version compatibility checker.
|
|
32
|
+
* Compares the installed @whatalo/app-sdk version against the CLI version
|
|
33
|
+
* and returns a warning if they are potentially incompatible.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Checks if the installed @whatalo/app-sdk version is compatible with the CLI.
|
|
37
|
+
* Returns a warning message or null if compatible.
|
|
38
|
+
*/
|
|
39
|
+
declare function checkSdkCompatibility(cliVersion: string, projectDir: string): string | null;
|
|
40
|
+
|
|
41
|
+
export { type VersionCheckResult, checkSdkCompatibility, getUpgradeCommand, isNewerVersion, scheduleVersionCheck };
|