ework-aio 0.1.17 → 0.2.1
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 +504 -0
- package/src/commands/config.ts +208 -0
- package/src/commands/env.ts +27 -0
- package/src/commands/install.ts +665 -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 +238 -0
- package/src/log.ts +102 -0
- package/src/opencode-config.ts +104 -0
- package/src/paths.ts +95 -0
- package/src/pidfile.ts +166 -0
- package/src/preflight.ts +48 -0
- package/src/systemd.ts +234 -0
- package/src/types.ts +95 -0
- package/bin/install.sh +0 -1200
package/src/cli.ts
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// CLI entry point. Manual arg parser (no commander.js — the constraint is
|
|
2
|
+
// no new external deps). Dispatches to command handlers under src/commands/.
|
|
3
|
+
//
|
|
4
|
+
// Parsing rules:
|
|
5
|
+
// - First positional = subcommand (install/uninstall/status/logs/env/
|
|
6
|
+
// config/start/stop/restart/ps/migrate/backfill-timestamps).
|
|
7
|
+
// - `install systemd` = install with --useSystemd. The literal "systemd"
|
|
8
|
+
// positional after install is the only special case.
|
|
9
|
+
// - Global flags (--data-dir, --port, --user, --system, --yes, etc.) can
|
|
10
|
+
// appear anywhere after the subcommand. `config` consumes its own
|
|
11
|
+
// subcommand position too (list/get/set/restart).
|
|
12
|
+
// - Unknown flags → error. We never silently swallow typos.
|
|
13
|
+
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { Logger, InstallError, log as defaultLogger } from "./log.ts";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULTS,
|
|
19
|
+
type GlobalOptions,
|
|
20
|
+
type ServiceTarget,
|
|
21
|
+
} from "./types.ts";
|
|
22
|
+
import { runInstall } from "./commands/install.ts";
|
|
23
|
+
import { runUninstall } from "./commands/uninstall.ts";
|
|
24
|
+
import {
|
|
25
|
+
runStart,
|
|
26
|
+
runStop,
|
|
27
|
+
runRestart,
|
|
28
|
+
runStatus,
|
|
29
|
+
} from "./commands/lifecycle.ts";
|
|
30
|
+
import { runLogs } from "./commands/logs.ts";
|
|
31
|
+
import { runEnv } from "./commands/env.ts";
|
|
32
|
+
import {
|
|
33
|
+
runConfig,
|
|
34
|
+
printConfigHelp,
|
|
35
|
+
type ConfigArgs,
|
|
36
|
+
} from "./commands/config.ts";
|
|
37
|
+
|
|
38
|
+
const VERSION = "0.2.0-dev";
|
|
39
|
+
|
|
40
|
+
const USAGE = `ework-aio <command> [options]
|
|
41
|
+
|
|
42
|
+
Commands:
|
|
43
|
+
install [systemd] [options] Install or upgrade the ework stack (default).
|
|
44
|
+
Add 'systemd' to also write+enable systemd
|
|
45
|
+
units. Without 'systemd', runs in pure
|
|
46
|
+
PID-file mode (no systemctl calls).
|
|
47
|
+
uninstall Stop services and remove units (data preserved)
|
|
48
|
+
status Show service status (PID-file mode)
|
|
49
|
+
logs [web|daemon] Tail logs (Ctrl+C to stop)
|
|
50
|
+
env Print key paths (no secrets)
|
|
51
|
+
config <subcommand> Read / change runtime config (.env keys)
|
|
52
|
+
config list List all settable keys + current values
|
|
53
|
+
config get <KEY> Print current value of one key
|
|
54
|
+
config set <KEY> <VALUE> Set a key, then restart affected service
|
|
55
|
+
(unless --no-restart is given)
|
|
56
|
+
config restart <web|daemon|both>
|
|
57
|
+
Restart one or both services
|
|
58
|
+
|
|
59
|
+
start [web|daemon|both] Start services in PID-file mode (default both)
|
|
60
|
+
stop [web|daemon|both] Stop services (SIGTERM, 5s grace, then SIGKILL)
|
|
61
|
+
restart [web|daemon|both] Stop + start
|
|
62
|
+
ps Show PID-file mode status (alias for 'status')
|
|
63
|
+
|
|
64
|
+
migrate [options] Migrate issues from a Gitea instance
|
|
65
|
+
backfill-timestamps Fix timestamps on already-migrated data
|
|
66
|
+
|
|
67
|
+
Install options:
|
|
68
|
+
systemd Also install systemd units + enable them
|
|
69
|
+
--user (with systemd) user-level units (default)
|
|
70
|
+
--system (with systemd) system-level units (needs sudo)
|
|
71
|
+
--data-dir <path> Override data directory
|
|
72
|
+
(default ~/.local/share/ework-aio)
|
|
73
|
+
--port <n> ework-web port (default ${DEFAULTS.workPort})
|
|
74
|
+
--daemon-port <n> ework-daemon port (default ${DEFAULTS.daemonPort})
|
|
75
|
+
--bot-name <login> Bot username (default ${DEFAULTS.botName})
|
|
76
|
+
--no-start Install but don't start services
|
|
77
|
+
--yes (-y) Skip all prompts (use generated defaults)
|
|
78
|
+
--as-user <login> (sudo only) drop privileges: re-exec install
|
|
79
|
+
as <login> after enabling linger
|
|
80
|
+
--allow-root (sudo only) override refuse-on-root default
|
|
81
|
+
|
|
82
|
+
Global options:
|
|
83
|
+
--no-restart With 'config set': edit .env but skip restart
|
|
84
|
+
-h, --help Show this help
|
|
85
|
+
-v, --version Print version
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
export interface ParsedArgs {
|
|
89
|
+
subcommand: string;
|
|
90
|
+
opts: GlobalOptions;
|
|
91
|
+
positionals: string[];
|
|
92
|
+
configArgs: ConfigArgs;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function defaultOpts(): GlobalOptions {
|
|
96
|
+
return {
|
|
97
|
+
workPort: DEFAULTS.workPort,
|
|
98
|
+
daemonPort: DEFAULTS.daemonPort,
|
|
99
|
+
botName: DEFAULTS.botName,
|
|
100
|
+
scope: DEFAULTS.scope,
|
|
101
|
+
useSystemd: false,
|
|
102
|
+
assumeYes: false,
|
|
103
|
+
allowRoot: false,
|
|
104
|
+
noRestart: false,
|
|
105
|
+
noStart: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// parseArgs: manual argv walk. Returns typed result. Throws InstallError
|
|
110
|
+
// on unknown flags / missing values. The shape is stable so tests can
|
|
111
|
+
// exercise it without touching process.argv.
|
|
112
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
113
|
+
// G1: normalize `--flag=value` (Unix-conventional equals form) into
|
|
114
|
+
// `--flag value` so the rest of the parser only handles the space
|
|
115
|
+
// form. Short flags (-h, -v, -y) don't get this treatment (they never
|
|
116
|
+
// take `=` syntax). Plain `--` (end-of-options marker) and tokens
|
|
117
|
+
// without `--` prefix are left alone.
|
|
118
|
+
const expanded: string[] = [];
|
|
119
|
+
for (const tok of argv) {
|
|
120
|
+
if (tok.length > 3 && tok.startsWith("--") && tok.indexOf("=") > 2) {
|
|
121
|
+
const eqIdx = tok.indexOf("=");
|
|
122
|
+
expanded.push(tok.slice(0, eqIdx), tok.slice(eqIdx + 1));
|
|
123
|
+
} else {
|
|
124
|
+
expanded.push(tok);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
argv = expanded;
|
|
128
|
+
|
|
129
|
+
const opts = defaultOpts();
|
|
130
|
+
const positionals: string[] = [];
|
|
131
|
+
const configArgs: ConfigArgs = { subcommand: "list" };
|
|
132
|
+
let inConfig = false;
|
|
133
|
+
let useSystemdFromPositional = false;
|
|
134
|
+
// S-1: track if scope was explicitly chosen. Under EUID=0 we used to
|
|
135
|
+
// silently flip default "user" → "system"; that hid the fact that root
|
|
136
|
+
// can't run user-scope systemd (XDG_RUNTIME_DIR unset). Now: defaulted
|
|
137
|
+
// scope still flips (back-compat), explicit --user under root throws.
|
|
138
|
+
let scopeExplicit = false;
|
|
139
|
+
|
|
140
|
+
let i = 0;
|
|
141
|
+
while (i < argv.length) {
|
|
142
|
+
const a = argv[i];
|
|
143
|
+
if (a === undefined) break;
|
|
144
|
+
|
|
145
|
+
// `config` consumes its own positional subcommand.
|
|
146
|
+
if (!inConfig && a === "config" && positionals.length === 0) {
|
|
147
|
+
inConfig = true;
|
|
148
|
+
positionals.push("config");
|
|
149
|
+
i++;
|
|
150
|
+
const peek = (k: number): string | undefined => {
|
|
151
|
+
const v = argv[k];
|
|
152
|
+
return typeof v === "string" && !v.startsWith("-") ? v : undefined;
|
|
153
|
+
};
|
|
154
|
+
// First positional after `config` is its subcommand. If absent or
|
|
155
|
+
// not a recognized subcommand name, fall through to "list" (default)
|
|
156
|
+
// or "help" (if user typed something unknown).
|
|
157
|
+
const sub = peek(i);
|
|
158
|
+
if (sub === undefined) {
|
|
159
|
+
// `config` alone — default to list.
|
|
160
|
+
} else if (sub === "list" || sub === "get" || sub === "set" || sub === "restart" || sub === "help") {
|
|
161
|
+
configArgs.subcommand = sub;
|
|
162
|
+
i++;
|
|
163
|
+
if (sub === "get") {
|
|
164
|
+
const key = peek(i);
|
|
165
|
+
if (key !== undefined) { configArgs.key = key; i++; }
|
|
166
|
+
} else if (sub === "set") {
|
|
167
|
+
const key = peek(i);
|
|
168
|
+
if (key !== undefined) { configArgs.key = key; i++; }
|
|
169
|
+
const value = peek(i);
|
|
170
|
+
if (value !== undefined) { configArgs.value = value; i++; }
|
|
171
|
+
} else if (sub === "restart") {
|
|
172
|
+
// Distinguish "no target given" (default to both) from "typo
|
|
173
|
+
// target given" (reject). Without this, `config restart bogus`
|
|
174
|
+
// silently restarts both services — a real foot-gun in production.
|
|
175
|
+
const next = argv[i];
|
|
176
|
+
if (next === "web" || next === "daemon" || next === "both") {
|
|
177
|
+
configArgs.target = next;
|
|
178
|
+
i++;
|
|
179
|
+
} else if (next !== undefined && !next.startsWith("-")) {
|
|
180
|
+
throw new InstallError(
|
|
181
|
+
`config restart: invalid target '${next}'. Expected: web | daemon | both`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// Unknown config subcommand — fall through to help.
|
|
187
|
+
configArgs.subcommand = "help";
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (a === "-h" || a === "--help") {
|
|
194
|
+
process.stdout.write(USAGE);
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
if (a === "-v" || a === "--version") {
|
|
198
|
+
process.stdout.write(`${VERSION}\n`);
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Boolean flags
|
|
203
|
+
if (a === "systemd") {
|
|
204
|
+
useSystemdFromPositional = true;
|
|
205
|
+
i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (a === "--user") { opts.scope = "user"; scopeExplicit = true; i++; continue; }
|
|
209
|
+
if (a === "--system") { opts.scope = "system"; scopeExplicit = true; i++; continue; }
|
|
210
|
+
if (a === "--yes" || a === "-y") { opts.assumeYes = true; i++; continue; }
|
|
211
|
+
if (a === "--allow-root") { opts.allowRoot = true; i++; continue; }
|
|
212
|
+
if (a === "--no-start") { opts.noStart = true; i++; continue; }
|
|
213
|
+
if (a === "--no-restart") { opts.noRestart = true; i++; continue; }
|
|
214
|
+
|
|
215
|
+
// Value flags (--flag <value>)
|
|
216
|
+
if (a === "--data-dir") {
|
|
217
|
+
const v = argv[++i];
|
|
218
|
+
if (v === undefined) throw new InstallError("--data-dir requires a value");
|
|
219
|
+
opts.dataDir = v;
|
|
220
|
+
i++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (a === "--port") {
|
|
224
|
+
const v = argv[++i];
|
|
225
|
+
if (v === undefined) throw new InstallError("--port requires a value");
|
|
226
|
+
const n = Number.parseInt(v, 10);
|
|
227
|
+
if (!Number.isFinite(n) || n <= 0 || n > 65535) {
|
|
228
|
+
throw new InstallError(`--port: invalid value '${v}' (must be 1-65535)`);
|
|
229
|
+
}
|
|
230
|
+
opts.workPort = n;
|
|
231
|
+
i++;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (a === "--daemon-port") {
|
|
235
|
+
const v = argv[++i];
|
|
236
|
+
if (v === undefined) throw new InstallError("--daemon-port requires a value");
|
|
237
|
+
const n = Number.parseInt(v, 10);
|
|
238
|
+
if (!Number.isFinite(n) || n <= 0 || n > 65535) {
|
|
239
|
+
throw new InstallError(`--daemon-port: invalid value '${v}' (must be 1-65535)`);
|
|
240
|
+
}
|
|
241
|
+
opts.daemonPort = n;
|
|
242
|
+
i++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (a === "--bot-name") {
|
|
246
|
+
const v = argv[++i];
|
|
247
|
+
if (v === undefined) throw new InstallError("--bot-name requires a value");
|
|
248
|
+
opts.botName = v;
|
|
249
|
+
i++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (a === "--as-user") {
|
|
253
|
+
const v = argv[++i];
|
|
254
|
+
if (v === undefined) throw new InstallError("--as-user requires a value");
|
|
255
|
+
opts.asUser = v;
|
|
256
|
+
i++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (a.startsWith("--")) {
|
|
261
|
+
throw new InstallError(`Unknown option: ${a} (try --help)`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Positional (e.g. "install", "web", "daemon", "both")
|
|
265
|
+
positionals.push(a);
|
|
266
|
+
i++;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (useSystemdFromPositional) opts.useSystemd = true;
|
|
270
|
+
|
|
271
|
+
// S-1: explicit --user under root would silently fail later (systemd
|
|
272
|
+
// user-scope needs XDG_RUNTIME_DIR which root doesn't have). Default
|
|
273
|
+
// scope (no flag) still flips to "system" under root for back-compat.
|
|
274
|
+
if (process.getuid && process.getuid() === 0 && opts.scope === "user") {
|
|
275
|
+
if (scopeExplicit) {
|
|
276
|
+
throw new InstallError(
|
|
277
|
+
"--user cannot be used when running as root: user-scope systemd needs XDG_RUNTIME_DIR which root does not have. " +
|
|
278
|
+
"Either drop privileges (run as a regular user) or use --system.",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
opts.scope = "system";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// S-10: --port and --daemon-port must differ (otherwise both services
|
|
285
|
+
// fight for the same port and neither binds).
|
|
286
|
+
if (opts.workPort === opts.daemonPort) {
|
|
287
|
+
throw new InstallError(
|
|
288
|
+
`--port and --daemon-port must differ (both are ${opts.workPort})`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const subcommand = positionals[0] ?? "install";
|
|
293
|
+
|
|
294
|
+
return { subcommand, opts, positionals, configArgs };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseServiceTarget(arg: string | undefined): ServiceTarget {
|
|
298
|
+
if (arg === undefined || arg === "") return "both";
|
|
299
|
+
if (arg === "web" || arg === "daemon" || arg === "both") return arg;
|
|
300
|
+
throw new InstallError(
|
|
301
|
+
`Invalid service target '${arg}'. Expected: web | daemon | both`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// stripAsUserFlag: return argv without `--as-user <login>` (the flag and
|
|
306
|
+
// its value), so the sudo'd child doesn't re-enter handleAsUser and loop.
|
|
307
|
+
//
|
|
308
|
+
// Iterates by index, NOT Array.filter on value. If the username happens to
|
|
309
|
+
// appear in another argument (path component, common word like "install",
|
|
310
|
+
// or a --bot-name matching the operator login), filter-on-value would
|
|
311
|
+
// silently drop that token and corrupt the child's argv.
|
|
312
|
+
export function stripAsUserFlag(argv: readonly string[]): string[] {
|
|
313
|
+
const out: string[] = [];
|
|
314
|
+
for (let i = 0; i < argv.length; i++) {
|
|
315
|
+
const tok = argv[i];
|
|
316
|
+
if (tok === undefined) break;
|
|
317
|
+
if (tok === "--as-user") {
|
|
318
|
+
i++; // also skip the value
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
out.push(tok);
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// handleAsUser: when invoked with --as-user (typically via sudo), re-exec
|
|
327
|
+
// the install as the named user after enabling linger. Skipped if not root
|
|
328
|
+
// or no --as-user flag.
|
|
329
|
+
function handleAsUser(opts: GlobalOptions, argv: string[]): void {
|
|
330
|
+
if (!opts.asUser) return;
|
|
331
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
332
|
+
throw new InstallError("--as-user requires running as root (use sudo)");
|
|
333
|
+
}
|
|
334
|
+
const target = opts.asUser;
|
|
335
|
+
|
|
336
|
+
// Resolve target user's home + uid via getpwnam equivalent (os.userInfo
|
|
337
|
+
// doesn't take a name arg, so we shell out to `id`).
|
|
338
|
+
const idRes = spawnSync("id", ["-u", target], { encoding: "utf8" });
|
|
339
|
+
if (idRes.status !== 0 || !idRes.stdout.trim()) {
|
|
340
|
+
throw new InstallError(`--as-user: user '${target}' not found`);
|
|
341
|
+
}
|
|
342
|
+
const targetUid = Number.parseInt(idRes.stdout.trim(), 10);
|
|
343
|
+
if (!Number.isFinite(targetUid)) {
|
|
344
|
+
throw new InstallError(`--as-user: could not parse uid for '${target}'`);
|
|
345
|
+
}
|
|
346
|
+
if (targetUid === 0) {
|
|
347
|
+
throw new InstallError(`--as-user: target '${target}' is root — use --allow-root instead`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Enable linger so user-level systemd services survive logout.
|
|
351
|
+
const linger = spawnSync("loginctl", ["enable-linger", target], { encoding: "utf8" });
|
|
352
|
+
if (linger.status !== 0) {
|
|
353
|
+
throw new InstallError(
|
|
354
|
+
`Failed to enable linger for '${target}': ${linger.stderr?.trim() ?? ""}. ` +
|
|
355
|
+
`Try: sudo loginctl enable-linger ${target}`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Re-exec as target user.
|
|
360
|
+
const self = process.argv[1]!;
|
|
361
|
+
const newArgs = stripAsUserFlag(argv);
|
|
362
|
+
const result = spawnSync("sudo", ["-u", target, "--login", self, ...newArgs], {
|
|
363
|
+
stdio: "inherit",
|
|
364
|
+
env: process.env,
|
|
365
|
+
});
|
|
366
|
+
if (result.status !== null) process.exit(result.status);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// enforceRootGuard: refuse install as root unless --allow-root or --as-user.
|
|
371
|
+
// Matches install.sh's root guard. Install is the only command that needs
|
|
372
|
+
// this — other commands are read-only or scoped to user data.
|
|
373
|
+
function enforceRootGuard(opts: GlobalOptions, logger: Logger): void {
|
|
374
|
+
if (!process.getuid || process.getuid() !== 0) return;
|
|
375
|
+
if (opts.asUser) return; // handled above
|
|
376
|
+
if (opts.allowRoot) {
|
|
377
|
+
logger.warn("running install as root with --allow-root — data will live under /root");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
process.stderr.write(
|
|
381
|
+
`ework-aio install refuses to run as root by default.\n\n` +
|
|
382
|
+
`Why this matters:\n` +
|
|
383
|
+
` - Data goes under /root/.local/share/ework-aio (unreadable by other users)\n` +
|
|
384
|
+
` - opencode is searched in root's PATH (usually not installed there)\n` +
|
|
385
|
+
` - npm packages install to system-wide prefix owned by root\n\n` +
|
|
386
|
+
`Option A (recommended): run as a regular user.\n` +
|
|
387
|
+
` npm config set prefix '~/.local'\n` +
|
|
388
|
+
` npm install -g ework-aio\n` +
|
|
389
|
+
` ework-aio install\n\n` +
|
|
390
|
+
`Option B: install with sudo but target a regular user.\n` +
|
|
391
|
+
` sudo ework-aio install --as-user ${os.userInfo().username}\n\n` +
|
|
392
|
+
`Option C: really install as root.\n` +
|
|
393
|
+
` sudo ework-aio install --allow-root\n`,
|
|
394
|
+
);
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// main: parse + dispatch. Returns exit code.
|
|
399
|
+
export async function main(
|
|
400
|
+
argv: string[],
|
|
401
|
+
logger: Logger = defaultLogger,
|
|
402
|
+
): Promise<number> {
|
|
403
|
+
let parsed: ParsedArgs;
|
|
404
|
+
try {
|
|
405
|
+
parsed = parseArgs(argv);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
if (err instanceof InstallError) {
|
|
408
|
+
logger.error(err.message);
|
|
409
|
+
return err.code;
|
|
410
|
+
}
|
|
411
|
+
throw err;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const { subcommand, opts, positionals, configArgs } = parsed;
|
|
415
|
+
|
|
416
|
+
// --as-user triggers a re-exec; this never returns.
|
|
417
|
+
if (opts.asUser && (subcommand === "install" || subcommand === undefined)) {
|
|
418
|
+
handleAsUser(opts, argv);
|
|
419
|
+
return 1;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
switch (subcommand) {
|
|
424
|
+
case "install": {
|
|
425
|
+
enforceRootGuard(opts, logger);
|
|
426
|
+
await runInstall(opts, logger);
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
case "uninstall": {
|
|
430
|
+
await runUninstall(opts, logger);
|
|
431
|
+
return 0;
|
|
432
|
+
}
|
|
433
|
+
case "status":
|
|
434
|
+
case "ps": {
|
|
435
|
+
await runStatus(opts, logger);
|
|
436
|
+
return 0;
|
|
437
|
+
}
|
|
438
|
+
case "logs": {
|
|
439
|
+
const svc = positionals[1] === "daemon" ? "daemon" : "web";
|
|
440
|
+
await runLogs(opts, logger, svc);
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
case "env": {
|
|
444
|
+
await runEnv(opts, logger);
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
case "config": {
|
|
448
|
+
if (configArgs.subcommand === "help") {
|
|
449
|
+
printConfigHelp(logger);
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
await runConfig(opts, logger, configArgs);
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
case "start": {
|
|
456
|
+
const target = parseServiceTarget(positionals[1]);
|
|
457
|
+
await runStart(opts, logger, target);
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
case "stop": {
|
|
461
|
+
const target = parseServiceTarget(positionals[1]);
|
|
462
|
+
await runStop(opts, logger, target);
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
case "restart": {
|
|
466
|
+
const target = parseServiceTarget(positionals[1]);
|
|
467
|
+
await runRestart(opts, logger, target);
|
|
468
|
+
return 0;
|
|
469
|
+
}
|
|
470
|
+
case "migrate": {
|
|
471
|
+
return runDelegateScript("migrate-from-gitea.ts", argv.slice(1));
|
|
472
|
+
}
|
|
473
|
+
case "backfill-timestamps": {
|
|
474
|
+
return runDelegateScript("backfill-timestamps.ts", argv.slice(1));
|
|
475
|
+
}
|
|
476
|
+
default:
|
|
477
|
+
logger.error(`Unknown command: ${subcommand} (try --help)`);
|
|
478
|
+
return 1;
|
|
479
|
+
}
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (err instanceof InstallError) {
|
|
482
|
+
logger.error(err.message);
|
|
483
|
+
return err.code;
|
|
484
|
+
}
|
|
485
|
+
logger.error(`unexpected error: ${(err as Error).message}`);
|
|
486
|
+
logger.error((err as Error).stack ?? "");
|
|
487
|
+
return 1;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// runDelegateScript: hand off to a sibling TS script via bun. Used for
|
|
492
|
+
// migrate + backfill-timestamps which live in scripts/ and have their own
|
|
493
|
+
// arg surfaces we don't want to absorb into cli.ts.
|
|
494
|
+
function runDelegateScript(scriptName: string, args: string[]): number {
|
|
495
|
+
const scriptPath = new URL(`../scripts/${scriptName}`, import.meta.url).pathname;
|
|
496
|
+
const result = spawnSync("bun", [scriptPath, ...args], {
|
|
497
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
498
|
+
env: process.env,
|
|
499
|
+
});
|
|
500
|
+
if (result.status !== null) return result.status;
|
|
501
|
+
return 1;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Entry-point is bin/ework-aio only. Tests import main() directly.
|