ework-aio 0.1.16 → 0.2.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/README.md +21 -20
- package/bin/ework-aio +3 -439
- package/package.json +6 -3
- package/src/cli.ts +467 -0
- package/src/commands/config.ts +208 -0
- package/src/commands/env.ts +27 -0
- package/src/commands/install.ts +637 -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 +219 -0
- package/src/log.ts +97 -0
- package/src/opencode-config.ts +104 -0
- package/src/paths.ts +88 -0
- package/src/pidfile.ts +164 -0
- package/src/preflight.ts +48 -0
- package/src/systemd.ts +234 -0
- package/src/types.ts +95 -0
- package/bin/install.sh +0 -1146
package/README.md
CHANGED
|
@@ -48,7 +48,8 @@ The install command checks for these and aborts with a hint if any are missing:
|
|
|
48
48
|
| `opencode` | 1.14 | always | https://opencode.ai |
|
|
49
49
|
| `npm` | any | always | ships with bun or node |
|
|
50
50
|
| `systemctl` | any | only `install systemd` (not default) | systemd-based Linux |
|
|
51
|
-
|
|
51
|
+
|
|
52
|
+
Everything else (HTTP requests, AES/HMAC, JSON parsing, env-file editing) is done in-process via Bun — no `curl`/`openssl`/`jq`/`awk` needed.
|
|
52
53
|
|
|
53
54
|
### Install variants
|
|
54
55
|
|
|
@@ -101,7 +102,7 @@ Keeping these in a separate, explicitly-invoked step is intentional:
|
|
|
101
102
|
|
|
102
103
|
## What it does
|
|
103
104
|
|
|
104
|
-
1. **Verifies prerequisites** (bun, npm, opencode
|
|
105
|
+
1. **Verifies prerequisites** (bun, npm, opencode; systemctl only if `install systemd`).
|
|
105
106
|
2. **Installs the 3 npm packages** globally (if not already present).
|
|
106
107
|
3. **Generates `.env`** with random tokens at `~/.local/share/ework-aio/{ework-web,ework-daemon}/.env` (preserved across re-runs).
|
|
107
108
|
4. **Writes systemd units** (`ework-web.service`, `ework-daemon.service`) — user-level by default, system-level when run as root.
|
|
@@ -116,7 +117,7 @@ Keeping these in a separate, explicitly-invoked step is intentional:
|
|
|
116
117
|
```
|
|
117
118
|
ework-aio install [options] Install or upgrade the stack
|
|
118
119
|
ework-aio uninstall Stop services and remove units (data preserved)
|
|
119
|
-
ework-aio status Show service status (systemd mode)
|
|
120
|
+
ework-aio status Show service status (works in PID-file and systemd mode)
|
|
120
121
|
ework-aio logs [web|daemon] Tail logs
|
|
121
122
|
ework-aio env Print key paths (no secrets)
|
|
122
123
|
ework-aio config <subcommand> Read / change runtime .env keys
|
|
@@ -127,24 +128,25 @@ ework-aio config <subcommand> Read / change runtime .env keys
|
|
|
127
128
|
config restart <web|daemon|both> Restart one or both services
|
|
128
129
|
|
|
129
130
|
# PID-file mode (no systemd required — see below)
|
|
130
|
-
ework-aio start [web|daemon|both]
|
|
131
|
-
ework-aio stop [web|daemon|both]
|
|
132
|
-
ework-aio restart [web|daemon|both]
|
|
133
|
-
ework-aio ps
|
|
131
|
+
ework-aio start [web|daemon|both] Start services in background (detached)
|
|
132
|
+
ework-aio stop [web|daemon|both] Stop services (SIGTERM, 5s, SIGKILL)
|
|
133
|
+
ework-aio restart [web|daemon|both] Stop + start
|
|
134
|
+
ework-aio ps Show PID-file mode status
|
|
134
135
|
```
|
|
135
136
|
|
|
136
137
|
### Install options
|
|
137
138
|
|
|
138
|
-
| Flag | Default | Description
|
|
139
|
-
| ----------------------- | ------------------------------------ |
|
|
140
|
-
| `--user` |
|
|
141
|
-
| `--system` |
|
|
142
|
-
| `--data-dir <path>` | `~/.local/share/ework-aio` | Override data root
|
|
143
|
-
| `--port <n>` | `3002` | ework-web port
|
|
144
|
-
| `--daemon-port <n>` | `3101` | ework-daemon port
|
|
145
|
-
| `--bot-name <login>` | `ework-daemon` | Bot username in ework-web
|
|
146
|
-
| `--no-start` | (off) | Install
|
|
147
|
-
| `--yes`
|
|
139
|
+
| Flag | Default | Description |
|
|
140
|
+
| ----------------------- | ------------------------------------ | ----------------------------------------------------------------- |
|
|
141
|
+
| `--user` | default when EUID≠0 | Use `systemctl --user` (only with `install systemd` subcommand) |
|
|
142
|
+
| `--system` | default when EUID=0 | Use `systemctl` system-level (only with `install systemd`, sudo) |
|
|
143
|
+
| `--data-dir <path>` | `~/.local/share/ework-aio` | Override data root |
|
|
144
|
+
| `--port <n>` | `3002` | ework-web port |
|
|
145
|
+
| `--daemon-port <n>` | `3101` | ework-daemon port |
|
|
146
|
+
| `--bot-name <login>` | `ework-daemon` | Bot username in ework-web |
|
|
147
|
+
| `--no-start` | (off) | Install but don't start services |
|
|
148
|
+
| `--yes` / `-y` | (off) | Skip prompts (use generated defaults) |
|
|
149
|
+
| `--as-user <login>` | (off) | **sudo only:** drop privileges — re-exec install as `<login>` |
|
|
148
150
|
|
|
149
151
|
## File layout
|
|
150
152
|
|
|
@@ -258,15 +260,14 @@ PID-file mode writes per-service PID + log under `~/.local/share/ework-aio/run/`
|
|
|
258
260
|
ework-aio install --no-start
|
|
259
261
|
|
|
260
262
|
# Then control with PID-file mode:
|
|
261
|
-
ework-aio start # start web + daemon in background
|
|
263
|
+
ework-aio start # start web + daemon in background (detached)
|
|
262
264
|
ework-aio start web # just web
|
|
263
|
-
ework-aio start daemon --foreground # run in current terminal
|
|
264
265
|
ework-aio stop # SIGTERM, 5s grace, SIGKILL if needed
|
|
265
266
|
ework-aio restart web # stop + start
|
|
266
267
|
ework-aio ps # show PID + log path for each service
|
|
267
268
|
```
|
|
268
269
|
|
|
269
|
-
Override the data dir with `
|
|
270
|
+
Override the data dir with `ework-aio start --data-dir /path` (or pass `--data-dir` to `install`).
|
|
270
271
|
|
|
271
272
|
**Notes**:
|
|
272
273
|
- `install` (with `--no-start`) is still required once to scaffold `.env` and the data directory. The systemd units it writes are inert when nothing manages them.
|
package/bin/ework-aio
CHANGED
|
@@ -1,441 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
chmodSync,
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
rmSync,
|
|
9
|
-
writeFileSync,
|
|
10
|
-
} from "fs";
|
|
11
|
-
import { dirname, join, resolve } from "path";
|
|
12
|
-
import { fileURLToPath } from "url";
|
|
2
|
+
import { main } from "../src/cli.ts";
|
|
13
3
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const args = process.argv.slice(2);
|
|
17
|
-
const subcommand = args[0] || "install";
|
|
18
|
-
|
|
19
|
-
function usage() {
|
|
20
|
-
console.log(`ework-aio — installer and controller for the ework stack
|
|
21
|
-
|
|
22
|
-
Usage:
|
|
23
|
-
ework-aio install [options] Install and start ework-web + ework-daemon
|
|
24
|
-
(uses systemd; see start/stop for non-systemd)
|
|
25
|
-
ework-aio uninstall Stop services and remove units (keeps data)
|
|
26
|
-
ework-aio status Show service status (systemd mode)
|
|
27
|
-
ework-aio logs [svc] Tail logs (svc: web | daemon)
|
|
28
|
-
ework-aio env Print the .env path and key names (no values)
|
|
29
|
-
ework-aio config <subcommand> Read / change runtime .env keys
|
|
30
|
-
config list List settable keys + current values
|
|
31
|
-
config get <KEY> Print one key's value
|
|
32
|
-
config set <KEY> <VALUE> Set a key, then restart affected service
|
|
33
|
-
(use --no-restart to defer)
|
|
34
|
-
config restart <web|daemon|both>
|
|
35
|
-
ework-aio start [svc] Start services in background WITHOUT systemd
|
|
36
|
-
(PID-file mode, works on WSL/macOS/Alpine).
|
|
37
|
-
svc: web | daemon | both (default both).
|
|
38
|
-
--foreground runs in current terminal.
|
|
39
|
-
ework-aio stop [svc] Stop services started via 'start'
|
|
40
|
-
(SIGTERM, 5s grace, then SIGKILL)
|
|
41
|
-
ework-aio restart [svc] Stop + start
|
|
42
|
-
ework-aio ps Show PID-file mode status (web+daemon)
|
|
43
|
-
ework-aio migrate [options] Migrate issues from a Gitea instance into
|
|
44
|
-
ework-web via the Gitea REST API. Idempotent.
|
|
45
|
-
Run \`ework-aio migrate --help\` for details.
|
|
46
|
-
ework-aio backfill-timestamps Fix created_at/updated_at on already-migrated
|
|
47
|
-
data. Use after migrate when ework.db is local.
|
|
48
|
-
ework-aio --version Print version
|
|
49
|
-
|
|
50
|
-
Install options:
|
|
51
|
-
--user Use user-level systemd units (default if non-root)
|
|
52
|
-
--system Use system-level systemd units (default if root)
|
|
53
|
-
--data-dir <path> Override data directory (default: ~/.local/share/ework-aio)
|
|
54
|
-
--port <n> ework-web port (default 3002)
|
|
55
|
-
--daemon-port <n> ework-daemon port (default 3101)
|
|
56
|
-
--bot-name <login> Bot username (default: ework-daemon)
|
|
57
|
-
--no-start Install units but don't start services
|
|
58
|
-
--yes Skip all prompts (use generated defaults)
|
|
59
|
-
|
|
60
|
-
PID-file mode (--foreground works with start):
|
|
61
|
-
--data-dir <path> Same as install option
|
|
62
|
-
`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
66
|
-
usage();
|
|
67
|
-
process.exit(0);
|
|
68
|
-
}
|
|
69
|
-
if (subcommand === "--version" || subcommand === "-v") {
|
|
70
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
|
|
71
|
-
console.log(`ework-aio ${pkg.version}`);
|
|
72
|
-
process.exit(0);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const installSh = resolve(__dirname, "install.sh");
|
|
76
|
-
if (!existsSync(installSh)) {
|
|
77
|
-
console.error(`Cannot find install.sh next to ework-aio bin (looked at ${installSh}).`);
|
|
78
|
-
console.error("This usually means the package was installed incorrectly.");
|
|
79
|
-
process.exit(1);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const knownSubs = ["install", "uninstall", "status", "logs", "env", "config"];
|
|
83
|
-
|
|
84
|
-
// ─── PID-file mode (no systemd required) ───────────────────────────────────
|
|
85
|
-
//
|
|
86
|
-
// start/stop/restart/ps bypass install.sh and the systemd dependency entirely.
|
|
87
|
-
// Each service writes its PID + log under $DATA_DIR/run/, managed via the
|
|
88
|
-
// standard node:child_process detached-spawn pattern (sets the child free of
|
|
89
|
-
// the parent's process group so the parent can exit while the service keeps
|
|
90
|
-
// running). This is the path for systems without systemd: macOS, WSL1, Alpine,
|
|
91
|
-
// containers, dev laptops.
|
|
92
|
-
|
|
93
|
-
const SVC_CONFIG = {
|
|
94
|
-
web: {
|
|
95
|
-
binName: "ework-web",
|
|
96
|
-
binFallback: "ework-web/bin/ework-web.js",
|
|
97
|
-
dataSubdir: "ework-web",
|
|
98
|
-
label: "ework-web",
|
|
99
|
-
},
|
|
100
|
-
daemon: {
|
|
101
|
-
binName: "ework-daemon-server",
|
|
102
|
-
binFallback: "ework-daemon/bin/ework-daemon-server.js",
|
|
103
|
-
dataSubdir: "ework-daemon",
|
|
104
|
-
label: "ework-daemon",
|
|
105
|
-
},
|
|
106
|
-
} as const;
|
|
107
|
-
|
|
108
|
-
type SvcName = keyof typeof SVC_CONFIG;
|
|
109
|
-
|
|
110
|
-
function resolveDataDir(): string {
|
|
111
|
-
const xdg = process.env.XDG_DATA_HOME || `${process.env.HOME}/.local/share`;
|
|
112
|
-
return process.env.EWORK_AIO_DATA_DIR || `${xdg}/ework-aio`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function runDir(): string {
|
|
116
|
-
const d = join(resolveDataDir(), "run");
|
|
117
|
-
if (!existsSync(d)) mkdirSync(d, { recursive: true });
|
|
118
|
-
return d;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function svcEnvPath(svc: SvcName): string {
|
|
122
|
-
return join(resolveDataDir(), SVC_CONFIG[svc].dataSubdir, ".env");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function svcPidPath(svc: SvcName): string {
|
|
126
|
-
return join(runDir(), `${svc}.pid`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function svcLogPath(svc: SvcName): string {
|
|
130
|
-
return join(runDir(), `${svc}.log`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// KEY=VALUE parser, ignores comments + blanks, never throws.
|
|
134
|
-
function parseEnvFile(path: string): Record<string, string> {
|
|
135
|
-
const out: Record<string, string> = {};
|
|
136
|
-
if (!existsSync(path)) return out;
|
|
137
|
-
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
138
|
-
const trimmed = line.trim();
|
|
139
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
140
|
-
const eq = trimmed.indexOf("=");
|
|
141
|
-
if (eq < 0) continue;
|
|
142
|
-
const k = trimmed.slice(0, eq).trim();
|
|
143
|
-
let v = trimmed.slice(eq + 1).trim();
|
|
144
|
-
if (
|
|
145
|
-
(v.startsWith('"') && v.endsWith('"')) ||
|
|
146
|
-
(v.startsWith("'") && v.endsWith("'"))
|
|
147
|
-
) {
|
|
148
|
-
v = v.slice(1, -1);
|
|
149
|
-
}
|
|
150
|
-
if (k) out[k] = v;
|
|
151
|
-
}
|
|
152
|
-
return out;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function resolveSvcBin(svc: SvcName): string | null {
|
|
156
|
-
const cfg = SVC_CONFIG[svc];
|
|
157
|
-
// PATH lookup first (covers PATH-installed global bins). `command -v` is a
|
|
158
|
-
// shell builtin so we have to invoke via sh -c.
|
|
159
|
-
const lookup = spawnSync(["sh", "-c", `command -v ${cfg.binName}`], {
|
|
160
|
-
env: process.env,
|
|
161
|
-
stdout: "pipe",
|
|
162
|
-
stderr: "ignore",
|
|
163
|
-
});
|
|
164
|
-
if (lookup.success) {
|
|
165
|
-
const p = lookup.stdout.toString().trim();
|
|
166
|
-
if (p && existsSync(p)) return p;
|
|
167
|
-
}
|
|
168
|
-
// npm global root fallback — covers npm-installed packages whose bin dir
|
|
169
|
-
// isn't on PATH (common with nvm-style installs).
|
|
170
|
-
const npmRoot = spawnSync(["npm", "root", "-g"], {
|
|
171
|
-
env: process.env,
|
|
172
|
-
stdout: "pipe",
|
|
173
|
-
stderr: "ignore",
|
|
174
|
-
});
|
|
175
|
-
if (npmRoot.success) {
|
|
176
|
-
const fallback = join(npmRoot.stdout.toString().trim(), cfg.binFallback);
|
|
177
|
-
if (existsSync(fallback)) return fallback;
|
|
178
|
-
}
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function pidAlive(pid: number): boolean {
|
|
183
|
-
if (!pid || pid <= 0) return false;
|
|
184
|
-
try {
|
|
185
|
-
process.kill(pid, 0);
|
|
186
|
-
return true;
|
|
187
|
-
} catch {
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function readPid(svc: SvcName): number | null {
|
|
193
|
-
const p = svcPidPath(svc);
|
|
194
|
-
if (!existsSync(p)) return null;
|
|
195
|
-
const raw = readFileSync(p, "utf8").trim();
|
|
196
|
-
const n = Number(raw);
|
|
197
|
-
return Number.isFinite(n) && n > 0 ? n : null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function writePid(svc: SvcName, pid: number): void {
|
|
201
|
-
writeFileSync(svcPidPath(svc), `${pid}\n`, { encoding: "utf8" });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function clearPid(svc: SvcName): void {
|
|
205
|
-
const p = svcPidPath(svc);
|
|
206
|
-
if (existsSync(p)) rmSync(p);
|
|
207
|
-
|
|
208
|
-
const staleSocket = join(runDir(), `${svc}.port`);
|
|
209
|
-
if (existsSync(staleSocket)) rmSync(staleSocket);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function waitForExit(pid: number, timeoutMs: number): boolean {
|
|
213
|
-
const deadline = Date.now() + timeoutMs;
|
|
214
|
-
while (Date.now() < deadline) {
|
|
215
|
-
if (!pidAlive(pid)) return true;
|
|
216
|
-
const slept = Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
217
|
-
void slept;
|
|
218
|
-
}
|
|
219
|
-
return !pidAlive(pid);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function parseSvcArg(argv: string[]): { svc: "both" | SvcName; foreground: boolean } {
|
|
223
|
-
let svc: "both" | SvcName = "both";
|
|
224
|
-
let foreground = false;
|
|
225
|
-
for (const a of argv.slice(1)) {
|
|
226
|
-
if (a === "--foreground" || a === "-f") foreground = true;
|
|
227
|
-
else if (a === "web" || a === "daemon") svc = a;
|
|
228
|
-
else if (a === "both") svc = "both";
|
|
229
|
-
else {
|
|
230
|
-
console.error(`Unknown argument: ${a}`);
|
|
231
|
-
console.error("Usage: ework-aio <start|stop|restart> [web|daemon|both] [--foreground]");
|
|
232
|
-
process.exit(2);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return { svc, foreground };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function startOne(svc: SvcName, foreground: boolean): boolean {
|
|
239
|
-
const existing = readPid(svc);
|
|
240
|
-
if (existing && pidAlive(existing)) {
|
|
241
|
-
console.log(`${SVC_CONFIG[svc].label} already running (pid ${existing})`);
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
if (existing) clearPid(svc);
|
|
245
|
-
|
|
246
|
-
const bin = resolveSvcBin(svc);
|
|
247
|
-
if (!bin) {
|
|
248
|
-
console.error(
|
|
249
|
-
`${SVC_CONFIG[svc].label}: cannot find bin '${SVC_CONFIG[svc].binName}' in PATH or npm global root.`
|
|
250
|
-
);
|
|
251
|
-
console.error(` Install with: npm i -g ${SVC_CONFIG[svc].binName.split("-")[0]}`);
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
const envPath = svcEnvPath(svc);
|
|
255
|
-
if (!existsSync(envPath)) {
|
|
256
|
-
console.error(`${SVC_CONFIG[svc].label}: missing .env at ${envPath}.`);
|
|
257
|
-
console.error(" Run `ework-aio install` first to scaffold config + data dir.");
|
|
258
|
-
console.error(" If install fails with npm ENOTEMPTY, a stale temp dir is blocking it.");
|
|
259
|
-
console.error(" Manual fix: sudo rm -rf \"$(npm root -g)/.ework-*\" \"$(npm root -g)\"/*.Trash*");
|
|
260
|
-
console.error(" Then re-run: ework-aio install");
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Sane perms on the bin (npm sometimes ships without +x).
|
|
265
|
-
try {
|
|
266
|
-
chmodSync(bin, 0o755);
|
|
267
|
-
} catch {
|
|
268
|
-
// best-effort
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const mergedEnv = { ...process.env, ...parseEnvFile(envPath) };
|
|
272
|
-
const logPath = svcLogPath(svc);
|
|
273
|
-
|
|
274
|
-
if (foreground) {
|
|
275
|
-
console.log(`Starting ${SVC_CONFIG[svc].label} in foreground (Ctrl+C to stop)`);
|
|
276
|
-
const r = spawnSync([bin], {
|
|
277
|
-
stdio: ["inherit", "inherit", "inherit"],
|
|
278
|
-
env: mergedEnv,
|
|
279
|
-
});
|
|
280
|
-
return r.status === 0;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Background: detached spawn via node:child_process so the parent can exit
|
|
284
|
-
// cleanly while the service keeps running. stdout+stderr append to logPath.
|
|
285
|
-
const { spawn: nodeSpawn } = require("node:child_process");
|
|
286
|
-
const { openSync } = require("node:fs");
|
|
287
|
-
const logFd = openSync(logPath, "a");
|
|
288
|
-
const child = nodeSpawn(bin, [], {
|
|
289
|
-
env: mergedEnv,
|
|
290
|
-
detached: true,
|
|
291
|
-
stdio: ["ignore", logFd, logFd],
|
|
292
|
-
});
|
|
293
|
-
child.unref();
|
|
294
|
-
if (!child.pid) {
|
|
295
|
-
console.error(`${SVC_CONFIG[svc].label}: spawn failed`);
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
writePid(svc, child.pid);
|
|
299
|
-
console.log(`${SVC_CONFIG[svc].label} started (pid ${child.pid}, log ${logPath})`);
|
|
300
|
-
|
|
301
|
-
// Grace window: if the process dies within 800ms it almost certainly failed
|
|
302
|
-
// to start (bad env, port in use, missing dep). Surface the last log lines.
|
|
303
|
-
const start = Date.now();
|
|
304
|
-
while (Date.now() - start < 800) {
|
|
305
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 80);
|
|
306
|
-
}
|
|
307
|
-
if (!pidAlive(child.pid)) {
|
|
308
|
-
console.error(`${SVC_CONFIG[svc].label}: exited within 800ms — likely a startup error.`);
|
|
309
|
-
console.error(` Tail of ${logPath}:`);
|
|
310
|
-
const tail = spawnSync(["tail", "-n", "20", logPath], { stdout: "pipe" });
|
|
311
|
-
if (tail.status === 0) console.error(tail.stdout.toString());
|
|
312
|
-
clearPid(svc);
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function stopOne(svc: SvcName): boolean {
|
|
319
|
-
const pid = readPid(svc);
|
|
320
|
-
if (!pid) {
|
|
321
|
-
console.log(`${SVC_CONFIG[svc].label} not running (no PID file)`);
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
if (!pidAlive(pid)) {
|
|
325
|
-
console.log(`${SVC_CONFIG[svc].label} not running (stale PID file, removing)`);
|
|
326
|
-
clearPid(svc);
|
|
327
|
-
return true;
|
|
328
|
-
}
|
|
329
|
-
try {
|
|
330
|
-
process.kill(pid, "SIGTERM");
|
|
331
|
-
} catch (e) {
|
|
332
|
-
console.error(`${SVC_CONFIG[svc].label}: SIGTERM failed: ${(e as Error).message}`);
|
|
333
|
-
return false;
|
|
334
|
-
}
|
|
335
|
-
console.log(`${SVC_CONFIG[svc].label}: SIGTERM sent (pid ${pid})`);
|
|
336
|
-
if (waitForExit(pid, 5000)) {
|
|
337
|
-
clearPid(svc);
|
|
338
|
-
console.log(`${SVC_CONFIG[svc].label} stopped`);
|
|
339
|
-
return true;
|
|
340
|
-
}
|
|
341
|
-
console.warn(`${SVC_CONFIG[svc].label}: did not exit in 5s, sending SIGKILL`);
|
|
342
|
-
try {
|
|
343
|
-
process.kill(pid, "SIGKILL");
|
|
344
|
-
} catch {
|
|
345
|
-
// best-effort
|
|
346
|
-
}
|
|
347
|
-
if (waitForExit(pid, 2000)) {
|
|
348
|
-
clearPid(svc);
|
|
349
|
-
console.log(`${SVC_CONFIG[svc].label} killed`);
|
|
350
|
-
return true;
|
|
351
|
-
}
|
|
352
|
-
console.error(`${SVC_CONFIG[svc].label}: failed to kill pid ${pid}`);
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function psAll(): void {
|
|
357
|
-
console.log("ework-aio PID-file mode status:");
|
|
358
|
-
for (const svc of Object.keys(SVC_CONFIG) as SvcName[]) {
|
|
359
|
-
const pid = readPid(svc);
|
|
360
|
-
const alive = pid !== null && pidAlive(pid);
|
|
361
|
-
const cfg = SVC_CONFIG[svc];
|
|
362
|
-
if (alive) {
|
|
363
|
-
console.log(` ${cfg.label.padEnd(18)} running pid ${pid} log ${svcLogPath(svc)}`);
|
|
364
|
-
} else if (pid !== null) {
|
|
365
|
-
console.log(` ${cfg.label.padEnd(18)} dead stale pid ${pid} (run: ework-aio stop ${svc})`);
|
|
366
|
-
} else {
|
|
367
|
-
console.log(` ${cfg.label.padEnd(18)} stopped`);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function runPidMode(command: "start" | "stop" | "restart", argv: string[]): number {
|
|
373
|
-
const { svc, foreground } = parseSvcArg(argv);
|
|
374
|
-
if (command === "ps") {
|
|
375
|
-
psAll();
|
|
376
|
-
return 0;
|
|
377
|
-
}
|
|
378
|
-
const targets: SvcName[] = svc === "both" ? ["web", "daemon"] : [svc];
|
|
379
|
-
let rc = 0;
|
|
380
|
-
if (command === "stop") {
|
|
381
|
-
for (const s of targets) if (!stopOne(s)) rc = 1;
|
|
382
|
-
return rc;
|
|
383
|
-
}
|
|
384
|
-
if (command === "start") {
|
|
385
|
-
for (const s of targets) if (!startOne(s, foreground)) rc = 1;
|
|
386
|
-
return rc;
|
|
387
|
-
}
|
|
388
|
-
// restart
|
|
389
|
-
for (const s of targets) stopOne(s);
|
|
390
|
-
for (const s of targets) if (!startOne(s, foreground)) rc = 1;
|
|
391
|
-
return rc;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
subcommand === "start" ||
|
|
396
|
-
subcommand === "stop" ||
|
|
397
|
-
subcommand === "restart" ||
|
|
398
|
-
subcommand === "ps"
|
|
399
|
-
) {
|
|
400
|
-
process.exit(runPidMode(subcommand as "start" | "stop" | "restart", args));
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (subcommand === "migrate") {
|
|
404
|
-
const migrateScript = resolve(__dirname, "..", "scripts", "migrate-from-gitea.ts");
|
|
405
|
-
if (!existsSync(migrateScript)) {
|
|
406
|
-
console.error(`Cannot find migrate script (looked at ${migrateScript}).`);
|
|
407
|
-
console.error("This usually means the package was installed incorrectly.");
|
|
408
|
-
process.exit(1);
|
|
409
|
-
}
|
|
410
|
-
const rest = args.slice(1);
|
|
411
|
-
const result = spawnSync(["bun", migrateScript, ...rest], {
|
|
412
|
-
stdio: ["inherit", "inherit", "inherit"],
|
|
413
|
-
env: process.env,
|
|
414
|
-
});
|
|
415
|
-
if (result.status !== null) process.exit(result.status);
|
|
416
|
-
process.exit(1);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (subcommand === "backfill-timestamps") {
|
|
420
|
-
const script = resolve(__dirname, "..", "scripts", "backfill-timestamps.ts");
|
|
421
|
-
if (!existsSync(script)) {
|
|
422
|
-
console.error(`Cannot find backfill-timestamps script (looked at ${script}).`);
|
|
423
|
-
process.exit(1);
|
|
424
|
-
}
|
|
425
|
-
const rest = args.slice(1);
|
|
426
|
-
const result = spawnSync(["bun", script, ...rest], {
|
|
427
|
-
stdio: ["inherit", "inherit", "inherit"],
|
|
428
|
-
env: process.env,
|
|
429
|
-
});
|
|
430
|
-
if (result.status !== null) process.exit(result.status);
|
|
431
|
-
process.exit(1);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const finalArgs = knownSubs.includes(subcommand) ? args : ["install", ...args];
|
|
435
|
-
|
|
436
|
-
const result = spawnSync(["bash", installSh, ...finalArgs], {
|
|
437
|
-
stdio: ["inherit", "inherit", "inherit"],
|
|
438
|
-
env: process.env,
|
|
439
|
-
});
|
|
440
|
-
if (result.status !== null) process.exit(result.status);
|
|
441
|
-
process.exit(1);
|
|
4
|
+
const argv = process.argv.slice(2);
|
|
5
|
+
main(argv).then((code) => process.exit(code));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ework-aio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "All-in-one installer for ework (issue tracker) + ework-daemon (AI bridge) + opencode-ework (plugin). One command: npm i -g ework-aio && ework-aio install.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,14 +40,17 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"start": "bun bin/ework-aio.js",
|
|
42
42
|
"check": "tsc --noEmit",
|
|
43
|
+
"test": "bun test",
|
|
43
44
|
"postinstall": "echo '\\n ework-aio installed. Run \\033[1mework-aio install\\033[0m to set up services.\\n Help: ework-aio --help\\n'"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"ework-web": "^0.1.0",
|
|
47
48
|
"ework-daemon": "^0.1.0",
|
|
48
|
-
"opencode-ework": "^0.1.0"
|
|
49
|
+
"opencode-ework": "^0.1.0",
|
|
50
|
+
"zod": "^3.23.0"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
|
-
"@types/bun": "latest"
|
|
53
|
+
"@types/bun": "latest",
|
|
54
|
+
"typescript": "^5.4.0"
|
|
52
55
|
}
|
|
53
56
|
}
|