@femtomc/mu-server 26.2.27 → 26.2.28

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/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { createServer } from "./server.js";
2
+ import { createServerAsync } from "./server.js";
3
3
  import { findRepoRoot } from "@femtomc/mu-core/node";
4
4
  const port = parseInt(process.env.PORT || "3000", 10);
5
5
  let repoRoot;
@@ -12,8 +12,24 @@ catch {
12
12
  }
13
13
  console.log(`Starting mu-server on port ${port}...`);
14
14
  console.log(`Repository root: ${repoRoot}`);
15
- const server = createServer({ repoRoot, port });
16
- Bun.serve(server);
15
+ const { serverConfig, controlPlane } = await createServerAsync({ repoRoot, port });
16
+ const server = Bun.serve(serverConfig);
17
17
  console.log(`Server running at http://localhost:${port}`);
18
- console.log(`Health check: http://localhost:${port}/healthz`);
19
- console.log(`API Status: http://localhost:${port}/api/status`);
18
+ if (controlPlane && controlPlane.activeAdapters.length > 0) {
19
+ console.log("Control plane: active");
20
+ for (const a of controlPlane.activeAdapters) {
21
+ console.log(` ${a.name.padEnd(12)} ${a.route}`);
22
+ }
23
+ }
24
+ else {
25
+ console.log(`Health check: http://localhost:${port}/healthz`);
26
+ console.log(`API Status: http://localhost:${port}/api/status`);
27
+ }
28
+ const cleanup = async () => {
29
+ await controlPlane?.stop();
30
+ server.stop();
31
+ process.exit(0);
32
+ };
33
+ process.on("SIGINT", cleanup);
34
+ process.on("SIGTERM", cleanup);
35
+ console.log("Press Ctrl+C to stop");
@@ -0,0 +1,41 @@
1
+ import { type Channel } from "@femtomc/mu-control-plane";
2
+ export declare const ENV_VARS: {
3
+ readonly slack: {
4
+ readonly signingSecret: "MU_SLACK_SIGNING_SECRET";
5
+ };
6
+ readonly discord: {
7
+ readonly signingSecret: "MU_DISCORD_SIGNING_SECRET";
8
+ };
9
+ readonly telegram: {
10
+ readonly webhookSecret: "MU_TELEGRAM_WEBHOOK_SECRET";
11
+ readonly botUsername: "MU_TELEGRAM_BOT_USERNAME";
12
+ readonly tenantId: "MU_TELEGRAM_TENANT_ID";
13
+ };
14
+ };
15
+ export type ActiveAdapter = {
16
+ name: Channel;
17
+ route: string;
18
+ };
19
+ export type ControlPlaneHandle = {
20
+ activeAdapters: ActiveAdapter[];
21
+ handleWebhook(path: string, req: Request): Promise<Response | null>;
22
+ stop(): Promise<void>;
23
+ };
24
+ type DetectedAdapter = {
25
+ name: "slack";
26
+ signingSecret: string;
27
+ } | {
28
+ name: "discord";
29
+ signingSecret: string;
30
+ } | {
31
+ name: "telegram";
32
+ webhookSecret: string;
33
+ botUsername: string | null;
34
+ tenantId: string | null;
35
+ };
36
+ export declare function detectAdapters(env: Record<string, string | undefined>): DetectedAdapter[];
37
+ export declare function bootstrapControlPlane(opts: {
38
+ repoRoot: string;
39
+ env?: Record<string, string | undefined>;
40
+ }): Promise<ControlPlaneHandle | null>;
41
+ export {};
@@ -0,0 +1,94 @@
1
+ import { ControlPlaneRuntime, ControlPlaneCommandPipeline, ControlPlaneOutbox, SlackControlPlaneAdapter, DiscordControlPlaneAdapter, TelegramControlPlaneAdapter, getControlPlanePaths, } from "@femtomc/mu-control-plane";
2
+ export const ENV_VARS = {
3
+ slack: { signingSecret: "MU_SLACK_SIGNING_SECRET" },
4
+ discord: { signingSecret: "MU_DISCORD_SIGNING_SECRET" },
5
+ telegram: {
6
+ webhookSecret: "MU_TELEGRAM_WEBHOOK_SECRET",
7
+ botUsername: "MU_TELEGRAM_BOT_USERNAME",
8
+ tenantId: "MU_TELEGRAM_TENANT_ID",
9
+ },
10
+ };
11
+ const ROUTE_MAP = {
12
+ slack: "/webhooks/slack",
13
+ discord: "/webhooks/discord",
14
+ telegram: "/webhooks/telegram",
15
+ };
16
+ export function detectAdapters(env) {
17
+ const adapters = [];
18
+ const slackSecret = env[ENV_VARS.slack.signingSecret];
19
+ if (slackSecret) {
20
+ adapters.push({ name: "slack", signingSecret: slackSecret });
21
+ }
22
+ const discordSecret = env[ENV_VARS.discord.signingSecret];
23
+ if (discordSecret) {
24
+ adapters.push({ name: "discord", signingSecret: discordSecret });
25
+ }
26
+ const telegramSecret = env[ENV_VARS.telegram.webhookSecret];
27
+ if (telegramSecret) {
28
+ adapters.push({
29
+ name: "telegram",
30
+ webhookSecret: telegramSecret,
31
+ botUsername: env[ENV_VARS.telegram.botUsername] ?? null,
32
+ tenantId: env[ENV_VARS.telegram.tenantId] ?? null,
33
+ });
34
+ }
35
+ return adapters;
36
+ }
37
+ export async function bootstrapControlPlane(opts) {
38
+ const env = opts.env ?? process.env;
39
+ const detected = detectAdapters(env);
40
+ if (detected.length === 0) {
41
+ return null;
42
+ }
43
+ const paths = getControlPlanePaths(opts.repoRoot);
44
+ const runtime = new ControlPlaneRuntime({ repoRoot: opts.repoRoot });
45
+ await runtime.start();
46
+ const pipeline = new ControlPlaneCommandPipeline({ runtime });
47
+ await pipeline.start();
48
+ const outbox = new ControlPlaneOutbox(paths.outboxPath);
49
+ await outbox.load();
50
+ const adapterMap = new Map();
51
+ for (const d of detected) {
52
+ const route = ROUTE_MAP[d.name];
53
+ let adapter;
54
+ switch (d.name) {
55
+ case "slack":
56
+ adapter = new SlackControlPlaneAdapter({
57
+ pipeline,
58
+ outbox,
59
+ signingSecret: d.signingSecret,
60
+ });
61
+ break;
62
+ case "discord":
63
+ adapter = new DiscordControlPlaneAdapter({
64
+ pipeline,
65
+ outbox,
66
+ signingSecret: d.signingSecret,
67
+ });
68
+ break;
69
+ case "telegram":
70
+ adapter = new TelegramControlPlaneAdapter({
71
+ pipeline,
72
+ outbox,
73
+ webhookSecret: d.webhookSecret,
74
+ botUsername: d.botUsername ?? undefined,
75
+ tenantId: d.tenantId ?? undefined,
76
+ });
77
+ break;
78
+ }
79
+ adapterMap.set(route, { adapter, info: { name: d.name, route } });
80
+ }
81
+ return {
82
+ activeAdapters: [...adapterMap.values()].map((v) => v.info),
83
+ async handleWebhook(path, req) {
84
+ const entry = adapterMap.get(path);
85
+ if (!entry)
86
+ return null;
87
+ const result = await entry.adapter.ingest(req);
88
+ return result.response;
89
+ },
90
+ async stop() {
91
+ await pipeline.stop();
92
+ },
93
+ };
94
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
- export type { ServerOptions, ServerContext } from "./server.js";
2
- export { createServer, createContext } from "./server.js";
1
+ export type { ServerOptions, ServerContext, ServerWithControlPlane } from "./server.js";
2
+ export { createServer, createServerAsync, createContext } from "./server.js";
3
+ export type { ControlPlaneHandle, ActiveAdapter } from "./control_plane.js";
4
+ export { bootstrapControlPlane, detectAdapters, ENV_VARS } from "./control_plane.js";
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
- export { createServer, createContext } from "./server.js";
1
+ export { createServer, createServerAsync, createContext } from "./server.js";
2
+ export { bootstrapControlPlane, detectAdapters, ENV_VARS } from "./control_plane.js";
package/dist/server.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { EventLog } from "@femtomc/mu-core/node";
2
2
  import { IssueStore } from "@femtomc/mu-issue";
3
3
  import { ForumStore } from "@femtomc/mu-forum";
4
+ import { type ControlPlaneHandle } from "./control_plane.js";
4
5
  export type ServerOptions = {
5
6
  repoRoot?: string;
6
7
  port?: number;
8
+ controlPlane?: ControlPlaneHandle | null;
7
9
  };
8
10
  export type ServerContext = {
9
11
  repoRoot: string;
@@ -17,3 +19,10 @@ export declare function createServer(options?: ServerOptions): {
17
19
  fetch: (request: Request) => Promise<Response>;
18
20
  hostname: string;
19
21
  };
22
+ export type ServerWithControlPlane = {
23
+ serverConfig: ReturnType<typeof createServer>;
24
+ controlPlane: ControlPlaneHandle | null;
25
+ };
26
+ export declare function createServerAsync(options?: Omit<ServerOptions, "controlPlane"> & {
27
+ env?: Record<string, string | undefined>;
28
+ }): Promise<ServerWithControlPlane>;
package/dist/server.js CHANGED
@@ -6,6 +6,7 @@ import { IssueStore } from "@femtomc/mu-issue";
6
6
  import { ForumStore } from "@femtomc/mu-forum";
7
7
  import { issueRoutes } from "./api/issues.js";
8
8
  import { forumRoutes } from "./api/forum.js";
9
+ import { bootstrapControlPlane } from "./control_plane.js";
9
10
  const MIME_TYPES = {
10
11
  ".html": "text/html; charset=utf-8",
11
12
  ".js": "text/javascript; charset=utf-8",
@@ -30,6 +31,7 @@ export function createContext(repoRoot) {
30
31
  export function createServer(options = {}) {
31
32
  const repoRoot = options.repoRoot || process.cwd();
32
33
  const context = createContext(repoRoot);
34
+ const controlPlane = options.controlPlane ?? null;
33
35
  const handleRequest = async (request) => {
34
36
  const url = new URL(request.url);
35
37
  const path = url.pathname;
@@ -55,7 +57,10 @@ export function createServer(options = {}) {
55
57
  return Response.json({
56
58
  repo_root: context.repoRoot,
57
59
  open_count: openIssues.length,
58
- ready_count: readyIssues.length
60
+ ready_count: readyIssues.length,
61
+ control_plane: controlPlane
62
+ ? { active: true, adapters: controlPlane.activeAdapters.map(a => a.name) }
63
+ : { active: false, adapters: [] },
59
64
  }, { headers });
60
65
  }
61
66
  // Issue routes
@@ -72,6 +77,14 @@ export function createServer(options = {}) {
72
77
  headers.forEach((value, key) => response.headers.set(key, value));
73
78
  return response;
74
79
  }
80
+ // Webhook routes (control plane)
81
+ if (path.startsWith("/webhooks/") && controlPlane) {
82
+ const response = await controlPlane.handleWebhook(path, request);
83
+ if (response) {
84
+ headers.forEach((value, key) => response.headers.set(key, value));
85
+ return response;
86
+ }
87
+ }
75
88
  // Static file serving (bundled web UI)
76
89
  if (existsSync(PUBLIC_DIR)) {
77
90
  // Try to serve the exact file (with path traversal protection)
@@ -103,3 +116,9 @@ export function createServer(options = {}) {
103
116
  };
104
117
  return server;
105
118
  }
119
+ export async function createServerAsync(options = {}) {
120
+ const repoRoot = options.repoRoot || process.cwd();
121
+ const controlPlane = await bootstrapControlPlane({ repoRoot, env: options.env });
122
+ const serverConfig = createServer({ ...options, controlPlane });
123
+ return { serverConfig, controlPlane };
124
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.27",
3
+ "version": "26.2.28",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,8 +23,9 @@
23
23
  "start": "bun run dist/cli.js"
24
24
  },
25
25
  "dependencies": {
26
- "@femtomc/mu-core": "26.2.27",
27
- "@femtomc/mu-issue": "26.2.27",
28
- "@femtomc/mu-forum": "26.2.27"
26
+ "@femtomc/mu-core": "26.2.28",
27
+ "@femtomc/mu-issue": "26.2.28",
28
+ "@femtomc/mu-forum": "26.2.28",
29
+ "@femtomc/mu-control-plane": "26.2.28"
29
30
  }
30
31
  }