ework-aio 0.2.10 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-aio",
3
- "version": "0.2.10",
3
+ "version": "0.3.0",
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",
@@ -46,7 +46,7 @@
46
46
  "postinstall": "echo '\\n ework-aio installed. Run \\033[1mework-aio install\\033[0m to set up services.\\n Help: ework-aio --help\\n'"
47
47
  },
48
48
  "dependencies": {
49
- "ework-web": "^0.1.0",
49
+ "ework-web": "^0.3.0",
50
50
  "ework-daemon": "^0.1.0",
51
51
  "opencode-ework": "^0.1.0",
52
52
  "zod": "^3.23.0"
@@ -132,7 +132,7 @@ pass "both services responding"
132
132
 
133
133
  info "configure opencode to use fake LLM"
134
134
  # Overwrite ~/.config/opencode/opencode.json with fake provider + plugin.
135
- # install.ts only registers the plugin; we add the fake provider here so
135
+ # install.ts only registers opencode-ework; we add the fake provider here so
136
136
  # the test stays isolated from any real API credentials.
137
137
  OPENCODE_CONFIG_DIR="$HOME/.config/opencode"
138
138
  mkdir -p "$OPENCODE_CONFIG_DIR"
@@ -150,6 +150,9 @@ cat > "$OPENCODE_CONFIG_DIR/opencode.json" <<OCJSON
150
150
  "models": {
151
151
  "fake-model": {
152
152
  "name": "Fake Model"
153
+ },
154
+ "e2e-daemon-override": {
155
+ "name": "E2E Daemon Override"
153
156
  }
154
157
  }
155
158
  }
@@ -198,6 +201,28 @@ COOKIE_SIG=$(printf '%s' "$WORK_TOKEN" \
198
201
  | base64 | tr '+/' '-_' | tr -d '=')
199
202
  AUTH_COOKIE="ework_auth=${WORK_TOKEN}.${COOKIE_SIG}"
200
203
 
204
+ info "configure global defaultModel via /settings (model config E2E)"
205
+ # Set defaultModel to a UNIQUE model name (different from the smoke test's
206
+ # fake/fake-model) so we can distinguish daemon-driven spawn from smoke test.
207
+ # If ework-daemon honors the new --model flag, fake-llm will see
208
+ # `model=e2e-daemon-override` in the request body. If it doesn't, fake-llm
209
+ # keeps seeing `model=fake-model` (opencode falls back to opencode.json).
210
+ E2E_MODEL_ID="fake/e2e-daemon-override"
211
+ SETTINGS_RESP=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
212
+ "http://127.0.0.1:$WORK_PORT/settings" \
213
+ -H "Cookie: $AUTH_COOKIE" \
214
+ --data-urlencode "defaultModel=$E2E_MODEL_ID")
215
+ [[ "$SETTINGS_RESP" == "303" ]] \
216
+ || fail "POST /settings defaultModel=$E2E_MODEL_ID failed (status=$SETTINGS_RESP)"
217
+ pass "global defaultModel configured: $E2E_MODEL_ID"
218
+ # Pre-populate the model_cache so the settings UI's select shows our entry
219
+ # (also exercises the refresh endpoint added in ework-web 0.2.0).
220
+ curl -sS -o /dev/null -X POST \
221
+ "http://127.0.0.1:$WORK_PORT/settings/models/refresh" \
222
+ -H "Cookie: $AUTH_COOKIE" || true
223
+ # Mark timestamp so we can scope fake-llm log analysis to AFTER this point.
224
+ E2E_MODEL_CONFIG_TS=$(date +%s)
225
+
201
226
  # Create project (idempotent — if it exists, 303 is still returned).
202
227
  curl -sS -o /dev/null -w 'project: %{http_code}\n' -X POST \
203
228
  "http://127.0.0.1:$WORK_PORT/projects" \
@@ -273,6 +298,24 @@ for i in $(seq 1 30); do
273
298
  [[ $i -eq 30 ]] && fail "session never got assistant reply (msgs=$MSG_COUNT)";
274
299
  done
275
300
 
301
+ info "verify daemon honored --model override from ework-web defaultModel"
302
+ # fake-llm logs `model=<X>` for each /v1/chat/completions request. The smoke
303
+ # test ran with fake/fake-model. The daemon-driven spawn (after we set
304
+ # defaultModel=fake/e2e-daemon-override in /settings) MUST show
305
+ # model=e2e-daemon-override in subsequent requests. If we still see only
306
+ # model=fake-model → daemon didn't pass --model through (regression).
307
+ if grep -q "model=e2e-daemon-override" /tmp/fake-llm.log; then
308
+ pass "daemon spawn honored ework-web defaultModel (fake-llm saw model=e2e-daemon-override)"
309
+ else
310
+ echo " --- fake-llm log (model= lines only) ---"
311
+ grep -E 'model=' /tmp/fake-llm.log | tail -10 || echo "(no model= entries)"
312
+ echo " --- daemon.log (last 30) ---"
313
+ tail -30 "$DATA_DIR/run/daemon.log" 2>/dev/null || echo "(no daemon.log)"
314
+ echo " --- ework-web + ework-daemon installed versions ---"
315
+ npm ls -g ework-web ework-daemon 2>&1 | grep -E 'ework-' | head -5
316
+ fail "daemon did NOT pass --model through (fake-llm never saw model=e2e-daemon-override)"
317
+ fi
318
+
276
319
  info "verify session is browsable via awork-web"
277
320
  # awork-web's OpencodeClient reads opencode.db and calls `opencode export`.
278
321
  # Hit the /sessions list and the specific session page. Both require the
@@ -293,6 +336,20 @@ if ! echo "$SESSIONS_HTML" | grep -q "$SESSION_ID"; then
293
336
  fi
294
337
  pass "/sessions lists the new session"
295
338
 
339
+ # Diagnostic: capture raw `opencode export <id>` stdout to see whether plugins
340
+ # emit banners into the JSON stream in this test environment. Production hit a
341
+ # bug where ework-web's JSON.parse choked on these preamble lines; if they
342
+ # appear here, the test must catch the failure rather than silently passing.
343
+ EXPORT_RAW=$(opencode export "$SESSION_ID" 2>/dev/null </dev/null || true)
344
+ EXPORT_LINE_COUNT=$(printf '%s\n' "$EXPORT_RAW" | wc -l)
345
+ JSON_START_LINE=$(printf '%s\n' "$EXPORT_RAW" | grep -nE '^\s*[{]' | head -1 | cut -d: -f1 || echo 0)
346
+ echo " opencode export raw stdout: $EXPORT_LINE_COUNT lines, JSON object opens at line $JSON_START_LINE"
347
+ if [[ "$EXPORT_LINE_COUNT" -gt 1 && "$JSON_START_LINE" -gt 1 ]]; then
348
+ echo " --- first 5 lines (non-JSON preamble) ---"
349
+ printf '%s\n' "$EXPORT_RAW" | head -5 | sed 's/^/ /'
350
+ echo " WARNING: stdout has non-JSON preamble — ework-web's old JSON.parse would fail here"
351
+ fi
352
+
296
353
  SESSION_PAGE_HTML=$(curl -sS -H "Cookie: $AUTH_COOKIE" "http://127.0.0.1:$WORK_PORT/sessions/$SESSION_ID")
297
354
  # Look for any marker that the fake LLM reply rendered. The reply body always
298
355
  # starts with "E2E fake-LLM reply" so we grep for that substring.
@@ -54,6 +54,39 @@ DAEMON_PORT="14101"
54
54
  # readonly so the container preflight sees it on PATH. Override via env.
55
55
  OPENCODE_HOST_BIN="${OPENCODE_HOST_BIN:-/home/dog/.local/bin/opencode}"
56
56
 
57
+ # Optional MySQL backend: E2E_DB=mysql starts a throwaway MySQL 8.0 sidecar
58
+ # reachable via the host network and passes WORK_DB_* through to ework-web.
59
+ # Default sqlite (no sidecar). Needs a published ework-web with MySQL support.
60
+ E2E_DB="${E2E_DB:-sqlite}"
61
+ MYSQL_FLAGS=()
62
+ MYSQL_CONTAINER=""
63
+ cleanup_mysql() { [[ -n "$MYSQL_CONTAINER" ]] && docker rm -f "$MYSQL_CONTAINER" >/dev/null 2>&1 || true; }
64
+ trap cleanup_mysql EXIT
65
+ if [[ "$E2E_DB" == "mysql" ]]; then
66
+ MYSQL_PORT="${E2E_MYSQL_PORT:-3312}"
67
+ MYSQL_CONTAINER="ework-e2e-mysql-$$"
68
+ info "E2E_DB=mysql: starting MySQL 8.0 sidecar (container $MYSQL_CONTAINER, host port $MYSQL_PORT)"
69
+ docker run -d --rm --name "$MYSQL_CONTAINER" -p "$MYSQL_PORT:3306" \
70
+ -e MYSQL_ROOT_PASSWORD=testpw -e MYSQL_DATABASE=ework_e2e mysql:8.0 >/dev/null
71
+ info "waiting for MySQL readiness (TCP+query gate, up to 60s)"
72
+ ready=0
73
+ for _ in $(seq 1 60); do
74
+ if docker exec "$MYSQL_CONTAINER" mysql -h 127.0.0.1 -ptestpw -e "SELECT 1" --silent 2>/dev/null; then
75
+ ready=1; break
76
+ fi
77
+ sleep 1
78
+ done
79
+ [[ "$ready" -eq 1 ]] || fail "MySQL sidecar did not become ready in 60s"
80
+ MYSQL_FLAGS=(
81
+ -e WORK_DB_DRIVER=mysql
82
+ -e WORK_DB_HOST=127.0.0.1
83
+ -e WORK_DB_PORT="$MYSQL_PORT"
84
+ -e WORK_DB_USER=root
85
+ -e WORK_DB_PASSWORD=testpw
86
+ -e WORK_DB_NAME=ework_e2e
87
+ )
88
+ fi
89
+
57
90
  "$RUNTIME" run --rm -i --network host \
58
91
  -v "$OPENCODE_HOST_BIN:/usr/local/bin/opencode:ro" \
59
92
  -e WORK_PORT="$WORK_PORT" \
@@ -64,6 +97,7 @@ OPENCODE_HOST_BIN="${OPENCODE_HOST_BIN:-/home/dog/.local/bin/opencode}"
64
97
  -e http_proxy="${HTTP_PROXY:-}" \
65
98
  -e https_proxy="${HTTPS_PROXY:-}" \
66
99
  -e NO_PROXY="127.0.0.1,localhost" \
100
+ "${MYSQL_FLAGS[@]}" \
67
101
  "$IMAGE" bash -euo pipefail <<'EOSCRIPT'
68
102
  set -euo pipefail
69
103
 
@@ -24,7 +24,7 @@ import { resolvePaths, type PathConfig } from "../paths.ts";
24
24
  import { type InstallContext } from "../config.ts";
25
25
  import { ensureEnvFile, parseEnvFile, patchEnvKey } from "../env.ts";
26
26
  import { startProcess, isProcessRunning, readPidFile } from "../pidfile.ts";
27
- import { checkPreflight, resolveCommand, REQUIRED_COMMANDS } from "../preflight.ts";
27
+ import { checkPreflight, resolveCommand, resolveBundledBin, REQUIRED_COMMANDS } from "../preflight.ts";
28
28
  import {
29
29
  generateUnitFile,
30
30
  writeUnitFile,
@@ -96,31 +96,45 @@ export async function runInstall(
96
96
  const opencodeBin = preflight.found.get("opencode")!;
97
97
  logger.ok(`preflight: bun, npm, opencode all on PATH`);
98
98
 
99
- // 2. Resolve ework-web / ework-daemon binaries (npm-installed global bins).
99
+ // 2. Resolve ework-web / ework-daemon binaries.
100
100
  //
101
101
  // ework-daemon ships TWO bins: `ework-daemon` (client CLI: status/issues/...)
102
102
  // and `ework-daemon-server` (actual HTTP server). We start the SERVER, not
103
103
  // the client — pre-v0.2.5 we spawned the client, which prints help and
104
104
  // exits, leaving the daemon "looking dead" with a help-text log.
105
- const webBin = resolveCommand("ework-web");
106
- const daemonClientBin = resolveCommand("ework-daemon");
107
- const daemonServerBin = resolveCommand("ework-daemon-server");
105
+ //
106
+ // B-1: ework-web / ework-daemon are declared dependencies of ework-aio, so
107
+ // they are always bundled in our own node_modules. Resolve from there first
108
+ // (self-contained), falling back to PATH for dev/standalone installs.
109
+ // Relying on PATH alone broke after uninstall+reinstall: npm does not
110
+ // reliably recreate global bin symlinks for reinstalled deps even though
111
+ // the package is present in node_modules — so install wrongly demanded
112
+ // "install ework-web first" for a package that is ework-aio's own job to
113
+ // provide.
114
+ const webBin = resolveBundledBin("ework-web", "bin/ework-web.js")
115
+ ?? resolveCommand("ework-web");
116
+ const daemonClientBin = resolveBundledBin("ework-daemon", "src/cli.ts")
117
+ ?? resolveCommand("ework-daemon");
118
+ const daemonServerBin = resolveBundledBin("ework-daemon", "bin/ework-daemon-server.js")
119
+ ?? resolveCommand("ework-daemon-server");
108
120
  if (!webBin) {
109
121
  throw new InstallError(
110
- `ework-web binary not found on PATH. Install with: npm install -g ework-web`,
122
+ `ework-web not found. It ships as part of ework-aio; your ework-aio install is incomplete. ` +
123
+ `Reinstall with: npm install -g ework-aio@latest`,
111
124
  );
112
125
  }
113
126
  if (!daemonClientBin && !daemonServerBin) {
114
127
  throw new InstallError(
115
- `ework-daemon not installed. Install with: npm install -g ework-daemon`,
128
+ `ework-daemon not found. It ships as part of ework-aio; your ework-aio install is incomplete. ` +
129
+ `Reinstall with: npm install -g ework-aio@latest`,
116
130
  );
117
131
  }
118
132
  if (!daemonServerBin) {
119
133
  // Client CLI present but server bin missing — old / partial install.
120
134
  throw new InstallError(
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`,
135
+ `ework-daemon-server binary not found, but ework-daemon (client) is present. ` +
136
+ `Your ework-aio install is incomplete or outdated. Reinstall with: ` +
137
+ `npm install -g ework-aio@latest`,
124
138
  );
125
139
  }
126
140
  logger.ok(`web bin : ${webBin}`);
package/src/preflight.ts CHANGED
@@ -1,4 +1,13 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ // Root of the ework-aio package itself. Derived from this file's location
6
+ // (<root>/src/preflight.ts). Used to resolve bins that ework-aio ships as
7
+ // declared dependencies (ework-web, ework-daemon) from our own node_modules,
8
+ // so install does not depend on npm having created a global bin symlink.
9
+ // Overridable via AIO_PACKAGE_ROOT (for tests / non-standard install prefix).
10
+ const DEFAULT_PACKAGE_ROOT = path.join(import.meta.dir, "..");
2
11
 
3
12
  export interface PreflightResult {
4
13
  missing: string[];
@@ -23,6 +32,19 @@ export function resolveCommand(cmd: string): string | null {
23
32
  return path === "" ? null : path;
24
33
  }
25
34
 
35
+ // Resolve a bin that ework-aio ships as a declared dependency, from our own
36
+ // node_modules. ework-web / ework-daemon are listed as deps in package.json,
37
+ // so they are always bundled. We must use them instead of requiring a global
38
+ // PATH bin: npm does not reliably recreate global bin symlinks after
39
+ // uninstall+reinstall, which previously made `ework-aio install` wrongly tell
40
+ // the user to "install ework-web first" even though it was already bundled
41
+ // (B-1). Returns the absolute bin path, or null if not bundled.
42
+ export function resolveBundledBin(pkgName: string, binRelPath: string): string | null {
43
+ const root = process.env.AIO_PACKAGE_ROOT || DEFAULT_PACKAGE_ROOT;
44
+ const candidate = path.join(root, "node_modules", pkgName, binRelPath);
45
+ return fs.existsSync(candidate) ? candidate : null;
46
+ }
47
+
26
48
  export function checkPreflight(
27
49
  required: string[],
28
50
  opts: PreflightOptions = {},