@gleapai/kai-bridge 0.7.0 → 0.9.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/README.md +28 -4
- package/bin/kai-bridge.mjs +7 -6
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -7
- package/scripts/runtime-smoke.mjs +1 -1
- package/src/daemon.mjs +37 -69
- package/src/executor.mjs +3 -16
- package/src/harness-errors.mjs +8 -0
- package/src/harness-install.mjs +73 -0
- package/src/harnesses.mjs +30 -25
- package/src/{hosted-ports.mjs → preview-ports.mjs} +2 -2
- package/src/preview.mjs +12 -18
- package/src/profiles.mjs +6 -5
- package/src/service.mjs +1 -1
- package/src/setup.mjs +1 -1
- package/src/workspace.mjs +4 -23
- package/fly/Dockerfile +0 -11
- package/fly/UPDATES.md +0 -76
- package/fly/codex-auth.mjs +0 -5
- package/fly/entrypoint.sh +0 -12
- package/fly/login-codex.sh +0 -9
- package/fly/runtime-cli.mjs +0 -15
- package/fly/runtime-cli.sh +0 -10
- package/fly/runtime-launch.mjs +0 -8
- package/fly/runtime-update.mjs +0 -137
- package/fly/start.mjs +0 -7
- package/fly/supervisord.conf +0 -45
- package/src/codex-broker-client.mjs +0 -16
- package/src/codex-broker.mjs +0 -85
- package/src/hosted-auth.mjs +0 -42
- package/src/hosted-git-credential.mjs +0 -24
- package/src/hosted-git.mjs +0 -33
- package/src/hosted-resources.mjs +0 -35
- package/src/hosted-tunnel.mjs +0 -76
- package/src/hosted.mjs +0 -86
package/src/harnesses.mjs
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// their binaries, install/update them, probe their login, and open a
|
|
3
|
-
// login.
|
|
4
|
-
//
|
|
5
|
-
// claude — Claude Code. BUNDLED: the ACP adapter ships the Agent SDK,
|
|
6
|
-
// which ships the CLI. Nothing to install; only a login.
|
|
7
|
-
// codex — Codex CLI. BUNDLED by codex-acp. Only a login.
|
|
8
|
-
// cursor — Cursor Agent CLI. NOT bundled (no npm package); installed by
|
|
9
|
-
// the bridge from Cursor's release tarball into
|
|
10
|
-
// ~/.kai/harnesses/cursor/<version>/ and called by path — never
|
|
11
|
-
// via the vendor's ~/.local/bin/agent symlink (other vendors
|
|
12
|
-
// use that name too: Grok's CLI on at least one dev machine).
|
|
13
|
-
//
|
|
14
|
-
// "Installed" therefore means: claude/codex always; cursor when the
|
|
15
|
-
// tarball is present. "Signed in" is per profile (see profiles.mjs).
|
|
1
|
+
// Coding agents: bundled defaults, optional managed updates, and per-profile sign-in.
|
|
16
2
|
|
|
17
3
|
import { execFileSync, spawn } from "node:child_process";
|
|
18
4
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -20,6 +6,7 @@ import { arch, homedir, platform } from "node:os";
|
|
|
20
6
|
import { dirname, join, resolve as resolvePath } from "node:path";
|
|
21
7
|
import { fileURLToPath } from "node:url";
|
|
22
8
|
import { createRequire } from 'node:module';
|
|
9
|
+
import { managedHarnessRoot, installManagedHarness } from './harness-install.mjs';
|
|
23
10
|
|
|
24
11
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
12
|
const PKG_BIN = join(PKG, "node_modules", ".bin");
|
|
@@ -76,12 +63,12 @@ export function cursorDownloadUrl(version = CURSOR_AGENT_VERSION) {
|
|
|
76
63
|
}
|
|
77
64
|
|
|
78
65
|
/** Bundled / installed binary for a harness, or null. */
|
|
79
|
-
|
|
66
|
+
function packageBinary(harness, packageRequire) {
|
|
80
67
|
if (harness === "claude") {
|
|
81
68
|
try {
|
|
82
69
|
// Resolve from the SDK exactly as the ACP adapter does, including nested
|
|
83
70
|
// npm installs and the correct libc. Directory order is not a platform check.
|
|
84
|
-
const sdkRequire = createRequire(
|
|
71
|
+
const sdkRequire = createRequire(packageRequire.resolve('@anthropic-ai/claude-agent-sdk'));
|
|
85
72
|
const libc = platform() === 'linux' && !process.report.getReport().header.glibcVersionRuntime ? '-musl' : '';
|
|
86
73
|
return sdkRequire.resolve(`@anthropic-ai/claude-agent-sdk-${platform()}-${arch()}${libc}/claude${platform() === 'win32' ? '.exe' : ''}`);
|
|
87
74
|
} catch {
|
|
@@ -94,15 +81,25 @@ export function harnessBinary(harness, kaiHome = join(homedir(), ".kai")) {
|
|
|
94
81
|
if (platform() === 'win32') {
|
|
95
82
|
// execFile/ACP cannot execute a JS entrypoint or .cmd shim directly on
|
|
96
83
|
// Windows. Resolve the same pinned package's native executable there.
|
|
97
|
-
const codexRequire = createRequire(
|
|
84
|
+
const codexRequire = createRequire(packageRequire.resolve('@openai/codex/package.json'));
|
|
98
85
|
const root = dirname(codexRequire.resolve(`@openai/codex-win32-${arch()}/package.json`));
|
|
99
86
|
const triple = `${arch() === 'arm64' ? 'aarch64' : 'x86_64'}-pc-windows-msvc`;
|
|
100
87
|
const binary = join(root, 'vendor', triple, 'bin', 'codex.exe');
|
|
101
88
|
return existsSync(binary) ? binary : null;
|
|
102
89
|
}
|
|
103
|
-
return
|
|
90
|
+
return packageRequire.resolve('@openai/codex/bin/codex.js');
|
|
104
91
|
} catch { return null; }
|
|
105
92
|
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function harnessBinary(harness, kaiHome = process.env.KAI_HOME || join(homedir(), ".kai")) {
|
|
97
|
+
const managed = managedHarnessRoot(kaiHome, harness);
|
|
98
|
+
if (managed) {
|
|
99
|
+
const binary = packageBinary(harness, createRequire(join(managed, "package.json")));
|
|
100
|
+
if (binary && existsSync(binary)) return binary;
|
|
101
|
+
}
|
|
102
|
+
if (harness === "claude" || harness === "codex") return packageBinary(harness, require);
|
|
106
103
|
if (harness === "cursor") {
|
|
107
104
|
const bin = join(CURSOR_CURRENT(kaiHome), "dist-package", platform() === "win32" ? "cursor-agent.exe" : "cursor-agent");
|
|
108
105
|
return existsSync(bin) ? bin : null;
|
|
@@ -146,18 +143,22 @@ export function describeHarnesses(kaiHome) {
|
|
|
146
143
|
}
|
|
147
144
|
|
|
148
145
|
/**
|
|
149
|
-
* Install (or update) a
|
|
146
|
+
* Install (or update) a coding agent. Cursor:
|
|
150
147
|
* download the release tarball for this platform into
|
|
151
148
|
* ~/.kai/harnesses/cursor/<version>/ and point `current` at it.
|
|
152
149
|
* `onLog(line)` receives progress; resolves `{ ok, version, binary }`.
|
|
153
150
|
*/
|
|
154
|
-
export async function installHarness(harness, { kaiHome, onLog = () => {}, version =
|
|
151
|
+
export async function installHarness(harness, { kaiHome = process.env.KAI_HOME || join(homedir(), ".kai"), onLog = () => {}, version, update = false } = {}) {
|
|
155
152
|
if (!HARNESS_IDS.includes(harness)) throw new Error(`unknown harness ${harness}`);
|
|
156
153
|
if (HARNESS_INFO[harness].bundled) {
|
|
157
154
|
const bin = harnessBinary(harness, kaiHome);
|
|
158
|
-
|
|
159
|
-
return { ok:
|
|
155
|
+
const currentVersion = bin ? versionOf(bin) : null;
|
|
156
|
+
if (!update && !version && currentVersion) return { ok: true, version: currentVersion, binary: bin };
|
|
157
|
+
return installManagedHarness({ harness, kaiHome, version: version || 'latest', onLog,
|
|
158
|
+
resolveBinary: root => packageBinary(harness, createRequire(join(root, 'package.json'))), probeVersion: versionOf });
|
|
160
159
|
}
|
|
160
|
+
version ||= CURSOR_AGENT_VERSION;
|
|
161
|
+
if (!/^[0-9][a-zA-Z0-9.-]*$/.test(version)) throw new Error("Invalid Cursor version.");
|
|
161
162
|
const root = CURSOR_ROOT(kaiHome);
|
|
162
163
|
const target = join(root, version);
|
|
163
164
|
const tmp = join(root, `.tmp-${version}-${process.pid}`);
|
|
@@ -197,7 +198,11 @@ export async function installHarness(harness, { kaiHome, onLog = () => {}, versi
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
/** Interactive login command for a harness under a profile's config dir. */
|
|
200
|
-
export function
|
|
201
|
+
export function isHeadless(env = process.env, os = platform()) {
|
|
202
|
+
return !!(env.SSH_CONNECTION || env.SSH_TTY || (os === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function harnessLoginCommand(harness, configDir, kaiHome, { deviceAuth = isHeadless() } = {}) {
|
|
201
206
|
const bin = harnessBinary(harness, kaiHome);
|
|
202
207
|
if (!bin) return null;
|
|
203
208
|
if (harness === "claude") {
|
|
@@ -207,7 +212,7 @@ export function harnessLoginCommand(harness, configDir, kaiHome) {
|
|
|
207
212
|
const env = resolvePath(configDir) !== resolvePath(claudeDefault) ? { CLAUDE_CONFIG_DIR: configDir } : {};
|
|
208
213
|
return { cmd: bin, args: ["auth", "login"], env };
|
|
209
214
|
}
|
|
210
|
-
if (harness === "codex") return { cmd: bin, args: ["login"], env: { CODEX_HOME: configDir } };
|
|
215
|
+
if (harness === "codex") return { cmd: bin, args: ["login", ...(deviceAuth ? ["--device-auth"] : [])], env: { CODEX_HOME: configDir } };
|
|
211
216
|
return { cmd: bin, args: ["login"], env: {} };
|
|
212
217
|
}
|
|
213
218
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Reserve before boot: two sessions can otherwise choose the same free port.
|
|
2
|
-
export class
|
|
2
|
+
export class PreviewPortPool {
|
|
3
3
|
constructor() { this.owners = new Map(); this.tail = Promise.resolve(); }
|
|
4
4
|
reserve(owner, preferred, pinned, listening) {
|
|
5
5
|
const operation = this.tail.then(async () => {
|
|
@@ -17,4 +17,4 @@ export class HostedPortPool {
|
|
|
17
17
|
}
|
|
18
18
|
releaseSession(sessionId) { for (const [port, owner] of this.owners) if (owner.startsWith(`${sessionId}/`)) this.owners.delete(port); }
|
|
19
19
|
}
|
|
20
|
-
export const
|
|
20
|
+
export const previewPorts = new PreviewPortPool();
|
package/src/preview.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { boundHostedProcess } from './hosted-resources.mjs';
|
|
1
|
+
import { previewPorts } from './preview-ports.mjs';
|
|
3
2
|
// Preview tier A — run the app's real dev servers next to the session.
|
|
4
3
|
//
|
|
5
4
|
// Each repo may commit a `.gleap/dev.yaml`:
|
|
@@ -389,7 +388,6 @@ function lockfileHash(cwd) {
|
|
|
389
388
|
|
|
390
389
|
/** Runs `cmd` through the user's login shell (darwin) or the platform shell, capturing output into `fd`. */
|
|
391
390
|
function spawnShell(cmd, { cwd, env, fd, detached = false }) {
|
|
392
|
-
if (process.env.KAI_HOSTED === '1') { env = { ...env, NODE_OPTIONS: '--max-old-space-size=1024' }; detached = true; }
|
|
393
391
|
if (process.platform !== "win32") cmd = withDaemonNode(cmd);
|
|
394
392
|
if (process.platform === "darwin") {
|
|
395
393
|
// Under launchd the daemon's PATH is frozen at install time
|
|
@@ -400,7 +398,6 @@ function spawnShell(cmd, { cwd, env, fd, detached = false }) {
|
|
|
400
398
|
// Windows has no process groups to kill and `detached` would open a
|
|
401
399
|
// console window — taskkill /T does the tree.
|
|
402
400
|
const child = spawn(cmd, { cwd, env, shell: true, stdio: ["ignore", fd, fd], detached: process.platform !== "win32" && detached, windowsHide: true });
|
|
403
|
-
boundHostedProcess(child, { limitMb: 1536 });
|
|
404
401
|
return child;
|
|
405
402
|
}
|
|
406
403
|
|
|
@@ -419,12 +416,11 @@ export class ServiceRunner {
|
|
|
419
416
|
* fires when a service that had become ready dies on its own;
|
|
420
417
|
* `onProcess(name, pid, "add" | "remove")` lets the daemon persist pids.
|
|
421
418
|
*/
|
|
422
|
-
constructor({ kaiHome, sessionId, log = () => {}, onStatus = () => {}, preferredPort = null, describeListener = null, settleMs = DEFAULT_SETTLE_MS, onServiceExit = () => {}, onProcess = () => {}, home = homedir()
|
|
419
|
+
constructor({ kaiHome, sessionId, log = () => {}, onStatus = () => {}, preferredPort = null, describeListener = null, settleMs = DEFAULT_SETTLE_MS, onServiceExit = () => {}, onProcess = () => {}, home = homedir() } = {}) {
|
|
423
420
|
if (settleMs === DEFAULT_SETTLE_MS) settleMs = defaultSettleMs();
|
|
424
421
|
this.kaiHome = kaiHome;
|
|
425
422
|
this.sessionId = sessionId;
|
|
426
|
-
this.
|
|
427
|
-
this.publicUrls = {};
|
|
423
|
+
this.serviceUrls = {};
|
|
428
424
|
this.log = log;
|
|
429
425
|
this.onStatus = onStatus;
|
|
430
426
|
this.preferredPort = typeof preferredPort === "function" ? preferredPort : null;
|
|
@@ -479,13 +475,10 @@ export class ServiceRunner {
|
|
|
479
475
|
}
|
|
480
476
|
if (this.ports[svc.name]) continue;
|
|
481
477
|
const declared = svc.port;
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
this.ports[svc.name] = await hostedPorts.reserve(`${this.sessionId}/${repoRoot}/${svc.name}`, declared || await this.preferredPort?.({ repoKey, service: svc.name }), pinned, isPortListening);
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
478
|
+
const owner = `${this.sessionId}/${repoRoot}/${svc.name}`;
|
|
479
|
+
const pinned = !!declared && (mode === 'local' || new RegExp(`(^|[^0-9])${declared}([^0-9]|$)`).test(`${svc.run} ${Object.values(svc.env).join(' ')}`));
|
|
487
480
|
if (declared && !(await isPortListening(declared))) {
|
|
488
|
-
this.ports[svc.name] = declared;
|
|
481
|
+
this.ports[svc.name] = await previewPorts.reserve(owner, declared, pinned, isPortListening);
|
|
489
482
|
continue;
|
|
490
483
|
}
|
|
491
484
|
if (declared) {
|
|
@@ -513,10 +506,11 @@ export class ServiceRunner {
|
|
|
513
506
|
});
|
|
514
507
|
}
|
|
515
508
|
}
|
|
516
|
-
|
|
509
|
+
const preferred = (declared ? await findFreePortNear(declared) : await this.stablePort(repoKey, svc.name)) ?? (await getFreePort());
|
|
510
|
+
this.ports[svc.name] = await previewPorts.reserve(owner, preferred, false, isPortListening);
|
|
517
511
|
}
|
|
518
512
|
for (const svc of services) {
|
|
519
|
-
this.
|
|
513
|
+
this.serviceUrls[svc.name] = `${svc.protocol || 'http'}://localhost:${this.ports[svc.name]}`;
|
|
520
514
|
}
|
|
521
515
|
this.registered.set(repoRoot, services);
|
|
522
516
|
this.previewNames ??= new Map();
|
|
@@ -594,7 +588,7 @@ export class ServiceRunner {
|
|
|
594
588
|
const env = {
|
|
595
589
|
...process.env,
|
|
596
590
|
PORT: String(port),
|
|
597
|
-
...Object.fromEntries(Object.entries(svc.env).map(([k, v]) => [k, substitutePorts(v, this.ports, this.
|
|
591
|
+
...Object.fromEntries(Object.entries(svc.env).map(([k, v]) => [k, substitutePorts(v, this.ports, this.serviceUrls)])),
|
|
598
592
|
KAI_SESSION_ID: String(this.sessionId),
|
|
599
593
|
BROWSER: "none",
|
|
600
594
|
};
|
|
@@ -603,7 +597,7 @@ export class ServiceRunner {
|
|
|
603
597
|
// straight into `command not found` was the #1 preview failure. The
|
|
604
598
|
// install is authoritative: when it fails, the dev command never runs.
|
|
605
599
|
await this.ensureDeps(cwd, { env, fd, logPath, service: name, repoKey });
|
|
606
|
-
const cmd = stripOpenFlag(substitutePorts(svc.run, this.ports, this.
|
|
600
|
+
const cmd = stripOpenFlag(substitutePorts(svc.run, this.ports, this.serviceUrls));
|
|
607
601
|
const child = spawnShell(cmd, { cwd, env, fd, detached: true });
|
|
608
602
|
m.ready = false;
|
|
609
603
|
child.on("exit", (code) => {
|
|
@@ -865,7 +859,7 @@ export class ServiceRunner {
|
|
|
865
859
|
}
|
|
866
860
|
|
|
867
861
|
stopAll() {
|
|
868
|
-
|
|
862
|
+
previewPorts.releaseSession(this.sessionId);
|
|
869
863
|
for (const [name, child] of this.processes) {
|
|
870
864
|
this.stopping.add(name);
|
|
871
865
|
if (process.platform === "win32") {
|
package/src/profiles.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { execFileSync, spawn } from "node:child_process";
|
|
15
15
|
import { cpSync, existsSync, mkdirSync } from "node:fs";
|
|
16
|
-
import { HARNESS_IDS as REGISTRY_IDS, harnessBinary, harnessLoginCommand, probeHarnessAuth } from "./harnesses.mjs";
|
|
16
|
+
import { HARNESS_IDS as REGISTRY_IDS, harnessBinary, harnessLoginCommand, isHeadless, probeHarnessAuth } from "./harnesses.mjs";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { dirname, join, resolve } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
@@ -112,8 +112,8 @@ export function createManagedProfile(harness, profileId, kaiHome) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/** Interactive login for a profile (opens the harness's own flow). */
|
|
115
|
-
export function loginCommand(harness, configDir, kaiHome = process.env.KAI_HOME || join(HOME, ".kai")) {
|
|
116
|
-
const c = harnessLoginCommand(harness, configDir, kaiHome);
|
|
115
|
+
export function loginCommand(harness, configDir, kaiHome = process.env.KAI_HOME || join(HOME, ".kai"), options) {
|
|
116
|
+
const c = harnessLoginCommand(harness, configDir, kaiHome, options);
|
|
117
117
|
if (!c) return null;
|
|
118
118
|
return { cmd: c.cmd, args: c.args, env: { ...process.env, ...c.env } };
|
|
119
119
|
}
|
|
@@ -124,8 +124,9 @@ export function loginCommand(harness, configDir, kaiHome = process.env.KAI_HOME
|
|
|
124
124
|
* DEVICE running the login under the profile's config dir. Returns the
|
|
125
125
|
* spawned process or null when no terminal could be opened.
|
|
126
126
|
*/
|
|
127
|
-
export function openLoginTerminal(harness, configDir) {
|
|
128
|
-
|
|
127
|
+
export function openLoginTerminal(harness, configDir, kaiHome) {
|
|
128
|
+
if (isHeadless()) return null;
|
|
129
|
+
const c = loginCommand(harness, configDir, kaiHome);
|
|
129
130
|
if (!c) return null;
|
|
130
131
|
// Ambient claude must log in with the env untouched so credentials land
|
|
131
132
|
// in the keychain, where the (equally untouched) probe and turns look.
|
package/src/service.mjs
CHANGED
|
@@ -70,7 +70,7 @@ export function displayEnv(env = process.env) {
|
|
|
70
70
|
export function renderSystemdUnit({ program, args, env = {} }) {
|
|
71
71
|
const q = (s) => `"${String(s).replace(/"/g, '\\"')}"`;
|
|
72
72
|
return `[Unit]
|
|
73
|
-
Description=
|
|
73
|
+
Description=Kai Code Bridge
|
|
74
74
|
Wants=network-online.target
|
|
75
75
|
After=network-online.target
|
|
76
76
|
|
package/src/setup.mjs
CHANGED
|
@@ -165,7 +165,7 @@ const rowLine = (h) => {
|
|
|
165
165
|
export async function runSetup({ binPath, prompter = makePrompter() } = {}) {
|
|
166
166
|
const config = loadConfig();
|
|
167
167
|
out("");
|
|
168
|
-
out("── Kai Bridge ────────────────────────────────────────────────");
|
|
168
|
+
out("── Kai Code Bridge ────────────────────────────────────────────────");
|
|
169
169
|
out("Run Kai Code sessions on this machine — with your own Claude /");
|
|
170
170
|
out("Codex / Cursor login, against your local checkouts.");
|
|
171
171
|
out("");
|
package/src/workspace.mjs
CHANGED
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
import { execFileSync } from "node:child_process";
|
|
16
16
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
|
-
import { createHash } from 'node:crypto';
|
|
19
18
|
|
|
20
19
|
import { seedNodeModules } from "./deps.mjs";
|
|
21
20
|
|
|
@@ -24,18 +23,13 @@ function git(cwd, args, opts = {}) {
|
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
export function sessionSlug(sessionId, title) {
|
|
27
|
-
if (process.env.KAI_HOSTED === '1') {
|
|
28
|
-
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(String(sessionId))) throw new Error('Invalid session identity.');
|
|
29
|
-
return `session-${sessionId}`;
|
|
30
|
-
}
|
|
31
26
|
const t = String(title || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
32
27
|
const id = String(sessionId || "").slice(-8);
|
|
33
28
|
return t ? `${t}-${id}` : `session-${id}`;
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
export function worktreePath(kaiHome, repoName, slug) {
|
|
37
|
-
|
|
38
|
-
return join(kaiHome, "worktrees", repo, slug);
|
|
32
|
+
return join(kaiHome, "worktrees", repoName, slug);
|
|
39
33
|
}
|
|
40
34
|
|
|
41
35
|
/**
|
|
@@ -176,22 +170,9 @@ export function discardChanges(cwd) {
|
|
|
176
170
|
export function removeWorktree({ kaiHome, repo, sessionId, title }) {
|
|
177
171
|
const dir = worktreePath(kaiHome, repo.name, sessionSlug(sessionId, title));
|
|
178
172
|
if (!existsSync(dir)) return false;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
git(repo.primaryPath, ['worktree', 'remove', dir]);
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
git(repo.primaryPath, ["worktree", "remove", "--force", dir]);
|
|
187
|
-
} catch {
|
|
188
|
-
rmSync(dir, { recursive: true, force: true });
|
|
189
|
-
try {
|
|
190
|
-
git(repo.primaryPath, ["worktree", "prune"]);
|
|
191
|
-
} catch {
|
|
192
|
-
/* best-effort */
|
|
193
|
-
}
|
|
194
|
-
}
|
|
173
|
+
// Never destroy dirty or unpushed work during session cleanup.
|
|
174
|
+
if (collectChanges(dir).files.length || git(dir, ['log', 'HEAD', '--not', '--remotes', '--oneline'])) return false;
|
|
175
|
+
git(repo.primaryPath, ['worktree', 'remove', dir]);
|
|
195
176
|
return true;
|
|
196
177
|
}
|
|
197
178
|
|
package/fly/Dockerfile
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
FROM node:24-bookworm-slim
|
|
2
|
-
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates curl sudo openssh-client lsof procps supervisor xvfb fluxbox x11vnc novnc websockify python3 make g++ && rm -rf /var/lib/apt/lists/*
|
|
3
|
-
RUN useradd --home-dir /data/home/kai --shell /bin/bash kai && printf 'kai ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/kai && chmod 440 /etc/sudoers.d/kai
|
|
4
|
-
WORKDIR /opt/kai-bridge
|
|
5
|
-
COPY package.json package-lock.json ./
|
|
6
|
-
RUN npm ci --ignore-scripts
|
|
7
|
-
COPY . .
|
|
8
|
-
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright
|
|
9
|
-
RUN node scripts/postinstall.mjs && node scripts/runtime-smoke.mjs && chmod +x src/codex-broker-client.mjs src/hosted-git.mjs fly/runtime-cli.sh fly/entrypoint.sh fly/login-codex.sh && for cli in codex claude git; do ln -s /opt/kai-bridge/fly/runtime-cli.sh /usr/local/bin/$cli; done && npx playwright install --with-deps chromium
|
|
10
|
-
ENV KAI_HOSTED=1 KAI_HOME=/data/home/kai/.kai HOME=/data/home/kai KAI_BRIDGE_NO_SELF_UPDATE=1 DISABLE_AUTOUPDATER=1
|
|
11
|
-
ENTRYPOINT ["/opt/kai-bridge/fly/entrypoint.sh"]
|
package/fly/UPDATES.md
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Hosted coding runtime releases
|
|
2
|
-
|
|
3
|
-
One Bridge release owns the complete coding runtime: Codex CLI, Claude Agent SDK
|
|
4
|
-
(including its native Claude Code binary), both ACP adapters, Bridge and its
|
|
5
|
-
runner. Versions are pinned in package.json and package-lock.json. The image no
|
|
6
|
-
longer installs separate global CLIs. `codex`, `claude`, login, version probes,
|
|
7
|
-
coding turns and the supervised Codex process all resolve this bundle.
|
|
8
|
-
|
|
9
|
-
## Routine updates
|
|
10
|
-
|
|
11
|
-
Dependabot checks npm dependencies daily and groups the coding runtime changes.
|
|
12
|
-
The `Coding runtime compatibility` workflow runs the full Bridge tests, native
|
|
13
|
-
CLI versions, Codex app-server and both ACP initialization checks, package checks
|
|
14
|
-
and the Linux image build. Vendor CLI updates are disabled for the managed
|
|
15
|
-
executables so they cannot mutate a tested bundle underneath active sessions.
|
|
16
|
-
|
|
17
|
-
After review and internal qualification, publish a new Bridge version and promote
|
|
18
|
-
that exact version to the npm `hosted-stable` dist-tag. Promotion is a release
|
|
19
|
-
operation, never something a customer VM decides from upstream `latest` versions.
|
|
20
|
-
The channel is intentionally separate from `latest`, which connected devices use.
|
|
21
|
-
No tag is published or promoted merely by adding this workflow.
|
|
22
|
-
|
|
23
|
-
`npm pack`/`npm publish` generate a published npm-shrinkwrap.json from the committed
|
|
24
|
-
lockfile. Consumers therefore receive the dependency tree that was tested. The
|
|
25
|
-
temporary shrinkwrap is removed after packing. Always bump the Bridge version and
|
|
26
|
-
refresh its lockfile for a new release; npm versions are immutable.
|
|
27
|
-
|
|
28
|
-
Each hosted machine checks `hosted-stable` on a normal start, at most once per day.
|
|
29
|
-
It does not wake a stopped machine to check. Updates only happen **before
|
|
30
|
-
supervisord starts**, under a file lock; running coding, validation, previews,
|
|
31
|
-
maintenance and login processes are never hot-replaced. A machine that remains
|
|
32
|
-
running applies updates on its next normal stop/start.
|
|
33
|
-
|
|
34
|
-
The updater downloads an exact release, checks protocol compatibility, installs
|
|
35
|
-
the shrinkwrapped dependencies with lifecycle scripts disabled, and runs native
|
|
36
|
-
startup checks with an empty temporary home. It selects the new bundle atomically
|
|
37
|
-
only after those checks pass. Registry failures, invalid releases, failed checks
|
|
38
|
-
and interrupted installs preserve the previous selection. Failed versions are
|
|
39
|
-
quarantined; the next distinct approved version can still update.
|
|
40
|
-
|
|
41
|
-
Runtime files are stored under `/opt/kai-runtime`. User repositories, worktrees,
|
|
42
|
-
home directories and authentication remain on `/data` and are never copied or
|
|
43
|
-
overwritten by updates. The current and one previous installed bundle are kept.
|
|
44
|
-
This does not rebuild the VM, reset its usage allowance, or require a new login.
|
|
45
|
-
Setup/update running time still counts against the slot allowance.
|
|
46
|
-
|
|
47
|
-
## Rollback and support
|
|
48
|
-
|
|
49
|
-
Inspect `/opt/kai-runtime/state.json` for the active version, previous version,
|
|
50
|
-
last check, failed version and update error. Bridge's normal version/harness
|
|
51
|
-
reports show the selected executable versions in Gleap. Updates can be disabled
|
|
52
|
-
with the machine environment `KAI_HOSTED_RUNTIME_UPDATES=0`.
|
|
53
|
-
|
|
54
|
-
If a runtime passes startup checks but later proves incompatible, schedule a
|
|
55
|
-
rollback through maintenance SSH:
|
|
56
|
-
|
|
57
|
-
```sh
|
|
58
|
-
sudo node /opt/kai-bridge/fly/runtime-update.mjs --rollback-next-start
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Then stop and start the machine through Gleap. This selects the retained previous
|
|
62
|
-
bundle (or the image's original bundle), preserves credentials/files and
|
|
63
|
-
quarantines the rejected release. The command does not interrupt current work.
|
|
64
|
-
Do not run `npm update` inside active release directories.
|
|
65
|
-
|
|
66
|
-
The startup checks exercise real binaries and protocol initialization without
|
|
67
|
-
account credentials or billable prompts. They do not replace authenticated
|
|
68
|
-
two-session coding/refresh/verification tests on the internal cohort before
|
|
69
|
-
promoting `hosted-stable`. A startup pass cannot guarantee compatibility with
|
|
70
|
-
every provider-side behavior. Keep the previous runtime available for rollback.
|
|
71
|
-
|
|
72
|
-
Changing the bootstrap protocol, OS packages, browser revision or persistent data formats requires
|
|
73
|
-
an explicit image/migration release; the boot updater rejects unsupported
|
|
74
|
-
protocol versions and Playwright revisions that differ from the image's installed
|
|
75
|
-
browser. System modifications survive normal starts as configured by
|
|
76
|
-
Fly root-filesystem persistence.
|
package/fly/codex-auth.mjs
DELETED
package/fly/entrypoint.sh
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
set -eu
|
|
3
|
-
mkdir -p /data/home/kai/.codex /data/home/kai/.claude /data/repos /data/logs
|
|
4
|
-
chown kai:kai /data/home/kai /data/home/kai/.codex /data/home/kai/.claude /data/repos /data/logs
|
|
5
|
-
touch /run/kai-runtime-update.lock
|
|
6
|
-
chown root:kai /run/kai-runtime-update.lock
|
|
7
|
-
chmod 660 /run/kai-runtime-update.lock
|
|
8
|
-
# The lock and boot boundary keep installations away from coding/login/preview
|
|
9
|
-
# processes. Failed or interrupted updates keep the previous validated bundle.
|
|
10
|
-
KAI_RUNTIME_ROOT="$(flock /run/kai-runtime-update.lock node /opt/kai-bridge/fly/runtime-update.mjs)"
|
|
11
|
-
export KAI_RUNTIME_ROOT
|
|
12
|
-
exec /usr/bin/supervisord -n -c /opt/kai-bridge/fly/supervisord.conf
|
package/fly/login-codex.sh
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
set -eu
|
|
3
|
-
# Pause the single refresh owner while native device authentication updates it.
|
|
4
|
-
exec 9>/data/codex-login.lock
|
|
5
|
-
flock -n 9 || { echo 'Another Codex sign-in is already in progress.'; exit 1; }
|
|
6
|
-
control='/opt/kai-bridge/fly/supervisord.conf'
|
|
7
|
-
supervisorctl -c "$control" stop codex-auth
|
|
8
|
-
trap 'supervisorctl -c "$control" start codex-auth' EXIT
|
|
9
|
-
sudo -iu kai codex login --device-auth
|
package/fly/runtime-cli.mjs
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import { runtimeRoot } from './runtime-update.mjs';
|
|
6
|
-
|
|
7
|
-
const root = process.env.KAI_RUNTIME_ROOT || runtimeRoot();
|
|
8
|
-
const name = process.env.KAI_RUNTIME_CLI_NAME || basename(process.argv[1]).replace(/\.mjs$/, '');
|
|
9
|
-
const { harnessBinary } = await import(pathToFileURL(join(root, 'src/harnesses.mjs')));
|
|
10
|
-
const command = name === 'git' ? process.execPath : harnessBinary(name);
|
|
11
|
-
if (!command) { process.stderr.write('The selected Kai runtime is missing this executable.\n'); process.exit(1); }
|
|
12
|
-
const args = name === 'git' ? [join(root, 'src/hosted-git.mjs'), ...process.argv.slice(2)] : process.argv.slice(2);
|
|
13
|
-
const child = spawn(command, args, { stdio: 'inherit', env: { ...process.env, KAI_RUNTIME_ROOT: root, DISABLE_AUTOUPDATER: '1' } });
|
|
14
|
-
child.on('error', () => process.exit(1));
|
|
15
|
-
child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 1); });
|
package/fly/runtime-cli.sh
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
set -eu
|
|
3
|
-
KAI_RUNTIME_CLI_NAME="${0##*/}"
|
|
4
|
-
export KAI_RUNTIME_CLI_NAME
|
|
5
|
-
# Startup validation has an isolated HOME and already holds the exclusive lock.
|
|
6
|
-
if [ "${KAI_RUNTIME_UPDATE_CHECK:-0}" = '1' ]; then
|
|
7
|
-
exec node /opt/kai-bridge/fly/runtime-cli.mjs "$@"
|
|
8
|
-
fi
|
|
9
|
-
# A native CLI opened through maintenance SSH waits for an in-progress update.
|
|
10
|
-
exec flock --shared /run/kai-runtime-update.lock node /opt/kai-bridge/fly/runtime-cli.mjs "$@"
|
package/fly/runtime-launch.mjs
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { pathToFileURL } from 'node:url';
|
|
3
|
-
import { runtimeRoot } from './runtime-update.mjs';
|
|
4
|
-
const entry = { bridge: 'start.mjs', 'codex-auth': 'codex-auth.mjs' }[process.argv[2]];
|
|
5
|
-
if (!entry) throw new Error('Unknown hosted runtime service.');
|
|
6
|
-
const root = process.env.KAI_RUNTIME_ROOT || runtimeRoot();
|
|
7
|
-
process.env.KAI_RUNTIME_ROOT = root;
|
|
8
|
-
await import(pathToFileURL(join(root, 'fly', entry)));
|
package/fly/runtime-update.mjs
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { promisify } from 'node:util';
|
|
6
|
-
import { isNewer } from '../src/selfupdate.mjs';
|
|
7
|
-
|
|
8
|
-
const exec = promisify(execFile);
|
|
9
|
-
export const BASE_RUNTIME = '/opt/kai-bridge';
|
|
10
|
-
export const RUNTIME_STORE = '/opt/kai-runtime';
|
|
11
|
-
const CHANNEL = 'https://registry.npmjs.org/@gleapai/kai-bridge/hosted-stable';
|
|
12
|
-
const DAY = 86400_000;
|
|
13
|
-
const version = value => typeof value === 'string' && /^\d+\.\d+\.\d+$/.test(value);
|
|
14
|
-
const read = path => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return {}; } };
|
|
15
|
-
const packageAt = root => read(join(root, 'package.json'));
|
|
16
|
-
export function browserVersion(root) {
|
|
17
|
-
const lock = read(join(root, existsSync(join(root, 'npm-shrinkwrap.json')) ? 'npm-shrinkwrap.json' : 'package-lock.json'));
|
|
18
|
-
return lock.packages?.['node_modules/playwright-core']?.version;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function assertImageCompatibility(candidate, base) {
|
|
22
|
-
// Chromium is installed by the image, outside the replaceable runtime. Never
|
|
23
|
-
// activate a new Playwright revision against the old browser executable.
|
|
24
|
-
if (!browserVersion(candidate) || browserVersion(candidate) !== browserVersion(base)) {
|
|
25
|
-
throw Object.assign(new Error('This release needs a browser image update.'), { code: 'image_required' });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function runtimeRoot(store = RUNTIME_STORE, base = BASE_RUNTIME) {
|
|
30
|
-
const state = read(join(store, 'state.json'));
|
|
31
|
-
const candidate = version(state.active) ? join(store, 'releases', state.active) : base;
|
|
32
|
-
return existsSync(join(candidate, 'package.json')) ? candidate : base;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function save(store, state) {
|
|
36
|
-
mkdirSync(store, { recursive: true });
|
|
37
|
-
const temporary = join(store, `state.${process.pid}.tmp`);
|
|
38
|
-
writeFileSync(temporary, JSON.stringify(state, null, 2) + '\n', { mode: 0o644 });
|
|
39
|
-
renameSync(temporary, join(store, 'state.json'));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function supervisorRunning() {
|
|
43
|
-
const pid = Number(readFileText('/run/supervisord.pid'));
|
|
44
|
-
if (!Number.isSafeInteger(pid) || pid <= 1) return false;
|
|
45
|
-
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
46
|
-
}
|
|
47
|
-
const readFileText = path => { try { return readFileSync(path, 'utf8').trim(); } catch { return ''; } };
|
|
48
|
-
|
|
49
|
-
export function requestRuntimeRollback(store = RUNTIME_STORE) {
|
|
50
|
-
const state = read(join(store, 'state.json'));
|
|
51
|
-
if (!state.active || !state.previousVersion) throw new Error('No previous runtime is available.');
|
|
52
|
-
writeFileSync(join(store, 'rollback-next-start'), '1\n', { mode: 0o600 });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function fetchHostedRelease() {
|
|
56
|
-
const response = await fetch(CHANNEL, { signal: AbortSignal.timeout(8000), headers: { accept: 'application/json' } });
|
|
57
|
-
if (response.status === 404) return null; // Channel has not been promoted yet.
|
|
58
|
-
if (!response.ok) throw new Error(`Release registry unavailable (${response.status}).`);
|
|
59
|
-
return response.json();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function installHostedRelease(release, destination, store, base = BASE_RUNTIME) {
|
|
63
|
-
mkdirSync(destination, { recursive: true });
|
|
64
|
-
const home = join(destination, '.install-home'); mkdirSync(home);
|
|
65
|
-
// No customer HOME, OAuth profiles, bootstrap token or npm login is inherited.
|
|
66
|
-
const env = { PATH: process.env.PATH, HOME: home, CI: '1', KAI_RUNTIME_UPDATE_CHECK: '1', npm_config_registry: 'https://registry.npmjs.org', npm_config_cache: join(store, 'cache') };
|
|
67
|
-
try {
|
|
68
|
-
const packed = await exec('npm', ['pack', `@gleapai/kai-bridge@${release.version}`, '--ignore-scripts', '--json', '--pack-destination', destination], { env, timeout: 30_000, maxBuffer: 1024 * 1024 });
|
|
69
|
-
const archive = JSON.parse(packed.stdout)?.[0]?.filename;
|
|
70
|
-
if (archive !== `gleapai-kai-bridge-${release.version}.tgz`) throw new Error('Unexpected runtime archive.');
|
|
71
|
-
await exec('tar', ['-xzf', join(destination, archive), '--strip-components=1', '-C', destination], { timeout: 10_000 });
|
|
72
|
-
const pkg = packageAt(destination), lock = read(join(destination, 'npm-shrinkwrap.json'));
|
|
73
|
-
if (pkg.name !== '@gleapai/kai-bridge' || pkg.version !== release.version || pkg.kaiHostedRuntime?.protocol !== 1 || lock.version !== pkg.version) throw new Error('Release lacks a matching locked runtime.');
|
|
74
|
-
assertImageCompatibility(destination, base);
|
|
75
|
-
await exec('npm', ['ci', '--ignore-scripts', '--omit=dev', '--no-audit', '--no-fund'], { cwd: destination, env, timeout: 60_000, maxBuffer: 1024 * 1024 });
|
|
76
|
-
await exec(process.execPath, [join(destination, 'scripts/runtime-smoke.mjs')], { cwd: destination, env, timeout: 45_000, maxBuffer: 1024 * 1024 });
|
|
77
|
-
rmSync(join(destination, archive), { force: true });
|
|
78
|
-
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Called under flock BEFORE supervisord starts. Running VMs never hot-swap
|
|
82
|
-
* runtimes. Install/check failures and interrupted installs retain the old root. */
|
|
83
|
-
export async function updateHostedRuntime({ store = RUNTIME_STORE, base = BASE_RUNTIME, now = Date.now(), busy = supervisorRunning(), enabled = true,
|
|
84
|
-
fetchRelease = fetchHostedRelease, install = installHostedRelease, log = message => process.stderr.write(`${message}\n`) } = {}) {
|
|
85
|
-
const current = runtimeRoot(store, base);
|
|
86
|
-
if (busy) return current;
|
|
87
|
-
const state = read(join(store, 'state.json'));
|
|
88
|
-
if (existsSync(join(store, 'rollback-next-start')) && state.active && state.previousVersion) {
|
|
89
|
-
save(store, { checkedAt: now, active: state.previous || null, failed: state.active, rolledBackAt: now });
|
|
90
|
-
rmSync(join(store, 'rollback-next-start'), { force: true });
|
|
91
|
-
log('Previous hosted runtime selected; the rejected release is quarantined.');
|
|
92
|
-
return runtimeRoot(store, base);
|
|
93
|
-
}
|
|
94
|
-
if (!enabled) return current;
|
|
95
|
-
if (state.checkedAt && now - state.checkedAt < DAY) return current;
|
|
96
|
-
state.checkedAt = now; save(store, state);
|
|
97
|
-
let release, staging;
|
|
98
|
-
try {
|
|
99
|
-
release = await fetchRelease();
|
|
100
|
-
if (!release || release.name !== '@gleapai/kai-bridge' || release.kaiHostedRuntime?.protocol !== 1 || !version(release.version)) return current;
|
|
101
|
-
if (!isNewer(release.version, packageAt(current).version) || state.failed === release.version) return current;
|
|
102
|
-
staging = join(store, 'releases', `.staging-${release.version}`);
|
|
103
|
-
rmSync(staging, { recursive: true, force: true });
|
|
104
|
-
await install(release, staging, store, base);
|
|
105
|
-
// Recheck admission boundary before activation, including manual invocation.
|
|
106
|
-
if (supervisorRunning()) throw new Error('Machine became busy during the update.');
|
|
107
|
-
const destination = join(store, 'releases', release.version);
|
|
108
|
-
rmSync(destination, { recursive: true, force: true });
|
|
109
|
-
renameSync(staging, destination);
|
|
110
|
-
save(store, { checkedAt: now, active: release.version, previous: state.active || null, previousVersion: packageAt(current).version, activatedAt: now });
|
|
111
|
-
// Keep one rollback bundle. User repositories and auth are outside this store.
|
|
112
|
-
try {
|
|
113
|
-
for (const entry of readdirSync(join(store, 'releases'))) {
|
|
114
|
-
if (version(entry) && entry !== release.version && entry !== state.active) rmSync(join(store, 'releases', entry), { recursive: true, force: true });
|
|
115
|
-
}
|
|
116
|
-
rmSync(join(store, 'cache'), { recursive: true, force: true });
|
|
117
|
-
} catch { /* Cleanup cannot invalidate an already-validated activation. */ }
|
|
118
|
-
log(`Hosted runtime ${release.version} validated and selected for this start.`);
|
|
119
|
-
return destination;
|
|
120
|
-
} catch (error) {
|
|
121
|
-
if (staging) rmSync(staging, { recursive: true, force: true });
|
|
122
|
-
save(store, { ...state, ...(release?.version && version(release.version) ? { failed: release.version } : {}), error: error.code === 'image_required' ? 'This release needs a browser image update; previous runtime retained.' : 'Runtime update failed; previous release retained.' });
|
|
123
|
-
log('Hosted runtime update failed; starting the previous release.');
|
|
124
|
-
return current;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
129
|
-
if (process.argv.includes('--rollback-next-start')) {
|
|
130
|
-
requestRuntimeRollback(); process.stdout.write('Rollback scheduled. Stop and start the machine from Gleap to apply it.\n');
|
|
131
|
-
} else {
|
|
132
|
-
// stdout is consumed by the entrypoint: emit only the selected absolute path.
|
|
133
|
-
updateHostedRuntime({ enabled: process.env.KAI_HOSTED_RUNTIME_UPDATES !== '0' })
|
|
134
|
-
.then(root => process.stdout.write(`${root}\n`))
|
|
135
|
-
.catch(() => process.stdout.write(`${runtimeRoot()}\n`));
|
|
136
|
-
}
|
|
137
|
-
}
|