systemd-ts 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -1,63 +1,312 @@
1
1
  # systemd-ts
2
2
 
3
- `systemd-ts` is a library for declaring, rendering, and managing `systemd`
4
- services and timers from TypeScript.
3
+ `systemd-ts` lets you work with `systemd` from TypeScript.
5
4
 
6
- ## Usage
5
+ It is designed around a few practical jobs:
7
6
 
8
- The example below materializes a user-scoped service and timer for a small
9
- nightly backup job. The service does the work, the timer schedules it, and
10
- `Systemd` writes both unit files before enabling and starting the timer.
7
+ - keep application-owned units defined in `.timer` and `.service`up to date from TypeScript
8
+ - use `systemctl` from TypeScript
9
+ - define a service from TypeScript
10
+ - define a service that runs on a timer from TypeScript
11
+ - keep the code that runs the job close to the code that defines the service and timer
12
+
13
+ The library has two layers:
14
+
15
+ - `systemd.ts.*` is the code-owned upkeep layer for attaching, reattaching, and
16
+ detaching application-managed units.
17
+ - `systemd.*` stays close to raw `systemd` concepts like materializing units and
18
+ starting them directly.
19
+
20
+ That lets the README start from the application code story without giving up a
21
+ lower-level `systemd` surface when you want it.
22
+
23
+ It is also a good place to learn `systemd` itself. The unit directives are
24
+ deeply typed, documented in the TypeScript surface, and maintained against
25
+ versioned upstream manpage material, so the library doubles as a guided way to
26
+ explore what `systemd` can do without starting from loose strings and scattered
27
+ shell examples.
28
+
29
+ ## What It Can Do
30
+
31
+ ### Keep application-owned units up to date
32
+
33
+ If your application owns a service or timer and wants to keep that unit set up
34
+ to date across reinstalls, updates, or reconfiguration, use `systemd.ts`.
11
35
 
12
36
  ```ts
13
37
  import { homedir } from "node:os";
14
38
  import { join } from "node:path";
15
39
 
40
+ import backupJob from "./backup-job.ts";
16
41
  import { Systemd, SystemdService, SystemdTimer } from "systemd-ts";
17
42
 
18
- const userUnitDir = join(homedir(), ".config/systemd/user");
43
+ const systemd = new Systemd({
44
+ scope: `user`,
45
+ unitDir: join(homedir(), `.config/systemd/user`),
46
+ linkUnits: true,
47
+ });
48
+
49
+ const service = new SystemdService({
50
+ name: `backup-db`,
51
+ service: {
52
+ Type: `oneshot`,
53
+ ExecStart: backupJob,
54
+ },
55
+ });
56
+
57
+ const timer = new SystemdTimer({
58
+ name: `backup-db`,
59
+ timer: {
60
+ OnCalendar: `daily`,
61
+ Persistent: true,
62
+ Unit: service.filename,
63
+ },
64
+ install: {
65
+ WantedBy: `timers.target`,
66
+ },
67
+ });
68
+
69
+ const attached = await systemd.ts.reattach([service, timer], {
70
+ owner: `com.example.backup-db`,
71
+ enable: true,
72
+ start: true,
73
+ prune: true,
74
+ });
75
+ if (!attached.ok) {
76
+ throw attached.error;
77
+ }
78
+ ```
79
+
80
+ This is the highest-level upkeep workflow in the library. It is designed for
81
+ desktop apps, self-updating agents, OTA-managed software, and other code-owned
82
+ service setups where the application itself is responsible for the unit set it
83
+ maintains.
84
+
85
+ ### Use `systemctl` from TypeScript
86
+
87
+ If you want a typed, argv-first `systemctl` client, use `Systemctl`.
88
+
89
+ ```ts
90
+ import { Systemctl } from "systemd-ts";
91
+
92
+ const systemctl = new Systemctl({ scope: `user` });
93
+
94
+ const timers = await systemctl.listTimers({ all: true });
95
+ if (!timers.ok) {
96
+ throw timers.error;
97
+ }
98
+
99
+ const status = await systemctl.showServiceStatus(`backup-db.service`);
100
+ if (!status.ok) {
101
+ throw status.error;
102
+ }
103
+
104
+ console.log(timers.value);
105
+ console.log(status.value);
106
+ ```
107
+
108
+ This is a good fit when you already know the units you want to inspect or
109
+ control and you want to stay inside TypeScript instead of shelling out from app
110
+ code by hand.
111
+
112
+ ### Define a service from TypeScript
113
+
114
+ If you want to describe a service unit in code, use `SystemdService`.
115
+
116
+ ```ts
117
+ import { SystemdService } from "systemd-ts";
19
118
 
20
119
  const service = new SystemdService({
21
- name: "backup-db",
120
+ name: `backup-db`,
22
121
  unit: {
23
- Description: "Write a nightly database backup",
122
+ Description: `Write a database backup`,
24
123
  },
25
124
  service: {
26
- Type: "oneshot",
27
- ExecStart: "/usr/bin/bash -lc '/srv/app/bin/backup-db >> /srv/app/log/backup-db.log 2>&1'",
125
+ Type: `oneshot`,
126
+ ExecStart: `/srv/app/bin/backup-db`,
127
+ StandardOutput: `append:/srv/app/log/backup-db.log`,
128
+ StandardError: `append:/srv/app/log/backup-db.log`,
129
+ },
130
+ });
131
+ ```
132
+
133
+ That service can be rendered into a real unit file, materialized into a unit
134
+ directory, and then managed through `Systemd` or `Systemctl`.
135
+
136
+ ### Define a service that runs on a timer
137
+
138
+ If you want a scheduled job, pair `SystemdService` with `SystemdTimer`.
139
+
140
+ ```ts
141
+ import { SystemdService, SystemdTimer } from "systemd-ts";
142
+
143
+ const service = new SystemdService({
144
+ name: `backup-db`,
145
+ service: {
146
+ Type: `oneshot`,
147
+ ExecStart: `/srv/app/bin/backup-db`,
28
148
  },
29
149
  });
30
150
 
31
151
  const timer = new SystemdTimer({
32
- name: "backup-db",
152
+ name: `backup-db`,
33
153
  unit: {
34
- Description: "Run the database backup every night",
154
+ Description: `Run the backup every night`,
155
+ },
156
+ timer: {
157
+ OnCalendar: `03:15`,
158
+ Persistent: true,
159
+ Unit: service.filename,
160
+ },
161
+ install: {
162
+ WantedBy: `timers.target`,
163
+ },
164
+ });
165
+ ```
166
+
167
+ This gives you a real `.service` plus a real `.timer`, both defined from the
168
+ same TypeScript module.
169
+
170
+ ### Keep the executable close to the unit definitions
171
+
172
+ For many projects, the nicest workflow is to colocate the job entrypoint with
173
+ the service and timer that run it.
174
+
175
+ `defineExecutable()` is designed for that pattern:
176
+
177
+ ```ts
178
+ import { defineExecutable } from "systemd-ts";
179
+
180
+ export default defineExecutable(async () => {
181
+ console.log(`Writing backup...`);
182
+ });
183
+ ```
184
+
185
+ Then you can use that executable directly in a service definition:
186
+
187
+ ```ts
188
+ import backupJob from "./backup-job.ts";
189
+ import { SystemdService, SystemdTimer } from "systemd-ts";
190
+
191
+ const service = new SystemdService({
192
+ name: `backup-db`,
193
+ service: {
194
+ Type: `oneshot`,
195
+ ExecStart: backupJob,
35
196
  },
197
+ });
198
+
199
+ const timer = new SystemdTimer({
200
+ name: `backup-db`,
201
+ timer: {
202
+ OnCalendar: `daily`,
203
+ Unit: service.filename,
204
+ },
205
+ install: {
206
+ WantedBy: `timers.target`,
207
+ },
208
+ });
209
+ ```
210
+
211
+ That keeps the scheduled code, the service definition, and the timer definition
212
+ near each other instead of scattering them across shell scripts and unit files.
213
+
214
+ ## A Typical Flow
215
+
216
+ Most applications use the library in a flow like this:
217
+
218
+ 1. Define a `SystemdService`
219
+ 2. Optionally define a matching `SystemdTimer`
220
+ 3. Reattach the desired unit set through `systemd.ts`
221
+ 4. Inspect or control units directly through `Systemctl` or `Systemd` when needed
222
+
223
+ ```ts
224
+ import { homedir } from "node:os";
225
+ import { join } from "node:path";
226
+
227
+ import backupJob from "./backup-job.ts";
228
+ import { Systemd, SystemdService, SystemdTimer } from "systemd-ts";
229
+
230
+ const systemd = new Systemd({
231
+ scope: `user`,
232
+ unitDir: join(homedir(), `.config/systemd/user`),
233
+ linkUnits: true,
234
+ });
235
+
236
+ const service = new SystemdService({
237
+ name: `backup-db`,
238
+ service: {
239
+ Type: `oneshot`,
240
+ ExecStart: backupJob,
241
+ },
242
+ });
243
+
244
+ const timer = new SystemdTimer({
245
+ name: `backup-db`,
36
246
  timer: {
37
- OnCalendar: "03:15",
247
+ OnCalendar: `daily`,
38
248
  Persistent: true,
39
249
  Unit: service.filename,
40
250
  },
41
251
  install: {
42
- WantedBy: "timers.target",
252
+ WantedBy: `timers.target`,
43
253
  },
44
254
  });
45
255
 
256
+ const reattached = await systemd.ts.reattach([service, timer], {
257
+ owner: `com.example.backup-db`,
258
+ enable: true,
259
+ start: true,
260
+ prune: true,
261
+ });
262
+ if (!reattached.ok) {
263
+ throw reattached.error;
264
+ }
265
+ ```
266
+
267
+ ### Using The `Systemd` API Directly
268
+
269
+ If you want to stay closer to raw `systemd` operations, use the lower-level
270
+ `Systemd` methods directly:
271
+
272
+ ```ts
273
+ import { homedir } from "node:os";
274
+ import { join } from "node:path";
275
+
276
+ import backupJob from "./backup-job.ts";
277
+ import { Systemd, SystemdService, SystemdTimer } from "systemd-ts";
278
+
46
279
  const systemd = new Systemd({
47
- scope: "user",
48
- unitDir: userUnitDir,
280
+ scope: `user`,
281
+ unitDir: join(homedir(), `.config/systemd/user`),
49
282
  linkUnits: true,
50
283
  });
51
284
 
285
+ const service = new SystemdService({
286
+ name: `backup-db`,
287
+ service: {
288
+ Type: `oneshot`,
289
+ ExecStart: backupJob,
290
+ },
291
+ });
292
+
293
+ const timer = new SystemdTimer({
294
+ name: `backup-db`,
295
+ timer: {
296
+ OnCalendar: `daily`,
297
+ Persistent: true,
298
+ Unit: service.filename,
299
+ },
300
+ install: {
301
+ WantedBy: `timers.target`,
302
+ },
303
+ });
304
+
52
305
  const materialized = await systemd.materialize(service, timer);
53
306
  if (!materialized.ok) {
54
307
  throw materialized.error;
55
308
  }
56
309
 
57
- console.log(systemd.pathFor(service));
58
- console.log(systemd.pathFor(timer));
59
- console.log(materialized.value.materialized);
60
-
61
310
  const enabled = await systemd.enable(timer);
62
311
  if (!enabled.ok) {
63
312
  throw enabled.error;
@@ -69,6 +318,79 @@ if (!started.ok) {
69
318
  }
70
319
  ```
71
320
 
72
- That will materialize `backup-db.service` and `backup-db.timer` into the user's
73
- unit directory, enable the timer under `timers.target`, and ask `systemd --user`
74
- to start waiting for the first scheduled run.
321
+ This lower-level flow is useful when you want exact control over
322
+ materialization, enablement, or startup behavior without going through the
323
+ code-owned upkeep layer.
324
+
325
+ ## Testing Against A Real User Manager
326
+
327
+ The package also ships a `systemd-ts/test` module for integration tests that
328
+ need a real `systemd --user` environment.
329
+
330
+ Use `ensureTestHost()` to warm the test host, `createTestSandbox()` to create
331
+ an isolated unit directory, and `sandboxSystemd()` when you want a `Systemd`
332
+ instance already pointed at that sandbox.
333
+
334
+ ```ts
335
+ import { afterEach, beforeAll, beforeEach, test } from "vite-plus/test";
336
+
337
+ import { SystemdService } from "systemd-ts";
338
+ import {
339
+ createTestSandbox,
340
+ destroyCurrentTestSandbox,
341
+ ensureTestHost,
342
+ sandboxSystemd,
343
+ useCurrentTestSandbox,
344
+ } from "systemd-ts/test";
345
+
346
+ beforeAll(async () => {
347
+ await ensureTestHost();
348
+ });
349
+
350
+ beforeEach(async (context) => {
351
+ await createTestSandbox(context.task.name);
352
+ });
353
+
354
+ afterEach(async () => {
355
+ await destroyCurrentTestSandbox();
356
+ });
357
+
358
+ test(`starts a service in a sandbox`, async () => {
359
+ const sandbox = useCurrentTestSandbox();
360
+ const systemd = sandboxSystemd();
361
+ const markerFile = `${sandbox.workDir}/started.txt`;
362
+ const service = new SystemdService({
363
+ name: sandbox.namePrefix,
364
+ service: {
365
+ Type: `oneshot`,
366
+ ExecStart: `/usr/bin/bash -lc 'echo started > ${markerFile}'`,
367
+ },
368
+ });
369
+
370
+ const materialized = await systemd.materialize(service);
371
+ if (!materialized.ok) {
372
+ throw materialized.error;
373
+ }
374
+
375
+ const started = await systemd.start(service);
376
+ if (!started.ok) {
377
+ throw started.error;
378
+ }
379
+ });
380
+ ```
381
+
382
+ If you need direct executor access for helpers outside `Systemd`, the same
383
+ module also exports `guestCommandExecutor`, `isolatedGuestCommandExecutor`, and
384
+ `createGuestCommandExecutor()`.
385
+
386
+ ## Explore Further
387
+
388
+ The README is meant to answer what the library is for.
389
+
390
+ If you want the exact method surface, result types, or lower-level details, the
391
+ next places to look are:
392
+
393
+ - `Systemctl` for direct `systemctl` usage from TypeScript
394
+ - `Systemd` for materialization plus unit lifecycle workflows
395
+ - `SystemdService` and `SystemdTimer` for unit definitions
396
+ - `Executable` and `defineExecutable()` for colocated runnable entrypoints