paratix 0.16.0 → 0.17.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-56SN6NQH.js → chunk-NXU7K6HY.js} +124 -46
- package/dist/cli.js +2 -2
- package/dist/{index-N3n0FmKh.d.ts → index-CGYwJDTf.d.ts} +24 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +1 -1
- package/llm-guide.md +60 -13
- package/package.json +1 -1
|
@@ -104,6 +104,7 @@ function shellQuote(s) {
|
|
|
104
104
|
return `'${s.replaceAll("'", "'\\''")}'`;
|
|
105
105
|
}
|
|
106
106
|
var DEFAULT_MAX_OUTPUT_BYTES = Number("1048576");
|
|
107
|
+
var CAPTURE_TRUNCATION_MARKER = "\n[output truncated]";
|
|
107
108
|
var CommandError = class _CommandError extends Error {
|
|
108
109
|
/** Full, untruncated standard error of the failed command. */
|
|
109
110
|
fullStderr;
|
|
@@ -1367,6 +1368,17 @@ async function setVersionedFlag(ssh2, flagName, flagPrefix) {
|
|
|
1367
1368
|
if (result.code === 0) return null;
|
|
1368
1369
|
return failedCommand(`[moduleHelpers] failed to persist versioned flag ${flagName}`, result);
|
|
1369
1370
|
}
|
|
1371
|
+
async function setFlag(ssh2, flagName) {
|
|
1372
|
+
validateFlagName(flagName, "flagName");
|
|
1373
|
+
const ensureFailure = await ensureFlagsDirectory(ssh2);
|
|
1374
|
+
if (ensureFailure) return ensureFailure;
|
|
1375
|
+
const result = await ssh2.exec(`touch ${FLAGS_DIRECTORY}/${shellQuote(flagName)}`, {
|
|
1376
|
+
ignoreExitCode: true,
|
|
1377
|
+
silent: true
|
|
1378
|
+
});
|
|
1379
|
+
if (result.code === 0) return null;
|
|
1380
|
+
return failedCommand(`[moduleHelpers] failed to persist flag ${flagName}`, result);
|
|
1381
|
+
}
|
|
1370
1382
|
async function applyWithFlagLock(ssh2, parameters) {
|
|
1371
1383
|
validateFlagName(parameters.flagName, "flagName");
|
|
1372
1384
|
const lockName = flagLockName(parameters.flagName);
|
|
@@ -1473,6 +1485,19 @@ async function tryApplyWithFlagLock(ssh2, parameters) {
|
|
|
1473
1485
|
return tryApplyWithFlagLock(ssh2, parameters);
|
|
1474
1486
|
}
|
|
1475
1487
|
|
|
1488
|
+
// src/modules/packageUpgradeSummary.ts
|
|
1489
|
+
var APT_UPGRADE_SUMMARY_PATTERN = new RegExp("^(?<upgraded>\\d+) upgraded, ", "mv");
|
|
1490
|
+
var UNKNOWN_UPGRADE_OUTCOME_DETAIL = "upgrade completed, package count unavailable";
|
|
1491
|
+
function describeAptUpgradeOutcome(stdout) {
|
|
1492
|
+
if (stdout.endsWith(CAPTURE_TRUNCATION_MARKER)) return UNKNOWN_UPGRADE_OUTCOME_DETAIL;
|
|
1493
|
+
const captured = APT_UPGRADE_SUMMARY_PATTERN.exec(stdout)?.groups?.upgraded;
|
|
1494
|
+
if (captured == null || captured === "") return UNKNOWN_UPGRADE_OUTCOME_DETAIL;
|
|
1495
|
+
const upgraded = Number(captured);
|
|
1496
|
+
if (!Number.isSafeInteger(upgraded)) return UNKNOWN_UPGRADE_OUTCOME_DETAIL;
|
|
1497
|
+
if (upgraded === 0) return "no packages upgraded";
|
|
1498
|
+
return upgraded === 1 ? "1 package upgraded" : `${upgraded} packages upgraded`;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1476
1501
|
// src/modules/remoteFileChecks.ts
|
|
1477
1502
|
import { posix as posix3 } from "path";
|
|
1478
1503
|
async function isRegularFileWithoutSymlink(ssh2, remotePath) {
|
|
@@ -1945,6 +1970,12 @@ var apt = {
|
|
|
1945
1970
|
* The pipeline is split into three separate SSH commands so that each step
|
|
1946
1971
|
* gets its own timeout window and produces a precise failure label.
|
|
1947
1972
|
*
|
|
1973
|
+
* The marker is written with `setFlag`: the flag name carries only the
|
|
1974
|
+
* date and no call-site identity, so an evicting prefix would be
|
|
1975
|
+
* host-global and two `apt.distUpgrade` calls with different dates would
|
|
1976
|
+
* delete each other's marker on every run. Retired markers are therefore
|
|
1977
|
+
* not pruned, and re-using an earlier date is skipped rather than re-run.
|
|
1978
|
+
*
|
|
1948
1979
|
* @param date - A date string used as the idempotency key (e.g. `"2024-01-15"`).
|
|
1949
1980
|
* @param options - Optional per-call overrides (e.g. SSH command `timeout`).
|
|
1950
1981
|
* The same `timeout` is applied to every step of the dist-upgrade pipeline.
|
|
@@ -1977,9 +2008,9 @@ var apt = {
|
|
|
1977
2008
|
);
|
|
1978
2009
|
if (upgrade.code !== 0)
|
|
1979
2010
|
return failedCommand("[apt.distUpgrade] apt-get dist-upgrade failed", upgrade);
|
|
1980
|
-
const flagFailure = await
|
|
2011
|
+
const flagFailure = await setFlag(ssh2, flagName);
|
|
1981
2012
|
if (flagFailure) return flagFailure;
|
|
1982
|
-
return { status: "changed" };
|
|
2013
|
+
return { detail: describeAptUpgradeOutcome(upgrade.stdout), status: "changed" };
|
|
1983
2014
|
},
|
|
1984
2015
|
flagName
|
|
1985
2016
|
});
|
|
@@ -2519,6 +2550,13 @@ async function validateExistingExtractDestination(conn, parameters) {
|
|
|
2519
2550
|
// src/modules/fileMetadataHelpers.ts
|
|
2520
2551
|
var EXEC_OPTS2 = { ignoreExitCode: true, silent: true };
|
|
2521
2552
|
var DEFAULT_FILE_WRITE_MODE = "0644";
|
|
2553
|
+
var EMPTY_OWNERSHIP = {
|
|
2554
|
+
group: "",
|
|
2555
|
+
groupId: "",
|
|
2556
|
+
mode: "",
|
|
2557
|
+
owner: "",
|
|
2558
|
+
ownerId: ""
|
|
2559
|
+
};
|
|
2522
2560
|
function assertValidChownOwnershipSpec(ownerSpec) {
|
|
2523
2561
|
if (ownerSpec === "") {
|
|
2524
2562
|
throw new Error("chown ownership spec must not be empty");
|
|
@@ -2581,20 +2619,23 @@ function renderGuardedMetadataCommand(kind, remotePath, value) {
|
|
|
2581
2619
|
].join("\n");
|
|
2582
2620
|
}
|
|
2583
2621
|
async function readOwnership(ssh2, remotePath) {
|
|
2584
|
-
const result = await ssh2.exec(`stat -c '%a %U %G' ${shellQuote(remotePath)}`, EXEC_OPTS2);
|
|
2585
|
-
if (result.code !== 0) return {
|
|
2586
|
-
const [mode = "", owner = "", group2 = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
2587
|
-
return { group: group2, mode, owner };
|
|
2622
|
+
const result = await ssh2.exec(`stat -c '%a %U %G %u %g' ${shellQuote(remotePath)}`, EXEC_OPTS2);
|
|
2623
|
+
if (result.code !== 0) return { ...EMPTY_OWNERSHIP };
|
|
2624
|
+
const [mode = "", owner = "", group2 = "", ownerId = "", groupId = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
2625
|
+
return { group: group2, groupId, mode, owner, ownerId };
|
|
2588
2626
|
}
|
|
2589
2627
|
function normalizeMode(mode) {
|
|
2590
2628
|
return mode.startsWith("0") ? mode : `0${mode}`;
|
|
2591
2629
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2630
|
+
var NUMERIC_ID_PATTERN = new RegExp("^\\d+$", "v");
|
|
2631
|
+
function isNumericId(value) {
|
|
2632
|
+
return NUMERIC_ID_PATTERN.test(value);
|
|
2594
2633
|
}
|
|
2595
|
-
function
|
|
2596
|
-
if (
|
|
2597
|
-
|
|
2634
|
+
function ownershipComponentMatches(expected, actual, actualId) {
|
|
2635
|
+
if (expected === "") return true;
|
|
2636
|
+
if (actual === expected || actualId === expected) return true;
|
|
2637
|
+
if (!isNumericId(expected) || !isNumericId(actualId)) return false;
|
|
2638
|
+
return Number(expected) === Number(actualId);
|
|
2598
2639
|
}
|
|
2599
2640
|
async function resolveWriteMode(ssh2, remotePath, explicitMode) {
|
|
2600
2641
|
if (explicitMode != null) return explicitMode;
|
|
@@ -2635,10 +2676,10 @@ function ownershipMatches(current, options) {
|
|
|
2635
2676
|
if (options?.mode != null && current.mode !== options.mode.replace(new RegExp("^0+", "v"), "")) return false;
|
|
2636
2677
|
if (options?.owner == null) return true;
|
|
2637
2678
|
const expectsGroup = options.owner.includes(":");
|
|
2638
|
-
const [expectedOwner, expectedGroup = ""] = options.owner.split(":", 2);
|
|
2639
|
-
if (!ownershipComponentMatches(expectedOwner, current.owner)) return false;
|
|
2640
|
-
if (!
|
|
2641
|
-
return
|
|
2679
|
+
const [expectedOwner = "", expectedGroup = ""] = options.owner.split(":", 2);
|
|
2680
|
+
if (!ownershipComponentMatches(expectedOwner, current.owner, current.ownerId)) return false;
|
|
2681
|
+
if (!expectsGroup) return true;
|
|
2682
|
+
return ownershipComponentMatches(expectedGroup, current.group, current.groupId);
|
|
2642
2683
|
}
|
|
2643
2684
|
function createMetadataModule(kind, remotePath, value) {
|
|
2644
2685
|
const name = `file.${kind}: ${remotePath}`;
|
|
@@ -2676,6 +2717,7 @@ var SILENT = { silent: true };
|
|
|
2676
2717
|
var FLAGS_DIR = "/var/lib/paratix/flags";
|
|
2677
2718
|
var ARCHIVE_MARKER_MODE = "0644";
|
|
2678
2719
|
var ARCHIVE_OWNER_MEMBER_CONCURRENCY = 8;
|
|
2720
|
+
var ARCHIVE_STAT_OWNERSHIP_FIELDS = 4;
|
|
2679
2721
|
async function mapWithConcurrencyLimit2(items, limit, mapper) {
|
|
2680
2722
|
if (items.length === 0) return [];
|
|
2681
2723
|
const results = [];
|
|
@@ -3055,11 +3097,9 @@ async function applyExtract(conn, parameters) {
|
|
|
3055
3097
|
}
|
|
3056
3098
|
}
|
|
3057
3099
|
function ownerMatchesStat(stdout, owner) {
|
|
3058
|
-
const [actualUser = "", actualGroup = ""] = stdout.trim().split(new RegExp("\\s+", "v"),
|
|
3100
|
+
const [actualUser = "", actualGroup = "", actualUserId = "", actualGroupId = ""] = stdout.trim().split(new RegExp("\\s+", "v"), ARCHIVE_STAT_OWNERSHIP_FIELDS);
|
|
3059
3101
|
const [expectedUser = "", expectedGroup = ""] = owner.split(":", 2);
|
|
3060
|
-
|
|
3061
|
-
if (expectedGroup !== "" && actualGroup !== expectedGroup) return false;
|
|
3062
|
-
return true;
|
|
3102
|
+
return ownershipComponentMatches(expectedUser, actualUser, actualUserId) && ownershipComponentMatches(expectedGroup, actualGroup, actualGroupId);
|
|
3063
3103
|
}
|
|
3064
3104
|
function isStringArray(value) {
|
|
3065
3105
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
@@ -3071,7 +3111,7 @@ async function extractedMemberOwnerMatches(conn, parameters) {
|
|
|
3071
3111
|
EXEC_OPTS3
|
|
3072
3112
|
);
|
|
3073
3113
|
if (exists.code !== 0) return false;
|
|
3074
|
-
const stat5 = await conn.exec(`stat -c '%U %G' -- ${shellQuote(path3)}`, EXEC_OPTS3);
|
|
3114
|
+
const stat5 = await conn.exec(`stat -c '%U %G %u %g' -- ${shellQuote(path3)}`, EXEC_OPTS3);
|
|
3075
3115
|
if (stat5.code !== 0) return false;
|
|
3076
3116
|
return ownerMatchesStat(stat5.stdout, owner);
|
|
3077
3117
|
}
|
|
@@ -5354,18 +5394,22 @@ function rejectSensitiveHeadersOverHttp(parameters) {
|
|
|
5354
5394
|
);
|
|
5355
5395
|
}
|
|
5356
5396
|
async function readDownloadOwnership(conn, destination) {
|
|
5357
|
-
const result = await conn.exec(`stat -c '%a %U %G' ${shellQuote(destination)}`, {
|
|
5397
|
+
const result = await conn.exec(`stat -c '%a %U %G %u %g' ${shellQuote(destination)}`, {
|
|
5358
5398
|
ignoreExitCode: true,
|
|
5359
5399
|
silent: true
|
|
5360
5400
|
});
|
|
5361
|
-
if (result.code !== 0) return { group: "", mode: "", owner: "" };
|
|
5362
|
-
const [mode = "", owner = "", group2 = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
5363
|
-
return { group: group2, mode, owner };
|
|
5401
|
+
if (result.code !== 0) return { group: "", groupId: "", mode: "", owner: "", ownerId: "" };
|
|
5402
|
+
const [mode = "", owner = "", group2 = "", ownerId = "", groupId = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
5403
|
+
return { group: group2, groupId, mode, owner, ownerId };
|
|
5404
|
+
}
|
|
5405
|
+
function downloadComponentMatches(expected, actual, actualId) {
|
|
5406
|
+
if (expected == null) return true;
|
|
5407
|
+
return ownershipComponentMatches(expected, actual, actualId);
|
|
5364
5408
|
}
|
|
5365
5409
|
function downloadOwnershipMatches(current, options) {
|
|
5366
5410
|
if (options.mode != null && current.mode !== options.mode.replace(new RegExp("^0+", "v"), "")) return false;
|
|
5367
|
-
if (options.owner
|
|
5368
|
-
if (options.group
|
|
5411
|
+
if (!downloadComponentMatches(options.owner, current.owner, current.ownerId)) return false;
|
|
5412
|
+
if (!downloadComponentMatches(options.group, current.group, current.groupId)) return false;
|
|
5369
5413
|
return true;
|
|
5370
5414
|
}
|
|
5371
5415
|
async function metadataMatches(conn, destination, options) {
|
|
@@ -5564,7 +5608,7 @@ function downloadModeDrifted(current, options) {
|
|
|
5564
5608
|
return options.mode != null && current.mode !== options.mode.replace(new RegExp("^0+", "v"), "");
|
|
5565
5609
|
}
|
|
5566
5610
|
function downloadOwnerDrifted(current, options) {
|
|
5567
|
-
return options.owner
|
|
5611
|
+
return !downloadComponentMatches(options.owner, current.owner, current.ownerId) || !downloadComponentMatches(options.group, current.group, current.groupId);
|
|
5568
5612
|
}
|
|
5569
5613
|
async function healModeDrift(conn, parameters, current) {
|
|
5570
5614
|
if (parameters.mode == null || !downloadModeDrifted(current, parameters)) {
|
|
@@ -6922,18 +6966,27 @@ ${endMarker}`
|
|
|
6922
6966
|
};
|
|
6923
6967
|
}
|
|
6924
6968
|
async function readPropertiesState(ssh2, remotePath) {
|
|
6925
|
-
const result = await ssh2.exec(`stat -c '%a %U %G' ${shellQuote(remotePath)}`, EXEC_OPTS6);
|
|
6926
|
-
if (result.code !== 0) return { group: "", mode: "", owner: "" };
|
|
6927
|
-
const [mode = "", owner = "", group2 = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
6928
|
-
return { group: group2, mode, owner };
|
|
6969
|
+
const result = await ssh2.exec(`stat -c '%a %U %G %u %g' ${shellQuote(remotePath)}`, EXEC_OPTS6);
|
|
6970
|
+
if (result.code !== 0) return { group: "", groupId: "", mode: "", owner: "", ownerId: "" };
|
|
6971
|
+
const [mode = "", owner = "", group2 = "", ownerId = "", groupId = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
6972
|
+
return { group: group2, groupId, mode, owner, ownerId };
|
|
6929
6973
|
}
|
|
6930
6974
|
function modeMatches(current, desired) {
|
|
6931
6975
|
return current === desired.replace(new RegExp("^0+", "v"), "");
|
|
6932
6976
|
}
|
|
6977
|
+
function assertValidOwnershipComponent(value, kind) {
|
|
6978
|
+
if (isNumericId(value)) return;
|
|
6979
|
+
if (kind === "user") assertValidUserName(value);
|
|
6980
|
+
else assertValidGroupName(value);
|
|
6981
|
+
}
|
|
6982
|
+
function propertiesComponentMatches(expected, actual, actualId) {
|
|
6983
|
+
if (expected == null) return true;
|
|
6984
|
+
return ownershipComponentMatches(expected, actual, actualId);
|
|
6985
|
+
}
|
|
6933
6986
|
function assertValidPropertiesOptions(options) {
|
|
6934
6987
|
if (options.mode != null) validateMode(options.mode);
|
|
6935
|
-
if (options.owner != null)
|
|
6936
|
-
if (options.group != null)
|
|
6988
|
+
if (options.owner != null) assertValidOwnershipComponent(options.owner, "user");
|
|
6989
|
+
if (options.group != null) assertValidOwnershipComponent(options.group, "group");
|
|
6937
6990
|
}
|
|
6938
6991
|
function isDriftFailure(result) {
|
|
6939
6992
|
return typeof result !== "boolean";
|
|
@@ -6949,8 +7002,8 @@ async function applyModeDrift(context) {
|
|
|
6949
7002
|
}
|
|
6950
7003
|
async function maybeApplyCombinedChown(context) {
|
|
6951
7004
|
const { current, options, remotePath, ssh: ssh2 } = context;
|
|
6952
|
-
const ownerNeedsUpdate = options.owner != null && current.owner
|
|
6953
|
-
const groupNeedsUpdate = options.group != null && current.group
|
|
7005
|
+
const ownerNeedsUpdate = options.owner != null && !propertiesComponentMatches(options.owner, current.owner, current.ownerId);
|
|
7006
|
+
const groupNeedsUpdate = options.group != null && !propertiesComponentMatches(options.group, current.group, current.groupId);
|
|
6954
7007
|
if (!ownerNeedsUpdate || !groupNeedsUpdate || options.owner == null || options.group == null) {
|
|
6955
7008
|
return false;
|
|
6956
7009
|
}
|
|
@@ -6963,7 +7016,9 @@ async function maybeApplyCombinedChown(context) {
|
|
|
6963
7016
|
}
|
|
6964
7017
|
async function maybeApplySingleChown(context) {
|
|
6965
7018
|
const { current, options, remotePath, ssh: ssh2 } = context;
|
|
6966
|
-
if (options.owner == null || current.owner
|
|
7019
|
+
if (options.owner == null || propertiesComponentMatches(options.owner, current.owner, current.ownerId)) {
|
|
7020
|
+
return false;
|
|
7021
|
+
}
|
|
6967
7022
|
const result = await ssh2.exec(renderGuardedChownCommand(options.owner, remotePath), EXEC_OPTS6);
|
|
6968
7023
|
if (result.code !== 0) {
|
|
6969
7024
|
return failedCommand(`[file.properties: ${remotePath}] chown failed`, result);
|
|
@@ -6972,7 +7027,9 @@ async function maybeApplySingleChown(context) {
|
|
|
6972
7027
|
}
|
|
6973
7028
|
async function maybeApplySingleChgrp(context) {
|
|
6974
7029
|
const { current, options, remotePath, ssh: ssh2 } = context;
|
|
6975
|
-
if (options.group == null || current.group
|
|
7030
|
+
if (options.group == null || propertiesComponentMatches(options.group, current.group, current.groupId)) {
|
|
7031
|
+
return false;
|
|
7032
|
+
}
|
|
6976
7033
|
const result = await ssh2.exec(renderGuardedChgrpCommand(options.group, remotePath), EXEC_OPTS6);
|
|
6977
7034
|
if (result.code !== 0) {
|
|
6978
7035
|
return failedCommand(`[file.properties: ${remotePath}] chgrp failed`, result);
|
|
@@ -7011,10 +7068,10 @@ function properties(remotePath, options) {
|
|
|
7011
7068
|
async check(ssh2) {
|
|
7012
7069
|
if (!ssh2) return NEEDS_APPLY;
|
|
7013
7070
|
if (await isSymlink(ssh2, remotePath)) return NEEDS_APPLY;
|
|
7014
|
-
const { group: group2, mode, owner } = await readPropertiesState(ssh2, remotePath);
|
|
7071
|
+
const { group: group2, groupId, mode, owner, ownerId } = await readPropertiesState(ssh2, remotePath);
|
|
7015
7072
|
if (options.mode != null && !modeMatches(mode, options.mode)) return NEEDS_APPLY;
|
|
7016
|
-
if (options.owner
|
|
7017
|
-
if (options.group
|
|
7073
|
+
if (!propertiesComponentMatches(options.owner, owner, ownerId)) return NEEDS_APPLY;
|
|
7074
|
+
if (!propertiesComponentMatches(options.group, group2, groupId)) return NEEDS_APPLY;
|
|
7018
7075
|
return "ok";
|
|
7019
7076
|
},
|
|
7020
7077
|
name: `file.properties: ${remotePath}`
|
|
@@ -10689,6 +10746,7 @@ async function runVersionedInstall(parameters) {
|
|
|
10689
10746
|
|
|
10690
10747
|
// src/modules/package.ts
|
|
10691
10748
|
var EXEC_OPTS12 = { ignoreExitCode: true, silent: true };
|
|
10749
|
+
var PACKAGE_INDEX_REFRESHED_DETAIL = "marker was missing, package index refreshed";
|
|
10692
10750
|
function execOptions(options) {
|
|
10693
10751
|
if (options?.timeout === void 0) return EXEC_OPTS12;
|
|
10694
10752
|
return { ...EXEC_OPTS12, timeout: options.timeout };
|
|
@@ -10920,7 +10978,16 @@ var pkg = {
|
|
|
10920
10978
|
* A flag file at `${FLAGS_DIRECTORY}/package-update-<date>` is created after
|
|
10921
10979
|
* a successful run. On the next run the flag is detected and the module
|
|
10922
10980
|
* reports `"ok"` without running the update again. Changing `date` to a new
|
|
10923
|
-
* value
|
|
10981
|
+
* value selects a new flag file, so the update runs once more.
|
|
10982
|
+
*
|
|
10983
|
+
* The marker is written with `setFlag`, not `setVersionedFlag`: the flag
|
|
10984
|
+
* name carries no call-site identity, only the date. An evicting prefix
|
|
10985
|
+
* would therefore be host-global, and two `package.update` calls with
|
|
10986
|
+
* different dates would delete each other's marker on every run so that
|
|
10987
|
+
* neither could ever converge. Two consequences are accepted deliberately:
|
|
10988
|
+
* markers for retired dates are not pruned (one empty file per distinct
|
|
10989
|
+
* date), and re-using an earlier date is skipped rather than re-run,
|
|
10990
|
+
* because that marker still exists.
|
|
10924
10991
|
*
|
|
10925
10992
|
* @param date - A date string used as the idempotency key (e.g. `"2024-01-15"`).
|
|
10926
10993
|
* @param options - Optional per-call overrides (e.g. SSH command `timeout`).
|
|
@@ -10942,9 +11009,9 @@ var pkg = {
|
|
|
10942
11009
|
if (result.code !== 0) {
|
|
10943
11010
|
return failedCommand(`[package.update: ${date}] package index refresh failed`, result);
|
|
10944
11011
|
}
|
|
10945
|
-
const flagFailure = await
|
|
11012
|
+
const flagFailure = await setFlag(ssh2, flagName);
|
|
10946
11013
|
if (flagFailure) return flagFailure;
|
|
10947
|
-
return { status: "changed" };
|
|
11014
|
+
return { detail: PACKAGE_INDEX_REFRESHED_DETAIL, status: "changed" };
|
|
10948
11015
|
},
|
|
10949
11016
|
async check(ssh2) {
|
|
10950
11017
|
if (!ssh2) return NEEDS_APPLY;
|
|
@@ -10959,7 +11026,13 @@ var pkg = {
|
|
|
10959
11026
|
* A flag file at `${FLAGS_DIRECTORY}/package-upgrade-<date>` is created after
|
|
10960
11027
|
* a successful run. On the next run the flag is detected and the module
|
|
10961
11028
|
* reports `"ok"` without running the upgrade again. Changing `date` to a new
|
|
10962
|
-
* value
|
|
11029
|
+
* value selects a new flag file, so the upgrade runs once more.
|
|
11030
|
+
*
|
|
11031
|
+
* The marker is written with `setFlag` for the same reason as in
|
|
11032
|
+
* `pkg.update`: the flag name carries only the date, so an
|
|
11033
|
+
* evicting prefix would be host-global and sibling calls could never
|
|
11034
|
+
* converge. Retired markers are therefore not pruned, and re-using an
|
|
11035
|
+
* earlier date is skipped rather than re-run.
|
|
10963
11036
|
*
|
|
10964
11037
|
* On apt systems this runs `dpkg --configure -a`, `apt-get update`, and
|
|
10965
11038
|
* `apt-get upgrade -y` as three separate commands (each subject to its own
|
|
@@ -10985,15 +11058,20 @@ var pkg = {
|
|
|
10985
11058
|
const pm = await detectPackageManager(ssh2);
|
|
10986
11059
|
if (!pm) return missingPackageManager(`package.upgrade: ${date}`);
|
|
10987
11060
|
const pipelineOptions = execOptions(options);
|
|
11061
|
+
let lastStdout = "";
|
|
10988
11062
|
for (const command2 of UPGRADE_COMMANDS[pm]) {
|
|
10989
11063
|
const result = await ssh2.exec(command2, pipelineOptions);
|
|
10990
11064
|
if (result.code !== 0) {
|
|
10991
11065
|
return failedCommand(`[package.upgrade: ${date}] package upgrade failed`, result);
|
|
10992
11066
|
}
|
|
11067
|
+
lastStdout = result.stdout;
|
|
10993
11068
|
}
|
|
10994
|
-
const flagFailure = await
|
|
11069
|
+
const flagFailure = await setFlag(ssh2, flagName);
|
|
10995
11070
|
if (flagFailure) return flagFailure;
|
|
10996
|
-
return {
|
|
11071
|
+
return {
|
|
11072
|
+
detail: pm === "apt" ? describeAptUpgradeOutcome(lastStdout) : UNKNOWN_UPGRADE_OUTCOME_DETAIL,
|
|
11073
|
+
status: "changed"
|
|
11074
|
+
};
|
|
10997
11075
|
},
|
|
10998
11076
|
async check(ssh2) {
|
|
10999
11077
|
if (!ssh2) return NEEDS_APPLY;
|
package/dist/cli.js
CHANGED
|
@@ -6262,7 +6262,7 @@ async function runApplyCommand(file, options, run = runPlaybook) {
|
|
|
6262
6262
|
if (options.diff && !options.dryRun) {
|
|
6263
6263
|
throw new CliUsageError("--diff requires --dry-run");
|
|
6264
6264
|
}
|
|
6265
|
-
printCliHeader("0.
|
|
6265
|
+
printCliHeader("0.17.0-a7265db");
|
|
6266
6266
|
const environmentOverrides = applyCliEnvironmentOverrides(options.env, {
|
|
6267
6267
|
firstRun: options.firstRun
|
|
6268
6268
|
});
|
|
@@ -6300,7 +6300,7 @@ function renderSubcommandOptionsHelp(command) {
|
|
|
6300
6300
|
return ["", `Options for "paratix ${command.name()} <file>":`, ...rows].join("\n");
|
|
6301
6301
|
}
|
|
6302
6302
|
var program = new Command();
|
|
6303
|
-
program.name("paratix").description("Idempotent VPS setup tool in TypeScript").version("0.
|
|
6303
|
+
program.name("paratix").description("Idempotent VPS setup tool in TypeScript").version("0.17.0-a7265db");
|
|
6304
6304
|
var applyCommand = program.command("apply <file>").description("Apply a server definition").option(
|
|
6305
6305
|
"--diff",
|
|
6306
6306
|
"When combined with --dry-run, show a unified diff per module that would change.",
|
|
@@ -429,7 +429,16 @@ declare const pkg: {
|
|
|
429
429
|
* A flag file at `${FLAGS_DIRECTORY}/package-update-<date>` is created after
|
|
430
430
|
* a successful run. On the next run the flag is detected and the module
|
|
431
431
|
* reports `"ok"` without running the update again. Changing `date` to a new
|
|
432
|
-
* value
|
|
432
|
+
* value selects a new flag file, so the update runs once more.
|
|
433
|
+
*
|
|
434
|
+
* The marker is written with `setFlag`, not `setVersionedFlag`: the flag
|
|
435
|
+
* name carries no call-site identity, only the date. An evicting prefix
|
|
436
|
+
* would therefore be host-global, and two `package.update` calls with
|
|
437
|
+
* different dates would delete each other's marker on every run so that
|
|
438
|
+
* neither could ever converge. Two consequences are accepted deliberately:
|
|
439
|
+
* markers for retired dates are not pruned (one empty file per distinct
|
|
440
|
+
* date), and re-using an earlier date is skipped rather than re-run,
|
|
441
|
+
* because that marker still exists.
|
|
433
442
|
*
|
|
434
443
|
* @param date - A date string used as the idempotency key (e.g. `"2024-01-15"`).
|
|
435
444
|
* @param options - Optional per-call overrides (e.g. SSH command `timeout`).
|
|
@@ -446,7 +455,13 @@ declare const pkg: {
|
|
|
446
455
|
* A flag file at `${FLAGS_DIRECTORY}/package-upgrade-<date>` is created after
|
|
447
456
|
* a successful run. On the next run the flag is detected and the module
|
|
448
457
|
* reports `"ok"` without running the upgrade again. Changing `date` to a new
|
|
449
|
-
* value
|
|
458
|
+
* value selects a new flag file, so the upgrade runs once more.
|
|
459
|
+
*
|
|
460
|
+
* The marker is written with `setFlag` for the same reason as in
|
|
461
|
+
* `pkg.update`: the flag name carries only the date, so an
|
|
462
|
+
* evicting prefix would be host-global and sibling calls could never
|
|
463
|
+
* converge. Retired markers are therefore not pruned, and re-using an
|
|
464
|
+
* earlier date is skipped rather than re-run.
|
|
450
465
|
*
|
|
451
466
|
* On apt systems this runs `dpkg --configure -a`, `apt-get update`, and
|
|
452
467
|
* `apt-get upgrade -y` as three separate commands (each subject to its own
|
|
@@ -504,6 +519,12 @@ declare const apt: {
|
|
|
504
519
|
* The pipeline is split into three separate SSH commands so that each step
|
|
505
520
|
* gets its own timeout window and produces a precise failure label.
|
|
506
521
|
*
|
|
522
|
+
* The marker is written with `setFlag`: the flag name carries only the
|
|
523
|
+
* date and no call-site identity, so an evicting prefix would be
|
|
524
|
+
* host-global and two `apt.distUpgrade` calls with different dates would
|
|
525
|
+
* delete each other's marker on every run. Retired markers are therefore
|
|
526
|
+
* not pruned, and re-using an earlier date is skipped rather than re-run.
|
|
527
|
+
*
|
|
507
528
|
* @param date - A date string used as the idempotency key (e.g. `"2024-01-15"`).
|
|
508
529
|
* @param options - Optional per-call overrides (e.g. SSH command `timeout`).
|
|
509
530
|
* The same `timeout` is applied to every step of the dist-upgrade pipeline.
|
|
@@ -1017,7 +1038,7 @@ type PropertiesOptions = {
|
|
|
1017
1038
|
/**
|
|
1018
1039
|
* Set file or directory ownership and permissions on the remote host.
|
|
1019
1040
|
* Only the attributes specified in `options` are checked and applied. Drift
|
|
1020
|
-
* is detected via `stat -c '%a %U %G'` so `apply` only invokes the relevant
|
|
1041
|
+
* is detected via `stat -c '%a %U %G %u %g'` so `apply` only invokes the relevant
|
|
1021
1042
|
* `chmod`/`chown`/`chgrp` for fields that actually differ from the desired
|
|
1022
1043
|
* state. When all desired fields already match, `apply` returns `status: "ok"`
|
|
1023
1044
|
* instead of falsely reporting a change.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E as Environment, M as Module, a as ModuleMetaEntry, b as MetaEnvironmentValue, c as EnvironmentMetaEntry, S as SshdPortMetaEntry, d as SystemHostMetaEntry, e as SystemRebootMetaEntry, f as ModuleResult, g as ExecResult, h as ModuleStatus, i as SshConnection, O as OrchestrationStep, j as ShutdownSignal, k as ServerDefinition } from './index-
|
|
2
|
-
export { l as EnvironmentValue, m as ExecOptions, N as NEEDS_APPLY, n as SshConfig, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from './index-
|
|
1
|
+
import { E as Environment, M as Module, a as ModuleMetaEntry, b as MetaEnvironmentValue, c as EnvironmentMetaEntry, S as SshdPortMetaEntry, d as SystemHostMetaEntry, e as SystemRebootMetaEntry, f as ModuleResult, g as ExecResult, h as ModuleStatus, i as SshConnection, O as OrchestrationStep, j as ShutdownSignal, k as ServerDefinition } from './index-CGYwJDTf.js';
|
|
2
|
+
export { l as EnvironmentValue, m as ExecOptions, N as NEEDS_APPLY, n as SshConfig, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from './index-CGYwJDTf.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Fail the run if a condition on the current env is not satisfied.
|
package/dist/index.js
CHANGED
package/dist/modules/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { V as PackageSpec, W as UpgradeOptions, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from '../index-
|
|
1
|
+
export { V as PackageSpec, W as UpgradeOptions, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from '../index-CGYwJDTf.js';
|
package/dist/modules/index.js
CHANGED
package/llm-guide.md
CHANGED
|
@@ -142,12 +142,12 @@ export default server({
|
|
|
142
142
|
|
|
143
143
|
### `apt`
|
|
144
144
|
|
|
145
|
-
| Method | Signature | Idempotent
|
|
146
|
-
| ----------------- | ---------------------------------------------------------------------------------------- |
|
|
147
|
-
| `apt.debconf` | `(packageName: string, selections: Record<string, string>): Module` | Yes
|
|
148
|
-
| `apt.distUpgrade` | `(date: string, options?: UpgradeOptions): Module` | Yes (
|
|
149
|
-
| `apt.key` | `(name: string, url: string, options: { fingerprint: string }): Module` | Yes
|
|
150
|
-
| `apt.repository` | `(nameOrPpa: string, source?: string, options?: { signedBy?: false \| string }): Module` | Yes
|
|
145
|
+
| Method | Signature | Idempotent |
|
|
146
|
+
| ----------------- | ---------------------------------------------------------------------------------------- | ---------------- |
|
|
147
|
+
| `apt.debconf` | `(packageName: string, selections: Record<string, string>): Module` | Yes |
|
|
148
|
+
| `apt.distUpgrade` | `(date: string, options?: UpgradeOptions): Module` | Yes (dated flag) |
|
|
149
|
+
| `apt.key` | `(name: string, url: string, options: { fingerprint: string }): Module` | Yes |
|
|
150
|
+
| `apt.repository` | `(nameOrPpa: string, source?: string, options?: { signedBy?: false \| string }): Module` | Yes |
|
|
151
151
|
|
|
152
152
|
### `archive`
|
|
153
153
|
|
|
@@ -224,6 +224,15 @@ very slow artifact hosts or large downloads.
|
|
|
224
224
|
| `file.stat` | `(remotePath: string): Module` | No (always-applies) |
|
|
225
225
|
| `file.template` | `(remotePath: string, templatePath: string, options?: { mode?: string; owner?: string; strict?: boolean }): Module` | Yes |
|
|
226
226
|
|
|
227
|
+
**Note on `owner` and `group` values:** Every ownership option accepts either a POSIX name or a
|
|
228
|
+
numeric id — `owner: "www-data:www-data"` and `owner: "65532:65532"` are both valid, and the two
|
|
229
|
+
components may be mixed (`owner: "root:65532"`). Numeric ids are the only option for accounts that
|
|
230
|
+
have no passwd or group entry on the target host, such as `65532` in distroless images or `70` in
|
|
231
|
+
the postgres image. Drift detection compares the declared value against both the name and the id
|
|
232
|
+
reported by the host, so a numerically declared owner converges to `status: "ok"` like a named one.
|
|
233
|
+
`file.properties` keeps `owner` and `group` as separate options and therefore rejects a combined
|
|
234
|
+
`"user:group"` spec.
|
|
235
|
+
|
|
227
236
|
**Note on `file.copy` and the default mode:** When `options.mode` is omitted, `file.copy` sets the mode to the documented default of `0644`. The mode is applied during upload (`uploadFile` with `{ mode }`) and compared with the desired value in `check`, so subsequent runs detect mode drift (for example, a manual `chmod 0600`) as `needs-apply`. Pass `{ mode: "0600" }` explicitly when more restrictive permissions are required, such as for secrets.
|
|
228
237
|
|
|
229
238
|
**Note on `file.line`:** The `line` value must be a single line without CR/LF characters. Use `file.block` for multiline content.
|
|
@@ -279,12 +288,12 @@ that are expected to respond more slowly.
|
|
|
279
288
|
|
|
280
289
|
Import with renaming: `import { package as pkg } from "paratix/modules"`. The word `package` is reserved in JavaScript, so you must alias it.
|
|
281
290
|
|
|
282
|
-
| Method | Signature | Idempotent
|
|
283
|
-
| ------------------- | --------------------------------------------------------------------------------- |
|
|
284
|
-
| `package.installed` | `(...packagesAndOptions: Array<string \| PackageSpec \| UpgradeOptions>): Module` | Yes
|
|
285
|
-
| `package.absent` | `(...packagesAndOptions: Array<string \| PackageSpec \| UpgradeOptions>): Module` | Yes
|
|
286
|
-
| `package.update` | `(date: string, options?: UpgradeOptions): Module` | Yes (
|
|
287
|
-
| `package.upgrade` | `(date: string, options?: UpgradeOptions): Module` | Yes (
|
|
291
|
+
| Method | Signature | Idempotent |
|
|
292
|
+
| ------------------- | --------------------------------------------------------------------------------- | ---------------- |
|
|
293
|
+
| `package.installed` | `(...packagesAndOptions: Array<string \| PackageSpec \| UpgradeOptions>): Module` | Yes |
|
|
294
|
+
| `package.absent` | `(...packagesAndOptions: Array<string \| PackageSpec \| UpgradeOptions>): Module` | Yes |
|
|
295
|
+
| `package.update` | `(date: string, options?: UpgradeOptions): Module` | Yes (dated flag) |
|
|
296
|
+
| `package.upgrade` | `(date: string, options?: UpgradeOptions): Module` | Yes (dated flag) |
|
|
288
297
|
|
|
289
298
|
#### `UpgradeOptions`
|
|
290
299
|
|
|
@@ -533,6 +542,7 @@ function myCustomModule(configPath: string, content: string): Module {
|
|
|
533
542
|
- Prefer `failedCommand("...", result)` when you used `ssh.exec(..., { ignoreExitCode: true })` and want stdout/stderr preserved for central runner output.
|
|
534
543
|
- Return `NEEDS_APPLY` (the exported constant), never the string literal `"needs-apply"`.
|
|
535
544
|
- `ModuleResult.status` must be one of: `"changed"`, `"failed"`, `"ok"`, `"skipped"`.
|
|
545
|
+
- Optionally return `ModuleResult.detail` with a short single-line reason; the runner appends it to the step line. Use it to say what a `changed` step actually did, and leave it unset when the step did no work. See [Status Detail on Changed Steps](#status-detail-on-changed-steps).
|
|
536
546
|
- Use typed `meta` entries in the return value, not loose objects.
|
|
537
547
|
- For normal downstream environment propagation, use `meta.env(name, value)`.
|
|
538
548
|
- `meta.env(...)` accepts strings, numbers, booleans, sync lazy functions, and async lazy functions.
|
|
@@ -773,6 +783,43 @@ async check(ssh) {
|
|
|
773
783
|
}
|
|
774
784
|
```
|
|
775
785
|
|
|
786
|
+
## Status Detail on Changed Steps
|
|
787
|
+
|
|
788
|
+
A module may return a short `detail` next to its status. The runner appends it to
|
|
789
|
+
the step line, both for top-level modules and for steps inside a recipe:
|
|
790
|
+
|
|
791
|
+
```
|
|
792
|
+
· · ↺ package.upgrade: 2026-03-01 changed no packages upgraded 4.1s
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
The dated-flag modules use this to say **why** they ran. Their step name already
|
|
796
|
+
carries the date, so the detail reports the outcome instead of repeating the key.
|
|
797
|
+
This matters most for the upgrade modules: they report `changed` whenever their
|
|
798
|
+
dated marker is absent, even when the package manager upgraded nothing.
|
|
799
|
+
|
|
800
|
+
| Module | Detail on `changed` |
|
|
801
|
+
| ----------------- | ----------------------------------------------------------------------- |
|
|
802
|
+
| `package.update` | `marker was missing, package index refreshed` |
|
|
803
|
+
| `package.upgrade` | `12 packages upgraded`, `1 package upgraded`, or `no packages upgraded` |
|
|
804
|
+
| `apt.distUpgrade` | same forms as `package.upgrade` |
|
|
805
|
+
|
|
806
|
+
Rules:
|
|
807
|
+
|
|
808
|
+
- The count is parsed from apt's upgrade summary. `package.upgrade` also serves
|
|
809
|
+
`dnf`, `yum` and `apk`, whose summaries differ; those report
|
|
810
|
+
`upgrade completed, package count unavailable`.
|
|
811
|
+
- The same fallback applies when the summary line is missing from the output, or
|
|
812
|
+
when the captured output was truncated at `maxOutputBytes`. apt prints its
|
|
813
|
+
summary last, so a truncated capture loses exactly that line, and reporting no
|
|
814
|
+
count is preferred over reporting a wrong one.
|
|
815
|
+
- A step that did no work carries no detail: the flag-already-exists path returns
|
|
816
|
+
a plain `ok`, and failures report through the normal error path.
|
|
817
|
+
- Dry runs keep the generic `changed (dry-run)` suffix. The detail is produced by
|
|
818
|
+
`apply`, which does not run in dry-run mode.
|
|
819
|
+
|
|
820
|
+
Custom modules may return `detail` the same way; see
|
|
821
|
+
[Key rules for custom modules](#key-rules-for-custom-modules).
|
|
822
|
+
|
|
776
823
|
## Dry-Run Diff Output (`--diff`)
|
|
777
824
|
|
|
778
825
|
Paratix runs a playbook in dry-run mode via `paratix apply <file> --dry-run`, which
|
|
@@ -876,7 +923,7 @@ The diff string is plain text — no ANSI codes. The output layer applies colors
|
|
|
876
923
|
5. Use `{{KEY|shell}}` or `{{KEY|raw}}` placeholders in `.tmpl` files — strict mode is on by default and bare `{{KEY}}` will throw. Provide values via `env` in `server()`.
|
|
877
924
|
6. Use `service.restart()` and `service.reload()` as `signals` in recipes, not directly in `run`.
|
|
878
925
|
7. Use `signals.flush()` only as an explicit checkpoint when staged flows require an early signal flush.
|
|
879
|
-
8. Always pass a date string to `package.upgrade()` and `package.update()` -- it is the idempotency key.
|
|
926
|
+
8. Always pass a date string to `package.upgrade()` and `package.update()` -- it is the idempotency key. Each distinct date keeps its own flag file, so several calls with different dates coexist; a date that was already applied stays applied and is not re-run. Read the step's [status detail](#status-detail-on-changed-steps) to tell a real upgrade from a step that only wrote its marker.
|
|
880
927
|
9. Specify `ssh.ports` as an array -- the runner tries each port in order.
|
|
881
928
|
10. Custom modules must implement both `check` and `apply`, both async.
|
|
882
929
|
11. Use `shellQuote()` when interpolating dynamic values into shell commands.
|