@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/server/cloud.js
CHANGED
|
@@ -4,21 +4,26 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { cloudFor, resolveWorkspace } from "../config.js";
|
|
6
6
|
import { workspaceRoot } from "../core/context.js";
|
|
7
|
+
import { perWorkspace, workspaceOpen } from "../core/workspace.js";
|
|
7
8
|
import { listAgents, toSummary } from "../core/agents.js";
|
|
8
9
|
import { listChannels, toChannelSummary } from "../core/channels.js";
|
|
9
10
|
import { tildify } from "../core/instances.js";
|
|
10
11
|
const claimUrlFor = (url, code) => (code ? `${url}/claim/${encodeURIComponent(code)}` : null);
|
|
11
12
|
const REQUEST_TIMEOUT_MS = 8_000;
|
|
12
13
|
const GOODBYE_TIMEOUT_MS = 800;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
/** The client's state, one per workspace: each served workspace registers and heartbeats on its own. */
|
|
15
|
+
const client = perWorkspace(() => ({
|
|
16
|
+
status: { url: "", state: "off" },
|
|
17
|
+
identity: null,
|
|
18
|
+
timer: null,
|
|
19
|
+
stopped: false,
|
|
20
|
+
lastRoster: "",
|
|
21
|
+
/** Where this workspace is reached on this machine, as the cloud lists it. */
|
|
22
|
+
selfUrl: "",
|
|
23
|
+
selfVersion: "",
|
|
24
|
+
startedAt: new Date().toISOString(),
|
|
25
|
+
}));
|
|
26
|
+
export const cloudStatus = () => client().status;
|
|
22
27
|
const cloudDir = () => path.join(os.homedir(), ".kraftwerk", "cloud");
|
|
23
28
|
const identityFile = (url, root) => path.join(cloudDir(), `${createHash("sha1").update(`${url}\n${root}`).digest("hex").slice(0, 16)}.json`);
|
|
24
29
|
async function readIdentity(file) {
|
|
@@ -46,9 +51,10 @@ async function roster() {
|
|
|
46
51
|
return { agents, channels };
|
|
47
52
|
}
|
|
48
53
|
async function post(url, body, timeout = REQUEST_TIMEOUT_MS) {
|
|
54
|
+
const c = client();
|
|
49
55
|
const r = await fetch(url, {
|
|
50
56
|
method: "POST",
|
|
51
|
-
headers: { "content-type": "application/json", "user-agent": `kraftwerk/${selfVersion}` },
|
|
57
|
+
headers: { "content-type": "application/json", "user-agent": `kraftwerk/${c.selfVersion}` },
|
|
52
58
|
body: JSON.stringify(body),
|
|
53
59
|
signal: AbortSignal.timeout(timeout),
|
|
54
60
|
});
|
|
@@ -66,16 +72,17 @@ async function post(url, body, timeout = REQUEST_TIMEOUT_MS) {
|
|
|
66
72
|
}
|
|
67
73
|
/** Register (or re-register) this instance. Returns false when the cloud refused or is unreachable. */
|
|
68
74
|
async function register() {
|
|
75
|
+
const c = client();
|
|
69
76
|
const project = await resolveWorkspace(workspaceRoot()).catch(() => null);
|
|
70
77
|
const cloud = project ? cloudFor(project) : undefined;
|
|
71
78
|
if (!project || !cloud)
|
|
72
79
|
return false;
|
|
73
80
|
const file = identityFile(cloud.url, project.root);
|
|
74
|
-
identity ??= await readIdentity(file);
|
|
81
|
+
c.identity ??= await readIdentity(file);
|
|
75
82
|
const { agents, channels } = await roster();
|
|
76
|
-
lastRoster = JSON.stringify({ agents, channels });
|
|
83
|
+
c.lastRoster = JSON.stringify({ agents, channels });
|
|
77
84
|
const body = {
|
|
78
|
-
...(identity ?? {}),
|
|
85
|
+
...(c.identity ?? {}),
|
|
79
86
|
name: project.config.name ?? path.basename(project.root),
|
|
80
87
|
icon: project.config.icon ?? "",
|
|
81
88
|
color: project.config.color ?? "",
|
|
@@ -83,106 +90,121 @@ async function register() {
|
|
|
83
90
|
rootLabel: tildify(project.root),
|
|
84
91
|
hostname: os.hostname(),
|
|
85
92
|
platform: `${process.platform}-${process.arch}`,
|
|
86
|
-
version: selfVersion,
|
|
87
|
-
url:
|
|
93
|
+
version: c.selfVersion,
|
|
94
|
+
url: c.selfUrl,
|
|
88
95
|
// Part of the cloud's register contract; always "" since the Cloudflare Tunnel was removed (0.65).
|
|
89
96
|
publicUrl: "",
|
|
90
97
|
agents,
|
|
91
98
|
channels,
|
|
92
99
|
interval: cloud.interval,
|
|
93
|
-
startedAt,
|
|
100
|
+
startedAt: c.startedAt,
|
|
94
101
|
...(cloud.token ? { accountToken: cloud.token } : {}),
|
|
95
102
|
};
|
|
96
103
|
try {
|
|
97
104
|
const r = await post(`${cloud.url}/api/instances/register`, body);
|
|
98
|
-
if (!identity || identity.id !== r.id || identity.secret !== r.secret) {
|
|
99
|
-
identity = { id: r.id, secret: r.secret };
|
|
100
|
-
await writeIdentity(file, cloud.url, project.root, identity);
|
|
105
|
+
if (!c.identity || c.identity.id !== r.id || c.identity.secret !== r.secret) {
|
|
106
|
+
c.identity = { id: r.id, secret: r.secret };
|
|
107
|
+
await writeIdentity(file, cloud.url, project.root, c.identity);
|
|
101
108
|
}
|
|
102
|
-
status = {
|
|
109
|
+
c.status = {
|
|
103
110
|
url: cloud.url, state: "connected", id: r.id, account: r.account, interval: r.interval, lastSeen: new Date().toISOString(),
|
|
104
111
|
claimCode: r.claimCode ?? null, claimUrl: claimUrlFor(cloud.url, r.claimCode),
|
|
105
112
|
};
|
|
106
113
|
return true;
|
|
107
114
|
}
|
|
108
115
|
catch (err) {
|
|
109
|
-
status = { url: cloud.url, state: "error", error: err.message, ...(identity ? { id: identity.id } : {}) };
|
|
116
|
+
c.status = { url: cloud.url, state: "error", error: err.message, ...(c.identity ? { id: c.identity.id } : {}) };
|
|
110
117
|
return false;
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
120
|
async function heartbeat() {
|
|
114
|
-
|
|
121
|
+
const c = client();
|
|
122
|
+
if (!c.identity || c.status.state === "off")
|
|
115
123
|
return;
|
|
116
124
|
const { agents, channels } = await roster();
|
|
117
125
|
const rosterNow = JSON.stringify({ agents, channels });
|
|
118
126
|
const body = {
|
|
119
|
-
...identity,
|
|
120
|
-
version: selfVersion,
|
|
121
|
-
...(rosterNow !== lastRoster ? { agents, channels } : {}),
|
|
127
|
+
...c.identity,
|
|
128
|
+
version: c.selfVersion,
|
|
129
|
+
...(rosterNow !== c.lastRoster ? { agents, channels } : {}),
|
|
122
130
|
};
|
|
123
131
|
try {
|
|
124
|
-
const r = await post(`${status.url}/api/instances/heartbeat`, body);
|
|
125
|
-
lastRoster = rosterNow;
|
|
126
|
-
status = {
|
|
127
|
-
...status, state: "connected", error: undefined, interval: r.interval, lastSeen: new Date().toISOString(),
|
|
132
|
+
const r = await post(`${c.status.url}/api/instances/heartbeat`, body);
|
|
133
|
+
c.lastRoster = rosterNow;
|
|
134
|
+
c.status = {
|
|
135
|
+
...c.status, state: "connected", error: undefined, interval: r.interval, lastSeen: new Date().toISOString(),
|
|
128
136
|
...(r.account !== undefined ? { account: r.account } : {}),
|
|
129
|
-
...(r.claimCode !== undefined ? { claimCode: r.claimCode, claimUrl: claimUrlFor(status.url, r.claimCode) } : {}),
|
|
137
|
+
...(r.claimCode !== undefined ? { claimCode: r.claimCode, claimUrl: claimUrlFor(c.status.url, r.claimCode) } : {}),
|
|
130
138
|
};
|
|
131
139
|
}
|
|
132
140
|
catch (err) {
|
|
133
141
|
const message = err.message;
|
|
134
|
-
status = { ...status, state: "error", error: message };
|
|
142
|
+
c.status = { ...c.status, state: "error", error: message };
|
|
135
143
|
// An unknown identity (the cloud lost it) is fixed by registering again, which hands out a new one.
|
|
136
144
|
if (/unknown instance|wrong instance secret|id and secret required/.test(message)) {
|
|
137
|
-
identity = null;
|
|
138
|
-
|
|
145
|
+
c.identity = null;
|
|
146
|
+
// Not after a goodbye: a stopped client registers nothing again.
|
|
147
|
+
if (!c.stopped && workspaceOpen())
|
|
148
|
+
await register();
|
|
139
149
|
}
|
|
140
150
|
}
|
|
141
151
|
}
|
|
142
152
|
/** Schedule the next tick after the effective interval; a failed cloud is retried at the same cadence. */
|
|
143
153
|
function schedule() {
|
|
144
|
-
|
|
154
|
+
const c = client();
|
|
155
|
+
if (c.stopped)
|
|
145
156
|
return;
|
|
146
|
-
const seconds = status.interval && status.interval > 0 ? status.interval : 60;
|
|
147
|
-
timer = setTimeout(async () => {
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
const seconds = c.status.interval && c.status.interval > 0 ? c.status.interval : 60;
|
|
158
|
+
c.timer = setTimeout(async () => {
|
|
159
|
+
// Stopped (or the workspace closed) while this timer waited, or while the call below ran: no next round.
|
|
160
|
+
if (c.stopped || !workspaceOpen())
|
|
161
|
+
return;
|
|
162
|
+
if (c.status.state === "connected" || c.status.state === "error") {
|
|
163
|
+
if (c.identity)
|
|
150
164
|
await heartbeat();
|
|
151
165
|
else
|
|
152
166
|
await register();
|
|
153
167
|
}
|
|
154
|
-
|
|
168
|
+
if (!c.stopped && workspaceOpen())
|
|
169
|
+
schedule();
|
|
155
170
|
}, seconds * 1000);
|
|
156
|
-
timer.unref();
|
|
171
|
+
c.timer.unref();
|
|
157
172
|
}
|
|
158
173
|
/**
|
|
159
174
|
* Start the client once the inspector listens. Resolves after the first
|
|
160
175
|
* registration attempt (success or not); the heartbeat loop runs on its
|
|
161
176
|
* own afterwards.
|
|
162
177
|
*/
|
|
163
|
-
export async function startCloudSync(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
export async function startCloudSync(url, version) {
|
|
179
|
+
const c = client();
|
|
180
|
+
c.selfUrl = url;
|
|
181
|
+
c.selfVersion = version;
|
|
182
|
+
c.stopped = false;
|
|
167
183
|
const project = await resolveWorkspace(workspaceRoot()).catch(() => null);
|
|
168
184
|
const cloud = project ? cloudFor(project) : undefined;
|
|
169
185
|
if (!cloud) {
|
|
170
|
-
status = { url: "", state: "off" };
|
|
186
|
+
c.status = { url: "", state: "off" };
|
|
171
187
|
return;
|
|
172
188
|
}
|
|
173
|
-
|
|
189
|
+
// The workspace may close (or the client stop) while this waits: then it does not register or schedule at all.
|
|
190
|
+
if (c.stopped || !workspaceOpen())
|
|
191
|
+
return;
|
|
192
|
+
c.status = { url: cloud.url, state: "connecting", interval: cloud.interval };
|
|
174
193
|
await register();
|
|
194
|
+
if (c.stopped || !workspaceOpen())
|
|
195
|
+
return;
|
|
175
196
|
// Until the cloud answers, retry at the configured cadence; once it does, at what it asked for.
|
|
176
|
-
if (status.state !== "connected")
|
|
177
|
-
status.interval = cloud.interval;
|
|
197
|
+
if (c.status.state !== "connected")
|
|
198
|
+
c.status.interval = cloud.interval;
|
|
178
199
|
schedule();
|
|
179
200
|
}
|
|
180
201
|
/** Stop the heartbeat loop (server close, tests). */
|
|
181
202
|
export function stopCloudSync() {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
203
|
+
const c = client();
|
|
204
|
+
c.stopped = true;
|
|
205
|
+
if (c.timer)
|
|
206
|
+
clearTimeout(c.timer);
|
|
207
|
+
c.timer = null;
|
|
186
208
|
}
|
|
187
209
|
/**
|
|
188
210
|
* Tell the cloud we are going away, so the record flips to "stopped" now
|
|
@@ -190,11 +212,21 @@ export function stopCloudSync() {
|
|
|
190
212
|
* this, and a slow cloud must not hold the process.
|
|
191
213
|
*/
|
|
192
214
|
export async function cloudGoodbye() {
|
|
215
|
+
const c = client();
|
|
193
216
|
stopCloudSync();
|
|
194
|
-
if (!identity || status.state === "off" || !status.url)
|
|
217
|
+
if (!c.identity || c.status.state === "off" || !c.status.url)
|
|
195
218
|
return;
|
|
196
219
|
try {
|
|
197
|
-
await post(`${status.url}/api/instances/goodbye`, identity, GOODBYE_TIMEOUT_MS);
|
|
220
|
+
await post(`${c.status.url}/api/instances/goodbye`, c.identity, GOODBYE_TIMEOUT_MS);
|
|
198
221
|
}
|
|
199
222
|
catch { }
|
|
200
223
|
}
|
|
224
|
+
/** Every served workspace: stop its heartbeat (the server closes). */
|
|
225
|
+
export function stopAllCloudSync() {
|
|
226
|
+
for (const [ws] of client.entries())
|
|
227
|
+
ws.run(stopCloudSync);
|
|
228
|
+
}
|
|
229
|
+
/** Every served workspace says goodbye, in parallel (still bounded per workspace). */
|
|
230
|
+
export async function cloudGoodbyeAll() {
|
|
231
|
+
await Promise.all(client.entries().map(([ws]) => ws.run(cloudGoodbye)));
|
|
232
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Hub } from "./server.js";
|
|
2
|
+
export declare const runInHub: <T>(hub: Hub, fn: () => T) => T;
|
|
3
|
+
export declare function currentHub(): Hub;
|
|
4
|
+
/** API paths that need no workspace: the machine's own (the hub, devices and pairing, the protocol, the socket). */
|
|
5
|
+
export declare const MACHINE_PATHS: RegExp;
|
|
6
|
+
/** Route names callable over the socket without a workspace. */
|
|
7
|
+
export declare const MACHINE_ROUTES: RegExp;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
/**
|
|
3
|
+
* The hub a request runs in, like the workspace (core/workspace.ts): the
|
|
4
|
+
* machine-level routes (hub.*) find theirs here, so several hubs in one
|
|
5
|
+
* process (tests) never act on each other's workspaces.
|
|
6
|
+
*/
|
|
7
|
+
const storage = new AsyncLocalStorage();
|
|
8
|
+
export const runInHub = (hub, fn) => storage.run(hub, fn);
|
|
9
|
+
export function currentHub() {
|
|
10
|
+
const hub = storage.getStore();
|
|
11
|
+
if (!hub)
|
|
12
|
+
throw new Error("no hub here");
|
|
13
|
+
return hub;
|
|
14
|
+
}
|
|
15
|
+
/** API paths that need no workspace: the machine's own (the hub, devices and pairing, the protocol, the socket). */
|
|
16
|
+
export const MACHINE_PATHS = /^\/api\/(hub(\/|$)|pair$|devices(\/|$)|protocol$|ws$)/;
|
|
17
|
+
/** Route names callable over the socket without a workspace. */
|
|
18
|
+
export const MACHINE_ROUTES = /^(hub|devices|protocol)\./;
|
package/dist/server/live.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ interface Watch {
|
|
|
37
37
|
* every change, and at least every `interval` ms. Returns the unwatch.
|
|
38
38
|
*/
|
|
39
39
|
export declare function watch(key: string, evaluate: Watch["evaluate"], interval: number, push: (data: unknown) => void): () => void;
|
|
40
|
+
/** The current workspace closes: its watches stop re-evaluating (their sockets close with it). */
|
|
41
|
+
export declare function closeWatches(): void;
|
|
40
42
|
/** Something changed: re-evaluate every watch of this workspace soon (debounced). */
|
|
41
43
|
export declare function touch(): void;
|
|
42
44
|
export {};
|
package/dist/server/live.js
CHANGED
|
@@ -71,6 +71,15 @@ export function watch(key, evaluate, interval, push) {
|
|
|
71
71
|
schedule(current);
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
/** The current workspace closes: its watches stop re-evaluating (their sockets close with it). */
|
|
75
|
+
export function closeWatches() {
|
|
76
|
+
const s = state();
|
|
77
|
+
for (const w of s.watches.values())
|
|
78
|
+
clearTimeout(w.timer);
|
|
79
|
+
s.watches.clear();
|
|
80
|
+
clearTimeout(s.touchTimer);
|
|
81
|
+
s.touchTimer = undefined;
|
|
82
|
+
}
|
|
74
83
|
/** Something changed: re-evaluate every watch of this workspace soon (debounced). */
|
|
75
84
|
export function touch() {
|
|
76
85
|
// Outside any workspace (a unit test, a CLI command) nobody can be watching.
|
package/dist/server/preview.js
CHANGED
|
@@ -108,7 +108,8 @@ export async function serveVibeable(req, res, url) {
|
|
|
108
108
|
let st = await fs.stat(abs).catch(() => null);
|
|
109
109
|
if (st?.isDirectory()) {
|
|
110
110
|
if (!url.pathname.endsWith("/")) {
|
|
111
|
-
|
|
111
|
+
// Relative, so it holds under a /w/<slug> prefix too.
|
|
112
|
+
res.writeHead(302, { location: `${url.pathname.split("/").pop()}/${url.search}`, "cache-control": "no-store" });
|
|
112
113
|
return void res.end();
|
|
113
114
|
}
|
|
114
115
|
abs = path.join(abs, "index.html");
|
package/dist/server/server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
+
import { Workspace } from "../core/workspace.js";
|
|
2
3
|
import { RESTART_EXIT_CODE } from "./version.js";
|
|
3
4
|
export { RESTART_EXIT_CODE };
|
|
4
5
|
export interface InspectorOptions {
|
|
@@ -14,5 +15,55 @@ export interface InspectorOptions {
|
|
|
14
15
|
*/
|
|
15
16
|
trustLoopback?: boolean;
|
|
16
17
|
}
|
|
17
|
-
/**
|
|
18
|
+
/** A workspace a hub serves. */
|
|
19
|
+
export interface OpenWorkspace {
|
|
20
|
+
slug: string;
|
|
21
|
+
root: string;
|
|
22
|
+
name: string;
|
|
23
|
+
/** Where it is reached: <slug>.localhost:<port> under a daemon, localhost:<port> standalone. */
|
|
24
|
+
url: string;
|
|
25
|
+
/** Its own port too (kraftwerk.yml `port`), when the hub listens there for it — old links keep working. */
|
|
26
|
+
aliasPort?: number;
|
|
27
|
+
ws: Workspace;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One HTTP server serving workspaces by slug (see target.ts): a daemon
|
|
31
|
+
* (`kraftwerk daemon`) serves every workspace that is open on the machine,
|
|
32
|
+
* a standalone `kraftwerk ui` exactly one — its default, also reached
|
|
33
|
+
* without a slug, as before. Each open workspace runs in its own Workspace
|
|
34
|
+
* (core/workspace.ts) with its own .env, background jobs (routines, git
|
|
35
|
+
* sync, cloud), registry entry and live state; closing one ends all of that
|
|
36
|
+
* and leaves the others running.
|
|
37
|
+
*/
|
|
38
|
+
export interface Hub {
|
|
39
|
+
readonly server: http.Server;
|
|
40
|
+
/** The port it listens on. */
|
|
41
|
+
readonly port: number;
|
|
42
|
+
/** Whether it is the machine's daemon (workspaces addressed by slug, none by default). */
|
|
43
|
+
readonly daemon: boolean;
|
|
44
|
+
/** Serve a workspace (idempotent: an open one is returned as it is). */
|
|
45
|
+
open(root: string, opts?: {
|
|
46
|
+
outputDir?: string;
|
|
47
|
+
}): Promise<OpenWorkspace>;
|
|
48
|
+
/** Stop serving one; false when it was not open. */
|
|
49
|
+
close(root: string): Promise<boolean>;
|
|
50
|
+
list(): OpenWorkspace[];
|
|
51
|
+
/** Close every workspace and the server. */
|
|
52
|
+
stop(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export interface HubOptions {
|
|
55
|
+
port: number;
|
|
56
|
+
staticDir: string;
|
|
57
|
+
trustLoopback?: boolean;
|
|
58
|
+
/** The machine's daemon: workspaces get `hub` instance files, their own ports as aliases, and no default. */
|
|
59
|
+
daemon?: boolean;
|
|
60
|
+
/** After a workspace opened or closed (the daemon notes what to reopen after a restart). */
|
|
61
|
+
onChange?: (hub: Hub) => void | Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
export declare function startHub(opts: HubOptions): Promise<Hub>;
|
|
64
|
+
/**
|
|
65
|
+
* A standalone server for one workspace (`kraftwerk ui`, tests): a hub
|
|
66
|
+
* serving it as its default, reached with or without its slug. Resolves
|
|
67
|
+
* once it listens.
|
|
68
|
+
*/
|
|
18
69
|
export declare function startInspector(opts: InspectorOptions): Promise<http.Server>;
|