@psychout98/tadaima 1.0.2 → 1.0.4

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.
Files changed (70) hide show
  1. package/dist/chunk-6O3GWKMO.js +16979 -0
  2. package/dist/chunk-7TPZ4T2V.js +37 -0
  3. package/dist/chunk-KDW3UEKL.js +105 -0
  4. package/dist/config-YQZEYLLR.js +7 -0
  5. package/dist/daemon-YZQZNLEX.js +14 -0
  6. package/dist/download-handler-R6AJCG4O.js +462 -0
  7. package/dist/index.js +253 -257
  8. package/dist/logger-XTDECBFK.js +85 -0
  9. package/dist/service-GBG6YHQW.js +199 -0
  10. package/dist/setup-OLV7C7ON.js +100 -0
  11. package/dist/status-writer-RGH2PULB.js +31 -0
  12. package/dist/tui-ZE4672PT.js +77 -0
  13. package/dist/updater-HXBNBU36.js +163 -0
  14. package/dist/ws-client-CLD6QAFC.js +178 -0
  15. package/package.json +6 -5
  16. package/dist/config.d.ts +0 -23
  17. package/dist/config.d.ts.map +0 -1
  18. package/dist/config.js +0 -25
  19. package/dist/config.js.map +0 -1
  20. package/dist/daemon.d.ts +0 -8
  21. package/dist/daemon.d.ts.map +0 -1
  22. package/dist/daemon.js +0 -91
  23. package/dist/daemon.js.map +0 -1
  24. package/dist/download-handler.d.ts +0 -15
  25. package/dist/download-handler.d.ts.map +0 -1
  26. package/dist/download-handler.js +0 -203
  27. package/dist/download-handler.js.map +0 -1
  28. package/dist/downloader.d.ts +0 -11
  29. package/dist/downloader.d.ts.map +0 -1
  30. package/dist/downloader.js +0 -65
  31. package/dist/downloader.js.map +0 -1
  32. package/dist/index.d.ts +0 -2
  33. package/dist/index.d.ts.map +0 -1
  34. package/dist/index.js.map +0 -1
  35. package/dist/logger.d.ts +0 -2
  36. package/dist/logger.d.ts.map +0 -1
  37. package/dist/logger.js +0 -74
  38. package/dist/logger.js.map +0 -1
  39. package/dist/organizer.d.ts +0 -12
  40. package/dist/organizer.d.ts.map +0 -1
  41. package/dist/organizer.js +0 -42
  42. package/dist/organizer.js.map +0 -1
  43. package/dist/rd-client.d.ts +0 -25
  44. package/dist/rd-client.d.ts.map +0 -1
  45. package/dist/rd-client.js +0 -129
  46. package/dist/rd-client.js.map +0 -1
  47. package/dist/service.d.ts +0 -3
  48. package/dist/service.d.ts.map +0 -1
  49. package/dist/service.js +0 -187
  50. package/dist/service.js.map +0 -1
  51. package/dist/setup.d.ts +0 -2
  52. package/dist/setup.d.ts.map +0 -1
  53. package/dist/setup.js +0 -92
  54. package/dist/setup.js.map +0 -1
  55. package/dist/status-writer.d.ts +0 -20
  56. package/dist/status-writer.d.ts.map +0 -1
  57. package/dist/status-writer.js +0 -34
  58. package/dist/status-writer.js.map +0 -1
  59. package/dist/tui.d.ts +0 -14
  60. package/dist/tui.d.ts.map +0 -1
  61. package/dist/tui.js +0 -73
  62. package/dist/tui.js.map +0 -1
  63. package/dist/updater.d.ts +0 -27
  64. package/dist/updater.d.ts.map +0 -1
  65. package/dist/updater.js +0 -191
  66. package/dist/updater.js.map +0 -1
  67. package/dist/ws-client.d.ts +0 -26
  68. package/dist/ws-client.d.ts.map +0 -1
  69. package/dist/ws-client.js +0 -155
  70. package/dist/ws-client.js.map +0 -1
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ config
4
+ } from "./chunk-7TPZ4T2V.js";
5
+
6
+ // src/service.ts
7
+ import { writeFileSync, unlinkSync, existsSync } from "fs";
8
+ import { execSync } from "child_process";
9
+ import { platform, homedir } from "os";
10
+ import { join, dirname } from "path";
11
+ var SERVICE_NAME = "tadaima-agent";
12
+ function installService() {
13
+ const os = platform();
14
+ if (os === "linux") {
15
+ installSystemd();
16
+ } else if (os === "darwin") {
17
+ installLaunchd();
18
+ } else if (os === "win32") {
19
+ installWindows();
20
+ } else {
21
+ console.log(`Unsupported platform: ${os}`);
22
+ }
23
+ }
24
+ function uninstallService() {
25
+ const os = platform();
26
+ if (os === "linux") {
27
+ uninstallSystemd();
28
+ } else if (os === "darwin") {
29
+ uninstallLaunchd();
30
+ } else if (os === "win32") {
31
+ uninstallWindows();
32
+ } else {
33
+ console.log(`Unsupported platform: ${os}`);
34
+ }
35
+ }
36
+ function installWindows() {
37
+ const execPath = process.execPath;
38
+ const scriptPath = process.argv[1];
39
+ const serviceName = "TadaimaAgent";
40
+ try {
41
+ execSync(
42
+ `sc create ${serviceName} binPath= "\\"${execPath}\\" \\"${scriptPath}\\" start" start= auto DisplayName= "Tadaima Agent"`
43
+ );
44
+ execSync(
45
+ `sc description ${serviceName} "Tadaima media download agent"`
46
+ );
47
+ execSync(
48
+ `sc failure ${serviceName} reset= 60 actions= restart/5000/restart/10000/restart/30000`
49
+ );
50
+ execSync(`sc start ${serviceName}`);
51
+ console.log(`Windows Service installed and started: ${serviceName}`);
52
+ console.log(` Status: sc query ${serviceName}`);
53
+ console.log(` Stop: sc stop ${serviceName}`);
54
+ console.log("");
55
+ console.log(
56
+ "Note: The service runs as LocalSystem by default. If it needs access"
57
+ );
58
+ console.log(
59
+ "to user directories, configure it to run as your user account:"
60
+ );
61
+ console.log(
62
+ ` sc config ${serviceName} obj= ".\\USERNAME" password= "PASSWORD"`
63
+ );
64
+ } catch (err) {
65
+ console.error(
66
+ "Failed to install Windows Service. Try running as Administrator.",
67
+ err instanceof Error ? err.message : ""
68
+ );
69
+ }
70
+ }
71
+ function uninstallWindows() {
72
+ const serviceName = "TadaimaAgent";
73
+ try {
74
+ execSync(`sc stop ${serviceName}`);
75
+ } catch {
76
+ }
77
+ try {
78
+ execSync(`sc delete ${serviceName}`);
79
+ console.log(`Windows Service removed: ${serviceName}`);
80
+ } catch (err) {
81
+ console.error(
82
+ "Failed to uninstall Windows Service. Try running as Administrator.",
83
+ err instanceof Error ? err.message : ""
84
+ );
85
+ }
86
+ }
87
+ function installSystemd() {
88
+ const execPath = process.execPath;
89
+ const scriptPath = process.argv[1];
90
+ const user = process.env.USER ?? "root";
91
+ const unit = `[Unit]
92
+ Description=Tadaima Agent
93
+ After=network-online.target
94
+ Wants=network-online.target
95
+
96
+ [Service]
97
+ Type=simple
98
+ User=${user}
99
+ ExecStart=${execPath} ${scriptPath} start
100
+ Restart=on-failure
101
+ RestartSec=5
102
+ Environment=NODE_ENV=production
103
+
104
+ [Install]
105
+ WantedBy=multi-user.target
106
+ `;
107
+ const unitPath = `/etc/systemd/system/${SERVICE_NAME}.service`;
108
+ try {
109
+ writeFileSync(unitPath, unit);
110
+ execSync("systemctl daemon-reload");
111
+ execSync(`systemctl enable ${SERVICE_NAME}`);
112
+ execSync(`systemctl start ${SERVICE_NAME}`);
113
+ console.log(`Service installed and started: ${SERVICE_NAME}`);
114
+ console.log(` Status: systemctl status ${SERVICE_NAME}`);
115
+ console.log(` Logs: journalctl -u ${SERVICE_NAME} -f`);
116
+ } catch (err) {
117
+ console.error(
118
+ "Failed to install service. Try running with sudo.",
119
+ err instanceof Error ? err.message : ""
120
+ );
121
+ }
122
+ }
123
+ function uninstallSystemd() {
124
+ const unitPath = `/etc/systemd/system/${SERVICE_NAME}.service`;
125
+ try {
126
+ execSync(`systemctl stop ${SERVICE_NAME}`).toString();
127
+ } catch {
128
+ }
129
+ try {
130
+ execSync(`systemctl disable ${SERVICE_NAME}`);
131
+ if (existsSync(unitPath)) unlinkSync(unitPath);
132
+ execSync("systemctl daemon-reload");
133
+ console.log(`Service removed: ${SERVICE_NAME}`);
134
+ } catch (err) {
135
+ console.error(
136
+ "Failed to uninstall service. Try running with sudo.",
137
+ err instanceof Error ? err.message : ""
138
+ );
139
+ }
140
+ }
141
+ function installLaunchd() {
142
+ const execPath = process.execPath;
143
+ const scriptPath = process.argv[1];
144
+ const label = `com.tadaima.agent`;
145
+ const plistPath = join(homedir(), "Library/LaunchAgents", `${label}.plist`);
146
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
147
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
148
+ <plist version="1.0">
149
+ <dict>
150
+ <key>Label</key>
151
+ <string>${label}</string>
152
+ <key>ProgramArguments</key>
153
+ <array>
154
+ <string>${execPath}</string>
155
+ <string>${scriptPath}</string>
156
+ <string>start</string>
157
+ </array>
158
+ <key>RunAtLoad</key>
159
+ <true/>
160
+ <key>KeepAlive</key>
161
+ <true/>
162
+ <key>StandardOutPath</key>
163
+ <string>${join(dirname(config.path), "logs", "tadaima.log")}</string>
164
+ <key>StandardErrorPath</key>
165
+ <string>${join(dirname(config.path), "logs", "tadaima.log")}</string>
166
+ </dict>
167
+ </plist>
168
+ `;
169
+ try {
170
+ writeFileSync(plistPath, plist);
171
+ execSync(`launchctl load ${plistPath}`);
172
+ console.log(`Service installed and started: ${label}`);
173
+ console.log(` Plist: ${plistPath}`);
174
+ console.log(` Stop: launchctl unload ${plistPath}`);
175
+ } catch (err) {
176
+ console.error(
177
+ "Failed to install service.",
178
+ err instanceof Error ? err.message : ""
179
+ );
180
+ }
181
+ }
182
+ function uninstallLaunchd() {
183
+ const label = `com.tadaima.agent`;
184
+ const plistPath = join(homedir(), "Library/LaunchAgents", `${label}.plist`);
185
+ try {
186
+ execSync(`launchctl unload ${plistPath}`);
187
+ } catch {
188
+ }
189
+ if (existsSync(plistPath)) {
190
+ unlinkSync(plistPath);
191
+ console.log(`Service removed: ${label}`);
192
+ } else {
193
+ console.log("Service not installed.");
194
+ }
195
+ }
196
+ export {
197
+ installService,
198
+ uninstallService
199
+ };
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ config
4
+ } from "./chunk-7TPZ4T2V.js";
5
+
6
+ // src/setup.ts
7
+ import prompts from "prompts";
8
+ import { hostname, platform } from "os";
9
+ function detectDeviceName() {
10
+ return hostname().toLowerCase().replace(/\.local$/, "");
11
+ }
12
+ function detectPlatform() {
13
+ const p = platform();
14
+ switch (p) {
15
+ case "darwin":
16
+ return "macos";
17
+ case "win32":
18
+ return "windows";
19
+ default:
20
+ return p;
21
+ }
22
+ }
23
+ async function runSetup() {
24
+ console.log("\n Tadaima Agent Setup\n");
25
+ const { relayUrl } = await prompts({
26
+ type: "text",
27
+ name: "relayUrl",
28
+ message: "Relay URL",
29
+ initial: config.get("relay") || "http://localhost:3000",
30
+ validate: (v) => v.startsWith("http") ? true : "Must start with http:// or https://"
31
+ });
32
+ if (!relayUrl) {
33
+ console.log("Setup cancelled.");
34
+ process.exit(0);
35
+ }
36
+ const { code } = await prompts({
37
+ type: "text",
38
+ name: "code",
39
+ message: "Pairing code (from web app)",
40
+ validate: (v) => v.length === 6 ? true : "Code must be 6 characters"
41
+ });
42
+ if (!code) {
43
+ console.log("Setup cancelled.");
44
+ process.exit(0);
45
+ }
46
+ const deviceName = detectDeviceName();
47
+ const devicePlatform = detectPlatform();
48
+ console.log(`
49
+ Pairing as "${deviceName}" (${devicePlatform})...`);
50
+ const res = await fetch(`${relayUrl}/api/devices/pair/claim`, {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({
54
+ code: code.toUpperCase(),
55
+ name: deviceName,
56
+ platform: devicePlatform
57
+ })
58
+ });
59
+ if (!res.ok) {
60
+ const err = await res.json().catch(() => ({ detail: res.statusText }));
61
+ console.error(`
62
+ Pairing failed: ${err.detail || err.error}`);
63
+ process.exit(1);
64
+ }
65
+ const { deviceId, deviceToken, rdApiKey, wsUrl } = await res.json();
66
+ const { moviesDir } = await prompts({
67
+ type: "text",
68
+ name: "moviesDir",
69
+ message: "Movies directory",
70
+ initial: config.get("directories.movies") || "/mnt/media/Movies",
71
+ validate: (v) => v.trim().length > 0 && v.startsWith("/") ? true : "Must be a non-empty absolute path"
72
+ });
73
+ const { tvDir } = await prompts({
74
+ type: "text",
75
+ name: "tvDir",
76
+ message: "TV Shows directory",
77
+ initial: config.get("directories.tv") || "/mnt/media/TV",
78
+ validate: (v) => v.trim().length > 0 && v.startsWith("/") ? true : "Must be a non-empty absolute path"
79
+ });
80
+ if (!moviesDir || !tvDir) {
81
+ console.log("Setup cancelled.");
82
+ process.exit(0);
83
+ }
84
+ config.set("relay", relayUrl);
85
+ config.set("deviceToken", deviceToken);
86
+ config.set("deviceId", deviceId);
87
+ config.set("deviceName", deviceName);
88
+ config.set("directories.movies", moviesDir || "/mnt/media/Movies");
89
+ config.set("directories.tv", tvDir || "/mnt/media/TV");
90
+ config.set("directories.staging", "/tmp/tadaima/staging");
91
+ config.set("realDebrid.apiKey", rdApiKey);
92
+ console.log(`
93
+ Connected! This device is now paired as "${deviceName}".`);
94
+ console.log(` Config saved to: ${config.path}`);
95
+ console.log(` WebSocket URL: ${wsUrl}
96
+ `);
97
+ }
98
+ export {
99
+ runSetup
100
+ };
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ config
4
+ } from "./chunk-7TPZ4T2V.js";
5
+
6
+ // src/status-writer.ts
7
+ import { writeFileSync, unlinkSync, renameSync, mkdirSync } from "fs";
8
+ import { dirname, join } from "path";
9
+ function getStatusPath() {
10
+ return join(dirname(config.path), "status.json");
11
+ }
12
+ function writeStatus(status) {
13
+ const statusPath = getStatusPath();
14
+ const tmpPath = statusPath + ".tmp";
15
+ try {
16
+ mkdirSync(dirname(statusPath), { recursive: true });
17
+ writeFileSync(tmpPath, JSON.stringify(status, null, 2));
18
+ renameSync(tmpPath, statusPath);
19
+ } catch {
20
+ }
21
+ }
22
+ function removeStatus() {
23
+ try {
24
+ unlinkSync(getStatusPath());
25
+ } catch {
26
+ }
27
+ }
28
+ export {
29
+ removeStatus,
30
+ writeStatus
31
+ };
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ config
4
+ } from "./chunk-7TPZ4T2V.js";
5
+
6
+ // src/tui.ts
7
+ import { freemem } from "os";
8
+ var TUI = class {
9
+ connected = false;
10
+ profileName = "";
11
+ version = "0.0.0";
12
+ interval = null;
13
+ recentCompleted = [];
14
+ constructor(version) {
15
+ this.version = version;
16
+ this.profileName = config.get("profileName") || "Unknown";
17
+ }
18
+ setConnected(connected) {
19
+ this.connected = connected;
20
+ }
21
+ addCompleted(title, size) {
22
+ this.recentCompleted.unshift({ title, size, completedAt: Date.now() });
23
+ if (this.recentCompleted.length > 5) this.recentCompleted.pop();
24
+ }
25
+ start() {
26
+ process.stdout.write("\x1B[?25l");
27
+ this.render();
28
+ this.interval = setInterval(() => this.render(), 1e3);
29
+ }
30
+ stop() {
31
+ if (this.interval) clearInterval(this.interval);
32
+ process.stdout.write("\x1B[?25h");
33
+ }
34
+ render() {
35
+ const lines = [];
36
+ const status = this.connected ? "Connected" : "Disconnected";
37
+ lines.push("");
38
+ lines.push(
39
+ ` tadaima v${this.version} \u2014 ${status} to relay (${this.profileName})`
40
+ );
41
+ lines.push(` ${"\u2500".repeat(50)}`);
42
+ if (this.recentCompleted.length === 0) {
43
+ lines.push(` Waiting for downloads...`);
44
+ }
45
+ for (const entry of this.recentCompleted) {
46
+ const ago = formatTimeAgo(entry.completedAt);
47
+ lines.push(
48
+ ` \u2713 ${entry.title.padEnd(35)} ${formatSize(entry.size).padStart(10)} \u305F\u3060\u3044\u307E \u2014 completed ${ago}`
49
+ );
50
+ }
51
+ lines.push(` ${"\u2500".repeat(50)}`);
52
+ const diskFree = formatSize(freemem());
53
+ lines.push(
54
+ ` ${diskFree} free`
55
+ );
56
+ lines.push("");
57
+ process.stdout.write("\x1B[2J\x1B[H");
58
+ process.stdout.write(lines.join("\n"));
59
+ }
60
+ };
61
+ function formatSize(bytes) {
62
+ const gb = bytes / (1024 * 1024 * 1024);
63
+ if (gb >= 1) return `${gb.toFixed(1)} GB`;
64
+ const mb = bytes / (1024 * 1024);
65
+ return `${mb.toFixed(0)} MB`;
66
+ }
67
+ function formatTimeAgo(timestamp) {
68
+ const diff = Date.now() - timestamp;
69
+ const secs = Math.floor(diff / 1e3);
70
+ if (secs < 60) return `${secs}s ago`;
71
+ const mins = Math.floor(secs / 60);
72
+ if (mins < 60) return `${mins}m ago`;
73
+ return `${Math.floor(mins / 60)}h ago`;
74
+ }
75
+ export {
76
+ TUI
77
+ };
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ GITHUB_RELEASES_API,
4
+ getAssetNameForPlatform
5
+ } from "./chunk-6O3GWKMO.js";
6
+ import {
7
+ config
8
+ } from "./chunk-7TPZ4T2V.js";
9
+
10
+ // src/updater.ts
11
+ import { createHash } from "crypto";
12
+ import { createWriteStream, readFileSync, copyFileSync, chmodSync, renameSync } from "fs";
13
+ import { pipeline } from "stream/promises";
14
+ import { Readable } from "stream";
15
+ import { dirname, join, extname } from "path";
16
+ var cachedETag = "";
17
+ async function checkViaRelay(currentVersion) {
18
+ const relayUrl = config.get("relay");
19
+ if (!relayUrl) return null;
20
+ try {
21
+ const res = await fetch(`${relayUrl}/api/version`, {
22
+ signal: AbortSignal.timeout(5e3)
23
+ });
24
+ if (!res.ok) return null;
25
+ const data = await res.json();
26
+ if (data.latest && isNewer(data.latest, currentVersion)) {
27
+ return data.latest;
28
+ }
29
+ return null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ async function checkForUpdate(currentVersion) {
35
+ const assetName = getAssetNameForPlatform();
36
+ if (!assetName) return null;
37
+ const relayLatest = await checkViaRelay(currentVersion);
38
+ if (relayLatest === null) {
39
+ }
40
+ const headers = {
41
+ Accept: "application/vnd.github+json",
42
+ "User-Agent": `tadaima-agent/${currentVersion}`
43
+ };
44
+ if (cachedETag) {
45
+ headers["If-None-Match"] = cachedETag;
46
+ }
47
+ const res = await fetch(GITHUB_RELEASES_API, { headers });
48
+ if (res.status === 304) return null;
49
+ if (!res.ok) {
50
+ throw new Error(`GitHub API returned ${res.status}: ${res.statusText}`);
51
+ }
52
+ const etag = res.headers.get("etag");
53
+ if (etag) cachedETag = etag;
54
+ const data = await res.json();
55
+ const latestVersion = data.tag_name.replace(/^v/, "");
56
+ if (!isNewer(latestVersion, currentVersion)) return null;
57
+ const binaryAsset = data.assets.find((a) => a.name === assetName);
58
+ const checksumAsset = data.assets.find((a) => a.name === "checksums.sha256");
59
+ if (!binaryAsset || !checksumAsset) return null;
60
+ config.set("lastUpdateCheck", (/* @__PURE__ */ new Date()).toISOString());
61
+ return {
62
+ version: latestVersion,
63
+ downloadUrl: binaryAsset.browser_download_url,
64
+ checksumUrl: checksumAsset.browser_download_url
65
+ };
66
+ }
67
+ async function applyUpdate(update) {
68
+ const configDir = dirname(config.path);
69
+ const ext = extname(process.execPath);
70
+ const tmpPath = join(configDir, `tadaima-agent-update-${update.version}${ext}`);
71
+ const backupPath = join(configDir, `tadaima-agent-previous${ext}`);
72
+ const binRes = await fetch(update.downloadUrl);
73
+ if (!binRes.ok || !binRes.body) {
74
+ throw new Error(`Failed to download binary: ${binRes.status}`);
75
+ }
76
+ const ws = createWriteStream(tmpPath);
77
+ await pipeline(Readable.fromWeb(binRes.body), ws);
78
+ const csRes = await fetch(update.checksumUrl);
79
+ if (!csRes.ok) {
80
+ throw new Error(`Failed to download checksums: ${csRes.status}`);
81
+ }
82
+ const checksumText = await csRes.text();
83
+ const assetName = getAssetNameForPlatform();
84
+ const expectedLine = checksumText.split("\n").find((l) => l.includes(assetName));
85
+ if (!expectedLine) {
86
+ throw new Error(`No checksum found for ${assetName}`);
87
+ }
88
+ const expectedHash = expectedLine.trim().split(/\s+/)[0].toLowerCase();
89
+ const actualHash = createHash("sha256").update(readFileSync(tmpPath)).digest("hex");
90
+ if (actualHash !== expectedHash) {
91
+ throw new Error(
92
+ `Checksum mismatch: expected ${expectedHash}, got ${actualHash}`
93
+ );
94
+ }
95
+ if (process.platform !== "win32") {
96
+ chmodSync(tmpPath, 493);
97
+ }
98
+ try {
99
+ copyFileSync(process.execPath, backupPath);
100
+ config.set("previousBinaryPath", backupPath);
101
+ } catch {
102
+ }
103
+ renameSync(tmpPath, process.execPath);
104
+ if (process.env.TADAIMA_DAEMON || isRunningAsService()) {
105
+ console.log(`Updated to v${update.version}. Exiting for service restart.`);
106
+ process.exit(0);
107
+ } else {
108
+ console.log(
109
+ `Update to v${update.version} downloaded. Restart to apply.`
110
+ );
111
+ }
112
+ }
113
+ function shouldCheckNow(trigger) {
114
+ const last = config.get("lastUpdateCheck");
115
+ if (!last) return true;
116
+ const elapsed = Date.now() - new Date(last).getTime();
117
+ const ONE_HOUR = 60 * 60 * 1e3;
118
+ const TWENTY_FOUR_HOURS = 24 * ONE_HOUR;
119
+ if (trigger === "startup") return elapsed > ONE_HOUR;
120
+ return elapsed > TWENTY_FOUR_HOURS;
121
+ }
122
+ function rollback() {
123
+ const prev = config.get("previousBinaryPath");
124
+ if (!prev) {
125
+ console.log("No previous binary to roll back to.");
126
+ return;
127
+ }
128
+ try {
129
+ copyFileSync(prev, process.execPath);
130
+ console.log(`Rolled back to previous binary from ${prev}.`);
131
+ console.log("Restart the agent to use the restored version.");
132
+ } catch (err) {
133
+ console.error(
134
+ "Rollback failed:",
135
+ err instanceof Error ? err.message : err
136
+ );
137
+ }
138
+ }
139
+ function logUpdateAdvisory(currentVersion, latestVersion) {
140
+ console.log(`
141
+ Tadaima v${latestVersion} is available (you have v${currentVersion}).`);
142
+ console.log(" npm: npm update -g @tadaima/agent");
143
+ console.log(" Docker: docker compose pull && docker compose up -d\n");
144
+ }
145
+ function isNewer(latest, current) {
146
+ const [lMaj, lMin, lPatch] = latest.split(".").map(Number);
147
+ const [cMaj, cMin, cPatch] = current.split(".").map(Number);
148
+ if (lMaj !== cMaj) return lMaj > cMaj;
149
+ if (lMin !== cMin) return lMin > cMin;
150
+ return lPatch > cPatch;
151
+ }
152
+ function isRunningAsService() {
153
+ return !!process.env.INVOCATION_ID || // systemd
154
+ process.ppid === 1 || // launchd (parent is launchd PID 1)
155
+ !!process.env.TADAIMA_SERVICE;
156
+ }
157
+ export {
158
+ applyUpdate,
159
+ checkForUpdate,
160
+ logUpdateAdvisory,
161
+ rollback,
162
+ shouldCheckNow
163
+ };