systemd-ts 0.0.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/README.md ADDED
@@ -0,0 +1,4 @@
1
+ # systemd-ts
2
+
3
+ `systemd-ts` is a library for declaring, rendering, and managing `systemd`
4
+ services and timers from TypeScript.
@@ -0,0 +1,132 @@
1
+ //#region src/index.d.ts
2
+ interface UnitSection {
3
+ readonly [key: string]: UnitValue | readonly UnitValue[] | undefined;
4
+ }
5
+ interface SystemdServiceOptions {
6
+ readonly install?: UnitSection;
7
+ readonly name: string;
8
+ readonly service: UnitSection;
9
+ readonly unit?: UnitSection;
10
+ }
11
+ interface SystemdTimerOptions {
12
+ readonly install?: UnitSection;
13
+ readonly name: string;
14
+ readonly timer: UnitSection;
15
+ readonly unit?: UnitSection;
16
+ }
17
+ interface SystemdOptions {
18
+ readonly executor?: CommandExecutor;
19
+ readonly linkUnits?: boolean;
20
+ readonly scope?: `system` | `user`;
21
+ readonly unitDir?: string;
22
+ }
23
+ interface CommandResult {
24
+ readonly stderr: string;
25
+ readonly stdout: string;
26
+ }
27
+ type CommandExecutor = (command: string, args: readonly string[]) => Promise<CommandResult>;
28
+ interface LogsOptions {
29
+ readonly lines?: number;
30
+ }
31
+ interface NotifyOptions {
32
+ readonly executor?: CommandExecutor;
33
+ readonly pid?: number;
34
+ readonly socketPath?: string;
35
+ readonly status?: string;
36
+ }
37
+ interface StartResult {
38
+ readonly activeState: string;
39
+ readonly execMainStatus: number | undefined;
40
+ readonly result: string;
41
+ readonly subState: string;
42
+ readonly unit: string;
43
+ }
44
+ interface ExecutableOptions {
45
+ readonly args?: readonly string[];
46
+ readonly modulePath?: string;
47
+ readonly runtimeEntrypoint?: string;
48
+ }
49
+ type StripUnitSuffix<Value extends string, Suffix extends string> = Value extends `${infer Base}${Suffix}` ? Base : Value;
50
+ type ServiceBaseName<Value extends string> = StripUnitSuffix<Value, `.service`>;
51
+ type TimerBaseName<Value extends string> = StripUnitSuffix<Value, `.timer`>;
52
+ type ServiceFilename<Value extends string> = `${ServiceBaseName<Value>}.service`;
53
+ type TimerFilename<Value extends string> = `${TimerBaseName<Value>}.timer`;
54
+ type TimerTargetUnit<TOptions extends SystemdTimerOptions> = TOptions[`timer`] extends {
55
+ readonly Unit: infer UnitName extends string;
56
+ } ? UnitName : ServiceFilename<TOptions[`name`]>;
57
+ type TimerTargetServiceName<TOptions extends SystemdTimerOptions> = TimerTargetUnit<TOptions> extends `${infer Base}.service` ? Base : never;
58
+ type IsWideString<Value extends string> = string extends Value ? true : false;
59
+ type AnySystemdService = SystemdService<SystemdServiceOptions>;
60
+ type AnySystemdTimer = SystemdTimer<SystemdTimerOptions>;
61
+ type SystemdUnit = AnySystemdService | AnySystemdTimer;
62
+ interface InstalledUnit<TUnit extends SystemdUnit = SystemdUnit> {
63
+ readonly path: string;
64
+ readonly unit: TUnit;
65
+ }
66
+ type ServiceNamesIn<TUnits extends readonly SystemdUnit[]> = TUnits[number] extends infer TUnit ? TUnit extends AnySystemdService ? TUnit[`name`] : never : never;
67
+ type TimerMatchesAnyService<TTimer extends AnySystemdTimer, TServiceNames extends string> = IsWideString<TimerTargetServiceName<TTimer[`options`]>> extends true ? true : IsWideString<TServiceNames> extends true ? true : [Extract<TServiceNames, TimerTargetServiceName<TTimer[`options`]>>] extends [never] ? false : true;
68
+ type MismatchedTimers<TUnits extends readonly SystemdUnit[]> = TUnits[number] extends infer TUnit ? TUnit extends AnySystemdTimer ? TimerMatchesAnyService<TUnit, ServiceNamesIn<TUnits>> extends true ? never : TUnit : never : never;
69
+ type HasServices<TUnits extends readonly SystemdUnit[]> = [ServiceNamesIn<TUnits>] extends [never] ? false : true;
70
+ type HasMismatchedServiceTimerPairs<TUnits extends readonly SystemdUnit[]> = HasServices<TUnits> extends true ? [MismatchedTimers<TUnits>] extends [never] ? false : true : false;
71
+ type ValidInstallUnits<TUnits extends readonly SystemdUnit[]> = HasMismatchedServiceTimerPairs<TUnits> extends true ? never : TUnits;
72
+ type UnitValue = string | number | boolean | Executable;
73
+ declare class Executable {
74
+ readonly args: readonly string[];
75
+ readonly modulePath: string;
76
+ readonly runtimeEntrypoint: string;
77
+ constructor(options?: ExecutableOptions);
78
+ toCommandParts(): readonly [string, ...string[]];
79
+ toExecStart(): string;
80
+ }
81
+ declare class SystemdService<const TOptions extends SystemdServiceOptions = SystemdServiceOptions> {
82
+ readonly install: TOptions[`install`] | undefined;
83
+ readonly name: ServiceBaseName<TOptions[`name`]>;
84
+ readonly options: Readonly<TOptions>;
85
+ readonly service: TOptions[`service`];
86
+ readonly unit: TOptions[`unit`] | undefined;
87
+ constructor(options: TOptions);
88
+ get filename(): ServiceFilename<TOptions[`name`]>;
89
+ render(): string;
90
+ }
91
+ declare class SystemdTimer<const TOptions extends SystemdTimerOptions = SystemdTimerOptions> {
92
+ readonly install: TOptions[`install`] | undefined;
93
+ readonly name: TimerBaseName<TOptions[`name`]>;
94
+ readonly options: Readonly<TOptions>;
95
+ readonly targetServiceName: TimerTargetServiceName<TOptions>;
96
+ readonly targetUnit: TimerTargetUnit<TOptions>;
97
+ readonly timer: TOptions[`timer`];
98
+ readonly unit: TOptions[`unit`] | undefined;
99
+ constructor(options: TOptions);
100
+ get filename(): TimerFilename<TOptions[`name`]>;
101
+ render(): string;
102
+ }
103
+ declare class SystemdInstallResult<TUnits extends readonly SystemdUnit[] = readonly SystemdUnit[]> {
104
+ readonly directory: string;
105
+ readonly installed: readonly InstalledUnit<TUnits[number]>[];
106
+ private readonly pathByFilename;
107
+ constructor(directory: string, installed: readonly InstalledUnit<TUnits[number]>[]);
108
+ pathFor(unit: TUnits[number]): string;
109
+ }
110
+ declare class Systemd {
111
+ readonly executor: CommandExecutor;
112
+ readonly linkUnits: boolean;
113
+ readonly scope: `system` | `user`;
114
+ readonly unitDir: string;
115
+ constructor(options?: SystemdOptions);
116
+ install<const TUnits extends readonly SystemdUnit[]>(...units: ValidInstallUnits<TUnits>): Promise<SystemdInstallResult<TUnits>>;
117
+ enable(...units: readonly SystemdUnit[]): Promise<void>;
118
+ start(unit: SystemdUnit): Promise<StartResult>;
119
+ logs(_unit: SystemdUnit, _options?: LogsOptions): Promise<string>;
120
+ pathFor(unit: SystemdUnit): string;
121
+ private prepareUnits;
122
+ private scopeArgs;
123
+ private collectLinkPaths;
124
+ }
125
+ declare function defaultSystemd(): Systemd;
126
+ declare const notify: {
127
+ ready(options?: NotifyOptions): Promise<void>;
128
+ watchdog(options?: NotifyOptions): Promise<void>;
129
+ };
130
+ declare function defineExecutable(fn: () => void | Promise<void>, options?: ExecutableOptions): Executable;
131
+ //#endregion
132
+ export { AnySystemdService, AnySystemdTimer, CommandExecutor, CommandResult, Executable, ExecutableOptions, InstalledUnit, LogsOptions, NotifyOptions, StartResult, Systemd, SystemdInstallResult, SystemdOptions, SystemdService, SystemdServiceOptions, SystemdTimer, SystemdTimerOptions, SystemdUnit, UnitSection, UnitValue, defaultSystemd, defineExecutable, notify };
package/dist/index.mjs ADDED
@@ -0,0 +1,393 @@
1
+ import { execFile } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
3
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+ //#region src/index.ts
8
+ const REQUIRED_EXEC_KEYS = [
9
+ `ExecStart`,
10
+ `ExecStop`,
11
+ `ExecReload`
12
+ ];
13
+ const execFileAsync = promisify(execFile);
14
+ const currentModulePath = fileURLToPath(import.meta.url);
15
+ var Executable = class {
16
+ args;
17
+ modulePath;
18
+ runtimeEntrypoint;
19
+ constructor(options = {}) {
20
+ this.runtimeEntrypoint = options.runtimeEntrypoint ?? process.execPath;
21
+ this.modulePath = options.modulePath ?? inferCallerModulePath();
22
+ this.args = Object.freeze([...options.args ?? []]);
23
+ Object.freeze(this);
24
+ }
25
+ toCommandParts() {
26
+ return [
27
+ this.runtimeEntrypoint,
28
+ this.modulePath,
29
+ ...this.args
30
+ ];
31
+ }
32
+ toExecStart() {
33
+ return this.toCommandParts().map(shellQuote).join(` `);
34
+ }
35
+ };
36
+ var SystemdService = class {
37
+ install;
38
+ name;
39
+ options;
40
+ service;
41
+ unit;
42
+ constructor(options) {
43
+ this.options = freezeUnitOptions(options);
44
+ this.name = normalizeUnitName(options.name, `.service`);
45
+ this.unit = cloneUnitSection(options.unit);
46
+ this.service = cloneUnitSection(options.service) ?? {};
47
+ this.install = cloneUnitSection(options.install);
48
+ Object.freeze(this);
49
+ }
50
+ get filename() {
51
+ return `${this.name}.service`;
52
+ }
53
+ render() {
54
+ validateServiceSection(this.service);
55
+ return renderUnitFile([
56
+ [`Unit`, this.unit],
57
+ [`Service`, this.service],
58
+ [`Install`, this.install]
59
+ ]);
60
+ }
61
+ };
62
+ var SystemdTimer = class {
63
+ install;
64
+ name;
65
+ options;
66
+ targetServiceName;
67
+ targetUnit;
68
+ timer;
69
+ unit;
70
+ constructor(options) {
71
+ this.options = freezeUnitOptions(options);
72
+ this.name = normalizeUnitName(options.name, `.timer`);
73
+ this.unit = cloneUnitSection(options.unit);
74
+ this.timer = cloneUnitSection(options.timer) ?? {};
75
+ this.install = cloneUnitSection(options.install);
76
+ this.targetUnit = resolveTimerTargetUnit(options);
77
+ this.targetServiceName = normalizeUnitName(this.targetUnit, `.service`);
78
+ Object.freeze(this);
79
+ }
80
+ get filename() {
81
+ return `${this.name}.timer`;
82
+ }
83
+ render() {
84
+ return renderUnitFile([
85
+ [`Unit`, this.unit],
86
+ [`Timer`, this.timer],
87
+ [`Install`, this.install]
88
+ ]);
89
+ }
90
+ };
91
+ var SystemdInstallResult = class {
92
+ directory;
93
+ installed;
94
+ pathByFilename;
95
+ constructor(directory, installed) {
96
+ this.directory = directory;
97
+ this.installed = Object.freeze([...installed]);
98
+ this.pathByFilename = new Map(installed.map((entry) => [entry.unit.filename, entry.path]));
99
+ Object.freeze(this);
100
+ }
101
+ pathFor(unit) {
102
+ const path = this.pathByFilename.get(unit.filename);
103
+ if (path === void 0) throw new Error(`No installed path is recorded for ${unit.filename}`);
104
+ return path;
105
+ }
106
+ };
107
+ var Systemd = class {
108
+ executor;
109
+ linkUnits;
110
+ scope;
111
+ unitDir;
112
+ constructor(options = {}) {
113
+ this.scope = options.scope ?? `system`;
114
+ this.unitDir = options.unitDir ?? defaultUnitDirForScope(this.scope);
115
+ this.linkUnits = options.linkUnits ?? false;
116
+ this.executor = options.executor ?? defaultCommandExecutor;
117
+ Object.freeze(this);
118
+ }
119
+ async install(...units) {
120
+ if (units.length === 0) throw new Error(`Systemd.install() requires at least one service or timer`);
121
+ assertInstallableTogether(units);
122
+ await mkdir(this.unitDir, { recursive: true });
123
+ const installed = [];
124
+ for (const unit of units) {
125
+ const path = join(this.unitDir, unit.filename);
126
+ await writeFile(path, unit.render(), `utf8`);
127
+ installed.push({
128
+ path,
129
+ unit
130
+ });
131
+ }
132
+ return new SystemdInstallResult(this.unitDir, installed);
133
+ }
134
+ async enable(...units) {
135
+ if (units.length === 0) throw new Error(`Systemd.enable() requires at least one service or timer`);
136
+ const scopeArgs = this.scopeArgs();
137
+ await this.prepareUnits(scopeArgs, units);
138
+ for (const unit of units) await this.executor(`systemctl`, [
139
+ ...scopeArgs,
140
+ `enable`,
141
+ unit.filename
142
+ ]);
143
+ }
144
+ async start(unit) {
145
+ const scopeArgs = this.scopeArgs();
146
+ await this.prepareUnits(scopeArgs, [unit]);
147
+ await this.executor(`systemctl`, [
148
+ ...scopeArgs,
149
+ `start`,
150
+ unit.filename
151
+ ]);
152
+ const status = await this.executor(`systemctl`, [
153
+ ...scopeArgs,
154
+ `show`,
155
+ unit.filename,
156
+ `--property=Id,ActiveState,SubState,Result,ExecMainStatus`
157
+ ]);
158
+ return parseStartResult(unit.filename, status.stdout);
159
+ }
160
+ async logs(_unit, _options) {
161
+ const fileLogPath = resolveUnitLogPath(_unit);
162
+ if (fileLogPath !== void 0) return tailLines(await readFile(fileLogPath, `utf8`), _options?.lines ?? 50);
163
+ const scopeArgs = this.scopeArgs();
164
+ const lines = _options?.lines ?? 50;
165
+ const command = [
166
+ `systemctl`,
167
+ ...scopeArgs,
168
+ `status`,
169
+ _unit.filename,
170
+ `--no-pager`,
171
+ `--lines`,
172
+ String(lines)
173
+ ].map(shellQuote).join(` `);
174
+ return (await this.executor(`bash`, [`-lc`, `${command} || true`])).stdout;
175
+ }
176
+ pathFor(unit) {
177
+ return join(this.unitDir, unit.filename);
178
+ }
179
+ async prepareUnits(scopeArgs, units) {
180
+ if (this.linkUnits) {
181
+ const linkPaths = await this.collectLinkPaths(units);
182
+ for (const path of linkPaths) await this.executor(`systemctl`, [
183
+ ...scopeArgs,
184
+ `link`,
185
+ path
186
+ ]);
187
+ }
188
+ await this.executor(`systemctl`, [...scopeArgs, `daemon-reload`]);
189
+ }
190
+ scopeArgs() {
191
+ return this.scope === `user` ? [`--user`] : [];
192
+ }
193
+ async collectLinkPaths(units) {
194
+ const paths = /* @__PURE__ */ new Set();
195
+ for (const unit of units) {
196
+ paths.add(this.pathFor(unit));
197
+ if (unit instanceof SystemdTimer) {
198
+ const targetPath = join(this.unitDir, unit.targetUnit);
199
+ if (await fileExists(targetPath)) paths.add(targetPath);
200
+ }
201
+ }
202
+ return [...paths];
203
+ }
204
+ };
205
+ function resolveUnitLogPath(unit) {
206
+ if (!(unit instanceof SystemdService)) return;
207
+ for (const key of [`StandardOutput`, `StandardError`]) {
208
+ const value = unit.service[key];
209
+ if (typeof value !== `string`) continue;
210
+ const path = parseFileLogPath(value);
211
+ if (path !== void 0) return path;
212
+ }
213
+ }
214
+ function parseFileLogPath(value) {
215
+ for (const prefix of [`append:`, `file:`]) if (value.startsWith(prefix)) return value.slice(prefix.length);
216
+ }
217
+ function tailLines(output, lines) {
218
+ return output.split(`\n`).slice(-lines).join(`\n`);
219
+ }
220
+ let lazyDefaultSystemd;
221
+ function defaultSystemd() {
222
+ lazyDefaultSystemd ??= new Systemd();
223
+ return lazyDefaultSystemd;
224
+ }
225
+ const notify = {
226
+ async ready(options = {}) {
227
+ await sendNotify(`READY=1`, options);
228
+ },
229
+ async watchdog(options = {}) {
230
+ await sendNotify(`WATCHDOG=1`, options);
231
+ }
232
+ };
233
+ function defineExecutable(fn, options = {}) {
234
+ const executable = new Executable(options);
235
+ if (isMainModule(executable.modulePath)) Promise.resolve(fn()).catch((error) => {
236
+ process.exitCode = 1;
237
+ throw error;
238
+ });
239
+ return executable;
240
+ }
241
+ function normalizeUnitName(name, suffix) {
242
+ return name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
243
+ }
244
+ function resolveTimerTargetUnit(options) {
245
+ const explicitTarget = options.timer[`Unit`];
246
+ if (typeof explicitTarget === `string` && explicitTarget.length > 0) return explicitTarget;
247
+ return `${normalizeUnitName(options.name, `.timer`)}.service`;
248
+ }
249
+ function defaultUnitDirForScope(scope) {
250
+ if (scope === `user`) return `${process.env[`HOME`] ?? `~`}/.config/systemd/user`;
251
+ return `/etc/systemd/system`;
252
+ }
253
+ function freezeUnitOptions(options) {
254
+ return Object.freeze({
255
+ ...options,
256
+ ...options.unit === void 0 ? {} : { unit: cloneUnitSection(options.unit) },
257
+ ...options.install === void 0 ? {} : { install: cloneUnitSection(options.install) },
258
+ ...hasServiceSection(options) ? { service: cloneUnitSection(options.service) } : {},
259
+ ...hasTimerSection(options) ? { timer: cloneUnitSection(options.timer) } : {}
260
+ });
261
+ }
262
+ function hasServiceSection(options) {
263
+ return `service` in options;
264
+ }
265
+ function hasTimerSection(options) {
266
+ return `timer` in options;
267
+ }
268
+ function cloneUnitSection(section) {
269
+ if (section === void 0) return section;
270
+ const entries = Object.entries(section).map(([key, value]) => {
271
+ if (isUnitValueList(value)) return [key, Object.freeze([...value])];
272
+ return [key, value];
273
+ });
274
+ return Object.freeze(Object.fromEntries(entries));
275
+ }
276
+ function validateServiceSection(service) {
277
+ for (const key of REQUIRED_EXEC_KEYS) {
278
+ const value = service[key];
279
+ if (typeof value === `string` && value.length > 0 && !value.startsWith(`/`)) throw new Error(`${key} must use an absolute executable path for systemd`);
280
+ if (value instanceof Executable && !value.runtimeEntrypoint.startsWith(`/`)) throw new Error(`${key} must use an absolute executable path for systemd`);
281
+ }
282
+ }
283
+ function assertInstallableTogether(units) {
284
+ const installedServices = new Set(units.filter((unit) => unit instanceof SystemdService).map((unit) => unit.name));
285
+ if (installedServices.size === 0) return;
286
+ for (const unit of units) {
287
+ if (!(unit instanceof SystemdTimer)) continue;
288
+ if (!installedServices.has(unit.targetServiceName)) throw new Error(`Cannot install ${unit.filename} alongside unrelated services: expected ${unit.targetUnit}`);
289
+ }
290
+ }
291
+ function parseStartResult(unit, output) {
292
+ const properties = Object.fromEntries(output.split(`\n`).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
293
+ const separatorIndex = line.indexOf(`=`);
294
+ if (separatorIndex === -1) return [line, ``];
295
+ return [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)];
296
+ }));
297
+ return {
298
+ activeState: properties[`ActiveState`] ?? `unknown`,
299
+ execMainStatus: properties[`ExecMainStatus`] === void 0 || properties[`ExecMainStatus`] === `` ? void 0 : Number(properties[`ExecMainStatus`]),
300
+ result: properties[`Result`] ?? `unknown`,
301
+ subState: properties[`SubState`] ?? `unknown`,
302
+ unit: properties[`Id`] ?? unit
303
+ };
304
+ }
305
+ function renderUnitFile(sections) {
306
+ const renderedSections = sections.flatMap(([sectionName, section]) => {
307
+ if (section === void 0) return [];
308
+ const lines = Object.entries(section).flatMap(([key, value]) => {
309
+ if (value === void 0) return [];
310
+ if (isUnitValueList(value)) return value.map((entry) => `${key}=${stringifyUnitValue(entry)}`);
311
+ return `${key}=${stringifyUnitValue(value)}`;
312
+ });
313
+ if (lines.length === 0) return [];
314
+ return [
315
+ `[${sectionName}]`,
316
+ ...lines,
317
+ ``
318
+ ];
319
+ }).join(`\n`);
320
+ return renderedSections.endsWith(`\n`) ? renderedSections : `${renderedSections}\n`;
321
+ }
322
+ function stringifyUnitValue(value) {
323
+ if (value instanceof Executable) return value.toExecStart();
324
+ if (typeof value === `boolean`) return value ? `true` : `false`;
325
+ return String(value);
326
+ }
327
+ function isUnitValueList(value) {
328
+ return Array.isArray(value);
329
+ }
330
+ function inferCallerModulePath() {
331
+ const stack = (/* @__PURE__ */ new Error()).stack ?? ``;
332
+ for (const line of stack.split(`\n`).slice(1)) {
333
+ const candidate = extractStackPath(line);
334
+ if (candidate === void 0 || candidate === currentModulePath) continue;
335
+ return candidate;
336
+ }
337
+ throw new Error(`Could not infer the calling module path for defineExecutable(); pass { modulePath } explicitly`);
338
+ }
339
+ function extractStackPath(line) {
340
+ const fileUrlMatch = line.match(/(file:\/\/\/[^)\s:]+(?:\.[cm]?[jt]s)?)/u);
341
+ if (fileUrlMatch !== null) return fileURLToPath(fileUrlMatch[1]);
342
+ const pathMatch = line.match(/(\/[^)\s:]+(?:\.[cm]?[jt]s)?)/u);
343
+ if (pathMatch !== null) return pathMatch[1];
344
+ }
345
+ function isMainModule(modulePath) {
346
+ const mainArg = process.argv[1];
347
+ if (mainArg === void 0) return false;
348
+ return normalizeFilePath(mainArg) === normalizeFilePath(modulePath);
349
+ }
350
+ function shellQuote(value) {
351
+ return `'${value.replaceAll(`'`, `'\\''`)}'`;
352
+ }
353
+ function normalizeFilePath(path) {
354
+ try {
355
+ return realpathSync(path);
356
+ } catch {
357
+ return path;
358
+ }
359
+ }
360
+ async function defaultCommandExecutor(command, args) {
361
+ const result = await execFileAsync(command, [...args]);
362
+ return {
363
+ stderr: result.stderr,
364
+ stdout: result.stdout
365
+ };
366
+ }
367
+ async function fileExists(path) {
368
+ try {
369
+ await access(path);
370
+ return true;
371
+ } catch {
372
+ return false;
373
+ }
374
+ }
375
+ async function sendNotify(state, options) {
376
+ const args = [state];
377
+ if (options.pid !== void 0) args.push(`MAINPID=${options.pid}`);
378
+ if (options.status !== void 0) args.push(`STATUS=${options.status}`);
379
+ if (options.executor !== void 0) {
380
+ const command = buildNotifyShellCommand(args, options.socketPath);
381
+ await options.executor(`bash`, [`-lc`, command]);
382
+ return;
383
+ }
384
+ await execFileAsync(`systemd-notify`, args, { env: {
385
+ ...process.env,
386
+ ...options.socketPath === void 0 ? {} : { NOTIFY_SOCKET: options.socketPath }
387
+ } });
388
+ }
389
+ function buildNotifyShellCommand(args, socketPath) {
390
+ return `${socketPath === void 0 ? `` : `NOTIFY_SOCKET=${shellQuote(socketPath)} `}${[`systemd-notify`, ...args].map(shellQuote).join(` `)}`;
391
+ }
392
+ //#endregion
393
+ export { Executable, Systemd, SystemdInstallResult, SystemdService, SystemdTimer, defaultSystemd, defineExecutable, notify };
@@ -0,0 +1,28 @@
1
+ //#region src/testing/host.d.ts
2
+ interface TestHostInfo {
3
+ readonly backend: TestHostBackendName;
4
+ readonly colimaHome?: string;
5
+ readonly colimaProfile?: string;
6
+ readonly limaHome?: string;
7
+ readonly repoRoot: string;
8
+ readonly stateRoot: string;
9
+ }
10
+ type TestHostBackendName = `colima` | `docker`;
11
+ declare function getTestHostInfo(): TestHostInfo;
12
+ declare function ensureTestHost(): Promise<TestHostInfo>;
13
+ declare function closeTestHost(): void;
14
+ declare function runGuestCommand(script: string): Promise<string>;
15
+ //#endregion
16
+ //#region src/testing/sandbox.d.ts
17
+ interface TestSandbox {
18
+ readonly id: string;
19
+ readonly linkedUnitDir: string;
20
+ readonly namePrefix: string;
21
+ readonly rootDir: string;
22
+ readonly workDir: string;
23
+ }
24
+ declare function createTestSandbox(testName?: string): Promise<TestSandbox>;
25
+ declare function destroyCurrentTestSandbox(): Promise<void>;
26
+ declare function useCurrentTestSandbox(): TestSandbox;
27
+ //#endregion
28
+ export { type TestHostInfo, type TestSandbox, closeTestHost, createTestSandbox, destroyCurrentTestSandbox, ensureTestHost, getTestHostInfo, runGuestCommand, useCurrentTestSandbox };
package/dist/test.mjs ADDED
@@ -0,0 +1,508 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { promisify } from "node:util";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
6
+ //#region src/testing/host.ts
7
+ const execFileAsync = promisify(execFile);
8
+ const commandMaxBufferBytes = 16 * 1024 * 1024;
9
+ const dockerBuildTimeoutMs = 300 * 1e3;
10
+ const dockerExecTimeoutMs = 60 * 1e3;
11
+ const guestCommandTimeoutMs = 10 * 1e3;
12
+ const repoRoot = fileURLToPath(new URL(`../../../../`, import.meta.url));
13
+ const dockerfilePath = fileURLToPath(new URL(`./systemd-container.Dockerfile`, import.meta.url));
14
+ const selectedBackendName = resolveBackendName(process.env[`SYSTEMD_TS_TEST_HOST_BACKEND`]);
15
+ const colimaHome = process.env[`COLIMA_HOME`] ?? `${repoRoot}.colima`;
16
+ const limaHome = process.env[`LIMA_HOME`] ?? `${colimaHome}/_lima`;
17
+ const colimaProfile = process.env[`COLIMA_PROFILE`] ?? `systemd-ts`;
18
+ const dockerStateRoot = process.env[`SYSTEMD_TS_TEST_HOST_ROOT`] ?? `${repoRoot}.docker`;
19
+ const dockerImage = process.env[`SYSTEMD_TS_TEST_DOCKER_IMAGE`] ?? `systemd-ts-test-host:local`;
20
+ const dockerUser = process.env[`SYSTEMD_TS_TEST_DOCKER_USER`] ?? `runner`;
21
+ const dockerConfiguredUserId = process.env[`SYSTEMD_TS_TEST_DOCKER_UID`];
22
+ const dockerContainer = process.env[`SYSTEMD_TS_TEST_DOCKER_CONTAINER`] ?? defaultDockerContainerName();
23
+ let ensuredHost;
24
+ let guestShell;
25
+ function getTestHostInfo() {
26
+ return backend.info;
27
+ }
28
+ function ensureTestHost() {
29
+ ensuredHost ??= ensureTestHostInner();
30
+ return ensuredHost;
31
+ }
32
+ function closeTestHost() {
33
+ closeGuestShell(`warmup complete`);
34
+ }
35
+ async function runGuestCommand(script) {
36
+ return getGuestShell().run(script);
37
+ }
38
+ async function ensureTestHostInner() {
39
+ logTestHost(`Ensuring ${backend.info.backend} test host`);
40
+ await backend.ensureHost();
41
+ logTestHost(`Waiting for systemd --user readiness`);
42
+ await waitForUserSystemd();
43
+ logTestHost(`Test host ready`);
44
+ return getTestHostInfo();
45
+ }
46
+ async function waitForUserSystemd() {
47
+ for (let attempt = 0; attempt < 30; attempt += 1) {
48
+ const state = await runGuestCommandWithTimeout(`printf 'pid1=%s\n' "$(ps -p 1 -o comm=)"; systemctl --user is-system-running || true; printf 'xdg=%s\n' "$XDG_RUNTIME_DIR"`, guestCommandTimeoutMs);
49
+ if (state.includes(`pid1=systemd`) && state.includes(`xdg=/run/user/`) && [`running`, `degraded`].some((status) => state.includes(status))) {
50
+ logTestHost(`systemd --user ready after ${attempt + 1} attempt${attempt === 0 ? `` : `s`}: ${summarizeSystemdState(state)}`);
51
+ return;
52
+ }
53
+ if (attempt === 0 || (attempt + 1) % 5 === 0) logTestHost(`systemd --user not ready yet (${attempt + 1}/30): ${summarizeSystemdState(state)}`);
54
+ await setTimeout$1(1e3);
55
+ }
56
+ throw new Error(`Timed out waiting for systemd --user in the ${backend.info.backend} guest`);
57
+ }
58
+ function getGuestShell() {
59
+ guestShell ??= new GuestShell(() => backend.createGuestShellProcess(), () => {
60
+ if (guestShell !== void 0) guestShell = void 0;
61
+ });
62
+ return guestShell;
63
+ }
64
+ function closeGuestShell(reason) {
65
+ if (guestShell === void 0) return;
66
+ logTestHost(`Resetting guest shell: ${reason}`);
67
+ guestShell.close();
68
+ guestShell = void 0;
69
+ }
70
+ async function runGuestCommandWithTimeout(script, timeoutMs) {
71
+ const shell = getGuestShell();
72
+ try {
73
+ return await shell.run(script, timeoutMs);
74
+ } catch (error) {
75
+ if (error instanceof Error && error.message.includes(`timed out`)) closeGuestShell(error.message);
76
+ throw error;
77
+ }
78
+ }
79
+ function mergeOutput(stdout, stderr) {
80
+ return [stdout, stderr].filter((output) => output.length > 0).join(`\n`);
81
+ }
82
+ function summarizeSystemdState(state) {
83
+ return state.split(`\n`).map((line) => line.trim()).filter((line) => line.length > 0).join(`; `);
84
+ }
85
+ function inferBackendName() {
86
+ if (process.platform === `darwin`) return `colima`;
87
+ if (process.platform === `linux`) return `docker`;
88
+ throw new Error(`Unsupported test host platform: ${process.platform}. Set SYSTEMD_TS_TEST_HOST_BACKEND explicitly if you have a compatible backend.`);
89
+ }
90
+ function resolveBackendName(value) {
91
+ if (value === void 0) return inferBackendName();
92
+ if (value === `colima` || value === `docker`) return value;
93
+ throw new Error(`Unsupported SYSTEMD_TS_TEST_HOST_BACKEND value: ${value}. Expected "colima" or "docker".`);
94
+ }
95
+ function defaultDockerContainerName() {
96
+ return `systemd-ts-${createHash(`sha256`).update(repoRoot).digest(`hex`).slice(0, 12)}`;
97
+ }
98
+ function logTestHost(message) {
99
+ console.info(`[systemd-ts:test-host] ${message}`);
100
+ }
101
+ function createBackend(name) {
102
+ switch (name) {
103
+ case `colima`: return new ColimaBackend();
104
+ case `docker`: return new DockerBackend();
105
+ }
106
+ }
107
+ var ColimaBackend = class {
108
+ info = {
109
+ backend: `colima`,
110
+ colimaHome,
111
+ colimaProfile,
112
+ limaHome,
113
+ repoRoot,
114
+ stateRoot: colimaHome
115
+ };
116
+ async ensureHost() {
117
+ if (await this.isRunning()) return;
118
+ console.info(`Starting repo-local Colima host for integration tests...`);
119
+ await this.execColima([
120
+ `start`,
121
+ `--cpu`,
122
+ `2`,
123
+ `--memory`,
124
+ `4`,
125
+ `--disk`,
126
+ `30`,
127
+ `--runtime`,
128
+ `containerd`
129
+ ]);
130
+ }
131
+ createGuestShellProcess() {
132
+ return spawn(`colima`, [
133
+ `ssh`,
134
+ `--`,
135
+ `bash`,
136
+ `--noprofile`,
137
+ `--norc`,
138
+ `-s`
139
+ ], {
140
+ env: this.hostEnv,
141
+ stdio: [
142
+ `pipe`,
143
+ `pipe`,
144
+ `pipe`
145
+ ]
146
+ });
147
+ }
148
+ async runIsolatedCommand(script) {
149
+ return this.execColima([
150
+ `ssh`,
151
+ `--`,
152
+ `bash`,
153
+ `-lc`,
154
+ script
155
+ ]);
156
+ }
157
+ hostEnv = {
158
+ ...process.env,
159
+ COLIMA_HOME: colimaHome,
160
+ COLIMA_PROFILE: colimaProfile,
161
+ LIMA_HOME: limaHome
162
+ };
163
+ async isRunning() {
164
+ try {
165
+ return (await this.execColima([`status`])).includes(`is running`);
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+ async execColima(args) {
171
+ const result = await execFileAsync(`colima`, args, {
172
+ env: this.hostEnv,
173
+ maxBuffer: commandMaxBufferBytes
174
+ });
175
+ return mergeOutput(result.stdout, result.stderr);
176
+ }
177
+ };
178
+ var DockerBackend = class {
179
+ info = {
180
+ backend: `docker`,
181
+ repoRoot,
182
+ stateRoot: dockerStateRoot
183
+ };
184
+ runtimeInfo;
185
+ async ensureHost() {
186
+ logTestHost(`Building Docker test host image ${dockerImage}`);
187
+ await this.execDocker([
188
+ `build`,
189
+ `--quiet`,
190
+ `--tag`,
191
+ dockerImage,
192
+ `--file`,
193
+ dockerfilePath,
194
+ repoRoot
195
+ ], { timeout: dockerBuildTimeoutMs });
196
+ logTestHost(`Docker test host image ready`);
197
+ if (!await this.isContainerRunning()) {
198
+ await this.removeExistingContainer();
199
+ logTestHost(`Starting Docker test host container ${dockerContainer}`);
200
+ await this.execDocker([
201
+ `run`,
202
+ `--detach`,
203
+ `--name`,
204
+ dockerContainer,
205
+ `--hostname`,
206
+ `systemd-ts-test-host`,
207
+ `--privileged`,
208
+ `--cgroupns=host`,
209
+ `--tmpfs`,
210
+ `/run`,
211
+ `--tmpfs`,
212
+ `/run/lock`,
213
+ `--volume`,
214
+ `/sys/fs/cgroup:/sys/fs/cgroup:rw`,
215
+ `--volume`,
216
+ `${repoRoot}:${repoRoot}`,
217
+ `--volume`,
218
+ `${dockerStateRoot}:${dockerStateRoot}`,
219
+ `--workdir`,
220
+ repoRoot,
221
+ dockerImage
222
+ ]);
223
+ logTestHost(`Docker test host container started`);
224
+ } else logTestHost(`Reusing running Docker test host container ${dockerContainer}`);
225
+ await this.ensureUserSession();
226
+ }
227
+ createGuestShellProcess() {
228
+ const runtimeInfo = this.requireRuntimeInfo();
229
+ return spawn(`docker`, [
230
+ `exec`,
231
+ `--interactive`,
232
+ `--user`,
233
+ dockerUser,
234
+ `--env`,
235
+ `HOME=${runtimeInfo.home}`,
236
+ `--env`,
237
+ `XDG_RUNTIME_DIR=${runtimeInfo.runtimeDir}`,
238
+ `--env`,
239
+ `DBUS_SESSION_BUS_ADDRESS=unix:path=${runtimeInfo.runtimeDir}/bus`,
240
+ dockerContainer,
241
+ `bash`,
242
+ `--noprofile`,
243
+ `--norc`,
244
+ `-s`
245
+ ], { stdio: [
246
+ `pipe`,
247
+ `pipe`,
248
+ `pipe`
249
+ ] });
250
+ }
251
+ async runIsolatedCommand(script) {
252
+ const runtimeInfo = this.requireRuntimeInfo();
253
+ const result = await this.execDockerAsUser(runtimeInfo, [
254
+ `bash`,
255
+ `-lc`,
256
+ script
257
+ ]);
258
+ return mergeOutput(result.stdout, result.stderr);
259
+ }
260
+ async isContainerRunning() {
261
+ try {
262
+ return (await this.execDocker([
263
+ `inspect`,
264
+ `--format`,
265
+ `{{.State.Running}}`,
266
+ dockerContainer
267
+ ])).trim() === `true`;
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+ async removeExistingContainer() {
273
+ try {
274
+ await this.execDocker([
275
+ `rm`,
276
+ `--force`,
277
+ dockerContainer
278
+ ]);
279
+ } catch {}
280
+ }
281
+ async ensureUserSession() {
282
+ const runtimeInfo = await this.resolveRuntimeInfo();
283
+ logTestHost(`Ensuring user session for ${dockerUser} (uid=${runtimeInfo.userId}, home=${runtimeInfo.home})`);
284
+ await this.execDocker([
285
+ `exec`,
286
+ dockerContainer,
287
+ `bash`,
288
+ `-lc`,
289
+ [
290
+ `mkdir -p ${shellQuote$1(dockerStateRoot)}`,
291
+ `chown ${runtimeInfo.userId}:${runtimeInfo.userId} ${shellQuote$1(dockerStateRoot)}`,
292
+ `loginctl enable-linger ${shellQuote$1(dockerUser)}`,
293
+ `systemctl start user@${runtimeInfo.userId}.service`
294
+ ].join(`\n`)
295
+ ], { timeout: dockerExecTimeoutMs });
296
+ await this.execDockerAsUser(runtimeInfo, [
297
+ `bash`,
298
+ `-lc`,
299
+ `mkdir -p ${shellQuote$1(`${dockerStateRoot}/tests`)}`
300
+ ]);
301
+ logTestHost(`User session started for ${dockerUser}`);
302
+ }
303
+ async execDocker(args, options) {
304
+ const result = await execFileAsync(`docker`, args, {
305
+ maxBuffer: commandMaxBufferBytes,
306
+ timeout: options?.timeout
307
+ });
308
+ return mergeOutput(result.stdout, result.stderr);
309
+ }
310
+ execDockerAsUser(runtimeInfo, command) {
311
+ return execFileAsync(`docker`, [
312
+ `exec`,
313
+ `--user`,
314
+ dockerUser,
315
+ `--env`,
316
+ `HOME=${runtimeInfo.home}`,
317
+ `--env`,
318
+ `XDG_RUNTIME_DIR=${runtimeInfo.runtimeDir}`,
319
+ `--env`,
320
+ `DBUS_SESSION_BUS_ADDRESS=unix:path=${runtimeInfo.runtimeDir}/bus`,
321
+ dockerContainer,
322
+ ...command
323
+ ], { maxBuffer: commandMaxBufferBytes });
324
+ }
325
+ requireRuntimeInfo() {
326
+ if (this.runtimeInfo === void 0) throw new Error(`Docker test host runtime info is unavailable before ensureHost() completes`);
327
+ return this.runtimeInfo;
328
+ }
329
+ async resolveRuntimeInfo() {
330
+ if (this.runtimeInfo !== void 0) return this.runtimeInfo;
331
+ const userId = dockerConfiguredUserId === void 0 ? await this.lookupUserId() : Number(dockerConfiguredUserId);
332
+ const home = await this.lookupUserHome();
333
+ this.runtimeInfo = {
334
+ home,
335
+ runtimeDir: `/run/user/${userId}`,
336
+ userId
337
+ };
338
+ return this.runtimeInfo;
339
+ }
340
+ async lookupUserHome() {
341
+ logTestHost(`Resolving home directory for Docker test user ${dockerUser}`);
342
+ const home = (await this.execDocker([
343
+ `exec`,
344
+ dockerContainer,
345
+ `bash`,
346
+ `-lc`,
347
+ `getent passwd ${shellQuote$1(dockerUser)} | cut -d: -f6`
348
+ ])).trim();
349
+ if (home.length === 0) throw new Error(`Could not resolve home directory for Docker test user ${dockerUser}`);
350
+ logTestHost(`Resolved home directory for ${dockerUser}: ${home}`);
351
+ return home;
352
+ }
353
+ async lookupUserId() {
354
+ logTestHost(`Resolving uid for Docker test user ${dockerUser}`);
355
+ const output = await this.execDocker([
356
+ `exec`,
357
+ dockerContainer,
358
+ `bash`,
359
+ `-lc`,
360
+ `id -u ${shellQuote$1(dockerUser)}`
361
+ ]);
362
+ const parsed = Number(output.trim());
363
+ if (!Number.isInteger(parsed)) throw new Error(`Could not resolve uid for Docker test user ${dockerUser}`);
364
+ logTestHost(`Resolved uid for ${dockerUser}: ${parsed}`);
365
+ return parsed;
366
+ }
367
+ };
368
+ var GuestShell = class {
369
+ process;
370
+ onExitCleanup;
371
+ closed = false;
372
+ buffer = ``;
373
+ current;
374
+ queue = [];
375
+ constructor(createProcess, onExitCleanup) {
376
+ this.process = createProcess();
377
+ this.onExitCleanup = onExitCleanup;
378
+ this.process.stdout.setEncoding(`utf8`);
379
+ this.process.stderr.setEncoding(`utf8`);
380
+ this.process.stdout.on(`data`, (chunk) => this.onData(chunk));
381
+ this.process.stderr.on(`data`, (chunk) => this.onData(chunk));
382
+ this.process.on(`exit`, (code, signal) => {
383
+ const reason = /* @__PURE__ */ new Error(`Persistent guest shell exited unexpectedly (code=${code ?? `null`}, signal=${signal ?? `null`})`);
384
+ this.failCurrent(reason);
385
+ while (this.queue.length > 0) this.queue.shift()?.reject(reason);
386
+ this.onExitCleanup();
387
+ });
388
+ process.once(`exit`, () => {
389
+ this.process.kill();
390
+ });
391
+ }
392
+ run(script, timeoutMs) {
393
+ return new Promise((resolve, reject) => {
394
+ this.queue.push({
395
+ script,
396
+ resolve,
397
+ reject,
398
+ timeoutMs
399
+ });
400
+ this.drain();
401
+ });
402
+ }
403
+ close() {
404
+ if (this.closed) return;
405
+ this.closed = true;
406
+ this.process.kill();
407
+ }
408
+ drain() {
409
+ if (this.current !== void 0) return;
410
+ const next = this.queue.shift();
411
+ if (next === void 0) return;
412
+ const marker = `__SYSTEMD_TS_END_${randomUUID()}__`;
413
+ const timeoutId = next.timeoutMs === void 0 ? void 0 : setTimeout(() => {
414
+ if (this.current?.marker !== marker) return;
415
+ const reason = /* @__PURE__ */ new Error(`Guest command timed out after ${next.timeoutMs}ms`);
416
+ this.failCurrent(reason);
417
+ this.close();
418
+ }, next.timeoutMs);
419
+ this.current = {
420
+ marker,
421
+ timeoutId,
422
+ reject: next.reject,
423
+ resolve: next.resolve
424
+ };
425
+ this.buffer = ``;
426
+ const command = `(
427
+ ${next.script}
428
+ ) 2>&1
429
+ status=$?
430
+ printf '\\n${marker}:%s\\n' "$status"
431
+ `;
432
+ this.process.stdin.write(command);
433
+ }
434
+ failCurrent(error) {
435
+ const current = this.current;
436
+ this.current = void 0;
437
+ if (current?.timeoutId !== void 0) clearTimeout(current.timeoutId);
438
+ current?.reject(error);
439
+ }
440
+ onData(chunk) {
441
+ if (this.current === void 0) return;
442
+ this.buffer += chunk;
443
+ const markerIndex = this.buffer.indexOf(this.current.marker);
444
+ if (markerIndex === -1) return;
445
+ const output = this.buffer.slice(0, markerIndex).replace(/\n$/, ``);
446
+ const statusMatch = this.buffer.slice(markerIndex).match(/^__SYSTEMD_TS_END_[^:]+__:(\d+)\r?\n?/u);
447
+ if (statusMatch === null) return;
448
+ const status = Number(statusMatch[1]);
449
+ const remainder = this.buffer.slice(markerIndex + statusMatch[0].length);
450
+ const current = this.current;
451
+ this.current = void 0;
452
+ this.buffer = remainder;
453
+ if (current.timeoutId !== void 0) clearTimeout(current.timeoutId);
454
+ if (status === 0) current.resolve(output);
455
+ else current.reject(new Error(output.length > 0 ? output : `Guest command failed`));
456
+ this.drain();
457
+ }
458
+ };
459
+ function shellQuote$1(value) {
460
+ return `'${value.replaceAll(`'`, `'\\''`)}'`;
461
+ }
462
+ const backend = createBackend(selectedBackendName);
463
+ //#endregion
464
+ //#region src/testing/sandbox.ts
465
+ let currentSandbox;
466
+ async function createTestSandbox(testName) {
467
+ const id = randomUUID().slice(0, 8);
468
+ const namePrefix = `systemd-ts-${slugify(testName ?? `sandbox`)}-${id}`;
469
+ const rootDir = `${getTestHostInfo().stateRoot}/tests/${namePrefix}`;
470
+ const linkedUnitDir = `${rootDir}/linked-units`;
471
+ const workDir = `${rootDir}/work`;
472
+ await runGuestCommand(`set -euo pipefail
473
+ rm -rf ${shellQuote(rootDir)}
474
+ mkdir -p ${shellQuote(linkedUnitDir)} ${shellQuote(workDir)}`);
475
+ currentSandbox = {
476
+ id,
477
+ linkedUnitDir,
478
+ namePrefix,
479
+ rootDir,
480
+ workDir
481
+ };
482
+ return currentSandbox;
483
+ }
484
+ async function destroyCurrentTestSandbox() {
485
+ if (currentSandbox === void 0) return;
486
+ const sandbox = currentSandbox;
487
+ currentSandbox = void 0;
488
+ const units = (await runGuestCommand(`systemctl --user list-unit-files --all --no-legend ${shellQuote(`${sandbox.namePrefix}*`)} 2>/dev/null | awk '{print $1}' || true`)).split(`\n`).map((unit) => unit.trim()).filter((unit) => unit.length > 0);
489
+ if (units.length > 0) {
490
+ await runGuestCommand(`systemctl --user stop ${units.map(shellQuote).join(` `)} || true`);
491
+ await runGuestCommand(`systemctl --user disable ${units.map(shellQuote).join(` `)} || true`);
492
+ await runGuestCommand(`systemctl --user reset-failed ${units.map(shellQuote).join(` `)} || true`);
493
+ }
494
+ await runGuestCommand(`systemctl --user daemon-reload || true`);
495
+ await runGuestCommand(`rm -rf ${shellQuote(sandbox.rootDir)}`);
496
+ }
497
+ function useCurrentTestSandbox() {
498
+ if (currentSandbox === void 0) throw new Error(`No active test sandbox is available`);
499
+ return currentSandbox;
500
+ }
501
+ function shellQuote(value) {
502
+ return `'${value.replaceAll(`'`, `'\\''`)}'`;
503
+ }
504
+ function slugify(value) {
505
+ return value.toLowerCase().replaceAll(/[^a-z0-9]+/g, `-`).replaceAll(/^-+|-+$/g, ``).slice(0, 32);
506
+ }
507
+ //#endregion
508
+ export { closeTestHost, createTestSandbox, destroyCurrentTestSandbox, ensureTestHost, getTestHostInfo, runGuestCommand, useCurrentTestSandbox };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "systemd-ts",
3
+ "version": "0.0.0",
4
+ "description": "TypeScript builders and helpers for systemd services and timers.",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "exports": {
11
+ ".": "./dist/index.mjs",
12
+ "./test": "./dist/test.mjs",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "build": "vp pack",
20
+ "dev": "vp pack --watch",
21
+ "test": "vp test",
22
+ "check": "vp check",
23
+ "prepublishOnly": "vp run build"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "catalog:",
27
+ "@typescript/native-preview": "catalog:",
28
+ "takua": "catalog:",
29
+ "typescript": "catalog:",
30
+ "vite-plus": "catalog:"
31
+ }
32
+ }