ework-aio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ework contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # ework-aio
2
+
3
+ All-in-one installer for the **ework** self-hosted AI development stack:
4
+
5
+ | Component | npm package | Role |
6
+ | --------------- | ---------------- | -------------------------------------------------------------------- |
7
+ | **ework-web** | `ework-web` | Multi-project issue tracker (web UI + Gitea-compat REST) |
8
+ | **ework-daemon**| `ework-daemon` | Issue-driven AI bridge (spawns `opencode` to resolve issues) |
9
+ | **opencode-ework** | `opencode-ework` | OpenCode plugin (gives agents `issue`/`reply`/`floor` tools) |
10
+
11
+ `ework-aio` wires the three together on a single host: it installs systemd units, generates tokens, bootstraps the bot user, and registers the plugin in `~/.config/opencode/opencode.json`.
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ # 1. Make sure prerequisites are on PATH
17
+ bun --version # >= 1.1.0 — https://bun.sh
18
+ opencode --version # >= 1.14 — https://opencode.ai
19
+ npm --version # ships with bun or node
20
+ systemctl --version # this installer requires systemd
21
+
22
+ # 2. One-shot install
23
+ npx ework-aio install
24
+ # or: npm install -g ework-aio && ework-aio install
25
+ ```
26
+
27
+ The installer prints your login URL and token when it finishes.
28
+
29
+ ## What it does
30
+
31
+ 1. **Verifies prerequisites** (bun, npm, opencode, systemctl, openssl, curl, jq).
32
+ 2. **Installs the 3 npm packages** globally (if not already present).
33
+ 3. **Generates `.env`** with random tokens at `~/.local/share/ework-aio/{ework-web,ework-daemon}/.env` (preserved across re-runs).
34
+ 4. **Writes systemd units** (`ework-web.service`, `ework-daemon.service`) — user-level by default, system-level when run as root.
35
+ 5. **Starts ework-web** and waits for it to listen.
36
+ 6. **Bootstraps the bot user** (`ework-daemon` by default) via `ework-web`'s `/admin/users/create`, then mints a PAT via `/me/tokens/create`. Saved to `~/.local/share/ework-aio/.bot-token` (reused on re-runs).
37
+ 7. **Writes ework-daemon `.env`** with the bot PAT.
38
+ 8. **Starts ework-daemon**.
39
+ 9. **Merges `opencode-ework`** into `~/.config/opencode/opencode.json`'s `plugin` array (idempotent; backs up the original).
40
+
41
+ ## Commands
42
+
43
+ ```
44
+ ework-aio install [options] Install or upgrade the stack
45
+ ework-aio uninstall Stop services and remove units (data preserved)
46
+ ework-aio status Show service status
47
+ ework-aio logs [web|daemon] Tail logs
48
+ ework-aio env Print key paths (no secrets)
49
+ ```
50
+
51
+ ### Install options
52
+
53
+ | Flag | Default | Description |
54
+ | ----------------------- | ------------------------------------ | ------------------------------------------ |
55
+ | `--user` | (auto: `--user` unless EUID=0) | Use `systemctl --user` |
56
+ | `--system` | (auto: `--system` if EUID=0) | Use `systemctl` (system-level, needs root) |
57
+ | `--data-dir <path>` | `~/.local/share/ework-aio` | Override data root |
58
+ | `--port <n>` | `3002` | ework-web port |
59
+ | `--daemon-port <n>` | `3101` | ework-daemon port |
60
+ | `--bot-name <login>` | `ework-daemon` | Bot username in ework-web |
61
+ | `--no-start` | (off) | Install units but don't start |
62
+ | `--yes` | (off) | Skip prompts (use generated defaults) |
63
+
64
+ ## File layout
65
+
66
+ ```
67
+ ~/.local/share/ework-aio/
68
+ ├── ework-web/
69
+ │ ├── .env # ework-web config (tokens, ports, paths)
70
+ │ ├── ework.db # SQLite database (issues, comments, users, ...)
71
+ │ └── attachments/ # Filesystem attachments
72
+ ├── ework-daemon/
73
+ │ ├── .env # ework-daemon config (bot creds, opencode paths)
74
+ │ └── ework-daemon.db # Daemon state (processes, runs, ...)
75
+ ├── opencode-workdir/ # Where opencode checks out repos
76
+ └── .bot-token # Bot PAT (chmod 600)
77
+
78
+ ~/.config/systemd/user/
79
+ ├── ework-web.service
80
+ └── ework-daemon.service
81
+
82
+ ~/.config/opencode/opencode.json # plugin: ["opencode-ework", ...]
83
+ ```
84
+
85
+ ## Idempotency
86
+
87
+ Re-running `ework-aio install` is safe:
88
+ - `.env` files are preserved (use `rm` to regenerate).
89
+ - Bot user creation returns 400/409 if it already exists; PAT is reused.
90
+ - Systemd units are overwritten (config drift auto-corrected).
91
+ - Plugin merge skips if `opencode-ework` already in `plugin` array.
92
+
93
+ ## Uninstall
94
+
95
+ ```bash
96
+ ework-aio uninstall # stops services, removes units
97
+ rm -rf ~/.local/share/ework-aio # also delete data
98
+ npm uninstall -g ework-aio ework-web ework-daemon opencode-ework
99
+ # Remove the plugin entry from ~/.config/opencode/opencode.json manually
100
+ ```
101
+
102
+ ## Alternatives
103
+
104
+ - **Docker AIO**: the `ework-web` repo ships `docker/build.sh` + `docker/run.sh` for a single-container deployment. Useful when you don't want to manage host systemd.
105
+ - **Manual**: install the 3 npm packages yourself and wire services by hand using the systemd unit templates in each package.
106
+
107
+ ## License
108
+
109
+ MIT
package/bin/ework-aio ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bun
2
+ import { spawn, spawnSync } from "bun";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { resolve, dirname, join } from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ const args = process.argv.slice(2);
10
+ const subcommand = args[0] || "install";
11
+
12
+ function usage() {
13
+ console.log(`ework-aio — installer and controller for the ework stack
14
+
15
+ Usage:
16
+ ework-aio install [options] Install and start ework-web + ework-daemon
17
+ ework-aio uninstall Stop services and remove units (keeps data)
18
+ ework-aio status Show service status
19
+ ework-aio logs [svc] Tail logs (svc: ework-web | ework-daemon)
20
+ ework-aio env Print the .env path and key names (no values)
21
+ ework-aio --version Print version
22
+
23
+ Install options:
24
+ --user Use user-level systemd units (default if non-root)
25
+ --system Use system-level systemd units (default if root)
26
+ --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)
29
+ --bot-name <login> Bot username (default: ework-daemon)
30
+ --no-start Install units but don't start services
31
+ --yes Skip all prompts (use generated defaults)
32
+ `);
33
+ }
34
+
35
+ if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
36
+ usage();
37
+ process.exit(0);
38
+ }
39
+ if (subcommand === "--version" || subcommand === "-v") {
40
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
41
+ console.log(`ework-aio ${pkg.version}`);
42
+ process.exit(0);
43
+ }
44
+
45
+ const installSh = resolve(__dirname, "install.sh");
46
+ if (!existsSync(installSh)) {
47
+ console.error(`Cannot find install.sh next to ework-aio bin (looked at ${installSh}).`);
48
+ console.error("This usually means the package was installed incorrectly.");
49
+ process.exit(1);
50
+ }
51
+
52
+ const knownSubs = ["install", "uninstall", "status", "logs", "env"];
53
+ const finalArgs = knownSubs.includes(subcommand) ? args : ["install", ...args];
54
+
55
+ const result = spawnSync(["bash", installSh, ...finalArgs], {
56
+ stdio: ["inherit", "inherit", "inherit"],
57
+ env: process.env,
58
+ });
59
+ if (result.status !== null) process.exit(result.status);
60
+ process.exit(1);
package/bin/install.sh ADDED
@@ -0,0 +1,444 @@
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
+ # ─── Pretty output ──────────────────────────────────────────────────────────
16
+ c_reset=$'\033[0m'; c_bold=$'\033[1m'; c_dim=$'\033[2m'
17
+ c_red=$'\033[31m'; c_grn=$'\033[32m'; c_ylw=$'\033[33m'; c_blu=$'\033[34m'
18
+ log() { printf '%s•%s %s\n' "$c_blu" "$c_reset" "$*"; }
19
+ ok() { printf '%s✓%s %s\n' "$c_grn" "$c_reset" "$*"; }
20
+ warn() { printf '%s!%s %s\n' "$c_ylw" "$c_reset" "$*" >&2; }
21
+ die() { printf '%s✗%s %s\n' "$c_red" "$c_reset" "$*" >&2; exit 1; }
22
+ hr() { printf '%s──%s\n' "$c_dim" "$c_reset"; }
23
+
24
+ # ─── Defaults & arg parsing ─────────────────────────────────────────────────
25
+ MODE="install"
26
+ SCOPENAME="--user"
27
+ DATA_DIR=""
28
+ WORK_PORT="3002"
29
+ DAEMON_PORT="3101"
30
+ BOT_NAME="ework-daemon"
31
+ NO_START=0
32
+ ASSUME_YES=0
33
+
34
+ usage() {
35
+ cat <<'EOF'
36
+ ework-aio install [options]
37
+
38
+ Options:
39
+ --user Use user-level systemd units (default if non-root)
40
+ --system Use system-level systemd units (default if root)
41
+ --data-dir <path> Override data directory (default: ~/.local/share/ework-aio)
42
+ --port <n> ework-web port (default: 3002)
43
+ --daemon-port <n> ework-daemon port (default: 3101)
44
+ --bot-name <login> Bot username (default: ework-daemon)
45
+ --no-start Install units but don't start services
46
+ --yes Skip all prompts (use generated defaults)
47
+ --mode <install|uninstall|status|logs|env> (internal; first positional arg wins)
48
+ -h, --help Show this help
49
+ EOF
50
+ }
51
+
52
+ while [[ $# -gt 0 ]]; do
53
+ case "$1" in
54
+ install|uninstall|status|logs|env) MODE="$1"; shift ;;
55
+ --user) SCOPENAME="--user"; shift ;;
56
+ --system) SCOPENAME="--system"; shift ;;
57
+ --data-dir) DATA_DIR="$2"; shift 2 ;;
58
+ --port) WORK_PORT="$2"; shift 2 ;;
59
+ --daemon-port) DAEMON_PORT="$2"; shift 2 ;;
60
+ --bot-name) BOT_NAME="$2"; shift 2 ;;
61
+ --no-start) NO_START=1; shift ;;
62
+ --yes|-y) ASSUME_YES=1; shift ;;
63
+ -h|--help) usage; exit 0 ;;
64
+ *) die "Unknown argument: $1 (try --help)" ;;
65
+ esac
66
+ done
67
+
68
+ # Resolve scope: default by uid
69
+ if [[ "$SCOPENAME" == "--user" && "${EUID:-$(id -u)}" == "0" ]]; then
70
+ SCOPENAME="--system"
71
+ fi
72
+
73
+ # ─── Pre-flight: command dependencies ───────────────────────────────────────
74
+ need_cmd() {
75
+ command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1. $2"
76
+ }
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"
80
+ 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
+ need_cmd awk "Should be present on any Linux."
85
+
86
+ # Verify the 3 npm packages are reachable. If not, try to install them now.
87
+ ensure_pkg() {
88
+ local pkg="$1"
89
+ if ! command -v "$pkg" >/dev/null 2>&1 \
90
+ && ! [[ -d "$(npm root -g 2>/dev/null)/$pkg" ]]; then
91
+ log "npm package '$pkg' not detected; installing globally now..."
92
+ npm install -g "$pkg" || die "npm install -g $pkg failed"
93
+ fi
94
+ }
95
+ case "$MODE" in
96
+ install)
97
+ log "Ensuring npm packages are installed..."
98
+ ensure_pkg "ework-web"
99
+ ensure_pkg "ework-daemon"
100
+ # opencode-ework is a library, not a bin; check node_modules
101
+ if ! [[ -d "$(npm root -g)/opencode-ework" ]] \
102
+ && ! [[ -d "$(npm root -g)/ework-aio/node_modules/opencode-ework" ]]; then
103
+ npm install -g opencode-ework || die "npm install -g opencode-ework failed"
104
+ fi
105
+ ok "npm packages ready"
106
+
107
+ # Absolute paths to the bin shims (resolved once, baked into systemd units)
108
+ EWORK_WEB_BIN="$(command -v ework-web 2>/dev/null || true)"
109
+ EWORK_DAEMON_BIN="$(command -v ework-daemon-server 2>/dev/null || true)"
110
+ [[ -n "$EWORK_WEB_BIN" ]] || EWORK_WEB_BIN="$(npm root -g)/ework-web/bin/ework-web.js"
111
+ [[ -n "$EWORK_DAEMON_BIN" ]] || EWORK_DAEMON_BIN="$(npm root -g)/ework-daemon/bin/ework-daemon-server.js"
112
+ [[ -x "$EWORK_WEB_BIN" || -f "$EWORK_WEB_BIN" ]] || die "ework-web bin not found at $EWORK_WEB_BIN"
113
+ [[ -x "$EWORK_DAEMON_BIN" || -f "$EWORK_DAEMON_BIN" ]] || die "ework-daemon-server bin not found at $EWORK_DAEMON_BIN"
114
+ export EWORK_WEB_BIN EWORK_DAEMON_BIN
115
+ ;;
116
+ esac
117
+
118
+ # ─── Paths ──────────────────────────────────────────────────────────────────
119
+ XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
120
+ XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
121
+ DATA_DIR="${DATA_DIR:-$XDG_DATA_HOME/ework-aio}"
122
+ WEB_DATA_DIR="$DATA_DIR/ework-web"
123
+ DAEMON_DATA_DIR="$DATA_DIR/ework-daemon"
124
+ WEB_ENV="$WEB_DATA_DIR/.env"
125
+ DAEMON_ENV="$DAEMON_DATA_DIR/.env"
126
+ BOT_TOKEN_FILE="$DATA_DIR/.bot-token"
127
+ OPENCODE_CFG="$XDG_CONFIG_HOME/opencode/opencode.json"
128
+
129
+ # systemd unit dir
130
+ if [[ "$SCOPENAME" == "--user" ]]; then
131
+ UNIT_DIR="$XDG_CONFIG_HOME/systemd/user"
132
+ else
133
+ UNIT_DIR="/etc/systemd/system"
134
+ fi
135
+ mkdir -p "$UNIT_DIR"
136
+
137
+ # ─── Helpers ────────────────────────────────────────────────────────────────
138
+ gen_token() { openssl rand -hex "${1:-24}"; }
139
+
140
+ write_web_env() {
141
+ mkdir -p "$(dirname "$WEB_ENV")"
142
+ if [[ -f "$WEB_ENV" ]]; then
143
+ log "Preserving existing $WEB_ENV"
144
+ return
145
+ fi
146
+ local tok secret webhook
147
+ tok=$(gen_token 20)
148
+ secret=$(gen_token 24)
149
+ webhook=$(gen_token 20)
150
+ cat > "$WEB_ENV" <<EOF
151
+ # Generated by ework-aio at $(date -u +%Y-%m-%dT%H:%M:%SZ)
152
+ WORK_PORT=$WORK_PORT
153
+ WORK_HOST=127.0.0.1
154
+ WORK_TOKEN=$tok
155
+ WORK_COOKIE_SECRET=$secret
156
+ WORK_OPERATOR_LOGIN=$USER
157
+ WORK_WRITES_ENABLED=true
158
+ WORK_DB_PATH=$WEB_DATA_DIR/ework.db
159
+ WORK_ATTACHMENT_ROOT=$WEB_DATA_DIR/attachments
160
+ WORK_FILE_ROOTS=/tmp,$DATA_DIR
161
+ WORK_DAEMON_BOT_LOGIN=$BOT_NAME
162
+ WORK_DAEMON_WEBHOOK_URL=http://127.0.0.1:$DAEMON_PORT
163
+ WORK_DAEMON_WEBHOOK_SECRET=$webhook
164
+ EOF
165
+ chmod 600 "$WEB_ENV"
166
+ ok "Wrote $WEB_ENV"
167
+ }
168
+
169
+ write_daemon_env() {
170
+ mkdir -p "$(dirname "$DAEMON_ENV")"
171
+ local bot_token="$1"
172
+ local webhook
173
+ # Reuse webhook secret from web env so daemon's signature matches web's verify
174
+ webhook=$(awk -F= '/^WORK_DAEMON_WEBHOOK_SECRET=/{print $2}' "$WEB_ENV" 2>/dev/null || true)
175
+ [[ -n "$webhook" ]] || webhook=$(gen_token 20)
176
+ cat > "$DAEMON_ENV" <<EOF
177
+ # Generated by ework-aio at $(date -u +%Y-%m-%dT%H:%M:%SZ)
178
+ DAEMON_ENV=production
179
+ DAEMON_PORT=$DAEMON_PORT
180
+ DAEMON_HOST=127.0.0.1
181
+ DAEMON_DB_PATH=$DAEMON_DATA_DIR/ework-daemon.db
182
+ GITEA_URL=http://127.0.0.1:$WORK_PORT
183
+ GITEA_TOKEN=$bot_token
184
+ GITEA_WEBHOOK_SECRET=$webhook
185
+ BOT_USERNAME=$BOT_NAME
186
+ BOT_TOKEN=$bot_token
187
+ OPENCODE_BINARY=$(command -v opencode)
188
+ OPENCODE_BASE_WORKDIR=$DATA_DIR/opencode-workdir
189
+ EOF
190
+ chmod 600 "$DAEMON_ENV"
191
+ ok "Wrote $DAEMON_ENV"
192
+ }
193
+
194
+ write_unit_file() {
195
+ # $1=name $2=description $3=execstart $4=workdir $5=envfile $6=unit-file-path
196
+ local name="$1" desc="$2" execstart="$3" workdir="$4" envfile="$5" out="$6"
197
+ local extra_env="${7:-}"
198
+ cat > "$out" <<EOF
199
+ [Unit]
200
+ Description=$desc
201
+ After=network-online.target
202
+ Wants=network-online.target
203
+
204
+ [Service]
205
+ Type=simple
206
+ WorkingDirectory=$workdir
207
+ ExecStart=$execstart
208
+ Restart=on-failure
209
+ RestartSec=5
210
+ KillMode=process
211
+ EnvironmentFile=$envfile
212
+ Environment="PATH=/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin:$HOME/.bun/bin"
213
+ Environment="XDG_DATA_HOME=$XDG_DATA_HOME"
214
+ Environment="XDG_CONFIG_HOME=$XDG_CONFIG_HOME"
215
+ Environment="HOME=$HOME"$extra_env
216
+ StandardOutput=journal
217
+ StandardError=journal
218
+ SyslogIdentifier=$name
219
+
220
+ [Install]
221
+ ${SCOPENAME:+WantedBy=default.target}
222
+ EOF
223
+ # If system-level, WantedBy is multi-user.target
224
+ if [[ "$SCOPENAME" == "--system" ]]; then
225
+ sed -i 's/^WantedBy=default.target/WantedBy=multi-user.target/' "$out"
226
+ fi
227
+ }
228
+
229
+ ctl() { systemctl "$SCOPENAME" "$@"; }
230
+
231
+ # ─── Modes ──────────────────────────────────────────────────────────────────
232
+ case "$MODE" in
233
+ env)
234
+ hr; log "ework-aio paths"; hr
235
+ printf ' data dir : %s\n' "$DATA_DIR"
236
+ printf ' web env : %s\n' "$WEB_ENV"
237
+ printf ' daemon env : %s\n' "$DAEMON_ENV"
238
+ printf ' bot token : %s\n' "$BOT_TOKEN_FILE"
239
+ printf ' opencode cfg : %s\n' "$OPENCODE_CFG"
240
+ printf ' systemd scope : %s\n' "$SCOPENAME"
241
+ printf ' unit dir : %s\n' "$UNIT_DIR"
242
+ exit 0
243
+ ;;
244
+
245
+ status)
246
+ hr; log "ework-aio status ($SCOPENAME)"; hr
247
+ ctl is-active ework-web.service || true
248
+ ctl is-active ework-daemon.service || true
249
+ ctl status --no-pager --lines=0 ework-web.service 2>/dev/null || true
250
+ hr
251
+ ctl status --no-pager --lines=0 ework-daemon.service 2>/dev/null || true
252
+ exit 0
253
+ ;;
254
+
255
+ logs)
256
+ svc="${1:-ework-web.service}"
257
+ [[ "$svc" == "daemon" || "$svc" == "ework-daemon" ]] && svc="ework-daemon.service"
258
+ [[ "$svc" == "web" || "$svc" == "ework-web" ]] && svc="ework-web.service"
259
+ exec journalctl "$SCOPENAME" -u "$svc" -f
260
+ ;;
261
+
262
+ uninstall)
263
+ hr; log "Uninstalling ework-aio services (keeping data)"; hr
264
+ ctl stop ework-web.service ework-daemon.service 2>/dev/null || true
265
+ ctl disable ework-web.service ework-daemon.service 2>/dev/null || true
266
+ rm -f "$UNIT_DIR/ework-web.service" "$UNIT_DIR/ework-daemon.service"
267
+ ctl daemon-reload
268
+ ok "Services removed. Data preserved at $DATA_DIR"
269
+ warn "To fully remove: rm -rf $DATA_DIR && npm uninstall -g ework-aio ework-web ework-daemon opencode-ework"
270
+ exit 0
271
+ ;;
272
+ esac
273
+
274
+ # ─── Install mode ───────────────────────────────────────────────────────────
275
+ hr
276
+ log "ework-aio install"
277
+ log " scope : $SCOPENAME"
278
+ log " data dir : $DATA_DIR"
279
+ log " web bin : $EWORK_WEB_BIN"
280
+ log " daemon bin : $EWORK_DAEMON_BIN"
281
+ log " opencode : $(command -v opencode) ($($(command -v opencode) --version 2>&1 | head -1))"
282
+ hr
283
+
284
+ if [[ "$SCOPENAME" == "--user" ]] && ! loginctl show-user "$USER" 2>/dev/null | grep -q '^Linger=yes'; then
285
+ warn "User-level systemd requires lingering to keep services alive after logout."
286
+ if [[ "$ASSUME_YES" == "1" ]]; then
287
+ warn "Run this manually: sudo loginctl enable-linger $USER"
288
+ else
289
+ read -rp "Enable linger now? (needs sudo) [Y/n] " ans
290
+ if [[ "${ans:-Y}" =~ ^[Yy]?$ ]]; then
291
+ sudo loginctl enable-linger "$USER" || warn "enable-linger failed; services will stop on logout"
292
+ fi
293
+ fi
294
+ fi
295
+
296
+ mkdir -p "$WEB_DATA_DIR" "$DAEMON_DATA_DIR" "$DATA_DIR/opencode-workdir"
297
+
298
+ write_web_env
299
+
300
+ # ─── Write ework-web systemd unit ──────────────────────────────────────────
301
+ WEB_UNIT="$UNIT_DIR/ework-web.service"
302
+ write_unit_file \
303
+ "ework-web" \
304
+ "ework — multi-project issue tracker" \
305
+ "$EWORK_WEB_BIN" \
306
+ "$WEB_DATA_DIR" \
307
+ "$WEB_ENV" \
308
+ "$WEB_UNIT"
309
+ ok "Wrote $WEB_UNIT"
310
+
311
+ # ─── Reload + start ework-web ──────────────────────────────────────────────
312
+ ctl daemon-reload
313
+ if [[ "$NO_START" == "0" ]]; then
314
+ log "Starting ework-web..."
315
+ ctl enable ework-web.service
316
+ ctl restart ework-web.service
317
+ for i in $(seq 1 60); do
318
+ if curl -sf -o /dev/null "http://127.0.0.1:$WORK_PORT/login"; then
319
+ ok "ework-web listening on :$WORK_PORT (after ${i} half-seconds)"
320
+ break
321
+ fi
322
+ sleep 0.5
323
+ [[ $i -eq 60 ]] && die "ework-web did not come up in 30s. Check: journalctl $SCOPENAME -u ework-web.service -n 50"
324
+ done
325
+ else
326
+ warn "--no-start: unit enabled but not started"
327
+ ctl enable ework-web.service
328
+ fi
329
+
330
+ # ─── Bootstrap bot user + PAT (idempotent) ─────────────────────────────────
331
+ WORK_TOKEN_VAL=$(awk -F= '/^WORK_TOKEN=/{print $2}' "$WEB_ENV")
332
+ WORK_COOKIE_SECRET_VAL=$(awk -F= '/^WORK_COOKIE_SECRET=/{print $2}' "$WEB_ENV")
333
+ # ework-web's checkAuth accepts legacy "<token>.<hmac>" cookie form
334
+ COOKIE_SIG=$(printf '%s' "$WORK_TOKEN_VAL" \
335
+ | openssl dgst -sha256 -hmac "$WORK_COOKIE_SECRET_VAL" -binary \
336
+ | base64 | tr '+/' '-_' | tr -d '=')
337
+ AUTH_COOKIE="ework_auth=${WORK_TOKEN_VAL}.${COOKIE_SIG}"
338
+
339
+ BOT_TOKEN=""
340
+ if [[ -f "$BOT_TOKEN_FILE" ]]; then
341
+ BOT_TOKEN=$(cat "$BOT_TOKEN_FILE")
342
+ ok "Reusing saved bot token from $BOT_TOKEN_FILE"
343
+ else
344
+ log "Bootstrapping bot user '$BOT_NAME'..."
345
+ BOT_PW=$(openssl rand -hex 24)
346
+ CREATE_CODE=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
347
+ "http://127.0.0.1:$WORK_PORT/admin/users/create" \
348
+ -H "Cookie: $AUTH_COOKIE" \
349
+ --data-urlencode "login=$BOT_NAME" \
350
+ --data-urlencode "password=$BOT_PW" \
351
+ --data-urlencode "kind=bot" \
352
+ --data-urlencode "is_admin=0") || CREATE_CODE=000
353
+ case "$CREATE_CODE" in
354
+ 303) ok "Bot user '$BOT_NAME' created" ;;
355
+ 400|409) warn "Bot user '$BOT_NAME' already exists (continuing)" ;;
356
+ *) die "Failed to create bot user: HTTP $CREATE_CODE" ;;
357
+ esac
358
+
359
+ log "Logging in as bot to mint PAT..."
360
+ COOKIE_JAR=$(mktemp)
361
+ LOGIN_CODE=$(curl -sS -c "$COOKIE_JAR" -X POST "http://127.0.0.1:$WORK_PORT/login" \
362
+ --data-urlencode "login=$BOT_NAME" \
363
+ --data-urlencode "password=$BOT_PW" \
364
+ -o /dev/null -w '%{http_code}') || LOGIN_CODE=000
365
+ BOT_COOKIE=$(awk '/ework_auth/ {print $7}' "$COOKIE_JAR")
366
+ rm -f "$COOKIE_JAR"
367
+ [[ "$LOGIN_CODE" == "302" && -n "$BOT_COOKIE" ]] \
368
+ || die "Bot login failed: HTTP $LOGIN_CODE"
369
+
370
+ log "Minting PAT..."
371
+ PAT_RES=$(curl -sS -X POST "http://127.0.0.1:$WORK_PORT/me/tokens/create" \
372
+ -H "Cookie: ework_auth=$BOT_COOKIE" \
373
+ --data-urlencode "name=aio-$(date +%s)")
374
+ BOT_TOKEN=$(printf '%s' "$PAT_RES" | grep -oE 'id="t">[a-f0-9]{40}<' | grep -oE '[a-f0-9]{40}' | head -1 || true)
375
+ [[ -n "$BOT_TOKEN" ]] || die "Could not extract PAT from token-create response"
376
+ printf '%s' "$BOT_TOKEN" > "$BOT_TOKEN_FILE"
377
+ chmod 600 "$BOT_TOKEN_FILE"
378
+ ok "Bot PAT saved to $BOT_TOKEN_FILE"
379
+ fi
380
+
381
+ # ─── Write ework-daemon .env ───────────────────────────────────────────────
382
+ write_daemon_env "$BOT_TOKEN"
383
+
384
+ # ─── Write ework-daemon systemd unit ───────────────────────────────────────
385
+ DAEMON_UNIT="$UNIT_DIR/ework-daemon.service"
386
+ write_unit_file \
387
+ "ework-daemon" \
388
+ "ework-daemon — issue-driven AI dev daemon" \
389
+ "$EWORK_DAEMON_BIN" \
390
+ "$DAEMON_DATA_DIR" \
391
+ "$DAEMON_ENV" \
392
+ "$DAEMON_UNIT" \
393
+ " \"\""
394
+ # (trailing " \"\"" is a no-op separator; kept for future extra env injection)
395
+ ok "Wrote $DAEMON_UNIT"
396
+
397
+ ctl daemon-reload
398
+ if [[ "$NO_START" == "0" ]]; then
399
+ log "Starting ework-daemon..."
400
+ ctl enable ework-daemon.service
401
+ ctl restart ework-daemon.service
402
+ sleep 1
403
+ if ctl is-active --quiet ework-daemon.service; then
404
+ ok "ework-daemon active"
405
+ else
406
+ warn "ework-daemon did not report active; check: journalctl $SCOPENAME -u ework-daemon.service -n 50"
407
+ fi
408
+ else
409
+ ctl enable ework-daemon.service
410
+ fi
411
+
412
+ # ─── Register opencode-ework plugin ────────────────────────────────────────
413
+ mkdir -p "$(dirname "$OPENCODE_CFG")"
414
+ if [[ ! -f "$OPENCODE_CFG" ]]; then
415
+ log "Writing $OPENCODE_CFG (registering opencode-ework plugin)"
416
+ cat > "$OPENCODE_CFG" <<'EOF'
417
+ {
418
+ "$schema": "https://opencode.ai/config.json",
419
+ "plugin": ["opencode-ework"]
420
+ }
421
+ EOF
422
+ elif grep -q '"opencode-ework"' "$OPENCODE_CFG"; then
423
+ ok "opencode-ework already in $OPENCODE_CFG"
424
+ else
425
+ log "Merging opencode-ework into existing $OPENCODE_CFG"
426
+ cp "$OPENCODE_CFG" "$OPENCODE_CFG.bak.$(date +%s)"
427
+ # Use jq to append to plugin array (creates array if missing)
428
+ tmp=$(mktemp)
429
+ jq 'if .plugin then .plugin += ["opencode-ework"] else . + {plugin:["opencode-ework"]} end' \
430
+ "$OPENCODE_CFG" > "$tmp" && mv "$tmp" "$OPENCODE_CFG"
431
+ ok "Plugin registered (backup at $OPENCODE_CFG.bak.*)"
432
+ fi
433
+
434
+ # ─── Done ──────────────────────────────────────────────────────────────────
435
+ hr
436
+ ok "Install complete."
437
+ hr
438
+ printf '\n%s→%s Open %shttp://127.0.0.1:%s/login%s\n' \
439
+ "$c_bold" "$c_reset" "$c_dim" "$WORK_PORT" "$c_reset"
440
+ printf ' Login token: %s%s%s\n' "$c_bold" "$WORK_TOKEN_VAL" "$c_reset"
441
+ printf ' Logs: ework-aio logs web | ework-aio logs daemon\n'
442
+ printf ' Status: ework-aio status\n'
443
+ printf ' Uninstall: ework-aio uninstall\n'
444
+ hr
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "ework-aio",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "contributors",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/ranxianglei/ework-aio.git"
11
+ },
12
+ "homepage": "https://github.com/ranxianglei/ework-aio",
13
+ "bugs": {
14
+ "url": "https://github.com/ranxianglei/ework-aio/issues"
15
+ },
16
+ "keywords": [
17
+ "ework",
18
+ "opencode",
19
+ "issue-tracker",
20
+ "ai-agent",
21
+ "installer",
22
+ "self-hosted"
23
+ ],
24
+ "bin": {
25
+ "ework-aio": "./bin/ework-aio"
26
+ },
27
+ "files": [
28
+ "bin",
29
+ "src",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "engines": {
34
+ "bun": ">=1.1.0"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "start": "bun bin/ework-aio.js",
41
+ "check": "tsc --noEmit"
42
+ },
43
+ "dependencies": {
44
+ "ework-web": "^0.1.0",
45
+ "ework-daemon": "^0.1.0",
46
+ "opencode-ework": "^0.1.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/bun": "latest"
50
+ }
51
+ }