paratix 0.3.0 → 0.5.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 +20 -1
- package/dist/{chunk-DUIGEB2J.js → chunk-3WK4QNJK.js} +73 -17
- package/dist/chunk-3WK4QNJK.js.map +1 -0
- package/dist/{chunk-ULJMW23T.js → chunk-LI47NIKN.js} +683 -64
- package/dist/chunk-LI47NIKN.js.map +1 -0
- package/dist/{chunk-G3BMCQKU.js → chunk-MHPFGCEY.js} +158 -34
- package/dist/chunk-MHPFGCEY.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +17 -2
- package/dist/index.js +197 -100
- package/dist/index.js.map +1 -1
- package/dist/modules/index.d.ts +23 -1313
- package/dist/modules/index.js +6 -2
- package/dist/user-CraCJci7.d.ts +1392 -0
- package/llm-guide.md +86 -4
- package/package.json +1 -1
- package/dist/chunk-DUIGEB2J.js.map +0 -1
- package/dist/chunk-G3BMCQKU.js.map +0 -1
- package/dist/chunk-ULJMW23T.js.map +0 -1
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
shellQuote,
|
|
10
10
|
sshdPortMeta,
|
|
11
11
|
validateMode
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-MHPFGCEY.js";
|
|
13
13
|
|
|
14
14
|
// src/moduleFailure.ts
|
|
15
15
|
function firstNonEmptyLine(text) {
|
|
@@ -676,11 +676,111 @@ function parseContainerStates(stdout) {
|
|
|
676
676
|
function sanitizeUnitValue(value) {
|
|
677
677
|
return value.replaceAll(new RegExp("[\\n\\r]", "gv"), "");
|
|
678
678
|
}
|
|
679
|
-
function
|
|
679
|
+
function validateGeneratedSystemdUnitContent(content, unitFileName) {
|
|
680
|
+
if (content.trim() === "") {
|
|
681
|
+
return failed(`[compose.systemd] generated empty unit content for ${unitFileName}`);
|
|
682
|
+
}
|
|
683
|
+
if (!content.includes("[Unit]") || !content.includes("[Service]")) {
|
|
684
|
+
return failed(`[compose.systemd] generated invalid unit content for ${unitFileName}`);
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
async function verifyNonEmptySystemdUnit(parameters) {
|
|
689
|
+
const writtenContent = await parameters.connection.readFile(parameters.filePath);
|
|
690
|
+
if (writtenContent.trim() === "") return "empty";
|
|
691
|
+
return writtenContent.trim() === parameters.content.trim() ? "matches" : "unexpected";
|
|
692
|
+
}
|
|
693
|
+
async function cleanupComposeSystemdTarget(parameters) {
|
|
694
|
+
await parameters.connection.exec(`rm -f ${shellQuote(parameters.filePath)}`, {
|
|
695
|
+
ignoreExitCode: true,
|
|
696
|
+
silent: true
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
async function rewriteComposeSystemdUnitViaShell(parameters) {
|
|
700
|
+
const encodedContent = Buffer.from(parameters.content, "utf8").toString("base64");
|
|
701
|
+
await cleanupComposeSystemdTarget(parameters);
|
|
702
|
+
const result = await parameters.connection.exec(
|
|
703
|
+
`printf '%s' ${shellQuote(encodedContent)} | base64 -d > ${shellQuote(parameters.filePath)} && chmod ${shellQuote(SYSTEMD_UNIT_MODE)} ${shellQuote(parameters.filePath)} && chown ${shellQuote("root:root")} ${shellQuote(parameters.filePath)}`,
|
|
704
|
+
EXEC_OPTS2
|
|
705
|
+
);
|
|
706
|
+
if (result.code !== 0) {
|
|
707
|
+
return failedCommand(
|
|
708
|
+
`[compose.systemd] shell fallback write failed for ${parameters.unitFileName}`,
|
|
709
|
+
result
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
const fallbackVerification = await verifyNonEmptySystemdUnit(parameters);
|
|
713
|
+
if (fallbackVerification === "matches") return null;
|
|
714
|
+
await cleanupComposeSystemdTarget(parameters);
|
|
715
|
+
if (fallbackVerification === "empty") {
|
|
716
|
+
return failed(
|
|
717
|
+
`[compose.systemd] wrote empty unit file for ${parameters.unitFileName} even after shell fallback`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
return failed(
|
|
721
|
+
`[compose.systemd] wrote unexpected unit content for ${parameters.unitFileName} even after shell fallback`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
async function applyComposeSystemdUnit(parameters) {
|
|
725
|
+
await prepareComposeSystemdTarget(parameters);
|
|
726
|
+
try {
|
|
727
|
+
await parameters.connection.writeFile(parameters.filePath, parameters.content, {
|
|
728
|
+
mode: SYSTEMD_UNIT_MODE
|
|
729
|
+
});
|
|
730
|
+
} catch (error) {
|
|
731
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
732
|
+
return failed(`[compose.systemd] atomic write failed for ${parameters.unitFileName}: ${reason}`);
|
|
733
|
+
}
|
|
734
|
+
const writeVerification = await verifyNonEmptySystemdUnit(parameters);
|
|
735
|
+
if (writeVerification !== "matches") {
|
|
736
|
+
const fallbackFailure = await rewriteComposeSystemdUnitViaShell(parameters);
|
|
737
|
+
if (fallbackFailure != null) return fallbackFailure;
|
|
738
|
+
}
|
|
739
|
+
const result = await parameters.connection.exec("systemctl daemon-reload", EXEC_OPTS2);
|
|
740
|
+
return result.code === 0 ? { status: "changed" } : failedCommand(`[compose.systemd] daemon-reload failed for ${parameters.unitFileName}`, result);
|
|
741
|
+
}
|
|
742
|
+
async function checkComposeSystemdUnit(parameters) {
|
|
743
|
+
const runtime = await getRuntime(parameters.ssh, parameters.explicitRuntime);
|
|
744
|
+
if (!runtime) return NEEDS_APPLY;
|
|
745
|
+
const exists = await parameters.ssh.exists(parameters.filePath);
|
|
746
|
+
if (!exists) return NEEDS_APPLY;
|
|
747
|
+
const content = generateSystemdUnit(parameters.projectDirectory, parameters.serviceName, {
|
|
748
|
+
detached: parameters.detached,
|
|
749
|
+
runtime
|
|
750
|
+
});
|
|
751
|
+
const remoteContent = await parameters.ssh.readFile(parameters.filePath);
|
|
752
|
+
return remoteContent.trim() === content.trim() ? "ok" : NEEDS_APPLY;
|
|
753
|
+
}
|
|
754
|
+
function resolveComposeSystemdIdentity(options) {
|
|
755
|
+
const serviceName = options.name ?? `compose-${basename(options.projectDirectory)}`;
|
|
756
|
+
if (!UNIT_NAME_PATTERN.test(serviceName)) {
|
|
757
|
+
throw new Error(
|
|
758
|
+
`compose.systemd: name must match ${String(UNIT_NAME_PATTERN)}, got: ${serviceName}`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const unitFileName = `${serviceName}.service`;
|
|
762
|
+
return {
|
|
763
|
+
filePath: `/etc/systemd/system/${unitFileName}`,
|
|
764
|
+
serviceName,
|
|
765
|
+
unitFileName
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
async function prepareComposeSystemdTarget(parameters) {
|
|
769
|
+
await parameters.connection.exec(`systemctl unmask ${shellQuote(parameters.unitFileName)}`, {
|
|
770
|
+
ignoreExitCode: true,
|
|
771
|
+
silent: true
|
|
772
|
+
});
|
|
773
|
+
await parameters.connection.exec(`rm -f ${shellQuote(parameters.filePath)}`, {
|
|
774
|
+
ignoreExitCode: true,
|
|
775
|
+
silent: true
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
function generateSystemdUnit(projectDirectory, name, options) {
|
|
680
779
|
const safeName = sanitizeUnitValue(name);
|
|
681
780
|
const safeDirectory = sanitizeUnitValue(projectDirectory);
|
|
682
|
-
const
|
|
683
|
-
|
|
781
|
+
const composeUpCommand = options.detached ? `/usr/bin/env ${options.runtime} compose up -d --remove-orphans` : `/usr/bin/env ${options.runtime} compose up --remove-orphans`;
|
|
782
|
+
const lines = ["[Unit]", `Description=Compose stack: ${safeName}`, "Wants=network-online.target"];
|
|
783
|
+
if (options.runtime === "docker") {
|
|
684
784
|
lines.push("After=network-online.target docker.service");
|
|
685
785
|
lines.push("Requires=docker.service");
|
|
686
786
|
} else {
|
|
@@ -692,8 +792,11 @@ function generateSystemdUnit(projectDirectory, name, runtime) {
|
|
|
692
792
|
"Type=oneshot",
|
|
693
793
|
"RemainAfterExit=yes",
|
|
694
794
|
`WorkingDirectory=${safeDirectory}`,
|
|
695
|
-
`ExecStart
|
|
696
|
-
`ExecStop=/usr/bin/env ${runtime} compose down`,
|
|
795
|
+
`ExecStart=${composeUpCommand}`,
|
|
796
|
+
`ExecStop=/usr/bin/env ${options.runtime} compose down`,
|
|
797
|
+
"TimeoutStartSec=0",
|
|
798
|
+
"StandardOutput=journal",
|
|
799
|
+
"StandardError=journal",
|
|
697
800
|
"",
|
|
698
801
|
"[Install]",
|
|
699
802
|
"WantedBy=multi-user.target",
|
|
@@ -888,6 +991,7 @@ var compose = {
|
|
|
888
991
|
* explicitly provided.
|
|
889
992
|
*
|
|
890
993
|
* @param options - Configuration for the systemd unit.
|
|
994
|
+
* @param options.detached - When true, use `compose up -d`; otherwise start attached.
|
|
891
995
|
* @param options.projectDirectory - The project directory on the remote host.
|
|
892
996
|
* @param options.name - Optional service name (without `.service` suffix).
|
|
893
997
|
* @param options.runtime - Explicit container runtime override.
|
|
@@ -895,14 +999,11 @@ var compose = {
|
|
|
895
999
|
*/
|
|
896
1000
|
systemd(options) {
|
|
897
1001
|
const { projectDirectory, runtime: explicitRuntime } = options;
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
const unitFileName = `${serviceName}.service`;
|
|
905
|
-
const filePath = `/etc/systemd/system/${unitFileName}`;
|
|
1002
|
+
const detached = options.detached ?? false;
|
|
1003
|
+
const { filePath, serviceName, unitFileName } = resolveComposeSystemdIdentity({
|
|
1004
|
+
name: options.name,
|
|
1005
|
+
projectDirectory
|
|
1006
|
+
});
|
|
906
1007
|
return {
|
|
907
1008
|
async apply(ssh2) {
|
|
908
1009
|
const connection = requireComposeSsh(ssh2, "systemd", projectDirectory);
|
|
@@ -914,20 +1015,26 @@ var compose = {
|
|
|
914
1015
|
ssh: connection
|
|
915
1016
|
});
|
|
916
1017
|
if (typeof runtime !== "string") return runtime;
|
|
917
|
-
const content = generateSystemdUnit(projectDirectory, serviceName, runtime);
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
return
|
|
1018
|
+
const content = generateSystemdUnit(projectDirectory, serviceName, { detached, runtime });
|
|
1019
|
+
const validationFailure = validateGeneratedSystemdUnitContent(content, unitFileName);
|
|
1020
|
+
if (validationFailure != null) return validationFailure;
|
|
1021
|
+
return applyComposeSystemdUnit({
|
|
1022
|
+
connection,
|
|
1023
|
+
content,
|
|
1024
|
+
filePath,
|
|
1025
|
+
unitFileName
|
|
1026
|
+
});
|
|
921
1027
|
},
|
|
922
1028
|
async check(ssh2) {
|
|
923
1029
|
if (!ssh2) return NEEDS_APPLY;
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1030
|
+
return checkComposeSystemdUnit({
|
|
1031
|
+
detached,
|
|
1032
|
+
explicitRuntime,
|
|
1033
|
+
filePath,
|
|
1034
|
+
projectDirectory,
|
|
1035
|
+
serviceName,
|
|
1036
|
+
ssh: ssh2
|
|
1037
|
+
});
|
|
931
1038
|
},
|
|
932
1039
|
name: `compose.systemd: ${unitFileName}`
|
|
933
1040
|
};
|
|
@@ -1755,7 +1862,7 @@ function stat(remotePath) {
|
|
|
1755
1862
|
};
|
|
1756
1863
|
}
|
|
1757
1864
|
|
|
1758
|
-
// src/modules/
|
|
1865
|
+
// src/modules/fileMetadataHelpers.ts
|
|
1759
1866
|
var DEFAULT_FILE_WRITE_MODE2 = "0644";
|
|
1760
1867
|
async function readOwnership(ssh2, remotePath) {
|
|
1761
1868
|
const raw = await ssh2.output(`stat -c '%a %U %G' ${shellQuote(remotePath)}`);
|
|
@@ -1794,9 +1901,39 @@ function ownershipMatches(current, options) {
|
|
|
1794
1901
|
if (expectsGroup && current.group !== expectedGroup) return false;
|
|
1795
1902
|
return true;
|
|
1796
1903
|
}
|
|
1904
|
+
function createMetadataModule(kind, remotePath, value) {
|
|
1905
|
+
const name = `file.${kind}: ${remotePath}`;
|
|
1906
|
+
return {
|
|
1907
|
+
async apply(ssh2) {
|
|
1908
|
+
if (!ssh2) return failed(`[${name}] SSH connection is required`);
|
|
1909
|
+
if (kind === "chmod") validateMode(value);
|
|
1910
|
+
await ssh2.exec(`${kind} ${shellQuote(value)} ${shellQuote(remotePath)}`, { silent: true });
|
|
1911
|
+
return { status: "changed" };
|
|
1912
|
+
},
|
|
1913
|
+
async check(ssh2) {
|
|
1914
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
1915
|
+
if (!await ssh2.exists(remotePath)) return NEEDS_APPLY;
|
|
1916
|
+
const ownership = await readOwnership(ssh2, remotePath);
|
|
1917
|
+
const matches = kind === "chmod" ? { mode: value } : { owner: value };
|
|
1918
|
+
return ownershipMatches(ownership, matches) ? "ok" : NEEDS_APPLY;
|
|
1919
|
+
},
|
|
1920
|
+
name
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/modules/file.ts
|
|
1797
1925
|
function splitLines(content) {
|
|
1798
1926
|
return content.split(new RegExp("\\r?\\n", "v"));
|
|
1799
1927
|
}
|
|
1928
|
+
function findFirstMatchingLineIndex(lines, pattern) {
|
|
1929
|
+
return lines.findIndex((candidateLine) => pattern.test(candidateLine));
|
|
1930
|
+
}
|
|
1931
|
+
function splitLinesPreservingTrailingNewline(content) {
|
|
1932
|
+
const hasTrailingNewline = new RegExp("\\r?\\n$", "v").test(content);
|
|
1933
|
+
const lines = splitLines(content);
|
|
1934
|
+
if (hasTrailingNewline && lines.at(-1) === "") lines.pop();
|
|
1935
|
+
return { hasTrailingNewline, lines };
|
|
1936
|
+
}
|
|
1800
1937
|
function validateAbsentPath(remotePath) {
|
|
1801
1938
|
const trimmedPath = remotePath.trim();
|
|
1802
1939
|
if (trimmedPath.length === 0) {
|
|
@@ -1836,6 +1973,12 @@ var file = {
|
|
|
1836
1973
|
},
|
|
1837
1974
|
assemble,
|
|
1838
1975
|
block,
|
|
1976
|
+
chmod(remotePath, mode) {
|
|
1977
|
+
return createMetadataModule("chmod", remotePath, mode);
|
|
1978
|
+
},
|
|
1979
|
+
chown(remotePath, owner) {
|
|
1980
|
+
return createMetadataModule("chown", remotePath, owner);
|
|
1981
|
+
},
|
|
1839
1982
|
/**
|
|
1840
1983
|
* Upload a local file to the remote host.
|
|
1841
1984
|
* The file is only transferred when the remote SHA-256 differs from the local one.
|
|
@@ -1936,13 +2079,17 @@ var file = {
|
|
|
1936
2079
|
});
|
|
1937
2080
|
} else {
|
|
1938
2081
|
const content = await ssh2.readFile(remotePath);
|
|
2082
|
+
const { hasTrailingNewline, lines } = splitLinesPreservingTrailingNewline(content);
|
|
1939
2083
|
const pattern = new RegExp(options.match, "mu");
|
|
1940
|
-
|
|
2084
|
+
const matchingLineIndex = findFirstMatchingLineIndex(lines, pattern);
|
|
2085
|
+
if (matchingLineIndex === -1) {
|
|
1941
2086
|
return failed(
|
|
1942
2087
|
`[file.line: ${remotePath}] No line matching ${options.match} found for replacement`
|
|
1943
2088
|
);
|
|
1944
2089
|
}
|
|
1945
|
-
|
|
2090
|
+
lines[matchingLineIndex] = line;
|
|
2091
|
+
let newContent = lines.join("\n");
|
|
2092
|
+
if (hasTrailingNewline) newContent += "\n";
|
|
1946
2093
|
const ownership = await readOwnership(ssh2, remotePath);
|
|
1947
2094
|
await guardedWriteFile(ssh2, {
|
|
1948
2095
|
mode: normalizeMode2(ownership.mode),
|
|
@@ -1961,7 +2108,8 @@ var file = {
|
|
|
1961
2108
|
const lines = splitLines(content);
|
|
1962
2109
|
if (options?.match != null) {
|
|
1963
2110
|
const matchPattern = new RegExp(options.match, "mu");
|
|
1964
|
-
const
|
|
2111
|
+
const matchedLineIndex = findFirstMatchingLineIndex(lines, matchPattern);
|
|
2112
|
+
const matchedLine = matchedLineIndex === -1 ? void 0 : lines[matchedLineIndex];
|
|
1965
2113
|
return matchedLine === line ? "ok" : NEEDS_APPLY;
|
|
1966
2114
|
}
|
|
1967
2115
|
return lines.includes(line) ? "ok" : NEEDS_APPLY;
|
|
@@ -3212,6 +3360,144 @@ var pkg = {
|
|
|
3212
3360
|
}
|
|
3213
3361
|
};
|
|
3214
3362
|
|
|
3363
|
+
// src/modules/quadlet.ts
|
|
3364
|
+
var CONTAINERS_SYSTEMD_DIRECTORY = "/etc/containers/systemd";
|
|
3365
|
+
var QUADLET_FILE_MODE = "0644";
|
|
3366
|
+
var SYSTEMCTL = "systemctl";
|
|
3367
|
+
var UNIT_NAME_PATTERN2 = new RegExp("^[\\w@.\\-]+$", "v");
|
|
3368
|
+
function sanitizeQuadletValue(value) {
|
|
3369
|
+
return value.replaceAll(new RegExp("[\\n\\r]", "gv"), "");
|
|
3370
|
+
}
|
|
3371
|
+
function renderQuadletLine(key, value) {
|
|
3372
|
+
return `${key}=${sanitizeQuadletValue(value)}`;
|
|
3373
|
+
}
|
|
3374
|
+
function renderQuadletSection(name, lines) {
|
|
3375
|
+
return [`[${name}]`, ...lines, ""].join("\n");
|
|
3376
|
+
}
|
|
3377
|
+
function validateQuadletName(name) {
|
|
3378
|
+
if (!UNIT_NAME_PATTERN2.test(name)) {
|
|
3379
|
+
throw new Error(`quadlet.container: name must match ${String(UNIT_NAME_PATTERN2)}, got: ${name}`);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
function renderQuadletEnvironment(environment) {
|
|
3383
|
+
return Object.entries(environment).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => renderQuadletLine("Environment", `${key}=${value}`));
|
|
3384
|
+
}
|
|
3385
|
+
function renderQuadletRepeated(key, values) {
|
|
3386
|
+
return values.map((value) => renderQuadletLine(key, value));
|
|
3387
|
+
}
|
|
3388
|
+
function maybeRenderQuadletLine(key, value, transform) {
|
|
3389
|
+
if (value == null || value === "") return null;
|
|
3390
|
+
return renderQuadletLine(key, transform == null ? value : transform(value));
|
|
3391
|
+
}
|
|
3392
|
+
function compactQuadletLines(lines) {
|
|
3393
|
+
return lines.filter((line) => line != null);
|
|
3394
|
+
}
|
|
3395
|
+
function buildQuadletUnitSection(options) {
|
|
3396
|
+
return renderQuadletSection("Unit", [
|
|
3397
|
+
renderQuadletLine("Description", options.description ?? `Podman container: ${options.name}`),
|
|
3398
|
+
"Wants=network-online.target",
|
|
3399
|
+
"After=network-online.target"
|
|
3400
|
+
]);
|
|
3401
|
+
}
|
|
3402
|
+
function buildQuadletContainerLines(options) {
|
|
3403
|
+
return compactQuadletLines([
|
|
3404
|
+
renderQuadletLine("Image", options.image),
|
|
3405
|
+
maybeRenderQuadletLine("ContainerName", options.containerName),
|
|
3406
|
+
maybeRenderQuadletLine("AutoUpdate", options.autoUpdate),
|
|
3407
|
+
maybeRenderQuadletLine("Exec", options.exec?.join(" ")),
|
|
3408
|
+
maybeRenderQuadletLine("Restart", options.restart),
|
|
3409
|
+
...renderQuadletRepeated("Network", options.networks ?? []),
|
|
3410
|
+
...renderQuadletRepeated("PodmanArgs", options.podmanArgs ?? []),
|
|
3411
|
+
...renderQuadletRepeated("PublishPort", options.publishPorts ?? []),
|
|
3412
|
+
...renderQuadletRepeated("Volume", options.volumes ?? []),
|
|
3413
|
+
...renderQuadletEnvironment(options.environment ?? {})
|
|
3414
|
+
]);
|
|
3415
|
+
}
|
|
3416
|
+
function buildQuadletInstallSection(options) {
|
|
3417
|
+
return renderQuadletSection("Install", [
|
|
3418
|
+
renderQuadletLine("WantedBy", options.wantedBy ?? "multi-user.target")
|
|
3419
|
+
]);
|
|
3420
|
+
}
|
|
3421
|
+
function generateContainerQuadlet(options) {
|
|
3422
|
+
return [
|
|
3423
|
+
buildQuadletUnitSection(options),
|
|
3424
|
+
renderQuadletSection("Container", buildQuadletContainerLines(options)),
|
|
3425
|
+
buildQuadletInstallSection(options)
|
|
3426
|
+
].join("\n").trimEnd();
|
|
3427
|
+
}
|
|
3428
|
+
async function createQuadletDirectory(ssh2) {
|
|
3429
|
+
return ssh2.exec(`mkdir -p ${shellQuote(CONTAINERS_SYSTEMD_DIRECTORY)}`, {
|
|
3430
|
+
ignoreExitCode: true,
|
|
3431
|
+
silent: true
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
async function applyQuadletFile(parameters) {
|
|
3435
|
+
const mkdirResult = await createQuadletDirectory(parameters.ssh);
|
|
3436
|
+
if (mkdirResult.code !== 0) {
|
|
3437
|
+
return failedCommand(
|
|
3438
|
+
`[quadlet.container: ${parameters.name}] failed to create quadlet directory`,
|
|
3439
|
+
mkdirResult
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
await parameters.ssh.writeFile(parameters.filePath, parameters.content, {
|
|
3443
|
+
mode: QUADLET_FILE_MODE
|
|
3444
|
+
});
|
|
3445
|
+
const daemonReload = await parameters.ssh.exec(`${SYSTEMCTL} daemon-reload`, {
|
|
3446
|
+
ignoreExitCode: true,
|
|
3447
|
+
silent: true
|
|
3448
|
+
});
|
|
3449
|
+
return daemonReload.code === 0 ? { status: "changed" } : failedCommand(
|
|
3450
|
+
`[quadlet.container: ${parameters.name}] systemctl daemon-reload failed`,
|
|
3451
|
+
daemonReload
|
|
3452
|
+
);
|
|
3453
|
+
}
|
|
3454
|
+
async function checkQuadletFile(parameters) {
|
|
3455
|
+
const exists = await parameters.ssh.exists(parameters.filePath);
|
|
3456
|
+
if (!exists) return NEEDS_APPLY;
|
|
3457
|
+
const remoteContent = await parameters.ssh.readFile(parameters.filePath);
|
|
3458
|
+
return remoteContent.trim() === parameters.content.trim() ? "ok" : NEEDS_APPLY;
|
|
3459
|
+
}
|
|
3460
|
+
var quadlet = {
|
|
3461
|
+
/**
|
|
3462
|
+
* Write a Podman Quadlet `.container` definition and reload systemd when it changes.
|
|
3463
|
+
*
|
|
3464
|
+
* The resulting generated service can be controlled with `service.enabled(name)`
|
|
3465
|
+
* and `service.running(name)`.
|
|
3466
|
+
*
|
|
3467
|
+
* @param options - Configuration for the Quadlet container definition.
|
|
3468
|
+
* @param options.name - Quadlet base name without `.container`.
|
|
3469
|
+
* @param options.image - Container image reference.
|
|
3470
|
+
* @param options.description - Optional systemd unit description.
|
|
3471
|
+
* @param options.containerName - Optional explicit Podman container name.
|
|
3472
|
+
* @param options.autoUpdate - Optional Podman auto-update policy.
|
|
3473
|
+
* @param options.environment - Optional environment variables.
|
|
3474
|
+
* @param options.exec - Optional command and arguments for `Exec=`.
|
|
3475
|
+
* @param options.networks - Optional `Network=` entries.
|
|
3476
|
+
* @param options.podmanArgs - Optional `PodmanArgs=` entries.
|
|
3477
|
+
* @param options.publishPorts - Optional `PublishPort=` entries.
|
|
3478
|
+
* @param options.restart - Optional `Restart=` policy in the `[Container]` section.
|
|
3479
|
+
* @param options.volumes - Optional `Volume=` entries.
|
|
3480
|
+
* @param options.wantedBy - Optional install target. Defaults to `multi-user.target`.
|
|
3481
|
+
* @returns A Module that ensures the Quadlet file is present and up to date.
|
|
3482
|
+
*/
|
|
3483
|
+
container(options) {
|
|
3484
|
+
validateQuadletName(options.name);
|
|
3485
|
+
const filePath = `${CONTAINERS_SYSTEMD_DIRECTORY}/${options.name}.container`;
|
|
3486
|
+
const content = generateContainerQuadlet(options);
|
|
3487
|
+
return {
|
|
3488
|
+
async apply(ssh2) {
|
|
3489
|
+
if (!ssh2) return failed(`[quadlet.container: ${options.name}] SSH connection is required`);
|
|
3490
|
+
return applyQuadletFile({ content, filePath, name: options.name, ssh: ssh2 });
|
|
3491
|
+
},
|
|
3492
|
+
async check(ssh2) {
|
|
3493
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
3494
|
+
return checkQuadletFile({ content, filePath, ssh: ssh2 });
|
|
3495
|
+
},
|
|
3496
|
+
name: `quadlet.container: ${options.name}`
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
};
|
|
3500
|
+
|
|
3215
3501
|
// src/modules/releaseUpgrade.ts
|
|
3216
3502
|
var NONINTERACTIVE2 = "DEBIAN_FRONTEND=noninteractive";
|
|
3217
3503
|
var CODENAME_RE = new RegExp("^[a-z]{3,20}$", "v");
|
|
@@ -3666,7 +3952,7 @@ var script = {
|
|
|
3666
3952
|
};
|
|
3667
3953
|
|
|
3668
3954
|
// src/modules/service.ts
|
|
3669
|
-
var
|
|
3955
|
+
var SYSTEMCTL2 = "systemctl";
|
|
3670
3956
|
var service = {
|
|
3671
3957
|
/**
|
|
3672
3958
|
* Ensure a systemd service is disabled and will not start on boot.
|
|
@@ -3677,7 +3963,7 @@ var service = {
|
|
|
3677
3963
|
return {
|
|
3678
3964
|
async apply(ssh2) {
|
|
3679
3965
|
if (!ssh2) return failed(`[service.disabled: ${name}] SSH connection is required`);
|
|
3680
|
-
const result = await ssh2.exec(`${
|
|
3966
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} disable ${shellQuote(name)}`, {
|
|
3681
3967
|
ignoreExitCode: true,
|
|
3682
3968
|
silent: true
|
|
3683
3969
|
});
|
|
@@ -3685,7 +3971,7 @@ var service = {
|
|
|
3685
3971
|
},
|
|
3686
3972
|
async check(ssh2) {
|
|
3687
3973
|
if (!ssh2) return NEEDS_APPLY;
|
|
3688
|
-
const enabled = await ssh2.test(`${
|
|
3974
|
+
const enabled = await ssh2.test(`${SYSTEMCTL2} is-enabled --quiet ${shellQuote(name)}`);
|
|
3689
3975
|
return enabled ? "needs-apply" : "ok";
|
|
3690
3976
|
},
|
|
3691
3977
|
name: `service.disabled: ${name}`
|
|
@@ -3700,7 +3986,7 @@ var service = {
|
|
|
3700
3986
|
return {
|
|
3701
3987
|
async apply(ssh2) {
|
|
3702
3988
|
if (!ssh2) return failed(`[service.enabled: ${name}] SSH connection is required`);
|
|
3703
|
-
const result = await ssh2.exec(`${
|
|
3989
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} enable ${shellQuote(name)}`, {
|
|
3704
3990
|
ignoreExitCode: true,
|
|
3705
3991
|
silent: true
|
|
3706
3992
|
});
|
|
@@ -3708,7 +3994,7 @@ var service = {
|
|
|
3708
3994
|
},
|
|
3709
3995
|
async check(ssh2) {
|
|
3710
3996
|
if (!ssh2) return NEEDS_APPLY;
|
|
3711
|
-
return await ssh2.test(`${
|
|
3997
|
+
return await ssh2.test(`${SYSTEMCTL2} is-enabled --quiet ${shellQuote(name)}`) ? "ok" : NEEDS_APPLY;
|
|
3712
3998
|
},
|
|
3713
3999
|
name: `service.enabled: ${name}`
|
|
3714
4000
|
};
|
|
@@ -3724,7 +4010,7 @@ var service = {
|
|
|
3724
4010
|
async apply(ssh2) {
|
|
3725
4011
|
if (!ssh2) return failed("[service.facts] SSH connection is required");
|
|
3726
4012
|
const result = await ssh2.exec(
|
|
3727
|
-
`${
|
|
4013
|
+
`${SYSTEMCTL2} list-units --type=service --all --no-pager --no-legend`,
|
|
3728
4014
|
{ ignoreExitCode: true, silent: true }
|
|
3729
4015
|
);
|
|
3730
4016
|
if (result.code !== 0) {
|
|
@@ -3759,7 +4045,7 @@ var service = {
|
|
|
3759
4045
|
return {
|
|
3760
4046
|
async apply(ssh2) {
|
|
3761
4047
|
if (!ssh2) return failed(`[service.reload: ${name}] SSH connection is required`);
|
|
3762
|
-
const result = await ssh2.exec(`${
|
|
4048
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} reload ${shellQuote(name)}`, {
|
|
3763
4049
|
ignoreExitCode: true,
|
|
3764
4050
|
silent: true
|
|
3765
4051
|
});
|
|
@@ -3781,7 +4067,7 @@ var service = {
|
|
|
3781
4067
|
return {
|
|
3782
4068
|
async apply(ssh2) {
|
|
3783
4069
|
if (!ssh2) return failed(`[service.restart: ${name}] SSH connection is required`);
|
|
3784
|
-
const result = await ssh2.exec(`${
|
|
4070
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} restart ${shellQuote(name)}`, {
|
|
3785
4071
|
ignoreExitCode: true,
|
|
3786
4072
|
silent: true
|
|
3787
4073
|
});
|
|
@@ -3803,7 +4089,7 @@ var service = {
|
|
|
3803
4089
|
return {
|
|
3804
4090
|
async apply(ssh2) {
|
|
3805
4091
|
if (!ssh2) return failed(`[service.running: ${name}] SSH connection is required`);
|
|
3806
|
-
const result = await ssh2.exec(`${
|
|
4092
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} start ${shellQuote(name)}`, {
|
|
3807
4093
|
ignoreExitCode: true,
|
|
3808
4094
|
silent: true
|
|
3809
4095
|
});
|
|
@@ -3811,7 +4097,7 @@ var service = {
|
|
|
3811
4097
|
},
|
|
3812
4098
|
async check(ssh2) {
|
|
3813
4099
|
if (!ssh2) return NEEDS_APPLY;
|
|
3814
|
-
return await ssh2.test(`${
|
|
4100
|
+
return await ssh2.test(`${SYSTEMCTL2} is-active --quiet ${shellQuote(name)}`) ? "ok" : NEEDS_APPLY;
|
|
3815
4101
|
},
|
|
3816
4102
|
name: `service.running: ${name}`
|
|
3817
4103
|
};
|
|
@@ -3825,7 +4111,7 @@ var service = {
|
|
|
3825
4111
|
return {
|
|
3826
4112
|
async apply(ssh2) {
|
|
3827
4113
|
if (!ssh2) return failed(`[service.stopped: ${name}] SSH connection is required`);
|
|
3828
|
-
const result = await ssh2.exec(`${
|
|
4114
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} stop ${shellQuote(name)}`, {
|
|
3829
4115
|
ignoreExitCode: true,
|
|
3830
4116
|
silent: true
|
|
3831
4117
|
});
|
|
@@ -3833,7 +4119,7 @@ var service = {
|
|
|
3833
4119
|
},
|
|
3834
4120
|
async check(ssh2) {
|
|
3835
4121
|
if (!ssh2) return NEEDS_APPLY;
|
|
3836
|
-
const active = await ssh2.test(`${
|
|
4122
|
+
const active = await ssh2.test(`${SYSTEMCTL2} is-active --quiet ${shellQuote(name)}`);
|
|
3837
4123
|
return active ? "needs-apply" : "ok";
|
|
3838
4124
|
},
|
|
3839
4125
|
name: `service.stopped: ${name}`
|
|
@@ -4124,7 +4410,7 @@ var DEFAULT_SSH_PORT3 = 22;
|
|
|
4124
4410
|
var PRIVILEGE_SEPARATION_DIRECTORY = "/run/sshd";
|
|
4125
4411
|
var SSHD_CONFIG_PATH = "/etc/ssh/sshd_config";
|
|
4126
4412
|
var SSHD_CONFIG_MODE = "0644";
|
|
4127
|
-
var
|
|
4413
|
+
var SYSTEMCTL3 = "systemctl";
|
|
4128
4414
|
var REGEXP_SPECIAL = /* @__PURE__ */ new Set(["?", ".", "(", ")", "[", "]", "{", "}", "*", "\\", "^", "+", "|", "$"]);
|
|
4129
4415
|
function escapeRegExp(s) {
|
|
4130
4416
|
let result = "";
|
|
@@ -4162,14 +4448,14 @@ async function disableSocketActivatedSsh(ssh2) {
|
|
|
4162
4448
|
});
|
|
4163
4449
|
}
|
|
4164
4450
|
async function resolveSshServiceUnit(ssh2) {
|
|
4165
|
-
const sshdExists = await ssh2.exec(`${
|
|
4451
|
+
const sshdExists = await ssh2.exec(`${SYSTEMCTL3} cat sshd.service >/dev/null 2>&1`, {
|
|
4166
4452
|
ignoreExitCode: true,
|
|
4167
4453
|
silent: true
|
|
4168
4454
|
});
|
|
4169
4455
|
if (sshdExists.code === 0) {
|
|
4170
4456
|
return "sshd";
|
|
4171
4457
|
}
|
|
4172
|
-
const sshExists = await ssh2.exec(`${
|
|
4458
|
+
const sshExists = await ssh2.exec(`${SYSTEMCTL3} cat ssh.service >/dev/null 2>&1`, {
|
|
4173
4459
|
ignoreExitCode: true,
|
|
4174
4460
|
silent: true
|
|
4175
4461
|
});
|
|
@@ -4182,7 +4468,7 @@ async function resolveSshServiceUnit(ssh2) {
|
|
|
4182
4468
|
}
|
|
4183
4469
|
async function reloadSshd(ssh2) {
|
|
4184
4470
|
const serviceUnit = await resolveSshServiceUnit(ssh2);
|
|
4185
|
-
const result = await ssh2.exec(`${
|
|
4471
|
+
const result = await ssh2.exec(`${SYSTEMCTL3} reload ${serviceUnit}`, {
|
|
4186
4472
|
ignoreExitCode: true,
|
|
4187
4473
|
silent: true
|
|
4188
4474
|
});
|
|
@@ -4263,7 +4549,7 @@ async function applySshdPort(ssh2, targetPort) {
|
|
|
4263
4549
|
try {
|
|
4264
4550
|
await disableSocketActivatedSsh(ssh2);
|
|
4265
4551
|
const serviceUnit = await resolveSshServiceUnit(ssh2);
|
|
4266
|
-
await ssh2.exec(`${
|
|
4552
|
+
await ssh2.exec(`${SYSTEMCTL3} restart ${serviceUnit}`, { silent: true });
|
|
4267
4553
|
} catch (error) {
|
|
4268
4554
|
if (!isRestartDisconnect(error)) {
|
|
4269
4555
|
ssh2.removePort(targetPort);
|
|
@@ -4374,8 +4660,259 @@ var sshd = {
|
|
|
4374
4660
|
}
|
|
4375
4661
|
};
|
|
4376
4662
|
|
|
4377
|
-
// src/modules/
|
|
4663
|
+
// src/modules/swapFileHelpers.ts
|
|
4664
|
+
import { dirname } from "path";
|
|
4378
4665
|
var EXEC_OPTS8 = { ignoreExitCode: true, silent: true };
|
|
4666
|
+
var FSTAB_PATH2 = "/etc/fstab";
|
|
4667
|
+
var FSTAB_MODE2 = "0644";
|
|
4668
|
+
var KIBI = 1024;
|
|
4669
|
+
var POWER_0 = 0;
|
|
4670
|
+
var POWER_1 = 1;
|
|
4671
|
+
var POWER_2 = 2;
|
|
4672
|
+
var POWER_3 = 3;
|
|
4673
|
+
var POWER_4 = 4;
|
|
4674
|
+
var POWER_5 = 5;
|
|
4675
|
+
var SIZE_POWERS = {
|
|
4676
|
+
"": POWER_0,
|
|
4677
|
+
G: POWER_3,
|
|
4678
|
+
K: POWER_1,
|
|
4679
|
+
M: POWER_2,
|
|
4680
|
+
P: POWER_5,
|
|
4681
|
+
T: POWER_4
|
|
4682
|
+
};
|
|
4683
|
+
function isSizeUnit(unit) {
|
|
4684
|
+
return Object.hasOwn(SIZE_POWERS, unit);
|
|
4685
|
+
}
|
|
4686
|
+
function normalizeSizeToBytes(size) {
|
|
4687
|
+
if (typeof size === "number") {
|
|
4688
|
+
if (!Number.isInteger(size) || size <= 0) {
|
|
4689
|
+
throw new Error("swap.file: numeric size must be a positive integer byte count");
|
|
4690
|
+
}
|
|
4691
|
+
return size;
|
|
4692
|
+
}
|
|
4693
|
+
const trimmed = size.trim();
|
|
4694
|
+
const match = new RegExp("^(?<value>\\d+)(?<unit>[KMGTP]?)$", "iv").exec(trimmed);
|
|
4695
|
+
if (match?.groups == null) {
|
|
4696
|
+
throw new Error(`swap.file: unsupported size format "${size}"`);
|
|
4697
|
+
}
|
|
4698
|
+
const value = Number.parseInt(match.groups.value, 10);
|
|
4699
|
+
const unit = match.groups.unit.toUpperCase();
|
|
4700
|
+
if (!isSizeUnit(unit)) {
|
|
4701
|
+
throw new Error(`swap.file: unsupported size unit "${unit}"`);
|
|
4702
|
+
}
|
|
4703
|
+
return value * KIBI ** SIZE_POWERS[unit];
|
|
4704
|
+
}
|
|
4705
|
+
function normalizeSizeForCommand(size) {
|
|
4706
|
+
return typeof size === "number" ? String(size) : size.trim();
|
|
4707
|
+
}
|
|
4708
|
+
function buildSwapFstabLine(path, priority) {
|
|
4709
|
+
const options = priority == null ? "sw" : `sw,pri=${String(priority)}`;
|
|
4710
|
+
return `${path} none swap ${options} 0 0`;
|
|
4711
|
+
}
|
|
4712
|
+
function findFstabEntry2(fstabContent, path) {
|
|
4713
|
+
for (const line of fstabContent.split("\n")) {
|
|
4714
|
+
const trimmed = line.trim();
|
|
4715
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
4716
|
+
const fields = trimmed.split(new RegExp("\\s+", "v"));
|
|
4717
|
+
if (fields[0] === path) return trimmed;
|
|
4718
|
+
}
|
|
4719
|
+
return null;
|
|
4720
|
+
}
|
|
4721
|
+
function upsertFstabEntry2(fstabContent, path, newLine) {
|
|
4722
|
+
const lines = fstabContent.split("\n");
|
|
4723
|
+
const index = lines.findIndex((line) => {
|
|
4724
|
+
const trimmed = line.trim();
|
|
4725
|
+
if (trimmed === "" || trimmed.startsWith("#")) return false;
|
|
4726
|
+
const fields = trimmed.split(new RegExp("\\s+", "v"));
|
|
4727
|
+
return fields[0] === path;
|
|
4728
|
+
});
|
|
4729
|
+
if (index === -1) {
|
|
4730
|
+
while (lines.length > 0 && lines.at(-1)?.trim() === "") {
|
|
4731
|
+
lines.pop();
|
|
4732
|
+
}
|
|
4733
|
+
lines.push(newLine);
|
|
4734
|
+
} else {
|
|
4735
|
+
lines[index] = newLine;
|
|
4736
|
+
}
|
|
4737
|
+
return `${lines.join("\n")}
|
|
4738
|
+
`;
|
|
4739
|
+
}
|
|
4740
|
+
function removeFstabEntry2(fstabContent, path) {
|
|
4741
|
+
const lines = fstabContent.split("\n");
|
|
4742
|
+
const result = lines.filter((line) => {
|
|
4743
|
+
const trimmed = line.trim();
|
|
4744
|
+
if (trimmed === "" || trimmed.startsWith("#")) return true;
|
|
4745
|
+
const fields = trimmed.split(new RegExp("\\s+", "v"));
|
|
4746
|
+
return fields[0] !== path;
|
|
4747
|
+
});
|
|
4748
|
+
return `${result.join("\n")}
|
|
4749
|
+
`;
|
|
4750
|
+
}
|
|
4751
|
+
async function isSwapActive(ssh2, path) {
|
|
4752
|
+
const activeSwaps = await ssh2.lines("swapon --show=NAME --noheadings");
|
|
4753
|
+
return activeSwaps.some((line) => line.trim() === path);
|
|
4754
|
+
}
|
|
4755
|
+
async function hasSwapSignature(ssh2, path) {
|
|
4756
|
+
return ssh2.test(`swaplabel ${shellQuote(path)} >/dev/null 2>&1`);
|
|
4757
|
+
}
|
|
4758
|
+
async function readFileSizeInBytes(ssh2, path) {
|
|
4759
|
+
const output = await ssh2.output(`stat -c %s ${shellQuote(path)}`);
|
|
4760
|
+
return Number.parseInt(output.trim(), 10);
|
|
4761
|
+
}
|
|
4762
|
+
async function ensureSwapFstabState(parameters) {
|
|
4763
|
+
const fstabContent = await parameters.ssh.readFile(FSTAB_PATH2);
|
|
4764
|
+
const currentEntry = findFstabEntry2(fstabContent, parameters.path);
|
|
4765
|
+
if (parameters.desiredLine == null) {
|
|
4766
|
+
if (currentEntry == null) return false;
|
|
4767
|
+
const removedContent = removeFstabEntry2(fstabContent, parameters.path);
|
|
4768
|
+
await guardedWriteFile(parameters.ssh, {
|
|
4769
|
+
mode: FSTAB_MODE2,
|
|
4770
|
+
newContent: removedContent,
|
|
4771
|
+
originalContent: fstabContent,
|
|
4772
|
+
remotePath: FSTAB_PATH2
|
|
4773
|
+
});
|
|
4774
|
+
return true;
|
|
4775
|
+
}
|
|
4776
|
+
if (currentEntry === parameters.desiredLine) return false;
|
|
4777
|
+
const updatedContent = upsertFstabEntry2(fstabContent, parameters.path, parameters.desiredLine);
|
|
4778
|
+
await guardedWriteFile(parameters.ssh, {
|
|
4779
|
+
mode: FSTAB_MODE2,
|
|
4780
|
+
newContent: updatedContent,
|
|
4781
|
+
originalContent: fstabContent,
|
|
4782
|
+
remotePath: FSTAB_PATH2
|
|
4783
|
+
});
|
|
4784
|
+
return true;
|
|
4785
|
+
}
|
|
4786
|
+
async function ensureSwapFilePresent(parameters) {
|
|
4787
|
+
const createDirectoryResult = await parameters.ssh.exec(
|
|
4788
|
+
`mkdir -p ${shellQuote(dirname(parameters.path))}`,
|
|
4789
|
+
EXEC_OPTS8
|
|
4790
|
+
);
|
|
4791
|
+
if (createDirectoryResult.code !== 0) {
|
|
4792
|
+
return failedCommand(`[swap.file: ${parameters.path}] mkdir failed`, createDirectoryResult);
|
|
4793
|
+
}
|
|
4794
|
+
const createFileResult = await parameters.ssh.exec(
|
|
4795
|
+
`fallocate -l ${shellQuote(parameters.size)} ${shellQuote(parameters.path)} || dd if=/dev/zero of=${shellQuote(parameters.path)} bs=${shellQuote(parameters.size)} count=1 status=none`,
|
|
4796
|
+
EXEC_OPTS8
|
|
4797
|
+
);
|
|
4798
|
+
if (createFileResult.code !== 0) {
|
|
4799
|
+
return failedCommand(
|
|
4800
|
+
`[swap.file: ${parameters.path}] swap file creation failed`,
|
|
4801
|
+
createFileResult
|
|
4802
|
+
);
|
|
4803
|
+
}
|
|
4804
|
+
const chmodResult = await parameters.ssh.exec(
|
|
4805
|
+
`chmod ${shellQuote(parameters.mode)} ${shellQuote(parameters.path)}`,
|
|
4806
|
+
EXEC_OPTS8
|
|
4807
|
+
);
|
|
4808
|
+
if (chmodResult.code !== 0) {
|
|
4809
|
+
return failedCommand(`[swap.file: ${parameters.path}] chmod failed`, chmodResult);
|
|
4810
|
+
}
|
|
4811
|
+
const makeSwapResult = await parameters.ssh.exec(
|
|
4812
|
+
`mkswap ${shellQuote(parameters.path)}`,
|
|
4813
|
+
EXEC_OPTS8
|
|
4814
|
+
);
|
|
4815
|
+
return makeSwapResult.code === 0 ? true : failedCommand(`[swap.file: ${parameters.path}] mkswap failed`, makeSwapResult);
|
|
4816
|
+
}
|
|
4817
|
+
function normalizeSwapFileOptions(options) {
|
|
4818
|
+
const state = options.state ?? "present";
|
|
4819
|
+
return {
|
|
4820
|
+
expectedFstabLine: state === "present" ? buildSwapFstabLine(options.path, options.priority) : null,
|
|
4821
|
+
mode: options.mode ?? "0600",
|
|
4822
|
+
path: options.path,
|
|
4823
|
+
sizeBytes: normalizeSizeToBytes(options.size),
|
|
4824
|
+
sizeForCommand: normalizeSizeForCommand(options.size),
|
|
4825
|
+
state
|
|
4826
|
+
};
|
|
4827
|
+
}
|
|
4828
|
+
async function needsSwapRecreation(ssh2, options) {
|
|
4829
|
+
if (!await ssh2.exists(options.path)) return true;
|
|
4830
|
+
if (await readFileSizeInBytes(ssh2, options.path) !== options.sizeBytes) return true;
|
|
4831
|
+
return !await hasSwapSignature(ssh2, options.path);
|
|
4832
|
+
}
|
|
4833
|
+
async function hasSwapFstabEntry(ssh2, options) {
|
|
4834
|
+
const currentFstabContent = await ssh2.readFile(FSTAB_PATH2);
|
|
4835
|
+
return findFstabEntry2(currentFstabContent, options.path) === options.expectedFstabLine;
|
|
4836
|
+
}
|
|
4837
|
+
async function hasNoSwapFstabEntry(ssh2, path) {
|
|
4838
|
+
const currentFstabContent = await ssh2.readFile(FSTAB_PATH2);
|
|
4839
|
+
return findFstabEntry2(currentFstabContent, path) == null;
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4842
|
+
// src/modules/swapHelpers.ts
|
|
4843
|
+
var EXEC_OPTS9 = { ignoreExitCode: true, silent: true };
|
|
4844
|
+
async function disableSwap(ssh2, path) {
|
|
4845
|
+
if (!await isSwapActive(ssh2, path)) return false;
|
|
4846
|
+
const result = await ssh2.exec(`swapoff ${shellQuote(path)}`, EXEC_OPTS9);
|
|
4847
|
+
return result.code === 0 ? true : failedCommand(`[swap.file: ${path}] swapoff failed`, result);
|
|
4848
|
+
}
|
|
4849
|
+
async function removeSwapFile(ssh2, path) {
|
|
4850
|
+
if (!await ssh2.exists(path)) return false;
|
|
4851
|
+
const result = await ssh2.exec(`rm -f ${shellQuote(path)}`, EXEC_OPTS9);
|
|
4852
|
+
return result.code === 0 ? true : failedCommand(`[swap.file: ${path}] rm failed`, result);
|
|
4853
|
+
}
|
|
4854
|
+
async function recreateSwapFile(ssh2, options) {
|
|
4855
|
+
if (!await needsSwapRecreation(ssh2, options)) return "ok";
|
|
4856
|
+
const disableResult = await disableSwap(ssh2, options.path);
|
|
4857
|
+
if (typeof disableResult !== "boolean") return disableResult;
|
|
4858
|
+
const removeResult = await removeSwapFile(ssh2, options.path);
|
|
4859
|
+
if (typeof removeResult !== "boolean") return removeResult;
|
|
4860
|
+
const createResult = await ensureSwapFilePresent({
|
|
4861
|
+
mode: options.mode,
|
|
4862
|
+
path: options.path,
|
|
4863
|
+
size: options.sizeForCommand,
|
|
4864
|
+
ssh: ssh2
|
|
4865
|
+
});
|
|
4866
|
+
return createResult === true ? "changed" : createResult;
|
|
4867
|
+
}
|
|
4868
|
+
async function enableSwap(ssh2, path) {
|
|
4869
|
+
if (await isSwapActive(ssh2, path)) return false;
|
|
4870
|
+
const result = await ssh2.exec(`swapon ${shellQuote(path)}`, EXEC_OPTS9);
|
|
4871
|
+
return result.code === 0 ? true : failedCommand(`[swap.file: ${path}] swapon failed`, result);
|
|
4872
|
+
}
|
|
4873
|
+
async function applyAbsentSwapFile(ssh2, options) {
|
|
4874
|
+
let swapChanged = false;
|
|
4875
|
+
const disableResult = await disableSwap(ssh2, options.path);
|
|
4876
|
+
if (typeof disableResult !== "boolean") return disableResult;
|
|
4877
|
+
if (disableResult) swapChanged = true;
|
|
4878
|
+
if (await ensureSwapFstabState({ desiredLine: null, path: options.path, ssh: ssh2 })) swapChanged = true;
|
|
4879
|
+
const removeResult = await removeSwapFile(ssh2, options.path);
|
|
4880
|
+
if (typeof removeResult !== "boolean") return removeResult;
|
|
4881
|
+
if (removeResult) swapChanged = true;
|
|
4882
|
+
return { status: swapChanged ? "changed" : "ok" };
|
|
4883
|
+
}
|
|
4884
|
+
async function applyPresentSwapFile(ssh2, options) {
|
|
4885
|
+
let swapChanged = false;
|
|
4886
|
+
const recreateResult = await recreateSwapFile(ssh2, options);
|
|
4887
|
+
if (typeof recreateResult !== "string") return recreateResult;
|
|
4888
|
+
if (recreateResult === "changed") swapChanged = true;
|
|
4889
|
+
const enableResult = await enableSwap(ssh2, options.path);
|
|
4890
|
+
if (typeof enableResult !== "boolean") return enableResult;
|
|
4891
|
+
if (enableResult) swapChanged = true;
|
|
4892
|
+
if (await ensureSwapFstabState({ desiredLine: options.expectedFstabLine, path: options.path, ssh: ssh2 })) {
|
|
4893
|
+
swapChanged = true;
|
|
4894
|
+
}
|
|
4895
|
+
return { status: swapChanged ? "changed" : "ok" };
|
|
4896
|
+
}
|
|
4897
|
+
async function applySwapFile(ssh2, options) {
|
|
4898
|
+
return options.state === "absent" ? applyAbsentSwapFile(ssh2, options) : applyPresentSwapFile(ssh2, options);
|
|
4899
|
+
}
|
|
4900
|
+
async function checkAbsent(ssh2, options) {
|
|
4901
|
+
if (await ssh2.exists(options.path)) return NEEDS_APPLY;
|
|
4902
|
+
if (await isSwapActive(ssh2, options.path)) return NEEDS_APPLY;
|
|
4903
|
+
return await hasNoSwapFstabEntry(ssh2, options.path) ? "ok" : NEEDS_APPLY;
|
|
4904
|
+
}
|
|
4905
|
+
async function checkPresent(ssh2, options) {
|
|
4906
|
+
if (await needsSwapRecreation(ssh2, options)) return NEEDS_APPLY;
|
|
4907
|
+
if (!await isSwapActive(ssh2, options.path)) return NEEDS_APPLY;
|
|
4908
|
+
return await hasSwapFstabEntry(ssh2, options) ? "ok" : NEEDS_APPLY;
|
|
4909
|
+
}
|
|
4910
|
+
async function checkSwapFile(ssh2, options) {
|
|
4911
|
+
return options.state === "absent" ? checkAbsent(ssh2, options) : checkPresent(ssh2, options);
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
// src/modules/sysctl.ts
|
|
4915
|
+
var EXEC_OPTS10 = { ignoreExitCode: true, silent: true };
|
|
4379
4916
|
var SYSCTL_DIR = "/etc/sysctl.d";
|
|
4380
4917
|
var SYSCTL_CONFIG_MODE = "0644";
|
|
4381
4918
|
function sanitizeKey(key) {
|
|
@@ -4411,28 +4948,28 @@ var sysctl = {
|
|
|
4411
4948
|
if (!conn) return failed(`[sysctl.set: ${key}] SSH connection is required`);
|
|
4412
4949
|
if (state === "present") {
|
|
4413
4950
|
const assignment = `${key}=${value}`;
|
|
4414
|
-
const result = await conn.exec(`sysctl -w ${shellQuote(assignment)}`,
|
|
4951
|
+
const result = await conn.exec(`sysctl -w ${shellQuote(assignment)}`, EXEC_OPTS10);
|
|
4415
4952
|
if (result.code !== 0) {
|
|
4416
4953
|
return failedCommand(`[sysctl.set: ${key}] sysctl -w failed`, result);
|
|
4417
4954
|
}
|
|
4418
4955
|
await conn.writeFile(configPath, expectedContent, { mode: SYSCTL_CONFIG_MODE });
|
|
4419
4956
|
} else {
|
|
4420
|
-
await conn.exec(`rm -f ${shellQuote(configPath)}`,
|
|
4957
|
+
await conn.exec(`rm -f ${shellQuote(configPath)}`, EXEC_OPTS10);
|
|
4421
4958
|
}
|
|
4422
4959
|
return { status: "changed" };
|
|
4423
4960
|
},
|
|
4424
4961
|
async check(conn) {
|
|
4425
4962
|
if (!conn) return NEEDS_APPLY;
|
|
4426
4963
|
if (state === "present") {
|
|
4427
|
-
const result = await conn.exec(`sysctl -n ${shellQuote(key)}`,
|
|
4964
|
+
const result = await conn.exec(`sysctl -n ${shellQuote(key)}`, EXEC_OPTS10);
|
|
4428
4965
|
const currentValue = result.stdout.trim();
|
|
4429
4966
|
if (currentValue !== value) return NEEDS_APPLY;
|
|
4430
|
-
const configExists = await conn.exec(`test -f ${shellQuote(configPath)}`,
|
|
4967
|
+
const configExists = await conn.exec(`test -f ${shellQuote(configPath)}`, EXEC_OPTS10);
|
|
4431
4968
|
if (configExists.code !== 0) return NEEDS_APPLY;
|
|
4432
4969
|
const fileContent = await conn.readFile(configPath);
|
|
4433
4970
|
return fileContent.trim() === expectedContent.trim() ? "ok" : NEEDS_APPLY;
|
|
4434
4971
|
}
|
|
4435
|
-
const fileExists = await conn.exec(`test -f ${shellQuote(configPath)}`,
|
|
4972
|
+
const fileExists = await conn.exec(`test -f ${shellQuote(configPath)}`, EXEC_OPTS10);
|
|
4436
4973
|
return fileExists.code === 0 ? NEEDS_APPLY : "ok";
|
|
4437
4974
|
},
|
|
4438
4975
|
name: state === "present" ? `sysctl.set: ${key}=${value}` : `sysctl.set: absent ${key}`
|
|
@@ -4440,6 +4977,53 @@ var sysctl = {
|
|
|
4440
4977
|
}
|
|
4441
4978
|
};
|
|
4442
4979
|
|
|
4980
|
+
// src/modules/swap.ts
|
|
4981
|
+
var swap = {
|
|
4982
|
+
/**
|
|
4983
|
+
* Ensure a file-backed swap area exists, is activated, and is persisted in `/etc/fstab`.
|
|
4984
|
+
*
|
|
4985
|
+
* @param options - Configuration for the swap file.
|
|
4986
|
+
* @param options.mode - File mode applied to the swap file. Defaults to `0600`.
|
|
4987
|
+
* @param options.path - Absolute path to the swap file, e.g. `/swapfile`.
|
|
4988
|
+
* @param options.priority - Optional swap priority written into the fstab entry.
|
|
4989
|
+
* @param options.size - Desired file size as bytes or a shell-friendly size string such as `2G`.
|
|
4990
|
+
* @param options.state - Whether the swap file should be `present` (default) or `absent`.
|
|
4991
|
+
* @returns A Module that manages the swap file lifecycle.
|
|
4992
|
+
*/
|
|
4993
|
+
file(options) {
|
|
4994
|
+
const normalized = normalizeSwapFileOptions(options);
|
|
4995
|
+
return {
|
|
4996
|
+
async apply(ssh2) {
|
|
4997
|
+
if (!ssh2) return failed(`[swap.file: ${normalized.path}] SSH connection is required`);
|
|
4998
|
+
return applySwapFile(ssh2, normalized);
|
|
4999
|
+
},
|
|
5000
|
+
async check(ssh2) {
|
|
5001
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
5002
|
+
return checkSwapFile(ssh2, normalized);
|
|
5003
|
+
},
|
|
5004
|
+
name: normalized.state === "present" ? `swap.file: ${normalized.path} (${normalized.sizeForCommand})` : `swap.file: absent ${normalized.path}`
|
|
5005
|
+
};
|
|
5006
|
+
},
|
|
5007
|
+
/**
|
|
5008
|
+
* Persist `vm.swappiness`.
|
|
5009
|
+
*
|
|
5010
|
+
* @param value - Desired swappiness value.
|
|
5011
|
+
* @returns A Module that manages `vm.swappiness`.
|
|
5012
|
+
*/
|
|
5013
|
+
swappiness(value) {
|
|
5014
|
+
return sysctl.set("vm.swappiness", String(value));
|
|
5015
|
+
},
|
|
5016
|
+
/**
|
|
5017
|
+
* Persist `vm.vfs_cache_pressure`.
|
|
5018
|
+
*
|
|
5019
|
+
* @param value - Desired VFS cache pressure value.
|
|
5020
|
+
* @returns A Module that manages `vm.vfs_cache_pressure`.
|
|
5021
|
+
*/
|
|
5022
|
+
vfsCachePressure(value) {
|
|
5023
|
+
return sysctl.set("vm.vfs_cache_pressure", String(value));
|
|
5024
|
+
}
|
|
5025
|
+
};
|
|
5026
|
+
|
|
4443
5027
|
// src/modules/system.ts
|
|
4444
5028
|
function parseOsRelease(content) {
|
|
4445
5029
|
const result = {};
|
|
@@ -4610,8 +5194,8 @@ ${message}`);
|
|
|
4610
5194
|
};
|
|
4611
5195
|
|
|
4612
5196
|
// src/modules/systemd.ts
|
|
4613
|
-
var
|
|
4614
|
-
var
|
|
5197
|
+
var SYSTEMCTL4 = "systemctl";
|
|
5198
|
+
var UNIT_NAME_PATTERN3 = new RegExp("^[\\w@.\\-]+$", "v");
|
|
4615
5199
|
var SYSTEMD_UNIT_MODE2 = "0644";
|
|
4616
5200
|
var systemd = {
|
|
4617
5201
|
/**
|
|
@@ -4623,7 +5207,7 @@ var systemd = {
|
|
|
4623
5207
|
return {
|
|
4624
5208
|
async apply(ssh2) {
|
|
4625
5209
|
if (!ssh2) return failed("[systemd.daemonReload] SSH connection is required");
|
|
4626
|
-
const result = await ssh2.exec(`${
|
|
5210
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} daemon-reload`, {
|
|
4627
5211
|
ignoreExitCode: true,
|
|
4628
5212
|
silent: true
|
|
4629
5213
|
});
|
|
@@ -4645,7 +5229,7 @@ var systemd = {
|
|
|
4645
5229
|
return {
|
|
4646
5230
|
async apply(ssh2) {
|
|
4647
5231
|
if (!ssh2) return failed(`[systemd.masked: ${name}] SSH connection is required`);
|
|
4648
|
-
const result = await ssh2.exec(`${
|
|
5232
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} mask ${shellQuote(name)}`, {
|
|
4649
5233
|
ignoreExitCode: true,
|
|
4650
5234
|
silent: true
|
|
4651
5235
|
});
|
|
@@ -4653,7 +5237,7 @@ var systemd = {
|
|
|
4653
5237
|
},
|
|
4654
5238
|
async check(ssh2) {
|
|
4655
5239
|
if (!ssh2) return NEEDS_APPLY;
|
|
4656
|
-
const result = await ssh2.exec(`${
|
|
5240
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} is-enabled ${shellQuote(name)}`, {
|
|
4657
5241
|
ignoreExitCode: true,
|
|
4658
5242
|
silent: true
|
|
4659
5243
|
});
|
|
@@ -4674,15 +5258,15 @@ var systemd = {
|
|
|
4674
5258
|
* @returns A Module that ensures the unit file is present with the given content.
|
|
4675
5259
|
*/
|
|
4676
5260
|
unit(name, content) {
|
|
4677
|
-
if (!
|
|
4678
|
-
throw new Error(`systemd.unit: name must match ${String(
|
|
5261
|
+
if (!UNIT_NAME_PATTERN3.test(name)) {
|
|
5262
|
+
throw new Error(`systemd.unit: name must match ${String(UNIT_NAME_PATTERN3)}, got: ${name}`);
|
|
4679
5263
|
}
|
|
4680
5264
|
const filePath = `/etc/systemd/system/${name}`;
|
|
4681
5265
|
return {
|
|
4682
5266
|
async apply(ssh2) {
|
|
4683
5267
|
if (!ssh2) return failed(`[systemd.unit: ${name}] SSH connection is required`);
|
|
4684
5268
|
await ssh2.writeFile(filePath, content, { mode: SYSTEMD_UNIT_MODE2 });
|
|
4685
|
-
const result = await ssh2.exec(`${
|
|
5269
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} daemon-reload`, {
|
|
4686
5270
|
ignoreExitCode: true,
|
|
4687
5271
|
silent: true
|
|
4688
5272
|
});
|
|
@@ -4707,7 +5291,7 @@ var systemd = {
|
|
|
4707
5291
|
return {
|
|
4708
5292
|
async apply(ssh2) {
|
|
4709
5293
|
if (!ssh2) return failed(`[systemd.unmasked: ${name}] SSH connection is required`);
|
|
4710
|
-
const result = await ssh2.exec(`${
|
|
5294
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} unmask ${shellQuote(name)}`, {
|
|
4711
5295
|
ignoreExitCode: true,
|
|
4712
5296
|
silent: true
|
|
4713
5297
|
});
|
|
@@ -4715,7 +5299,7 @@ var systemd = {
|
|
|
4715
5299
|
},
|
|
4716
5300
|
async check(ssh2) {
|
|
4717
5301
|
if (!ssh2) return NEEDS_APPLY;
|
|
4718
|
-
const result = await ssh2.exec(`${
|
|
5302
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} is-enabled ${shellQuote(name)}`, {
|
|
4719
5303
|
ignoreExitCode: true,
|
|
4720
5304
|
silent: true
|
|
4721
5305
|
});
|
|
@@ -4729,6 +5313,37 @@ var systemd = {
|
|
|
4729
5313
|
// src/modules/ufw.ts
|
|
4730
5314
|
var UFW = "ufw";
|
|
4731
5315
|
var ufw = {
|
|
5316
|
+
/**
|
|
5317
|
+
* Ensure UFW is inactive. If UFW is not installed, this is treated as already satisfied.
|
|
5318
|
+
*
|
|
5319
|
+
* @returns A Module that ensures UFW is disabled.
|
|
5320
|
+
*/
|
|
5321
|
+
disabled() {
|
|
5322
|
+
return {
|
|
5323
|
+
async apply(ssh2) {
|
|
5324
|
+
if (!ssh2) return failed("[ufw.disabled] SSH connection is required");
|
|
5325
|
+
const pm = await detectPackageManager(ssh2);
|
|
5326
|
+
if (pm == null || !await isPackageInstalled(ssh2, pm, UFW)) {
|
|
5327
|
+
return { status: "ok" };
|
|
5328
|
+
}
|
|
5329
|
+
const result = await ssh2.exec(`${UFW} --force disable`, {
|
|
5330
|
+
ignoreExitCode: true,
|
|
5331
|
+
silent: true
|
|
5332
|
+
});
|
|
5333
|
+
return result.code === 0 ? { status: "changed" } : failedCommand("[ufw.disabled] ufw disable failed", result);
|
|
5334
|
+
},
|
|
5335
|
+
async check(ssh2) {
|
|
5336
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
5337
|
+
const pm = await detectPackageManager(ssh2);
|
|
5338
|
+
if (pm == null || !await isPackageInstalled(ssh2, pm, UFW)) {
|
|
5339
|
+
return "ok";
|
|
5340
|
+
}
|
|
5341
|
+
const status = await ssh2.output(`${UFW} status`);
|
|
5342
|
+
return status.includes("Status: inactive") ? "ok" : NEEDS_APPLY;
|
|
5343
|
+
},
|
|
5344
|
+
name: "ufw.disabled"
|
|
5345
|
+
};
|
|
5346
|
+
},
|
|
4732
5347
|
/**
|
|
4733
5348
|
* Ensure UFW is active. Enables the firewall non-interactively if not already running.
|
|
4734
5349
|
*
|
|
@@ -4932,6 +5547,9 @@ export {
|
|
|
4932
5547
|
failed,
|
|
4933
5548
|
failedCommand,
|
|
4934
5549
|
NEEDS_APPLY,
|
|
5550
|
+
detectPackageManager,
|
|
5551
|
+
isPackageInstalled,
|
|
5552
|
+
pkg,
|
|
4935
5553
|
apt,
|
|
4936
5554
|
archive,
|
|
4937
5555
|
command,
|
|
@@ -4945,7 +5563,7 @@ export {
|
|
|
4945
5563
|
mount,
|
|
4946
5564
|
net,
|
|
4947
5565
|
op,
|
|
4948
|
-
|
|
5566
|
+
quadlet,
|
|
4949
5567
|
releaseUpgrade,
|
|
4950
5568
|
rsync,
|
|
4951
5569
|
script,
|
|
@@ -4953,9 +5571,10 @@ export {
|
|
|
4953
5571
|
ssh,
|
|
4954
5572
|
sshd,
|
|
4955
5573
|
sysctl,
|
|
5574
|
+
swap,
|
|
4956
5575
|
system,
|
|
4957
5576
|
systemd,
|
|
4958
5577
|
ufw,
|
|
4959
5578
|
user
|
|
4960
5579
|
};
|
|
4961
|
-
//# sourceMappingURL=chunk-
|
|
5580
|
+
//# sourceMappingURL=chunk-LI47NIKN.js.map
|