ework-aio 0.1.17 → 0.2.1
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 +21 -20
- package/bin/ework-aio +3 -439
- package/package.json +6 -3
- package/src/cli.ts +504 -0
- package/src/commands/config.ts +208 -0
- package/src/commands/env.ts +27 -0
- package/src/commands/install.ts +665 -0
- package/src/commands/lifecycle.ts +206 -0
- package/src/commands/logs.ts +71 -0
- package/src/commands/uninstall.ts +64 -0
- package/src/config.ts +88 -0
- package/src/env.ts +238 -0
- package/src/log.ts +102 -0
- package/src/opencode-config.ts +104 -0
- package/src/paths.ts +95 -0
- package/src/pidfile.ts +166 -0
- package/src/preflight.ts +48 -0
- package/src/systemd.ts +234 -0
- package/src/types.ts +95 -0
- package/bin/install.sh +0 -1200
package/bin/install.sh
DELETED
|
@@ -1,1200 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ework-aio — host installer for the ework stack.
|
|
3
|
-
#
|
|
4
|
-
# Boots:
|
|
5
|
-
# • ework-web — multi-project issue tracker (web UI + Gitea-compat REST)
|
|
6
|
-
# • ework-daemon — issue-driven AI bridge (spawns opencode)
|
|
7
|
-
# • opencode-ework — plugin registered in ~/.config/opencode/opencode.json
|
|
8
|
-
#
|
|
9
|
-
# Idempotent. Re-runs preserve existing .env, bot token, and DB.
|
|
10
|
-
# Data lives under $DATA_DIR (default ~/.local/share/ework-aio).
|
|
11
|
-
# Services run as the current user via systemd --user (or --system if root).
|
|
12
|
-
|
|
13
|
-
set -euo pipefail
|
|
14
|
-
|
|
15
|
-
# ERR trap: when `set -e` kills the script, print the line + last command so
|
|
16
|
-
# the user sees why instead of an empty prompt. Tricky failure modes (root
|
|
17
|
-
# scope + polkit redirect, missing systemd PID 1, journalctl-as-root) all
|
|
18
|
-
# manifest as silent exit at a systemctl call — without this trace, the user
|
|
19
|
-
# just sees the prompt return and assumes install succeeded.
|
|
20
|
-
trap 'rc=$?; if [[ $rc -ne 0 ]]; then echo "${c_red}✗${c_reset} install.sh exited with code $rc at line $LINENO (most recent command: ${BASH_COMMAND})" >&2; echo " If running in PID-file mode (default), this is a real bug — please report." >&2; echo " If running with '\''systemd'\'' subcommand and systemd is unavailable, retry without it." >&2; fi' ERR
|
|
21
|
-
|
|
22
|
-
# ─── Pretty output ──────────────────────────────────────────────────────────
|
|
23
|
-
c_reset=$'\033[0m'; c_bold=$'\033[1m'; c_dim=$'\033[2m'
|
|
24
|
-
c_red=$'\033[31m'; c_grn=$'\033[32m'; c_ylw=$'\033[33m'; c_blu=$'\033[34m'
|
|
25
|
-
log() { printf '%s•%s %s\n' "$c_blu" "$c_reset" "$*"; }
|
|
26
|
-
ok() { printf '%s✓%s %s\n' "$c_grn" "$c_reset" "$*"; }
|
|
27
|
-
warn() { printf '%s!%s %s\n' "$c_ylw" "$c_reset" "$*" >&2; }
|
|
28
|
-
die() { printf '%s✗%s %s\n' "$c_red" "$c_reset" "$*" >&2; exit 1; }
|
|
29
|
-
hr() { printf '%s──%s\n' "$c_dim" "$c_reset"; }
|
|
30
|
-
|
|
31
|
-
# ─── Defaults & arg parsing ─────────────────────────────────────────────────
|
|
32
|
-
MODE="install"
|
|
33
|
-
SCOPENAME="--user"
|
|
34
|
-
DATA_DIR=""
|
|
35
|
-
WORK_PORT="3002"
|
|
36
|
-
DAEMON_PORT="3101"
|
|
37
|
-
BOT_NAME="ework-daemon"
|
|
38
|
-
NO_START=0
|
|
39
|
-
ASSUME_YES=0
|
|
40
|
-
NO_RESTART=0
|
|
41
|
-
AS_USER=""
|
|
42
|
-
ALLOW_ROOT=0
|
|
43
|
-
CFG_ARGS=()
|
|
44
|
-
MODE_ARGS=()
|
|
45
|
-
# USE_SYSTEMD=1 only when user runs `install systemd`. Default install is
|
|
46
|
-
# pure PID-file mode (no systemctl calls, no unit files, no linger prompt).
|
|
47
|
-
# This is the inverse of v0.1.x behavior where install always tried systemd
|
|
48
|
-
# first. PID-file mode is simpler, works on hosts without systemd, and
|
|
49
|
-
# matches the dominant failure mode we've seen in the wild.
|
|
50
|
-
USE_SYSTEMD=0
|
|
51
|
-
|
|
52
|
-
usage() {
|
|
53
|
-
cat <<'EOF'
|
|
54
|
-
ework-aio <command> [options]
|
|
55
|
-
|
|
56
|
-
Commands:
|
|
57
|
-
install [systemd] [options] Install or upgrade the ework stack (default)
|
|
58
|
-
Add 'systemd' to also write+enable systemd
|
|
59
|
-
units. Without 'systemd', runs in pure
|
|
60
|
-
PID-file mode (no systemctl calls).
|
|
61
|
-
uninstall Stop services and remove units (data preserved)
|
|
62
|
-
status Show service status
|
|
63
|
-
logs [web|daemon] Tail logs
|
|
64
|
-
env Print key paths (no secrets)
|
|
65
|
-
config <subcommand> Read / change runtime config (.env keys)
|
|
66
|
-
config list List all settable keys + current values
|
|
67
|
-
config get <KEY> Print current value of one key
|
|
68
|
-
config set <KEY> <VALUE> Set a key, then restart affected service
|
|
69
|
-
(unless --no-restart is given)
|
|
70
|
-
config restart <web|daemon> Restart one or both services
|
|
71
|
-
|
|
72
|
-
Install options:
|
|
73
|
-
systemd Also install systemd units + enable them.
|
|
74
|
-
Without this flag, install is PID-file only.
|
|
75
|
-
--user (with systemd) user-level units (default)
|
|
76
|
-
--system (with systemd) system-level units (needs sudo)
|
|
77
|
-
--data-dir <path> Override data directory (default: ~/.local/share/ework-aio)
|
|
78
|
-
--port <n> ework-web port (default: 3002)
|
|
79
|
-
--daemon-port <n> ework-daemon port (default: 3101)
|
|
80
|
-
--bot-name <login> Bot username (default: ework-daemon)
|
|
81
|
-
--no-start Install but don't start services
|
|
82
|
-
--yes Skip all prompts (use generated defaults)
|
|
83
|
-
--as-user <login> (sudo only) drop priv: re-exec install as <login>
|
|
84
|
-
after enabling linger. Data + opencode + npm
|
|
85
|
-
resolved under that user's HOME.
|
|
86
|
-
--allow-root (sudo only) override default refuse-on-root
|
|
87
|
-
|
|
88
|
-
Global options:
|
|
89
|
-
--no-restart With `config set`: edit .env but skip the restart
|
|
90
|
-
-h, --help Show this help
|
|
91
|
-
EOF
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
while [[ $# -gt 0 ]]; do
|
|
95
|
-
case "$1" in
|
|
96
|
-
install|uninstall|status|logs|env) MODE="$1"; shift ;;
|
|
97
|
-
config)
|
|
98
|
-
MODE="config"
|
|
99
|
-
shift
|
|
100
|
-
# Consume remaining args as config subcommand + its positionals.
|
|
101
|
-
# Global flags (--data-dir, --user, --system, etc.) are still
|
|
102
|
-
# respected here so `config set K V --data-dir X` works as expected.
|
|
103
|
-
while [[ $# -gt 0 ]]; do
|
|
104
|
-
case "$1" in
|
|
105
|
-
--no-restart) NO_RESTART=1; shift ;;
|
|
106
|
-
-h|--help) CFG_ARGS=("help"); shift ;;
|
|
107
|
-
--user) SCOPENAME="--user"; shift ;;
|
|
108
|
-
--system) SCOPENAME="--system"; shift ;;
|
|
109
|
-
--data-dir) DATA_DIR="$2"; shift 2 ;;
|
|
110
|
-
--port) WORK_PORT="$2"; shift 2 ;;
|
|
111
|
-
--daemon-port) DAEMON_PORT="$2"; shift 2 ;;
|
|
112
|
-
--bot-name) BOT_NAME="$2"; shift 2 ;;
|
|
113
|
-
--yes|-y) ASSUME_YES=1; shift ;;
|
|
114
|
-
*) CFG_ARGS+=("$1"); shift ;;
|
|
115
|
-
esac
|
|
116
|
-
done
|
|
117
|
-
;;
|
|
118
|
-
--user) SCOPENAME="--user"; shift ;;
|
|
119
|
-
--system) SCOPENAME="--system"; shift ;;
|
|
120
|
-
--data-dir) DATA_DIR="$2"; shift 2 ;;
|
|
121
|
-
--port) WORK_PORT="$2"; shift 2 ;;
|
|
122
|
-
--daemon-port) DAEMON_PORT="$2"; shift 2 ;;
|
|
123
|
-
--bot-name) BOT_NAME="$2"; shift 2 ;;
|
|
124
|
-
--no-start) NO_START=1; shift ;;
|
|
125
|
-
--no-restart) NO_RESTART=1; shift ;;
|
|
126
|
-
--yes|-y) ASSUME_YES=1; shift ;;
|
|
127
|
-
--as-user) AS_USER="$2"; shift 2 ;;
|
|
128
|
-
--allow-root) ALLOW_ROOT=1; shift ;;
|
|
129
|
-
-h|--help) usage; exit 0 ;;
|
|
130
|
-
--*) die "Unknown option: $1 (try --help)" ;;
|
|
131
|
-
*) MODE_ARGS+=("$1"); shift ;;
|
|
132
|
-
esac
|
|
133
|
-
done
|
|
134
|
-
|
|
135
|
-
# ─── Root guard: refuse install as root by default ──────────────────────────
|
|
136
|
-
# Running install as root puts data under /root/.local/share/ework-aio, bakes
|
|
137
|
-
# root-only paths into systemd units, and looks for opencode in root's PATH
|
|
138
|
-
# (usually missing). The right answer is almost always: don't use sudo.
|
|
139
|
-
# --as-user re-execs the script as the named user after enabling linger.
|
|
140
|
-
# --allow-root is an explicit acknowledgement that you've set up root's
|
|
141
|
-
# environment (opencode installed, npm prefix configured, etc.).
|
|
142
|
-
if [[ "$MODE" == "install" && "${EUID:-$(id -u)}" == "0" ]]; then
|
|
143
|
-
if [[ -n "$AS_USER" ]]; then
|
|
144
|
-
AS_USER_HOME="$(getent passwd "$AS_USER" 2>/dev/null | cut -d: -f6 || true)"
|
|
145
|
-
AS_USER_UID="$(getent passwd "$AS_USER" 2>/dev/null | cut -d: -f3 || true)"
|
|
146
|
-
if [[ -z "$AS_USER_HOME" || -z "$AS_USER_UID" ]]; then
|
|
147
|
-
die "--as-user: user '$AS_USER' not found in passwd database"
|
|
148
|
-
fi
|
|
149
|
-
if [[ "$AS_USER_UID" == "0" ]]; then
|
|
150
|
-
die "--as-user: target user is root — just use --allow-root instead"
|
|
151
|
-
fi
|
|
152
|
-
log "Enable linger for '$AS_USER' so --user services survive logout..."
|
|
153
|
-
loginctl enable-linger "$AS_USER" \
|
|
154
|
-
|| die "Failed to enable linger for '$AS_USER'. Try: sudo loginctl enable-linger $AS_USER"
|
|
155
|
-
ok "Linger enabled for '$AS_USER'"
|
|
156
|
-
log "Re-execing as '$AS_USER' (HOME=$AS_USER_HOME)..."
|
|
157
|
-
# Strip --as-user <name> from argv, preserve everything else.
|
|
158
|
-
new_args=()
|
|
159
|
-
skip=0
|
|
160
|
-
for a in "$@"; do
|
|
161
|
-
if [[ "$skip" == "1" ]]; then skip=0; continue; fi
|
|
162
|
-
if [[ "$a" == "--as-user" ]]; then skip=1; continue; fi
|
|
163
|
-
new_args+=("$a")
|
|
164
|
-
done
|
|
165
|
-
# Resolve our own path (can't rely on $0 after sudo changes PATH).
|
|
166
|
-
SELF="$(command -v ework-aio 2>/dev/null || true)"
|
|
167
|
-
[[ -n "$SELF" ]] || SELF="${BASH_SOURCE[0]:-$0}"
|
|
168
|
-
exec sudo -u "$AS_USER" --login -- "$SELF" "${new_args[@]}"
|
|
169
|
-
fi
|
|
170
|
-
if [[ "$ALLOW_ROOT" != "1" ]]; then
|
|
171
|
-
cat >&2 <<EOF
|
|
172
|
-
${c_red}ework-aio install refuses to run as root by default.${c_reset}
|
|
173
|
-
|
|
174
|
-
Why this matters:
|
|
175
|
-
- Data goes under /root/.local/share/ework-aio (unreadable by other users)
|
|
176
|
-
- opencode is searched in root's PATH (usually not installed there)
|
|
177
|
-
- npm packages install to system-wide prefix owned by root
|
|
178
|
-
- systemd units bake root-only paths that other users can't run
|
|
179
|
-
|
|
180
|
-
${c_bold}Option A (recommended):${c_reset} run as a regular user.
|
|
181
|
-
npm config set prefix '~/.local' # one-time, makes -g user-writable
|
|
182
|
-
npm install -g ework-aio # no sudo needed
|
|
183
|
-
ework-aio install # uses --user systemd scope
|
|
184
|
-
|
|
185
|
-
${c_bold}Option B:${c_reset} install with sudo but target a regular user.
|
|
186
|
-
sudo ework-aio install --as-user $(logname 2>/dev/null || echo '<your-user>')
|
|
187
|
-
# All data + opencode + npm resolved under that user's HOME.
|
|
188
|
-
# systemd --user scope (linger auto-enabled for the target user).
|
|
189
|
-
|
|
190
|
-
${c_bold}Option C:${c_reset} really install as root (you've set up root's env).
|
|
191
|
-
sudo ework-aio install --allow-root
|
|
192
|
-
EOF
|
|
193
|
-
exit 1
|
|
194
|
-
fi
|
|
195
|
-
warn "Running install as root with --allow-root — data will live under /root."
|
|
196
|
-
fi
|
|
197
|
-
|
|
198
|
-
# Resolve scope: default by uid
|
|
199
|
-
if [[ "$SCOPENAME" == "--user" && "${EUID:-$(id -u)}" == "0" ]]; then
|
|
200
|
-
SCOPENAME="--system"
|
|
201
|
-
fi
|
|
202
|
-
|
|
203
|
-
# ─── Ensure --user mode can actually talk to systemd ─────────────────────────
|
|
204
|
-
# When invoked from ssh/cron/non-PAM-login shells, XDG_RUNTIME_DIR is often
|
|
205
|
-
# empty and systemctl --user fails with "Failed to connect to bus" or — worse —
|
|
206
|
-
# "Unit not found" (when a stale bus from another session responds but doesn't
|
|
207
|
-
# know about installed units). Auto-export the runtime dir + bus socket if we
|
|
208
|
-
# can find them. No-op for --system scope.
|
|
209
|
-
ensure_user_session() {
|
|
210
|
-
[[ "$USE_SYSTEMD" == "1" ]] || return 0
|
|
211
|
-
[[ "$SCOPENAME" == "--user" ]] || return 0
|
|
212
|
-
local uid
|
|
213
|
-
uid="$(id -u)"
|
|
214
|
-
local rundir="/run/user/$uid"
|
|
215
|
-
|
|
216
|
-
if [[ -z "${XDG_RUNTIME_DIR:-}" && -d "$rundir" ]]; then
|
|
217
|
-
export XDG_RUNTIME_DIR="$rundir"
|
|
218
|
-
fi
|
|
219
|
-
# ${XDG_RUNTIME_DIR:-} not bare ${XDG_RUNTIME_DIR}: the first if above only
|
|
220
|
-
# exports when /run/user/$uid exists. On hosts without linger / without any
|
|
221
|
-
# login session, /run/user/$uid is absent, XDG_RUNTIME_DIR stays unset, and
|
|
222
|
-
# `set -u` would kill install here before the systemd_reachable() fallback
|
|
223
|
-
# can route to PID-file mode.
|
|
224
|
-
if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" && -S "${XDG_RUNTIME_DIR:-}/bus" ]]; then
|
|
225
|
-
export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
|
|
226
|
-
fi
|
|
227
|
-
|
|
228
|
-
# Final sanity: can we reach systemd --user at all?
|
|
229
|
-
if ! systemctl --user is-system-running >/dev/null 2>&1 \
|
|
230
|
-
&& ! systemctl --user list-units >/dev/null 2>&1; then
|
|
231
|
-
warn "systemctl --user cannot reach the user session bus."
|
|
232
|
-
warn " XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-<empty>}"
|
|
233
|
-
warn " DBUS_SESSION_BUS_ADDRESS=${DBUS_SESSION_BUS_ADDRESS:-<empty>}"
|
|
234
|
-
warn "Likely causes: ssh without PAM, no linger, or session never started."
|
|
235
|
-
warn "Fix: 'sudo loginctl enable-linger \$USER' then relogin; or reinstall with --system."
|
|
236
|
-
return 1
|
|
237
|
-
fi
|
|
238
|
-
return 0
|
|
239
|
-
}
|
|
240
|
-
ensure_user_session || true # warn but don't hard-fail — let later steps report specifics
|
|
241
|
-
|
|
242
|
-
# ─── Post-parse: detect `install systemd` sub-variant ──────────────────────
|
|
243
|
-
# `install systemd` (or `systemd install`, or `install ... systemd`) opts in
|
|
244
|
-
# to writing+enabling systemd units. Without this token, install is pure
|
|
245
|
-
# PID-file mode (no systemctl calls, no unit files, no linger prompt).
|
|
246
|
-
if [[ "$MODE" == "install" ]]; then
|
|
247
|
-
new_mode_args=()
|
|
248
|
-
for a in "${MODE_ARGS[@]:-}"; do
|
|
249
|
-
[[ -z "$a" ]] && continue
|
|
250
|
-
if [[ "$a" == "systemd" ]]; then
|
|
251
|
-
USE_SYSTEMD=1
|
|
252
|
-
else
|
|
253
|
-
new_mode_args+=("$a")
|
|
254
|
-
fi
|
|
255
|
-
done
|
|
256
|
-
MODE_ARGS=("${new_mode_args[@]:-}")
|
|
257
|
-
fi
|
|
258
|
-
|
|
259
|
-
# ─── Pre-flight: command dependencies ───────────────────────────────────────
|
|
260
|
-
need_cmd() {
|
|
261
|
-
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1. $2"
|
|
262
|
-
}
|
|
263
|
-
need_cmd awk "Should be present on any Linux."
|
|
264
|
-
|
|
265
|
-
# systemctl only required when user opts into systemd variant. Default
|
|
266
|
-
# install path is PID-file only and works on hosts without systemd.
|
|
267
|
-
if [[ "$MODE" == "install" ]]; then
|
|
268
|
-
[[ "$USE_SYSTEMD" == "1" ]] && need_cmd systemctl "Install with 'systemd' subcommand requires systemctl. Drop 'systemd' to install in PID-file mode."
|
|
269
|
-
need_cmd bun "Install from https://bun.sh"
|
|
270
|
-
need_cmd npm "Install Node.js or Bun (ships npm)"
|
|
271
|
-
need_cmd opencode "Install from https://opencode.ai"
|
|
272
|
-
need_cmd openssl "Install the openssl package."
|
|
273
|
-
need_cmd curl "Install curl."
|
|
274
|
-
need_cmd jq "Install jq (for opencode.json merge)."
|
|
275
|
-
fi
|
|
276
|
-
|
|
277
|
-
# EWORK_AIO_BIN: always resolvable (used by status/logs/config/uninstall
|
|
278
|
-
# to delegate to PID-file mode operations when systemd is unreachable).
|
|
279
|
-
EWORK_AIO_BIN="$(command -v ework-aio 2>/dev/null || true)"
|
|
280
|
-
[[ -n "$EWORK_AIO_BIN" ]] || EWORK_AIO_BIN="$(npm root -g 2>/dev/null)/ework-aio/bin/ework-aio"
|
|
281
|
-
|
|
282
|
-
# systemd_reachable: 0 if `systemctl $SCOPENAME` can talk to systemd, 1 if not.
|
|
283
|
-
# Used by status/logs/config/uninstall to decide whether to use systemd
|
|
284
|
-
# (preferred) or fall back to PID-file mode via the ework-aio dispatcher.
|
|
285
|
-
# Hosts where this returns 1: containers without systemd PID 1, polkit
|
|
286
|
-
# redirects under sudo that drop the bus, hosts where the user isn't
|
|
287
|
-
# permissioned for the system bus.
|
|
288
|
-
systemd_reachable() {
|
|
289
|
-
command -v systemctl >/dev/null 2>&1 || return 1
|
|
290
|
-
systemctl "$SCOPENAME" is-system-running >/dev/null 2>&1 \
|
|
291
|
-
|| systemctl "$SCOPENAME" list-units --no-pager >/dev/null 2>&1
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
# Verify the 3 npm packages are reachable. If not, try to install them now.
|
|
295
|
-
ensure_pkg() {
|
|
296
|
-
local pkg="$1"
|
|
297
|
-
if ! command -v "$pkg" >/dev/null 2>&1 \
|
|
298
|
-
&& ! [[ -d "$(npm root -g 2>/dev/null)/$pkg" ]]; then
|
|
299
|
-
log "npm package '$pkg' not detected; installing globally now..."
|
|
300
|
-
npm install -g "$pkg" || die "npm install -g $pkg failed"
|
|
301
|
-
fi
|
|
302
|
-
}
|
|
303
|
-
case "$MODE" in
|
|
304
|
-
install)
|
|
305
|
-
# Pre-clean stale npm rename temp-dirs from any previous failed install.
|
|
306
|
-
# npm's atomic-install pattern renames the existing package dir to a
|
|
307
|
-
# `.<pkg>-<rand>` sibling before extracting the new tarball; if a prior
|
|
308
|
-
# install crashed mid-flight, that temp dir stays behind and the NEXT
|
|
309
|
-
# install fails with ENOTEMPTY on the rename. Symptoms:
|
|
310
|
-
# npm error code ENOTEMPTY
|
|
311
|
-
# npm error syscall rename
|
|
312
|
-
# npm error path .../node_modules/ework-web
|
|
313
|
-
# npm error dest .../node_modules/.ework-web-Cx2Tkt83
|
|
314
|
-
clean_npm_stale_dirs() {
|
|
315
|
-
local npm_root
|
|
316
|
-
npm_root="$(npm root -g 2>/dev/null)" || return 0
|
|
317
|
-
[[ -d "$npm_root" ]] || return 0
|
|
318
|
-
# Stale temp patterns: .ework-web-XXXX, .ework-daemon-XXXX, .opencode-ework-XXXX,
|
|
319
|
-
# .ework-aio-XXXX, plus the .Trash suffix some npm versions use.
|
|
320
|
-
local found=()
|
|
321
|
-
while IFS= read -r p; do
|
|
322
|
-
[[ -n "$p" ]] && found+=("$p")
|
|
323
|
-
done < <(find "$npm_root" -maxdepth 1 \( \
|
|
324
|
-
-name '.ework-web-*' -o \
|
|
325
|
-
-name '.ework-daemon-*' -o \
|
|
326
|
-
-name '.ework-aio-*' -o \
|
|
327
|
-
-name '.opencode-ework-*' -o \
|
|
328
|
-
-name 'ework-*.Trash*' -o \
|
|
329
|
-
-name 'opencode-ework.Trash*' \) 2>/dev/null || true)
|
|
330
|
-
if [[ ${#found[@]} -gt 0 ]]; then
|
|
331
|
-
log "Found ${#found[@]} stale npm temp dir(s) under $npm_root from a previous failed install:"
|
|
332
|
-
printf ' %s\n' "${found[@]}"
|
|
333
|
-
log "Removing..."
|
|
334
|
-
local d
|
|
335
|
-
for d in "${found[@]}"; do
|
|
336
|
-
rm -rf "$d" 2>/dev/null || warn "could not remove $d (try: sudo rm -rf $d)"
|
|
337
|
-
done
|
|
338
|
-
fi
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
npm_install_global() {
|
|
342
|
-
# One retry after a cleanup pass — covers the ENOTEMPTY case where npm
|
|
343
|
-
# itself produced a fresh temp dir during this same failed run.
|
|
344
|
-
if ! npm install -g "$1@latest"; then
|
|
345
|
-
warn "npm install -g $1@latest failed; cleaning stale temp dirs and retrying once..."
|
|
346
|
-
clean_npm_stale_dirs
|
|
347
|
-
npm install -g "$1@latest"
|
|
348
|
-
fi
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
clean_npm_stale_dirs
|
|
352
|
-
log "Installing/updating npm packages globally..."
|
|
353
|
-
# Install each as a top-level global so bins are linked to PATH.
|
|
354
|
-
# (npm v9 doesn't hoist nested deps' bins from `npm i -g ework-aio`.)
|
|
355
|
-
# Always run `npm install -g` (not gated on presence) so re-runs pick up new versions.
|
|
356
|
-
for pkg in ework-web ework-daemon opencode-ework ework-aio; do
|
|
357
|
-
npm_install_global "$pkg" || die "npm install -g $pkg@latest failed (even after cleanup retry; manual fix: sudo rm -rf $(npm root -g)/.$pkg-* $(npm root -g)/$pkg.Trash*)"
|
|
358
|
-
done
|
|
359
|
-
ok "npm packages ready"
|
|
360
|
-
|
|
361
|
-
# Absolute paths to the bin shims (resolved once, baked into systemd units)
|
|
362
|
-
EWORK_WEB_BIN="$(command -v ework-web 2>/dev/null || true)"
|
|
363
|
-
EWORK_DAEMON_BIN="$(command -v ework-daemon-server 2>/dev/null || true)"
|
|
364
|
-
[[ -n "$EWORK_WEB_BIN" ]] || EWORK_WEB_BIN="$(npm root -g)/ework-web/bin/ework-web.js"
|
|
365
|
-
[[ -n "$EWORK_DAEMON_BIN" ]] || EWORK_DAEMON_BIN="$(npm root -g)/ework-daemon/bin/ework-daemon-server.js"
|
|
366
|
-
[[ -x "$EWORK_WEB_BIN" || -f "$EWORK_WEB_BIN" ]] || die "ework-web bin not found at $EWORK_WEB_BIN"
|
|
367
|
-
[[ -x "$EWORK_DAEMON_BIN" || -f "$EWORK_DAEMON_BIN" ]] || die "ework-daemon-server bin not found at $EWORK_DAEMON_BIN"
|
|
368
|
-
export EWORK_WEB_BIN EWORK_DAEMON_BIN
|
|
369
|
-
;;
|
|
370
|
-
esac
|
|
371
|
-
|
|
372
|
-
# ─── Paths ──────────────────────────────────────────────────────────────────
|
|
373
|
-
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
|
|
374
|
-
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
375
|
-
DATA_DIR="${DATA_DIR:-$XDG_DATA_HOME/ework-aio}"
|
|
376
|
-
WEB_DATA_DIR="$DATA_DIR/ework-web"
|
|
377
|
-
DAEMON_DATA_DIR="$DATA_DIR/ework-daemon"
|
|
378
|
-
WEB_ENV="$WEB_DATA_DIR/.env"
|
|
379
|
-
DAEMON_ENV="$DAEMON_DATA_DIR/.env"
|
|
380
|
-
BOT_TOKEN_FILE="$DATA_DIR/.bot-token"
|
|
381
|
-
OPENCODE_CFG="$XDG_CONFIG_HOME/opencode/opencode.json"
|
|
382
|
-
|
|
383
|
-
# systemd unit dir
|
|
384
|
-
if [[ "$SCOPENAME" == "--user" ]]; then
|
|
385
|
-
UNIT_DIR="$XDG_CONFIG_HOME/systemd/user"
|
|
386
|
-
else
|
|
387
|
-
UNIT_DIR="/etc/systemd/system"
|
|
388
|
-
fi
|
|
389
|
-
mkdir -p "$UNIT_DIR"
|
|
390
|
-
|
|
391
|
-
# ─── Helpers ────────────────────────────────────────────────────────────────
|
|
392
|
-
gen_token() { openssl rand -hex "${1:-24}"; }
|
|
393
|
-
|
|
394
|
-
write_web_env() {
|
|
395
|
-
mkdir -p "$(dirname "$WEB_ENV")"
|
|
396
|
-
if [[ -f "$WEB_ENV" ]]; then
|
|
397
|
-
log "Preserving existing $WEB_ENV"
|
|
398
|
-
# Older installs (pre-0.1.16) may have a .env that predates some keys
|
|
399
|
-
# ework-web's Zod schema now requires (WORK_COOKIE_SECRET in particular).
|
|
400
|
-
# Fill any missing required keys with fresh defaults so the preserved
|
|
401
|
-
# .env doesn't crash the next start. User-set keys are never overwritten.
|
|
402
|
-
ensure_web_env_keys
|
|
403
|
-
return
|
|
404
|
-
fi
|
|
405
|
-
local tok secret webhook
|
|
406
|
-
tok=$(gen_token 20)
|
|
407
|
-
secret=$(gen_token 24)
|
|
408
|
-
webhook=$(gen_token 20)
|
|
409
|
-
cat > "$WEB_ENV" <<EOF
|
|
410
|
-
# Generated by ework-aio at $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
411
|
-
WORK_PORT=$WORK_PORT
|
|
412
|
-
WORK_HOST=127.0.0.1
|
|
413
|
-
WORK_TOKEN=$tok
|
|
414
|
-
WORK_COOKIE_SECRET=$secret
|
|
415
|
-
WORK_OPERATOR_LOGIN=$USER
|
|
416
|
-
WORK_WRITES_ENABLED=true
|
|
417
|
-
WORK_DB_PATH=$WEB_DATA_DIR/ework.db
|
|
418
|
-
WORK_ATTACHMENT_ROOT=$WEB_DATA_DIR/attachments
|
|
419
|
-
WORK_FILE_ROOTS=/tmp,$DATA_DIR
|
|
420
|
-
WORK_DAEMON_BOT_LOGIN=$BOT_NAME
|
|
421
|
-
WORK_DAEMON_WEBHOOK_URL=http://127.0.0.1:$DAEMON_PORT
|
|
422
|
-
WORK_DAEMON_WEBHOOK_SECRET=$webhook
|
|
423
|
-
EOF
|
|
424
|
-
chmod 600 "$WEB_ENV"
|
|
425
|
-
ok "Wrote $WEB_ENV"
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
# ensure_env_key: append KEY=value to $file iff KEY is not already present.
|
|
429
|
-
# Used to forward-fill schema-required keys on preserved .env files without
|
|
430
|
-
# clobbering any user-set values.
|
|
431
|
-
ensure_env_key() {
|
|
432
|
-
local file="$1" key="$2" val="$3"
|
|
433
|
-
if ! grep -qE "^${key}=" "$file" 2>/dev/null; then
|
|
434
|
-
printf '%s=%s\n' "$key" "$val" >> "$file"
|
|
435
|
-
log " + added missing key: $key"
|
|
436
|
-
fi
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
ensure_web_env_keys() {
|
|
440
|
-
# Only fills keys that are missing; never overwrites existing values.
|
|
441
|
-
# Tokens / secrets get fresh random values (safe because if they were
|
|
442
|
-
# missing, web couldn't have started, so nothing depends on them yet).
|
|
443
|
-
local tok secret webhook
|
|
444
|
-
tok=$(gen_token 20)
|
|
445
|
-
secret=$(gen_token 24)
|
|
446
|
-
webhook=$(gen_token 20)
|
|
447
|
-
ensure_env_key "$WEB_ENV" WORK_PORT "$WORK_PORT"
|
|
448
|
-
ensure_env_key "$WEB_ENV" WORK_HOST "127.0.0.1"
|
|
449
|
-
ensure_env_key "$WEB_ENV" WORK_TOKEN "$tok"
|
|
450
|
-
ensure_env_key "$WEB_ENV" WORK_COOKIE_SECRET "$secret"
|
|
451
|
-
ensure_env_key "$WEB_ENV" WORK_OPERATOR_LOGIN "$USER"
|
|
452
|
-
ensure_env_key "$WEB_ENV" WORK_WRITES_ENABLED "true"
|
|
453
|
-
ensure_env_key "$WEB_ENV" WORK_DB_PATH "$WEB_DATA_DIR/ework.db"
|
|
454
|
-
ensure_env_key "$WEB_ENV" WORK_ATTACHMENT_ROOT "$WEB_DATA_DIR/attachments"
|
|
455
|
-
ensure_env_key "$WEB_ENV" WORK_FILE_ROOTS "/tmp,$DATA_DIR"
|
|
456
|
-
ensure_env_key "$WEB_ENV" WORK_DAEMON_BOT_LOGIN "$BOT_NAME"
|
|
457
|
-
ensure_env_key "$WEB_ENV" WORK_DAEMON_WEBHOOK_URL "http://127.0.0.1:$DAEMON_PORT"
|
|
458
|
-
ensure_env_key "$WEB_ENV" WORK_DAEMON_WEBHOOK_SECRET "$webhook"
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
write_daemon_env() {
|
|
462
|
-
mkdir -p "$(dirname "$DAEMON_ENV")"
|
|
463
|
-
local bot_token="$1"
|
|
464
|
-
local webhook
|
|
465
|
-
# Reuse webhook secret from web env so daemon's signature matches web's verify
|
|
466
|
-
webhook=$(awk -F= '/^WORK_DAEMON_WEBHOOK_SECRET=/{print $2}' "$WEB_ENV" 2>/dev/null || true)
|
|
467
|
-
[[ -n "$webhook" ]] || webhook=$(gen_token 20)
|
|
468
|
-
if [[ -f "$DAEMON_ENV" ]]; then
|
|
469
|
-
log "Preserving existing $DAEMON_ENV"
|
|
470
|
-
# Same forward-fill pattern as web: required keys get filled if missing.
|
|
471
|
-
ensure_env_key "$DAEMON_ENV" DAEMON_ENV "production"
|
|
472
|
-
ensure_env_key "$DAEMON_ENV" DAEMON_PORT "$DAEMON_PORT"
|
|
473
|
-
ensure_env_key "$DAEMON_ENV" DAEMON_HOST "127.0.0.1"
|
|
474
|
-
ensure_env_key "$DAEMON_ENV" DAEMON_DB_PATH "$DAEMON_DATA_DIR/ework-daemon.db"
|
|
475
|
-
ensure_env_key "$DAEMON_ENV" GITEA_URL "http://127.0.0.1:$WORK_PORT"
|
|
476
|
-
ensure_env_key "$DAEMON_ENV" GITEA_TOKEN "$bot_token"
|
|
477
|
-
ensure_env_key "$DAEMON_ENV" GITEA_WEBHOOK_SECRET "$webhook"
|
|
478
|
-
ensure_env_key "$DAEMON_ENV" BOT_USERNAME "$BOT_NAME"
|
|
479
|
-
ensure_env_key "$DAEMON_ENV" BOT_TOKEN "$bot_token"
|
|
480
|
-
ensure_env_key "$DAEMON_ENV" OPENCODE_BINARY "$(command -v opencode)"
|
|
481
|
-
ensure_env_key "$DAEMON_ENV" OPENCODE_BASE_WORKDIR "$DATA_DIR/opencode-workdir"
|
|
482
|
-
return
|
|
483
|
-
fi
|
|
484
|
-
cat > "$DAEMON_ENV" <<EOF
|
|
485
|
-
# Generated by ework-aio at $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
486
|
-
DAEMON_ENV=production
|
|
487
|
-
DAEMON_PORT=$DAEMON_PORT
|
|
488
|
-
DAEMON_HOST=127.0.0.1
|
|
489
|
-
DAEMON_DB_PATH=$DAEMON_DATA_DIR/ework-daemon.db
|
|
490
|
-
GITEA_URL=http://127.0.0.1:$WORK_PORT
|
|
491
|
-
GITEA_TOKEN=$bot_token
|
|
492
|
-
GITEA_WEBHOOK_SECRET=$webhook
|
|
493
|
-
BOT_USERNAME=$BOT_NAME
|
|
494
|
-
BOT_TOKEN=$bot_token
|
|
495
|
-
OPENCODE_BINARY=$(command -v opencode)
|
|
496
|
-
OPENCODE_BASE_WORKDIR=$DATA_DIR/opencode-workdir
|
|
497
|
-
EOF
|
|
498
|
-
chmod 600 "$DAEMON_ENV"
|
|
499
|
-
ok "Wrote $DAEMON_ENV"
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
write_unit_file() {
|
|
503
|
-
# $1=name $2=description $3=execstart $4=workdir $5=envfile $6=unit-file-path
|
|
504
|
-
local name="$1" desc="$2" execstart="$3" workdir="$4" envfile="$5" out="$6"
|
|
505
|
-
local extra_env="${7:-}"
|
|
506
|
-
cat > "$out" <<EOF
|
|
507
|
-
[Unit]
|
|
508
|
-
Description=$desc
|
|
509
|
-
After=network-online.target
|
|
510
|
-
Wants=network-online.target
|
|
511
|
-
|
|
512
|
-
[Service]
|
|
513
|
-
Type=simple
|
|
514
|
-
WorkingDirectory=$workdir
|
|
515
|
-
ExecStart=$execstart
|
|
516
|
-
Restart=on-failure
|
|
517
|
-
RestartSec=5
|
|
518
|
-
KillMode=process
|
|
519
|
-
EnvironmentFile=$envfile
|
|
520
|
-
Environment="PATH=/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin:$HOME/.bun/bin"
|
|
521
|
-
Environment="XDG_DATA_HOME=$XDG_DATA_HOME"
|
|
522
|
-
Environment="XDG_CONFIG_HOME=$XDG_CONFIG_HOME"
|
|
523
|
-
Environment="HOME=$HOME"$extra_env
|
|
524
|
-
StandardOutput=journal
|
|
525
|
-
StandardError=journal
|
|
526
|
-
SyslogIdentifier=$name
|
|
527
|
-
|
|
528
|
-
[Install]
|
|
529
|
-
${SCOPENAME:+WantedBy=default.target}
|
|
530
|
-
EOF
|
|
531
|
-
# If system-level, WantedBy is multi-user.target
|
|
532
|
-
if [[ "$SCOPENAME" == "--system" ]]; then
|
|
533
|
-
sed -i 's/^WantedBy=default.target/WantedBy=multi-user.target/' "$out"
|
|
534
|
-
fi
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
# ctl wraps `systemctl $SCOPENAME` with three hardenings over the bare command:
|
|
538
|
-
# 1. If output contains "Unit ... not found", retry once after daemon-reload
|
|
539
|
-
# — fresh user sessions (no linger, post-ssh-reconnect) often have a stale
|
|
540
|
-
# unit cache and this is the standard fix.
|
|
541
|
-
# 2. If output mentions any D-Bus / user-bus connection failure, print an
|
|
542
|
-
# actionable hint. The user-bus daemon emits several variants across
|
|
543
|
-
# hosts and bash versions ("Failed to connect to bus", "No medium found",
|
|
544
|
-
# "Failed to get D-Bus connection: Operation not permitted",
|
|
545
|
-
# "Call to org.freedesktop.DBus ... failed"); we match the common stem
|
|
546
|
-
# "bus" / "D-Bus" so a new variant can't slip past.
|
|
547
|
-
# 3. ALWAYS return systemctl's real exit code. Previously the trailing
|
|
548
|
-
# `printf` masked the failure (returning 0) when no pattern matched,
|
|
549
|
-
# which made every caller's `|| fallback` silently not fire.
|
|
550
|
-
ctl() {
|
|
551
|
-
local out rc
|
|
552
|
-
out="$(systemctl "$SCOPENAME" "$@" 2>&1)"
|
|
553
|
-
rc=$?
|
|
554
|
-
if [[ $rc -ne 0 ]]; then
|
|
555
|
-
if [[ "$out" == *([Bb]us|D-Bus|No medium found)* ]]; then
|
|
556
|
-
cat >&2 <<EOF
|
|
557
|
-
${c_red}systemctl $SCOPENAME failed to reach the user bus.${c_reset}
|
|
558
|
-
$out
|
|
559
|
-
Hint: run from a logged-in session, or:
|
|
560
|
-
export XDG_RUNTIME_DIR=/run/user/$(id -u)
|
|
561
|
-
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
|
|
562
|
-
or reinstall with: sudo ework-aio install --system
|
|
563
|
-
EOF
|
|
564
|
-
return $rc
|
|
565
|
-
fi
|
|
566
|
-
if [[ "$out" == *"not found"* ]]; then
|
|
567
|
-
systemctl "$SCOPENAME" daemon-reload >/dev/null 2>&1 || true
|
|
568
|
-
out="$(systemctl "$SCOPENAME" "$@" 2>&1)"
|
|
569
|
-
rc=$?
|
|
570
|
-
if [[ $rc -ne 0 ]]; then
|
|
571
|
-
cat >&2 <<EOF
|
|
572
|
-
${c_red}systemctl $SCOPENAME $*: unit not found after daemon-reload.${c_reset}
|
|
573
|
-
The unit file may be missing from $UNIT_DIR. Re-run:
|
|
574
|
-
ework-aio install --no-start # rewrites units, preserves .env/data
|
|
575
|
-
EOF
|
|
576
|
-
return $rc
|
|
577
|
-
fi
|
|
578
|
-
fi
|
|
579
|
-
fi
|
|
580
|
-
printf '%s\n' "$out"
|
|
581
|
-
return "$rc"
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
# ─── Config mode: read/change runtime .env keys ─────────────────────────────
|
|
585
|
-
# Each entry: KEY|SERVICE|DESCRIPTION. Only keys in this list are settable via
|
|
586
|
-
# `config set`. Secrets (WORK_TOKEN, *_WEBHOOK_SECRET, BOT_TOKEN), DB paths,
|
|
587
|
-
# and the web<->daemon contract (GITEA_URL/TOKEN, WORK_DAEMON_WEBHOOK_*) are
|
|
588
|
-
# deliberately excluded — changing them by hand breaks the install.
|
|
589
|
-
SETTABLE_KEYS=(
|
|
590
|
-
"WORK_PORT|web|ework-web listen port (default 3002)"
|
|
591
|
-
"WORK_HOST|web|ework-web bind address (default 127.0.0.1; use 0.0.0.0 for LAN)"
|
|
592
|
-
"WORK_OPERATOR_LOGIN|web|login auto-promoted to admin"
|
|
593
|
-
"WORK_OPENCODE_BIN|web|opencode binary path used by ework-web"
|
|
594
|
-
"WORK_TRANSLATE_URL|web|OpenAI-compat /v1/chat/completions endpoint for translate"
|
|
595
|
-
"WORK_TRANSLATE_MODEL|web|translate model name"
|
|
596
|
-
"WORK_TTS_SPEED|web|TTS playback rate (default 1.0)"
|
|
597
|
-
"WORK_FILE_ROOTS|web|comma-separated file-viewer roots"
|
|
598
|
-
"WORK_COMMENT_SORT|web|comment sort order: desc|asc"
|
|
599
|
-
"DAEMON_PORT|daemon|ework-daemon listen port (default 3101)"
|
|
600
|
-
"DAEMON_HOST|daemon|ework-daemon bind address (default 127.0.0.1)"
|
|
601
|
-
"OPENCODE_BINARY|daemon|opencode binary path"
|
|
602
|
-
"OPENCODE_BASE_WORKDIR|daemon|opencode working directory base"
|
|
603
|
-
"COMPLETION_CHECK_API_KEY|daemon|completion-check API key"
|
|
604
|
-
"COMPLETION_CHECK_BASE_URL|daemon|completion-check API base URL"
|
|
605
|
-
"COMPLETION_CHECK_MODEL|daemon|completion-check model name"
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
key_service() {
|
|
609
|
-
local k="$1" e
|
|
610
|
-
for e in "${SETTABLE_KEYS[@]}"; do
|
|
611
|
-
[[ "$e" == "$k|"* ]] || continue
|
|
612
|
-
local rest="${e#*|}"
|
|
613
|
-
echo "${rest%%|*}"
|
|
614
|
-
return
|
|
615
|
-
done
|
|
616
|
-
}
|
|
617
|
-
key_default() {
|
|
618
|
-
local k="$1" e
|
|
619
|
-
for e in "${SETTABLE_KEYS[@]}"; do
|
|
620
|
-
[[ "$e" == "$k|"* ]] || continue
|
|
621
|
-
local rest="${e#*|}"
|
|
622
|
-
echo "${rest#*|}"
|
|
623
|
-
return
|
|
624
|
-
done
|
|
625
|
-
}
|
|
626
|
-
key_settable() { local k="$1"; local e; for e in "${SETTABLE_KEYS[@]}"; do [[ "$e" == "$k|"* ]] && return 0; done; return 1; }
|
|
627
|
-
|
|
628
|
-
env_file_for() {
|
|
629
|
-
case "$1" in
|
|
630
|
-
web) echo "$WEB_ENV" ;;
|
|
631
|
-
daemon) echo "$DAEMON_ENV" ;;
|
|
632
|
-
*) return 1 ;;
|
|
633
|
-
esac
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
env_get() {
|
|
637
|
-
local file="$1" key="$2"
|
|
638
|
-
[[ -f "$file" ]] || return 1
|
|
639
|
-
awk -F= -v k="$key" '$1==k {sub(/^[^=]*=/,""); print; found=1} END{exit !found}' "$file"
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
env_set() {
|
|
643
|
-
local file="$1" key="$2" val="$3"
|
|
644
|
-
[[ -f "$file" ]] || { mkdir -p "$(dirname "$file")"; touch "$file"; chmod 600 "$file"; }
|
|
645
|
-
if grep -q "^${key}=" "$file"; then
|
|
646
|
-
sed -i "s|^${key}=.*|${key}=${val}|" "$file"
|
|
647
|
-
else
|
|
648
|
-
printf '%s=%s\n' "$key" "$val" >> "$file"
|
|
649
|
-
fi
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
service_for_key() {
|
|
653
|
-
case "$1" in
|
|
654
|
-
WORK_PORT)
|
|
655
|
-
echo "both" ;;
|
|
656
|
-
DAEMON_PORT)
|
|
657
|
-
echo "both" ;;
|
|
658
|
-
*)
|
|
659
|
-
echo "$(key_service "$1")" ;;
|
|
660
|
-
esac
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
restart_service() {
|
|
664
|
-
local svc="$1"
|
|
665
|
-
local rc=0
|
|
666
|
-
if ! systemd_reachable; then
|
|
667
|
-
log "systemd not reachable — using PID-file mode restart"
|
|
668
|
-
if [[ "$svc" == "both" ]]; then
|
|
669
|
-
"$EWORK_AIO_BIN" restart both && ok "Restarted ework-web + ework-daemon (PID-file mode)" || rc=$?
|
|
670
|
-
else
|
|
671
|
-
"$EWORK_AIO_BIN" restart "$svc" && ok "Restarted ework-$svc (PID-file mode)" || rc=$?
|
|
672
|
-
fi
|
|
673
|
-
if [[ $rc -ne 0 ]]; then
|
|
674
|
-
warn "PID-file restart of '$svc' failed (exit $rc)."
|
|
675
|
-
warn "The .env changes are saved; run 'ework-aio restart $svc' manually."
|
|
676
|
-
fi
|
|
677
|
-
return $rc
|
|
678
|
-
fi
|
|
679
|
-
case "$svc" in
|
|
680
|
-
web) ctl restart ework-web.service && ok "Restarted ework-web" || rc=$? ;;
|
|
681
|
-
daemon) ctl restart ework-daemon.service && ok "Restarted ework-daemon" || rc=$? ;;
|
|
682
|
-
both)
|
|
683
|
-
ctl restart ework-web.service ework-daemon.service \
|
|
684
|
-
&& ok "Restarted ework-web + ework-daemon" || rc=$?
|
|
685
|
-
;;
|
|
686
|
-
*) return 1 ;;
|
|
687
|
-
esac
|
|
688
|
-
if [[ $rc -ne 0 ]]; then
|
|
689
|
-
warn "restart of '$svc' failed (exit $rc)."
|
|
690
|
-
warn "The .env changes are saved; services still need to be reloaded."
|
|
691
|
-
warn "Recovery: 'ework-aio config restart $svc' from a logged-in shell,"
|
|
692
|
-
warn "or 'sudo ework-aio install --system' to escape user-bus limitations."
|
|
693
|
-
return $rc
|
|
694
|
-
fi
|
|
695
|
-
return 0
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
config_help() {
|
|
699
|
-
cat <<'EOF'
|
|
700
|
-
ework-aio config <subcommand>
|
|
701
|
-
|
|
702
|
-
Subcommands:
|
|
703
|
-
list List all settable keys + current values
|
|
704
|
-
get <KEY> Print current value of one key
|
|
705
|
-
set <KEY> <VALUE> Set a key in .env, then restart the affected
|
|
706
|
-
service (unless --no-restart is given)
|
|
707
|
-
restart <web|daemon|both> Restart one or both services
|
|
708
|
-
|
|
709
|
-
Examples:
|
|
710
|
-
ework-aio config list
|
|
711
|
-
ework-aio config get WORK_PORT
|
|
712
|
-
ework-aio config set WORK_PORT 8080
|
|
713
|
-
ework-aio config set WORK_TRANSLATE_URL http://127.0.0.1:8000/v1 --no-restart
|
|
714
|
-
ework-aio config restart both
|
|
715
|
-
|
|
716
|
-
Note: changing WORK_PORT or DAEMON_PORT also rewrites the cross-link the other
|
|
717
|
-
service uses (GITEA_URL on daemon side, WORK_DAEMON_WEBHOOK_URL on web side),
|
|
718
|
-
and restarts both. Secrets and DB paths are not settable here — rerun
|
|
719
|
-
`ework-aio install` (with `rm .env` first if you need new tokens).
|
|
720
|
-
EOF
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
config_list() {
|
|
724
|
-
hr; log "Settable config keys"; hr
|
|
725
|
-
printf ' %-28s %-8s %s\n' "KEY" "SERVICE" "VALUE"
|
|
726
|
-
local e k svc val env_file
|
|
727
|
-
for e in "${SETTABLE_KEYS[@]}"; do
|
|
728
|
-
k="${e%%|*}"
|
|
729
|
-
svc="$(key_service "$k")"
|
|
730
|
-
env_file="$(env_file_for "$svc")"
|
|
731
|
-
val="$(env_get "$env_file" "$k" 2>/dev/null || echo '')"
|
|
732
|
-
[[ -z "$val" ]] && val="(unset)"
|
|
733
|
-
printf ' %-28s %-8s %s\n' "$k" "$svc" "$val"
|
|
734
|
-
done
|
|
735
|
-
hr
|
|
736
|
-
printf 'Use %sconfig set <KEY> <VALUE>%s to change a key.\n' "$c_bold" "$c_reset"
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
config_get() {
|
|
740
|
-
local k="$1"
|
|
741
|
-
[[ -n "$k" ]] || die "Usage: ework-aio config get <KEY>"
|
|
742
|
-
key_settable "$k" || die "Key '$k' is not settable. Run 'ework-aio config list' for the allow-list."
|
|
743
|
-
local svc env_file val
|
|
744
|
-
svc="$(key_service "$k")"
|
|
745
|
-
env_file="$(env_file_for "$svc")"
|
|
746
|
-
val="$(env_get "$env_file" "$k" 2>/dev/null)" || { warn "$k is not currently set in $env_file"; exit 0; }
|
|
747
|
-
printf '%s\n' "$val"
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
config_set() {
|
|
751
|
-
local k="$1" v="$2"
|
|
752
|
-
[[ -n "$k" && -n "$v" ]] || die "Usage: ework-aio config set <KEY> <VALUE>"
|
|
753
|
-
key_settable "$k" || die "Key '$k' is not settable. Run 'ework-aio config list' for the allow-list."
|
|
754
|
-
|
|
755
|
-
local svc env_file
|
|
756
|
-
svc="$(key_service "$k")"
|
|
757
|
-
env_file="$(env_file_for "$svc")"
|
|
758
|
-
|
|
759
|
-
log "Setting $k=$v in $env_file"
|
|
760
|
-
cp "$env_file" "$env_file.bak.$(date +%s)" 2>/dev/null || true
|
|
761
|
-
env_set "$env_file" "$k" "$v"
|
|
762
|
-
ok "$k updated"
|
|
763
|
-
|
|
764
|
-
case "$k" in
|
|
765
|
-
WORK_PORT)
|
|
766
|
-
log "Propagating to daemon (GITEA_URL)"
|
|
767
|
-
cp "$DAEMON_ENV" "$DAEMON_ENV.bak.$(date +%s)" 2>/dev/null || true
|
|
768
|
-
env_set "$DAEMON_ENV" "GITEA_URL" "http://127.0.0.1:$v"
|
|
769
|
-
ok "DAEMON_ENV GITEA_URL updated"
|
|
770
|
-
;;
|
|
771
|
-
DAEMON_PORT)
|
|
772
|
-
log "Propagating to web (WORK_DAEMON_WEBHOOK_URL)"
|
|
773
|
-
cp "$WEB_ENV" "$WEB_ENV.bak.$(date +%s)" 2>/dev/null || true
|
|
774
|
-
env_set "$WEB_ENV" "WORK_DAEMON_WEBHOOK_URL" "http://127.0.0.1:$v"
|
|
775
|
-
ok "WEB_ENV WORK_DAEMON_WEBHOOK_URL updated"
|
|
776
|
-
;;
|
|
777
|
-
esac
|
|
778
|
-
|
|
779
|
-
if [[ "$NO_RESTART" == "1" ]]; then
|
|
780
|
-
local to_restart
|
|
781
|
-
to_restart="$(service_for_key "$k")"
|
|
782
|
-
warn "--no-restart: changes saved but service not reloaded. Run 'ework-aio config restart $to_restart' to apply."
|
|
783
|
-
return
|
|
784
|
-
fi
|
|
785
|
-
|
|
786
|
-
local target
|
|
787
|
-
target="$(service_for_key "$k")"
|
|
788
|
-
log "Restarting $target..."
|
|
789
|
-
restart_service "$target" || warn "restart failed — services may need manual reload"
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
config_restart() {
|
|
793
|
-
local svc="${1:-both}"
|
|
794
|
-
case "$svc" in
|
|
795
|
-
web|daemon|both) restart_service "$svc" ;;
|
|
796
|
-
*) die "Usage: ework-aio config restart <web|daemon|both>" ;;
|
|
797
|
-
esac
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
run_config() {
|
|
801
|
-
local sub="${CFG_ARGS[0]:-list}"
|
|
802
|
-
case "$sub" in
|
|
803
|
-
list) config_list ;;
|
|
804
|
-
get) config_get "${CFG_ARGS[1]:-}" ;;
|
|
805
|
-
set) config_set "${CFG_ARGS[1]:-}" "${CFG_ARGS[2]:-}" ;;
|
|
806
|
-
restart) config_restart "${CFG_ARGS[1]:-both}" ;;
|
|
807
|
-
help|-h|--help) config_help ;;
|
|
808
|
-
*) die "Unknown config subcommand: $sub (try: ework-aio config help)" ;;
|
|
809
|
-
esac
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
# ─── Modes ──────────────────────────────────────────────────────────────────
|
|
813
|
-
case "$MODE" in
|
|
814
|
-
env)
|
|
815
|
-
hr; log "ework-aio paths"; hr
|
|
816
|
-
printf ' data dir : %s\n' "$DATA_DIR"
|
|
817
|
-
printf ' web env : %s\n' "$WEB_ENV"
|
|
818
|
-
printf ' daemon env : %s\n' "$DAEMON_ENV"
|
|
819
|
-
printf ' bot token : %s\n' "$BOT_TOKEN_FILE"
|
|
820
|
-
printf ' opencode cfg : %s\n' "$OPENCODE_CFG"
|
|
821
|
-
printf ' systemd scope : %s\n' "$SCOPENAME"
|
|
822
|
-
printf ' unit dir : %s\n' "$UNIT_DIR"
|
|
823
|
-
exit 0
|
|
824
|
-
;;
|
|
825
|
-
|
|
826
|
-
config)
|
|
827
|
-
run_config
|
|
828
|
-
exit 0
|
|
829
|
-
;;
|
|
830
|
-
|
|
831
|
-
status)
|
|
832
|
-
hr; log "ework-aio status"; hr
|
|
833
|
-
if ! systemd_reachable; then
|
|
834
|
-
log "(systemd unreachable — showing PID-file mode status)"
|
|
835
|
-
"$EWORK_AIO_BIN" ps 2>/dev/null || warn "ework-aio ps failed"
|
|
836
|
-
hr
|
|
837
|
-
log "Port listeners:"
|
|
838
|
-
p=""
|
|
839
|
-
for p in $(grep -hE '^(WORK_PORT|DAEMON_PORT)=' "$WEB_ENV" "$DAEMON_ENV" 2>/dev/null | cut -d= -f2 | sort -u); do
|
|
840
|
-
if curl -sf -o /dev/null --max-time 1 "http://127.0.0.1:$p/login" 2>/dev/null; then
|
|
841
|
-
printf ' :%s ✓ listening\n' "$p"
|
|
842
|
-
else
|
|
843
|
-
printf ' :%s ✗ not responding\n' "$p"
|
|
844
|
-
fi
|
|
845
|
-
done
|
|
846
|
-
exit 0
|
|
847
|
-
fi
|
|
848
|
-
ctl is-active ework-web.service || true
|
|
849
|
-
ctl is-active ework-daemon.service || true
|
|
850
|
-
ctl status --no-pager --lines=0 ework-web.service 2>/dev/null || true
|
|
851
|
-
hr
|
|
852
|
-
ctl status --no-pager --lines=0 ework-daemon.service 2>/dev/null || true
|
|
853
|
-
exit 0
|
|
854
|
-
;;
|
|
855
|
-
|
|
856
|
-
logs)
|
|
857
|
-
svc="${MODE_ARGS[0]:-ework-web.service}"
|
|
858
|
-
[[ "$svc" == "daemon" || "$svc" == "ework-daemon" ]] && svc="ework-daemon.service"
|
|
859
|
-
[[ "$svc" == "web" || "$svc" == "ework-web" ]] && svc="ework-web.service"
|
|
860
|
-
if ! systemd_reachable; then
|
|
861
|
-
svc_short="${svc%.service}"
|
|
862
|
-
svc_short="${svc_short#ework-}"
|
|
863
|
-
log_file="$DATA_DIR/run/${svc_short}.log"
|
|
864
|
-
if [[ ! -f "$log_file" ]]; then
|
|
865
|
-
die "systemd unreachable and no PID-file log at $log_file. Start services with: ework-aio start $svc_short"
|
|
866
|
-
fi
|
|
867
|
-
log "(systemd unreachable — tailing PID-file log $log_file)"
|
|
868
|
-
exec tail -n 200 -f "$log_file"
|
|
869
|
-
fi
|
|
870
|
-
exec journalctl "$SCOPENAME" -u "$svc" -f
|
|
871
|
-
;;
|
|
872
|
-
|
|
873
|
-
uninstall)
|
|
874
|
-
hr; log "Uninstalling ework-aio services (keeping data)"; hr
|
|
875
|
-
"$EWORK_AIO_BIN" stop both 2>/dev/null || true
|
|
876
|
-
if systemd_reachable; then
|
|
877
|
-
ctl stop ework-web.service ework-daemon.service 2>/dev/null || true
|
|
878
|
-
ctl disable ework-web.service ework-daemon.service 2>/dev/null || true
|
|
879
|
-
rm -f "$UNIT_DIR/ework-web.service" "$UNIT_DIR/ework-daemon.service"
|
|
880
|
-
ctl daemon-reload 2>/dev/null || true
|
|
881
|
-
else
|
|
882
|
-
log "(systemd unreachable — skipped unit cleanup; PID-file mode services stopped above)"
|
|
883
|
-
rm -f "$UNIT_DIR/ework-web.service" "$UNIT_DIR/ework-daemon.service" 2>/dev/null || true
|
|
884
|
-
fi
|
|
885
|
-
ok "Services removed. Data preserved at $DATA_DIR"
|
|
886
|
-
warn "To fully remove: rm -rf $DATA_DIR && npm uninstall -g ework-aio ework-web ework-daemon opencode-ework"
|
|
887
|
-
exit 0
|
|
888
|
-
;;
|
|
889
|
-
esac
|
|
890
|
-
|
|
891
|
-
# ─── Install mode ───────────────────────────────────────────────────────────
|
|
892
|
-
hr
|
|
893
|
-
log "ework-aio install"
|
|
894
|
-
log " mode : $([[ "$USE_SYSTEMD" == "1" ]] && echo "systemd" || echo "PID-file (no systemd)")"
|
|
895
|
-
log " data dir : $DATA_DIR"
|
|
896
|
-
log " web bin : $EWORK_WEB_BIN"
|
|
897
|
-
log " daemon bin : $EWORK_DAEMON_BIN"
|
|
898
|
-
log " opencode : $(command -v opencode) ($($(command -v opencode) --version 2>&1 | head -1))"
|
|
899
|
-
hr
|
|
900
|
-
|
|
901
|
-
if [[ "$USE_SYSTEMD" == "1" && "$SCOPENAME" == "--user" ]] && ! loginctl show-user "$USER" 2>/dev/null | grep -q '^Linger=yes'; then
|
|
902
|
-
warn "User-level systemd requires lingering to keep services alive after logout."
|
|
903
|
-
if [[ "$ASSUME_YES" == "1" ]]; then
|
|
904
|
-
warn "Run this manually: sudo loginctl enable-linger $USER"
|
|
905
|
-
else
|
|
906
|
-
read -rp "Enable linger now? (needs sudo) [Y/n] " ans
|
|
907
|
-
if [[ "${ans:-Y}" =~ ^[Yy]?$ ]]; then
|
|
908
|
-
sudo loginctl enable-linger "$USER" || warn "enable-linger failed; services will stop on logout"
|
|
909
|
-
fi
|
|
910
|
-
fi
|
|
911
|
-
fi
|
|
912
|
-
|
|
913
|
-
mkdir -p "$WEB_DATA_DIR" "$DAEMON_DATA_DIR" "$DATA_DIR/opencode-workdir"
|
|
914
|
-
|
|
915
|
-
write_web_env
|
|
916
|
-
|
|
917
|
-
# ─── Write ework-web systemd unit (only with `install systemd`) ────────────
|
|
918
|
-
SYSTEMD_OK=0
|
|
919
|
-
if [[ "$USE_SYSTEMD" == "1" ]]; then
|
|
920
|
-
WEB_UNIT="$UNIT_DIR/ework-web.service"
|
|
921
|
-
write_unit_file \
|
|
922
|
-
"ework-web" \
|
|
923
|
-
"ework — multi-project issue tracker" \
|
|
924
|
-
"$EWORK_WEB_BIN" \
|
|
925
|
-
"$WEB_DATA_DIR" \
|
|
926
|
-
"$WEB_ENV" \
|
|
927
|
-
"$WEB_UNIT"
|
|
928
|
-
ok "Wrote $WEB_UNIT"
|
|
929
|
-
|
|
930
|
-
# systemd calls here are best-effort. If systemd isn't functional on this host
|
|
931
|
-
# (containers without systemd PID 1, polkit redirects under sudo, missing
|
|
932
|
-
# /etc/systemd/system) we still want install to complete so the user can run
|
|
933
|
-
# `ework-aio start` (PID-file mode) against the scaffolded .env.
|
|
934
|
-
SYSTEMD_OK=1
|
|
935
|
-
if ! ctl daemon-reload; then
|
|
936
|
-
warn "systemctl daemon-reload failed — systemd may be unavailable on this host."
|
|
937
|
-
SYSTEMD_OK=0
|
|
938
|
-
fi
|
|
939
|
-
|
|
940
|
-
if [[ "$NO_START" == "0" && "$SYSTEMD_OK" == "1" ]]; then
|
|
941
|
-
log "Starting ework-web (systemd)..."
|
|
942
|
-
ctl enable ework-web.service || { warn "systemctl enable failed"; SYSTEMD_OK=0; }
|
|
943
|
-
if [[ "$SYSTEMD_OK" == "1" ]]; then
|
|
944
|
-
ctl restart ework-web.service || { warn "systemctl restart failed"; SYSTEMD_OK=0; }
|
|
945
|
-
fi
|
|
946
|
-
if [[ "$SYSTEMD_OK" == "1" ]]; then
|
|
947
|
-
for i in $(seq 1 60); do
|
|
948
|
-
if curl -sf -o /dev/null "http://127.0.0.1:$WORK_PORT/login"; then
|
|
949
|
-
ok "ework-web listening on :$WORK_PORT (after ${i} half-seconds)"
|
|
950
|
-
break
|
|
951
|
-
fi
|
|
952
|
-
sleep 0.5
|
|
953
|
-
[[ $i -eq 60 ]] && { warn "ework-web did not come up via systemd in 30s. Falling back to PID-file mode."; SYSTEMD_OK=0; }
|
|
954
|
-
done
|
|
955
|
-
fi
|
|
956
|
-
else
|
|
957
|
-
warn "--no-start: unit enabled but not started"
|
|
958
|
-
ctl enable ework-web.service 2>/dev/null || true
|
|
959
|
-
fi
|
|
960
|
-
fi
|
|
961
|
-
|
|
962
|
-
# ─── Start ework-web in PID-file mode ──────────────────────────────────────
|
|
963
|
-
# Default path (no `systemd` arg) OR systemd fallback. We bring web up via
|
|
964
|
-
# setsid+nohup so the bot bootstrap below can HTTP-probe it. Web stays
|
|
965
|
-
# running in PID-file mode after install — use `ework-aio stop` to kill it.
|
|
966
|
-
if [[ "$NO_START" == "0" && "$SYSTEMD_OK" == "0" ]]; then
|
|
967
|
-
log "Starting ework-web (PID-file mode)..."
|
|
968
|
-
WEB_LOG_FILE="$DATA_DIR/run/web.log"
|
|
969
|
-
mkdir -p "$(dirname "$WEB_LOG_FILE")"
|
|
970
|
-
setsid nohup "$EWORK_WEB_BIN" >>"$WEB_LOG_FILE" 2>&1 </dev/null &
|
|
971
|
-
WEB_PID=$!
|
|
972
|
-
disown "$WEB_PID" 2>/dev/null || true
|
|
973
|
-
printf '%s\n' "$WEB_PID" > "$DATA_DIR/run/web.pid"
|
|
974
|
-
ok "ework-web started in PID-file mode (pid $WEB_PID, log $WEB_LOG_FILE)"
|
|
975
|
-
for i in $(seq 1 60); do
|
|
976
|
-
if curl -sf -o /dev/null "http://127.0.0.1:$WORK_PORT/login"; then
|
|
977
|
-
ok "ework-web listening on :$WORK_PORT (after ${i} half-seconds, PID-file mode)"
|
|
978
|
-
break
|
|
979
|
-
fi
|
|
980
|
-
sleep 0.5
|
|
981
|
-
[[ $i -eq 60 ]] && die "ework-web did not come up in 30s. Tail of $WEB_LOG_FILE: $(tail -n 20 "$WEB_LOG_FILE" 2>/dev/null)"
|
|
982
|
-
done
|
|
983
|
-
fi
|
|
984
|
-
|
|
985
|
-
# ─── Bootstrap bot user + PAT (idempotent) ─────────────────────────────────
|
|
986
|
-
WORK_TOKEN_VAL=$(awk -F= '/^WORK_TOKEN=/{print $2}' "$WEB_ENV")
|
|
987
|
-
WORK_COOKIE_SECRET_VAL=$(awk -F= '/^WORK_COOKIE_SECRET=/{print $2}' "$WEB_ENV")
|
|
988
|
-
# ework-web's checkAuth accepts legacy "<token>.<hmac>" cookie form
|
|
989
|
-
COOKIE_SIG=$(printf '%s' "$WORK_TOKEN_VAL" \
|
|
990
|
-
| openssl dgst -sha256 -hmac "$WORK_COOKIE_SECRET_VAL" -binary \
|
|
991
|
-
| base64 | tr '+/' '-_' | tr -d '=')
|
|
992
|
-
AUTH_COOKIE="ework_auth=${WORK_TOKEN_VAL}.${COOKIE_SIG}"
|
|
993
|
-
|
|
994
|
-
BOT_TOKEN=""
|
|
995
|
-
BOT_BOOTSTRAP_OK=1
|
|
996
|
-
if [[ -f "$BOT_TOKEN_FILE" ]]; then
|
|
997
|
-
BOT_TOKEN=$(cat "$BOT_TOKEN_FILE")
|
|
998
|
-
ok "Reusing saved bot token from $BOT_TOKEN_FILE"
|
|
999
|
-
else
|
|
1000
|
-
# Pre-flight: web must be reachable for the bot bootstrap HTTP calls below.
|
|
1001
|
-
# In --no-start mode OR on hosts where systemd couldn't bring web up, skip
|
|
1002
|
-
# the bootstrap entirely — daemon .env still gets written (with empty token)
|
|
1003
|
-
# so the user can `ework-aio start` and re-run install later.
|
|
1004
|
-
if ! curl -sf -o /dev/null "http://127.0.0.1:$WORK_PORT/login"; then
|
|
1005
|
-
warn "ework-web not reachable at :$WORK_PORT — skipping bot bootstrap."
|
|
1006
|
-
warn "Daemon will not be able to talk to web until you re-run install with web running."
|
|
1007
|
-
BOT_BOOTSTRAP_OK=0
|
|
1008
|
-
fi
|
|
1009
|
-
fi
|
|
1010
|
-
|
|
1011
|
-
if [[ "$BOT_BOOTSTRAP_OK" == "1" && ! -f "$BOT_TOKEN_FILE" ]]; then
|
|
1012
|
-
log "Bootstrapping bot user '$BOT_NAME'..."
|
|
1013
|
-
BOT_PW=$(openssl rand -hex 24)
|
|
1014
|
-
CREATE_CODE=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
|
1015
|
-
"http://127.0.0.1:$WORK_PORT/admin/users/create" \
|
|
1016
|
-
-H "Cookie: $AUTH_COOKIE" \
|
|
1017
|
-
--data-urlencode "login=$BOT_NAME" \
|
|
1018
|
-
--data-urlencode "password=$BOT_PW" \
|
|
1019
|
-
--data-urlencode "kind=bot" \
|
|
1020
|
-
--data-urlencode "is_admin=0") || CREATE_CODE=000
|
|
1021
|
-
case "$CREATE_CODE" in
|
|
1022
|
-
303) ok "Bot user '$BOT_NAME' created" ;;
|
|
1023
|
-
400|409) warn "Bot user '$BOT_NAME' already exists (continuing)" ;;
|
|
1024
|
-
*) warn "Failed to create bot user: HTTP $CREATE_CODE — skipping PAT mint; daemon will not be able to talk to web until re-run"; BOT_BOOTSTRAP_OK=0 ;;
|
|
1025
|
-
esac
|
|
1026
|
-
fi
|
|
1027
|
-
|
|
1028
|
-
if [[ "$BOT_BOOTSTRAP_OK" == "1" && ! -f "$BOT_TOKEN_FILE" ]]; then
|
|
1029
|
-
log "Logging in as bot to mint PAT..."
|
|
1030
|
-
COOKIE_JAR=$(mktemp)
|
|
1031
|
-
LOGIN_CODE=$(curl -sS -c "$COOKIE_JAR" -X POST "http://127.0.0.1:$WORK_PORT/login" \
|
|
1032
|
-
--data-urlencode "login=$BOT_NAME" \
|
|
1033
|
-
--data-urlencode "password=$BOT_PW" \
|
|
1034
|
-
-o /dev/null -w '%{http_code}') || LOGIN_CODE=000
|
|
1035
|
-
BOT_COOKIE=$(awk '/ework_auth/ {print $7}' "$COOKIE_JAR")
|
|
1036
|
-
rm -f "$COOKIE_JAR"
|
|
1037
|
-
if [[ "$LOGIN_CODE" != "302" || -z "$BOT_COOKIE" ]]; then
|
|
1038
|
-
warn "Bot login failed: HTTP $LOGIN_CODE — skipping PAT mint"
|
|
1039
|
-
BOT_BOOTSTRAP_OK=0
|
|
1040
|
-
fi
|
|
1041
|
-
fi
|
|
1042
|
-
|
|
1043
|
-
if [[ "$BOT_BOOTSTRAP_OK" == "1" && ! -f "$BOT_TOKEN_FILE" ]]; then
|
|
1044
|
-
log "Minting PAT..."
|
|
1045
|
-
PAT_RES=$(curl -sS -X POST "http://127.0.0.1:$WORK_PORT/me/tokens/create" \
|
|
1046
|
-
-H "Cookie: ework_auth=$BOT_COOKIE" \
|
|
1047
|
-
--data-urlencode "name=aio-$(date +%s)")
|
|
1048
|
-
BOT_TOKEN=$(printf '%s' "$PAT_RES" | grep -oE 'id="t">[a-f0-9]{40}<' | grep -oE '[a-f0-9]{40}' | head -1 || true)
|
|
1049
|
-
if [[ -z "$BOT_TOKEN" ]]; then
|
|
1050
|
-
warn "Could not extract PAT from token-create response — daemon .env will have empty token"
|
|
1051
|
-
else
|
|
1052
|
-
printf '%s' "$BOT_TOKEN" > "$BOT_TOKEN_FILE"
|
|
1053
|
-
chmod 600 "$BOT_TOKEN_FILE"
|
|
1054
|
-
ok "Bot PAT saved to $BOT_TOKEN_FILE"
|
|
1055
|
-
fi
|
|
1056
|
-
fi
|
|
1057
|
-
|
|
1058
|
-
# ─── Write ework-daemon .env ───────────────────────────────────────────────
|
|
1059
|
-
write_daemon_env "$BOT_TOKEN"
|
|
1060
|
-
|
|
1061
|
-
# ─── Write + start ework-daemon (systemd path, only with `install systemd`) ─
|
|
1062
|
-
if [[ "$USE_SYSTEMD" == "1" ]]; then
|
|
1063
|
-
DAEMON_UNIT="$UNIT_DIR/ework-daemon.service"
|
|
1064
|
-
write_unit_file \
|
|
1065
|
-
"ework-daemon" \
|
|
1066
|
-
"ework-daemon — issue-driven AI dev daemon" \
|
|
1067
|
-
"$EWORK_DAEMON_BIN" \
|
|
1068
|
-
"$DAEMON_DATA_DIR" \
|
|
1069
|
-
"$DAEMON_ENV" \
|
|
1070
|
-
"$DAEMON_UNIT" \
|
|
1071
|
-
" \"\""
|
|
1072
|
-
# (trailing " \"\"" is a no-op separator; kept for future extra env injection)
|
|
1073
|
-
ok "Wrote $DAEMON_UNIT"
|
|
1074
|
-
|
|
1075
|
-
ctl daemon-reload 2>/dev/null || true
|
|
1076
|
-
if [[ "$NO_START" == "0" && "$SYSTEMD_OK" == "1" ]]; then
|
|
1077
|
-
log "Starting ework-daemon (systemd)..."
|
|
1078
|
-
ctl enable ework-daemon.service 2>/dev/null || true
|
|
1079
|
-
ctl restart ework-daemon.service 2>/dev/null || warn "ework-daemon restart via systemd failed; falling back to PID-file mode"
|
|
1080
|
-
sleep 1
|
|
1081
|
-
if ctl is-active --quiet ework-daemon.service 2>/dev/null; then
|
|
1082
|
-
ok "ework-daemon active (systemd)"
|
|
1083
|
-
else
|
|
1084
|
-
warn "ework-daemon not active via systemd — falling back to PID-file mode"
|
|
1085
|
-
SYSTEMD_OK=0
|
|
1086
|
-
fi
|
|
1087
|
-
else
|
|
1088
|
-
ctl enable ework-daemon.service 2>/dev/null || true
|
|
1089
|
-
fi
|
|
1090
|
-
fi
|
|
1091
|
-
|
|
1092
|
-
# ─── Start ework-daemon in PID-file mode (default path) ────────────────────
|
|
1093
|
-
# Mirrors the web PID-file start. Skipped if systemd already brought it up.
|
|
1094
|
-
if [[ "$NO_START" == "0" && "$SYSTEMD_OK" == "0" ]]; then
|
|
1095
|
-
log "Starting ework-daemon (PID-file mode)..."
|
|
1096
|
-
DAEMON_LOG_FILE="$DATA_DIR/run/daemon.log"
|
|
1097
|
-
mkdir -p "$(dirname "$DAEMON_LOG_FILE")"
|
|
1098
|
-
setsid nohup "$EWORK_DAEMON_BIN" >>"$DAEMON_LOG_FILE" 2>&1 </dev/null &
|
|
1099
|
-
DAEMON_PID=$!
|
|
1100
|
-
disown "$DAEMON_PID" 2>/dev/null || true
|
|
1101
|
-
printf '%s\n' "$DAEMON_PID" > "$DATA_DIR/run/daemon.pid"
|
|
1102
|
-
ok "ework-daemon started in PID-file mode (pid $DAEMON_PID, log $DAEMON_LOG_FILE)"
|
|
1103
|
-
fi
|
|
1104
|
-
|
|
1105
|
-
if [[ "$USE_SYSTEMD" == "1" && "$SYSTEMD_OK" == "0" ]]; then
|
|
1106
|
-
hr
|
|
1107
|
-
warn "systemd mode did not come up cleanly on this host."
|
|
1108
|
-
warn "Services started in PID-file mode instead. systemd unit files were"
|
|
1109
|
-
warn "written but are not active. To clean them up: ework-aio uninstall"
|
|
1110
|
-
hr
|
|
1111
|
-
fi
|
|
1112
|
-
|
|
1113
|
-
# ─── Register opencode-ework plugin ────────────────────────────────────────
|
|
1114
|
-
# Safety: every edit writes to a temp file, validates with `jq -e .`, only
|
|
1115
|
-
# then atomically replaces the original. A timestamped .bak is always kept.
|
|
1116
|
-
mkdir -p "$(dirname "$OPENCODE_CFG")"
|
|
1117
|
-
|
|
1118
|
-
json_edit() {
|
|
1119
|
-
# $1 = file, $2 = jq program, $3 = expected JSON type (optional, e.g. "object").
|
|
1120
|
-
# Writes to temp, validates (parses + optional type check), only then swaps.
|
|
1121
|
-
# Backup at $file.bak.<epoch>.
|
|
1122
|
-
local file="$1" prog="$2" expected="${3:-}" tmp
|
|
1123
|
-
tmp=$(mktemp)
|
|
1124
|
-
if ! jq "$prog" "$file" > "$tmp" 2>/dev/null; then
|
|
1125
|
-
rm -f "$tmp"
|
|
1126
|
-
die "jq edit failed on $file (program: $prog)"
|
|
1127
|
-
fi
|
|
1128
|
-
if ! jq -e . "$tmp" >/dev/null 2>&1; then
|
|
1129
|
-
rm -f "$tmp"
|
|
1130
|
-
die "jq edit produced invalid JSON on $file — aborted, original untouched"
|
|
1131
|
-
fi
|
|
1132
|
-
if [[ -n "$expected" ]] && ! jq -e "type == \"$expected\"" "$tmp" >/dev/null 2>&1; then
|
|
1133
|
-
rm -f "$tmp"
|
|
1134
|
-
die "jq edit produced wrong type on $file (expected $expected) — aborted, original untouched"
|
|
1135
|
-
fi
|
|
1136
|
-
cp "$file" "$file.bak.$(date +%s)"
|
|
1137
|
-
mv "$tmp" "$file"
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
if [[ ! -f "$OPENCODE_CFG" ]]; then
|
|
1141
|
-
log "Writing $OPENCODE_CFG (registering opencode-ework plugin)"
|
|
1142
|
-
cat > "$OPENCODE_CFG" <<'EOF'
|
|
1143
|
-
{
|
|
1144
|
-
"$schema": "https://opencode.ai/config.json",
|
|
1145
|
-
"plugin": ["opencode-ework@latest"]
|
|
1146
|
-
}
|
|
1147
|
-
EOF
|
|
1148
|
-
elif grep -q '"opencode-ework@latest"' "$OPENCODE_CFG"; then
|
|
1149
|
-
ok "opencode-ework@latest already in $OPENCODE_CFG"
|
|
1150
|
-
elif grep -q '"opencode-ework"' "$OPENCODE_CFG"; then
|
|
1151
|
-
log "Upgrading opencode-ework → opencode-ework@latest in $OPENCODE_CFG"
|
|
1152
|
-
json_edit "$OPENCODE_CFG" \
|
|
1153
|
-
'.plugin = ((.plugin // []) | map(if . == "opencode-ework" then "opencode-ework@latest" else . end))' \
|
|
1154
|
-
object
|
|
1155
|
-
ok "Plugin upgraded (backup at $OPENCODE_CFG.bak.*)"
|
|
1156
|
-
else
|
|
1157
|
-
log "Merging opencode-ework@latest into existing $OPENCODE_CFG"
|
|
1158
|
-
json_edit "$OPENCODE_CFG" \
|
|
1159
|
-
'if .plugin then .plugin += ["opencode-ework@latest"] else . + {plugin:["opencode-ework@latest"]} end' \
|
|
1160
|
-
object
|
|
1161
|
-
ok "Plugin registered (backup at $OPENCODE_CFG.bak.*)"
|
|
1162
|
-
fi
|
|
1163
|
-
|
|
1164
|
-
if ! jq -e 'type == "object"' "$OPENCODE_CFG" >/dev/null 2>&1; then
|
|
1165
|
-
warn "$OPENCODE_CFG is not a JSON object — check $OPENCODE_CFG.bak.* for restore"
|
|
1166
|
-
fi
|
|
1167
|
-
|
|
1168
|
-
# ─── Done ──────────────────────────────────────────────────────────────────
|
|
1169
|
-
hr
|
|
1170
|
-
ok "Install complete ($([[ "$USE_SYSTEMD" == "1" ]] && echo "systemd mode" || echo "PID-file mode"))."
|
|
1171
|
-
hr
|
|
1172
|
-
printf '\n%s→%s Open %shttp://127.0.0.1:%s/login%s\n' \
|
|
1173
|
-
"$c_bold" "$c_reset" "$c_dim" "$WORK_PORT" "$c_reset"
|
|
1174
|
-
printf ' Operator login: %s%s%s (auto-promoted admin; derived from $USER at install time)\n' \
|
|
1175
|
-
"$c_bold" "$USER" "$c_reset"
|
|
1176
|
-
printf ' Login token: %s%s%s\n' "$c_bold" "$WORK_TOKEN_VAL" "$c_reset"
|
|
1177
|
-
printf ' Bot user: %s%s%s (auto-created, used by ework-daemon)\n' \
|
|
1178
|
-
"$c_bold" "$BOT_NAME" "$c_reset"
|
|
1179
|
-
printf ' Data dir: %s%s%s\n' "$c_dim" "$DATA_DIR" "$c_reset"
|
|
1180
|
-
printf ' Logs: ework-aio logs web | ework-aio logs daemon\n'
|
|
1181
|
-
printf ' Status: ework-aio status\n'
|
|
1182
|
-
printf ' Stop: ework-aio stop\n'
|
|
1183
|
-
printf ' Uninstall: ework-aio uninstall\n'
|
|
1184
|
-
if [[ "$USE_SYSTEMD" != "1" ]]; then
|
|
1185
|
-
hr
|
|
1186
|
-
printf '\n%sPID-file mode note%s (services run via nohup, not systemd)\n' "$c_bold" "$c_reset"
|
|
1187
|
-
printf ' • To enable auto-restart on boot, re-run with: ework-aio install systemd\n'
|
|
1188
|
-
printf ' • Services stop when you kill them (no supervisor). Use ework-aio stop to stop cleanly.\n'
|
|
1189
|
-
fi
|
|
1190
|
-
hr
|
|
1191
|
-
printf '\n%sNext steps (optional config)%s\n' "$c_bold" "$c_reset"
|
|
1192
|
-
printf ' • 朗读 (TTS): %shttp://127.0.0.1:%s/admin/tts%s — needs an OpenAI-compat /audio/speech endpoint\n' \
|
|
1193
|
-
"$c_dim" "$WORK_PORT" "$c_reset"
|
|
1194
|
-
printf ' • 翻译: edit %s%s/.env%s, set WORK_TRANSLATE_URL + WORK_TRANSLATE_MODEL\n' \
|
|
1195
|
-
"$c_dim" "$WEB_DATA_DIR" "$c_reset"
|
|
1196
|
-
printf ' (OpenAI-compat /v1/chat/completions endpoint), then: ework-aio install\n'
|
|
1197
|
-
printf ' • Per-project webhook: open %s/<owner>/<repo>/webhooks%s to wire downstream\n' \
|
|
1198
|
-
"$c_dim" "$c_reset"
|
|
1199
|
-
printf ' consumers (GitHub Actions, etc.). Gitea-compat payload + HMAC-SHA256 sig.\n'
|
|
1200
|
-
hr
|