-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.ts
More file actions
137 lines (126 loc) · 4.83 KB
/
Copy pathcli.ts
File metadata and controls
137 lines (126 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/** @jsxImportSource react */
import { Command } from "commander";
import { VERSION } from "./version.js";
import { render } from "ink";
import React from "react";
import { App } from "./ui/App.js";
import { readState } from "./state/store.js";
import { runDoctor } from "./usecase/doctor.js";
import { runUpdate } from "./usecase/update.js";
import { showPassword } from "./usecase/show-password.js";
import { runUninstall } from "./usecase/uninstall.js";
import { tailLogs } from "./usecase/logs.js";
import { purgeInstallDir } from "./usecase/cleanup.js";
import { runHosts, revertHosts } from "./usecase/hosts.js";
import { exec } from "./lib/exec.js";
import { existsSync } from "node:fs";
import { join } from "node:path";
const program = new Command();
program
.name("arrstack")
.description("Self-hosted arr media stack installer")
.version(VERSION)
.option("--verbose", "enable verbose output")
.option("--non-interactive", "disable interactive prompts");
program
.command("install")
.description("Install the arr media stack")
.option("--fresh", "perform a fresh installation")
.option("--resume", "resume a previously interrupted installation")
.option("--install-dir <path>", "installation directory", `${process.env.HOME}/arrstack`)
.action(async (opts) => {
const installDir = opts.installDir ?? `${process.env.HOME}/arrstack`;
if (opts.fresh) {
const composeFile = join(installDir, "docker-compose.yml");
if (existsSync(composeFile)) {
console.log("Stopping and removing existing containers...");
const down = await exec(
`docker compose -f "${composeFile}" down --remove-orphans`,
{ timeoutMs: 120_000 }
);
if (!down.ok) {
console.warn(`docker compose down returned an error: ${down.stderr}`);
}
}
const removed = await purgeInstallDir(installDir);
for (const path of removed) {
console.log(`Removed: ${path}`);
}
}
const existing = opts.fresh ? null : readState(installDir);
const { waitUntilExit } = render(React.createElement(App, { existingState: existing }));
await waitUntilExit();
});
program
.command("doctor")
.description("Check system requirements and diagnose issues")
.option("--install-dir <path>", "installation directory", `${process.env.HOME}/arrstack`)
.action(async (opts) => {
await runDoctor(opts.installDir).catch((err) => {
console.error(`Error: ${err.message}`);
process.exit(1);
});
});
program
.command("update")
.description("Update the arr media stack to the latest version")
.option("--install-dir <path>", "installation directory", `${process.env.HOME}/arrstack`)
.action(async (opts) => {
await runUpdate(opts.installDir).catch((err) => {
console.error(`Error: ${err.message}`);
process.exit(1);
});
});
program
.command("show-password")
.description("Show service passwords")
.option("--install-dir <path>", "installation directory", `${process.env.HOME}/arrstack`)
.action(async (opts) => {
await showPassword(opts.installDir).catch((err) => {
console.error(`Error: ${err.message}`);
process.exit(1);
});
});
program
.command("uninstall")
.description("Uninstall the arr media stack")
.option("--install-dir <path>", "installation directory", `${process.env.HOME}/arrstack`)
.option("--purge", "also remove config files (media data is always preserved)")
.action(async (opts) => {
await runUninstall(opts.installDir, opts.purge ?? false).catch((err) => {
console.error(`Error: ${err.message}`);
process.exit(1);
});
});
program
.command("hosts")
.description(
"Map arrstack hostnames to this host's LAN IP in /etc/hosts (uses sudo). Lets LAN clients reach https://{svc}.{domain} without hairpin-NAT."
)
.option("--revert", "remove the arrstack block from /etc/hosts instead of writing it")
.option("--install-dir <path>", "installation directory", `${process.env.HOME}/arrstack`)
.action(async (opts) => {
const action = opts.revert ? revertHosts() : runHosts(opts.installDir);
await action.catch((err) => {
console.error(`Error: ${err.message}`);
process.exit(1);
});
});
program
.command("logs <service>")
.description("Show logs for a service")
.option("--install-dir <path>", "installation directory", `${process.env.HOME}/arrstack`)
.action(async (service, opts) => {
await tailLogs(opts.installDir, service).catch((err) => {
console.error(`Error: ${err.message}`);
process.exit(1);
});
});
// If no subcommand provided (just `arrstack`), launch the TUI
program.action(async () => {
const installDir = `${process.env.HOME}/arrstack`;
const existing = readState(installDir);
const { waitUntilExit } = render(React.createElement(App, { existingState: existing }));
await waitUntilExit();
});
program.parse(process.argv);