@oscharko-dev/keiko-cli 0.2.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/.tsbuildinfo +1 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +103 -0
- package/dist/doctor.d.ts +24 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +108 -0
- package/dist/evaluate.d.ts +8 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +270 -0
- package/dist/evidence.d.ts +9 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +129 -0
- package/dist/gateway-config.d.ts +12 -0
- package/dist/gateway-config.d.ts.map +1 -0
- package/dist/gateway-config.js +19 -0
- package/dist/gen-tests.d.ts +8 -0
- package/dist/gen-tests.d.ts.map +1 -0
- package/dist/gen-tests.js +216 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/init.d.ts +9 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +122 -0
- package/dist/install-layout.d.ts +19 -0
- package/dist/install-layout.d.ts.map +1 -0
- package/dist/install-layout.js +76 -0
- package/dist/investigate.d.ts +9 -0
- package/dist/investigate.d.ts.map +1 -0
- package/dist/investigate.js +249 -0
- package/dist/launcher-paths.d.ts +4 -0
- package/dist/launcher-paths.d.ts.map +1 -0
- package/dist/launcher-paths.js +69 -0
- package/dist/launcher-platforms.d.ts +25 -0
- package/dist/launcher-platforms.d.ts.map +1 -0
- package/dist/launcher-platforms.js +131 -0
- package/dist/launcher-state.d.ts +25 -0
- package/dist/launcher-state.d.ts.map +1 -0
- package/dist/launcher-state.js +228 -0
- package/dist/launcher.d.ts +21 -0
- package/dist/launcher.d.ts.map +1 -0
- package/dist/launcher.js +439 -0
- package/dist/lifecycle.d.ts +22 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +425 -0
- package/dist/memory.d.ts +14 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +290 -0
- package/dist/models.d.ts +4 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +62 -0
- package/dist/prompt-enhancer.d.ts +13 -0
- package/dist/prompt-enhancer.d.ts.map +1 -0
- package/dist/prompt-enhancer.js +261 -0
- package/dist/repair.d.ts +10 -0
- package/dist/repair.d.ts.map +1 -0
- package/dist/repair.js +402 -0
- package/dist/run.d.ts +10 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +269 -0
- package/dist/runner.d.ts +7 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +108 -0
- package/dist/state-paths.d.ts +43 -0
- package/dist/state-paths.d.ts.map +1 -0
- package/dist/state-paths.js +396 -0
- package/dist/ui.d.ts +39 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +450 -0
- package/dist/uninstall.d.ts +10 -0
- package/dist/uninstall.d.ts.map +1 -0
- package/dist/uninstall.js +345 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +108 -0
- package/package.json +42 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
2
|
+
import type { EnvSource } from "@oscharko-dev/keiko-model-gateway";
|
|
3
|
+
import type { CliIo } from "./runner.js";
|
|
4
|
+
type LifecycleCommand = "start" | "stop" | "status" | "restart";
|
|
5
|
+
type SpawnFn = (command: string, args: readonly string[], opts: SpawnOptions) => ChildProcess;
|
|
6
|
+
type FetchFn = (input: string, init?: RequestInit) => Promise<Response>;
|
|
7
|
+
type SleepFn = (ms: number) => Promise<void>;
|
|
8
|
+
type ProcessKiller = (pid: number, signal?: NodeJS.Signals | 0) => void;
|
|
9
|
+
type PortAvailabilityFn = (host: string, port: number) => Promise<boolean>;
|
|
10
|
+
export interface LifecycleCliDeps {
|
|
11
|
+
readonly cwd?: string | undefined;
|
|
12
|
+
readonly spawnFn?: SpawnFn | undefined;
|
|
13
|
+
readonly fetchImpl?: FetchFn | undefined;
|
|
14
|
+
readonly sleep?: SleepFn | undefined;
|
|
15
|
+
readonly isProcessAlive?: ((pid: number) => boolean) | undefined;
|
|
16
|
+
readonly killProcess?: ProcessKiller | undefined;
|
|
17
|
+
readonly isPortAvailable?: PortAvailabilityFn | undefined;
|
|
18
|
+
readonly openExternal?: ((url: string) => void) | undefined;
|
|
19
|
+
}
|
|
20
|
+
export declare function runLifecycleCli(command: LifecycleCommand, args: readonly string[], io: CliIo, env: EnvSource, deps?: LifecycleCliDeps): Promise<number>;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AASA,OAAO,EAAS,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAInE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,KAAK,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAChE,KAAK,OAAO,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,KAAK,YAAY,CAAC;AAC9F,KAAK,OAAO,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AACxE,KAAK,OAAO,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAC7C,KAAK,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,OAAO,GAAG,CAAC,KAAK,IAAI,CAAC;AACxE,KAAK,kBAAkB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAmD3E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;IACjE,QAAQ,CAAC,WAAW,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,eAAe,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IAC1D,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAC7D;AA+dD,wBAAsB,eAAe,CACnC,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,KAAK,EACT,GAAG,EAAE,SAAS,EACd,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createServer as createNetServer } from "node:net";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { SDK_VERSION } from "@oscharko-dev/keiko-sdk";
|
|
7
|
+
import { DEFAULT_UI_PORT, UI_HOST } from "@oscharko-dev/keiko-server";
|
|
8
|
+
import { resolvePreferredInstallLayout } from "./install-layout.js";
|
|
9
|
+
const ALLOWED_HOSTS = new Set(["127.0.0.1", "localhost"]);
|
|
10
|
+
const KEIKO_PROCESS_TITLE = "Keiko";
|
|
11
|
+
const LIFECYCLE_FLAG_SETTERS = {
|
|
12
|
+
"--port": (raw, value) => {
|
|
13
|
+
raw.portRaw = value;
|
|
14
|
+
},
|
|
15
|
+
"--host": (raw, value) => {
|
|
16
|
+
raw.hostRaw = value;
|
|
17
|
+
},
|
|
18
|
+
"--state-dir": (raw, value) => {
|
|
19
|
+
raw.stateDirRaw = value;
|
|
20
|
+
},
|
|
21
|
+
"--start-timeout": (raw, value) => {
|
|
22
|
+
raw.startTimeoutRaw = value;
|
|
23
|
+
},
|
|
24
|
+
"--stop-timeout": (raw, value) => {
|
|
25
|
+
raw.stopTimeoutRaw = value;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const USAGE = `Usage:
|
|
29
|
+
keiko start [--port PORT] [--host 127.0.0.1|localhost] [--state-dir PATH] [--open]
|
|
30
|
+
keiko stop [--state-dir PATH]
|
|
31
|
+
keiko restart [--port PORT] [--host 127.0.0.1|localhost] [--state-dir PATH] [--open]
|
|
32
|
+
keiko status [--port PORT] [--host 127.0.0.1|localhost] [--state-dir PATH]
|
|
33
|
+
|
|
34
|
+
Manages the local Keiko UI process. Runtime state is written to .keiko/ by default.
|
|
35
|
+
`;
|
|
36
|
+
function staleProcessReason(health) {
|
|
37
|
+
if (!health.reachable)
|
|
38
|
+
return "health check is unreachable";
|
|
39
|
+
if (health.version === undefined)
|
|
40
|
+
return "health check did not return the current Keiko version";
|
|
41
|
+
return `running version ${health.version} differs from installed version ${SDK_VERSION}`;
|
|
42
|
+
}
|
|
43
|
+
function readFlagValue(args, index) {
|
|
44
|
+
const value = args[index + 1];
|
|
45
|
+
return value === undefined || value.startsWith("--") ? null : value;
|
|
46
|
+
}
|
|
47
|
+
function parsePort(raw) {
|
|
48
|
+
if (!/^\d{1,5}$/.test(raw))
|
|
49
|
+
return null;
|
|
50
|
+
const port = Number(raw);
|
|
51
|
+
return port >= 1 && port <= 65535 ? port : null;
|
|
52
|
+
}
|
|
53
|
+
function parsePositiveSeconds(raw) {
|
|
54
|
+
if (!/^[1-9]\d*$/.test(raw))
|
|
55
|
+
return null;
|
|
56
|
+
return Number(raw) * 1000;
|
|
57
|
+
}
|
|
58
|
+
function optionOrEnv(value, envValue, fallback) {
|
|
59
|
+
return value ?? envValue ?? fallback;
|
|
60
|
+
}
|
|
61
|
+
function resolveStateDir(cwd, value) {
|
|
62
|
+
return isAbsolute(value) ? value : resolve(cwd, value);
|
|
63
|
+
}
|
|
64
|
+
function isLifecycleFlag(arg) {
|
|
65
|
+
return Object.prototype.hasOwnProperty.call(LIFECYCLE_FLAG_SETTERS, arg);
|
|
66
|
+
}
|
|
67
|
+
function collectLifecycleOptions(args) {
|
|
68
|
+
const raw = {};
|
|
69
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
70
|
+
const arg = args[i];
|
|
71
|
+
if (arg === undefined)
|
|
72
|
+
return null;
|
|
73
|
+
if (arg === "--help" || arg === "-h") {
|
|
74
|
+
return "help";
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--open") {
|
|
77
|
+
raw.openBrowser = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!isLifecycleFlag(arg))
|
|
81
|
+
return null;
|
|
82
|
+
const value = readFlagValue(args, i);
|
|
83
|
+
if (value === null)
|
|
84
|
+
return null;
|
|
85
|
+
LIFECYCLE_FLAG_SETTERS[arg](raw, value);
|
|
86
|
+
i += 1;
|
|
87
|
+
}
|
|
88
|
+
return raw;
|
|
89
|
+
}
|
|
90
|
+
function buildLifecycleOptions(raw, cwd, env) {
|
|
91
|
+
const port = parsePort(optionOrEnv(raw.portRaw, env.KEIKO_UI_PORT, String(DEFAULT_UI_PORT)));
|
|
92
|
+
const host = optionOrEnv(raw.hostRaw, env.KEIKO_UI_HOST, UI_HOST);
|
|
93
|
+
const startTimeoutMs = parsePositiveSeconds(optionOrEnv(raw.startTimeoutRaw, env.KEIKO_START_TIMEOUT_SECS, "20"));
|
|
94
|
+
const stopTimeoutMs = parsePositiveSeconds(optionOrEnv(raw.stopTimeoutRaw, env.KEIKO_STOP_TIMEOUT_SECS, "10"));
|
|
95
|
+
if (port === null ||
|
|
96
|
+
!ALLOWED_HOSTS.has(host) ||
|
|
97
|
+
startTimeoutMs === null ||
|
|
98
|
+
stopTimeoutMs === null) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
port,
|
|
103
|
+
host,
|
|
104
|
+
stateDir: resolveStateDir(cwd, optionOrEnv(raw.stateDirRaw, env.KEIKO_STATE_DIR, ".keiko")),
|
|
105
|
+
startTimeoutMs,
|
|
106
|
+
stopTimeoutMs,
|
|
107
|
+
openBrowser: raw.openBrowser === true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function parseLifecycleArgs(args, cwd, env) {
|
|
111
|
+
const raw = collectLifecycleOptions(args);
|
|
112
|
+
if (raw === "help" || raw === null)
|
|
113
|
+
return raw;
|
|
114
|
+
return buildLifecycleOptions(raw, cwd, env);
|
|
115
|
+
}
|
|
116
|
+
function pidFile(options) {
|
|
117
|
+
return join(options.stateDir, "ui.pid");
|
|
118
|
+
}
|
|
119
|
+
function logFile(options) {
|
|
120
|
+
return join(options.stateDir, "ui.log");
|
|
121
|
+
}
|
|
122
|
+
function healthUrl(options) {
|
|
123
|
+
return `http://${options.host}:${String(options.port)}/api/health`;
|
|
124
|
+
}
|
|
125
|
+
function lifecycleBaseUrl(options) {
|
|
126
|
+
return healthUrl(options).replace("/api/health", "");
|
|
127
|
+
}
|
|
128
|
+
function healthVersion(payload) {
|
|
129
|
+
if (typeof payload !== "object" || payload === null)
|
|
130
|
+
return undefined;
|
|
131
|
+
const version = payload.version;
|
|
132
|
+
return typeof version === "string" ? version : undefined;
|
|
133
|
+
}
|
|
134
|
+
async function probeHealth(options, fetchImpl) {
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetchImpl(healthUrl(options), {
|
|
137
|
+
signal: AbortSignal.timeout(1_000),
|
|
138
|
+
});
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
return { reachable: false, version: undefined };
|
|
141
|
+
}
|
|
142
|
+
let body;
|
|
143
|
+
try {
|
|
144
|
+
body = await response.json();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { reachable: true, version: undefined };
|
|
148
|
+
}
|
|
149
|
+
return { reachable: true, version: healthVersion(body) };
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return { reachable: false, version: undefined };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function readPid(path) {
|
|
156
|
+
if (!existsSync(path))
|
|
157
|
+
return undefined;
|
|
158
|
+
const raw = readFileSync(path, "utf8").trim();
|
|
159
|
+
if (!/^[1-9]\d*$/.test(raw))
|
|
160
|
+
return undefined;
|
|
161
|
+
return Number(raw);
|
|
162
|
+
}
|
|
163
|
+
function defaultIsProcessAlive(pid) {
|
|
164
|
+
try {
|
|
165
|
+
process.kill(pid, 0);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EPERM";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function defaultIsPortAvailable(host, port) {
|
|
173
|
+
return new Promise((resolveAvailable) => {
|
|
174
|
+
const server = createNetServer();
|
|
175
|
+
let settled = false;
|
|
176
|
+
const settle = (available) => {
|
|
177
|
+
if (settled)
|
|
178
|
+
return;
|
|
179
|
+
settled = true;
|
|
180
|
+
server.removeAllListeners("error");
|
|
181
|
+
server.removeAllListeners("listening");
|
|
182
|
+
if (server.listening) {
|
|
183
|
+
server.close(() => {
|
|
184
|
+
resolveAvailable(available);
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
resolveAvailable(available);
|
|
189
|
+
};
|
|
190
|
+
server.once("error", () => {
|
|
191
|
+
settle(false);
|
|
192
|
+
});
|
|
193
|
+
server.once("listening", () => {
|
|
194
|
+
settle(true);
|
|
195
|
+
});
|
|
196
|
+
server.listen(port, host);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function runningPid(options, isAlive) {
|
|
200
|
+
const path = pidFile(options);
|
|
201
|
+
const pid = readPid(path);
|
|
202
|
+
if (pid === undefined) {
|
|
203
|
+
rmSync(path, { force: true });
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
if (!isAlive(pid)) {
|
|
207
|
+
rmSync(path, { force: true });
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return pid;
|
|
211
|
+
}
|
|
212
|
+
function childEnv(env) {
|
|
213
|
+
const next = {};
|
|
214
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
215
|
+
if (!Object.prototype.hasOwnProperty.call(env, key) && value !== undefined) {
|
|
216
|
+
next[key] = value;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const [key, value] of Object.entries(env)) {
|
|
220
|
+
if (value !== undefined) {
|
|
221
|
+
next[key] = value;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return next;
|
|
225
|
+
}
|
|
226
|
+
function cliEntryPath(cwd) {
|
|
227
|
+
const preferredLayout = resolvePreferredInstallLayout(cwd);
|
|
228
|
+
if (preferredLayout !== undefined)
|
|
229
|
+
return preferredLayout.binPath;
|
|
230
|
+
// The root bin entry (`dist/cli/index.js`) surfaces `KEIKO_CLI_BIN_PATH` so
|
|
231
|
+
// re-exec'd children spawned by `keiko start` invoke the published bin rather
|
|
232
|
+
// than the cli package barrel (which is not executable). The
|
|
233
|
+
// import.meta.url fallback preserves direct package-local invocation for callers
|
|
234
|
+
// that invoke runLifecycleCli without going through the published bin entry.
|
|
235
|
+
const fromEnv = process.env.KEIKO_CLI_BIN_PATH;
|
|
236
|
+
if (fromEnv !== undefined && fromEnv !== "")
|
|
237
|
+
return fromEnv;
|
|
238
|
+
return join(dirname(fileURLToPath(import.meta.url)), "index.js");
|
|
239
|
+
}
|
|
240
|
+
function defaultOpenExternal(url) {
|
|
241
|
+
const opener = process.platform === "darwin"
|
|
242
|
+
? { command: "open", args: [url] }
|
|
243
|
+
: process.platform === "win32"
|
|
244
|
+
? { command: "cmd", args: ["/c", "start", "", url] }
|
|
245
|
+
: { command: "xdg-open", args: [url] };
|
|
246
|
+
const child = spawn(opener.command, opener.args, {
|
|
247
|
+
detached: true,
|
|
248
|
+
stdio: "ignore",
|
|
249
|
+
});
|
|
250
|
+
child.unref();
|
|
251
|
+
}
|
|
252
|
+
function maybeOpenBrowser(options, io, openExternal) {
|
|
253
|
+
if (!options.openBrowser)
|
|
254
|
+
return;
|
|
255
|
+
const baseUrl = lifecycleBaseUrl(options);
|
|
256
|
+
try {
|
|
257
|
+
openExternal(baseUrl);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
io.err(`keiko start: failed to open ${baseUrl} in the default browser.\n`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function reportHealthyStart(options, io, pid, logPath, openExternal) {
|
|
264
|
+
io.out(`Keiko UI running on ${lifecycleBaseUrl(options)} (pid ${String(pid)}).\n`);
|
|
265
|
+
io.out(`Logs: ${logPath}\n`);
|
|
266
|
+
maybeOpenBrowser(options, io, openExternal);
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
function spawnUiProcess(options, env, deps, cwd) {
|
|
270
|
+
mkdirSync(options.stateDir, { recursive: true, mode: 0o700 });
|
|
271
|
+
const logPath = logFile(options);
|
|
272
|
+
const fd = openSync(logPath, "a", 0o600);
|
|
273
|
+
const preferredLayout = resolvePreferredInstallLayout(cwd);
|
|
274
|
+
const uiEnv = childEnv({
|
|
275
|
+
...env,
|
|
276
|
+
KEIKO_STATE_DIR: options.stateDir,
|
|
277
|
+
...(preferredLayout === undefined
|
|
278
|
+
? {}
|
|
279
|
+
: {
|
|
280
|
+
KEIKO_CLI_BIN_PATH: preferredLayout.binPath,
|
|
281
|
+
KEIKO_UI_STATIC_ROOT: preferredLayout.staticRoot,
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
try {
|
|
285
|
+
return {
|
|
286
|
+
child: deps.spawnFn(process.execPath, [cliEntryPath(cwd), "ui", "--port", String(options.port), "--host", options.host], {
|
|
287
|
+
argv0: KEIKO_PROCESS_TITLE,
|
|
288
|
+
cwd,
|
|
289
|
+
detached: true,
|
|
290
|
+
env: uiEnv,
|
|
291
|
+
stdio: ["ignore", fd, fd],
|
|
292
|
+
}),
|
|
293
|
+
logPath,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
closeSync(fd);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function waitForHealth(options, pid, deps) {
|
|
301
|
+
const deadline = Date.now() + options.startTimeoutMs;
|
|
302
|
+
while (Date.now() <= deadline) {
|
|
303
|
+
if (!deps.isProcessAlive(pid))
|
|
304
|
+
return false;
|
|
305
|
+
const health = await probeHealth(options, deps.fetchImpl);
|
|
306
|
+
if (health.version === SDK_VERSION && deps.isProcessAlive(pid)) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
await deps.sleep(500);
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
async function ensureStartPortAvailable(options, io, deps) {
|
|
314
|
+
if (await deps.isPortAvailable(options.host, options.port))
|
|
315
|
+
return true;
|
|
316
|
+
io.err(`keiko start: port ${options.host}:${String(options.port)} is already in use. Stop the existing process or choose another port with --port.\n`);
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
async function cmdStart(options, io, env, deps, cwd) {
|
|
320
|
+
const running = runningPid(options, deps.isProcessAlive);
|
|
321
|
+
if (running !== undefined) {
|
|
322
|
+
const health = await probeHealth(options, deps.fetchImpl);
|
|
323
|
+
if (health.version === SDK_VERSION) {
|
|
324
|
+
io.out(`Keiko UI already running on ${lifecycleBaseUrl(options)} (pid ${String(running)}).\n`);
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
io.out(`Keiko UI process is stale (${staleProcessReason(health)}); restarting pid ${String(running)}.\n`);
|
|
328
|
+
const stopped = await cmdStop(options, io, deps);
|
|
329
|
+
if (stopped !== 0)
|
|
330
|
+
return stopped;
|
|
331
|
+
}
|
|
332
|
+
if (!(await ensureStartPortAvailable(options, io, deps)))
|
|
333
|
+
return 1;
|
|
334
|
+
const { child, logPath } = spawnUiProcess(options, env, deps, cwd);
|
|
335
|
+
if (child.pid === undefined) {
|
|
336
|
+
io.err("keiko start: failed to spawn the UI process.\n");
|
|
337
|
+
return 1;
|
|
338
|
+
}
|
|
339
|
+
child.unref();
|
|
340
|
+
writeFileSync(pidFile(options), `${String(child.pid)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
341
|
+
io.out(`Starting Keiko UI on ${lifecycleBaseUrl(options)} ...\n`);
|
|
342
|
+
const healthy = await waitForHealth(options, child.pid, deps);
|
|
343
|
+
if (healthy)
|
|
344
|
+
return reportHealthyStart(options, io, child.pid, logPath, deps.openExternal);
|
|
345
|
+
deps.killProcess(child.pid, "SIGTERM");
|
|
346
|
+
rmSync(pidFile(options), { force: true });
|
|
347
|
+
io.err(`keiko start: UI did not become healthy. Logs: ${logPath}\n`);
|
|
348
|
+
return 1;
|
|
349
|
+
}
|
|
350
|
+
async function cmdStop(options, io, deps) {
|
|
351
|
+
const pid = runningPid(options, deps.isProcessAlive);
|
|
352
|
+
if (pid === undefined) {
|
|
353
|
+
io.out("Keiko UI is not running.\n");
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
io.out(`Stopping Keiko UI (pid ${String(pid)}) ...\n`);
|
|
357
|
+
deps.killProcess(pid, "SIGTERM");
|
|
358
|
+
const deadline = Date.now() + options.stopTimeoutMs;
|
|
359
|
+
while (Date.now() <= deadline) {
|
|
360
|
+
if (!deps.isProcessAlive(pid)) {
|
|
361
|
+
rmSync(pidFile(options), { force: true });
|
|
362
|
+
io.out("Keiko UI stopped.\n");
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
await deps.sleep(500);
|
|
366
|
+
}
|
|
367
|
+
io.err("keiko stop: UI did not exit gracefully; sending SIGKILL.\n");
|
|
368
|
+
deps.killProcess(pid, "SIGKILL");
|
|
369
|
+
// Sleep at most 500 ms but respect whatever budget remains in stopTimeoutMs.
|
|
370
|
+
await deps.sleep(Math.max(0, Math.min(500, deadline - Date.now())));
|
|
371
|
+
if (deps.isProcessAlive(pid)) {
|
|
372
|
+
io.err(`keiko stop: failed to stop pid ${String(pid)}.\n`);
|
|
373
|
+
return 1;
|
|
374
|
+
}
|
|
375
|
+
rmSync(pidFile(options), { force: true });
|
|
376
|
+
io.out("Keiko UI stopped (forced).\n");
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
function cmdStatus(options, io, isAlive) {
|
|
380
|
+
const pid = runningPid(options, isAlive);
|
|
381
|
+
if (pid === undefined) {
|
|
382
|
+
io.out("Keiko UI is not running.\n");
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
io.out(`Keiko UI is running on ${lifecycleBaseUrl(options)} (pid ${String(pid)}).\n`);
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
async function cmdRestart(options, io, env, deps, cwd) {
|
|
389
|
+
const stopped = await cmdStop(options, io, deps);
|
|
390
|
+
if (stopped !== 0)
|
|
391
|
+
return stopped;
|
|
392
|
+
return cmdStart(options, io, env, deps, cwd);
|
|
393
|
+
}
|
|
394
|
+
function runtimeDeps(deps) {
|
|
395
|
+
return {
|
|
396
|
+
spawnFn: deps.spawnFn ?? spawn,
|
|
397
|
+
fetchImpl: deps.fetchImpl ?? fetch,
|
|
398
|
+
sleep: deps.sleep ??
|
|
399
|
+
((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms))),
|
|
400
|
+
isProcessAlive: deps.isProcessAlive ?? defaultIsProcessAlive,
|
|
401
|
+
killProcess: deps.killProcess ?? process.kill.bind(process),
|
|
402
|
+
isPortAvailable: deps.isPortAvailable ?? defaultIsPortAvailable,
|
|
403
|
+
openExternal: deps.openExternal ?? defaultOpenExternal,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
export async function runLifecycleCli(command, args, io, env, deps = {}) {
|
|
407
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
408
|
+
const options = parseLifecycleArgs(args, cwd, env);
|
|
409
|
+
if (options === "help") {
|
|
410
|
+
io.out(USAGE);
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
if (options === null) {
|
|
414
|
+
io.err(USAGE);
|
|
415
|
+
return 2;
|
|
416
|
+
}
|
|
417
|
+
const fullDeps = runtimeDeps(deps);
|
|
418
|
+
const handlers = {
|
|
419
|
+
start: () => cmdStart(options, io, env, fullDeps, cwd),
|
|
420
|
+
stop: () => cmdStop(options, io, fullDeps),
|
|
421
|
+
status: () => Promise.resolve(cmdStatus(options, io, fullDeps.isProcessAlive)),
|
|
422
|
+
restart: () => cmdRestart(options, io, env, fullDeps, cwd),
|
|
423
|
+
};
|
|
424
|
+
return handlers[command]();
|
|
425
|
+
}
|
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type MemoryVaultStore } from "@oscharko-dev/keiko-memory-vault";
|
|
2
|
+
import { type MemoryEmbedder } from "@oscharko-dev/keiko-server";
|
|
3
|
+
import { type EvidenceStore } from "@oscharko-dev/keiko-evidence";
|
|
4
|
+
import { type EnvSource } from "@oscharko-dev/keiko-model-gateway";
|
|
5
|
+
import type { CliIo } from "./runner.js";
|
|
6
|
+
export interface MemoryCliDeps {
|
|
7
|
+
readonly vault?: MemoryVaultStore | undefined;
|
|
8
|
+
readonly openVault?: ((memoryDir: string | undefined, env: EnvSource) => MemoryVaultStore) | undefined;
|
|
9
|
+
readonly evidenceStore?: EvidenceStore | undefined;
|
|
10
|
+
readonly redactString?: ((input: string) => string) | undefined;
|
|
11
|
+
readonly embedText?: MemoryEmbedder | null | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare function runMemoryCli(rest: readonly string[], io: CliIo, env?: EnvSource, deps?: MemoryCliDeps): number | Promise<number>;
|
|
14
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAcA,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAC5F,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,mCAAmC,CAAC;AAG3C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AA2BzC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,SAAS,CAAC,EACf,CAAC,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,SAAS,KAAK,gBAAgB,CAAC,GACrE,SAAS,CAAC;IACd,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,CAAC;IAChE,QAAQ,CAAC,SAAS,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC;CACxD;AAoSD,wBAAgB,YAAY,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,KAAK,EACT,GAAG,GAAE,SAAc,EACnB,IAAI,GAAE,aAAkB,GACvB,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO1B"}
|