ework-aio 0.3.4 → 0.3.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-aio",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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",
package/src/cli.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  // - Unknown flags → error. We never silently swallow typos.
13
13
 
14
14
  import os from "node:os";
15
+ import { readFileSync } from "node:fs";
15
16
  import { spawnSync } from "node:child_process";
16
17
  import { Logger, InstallError, log as defaultLogger } from "./log.ts";
17
18
  import {
@@ -21,6 +22,7 @@ import {
21
22
  } from "./types.ts";
22
23
  import { runInstall } from "./commands/install.ts";
23
24
  import { runUninstall } from "./commands/uninstall.ts";
25
+ import { runUpgrade } from "./commands/upgrade.ts";
24
26
  import {
25
27
  runStart,
26
28
  runStop,
@@ -35,7 +37,10 @@ import {
35
37
  type ConfigArgs,
36
38
  } from "./commands/config.ts";
37
39
 
38
- const VERSION = "0.2.7-dev";
40
+ // Read from package.json — never hardcode (every release reported "0.2.7-dev").
41
+ const VERSION: string = JSON.parse(
42
+ readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
43
+ ).version;
39
44
 
40
45
  const USAGE = `ework-aio <command> [options]
41
46
 
@@ -44,6 +49,9 @@ Commands:
44
49
  Add 'systemd' to also write+enable systemd
45
50
  units. Without 'systemd', runs in pure
46
51
  PID-file mode (no systemctl calls).
52
+ upgrade Pull latest ework-aio from npm and restart
53
+ services. Use this to update — don't re-run
54
+ 'install' for version bumps.
47
55
  uninstall Stop services and remove units (data preserved)
48
56
  status Show service status (PID-file mode)
49
57
  logs [web|daemon] Tail logs (Ctrl+C to stop)
@@ -426,6 +434,10 @@ export async function main(
426
434
  await runInstall(opts, logger);
427
435
  return 0;
428
436
  }
437
+ case "upgrade": {
438
+ await runUpgrade(opts, logger);
439
+ return 0;
440
+ }
429
441
  case "uninstall": {
430
442
  await runUninstall(opts, logger);
431
443
  return 0;
@@ -24,7 +24,7 @@ import { resolvePaths, type PathConfig } from "../paths.ts";
24
24
  import { type InstallContext } from "../config.ts";
25
25
  import { ensureEnvFile, parseEnvFile, patchEnvKey } from "../env.ts";
26
26
  import { startProcess, isProcessRunning, readPidFile } from "../pidfile.ts";
27
- import { checkPreflight, resolveCommand, resolveBundledBin, REQUIRED_COMMANDS } from "../preflight.ts";
27
+ import { checkPreflight, resolveCommand, resolveBundledBin, ensureSelfBinSymlink, REQUIRED_COMMANDS } from "../preflight.ts";
28
28
  import {
29
29
  generateUnitFile,
30
30
  writeUnitFile,
@@ -83,6 +83,15 @@ export async function runInstall(
83
83
  logger.log(` data dir : ${opts.dataDir ?? "(default ~/.local/share/ework-aio)"}`);
84
84
  logger.hr();
85
85
 
86
+ const preflightPaths = resolvePaths({
87
+ dataDir: opts.dataDir,
88
+ scope: opts.scope,
89
+ useSystemd: opts.useSystemd,
90
+ });
91
+ if (fs.existsSync(preflightPaths.webEnvFile)) {
92
+ logger.warn("existing install detected — use 'ework-aio upgrade' to update versions");
93
+ }
94
+
86
95
  // 1. Preflight: bun/npm/opencode must exist on PATH.
87
96
  const preflight = checkPreflight([...REQUIRED_COMMANDS], {
88
97
  optionalCommands: ["systemctl"],
@@ -96,6 +105,8 @@ export async function runInstall(
96
105
  const opencodeBin = preflight.found.get("opencode")!;
97
106
  logger.ok(`preflight: bun, npm, opencode all on PATH`);
98
107
 
108
+ ensureSelfBinSymlink(logger);
109
+
99
110
  // 2. Resolve ework-web / ework-daemon binaries.
100
111
  //
101
112
  // ework-daemon ships TWO bins: `ework-daemon` (client CLI: status/issues/...)
@@ -17,12 +17,13 @@ import {
17
17
  isProcessRunning,
18
18
  } from "../pidfile.ts";
19
19
  import { parseEnvFile } from "../env.ts";
20
- import { resolveCommand } from "../preflight.ts";
20
+ import { resolveCommand, resolveBundledBin } from "../preflight.ts";
21
21
  import type { GlobalOptions, ServiceTarget } from "../types.ts";
22
22
 
23
23
  interface ServicePaths {
24
24
  bin: string;
25
25
  pkg: string;
26
+ binRelPath: string;
26
27
  dataDir: string;
27
28
  envFile: string;
28
29
  pidFile: string;
@@ -35,6 +36,7 @@ function servicePaths(paths: PathConfig, svc: "web" | "daemon"): ServicePaths {
35
36
  return {
36
37
  bin: "ework-web",
37
38
  pkg: "ework-web",
39
+ binRelPath: "bin/ework-web.js",
38
40
  dataDir: paths.webDataDir,
39
41
  envFile: paths.webEnvFile,
40
42
  pidFile: paths.webPidFile,
@@ -43,14 +45,9 @@ function servicePaths(paths: PathConfig, svc: "web" | "daemon"): ServicePaths {
43
45
  };
44
46
  }
45
47
  return {
46
- // ework-daemon ships two bins; only `ework-daemon-server` actually starts
47
- // the HTTP server. The `ework-daemon` bin is a client CLI that prints help
48
- // and exits — spawning it leaves the daemon "looking dead" with help text
49
- // in the log. See install.ts preflight for the full rationale.
50
48
  bin: "ework-daemon-server",
51
- // npm package name differs from the bin name — the error hint must use the
52
- // package name, not the bin, or the user gets a 404 trying to install.
53
49
  pkg: "ework-daemon",
50
+ binRelPath: "bin/ework-daemon-server.js",
54
51
  dataDir: paths.daemonDataDir,
55
52
  envFile: paths.daemonEnvFile,
56
53
  pidFile: paths.daemonPidFile,
@@ -77,9 +74,9 @@ async function startOne(
77
74
  logger: Logger,
78
75
  ): Promise<boolean> {
79
76
  const sp = servicePaths(paths, svc);
80
- const binPath = resolveCommand(sp.bin);
77
+ const binPath = resolveBundledBin(sp.pkg, sp.binRelPath) ?? resolveCommand(sp.bin);
81
78
  if (!binPath) {
82
- throw new InstallError(`${sp.bin} not found on PATH install with: npm install -g ${sp.pkg}`);
79
+ throw new InstallError(`${sp.bin} not found — update with: npm install -g ework-aio@latest`);
83
80
  }
84
81
  const existingPid = await readPidFile(sp.pidFile);
85
82
  if (existingPid !== null && isProcessRunning(existingPid)) {
@@ -0,0 +1,40 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { Logger, InstallError } from "../log.ts";
3
+ import type { GlobalOptions } from "../types.ts";
4
+
5
+ export async function runUpgrade(opts: GlobalOptions, logger: Logger): Promise<void> {
6
+ logger.hr();
7
+ logger.log("ework-aio upgrade");
8
+ logger.hr();
9
+
10
+ logger.log("pulling latest ework-aio from npm...");
11
+ const result = spawnSync("npm", ["install", "-g", "ework-aio@latest"], {
12
+ encoding: "utf8",
13
+ stdio: ["ignore", "pipe", "pipe"],
14
+ env: process.env,
15
+ });
16
+ if (result.status !== 0) {
17
+ const stderr = result.stderr ?? "(no stderr)";
18
+ throw new InstallError(`npm install -g ework-aio@latest failed:\n${stderr}`);
19
+ }
20
+ logger.ok("ework-aio updated to latest");
21
+
22
+ // Re-exec as a fresh process so the NEW code resolves bundled deps.
23
+ // The current process still holds the old version in memory.
24
+ logger.log("restarting services with new version...");
25
+ const restartResult = spawnSync("ework-aio", ["restart", ...restartFlags(opts)], {
26
+ encoding: "utf8",
27
+ stdio: "inherit",
28
+ env: process.env,
29
+ });
30
+ if (restartResult.status !== 0) {
31
+ throw new InstallError("restart after upgrade failed — run 'ework-aio restart' manually");
32
+ }
33
+ logger.ok("upgrade complete");
34
+ }
35
+
36
+ function restartFlags(opts: GlobalOptions): string[] {
37
+ const flags: string[] = [];
38
+ if (opts.dataDir) flags.push("--data-dir", opts.dataDir);
39
+ return flags;
40
+ }
package/src/preflight.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import type { Logger } from "./log.ts";
4
5
 
5
6
  // Root of the ework-aio package itself. Derived from this file's location
6
7
  // (<root>/src/preflight.ts). Used to resolve bins that ework-aio ships as
@@ -68,3 +69,47 @@ export function checkPreflight(
68
69
 
69
70
  export const REQUIRED_COMMANDS: readonly string[] = ["bun", "npm", "opencode"];
70
71
  export const OPTIONAL_COMMANDS: readonly string[] = ["systemctl", "sudo"];
72
+
73
+ // Ensure `ework-aio` is reachable from PATH. npm puts the bin at its own
74
+ // global prefix's bin dir, which may differ from every dir on PATH (e.g.
75
+ // prefix changed after a node upgrade). Walk PATH in order and, at the
76
+ // first writable dir, create/repair a symlink to our actual bin.
77
+ export function ensureSelfBinSymlink(logger: Logger): void {
78
+ const ourBin = path.resolve(import.meta.dir, "..", "bin", "ework-aio");
79
+ if (!fs.existsSync(ourBin)) return;
80
+ const ourBinReal = fs.realpathSync(ourBin);
81
+
82
+ const pathDirs = (process.env.PATH ?? "").split(":").filter(Boolean);
83
+ for (const dir of pathDirs) {
84
+ try {
85
+ fs.accessSync(dir, fs.constants.W_OK);
86
+ } catch {
87
+ continue;
88
+ }
89
+
90
+ const target = path.join(dir, "ework-aio");
91
+ try {
92
+ const stat = fs.lstatSync(target);
93
+ if (stat.isSymbolicLink()) {
94
+ try {
95
+ if (fs.realpathSync(target) === ourBinReal) return;
96
+ } catch {
97
+ // broken symlink — fall through to replace
98
+ }
99
+ fs.unlinkSync(target);
100
+ } else {
101
+ continue; // regular file — don't clobber
102
+ }
103
+ } catch {
104
+ // doesn't exist — fall through to create
105
+ }
106
+
107
+ try {
108
+ fs.symlinkSync(ourBin, target);
109
+ logger.ok(`bin symlink: ${target} → ${ourBin}`);
110
+ return;
111
+ } catch {
112
+ continue;
113
+ }
114
+ }
115
+ }