paratix 0.12.8 → 0.14.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 +18 -10
- package/dist/{chunk-M3ZQJNKM.js → chunk-475OT32R.js} +260 -29
- package/dist/cli.js +2297 -1529
- package/dist/{index-udpAybq3.d.ts → index-DlZISXJx.d.ts} +27 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.js +46 -14
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +1 -2
- package/llm-guide.md +55 -45
- package/package.json +4 -2
- package/dist/chunk-M3ZQJNKM.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/modules/index.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -307,83 +307,82 @@ function describeHostValidationFailure(failure) {
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
// src/
|
|
311
|
-
|
|
310
|
+
// src/dryRunDispatch.ts
|
|
311
|
+
function shouldExecuteApplyDuringDryRun(module, diffEnabled) {
|
|
312
|
+
if (module._dryRunBlocker === true || module._dryRunMetaProducer === true) return true;
|
|
313
|
+
if (diffEnabled && module._dryRunDiffProducer === true && module._applyDryRun != null) return true;
|
|
314
|
+
return module._applyDryRun != null && module._dryRunDiffProducer !== true;
|
|
315
|
+
}
|
|
312
316
|
|
|
313
|
-
// src/
|
|
314
|
-
import {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
317
|
+
// src/knownHosts.ts
|
|
318
|
+
import { createHash, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
319
|
+
import { appendFile, mkdir, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
320
|
+
import { homedir } from "os";
|
|
321
|
+
import { join } from "path";
|
|
322
|
+
|
|
323
|
+
// src/knownHostPatterns.ts
|
|
324
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
325
|
+
var HASHED_HOST_PARTS = 4;
|
|
326
|
+
function matchesKnownHostPatternList(patterns, needle) {
|
|
327
|
+
let matchedPositivePattern = false;
|
|
328
|
+
for (const rawPattern of patterns) {
|
|
329
|
+
const negated = rawPattern.startsWith("!");
|
|
330
|
+
const pattern = negated ? rawPattern.slice(1) : rawPattern;
|
|
331
|
+
if (pattern.length === 0) continue;
|
|
332
|
+
const matched = matchesHashedHost(pattern, needle) || matchesPlainHostPattern(pattern, needle);
|
|
333
|
+
if (!matched) continue;
|
|
334
|
+
if (negated) return false;
|
|
335
|
+
matchedPositivePattern = true;
|
|
330
336
|
}
|
|
331
|
-
return
|
|
337
|
+
return matchedPositivePattern;
|
|
332
338
|
}
|
|
333
|
-
function
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
(parameters.terminalColumns ?? DEFAULT_TERMINAL_COLUMNS) - parameters.continuationIndentWidth,
|
|
337
|
-
MIN_PACKAGE_COLUMNS_WIDTH
|
|
338
|
-
);
|
|
339
|
-
const widestPackage = Math.max(...parameters.packages.map((entry) => entry.length));
|
|
340
|
-
const columnWidth = Math.max(widestPackage + PACKAGE_COLUMN_GAP_WIDTH, MIN_PACKAGE_COLUMN_WIDTH);
|
|
341
|
-
const columnCount = Math.max(Math.floor(availableWidth / columnWidth), 1);
|
|
342
|
-
const rowCount = Math.ceil(parameters.packages.length / columnCount);
|
|
343
|
-
return Array.from(
|
|
344
|
-
{ length: rowCount },
|
|
345
|
-
(_row, rowIndex) => Array.from({ length: columnCount }, (_column, columnIndex) => {
|
|
346
|
-
const packageIndex = rowIndex + columnIndex * rowCount;
|
|
347
|
-
const packageName = parameters.packages.at(packageIndex);
|
|
348
|
-
if (packageName == null) return "";
|
|
349
|
-
const isLastVisibleColumn = columnIndex === columnCount - 1 || packageIndex + rowCount >= parameters.packages.length;
|
|
350
|
-
return isLastVisibleColumn ? packageName : packageName.padEnd(columnWidth);
|
|
351
|
-
}).filter(Boolean).join("")
|
|
352
|
-
);
|
|
339
|
+
function matchesPlainHostPattern(pattern, needle) {
|
|
340
|
+
if (pattern.startsWith("|1|")) return false;
|
|
341
|
+
return matchesHostPattern(pattern.toLowerCase(), needle.toLowerCase());
|
|
353
342
|
}
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
|
|
343
|
+
function matchesHashedHost(pattern, needle) {
|
|
344
|
+
if (!pattern.startsWith("|1|")) return false;
|
|
345
|
+
const parts = pattern.split("|");
|
|
346
|
+
if (parts.length !== HASHED_HOST_PARTS || parts[1] !== "1") return false;
|
|
347
|
+
const salt = Buffer.from(parts[2] ?? "", "base64");
|
|
348
|
+
const expectedHash = Buffer.from(parts[3] ?? "", "base64");
|
|
349
|
+
if (salt.length === 0 || expectedHash.length === 0) return false;
|
|
350
|
+
const actualHash = createHmac("sha1", salt).update(needle).digest();
|
|
351
|
+
return actualHash.length === expectedHash.length && timingSafeEqual(actualHash, expectedHash);
|
|
357
352
|
}
|
|
358
|
-
function
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
353
|
+
function matchesHostPattern(pattern, needle) {
|
|
354
|
+
let state = { needleIndex: 0, patternIndex: 0, starIndex: -1, starNeedleIndex: 0 };
|
|
355
|
+
while (state.needleIndex < needle.length) {
|
|
356
|
+
const nextState = advanceHostPatternMatch(pattern, needle, state);
|
|
357
|
+
if (nextState === null) return false;
|
|
358
|
+
state = nextState;
|
|
359
|
+
}
|
|
360
|
+
while (pattern[state.patternIndex] === "*") state.patternIndex += 1;
|
|
361
|
+
return state.patternIndex === pattern.length;
|
|
363
362
|
}
|
|
364
|
-
function
|
|
365
|
-
const
|
|
366
|
-
if (
|
|
363
|
+
function advanceHostPatternMatch(pattern, needle, state) {
|
|
364
|
+
const patternCharacter = pattern[state.patternIndex];
|
|
365
|
+
if (patternCharacter === "?" || patternCharacter === needle[state.needleIndex]) {
|
|
366
|
+
return { ...state, needleIndex: state.needleIndex + 1, patternIndex: state.patternIndex + 1 };
|
|
367
|
+
}
|
|
368
|
+
if (patternCharacter === "*") {
|
|
367
369
|
return {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
370
|
+
...state,
|
|
371
|
+
patternIndex: state.patternIndex + 1,
|
|
372
|
+
starIndex: state.patternIndex,
|
|
373
|
+
starNeedleIndex: state.needleIndex
|
|
371
374
|
};
|
|
372
375
|
}
|
|
376
|
+
if (state.starIndex === -1) return null;
|
|
377
|
+
const starNeedleIndex = state.starNeedleIndex + 1;
|
|
373
378
|
return {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
terminalColumns: parameters.terminalColumns
|
|
379
|
-
}),
|
|
380
|
-
name: packageModule.summaryName
|
|
379
|
+
...state,
|
|
380
|
+
needleIndex: starNeedleIndex,
|
|
381
|
+
patternIndex: state.starIndex + 1,
|
|
382
|
+
starNeedleIndex
|
|
381
383
|
};
|
|
382
384
|
}
|
|
383
385
|
|
|
384
|
-
// src/secretSink.ts
|
|
385
|
-
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
386
|
-
|
|
387
386
|
// src/sshHelpers.ts
|
|
388
387
|
import { StringDecoder } from "string_decoder";
|
|
389
388
|
|
|
@@ -911,1320 +910,1363 @@ async function tryConnectOnPort(parameters) {
|
|
|
911
910
|
});
|
|
912
911
|
}
|
|
913
912
|
|
|
914
|
-
// src/
|
|
915
|
-
var
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
throw new Error("Secret registrations must not contain the redaction placeholder");
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
var MINIMUM_SECRET_LENGTH = 8;
|
|
925
|
-
function registerSecret(secret) {
|
|
926
|
-
if (secret.length < MINIMUM_SECRET_LENGTH) return;
|
|
927
|
-
assertRegistrableSecret(secret);
|
|
928
|
-
secretCounts.set(secret, (secretCounts.get(secret) ?? 0) + 1);
|
|
929
|
-
}
|
|
930
|
-
function unregisterSecret(secret) {
|
|
931
|
-
if (secret.length < MINIMUM_SECRET_LENGTH) return;
|
|
932
|
-
const current = secretCounts.get(secret);
|
|
933
|
-
if (current == null) return;
|
|
934
|
-
if (current <= 1) {
|
|
935
|
-
secretCounts.delete(secret);
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
secretCounts.set(secret, current - 1);
|
|
913
|
+
// src/knownHosts.ts
|
|
914
|
+
var knownHostsLock = Promise.resolve();
|
|
915
|
+
async function withKnownHostsLock(operation) {
|
|
916
|
+
const previous = knownHostsLock;
|
|
917
|
+
const next = previous.then(async () => operation());
|
|
918
|
+
knownHostsLock = next.catch(() => null);
|
|
919
|
+
return next;
|
|
939
920
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
921
|
+
var HostKeyVerificationError = class _HostKeyVerificationError extends Error {
|
|
922
|
+
constructor(message) {
|
|
923
|
+
super(message);
|
|
924
|
+
this.name = "HostKeyVerificationError";
|
|
925
|
+
Error.captureStackTrace(this, _HostKeyVerificationError);
|
|
945
926
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
return maskScopedResult(result, secrets);
|
|
953
|
-
} catch (error) {
|
|
954
|
-
throw maskScopedError(error, secrets);
|
|
955
|
-
} finally {
|
|
956
|
-
for (const secret of registered) {
|
|
957
|
-
unregisterSecret(secret);
|
|
958
|
-
}
|
|
927
|
+
};
|
|
928
|
+
var KnownHostsValidationError = class _KnownHostsValidationError extends Error {
|
|
929
|
+
constructor(message) {
|
|
930
|
+
super(message);
|
|
931
|
+
this.name = "KnownHostsValidationError";
|
|
932
|
+
Error.captureStackTrace(this, _KnownHostsValidationError);
|
|
959
933
|
}
|
|
934
|
+
};
|
|
935
|
+
var MIN_KNOWN_HOSTS_FIELDS = 3;
|
|
936
|
+
var DEFAULT_SSH_PORT = 22;
|
|
937
|
+
var UINT32_SIZE = 4;
|
|
938
|
+
var STRICT_BASE64_PATTERN = new RegExp("^[A-Za-z0-9+\\/]+=*$", "v");
|
|
939
|
+
var CERT_AUTHORITY_MESSAGE = "HOST KEY VERIFICATION FAILED \u2014 known_hosts contains a @cert-authority entry, but paratix does not validate certificate-authority host keys. Remove the @cert-authority marker or pin the host with ssh.expectedHostFingerprint / ssh.expectedHostPublicKey instead.";
|
|
940
|
+
var defaultHostKeyCache = /* @__PURE__ */ new Map();
|
|
941
|
+
function createHostKeyCache() {
|
|
942
|
+
return /* @__PURE__ */ new Map();
|
|
960
943
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
}
|
|
944
|
+
function parseKnownHostsLine(line) {
|
|
945
|
+
const parts = line.split(new RegExp("\\s+", "v"));
|
|
946
|
+
const offset = parts[0]?.startsWith("@") ? 1 : 0;
|
|
947
|
+
if (parts.length < MIN_KNOWN_HOSTS_FIELDS + offset) return [];
|
|
948
|
+
const marker = offset === 1 ? parts[0] : void 0;
|
|
949
|
+
const hostsPart = parts[offset];
|
|
950
|
+
const algo = parts[offset + 1];
|
|
951
|
+
const base64Key = parts[offset + 2];
|
|
952
|
+
if (!STRICT_BASE64_PATTERN.test(base64Key)) return [];
|
|
953
|
+
const key = Buffer.from(base64Key, "base64");
|
|
954
|
+
const hostPatterns = hostsPart.split(",");
|
|
955
|
+
return hostPatterns.map((host) => ({ algo, host, hostPatterns, key, marker }));
|
|
975
956
|
}
|
|
976
|
-
function
|
|
977
|
-
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
if (maskedDetail !== result.detail) {
|
|
983
|
-
next = { ...result, detail: maskedDetail };
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
if (isModuleResult(next) && next.status === "failed" && next.error != null) {
|
|
987
|
-
next = { ...next, error: maskScopedError(next.error, secrets) };
|
|
957
|
+
function parseKnownHosts(content) {
|
|
958
|
+
const entries = [];
|
|
959
|
+
for (const raw of content.split("\n")) {
|
|
960
|
+
const line = raw.trim();
|
|
961
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
962
|
+
entries.push(...parseKnownHostsLine(line));
|
|
988
963
|
}
|
|
989
|
-
return
|
|
990
|
-
}
|
|
991
|
-
function isModuleResult(value) {
|
|
992
|
-
return typeof value === "object" && value !== null && "status" in value && typeof value.status === "string";
|
|
964
|
+
return entries;
|
|
993
965
|
}
|
|
994
|
-
function
|
|
995
|
-
|
|
996
|
-
if (cause instanceof Error) return maskScopedError(cause, secrets);
|
|
997
|
-
return maskSecrets(stringifyCause(cause, secretList), secretList);
|
|
966
|
+
function formatHostNeedle(host, port) {
|
|
967
|
+
return port === DEFAULT_SSH_PORT ? host : `[${host}]:${port}`;
|
|
998
968
|
}
|
|
999
|
-
function
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
969
|
+
function assertSafeHostForKnownHosts(host) {
|
|
970
|
+
const hostValidationFailure = validateHostLabel(host);
|
|
971
|
+
if (hostValidationFailure == null) return;
|
|
972
|
+
throw new KnownHostsValidationError(
|
|
973
|
+
`Refusing to persist known_hosts entry: host label ${describeHostValidationFailure(hostValidationFailure)}`
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
function matchesKnownHostEntry(entry, needle) {
|
|
977
|
+
return matchesKnownHostPatternList(entry.hostPatterns ?? [entry.host], needle);
|
|
978
|
+
}
|
|
979
|
+
function findMatchingEntries(entries, host, port) {
|
|
980
|
+
const needle = formatHostNeedle(host, port);
|
|
981
|
+
return entries.filter((entry) => matchesKnownHostEntry(entry, needle));
|
|
982
|
+
}
|
|
983
|
+
function findRevokedEntry(entries, key) {
|
|
984
|
+
return entries.find(
|
|
985
|
+
(entry) => entry.marker === "@revoked" && entry.key.length === key.length && timingSafeEqual2(entry.key, key)
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
function throwHostKeyMismatch(host, presentedKey, existingKey) {
|
|
989
|
+
const presentedAlgo = describeAlgoForDiagnostics(presentedKey);
|
|
990
|
+
const knownHostsDetails = existingKey == null ? "remote host key does not match the key in known_hosts. " : `remote host key (${presentedAlgo}) does not match the key in known_hosts (${describeAlgoForDiagnostics(existingKey)}). `;
|
|
991
|
+
throw new HostKeyVerificationError(
|
|
992
|
+
`HOST KEY VERIFICATION FAILED for ${host}: ${knownHostsDetails}This could indicate a man-in-the-middle attack.`
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
function verifyHostKeyAgainstKnownEntries(parameters) {
|
|
996
|
+
const { cachedKey, fileEntries, host, key } = parameters;
|
|
997
|
+
const revokedKey = findRevokedEntry(fileEntries, key);
|
|
998
|
+
if (revokedKey != null) {
|
|
999
|
+
throw new HostKeyVerificationError(
|
|
1000
|
+
`HOST KEY VERIFICATION FAILED for ${host}: remote host key (${describeAlgoForDiagnostics(revokedKey.key)}) is marked as revoked in known_hosts.`
|
|
1005
1001
|
);
|
|
1006
1002
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
return
|
|
1003
|
+
if (fileEntries.some((entry) => entry.marker === "@cert-authority"))
|
|
1004
|
+
throw new HostKeyVerificationError(`${host}: ${CERT_AUTHORITY_MESSAGE}`);
|
|
1005
|
+
const rawHostKeyEntries = fileEntries.filter((entry) => entry.marker == null);
|
|
1006
|
+
const matchingEntry = rawHostKeyEntries.find(
|
|
1007
|
+
(entry) => entry.key.length === key.length && timingSafeEqual2(entry.key, key)
|
|
1008
|
+
);
|
|
1009
|
+
if (matchingEntry != null) return true;
|
|
1010
|
+
if (cachedKey?.length === key.length && timingSafeEqual2(cachedKey, key)) {
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
const firstRawHostKey = rawHostKeyEntries.at(0)?.key;
|
|
1014
|
+
if (firstRawHostKey != null) throwHostKeyMismatch(host, key, firstRawHostKey);
|
|
1015
|
+
if (cachedKey != null) throwHostKeyMismatch(host, key, cachedKey);
|
|
1016
|
+
return false;
|
|
1014
1017
|
}
|
|
1015
|
-
function
|
|
1016
|
-
if (
|
|
1017
|
-
|
|
1018
|
+
function extractAlgoFromKey(keyBuffer) {
|
|
1019
|
+
if (keyBuffer.length < UINT32_SIZE) {
|
|
1020
|
+
throw new Error("Invalid SSH key buffer: too short to contain algorithm length");
|
|
1018
1021
|
}
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
const maskedCause = maskCauseValue(error.cause, secrets, secretList);
|
|
1023
|
-
const clone = buildMaskedErrorClone(error, maskedMessage, secretList);
|
|
1024
|
-
clone.name = error.name;
|
|
1025
|
-
if (maskedStack != null) {
|
|
1026
|
-
Object.defineProperty(clone, "stack", {
|
|
1027
|
-
configurable: true,
|
|
1028
|
-
value: maskedStack,
|
|
1029
|
-
writable: true
|
|
1030
|
-
});
|
|
1022
|
+
const algoLength = keyBuffer.readUInt32BE(0);
|
|
1023
|
+
if (algoLength === 0 || UINT32_SIZE + algoLength > keyBuffer.length) {
|
|
1024
|
+
throw new Error("Invalid SSH key buffer: algorithm length exceeds buffer size");
|
|
1031
1025
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
value: maskedCause,
|
|
1036
|
-
writable: true
|
|
1037
|
-
});
|
|
1026
|
+
const algo = keyBuffer.subarray(UINT32_SIZE, UINT32_SIZE + algoLength).toString("ascii");
|
|
1027
|
+
if (!PINNED_HOST_KEY_ALGORITHM_PATTERN.test(algo)) {
|
|
1028
|
+
throw new KnownHostsValidationError(`Invalid SSH key buffer: unsupported algorithm '${algo}'`);
|
|
1038
1029
|
}
|
|
1039
|
-
return
|
|
1040
|
-
}
|
|
1041
|
-
function stringifyCause(cause, secretList) {
|
|
1042
|
-
if (typeof cause === "string") return cause;
|
|
1043
|
-
if (typeof cause === "function")
|
|
1044
|
-
return cause.name.length > 0 ? `[Function: ${cause.name}]` : "[Function]";
|
|
1045
|
-
if (typeof cause === "number") return String(cause);
|
|
1046
|
-
if (typeof cause === "boolean") return String(cause);
|
|
1047
|
-
if (typeof cause === "bigint") return String(cause);
|
|
1048
|
-
if (typeof cause === "symbol") return String(cause);
|
|
1049
|
-
if (cause === void 0) return String(cause);
|
|
1050
|
-
if (cause === null) return "null";
|
|
1051
|
-
return stringifyObjectCause(cause, secretList);
|
|
1030
|
+
return algo;
|
|
1052
1031
|
}
|
|
1053
|
-
function
|
|
1032
|
+
function describeAlgoForDiagnostics(keyBuffer) {
|
|
1054
1033
|
try {
|
|
1055
|
-
|
|
1056
|
-
return serialized ?? Object.prototype.toString.call(cause);
|
|
1034
|
+
return extractAlgoFromKey(keyBuffer);
|
|
1057
1035
|
} catch {
|
|
1058
|
-
return
|
|
1036
|
+
return "<unknown>";
|
|
1059
1037
|
}
|
|
1060
1038
|
}
|
|
1061
|
-
function
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1039
|
+
function computeFingerprint(key) {
|
|
1040
|
+
const hash = createHash("sha256").update(key).digest("base64");
|
|
1041
|
+
return `SHA256:${hash.replaceAll("=", "")}`;
|
|
1042
|
+
}
|
|
1043
|
+
async function appendHostKey(host, port, keyBuffer) {
|
|
1044
|
+
assertSafeHostForKnownHosts(host);
|
|
1045
|
+
const hostLabel = formatHostNeedle(host, port);
|
|
1046
|
+
const algo = extractAlgoFromKey(keyBuffer);
|
|
1047
|
+
const base64Key = keyBuffer.toString("base64");
|
|
1048
|
+
if (new RegExp("\\s", "v").test(hostLabel)) {
|
|
1049
|
+
throw new KnownHostsValidationError(
|
|
1050
|
+
`Refusing to persist known_hosts entry: host label contains whitespace`
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
if (new RegExp("\\s", "v").test(algo)) {
|
|
1054
|
+
throw new KnownHostsValidationError(
|
|
1055
|
+
`Refusing to persist known_hosts entry: algorithm contains whitespace`
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
if (new RegExp("\\s", "v").test(base64Key)) {
|
|
1059
|
+
throw new KnownHostsValidationError(
|
|
1060
|
+
`Refusing to persist known_hosts entry: base64 key contains whitespace`
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
const line = `${hostLabel} ${algo} ${base64Key}
|
|
1064
|
+
`;
|
|
1065
|
+
const sshDirectory = join(homedir(), ".ssh");
|
|
1066
|
+
const filePath = join(sshDirectory, "known_hosts");
|
|
1067
|
+
await withKnownHostsLock(async () => {
|
|
1068
|
+
await mkdir(sshDirectory, { mode: 448, recursive: true });
|
|
1069
|
+
const existingEntries = await loadKnownHostEntries();
|
|
1070
|
+
const matchingRawHostEntries = existingEntries.filter(
|
|
1071
|
+
(entry) => entry.marker == null && matchesKnownHostEntry(entry, hostLabel)
|
|
1072
|
+
);
|
|
1073
|
+
const alreadyTrusted = matchingRawHostEntries.some(
|
|
1074
|
+
(entry) => entry.algo === algo && entry.key.length === keyBuffer.length && timingSafeEqual2(entry.key, keyBuffer)
|
|
1075
|
+
);
|
|
1076
|
+
if (alreadyTrusted) return;
|
|
1077
|
+
const conflictingEntry = matchingRawHostEntries.find(
|
|
1078
|
+
(entry) => entry.key.length !== keyBuffer.length || !timingSafeEqual2(entry.key, keyBuffer)
|
|
1079
|
+
);
|
|
1080
|
+
if (conflictingEntry != null) throwHostKeyMismatch(host, keyBuffer, conflictingEntry.key);
|
|
1081
|
+
await appendFile(filePath, line, { mode: 384 });
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
function getFileSystemErrorCode(error) {
|
|
1085
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
1086
|
+
}
|
|
1087
|
+
var KIBIBYTE_BYTES = 1024;
|
|
1088
|
+
var MEBIBYTE_BYTES = KIBIBYTE_BYTES * KIBIBYTE_BYTES;
|
|
1089
|
+
var KNOWN_HOSTS_FILE_LIMIT_MIB = 16;
|
|
1090
|
+
var KNOWN_HOSTS_FILE_BYTE_LIMIT = KNOWN_HOSTS_FILE_LIMIT_MIB * MEBIBYTE_BYTES;
|
|
1091
|
+
async function loadKnownHostEntries() {
|
|
1092
|
+
const filePath = join(homedir(), ".ssh", "known_hosts");
|
|
1066
1093
|
try {
|
|
1067
|
-
|
|
1068
|
-
|
|
1094
|
+
const stats = await stat2(filePath);
|
|
1095
|
+
if (stats.size > KNOWN_HOSTS_FILE_BYTE_LIMIT) {
|
|
1096
|
+
throw new HostKeyVerificationError(
|
|
1097
|
+
`Refusing to read known_hosts at ${filePath}: size ${stats.size} bytes exceeds the ${KNOWN_HOSTS_FILE_BYTE_LIMIT}-byte cap`
|
|
1098
|
+
);
|
|
1069
1099
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
1076
|
-
normalized[key] = normalizeCausePropertyValue(value, secretList, seen);
|
|
1100
|
+
return parseKnownHosts(await readFile2(filePath, "utf8"));
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
if (error instanceof HostKeyVerificationError) throw error;
|
|
1103
|
+
if (getFileSystemErrorCode(error) === "ENOENT") {
|
|
1104
|
+
return [];
|
|
1077
1105
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1106
|
+
throw new HostKeyVerificationError(
|
|
1107
|
+
`Could not read known_hosts at ${filePath}: ${String(error)}`
|
|
1108
|
+
);
|
|
1081
1109
|
}
|
|
1082
1110
|
}
|
|
1083
|
-
function
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1111
|
+
function formatAcceptedHostKeyWarning(host, key) {
|
|
1112
|
+
try {
|
|
1113
|
+
const algo = extractAlgoFromKey(key);
|
|
1114
|
+
const fingerprint = computeFingerprint(key);
|
|
1115
|
+
return `WARNING: Permanently added '${host}' (${algo}) to the list of known hosts. Fingerprint: ${fingerprint}
|
|
1116
|
+
`;
|
|
1117
|
+
} catch {
|
|
1118
|
+
return `WARNING: Permanently added '${host}' to the list of known hosts.
|
|
1119
|
+
`;
|
|
1120
|
+
}
|
|
1091
1121
|
}
|
|
1092
|
-
function
|
|
1093
|
-
|
|
1122
|
+
function writeHostKeyPersistFallbackWarning(host, port, error) {
|
|
1123
|
+
const keyscanArguments = port === DEFAULT_SSH_PORT ? shellQuote(host) : `-p ${port} ${shellQuote(host)}`;
|
|
1124
|
+
process.stderr.write(
|
|
1125
|
+
`WARNING: Could not persist host key for ${host} \u2014 the key is cached in memory for this session. To persist it, ensure ~/.ssh/ is writable or run: ssh-keyscan ${keyscanArguments} >> ~/.ssh/known_hosts. ${String(error)}
|
|
1126
|
+
`
|
|
1127
|
+
);
|
|
1094
1128
|
}
|
|
1095
|
-
function
|
|
1096
|
-
|
|
1129
|
+
async function acceptAndPersistHostKey(location, key, cache) {
|
|
1130
|
+
const { host, port } = location;
|
|
1131
|
+
const acceptedWarning = formatAcceptedHostKeyWarning(host, key);
|
|
1132
|
+
try {
|
|
1133
|
+
await appendHostKey(host, port, key);
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
if (error instanceof HostKeyVerificationError || error instanceof KnownHostsValidationError) {
|
|
1136
|
+
throw error;
|
|
1137
|
+
}
|
|
1138
|
+
cache.set(formatHostNeedle(host, port), key);
|
|
1139
|
+
process.stderr.write(acceptedWarning);
|
|
1140
|
+
writeHostKeyPersistFallbackWarning(host, port, error);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
cache.set(formatHostNeedle(host, port), key);
|
|
1144
|
+
process.stderr.write(acceptedWarning);
|
|
1097
1145
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1146
|
+
var PINNED_HOST_KEY_ALGORITHM_PATTERN = new RegExp("^(?:ssh-ed25519|ssh-rsa|ecdsa-sha2-(?:nistp256|nistp384|nistp521))$", "v");
|
|
1147
|
+
function normalizePinnedPublicKey(publicKey) {
|
|
1148
|
+
const parts = publicKey.trim().split(new RegExp("\\s+", "v"));
|
|
1149
|
+
if (parts.length < 2) {
|
|
1150
|
+
throw new Error("Expected host public key must use the format '<algorithm> <base64>'");
|
|
1151
|
+
}
|
|
1152
|
+
const [algorithm, key] = parts;
|
|
1153
|
+
if (!PINNED_HOST_KEY_ALGORITHM_PATTERN.test(algorithm)) {
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
`Expected host public key uses unsupported algorithm '${algorithm}'. Supported algorithms: ssh-ed25519, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521.`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
if (!STRICT_BASE64_PATTERN.test(key)) {
|
|
1159
|
+
throw new Error(
|
|
1160
|
+
"Expected host public key contains invalid base64 in the key field. Use the exact value from `ssh-keygen -y` or the second field of an OpenSSH known_hosts line."
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
return `${algorithm} ${key}`;
|
|
1101
1164
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
var OUTPUT_INDENT_UNIT = " ";
|
|
1110
|
-
var SPINNER_FRAME_INTERVAL_MS = 80;
|
|
1111
|
-
var ASCII_ESC = 27;
|
|
1112
|
-
var ANSI_HIDE_CURSOR = `${String.fromCharCode(ASCII_ESC)}[?25l`;
|
|
1113
|
-
var ANSI_SHOW_CURSOR = `${String.fromCharCode(ASCII_ESC)}[?25h`;
|
|
1114
|
-
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1115
|
-
var STATUS_ICONS = {
|
|
1116
|
-
changed: pc.yellow("\u21BA"),
|
|
1117
|
-
failed: pc.red("\u2717"),
|
|
1118
|
-
ok: pc.green("\u2713"),
|
|
1119
|
-
skipped: pc.dim("\u2298"),
|
|
1120
|
-
waiting: pc.cyan("\u23F8")
|
|
1121
|
-
};
|
|
1122
|
-
var CLI_HEADER_LINES = [
|
|
1123
|
-
" _ _ ",
|
|
1124
|
-
" | | (_) ",
|
|
1125
|
-
" _ __ __ _ _ __ __ _| |_ ___ __",
|
|
1126
|
-
" | '_ \\ / _` | '__/ _` | __| \\ \\/ /",
|
|
1127
|
-
" | |_) | (_| | | | (_| | |_| |> < ",
|
|
1128
|
-
" | .__/ \\__,_|_| \\__,_|\\__|_/_/\\_\\",
|
|
1129
|
-
" | | ",
|
|
1130
|
-
" |_| "
|
|
1131
|
-
];
|
|
1132
|
-
var LIVE_OUTPUT_STATE_KEY = /* @__PURE__ */ Symbol.for("paratix.output.liveState");
|
|
1133
|
-
function getSharedLiveOutputState() {
|
|
1134
|
-
const registry = globalThis;
|
|
1135
|
-
const existing = registry[LIVE_OUTPUT_STATE_KEY];
|
|
1136
|
-
if (existing != null) return existing;
|
|
1137
|
-
const created = {
|
|
1138
|
-
activeRecipeGuideDepths: [],
|
|
1139
|
-
activeSpinner: null,
|
|
1140
|
-
cursorHidden: false,
|
|
1141
|
-
pendingRecipeClosureGuideDepths: [],
|
|
1142
|
-
recipeOutputDepth: -1
|
|
1143
|
-
};
|
|
1144
|
-
registry[LIVE_OUTPUT_STATE_KEY] = created;
|
|
1145
|
-
return created;
|
|
1146
|
-
}
|
|
1147
|
-
var liveOutputState = getSharedLiveOutputState();
|
|
1148
|
-
function renderCliHeader(version) {
|
|
1149
|
-
const versionText = pc.dim(`v${version}`);
|
|
1150
|
-
return `${pc.cyan(CLI_HEADER_LINES.join("\n"))}${versionText}
|
|
1151
|
-
`;
|
|
1152
|
-
}
|
|
1153
|
-
function printCliHeader(version) {
|
|
1154
|
-
console.log(renderCliHeader(version));
|
|
1165
|
+
function validateExpectedHostPublicKey(publicKey) {
|
|
1166
|
+
try {
|
|
1167
|
+
normalizePinnedPublicKey(publicKey);
|
|
1168
|
+
return null;
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
return error instanceof Error ? error.message : String(error);
|
|
1171
|
+
}
|
|
1155
1172
|
}
|
|
1156
|
-
function
|
|
1157
|
-
|
|
1173
|
+
function formatPresentedPublicKey(key) {
|
|
1174
|
+
try {
|
|
1175
|
+
return `${extractAlgoFromKey(key)} ${key.toString("base64")}`;
|
|
1176
|
+
} catch {
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1158
1179
|
}
|
|
1159
|
-
function
|
|
1160
|
-
return
|
|
1180
|
+
function hasPinnedHostTrustAnchor(options) {
|
|
1181
|
+
return options?.expectedHostFingerprint != null || options?.expectedHostPublicKey != null;
|
|
1161
1182
|
}
|
|
1162
|
-
function
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
return pc.green(status);
|
|
1172
|
-
}
|
|
1173
|
-
case "skipped": {
|
|
1174
|
-
return pc.dim(status);
|
|
1175
|
-
}
|
|
1176
|
-
case "waiting": {
|
|
1177
|
-
return pc.cyan("running");
|
|
1178
|
-
}
|
|
1183
|
+
function verifyPinnedHostKey(host, key, options) {
|
|
1184
|
+
const normalizedExpectedPublicKey = options.expectedHostPublicKey == null ? null : normalizePinnedPublicKey(options.expectedHostPublicKey);
|
|
1185
|
+
const expectedFingerprint = options.expectedHostFingerprint ?? null;
|
|
1186
|
+
const presentedPublicKey = formatPresentedPublicKey(key);
|
|
1187
|
+
const presentedFingerprint = computeFingerprint(key);
|
|
1188
|
+
const publicKeyMatches = normalizedExpectedPublicKey == null || presentedPublicKey != null && normalizedExpectedPublicKey === presentedPublicKey;
|
|
1189
|
+
const fingerprintMatches = expectedFingerprint == null || expectedFingerprint === presentedFingerprint;
|
|
1190
|
+
if (publicKeyMatches && fingerprintMatches) {
|
|
1191
|
+
return;
|
|
1179
1192
|
}
|
|
1193
|
+
throw new HostKeyVerificationError(
|
|
1194
|
+
`HOST KEY VERIFICATION FAILED for ${host}: the remote host key does not match the configured trust anchor.`
|
|
1195
|
+
);
|
|
1180
1196
|
}
|
|
1181
|
-
function
|
|
1182
|
-
|
|
1197
|
+
async function buildHostVerifier(mode, location, options = {}) {
|
|
1198
|
+
const cache = options.cache ?? defaultHostKeyCache;
|
|
1199
|
+
const { host, port } = location;
|
|
1200
|
+
if (mode === "no" && !hasPinnedHostTrustAnchor(options)) return {};
|
|
1201
|
+
const entries = await withKnownHostsLock(loadKnownHostEntries);
|
|
1202
|
+
const fileEntries = findMatchingEntries(entries, host, port);
|
|
1203
|
+
const cachedKey = cache.get(formatHostNeedle(host, port)) ?? null;
|
|
1204
|
+
let acceptedHostKey = null;
|
|
1205
|
+
const result = {
|
|
1206
|
+
async commitAcceptedHostKey() {
|
|
1207
|
+
if (acceptedHostKey != null) await acceptAndPersistHostKey(location, acceptedHostKey, cache);
|
|
1208
|
+
},
|
|
1209
|
+
hostVerifier(key) {
|
|
1210
|
+
if (mode !== "no" && verifyHostKeyAgainstKnownEntries({ cachedKey, fileEntries, host, key })) {
|
|
1211
|
+
if (hasPinnedHostTrustAnchor(options)) verifyPinnedHostKey(host, key, options);
|
|
1212
|
+
return true;
|
|
1213
|
+
}
|
|
1214
|
+
if (hasPinnedHostTrustAnchor(options)) {
|
|
1215
|
+
verifyPinnedHostKey(host, key, options);
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
if (mode === "yes") {
|
|
1219
|
+
throw new HostKeyVerificationError(
|
|
1220
|
+
`Host key for ${host} not found in known_hosts. Set strictHostKeyChecking to "accept-new" for explicit TOFU or configure ssh.expectedHostFingerprint / ssh.expectedHostPublicKey.`
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
if (mode === "no") return true;
|
|
1224
|
+
acceptedHostKey = Buffer.from(key);
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
return result;
|
|
1183
1229
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1230
|
+
|
|
1231
|
+
// src/serverDefinitionValidation.ts
|
|
1232
|
+
var STRICT_HOST_KEY_ERROR = `Invalid property 'ssh.strictHostKeyChecking' (expected "accept-new", "no", or "yes")`;
|
|
1233
|
+
var MAX_TCP_PORT = 65535;
|
|
1234
|
+
function describeType(value) {
|
|
1235
|
+
return value === null ? "null" : typeof value;
|
|
1186
1236
|
}
|
|
1187
|
-
function
|
|
1188
|
-
|
|
1189
|
-
const guideDepths = [
|
|
1190
|
-
...options?.activeGuideDepths ?? liveOutputState.activeRecipeGuideDepths,
|
|
1191
|
-
...options?.extraGuideDepths ?? []
|
|
1192
|
-
];
|
|
1193
|
-
for (const guideDepth of guideDepths) {
|
|
1194
|
-
const guideCharacterIndex = OUTPUT_INDENT_UNIT.length * (guideDepth + 1);
|
|
1195
|
-
if (guideCharacterIndex >= indentCharacters.length) continue;
|
|
1196
|
-
indentCharacters[guideCharacterIndex] = options?.colorize === false ? "\xB7" : getGuideDot(guideDepth);
|
|
1197
|
-
}
|
|
1198
|
-
return indentCharacters.join("");
|
|
1237
|
+
function isHostKeyMode(value) {
|
|
1238
|
+
return value === "accept-new" || value === "no" || value === "yes";
|
|
1199
1239
|
}
|
|
1200
|
-
function
|
|
1201
|
-
|
|
1240
|
+
function isRecord(value) {
|
|
1241
|
+
return Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null;
|
|
1202
1242
|
}
|
|
1203
|
-
function
|
|
1204
|
-
if (
|
|
1205
|
-
|
|
1243
|
+
function collectOptionalBooleanErrors(object, key, errors) {
|
|
1244
|
+
if (!(key in object) || object[key] == null) return;
|
|
1245
|
+
if (typeof object[key] !== "boolean") {
|
|
1246
|
+
errors.push(
|
|
1247
|
+
`Invalid property 'ssh.${key}' (expected boolean, got ${describeType(object[key])})`
|
|
1248
|
+
);
|
|
1206
1249
|
}
|
|
1207
|
-
return OUTPUT_INDENT_UNIT.repeat(getCurrentOutputDepth() + 2);
|
|
1208
1250
|
}
|
|
1209
|
-
function
|
|
1210
|
-
|
|
1211
|
-
|
|
1251
|
+
function collectOptionalNumberErrors(parameters) {
|
|
1252
|
+
const { errors, key, object, options } = parameters;
|
|
1253
|
+
if (!(key in object) || object[key] == null) return;
|
|
1254
|
+
const value = object[key];
|
|
1255
|
+
const validatedNumber = validateNumberValue(value);
|
|
1256
|
+
if (validatedNumber == null) {
|
|
1257
|
+
errors.push(`Invalid property 'ssh.${key}' (expected number, got ${describeType(value)})`);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (options?.integer === true && !Number.isInteger(validatedNumber)) {
|
|
1261
|
+
errors.push(`Property 'ssh.${key}' must be an integer`);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (options?.positive === true && validatedNumber <= 0) {
|
|
1265
|
+
errors.push(`Property 'ssh.${key}' must be greater than 0`);
|
|
1266
|
+
}
|
|
1212
1267
|
}
|
|
1213
|
-
function
|
|
1214
|
-
return
|
|
1268
|
+
function validateNumberValue(value) {
|
|
1269
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1215
1270
|
}
|
|
1216
|
-
function
|
|
1217
|
-
return
|
|
1271
|
+
function isValidTcpPort(value) {
|
|
1272
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_TCP_PORT;
|
|
1218
1273
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1274
|
+
function collectOptionalStringErrors(object, key, errors) {
|
|
1275
|
+
if (!(key in object) || object[key] == null) return;
|
|
1276
|
+
if (typeof object[key] !== "string") {
|
|
1277
|
+
errors.push(`Invalid property 'ssh.${key}' (expected string, got ${describeType(object[key])})`);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
if (object[key].length === 0) {
|
|
1281
|
+
errors.push(`Property 'ssh.${key}' must not be an empty string`);
|
|
1227
1282
|
}
|
|
1228
1283
|
}
|
|
1229
|
-
function
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1284
|
+
function collectPortsErrors(ssh, errors) {
|
|
1285
|
+
if (!("ports" in ssh)) {
|
|
1286
|
+
errors.push("Missing property 'ssh.ports' (expected array)");
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (!Array.isArray(ssh.ports)) {
|
|
1290
|
+
errors.push(`Invalid property 'ssh.ports' (expected array, got ${describeType(ssh.ports)})`);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (ssh.ports.length === 0) {
|
|
1294
|
+
errors.push("Property 'ssh.ports' must not be empty");
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
for (const [index, port] of ssh.ports.entries()) {
|
|
1298
|
+
if (!isValidTcpPort(port)) {
|
|
1299
|
+
errors.push(`Property 'ssh.ports[${index}]' must be an integer between 1 and 65535`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1238
1302
|
}
|
|
1239
|
-
function
|
|
1240
|
-
if (
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1303
|
+
function collectRequiredUserErrors(ssh, errors) {
|
|
1304
|
+
if (!("user" in ssh)) {
|
|
1305
|
+
errors.push("Missing property 'ssh.user' (expected string)");
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (typeof ssh.user !== "string") {
|
|
1309
|
+
errors.push(`Invalid property 'ssh.user' (expected string, got ${describeType(ssh.user)})`);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (ssh.user.length === 0) {
|
|
1313
|
+
errors.push("Property 'ssh.user' must not be an empty string");
|
|
1314
|
+
}
|
|
1244
1315
|
}
|
|
1245
|
-
function
|
|
1246
|
-
if (!
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
function writeAnimatedModuleLine(line) {
|
|
1251
|
-
hideCursor();
|
|
1252
|
-
process.stdout.clearLine(0);
|
|
1253
|
-
process.stdout.cursorTo(0);
|
|
1254
|
-
process.stdout.write(fitAnimatedModuleLine(line, process.stdout.columns));
|
|
1255
|
-
}
|
|
1256
|
-
function stopAnimatedModuleLine(clearCurrentLine = false) {
|
|
1257
|
-
showCursor();
|
|
1258
|
-
if (liveOutputState.activeSpinner == null) return;
|
|
1259
|
-
clearInterval(liveOutputState.activeSpinner.interval);
|
|
1260
|
-
liveOutputState.activeSpinner = null;
|
|
1261
|
-
if (clearCurrentLine && supportsAnimatedModuleOutput()) {
|
|
1262
|
-
process.stdout.clearLine(0);
|
|
1263
|
-
process.stdout.cursorTo(0);
|
|
1316
|
+
function collectStrictHostKeyCheckingErrors(ssh, errors) {
|
|
1317
|
+
if (!("strictHostKeyChecking" in ssh) || ssh.strictHostKeyChecking == null) return;
|
|
1318
|
+
if (typeof ssh.strictHostKeyChecking !== "string" || !isHostKeyMode(ssh.strictHostKeyChecking)) {
|
|
1319
|
+
errors.push(STRICT_HOST_KEY_ERROR);
|
|
1264
1320
|
}
|
|
1265
1321
|
}
|
|
1266
|
-
function
|
|
1267
|
-
|
|
1322
|
+
function collectExpectedHostPublicKeyErrors(ssh, errors) {
|
|
1323
|
+
if (typeof ssh.expectedHostPublicKey !== "string") return;
|
|
1324
|
+
const error = validateExpectedHostPublicKey(ssh.expectedHostPublicKey);
|
|
1325
|
+
if (error != null) errors.push(`Invalid property 'ssh.expectedHostPublicKey': ${error}`);
|
|
1268
1326
|
}
|
|
1269
|
-
function
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1327
|
+
function collectOptionalSshFieldErrors(ssh, errors) {
|
|
1328
|
+
collectOptionalStringErrors(ssh, "privateKey", errors);
|
|
1329
|
+
collectOptionalStringErrors(ssh, "sudoPassword", errors);
|
|
1330
|
+
collectOptionalStringErrors(ssh, "expectedHostFingerprint", errors);
|
|
1331
|
+
collectOptionalStringErrors(ssh, "expectedHostPublicKey", errors);
|
|
1332
|
+
collectExpectedHostPublicKeyErrors(ssh, errors);
|
|
1333
|
+
collectOptionalBooleanErrors(ssh, "agentForward", errors);
|
|
1334
|
+
collectOptionalBooleanErrors(ssh, "passwordFallback", errors);
|
|
1335
|
+
collectOptionalNumberErrors({
|
|
1336
|
+
errors,
|
|
1337
|
+
key: "reconnectTimeout",
|
|
1338
|
+
object: ssh,
|
|
1339
|
+
options: { positive: true }
|
|
1281
1340
|
});
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
detail: spinner.detail,
|
|
1290
|
-
name: displayModule.name,
|
|
1291
|
-
status: "waiting",
|
|
1292
|
-
waitingFrame: SPINNER_FRAMES[spinner.frameIndex]
|
|
1293
|
-
})
|
|
1294
|
-
);
|
|
1295
|
-
}, SPINNER_FRAME_INTERVAL_MS)
|
|
1296
|
-
};
|
|
1297
|
-
if (typeof spinner.interval.unref === "function") spinner.interval.unref();
|
|
1298
|
-
liveOutputState.activeSpinner = spinner;
|
|
1299
|
-
writeAnimatedModuleLine(
|
|
1300
|
-
renderModuleLine({
|
|
1301
|
-
detail: displayModule.detail,
|
|
1302
|
-
name: displayModule.name,
|
|
1303
|
-
status: "waiting",
|
|
1304
|
-
waitingFrame: SPINNER_FRAMES[0]
|
|
1305
|
-
})
|
|
1306
|
-
);
|
|
1341
|
+
collectOptionalNumberErrors({
|
|
1342
|
+
errors,
|
|
1343
|
+
key: "maxReconnectAttempts",
|
|
1344
|
+
object: ssh,
|
|
1345
|
+
options: { integer: true, positive: true }
|
|
1346
|
+
});
|
|
1347
|
+
collectStrictHostKeyCheckingErrors(ssh, errors);
|
|
1307
1348
|
}
|
|
1308
|
-
function
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1349
|
+
function collectSshConfigErrors(value) {
|
|
1350
|
+
const errors = [];
|
|
1351
|
+
if (value === null) {
|
|
1352
|
+
errors.push("Invalid property 'ssh' (expected object, got null)");
|
|
1353
|
+
return errors;
|
|
1354
|
+
}
|
|
1355
|
+
if (typeof value !== "object") {
|
|
1356
|
+
errors.push(`Invalid property 'ssh' (expected object, got ${describeType(value)})`);
|
|
1357
|
+
return errors;
|
|
1358
|
+
}
|
|
1359
|
+
if (!isRecord(value)) {
|
|
1360
|
+
errors.push("Invalid property 'ssh' (expected object, got object)");
|
|
1361
|
+
return errors;
|
|
1315
1362
|
}
|
|
1363
|
+
const ssh = value;
|
|
1364
|
+
collectPortsErrors(ssh, errors);
|
|
1365
|
+
collectRequiredUserErrors(ssh, errors);
|
|
1366
|
+
collectOptionalSshFieldErrors(ssh, errors);
|
|
1367
|
+
return errors;
|
|
1316
1368
|
}
|
|
1317
|
-
function
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1369
|
+
function normalizeServerDefinitionSshError(error) {
|
|
1370
|
+
const mappedErrors = {
|
|
1371
|
+
"Missing property 'ssh.user' (expected string)": "ssh.user is required",
|
|
1372
|
+
"Property 'ssh.expectedHostFingerprint' must not be an empty string": "ssh.expectedHostFingerprint must not be an empty string",
|
|
1373
|
+
"Property 'ssh.expectedHostPublicKey' must not be an empty string": "ssh.expectedHostPublicKey must not be an empty string",
|
|
1374
|
+
"Property 'ssh.ports' must not be empty": "ssh.ports must not be empty",
|
|
1375
|
+
"Property 'ssh.privateKey' must not be an empty string": "ssh.privateKey must not be an empty string",
|
|
1376
|
+
"Property 'ssh.user' must not be an empty string": "ssh.user is required",
|
|
1377
|
+
[STRICT_HOST_KEY_ERROR]: "ssh.strictHostKeyChecking must be one of accept-new, no, yes"
|
|
1378
|
+
};
|
|
1379
|
+
return mappedErrors[error] ?? error;
|
|
1323
1380
|
}
|
|
1324
|
-
function
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
if (line.startsWith("+")) return pc.green(line);
|
|
1330
|
-
return pc.dim(line);
|
|
1381
|
+
function validateSshConfig(ssh) {
|
|
1382
|
+
const errors = collectSshConfigErrors(ssh);
|
|
1383
|
+
if (errors.length === 0) return;
|
|
1384
|
+
const message = errors.map((error) => normalizeServerDefinitionSshError(error)).join("; ");
|
|
1385
|
+
throw new Error(`ServerDefinition: ${message}`);
|
|
1331
1386
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1387
|
+
|
|
1388
|
+
// src/meta.ts
|
|
1389
|
+
var SYSTEM_HOST_KIND = "system.host";
|
|
1390
|
+
var SYSTEM_REBOOT_KIND = "system.reboot";
|
|
1391
|
+
function hasValidMetaName(name) {
|
|
1392
|
+
return typeof name === "string" && name.length > 0;
|
|
1336
1393
|
}
|
|
1337
|
-
function
|
|
1338
|
-
|
|
1339
|
-
if (via === "stdout") {
|
|
1340
|
-
process.stdout.write(`${composed}
|
|
1341
|
-
`);
|
|
1342
|
-
} else {
|
|
1343
|
-
console.log(composed);
|
|
1344
|
-
}
|
|
1394
|
+
function isMetaValueType(value) {
|
|
1395
|
+
return value === "boolean" || value === "number" || value === "string";
|
|
1345
1396
|
}
|
|
1346
|
-
function
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1397
|
+
function validateResolvedMetaValue(parameters) {
|
|
1398
|
+
const { actualType, declaredValueType, enforceDeclaredValueType, entryKey } = parameters;
|
|
1399
|
+
if (!isMetaValueType(actualType)) {
|
|
1400
|
+
throw new TypeError(
|
|
1401
|
+
`Env meta entry ${JSON.stringify(entryKey)} resolved to typeof ${actualType}, expected boolean, number, or string`
|
|
1402
|
+
);
|
|
1352
1403
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
const maskedDetail = parameters.detail == null ? void 0 : maskRegisteredSecrets(parameters.detail);
|
|
1358
|
-
const displayModule = formatDisplayModule({
|
|
1359
|
-
continuationIndentWidth: `${buildGuideIndent(
|
|
1360
|
-
OUTPUT_INDENT_UNIT.repeat(Math.max(getCurrentOutputDepth() + 2, 1)),
|
|
1361
|
-
{ extraGuideDepths }
|
|
1362
|
-
)} `.length,
|
|
1363
|
-
detail: maskedDetail,
|
|
1364
|
-
name: maskedName,
|
|
1365
|
-
status: parameters.status,
|
|
1366
|
-
terminalColumns: process.stdout.columns
|
|
1367
|
-
});
|
|
1368
|
-
const line = renderModuleLine({
|
|
1369
|
-
detail: displayModule.detail,
|
|
1370
|
-
extraGuideDepths,
|
|
1371
|
-
name: displayModule.name,
|
|
1372
|
-
status: parameters.status
|
|
1373
|
-
});
|
|
1374
|
-
const diffLines = parameters.diff == null ? [] : renderDiffLines(parameters.diff);
|
|
1375
|
-
const usesSpinner = supportsAnimatedModuleOutput() && liveOutputState.activeSpinner != null;
|
|
1376
|
-
if (usesSpinner) {
|
|
1377
|
-
stopAnimatedModuleLine();
|
|
1378
|
-
writeAnimatedModuleLine(line);
|
|
1379
|
-
process.stdout.write("\n");
|
|
1380
|
-
} else {
|
|
1381
|
-
console.log(line);
|
|
1404
|
+
if (enforceDeclaredValueType && actualType !== declaredValueType) {
|
|
1405
|
+
throw new TypeError(
|
|
1406
|
+
`Env meta entry ${JSON.stringify(entryKey)} resolved to typeof ${actualType}, expected ${declaredValueType}`
|
|
1407
|
+
);
|
|
1382
1408
|
}
|
|
1383
|
-
writeContinuationBlock({
|
|
1384
|
-
detailLines: displayModule.detailLines,
|
|
1385
|
-
diffLines,
|
|
1386
|
-
extraGuideDepths,
|
|
1387
|
-
via: usesSpinner ? "stdout" : "console"
|
|
1388
|
-
});
|
|
1389
1409
|
}
|
|
1390
|
-
function
|
|
1391
|
-
|
|
1392
|
-
printRenderedModuleResult({ detail, diff, name, status });
|
|
1410
|
+
function isRecord2(value) {
|
|
1411
|
+
return typeof value === "object" && value !== null;
|
|
1393
1412
|
}
|
|
1394
|
-
function
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
const lines = [];
|
|
1398
|
-
if (maskedStderr.trim()) {
|
|
1399
|
-
lines.push(...maskedStderr.trim().split("\n"));
|
|
1413
|
+
function assertValidEnvironmentMetaEntry(candidate) {
|
|
1414
|
+
if (!hasValidMetaName(candidate.name)) {
|
|
1415
|
+
throw new TypeError("Invalid env meta entry: name must be a non-empty string");
|
|
1400
1416
|
}
|
|
1401
|
-
if (
|
|
1402
|
-
|
|
1417
|
+
if (typeof candidate.resolve !== "function") {
|
|
1418
|
+
throw new TypeError("Invalid env meta entry: resolve must be a function returning a Promise");
|
|
1403
1419
|
}
|
|
1404
|
-
if (
|
|
1405
|
-
|
|
1406
|
-
for (const line of lines) {
|
|
1407
|
-
console.error(pc.red(`${getErrorIndent()}${line}`));
|
|
1408
|
-
}
|
|
1420
|
+
if (candidate.valueType !== "boolean" && candidate.valueType !== "number" && candidate.valueType !== "string") {
|
|
1421
|
+
throw new TypeError("Invalid env meta entry: valueType must be boolean, number, or string");
|
|
1409
1422
|
}
|
|
1410
1423
|
}
|
|
1411
|
-
function
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
if (maskedStderr.trim()) {
|
|
1415
|
-
console.error(pc.red(`${getErrorIndent()}Full stderr:`));
|
|
1416
|
-
for (const line of maskedStderr.trim().split("\n")) {
|
|
1417
|
-
console.error(pc.red(`${getErrorIndent()}${line}`));
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
if (maskedStdout.trim()) {
|
|
1421
|
-
console.error(pc.red(`${getErrorIndent()}Full stdout:`));
|
|
1422
|
-
for (const line of maskedStdout.trim().split("\n")) {
|
|
1423
|
-
console.error(pc.red(`${getErrorIndent()}${line}`));
|
|
1424
|
-
}
|
|
1424
|
+
function assertValidSshdPortMetaEntry(candidate) {
|
|
1425
|
+
if (!isValidTcpPort(candidate.port)) {
|
|
1426
|
+
throw new TypeError("Invalid sshd.port meta entry: port must be an integer between 1 and 65535");
|
|
1425
1427
|
}
|
|
1426
1428
|
}
|
|
1427
|
-
function
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
console.error(pc.red(`${getErrorIndent()}${line}`));
|
|
1429
|
+
function assertValidSystemHostMetaEntry(candidate) {
|
|
1430
|
+
const hostValidationFailure = validateHostLabel(candidate.host);
|
|
1431
|
+
if (hostValidationFailure != null) {
|
|
1432
|
+
throw new TypeError(
|
|
1433
|
+
`Invalid system.host meta entry: host ${describeHostValidationFailure(hostValidationFailure)}`
|
|
1434
|
+
);
|
|
1434
1435
|
}
|
|
1435
1436
|
}
|
|
1436
|
-
function
|
|
1437
|
-
return
|
|
1437
|
+
function isEnvironmentMetaEntry(entry) {
|
|
1438
|
+
return entry.kind === "env";
|
|
1438
1439
|
}
|
|
1439
|
-
function
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1440
|
+
function isSshdPortMetaEntry(entry) {
|
|
1441
|
+
return entry.kind === "sshd.port";
|
|
1442
|
+
}
|
|
1443
|
+
function isSystemHostMetaEntry(entry) {
|
|
1444
|
+
return entry.kind === SYSTEM_HOST_KIND;
|
|
1445
|
+
}
|
|
1446
|
+
function isSystemRebootMetaEntry(entry) {
|
|
1447
|
+
return entry.kind === SYSTEM_REBOOT_KIND;
|
|
1448
|
+
}
|
|
1449
|
+
function assertValidModuleMetaEntry(entry) {
|
|
1450
|
+
if (!isRecord2(entry)) {
|
|
1451
|
+
throw new TypeError(`Invalid meta entry: expected object, got ${typeof entry}`);
|
|
1452
|
+
}
|
|
1453
|
+
switch (entry.kind) {
|
|
1454
|
+
case "env": {
|
|
1455
|
+
assertValidEnvironmentMetaEntry(entry);
|
|
1444
1456
|
return;
|
|
1445
1457
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1458
|
+
case "sshd.port": {
|
|
1459
|
+
assertValidSshdPortMetaEntry(entry);
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
case SYSTEM_HOST_KIND: {
|
|
1463
|
+
assertValidSystemHostMetaEntry(entry);
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
case SYSTEM_REBOOT_KIND: {
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
default: {
|
|
1470
|
+
throw new TypeError(`Invalid meta entry kind: ${String(entry.kind)}`);
|
|
1453
1471
|
}
|
|
1454
|
-
return;
|
|
1455
1472
|
}
|
|
1456
|
-
printVerboseErrorBlock(label, maskRegisteredSecrets(formatCauseValue(cause)));
|
|
1457
1473
|
}
|
|
1458
|
-
function
|
|
1459
|
-
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
const cause = getErrorCause(error);
|
|
1463
|
-
if (cause !== void 0) {
|
|
1464
|
-
const visitedCauses = /* @__PURE__ */ new WeakSet();
|
|
1465
|
-
visitedCauses.add(error);
|
|
1466
|
-
printVerboseErrorCause(cause, 1, visitedCauses);
|
|
1474
|
+
function assertValidModuleMetaEntries(entries) {
|
|
1475
|
+
if (entries == null) return;
|
|
1476
|
+
for (const entry of entries) {
|
|
1477
|
+
assertValidModuleMetaEntry(entry);
|
|
1467
1478
|
}
|
|
1468
1479
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
});
|
|
1476
|
-
}
|
|
1477
|
-
function printCauseChain(error) {
|
|
1478
|
-
const visited = /* @__PURE__ */ new WeakSet();
|
|
1479
|
-
visited.add(error);
|
|
1480
|
-
let cause = getErrorCause(error);
|
|
1481
|
-
while (cause !== void 0) {
|
|
1482
|
-
if (cause instanceof Error) {
|
|
1483
|
-
if (visited.has(cause)) return;
|
|
1484
|
-
visited.add(cause);
|
|
1485
|
-
}
|
|
1486
|
-
console.error(pc.red(`${getErrorIndent()}Cause: ${maskRegisteredSecrets(formatCauseValue(cause))}`));
|
|
1487
|
-
if (cause instanceof CommandError) {
|
|
1488
|
-
printVerboseCommandError(
|
|
1489
|
-
maskRegisteredSecrets(cause.fullStdout),
|
|
1490
|
-
maskRegisteredSecrets(cause.fullStderr)
|
|
1491
|
-
);
|
|
1492
|
-
}
|
|
1493
|
-
cause = cause instanceof Error ? getErrorCause(cause) : void 0;
|
|
1480
|
+
var META_ENV_SEGMENT_PATTERN = new RegExp("^[A-Za-z_]\\w*$", "v");
|
|
1481
|
+
function isAllowedMetaEnvironmentName(name) {
|
|
1482
|
+
if (name.length === 0) return false;
|
|
1483
|
+
const segments = name.split(".");
|
|
1484
|
+
for (const segment of segments) {
|
|
1485
|
+
if (!META_ENV_SEGMENT_PATTERN.test(segment)) return false;
|
|
1494
1486
|
}
|
|
1487
|
+
return true;
|
|
1495
1488
|
}
|
|
1496
|
-
function
|
|
1497
|
-
if (
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
printVerboseCommandError(
|
|
1501
|
-
maskRegisteredSecrets(error.fullStdout),
|
|
1502
|
-
maskRegisteredSecrets(error.fullStderr)
|
|
1489
|
+
function assertAllowedEnvironmentMetaName(name) {
|
|
1490
|
+
if (!isAllowedMetaEnvironmentName(name)) {
|
|
1491
|
+
throw new Error(
|
|
1492
|
+
`Invalid env meta name ${JSON.stringify(name)}: expected [A-Za-z_]\\w*(\\.[A-Za-z_]\\w*)*`
|
|
1503
1493
|
);
|
|
1504
|
-
return;
|
|
1505
1494
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
}
|
|
1511
|
-
if (verbose) {
|
|
1512
|
-
printVerboseGenericError(error);
|
|
1513
|
-
}
|
|
1495
|
+
if (ENVIRONMENT_FORBIDDEN_KEYS.has(name)) {
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
`Forbidden env meta entry name: ${JSON.stringify(name)} (reserved JavaScript identifier)`
|
|
1498
|
+
);
|
|
1514
1499
|
}
|
|
1515
1500
|
}
|
|
1516
|
-
function
|
|
1517
|
-
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1501
|
+
function createMemoizedEnvironmentResolver(entry) {
|
|
1502
|
+
let cachedResolution = null;
|
|
1503
|
+
const declaredValueType = entry.valueType;
|
|
1504
|
+
const enforceDeclaredValueType = entry.valueTypeExplicit !== false;
|
|
1505
|
+
const entryKey = entry.name;
|
|
1506
|
+
return async () => {
|
|
1507
|
+
if (cachedResolution != null) return cachedResolution;
|
|
1508
|
+
const pending = entry.resolve().then((resolved) => {
|
|
1509
|
+
validateResolvedMetaValue({
|
|
1510
|
+
actualType: typeof resolved,
|
|
1511
|
+
declaredValueType,
|
|
1512
|
+
enforceDeclaredValueType,
|
|
1513
|
+
entryKey
|
|
1514
|
+
});
|
|
1515
|
+
return resolved;
|
|
1516
|
+
}).catch((error) => {
|
|
1517
|
+
cachedResolution = null;
|
|
1518
|
+
throw error;
|
|
1519
|
+
});
|
|
1520
|
+
cachedResolution = pending;
|
|
1521
|
+
return pending;
|
|
1522
|
+
};
|
|
1527
1523
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1524
|
+
async function mergeEnvironmentFromMeta(environment, entries) {
|
|
1525
|
+
if (entries == null || entries.length === 0) {
|
|
1526
|
+
await Promise.resolve();
|
|
1527
|
+
return environment;
|
|
1528
|
+
}
|
|
1529
|
+
const nextEnvironment = Object.assign(createNullPrototypeEnvironment(), environment);
|
|
1530
|
+
for (const entry of entries) {
|
|
1531
|
+
if (!isEnvironmentMetaEntry(entry)) continue;
|
|
1532
|
+
assertAllowedEnvironmentMetaName(entry.name);
|
|
1533
|
+
nextEnvironment[entry.name] = createMemoizedEnvironmentResolver(entry);
|
|
1534
|
+
}
|
|
1535
|
+
await Promise.resolve();
|
|
1536
|
+
return nextEnvironment;
|
|
1534
1537
|
}
|
|
1535
1538
|
|
|
1536
|
-
// src/
|
|
1537
|
-
import
|
|
1538
|
-
import { appendFile, mkdir, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
1539
|
-
import { homedir } from "os";
|
|
1540
|
-
import { join } from "path";
|
|
1539
|
+
// src/output.ts
|
|
1540
|
+
import pc from "picocolors";
|
|
1541
1541
|
|
|
1542
|
-
// src/
|
|
1543
|
-
import {
|
|
1544
|
-
var
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1542
|
+
// src/outputFormatting.ts
|
|
1543
|
+
import { stripVTControlCharacters } from "util";
|
|
1544
|
+
var PACKAGE_MODULE_SUFFIX_LENGTH = 2;
|
|
1545
|
+
var PACKAGE_COLUMN_GAP_WIDTH = 2;
|
|
1546
|
+
var DEFAULT_TERMINAL_COLUMNS = 100;
|
|
1547
|
+
var MIN_PACKAGE_COLUMNS_WIDTH = 24;
|
|
1548
|
+
var MIN_PACKAGE_COLUMN_WIDTH = 18;
|
|
1549
|
+
var MIN_ANIMATED_LINE_COLUMNS = 8;
|
|
1550
|
+
var ELAPSED_DISPLAY_THRESHOLD_MS = 1e3;
|
|
1551
|
+
var MILLISECONDS_PER_SECOND = 1e3;
|
|
1552
|
+
var MILLISECONDS_PER_MINUTE = 6e4;
|
|
1553
|
+
var SECONDS_PER_MINUTE = 60;
|
|
1554
|
+
function splitPackageModuleName(name) {
|
|
1555
|
+
for (const prefix of ["package.installed: ", "package.absent: "]) {
|
|
1556
|
+
if (!name.startsWith(prefix)) continue;
|
|
1557
|
+
const packages = name.slice(prefix.length).split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
1558
|
+
if (packages.length === 0) return null;
|
|
1559
|
+
return {
|
|
1560
|
+
packages,
|
|
1561
|
+
summaryName: prefix.slice(0, -PACKAGE_MODULE_SUFFIX_LENGTH)
|
|
1562
|
+
};
|
|
1555
1563
|
}
|
|
1556
|
-
return
|
|
1564
|
+
return null;
|
|
1557
1565
|
}
|
|
1558
|
-
function
|
|
1559
|
-
if (
|
|
1560
|
-
|
|
1566
|
+
function getPackageColumns(parameters) {
|
|
1567
|
+
if (parameters.packages.length <= 1) return parameters.packages;
|
|
1568
|
+
const availableWidth = Math.max(
|
|
1569
|
+
(parameters.terminalColumns ?? DEFAULT_TERMINAL_COLUMNS) - parameters.continuationIndentWidth,
|
|
1570
|
+
MIN_PACKAGE_COLUMNS_WIDTH
|
|
1571
|
+
);
|
|
1572
|
+
const widestPackage = Math.max(...parameters.packages.map((entry) => entry.length));
|
|
1573
|
+
const columnWidth = Math.max(widestPackage + PACKAGE_COLUMN_GAP_WIDTH, MIN_PACKAGE_COLUMN_WIDTH);
|
|
1574
|
+
const columnCount = Math.max(Math.floor(availableWidth / columnWidth), 1);
|
|
1575
|
+
const rowCount = Math.ceil(parameters.packages.length / columnCount);
|
|
1576
|
+
return Array.from(
|
|
1577
|
+
{ length: rowCount },
|
|
1578
|
+
(_row, rowIndex) => Array.from({ length: columnCount }, (_column, columnIndex) => {
|
|
1579
|
+
const packageIndex = rowIndex + columnIndex * rowCount;
|
|
1580
|
+
const packageName = parameters.packages.at(packageIndex);
|
|
1581
|
+
if (packageName == null) return "";
|
|
1582
|
+
const isLastVisibleColumn = columnIndex === columnCount - 1 || packageIndex + rowCount >= parameters.packages.length;
|
|
1583
|
+
return isLastVisibleColumn ? packageName : packageName.padEnd(columnWidth);
|
|
1584
|
+
}).filter(Boolean).join("")
|
|
1585
|
+
);
|
|
1561
1586
|
}
|
|
1562
|
-
function
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
if (parts.length !== HASHED_HOST_PARTS || parts[1] !== "1") return false;
|
|
1566
|
-
const salt = Buffer.from(parts[2] ?? "", "base64");
|
|
1567
|
-
const expectedHash = Buffer.from(parts[3] ?? "", "base64");
|
|
1568
|
-
if (salt.length === 0 || expectedHash.length === 0) return false;
|
|
1569
|
-
const actualHash = createHmac("sha1", salt).update(needle).digest();
|
|
1570
|
-
return actualHash.length === expectedHash.length && timingSafeEqual(actualHash, expectedHash);
|
|
1587
|
+
function getPackageSummaryDetail(packageCount, detail) {
|
|
1588
|
+
const packageLabel = packageCount === 1 ? "1 package" : `${packageCount} packages`;
|
|
1589
|
+
return detail == null ? packageLabel : `${packageLabel} \xB7 ${detail}`;
|
|
1571
1590
|
}
|
|
1572
|
-
function
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
if (nextState === null) return false;
|
|
1577
|
-
state = nextState;
|
|
1591
|
+
function formatModuleElapsed(elapsedMs) {
|
|
1592
|
+
if (!Number.isFinite(elapsedMs) || elapsedMs < ELAPSED_DISPLAY_THRESHOLD_MS) return void 0;
|
|
1593
|
+
if (elapsedMs < MILLISECONDS_PER_MINUTE) {
|
|
1594
|
+
return `${(elapsedMs / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
|
|
1578
1595
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1596
|
+
const totalSeconds = Math.floor(elapsedMs / MILLISECONDS_PER_SECOND);
|
|
1597
|
+
const minutes = Math.floor(totalSeconds / SECONDS_PER_MINUTE);
|
|
1598
|
+
const seconds = totalSeconds % SECONDS_PER_MINUTE;
|
|
1599
|
+
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
1581
1600
|
}
|
|
1582
|
-
function
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1601
|
+
function fitAnimatedModuleLine(line, columns) {
|
|
1602
|
+
if (columns === void 0 || columns < MIN_ANIMATED_LINE_COLUMNS) return line;
|
|
1603
|
+
const plainLine = stripVTControlCharacters(line);
|
|
1604
|
+
if (plainLine.length < columns) return line;
|
|
1605
|
+
return `${plainLine.slice(0, columns - 1)}\u2026`;
|
|
1606
|
+
}
|
|
1607
|
+
function formatDisplayModule(parameters) {
|
|
1608
|
+
const packageModule = splitPackageModuleName(parameters.name);
|
|
1609
|
+
if (packageModule == null) {
|
|
1588
1610
|
return {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
starNeedleIndex: state.needleIndex
|
|
1611
|
+
detail: parameters.detail,
|
|
1612
|
+
detailLines: [],
|
|
1613
|
+
name: parameters.name
|
|
1593
1614
|
};
|
|
1594
1615
|
}
|
|
1595
|
-
if (state.starIndex === -1) return null;
|
|
1596
|
-
const starNeedleIndex = state.starNeedleIndex + 1;
|
|
1597
1616
|
return {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1617
|
+
detail: getPackageSummaryDetail(packageModule.packages.length, parameters.detail),
|
|
1618
|
+
detailLines: parameters.status === "waiting" ? [] : getPackageColumns({
|
|
1619
|
+
continuationIndentWidth: parameters.continuationIndentWidth,
|
|
1620
|
+
packages: packageModule.packages,
|
|
1621
|
+
terminalColumns: parameters.terminalColumns
|
|
1622
|
+
}),
|
|
1623
|
+
name: packageModule.summaryName
|
|
1602
1624
|
};
|
|
1603
1625
|
}
|
|
1604
1626
|
|
|
1605
|
-
// src/
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
constructor(message) {
|
|
1615
|
-
super(message);
|
|
1616
|
-
this.name = "HostKeyVerificationError";
|
|
1617
|
-
Error.captureStackTrace(this, _HostKeyVerificationError);
|
|
1618
|
-
}
|
|
1619
|
-
};
|
|
1620
|
-
var KnownHostsValidationError = class _KnownHostsValidationError extends Error {
|
|
1621
|
-
constructor(message) {
|
|
1622
|
-
super(message);
|
|
1623
|
-
this.name = "KnownHostsValidationError";
|
|
1624
|
-
Error.captureStackTrace(this, _KnownHostsValidationError);
|
|
1627
|
+
// src/secretSink.ts
|
|
1628
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
1629
|
+
var secretCounts = /* @__PURE__ */ new Map();
|
|
1630
|
+
var runScopedSecretCounts = new AsyncLocalStorage2();
|
|
1631
|
+
var REDACTED_PLACEHOLDER = REDACTED_SECRET_FIELD_PLACEHOLDER;
|
|
1632
|
+
var CIRCULAR_PLACEHOLDER = "[Circular]";
|
|
1633
|
+
function assertRegistrableSecret(secret) {
|
|
1634
|
+
if (secret.includes(REDACTED_PLACEHOLDER)) {
|
|
1635
|
+
throw new Error("Secret registrations must not contain the redaction placeholder");
|
|
1625
1636
|
}
|
|
1626
|
-
};
|
|
1627
|
-
var MIN_KNOWN_HOSTS_FIELDS = 3;
|
|
1628
|
-
var DEFAULT_SSH_PORT = 22;
|
|
1629
|
-
var UINT32_SIZE = 4;
|
|
1630
|
-
var STRICT_BASE64_PATTERN = new RegExp("^[A-Za-z0-9+\\/]+=*$", "v");
|
|
1631
|
-
var CERT_AUTHORITY_MESSAGE = "HOST KEY VERIFICATION FAILED \u2014 known_hosts contains a @cert-authority entry, but paratix does not validate certificate-authority host keys. Remove the @cert-authority marker or pin the host with ssh.expectedHostFingerprint / ssh.expectedHostPublicKey instead.";
|
|
1632
|
-
var defaultHostKeyCache = /* @__PURE__ */ new Map();
|
|
1633
|
-
function createHostKeyCache() {
|
|
1634
|
-
return /* @__PURE__ */ new Map();
|
|
1635
1637
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
const hostsPart = parts[offset];
|
|
1642
|
-
const algo = parts[offset + 1];
|
|
1643
|
-
const base64Key = parts[offset + 2];
|
|
1644
|
-
if (!STRICT_BASE64_PATTERN.test(base64Key)) return [];
|
|
1645
|
-
const key = Buffer.from(base64Key, "base64");
|
|
1646
|
-
const hostPatterns = hostsPart.split(",");
|
|
1647
|
-
return hostPatterns.map((host) => ({ algo, host, hostPatterns, key, marker }));
|
|
1638
|
+
var MINIMUM_SECRET_LENGTH = 8;
|
|
1639
|
+
function registerSecret(secret) {
|
|
1640
|
+
if (secret.length < MINIMUM_SECRET_LENGTH) return;
|
|
1641
|
+
assertRegistrableSecret(secret);
|
|
1642
|
+
secretCounts.set(secret, (secretCounts.get(secret) ?? 0) + 1);
|
|
1648
1643
|
}
|
|
1649
|
-
function
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1644
|
+
function unregisterSecret(secret) {
|
|
1645
|
+
if (secret.length < MINIMUM_SECRET_LENGTH) return;
|
|
1646
|
+
const current = secretCounts.get(secret);
|
|
1647
|
+
if (current == null) return;
|
|
1648
|
+
if (current <= 1) {
|
|
1649
|
+
secretCounts.delete(secret);
|
|
1650
|
+
return;
|
|
1655
1651
|
}
|
|
1656
|
-
|
|
1657
|
-
}
|
|
1658
|
-
function formatHostNeedle(host, port) {
|
|
1659
|
-
return port === DEFAULT_SSH_PORT ? host : `[${host}]:${port}`;
|
|
1660
|
-
}
|
|
1661
|
-
function assertSafeHostForKnownHosts(host) {
|
|
1662
|
-
const hostValidationFailure = validateHostLabel(host);
|
|
1663
|
-
if (hostValidationFailure == null) return;
|
|
1664
|
-
throw new KnownHostsValidationError(
|
|
1665
|
-
`Refusing to persist known_hosts entry: host label ${describeHostValidationFailure(hostValidationFailure)}`
|
|
1666
|
-
);
|
|
1667
|
-
}
|
|
1668
|
-
function matchesKnownHostEntry(entry, needle) {
|
|
1669
|
-
return matchesKnownHostPatternList(entry.hostPatterns ?? [entry.host], needle);
|
|
1670
|
-
}
|
|
1671
|
-
function findMatchingEntries(entries, host, port) {
|
|
1672
|
-
const needle = formatHostNeedle(host, port);
|
|
1673
|
-
return entries.filter((entry) => matchesKnownHostEntry(entry, needle));
|
|
1674
|
-
}
|
|
1675
|
-
function findRevokedEntry(entries, key) {
|
|
1676
|
-
return entries.find(
|
|
1677
|
-
(entry) => entry.marker === "@revoked" && entry.key.length === key.length && timingSafeEqual2(entry.key, key)
|
|
1678
|
-
);
|
|
1679
|
-
}
|
|
1680
|
-
function throwHostKeyMismatch(host, presentedKey, existingKey) {
|
|
1681
|
-
const presentedAlgo = describeAlgoForDiagnostics(presentedKey);
|
|
1682
|
-
const knownHostsDetails = existingKey == null ? "remote host key does not match the key in known_hosts. " : `remote host key (${presentedAlgo}) does not match the key in known_hosts (${describeAlgoForDiagnostics(existingKey)}). `;
|
|
1683
|
-
throw new HostKeyVerificationError(
|
|
1684
|
-
`HOST KEY VERIFICATION FAILED for ${host}: ${knownHostsDetails}This could indicate a man-in-the-middle attack.`
|
|
1685
|
-
);
|
|
1652
|
+
secretCounts.set(secret, current - 1);
|
|
1686
1653
|
}
|
|
1687
|
-
function
|
|
1688
|
-
const
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
`HOST KEY VERIFICATION FAILED for ${host}: remote host key (${describeAlgoForDiagnostics(revokedKey.key)}) is marked as revoked in known_hosts.`
|
|
1693
|
-
);
|
|
1654
|
+
async function withRegisteredSecrets(secrets, body) {
|
|
1655
|
+
const registered = [];
|
|
1656
|
+
const registerableSecrets = secrets.filter((secret) => secret.length >= MINIMUM_SECRET_LENGTH);
|
|
1657
|
+
for (const secret of secrets) {
|
|
1658
|
+
assertRegistrableSecret(secret);
|
|
1694
1659
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1660
|
+
try {
|
|
1661
|
+
for (const secret of registerableSecrets) {
|
|
1662
|
+
registerSecret(secret);
|
|
1663
|
+
registered.push(secret);
|
|
1664
|
+
}
|
|
1665
|
+
const result = await body();
|
|
1666
|
+
return maskScopedResult(result, secrets);
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
throw maskScopedError(error, secrets);
|
|
1669
|
+
} finally {
|
|
1670
|
+
for (const secret of registered) {
|
|
1671
|
+
unregisterSecret(secret);
|
|
1672
|
+
}
|
|
1704
1673
|
}
|
|
1705
|
-
const firstRawHostKey = rawHostKeyEntries.at(0)?.key;
|
|
1706
|
-
if (firstRawHostKey != null) throwHostKeyMismatch(host, key, firstRawHostKey);
|
|
1707
|
-
if (cachedKey != null) throwHostKeyMismatch(host, key, cachedKey);
|
|
1708
|
-
return false;
|
|
1709
1674
|
}
|
|
1710
|
-
function
|
|
1711
|
-
if (
|
|
1712
|
-
|
|
1713
|
-
}
|
|
1714
|
-
const algoLength = keyBuffer.readUInt32BE(0);
|
|
1715
|
-
if (algoLength === 0 || UINT32_SIZE + algoLength > keyBuffer.length) {
|
|
1716
|
-
throw new Error("Invalid SSH key buffer: algorithm length exceeds buffer size");
|
|
1675
|
+
async function withRunScopedSecrets(body) {
|
|
1676
|
+
if (runScopedSecretCounts.getStore() != null) {
|
|
1677
|
+
return body();
|
|
1717
1678
|
}
|
|
1718
|
-
const
|
|
1719
|
-
|
|
1720
|
-
|
|
1679
|
+
const scopedSecrets = /* @__PURE__ */ new Map();
|
|
1680
|
+
try {
|
|
1681
|
+
return await runScopedSecretCounts.run(scopedSecrets, body);
|
|
1682
|
+
} finally {
|
|
1683
|
+
for (const [secret, count] of scopedSecrets) {
|
|
1684
|
+
for (let index = 0; index < count; index += 1) {
|
|
1685
|
+
unregisterSecret(secret);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1721
1688
|
}
|
|
1722
|
-
return algo;
|
|
1723
1689
|
}
|
|
1724
|
-
function
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1690
|
+
function maskScopedResult(result, secrets) {
|
|
1691
|
+
if (secrets.length === 0 || !isModuleResult(result)) return result;
|
|
1692
|
+
const secretList = [...secrets];
|
|
1693
|
+
let next = result;
|
|
1694
|
+
if (result.detail != null) {
|
|
1695
|
+
const maskedDetail = maskSecrets(result.detail, secretList);
|
|
1696
|
+
if (maskedDetail !== result.detail) {
|
|
1697
|
+
next = { ...result, detail: maskedDetail };
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
if (isModuleResult(next) && next.status === "failed" && next.error != null) {
|
|
1701
|
+
next = { ...next, error: maskScopedError(next.error, secrets) };
|
|
1729
1702
|
}
|
|
1703
|
+
return next;
|
|
1730
1704
|
}
|
|
1731
|
-
function
|
|
1732
|
-
|
|
1733
|
-
return `SHA256:${hash.replaceAll("=", "")}`;
|
|
1705
|
+
function isModuleResult(value) {
|
|
1706
|
+
return typeof value === "object" && value !== null && "status" in value && typeof value.status === "string";
|
|
1734
1707
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
throw new KnownHostsValidationError(
|
|
1747
|
-
`Refusing to persist known_hosts entry: algorithm contains whitespace`
|
|
1708
|
+
function maskCauseValue(cause, secrets, secretList) {
|
|
1709
|
+
if (cause === void 0) return void 0;
|
|
1710
|
+
if (cause instanceof Error) return maskScopedError(cause, secrets);
|
|
1711
|
+
return maskSecrets(stringifyCause(cause, secretList), secretList);
|
|
1712
|
+
}
|
|
1713
|
+
function buildMaskedErrorClone(error, maskedMessage, secretList) {
|
|
1714
|
+
if (error instanceof CommandError) {
|
|
1715
|
+
return new CommandError(
|
|
1716
|
+
maskedMessage,
|
|
1717
|
+
maskSecrets(error.fullStdout, secretList),
|
|
1718
|
+
maskSecrets(error.fullStderr, secretList)
|
|
1748
1719
|
);
|
|
1749
1720
|
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1721
|
+
const clone = Object.create(Object.getPrototypeOf(error));
|
|
1722
|
+
Object.defineProperty(clone, "message", {
|
|
1723
|
+
configurable: true,
|
|
1724
|
+
value: maskedMessage,
|
|
1725
|
+
writable: true
|
|
1726
|
+
});
|
|
1727
|
+
return clone;
|
|
1728
|
+
}
|
|
1729
|
+
function maskScopedError(error, secrets) {
|
|
1730
|
+
if (!(error instanceof Error) || secrets.length === 0) {
|
|
1731
|
+
return error instanceof Error ? error : new Error(maskSecrets(String(error), [...secrets]));
|
|
1754
1732
|
}
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
const
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
);
|
|
1768
|
-
if (alreadyTrusted) return;
|
|
1769
|
-
const conflictingEntry = matchingRawHostEntries.find(
|
|
1770
|
-
(entry) => entry.key.length !== keyBuffer.length || !timingSafeEqual2(entry.key, keyBuffer)
|
|
1771
|
-
);
|
|
1772
|
-
if (conflictingEntry != null) throwHostKeyMismatch(host, keyBuffer, conflictingEntry.key);
|
|
1773
|
-
await appendFile(filePath, line, { mode: 384 });
|
|
1774
|
-
});
|
|
1775
|
-
}
|
|
1776
|
-
function getFileSystemErrorCode(error) {
|
|
1777
|
-
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
1778
|
-
}
|
|
1779
|
-
var KIBIBYTE_BYTES = 1024;
|
|
1780
|
-
var MEBIBYTE_BYTES = KIBIBYTE_BYTES * KIBIBYTE_BYTES;
|
|
1781
|
-
var KNOWN_HOSTS_FILE_LIMIT_MIB = 16;
|
|
1782
|
-
var KNOWN_HOSTS_FILE_BYTE_LIMIT = KNOWN_HOSTS_FILE_LIMIT_MIB * MEBIBYTE_BYTES;
|
|
1783
|
-
async function loadKnownHostEntries() {
|
|
1784
|
-
const filePath = join(homedir(), ".ssh", "known_hosts");
|
|
1785
|
-
try {
|
|
1786
|
-
const stats = await stat2(filePath);
|
|
1787
|
-
if (stats.size > KNOWN_HOSTS_FILE_BYTE_LIMIT) {
|
|
1788
|
-
throw new HostKeyVerificationError(
|
|
1789
|
-
`Refusing to read known_hosts at ${filePath}: size ${stats.size} bytes exceeds the ${KNOWN_HOSTS_FILE_BYTE_LIMIT}-byte cap`
|
|
1790
|
-
);
|
|
1791
|
-
}
|
|
1792
|
-
return parseKnownHosts(await readFile2(filePath, "utf8"));
|
|
1793
|
-
} catch (error) {
|
|
1794
|
-
if (error instanceof HostKeyVerificationError) throw error;
|
|
1795
|
-
if (getFileSystemErrorCode(error) === "ENOENT") {
|
|
1796
|
-
return [];
|
|
1797
|
-
}
|
|
1798
|
-
throw new HostKeyVerificationError(
|
|
1799
|
-
`Could not read known_hosts at ${filePath}: ${String(error)}`
|
|
1800
|
-
);
|
|
1733
|
+
const secretList = [...secrets];
|
|
1734
|
+
const maskedMessage = maskSecrets(error.message, secretList);
|
|
1735
|
+
const maskedStack = error.stack == null ? void 0 : maskSecrets(error.stack, secretList);
|
|
1736
|
+
const maskedCause = maskCauseValue(error.cause, secrets, secretList);
|
|
1737
|
+
const clone = buildMaskedErrorClone(error, maskedMessage, secretList);
|
|
1738
|
+
clone.name = error.name;
|
|
1739
|
+
if (maskedStack != null) {
|
|
1740
|
+
Object.defineProperty(clone, "stack", {
|
|
1741
|
+
configurable: true,
|
|
1742
|
+
value: maskedStack,
|
|
1743
|
+
writable: true
|
|
1744
|
+
});
|
|
1801
1745
|
}
|
|
1746
|
+
if (maskedCause !== void 0) {
|
|
1747
|
+
Object.defineProperty(clone, "cause", {
|
|
1748
|
+
configurable: true,
|
|
1749
|
+
value: maskedCause,
|
|
1750
|
+
writable: true
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
return clone;
|
|
1802
1754
|
}
|
|
1803
|
-
function
|
|
1755
|
+
function stringifyCause(cause, secretList) {
|
|
1756
|
+
if (typeof cause === "string") return cause;
|
|
1757
|
+
if (typeof cause === "function")
|
|
1758
|
+
return cause.name.length > 0 ? `[Function: ${cause.name}]` : "[Function]";
|
|
1759
|
+
if (typeof cause === "number") return String(cause);
|
|
1760
|
+
if (typeof cause === "boolean") return String(cause);
|
|
1761
|
+
if (typeof cause === "bigint") return String(cause);
|
|
1762
|
+
if (typeof cause === "symbol") return String(cause);
|
|
1763
|
+
if (cause === void 0) return String(cause);
|
|
1764
|
+
if (cause === null) return "null";
|
|
1765
|
+
return stringifyObjectCause(cause, secretList);
|
|
1766
|
+
}
|
|
1767
|
+
function stringifyObjectCause(cause, secretList) {
|
|
1804
1768
|
try {
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
return `WARNING: Permanently added '${host}' (${algo}) to the list of known hosts. Fingerprint: ${fingerprint}
|
|
1808
|
-
`;
|
|
1769
|
+
const serialized = JSON.stringify(normalizeObjectCause(cause, secretList, /* @__PURE__ */ new WeakSet()));
|
|
1770
|
+
return serialized ?? Object.prototype.toString.call(cause);
|
|
1809
1771
|
} catch {
|
|
1810
|
-
return
|
|
1811
|
-
`;
|
|
1772
|
+
return Object.prototype.toString.call(cause);
|
|
1812
1773
|
}
|
|
1813
1774
|
}
|
|
1814
|
-
function
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
);
|
|
1820
|
-
}
|
|
1821
|
-
async function acceptAndPersistHostKey(location, key, cache) {
|
|
1822
|
-
const { host, port } = location;
|
|
1823
|
-
const acceptedWarning = formatAcceptedHostKeyWarning(host, key);
|
|
1775
|
+
function normalizeObjectCause(cause, secretList, seen) {
|
|
1776
|
+
if (isBinaryCauseValue(cause)) return REDACTED_PLACEHOLDER;
|
|
1777
|
+
if (cause instanceof Date) return cause.toJSON();
|
|
1778
|
+
if (seen.has(cause)) return CIRCULAR_PLACEHOLDER;
|
|
1779
|
+
seen.add(cause);
|
|
1824
1780
|
try {
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
if (error instanceof HostKeyVerificationError || error instanceof KnownHostsValidationError) {
|
|
1828
|
-
throw error;
|
|
1781
|
+
if (Array.isArray(cause)) {
|
|
1782
|
+
return cause.map((value) => normalizeCausePropertyValue(value, secretList, seen));
|
|
1829
1783
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1784
|
+
const normalized = {};
|
|
1785
|
+
for (const [key, value] of Object.entries(cause)) {
|
|
1786
|
+
if (isSecretDiagnosticField(key)) {
|
|
1787
|
+
normalized[key] = REDACTED_PLACEHOLDER;
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
normalized[key] = normalizeCausePropertyValue(value, secretList, seen);
|
|
1791
|
+
}
|
|
1792
|
+
return normalized;
|
|
1793
|
+
} finally {
|
|
1794
|
+
seen.delete(cause);
|
|
1834
1795
|
}
|
|
1835
|
-
cache.set(formatHostNeedle(host, port), key);
|
|
1836
|
-
process.stderr.write(acceptedWarning);
|
|
1837
1796
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
if (
|
|
1842
|
-
|
|
1843
|
-
}
|
|
1844
|
-
const [algorithm, key] = parts;
|
|
1845
|
-
if (!PINNED_HOST_KEY_ALGORITHM_PATTERN.test(algorithm)) {
|
|
1846
|
-
throw new Error(
|
|
1847
|
-
`Expected host public key uses unsupported algorithm '${algorithm}'. Supported algorithms: ssh-ed25519, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521.`
|
|
1848
|
-
);
|
|
1849
|
-
}
|
|
1850
|
-
if (!STRICT_BASE64_PATTERN.test(key)) {
|
|
1851
|
-
throw new Error(
|
|
1852
|
-
"Expected host public key contains invalid base64 in the key field. Use the exact value from `ssh-keygen -y` or the second field of an OpenSSH known_hosts line."
|
|
1853
|
-
);
|
|
1854
|
-
}
|
|
1855
|
-
return `${algorithm} ${key}`;
|
|
1797
|
+
function normalizeCausePropertyValue(value, secretList, seen) {
|
|
1798
|
+
if (value === null) return null;
|
|
1799
|
+
if (typeof value === "bigint") return String(value);
|
|
1800
|
+
if (typeof value !== "object") return value;
|
|
1801
|
+
return normalizeObjectCause(value, secretList, seen);
|
|
1856
1802
|
}
|
|
1857
|
-
function
|
|
1858
|
-
|
|
1859
|
-
normalizePinnedPublicKey(publicKey);
|
|
1860
|
-
return null;
|
|
1861
|
-
} catch (error) {
|
|
1862
|
-
return error instanceof Error ? error.message : String(error);
|
|
1863
|
-
}
|
|
1803
|
+
function isBinaryCauseValue(value) {
|
|
1804
|
+
return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
|
|
1864
1805
|
}
|
|
1865
|
-
function
|
|
1866
|
-
|
|
1867
|
-
return `${extractAlgoFromKey(key)} ${key.toString("base64")}`;
|
|
1868
|
-
} catch {
|
|
1869
|
-
return null;
|
|
1870
|
-
}
|
|
1806
|
+
function getRegisteredSecrets() {
|
|
1807
|
+
return [...secretCounts.keys()];
|
|
1871
1808
|
}
|
|
1872
|
-
function
|
|
1873
|
-
|
|
1809
|
+
function clearRegisteredSecrets() {
|
|
1810
|
+
secretCounts.clear();
|
|
1874
1811
|
}
|
|
1875
|
-
function
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
const presentedPublicKey = formatPresentedPublicKey(key);
|
|
1879
|
-
const presentedFingerprint = computeFingerprint(key);
|
|
1880
|
-
const publicKeyMatches = normalizedExpectedPublicKey == null || presentedPublicKey != null && normalizedExpectedPublicKey === presentedPublicKey;
|
|
1881
|
-
const fingerprintMatches = expectedFingerprint == null || expectedFingerprint === presentedFingerprint;
|
|
1882
|
-
if (publicKeyMatches && fingerprintMatches) {
|
|
1883
|
-
return;
|
|
1884
|
-
}
|
|
1885
|
-
throw new HostKeyVerificationError(
|
|
1886
|
-
`HOST KEY VERIFICATION FAILED for ${host}: the remote host key does not match the configured trust anchor.`
|
|
1887
|
-
);
|
|
1812
|
+
function maskRegisteredSecrets(text) {
|
|
1813
|
+
if (secretCounts.size === 0) return text;
|
|
1814
|
+
return maskSecrets(text, getRegisteredSecrets());
|
|
1888
1815
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1816
|
+
|
|
1817
|
+
// src/output.ts
|
|
1818
|
+
var CAUSE_INSPECT_DEPTH = 2;
|
|
1819
|
+
var CAUSE_INSPECT_MAX_STRING_LENGTH = 1024;
|
|
1820
|
+
var CAUSE_REDACT_BINARY_MAX_DEPTH = CAUSE_INSPECT_DEPTH + 1;
|
|
1821
|
+
var MODULE_NAME_WIDTH = 56;
|
|
1822
|
+
var MIN_MODULE_NAME_WIDTH = 12;
|
|
1823
|
+
var OUTPUT_INDENT_UNIT = " ";
|
|
1824
|
+
var SPINNER_FRAME_INTERVAL_MS = 80;
|
|
1825
|
+
var ASCII_ESC = 27;
|
|
1826
|
+
var ANSI_HIDE_CURSOR = `${String.fromCharCode(ASCII_ESC)}[?25l`;
|
|
1827
|
+
var ANSI_SHOW_CURSOR = `${String.fromCharCode(ASCII_ESC)}[?25h`;
|
|
1828
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1829
|
+
var STATUS_ICONS = {
|
|
1830
|
+
changed: pc.yellow("\u21BA"),
|
|
1831
|
+
failed: pc.red("\u2717"),
|
|
1832
|
+
ok: pc.green("\u2713"),
|
|
1833
|
+
skipped: pc.dim("\u2298"),
|
|
1834
|
+
waiting: pc.cyan("\u23F8")
|
|
1835
|
+
};
|
|
1836
|
+
var CLI_HEADER_LINES = [
|
|
1837
|
+
" _ _ ",
|
|
1838
|
+
" | | (_) ",
|
|
1839
|
+
" _ __ __ _ _ __ __ _| |_ ___ __",
|
|
1840
|
+
" | '_ \\ / _` | '__/ _` | __| \\ \\/ /",
|
|
1841
|
+
" | |_) | (_| | | | (_| | |_| |> < ",
|
|
1842
|
+
" | .__/ \\__,_|_| \\__,_|\\__|_/_/\\_\\",
|
|
1843
|
+
" | | ",
|
|
1844
|
+
" |_| "
|
|
1845
|
+
];
|
|
1846
|
+
var LIVE_OUTPUT_STATE_KEY = /* @__PURE__ */ Symbol.for("paratix.output.liveState");
|
|
1847
|
+
function getSharedLiveOutputState() {
|
|
1848
|
+
const registry = globalThis;
|
|
1849
|
+
const existing = registry[LIVE_OUTPUT_STATE_KEY];
|
|
1850
|
+
if (existing != null) return existing;
|
|
1851
|
+
const created = {
|
|
1852
|
+
activeModuleStartedAt: null,
|
|
1853
|
+
activeRecipeGuideDepths: [],
|
|
1854
|
+
activeSpinner: null,
|
|
1855
|
+
cursorHidden: false,
|
|
1856
|
+
pendingRecipeClosureGuideDepths: [],
|
|
1857
|
+
recipeOutputDepth: -1
|
|
1858
|
+
};
|
|
1859
|
+
registry[LIVE_OUTPUT_STATE_KEY] = created;
|
|
1860
|
+
return created;
|
|
1861
|
+
}
|
|
1862
|
+
var liveOutputState = getSharedLiveOutputState();
|
|
1863
|
+
function renderCliHeader(version) {
|
|
1864
|
+
const versionText = pc.dim(`v${version}`);
|
|
1865
|
+
return `${pc.cyan(CLI_HEADER_LINES.join("\n"))}${versionText}
|
|
1866
|
+
`;
|
|
1867
|
+
}
|
|
1868
|
+
function printCliHeader(version) {
|
|
1869
|
+
console.log(renderCliHeader(version));
|
|
1870
|
+
}
|
|
1871
|
+
function supportsAnimatedModuleOutput() {
|
|
1872
|
+
return process.stdout.isTTY && typeof process.stdout.clearLine === "function" && typeof process.stdout.cursorTo === "function";
|
|
1873
|
+
}
|
|
1874
|
+
function getModuleIcon(status, waitingFrame) {
|
|
1875
|
+
return status === "waiting" ? pc.cyan(waitingFrame ?? "|") : STATUS_ICONS[status];
|
|
1876
|
+
}
|
|
1877
|
+
function getModuleStatusText(status) {
|
|
1878
|
+
switch (status) {
|
|
1879
|
+
case "changed": {
|
|
1880
|
+
return pc.yellow(status);
|
|
1881
|
+
}
|
|
1882
|
+
case "failed": {
|
|
1883
|
+
return pc.red(status);
|
|
1884
|
+
}
|
|
1885
|
+
case "ok": {
|
|
1886
|
+
return pc.green(status);
|
|
1887
|
+
}
|
|
1888
|
+
case "skipped": {
|
|
1889
|
+
return pc.dim(status);
|
|
1890
|
+
}
|
|
1891
|
+
case "waiting": {
|
|
1892
|
+
return pc.cyan("running");
|
|
1918
1893
|
}
|
|
1919
|
-
}
|
|
1920
|
-
return result;
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
// src/serverDefinitionValidation.ts
|
|
1924
|
-
var STRICT_HOST_KEY_ERROR = `Invalid property 'ssh.strictHostKeyChecking' (expected "accept-new", "no", or "yes")`;
|
|
1925
|
-
var MAX_TCP_PORT = 65535;
|
|
1926
|
-
function describeType(value) {
|
|
1927
|
-
return value === null ? "null" : typeof value;
|
|
1894
|
+
}
|
|
1928
1895
|
}
|
|
1929
|
-
function
|
|
1930
|
-
return
|
|
1896
|
+
function getCurrentOutputDepth() {
|
|
1897
|
+
return Math.max(liveOutputState.recipeOutputDepth, 0);
|
|
1931
1898
|
}
|
|
1932
|
-
function
|
|
1933
|
-
return
|
|
1899
|
+
function getGuideDot(depth) {
|
|
1900
|
+
return depth % 2 === 0 ? pc.gray("\xB7") : pc.cyan("\xB7");
|
|
1934
1901
|
}
|
|
1935
|
-
function
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1902
|
+
function buildGuideIndent(baseIndent, options) {
|
|
1903
|
+
const indentCharacters = [...baseIndent];
|
|
1904
|
+
const guideDepths = [
|
|
1905
|
+
...options?.activeGuideDepths ?? liveOutputState.activeRecipeGuideDepths,
|
|
1906
|
+
...options?.extraGuideDepths ?? []
|
|
1907
|
+
];
|
|
1908
|
+
for (const guideDepth of guideDepths) {
|
|
1909
|
+
const guideCharacterIndex = OUTPUT_INDENT_UNIT.length * (guideDepth + 1);
|
|
1910
|
+
if (guideCharacterIndex >= indentCharacters.length) continue;
|
|
1911
|
+
indentCharacters[guideCharacterIndex] = options?.colorize === false ? "\xB7" : getGuideDot(guideDepth);
|
|
1941
1912
|
}
|
|
1913
|
+
return indentCharacters.join("");
|
|
1942
1914
|
}
|
|
1943
|
-
function
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
errors.push(`Invalid property 'ssh.${key}' (expected number, got ${describeType(value)})`);
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
if (options?.integer === true && !Number.isInteger(validatedNumber)) {
|
|
1953
|
-
errors.push(`Property 'ssh.${key}' must be an integer`);
|
|
1954
|
-
return;
|
|
1955
|
-
}
|
|
1956
|
-
if (options?.positive === true && validatedNumber <= 0) {
|
|
1957
|
-
errors.push(`Property 'ssh.${key}' must be greater than 0`);
|
|
1915
|
+
function clearPendingRecipeClosureGuides() {
|
|
1916
|
+
liveOutputState.pendingRecipeClosureGuideDepths = [];
|
|
1917
|
+
}
|
|
1918
|
+
function getModuleIndent() {
|
|
1919
|
+
if (liveOutputState.recipeOutputDepth < 0) {
|
|
1920
|
+
return OUTPUT_INDENT_UNIT;
|
|
1958
1921
|
}
|
|
1922
|
+
return OUTPUT_INDENT_UNIT.repeat(getCurrentOutputDepth() + 2);
|
|
1959
1923
|
}
|
|
1960
|
-
function
|
|
1961
|
-
|
|
1924
|
+
function getRecipeHeaderIndent() {
|
|
1925
|
+
if (liveOutputState.recipeOutputDepth < 0) return "";
|
|
1926
|
+
return OUTPUT_INDENT_UNIT.repeat(getCurrentOutputDepth() + 1);
|
|
1962
1927
|
}
|
|
1963
|
-
function
|
|
1964
|
-
return
|
|
1928
|
+
function getErrorIndent() {
|
|
1929
|
+
return `${getModuleIndent()}\u2502 `;
|
|
1965
1930
|
}
|
|
1966
|
-
function
|
|
1967
|
-
|
|
1968
|
-
if (typeof object[key] !== "string") {
|
|
1969
|
-
errors.push(`Invalid property 'ssh.${key}' (expected string, got ${describeType(object[key])})`);
|
|
1970
|
-
return;
|
|
1971
|
-
}
|
|
1972
|
-
if (object[key].length === 0) {
|
|
1973
|
-
errors.push(`Property 'ssh.${key}' must not be an empty string`);
|
|
1974
|
-
}
|
|
1931
|
+
function getContinuationIndent() {
|
|
1932
|
+
return `${getModuleIndent()} `;
|
|
1975
1933
|
}
|
|
1976
|
-
function
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
return;
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
errors.push("Property 'ssh.ports' must not be empty");
|
|
1987
|
-
return;
|
|
1988
|
-
}
|
|
1989
|
-
for (const [index, port] of ssh.ports.entries()) {
|
|
1990
|
-
if (!isValidTcpPort(port)) {
|
|
1991
|
-
errors.push(`Property 'ssh.ports[${index}]' must be an integer between 1 and 65535`);
|
|
1992
|
-
}
|
|
1934
|
+
async function withRecipeOutputScope(scopedOperation) {
|
|
1935
|
+
liveOutputState.recipeOutputDepth += 1;
|
|
1936
|
+
try {
|
|
1937
|
+
return await scopedOperation();
|
|
1938
|
+
} finally {
|
|
1939
|
+
liveOutputState.activeRecipeGuideDepths = liveOutputState.activeRecipeGuideDepths.filter(
|
|
1940
|
+
(depth) => depth !== liveOutputState.recipeOutputDepth
|
|
1941
|
+
);
|
|
1942
|
+
liveOutputState.pendingRecipeClosureGuideDepths = [liveOutputState.recipeOutputDepth];
|
|
1943
|
+
liveOutputState.recipeOutputDepth -= 1;
|
|
1993
1944
|
}
|
|
1994
1945
|
}
|
|
1995
|
-
function
|
|
1996
|
-
if (
|
|
1997
|
-
|
|
1998
|
-
return;
|
|
1999
|
-
}
|
|
2000
|
-
if (typeof ssh.user !== "string") {
|
|
2001
|
-
errors.push(`Invalid property 'ssh.user' (expected string, got ${describeType(ssh.user)})`);
|
|
2002
|
-
return;
|
|
2003
|
-
}
|
|
2004
|
-
if (ssh.user.length === 0) {
|
|
2005
|
-
errors.push("Property 'ssh.user' must not be an empty string");
|
|
2006
|
-
}
|
|
1946
|
+
function getActiveModuleElapsed() {
|
|
1947
|
+
if (liveOutputState.activeModuleStartedAt == null) return void 0;
|
|
1948
|
+
return formatModuleElapsed(Date.now() - liveOutputState.activeModuleStartedAt);
|
|
2007
1949
|
}
|
|
2008
|
-
function
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
1950
|
+
function renderModuleLine(parameters) {
|
|
1951
|
+
const { detail, elapsed, extraGuideDepths = [], name, status, waitingFrame } = parameters;
|
|
1952
|
+
const baseIndent = getModuleIndent();
|
|
1953
|
+
const indent = buildGuideIndent(baseIndent, { extraGuideDepths });
|
|
1954
|
+
const icon = getModuleIcon(status, waitingFrame);
|
|
1955
|
+
const statusText = getModuleStatusText(status);
|
|
1956
|
+
const detailSuffix = detail == null ? "" : ` ${pc.dim(detail)}`;
|
|
1957
|
+
const elapsedSuffix = elapsed == null ? "" : ` ${pc.dim(elapsed)}`;
|
|
1958
|
+
const alignedNameWidth = Math.max(MODULE_NAME_WIDTH - baseIndent.length, MIN_MODULE_NAME_WIDTH);
|
|
1959
|
+
return `${indent}${icon} ${name.padEnd(alignedNameWidth)} ${statusText}${detailSuffix}${elapsedSuffix}`;
|
|
2013
1960
|
}
|
|
2014
|
-
function
|
|
2015
|
-
if (
|
|
2016
|
-
|
|
2017
|
-
|
|
1961
|
+
function hideCursor() {
|
|
1962
|
+
if (liveOutputState.cursorHidden) return;
|
|
1963
|
+
if (!supportsAnimatedModuleOutput()) return;
|
|
1964
|
+
process.stdout.write(ANSI_HIDE_CURSOR);
|
|
1965
|
+
liveOutputState.cursorHidden = true;
|
|
2018
1966
|
}
|
|
2019
|
-
function
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
collectOptionalStringErrors(ssh, "expectedHostPublicKey", errors);
|
|
2024
|
-
collectExpectedHostPublicKeyErrors(ssh, errors);
|
|
2025
|
-
collectOptionalBooleanErrors(ssh, "agentForward", errors);
|
|
2026
|
-
collectOptionalBooleanErrors(ssh, "passwordFallback", errors);
|
|
2027
|
-
collectOptionalNumberErrors({
|
|
2028
|
-
errors,
|
|
2029
|
-
key: "reconnectTimeout",
|
|
2030
|
-
object: ssh,
|
|
2031
|
-
options: { positive: true }
|
|
2032
|
-
});
|
|
2033
|
-
collectOptionalNumberErrors({
|
|
2034
|
-
errors,
|
|
2035
|
-
key: "maxReconnectAttempts",
|
|
2036
|
-
object: ssh,
|
|
2037
|
-
options: { integer: true, positive: true }
|
|
2038
|
-
});
|
|
2039
|
-
collectStrictHostKeyCheckingErrors(ssh, errors);
|
|
1967
|
+
function showCursor() {
|
|
1968
|
+
if (!liveOutputState.cursorHidden) return;
|
|
1969
|
+
process.stdout.write(ANSI_SHOW_CURSOR);
|
|
1970
|
+
liveOutputState.cursorHidden = false;
|
|
2040
1971
|
}
|
|
2041
|
-
function
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
1972
|
+
function writeAnimatedModuleLine(line) {
|
|
1973
|
+
hideCursor();
|
|
1974
|
+
process.stdout.clearLine(0);
|
|
1975
|
+
process.stdout.cursorTo(0);
|
|
1976
|
+
process.stdout.write(fitAnimatedModuleLine(line, process.stdout.columns));
|
|
1977
|
+
}
|
|
1978
|
+
function stopAnimatedModuleLine(clearCurrentLine = false) {
|
|
1979
|
+
showCursor();
|
|
1980
|
+
if (liveOutputState.activeSpinner == null) return;
|
|
1981
|
+
clearInterval(liveOutputState.activeSpinner.interval);
|
|
1982
|
+
liveOutputState.activeSpinner = null;
|
|
1983
|
+
if (clearCurrentLine && supportsAnimatedModuleOutput()) {
|
|
1984
|
+
process.stdout.clearLine(0);
|
|
1985
|
+
process.stdout.cursorTo(0);
|
|
2054
1986
|
}
|
|
2055
|
-
const ssh = value;
|
|
2056
|
-
collectPortsErrors(ssh, errors);
|
|
2057
|
-
collectRequiredUserErrors(ssh, errors);
|
|
2058
|
-
collectOptionalSshFieldErrors(ssh, errors);
|
|
2059
|
-
return errors;
|
|
2060
1987
|
}
|
|
2061
|
-
function
|
|
2062
|
-
|
|
2063
|
-
"Missing property 'ssh.user' (expected string)": "ssh.user is required",
|
|
2064
|
-
"Property 'ssh.expectedHostFingerprint' must not be an empty string": "ssh.expectedHostFingerprint must not be an empty string",
|
|
2065
|
-
"Property 'ssh.expectedHostPublicKey' must not be an empty string": "ssh.expectedHostPublicKey must not be an empty string",
|
|
2066
|
-
"Property 'ssh.ports' must not be empty": "ssh.ports must not be empty",
|
|
2067
|
-
"Property 'ssh.privateKey' must not be an empty string": "ssh.privateKey must not be an empty string",
|
|
2068
|
-
"Property 'ssh.user' must not be an empty string": "ssh.user is required",
|
|
2069
|
-
[STRICT_HOST_KEY_ERROR]: "ssh.strictHostKeyChecking must be one of accept-new, no, yes"
|
|
2070
|
-
};
|
|
2071
|
-
return mappedErrors[error] ?? error;
|
|
1988
|
+
function stopLiveModuleOutput(clearCurrentLine = false) {
|
|
1989
|
+
stopAnimatedModuleLine(clearCurrentLine);
|
|
2072
1990
|
}
|
|
2073
|
-
function
|
|
2074
|
-
|
|
2075
|
-
if (
|
|
2076
|
-
|
|
1991
|
+
function startModuleSpinner(name, detail) {
|
|
1992
|
+
liveOutputState.activeModuleStartedAt = Date.now();
|
|
1993
|
+
if (!supportsAnimatedModuleOutput()) return;
|
|
1994
|
+
clearPendingRecipeClosureGuides();
|
|
1995
|
+
stopAnimatedModuleLine();
|
|
1996
|
+
const maskedName = maskRegisteredSecrets(name);
|
|
1997
|
+
const maskedDetail = detail == null ? void 0 : maskRegisteredSecrets(detail);
|
|
1998
|
+
const displayModule = formatDisplayModule({
|
|
1999
|
+
continuationIndentWidth: getContinuationIndent().length,
|
|
2000
|
+
detail: maskedDetail,
|
|
2001
|
+
name: maskedName,
|
|
2002
|
+
status: "waiting",
|
|
2003
|
+
terminalColumns: process.stdout.columns
|
|
2004
|
+
});
|
|
2005
|
+
const spinner = {
|
|
2006
|
+
detail: displayModule.detail,
|
|
2007
|
+
frameIndex: 0,
|
|
2008
|
+
interval: setInterval(() => {
|
|
2009
|
+
spinner.frameIndex = (spinner.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
2010
|
+
writeAnimatedModuleLine(
|
|
2011
|
+
renderModuleLine({
|
|
2012
|
+
detail: spinner.detail,
|
|
2013
|
+
elapsed: getActiveModuleElapsed(),
|
|
2014
|
+
name: displayModule.name,
|
|
2015
|
+
status: "waiting",
|
|
2016
|
+
waitingFrame: SPINNER_FRAMES[spinner.frameIndex]
|
|
2017
|
+
})
|
|
2018
|
+
);
|
|
2019
|
+
}, SPINNER_FRAME_INTERVAL_MS)
|
|
2020
|
+
};
|
|
2021
|
+
if (typeof spinner.interval.unref === "function") spinner.interval.unref();
|
|
2022
|
+
liveOutputState.activeSpinner = spinner;
|
|
2023
|
+
writeAnimatedModuleLine(
|
|
2024
|
+
renderModuleLine({
|
|
2025
|
+
detail: displayModule.detail,
|
|
2026
|
+
elapsed: getActiveModuleElapsed(),
|
|
2027
|
+
name: displayModule.name,
|
|
2028
|
+
status: "waiting",
|
|
2029
|
+
waitingFrame: SPINNER_FRAMES[0]
|
|
2030
|
+
})
|
|
2031
|
+
);
|
|
2077
2032
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2033
|
+
function printRecipeHeader(name) {
|
|
2034
|
+
stopAnimatedModuleLine(true);
|
|
2035
|
+
clearPendingRecipeClosureGuides();
|
|
2036
|
+
const header = pc.bold(pc.blue(`[${name}]`));
|
|
2037
|
+
console.log(`${buildGuideIndent(getRecipeHeaderIndent())}${header}`);
|
|
2038
|
+
if (liveOutputState.recipeOutputDepth >= 0) {
|
|
2039
|
+
liveOutputState.activeRecipeGuideDepths = [
|
|
2040
|
+
...liveOutputState.activeRecipeGuideDepths,
|
|
2041
|
+
liveOutputState.recipeOutputDepth
|
|
2042
|
+
];
|
|
2043
|
+
}
|
|
2084
2044
|
}
|
|
2085
|
-
function
|
|
2086
|
-
|
|
2045
|
+
function printRunContext(parameters) {
|
|
2046
|
+
const mode = parameters.dryRun ? pc.yellow("dry-run") : pc.green("apply");
|
|
2047
|
+
const ports = parameters.ports.join(", ");
|
|
2048
|
+
console.log(
|
|
2049
|
+
pc.dim(`Run ${parameters.name} \xB7 host ${parameters.host} \xB7 ports ${ports} \xB7 mode ${mode}`)
|
|
2050
|
+
);
|
|
2087
2051
|
}
|
|
2088
|
-
function
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
throw new TypeError(
|
|
2092
|
-
`Env meta entry ${JSON.stringify(entryKey)} resolved to typeof ${actualType}, expected boolean, number, or string`
|
|
2093
|
-
);
|
|
2094
|
-
}
|
|
2095
|
-
if (enforceDeclaredValueType && actualType !== declaredValueType) {
|
|
2096
|
-
throw new TypeError(
|
|
2097
|
-
`Env meta entry ${JSON.stringify(entryKey)} resolved to typeof ${actualType}, expected ${declaredValueType}`
|
|
2098
|
-
);
|
|
2052
|
+
function colorizeDiffLine(line) {
|
|
2053
|
+
if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("@@")) {
|
|
2054
|
+
return pc.dim(line);
|
|
2099
2055
|
}
|
|
2056
|
+
if (line.startsWith("-")) return pc.red(line);
|
|
2057
|
+
if (line.startsWith("+")) return pc.green(line);
|
|
2058
|
+
return pc.dim(line);
|
|
2100
2059
|
}
|
|
2101
|
-
function
|
|
2102
|
-
|
|
2060
|
+
function renderDiffLines(diff) {
|
|
2061
|
+
if (diff.trim() === "") return [];
|
|
2062
|
+
const sanitized = sanitizeTerminalText(maskRegisteredSecrets(diff));
|
|
2063
|
+
return sanitized.split("\n").map((line) => `\u2502 ${colorizeDiffLine(line)}`);
|
|
2103
2064
|
}
|
|
2104
|
-
function
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
if (candidate.valueType !== "boolean" && candidate.valueType !== "number" && candidate.valueType !== "string") {
|
|
2112
|
-
throw new TypeError("Invalid env meta entry: valueType must be boolean, number, or string");
|
|
2065
|
+
function writeContinuationLine(line, extraGuideDepths, via) {
|
|
2066
|
+
const composed = `${buildGuideIndent(getContinuationIndent(), { extraGuideDepths })}${line}`;
|
|
2067
|
+
if (via === "stdout") {
|
|
2068
|
+
process.stdout.write(`${composed}
|
|
2069
|
+
`);
|
|
2070
|
+
} else {
|
|
2071
|
+
console.log(composed);
|
|
2113
2072
|
}
|
|
2114
2073
|
}
|
|
2115
|
-
function
|
|
2116
|
-
|
|
2117
|
-
|
|
2074
|
+
function writeContinuationBlock(parameters) {
|
|
2075
|
+
for (const detailLine of parameters.detailLines) {
|
|
2076
|
+
writeContinuationLine(pc.dim(detailLine), parameters.extraGuideDepths, parameters.via);
|
|
2118
2077
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
const hostValidationFailure = validateHostLabel(candidate.host);
|
|
2122
|
-
if (hostValidationFailure != null) {
|
|
2123
|
-
throw new TypeError(
|
|
2124
|
-
`Invalid system.host meta entry: host ${describeHostValidationFailure(hostValidationFailure)}`
|
|
2125
|
-
);
|
|
2078
|
+
for (const diffLine of parameters.diffLines) {
|
|
2079
|
+
writeContinuationLine(diffLine, parameters.extraGuideDepths, parameters.via);
|
|
2126
2080
|
}
|
|
2127
2081
|
}
|
|
2128
|
-
function
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2082
|
+
function printRenderedModuleResult(parameters) {
|
|
2083
|
+
const extraGuideDepths = parameters.extraGuideDepths ?? [];
|
|
2084
|
+
const maskedName = maskRegisteredSecrets(parameters.name);
|
|
2085
|
+
const maskedDetail = parameters.detail == null ? void 0 : maskRegisteredSecrets(parameters.detail);
|
|
2086
|
+
const displayModule = formatDisplayModule({
|
|
2087
|
+
continuationIndentWidth: `${buildGuideIndent(
|
|
2088
|
+
OUTPUT_INDENT_UNIT.repeat(Math.max(getCurrentOutputDepth() + 2, 1)),
|
|
2089
|
+
{ extraGuideDepths }
|
|
2090
|
+
)} `.length,
|
|
2091
|
+
detail: maskedDetail,
|
|
2092
|
+
name: maskedName,
|
|
2093
|
+
status: parameters.status,
|
|
2094
|
+
terminalColumns: process.stdout.columns
|
|
2095
|
+
});
|
|
2096
|
+
const elapsed = getActiveModuleElapsed();
|
|
2097
|
+
liveOutputState.activeModuleStartedAt = null;
|
|
2098
|
+
const line = renderModuleLine({
|
|
2099
|
+
detail: displayModule.detail,
|
|
2100
|
+
elapsed,
|
|
2101
|
+
extraGuideDepths,
|
|
2102
|
+
name: displayModule.name,
|
|
2103
|
+
status: parameters.status
|
|
2104
|
+
});
|
|
2105
|
+
const diffLines = parameters.diff == null ? [] : renderDiffLines(parameters.diff);
|
|
2106
|
+
const usesSpinner = supportsAnimatedModuleOutput() && liveOutputState.activeSpinner != null;
|
|
2107
|
+
if (usesSpinner) {
|
|
2108
|
+
stopAnimatedModuleLine();
|
|
2109
|
+
writeAnimatedModuleLine(line);
|
|
2110
|
+
process.stdout.write("\n");
|
|
2111
|
+
} else {
|
|
2112
|
+
console.log(line);
|
|
2113
|
+
}
|
|
2114
|
+
writeContinuationBlock({
|
|
2115
|
+
detailLines: displayModule.detailLines,
|
|
2116
|
+
diffLines,
|
|
2117
|
+
extraGuideDepths,
|
|
2118
|
+
via: usesSpinner ? "stdout" : "console"
|
|
2119
|
+
});
|
|
2133
2120
|
}
|
|
2134
|
-
function
|
|
2135
|
-
|
|
2121
|
+
function printModuleResult(name, status, detail, diff) {
|
|
2122
|
+
clearPendingRecipeClosureGuides();
|
|
2123
|
+
printRenderedModuleResult({ detail, diff, name, status });
|
|
2136
2124
|
}
|
|
2137
|
-
function
|
|
2138
|
-
|
|
2125
|
+
function printRecipeModuleResult(name, status, detail, diff) {
|
|
2126
|
+
printRenderedModuleResult({
|
|
2127
|
+
detail,
|
|
2128
|
+
diff,
|
|
2129
|
+
extraGuideDepths: liveOutputState.pendingRecipeClosureGuideDepths,
|
|
2130
|
+
name,
|
|
2131
|
+
status
|
|
2132
|
+
});
|
|
2133
|
+
clearPendingRecipeClosureGuides();
|
|
2139
2134
|
}
|
|
2140
|
-
function
|
|
2141
|
-
|
|
2142
|
-
|
|
2135
|
+
function printCommandError(stdout, stderr) {
|
|
2136
|
+
const maskedStdout = sanitizeTerminalText(maskRegisteredSecrets(stdout));
|
|
2137
|
+
const maskedStderr = sanitizeTerminalText(maskRegisteredSecrets(stderr));
|
|
2138
|
+
const lines = [];
|
|
2139
|
+
if (maskedStderr.trim()) {
|
|
2140
|
+
lines.push(...maskedStderr.trim().split("\n"));
|
|
2143
2141
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
return;
|
|
2152
|
-
}
|
|
2153
|
-
case SYSTEM_HOST_KIND: {
|
|
2154
|
-
assertValidSystemHostMetaEntry(entry);
|
|
2155
|
-
return;
|
|
2142
|
+
if (maskedStdout.trim()) {
|
|
2143
|
+
lines.push(...maskedStdout.trim().split("\n"));
|
|
2144
|
+
}
|
|
2145
|
+
if (lines.length > 0) {
|
|
2146
|
+
console.error(pc.red(`${getErrorIndent()}Error output:`));
|
|
2147
|
+
for (const line of lines) {
|
|
2148
|
+
console.error(pc.red(`${getErrorIndent()}${line}`));
|
|
2156
2149
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
function printVerboseCommandError(stdout, stderr) {
|
|
2153
|
+
const maskedStdout = sanitizeTerminalText(maskRegisteredSecrets(stdout));
|
|
2154
|
+
const maskedStderr = sanitizeTerminalText(maskRegisteredSecrets(stderr));
|
|
2155
|
+
if (maskedStderr.trim()) {
|
|
2156
|
+
console.error(pc.red(`${getErrorIndent()}Full stderr:`));
|
|
2157
|
+
for (const line of maskedStderr.trim().split("\n")) {
|
|
2158
|
+
console.error(pc.red(`${getErrorIndent()}${line}`));
|
|
2159
2159
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2160
|
+
}
|
|
2161
|
+
if (maskedStdout.trim()) {
|
|
2162
|
+
console.error(pc.red(`${getErrorIndent()}Full stdout:`));
|
|
2163
|
+
for (const line of maskedStdout.trim().split("\n")) {
|
|
2164
|
+
console.error(pc.red(`${getErrorIndent()}${line}`));
|
|
2162
2165
|
}
|
|
2163
2166
|
}
|
|
2164
2167
|
}
|
|
2165
|
-
function
|
|
2166
|
-
if (
|
|
2167
|
-
|
|
2168
|
-
assertValidModuleMetaEntry(entry);
|
|
2168
|
+
function printVerboseErrorBlock(label, content) {
|
|
2169
|
+
if (!content.trim()) {
|
|
2170
|
+
return;
|
|
2169
2171
|
}
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
if (name.length === 0) return false;
|
|
2174
|
-
const segments = name.split(".");
|
|
2175
|
-
for (const segment of segments) {
|
|
2176
|
-
if (!META_ENV_SEGMENT_PATTERN.test(segment)) return false;
|
|
2172
|
+
console.error(pc.red(`${getErrorIndent()}${label}`));
|
|
2173
|
+
for (const line of content.trim().split("\n")) {
|
|
2174
|
+
console.error(pc.red(`${getErrorIndent()}${line}`));
|
|
2177
2175
|
}
|
|
2178
|
-
return true;
|
|
2179
2176
|
}
|
|
2180
|
-
function
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2177
|
+
function getErrorCause(error) {
|
|
2178
|
+
return error.cause;
|
|
2179
|
+
}
|
|
2180
|
+
function printVerboseErrorCause(cause, depth, visitedCauses) {
|
|
2181
|
+
const label = `Cause ${depth}:`;
|
|
2182
|
+
if (cause instanceof Error) {
|
|
2183
|
+
if (visitedCauses.has(cause)) {
|
|
2184
|
+
printVerboseErrorBlock(label, "<cycle detected>");
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
visitedCauses.add(cause);
|
|
2188
|
+
const stack = cause.stack?.trim() ?? "";
|
|
2189
|
+
const stackOrMessage = stack.length > 0 ? stack : String(cause);
|
|
2190
|
+
printVerboseErrorBlock(label, maskRegisteredSecrets(stackOrMessage));
|
|
2191
|
+
const nestedCause = getErrorCause(cause);
|
|
2192
|
+
if (nestedCause !== void 0) {
|
|
2193
|
+
printVerboseErrorCause(nestedCause, depth + 1, visitedCauses);
|
|
2194
|
+
}
|
|
2195
|
+
return;
|
|
2185
2196
|
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2197
|
+
printVerboseErrorBlock(label, maskRegisteredSecrets(formatCauseValue(cause)));
|
|
2198
|
+
}
|
|
2199
|
+
function printVerboseGenericError(error) {
|
|
2200
|
+
const stack = error.stack?.trim() ?? "";
|
|
2201
|
+
const stackOrMessage = stack.length > 0 ? stack : String(error);
|
|
2202
|
+
printVerboseErrorBlock("Full stack:", maskRegisteredSecrets(stackOrMessage));
|
|
2203
|
+
const cause = getErrorCause(error);
|
|
2204
|
+
if (cause !== void 0) {
|
|
2205
|
+
const visitedCauses = /* @__PURE__ */ new WeakSet();
|
|
2206
|
+
visitedCauses.add(error);
|
|
2207
|
+
printVerboseErrorCause(cause, 1, visitedCauses);
|
|
2190
2208
|
}
|
|
2191
2209
|
}
|
|
2192
|
-
function
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
return
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2210
|
+
function formatCauseValue(cause) {
|
|
2211
|
+
if (cause instanceof Error) return cause.message;
|
|
2212
|
+
return inspectRedactedDiagnosticValue(cause, {
|
|
2213
|
+
depth: CAUSE_INSPECT_DEPTH,
|
|
2214
|
+
maxStringLength: CAUSE_INSPECT_MAX_STRING_LENGTH,
|
|
2215
|
+
redactMaxDepth: CAUSE_REDACT_BINARY_MAX_DEPTH
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
function printCauseChain(error) {
|
|
2219
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
2220
|
+
visited.add(error);
|
|
2221
|
+
let cause = getErrorCause(error);
|
|
2222
|
+
while (cause !== void 0) {
|
|
2223
|
+
if (cause instanceof Error) {
|
|
2224
|
+
if (visited.has(cause)) return;
|
|
2225
|
+
visited.add(cause);
|
|
2226
|
+
}
|
|
2227
|
+
console.error(
|
|
2228
|
+
pc.red(`${getErrorIndent()}Cause: ${maskRegisteredSecrets(formatCauseValue(cause))}`)
|
|
2229
|
+
);
|
|
2230
|
+
if (cause instanceof CommandError) {
|
|
2231
|
+
printVerboseCommandError(
|
|
2232
|
+
maskRegisteredSecrets(cause.fullStdout),
|
|
2233
|
+
maskRegisteredSecrets(cause.fullStderr)
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
cause = cause instanceof Error ? getErrorCause(cause) : void 0;
|
|
2237
|
+
}
|
|
2214
2238
|
}
|
|
2215
|
-
|
|
2216
|
-
if (
|
|
2217
|
-
|
|
2218
|
-
|
|
2239
|
+
function printCommandFailure(error, verbose) {
|
|
2240
|
+
if (verbose && error instanceof CommandError) {
|
|
2241
|
+
const summaryLine = error.message.split("\n")[0];
|
|
2242
|
+
printCommandError("", maskRegisteredSecrets(summaryLine));
|
|
2243
|
+
printVerboseCommandError(
|
|
2244
|
+
maskRegisteredSecrets(error.fullStdout),
|
|
2245
|
+
maskRegisteredSecrets(error.fullStderr)
|
|
2246
|
+
);
|
|
2247
|
+
return;
|
|
2219
2248
|
}
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
if (!
|
|
2223
|
-
|
|
2224
|
-
|
|
2249
|
+
printCommandError("", maskRegisteredSecrets(String(error)));
|
|
2250
|
+
if (error instanceof Error) {
|
|
2251
|
+
if (!(error instanceof CommandError)) {
|
|
2252
|
+
printCauseChain(error);
|
|
2253
|
+
}
|
|
2254
|
+
if (verbose) {
|
|
2255
|
+
printVerboseGenericError(error);
|
|
2256
|
+
}
|
|
2225
2257
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2258
|
+
}
|
|
2259
|
+
function printSummary(stats) {
|
|
2260
|
+
stopAnimatedModuleLine(true);
|
|
2261
|
+
const parts = [
|
|
2262
|
+
pc.yellow(`${stats.changed} changed`),
|
|
2263
|
+
pc.green(`${stats.ok} ok`),
|
|
2264
|
+
pc.dim(`${stats.skipped} skipped`),
|
|
2265
|
+
stats.failed > 0 ? pc.red(`${stats.failed} failed`) : `${stats.failed} failed`,
|
|
2266
|
+
pc.cyan(`${stats.signals} signals triggered`)
|
|
2267
|
+
];
|
|
2268
|
+
console.log(`
|
|
2269
|
+
${parts.join(pc.dim(" \xB7 "))}`);
|
|
2228
2270
|
}
|
|
2229
2271
|
|
|
2230
2272
|
// src/dryRunRecipe.ts
|
|
@@ -2243,109 +2285,601 @@ async function executeDryRunBlockingModule(parameters) {
|
|
|
2243
2285
|
const result = childModule._applyDryRun == null ? await childModule.apply(connection, environment) : await childModule._applyDryRun(connection, environment, {
|
|
2244
2286
|
shutdownSignal: parameters.shutdownSignal
|
|
2245
2287
|
});
|
|
2246
|
-
const nextEnvironment = result.meta == null ? environment : await mergeEnvironmentFromMeta(environment, result.meta);
|
|
2247
|
-
const diffOutput = diff ? result.diff : void 0;
|
|
2248
|
-
printModuleResult(
|
|
2249
|
-
childModule.name,
|
|
2250
|
-
result.status,
|
|
2251
|
-
result._dryRunDetail ?? "(dry-run)",
|
|
2252
|
-
diffOutput
|
|
2253
|
-
);
|
|
2288
|
+
const nextEnvironment = result.meta == null ? environment : await mergeEnvironmentFromMeta(environment, result.meta);
|
|
2289
|
+
const diffOutput = diff ? result.diff : void 0;
|
|
2290
|
+
printModuleResult(
|
|
2291
|
+
childModule.name,
|
|
2292
|
+
result.status,
|
|
2293
|
+
result._dryRunDetail ?? "(dry-run)",
|
|
2294
|
+
diffOutput
|
|
2295
|
+
);
|
|
2296
|
+
if (result.status === "failed" && result.error != null) {
|
|
2297
|
+
printCommandFailure(result.error, verbose);
|
|
2298
|
+
}
|
|
2299
|
+
return {
|
|
2300
|
+
env: nextEnvironment,
|
|
2301
|
+
meta: result.meta,
|
|
2302
|
+
shouldBreak: result.status === "failed" || result._stopRun === true,
|
|
2303
|
+
status: result.status,
|
|
2304
|
+
stopRun: result._stopRun
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
async function executeDryRunChildModule(parameters) {
|
|
2308
|
+
const { childModule, diff, environment, ssh, verbose } = parameters;
|
|
2309
|
+
if (parameters.shutdownSignal() != null) return { env: environment, shouldBreak: true };
|
|
2310
|
+
const connection = childModule.local === true ? null : ssh;
|
|
2311
|
+
startModuleSpinner(childModule.name);
|
|
2312
|
+
const checkResult = await childModule.check(connection, environment);
|
|
2313
|
+
if (parameters.shutdownSignal() != null) return { env: environment, shouldBreak: true };
|
|
2314
|
+
if (checkResult !== "ok" && shouldExecuteApplyDuringDryRun(childModule, diff)) {
|
|
2315
|
+
return executeDryRunBlockingModule({
|
|
2316
|
+
childModule,
|
|
2317
|
+
connection,
|
|
2318
|
+
diff,
|
|
2319
|
+
environment,
|
|
2320
|
+
shutdownSignal: parameters.shutdownSignal,
|
|
2321
|
+
verbose
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
const status = checkResult === "ok" ? "ok" : "changed";
|
|
2325
|
+
const suffix = checkResult === "ok" ? void 0 : "(dry-run)";
|
|
2326
|
+
printModuleResult(childModule.name, status, suffix);
|
|
2327
|
+
return { env: environment, shouldBreak: false, status };
|
|
2328
|
+
}
|
|
2329
|
+
function applyDryRunChildResult(accumulator, result) {
|
|
2330
|
+
return {
|
|
2331
|
+
aggregatedMeta: result.meta == null ? accumulator.aggregatedMeta : [...accumulator.aggregatedMeta, ...result.meta],
|
|
2332
|
+
aggregatedStatus: result.status === "changed" ? "changed" : accumulator.aggregatedStatus,
|
|
2333
|
+
currentEnvironment: result.env
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
async function runDryRunChildLoop(parameters) {
|
|
2337
|
+
let accumulator = parameters.accumulator;
|
|
2338
|
+
for (const childModule of parameters.recipeModule._modules) {
|
|
2339
|
+
if (parameters.shutdownSignal() != null) {
|
|
2340
|
+
return interruptedDryRunResult({
|
|
2341
|
+
aggregatedMeta: accumulator.aggregatedMeta,
|
|
2342
|
+
aggregatedStatus: accumulator.aggregatedStatus,
|
|
2343
|
+
currentEnvironment: accumulator.currentEnvironment
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
const result = await executeDryRunChildModule({
|
|
2347
|
+
childModule,
|
|
2348
|
+
diff: parameters.diff,
|
|
2349
|
+
environment: accumulator.currentEnvironment,
|
|
2350
|
+
shutdownSignal: parameters.shutdownSignal,
|
|
2351
|
+
ssh: parameters.ssh,
|
|
2352
|
+
verbose: parameters.verbose
|
|
2353
|
+
});
|
|
2354
|
+
if (result.shouldBreak) return result;
|
|
2355
|
+
accumulator = applyDryRunChildResult(accumulator, result);
|
|
2356
|
+
}
|
|
2357
|
+
return accumulator;
|
|
2358
|
+
}
|
|
2359
|
+
async function dryRunRecipeModule(parameters) {
|
|
2360
|
+
return withRecipeOutputScope(async () => {
|
|
2361
|
+
const { environment, recipeModule, ssh } = parameters;
|
|
2362
|
+
printRecipeHeader(recipeModule.name);
|
|
2363
|
+
const shutdownSignal = parameters.shutdownSignal ?? (() => null);
|
|
2364
|
+
const loopResult = await runDryRunChildLoop({
|
|
2365
|
+
accumulator: {
|
|
2366
|
+
aggregatedMeta: [],
|
|
2367
|
+
aggregatedStatus: "ok",
|
|
2368
|
+
currentEnvironment: environment
|
|
2369
|
+
},
|
|
2370
|
+
diff: parameters.options?.diff ?? false,
|
|
2371
|
+
recipeModule,
|
|
2372
|
+
shutdownSignal,
|
|
2373
|
+
ssh,
|
|
2374
|
+
verbose: parameters.options?.verbose ?? false
|
|
2375
|
+
});
|
|
2376
|
+
if ("shouldBreak" in loopResult) return loopResult;
|
|
2377
|
+
return {
|
|
2378
|
+
env: loopResult.currentEnvironment,
|
|
2379
|
+
meta: loopResult.aggregatedMeta.length === 0 ? void 0 : loopResult.aggregatedMeta,
|
|
2380
|
+
shouldBreak: false,
|
|
2381
|
+
status: loopResult.aggregatedStatus
|
|
2382
|
+
};
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
// src/runnerAbortSignal.ts
|
|
2387
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
2388
|
+
var runnerAbortSignalStorage = new AsyncLocalStorage3();
|
|
2389
|
+
async function withRunnerAbortSignal(signal, body) {
|
|
2390
|
+
return runnerAbortSignalStorage.run(signal, body);
|
|
2391
|
+
}
|
|
2392
|
+
function getRunnerAbortSignal() {
|
|
2393
|
+
return runnerAbortSignalStorage.getStore();
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// src/signalOrchestration.ts
|
|
2397
|
+
function handleSignalResult(parameters) {
|
|
2398
|
+
const { hooks, result, signalName, verbose } = parameters;
|
|
2399
|
+
printModuleResult(`signal: ${signalName}`, result.status);
|
|
2400
|
+
if (result.status === "failed" && result.error != null) {
|
|
2401
|
+
printCommandFailure(result.error, verbose);
|
|
2402
|
+
}
|
|
2403
|
+
hooks?.onSignalFinished?.(result.status);
|
|
2404
|
+
return result.status === "failed" ? "failed" : "changed";
|
|
2405
|
+
}
|
|
2406
|
+
function handleSignalFailure(parameters) {
|
|
2407
|
+
const { error, hooks, signalName, verbose } = parameters;
|
|
2408
|
+
printModuleResult(`signal: ${signalName}`, "failed");
|
|
2409
|
+
printCommandFailure(error, verbose);
|
|
2410
|
+
hooks?.onSignalFinished?.("failed");
|
|
2411
|
+
return "failed";
|
|
2412
|
+
}
|
|
2413
|
+
async function applySignalMeta(parameters) {
|
|
2414
|
+
assertValidModuleMetaEntries(parameters.result.meta);
|
|
2415
|
+
const nextEnvironment = parameters.result.status === "failed" ? parameters.currentEnvironment : await mergeEnvironmentFromMeta(parameters.currentEnvironment, parameters.result.meta);
|
|
2416
|
+
await parameters.onSignalStep?.({
|
|
2417
|
+
env: nextEnvironment,
|
|
2418
|
+
meta: parameters.result.meta,
|
|
2419
|
+
status: parameters.result.status
|
|
2420
|
+
});
|
|
2421
|
+
return nextEnvironment;
|
|
2422
|
+
}
|
|
2423
|
+
async function runOneSignal(parameters) {
|
|
2424
|
+
const connection = parameters.signal.local === true ? null : parameters.ssh;
|
|
2425
|
+
startModuleSpinner(`signal: ${parameters.signal.name}`);
|
|
2426
|
+
const result = parameters.shutdownSignal == null ? await parameters.signal.apply(connection, parameters.currentEnvironment) : await parameters.signal.apply(connection, parameters.currentEnvironment, {
|
|
2427
|
+
shutdownSignal: parameters.shutdownSignal
|
|
2428
|
+
});
|
|
2429
|
+
const nextEnvironment = await applySignalMeta({
|
|
2430
|
+
currentEnvironment: parameters.currentEnvironment,
|
|
2431
|
+
onSignalStep: parameters.onSignalStep,
|
|
2432
|
+
result
|
|
2433
|
+
});
|
|
2434
|
+
return {
|
|
2435
|
+
nextEnvironment,
|
|
2436
|
+
status: handleSignalResult({
|
|
2437
|
+
hooks: parameters.hooks,
|
|
2438
|
+
result,
|
|
2439
|
+
signalName: parameters.signal.name,
|
|
2440
|
+
verbose: parameters.verbose
|
|
2441
|
+
})
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
async function runSignalModules(parameters) {
|
|
2445
|
+
const getShutdownSignal = parameters.shutdownSignal ?? (() => null);
|
|
2446
|
+
const verbose = parameters.verbose ?? false;
|
|
2447
|
+
let currentEnvironment = parameters.environment;
|
|
2448
|
+
let status = "changed";
|
|
2449
|
+
for (const signal of parameters.signals) {
|
|
2450
|
+
if (getShutdownSignal() != null) break;
|
|
2451
|
+
parameters.hooks?.onSignalStarted?.();
|
|
2452
|
+
try {
|
|
2453
|
+
const signalStep = await runOneSignal({
|
|
2454
|
+
currentEnvironment,
|
|
2455
|
+
hooks: parameters.hooks,
|
|
2456
|
+
onSignalStep: parameters.onSignalStep,
|
|
2457
|
+
shutdownSignal: parameters.shutdownSignal,
|
|
2458
|
+
signal,
|
|
2459
|
+
ssh: parameters.ssh,
|
|
2460
|
+
verbose
|
|
2461
|
+
});
|
|
2462
|
+
currentEnvironment = signalStep.nextEnvironment;
|
|
2463
|
+
if (signalStep.status === "failed") status = "failed";
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
status = handleSignalFailure({
|
|
2466
|
+
error,
|
|
2467
|
+
hooks: parameters.hooks,
|
|
2468
|
+
signalName: signal.name,
|
|
2469
|
+
verbose
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
return status;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// src/types.ts
|
|
2477
|
+
var NEEDS_APPLY = "needs-apply";
|
|
2478
|
+
|
|
2479
|
+
// src/recipe.ts
|
|
2480
|
+
var INTERRUPTED_BEFORE_APPLY = /* @__PURE__ */ Symbol("recipe-interrupted-before-apply");
|
|
2481
|
+
function isRecipe(module) {
|
|
2482
|
+
return module.kind === "recipe";
|
|
2483
|
+
}
|
|
2484
|
+
function printRecipeChildResult(module, result) {
|
|
2485
|
+
if (isRecipe(module)) {
|
|
2486
|
+
printRecipeModuleResult(module.name, result.status, result.detail);
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
printModuleResult(module.name, result.status, result.detail);
|
|
2490
|
+
}
|
|
2491
|
+
function applyRecipeStepToState(state, step, preserveControlPlaneMeta) {
|
|
2492
|
+
let stepMeta;
|
|
2493
|
+
if (step.meta == null) {
|
|
2494
|
+
stepMeta = void 0;
|
|
2495
|
+
} else if (preserveControlPlaneMeta) {
|
|
2496
|
+
stepMeta = step.meta;
|
|
2497
|
+
} else {
|
|
2498
|
+
stepMeta = step.meta.filter(isEnvironmentMetaEntry);
|
|
2499
|
+
}
|
|
2500
|
+
const nextMeta = stepMeta == null ? state.meta ?? [] : [...state.meta ?? [], ...stepMeta];
|
|
2501
|
+
let nextStatus = state.status;
|
|
2502
|
+
if (step.status === "failed") nextStatus = "failed";
|
|
2503
|
+
else if (step.status === "changed") nextStatus = "changed";
|
|
2504
|
+
return {
|
|
2505
|
+
env: step.env,
|
|
2506
|
+
meta: nextMeta,
|
|
2507
|
+
signalsPending: step.status === "changed" ? true : state.signalsPending,
|
|
2508
|
+
status: nextStatus,
|
|
2509
|
+
stopRun: step._stopRun === true ? true : state.stopRun
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
async function executeOneModule(parameters) {
|
|
2513
|
+
const { currentEnvironment, ssh, targetModule } = parameters;
|
|
2514
|
+
const verbose = parameters.verbose ?? false;
|
|
2515
|
+
const connection = targetModule.local === true ? null : ssh;
|
|
2516
|
+
const checkResult = await checkRecipeChild(targetModule, connection, currentEnvironment);
|
|
2517
|
+
if (checkResult === "ok") {
|
|
2518
|
+
printModuleResult(targetModule.name, "ok");
|
|
2519
|
+
return null;
|
|
2520
|
+
}
|
|
2521
|
+
if ((parameters.shutdownSignal?.() ?? null) != null) {
|
|
2522
|
+
return INTERRUPTED_BEFORE_APPLY;
|
|
2523
|
+
}
|
|
2524
|
+
const result = await applyRecipeChild({
|
|
2525
|
+
connection,
|
|
2526
|
+
currentEnvironment,
|
|
2527
|
+
onChildStep: parameters.onChildStep,
|
|
2528
|
+
onSignalStep: parameters.onSignalStep,
|
|
2529
|
+
shutdownSignal: parameters.shutdownSignal,
|
|
2530
|
+
signalHooks: parameters.signalHooks,
|
|
2531
|
+
targetModule,
|
|
2532
|
+
verbose
|
|
2533
|
+
});
|
|
2534
|
+
printRecipeChildResult(targetModule, result);
|
|
2254
2535
|
if (result.status === "failed" && result.error != null) {
|
|
2255
2536
|
printCommandFailure(result.error, verbose);
|
|
2256
2537
|
}
|
|
2538
|
+
const environment = result.status === "failed" ? currentEnvironment : await mergeEnvironmentFromMeta(currentEnvironment, result.meta);
|
|
2257
2539
|
return {
|
|
2258
|
-
|
|
2540
|
+
_flushSignals: result._flushSignals,
|
|
2541
|
+
_stopRun: result._stopRun,
|
|
2542
|
+
env: environment,
|
|
2259
2543
|
meta: result.meta,
|
|
2260
|
-
|
|
2261
|
-
status: result.status,
|
|
2262
|
-
stopRun: result._stopRun
|
|
2544
|
+
status: result.status
|
|
2263
2545
|
};
|
|
2264
2546
|
}
|
|
2265
|
-
async function
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
childModule,
|
|
2275
|
-
connection,
|
|
2276
|
-
diff,
|
|
2277
|
-
environment,
|
|
2547
|
+
async function checkRecipeChild(targetModule, connection, currentEnvironment) {
|
|
2548
|
+
startModuleSpinner(targetModule.name);
|
|
2549
|
+
return targetModule.check(connection, currentEnvironment);
|
|
2550
|
+
}
|
|
2551
|
+
async function applyRecipeChild(parameters) {
|
|
2552
|
+
if (isRecipe(parameters.targetModule)) {
|
|
2553
|
+
return parameters.targetModule.apply(parameters.connection, parameters.currentEnvironment, {
|
|
2554
|
+
onChildStep: parameters.onChildStep,
|
|
2555
|
+
onSignalStep: parameters.onSignalStep,
|
|
2278
2556
|
shutdownSignal: parameters.shutdownSignal,
|
|
2279
|
-
|
|
2557
|
+
signalHooks: parameters.signalHooks,
|
|
2558
|
+
verbose: parameters.verbose
|
|
2280
2559
|
});
|
|
2281
2560
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2561
|
+
if (parameters.targetModule._supportsChildStepHook === true && (parameters.onChildStep != null || parameters.shutdownSignal != null)) {
|
|
2562
|
+
return parameters.targetModule.apply(parameters.connection, parameters.currentEnvironment, {
|
|
2563
|
+
onChildStep: parameters.onChildStep,
|
|
2564
|
+
shutdownSignal: parameters.shutdownSignal
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
return parameters.targetModule.apply(parameters.connection, parameters.currentEnvironment);
|
|
2286
2568
|
}
|
|
2287
|
-
function
|
|
2569
|
+
async function applyExecutedRecipeStep(parameters) {
|
|
2570
|
+
if (parameters.step == null) return parameters.state;
|
|
2571
|
+
if (parameters.step === INTERRUPTED_BEFORE_APPLY) return null;
|
|
2572
|
+
if (parameters.onChildStep != null) {
|
|
2573
|
+
await parameters.onChildStep(parameters.step);
|
|
2574
|
+
}
|
|
2575
|
+
return applyRecipeStepToState(
|
|
2576
|
+
parameters.state,
|
|
2577
|
+
parameters.step,
|
|
2578
|
+
parameters.preserveControlPlaneMeta
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
function failedRecipeState(environment) {
|
|
2288
2582
|
return {
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2583
|
+
env: environment,
|
|
2584
|
+
meta: void 0,
|
|
2585
|
+
signalsPending: false,
|
|
2586
|
+
status: "failed"
|
|
2292
2587
|
};
|
|
2293
2588
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2589
|
+
function annotateRecipeChildError(moduleName, error) {
|
|
2590
|
+
const prefix = `[${moduleName}] `;
|
|
2591
|
+
if (error instanceof CommandError) {
|
|
2592
|
+
return new CommandError(`${prefix}${error.message}`, error.fullStdout, error.fullStderr);
|
|
2593
|
+
}
|
|
2594
|
+
if (error instanceof Error) {
|
|
2595
|
+
return new Error(`${prefix}${error.message}`);
|
|
2596
|
+
}
|
|
2597
|
+
return new Error(`${prefix}${String(error)}`);
|
|
2598
|
+
}
|
|
2599
|
+
async function executeRecipeChildStep(parameters) {
|
|
2600
|
+
try {
|
|
2601
|
+
return { kind: "step", step: await executeOneModule(parameters) };
|
|
2602
|
+
} catch (error) {
|
|
2297
2603
|
if (parameters.shutdownSignal() != null) {
|
|
2298
|
-
return
|
|
2299
|
-
aggregatedMeta: accumulator.aggregatedMeta,
|
|
2300
|
-
aggregatedStatus: accumulator.aggregatedStatus,
|
|
2301
|
-
currentEnvironment: accumulator.currentEnvironment
|
|
2302
|
-
});
|
|
2604
|
+
return { kind: "step", step: INTERRUPTED_BEFORE_APPLY };
|
|
2303
2605
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2606
|
+
printModuleResult(parameters.targetModule.name, "failed");
|
|
2607
|
+
printCommandFailure(error, parameters.verbose);
|
|
2608
|
+
return { kind: "failed", state: failedRecipeState(parameters.currentEnvironment) };
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
async function processRecipeStep(parameters) {
|
|
2612
|
+
if (parameters.step.kind === "failed") {
|
|
2613
|
+
return { kind: "break", state: parameters.step.state };
|
|
2614
|
+
}
|
|
2615
|
+
const nextState = await applyExecutedRecipeStep({
|
|
2616
|
+
onChildStep: parameters.onChildStep,
|
|
2617
|
+
preserveControlPlaneMeta: parameters.preserveControlPlaneMeta,
|
|
2618
|
+
state: parameters.state,
|
|
2619
|
+
step: parameters.step.step
|
|
2620
|
+
});
|
|
2621
|
+
if (nextState == null) {
|
|
2622
|
+
return { kind: "break", state: parameters.state };
|
|
2623
|
+
}
|
|
2624
|
+
let state = nextState;
|
|
2625
|
+
if (shouldFlushRecipeSignals(parameters.step.step, state, parameters.signals)) {
|
|
2626
|
+
const signalStatus = await flushPendingRecipeSignals({
|
|
2627
|
+
environment: state.env,
|
|
2628
|
+
onSignalStep: parameters.onSignalStep,
|
|
2308
2629
|
shutdownSignal: parameters.shutdownSignal,
|
|
2630
|
+
signalHooks: parameters.signalHooks,
|
|
2631
|
+
signals: parameters.signals,
|
|
2309
2632
|
ssh: parameters.ssh,
|
|
2310
2633
|
verbose: parameters.verbose
|
|
2311
2634
|
});
|
|
2312
|
-
|
|
2313
|
-
accumulator = applyDryRunChildResult(accumulator, result);
|
|
2635
|
+
state = applyRecipeSignalStatus(state, signalStatus);
|
|
2314
2636
|
}
|
|
2315
|
-
|
|
2637
|
+
if (state.status === "failed" || state.stopRun === true) {
|
|
2638
|
+
return { kind: "break", state };
|
|
2639
|
+
}
|
|
2640
|
+
return { kind: "continue", state };
|
|
2316
2641
|
}
|
|
2317
|
-
async function
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2642
|
+
async function executeModules(modules, ssh, parameters) {
|
|
2643
|
+
const onChildStep = parameters.onChildStep;
|
|
2644
|
+
const preserveControlPlaneMeta = onChildStep == null;
|
|
2645
|
+
const shutdownSignal = parameters.shutdownSignal ?? (() => null);
|
|
2646
|
+
const verbose = parameters.verbose ?? false;
|
|
2647
|
+
let state = {
|
|
2648
|
+
// R-0000079: preserve the null-prototype guarantee that
|
|
2649
|
+
// R-0000069/R-0000070/R-0000074 establish for the runner-level
|
|
2650
|
+
// environment. Plain object spread (`{ ...environment }`) would create a
|
|
2651
|
+
// map with `Object.prototype`, exposing the first child module of every
|
|
2652
|
+
// recipe to a polluted-fähige environment. Mirror the meta.ts:214
|
|
2653
|
+
// approach.
|
|
2654
|
+
env: Object.assign(createNullPrototypeEnvironment(), parameters.environment),
|
|
2655
|
+
meta: void 0,
|
|
2656
|
+
signalsPending: false,
|
|
2657
|
+
status: "ok"
|
|
2658
|
+
};
|
|
2659
|
+
for (const currentModule of modules) {
|
|
2660
|
+
if (shutdownSignal() != null) break;
|
|
2661
|
+
const step = await executeRecipeChildStep({
|
|
2662
|
+
currentEnvironment: state.env,
|
|
2663
|
+
onChildStep,
|
|
2664
|
+
onSignalStep: parameters.onSignalStep,
|
|
2330
2665
|
shutdownSignal,
|
|
2666
|
+
signalHooks: parameters.signalHooks,
|
|
2331
2667
|
ssh,
|
|
2332
|
-
|
|
2668
|
+
targetModule: currentModule,
|
|
2669
|
+
verbose
|
|
2333
2670
|
});
|
|
2334
|
-
|
|
2671
|
+
const processedStep = await processRecipeStep({
|
|
2672
|
+
onChildStep,
|
|
2673
|
+
onSignalStep: parameters.onSignalStep,
|
|
2674
|
+
preserveControlPlaneMeta,
|
|
2675
|
+
shutdownSignal,
|
|
2676
|
+
signalHooks: parameters.signalHooks,
|
|
2677
|
+
signals: parameters.signals,
|
|
2678
|
+
ssh,
|
|
2679
|
+
state,
|
|
2680
|
+
step,
|
|
2681
|
+
verbose
|
|
2682
|
+
});
|
|
2683
|
+
state = processedStep.state;
|
|
2684
|
+
if (processedStep.kind === "break") return state;
|
|
2685
|
+
}
|
|
2686
|
+
return state;
|
|
2687
|
+
}
|
|
2688
|
+
async function triggerSignals(parameters) {
|
|
2689
|
+
return runSignalModules({
|
|
2690
|
+
environment: parameters.environment,
|
|
2691
|
+
hooks: parameters.signalHooks,
|
|
2692
|
+
onSignalStep: parameters.onSignalStep,
|
|
2693
|
+
shutdownSignal: parameters.shutdownSignal,
|
|
2694
|
+
signals: parameters.signals,
|
|
2695
|
+
ssh: parameters.ssh,
|
|
2696
|
+
verbose: parameters.verbose
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
function shouldFlushRecipeSignals(step, state, signals) {
|
|
2700
|
+
return step != null && step !== INTERRUPTED_BEFORE_APPLY && step._flushSignals === true && state.signalsPending && signals != null;
|
|
2701
|
+
}
|
|
2702
|
+
function applyRecipeSignalStatus(state, signalStatus) {
|
|
2703
|
+
return {
|
|
2704
|
+
...state,
|
|
2705
|
+
signalsPending: false,
|
|
2706
|
+
status: signalStatus === "failed" ? "failed" : state.status
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
async function flushPendingRecipeSignals(parameters) {
|
|
2710
|
+
return triggerSignals(parameters);
|
|
2711
|
+
}
|
|
2712
|
+
async function applyRecipe(parameters) {
|
|
2713
|
+
return withRecipeOutputScope(async () => {
|
|
2714
|
+
const shutdownSignal = parameters.options?.shutdownSignal;
|
|
2715
|
+
const verbose = parameters.options?.verbose ?? false;
|
|
2716
|
+
printRecipeHeader(parameters.name);
|
|
2717
|
+
const state = await executeModules(parameters.modules, parameters.ssh, {
|
|
2718
|
+
environment: parameters.environment,
|
|
2719
|
+
onChildStep: parameters.options?.onChildStep,
|
|
2720
|
+
onSignalStep: parameters.options?.onSignalStep,
|
|
2721
|
+
shutdownSignal,
|
|
2722
|
+
signalHooks: parameters.options?.signalHooks,
|
|
2723
|
+
signals: parameters.signals,
|
|
2724
|
+
verbose
|
|
2725
|
+
});
|
|
2726
|
+
if (shouldRunRecipeSignalsAtEnd(state, parameters.signals)) {
|
|
2727
|
+
state.status = await triggerSignals({
|
|
2728
|
+
environment: state.env,
|
|
2729
|
+
onSignalStep: parameters.options?.onSignalStep,
|
|
2730
|
+
shutdownSignal,
|
|
2731
|
+
signalHooks: parameters.options?.signalHooks,
|
|
2732
|
+
signals: parameters.signals,
|
|
2733
|
+
ssh: parameters.ssh,
|
|
2734
|
+
verbose
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2335
2737
|
return {
|
|
2336
|
-
|
|
2337
|
-
meta:
|
|
2338
|
-
|
|
2339
|
-
status: loopResult.aggregatedStatus
|
|
2738
|
+
_stopRun: state.stopRun,
|
|
2739
|
+
meta: state.meta,
|
|
2740
|
+
status: state.status
|
|
2340
2741
|
};
|
|
2341
2742
|
});
|
|
2342
2743
|
}
|
|
2744
|
+
function shouldRunRecipeSignalsAtEnd(state, signals) {
|
|
2745
|
+
return state.signalsPending && state.status === "changed" && signals != null;
|
|
2746
|
+
}
|
|
2747
|
+
function shouldExecuteRecipeDryRun(module) {
|
|
2748
|
+
return module._applyDryRun != null || module._dryRunBlocker === true || module._dryRunMetaProducer === true;
|
|
2749
|
+
}
|
|
2750
|
+
function recipeNeedsDryRunApply(modules) {
|
|
2751
|
+
return modules.some((module) => shouldExecuteRecipeDryRun(module));
|
|
2752
|
+
}
|
|
2753
|
+
function createRecipeDryRunApply(name, modules, needsDryRunApply) {
|
|
2754
|
+
if (!needsDryRunApply) return void 0;
|
|
2755
|
+
return async (ssh, environment, parameters) => {
|
|
2756
|
+
const result = await dryRunRecipeModule({
|
|
2757
|
+
environment,
|
|
2758
|
+
recipeModule: {
|
|
2759
|
+
_modules: modules,
|
|
2760
|
+
async apply() {
|
|
2761
|
+
await Promise.resolve();
|
|
2762
|
+
return { status: "ok" };
|
|
2763
|
+
},
|
|
2764
|
+
async check() {
|
|
2765
|
+
await Promise.resolve();
|
|
2766
|
+
return "ok";
|
|
2767
|
+
},
|
|
2768
|
+
kind: "recipe",
|
|
2769
|
+
name
|
|
2770
|
+
},
|
|
2771
|
+
shutdownSignal: parameters?.shutdownSignal,
|
|
2772
|
+
ssh
|
|
2773
|
+
});
|
|
2774
|
+
return {
|
|
2775
|
+
_stopRun: result.stopRun,
|
|
2776
|
+
meta: result.meta,
|
|
2777
|
+
status: result.status ?? "ok"
|
|
2778
|
+
};
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
function recipe(name, modules, options) {
|
|
2782
|
+
const needsDryRunApply = recipeNeedsDryRunApply(modules);
|
|
2783
|
+
const applyDryRun = createRecipeDryRunApply(name, modules, needsDryRunApply);
|
|
2784
|
+
return {
|
|
2785
|
+
...modules.some((module) => module._dryRunBlocker === true) ? { _dryRunBlocker: true } : {},
|
|
2786
|
+
...modules.some((module) => module._dryRunMetaProducer === true) ? { _dryRunMetaProducer: true } : {},
|
|
2787
|
+
...applyDryRun == null ? {} : { _applyDryRun: applyDryRun },
|
|
2788
|
+
_modules: modules,
|
|
2789
|
+
_signals: options?.signals,
|
|
2790
|
+
_supportsChildStepHook: true,
|
|
2791
|
+
async apply(ssh, environment, parameters) {
|
|
2792
|
+
return applyRecipe({
|
|
2793
|
+
environment,
|
|
2794
|
+
modules,
|
|
2795
|
+
name,
|
|
2796
|
+
options: parameters,
|
|
2797
|
+
signals: options?.signals,
|
|
2798
|
+
ssh
|
|
2799
|
+
});
|
|
2800
|
+
},
|
|
2801
|
+
async check(ssh, environment) {
|
|
2802
|
+
for (const childModule of modules) {
|
|
2803
|
+
if (getRunnerAbortSignal()?.aborted === true) return NEEDS_APPLY;
|
|
2804
|
+
const connection = childModule.local === true ? null : ssh;
|
|
2805
|
+
let result;
|
|
2806
|
+
try {
|
|
2807
|
+
result = await childModule.check(connection, environment);
|
|
2808
|
+
} catch (error) {
|
|
2809
|
+
throw annotateRecipeChildError(childModule.name, error);
|
|
2810
|
+
}
|
|
2811
|
+
if (result === NEEDS_APPLY) return NEEDS_APPLY;
|
|
2812
|
+
}
|
|
2813
|
+
return "ok";
|
|
2814
|
+
},
|
|
2815
|
+
kind: "recipe",
|
|
2816
|
+
name
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2343
2819
|
|
|
2344
|
-
// src/
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2820
|
+
// src/moduleFilter.ts
|
|
2821
|
+
var SKIP_DRY_RUN_DETAIL = "filtered out";
|
|
2822
|
+
function parseFilterNames(rawValues) {
|
|
2823
|
+
const names = [];
|
|
2824
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2825
|
+
for (const rawValue of rawValues) {
|
|
2826
|
+
for (const part of rawValue.split(",")) {
|
|
2827
|
+
const name = part.trim();
|
|
2828
|
+
if (name === "" || seen.has(name)) continue;
|
|
2829
|
+
seen.add(name);
|
|
2830
|
+
names.push(name);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
return names;
|
|
2834
|
+
}
|
|
2835
|
+
function collectModuleNames(modules) {
|
|
2836
|
+
const names = /* @__PURE__ */ new Set();
|
|
2837
|
+
const visit = (module) => {
|
|
2838
|
+
names.add(module.name);
|
|
2839
|
+
if (isRecipe(module)) {
|
|
2840
|
+
for (const child of module._modules) visit(child);
|
|
2841
|
+
}
|
|
2842
|
+
};
|
|
2843
|
+
for (const module of modules) visit(module);
|
|
2844
|
+
return names;
|
|
2845
|
+
}
|
|
2846
|
+
function subtreeHasFilterMatch(module, filter) {
|
|
2847
|
+
if (filter.has(module.name)) return true;
|
|
2848
|
+
if (isRecipe(module)) {
|
|
2849
|
+
return module._modules.some((child) => subtreeHasFilterMatch(child, filter));
|
|
2850
|
+
}
|
|
2851
|
+
return false;
|
|
2852
|
+
}
|
|
2853
|
+
function createSkipModule(name) {
|
|
2854
|
+
return {
|
|
2855
|
+
async _applyDryRun() {
|
|
2856
|
+
await Promise.resolve();
|
|
2857
|
+
return { _dryRunDetail: SKIP_DRY_RUN_DETAIL, status: "skipped" };
|
|
2858
|
+
},
|
|
2859
|
+
_dryRunBlocker: true,
|
|
2860
|
+
async apply() {
|
|
2861
|
+
await Promise.resolve();
|
|
2862
|
+
return { status: "skipped" };
|
|
2863
|
+
},
|
|
2864
|
+
async check() {
|
|
2865
|
+
await Promise.resolve();
|
|
2866
|
+
return NEEDS_APPLY;
|
|
2867
|
+
},
|
|
2868
|
+
local: true,
|
|
2869
|
+
name
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
function filterNode(module, filter, ancestorSelected) {
|
|
2873
|
+
const selfSelected = ancestorSelected || filter.has(module.name);
|
|
2874
|
+
if (selfSelected) return module;
|
|
2875
|
+
if (isRecipe(module) && subtreeHasFilterMatch(module, filter)) {
|
|
2876
|
+
const filteredChildren = module._modules.map((child) => filterNode(child, filter, false));
|
|
2877
|
+
return recipe(module.name, filteredChildren, { signals: module._signals });
|
|
2878
|
+
}
|
|
2879
|
+
return createSkipModule(module.name);
|
|
2880
|
+
}
|
|
2881
|
+
function applyModuleFilter(modules, filter) {
|
|
2882
|
+
return modules.map((module) => filterNode(module, filter, false));
|
|
2349
2883
|
}
|
|
2350
2884
|
|
|
2351
2885
|
// src/runnerHelpers.ts
|
|
@@ -2425,86 +2959,6 @@ function getSignalBus() {
|
|
|
2425
2959
|
return readActiveBus() ?? defaultProcessSignalBus;
|
|
2426
2960
|
}
|
|
2427
2961
|
|
|
2428
|
-
// src/signalOrchestration.ts
|
|
2429
|
-
function handleSignalResult(parameters) {
|
|
2430
|
-
const { hooks, result, signalName, verbose } = parameters;
|
|
2431
|
-
printModuleResult(`signal: ${signalName}`, result.status);
|
|
2432
|
-
if (result.status === "failed" && result.error != null) {
|
|
2433
|
-
printCommandFailure(result.error, verbose);
|
|
2434
|
-
}
|
|
2435
|
-
hooks?.onSignalFinished?.(result.status);
|
|
2436
|
-
return result.status === "failed" ? "failed" : "changed";
|
|
2437
|
-
}
|
|
2438
|
-
function handleSignalFailure(parameters) {
|
|
2439
|
-
const { error, hooks, signalName, verbose } = parameters;
|
|
2440
|
-
printModuleResult(`signal: ${signalName}`, "failed");
|
|
2441
|
-
printCommandFailure(error, verbose);
|
|
2442
|
-
hooks?.onSignalFinished?.("failed");
|
|
2443
|
-
return "failed";
|
|
2444
|
-
}
|
|
2445
|
-
async function applySignalMeta(parameters) {
|
|
2446
|
-
assertValidModuleMetaEntries(parameters.result.meta);
|
|
2447
|
-
const nextEnvironment = parameters.result.status === "failed" ? parameters.currentEnvironment : await mergeEnvironmentFromMeta(parameters.currentEnvironment, parameters.result.meta);
|
|
2448
|
-
await parameters.onSignalStep?.({
|
|
2449
|
-
env: nextEnvironment,
|
|
2450
|
-
meta: parameters.result.meta,
|
|
2451
|
-
status: parameters.result.status
|
|
2452
|
-
});
|
|
2453
|
-
return nextEnvironment;
|
|
2454
|
-
}
|
|
2455
|
-
async function runOneSignal(parameters) {
|
|
2456
|
-
const connection = parameters.signal.local === true ? null : parameters.ssh;
|
|
2457
|
-
startModuleSpinner(`signal: ${parameters.signal.name}`);
|
|
2458
|
-
const result = parameters.shutdownSignal == null ? await parameters.signal.apply(connection, parameters.currentEnvironment) : await parameters.signal.apply(connection, parameters.currentEnvironment, {
|
|
2459
|
-
shutdownSignal: parameters.shutdownSignal
|
|
2460
|
-
});
|
|
2461
|
-
const nextEnvironment = await applySignalMeta({
|
|
2462
|
-
currentEnvironment: parameters.currentEnvironment,
|
|
2463
|
-
onSignalStep: parameters.onSignalStep,
|
|
2464
|
-
result
|
|
2465
|
-
});
|
|
2466
|
-
return {
|
|
2467
|
-
nextEnvironment,
|
|
2468
|
-
status: handleSignalResult({
|
|
2469
|
-
hooks: parameters.hooks,
|
|
2470
|
-
result,
|
|
2471
|
-
signalName: parameters.signal.name,
|
|
2472
|
-
verbose: parameters.verbose
|
|
2473
|
-
})
|
|
2474
|
-
};
|
|
2475
|
-
}
|
|
2476
|
-
async function runSignalModules(parameters) {
|
|
2477
|
-
const getShutdownSignal = parameters.shutdownSignal ?? (() => null);
|
|
2478
|
-
const verbose = parameters.verbose ?? false;
|
|
2479
|
-
let currentEnvironment = parameters.environment;
|
|
2480
|
-
let status = "changed";
|
|
2481
|
-
for (const signal of parameters.signals) {
|
|
2482
|
-
if (getShutdownSignal() != null) break;
|
|
2483
|
-
parameters.hooks?.onSignalStarted?.();
|
|
2484
|
-
try {
|
|
2485
|
-
const signalStep = await runOneSignal({
|
|
2486
|
-
currentEnvironment,
|
|
2487
|
-
hooks: parameters.hooks,
|
|
2488
|
-
onSignalStep: parameters.onSignalStep,
|
|
2489
|
-
shutdownSignal: parameters.shutdownSignal,
|
|
2490
|
-
signal,
|
|
2491
|
-
ssh: parameters.ssh,
|
|
2492
|
-
verbose
|
|
2493
|
-
});
|
|
2494
|
-
currentEnvironment = signalStep.nextEnvironment;
|
|
2495
|
-
if (signalStep.status === "failed") status = "failed";
|
|
2496
|
-
} catch (error) {
|
|
2497
|
-
status = handleSignalFailure({
|
|
2498
|
-
error,
|
|
2499
|
-
hooks: parameters.hooks,
|
|
2500
|
-
signalName: signal.name,
|
|
2501
|
-
verbose
|
|
2502
|
-
});
|
|
2503
|
-
}
|
|
2504
|
-
}
|
|
2505
|
-
return status;
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
2962
|
// src/ssh.ts
|
|
2509
2963
|
import { createHash as createHash2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
2510
2964
|
import { createReadStream as createReadStream2 } from "fs";
|
|
@@ -3043,49 +3497,37 @@ function expandHomePath(path) {
|
|
|
3043
3497
|
if (path === "~") return homedir2();
|
|
3044
3498
|
if (path.startsWith("~/")) return join3(homedir2(), path.slice(2));
|
|
3045
3499
|
return path;
|
|
3046
|
-
}
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
`[ssh.writeFile: ${remotePath}] missing options.mode; pass { mode: "0644" } or another explicit file mode`
|
|
3051
|
-
);
|
|
3052
|
-
}
|
|
3500
|
+
}
|
|
3501
|
+
var DEFAULT_FILE_MODE = "0600";
|
|
3502
|
+
function resolveFileMode(operation, remotePath, options) {
|
|
3503
|
+
const mode = options?.mode ?? DEFAULT_FILE_MODE;
|
|
3053
3504
|
try {
|
|
3054
|
-
validateMode(
|
|
3505
|
+
validateMode(mode);
|
|
3055
3506
|
} catch (error) {
|
|
3056
3507
|
const reason = error instanceof Error ? error.message : String(error);
|
|
3057
|
-
throw new Error(
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
);
|
|
3508
|
+
throw new Error(`[ssh.${operation}: ${remotePath}] invalid options.mode "${mode}": ${reason}`, {
|
|
3509
|
+
cause: error
|
|
3510
|
+
});
|
|
3061
3511
|
}
|
|
3062
|
-
return
|
|
3512
|
+
return mode;
|
|
3063
3513
|
}
|
|
3064
3514
|
var COMMAND_TIMEOUT = 12e4;
|
|
3065
|
-
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
|
|
3066
3515
|
var DEFAULT_RECONNECT_TIMEOUT = 12e4;
|
|
3516
|
+
function resolveReconnectBudget(config, defaultTimeout) {
|
|
3517
|
+
return {
|
|
3518
|
+
maxAttempts: config.maxReconnectAttempts ?? Number.POSITIVE_INFINITY,
|
|
3519
|
+
timeout: config.reconnectTimeout ?? defaultTimeout ?? DEFAULT_RECONNECT_TIMEOUT
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3067
3522
|
var EMPTY_FILE_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
3068
3523
|
var DISCONNECT_DESTROY_FALLBACK_MS = 5e3;
|
|
3069
3524
|
var JITTER_BASE = 0.75;
|
|
3070
3525
|
var JITTER_RANGE = 0.5;
|
|
3071
3526
|
var RECONNECT_BASE_DELAY = 1e3;
|
|
3072
3527
|
var RECONNECT_MAX_DELAY = 3e4;
|
|
3073
|
-
var RAW_OUTPUT_ERROR_SNIPPET_LENGTH = 500;
|
|
3074
3528
|
function getAbortReason(signal) {
|
|
3075
3529
|
return signal.reason instanceof Error ? signal.reason : new Error("SSH operation aborted");
|
|
3076
3530
|
}
|
|
3077
|
-
function truncateRawOutputErrorSnippet(text) {
|
|
3078
|
-
let count = 0;
|
|
3079
|
-
let sliceEnd = 0;
|
|
3080
|
-
for (const char of text) {
|
|
3081
|
-
if (count >= RAW_OUTPUT_ERROR_SNIPPET_LENGTH) {
|
|
3082
|
-
return `${text.slice(0, sliceEnd)}\u2026(truncated)`;
|
|
3083
|
-
}
|
|
3084
|
-
sliceEnd += char.length;
|
|
3085
|
-
count++;
|
|
3086
|
-
}
|
|
3087
|
-
return text;
|
|
3088
|
-
}
|
|
3089
3531
|
function toError(error) {
|
|
3090
3532
|
return error instanceof Error ? error : new Error(String(error));
|
|
3091
3533
|
}
|
|
@@ -3363,9 +3805,8 @@ var SshConnectionImpl = class {
|
|
|
3363
3805
|
}
|
|
3364
3806
|
return result.stdout;
|
|
3365
3807
|
}
|
|
3366
|
-
async reconnect() {
|
|
3367
|
-
const timeout = this.config
|
|
3368
|
-
const maxAttempts = this.config.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
3808
|
+
async reconnect(options) {
|
|
3809
|
+
const { maxAttempts, timeout } = resolveReconnectBudget(this.config, options?.defaultTimeout);
|
|
3369
3810
|
const deadline = Date.now() + timeout;
|
|
3370
3811
|
let attempt = 0;
|
|
3371
3812
|
while (Date.now() < deadline && attempt < maxAttempts) {
|
|
@@ -3407,13 +3848,25 @@ var SshConnectionImpl = class {
|
|
|
3407
3848
|
updateHost(host) {
|
|
3408
3849
|
this.runtime.host = host;
|
|
3409
3850
|
}
|
|
3851
|
+
/**
|
|
3852
|
+
* Upload a local file to the remote host via SFTP, staged to a temp file and
|
|
3853
|
+
* finalized atomically with `mv`.
|
|
3854
|
+
*
|
|
3855
|
+
* @param localPath - Path to the local upload source.
|
|
3856
|
+
* @param remotePath - Destination path on the remote host.
|
|
3857
|
+
* @param options - Optional settings for the remote upload.
|
|
3858
|
+
* @param options.mode - File mode to set via `chmod` on the temp file before
|
|
3859
|
+
* moving (e.g. `"0644"`). Optional; defaults to `"0600"` when omitted, matching
|
|
3860
|
+
* {@link writeFile}.
|
|
3861
|
+
*/
|
|
3410
3862
|
async uploadFile(localPath, remotePath, options) {
|
|
3411
3863
|
const client = this.ensureClient();
|
|
3412
3864
|
const localFileStats = await statLocalFile(localPath);
|
|
3413
3865
|
const localFileSize = localFileStats.size;
|
|
3414
3866
|
const expectedHash = await computeLocalFileSha256(localPath);
|
|
3415
3867
|
const temporaryPath = await this.createRemoteWritableTempPath(remotePath, "paratix-upload");
|
|
3416
|
-
const temporaryMode =
|
|
3868
|
+
const temporaryMode = resolveFileMode("uploadFile", remotePath, options);
|
|
3869
|
+
let finalized = false;
|
|
3417
3870
|
try {
|
|
3418
3871
|
await sftpUpload(
|
|
3419
3872
|
client,
|
|
@@ -3425,23 +3878,26 @@ var SshConnectionImpl = class {
|
|
|
3425
3878
|
// remote temp path before they reach an operator-visible error.
|
|
3426
3879
|
prepareSecrets(this.buildSecrets())
|
|
3427
3880
|
);
|
|
3428
|
-
await this.
|
|
3429
|
-
|
|
3430
|
-
|
|
3881
|
+
await this.stageRemoteTempFile({
|
|
3882
|
+
expectedSize: localFileSize,
|
|
3883
|
+
mode: temporaryMode,
|
|
3884
|
+
operation: "uploadFile",
|
|
3885
|
+
temporaryPath
|
|
3886
|
+
});
|
|
3887
|
+
const remoteHash = await this.finalizeAndHashRemoteFile(
|
|
3888
|
+
temporaryPath,
|
|
3889
|
+
remotePath,
|
|
3890
|
+
temporaryMode
|
|
3891
|
+
);
|
|
3892
|
+
finalized = true;
|
|
3431
3893
|
await this.ensureRemoteUploadFile({
|
|
3432
3894
|
expectedHash,
|
|
3433
3895
|
expectedSize: localFileSize,
|
|
3896
|
+
remoteHash,
|
|
3434
3897
|
remotePath
|
|
3435
3898
|
});
|
|
3436
3899
|
} finally {
|
|
3437
|
-
|
|
3438
|
-
await this.cleanupRemoteTempFile(temporaryPath);
|
|
3439
|
-
} catch (cleanupError) {
|
|
3440
|
-
process.stderr.write(
|
|
3441
|
-
`Warning: failed to remove temp file ${temporaryPath}: ${maskSecrets(String(cleanupError), this.buildSecrets())}
|
|
3442
|
-
`
|
|
3443
|
-
);
|
|
3444
|
-
}
|
|
3900
|
+
if (!finalized) await this.cleanupUploadTemporaryPath(temporaryPath);
|
|
3445
3901
|
}
|
|
3446
3902
|
}
|
|
3447
3903
|
/**
|
|
@@ -3453,15 +3909,18 @@ var SshConnectionImpl = class {
|
|
|
3453
3909
|
*
|
|
3454
3910
|
* @param remotePath - Destination path on the remote host.
|
|
3455
3911
|
* @param content - The string content to write.
|
|
3456
|
-
* @param options -
|
|
3457
|
-
* @param options.mode - File mode to set via `chmod` on the temp file before
|
|
3912
|
+
* @param options - Optional settings for the remote write.
|
|
3913
|
+
* @param options.mode - File mode to set via `chmod` on the temp file before
|
|
3914
|
+
* moving (e.g. `"0644"`). Optional; defaults to `"0600"` when omitted, matching
|
|
3915
|
+
* {@link uploadFile}.
|
|
3458
3916
|
*/
|
|
3459
3917
|
async writeFile(remotePath, content, options) {
|
|
3460
3918
|
const client = this.ensureClient();
|
|
3461
3919
|
const remoteTemporary = await this.createRemoteWritableTempPath(remotePath, "paratix-write");
|
|
3462
|
-
const temporaryMode =
|
|
3920
|
+
const temporaryMode = resolveFileMode("writeFile", remotePath, options);
|
|
3463
3921
|
const expectedSize = Buffer.byteLength(content, "utf8");
|
|
3464
3922
|
const expectedHash = createHash2("sha256").update(content, "utf8").digest("hex");
|
|
3923
|
+
let finalized = false;
|
|
3465
3924
|
try {
|
|
3466
3925
|
await sftpUploadContent(
|
|
3467
3926
|
client,
|
|
@@ -3474,18 +3933,28 @@ var SshConnectionImpl = class {
|
|
|
3474
3933
|
// with values registered in the secret sink.
|
|
3475
3934
|
prepareSecrets(this.buildSecrets())
|
|
3476
3935
|
);
|
|
3477
|
-
await this.
|
|
3478
|
-
|
|
3479
|
-
|
|
3936
|
+
await this.stageRemoteTempFile({
|
|
3937
|
+
expectedSize,
|
|
3938
|
+
mode: temporaryMode,
|
|
3939
|
+
operation: "writeFile",
|
|
3940
|
+
temporaryPath: remoteTemporary
|
|
3941
|
+
});
|
|
3942
|
+
const remoteHash = await this.finalizeAndHashRemoteFile(
|
|
3943
|
+
remoteTemporary,
|
|
3944
|
+
remotePath,
|
|
3945
|
+
temporaryMode
|
|
3946
|
+
);
|
|
3947
|
+
finalized = true;
|
|
3480
3948
|
await this.ensureRemoteWriteFile({
|
|
3481
3949
|
content,
|
|
3482
3950
|
expectedHash,
|
|
3483
3951
|
expectedSize,
|
|
3484
3952
|
mode: temporaryMode,
|
|
3953
|
+
remoteHash,
|
|
3485
3954
|
remotePath
|
|
3486
3955
|
});
|
|
3487
3956
|
} finally {
|
|
3488
|
-
await this.cleanupWriteFileTemporaryPath(remoteTemporary);
|
|
3957
|
+
if (!finalized) await this.cleanupWriteFileTemporaryPath(remoteTemporary);
|
|
3489
3958
|
}
|
|
3490
3959
|
}
|
|
3491
3960
|
async agentSocketExists(agent) {
|
|
@@ -3516,29 +3985,6 @@ var SshConnectionImpl = class {
|
|
|
3516
3985
|
);
|
|
3517
3986
|
}
|
|
3518
3987
|
}
|
|
3519
|
-
async assertRemoteFileSize(remotePath, expectedSize, operation = "uploadFile") {
|
|
3520
|
-
const statCommand = `stat -c '%s' ${shellQuote(remotePath)}`;
|
|
3521
|
-
const rawSize = this.config.user === "root" ? await this.output(statCommand) : await this.outputWithoutSudo(statCommand);
|
|
3522
|
-
const actualSize = Number(rawSize.trim());
|
|
3523
|
-
if (!Number.isFinite(actualSize)) {
|
|
3524
|
-
throw new TypeError(
|
|
3525
|
-
`[ssh.${operation}: ${remotePath}] could not determine remote file size after upload`
|
|
3526
|
-
);
|
|
3527
|
-
}
|
|
3528
|
-
if (actualSize === 0 && expectedSize > 0) {
|
|
3529
|
-
const diskInfo = await this.checkRemoteDiskSpace(remotePath);
|
|
3530
|
-
if (diskInfo != null && diskInfo.availableBytes < expectedSize) {
|
|
3531
|
-
throw new Error(
|
|
3532
|
-
`[ssh.${operation}: ${remotePath}] disk full \u2013 ${diskInfo.availableBytes} bytes available on ${diskInfo.mountpoint}; the file was written as 0 bytes because there is no space left on the device`
|
|
3533
|
-
);
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
|
-
if (actualSize !== expectedSize) {
|
|
3537
|
-
throw new Error(
|
|
3538
|
-
`[ssh.${operation}: ${remotePath}] remote file size mismatch after upload; expected ${expectedSize} bytes, got ${actualSize}`
|
|
3539
|
-
);
|
|
3540
|
-
}
|
|
3541
|
-
}
|
|
3542
3988
|
/**
|
|
3543
3989
|
* R-0000669: build the abort signal that {@link tryConnectOnPort} subscribes
|
|
3544
3990
|
* to. Combines the prompt-level abort signal (which fires from the SIGINT
|
|
@@ -3557,6 +4003,32 @@ var SshConnectionImpl = class {
|
|
|
3557
4003
|
if (this.promptAbortSignal != null) signals.push(this.promptAbortSignal);
|
|
3558
4004
|
return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
3559
4005
|
}
|
|
4006
|
+
/**
|
|
4007
|
+
* R-0000141 / R-0000693: build the shell lines that reject a destination
|
|
4008
|
+
* directory whose resolved path differs from the literal one — i.e. at least
|
|
4009
|
+
* one path component is a symbolic link. Resolving via `command -p realpath
|
|
4010
|
+
* -m` runs the lookup against the POSIX default PATH so a wrapper earlier in
|
|
4011
|
+
* the connected shell's PATH cannot short-circuit the check, and `-m`
|
|
4012
|
+
* tolerates trailing components that do not yet exist.
|
|
4013
|
+
*
|
|
4014
|
+
* The lines are meant to be embedded at the top of a `set -eu` script so the
|
|
4015
|
+
* guard fails the whole round-trip (via the `paratix-symlink-component`
|
|
4016
|
+
* marker + exit 20) before any `mktemp`/`mv` runs. An empty string is
|
|
4017
|
+
* returned for `""`/`"/"`, mirroring the early return in
|
|
4018
|
+
* {@link assertDirnameHasNoSymlinkComponent}.
|
|
4019
|
+
*
|
|
4020
|
+
* @param directory - The destination directory derived from `posix.dirname`.
|
|
4021
|
+
* @returns Shell lines (newline-terminated) or an empty string when no check is needed.
|
|
4022
|
+
*/
|
|
4023
|
+
buildDirSymlinkCheckScript(directory) {
|
|
4024
|
+
if (directory === "" || directory === "/") return "";
|
|
4025
|
+
return `dir_resolved=$(command -p realpath -m -- ${shellQuote(directory)})
|
|
4026
|
+
if [ "$dir_resolved" != ${shellQuote(directory)} ]; then
|
|
4027
|
+
printf 'paratix-symlink-component %s\\n' "$dir_resolved" >&2
|
|
4028
|
+
exit 20
|
|
4029
|
+
fi
|
|
4030
|
+
`;
|
|
4031
|
+
}
|
|
3560
4032
|
buildEnvPrefix(environment) {
|
|
3561
4033
|
if (environment == null) return "";
|
|
3562
4034
|
for (const key of Object.keys(environment)) {
|
|
@@ -3567,6 +4039,62 @@ var SshConnectionImpl = class {
|
|
|
3567
4039
|
const pairs = Object.entries(environment).map(([k, v]) => `${k}=${shellQuote(v)}`);
|
|
3568
4040
|
return `${pairs.join(" ")} `;
|
|
3569
4041
|
}
|
|
4042
|
+
/**
|
|
4043
|
+
* Build the non-root finalize+hash script. Mirrors the privileged finalize
|
|
4044
|
+
* in {@link finalizeRemoteTempFile} (guard, intermediate in-destination
|
|
4045
|
+
* `mktemp`, `mv`/`chmod`/`chown`/`mv`) and additionally folds in the
|
|
4046
|
+
* dirname-symlink guard and a trailing `sha256sum` of the destination.
|
|
4047
|
+
*
|
|
4048
|
+
* @param temporaryPath - The staged temp path to move into place.
|
|
4049
|
+
* @param remotePath - The destination path on the remote host.
|
|
4050
|
+
* @param mode - The validated file mode to apply to the finalized file.
|
|
4051
|
+
* @returns The `set -eu` finalize+hash script.
|
|
4052
|
+
*/
|
|
4053
|
+
buildNonRootFinalizeAndHashScript(temporaryPath, remotePath, mode) {
|
|
4054
|
+
const directory = posix.dirname(remotePath);
|
|
4055
|
+
const basename = posix.basename(remotePath);
|
|
4056
|
+
const targetGuard = `[ ! -d ${shellQuote(remotePath)} ] && [ ! -L ${shellQuote(remotePath)} ]`;
|
|
4057
|
+
const finalTemplate = `.${basename}.paratix.XXXXXX`;
|
|
4058
|
+
return `set -eu
|
|
4059
|
+
${this.buildDirSymlinkCheckScript(directory)}if ! ${targetGuard}; then
|
|
4060
|
+
printf '%s
|
|
4061
|
+
' 'target path must not be a directory or symlink' >&2
|
|
4062
|
+
exit 1
|
|
4063
|
+
fi
|
|
4064
|
+
target_owner=$(stat -c '%u:%g' ${shellQuote(remotePath)} 2>/dev/null || stat -c '%u:%g' ${shellQuote(directory)} 2>/dev/null || printf '0:0')
|
|
4065
|
+
target_temp=''
|
|
4066
|
+
cleanup() {
|
|
4067
|
+
if [ -n "$target_temp" ]; then
|
|
4068
|
+
rm -f -- "$target_temp"
|
|
4069
|
+
fi
|
|
4070
|
+
}
|
|
4071
|
+
trap cleanup EXIT
|
|
4072
|
+
target_temp=$(mktemp -p ${shellQuote(directory)} -- ${shellQuote(finalTemplate)})
|
|
4073
|
+
[ -n "$target_temp" ] || exit 1
|
|
4074
|
+
mv -T -- ${shellQuote(temporaryPath)} "$target_temp"
|
|
4075
|
+
chmod ${shellQuote(mode)} "$target_temp"
|
|
4076
|
+
chown "$target_owner" "$target_temp"
|
|
4077
|
+
mv -T -- "$target_temp" ${shellQuote(remotePath)}
|
|
4078
|
+
trap - EXIT
|
|
4079
|
+
sha256sum -- ${shellQuote(remotePath)} 2>/dev/null || printf 'paratix-hash-failed
|
|
4080
|
+
'
|
|
4081
|
+
`;
|
|
4082
|
+
}
|
|
4083
|
+
/**
|
|
4084
|
+
* Build the root finalize+hash command. The destination is moved into place
|
|
4085
|
+
* only when it is neither a directory nor a symlink, and hashed only after a
|
|
4086
|
+
* successful `mv`; a `mv` failure short-circuits before the hash so the
|
|
4087
|
+
* caller sees a finalize `CommandError` rather than a stale-destination hash.
|
|
4088
|
+
*
|
|
4089
|
+
* @param temporaryPath - The staged temp path to move into place.
|
|
4090
|
+
* @param remotePath - The destination path on the remote host.
|
|
4091
|
+
* @returns The finalize+hash command string.
|
|
4092
|
+
*/
|
|
4093
|
+
buildRootFinalizeAndHashScript(temporaryPath, remotePath) {
|
|
4094
|
+
const targetGuard = `[ ! -d ${shellQuote(remotePath)} ] && [ ! -L ${shellQuote(remotePath)} ]`;
|
|
4095
|
+
return `${targetGuard} && mv -T -- ${shellQuote(temporaryPath)} ${shellQuote(remotePath)} && { sha256sum -- ${shellQuote(remotePath)} 2>/dev/null || printf 'paratix-hash-failed
|
|
4096
|
+
'; }`;
|
|
4097
|
+
}
|
|
3570
4098
|
buildSecrets(extra) {
|
|
3571
4099
|
const cachedPasswordSecret = this.cachedSudoPassword == null ? [] : [() => this.cachedSudoPassword?.toString("utf8") ?? ""];
|
|
3572
4100
|
const registeredSecrets = getRegisteredSecrets();
|
|
@@ -3603,7 +4131,7 @@ var SshConnectionImpl = class {
|
|
|
3603
4131
|
const KB_TO_BYTES = 1024;
|
|
3604
4132
|
try {
|
|
3605
4133
|
const directory = remotePath.includes("/") ? remotePath.slice(0, remotePath.lastIndexOf("/")) || "/" : ".";
|
|
3606
|
-
const dfOutput = await this.output(`df -
|
|
4134
|
+
const dfOutput = await this.output(`df -Pk ${shellQuote(directory)}`);
|
|
3607
4135
|
const lines = dfOutput.trim().split("\n");
|
|
3608
4136
|
if (lines.length < 2) return null;
|
|
3609
4137
|
const columns = lines[1].split(new RegExp("\\s+", "v"));
|
|
@@ -3615,6 +4143,39 @@ var SshConnectionImpl = class {
|
|
|
3615
4143
|
return null;
|
|
3616
4144
|
}
|
|
3617
4145
|
}
|
|
4146
|
+
/**
|
|
4147
|
+
* Classify a raw `sha256sum` reply for the finalized remote file against the
|
|
4148
|
+
* expected digest. Shared by the combined finalize+hash round-trip and the
|
|
4149
|
+
* shell-fallback re-verification.
|
|
4150
|
+
*
|
|
4151
|
+
* @param remotePath - The finalized destination path (for diagnostics).
|
|
4152
|
+
* @param expectedHash - Lowercase hex SHA-256 digest of the intended content.
|
|
4153
|
+
* @param rawHash - Raw stdout of the finalize+hash or verify command.
|
|
4154
|
+
* @returns `"matches"`, `"empty"` (disk-full indicator) or `"hash-mismatch"`.
|
|
4155
|
+
* @throws {RemoteStatTransientError} When the reply cannot be evaluated into a verdict.
|
|
4156
|
+
*/
|
|
4157
|
+
classifyRemoteHash(remotePath, expectedHash, rawHash) {
|
|
4158
|
+
if (rawHash.includes("paratix-hash-failed")) {
|
|
4159
|
+
throw new RemoteStatTransientError(
|
|
4160
|
+
`[ssh.writeFile: ${remotePath}] could not hash remote file after upload/finalize`
|
|
4161
|
+
);
|
|
4162
|
+
}
|
|
4163
|
+
if (rawHash.endsWith(CAPTURE_TRUNCATION_MARKER)) {
|
|
4164
|
+
throw new RemoteStatTransientError(
|
|
4165
|
+
`[ssh.writeFile: ${remotePath}] sha256sum output exceeds the captured-output cap of ${DEFAULT_MAX_OUTPUT_BYTES} bytes; refusing to derive a verdict from truncated output`
|
|
4166
|
+
);
|
|
4167
|
+
}
|
|
4168
|
+
const actualHash = rawHash.trim().split(new RegExp("\\s+", "v"))[0]?.toLowerCase() ?? "";
|
|
4169
|
+
if (!new RegExp("^[0-9a-f]{64}$", "v").test(actualHash)) {
|
|
4170
|
+
throw new RemoteStatTransientError(
|
|
4171
|
+
`[ssh.writeFile: ${remotePath}] could not determine remote file hash after upload/finalize`
|
|
4172
|
+
);
|
|
4173
|
+
}
|
|
4174
|
+
const expected = expectedHash.toLowerCase();
|
|
4175
|
+
if (actualHash === expected) return "matches";
|
|
4176
|
+
if (actualHash === EMPTY_FILE_SHA256 && expected !== EMPTY_FILE_SHA256) return "empty";
|
|
4177
|
+
return "hash-mismatch";
|
|
4178
|
+
}
|
|
3618
4179
|
async cleanupPrivilegedRemoteTempFile(remotePath) {
|
|
3619
4180
|
await this.exec(`rm -f -- ${shellQuote(remotePath)}`, {
|
|
3620
4181
|
ignoreExitCode: true,
|
|
@@ -3633,6 +4194,23 @@ var SshConnectionImpl = class {
|
|
|
3633
4194
|
} catch (cleanupError) {
|
|
3634
4195
|
process.stderr.write(
|
|
3635
4196
|
`Warning: failed to remove temp file ${remotePath}: ${maskSecrets(String(cleanupError), this.buildSecrets())}
|
|
4197
|
+
`
|
|
4198
|
+
);
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
/**
|
|
4202
|
+
* Best-effort cleanup of the staged upload temp file. Extracted from
|
|
4203
|
+
* {@link uploadFile}'s `finally` so the primary method stays within the
|
|
4204
|
+
* statement budget; mirrors {@link cleanupWriteFileTemporaryPath}.
|
|
4205
|
+
*
|
|
4206
|
+
* @param temporaryPath - The staged temp path to remove.
|
|
4207
|
+
*/
|
|
4208
|
+
async cleanupUploadTemporaryPath(temporaryPath) {
|
|
4209
|
+
try {
|
|
4210
|
+
await this.cleanupRemoteTempFile(temporaryPath);
|
|
4211
|
+
} catch (cleanupError) {
|
|
4212
|
+
process.stderr.write(
|
|
4213
|
+
`Warning: failed to remove temp file ${temporaryPath}: ${maskSecrets(String(cleanupError), this.buildSecrets())}
|
|
3636
4214
|
`
|
|
3637
4215
|
);
|
|
3638
4216
|
}
|
|
@@ -3776,10 +4354,17 @@ var SshConnectionImpl = class {
|
|
|
3776
4354
|
}
|
|
3777
4355
|
async createRemoteTempPathInDestination(remotePath, prefix) {
|
|
3778
4356
|
const directory = posix.dirname(remotePath);
|
|
3779
|
-
await this.assertDirnameHasNoSymlinkComponent(directory, remotePath);
|
|
3780
4357
|
const template = `${prefix}.XXXXXX`;
|
|
3781
|
-
const
|
|
3782
|
-
|
|
4358
|
+
const script = `set -eu
|
|
4359
|
+
${this.buildDirSymlinkCheckScript(directory)}mktemp -p ${shellQuote(directory)} -- ${shellQuote(template)}
|
|
4360
|
+
`;
|
|
4361
|
+
let path;
|
|
4362
|
+
try {
|
|
4363
|
+
path = this.config.user === "root" ? await this.output(script) : await this.outputWithoutSudo(script);
|
|
4364
|
+
} catch (error) {
|
|
4365
|
+
this.throwIfDirSymlinkMarker(error, directory, remotePath);
|
|
4366
|
+
throw error;
|
|
4367
|
+
}
|
|
3783
4368
|
return validateMktempPath(directory, path, prefix);
|
|
3784
4369
|
}
|
|
3785
4370
|
async createRemoteWritableTempPath(remotePath, prefix) {
|
|
@@ -3868,14 +4453,24 @@ var SshConnectionImpl = class {
|
|
|
3868
4453
|
* propagates as {@link RemoteStatTransientError} so the caller can retry
|
|
3869
4454
|
* instead of acting on a non-verdict result.
|
|
3870
4455
|
*
|
|
4456
|
+
* #82: the finalized-destination hash is now produced by the combined
|
|
4457
|
+
* finalize+hash round-trip ({@link finalizeAndHashRemoteFile}) and passed in
|
|
4458
|
+
* via `options.remoteHash`, so this method only classifies the verdict — it
|
|
4459
|
+
* no longer issues a separate `sha256sum` exec.
|
|
4460
|
+
*
|
|
3871
4461
|
* @param options - Verification inputs.
|
|
3872
4462
|
* @param options.expectedHash - Lowercase hex SHA-256 digest of the local file.
|
|
3873
4463
|
* @param options.expectedSize - Byte length of the local file (used for disk-full diagnostics).
|
|
4464
|
+
* @param options.remoteHash - Raw stdout of the finalize+hash round-trip.
|
|
3874
4465
|
* @param options.remotePath - The finalized destination path on the remote host.
|
|
3875
4466
|
* @throws {Error} When the remote file is empty (disk-full) or its hash does not match.
|
|
3876
4467
|
*/
|
|
3877
4468
|
async ensureRemoteUploadFile(options) {
|
|
3878
|
-
const verification =
|
|
4469
|
+
const verification = this.classifyRemoteHash(
|
|
4470
|
+
options.remotePath,
|
|
4471
|
+
options.expectedHash,
|
|
4472
|
+
options.remoteHash
|
|
4473
|
+
);
|
|
3879
4474
|
if (verification === "matches") return;
|
|
3880
4475
|
if (verification === "empty") {
|
|
3881
4476
|
const diskInfo = await this.checkRemoteDiskSpace(options.remotePath);
|
|
@@ -3893,7 +4488,11 @@ var SshConnectionImpl = class {
|
|
|
3893
4488
|
);
|
|
3894
4489
|
}
|
|
3895
4490
|
async ensureRemoteWriteFile(options) {
|
|
3896
|
-
const verification =
|
|
4491
|
+
const verification = this.classifyRemoteHash(
|
|
4492
|
+
options.remotePath,
|
|
4493
|
+
options.expectedHash,
|
|
4494
|
+
options.remoteHash
|
|
4495
|
+
);
|
|
3897
4496
|
if (verification === "matches") return;
|
|
3898
4497
|
await this.rewriteRemoteFileViaShell(options.remotePath, options.content, options.mode);
|
|
3899
4498
|
const fallbackVerification = await this.verifyRemoteWriteFile(
|
|
@@ -3946,6 +4545,41 @@ var SshConnectionImpl = class {
|
|
|
3946
4545
|
throw error;
|
|
3947
4546
|
}
|
|
3948
4547
|
}
|
|
4548
|
+
/**
|
|
4549
|
+
* Apply the pre-finalize size smoke-test to the `stat`-reported byte count of
|
|
4550
|
+
* the staged temp file. Kept in TypeScript (rather than folded into the
|
|
4551
|
+
* combined shell command) so the disk-full diagnosis and the size-mismatch
|
|
4552
|
+
* error stay identical to the previous `assertRemoteFileSize` behavior.
|
|
4553
|
+
*
|
|
4554
|
+
* @param options - Size-evaluation inputs.
|
|
4555
|
+
* @param options.remotePath - The staged temp path (used for diagnostics).
|
|
4556
|
+
* @param options.expectedSize - The expected byte length of the staged content.
|
|
4557
|
+
* @param options.rawSize - The raw `stat -c '%s'` output captured from the remote.
|
|
4558
|
+
* @param options.operation - Which public method drives this write (for diagnostics).
|
|
4559
|
+
* @throws {Error} When the size is unreadable, indicates disk-full, or mismatches.
|
|
4560
|
+
*/
|
|
4561
|
+
async evaluateStagedFileSize(options) {
|
|
4562
|
+
const { expectedSize, operation, rawSize, remotePath } = options;
|
|
4563
|
+
const actualSize = Number(rawSize.trim());
|
|
4564
|
+
if (!Number.isFinite(actualSize)) {
|
|
4565
|
+
throw new TypeError(
|
|
4566
|
+
`[ssh.${operation}: ${remotePath}] could not determine remote file size after upload`
|
|
4567
|
+
);
|
|
4568
|
+
}
|
|
4569
|
+
if (actualSize === 0 && expectedSize > 0) {
|
|
4570
|
+
const diskInfo = await this.checkRemoteDiskSpace(remotePath);
|
|
4571
|
+
if (diskInfo != null && diskInfo.availableBytes < expectedSize) {
|
|
4572
|
+
throw new Error(
|
|
4573
|
+
`[ssh.${operation}: ${remotePath}] disk full \u2013 ${diskInfo.availableBytes} bytes available on ${diskInfo.mountpoint}; the file was written as 0 bytes because there is no space left on the device`
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
if (actualSize !== expectedSize) {
|
|
4578
|
+
throw new Error(
|
|
4579
|
+
`[ssh.${operation}: ${remotePath}] remote file size mismatch after upload; expected ${expectedSize} bytes, got ${actualSize}`
|
|
4580
|
+
);
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
3949
4583
|
async execPrepared(command, options = {}) {
|
|
3950
4584
|
const client = this.ensureClient();
|
|
3951
4585
|
const environmentPrefix = this.buildEnvPrefix(options.env);
|
|
@@ -4002,19 +4636,34 @@ var SshConnectionImpl = class {
|
|
|
4002
4636
|
}
|
|
4003
4637
|
/**
|
|
4004
4638
|
* Execute a command directly over the SSH transport without sudo wrapping.
|
|
4005
|
-
* Used
|
|
4639
|
+
* Used by the sudo probes ({@link ensureSudoInstalled},
|
|
4640
|
+
* {@link hasPasswordlessSudo}) and the non-root finalize/verify paths
|
|
4641
|
+
* ({@link execWithoutSudo}, {@link outputWithoutSudo}).
|
|
4642
|
+
*
|
|
4643
|
+
* Stream capture, the output byte-cap, secret masking and the signal /
|
|
4644
|
+
* exit-code handling are delegated to {@link collectStreamOutput} — the same
|
|
4645
|
+
* helper {@link execPrepared} uses — so the raw path shares one capping and
|
|
4646
|
+
* one truncation implementation with the sudo path. `ignoreExitCode: true`
|
|
4647
|
+
* preserves the historical contract of returning a non-zero exit code to the
|
|
4648
|
+
* caller instead of throwing; a delivered signal still rejects. `silent: true`
|
|
4649
|
+
* keeps the probe/verify output off the live terminal, matching the previous
|
|
4650
|
+
* discard-stderr / buffer-stdout behavior. Only stdout and the exit code are
|
|
4651
|
+
* surfaced to callers; stderr is captured for masking but not returned.
|
|
4006
4652
|
*
|
|
4007
4653
|
* @param command - The raw shell command to run.
|
|
4008
4654
|
* @returns The exit code and captured stdout.
|
|
4009
4655
|
*/
|
|
4010
4656
|
async execRaw(command) {
|
|
4011
4657
|
const client = this.ensureClient();
|
|
4012
|
-
|
|
4013
|
-
|
|
4658
|
+
const secrets = prepareSecrets(this.buildSecrets());
|
|
4659
|
+
const result = await new Promise((resolve2, reject) => {
|
|
4660
|
+
const { isSettled, wrappedReject, wrappedResolve } = this.createSettledCallbacks(
|
|
4661
|
+
resolve2,
|
|
4662
|
+
reject
|
|
4663
|
+
);
|
|
4014
4664
|
let activeStream = null;
|
|
4015
4665
|
const timer = setTimeout(() => {
|
|
4016
4666
|
activeStream?.close();
|
|
4017
|
-
const secrets = prepareSecrets(this.buildSecrets());
|
|
4018
4667
|
wrappedReject(
|
|
4019
4668
|
new Error(
|
|
4020
4669
|
`Command timed out after ${COMMAND_TIMEOUT}ms: ${maskPreparedSecrets(command, secrets)}`
|
|
@@ -4033,62 +4682,14 @@ var SshConnectionImpl = class {
|
|
|
4033
4682
|
return;
|
|
4034
4683
|
}
|
|
4035
4684
|
activeStream = stream;
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
DEFAULT_MAX_OUTPUT_BYTES - (stdoutBytes - chunk.length)
|
|
4045
|
-
);
|
|
4046
|
-
if (remainingBytes > 0) {
|
|
4047
|
-
chunks.push(chunk.subarray(0, remainingBytes));
|
|
4048
|
-
}
|
|
4049
|
-
const capturedStdout = Buffer.concat(chunks).toString("utf8");
|
|
4050
|
-
const secrets = prepareSecrets(this.buildSecrets());
|
|
4051
|
-
activeStream?.close();
|
|
4052
|
-
wrappedReject(
|
|
4053
|
-
new Error(
|
|
4054
|
-
`Command stdout exceeded ${DEFAULT_MAX_OUTPUT_BYTES} bytes: ${maskPreparedSecrets(
|
|
4055
|
-
command,
|
|
4056
|
-
secrets
|
|
4057
|
-
)}
|
|
4058
|
-
stdout: ${truncateRawOutputErrorSnippet(
|
|
4059
|
-
maskPreparedSecrets(capturedStdout, secrets)
|
|
4060
|
-
)}`
|
|
4061
|
-
)
|
|
4062
|
-
);
|
|
4063
|
-
return;
|
|
4064
|
-
}
|
|
4065
|
-
chunks.push(chunk);
|
|
4066
|
-
});
|
|
4067
|
-
stream.on("close", (code, signal) => {
|
|
4068
|
-
clearTimeout(timer);
|
|
4069
|
-
if (signal != null && signal !== "") {
|
|
4070
|
-
const secrets = prepareSecrets(this.buildSecrets());
|
|
4071
|
-
wrappedReject(
|
|
4072
|
-
new Error(
|
|
4073
|
-
`Command failed with signal ${signal}: ${maskPreparedSecrets(command, secrets)}`
|
|
4074
|
-
)
|
|
4075
|
-
);
|
|
4076
|
-
return;
|
|
4077
|
-
}
|
|
4078
|
-
wrappedResolve({
|
|
4079
|
-
exitCode: normalizeSshCloseCode(code),
|
|
4080
|
-
stdout: Buffer.concat(chunks).toString("utf8")
|
|
4081
|
-
});
|
|
4082
|
-
});
|
|
4083
|
-
stream.on("error", (error2) => {
|
|
4084
|
-
clearTimeout(timer);
|
|
4085
|
-
wrappedReject(error2);
|
|
4086
|
-
});
|
|
4087
|
-
stream.stderr.on("data", () => {
|
|
4088
|
-
});
|
|
4089
|
-
stream.stderr.on("error", (error2) => {
|
|
4090
|
-
clearTimeout(timer);
|
|
4091
|
-
wrappedReject(error2);
|
|
4685
|
+
collectStreamOutput({
|
|
4686
|
+
command,
|
|
4687
|
+
options: { ignoreExitCode: true, silent: true },
|
|
4688
|
+
reject: wrappedReject,
|
|
4689
|
+
resolve: wrappedResolve,
|
|
4690
|
+
secrets,
|
|
4691
|
+
stream,
|
|
4692
|
+
timer
|
|
4092
4693
|
});
|
|
4093
4694
|
});
|
|
4094
4695
|
} catch (error) {
|
|
@@ -4097,14 +4698,46 @@ stdout: ${truncateRawOutputErrorSnippet(
|
|
|
4097
4698
|
wrappedReject(toError(error));
|
|
4098
4699
|
}
|
|
4099
4700
|
});
|
|
4701
|
+
return { exitCode: result.code, stdout: result.stdout };
|
|
4100
4702
|
}
|
|
4101
4703
|
async execWithoutSudo(command) {
|
|
4102
4704
|
const result = await this.execRaw(command);
|
|
4103
4705
|
if (result.exitCode !== 0) {
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4706
|
+
throw new Error(`Command failed (exit code ${result.exitCode}): ${this.maskCommand(command)}`);
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
/**
|
|
4710
|
+
* #82: finalize the staged temp file (`mv -T`) AND hash the finalized
|
|
4711
|
+
* destination in a single privileged exec round-trip, returning the raw
|
|
4712
|
+
* `sha256sum` output for the caller to classify.
|
|
4713
|
+
*
|
|
4714
|
+
* R-0000522 / R-0000599: hashing the finalized destination catches a
|
|
4715
|
+
* same-length TOCTOU swap between the staged temp and the privileged move;
|
|
4716
|
+
* computing the digest inside the same script that performs the move keeps
|
|
4717
|
+
* the window to a single remote command instead of a separate network
|
|
4718
|
+
* round-trip. R-0000141: for non-root users the destination dirname-symlink
|
|
4719
|
+
* guard is folded into the same script (the staged temp lives in `/tmp`, so
|
|
4720
|
+
* unlike the root path there was no earlier in-destination `mktemp` to guard).
|
|
4721
|
+
*
|
|
4722
|
+
* A `sha256sum` failure that occurs *after* a successful move emits the
|
|
4723
|
+
* `paratix-hash-failed` marker and still exits 0, so
|
|
4724
|
+
* {@link classifyRemoteHash} reports a transient verification failure rather
|
|
4725
|
+
* than misclassifying a correctly finalized file as a content mismatch.
|
|
4726
|
+
*
|
|
4727
|
+
* @param temporaryPath - The staged temp path to move into place.
|
|
4728
|
+
* @param remotePath - The destination path on the remote host.
|
|
4729
|
+
* @param mode - The validated file mode applied during the non-root finalize.
|
|
4730
|
+
* @returns The raw stdout of the finalize+hash script (a `sha256sum` line or marker).
|
|
4731
|
+
*/
|
|
4732
|
+
async finalizeAndHashRemoteFile(temporaryPath, remotePath, mode) {
|
|
4733
|
+
validateMode(mode);
|
|
4734
|
+
const script = this.config.user === "root" ? this.buildRootFinalizeAndHashScript(temporaryPath, remotePath) : this.buildNonRootFinalizeAndHashScript(temporaryPath, remotePath, mode);
|
|
4735
|
+
try {
|
|
4736
|
+
const result = await this.exec(script, { silent: true });
|
|
4737
|
+
return result.stdout;
|
|
4738
|
+
} catch (error) {
|
|
4739
|
+
this.throwIfDirSymlinkMarker(error, posix.dirname(remotePath), remotePath);
|
|
4740
|
+
throw error;
|
|
4108
4741
|
}
|
|
4109
4742
|
}
|
|
4110
4743
|
async finalizeRemoteTempFile(temporaryPath, remotePath, mode) {
|
|
@@ -4211,13 +4844,24 @@ trap - EXIT
|
|
|
4211
4844
|
isSudoReadyWithoutProbe() {
|
|
4212
4845
|
return this.config.user === "root" || this.cachedSudoPassword != null;
|
|
4213
4846
|
}
|
|
4847
|
+
/**
|
|
4848
|
+
* Single entry point for masking a command string before it is interpolated
|
|
4849
|
+
* into an operator-visible error message. Wraps the repeated
|
|
4850
|
+
* `maskPreparedSecrets(command, prepareSecrets(this.buildSecrets()))` pattern
|
|
4851
|
+
* (R-0000667) so every raw-exec error path redacts the same registered-secret
|
|
4852
|
+
* material — sudo password, op tokens, signed download URLs, … — identically.
|
|
4853
|
+
*
|
|
4854
|
+
* @param command - The command whose secret material must be redacted.
|
|
4855
|
+
* @returns The command with every registered secret variant replaced by the
|
|
4856
|
+
* redaction placeholder.
|
|
4857
|
+
*/
|
|
4858
|
+
maskCommand(command) {
|
|
4859
|
+
return maskPreparedSecrets(command, prepareSecrets(this.buildSecrets()));
|
|
4860
|
+
}
|
|
4214
4861
|
async outputWithoutSudo(command) {
|
|
4215
4862
|
const result = await this.execRaw(command);
|
|
4216
4863
|
if (result.exitCode !== 0) {
|
|
4217
|
-
|
|
4218
|
-
throw new Error(
|
|
4219
|
-
`Command failed (exit code ${result.exitCode}): ${maskPreparedSecrets(command, secrets)}`
|
|
4220
|
-
);
|
|
4864
|
+
throw new Error(`Command failed (exit code ${result.exitCode}): ${this.maskCommand(command)}`);
|
|
4221
4865
|
}
|
|
4222
4866
|
return result.stdout.trim();
|
|
4223
4867
|
}
|
|
@@ -4385,14 +5029,39 @@ trap - EXIT
|
|
|
4385
5029
|
} catch {
|
|
4386
5030
|
}
|
|
4387
5031
|
}
|
|
4388
|
-
|
|
5032
|
+
/**
|
|
5033
|
+
* #82: set the staged temp file's mode and read its size back in a single
|
|
5034
|
+
* exec round-trip, then run the pre-finalize size smoke-test. `chmod` and
|
|
5035
|
+
* `stat` are chained with `&&` so a failed `chmod` short-circuits before the
|
|
5036
|
+
* `stat` and surfaces as a `CommandError`, exactly as the previous separate
|
|
5037
|
+
* `setRemoteTempMode` call did.
|
|
5038
|
+
*
|
|
5039
|
+
* R-0000266: the staged temp path is owned by the connecting user (mktemp
|
|
5040
|
+
* staged it under /tmp without sudo). Reading the size through `output` would
|
|
5041
|
+
* funnel the call through `ensureSudoReady` and could fail with a sudo-auth
|
|
5042
|
+
* error after the cached credentials expired. The combined command therefore
|
|
5043
|
+
* runs through the raw (non-sudo) exec path for non-root users so the size
|
|
5044
|
+
* check stays a pure stat call and surfaces a real size mismatch instead of
|
|
5045
|
+
* a sudo prompt failure.
|
|
5046
|
+
*
|
|
5047
|
+
* @param options - Staging inputs.
|
|
5048
|
+
* @param options.temporaryPath - The staged temp path created by `mktemp`.
|
|
5049
|
+
* @param options.mode - The validated file mode to apply before the finalize.
|
|
5050
|
+
* @param options.expectedSize - The expected byte length of the staged content.
|
|
5051
|
+
* @param options.operation - Which public method drives this write (for diagnostics).
|
|
5052
|
+
* @throws {Error} When the staged size does not match `expectedSize`.
|
|
5053
|
+
*/
|
|
5054
|
+
async stageRemoteTempFile(options) {
|
|
5055
|
+
const { expectedSize, mode, operation, temporaryPath } = options;
|
|
4389
5056
|
validateMode(mode);
|
|
4390
|
-
const command = `chmod ${shellQuote(mode)} ${shellQuote(
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
5057
|
+
const command = `chmod ${shellQuote(mode)} ${shellQuote(temporaryPath)} && stat -c '%s' ${shellQuote(temporaryPath)}`;
|
|
5058
|
+
const rawSize = this.config.user === "root" ? await this.output(command) : await this.outputWithoutSudo(command);
|
|
5059
|
+
await this.evaluateStagedFileSize({
|
|
5060
|
+
expectedSize,
|
|
5061
|
+
operation,
|
|
5062
|
+
rawSize,
|
|
5063
|
+
remotePath: temporaryPath
|
|
5064
|
+
});
|
|
4396
5065
|
}
|
|
4397
5066
|
/**
|
|
4398
5067
|
* Build the sudo-wrapped command string for execution.
|
|
@@ -4442,6 +5111,26 @@ trap - EXIT
|
|
|
4442
5111
|
clearTimeout(fallback);
|
|
4443
5112
|
});
|
|
4444
5113
|
}
|
|
5114
|
+
/**
|
|
5115
|
+
* Re-throw the folded dirname-symlink guard's failure as the dedicated
|
|
5116
|
+
* R-0000141 diagnostic when the `paratix-symlink-component` marker is present
|
|
5117
|
+
* in a failed command's stderr; otherwise return so the caller re-throws the
|
|
5118
|
+
* original error unchanged.
|
|
5119
|
+
*
|
|
5120
|
+
* @param error - The error raised by a combined mktemp/finalize round-trip.
|
|
5121
|
+
* @param directory - The destination directory that was guarded.
|
|
5122
|
+
* @param remotePath - The original remote path (for the diagnostic message).
|
|
5123
|
+
* @throws {Error} The mapped symlink diagnostic when the marker is present.
|
|
5124
|
+
*/
|
|
5125
|
+
throwIfDirSymlinkMarker(error, directory, remotePath) {
|
|
5126
|
+
if (!(error instanceof CommandError)) return;
|
|
5127
|
+
const match = new RegExp("paratix-symlink-component (?<resolved>.*)", "v").exec(error.fullStderr);
|
|
5128
|
+
if (match?.groups?.resolved == null) return;
|
|
5129
|
+
throw new Error(
|
|
5130
|
+
`[ssh.mktemp: ${remotePath}] destination directory ${directory} resolves to ${match.groups.resolved}; refusing to mktemp because at least one path component is a symbolic link`,
|
|
5131
|
+
{ cause: error }
|
|
5132
|
+
);
|
|
5133
|
+
}
|
|
4445
5134
|
/**
|
|
4446
5135
|
* Iterate over `config.ports` and attempt a connection on each one.
|
|
4447
5136
|
*
|
|
@@ -4525,21 +5214,7 @@ trap - EXIT
|
|
|
4525
5214
|
{ cause: error }
|
|
4526
5215
|
);
|
|
4527
5216
|
}
|
|
4528
|
-
|
|
4529
|
-
throw new RemoteStatTransientError(
|
|
4530
|
-
`[ssh.writeFile: ${remotePath}] sha256sum output exceeds the captured-output cap of ${DEFAULT_MAX_OUTPUT_BYTES} bytes; refusing to derive a verdict from truncated output`
|
|
4531
|
-
);
|
|
4532
|
-
}
|
|
4533
|
-
const actualHash = rawHash.trim().split(new RegExp("\\s+", "v"))[0]?.toLowerCase() ?? "";
|
|
4534
|
-
if (!new RegExp("^[0-9a-f]{64}$", "v").test(actualHash)) {
|
|
4535
|
-
throw new RemoteStatTransientError(
|
|
4536
|
-
`[ssh.writeFile: ${remotePath}] could not determine remote file hash after upload/finalize`
|
|
4537
|
-
);
|
|
4538
|
-
}
|
|
4539
|
-
const expected = expectedHash.toLowerCase();
|
|
4540
|
-
if (actualHash === expected) return "matches";
|
|
4541
|
-
if (actualHash === EMPTY_FILE_SHA256 && expected !== EMPTY_FILE_SHA256) return "empty";
|
|
4542
|
-
return "hash-mismatch";
|
|
5217
|
+
return this.classifyRemoteHash(remotePath, expectedHash, rawHash);
|
|
4543
5218
|
}
|
|
4544
5219
|
/**
|
|
4545
5220
|
* Forward sudo password / `options.input` to a stream while tolerating
|
|
@@ -4586,6 +5261,13 @@ trap - EXIT
|
|
|
4586
5261
|
// src/runner.ts
|
|
4587
5262
|
var ASCII_ESC3 = 27;
|
|
4588
5263
|
var ANSI_SHOW_CURSOR3 = `${String.fromCharCode(ASCII_ESC3)}[?25h`;
|
|
5264
|
+
var activeSshConnections = /* @__PURE__ */ new Set();
|
|
5265
|
+
function registerActiveSsh(connection) {
|
|
5266
|
+
activeSshConnections.add(connection);
|
|
5267
|
+
}
|
|
5268
|
+
function unregisterActiveSsh(connection) {
|
|
5269
|
+
activeSshConnections.delete(connection);
|
|
5270
|
+
}
|
|
4589
5271
|
function performShutdownBestEffortCleanup() {
|
|
4590
5272
|
try {
|
|
4591
5273
|
stopLiveModuleOutput(true);
|
|
@@ -4606,6 +5288,16 @@ function performShutdownBestEffortCleanup() {
|
|
|
4606
5288
|
} catch {
|
|
4607
5289
|
}
|
|
4608
5290
|
}
|
|
5291
|
+
function performLastResortCleanup() {
|
|
5292
|
+
performShutdownBestEffortCleanup();
|
|
5293
|
+
for (const connection of activeSshConnections) {
|
|
5294
|
+
try {
|
|
5295
|
+
connection.forceDestroy();
|
|
5296
|
+
} catch {
|
|
5297
|
+
}
|
|
5298
|
+
}
|
|
5299
|
+
activeSshConnections.clear();
|
|
5300
|
+
}
|
|
4609
5301
|
function setupShutdownHandlers() {
|
|
4610
5302
|
let receivedSignal = null;
|
|
4611
5303
|
let ssh = null;
|
|
@@ -4635,12 +5327,14 @@ Received ${signal}, shutting down\u2026`);
|
|
|
4635
5327
|
promptAbortSignal: promptAbortController.signal,
|
|
4636
5328
|
setSsh(connection) {
|
|
4637
5329
|
ssh = connection;
|
|
5330
|
+
registerActiveSsh(connection);
|
|
4638
5331
|
},
|
|
4639
5332
|
shutdownAbortSignal: shutdownAbortController.signal,
|
|
4640
5333
|
shutdownSignal: () => receivedSignal
|
|
4641
5334
|
};
|
|
4642
5335
|
}
|
|
4643
5336
|
var DEFAULT_REBOOT_GRACE_SECONDS = 15;
|
|
5337
|
+
var DEFAULT_REBOOT_RECONNECT_TIMEOUT = 3e5;
|
|
4644
5338
|
var REBOOT_GRACE_SECONDS_TO_MS = 1e3;
|
|
4645
5339
|
function createRebootGraceContext(options, shutdownSignal, abortSignal) {
|
|
4646
5340
|
const seconds = options.rebootGraceSeconds ?? DEFAULT_REBOOT_GRACE_SECONDS;
|
|
@@ -4737,9 +5431,6 @@ function handleCaughtStepError(parameters) {
|
|
|
4737
5431
|
printCommandFailure(parameters.error, parameters.verbose);
|
|
4738
5432
|
return { env: parameters.environment, shouldBreak: true, status: "failed" };
|
|
4739
5433
|
}
|
|
4740
|
-
function isRecipe(target) {
|
|
4741
|
-
return "_isRecipe" in target && target._isRecipe;
|
|
4742
|
-
}
|
|
4743
5434
|
function addSshdPorts(ssh, metaEntries) {
|
|
4744
5435
|
const portEntries = metaEntries?.filter((entry) => isSshdPortMetaEntry(entry)) ?? [];
|
|
4745
5436
|
const addedPorts = [];
|
|
@@ -4796,7 +5487,7 @@ async function handleReboot(ssh, metaEntries, rebootGrace) {
|
|
|
4796
5487
|
keepAlive: true
|
|
4797
5488
|
});
|
|
4798
5489
|
try {
|
|
4799
|
-
await ssh.reconnect();
|
|
5490
|
+
await ssh.reconnect({ defaultTimeout: DEFAULT_REBOOT_RECONNECT_TIMEOUT });
|
|
4800
5491
|
} catch (error) {
|
|
4801
5492
|
console.error(`Failed to reconnect after reboot: ${String(error)}`);
|
|
4802
5493
|
throw error;
|
|
@@ -5232,6 +5923,7 @@ function teardownPlaybookResources(parameters) {
|
|
|
5232
5923
|
stopLiveModuleOutput(true);
|
|
5233
5924
|
for (const signal of ["SIGINT", "SIGTERM"])
|
|
5234
5925
|
getSignalBus().off(signal, parameters.handleShutdownSignal);
|
|
5926
|
+
if (parameters.ssh != null) unregisterActiveSsh(parameters.ssh);
|
|
5235
5927
|
parameters.ssh?.disconnect();
|
|
5236
5928
|
}
|
|
5237
5929
|
function initializeRunPlaybookContext(options) {
|
|
@@ -5550,17 +6242,35 @@ var CliUsageError = class extends Error {
|
|
|
5550
6242
|
this.exitCode = exitCode;
|
|
5551
6243
|
}
|
|
5552
6244
|
};
|
|
6245
|
+
function resolveFilteredRun(definition, rawFilter) {
|
|
6246
|
+
if (rawFilter.length === 0) return definition.run;
|
|
6247
|
+
const names = parseFilterNames(rawFilter);
|
|
6248
|
+
if (names.length === 0) {
|
|
6249
|
+
throw new CliUsageError("--filter requires at least one non-empty module name");
|
|
6250
|
+
}
|
|
6251
|
+
const available = collectModuleNames(definition.run);
|
|
6252
|
+
const unknown = names.filter((name) => !available.has(name));
|
|
6253
|
+
if (unknown.length > 0) {
|
|
6254
|
+
const quoted = unknown.map((name) => `"${name}"`).join(", ");
|
|
6255
|
+
throw new CliUsageError(
|
|
6256
|
+
`--filter matched no module: ${quoted}. Available names: ${[...available].join(", ")}`
|
|
6257
|
+
);
|
|
6258
|
+
}
|
|
6259
|
+
return applyModuleFilter(definition.run, new Set(names));
|
|
6260
|
+
}
|
|
5553
6261
|
async function runApplyCommand(file, options, run = runPlaybook) {
|
|
5554
6262
|
if (options.diff && !options.dryRun) {
|
|
5555
6263
|
throw new CliUsageError("--diff requires --dry-run");
|
|
5556
6264
|
}
|
|
5557
|
-
printCliHeader("0.
|
|
6265
|
+
printCliHeader("0.14.0-5a61f29");
|
|
5558
6266
|
const environmentOverrides = applyCliEnvironmentOverrides(options.env, {
|
|
5559
6267
|
firstRun: options.firstRun
|
|
5560
6268
|
});
|
|
5561
6269
|
const definition = await loadServerDefinitionFromFile(file, {
|
|
5562
6270
|
firstRun: options.firstRun
|
|
5563
6271
|
});
|
|
6272
|
+
const runModules = resolveFilteredRun(definition, options.filter);
|
|
6273
|
+
const targetDefinition = runModules === definition.run ? definition : { ...definition, run: runModules };
|
|
5564
6274
|
const runOptions = {
|
|
5565
6275
|
diff: options.diff,
|
|
5566
6276
|
dryRun: options.dryRun,
|
|
@@ -5571,7 +6281,7 @@ async function runApplyCommand(file, options, run = runPlaybook) {
|
|
|
5571
6281
|
if (options.reconnectTimeout !== void 0) {
|
|
5572
6282
|
runOptions.reconnectTimeout = options.reconnectTimeout * SECONDS_TO_MS;
|
|
5573
6283
|
}
|
|
5574
|
-
await run(
|
|
6284
|
+
await run(targetDefinition, runOptions);
|
|
5575
6285
|
}
|
|
5576
6286
|
function exitAfterApplyError(error, verbose) {
|
|
5577
6287
|
if (error instanceof CliUsageError) {
|
|
@@ -5582,9 +6292,16 @@ function exitAfterApplyError(error, verbose) {
|
|
|
5582
6292
|
const exitCode = process.exitCode === void 0 || process.exitCode === 0 ? 2 : process.exitCode;
|
|
5583
6293
|
process.exit(exitCode);
|
|
5584
6294
|
}
|
|
6295
|
+
function renderSubcommandOptionsHelp(command) {
|
|
6296
|
+
const flagsWidth = Math.max(...command.options.map((option) => option.flags.length));
|
|
6297
|
+
const rows = command.options.map(
|
|
6298
|
+
(option) => ` ${option.flags.padEnd(flagsWidth)} ${option.description}`
|
|
6299
|
+
);
|
|
6300
|
+
return ["", `Options for "paratix ${command.name()} <file>":`, ...rows].join("\n");
|
|
6301
|
+
}
|
|
5585
6302
|
var program = new Command();
|
|
5586
|
-
program.name("paratix").description("Idempotent VPS setup tool in TypeScript").version("0.
|
|
5587
|
-
program.command("apply <file>").description("Apply a server definition").option(
|
|
6303
|
+
program.name("paratix").description("Idempotent VPS setup tool in TypeScript").version("0.14.0-5a61f29");
|
|
6304
|
+
var applyCommand = program.command("apply <file>").description("Apply a server definition").option(
|
|
5588
6305
|
"--diff",
|
|
5589
6306
|
"When combined with --dry-run, show a unified diff per module that would change.",
|
|
5590
6307
|
false
|
|
@@ -5592,9 +6309,19 @@ program.command("apply <file>").description("Apply a server definition").option(
|
|
|
5592
6309
|
"--dry-run",
|
|
5593
6310
|
"Only check, do not apply. Some modules validate prospective config but cannot verify runtime restarts.",
|
|
5594
6311
|
false
|
|
5595
|
-
).option(
|
|
6312
|
+
).option(
|
|
6313
|
+
"--env <key=value...>",
|
|
6314
|
+
"Set environment values passed to the playbook (repeatable; key=value).",
|
|
6315
|
+
collectEnvironment,
|
|
6316
|
+
{}
|
|
6317
|
+
).option("--env-file <path>", "Load environment values from a dotenv file.").option(
|
|
6318
|
+
"--filter <names>",
|
|
6319
|
+
"Run only the named recipes/modules (comma-separated, repeatable). Every other node is shown as skipped.",
|
|
6320
|
+
collectFilter,
|
|
6321
|
+
[]
|
|
6322
|
+
).option("--first-run", "Set PARATIX_FIRST_RUN=true before loading the playbook", false).option(
|
|
5596
6323
|
"--reconnect-timeout <seconds>",
|
|
5597
|
-
"SSH reconnect timeout (seconds, max 86400)",
|
|
6324
|
+
"SSH reconnect timeout override for reboots and port changes (seconds, max 86400; reboot default 300)",
|
|
5598
6325
|
parseReconnectTimeoutSeconds
|
|
5599
6326
|
).option("--verbose", "Show full stack traces on error", false).action(async (file, options) => {
|
|
5600
6327
|
try {
|
|
@@ -5608,6 +6335,8 @@ program.command("apply <file>").description("Apply a server definition").option(
|
|
|
5608
6335
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Commander options typed as Record<string, unknown>
|
|
5609
6336
|
envFile: options.envFile,
|
|
5610
6337
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Commander options typed as Record<string, unknown>
|
|
6338
|
+
filter: options.filter,
|
|
6339
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Commander options typed as Record<string, unknown>
|
|
5611
6340
|
firstRun: options.firstRun,
|
|
5612
6341
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Commander options typed as Record<string, unknown>
|
|
5613
6342
|
reconnectTimeout: options.reconnectTimeout,
|
|
@@ -5618,6 +6347,7 @@ program.command("apply <file>").description("Apply a server definition").option(
|
|
|
5618
6347
|
exitAfterApplyError(error, options.verbose);
|
|
5619
6348
|
}
|
|
5620
6349
|
});
|
|
6350
|
+
program.addHelpText("after", () => renderSubcommandOptionsHelp(applyCommand));
|
|
5621
6351
|
function parsePositiveNumber(value, options = {}) {
|
|
5622
6352
|
const parsed = Number(value);
|
|
5623
6353
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -5637,6 +6367,9 @@ function parsePositiveNumber(value, options = {}) {
|
|
|
5637
6367
|
function parseReconnectTimeoutSeconds(value) {
|
|
5638
6368
|
return parsePositiveNumber(value, { max: RECONNECT_TIMEOUT_MAX_SECONDS });
|
|
5639
6369
|
}
|
|
6370
|
+
function collectFilter(value, previous) {
|
|
6371
|
+
return [...previous, value];
|
|
6372
|
+
}
|
|
5640
6373
|
function collectEnvironment(value, previous) {
|
|
5641
6374
|
const eqIndex = value.indexOf("=");
|
|
5642
6375
|
if (eqIndex === -1) {
|
|
@@ -5658,8 +6391,38 @@ function collectEnvironment(value, previous) {
|
|
|
5658
6391
|
const accumulator = /* @__PURE__ */ Object.create(null);
|
|
5659
6392
|
return Object.assign(accumulator, previous, { [key]: value_ });
|
|
5660
6393
|
}
|
|
6394
|
+
var lastResortHandled = false;
|
|
6395
|
+
var LAST_RESORT_EXIT_CODE = 1;
|
|
6396
|
+
function handleLastResortError(error) {
|
|
6397
|
+
if (lastResortHandled) return;
|
|
6398
|
+
lastResortHandled = true;
|
|
6399
|
+
printExceptionError(error, false);
|
|
6400
|
+
performLastResortCleanup();
|
|
6401
|
+
if (process.exitCode === void 0 || process.exitCode === 0) {
|
|
6402
|
+
process.exitCode = LAST_RESORT_EXIT_CODE;
|
|
6403
|
+
}
|
|
6404
|
+
}
|
|
6405
|
+
function resetLastResortHandlerForTests() {
|
|
6406
|
+
lastResortHandled = false;
|
|
6407
|
+
}
|
|
6408
|
+
function forceExitAfterHandling() {
|
|
6409
|
+
setImmediate(() => {
|
|
6410
|
+
process.exit(process.exitCode ?? LAST_RESORT_EXIT_CODE);
|
|
6411
|
+
});
|
|
6412
|
+
}
|
|
6413
|
+
function installLastResortErrorHandlers() {
|
|
6414
|
+
process.on("unhandledRejection", (reason) => {
|
|
6415
|
+
handleLastResortError(reason);
|
|
6416
|
+
forceExitAfterHandling();
|
|
6417
|
+
});
|
|
6418
|
+
process.on("uncaughtException", (error) => {
|
|
6419
|
+
handleLastResortError(error);
|
|
6420
|
+
forceExitAfterHandling();
|
|
6421
|
+
});
|
|
6422
|
+
}
|
|
5661
6423
|
var entryScript = process.argv[1];
|
|
5662
6424
|
if (isDirectCliExecution(import.meta.url, entryScript)) {
|
|
6425
|
+
installLastResortErrorHandlers();
|
|
5663
6426
|
await program.parseAsync();
|
|
5664
6427
|
}
|
|
5665
6428
|
export {
|
|
@@ -5667,8 +6430,11 @@ export {
|
|
|
5667
6430
|
applyCliEnvironmentOverrides,
|
|
5668
6431
|
collectDefinitionErrors,
|
|
5669
6432
|
collectEnvironment,
|
|
6433
|
+
collectFilter,
|
|
5670
6434
|
exitAfterApplyError,
|
|
6435
|
+
handleLastResortError,
|
|
5671
6436
|
handleTsxLoadFailure,
|
|
6437
|
+
installLastResortErrorHandlers,
|
|
5672
6438
|
isDirectCliExecution,
|
|
5673
6439
|
isFirstRun,
|
|
5674
6440
|
isServerDefinitionLike,
|
|
@@ -5676,9 +6442,11 @@ export {
|
|
|
5676
6442
|
parsePositiveNumber,
|
|
5677
6443
|
parseReconnectTimeoutSeconds,
|
|
5678
6444
|
printExceptionError,
|
|
6445
|
+
renderSubcommandOptionsHelp,
|
|
6446
|
+
resetLastResortHandlerForTests,
|
|
5679
6447
|
resetTsxRegistrationForTests,
|
|
6448
|
+
resolveFilteredRun,
|
|
5680
6449
|
runApplyCommand,
|
|
5681
6450
|
withCliProcessEnvironment,
|
|
5682
6451
|
withSerializedPlaybookImport
|
|
5683
6452
|
};
|
|
5684
|
-
//# sourceMappingURL=cli.js.map
|