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 +21 -0
- package/README.md +109 -0
- package/bin/ework-aio +60 -0
- package/bin/install.sh +444 -0
- package/package.json +51 -0
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
|
+
}
|