kandev 0.1.3
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/README.md +77 -0
- package/bin/cli.js +3 -0
- package/dist/bundle.js +42 -0
- package/dist/cli.js +126 -0
- package/dist/constants.js +21 -0
- package/dist/dev.js +57 -0
- package/dist/github.js +155 -0
- package/dist/health.js +27 -0
- package/dist/platform.js +50 -0
- package/dist/ports.js +76 -0
- package/dist/process.js +28 -0
- package/dist/run.js +110 -0
- package/dist/update.js +97 -0
- package/dist/web.js +36 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Kandev Launcher (npx)
|
|
2
|
+
|
|
3
|
+
This package powers `npx kandev` by downloading prebuilt release bundles from GitHub Releases and running them locally. It:
|
|
4
|
+
- Detects OS/arch and fetches the matching bundle ZIP.
|
|
5
|
+
- Verifies the SHA256 checksum when available.
|
|
6
|
+
- Extracts the bundle into `~/.kandev/bin/<version>/<platform>/`.
|
|
7
|
+
- Starts the backend binary and waits for the `/health` endpoint.
|
|
8
|
+
- Starts the Next.js standalone server with runtime `KANDEV_API_BASE_URL`.
|
|
9
|
+
- Uses the latest GitHub Release by default, so runtime bundles update automatically.
|
|
10
|
+
It also supports local dev runs from a repo checkout.
|
|
11
|
+
|
|
12
|
+
## Updates
|
|
13
|
+
|
|
14
|
+
On `run`, the launcher checks npm for the latest `kandev` CLI version and prompts:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Update available: <current> -> <latest>. Update now? [y/N]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If you accept, it re-runs `npx kandev@latest` with the same arguments.
|
|
21
|
+
This check is skipped in `dev` mode.
|
|
22
|
+
|
|
23
|
+
Note: the runtime bundles are pulled from the latest GitHub Release by default, even if the CLI version is unchanged. So:
|
|
24
|
+
- **New runtime release without CLI publish**: users get new runtime automatically, but no update prompt.
|
|
25
|
+
- **New CLI publish**: users get an update prompt and then re-run with the new CLI.
|
|
26
|
+
|
|
27
|
+
You can disable the prompt with:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
KANDEV_NO_UPDATE_PROMPT=1 npx kandev
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Run the latest release bundle
|
|
37
|
+
npx kandev
|
|
38
|
+
|
|
39
|
+
# Run a specific release tag
|
|
40
|
+
npx kandev run --version v0.1.0
|
|
41
|
+
|
|
42
|
+
# Local dev (from repo root)
|
|
43
|
+
npx kandev dev
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Local test the built CLI (from repo root)
|
|
47
|
+
pnpm -C apps/cli build
|
|
48
|
+
pnpm -C apps/cli start
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Local Development
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm -C apps/cli dev
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Build / Publish
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pnpm -C apps/cli build
|
|
61
|
+
npm publish --access public
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The published package name is `kandev`, with a bin entry `kandev`.
|
|
65
|
+
|
|
66
|
+
## Release
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
scripts/release/publish-launcher.sh 0.1.0
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Environment Overrides
|
|
73
|
+
|
|
74
|
+
- `KANDEV_GITHUB_OWNER`, `KANDEV_GITHUB_REPO`: Override the GitHub repo to fetch releases from.
|
|
75
|
+
- `KANDEV_GITHUB_TOKEN`: Optional token for GitHub API rate limits.
|
|
76
|
+
- `KANDEV_NO_UPDATE_PROMPT=1`: Disable the update prompt.
|
|
77
|
+
- `KANDEV_SKIP_UPDATE=1`: Internal guard to avoid update loops.
|
package/bin/cli.js
ADDED
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.extractZip = extractZip;
|
|
7
|
+
exports.ensureExtracted = ensureExtracted;
|
|
8
|
+
exports.findBundleRoot = findBundleRoot;
|
|
9
|
+
exports.resolveWebServerPath = resolveWebServerPath;
|
|
10
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
function extractZip(zipPath, destDir) {
|
|
14
|
+
const zip = new adm_zip_1.default(zipPath);
|
|
15
|
+
zip.extractAllTo(destDir, true);
|
|
16
|
+
}
|
|
17
|
+
function ensureExtracted(zipPath, destDir) {
|
|
18
|
+
const marker = node_path_1.default.join(destDir, ".extracted");
|
|
19
|
+
if (node_fs_1.default.existsSync(marker)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
extractZip(zipPath, destDir);
|
|
23
|
+
node_fs_1.default.writeFileSync(marker, "");
|
|
24
|
+
}
|
|
25
|
+
function findBundleRoot(cacheDir) {
|
|
26
|
+
const candidate = node_path_1.default.join(cacheDir, "kandev");
|
|
27
|
+
if (node_fs_1.default.existsSync(candidate)) {
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
return cacheDir;
|
|
31
|
+
}
|
|
32
|
+
function resolveWebServerPath(bundleDir) {
|
|
33
|
+
const direct = node_path_1.default.join(bundleDir, "web", "server.js");
|
|
34
|
+
if (node_fs_1.default.existsSync(direct)) {
|
|
35
|
+
return direct;
|
|
36
|
+
}
|
|
37
|
+
const nested = node_path_1.default.join(bundleDir, "web", "apps", "web", "server.js");
|
|
38
|
+
if (node_fs_1.default.existsSync(nested)) {
|
|
39
|
+
return nested;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
9
|
+
const dev_1 = require("./dev");
|
|
10
|
+
const run_1 = require("./run");
|
|
11
|
+
const ports_1 = require("./ports");
|
|
12
|
+
const update_1 = require("./update");
|
|
13
|
+
function printHelp() {
|
|
14
|
+
console.log(`kandev launcher
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
kandev run [--version <tag>] [--backend-port <port>] [--web-port <port>]
|
|
18
|
+
kandev dev [--backend-port <port>] [--web-port <port>]
|
|
19
|
+
kandev [--version <tag>] [--backend-port <port>] [--web-port <port>]
|
|
20
|
+
kandev --dev [--backend-port <port>] [--web-port <port>]
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
kandev
|
|
24
|
+
kandev run
|
|
25
|
+
kandev --dev
|
|
26
|
+
kandev dev
|
|
27
|
+
kandev --version v0.1.0
|
|
28
|
+
kandev --backend-port 18080 --web-port 13000
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
dev Use local repo for dev (make dev + next dev) if available.
|
|
32
|
+
run Use release bundles (default).
|
|
33
|
+
--dev Alias for "dev".
|
|
34
|
+
--version Release tag to install (default: latest).
|
|
35
|
+
--backend-port Override backend port.
|
|
36
|
+
--web-port Override web port.
|
|
37
|
+
--help, -h Show help.
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const opts = { command: "run" };
|
|
42
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
43
|
+
const arg = argv[i];
|
|
44
|
+
if (arg === "--help" || arg === "-h") {
|
|
45
|
+
printHelp();
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
if (arg === "dev" || arg === "run") {
|
|
49
|
+
opts.command = arg;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (arg === "--version") {
|
|
53
|
+
opts.version = argv[i + 1];
|
|
54
|
+
i += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (arg.startsWith("--version=")) {
|
|
58
|
+
opts.version = arg.split("=")[1];
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (arg === "--dev") {
|
|
62
|
+
opts.command = "dev";
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (arg === "--backend-port") {
|
|
66
|
+
opts.backendPort = Number(argv[i + 1]);
|
|
67
|
+
i += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg.startsWith("--backend-port=")) {
|
|
71
|
+
opts.backendPort = Number(arg.split("=")[1]);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (arg === "--web-port") {
|
|
75
|
+
opts.webPort = Number(argv[i + 1]);
|
|
76
|
+
i += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (arg.startsWith("--web-port=")) {
|
|
80
|
+
opts.webPort = Number(arg.split("=")[1]);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return opts;
|
|
85
|
+
}
|
|
86
|
+
function findRepoRoot(startDir) {
|
|
87
|
+
let current = node_path_1.default.resolve(startDir);
|
|
88
|
+
while (true) {
|
|
89
|
+
if (current.endsWith(`${node_path_1.default.sep}apps`)) {
|
|
90
|
+
const backendInApps = node_path_1.default.join(current, "backend");
|
|
91
|
+
const webInApps = node_path_1.default.join(current, "web");
|
|
92
|
+
if (node_fs_1.default.existsSync(backendInApps) && node_fs_1.default.existsSync(webInApps)) {
|
|
93
|
+
return node_path_1.default.dirname(current);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const backendDir = node_path_1.default.join(current, "apps", "backend");
|
|
97
|
+
const webDir = node_path_1.default.join(current, "apps", "web");
|
|
98
|
+
if (node_fs_1.default.existsSync(backendDir) && node_fs_1.default.existsSync(webDir)) {
|
|
99
|
+
return current;
|
|
100
|
+
}
|
|
101
|
+
const parent = node_path_1.default.dirname(current);
|
|
102
|
+
if (parent === current) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
current = parent;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function main() {
|
|
109
|
+
const raw = parseArgs(process.argv.slice(2));
|
|
110
|
+
const backendPort = (0, ports_1.ensureValidPort)(raw.backendPort, "backend port");
|
|
111
|
+
const webPort = (0, ports_1.ensureValidPort)(raw.webPort, "web port");
|
|
112
|
+
if (raw.command === "dev") {
|
|
113
|
+
const repoRoot = findRepoRoot(process.cwd());
|
|
114
|
+
if (!repoRoot) {
|
|
115
|
+
throw new Error("Unable to locate repo root for dev. Run from the repo.");
|
|
116
|
+
}
|
|
117
|
+
await (0, dev_1.runDev)({ repoRoot, backendPort, webPort });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await (0, update_1.maybePromptForUpdate)(package_json_1.default.version, process.argv.slice(2));
|
|
121
|
+
await (0, run_1.runRelease)({ version: raw.version, backendPort, webPort });
|
|
122
|
+
}
|
|
123
|
+
main().catch((err) => {
|
|
124
|
+
console.error(`[kandev] ${err instanceof Error ? err.message : String(err)}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DATA_DIR = exports.CACHE_DIR = exports.HEALTH_TIMEOUT_MS = exports.RANDOM_PORT_RETRIES = exports.RANDOM_PORT_MAX = exports.RANDOM_PORT_MIN = exports.DEFAULT_AGENTCTL_PORT = exports.DEFAULT_WEB_PORT = exports.DEFAULT_BACKEND_PORT = void 0;
|
|
7
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
// Default service ports (will auto-fallback if busy).
|
|
10
|
+
exports.DEFAULT_BACKEND_PORT = 8080;
|
|
11
|
+
exports.DEFAULT_WEB_PORT = 3000;
|
|
12
|
+
exports.DEFAULT_AGENTCTL_PORT = 9999;
|
|
13
|
+
// Random fallback range for port selection.
|
|
14
|
+
exports.RANDOM_PORT_MIN = 10000;
|
|
15
|
+
exports.RANDOM_PORT_MAX = 60000;
|
|
16
|
+
exports.RANDOM_PORT_RETRIES = 10;
|
|
17
|
+
// Backend healthcheck timeout during startup.
|
|
18
|
+
exports.HEALTH_TIMEOUT_MS = 15000;
|
|
19
|
+
// Local user cache/data directories for release bundles and DB.
|
|
20
|
+
exports.CACHE_DIR = node_path_1.default.join(node_os_1.default.homedir(), ".kandev", "bin");
|
|
21
|
+
exports.DATA_DIR = node_path_1.default.join(node_os_1.default.homedir(), ".kandev", "data");
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runDev = runDev;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const constants_1 = require("./constants");
|
|
10
|
+
const health_1 = require("./health");
|
|
11
|
+
const ports_1 = require("./ports");
|
|
12
|
+
const process_1 = require("./process");
|
|
13
|
+
const web_1 = require("./web");
|
|
14
|
+
async function runDev({ repoRoot, backendPort, webPort }) {
|
|
15
|
+
const actualBackendPort = backendPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_BACKEND_PORT));
|
|
16
|
+
const actualWebPort = webPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_WEB_PORT));
|
|
17
|
+
const agentctlPort = await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_AGENTCTL_PORT);
|
|
18
|
+
const backendUrl = `http://localhost:${actualBackendPort}`;
|
|
19
|
+
const backendEnv = {
|
|
20
|
+
...process.env,
|
|
21
|
+
KANDEV_SERVER_PORT: String(actualBackendPort),
|
|
22
|
+
KANDEV_AGENT_STANDALONE_PORT: String(agentctlPort),
|
|
23
|
+
};
|
|
24
|
+
const webEnv = {
|
|
25
|
+
...process.env,
|
|
26
|
+
KANDEV_API_BASE_URL: backendUrl,
|
|
27
|
+
NEXT_PUBLIC_KANDEV_API_BASE_URL: backendUrl,
|
|
28
|
+
PORT: String(actualWebPort),
|
|
29
|
+
NEXT_PUBLIC_KANDEV_DEBUG: "true",
|
|
30
|
+
};
|
|
31
|
+
console.log("[kandev] dev mode: using local repo");
|
|
32
|
+
console.log("[kandev] backend port:", actualBackendPort);
|
|
33
|
+
console.log("[kandev] web port:", actualWebPort);
|
|
34
|
+
console.log("[kandev] agentctl port:", agentctlPort);
|
|
35
|
+
const supervisor = (0, process_1.createProcessSupervisor)();
|
|
36
|
+
supervisor.attachSignalHandlers();
|
|
37
|
+
const backendProc = (0, node_child_process_1.spawn)("make", ["-C", node_path_1.default.join("apps", "backend"), "dev"], { cwd: repoRoot, env: backendEnv, stdio: "inherit" });
|
|
38
|
+
supervisor.children.push(backendProc);
|
|
39
|
+
backendProc.on("exit", (code, signal) => {
|
|
40
|
+
console.error(`[kandev] backend exited (code=${code}, signal=${signal})`);
|
|
41
|
+
const exitCode = signal ? 0 : code ?? 1;
|
|
42
|
+
void supervisor.shutdown("backend exit").then(() => process.exit(exitCode));
|
|
43
|
+
});
|
|
44
|
+
await (0, health_1.waitForHealth)(backendUrl, backendProc, constants_1.HEALTH_TIMEOUT_MS);
|
|
45
|
+
console.log(`[kandev] backend ready at ${backendUrl}`);
|
|
46
|
+
const webUrl = `http://localhost:${actualWebPort}`;
|
|
47
|
+
(0, web_1.launchWebApp)({
|
|
48
|
+
command: "pnpm",
|
|
49
|
+
args: ["-C", "apps", "--filter", "@kandev/web", "dev"],
|
|
50
|
+
cwd: repoRoot,
|
|
51
|
+
env: webEnv,
|
|
52
|
+
url: webUrl,
|
|
53
|
+
supervisor,
|
|
54
|
+
label: "web",
|
|
55
|
+
});
|
|
56
|
+
console.log(`[kandev] web ready at ${webUrl}`);
|
|
57
|
+
}
|
package/dist/github.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getRelease = getRelease;
|
|
7
|
+
exports.ensureAsset = ensureAsset;
|
|
8
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
// Allow overriding the GitHub repo for forks/testing.
|
|
13
|
+
const OWNER = process.env.KANDEV_GITHUB_OWNER || "kandev";
|
|
14
|
+
const REPO = process.env.KANDEV_GITHUB_REPO || "kandev";
|
|
15
|
+
const API_BASE = `https://api.github.com/repos/${OWNER}/${REPO}`;
|
|
16
|
+
function requestJson(url) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const req = node_https_1.default.get(url, {
|
|
19
|
+
headers: {
|
|
20
|
+
"User-Agent": "kandev-npx",
|
|
21
|
+
Accept: "application/vnd.github+json",
|
|
22
|
+
...(process.env.KANDEV_GITHUB_TOKEN
|
|
23
|
+
? { Authorization: `Bearer ${process.env.KANDEV_GITHUB_TOKEN}` }
|
|
24
|
+
: {}),
|
|
25
|
+
},
|
|
26
|
+
}, (res) => {
|
|
27
|
+
if (res.statusCode !== 200) {
|
|
28
|
+
return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
29
|
+
}
|
|
30
|
+
let body = "";
|
|
31
|
+
res.on("data", (chunk) => (body += chunk));
|
|
32
|
+
res.on("end", () => {
|
|
33
|
+
try {
|
|
34
|
+
resolve(JSON.parse(body));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
reject(new Error(`Failed to parse JSON from ${url}`));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
req.setTimeout(5000, () => {
|
|
42
|
+
req.destroy(new Error(`Request timed out fetching ${url}`));
|
|
43
|
+
});
|
|
44
|
+
req.on("error", reject);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function downloadFile(url, destPath, expectedSha256, onProgress) {
|
|
48
|
+
const tempPath = `${destPath}.tmp`;
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const file = node_fs_1.default.createWriteStream(tempPath);
|
|
51
|
+
const hash = node_crypto_1.default.createHash("sha256");
|
|
52
|
+
const cleanup = () => {
|
|
53
|
+
try {
|
|
54
|
+
node_fs_1.default.unlinkSync(tempPath);
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
};
|
|
58
|
+
const req = node_https_1.default.get(url, {
|
|
59
|
+
headers: {
|
|
60
|
+
"User-Agent": "kandev-npx",
|
|
61
|
+
Accept: "application/octet-stream",
|
|
62
|
+
...(process.env.KANDEV_GITHUB_TOKEN
|
|
63
|
+
? { Authorization: `Bearer ${process.env.KANDEV_GITHUB_TOKEN}` }
|
|
64
|
+
: {}),
|
|
65
|
+
},
|
|
66
|
+
}, (res) => {
|
|
67
|
+
if (res.statusCode !== 200) {
|
|
68
|
+
file.close();
|
|
69
|
+
cleanup();
|
|
70
|
+
return reject(new Error(`HTTP ${res.statusCode} downloading ${url}`));
|
|
71
|
+
}
|
|
72
|
+
const totalSize = parseInt(res.headers["content-length"] || "0", 10);
|
|
73
|
+
let downloadedSize = 0;
|
|
74
|
+
res.on("data", (chunk) => {
|
|
75
|
+
downloadedSize += chunk.length;
|
|
76
|
+
hash.update(chunk);
|
|
77
|
+
if (onProgress)
|
|
78
|
+
onProgress(downloadedSize, totalSize);
|
|
79
|
+
});
|
|
80
|
+
res.pipe(file);
|
|
81
|
+
file.on("finish", () => {
|
|
82
|
+
file.close();
|
|
83
|
+
const actualSha256 = hash.digest("hex");
|
|
84
|
+
if (expectedSha256 && actualSha256 !== expectedSha256) {
|
|
85
|
+
cleanup();
|
|
86
|
+
return reject(new Error(`Checksum mismatch: expected ${expectedSha256}, got ${actualSha256}`));
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
node_fs_1.default.renameSync(tempPath, destPath);
|
|
90
|
+
resolve(destPath);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
cleanup();
|
|
94
|
+
reject(err);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
req.setTimeout(5000, () => {
|
|
99
|
+
req.destroy(new Error(`Request timed out downloading ${url}`));
|
|
100
|
+
});
|
|
101
|
+
req.on("error", (err) => {
|
|
102
|
+
file.close();
|
|
103
|
+
cleanup();
|
|
104
|
+
reject(err);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function findAsset(release, name) {
|
|
109
|
+
return release.assets?.find((asset) => asset.name === name);
|
|
110
|
+
}
|
|
111
|
+
function readSha256(pathToSha) {
|
|
112
|
+
if (!node_fs_1.default.existsSync(pathToSha)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const content = node_fs_1.default.readFileSync(pathToSha, "utf8").trim();
|
|
116
|
+
const first = content.split(/\s+/)[0];
|
|
117
|
+
return first || null;
|
|
118
|
+
}
|
|
119
|
+
async function getRelease(version) {
|
|
120
|
+
if (version) {
|
|
121
|
+
return requestJson(`${API_BASE}/releases/tags/${version}`);
|
|
122
|
+
}
|
|
123
|
+
return requestJson(`${API_BASE}/releases/latest`);
|
|
124
|
+
}
|
|
125
|
+
async function ensureAsset(release, assetName, cacheDir, onProgress) {
|
|
126
|
+
const asset = findAsset(release, assetName);
|
|
127
|
+
if (!asset) {
|
|
128
|
+
throw new Error(`Release asset not found: ${assetName}`);
|
|
129
|
+
}
|
|
130
|
+
node_fs_1.default.mkdirSync(cacheDir, { recursive: true });
|
|
131
|
+
const destPath = node_path_1.default.join(cacheDir, assetName);
|
|
132
|
+
const shaPath = `${destPath}.sha256`;
|
|
133
|
+
let expectedSha = readSha256(shaPath);
|
|
134
|
+
if (!expectedSha) {
|
|
135
|
+
const shaAsset = findAsset(release, `${assetName}.sha256`);
|
|
136
|
+
if (shaAsset) {
|
|
137
|
+
await downloadFile(shaAsset.browser_download_url, shaPath);
|
|
138
|
+
expectedSha = readSha256(shaPath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (node_fs_1.default.existsSync(destPath)) {
|
|
142
|
+
if (!expectedSha) {
|
|
143
|
+
return destPath;
|
|
144
|
+
}
|
|
145
|
+
const hash = node_crypto_1.default.createHash("sha256");
|
|
146
|
+
const file = node_fs_1.default.readFileSync(destPath);
|
|
147
|
+
hash.update(file);
|
|
148
|
+
if (hash.digest("hex") === expectedSha) {
|
|
149
|
+
return destPath;
|
|
150
|
+
}
|
|
151
|
+
node_fs_1.default.unlinkSync(destPath);
|
|
152
|
+
}
|
|
153
|
+
await downloadFile(asset.browser_download_url, destPath, expectedSha, onProgress);
|
|
154
|
+
return destPath;
|
|
155
|
+
}
|
package/dist/health.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.delay = delay;
|
|
4
|
+
exports.waitForHealth = waitForHealth;
|
|
5
|
+
function delay(ms) {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
async function waitForHealth(baseUrl, proc, timeoutMs) {
|
|
9
|
+
const deadline = Date.now() + timeoutMs;
|
|
10
|
+
const healthUrl = `${baseUrl}/health`;
|
|
11
|
+
while (Date.now() < deadline) {
|
|
12
|
+
if (proc.exitCode !== null) {
|
|
13
|
+
throw new Error("Backend exited before healthcheck passed");
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(healthUrl);
|
|
17
|
+
if (res.ok) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// ignore until timeout
|
|
23
|
+
}
|
|
24
|
+
await delay(300);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Backend healthcheck timed out after ${timeoutMs}ms`);
|
|
27
|
+
}
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getBinaryName = getBinaryName;
|
|
4
|
+
exports.getPlatformDir = getPlatformDir;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const isWindows = process.platform === "win32";
|
|
7
|
+
function getBinaryName(base) {
|
|
8
|
+
return isWindows ? `${base}.exe` : base;
|
|
9
|
+
}
|
|
10
|
+
function getEffectiveArch() {
|
|
11
|
+
const platform = process.platform;
|
|
12
|
+
const nodeArch = process.arch;
|
|
13
|
+
if (platform === "darwin") {
|
|
14
|
+
if (nodeArch === "arm64")
|
|
15
|
+
return "arm64";
|
|
16
|
+
try {
|
|
17
|
+
const translated = (0, node_child_process_1.execSync)("sysctl -in sysctl.proc_translated", {
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
}).trim();
|
|
20
|
+
if (translated === "1")
|
|
21
|
+
return "arm64";
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
return "x64";
|
|
25
|
+
}
|
|
26
|
+
if (/arm/i.test(nodeArch))
|
|
27
|
+
return "arm64";
|
|
28
|
+
if (platform === "win32") {
|
|
29
|
+
const pa = process.env.PROCESSOR_ARCHITECTURE || "";
|
|
30
|
+
const paw = process.env.PROCESSOR_ARCHITEW6432 || "";
|
|
31
|
+
if (/arm/i.test(pa) || /arm/i.test(paw))
|
|
32
|
+
return "arm64";
|
|
33
|
+
}
|
|
34
|
+
return "x64";
|
|
35
|
+
}
|
|
36
|
+
function getPlatformDir() {
|
|
37
|
+
const platform = process.platform;
|
|
38
|
+
const arch = getEffectiveArch();
|
|
39
|
+
if (platform === "linux" && arch === "x64")
|
|
40
|
+
return "linux-x64";
|
|
41
|
+
if (platform === "darwin" && arch === "x64")
|
|
42
|
+
return "macos-x64";
|
|
43
|
+
if (platform === "darwin" && arch === "arm64")
|
|
44
|
+
return "macos-arm64";
|
|
45
|
+
if (platform === "win32" && arch === "x64")
|
|
46
|
+
return "windows-x64";
|
|
47
|
+
if (platform === "win32" && arch === "arm64")
|
|
48
|
+
return "windows-arm64";
|
|
49
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
|
50
|
+
}
|
package/dist/ports.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ensureValidPort = ensureValidPort;
|
|
7
|
+
exports.pickAvailablePort = pickAvailablePort;
|
|
8
|
+
exports.pickAndReservePort = pickAndReservePort;
|
|
9
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
10
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
11
|
+
const constants_1 = require("./constants");
|
|
12
|
+
function ensureValidPort(port, name) {
|
|
13
|
+
if (port === undefined) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
17
|
+
throw new Error(`${name} must be an integer between 1 and 65535`);
|
|
18
|
+
}
|
|
19
|
+
return port;
|
|
20
|
+
}
|
|
21
|
+
function isPortAvailable(port, host = "0.0.0.0") {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const server = node_net_1.default.createServer();
|
|
24
|
+
server.unref();
|
|
25
|
+
server.on("error", (err) => {
|
|
26
|
+
if (err.code === "EADDRINUSE" || err.code === "EACCES") {
|
|
27
|
+
resolve(false);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
resolve(false);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
server.listen(port, host, () => {
|
|
34
|
+
server.close(() => resolve(true));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function reserveSpecificPort(port, host = "127.0.0.1") {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const server = node_net_1.default.createServer();
|
|
41
|
+
server.on("error", () => resolve(null));
|
|
42
|
+
server.listen(port, host, () => resolve(server));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function pickAvailablePort(preferred, retries = constants_1.RANDOM_PORT_RETRIES) {
|
|
46
|
+
if (await isPortAvailable(preferred)) {
|
|
47
|
+
return preferred;
|
|
48
|
+
}
|
|
49
|
+
for (let i = 0; i < retries; i += 1) {
|
|
50
|
+
const candidate = node_crypto_1.default.randomInt(constants_1.RANDOM_PORT_MIN, constants_1.RANDOM_PORT_MAX + 1);
|
|
51
|
+
if (await isPortAvailable(candidate)) {
|
|
52
|
+
return candidate;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Unable to find a free port after ${retries + 1} attempts`);
|
|
56
|
+
}
|
|
57
|
+
async function pickAndReservePort(preferred, retries = constants_1.RANDOM_PORT_RETRIES) {
|
|
58
|
+
const reservedPreferred = await reserveSpecificPort(preferred);
|
|
59
|
+
if (reservedPreferred) {
|
|
60
|
+
return {
|
|
61
|
+
port: preferred,
|
|
62
|
+
release: () => new Promise((resolve) => reservedPreferred.close(() => resolve())),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < retries; i += 1) {
|
|
66
|
+
const candidate = node_crypto_1.default.randomInt(constants_1.RANDOM_PORT_MIN, constants_1.RANDOM_PORT_MAX + 1);
|
|
67
|
+
const reserved = await reserveSpecificPort(candidate);
|
|
68
|
+
if (reserved) {
|
|
69
|
+
return {
|
|
70
|
+
port: candidate,
|
|
71
|
+
release: () => new Promise((resolve) => reserved.close(() => resolve())),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`Unable to reserve a free port after ${retries + 1} attempts`);
|
|
76
|
+
}
|
package/dist/process.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createProcessSupervisor = createProcessSupervisor;
|
|
7
|
+
const tree_kill_1 = __importDefault(require("tree-kill"));
|
|
8
|
+
function createProcessSupervisor() {
|
|
9
|
+
let shuttingDown = false;
|
|
10
|
+
const children = [];
|
|
11
|
+
const shutdown = async (reason) => {
|
|
12
|
+
if (shuttingDown)
|
|
13
|
+
return;
|
|
14
|
+
shuttingDown = true;
|
|
15
|
+
console.log(`[kandev] shutting down (${reason})...`);
|
|
16
|
+
await Promise.all(children
|
|
17
|
+
.filter((child) => child.pid)
|
|
18
|
+
.map((child) => new Promise((resolve) => (0, tree_kill_1.default)(child.pid, "SIGTERM", () => resolve()))));
|
|
19
|
+
};
|
|
20
|
+
const onSignal = (signal) => {
|
|
21
|
+
void shutdown(`signal ${signal}`).then(() => process.exit(0));
|
|
22
|
+
};
|
|
23
|
+
const attachSignalHandlers = () => {
|
|
24
|
+
process.on("SIGINT", onSignal);
|
|
25
|
+
process.on("SIGTERM", onSignal);
|
|
26
|
+
};
|
|
27
|
+
return { children, shutdown, attachSignalHandlers };
|
|
28
|
+
}
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runRelease = runRelease;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const bundle_1 = require("./bundle");
|
|
11
|
+
const constants_1 = require("./constants");
|
|
12
|
+
const github_1 = require("./github");
|
|
13
|
+
const constants_2 = require("./constants");
|
|
14
|
+
const health_1 = require("./health");
|
|
15
|
+
const platform_1 = require("./platform");
|
|
16
|
+
const ports_1 = require("./ports");
|
|
17
|
+
const process_1 = require("./process");
|
|
18
|
+
const web_1 = require("./web");
|
|
19
|
+
async function prepareReleaseBundle({ version, backendPort, webPort, }) {
|
|
20
|
+
const platformDir = (0, platform_1.getPlatformDir)();
|
|
21
|
+
const release = await (0, github_1.getRelease)(version);
|
|
22
|
+
const tag = release.tag_name || "latest";
|
|
23
|
+
const assetName = `kandev-${platformDir}.zip`;
|
|
24
|
+
const cacheDir = node_path_1.default.join(constants_2.CACHE_DIR, tag, platformDir);
|
|
25
|
+
const zipPath = await (0, github_1.ensureAsset)(release, assetName, cacheDir, (downloaded, total) => {
|
|
26
|
+
const percent = total ? Math.round((downloaded / total) * 100) : 0;
|
|
27
|
+
const mb = (downloaded / (1024 * 1024)).toFixed(1);
|
|
28
|
+
const totalMb = total ? (total / (1024 * 1024)).toFixed(1) : "?";
|
|
29
|
+
process.stderr.write(`\r Downloading: ${mb}MB / ${totalMb}MB (${percent}%)`);
|
|
30
|
+
});
|
|
31
|
+
process.stderr.write("\n");
|
|
32
|
+
(0, bundle_1.ensureExtracted)(zipPath, cacheDir);
|
|
33
|
+
const bundleDir = (0, bundle_1.findBundleRoot)(cacheDir);
|
|
34
|
+
const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
|
|
35
|
+
if (!node_fs_1.default.existsSync(backendBin)) {
|
|
36
|
+
throw new Error(`Backend binary not found at ${backendBin}`);
|
|
37
|
+
}
|
|
38
|
+
const agentctlBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("agentctl"));
|
|
39
|
+
if (!node_fs_1.default.existsSync(agentctlBin)) {
|
|
40
|
+
throw new Error(`agentctl binary not found at ${agentctlBin}`);
|
|
41
|
+
}
|
|
42
|
+
const actualBackendPort = backendPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_BACKEND_PORT));
|
|
43
|
+
const actualWebPort = webPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_WEB_PORT));
|
|
44
|
+
const agentctlPort = await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_AGENTCTL_PORT);
|
|
45
|
+
const backendUrl = `http://localhost:${actualBackendPort}`;
|
|
46
|
+
node_fs_1.default.mkdirSync(constants_1.DATA_DIR, { recursive: true });
|
|
47
|
+
const dbPath = node_path_1.default.join(constants_1.DATA_DIR, "kandev.db");
|
|
48
|
+
const backendEnv = {
|
|
49
|
+
...process.env,
|
|
50
|
+
KANDEV_SERVER_PORT: String(actualBackendPort),
|
|
51
|
+
KANDEV_AGENT_STANDALONE_PORT: String(agentctlPort),
|
|
52
|
+
KANDEV_DB_PATH: dbPath,
|
|
53
|
+
};
|
|
54
|
+
const webEnv = {
|
|
55
|
+
...process.env,
|
|
56
|
+
KANDEV_API_BASE_URL: backendUrl,
|
|
57
|
+
NEXT_PUBLIC_KANDEV_API_BASE_URL: backendUrl,
|
|
58
|
+
PORT: String(actualWebPort),
|
|
59
|
+
NODE_ENV: "production",
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
bundleDir,
|
|
63
|
+
backendBin,
|
|
64
|
+
backendUrl,
|
|
65
|
+
backendEnv,
|
|
66
|
+
webEnv,
|
|
67
|
+
webPort: actualWebPort,
|
|
68
|
+
agentctlPort,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function launchReleaseApps(prepared) {
|
|
72
|
+
console.log("[kandev] backend port:", prepared.backendEnv.KANDEV_SERVER_PORT);
|
|
73
|
+
console.log("[kandev] web port:", prepared.webPort);
|
|
74
|
+
console.log("[kandev] agentctl port:", prepared.agentctlPort);
|
|
75
|
+
const supervisor = (0, process_1.createProcessSupervisor)();
|
|
76
|
+
supervisor.attachSignalHandlers();
|
|
77
|
+
const backendProc = (0, node_child_process_1.spawn)(prepared.backendBin, [], {
|
|
78
|
+
cwd: node_path_1.default.dirname(prepared.backendBin),
|
|
79
|
+
env: prepared.backendEnv,
|
|
80
|
+
stdio: "inherit",
|
|
81
|
+
});
|
|
82
|
+
supervisor.children.push(backendProc);
|
|
83
|
+
backendProc.on("exit", (code, signal) => {
|
|
84
|
+
console.error(`[kandev] backend exited (code=${code}, signal=${signal})`);
|
|
85
|
+
void supervisor.shutdown("backend exit").then(() => process.exit(code || 1));
|
|
86
|
+
});
|
|
87
|
+
const webServerPath = (0, bundle_1.resolveWebServerPath)(prepared.bundleDir);
|
|
88
|
+
if (!webServerPath) {
|
|
89
|
+
throw new Error("Web server entry (server.js) not found in bundle");
|
|
90
|
+
}
|
|
91
|
+
const webUrl = `http://localhost:${prepared.webPort}`;
|
|
92
|
+
const webProc = (0, web_1.launchWebApp)({
|
|
93
|
+
command: "node",
|
|
94
|
+
args: [webServerPath],
|
|
95
|
+
cwd: node_path_1.default.dirname(webServerPath),
|
|
96
|
+
env: prepared.webEnv,
|
|
97
|
+
url: webUrl,
|
|
98
|
+
supervisor,
|
|
99
|
+
label: "web",
|
|
100
|
+
});
|
|
101
|
+
return { supervisor, backendProc, webServerPath };
|
|
102
|
+
}
|
|
103
|
+
async function runRelease({ version, backendPort, webPort }) {
|
|
104
|
+
const prepared = await prepareReleaseBundle({ version, backendPort, webPort });
|
|
105
|
+
const { backendProc } = launchReleaseApps(prepared);
|
|
106
|
+
// Wait for backend before announcing the web URL.
|
|
107
|
+
await (0, health_1.waitForHealth)(prepared.backendUrl, backendProc, constants_1.HEALTH_TIMEOUT_MS);
|
|
108
|
+
console.log(`[kandev] backend ready at ${prepared.backendUrl}`);
|
|
109
|
+
console.log(`[kandev] web ready at http://localhost:${prepared.webPort}`);
|
|
110
|
+
}
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.maybePromptForUpdate = maybePromptForUpdate;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
9
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
10
|
+
function requestJson(url) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const req = node_https_1.default.get(url, { headers: { "User-Agent": "kandev-npx" } }, (res) => {
|
|
13
|
+
if (res.statusCode !== 200) {
|
|
14
|
+
return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
15
|
+
}
|
|
16
|
+
let body = "";
|
|
17
|
+
res.on("data", (chunk) => (body += chunk));
|
|
18
|
+
res.on("end", () => {
|
|
19
|
+
try {
|
|
20
|
+
resolve(JSON.parse(body));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
reject(new Error(`Failed to parse JSON from ${url}`));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
req.setTimeout(5000, () => {
|
|
28
|
+
req.destroy(new Error(`Request timed out fetching ${url}`));
|
|
29
|
+
});
|
|
30
|
+
req.on("error", reject);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function getLatestNpmVersion() {
|
|
34
|
+
const data = await requestJson("https://registry.npmjs.org/kandev");
|
|
35
|
+
return data?.["dist-tags"]?.latest;
|
|
36
|
+
}
|
|
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
|
+
function promptYesNo(question, defaultYes = false) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
if (!process.stdin.isTTY) {
|
|
53
|
+
resolve(false);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const rl = node_readline_1.default.createInterface({
|
|
57
|
+
input: process.stdin,
|
|
58
|
+
output: process.stdout,
|
|
59
|
+
});
|
|
60
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
61
|
+
rl.question(`${question} ${suffix} `, (answer) => {
|
|
62
|
+
rl.close();
|
|
63
|
+
const normalized = String(answer || "").trim().toLowerCase();
|
|
64
|
+
if (!normalized) {
|
|
65
|
+
resolve(Boolean(defaultYes));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
resolve(normalized === "y" || normalized === "yes");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function maybePromptForUpdate(currentVersion, args) {
|
|
73
|
+
// Allow disabling update checks for CI or automation.
|
|
74
|
+
if (process.env.KANDEV_SKIP_UPDATE === "1" || process.env.KANDEV_NO_UPDATE_PROMPT === "1") {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const latest = await getLatestNpmVersion();
|
|
79
|
+
if (!latest)
|
|
80
|
+
return;
|
|
81
|
+
if (compareVersions(latest, currentVersion) <= 0)
|
|
82
|
+
return;
|
|
83
|
+
const wantsUpdate = await promptYesNo(`Update available: ${currentVersion} -> ${latest}. Update now?`, false);
|
|
84
|
+
if (!wantsUpdate)
|
|
85
|
+
return;
|
|
86
|
+
const env = { ...process.env, KANDEV_SKIP_UPDATE: "1" };
|
|
87
|
+
const child = (0, node_child_process_1.spawn)("npx", ["kandev@latest", ...args], {
|
|
88
|
+
stdio: "inherit",
|
|
89
|
+
env,
|
|
90
|
+
});
|
|
91
|
+
child.on("exit", (code) => process.exit(code || 0));
|
|
92
|
+
await new Promise(() => { });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// ignore update errors
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/web.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openBrowser = openBrowser;
|
|
4
|
+
exports.launchWebApp = launchWebApp;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
function openBrowser(url) {
|
|
7
|
+
if (process.env.KANDEV_NO_BROWSER === "1") {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const opener = process.platform === "darwin"
|
|
11
|
+
? "open"
|
|
12
|
+
: process.platform === "win32"
|
|
13
|
+
? "cmd"
|
|
14
|
+
: "xdg-open";
|
|
15
|
+
const args = process.platform === "win32"
|
|
16
|
+
? ["/c", "start", "", url]
|
|
17
|
+
: [url];
|
|
18
|
+
try {
|
|
19
|
+
const child = (0, node_child_process_1.spawn)(opener, args, { stdio: "ignore", detached: true });
|
|
20
|
+
child.unref();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// ignore browser launch errors
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function launchWebApp({ command, args, cwd, env, url, supervisor, label, }) {
|
|
27
|
+
const proc = (0, node_child_process_1.spawn)(command, args, { cwd, env, stdio: "inherit" });
|
|
28
|
+
supervisor.children.push(proc);
|
|
29
|
+
proc.on("exit", (code, signal) => {
|
|
30
|
+
console.error(`[kandev] ${label} exited (code=${code}, signal=${signal})`);
|
|
31
|
+
const exitCode = signal ? 0 : code ?? 1;
|
|
32
|
+
void supervisor.shutdown(`${label} exit`).then(() => process.exit(exitCode));
|
|
33
|
+
});
|
|
34
|
+
openBrowser(url);
|
|
35
|
+
return proc;
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kandev",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "NPX launcher for Kandev",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"bin": {
|
|
9
|
+
"kandev": "bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/cli.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"adm-zip": "^0.5.16",
|
|
18
|
+
"tree-kill": "^1.2.2"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/adm-zip": "^0.5.7",
|
|
22
|
+
"@types/node": "^20",
|
|
23
|
+
"tsx": "^4.15.7",
|
|
24
|
+
"typescript": "^5"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "tsx src/cli.ts",
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"start": "node dist/cli.js",
|
|
30
|
+
"prepublishOnly": "pnpm build"
|
|
31
|
+
}
|
|
32
|
+
}
|