systemd-ts 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
2
  import { realpathSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { execFile } from "node:child_process";
5
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
6
  import { promisify } from "node:util";
7
7
  import { join } from "node:path";
8
8
  //#region src/main/errors.ts
@@ -144,6 +144,30 @@ var NotifySendError = class extends SystemdTsError {
144
144
  this.stdout = details.stdout;
145
145
  }
146
146
  };
147
+ var SystemctlCommandError = class extends SystemdTsError {
148
+ args;
149
+ command;
150
+ environmentReason;
151
+ exitCode;
152
+ operation;
153
+ reason;
154
+ stage;
155
+ stderr;
156
+ stdout;
157
+ constructor(message, options = {}) {
158
+ super(`SYSTEMD_TS_SYSTEMCTL`, message, options);
159
+ const details = extractCommandErrorDetails(options.cause);
160
+ this.args = options.args;
161
+ this.command = options.command;
162
+ this.environmentReason = options.environmentReason ?? classifyCommandEnvironmentReason(options.cause);
163
+ this.exitCode = details.exitCode;
164
+ this.operation = options.operation;
165
+ this.reason = options.reason;
166
+ this.stage = options.stage;
167
+ this.stderr = details.stderr;
168
+ this.stdout = details.stdout;
169
+ }
170
+ };
147
171
  function classifyCommandEnvironmentReason(cause) {
148
172
  if (cause === null || typeof cause !== `object`) return;
149
173
  const record = cause;
@@ -245,7 +269,7 @@ var Executable = class {
245
269
  if (!commandParts.ok) return commandParts;
246
270
  return {
247
271
  ok: true,
248
- value: commandParts.value.map(shellQuote$1).join(` `)
272
+ value: commandParts.value.map(shellQuote).join(` `)
249
273
  };
250
274
  }
251
275
  };
@@ -295,7 +319,7 @@ function isMainModule(modulePath) {
295
319
  if (mainArg === void 0) return false;
296
320
  return normalizeFilePath(mainArg) === normalizeFilePath(modulePath);
297
321
  }
298
- function shellQuote$1(value) {
322
+ function shellQuote(value) {
299
323
  return `'${value.replaceAll(`'`, `'\\''`)}'`;
300
324
  }
301
325
  function normalizeFilePath(path) {
@@ -311,6 +335,7 @@ var internal_exports = /* @__PURE__ */ __exportAll({
311
335
  cloneUnitSection: () => cloneUnitSection,
312
336
  defaultCommandExecutor: () => defaultCommandExecutor,
313
337
  defaultUnitDirForScope: () => defaultUnitDirForScope,
338
+ extractCommandOutput: () => extractCommandOutput,
314
339
  fileExists: () => fileExists,
315
340
  freezeUnitOptions: () => freezeUnitOptions,
316
341
  normalizeUnitName: () => normalizeUnitName,
@@ -318,7 +343,6 @@ var internal_exports = /* @__PURE__ */ __exportAll({
318
343
  renderUnitFile: () => renderUnitFile,
319
344
  resolveTimerTargetUnit: () => resolveTimerTargetUnit,
320
345
  sendNotify: () => sendNotify,
321
- shellQuote: () => shellQuote,
322
346
  validateServiceSection: () => validateServiceSection
323
347
  });
324
348
  const EXEC_DIRECTIVE_KEYS = [
@@ -422,11 +446,11 @@ function renderUnitFile(sections) {
422
446
  };
423
447
  }
424
448
  }
425
- function shellQuote(value) {
426
- return `'${value.replaceAll(`'`, `'\\''`)}'`;
427
- }
428
- async function defaultCommandExecutor(command, args) {
429
- const result = await execFileAsync(command, [...args]);
449
+ async function defaultCommandExecutor(command, args, options = {}) {
450
+ const result = await execFileAsync(command, [...args], options.env === void 0 ? {} : { env: {
451
+ ...process.env,
452
+ ...options.env
453
+ } });
430
454
  return {
431
455
  stderr: result.stderr,
432
456
  stdout: result.stdout
@@ -441,18 +465,17 @@ async function fileExists(path) {
441
465
  }
442
466
  }
443
467
  async function sendNotify(state, options) {
444
- const args = [state];
468
+ const args = [`--no-block`, state];
445
469
  if (options.pid !== void 0) args.push(`MAINPID=${options.pid}`);
446
470
  if (options.status !== void 0) args.push(`STATUS=${options.status}`);
447
471
  if (options.executor !== void 0) {
448
- const command = buildNotifyShellCommand(args, options.socketPath);
449
472
  try {
450
- await options.executor(`bash`, [`-lc`, command]);
473
+ await options.executor(`systemd-notify`, args, options.socketPath === void 0 ? {} : { env: { NOTIFY_SOCKET: options.socketPath } });
451
474
  } catch (cause) {
452
475
  throw new NotifySendError(`Failed to send systemd notification through the configured executor`, {
453
- args: [`-lc`, command],
476
+ args,
454
477
  cause,
455
- command: `bash`,
478
+ command: `systemd-notify`,
456
479
  reason: `executor-failed`,
457
480
  stage: `executor`
458
481
  });
@@ -474,6 +497,17 @@ async function sendNotify(state, options) {
474
497
  });
475
498
  }
476
499
  }
500
+ function extractCommandOutput(cause) {
501
+ if (cause === null || typeof cause !== `object`) return;
502
+ const record = cause;
503
+ const stdout = typeof record[`stdout`] === `string` ? record[`stdout`] : void 0;
504
+ const stderr = typeof record[`stderr`] === `string` ? record[`stderr`] : void 0;
505
+ if (stdout === void 0 && stderr === void 0) return;
506
+ return {
507
+ stderr: stderr ?? ``,
508
+ stdout: stdout ?? ``
509
+ };
510
+ }
477
511
  function hasServiceSection(options) {
478
512
  return `service` in options;
479
513
  }
@@ -546,9 +580,6 @@ function isAbsoluteExecCommand(value) {
546
580
  }
547
581
  return value[index] === `/`;
548
582
  }
549
- function buildNotifyShellCommand(args, socketPath) {
550
- return `${socketPath === void 0 ? `` : `NOTIFY_SOCKET=${shellQuote(socketPath)} `}${[`systemd-notify`, ...args].map(shellQuote).join(` `)}`;
551
- }
552
583
  //#endregion
553
584
  //#region src/main/notify.ts
554
585
  /**
@@ -730,6 +761,224 @@ var SystemdTimer = class {
730
761
  }
731
762
  };
732
763
  //#endregion
764
+ //#region src/main/systemctl.ts
765
+ /**
766
+ * A TypeScript-first client for invoking `systemctl` with structured argument
767
+ * vectors instead of shell-composed command strings.
768
+ *
769
+ * This lower-level primitive is useful both on its own and as the subprocess
770
+ * transport underneath higher-level abstractions such as {@link Systemd}.
771
+ */
772
+ var Systemctl = class {
773
+ /** Command executor used for `systemctl` subprocesses. */
774
+ executor;
775
+ /** The target manager scope, either `system` or `user`. */
776
+ scope;
777
+ constructor(options = {}) {
778
+ this.scope = options.scope ?? `system`;
779
+ this.executor = options.executor ?? defaultCommandExecutor;
780
+ Object.freeze(this);
781
+ }
782
+ /** Reloads the targeted systemd manager. */
783
+ async daemonReload() {
784
+ return this.run(`daemon-reload`);
785
+ }
786
+ /** Enables a unit by filename. */
787
+ async enable(unit) {
788
+ return this.run(`enable`, unit);
789
+ }
790
+ /** Disables a unit by filename. */
791
+ async disable(unit) {
792
+ return this.run(`disable`, unit);
793
+ }
794
+ /** Links a unit file path into the targeted manager. */
795
+ async link(path) {
796
+ return this.run(`link`, path);
797
+ }
798
+ /**
799
+ * Checks the install-time enablement state of a unit file.
800
+ *
801
+ * Source:
802
+ * - systemd v260.1, `systemctl(1)`
803
+ */
804
+ async isEnabled(unit, options = {}) {
805
+ const output = await this.run(`is-enabled`, ...options.full ? [`--full`] : [], ...options.quiet ? [`--quiet`] : [], unit);
806
+ if (!output.ok) return output;
807
+ return ok$1(firstNonEmptyOutputLine(output.value));
808
+ }
809
+ /**
810
+ * Checks the runtime active state of a unit.
811
+ *
812
+ * Unless `quiet` is set, this returns the state string printed by
813
+ * `systemctl is-active`.
814
+ */
815
+ async isActive(unit, options = {}) {
816
+ const output = await this.run(`is-active`, ...options.quiet ? [`--quiet`] : [], unit);
817
+ if (!output.ok) return output;
818
+ return ok$1(firstNonEmptyOutputLine(output.value));
819
+ }
820
+ /**
821
+ * Checks whether a unit is in the failed state.
822
+ *
823
+ * Unless `quiet` is set, this returns the state string printed by
824
+ * `systemctl is-failed`.
825
+ */
826
+ async isFailed(unit, options = {}) {
827
+ const output = await this.run(`is-failed`, ...options.quiet ? [`--quiet`] : [], unit);
828
+ if (!output.ok) return output;
829
+ return ok$1(firstNonEmptyOutputLine(output.value));
830
+ }
831
+ /**
832
+ * Queries the overall manager state reported by `systemctl is-system-running`.
833
+ *
834
+ * Source:
835
+ * - systemd v260.1, `systemctl(1)`
836
+ */
837
+ async isSystemRunning(options = {}) {
838
+ const output = await this.run(`is-system-running`, ...options.wait ? [`--wait`] : []);
839
+ if (!output.ok) return output;
840
+ return ok$1(firstNonEmptyOutputLine(output.value));
841
+ }
842
+ /**
843
+ * Queries `systemctl show` for a unit.
844
+ *
845
+ * When `properties` are provided, only those properties are requested.
846
+ */
847
+ async show(unit, options = {}) {
848
+ const propertyFlag = options.properties === void 0 || options.properties.length === 0 ? [] : [`--property=${options.properties.join(`,`)}`];
849
+ return this.run(`show`, unit, ...propertyFlag);
850
+ }
851
+ /**
852
+ * Queries `systemctl show` and parses the result into a key-value object.
853
+ *
854
+ * Properties are returned exactly as printed by `systemctl show`.
855
+ */
856
+ async showProperties(unit, options = {}) {
857
+ const output = await this.show(unit, options);
858
+ if (!output.ok) return output;
859
+ return ok$1(parseSystemctlShowOutput(output.value.stdout));
860
+ }
861
+ /**
862
+ * Queries the standard status-related `show` properties for a unit and
863
+ * returns a parsed snapshot.
864
+ */
865
+ async showStatus(unit) {
866
+ return this.showServiceStatus(unit);
867
+ }
868
+ /**
869
+ * Queries the standard service-status `show` properties for a service unit
870
+ * and returns a parsed snapshot.
871
+ */
872
+ async showServiceStatus(unit) {
873
+ const output = await this.show(unit, { properties: [
874
+ `Id`,
875
+ `ActiveState`,
876
+ `SubState`,
877
+ `Result`,
878
+ `ExecMainStatus`
879
+ ] });
880
+ if (!output.ok) return output;
881
+ return ok$1(parseStartStatus(unit, output.value.stdout));
882
+ }
883
+ /**
884
+ * Queries `systemctl list-timers` and returns the observed JSON payload
885
+ * shape directly, with raw integer timestamps left intact.
886
+ */
887
+ async listTimers(options = {}) {
888
+ const args = [
889
+ `list-timers`,
890
+ ...options.all ? [`--all`] : [],
891
+ ...options.noPager ?? true ? [`--no-pager`] : [],
892
+ `--output=json`,
893
+ ...options.patterns ?? []
894
+ ];
895
+ const output = await this.run(...args);
896
+ if (!output.ok) return output;
897
+ try {
898
+ return ok$1(JSON.parse(output.value.stdout));
899
+ } catch (cause) {
900
+ return err$1(new SystemctlCommandError(`Failed to parse JSON output from systemctl list-timers`, {
901
+ args: [...this.scopeArgs(), ...args],
902
+ cause,
903
+ command: `systemctl`,
904
+ operation: `list-timers`,
905
+ reason: `invalid-json`
906
+ }));
907
+ }
908
+ }
909
+ /** Starts a unit by filename. */
910
+ async start(unit) {
911
+ return this.run(`start`, unit);
912
+ }
913
+ /** Stops a unit by filename. */
914
+ async stop(unit) {
915
+ return this.run(`stop`, unit);
916
+ }
917
+ /** Restarts a unit by filename. */
918
+ async restart(unit) {
919
+ return this.run(`restart`, unit);
920
+ }
921
+ /**
922
+ * Resets the failed state for matching units, or for all units when no
923
+ * patterns are provided.
924
+ */
925
+ async resetFailed(patterns = []) {
926
+ return this.run(`reset-failed`, ...patterns);
927
+ }
928
+ /**
929
+ * Queries `systemctl status` for a unit.
930
+ *
931
+ * This is primarily a human-oriented command and is best paired with
932
+ * `show()` when structured state is needed.
933
+ */
934
+ async status(unit, options = {}) {
935
+ const args = [`status`, unit];
936
+ if (options.noPager ?? true) args.push(`--no-pager`);
937
+ if (options.lines !== void 0) args.push(`--lines`, String(options.lines));
938
+ return this.run(...args);
939
+ }
940
+ /** Executes a raw `systemctl` invocation with scope-aware arguments. */
941
+ async run(...args) {
942
+ const scopedArgs = [...this.scopeArgs(), ...args];
943
+ try {
944
+ return ok$1(await this.executor(`systemctl`, scopedArgs));
945
+ } catch (cause) {
946
+ return err$1(new SystemctlCommandError(`Failed to run systemctl ${args[0] ?? `command`}`, {
947
+ args: scopedArgs,
948
+ cause,
949
+ command: `systemctl`,
950
+ operation: args[0],
951
+ reason: `executor-failed`
952
+ }));
953
+ }
954
+ }
955
+ scopeArgs() {
956
+ return this.scope === `user` ? [`--user`] : [];
957
+ }
958
+ };
959
+ function ok$1(value) {
960
+ return {
961
+ ok: true,
962
+ value
963
+ };
964
+ }
965
+ function err$1(error) {
966
+ return {
967
+ ok: false,
968
+ error
969
+ };
970
+ }
971
+ function parseSystemctlShowOutput(output) {
972
+ return Object.freeze(Object.fromEntries(output.split(`\n`).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
973
+ const separatorIndex = line.indexOf(`=`);
974
+ if (separatorIndex === -1) return [line, ``];
975
+ return [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)];
976
+ })));
977
+ }
978
+ function firstNonEmptyOutputLine(output) {
979
+ return [output.stdout, output.stderr].flatMap((value) => value.split(`\n`)).map((value) => value.trim()).find((value) => value.length > 0) ?? ``;
980
+ }
981
+ //#endregion
733
982
  //#region src/main/systemd.ts
734
983
  /**
735
984
  * A successful systemd unit materialization.
@@ -765,6 +1014,10 @@ var Systemd = class {
765
1014
  linkUnits;
766
1015
  /** The target manager scope, either `system` or `user`. */
767
1016
  scope;
1017
+ /** The low-level `systemctl` client for this configured target. */
1018
+ systemctl;
1019
+ /** Higher-level `systemd-ts` upkeep helpers for application-owned units. */
1020
+ ts;
768
1021
  /** The directory used when materializing unit files. */
769
1022
  unitDir;
770
1023
  /**
@@ -779,6 +1032,11 @@ var Systemd = class {
779
1032
  this.unitDir = options.unitDir ?? defaultUnitDirForScope(this.scope);
780
1033
  this.linkUnits = options.linkUnits ?? false;
781
1034
  this.executor = options.executor ?? defaultCommandExecutor;
1035
+ this.systemctl = new Systemctl({
1036
+ executor: this.executor,
1037
+ scope: this.scope
1038
+ });
1039
+ this.ts = new SystemdTs(this);
782
1040
  Object.freeze(this);
783
1041
  }
784
1042
  /**
@@ -816,16 +1074,17 @@ var Systemd = class {
816
1074
  `enable`,
817
1075
  unit.filename
818
1076
  ];
819
- try {
820
- await this.executor(`systemctl`, args);
821
- } catch (cause) {
822
- return err(new UnitEnableError(`Failed to enable ${unit.filename}`, {
1077
+ const enabled = await this.systemctl.enable(unit.filename);
1078
+ if (!enabled.ok) {
1079
+ const errorOptions = {
823
1080
  args,
824
- cause,
1081
+ cause: enabled.error,
825
1082
  command: `systemctl`,
826
1083
  stage: `enable`,
827
- unitName: unit.filename
828
- }));
1084
+ unitName: unit.filename,
1085
+ ...enabled.error.environmentReason === void 0 ? {} : { environmentReason: enabled.error.environmentReason }
1086
+ };
1087
+ return err(new UnitEnableError(`Failed to enable ${unit.filename}`, errorOptions));
829
1088
  }
830
1089
  }
831
1090
  return ok(void 0);
@@ -855,38 +1114,38 @@ var Systemd = class {
855
1114
  `start`,
856
1115
  unit.filename
857
1116
  ];
858
- try {
859
- await this.executor(`systemctl`, startArgs);
860
- } catch (cause) {
861
- return err(new UnitStartError(`Failed to start ${unit.filename}`, {
1117
+ const started = await this.systemctl.start(unit.filename);
1118
+ if (!started.ok) {
1119
+ const errorOptions = {
862
1120
  args: startArgs,
863
- cause,
1121
+ cause: started.error,
864
1122
  command: `systemctl`,
865
1123
  diagnostics: await this.collectStartDiagnostics(scopeArgs, unit.filename),
866
1124
  stage: `start`,
867
- unitName: unit.filename
868
- }));
1125
+ unitName: unit.filename,
1126
+ ...started.error.environmentReason === void 0 ? {} : { environmentReason: started.error.environmentReason }
1127
+ };
1128
+ return err(new UnitStartError(`Failed to start ${unit.filename}`, errorOptions));
869
1129
  }
870
- const statusArgs = [
871
- ...scopeArgs,
872
- `show`,
873
- unit.filename,
874
- `--property=Id,ActiveState,SubState,Result,ExecMainStatus`
875
- ];
876
- let status;
877
- try {
878
- status = await this.executor(`systemctl`, statusArgs);
879
- } catch (cause) {
880
- return err(new UnitStartError(`Started ${unit.filename} but failed to query its status`, {
881
- args: statusArgs,
882
- cause,
1130
+ const status = await this.systemctl.showServiceStatus(unit.filename);
1131
+ if (!status.ok) {
1132
+ const errorOptions = {
1133
+ args: [
1134
+ ...scopeArgs,
1135
+ `show`,
1136
+ unit.filename,
1137
+ `--property=Id,ActiveState,SubState,Result,ExecMainStatus`
1138
+ ],
1139
+ cause: status.error,
883
1140
  command: `systemctl`,
884
1141
  diagnostics: await this.collectStartDiagnostics(scopeArgs, unit.filename),
885
1142
  stage: `show-status`,
886
- unitName: unit.filename
887
- }));
1143
+ unitName: unit.filename,
1144
+ ...status.error.environmentReason === void 0 ? {} : { environmentReason: status.error.environmentReason }
1145
+ };
1146
+ return err(new UnitStartError(`Started ${unit.filename} but failed to query its status`, errorOptions));
888
1147
  }
889
- return ok(parseStartStatus(unit.filename, status.stdout));
1148
+ return ok(status.value);
890
1149
  }
891
1150
  /**
892
1151
  * Reads recent output for a managed unit.
@@ -910,29 +1169,26 @@ var Systemd = class {
910
1169
  }
911
1170
  const scopeArgs = this.scopeArgs();
912
1171
  const lines = options?.lines ?? 50;
913
- const args = [`-lc`, `${[
914
- `systemctl`,
1172
+ const logs = await this.systemctl.status(unit.filename, { lines });
1173
+ if (logs.ok) return ok(joinCommandOutput(logs.value));
1174
+ const errorOutput = extractCommandOutput(logs.error);
1175
+ if (errorOutput !== void 0) return ok(joinCommandOutput(errorOutput));
1176
+ const args = [
915
1177
  ...scopeArgs,
916
1178
  `status`,
917
1179
  unit.filename,
918
1180
  `--no-pager`,
919
1181
  `--lines`,
920
1182
  String(lines)
921
- ].map(shellQuote).join(` `)} 2>&1 || true`];
922
- let logs;
923
- try {
924
- logs = await this.executor(`bash`, args);
925
- } catch (cause) {
926
- return err(new UnitLogsReadError(`Failed to query logs for ${unit.filename} from systemctl status`, {
927
- args,
928
- cause,
929
- command: `bash`,
930
- reason: `status-command-failed`,
931
- stage: `status`,
932
- unitName: unit.filename
933
- }));
934
- }
935
- return ok([logs.stdout, logs.stderr].filter((value) => value.length > 0).join(`\n`));
1183
+ ];
1184
+ return err(new UnitLogsReadError(`Failed to query logs for ${unit.filename} from systemctl status`, {
1185
+ args,
1186
+ cause: logs.error,
1187
+ command: `systemctl`,
1188
+ reason: `status-command-failed`,
1189
+ stage: `status`,
1190
+ unitName: unit.filename
1191
+ }));
936
1192
  }
937
1193
  /** Returns the on-disk unit-file path this instance uses for the given unit. */
938
1194
  pathFor(unit) {
@@ -947,39 +1203,34 @@ var Systemd = class {
947
1203
  `link`,
948
1204
  path
949
1205
  ];
950
- try {
951
- await this.executor(`systemctl`, args);
952
- } catch (cause) {
1206
+ const linked = await this.systemctl.link(path);
1207
+ if (!linked.ok) {
953
1208
  const linkedUnit = units.find((candidate) => this.pathFor(candidate) === path);
954
1209
  const errorContext = {
955
1210
  args,
956
- cause,
1211
+ cause: linked.error,
957
1212
  command: `systemctl`,
958
1213
  stage: `link`,
959
- ...linkedUnit === void 0 ? {} : { unitName: linkedUnit.filename },
960
- unitPath: path
1214
+ unitPath: path,
1215
+ ...linked.error.environmentReason === void 0 ? {} : { environmentReason: linked.error.environmentReason },
1216
+ ...linkedUnit === void 0 ? {} : { unitName: linkedUnit.filename }
961
1217
  };
962
1218
  throw operation === `enable` ? new UnitEnableError(`Failed to link ${path} before enable`, errorContext) : new UnitStartError(`Failed to link ${path} before start`, errorContext);
963
1219
  }
964
1220
  }
965
1221
  }
966
1222
  const args = [...scopeArgs, `daemon-reload`];
967
- try {
968
- await this.executor(`systemctl`, args);
969
- } catch (cause) {
970
- throw operation === `enable` ? new UnitEnableError(`Failed to reload systemd before enable`, {
971
- args,
972
- cause,
973
- command: `systemctl`,
974
- stage: `daemon-reload`,
975
- unitName: units[0]?.filename
976
- }) : new UnitStartError(`Failed to reload systemd before start`, {
1223
+ const reloaded = await this.systemctl.daemonReload();
1224
+ if (!reloaded.ok) {
1225
+ const errorContext = {
977
1226
  args,
978
- cause,
1227
+ cause: reloaded.error,
979
1228
  command: `systemctl`,
980
1229
  stage: `daemon-reload`,
981
- unitName: units[0]?.filename
982
- });
1230
+ ...reloaded.error.environmentReason === void 0 ? {} : { environmentReason: reloaded.error.environmentReason },
1231
+ ...units[0]?.filename === void 0 ? {} : { unitName: units[0].filename }
1232
+ };
1233
+ throw operation === `enable` ? new UnitEnableError(`Failed to reload systemd before enable`, errorContext) : new UnitStartError(`Failed to reload systemd before start`, errorContext);
983
1234
  }
984
1235
  }
985
1236
  scopeArgs() {
@@ -987,29 +1238,31 @@ var Systemd = class {
987
1238
  }
988
1239
  async collectStartDiagnostics(scopeArgs, unitName) {
989
1240
  const diagnostics = {};
990
- const showCommand = [
991
- `systemctl`,
1241
+ const showArgs = [
992
1242
  ...scopeArgs,
993
1243
  `show`,
994
1244
  unitName,
995
1245
  `--property=Id,ActiveState,SubState,Result,ExecMainStatus`
996
- ].map(shellQuote).join(` `);
997
- const show = await this.tryBestEffortCommand(`bash`, [`-lc`, `${showCommand} 2>&1 || true`]);
998
- if (show !== void 0 && show.length > 0) {
999
- diagnostics.showOutput = show;
1000
- diagnostics.showStatus = parseStartStatus(unitName, show);
1246
+ ];
1247
+ const show = await this.tryBestEffortCommand(`systemctl`, showArgs);
1248
+ if (show !== void 0) {
1249
+ const showOutput = joinCommandOutput(show);
1250
+ if (showOutput.length > 0) diagnostics.showOutput = showOutput;
1251
+ if (show.stdout.length > 0) diagnostics.showStatus = parseStartStatus(unitName, show.stdout);
1001
1252
  }
1002
- const statusCommand = [
1003
- `systemctl`,
1253
+ const statusArgs = [
1004
1254
  ...scopeArgs,
1005
1255
  `status`,
1006
1256
  unitName,
1007
1257
  `--no-pager`,
1008
1258
  `--lines`,
1009
1259
  `20`
1010
- ].map(shellQuote).join(` `);
1011
- const status = await this.tryBestEffortCommand(`bash`, [`-lc`, `${statusCommand} 2>&1 || true`]);
1012
- if (status !== void 0 && status.length > 0) diagnostics.statusOutput = status;
1260
+ ];
1261
+ const status = await this.tryBestEffortCommand(`systemctl`, statusArgs);
1262
+ if (status !== void 0) {
1263
+ const statusOutput = joinCommandOutput(status);
1264
+ if (statusOutput.length > 0) diagnostics.statusOutput = statusOutput;
1265
+ }
1013
1266
  return diagnostics;
1014
1267
  }
1015
1268
  async collectLinkPaths(units) {
@@ -1063,11 +1316,240 @@ var Systemd = class {
1063
1316
  }
1064
1317
  async tryBestEffortCommand(command, args) {
1065
1318
  try {
1066
- return (await this.executor(command, args)).stdout;
1319
+ return await this.executor(command, args);
1320
+ } catch (cause) {
1321
+ const output = extractCommandOutput(cause);
1322
+ if (output !== void 0) return output;
1323
+ return;
1324
+ }
1325
+ }
1326
+ };
1327
+ /**
1328
+ * Opinionated `systemd-ts` upkeep helpers for application-owned units.
1329
+ *
1330
+ * This layer is intentionally analogous to a subset of `portablectl`'s
1331
+ * ownership workflow, but it operates on regular materialized units instead of
1332
+ * portable service images.
1333
+ */
1334
+ var SystemdTs = class {
1335
+ systemd;
1336
+ constructor(systemd) {
1337
+ this.systemd = systemd;
1338
+ Object.freeze(this);
1339
+ }
1340
+ async attach(units, options) {
1341
+ if (units.length === 0) return err(new NoUnitsProvidedError(`Systemd.ts.attach()`));
1342
+ const described = await this.describeUnits(units);
1343
+ if (!described.ok) return described;
1344
+ const manifestPath = this.manifestPathFor(options.owner);
1345
+ const prior = await this.readManifest(options.owner);
1346
+ const diff = await this.diffManifest(prior, described.value);
1347
+ const materialized = await this.systemd.materialize(...units);
1348
+ if (!materialized.ok) return materialized;
1349
+ const enabled = [];
1350
+ if (options.enable ?? false) {
1351
+ const result = await this.systemd.enable(...units);
1352
+ if (!result.ok) return result;
1353
+ enabled.push(...units.map((unit) => unit.filename));
1354
+ }
1355
+ const started = [];
1356
+ if (options.start ?? false) for (const unit of units) {
1357
+ const result = await this.systemd.start(unit);
1358
+ if (!result.ok) return result;
1359
+ started.push(unit.filename);
1360
+ }
1361
+ await this.writeManifest(options.owner, described.value);
1362
+ return ok({
1363
+ added: diff.added,
1364
+ directory: this.systemd.unitDir,
1365
+ enabled,
1366
+ manifestPath,
1367
+ owner: options.owner,
1368
+ removed: diff.removed,
1369
+ restarted: [],
1370
+ started,
1371
+ unchanged: diff.unchanged,
1372
+ updated: diff.updated
1373
+ });
1374
+ }
1375
+ async detach(selector, options = {}) {
1376
+ const manifest = await this.readManifest(selector.owner);
1377
+ if (manifest === void 0) return ok({
1378
+ deleted: [],
1379
+ detached: [],
1380
+ disabled: [],
1381
+ owner: selector.owner,
1382
+ stopped: []
1383
+ });
1384
+ const filenames = manifest.units.map((unit) => unit.filename);
1385
+ const deleted = [];
1386
+ const disabled = [];
1387
+ const stopped = [];
1388
+ if (options.stop ?? true) {
1389
+ await this.runSystemctl(`stop`, filenames);
1390
+ stopped.push(...filenames);
1391
+ }
1392
+ if (options.disable ?? true) {
1393
+ await this.runSystemctl(`disable`, filenames);
1394
+ disabled.push(...filenames);
1395
+ }
1396
+ if (options.deleteUnitFiles ?? true) {
1397
+ for (const unit of manifest.units) {
1398
+ await this.removeManagedUnitPath(unit.path);
1399
+ await rm(unit.path, { force: true });
1400
+ deleted.push(unit.filename);
1401
+ }
1402
+ await this.runSystemctl(`daemon-reload`);
1403
+ }
1404
+ await rm(this.manifestPathFor(selector.owner), { force: true });
1405
+ return ok({
1406
+ deleted,
1407
+ detached: filenames,
1408
+ disabled,
1409
+ owner: selector.owner,
1410
+ stopped
1411
+ });
1412
+ }
1413
+ async reattach(units, options) {
1414
+ if (units.length === 0) return err(new NoUnitsProvidedError(`Systemd.ts.reattach()`));
1415
+ const described = await this.describeUnits(units);
1416
+ if (!described.ok) return described;
1417
+ const manifestPath = this.manifestPathFor(options.owner);
1418
+ const prior = await this.readManifest(options.owner);
1419
+ const diff = await this.diffManifest(prior, described.value);
1420
+ if ((options.stopRemoved ?? true) && diff.removed.length > 0) await this.runSystemctl(`stop`, diff.removed);
1421
+ if ((options.disableRemoved ?? true) && diff.removed.length > 0) await this.runSystemctl(`disable`, diff.removed);
1422
+ if ((options.prune ?? true) && prior !== void 0) {
1423
+ for (const unit of prior.units) {
1424
+ if (!diff.removed.includes(unit.filename)) continue;
1425
+ await this.removeManagedUnitPath(unit.path);
1426
+ await rm(unit.path, { force: true });
1427
+ }
1428
+ if (diff.removed.length > 0) await this.runSystemctl(`daemon-reload`);
1429
+ }
1430
+ const materialized = await this.systemd.materialize(...units);
1431
+ if (!materialized.ok) return materialized;
1432
+ const enabled = [];
1433
+ if (options.enable ?? false) {
1434
+ const result = await this.systemd.enable(...units);
1435
+ if (!result.ok) return result;
1436
+ enabled.push(...units.map((unit) => unit.filename));
1437
+ }
1438
+ const restarted = [];
1439
+ if (options.restartUpdated ?? false) {
1440
+ const restartable = units.filter((unit) => diff.updated.includes(unit.filename)).map((unit) => unit.filename);
1441
+ if (restartable.length > 0) {
1442
+ await this.runSystemctl(`restart`, restartable);
1443
+ restarted.push(...restartable);
1444
+ }
1445
+ }
1446
+ const started = [];
1447
+ if (options.start ?? false) for (const unit of units) {
1448
+ if (restarted.includes(unit.filename)) continue;
1449
+ const result = await this.systemd.start(unit);
1450
+ if (!result.ok) return result;
1451
+ started.push(unit.filename);
1452
+ }
1453
+ await this.writeManifest(options.owner, described.value);
1454
+ return ok({
1455
+ added: diff.added,
1456
+ directory: this.systemd.unitDir,
1457
+ enabled,
1458
+ manifestPath,
1459
+ owner: options.owner,
1460
+ removed: diff.removed,
1461
+ restarted,
1462
+ started,
1463
+ unchanged: diff.unchanged,
1464
+ updated: diff.updated
1465
+ });
1466
+ }
1467
+ async describeUnits(units) {
1468
+ const described = [];
1469
+ for (const unit of units) {
1470
+ const rendered = unit.render();
1471
+ if (!rendered.ok) return rendered;
1472
+ described.push({
1473
+ filename: unit.filename,
1474
+ path: this.systemd.pathFor(unit),
1475
+ rendered: rendered.value
1476
+ });
1477
+ }
1478
+ return ok(described);
1479
+ }
1480
+ async diffManifest(prior, desired) {
1481
+ const priorUnits = new Map(prior?.units.map((unit) => [unit.filename, unit.path]) ?? []);
1482
+ const desiredNames = new Set(desired.map((unit) => unit.filename));
1483
+ const added = [];
1484
+ const removed = [...priorUnits.keys()].filter((filename) => !desiredNames.has(filename));
1485
+ const unchanged = [];
1486
+ const updated = [];
1487
+ for (const unit of desired) {
1488
+ const priorPath = priorUnits.get(unit.filename);
1489
+ if (priorPath === void 0) {
1490
+ added.push(unit.filename);
1491
+ continue;
1492
+ }
1493
+ try {
1494
+ if (await readFile(priorPath, `utf8`) === unit.rendered) {
1495
+ unchanged.push(unit.filename);
1496
+ continue;
1497
+ }
1498
+ } catch {
1499
+ added.push(unit.filename);
1500
+ continue;
1501
+ }
1502
+ updated.push(unit.filename);
1503
+ }
1504
+ return {
1505
+ added,
1506
+ removed,
1507
+ unchanged,
1508
+ updated
1509
+ };
1510
+ }
1511
+ async manifestDir() {
1512
+ const directory = join(this.systemd.unitDir, `.systemd-ts`);
1513
+ await mkdir(directory, { recursive: true });
1514
+ return directory;
1515
+ }
1516
+ manifestPathFor(owner) {
1517
+ return join(this.systemd.unitDir, `.systemd-ts`, `${encodeURIComponent(owner)}.json`);
1518
+ }
1519
+ async readManifest(owner) {
1520
+ try {
1521
+ const content = await readFile(this.manifestPathFor(owner), `utf8`);
1522
+ return JSON.parse(content);
1067
1523
  } catch {
1068
1524
  return;
1069
1525
  }
1070
1526
  }
1527
+ async runSystemctl(action, units = []) {
1528
+ const args = action === `daemon-reload` ? [...this.scopeArgs(), action] : [
1529
+ ...this.scopeArgs(),
1530
+ action,
1531
+ ...units
1532
+ ];
1533
+ await this.systemd.executor(`systemctl`, args);
1534
+ }
1535
+ async removeManagedUnitPath(path) {
1536
+ await this.systemd.executor(`rm`, [`-f`, path]);
1537
+ }
1538
+ scopeArgs() {
1539
+ return this.systemd.scope === `user` ? [`--user`] : [];
1540
+ }
1541
+ async writeManifest(owner, units) {
1542
+ await this.manifestDir();
1543
+ const manifest = {
1544
+ owner,
1545
+ units: units.map((unit) => ({
1546
+ filename: unit.filename,
1547
+ path: unit.path
1548
+ })),
1549
+ version: 1
1550
+ };
1551
+ await writeFile(this.manifestPathFor(owner), JSON.stringify(manifest, null, 2), `utf8`);
1552
+ }
1071
1553
  };
1072
1554
  function ok(value) {
1073
1555
  return {
@@ -1081,6 +1563,9 @@ function err(error) {
1081
1563
  error
1082
1564
  };
1083
1565
  }
1566
+ function joinCommandOutput(output) {
1567
+ return [output.stdout, output.stderr].filter((value) => value.length > 0).join(`\n`);
1568
+ }
1084
1569
  let lazyDefaultSystemd;
1085
1570
  /**
1086
1571
  * Returns the lazily created default `Systemd` instance.
@@ -1122,4 +1607,4 @@ async function writeUnitDirectory(path) {
1122
1607
  */
1123
1608
  const Internal = internal_exports;
1124
1609
  //#endregion
1125
- export { Executable, ExecutableInferenceError, Internal, InvalidExecDirectiveError, NoUnitsProvidedError, NotifySendError, Systemd, SystemdMaterialization, SystemdService, SystemdTimer, SystemdTsError, UnitEnableError, UnitLogsReadError, UnitMaterializationError, UnitStartError, defaultSystemd, defineExecutable, notify };
1610
+ export { Executable, ExecutableInferenceError, Internal, InvalidExecDirectiveError, NoUnitsProvidedError, NotifySendError, Systemctl, SystemctlCommandError, Systemd, SystemdMaterialization, SystemdService, SystemdTimer, SystemdTs, SystemdTsError, UnitEnableError, UnitLogsReadError, UnitMaterializationError, UnitStartError, defaultSystemd, defineExecutable, notify };