@kahitsan/plugin-sdk 0.3.0 → 0.4.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.
Files changed (64) hide show
  1. package/bin/cli.mjs +6 -4
  2. package/dist/index.js +1 -1
  3. package/package.json +3 -2
  4. package/shell/_build/.vite/manifest.json +189 -0
  5. package/shell/_build/.vite/manifest.json.br +0 -0
  6. package/shell/_build/.vite/manifest.json.gz +0 -0
  7. package/shell/_build/assets/_...slug_-BNycC89F.js +1 -0
  8. package/shell/_build/assets/_...slug_-BNycC89F.js.br +0 -0
  9. package/shell/_build/assets/_...slug_-BNycC89F.js.gz +0 -0
  10. package/shell/_build/assets/api-keys-zDu0DTIi.js +1 -0
  11. package/shell/_build/assets/auth-CPW4mGTx.js +70 -0
  12. package/shell/_build/assets/auth-CPW4mGTx.js.br +0 -0
  13. package/shell/_build/assets/auth-CPW4mGTx.js.gz +0 -0
  14. package/shell/_build/assets/client-B0tH7z8C.css +1 -0
  15. package/shell/_build/assets/client-B0tH7z8C.css.br +0 -0
  16. package/shell/_build/assets/client-B0tH7z8C.css.gz +0 -0
  17. package/shell/_build/assets/client-BuqEzg-5.js +2 -0
  18. package/shell/_build/assets/client-BuqEzg-5.js.br +0 -0
  19. package/shell/_build/assets/client-BuqEzg-5.js.gz +0 -0
  20. package/shell/_build/assets/components-kMeatXhB.js +1 -0
  21. package/shell/_build/assets/dashboard-BwMkrsX2.js +1 -0
  22. package/shell/_build/assets/index-dCQOOcel.js +1 -0
  23. package/shell/_build/assets/login-BwMkrsX2.js +1 -0
  24. package/shell/_build/assets/members-CKVe_2mI.js +1 -0
  25. package/shell/_build/assets/plugin-ui-rgxz3UHD.js +10 -0
  26. package/shell/_build/assets/plugin-ui-rgxz3UHD.js.br +0 -0
  27. package/shell/_build/assets/plugin-ui-rgxz3UHD.js.gz +0 -0
  28. package/shell/_build/assets/register-BwMkrsX2.js +1 -0
  29. package/shell/_build/assets/routing-BlHUL5zP.js +1 -0
  30. package/shell/_build/assets/routing-BlHUL5zP.js.br +0 -0
  31. package/shell/_build/assets/routing-BlHUL5zP.js.gz +0 -0
  32. package/shell/_build/assets/settings-BwMkrsX2.js +1 -0
  33. package/shell/_build/assets/users-BwMkrsX2.js +1 -0
  34. package/shell/_build/assets/workspace-nav-DsOJmRdY.js +1 -0
  35. package/shell/_build/assets/workspace-nav-DsOJmRdY.js.br +0 -0
  36. package/shell/_build/assets/workspace-nav-DsOJmRdY.js.gz +0 -0
  37. package/shell/_build/assets/workspace-settings-BwMkrsX2.js +1 -0
  38. package/shell/_build/assets/workspaces-BwMkrsX2.js +1 -0
  39. package/shell/_headers +20 -0
  40. package/shell/_redirects +4 -0
  41. package/shell/_server/assets/app-B0tH7z8C.css +1 -0
  42. package/shell/_server/assets/app-B0tH7z8C.css.br +0 -0
  43. package/shell/_server/assets/app-B0tH7z8C.css.gz +0 -0
  44. package/shell/apple-touch-icon.png +0 -0
  45. package/shell/dev/apple-touch-icon.png +0 -0
  46. package/shell/dev/favicon-32.png +0 -0
  47. package/shell/dev/favicon.ico +0 -0
  48. package/shell/dev/manifest.webmanifest +31 -0
  49. package/shell/dev/pwa-192.png +0 -0
  50. package/shell/dev/pwa-512-maskable.png +0 -0
  51. package/shell/dev/pwa-512.png +0 -0
  52. package/shell/favicon-32.png +0 -0
  53. package/shell/favicon.ico +0 -0
  54. package/shell/index.html +1 -0
  55. package/shell/index.html.br +0 -0
  56. package/shell/index.html.gz +0 -0
  57. package/shell/manifest.webmanifest +31 -0
  58. package/shell/pwa-192.png +0 -0
  59. package/shell/pwa-512-maskable.png +0 -0
  60. package/shell/pwa-512.png +0 -0
  61. package/shell/sw.js +103 -0
  62. package/shell/sw.js.br +0 -0
  63. package/shell/sw.js.gz +0 -0
  64. package/src/dev/serve.ts +141 -0
@@ -0,0 +1 @@
1
+ <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="theme-color" content="#0a0a0a"><link rel="icon" href="/favicon.ico" sizes="any"><link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32"><link rel="manifest" href="/manifest.webmanifest"><link rel="apple-touch-icon" href="/apple-touch-icon.png"><link href="/_build/assets/client-B0tH7z8C.css" rel="stylesheet" fetchPriority="high" /><link href="/_build/assets/routing-BlHUL5zP.js" rel="modulepreload" /><link href="/_build/assets/components-kMeatXhB.js" rel="modulepreload" /><link href="/_build/assets/auth-CPW4mGTx.js" rel="modulepreload" /><link href="/_build/assets/plugin-ui-rgxz3UHD.js" rel="modulepreload" /><link href="/_build/assets/workspace-nav-DsOJmRdY.js" rel="modulepreload" /><link href="/_build/assets/client-BuqEzg-5.js" rel="modulepreload" /></head><body><div id="app"></div><!--$--><script>window.manifest = {"src/routes/[...slug].tsx?pick=default&pick=$css":{"output":"/_build/assets/_...slug_-BNycC89F.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/auth-CPW4mGTx.js","key":"/_build/assets/auth-CPW4mGTx.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/plugin-ui-rgxz3UHD.js","key":"/_build/assets/plugin-ui-rgxz3UHD.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/_...slug_-BNycC89F.js","key":"/_build/assets/_...slug_-BNycC89F.js","rel":"modulepreload"}}]},"src/routes/api-keys.tsx?pick=default&pick=$css":{"output":"/_build/assets/api-keys-zDu0DTIi.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/api-keys-zDu0DTIi.js","key":"/_build/assets/api-keys-zDu0DTIi.js","rel":"modulepreload"}}]},"src/routes/dashboard.tsx?pick=thisWeekToken&pick=thisYearToken&pick=default&pick=$css":{"output":"/_build/assets/dashboard-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/dashboard-BwMkrsX2.js","key":"/_build/assets/dashboard-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/index.tsx?pick=default&pick=$css":{"output":"/_build/assets/index-dCQOOcel.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/auth-CPW4mGTx.js","key":"/_build/assets/auth-CPW4mGTx.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/workspace-nav-DsOJmRdY.js","key":"/_build/assets/workspace-nav-DsOJmRdY.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/index-dCQOOcel.js","key":"/_build/assets/index-dCQOOcel.js","rel":"modulepreload"}}]},"src/routes/login.tsx?pick=default&pick=$css":{"output":"/_build/assets/login-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/login-BwMkrsX2.js","key":"/_build/assets/login-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/members.tsx?pick=default&pick=$css":{"output":"/_build/assets/members-CKVe_2mI.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/members-CKVe_2mI.js","key":"/_build/assets/members-CKVe_2mI.js","rel":"modulepreload"}}]},"src/routes/register.tsx?pick=default&pick=$css":{"output":"/_build/assets/register-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/register-BwMkrsX2.js","key":"/_build/assets/register-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/settings.tsx?pick=default&pick=$css":{"output":"/_build/assets/settings-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/settings-BwMkrsX2.js","key":"/_build/assets/settings-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/users.tsx?pick=default&pick=$css":{"output":"/_build/assets/users-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/users-BwMkrsX2.js","key":"/_build/assets/users-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/workspace-settings.tsx?pick=default&pick=$css":{"output":"/_build/assets/workspace-settings-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/workspace-settings-BwMkrsX2.js","key":"/_build/assets/workspace-settings-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/workspaces.tsx?pick=default&pick=$css":{"output":"/_build/assets/workspaces-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/workspaces-BwMkrsX2.js","key":"/_build/assets/workspaces-BwMkrsX2.js","rel":"modulepreload"}}]},"virtual:$vinxi/handler/client":{"output":"/_build/assets/client-BuqEzg-5.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/client-B0tH7z8C.css","key":"/_build/assets/client-B0tH7z8C.css","rel":"stylesheet","fetchPriority":"high"}},{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/auth-CPW4mGTx.js","key":"/_build/assets/auth-CPW4mGTx.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/plugin-ui-rgxz3UHD.js","key":"/_build/assets/plugin-ui-rgxz3UHD.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/workspace-nav-DsOJmRdY.js","key":"/_build/assets/workspace-nav-DsOJmRdY.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/client-BuqEzg-5.js","key":"/_build/assets/client-BuqEzg-5.js","rel":"modulepreload"}}]}}</script><script type="module" src="/_build/assets/client-BuqEzg-5.js"></script><!--/--></body></html>
Binary file
Binary file
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "Hilinga",
3
+ "short_name": "Hilinga",
4
+ "description": "Hilinga by Kahitsan Corp. — clients, transactions, packages, vouchers, and timesheets.",
5
+ "start_url": "/",
6
+ "scope": "/",
7
+ "display": "standalone",
8
+ "orientation": "any",
9
+ "background_color": "#0a0a0a",
10
+ "theme_color": "#0a0a0a",
11
+ "icons": [
12
+ {
13
+ "src": "/pwa-192.png",
14
+ "sizes": "192x192",
15
+ "type": "image/png",
16
+ "purpose": "any"
17
+ },
18
+ {
19
+ "src": "/pwa-512.png",
20
+ "sizes": "512x512",
21
+ "type": "image/png",
22
+ "purpose": "any"
23
+ },
24
+ {
25
+ "src": "/pwa-512-maskable.png",
26
+ "sizes": "512x512",
27
+ "type": "image/png",
28
+ "purpose": "maskable"
29
+ }
30
+ ]
31
+ }
Binary file
Binary file
Binary file
package/shell/sw.js ADDED
@@ -0,0 +1,103 @@
1
+ /* ksERP service worker — generated at build time. Do not edit by hand. */
2
+ /* The build replaces 0.76.4 with the value from package.json. */
3
+ /* eslint-env serviceworker */
4
+
5
+ const APP_VERSION = "0.76.4";
6
+ const CACHE_NAME = "ks-erp-v" + APP_VERSION;
7
+
8
+ // App shell — the bare-minimum entrypoints needed to render an offline page.
9
+ // We keep this short because the SPA's hashed chunks can't all be listed; the
10
+ // fetch handler caches them as the user visits them, with network-first.
11
+ const PRECACHE_URLS = ["/", "/manifest.webmanifest"];
12
+
13
+ self.addEventListener("install", (event) => {
14
+ event.waitUntil(
15
+ caches.open(CACHE_NAME).then((cache) => {
16
+ return cache.addAll(PRECACHE_URLS).catch(() => {
17
+ // Best-effort: if any URL 404s in dev/preview, don't block install.
18
+ });
19
+ }),
20
+ );
21
+ // Do NOT skipWaiting() here. We want updates to wait until the user clicks
22
+ // Reload in the PwaUpdatePrompt banner. The user-driven flow lives in the
23
+ // message handler below (SKIP_WAITING). On a fresh install with no
24
+ // previous controller, the worker activates immediately anyway (nothing
25
+ // to wait for), so this only changes behavior on the update path.
26
+ });
27
+
28
+ self.addEventListener("activate", (event) => {
29
+ event.waitUntil(
30
+ (async () => {
31
+ const keys = await caches.keys();
32
+ await Promise.all(
33
+ keys
34
+ .filter((key) => key.startsWith("ks-erp-v") && key !== CACHE_NAME)
35
+ .map((key) => caches.delete(key)),
36
+ );
37
+ await self.clients.claim();
38
+ })(),
39
+ );
40
+ });
41
+
42
+ self.addEventListener("fetch", (event) => {
43
+ const req = event.request;
44
+
45
+ // Only handle GETs. Don't touch /api/* — let those go straight to the network.
46
+ if (req.method !== "GET") return;
47
+
48
+ const url = new URL(req.url);
49
+ if (url.origin !== self.location.origin) return;
50
+ if (url.pathname.startsWith("/api/") || url.pathname.startsWith("/uploads/")) return;
51
+
52
+ // Network-first for navigations: fresh deploys aren't masked by stale shell.
53
+ if (req.mode === "navigate") {
54
+ event.respondWith(
55
+ fetch(req)
56
+ .then((res) => {
57
+ const copy = res.clone();
58
+ caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)).catch(() => {});
59
+ return res;
60
+ })
61
+ .catch(async () => {
62
+ const cached = await caches.match(req);
63
+ if (cached) return cached;
64
+ const fallback = await caches.match("/");
65
+ return (
66
+ fallback ||
67
+ new Response("You are offline.", { status: 503, headers: { "Content-Type": "text/plain" } })
68
+ );
69
+ }),
70
+ );
71
+ return;
72
+ }
73
+
74
+ // Static assets (JS/CSS/icons): stale-while-revalidate.
75
+ event.respondWith(
76
+ caches.match(req).then((cached) => {
77
+ const networkFetch = fetch(req)
78
+ .then((res) => {
79
+ if (res && res.ok) {
80
+ const copy = res.clone();
81
+ caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)).catch(() => {});
82
+ }
83
+ return res;
84
+ })
85
+ .catch(
86
+ () =>
87
+ cached ||
88
+ new Response("Offline", {
89
+ status: 503,
90
+ headers: { "Content-Type": "text/plain" },
91
+ }),
92
+ );
93
+ return cached || networkFetch;
94
+ }),
95
+ );
96
+ });
97
+
98
+ // Allow the page to ask the SW to take over immediately after an update.
99
+ self.addEventListener("message", (event) => {
100
+ if (event.data && event.data.type === "SKIP_WAITING") {
101
+ self.skipWaiting();
102
+ }
103
+ });
package/shell/sw.js.br ADDED
Binary file
package/shell/sw.js.gz ADDED
Binary file
@@ -0,0 +1,141 @@
1
+ // §5.3 — the FULL toolkit launcher. Boots N plugins + the dev-kernel (the complete
2
+ // `/api` + `/_internal` backend) AND serves the prebuilt kernel-identical host-UI
3
+ // SHELL, so `npx @kahitsan/plugin-sdk <dir>…` brings up the real visual app with ZERO
4
+ // kserp source. The shell is a static SPA (the host UI is `ssr:false`, so a toolkit
5
+ // `vinxi build` emits pure static files) shipped in the package at `<pkg>/shell`.
6
+ //
7
+ // bun packages/plugin-sdk/src/dev/serve.ts <pluginDir> [<pluginDir> …]
8
+ //
9
+ // Topology: the browser only ever talks to THIS server (one origin). Static asset →
10
+ // served from ./shell; `/api/*` or `/_internal/*` → proxied to the dev-kernel (which
11
+ // itself proxies `<basePath>/_ui/remote.js` to each plugin); anything else → index.html
12
+ // (the SPA owns client routing). dev-host.ts remains the headless/backend-only variant.
13
+
14
+ import { readFileSync, existsSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+ import { spawn, type Subprocess } from "bun";
17
+
18
+ const PRELOAD = join(import.meta.dir, "preload.ts");
19
+ const DEV_KERNEL = join(import.meta.dir, "dev-kernel.ts");
20
+ // The prebuilt static shell: `<pkg>/shell` (src/dev → ../../shell). Built by
21
+ // `npm run build:toolkit-shell`; shipped via package.json `files`.
22
+ const SHELL_DIR = resolve(import.meta.dir, "../../shell");
23
+ const DEV_SECRET = "dev-stub-internal-secret";
24
+ const BASE = Number(process.env.KSERP_DEV_BASE_PORT || "4500");
25
+
26
+ const dirs = process.argv.slice(2).map((d) => resolve(d));
27
+ if (dirs.length === 0) {
28
+ console.error("usage: bun serve.ts <pluginDir> [<pluginDir> …]");
29
+ process.exit(1);
30
+ }
31
+ if (!existsSync(join(SHELL_DIR, "index.html"))) {
32
+ console.error(
33
+ `[toolkit] host-UI shell missing at ${SHELL_DIR}.\n` +
34
+ ` In a source checkout run \`npm run build:toolkit-shell\`; in an installed package this\n` +
35
+ ` means the published tarball didn't ship ./shell — reinstall @kahitsan/plugin-sdk.`,
36
+ );
37
+ process.exit(1);
38
+ }
39
+
40
+ const uiPort = BASE; // the visual app the developer opens
41
+ const kernelPort = BASE + 1; // dev-kernel: /api + /_internal
42
+ const KERNEL = `http://127.0.0.1:${kernelPort}`;
43
+
44
+ interface PluginReg {
45
+ name: string;
46
+ dir: string;
47
+ port: number;
48
+ }
49
+ const regs: PluginReg[] = dirs.map((dir, i) => {
50
+ const m = JSON.parse(readFileSync(join(dir, "plugin.manifest.json"), "utf8")) as {
51
+ name: string;
52
+ };
53
+ return { name: m.name, dir, port: BASE + 2 + i };
54
+ });
55
+
56
+ // ── spawn each plugin (Bun + preload → SQLite + synthetic admin), pointed at the dev-kernel ──
57
+ const procs: Subprocess[] = [];
58
+ for (const r of regs) {
59
+ const m = JSON.parse(readFileSync(join(r.dir, "plugin.manifest.json"), "utf8")) as {
60
+ schemas?: string[];
61
+ };
62
+ procs.push(
63
+ spawn({
64
+ cmd: ["bun", "--preload", PRELOAD, join(r.dir, "server", "main.ts")],
65
+ cwd: r.dir,
66
+ env: {
67
+ ...process.env,
68
+ KSERP_DEV_PLUGIN_DIR: r.dir,
69
+ KSERP_DB_ENGINE: "sqlite",
70
+ KSERP_PLUGIN_PORT: String(r.port),
71
+ KSERP_PLUGIN_BIND: "127.0.0.1",
72
+ KSERP_PLUGIN_SCHEMAS: (m.schemas ?? []).join(","),
73
+ KSERP_KERNEL_URL: KERNEL,
74
+ KSERP_INTERNAL_SECRET: DEV_SECRET,
75
+ },
76
+ stdout: "inherit",
77
+ stderr: "inherit",
78
+ }),
79
+ );
80
+ }
81
+
82
+ // ── spawn the dev-kernel (serves /api/* + /_internal/rpc + /_internal/assets, and proxies
83
+ // each plugin's bundle + routes by basePath) ──
84
+ const kernelProc = spawn({
85
+ cmd: ["bun", DEV_KERNEL],
86
+ env: {
87
+ ...process.env,
88
+ KSERP_DEV_KERNEL_PORT: String(kernelPort),
89
+ KSERP_INTERNAL_SECRET: DEV_SECRET,
90
+ KSERP_DEV_PLUGINS: JSON.stringify(
91
+ regs.map((r) => ({ name: r.name, dir: r.dir, port: r.port })),
92
+ ),
93
+ },
94
+ stdout: "inherit",
95
+ stderr: "inherit",
96
+ });
97
+
98
+ // ── the one origin the browser talks to: static shell + a proxy to the dev-kernel ──
99
+ const ui = Bun.serve({
100
+ port: uiPort,
101
+ async fetch(req) {
102
+ const url = new URL(req.url);
103
+ const path = url.pathname;
104
+ // backend (kernel API, cross-plugin RPC, assets, AND plugin bundles under /api/<base>/_ui)
105
+ if (path === "/api" || path.startsWith("/api/") || path.startsWith("/_internal/")) {
106
+ const upstream = await fetch(KERNEL + path + url.search, {
107
+ method: req.method,
108
+ headers: req.headers,
109
+ body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),
110
+ redirect: "manual",
111
+ }).catch(() => null);
112
+ if (!upstream) return new Response("dev-kernel not ready", { status: 502 });
113
+ return new Response(upstream.body, { status: upstream.status, headers: upstream.headers });
114
+ }
115
+ // a real static asset (index.html, /_build/*, icons, /dev/*)
116
+ const rel = path === "/" ? "/index.html" : path;
117
+ const asset = Bun.file(join(SHELL_DIR, rel));
118
+ if (await asset.exists()) return new Response(asset);
119
+ // otherwise it's a client route (/payees, /transactions, …) → the SPA owns it
120
+ return new Response(Bun.file(join(SHELL_DIR, "index.html")), {
121
+ headers: { "content-type": "text/html" },
122
+ });
123
+ },
124
+ });
125
+
126
+ console.log("\n────────────────────────────────────────────────────────");
127
+ console.log(` ▸ toolkit app http://127.0.0.1:${ui.port} ← open this`);
128
+ console.log(` ▸ dev-kernel http://127.0.0.1:${kernelPort} (/api + /_internal)`);
129
+ for (const r of regs) console.log(` ▸ ${r.name.padEnd(18)} :${r.port}`);
130
+ console.log("────────────────────────────────────────────────────────");
131
+ console.log(" The kernel-identical shell is served from the package — no kserp source needed.");
132
+ console.log(" Ctrl-C stops everything.\n");
133
+
134
+ function shutdown() {
135
+ for (const p of procs) p.kill();
136
+ kernelProc.kill();
137
+ ui.stop(true);
138
+ process.exit(0);
139
+ }
140
+ process.on("SIGINT", shutdown);
141
+ process.on("SIGTERM", shutdown);