rigscope 0.1.1 → 0.1.3

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 (109) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +2 -2
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +3 -3
  13. package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  14. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
  15. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  16. package/.next/server/app/administration/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/administration.html +1 -1
  18. package/.next/server/app/administration.rsc +3 -3
  19. package/.next/server/app/administration.segments/_full.segment.rsc +3 -3
  20. package/.next/server/app/administration.segments/_tree.segment.rsc +2 -2
  21. package/.next/server/app/administration.segments/administration/__PAGE__.segment.rsc +3 -3
  22. package/.next/server/app/alerts/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/alerts.html +1 -1
  24. package/.next/server/app/alerts.rsc +3 -3
  25. package/.next/server/app/alerts.segments/_full.segment.rsc +3 -3
  26. package/.next/server/app/alerts.segments/_tree.segment.rsc +2 -2
  27. package/.next/server/app/alerts.segments/alerts/__PAGE__.segment.rsc +3 -3
  28. package/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  29. package/.next/server/app/connections.html +1 -1
  30. package/.next/server/app/connections.rsc +3 -3
  31. package/.next/server/app/connections.segments/_full.segment.rsc +3 -3
  32. package/.next/server/app/connections.segments/_tree.segment.rsc +2 -2
  33. package/.next/server/app/connections.segments/connections/__PAGE__.segment.rsc +3 -3
  34. package/.next/server/app/history/page.js +2 -2
  35. package/.next/server/app/history/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/history.html +1 -1
  37. package/.next/server/app/history.rsc +6 -6
  38. package/.next/server/app/history.segments/_full.segment.rsc +6 -6
  39. package/.next/server/app/history.segments/_tree.segment.rsc +2 -2
  40. package/.next/server/app/history.segments/history/__PAGE__.segment.rsc +6 -6
  41. package/.next/server/app/index.html +1 -1
  42. package/.next/server/app/index.rsc +3 -3
  43. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  44. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  45. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  46. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  47. package/.next/server/app/login.html +1 -1
  48. package/.next/server/app/login.rsc +3 -3
  49. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  50. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  51. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
  52. package/.next/server/app/machines/[machineId]/page_client-reference-manifest.js +1 -1
  53. package/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/server/app/processes/page.js +1 -1
  55. package/.next/server/app/processes/page_client-reference-manifest.js +1 -1
  56. package/.next/server/app/processes.html +1 -1
  57. package/.next/server/app/processes.rsc +4 -4
  58. package/.next/server/app/processes.segments/_full.segment.rsc +4 -4
  59. package/.next/server/app/processes.segments/_tree.segment.rsc +2 -2
  60. package/.next/server/app/processes.segments/processes/__PAGE__.segment.rsc +4 -4
  61. package/.next/server/app-paths-manifest.json +2 -2
  62. package/.next/server/chunks/161.js +1 -1
  63. package/.next/server/chunks/168.js +1 -1
  64. package/.next/server/middleware-build-manifest.js +1 -1
  65. package/.next/server/pages/404.html +1 -1
  66. package/.next/server/pages/500.html +1 -1
  67. package/.next/server/route-cache/APP_PAGE/16d1ab46157a868f438fa01e04bf53e805c71cddec593ea1535905c59625cd42/$/login.html +1 -0
  68. package/.next/server/route-cache/APP_PAGE/16d1ab46157a868f438fa01e04bf53e805c71cddec593ea1535905c59625cd42/$/login.meta +1 -0
  69. package/.next/server/route-cache/APP_PAGE/16d1ab46157a868f438fa01e04bf53e805c71cddec593ea1535905c59625cd42/$/login.rsc +26 -0
  70. package/.next/server/route-cache/APP_PAGE/16d1ab46157a868f438fa01e04bf53e805c71cddec593ea1535905c59625cd42/$/login.segments/_full.segment.rsc +26 -0
  71. package/.next/server/route-cache/APP_PAGE/16d1ab46157a868f438fa01e04bf53e805c71cddec593ea1535905c59625cd42/$/login.segments/_tree.segment.rsc +4 -0
  72. package/.next/server/route-cache/APP_PAGE/16d1ab46157a868f438fa01e04bf53e805c71cddec593ea1535905c59625cd42/$/login.segments/login/__PAGE__.segment.rsc +32 -0
  73. package/.next/server/route-cache/APP_PAGE/41da0ef926a63354a29f601d8e8d0a5951196c45ebcd28b7d4708fb7f56f3bf1/$/_not-found.html +1 -0
  74. package/.next/server/route-cache/APP_PAGE/41da0ef926a63354a29f601d8e8d0a5951196c45ebcd28b7d4708fb7f56f3bf1/$/_not-found.meta +1 -0
  75. package/.next/server/route-cache/APP_PAGE/41da0ef926a63354a29f601d8e8d0a5951196c45ebcd28b7d4708fb7f56f3bf1/$/_not-found.rsc +24 -0
  76. package/.next/server/route-cache/APP_PAGE/41da0ef926a63354a29f601d8e8d0a5951196c45ebcd28b7d4708fb7f56f3bf1/$/_not-found.segments/_full.segment.rsc +24 -0
  77. package/.next/server/route-cache/APP_PAGE/41da0ef926a63354a29f601d8e8d0a5951196c45ebcd28b7d4708fb7f56f3bf1/$/_not-found.segments/_not-found/__PAGE__.segment.rsc +30 -0
  78. package/.next/server/route-cache/APP_PAGE/41da0ef926a63354a29f601d8e8d0a5951196c45ebcd28b7d4708fb7f56f3bf1/$/_not-found.segments/_tree.segment.rsc +4 -0
  79. package/.next/server/route-cache/APP_PAGE/c6377dd9ce6a1c2cd100eb4344e843eb4639b9ed26f84643f77680e9eb454c4b/$/index.html +1 -0
  80. package/.next/server/route-cache/APP_PAGE/c6377dd9ce6a1c2cd100eb4344e843eb4639b9ed26f84643f77680e9eb454c4b/$/index.meta +1 -0
  81. package/.next/server/route-cache/APP_PAGE/c6377dd9ce6a1c2cd100eb4344e843eb4639b9ed26f84643f77680e9eb454c4b/$/index.rsc +26 -0
  82. package/.next/server/route-cache/APP_PAGE/c6377dd9ce6a1c2cd100eb4344e843eb4639b9ed26f84643f77680e9eb454c4b/$/index.segments/__PAGE__.segment.rsc +31 -0
  83. package/.next/server/route-cache/APP_PAGE/c6377dd9ce6a1c2cd100eb4344e843eb4639b9ed26f84643f77680e9eb454c4b/$/index.segments/_full.segment.rsc +26 -0
  84. package/.next/server/route-cache/APP_PAGE/c6377dd9ce6a1c2cd100eb4344e843eb4639b9ed26f84643f77680e9eb454c4b/$/index.segments/_tree.segment.rsc +3 -0
  85. package/.next/static/chunks/408-4fe7147e38981ee1.js +1 -0
  86. package/.next/static/chunks/939-514e554087419c71.js +1 -0
  87. package/.next/static/chunks/app/history/page-69ea64476ee5db85.js +3 -0
  88. package/.next/static/chunks/app/processes/{page-e471ed23a1d5632f.js → page-0e98a4a64e829c5f.js} +1 -1
  89. package/.next/static/css/3fbf75e4c471c6a8.css +1 -0
  90. package/ARCHITECTURE.md +30 -4
  91. package/CHANGELOG.md +47 -2
  92. package/dist/cli.cjs +1627 -1042
  93. package/dist/collector.cjs +138 -226
  94. package/dist/hub.cjs +7 -20
  95. package/dist/ops.cjs +160 -33
  96. package/dist/system.cjs +869 -450
  97. package/docs/bottlenecks.md +4 -6
  98. package/docs/configuration.md +2 -0
  99. package/docs/installation.md +42 -7
  100. package/docs/operations.md +32 -16
  101. package/docs/releasing.md +21 -17
  102. package/docs/system-installation.md +11 -2
  103. package/package.json +1 -2
  104. package/.next/static/chunks/408-dc6158e0bb450ef6.js +0 -1
  105. package/.next/static/chunks/939-2600d7c8e583d5ef.js +0 -1
  106. package/.next/static/chunks/app/history/page-1475454737279ee0.js +0 -3
  107. package/.next/static/css/e45c0c7ef5ba0336.css +0 -1
  108. /package/.next/static/{NE6PGEiJxUnKRceuzUWPc → xjSDCOO8Gpm5iPO9tyRG5}/_buildManifest.js +0 -0
  109. /package/.next/static/{NE6PGEiJxUnKRceuzUWPc → xjSDCOO8Gpm5iPO9tyRG5}/_ssgManifest.js +0 -0
package/dist/system.cjs CHANGED
@@ -28,7 +28,6 @@ __export(system_exports, {
28
28
  RAPL_RULE_NAME: () => RAPL_RULE_NAME,
29
29
  STATE_FILE_NAME: () => STATE_FILE_NAME,
30
30
  SYSTEM_SCOPE: () => SYSTEM_SCOPE,
31
- SYSTEM_STATE_FORMAT_VERSION: () => SYSTEM_STATE_FORMAT_VERSION,
32
31
  SYSUSERS_NAME: () => SYSUSERS_NAME,
33
32
  SystemError: () => SystemError,
34
33
  UNIT_NAMES: () => UNIT_NAMES,
@@ -73,6 +72,7 @@ __export(system_exports, {
73
72
  startSystem: () => startSystem,
74
73
  stateFilePath: () => stateFilePath,
75
74
  stopSystem: () => stopSystem,
75
+ systemStateIsLegacy: () => systemStateIsLegacy,
76
76
  systemStatus: () => systemStatus,
77
77
  systemdQuote: () => systemdQuote,
78
78
  sysusersPath: () => sysusersPath,
@@ -89,7 +89,7 @@ module.exports = __toCommonJS(system_exports);
89
89
  var import_node_fs3 = require("node:fs");
90
90
  var import_node_child_process = require("node:child_process");
91
91
  var import_node_crypto5 = require("node:crypto");
92
- var import_node_path3 = require("node:path");
92
+ var import_node_path4 = require("node:path");
93
93
  var import_node_sqlite = require("node:sqlite");
94
94
  var import_node_util = require("node:util");
95
95
 
@@ -97,12 +97,12 @@ var import_node_util = require("node:util");
97
97
  var import_node_http = require("node:http");
98
98
  var MAX_HEALTH_BODY_BYTES = 64 * 1024;
99
99
  function probeHealth(target, timeoutMs = 3e3) {
100
- return new Promise((resolve3) => {
100
+ return new Promise((resolve4) => {
101
101
  let settled = false;
102
102
  const finish = (result) => {
103
103
  if (settled) return;
104
104
  settled = true;
105
- resolve3(result);
105
+ resolve4(result);
106
106
  };
107
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
108
  let req;
@@ -185,7 +185,7 @@ function bounded(value, min, max) {
185
185
  return Math.min(max, Math.max(min, Number.isFinite(value) ? value : min));
186
186
  }
187
187
  function delay(ms) {
188
- return new Promise((resolve3) => setTimeout(resolve3, ms));
188
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
189
189
  }
190
190
  function errorText(error) {
191
191
  return error instanceof Error ? error.message : String(error);
@@ -390,7 +390,6 @@ var ConfigError = class extends Error {
390
390
  var DEFAULT_COLLECTOR_ID = "mock-collector";
391
391
  var MOCK_HOSTNAME = "mock-machine";
392
392
  var CONNECT_PATH = "/api/collector/connect";
393
- var PREVIOUS_CONNECT_PATH = "/api/collector/v1/connect";
394
393
  var MAX_DESTINATIONS = 8;
395
394
  var DEFAULT_INTERVAL_MS = 1e3;
396
395
  var MIN_INTERVAL_MS = 100;
@@ -467,7 +466,6 @@ function validateHubUrl(raw, label, issues) {
467
466
  issues.push(`${label}: URL must include a host`);
468
467
  return null;
469
468
  }
470
- if (parsed.pathname === PREVIOUS_CONNECT_PATH) parsed.pathname = CONNECT_PATH;
471
469
  if (parsed.pathname !== CONNECT_PATH) {
472
470
  issues.push(`${label}: path must be exactly ${CONNECT_PATH}`);
473
471
  return null;
@@ -634,18 +632,26 @@ var import_node_path2 = require("node:path");
634
632
  function readState(path) {
635
633
  const stat = (0, import_node_fs2.lstatSync)(path);
636
634
  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");
635
+ const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
636
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid collector state");
637
+ const raw = parsed;
638
+ if (Object.hasOwn(raw, "version") || Object.hasOwn(raw, "formatVersion")) {
639
+ throw new Error("collector state uses an older versioned shape; run rigscope install or upgrade to reset it");
640
+ }
641
+ if (typeof raw.collectorId !== "string" || !/^[\x21-\x7e]{1,128}$/.test(raw.collectorId) || !Array.isArray(raw.destinations)) throw new Error("invalid collector state");
639
642
  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) => {
643
+ const pending = raw.pendingEnrollments;
644
+ if (pending !== void 0 && (!Array.isArray(pending) || pending.length > 8 || pending.some((p) => !p || typeof p !== "object" || 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)))) {
645
+ throw new Error("invalid pending enrollment recovery state");
646
+ }
647
+ const pendingEnrollments = pending?.map((value) => {
648
+ const pendingEnrollment = value;
642
649
  const issues = [];
643
- const url = validateHubUrl(pending.url, "pending enrollment", issues);
650
+ const url = validateHubUrl(pendingEnrollment.url, "pending enrollment", issues);
644
651
  if (!url || issues.length) throw new ConfigError(issues);
645
- return { ...pending, url: url.href };
652
+ return { ...pendingEnrollment, url: url.href };
646
653
  });
647
654
  return {
648
- version: 1,
649
655
  collectorId: raw.collectorId,
650
656
  destinations,
651
657
  ...pendingEnrollments === void 0 ? {} : { pendingEnrollments }
@@ -678,9 +684,14 @@ function updateInstallation(path, change = (s) => s) {
678
684
  state = readState(path);
679
685
  } catch (error) {
680
686
  if (error.code !== "ENOENT") throw error;
681
- state = { version: 1, collectorId: (0, import_node_crypto3.randomUUID)(), destinations: [] };
687
+ state = { collectorId: (0, import_node_crypto3.randomUUID)(), destinations: [] };
682
688
  }
683
- const next = change(state);
689
+ const changed = change(state);
690
+ const next = {
691
+ collectorId: changed.collectorId,
692
+ destinations: changed.destinations,
693
+ ...changed.pendingEnrollments === void 0 ? {} : { pendingEnrollments: changed.pendingEnrollments }
694
+ };
684
695
  temporary = `${path}.${(0, import_node_crypto3.randomUUID)()}.tmp`;
685
696
  const out = (0, import_node_fs2.openSync)(temporary, "wx", 384);
686
697
  try {
@@ -775,8 +786,241 @@ async function enrollCollector(path, connectUrl, enrollmentToken, sharing, repla
775
786
  }
776
787
  }
777
788
 
789
+ // src/hub/config.ts
790
+ var import_node_net = require("node:net");
791
+ var import_node_path3 = require("node:path");
792
+ var MAX_HUB_COLLECTORS_BYTES = 1024 * 1024;
793
+ var MAX_COLLECTOR_ID_LENGTH = 128;
794
+ var MAX_MACHINE_ID_LENGTH = 128;
795
+ var MAX_TOKEN_LENGTH2 = 256;
796
+ var MAX_UNIX_SOCKET_PATH_BYTES = 100;
797
+ var MAX_PUBLIC_ORIGIN_LENGTH = 512;
798
+ var HubConfigError = class extends Error {
799
+ constructor(message) {
800
+ super(message);
801
+ this.name = "HubConfigError";
802
+ }
803
+ };
804
+ function parseHubConfig(env) {
805
+ const development = env.NODE_ENV === "development";
806
+ const collectors = parseCollectors(env.HUB_COLLECTORS);
807
+ const publicOrigin = nonEmpty(env.HUB_PUBLIC_ORIGIN);
808
+ const socketPath = nonEmpty(env.HUB_SOCKET_PATH);
809
+ let maxCollectorConnections;
810
+ if (env.HUB_MAX_COLLECTOR_CONNECTIONS !== void 0) {
811
+ const value = env.HUB_MAX_COLLECTOR_CONNECTIONS;
812
+ const parsed = Number(value);
813
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(parsed) || parsed < 1) {
814
+ throw new HubConfigError("HUB_MAX_COLLECTOR_CONNECTIONS must be a positive safe integer");
815
+ }
816
+ maxCollectorConnections = parsed;
817
+ }
818
+ const connectionBudget = maxCollectorConnections === void 0 ? {} : { maxCollectorConnections };
819
+ if (publicOrigin === void 0 !== (socketPath === void 0)) {
820
+ throw new HubConfigError(
821
+ "HUB_PUBLIC_ORIGIN and HUB_SOCKET_PATH must be set together: remote mode needs both the exact HTTPS origin and the Unix socket path"
822
+ );
823
+ }
824
+ if (publicOrigin !== void 0 && socketPath !== void 0) {
825
+ if (nonEmpty(env.HUB_HOST) !== void 0 || nonEmpty(env.HUB_PORT) !== void 0) {
826
+ throw new HubConfigError(
827
+ "HUB_HOST/HUB_PORT are not valid in remote mode; the hub listens only on HUB_SOCKET_PATH"
828
+ );
829
+ }
830
+ return {
831
+ // Keep stable local defaults for callers that inspect these fields;
832
+ // main.ts never creates a TCP listener when `remote` is set.
833
+ host: "127.0.0.1",
834
+ port: 8081,
835
+ collectors,
836
+ development,
837
+ remote: parseRemoteConfig(publicOrigin, socketPath),
838
+ ...connectionBudget
839
+ };
840
+ }
841
+ return {
842
+ host: parseHost(env.HUB_HOST),
843
+ port: parsePort(env.HUB_PORT),
844
+ collectors,
845
+ development,
846
+ remote: null,
847
+ ...connectionBudget
848
+ };
849
+ }
850
+ function nonEmpty(value) {
851
+ return value === void 0 || value === "" ? void 0 : value;
852
+ }
853
+ function parsePublicOrigin(value) {
854
+ if (value.length > MAX_PUBLIC_ORIGIN_LENGTH) {
855
+ throw new HubConfigError(
856
+ `HUB_PUBLIC_ORIGIN must be at most ${MAX_PUBLIC_ORIGIN_LENGTH} characters`
857
+ );
858
+ }
859
+ if (!/^[\x21-\x7e]+$/.test(value)) {
860
+ throw new HubConfigError(
861
+ "HUB_PUBLIC_ORIGIN must be printable ASCII without spaces or control characters"
862
+ );
863
+ }
864
+ let parsed;
865
+ try {
866
+ parsed = new URL(value);
867
+ } catch {
868
+ throw new HubConfigError(
869
+ `HUB_PUBLIC_ORIGIN "${value}" is not a valid absolute origin`
870
+ );
871
+ }
872
+ if (parsed.protocol !== "https:") {
873
+ throw new HubConfigError("HUB_PUBLIC_ORIGIN must use https://");
874
+ }
875
+ if (parsed.username !== "" || parsed.password !== "") {
876
+ throw new HubConfigError("HUB_PUBLIC_ORIGIN must not contain userinfo");
877
+ }
878
+ if (parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "") {
879
+ throw new HubConfigError(
880
+ "HUB_PUBLIC_ORIGIN must be exactly scheme://host[:port] with no path, query, or fragment"
881
+ );
882
+ }
883
+ if (parsed.origin !== value) {
884
+ throw new HubConfigError(
885
+ `HUB_PUBLIC_ORIGIN must be canonical; use "${parsed.origin}"`
886
+ );
887
+ }
888
+ if (parsed.hostname.length === 0 || parsed.hostname.endsWith(".")) {
889
+ throw new HubConfigError("HUB_PUBLIC_ORIGIN must name a nonempty host");
890
+ }
891
+ if (parsed.host.length > 255) {
892
+ throw new HubConfigError("HUB_PUBLIC_ORIGIN host is too long");
893
+ }
894
+ if (parsed.port === "0") {
895
+ throw new HubConfigError("HUB_PUBLIC_ORIGIN must use a nonzero public port");
896
+ }
897
+ return {
898
+ publicOrigin: value,
899
+ hostHeader: parsed.host,
900
+ hostname: parsed.hostname,
901
+ port: parsed.port === "" ? 443 : Number(parsed.port)
902
+ };
903
+ }
904
+ function parseRemoteConfig(publicOrigin, socketPath) {
905
+ const origin = parsePublicOrigin(publicOrigin);
906
+ validateUnixSocketPathSyntax(socketPath);
907
+ return { ...origin, socketPath };
908
+ }
909
+ function validateUnixSocketPathSyntax(value) {
910
+ if (typeof value !== "string" || value.length === 0) {
911
+ throw new HubConfigError("HUB_SOCKET_PATH must be a nonempty absolute path");
912
+ }
913
+ if (Buffer.byteLength(value, "utf8") > MAX_UNIX_SOCKET_PATH_BYTES) {
914
+ throw new HubConfigError(
915
+ `HUB_SOCKET_PATH must be at most ${MAX_UNIX_SOCKET_PATH_BYTES} bytes for a Unix socket`
916
+ );
917
+ }
918
+ if (/[\u0000-\u001f\u007f]/.test(value)) {
919
+ throw new HubConfigError(
920
+ "HUB_SOCKET_PATH must not contain NUL or control characters"
921
+ );
922
+ }
923
+ if (!(0, import_node_path3.isAbsolute)(value)) {
924
+ throw new HubConfigError("HUB_SOCKET_PATH must be an absolute path");
925
+ }
926
+ if (value !== (0, import_node_path3.resolve)(value)) {
927
+ throw new HubConfigError("HUB_SOCKET_PATH must be a normalized absolute path");
928
+ }
929
+ const name = (0, import_node_path3.basename)(value);
930
+ if (name === "" || name === "." || name === ".." || value === "/") {
931
+ throw new HubConfigError("HUB_SOCKET_PATH must name a socket file");
932
+ }
933
+ }
934
+ function parseHost(value) {
935
+ const host = value === void 0 || value === "" ? "127.0.0.1" : value;
936
+ if (typeof host !== "string" || host.length === 0 || host.length > 45) {
937
+ throw new HubConfigError(`HUB_HOST must be a literal loopback address`);
938
+ }
939
+ if (isIPv4Loopback(host)) return host;
940
+ if (host === "::1") return host;
941
+ throw new HubConfigError(
942
+ `HUB_HOST "${host}" is not a literal loopback address; only 127.0.0.0/8 and ::1 are allowed`
943
+ );
944
+ }
945
+ function isIPv4Loopback(host) {
946
+ if ((0, import_node_net.isIP)(host) !== 4) return false;
947
+ const parts = host.split(".");
948
+ if (parts[0] !== "127" || parts.length !== 4) return false;
949
+ return parts.every((part) => /^\d+$/.test(part) && Number(part) <= 255);
950
+ }
951
+ function parsePort(value) {
952
+ const text = value === void 0 || value === "" ? "8081" : value.trim();
953
+ if (!/^\d+$/.test(text)) {
954
+ throw new HubConfigError(`HUB_PORT must be a nonnegative integer, got "${text}"`);
955
+ }
956
+ const port = Number(text);
957
+ if (port > 65535) {
958
+ throw new HubConfigError(`HUB_PORT must be at most 65535, got ${text}`);
959
+ }
960
+ return port;
961
+ }
962
+ function parseCollectors(value) {
963
+ if (value === void 0 || value === "") return [];
964
+ if (Buffer.byteLength(value, "utf8") > MAX_HUB_COLLECTORS_BYTES) {
965
+ throw new HubConfigError(
966
+ `HUB_COLLECTORS must be at most ${MAX_HUB_COLLECTORS_BYTES} bytes`
967
+ );
968
+ }
969
+ let parsed;
970
+ try {
971
+ parsed = JSON.parse(value);
972
+ } catch {
973
+ throw new HubConfigError(`HUB_COLLECTORS is not valid JSON`);
974
+ }
975
+ if (!Array.isArray(parsed)) {
976
+ throw new HubConfigError(`HUB_COLLECTORS must be a JSON array`);
977
+ }
978
+ const collectors = [];
979
+ const ids = /* @__PURE__ */ new Set();
980
+ const machineIds = /* @__PURE__ */ new Set();
981
+ const tokens = /* @__PURE__ */ new Set();
982
+ for (const [index, item] of parsed.entries()) {
983
+ const message = `HUB_COLLECTORS[${index}]`;
984
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
985
+ throw new HubConfigError(`${message} must be an object`);
986
+ }
987
+ const record = item;
988
+ const keys = Object.keys(record).sort();
989
+ const expected = ["collectorId", "machineId", "token"].sort();
990
+ if (keys.length !== expected.length || keys.some((key, i) => key !== expected[i])) {
991
+ throw new HubConfigError(
992
+ `${message} must contain exactly collectorId, machineId, and token`
993
+ );
994
+ }
995
+ const collectorId = boundedString(record.collectorId, "collectorId", message, MAX_COLLECTOR_ID_LENGTH);
996
+ const machineId = boundedString(record.machineId, "machineId", message, MAX_MACHINE_ID_LENGTH);
997
+ const token = boundedString(record.token, "token", message, MAX_TOKEN_LENGTH2);
998
+ if (ids.has(collectorId)) {
999
+ throw new HubConfigError(`${message}: duplicate collectorId "${collectorId}"`);
1000
+ }
1001
+ if (machineIds.has(machineId)) {
1002
+ throw new HubConfigError(`${message}: duplicate machineId "${machineId}"`);
1003
+ }
1004
+ if (tokens.has(token)) {
1005
+ throw new HubConfigError(`${message}: duplicate token`);
1006
+ }
1007
+ ids.add(collectorId);
1008
+ machineIds.add(machineId);
1009
+ tokens.add(token);
1010
+ collectors.push({ collectorId, machineId, token });
1011
+ }
1012
+ return collectors;
1013
+ }
1014
+ function boundedString(value, field, message, maxLength) {
1015
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
1016
+ throw new HubConfigError(
1017
+ `${message}: ${field} must be a nonempty string of at most ${maxLength} characters`
1018
+ );
1019
+ }
1020
+ return value;
1021
+ }
1022
+
778
1023
  // src/cli/system.ts
779
- var SYSTEM_STATE_FORMAT_VERSION = 2;
780
1024
  var SYSTEM_SCOPE = "system";
781
1025
  var DEFAULT_SYSTEM_ACCOUNTS = {
782
1026
  hub: "rigscope-hub",
@@ -788,6 +1032,7 @@ var UNIT_NAMES = {
788
1032
  hub: "rigscope-hub.service",
789
1033
  collector: "rigscope-collector.service"
790
1034
  };
1035
+ var LEGACY_UNIT_NAMES = ["rigscope.service"];
791
1036
  var RAPL_RULE_NAME = "90-rigscope-rapl.rules";
792
1037
  var SYSUSERS_NAME = "rigscope.conf";
793
1038
  var POWER_SYSUSERS_NAME = "rigscope-rapl.conf";
@@ -884,13 +1129,13 @@ function createSystemHost(overrides = {}) {
884
1129
  }
885
1130
  function defaultSystemPaths(overrides = {}) {
886
1131
  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")
1132
+ prefix: (0, import_node_path4.resolve)(overrides.prefix ?? "/opt/rigscope"),
1133
+ configDir: (0, import_node_path4.resolve)(overrides.configDir ?? "/etc/rigscope"),
1134
+ stateDir: (0, import_node_path4.resolve)(overrides.stateDir ?? "/var/lib/rigscope"),
1135
+ unitsDir: (0, import_node_path4.resolve)(overrides.unitsDir ?? "/etc/systemd/system"),
1136
+ sysusersDir: (0, import_node_path4.resolve)(overrides.sysusersDir ?? "/etc/sysusers.d"),
1137
+ udevRulesDir: (0, import_node_path4.resolve)(overrides.udevRulesDir ?? "/etc/udev/rules.d"),
1138
+ powercapRoot: (0, import_node_path4.resolve)(overrides.powercapRoot ?? "/sys/class/powercap")
894
1139
  };
895
1140
  }
896
1141
  function unitNamesFor(role) {
@@ -905,11 +1150,11 @@ function rolesFor(role) {
905
1150
  return { hub: role !== "collector", collector: role !== "hub" };
906
1151
  }
907
1152
  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);
1153
+ const rel = (0, import_node_path4.relative)((0, import_node_path4.resolve)(parent), (0, import_node_path4.resolve)(candidate));
1154
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${import_node_path4.sep}`) && !(0, import_node_path4.isAbsolute)(rel);
910
1155
  }
911
1156
  function ensureAbsolute(path, label) {
912
- const absolute = (0, import_node_path3.resolve)(path);
1157
+ const absolute = (0, import_node_path4.resolve)(path);
913
1158
  if (/[\0\r\n]/.test(absolute)) throw new SystemError("unsafe-path", `${label} contains control characters`);
914
1159
  if (absolute === "/") throw new SystemError("unsafe-path", `${label} must not be the filesystem root`);
915
1160
  for (const forbidden of ["/home", "/root", "/proc", "/sys", "/dev", "/boot"]) {
@@ -923,7 +1168,7 @@ function validateSystemPaths(input = {}) {
923
1168
  const paths = defaultSystemPaths(input);
924
1169
  for (const [label, value] of Object.entries(paths)) {
925
1170
  if (label === "powercapRoot") {
926
- if (!(0, import_node_path3.isAbsolute)(value) || /[\0\r\n]/.test(value) || (0, import_node_path3.resolve)(value) === "/") {
1171
+ if (!(0, import_node_path4.isAbsolute)(value) || /[\0\r\n]/.test(value) || (0, import_node_path4.resolve)(value) === "/") {
927
1172
  throw new SystemError("unsafe-path", `powercap root is unsafe: ${value}`);
928
1173
  }
929
1174
  } else {
@@ -982,8 +1227,8 @@ function sha256File(path) {
982
1227
  return (0, import_node_crypto5.createHash)("sha256").update((0, import_node_fs3.readFileSync)(path)).digest("hex");
983
1228
  }
984
1229
  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)}`);
1230
+ (0, import_node_fs3.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true, mode: 493 });
1231
+ const temporary = (0, import_node_path4.join)((0, import_node_path4.dirname)(path), `.${(0, import_node_path4.basename)(path)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
987
1232
  try {
988
1233
  (0, import_node_fs3.writeFileSync)(temporary, content, { mode });
989
1234
  (0, import_node_fs3.chmodSync)(temporary, mode);
@@ -1115,38 +1360,39 @@ function connectDestinationUrl(value) {
1115
1360
  }
1116
1361
  if (url.search !== "" || url.hash !== "") throw new SystemError("invalid-url", "Hub URLs must not contain a query or fragment.");
1117
1362
  if (url.hostname === "localhost") url.hostname = "127.0.0.1";
1118
- if (url.pathname === "/") url.pathname = "/api/collector/connect";
1363
+ if (url.pathname === "/") url.pathname = CONNECT_PATH;
1364
+ if (url.pathname !== CONNECT_PATH) throw new SystemError("invalid-url", `Collector endpoint must be ${CONNECT_PATH}.`);
1119
1365
  return url.href;
1120
1366
  }
1121
1367
  function hubDatabasePath(paths) {
1122
- return (0, import_node_path3.join)(paths.stateDir, "hub", "telemetry.sqlite");
1368
+ return (0, import_node_path4.join)(paths.stateDir, "hub", "telemetry.sqlite");
1123
1369
  }
1124
1370
  function hubEnrollmentPath(paths) {
1125
- return (0, import_node_path3.join)(paths.stateDir, "hub", "enrollment.json");
1371
+ return (0, import_node_path4.join)(paths.stateDir, "hub", "enrollment.json");
1126
1372
  }
1127
1373
  function hubSocketPath(paths) {
1128
- return (0, import_node_path3.join)(paths.stateDir, "hub", "run", "hub.sock");
1374
+ return (0, import_node_path4.join)(paths.stateDir, "hub", "run", "hub.sock");
1129
1375
  }
1130
1376
  function collectorStatePath(paths) {
1131
- return (0, import_node_path3.join)(paths.stateDir, "collector", "state.json");
1377
+ return (0, import_node_path4.join)(paths.stateDir, "collector", "state.json");
1132
1378
  }
1133
1379
  function hubEnvPath(paths) {
1134
- return (0, import_node_path3.join)(paths.configDir, "hub.env");
1380
+ return (0, import_node_path4.join)(paths.configDir, "hub.env");
1135
1381
  }
1136
1382
  function collectorEnvPath(paths) {
1137
- return (0, import_node_path3.join)(paths.configDir, "collector.env");
1383
+ return (0, import_node_path4.join)(paths.configDir, "collector.env");
1138
1384
  }
1139
1385
  function sysusersPath(paths) {
1140
- return (0, import_node_path3.join)(paths.sysusersDir, SYSUSERS_NAME);
1386
+ return (0, import_node_path4.join)(paths.sysusersDir, SYSUSERS_NAME);
1141
1387
  }
1142
1388
  function powerSysusersPath(paths) {
1143
- return (0, import_node_path3.join)(paths.sysusersDir, POWER_SYSUSERS_NAME);
1389
+ return (0, import_node_path4.join)(paths.sysusersDir, POWER_SYSUSERS_NAME);
1144
1390
  }
1145
1391
  function raplRulePath(paths) {
1146
- return (0, import_node_path3.join)(paths.udevRulesDir, RAPL_RULE_NAME);
1392
+ return (0, import_node_path4.join)(paths.udevRulesDir, RAPL_RULE_NAME);
1147
1393
  }
1148
1394
  function stateFilePath(paths) {
1149
- return (0, import_node_path3.join)(paths.stateDir, STATE_FILE_NAME);
1395
+ return (0, import_node_path4.join)(paths.stateDir, STATE_FILE_NAME);
1150
1396
  }
1151
1397
  function readCollectorIdentity(path) {
1152
1398
  if (!(0, import_node_fs3.existsSync)(path)) return null;
@@ -1161,7 +1407,10 @@ function readCollectorIdentity(path) {
1161
1407
  }
1162
1408
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new SystemError("invalid-file", `${path} is invalid`);
1163
1409
  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)) {
1410
+ if (Object.prototype.hasOwnProperty.call(record, "version") || Object.prototype.hasOwnProperty.call(record, "formatVersion")) {
1411
+ throw new SystemError("invalid-file", `${path} is pre-canonical; reinstall or reset collector state`);
1412
+ }
1413
+ if (typeof record.collectorId !== "string" || !/^[\x21-\x7e]{1,128}$/.test(record.collectorId) || !Array.isArray(record.destinations)) {
1165
1414
  throw new SystemError("invalid-file", `${path} is not a collector installation state`);
1166
1415
  }
1167
1416
  const destinations = [];
@@ -1173,16 +1422,20 @@ function readCollectorIdentity(path) {
1173
1422
  if (typeof destination.url !== "string" || typeof destination.token !== "string" || destination.token === "") {
1174
1423
  throw new SystemError("invalid-file", `${path} contains an invalid destination`);
1175
1424
  }
1425
+ const issues = [];
1426
+ if (!validateHubUrl(destination.url, "saved collector destination", issues)) {
1427
+ throw new SystemError("invalid-file", `${path} contains an incompatible collector endpoint; reset or reinstall it`);
1428
+ }
1176
1429
  destinations.push({
1177
1430
  url: destination.url,
1178
1431
  token: destination.token,
1179
1432
  ...destination.sharing !== null && typeof destination.sharing === "object" && !Array.isArray(destination.sharing) ? { sharing: destination.sharing } : {}
1180
1433
  });
1181
1434
  }
1182
- return { version: 1, collectorId: record.collectorId, destinations };
1435
+ return { collectorId: record.collectorId, destinations };
1183
1436
  }
1184
1437
  function writeCollectorIdentity(path, identity) {
1185
- ensureDirectory((0, import_node_path3.dirname)(path), 448, true);
1438
+ ensureDirectory((0, import_node_path4.dirname)(path), 448, true);
1186
1439
  atomicWrite(path, `${JSON.stringify(identity)}
1187
1440
  `, 384);
1188
1441
  }
@@ -1355,22 +1608,20 @@ async function pairCollectorWithHub(options) {
1355
1608
  note: "the local hub has no reachable loopback or HTTPS destination"
1356
1609
  };
1357
1610
  }
1358
- if (!(0, import_node_fs3.existsSync)((0, import_node_path3.join)(paths.unitsDir, UNIT_NAMES.hub))) {
1611
+ if (!(0, import_node_fs3.existsSync)((0, import_node_path4.join)(paths.unitsDir, UNIT_NAMES.hub))) {
1359
1612
  return { paired: false, awaiting: true, hubWasActive: false, note: "the local hub unit is not installed" };
1360
1613
  }
1361
1614
  const hubWasActive = systemdActive(host, UNIT_NAMES.hub);
1362
1615
  if (hubWasActive) {
1363
1616
  if (existing !== null) {
1364
- writeCollectorIdentity(collectorStatePath(paths), existing);
1365
1617
  const registered = findCollectorInHubDatabase(paths, existing.collectorId);
1366
1618
  if (registered !== null) {
1367
1619
  const adopted = {
1368
- version: 1,
1369
1620
  collectorId: existing.collectorId,
1370
1621
  destinations: [{ url: destination.url, token: registered.token, sharing: { ...SYSTEM_DEFAULT_SHARING } }]
1371
1622
  };
1372
1623
  writeCollectorIdentity(collectorStatePath(paths), adopted);
1373
- chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1624
+ chownAccount([(0, import_node_path4.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1374
1625
  return { paired: true, awaiting: false, hubWasActive, destination: destination.url };
1375
1626
  }
1376
1627
  }
@@ -1378,7 +1629,7 @@ async function pairCollectorWithHub(options) {
1378
1629
  try {
1379
1630
  grant = createLocalEnrollmentGrant(paths, host);
1380
1631
  } catch (error) {
1381
- chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1632
+ chownAccount([(0, import_node_path4.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1382
1633
  return {
1383
1634
  paired: false,
1384
1635
  awaiting: true,
@@ -1396,12 +1647,12 @@ async function pairCollectorWithHub(options) {
1396
1647
  note: `enrollment through the live hub failed: ${error instanceof Error ? error.message : String(error)}`
1397
1648
  };
1398
1649
  } finally {
1399
- chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1650
+ chownAccount([(0, import_node_path4.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1400
1651
  }
1401
1652
  if (!collectorConfigured(paths).configured) {
1402
1653
  return { paired: false, awaiting: true, hubWasActive, note: "the hub accepted enrollment but no destination was saved" };
1403
1654
  }
1404
- chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1655
+ chownAccount([(0, import_node_path4.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1405
1656
  return { paired: true, awaiting: false, hubWasActive, destination: destination.url };
1406
1657
  }
1407
1658
  const identity = {
@@ -1425,7 +1676,7 @@ async function pairCollectorWithHub(options) {
1425
1676
  }
1426
1677
  }
1427
1678
  }
1428
- const next = existing ?? { version: 1, collectorId: identity.collectorId, destinations: [] };
1679
+ const next = existing ?? { collectorId: identity.collectorId, destinations: [] };
1429
1680
  next.destinations = [
1430
1681
  {
1431
1682
  url: destination.url,
@@ -1434,7 +1685,7 @@ async function pairCollectorWithHub(options) {
1434
1685
  }
1435
1686
  ];
1436
1687
  writeCollectorIdentity(collectorStatePath(paths), next);
1437
- chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1688
+ chownAccount([(0, import_node_path4.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
1438
1689
  return { paired: true, awaiting: false, hubWasActive, destination: destination.url };
1439
1690
  }
1440
1691
  function unitCommon(name, description, paths, account, envPath, execStart, extraService = []) {
@@ -1451,7 +1702,7 @@ function unitCommon(name, description, paths, account, envPath, execStart, extra
1451
1702
  "Type=simple",
1452
1703
  `User=${account}`,
1453
1704
  `Group=${account}`,
1454
- `WorkingDirectory=${(0, import_node_path3.join)(paths.prefix, "current").replace(/%/g, "%%")}`,
1705
+ `WorkingDirectory=${(0, import_node_path4.join)(paths.prefix, "current").replace(/%/g, "%%")}`,
1455
1706
  `EnvironmentFile=${envPath.replace(/%/g, "%%")}`,
1456
1707
  "Environment=NODE_ENV=production",
1457
1708
  "Restart=on-failure",
@@ -1486,13 +1737,13 @@ function unitCommon(name, description, paths, account, envPath, execStart, extra
1486
1737
  }
1487
1738
  function renderSystemHubUnit(options) {
1488
1739
  const { paths, accounts } = options;
1489
- const hubScript = (0, import_node_path3.join)(paths.prefix, "current", "dist", "hub.cjs");
1740
+ const hubScript = (0, import_node_path4.join)(paths.prefix, "current", "dist", "hub.cjs");
1490
1741
  const extra = [
1491
1742
  `Environment=${systemdQuote(`HUB_DATABASE_PATH=${options.database}`)}`,
1492
1743
  `Environment=${systemdQuote(`HUB_STATE_FILE=${options.enrollment}`)}`,
1493
1744
  ...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"))}`,
1745
+ `ReadWritePaths=${systemdQuote((0, import_node_path4.join)(paths.stateDir, "hub"))}`,
1746
+ `ReadOnlyPaths=${systemdQuote((0, import_node_path4.join)(paths.prefix, "current"))}`,
1496
1747
  "PrivateDevices=yes",
1497
1748
  "TimeoutStopSec=15s"
1498
1749
  ];
@@ -1511,14 +1762,14 @@ function renderSystemHubUnit(options) {
1511
1762
  }
1512
1763
  function renderSystemCollectorUnit(options) {
1513
1764
  const { paths, accounts } = options;
1514
- const collectorScript = (0, import_node_path3.join)(paths.prefix, "current", "dist", "collector.cjs");
1765
+ const collectorScript = (0, import_node_path4.join)(paths.prefix, "current", "dist", "collector.cjs");
1515
1766
  const extra = [
1516
1767
  ...options.powerAccess ? [`SupplementaryGroups=${accounts.powerGroup}`] : [],
1517
1768
  `Environment=${systemdQuote(`COLLECTOR_MODE=real`)}`,
1518
1769
  `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"))}`,
1770
+ `Environment=${systemdQuote(`COLLECTOR_HEALTH_FILE=${(0, import_node_path4.join)(paths.stateDir, "collector", "health.json")}`)}`,
1771
+ `ReadWritePaths=${systemdQuote((0, import_node_path4.join)(paths.stateDir, "collector"))}`,
1772
+ `ReadOnlyPaths=${systemdQuote((0, import_node_path4.join)(paths.prefix, "current"))}`,
1522
1773
  "TimeoutStopSec=10s"
1523
1774
  ];
1524
1775
  return [
@@ -1621,6 +1872,12 @@ function readJsonObject(path) {
1621
1872
  }
1622
1873
  return value;
1623
1874
  }
1875
+ function isCanonicalComponents(value) {
1876
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1877
+ const record = value;
1878
+ const enabled = (entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) && typeof entry.enabled === "boolean";
1879
+ return enabled(record.hub) && enabled(record.collector);
1880
+ }
1624
1881
  function readSystemState(paths) {
1625
1882
  const path = stateFilePath(paths);
1626
1883
  if (!(0, import_node_fs3.existsSync)(path)) return null;
@@ -1628,7 +1885,7 @@ function readSystemState(paths) {
1628
1885
  if (!stat.isFile() || stat.isSymbolicLink()) throw new SystemError("unsafe-path", `${path} must be a regular file`);
1629
1886
  if ((stat.mode & 63) !== 0) throw new SystemError("insecure-permissions", `${path} must be 0600`);
1630
1887
  const raw = readJsonObject(path);
1631
- if (raw.formatVersion !== 1 && raw.formatVersion !== SYSTEM_STATE_FORMAT_VERSION || raw.scope !== SYSTEM_SCOPE) {
1888
+ if (raw.scope !== SYSTEM_SCOPE) {
1632
1889
  throw new SystemError("invalid-state", `${path} is not a Rigscope system installation state`);
1633
1890
  }
1634
1891
  const savedPaths = raw.paths;
@@ -1640,24 +1897,40 @@ function readSystemState(paths) {
1640
1897
  if (raw.role !== "local" && raw.role !== "hub" && raw.role !== "collector") {
1641
1898
  throw new SystemError("invalid-state", "system installation state has an invalid role");
1642
1899
  }
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 } };
1900
+ const saved = raw.components;
1901
+ if (!isCanonicalComponents(saved)) {
1902
+ throw new SystemError("invalid-state", "system installation state has invalid component enabled state");
1653
1903
  }
1904
+ if (Object.prototype.hasOwnProperty.call(raw, "formatVersion")) {
1905
+ throw new SystemError("invalid-state", "system installation state has an obsolete format version; reinstall or upgrade to reset it");
1906
+ }
1907
+ const components = { hub: { enabled: saved.hub.enabled }, collector: { enabled: saved.collector.enabled } };
1654
1908
  return {
1655
1909
  ...raw,
1656
- formatVersion: SYSTEM_STATE_FORMAT_VERSION,
1657
1910
  accounts,
1658
1911
  components
1659
1912
  };
1660
1913
  }
1914
+ function systemStateIsLegacy(paths) {
1915
+ const path = stateFilePath(paths);
1916
+ if (!(0, import_node_fs3.existsSync)(path)) return false;
1917
+ let raw;
1918
+ try {
1919
+ raw = readJsonObject(path);
1920
+ } catch {
1921
+ return false;
1922
+ }
1923
+ if (Object.prototype.hasOwnProperty.call(raw, "formatVersion")) return true;
1924
+ return !isCanonicalComponents(raw.components);
1925
+ }
1926
+ function legacySystemRole(paths) {
1927
+ try {
1928
+ const raw = readJsonObject(stateFilePath(paths));
1929
+ if (raw.role === "local" || raw.role === "hub" || raw.role === "collector") return raw.role;
1930
+ } catch {
1931
+ }
1932
+ return "local";
1933
+ }
1661
1934
  function writeSystemState(paths, state) {
1662
1935
  ensureDirectory(paths.stateDir, 493, false);
1663
1936
  atomicWrite(stateFilePath(paths), `${JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
@@ -1667,7 +1940,6 @@ function emptyState(options) {
1667
1940
  const timestamp = new Date(options.now()).toISOString();
1668
1941
  const components = options.components ?? componentsForRole(options.role);
1669
1942
  return {
1670
- formatVersion: SYSTEM_STATE_FORMAT_VERSION,
1671
1943
  scope: SYSTEM_SCOPE,
1672
1944
  role: options.role,
1673
1945
  active: true,
@@ -1692,17 +1964,21 @@ function validateVersion(value) {
1692
1964
  return value;
1693
1965
  }
1694
1966
  function releaseVersion(root) {
1695
- return validateVersion(readJsonObject((0, import_node_path3.join)(root, "package.json")).version);
1967
+ const metadata = readJsonObject((0, import_node_path4.join)(root, "package.json"));
1968
+ if (Object.hasOwn(metadata, "rigscopeConfigVersion")) {
1969
+ throw new SystemError("invalid-release", "release uses unfinished configuration metadata; select a canonical release");
1970
+ }
1971
+ return validateVersion(metadata.version);
1696
1972
  }
1697
1973
  function releaseSchemaVersion(root) {
1698
- const manifestPath = (0, import_node_path3.join)(root, "manifest.json");
1974
+ const manifestPath = (0, import_node_path4.join)(root, "manifest.json");
1699
1975
  if (!(0, import_node_fs3.existsSync)(manifestPath)) return null;
1700
1976
  const manifest = readJsonObject(manifestPath);
1701
1977
  return Number.isSafeInteger(manifest.schemaVersion) ? Number(manifest.schemaVersion) : null;
1702
1978
  }
1703
1979
  function assertReleaseArtifacts(root) {
1704
1980
  for (const artifact of REQUIRED_ARTIFACTS) {
1705
- const path = (0, import_node_path3.join)(root, artifact);
1981
+ const path = (0, import_node_path4.join)(root, artifact);
1706
1982
  let stat;
1707
1983
  try {
1708
1984
  stat = (0, import_node_fs3.lstatSync)(path);
@@ -1713,12 +1989,12 @@ function assertReleaseArtifacts(root) {
1713
1989
  throw new SystemError("invalid-artifact", `release artifact is not a non-empty regular file: ${artifact}`);
1714
1990
  }
1715
1991
  }
1716
- const buildId = (0, import_node_fs3.readFileSync)((0, import_node_path3.join)(root, ".next", "BUILD_ID"), "utf8").trim();
1992
+ const buildId = (0, import_node_fs3.readFileSync)((0, import_node_path4.join)(root, ".next", "BUILD_ID"), "utf8").trim();
1717
1993
  if (buildId === "" || /\s/.test(buildId)) throw new SystemError("invalid-artifact", ".next/BUILD_ID is invalid");
1718
1994
  for (const directory of [".next/server", ".next/static"]) {
1719
1995
  let stat;
1720
1996
  try {
1721
- stat = (0, import_node_fs3.lstatSync)((0, import_node_path3.join)(root, directory));
1997
+ stat = (0, import_node_fs3.lstatSync)((0, import_node_path4.join)(root, directory));
1722
1998
  } catch {
1723
1999
  throw new SystemError("missing-artifact", `release is missing ${directory}/`);
1724
2000
  }
@@ -1726,10 +2002,10 @@ function assertReleaseArtifacts(root) {
1726
2002
  }
1727
2003
  }
1728
2004
  function verifyReleaseManifest(root) {
1729
- const manifestPath = (0, import_node_path3.join)(root, "manifest.json");
2005
+ const manifestPath = (0, import_node_path4.join)(root, "manifest.json");
1730
2006
  if (!(0, import_node_fs3.existsSync)(manifestPath)) return;
1731
2007
  const manifest = readJsonObject(manifestPath);
1732
- const packageJson = readJsonObject((0, import_node_path3.join)(root, "package.json"));
2008
+ const packageJson = readJsonObject((0, import_node_path4.join)(root, "package.json"));
1733
2009
  if (manifest.name !== void 0 && manifest.name !== packageJson.name) {
1734
2010
  throw new SystemError("manifest-mismatch", "release manifest name does not match package.json");
1735
2011
  }
@@ -1748,7 +2024,7 @@ function verifyReleaseManifest(root) {
1748
2024
  if (typeof relativePath !== "string" || relativePath === "" || relativePath.startsWith("/") || relativePath.includes("\\") || relativePath.split("/").includes("..")) {
1749
2025
  throw new SystemError("invalid-manifest", `release manifest contains an unsafe path: ${String(relativePath)}`);
1750
2026
  }
1751
- const absolute = (0, import_node_path3.join)(root, relativePath);
2027
+ const absolute = (0, import_node_path4.join)(root, relativePath);
1752
2028
  const stat = (0, import_node_fs3.lstatSync)(absolute);
1753
2029
  if (!stat.isFile() || stat.isSymbolicLink()) throw new SystemError("invalid-manifest", `release artifact is not a regular file: ${relativePath}`);
1754
2030
  if (entry.bytes !== stat.size) throw new SystemError("artifact-mismatch", `release artifact size changed: ${relativePath}`);
@@ -1757,8 +2033,8 @@ function verifyReleaseManifest(root) {
1757
2033
  }
1758
2034
  const visit = (current) => {
1759
2035
  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("/");
2036
+ const absolute = (0, import_node_path4.join)(current, entry.name);
2037
+ const rel = (0, import_node_path4.relative)(root, absolute).split(import_node_path4.sep).join("/");
1762
2038
  if (rel === "node_modules" || rel.startsWith("node_modules/") || rel === ".next/cache" || rel.startsWith(".next/cache/")) continue;
1763
2039
  if (entry.isSymbolicLink()) throw new SystemError("unsafe-release", `release contains a symlink: ${rel}`);
1764
2040
  if (entry.isDirectory()) {
@@ -1774,30 +2050,30 @@ function verifyReleaseManifest(root) {
1774
2050
  function assertNoSymlinks(root) {
1775
2051
  const visit = (path) => {
1776
2052
  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)}`);
2053
+ if (stat.isSymbolicLink()) throw new SystemError("unsafe-release", `release contains a symlink: ${(0, import_node_path4.relative)(root, path)}`);
1778
2054
  if (!stat.isDirectory()) return;
1779
- for (const entry of (0, import_node_fs3.readdirSync)(path)) visit((0, import_node_path3.join)(path, entry));
2055
+ for (const entry of (0, import_node_fs3.readdirSync)(path)) visit((0, import_node_path4.join)(path, entry));
1780
2056
  };
1781
2057
  visit(root);
1782
2058
  }
1783
2059
  function copyReleaseTree(source, destination) {
1784
2060
  const visit = (current, target) => {
1785
2061
  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)}`);
2062
+ if (stat.isSymbolicLink()) throw new SystemError("unsafe-release", `source contains a symlink: ${(0, import_node_path4.relative)(source, current)}`);
1787
2063
  if (stat.isDirectory()) {
1788
2064
  (0, import_node_fs3.mkdirSync)(target, { recursive: true, mode: 493 });
1789
2065
  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);
2066
+ const child = (0, import_node_path4.join)(current, entry);
2067
+ const rel = (0, import_node_path4.relative)(source, child);
1792
2068
  if (rel === ".git" || rel === ".next-dev" || rel === ".next/cache" || rel === "node_modules" || rel.startsWith("node_modules/")) {
1793
2069
  continue;
1794
2070
  }
1795
- visit(child, (0, import_node_path3.join)(target, entry));
2071
+ visit(child, (0, import_node_path4.join)(target, entry));
1796
2072
  }
1797
2073
  return;
1798
2074
  }
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 });
2075
+ if (!stat.isFile()) throw new SystemError("unsafe-release", `source contains a non-regular file: ${(0, import_node_path4.relative)(source, current)}`);
2076
+ (0, import_node_fs3.mkdirSync)((0, import_node_path4.dirname)(target), { recursive: true, mode: 493 });
1801
2077
  (0, import_node_fs3.copyFileSync)(current, target);
1802
2078
  (0, import_node_fs3.chmodSync)(target, 420);
1803
2079
  };
@@ -1807,8 +2083,8 @@ function copyReleaseAllowlisted(source, destination) {
1807
2083
  for (const entry of (0, import_node_fs3.readdirSync)(source)) {
1808
2084
  if (!PACKAGE_TOP_LEVEL.has(entry)) continue;
1809
2085
  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);
2086
+ const child = (0, import_node_path4.join)(source, entry);
2087
+ const target = (0, import_node_path4.join)(destination, entry);
1812
2088
  const stat = (0, import_node_fs3.lstatSync)(child);
1813
2089
  if (stat.isSymbolicLink()) throw new SystemError("unsafe-release", `source contains a symlink: ${entry}`);
1814
2090
  if (stat.isDirectory()) {
@@ -1848,59 +2124,102 @@ function extractArchive(archive, stage, host) {
1848
2124
  runRequired(host, "tar", args, "archive extraction");
1849
2125
  assertNoSymlinks(stage);
1850
2126
  }
2127
+ function discardStagedRelease(staged) {
2128
+ if (staged === null) return;
2129
+ (0, import_node_fs3.rmSync)(staged.stageDir, { recursive: true, force: true });
2130
+ }
2131
+ function releaseTargetDir(paths, version) {
2132
+ return (0, import_node_path4.join)(paths.prefix, "releases", version);
2133
+ }
2134
+ function assertReleaseTargetAvailable(paths, releaseDir, replaceRelease) {
2135
+ if (!(0, import_node_fs3.existsSync)(releaseDir)) return;
2136
+ const current = currentReleaseDir(paths);
2137
+ const previous = previousReleaseDir(paths);
2138
+ if (!replaceRelease || current === releaseDir || previous === releaseDir) {
2139
+ throw new SystemError(
2140
+ "release-exists",
2141
+ `release ${(0, import_node_path4.basename)(releaseDir)} is already installed${current === releaseDir || previous === releaseDir ? " or retained" : ""}; choose a new version`
2142
+ );
2143
+ }
2144
+ }
1851
2145
  function stageRelease(options) {
1852
- const source = (0, import_node_path3.resolve)(options.source);
2146
+ const source = (0, import_node_fs3.realpathSync)((0, import_node_path4.resolve)(options.source));
1853
2147
  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 });
2148
+ ensureDirectory(options.paths.prefix, 493);
2149
+ const stageDir = (0, import_node_path4.join)(options.paths.prefix, `.stage-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
2150
+ (0, import_node_fs3.mkdirSync)(stageDir, { recursive: true, mode: 448 });
1858
2151
  try {
1859
2152
  if (stat.isDirectory() && !stat.isSymbolicLink()) {
1860
- copyReleaseAllowlisted(source, stage);
2153
+ copyReleaseAllowlisted(source, stageDir);
1861
2154
  } else if (stat.isFile() && !stat.isSymbolicLink()) {
1862
- extractArchive(source, stage, options.host);
2155
+ extractArchive(source, stageDir, options.host);
1863
2156
  } else {
1864
2157
  throw new SystemError("invalid-source", "release source must be a directory or .tar.gz archive");
1865
2158
  }
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 });
2159
+ assertReleaseArtifacts(stageDir);
2160
+ verifyReleaseManifest(stageDir);
2161
+ return {
2162
+ version: releaseVersion(stageDir),
2163
+ stageDir,
2164
+ schemaVersion: releaseSchemaVersion(stageDir),
2165
+ artifacts: [...REQUIRED_ARTIFACTS]
2166
+ };
2167
+ } catch (error) {
2168
+ (0, import_node_fs3.rmSync)(stageDir, { recursive: true, force: true });
2169
+ throw error;
2170
+ }
2171
+ }
2172
+ function prepareStagedRelease(options) {
2173
+ const staged = stageRelease(options);
2174
+ try {
2175
+ if (options.installDeps === true) {
2176
+ installDependencies(staged.stageDir, options.host, options.installDepsCommand ?? "npm");
1880
2177
  }
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] };
2178
+ const missing = missingDependencies(staged.stageDir);
2179
+ if (missing.length > 0) {
2180
+ throw new SystemError(
2181
+ "missing-dependencies",
2182
+ `production dependencies are missing (${missing.join(", ")}); stage with --install-deps or use a built release directory`
2183
+ );
2184
+ }
2185
+ chmodTree(staged.stageDir, options.host);
2186
+ runRequired(options.host, "chown", ["-R", "root:root", staged.stageDir], "release ownership");
2187
+ return staged;
1884
2188
  } catch (error) {
1885
- (0, import_node_fs3.rmSync)(stage, { recursive: true, force: true });
2189
+ discardStagedRelease(staged);
1886
2190
  throw error;
1887
2191
  }
1888
2192
  }
2193
+ function commitStagedRelease(options) {
2194
+ const releasesDir = (0, import_node_path4.join)(options.paths.prefix, "releases");
2195
+ ensureDirectory(releasesDir, 493);
2196
+ const releaseDir = releaseTargetDir(options.paths, options.staged.version);
2197
+ assertReleaseTargetAvailable(options.paths, releaseDir, options.replaceRelease);
2198
+ if ((0, import_node_fs3.existsSync)(releaseDir)) (0, import_node_fs3.rmSync)(releaseDir, { recursive: true, force: true });
2199
+ (0, import_node_fs3.renameSync)(options.staged.stageDir, releaseDir);
2200
+ ensureDirectory((0, import_node_path4.join)(releaseDir, ".next", "cache"), 493);
2201
+ return {
2202
+ version: options.staged.version,
2203
+ releaseDir,
2204
+ schemaVersion: options.staged.schemaVersion,
2205
+ artifacts: options.staged.artifacts
2206
+ };
2207
+ }
1889
2208
  function dependencyNames(root) {
1890
- const pkg = readJsonObject((0, import_node_path3.join)(root, "package.json"));
2209
+ const pkg = readJsonObject((0, import_node_path4.join)(root, "package.json"));
1891
2210
  const dependencies = pkg.dependencies;
1892
2211
  if (dependencies === null || typeof dependencies !== "object" || Array.isArray(dependencies)) return [];
1893
2212
  return Object.keys(dependencies).filter((name) => /^[A-Za-z0-9@/._-]+$/.test(name));
1894
2213
  }
1895
2214
  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")));
2215
+ return dependencyNames(root).filter((name) => !(0, import_node_fs3.existsSync)((0, import_node_path4.join)(root, "node_modules", name, "package.json")));
1897
2216
  }
1898
2217
  function installDependencies(root, host, command = "npm") {
1899
- const hasLock = (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, "package-lock.json"));
2218
+ const hasLock = (0, import_node_fs3.existsSync)((0, import_node_path4.join)(root, "package-lock.json"));
1900
2219
  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");
2220
+ const siblingNpm = (0, import_node_path4.join)((0, import_node_path4.dirname)(process.execPath), "npm");
1902
2221
  const executable = command === "npm" && (0, import_node_fs3.existsSync)(siblingNpm) ? siblingNpm : command;
1903
- const nodeBin = (0, import_node_path3.dirname)(process.execPath);
2222
+ const nodeBin = (0, import_node_path4.dirname)(process.execPath);
1904
2223
  const path = [nodeBin, process.env.PATH ?? ""].filter(Boolean).join(":");
1905
2224
  runRequired(host, executable, args, "production dependency installation", {
1906
2225
  cwd: root,
@@ -1909,21 +2228,21 @@ function installDependencies(root, host, command = "npm") {
1909
2228
  });
1910
2229
  }
1911
2230
  function currentReleaseDir(paths) {
1912
- const link = (0, import_node_path3.join)(paths.prefix, "current");
2231
+ const link = (0, import_node_path4.join)(paths.prefix, "current");
1913
2232
  try {
1914
2233
  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;
2234
+ const absolute = (0, import_node_path4.isAbsolute)(target) ? target : (0, import_node_path4.resolve)(paths.prefix, target);
2235
+ return isWithin((0, import_node_path4.join)(paths.prefix, "releases"), absolute) ? absolute : null;
1917
2236
  } catch {
1918
2237
  return null;
1919
2238
  }
1920
2239
  }
1921
2240
  function previousReleaseDir(paths) {
1922
- const link = (0, import_node_path3.join)(paths.prefix, "previous");
2241
+ const link = (0, import_node_path4.join)(paths.prefix, "previous");
1923
2242
  try {
1924
2243
  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;
2244
+ const absolute = (0, import_node_path4.isAbsolute)(target) ? target : (0, import_node_path4.resolve)(paths.prefix, target);
2245
+ return isWithin((0, import_node_path4.join)(paths.prefix, "releases"), absolute) ? absolute : null;
1927
2246
  } catch {
1928
2247
  return null;
1929
2248
  }
@@ -1958,15 +2277,16 @@ function nodeVersion(host, nodePath) {
1958
2277
  return output;
1959
2278
  }
1960
2279
  function ensureRootOwnedNode(options) {
1961
- const selected = (0, import_node_path3.resolve)(options.nodePath ?? process.execPath);
2280
+ const selected = (0, import_node_path4.resolve)(options.nodePath ?? process.execPath);
1962
2281
  const real = (0, import_node_fs3.realpathSync)(selected);
1963
2282
  nodeVersion(options.host, real);
1964
2283
  const stat = (0, import_node_fs3.statSync)(real);
1965
- const unsafe = stat.uid !== 0 || (stat.mode & 18) !== 0 || options.copyNode === true;
2284
+ const insideRelease = isWithin((0, import_node_path4.join)(options.paths.prefix, "releases"), real);
2285
+ const unsafe = stat.uid !== 0 || (stat.mode & 18) !== 0 || options.copyNode === true || insideRelease;
1966
2286
  if (!unsafe || options.trustedNode === true) return real;
1967
- const target = (0, import_node_path3.join)(options.paths.prefix, "node", "node");
2287
+ const target = (0, import_node_path4.join)(options.paths.prefix, "node", "node");
1968
2288
  if (!options.apply) return target;
1969
- ensureDirectory((0, import_node_path3.join)(options.paths.prefix, "node"), 493);
2289
+ ensureDirectory((0, import_node_path4.join)(options.paths.prefix, "node"), 493);
1970
2290
  (0, import_node_fs3.copyFileSync)(real, target);
1971
2291
  (0, import_node_fs3.chmodSync)(target, 493);
1972
2292
  runRequired(options.host, "chown", ["root:root", target], "node ownership");
@@ -1989,60 +2309,44 @@ function normalizedJsonArray(value, label) {
1989
2309
  function validatedOrigin(value) {
1990
2310
  if (value === void 0 || value === null || value === "") return void 0;
1991
2311
  if (typeof value !== "string") throw new SystemError("invalid-adoption", "adopted hub origin is not a URL");
1992
- let parsed;
1993
2312
  try {
1994
- parsed = new URL(value);
2313
+ parsePublicOrigin(value);
1995
2314
  } catch {
1996
- throw new SystemError("invalid-adoption", "adopted hub origin is not a URL");
2315
+ throw new SystemError("invalid-adoption", "adopted hub origin must be an exact HTTPS origin");
1997
2316
  }
1998
- if (parsed.protocol !== "https:") throw new SystemError("invalid-adoption", "adopted hub origin must be https");
1999
2317
  return value;
2000
2318
  }
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
2319
  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
- };
2320
+ const configPath = (0, import_node_path4.join)(source, "config.json");
2321
+ if (!(0, import_node_fs3.existsSync)(configPath)) {
2322
+ throw new SystemError("invalid-adoption", "adopted installation has no config.json");
2025
2323
  }
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
- };
2324
+ assertPrivateSourceFile(configPath, "adopted setup");
2325
+ const setup = readJsonObject(configPath);
2326
+ if (Object.prototype.hasOwnProperty.call(setup, "version") || Object.prototype.hasOwnProperty.call(setup, "formatVersion")) {
2327
+ throw new SystemError("invalid-adoption", "adopted setup is pre-canonical; reset or reinstall it before adoption");
2328
+ }
2329
+ const role = setup.role;
2330
+ if (typeof role !== "string" || !["local", "hub", "collector"].includes(role)) {
2331
+ throw new SystemError("invalid-adoption", "adopted setup role is incompatible");
2041
2332
  }
2042
- if (hasCollectorIdentity(source) && (0, import_node_fs3.existsSync)((0, import_node_path3.join)(source, "collector"))) {
2043
- return { role: "collector", port: 8081, legacy: true };
2333
+ const port = Number(setup.port);
2334
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new SystemError("invalid-adoption", "adopted hub port is invalid");
2335
+ if (!Array.isArray(setup.users) || !Array.isArray(setup.collectors)) {
2336
+ throw new SystemError("invalid-adoption", "adopted setup must contain users and collectors arrays");
2337
+ }
2338
+ if (!isCanonicalComponents(setup.components)) {
2339
+ throw new SystemError("invalid-adoption", "adopted setup has an invalid component enabled state");
2044
2340
  }
2045
- throw new SystemError("invalid-adoption", "adopted installation has no config.json or hub-config.json");
2341
+ return {
2342
+ role,
2343
+ components: componentSet({ hub: setup.components.hub.enabled, collector: setup.components.collector.enabled }),
2344
+ port,
2345
+ origin: validatedOrigin(setup.origin),
2346
+ usersJson: normalizedJsonArray(setup.users, "HUB_BROWSER_USERS"),
2347
+ collectorsJson: normalizedJsonArray(setup.collectors, "HUB_COLLECTORS"),
2348
+ maxConnections: Number.isSafeInteger(setup.maxCollectorConnections) ? Number(setup.maxCollectorConnections) : void 0
2349
+ };
2046
2350
  }
2047
2351
  function resolveRole(options) {
2048
2352
  const role = options.role ?? "local";
@@ -2063,7 +2367,7 @@ function detectUserInstallations(options = {}) {
2063
2367
  const found = [];
2064
2368
  for (const homeRoot of roots) {
2065
2369
  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));
2370
+ const homes = homeRoot === "/root" ? ["/root"] : (0, import_node_fs3.readdirSync)(homeRoot).map((entry) => (0, import_node_path4.join)(homeRoot, entry));
2067
2371
  for (const home of homes) {
2068
2372
  let stat;
2069
2373
  try {
@@ -2072,7 +2376,7 @@ function detectUserInstallations(options = {}) {
2072
2376
  continue;
2073
2377
  }
2074
2378
  if (!stat.isDirectory()) continue;
2075
- const unitDir = (0, import_node_path3.join)(home, ".config", "systemd", "user");
2379
+ const unitDir = (0, import_node_path4.join)(home, ".config", "systemd", "user");
2076
2380
  let units = [];
2077
2381
  try {
2078
2382
  units = (0, import_node_fs3.readdirSync)(unitDir);
@@ -2081,16 +2385,16 @@ function detectUserInstallations(options = {}) {
2081
2385
  }
2082
2386
  for (const unitName of units) {
2083
2387
  if (!/^rigscope(?:-[0-9a-f]{12})?(?:-(?:hub|collector))?\.service$/.test(unitName)) continue;
2084
- const unitPath = (0, import_node_path3.join)(unitDir, unitName);
2388
+ const unitPath = (0, import_node_path4.join)(unitDir, unitName);
2085
2389
  let execStart = null;
2086
2390
  try {
2087
2391
  const line = (0, import_node_fs3.readFileSync)(unitPath, "utf8").split("\n").find((item) => item.startsWith("ExecStart="));
2088
2392
  execStart = line ? line.slice("ExecStart=".length) : null;
2089
2393
  } catch {
2090
2394
  }
2091
- const stateCandidates = [(0, import_node_path3.join)(home, ".local", "state", "rigscope")];
2395
+ const stateCandidates = [(0, import_node_path4.join)(home, ".local", "state", "rigscope")];
2092
2396
  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 });
2397
+ found.push({ user: (0, import_node_path4.basename)(home), home, unitPath, unitName, execStart, stateDir });
2094
2398
  }
2095
2399
  }
2096
2400
  }
@@ -2118,9 +2422,9 @@ function lockIsLive(path) {
2118
2422
  }
2119
2423
  function directoryLocks(directory) {
2120
2424
  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")
2425
+ (0, import_node_path4.join)(directory, "hub", "telemetry.sqlite.hub.lock"),
2426
+ (0, import_node_path4.join)(directory, "run", "rigscope.lock"),
2427
+ (0, import_node_path4.join)(directory, "collector", "collector-runtime.lock")
2124
2428
  ];
2125
2429
  }
2126
2430
  function assertDirectoryStopped(directory, scope = "local") {
@@ -2151,7 +2455,7 @@ function assertSystemStartAllowed(paths, components) {
2151
2455
  }
2152
2456
  function detectPowerDriver(moduleRoot = "/sys/module") {
2153
2457
  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)));
2458
+ return candidates.filter((name) => (0, import_node_fs3.existsSync)((0, import_node_path4.join)(moduleRoot, name)));
2155
2459
  }
2156
2460
  function readPasswd(path = "/etc/passwd") {
2157
2461
  const users = /* @__PURE__ */ new Map();
@@ -2224,15 +2528,15 @@ function inspectPowerAccess(options = {}) {
2224
2528
  entries = [];
2225
2529
  }
2226
2530
  for (const entry of entries.filter((name) => /^intel-rapl:\d+$/.test(name)).sort()) {
2227
- const domainPath = (0, import_node_path3.join)(paths.powercapRoot, entry);
2531
+ const domainPath = (0, import_node_path4.join)(paths.powercapRoot, entry);
2228
2532
  let name = entry;
2229
2533
  try {
2230
- name = (0, import_node_fs3.readFileSync)((0, import_node_path3.join)(domainPath, "name"), "utf8").trim() || entry;
2534
+ name = (0, import_node_fs3.readFileSync)((0, import_node_path4.join)(domainPath, "name"), "utf8").trim() || entry;
2231
2535
  } catch {
2232
2536
  }
2233
2537
  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");
2538
+ const energyPath = (0, import_node_path4.join)(domainPath, "energy_uj");
2539
+ const maxRangePath = (0, import_node_path4.join)(domainPath, "max_energy_range_uj");
2236
2540
  const readStat = (path) => {
2237
2541
  if (options.statOverride) {
2238
2542
  const override = options.statOverride(path);
@@ -2301,7 +2605,7 @@ function inspectPowerAccess(options = {}) {
2301
2605
  };
2302
2606
  }
2303
2607
  async function backupDatabase(source, output, host) {
2304
- ensureDirectory((0, import_node_path3.dirname)(output), 448, true);
2608
+ ensureDirectory((0, import_node_path4.dirname)(output), 448, true);
2305
2609
  const db = new import_node_sqlite.DatabaseSync(source, { readOnly: true, timeout: 5e3 });
2306
2610
  try {
2307
2611
  await (0, import_node_sqlite.backup)(db, output);
@@ -2338,11 +2642,11 @@ function replaceDatabaseFromBackup(backupPath, target, safetyBackup, host) {
2338
2642
  } finally {
2339
2643
  backup.close();
2340
2644
  }
2341
- ensureDirectory((0, import_node_path3.dirname)(target), 448, true);
2645
+ ensureDirectory((0, import_node_path4.dirname)(target), 448, true);
2342
2646
  if (safetyBackup !== null && (0, import_node_fs3.existsSync)(target)) {
2343
2647
  void host;
2344
2648
  }
2345
- const temporary = (0, import_node_path3.join)((0, import_node_path3.dirname)(target), `.restore-${process.pid}-${Date.now()}.sqlite`);
2649
+ const temporary = (0, import_node_path4.join)((0, import_node_path4.dirname)(target), `.restore-${process.pid}-${Date.now()}.sqlite`);
2346
2650
  (0, import_node_fs3.copyFileSync)(backupPath, temporary);
2347
2651
  (0, import_node_fs3.chmodSync)(temporary, 384);
2348
2652
  for (const sidecar of [`${target}-wal`, `${target}-shm`]) {
@@ -2358,7 +2662,7 @@ function newestTimestamp() {
2358
2662
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2359
2663
  }
2360
2664
  function adoptUserState(options) {
2361
- const source = (0, import_node_path3.resolve)(options.source);
2665
+ const source = (0, import_node_path4.resolve)(options.source);
2362
2666
  if (!(0, import_node_fs3.existsSync)(source)) throw new SystemError("invalid-adoption", `user installation does not exist: ${source}`);
2363
2667
  if (isWithin(options.paths.prefix, source) || isWithin(options.paths.stateDir, source) || isWithin(source, options.paths.stateDir)) {
2364
2668
  throw new SystemError("invalid-adoption", "adoption source and system paths must not overlap");
@@ -2368,6 +2672,28 @@ function adoptUserState(options) {
2368
2672
  if (setup.role !== options.role && setup.role !== "local") throw new SystemError("invalid-adoption", "adopted setup role is incompatible");
2369
2673
  const { port, origin } = setup;
2370
2674
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new SystemError("invalid-adoption", "adopted hub port is invalid");
2675
+ if (options.role !== "collector") {
2676
+ const sourceDatabase = (0, import_node_path4.join)(source, "hub", "telemetry.sqlite");
2677
+ if ((0, import_node_fs3.existsSync)(sourceDatabase)) {
2678
+ assertPrivateSourceFile(sourceDatabase, "adopted database");
2679
+ const database = new import_node_sqlite.DatabaseSync(sourceDatabase, { readOnly: true });
2680
+ try {
2681
+ const row = database.prepare("PRAGMA integrity_check").get();
2682
+ if (!row || Object.values(row)[0] !== "ok") throw new SystemError("invalid-adoption", "adopted database failed its integrity check");
2683
+ } finally {
2684
+ database.close();
2685
+ }
2686
+ }
2687
+ const sourceEnrollment = (0, import_node_path4.join)(source, "hub", "enrollment.json");
2688
+ if ((0, import_node_fs3.existsSync)(sourceEnrollment)) assertPrivateSourceFile(sourceEnrollment, "adopted enrollment");
2689
+ }
2690
+ if (options.role !== "hub") {
2691
+ const sourceCollectorState = (0, import_node_path4.join)(source, "collector", "state.json");
2692
+ if ((0, import_node_fs3.existsSync)(sourceCollectorState)) {
2693
+ assertPrivateSourceFile(sourceCollectorState, "adopted collector state");
2694
+ readCollectorIdentity(sourceCollectorState);
2695
+ }
2696
+ }
2371
2697
  const copied = [];
2372
2698
  const warnings = [];
2373
2699
  const targetDatabase = hubDatabasePath(options.paths);
@@ -2413,12 +2739,12 @@ function copyAdoptedTree(source, target, copied, skipLocks = true) {
2413
2739
  if (stat.isSymbolicLink()) return;
2414
2740
  if (stat.isDirectory()) {
2415
2741
  (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));
2742
+ for (const entry of (0, import_node_fs3.readdirSync)(current)) visit((0, import_node_path4.join)(current, entry), (0, import_node_path4.join)(destination, entry));
2417
2743
  return;
2418
2744
  }
2419
2745
  if (!stat.isFile()) return;
2420
2746
  if (skipLocks && /\.(lock|sock)$/.test(current)) return;
2421
- (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(destination), { recursive: true, mode: 448 });
2747
+ (0, import_node_fs3.mkdirSync)((0, import_node_path4.dirname)(destination), { recursive: true, mode: 448 });
2422
2748
  (0, import_node_fs3.copyFileSync)(current, destination);
2423
2749
  (0, import_node_fs3.chmodSync)(destination, 384);
2424
2750
  copied.push({ source: current, target: destination, sha256: sha256File(destination), bytes: stat.size });
@@ -2429,10 +2755,10 @@ async function performAdoptionCopyAsync(options) {
2429
2755
  const { source, paths, role, host, copy } = options;
2430
2756
  assertDirectoryStopped(source, role);
2431
2757
  const targetDatabase = hubDatabasePath(paths);
2432
- const sourceDatabase = (0, import_node_path3.join)(source, "hub", "telemetry.sqlite");
2758
+ const sourceDatabase = (0, import_node_path4.join)(source, "hub", "telemetry.sqlite");
2433
2759
  if (role !== "collector" && (0, import_node_fs3.existsSync)(sourceDatabase) && !(0, import_node_fs3.existsSync)(targetDatabase)) {
2434
2760
  assertPrivateSourceFile(sourceDatabase, "adopted database");
2435
- ensureDirectory((0, import_node_path3.join)(paths.stateDir, "hub"), 448, true);
2761
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "hub"), 448, true);
2436
2762
  await backupDatabase(sourceDatabase, targetDatabase, host);
2437
2763
  copy.record.files.push({
2438
2764
  source: sourceDatabase,
@@ -2441,13 +2767,10 @@ async function performAdoptionCopyAsync(options) {
2441
2767
  bytes: (0, import_node_fs3.statSync)(targetDatabase).size
2442
2768
  });
2443
2769
  }
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) {
2770
+ const sourceCollectorState = (0, import_node_path4.join)(source, "collector", "state.json");
2771
+ if (role !== "hub" && (0, import_node_fs3.existsSync)(sourceCollectorState)) {
2449
2772
  assertPrivateSourceFile(sourceCollectorState, "adopted collector state");
2450
- ensureDirectory((0, import_node_path3.join)(paths.stateDir, "collector"), 448, true);
2773
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "collector"), 448, true);
2451
2774
  const targetCollector = collectorStatePath(paths);
2452
2775
  (0, import_node_fs3.copyFileSync)(sourceCollectorState, targetCollector);
2453
2776
  (0, import_node_fs3.chmodSync)(targetCollector, 384);
@@ -2457,10 +2780,10 @@ async function performAdoptionCopyAsync(options) {
2457
2780
  sha256: sha256File(targetCollector),
2458
2781
  bytes: (0, import_node_fs3.statSync)(targetCollector).size
2459
2782
  });
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);
2783
+ const outbox = (0, import_node_path4.join)(source, "collector", "outbox");
2784
+ if ((0, import_node_fs3.existsSync)(outbox)) copyAdoptedTree(outbox, (0, import_node_path4.join)(paths.stateDir, "collector", "outbox"), copy.record.files);
2462
2785
  }
2463
- const enrollment = (0, import_node_path3.join)(source, "hub", "enrollment.json");
2786
+ const enrollment = (0, import_node_path4.join)(source, "hub", "enrollment.json");
2464
2787
  if (role !== "collector" && (0, import_node_fs3.existsSync)(enrollment)) {
2465
2788
  const target = hubEnrollmentPath(paths);
2466
2789
  (0, import_node_fs3.copyFileSync)(enrollment, target);
@@ -2505,13 +2828,13 @@ function writePowerRule(paths, accounts) {
2505
2828
  writeManagedFile(raplRulePath(paths), renderRaplRule(accounts), 420, true);
2506
2829
  }
2507
2830
  function powerOriginalPath(paths) {
2508
- return (0, import_node_path3.join)(paths.stateDir, POWER_STATE_FILE_NAME);
2831
+ return (0, import_node_path4.join)(paths.stateDir, POWER_STATE_FILE_NAME);
2509
2832
  }
2510
2833
  function capturePowerOriginals(paths) {
2511
2834
  const files = {};
2512
2835
  for (const domain of packageDomainPaths(paths.powercapRoot)) {
2513
2836
  for (const name of ["energy_uj", "max_energy_range_uj"]) {
2514
- const path = (0, import_node_path3.join)(domain, name);
2837
+ const path = (0, import_node_path4.join)(domain, name);
2515
2838
  try {
2516
2839
  const stat = (0, import_node_fs3.statSync)(path);
2517
2840
  files[path] = { mode: stat.mode & 511, uid: stat.uid, gid: stat.gid };
@@ -2525,7 +2848,7 @@ function capturePowerOriginals(paths) {
2525
2848
  function applyPowerAccess(paths, accounts, host) {
2526
2849
  for (const domain of packageDomainPaths(paths.powercapRoot)) {
2527
2850
  for (const name of ["energy_uj", "max_energy_range_uj"]) {
2528
- const path = (0, import_node_path3.join)(domain, name);
2851
+ const path = (0, import_node_path4.join)(domain, name);
2529
2852
  if (!(0, import_node_fs3.existsSync)(path)) continue;
2530
2853
  runRequired(host, "chgrp", [accounts.powerGroup, path], `power counter group ${path}`);
2531
2854
  runRequired(host, "chmod", ["0440", path], `power counter mode ${path}`);
@@ -2539,9 +2862,9 @@ function resumePowerRule(paths, host) {
2539
2862
  }
2540
2863
  function packageDomainPaths(root) {
2541
2864
  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) => {
2865
+ return (0, import_node_fs3.readdirSync)(root).filter((entry) => /^intel-rapl:\d+$/.test(entry)).map((entry) => (0, import_node_path4.join)(root, entry)).filter((path) => {
2543
2866
  try {
2544
- return /^package/.test((0, import_node_fs3.readFileSync)((0, import_node_path3.join)(path, "name"), "utf8").trim());
2867
+ return /^package/.test((0, import_node_fs3.readFileSync)((0, import_node_path4.join)(path, "name"), "utf8").trim());
2545
2868
  } catch {
2546
2869
  return false;
2547
2870
  }
@@ -2561,7 +2884,7 @@ function restorePowerOriginals(paths, host) {
2561
2884
  const files = { ...originals?.files ?? {} };
2562
2885
  for (const domain of packageDomainPaths(paths.powercapRoot)) {
2563
2886
  for (const name of ["energy_uj", "max_energy_range_uj"]) {
2564
- const path = (0, import_node_path3.join)(domain, name);
2887
+ const path = (0, import_node_path4.join)(domain, name);
2565
2888
  if (!(0, import_node_fs3.existsSync)(path)) continue;
2566
2889
  const original = files[path];
2567
2890
  if (original) {
@@ -2601,6 +2924,10 @@ function stopUnits(host, units) {
2601
2924
  }
2602
2925
  if (units.some((unit) => systemdActive(host, unit))) throw new SystemError("service-active", "system services did not stop; no files were restored");
2603
2926
  }
2927
+ function unitsForCleanup(paths, host) {
2928
+ const legacy = LEGACY_UNIT_NAMES.filter((unit) => (0, import_node_fs3.existsSync)((0, import_node_path4.join)(paths.unitsDir, unit)) || systemdActive(host, unit));
2929
+ return [...Object.values(UNIT_NAMES), ...legacy];
2930
+ }
2604
2931
  function startUnits(host, units) {
2605
2932
  runRequired(host, "systemctl", ["daemon-reload"], "systemd daemon-reload");
2606
2933
  for (const unit of units) runRequired(host, "systemctl", ["start", unit], `starting ${unit}`);
@@ -2636,7 +2963,7 @@ async function verifySystemHealth(paths, state, host, timeoutMs, components = en
2636
2963
  const report = runRequired(
2637
2964
  host,
2638
2965
  state.nodePath,
2639
- [(0, import_node_path3.join)(paths.prefix, "current/dist/ops.cjs"), "inspect", "--database", hubDatabasePath(paths), "--json"],
2966
+ [(0, import_node_path4.join)(paths.prefix, "current/dist/ops.cjs"), "inspect", "--database", hubDatabasePath(paths), "--json"],
2640
2967
  "activated database verification",
2641
2968
  { timeoutMs: Math.max(100, deadline - Date.now()) }
2642
2969
  );
@@ -2652,8 +2979,8 @@ async function verifySystemHealth(paths, state, host, timeoutMs, components = en
2652
2979
  let problem = "no sampling marker";
2653
2980
  while (Date.now() < deadline) {
2654
2981
  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"));
2982
+ const marker = readJsonObject((0, import_node_path4.join)(paths.stateDir, "collector/health.json"));
2983
+ const lease = readJsonObject((0, import_node_path4.join)(paths.stateDir, "collector/collector-runtime.lock"));
2657
2984
  if (marker.pid !== lease.pid || !Number.isSafeInteger(marker.pid)) throw new Error("sampling marker belongs to another process");
2658
2985
  process.kill(Number(marker.pid), 0);
2659
2986
  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");
@@ -2661,7 +2988,7 @@ async function verifySystemHealth(paths, state, host, timeoutMs, components = en
2661
2988
  } catch (error) {
2662
2989
  problem = error instanceof Error ? error.message : String(error);
2663
2990
  }
2664
- await new Promise((resolve3) => setTimeout(resolve3, 100));
2991
+ await new Promise((resolve4) => setTimeout(resolve4, 100));
2665
2992
  }
2666
2993
  throw new SystemError("collector-unready", `system collector did not sample: ${problem}`);
2667
2994
  }
@@ -2684,9 +3011,9 @@ function pickRollbackBackup(state, previousSchema) {
2684
3011
  async function recoverFailedUpgrade(options) {
2685
3012
  stopUnits(options.host, options.units);
2686
3013
  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);
3014
+ replaceSymlink((0, import_node_path4.join)(options.paths.prefix, "current"), options.oldCurrent, options.host);
3015
+ if (options.oldPrevious === null) removeSymlink((0, import_node_path4.join)(options.paths.prefix, "previous"));
3016
+ else replaceSymlink((0, import_node_path4.join)(options.paths.prefix, "previous"), options.oldPrevious, options.host);
2690
3017
  const database = hubDatabasePath(options.paths);
2691
3018
  let afterSchema = null;
2692
3019
  try {
@@ -2695,12 +3022,12 @@ async function recoverFailedUpgrade(options) {
2695
3022
  afterSchema = null;
2696
3023
  }
2697
3024
  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);
3025
+ await backupDatabase(database, (0, import_node_path4.join)(options.paths.stateDir, "backups", `failed-upgrade-${newestTimestamp()}.sqlite`), options.host);
2699
3026
  replaceDatabaseFromBackup(options.backupPath, database, null, options.host);
2700
3027
  runRequired(options.host, "chown", [`${options.state.accounts.hub}:${options.state.accounts.hub}`, database], "restored database ownership");
2701
3028
  }
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);
3029
+ options.state.previousVersion = options.oldPrevious === null ? null : (0, import_node_path4.basename)(options.oldPrevious);
3030
+ options.state.currentVersion = (0, import_node_path4.basename)(options.oldCurrent);
2704
3031
  writeSystemState(options.paths, options.state);
2705
3032
  if (options.activeComponents.length > 0) {
2706
3033
  startUnits(options.host, options.units);
@@ -2716,7 +3043,7 @@ function assertManagedFileOwnership(paths, path) {
2716
3043
  if ((stat.mode & 18) !== 0) throw new SystemError("insecure-permissions", `${path} must not be group/world-writable`);
2717
3044
  }
2718
3045
  function writeUnitFile(paths, unit, content, replace) {
2719
- const path = (0, import_node_path3.join)(paths.unitsDir, unit);
3046
+ const path = (0, import_node_path4.join)(paths.unitsDir, unit);
2720
3047
  assertManagedFileOwnership(paths, path);
2721
3048
  writeManagedFile(path, content, 420, replace);
2722
3049
  }
@@ -2731,10 +3058,10 @@ function ensureComponentSetup(options) {
2731
3058
  ensureDirectory(paths.configDir, 493);
2732
3059
  ensureDirectory(paths.stateDir, 493);
2733
3060
  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);
3061
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "hub"), 448, true);
3062
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "hub", "run"), 448, true);
3063
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "collector"), 448, true);
3064
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "backups"), 448, true);
2738
3065
  const release = currentReleaseOrThrow(paths);
2739
3066
  void release;
2740
3067
  writeSysusersFiles(paths, accounts, "local", power, host);
@@ -2743,7 +3070,7 @@ function ensureComponentSetup(options) {
2743
3070
  const env = hubEnvTemplate(paths);
2744
3071
  writeManagedFile(hubEnvPath(paths), env, 384, false);
2745
3072
  }
2746
- if (!(0, import_node_fs3.existsSync)((0, import_node_path3.join)(paths.unitsDir, UNIT_NAMES.hub))) {
3073
+ if (!(0, import_node_fs3.existsSync)((0, import_node_path4.join)(paths.unitsDir, UNIT_NAMES.hub))) {
2747
3074
  const env = readHubEnvironment(paths);
2748
3075
  const remote = env.HUB_PUBLIC_ORIGIN !== void 0 && env.HUB_PUBLIC_ORIGIN !== "";
2749
3076
  const unit = renderSystemHubUnit({
@@ -2776,14 +3103,14 @@ function ensureComponentSetup(options) {
2776
3103
  const validation = runRequired(
2777
3104
  host,
2778
3105
  "systemd-analyze",
2779
- ["verify", "--man=no", (0, import_node_path3.join)(paths.unitsDir, unitFor(component))],
3106
+ ["verify", "--man=no", (0, import_node_path4.join)(paths.unitsDir, unitFor(component))],
2780
3107
  "native systemd unit validation"
2781
3108
  );
2782
3109
  if (/path is not absolute|bad unit file|fatal error|unknown (?:key|lvalue)|ignoring/i.test(validation.stderr)) {
2783
3110
  throw new SystemError("invalid-unit", "native systemd validation rejected a generated directive");
2784
3111
  }
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);
3112
+ if (component === "hub") chownAccount([(0, import_node_path4.join)(paths.stateDir, "hub")], accounts.hub, accounts.hub, host);
3113
+ if (component === "collector") chownAccount([(0, import_node_path4.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
2787
3114
  }
2788
3115
  function ensurePowerAccess(paths, accounts, host) {
2789
3116
  if (!(0, import_node_fs3.existsSync)(powerOriginalPath(paths))) capturePowerOriginals(paths);
@@ -2791,19 +3118,36 @@ function ensurePowerAccess(paths, accounts, host) {
2791
3118
  resumePowerRule(paths, host);
2792
3119
  applyPowerAccess(paths, accounts, host);
2793
3120
  }
3121
+ function resetUnfinishedSystemState(options) {
3122
+ uninstallSystem({
3123
+ apply: true,
3124
+ paths: options.paths,
3125
+ host: options.host,
3126
+ purgeData: true,
3127
+ confirmPurgeHistory: true,
3128
+ preserveNode: true,
3129
+ now: options.now
3130
+ });
3131
+ }
3132
+ function installPreset(options, role) {
3133
+ if (options.adoptFrom !== void 0) {
3134
+ return componentSet(readAdoptionSetup((0, import_node_path4.resolve)(options.adoptFrom)).components);
3135
+ }
3136
+ return componentsForRole(role);
3137
+ }
2794
3138
  function planSystemInstall(options) {
2795
3139
  const paths = validateSystemPaths(options.paths);
2796
3140
  const accounts = validateAccounts(options.accounts);
2797
3141
  const role = resolveRole(options);
2798
- const preset = componentSet(componentsForRole(role));
3142
+ const preset = componentSet(installPreset(options, role));
2799
3143
  const power = options.power !== false && preset.collector;
2800
3144
  const initial = options.enable === true ? preset : { hub: false, collector: false };
2801
3145
  const files = [
2802
- { path: (0, import_node_path3.join)(paths.prefix, "current"), mode: "symlink", purpose: "active root-owned release" },
3146
+ { path: (0, import_node_path4.join)(paths.prefix, "current"), mode: "symlink", purpose: "active root-owned release" },
2803
3147
  { path: hubEnvPath(paths), mode: "0600", purpose: "root-owned hub configuration" },
2804
3148
  { path: collectorEnvPath(paths), mode: "0600", purpose: "root-owned collector configuration" },
2805
3149
  { 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` }))
3150
+ ...COMPONENTS.map((component) => ({ path: (0, import_node_path4.join)(paths.unitsDir, unitFor(component)), mode: "0644", purpose: `${component} boot unit` }))
2807
3151
  ];
2808
3152
  if (power) {
2809
3153
  files.push({ path: raplRulePath(paths), mode: "0644", purpose: "package RAPL read rule" });
@@ -2818,7 +3162,7 @@ function planSystemInstall(options) {
2818
3162
  ];
2819
3163
  const notes = [
2820
3164
  "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.",
3165
+ options.adoptFrom ? `Adopts ${(0, import_node_path4.resolve)(options.adoptFrom)} by copy only; the original user setup is preserved.` : "Existing user services are not modified.",
2822
3166
  "One complete package: both runtimes, units and accounts are installed; --role only sets the initial enabled preset.",
2823
3167
  "Hub and collector use separate unprivileged accounts; Node gets no capabilities.",
2824
3168
  ...options.enable && preset.hub ? ["Enabling the hub creates its owner account once and prints the password once."] : [],
@@ -2826,7 +3170,10 @@ function planSystemInstall(options) {
2826
3170
  ];
2827
3171
  return { operation: "install", apply: options.apply === true, role, components: initial, paths, accounts, commands, files, notes };
2828
3172
  }
2829
- async function installSystem(options) {
3173
+ function installSystem(options) {
3174
+ return withSystemOperation(options, () => installSystemUnlocked(options));
3175
+ }
3176
+ async function installSystemUnlocked(options) {
2830
3177
  const host = options.host ?? createSystemHost();
2831
3178
  const now = options.now ?? Date.now;
2832
3179
  const paths = validateSystemPaths(options.paths);
@@ -2835,7 +3182,8 @@ async function installSystem(options) {
2835
3182
  const plan = planSystemInstall({ ...options, paths, accounts, role });
2836
3183
  if (options.apply !== true) return { operation: "install", ok: true, apply: false, message: "plan only", plan };
2837
3184
  assertApply(host);
2838
- const existing = readSystemState(paths);
3185
+ const legacy = systemStateIsLegacy(paths);
3186
+ const existing = legacy ? null : readSystemState(paths);
2839
3187
  if (existing?.active === true) {
2840
3188
  throw new SystemError("already-installed", "a system installation already exists; use `rigscope system upgrade` or uninstall first");
2841
3189
  }
@@ -2847,123 +3195,126 @@ async function installSystem(options) {
2847
3195
  `existing user service(s) detected (${detail}); stop them and use --adopt-user-state for explicit copy-based adoption`
2848
3196
  );
2849
3197
  }
2850
- assertSystemStartAllowed(paths, options.start === true ? componentsForRole(role) : { hub: false, collector: false });
2851
- const source = (0, import_node_path3.resolve)(options.source);
3198
+ const source = (0, import_node_path4.resolve)(options.source);
2852
3199
  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
3200
  const port = options.port ?? 8081;
2883
3201
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new SystemError("invalid-argument", "--port must be 1-65535");
2884
- const preset = componentSet(componentsForRole(role));
3202
+ parseHubConfig(options.origin === void 0 ? { HUB_HOST: "127.0.0.1", HUB_PORT: String(port) } : { HUB_PUBLIC_ORIGIN: options.origin, HUB_SOCKET_PATH: hubSocketPath(paths) });
3203
+ const preset = componentSet(installPreset(options, role));
2885
3204
  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({
3205
+ if (options.adoptFrom) {
3206
+ adoptUserState({ source: options.adoptFrom, paths, role, accounts, apply: false, host, now });
3207
+ }
3208
+ assertManagedPathSafety(paths, isDefaultSystemLayout(paths));
3209
+ ensureDirectory(paths.prefix, 493);
3210
+ const nodePath = ensureRootOwnedNode({ paths, host, nodePath: options.nodePath, copyNode: options.copyNode, trustedNode: options.trustedNode, apply: true });
3211
+ const staged = prepareStagedRelease({
3212
+ source,
2902
3213
  paths,
2903
- accounts,
2904
- nodePath,
2905
- collectorState: collectorStatePath(paths),
2906
- powerAccess: power
3214
+ host,
3215
+ installDeps: options.installDeps,
3216
+ installDepsCommand: options.installDepsCommand
2907
3217
  });
2908
- writeUnitFile(paths, UNIT_NAMES.hub, `${hubUnit}
3218
+ try {
3219
+ if (legacy) resetUnfinishedSystemState({ paths, host, now });
3220
+ ensureDirectory((0, import_node_path4.join)(paths.prefix, "releases"), 493);
3221
+ ensureDirectory(paths.stateDir, 493);
3222
+ ensureDirectory(paths.configDir, 493);
3223
+ ensureDirectory(paths.unitsDir, 493);
3224
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "hub"), 448, true);
3225
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "collector"), 448, true);
3226
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "backups"), 448, true);
3227
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "hub", "run"), 448, true);
3228
+ const release = commitStagedRelease({ staged, paths, replaceRelease: options.replaceRelease });
3229
+ let adoption = null;
3230
+ if (options.adoptFrom) {
3231
+ adoption = adoptUserState({ source: options.adoptFrom, paths, role, accounts, apply: true, host, now });
3232
+ await performAdoptionCopyAsync({ source: options.adoptFrom, paths, role, host, copy: adoption });
3233
+ }
3234
+ const hubEnv = adoption?.hubEnv ?? hubEnvTemplate(paths, options.origin, port);
3235
+ const collectorEnv = adoption?.collectorEnv ?? collectorEnvTemplate();
3236
+ writeManagedFile(hubEnvPath(paths), hubEnv, 384, false);
3237
+ writeManagedFile(collectorEnvPath(paths), collectorEnv, 384, false);
3238
+ writeSysusersFiles(paths, accounts, "local", power, host);
3239
+ const hubUnit = renderSystemHubUnit({
3240
+ paths,
3241
+ accounts,
3242
+ nodePath,
3243
+ database: hubDatabasePath(paths),
3244
+ enrollment: hubEnrollmentPath(paths),
3245
+ socket: hubSocketPath(paths),
3246
+ remote: adoption?.remote === true || typeof options.origin === "string",
3247
+ powerAccess: false
3248
+ });
3249
+ const collectorUnit = renderSystemCollectorUnit({
3250
+ paths,
3251
+ accounts,
3252
+ nodePath,
3253
+ collectorState: collectorStatePath(paths),
3254
+ powerAccess: power
3255
+ });
3256
+ writeUnitFile(paths, UNIT_NAMES.hub, `${hubUnit}
2909
3257
  `, true);
2910
- writeUnitFile(paths, UNIT_NAMES.collector, `${collectorUnit}
3258
+ writeUnitFile(paths, UNIT_NAMES.collector, `${collectorUnit}
2911
3259
  `, 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 } };
3260
+ const validation = runRequired(host, "systemd-analyze", ["verify", "--man=no", ...COMPONENTS.map((component) => (0, import_node_path4.join)(paths.unitsDir, unitFor(component)))], "native systemd unit validation");
3261
+ if (/path is not absolute|bad unit file|fatal error|unknown (?:key|lvalue)|ignoring/i.test(validation.stderr)) {
3262
+ throw new SystemError("invalid-unit", "native systemd validation rejected a generated directive");
3263
+ }
3264
+ if (power) ensurePowerAccess(paths, accounts, host);
3265
+ chownAccount([(0, import_node_path4.join)(paths.stateDir, "hub")], accounts.hub, accounts.hub, host);
3266
+ chownAccount([(0, import_node_path4.join)(paths.stateDir, "collector")], accounts.collector, accounts.collector, host);
3267
+ const state = emptyState({ paths, accounts, role, components: { hub: false, collector: false }, nodePath, now });
3268
+ state.currentVersion = release.version;
3269
+ state.releases[release.version] = {
3270
+ releaseDir: release.releaseDir,
3271
+ schemaVersion: release.schemaVersion,
3272
+ installedAt: new Date(now()).toISOString()
3273
+ };
3274
+ state.adoption = adoption?.record ?? null;
3275
+ state.power = power ? { enabled: true, rulePath: raplRulePath(paths), group: accounts.powerGroup } : null;
3276
+ replaceSymlink((0, import_node_path4.join)(paths.prefix, "current"), release.releaseDir, host);
2942
3277
  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);
3278
+ const wantsHub = preset.hub && (options.enable === true || options.start === true);
3279
+ const wantsCollector = preset.collector && (options.enable === true || options.start === true);
3280
+ let password;
3281
+ const awaiting = [];
3282
+ if (wantsHub) password = ensureHubOwner(paths);
3283
+ if (wantsCollector && !collectorConfigured(paths).configured) {
3284
+ const pairing = await pairCollectorWithHub({ paths, accounts, host });
3285
+ if (pairing.awaiting) awaiting.push("collector");
3286
+ }
3287
+ if (options.enable === true) {
3288
+ enableUnits(host, unitNamesForComponents(preset));
3289
+ state.components = { hub: { enabled: preset.hub }, collector: { enabled: preset.collector } };
3290
+ writeSystemState(paths, state);
3291
+ }
3292
+ const started = [];
3293
+ if (options.start === true) {
3294
+ assertSystemStartAllowed(paths, componentSet(preset));
3295
+ if (preset.hub) started.push("hub");
3296
+ if (preset.collector && !awaiting.includes("collector")) started.push("collector");
3297
+ if (started.length > 0) {
3298
+ startUnits(host, started.map(unitFor));
3299
+ await verifySystemHealth(paths, state, host, options.startupTimeoutMs ?? 2e4, started);
3300
+ }
2952
3301
  }
3302
+ return {
3303
+ operation: "install",
3304
+ ok: true,
3305
+ apply: true,
3306
+ message: `installed ${release.version} for ${role} preset`,
3307
+ version: release.version,
3308
+ current: release.releaseDir,
3309
+ previous: null,
3310
+ state,
3311
+ password,
3312
+ ...awaiting.length > 0 ? { awaitingConnection: awaiting } : {},
3313
+ warnings: adoption?.warnings ?? []
3314
+ };
3315
+ } finally {
3316
+ discardStagedRelease(staged);
2953
3317
  }
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
3318
  }
2968
3319
  function systemStatus(options = {}) {
2969
3320
  const host = options.host ?? createSystemHost();
@@ -2976,14 +3327,14 @@ function systemStatus(options = {}) {
2976
3327
  } catch (error) {
2977
3328
  stateError = error instanceof Error ? error.message : String(error);
2978
3329
  }
2979
- const presentUnits = [UNIT_NAMES.hub, UNIT_NAMES.collector].filter((unit) => (0, import_node_fs3.existsSync)((0, import_node_path3.join)(paths.unitsDir, unit)));
3330
+ const presentUnits = [UNIT_NAMES.hub, UNIT_NAMES.collector].filter((unit) => (0, import_node_fs3.existsSync)((0, import_node_path4.join)(paths.unitsDir, unit)));
2980
3331
  const publicRole = presentUnits.length === 2 ? "local" : presentUnits[0] === UNIT_NAMES.hub ? "hub" : presentUnits[0] === UNIT_NAMES.collector ? "collector" : null;
2981
3332
  const role = state?.role ?? publicRole ?? "local";
2982
3333
  const current = currentReleaseDir(paths);
2983
3334
  let publicVersion = null;
2984
3335
  if (current) {
2985
3336
  try {
2986
- const manifest = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.join)(current, "package.json"), "utf8"));
3337
+ const manifest = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path4.join)(current, "package.json"), "utf8"));
2987
3338
  if (typeof manifest.version === "string") publicVersion = manifest.version;
2988
3339
  } catch {
2989
3340
  }
@@ -2994,7 +3345,7 @@ function systemStatus(options = {}) {
2994
3345
  const componentReports = {};
2995
3346
  for (const component of COMPONENTS) {
2996
3347
  const unit = unitFor(component);
2997
- const present = (0, import_node_fs3.existsSync)((0, import_node_path3.join)(paths.unitsDir, unit));
3348
+ const present = (0, import_node_fs3.existsSync)((0, import_node_path4.join)(paths.unitsDir, unit));
2998
3349
  const active = present && systemdActive(host, unit);
2999
3350
  const unitEnabled = present && systemdEnabled(host, unit);
3000
3351
  const enabled = state !== null ? state.components[component].enabled : unitEnabled;
@@ -3003,7 +3354,7 @@ function systemStatus(options = {}) {
3003
3354
  componentReports[component] = {
3004
3355
  component,
3005
3356
  unit,
3006
- path: (0, import_node_path3.join)(paths.unitsDir, unit),
3357
+ path: (0, import_node_path4.join)(paths.unitsDir, unit),
3007
3358
  present,
3008
3359
  enabled,
3009
3360
  unitEnabled,
@@ -3225,7 +3576,7 @@ async function enableSystem(options) {
3225
3576
  ],
3226
3577
  files: [
3227
3578
  { 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` }
3579
+ { path: (0, import_node_path4.join)(paths.unitsDir, unitFor(component)), mode: "0644", purpose: `${component} boot unit` }
3229
3580
  ],
3230
3581
  notes: [
3231
3582
  "Enables only the requested component; the other component keeps running.",
@@ -3338,7 +3689,7 @@ async function connectSystem(options) {
3338
3689
  const state = readSystemState(paths);
3339
3690
  if (state === null || !state.active) throw new SystemError("not-installed", "no active system installation; run install first");
3340
3691
  const destination = connectDestinationUrl(options.url);
3341
- const tokenFile = (0, import_node_path3.resolve)(options.tokenFile);
3692
+ const tokenFile = (0, import_node_path4.resolve)(options.tokenFile);
3342
3693
  const configured = collectorConfigured(paths);
3343
3694
  if (configured.configured) {
3344
3695
  throw new SystemError(
@@ -3373,10 +3724,6 @@ async function connectSystem(options) {
3373
3724
  ensurePowerAccess(paths, state.accounts, host);
3374
3725
  state.power = { enabled: true, rulePath: raplRulePath(paths), group: state.accounts.powerGroup };
3375
3726
  }
3376
- const existingIdentity = readCollectorIdentity(collectorStatePath(paths));
3377
- if (existingIdentity !== null) {
3378
- writeCollectorIdentity(collectorStatePath(paths), existingIdentity);
3379
- }
3380
3727
  const wasActive = systemdActive(host, UNIT_NAMES.collector);
3381
3728
  if (wasActive) {
3382
3729
  stopUnits(host, [UNIT_NAMES.collector]);
@@ -3390,12 +3737,12 @@ async function connectSystem(options) {
3390
3737
  `collector enrollment failed: ${error instanceof Error ? error.message : String(error)}`
3391
3738
  );
3392
3739
  } finally {
3393
- chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], state.accounts.collector, state.accounts.collector, host);
3740
+ chownAccount([(0, import_node_path4.join)(paths.stateDir, "collector")], state.accounts.collector, state.accounts.collector, host);
3394
3741
  }
3395
3742
  if (!collectorConfigured(paths).configured) {
3396
3743
  throw new SystemError("enrollment-failed", "the hub accepted enrollment but no destination was saved; check private state");
3397
3744
  }
3398
- chownAccount([(0, import_node_path3.join)(paths.stateDir, "collector")], state.accounts.collector, state.accounts.collector, host);
3745
+ chownAccount([(0, import_node_path4.join)(paths.stateDir, "collector")], state.accounts.collector, state.accounts.collector, host);
3399
3746
  writeSystemState(paths, state);
3400
3747
  const enabled = state.components.collector.enabled;
3401
3748
  if (enabled || wasActive) {
@@ -3416,6 +3763,60 @@ async function upgradeSystemUnlocked(options) {
3416
3763
  const host = options.host ?? createSystemHost();
3417
3764
  const now = options.now ?? Date.now;
3418
3765
  const paths = validateSystemPaths(options.paths);
3766
+ if (systemStateIsLegacy(paths)) {
3767
+ const source2 = (0, import_node_path4.resolve)(options.source);
3768
+ if (!(0, import_node_fs3.existsSync)(source2)) throw new SystemError("missing-source", `release source does not exist: ${source2}`);
3769
+ const role = legacySystemRole(paths);
3770
+ const preset = componentsForRole(role);
3771
+ const enable = options.enable ?? (options.start === true || unitNamesForComponents(preset).some((unit) => systemdEnabled(host, unit)));
3772
+ const units = [...Object.values(UNIT_NAMES), ...LEGACY_UNIT_NAMES];
3773
+ const plan2 = {
3774
+ operation: "upgrade",
3775
+ apply: options.apply === true,
3776
+ role,
3777
+ components: preset,
3778
+ paths,
3779
+ accounts: validateAccounts(options.accounts),
3780
+ commands: [
3781
+ "copy and validate the replacement release",
3782
+ ...units.map((unit) => `systemctl stop ${unit}`),
3783
+ "verify every owned runtime lease is stopped",
3784
+ "remove unfinished configuration, history and application releases",
3785
+ "install the canonical layout from the validated replacement",
3786
+ ...enable ? unitNamesForComponents(preset).map((unit) => `systemctl enable ${unit}`) : [],
3787
+ ...options.start ? unitNamesForComponents(preset).map((unit) => `systemctl start ${unit}`) : []
3788
+ ],
3789
+ files: [
3790
+ { path: stateFilePath(paths), mode: "0600", purpose: "canonical replacement state" },
3791
+ { path: (0, import_node_path4.join)(paths.stateDir, "backups"), mode: "0700", purpose: "removed with unfinished history" }
3792
+ ],
3793
+ notes: [
3794
+ "An unfinished installation is reset, not migrated; its history and credentials are removed.",
3795
+ "The replacement is copied and validated before any unit is stopped or state is removed.",
3796
+ "Both owned units and legacy combined units are stopped before the reset."
3797
+ ]
3798
+ };
3799
+ if (options.apply !== true) return { operation: "upgrade", ok: true, apply: false, message: "plan only", plan: plan2 };
3800
+ return installSystemUnlocked({
3801
+ apply: true,
3802
+ source: options.source,
3803
+ role,
3804
+ paths: options.paths,
3805
+ accounts: options.accounts,
3806
+ host: options.host,
3807
+ start: options.start,
3808
+ enable,
3809
+ installDeps: options.installDeps,
3810
+ installDepsCommand: options.installDepsCommand,
3811
+ replaceRelease: options.replaceRelease,
3812
+ startupTimeoutMs: options.startupTimeoutMs,
3813
+ homeRoots: options.homeRoots,
3814
+ // This is an existing system installation; unrelated user-scope units
3815
+ // must not block its reset.
3816
+ allowUserConflicts: options.allowUserConflicts ?? true,
3817
+ now: options.now
3818
+ });
3819
+ }
3419
3820
  const state = readSystemState(paths);
3420
3821
  if (state === null || !state.active) throw new SystemError("not-installed", "no active system installation; run install first");
3421
3822
  const activeComponents = COMPONENTS.filter((component) => systemdActive(host, unitFor(component)));
@@ -3428,7 +3829,7 @@ async function upgradeSystemUnlocked(options) {
3428
3829
  paths,
3429
3830
  accounts: state.accounts,
3430
3831
  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" }],
3832
+ files: [{ path: (0, import_node_path4.join)(paths.stateDir, "backups"), mode: "0700", purpose: "pre-upgrade database backup" }],
3432
3833
  notes: [
3433
3834
  "Staging happens before any service is stopped.",
3434
3835
  "The exact active component set is restored; --start does not start a component that was stopped.",
@@ -3438,91 +3839,105 @@ async function upgradeSystemUnlocked(options) {
3438
3839
  if (options.apply !== true) return { operation: "upgrade", ok: true, apply: false, message: "plan only", plan };
3439
3840
  assertApply(host);
3440
3841
  assertManagedPathSafety(paths, isDefaultSystemLayout(paths));
3441
- const source = (0, import_node_path3.resolve)(options.source);
3842
+ const source = (0, import_node_path4.resolve)(options.source);
3442
3843
  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;
3844
+ const staged = prepareStagedRelease({
3845
+ source,
3846
+ paths,
3847
+ host,
3848
+ installDeps: options.installDeps,
3849
+ installDepsCommand: options.installDepsCommand
3850
+ });
3457
3851
  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);
3852
+ if (state.currentVersion === staged.version) {
3853
+ throw new SystemError("same-version", `release ${staged.version} is already current; use a different version`);
3468
3854
  }
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
3855
+ assertReleaseTargetAvailable(paths, releaseTargetDir(paths, staged.version), options.replaceRelease);
3856
+ const oldCurrent = currentReleaseDir(paths);
3857
+ if (oldCurrent === null) throw new SystemError("invalid-state", "current release symlink is missing");
3858
+ const oldPrevious = previousReleaseDir(paths);
3859
+ let backupPath = null;
3860
+ let beforeSchema = null;
3861
+ let release = null;
3862
+ try {
3863
+ stopUnits(host, activeUnits);
3864
+ assertDirectoryStopped(paths.stateDir);
3865
+ const lock = systemLockState(paths);
3866
+ if (lock.live) throw new SystemError("duplicate-runtime", `hub did not stop cleanly; lock remains at ${lock.path}`);
3867
+ const database = hubDatabasePath(paths);
3868
+ if ((0, import_node_fs3.existsSync)(database)) {
3869
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "backups"), 448, true);
3870
+ beforeSchema = databaseSchema(database);
3871
+ backupPath = (0, import_node_path4.join)(paths.stateDir, "backups", `pre-upgrade-${staged.version}-${newestTimestamp()}.sqlite`);
3872
+ await backupDatabase(database, backupPath, host);
3873
+ }
3874
+ release = commitStagedRelease({ staged, paths, replaceRelease: options.replaceRelease });
3875
+ replaceSymlink((0, import_node_path4.join)(paths.prefix, "previous"), oldCurrent, host);
3876
+ replaceSymlink((0, import_node_path4.join)(paths.prefix, "current"), release.releaseDir, host);
3877
+ state.previousVersion = state.currentVersion;
3878
+ state.currentVersion = release.version;
3879
+ state.releases[release.version] = {
3880
+ releaseDir: release.releaseDir,
3881
+ schemaVersion: release.schemaVersion,
3882
+ installedAt: new Date(now()).toISOString()
3487
3883
  };
3884
+ if (backupPath !== null) {
3885
+ state.backups[(0, import_node_path4.basename)(backupPath)] = {
3886
+ path: backupPath,
3887
+ schemaVersion: beforeSchema,
3888
+ fromSchema: beforeSchema,
3889
+ toSchema: release.schemaVersion,
3890
+ createdAt: new Date(now()).toISOString(),
3891
+ fromVersion: state.previousVersion ?? void 0,
3892
+ toVersion: release.version
3893
+ };
3894
+ }
3895
+ writeSystemState(paths, state);
3896
+ if (activeComponents.length > 0) {
3897
+ startUnits(host, activeUnits);
3898
+ await verifySystemHealth(paths, state, host, options.startupTimeoutMs ?? 2e4, activeComponents);
3899
+ }
3900
+ } catch (error) {
3901
+ await recoverFailedUpgrade({
3902
+ paths,
3903
+ state,
3904
+ oldCurrent,
3905
+ oldPrevious,
3906
+ beforeSchema,
3907
+ backupPath,
3908
+ host,
3909
+ activeComponents,
3910
+ units: activeUnits
3911
+ });
3912
+ throw error;
3488
3913
  }
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,
3914
+ const upgraded = release;
3915
+ return {
3916
+ operation: "upgrade",
3917
+ ok: true,
3918
+ apply: true,
3919
+ message: `upgraded ${state.previousVersion ?? "previous"} -> ${upgraded.version}`,
3920
+ version: upgraded.version,
3921
+ current: upgraded.releaseDir,
3922
+ previous: oldCurrent,
3501
3923
  backupPath,
3502
- host,
3503
- activeComponents,
3504
- units: activeUnits
3505
- });
3506
- throw error;
3924
+ state
3925
+ };
3926
+ } finally {
3927
+ discardStagedRelease(staged);
3507
3928
  }
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
- };
3929
+ }
3930
+ function systemOperationLockPath(paths) {
3931
+ return (0, import_node_path4.join)(paths.prefix, "run", "operation.lock");
3519
3932
  }
3520
3933
  async function withSystemOperation(options, operation) {
3521
3934
  if (!options.apply) return operation();
3522
3935
  const host = options.host ?? createSystemHost();
3523
3936
  assertApply(host);
3524
3937
  const paths = validateSystemPaths(options.paths);
3525
- const release = acquireProcessLease((0, import_node_path3.join)(paths.stateDir, "backups/operation.lock"));
3938
+ ensureDirectory(paths.prefix, 493);
3939
+ ensureDirectory((0, import_node_path4.join)(paths.prefix, "run"), 448, true);
3940
+ const release = acquireProcessLease(systemOperationLockPath(paths));
3526
3941
  try {
3527
3942
  return await operation();
3528
3943
  } finally {
@@ -3573,7 +3988,7 @@ async function rollbackSystemUnlocked(options = {}) {
3573
3988
  try {
3574
3989
  stopUnits(host, units);
3575
3990
  assertDirectoryStopped(paths.stateDir);
3576
- if (systemLockState(paths).live || lockIsLive((0, import_node_path3.join)(paths.stateDir, "collector/collector-runtime.lock")).live) {
3991
+ if (systemLockState(paths).live || lockIsLive((0, import_node_path4.join)(paths.stateDir, "collector/collector-runtime.lock")).live) {
3577
3992
  throw new SystemError("runtime-active", "runtime lease remains after stop; rollback refused");
3578
3993
  }
3579
3994
  const database = hubDatabasePath(paths);
@@ -3589,8 +4004,8 @@ async function rollbackSystemUnlocked(options = {}) {
3589
4004
  }
3590
4005
  }
3591
4006
  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`);
4007
+ ensureDirectory((0, import_node_path4.join)(paths.stateDir, "backups"), 448, true);
4008
+ safetyBackup = (0, import_node_path4.join)(paths.stateDir, "backups", `pre-rollback-${newestTimestamp()}.sqlite`);
3594
4009
  await backupDatabase(database, safetyBackup, host);
3595
4010
  }
3596
4011
  if (backupToRestore !== null) {
@@ -3598,8 +4013,8 @@ async function rollbackSystemUnlocked(options = {}) {
3598
4013
  restoredDatabase = true;
3599
4014
  runRequired(host, "chown", [`${state.accounts.hub}:${state.accounts.hub}`, database], "restored database ownership");
3600
4015
  }
3601
- replaceSymlink((0, import_node_path3.join)(paths.prefix, "previous"), oldCurrent, host);
3602
- replaceSymlink((0, import_node_path3.join)(paths.prefix, "current"), previous, host);
4016
+ replaceSymlink((0, import_node_path4.join)(paths.prefix, "previous"), oldCurrent, host);
4017
+ replaceSymlink((0, import_node_path4.join)(paths.prefix, "current"), previous, host);
3603
4018
  state.previousVersion = state.currentVersion;
3604
4019
  state.currentVersion = previousVersion;
3605
4020
  state.updatedAt = new Date(now()).toISOString();
@@ -3621,10 +4036,10 @@ async function rollbackSystemUnlocked(options = {}) {
3621
4036
  };
3622
4037
  } catch (error) {
3623
4038
  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);
4039
+ replaceSymlink((0, import_node_path4.join)(paths.prefix, "current"), oldCurrent, host);
4040
+ replaceSymlink((0, import_node_path4.join)(paths.prefix, "previous"), previous, host);
3626
4041
  if (restoredDatabase && safetyBackup) {
3627
- const failed = (0, import_node_path3.join)(paths.stateDir, "backups", `failed-rollback-${newestTimestamp()}.sqlite`);
4042
+ const failed = (0, import_node_path4.join)(paths.stateDir, "backups", `failed-rollback-${newestTimestamp()}.sqlite`);
3628
4043
  await backupDatabase(hubDatabasePath(paths), failed, host);
3629
4044
  replaceDatabaseFromBackup(safetyBackup, hubDatabasePath(paths), null, host);
3630
4045
  runRequired(host, "chown", [`${state.accounts.hub}:${state.accounts.hub}`, hubDatabasePath(paths)], "restored database ownership");
@@ -3641,8 +4056,9 @@ function uninstallSystem(options = {}) {
3641
4056
  const host = options.host ?? createSystemHost();
3642
4057
  const now = options.now ?? Date.now;
3643
4058
  const paths = validateSystemPaths(options.paths);
3644
- const state = readSystemState(paths);
3645
- const units = [UNIT_NAMES.hub, UNIT_NAMES.collector];
4059
+ const state = systemStateIsLegacy(paths) ? null : readSystemState(paths);
4060
+ const plannedUnits = [...Object.values(UNIT_NAMES), ...LEGACY_UNIT_NAMES];
4061
+ const units = options.apply === true ? unitsForCleanup(paths, host) : plannedUnits;
3646
4062
  const plan = {
3647
4063
  operation: "uninstall",
3648
4064
  apply: options.apply === true,
@@ -3680,7 +4096,7 @@ function uninstallSystem(options = {}) {
3680
4096
  assertDirectoryStopped(paths.stateDir);
3681
4097
  for (const unit of units) runMaybe(host, "systemctl", ["disable", unit]);
3682
4098
  for (const unit of units) {
3683
- const path = (0, import_node_path3.join)(paths.unitsDir, unit);
4099
+ const path = (0, import_node_path4.join)(paths.unitsDir, unit);
3684
4100
  assertManagedFileOwnership(paths, path);
3685
4101
  try {
3686
4102
  (0, import_node_fs3.unlinkSync)(path);
@@ -3701,10 +4117,10 @@ function uninstallSystem(options = {}) {
3701
4117
  restorePowerOriginals(paths, host);
3702
4118
  resumePowerRule(paths, host);
3703
4119
  }
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 });
4120
+ removeSymlink((0, import_node_path4.join)(paths.prefix, "current"));
4121
+ removeSymlink((0, import_node_path4.join)(paths.prefix, "previous"));
4122
+ (0, import_node_fs3.rmSync)((0, import_node_path4.join)(paths.prefix, "releases"), { recursive: true, force: true });
4123
+ if (options.preserveNode !== true) (0, import_node_fs3.rmSync)((0, import_node_path4.join)(paths.prefix, "node"), { recursive: true, force: true });
3708
4124
  if (state !== null) {
3709
4125
  state.active = false;
3710
4126
  state.power = null;
@@ -4088,7 +4504,7 @@ ${humanPlan(report.plan)}`);
4088
4504
  if (parsed.command === "logs") {
4089
4505
  const selected = validateSystemPaths(paths);
4090
4506
  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)));
4507
+ const units = (component ? [unitFor(component)] : [UNIT_NAMES.hub, UNIT_NAMES.collector]).filter((unit) => (0, import_node_fs3.existsSync)((0, import_node_path4.join)(selected.unitsDir, unit)));
4092
4508
  if (units.length === 0) throw new SystemError("not-installed", component ? `${component} logs are unavailable; its unit is not installed` : "no system services installed");
4093
4509
  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
4510
  if (result.error) throw result.error;
@@ -4153,6 +4569,7 @@ ${humanPlan(report.plan)}`);
4153
4569
  paths,
4154
4570
  host,
4155
4571
  start: boolValue(values, "start"),
4572
+ enable: boolValue(values, "enable") || void 0,
4156
4573
  installDeps: boolValue(values, "install-deps"),
4157
4574
  installDepsCommand: stringValue(values, "install-deps-command"),
4158
4575
  replaceRelease: boolValue(values, "replace-release"),
@@ -4160,8 +4577,10 @@ ${humanPlan(report.plan)}`);
4160
4577
  });
4161
4578
  if (json) console.log(formatSystemReport(report));
4162
4579
  else if (report.plan) console.log(humanPlan(report.plan));
4163
- else console.log(`${report.message}
4164
- Backup: ${report.backupPath ?? "none"}`);
4580
+ else {
4581
+ printMutationReport(report);
4582
+ console.log(`Backup: ${report.backupPath ?? "none"}`);
4583
+ }
4165
4584
  return 0;
4166
4585
  }
4167
4586
  if (parsed.command === "rollback") {
@@ -4274,7 +4693,6 @@ if (isMainModule()) {
4274
4693
  RAPL_RULE_NAME,
4275
4694
  STATE_FILE_NAME,
4276
4695
  SYSTEM_SCOPE,
4277
- SYSTEM_STATE_FORMAT_VERSION,
4278
4696
  SYSUSERS_NAME,
4279
4697
  SystemError,
4280
4698
  UNIT_NAMES,
@@ -4319,6 +4737,7 @@ if (isMainModule()) {
4319
4737
  startSystem,
4320
4738
  stateFilePath,
4321
4739
  stopSystem,
4740
+ systemStateIsLegacy,
4322
4741
  systemStatus,
4323
4742
  systemdQuote,
4324
4743
  sysusersPath,