@netnodeag/kraftwerk 0.65.0 → 0.67.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 +112 -27
- package/dist/acp.js +3 -1
- package/dist/cli/daemon.d.ts +14 -0
- package/dist/cli/daemon.js +60 -0
- package/dist/cli/doctor.js +9 -1
- package/dist/cli/init.js +1 -0
- package/dist/cli/kraftwerk.js +4 -0
- package/dist/cli/ui.d.ts +8 -0
- package/dist/cli/ui.js +35 -8
- package/dist/cli/workspaces.js +5 -3
- package/dist/client/index.d.ts +8 -0
- package/dist/client/index.js +2 -1
- package/dist/client/live.d.ts +1 -2
- package/dist/client/live.js +2 -1
- package/dist/client/routes.d.ts +3 -0
- package/dist/client/routes.js +3 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +23 -1
- package/dist/core/chat/pi.js +2 -1
- package/dist/core/chat/sessions.d.ts +1 -1
- package/dist/core/chat/sessions.js +1 -1
- package/dist/core/chat/state.d.ts +2 -0
- package/dist/core/chat/state.js +5 -0
- package/dist/core/daemon.d.ts +27 -0
- package/dist/core/daemon.js +61 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +45 -0
- package/dist/core/git.d.ts +2 -0
- package/dist/core/git.js +20 -4
- package/dist/core/instances.d.ts +34 -12
- package/dist/core/instances.js +142 -66
- package/dist/core/routines.d.ts +6 -0
- package/dist/core/routines.js +11 -0
- package/dist/core/runner.js +7 -3
- package/dist/core/search.d.ts +2 -0
- package/dist/core/search.js +1 -0
- package/dist/core/vibeables.d.ts +2 -0
- package/dist/core/vibeables.js +17 -5
- package/dist/core/workspace.d.ts +13 -0
- package/dist/core/workspace.js +25 -0
- package/dist/server/api/hub.d.ts +25 -0
- package/dist/server/api/hub.js +32 -0
- package/dist/server/api/index.d.ts +35 -3
- package/dist/server/api/index.js +2 -0
- package/dist/server/api/socket.d.ts +9 -1
- package/dist/server/api/socket.js +27 -7
- package/dist/server/api/workspace.d.ts +16 -2
- package/dist/server/api/workspace.js +44 -4
- package/dist/server/cloud.d.ts +5 -1
- package/dist/server/cloud.js +86 -54
- package/dist/server/hub-context.d.ts +7 -0
- package/dist/server/hub-context.js +18 -0
- package/dist/server/live.d.ts +2 -0
- package/dist/server/live.js +9 -0
- package/dist/server/preview.js +2 -1
- package/dist/server/server.d.ts +52 -1
- package/dist/server/server.js +283 -52
- package/dist/server/target.d.ts +20 -0
- package/dist/server/target.js +18 -0
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +5 -0
- package/web/dist/assets/{dist-10uPVFvu.js → dist-BMZBs9W4.js} +1 -1
- package/web/dist/assets/{dist-v6-49xpW.js → dist-BkZKygT6.js} +1 -1
- package/web/dist/assets/{dist-Bnxg6V5L.js → dist-CZjwFXw-.js} +1 -1
- package/web/dist/assets/{dist-DS1R2xKJ.js → dist-D7BYSDIr.js} +1 -1
- package/web/dist/assets/dist-DZLlNafq.js +1 -0
- package/web/dist/assets/{dist-BCfXUgtf.js → dist-D_nqqdyh.js} +1 -1
- package/web/dist/assets/{dist-nA7q2-h7.js → dist-IO8GeoYh.js} +1 -1
- package/web/dist/assets/{dist-hevbMnoj.js → dist-LUKHNpC7.js} +1 -1
- package/web/dist/assets/{dist-DnMgiPT1.js → dist-Y0KLvK_g.js} +1 -1
- package/web/dist/assets/{dist-C-CclD1E.js → dist-aMqhK50K.js} +1 -1
- package/web/dist/assets/{dist-CAWmDGJC.js → dist-jyZ9dxfk.js} +1 -1
- package/web/dist/assets/{editor-C8yy8YS9.js → editor-DQouCIUq.js} +3 -3
- package/web/dist/assets/index-D57IrV_V.js +229 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/dist-yoh47aAL.js +0 -1
- package/web/dist/assets/index-DWS1u71Y.js +0 -229
package/dist/core/workspace.js
CHANGED
|
@@ -20,22 +20,42 @@ import path from "node:path";
|
|
|
20
20
|
export class Workspace {
|
|
21
21
|
root;
|
|
22
22
|
outputDir;
|
|
23
|
+
/** Its address on this machine (kraftwerk.yml `slug`, else the folder name), when known. */
|
|
24
|
+
slug;
|
|
25
|
+
/** Its `.env` (parsed, not applied — see env.ts); {} when none was given. */
|
|
26
|
+
env;
|
|
23
27
|
slots = new Map();
|
|
24
28
|
/** `root` defaults to the parent of the output dir (wrong for nested output dirs — pass it when known). */
|
|
25
29
|
constructor(opts) {
|
|
26
30
|
this.outputDir = path.resolve(opts.outputDir);
|
|
27
31
|
this.root = opts.root ? path.resolve(opts.root) : path.dirname(this.outputDir);
|
|
32
|
+
this.slug = opts.slug;
|
|
33
|
+
this.env = opts.env ?? {};
|
|
28
34
|
known.add(this);
|
|
29
35
|
}
|
|
30
36
|
/** Run `fn` (and everything it starts) in this workspace. */
|
|
31
37
|
run(fn) {
|
|
32
38
|
return storage.run(this, fn);
|
|
33
39
|
}
|
|
40
|
+
/** Closed (no longer served): its state is gone and may not come back — see close(). */
|
|
41
|
+
get closed() {
|
|
42
|
+
return this.isClosed;
|
|
43
|
+
}
|
|
44
|
+
isClosed = false;
|
|
34
45
|
slot(key, init) {
|
|
46
|
+
// Something still running for a closed workspace (a fetch in flight, a heartbeat) must not bring its state back.
|
|
47
|
+
if (this.isClosed)
|
|
48
|
+
throw new Error("workspace closed");
|
|
35
49
|
if (!this.slots.has(key))
|
|
36
50
|
this.slots.set(key, init());
|
|
37
51
|
return this.slots.get(key);
|
|
38
52
|
}
|
|
53
|
+
/** No longer served: sweeps over all workspaces (shutdown, the idle reaper) skip it. Its slots are dropped. */
|
|
54
|
+
close() {
|
|
55
|
+
this.isClosed = true;
|
|
56
|
+
known.delete(this);
|
|
57
|
+
this.slots.clear();
|
|
58
|
+
}
|
|
39
59
|
peek(key) {
|
|
40
60
|
return this.slots.get(key);
|
|
41
61
|
}
|
|
@@ -48,6 +68,11 @@ export function setDefaultWorkspace(ws) {
|
|
|
48
68
|
fallback = ws;
|
|
49
69
|
return ws;
|
|
50
70
|
}
|
|
71
|
+
/** Whether the caller's workspace is still served (a background loop checks before its next round); true outside any. */
|
|
72
|
+
export function workspaceOpen() {
|
|
73
|
+
const ws = storage.getStore() ?? fallback;
|
|
74
|
+
return !ws?.closed;
|
|
75
|
+
}
|
|
51
76
|
export function currentWorkspace() {
|
|
52
77
|
const ws = storage.getStore() ?? fallback;
|
|
53
78
|
if (!ws)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
/** What a hub serves (server.ts Hub): the machine's open workspaces, and opening or closing one. Need no workspace. */
|
|
3
|
+
export declare const hubRoutes: (import("./router.js").Route<"hub.close", {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
}, "/api/hub/close", z.ZodObject<{
|
|
6
|
+
root: z.ZodString;
|
|
7
|
+
}, z.core.$strip>> | import("./router.js").Route<"hub.list", {
|
|
8
|
+
daemon: boolean;
|
|
9
|
+
port: number;
|
|
10
|
+
workspaces: {
|
|
11
|
+
slug: string;
|
|
12
|
+
name: string;
|
|
13
|
+
url: string;
|
|
14
|
+
aliasPort?: number | undefined;
|
|
15
|
+
root?: string | undefined;
|
|
16
|
+
rootLabel?: string | undefined;
|
|
17
|
+
}[];
|
|
18
|
+
}, "/api/hub", undefined> | import("./router.js").Route<"hub.open", {
|
|
19
|
+
slug: string;
|
|
20
|
+
name: string;
|
|
21
|
+
url: string;
|
|
22
|
+
aliasPort?: number | undefined;
|
|
23
|
+
}, "/api/hub/open", z.ZodObject<{
|
|
24
|
+
root: z.ZodString;
|
|
25
|
+
}, z.core.$strip>>)[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { tildify } from "../../core/instances.js";
|
|
3
|
+
import { currentHub } from "../hub-context.js";
|
|
4
|
+
import { fail, route } from "./router.js";
|
|
5
|
+
const Root = z.object({ root: z.string().min(1, "root is required") });
|
|
6
|
+
/** What a hub serves (server.ts Hub): the machine's open workspaces, and opening or closing one. Need no workspace. */
|
|
7
|
+
export const hubRoutes = [
|
|
8
|
+
// The folder is this machine's business; a paired device sees names and addresses.
|
|
9
|
+
route({ name: "hub.list", method: "GET", path: "/api/hub", summary: "the workspaces this kraftwerk serves: slug, name, address" }, async (c) => {
|
|
10
|
+
const hub = currentHub();
|
|
11
|
+
return {
|
|
12
|
+
daemon: hub.daemon,
|
|
13
|
+
port: hub.port,
|
|
14
|
+
workspaces: hub.list().map((o) => ({
|
|
15
|
+
slug: o.slug,
|
|
16
|
+
name: o.name,
|
|
17
|
+
url: o.url,
|
|
18
|
+
...(o.aliasPort ? { aliasPort: o.aliasPort } : {}),
|
|
19
|
+
...(c.trust.kind === "local" ? { root: o.root, rootLabel: tildify(o.root) } : {}),
|
|
20
|
+
})),
|
|
21
|
+
};
|
|
22
|
+
}),
|
|
23
|
+
route({ name: "hub.open", method: "POST", path: "/api/hub/open", summary: "serve a workspace {root} next to the others", access: "local", body: Root, errors: 400 }, async (c) => {
|
|
24
|
+
const { root } = await c.body();
|
|
25
|
+
const o = await currentHub().open(root);
|
|
26
|
+
return { slug: o.slug, name: o.name, url: o.url, ...(o.aliasPort ? { aliasPort: o.aliasPort } : {}) };
|
|
27
|
+
}),
|
|
28
|
+
route({ name: "hub.close", method: "POST", path: "/api/hub/close", summary: "stop serving a workspace {root}; the others keep running", access: "local", body: Root }, async (c) => {
|
|
29
|
+
const { root } = await c.body();
|
|
30
|
+
return (await currentHub().close(root)) ? { ok: true } : fail(404, "not open here");
|
|
31
|
+
}),
|
|
32
|
+
];
|
|
@@ -337,7 +337,29 @@ export declare const routes: (Route<"agents.addJournal", {
|
|
|
337
337
|
}>, "/api/git/pull", undefined> | Route<"git.push", import("./router.js").Reply<{
|
|
338
338
|
ok: boolean;
|
|
339
339
|
error?: string;
|
|
340
|
-
}>, "/api/git/push", undefined> | Route<"git.status", import("../../core/git.js").GitStatus, "/api/git", undefined> | Route<"
|
|
340
|
+
}>, "/api/git/push", undefined> | Route<"git.status", import("../../core/git.js").GitStatus, "/api/git", undefined> | Route<"hub.close", {
|
|
341
|
+
ok: boolean;
|
|
342
|
+
}, "/api/hub/close", z.ZodObject<{
|
|
343
|
+
root: z.ZodString;
|
|
344
|
+
}, z.core.$strip>> | Route<"hub.list", {
|
|
345
|
+
daemon: boolean;
|
|
346
|
+
port: number;
|
|
347
|
+
workspaces: {
|
|
348
|
+
slug: string;
|
|
349
|
+
name: string;
|
|
350
|
+
url: string;
|
|
351
|
+
aliasPort?: number | undefined;
|
|
352
|
+
root?: string | undefined;
|
|
353
|
+
rootLabel?: string | undefined;
|
|
354
|
+
}[];
|
|
355
|
+
}, "/api/hub", undefined> | Route<"hub.open", {
|
|
356
|
+
slug: string;
|
|
357
|
+
name: string;
|
|
358
|
+
url: string;
|
|
359
|
+
aliasPort?: number | undefined;
|
|
360
|
+
}, "/api/hub/open", z.ZodObject<{
|
|
361
|
+
root: z.ZodString;
|
|
362
|
+
}, z.core.$strip>> | Route<"knowledge.concept", import("../../okf.js").ConceptDetail, "/api/knowledge/:bundle/concept", undefined> | Route<"knowledge.create", import("../../okf.js").BundleInfo, "/api/knowledge", z.ZodObject<{
|
|
341
363
|
name: z.ZodDefault<z.ZodString>;
|
|
342
364
|
}, z.core.$strip>> | Route<"knowledge.delete", {
|
|
343
365
|
ok: boolean;
|
|
@@ -348,6 +370,7 @@ export declare const routes: (Route<"agents.addJournal", {
|
|
|
348
370
|
id: z.ZodDefault<z.ZodString>;
|
|
349
371
|
}, z.core.$strip>> | Route<"meta.get", {
|
|
350
372
|
workspaceName: string;
|
|
373
|
+
workspaceSlug: string;
|
|
351
374
|
workspaceIcon: string;
|
|
352
375
|
workspaceColor: string;
|
|
353
376
|
workspaceNamed: boolean;
|
|
@@ -458,7 +481,11 @@ export declare const routes: (Route<"agents.addJournal", {
|
|
|
458
481
|
runs: import("../../core/runs.js").RunListItem[];
|
|
459
482
|
}, "/api/runs", undefined> | Route<"runs.stop", {
|
|
460
483
|
stopped: boolean;
|
|
461
|
-
}, "/api/runs/:id/stop", undefined> | Route<"search.agents",
|
|
484
|
+
}, "/api/runs/:id/stop", undefined> | Route<"search.agents", {
|
|
485
|
+
workspaces: (import("../../core/search.js").WorkspaceAgents & {
|
|
486
|
+
here?: true;
|
|
487
|
+
})[];
|
|
488
|
+
}, "/api/search/agents", undefined> | Route<"server.restart", {
|
|
462
489
|
ok: boolean;
|
|
463
490
|
}, "/api/restart", undefined> | Route<"settings.get", import("../../core/settings.js").SettingsView, "/api/settings", undefined> | Route<"settings.save", import("../../core/settings.js").SettingsView, "/api/settings", z.ZodObject<{
|
|
464
491
|
name: z.ZodOptional<z.ZodString>;
|
|
@@ -554,7 +581,12 @@ export declare const routes: (Route<"agents.addJournal", {
|
|
|
554
581
|
ok: boolean;
|
|
555
582
|
}, "/api/workspaces/forget", z.ZodObject<{
|
|
556
583
|
root: z.ZodOptional<z.ZodString>;
|
|
557
|
-
}, z.core.$strip>> | Route<"workspaces.list", import("../../core/instances.js").WorkspaceDetail
|
|
584
|
+
}, z.core.$strip>> | Route<"workspaces.list", (import("../../core/instances.js").WorkspaceDetail & {
|
|
585
|
+
here?: true;
|
|
586
|
+
})[], "/api/workspaces", undefined> | Route<"workspaces.start", import("./router.js").Reply<import("../../core/instances.js").StartResult & {
|
|
587
|
+
slug?: string;
|
|
588
|
+
here?: true;
|
|
589
|
+
}>, "/api/workspaces/start", z.ZodObject<{
|
|
558
590
|
root: z.ZodOptional<z.ZodString>;
|
|
559
591
|
}, z.core.$strip>> | Route<"workspaces.stop", import("./router.js").Reply<{
|
|
560
592
|
ok: boolean;
|
package/dist/server/api/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { attentionRoutes } from "./attention.js";
|
|
|
4
4
|
import { channelRoutes } from "./channels.js";
|
|
5
5
|
import { chatRoutes } from "./chats.js";
|
|
6
6
|
import { deviceRoutes } from "./devices.js";
|
|
7
|
+
import { hubRoutes } from "./hub.js";
|
|
7
8
|
import { fileRoutes } from "./files.js";
|
|
8
9
|
import { gitRoutes } from "./git.js";
|
|
9
10
|
import { knowledgeRoutes } from "./knowledge.js";
|
|
@@ -34,6 +35,7 @@ const domainRoutes = [
|
|
|
34
35
|
...gitRoutes,
|
|
35
36
|
...vibeableRoutes,
|
|
36
37
|
...deviceRoutes,
|
|
38
|
+
...hubRoutes,
|
|
37
39
|
];
|
|
38
40
|
const protocolRoute = route({ name: "protocol.get", method: "GET", path: "/api/protocol", summary: "the protocol version and every route", access: "public" }, async () => ({
|
|
39
41
|
version: PROTOCOL_VERSION,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Workspace } from "../../core/workspace.js";
|
|
2
|
+
import type { Hub } from "../server.js";
|
|
2
3
|
import type { Trust } from "../trust.js";
|
|
3
4
|
import type { Socket } from "../ws.js";
|
|
4
5
|
/**
|
|
@@ -6,4 +7,11 @@ import type { Socket } from "../ws.js";
|
|
|
6
7
|
* been revoked since it connected): a device whose answer is no longer
|
|
7
8
|
* `device` is disconnected at once (close code 4401).
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* `workspaceOf` finds the workspace a message is for: its `ws` (a slug), or
|
|
12
|
+
* the connection's own (the one the socket was opened for) when it names
|
|
13
|
+
* none. A daemon's socket opened at its root has no workspace of its own:
|
|
14
|
+
* there, a message without `ws` reaches only the machine's routes (hub,
|
|
15
|
+
* devices, protocol).
|
|
16
|
+
*/
|
|
17
|
+
export declare function serveSocket(conn: Socket, workspaceOf: (slug?: string) => Workspace | undefined, version: string, trust: Trust, recheck: () => Promise<Trust>, hub: Hub): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { subscribeChat } from "../../core/chat/sessions.js";
|
|
2
2
|
import { watch } from "../live.js";
|
|
3
3
|
import { resolveVibeable, subscribeVibeable } from "../../core/vibeables.js";
|
|
4
|
+
import { MACHINE_ROUTES, runInHub } from "../hub-context.js";
|
|
4
5
|
import { apiRouter, PROTOCOL_VERSION, routes } from "./index.js";
|
|
5
6
|
/**
|
|
6
7
|
* The control plane over one WebSocket (`/api/ws`): every JSON route by
|
|
@@ -30,7 +31,14 @@ const RECHECK_MS = 5_000;
|
|
|
30
31
|
* been revoked since it connected): a device whose answer is no longer
|
|
31
32
|
* `device` is disconnected at once (close code 4401).
|
|
32
33
|
*/
|
|
33
|
-
|
|
34
|
+
/**
|
|
35
|
+
* `workspaceOf` finds the workspace a message is for: its `ws` (a slug), or
|
|
36
|
+
* the connection's own (the one the socket was opened for) when it names
|
|
37
|
+
* none. A daemon's socket opened at its root has no workspace of its own:
|
|
38
|
+
* there, a message without `ws` reaches only the machine's routes (hub,
|
|
39
|
+
* devices, protocol).
|
|
40
|
+
*/
|
|
41
|
+
export function serveSocket(conn, workspaceOf, version, trust, recheck, hub) {
|
|
34
42
|
const trustKey = trust.kind === "device" ? `device:${trust.device.id}` : trust.kind;
|
|
35
43
|
const stops = new Map();
|
|
36
44
|
const send = (msg) => conn.send(JSON.stringify(msg));
|
|
@@ -72,6 +80,8 @@ export function serveSocket(conn, workspace, version, trust, recheck) {
|
|
|
72
80
|
return unsubscribe();
|
|
73
81
|
stops.set(key, unsubscribe);
|
|
74
82
|
}
|
|
83
|
+
/** A refusal answered the way the message expects one (its id, watch or sub key). */
|
|
84
|
+
const refuse = (msg, status, error) => send({ id: msg.id, ...(typeof msg.watch === "string" ? { watch: msg.watch } : {}), ...(typeof msg.sub === "string" ? { sub: msg.sub } : {}), status, data: { error }, error });
|
|
75
85
|
async function handle(msg) {
|
|
76
86
|
if (typeof msg.call === "string") {
|
|
77
87
|
const r = await apiRouter.invoke(msg.call, objectOf(msg.input), trust);
|
|
@@ -129,17 +139,27 @@ export function serveSocket(conn, workspace, version, trust, recheck) {
|
|
|
129
139
|
return send({ error: "invalid JSON" });
|
|
130
140
|
}
|
|
131
141
|
// Every message runs in the connection's workspace, whatever started the read; a revoked device's message runs nowhere.
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
const slug = typeof msg.ws === "string" ? msg.ws : undefined;
|
|
143
|
+
const ws = workspaceOf(slug);
|
|
144
|
+
if (msg.ws !== undefined && !ws)
|
|
145
|
+
return refuse(msg, 404, `no workspace "${String(msg.ws)}" here`);
|
|
146
|
+
if (!ws && !(typeof msg.call === "string" && MACHINE_ROUTES.test(msg.call)))
|
|
147
|
+
return refuse(msg, 404, "name a workspace: `ws` on the message");
|
|
148
|
+
const run = (fn) => runInHub(hub, () => (ws ? ws.run(fn) : fn()));
|
|
149
|
+
void run(async () => {
|
|
134
150
|
if (await stillPaired())
|
|
135
151
|
await handle(msg);
|
|
136
|
-
})
|
|
137
|
-
.catch((err) => send({ id: msg.id, error: err.message }));
|
|
152
|
+
}).catch((err) => send({ id: msg.id, error: err.message }));
|
|
138
153
|
});
|
|
139
154
|
conn.onClose(() => {
|
|
140
155
|
clearInterval(watchdog);
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
// A subscription of a workspace that closed meanwhile has nothing left to let go of.
|
|
157
|
+
for (const fn of stops.values()) {
|
|
158
|
+
try {
|
|
159
|
+
fn();
|
|
160
|
+
}
|
|
161
|
+
catch { }
|
|
162
|
+
}
|
|
143
163
|
stops.clear();
|
|
144
164
|
});
|
|
145
165
|
send({ hello: { protocol: PROTOCOL_VERSION, version } });
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
+
import { type StartResult } from "../../core/instances.js";
|
|
3
|
+
/** A start's answer; `here` (with its slug) when the hub answering serves it now. */
|
|
4
|
+
type StartReply = StartResult & {
|
|
5
|
+
slug?: string;
|
|
6
|
+
here?: true;
|
|
7
|
+
};
|
|
2
8
|
/** The workspace itself: identity and features, settings, the other workspaces, updates. */
|
|
3
9
|
export declare const workspaceRoutes: (import("./router.js").Route<"meta.get", {
|
|
4
10
|
workspaceName: string;
|
|
11
|
+
workspaceSlug: string;
|
|
5
12
|
workspaceIcon: string;
|
|
6
13
|
workspaceColor: string;
|
|
7
14
|
workspaceNamed: boolean;
|
|
@@ -25,7 +32,11 @@ export declare const workspaceRoutes: (import("./router.js").Route<"meta.get", {
|
|
|
25
32
|
publicUrl: string;
|
|
26
33
|
cloud: import("../cloud.js").CloudStatus;
|
|
27
34
|
switcher: import("../../config.js").SwitcherEntry[];
|
|
28
|
-
}, "/api/meta", undefined> | import("./router.js").Route<"search.agents",
|
|
35
|
+
}, "/api/meta", undefined> | import("./router.js").Route<"search.agents", {
|
|
36
|
+
workspaces: (import("../../core/search.js").WorkspaceAgents & {
|
|
37
|
+
here?: true;
|
|
38
|
+
})[];
|
|
39
|
+
}, "/api/search/agents", undefined> | import("./router.js").Route<"server.restart", {
|
|
29
40
|
ok: boolean;
|
|
30
41
|
}, "/api/restart", undefined> | import("./router.js").Route<"settings.get", import("../../core/settings.js").SettingsView, "/api/settings", undefined> | import("./router.js").Route<"settings.save", import("../../core/settings.js").SettingsView, "/api/settings", z.ZodObject<{
|
|
31
42
|
name: z.ZodOptional<z.ZodString>;
|
|
@@ -90,7 +101,9 @@ export declare const workspaceRoutes: (import("./router.js").Route<"meta.get", {
|
|
|
90
101
|
ok: boolean;
|
|
91
102
|
}, "/api/workspaces/forget", z.ZodObject<{
|
|
92
103
|
root: z.ZodOptional<z.ZodString>;
|
|
93
|
-
}, z.core.$strip>> | import("./router.js").Route<"workspaces.list", import("../../core/instances.js").WorkspaceDetail
|
|
104
|
+
}, z.core.$strip>> | import("./router.js").Route<"workspaces.list", (import("../../core/instances.js").WorkspaceDetail & {
|
|
105
|
+
here?: true;
|
|
106
|
+
})[], "/api/workspaces", undefined> | import("./router.js").Route<"workspaces.start", import("./router.js").Reply<StartReply>, "/api/workspaces/start", z.ZodObject<{
|
|
94
107
|
root: z.ZodOptional<z.ZodString>;
|
|
95
108
|
}, z.core.$strip>> | import("./router.js").Route<"workspaces.stop", import("./router.js").Reply<{
|
|
96
109
|
ok: boolean;
|
|
@@ -99,3 +112,4 @@ export declare const workspaceRoutes: (import("./router.js").Route<"meta.get", {
|
|
|
99
112
|
root: z.ZodOptional<z.ZodString>;
|
|
100
113
|
url: z.ZodOptional<z.ZodString>;
|
|
101
114
|
}, z.core.$strip>>)[];
|
|
115
|
+
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
-
import { resolveWorkspace } from "../../config.js";
|
|
3
|
+
import { absolutePath, resolveWorkspace, slugFromFolder, workspaceSlug } from "../../config.js";
|
|
4
4
|
import { disposeAllBackends } from "../../core/chat/sessions.js";
|
|
5
5
|
import { cloudStatus } from "../cloud.js";
|
|
6
6
|
import { workspaceRoot } from "../../core/context.js";
|
|
7
7
|
import { discoverWorkspaces, forgetWorkspace, listWorkspacesDetailed, startWorkspace, stopWorkspace, tildify } from "../../core/instances.js";
|
|
8
|
+
import { currentHub } from "../hub-context.js";
|
|
8
9
|
import { searchAgents } from "../../core/search.js";
|
|
9
10
|
import { getSettings, saveSettings } from "../../core/settings.js";
|
|
10
11
|
import { canSelfUpdate, startUpdate, updateStatus } from "../../core/update.js";
|
|
@@ -32,6 +33,26 @@ const SettingsFields = z.looseObject({
|
|
|
32
33
|
vibeables: RootBlock.optional(),
|
|
33
34
|
projects: RootBlock.optional(),
|
|
34
35
|
});
|
|
36
|
+
/**
|
|
37
|
+
* Mark the workspaces the hub answering this request serves — or, a daemon,
|
|
38
|
+
* would serve once started (it opens a stopped one next to the others, at
|
|
39
|
+
* <slug>.localhost:<its port>) — as `here`. The UI links those in the form
|
|
40
|
+
* the page itself was reached by: a paired phone on the network cannot
|
|
41
|
+
* resolve <slug>.localhost, so it gets /w/<slug>/ on the address it used.
|
|
42
|
+
*/
|
|
43
|
+
function placeInHub(entries) {
|
|
44
|
+
const hub = currentHub();
|
|
45
|
+
const served = new Set(hub.list().map((o) => o.root));
|
|
46
|
+
return entries.map((e) => {
|
|
47
|
+
if (!e.root || !e.slug)
|
|
48
|
+
return e;
|
|
49
|
+
if (served.has(e.root))
|
|
50
|
+
return { ...e, here: true };
|
|
51
|
+
if (hub.daemon && !e.live && e.exists !== false)
|
|
52
|
+
return { ...e, here: true, url: `http://${e.slug}.localhost:${hub.port}` };
|
|
53
|
+
return e;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
35
56
|
/** The workspace itself: identity and features, settings, the other workspaces, updates. */
|
|
36
57
|
export const workspaceRoutes = [
|
|
37
58
|
// ?probe=1 marks a discovery probe from another instance: answer identity
|
|
@@ -42,7 +63,7 @@ export const workspaceRoutes = [
|
|
|
42
63
|
const name = ws?.config.name ?? (ws ? path.basename(ws.root) : "");
|
|
43
64
|
const root = ws?.root ?? workspaceRoot();
|
|
44
65
|
const manual = ws?.config.switcher ?? [];
|
|
45
|
-
const discovered = c.query.get("probe") === "1" ? [] : await discoverWorkspaces();
|
|
66
|
+
const discovered = c.query.get("probe") === "1" ? [] : placeInHub(await discoverWorkspaces());
|
|
46
67
|
// Manual switcher entries keep their configured name/icon; discovered
|
|
47
68
|
// workspaces that duplicate one (same url, running) only contribute
|
|
48
69
|
// the live flag. Stopped workspaces never collide — they carry a root,
|
|
@@ -56,6 +77,7 @@ export const workspaceRoutes = [
|
|
|
56
77
|
];
|
|
57
78
|
const identity = {
|
|
58
79
|
workspaceName: name,
|
|
80
|
+
workspaceSlug: ws ? workspaceSlug(ws) : slugFromFolder(path.basename(root)),
|
|
59
81
|
workspaceIcon: ws?.config.icon ?? "",
|
|
60
82
|
workspaceColor: ws?.config.color ?? "",
|
|
61
83
|
workspaceNamed: !!ws?.config.name,
|
|
@@ -88,12 +110,23 @@ export const workspaceRoutes = [
|
|
|
88
110
|
}),
|
|
89
111
|
route({ name: "settings.get", method: "GET", path: "/api/settings", summary: "kraftwerk.yml, parsed, with resolved paths" }, async () => getSettings()),
|
|
90
112
|
route({ name: "settings.save", method: "PUT", path: "/api/settings", summary: "write the UI-editable subset of kraftwerk.yml", errors: 400, body: SettingsFields }, async (c) => saveSettings((await c.body()))),
|
|
91
|
-
route({ name: "workspaces.list", method: "GET", path: "/api/workspaces", summary: "every known workspace incl. this one, with state and counts" }, async () => listWorkspacesDetailed()),
|
|
113
|
+
route({ name: "workspaces.list", method: "GET", path: "/api/workspaces", summary: "every known workspace incl. this one, with state and counts" }, async () => placeInHub(await listWorkspacesDetailed())),
|
|
92
114
|
// Launches `kraftwerk ui` for a known workspace as a detached process (the switcher's Start button).
|
|
93
115
|
route({ name: "workspaces.start", method: "POST", path: "/api/workspaces/start", summary: "start a known workspace {root}", errors: 400, body: z.object({ root: z.string().optional() }) }, async (c) => {
|
|
94
116
|
const { root } = await c.body();
|
|
95
117
|
if (!root)
|
|
96
118
|
fail(400, "root required");
|
|
119
|
+
// A daemon opens it itself, next to the workspace asking.
|
|
120
|
+
const hub = currentHub();
|
|
121
|
+
if (hub.daemon) {
|
|
122
|
+
try {
|
|
123
|
+
const o = await hub.open(absolutePath(root));
|
|
124
|
+
return reply(200, { ok: true, url: o.url, slug: o.slug, here: true, live: true, pid: process.pid });
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return reply(409, { ok: false, error: err.message });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
97
130
|
const result = await startWorkspace(root);
|
|
98
131
|
return reply(result.ok ? 200 : 409, result);
|
|
99
132
|
}),
|
|
@@ -101,6 +134,13 @@ export const workspaceRoutes = [
|
|
|
101
134
|
const target = await c.body();
|
|
102
135
|
if (!target.root && !target.url)
|
|
103
136
|
fail(400, "root or url required");
|
|
137
|
+
// One the hub answering serves (not the one asking): closed right here.
|
|
138
|
+
const hub = currentHub();
|
|
139
|
+
const open = target.root ? hub.list().find((o) => o.root === absolutePath(target.root)) : undefined;
|
|
140
|
+
if (hub.daemon && open && open.root !== workspaceRoot()) {
|
|
141
|
+
await hub.close(open.root);
|
|
142
|
+
return reply(200, { ok: true });
|
|
143
|
+
}
|
|
104
144
|
const result = await stopWorkspace(target);
|
|
105
145
|
return reply(result.ok ? 200 : 409, result);
|
|
106
146
|
}),
|
|
@@ -110,7 +150,7 @@ export const workspaceRoutes = [
|
|
|
110
150
|
fail(400, "root required");
|
|
111
151
|
return { ok: await forgetWorkspace(root) };
|
|
112
152
|
}),
|
|
113
|
-
route({ name: "search.agents", method: "GET", path: "/api/search/agents", summary: "active agents and channels of every reachable workspace (⌘K)" }, async () => searchAgents()),
|
|
153
|
+
route({ name: "search.agents", method: "GET", path: "/api/search/agents", summary: "active agents and channels of every reachable workspace (⌘K)" }, async () => searchAgents().then((r) => ({ workspaces: placeInHub(r.workspaces) }))),
|
|
114
154
|
route({ name: "update.check", method: "GET", path: "/api/update-check", summary: "the latest version on the npm registry" }, async () => {
|
|
115
155
|
const name = await getPkgName();
|
|
116
156
|
const current = await getDiskVersion();
|
package/dist/server/cloud.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export declare const cloudStatus: () => CloudStatus;
|
|
|
38
38
|
* registration attempt (success or not); the heartbeat loop runs on its
|
|
39
39
|
* own afterwards.
|
|
40
40
|
*/
|
|
41
|
-
export declare function startCloudSync(
|
|
41
|
+
export declare function startCloudSync(url: string, version: string): Promise<void>;
|
|
42
42
|
/** Stop the heartbeat loop (server close, tests). */
|
|
43
43
|
export declare function stopCloudSync(): void;
|
|
44
44
|
/**
|
|
@@ -47,3 +47,7 @@ export declare function stopCloudSync(): void;
|
|
|
47
47
|
* this, and a slow cloud must not hold the process.
|
|
48
48
|
*/
|
|
49
49
|
export declare function cloudGoodbye(): Promise<void>;
|
|
50
|
+
/** Every served workspace: stop its heartbeat (the server closes). */
|
|
51
|
+
export declare function stopAllCloudSync(): void;
|
|
52
|
+
/** Every served workspace says goodbye, in parallel (still bounded per workspace). */
|
|
53
|
+
export declare function cloudGoodbyeAll(): Promise<void>;
|