@opentig/cli 0.1.4 → 0.1.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.
Files changed (116) hide show
  1. package/README.md +73 -17
  2. package/dist/bin.mjs +2 -2
  3. package/dist/chunks/{cli-B3AR8LUd.mjs → cli-ABzC6-gR.mjs} +232 -57
  4. package/dist/chunks/service-manager-DkpSHxsP.mjs +394 -0
  5. package/dist/client/assets/App-CBetJ5ZN.js +74 -0
  6. package/dist/client/assets/CreatePullRequestDialog-W82oHjYm.js +2 -0
  7. package/dist/client/assets/{DiffWorkspace-Dg7YI5fJ.js → DiffWorkspace-BWkTIl5n.js} +1 -1
  8. package/dist/client/assets/{EditableFileViewer-CmxzBnAY.js → EditableFileViewer-Cv3aNJBN.js} +1 -1
  9. package/dist/client/assets/{File-CYcpdoXz.js → File-DiOOsHOl.js} +1 -1
  10. package/dist/client/assets/{FilesView-BFCwGLC2.js → FilesView-DIqPfRbv.js} +1 -1
  11. package/dist/client/assets/{GitHubAvatar-BcncAgKT.js → GitHubAvatar-DBc3Vxnk.js} +1 -1
  12. package/dist/client/assets/HistoryView-DYxWYYVx.js +1 -0
  13. package/dist/client/assets/{HtmlFileViewer-jdBVtlNv.js → HtmlFileViewer-BsewwJtM.js} +1 -1
  14. package/dist/client/assets/{IconEdit-DQZu_Rpb.js → IconEdit-CxvzMyLV.js} +1 -1
  15. package/dist/client/assets/{IconFolderPlus-DvEWPQIQ.js → IconFolderPlus-1vLDIzhC.js} +1 -1
  16. package/dist/client/assets/{ImageFileViewer-BW1MbklH.js → ImageFileViewer-CoQZkgET.js} +1 -1
  17. package/dist/client/assets/{LocalRefsDialog-9ggtgVW_.js → LocalRefsDialog-sw-ROjfe.js} +1 -1
  18. package/dist/client/assets/{MarkdownFileViewer-Bhrr1l2Q.js → MarkdownFileViewer-BedKCXTI.js} +2 -2
  19. package/dist/client/assets/{PairingPage-CkRMPzli.js → PairingPage-CYJXzBZC.js} +1 -1
  20. package/dist/client/assets/{PierreDiffViewer-CYdNkBG9.js → PierreDiffViewer-uhTsb-F7.js} +1 -1
  21. package/dist/client/assets/PierreEditBoundary-Bb4dONIq.js +2 -0
  22. package/dist/client/assets/{PierreWorkerPool-B2iHe3DC.js → PierreWorkerPool-BuBzE8K1.js} +9 -9
  23. package/dist/client/assets/{PullRequestDiff-DZRDxOx9.js → PullRequestDiff-CgY4QIHC.js} +1 -1
  24. package/dist/client/assets/{PullRequestViewer-Bsh-F3-d.js → PullRequestViewer-KZl0osME.js} +2 -2
  25. package/dist/client/assets/PullRequestsView-DkN3pW33.js +1 -0
  26. package/dist/client/assets/{QuickOpenDialog-CtJqcFuk.js → QuickOpenDialog-Cn-aQRAS.js} +1 -1
  27. package/dist/client/assets/RepositoryProjectsDialog-F-EctLXx.js +1 -0
  28. package/dist/client/assets/SearchView-BtKBEJ7e.js +1 -0
  29. package/dist/client/assets/SettingsDialog-wfWhEzZY.js +9 -0
  30. package/dist/client/assets/Viewer-D-oFua7K.js +3 -0
  31. package/dist/client/assets/{abnfDiagram-N423BO3Z-DYcwdF-j.js → abnfDiagram-N423BO3Z-DLBLFOdy.js} +1 -1
  32. package/dist/client/assets/{arc-CZ6a8_6G.js → arc-D-Yei3Vr.js} +1 -1
  33. package/dist/client/assets/{architectureDiagram-T3A2C74G-CViM1eoQ.js → architectureDiagram-T3A2C74G-BOgBvzWh.js} +1 -1
  34. package/dist/client/assets/{base-ui-D4MeJpa3.js → base-ui-qeHDA5zu.js} +1 -1
  35. package/dist/client/assets/{blockDiagram-VBNYF7ZC-BzRc4Zb8.js → blockDiagram-VBNYF7ZC-BQXRJDyR.js} +1 -1
  36. package/dist/client/assets/{c4Diagram-5PPSVZJV-BZsdylz9.js → c4Diagram-5PPSVZJV-DTZseD56.js} +1 -1
  37. package/dist/client/assets/channel-DrMuSkDK.js +1 -0
  38. package/dist/client/assets/{checkbox-iSOOddl1.js → checkbox-BwbxYGgK.js} +1 -1
  39. package/dist/client/assets/{chunk-2GRJ4B5K-CsL1szop.js → chunk-2GRJ4B5K-spRCgfPY.js} +1 -1
  40. package/dist/client/assets/{chunk-2Q5K7J3B-Dpm-BZHi.js → chunk-2Q5K7J3B-CLVClH0M.js} +1 -1
  41. package/dist/client/assets/{chunk-5RXB4S5H-BNNb0lga.js → chunk-5RXB4S5H-CuQ5tYjB.js} +1 -1
  42. package/dist/client/assets/{chunk-5VM5RSS4-CcXWQAK-.js → chunk-5VM5RSS4-DyOEOe90.js} +1 -1
  43. package/dist/client/assets/{chunk-6Q2QTUOP-BIpZKHoj.js → chunk-6Q2QTUOP-CgMtuJIx.js} +1 -1
  44. package/dist/client/assets/{chunk-GF5L2VYU-CXSHzIYB.js → chunk-GF5L2VYU-BH63P4Vv.js} +1 -1
  45. package/dist/client/assets/{chunk-JWPE2WC7-DxFk0X7_.js → chunk-JWPE2WC7-BANDbaPq.js} +1 -1
  46. package/dist/client/assets/{chunk-KBJHAD2P-WRKVeb4h.js → chunk-KBJHAD2P-Bfty2upo.js} +1 -1
  47. package/dist/client/assets/{chunk-RYQCIY6F-Br5DeHdX.js → chunk-RYQCIY6F-Cvu8BZS9.js} +1 -1
  48. package/dist/client/assets/{chunk-XXDRQBXY-DFCzSljb.js → chunk-XXDRQBXY-bnohf1yd.js} +1 -1
  49. package/dist/client/assets/classDiagram-JCYQIIEL-BKiL0hkR.js +1 -0
  50. package/dist/client/assets/classDiagram-v2-OCEON4UE-BKiL0hkR.js +1 -0
  51. package/dist/client/assets/{cose-bilkent-JH36ORCC-CE8E-qIe.js → cose-bilkent-JH36ORCC-DCTEAZwO.js} +1 -1
  52. package/dist/client/assets/{cynefin-VYW2F7L2-CHf3vLku.js → cynefin-VYW2F7L2-Bp2wXvdt.js} +1 -1
  53. package/dist/client/assets/{cynefinDiagram-MW4NZA55-90txNnhw.js → cynefinDiagram-MW4NZA55-DRRWzuF0.js} +1 -1
  54. package/dist/client/assets/{dagre-VZM6K2ZE-6somveWH.js → dagre-VZM6K2ZE-DDvsotRK.js} +1 -1
  55. package/dist/client/assets/{diagram-7IWD3JNH-D4GP5bhO.js → diagram-7IWD3JNH-CMqhRNRq.js} +1 -1
  56. package/dist/client/assets/{diagram-B4RE2ZJO-Cz4bx70D.js → diagram-B4RE2ZJO-D_PYI1ut.js} +1 -1
  57. package/dist/client/assets/{diagram-LBJQPF4R-BXjlhC7t.js → diagram-LBJQPF4R-BSUx7mMM.js} +1 -1
  58. package/dist/client/assets/{diagram-Q27KOJAE-B_dDT61b.js → diagram-Q27KOJAE-C8O3g6WT.js} +1 -1
  59. package/dist/client/assets/{diagram-UB23O5K3-BotmyDTm.js → diagram-UB23O5K3-C5-NDsgC.js} +1 -1
  60. package/dist/client/assets/{ebnfDiagram-BXEA7PRR-CBey4stz.js → ebnfDiagram-BXEA7PRR-D71hx4Ww.js} +1 -1
  61. package/dist/client/assets/{erDiagram-JOGREHBK-B2g7aHUP.js → erDiagram-JOGREHBK-yd9B5d8I.js} +1 -1
  62. package/dist/client/assets/{flowDiagram-UKHOOZJN-B181B3KJ.js → flowDiagram-UKHOOZJN-CWU5VVbw.js} +1 -1
  63. package/dist/client/assets/{ganttDiagram-PKOTCBZU-BFslA07w.js → ganttDiagram-PKOTCBZU-D2siVjfa.js} +1 -1
  64. package/dist/client/assets/{gh-utils-rGdT2YZN.js → gh-utils-B0Nlmxep.js} +1 -1
  65. package/dist/client/assets/{gitGraphDiagram-DS77QQ5N-B_uJt_Hp.js → gitGraphDiagram-DS77QQ5N-CIvIFcxR.js} +1 -1
  66. package/dist/client/assets/index-BOPi2b1j.js +47 -0
  67. package/dist/client/assets/index-CryuKDT3.css +1 -0
  68. package/dist/client/assets/{infoDiagram-6WML65LV-BuajoWMd.js → infoDiagram-6WML65LV-DRvl8KfO.js} +1 -1
  69. package/dist/client/assets/{ishikawaDiagram-WSZJBQD7-CGRAyQ-L.js → ishikawaDiagram-WSZJBQD7-H5EQq6uE.js} +1 -1
  70. package/dist/client/assets/{journeyDiagram-NVQOT4AX-D8bgj_4i.js → journeyDiagram-NVQOT4AX-CCwLmD-U.js} +1 -1
  71. package/dist/client/assets/{kanban-definition-27J2QSJJ-QrwKHnlk.js → kanban-definition-27J2QSJJ-DS--XHEu.js} +1 -1
  72. package/dist/client/assets/{linear-BSIPbUpM.js → linear-Q3JNM6CR.js} +1 -1
  73. package/dist/client/assets/{mermaid.core-D4_R4ZQ7.js → mermaid.core-EwS_m22C.js} +4 -4
  74. package/dist/client/assets/{mindmap-definition-FAOFIHXS-DTiuF5JK.js → mindmap-definition-FAOFIHXS-D15nrwfe.js} +1 -1
  75. package/dist/client/assets/{pegDiagram-VL7TDLO6-yCIq1xSC.js → pegDiagram-VL7TDLO6-DOG19gxx.js} +1 -1
  76. package/dist/client/assets/{pieDiagram-7S7Q4E2Y-B_Vgs3pz.js → pieDiagram-7S7Q4E2Y-Bstdr7Wh.js} +1 -1
  77. package/dist/client/assets/{quadrantDiagram-CIZ2JOQS-5Ne6-Suh.js → quadrantDiagram-CIZ2JOQS-DfKs4qZW.js} +1 -1
  78. package/dist/client/assets/{railroadDiagram-AXF67PYL-CRQ23Ne5.js → railroadDiagram-AXF67PYL-DJKxyuhV.js} +1 -1
  79. package/dist/client/assets/{render-markdown-D7BGt-zy.js → render-markdown-C7X2TwRk.js} +1 -1
  80. package/dist/client/assets/{requirementDiagram-LRYGKXZP-Ctks1nFE.js → requirementDiagram-LRYGKXZP-SGJWVmBV.js} +1 -1
  81. package/dist/client/assets/{sankeyDiagram-W5VNT64P-D127Ar8U.js → sankeyDiagram-W5VNT64P-CpZFct0O.js} +1 -1
  82. package/dist/client/assets/{sequenceDiagram-SI44F4Z6-6hK5AS2n.js → sequenceDiagram-SI44F4Z6-B7plRik9.js} +1 -1
  83. package/dist/client/assets/{sizeCapture-X5ZJPWSS-GhdZlEoa.js → sizeCapture-X5ZJPWSS-CNbK285t.js} +1 -1
  84. package/dist/client/assets/{stateDiagram-OKZ733FA-u3G3OuQX.js → stateDiagram-OKZ733FA-BIHk9QCC.js} +1 -1
  85. package/dist/client/assets/stateDiagram-v2-UEYNNEHI-Cqw0H5cq.js +1 -0
  86. package/dist/client/assets/{swimlanes-SLNWSIFB-BIfNt9T9.js → swimlanes-SLNWSIFB-7g_XCosC.js} +2 -2
  87. package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-BzKpdiGK.js +8 -0
  88. package/dist/client/assets/{tanstack-Bsd5UOY4.js → tanstack-B8_amOz8.js} +1 -1
  89. package/dist/client/assets/{timeline-definition-Z64GVDOM-CXwiFUZq.js → timeline-definition-Z64GVDOM-B-cgSggY.js} +1 -1
  90. package/dist/client/assets/{vennDiagram-T6HMQDX7-D9Rhew2E.js → vennDiagram-T6HMQDX7-Bt5-8VZ2.js} +1 -1
  91. package/dist/client/assets/{viewer-tabs-BW5-uV6M.js → viewer-tabs-DxCOe0ut.js} +1 -1
  92. package/dist/client/assets/{wardleyDiagram-T6FBY63Y-CzCcAcBb.js → wardleyDiagram-T6FBY63Y-B5Imx6zh.js} +1 -1
  93. package/dist/client/assets/{xychartDiagram-ELKLHX3M-BfXVbrBJ.js → xychartDiagram-ELKLHX3M-CDaY4xFS.js} +1 -1
  94. package/dist/client/index.html +4 -4
  95. package/dist/dev.mjs +4 -4
  96. package/dist/manifest.json +1 -1
  97. package/dist/server.mjs +59 -8
  98. package/dist/service-update.mjs +12 -10
  99. package/package.json +1 -1
  100. package/dist/chunks/cli-service-B939lTxw.mjs +0 -237
  101. package/dist/client/assets/App-BWpP-n84.js +0 -118
  102. package/dist/client/assets/CreatePullRequestDialog-CQKBUxKz.js +0 -2
  103. package/dist/client/assets/HistoryView-Dev4CVOY.js +0 -1
  104. package/dist/client/assets/PierreEditBoundary-Cu19fc5Z.js +0 -2
  105. package/dist/client/assets/PullRequestsView-Brg-sbGb.js +0 -1
  106. package/dist/client/assets/RepositoryProjectsDialog-BIUnKMb-.js +0 -1
  107. package/dist/client/assets/SearchView-C5_R_0ZU.js +0 -1
  108. package/dist/client/assets/SettingsDialog-CyJA7ajO.js +0 -9
  109. package/dist/client/assets/Viewer-xMTNRGDO.js +0 -3
  110. package/dist/client/assets/channel-DUwjky93.js +0 -1
  111. package/dist/client/assets/classDiagram-JCYQIIEL-BtqFLKHp.js +0 -1
  112. package/dist/client/assets/classDiagram-v2-OCEON4UE-BtqFLKHp.js +0 -1
  113. package/dist/client/assets/index-DXR6FOp1.css +0 -1
  114. package/dist/client/assets/index-DaaX_e8C.js +0 -3
  115. package/dist/client/assets/stateDiagram-v2-UEYNNEHI-BSIBplQO.js +0 -1
  116. package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-DGRBYdxa.js +0 -8
@@ -0,0 +1,394 @@
1
+ import { readFile, writeFile, rename, rm, mkdir } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { setTimeout } from "node:timers/promises";
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ const execute = promisify(execFile);
9
+ const SERVICE_NAME = "opentig.service";
10
+ const serviceUnitPath = () => path.join(os.homedir(), ".config/systemd/user", SERVICE_NAME);
11
+ const stableVersion = (value) => typeof value === "string" && /^\d+\.\d+\.\d+$/.test(value);
12
+ function newerVersion(a, b) {
13
+ if (!stableVersion(a) || !stableVersion(b)) return false;
14
+ const left = a.split(".").map(Number), right = b.split(".").map(Number);
15
+ for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i] > right[i];
16
+ return false;
17
+ }
18
+ function packageDirectory(home, install) {
19
+ if (!stableVersion(install.version) || !["flat", "npm"].includes(install.layout)) throw new Error("Invalid managed service version.");
20
+ const base = path.join(home, "service", `app-${install.version}`);
21
+ return install.layout === "flat" ? base : path.join(base, "node_modules/@opentig/cli");
22
+ }
23
+ async function readInstallation(home) {
24
+ let value;
25
+ try {
26
+ value = JSON.parse(await readFile(path.join(home, "service/installation.json"), "utf8"));
27
+ } catch (error) {
28
+ if (error.code === "ENOENT") return null;
29
+ throw error;
30
+ }
31
+ if (value.manager !== void 0 && !["systemd", "launchd", "windows-task"].includes(value.manager) || value.schema !== 1 || !stableVersion(value.version) || !["flat", "npm"].includes(value.layout) || typeof value.host !== "string" || !/^[a-zA-Z0-9.:[\]-]+$/.test(value.host) || !Number.isInteger(value.port) || value.port < 1 || value.port > 65535 || value.environmentPath !== void 0 && (typeof value.environmentPath !== "string" || /[\0\r\n]/.test(value.environmentPath)) || typeof value.node !== "string" || !path.isAbsolute(value.node) || /[\0\r\n]/.test(value.node)) throw new Error("Invalid managed service installation.");
32
+ return value;
33
+ }
34
+ async function atomicWrite(file, contents) {
35
+ const temporary = `${file}.${process.pid}.tmp`;
36
+ await writeFile(temporary, contents, { mode: 384 });
37
+ await rename(temporary, file);
38
+ }
39
+ async function saveInstallation(home, installation) {
40
+ await atomicWrite(path.join(home, "service/installation.json"), JSON.stringify(installation));
41
+ }
42
+ async function systemctl(...args) {
43
+ return (await execute("systemctl", ["--user", ...args], { timeout: 3e4 })).stdout.trim();
44
+ }
45
+ function renderSystemdUnit(input) {
46
+ const args = [
47
+ input.nodeExecutable,
48
+ input.cliEntrypoint,
49
+ "serve",
50
+ "--host",
51
+ input.host,
52
+ "--port",
53
+ String(input.port),
54
+ "--home",
55
+ input.home
56
+ ];
57
+ return `[Unit]
58
+ Description=OpenTig browser Git client
59
+ After=network.target
60
+
61
+ [Service]
62
+ Type=simple
63
+ WorkingDirectory=${systemdWorkingDirectory(input.home)}
64
+ ExecStart=${args.map((arg) => systemdQuote(arg.replaceAll("$", () => "$$"))).join(" ")}
65
+ Environment=NODE_ENV=production
66
+ ${input.environmentPath === void 0 ? "" : `Environment=${systemdQuote(`PATH=${input.environmentPath}`)}
67
+ `}Restart=on-failure
68
+ RestartSec=3
69
+ KillSignal=SIGTERM
70
+ TimeoutStopSec=10
71
+
72
+ [Install]
73
+ WantedBy=default.target
74
+ `;
75
+ }
76
+ function systemdQuote(value) {
77
+ if (value.includes("\0") || value.includes("\r") || value.includes("\n")) throw new Error("Service paths and arguments cannot contain control characters.");
78
+ return `"${value.replaceAll("%", "%%").replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
79
+ }
80
+ function systemdWorkingDirectory(value) {
81
+ if (!path.isAbsolute(value) || /[\0\r\n]/.test(value)) throw new Error("Service home must be an absolute single-line path.");
82
+ return `${value.replaceAll("%", "%%")}/`;
83
+ }
84
+ function xml(value) {
85
+ if ([...value].some((char) => char.charCodeAt(0) < 32 && ![" ", "\n", "\r"].includes(char))) throw new Error("Service arguments cannot contain control characters.");
86
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
87
+ }
88
+ function serviceOwnerMarker(home) {
89
+ return `OpenTig CLI home: ${Buffer.from(home, "utf8").toString("base64")}`;
90
+ }
91
+ function psQuote(value) {
92
+ return `'${value.replaceAll("'", "''")}'`;
93
+ }
94
+ function encodedPowerShell(script) {
95
+ return Buffer.from(script, "utf16le").toString("base64");
96
+ }
97
+ function serviceArgs(input) {
98
+ return [input.cliEntrypoint, "serve", "--host", input.host, "--port", String(input.port), "--home", input.home];
99
+ }
100
+ function renderLaunchAgent(input, label) {
101
+ const args = [input.nodeExecutable, ...serviceArgs(input)];
102
+ return `<?xml version="1.0" encoding="UTF-8"?>
103
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
104
+ <plist version="1.0"><dict>
105
+ <key>Label</key><string>${xml(label)}</string>
106
+ <key>ProgramArguments</key><array>${args.map((arg) => `<string>${xml(arg)}</string>`).join("")}</array>
107
+ <key>WorkingDirectory</key><string>${xml(input.home)}</string>
108
+ <key>EnvironmentVariables</key><dict>
109
+ <key>NODE_ENV</key><string>production</string>
110
+ <key>OPENTIG_SERVICE_HOME</key><string>${xml(input.home)}</string>
111
+ ${input.environmentPath === void 0 ? "" : `<key>PATH</key><string>${xml(input.environmentPath)}</string>`}
112
+ </dict>
113
+ <key>RunAtLoad</key><true/>
114
+ <key>KeepAlive</key><true/>
115
+ <key>ThrottleInterval</key><integer>3</integer>
116
+ <key>ExitTimeOut</key><integer>10</integer>
117
+ </dict></plist>
118
+ `;
119
+ }
120
+ function windowsServerScript(input) {
121
+ return `$ErrorActionPreference = 'Stop'; $env:NODE_ENV = 'production'; $env:OPENTIG_SERVICE_HOME = ${psQuote(input.home)}; ` + (input.environmentPath === void 0 ? "" : `$env:PATH = ${psQuote(input.environmentPath)}; `) + `& ${[input.nodeExecutable, ...serviceArgs(input)].map(psQuote).join(" ")} *> $null; exit $LASTEXITCODE`;
122
+ }
123
+ function renderWindowsTask(input) {
124
+ return `<?xml version="1.0" encoding="UTF-16"?>
125
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
126
+ <RegistrationInfo><Description>${serviceOwnerMarker(input.home)}</Description></RegistrationInfo>
127
+ <Triggers>${input.worker ? "" : `<LogonTrigger><Enabled>true</Enabled><UserId>${xml(input.sid)}</UserId></LogonTrigger>`}</Triggers>
128
+ <Principals><Principal id="Owner"><UserId>${xml(input.sid)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
129
+ <Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries><AllowHardTerminate>true</AllowHardTerminate><StartWhenAvailable>true</StartWhenAvailable><AllowStartOnDemand>true</AllowStartOnDemand><Enabled>true</Enabled><ExecutionTimeLimit>${input.worker ? "PT5M" : "PT0S"}</ExecutionTimeLimit>${input.worker ? "" : "<RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure>"}</Settings>
130
+ <Actions Context="Owner"><Exec><Command>${xml(input.powershell)}</Command><Arguments>-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ${encodedPowerShell(input.script)}</Arguments><WorkingDirectory>${xml(input.home)}</WorkingDirectory></Exec></Actions>
131
+ </Task>`;
132
+ }
133
+ async function fileOrNull(file) {
134
+ try {
135
+ return await readFile(file, "utf8");
136
+ } catch (error) {
137
+ if (error.code === "ENOENT") return null;
138
+ throw error;
139
+ }
140
+ }
141
+ async function writeDefinition(file, definition) {
142
+ await mkdir(path.dirname(file), { recursive: true, mode: 448 });
143
+ await atomicWrite(file, definition);
144
+ }
145
+ function managerKind(platform = process.platform) {
146
+ if (platform === "linux") return "systemd";
147
+ if (platform === "darwin") return "launchd";
148
+ if (platform === "win32") return "windows-task";
149
+ throw new Error("Managed CLI services support Linux, macOS, and Windows.");
150
+ }
151
+ function assertManager(installation, kind) {
152
+ if ((installation.manager ?? "systemd") !== kind) throw new Error("This service was installed for another operating system.");
153
+ }
154
+ async function createServiceManager(home, identity) {
155
+ switch (managerKind()) {
156
+ case "systemd":
157
+ return linuxManager(home, identity);
158
+ case "launchd":
159
+ return macManager(home);
160
+ case "windows-task":
161
+ return windowsManager(home);
162
+ }
163
+ }
164
+ function linuxManager(home, identity) {
165
+ const name = SERVICE_NAME;
166
+ const file = identity ? path.join(path.dirname(serviceUnitPath()), name) : serviceUnitPath();
167
+ const status = async () => {
168
+ const properties = await systemctl("show", name, "--property=MainPID,ActiveState,UnitFileState");
169
+ const values = Object.fromEntries(properties.split("\n").map((line) => line.split("=")));
170
+ return { running: values.ActiveState === "active", enabled: values.UnitFileState === "enabled", pid: Number(values.MainPID) };
171
+ };
172
+ return {
173
+ kind: "systemd",
174
+ location: file,
175
+ startup: "Starts at boot and continues after logout (systemd user service with lingering).",
176
+ preflight: async () => {
177
+ await systemctl("show-environment");
178
+ try {
179
+ await execute("loginctl", ["enable-linger"], { timeout: 15e3 });
180
+ } catch {
181
+ throw new Error("Lingering is required. Ask an administrator to run: sudo loginctl enable-linger <your-user>. Then retry service install as your normal user.");
182
+ }
183
+ },
184
+ render: renderSystemdUnit,
185
+ read: () => fileOrNull(file),
186
+ owns: (definition) => definition.includes(`WorkingDirectory=${home.replaceAll("%", "%%")}/
187
+ `),
188
+ write: (definition) => writeDefinition(file, definition),
189
+ restart: async () => {
190
+ await systemctl("daemon-reload");
191
+ await systemctl("enable", name);
192
+ await systemctl("restart", name);
193
+ },
194
+ stop: async () => {
195
+ await systemctl("disable", "--now", name);
196
+ },
197
+ remove: async () => {
198
+ await rm(file, { force: true });
199
+ await systemctl("daemon-reload");
200
+ },
201
+ status,
202
+ ownsProcess: async () => Boolean(process.env.INVOCATION_ID) && await systemctl("show", name, "--property=FragmentPath", "--value") === file && (await status()).pid === process.pid,
203
+ launchWorker: async (node, entry) => {
204
+ const unit = `opentig-update-${randomUUID()}`;
205
+ await execute("systemd-run", ["--user", "--collect", "--unit", unit, "--property=Type=exec", node, entry, home], { timeout: 15e3 });
206
+ return { running: async () => ["active", "activating", "deactivating"].includes(await systemctl("show", unit, "--property=ActiveState", "--value").catch(() => "inactive")), cleanup: async () => void 0 };
207
+ },
208
+ cleanupWorker: async () => void 0
209
+ };
210
+ }
211
+ function macManager(home, identity) {
212
+ const label = "com.opentig.cli";
213
+ const domain = `gui/${process.getuid()}`;
214
+ const job = `${domain}/${label}`;
215
+ const file = path.join(os.homedir(), "Library/LaunchAgents", `${label}.plist`);
216
+ const launchctl = async (...args) => (await execute("/bin/launchctl", args, { timeout: 3e4 })).stdout;
217
+ const inspect = async (target) => {
218
+ try {
219
+ return await launchctl("print", target);
220
+ } catch (error) {
221
+ if (error.code === 113) return null;
222
+ throw error;
223
+ }
224
+ };
225
+ const stop = async () => {
226
+ const info = await inspect(job);
227
+ if (info === null) return;
228
+ const pid = Number(info.match(/\bpid = (\d+)/)?.[1]);
229
+ await launchctl("bootout", job);
230
+ for (let i = 0; i < 150; i++) {
231
+ let alive = false;
232
+ if (pid > 0) {
233
+ try {
234
+ process.kill(pid, 0);
235
+ alive = true;
236
+ } catch (error) {
237
+ if (error.code !== "ESRCH") throw error;
238
+ }
239
+ }
240
+ if (!alive && await inspect(job) === null) return;
241
+ await setTimeout(100);
242
+ }
243
+ throw new Error("The macOS service did not finish stopping.");
244
+ };
245
+ return {
246
+ kind: "launchd",
247
+ location: file,
248
+ startup: "Starts at macOS login and stops at logout. Keep the user signed in and the Mac awake for remote access.",
249
+ preflight: async () => {
250
+ try {
251
+ await launchctl("print", domain);
252
+ } catch {
253
+ throw new Error("No macOS login session is available. Sign in on the Mac, then run service install as that user.");
254
+ }
255
+ },
256
+ render: (input) => renderLaunchAgent(input, label),
257
+ read: () => fileOrNull(file),
258
+ owns: (definition) => definition.includes(`<key>OPENTIG_SERVICE_HOME</key><string>${xml(home)}</string>`),
259
+ write: (definition) => writeDefinition(file, definition),
260
+ restart: async () => {
261
+ await stop();
262
+ await launchctl("enable", job);
263
+ await launchctl("bootstrap", domain, file);
264
+ },
265
+ stop,
266
+ remove: () => rm(file, { force: true }),
267
+ status: async () => {
268
+ const info = await inspect(job);
269
+ const disabled = await launchctl("print-disabled", domain);
270
+ return { running: /state = running/.test(info ?? ""), pid: Number(info?.match(/\bpid = (\d+)/)?.[1]), enabled: !disabled.includes(`"${label}" => true`) && await fileOrNull(file) !== null };
271
+ },
272
+ ownsProcess: async () => process.env.OPENTIG_SERVICE_HOME === home && Number((await inspect(job))?.match(/\bpid = (\d+)/)?.[1]) === process.pid,
273
+ launchWorker: async (node, entry) => {
274
+ const id = `com.opentig.update.${randomUUID()}`;
275
+ await launchctl("submit", "-l", id, "--", node, entry, home, id);
276
+ return { running: async () => /state = running/.test(await inspect(`${domain}/${id}`) ?? ""), cleanup: async () => {
277
+ if (await inspect(`${domain}/${id}`) !== null) await launchctl("bootout", `${domain}/${id}`);
278
+ } };
279
+ },
280
+ cleanupWorker: async (id) => {
281
+ if (/^com\.opentig\.update\.[a-f0-9-]+$/.test(id)) await launchctl("bootout", `${domain}/${id}`);
282
+ }
283
+ };
284
+ }
285
+ async function windowsManager(home, identity) {
286
+ const powershell = path.win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32/WindowsPowerShell/v1.0/powershell.exe");
287
+ const ps = async (script) => (await execute(powershell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encodedPowerShell(`$ErrorActionPreference = 'Stop'; [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); ${script}`)], { timeout: 3e4, windowsHide: true, maxBuffer: 2e6 })).stdout.trim();
288
+ const sid = await ps("[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value");
289
+ if (!/^S-1-[\d-]+$/.test(sid)) throw new Error("Could not determine the Windows service owner.");
290
+ const name = `OpenTig CLI ${sid}`;
291
+ const connection = "$scheduler = New-Object -ComObject 'Schedule.Service'; $scheduler.Connect(); $folder = $scheduler.GetFolder('\\'); ";
292
+ const task = (taskName) => `${connection} $task = $null; try { $task = $folder.GetTask(${psQuote(taskName)}) } catch { $e = $_.Exception; while ($null -ne $e.InnerException) { $e = $e.InnerException }; if ($e.HResult -ne -2147024894) { throw } }; `;
293
+ const register = async (taskName, definition) => {
294
+ const file = path.join(home, "service", `.task-${randomUUID()}.xml`);
295
+ await mkdir(path.dirname(file), { recursive: true, mode: 448 });
296
+ try {
297
+ await writeFile(file, definition, { encoding: "utf16le", mode: 384 });
298
+ await ps(`${connection} $xml = [IO.File]::ReadAllText(${psQuote(file)}, [Text.Encoding]::Unicode); $null = $folder.RegisterTask(${psQuote(taskName)}, $xml, 6, ${psQuote(sid)}, $null, 3, $null)`);
299
+ } finally {
300
+ await rm(file, { force: true });
301
+ }
302
+ };
303
+ const stopTask = async (taskName) => {
304
+ await ps(`${task(taskName)}
305
+ if ($null -ne $task) {
306
+ $task.Enabled = $false
307
+ $action = $task.Definition.Actions.Item(1)
308
+ $arguments = [string]$action.Arguments
309
+ if ($arguments -notmatch '-EncodedCommand ([A-Za-z0-9+/=]+)$') { throw 'Unexpected OpenTig task action' }
310
+ $encoded = $Matches[1]
311
+ $pattern = '(?i)(?:^|\\s)-EncodedCommand\\s+"?' + [regex]::Escape($encoded) + '"?\\s*$'
312
+ # Stop the PowerShell wrapper AND its descendants before Scheduler loses the
313
+ # parent PID. Stop() alone can leave Node running and holding the data directory.
314
+ $wrappers = @(Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe'" | Where-Object {
315
+ $_.ExecutablePath -eq $action.Path -and $_.CommandLine -match $pattern
316
+ })
317
+ foreach ($wrapper in $wrappers) {
318
+ $owner = Invoke-CimMethod -InputObject $wrapper -MethodName GetOwnerSid
319
+ if ($owner.ReturnValue -ne 0 -or $owner.Sid -ne ${psQuote(sid)}) { throw 'Cannot verify OpenTig task process owner' }
320
+ & "$env:SystemRoot\\System32\\taskkill.exe" /PID $wrapper.ProcessId /T /F | Out-Null
321
+ if ($LASTEXITCODE -ne 0 -and (Get-Process -Id $wrapper.ProcessId -ErrorAction SilentlyContinue)) { throw 'OpenTig process tree did not stop' }
322
+ }
323
+ $task.Stop(0)
324
+ for ($i = 0; $i -lt 100 -and $task.State -in @(2, 4); $i++) {
325
+ Start-Sleep -Milliseconds 100
326
+ $task = $folder.GetTask(${psQuote(taskName)})
327
+ }
328
+ if ($task.State -in @(2, 4)) { throw 'OpenTig task did not stop' }
329
+ }`);
330
+ };
331
+ const removeTask = async (taskName) => {
332
+ await ps(`${task(taskName)} if ($null -ne $task) { $folder.DeleteTask(${psQuote(taskName)}, 0) }`);
333
+ };
334
+ const startTask = async (taskName) => {
335
+ await ps(`${task(taskName)} if ($null -eq $task) { throw 'OpenTig task is missing' }; $task.Enabled = $true; $null = $task.Run($null)`);
336
+ };
337
+ const state = async (taskName) => JSON.parse(await ps(`${task(taskName)} @{ running = ($null -ne $task -and $task.State -eq 4); enabled = ($null -ne $task -and $task.Enabled); starting = ($null -ne $task -and $task.State -eq 2) } | ConvertTo-Json -Compress`));
338
+ return {
339
+ kind: "windows-task",
340
+ location: `Task Scheduler: ${name}`,
341
+ startup: "Starts at Windows login and stops at logout. Keep the user signed in and the PC awake for remote access.",
342
+ preflight: async () => {
343
+ await ps(connection);
344
+ },
345
+ render: (input) => renderWindowsTask({ sid, powershell, home, script: windowsServerScript(input) }),
346
+ read: async () => await ps(`${task(name)} if ($null -ne $task) { $task.Xml }`) || null,
347
+ owns: (definition) => definition.includes(`<Description>${serviceOwnerMarker(home)}</Description>`),
348
+ // Stop using the OLD action before overwriting its identifying arguments.
349
+ write: async (definition) => {
350
+ await stopTask(name);
351
+ await register(name, definition);
352
+ },
353
+ restart: async () => {
354
+ await stopTask(name);
355
+ await startTask(name);
356
+ },
357
+ stop: () => stopTask(name),
358
+ remove: () => removeTask(name),
359
+ status: () => state(name),
360
+ ownsProcess: async () => process.env.OPENTIG_SERVICE_HOME === home && (await state(name)).running,
361
+ launchWorker: async (node, entry) => {
362
+ const id = `OpenTig Update ${randomUUID()}`;
363
+ const script = `& ${[node, entry, home, id].map(psQuote).join(" ")}; exit $LASTEXITCODE`;
364
+ await register(id, renderWindowsTask({ sid, powershell, script, home, worker: true }));
365
+ try {
366
+ await startTask(id);
367
+ } catch (error) {
368
+ await removeTask(id);
369
+ throw error;
370
+ }
371
+ return { running: async () => {
372
+ const current = await state(id);
373
+ return current.running || current.starting === true;
374
+ }, cleanup: async () => {
375
+ await stopTask(id);
376
+ await removeTask(id);
377
+ } };
378
+ },
379
+ cleanupWorker: async (id) => {
380
+ if (/^OpenTig Update [a-f0-9-]+$/.test(id)) await removeTask(id);
381
+ }
382
+ };
383
+ }
384
+ export {
385
+ assertManager as a,
386
+ atomicWrite as b,
387
+ createServiceManager as c,
388
+ stableVersion as d,
389
+ execute as e,
390
+ newerVersion as n,
391
+ packageDirectory as p,
392
+ readInstallation as r,
393
+ saveInstallation as s
394
+ };