ework-aio 0.2.4 → 0.2.6
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 +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/install.ts +41 -7
- package/src/commands/lifecycle.ts +5 -1
- package/src/config.ts +26 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ework-aio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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",
|
package/src/cli.ts
CHANGED
package/src/commands/install.ts
CHANGED
|
@@ -97,20 +97,35 @@ export async function runInstall(
|
|
|
97
97
|
logger.ok(`preflight: bun, npm, opencode all on PATH`);
|
|
98
98
|
|
|
99
99
|
// 2. Resolve ework-web / ework-daemon binaries (npm-installed global bins).
|
|
100
|
+
//
|
|
101
|
+
// ework-daemon ships TWO bins: `ework-daemon` (client CLI: status/issues/...)
|
|
102
|
+
// and `ework-daemon-server` (actual HTTP server). We start the SERVER, not
|
|
103
|
+
// the client — pre-v0.2.5 we spawned the client, which prints help and
|
|
104
|
+
// exits, leaving the daemon "looking dead" with a help-text log.
|
|
100
105
|
const webBin = resolveCommand("ework-web");
|
|
101
|
-
const
|
|
106
|
+
const daemonClientBin = resolveCommand("ework-daemon");
|
|
107
|
+
const daemonServerBin = resolveCommand("ework-daemon-server");
|
|
102
108
|
if (!webBin) {
|
|
103
109
|
throw new InstallError(
|
|
104
110
|
`ework-web binary not found on PATH. Install with: npm install -g ework-web`,
|
|
105
111
|
);
|
|
106
112
|
}
|
|
107
|
-
if (!
|
|
113
|
+
if (!daemonClientBin && !daemonServerBin) {
|
|
114
|
+
throw new InstallError(
|
|
115
|
+
`ework-daemon not installed. Install with: npm install -g ework-daemon`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (!daemonServerBin) {
|
|
119
|
+
// Client CLI present but server bin missing — old / partial install.
|
|
108
120
|
throw new InstallError(
|
|
109
|
-
`ework-daemon binary not found on PATH
|
|
121
|
+
`ework-daemon-server binary not found on PATH, but ework-daemon (client) is present. ` +
|
|
122
|
+
`Your ework-daemon npm install is incomplete or outdated. Reinstall with: ` +
|
|
123
|
+
`npm install -g ework-daemon@latest`,
|
|
110
124
|
);
|
|
111
125
|
}
|
|
112
|
-
logger.ok(`web bin
|
|
113
|
-
logger.ok(`daemon
|
|
126
|
+
logger.ok(`web bin : ${webBin}`);
|
|
127
|
+
logger.ok(`daemon client : ${daemonClientBin ?? "(not on PATH)"}`);
|
|
128
|
+
logger.ok(`daemon server : ${daemonServerBin}`);
|
|
114
129
|
|
|
115
130
|
// 3. Resolve all filesystem paths.
|
|
116
131
|
const paths = resolvePaths({
|
|
@@ -342,6 +357,25 @@ export async function runInstall(
|
|
|
342
357
|
await patchEnvKey(paths.daemonEnvFile, "GITEA_TOKEN", botToken);
|
|
343
358
|
}
|
|
344
359
|
|
|
360
|
+
// 10.5 Reconcile webhook secret across web and daemon .env files.
|
|
361
|
+
// Pre-v0.2.6 installs generated two independent hex(20) values for
|
|
362
|
+
// WORK_DAEMON_WEBHOOK_SECRET (web) and GITEA_WEBHOOK_SECRET (daemon),
|
|
363
|
+
// so signature verification always failed. Forward-fill won't fix this
|
|
364
|
+
// because both keys already exist. We treat web as the source of truth
|
|
365
|
+
// (ework-web's DB has webhook rows signed with whatever
|
|
366
|
+
// WORK_DAEMON_WEBHOOK_SECRET was at creation time) and overwrite daemon.
|
|
367
|
+
{
|
|
368
|
+
const webSecret = await readEnvKey(paths.webEnvFile, "WORK_DAEMON_WEBHOOK_SECRET");
|
|
369
|
+
const daemonSecret = await readEnvKey(paths.daemonEnvFile, "GITEA_WEBHOOK_SECRET");
|
|
370
|
+
if (webSecret && daemonSecret && webSecret !== daemonSecret) {
|
|
371
|
+
await patchEnvKey(paths.daemonEnvFile, "GITEA_WEBHOOK_SECRET", webSecret);
|
|
372
|
+
logger.warn(`webhook secret mismatch — overwrote daemon GITEA_WEBHOOK_SECRET to match web`);
|
|
373
|
+
} else if (webSecret && !daemonSecret) {
|
|
374
|
+
await patchEnvKey(paths.daemonEnvFile, "GITEA_WEBHOOK_SECRET", webSecret);
|
|
375
|
+
logger.ok(`propagated WORK_DAEMON_WEBHOOK_SECRET → GITEA_WEBHOOK_SECRET`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
345
379
|
// 11. (Systemd only) Write + install daemon unit.
|
|
346
380
|
if (opts.useSystemd && paths.daemonUnitFile && systemdOk && !opts.noStart) {
|
|
347
381
|
const userInfo = os.userInfo();
|
|
@@ -349,7 +383,7 @@ export async function runInstall(
|
|
|
349
383
|
user: userInfo.username,
|
|
350
384
|
group: userInfo.username,
|
|
351
385
|
binPath: preflight.found.get("bun")!,
|
|
352
|
-
mainScript:
|
|
386
|
+
mainScript: daemonServerBin,
|
|
353
387
|
envFile: paths.daemonEnvFile,
|
|
354
388
|
workingDirectory: paths.daemonDataDir,
|
|
355
389
|
logFile: paths.daemonLogFile,
|
|
@@ -388,7 +422,7 @@ export async function runInstall(
|
|
|
388
422
|
logger.log(`starting ework-daemon (PID-file mode)...`);
|
|
389
423
|
const env = await loadEnvIntoProcess(paths.daemonEnvFile);
|
|
390
424
|
const { pid } = await startProcess({
|
|
391
|
-
cmd:
|
|
425
|
+
cmd: daemonServerBin,
|
|
392
426
|
args: [],
|
|
393
427
|
cwd: paths.daemonDataDir,
|
|
394
428
|
env,
|
|
@@ -41,7 +41,11 @@ function servicePaths(paths: PathConfig, svc: "web" | "daemon"): ServicePaths {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
return {
|
|
44
|
-
|
|
44
|
+
// ework-daemon ships two bins; only `ework-daemon-server` actually starts
|
|
45
|
+
// the HTTP server. The `ework-daemon` bin is a client CLI that prints help
|
|
46
|
+
// and exits — spawning it leaves the daemon "looking dead" with help text
|
|
47
|
+
// in the log. See install.ts preflight for the full rationale.
|
|
48
|
+
bin: "ework-daemon-server",
|
|
45
49
|
dataDir: paths.daemonDataDir,
|
|
46
50
|
envFile: paths.daemonEnvFile,
|
|
47
51
|
pidFile: paths.daemonPidFile,
|
package/src/config.ts
CHANGED
|
@@ -38,6 +38,26 @@ export interface EnvKeySpec {
|
|
|
38
38
|
|
|
39
39
|
const hex = (bytes: number): string => randomBytes(bytes).toString("hex");
|
|
40
40
|
|
|
41
|
+
// ework-web signs outgoing webhook POSTs with WORK_DAEMON_WEBHOOK_SECRET
|
|
42
|
+
// and ework-daemon verifies them with GITEA_WEBHOOK_SECRET. They MUST be
|
|
43
|
+
// the same value — pre-v0.2.6 each key had an independent hex(20) generator,
|
|
44
|
+
// so the two secrets never matched and every webhook was rejected with
|
|
45
|
+
// "invalid signature" on the daemon side.
|
|
46
|
+
//
|
|
47
|
+
// The closure-memoized `sharedWebhookSecret` returns the same value across
|
|
48
|
+
// both calls within a single install run, so first install is correct.
|
|
49
|
+
// Reconciliation for already-broken installs lives in install.ts (it reads
|
|
50
|
+
// both .env files and, if they differ, overwrites the daemon side from the
|
|
51
|
+
// web side — web is the source of truth because ework-web's DB has webhooks
|
|
52
|
+
// signed with whatever WORK_DAEMON_WEBHOOK_SECRET was at creation time).
|
|
53
|
+
let _sharedWebhookSecret: string | null = null;
|
|
54
|
+
function sharedWebhookSecret(): string {
|
|
55
|
+
if (_sharedWebhookSecret === null) {
|
|
56
|
+
_sharedWebhookSecret = hex(20);
|
|
57
|
+
}
|
|
58
|
+
return _sharedWebhookSecret;
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
export const WEB_ENV_KEYS: readonly EnvKeySpec[] = [
|
|
42
62
|
{ envVar: "WORK_PORT", file: "web", generate: (c) => String(c.workPort) },
|
|
43
63
|
{ envVar: "WORK_HOST", file: "web", generate: () => "127.0.0.1" },
|
|
@@ -48,9 +68,13 @@ export const WEB_ENV_KEYS: readonly EnvKeySpec[] = [
|
|
|
48
68
|
{ envVar: "WORK_DB_PATH", file: "web", generate: (c) => c.paths.webDbPath },
|
|
49
69
|
{ envVar: "WORK_ATTACHMENT_ROOT", file: "web", generate: (c) => c.paths.webAttachmentRoot },
|
|
50
70
|
{ envVar: "WORK_FILE_ROOTS", file: "web", generate: (c) => `/tmp,${c.paths.dataDir}` },
|
|
71
|
+
// Without this, ework-web falls back to /tmp/ework-access.log which is
|
|
72
|
+
// owned by whichever user touched it first. On shared boxes the runtime
|
|
73
|
+
// user can't append → "EACCES: permission denied" on every request.
|
|
74
|
+
{ envVar: "WORK_ACCESS_LOG", file: "web", generate: (c) => `${c.paths.runDir}/web-access.log` },
|
|
51
75
|
{ envVar: "WORK_DAEMON_BOT_LOGIN", file: "web", generate: (c) => c.botName },
|
|
52
76
|
{ envVar: "WORK_DAEMON_WEBHOOK_URL", file: "web", generate: (c) => `http://127.0.0.1:${c.daemonPort}` },
|
|
53
|
-
{ envVar: "WORK_DAEMON_WEBHOOK_SECRET", file: "web", secret: true, generate:
|
|
77
|
+
{ envVar: "WORK_DAEMON_WEBHOOK_SECRET", file: "web", secret: true, generate: sharedWebhookSecret },
|
|
54
78
|
] as const;
|
|
55
79
|
|
|
56
80
|
export const DAEMON_ENV_KEYS: readonly EnvKeySpec[] = [
|
|
@@ -62,7 +86,7 @@ export const DAEMON_ENV_KEYS: readonly EnvKeySpec[] = [
|
|
|
62
86
|
// These tokens come from the bot bootstrap flow — empty placeholder here,
|
|
63
87
|
// filled in by write_daemon_env after PAT is minted.
|
|
64
88
|
{ envVar: "GITEA_TOKEN", file: "daemon", secret: true, generate: () => "" },
|
|
65
|
-
{ envVar: "GITEA_WEBHOOK_SECRET", file: "daemon", secret: true, generate:
|
|
89
|
+
{ envVar: "GITEA_WEBHOOK_SECRET", file: "daemon", secret: true, generate: sharedWebhookSecret },
|
|
66
90
|
{ envVar: "BOT_USERNAME", file: "daemon", generate: (c) => c.botName },
|
|
67
91
|
{ envVar: "BOT_TOKEN", file: "daemon", secret: true, generate: () => "" },
|
|
68
92
|
{ envVar: "OPENCODE_BINARY", file: "daemon", generate: (c) => c.opencodeBin },
|