rigscope 0.0.0-stage → 0.1.0

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 (188) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-path-routes-manifest.json +13 -0
  3. package/.next/build-manifest.json +20 -0
  4. package/.next/export-marker.json +6 -0
  5. package/.next/images-manifest.json +68 -0
  6. package/.next/next-minimal-server.js.nft.json +1 -0
  7. package/.next/next-server.js.nft.json +1 -0
  8. package/.next/package.json +1 -0
  9. package/.next/prerender-manifest.json +297 -0
  10. package/.next/react-loadable-manifest.json +1 -0
  11. package/.next/required-server-files.json +369 -0
  12. package/.next/routes-manifest.json +114 -0
  13. package/.next/server/app/_global-error/page.js +31 -0
  14. package/.next/server/app/_global-error/page.js.nft.json +1 -0
  15. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -0
  16. package/.next/server/app/_global-error.html +1 -0
  17. package/.next/server/app/_global-error.meta +35 -0
  18. package/.next/server/app/_global-error.rsc +18 -0
  19. package/.next/server/app/_global-error.segments/_full.segment.rsc +18 -0
  20. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +24 -0
  21. package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -0
  22. package/.next/server/app/_not-found/page.js +1 -0
  23. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  24. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  25. package/.next/server/app/_not-found.html +1 -0
  26. package/.next/server/app/_not-found.meta +35 -0
  27. package/.next/server/app/_not-found.rsc +24 -0
  28. package/.next/server/app/_not-found.segments/_full.segment.rsc +24 -0
  29. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +30 -0
  30. package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -0
  31. package/.next/server/app/administration/page.js +1 -0
  32. package/.next/server/app/administration/page.js.nft.json +1 -0
  33. package/.next/server/app/administration/page_client-reference-manifest.js +1 -0
  34. package/.next/server/app/administration.html +1 -0
  35. package/.next/server/app/administration.meta +34 -0
  36. package/.next/server/app/administration.rsc +29 -0
  37. package/.next/server/app/administration.segments/_full.segment.rsc +29 -0
  38. package/.next/server/app/administration.segments/_tree.segment.rsc +4 -0
  39. package/.next/server/app/administration.segments/administration/__PAGE__.segment.rsc +35 -0
  40. package/.next/server/app/alerts/page.js +1 -0
  41. package/.next/server/app/alerts/page.js.nft.json +1 -0
  42. package/.next/server/app/alerts/page_client-reference-manifest.js +1 -0
  43. package/.next/server/app/alerts.html +1 -0
  44. package/.next/server/app/alerts.meta +34 -0
  45. package/.next/server/app/alerts.rsc +28 -0
  46. package/.next/server/app/alerts.segments/_full.segment.rsc +28 -0
  47. package/.next/server/app/alerts.segments/_tree.segment.rsc +4 -0
  48. package/.next/server/app/alerts.segments/alerts/__PAGE__.segment.rsc +34 -0
  49. package/.next/server/app/connections/page.js +1 -0
  50. package/.next/server/app/connections/page.js.nft.json +1 -0
  51. package/.next/server/app/connections/page_client-reference-manifest.js +1 -0
  52. package/.next/server/app/connections.html +1 -0
  53. package/.next/server/app/connections.meta +34 -0
  54. package/.next/server/app/connections.rsc +29 -0
  55. package/.next/server/app/connections.segments/_full.segment.rsc +29 -0
  56. package/.next/server/app/connections.segments/_tree.segment.rsc +4 -0
  57. package/.next/server/app/connections.segments/connections/__PAGE__.segment.rsc +35 -0
  58. package/.next/server/app/history/page.js +2 -0
  59. package/.next/server/app/history/page.js.nft.json +1 -0
  60. package/.next/server/app/history/page_client-reference-manifest.js +1 -0
  61. package/.next/server/app/history.html +1 -0
  62. package/.next/server/app/history.meta +34 -0
  63. package/.next/server/app/history.rsc +33 -0
  64. package/.next/server/app/history.segments/_full.segment.rsc +33 -0
  65. package/.next/server/app/history.segments/_tree.segment.rsc +4 -0
  66. package/.next/server/app/history.segments/history/__PAGE__.segment.rsc +37 -0
  67. package/.next/server/app/icon.svg/route.js +1 -0
  68. package/.next/server/app/icon.svg/route.js.nft.json +1 -0
  69. package/.next/server/app/icon.svg.body +1 -0
  70. package/.next/server/app/icon.svg.meta +1 -0
  71. package/.next/server/app/index.html +1 -0
  72. package/.next/server/app/index.meta +29 -0
  73. package/.next/server/app/index.rsc +26 -0
  74. package/.next/server/app/index.segments/__PAGE__.segment.rsc +31 -0
  75. package/.next/server/app/index.segments/_full.segment.rsc +26 -0
  76. package/.next/server/app/index.segments/_tree.segment.rsc +3 -0
  77. package/.next/server/app/login/page.js +1 -0
  78. package/.next/server/app/login/page.js.nft.json +1 -0
  79. package/.next/server/app/login/page_client-reference-manifest.js +1 -0
  80. package/.next/server/app/login.html +1 -0
  81. package/.next/server/app/login.meta +34 -0
  82. package/.next/server/app/login.rsc +26 -0
  83. package/.next/server/app/login.segments/_full.segment.rsc +26 -0
  84. package/.next/server/app/login.segments/_tree.segment.rsc +4 -0
  85. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +32 -0
  86. package/.next/server/app/machines/[machineId]/page.js +1 -0
  87. package/.next/server/app/machines/[machineId]/page.js.nft.json +1 -0
  88. package/.next/server/app/machines/[machineId]/page_client-reference-manifest.js +1 -0
  89. package/.next/server/app/page.js +1 -0
  90. package/.next/server/app/page.js.nft.json +1 -0
  91. package/.next/server/app/page_client-reference-manifest.js +1 -0
  92. package/.next/server/app/processes/page.js +2 -0
  93. package/.next/server/app/processes/page.js.nft.json +1 -0
  94. package/.next/server/app/processes/page_client-reference-manifest.js +1 -0
  95. package/.next/server/app/processes.html +1 -0
  96. package/.next/server/app/processes.meta +34 -0
  97. package/.next/server/app/processes.rsc +30 -0
  98. package/.next/server/app/processes.segments/_full.segment.rsc +30 -0
  99. package/.next/server/app/processes.segments/_tree.segment.rsc +4 -0
  100. package/.next/server/app/processes.segments/processes/__PAGE__.segment.rsc +36 -0
  101. package/.next/server/app-paths-manifest.json +13 -0
  102. package/.next/server/chunks/161.js +1 -0
  103. package/.next/server/chunks/168.js +1 -0
  104. package/.next/server/chunks/266.js +68 -0
  105. package/.next/server/chunks/430.js +116 -0
  106. package/.next/server/chunks/721.js +1 -0
  107. package/.next/server/chunks/751.js +1 -0
  108. package/.next/server/chunks/877.js +1 -0
  109. package/.next/server/chunks/894.js +2 -0
  110. package/.next/server/functions-config-manifest.json +4 -0
  111. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  112. package/.next/server/middleware-build-manifest.js +1 -0
  113. package/.next/server/middleware-manifest.json +6 -0
  114. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  115. package/.next/server/next-font-manifest.js +1 -0
  116. package/.next/server/next-font-manifest.json +1 -0
  117. package/.next/server/pages/404.html +1 -0
  118. package/.next/server/pages/500.html +1 -0
  119. package/.next/server/pages-manifest.json +4 -0
  120. package/.next/server/prefetch-hints.json +123 -0
  121. package/.next/server/preview-props.json +5 -0
  122. package/.next/server/server-reference-manifest.js +1 -0
  123. package/.next/server/server-reference-manifest.json +1 -0
  124. package/.next/server/webpack-runtime.js +1 -0
  125. package/.next/static/DZKWF9ur7H5wI0c7RAhdq/_buildManifest.js +1 -0
  126. package/.next/static/DZKWF9ur7H5wI0c7RAhdq/_ssgManifest.js +1 -0
  127. package/.next/static/chunks/103-c47318242b24e9ac.js +1 -0
  128. package/.next/static/chunks/157-0d36ebf66af048ac.js +1 -0
  129. package/.next/static/chunks/227-417939c480f50747.js +1 -0
  130. package/.next/static/chunks/408-18819c6490a5f8d3.js +1 -0
  131. package/.next/static/chunks/4bd1b696-c7408084347438ae.js +1 -0
  132. package/.next/static/chunks/794-ceeb732a1a81bc27.js +52 -0
  133. package/.next/static/chunks/939-2600d7c8e583d5ef.js +1 -0
  134. package/.next/static/chunks/app/_global-error/page-aba28f88d5781771.js +1 -0
  135. package/.next/static/chunks/app/_not-found/page-a2a510233e4836e2.js +1 -0
  136. package/.next/static/chunks/app/administration/page-4b16b19bb6a39c36.js +1 -0
  137. package/.next/static/chunks/app/alerts/page-fe0e877778c60af8.js +1 -0
  138. package/.next/static/chunks/app/connections/page-99cd54d6929b3f1f.js +1 -0
  139. package/.next/static/chunks/app/history/page-3fa5846eca26b7f7.js +3 -0
  140. package/.next/static/chunks/app/layout-4feff677c4071745.js +1 -0
  141. package/.next/static/chunks/app/login/page-c57e12dbe5146737.js +1 -0
  142. package/.next/static/chunks/app/machines/[machineId]/page-a5aa1d8676272f38.js +1 -0
  143. package/.next/static/chunks/app/page-1ae9fc94c897785b.js +1 -0
  144. package/.next/static/chunks/app/processes/page-e471ed23a1d5632f.js +3 -0
  145. package/.next/static/chunks/framework-ea4f97ca039bd777.js +1 -0
  146. package/.next/static/chunks/main-a458d32948147b84.js +5 -0
  147. package/.next/static/chunks/main-app-f92328c8950d2e53.js +1 -0
  148. package/.next/static/chunks/next/dist/client/components/builtin/app-error-aba28f88d5781771.js +1 -0
  149. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-aba28f88d5781771.js +1 -0
  150. package/.next/static/chunks/next/dist/client/components/builtin/global-error-43cbaa3aa1576eb4.js +1 -0
  151. package/.next/static/chunks/next/dist/client/components/builtin/not-found-aba28f88d5781771.js +1 -0
  152. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-aba28f88d5781771.js +1 -0
  153. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  154. package/.next/static/chunks/webpack-9ca43f4ce1bb8938.js +1 -0
  155. package/.next/static/css/e41ccbccc3afaae5.css +1 -0
  156. package/ARCHITECTURE.md +279 -0
  157. package/CHANGELOG.md +25 -0
  158. package/DESIGN.md +299 -0
  159. package/LICENSE +21 -0
  160. package/README.md +157 -2
  161. package/SECURITY.md +12 -0
  162. package/THIRD_PARTY_NOTICES.md +8847 -0
  163. package/bin/rigscope.cjs +6 -0
  164. package/dist/cli.cjs +9075 -0
  165. package/dist/collector.cjs +5671 -0
  166. package/dist/hub.cjs +8355 -0
  167. package/dist/ops.cjs +4972 -0
  168. package/dist/system.cjs +4333 -0
  169. package/docs/bottlenecks.md +62 -0
  170. package/docs/development.md +46 -0
  171. package/docs/exports.md +39 -0
  172. package/docs/installation.md +477 -0
  173. package/docs/operations.md +515 -0
  174. package/docs/releasing.md +152 -0
  175. package/docs/scale-acceptance.json +412 -0
  176. package/docs/scale-validation.json +1898 -0
  177. package/docs/scale-validation.md +263 -0
  178. package/next.config.js +38 -0
  179. package/package.json +109 -4
  180. package/packaging/README.md +5 -0
  181. package/packaging/collector.env.example +24 -0
  182. package/packaging/hub.env.example +31 -0
  183. package/packaging/system/90-rigscope-rapl.rules +7 -0
  184. package/packaging/system/README.md +5 -0
  185. package/packaging/system/rigscope-collector.service.example +53 -0
  186. package/packaging/system/rigscope-hub.service.example +50 -0
  187. package/packaging/system/rigscope-rapl.sysusers +5 -0
  188. package/packaging/system/rigscope.sysusers +7 -0
@@ -0,0 +1,4333 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/cli/system.ts
21
+ var system_exports = {};
22
+ __export(system_exports, {
23
+ COMPONENTS: () => COMPONENTS,
24
+ DEFAULT_NODE_PATH: () => DEFAULT_NODE_PATH,
25
+ DEFAULT_SYSTEM_ACCOUNTS: () => DEFAULT_SYSTEM_ACCOUNTS,
26
+ POWER_STATE_FILE_NAME: () => POWER_STATE_FILE_NAME,
27
+ POWER_SYSUSERS_NAME: () => POWER_SYSUSERS_NAME,
28
+ RAPL_RULE_NAME: () => RAPL_RULE_NAME,
29
+ STATE_FILE_NAME: () => STATE_FILE_NAME,
30
+ SYSTEM_SCOPE: () => SYSTEM_SCOPE,
31
+ SYSTEM_STATE_FORMAT_VERSION: () => SYSTEM_STATE_FORMAT_VERSION,
32
+ SYSUSERS_NAME: () => SYSUSERS_NAME,
33
+ SystemError: () => SystemError,
34
+ UNIT_NAMES: () => UNIT_NAMES,
35
+ assertDirectoryStopped: () => assertDirectoryStopped,
36
+ assertManagedFileOwnership: () => assertManagedFileOwnership,
37
+ collectorEnvPath: () => collectorEnvPath,
38
+ collectorStatePath: () => collectorStatePath,
39
+ componentEnabled: () => componentEnabled,
40
+ componentSet: () => componentSet,
41
+ componentsForRole: () => componentsForRole,
42
+ connectDestinationUrl: () => connectDestinationUrl,
43
+ connectSystem: () => connectSystem,
44
+ createSystemHost: () => createSystemHost,
45
+ defaultSystemPaths: () => defaultSystemPaths,
46
+ detectPowerDriver: () => detectPowerDriver,
47
+ detectUserInstallations: () => detectUserInstallations,
48
+ disableSystem: () => disableSystem,
49
+ enableSystem: () => enableSystem,
50
+ enabledComponents: () => enabledComponents,
51
+ formatSystemReport: () => formatSystemReport,
52
+ grantPower: () => grantPower,
53
+ hubDatabasePath: () => hubDatabasePath,
54
+ hubEnrollmentPath: () => hubEnrollmentPath,
55
+ hubEnvPath: () => hubEnvPath,
56
+ hubSocketPath: () => hubSocketPath,
57
+ inspectPowerAccess: () => inspectPowerAccess,
58
+ installSystem: () => installSystem,
59
+ powerStatus: () => powerStatus,
60
+ powerSysusersPath: () => powerSysusersPath,
61
+ raplRulePath: () => raplRulePath,
62
+ readSystemState: () => readSystemState,
63
+ renderPowerSysusers: () => renderPowerSysusers,
64
+ renderRaplRule: () => renderRaplRule,
65
+ renderSystemCollectorUnit: () => renderSystemCollectorUnit,
66
+ renderSystemHubUnit: () => renderSystemHubUnit,
67
+ renderSysusers: () => renderSysusers,
68
+ restartSystem: () => restartSystem,
69
+ revokePower: () => revokePower,
70
+ rolesFor: () => rolesFor,
71
+ rollbackSystem: () => rollbackSystem,
72
+ runSystemCommand: () => runSystemCommand,
73
+ startSystem: () => startSystem,
74
+ stateFilePath: () => stateFilePath,
75
+ stopSystem: () => stopSystem,
76
+ systemStatus: () => systemStatus,
77
+ systemdQuote: () => systemdQuote,
78
+ sysusersPath: () => sysusersPath,
79
+ uninstallSystem: () => uninstallSystem,
80
+ unitFor: () => unitFor,
81
+ unitNamesFor: () => unitNamesFor,
82
+ unitNamesForComponents: () => unitNamesForComponents,
83
+ upgradeSystem: () => upgradeSystem,
84
+ validateAccounts: () => validateAccounts,
85
+ validateSystemPaths: () => validateSystemPaths,
86
+ verifySystemHealth: () => verifySystemHealth
87
+ });
88
+ module.exports = __toCommonJS(system_exports);
89
+ var import_node_fs3 = require("node:fs");
90
+ var import_node_child_process = require("node:child_process");
91
+ var import_node_crypto5 = require("node:crypto");
92
+ var import_node_path3 = require("node:path");
93
+ var import_node_sqlite = require("node:sqlite");
94
+ var import_node_util = require("node:util");
95
+
96
+ // src/cli/health.ts
97
+ var import_node_http = require("node:http");
98
+ var MAX_HEALTH_BODY_BYTES = 64 * 1024;
99
+ function probeHealth(target, timeoutMs = 3e3) {
100
+ return new Promise((resolve3) => {
101
+ let settled = false;
102
+ const finish = (result) => {
103
+ if (settled) return;
104
+ settled = true;
105
+ resolve3(result);
106
+ };
107
+ const options = target.kind === "unix" ? { socketPath: target.socketPath, path: target.path, method: "GET", headers: { Host: target.hostHeader } } : { host: target.host, port: target.port, path: target.path, method: "GET" };
108
+ let req;
109
+ try {
110
+ req = (0, import_node_http.request)(options, (res) => {
111
+ const chunks = [];
112
+ let bytes = 0;
113
+ res.on("data", (chunk) => {
114
+ bytes += chunk.length;
115
+ if (bytes > MAX_HEALTH_BODY_BYTES) {
116
+ req.destroy();
117
+ finish({ ok: false, statusCode: res.statusCode ?? null, body: null, error: "health response is oversized" });
118
+ return;
119
+ }
120
+ chunks.push(chunk);
121
+ });
122
+ res.on("end", () => {
123
+ const statusCode = res.statusCode ?? null;
124
+ const text = Buffer.concat(chunks).toString("utf8");
125
+ let body = null;
126
+ try {
127
+ const value = JSON.parse(text);
128
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
129
+ body = value;
130
+ }
131
+ } catch {
132
+ }
133
+ const ok = statusCode === 200 && body?.status === "ok";
134
+ finish({
135
+ ok,
136
+ statusCode,
137
+ body,
138
+ error: ok ? null : statusCode === 200 ? "health response is not ok" : `health returned HTTP ${statusCode ?? "unknown"}`
139
+ });
140
+ });
141
+ });
142
+ } catch (error) {
143
+ finish({ ok: false, statusCode: null, body: null, error: errorText(error) });
144
+ return;
145
+ }
146
+ const deadline = setTimeout(() => {
147
+ req.destroy();
148
+ finish({ ok: false, statusCode: null, body: null, error: `health probe timed out after ${timeoutMs}ms` });
149
+ }, timeoutMs);
150
+ req.once("close", () => {
151
+ clearTimeout(deadline);
152
+ finish({ ok: false, statusCode: null, body: null, error: "health connection closed before a complete response" });
153
+ });
154
+ req.on("error", (error) => {
155
+ finish({ ok: false, statusCode: null, body: null, error: errorText(error) });
156
+ });
157
+ req.end();
158
+ });
159
+ }
160
+ async function waitForHealth(target, options = {}) {
161
+ const deadlineMs = bounded(options.deadlineMs ?? 2e4, 100, 12e4);
162
+ const intervalMs = bounded(options.intervalMs ?? 250, 25, 5e3);
163
+ const probeTimeoutMs = bounded(options.probeTimeoutMs ?? 3e3, 100, 3e4);
164
+ const started = Date.now();
165
+ let attempts = 0;
166
+ let last = { ok: false, statusCode: null, body: null, error: "health was not probed" };
167
+ while (Date.now() - started < deadlineMs) {
168
+ attempts += 1;
169
+ const remaining = Math.max(100, deadlineMs - (Date.now() - started));
170
+ last = await probeHealth(target, Math.min(probeTimeoutMs, remaining));
171
+ const reported = typeof last.body?.version === "string" ? last.body.version : null;
172
+ const versionMatches = options.expectedVersion === void 0 || reported === options.expectedVersion;
173
+ if (last.ok && versionMatches) {
174
+ return { ok: true, attempts, elapsedMs: Date.now() - started, last, version: reported };
175
+ }
176
+ if (last.ok && !versionMatches) {
177
+ last = { ...last, ok: false, error: `running version ${reported} does not match expected ${options.expectedVersion}` };
178
+ }
179
+ const wait = Math.min(intervalMs, Math.max(0, deadlineMs - (Date.now() - started)));
180
+ if (wait > 0) await delay(wait);
181
+ }
182
+ return { ok: false, attempts, elapsedMs: Date.now() - started, last, version: null };
183
+ }
184
+ function bounded(value, min, max) {
185
+ return Math.min(max, Math.max(min, Number.isFinite(value) ? value : min));
186
+ }
187
+ function delay(ms) {
188
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
189
+ }
190
+ function errorText(error) {
191
+ return error instanceof Error ? error.message : String(error);
192
+ }
193
+
194
+ // src/runtime/process-lease.ts
195
+ var import_node_fs = require("node:fs");
196
+ var import_node_path = require("node:path");
197
+ var import_node_crypto = require("node:crypto");
198
+ var MAX_LOCK_BYTES = 1024;
199
+ var MAX_ACQUIRE_ATTEMPTS = 8;
200
+ var ProcessLeaseError = class extends Error {
201
+ code;
202
+ constructor(code, message) {
203
+ super(message);
204
+ this.name = "ProcessLeaseError";
205
+ this.code = code;
206
+ }
207
+ };
208
+ function acquireProcessLease(path) {
209
+ const absolute = (0, import_node_path.resolve)(path);
210
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(absolute), { recursive: true, mode: 448 });
211
+ const parent = (0, import_node_fs.lstatSync)((0, import_node_path.dirname)(absolute));
212
+ if (!parent.isDirectory() || (parent.mode & 63) !== 0) {
213
+ throw new ProcessLeaseError("unsafe", "runtime state directory must be private");
214
+ }
215
+ for (let attempt = 0; attempt < MAX_ACQUIRE_ATTEMPTS; attempt++) {
216
+ let fd;
217
+ try {
218
+ fd = (0, import_node_fs.openSync)(absolute, "wx", 384);
219
+ } catch (error) {
220
+ if (error.code !== "EEXIST") throw error;
221
+ const snapshot = inspectLock(absolute);
222
+ if (snapshot === null) {
223
+ continue;
224
+ }
225
+ if (snapshot.alive) {
226
+ throw new ProcessLeaseError(
227
+ "busy",
228
+ "another process owns this runtime state; stop it before starting another"
229
+ );
230
+ }
231
+ try {
232
+ const current = (0, import_node_fs.lstatSync)(absolute);
233
+ if (current.ino !== snapshot.stats.ino || current.dev !== snapshot.stats.dev) {
234
+ continue;
235
+ }
236
+ (0, import_node_fs.unlinkSync)(absolute);
237
+ } catch (error2) {
238
+ if (error2.code === "ENOENT") continue;
239
+ throw new ProcessLeaseError(
240
+ "unavailable",
241
+ "runtime lock could not be recovered; inspect it before recovery"
242
+ );
243
+ }
244
+ continue;
245
+ }
246
+ const inode = (0, import_node_fs.fstatSync)(fd);
247
+ try {
248
+ (0, import_node_fs.writeFileSync)(fd, JSON.stringify({ pid: process.pid, instance: (0, import_node_crypto.randomUUID)() }));
249
+ (0, import_node_fs.fsyncSync)(fd);
250
+ } catch (error) {
251
+ (0, import_node_fs.closeSync)(fd);
252
+ removeIfSameInode(absolute, inode);
253
+ throw error;
254
+ }
255
+ (0, import_node_fs.closeSync)(fd);
256
+ return createRelease(absolute, inode);
257
+ }
258
+ throw new ProcessLeaseError(
259
+ "recovering",
260
+ "runtime lease could not be acquired after repeated stale-lock recovery"
261
+ );
262
+ }
263
+ function inspectLock(path) {
264
+ let stats;
265
+ try {
266
+ stats = (0, import_node_fs.lstatSync)(path);
267
+ } catch (error) {
268
+ if (error.code === "ENOENT") return null;
269
+ throw new ProcessLeaseError(
270
+ "unavailable",
271
+ "runtime lock could not be inspected; inspect it before recovery"
272
+ );
273
+ }
274
+ if (!stats.isFile() || stats.size > MAX_LOCK_BYTES || (stats.mode & 63) !== 0) {
275
+ throw new ProcessLeaseError("unsafe", "runtime lock is unsafe; inspect it before recovery");
276
+ }
277
+ let raw;
278
+ try {
279
+ raw = (0, import_node_fs.readFileSync)(path, "utf8");
280
+ } catch (error) {
281
+ if (error.code === "ENOENT") return null;
282
+ throw new ProcessLeaseError(
283
+ "unavailable",
284
+ "runtime lock could not be read; inspect it before recovery"
285
+ );
286
+ }
287
+ let owner;
288
+ try {
289
+ owner = JSON.parse(raw);
290
+ } catch {
291
+ throw new ProcessLeaseError(
292
+ "incomplete",
293
+ "runtime lock is incomplete; inspect it before recovery"
294
+ );
295
+ }
296
+ if (owner === null || typeof owner !== "object" || Array.isArray(owner)) {
297
+ throw new ProcessLeaseError("invalid", "runtime lock has an invalid owner");
298
+ }
299
+ const pid = owner.pid;
300
+ if (!Number.isSafeInteger(pid) || pid < 1) {
301
+ throw new ProcessLeaseError("invalid", "runtime lock has an invalid owner");
302
+ }
303
+ try {
304
+ process.kill(pid, 0);
305
+ return { stats, alive: true };
306
+ } catch (error) {
307
+ const code = error.code;
308
+ if (code === "ESRCH") return { stats, alive: false };
309
+ if (code === "EPERM" || code === "EACCES") return { stats, alive: true };
310
+ throw new ProcessLeaseError(
311
+ "unavailable",
312
+ "runtime lock owner could not be verified; inspect it before recovery"
313
+ );
314
+ }
315
+ }
316
+ function createRelease(path, inode) {
317
+ let released = false;
318
+ const cleanup = () => {
319
+ removeIfSameInode(path, inode);
320
+ };
321
+ const onExit = () => {
322
+ if (released) return;
323
+ released = true;
324
+ cleanup();
325
+ };
326
+ process.once("exit", onExit);
327
+ return () => {
328
+ if (released) return;
329
+ released = true;
330
+ process.removeListener("exit", onExit);
331
+ cleanup();
332
+ };
333
+ }
334
+ function removeIfSameInode(path, inode) {
335
+ try {
336
+ const stat = (0, import_node_fs.lstatSync)(path);
337
+ if (stat.ino === inode.ino && stat.dev === inode.dev) (0, import_node_fs.unlinkSync)(path);
338
+ } catch {
339
+ }
340
+ }
341
+
342
+ // src/hub/auth/password.ts
343
+ var import_node_crypto2 = require("node:crypto");
344
+ var SCRYPT_N = 16384;
345
+ var SCRYPT_R = 8;
346
+ var SCRYPT_P = 1;
347
+ var SALT_BYTES = 16;
348
+ var KEY_BYTES = 64;
349
+ var MAX_PASSWORD_CHARS = 1024;
350
+ var HASH_PREFIX = `scrypt$${SCRYPT_N}$${SCRYPT_R}$${SCRYPT_P}$`;
351
+ var HASH_PATTERN = new RegExp(
352
+ `^scrypt\\$${SCRYPT_N}\\$${SCRYPT_R}\\$${SCRYPT_P}\\$[0-9a-f]{${SALT_BYTES * 2}}\\$[0-9a-f]{${KEY_BYTES * 2}}$`
353
+ );
354
+ function hashPassword(password) {
355
+ if (typeof password !== "string" || password.length === 0 || password.length > MAX_PASSWORD_CHARS) {
356
+ throw new Error(`password must be a string of 1..${MAX_PASSWORD_CHARS} characters`);
357
+ }
358
+ const salt = (0, import_node_crypto2.randomBytes)(SALT_BYTES);
359
+ const key = (0, import_node_crypto2.scryptSync)(password.normalize("NFKC"), salt, KEY_BYTES, {
360
+ N: SCRYPT_N,
361
+ r: SCRYPT_R,
362
+ p: SCRYPT_P
363
+ });
364
+ return `${HASH_PREFIX}${salt.toString("hex")}$${key.toString("hex")}`;
365
+ }
366
+
367
+ // collector/destinations/policy.ts
368
+ var SHARING_KEYS = [
369
+ "cpu",
370
+ "memory",
371
+ "swap",
372
+ "gpus",
373
+ "disks",
374
+ "network",
375
+ "networkAddresses",
376
+ "processes",
377
+ "processArguments"
378
+ ];
379
+
380
+ // collector/config.ts
381
+ var ConfigError = class extends Error {
382
+ issues;
383
+ constructor(issues) {
384
+ super(`invalid collector configuration:
385
+ - ${issues.join("\n- ")}`);
386
+ this.name = "ConfigError";
387
+ this.issues = [...issues];
388
+ }
389
+ };
390
+ var DEFAULT_COLLECTOR_ID = "mock-collector";
391
+ var MOCK_HOSTNAME = "mock-machine";
392
+ var CONNECT_PATH = "/api/collector/connect";
393
+ var PREVIOUS_CONNECT_PATH = "/api/collector/v1/connect";
394
+ var MAX_DESTINATIONS = 8;
395
+ var DEFAULT_INTERVAL_MS = 1e3;
396
+ var MIN_INTERVAL_MS = 100;
397
+ var MAX_INTERVAL_MS = 36e5;
398
+ var MAX_ID_LENGTH = 128;
399
+ var MAX_HOSTNAME_LENGTH = 253;
400
+ var MAX_TOKEN_LENGTH = 256;
401
+ var PRINTABLE_ASCII = /^[\x21-\x7e]+$/;
402
+ var IPV4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
403
+ var DIGITS = /^\d+$/;
404
+ function requirePrintable(value, label, issues, maxLength) {
405
+ if (typeof value !== "string" || !PRINTABLE_ASCII.test(value)) {
406
+ issues.push(
407
+ `${label} must be a non-empty printable-ASCII string without spaces/control characters`
408
+ );
409
+ return;
410
+ }
411
+ if (value.length > maxLength) {
412
+ issues.push(`${label} must be at most ${maxLength} characters`);
413
+ }
414
+ }
415
+ function isLoopbackHostname(hostname) {
416
+ let lower = hostname.toLowerCase();
417
+ if (lower.startsWith("[") && lower.endsWith("]")) {
418
+ lower = lower.slice(1, -1);
419
+ }
420
+ if (lower === "::1") {
421
+ return true;
422
+ }
423
+ const match = IPV4.exec(lower);
424
+ if (match === null) {
425
+ return false;
426
+ }
427
+ if (Number(match[1]) !== 127) {
428
+ return false;
429
+ }
430
+ return [match[2], match[3], match[4]].every((part) => Number(part) <= 255);
431
+ }
432
+ function validateHubUrl(raw, label, issues) {
433
+ if (typeof raw !== "string" || raw.trim() === "") {
434
+ issues.push(`${label}: url must be a non-empty string`);
435
+ return null;
436
+ }
437
+ if (raw.length > 2048) {
438
+ issues.push(`${label}: url must be at most 2048 characters`);
439
+ return null;
440
+ }
441
+ let parsed;
442
+ try {
443
+ parsed = new URL(raw);
444
+ } catch {
445
+ issues.push(`${label}: not a valid URL`);
446
+ return null;
447
+ }
448
+ if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
449
+ issues.push(`${label}: scheme must be ws:// or wss://`);
450
+ return null;
451
+ }
452
+ if (parsed.username !== "" || parsed.password !== "") {
453
+ issues.push(
454
+ `${label}: URL must not embed credentials (userinfo); send the token in the Authorization header`
455
+ );
456
+ return null;
457
+ }
458
+ if (parsed.search !== "") {
459
+ issues.push(`${label}: URL must not include a query string`);
460
+ return null;
461
+ }
462
+ if (parsed.hash !== "") {
463
+ issues.push(`${label}: URL must not include a fragment`);
464
+ return null;
465
+ }
466
+ if (parsed.hostname === "") {
467
+ issues.push(`${label}: URL must include a host`);
468
+ return null;
469
+ }
470
+ if (parsed.pathname === PREVIOUS_CONNECT_PATH) parsed.pathname = CONNECT_PATH;
471
+ if (parsed.pathname !== CONNECT_PATH) {
472
+ issues.push(`${label}: path must be exactly ${CONNECT_PATH}`);
473
+ return null;
474
+ }
475
+ if (parsed.protocol === "ws:" && !isLoopbackHostname(parsed.hostname)) {
476
+ issues.push(
477
+ `${label}: plaintext ws:// is restricted to literal loopback addresses; use wss:// otherwise`
478
+ );
479
+ return null;
480
+ }
481
+ return parsed;
482
+ }
483
+ var SHARING_KEY_SET = new Set(SHARING_KEYS);
484
+ function parseSharing(raw, label, issues) {
485
+ if (raw === void 0) return void 0;
486
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
487
+ issues.push(`${label}.sharing must be an object`);
488
+ return void 0;
489
+ }
490
+ const record = raw;
491
+ let valid = true;
492
+ for (const key of Object.keys(record)) {
493
+ if (!SHARING_KEY_SET.has(key)) {
494
+ issues.push(`${label}.sharing has unknown field "${key}"`);
495
+ valid = false;
496
+ continue;
497
+ }
498
+ if (typeof record[key] !== "boolean") {
499
+ issues.push(`${label}.sharing.${key} must be a boolean`);
500
+ valid = false;
501
+ }
502
+ }
503
+ if (typeof record["cpu"] !== "boolean" || typeof record["memory"] !== "boolean") {
504
+ issues.push(`${label}.sharing must contain boolean cpu and memory fields`);
505
+ valid = false;
506
+ }
507
+ if (!valid) return void 0;
508
+ const out = {};
509
+ for (const key of SHARING_KEYS) {
510
+ const value = record[key];
511
+ if (typeof value === "boolean") out[key] = value;
512
+ }
513
+ return out;
514
+ }
515
+ function parseDestinations(env, issues, destinations) {
516
+ const raw = env["COLLECTOR_HUBS"]?.trim();
517
+ if (raw === void 0 || raw === "") {
518
+ issues.push(
519
+ "COLLECTOR_HUBS is required: JSON array of {url, token} objects (1..8 destinations)"
520
+ );
521
+ return;
522
+ }
523
+ let value;
524
+ try {
525
+ value = JSON.parse(raw);
526
+ } catch {
527
+ issues.push("COLLECTOR_HUBS is not valid JSON");
528
+ return;
529
+ }
530
+ if (!Array.isArray(value)) {
531
+ issues.push("COLLECTOR_HUBS must be a JSON array");
532
+ return;
533
+ }
534
+ if (value.length === 0) {
535
+ issues.push("COLLECTOR_HUBS must contain at least one destination");
536
+ return;
537
+ }
538
+ if (value.length > MAX_DESTINATIONS) {
539
+ issues.push(
540
+ `COLLECTOR_HUBS allows at most ${MAX_DESTINATIONS} destinations (got ${value.length})`
541
+ );
542
+ }
543
+ const seenUrls = /* @__PURE__ */ new Set();
544
+ const limit = Math.min(value.length, MAX_DESTINATIONS);
545
+ for (let index = 0; index < limit; index += 1) {
546
+ const label = `COLLECTOR_HUBS[${index}]`;
547
+ const entry = value[index];
548
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
549
+ issues.push(`${label} must be an object`);
550
+ continue;
551
+ }
552
+ const keys = Object.keys(entry).sort();
553
+ if (!keys.includes("url") || !keys.includes("token") || keys.some((k) => !["url", "token", "sharing", "intervalMs"].includes(k))) {
554
+ issues.push(`${label} must have exactly the fields {url, token}, plus optional sharing and intervalMs`);
555
+ continue;
556
+ }
557
+ const candidate = entry;
558
+ const sharing = parseSharing(candidate.sharing, label, issues);
559
+ if (candidate.intervalMs !== void 0 && (!Number.isSafeInteger(candidate.intervalMs) || Number(candidate.intervalMs) < MIN_INTERVAL_MS || Number(candidate.intervalMs) > MAX_INTERVAL_MS)) {
560
+ issues.push(`${label}.intervalMs is outside cadence bounds`);
561
+ }
562
+ const token = candidate.token;
563
+ if (typeof token !== "string" || token === "") {
564
+ issues.push(`${label}.token must be a non-empty string`);
565
+ } else {
566
+ requirePrintable(token, `${label}.token`, issues, MAX_TOKEN_LENGTH);
567
+ }
568
+ const url = validateHubUrl(candidate.url, `${label}.url`, issues);
569
+ if (url === null) {
570
+ continue;
571
+ }
572
+ if (seenUrls.has(url.href)) {
573
+ issues.push(`${label}: duplicate destination URL`);
574
+ continue;
575
+ }
576
+ seenUrls.add(url.href);
577
+ if (typeof token === "string" && token !== "") {
578
+ destinations.push({
579
+ url: url.href,
580
+ token,
581
+ ...sharing === void 0 ? {} : { sharing },
582
+ ...candidate.intervalMs === void 0 ? {} : { intervalMs: Number(candidate.intervalMs) }
583
+ });
584
+ }
585
+ }
586
+ }
587
+ function parseIntervalMs(env, issues) {
588
+ const raw = env["COLLECTOR_INTERVAL_MS"]?.trim();
589
+ if (raw === void 0 || raw === "") {
590
+ return DEFAULT_INTERVAL_MS;
591
+ }
592
+ const parsed = DIGITS.test(raw) ? Number(raw) : Number.NaN;
593
+ if (!Number.isSafeInteger(parsed) || parsed < MIN_INTERVAL_MS || parsed > MAX_INTERVAL_MS) {
594
+ issues.push(
595
+ `COLLECTOR_INTERVAL_MS must be an integer between ${MIN_INTERVAL_MS} and ${MAX_INTERVAL_MS}`
596
+ );
597
+ return DEFAULT_INTERVAL_MS;
598
+ }
599
+ return parsed;
600
+ }
601
+ function parseCollectorConfig(env) {
602
+ const issues = [];
603
+ const rawCollectorId = env["COLLECTOR_ID"]?.trim() ?? "";
604
+ let collectorId = DEFAULT_COLLECTOR_ID;
605
+ if (rawCollectorId !== "") {
606
+ requirePrintable(rawCollectorId, "COLLECTOR_ID", issues, MAX_ID_LENGTH);
607
+ collectorId = rawCollectorId;
608
+ }
609
+ const rawHostname = env["COLLECTOR_HOSTNAME"]?.trim() ?? "";
610
+ let hostname = MOCK_HOSTNAME;
611
+ if (rawHostname !== "") {
612
+ requirePrintable(rawHostname, "COLLECTOR_HOSTNAME", issues, MAX_HOSTNAME_LENGTH);
613
+ hostname = rawHostname;
614
+ }
615
+ const destinations = [];
616
+ const intervalMs = parseIntervalMs(env, issues);
617
+ parseDestinations(env, issues, destinations);
618
+ if (issues.length > 0) {
619
+ throw new ConfigError(issues);
620
+ }
621
+ return {
622
+ collectorId,
623
+ hostname,
624
+ hostnameIsMockDefault: rawHostname === "",
625
+ intervalMs,
626
+ destinations
627
+ };
628
+ }
629
+
630
+ // collector/local-state.ts
631
+ var import_node_crypto3 = require("node:crypto");
632
+ var import_node_fs2 = require("node:fs");
633
+ var import_node_path2 = require("node:path");
634
+ function readState(path) {
635
+ const stat = (0, import_node_fs2.lstatSync)(path);
636
+ if (!stat.isFile() || stat.size > 64 * 1024 || (stat.mode & 63) !== 0) throw new Error("collector state must be a private regular file");
637
+ const raw = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
638
+ if (raw.version !== 1 || typeof raw.collectorId !== "string" || !/^[\x21-\x7e]{1,128}$/.test(raw.collectorId) || !Array.isArray(raw.destinations)) throw new Error("invalid collector state");
639
+ const destinations = raw.destinations.length === 0 ? [] : [...parseCollectorConfig({ COLLECTOR_HUBS: JSON.stringify(raw.destinations) }).destinations];
640
+ if (raw.pendingEnrollments !== void 0 && (!Array.isArray(raw.pendingEnrollments) || raw.pendingEnrollments.length > 8 || raw.pendingEnrollments.some((p) => !p || typeof p.url !== "string" || typeof p.requestId !== "string" || !/^[\x21-\x7e]{1,128}$/.test(p.requestId) || typeof p.recoverySecret !== "string" || !/^[A-Za-z0-9_-]{32,128}$/.test(p.recoverySecret)))) throw new Error("invalid pending enrollment recovery state");
641
+ const pendingEnrollments = raw.pendingEnrollments?.map((pending) => {
642
+ const issues = [];
643
+ const url = validateHubUrl(pending.url, "pending enrollment", issues);
644
+ if (!url || issues.length) throw new ConfigError(issues);
645
+ return { ...pending, url: url.href };
646
+ });
647
+ return {
648
+ version: 1,
649
+ collectorId: raw.collectorId,
650
+ destinations,
651
+ ...pendingEnrollments === void 0 ? {} : { pendingEnrollments }
652
+ };
653
+ }
654
+ function updateInstallation(path, change = (s) => s) {
655
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true, mode: 448 });
656
+ const directoryStat = (0, import_node_fs2.lstatSync)((0, import_node_path2.dirname)(path));
657
+ if (!directoryStat.isDirectory() || (directoryStat.mode & 63) !== 0) {
658
+ throw new ConfigError(["collector state requires a private directory (0700)"]);
659
+ }
660
+ const lock = `${path}.lock`;
661
+ let releaseLock;
662
+ try {
663
+ releaseLock = acquireProcessLease(lock);
664
+ } catch (error) {
665
+ if (error instanceof ProcessLeaseError) {
666
+ if (error.code === "busy") {
667
+ throw new ConfigError(["collector state is locked by another process; stop it before retrying"]);
668
+ }
669
+ throw new ConfigError(["collector state is locked by an unsafe or incomplete lock file; inspect it before recovery"]);
670
+ }
671
+ throw new ConfigError(["collector state lock could not be acquired; check the state directory permissions"]);
672
+ }
673
+ let temporary;
674
+ try {
675
+ let state;
676
+ try {
677
+ (0, import_node_fs2.lstatSync)(path);
678
+ state = readState(path);
679
+ } catch (error) {
680
+ if (error.code !== "ENOENT") throw error;
681
+ state = { version: 1, collectorId: (0, import_node_crypto3.randomUUID)(), destinations: [] };
682
+ }
683
+ const next = change(state);
684
+ temporary = `${path}.${(0, import_node_crypto3.randomUUID)()}.tmp`;
685
+ const out = (0, import_node_fs2.openSync)(temporary, "wx", 384);
686
+ try {
687
+ (0, import_node_fs2.writeFileSync)(out, JSON.stringify(next) + "\n");
688
+ (0, import_node_fs2.fsyncSync)(out);
689
+ } finally {
690
+ (0, import_node_fs2.closeSync)(out);
691
+ }
692
+ (0, import_node_fs2.renameSync)(temporary, path);
693
+ temporary = void 0;
694
+ const directory = (0, import_node_fs2.openSync)((0, import_node_path2.dirname)(path), "r");
695
+ try {
696
+ (0, import_node_fs2.fsyncSync)(directory);
697
+ } finally {
698
+ (0, import_node_fs2.closeSync)(directory);
699
+ }
700
+ return next;
701
+ } finally {
702
+ try {
703
+ if (temporary) (0, import_node_fs2.unlinkSync)(temporary);
704
+ } finally {
705
+ releaseLock();
706
+ }
707
+ }
708
+ }
709
+
710
+ // collector/enroll.ts
711
+ var import_node_crypto4 = require("node:crypto");
712
+ async function enrollCollector(path, connectUrl, enrollmentToken, sharing, replace = false) {
713
+ const issues = [];
714
+ const url = validateHubUrl(connectUrl, "enrollment destination", issues);
715
+ if (!url || issues.length) throw new Error("invalid enrollment destination");
716
+ if (!/^[\x21-\x7e]{1,256}$/.test(enrollmentToken)) throw new Error("invalid enrollment token");
717
+ const state = updateInstallation(path);
718
+ if (!replace && state.destinations.some((d) => d.url === url.href) || !state.destinations.some((d) => d.url === url.href) && state.destinations.length >= 8) throw new Error("destination already enrolled or destination budget exhausted");
719
+ const prepared = updateInstallation(path, (current) => {
720
+ if (current.pendingEnrollments?.some((p) => p.url === url.href)) return current;
721
+ return { ...current, pendingEnrollments: [
722
+ ...current.pendingEnrollments ?? [],
723
+ { url: url.href, requestId: (0, import_node_crypto4.randomUUID)(), recoverySecret: (0, import_node_crypto4.randomBytes)(32).toString("base64url") }
724
+ ] };
725
+ });
726
+ const proof = prepared.pendingEnrollments.find((p) => p.url === url.href);
727
+ const endpoint = new URL(url.href);
728
+ endpoint.protocol = url.protocol === "wss:" ? "https:" : "http:";
729
+ endpoint.pathname = "/api/collector/enroll";
730
+ let response;
731
+ try {
732
+ response = await fetch(endpoint, {
733
+ method: "POST",
734
+ redirect: "error",
735
+ signal: AbortSignal.timeout(5e3),
736
+ headers: { "Authorization": `Bearer ${enrollmentToken}`, "Content-Type": "application/json" },
737
+ body: JSON.stringify({ collectorId: state.collectorId, requestId: proof.requestId, recoverySecret: proof.recoverySecret })
738
+ });
739
+ } catch {
740
+ throw new ConfigError(["enrollment request failed; check destination and TLS trust"]);
741
+ }
742
+ if (response.status !== 201) {
743
+ await response.body?.cancel();
744
+ throw new ConfigError([`enrollment rejected (${response.status}); check the grant and hub enrollment state`]);
745
+ }
746
+ const reader = response.body?.getReader();
747
+ if (!reader) throw new Error("missing enrollment response");
748
+ let text = "";
749
+ let bytes = 0;
750
+ try {
751
+ for (; ; ) {
752
+ const { value, done } = await reader.read();
753
+ if (done) break;
754
+ bytes += value.byteLength;
755
+ if (bytes > 4096) throw new Error("oversized enrollment response");
756
+ text += new TextDecoder().decode(value, { stream: true });
757
+ }
758
+ } finally {
759
+ await reader.cancel();
760
+ }
761
+ const result = JSON.parse(text);
762
+ if (result.collectorId !== state.collectorId || typeof result.machineId !== "string" || !result.machineId || typeof result.token !== "string") throw new Error("invalid enrollment response");
763
+ const destination = parseCollectorConfig({ COLLECTOR_HUBS: JSON.stringify([{ url: url.href, token: result.token, sharing }]) }).destinations[0];
764
+ try {
765
+ updateInstallation(path, (current) => {
766
+ if (current.collectorId !== state.collectorId || !replace && current.destinations.some((d) => d.url === url.href) || !current.destinations.some((d) => d.url === url.href) && current.destinations.length >= 8) throw new Error("collector state changed during enrollment");
767
+ return {
768
+ ...current,
769
+ destinations: [...current.destinations.filter((d) => d.url !== url.href), destination],
770
+ pendingEnrollments: (current.pendingEnrollments ?? []).filter((p) => p.url !== url.href)
771
+ };
772
+ });
773
+ } catch {
774
+ throw new ConfigError(["hub enrollment succeeded but local credential save failed; operator recovery is required"]);
775
+ }
776
+ }
777
+
778
+ // src/cli/system.ts
779
+ var SYSTEM_STATE_FORMAT_VERSION = 2;
780
+ var SYSTEM_SCOPE = "system";
781
+ var DEFAULT_SYSTEM_ACCOUNTS = {
782
+ hub: "rigscope-hub",
783
+ collector: "rigscope-collector",
784
+ powerGroup: "rigscope-rapl"
785
+ };
786
+ var DEFAULT_NODE_PATH = "/opt/rigscope/node/node";
787
+ var UNIT_NAMES = {
788
+ hub: "rigscope-hub.service",
789
+ collector: "rigscope-collector.service"
790
+ };
791
+ var RAPL_RULE_NAME = "90-rigscope-rapl.rules";
792
+ var SYSUSERS_NAME = "rigscope.conf";
793
+ var POWER_SYSUSERS_NAME = "rigscope-rapl.conf";
794
+ var STATE_FILE_NAME = "system-install.json";
795
+ var POWER_STATE_FILE_NAME = "power-access.json";
796
+ var COMPONENTS = ["hub", "collector"];
797
+ function componentsForRole(role) {
798
+ return { hub: role !== "collector", collector: role !== "hub" };
799
+ }
800
+ function componentSet(value) {
801
+ if (value === void 0) return { hub: false, collector: false };
802
+ if (typeof value === "string") return { hub: value === "hub", collector: value === "collector" };
803
+ if (Array.isArray(value)) return { hub: value.includes("hub"), collector: value.includes("collector") };
804
+ return { hub: value.hub === true, collector: value.collector === true };
805
+ }
806
+ function unitFor(component) {
807
+ return component === "hub" ? UNIT_NAMES.hub : UNIT_NAMES.collector;
808
+ }
809
+ function componentEnabled(state, component) {
810
+ return state.components[component].enabled;
811
+ }
812
+ function enabledComponents(state) {
813
+ return COMPONENTS.filter((component) => componentEnabled(state, component));
814
+ }
815
+ var SYSTEM_DEFAULT_SHARING = {
816
+ cpu: true,
817
+ memory: true,
818
+ swap: true,
819
+ gpus: true,
820
+ disks: true,
821
+ network: true,
822
+ networkAddresses: false,
823
+ processes: true,
824
+ processArguments: false
825
+ };
826
+ var SystemError = class extends Error {
827
+ code;
828
+ constructor(code, message) {
829
+ super(message);
830
+ this.name = "SystemError";
831
+ this.code = code;
832
+ }
833
+ };
834
+ var ACCOUNT_PATTERN = /^[a-z_][a-z0-9_-]{0,31}$/;
835
+ var VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
836
+ var PACKAGE_TOP_LEVEL = /* @__PURE__ */ new Set([
837
+ "bin",
838
+ "dist",
839
+ ".next",
840
+ "public",
841
+ "packaging",
842
+ "docs",
843
+ "manifest.json",
844
+ "package.json",
845
+ "package-lock.json",
846
+ "next.config.js",
847
+ "next-env.d.ts",
848
+ "tsconfig.json",
849
+ "README.md",
850
+ "LICENSE",
851
+ "LICENSE.md",
852
+ "SECURITY.md",
853
+ "THIRD_PARTY_NOTICES.md"
854
+ ]);
855
+ var REQUIRED_ARTIFACTS = [
856
+ "dist/hub.cjs",
857
+ "dist/collector.cjs",
858
+ "dist/ops.cjs",
859
+ "dist/cli.cjs",
860
+ ".next/BUILD_ID",
861
+ "package.json"
862
+ ];
863
+ function createSystemHost(overrides = {}) {
864
+ return {
865
+ uid: () => process.getuid?.() ?? -1,
866
+ now: () => Date.now(),
867
+ run: (command, args, options = {}) => {
868
+ const result = (0, import_node_child_process.spawnSync)(command, args, {
869
+ cwd: options.cwd,
870
+ env: options.env,
871
+ encoding: "utf8",
872
+ timeout: options.timeoutMs ?? 12e4,
873
+ maxBuffer: 16 * 1024 * 1024
874
+ });
875
+ return {
876
+ status: result.status,
877
+ stdout: result.stdout ?? "",
878
+ stderr: result.stderr ?? "",
879
+ error: result.error?.message
880
+ };
881
+ },
882
+ ...overrides
883
+ };
884
+ }
885
+ function defaultSystemPaths(overrides = {}) {
886
+ return {
887
+ prefix: (0, import_node_path3.resolve)(overrides.prefix ?? "/opt/rigscope"),
888
+ configDir: (0, import_node_path3.resolve)(overrides.configDir ?? "/etc/rigscope"),
889
+ stateDir: (0, import_node_path3.resolve)(overrides.stateDir ?? "/var/lib/rigscope"),
890
+ unitsDir: (0, import_node_path3.resolve)(overrides.unitsDir ?? "/etc/systemd/system"),
891
+ sysusersDir: (0, import_node_path3.resolve)(overrides.sysusersDir ?? "/etc/sysusers.d"),
892
+ udevRulesDir: (0, import_node_path3.resolve)(overrides.udevRulesDir ?? "/etc/udev/rules.d"),
893
+ powercapRoot: (0, import_node_path3.resolve)(overrides.powercapRoot ?? "/sys/class/powercap")
894
+ };
895
+ }
896
+ function unitNamesFor(role) {
897
+ if (role === "hub") return [UNIT_NAMES.hub];
898
+ if (role === "collector") return [UNIT_NAMES.collector];
899
+ return [UNIT_NAMES.hub, UNIT_NAMES.collector];
900
+ }
901
+ function unitNamesForComponents(components) {
902
+ return COMPONENTS.filter((component) => componentSet(components)[component]).map(unitFor);
903
+ }
904
+ function rolesFor(role) {
905
+ return { hub: role !== "collector", collector: role !== "hub" };
906
+ }
907
+ function isWithin(parent, candidate) {
908
+ const rel = (0, import_node_path3.relative)((0, import_node_path3.resolve)(parent), (0, import_node_path3.resolve)(candidate));
909
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${import_node_path3.sep}`) && !(0, import_node_path3.isAbsolute)(rel);
910
+ }
911
+ function ensureAbsolute(path, label) {
912
+ const absolute = (0, import_node_path3.resolve)(path);
913
+ if (/[\0\r\n]/.test(absolute)) throw new SystemError("unsafe-path", `${label} contains control characters`);
914
+ if (absolute === "/") throw new SystemError("unsafe-path", `${label} must not be the filesystem root`);
915
+ for (const forbidden of ["/home", "/root", "/proc", "/sys", "/dev", "/boot"]) {
916
+ if (absolute === forbidden || absolute.startsWith(`${forbidden}/`)) {
917
+ throw new SystemError("unsafe-path", `${label} must not live under ${forbidden}: ${absolute}`);
918
+ }
919
+ }
920
+ return absolute;
921
+ }
922
+ function validateSystemPaths(input = {}) {
923
+ const paths = defaultSystemPaths(input);
924
+ for (const [label, value] of Object.entries(paths)) {
925
+ if (label === "powercapRoot") {
926
+ if (!(0, import_node_path3.isAbsolute)(value) || /[\0\r\n]/.test(value) || (0, import_node_path3.resolve)(value) === "/") {
927
+ throw new SystemError("unsafe-path", `powercap root is unsafe: ${value}`);
928
+ }
929
+ } else {
930
+ ensureAbsolute(value, label);
931
+ }
932
+ }
933
+ const fixed = [
934
+ ["prefix", paths.prefix],
935
+ ["configDir", paths.configDir],
936
+ ["stateDir", paths.stateDir],
937
+ ["unitsDir", paths.unitsDir],
938
+ ["sysusersDir", paths.sysusersDir],
939
+ ["udevRulesDir", paths.udevRulesDir]
940
+ ];
941
+ for (let i = 0; i < fixed.length; i += 1) {
942
+ for (let j = i + 1; j < fixed.length; j += 1) {
943
+ const [leftName, left] = fixed[i];
944
+ const [rightName, right] = fixed[j];
945
+ if (left === right || isWithin(left, right) || isWithin(right, left)) {
946
+ throw new SystemError("unsafe-path", `${leftName} and ${rightName} must be separate paths`);
947
+ }
948
+ }
949
+ }
950
+ return paths;
951
+ }
952
+ function validateAccounts(input = {}) {
953
+ const accounts = { ...DEFAULT_SYSTEM_ACCOUNTS, ...input };
954
+ for (const [label, value] of Object.entries(accounts)) {
955
+ if (!ACCOUNT_PATTERN.test(value)) throw new SystemError("invalid-account", `${label} account name is invalid: ${value}`);
956
+ }
957
+ if (new Set(Object.values(accounts)).size !== Object.values(accounts).length) {
958
+ throw new SystemError("invalid-account", "hub, collector and power group names must differ");
959
+ }
960
+ if (Object.values(accounts).includes("root")) {
961
+ throw new SystemError("invalid-account", "system services must not run as root");
962
+ }
963
+ return accounts;
964
+ }
965
+ function assertApply(host) {
966
+ if (host.uid() !== 0) {
967
+ throw new SystemError("root-required", "This mutation requires root. Re-run the same command with sudo; no change was made.");
968
+ }
969
+ }
970
+ function runRequired(host, command, args, label, options) {
971
+ const result = host.run(command, args, options);
972
+ if (result.status !== 0) {
973
+ const detail = (result.stderr || result.stdout || result.error || "").trim().split("\n").slice(-3).join(" ");
974
+ throw new SystemError("command-failed", `${label} failed${detail ? `: ${detail}` : ""}`);
975
+ }
976
+ return result;
977
+ }
978
+ function runMaybe(host, command, args, options) {
979
+ return host.run(command, args, options);
980
+ }
981
+ function sha256File(path) {
982
+ return (0, import_node_crypto5.createHash)("sha256").update((0, import_node_fs3.readFileSync)(path)).digest("hex");
983
+ }
984
+ function atomicWrite(path, content, mode) {
985
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(path), { recursive: true, mode: 493 });
986
+ const temporary = (0, import_node_path3.join)((0, import_node_path3.dirname)(path), `.${(0, import_node_path3.basename)(path)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
987
+ try {
988
+ (0, import_node_fs3.writeFileSync)(temporary, content, { mode });
989
+ (0, import_node_fs3.chmodSync)(temporary, mode);
990
+ (0, import_node_fs3.renameSync)(temporary, path);
991
+ } catch (error) {
992
+ try {
993
+ (0, import_node_fs3.unlinkSync)(temporary);
994
+ } catch {
995
+ }
996
+ throw error;
997
+ }
998
+ }
999
+ function ensureDirectory(path, mode, privateDirectory = false) {
1000
+ let stat;
1001
+ try {
1002
+ stat = (0, import_node_fs3.lstatSync)(path);
1003
+ } catch (error) {
1004
+ if (error.code !== "ENOENT") throw error;
1005
+ (0, import_node_fs3.mkdirSync)(path, { recursive: true, mode });
1006
+ (0, import_node_fs3.chmodSync)(path, mode);
1007
+ stat = (0, import_node_fs3.lstatSync)(path);
1008
+ }
1009
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
1010
+ throw new SystemError("unsafe-path", `${path} must be a real directory`);
1011
+ }
1012
+ const actual = stat.mode & 511;
1013
+ if (privateDirectory && (actual & 63) !== 0) {
1014
+ throw new SystemError("insecure-permissions", `${path} must be private (0700 or stricter); found 0${actual.toString(8)}`);
1015
+ }
1016
+ }
1017
+ function isDefaultSystemLayout(paths) {
1018
+ return paths.prefix === "/opt/rigscope" && paths.configDir === "/etc/rigscope" && paths.stateDir === "/var/lib/rigscope";
1019
+ }
1020
+ function assertManagedPathSafety(paths, enforceRoot) {
1021
+ for (const path of [paths.prefix, paths.configDir, paths.stateDir, paths.unitsDir, paths.sysusersDir, paths.udevRulesDir]) {
1022
+ if (!(0, import_node_fs3.existsSync)(path)) continue;
1023
+ const stat = (0, import_node_fs3.lstatSync)(path);
1024
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
1025
+ throw new SystemError("unsafe-path", `${path} must be a real directory`);
1026
+ }
1027
+ if ((stat.mode & 18) !== 0) {
1028
+ throw new SystemError("insecure-permissions", `${path} must not be group/world-writable; found 0${(stat.mode & 511).toString(8)}`);
1029
+ }
1030
+ if (enforceRoot && stat.uid !== 0) {
1031
+ throw new SystemError("insecure-ownership", `${path} must be root-owned for a default system installation`);
1032
+ }
1033
+ }
1034
+ }
1035
+ function writeManagedFile(path, content, mode, replace) {
1036
+ if ((0, import_node_fs3.existsSync)(path)) {
1037
+ const stat = (0, import_node_fs3.lstatSync)(path);
1038
+ if (!stat.isFile() || stat.isSymbolicLink()) {
1039
+ throw new SystemError("unsafe-path", `${path} must be a regular file, not a symlink`);
1040
+ }
1041
+ if (!replace) {
1042
+ if ((stat.mode & 63) !== 0) {
1043
+ throw new SystemError("insecure-permissions", `${path} must be private (0600); found 0${(stat.mode & 511).toString(8)}`);
1044
+ }
1045
+ return { created: false };
1046
+ }
1047
+ }
1048
+ atomicWrite(path, content, mode);
1049
+ return { created: true };
1050
+ }
1051
+ function systemdQuote(value) {
1052
+ if (/[\r\n\0]/.test(value)) throw new SystemError("unsafe-value", "systemd values must not contain control characters");
1053
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%").replace(/\$/g, "$$")}"`;
1054
+ }
1055
+ function envLine(key, value) {
1056
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key)) throw new SystemError("invalid-env", `invalid environment key: ${key}`);
1057
+ if (/[\r\n\0]/.test(value)) throw new SystemError("invalid-env", `environment value for ${key} contains control characters`);
1058
+ return `${key}=${JSON.stringify(value)}`;
1059
+ }
1060
+ function parseEnvValue(value) {
1061
+ if (value.startsWith('"')) {
1062
+ try {
1063
+ return JSON.parse(value);
1064
+ } catch {
1065
+ return value.slice(1, -1);
1066
+ }
1067
+ }
1068
+ if (value.startsWith("'") && value.endsWith("'")) {
1069
+ return value.slice(1, -1).replace(/\\'/g, "'").replace(/\\\\/g, "\\");
1070
+ }
1071
+ return value;
1072
+ }
1073
+ function readEnvDocument(path) {
1074
+ let raw;
1075
+ try {
1076
+ raw = (0, import_node_fs3.readFileSync)(path, "utf8");
1077
+ } catch (error) {
1078
+ if (error.code === "ENOENT") return null;
1079
+ throw error;
1080
+ }
1081
+ const lines = raw.split("\n");
1082
+ const values = {};
1083
+ for (const line of lines) {
1084
+ const match = /^([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
1085
+ if (match) values[match[1]] = parseEnvValue(match[2]);
1086
+ }
1087
+ return { lines, values };
1088
+ }
1089
+ function writeEnvValue(path, key, value) {
1090
+ const document = readEnvDocument(path);
1091
+ const lines = (document?.lines ?? ["# Rigscope private environment. Mode 0600; never printed."]).filter(
1092
+ (line) => !line.startsWith(`${key}=`)
1093
+ );
1094
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
1095
+ const comment = lines.findIndex((line) => line.startsWith(`# ${key}=`) || line.startsWith(`#${key}=`));
1096
+ if (comment >= 0) lines.splice(comment + 1, 0, envLine(key, value));
1097
+ else lines.push(envLine(key, value));
1098
+ atomicWrite(path, `${lines.join("\n")}
1099
+ `, 384);
1100
+ }
1101
+ function connectDestinationUrl(value) {
1102
+ let url;
1103
+ try {
1104
+ url = new URL(value);
1105
+ } catch {
1106
+ throw new SystemError("invalid-url", "Use a hub URL such as https://hub.example.com.");
1107
+ }
1108
+ if (url.protocol === "https:") url.protocol = "wss:";
1109
+ else if (url.protocol === "http:") url.protocol = "ws:";
1110
+ else if (url.protocol !== "ws:" && url.protocol !== "wss:") {
1111
+ throw new SystemError("invalid-url", "Use an http(s) or ws(s) hub URL.");
1112
+ }
1113
+ if (url.username !== "" || url.password !== "") {
1114
+ throw new SystemError("invalid-url", "Hub URLs must not contain credentials; use --token-file.");
1115
+ }
1116
+ if (url.search !== "" || url.hash !== "") throw new SystemError("invalid-url", "Hub URLs must not contain a query or fragment.");
1117
+ if (url.hostname === "localhost") url.hostname = "127.0.0.1";
1118
+ if (url.pathname === "/") url.pathname = "/api/collector/connect";
1119
+ return url.href;
1120
+ }
1121
+ function hubDatabasePath(paths) {
1122
+ return (0, import_node_path3.join)(paths.stateDir, "hub", "telemetry.sqlite");
1123
+ }
1124
+ function hubEnrollmentPath(paths) {
1125
+ return (0, import_node_path3.join)(paths.stateDir, "hub", "enrollment.json");
1126
+ }
1127
+ function hubSocketPath(paths) {
1128
+ return (0, import_node_path3.join)(paths.stateDir, "hub", "run", "hub.sock");
1129
+ }
1130
+ function collectorStatePath(paths) {
1131
+ return (0, import_node_path3.join)(paths.stateDir, "collector", "state.json");
1132
+ }
1133
+ function hubEnvPath(paths) {
1134
+ return (0, import_node_path3.join)(paths.configDir, "hub.env");
1135
+ }
1136
+ function collectorEnvPath(paths) {
1137
+ return (0, import_node_path3.join)(paths.configDir, "collector.env");
1138
+ }
1139
+ function sysusersPath(paths) {
1140
+ return (0, import_node_path3.join)(paths.sysusersDir, SYSUSERS_NAME);
1141
+ }
1142
+ function powerSysusersPath(paths) {
1143
+ return (0, import_node_path3.join)(paths.sysusersDir, POWER_SYSUSERS_NAME);
1144
+ }
1145
+ function raplRulePath(paths) {
1146
+ return (0, import_node_path3.join)(paths.udevRulesDir, RAPL_RULE_NAME);
1147
+ }
1148
+ function stateFilePath(paths) {
1149
+ return (0, import_node_path3.join)(paths.stateDir, STATE_FILE_NAME);
1150
+ }
1151
+ function readCollectorIdentity(path) {
1152
+ if (!(0, import_node_fs3.existsSync)(path)) return null;
1153
+ const stat = (0, import_node_fs3.lstatSync)(path);
1154
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new SystemError("unsafe-path", `${path} must be a regular file`);
1155
+ if ((stat.mode & 63) !== 0) throw new SystemError("insecure-permissions", `${path} must be private (0600)`);
1156
+ let raw;
1157
+ try {
1158
+ raw = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
1159
+ } catch {
1160
+ throw new SystemError("invalid-file", `${path} is not valid JSON`);
1161
+ }
1162
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new SystemError("invalid-file", `${path} is invalid`);
1163
+ const record = raw;
1164
+ if (record.version !== void 0 && record.version !== 1 || typeof record.collectorId !== "string" || !/^[\x21-\x7e]{1,128}$/.test(record.collectorId) || !Array.isArray(record.destinations)) {
1165
+ throw new SystemError("invalid-file", `${path} is not a collector installation state`);
1166
+ }
1167
+ const destinations = [];
1168
+ for (const entry of record.destinations) {
1169
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
1170
+ throw new SystemError("invalid-file", `${path} contains an invalid destination`);
1171
+ }
1172
+ const destination = entry;
1173
+ if (typeof destination.url !== "string" || typeof destination.token !== "string" || destination.token === "") {
1174
+ throw new SystemError("invalid-file", `${path} contains an invalid destination`);
1175
+ }
1176
+ destinations.push({
1177
+ url: destination.url,
1178
+ token: destination.token,
1179
+ ...destination.sharing !== null && typeof destination.sharing === "object" && !Array.isArray(destination.sharing) ? { sharing: destination.sharing } : {}
1180
+ });
1181
+ }
1182
+ return { version: 1, collectorId: record.collectorId, destinations };
1183
+ }
1184
+ function writeCollectorIdentity(path, identity) {
1185
+ ensureDirectory((0, import_node_path3.dirname)(path), 448, true);
1186
+ atomicWrite(path, `${JSON.stringify(identity)}
1187
+ `, 384);
1188
+ }
1189
+ function hubOwnerState(paths) {
1190
+ const document = readEnvDocument(hubEnvPath(paths));
1191
+ if (document === null) return { configured: false };
1192
+ const value = document.values.HUB_BROWSER_USERS;
1193
+ if (value === void 0 || value === "") return { configured: false };
1194
+ try {
1195
+ const parsed = JSON.parse(value);
1196
+ if (!Array.isArray(parsed)) return { configured: false, problem: "HUB_BROWSER_USERS must be a JSON array" };
1197
+ return { configured: parsed.length > 0 };
1198
+ } catch {
1199
+ return { configured: false, problem: "HUB_BROWSER_USERS is not valid JSON" };
1200
+ }
1201
+ }
1202
+ function collectorConfigured(paths) {
1203
+ try {
1204
+ const identity = readCollectorIdentity(collectorStatePath(paths));
1205
+ if (identity === null) return { configured: false, destinations: 0 };
1206
+ return { configured: identity.destinations.length > 0, destinations: identity.destinations.length };
1207
+ } catch (error) {
1208
+ return { configured: false, destinations: 0, problem: error instanceof Error ? error.message : String(error) };
1209
+ }
1210
+ }
1211
+ function ensureHubOwner(paths) {
1212
+ const document = readEnvDocument(hubEnvPath(paths));
1213
+ if (document === null) throw new SystemError("missing-config", `hub configuration is missing: ${hubEnvPath(paths)}; run install or enable hub`);
1214
+ const existing = document.values.HUB_BROWSER_USERS;
1215
+ if (existing !== void 0 && existing !== "") {
1216
+ let parsed;
1217
+ try {
1218
+ parsed = JSON.parse(existing);
1219
+ } catch {
1220
+ throw new SystemError("invalid-config", "HUB_BROWSER_USERS is not valid JSON; repair the private hub environment");
1221
+ }
1222
+ if (!Array.isArray(parsed)) throw new SystemError("invalid-config", "HUB_BROWSER_USERS must be a JSON array");
1223
+ if (parsed.length > 0) return void 0;
1224
+ }
1225
+ const password = (0, import_node_crypto5.randomBytes)(18).toString("base64url");
1226
+ const users = [{ username: "owner", passwordHash: hashPassword(password), machineIds: "*", admin: true }];
1227
+ writeEnvValue(hubEnvPath(paths), "HUB_BROWSER_USERS", JSON.stringify(users));
1228
+ return password;
1229
+ }
1230
+ function hubDestinationUrl(paths) {
1231
+ const document = readEnvDocument(hubEnvPath(paths));
1232
+ if (document === null) return null;
1233
+ const origin = document.values.HUB_PUBLIC_ORIGIN;
1234
+ if (origin !== void 0 && origin !== "") {
1235
+ let parsed;
1236
+ try {
1237
+ parsed = new URL(origin);
1238
+ } catch {
1239
+ return null;
1240
+ }
1241
+ if (parsed.protocol !== "https:") return null;
1242
+ parsed.protocol = "wss:";
1243
+ parsed.pathname = "/api/collector/connect";
1244
+ return { url: parsed.href, remote: true };
1245
+ }
1246
+ const host = document.values.HUB_HOST ?? "127.0.0.1";
1247
+ if (host !== "127.0.0.1" && host !== "::1") return null;
1248
+ const port = Number(document.values.HUB_PORT ?? "8081");
1249
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
1250
+ return { url: `ws://127.0.0.1:${port}/api/collector/connect`, remote: false };
1251
+ }
1252
+ function updateHubCollectorRegistry(paths, identity) {
1253
+ const document = readEnvDocument(hubEnvPath(paths));
1254
+ if (document === null) return { added: false };
1255
+ let collectors = [];
1256
+ const raw = document.values.HUB_COLLECTORS;
1257
+ if (raw !== void 0 && raw !== "") {
1258
+ let parsed;
1259
+ try {
1260
+ parsed = JSON.parse(raw);
1261
+ } catch {
1262
+ throw new SystemError("invalid-config", "HUB_COLLECTORS is not valid JSON; repair the private hub environment");
1263
+ }
1264
+ if (!Array.isArray(parsed)) throw new SystemError("invalid-config", "HUB_COLLECTORS must be a JSON array");
1265
+ collectors = parsed;
1266
+ }
1267
+ if (collectors.some((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) && entry.collectorId === identity.collectorId)) {
1268
+ return { added: false };
1269
+ }
1270
+ collectors = [...collectors, { collectorId: identity.collectorId, machineId: identity.machineId, token: identity.token }];
1271
+ writeEnvValue(hubEnvPath(paths), "HUB_COLLECTORS", JSON.stringify(collectors));
1272
+ return { added: true };
1273
+ }
1274
+ function registerCollectorInHubDatabase(paths, identity, host) {
1275
+ const database = hubDatabasePath(paths);
1276
+ if (!(0, import_node_fs3.existsSync)(database)) return { inserted: false };
1277
+ const db = new import_node_sqlite.DatabaseSync(database, { timeout: 5e3 });
1278
+ try {
1279
+ const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='collector_registry'").get();
1280
+ if (!table) return { inserted: false };
1281
+ const existing = db.prepare("SELECT collector_id,machine_id,token FROM collector_registry WHERE collector_id=?").get(identity.collectorId);
1282
+ if (existing) return { inserted: false, existing: { machineId: existing.machine_id, token: existing.token } };
1283
+ db.prepare("INSERT INTO collector_registry(collector_id,machine_id,token,revoked,created_at_ms,updated_at_ms) VALUES(?,?,?,0,?,?)").run(identity.collectorId, identity.machineId, identity.token, host.now(), host.now());
1284
+ return { inserted: true };
1285
+ } finally {
1286
+ db.close();
1287
+ }
1288
+ }
1289
+ function findCollectorInHubDatabase(paths, collectorId) {
1290
+ const database = hubDatabasePath(paths);
1291
+ if (!(0, import_node_fs3.existsSync)(database)) return null;
1292
+ const db = new import_node_sqlite.DatabaseSync(database, { readOnly: true, timeout: 5e3 });
1293
+ try {
1294
+ const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='collector_registry'").get();
1295
+ if (!table) return null;
1296
+ const row = db.prepare("SELECT machine_id,token FROM collector_registry WHERE collector_id=? AND revoked=0").get(collectorId);
1297
+ return row ? { machineId: row.machine_id, token: row.token } : null;
1298
+ } finally {
1299
+ db.close();
1300
+ }
1301
+ }
1302
+ function createLocalEnrollmentGrant(paths, host) {
1303
+ const database = hubDatabasePath(paths);
1304
+ if (!(0, import_node_fs3.existsSync)(database)) throw new SystemError("hub-unavailable", "the hub database does not exist yet");
1305
+ const token = (0, import_node_crypto5.randomBytes)(32).toString("base64url");
1306
+ const tokenHash = (0, import_node_crypto5.createHash)("sha256").update(token).digest("hex");
1307
+ const expiresAt = host.now() + 10 * 60 * 1e3;
1308
+ const db = new import_node_sqlite.DatabaseSync(database, { timeout: 5e3 });
1309
+ try {
1310
+ const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='collector_grants'").get();
1311
+ if (!table) throw new SystemError("hub-unavailable", "the hub registry is not initialized");
1312
+ db.exec("BEGIN IMMEDIATE");
1313
+ try {
1314
+ db.prepare("INSERT OR IGNORE INTO collector_registry_meta VALUES('bootstrapped','1')").run();
1315
+ db.prepare("DELETE FROM collector_grants WHERE expires_at_ms < ? AND (recovery_until_ms IS NULL OR recovery_until_ms < ?)").run(host.now(), host.now());
1316
+ const count = db.prepare("SELECT COUNT(*) AS n FROM collector_grants").get();
1317
+ if (count.n >= 256) throw new SystemError("grant-budget", "the hub enrollment grant budget is exhausted");
1318
+ db.prepare(
1319
+ "INSERT INTO collector_grants(token_hash,expires_at_ms,replace_machine_id,consumed_by,request_id,recovery_hash,recovery_until_ms,actor) VALUES(?,?,NULL,NULL,NULL,NULL,NULL,?)"
1320
+ ).run(tokenHash, expiresAt, "system-installer");
1321
+ db.exec("COMMIT");
1322
+ } catch (error) {
1323
+ try {
1324
+ db.exec("ROLLBACK");
1325
+ } catch {
1326
+ }
1327
+ throw error;
1328
+ }
1329
+ } finally {
1330
+ db.close();
1331
+ }
1332
+ return token;
1333
+ }
1334
+ async function pairCollectorWithHub(options) {
1335
+ const { paths, accounts, host } = options;
1336
+ const existing = readCollectorIdentity(collectorStatePath(paths));
1337
+ if (existing !== null && existing.destinations.length > 0) {
1338
+ return { paired: false, awaiting: false, hubWasActive: false, note: "configured collector keeps its destinations" };
1339
+ }
1340
+ const owner = hubOwnerState(paths);
1341
+ if (!owner.configured) {
1342
+ return {
1343
+ paired: false,
1344
+ awaiting: true,
1345
+ hubWasActive: false,
1346
+ note: owner.problem ?? "the local hub has no owner yet"
1347
+ };
1348
+ }
1349
+ const destination = hubDestinationUrl(paths);
1350
+ if (destination === null) {
1351
+ return {
1352
+ paired: false,
1353
+ awaiting: true,
1354
+ hubWasActive: false,
1355
+ note: "the local hub has no reachable loopback or HTTPS destination"
1356
+ };
1357
+ }
1358
+ if (!(0, import_node_fs3.existsSync)((0, import_node_path3.join)(paths.unitsDir, UNIT_NAMES.hub))) {
1359
+ return { paired: false, awaiting: true, hubWasActive: false, note: "the local hub unit is not installed" };
1360
+ }
1361
+ const hubWasActive = systemdActive(host, UNIT_NAMES.hub);
1362
+ if (hubWasActive) {
1363
+ if (existing !== null) {
1364
+ writeCollectorIdentity(collectorStatePath(paths), existing);
1365
+ const registered = findCollectorInHubDatabase(paths, existing.collectorId);
1366
+ if (registered !== null) {
1367
+ const adopted = {
1368
+ version: 1,
1369
+ collectorId: existing.collectorId,
1370
+ destinations: [{ url: destination.url, token: registered.token, sharing: { ...SYSTEM_DEFAULT_SHARING } }]
1371
+ };
1372
+ writeCollectorIdentity(collectorStatePath(paths), adopted);
1373
+ chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1374
+ return { paired: true, awaiting: false, hubWasActive, destination: destination.url };
1375
+ }
1376
+ }
1377
+ let grant;
1378
+ try {
1379
+ grant = createLocalEnrollmentGrant(paths, host);
1380
+ } catch (error) {
1381
+ chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1382
+ return {
1383
+ paired: false,
1384
+ awaiting: true,
1385
+ hubWasActive,
1386
+ note: `the live hub registry is unavailable: ${error instanceof Error ? error.message : String(error)}`
1387
+ };
1388
+ }
1389
+ try {
1390
+ await enrollCollector(collectorStatePath(paths), destination.url, grant, { ...SYSTEM_DEFAULT_SHARING }, false);
1391
+ } catch (error) {
1392
+ return {
1393
+ paired: false,
1394
+ awaiting: true,
1395
+ hubWasActive,
1396
+ note: `enrollment through the live hub failed: ${error instanceof Error ? error.message : String(error)}`
1397
+ };
1398
+ } finally {
1399
+ chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1400
+ }
1401
+ if (!collectorConfigured(paths).configured) {
1402
+ return { paired: false, awaiting: true, hubWasActive, note: "the hub accepted enrollment but no destination was saved" };
1403
+ }
1404
+ chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1405
+ return { paired: true, awaiting: false, hubWasActive, destination: destination.url };
1406
+ }
1407
+ const identity = {
1408
+ collectorId: existing?.collectorId ?? (0, import_node_crypto5.randomUUID)(),
1409
+ machineId: (0, import_node_crypto5.randomUUID)(),
1410
+ token: (0, import_node_crypto5.randomBytes)(32).toString("base64url")
1411
+ };
1412
+ const registry = registerCollectorInHubDatabase(paths, identity, host);
1413
+ let token = identity.token;
1414
+ if (registry.existing) {
1415
+ token = registry.existing.token;
1416
+ } else if (!registry.inserted) {
1417
+ const added = updateHubCollectorRegistry(paths, identity);
1418
+ if (!added.added) {
1419
+ const document = readEnvDocument(hubEnvPath(paths));
1420
+ const raw = document?.values.HUB_COLLECTORS;
1421
+ if (raw !== void 0) {
1422
+ const parsed = JSON.parse(raw);
1423
+ const match = parsed.find((entry) => entry.collectorId === identity.collectorId);
1424
+ if (match && typeof match.machineId === "string" && typeof match.token === "string") token = match.token;
1425
+ }
1426
+ }
1427
+ }
1428
+ const next = existing ?? { version: 1, collectorId: identity.collectorId, destinations: [] };
1429
+ next.destinations = [
1430
+ {
1431
+ url: destination.url,
1432
+ token,
1433
+ sharing: { ...SYSTEM_DEFAULT_SHARING }
1434
+ }
1435
+ ];
1436
+ writeCollectorIdentity(collectorStatePath(paths), next);
1437
+ chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1438
+ return { paired: true, awaiting: false, hubWasActive, destination: destination.url };
1439
+ }
1440
+ function unitCommon(name, description, paths, account, envPath, execStart, extraService = []) {
1441
+ return [
1442
+ "[Unit]",
1443
+ `Description=${description}`,
1444
+ `Documentation=file:${paths.prefix}/current/README.md`,
1445
+ "After=network-online.target",
1446
+ "Wants=network-online.target",
1447
+ "StartLimitIntervalSec=60",
1448
+ "StartLimitBurst=5",
1449
+ "",
1450
+ "[Service]",
1451
+ "Type=simple",
1452
+ `User=${account}`,
1453
+ `Group=${account}`,
1454
+ `WorkingDirectory=${(0, import_node_path3.join)(paths.prefix, "current").replace(/%/g, "%%")}`,
1455
+ `EnvironmentFile=${envPath.replace(/%/g, "%%")}`,
1456
+ "Environment=NODE_ENV=production",
1457
+ "Restart=on-failure",
1458
+ "RestartSec=2s",
1459
+ "KillSignal=SIGTERM",
1460
+ "UMask=0077",
1461
+ "NoNewPrivileges=yes",
1462
+ "PrivateTmp=yes",
1463
+ "ProtectSystem=strict",
1464
+ "ProtectHome=yes",
1465
+ "ProtectKernelTunables=yes",
1466
+ "ProtectKernelModules=yes",
1467
+ "ProtectKernelLogs=yes",
1468
+ "ProtectControlGroups=yes",
1469
+ "RestrictRealtime=yes",
1470
+ "RestrictSUIDSGID=yes",
1471
+ "RestrictNamespaces=yes",
1472
+ "LockPersonality=yes",
1473
+ "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6",
1474
+ "CapabilityBoundingSet=",
1475
+ "AmbientCapabilities=",
1476
+ `ExecStart=${execStart}`,
1477
+ ...extraService,
1478
+ "StandardOutput=journal",
1479
+ "StandardError=journal",
1480
+ `SyslogIdentifier=${name}`,
1481
+ "",
1482
+ "[Install]",
1483
+ "WantedBy=multi-user.target",
1484
+ ""
1485
+ ];
1486
+ }
1487
+ function renderSystemHubUnit(options) {
1488
+ const { paths, accounts } = options;
1489
+ const hubScript = (0, import_node_path3.join)(paths.prefix, "current", "dist", "hub.cjs");
1490
+ const extra = [
1491
+ `Environment=${systemdQuote(`HUB_DATABASE_PATH=${options.database}`)}`,
1492
+ `Environment=${systemdQuote(`HUB_STATE_FILE=${options.enrollment}`)}`,
1493
+ ...options.remote ? [`Environment=${systemdQuote(`HUB_SOCKET_PATH=${options.socket}`)}`] : [],
1494
+ `ReadWritePaths=${systemdQuote((0, import_node_path3.join)(paths.stateDir, "hub"))}`,
1495
+ `ReadOnlyPaths=${systemdQuote((0, import_node_path3.join)(paths.prefix, "current"))}`,
1496
+ "PrivateDevices=yes",
1497
+ "TimeoutStopSec=15s"
1498
+ ];
1499
+ return [
1500
+ "# Managed by `rigscope system`. Enable explicitly; do not edit in place.",
1501
+ ...unitCommon(
1502
+ "rigscope-hub",
1503
+ "Rigscope hub (system scope)",
1504
+ paths,
1505
+ accounts.hub,
1506
+ hubEnvPath(paths),
1507
+ `${systemdQuote(options.nodePath)} ${systemdQuote(hubScript)}`,
1508
+ extra
1509
+ )
1510
+ ].join("\n");
1511
+ }
1512
+ function renderSystemCollectorUnit(options) {
1513
+ const { paths, accounts } = options;
1514
+ const collectorScript = (0, import_node_path3.join)(paths.prefix, "current", "dist", "collector.cjs");
1515
+ const extra = [
1516
+ ...options.powerAccess ? [`SupplementaryGroups=${accounts.powerGroup}`] : [],
1517
+ `Environment=${systemdQuote(`COLLECTOR_MODE=real`)}`,
1518
+ `Environment=${systemdQuote(`COLLECTOR_STATE_FILE=${options.collectorState}`)}`,
1519
+ `Environment=${systemdQuote(`COLLECTOR_HEALTH_FILE=${(0, import_node_path3.join)(paths.stateDir, "collector", "health.json")}`)}`,
1520
+ `ReadWritePaths=${systemdQuote((0, import_node_path3.join)(paths.stateDir, "collector"))}`,
1521
+ `ReadOnlyPaths=${systemdQuote((0, import_node_path3.join)(paths.prefix, "current"))}`,
1522
+ "TimeoutStopSec=10s"
1523
+ ];
1524
+ return [
1525
+ "# Managed by `rigscope system`. Enable explicitly after enrollment.",
1526
+ "# The collector is nologin and has no capability grants.",
1527
+ "# Package power reads use the private group rule installed separately.",
1528
+ "# Node receives no capabilities, no root, and no device-control access.",
1529
+ ...unitCommon(
1530
+ "rigscope-collector",
1531
+ "Rigscope collector (system scope)",
1532
+ paths,
1533
+ accounts.collector,
1534
+ collectorEnvPath(paths),
1535
+ `${systemdQuote(options.nodePath)} ${systemdQuote(collectorScript)} --await-connection`,
1536
+ extra
1537
+ )
1538
+ ].join("\n");
1539
+ }
1540
+ function renderSysusers(paths, accounts, _role = "local") {
1541
+ void paths;
1542
+ return [
1543
+ `# Managed by Rigscope system installation.`,
1544
+ "# Accounts are unprivileged and cannot log in.",
1545
+ `g ${accounts.hub} - -`,
1546
+ `u ${accounts.hub} - "Rigscope hub" /nonexistent /usr/sbin/nologin`,
1547
+ `g ${accounts.collector} - -`,
1548
+ `u ${accounts.collector} - "Rigscope collector" /nonexistent /usr/sbin/nologin`,
1549
+ ""
1550
+ ].join("\n");
1551
+ }
1552
+ function renderPowerSysusers(_paths, accounts) {
1553
+ return [
1554
+ "# Managed by Rigscope. Grants only the collector account membership in the power group.",
1555
+ `g ${accounts.powerGroup} - -`,
1556
+ `m ${accounts.collector} ${accounts.powerGroup}`,
1557
+ ""
1558
+ ].join("\n");
1559
+ }
1560
+ function renderRaplRule(accounts) {
1561
+ return [
1562
+ "# Managed by Rigscope: read-only package energy/range counters for the collector group.",
1563
+ "# Top-level package devices only; subdomains and constraint files are not touched.",
1564
+ 'SUBSYSTEM=="powercap", ACTION=="add|change", KERNEL=="intel-rapl:[0-9]*", KERNEL!="intel-rapl:*:*", ATTR{name}=="package*", \\',
1565
+ ` RUN+="/usr/bin/chgrp ${accounts.powerGroup} /sys%p/energy_uj", \\`,
1566
+ ' RUN+="/usr/bin/chmod 0440 /sys%p/energy_uj", \\',
1567
+ ` RUN+="/usr/bin/chgrp ${accounts.powerGroup} /sys%p/max_energy_range_uj", \\`,
1568
+ ' RUN+="/usr/bin/chmod 0440 /sys%p/max_energy_range_uj"',
1569
+ ""
1570
+ ].join("\n");
1571
+ }
1572
+ function hubEnvTemplate(paths, origin, port = 8081) {
1573
+ const lines = [
1574
+ "# Rigscope hub environment. Root-owned private file, mode 0600.",
1575
+ "# Credentials remain commented out until an operator adds them: with no",
1576
+ "# browser users the hub fails closed and exposes no protected data.",
1577
+ "# Generate password hashes locally with scripts/hash-browser-password.cjs.",
1578
+ envLine("HUB_DATABASE_PATH", hubDatabasePath(paths)),
1579
+ envLine("HUB_STATE_FILE", hubEnrollmentPath(paths)),
1580
+ ...origin ? [envLine("HUB_PUBLIC_ORIGIN", origin), envLine("HUB_SOCKET_PATH", hubSocketPath(paths))] : [envLine("HUB_HOST", "127.0.0.1"), envLine("HUB_PORT", String(port))],
1581
+ '# HUB_BROWSER_USERS=[{"username":"operator","passwordHash":"<private hash>","machineIds":"*"}]',
1582
+ '# HUB_COLLECTORS=[{"collectorId":"...","machineId":"...","token":"..."}]',
1583
+ ""
1584
+ ];
1585
+ return lines.join("\n");
1586
+ }
1587
+ function collectorEnvTemplate() {
1588
+ return [
1589
+ "# Rigscope collector environment. Root-owned private file, mode 0600.",
1590
+ "# Enrollment writes the durable destination/token state under /var/lib/rigscope.",
1591
+ "# COLLECTOR_ENROLL_URL=ws://127.0.0.1:8081/api/collector/connect",
1592
+ "# COLLECTOR_ENROLL_TOKEN=<one-use grant>",
1593
+ "# COLLECTOR_MODE=real",
1594
+ ""
1595
+ ].join("\n");
1596
+ }
1597
+ function assertPrivateSourceFile(path, label) {
1598
+ const stat = (0, import_node_fs3.lstatSync)(path);
1599
+ if (!stat.isFile() || stat.isSymbolicLink()) {
1600
+ throw new SystemError("unsafe-adoption", `${label} must be a regular file: ${path}`);
1601
+ }
1602
+ if ((stat.mode & 63) !== 0) {
1603
+ throw new SystemError("insecure-permissions", `${label} must be private (0600) before adoption: ${path}`);
1604
+ }
1605
+ }
1606
+ function readJsonObject(path) {
1607
+ let raw;
1608
+ try {
1609
+ raw = (0, import_node_fs3.readFileSync)(path, "utf8");
1610
+ } catch (error) {
1611
+ throw new SystemError("missing-file", `cannot read ${path}: ${error.message}`);
1612
+ }
1613
+ let value;
1614
+ try {
1615
+ value = JSON.parse(raw);
1616
+ } catch {
1617
+ throw new SystemError("invalid-file", `${path} is not valid JSON`);
1618
+ }
1619
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1620
+ throw new SystemError("invalid-file", `${path} must contain a JSON object`);
1621
+ }
1622
+ return value;
1623
+ }
1624
+ function readSystemState(paths) {
1625
+ const path = stateFilePath(paths);
1626
+ if (!(0, import_node_fs3.existsSync)(path)) return null;
1627
+ const stat = (0, import_node_fs3.lstatSync)(path);
1628
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new SystemError("unsafe-path", `${path} must be a regular file`);
1629
+ if ((stat.mode & 63) !== 0) throw new SystemError("insecure-permissions", `${path} must be 0600`);
1630
+ const raw = readJsonObject(path);
1631
+ if (raw.formatVersion !== 1 && raw.formatVersion !== SYSTEM_STATE_FORMAT_VERSION || raw.scope !== SYSTEM_SCOPE) {
1632
+ throw new SystemError("invalid-state", `${path} is not a Rigscope system installation state`);
1633
+ }
1634
+ const savedPaths = raw.paths;
1635
+ if (!savedPaths || savedPaths.prefix !== paths.prefix || savedPaths.stateDir !== paths.stateDir) {
1636
+ throw new SystemError("invalid-state", "system installation state belongs to different paths");
1637
+ }
1638
+ const savedAccounts = raw.accounts;
1639
+ const accounts = validateAccounts(savedAccounts ?? {});
1640
+ if (raw.role !== "local" && raw.role !== "hub" && raw.role !== "collector") {
1641
+ throw new SystemError("invalid-state", "system installation state has an invalid role");
1642
+ }
1643
+ let components;
1644
+ if (raw.formatVersion === 1) {
1645
+ const mapped = componentsForRole(raw.role);
1646
+ components = { hub: { enabled: mapped.hub }, collector: { enabled: mapped.collector } };
1647
+ } else {
1648
+ const saved = raw.components;
1649
+ if (saved === null || typeof saved !== "object" || saved.hub === null || saved.collector === null || typeof saved.hub !== "object" || typeof saved.collector !== "object" || typeof saved.hub.enabled !== "boolean" || typeof saved.collector.enabled !== "boolean") {
1650
+ throw new SystemError("invalid-state", "system installation state has invalid component enabled state");
1651
+ }
1652
+ components = { hub: { enabled: saved.hub.enabled }, collector: { enabled: saved.collector.enabled } };
1653
+ }
1654
+ return {
1655
+ ...raw,
1656
+ formatVersion: SYSTEM_STATE_FORMAT_VERSION,
1657
+ accounts,
1658
+ components
1659
+ };
1660
+ }
1661
+ function writeSystemState(paths, state) {
1662
+ ensureDirectory(paths.stateDir, 493, false);
1663
+ atomicWrite(stateFilePath(paths), `${JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
1664
+ `, 384);
1665
+ }
1666
+ function emptyState(options) {
1667
+ const timestamp = new Date(options.now()).toISOString();
1668
+ const components = options.components ?? componentsForRole(options.role);
1669
+ return {
1670
+ formatVersion: SYSTEM_STATE_FORMAT_VERSION,
1671
+ scope: SYSTEM_SCOPE,
1672
+ role: options.role,
1673
+ active: true,
1674
+ components: { hub: { enabled: components.hub }, collector: { enabled: components.collector } },
1675
+ paths: options.paths,
1676
+ accounts: options.accounts,
1677
+ nodePath: options.nodePath,
1678
+ currentVersion: null,
1679
+ previousVersion: null,
1680
+ releases: {},
1681
+ backups: {},
1682
+ adoption: null,
1683
+ power: null,
1684
+ installedAt: timestamp,
1685
+ updatedAt: timestamp
1686
+ };
1687
+ }
1688
+ function validateVersion(value) {
1689
+ if (typeof value !== "string" || !VERSION_PATTERN.test(value)) {
1690
+ throw new SystemError("invalid-version", `invalid release version: ${String(value)}`);
1691
+ }
1692
+ return value;
1693
+ }
1694
+ function releaseVersion(root) {
1695
+ return validateVersion(readJsonObject((0, import_node_path3.join)(root, "package.json")).version);
1696
+ }
1697
+ function releaseSchemaVersion(root) {
1698
+ const manifestPath = (0, import_node_path3.join)(root, "manifest.json");
1699
+ if (!(0, import_node_fs3.existsSync)(manifestPath)) return null;
1700
+ const manifest = readJsonObject(manifestPath);
1701
+ return Number.isSafeInteger(manifest.schemaVersion) ? Number(manifest.schemaVersion) : null;
1702
+ }
1703
+ function assertReleaseArtifacts(root) {
1704
+ for (const artifact of REQUIRED_ARTIFACTS) {
1705
+ const path = (0, import_node_path3.join)(root, artifact);
1706
+ let stat;
1707
+ try {
1708
+ stat = (0, import_node_fs3.lstatSync)(path);
1709
+ } catch {
1710
+ throw new SystemError("missing-artifact", `release is missing ${artifact}`);
1711
+ }
1712
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size === 0) {
1713
+ throw new SystemError("invalid-artifact", `release artifact is not a non-empty regular file: ${artifact}`);
1714
+ }
1715
+ }
1716
+ const buildId = (0, import_node_fs3.readFileSync)((0, import_node_path3.join)(root, ".next", "BUILD_ID"), "utf8").trim();
1717
+ if (buildId === "" || /\s/.test(buildId)) throw new SystemError("invalid-artifact", ".next/BUILD_ID is invalid");
1718
+ for (const directory of [".next/server", ".next/static"]) {
1719
+ let stat;
1720
+ try {
1721
+ stat = (0, import_node_fs3.lstatSync)((0, import_node_path3.join)(root, directory));
1722
+ } catch {
1723
+ throw new SystemError("missing-artifact", `release is missing ${directory}/`);
1724
+ }
1725
+ if (!stat.isDirectory()) throw new SystemError("invalid-artifact", `${directory} is not a directory`);
1726
+ }
1727
+ }
1728
+ function verifyReleaseManifest(root) {
1729
+ const manifestPath = (0, import_node_path3.join)(root, "manifest.json");
1730
+ if (!(0, import_node_fs3.existsSync)(manifestPath)) return;
1731
+ const manifest = readJsonObject(manifestPath);
1732
+ const packageJson = readJsonObject((0, import_node_path3.join)(root, "package.json"));
1733
+ if (manifest.name !== void 0 && manifest.name !== packageJson.name) {
1734
+ throw new SystemError("manifest-mismatch", "release manifest name does not match package.json");
1735
+ }
1736
+ if (manifest.version !== void 0 && manifest.version !== packageJson.version) {
1737
+ throw new SystemError("manifest-mismatch", "release manifest version does not match package.json");
1738
+ }
1739
+ const artifacts = manifest.artifacts;
1740
+ if (!Array.isArray(artifacts) || artifacts.length === 0) return;
1741
+ const listed = /* @__PURE__ */ new Set();
1742
+ for (const raw of artifacts) {
1743
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
1744
+ throw new SystemError("invalid-manifest", "release manifest contains an invalid artifact entry");
1745
+ }
1746
+ const entry = raw;
1747
+ const relativePath = entry.path;
1748
+ if (typeof relativePath !== "string" || relativePath === "" || relativePath.startsWith("/") || relativePath.includes("\\") || relativePath.split("/").includes("..")) {
1749
+ throw new SystemError("invalid-manifest", `release manifest contains an unsafe path: ${String(relativePath)}`);
1750
+ }
1751
+ const absolute = (0, import_node_path3.join)(root, relativePath);
1752
+ const stat = (0, import_node_fs3.lstatSync)(absolute);
1753
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new SystemError("invalid-manifest", `release artifact is not a regular file: ${relativePath}`);
1754
+ if (entry.bytes !== stat.size) throw new SystemError("artifact-mismatch", `release artifact size changed: ${relativePath}`);
1755
+ if (entry.sha256 !== sha256File(absolute)) throw new SystemError("artifact-mismatch", `release artifact hash changed: ${relativePath}`);
1756
+ listed.add(relativePath);
1757
+ }
1758
+ const visit = (current) => {
1759
+ for (const entry of (0, import_node_fs3.readdirSync)(current, { withFileTypes: true })) {
1760
+ const absolute = (0, import_node_path3.join)(current, entry.name);
1761
+ const rel = (0, import_node_path3.relative)(root, absolute).split(import_node_path3.sep).join("/");
1762
+ if (rel === "node_modules" || rel.startsWith("node_modules/") || rel === ".next/cache" || rel.startsWith(".next/cache/")) continue;
1763
+ if (entry.isSymbolicLink()) throw new SystemError("unsafe-release", `release contains a symlink: ${rel}`);
1764
+ if (entry.isDirectory()) {
1765
+ visit(absolute);
1766
+ continue;
1767
+ }
1768
+ if (rel === "manifest.json") continue;
1769
+ if (!listed.has(rel)) throw new SystemError("artifact-mismatch", `release contains an unlisted file: ${rel}`);
1770
+ }
1771
+ };
1772
+ visit(root);
1773
+ }
1774
+ function assertNoSymlinks(root) {
1775
+ const visit = (path) => {
1776
+ const stat = (0, import_node_fs3.lstatSync)(path);
1777
+ if (stat.isSymbolicLink()) throw new SystemError("unsafe-release", `release contains a symlink: ${(0, import_node_path3.relative)(root, path)}`);
1778
+ if (!stat.isDirectory()) return;
1779
+ for (const entry of (0, import_node_fs3.readdirSync)(path)) visit((0, import_node_path3.join)(path, entry));
1780
+ };
1781
+ visit(root);
1782
+ }
1783
+ function copyReleaseTree(source, destination) {
1784
+ const visit = (current, target) => {
1785
+ const stat = (0, import_node_fs3.lstatSync)(current);
1786
+ if (stat.isSymbolicLink()) throw new SystemError("unsafe-release", `source contains a symlink: ${(0, import_node_path3.relative)(source, current)}`);
1787
+ if (stat.isDirectory()) {
1788
+ (0, import_node_fs3.mkdirSync)(target, { recursive: true, mode: 493 });
1789
+ for (const entry of (0, import_node_fs3.readdirSync)(current)) {
1790
+ const child = (0, import_node_path3.join)(current, entry);
1791
+ const rel = (0, import_node_path3.relative)(source, child);
1792
+ if (rel === ".git" || rel === ".next-dev" || rel === ".next/cache" || rel === "node_modules" || rel.startsWith("node_modules/")) {
1793
+ continue;
1794
+ }
1795
+ visit(child, (0, import_node_path3.join)(target, entry));
1796
+ }
1797
+ return;
1798
+ }
1799
+ if (!stat.isFile()) throw new SystemError("unsafe-release", `source contains a non-regular file: ${(0, import_node_path3.relative)(source, current)}`);
1800
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(target), { recursive: true, mode: 493 });
1801
+ (0, import_node_fs3.copyFileSync)(current, target);
1802
+ (0, import_node_fs3.chmodSync)(target, 420);
1803
+ };
1804
+ visit(source, destination);
1805
+ }
1806
+ function copyReleaseAllowlisted(source, destination) {
1807
+ for (const entry of (0, import_node_fs3.readdirSync)(source)) {
1808
+ if (!PACKAGE_TOP_LEVEL.has(entry)) continue;
1809
+ if (entry === "node_modules") continue;
1810
+ const child = (0, import_node_path3.join)(source, entry);
1811
+ const target = (0, import_node_path3.join)(destination, entry);
1812
+ const stat = (0, import_node_fs3.lstatSync)(child);
1813
+ if (stat.isSymbolicLink()) throw new SystemError("unsafe-release", `source contains a symlink: ${entry}`);
1814
+ if (stat.isDirectory()) {
1815
+ copyReleaseTree(child, target);
1816
+ } else if (stat.isFile()) {
1817
+ (0, import_node_fs3.copyFileSync)(child, target);
1818
+ (0, import_node_fs3.chmodSync)(target, 420);
1819
+ }
1820
+ }
1821
+ }
1822
+ function extractArchive(archive, stage, host) {
1823
+ const listing = runRequired(host, "tar", ["-tvzf", archive], "archive inspection");
1824
+ const lines = listing.stdout.split("\n").filter((line) => line.trim() !== "");
1825
+ if (lines.length === 0) throw new SystemError("invalid-archive", "release archive is empty");
1826
+ let strip = 0;
1827
+ const names = [];
1828
+ for (const line of lines) {
1829
+ if (/^[lhcbps]/.test(line.trim())) {
1830
+ throw new SystemError("unsafe-release", "release archive contains a link or special file");
1831
+ }
1832
+ const match = / -> /.test(line);
1833
+ if (match) throw new SystemError("unsafe-release", "release archive contains a symlink");
1834
+ const parts = line.trim().split(/\s+/);
1835
+ const name = parts[parts.length - 1] ?? "";
1836
+ names.push(name);
1837
+ }
1838
+ if (names.every((name) => name === "package" || name.startsWith("package/"))) strip = 1;
1839
+ for (const name of names) {
1840
+ const normalized = name.replace(/^package\//, "").replace(/^\.\//, "");
1841
+ if (normalized === "" || normalized === ".") continue;
1842
+ if (normalized.startsWith("/") || normalized.split("/").includes("..") || normalized.includes("\\")) {
1843
+ throw new SystemError("unsafe-release", `release archive contains an unsafe path: ${name}`);
1844
+ }
1845
+ }
1846
+ const args = ["-xzf", archive, "-C", stage, "--no-same-owner", "--no-same-permissions"];
1847
+ if (strip === 1) args.push("--strip-components=1");
1848
+ runRequired(host, "tar", args, "archive extraction");
1849
+ assertNoSymlinks(stage);
1850
+ }
1851
+ function stageRelease(options) {
1852
+ const source = (0, import_node_path3.resolve)(options.source);
1853
+ const stat = (0, import_node_fs3.lstatSync)(source);
1854
+ const releasesDir = (0, import_node_path3.join)(options.paths.prefix, "releases");
1855
+ ensureDirectory(releasesDir, 493);
1856
+ const stage = (0, import_node_path3.join)(releasesDir, `.stage-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
1857
+ (0, import_node_fs3.mkdirSync)(stage, { recursive: true, mode: 448 });
1858
+ try {
1859
+ if (stat.isDirectory() && !stat.isSymbolicLink()) {
1860
+ copyReleaseAllowlisted(source, stage);
1861
+ } else if (stat.isFile() && !stat.isSymbolicLink()) {
1862
+ extractArchive(source, stage, options.host);
1863
+ } else {
1864
+ throw new SystemError("invalid-source", "release source must be a directory or .tar.gz archive");
1865
+ }
1866
+ assertReleaseArtifacts(stage);
1867
+ verifyReleaseManifest(stage);
1868
+ const version = releaseVersion(stage);
1869
+ const releaseDir = (0, import_node_path3.join)(releasesDir, version);
1870
+ if ((0, import_node_fs3.existsSync)(releaseDir)) {
1871
+ const current = currentReleaseDir(options.paths);
1872
+ const previous = previousReleaseDir(options.paths);
1873
+ if (!options.replaceRelease || current === releaseDir || previous === releaseDir) {
1874
+ throw new SystemError(
1875
+ "release-exists",
1876
+ `release ${version} is already installed${current === releaseDir || previous === releaseDir ? " or retained" : ""}; choose a new version`
1877
+ );
1878
+ }
1879
+ (0, import_node_fs3.rmSync)(releaseDir, { recursive: true, force: true });
1880
+ }
1881
+ (0, import_node_fs3.renameSync)(stage, releaseDir);
1882
+ ensureDirectory((0, import_node_path3.join)(releaseDir, ".next", "cache"), 493);
1883
+ return { version, releaseDir, schemaVersion: releaseSchemaVersion(releaseDir), artifacts: [...REQUIRED_ARTIFACTS] };
1884
+ } catch (error) {
1885
+ (0, import_node_fs3.rmSync)(stage, { recursive: true, force: true });
1886
+ throw error;
1887
+ }
1888
+ }
1889
+ function dependencyNames(root) {
1890
+ const pkg = readJsonObject((0, import_node_path3.join)(root, "package.json"));
1891
+ const dependencies = pkg.dependencies;
1892
+ if (dependencies === null || typeof dependencies !== "object" || Array.isArray(dependencies)) return [];
1893
+ return Object.keys(dependencies).filter((name) => /^[A-Za-z0-9@/._-]+$/.test(name));
1894
+ }
1895
+ function missingDependencies(root) {
1896
+ return dependencyNames(root).filter((name) => !(0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, "node_modules", name, "package.json")));
1897
+ }
1898
+ function installDependencies(root, host, command = "npm") {
1899
+ const hasLock = (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, "package-lock.json"));
1900
+ const args = hasLock ? ["ci", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"] : ["install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"];
1901
+ const siblingNpm = (0, import_node_path3.join)((0, import_node_path3.dirname)(process.execPath), "npm");
1902
+ const executable = command === "npm" && (0, import_node_fs3.existsSync)(siblingNpm) ? siblingNpm : command;
1903
+ const nodeBin = (0, import_node_path3.dirname)(process.execPath);
1904
+ const path = [nodeBin, process.env.PATH ?? ""].filter(Boolean).join(":");
1905
+ runRequired(host, executable, args, "production dependency installation", {
1906
+ cwd: root,
1907
+ env: { ...process.env, NODE_ENV: "production", PATH: path },
1908
+ timeoutMs: 6e5
1909
+ });
1910
+ }
1911
+ function currentReleaseDir(paths) {
1912
+ const link = (0, import_node_path3.join)(paths.prefix, "current");
1913
+ try {
1914
+ const target = (0, import_node_fs3.readlinkSync)(link);
1915
+ const absolute = (0, import_node_path3.isAbsolute)(target) ? target : (0, import_node_path3.resolve)(paths.prefix, target);
1916
+ return isWithin((0, import_node_path3.join)(paths.prefix, "releases"), absolute) ? absolute : null;
1917
+ } catch {
1918
+ return null;
1919
+ }
1920
+ }
1921
+ function previousReleaseDir(paths) {
1922
+ const link = (0, import_node_path3.join)(paths.prefix, "previous");
1923
+ try {
1924
+ const target = (0, import_node_fs3.readlinkSync)(link);
1925
+ const absolute = (0, import_node_path3.isAbsolute)(target) ? target : (0, import_node_path3.resolve)(paths.prefix, target);
1926
+ return isWithin((0, import_node_path3.join)(paths.prefix, "releases"), absolute) ? absolute : null;
1927
+ } catch {
1928
+ return null;
1929
+ }
1930
+ }
1931
+ function replaceSymlink(link, target, host) {
1932
+ const temporary = `${link}.tmp-${process.pid}-${Date.now()}`;
1933
+ try {
1934
+ (0, import_node_fs3.symlinkSync)(target, temporary);
1935
+ (0, import_node_fs3.renameSync)(temporary, link);
1936
+ } catch (error) {
1937
+ try {
1938
+ (0, import_node_fs3.unlinkSync)(temporary);
1939
+ } catch {
1940
+ }
1941
+ throw error;
1942
+ }
1943
+ void host;
1944
+ }
1945
+ function removeSymlink(link) {
1946
+ try {
1947
+ const stat = (0, import_node_fs3.lstatSync)(link);
1948
+ if (stat.isSymbolicLink()) (0, import_node_fs3.unlinkSync)(link);
1949
+ } catch {
1950
+ }
1951
+ }
1952
+ function nodeVersion(host, nodePath) {
1953
+ const result = host.run(nodePath, ["--version"], { timeoutMs: 5e3 });
1954
+ const output = result.stdout.trim();
1955
+ if (result.status !== 0 || !/^v26\.\d+\.\d+$/.test(output)) {
1956
+ throw new SystemError("unsupported-node", `selected executable must run Node 26; got ${output || "no version"}`);
1957
+ }
1958
+ return output;
1959
+ }
1960
+ function ensureRootOwnedNode(options) {
1961
+ const selected = (0, import_node_path3.resolve)(options.nodePath ?? process.execPath);
1962
+ const real = (0, import_node_fs3.realpathSync)(selected);
1963
+ nodeVersion(options.host, real);
1964
+ const stat = (0, import_node_fs3.statSync)(real);
1965
+ const unsafe = stat.uid !== 0 || (stat.mode & 18) !== 0 || options.copyNode === true;
1966
+ if (!unsafe || options.trustedNode === true) return real;
1967
+ const target = (0, import_node_path3.join)(options.paths.prefix, "node", "node");
1968
+ if (!options.apply) return target;
1969
+ ensureDirectory((0, import_node_path3.join)(options.paths.prefix, "node"), 493);
1970
+ (0, import_node_fs3.copyFileSync)(real, target);
1971
+ (0, import_node_fs3.chmodSync)(target, 493);
1972
+ runRequired(options.host, "chown", ["root:root", target], "node ownership");
1973
+ nodeVersion(options.host, target);
1974
+ return target;
1975
+ }
1976
+ function normalizedJsonArray(value, label) {
1977
+ if (value === void 0 || value === null || value === "") return void 0;
1978
+ const candidate = typeof value === "string" ? value : JSON.stringify(value);
1979
+ if (typeof candidate !== "string") throw new SystemError("invalid-adoption", `${label} must be JSON`);
1980
+ let parsed;
1981
+ try {
1982
+ parsed = JSON.parse(candidate);
1983
+ } catch {
1984
+ throw new SystemError("invalid-adoption", `${label} is not valid JSON`);
1985
+ }
1986
+ if (!Array.isArray(parsed)) throw new SystemError("invalid-adoption", `${label} must be a JSON array`);
1987
+ return JSON.stringify(parsed);
1988
+ }
1989
+ function validatedOrigin(value) {
1990
+ if (value === void 0 || value === null || value === "") return void 0;
1991
+ if (typeof value !== "string") throw new SystemError("invalid-adoption", "adopted hub origin is not a URL");
1992
+ let parsed;
1993
+ try {
1994
+ parsed = new URL(value);
1995
+ } catch {
1996
+ throw new SystemError("invalid-adoption", "adopted hub origin is not a URL");
1997
+ }
1998
+ if (parsed.protocol !== "https:") throw new SystemError("invalid-adoption", "adopted hub origin must be https");
1999
+ return value;
2000
+ }
2001
+ function hasCollectorIdentity(source) {
2002
+ return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(source, "collector", "state.json")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(source, "collector", "collector.json"));
2003
+ }
2004
+ function readAdoptionSetup(source) {
2005
+ const modernPath = (0, import_node_path3.join)(source, "config.json");
2006
+ const legacyPath = (0, import_node_path3.join)(source, "hub-config.json");
2007
+ if ((0, import_node_fs3.existsSync)(modernPath)) {
2008
+ assertPrivateSourceFile(modernPath, "adopted setup");
2009
+ const setup = readJsonObject(modernPath);
2010
+ const role = setup.role;
2011
+ if (setup.version !== 1 || typeof role !== "string" || !["local", "hub", "collector"].includes(role)) {
2012
+ throw new SystemError("invalid-adoption", "adopted setup version or role is incompatible");
2013
+ }
2014
+ const port = Number(setup.port ?? 8081);
2015
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new SystemError("invalid-adoption", "adopted hub port is invalid");
2016
+ return {
2017
+ role,
2018
+ port,
2019
+ origin: validatedOrigin(setup.origin),
2020
+ usersJson: normalizedJsonArray(setup.users, "HUB_BROWSER_USERS"),
2021
+ collectorsJson: normalizedJsonArray(setup.collectors, "HUB_COLLECTORS"),
2022
+ maxConnections: Number.isSafeInteger(setup.maxCollectorConnections) ? Number(setup.maxCollectorConnections) : void 0,
2023
+ legacy: false
2024
+ };
2025
+ }
2026
+ if ((0, import_node_fs3.existsSync)(legacyPath)) {
2027
+ assertPrivateSourceFile(legacyPath, "adopted hub configuration");
2028
+ const legacy = readJsonObject(legacyPath);
2029
+ const hasHub = ["HUB_PUBLIC_ORIGIN", "HUB_BROWSER_USERS", "HUB_DATABASE_PATH", "HUB_HOST", "HUB_PORT"].some((key) => key in legacy);
2030
+ const hasCollector = hasCollectorIdentity(source);
2031
+ if (!hasHub && !hasCollector) throw new SystemError("invalid-adoption", "adopted configuration is neither a hub nor a collector setup");
2032
+ return {
2033
+ role: hasHub && hasCollector ? "local" : hasHub ? "hub" : "collector",
2034
+ port: Number(legacy.HUB_PORT ?? 8081),
2035
+ origin: validatedOrigin(legacy.HUB_PUBLIC_ORIGIN),
2036
+ usersJson: normalizedJsonArray(legacy.HUB_BROWSER_USERS, "HUB_BROWSER_USERS"),
2037
+ collectorsJson: normalizedJsonArray(legacy.HUB_COLLECTORS, "HUB_COLLECTORS"),
2038
+ maxConnections: Number.isSafeInteger(Number(legacy.HUB_MAX_COLLECTOR_CONNECTIONS)) ? Number(legacy.HUB_MAX_COLLECTOR_CONNECTIONS) : void 0,
2039
+ legacy: true
2040
+ };
2041
+ }
2042
+ if (hasCollectorIdentity(source) && (0, import_node_fs3.existsSync)((0, import_node_path3.join)(source, "collector"))) {
2043
+ return { role: "collector", port: 8081, legacy: true };
2044
+ }
2045
+ throw new SystemError("invalid-adoption", "adopted installation has no config.json or hub-config.json");
2046
+ }
2047
+ function resolveRole(options) {
2048
+ const role = options.role ?? "local";
2049
+ if (!["local", "hub", "collector"].includes(role)) {
2050
+ throw new SystemError("invalid-role", "--role must be local, hub or collector");
2051
+ }
2052
+ if (options.adoptFrom) {
2053
+ const setup = readAdoptionSetup(options.adoptFrom);
2054
+ if (options.role !== void 0 && options.role !== setup.role && setup.role !== "local") {
2055
+ throw new SystemError("role-mismatch", `the adopted setup is ${setup.role}; do not change its role during adoption`);
2056
+ }
2057
+ return options.role ?? setup.role;
2058
+ }
2059
+ return role;
2060
+ }
2061
+ function detectUserInstallations(options = {}) {
2062
+ const roots = options.homeRoots ?? ["/home", "/root"];
2063
+ const found = [];
2064
+ for (const homeRoot of roots) {
2065
+ if (!(0, import_node_fs3.existsSync)(homeRoot)) continue;
2066
+ const homes = homeRoot === "/root" ? ["/root"] : (0, import_node_fs3.readdirSync)(homeRoot).map((entry) => (0, import_node_path3.join)(homeRoot, entry));
2067
+ for (const home of homes) {
2068
+ let stat;
2069
+ try {
2070
+ stat = (0, import_node_fs3.lstatSync)(home);
2071
+ } catch {
2072
+ continue;
2073
+ }
2074
+ if (!stat.isDirectory()) continue;
2075
+ const unitDir = (0, import_node_path3.join)(home, ".config", "systemd", "user");
2076
+ let units = [];
2077
+ try {
2078
+ units = (0, import_node_fs3.readdirSync)(unitDir);
2079
+ } catch {
2080
+ units = [];
2081
+ }
2082
+ for (const unitName of units) {
2083
+ if (!/^rigscope(?:-[0-9a-f]{12})?(?:-(?:hub|collector))?\.service$/.test(unitName)) continue;
2084
+ const unitPath = (0, import_node_path3.join)(unitDir, unitName);
2085
+ let execStart = null;
2086
+ try {
2087
+ const line = (0, import_node_fs3.readFileSync)(unitPath, "utf8").split("\n").find((item) => item.startsWith("ExecStart="));
2088
+ execStart = line ? line.slice("ExecStart=".length) : null;
2089
+ } catch {
2090
+ }
2091
+ const stateCandidates = [(0, import_node_path3.join)(home, ".local", "state", "rigscope")];
2092
+ const stateDir = stateCandidates.find((candidate) => (0, import_node_fs3.existsSync)(candidate)) ?? null;
2093
+ found.push({ user: (0, import_node_path3.basename)(home), home, unitPath, unitName, execStart, stateDir });
2094
+ }
2095
+ }
2096
+ }
2097
+ return found;
2098
+ }
2099
+ function lockIsLive(path) {
2100
+ try {
2101
+ const stat = (0, import_node_fs3.lstatSync)(path);
2102
+ if (!stat.isFile() || stat.size > 4096) return { exists: true, live: false, pid: null, ambiguous: true };
2103
+ const owner = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
2104
+ const pid = owner.pid;
2105
+ if (!Number.isSafeInteger(pid) || pid < 1) return { exists: true, live: false, pid: null, ambiguous: true };
2106
+ try {
2107
+ process.kill(pid, 0);
2108
+ return { exists: true, live: true, pid, ambiguous: false };
2109
+ } catch (error) {
2110
+ const code = error.code;
2111
+ if (code === "ESRCH") return { exists: true, live: false, pid, ambiguous: false };
2112
+ return { exists: true, live: true, pid, ambiguous: true };
2113
+ }
2114
+ } catch (error) {
2115
+ if (error.code === "ENOENT") return { exists: false, live: false, pid: null, ambiguous: false };
2116
+ return { exists: true, live: false, pid: null, ambiguous: true };
2117
+ }
2118
+ }
2119
+ function directoryLocks(directory) {
2120
+ return [
2121
+ (0, import_node_path3.join)(directory, "hub", "telemetry.sqlite.hub.lock"),
2122
+ (0, import_node_path3.join)(directory, "run", "rigscope.lock"),
2123
+ (0, import_node_path3.join)(directory, "collector", "collector-runtime.lock")
2124
+ ];
2125
+ }
2126
+ function assertDirectoryStopped(directory, scope = "local") {
2127
+ const components = typeof scope === "string" && (scope === "local" || scope === "hub" || scope === "collector") ? componentsForRole(scope) : componentSet(scope);
2128
+ const locks = directoryLocks(directory).filter((path) => (!path.includes("/collector/") || components.collector) && (path.includes("/collector/") || components.hub));
2129
+ for (const lock of locks) {
2130
+ const state = lockIsLive(lock);
2131
+ if (state.live || state.ambiguous) {
2132
+ throw new SystemError(
2133
+ "service-active",
2134
+ `stop the existing installation before adoption; a live or ambiguous runtime lock exists: ${lock}`
2135
+ );
2136
+ }
2137
+ }
2138
+ }
2139
+ function systemLockState(paths) {
2140
+ const path = `${hubDatabasePath(paths)}.hub.lock`;
2141
+ const state = lockIsLive(path);
2142
+ return { live: state.live || state.ambiguous, path, pid: state.pid, ambiguous: state.ambiguous };
2143
+ }
2144
+ function assertSystemStartAllowed(paths, components) {
2145
+ if (componentSet(components).hub) {
2146
+ const lock = systemLockState(paths);
2147
+ if (lock.live) {
2148
+ throw new SystemError("duplicate-runtime", `a hub runtime already owns ${lock.path}; stop it before starting another`);
2149
+ }
2150
+ }
2151
+ }
2152
+ function detectPowerDriver(moduleRoot = "/sys/module") {
2153
+ const candidates = ["intel_rapl_msr", "intel_rapl_common", "intel_rapl_tpmi", "intel_rapl", "amd_energy"];
2154
+ return candidates.filter((name) => (0, import_node_fs3.existsSync)((0, import_node_path3.join)(moduleRoot, name)));
2155
+ }
2156
+ function readPasswd(path = "/etc/passwd") {
2157
+ const users = /* @__PURE__ */ new Map();
2158
+ let lines;
2159
+ try {
2160
+ lines = (0, import_node_fs3.readFileSync)(path, "utf8").split("\n");
2161
+ } catch {
2162
+ return users;
2163
+ }
2164
+ for (const line of lines) {
2165
+ const parts = line.split(":");
2166
+ if (parts.length < 4) continue;
2167
+ const uid = Number(parts[2]);
2168
+ const gid = Number(parts[3]);
2169
+ if (parts[0] && Number.isSafeInteger(uid) && Number.isSafeInteger(gid)) users.set(parts[0], { uid, gid });
2170
+ }
2171
+ return users;
2172
+ }
2173
+ function readGroups(path = "/etc/group") {
2174
+ const groups = /* @__PURE__ */ new Map();
2175
+ let lines;
2176
+ try {
2177
+ lines = (0, import_node_fs3.readFileSync)(path, "utf8").split("\n");
2178
+ } catch {
2179
+ return groups;
2180
+ }
2181
+ for (const line of lines) {
2182
+ const parts = line.split(":");
2183
+ if (parts.length < 4) continue;
2184
+ const gid = Number(parts[2]);
2185
+ if (!parts[0] || !Number.isSafeInteger(gid)) continue;
2186
+ groups.set(parts[0], { gid, members: parts[3] ? parts[3].split(",").filter(Boolean) : [] });
2187
+ }
2188
+ return groups;
2189
+ }
2190
+ function modeString(mode) {
2191
+ return `0${(mode & 511).toString(8).padStart(3, "0")}`;
2192
+ }
2193
+ function inspectPowerAccess(options = {}) {
2194
+ const paths = validateSystemPaths(options.paths ?? {});
2195
+ const accounts = validateAccounts(options.accounts ?? {});
2196
+ const users = readPasswd(options.passwdFile ?? "/etc/passwd");
2197
+ const groups = readGroups(options.groupFile ?? "/etc/group");
2198
+ const collector = users.get(accounts.collector) ?? null;
2199
+ const group = groups.get(accounts.powerGroup) ?? null;
2200
+ const collectorGids = collector ? [collector.gid, ...group?.members.includes(accounts.collector) ? [group.gid] : []] : [];
2201
+ const rulePath = options.rulePath ?? raplRulePath(paths);
2202
+ const sysusers = options.sysusersPath ?? powerSysusersPath(paths);
2203
+ let ruleText = null;
2204
+ try {
2205
+ ruleText = (0, import_node_fs3.readFileSync)(rulePath, "utf8");
2206
+ } catch {
2207
+ ruleText = null;
2208
+ }
2209
+ const requiredFragments = [
2210
+ 'SUBSYSTEM=="powercap"',
2211
+ 'KERNEL=="intel-rapl:[0-9]*"',
2212
+ 'ATTR{name}=="package*"',
2213
+ `/sys%p/energy_uj`,
2214
+ `/sys%p/max_energy_range_uj`,
2215
+ `chgrp ${accounts.powerGroup}`,
2216
+ "chmod 0440"
2217
+ ];
2218
+ const compliant = ruleText !== null && requiredFragments.every((fragment) => ruleText.includes(fragment));
2219
+ const domains = [];
2220
+ let entries = [];
2221
+ try {
2222
+ entries = (0, import_node_fs3.readdirSync)(paths.powercapRoot);
2223
+ } catch {
2224
+ entries = [];
2225
+ }
2226
+ for (const entry of entries.filter((name) => /^intel-rapl:\d+$/.test(name)).sort()) {
2227
+ const domainPath = (0, import_node_path3.join)(paths.powercapRoot, entry);
2228
+ let name = entry;
2229
+ try {
2230
+ name = (0, import_node_fs3.readFileSync)((0, import_node_path3.join)(domainPath, "name"), "utf8").trim() || entry;
2231
+ } catch {
2232
+ }
2233
+ if (!/^package/.test(name)) continue;
2234
+ const energyPath = (0, import_node_path3.join)(domainPath, "energy_uj");
2235
+ const maxRangePath = (0, import_node_path3.join)(domainPath, "max_energy_range_uj");
2236
+ const readStat = (path) => {
2237
+ if (options.statOverride) {
2238
+ const override = options.statOverride(path);
2239
+ return override === null ? null : { mode: modeString(override.mode), uid: override.uid, gid: override.gid };
2240
+ }
2241
+ try {
2242
+ const stat = (0, import_node_fs3.statSync)(path);
2243
+ return { mode: modeString(stat.mode), uid: stat.uid, gid: stat.gid };
2244
+ } catch {
2245
+ return null;
2246
+ }
2247
+ };
2248
+ const energyStat = readStat(energyPath);
2249
+ const maxStat = readStat(maxRangePath);
2250
+ const energyMode = energyStat === null ? 0 : Number.parseInt(energyStat.mode, 8);
2251
+ const readable = energyStat !== null && (collector !== null && (energyStat.uid === collector.uid && (energyMode & 256) !== 0 || collectorGids.includes(energyStat.gid) && (energyMode & 32) !== 0 || (energyMode & 4) !== 0));
2252
+ domains.push({
2253
+ path: domainPath,
2254
+ name,
2255
+ energyPath,
2256
+ energyExists: energyStat !== null,
2257
+ energy: energyStat === null ? null : {
2258
+ ...energyStat,
2259
+ readable,
2260
+ worldReadable: (energyMode & 4) !== 0,
2261
+ writable: (energyMode & 146) !== 0
2262
+ },
2263
+ maxRangePath,
2264
+ maxRangeExists: maxStat !== null,
2265
+ maxRange: maxStat
2266
+ });
2267
+ }
2268
+ const problems = [];
2269
+ if (domains.length === 0) problems.push("no top-level package powercap counters were found");
2270
+ if (!collector) problems.push(`collector account ${accounts.collector} does not exist`);
2271
+ if (!group) problems.push(`power group ${accounts.powerGroup} does not exist`);
2272
+ if (collector && group && !group.members.includes(accounts.collector)) {
2273
+ problems.push(`collector account ${accounts.collector} is not a member of ${accounts.powerGroup}`);
2274
+ }
2275
+ if (!compliant) problems.push(`persistent udev rule is missing or not compliant: ${rulePath}`);
2276
+ if (!(0, import_node_fs3.existsSync)(sysusers)) problems.push(`power sysusers definition is missing: ${sysusers}`);
2277
+ for (const domain of domains) {
2278
+ if (!domain.energyExists) problems.push(`${domain.name}: energy_uj is unavailable`);
2279
+ else if (domain.energy?.worldReadable) problems.push(`${domain.name}: energy_uj is world-readable`);
2280
+ else if (domain.energy?.writable) problems.push(`${domain.name}: energy_uj is writable`);
2281
+ else if (!domain.energy?.readable) problems.push(`${domain.name}: collector cannot read energy_uj`);
2282
+ }
2283
+ const worldReadable = domains.some((domain) => domain.energy?.worldReadable === true);
2284
+ const writableByCollector = domains.some((domain) => domain.energy?.writable === true);
2285
+ const collectorCanRead = domains.length > 0 && domains.every((domain) => domain.energy?.readable === true);
2286
+ return {
2287
+ available: domains.length > 0,
2288
+ driver: detectPowerDriver(options.moduleRoot ?? "/sys/module"),
2289
+ domains,
2290
+ collectorUser: accounts.collector,
2291
+ collectorUid: collector?.uid ?? null,
2292
+ collectorGids,
2293
+ group: { name: accounts.powerGroup, exists: group !== null, gid: group?.gid ?? null, members: group?.members ?? [] },
2294
+ rule: { path: rulePath, present: ruleText !== null, compliant },
2295
+ sysusers: { path: sysusers, present: (0, import_node_fs3.existsSync)(sysusers) },
2296
+ collectorCanRead,
2297
+ persistent: compliant && group !== null && collector !== null && group.members.includes(accounts.collector),
2298
+ worldReadable,
2299
+ writableByCollector,
2300
+ problems
2301
+ };
2302
+ }
2303
+ async function backupDatabase(source, output, host) {
2304
+ ensureDirectory((0, import_node_path3.dirname)(output), 448, true);
2305
+ const db = new import_node_sqlite.DatabaseSync(source, { readOnly: true, timeout: 5e3 });
2306
+ try {
2307
+ await (0, import_node_sqlite.backup)(db, output);
2308
+ } finally {
2309
+ db.close();
2310
+ }
2311
+ (0, import_node_fs3.chmodSync)(output, 384);
2312
+ const check = new import_node_sqlite.DatabaseSync(output, { readOnly: true });
2313
+ try {
2314
+ const result = check.prepare("PRAGMA integrity_check").get();
2315
+ if (!result || Object.values(result)[0] !== "ok") throw new SystemError("invalid-backup", `backup integrity check failed: ${output}`);
2316
+ } finally {
2317
+ check.close();
2318
+ }
2319
+ void host;
2320
+ }
2321
+ function databaseSchema(path) {
2322
+ if (!(0, import_node_fs3.existsSync)(path)) return null;
2323
+ const db = new import_node_sqlite.DatabaseSync(path, { readOnly: true, timeout: 5e3 });
2324
+ try {
2325
+ const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_migrations'").get();
2326
+ if (!table) return null;
2327
+ const row = db.prepare("SELECT MAX(version) AS version FROM schema_migrations").get();
2328
+ return row && Number.isSafeInteger(Number(row.version)) ? Number(row.version) : null;
2329
+ } finally {
2330
+ db.close();
2331
+ }
2332
+ }
2333
+ function replaceDatabaseFromBackup(backupPath, target, safetyBackup, host) {
2334
+ assertPrivateSourceFile(backupPath, "database backup");
2335
+ const backup = new import_node_sqlite.DatabaseSync(backupPath, { readOnly: true });
2336
+ try {
2337
+ if (Object.values(backup.prepare("PRAGMA integrity_check").get())[0] !== "ok") throw new SystemError("invalid-backup", "rollback backup integrity failed");
2338
+ } finally {
2339
+ backup.close();
2340
+ }
2341
+ ensureDirectory((0, import_node_path3.dirname)(target), 448, true);
2342
+ if (safetyBackup !== null && (0, import_node_fs3.existsSync)(target)) {
2343
+ void host;
2344
+ }
2345
+ const temporary = (0, import_node_path3.join)((0, import_node_path3.dirname)(target), `.restore-${process.pid}-${Date.now()}.sqlite`);
2346
+ (0, import_node_fs3.copyFileSync)(backupPath, temporary);
2347
+ (0, import_node_fs3.chmodSync)(temporary, 384);
2348
+ for (const sidecar of [`${target}-wal`, `${target}-shm`]) {
2349
+ try {
2350
+ (0, import_node_fs3.unlinkSync)(sidecar);
2351
+ } catch {
2352
+ }
2353
+ }
2354
+ (0, import_node_fs3.renameSync)(temporary, target);
2355
+ (0, import_node_fs3.chmodSync)(target, 384);
2356
+ }
2357
+ function newestTimestamp() {
2358
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2359
+ }
2360
+ function adoptUserState(options) {
2361
+ const source = (0, import_node_path3.resolve)(options.source);
2362
+ if (!(0, import_node_fs3.existsSync)(source)) throw new SystemError("invalid-adoption", `user installation does not exist: ${source}`);
2363
+ if (isWithin(options.paths.prefix, source) || isWithin(options.paths.stateDir, source) || isWithin(source, options.paths.stateDir)) {
2364
+ throw new SystemError("invalid-adoption", "adoption source and system paths must not overlap");
2365
+ }
2366
+ assertDirectoryStopped(source, options.role);
2367
+ const setup = readAdoptionSetup(source);
2368
+ if (setup.role !== options.role && setup.role !== "local") throw new SystemError("invalid-adoption", "adopted setup role is incompatible");
2369
+ const { port, origin } = setup;
2370
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new SystemError("invalid-adoption", "adopted hub port is invalid");
2371
+ const copied = [];
2372
+ const warnings = [];
2373
+ const targetDatabase = hubDatabasePath(options.paths);
2374
+ const targetCollector = collectorStatePath(options.paths);
2375
+ if (options.apply && (0, import_node_fs3.existsSync)(targetDatabase)) {
2376
+ throw new SystemError("destination-exists", `refusing to overwrite ${targetDatabase}`);
2377
+ }
2378
+ const hubEnv = options.role === "collector" ? null : [
2379
+ "# Adopted by `rigscope system install --adopt-user-state`.",
2380
+ "# Credentials were copied into this root-owned 0600 file; the original user setup is untouched.",
2381
+ envLine("HUB_DATABASE_PATH", targetDatabase),
2382
+ envLine("HUB_STATE_FILE", hubEnrollmentPath(options.paths)),
2383
+ ...origin ? [envLine("HUB_PUBLIC_ORIGIN", origin), envLine("HUB_SOCKET_PATH", hubSocketPath(options.paths))] : [envLine("HUB_HOST", "127.0.0.1"), envLine("HUB_PORT", String(port))],
2384
+ ...setup.usersJson !== void 0 ? [envLine("HUB_BROWSER_USERS", setup.usersJson)] : [],
2385
+ ...setup.collectorsJson !== void 0 ? [envLine("HUB_COLLECTORS", setup.collectorsJson)] : [],
2386
+ ...setup.maxConnections !== void 0 ? [envLine("HUB_MAX_COLLECTOR_CONNECTIONS", String(setup.maxConnections))] : [],
2387
+ ""
2388
+ ].join("\n");
2389
+ const collectorEnv = options.role === "hub" ? null : [
2390
+ "# Adopted by `rigscope system install --adopt-user-state`.",
2391
+ envLine("COLLECTOR_MODE", "real"),
2392
+ envLine("COLLECTOR_STATE_FILE", targetCollector),
2393
+ ""
2394
+ ].join("\n");
2395
+ void options.accounts;
2396
+ return {
2397
+ hubEnv,
2398
+ collectorEnv,
2399
+ record: {
2400
+ source,
2401
+ copiedAt: new Date(options.now()).toISOString(),
2402
+ files: copied,
2403
+ reversible: true
2404
+ },
2405
+ warnings,
2406
+ remote: origin !== void 0,
2407
+ port
2408
+ };
2409
+ }
2410
+ function copyAdoptedTree(source, target, copied, skipLocks = true) {
2411
+ const visit = (current, destination) => {
2412
+ const stat = (0, import_node_fs3.lstatSync)(current);
2413
+ if (stat.isSymbolicLink()) return;
2414
+ if (stat.isDirectory()) {
2415
+ (0, import_node_fs3.mkdirSync)(destination, { recursive: true, mode: 448 });
2416
+ for (const entry of (0, import_node_fs3.readdirSync)(current)) visit((0, import_node_path3.join)(current, entry), (0, import_node_path3.join)(destination, entry));
2417
+ return;
2418
+ }
2419
+ if (!stat.isFile()) return;
2420
+ if (skipLocks && /\.(lock|sock)$/.test(current)) return;
2421
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(destination), { recursive: true, mode: 448 });
2422
+ (0, import_node_fs3.copyFileSync)(current, destination);
2423
+ (0, import_node_fs3.chmodSync)(destination, 384);
2424
+ copied.push({ source: current, target: destination, sha256: sha256File(destination), bytes: stat.size });
2425
+ };
2426
+ visit(source, target);
2427
+ }
2428
+ async function performAdoptionCopyAsync(options) {
2429
+ const { source, paths, role, host, copy } = options;
2430
+ assertDirectoryStopped(source, role);
2431
+ const targetDatabase = hubDatabasePath(paths);
2432
+ const sourceDatabase = (0, import_node_path3.join)(source, "hub", "telemetry.sqlite");
2433
+ if (role !== "collector" && (0, import_node_fs3.existsSync)(sourceDatabase) && !(0, import_node_fs3.existsSync)(targetDatabase)) {
2434
+ assertPrivateSourceFile(sourceDatabase, "adopted database");
2435
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "hub"), 448, true);
2436
+ await backupDatabase(sourceDatabase, targetDatabase, host);
2437
+ copy.record.files.push({
2438
+ source: sourceDatabase,
2439
+ target: targetDatabase,
2440
+ sha256: sha256File(targetDatabase),
2441
+ bytes: (0, import_node_fs3.statSync)(targetDatabase).size
2442
+ });
2443
+ }
2444
+ const sourceCollectorState = [
2445
+ (0, import_node_path3.join)(source, "collector", "state.json"),
2446
+ (0, import_node_path3.join)(source, "collector", "collector.json")
2447
+ ].find((candidate) => (0, import_node_fs3.existsSync)(candidate));
2448
+ if (role !== "hub" && sourceCollectorState) {
2449
+ assertPrivateSourceFile(sourceCollectorState, "adopted collector state");
2450
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "collector"), 448, true);
2451
+ const targetCollector = collectorStatePath(paths);
2452
+ (0, import_node_fs3.copyFileSync)(sourceCollectorState, targetCollector);
2453
+ (0, import_node_fs3.chmodSync)(targetCollector, 384);
2454
+ copy.record.files.push({
2455
+ source: sourceCollectorState,
2456
+ target: targetCollector,
2457
+ sha256: sha256File(targetCollector),
2458
+ bytes: (0, import_node_fs3.statSync)(targetCollector).size
2459
+ });
2460
+ const outbox = (0, import_node_path3.join)(source, "collector", "outbox");
2461
+ if ((0, import_node_fs3.existsSync)(outbox)) copyAdoptedTree(outbox, (0, import_node_path3.join)(paths.stateDir, "collector", "outbox"), copy.record.files);
2462
+ }
2463
+ const enrollment = (0, import_node_path3.join)(source, "hub", "enrollment.json");
2464
+ if (role !== "collector" && (0, import_node_fs3.existsSync)(enrollment)) {
2465
+ const target = hubEnrollmentPath(paths);
2466
+ (0, import_node_fs3.copyFileSync)(enrollment, target);
2467
+ (0, import_node_fs3.chmodSync)(target, 384);
2468
+ copy.record.files.push({ source: enrollment, target, sha256: sha256File(target), bytes: (0, import_node_fs3.statSync)(target).size });
2469
+ }
2470
+ }
2471
+ function writeSysusersFiles(paths, accounts, role, power, host) {
2472
+ ensureDirectory(paths.sysusersDir, 493);
2473
+ writeManagedFile(sysusersPath(paths), renderSysusers(paths, accounts, role), 420, true);
2474
+ if (power) {
2475
+ writeManagedFile(powerSysusersPath(paths), renderPowerSysusers(paths, accounts), 420, true);
2476
+ }
2477
+ runRequired(host, "systemd-sysusers", [], "system account creation");
2478
+ assertSystemAccounts(host, accounts, power, [...COMPONENTS]);
2479
+ }
2480
+ function assertSystemAccounts(host, accounts, power, required = COMPONENTS) {
2481
+ const checked = required.map((component) => component === "hub" ? accounts.hub : accounts.collector);
2482
+ for (const account of checked) {
2483
+ const fields = runRequired(host, "getent", ["passwd", account], "service account inspection").stdout.trim().split(":");
2484
+ if (fields[0] !== account || !(Number(fields[2]) > 0) || !(Number(fields[3]) > 0) || !/\/(nologin|false)$/.test(fields[6] ?? "")) {
2485
+ throw new SystemError("unsafe-account", `service account ${account} must be non-root with a nologin shell`);
2486
+ }
2487
+ }
2488
+ if (power) {
2489
+ const fields = runRequired(host, "getent", ["group", accounts.powerGroup], "power group inspection").stdout.trim().split(":");
2490
+ const members = (fields[3] ?? "").split(",").filter(Boolean);
2491
+ if (!(Number(fields[2]) > 0) || members.length !== 1 || members[0] !== accounts.collector) {
2492
+ throw new SystemError("unsafe-group", "RAPL group must contain only the dedicated collector");
2493
+ }
2494
+ const passwd = runRequired(host, "getent", ["passwd"], "power group primary membership inspection").stdout;
2495
+ if (passwd.split("\n").some((line) => {
2496
+ const p = line.split(":");
2497
+ return p[3] === fields[2] && p[0] !== accounts.collector;
2498
+ })) {
2499
+ throw new SystemError("unsafe-group", "another account uses the RAPL group as its primary group");
2500
+ }
2501
+ }
2502
+ }
2503
+ function writePowerRule(paths, accounts) {
2504
+ ensureDirectory(paths.udevRulesDir, 493);
2505
+ writeManagedFile(raplRulePath(paths), renderRaplRule(accounts), 420, true);
2506
+ }
2507
+ function powerOriginalPath(paths) {
2508
+ return (0, import_node_path3.join)(paths.stateDir, POWER_STATE_FILE_NAME);
2509
+ }
2510
+ function capturePowerOriginals(paths) {
2511
+ const files = {};
2512
+ for (const domain of packageDomainPaths(paths.powercapRoot)) {
2513
+ for (const name of ["energy_uj", "max_energy_range_uj"]) {
2514
+ const path = (0, import_node_path3.join)(domain, name);
2515
+ try {
2516
+ const stat = (0, import_node_fs3.statSync)(path);
2517
+ files[path] = { mode: stat.mode & 511, uid: stat.uid, gid: stat.gid };
2518
+ } catch {
2519
+ }
2520
+ }
2521
+ }
2522
+ atomicWrite(powerOriginalPath(paths), `${JSON.stringify({ files }, null, 2)}
2523
+ `, 384);
2524
+ }
2525
+ function applyPowerAccess(paths, accounts, host) {
2526
+ for (const domain of packageDomainPaths(paths.powercapRoot)) {
2527
+ for (const name of ["energy_uj", "max_energy_range_uj"]) {
2528
+ const path = (0, import_node_path3.join)(domain, name);
2529
+ if (!(0, import_node_fs3.existsSync)(path)) continue;
2530
+ runRequired(host, "chgrp", [accounts.powerGroup, path], `power counter group ${path}`);
2531
+ runRequired(host, "chmod", ["0440", path], `power counter mode ${path}`);
2532
+ }
2533
+ }
2534
+ }
2535
+ function resumePowerRule(paths, host) {
2536
+ runRequired(host, "udevadm", ["control", "--reload-rules"], "udev rule reload");
2537
+ runRequired(host, "udevadm", ["trigger", "--subsystem-match=powercap", "--action=change"], "powercap rule application");
2538
+ runRequired(host, "udevadm", ["settle", "--timeout=10"], "powercap event completion");
2539
+ }
2540
+ function packageDomainPaths(root) {
2541
+ try {
2542
+ return (0, import_node_fs3.readdirSync)(root).filter((entry) => /^intel-rapl:\d+$/.test(entry)).map((entry) => (0, import_node_path3.join)(root, entry)).filter((path) => {
2543
+ try {
2544
+ return /^package/.test((0, import_node_fs3.readFileSync)((0, import_node_path3.join)(path, "name"), "utf8").trim());
2545
+ } catch {
2546
+ return false;
2547
+ }
2548
+ }).sort();
2549
+ } catch {
2550
+ return [];
2551
+ }
2552
+ }
2553
+ function restorePowerOriginals(paths, host) {
2554
+ const restored = [];
2555
+ let originals = null;
2556
+ try {
2557
+ originals = readJsonObject(powerOriginalPath(paths));
2558
+ } catch {
2559
+ originals = null;
2560
+ }
2561
+ const files = { ...originals?.files ?? {} };
2562
+ for (const domain of packageDomainPaths(paths.powercapRoot)) {
2563
+ for (const name of ["energy_uj", "max_energy_range_uj"]) {
2564
+ const path = (0, import_node_path3.join)(domain, name);
2565
+ if (!(0, import_node_fs3.existsSync)(path)) continue;
2566
+ const original = files[path];
2567
+ if (original) {
2568
+ runRequired(host, "chown", [`${original.uid}:${original.gid}`, path], "restore counter ownership");
2569
+ runRequired(host, "chmod", [modeString(original.mode).slice(1), path], "restore counter mode");
2570
+ } else if (name === "energy_uj") {
2571
+ runRequired(host, "chown", ["root:root", path], "restore counter ownership");
2572
+ runRequired(host, "chmod", ["0400", path], "restore counter mode");
2573
+ } else {
2574
+ runRequired(host, "chown", ["root:root", path], "restore counter ownership");
2575
+ runRequired(host, "chmod", ["0444", path], "restore counter mode");
2576
+ }
2577
+ restored.push(path);
2578
+ }
2579
+ }
2580
+ try {
2581
+ (0, import_node_fs3.unlinkSync)(powerOriginalPath(paths));
2582
+ } catch {
2583
+ }
2584
+ return restored;
2585
+ }
2586
+ function systemdActive(host, unit) {
2587
+ return host.run("systemctl", ["is-active", "--quiet", unit], { timeoutMs: 1e3 }).status === 0;
2588
+ }
2589
+ function systemdEnabled(host, unit) {
2590
+ return host.run("systemctl", ["is-enabled", "--quiet", unit]).status === 0;
2591
+ }
2592
+ function stopUnits(host, units) {
2593
+ for (const unit of units) {
2594
+ const stopped = host.run("systemctl", ["stop", unit]);
2595
+ if (stopped.status !== 0) {
2596
+ const load = host.run("systemctl", ["show", unit, "--property=LoadState", "--value"]).stdout.trim();
2597
+ if (!["not-found", "bad-setting", "error"].includes(load) || systemdActive(host, unit)) {
2598
+ throw new SystemError("stop-failed", `could not stop ${unit}; no runtime files may be restored`);
2599
+ }
2600
+ }
2601
+ }
2602
+ if (units.some((unit) => systemdActive(host, unit))) throw new SystemError("service-active", "system services did not stop; no files were restored");
2603
+ }
2604
+ function startUnits(host, units) {
2605
+ runRequired(host, "systemctl", ["daemon-reload"], "systemd daemon-reload");
2606
+ for (const unit of units) runRequired(host, "systemctl", ["start", unit], `starting ${unit}`);
2607
+ }
2608
+ function enableUnits(host, units) {
2609
+ runRequired(host, "systemctl", ["daemon-reload"], "systemd daemon-reload");
2610
+ for (const unit of units) runRequired(host, "systemctl", ["enable", unit], `enabling ${unit}`);
2611
+ }
2612
+ async function waitForUnits(host, units, timeoutMs) {
2613
+ if (timeoutMs <= 0) return units.every((unit) => systemdActive(host, unit));
2614
+ const deadline = Date.now() + timeoutMs;
2615
+ while (Date.now() < deadline) {
2616
+ if (units.every((unit) => systemdActive(host, unit))) return true;
2617
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 250));
2618
+ }
2619
+ return units.every((unit) => systemdActive(host, unit));
2620
+ }
2621
+ function readHubEnvironment(paths) {
2622
+ return readEnvDocument(hubEnvPath(paths))?.values ?? {};
2623
+ }
2624
+ async function verifySystemHealth(paths, state, host, timeoutMs, components = enabledComponents(state)) {
2625
+ if (host.verifyHealth) return host.verifyHealth(paths, state, timeoutMs);
2626
+ const selected = componentSet(components);
2627
+ const selectedUnits = COMPONENTS.filter((component) => selected[component]).map(unitFor);
2628
+ if (selectedUnits.length === 0) return;
2629
+ const deadline = Date.now() + timeoutMs;
2630
+ if (!await waitForUnits(host, selectedUnits, timeoutMs)) throw new SystemError("startup-failed", "system units did not become active");
2631
+ if (selected.hub) {
2632
+ const env = readHubEnvironment(paths);
2633
+ const target = env.HUB_PUBLIC_ORIGIN ? { kind: "unix", socketPath: hubSocketPath(paths), hostHeader: new URL(env.HUB_PUBLIC_ORIGIN).host, path: "/api/health" } : { kind: "loopback", host: "127.0.0.1", port: Number(env.HUB_PORT ?? 8081), path: "/api/health" };
2634
+ const health = await waitForHealth(target, { expectedVersion: state.currentVersion, deadlineMs: Math.max(100, deadline - Date.now()) });
2635
+ if (!health.ok) throw new SystemError("startup-failed", `system hub readiness failed: ${health.last.error}`);
2636
+ const report = runRequired(
2637
+ host,
2638
+ state.nodePath,
2639
+ [(0, import_node_path3.join)(paths.prefix, "current/dist/ops.cjs"), "inspect", "--database", hubDatabasePath(paths), "--json"],
2640
+ "activated database verification",
2641
+ { timeoutMs: Math.max(100, deadline - Date.now()) }
2642
+ );
2643
+ const info = JSON.parse(report.stdout);
2644
+ if (!info.integrityOk || info.newerSchema || info.ledgerProblem || info.schemaVersion !== info.supportedSchemaVersion) {
2645
+ throw new SystemError("database-unready", "activated hub database schema/integrity is not ready");
2646
+ }
2647
+ if (!info.tables?.some((table) => table.name === "users" && Number(table.rows) > 0)) {
2648
+ throw new SystemError("users-unconfigured", "hub has no managed browser users; configure its private bootstrap users before first start");
2649
+ }
2650
+ }
2651
+ if (selected.collector) {
2652
+ let problem = "no sampling marker";
2653
+ while (Date.now() < deadline) {
2654
+ try {
2655
+ const marker = readJsonObject((0, import_node_path3.join)(paths.stateDir, "collector/health.json"));
2656
+ const lease = readJsonObject((0, import_node_path3.join)(paths.stateDir, "collector/collector-runtime.lock"));
2657
+ if (marker.pid !== lease.pid || !Number.isSafeInteger(marker.pid)) throw new Error("sampling marker belongs to another process");
2658
+ process.kill(Number(marker.pid), 0);
2659
+ if (marker.version !== state.currentVersion || !Number.isFinite(marker.sampledAt) || Date.now() - Number(marker.sampledAt) > 15e3) throw new Error("collector version or sampling freshness is wrong");
2660
+ return;
2661
+ } catch (error) {
2662
+ problem = error instanceof Error ? error.message : String(error);
2663
+ }
2664
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
2665
+ }
2666
+ throw new SystemError("collector-unready", `system collector did not sample: ${problem}`);
2667
+ }
2668
+ }
2669
+ function chownAccount(paths, owner, group, host) {
2670
+ for (const path of paths) {
2671
+ if (!(0, import_node_fs3.existsSync)(path)) continue;
2672
+ runRequired(host, "chown", ["-R", `${owner}:${group}`, path], `ownership ${path}`);
2673
+ }
2674
+ }
2675
+ function chmodTree(path, host) {
2676
+ if (!(0, import_node_fs3.existsSync)(path)) return;
2677
+ runRequired(host, "chmod", ["-R", "u=rwX,go=rX", path], `release permissions ${path}`);
2678
+ }
2679
+ function pickRollbackBackup(state, previousSchema) {
2680
+ const backups = Object.values(state.backups).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2681
+ if (previousSchema === null) return null;
2682
+ return [...backups].reverse().find((backup) => backup.fromVersion === state.previousVersion && backup.toVersion === state.currentVersion && backup.schemaVersion !== null && backup.schemaVersion <= previousSchema) ?? null;
2683
+ }
2684
+ async function recoverFailedUpgrade(options) {
2685
+ stopUnits(options.host, options.units);
2686
+ assertDirectoryStopped(options.paths.stateDir);
2687
+ replaceSymlink((0, import_node_path3.join)(options.paths.prefix, "current"), options.oldCurrent, options.host);
2688
+ if (options.oldPrevious === null) removeSymlink((0, import_node_path3.join)(options.paths.prefix, "previous"));
2689
+ else replaceSymlink((0, import_node_path3.join)(options.paths.prefix, "previous"), options.oldPrevious, options.host);
2690
+ const database = hubDatabasePath(options.paths);
2691
+ let afterSchema = null;
2692
+ try {
2693
+ afterSchema = databaseSchema(database);
2694
+ } catch {
2695
+ afterSchema = null;
2696
+ }
2697
+ if (options.backupPath !== null && (0, import_node_fs3.existsSync)(options.backupPath) && (afterSchema === null || options.beforeSchema === null || afterSchema > options.beforeSchema)) {
2698
+ await backupDatabase(database, (0, import_node_path3.join)(options.paths.stateDir, "backups", `failed-upgrade-${newestTimestamp()}.sqlite`), options.host);
2699
+ replaceDatabaseFromBackup(options.backupPath, database, null, options.host);
2700
+ runRequired(options.host, "chown", [`${options.state.accounts.hub}:${options.state.accounts.hub}`, database], "restored database ownership");
2701
+ }
2702
+ options.state.previousVersion = options.oldPrevious === null ? null : (0, import_node_path3.basename)(options.oldPrevious);
2703
+ options.state.currentVersion = (0, import_node_path3.basename)(options.oldCurrent);
2704
+ writeSystemState(options.paths, options.state);
2705
+ if (options.activeComponents.length > 0) {
2706
+ startUnits(options.host, options.units);
2707
+ await verifySystemHealth(options.paths, options.state, options.host, 2e4, options.activeComponents);
2708
+ }
2709
+ }
2710
+ function assertManagedFileOwnership(paths, path) {
2711
+ if (!isDefaultSystemLayout(paths)) return;
2712
+ if (!(0, import_node_fs3.existsSync)(path)) return;
2713
+ const stat = (0, import_node_fs3.lstatSync)(path);
2714
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new SystemError("unsafe-path", `${path} must be a regular managed file`);
2715
+ if (stat.uid !== 0) throw new SystemError("insecure-ownership", `${path} must be root-owned before replacement`);
2716
+ if ((stat.mode & 18) !== 0) throw new SystemError("insecure-permissions", `${path} must not be group/world-writable`);
2717
+ }
2718
+ function writeUnitFile(paths, unit, content, replace) {
2719
+ const path = (0, import_node_path3.join)(paths.unitsDir, unit);
2720
+ assertManagedFileOwnership(paths, path);
2721
+ writeManagedFile(path, content, 420, replace);
2722
+ }
2723
+ function currentReleaseOrThrow(paths) {
2724
+ const current = currentReleaseDir(paths);
2725
+ if (current === null) throw new SystemError("invalid-state", "the active release is missing; reinstall or upgrade first");
2726
+ return current;
2727
+ }
2728
+ function ensureComponentSetup(options) {
2729
+ const { paths, accounts, nodePath, component, power, host } = options;
2730
+ ensureDirectory(paths.prefix, 493);
2731
+ ensureDirectory(paths.configDir, 493);
2732
+ ensureDirectory(paths.stateDir, 493);
2733
+ ensureDirectory(paths.unitsDir, 493);
2734
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "hub"), 448, true);
2735
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "hub", "run"), 448, true);
2736
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "collector"), 448, true);
2737
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "backups"), 448, true);
2738
+ const release = currentReleaseOrThrow(paths);
2739
+ void release;
2740
+ writeSysusersFiles(paths, accounts, "local", power, host);
2741
+ if (component === "hub") {
2742
+ if (!(0, import_node_fs3.existsSync)(hubEnvPath(paths))) {
2743
+ const env = hubEnvTemplate(paths);
2744
+ writeManagedFile(hubEnvPath(paths), env, 384, false);
2745
+ }
2746
+ if (!(0, import_node_fs3.existsSync)((0, import_node_path3.join)(paths.unitsDir, UNIT_NAMES.hub))) {
2747
+ const env = readHubEnvironment(paths);
2748
+ const remote = env.HUB_PUBLIC_ORIGIN !== void 0 && env.HUB_PUBLIC_ORIGIN !== "";
2749
+ const unit = renderSystemHubUnit({
2750
+ paths,
2751
+ accounts,
2752
+ nodePath,
2753
+ database: hubDatabasePath(paths),
2754
+ enrollment: hubEnrollmentPath(paths),
2755
+ socket: hubSocketPath(paths),
2756
+ remote,
2757
+ powerAccess: false
2758
+ });
2759
+ writeUnitFile(paths, UNIT_NAMES.hub, `${unit}
2760
+ `, false);
2761
+ }
2762
+ } else {
2763
+ if (!(0, import_node_fs3.existsSync)(collectorEnvPath(paths))) {
2764
+ writeManagedFile(collectorEnvPath(paths), collectorEnvTemplate(), 384, false);
2765
+ }
2766
+ const unit = renderSystemCollectorUnit({
2767
+ paths,
2768
+ accounts,
2769
+ nodePath,
2770
+ collectorState: collectorStatePath(paths),
2771
+ powerAccess: power
2772
+ });
2773
+ writeUnitFile(paths, UNIT_NAMES.collector, `${unit}
2774
+ `, true);
2775
+ }
2776
+ const validation = runRequired(
2777
+ host,
2778
+ "systemd-analyze",
2779
+ ["verify", "--man=no", (0, import_node_path3.join)(paths.unitsDir, unitFor(component))],
2780
+ "native systemd unit validation"
2781
+ );
2782
+ if (/path is not absolute|bad unit file|fatal error|unknown (?:key|lvalue)|ignoring/i.test(validation.stderr)) {
2783
+ throw new SystemError("invalid-unit", "native systemd validation rejected a generated directive");
2784
+ }
2785
+ if (component === "hub") chownAccount([(0, import_node_path3.join)(paths.stateDir, "hub")], accounts.hub, accounts.hub, host);
2786
+ if (component === "collector") chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
2787
+ }
2788
+ function ensurePowerAccess(paths, accounts, host) {
2789
+ if (!(0, import_node_fs3.existsSync)(powerOriginalPath(paths))) capturePowerOriginals(paths);
2790
+ writePowerRule(paths, accounts);
2791
+ resumePowerRule(paths, host);
2792
+ applyPowerAccess(paths, accounts, host);
2793
+ }
2794
+ function planSystemInstall(options) {
2795
+ const paths = validateSystemPaths(options.paths);
2796
+ const accounts = validateAccounts(options.accounts);
2797
+ const role = resolveRole(options);
2798
+ const preset = componentSet(componentsForRole(role));
2799
+ const power = options.power !== false && preset.collector;
2800
+ const initial = options.enable === true ? preset : { hub: false, collector: false };
2801
+ const files = [
2802
+ { path: (0, import_node_path3.join)(paths.prefix, "current"), mode: "symlink", purpose: "active root-owned release" },
2803
+ { path: hubEnvPath(paths), mode: "0600", purpose: "root-owned hub configuration" },
2804
+ { path: collectorEnvPath(paths), mode: "0600", purpose: "root-owned collector configuration" },
2805
+ { path: sysusersPath(paths), mode: "0644", purpose: "dedicated nologin accounts" },
2806
+ ...COMPONENTS.map((component) => ({ path: (0, import_node_path3.join)(paths.unitsDir, unitFor(component)), mode: "0644", purpose: `${component} boot unit` }))
2807
+ ];
2808
+ if (power) {
2809
+ files.push({ path: raplRulePath(paths), mode: "0644", purpose: "package RAPL read rule" });
2810
+ files.push({ path: powerSysusersPath(paths), mode: "0644", purpose: "collector-only power group membership" });
2811
+ }
2812
+ const commands = [
2813
+ "systemd-sysusers",
2814
+ "systemctl daemon-reload",
2815
+ ...power ? ["udevadm control --reload-rules", "udevadm trigger --subsystem-match=powercap --action=change"] : [],
2816
+ ...options.enable ? unitNamesForComponents(initial).map((unit) => `systemctl enable ${unit}`) : [],
2817
+ ...options.start ? unitNamesForComponents(initial).map((unit) => `systemctl start ${unit}`) : []
2818
+ ];
2819
+ const notes = [
2820
+ "Nothing is changed without --apply and root.",
2821
+ options.adoptFrom ? `Adopts ${(0, import_node_path3.resolve)(options.adoptFrom)} by copy only; the original user setup is preserved.` : "Existing user services are not modified.",
2822
+ "One complete package: both runtimes, units and accounts are installed; --role only sets the initial enabled preset.",
2823
+ "Hub and collector use separate unprivileged accounts; Node gets no capabilities.",
2824
+ ...options.enable && preset.hub ? ["Enabling the hub creates its owner account once and prints the password once."] : [],
2825
+ ...options.enable && preset.collector ? ["A collector with no destination pairs with a configured colocated hub, otherwise it waits for `system connect`."] : []
2826
+ ];
2827
+ return { operation: "install", apply: options.apply === true, role, components: initial, paths, accounts, commands, files, notes };
2828
+ }
2829
+ async function installSystem(options) {
2830
+ const host = options.host ?? createSystemHost();
2831
+ const now = options.now ?? Date.now;
2832
+ const paths = validateSystemPaths(options.paths);
2833
+ const accounts = validateAccounts(options.accounts);
2834
+ const role = resolveRole(options);
2835
+ const plan = planSystemInstall({ ...options, paths, accounts, role });
2836
+ if (options.apply !== true) return { operation: "install", ok: true, apply: false, message: "plan only", plan };
2837
+ assertApply(host);
2838
+ const existing = readSystemState(paths);
2839
+ if (existing?.active === true) {
2840
+ throw new SystemError("already-installed", "a system installation already exists; use `rigscope system upgrade` or uninstall first");
2841
+ }
2842
+ const conflicts = detectUserInstallations({ homeRoots: options.homeRoots });
2843
+ if (conflicts.length > 0 && options.adoptFrom === void 0 && options.allowUserConflicts !== true) {
2844
+ const detail = conflicts.map((item) => `${item.user}:${item.unitName}`).join(", ");
2845
+ throw new SystemError(
2846
+ "user-installation-present",
2847
+ `existing user service(s) detected (${detail}); stop them and use --adopt-user-state for explicit copy-based adoption`
2848
+ );
2849
+ }
2850
+ assertSystemStartAllowed(paths, options.start === true ? componentsForRole(role) : { hub: false, collector: false });
2851
+ const source = (0, import_node_path3.resolve)(options.source);
2852
+ if (!(0, import_node_fs3.existsSync)(source)) throw new SystemError("missing-source", `release source does not exist: ${source}`);
2853
+ ensureDirectory(paths.prefix, 493);
2854
+ ensureDirectory((0, import_node_path3.join)(paths.prefix, "releases"), 493);
2855
+ ensureDirectory(paths.stateDir, 493);
2856
+ ensureDirectory(paths.configDir, 493);
2857
+ ensureDirectory(paths.unitsDir, 493);
2858
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "hub"), 448, true);
2859
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "collector"), 448, true);
2860
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "backups"), 448, true);
2861
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "hub", "run"), 448, true);
2862
+ assertManagedPathSafety(paths, isDefaultSystemLayout(paths));
2863
+ const nodePath = ensureRootOwnedNode({ paths, host, nodePath: options.nodePath, copyNode: options.copyNode, trustedNode: options.trustedNode, apply: true });
2864
+ const stage = stageRelease({ source, paths, host, replaceRelease: options.replaceRelease, now });
2865
+ if (options.installDeps === true) {
2866
+ installDependencies(stage.releaseDir, host, options.installDepsCommand ?? "npm");
2867
+ }
2868
+ const missing = missingDependencies(stage.releaseDir);
2869
+ if (missing.length > 0) {
2870
+ throw new SystemError(
2871
+ "missing-dependencies",
2872
+ `production dependencies are missing (${missing.join(", ")}); stage with --install-deps or use a built release directory`
2873
+ );
2874
+ }
2875
+ chmodTree(stage.releaseDir, host);
2876
+ runRequired(host, "chown", ["-R", "root:root", stage.releaseDir], "release ownership");
2877
+ let adoption = null;
2878
+ if (options.adoptFrom) {
2879
+ adoption = adoptUserState({ source: options.adoptFrom, paths, role, accounts, apply: true, host, now });
2880
+ await performAdoptionCopyAsync({ source: options.adoptFrom, paths, role, host, copy: adoption });
2881
+ }
2882
+ const port = options.port ?? 8081;
2883
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new SystemError("invalid-argument", "--port must be 1-65535");
2884
+ const preset = componentSet(componentsForRole(role));
2885
+ const power = options.power !== false && preset.collector;
2886
+ const hubEnv = adoption?.hubEnv ?? hubEnvTemplate(paths, options.origin, port);
2887
+ const collectorEnv = adoption?.collectorEnv ?? collectorEnvTemplate();
2888
+ writeManagedFile(hubEnvPath(paths), hubEnv, 384, false);
2889
+ writeManagedFile(collectorEnvPath(paths), collectorEnv, 384, false);
2890
+ writeSysusersFiles(paths, accounts, "local", power, host);
2891
+ const hubUnit = renderSystemHubUnit({
2892
+ paths,
2893
+ accounts,
2894
+ nodePath,
2895
+ database: hubDatabasePath(paths),
2896
+ enrollment: hubEnrollmentPath(paths),
2897
+ socket: hubSocketPath(paths),
2898
+ remote: adoption?.remote === true || typeof options.origin === "string",
2899
+ powerAccess: false
2900
+ });
2901
+ const collectorUnit = renderSystemCollectorUnit({
2902
+ paths,
2903
+ accounts,
2904
+ nodePath,
2905
+ collectorState: collectorStatePath(paths),
2906
+ powerAccess: power
2907
+ });
2908
+ writeUnitFile(paths, UNIT_NAMES.hub, `${hubUnit}
2909
+ `, true);
2910
+ writeUnitFile(paths, UNIT_NAMES.collector, `${collectorUnit}
2911
+ `, true);
2912
+ const validation = runRequired(host, "systemd-analyze", ["verify", "--man=no", ...COMPONENTS.map((component) => (0, import_node_path3.join)(paths.unitsDir, unitFor(component)))], "native systemd unit validation");
2913
+ if (/path is not absolute|bad unit file|fatal error|unknown (?:key|lvalue)|ignoring/i.test(validation.stderr)) {
2914
+ throw new SystemError("invalid-unit", "native systemd validation rejected a generated directive");
2915
+ }
2916
+ if (power) ensurePowerAccess(paths, accounts, host);
2917
+ chownAccount([(0, import_node_path3.join)(paths.stateDir, "hub")], accounts.hub, accounts.hub, host);
2918
+ chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
2919
+ const state = emptyState({ paths, accounts, role, components: { hub: false, collector: false }, nodePath, now });
2920
+ state.currentVersion = stage.version;
2921
+ state.releases[stage.version] = {
2922
+ releaseDir: stage.releaseDir,
2923
+ schemaVersion: stage.schemaVersion,
2924
+ installedAt: new Date(now()).toISOString()
2925
+ };
2926
+ state.adoption = adoption?.record ?? null;
2927
+ state.power = power ? { enabled: true, rulePath: raplRulePath(paths), group: accounts.powerGroup } : null;
2928
+ replaceSymlink((0, import_node_path3.join)(paths.prefix, "current"), stage.releaseDir, host);
2929
+ writeSystemState(paths, state);
2930
+ const wantsHub = preset.hub && (options.enable === true || options.start === true);
2931
+ const wantsCollector = preset.collector && (options.enable === true || options.start === true);
2932
+ let password;
2933
+ const awaiting = [];
2934
+ if (wantsHub) password = ensureHubOwner(paths);
2935
+ if (wantsCollector && !collectorConfigured(paths).configured) {
2936
+ const pairing = await pairCollectorWithHub({ paths, accounts, host });
2937
+ if (pairing.awaiting) awaiting.push("collector");
2938
+ }
2939
+ if (options.enable === true) {
2940
+ enableUnits(host, unitNamesForComponents(preset));
2941
+ state.components = { hub: { enabled: preset.hub }, collector: { enabled: preset.collector } };
2942
+ writeSystemState(paths, state);
2943
+ }
2944
+ const started = [];
2945
+ if (options.start === true) {
2946
+ assertSystemStartAllowed(paths, componentSet(preset));
2947
+ if (preset.hub) started.push("hub");
2948
+ if (preset.collector && !awaiting.includes("collector")) started.push("collector");
2949
+ if (started.length > 0) {
2950
+ startUnits(host, started.map(unitFor));
2951
+ await verifySystemHealth(paths, state, host, options.startupTimeoutMs ?? 2e4, started);
2952
+ }
2953
+ }
2954
+ return {
2955
+ operation: "install",
2956
+ ok: true,
2957
+ apply: true,
2958
+ message: `installed ${stage.version} for ${role} preset`,
2959
+ version: stage.version,
2960
+ current: stage.releaseDir,
2961
+ previous: null,
2962
+ state,
2963
+ password,
2964
+ ...awaiting.length > 0 ? { awaitingConnection: awaiting } : {},
2965
+ warnings: adoption?.warnings ?? []
2966
+ };
2967
+ }
2968
+ function systemStatus(options = {}) {
2969
+ const host = options.host ?? createSystemHost();
2970
+ const paths = validateSystemPaths(options.paths);
2971
+ const accounts = validateAccounts(options.accounts);
2972
+ let state = null;
2973
+ let stateError = null;
2974
+ try {
2975
+ state = readSystemState(paths);
2976
+ } catch (error) {
2977
+ stateError = error instanceof Error ? error.message : String(error);
2978
+ }
2979
+ const presentUnits = [UNIT_NAMES.hub, UNIT_NAMES.collector].filter((unit) => (0, import_node_fs3.existsSync)((0, import_node_path3.join)(paths.unitsDir, unit)));
2980
+ const publicRole = presentUnits.length === 2 ? "local" : presentUnits[0] === UNIT_NAMES.hub ? "hub" : presentUnits[0] === UNIT_NAMES.collector ? "collector" : null;
2981
+ const role = state?.role ?? publicRole ?? "local";
2982
+ const current = currentReleaseDir(paths);
2983
+ let publicVersion = null;
2984
+ if (current) {
2985
+ try {
2986
+ const manifest = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.join)(current, "package.json"), "utf8"));
2987
+ if (typeof manifest.version === "string") publicVersion = manifest.version;
2988
+ } catch {
2989
+ }
2990
+ }
2991
+ const selected = options.component ? [options.component] : [...COMPONENTS];
2992
+ const hubOwner = hubOwnerState(paths);
2993
+ const collector = collectorConfigured(paths);
2994
+ const componentReports = {};
2995
+ for (const component of COMPONENTS) {
2996
+ const unit = unitFor(component);
2997
+ const present = (0, import_node_fs3.existsSync)((0, import_node_path3.join)(paths.unitsDir, unit));
2998
+ const active = present && systemdActive(host, unit);
2999
+ const unitEnabled = present && systemdEnabled(host, unit);
3000
+ const enabled = state !== null ? state.components[component].enabled : unitEnabled;
3001
+ const configured = component === "hub" ? hubOwner.configured : collector.configured;
3002
+ const readiness = !present ? "missing" : component === "collector" && collector.problem ? "invalid-state" : component === "hub" && hubOwner.problem ? "invalid-config" : !configured ? component === "collector" ? "awaiting-connection" : "unconfigured" : active ? "ready" : "stopped";
3003
+ componentReports[component] = {
3004
+ component,
3005
+ unit,
3006
+ path: (0, import_node_path3.join)(paths.unitsDir, unit),
3007
+ present,
3008
+ enabled,
3009
+ unitEnabled,
3010
+ active,
3011
+ configured,
3012
+ readiness,
3013
+ ...component === "collector" ? { destinations: collector.destinations } : {},
3014
+ ...component === "hub" ? { ownerConfigured: hubOwner.configured } : {}
3015
+ };
3016
+ }
3017
+ const units = selected.map((component) => componentReports[component]);
3018
+ const lock = systemLockState(paths);
3019
+ const database = hubDatabasePath(paths);
3020
+ let schema = null;
3021
+ try {
3022
+ schema = databaseSchema(database);
3023
+ } catch {
3024
+ schema = null;
3025
+ }
3026
+ const power = inspectPowerAccess({ paths, accounts });
3027
+ return {
3028
+ scope: SYSTEM_SCOPE,
3029
+ installed: state?.active ?? (current !== null && publicRole !== null),
3030
+ stateError,
3031
+ role: state?.role ?? publicRole,
3032
+ currentVersion: state?.currentVersion ?? publicVersion,
3033
+ previousVersion: state?.previousVersion ?? null,
3034
+ current,
3035
+ previous: previousReleaseDir(paths),
3036
+ paths,
3037
+ accounts,
3038
+ nodePath: state?.nodePath ?? null,
3039
+ ...options.component ? { selected: options.component } : {},
3040
+ components: componentReports,
3041
+ units: units.map((unit) => ({
3042
+ unit: unit.unit,
3043
+ path: unit.path,
3044
+ present: unit.present,
3045
+ active: unit.active,
3046
+ enabled: unit.enabled
3047
+ })),
3048
+ database: {
3049
+ path: database,
3050
+ exists: (0, import_node_fs3.existsSync)(database),
3051
+ schema,
3052
+ mode: (0, import_node_fs3.existsSync)(database) ? modeString((0, import_node_fs3.statSync)(database).mode) : null,
3053
+ lock
3054
+ },
3055
+ socket: { path: hubSocketPath(paths), exists: (0, import_node_fs3.existsSync)(hubSocketPath(paths)) },
3056
+ config: {
3057
+ dir: paths.configDir,
3058
+ hubEnv: { path: hubEnvPath(paths), present: (0, import_node_fs3.existsSync)(hubEnvPath(paths)) },
3059
+ collectorEnv: { path: collectorEnvPath(paths), present: (0, import_node_fs3.existsSync)(collectorEnvPath(paths)) }
3060
+ },
3061
+ adoption: state?.adoption ?? null,
3062
+ power,
3063
+ userInstallations: detectUserInstallations({ homeRoots: options.homeRoots })
3064
+ };
3065
+ }
3066
+ function selectedComponents(state, component) {
3067
+ if (component !== void 0) return [component];
3068
+ const enabled = enabledComponents(state);
3069
+ if (enabled.length === 0) {
3070
+ throw new SystemError("no-enabled-components", "no system components are enabled; run `rigscope system enable hub|collector` first");
3071
+ }
3072
+ return enabled;
3073
+ }
3074
+ function assertComponentConfigured(paths, components) {
3075
+ if (components.includes("hub")) {
3076
+ const owner = hubOwnerState(paths);
3077
+ if (!owner.configured) {
3078
+ throw new SystemError("hub-unconfigured", "the hub has no owner; run `rigscope system enable hub --apply` to create it once");
3079
+ }
3080
+ }
3081
+ if (components.includes("collector")) {
3082
+ const collector = collectorConfigured(paths);
3083
+ if (!collector.configured) {
3084
+ throw new SystemError(
3085
+ "collector-awaiting-connection",
3086
+ "the collector has no destination; run `rigscope system connect https://<hub> --token-file /private/grant --apply`"
3087
+ );
3088
+ }
3089
+ }
3090
+ }
3091
+ async function startSystem(options = {}) {
3092
+ const host = options.host ?? createSystemHost();
3093
+ const paths = validateSystemPaths(options.paths);
3094
+ const state = readSystemState(paths);
3095
+ if (state === null || !state.active) throw new SystemError("not-installed", "no active system installation; run install first");
3096
+ const components = selectedComponents(state, options.component);
3097
+ const units = components.map(unitFor);
3098
+ const plan = {
3099
+ operation: "start",
3100
+ apply: options.apply === true,
3101
+ role: state.role,
3102
+ ...options.component ? { component: options.component } : {},
3103
+ components: { hub: components.includes("hub"), collector: components.includes("collector") },
3104
+ paths,
3105
+ accounts: state.accounts,
3106
+ commands: ["systemctl daemon-reload", ...options.enable ? units.map((unit) => `systemctl enable ${unit}`) : [], ...units.map((unit) => `systemctl start ${unit}`)],
3107
+ files: [],
3108
+ notes: ["Starting refuses to create a second hub runtime for the same database."]
3109
+ };
3110
+ if (options.apply !== true) return { operation: "start", ok: true, apply: false, message: "plan only", plan };
3111
+ assertApply(host);
3112
+ assertComponentConfigured(paths, components);
3113
+ if (state.adoption) assertDirectoryStopped(state.adoption.source, componentSet(components));
3114
+ assertSystemStartAllowed(paths, componentSet(components));
3115
+ if (options.enable === true) {
3116
+ enableUnits(host, units);
3117
+ for (const component of components) state.components[component].enabled = true;
3118
+ }
3119
+ startUnits(host, units);
3120
+ await verifySystemHealth(paths, state, host, options.startupTimeoutMs ?? 2e4, components);
3121
+ writeSystemState(paths, state);
3122
+ return {
3123
+ operation: "start",
3124
+ ok: true,
3125
+ apply: true,
3126
+ message: components.length === 1 ? `${components[0]} started` : "both components started",
3127
+ ...options.component ? { component: options.component } : {},
3128
+ components: { hub: components.includes("hub"), collector: components.includes("collector") },
3129
+ state
3130
+ };
3131
+ }
3132
+ function stopSystem(options = {}) {
3133
+ const host = options.host ?? createSystemHost();
3134
+ const paths = validateSystemPaths(options.paths);
3135
+ const state = readSystemState(paths);
3136
+ if (state === null) throw new SystemError("not-installed", "no system installation state");
3137
+ const components = selectedComponents(state, options.component);
3138
+ const units = components.map(unitFor);
3139
+ const plan = {
3140
+ operation: "stop",
3141
+ apply: options.apply === true,
3142
+ role: state.role,
3143
+ ...options.component ? { component: options.component } : {},
3144
+ components: { hub: components.includes("hub"), collector: components.includes("collector") },
3145
+ paths,
3146
+ accounts: state.accounts,
3147
+ commands: units.map((unit) => `systemctl stop ${unit}`),
3148
+ files: [],
3149
+ notes: ["Stop is bounded by each unit's TimeoutStopSec."]
3150
+ };
3151
+ if (options.apply !== true) return { operation: "stop", ok: true, apply: false, message: "plan only", plan };
3152
+ assertApply(host);
3153
+ stopUnits(host, units);
3154
+ assertDirectoryStopped(paths.stateDir, componentSet(components));
3155
+ writeSystemState(paths, state);
3156
+ return {
3157
+ operation: "stop",
3158
+ ok: true,
3159
+ apply: true,
3160
+ message: components.length === 1 ? `${components[0]} stopped` : "both components stopped",
3161
+ ...options.component ? { component: options.component } : {},
3162
+ components: { hub: components.includes("hub"), collector: components.includes("collector") },
3163
+ state
3164
+ };
3165
+ }
3166
+ async function restartSystem(options = {}) {
3167
+ const host = options.host ?? createSystemHost();
3168
+ const paths = validateSystemPaths(options.paths);
3169
+ const state = readSystemState(paths);
3170
+ if (state === null || !state.active) throw new SystemError("not-installed", "no active system installation");
3171
+ const components = selectedComponents(state, options.component);
3172
+ assertComponentConfigured(paths, components);
3173
+ const units = components.map(unitFor);
3174
+ const plan = {
3175
+ operation: "restart",
3176
+ apply: options.apply === true,
3177
+ role: state.role,
3178
+ ...options.component ? { component: options.component } : {},
3179
+ components: { hub: components.includes("hub"), collector: components.includes("collector") },
3180
+ paths,
3181
+ accounts: state.accounts,
3182
+ commands: units.map((unit) => `systemctl restart ${unit}`),
3183
+ files: [],
3184
+ notes: ["Restart refuses to create a duplicate hub runtime."]
3185
+ };
3186
+ if (options.apply !== true) return { operation: "restart", ok: true, apply: false, message: "plan only", plan };
3187
+ assertApply(host);
3188
+ stopUnits(host, units);
3189
+ assertDirectoryStopped(paths.stateDir, componentSet(components));
3190
+ assertSystemStartAllowed(paths, componentSet(components));
3191
+ startUnits(host, units);
3192
+ await verifySystemHealth(paths, state, host, options.startupTimeoutMs ?? 2e4, components);
3193
+ writeSystemState(paths, state);
3194
+ return {
3195
+ operation: "restart",
3196
+ ok: true,
3197
+ apply: true,
3198
+ message: components.length === 1 ? `${components[0]} restarted` : "both components restarted",
3199
+ ...options.component ? { component: options.component } : {},
3200
+ components: { hub: components.includes("hub"), collector: components.includes("collector") },
3201
+ state
3202
+ };
3203
+ }
3204
+ async function enableSystem(options) {
3205
+ const host = options.host ?? createSystemHost();
3206
+ const paths = validateSystemPaths(options.paths);
3207
+ const state = readSystemState(paths);
3208
+ if (state === null || !state.active) throw new SystemError("not-installed", "no active system installation; run install first");
3209
+ const component = options.component;
3210
+ const power = component === "collector" && options.power !== false;
3211
+ const plan = {
3212
+ operation: "enable",
3213
+ apply: options.apply === true,
3214
+ role: state.role,
3215
+ component,
3216
+ components: { hub: component === "hub" || state.components.hub.enabled, collector: component === "collector" || state.components.collector.enabled },
3217
+ paths,
3218
+ accounts: state.accounts,
3219
+ commands: [
3220
+ "systemd-sysusers",
3221
+ "systemctl daemon-reload",
3222
+ ...power ? ["udevadm control --reload-rules", "udevadm trigger --subsystem-match=powercap --action=change"] : [],
3223
+ `systemctl enable ${unitFor(component)}`,
3224
+ `systemctl start ${unitFor(component)}`
3225
+ ],
3226
+ files: [
3227
+ { path: component === "hub" ? hubEnvPath(paths) : collectorEnvPath(paths), mode: "0600", purpose: `${component} configuration` },
3228
+ { path: (0, import_node_path3.join)(paths.unitsDir, unitFor(component)), mode: "0644", purpose: `${component} boot unit` }
3229
+ ],
3230
+ notes: [
3231
+ "Enables only the requested component; the other component keeps running.",
3232
+ ...component === "hub" ? ["Creates the owner account once when the hub has no browser users; the password is printed once."] : [],
3233
+ ...component === "collector" ? ["Pairs with a configured colocated hub when no destination exists; otherwise reports Awaiting connection."] : []
3234
+ ]
3235
+ };
3236
+ if (options.apply !== true) return { operation: "enable", ok: true, apply: false, message: "plan only", plan };
3237
+ assertApply(host);
3238
+ ensureComponentSetup({ paths, accounts: state.accounts, nodePath: state.nodePath, component, power, host });
3239
+ if (power) {
3240
+ ensurePowerAccess(paths, state.accounts, host);
3241
+ state.power = { enabled: true, rulePath: raplRulePath(paths), group: state.accounts.powerGroup };
3242
+ }
3243
+ let password;
3244
+ const awaiting = [];
3245
+ if (component === "hub") {
3246
+ const hubWasActive = systemdActive(host, UNIT_NAMES.hub);
3247
+ password = ensureHubOwner(paths);
3248
+ if (password !== void 0 && hubWasActive) {
3249
+ stopUnits(host, [UNIT_NAMES.hub]);
3250
+ assertDirectoryStopped(paths.stateDir, "hub");
3251
+ }
3252
+ } else if (!collectorConfigured(paths).configured) {
3253
+ if (systemdActive(host, UNIT_NAMES.collector)) {
3254
+ stopUnits(host, [UNIT_NAMES.collector]);
3255
+ assertDirectoryStopped(paths.stateDir, "collector");
3256
+ }
3257
+ const pairing = await pairCollectorWithHub({ paths, accounts: state.accounts, host });
3258
+ if (pairing.awaiting) awaiting.push("collector");
3259
+ }
3260
+ enableUnits(host, [unitFor(component)]);
3261
+ state.components[component].enabled = true;
3262
+ writeSystemState(paths, state);
3263
+ const canStart = component === "hub" || !awaiting.includes("collector");
3264
+ if (canStart) {
3265
+ startUnits(host, [unitFor(component)]);
3266
+ await verifySystemHealth(paths, state, host, options.startupTimeoutMs ?? 2e4, [component]);
3267
+ }
3268
+ return {
3269
+ operation: "enable",
3270
+ ok: true,
3271
+ apply: true,
3272
+ message: component === "hub" ? "hub enabled" : awaiting.length > 0 ? "collector enabled; awaiting connection" : "collector enabled",
3273
+ component,
3274
+ components: { hub: state.components.hub.enabled, collector: state.components.collector.enabled },
3275
+ ...awaiting.length > 0 ? { awaitingConnection: awaiting } : {},
3276
+ password,
3277
+ state
3278
+ };
3279
+ }
3280
+ function disableSystem(options) {
3281
+ const host = options.host ?? createSystemHost();
3282
+ const paths = validateSystemPaths(options.paths);
3283
+ const state = readSystemState(paths);
3284
+ if (state === null) throw new SystemError("not-installed", "no system installation state");
3285
+ const component = options.component;
3286
+ const plan = {
3287
+ operation: "disable",
3288
+ apply: options.apply === true,
3289
+ role: state.role,
3290
+ component,
3291
+ components: { hub: component === "hub" || state.components.hub.enabled, collector: component === "collector" || state.components.collector.enabled },
3292
+ paths,
3293
+ accounts: state.accounts,
3294
+ commands: [`systemctl stop ${unitFor(component)}`, `systemctl disable ${unitFor(component)}`],
3295
+ files: [],
3296
+ notes: ["Settings, credentials, history and outboxes are retained while the component is disabled."]
3297
+ };
3298
+ if (options.apply !== true) return { operation: "disable", ok: true, apply: false, message: "plan only", plan };
3299
+ assertApply(host);
3300
+ stopUnits(host, [unitFor(component)]);
3301
+ assertDirectoryStopped(paths.stateDir, component);
3302
+ runMaybe(host, "systemctl", ["disable", unitFor(component)]);
3303
+ state.components[component].enabled = false;
3304
+ writeSystemState(paths, state);
3305
+ return {
3306
+ operation: "disable",
3307
+ ok: true,
3308
+ apply: true,
3309
+ message: `${component} disabled; settings and data retained`,
3310
+ component,
3311
+ components: { hub: state.components.hub.enabled, collector: state.components.collector.enabled },
3312
+ state
3313
+ };
3314
+ }
3315
+ function readPrivateToken(path, read) {
3316
+ let stat;
3317
+ try {
3318
+ stat = (0, import_node_fs3.lstatSync)(path);
3319
+ } catch (error) {
3320
+ if (error.code === "ENOENT") {
3321
+ throw new SystemError("missing-grant", `grant file does not exist: ${path}`);
3322
+ }
3323
+ throw error;
3324
+ }
3325
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new SystemError("unsafe-token", `grant must be a regular file: ${path}`);
3326
+ if ((stat.mode & 63) !== 0) throw new SystemError("insecure-permissions", `grant file must be private (0600): ${path}`);
3327
+ if (stat.size < 1 || stat.size > 4096) throw new SystemError("invalid-token", `grant file must contain 1-4096 bytes: ${path}`);
3328
+ if (!read) return void 0;
3329
+ const token = (0, import_node_fs3.readFileSync)(path, "utf8").trim();
3330
+ if (token === "" || /[\r\n\0]/.test(token) || !/^[\x21-\x7e]{1,256}$/.test(token)) {
3331
+ throw new SystemError("invalid-token", "grant file contains an invalid token");
3332
+ }
3333
+ return token;
3334
+ }
3335
+ async function connectSystem(options) {
3336
+ const host = options.host ?? createSystemHost();
3337
+ const paths = validateSystemPaths(options.paths);
3338
+ const state = readSystemState(paths);
3339
+ if (state === null || !state.active) throw new SystemError("not-installed", "no active system installation; run install first");
3340
+ const destination = connectDestinationUrl(options.url);
3341
+ const tokenFile = (0, import_node_path3.resolve)(options.tokenFile);
3342
+ const configured = collectorConfigured(paths);
3343
+ if (configured.configured) {
3344
+ throw new SystemError(
3345
+ "already-connected",
3346
+ "the collector already has a destination; this command never overwrites configured remote destinations"
3347
+ );
3348
+ }
3349
+ const plan = {
3350
+ operation: "connect",
3351
+ apply: options.apply === true,
3352
+ role: state.role,
3353
+ component: "collector",
3354
+ components: { hub: state.components.hub.enabled, collector: true },
3355
+ paths,
3356
+ accounts: state.accounts,
3357
+ commands: ["systemd-sysusers", "systemctl daemon-reload", "stop collector if running", "enroll collector through the hub API", "start collector if enabled"],
3358
+ files: [{ path: collectorStatePath(paths), mode: "0600", purpose: "collector identity and destination" }],
3359
+ notes: [`Connects to ${destination} using the grant in ${tokenFile}; the grant is consumed once and never printed.`]
3360
+ };
3361
+ if (options.apply !== true) return { operation: "connect", ok: true, apply: false, message: "plan only", plan };
3362
+ assertApply(host);
3363
+ const token = readPrivateToken(tokenFile, true);
3364
+ ensureComponentSetup({
3365
+ paths,
3366
+ accounts: state.accounts,
3367
+ nodePath: state.nodePath,
3368
+ component: "collector",
3369
+ power: options.power !== false,
3370
+ host
3371
+ });
3372
+ if (options.power !== false) {
3373
+ ensurePowerAccess(paths, state.accounts, host);
3374
+ state.power = { enabled: true, rulePath: raplRulePath(paths), group: state.accounts.powerGroup };
3375
+ }
3376
+ const existingIdentity = readCollectorIdentity(collectorStatePath(paths));
3377
+ if (existingIdentity !== null) {
3378
+ writeCollectorIdentity(collectorStatePath(paths), existingIdentity);
3379
+ }
3380
+ const wasActive = systemdActive(host, UNIT_NAMES.collector);
3381
+ if (wasActive) {
3382
+ stopUnits(host, [UNIT_NAMES.collector]);
3383
+ assertDirectoryStopped(paths.stateDir, "collector");
3384
+ }
3385
+ try {
3386
+ await enrollCollector(collectorStatePath(paths), destination, token, { ...SYSTEM_DEFAULT_SHARING }, false);
3387
+ } catch (error) {
3388
+ throw new SystemError(
3389
+ "enrollment-failed",
3390
+ `collector enrollment failed: ${error instanceof Error ? error.message : String(error)}`
3391
+ );
3392
+ } finally {
3393
+ chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], state.accounts.collector, state.accounts.collector, host);
3394
+ }
3395
+ if (!collectorConfigured(paths).configured) {
3396
+ throw new SystemError("enrollment-failed", "the hub accepted enrollment but no destination was saved; check private state");
3397
+ }
3398
+ chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], state.accounts.collector, state.accounts.collector, host);
3399
+ writeSystemState(paths, state);
3400
+ const enabled = state.components.collector.enabled;
3401
+ if (enabled || wasActive) {
3402
+ startUnits(host, [UNIT_NAMES.collector]);
3403
+ await verifySystemHealth(paths, state, host, options.startupTimeoutMs ?? 2e4, ["collector"]);
3404
+ }
3405
+ return {
3406
+ operation: "connect",
3407
+ ok: true,
3408
+ apply: true,
3409
+ message: enabled ? "collector connected and started" : "collector connected; run `rigscope system start collector`",
3410
+ component: "collector",
3411
+ components: { hub: state.components.hub.enabled, collector: enabled },
3412
+ state
3413
+ };
3414
+ }
3415
+ async function upgradeSystemUnlocked(options) {
3416
+ const host = options.host ?? createSystemHost();
3417
+ const now = options.now ?? Date.now;
3418
+ const paths = validateSystemPaths(options.paths);
3419
+ const state = readSystemState(paths);
3420
+ if (state === null || !state.active) throw new SystemError("not-installed", "no active system installation; run install first");
3421
+ const activeComponents = COMPONENTS.filter((component) => systemdActive(host, unitFor(component)));
3422
+ const activeUnits = activeComponents.map(unitFor);
3423
+ const plan = {
3424
+ operation: "upgrade",
3425
+ apply: options.apply === true,
3426
+ role: state.role,
3427
+ components: { hub: state.components.hub.enabled, collector: state.components.collector.enabled },
3428
+ paths,
3429
+ accounts: state.accounts,
3430
+ commands: ["stage new release", ...activeUnits.map((unit) => `systemctl stop ${unit}`), "backup database", "switch current", ...activeUnits.map((unit) => `systemctl start ${unit}`)],
3431
+ files: [{ path: (0, import_node_path3.join)(paths.stateDir, "backups"), mode: "0700", purpose: "pre-upgrade database backup" }],
3432
+ notes: [
3433
+ "Staging happens before any service is stopped.",
3434
+ "The exact active component set is restored; --start does not start a component that was stopped.",
3435
+ "A failed post-upgrade start restores the previous release and matching database backup."
3436
+ ]
3437
+ };
3438
+ if (options.apply !== true) return { operation: "upgrade", ok: true, apply: false, message: "plan only", plan };
3439
+ assertApply(host);
3440
+ assertManagedPathSafety(paths, isDefaultSystemLayout(paths));
3441
+ const source = (0, import_node_path3.resolve)(options.source);
3442
+ if (!(0, import_node_fs3.existsSync)(source)) throw new SystemError("missing-source", `release source does not exist: ${source}`);
3443
+ const stage = stageRelease({ source, paths, host, replaceRelease: options.replaceRelease, now });
3444
+ if (state.currentVersion === stage.version) {
3445
+ throw new SystemError("same-version", `release ${stage.version} is already current; use a different version`);
3446
+ }
3447
+ if (options.installDeps === true) installDependencies(stage.releaseDir, host, options.installDepsCommand ?? "npm");
3448
+ const missing = missingDependencies(stage.releaseDir);
3449
+ if (missing.length > 0) throw new SystemError("missing-dependencies", `production dependencies are missing: ${missing.join(", ")}`);
3450
+ chmodTree(stage.releaseDir, host);
3451
+ runRequired(host, "chown", ["-R", "root:root", stage.releaseDir], "release ownership");
3452
+ const oldCurrent = currentReleaseDir(paths);
3453
+ if (oldCurrent === null) throw new SystemError("invalid-state", "current release symlink is missing");
3454
+ const oldPrevious = previousReleaseDir(paths);
3455
+ let backupPath = null;
3456
+ let beforeSchema = null;
3457
+ try {
3458
+ stopUnits(host, activeUnits);
3459
+ assertDirectoryStopped(paths.stateDir);
3460
+ const lock = systemLockState(paths);
3461
+ if (lock.live) throw new SystemError("duplicate-runtime", `hub did not stop cleanly; lock remains at ${lock.path}`);
3462
+ const database = hubDatabasePath(paths);
3463
+ if ((0, import_node_fs3.existsSync)(database)) {
3464
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "backups"), 448, true);
3465
+ beforeSchema = databaseSchema(database);
3466
+ backupPath = (0, import_node_path3.join)(paths.stateDir, "backups", `pre-upgrade-${stage.version}-${newestTimestamp()}.sqlite`);
3467
+ await backupDatabase(database, backupPath, host);
3468
+ }
3469
+ replaceSymlink((0, import_node_path3.join)(paths.prefix, "previous"), oldCurrent, host);
3470
+ replaceSymlink((0, import_node_path3.join)(paths.prefix, "current"), stage.releaseDir, host);
3471
+ state.previousVersion = state.currentVersion;
3472
+ state.currentVersion = stage.version;
3473
+ state.releases[stage.version] = {
3474
+ releaseDir: stage.releaseDir,
3475
+ schemaVersion: stage.schemaVersion,
3476
+ installedAt: new Date(now()).toISOString()
3477
+ };
3478
+ if (backupPath !== null) {
3479
+ state.backups[(0, import_node_path3.basename)(backupPath)] = {
3480
+ path: backupPath,
3481
+ schemaVersion: beforeSchema,
3482
+ fromSchema: beforeSchema,
3483
+ toSchema: stage.schemaVersion,
3484
+ createdAt: new Date(now()).toISOString(),
3485
+ fromVersion: state.previousVersion ?? void 0,
3486
+ toVersion: stage.version
3487
+ };
3488
+ }
3489
+ writeSystemState(paths, state);
3490
+ if (activeComponents.length > 0) {
3491
+ startUnits(host, activeUnits);
3492
+ await verifySystemHealth(paths, state, host, options.startupTimeoutMs ?? 2e4, activeComponents);
3493
+ }
3494
+ } catch (error) {
3495
+ await recoverFailedUpgrade({
3496
+ paths,
3497
+ state,
3498
+ oldCurrent,
3499
+ oldPrevious,
3500
+ beforeSchema,
3501
+ backupPath,
3502
+ host,
3503
+ activeComponents,
3504
+ units: activeUnits
3505
+ });
3506
+ throw error;
3507
+ }
3508
+ return {
3509
+ operation: "upgrade",
3510
+ ok: true,
3511
+ apply: true,
3512
+ message: `upgraded ${state.previousVersion ?? "previous"} -> ${stage.version}`,
3513
+ version: stage.version,
3514
+ current: stage.releaseDir,
3515
+ previous: oldCurrent,
3516
+ backupPath,
3517
+ state
3518
+ };
3519
+ }
3520
+ async function withSystemOperation(options, operation) {
3521
+ if (!options.apply) return operation();
3522
+ const host = options.host ?? createSystemHost();
3523
+ assertApply(host);
3524
+ const paths = validateSystemPaths(options.paths);
3525
+ const release = acquireProcessLease((0, import_node_path3.join)(paths.stateDir, "backups/operation.lock"));
3526
+ try {
3527
+ return await operation();
3528
+ } finally {
3529
+ release();
3530
+ }
3531
+ }
3532
+ function upgradeSystem(options) {
3533
+ return withSystemOperation(options, () => upgradeSystemUnlocked(options));
3534
+ }
3535
+ function rollbackSystem(options = {}) {
3536
+ return withSystemOperation(options, () => rollbackSystemUnlocked(options));
3537
+ }
3538
+ async function rollbackSystemUnlocked(options = {}) {
3539
+ const host = options.host ?? createSystemHost();
3540
+ const now = options.now ?? Date.now;
3541
+ const paths = validateSystemPaths(options.paths);
3542
+ const state = readSystemState(paths);
3543
+ if (state === null || !state.active) throw new SystemError("not-installed", "no active system installation");
3544
+ const previous = previousReleaseDir(paths);
3545
+ if (previous === null || state.previousVersion === null) {
3546
+ throw new SystemError("no-previous", "no previous release is retained for rollback");
3547
+ }
3548
+ const previousVersion = releaseVersion(previous);
3549
+ const previousSchema = releaseSchemaVersion(previous);
3550
+ const activeComponents = COMPONENTS.filter((component) => systemdActive(host, unitFor(component)));
3551
+ const units = activeComponents.map(unitFor);
3552
+ const plan = {
3553
+ operation: "rollback",
3554
+ apply: options.apply === true,
3555
+ role: state.role,
3556
+ components: { hub: state.components.hub.enabled, collector: state.components.collector.enabled },
3557
+ paths,
3558
+ accounts: state.accounts,
3559
+ commands: [...units.map((unit) => `systemctl stop ${unit}`), "restore matching database backup if needed", `switch current to ${previousVersion}`, ...units.map((unit) => `systemctl start ${unit}`)],
3560
+ files: [],
3561
+ notes: [
3562
+ "An old release is never run against a newer schema without restoring a matching verified backup.",
3563
+ "The exact active component set is restored; a stopped component stays stopped."
3564
+ ]
3565
+ };
3566
+ if (options.apply !== true) return { operation: "rollback", ok: true, apply: false, message: "plan only", plan };
3567
+ assertApply(host);
3568
+ const oldCurrent = currentReleaseDir(paths);
3569
+ if (oldCurrent === null) throw new SystemError("invalid-state", "current release symlink is missing");
3570
+ const originalState = JSON.parse(JSON.stringify(state));
3571
+ let safetyBackup = null;
3572
+ let restoredDatabase = false;
3573
+ try {
3574
+ stopUnits(host, units);
3575
+ assertDirectoryStopped(paths.stateDir);
3576
+ if (systemLockState(paths).live || lockIsLive((0, import_node_path3.join)(paths.stateDir, "collector/collector-runtime.lock")).live) {
3577
+ throw new SystemError("runtime-active", "runtime lease remains after stop; rollback refused");
3578
+ }
3579
+ const database = hubDatabasePath(paths);
3580
+ const currentSchema = (0, import_node_fs3.existsSync)(database) ? databaseSchema(database) : null;
3581
+ let backupToRestore = null;
3582
+ if (currentSchema !== null && (previousSchema === null || currentSchema > previousSchema)) {
3583
+ backupToRestore = pickRollbackBackup(state, previousSchema);
3584
+ if (backupToRestore === null || !(0, import_node_fs3.existsSync)(backupToRestore.path)) {
3585
+ throw new SystemError(
3586
+ "incompatible-schema",
3587
+ `current database schema ${currentSchema} is newer than release ${previousVersion}; no matching pre-upgrade backup is recorded`
3588
+ );
3589
+ }
3590
+ }
3591
+ if ((0, import_node_fs3.existsSync)(database)) {
3592
+ ensureDirectory((0, import_node_path3.join)(paths.stateDir, "backups"), 448, true);
3593
+ safetyBackup = (0, import_node_path3.join)(paths.stateDir, "backups", `pre-rollback-${newestTimestamp()}.sqlite`);
3594
+ await backupDatabase(database, safetyBackup, host);
3595
+ }
3596
+ if (backupToRestore !== null) {
3597
+ replaceDatabaseFromBackup(backupToRestore.path, database, safetyBackup, host);
3598
+ restoredDatabase = true;
3599
+ runRequired(host, "chown", [`${state.accounts.hub}:${state.accounts.hub}`, database], "restored database ownership");
3600
+ }
3601
+ replaceSymlink((0, import_node_path3.join)(paths.prefix, "previous"), oldCurrent, host);
3602
+ replaceSymlink((0, import_node_path3.join)(paths.prefix, "current"), previous, host);
3603
+ state.previousVersion = state.currentVersion;
3604
+ state.currentVersion = previousVersion;
3605
+ state.updatedAt = new Date(now()).toISOString();
3606
+ writeSystemState(paths, state);
3607
+ if (activeComponents.length > 0) {
3608
+ startUnits(host, units);
3609
+ await verifySystemHealth(paths, state, host, options.startupTimeoutMs ?? 2e4, activeComponents);
3610
+ }
3611
+ return {
3612
+ operation: "rollback",
3613
+ ok: true,
3614
+ apply: true,
3615
+ message: `rolled back to ${previousVersion}`,
3616
+ version: previousVersion,
3617
+ current: previous,
3618
+ previous: oldCurrent,
3619
+ backupPath: backupToRestore?.path ?? safetyBackup,
3620
+ state
3621
+ };
3622
+ } catch (error) {
3623
+ stopUnits(host, units);
3624
+ replaceSymlink((0, import_node_path3.join)(paths.prefix, "current"), oldCurrent, host);
3625
+ replaceSymlink((0, import_node_path3.join)(paths.prefix, "previous"), previous, host);
3626
+ if (restoredDatabase && safetyBackup) {
3627
+ const failed = (0, import_node_path3.join)(paths.stateDir, "backups", `failed-rollback-${newestTimestamp()}.sqlite`);
3628
+ await backupDatabase(hubDatabasePath(paths), failed, host);
3629
+ replaceDatabaseFromBackup(safetyBackup, hubDatabasePath(paths), null, host);
3630
+ runRequired(host, "chown", [`${state.accounts.hub}:${state.accounts.hub}`, hubDatabasePath(paths)], "restored database ownership");
3631
+ }
3632
+ writeSystemState(paths, originalState);
3633
+ if (activeComponents.length > 0) {
3634
+ startUnits(host, units);
3635
+ await verifySystemHealth(paths, originalState, host, options.startupTimeoutMs ?? 2e4, activeComponents);
3636
+ }
3637
+ throw error;
3638
+ }
3639
+ }
3640
+ function uninstallSystem(options = {}) {
3641
+ const host = options.host ?? createSystemHost();
3642
+ const now = options.now ?? Date.now;
3643
+ const paths = validateSystemPaths(options.paths);
3644
+ const state = readSystemState(paths);
3645
+ const units = [UNIT_NAMES.hub, UNIT_NAMES.collector];
3646
+ const plan = {
3647
+ operation: "uninstall",
3648
+ apply: options.apply === true,
3649
+ role: state?.role,
3650
+ components: { hub: false, collector: false },
3651
+ paths,
3652
+ accounts: state?.accounts ?? validateAccounts(options.accounts),
3653
+ commands: [
3654
+ ...units.map((unit) => `systemctl stop ${unit}`),
3655
+ ...units.map((unit) => `systemctl disable ${unit}`),
3656
+ "systemctl daemon-reload",
3657
+ "remove both Rigscope units",
3658
+ "remove releases, copied Node and active links",
3659
+ "remove RAPL rule and restore counter permissions",
3660
+ ...options.purgeData ? ["remove /var/lib/rigscope and /etc/rigscope (history)"] : [],
3661
+ ...options.removeUsers ? ["delete service accounts and power group"] : []
3662
+ ],
3663
+ files: [],
3664
+ notes: [
3665
+ "The default uninstall removes both units and the application while preserving the database, history, configuration and accounts.",
3666
+ "History is removed only with --purge-data --confirm-purge-history.",
3667
+ "Adopted user data remains untouched at the recorded source path."
3668
+ ]
3669
+ };
3670
+ if (options.apply !== true) return { operation: "uninstall", ok: true, apply: false, message: "plan only", plan };
3671
+ assertApply(host);
3672
+ if (options.purgeData === true && options.confirmPurgeHistory !== true) {
3673
+ throw new SystemError("confirmation-required", "--purge-data requires --confirm-purge-history");
3674
+ }
3675
+ if (options.removeUsers === true && options.purgeData !== true) {
3676
+ throw new SystemError("order-required", "refusing to delete accounts while their data remains; combine --remove-users with --purge-data");
3677
+ }
3678
+ const accounts = state?.accounts ?? validateAccounts(options.accounts);
3679
+ stopUnits(host, units);
3680
+ assertDirectoryStopped(paths.stateDir);
3681
+ for (const unit of units) runMaybe(host, "systemctl", ["disable", unit]);
3682
+ for (const unit of units) {
3683
+ const path = (0, import_node_path3.join)(paths.unitsDir, unit);
3684
+ assertManagedFileOwnership(paths, path);
3685
+ try {
3686
+ (0, import_node_fs3.unlinkSync)(path);
3687
+ } catch {
3688
+ }
3689
+ }
3690
+ runMaybe(host, "systemctl", ["daemon-reload"]);
3691
+ const hadPowerAccess = state?.power?.enabled === true || (0, import_node_fs3.existsSync)(raplRulePath(paths)) || (0, import_node_fs3.existsSync)(powerOriginalPath(paths));
3692
+ if (hadPowerAccess) {
3693
+ try {
3694
+ (0, import_node_fs3.unlinkSync)(raplRulePath(paths));
3695
+ } catch {
3696
+ }
3697
+ try {
3698
+ (0, import_node_fs3.unlinkSync)(powerSysusersPath(paths));
3699
+ } catch {
3700
+ }
3701
+ restorePowerOriginals(paths, host);
3702
+ resumePowerRule(paths, host);
3703
+ }
3704
+ removeSymlink((0, import_node_path3.join)(paths.prefix, "current"));
3705
+ removeSymlink((0, import_node_path3.join)(paths.prefix, "previous"));
3706
+ (0, import_node_fs3.rmSync)((0, import_node_path3.join)(paths.prefix, "releases"), { recursive: true, force: true });
3707
+ (0, import_node_fs3.rmSync)((0, import_node_path3.join)(paths.prefix, "node"), { recursive: true, force: true });
3708
+ if (state !== null) {
3709
+ state.active = false;
3710
+ state.power = null;
3711
+ state.components = { hub: { enabled: false }, collector: { enabled: false } };
3712
+ state.updatedAt = new Date(now()).toISOString();
3713
+ writeSystemState(paths, state);
3714
+ }
3715
+ if (options.removeUsers === true) {
3716
+ for (const account of [accounts.hub, accounts.collector]) runMaybe(host, "userdel", [account]);
3717
+ for (const group of [accounts.powerGroup, accounts.hub, accounts.collector]) runMaybe(host, "groupdel", [group]);
3718
+ }
3719
+ if (options.purgeData === true) {
3720
+ (0, import_node_fs3.rmSync)(paths.stateDir, { recursive: true, force: true });
3721
+ (0, import_node_fs3.rmSync)(paths.configDir, { recursive: true, force: true });
3722
+ }
3723
+ return {
3724
+ operation: "uninstall",
3725
+ ok: true,
3726
+ apply: true,
3727
+ message: options.purgeData === true ? "uninstalled; system data and history removed" : "uninstalled; application removed, data and history preserved",
3728
+ ...options.removeUsers === true ? { components: { hub: false, collector: false } } : {},
3729
+ state
3730
+ };
3731
+ }
3732
+ function powerStatus(options = {}) {
3733
+ return inspectPowerAccess({
3734
+ paths: options.paths,
3735
+ accounts: options.accounts,
3736
+ passwdFile: options.passwdFile,
3737
+ groupFile: options.groupFile,
3738
+ moduleRoot: options.moduleRoot
3739
+ });
3740
+ }
3741
+ function grantPower(options = {}) {
3742
+ const host = options.host ?? createSystemHost();
3743
+ const paths = validateSystemPaths(options.paths);
3744
+ const accounts = validateAccounts(options.accounts);
3745
+ const plan = {
3746
+ operation: "power-grant",
3747
+ apply: options.apply === true,
3748
+ paths,
3749
+ accounts,
3750
+ commands: [
3751
+ "systemd-sysusers",
3752
+ "udevadm control --reload-rules",
3753
+ "udevadm trigger --subsystem-match=powercap --action=change",
3754
+ `chgrp ${accounts.powerGroup} <package>/energy_uj`,
3755
+ "chmod 0440 <package>/energy_uj"
3756
+ ],
3757
+ files: [
3758
+ { path: raplRulePath(paths), mode: "0644", purpose: "package-only RAPL rule" },
3759
+ { path: powerSysusersPath(paths), mode: "0644", purpose: "collector-only power group" }
3760
+ ],
3761
+ notes: ["Only top-level package energy/range counters are granted; no capabilities are added to Node."]
3762
+ };
3763
+ if (options.apply !== true) return { operation: "power-grant", ok: true, apply: false, message: "plan only", plan };
3764
+ assertApply(host);
3765
+ ensureDirectory(paths.sysusersDir, 493);
3766
+ ensureDirectory(paths.udevRulesDir, 493);
3767
+ ensureDirectory(paths.stateDir, 493);
3768
+ writeManagedFile(powerSysusersPath(paths), renderPowerSysusers(paths, accounts), 420, true);
3769
+ runRequired(host, "systemd-sysusers", [], "power group creation");
3770
+ assertSystemAccounts(host, accounts, true, ["collector"]);
3771
+ if (!(0, import_node_fs3.existsSync)(powerOriginalPath(paths))) capturePowerOriginals(paths);
3772
+ writePowerRule(paths, accounts);
3773
+ resumePowerRule(paths, host);
3774
+ applyPowerAccess(paths, accounts, host);
3775
+ const access = inspectPowerAccess({ paths, accounts, passwdFile: options.passwdFile, groupFile: options.groupFile, moduleRoot: options.moduleRoot });
3776
+ return { operation: "power-grant", ok: true, apply: true, message: "power access granted", plan, access };
3777
+ }
3778
+ function revokePower(options = {}) {
3779
+ const host = options.host ?? createSystemHost();
3780
+ const paths = validateSystemPaths(options.paths);
3781
+ const accounts = validateAccounts(options.accounts);
3782
+ const plan = {
3783
+ operation: "power-revoke",
3784
+ apply: options.apply === true,
3785
+ paths,
3786
+ accounts,
3787
+ commands: [
3788
+ "remove RAPL rule",
3789
+ "udevadm control --reload-rules",
3790
+ "udevadm trigger --subsystem-match=powercap --action=change",
3791
+ `gpasswd -d ${accounts.collector} ${accounts.powerGroup}`,
3792
+ "restore recorded counter modes"
3793
+ ],
3794
+ files: [],
3795
+ notes: ["Restores the exact modes/ownership recorded at grant time; never leaves energy_uj group-readable."]
3796
+ };
3797
+ if (options.apply !== true) return { operation: "power-revoke", ok: true, apply: false, message: "plan only", plan };
3798
+ assertApply(host);
3799
+ try {
3800
+ (0, import_node_fs3.unlinkSync)(raplRulePath(paths));
3801
+ } catch {
3802
+ }
3803
+ try {
3804
+ (0, import_node_fs3.unlinkSync)(powerSysusersPath(paths));
3805
+ } catch {
3806
+ }
3807
+ restorePowerOriginals(paths, host);
3808
+ resumePowerRule(paths, host);
3809
+ runMaybe(host, "gpasswd", ["-d", accounts.collector, accounts.powerGroup]);
3810
+ if (options.removeGroup === true) runMaybe(host, "groupdel", [accounts.powerGroup]);
3811
+ const access = inspectPowerAccess({ paths, accounts, passwdFile: options.passwdFile, groupFile: options.groupFile, moduleRoot: options.moduleRoot });
3812
+ return { operation: "power-revoke", ok: true, apply: true, message: "power access revoked", plan, access };
3813
+ }
3814
+ var HELP = `Rigscope system installation (optional, root/sudo)
3815
+
3816
+ Usage:
3817
+ rigscope system plan --source ARCHIVE_OR_DIR [options]
3818
+ rigscope system install --source ARCHIVE_OR_DIR --apply [options]
3819
+ rigscope system status [hub|collector] [--json]
3820
+ rigscope system logs [hub|collector] [--follow]
3821
+ rigscope system enable hub|collector --apply
3822
+ rigscope system disable hub|collector --apply
3823
+ rigscope system start|stop|restart [hub|collector] --apply
3824
+ rigscope system connect URL --token-file FILE --apply
3825
+ rigscope system upgrade --source ARCHIVE_OR_DIR --apply
3826
+ rigscope system rollback --apply
3827
+ rigscope system uninstall --apply [--purge-data --confirm-purge-history] [--remove-users]
3828
+ rigscope system power status|grant|revoke [--apply]
3829
+
3830
+ Options:
3831
+ --role local|hub|collector Initial enabled preset; not a permanent restriction
3832
+ --adopt-user-state DIR Explicit, copy-only adoption of a stopped user installation
3833
+ --allow-user-conflicts Install beside existing user units (they must be stopped; no adoption)
3834
+ --node PATH Node 26 executable; a non-root-owned binary is copied to /opt/rigscope/node
3835
+ --copy-node Always copy the selected Node executable into the root-owned prefix
3836
+ --install-deps Run npm ci --omit=dev --ignore-scripts as root while staging
3837
+ --without-power Do not install collector group access to package counters
3838
+ --token-file PATH Read a one-time connection grant from a private (0600) file
3839
+ --start / --enable Start and/or enable the initial preset units explicitly
3840
+ --json Machine-readable output
3841
+ --apply Actually mutate; without it only the exact plan is printed
3842
+
3843
+ Path overrides (testing/custom hosts):
3844
+ --prefix DIR --config-dir DIR --state-dir DIR --units-dir DIR
3845
+ --sysusers-dir DIR --udev-rules-dir DIR --powercap-root DIR
3846
+
3847
+ One package contains both runtimes. --role only sets the initial enabled
3848
+ preset; enable or disable hub and collector independently at any time.
3849
+ Safety: mutations require root, never run systemctl --user, preserve user state,
3850
+ and refuse duplicate hub runtimes or world-readable RAPL access.`;
3851
+ function parseSystemArgs(argv) {
3852
+ if (argv.length === 0 || argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
3853
+ return { command: "help", values: {}, positionals: [] };
3854
+ }
3855
+ const command = argv[0];
3856
+ const rest = argv.slice(1);
3857
+ let subcommand;
3858
+ if (command === "power" && rest[0] !== void 0 && !rest[0].startsWith("-")) {
3859
+ subcommand = rest[0];
3860
+ rest.shift();
3861
+ }
3862
+ const parsed = (0, import_node_util.parseArgs)({
3863
+ args: rest,
3864
+ allowPositionals: true,
3865
+ options: {
3866
+ apply: { type: "boolean" },
3867
+ source: { type: "string" },
3868
+ role: { type: "string" },
3869
+ node: { type: "string" },
3870
+ "copy-node": { type: "boolean" },
3871
+ "install-deps": { type: "boolean" },
3872
+ "install-deps-command": { type: "string" },
3873
+ "without-power": { type: "boolean" },
3874
+ token: { type: "string" },
3875
+ "token-file": { type: "string" },
3876
+ start: { type: "boolean" },
3877
+ enable: { type: "boolean" },
3878
+ "adopt-user-state": { type: "string" },
3879
+ "allow-user-conflicts": { type: "boolean" },
3880
+ "replace-release": { type: "boolean" },
3881
+ port: { type: "string" },
3882
+ origin: { type: "string" },
3883
+ json: { type: "boolean" },
3884
+ follow: { type: "boolean" },
3885
+ "purge-data": { type: "boolean" },
3886
+ "confirm-purge-history": { type: "boolean" },
3887
+ "remove-users": { type: "boolean" },
3888
+ "purge-program": { type: "boolean" },
3889
+ "remove-group": { type: "boolean" },
3890
+ "startup-timeout": { type: "string" },
3891
+ prefix: { type: "string" },
3892
+ "config-dir": { type: "string" },
3893
+ "state-dir": { type: "string" },
3894
+ "units-dir": { type: "string" },
3895
+ "sysusers-dir": { type: "string" },
3896
+ "udev-rules-dir": { type: "string" },
3897
+ "powercap-root": { type: "string" },
3898
+ "passwd-file": { type: "string" },
3899
+ "group-file": { type: "string" },
3900
+ "module-root": { type: "string" }
3901
+ }
3902
+ });
3903
+ return { command, subcommand, values: parsed.values, positionals: parsed.positionals };
3904
+ }
3905
+ function stringValue(values, key) {
3906
+ const value = values[key];
3907
+ if (value === void 0) return void 0;
3908
+ if (typeof value !== "string" || value === "") throw new SystemError("invalid-argument", `--${key} requires a value`);
3909
+ return value;
3910
+ }
3911
+ function boolValue(values, key) {
3912
+ return values[key] === true;
3913
+ }
3914
+ function pathsFrom(values) {
3915
+ return {
3916
+ ...stringValue(values, "prefix") ? { prefix: stringValue(values, "prefix") } : {},
3917
+ ...stringValue(values, "config-dir") ? { configDir: stringValue(values, "config-dir") } : {},
3918
+ ...stringValue(values, "state-dir") ? { stateDir: stringValue(values, "state-dir") } : {},
3919
+ ...stringValue(values, "units-dir") ? { unitsDir: stringValue(values, "units-dir") } : {},
3920
+ ...stringValue(values, "sysusers-dir") ? { sysusersDir: stringValue(values, "sysusers-dir") } : {},
3921
+ ...stringValue(values, "udev-rules-dir") ? { udevRulesDir: stringValue(values, "udev-rules-dir") } : {},
3922
+ ...stringValue(values, "powercap-root") ? { powercapRoot: stringValue(values, "powercap-root") } : {}
3923
+ };
3924
+ }
3925
+ function roleFrom(values) {
3926
+ const role = stringValue(values, "role");
3927
+ if (role === void 0) return void 0;
3928
+ if (role !== "local" && role !== "hub" && role !== "collector") throw new SystemError("invalid-role", "--role must be local, hub or collector");
3929
+ return role;
3930
+ }
3931
+ function componentFrom(parsed, required) {
3932
+ if (parsed.positionals.length > 1) {
3933
+ throw new SystemError("invalid-argument", `${parsed.command} accepts at most one component (hub or collector)`);
3934
+ }
3935
+ const value = parsed.positionals[0];
3936
+ if (value === void 0) {
3937
+ if (required) throw new SystemError("missing-argument", `${parsed.command} requires hub or collector`);
3938
+ return void 0;
3939
+ }
3940
+ if (value !== "hub" && value !== "collector") {
3941
+ throw new SystemError("invalid-argument", `${parsed.command} expects hub or collector; got ${JSON.stringify(value)}`);
3942
+ }
3943
+ return value;
3944
+ }
3945
+ function startupTimeoutFrom(values) {
3946
+ const raw = stringValue(values, "startup-timeout");
3947
+ if (raw === void 0) return void 0;
3948
+ const value = Number(raw);
3949
+ if (!Number.isInteger(value) || value < 0 || value > 12e4) throw new SystemError("invalid-argument", "--startup-timeout must be 0-120000 ms");
3950
+ return value;
3951
+ }
3952
+ function formatSystemReport(report) {
3953
+ return `${JSON.stringify(report, null, 2)}
3954
+ `;
3955
+ }
3956
+ function humanPlan(plan) {
3957
+ const lines = [
3958
+ `Operation: ${plan.operation}${plan.apply ? "" : " (plan only; add --apply with sudo to execute)"}`,
3959
+ `Role: ${plan.role ?? "n/a"}${plan.role ? " (initial preset metadata)" : ""}`,
3960
+ ...plan.component ? [`Component: ${plan.component}`] : [],
3961
+ ...plan.components ? [`Enabled: ${COMPONENTS.filter((component) => plan.components[component]).join(", ") || "none"}`] : [],
3962
+ `Prefix: ${plan.paths.prefix}`,
3963
+ `Config: ${plan.paths.configDir}`,
3964
+ `State: ${plan.paths.stateDir}`,
3965
+ `Units: ${plan.paths.unitsDir}`,
3966
+ `Accounts: hub=${plan.accounts.hub} collector=${plan.accounts.collector} power=${plan.accounts.powerGroup}`,
3967
+ "",
3968
+ "Files:",
3969
+ ...plan.files.map((file) => ` ${file.path} (${file.mode}) - ${file.purpose}`),
3970
+ "",
3971
+ "Commands:",
3972
+ ...plan.commands.map((command) => ` ${command}`),
3973
+ "",
3974
+ ...plan.notes.map((note) => `- ${note}`)
3975
+ ];
3976
+ return `${lines.join("\n")}
3977
+ `;
3978
+ }
3979
+ function humanStatus(status) {
3980
+ const paths = status.paths;
3981
+ const database = status.database;
3982
+ const components = status.components;
3983
+ const power = status.power;
3984
+ const adoption = status.adoption;
3985
+ const lines = [
3986
+ `Scope: system`,
3987
+ `Installed: ${status.installed === true ? "yes" : "no"}`,
3988
+ `Role: ${String(status.role ?? "n/a")} (initial preset metadata)`,
3989
+ `Current: ${status.currentVersion === null || status.currentVersion === void 0 ? "none" : `${status.currentVersion} (${status.current})`}`,
3990
+ `Previous: ${status.previousVersion === null || status.previousVersion === void 0 ? "none" : `${status.previousVersion} (${status.previous})`}`,
3991
+ `Prefix: ${paths.prefix}`,
3992
+ `Config: ${paths.configDir}`,
3993
+ `State: ${paths.stateDir}`,
3994
+ `Database: ${database.path} (${database.exists ? `schema ${database.schema ?? "unknown"}` : "absent"})${database.lock.live ? " RUNNING" : ""}`,
3995
+ `Socket: ${String(status.socket.path)}${status.socket.exists ? " (present)" : ""}`,
3996
+ "",
3997
+ "Components:",
3998
+ ...(status.selected ? [status.selected] : [...COMPONENTS]).map((name) => {
3999
+ const component = components[name];
4000
+ const detail = name === "collector" ? `, ${component.destinations ?? 0} destination(s)` : "";
4001
+ return ` ${name}: ${component.present ? "installed" : "missing"}, ${component.enabled ? "enabled" : "disabled"}, ${component.active ? "active" : "inactive"}, ${component.readiness}${detail}`;
4002
+ }),
4003
+ "",
4004
+ `Power: ${power.available ? `${power.domains.length} package counter(s)` : "no package counters"}; collector read ${power.collectorCanRead ? "yes" : "no"}; persistent ${power.persistent ? "yes" : "no"}; world-readable ${power.worldReadable ? "YES" : "no"}`,
4005
+ ...power.problems.length > 0 ? ["Power notes:", ...power.problems.map((problem) => ` - ${problem}`)] : [],
4006
+ ...adoption ? ["", `Adoption: copied from ${adoption.source} (original preserved; reversible by deleting the system copy)`] : [],
4007
+ ...status.stateError ? ["", `State warning: ${String(status.stateError)}`] : []
4008
+ ];
4009
+ return `${lines.join("\n")}
4010
+ `;
4011
+ }
4012
+ function printMutationReport(report) {
4013
+ console.log(report.message);
4014
+ if (report.password) console.log(`Owner password: ${report.password} (shown once; store it now)`);
4015
+ if (report.awaitingConnection?.includes("collector")) {
4016
+ console.log("Awaiting connection: create a collector grant at the hub, save it to a private (0600) file,");
4017
+ console.log("then run `rigscope system connect https://<hub> --token-file <file> --apply`.");
4018
+ }
4019
+ }
4020
+ async function runSystemCommand(argv) {
4021
+ const parsed = parseSystemArgs(argv);
4022
+ if (parsed.command === "help") {
4023
+ console.log(HELP);
4024
+ return 0;
4025
+ }
4026
+ const values = parsed.values;
4027
+ const paths = pathsFrom(values);
4028
+ const json = boolValue(values, "json");
4029
+ const host = createSystemHost();
4030
+ try {
4031
+ if (parsed.command === "plan") {
4032
+ if (parsed.values.source === void 0) throw new SystemError("missing-argument", "plan requires --source");
4033
+ const plan = planSystemInstall({
4034
+ apply: false,
4035
+ source: stringValue(values, "source"),
4036
+ role: roleFrom(values),
4037
+ paths,
4038
+ nodePath: stringValue(values, "node"),
4039
+ copyNode: boolValue(values, "copy-node"),
4040
+ port: stringValue(values, "port") ? Number(stringValue(values, "port")) : void 0,
4041
+ origin: stringValue(values, "origin"),
4042
+ adoptFrom: stringValue(values, "adopt-user-state"),
4043
+ installDeps: boolValue(values, "install-deps"),
4044
+ start: boolValue(values, "start"),
4045
+ enable: boolValue(values, "enable")
4046
+ });
4047
+ console.log(json ? formatSystemReport(plan) : humanPlan(plan));
4048
+ return 0;
4049
+ }
4050
+ if (parsed.command === "install") {
4051
+ if (stringValue(values, "source") === void 0) throw new SystemError("missing-argument", "install requires --source");
4052
+ const report = await installSystem({
4053
+ apply: boolValue(values, "apply"),
4054
+ source: stringValue(values, "source"),
4055
+ role: roleFrom(values),
4056
+ paths,
4057
+ nodePath: stringValue(values, "node"),
4058
+ copyNode: boolValue(values, "copy-node"),
4059
+ port: stringValue(values, "port") ? Number(stringValue(values, "port")) : void 0,
4060
+ origin: stringValue(values, "origin"),
4061
+ adoptFrom: stringValue(values, "adopt-user-state"),
4062
+ installDeps: boolValue(values, "install-deps"),
4063
+ installDepsCommand: stringValue(values, "install-deps-command"),
4064
+ power: boolValue(values, "without-power") ? false : void 0,
4065
+ start: boolValue(values, "start"),
4066
+ enable: boolValue(values, "enable"),
4067
+ replaceRelease: boolValue(values, "replace-release"),
4068
+ allowUserConflicts: boolValue(values, "allow-user-conflicts"),
4069
+ startupTimeoutMs: startupTimeoutFrom(values),
4070
+ host
4071
+ });
4072
+ if (json) console.log(formatSystemReport(report));
4073
+ else if (report.plan) console.log(`Installed: no (plan only)
4074
+
4075
+ ${humanPlan(report.plan)}`);
4076
+ else {
4077
+ printMutationReport(report);
4078
+ console.log(`Current: ${report.current ?? "none"}`);
4079
+ }
4080
+ return 0;
4081
+ }
4082
+ if (parsed.command === "status") {
4083
+ const component = componentFrom(parsed, false);
4084
+ const status = systemStatus({ paths, host, json, component, homeRoots: void 0 });
4085
+ console.log(json ? formatSystemReport(status) : humanStatus(status));
4086
+ return 0;
4087
+ }
4088
+ if (parsed.command === "logs") {
4089
+ const selected = validateSystemPaths(paths);
4090
+ const component = componentFrom(parsed, false);
4091
+ const units = (component ? [unitFor(component)] : [UNIT_NAMES.hub, UNIT_NAMES.collector]).filter((unit) => (0, import_node_fs3.existsSync)((0, import_node_path3.join)(selected.unitsDir, unit)));
4092
+ if (units.length === 0) throw new SystemError("not-installed", component ? `${component} logs are unavailable; its unit is not installed` : "no system services installed");
4093
+ const result = (0, import_node_child_process.spawnSync)("journalctl", [...units.flatMap((unit) => ["--unit", unit]), "--lines=100", "--no-pager", ...boolValue(values, "follow") ? ["--follow"] : []], { stdio: "inherit" });
4094
+ if (result.error) throw result.error;
4095
+ return result.status ?? 1;
4096
+ }
4097
+ if (parsed.command === "enable" || parsed.command === "disable") {
4098
+ const component = componentFrom(parsed, true);
4099
+ const options = {
4100
+ apply: boolValue(values, "apply"),
4101
+ paths,
4102
+ host,
4103
+ component,
4104
+ power: boolValue(values, "without-power") ? false : void 0,
4105
+ startupTimeoutMs: startupTimeoutFrom(values)
4106
+ };
4107
+ const report = parsed.command === "enable" ? await enableSystem({ ...options, component }) : disableSystem({ ...options, component });
4108
+ if (json) console.log(formatSystemReport(report));
4109
+ else if (report.plan) console.log(humanPlan(report.plan));
4110
+ else printMutationReport(report);
4111
+ return 0;
4112
+ }
4113
+ if (parsed.command === "connect") {
4114
+ const url = parsed.positionals[0];
4115
+ if (url === void 0) throw new SystemError("missing-argument", "connect requires a hub URL");
4116
+ if (parsed.positionals.length > 1) throw new SystemError("invalid-argument", "connect accepts one hub URL");
4117
+ const tokenFile = stringValue(values, "token-file");
4118
+ if (tokenFile === void 0) throw new SystemError("missing-argument", "connect requires --token-file with a private (0600) grant file");
4119
+ const report = await connectSystem({
4120
+ apply: boolValue(values, "apply"),
4121
+ paths,
4122
+ host,
4123
+ url,
4124
+ tokenFile,
4125
+ power: boolValue(values, "without-power") ? false : void 0,
4126
+ startupTimeoutMs: startupTimeoutFrom(values)
4127
+ });
4128
+ if (json) console.log(formatSystemReport(report));
4129
+ else if (report.plan) console.log(humanPlan(report.plan));
4130
+ else printMutationReport(report);
4131
+ return 0;
4132
+ }
4133
+ if (parsed.command === "start" || parsed.command === "stop" || parsed.command === "restart") {
4134
+ const component = componentFrom(parsed, false);
4135
+ const options = {
4136
+ apply: boolValue(values, "apply"),
4137
+ paths,
4138
+ host,
4139
+ component,
4140
+ enable: boolValue(values, "enable"),
4141
+ startupTimeoutMs: startupTimeoutFrom(values)
4142
+ };
4143
+ const report = parsed.command === "start" ? await startSystem(options) : parsed.command === "stop" ? stopSystem(options) : await restartSystem(options);
4144
+ if (json) console.log(formatSystemReport(report));
4145
+ else if (report.plan) console.log(humanPlan(report.plan));
4146
+ else printMutationReport(report);
4147
+ return 0;
4148
+ }
4149
+ if (parsed.command === "upgrade") {
4150
+ const report = await upgradeSystem({
4151
+ apply: boolValue(values, "apply"),
4152
+ source: stringValue(values, "source") ?? "",
4153
+ paths,
4154
+ host,
4155
+ start: boolValue(values, "start"),
4156
+ installDeps: boolValue(values, "install-deps"),
4157
+ installDepsCommand: stringValue(values, "install-deps-command"),
4158
+ replaceRelease: boolValue(values, "replace-release"),
4159
+ startupTimeoutMs: startupTimeoutFrom(values)
4160
+ });
4161
+ if (json) console.log(formatSystemReport(report));
4162
+ else if (report.plan) console.log(humanPlan(report.plan));
4163
+ else console.log(`${report.message}
4164
+ Backup: ${report.backupPath ?? "none"}`);
4165
+ return 0;
4166
+ }
4167
+ if (parsed.command === "rollback") {
4168
+ const report = await rollbackSystem({ apply: boolValue(values, "apply"), paths, host, start: boolValue(values, "start"), startupTimeoutMs: startupTimeoutFrom(values) });
4169
+ if (json) console.log(formatSystemReport(report));
4170
+ else if (report.plan) console.log(humanPlan(report.plan));
4171
+ else console.log(`${report.message}
4172
+ Backup: ${report.backupPath ?? "none"}`);
4173
+ return 0;
4174
+ }
4175
+ if (parsed.command === "uninstall") {
4176
+ const report = uninstallSystem({
4177
+ apply: boolValue(values, "apply"),
4178
+ paths,
4179
+ host,
4180
+ purgeData: boolValue(values, "purge-data"),
4181
+ confirmPurgeHistory: boolValue(values, "confirm-purge-history"),
4182
+ removeUsers: boolValue(values, "remove-users"),
4183
+ purgeProgram: boolValue(values, "purge-program")
4184
+ });
4185
+ if (json) console.log(formatSystemReport(report));
4186
+ else if (report.plan) console.log(humanPlan(report.plan));
4187
+ else console.log(report.message);
4188
+ return 0;
4189
+ }
4190
+ if (parsed.command === "power") {
4191
+ const sub = parsed.subcommand ?? "status";
4192
+ if (sub === "status") {
4193
+ const access = powerStatus({
4194
+ paths,
4195
+ passwdFile: stringValue(values, "passwd-file"),
4196
+ groupFile: stringValue(values, "group-file"),
4197
+ moduleRoot: stringValue(values, "module-root")
4198
+ });
4199
+ if (json) console.log(formatSystemReport(access));
4200
+ else {
4201
+ console.log(`Driver: ${access.driver.length > 0 ? access.driver.join(", ") : "not detected"}`);
4202
+ console.log(`Domains: ${access.domains.length} package counter(s)`);
4203
+ for (const domain of access.domains) {
4204
+ console.log(` ${domain.name}: energy ${domain.energy?.mode ?? "missing"} ${domain.energy?.readable ? "collector-readable" : "denied"}${domain.energy?.worldReadable ? " WORLD-READABLE" : ""}`);
4205
+ }
4206
+ console.log(`Access: collector read ${access.collectorCanRead ? "yes" : "no"}; persistent ${access.persistent ? "yes" : "no"}; world-readable ${access.worldReadable ? "YES" : "no"}`);
4207
+ for (const problem of access.problems) console.log(` - ${problem}`);
4208
+ }
4209
+ return access.worldReadable || access.writableByCollector ? 1 : 0;
4210
+ }
4211
+ if (sub === "grant") {
4212
+ const report = grantPower({
4213
+ apply: boolValue(values, "apply"),
4214
+ paths,
4215
+ host,
4216
+ passwdFile: stringValue(values, "passwd-file"),
4217
+ groupFile: stringValue(values, "group-file"),
4218
+ moduleRoot: stringValue(values, "module-root")
4219
+ });
4220
+ if (json) console.log(formatSystemReport(report));
4221
+ else if (report.plan) console.log(humanPlan(report.plan));
4222
+ else console.log(report.message);
4223
+ return 0;
4224
+ }
4225
+ if (sub === "revoke") {
4226
+ const report = revokePower({
4227
+ apply: boolValue(values, "apply"),
4228
+ paths,
4229
+ host,
4230
+ removeGroup: boolValue(values, "remove-group"),
4231
+ passwdFile: stringValue(values, "passwd-file"),
4232
+ groupFile: stringValue(values, "group-file"),
4233
+ moduleRoot: stringValue(values, "module-root")
4234
+ });
4235
+ if (json) console.log(formatSystemReport(report));
4236
+ else if (report.plan) console.log(humanPlan(report.plan));
4237
+ else console.log(report.message);
4238
+ return 0;
4239
+ }
4240
+ throw new SystemError("unknown-command", "system power expects status, grant or revoke");
4241
+ }
4242
+ throw new SystemError("unknown-command", `unknown system command ${JSON.stringify(parsed.command)}; run rigscope system help`);
4243
+ } catch (error) {
4244
+ if (json) {
4245
+ console.log(formatSystemReport({ ok: false, error: error instanceof Error ? error.message : String(error), code: error instanceof SystemError ? error.code : "error" }));
4246
+ } else {
4247
+ console.error(`rigscope system: ${error instanceof Error ? error.message : String(error)}`);
4248
+ }
4249
+ return 1;
4250
+ }
4251
+ }
4252
+ function isMainModule() {
4253
+ try {
4254
+ return require.main === module;
4255
+ } catch {
4256
+ return true;
4257
+ }
4258
+ }
4259
+ if (isMainModule()) {
4260
+ runSystemCommand(process.argv.slice(2)).then((code) => {
4261
+ process.exitCode = code;
4262
+ }).catch((error) => {
4263
+ console.error(`rigscope system: ${error instanceof Error ? error.message : String(error)}`);
4264
+ process.exitCode = 1;
4265
+ });
4266
+ }
4267
+ // Annotate the CommonJS export names for ESM import in node:
4268
+ 0 && (module.exports = {
4269
+ COMPONENTS,
4270
+ DEFAULT_NODE_PATH,
4271
+ DEFAULT_SYSTEM_ACCOUNTS,
4272
+ POWER_STATE_FILE_NAME,
4273
+ POWER_SYSUSERS_NAME,
4274
+ RAPL_RULE_NAME,
4275
+ STATE_FILE_NAME,
4276
+ SYSTEM_SCOPE,
4277
+ SYSTEM_STATE_FORMAT_VERSION,
4278
+ SYSUSERS_NAME,
4279
+ SystemError,
4280
+ UNIT_NAMES,
4281
+ assertDirectoryStopped,
4282
+ assertManagedFileOwnership,
4283
+ collectorEnvPath,
4284
+ collectorStatePath,
4285
+ componentEnabled,
4286
+ componentSet,
4287
+ componentsForRole,
4288
+ connectDestinationUrl,
4289
+ connectSystem,
4290
+ createSystemHost,
4291
+ defaultSystemPaths,
4292
+ detectPowerDriver,
4293
+ detectUserInstallations,
4294
+ disableSystem,
4295
+ enableSystem,
4296
+ enabledComponents,
4297
+ formatSystemReport,
4298
+ grantPower,
4299
+ hubDatabasePath,
4300
+ hubEnrollmentPath,
4301
+ hubEnvPath,
4302
+ hubSocketPath,
4303
+ inspectPowerAccess,
4304
+ installSystem,
4305
+ powerStatus,
4306
+ powerSysusersPath,
4307
+ raplRulePath,
4308
+ readSystemState,
4309
+ renderPowerSysusers,
4310
+ renderRaplRule,
4311
+ renderSystemCollectorUnit,
4312
+ renderSystemHubUnit,
4313
+ renderSysusers,
4314
+ restartSystem,
4315
+ revokePower,
4316
+ rolesFor,
4317
+ rollbackSystem,
4318
+ runSystemCommand,
4319
+ startSystem,
4320
+ stateFilePath,
4321
+ stopSystem,
4322
+ systemStatus,
4323
+ systemdQuote,
4324
+ sysusersPath,
4325
+ uninstallSystem,
4326
+ unitFor,
4327
+ unitNamesFor,
4328
+ unitNamesForComponents,
4329
+ upgradeSystem,
4330
+ validateAccounts,
4331
+ validateSystemPaths,
4332
+ verifySystemHealth
4333
+ });