paratix 0.8.0 → 0.10.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/{chunk-FFQ6FR4N.js → chunk-M7GETOJ5.js} +428 -35
- package/dist/chunk-M7GETOJ5.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/modules/index.d.ts +79 -2
- package/dist/modules/index.js +3 -1
- package/dist/{user-BJMqDePy.d.ts → user-CJDqZC8n.d.ts} +130 -84
- package/llm-guide.md +70 -10
- package/package.json +1 -1
- package/dist/chunk-FFQ6FR4N.js.map +0 -1
|
@@ -173,6 +173,11 @@ async function setFlag(ssh2, flagName) {
|
|
|
173
173
|
// src/modules/apt.ts
|
|
174
174
|
var NONINTERACTIVE = "DEBIAN_FRONTEND=noninteractive";
|
|
175
175
|
var APT_REPOSITORY_MODE = "0644";
|
|
176
|
+
var APT_BASE_EXEC_OPTS = { ignoreExitCode: true, silent: true };
|
|
177
|
+
function aptExecOptions(options) {
|
|
178
|
+
if (options?.timeout === void 0) return APT_BASE_EXEC_OPTS;
|
|
179
|
+
return { ...APT_BASE_EXEC_OPTS, timeout: options.timeout };
|
|
180
|
+
}
|
|
176
181
|
var PPA_PREFIX = "ppa:";
|
|
177
182
|
function parseDebconfOutput(stdout) {
|
|
178
183
|
const values = {};
|
|
@@ -292,30 +297,31 @@ var apt = {
|
|
|
292
297
|
* Run `apt-get update && apt-get dist-upgrade` once per dated flag.
|
|
293
298
|
* Performs a full distribution upgrade with dependency resolution.
|
|
294
299
|
*
|
|
300
|
+
* The pipeline is split into three separate SSH commands so that each step
|
|
301
|
+
* gets its own timeout window and produces a precise failure label.
|
|
302
|
+
*
|
|
295
303
|
* @param date - A date string used as the idempotency key (e.g. `"2024-01-15"`).
|
|
304
|
+
* @param options - Optional per-call overrides (e.g. SSH command `timeout`).
|
|
305
|
+
* The same `timeout` is applied to every step of the dist-upgrade pipeline.
|
|
296
306
|
* @returns A Module that performs the dist-upgrade.
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* apt.distUpgrade("2024-01-15")
|
|
310
|
+
* apt.distUpgrade("2024-01-15", { timeout: 900_000 })
|
|
297
311
|
*/
|
|
298
|
-
distUpgrade(date) {
|
|
312
|
+
distUpgrade(date, options) {
|
|
299
313
|
const flagName = `apt-dist-upgrade-${date}`;
|
|
300
314
|
return {
|
|
301
315
|
async apply(ssh2) {
|
|
302
316
|
if (!ssh2) return failed(`[apt.distUpgrade] SSH connection is required for ${date}`);
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
silent: true
|
|
306
|
-
});
|
|
317
|
+
const pipelineOptions = aptExecOptions(options);
|
|
318
|
+
const update = await ssh2.exec(`${NONINTERACTIVE} apt-get update`, pipelineOptions);
|
|
307
319
|
if (update.code !== 0)
|
|
308
320
|
return failedCommand("[apt.distUpgrade] apt-get update failed", update);
|
|
309
|
-
const configure = await ssh2.exec(`${NONINTERACTIVE} dpkg --configure -a`,
|
|
310
|
-
ignoreExitCode: true,
|
|
311
|
-
silent: true
|
|
312
|
-
});
|
|
321
|
+
const configure = await ssh2.exec(`${NONINTERACTIVE} dpkg --configure -a`, pipelineOptions);
|
|
313
322
|
if (configure.code !== 0)
|
|
314
323
|
return failedCommand("[apt.distUpgrade] dpkg --configure -a failed", configure);
|
|
315
|
-
const upgrade = await ssh2.exec(`${NONINTERACTIVE} apt-get dist-upgrade -y`,
|
|
316
|
-
ignoreExitCode: true,
|
|
317
|
-
silent: true
|
|
318
|
-
});
|
|
324
|
+
const upgrade = await ssh2.exec(`${NONINTERACTIVE} apt-get dist-upgrade -y`, pipelineOptions);
|
|
319
325
|
if (upgrade.code !== 0)
|
|
320
326
|
return failedCommand("[apt.distUpgrade] apt-get dist-upgrade failed", upgrade);
|
|
321
327
|
await setVersionedFlag(ssh2, flagName, "apt-dist-upgrade-");
|
|
@@ -1117,7 +1123,48 @@ function hasMarkedJob(lines, marker, cronJob) {
|
|
|
1117
1123
|
const index = lines.indexOf(marker);
|
|
1118
1124
|
return index !== -1 && index + 1 < lines.length && lines[index + 1] === cronJob;
|
|
1119
1125
|
}
|
|
1126
|
+
function assertCronName(name) {
|
|
1127
|
+
if (new RegExp("[\\n\\r]", "v").test(name)) {
|
|
1128
|
+
throw new Error(`cron: name must not contain newlines: ${JSON.stringify(name)}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1120
1131
|
var cron = {
|
|
1132
|
+
/**
|
|
1133
|
+
* Ensure a cron job is absent from a user's crontab.
|
|
1134
|
+
*
|
|
1135
|
+
* Removes the `# paratix: <name>` marker comment and the crontab line
|
|
1136
|
+
* directly below it. If the marker is not found, the module reports `ok`
|
|
1137
|
+
* without writing the crontab. When the crontab becomes empty after the
|
|
1138
|
+
* removal, it is deleted entirely via `crontab -r`.
|
|
1139
|
+
*
|
|
1140
|
+
* Equivalent to `cron.job(user, name, { job: "<unused>", state: "absent" })`,
|
|
1141
|
+
* but does not require a placeholder `job` argument.
|
|
1142
|
+
*
|
|
1143
|
+
* @param user - The target user whose crontab is managed.
|
|
1144
|
+
* @param name - Unique identifier of the cron job marker to remove.
|
|
1145
|
+
* @returns A Module that ensures the cron job entry is absent.
|
|
1146
|
+
*/
|
|
1147
|
+
absent(user2, name) {
|
|
1148
|
+
assertCronName(name);
|
|
1149
|
+
const marker = `# paratix: ${name}`;
|
|
1150
|
+
return {
|
|
1151
|
+
async apply(ssh2) {
|
|
1152
|
+
if (!ssh2) return failed(`[cron.absent: ${name} (${user2})] SSH connection is required`);
|
|
1153
|
+
const lines = await readCrontab(ssh2, user2);
|
|
1154
|
+
const markerIndex = lines.indexOf(marker);
|
|
1155
|
+
if (markerIndex === -1) return { status: "ok" };
|
|
1156
|
+
lines.splice(markerIndex, 2);
|
|
1157
|
+
await writeCrontab(ssh2, user2, lines);
|
|
1158
|
+
return { status: "changed" };
|
|
1159
|
+
},
|
|
1160
|
+
async check(ssh2) {
|
|
1161
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
1162
|
+
const lines = await readCrontab(ssh2, user2);
|
|
1163
|
+
return lines.includes(marker) ? NEEDS_APPLY : "ok";
|
|
1164
|
+
},
|
|
1165
|
+
name: `cron.absent: ${name} (${user2})`
|
|
1166
|
+
};
|
|
1167
|
+
},
|
|
1121
1168
|
/**
|
|
1122
1169
|
* Ensure a cron job is present in (or absent from) a user's crontab.
|
|
1123
1170
|
*
|
|
@@ -1134,9 +1181,7 @@ var cron = {
|
|
|
1134
1181
|
* @returns A Module that manages the cron job entry.
|
|
1135
1182
|
*/
|
|
1136
1183
|
job(user2, name, options) {
|
|
1137
|
-
|
|
1138
|
-
throw new Error(`cron.job: name must not contain newlines: ${JSON.stringify(name)}`);
|
|
1139
|
-
}
|
|
1184
|
+
assertCronName(name);
|
|
1140
1185
|
if (new RegExp("[\\n\\r]", "v").test(options.job)) {
|
|
1141
1186
|
throw new Error(`cron.job: job must not contain newlines: ${JSON.stringify(options.job)}`);
|
|
1142
1187
|
}
|
|
@@ -3137,6 +3182,22 @@ var op = {
|
|
|
3137
3182
|
|
|
3138
3183
|
// src/modules/package.ts
|
|
3139
3184
|
var EXEC_OPTS6 = { ignoreExitCode: true, silent: true };
|
|
3185
|
+
function execOptions(options) {
|
|
3186
|
+
if (options?.timeout === void 0) return EXEC_OPTS6;
|
|
3187
|
+
return { ...EXEC_OPTS6, timeout: options.timeout };
|
|
3188
|
+
}
|
|
3189
|
+
function splitPackagesAndOptions(values) {
|
|
3190
|
+
const packages = [];
|
|
3191
|
+
let options;
|
|
3192
|
+
for (const [index, value] of values.entries()) {
|
|
3193
|
+
if (typeof value === "string") {
|
|
3194
|
+
packages.push(value);
|
|
3195
|
+
} else if (index === values.length - 1) {
|
|
3196
|
+
options = value;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
return { options, packages };
|
|
3200
|
+
}
|
|
3140
3201
|
var pmCache = /* @__PURE__ */ new WeakMap();
|
|
3141
3202
|
var INSTALL_COMMANDS = {
|
|
3142
3203
|
apk: (pkgs) => `apk add ${pkgs}`,
|
|
@@ -3157,10 +3218,14 @@ var UPDATE_COMMANDS = {
|
|
|
3157
3218
|
yum: "yum makecache"
|
|
3158
3219
|
};
|
|
3159
3220
|
var UPGRADE_COMMANDS = {
|
|
3160
|
-
apk: "apk update
|
|
3161
|
-
apt:
|
|
3162
|
-
|
|
3163
|
-
|
|
3221
|
+
apk: ["apk update", "apk upgrade"],
|
|
3222
|
+
apt: [
|
|
3223
|
+
"DEBIAN_FRONTEND=noninteractive dpkg --configure -a",
|
|
3224
|
+
"DEBIAN_FRONTEND=noninteractive apt-get update",
|
|
3225
|
+
"DEBIAN_FRONTEND=noninteractive apt-get upgrade -y"
|
|
3226
|
+
],
|
|
3227
|
+
dnf: ["dnf upgrade -y"],
|
|
3228
|
+
yum: ["yum update -y"]
|
|
3164
3229
|
};
|
|
3165
3230
|
function missingPackageManager(moduleName) {
|
|
3166
3231
|
return failed(`[${moduleName}] No supported package manager found (apt, dnf, yum, apk)`);
|
|
@@ -3200,13 +3265,19 @@ var pkg = {
|
|
|
3200
3265
|
* The check phase queries the package database for each package individually;
|
|
3201
3266
|
* the remove command is only executed when at least one package is present.
|
|
3202
3267
|
*
|
|
3203
|
-
*
|
|
3268
|
+
* Pass an `UpgradeOptions` object as the last argument to override the SSH
|
|
3269
|
+
* timeout for slow remove operations.
|
|
3270
|
+
*
|
|
3271
|
+
* @param packagesAndOptions - One or more package names, optionally followed
|
|
3272
|
+
* by an `UpgradeOptions` object as the last argument.
|
|
3204
3273
|
* @returns A Module that removes the packages if any are present.
|
|
3205
3274
|
*
|
|
3206
3275
|
* @example
|
|
3207
3276
|
* pkg.absent("vim", "nano")
|
|
3277
|
+
* pkg.absent("vim", "nano", { timeout: 600_000 })
|
|
3208
3278
|
*/
|
|
3209
|
-
absent(...
|
|
3279
|
+
absent(...packagesAndOptions) {
|
|
3280
|
+
const { options, packages } = splitPackagesAndOptions(packagesAndOptions);
|
|
3210
3281
|
if (packages.length === 0) {
|
|
3211
3282
|
throw new Error("package.absent: at least one package name is required");
|
|
3212
3283
|
}
|
|
@@ -3217,7 +3288,7 @@ var pkg = {
|
|
|
3217
3288
|
const pm = await detectPackageManager(ssh2);
|
|
3218
3289
|
if (!pm) return missingPackageManager(`package.absent: ${packages.join(", ")}`);
|
|
3219
3290
|
const quoted = packages.map((p) => shellQuote(p)).join(" ");
|
|
3220
|
-
const result = await ssh2.exec(REMOVE_COMMANDS[pm](quoted),
|
|
3291
|
+
const result = await ssh2.exec(REMOVE_COMMANDS[pm](quoted), execOptions(options));
|
|
3221
3292
|
if (result.code !== 0) {
|
|
3222
3293
|
return failedCommand(
|
|
3223
3294
|
`[package.absent: ${packages.join(", ")}] package removal failed`,
|
|
@@ -3244,13 +3315,19 @@ var pkg = {
|
|
|
3244
3315
|
* The check phase queries the package database for each package individually;
|
|
3245
3316
|
* the install command is only executed when at least one package is missing.
|
|
3246
3317
|
*
|
|
3247
|
-
*
|
|
3318
|
+
* Pass an `UpgradeOptions` object as the last argument to override the SSH
|
|
3319
|
+
* timeout for slow install operations.
|
|
3320
|
+
*
|
|
3321
|
+
* @param packagesAndOptions - One or more package names, optionally followed
|
|
3322
|
+
* by an `UpgradeOptions` object as the last argument.
|
|
3248
3323
|
* @returns A Module that installs missing packages.
|
|
3249
3324
|
*
|
|
3250
3325
|
* @example
|
|
3251
3326
|
* pkg.installed("git", "curl", "unzip")
|
|
3327
|
+
* pkg.installed("texlive-full", { timeout: 900_000 })
|
|
3252
3328
|
*/
|
|
3253
|
-
installed(...
|
|
3329
|
+
installed(...packagesAndOptions) {
|
|
3330
|
+
const { options, packages } = splitPackagesAndOptions(packagesAndOptions);
|
|
3254
3331
|
if (packages.length === 0) {
|
|
3255
3332
|
throw new Error("package.installed: at least one package name is required");
|
|
3256
3333
|
}
|
|
@@ -3262,7 +3339,7 @@ var pkg = {
|
|
|
3262
3339
|
const pm = await detectPackageManager(ssh2);
|
|
3263
3340
|
if (!pm) return missingPackageManager(`package.installed: ${packages.join(", ")}`);
|
|
3264
3341
|
const quoted = packages.map((p) => shellQuote(p)).join(" ");
|
|
3265
|
-
const result = await ssh2.exec(INSTALL_COMMANDS[pm](quoted),
|
|
3342
|
+
const result = await ssh2.exec(INSTALL_COMMANDS[pm](quoted), execOptions(options));
|
|
3266
3343
|
if (result.code !== 0) {
|
|
3267
3344
|
return failedCommand(
|
|
3268
3345
|
`[package.installed: ${packages.join(", ")}] package installation failed`,
|
|
@@ -3292,19 +3369,21 @@ var pkg = {
|
|
|
3292
3369
|
* value invalidates all previous flags for this operation.
|
|
3293
3370
|
*
|
|
3294
3371
|
* @param date - A date string used as the idempotency key (e.g. `"2024-01-15"`).
|
|
3372
|
+
* @param options - Optional per-call overrides (e.g. SSH command `timeout`).
|
|
3295
3373
|
* @returns A Module that refreshes package lists.
|
|
3296
3374
|
*
|
|
3297
3375
|
* @example
|
|
3298
3376
|
* pkg.update("2024-01-15")
|
|
3377
|
+
* pkg.update("2024-01-15", { timeout: 600_000 })
|
|
3299
3378
|
*/
|
|
3300
|
-
update(date) {
|
|
3379
|
+
update(date, options) {
|
|
3301
3380
|
const flagName = `package-update-${date}`;
|
|
3302
3381
|
return {
|
|
3303
3382
|
async apply(ssh2) {
|
|
3304
3383
|
if (!ssh2) return failed(`[package.update: ${date}] SSH connection is required`);
|
|
3305
3384
|
const pm = await detectPackageManager(ssh2);
|
|
3306
3385
|
if (!pm) return missingPackageManager(`package.update: ${date}`);
|
|
3307
|
-
const result = await ssh2.exec(UPDATE_COMMANDS[pm],
|
|
3386
|
+
const result = await ssh2.exec(UPDATE_COMMANDS[pm], execOptions(options));
|
|
3308
3387
|
if (result.code !== 0) {
|
|
3309
3388
|
return failedCommand(`[package.update: ${date}] package index refresh failed`, result);
|
|
3310
3389
|
}
|
|
@@ -3326,27 +3405,34 @@ var pkg = {
|
|
|
3326
3405
|
* reports `"ok"` without running the upgrade again. Changing `date` to a new
|
|
3327
3406
|
* value invalidates all previous flags for this operation.
|
|
3328
3407
|
*
|
|
3329
|
-
* On apt systems this runs `
|
|
3330
|
-
* `
|
|
3408
|
+
* On apt systems this runs `dpkg --configure -a`, `apt-get update`, and
|
|
3409
|
+
* `apt-get upgrade -y` as three separate commands (each subject to its own
|
|
3410
|
+
* SSH `timeout`). For full dependency resolution use `apt.distUpgrade`.
|
|
3331
3411
|
*
|
|
3332
3412
|
* @param date - A date string used as the idempotency key (e.g. `"2024-01-15"`).
|
|
3413
|
+
* @param options - Optional per-call overrides (e.g. SSH command `timeout`).
|
|
3414
|
+
* The same `timeout` is applied to every step of the upgrade pipeline.
|
|
3333
3415
|
* @returns A Module that upgrades all packages.
|
|
3334
3416
|
*
|
|
3335
3417
|
* @example
|
|
3336
3418
|
* pkg.upgrade("2024-01-15")
|
|
3419
|
+
* pkg.upgrade("2024-01-15", { timeout: 900_000 })
|
|
3337
3420
|
*
|
|
3338
3421
|
* @see apt.distUpgrade
|
|
3339
3422
|
*/
|
|
3340
|
-
upgrade(date) {
|
|
3423
|
+
upgrade(date, options) {
|
|
3341
3424
|
const flagName = `package-upgrade-${date}`;
|
|
3342
3425
|
return {
|
|
3343
3426
|
async apply(ssh2) {
|
|
3344
3427
|
if (!ssh2) return failed(`[package.upgrade: ${date}] SSH connection is required`);
|
|
3345
3428
|
const pm = await detectPackageManager(ssh2);
|
|
3346
3429
|
if (!pm) return missingPackageManager(`package.upgrade: ${date}`);
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3349
|
-
|
|
3430
|
+
const pipelineOptions = execOptions(options);
|
|
3431
|
+
for (const command2 of UPGRADE_COMMANDS[pm]) {
|
|
3432
|
+
const result = await ssh2.exec(command2, pipelineOptions);
|
|
3433
|
+
if (result.code !== 0) {
|
|
3434
|
+
return failedCommand(`[package.upgrade: ${date}] package upgrade failed`, result);
|
|
3435
|
+
}
|
|
3350
3436
|
}
|
|
3351
3437
|
await setVersionedFlag(ssh2, flagName, "package-upgrade-");
|
|
3352
3438
|
return { status: "changed" };
|
|
@@ -5574,6 +5660,312 @@ var systemd = {
|
|
|
5574
5660
|
}
|
|
5575
5661
|
};
|
|
5576
5662
|
|
|
5663
|
+
// src/modules/timerHelpers.ts
|
|
5664
|
+
var SYSTEMD_DIRECTORY = "/etc/systemd/system";
|
|
5665
|
+
var TIMER_NAME_PATTERN = new RegExp("^[A-Za-z0-9_\\-]+$", "v");
|
|
5666
|
+
var ENVIRONMENT_KEY_PATTERN = new RegExp("^[A-Za-z_]\\w*$", "v");
|
|
5667
|
+
var ENVIRONMENT_VALUE_NEEDS_QUOTING = new RegExp('[\\s"\\\\]', "v");
|
|
5668
|
+
function assertNoNewline(field, value) {
|
|
5669
|
+
if (new RegExp("[\\n\\r]", "v").test(value)) {
|
|
5670
|
+
throw new Error(`timer.scheduled: ${field} must not contain newlines: ${JSON.stringify(value)}`);
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
function assertNotBlank(field, value) {
|
|
5674
|
+
if (value.trim().length === 0) {
|
|
5675
|
+
throw new Error(`timer.scheduled: ${field} must not be empty: ${JSON.stringify(value)}`);
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
function normalizeOnCalendar(onCalendar) {
|
|
5679
|
+
const entries = Array.isArray(onCalendar) ? onCalendar : [onCalendar];
|
|
5680
|
+
if (entries.length === 0) {
|
|
5681
|
+
throw new Error("timer.scheduled: onCalendar must contain at least one entry");
|
|
5682
|
+
}
|
|
5683
|
+
for (const entry of entries) {
|
|
5684
|
+
assertNoNewline("onCalendar entry", entry);
|
|
5685
|
+
if (entry.trim().length === 0) {
|
|
5686
|
+
throw new Error("timer.scheduled: onCalendar entries must not be empty");
|
|
5687
|
+
}
|
|
5688
|
+
}
|
|
5689
|
+
return entries;
|
|
5690
|
+
}
|
|
5691
|
+
function quoteEnvironmentValue(value) {
|
|
5692
|
+
if (!ENVIRONMENT_VALUE_NEEDS_QUOTING.test(value)) return value;
|
|
5693
|
+
const escaped = value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
5694
|
+
return `"${escaped}"`;
|
|
5695
|
+
}
|
|
5696
|
+
function renderServiceUnit(name, options) {
|
|
5697
|
+
const description = options.description ?? `Paratix scheduled task: ${name}`;
|
|
5698
|
+
const lines = ["[Unit]", `Description=${description}`, "", "[Service]", "Type=oneshot"];
|
|
5699
|
+
if (options.user != null) lines.push(`User=${options.user}`);
|
|
5700
|
+
if (options.group != null) lines.push(`Group=${options.group}`);
|
|
5701
|
+
if (options.workingDirectory != null) {
|
|
5702
|
+
lines.push(`WorkingDirectory=${options.workingDirectory}`);
|
|
5703
|
+
}
|
|
5704
|
+
if (options.environment) {
|
|
5705
|
+
for (const [key, value] of Object.entries(options.environment)) {
|
|
5706
|
+
lines.push(`Environment=${key}=${quoteEnvironmentValue(value)}`);
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
lines.push(`ExecStart=${options.exec}`);
|
|
5710
|
+
return `${lines.join("\n")}
|
|
5711
|
+
`;
|
|
5712
|
+
}
|
|
5713
|
+
function renderTimerUnit(name, options) {
|
|
5714
|
+
const description = options.description ?? `Paratix scheduled task: ${name}`;
|
|
5715
|
+
const persistent = options.persistent ?? true;
|
|
5716
|
+
const lines = ["[Unit]", `Description=${description} (timer)`, "", "[Timer]"];
|
|
5717
|
+
for (const entry of normalizeOnCalendar(options.onCalendar)) {
|
|
5718
|
+
lines.push(`OnCalendar=${entry}`);
|
|
5719
|
+
}
|
|
5720
|
+
if (persistent) lines.push("Persistent=true");
|
|
5721
|
+
if (options.randomizedDelaySec != null) {
|
|
5722
|
+
lines.push(`RandomizedDelaySec=${String(options.randomizedDelaySec)}`);
|
|
5723
|
+
}
|
|
5724
|
+
if (options.accuracySec != null) lines.push(`AccuracySec=${String(options.accuracySec)}`);
|
|
5725
|
+
lines.push(`Unit=${name}.service`, "", "[Install]", "WantedBy=timers.target");
|
|
5726
|
+
return `${lines.join("\n")}
|
|
5727
|
+
`;
|
|
5728
|
+
}
|
|
5729
|
+
function validateEnvironment(environment) {
|
|
5730
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
5731
|
+
if (!ENVIRONMENT_KEY_PATTERN.test(key)) {
|
|
5732
|
+
throw new Error(
|
|
5733
|
+
`timer.scheduled: environment key must match ${String(ENVIRONMENT_KEY_PATTERN)}, got: ${JSON.stringify(key)}`
|
|
5734
|
+
);
|
|
5735
|
+
}
|
|
5736
|
+
assertNoNewline(`environment[${key}]`, value);
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
function validatePresentOptions(options) {
|
|
5740
|
+
assertNoNewline("exec", options.exec);
|
|
5741
|
+
assertNotBlank("exec", options.exec);
|
|
5742
|
+
const optionalStringFields = [
|
|
5743
|
+
["description", options.description],
|
|
5744
|
+
["user", options.user],
|
|
5745
|
+
["group", options.group],
|
|
5746
|
+
["workingDirectory", options.workingDirectory]
|
|
5747
|
+
];
|
|
5748
|
+
for (const [field, value] of optionalStringFields) {
|
|
5749
|
+
if (value != null) {
|
|
5750
|
+
assertNoNewline(field, value);
|
|
5751
|
+
assertNotBlank(field, value);
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
if (options.randomizedDelaySec != null) {
|
|
5755
|
+
assertNoNewline("randomizedDelaySec", String(options.randomizedDelaySec));
|
|
5756
|
+
}
|
|
5757
|
+
if (options.accuracySec != null) {
|
|
5758
|
+
assertNoNewline("accuracySec", String(options.accuracySec));
|
|
5759
|
+
}
|
|
5760
|
+
if (options.environment) validateEnvironment(options.environment);
|
|
5761
|
+
normalizeOnCalendar(options.onCalendar);
|
|
5762
|
+
}
|
|
5763
|
+
function assertTimerName(name) {
|
|
5764
|
+
if (!TIMER_NAME_PATTERN.test(name)) {
|
|
5765
|
+
throw new Error(
|
|
5766
|
+
`timer: name must match ${String(TIMER_NAME_PATTERN)}, got: ${JSON.stringify(name)}`
|
|
5767
|
+
);
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
function buildTimerLocations(name) {
|
|
5771
|
+
return {
|
|
5772
|
+
servicePath: `${SYSTEMD_DIRECTORY}/${name}.service`,
|
|
5773
|
+
timerPath: `${SYSTEMD_DIRECTORY}/${name}.timer`,
|
|
5774
|
+
timerUnit: `${name}.timer`
|
|
5775
|
+
};
|
|
5776
|
+
}
|
|
5777
|
+
function buildTimerPaths(name, options) {
|
|
5778
|
+
return {
|
|
5779
|
+
...buildTimerLocations(name),
|
|
5780
|
+
serviceContent: renderServiceUnit(name, options),
|
|
5781
|
+
timerContent: renderTimerUnit(name, options)
|
|
5782
|
+
};
|
|
5783
|
+
}
|
|
5784
|
+
|
|
5785
|
+
// src/modules/timer.ts
|
|
5786
|
+
var SYSTEMCTL5 = "systemctl";
|
|
5787
|
+
var UNIT_FILE_MODE = "0644";
|
|
5788
|
+
async function fileMatches(ssh2, path, expected) {
|
|
5789
|
+
if (!await ssh2.exists(path)) return false;
|
|
5790
|
+
const remote = await ssh2.readFile(path);
|
|
5791
|
+
return remote.trim() === expected.trim();
|
|
5792
|
+
}
|
|
5793
|
+
async function checkPresent2(ssh2, paths) {
|
|
5794
|
+
if (!await fileMatches(ssh2, paths.servicePath, paths.serviceContent)) return NEEDS_APPLY;
|
|
5795
|
+
if (!await fileMatches(ssh2, paths.timerPath, paths.timerContent)) return NEEDS_APPLY;
|
|
5796
|
+
const enabled = await ssh2.test(`${SYSTEMCTL5} is-enabled --quiet ${shellQuote(paths.timerUnit)}`);
|
|
5797
|
+
if (!enabled) return NEEDS_APPLY;
|
|
5798
|
+
const active = await ssh2.test(`${SYSTEMCTL5} is-active --quiet ${shellQuote(paths.timerUnit)}`);
|
|
5799
|
+
return active ? "ok" : NEEDS_APPLY;
|
|
5800
|
+
}
|
|
5801
|
+
async function checkAbsent2(ssh2, locations) {
|
|
5802
|
+
if (await ssh2.exists(locations.servicePath)) return NEEDS_APPLY;
|
|
5803
|
+
if (await ssh2.exists(locations.timerPath)) return NEEDS_APPLY;
|
|
5804
|
+
return "ok";
|
|
5805
|
+
}
|
|
5806
|
+
async function syncUnitFiles(ssh2, name, paths) {
|
|
5807
|
+
const serviceMatched = await fileMatches(ssh2, paths.servicePath, paths.serviceContent);
|
|
5808
|
+
const timerMatched = await fileMatches(ssh2, paths.timerPath, paths.timerContent);
|
|
5809
|
+
if (!serviceMatched) {
|
|
5810
|
+
await ssh2.writeFile(paths.servicePath, paths.serviceContent, { mode: UNIT_FILE_MODE });
|
|
5811
|
+
}
|
|
5812
|
+
if (!timerMatched) {
|
|
5813
|
+
await ssh2.writeFile(paths.timerPath, paths.timerContent, { mode: UNIT_FILE_MODE });
|
|
5814
|
+
}
|
|
5815
|
+
if (!serviceMatched || !timerMatched) {
|
|
5816
|
+
const reload = await ssh2.exec(`${SYSTEMCTL5} daemon-reload`, {
|
|
5817
|
+
ignoreExitCode: true,
|
|
5818
|
+
silent: true
|
|
5819
|
+
});
|
|
5820
|
+
if (reload.code !== 0) {
|
|
5821
|
+
return {
|
|
5822
|
+
failure: failedCommand(`[timer.scheduled: ${name}] systemctl daemon-reload failed`, reload),
|
|
5823
|
+
ok: false
|
|
5824
|
+
};
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
return { ok: true, serviceMatched, timerMatched };
|
|
5828
|
+
}
|
|
5829
|
+
async function isTimerFullyActive(ssh2, timerUnit) {
|
|
5830
|
+
const enabled = await ssh2.test(`${SYSTEMCTL5} is-enabled --quiet ${shellQuote(timerUnit)}`);
|
|
5831
|
+
if (!enabled) return false;
|
|
5832
|
+
return ssh2.test(`${SYSTEMCTL5} is-active --quiet ${shellQuote(timerUnit)}`);
|
|
5833
|
+
}
|
|
5834
|
+
async function applyPresent(ssh2, name, paths) {
|
|
5835
|
+
const sync = await syncUnitFiles(ssh2, name, paths);
|
|
5836
|
+
if (!sync.ok) return sync.failure;
|
|
5837
|
+
if (sync.serviceMatched && sync.timerMatched && await isTimerFullyActive(ssh2, paths.timerUnit)) {
|
|
5838
|
+
return { status: "ok" };
|
|
5839
|
+
}
|
|
5840
|
+
const enable = await ssh2.exec(`${SYSTEMCTL5} enable --now ${shellQuote(paths.timerUnit)}`, {
|
|
5841
|
+
ignoreExitCode: true,
|
|
5842
|
+
silent: true
|
|
5843
|
+
});
|
|
5844
|
+
if (enable.code !== 0) {
|
|
5845
|
+
return failedCommand(`[timer.scheduled: ${name}] systemctl enable --now failed`, enable);
|
|
5846
|
+
}
|
|
5847
|
+
if (!sync.timerMatched) {
|
|
5848
|
+
const restart = await ssh2.exec(`${SYSTEMCTL5} restart ${shellQuote(paths.timerUnit)}`, {
|
|
5849
|
+
ignoreExitCode: true,
|
|
5850
|
+
silent: true
|
|
5851
|
+
});
|
|
5852
|
+
if (restart.code !== 0) {
|
|
5853
|
+
return failedCommand(`[timer.scheduled: ${name}] systemctl restart failed`, restart);
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
return { status: "changed" };
|
|
5857
|
+
}
|
|
5858
|
+
async function applyAbsent(ssh2, context) {
|
|
5859
|
+
const { locations, module, name } = context;
|
|
5860
|
+
const serviceExists = await ssh2.exists(locations.servicePath);
|
|
5861
|
+
const timerExists = await ssh2.exists(locations.timerPath);
|
|
5862
|
+
if (!serviceExists && !timerExists) return { status: "ok" };
|
|
5863
|
+
await ssh2.exec(`${SYSTEMCTL5} disable --now ${shellQuote(locations.timerUnit)}`, {
|
|
5864
|
+
ignoreExitCode: true,
|
|
5865
|
+
silent: true
|
|
5866
|
+
});
|
|
5867
|
+
const remove = await ssh2.exec(
|
|
5868
|
+
`rm -f ${shellQuote(locations.timerPath)} ${shellQuote(locations.servicePath)}`,
|
|
5869
|
+
{ ignoreExitCode: true, silent: true }
|
|
5870
|
+
);
|
|
5871
|
+
if (remove.code !== 0) {
|
|
5872
|
+
return failedCommand(`[${module}: ${name}] failed to remove unit files`, remove);
|
|
5873
|
+
}
|
|
5874
|
+
const reload = await ssh2.exec(`${SYSTEMCTL5} daemon-reload`, {
|
|
5875
|
+
ignoreExitCode: true,
|
|
5876
|
+
silent: true
|
|
5877
|
+
});
|
|
5878
|
+
if (reload.code !== 0) {
|
|
5879
|
+
return failedCommand(`[${module}: ${name}] systemctl daemon-reload failed`, reload);
|
|
5880
|
+
}
|
|
5881
|
+
return { status: "changed" };
|
|
5882
|
+
}
|
|
5883
|
+
var timer = {
|
|
5884
|
+
/**
|
|
5885
|
+
* Ensure a systemd timer-driven scheduled task does not exist.
|
|
5886
|
+
*
|
|
5887
|
+
* Disables and stops `<name>.timer`, removes both `<name>.service` and
|
|
5888
|
+
* `<name>.timer` from `/etc/systemd/system/`, and reloads systemd. The
|
|
5889
|
+
* `disable --now` step also removes the timer's `wants/` symlink. Failure
|
|
5890
|
+
* to disable a unit that does not exist is ignored, so this method is
|
|
5891
|
+
* safe to apply repeatedly.
|
|
5892
|
+
*
|
|
5893
|
+
* Behaves like `timer.scheduled(name, { exec: "<unused>", onCalendar: "<unused>", state: "absent" })`,
|
|
5894
|
+
* but does not require placeholder values for `exec` or `onCalendar` and
|
|
5895
|
+
* reports failures with a `timer.absent` prefix instead of `timer.scheduled`.
|
|
5896
|
+
*
|
|
5897
|
+
* @param name - Base unit name without extension. Must match
|
|
5898
|
+
* `^[A-Za-z0-9_\-]+$`.
|
|
5899
|
+
* @returns A Module that ensures the timer-driven scheduled task is absent.
|
|
5900
|
+
*/
|
|
5901
|
+
absent(name) {
|
|
5902
|
+
assertTimerName(name);
|
|
5903
|
+
const locations = buildTimerLocations(name);
|
|
5904
|
+
return {
|
|
5905
|
+
async apply(ssh2) {
|
|
5906
|
+
if (!ssh2) return failed(`[timer.absent: ${name}] SSH connection is required`);
|
|
5907
|
+
return applyAbsent(ssh2, { locations, module: "timer.absent", name });
|
|
5908
|
+
},
|
|
5909
|
+
async check(ssh2) {
|
|
5910
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
5911
|
+
return checkAbsent2(ssh2, locations);
|
|
5912
|
+
},
|
|
5913
|
+
name: `timer.absent: ${name}`
|
|
5914
|
+
};
|
|
5915
|
+
},
|
|
5916
|
+
/**
|
|
5917
|
+
* Ensure a systemd timer-driven scheduled task is present (or absent).
|
|
5918
|
+
*
|
|
5919
|
+
* Generates `<name>.service` (`Type=oneshot`, no `[Install]` section since
|
|
5920
|
+
* it is triggered exclusively by the timer) and `<name>.timer` under
|
|
5921
|
+
* `/etc/systemd/system/`, reloads systemd, and runs
|
|
5922
|
+
* `systemctl enable --now <name>.timer`. When the timer unit content
|
|
5923
|
+
* changed, the timer is restarted so a new `OnCalendar=` schedule is picked
|
|
5924
|
+
* up immediately. With `state: "absent"`, the timer is disabled and
|
|
5925
|
+
* stopped, both unit files are removed, and systemd is reloaded.
|
|
5926
|
+
*
|
|
5927
|
+
* Validation of `exec`, `description`, `user`, `group`, `workingDirectory`,
|
|
5928
|
+
* `environment` and `onCalendar` only runs when `state` is `"present"`,
|
|
5929
|
+
* so callers can pass placeholder values when removing a timer.
|
|
5930
|
+
*
|
|
5931
|
+
* @param name - Base unit name without extension. Used as `<name>.service`
|
|
5932
|
+
* and `<name>.timer`. Must match `^[A-Za-z0-9_\-]+$`.
|
|
5933
|
+
* @param options - Schedule, command, optional service hardening, and state.
|
|
5934
|
+
* @returns A Module that manages the timer-driven scheduled task.
|
|
5935
|
+
*/
|
|
5936
|
+
scheduled(name, options) {
|
|
5937
|
+
assertTimerName(name);
|
|
5938
|
+
const state = options.state ?? "present";
|
|
5939
|
+
if (state === "absent") {
|
|
5940
|
+
const locations = buildTimerLocations(name);
|
|
5941
|
+
return {
|
|
5942
|
+
async apply(ssh2) {
|
|
5943
|
+
if (!ssh2) return failed(`[timer.scheduled: ${name}] SSH connection is required`);
|
|
5944
|
+
return applyAbsent(ssh2, { locations, module: "timer.scheduled", name });
|
|
5945
|
+
},
|
|
5946
|
+
async check(ssh2) {
|
|
5947
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
5948
|
+
return checkAbsent2(ssh2, locations);
|
|
5949
|
+
},
|
|
5950
|
+
name: `timer.scheduled: ${name}`
|
|
5951
|
+
};
|
|
5952
|
+
}
|
|
5953
|
+
validatePresentOptions(options);
|
|
5954
|
+
const paths = buildTimerPaths(name, options);
|
|
5955
|
+
return {
|
|
5956
|
+
async apply(ssh2) {
|
|
5957
|
+
if (!ssh2) return failed(`[timer.scheduled: ${name}] SSH connection is required`);
|
|
5958
|
+
return applyPresent(ssh2, name, paths);
|
|
5959
|
+
},
|
|
5960
|
+
async check(ssh2) {
|
|
5961
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
5962
|
+
return checkPresent2(ssh2, paths);
|
|
5963
|
+
},
|
|
5964
|
+
name: `timer.scheduled: ${name}`
|
|
5965
|
+
};
|
|
5966
|
+
}
|
|
5967
|
+
};
|
|
5968
|
+
|
|
5577
5969
|
// src/modules/ufw.ts
|
|
5578
5970
|
var UFW = "ufw";
|
|
5579
5971
|
var ufw = {
|
|
@@ -5838,7 +6230,8 @@ export {
|
|
|
5838
6230
|
swap,
|
|
5839
6231
|
system,
|
|
5840
6232
|
systemd,
|
|
6233
|
+
timer,
|
|
5841
6234
|
ufw,
|
|
5842
6235
|
user
|
|
5843
6236
|
};
|
|
5844
|
-
//# sourceMappingURL=chunk-
|
|
6237
|
+
//# sourceMappingURL=chunk-M7GETOJ5.js.map
|