kandev 0.2.0 → 0.4.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/dev.js +5 -2
- package/dist/github.js +104 -53
- package/dist/health.js +18 -0
- package/dist/run.js +106 -25
- package/dist/start.js +6 -3
- package/dist/update.js +2 -14
- package/dist/version.js +57 -0
- package/dist/web.js +1 -2
- package/package.json +4 -2
package/dist/dev.js
CHANGED
|
@@ -37,17 +37,20 @@ async function runDev({ repoRoot, backendPort, webPort }) {
|
|
|
37
37
|
supervisor.children.push(backendProc);
|
|
38
38
|
(0, shared_1.attachBackendExitHandler)(backendProc, supervisor);
|
|
39
39
|
const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_DEV);
|
|
40
|
+
console.log("[kandev] starting backend...");
|
|
40
41
|
await (0, health_1.waitForHealth)(ports.backendUrl, backendProc, healthTimeoutMs);
|
|
41
42
|
console.log(`[kandev] backend ready at ${ports.backendUrl}`);
|
|
42
43
|
const webUrl = `http://localhost:${ports.webPort}`;
|
|
43
|
-
(
|
|
44
|
+
console.log("[kandev] starting web...");
|
|
45
|
+
const webProc = (0, web_1.launchWebApp)({
|
|
44
46
|
command: "pnpm",
|
|
45
47
|
args: ["-C", "apps", "--filter", "@kandev/web", "dev"],
|
|
46
48
|
cwd: repoRoot,
|
|
47
49
|
env: webEnv,
|
|
48
|
-
url: webUrl,
|
|
49
50
|
supervisor,
|
|
50
51
|
label: "web",
|
|
51
52
|
});
|
|
53
|
+
await (0, health_1.waitForUrlReady)(webUrl, webProc, healthTimeoutMs);
|
|
52
54
|
console.log(`[kandev] web ready at ${webUrl}`);
|
|
55
|
+
(0, web_1.openBrowser)(webUrl);
|
|
53
56
|
}
|
package/dist/github.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getRelease = getRelease;
|
|
7
|
+
exports.readSha256 = readSha256;
|
|
7
8
|
exports.ensureAsset = ensureAsset;
|
|
8
9
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
9
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -12,38 +13,99 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
12
13
|
// Allow overriding the GitHub repo for forks/testing.
|
|
13
14
|
const OWNER = process.env.KANDEV_GITHUB_OWNER || "kdlbs";
|
|
14
15
|
const REPO = process.env.KANDEV_GITHUB_REPO || "kandev";
|
|
15
|
-
const
|
|
16
|
-
function
|
|
16
|
+
const WEB_BASE = `https://github.com/${OWNER}/${REPO}`;
|
|
17
|
+
function authHeaders() {
|
|
18
|
+
if (process.env.KANDEV_GITHUB_TOKEN) {
|
|
19
|
+
return { Authorization: `Bearer ${process.env.KANDEV_GITHUB_TOKEN}` };
|
|
20
|
+
}
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the latest release tag by following the redirect from
|
|
25
|
+
* github.com/{owner}/{repo}/releases/latest.
|
|
26
|
+
*
|
|
27
|
+
* Uses github.com (not api.github.com) so it is not subject to
|
|
28
|
+
* the REST API rate limit (60 req/hour per IP).
|
|
29
|
+
*/
|
|
30
|
+
function resolveLatestTag() {
|
|
31
|
+
const url = `${WEB_BASE}/releases/latest`;
|
|
17
32
|
return new Promise((resolve, reject) => {
|
|
18
33
|
const req = node_https_1.default.get(url, {
|
|
19
|
-
headers: {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
...(process.env.KANDEV_GITHUB_TOKEN
|
|
23
|
-
? { Authorization: `Bearer ${process.env.KANDEV_GITHUB_TOKEN}` }
|
|
24
|
-
: {}),
|
|
25
|
-
},
|
|
34
|
+
headers: { "User-Agent": "kandev-npx", ...authHeaders() },
|
|
35
|
+
// Do not follow redirects — we just need the Location header.
|
|
36
|
+
followRedirect: false,
|
|
26
37
|
}, (res) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
resolve(JSON.parse(body));
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
reject(new Error(`Failed to parse JSON from ${url}`));
|
|
38
|
+
// Drain the response body to free the socket.
|
|
39
|
+
res.resume();
|
|
40
|
+
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
|
|
41
|
+
// Location: https://github.com/{owner}/{repo}/releases/tag/v0.2
|
|
42
|
+
const match = res.headers.location.match(/\/releases\/tag\/(.+)$/);
|
|
43
|
+
if (match) {
|
|
44
|
+
return resolve(match[1]);
|
|
38
45
|
}
|
|
39
|
-
|
|
46
|
+
return reject(new Error(`Could not parse tag from redirect: ${res.headers.location}`));
|
|
47
|
+
}
|
|
48
|
+
// GitHub returns 200 if there's only one release (no redirect).
|
|
49
|
+
// In that case we need to parse the page — but this is uncommon.
|
|
50
|
+
// Fall back to a HEAD request on the resolved URL.
|
|
51
|
+
if (res.statusCode === 200 && res.headers.location) {
|
|
52
|
+
const match = res.headers.location.match(/\/releases\/tag\/(.+)$/);
|
|
53
|
+
if (match)
|
|
54
|
+
return resolve(match[1]);
|
|
55
|
+
}
|
|
56
|
+
reject(new Error(`Failed to resolve latest release (HTTP ${res.statusCode})`));
|
|
40
57
|
});
|
|
41
58
|
req.setTimeout(5000, () => {
|
|
42
|
-
req.destroy(new Error(
|
|
59
|
+
req.destroy(new Error("Request timed out resolving latest release"));
|
|
43
60
|
});
|
|
44
61
|
req.on("error", reject);
|
|
45
62
|
});
|
|
46
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Verify that a specific release tag exists.
|
|
66
|
+
*/
|
|
67
|
+
function verifyTagExists(tag) {
|
|
68
|
+
const url = `${WEB_BASE}/releases/tag/${tag}`;
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const req = node_https_1.default.request(url, {
|
|
71
|
+
method: "HEAD",
|
|
72
|
+
headers: { "User-Agent": "kandev-npx", ...authHeaders() },
|
|
73
|
+
}, (res) => {
|
|
74
|
+
res.resume();
|
|
75
|
+
// GitHub returns 200 for the tag page, or 302 redirect to the tag page.
|
|
76
|
+
if (res.statusCode === 200 || res.statusCode === 301 || res.statusCode === 302) {
|
|
77
|
+
return resolve();
|
|
78
|
+
}
|
|
79
|
+
reject(new Error(`Release tag '${tag}' not found (HTTP ${res.statusCode})`));
|
|
80
|
+
});
|
|
81
|
+
req.setTimeout(5000, () => {
|
|
82
|
+
req.destroy(new Error(`Request timed out verifying tag '${tag}'`));
|
|
83
|
+
});
|
|
84
|
+
req.on("error", reject);
|
|
85
|
+
req.end();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get release info. Uses github.com web URLs (not api.github.com)
|
|
90
|
+
* to avoid REST API rate limits.
|
|
91
|
+
*/
|
|
92
|
+
async function getRelease(version) {
|
|
93
|
+
if (version) {
|
|
94
|
+
await verifyTagExists(version);
|
|
95
|
+
return { tag_name: version };
|
|
96
|
+
}
|
|
97
|
+
const tag = await resolveLatestTag();
|
|
98
|
+
return { tag_name: tag };
|
|
99
|
+
}
|
|
100
|
+
// -- Asset downloading --------------------------------------------------------
|
|
101
|
+
function readSha256(pathToSha) {
|
|
102
|
+
if (!node_fs_1.default.existsSync(pathToSha)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const content = node_fs_1.default.readFileSync(pathToSha, "utf8").trim();
|
|
106
|
+
const first = content.split(/\s+/)[0];
|
|
107
|
+
return first?.toLowerCase() || null;
|
|
108
|
+
}
|
|
47
109
|
function downloadFile(url, destPath, expectedSha256, onProgress) {
|
|
48
110
|
const tempPath = `${destPath}.tmp`;
|
|
49
111
|
return new Promise((resolve, reject) => {
|
|
@@ -56,7 +118,7 @@ function downloadFile(url, destPath, expectedSha256, onProgress) {
|
|
|
56
118
|
catch { }
|
|
57
119
|
};
|
|
58
120
|
const handleResponse = (res) => {
|
|
59
|
-
// Follow redirects (GitHub
|
|
121
|
+
// Follow redirects (GitHub returns 302 to signed S3/CDN URL).
|
|
60
122
|
// Strip auth header on redirect to avoid S3 rejecting it.
|
|
61
123
|
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
|
|
62
124
|
const redirectReq = node_https_1.default.get(res.headers.location, { headers: { "User-Agent": "kandev-npx" } }, handleResponse);
|
|
@@ -104,10 +166,7 @@ function downloadFile(url, destPath, expectedSha256, onProgress) {
|
|
|
104
166
|
const req = node_https_1.default.get(url, {
|
|
105
167
|
headers: {
|
|
106
168
|
"User-Agent": "kandev-npx",
|
|
107
|
-
|
|
108
|
-
...(process.env.KANDEV_GITHUB_TOKEN
|
|
109
|
-
? { Authorization: `Bearer ${process.env.KANDEV_GITHUB_TOKEN}` }
|
|
110
|
-
: {}),
|
|
169
|
+
...authHeaders(),
|
|
111
170
|
},
|
|
112
171
|
}, handleResponse);
|
|
113
172
|
req.setTimeout(30000, () => {
|
|
@@ -120,39 +179,29 @@ function downloadFile(url, destPath, expectedSha256, onProgress) {
|
|
|
120
179
|
});
|
|
121
180
|
});
|
|
122
181
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const content = node_fs_1.default.readFileSync(pathToSha, "utf8").trim();
|
|
131
|
-
const first = content.split(/\s+/)[0];
|
|
132
|
-
return first?.toLowerCase() || null;
|
|
133
|
-
}
|
|
134
|
-
async function getRelease(version) {
|
|
135
|
-
if (version) {
|
|
136
|
-
return requestJson(`${API_BASE}/releases/tags/${version}`);
|
|
137
|
-
}
|
|
138
|
-
return requestJson(`${API_BASE}/releases/latest`);
|
|
139
|
-
}
|
|
140
|
-
async function ensureAsset(release, assetName, cacheDir, onProgress) {
|
|
141
|
-
const asset = findAsset(release, assetName);
|
|
142
|
-
if (!asset) {
|
|
143
|
-
throw new Error(`Release asset not found: ${assetName}`);
|
|
144
|
-
}
|
|
182
|
+
/**
|
|
183
|
+
* Ensure a release asset is downloaded and cached.
|
|
184
|
+
*
|
|
185
|
+
* Downloads directly from github.com/{owner}/{repo}/releases/download/{tag}/{asset}
|
|
186
|
+
* instead of the API, avoiding rate limits.
|
|
187
|
+
*/
|
|
188
|
+
async function ensureAsset(tag, assetName, cacheDir, onProgress) {
|
|
145
189
|
node_fs_1.default.mkdirSync(cacheDir, { recursive: true });
|
|
146
190
|
const destPath = node_path_1.default.join(cacheDir, assetName);
|
|
147
191
|
const shaPath = `${destPath}.sha256`;
|
|
192
|
+
// Download sha256 checksum if not already cached.
|
|
148
193
|
let expectedSha = readSha256(shaPath);
|
|
149
194
|
if (!expectedSha) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
await downloadFile(
|
|
195
|
+
const shaUrl = `${WEB_BASE}/releases/download/${tag}/${assetName}.sha256`;
|
|
196
|
+
try {
|
|
197
|
+
await downloadFile(shaUrl, shaPath);
|
|
153
198
|
expectedSha = readSha256(shaPath);
|
|
154
199
|
}
|
|
200
|
+
catch {
|
|
201
|
+
// sha256 file may not exist for this release — continue without it.
|
|
202
|
+
}
|
|
155
203
|
}
|
|
204
|
+
// Return cached tarball if it exists and checksum matches.
|
|
156
205
|
if (node_fs_1.default.existsSync(destPath)) {
|
|
157
206
|
if (!expectedSha) {
|
|
158
207
|
return destPath;
|
|
@@ -165,6 +214,8 @@ async function ensureAsset(release, assetName, cacheDir, onProgress) {
|
|
|
165
214
|
}
|
|
166
215
|
node_fs_1.default.unlinkSync(destPath);
|
|
167
216
|
}
|
|
168
|
-
|
|
217
|
+
// Download the asset.
|
|
218
|
+
const assetUrl = `${WEB_BASE}/releases/download/${tag}/${assetName}`;
|
|
219
|
+
await downloadFile(assetUrl, destPath, expectedSha, onProgress);
|
|
169
220
|
return destPath;
|
|
170
221
|
}
|
package/dist/health.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.delay = delay;
|
|
4
4
|
exports.resolveHealthTimeoutMs = resolveHealthTimeoutMs;
|
|
5
5
|
exports.waitForHealth = waitForHealth;
|
|
6
|
+
exports.waitForUrlReady = waitForUrlReady;
|
|
6
7
|
function delay(ms) {
|
|
7
8
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
9
|
}
|
|
@@ -47,3 +48,20 @@ async function waitForHealth(baseUrl, proc, timeoutMs) {
|
|
|
47
48
|
}
|
|
48
49
|
throw new Error(`Backend healthcheck timed out after ${timeoutMs}ms`);
|
|
49
50
|
}
|
|
51
|
+
async function waitForUrlReady(url, proc, timeoutMs) {
|
|
52
|
+
const deadline = Date.now() + timeoutMs;
|
|
53
|
+
while (Date.now() < deadline) {
|
|
54
|
+
if (proc.exitCode !== null) {
|
|
55
|
+
throw new Error("Web process exited before URL became reachable");
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await fetch(url);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// ignore until timeout
|
|
63
|
+
}
|
|
64
|
+
await delay(300);
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Web URL readiness timed out after ${timeoutMs}ms (${url})`);
|
|
67
|
+
}
|
package/dist/run.js
CHANGED
|
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.findCachedRelease = findCachedRelease;
|
|
7
|
+
exports.cleanOldReleases = cleanOldReleases;
|
|
6
8
|
exports.runRelease = runRelease;
|
|
7
9
|
const node_child_process_1 = require("node:child_process");
|
|
8
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -10,27 +12,103 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
10
12
|
const bundle_1 = require("./bundle");
|
|
11
13
|
const constants_1 = require("./constants");
|
|
12
14
|
const github_1 = require("./github");
|
|
13
|
-
const constants_2 = require("./constants");
|
|
14
15
|
const health_1 = require("./health");
|
|
15
16
|
const platform_1 = require("./platform");
|
|
17
|
+
const version_1 = require("./version");
|
|
16
18
|
const ports_1 = require("./ports");
|
|
17
19
|
const process_1 = require("./process");
|
|
18
20
|
const shared_1 = require("./shared");
|
|
19
21
|
const web_1 = require("./web");
|
|
22
|
+
/**
|
|
23
|
+
* Find a cached release binary to use when GitHub is unreachable.
|
|
24
|
+
* If version is specified, checks that exact tag. Otherwise, picks
|
|
25
|
+
* the highest semver tag available in the cache.
|
|
26
|
+
*/
|
|
27
|
+
function findCachedRelease(platformDir, version) {
|
|
28
|
+
if (version) {
|
|
29
|
+
const cacheDir = node_path_1.default.join(constants_1.CACHE_DIR, version, platformDir);
|
|
30
|
+
const bundleDir = node_path_1.default.join(cacheDir, "kandev");
|
|
31
|
+
const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
|
|
32
|
+
if (node_fs_1.default.existsSync(backendBin)) {
|
|
33
|
+
return { cacheDir, tag: version };
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
// No version specified — scan for cached tags and pick the latest.
|
|
38
|
+
if (!node_fs_1.default.existsSync(constants_1.CACHE_DIR))
|
|
39
|
+
return null;
|
|
40
|
+
const entries = node_fs_1.default.readdirSync(constants_1.CACHE_DIR).filter((d) => d.startsWith("v"));
|
|
41
|
+
if (entries.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
const sorted = (0, version_1.sortVersionsDesc)(entries);
|
|
44
|
+
for (const tag of sorted) {
|
|
45
|
+
const cacheDir = node_path_1.default.join(constants_1.CACHE_DIR, tag, platformDir);
|
|
46
|
+
const bundleDir = node_path_1.default.join(cacheDir, "kandev");
|
|
47
|
+
const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
|
|
48
|
+
if (node_fs_1.default.existsSync(backendBin)) {
|
|
49
|
+
return { cacheDir, tag };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Remove old cached releases, keeping only the 2 most recent tags.
|
|
56
|
+
* Runs after a successful download so we don't accumulate stale versions.
|
|
57
|
+
* The previous version is kept as a fallback for offline use.
|
|
58
|
+
*/
|
|
59
|
+
function cleanOldReleases(currentTag) {
|
|
60
|
+
try {
|
|
61
|
+
if (!node_fs_1.default.existsSync(constants_1.CACHE_DIR))
|
|
62
|
+
return;
|
|
63
|
+
const entries = node_fs_1.default.readdirSync(constants_1.CACHE_DIR).filter((d) => d.startsWith("v"));
|
|
64
|
+
if (entries.length <= 2)
|
|
65
|
+
return;
|
|
66
|
+
const sorted = (0, version_1.sortVersionsDesc)(entries);
|
|
67
|
+
// Always keep currentTag + the next most recent.
|
|
68
|
+
const keep = new Set([currentTag, sorted[0], sorted[1]]);
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (!keep.has(entry)) {
|
|
71
|
+
node_fs_1.default.rmSync(node_path_1.default.join(constants_1.CACHE_DIR, entry), { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Non-critical — don't fail the launch if cleanup errors.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
20
79
|
async function prepareReleaseBundle({ version, backendPort, webPort, }) {
|
|
21
80
|
const platformDir = (0, platform_1.getPlatformDir)();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
81
|
+
let tag;
|
|
82
|
+
let cacheDir;
|
|
83
|
+
try {
|
|
84
|
+
const release = await (0, github_1.getRelease)(version);
|
|
85
|
+
tag = release.tag_name;
|
|
86
|
+
const assetName = `kandev-${platformDir}.tar.gz`;
|
|
87
|
+
cacheDir = node_path_1.default.join(constants_1.CACHE_DIR, tag, platformDir);
|
|
88
|
+
const archivePath = await (0, github_1.ensureAsset)(tag, assetName, cacheDir, (downloaded, total) => {
|
|
89
|
+
const percent = total ? Math.round((downloaded / total) * 100) : 0;
|
|
90
|
+
const mb = (downloaded / (1024 * 1024)).toFixed(1);
|
|
91
|
+
const totalMb = total ? (total / (1024 * 1024)).toFixed(1) : "?";
|
|
92
|
+
process.stderr.write(`\r Downloading: ${mb}MB / ${totalMb}MB (${percent}%)`);
|
|
93
|
+
});
|
|
94
|
+
process.stderr.write("\n");
|
|
95
|
+
(0, bundle_1.ensureExtracted)(archivePath, cacheDir);
|
|
96
|
+
cleanOldReleases(tag);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// GitHub unreachable — try to launch from cache.
|
|
100
|
+
const cached = findCachedRelease(platformDir, version);
|
|
101
|
+
if (!cached) {
|
|
102
|
+
const target = version ? `version ${version}` : "latest version";
|
|
103
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
104
|
+
throw new Error(`Failed to fetch ${target} and no cached release found.\n` +
|
|
105
|
+
` Reason: ${reason}\n` +
|
|
106
|
+
` Run kandev once while online to cache a release for offline use.`);
|
|
107
|
+
}
|
|
108
|
+
tag = cached.tag;
|
|
109
|
+
cacheDir = cached.cacheDir;
|
|
110
|
+
process.stderr.write(`[kandev] GitHub unreachable — using cached release ${tag}\n`);
|
|
111
|
+
}
|
|
34
112
|
const bundleDir = (0, bundle_1.findBundleRoot)(cacheDir);
|
|
35
113
|
const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
|
|
36
114
|
if (!node_fs_1.default.existsSync(backendBin)) {
|
|
@@ -109,24 +187,27 @@ function launchReleaseApps(prepared) {
|
|
|
109
187
|
if (!webServerPath) {
|
|
110
188
|
throw new Error("Web server entry (server.js) not found in bundle");
|
|
111
189
|
}
|
|
190
|
+
return { supervisor, backendProc, webServerPath };
|
|
191
|
+
}
|
|
192
|
+
async function runRelease({ version, backendPort, webPort }) {
|
|
193
|
+
const prepared = await prepareReleaseBundle({ version, backendPort, webPort });
|
|
194
|
+
const { supervisor, backendProc, webServerPath } = launchReleaseApps(prepared);
|
|
195
|
+
const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_RELEASE);
|
|
196
|
+
console.log("[kandev] starting backend...");
|
|
197
|
+
await (0, health_1.waitForHealth)(prepared.backendUrl, backendProc, healthTimeoutMs);
|
|
198
|
+
console.log(`[kandev] backend ready at ${prepared.backendUrl}`);
|
|
112
199
|
const webUrl = `http://localhost:${prepared.webPort}`;
|
|
113
|
-
(
|
|
200
|
+
console.log("[kandev] starting web...");
|
|
201
|
+
const webProc = (0, web_1.launchWebApp)({
|
|
114
202
|
command: "node",
|
|
115
203
|
args: [webServerPath],
|
|
116
204
|
cwd: node_path_1.default.dirname(webServerPath),
|
|
117
205
|
env: prepared.webEnv,
|
|
118
|
-
url: webUrl,
|
|
119
206
|
supervisor,
|
|
120
207
|
label: "web",
|
|
208
|
+
quiet: true,
|
|
121
209
|
});
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const prepared = await prepareReleaseBundle({ version, backendPort, webPort });
|
|
126
|
-
const { backendProc } = launchReleaseApps(prepared);
|
|
127
|
-
// Wait for backend before announcing the web URL.
|
|
128
|
-
const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_RELEASE);
|
|
129
|
-
await (0, health_1.waitForHealth)(prepared.backendUrl, backendProc, healthTimeoutMs);
|
|
130
|
-
console.log(`[kandev] backend ready at ${prepared.backendUrl}`);
|
|
131
|
-
console.log(`[kandev] web ready at http://localhost:${prepared.webPort}`);
|
|
210
|
+
await (0, health_1.waitForUrlReady)(webUrl, webProc, healthTimeoutMs);
|
|
211
|
+
console.log(`[kandev] web ready at ${webUrl}`);
|
|
212
|
+
(0, web_1.openBrowser)(webUrl);
|
|
132
213
|
}
|
package/dist/start.js
CHANGED
|
@@ -107,19 +107,22 @@ async function runStart({ repoRoot, backendPort, webPort, verbose = false, debug
|
|
|
107
107
|
}
|
|
108
108
|
(0, shared_1.attachBackendExitHandler)(backendProc, supervisor);
|
|
109
109
|
const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_RELEASE);
|
|
110
|
+
console.log("[kandev] starting backend...");
|
|
110
111
|
await (0, health_1.waitForHealth)(ports.backendUrl, backendProc, healthTimeoutMs);
|
|
112
|
+
console.log(`[kandev] backend ready at ${ports.backendUrl}`);
|
|
111
113
|
// Use standalone server.js directly (not pnpm start)
|
|
112
114
|
const webUrl = `http://localhost:${ports.webPort}`;
|
|
113
|
-
(
|
|
115
|
+
console.log("[kandev] starting web...");
|
|
116
|
+
const webProc = (0, web_1.launchWebApp)({
|
|
114
117
|
command: "node",
|
|
115
118
|
args: [webServerPath],
|
|
116
119
|
cwd: webStandaloneDir,
|
|
117
120
|
env: webEnv,
|
|
118
|
-
url: webUrl,
|
|
119
121
|
supervisor,
|
|
120
122
|
label: "web",
|
|
121
123
|
quiet: !showOutput,
|
|
122
124
|
});
|
|
123
|
-
|
|
125
|
+
await (0, health_1.waitForUrlReady)(webUrl, webProc, healthTimeoutMs);
|
|
124
126
|
console.log(`[kandev] web ready at ${webUrl}`);
|
|
127
|
+
(0, web_1.openBrowser)(webUrl);
|
|
125
128
|
}
|
package/dist/update.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.maybePromptForUpdate = maybePromptForUpdate;
|
|
|
7
7
|
const node_child_process_1 = require("node:child_process");
|
|
8
8
|
const node_https_1 = __importDefault(require("node:https"));
|
|
9
9
|
const node_readline_1 = __importDefault(require("node:readline"));
|
|
10
|
+
const version_1 = require("./version");
|
|
10
11
|
function requestJson(url) {
|
|
11
12
|
return new Promise((resolve, reject) => {
|
|
12
13
|
const req = node_https_1.default.get(url, { headers: { "User-Agent": "kandev-npx" } }, (res) => {
|
|
@@ -34,19 +35,6 @@ async function getLatestNpmVersion() {
|
|
|
34
35
|
const data = await requestJson("https://registry.npmjs.org/kandev");
|
|
35
36
|
return data?.["dist-tags"]?.latest;
|
|
36
37
|
}
|
|
37
|
-
function compareVersions(a, b) {
|
|
38
|
-
const pa = String(a).replace(/^v/, "").split(".").map(Number);
|
|
39
|
-
const pb = String(b).replace(/^v/, "").split(".").map(Number);
|
|
40
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
|
|
41
|
-
const av = pa[i] ?? 0;
|
|
42
|
-
const bv = pb[i] ?? 0;
|
|
43
|
-
if (av > bv)
|
|
44
|
-
return 1;
|
|
45
|
-
if (av < bv)
|
|
46
|
-
return -1;
|
|
47
|
-
}
|
|
48
|
-
return 0;
|
|
49
|
-
}
|
|
50
38
|
function promptYesNo(question, defaultYes = false) {
|
|
51
39
|
return new Promise((resolve) => {
|
|
52
40
|
if (!process.stdin.isTTY) {
|
|
@@ -80,7 +68,7 @@ async function maybePromptForUpdate(currentVersion, args) {
|
|
|
80
68
|
const latest = await getLatestNpmVersion();
|
|
81
69
|
if (!latest)
|
|
82
70
|
return;
|
|
83
|
-
if (compareVersions(latest, currentVersion) <= 0)
|
|
71
|
+
if ((0, version_1.compareVersions)(latest, currentVersion) <= 0)
|
|
84
72
|
return;
|
|
85
73
|
const wantsUpdate = await promptYesNo(`Update available: ${currentVersion} -> ${latest}. Update now?`, false);
|
|
86
74
|
if (!wantsUpdate)
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Centralized version parsing and comparison for the CLI.
|
|
4
|
+
*
|
|
5
|
+
* Version format: optional "v" prefix + dot-separated numeric segments.
|
|
6
|
+
* Examples: "v1.2", "0.3.0", "v2.0.1", "1"
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.parseVersion = parseVersion;
|
|
10
|
+
exports.compareVersions = compareVersions;
|
|
11
|
+
exports.sortVersionsDesc = sortVersionsDesc;
|
|
12
|
+
/**
|
|
13
|
+
* Parse a version string into numeric segments.
|
|
14
|
+
* Strips leading "v" prefix if present.
|
|
15
|
+
*
|
|
16
|
+
* "v1.2.3" -> { raw: "v1.2.3", segments: [1, 2, 3] }
|
|
17
|
+
* "0.3" -> { raw: "0.3", segments: [0, 3] }
|
|
18
|
+
* "bad" -> { raw: "bad", segments: [] }
|
|
19
|
+
*/
|
|
20
|
+
function parseVersion(version) {
|
|
21
|
+
const cleaned = String(version).replace(/^v/, "");
|
|
22
|
+
const parts = cleaned.split(".");
|
|
23
|
+
const segments = [];
|
|
24
|
+
for (const part of parts) {
|
|
25
|
+
const n = parseInt(part, 10);
|
|
26
|
+
if (Number.isNaN(n))
|
|
27
|
+
return { raw: version, segments: [] };
|
|
28
|
+
segments.push(n);
|
|
29
|
+
}
|
|
30
|
+
return { raw: version, segments };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compare two version strings.
|
|
34
|
+
* Returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
35
|
+
* Missing segments are treated as 0 (e.g. "1.0" == "1.0.0").
|
|
36
|
+
*/
|
|
37
|
+
function compareVersions(a, b) {
|
|
38
|
+
const pa = parseVersion(a).segments;
|
|
39
|
+
const pb = parseVersion(b).segments;
|
|
40
|
+
const len = Math.max(pa.length, pb.length);
|
|
41
|
+
for (let i = 0; i < len; i++) {
|
|
42
|
+
const av = pa[i] ?? 0;
|
|
43
|
+
const bv = pb[i] ?? 0;
|
|
44
|
+
if (av > bv)
|
|
45
|
+
return 1;
|
|
46
|
+
if (av < bv)
|
|
47
|
+
return -1;
|
|
48
|
+
}
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Sort version strings in descending order (newest first).
|
|
53
|
+
* Non-parseable strings sort to the end.
|
|
54
|
+
*/
|
|
55
|
+
function sortVersionsDesc(versions) {
|
|
56
|
+
return [...versions].sort((a, b) => compareVersions(b, a));
|
|
57
|
+
}
|
package/dist/web.js
CHANGED
|
@@ -32,7 +32,7 @@ function openBrowser(url) {
|
|
|
32
32
|
// ignore browser launch errors
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
function launchWebApp({ command, args, cwd, env,
|
|
35
|
+
function launchWebApp({ command, args, cwd, env, supervisor, label, quiet = false, }) {
|
|
36
36
|
const stdio = quiet ? ["ignore", "pipe", "pipe"] : "inherit";
|
|
37
37
|
const proc = (0, node_child_process_1.spawn)(command, args, { cwd, env, stdio });
|
|
38
38
|
supervisor.children.push(proc);
|
|
@@ -45,6 +45,5 @@ function launchWebApp({ command, args, cwd, env, url, supervisor, label, quiet =
|
|
|
45
45
|
const exitCode = signal ? 0 : (code ?? 1);
|
|
46
46
|
void supervisor.shutdown(`${label} exit`).then(() => process.exit(exitCode));
|
|
47
47
|
});
|
|
48
|
-
openBrowser(url);
|
|
49
48
|
return proc;
|
|
50
49
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kandev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "NPX launcher for Kandev",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -20,12 +20,14 @@
|
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^20",
|
|
22
22
|
"tsx": "^4.15.7",
|
|
23
|
-
"typescript": "^5"
|
|
23
|
+
"typescript": "^5",
|
|
24
|
+
"vitest": "^1.6.0"
|
|
24
25
|
},
|
|
25
26
|
"scripts": {
|
|
26
27
|
"dev": "tsx src/cli.ts",
|
|
27
28
|
"build": "tsc -p tsconfig.json",
|
|
28
29
|
"start": "node dist/cli.js",
|
|
30
|
+
"test": "vitest run",
|
|
29
31
|
"prepublishOnly": "pnpm build"
|
|
30
32
|
}
|
|
31
33
|
}
|