@danypops/tickets 0.10.1 → 0.10.2
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 +2 -2
- package/src/cli/index.ts +32 -17
- package/src/cli/systemd-service.ts +27 -88
- package/src/process/main.ts +19 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/tickets",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Unified CLI, daemon, and TypeScript library for issue tracking across GitHub, GitLab, and Jira.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@danypops/vehicle-core": "^0.12.3",
|
|
28
|
-
"@danypops/vehicle-server": "^0.
|
|
28
|
+
"@danypops/vehicle-server": "^0.18.2",
|
|
29
29
|
"@danypops/vehicle-client": "^0.5.0",
|
|
30
30
|
"@danypops/enigma-client": "^0.6.1",
|
|
31
31
|
"@gitbeaker/rest": "^43.8.0",
|
package/src/cli/index.ts
CHANGED
|
@@ -15,8 +15,9 @@ import { promptMaskedSecret } from "../auth/masked-prompt.js";
|
|
|
15
15
|
import { deleteToken, isTokenFresh, listStoredBackends, loadToken, saveToken } from "../auth/token-store.js";
|
|
16
16
|
import type { CreateInput, ListFilter, Priority, Status, UpdateInput } from "../issue/issue.js";
|
|
17
17
|
import { parseStatus } from "../issue/issue.js";
|
|
18
|
+
import { serveMain } from "../process/main.js";
|
|
18
19
|
import type { StagePatchFields, StagePayload } from "../stage/store.js";
|
|
19
|
-
import {
|
|
20
|
+
import { ticketsServiceCli } from "./systemd-service.js";
|
|
20
21
|
import { createTicketsClient, type TicketsRpcClient } from "./tickets-client.js";
|
|
21
22
|
|
|
22
23
|
function printJson(value: unknown): void {
|
|
@@ -489,32 +490,53 @@ daemon
|
|
|
489
490
|
}
|
|
490
491
|
});
|
|
491
492
|
|
|
493
|
+
program
|
|
494
|
+
.command("serve")
|
|
495
|
+
.description("run the tickets daemon in the foreground -- Armada's service spec launches exactly this (see ./systemd-service.js)")
|
|
496
|
+
.action(async () => {
|
|
497
|
+
await serveMain();
|
|
498
|
+
});
|
|
499
|
+
|
|
492
500
|
const service = program
|
|
493
501
|
.command("service")
|
|
494
502
|
.description(
|
|
495
|
-
"deploy the tickets daemon as a persistent
|
|
503
|
+
"deploy the tickets daemon as a persistent, Armada-supervised service (Linux/macOS/Windows; survives logout/reboot and restarts on crash, unlike `daemon start`'s on-demand spawn)",
|
|
496
504
|
);
|
|
497
505
|
|
|
498
506
|
service
|
|
499
507
|
.command("install")
|
|
500
|
-
.description("
|
|
508
|
+
.description("register this install with Armada as the tickets vehicle and reconcile it (write/enable/start its systemd unit)")
|
|
501
509
|
.action(() => {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
process.
|
|
510
|
+
const cli = ticketsServiceCli();
|
|
511
|
+
const result = cli.install();
|
|
512
|
+
if (!result.installed) {
|
|
513
|
+
process.stderr.write(`error: ${result.reason}\n`);
|
|
514
|
+
process.exitCode = 1;
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
printJson({ status: "installed", unitName: cli.unitName, via: "armada" });
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
service
|
|
521
|
+
.command("uninstall")
|
|
522
|
+
.description("remove this vehicle from Armada's fleet (stops and un-enrolls the tickets-daemon service)")
|
|
523
|
+
.action(() => {
|
|
524
|
+
const result = ticketsServiceCli().uninstall();
|
|
525
|
+
if (!result.installed) {
|
|
526
|
+
process.stderr.write(`error: ${result.reason}\n`);
|
|
507
527
|
process.exitCode = 1;
|
|
528
|
+
return;
|
|
508
529
|
}
|
|
530
|
+
printJson({ status: "uninstalled", via: "armada" });
|
|
509
531
|
});
|
|
510
532
|
|
|
511
533
|
for (const action of ["start", "stop", "restart", "status"] as const) {
|
|
512
534
|
service
|
|
513
535
|
.command(action)
|
|
514
|
-
.description(`systemctl --user ${action} tickets
|
|
536
|
+
.description(`systemctl --user ${action} the Armada-managed tickets unit`)
|
|
515
537
|
.action(() => {
|
|
516
538
|
try {
|
|
517
|
-
|
|
539
|
+
ticketsServiceCli().action(action);
|
|
518
540
|
} catch (err) {
|
|
519
541
|
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
520
542
|
process.exitCode = 1;
|
|
@@ -522,13 +544,6 @@ for (const action of ["start", "stop", "restart", "status"] as const) {
|
|
|
522
544
|
});
|
|
523
545
|
}
|
|
524
546
|
|
|
525
|
-
service
|
|
526
|
-
.command("path")
|
|
527
|
-
.description("print where the systemd unit file would be written")
|
|
528
|
-
.action(() => {
|
|
529
|
-
printJson({ unitPath: systemdUnitPath() });
|
|
530
|
-
});
|
|
531
|
-
|
|
532
547
|
const auth = program.command("auth").description("delegated OAuth login (device flow for GitHub/GitLab, authorization code for Jira)");
|
|
533
548
|
|
|
534
549
|
auth
|
|
@@ -1,97 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deploys the tickets daemon as
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `papyrus service <install|start|stop|restart|status>` pattern exactly
|
|
6
|
-
* (see ~/Projects/papyrus/src/cli.ts) -- same shape, same systemctl --user
|
|
7
|
-
* verbs, same install order (write unit -> daemon-reload -> enable ->
|
|
8
|
-
* restart). Every side effect (file write, directory creation, systemctl
|
|
9
|
-
* invocation) is injectable so this is fully testable without touching a
|
|
10
|
-
* real filesystem or spawning a real systemctl process.
|
|
2
|
+
* Deploys the tickets daemon as an Armada-supervised service. Builds the
|
|
3
|
+
* spec; createServiceCli (vehicle-server) owns install/uninstall/systemctl
|
|
4
|
+
* wiring shared by every Vehicle-backed daemon's CLI.
|
|
11
5
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createServiceCli, type ServiceCli, type ServiceSpec } from "@danypops/vehicle-server/service";
|
|
8
|
+
import { readPackageVersion } from "@danypops/vehicle-server/version";
|
|
15
9
|
import { TICKETS_DAEMON_NAMES } from "../rpc/ops.js";
|
|
16
|
-
import {
|
|
10
|
+
import { ticketsPaths } from "./tickets-client.js";
|
|
17
11
|
|
|
18
|
-
export interface
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
export interface TicketsServiceSpecOptions {
|
|
13
|
+
version?: string;
|
|
14
|
+
/** Overridden in tests. */
|
|
15
|
+
cliEntryPath?: string;
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** XDG_CONFIG_HOME/systemd/user/tickets-daemon.service, falling back to ~/.config like systemd itself does. */
|
|
40
|
-
export function systemdUnitPath(env: Record<string, string | undefined> = process.env): string {
|
|
41
|
-
const configHome = env.XDG_CONFIG_HOME ?? join(env.HOME ?? "", ".config");
|
|
42
|
-
return join(configHome, "systemd", "user", TICKETS_DAEMON_NAMES.systemdUnitName);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type CommandRunner = (command: string, args: string[]) => void;
|
|
46
|
-
|
|
47
|
-
const defaultRunner: CommandRunner = (command, args) => {
|
|
48
|
-
try {
|
|
49
|
-
execFileSync(command, args, { stdio: "inherit" });
|
|
50
|
-
} catch (err) {
|
|
51
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
52
|
-
throw new Error(`${command} ${args.join(" ")} failed (is systemd --user available? this feature is Linux-only): ${message}`);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export type SystemctlAction = "start" | "stop" | "restart" | "status" | "enable" | "daemon-reload";
|
|
57
|
-
|
|
58
|
-
/** Always targets the tickets unit name under --user scope; daemon-reload takes no unit argument. */
|
|
59
|
-
export function systemctlTickets(action: SystemctlAction, runner: CommandRunner = defaultRunner): void {
|
|
60
|
-
const args = action === "daemon-reload" ? ["--user", "daemon-reload"] : ["--user", action, TICKETS_DAEMON_NAMES.systemdUnitName];
|
|
61
|
-
runner("systemctl", args);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface InstallOptions {
|
|
65
|
-
bunBin?: string;
|
|
66
|
-
daemonMainPath?: string;
|
|
67
|
-
env?: Record<string, string | undefined>;
|
|
68
|
-
runner?: CommandRunner;
|
|
69
|
-
writeFile?: (path: string, content: string) => void;
|
|
70
|
-
ensureDir?: (path: string) => void;
|
|
18
|
+
/** The daemon is launched as `<bin> <this CLI's entry path> serve`. */
|
|
19
|
+
export function ticketsServiceSpec(opts: TicketsServiceSpecOptions = {}): ServiceSpec {
|
|
20
|
+
const cliEntryPath = opts.cliEntryPath ?? fileURLToPath(new URL("./index.js", import.meta.url));
|
|
21
|
+
const version = opts.version ?? readPackageVersion(new URL("../../package.json", import.meta.url), "Tickets");
|
|
22
|
+
return {
|
|
23
|
+
name: TICKETS_DAEMON_NAMES.stateDirectoryName,
|
|
24
|
+
displayName: "Tickets daemon",
|
|
25
|
+
version,
|
|
26
|
+
binPath: process.execPath,
|
|
27
|
+
args: [cliEntryPath, "serve"],
|
|
28
|
+
handlePath: ticketsPaths().handle,
|
|
29
|
+
restartOnFailure: true,
|
|
30
|
+
restartSec: 2,
|
|
31
|
+
};
|
|
71
32
|
}
|
|
72
33
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* order -- systemd must see the file before enable/restart can act on it,
|
|
76
|
-
* and restart (not start) so re-running install after an upgrade picks up
|
|
77
|
-
* a changed ExecStart path immediately rather than requiring a manual stop.
|
|
78
|
-
*/
|
|
79
|
-
export function installTicketsService(opts: InstallOptions = {}): { unitPath: string } {
|
|
80
|
-
const unitPath = systemdUnitPath(opts.env);
|
|
81
|
-
const ensureDir = opts.ensureDir ?? ((dir: string) => mkdirSync(dir, { recursive: true }));
|
|
82
|
-
const writeFile = opts.writeFile ?? writeFileSync;
|
|
83
|
-
const runner = opts.runner ?? defaultRunner;
|
|
84
|
-
|
|
85
|
-
ensureDir(dirname(unitPath));
|
|
86
|
-
writeFile(
|
|
87
|
-
unitPath,
|
|
88
|
-
renderSystemdUnit({
|
|
89
|
-
bunBin: opts.bunBin ?? process.execPath,
|
|
90
|
-
daemonMainPath: opts.daemonMainPath ?? resolveDaemonEntryPath(),
|
|
91
|
-
}),
|
|
92
|
-
);
|
|
93
|
-
systemctlTickets("daemon-reload", runner);
|
|
94
|
-
systemctlTickets("enable", runner);
|
|
95
|
-
systemctlTickets("restart", runner);
|
|
96
|
-
return { unitPath };
|
|
34
|
+
export function ticketsServiceCli(opts: TicketsServiceSpecOptions = {}): ServiceCli {
|
|
35
|
+
return createServiceCli(ticketsServiceSpec(opts));
|
|
97
36
|
}
|
package/src/process/main.ts
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
3
|
* The real tickets-daemon binary. Requires Bun (bun:sqlite, Bun.serve via
|
|
4
|
-
* vehicle-server). Everything else in this package
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* vehicle-server). Everything else in this package talks to this process
|
|
5
|
+
* only over the loopback HTTP RPC surface — see client.ts.
|
|
6
|
+
*
|
|
7
|
+
* serveMain() is also what `tickets serve` (cli/index.ts) runs directly --
|
|
8
|
+
* that's the command Armada's ServiceSpec launches.
|
|
7
9
|
*/
|
|
8
10
|
import { runDaemonProcess } from "@danypops/vehicle-server/daemon";
|
|
9
11
|
import { readPackageVersion } from "@danypops/vehicle-server/version";
|
|
10
12
|
import { bootstrap } from "./bootstrap.js";
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
+
export async function serveMain(): Promise<void> {
|
|
15
|
+
const version = readPackageVersion(new URL("../../package.json", import.meta.url), "Tickets");
|
|
16
|
+
const { options } = await bootstrap({ version });
|
|
14
17
|
|
|
15
|
-
runDaemonProcess({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
18
|
+
runDaemonProcess({
|
|
19
|
+
...options,
|
|
20
|
+
onListen: (info) => {
|
|
21
|
+
options.logger?.info("tickets daemon listening", { host: info.host, port: info.port, version });
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (import.meta.main) {
|
|
27
|
+
await serveMain();
|
|
28
|
+
}
|