@netnodeag/kraftwerk 0.67.0 → 0.69.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 +24 -15
- package/dist/cli/daemon-login.d.ts +19 -0
- package/dist/cli/daemon-login.js +106 -0
- package/dist/cli/daemon.js +19 -2
- package/dist/cli/doctor.js +12 -0
- package/dist/server/server.d.ts +2 -0
- package/dist/server/server.js +28 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,11 +65,12 @@ foreground; Ctrl-C stops it. That is all most people need.
|
|
|
65
65
|
|
|
66
66
|
**Several workspaces: `kraftwerk daemon`.** With more than one workspace,
|
|
67
67
|
run one kraftwerk for all of them instead of a server and a port each.
|
|
68
|
-
Start the daemon once and leave it running
|
|
69
|
-
|
|
68
|
+
Start the daemon once and leave it running, or let your Mac start it at
|
|
69
|
+
login:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
kraftwerk daemon # the machine's kraftwerk, on http://localhost:1980
|
|
73
|
+
kraftwerk daemon install # or: start it at login from now on (macOS; `uninstall` undoes it)
|
|
73
74
|
|
|
74
75
|
cd ~/work/team-blau && kraftwerk ui # opens this workspace in the daemon:
|
|
75
76
|
# http://team-blau.localhost:1980
|
|
@@ -94,13 +95,18 @@ you when it opens.
|
|
|
94
95
|
`kraftwerk ui` hands the workspace to the daemon whenever one is running;
|
|
95
96
|
`kraftwerk ui --standalone` starts a server of its own anyway.
|
|
96
97
|
|
|
97
|
-
**From your phone or another computer
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
**From your phone or another computer** on the same network:
|
|
99
|
+
|
|
100
|
+
1. Let the daemon listen on your network: `kraftwerk daemon install --lan`
|
|
101
|
+
(or run `kraftwerk daemon --lan`). It prints the address, for example
|
|
102
|
+
`http://192.168.1.20:1980`.
|
|
103
|
+
2. Open that address on the phone. It shows a pairing form, nothing else.
|
|
104
|
+
3. On this computer, run `kraftwerk devices pair` (or settings → devices →
|
|
105
|
+
pair a device) and type the code on the phone.
|
|
106
|
+
|
|
107
|
+
The phone now sees your workspaces, each at `/w/<slug>/` on that address,
|
|
108
|
+
and the workspace menu switches between them. One pairing covers all of them
|
|
109
|
+
(see [Inspector on your phone](#inspector-on-your-phone-paired-devices)).
|
|
104
110
|
|
|
105
111
|
**What to do first in the inspector.** Talk to **Ralv**, the workspace's
|
|
106
112
|
general chat, in plain words: it knows what is in the workspace and helps
|
|
@@ -230,6 +236,7 @@ kraftwerk runs # past runs from output/*/trace.jsonl; r
|
|
|
230
236
|
kraftwerk knowledge # Knowledge: OKF bundles (list/get/put/verify/search/...)
|
|
231
237
|
kraftwerk ui # start this workspace: http://localhost:1981 (or open it in the daemon); --port, --lan, --standalone
|
|
232
238
|
kraftwerk daemon # one kraftwerk for every open workspace: http://<slug>.localhost:1980; --port, --lan
|
|
239
|
+
kraftwerk daemon install # start the daemon at login (macOS LaunchAgent); --lan, --port; `daemon uninstall` removes it
|
|
233
240
|
kraftwerk workspaces # every workspace on this machine; workspaces start|stop|forget <ref>
|
|
234
241
|
kraftwerk devices # paired devices (a phone, another computer); devices pair|revoke
|
|
235
242
|
kraftwerk projects # goal-scoped project folders; projects create|show|link|log|remove
|
|
@@ -382,13 +389,15 @@ follows to author the workflow folder and validate and smoke it with this CLI.
|
|
|
382
389
|
|
|
383
390
|
### Inspector on your phone (paired devices)
|
|
384
391
|
|
|
385
|
-
`kraftwerk ui --lan` (or `kraftwerk daemon --lan`
|
|
386
|
-
network and prints the
|
|
387
|
-
On the daemon, a device
|
|
392
|
+
`kraftwerk ui --lan` (or `kraftwerk daemon --lan`, `kraftwerk daemon install
|
|
393
|
+
--lan` for the login item) also listens on your network and prints the
|
|
394
|
+
addresses (for example `http://192.168.1.20:1981`). On the daemon, a device
|
|
395
|
+
opens a workspace at `/w/<slug>/` on that address
|
|
388
396
|
(`http://192.168.1.20:1980/w/team-blau/`), because `<slug>.localhost` only
|
|
389
|
-
works on this machine. The daemon's own address lists them
|
|
390
|
-
|
|
391
|
-
|
|
397
|
+
works on this machine. The daemon's own address lists them once the device
|
|
398
|
+
is paired, and pairs it when it is not, and the workspace menu links the
|
|
399
|
+
others the same way. Anyone who opens one gets a pairing screen, nothing
|
|
400
|
+
else. To let a device in, make a one-time code on
|
|
392
401
|
the machine kraftwerk runs on, with `kraftwerk devices pair` or settings →
|
|
393
402
|
devices → pair a device, and enter it on the device. The code works once,
|
|
394
403
|
for 10 minutes. The device stays paired until you unpair it in the same
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kraftwerk daemon install` / `uninstall`: the daemon as a macOS login item
|
|
3
|
+
* (a LaunchAgent), so it runs after a reboot without a terminal or the
|
|
4
|
+
* desktop app. launchd starts the same `kraftwerk daemon` a terminal would —
|
|
5
|
+
* this node, this install — with the PATH of the shell that installed it,
|
|
6
|
+
* because launchd's own PATH would not find the agents (claude, codex, pi).
|
|
7
|
+
* The daemon's supervisor restarts it after a self-update; launchd only
|
|
8
|
+
* starts it at login.
|
|
9
|
+
*/
|
|
10
|
+
export declare const LAUNCH_LABEL = "ch.netnode.kraftwerk.daemon";
|
|
11
|
+
/** The LaunchAgent: `program` at login, `env` set, output appended to `log`. */
|
|
12
|
+
export declare function renderPlist(program: string[], env: Record<string, string>, log: string): string;
|
|
13
|
+
export declare function installDaemon(opts: {
|
|
14
|
+
port?: string;
|
|
15
|
+
lan?: boolean;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
export declare function uninstallDaemon(): Promise<void>;
|
|
18
|
+
/** For doctor: whether the login item is installed. */
|
|
19
|
+
export declare const daemonLoginItem: () => string | null;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, promises as fs } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { findDaemon } from "../core/daemon.js";
|
|
7
|
+
import { selfCommand } from "../core/self-command.js";
|
|
8
|
+
import { tildify } from "../core/instances.js";
|
|
9
|
+
/**
|
|
10
|
+
* `kraftwerk daemon install` / `uninstall`: the daemon as a macOS login item
|
|
11
|
+
* (a LaunchAgent), so it runs after a reboot without a terminal or the
|
|
12
|
+
* desktop app. launchd starts the same `kraftwerk daemon` a terminal would —
|
|
13
|
+
* this node, this install — with the PATH of the shell that installed it,
|
|
14
|
+
* because launchd's own PATH would not find the agents (claude, codex, pi).
|
|
15
|
+
* The daemon's supervisor restarts it after a self-update; launchd only
|
|
16
|
+
* starts it at login.
|
|
17
|
+
*/
|
|
18
|
+
export const LAUNCH_LABEL = "ch.netnode.kraftwerk.daemon";
|
|
19
|
+
const plistPath = () => path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
|
|
20
|
+
const logPath = () => path.join(os.homedir(), ".kraftwerk", "logs", "daemon.log");
|
|
21
|
+
/** A stand-in launchctl (tests); the real one otherwise. */
|
|
22
|
+
const launchctl = () => process.env.KRAFTWERK_LAUNCHCTL || "launchctl";
|
|
23
|
+
const domain = () => `gui/${os.userInfo().uid}`;
|
|
24
|
+
const xml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
25
|
+
/** The LaunchAgent: `program` at login, `env` set, output appended to `log`. */
|
|
26
|
+
export function renderPlist(program, env, log) {
|
|
27
|
+
const strings = (xs) => xs.map((x) => `\t\t<string>${xml(x)}</string>`).join("\n");
|
|
28
|
+
const vars = Object.entries(env)
|
|
29
|
+
.map(([k, v]) => `\t\t<key>${xml(k)}</key>\n\t\t<string>${xml(v)}</string>`)
|
|
30
|
+
.join("\n");
|
|
31
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
32
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
33
|
+
<plist version="1.0">
|
|
34
|
+
<dict>
|
|
35
|
+
\t<key>Label</key>
|
|
36
|
+
\t<string>${LAUNCH_LABEL}</string>
|
|
37
|
+
\t<key>ProgramArguments</key>
|
|
38
|
+
\t<array>
|
|
39
|
+
${strings(program)}
|
|
40
|
+
\t</array>
|
|
41
|
+
\t<key>EnvironmentVariables</key>
|
|
42
|
+
\t<dict>
|
|
43
|
+
${vars}
|
|
44
|
+
\t</dict>
|
|
45
|
+
\t<key>WorkingDirectory</key>
|
|
46
|
+
\t<string>${xml(os.homedir())}</string>
|
|
47
|
+
\t<key>RunAtLoad</key>
|
|
48
|
+
\t<true/>
|
|
49
|
+
\t<key>StandardOutPath</key>
|
|
50
|
+
\t<string>${xml(log)}</string>
|
|
51
|
+
\t<key>StandardErrorPath</key>
|
|
52
|
+
\t<string>${xml(log)}</string>
|
|
53
|
+
</dict>
|
|
54
|
+
</plist>
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
function requireMac() {
|
|
58
|
+
if (process.platform !== "darwin") {
|
|
59
|
+
console.error(chalk.red("✖ a login item is macOS only for now — run `kraftwerk daemon` from your system's startup instead"));
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function installDaemon(opts) {
|
|
64
|
+
requireMac();
|
|
65
|
+
const { cmd, args } = selfCommand(["daemon", ...(opts.port ? ["--port", opts.port] : []), ...(opts.lan ? ["--lan"] : [])]);
|
|
66
|
+
const env = { PATH: process.env.PATH ?? "/usr/bin:/bin" };
|
|
67
|
+
if (process.env.KRAFTWERK_DAEMON_PORT && !opts.port)
|
|
68
|
+
env.KRAFTWERK_DAEMON_PORT = process.env.KRAFTWERK_DAEMON_PORT;
|
|
69
|
+
const file = plistPath();
|
|
70
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
71
|
+
await fs.mkdir(path.dirname(logPath()), { recursive: true });
|
|
72
|
+
await fs.writeFile(file, renderPlist([cmd, ...args], env, logPath()));
|
|
73
|
+
// An older version of the item is replaced: unload it first (an error just means it was not loaded).
|
|
74
|
+
spawnSync(launchctl(), ["bootout", `${domain()}/${LAUNCH_LABEL}`], { stdio: "ignore" });
|
|
75
|
+
const running = await findDaemon();
|
|
76
|
+
if (running) {
|
|
77
|
+
// Loading now would start a second daemon, which refuses to run. launchd loads the file at the next login.
|
|
78
|
+
console.log(`${chalk.green("✔")} login item installed: ${chalk.dim(tildify(file))}`);
|
|
79
|
+
console.log(chalk.dim(` a daemon already runs (pid ${running.pid}); from the next login on, launchd starts it`));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const r = spawnSync(launchctl(), ["bootstrap", domain(), file], { encoding: "utf8" });
|
|
83
|
+
if (r.status !== 0) {
|
|
84
|
+
console.error(chalk.red(`✖ launchctl bootstrap failed: ${(r.stderr || r.stdout || "").trim() || `exit ${r.status}`}`));
|
|
85
|
+
console.error(chalk.dim(` the file is in place (${tildify(file)}) and loads at the next login`));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
console.log(`${chalk.green("✔")} login item installed and started: ${chalk.dim(tildify(file))}`);
|
|
89
|
+
if (opts.lan)
|
|
90
|
+
console.log(chalk.dim(" on your network too: devices pair first (`kraftwerk devices pair`)"));
|
|
91
|
+
console.log(chalk.dim(` log: ${tildify(logPath())} · \`kraftwerk daemon uninstall\` removes it`));
|
|
92
|
+
}
|
|
93
|
+
export async function uninstallDaemon() {
|
|
94
|
+
requireMac();
|
|
95
|
+
const file = plistPath();
|
|
96
|
+
if (!existsSync(file)) {
|
|
97
|
+
console.log(chalk.dim("No login item installed."));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Unloading stops the daemon launchd started; one started from a terminal keeps running.
|
|
101
|
+
spawnSync(launchctl(), ["bootout", `${domain()}/${LAUNCH_LABEL}`], { stdio: "ignore" });
|
|
102
|
+
await fs.rm(file, { force: true });
|
|
103
|
+
console.log(`${chalk.green("✔")} login item removed ${chalk.dim(`(${tildify(file)})`)}`);
|
|
104
|
+
}
|
|
105
|
+
/** For doctor: whether the login item is installed. */
|
|
106
|
+
export const daemonLoginItem = () => (existsSync(plistPath()) ? plistPath() : null);
|
package/dist/cli/daemon.js
CHANGED
|
@@ -4,6 +4,7 @@ import { tildify } from "../core/instances.js";
|
|
|
4
4
|
import { networkUrls } from "../server/bind.js";
|
|
5
5
|
import { startHub } from "../server/server.js";
|
|
6
6
|
import { ensureBuilt, supervise } from "./ui.js";
|
|
7
|
+
import { installDaemon, uninstallDaemon } from "./daemon-login.js";
|
|
7
8
|
/**
|
|
8
9
|
* `kraftwerk daemon` — the machine's kraftwerk: one process serving every
|
|
9
10
|
* open workspace, each at `<slug>.localhost:<port>` (and its own port, when
|
|
@@ -49,12 +50,28 @@ export async function runDaemon(opts) {
|
|
|
49
50
|
console.log(`${chalk.green("✔")} On your network: ${lan.map((u) => chalk.cyan(u)).join(", ")} ${chalk.dim("(add /w/<slug>/; devices pair first)")}`);
|
|
50
51
|
}
|
|
51
52
|
export function registerDaemonCommand(program) {
|
|
52
|
-
program
|
|
53
|
+
const daemon = program
|
|
53
54
|
.command("daemon")
|
|
54
|
-
.description("Run the machine's kraftwerk: one process serving every open workspace at <slug>.localhost")
|
|
55
|
+
.description("Run the machine's kraftwerk: one process serving every open workspace at <slug>.localhost; install it as a login item")
|
|
55
56
|
.option("--port <port>", `Port (default: KRAFTWERK_DAEMON_PORT, else ${DAEMON_PORT})`)
|
|
56
57
|
.option("--lan", "Listen on the network too, for paired devices")
|
|
57
58
|
.action(async (opts) => {
|
|
58
59
|
await runDaemon(opts);
|
|
59
60
|
});
|
|
61
|
+
daemon
|
|
62
|
+
.command("install")
|
|
63
|
+
.description("Start the daemon at login (macOS LaunchAgent), and now unless one runs")
|
|
64
|
+
.option("--port <port>", `Port (default: KRAFTWERK_DAEMON_PORT, else ${DAEMON_PORT})`)
|
|
65
|
+
.option("--lan", "Listen on the network too, for paired devices (a phone on the same Wi-Fi)")
|
|
66
|
+
.action(async (opts) => {
|
|
67
|
+
// `--port`/`--lan` after `install` are taken by `daemon` itself, which has the same options.
|
|
68
|
+
const parent = daemon.opts();
|
|
69
|
+
await installDaemon({ port: opts.port ?? parent.port, lan: opts.lan ?? parent.lan });
|
|
70
|
+
});
|
|
71
|
+
daemon
|
|
72
|
+
.command("uninstall")
|
|
73
|
+
.description("Remove the login item (stops a daemon launchd started)")
|
|
74
|
+
.action(async () => {
|
|
75
|
+
await uninstallDaemon();
|
|
76
|
+
});
|
|
60
77
|
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -9,6 +9,8 @@ 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 { findDaemon } from "../core/daemon.js";
|
|
13
|
+
import { daemonLoginItem } from "./daemon-login.js";
|
|
12
14
|
const ICONS = {
|
|
13
15
|
ok: chalk.green("✔"),
|
|
14
16
|
warn: chalk.yellow("⚠"),
|
|
@@ -210,6 +212,16 @@ export async function runDoctor(cwd) {
|
|
|
210
212
|
if (process.env.KRAFTWERK_UI_TRUST_NETWORK === "1")
|
|
211
213
|
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`)");
|
|
212
214
|
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`");
|
|
215
|
+
// The daemon: whether one runs, and whether it starts at login.
|
|
216
|
+
const daemon = await findDaemon();
|
|
217
|
+
const loginItem = daemonLoginItem();
|
|
218
|
+
report("info", daemon ? `kraftwerk daemon running on :${daemon.port}` : "no kraftwerk daemon running", loginItem
|
|
219
|
+
? "starts at login (`kraftwerk daemon uninstall` removes that)"
|
|
220
|
+
: process.platform === "darwin"
|
|
221
|
+
? "`kraftwerk daemon install` starts it at login"
|
|
222
|
+
: daemon
|
|
223
|
+
? undefined
|
|
224
|
+
: "`kraftwerk daemon` serves every workspace in one process");
|
|
213
225
|
// Legacy: what older versions left behind, and what still runs or is installed at one.
|
|
214
226
|
for (const f of await legacyFindings(project, pkg.version))
|
|
215
227
|
report(f.level, f.label, f.detail);
|
package/dist/server/server.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface OpenWorkspace {
|
|
|
25
25
|
/** Its own port too (kraftwerk.yml `port`), when the hub listens there for it — old links keep working. */
|
|
26
26
|
aliasPort?: number;
|
|
27
27
|
ws: Workspace;
|
|
28
|
+
/** Its instance file being written (see openNow): closing waits for it, so no file lands after the close. */
|
|
29
|
+
registered?: Promise<void>;
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* One HTTP server serving workspaces by slug (see target.ts): a daemon
|
package/dist/server/server.js
CHANGED
|
@@ -169,8 +169,8 @@ function installExitHandlers() {
|
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
171
|
/**
|
|
172
|
-
* Listen on `port`. On the default bind (127.0.0.1)
|
|
173
|
-
* feeds the same server: macOS resolves localhost and <slug>.localhost to
|
|
172
|
+
* Listen on `port`. On the default bind (127.0.0.1), and on 0.0.0.0
|
|
173
|
+
* (`--lan`, IPv4 only), the same port on ::1 feeds the same server: macOS resolves localhost and <slug>.localhost to
|
|
174
174
|
* ::1 first, and a client that does not fall back to IPv4 (a native app's
|
|
175
175
|
* HTTP library) would find nothing there. Best effort — without IPv6, or
|
|
176
176
|
* with ::1:port taken, it stays IPv4 only, as before.
|
|
@@ -184,7 +184,7 @@ async function listen(server, port) {
|
|
|
184
184
|
resolve(typeof addr === "object" && addr ? addr.port : port);
|
|
185
185
|
});
|
|
186
186
|
});
|
|
187
|
-
if (INSPECTOR_HOST !== "127.0.0.1")
|
|
187
|
+
if (INSPECTOR_HOST !== "127.0.0.1" && INSPECTOR_HOST !== "0.0.0.0")
|
|
188
188
|
return bound;
|
|
189
189
|
const twin = net.createServer((socket) => server.emit("connection", socket));
|
|
190
190
|
const ok = await new Promise((resolve) => {
|
|
@@ -205,18 +205,35 @@ async function listen(server, port) {
|
|
|
205
205
|
/**
|
|
206
206
|
* A small page at a daemon's own address: the open workspaces, each a link —
|
|
207
207
|
* <slug>.localhost on this machine, the path form on the address a paired
|
|
208
|
-
* device used (it cannot resolve <slug>.localhost).
|
|
208
|
+
* device used (it cannot resolve <slug>.localhost). A device that is not
|
|
209
|
+
* paired yet gets a pairing form instead, and no workspace names: the code
|
|
210
|
+
* comes from this machine (`kraftwerk devices pair`), and the cookie it earns
|
|
211
|
+
* covers every workspace on this address.
|
|
209
212
|
*/
|
|
210
|
-
function hubPage(req, res, open) {
|
|
213
|
+
function hubPage(req, res, open, trust) {
|
|
211
214
|
const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
212
215
|
// Behind a proxy on this machine the browser's name is the forwarded one.
|
|
213
216
|
const host = hostnameOf(forwardedHost(req) ?? req.headers.host ?? "");
|
|
214
217
|
const local = LOOPBACK_NAMES.has(host) || host.endsWith(".localhost");
|
|
218
|
+
const head = `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>kraftwerk</title>` +
|
|
219
|
+
`<style>:root{color-scheme:light dark}body{font:16px/1.6 system-ui,sans-serif;max-width:560px;margin:40px auto;padding:0 16px}` +
|
|
220
|
+
`code{opacity:.7;font-size:13px}ul{list-style:none;padding:0}li{border-bottom:1px solid #8884}li a{display:block;padding:12px 0;font-weight:600;text-decoration:none;color:inherit}` +
|
|
221
|
+
`input,button{font:inherit;padding:10px 12px;border-radius:10px;border:1px solid #8888;width:100%;box-sizing:border-box;margin:6px 0}` +
|
|
222
|
+
`input[name=code]{letter-spacing:.2em;text-transform:uppercase}button{background:#5b4bd5;color:#fff;border:0;font-weight:600}#err{color:#c62828}</style><h1>kraftwerk</h1>`;
|
|
223
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
224
|
+
if (trust.kind === "none") {
|
|
225
|
+
return void res.end(head +
|
|
226
|
+
`<p>Pair this device to use kraftwerk on this computer. On the computer, run <code>kraftwerk devices pair</code> (or settings → devices → pair a device) and enter the code here.</p>` +
|
|
227
|
+
`<form id="pair"><input name="code" placeholder="code" autocomplete="one-time-code" autocapitalize="characters" required>` +
|
|
228
|
+
`<input name="name" placeholder="this device's name, e.g. Lukas' iPhone"><button>pair</button><p id="err"></p></form>` +
|
|
229
|
+
`<script>document.getElementById("pair").onsubmit=async(e)=>{e.preventDefault();const f=new FormData(e.target);` +
|
|
230
|
+
`const r=await fetch("/api/pair",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({code:String(f.get("code")).trim(),name:String(f.get("name")||navigator.platform||"device")})});` +
|
|
231
|
+
`if(r.ok)location.reload();else document.getElementById("err").textContent=(await r.json().catch(()=>({}))).error||"pairing failed"}</script>`);
|
|
232
|
+
}
|
|
215
233
|
const rows = open.length
|
|
216
|
-
? open.map((o) => `<li><a href="${esc(local ? `${o.url}/` : `/w/${o.slug}/`)}">${esc(o.name)}
|
|
234
|
+
? open.map((o) => `<li><a href="${esc(local ? `${o.url}/` : `/w/${o.slug}/`)}">${esc(o.name)} <code>${esc(o.slug)}</code></a></li>`).join("")
|
|
217
235
|
: "<li>No workspace is open. Run <code>kraftwerk ui</code> in a workspace folder.</li>";
|
|
218
|
-
res.
|
|
219
|
-
res.end(`<!doctype html><meta charset="utf-8"><title>kraftwerk</title><style>body{font:15px/1.6 system-ui,sans-serif;max-width:640px;margin:48px auto;padding:0 16px;color:#1b1b1f}code{color:#46464f}</style><h1>kraftwerk</h1><ul>${rows}</ul>`);
|
|
236
|
+
res.end(`${head}<ul>${rows}</ul>`);
|
|
220
237
|
}
|
|
221
238
|
export async function startHub(opts) {
|
|
222
239
|
const trustOpts = { trustLoopback: opts.trustLoopback !== false && !REQUIRE_PAIRING, trustNetwork: TRUST_NETWORK };
|
|
@@ -260,7 +277,7 @@ export async function startHub(opts) {
|
|
|
260
277
|
}
|
|
261
278
|
// A daemon's own address lists its workspaces; anywhere else, the web UI's shell (public: an unpaired browser needs it to pair).
|
|
262
279
|
if (!open && (url.pathname === "/" || url.pathname === "/index.html"))
|
|
263
|
-
return hubPage(req, res, [...bySlug.values()]);
|
|
280
|
+
return hubPage(req, res, [...bySlug.values()], trust);
|
|
264
281
|
await serveStatic(res, opts.staticDir, url.pathname);
|
|
265
282
|
}
|
|
266
283
|
catch (err) {
|
|
@@ -388,7 +405,7 @@ export async function startHub(opts) {
|
|
|
388
405
|
startRoutineScheduler();
|
|
389
406
|
startGitSync();
|
|
390
407
|
// Best-effort, in the background: none of them may hold up (or, failing, break) the open.
|
|
391
|
-
|
|
408
|
+
entry.registered = registerInstance(entry.aliasPort ?? port, abs, opts.daemon ? { hub: true, slug, url: entry.url } : {}).catch(() => { });
|
|
392
409
|
void registerWorkspace(abs).catch(() => { });
|
|
393
410
|
void startCloudSync(entry.url, version).catch(() => { });
|
|
394
411
|
}));
|
|
@@ -419,6 +436,7 @@ export async function startHub(opts) {
|
|
|
419
436
|
await cloudGoodbye();
|
|
420
437
|
stopCloudSync();
|
|
421
438
|
}));
|
|
439
|
+
await entry.registered;
|
|
422
440
|
unregisterInstance(abs);
|
|
423
441
|
markWorkspaceStopped(abs);
|
|
424
442
|
entry.ws.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netnodeag/kraftwerk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.0",
|
|
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",
|