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 +1 -1
- package/src/cli.ts +13 -1
- package/src/commands/install.ts +12 -1
- package/src/commands/lifecycle.ts +6 -9
- package/src/commands/upgrade.ts +40 -0
- package/src/preflight.ts +45 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ework-aio",
|
|
3
|
-
"version": "0.3.
|
|
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
|
-
|
|
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;
|
package/src/commands/install.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|