ework-aio 0.2.7 → 0.2.8

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.7",
3
+ "version": "0.2.8",
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",
@@ -1,19 +1,24 @@
1
1
  #!/usr/bin/env bash
2
- # End-to-end install test.
2
+ # End-to-end install + lifecycle + subcommand test.
3
3
  #
4
- # Spawns a clean debian container (via Dockerfile.regression) and runs the
5
- # FULL `ework-aio install` flow — not a hand-rolled .env like the older
6
- # regression.sh. This is the test that would have caught every bug we
7
- # shipped between v0.2.0 and v0.2.6:
4
+ # Spawns a clean debian container (via Dockerfile.regression) and exercises:
5
+ # 1. Install paths: --allow-root + re-install idempotency
6
+ # 2. Service startup: web /login + daemon /api/status
7
+ # 3. Subcommand smoke: status, env, config list/get/set, logs
8
+ # 4. Service lifecycle: stop / start / restart (PIDs change)
9
+ # 5. Issue flow: create project → create issue → daemon receives webhook
10
+ # 6. Cleanup: uninstall removes pidfiles + kills services
8
11
  #
9
- # - v0.2.4 plugin key fix → caught by issue-create daemon receives webhook
10
- # - v0.2.5 daemon binary fixcaught by /api/status probe (was printing help)
11
- # - v0.2.6 webhook secret caught by "no invalid signature in daemon.log"
12
- # - v0.2.6 access log caught by "no EACCES in web.log"
12
+ # Each block also documents which past bug the assertion prevents:
13
+ # v0.2.4 plugin key → issue-createdaemon receives webhook
14
+ # v0.2.5 daemon binary /api/status probe (was printing help + exiting 0)
15
+ # v0.2.6 secret drift → "no invalid signature in daemon.log"
16
+ # v0.2.6 access log → "no EACCES in web.log"
17
+ # v0.2.7+ regression → all subcommands exit 0 with expected markers
13
18
  #
14
19
  # Usage: ./scripts/e2e-install.sh [container-runtime] [npm-tag]
15
20
  # container-runtime: docker (default) | podman
16
- # npm-tag: latest (default) | 0.2.6 | file:.. (for local builds)
21
+ # npm-tag: latest (default) | 0.2.7 | file:.. (for local builds)
17
22
 
18
23
  set -euo pipefail
19
24
 
@@ -38,9 +43,12 @@ fi
38
43
  info "Using image: $IMAGE (testing npm tag: $NPM_TAG)"
39
44
 
40
45
  # Unique ports so we don't collide with a host-side ework stack when run with
41
- # --network host. Override via env if needed.
42
- WORK_PORT="${WORK_PORT:-14002}"
43
- DAEMON_PORT="${DAEMON_PORT:-14101}"
46
+ # --network host. OVERRIDE via env only if you know what you're doing — by
47
+ # default we IGNORE any inherited WORK_PORT/DAEMON_PORT from the host shell
48
+ # (a host running ework-daemon on :3100 would otherwise leak into the test
49
+ # and break assertions about which port the container's daemon binds to).
50
+ WORK_PORT="14002"
51
+ DAEMON_PORT="14101"
44
52
 
45
53
  # opencode binary is host-specific (path may differ across machines). Mount
46
54
  # readonly so the container preflight sees it on PATH. Override via env.
@@ -69,14 +77,96 @@ DAEMON_PORT="${DAEMON_PORT:-14101}"
69
77
  NPM_TAG="${NPM_TAG:-latest}"
70
78
  DATA_DIR=/tmp/aio-e2e
71
79
 
80
+ # Defend against host env leak: the OUTER script always passes -e WORK_PORT
81
+ # and -e DAEMON_PORT, but if the user invokes this heredoc directly from a
82
+ # shell that has DAEMON_PORT set (e.g. their production daemon on :3100),
83
+ # the assertions below will fail mysteriously. The values above are the
84
+ # authoritative defaults; only override via the outer script's env passthrough.
85
+
86
+ # -----------------------------------------------------------------------------
87
+ # Helpers
88
+ # -----------------------------------------------------------------------------
89
+
90
+ # Poll a URL until it returns < 500, or fail after N half-seconds.
91
+ wait_for_url() {
92
+ local url="$1" name="$2" half_seconds="${3:-60}"
93
+ for i in $(seq 1 "$half_seconds"); do
94
+ if curl -sf -o /dev/null "$url"; then
95
+ pass "$name responds (after ${i} half-seconds)"
96
+ return 0
97
+ fi
98
+ sleep 0.5
99
+ done
100
+ return 1
101
+ }
102
+
103
+ # Read .env key value (last match wins — install.ts overwrites keys in place).
104
+ env_val() { grep "^$1=" "$2" | tail -1 | cut -d= -f2-; }
105
+
106
+ # Build admin auth cookie from web .env.
107
+ build_auth_cookie() {
108
+ local web_env="$DATA_DIR/ework-web/.env"
109
+ local token secret sig
110
+ token=$(env_val WORK_TOKEN "$web_env")
111
+ secret=$(env_val WORK_COOKIE_SECRET "$web_env")
112
+ sig=$(printf '%s' "$token" \
113
+ | openssl dgst -sha256 -hmac "$secret" -binary \
114
+ | base64 | tr '+/' '-_' | tr -d '=')
115
+ echo "ework_auth=${token}.${sig}"
116
+ }
117
+
118
+ # Read PID from pidfile (or echo empty if missing/empty).
119
+ pid_of() {
120
+ local pf="$DATA_DIR/run/$1.pid"
121
+ [[ -f "$pf" ]] || { echo ""; return; }
122
+ echo "$(cat "$pf")"
123
+ }
124
+
125
+ # Is a PID alive?
126
+ pid_alive() { kill -0 "$1" >/dev/null 2>&1; }
127
+
128
+ # Wait for a service's pidfile PID to differ from the given "old" PID.
129
+ wait_for_new_pid() {
130
+ local svc="$1" old_pid="$2" half_seconds="${3:-40}"
131
+ for i in $(seq 1 "$half_seconds"); do
132
+ local new_pid
133
+ new_pid=$(pid_of "$svc")
134
+ if [[ -n "$new_pid" && "$new_pid" != "$old_pid" ]] && pid_alive "$new_pid"; then
135
+ pass "$svc pid changed ($old_pid → $new_pid) after ${i} half-seconds"
136
+ return 0
137
+ fi
138
+ sleep 0.5
139
+ done
140
+ return 1
141
+ }
142
+
143
+ # Wait for a service's pidfile to vanish (or PID to die).
144
+ wait_for_no_pid() {
145
+ local svc="$1" half_seconds="${2:-30}"
146
+ for i in $(seq 1 "$half_seconds"); do
147
+ local pid
148
+ pid=$(pid_of "$svc")
149
+ if [[ -z "$pid" ]] || ! pid_alive "$pid"; then
150
+ pass "$svc stopped (no live pid) after ${i} half-seconds"
151
+ return 0
152
+ fi
153
+ sleep 0.5
154
+ done
155
+ return 1
156
+ }
157
+
158
+ echo "====================================================="
159
+ echo "Phase 0: clean state + npm install"
160
+ echo "====================================================="
72
161
  info "clean state"
73
162
  rm -rf "$DATA_DIR"
74
163
  mkdir -p "$DATA_DIR"
75
164
 
76
165
  info "opencode binary on PATH"
77
166
  opencode --version | head -1
167
+ pass "opencode present"
78
168
 
79
- info "npm install -g ework-web@${NPM_TAG} ework-daemon@latest ework-aio@${NPM_TAG}"
169
+ info "npm install -g ework-web@latest ework-daemon@latest opencode-ework@latest ework-aio@${NPM_TAG}"
80
170
  # Note: ework-web and ework-daemon aren't version-locked to ework-aio. We
81
171
  # always install latest of the services and only parameterize ework-aio so
82
172
  # we can E2E-test a PR branch against published services.
@@ -85,13 +175,21 @@ for pkg in ework-web ework-daemon opencode-ework; do
85
175
  done
86
176
  npm install -g "ework-aio@${NPM_TAG}" 2>&1 | tail -2
87
177
 
178
+ # -----------------------------------------------------------------------------
179
+ # Phase 1: install paths
180
+ # -----------------------------------------------------------------------------
181
+ echo
182
+ echo "====================================================="
183
+ echo "Phase 1: install paths"
184
+ echo "====================================================="
185
+
88
186
  info "v0.2.5 regression: all 4 bins on PATH"
89
187
  for b in ework-aio ework-web ework-daemon ework-daemon-server; do
90
188
  command -v "$b" >/dev/null 2>&1 || fail "$b not on PATH"
91
189
  pass "$b -> $(command -v "$b")"
92
190
  done
93
191
 
94
- info "run ework-aio install (the real installer no hand-rolled .env)"
192
+ info "install path A: --allow-root + custom ports + --yes"
95
193
  # --allow-root: container runs as root; refusing would test an unrealistic
96
194
  # path. The installer's data still lands in /tmp/aio-e2e via --data-dir.
97
195
  ework-aio install \
@@ -102,11 +200,33 @@ ework-aio install \
102
200
  --bot-name e2e-bot \
103
201
  --yes 2>&1 | tee /tmp/install.log
104
202
 
105
- pass "install exited 0"
203
+ pass "install (path A) exited 0"
204
+
205
+ info "install path B: re-install over existing data dir (idempotency)"
206
+ # Re-running install should not crash on existing .env / pidfiles / webhooks.
207
+ # This catches bugs where install.ts isn't idempotent (e.g. UNIQUE constraint
208
+ # on second bot token, or "file exists" on second .env write).
209
+ ework-aio install \
210
+ --allow-root \
211
+ --data-dir "$DATA_DIR" \
212
+ --port "$WORK_PORT" \
213
+ --daemon-port "$DAEMON_PORT" \
214
+ --bot-name e2e-bot \
215
+ --yes 2>&1 | tee /tmp/install2.log | tail -10
216
+
217
+ pass "install (path B: idempotent re-install) exited 0"
218
+
219
+ # -----------------------------------------------------------------------------
220
+ # Phase 2: post-install assertions (regression suite for v0.2.5 / v0.2.6)
221
+ # -----------------------------------------------------------------------------
222
+ echo
223
+ echo "====================================================="
224
+ echo "Phase 2: post-install env / regression assertions"
225
+ echo "====================================================="
106
226
 
107
227
  info "v0.2.6 regression: webhook secrets match across web and daemon .env"
108
- WEB_SEC=$(grep ^WORK_DAEMON_WEBHOOK_SECRET= "$DATA_DIR/ework-web/.env" | cut -d= -f2-)
109
- DAE_SEC=$(grep ^GITEA_WEBHOOK_SECRET= "$DATA_DIR/ework-daemon/.env" | cut -d= -f2-)
228
+ WEB_SEC=$(env_val WORK_DAEMON_WEBHOOK_SECRET "$DATA_DIR/ework-web/.env")
229
+ DAE_SEC=$(env_val GITEA_WEBHOOK_SECRET "$DATA_DIR/ework-daemon/.env")
110
230
  [[ -n "$WEB_SEC" ]] || fail "WORK_DAEMON_WEBHOOK_SECRET empty in web .env"
111
231
  [[ -n "$DAE_SEC" ]] || fail "GITEA_WEBHOOK_SECRET empty in daemon .env"
112
232
  [[ "$WEB_SEC" == "$DAE_SEC" ]] \
@@ -114,31 +234,19 @@ DAE_SEC=$(grep ^GITEA_WEBHOOK_SECRET= "$DATA_DIR/ework-daemon/.env" | cut -d= -f
114
234
  pass "secrets match (${#WEB_SEC} chars)"
115
235
 
116
236
  info "v0.2.6 regression: WORK_ACCESS_LOG set, not defaulting to /tmp/ework-access.log"
117
- ACCESS_LOG_VAL=$(grep ^WORK_ACCESS_LOG= "$DATA_DIR/ework-web/.env" | cut -d= -f2-)
237
+ ACCESS_LOG_VAL=$(env_val WORK_ACCESS_LOG "$DATA_DIR/ework-web/.env")
118
238
  [[ -n "$ACCESS_LOG_VAL" ]] || fail "WORK_ACCESS_LOG missing from web .env"
119
239
  [[ "$ACCESS_LOG_VAL" != "/tmp/ework-access.log" ]] \
120
240
  || fail "WORK_ACCESS_LOG still defaults to /tmp/ework-access.log"
121
241
  pass "WORK_ACCESS_LOG=$ACCESS_LOG_VAL"
122
242
 
123
243
  info "wait for ework-web /login"
124
- for i in $(seq 1 60); do
125
- if curl -sf -o /dev/null "http://127.0.0.1:$WORK_PORT/login"; then
126
- pass "/login responds (after ${i} half-seconds)"
127
- break
128
- fi
129
- sleep 0.5
130
- [[ $i -eq 60 ]] && { tail -30 "$DATA_DIR/run/web.log" 2>/dev/null; fail "ework-web did not come up"; }
131
- done
244
+ wait_for_url "http://127.0.0.1:$WORK_PORT/login" "/login" 60 \
245
+ || { tail -30 "$DATA_DIR/run/web.log" 2>/dev/null; fail "ework-web did not come up"; }
132
246
 
133
247
  info "wait for daemon /api/status"
134
- for i in $(seq 1 60); do
135
- if curl -sf -o /dev/null "http://127.0.0.1:$DAEMON_PORT/api/status"; then
136
- pass "/api/status responds (after ${i} half-seconds)"
137
- break
138
- fi
139
- sleep 0.5
140
- [[ $i -eq 60 ]] && { tail -30 "$DATA_DIR/run/daemon.log" 2>/dev/null; fail "daemon did not come up"; }
141
- done
248
+ wait_for_url "http://127.0.0.1:$DAEMON_PORT/api/status" "/api/status" 60 \
249
+ || { tail -30 "$DATA_DIR/run/daemon.log" 2>/dev/null; fail "daemon did not come up"; }
142
250
 
143
251
  info "v0.2.6 regression: no EACCES in web.log"
144
252
  sleep 1
@@ -155,13 +263,170 @@ if grep -q "invalid signature" "$DATA_DIR/run/daemon.log" 2>/dev/null; then
155
263
  fi
156
264
  pass "no invalid signature yet"
157
265
 
266
+ # -----------------------------------------------------------------------------
267
+ # Phase 3: subcommand smoke tests
268
+ # -----------------------------------------------------------------------------
269
+ echo
270
+ echo "====================================================="
271
+ echo "Phase 3: subcommand smoke tests"
272
+ echo "====================================================="
273
+
274
+ info "status: lists both services, both running, both listening"
275
+ STATUS_OUT=$(ework-aio status --data-dir "$DATA_DIR" 2>&1)
276
+ echo "$STATUS_OUT"
277
+ echo "$STATUS_OUT" | grep -q "ework-web" || fail "status missing ework-web line"
278
+ echo "$STATUS_OUT" | grep -q "ework-daemon" || fail "status missing ework-daemon line"
279
+ echo "$STATUS_OUT" | grep "ework-web" | grep -q "✓ running" || fail "web not reported running"
280
+ echo "$STATUS_OUT" | grep "ework-daemon" | grep -q "✓ running" || {
281
+ echo "$STATUS_OUT" | grep "ework-daemon"
282
+ fail "daemon not reported running (see status output above)"
283
+ }
284
+ pass "status lists both services as running"
285
+
286
+ info "env: prints key paths"
287
+ ENV_OUT=$(ework-aio env --data-dir "$DATA_DIR" 2>&1)
288
+ echo "$ENV_OUT"
289
+ echo "$ENV_OUT" | grep -q "data dir" || fail "env missing 'data dir'"
290
+ echo "$ENV_OUT" | grep -q "web env" || fail "env missing 'web env'"
291
+ echo "$ENV_OUT" | grep -q "daemon env" || fail "env missing 'daemon env'"
292
+ echo "$ENV_OUT" | grep -q "bot token" || fail "env missing 'bot token'"
293
+ # env.ts globs for bot-token* (install.ts writes bot-token.<botName> when
294
+ # --bot-name isn't default). With --bot-name e2e-bot in this run, the env
295
+ # output MUST list the suffixed file.
296
+ echo "$ENV_OUT" | grep -q "bot-token.e2e-bot" \
297
+ || fail "env missing bot-token.e2e-bot (install wrote it but env didn't list it)"
298
+ # Sanity: the printed .env paths must actually exist.
299
+ for p in "$DATA_DIR/ework-web/.env" "$DATA_DIR/ework-daemon/.env"; do
300
+ [[ -f "$p" ]] || fail "env printed non-existent path: $p"
301
+ done
302
+ pass "env prints valid paths (including bot-token suffix)"
303
+
304
+ info "config list: shows settable keys"
305
+ CONFIG_LIST_OUT=$(ework-aio config list --data-dir "$DATA_DIR" 2>&1)
306
+ echo "$CONFIG_LIST_OUT"
307
+ echo "$CONFIG_LIST_OUT" | grep -q "WORK_PORT" || fail "config list missing WORK_PORT"
308
+ echo "$CONFIG_LIST_OUT" | grep -q "DAEMON_PORT" || fail "config list missing DAEMON_PORT"
309
+ # Secrets must NOT appear (enforced by SECRET_ENV_VARS exclusion).
310
+ echo "$CONFIG_LIST_OUT" | grep -q "WORK_TOKEN" \
311
+ && fail "config list leaks WORK_TOKEN (should be excluded)" || true
312
+ pass "config list shows settable keys, excludes secrets"
313
+
314
+ info "config get WORK_PORT: prints the port"
315
+ CONFIG_GET_OUT=$(ework-aio config get WORK_PORT --data-dir "$DATA_DIR" 2>&1)
316
+ echo "$CONFIG_GET_OUT"
317
+ [[ "$CONFIG_GET_OUT" == "$WORK_PORT" ]] \
318
+ || fail "config get WORK_PORT returned '$CONFIG_GET_OUT', expected '$WORK_PORT'"
319
+ pass "config get WORK_PORT = $WORK_PORT"
320
+
321
+ info "config get on a secret key: rejects"
322
+ if ework-aio config get WORK_TOKEN --data-dir "$DATA_DIR" 2>/dev/null; then
323
+ fail "config get WORK_TOKEN should have been rejected"
324
+ fi
325
+ pass "config get on secret rejected"
326
+
327
+ info "config set WORK_TTS_SPEED 1.5 --no-restart: writes .env, no restart"
328
+ ework-aio config set WORK_TTS_SPEED 1.5 --no-restart --data-dir "$DATA_DIR" 2>&1 | tail -5
329
+ TTS_VAL=$(env_val WORK_TTS_SPEED "$DATA_DIR/ework-web/.env")
330
+ [[ "$TTS_VAL" == "1.5" ]] || fail "WORK_TTS_SPEED not persisted in .env (got '$TTS_VAL')"
331
+ pass "config set persisted WORK_TTS_SPEED=1.5"
332
+
333
+ info "config set with newline value: rejected (env-injection guard)"
334
+ # Pre-v0.2.6, a value like "alice\nWORK_TOKEN=evil" would inject a new key
335
+ # into .env. cli-dispatch.test.ts:138 covers this in unit tests; we cover
336
+ # it here too because the integration surface (bash quoting) differs.
337
+ if ework-aio config set WORK_OPERATOR_LOGIN $'alice\nWORK_TOKEN=evil' \
338
+ --no-restart --data-dir "$DATA_DIR" 2>/dev/null; then
339
+ fail "config set accepted a newline value (env-injection regression)"
340
+ fi
341
+ pass "config set rejects newline value"
342
+ # Confirm the injection didn't actually happen.
343
+ if grep -q "^WORK_TOKEN=evil" "$DATA_DIR/ework-web/.env"; then
344
+ fail "WORK_TOKEN=evil was injected into .env despite rejection"
345
+ fi
346
+ pass "no env injection occurred"
347
+
348
+ info "logs web: starts tailing, exits on SIGTERM"
349
+ # logs.ts uses fs.watchFile and never resolves — must be killed.
350
+ # Use `timeout` to send SIGTERM after 1s; expect exit 124 (timeout's SIGTERM
351
+ # exit) or 143 (SIGTERM). The point: it started without error (no
352
+ # "log file not found") and produced output.
353
+ set +e
354
+ LOGS_OUT=$(timeout -s TERM 1 ework-aio logs web --data-dir "$DATA_DIR" 2>&1)
355
+ LOGS_RC=$?
356
+ set -e
357
+ [[ $LOGS_RC -eq 124 || $LOGS_RC -eq 143 ]] \
358
+ || fail "logs web exited $LOGS_RC (expected 124/143 from SIGTERM)"
359
+ # Output should contain SOMETHING (the "tailing" line + recent log content).
360
+ [[ -n "$LOGS_OUT" ]] || fail "logs web produced no output"
361
+ pass "logs web tail started, exited cleanly on SIGTERM"
362
+
363
+ # -----------------------------------------------------------------------------
364
+ # Phase 4: service lifecycle (stop / start / restart)
365
+ # -----------------------------------------------------------------------------
366
+ echo
367
+ echo "====================================================="
368
+ echo "Phase 4: service lifecycle"
369
+ echo "====================================================="
370
+
371
+ WEB_PID_BEFORE=$(pid_of web)
372
+ DAEMON_PID_BEFORE=$(pid_of daemon)
373
+ [[ -n "$WEB_PID_BEFORE" ]] || fail "web pid empty before stop"
374
+ [[ -n "$DAEMON_PID_BEFORE" ]] || fail "daemon pid empty before stop"
375
+ pass "before stop: web pid=$WEB_PID_BEFORE, daemon pid=$DAEMON_PID_BEFORE"
376
+
377
+ info "stop both services"
378
+ ework-aio stop --data-dir "$DATA_DIR" 2>&1 | tail -5
379
+ wait_for_no_pid web || fail "web pid still alive after stop"
380
+ wait_for_no_pid daemon || fail "daemon pid still alive after stop"
381
+
382
+ info "confirm /login stops responding"
383
+ sleep 1
384
+ if curl -sf -o /dev/null "http://127.0.0.1:$WORK_PORT/login" 2>/dev/null; then
385
+ fail "/login still responds after stop — web did not actually stop"
386
+ fi
387
+ pass "/login stopped responding"
388
+
389
+ info "start both services"
390
+ ework-aio start --data-dir "$DATA_DIR" 2>&1 | tail -5
391
+ # Wait for both services to bind their ports again.
392
+ wait_for_url "http://127.0.0.1:$WORK_PORT/login" "/login after start" 60 \
393
+ || { tail -30 "$DATA_DIR/run/web.log"; fail "web did not come up after start"; }
394
+ wait_for_url "http://127.0.0.1:$DAEMON_PORT/api/status" "/api/status after start" 60 \
395
+ || { tail -30 "$DATA_DIR/run/daemon.log"; fail "daemon did not come up after start"; }
396
+
397
+ WEB_PID_AFTER_START=$(pid_of web)
398
+ DAEMON_PID_AFTER_START=$(pid_of daemon)
399
+ [[ "$WEB_PID_AFTER_START" != "$WEB_PID_BEFORE" ]] \
400
+ || fail "web pid unchanged across stop+start ($WEB_PID_BEFORE)"
401
+ [[ "$DAEMON_PID_AFTER_START" != "$DAEMON_PID_BEFORE" ]] \
402
+ || fail "daemon pid unchanged across stop+start ($DAEMON_PID_BEFORE)"
403
+ pass "PIDs changed after stop+start (web $WEB_PID_BEFORE→$WEB_PID_AFTER_START)"
404
+
405
+ info "restart both services (config restart both)"
406
+ ework-aio config restart both --data-dir "$DATA_DIR" 2>&1 | tail -5
407
+ wait_for_new_pid web "$WEB_PID_AFTER_START" 60 \
408
+ || fail "web pid did not change after restart"
409
+ wait_for_new_pid daemon "$DAEMON_PID_AFTER_START" 60 \
410
+ || fail "daemon pid did not change after restart"
411
+
412
+ info "confirm services respond after restart"
413
+ wait_for_url "http://127.0.0.1:$WORK_PORT/login" "/login after restart" 60 \
414
+ || fail "web did not come up after restart"
415
+ wait_for_url "http://127.0.0.1:$DAEMON_PORT/api/status" "/api/status after restart" 60 \
416
+ || fail "daemon did not come up after restart"
417
+
418
+ # -----------------------------------------------------------------------------
419
+ # Phase 5: issue flow (the actual product path)
420
+ # -----------------------------------------------------------------------------
421
+ echo
422
+ echo "====================================================="
423
+ echo "Phase 5: issue flow (create project → create issue → webhook)"
424
+ echo "====================================================="
425
+
158
426
  info "build admin auth cookie"
159
- WORK_TOKEN=$(grep ^WORK_TOKEN= "$DATA_DIR/ework-web/.env" | cut -d= -f2-)
160
- WORK_COOKIE_SECRET=$(grep ^WORK_COOKIE_SECRET= "$DATA_DIR/ework-web/.env" | cut -d= -f2-)
161
- COOKIE_SIG=$(printf '%s' "$WORK_TOKEN" \
162
- | openssl dgst -sha256 -hmac "$WORK_COOKIE_SECRET" -binary \
163
- | base64 | tr '+/' '-_' | tr -d '=')
164
- AUTH_COOKIE="ework_auth=${WORK_TOKEN}.${COOKIE_SIG}"
427
+ AUTH_COOKIE=$(build_auth_cookie)
428
+ [[ -n "$AUTH_COOKIE" ]] || fail "auth cookie empty"
429
+ pass "auth cookie built"
165
430
 
166
431
  info "create test project (triggers autoWire webhook registration)"
167
432
  PROJ_CODE=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
@@ -200,8 +465,41 @@ ACTIVE=$(curl -sS "http://127.0.0.1:$DAEMON_PORT/api/status" | jq -r '.issues //
200
465
  || fail "daemon did not register the issue (issues=$ACTIVE)"
201
466
  pass "daemon registered issue (issues=$ACTIVE)"
202
467
 
468
+ # -----------------------------------------------------------------------------
469
+ # Phase 6: uninstall
470
+ # -----------------------------------------------------------------------------
471
+ echo
472
+ echo "====================================================="
473
+ echo "Phase 6: uninstall"
474
+ echo "====================================================="
475
+
476
+ info "ework-aio uninstall: stops services + removes pidfiles"
477
+ ework-aio uninstall --data-dir "$DATA_DIR" 2>&1 | tail -10
478
+
479
+ wait_for_no_pid web 15 || fail "web pid still alive after uninstall"
480
+ wait_for_no_pid daemon 15 || fail "daemon pid still alive after uninstall"
481
+
482
+ # pidfiles should be gone OR empty.
483
+ for svc in web daemon; do
484
+ pf="$DATA_DIR/run/$svc.pid"
485
+ if [[ -f "$pf" && -s "$pf" ]]; then
486
+ fail "$svc.pid still exists and non-empty after uninstall: $(cat "$pf")"
487
+ fi
488
+ done
489
+ pass "pidfiles cleared"
490
+
491
+ info "uninstall preserves data dir"
492
+ # uninstall.ts:62 documents "services removed. data preserved at <dir>".
493
+ # Verify the .env files survive so a re-install picks up the same token.
494
+ [[ -f "$DATA_DIR/ework-web/.env" ]] || fail "web .env deleted by uninstall"
495
+ [[ -f "$DATA_DIR/ework-daemon/.env" ]] || fail "daemon .env deleted by uninstall"
496
+ pass "data preserved (both .env files intact)"
497
+
498
+ # -----------------------------------------------------------------------------
203
499
  echo
204
- echo "===== E2E PASSED ====="
500
+ echo "====================================================="
501
+ echo "E2E PASSED"
502
+ echo "====================================================="
205
503
  echo " ework-aio version: $(ework-aio --version 2>/dev/null || echo unknown)"
206
504
  echo " data dir: $DATA_DIR"
207
505
  echo " web: http://127.0.0.1:$WORK_PORT/login"
@@ -1,6 +1,8 @@
1
1
  // `ework-aio env` — print resolved paths (no secrets). Useful for users
2
2
  // to find where data, .env files, and the bot token live without grepping.
3
3
 
4
+ import fs from "node:fs";
5
+ import path from "node:path";
4
6
  import { Logger } from "../log.ts";
5
7
  import { resolvePaths } from "../paths.ts";
6
8
  import type { GlobalOptions } from "../types.ts";
@@ -12,13 +14,33 @@ export async function runEnv(opts: GlobalOptions, logger: Logger): Promise<void>
12
14
  useSystemd: opts.useSystemd,
13
15
  });
14
16
 
17
+ // install.ts writes the bot PAT to <dataDir>/bot-token OR
18
+ // <dataDir>/bot-token.<botName> when --bot-name isn't the default.
19
+ // paths.botTokenFile only knows the un-suffixed path, so glob for any
20
+ // bot-token* matches in the data dir rather than confidently printing a
21
+ // path that may not exist.
22
+ let botTokenPaths: string[] = [];
23
+ try {
24
+ for (const name of await fs.promises.readdir(paths.dataDir)) {
25
+ if (name === "bot-token" || name.startsWith("bot-token.")) {
26
+ botTokenPaths.push(path.join(paths.dataDir, name));
27
+ }
28
+ }
29
+ } catch {
30
+ // data dir doesn't exist yet — fall through to the un-suffixed default
31
+ // so the user sees what path install.ts WOULD use.
32
+ }
33
+ const botTokenLine = botTokenPaths.length === 0
34
+ ? `${paths.botTokenFile} (not yet created — run ework-aio install)`
35
+ : botTokenPaths.join(", ");
36
+
15
37
  logger.hr();
16
38
  logger.log("ework-aio paths");
17
39
  logger.hr();
18
40
  logger.log(` data dir : ${paths.dataDir}`);
19
41
  logger.log(` web env : ${paths.webEnvFile}`);
20
42
  logger.log(` daemon env : ${paths.daemonEnvFile}`);
21
- logger.log(` bot token : ${paths.botTokenFile}`);
43
+ logger.log(` bot token : ${botTokenLine}`);
22
44
  logger.log(` opencode cfg : ${paths.opencodeConfigFile}`);
23
45
  logger.log(` scope : ${opts.scope}`);
24
46
  logger.log(` mode : ${opts.useSystemd ? "systemd" : "PID-file"}`);