ework-aio 0.5.19 → 0.5.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-aio",
3
- "version": "0.5.19",
3
+ "version": "0.5.20",
4
4
  "description": "All-in-one installer for ework (issue tracker) + ework-daemon (AI bridge) + opencode-ework (plugin). One command: npm i -g ework-aio && ework-aio install.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -178,8 +178,9 @@ export async function runInstall(
178
178
  // 5. Write (or forward-fill) web .env.
179
179
  const ctx: InstallContext = {
180
180
  paths,
181
- workPort: opts.workPort,
182
- daemonPort: opts.daemonPort,
181
+ workPort: opts.workPort,
182
+ daemonPort: opts.daemonPort,
183
+ routerPort: DEFAULTS.routerPort,
183
184
  botName: opts.botName,
184
185
  operatorLogin,
185
186
  opencodeBin,
@@ -463,6 +464,44 @@ export async function runInstall(
463
464
  }
464
465
  }
465
466
 
467
+ // 12.5 Write router .env + start ework-router (webhook routing layer).
468
+ const routerEnvResult = await ensureEnvFile({ file: "router", filePath: paths.routerEnvFile, ctx });
469
+ if (routerEnvResult.created) {
470
+ logger.ok(`wrote ${paths.routerEnvFile}`);
471
+ } else if (routerEnvResult.added.length > 0) {
472
+ logger.ok(`forward-filled ${paths.routerEnvFile}: +${routerEnvResult.added.length} keys`);
473
+ }
474
+
475
+ let routerStarted = false;
476
+ if (opts.noStart) {
477
+ logger.warn(`--no-start: skipping ework-router startup`);
478
+ } else {
479
+ const routerPid = await readPidFile(paths.routerPidFile);
480
+ if (routerPid !== null && isProcessRunning(routerPid)) {
481
+ routerStarted = true;
482
+ logger.log(`ework-router already running (pid ${routerPid})`);
483
+ }
484
+ if (!routerStarted) {
485
+ const routerBin = resolveBundledBin("ework-router", "bin/ework-router.js");
486
+ if (routerBin) {
487
+ logger.log(`starting ework-router (PID-file mode)...`);
488
+ const env = await loadEnvIntoProcess(paths.routerEnvFile);
489
+ const { pid } = await startProcess({
490
+ cmd: "bun",
491
+ args: [routerBin],
492
+ cwd: paths.routerDataDir,
493
+ env,
494
+ logFile: paths.routerLogFile,
495
+ pidFile: paths.routerPidFile,
496
+ });
497
+ logger.ok(`ework-router started (pid ${pid}, log ${paths.routerLogFile})`);
498
+ routerStarted = true;
499
+ } else {
500
+ logger.warn(`ework-router binary not found — router skipped (install with: npm install -g ework-router)`);
501
+ }
502
+ }
503
+ }
504
+
466
505
  // 13. Register opencode-ework plugin in opencode.json (idempotent).
467
506
  const { added: pluginAdded } = await ensurePluginInFile(paths.opencodeConfigFile, PLUGIN_NAME);
468
507
  if (pluginAdded) {
@@ -130,9 +130,10 @@ export async function startFromSp(
130
130
  return false;
131
131
  }
132
132
  const env = await loadEnv(sp.envFile);
133
+ const useBun = binPath.endsWith(".js");
133
134
  const { pid } = await startProcess({
134
- cmd: binPath,
135
- args: [],
135
+ cmd: useBun ? "bun" : binPath,
136
+ args: useBun ? [binPath] : [],
136
137
  cwd: sp.dataDir,
137
138
  env,
138
139
  logFile: sp.logFile,
@@ -240,14 +241,6 @@ export async function runStart(
240
241
  ): Promise<void> {
241
242
  const paths = resolvePaths({ dataDir: opts.dataDir, scope: opts.scope, useSystemd: false });
242
243
  for (const { sp, label } of iterTargets(paths, target)) {
243
- if (label === "router") {
244
- try {
245
- await startFromSp(sp, label, logger);
246
- } catch (e) {
247
- logger.log(`ework-router skipped (${(e as Error).message})`);
248
- }
249
- continue;
250
- }
251
244
  await startFromSp(sp, label, logger);
252
245
  }
253
246
  }
@@ -271,14 +264,6 @@ export async function runRestart(
271
264
  const paths = resolvePaths({ dataDir: opts.dataDir, scope: opts.scope, useSystemd: false });
272
265
  for (const { sp, label } of iterTargets(paths, target)) {
273
266
  await stopFromSp(sp, label, logger);
274
- if (label === "router") {
275
- try {
276
- await startFromSp(sp, label, logger);
277
- } catch (e) {
278
- logger.log(`ework-router skipped (${(e as Error).message})`);
279
- }
280
- continue;
281
- }
282
267
  await startFromSp(sp, label, logger);
283
268
  }
284
269
  }
package/src/config.ts CHANGED
@@ -17,12 +17,13 @@
17
17
  import { randomBytes } from "node:crypto";
18
18
  import type { PathConfig } from "./paths.ts";
19
19
 
20
- export type EnvFile = "web" | "daemon";
20
+ export type EnvFile = "web" | "daemon" | "router";
21
21
 
22
22
  export interface InstallContext {
23
23
  paths: PathConfig;
24
24
  workPort: number;
25
25
  daemonPort: number;
26
+ routerPort: number;
26
27
  botName: string;
27
28
  operatorLogin: string;
28
29
  // opencode binary path (looked up via preflight)
@@ -73,7 +74,7 @@ export const WEB_ENV_KEYS: readonly EnvKeySpec[] = [
73
74
  // user can't append → "EACCES: permission denied" on every request.
74
75
  { envVar: "WORK_ACCESS_LOG", file: "web", generate: (c) => `${c.paths.runDir}/web-access.log` },
75
76
  { envVar: "WORK_DAEMON_BOT_LOGIN", file: "web", generate: (c) => c.botName },
76
- { envVar: "WORK_DAEMON_WEBHOOK_URL", file: "web", generate: (c) => `http://127.0.0.1:${c.daemonPort}` },
77
+ { envVar: "WORK_DAEMON_WEBHOOK_URL", file: "web", generate: (c) => `http://127.0.0.1:${c.routerPort}` },
77
78
  { envVar: "WORK_DAEMON_WEBHOOK_SECRET", file: "web", secret: true, generate: sharedWebhookSecret },
78
79
  ] as const;
79
80
 
@@ -94,20 +95,34 @@ export const DAEMON_ENV_KEYS: readonly EnvKeySpec[] = [
94
95
  { envVar: "OPENCODE_BASE_WORKDIR", file: "daemon", generate: (c) => c.paths.opencodeWorkdir },
95
96
  ] as const;
96
97
 
98
+ export const ROUTER_ENV_KEYS: readonly EnvKeySpec[] = [
99
+ { envVar: "ROUTER_ENV", file: "router", generate: () => "production" },
100
+ { envVar: "ROUTER_PORT", file: "router", generate: (c) => String(c.routerPort) },
101
+ { envVar: "ROUTER_HOST", file: "router", generate: () => "127.0.0.1" },
102
+ { envVar: "ROUTER_STRATEGY", file: "router", generate: () => "least-loaded" },
103
+ { envVar: "WORK_DB_DRIVER", file: "router", generate: () => "sqlite" },
104
+ { envVar: "WORK_DB_PATH", file: "router", generate: (c) => c.paths.webDbPath },
105
+ { envVar: "WORK_DB_PREFIX", file: "router", generate: () => "" },
106
+ { envVar: "DAEMON_TABLE_PREFIX", file: "router", generate: () => "d_" },
107
+ ] as const;
108
+
97
109
  export function keysForFile(file: EnvFile): readonly EnvKeySpec[] {
98
- return file === "web" ? WEB_ENV_KEYS : DAEMON_ENV_KEYS;
110
+ if (file === "web") return WEB_ENV_KEYS;
111
+ if (file === "router") return ROUTER_ENV_KEYS;
112
+ return DAEMON_ENV_KEYS;
99
113
  }
100
114
 
101
- // Set of env vars that, if missing, would crash ework-web or ework-daemon
102
- // at startup (Zod schema rejects). Used by env.ts forward-fill to log
103
- // which keys were injected.
115
+ // Set of env vars that, if missing, would crash ework-web, ework-daemon, or
116
+ // ework-router at startup (Zod schema rejects). Used by env.ts forward-fill.
104
117
  export const REQUIRED_KEYS: ReadonlySet<string> = new Set([
105
118
  ...WEB_ENV_KEYS.map((k) => k.envVar),
106
119
  ...DAEMON_ENV_KEYS.map((k) => k.envVar),
120
+ ...ROUTER_ENV_KEYS.map((k) => k.envVar),
107
121
  ]);
108
122
 
109
123
  // Keys whose value should be redacted in `config list` output.
110
124
  export const SECRET_ENV_VARS: ReadonlySet<string> = new Set([
111
125
  ...WEB_ENV_KEYS.filter((k) => k.secret).map((k) => k.envVar),
112
126
  ...DAEMON_ENV_KEYS.filter((k) => k.secret).map((k) => k.envVar),
127
+ ...ROUTER_ENV_KEYS.filter((k) => k.secret).map((k) => k.envVar),
113
128
  ]);
package/src/types.ts CHANGED
@@ -31,6 +31,7 @@ export interface GlobalOptions {
31
31
  export const DEFAULTS = {
32
32
  workPort: 3002,
33
33
  daemonPort: 3101,
34
+ routerPort: 3104,
34
35
  botName: "ework-daemon",
35
36
  scope: "user" as Scope,
36
37
  } as const;