@netnodeag/kraftwerk 0.75.0 → 0.75.1
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/cli/daemon-control.js +29 -12
- package/dist/cli/daemon-login.js +3 -3
- package/dist/cli/daemon.js +5 -4
- package/dist/cli/doctor.js +4 -3
- package/dist/core/daemon.d.ts +15 -1
- package/dist/core/daemon.js +46 -8
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, openSync, promises as fs } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import { daemonLogPath, findDaemon, readDaemonFile } from "../core/daemon.js";
|
|
5
|
+
import { daemonLogPath, daemonState, findDaemon, readDaemonFile } from "../core/daemon.js";
|
|
6
6
|
import { tildify } from "../core/instances.js";
|
|
7
7
|
import { selfCommand } from "../core/self-command.js";
|
|
8
8
|
import { getPkgVersion } from "../server/version.js";
|
|
@@ -42,7 +42,7 @@ const ago = (iso) => {
|
|
|
42
42
|
};
|
|
43
43
|
async function getJson(port, p) {
|
|
44
44
|
try {
|
|
45
|
-
const r = await fetch(`http://127.0.0.1:${port}${p}`, { signal: AbortSignal.timeout(
|
|
45
|
+
const r = await fetch(`http://127.0.0.1:${port}${p}`, { signal: AbortSignal.timeout(3000) });
|
|
46
46
|
return r.ok ? (await r.json()) : null;
|
|
47
47
|
}
|
|
48
48
|
catch {
|
|
@@ -62,10 +62,14 @@ async function tail(n) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
async function status() {
|
|
65
|
-
const [
|
|
65
|
+
const [found, rec, installed] = await Promise.all([daemonState(), readDaemonFile(), getPkgVersion()]);
|
|
66
66
|
const base = { installed, loginItem: daemonLoginItem() !== null, log: daemonLogPath() };
|
|
67
|
-
if (
|
|
67
|
+
if (found.state === "stopped")
|
|
68
68
|
return { running: false, ...base };
|
|
69
|
+
const running = found;
|
|
70
|
+
if (found.state === "busy") {
|
|
71
|
+
return { running: true, busy: true, ...base, pid: found.pid, port: found.port, ...(rec?.version ? { version: rec.version } : {}), ...(rec?.startedAt ? { startedAt: rec.startedAt } : {}) };
|
|
72
|
+
}
|
|
69
73
|
const [hub, relay] = await Promise.all([
|
|
70
74
|
getJson(running.port, "/api/hub"),
|
|
71
75
|
getJson(running.port, "/api/devices/relay"),
|
|
@@ -87,7 +91,12 @@ export async function daemonStatus(opts) {
|
|
|
87
91
|
console.log(JSON.stringify(s, null, 2));
|
|
88
92
|
process.exit(s.running ? 0 : NOT_RUNNING);
|
|
89
93
|
}
|
|
90
|
-
if (s.
|
|
94
|
+
if (s.busy) {
|
|
95
|
+
const up = ago(s.startedAt);
|
|
96
|
+
console.log(`${chalk.yellow("●")} kraftwerk daemon ${chalk.yellow("running, but slow to answer")} ${chalk.dim(`pid ${s.pid}, port ${s.port}${up ? `, up ${up}` : ""}${s.version ? `, ${s.version}` : ""}`)}`);
|
|
97
|
+
console.log(chalk.dim(" its process lives and holds its port — busy (agents at work, a loaded machine); ask again in a moment"));
|
|
98
|
+
}
|
|
99
|
+
else if (s.running) {
|
|
91
100
|
const up = ago(s.startedAt);
|
|
92
101
|
const behind = s.version && s.version !== s.installed ? chalk.yellow(` — ${s.installed} is installed: \`kraftwerk daemon restart\``) : "";
|
|
93
102
|
console.log(`${chalk.green("●")} kraftwerk daemon ${chalk.green("running")} ${chalk.dim(`pid ${s.pid}${up ? `, up ${up}` : ""}${s.version ? `, ${s.version}` : ""}`)}${behind}`);
|
|
@@ -138,9 +147,10 @@ async function launch(port) {
|
|
|
138
147
|
return "background";
|
|
139
148
|
}
|
|
140
149
|
export async function startDaemon(opts) {
|
|
141
|
-
const running = await
|
|
142
|
-
if (running) {
|
|
143
|
-
|
|
150
|
+
const running = await daemonState();
|
|
151
|
+
if (running.state !== "stopped") {
|
|
152
|
+
const busy = running.state === "busy" ? ", busy — slow to answer right now" : "";
|
|
153
|
+
console.log(`${chalk.green("✔")} the daemon already runs ${chalk.dim(`(pid ${running.pid}, http://localhost:${running.port}${busy})`)}`);
|
|
144
154
|
return;
|
|
145
155
|
}
|
|
146
156
|
const how = await launch(opts.port);
|
|
@@ -157,9 +167,10 @@ export async function startDaemon(opts) {
|
|
|
157
167
|
}
|
|
158
168
|
/** Stop it: its supervisor first (else it would start the daemon again), then wait until it is gone. */
|
|
159
169
|
async function stopRunning() {
|
|
160
|
-
const [
|
|
161
|
-
if (
|
|
170
|
+
const [found, rec] = await Promise.all([daemonState(), readDaemonFile()]);
|
|
171
|
+
if (found.state === "stopped")
|
|
162
172
|
return false;
|
|
173
|
+
const running = found;
|
|
163
174
|
const target = rec?.pid === running.pid && alive(rec.supervisor) ? rec.supervisor : running.pid;
|
|
164
175
|
process.kill(target, "SIGTERM");
|
|
165
176
|
const gone = await until(async () => !alive(running.pid) && !alive(target) && (await findDaemon()) === null, 20_000);
|
|
@@ -182,9 +193,15 @@ export async function stopDaemon() {
|
|
|
182
193
|
}
|
|
183
194
|
/** Restart: the running daemon relaunches itself with the current install (its supervisor stays); none running — start one. */
|
|
184
195
|
export async function restartDaemon(opts) {
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
196
|
+
const found = await daemonState();
|
|
197
|
+
if (found.state === "stopped")
|
|
187
198
|
return startDaemon(opts);
|
|
199
|
+
if (found.state === "busy") {
|
|
200
|
+
// It does not answer, so it cannot be asked to relaunch: stopping it is the only way (its agents are interrupted).
|
|
201
|
+
await stopRunning();
|
|
202
|
+
return startDaemon(opts);
|
|
203
|
+
}
|
|
204
|
+
const running = found;
|
|
188
205
|
// The restart route lives in every workspace (the daemon's root answers machine routes only); any open one will do.
|
|
189
206
|
const hub = await getJson(running.port, "/api/hub");
|
|
190
207
|
const slug = hub?.workspaces?.[0]?.slug;
|
package/dist/cli/daemon-login.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, promises as fs } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
-
import { daemonLogPath,
|
|
6
|
+
import { daemonLogPath, daemonState } from "../core/daemon.js";
|
|
7
7
|
import { selfCommand } from "../core/self-command.js";
|
|
8
8
|
import { tildify } from "../core/instances.js";
|
|
9
9
|
/**
|
|
@@ -72,8 +72,8 @@ export async function installDaemon(opts) {
|
|
|
72
72
|
await fs.writeFile(file, renderPlist([cmd, ...args], env, logPath()));
|
|
73
73
|
// An older version of the item is replaced: unload it first (an error just means it was not loaded).
|
|
74
74
|
spawnSync(launchctl(), ["bootout", `${domain()}/${LAUNCH_LABEL}`], { stdio: "ignore" });
|
|
75
|
-
const running = await
|
|
76
|
-
if (running) {
|
|
75
|
+
const running = await daemonState();
|
|
76
|
+
if (running.state !== "stopped") {
|
|
77
77
|
// Loading now would start a second daemon, which refuses to run. launchd loads the file at the next login.
|
|
78
78
|
console.log(`${chalk.green("✔")} login item installed: ${chalk.dim(tildify(file))}`);
|
|
79
79
|
console.log(chalk.dim(` a daemon already runs (pid ${running.pid}); from the next login on, launchd starts it`));
|
package/dist/cli/daemon.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { DAEMON_PORT,
|
|
2
|
+
import { DAEMON_PORT, daemonState, lastOpenRoots, setDaemonVersion, writeDaemonFile } from "../core/daemon.js";
|
|
3
3
|
import { tildify } from "../core/instances.js";
|
|
4
4
|
import { networkUrls } from "../server/bind.js";
|
|
5
5
|
import { startPushWatcher } from "../server/push.js";
|
|
@@ -22,9 +22,10 @@ export async function runDaemon(opts) {
|
|
|
22
22
|
process.env.KRAFTWERK_UI_HOST ||= "0.0.0.0";
|
|
23
23
|
if (process.env.KRAFTWERK_UI_SUPERVISED !== "1")
|
|
24
24
|
return supervise(["daemon", ...(opts.port ? ["--port", opts.port] : [])], "the daemon");
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// A busy one counts too: a second daemon would only crash on its port.
|
|
26
|
+
const running = await daemonState();
|
|
27
|
+
if (running.state !== "stopped" && running.pid !== process.pid) {
|
|
28
|
+
console.error(chalk.red(`✖ a kraftwerk daemon already runs (pid ${running.pid}, port ${running.port}${running.state === "busy" ? ", busy right now" : ""})`));
|
|
28
29
|
process.exit(1);
|
|
29
30
|
}
|
|
30
31
|
const port = Number(opts.port ?? process.env.KRAFTWERK_DAEMON_PORT ?? DAEMON_PORT);
|
package/dist/cli/doctor.js
CHANGED
|
@@ -9,7 +9,7 @@ import { applyDotenv, DOTENV_FILE } from "../dotenv.js";
|
|
|
9
9
|
import { legacyFindings, versionLt } from "./doctor-legacy.js";
|
|
10
10
|
import { listDevices } from "../core/devices.js";
|
|
11
11
|
import { slugClashes } from "../core/instances.js";
|
|
12
|
-
import {
|
|
12
|
+
import { daemonState } from "../core/daemon.js";
|
|
13
13
|
import { daemonLoginItem } from "./daemon-login.js";
|
|
14
14
|
import { readRelaySettings } from "../server/relay.js";
|
|
15
15
|
const ICONS = {
|
|
@@ -214,9 +214,10 @@ export async function runDoctor(cwd) {
|
|
|
214
214
|
report("warn", "KRAFTWERK_UI_TRUST_NETWORK=1", "anyone who reaches the UI over the network uses it without pairing — unset it and pair devices instead (`kraftwerk devices pair`)");
|
|
215
215
|
report("info", devices.length ? `${devices.length} paired device(s)` : "no paired devices", devices.length ? devices.map((d) => d.name).join(", ") : "a phone or another computer can use kraftwerk once paired: `kraftwerk ui --lan`, then `kraftwerk devices pair`");
|
|
216
216
|
// The daemon: whether one runs, and whether it starts at login.
|
|
217
|
-
const
|
|
217
|
+
const state = await daemonState();
|
|
218
|
+
const daemon = state.state === "stopped" ? null : state;
|
|
218
219
|
const loginItem = daemonLoginItem();
|
|
219
|
-
report("info", daemon ? `kraftwerk daemon running on :${daemon.port}` :
|
|
220
|
+
report("info", !daemon ? "no kraftwerk daemon running" : daemon.state === "busy" ? `kraftwerk daemon running on :${daemon.port}, but slow to answer (busy)` : `kraftwerk daemon running on :${daemon.port}`, loginItem
|
|
220
221
|
? daemon
|
|
221
222
|
? "starts at login · `kraftwerk daemon status` for more"
|
|
222
223
|
: "starts at login, but not running now · `kraftwerk daemon start`, `kraftwerk daemon logs` says why it stopped"
|
package/dist/core/daemon.d.ts
CHANGED
|
@@ -29,10 +29,24 @@ export declare function lastOpenRoots(): Promise<string[]>;
|
|
|
29
29
|
/** The daemon file as the last daemon left it; null without one. */
|
|
30
30
|
export declare function readDaemonFile(): Promise<DaemonFile | null>;
|
|
31
31
|
/** The running daemon (its file, and a hub that answers on its port), or null. */
|
|
32
|
-
export declare function findDaemon(): Promise<{
|
|
32
|
+
export declare function findDaemon(timeoutMs?: number): Promise<{
|
|
33
33
|
pid: number;
|
|
34
34
|
port: number;
|
|
35
35
|
} | null>;
|
|
36
|
+
export type DaemonState = {
|
|
37
|
+
state: "running" | "busy";
|
|
38
|
+
pid: number;
|
|
39
|
+
port: number;
|
|
40
|
+
} | {
|
|
41
|
+
state: "stopped";
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Whether a daemon runs, telling a slow one from a stopped one: "busy" is a
|
|
45
|
+
* daemon whose process lives and whose port is taken but that did not answer
|
|
46
|
+
* in time. A busy daemon is never "stopped" — starting another would only
|
|
47
|
+
* crash on the taken port, and restarting it would interrupt its agents.
|
|
48
|
+
*/
|
|
49
|
+
export declare function daemonState(timeoutMs?: number): Promise<DaemonState>;
|
|
36
50
|
/** Ask the daemon to open or close a workspace (this machine's request: local trust). */
|
|
37
51
|
export declare function daemonCall<T>(port: number, verb: "open" | "close", root: string): Promise<{
|
|
38
52
|
ok: boolean;
|
package/dist/core/daemon.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
|
+
import net from "node:net";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
/**
|
|
@@ -47,18 +48,20 @@ export async function readDaemonFile() {
|
|
|
47
48
|
return null;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* How long a daemon may take to answer before it counts as not there. A
|
|
53
|
+
* daemon under load (agents at work, a busy machine) can take a second or
|
|
54
|
+
* two; 0.8 s once read such a one as stopped.
|
|
55
|
+
*/
|
|
56
|
+
const ANSWER_MS = 3000;
|
|
50
57
|
/** The running daemon (its file, and a hub that answers on its port), or null. */
|
|
51
|
-
export async function findDaemon() {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
rec = JSON.parse(await fs.readFile(daemonFile(), "utf8"));
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
58
|
+
export async function findDaemon(timeoutMs = ANSWER_MS) {
|
|
59
|
+
const rec = await readDaemonFile();
|
|
60
|
+
if (!rec)
|
|
57
61
|
return null;
|
|
58
|
-
}
|
|
59
62
|
try {
|
|
60
63
|
// Every kraftwerk server answers /api/hub; only the daemon says it is one (a standalone `kraftwerk ui` may sit on that port now).
|
|
61
|
-
const r = await fetch(`http://127.0.0.1:${rec.port}/api/hub`, { signal: AbortSignal.timeout(
|
|
64
|
+
const r = await fetch(`http://127.0.0.1:${rec.port}/api/hub`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
62
65
|
const body = r.ok ? (await r.json()) : null;
|
|
63
66
|
return body?.daemon === true ? { pid: rec.pid, port: rec.port } : null;
|
|
64
67
|
}
|
|
@@ -66,6 +69,41 @@ export async function findDaemon() {
|
|
|
66
69
|
return null;
|
|
67
70
|
}
|
|
68
71
|
}
|
|
72
|
+
const pidAlive = (pid) => {
|
|
73
|
+
try {
|
|
74
|
+
process.kill(pid, 0);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
// EPERM: it exists, it is just not ours to signal.
|
|
79
|
+
return err.code === "EPERM";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const portTaken = (port) => new Promise((resolve) => {
|
|
83
|
+
const sock = net.connect({ host: "127.0.0.1", port });
|
|
84
|
+
const done = (taken) => {
|
|
85
|
+
sock.destroy();
|
|
86
|
+
resolve(taken);
|
|
87
|
+
};
|
|
88
|
+
sock.setTimeout(1000, () => done(true)); // accepted nothing yet, refused nothing either: something holds it
|
|
89
|
+
sock.once("connect", () => done(true));
|
|
90
|
+
sock.once("error", () => done(false));
|
|
91
|
+
});
|
|
92
|
+
/**
|
|
93
|
+
* Whether a daemon runs, telling a slow one from a stopped one: "busy" is a
|
|
94
|
+
* daemon whose process lives and whose port is taken but that did not answer
|
|
95
|
+
* in time. A busy daemon is never "stopped" — starting another would only
|
|
96
|
+
* crash on the taken port, and restarting it would interrupt its agents.
|
|
97
|
+
*/
|
|
98
|
+
export async function daemonState(timeoutMs = ANSWER_MS) {
|
|
99
|
+
const found = await findDaemon(timeoutMs);
|
|
100
|
+
if (found)
|
|
101
|
+
return { state: "running", ...found };
|
|
102
|
+
const rec = await readDaemonFile();
|
|
103
|
+
if (rec && pidAlive(rec.pid) && (await portTaken(rec.port)))
|
|
104
|
+
return { state: "busy", pid: rec.pid, port: rec.port };
|
|
105
|
+
return { state: "stopped" };
|
|
106
|
+
}
|
|
69
107
|
/** Ask the daemon to open or close a workspace (this machine's request: local trust). */
|
|
70
108
|
export async function daemonCall(port, verb, root) {
|
|
71
109
|
const r = await fetch(`http://127.0.0.1:${port}/api/hub/${verb}`, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netnodeag/kraftwerk",
|
|
3
|
-
"version": "0.75.
|
|
3
|
+
"version": "0.75.1",
|
|
4
4
|
"description": "Open-source agentic workspace for teams — where people and AI agents get work done together: agents (Claude Code, Codex, Pi), skills, shared knowledge, repeatable workflows, and deterministic verification",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|