ework-aio 0.1.4 → 0.1.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/README.md CHANGED
@@ -103,6 +103,12 @@ ework-aio uninstall Stop services and remove units (data preserved)
103
103
  ework-aio status Show service status
104
104
  ework-aio logs [web|daemon] Tail logs
105
105
  ework-aio env Print key paths (no secrets)
106
+ ework-aio config <subcommand> Read / change runtime .env keys
107
+ config list List settable keys + current values
108
+ config get <KEY> Print one key's current value
109
+ config set <KEY> <VALUE> Set a key, then restart affected service
110
+ (use --no-restart to defer)
111
+ config restart <web|daemon|both> Restart one or both services
106
112
  ```
107
113
 
108
114
  ### Install options
@@ -147,6 +153,56 @@ Re-running `ework-aio install` is safe:
147
153
  - Systemd units are overwritten (config drift auto-corrected).
148
154
  - Plugin merge skips if `opencode-ework` already in `plugin` array.
149
155
 
156
+ ## Runtime configuration (`ework-aio config`)
157
+
158
+ For changing settings after install, prefer `config` over editing `.env` by hand — it writes the right file, handles cross-service dependencies, and restarts the affected service for you.
159
+
160
+ ```bash
161
+ ework-aio config list # see all settable keys + current values
162
+ ework-aio config get WORK_PORT # read one
163
+ ework-aio config set WORK_PORT 8080 # set + auto-restart ework-web
164
+ ework-aio config set WORK_PORT 8080 --no-restart # stage the change, apply later
165
+ ework-aio config restart both # explicit restart
166
+ ```
167
+
168
+ ### Settable keys
169
+
170
+ | Key | Service | What it controls |
171
+ | ---------------------------- | ------- | ---------------------------------------------------------- |
172
+ | `WORK_PORT` | web | ework-web listen port (default 3002) |
173
+ | `WORK_HOST` | web | bind address (default 127.0.0.1; use 0.0.0.0 for LAN) |
174
+ | `WORK_OPERATOR_LOGIN` | web | login auto-promoted to admin |
175
+ | `WORK_OPENCODE_BIN` | web | opencode binary path used by ework-web |
176
+ | `WORK_TRANSLATE_URL` | web | OpenAI-compat `/v1/chat/completions` endpoint for translate |
177
+ | `WORK_TRANSLATE_MODEL` | web | translate model name |
178
+ | `WORK_TTS_SPEED` | web | TTS playback rate (default 1.0) |
179
+ | `WORK_FILE_ROOTS` | web | comma-separated file-viewer roots |
180
+ | `WORK_COMMENT_SORT` | web | `desc` or `asc` |
181
+ | `DAEMON_PORT` | daemon | ework-daemon listen port (default 3101) |
182
+ | `DAEMON_HOST` | daemon | bind address |
183
+ | `OPENCODE_BINARY` | daemon | opencode binary path |
184
+ | `OPENCODE_BASE_WORKDIR` | daemon | opencode working directory base |
185
+ | `COMPLETION_CHECK_API_KEY` | daemon | completion-check API key |
186
+ | `COMPLETION_CHECK_BASE_URL` | daemon | completion-check API base URL |
187
+ | `COMPLETION_CHECK_MODEL` | daemon | completion-check model name |
188
+
189
+ ### Cross-service dependencies
190
+
191
+ Changing `WORK_PORT` or `DAEMON_PORT` also rewrites the URL the *other* service uses to call it (so the daemon still finds web, and web still finds the daemon), and restarts **both**:
192
+
193
+ - `config set WORK_PORT 8080` → updates `WORK_PORT` in web env + `GITEA_URL` in daemon env, restarts both
194
+ - `config set DAEMON_PORT 3102` → updates `DAEMON_PORT` in daemon env + `WORK_DAEMON_WEBHOOK_URL` in web env, restarts both
195
+
196
+ ### Not settable here (use install)
197
+
198
+ Secrets and the web↔daemon contract aren't exposed via `config` — rerun `ework-aio install` to regenerate them (delete the relevant `.env` first if you want fresh tokens):
199
+
200
+ - Random secrets: `WORK_TOKEN`, `WORK_COOKIE_SECRET`, `WORK_DAEMON_WEBHOOK_SECRET`, `BOT_TOKEN`
201
+ - DB / attachment paths: `WORK_DB_PATH`, `WORK_ATTACHMENT_ROOT`, `DAEMON_DB_PATH`
202
+ - Web↔daemon contract: `GITEA_URL`, `GITEA_TOKEN`, `WORK_DAEMON_BOT_LOGIN`, `WORK_DAEMON_WEBHOOK_URL`, `BOT_USERNAME`
203
+
204
+ To regenerate all of them: `rm -rf ~/.local/share/ework-aio && ework-aio install`.
205
+
150
206
  ## Uninstall
151
207
 
152
208
  ```bash
package/bin/ework-aio CHANGED
@@ -16,16 +16,22 @@ Usage:
16
16
  ework-aio install [options] Install and start ework-web + ework-daemon
17
17
  ework-aio uninstall Stop services and remove units (keeps data)
18
18
  ework-aio status Show service status
19
- ework-aio logs [svc] Tail logs (svc: ework-web | ework-daemon)
19
+ ework-aio logs [svc] Tail logs (svc: web | daemon)
20
20
  ework-aio env Print the .env path and key names (no values)
21
+ ework-aio config <subcommand> Read / change runtime .env keys
22
+ config list List settable keys + current values
23
+ config get <KEY> Print one key's value
24
+ config set <KEY> <VALUE> Set a key, then restart affected service
25
+ (use --no-restart to defer)
26
+ config restart <web|daemon|both>
21
27
  ework-aio --version Print version
22
28
 
23
29
  Install options:
24
30
  --user Use user-level systemd units (default if non-root)
25
31
  --system Use system-level systemd units (default if root)
26
32
  --data-dir <path> Override data directory (default: ~/.local/share/ework-aio)
27
- --port <n> ework-web port (default: 3002)
28
- --daemon-port <n> ework-daemon port (default: 3101)
33
+ --port <n> ework-web port (default 3002)
34
+ --daemon-port <n> ework-daemon port (default 3101)
29
35
  --bot-name <login> Bot username (default: ework-daemon)
30
36
  --no-start Install units but don't start services
31
37
  --yes Skip all prompts (use generated defaults)
@@ -49,7 +55,7 @@ if (!existsSync(installSh)) {
49
55
  process.exit(1);
50
56
  }
51
57
 
52
- const knownSubs = ["install", "uninstall", "status", "logs", "env"];
58
+ const knownSubs = ["install", "uninstall", "status", "logs", "env", "config"];
53
59
  const finalArgs = knownSubs.includes(subcommand) ? args : ["install", ...args];
54
60
 
55
61
  const result = spawnSync(["bash", installSh, ...finalArgs], {
package/bin/install.sh CHANGED
@@ -30,12 +30,27 @@ DAEMON_PORT="3101"
30
30
  BOT_NAME="ework-daemon"
31
31
  NO_START=0
32
32
  ASSUME_YES=0
33
+ NO_RESTART=0
34
+ CFG_ARGS=()
33
35
 
34
36
  usage() {
35
37
  cat <<'EOF'
36
- ework-aio install [options]
37
-
38
- Options:
38
+ ework-aio <command> [options]
39
+
40
+ Commands:
41
+ install [options] Install or upgrade the ework stack (default)
42
+ uninstall Stop services and remove units (data preserved)
43
+ status Show service status
44
+ logs [web|daemon] Tail logs
45
+ env Print key paths (no secrets)
46
+ config <subcommand> Read / change runtime config (.env keys)
47
+ config list List all settable keys + current values
48
+ config get <KEY> Print current value of one key
49
+ config set <KEY> <VALUE> Set a key, then restart affected service
50
+ (unless --no-restart is given)
51
+ config restart <web|daemon> Restart one or both services
52
+
53
+ Install options:
39
54
  --user Use user-level systemd units (default if non-root)
40
55
  --system Use system-level systemd units (default if root)
41
56
  --data-dir <path> Override data directory (default: ~/.local/share/ework-aio)
@@ -44,7 +59,9 @@ Options:
44
59
  --bot-name <login> Bot username (default: ework-daemon)
45
60
  --no-start Install units but don't start services
46
61
  --yes Skip all prompts (use generated defaults)
47
- --mode <install|uninstall|status|logs|env> (internal; first positional arg wins)
62
+
63
+ Global options:
64
+ --no-restart With `config set`: edit .env but skip the restart
48
65
  -h, --help Show this help
49
66
  EOF
50
67
  }
@@ -52,6 +69,18 @@ EOF
52
69
  while [[ $# -gt 0 ]]; do
53
70
  case "$1" in
54
71
  install|uninstall|status|logs|env) MODE="$1"; shift ;;
72
+ config)
73
+ MODE="config"
74
+ shift
75
+ # Consume remaining args as config subcommand + its positionals.
76
+ while [[ $# -gt 0 ]]; do
77
+ case "$1" in
78
+ --no-restart) NO_RESTART=1; shift ;;
79
+ -h|--help) CFG_ARGS=("help"); shift ;;
80
+ *) CFG_ARGS+=("$1"); shift ;;
81
+ esac
82
+ done
83
+ ;;
55
84
  --user) SCOPENAME="--user"; shift ;;
56
85
  --system) SCOPENAME="--system"; shift ;;
57
86
  --data-dir) DATA_DIR="$2"; shift 2 ;;
@@ -59,6 +88,7 @@ while [[ $# -gt 0 ]]; do
59
88
  --daemon-port) DAEMON_PORT="$2"; shift 2 ;;
60
89
  --bot-name) BOT_NAME="$2"; shift 2 ;;
61
90
  --no-start) NO_START=1; shift ;;
91
+ --no-restart) NO_RESTART=1; shift ;;
62
92
  --yes|-y) ASSUME_YES=1; shift ;;
63
93
  -h|--help) usage; exit 0 ;;
64
94
  *) die "Unknown argument: $1 (try --help)" ;;
@@ -74,15 +104,20 @@ fi
74
104
  need_cmd() {
75
105
  command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1. $2"
76
106
  }
77
- need_cmd bun "Install from https://bun.sh"
78
- need_cmd npm "Install Node.js or Bun (ships npm)"
79
- need_cmd opencode "Install from https://opencode.ai"
107
+ # Commands every mode needs (status/logs/env/config/uninstall all use systemctl).
80
108
  need_cmd systemctl "This installer requires systemd."
81
- need_cmd openssl "Install the openssl package."
82
- need_cmd curl "Install curl."
83
- need_cmd jq "Install jq (for opencode.json merge)."
84
109
  need_cmd awk "Should be present on any Linux."
85
110
 
111
+ # Install-only deps. Other modes work even if these are missing.
112
+ if [[ "$MODE" == "install" ]]; then
113
+ need_cmd bun "Install from https://bun.sh"
114
+ need_cmd npm "Install Node.js or Bun (ships npm)"
115
+ need_cmd opencode "Install from https://opencode.ai"
116
+ need_cmd openssl "Install the openssl package."
117
+ need_cmd curl "Install curl."
118
+ need_cmd jq "Install jq (for opencode.json merge)."
119
+ fi
120
+
86
121
  # Verify the 3 npm packages are reachable. If not, try to install them now.
87
122
  ensure_pkg() {
88
123
  local pkg="$1"
@@ -227,6 +262,211 @@ EOF
227
262
 
228
263
  ctl() { systemctl "$SCOPENAME" "$@"; }
229
264
 
265
+ # ─── Config mode: read/change runtime .env keys ─────────────────────────────
266
+ # Each entry: KEY|SERVICE|DESCRIPTION. Only keys in this list are settable via
267
+ # `config set`. Secrets (WORK_TOKEN, *_WEBHOOK_SECRET, BOT_TOKEN), DB paths,
268
+ # and the web<->daemon contract (GITEA_URL/TOKEN, WORK_DAEMON_WEBHOOK_*) are
269
+ # deliberately excluded — changing them by hand breaks the install.
270
+ SETTABLE_KEYS=(
271
+ "WORK_PORT|web|ework-web listen port (default 3002)"
272
+ "WORK_HOST|web|ework-web bind address (default 127.0.0.1; use 0.0.0.0 for LAN)"
273
+ "WORK_OPERATOR_LOGIN|web|login auto-promoted to admin"
274
+ "WORK_OPENCODE_BIN|web|opencode binary path used by ework-web"
275
+ "WORK_TRANSLATE_URL|web|OpenAI-compat /v1/chat/completions endpoint for translate"
276
+ "WORK_TRANSLATE_MODEL|web|translate model name"
277
+ "WORK_TTS_SPEED|web|TTS playback rate (default 1.0)"
278
+ "WORK_FILE_ROOTS|web|comma-separated file-viewer roots"
279
+ "WORK_COMMENT_SORT|web|comment sort order: desc|asc"
280
+ "DAEMON_PORT|daemon|ework-daemon listen port (default 3101)"
281
+ "DAEMON_HOST|daemon|ework-daemon bind address (default 127.0.0.1)"
282
+ "OPENCODE_BINARY|daemon|opencode binary path"
283
+ "OPENCODE_BASE_WORKDIR|daemon|opencode working directory base"
284
+ "COMPLETION_CHECK_API_KEY|daemon|completion-check API key"
285
+ "COMPLETION_CHECK_BASE_URL|daemon|completion-check API base URL"
286
+ "COMPLETION_CHECK_MODEL|daemon|completion-check model name"
287
+ )
288
+
289
+ key_service() {
290
+ local k="$1" e
291
+ for e in "${SETTABLE_KEYS[@]}"; do
292
+ [[ "$e" == "$k|"* ]] || continue
293
+ local rest="${e#*|}"
294
+ echo "${rest%%|*}"
295
+ return
296
+ done
297
+ }
298
+ key_default() {
299
+ local k="$1" e
300
+ for e in "${SETTABLE_KEYS[@]}"; do
301
+ [[ "$e" == "$k|"* ]] || continue
302
+ local rest="${e#*|}"
303
+ echo "${rest#*|}"
304
+ return
305
+ done
306
+ }
307
+ key_settable() { local k="$1"; local e; for e in "${SETTABLE_KEYS[@]}"; do [[ "$e" == "$k|"* ]] && return 0; done; return 1; }
308
+
309
+ env_file_for() {
310
+ case "$1" in
311
+ web) echo "$WEB_ENV" ;;
312
+ daemon) echo "$DAEMON_ENV" ;;
313
+ *) return 1 ;;
314
+ esac
315
+ }
316
+
317
+ env_get() {
318
+ local file="$1" key="$2"
319
+ [[ -f "$file" ]] || return 1
320
+ awk -F= -v k="$key" '$1==k {sub(/^[^=]*=/,""); print; found=1} END{exit !found}' "$file"
321
+ }
322
+
323
+ env_set() {
324
+ local file="$1" key="$2" val="$3"
325
+ [[ -f "$file" ]] || { mkdir -p "$(dirname "$file")"; touch "$file"; chmod 600 "$file"; }
326
+ if grep -q "^${key}=" "$file"; then
327
+ sed -i "s|^${key}=.*|${key}=${val}|" "$file"
328
+ else
329
+ printf '%s=%s\n' "$key" "$val" >> "$file"
330
+ fi
331
+ }
332
+
333
+ service_for_key() {
334
+ case "$1" in
335
+ WORK_PORT)
336
+ echo "both" ;;
337
+ DAEMON_PORT)
338
+ echo "both" ;;
339
+ *)
340
+ echo "$(key_service "$1")" ;;
341
+ esac
342
+ }
343
+
344
+ restart_service() {
345
+ local svc="$1"
346
+ case "$svc" in
347
+ web) ctl restart ework-web.service && ok "Restarted ework-web" ;;
348
+ daemon) ctl restart ework-daemon.service && ok "Restarted ework-daemon" ;;
349
+ both)
350
+ ctl restart ework-web.service ework-daemon.service && ok "Restarted ework-web + ework-daemon"
351
+ ;;
352
+ *) return 1 ;;
353
+ esac
354
+ }
355
+
356
+ config_help() {
357
+ cat <<'EOF'
358
+ ework-aio config <subcommand>
359
+
360
+ Subcommands:
361
+ list List all settable keys + current values
362
+ get <KEY> Print current value of one key
363
+ set <KEY> <VALUE> Set a key in .env, then restart the affected
364
+ service (unless --no-restart is given)
365
+ restart <web|daemon|both> Restart one or both services
366
+
367
+ Examples:
368
+ ework-aio config list
369
+ ework-aio config get WORK_PORT
370
+ ework-aio config set WORK_PORT 8080
371
+ ework-aio config set WORK_TRANSLATE_URL http://127.0.0.1:8000/v1 --no-restart
372
+ ework-aio config restart both
373
+
374
+ Note: changing WORK_PORT or DAEMON_PORT also rewrites the cross-link the other
375
+ service uses (GITEA_URL on daemon side, WORK_DAEMON_WEBHOOK_URL on web side),
376
+ and restarts both. Secrets and DB paths are not settable here — rerun
377
+ `ework-aio install` (with `rm .env` first if you need new tokens).
378
+ EOF
379
+ }
380
+
381
+ config_list() {
382
+ hr; log "Settable config keys"; hr
383
+ printf ' %-28s %-8s %s\n' "KEY" "SERVICE" "VALUE"
384
+ local e k svc val env_file
385
+ for e in "${SETTABLE_KEYS[@]}"; do
386
+ k="${e%%|*}"
387
+ svc="$(key_service "$k")"
388
+ env_file="$(env_file_for "$svc")"
389
+ val="$(env_get "$env_file" "$k" 2>/dev/null || echo '')"
390
+ [[ -z "$val" ]] && val="(unset)"
391
+ printf ' %-28s %-8s %s\n' "$k" "$svc" "$val"
392
+ done
393
+ hr
394
+ printf 'Use %sconfig set <KEY> <VALUE>%s to change a key.\n' "$c_bold" "$c_reset"
395
+ }
396
+
397
+ config_get() {
398
+ local k="$1"
399
+ [[ -n "$k" ]] || die "Usage: ework-aio config get <KEY>"
400
+ key_settable "$k" || die "Key '$k' is not settable. Run 'ework-aio config list' for the allow-list."
401
+ local svc env_file val
402
+ svc="$(key_service "$k")"
403
+ env_file="$(env_file_for "$svc")"
404
+ val="$(env_get "$env_file" "$k" 2>/dev/null)" || { warn "$k is not currently set in $env_file"; exit 0; }
405
+ printf '%s\n' "$val"
406
+ }
407
+
408
+ config_set() {
409
+ local k="$1" v="$2"
410
+ [[ -n "$k" && -n "$v" ]] || die "Usage: ework-aio config set <KEY> <VALUE>"
411
+ key_settable "$k" || die "Key '$k' is not settable. Run 'ework-aio config list' for the allow-list."
412
+
413
+ local svc env_file
414
+ svc="$(key_service "$k")"
415
+ env_file="$(env_file_for "$svc")"
416
+
417
+ log "Setting $k=$v in $env_file"
418
+ cp "$env_file" "$env_file.bak.$(date +%s)" 2>/dev/null || true
419
+ env_set "$env_file" "$k" "$v"
420
+ ok "$k updated"
421
+
422
+ case "$k" in
423
+ WORK_PORT)
424
+ log "Propagating to daemon (GITEA_URL)"
425
+ cp "$DAEMON_ENV" "$DAEMON_ENV.bak.$(date +%s)" 2>/dev/null || true
426
+ env_set "$DAEMON_ENV" "GITEA_URL" "http://127.0.0.1:$v"
427
+ ok "DAEMON_ENV GITEA_URL updated"
428
+ ;;
429
+ DAEMON_PORT)
430
+ log "Propagating to web (WORK_DAEMON_WEBHOOK_URL)"
431
+ cp "$WEB_ENV" "$WEB_ENV.bak.$(date +%s)" 2>/dev/null || true
432
+ env_set "$WEB_ENV" "WORK_DAEMON_WEBHOOK_URL" "http://127.0.0.1:$v"
433
+ ok "WEB_ENV WORK_DAEMON_WEBHOOK_URL updated"
434
+ ;;
435
+ esac
436
+
437
+ if [[ "$NO_RESTART" == "1" ]]; then
438
+ local to_restart
439
+ to_restart="$(service_for_key "$k")"
440
+ warn "--no-restart: changes saved but service not reloaded. Run 'ework-aio config restart $to_restart' to apply."
441
+ return
442
+ fi
443
+
444
+ local target
445
+ target="$(service_for_key "$k")"
446
+ log "Restarting $target..."
447
+ restart_service "$target" || warn "restart failed — services may need manual reload"
448
+ }
449
+
450
+ config_restart() {
451
+ local svc="${1:-both}"
452
+ case "$svc" in
453
+ web|daemon|both) restart_service "$svc" ;;
454
+ *) die "Usage: ework-aio config restart <web|daemon|both>" ;;
455
+ esac
456
+ }
457
+
458
+ run_config() {
459
+ local sub="${CFG_ARGS[0]:-list}"
460
+ case "$sub" in
461
+ list) config_list ;;
462
+ get) config_get "${CFG_ARGS[1]:-}" ;;
463
+ set) config_set "${CFG_ARGS[1]:-}" "${CFG_ARGS[2]:-}" ;;
464
+ restart) config_restart "${CFG_ARGS[1]:-both}" ;;
465
+ help|-h|--help) config_help ;;
466
+ *) die "Unknown config subcommand: $sub (try: ework-aio config help)" ;;
467
+ esac
468
+ }
469
+
230
470
  # ─── Modes ──────────────────────────────────────────────────────────────────
231
471
  case "$MODE" in
232
472
  env)
@@ -241,6 +481,11 @@ env)
241
481
  exit 0
242
482
  ;;
243
483
 
484
+ config)
485
+ run_config
486
+ exit 0
487
+ ;;
488
+
244
489
  status)
245
490
  hr; log "ework-aio status ($SCOPENAME)"; hr
246
491
  ctl is-active ework-web.service || true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-aio",
3
- "version": "0.1.4",
3
+ "version": "0.1.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",