ework-aio 0.1.7 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -100,7 +100,7 @@ Keeping these in a separate, explicitly-invoked step is intentional:
100
100
  ```
101
101
  ework-aio install [options] Install or upgrade the stack
102
102
  ework-aio uninstall Stop services and remove units (data preserved)
103
- ework-aio status Show service status
103
+ ework-aio status Show service status (systemd mode)
104
104
  ework-aio logs [web|daemon] Tail logs
105
105
  ework-aio env Print key paths (no secrets)
106
106
  ework-aio config <subcommand> Read / change runtime .env keys
@@ -109,6 +109,12 @@ ework-aio config <subcommand> Read / change runtime .env keys
109
109
  config set <KEY> <VALUE> Set a key, then restart affected service
110
110
  (use --no-restart to defer)
111
111
  config restart <web|daemon|both> Restart one or both services
112
+
113
+ # PID-file mode (no systemd required — see below)
114
+ ework-aio start [web|daemon|both] [--foreground] Start services in background
115
+ ework-aio stop [web|daemon|both] Stop services (SIGTERM, 5s, SIGKILL)
116
+ ework-aio restart [web|daemon|both] Stop + start
117
+ ework-aio ps Show PID-file mode status
112
118
  ```
113
119
 
114
120
  ### Install options
@@ -212,6 +218,45 @@ npm uninstall -g ework-aio ework-web ework-daemon opencode-ework
212
218
  # Remove the plugin entry from ~/.config/opencode/opencode.json manually
213
219
  ```
214
220
 
221
+ ## PID-file mode (no systemd)
222
+
223
+ `install` / `status` / `config` / `uninstall` go through `systemctl`, which assumes systemd. If you're on a system without systemd — **macOS, WSL1, Alpine without systemd, Docker containers, dev laptops** — use the parallel PID-file mode instead:
224
+
225
+ | Mode | Start | Stop | Status | Restart |
226
+ | ----------- | ----------------------------- | ----------------------------- | ------------------- | -------------------------------- |
227
+ | systemd | `ework-aio install` | `ework-aio uninstall` | `ework-aio status` | `ework-aio config restart both` |
228
+ | **PID-file**| `ework-aio start` | `ework-aio stop` | `ework-aio ps` | `ework-aio restart` |
229
+
230
+ PID-file mode writes per-service PID + log under `~/.local/share/ework-aio/run/`:
231
+
232
+ ```
233
+ ~/.local/share/ework-aio/run/
234
+ ├── web.pid # ework-web PID
235
+ ├── web.log # nohup stdout+stderr
236
+ ├── daemon.pid
237
+ └── daemon.log
238
+ ```
239
+
240
+ ```bash
241
+ # One-time setup: scaffold .env + data dir (no systemd required if you skip the unit install)
242
+ ework-aio install --no-start
243
+
244
+ # Then control with PID-file mode:
245
+ ework-aio start # start web + daemon in background
246
+ ework-aio start web # just web
247
+ ework-aio start daemon --foreground # run in current terminal
248
+ ework-aio stop # SIGTERM, 5s grace, SIGKILL if needed
249
+ ework-aio restart web # stop + start
250
+ ework-aio ps # show PID + log path for each service
251
+ ```
252
+
253
+ Override the data dir with `EWORK_AIO_DATA_DIR=/path ework-aio start`.
254
+
255
+ **Notes**:
256
+ - `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.
257
+ - PID-file mode does NOT auto-restart on crash. Consider a process supervisor (`pm2`, `supervisord`, `runit`, `launchd`) if you need that.
258
+ - Both modes can coexist (systemd-managed services ignore PID files, and vice versa) but you should pick one to avoid port conflicts.
259
+
215
260
  ## Alternatives
216
261
 
217
262
  - **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.
package/bin/ework-aio CHANGED
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env bun
2
2
  import { spawn, spawnSync } from "bun";
3
- import { existsSync, readFileSync } from "fs";
4
- import { resolve, dirname, join } from "path";
3
+ import {
4
+ chmodSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from "fs";
11
+ import { dirname, join, resolve } from "path";
5
12
  import { fileURLToPath } from "url";
6
13
 
7
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -14,8 +21,9 @@ function usage() {
14
21
 
15
22
  Usage:
16
23
  ework-aio install [options] Install and start ework-web + ework-daemon
24
+ (uses systemd; see start/stop for non-systemd)
17
25
  ework-aio uninstall Stop services and remove units (keeps data)
18
- ework-aio status Show service status
26
+ ework-aio status Show service status (systemd mode)
19
27
  ework-aio logs [svc] Tail logs (svc: web | daemon)
20
28
  ework-aio env Print the .env path and key names (no values)
21
29
  ework-aio config <subcommand> Read / change runtime .env keys
@@ -24,6 +32,19 @@ Usage:
24
32
  config set <KEY> <VALUE> Set a key, then restart affected service
25
33
  (use --no-restart to defer)
26
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.
27
48
  ework-aio --version Print version
28
49
 
29
50
  Install options:
@@ -35,6 +56,9 @@ Install options:
35
56
  --bot-name <login> Bot username (default: ework-daemon)
36
57
  --no-start Install units but don't start services
37
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
38
62
  `);
39
63
  }
40
64
 
@@ -56,6 +80,357 @@ if (!existsSync(installSh)) {
56
80
  }
57
81
 
58
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
+
59
434
  const finalArgs = knownSubs.includes(subcommand) ? args : ["install", ...args];
60
435
 
61
436
  const result = spawnSync(["bash", installSh, ...finalArgs], {
package/bin/install.sh CHANGED
@@ -162,12 +162,59 @@ ensure_pkg() {
162
162
  }
163
163
  case "$MODE" in
164
164
  install)
165
+ # Pre-clean stale npm rename temp-dirs from any previous failed install.
166
+ # npm's atomic-install pattern renames the existing package dir to a
167
+ # `.<pkg>-<rand>` sibling before extracting the new tarball; if a prior
168
+ # install crashed mid-flight, that temp dir stays behind and the NEXT
169
+ # install fails with ENOTEMPTY on the rename. Symptoms:
170
+ # npm error code ENOTEMPTY
171
+ # npm error syscall rename
172
+ # npm error path .../node_modules/ework-web
173
+ # npm error dest .../node_modules/.ework-web-Cx2Tkt83
174
+ clean_npm_stale_dirs() {
175
+ local npm_root
176
+ npm_root="$(npm root -g 2>/dev/null)" || return 0
177
+ [[ -d "$npm_root" ]] || return 0
178
+ # Stale temp patterns: .ework-web-XXXX, .ework-daemon-XXXX, .opencode-ework-XXXX,
179
+ # .ework-aio-XXXX, plus the .Trash suffix some npm versions use.
180
+ local found=()
181
+ while IFS= read -r p; do
182
+ [[ -n "$p" ]] && found+=("$p")
183
+ done < <(find "$npm_root" -maxdepth 1 \( \
184
+ -name '.ework-web-*' -o \
185
+ -name '.ework-daemon-*' -o \
186
+ -name '.ework-aio-*' -o \
187
+ -name '.opencode-ework-*' -o \
188
+ -name 'ework-*.Trash*' -o \
189
+ -name 'opencode-ework.Trash*' \) 2>/dev/null || true)
190
+ if [[ ${#found[@]} -gt 0 ]]; then
191
+ log "Found ${#found[@]} stale npm temp dir(s) under $npm_root from a previous failed install:"
192
+ printf ' %s\n' "${found[@]}"
193
+ log "Removing..."
194
+ local d
195
+ for d in "${found[@]}"; do
196
+ rm -rf "$d" 2>/dev/null || warn "could not remove $d (try: sudo rm -rf $d)"
197
+ done
198
+ fi
199
+ }
200
+
201
+ npm_install_global() {
202
+ # One retry after a cleanup pass — covers the ENOTEMPTY case where npm
203
+ # itself produced a fresh temp dir during this same failed run.
204
+ if ! npm install -g "$1@latest"; then
205
+ warn "npm install -g $1@latest failed; cleaning stale temp dirs and retrying once..."
206
+ clean_npm_stale_dirs
207
+ npm install -g "$1@latest"
208
+ fi
209
+ }
210
+
211
+ clean_npm_stale_dirs
165
212
  log "Installing/updating npm packages globally..."
166
213
  # Install each as a top-level global so bins are linked to PATH.
167
214
  # (npm v9 doesn't hoist nested deps' bins from `npm i -g ework-aio`.)
168
215
  # Always run `npm install -g` (not gated on presence) so re-runs pick up new versions.
169
216
  for pkg in ework-web ework-daemon opencode-ework ework-aio; do
170
- npm install -g "$pkg@latest" || die "npm install -g $pkg@latest failed"
217
+ 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*)"
171
218
  done
172
219
  ok "npm packages ready"
173
220
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-aio",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
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",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "files": [
28
28
  "bin",
29
+ "scripts",
29
30
  "src",
30
31
  "README.md",
31
32
  "LICENSE"