@kahitsan/plugin-sdk 0.2.0-staging.37 → 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/index.js +1 -1
- package/package.json +2 -7
- package/bin/cli.mjs +0 -52
- package/src/dev/dev-assets.ts +0 -156
- package/src/dev/dev-host.ts +0 -194
- package/src/dev/dev-kernel.ts +0 -499
- package/src/dev/preload.ts +0 -61
- package/src/dev/sqlite-pool.ts +0 -358
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahitsan/plugin-sdk",
|
|
3
|
-
"version": "0.2.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "THE single public author surface for Hilinga plugins: createPlugin, auth guards, the workspace-scoped data surface (makeDataSurface) + migrations + withTenantContext, cross-plugin RPC, and the wire-protocol verify surface. One npm package, no -composite/-protocol companions. Self-contained, no kernel internals, no signing capability.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,13 +16,8 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"dist"
|
|
20
|
-
"bin",
|
|
21
|
-
"src/dev"
|
|
19
|
+
"dist"
|
|
22
20
|
],
|
|
23
|
-
"bin": {
|
|
24
|
-
"plugin-sdk": "./bin/cli.mjs"
|
|
25
|
-
},
|
|
26
21
|
"dependencies": {
|
|
27
22
|
"express": "^5.1.0",
|
|
28
23
|
"pg": "^8.16.0",
|
package/bin/cli.mjs
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @kahitsan/plugin-sdk CLI — the dev toolkit launcher (Vision §5.1, K3/K4).
|
|
3
|
-
//
|
|
4
|
-
// npx @kahitsan/plugin-sdk <pluginDir> [<pluginDir> …] ← headline form
|
|
5
|
-
// npx @kahitsan/plugin-sdk toolkit <pluginDir> … ← alias (back-compat)
|
|
6
|
-
//
|
|
7
|
-
// Boots the dev-kernel mediator + each plugin on Bun (local SQLite + a synthetic
|
|
8
|
-
// logged-in admin) entirely from the INSTALLED package — no kernel source required.
|
|
9
|
-
// Pass several plugin dirs to exercise the CONTRACT between plugins (does B expose
|
|
10
|
-
// the method A consumes?) locally — no kernel, no Postgres. The harness is Bun-only,
|
|
11
|
-
// so the launcher re-execs under Bun; install it from https://bun.sh.
|
|
12
|
-
import { spawn } from "node:child_process";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { dirname, join } from "node:path";
|
|
15
|
-
|
|
16
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
let args = process.argv.slice(2);
|
|
18
|
-
// `toolkit` is an optional leading verb (kept for back-compat); the headline form
|
|
19
|
-
// takes plugin dirs directly, so a global `npx @kahitsan/plugin-sdk ./a ./b` works.
|
|
20
|
-
if (args[0] === "toolkit") args = args.slice(1);
|
|
21
|
-
const wantsHelp = args.includes("-h") || args.includes("--help");
|
|
22
|
-
const dirs = args.filter((a) => !a.startsWith("-"));
|
|
23
|
-
|
|
24
|
-
if (dirs.length === 0 || wantsHelp) {
|
|
25
|
-
const out = wantsHelp ? console.log : console.error;
|
|
26
|
-
out("usage: npx @kahitsan/plugin-sdk <pluginDir> [<pluginDir> …]");
|
|
27
|
-
out("");
|
|
28
|
-
out(" Boots the local dev toolkit — a dev-kernel mediator + each plugin on SQLite");
|
|
29
|
-
out(" + a synthetic logged-in admin, all from this installed package (no kernel");
|
|
30
|
-
out(" source). Load several plugins at once to exercise their cross-plugin calls:");
|
|
31
|
-
out("");
|
|
32
|
-
out(" npx @kahitsan/plugin-sdk ./kplugin_transactions ./kplugin_payees");
|
|
33
|
-
out("");
|
|
34
|
-
out(" Requires Bun (https://bun.sh). Ctrl-C stops every plugin.");
|
|
35
|
-
process.exit(wantsHelp ? 0 : 2);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// The harness ships as Bun source next to the package root (src/dev/), reachable from
|
|
39
|
-
// the published package without any kernel checkout. dev-host.ts takes N plugin dirs.
|
|
40
|
-
const devHost = join(here, "..", "src", "dev", "dev-host.ts");
|
|
41
|
-
const child = spawn("bun", [devHost, ...dirs], { stdio: "inherit" });
|
|
42
|
-
child.on("error", (err) => {
|
|
43
|
-
if (err && err.code === "ENOENT") {
|
|
44
|
-
console.error(
|
|
45
|
-
"The Hilinga plugin toolkit requires Bun (https://bun.sh) — install it and retry.",
|
|
46
|
-
);
|
|
47
|
-
process.exit(127);
|
|
48
|
-
}
|
|
49
|
-
console.error(err);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
});
|
|
52
|
-
child.on("exit", (code) => process.exit(code ?? 0));
|
package/src/dev/dev-assets.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
// Dev-stub asset backend (Phase 5 K3, Vision Data-Arch §8) — the toolkit's stand-in for
|
|
2
|
-
// the kernel's `/_internal/assets/*` endpoints (server/routes/internal-assets.ts). A plugin
|
|
3
|
-
// built with `createPluginServer({ assets: true })` relays `api.assets.{upload,presign,
|
|
4
|
-
// delete}` to KSERP_KERNEL_URL (the dev-kernel) exactly as in prod; this module answers that
|
|
5
|
-
// SAME contract against LOCAL MinIO + an in-memory ledger, clamped to the active view's
|
|
6
|
-
// workspace + owner — so a toolkit dev can upload AND read a file back (the Gate-5 asset
|
|
7
|
-
// requirement) and the ownership clamp behaves like prod (an asset from another workspace/
|
|
8
|
-
// owner is a 404). Bun-only (matches the dev-kernel); never in prod.
|
|
9
|
-
|
|
10
|
-
import { randomUUID } from "node:crypto";
|
|
11
|
-
|
|
12
|
-
// The slice of Bun.S3Client this file uses — mirrors packages/plugin-server-utils/src/s3.ts
|
|
13
|
-
// so the dev path stores objects identically to prod, just pointed at local MinIO.
|
|
14
|
-
interface BunS3File {
|
|
15
|
-
delete(): Promise<number>;
|
|
16
|
-
}
|
|
17
|
-
interface BunS3Client {
|
|
18
|
-
write(key: string, data: Uint8Array, opts?: { type?: string; acl?: string }): Promise<number>;
|
|
19
|
-
file(key: string): BunS3File;
|
|
20
|
-
presign(key: string, opts?: { method?: string; expiresIn?: number }): string;
|
|
21
|
-
}
|
|
22
|
-
interface BunRuntime {
|
|
23
|
-
S3Client: new (o: {
|
|
24
|
-
accessKeyId: string;
|
|
25
|
-
secretAccessKey: string;
|
|
26
|
-
bucket: string;
|
|
27
|
-
region: string;
|
|
28
|
-
endpoint: string;
|
|
29
|
-
}) => BunS3Client;
|
|
30
|
-
}
|
|
31
|
-
const BUN = (globalThis as { Bun?: BunRuntime }).Bun;
|
|
32
|
-
|
|
33
|
-
/** A Bun S3 client for local MinIO, or null when S3_* isn't configured (→ 503, like prod). */
|
|
34
|
-
function devS3(): BunS3Client | null {
|
|
35
|
-
const { S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY } = process.env;
|
|
36
|
-
if (!BUN || !S3_ENDPOINT || !S3_BUCKET || !S3_ACCESS_KEY || !S3_SECRET_KEY) return null;
|
|
37
|
-
return new BUN.S3Client({
|
|
38
|
-
accessKeyId: S3_ACCESS_KEY,
|
|
39
|
-
secretAccessKey: S3_SECRET_KEY,
|
|
40
|
-
bucket: S3_BUCKET,
|
|
41
|
-
region: S3_REGION || "us-east-1",
|
|
42
|
-
endpoint: new URL(S3_ENDPOINT).origin,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface DevAsset {
|
|
47
|
-
objectKey: string;
|
|
48
|
-
workspaceId: number;
|
|
49
|
-
ownerId: string;
|
|
50
|
-
fileName: string;
|
|
51
|
-
fileSize: number;
|
|
52
|
-
mimeType: string;
|
|
53
|
-
}
|
|
54
|
-
// The in-memory equivalent of the prod `kernel_assets` ledger — a single local dev session,
|
|
55
|
-
// so a process-global Map is enough. Cleared on dev-kernel restart (matches "no seeding").
|
|
56
|
-
const ledger = new Map<number, DevAsset>();
|
|
57
|
-
let nextId = 1;
|
|
58
|
-
|
|
59
|
-
const PRESIGN_TTL_SECONDS = 3600;
|
|
60
|
-
|
|
61
|
-
function json(body: unknown, status = 200): Response {
|
|
62
|
-
return Response.json(body, { status });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** A ledger row, but ONLY if it belongs to the active view's workspace + owner — mirrors
|
|
66
|
-
* the prod `ownedObjectKey` clamp so a wrong-workspace/owner id is indistinguishable from
|
|
67
|
-
* absent (404). */
|
|
68
|
-
function owned(
|
|
69
|
-
assetId: unknown,
|
|
70
|
-
workspaceId: number,
|
|
71
|
-
ownerId: string,
|
|
72
|
-
): { id: number; row: DevAsset } | null {
|
|
73
|
-
const id = Number(assetId);
|
|
74
|
-
if (!Number.isInteger(id) || id <= 0) return null;
|
|
75
|
-
const row = ledger.get(id);
|
|
76
|
-
if (!row || row.workspaceId !== workspaceId || row.ownerId !== ownerId) return null;
|
|
77
|
-
return { id, row };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
interface DevIdentity {
|
|
81
|
-
workspaceId: number;
|
|
82
|
-
ownerId: string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function doUpload(req: Request, id: DevIdentity, s3: BunS3Client): Promise<Response> {
|
|
86
|
-
const body = (await req.json().catch(() => ({}))) as {
|
|
87
|
-
pluginId?: string;
|
|
88
|
-
fileName?: string;
|
|
89
|
-
mimeType?: string;
|
|
90
|
-
data?: string;
|
|
91
|
-
};
|
|
92
|
-
const pluginId = String(body.pluginId ?? "");
|
|
93
|
-
const fileName = String(body.fileName ?? "");
|
|
94
|
-
const mimeType = String(body.mimeType ?? "");
|
|
95
|
-
if (!pluginId || !fileName || !mimeType || typeof body.data !== "string") {
|
|
96
|
-
return json({ error: "pluginId, fileName, mimeType, data required" }, 400);
|
|
97
|
-
}
|
|
98
|
-
const buf = Buffer.from(body.data, "base64");
|
|
99
|
-
// Kernel-built key ({ws}/{plugin}/{rand}), never caller-supplied — a plugin can't write
|
|
100
|
-
// outside its own prefix, same invariant as prod.
|
|
101
|
-
const objectKey = `${id.workspaceId}/${pluginId}/${randomUUID()}`;
|
|
102
|
-
await s3.write(objectKey, new Uint8Array(buf), { type: mimeType, acl: "private" });
|
|
103
|
-
const assetId = nextId++;
|
|
104
|
-
ledger.set(assetId, {
|
|
105
|
-
objectKey,
|
|
106
|
-
workspaceId: id.workspaceId,
|
|
107
|
-
ownerId: id.ownerId,
|
|
108
|
-
fileName,
|
|
109
|
-
fileSize: buf.length,
|
|
110
|
-
mimeType,
|
|
111
|
-
});
|
|
112
|
-
return json({ id: assetId, objectKey, fileName, fileSize: buf.length, mimeType });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function doPresign(req: Request, id: DevIdentity, s3: BunS3Client): Promise<Response> {
|
|
116
|
-
const body = (await req.json().catch(() => ({}))) as { assetId?: number; expirySeconds?: number };
|
|
117
|
-
const hit = owned(body.assetId, id.workspaceId, id.ownerId);
|
|
118
|
-
if (!hit) return json({ error: "asset not found" }, 404);
|
|
119
|
-
const ttl =
|
|
120
|
-
Number.isInteger(body.expirySeconds) && (body.expirySeconds as number) > 0
|
|
121
|
-
? (body.expirySeconds as number)
|
|
122
|
-
: PRESIGN_TTL_SECONDS;
|
|
123
|
-
const presignedUrl = s3.presign(hit.row.objectKey, { method: "GET", expiresIn: ttl });
|
|
124
|
-
const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
|
|
125
|
-
return json({ presignedUrl, expiresAt });
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function doDelete(req: Request, id: DevIdentity, s3: BunS3Client): Promise<Response> {
|
|
129
|
-
const body = (await req.json().catch(() => ({}))) as { assetId?: number };
|
|
130
|
-
const hit = owned(body.assetId, id.workspaceId, id.ownerId);
|
|
131
|
-
if (!hit) return json({ error: "asset not found" }, 404);
|
|
132
|
-
await s3.file(hit.row.objectKey).delete();
|
|
133
|
-
ledger.delete(hit.id);
|
|
134
|
-
return json({});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Answer the `/_internal/assets/*` contract for the dev-kernel, clamped to the supplied
|
|
139
|
-
* active-view identity. Returns null for a non-asset path (caller falls through to its
|
|
140
|
-
* other routes).
|
|
141
|
-
*/
|
|
142
|
-
export async function handleDevAsset(
|
|
143
|
-
path: string,
|
|
144
|
-
req: Request,
|
|
145
|
-
identity: DevIdentity,
|
|
146
|
-
): Promise<Response | null> {
|
|
147
|
-
if (!path.startsWith("/_internal/assets/")) return null;
|
|
148
|
-
const s3 = devS3();
|
|
149
|
-
if (!s3) return json({ error: "asset storage not configured (set S3_* for local MinIO)" }, 503);
|
|
150
|
-
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
151
|
-
|
|
152
|
-
if (path === "/_internal/assets/upload") return doUpload(req, identity, s3);
|
|
153
|
-
if (path === "/_internal/assets/presign") return doPresign(req, identity, s3);
|
|
154
|
-
if (path === "/_internal/assets/delete") return doDelete(req, identity, s3);
|
|
155
|
-
return json({ error: "unknown asset endpoint" }, 404);
|
|
156
|
-
}
|
package/src/dev/dev-host.ts
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
// The dev host (Vision §5.1, K3) — the local environment a plugin developer runs.
|
|
2
|
-
// Boots ONE OR MORE plugins on Bun, each on its own SQLite + synthetic logged-in
|
|
3
|
-
// admin (via the preload), behind a tiny "dev kernel" mediator that stands in for
|
|
4
|
-
// the production kernel's `/_internal/rpc` gateway so cross-plugin calls work — and
|
|
5
|
-
// **degrade gracefully** when a peer isn't loaded (the resilience rule: a missing
|
|
6
|
-
// dependency shows an error, it never crashes the app).
|
|
7
|
-
//
|
|
8
|
-
// bun packages/plugin-sdk/src/dev/dev-host.ts <pluginDir> [<pluginDir> …]
|
|
9
|
-
//
|
|
10
|
-
// Each plugin's own `server/main.ts` is byte-identical to prod — it just finds the
|
|
11
|
-
// dev hooks installed by the preload and runs on the local engine. Loading several
|
|
12
|
-
// at once lets a developer exercise the CONTRACT between plugins (does B expose the
|
|
13
|
-
// method A consumes?) entirely locally, no kernel, no Postgres.
|
|
14
|
-
|
|
15
|
-
import { readFileSync } from "node:fs";
|
|
16
|
-
import { join, resolve } from "node:path";
|
|
17
|
-
import { spawn, type Subprocess } from "bun";
|
|
18
|
-
import { handleDevAsset } from "./dev-assets.js";
|
|
19
|
-
|
|
20
|
-
const DEV_SECRET = "dev-stub-internal-secret";
|
|
21
|
-
const BASE_PORT = Number(process.env.KSERP_DEV_BASE_PORT || "4500");
|
|
22
|
-
const PRELOAD = join(import.meta.dir, "preload.ts");
|
|
23
|
-
|
|
24
|
-
interface Manifest {
|
|
25
|
-
name: string;
|
|
26
|
-
schemas?: string[];
|
|
27
|
-
}
|
|
28
|
-
interface Loaded {
|
|
29
|
-
dir: string;
|
|
30
|
-
name: string;
|
|
31
|
-
port: number;
|
|
32
|
-
proc: Subprocess;
|
|
33
|
-
status: "starting" | "up" | "failed";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const dirs = process.argv.slice(2).map((d) => resolve(d));
|
|
37
|
-
if (dirs.length === 0) {
|
|
38
|
-
console.error("usage: bun dev-host.ts <pluginDir> [<pluginDir> …]");
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ── assign ports + read manifests (the mediator routes by plugin name → port) ──
|
|
43
|
-
const mediatorPort = BASE_PORT;
|
|
44
|
-
const registry = new Map<string, number>();
|
|
45
|
-
const loaded: Loaded[] = [];
|
|
46
|
-
dirs.forEach((dir, i) => {
|
|
47
|
-
const manifest = JSON.parse(readFileSync(join(dir, "plugin.manifest.json"), "utf8")) as Manifest;
|
|
48
|
-
const port = BASE_PORT + 1 + i;
|
|
49
|
-
registry.set(manifest.name, port);
|
|
50
|
-
loaded.push({ dir, name: manifest.name, port, proc: undefined as never, status: "starting" });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Answer the api.assets relay (`/_internal/assets/*`) against local MinIO + an in-memory
|
|
54
|
-
// ledger, clamped to the active (synthetic) identity the relay forwarded in x-kserp-identity.
|
|
55
|
-
// Returns null for a non-asset path. Extracted from the mediator's fetch to keep that handler
|
|
56
|
-
// within its complexity budget.
|
|
57
|
-
async function tryAssetRelay(pathname: string, req: Request): Promise<Response | null> {
|
|
58
|
-
if (!pathname.startsWith("/_internal/assets/")) return null;
|
|
59
|
-
let identity = { workspaceId: 1, ownerId: "dev-admin" };
|
|
60
|
-
const idHdr = req.headers.get("x-kserp-identity");
|
|
61
|
-
if (idHdr) {
|
|
62
|
-
try {
|
|
63
|
-
const v = JSON.parse(idHdr) as { workspaceId?: number; userId?: string };
|
|
64
|
-
identity = {
|
|
65
|
-
workspaceId: Number(v.workspaceId) || 1,
|
|
66
|
-
ownerId: String(v.userId ?? "dev-admin"),
|
|
67
|
-
};
|
|
68
|
-
} catch {
|
|
69
|
-
/* malformed → fall back to the default synthetic admin */
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return handleDevAsset(pathname, req, identity);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ── the dev-kernel mediator: stands in for the prod kernel's /_internal/rpc. Routes
|
|
76
|
-
// a cross-plugin call to the target's /_internal/services/<method>; a peer that
|
|
77
|
-
// isn't loaded → 503, which the SDK's tryCallPlugin turns into a graceful null and
|
|
78
|
-
// callPlugin into a thrown error the consumer's route reports (never a crash). ──
|
|
79
|
-
const mediator = Bun.serve({
|
|
80
|
-
port: mediatorPort,
|
|
81
|
-
async fetch(req) {
|
|
82
|
-
const url = new URL(req.url);
|
|
83
|
-
if (url.pathname === "/_internal/rpc" && req.method === "POST") {
|
|
84
|
-
const body = (await req.json()) as { target: string; method: string; args: unknown };
|
|
85
|
-
const targetPort = registry.get(body.target);
|
|
86
|
-
if (!targetPort) {
|
|
87
|
-
return Response.json(
|
|
88
|
-
{ error: `plugin "${body.target}" is not loaded in this dev session` },
|
|
89
|
-
{ status: 503 },
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
try {
|
|
93
|
-
const upstream = await fetch(
|
|
94
|
-
`http://127.0.0.1:${targetPort}/_internal/services/${encodeURIComponent(body.method)}`,
|
|
95
|
-
{
|
|
96
|
-
method: "POST",
|
|
97
|
-
headers: {
|
|
98
|
-
"content-type": "application/json",
|
|
99
|
-
"x-kserp-internal": DEV_SECRET,
|
|
100
|
-
...(req.headers.get("x-kserp-identity")
|
|
101
|
-
? { "x-kserp-identity": req.headers.get("x-kserp-identity")! }
|
|
102
|
-
: {}),
|
|
103
|
-
},
|
|
104
|
-
body: JSON.stringify(body.args ?? {}),
|
|
105
|
-
},
|
|
106
|
-
);
|
|
107
|
-
return new Response(await upstream.text(), {
|
|
108
|
-
status: upstream.status,
|
|
109
|
-
headers: { "content-type": "application/json" },
|
|
110
|
-
});
|
|
111
|
-
} catch {
|
|
112
|
-
// target loaded but unreachable (still booting / crashed) → 503 = degrade.
|
|
113
|
-
return Response.json({ error: `plugin "${body.target}" unreachable` }, { status: 503 });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
// api.assets (A1) in the toolkit: a plugin relays upload/presign/delete here.
|
|
117
|
-
const asset = await tryAssetRelay(url.pathname, req);
|
|
118
|
-
if (asset) return asset;
|
|
119
|
-
if (url.pathname === "/" || url.pathname === "/_dev/status") {
|
|
120
|
-
return Response.json({
|
|
121
|
-
mediator: `http://127.0.0.1:${mediatorPort}`,
|
|
122
|
-
plugins: loaded.map((l) => ({ name: l.name, port: l.port, status: l.status })),
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
return new Response("dev-kernel", { status: 404 });
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// ── spawn each plugin (Bun + preload → SQLite + synthetic admin) ──
|
|
130
|
-
for (const l of loaded) {
|
|
131
|
-
const manifest = JSON.parse(
|
|
132
|
-
readFileSync(join(l.dir, "plugin.manifest.json"), "utf8"),
|
|
133
|
-
) as Manifest;
|
|
134
|
-
l.proc = spawn({
|
|
135
|
-
cmd: ["bun", "--preload", PRELOAD, join(l.dir, "server", "main.ts")],
|
|
136
|
-
cwd: l.dir,
|
|
137
|
-
env: {
|
|
138
|
-
...process.env,
|
|
139
|
-
KSERP_DEV_PLUGIN_DIR: l.dir,
|
|
140
|
-
KSERP_DB_ENGINE: "sqlite",
|
|
141
|
-
KSERP_PLUGIN_PORT: String(l.port),
|
|
142
|
-
KSERP_PLUGIN_BIND: "127.0.0.1",
|
|
143
|
-
KSERP_PLUGIN_SCHEMAS: (manifest.schemas ?? []).join(","),
|
|
144
|
-
KSERP_KERNEL_URL: `http://127.0.0.1:${mediatorPort}`,
|
|
145
|
-
KSERP_INTERNAL_SECRET: DEV_SECRET,
|
|
146
|
-
},
|
|
147
|
-
stdout: "inherit",
|
|
148
|
-
stderr: "inherit",
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ── wait for health, report (a plugin whose own boot throws — e.g. a peer-dependent
|
|
153
|
-
// migration — is reported failed, not fatal to the host) ──
|
|
154
|
-
async function waitHealthy(l: Loaded, timeoutMs = 15000): Promise<void> {
|
|
155
|
-
const deadline = Date.now() + timeoutMs;
|
|
156
|
-
while (Date.now() < deadline) {
|
|
157
|
-
try {
|
|
158
|
-
const r = await fetch(`http://127.0.0.1:${l.port}/_internal/health`);
|
|
159
|
-
if (r.ok) {
|
|
160
|
-
l.status = "up";
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
} catch {
|
|
164
|
-
/* not up yet */
|
|
165
|
-
}
|
|
166
|
-
if (l.proc.exitCode !== null) {
|
|
167
|
-
l.status = "failed";
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
await Bun.sleep(300);
|
|
171
|
-
}
|
|
172
|
-
l.status = l.proc.exitCode === null ? "up" : "failed";
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
await Promise.all(loaded.map((l) => waitHealthy(l)));
|
|
176
|
-
|
|
177
|
-
console.log("\n────────────────────────────────────────────────────────");
|
|
178
|
-
console.log(` dev kernel http://127.0.0.1:${mediatorPort} (cross-plugin RPC mediator)`);
|
|
179
|
-
for (const l of loaded) {
|
|
180
|
-
const mark = l.status === "up" ? "✓" : "✗";
|
|
181
|
-
console.log(` ${mark} ${l.name.padEnd(20)} http://127.0.0.1:${l.port} [${l.status}]`);
|
|
182
|
-
}
|
|
183
|
-
console.log("────────────────────────────────────────────────────────");
|
|
184
|
-
console.log(
|
|
185
|
-
` ${loaded.filter((l) => l.status === "up").length}/${loaded.length} plugins up. Ctrl-C to stop.\n`,
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
function shutdown() {
|
|
189
|
-
for (const l of loaded) l.proc.kill();
|
|
190
|
-
mediator.stop(true);
|
|
191
|
-
process.exit(0);
|
|
192
|
-
}
|
|
193
|
-
process.on("SIGINT", shutdown);
|
|
194
|
-
process.on("SIGTERM", shutdown);
|