@williamthorsen/nmr 2.0.3 → 2.2.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/.meta/changelog.json +77 -0
- package/.readyup/kits/default.js +66 -35
- package/.readyup/manifest.json +38 -39
- package/CHANGELOG.md +26 -0
- package/README.md +8 -1
- package/dist/esm/assertResolvableCommandLists.d.ts +18 -0
- package/dist/esm/{check-cache/config-check-cache.js → assertResolvableCommandLists.js} +20 -12
- package/dist/esm/check-cache/check-cache-entries.js +1 -1
- package/dist/esm/check-cache/check-cache-gate.js +1 -1
- package/dist/esm/check-cache/check-cache-record.js +1 -1
- package/dist/esm/check-cache/check-cache.js +1 -1
- package/dist/esm/commands/build.js +1 -1
- package/dist/esm/commands/fmt.js +1 -1
- package/dist/esm/commands/taze.js +2 -2
- package/dist/esm/config-validation.js +25 -3
- package/dist/esm/config.js +1 -1
- package/dist/esm/crossing-diagnostics.js +1 -1
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/delegation/delegate-host.js +10 -3
- package/dist/esm/delegation/host-filesystem.d.ts +24 -0
- package/dist/esm/delegation/host-filesystem.js +45 -0
- package/dist/esm/delegation/remote-probe.js +1 -1
- package/dist/esm/delegation/remote-run.d.ts +12 -6
- package/dist/esm/delegation/remote-run.js +30 -8
- package/dist/esm/delegation/removeStaleCheckouts.d.ts +33 -0
- package/dist/esm/delegation/removeStaleCheckouts.js +98 -0
- package/dist/esm/delegation/user-config.d.ts +6 -1
- package/dist/esm/delegation/user-config.js +14 -4
- package/dist/esm/{help.js → describeHelp.js} +1 -1
- package/dist/esm/fan-out.d.ts +7 -0
- package/dist/esm/fan-out.js +7 -2
- package/dist/esm/invocation.d.ts +1 -1
- package/dist/esm/machine-lock.d.ts +0 -4
- package/dist/esm/machine-lock.js +2 -2
- package/dist/esm/presentation.js +1 -1
- package/dist/esm/report-format.d.ts +2 -8
- package/dist/esm/report-format.js +8 -8
- package/dist/esm/resolve-scripts.js +1 -1
- package/dist/esm/route.js +1 -1
- package/dist/esm/run-chain.js +1 -1
- package/dist/esm/runCli.js +2 -2
- package/dist/esm/tiers.js +1 -1
- package/dist/esm/types.d.ts +12 -0
- package/dist/esm/verbosity.d.ts +2 -1
- package/dist/esm/verbosity.js +1 -1
- package/dist/esm/vitest.js +2 -2
- package/docs/delegate.md +22 -5
- package/docs/scripts.md +12 -0
- package/package.json +8 -8
- package/dist/esm/check-cache/config-check-cache.d.ts +0 -17
- /package/dist/esm/check-cache/{replay-assembly.d.ts → assembleReplay.d.ts} +0 -0
- /package/dist/esm/check-cache/{replay-assembly.js → assembleReplay.js} +0 -0
- /package/dist/esm/check-cache/{check-cache-delegation.d.ts → recordDelegatedPass.d.ts} +0 -0
- /package/dist/esm/check-cache/{check-cache-delegation.js → recordDelegatedPass.js} +0 -0
- /package/dist/esm/commands/{load-package-maturity-period.d.ts → loadPackageMaturityPeriod.d.ts} +0 -0
- /package/dist/esm/commands/{load-package-maturity-period.js → loadPackageMaturityPeriod.js} +0 -0
- /package/dist/esm/commands/{taze-passes.d.ts → planTazePasses.d.ts} +0 -0
- /package/dist/esm/commands/{taze-passes.js → planTazePasses.js} +0 -0
- /package/dist/esm/commands/{format-targets.d.ts → resolveFormatTargets.d.ts} +0 -0
- /package/dist/esm/commands/{format-targets.js → resolveFormatTargets.js} +0 -0
- /package/dist/esm/commands/{tsconfig-chain.d.ts → resolveTsconfigChain.d.ts} +0 -0
- /package/dist/esm/commands/{tsconfig-chain.js → resolveTsconfigChain.js} +0 -0
- /package/dist/esm/delegation/{remote-snapshot.d.ts → createSnapshot.d.ts} +0 -0
- /package/dist/esm/delegation/{remote-snapshot.js → createSnapshot.js} +0 -0
- /package/dist/esm/delegation/{remote-probe-host.d.ts → probeHostLocks.d.ts} +0 -0
- /package/dist/esm/delegation/{remote-probe-host.js → probeHostLocks.js} +0 -0
- /package/dist/esm/delegation/{delegation-target.d.ts → resolveDelegationTarget.d.ts} +0 -0
- /package/dist/esm/delegation/{delegation-target.js → resolveDelegationTarget.js} +0 -0
- /package/dist/esm/{help.d.ts → describeHelp.d.ts} +0 -0
- /package/dist/esm/helpers/{hook-name.d.ts → isHookName.d.ts} +0 -0
- /package/dist/esm/helpers/{hook-name.js → isHookName.js} +0 -0
- /package/dist/esm/helpers/{type-guards.d.ts → isStringRecord.d.ts} +0 -0
- /package/dist/esm/helpers/{type-guards.js → isStringRecord.js} +0 -0
- /package/dist/esm/{git-ignored-paths.d.ts → listGitIgnoredPaths.d.ts} +0 -0
- /package/dist/esm/{git-ignored-paths.js → listGitIgnoredPaths.js} +0 -0
- /package/dist/esm/{bin-path.d.ts → prependBinDirs.d.ts} +0 -0
- /package/dist/esm/{bin-path.js → prependBinDirs.js} +0 -0
- /package/dist/esm/{workspace-globs.d.ts → toWorkspacePackageGlobs.d.ts} +0 -0
- /package/dist/esm/{workspace-globs.js → toWorkspacePackageGlobs.js} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { isRecord, readCacheEntry, readJsonCacheEntry, removeCacheDir, removeCacheEntry, resolveCacheEntryPath, writeCacheEntry, } from '@williamthorsen/nmr-core';
|
|
3
3
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
|
-
import { isStringRecord } from "../helpers/
|
|
4
|
+
import { isStringRecord } from "../helpers/isStringRecord.js";
|
|
5
5
|
import { writeDebugNote } from "./check-cache.js";
|
|
6
6
|
const CACHE_TOOL = 'nmr-check';
|
|
7
7
|
const UNSAFE_SLUG_CHARACTERS = /[^\w.-]+/g;
|
|
@@ -7,11 +7,11 @@ import { resolveChannel } from "../output-channel.js";
|
|
|
7
7
|
import { splitSegments } from "../shell-syntax.js";
|
|
8
8
|
import { formatMisplacedNoCacheWarning, isCacheableCommand, resolveTreeSnapshot, writeDebugNote, } from "./check-cache.js";
|
|
9
9
|
import { findStaleBuildOutput, readBuildOutputState } from "./check-cache-build-output.js";
|
|
10
|
-
import { recordDelegatedPass } from "./check-cache-delegation.js";
|
|
11
10
|
import { certifyRetention, readCheckCacheEntry } from "./check-cache-entries.js";
|
|
12
11
|
import { computeCacheKey, computeRetentionKey } from "./check-cache-key.js";
|
|
13
12
|
import { recordPass } from "./check-cache-record.js";
|
|
14
13
|
import { writePassManifestEntry } from "./pass-manifest.js";
|
|
14
|
+
import { recordDelegatedPass } from "./recordDelegatedPass.js";
|
|
15
15
|
export function openGate(options) {
|
|
16
16
|
const { command, config, env, monorepoRoot, passthrough, readTree, stderr, style } = options;
|
|
17
17
|
if (!isCacheableCommand(config.checkCache, command)) {
|
|
@@ -4,13 +4,13 @@ import { isEnvSwitchOn } from "../env-switch.js";
|
|
|
4
4
|
import { deriveExcerpt } from "../helpers/deriveExcerpt.js";
|
|
5
5
|
import { composeTranscript } from "../helpers/transcript.js";
|
|
6
6
|
import { RUN_IF_PRESENT_ENV_VAR, VERSION } from "../invocation.js";
|
|
7
|
+
import { assembleReplay } from "./assembleReplay.js";
|
|
7
8
|
import { isCacheableCommand, resolveTreeSnapshot, writeDebugNote, } from "./check-cache.js";
|
|
8
9
|
import { findStaleBuildOutput, readBuildOutputState } from "./check-cache-build-output.js";
|
|
9
10
|
import { readCheckCacheEntry, recordTranscript, writeCheckCacheEntry, } from "./check-cache-entries.js";
|
|
10
11
|
import { CURRENT_RUNTIME } from "./check-cache-key.js";
|
|
11
12
|
import { writePassManifestEntry } from "./pass-manifest.js";
|
|
12
13
|
import { renderRecording, renderRefusal, resolveRecording } from "./recording.js";
|
|
13
|
-
import { assembleReplay } from "./replay-assembly.js";
|
|
14
14
|
export async function recordHostPass(options) {
|
|
15
15
|
const { env, host, key, ref, retention, snapshot, stderr } = options;
|
|
16
16
|
const recorded = await readCheckCacheEntry(ref);
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { hashWorkingTree, readHeadSha, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
|
|
4
4
|
import { isEnvSwitchOn } from "../env-switch.js";
|
|
5
|
-
import { isHookName } from "../helpers/
|
|
5
|
+
import { isHookName } from "../helpers/isHookName.js";
|
|
6
6
|
export const GIT_TREE_READER = { hashWorkingTree, readHeadSha };
|
|
7
7
|
export const DEBUG_ENV_VAR = 'NMR_DEBUG';
|
|
8
8
|
export const DEFAULT_CACHEABLE_COMMANDS = [
|
|
@@ -8,8 +8,8 @@ import { writeStatedOutcome } from "../stated-outcome.js";
|
|
|
8
8
|
import { DEFAULT_OUTDIR, hasExpectedBuildOutput, readEmitDuration, resolveBuildCachePath, resolveEntryPoints, resolveToolchainFingerprint, writeEmitDuration, } from "./build-output.js";
|
|
9
9
|
import { assertSupportedTypeScript, formatDiagnostics, getCompiler, loadCompiler } from "./compiler.js";
|
|
10
10
|
import { discardScratchDirs, publishOutput, removeScratchDirs } from "./publish-output.js";
|
|
11
|
+
import { resolveTsconfigChain } from "./resolveTsconfigChain.js";
|
|
11
12
|
import { rewriteSpecifiers } from "./rewrite-specifiers.js";
|
|
12
|
-
import { resolveTsconfigChain } from "./tsconfig-chain.js";
|
|
13
13
|
const SOURCE_ROOT = 'src';
|
|
14
14
|
export async function buildPackage(packageDir, options = {}) {
|
|
15
15
|
await loadCompiler(options.loadTypeScript);
|
package/dist/esm/commands/fmt.js
CHANGED
|
@@ -10,8 +10,8 @@ import { NMR_FMT_BIN } from "../bins.js";
|
|
|
10
10
|
import { NO_CACHE_ENV_VAR } from "../check-cache/check-cache.js";
|
|
11
11
|
import { readEnvSwitch } from "../env-switch.js";
|
|
12
12
|
import { prepareFmtCache } from "./fmt-cache.js";
|
|
13
|
-
import { resolveFormatTargets } from "./format-targets.js";
|
|
14
13
|
import { BROKEN_MANIFEST_FIX, findPackageManifest, resolveDeclaredCli, } from "./package-manifest.js";
|
|
14
|
+
import { resolveFormatTargets } from "./resolveFormatTargets.js";
|
|
15
15
|
import { resolveInheritedExitCode, spawnInherited } from "./spawn-inherited.js";
|
|
16
16
|
const PRETTIER_PACKAGE = 'prettier';
|
|
17
17
|
const DEFAULT_CONFIG_PATH = fileURLToPath(new URL(`../prettier-default.config${path.extname(fileURLToPath(import.meta.url))}`, import.meta.url));
|
|
@@ -4,11 +4,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
5
5
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
6
6
|
import { NMR_TAZE_BIN } from "../bins.js";
|
|
7
|
-
import { loadPackageMaturityPeriod } from "./
|
|
7
|
+
import { loadPackageMaturityPeriod } from "./loadPackageMaturityPeriod.js";
|
|
8
8
|
import { checkNodePin as checkNodePinDefault } from "./node-pin.js";
|
|
9
|
+
import { planTazePasses } from "./planTazePasses.js";
|
|
9
10
|
import { resolveInheritedExitCode, spawnInherited } from "./spawn-inherited.js";
|
|
10
11
|
import { isSilent, readFlag } from "./taze-args.js";
|
|
11
|
-
import { planTazePasses } from "./taze-passes.js";
|
|
12
12
|
const TAZE_CLI_SPECIFIER = 'taze/cli';
|
|
13
13
|
const REQUEST_TIMEOUT_FLAG = '--request-timeout';
|
|
14
14
|
const REQUEST_TIMEOUT_FLAGS = [REQUEST_TIMEOUT_FLAG, '--requestTimeout'];
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { isRecord, UserError } from '@williamthorsen/nmr-core';
|
|
2
|
-
import {
|
|
2
|
+
import { assertResolvableCommandLists } from "./assertResolvableCommandLists.js";
|
|
3
3
|
import { findUnexpressibleToken } from "./steps.js";
|
|
4
4
|
import { formatVerbosityRejection, isCommandVerbosity } from "./verbosity.js";
|
|
5
5
|
export const CONFIG_TIERS = {
|
|
6
6
|
root: {
|
|
7
7
|
label: 'a monorepo-root config',
|
|
8
|
-
honoredKeys: ['checkCache', 'delegate', 'output', 'rootScripts', 'selfHostedRunner', 'workspaceScripts'],
|
|
8
|
+
honoredKeys: ['checkCache', 'delegate', 'fanOut', 'output', 'rootScripts', 'selfHostedRunner', 'workspaceScripts'],
|
|
9
9
|
elsewhere: "the package's own config",
|
|
10
10
|
},
|
|
11
11
|
workspace: {
|
|
@@ -17,6 +17,7 @@ export const CONFIG_TIERS = {
|
|
|
17
17
|
const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
|
|
18
18
|
const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
|
|
19
19
|
const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
|
|
20
|
+
const RECOGNIZED_FAN_OUT_KEYS = ['extraSortedCommands'];
|
|
20
21
|
const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
|
|
21
22
|
const RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ['exempt'];
|
|
22
23
|
const RECOGNIZED_STEP_KEYS = ['run', 'shouldDeclineArguments', 'shouldRunInBackground'];
|
|
@@ -43,6 +44,9 @@ export function validateConfig(value, configPath, baseDir) {
|
|
|
43
44
|
config.checkCache = checkCache;
|
|
44
45
|
if (value['delegate'] !== undefined)
|
|
45
46
|
config.delegate = validateDelegateField(value['delegate'], configPath);
|
|
47
|
+
const fanOut = validateFanOutField(value, configPath);
|
|
48
|
+
if (fanOut)
|
|
49
|
+
config.fanOut = fanOut;
|
|
46
50
|
const output = validateOutputField(value, configPath);
|
|
47
51
|
if (output)
|
|
48
52
|
config.output = output;
|
|
@@ -55,7 +59,7 @@ export function validateConfig(value, configPath, baseDir) {
|
|
|
55
59
|
const selfHostedRunner = validateSelfHostedRunnerField(value, configPath);
|
|
56
60
|
if (selfHostedRunner)
|
|
57
61
|
config.selfHostedRunner = selfHostedRunner;
|
|
58
|
-
|
|
62
|
+
assertResolvableCommandLists(config, configPath, baseDir);
|
|
59
63
|
return config;
|
|
60
64
|
}
|
|
61
65
|
function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
@@ -166,6 +170,24 @@ function validateDelegateField(delegate, configPath) {
|
|
|
166
170
|
throw new UserError(`Invalid nmr config at ${configPath}: \`delegate\` accepts only \`false\`, which keeps the repo's checks on the ` +
|
|
167
171
|
'machine that has the tree. Hosts and `when` belong in the user config, `~/.config/nmr/config.ts`.');
|
|
168
172
|
}
|
|
173
|
+
function validateFanOutField(value, configPath) {
|
|
174
|
+
const fanOut = value['fanOut'];
|
|
175
|
+
if (fanOut === undefined) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
if (!isRecord(fanOut)) {
|
|
179
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`fanOut\` must be an object`);
|
|
180
|
+
}
|
|
181
|
+
assertRecognizedKeys(fanOut, RECOGNIZED_FAN_OUT_KEYS, configPath, 'fanOut.');
|
|
182
|
+
const extraSortedCommands = fanOut['extraSortedCommands'];
|
|
183
|
+
if (extraSortedCommands === undefined) {
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
if (!isStringArray(extraSortedCommands)) {
|
|
187
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`fanOut.extraSortedCommands\` must be a string[]`);
|
|
188
|
+
}
|
|
189
|
+
return { extraSortedCommands };
|
|
190
|
+
}
|
|
169
191
|
function validateOutputField(value, configPath) {
|
|
170
192
|
const output = value['output'];
|
|
171
193
|
if (output === undefined) {
|
package/dist/esm/config.js
CHANGED
|
@@ -3,8 +3,8 @@ import { UserError } from '@williamthorsen/nmr-core';
|
|
|
3
3
|
import { CONFIG_TIERS, validateConfig } from "./config-validation.js";
|
|
4
4
|
import { resolveConfigPath } from "./helpers/config-path.js";
|
|
5
5
|
import { escapeControlCharacters } from "./helpers/escapeControlCharacters.js";
|
|
6
|
-
import { isHookName } from "./helpers/hook-name.js";
|
|
7
6
|
import { importConfigDefault } from "./helpers/importConfigDefault.js";
|
|
7
|
+
import { isHookName } from "./helpers/isHookName.js";
|
|
8
8
|
import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
9
9
|
import { describeScript } from "./resolver.js";
|
|
10
10
|
import { readSelfReference, readStepListSelfReference } from "./steps.js";
|
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { STATUS_GLYPHS, UserError } from '@williamthorsen/nmr-core';
|
|
3
3
|
import { resolveConfigPath } from "./helpers/config-path.js";
|
|
4
4
|
import { escapeControlCharacters } from "./helpers/escapeControlCharacters.js";
|
|
5
|
-
import { isHookName } from "./helpers/
|
|
5
|
+
import { isHookName } from "./helpers/isHookName.js";
|
|
6
6
|
import { resolvePackageJsonPath } from "./helpers/package-json.js";
|
|
7
7
|
import { hasRunnableHook } from "./hooks.js";
|
|
8
8
|
import { expandScript, findChainedSelfReference, readPackageJsonScripts, } from "./resolver.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { NmrConfig } from './types.js';
|
|
2
|
-
export type { BuildConfig, CheckCacheConfig, CommandVerbosity, NmrConfig, OutputConfig, ScriptValue, SelfHostedRunnerConfig, StepSpec, } from './types.js';
|
|
2
|
+
export type { BuildConfig, CheckCacheConfig, CommandVerbosity, FanOutConfig, NmrConfig, OutputConfig, ScriptValue, SelfHostedRunnerConfig, StepSpec, } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Returns `config` unchanged, typing a configuration file's default export as `NmrConfig`.
|
|
5
5
|
*
|
|
@@ -9,11 +9,11 @@ import { createBoundedBuffer } from "../helpers/createBoundedBuffer.js";
|
|
|
9
9
|
import { USAGE_EXIT_CODE, } from "../invocation.js";
|
|
10
10
|
import { isDelegationFlag } from "../invocation-args.js";
|
|
11
11
|
import { reportVerdict } from "../presentation.js";
|
|
12
|
-
import {
|
|
12
|
+
import { createSnapshot } from "./createSnapshot.js";
|
|
13
13
|
import { chooseHost, probeHost } from "./remote-probe.js";
|
|
14
14
|
import { DELEGATE_ENV_VAR, DELEGATE_FLAG, DELEGATE_TO_FLAG, DELEGATION_FAILURE_EXIT_CODE, deriveCheckoutPath, formatMisplacedDelegationWarning, readLastLine, runRemotely, } from "./remote-run.js";
|
|
15
|
-
import {
|
|
16
|
-
import { formatDisplayPath, loadUserConfig } from "./user-config.js";
|
|
15
|
+
import { resolveDelegationTarget } from "./resolveDelegationTarget.js";
|
|
16
|
+
import { DEFAULT_REMOVE_CHECKOUTS_AFTER_DAYS, formatDisplayPath, loadUserConfig } from "./user-config.js";
|
|
17
17
|
export async function resolveExecutionTarget(invocation) {
|
|
18
18
|
const { command, context, env, parsedArgs, stderr, styles } = invocation;
|
|
19
19
|
if (parsedArgs.passthrough.some(isDelegationFlag)) {
|
|
@@ -69,6 +69,7 @@ export async function runPlanOnHost(plan, target) {
|
|
|
69
69
|
target,
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
72
73
|
const PROBE_RESULT_PHRASES = {
|
|
73
74
|
busy: 'is busy',
|
|
74
75
|
free: 'is free',
|
|
@@ -133,6 +134,7 @@ async function delegate(options) {
|
|
|
133
134
|
}
|
|
134
135
|
const { host } = choice;
|
|
135
136
|
const retained = { stderr: createBoundedBuffer(), stdout: createBoundedBuffer() };
|
|
137
|
+
const removeCheckoutsAfterMs = snapshot.ok ? await readRemoveCheckoutsAfterMs(env) : null;
|
|
136
138
|
const result = snapshot.ok
|
|
137
139
|
? await runRemotely({
|
|
138
140
|
command,
|
|
@@ -142,6 +144,7 @@ async function delegate(options) {
|
|
|
142
144
|
monorepoRoot: context.monorepoRoot,
|
|
143
145
|
nmrArgs: composeForwardedArgs(command, parsedArgs),
|
|
144
146
|
remoteEnv,
|
|
147
|
+
removeCheckoutsAfterMs,
|
|
145
148
|
snapshot,
|
|
146
149
|
stderr: createTee(stderr, retained.stderr),
|
|
147
150
|
stdout: createTee(stdout, retained.stdout),
|
|
@@ -178,6 +181,10 @@ async function delegate(options) {
|
|
|
178
181
|
function formatRunHereLine(command, listedHosts, reason) {
|
|
179
182
|
return `nmr: running ${command} here rather than on ${listedHosts}: ${reason}\n`;
|
|
180
183
|
}
|
|
184
|
+
async function readRemoveCheckoutsAfterMs(env) {
|
|
185
|
+
const days = (await loadUserConfig(env))?.config.delegate?.removeCheckoutsAfterDays ?? DEFAULT_REMOVE_CHECKOUTS_AFTER_DAYS;
|
|
186
|
+
return days === false ? null : days * MS_PER_DAY;
|
|
187
|
+
}
|
|
181
188
|
async function resolveHost(hosts, toplevel, env, stderr) {
|
|
182
189
|
const [first, ...rest] = hosts;
|
|
183
190
|
if (rest.length === 0) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appends to a failed step's message the resource that the host's filesystem has run out of, when the step's output
|
|
3
|
+
* reports a full filesystem and the counts that `runOnHost` reads say which. A write fails alike for lack of space and
|
|
4
|
+
* of inodes.
|
|
5
|
+
*/
|
|
6
|
+
export declare function appendFilesystemReason(message: string, output: string, runOnHost: (script: string) => Promise<{
|
|
7
|
+
exitCode: number;
|
|
8
|
+
output: string;
|
|
9
|
+
}>): Promise<string>;
|
|
10
|
+
/** Composes the shell command that prints, on the host, the free counts of the filesystem that holds its home. */
|
|
11
|
+
export declare function composeFilesystemCheckScript(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Names the resource that the host's filesystem has run out of, from what `composeFilesystemCheckScript` printed, or
|
|
14
|
+
* returns `undefined` when neither is exhausted or the output does not include the counts. A filesystem that reports
|
|
15
|
+
* a total of zero inodes, as btrfs does, allocates them as it goes, so it cannot run out of them.
|
|
16
|
+
*/
|
|
17
|
+
export declare function describeExhaustedResource(output: string): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Runs on the host and prints, as one JSON line, the total and free inodes and the blocks available to this user on
|
|
20
|
+
* the filesystem that holds the home directory. It is sent as source, so it refers to nothing outside its own body.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export declare function readHostFilesystem(): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { quoteForShell } from "../helpers/quoteForShell.js";
|
|
2
|
+
export async function appendFilesystemReason(message, output, runOnHost) {
|
|
3
|
+
if (!FULL_FILESYSTEM_PATTERN.test(output))
|
|
4
|
+
return message;
|
|
5
|
+
const check = await runOnHost(composeFilesystemCheckScript());
|
|
6
|
+
const reason = check.exitCode === 0 ? describeExhaustedResource(check.output) : undefined;
|
|
7
|
+
return reason === undefined ? message : `${message}: ${reason}`;
|
|
8
|
+
}
|
|
9
|
+
export function composeFilesystemCheckScript() {
|
|
10
|
+
return `node -e ${quoteForShell(`(${readHostFilesystem.toString()})();`)}`;
|
|
11
|
+
}
|
|
12
|
+
export function describeExhaustedResource(output) {
|
|
13
|
+
const counts = output
|
|
14
|
+
.split('\n')
|
|
15
|
+
.map(parseCounts)
|
|
16
|
+
.findLast((value) => value !== undefined);
|
|
17
|
+
if (counts !== undefined && counts.files > 0 && counts.ffree === 0)
|
|
18
|
+
return "the host's filesystem has run out of inodes";
|
|
19
|
+
if (counts?.bavail === 0)
|
|
20
|
+
return "the host's filesystem has run out of space";
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
export function readHostFilesystem() {
|
|
24
|
+
const fs = process.getBuiltinModule('node:fs');
|
|
25
|
+
const os = process.getBuiltinModule('node:os');
|
|
26
|
+
const { bavail, ffree, files } = fs.statfsSync(os.homedir());
|
|
27
|
+
process.stdout.write(`${JSON.stringify({ bavail, ffree, files })}\n`);
|
|
28
|
+
}
|
|
29
|
+
const FULL_FILESYSTEM_PATTERN = /no space left on device|ENOSPC/i;
|
|
30
|
+
function parseCounts(line) {
|
|
31
|
+
try {
|
|
32
|
+
const value = JSON.parse(line);
|
|
33
|
+
if (typeof value !== 'object' || value === null)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (!('bavail' in value) || !('ffree' in value) || !('files' in value))
|
|
36
|
+
return undefined;
|
|
37
|
+
const { bavail, ffree, files } = value;
|
|
38
|
+
return typeof bavail === 'number' && typeof ffree === 'number' && typeof files === 'number'
|
|
39
|
+
? { bavail, ffree, files }
|
|
40
|
+
: undefined;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -2,8 +2,8 @@ import path from 'node:path';
|
|
|
2
2
|
import { isRecord } from '@williamthorsen/nmr-core';
|
|
3
3
|
import { quoteForShell } from "../helpers/quoteForShell.js";
|
|
4
4
|
import { LOCK_DIR_ENV_VAR, LOCK_FILE, STALE_AFTER_MS } from "../machine-lock.js";
|
|
5
|
+
import { probeHostLocks } from "./probeHostLocks.js";
|
|
5
6
|
import { CHECKOUT_LOCK_PATH } from "./remote-host-session.js";
|
|
6
|
-
import { probeHostLocks } from "./remote-probe-host.js";
|
|
7
7
|
import { DELEGATION_FAILURE_EXIT_CODE, readLastLine, runSsh } from "./remote-run.js";
|
|
8
8
|
export function chooseHost(options) {
|
|
9
9
|
const { hosts, onSettled, probe } = options;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Writable } from 'node:stream';
|
|
2
2
|
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
3
|
+
/** The directory on the host, relative to its home directory, that contains one directory per checkout key. */
|
|
4
|
+
export declare const CHECKOUTS_DIR = ".cache/nmr/delegate";
|
|
3
5
|
/** Names the hosts to which nmr delegates a command when the invocation does not say otherwise. */
|
|
4
6
|
export declare const DELEGATE_ENV_VAR = "NMR_DELEGATE";
|
|
5
7
|
/** The flag that runs the command on a host that `NMR_DELEGATE` or the user config names. */
|
|
@@ -56,9 +58,11 @@ export declare function composeGitSshCommand(env: NodeJS.ProcessEnv): string;
|
|
|
56
58
|
* @internal
|
|
57
59
|
*/
|
|
58
60
|
export declare function composeSshArgv(env: NodeJS.ProcessEnv, host: string, remoteCommand: string): string[];
|
|
61
|
+
/** Returns the key of the checkout on the host that serves the worktree at `toplevel` on the machine `hostname`. */
|
|
62
|
+
export declare function deriveCheckoutKey(hostname: string, toplevel: string): string;
|
|
59
63
|
/**
|
|
60
|
-
* Returns the checkout's path on the host, relative to its home directory:
|
|
61
|
-
* repository
|
|
64
|
+
* Returns the checkout's path on the host, relative to its home directory: the checkout's key, followed by the
|
|
65
|
+
* repository's own directory name, which nmr on the host reports as the root's scope.
|
|
62
66
|
*
|
|
63
67
|
* @internal
|
|
64
68
|
*/
|
|
@@ -74,11 +78,12 @@ export declare function readLastLine(output: string): string;
|
|
|
74
78
|
* Runs an nmr invocation on `host` against a snapshot of the local working tree, streaming the command's output to
|
|
75
79
|
* `stdout` and `stderr` and returning its exit status.
|
|
76
80
|
*
|
|
77
|
-
* The host keeps one checkout per
|
|
81
|
+
* The host keeps one checkout per worktree on each delegating machine, under `~/.cache/nmr/delegate/`, so concurrent
|
|
78
82
|
* delegations from different worktrees do not share a checkout, and `node_modules` and build output persist
|
|
79
|
-
* between runs.
|
|
80
|
-
*
|
|
81
|
-
*
|
|
83
|
+
* between runs. Before the push, the delegation removes the host's other checkouts that have not been used for
|
|
84
|
+
* longer than `removeCheckoutsAfterMs`, unless it is `null`. Check out, install, and run take one connection, whose
|
|
85
|
+
* session holds the checkout's lock, so that two delegations from one worktree run one after the other; what a phase
|
|
86
|
+
* other than `run` prints is returned rather than streamed, because it matters only when the phase fails.
|
|
82
87
|
*/
|
|
83
88
|
export declare function runRemotely(options: {
|
|
84
89
|
command: string;
|
|
@@ -88,6 +93,7 @@ export declare function runRemotely(options: {
|
|
|
88
93
|
monorepoRoot: string;
|
|
89
94
|
nmrArgs: readonly string[];
|
|
90
95
|
remoteEnv: Readonly<Record<string, string>>;
|
|
96
|
+
removeCheckoutsAfterMs: number | null;
|
|
91
97
|
snapshot: {
|
|
92
98
|
sha: string;
|
|
93
99
|
toplevel: string;
|
|
@@ -9,8 +9,11 @@ import { createBoundedBuffer } from "../helpers/createBoundedBuffer.js";
|
|
|
9
9
|
import { quoteForShell } from "../helpers/quoteForShell.js";
|
|
10
10
|
import { STALE_AFTER_MS } from "../machine-lock.js";
|
|
11
11
|
import { formatFlagSpelling } from "../nmr-flags.js";
|
|
12
|
+
import { appendFilesystemReason } from "./host-filesystem.js";
|
|
12
13
|
import { CHECKOUT_LOCK_PATH } from "./remote-host-session.js";
|
|
13
14
|
import { composeSessionScript, createSessionDemultiplexer, formatDelegationWaitingLine } from "./remote-session.js";
|
|
15
|
+
import { CHECKOUT_STAMP_PATH, composeCheckoutRemovalScript } from "./removeStaleCheckouts.js";
|
|
16
|
+
export const CHECKOUTS_DIR = '.cache/nmr/delegate';
|
|
14
17
|
export const DELEGATE_ENV_VAR = 'NMR_DELEGATE';
|
|
15
18
|
export const DELEGATE_FLAG = formatFlagSpelling('delegate');
|
|
16
19
|
export const DELEGATE_TO_FLAG = formatFlagSpelling('delegateTo');
|
|
@@ -28,9 +31,11 @@ export function composeSshArgv(env, host, remoteCommand) {
|
|
|
28
31
|
? ['ssh', ...sshArgs]
|
|
29
32
|
: ['/bin/sh', '-c', `${sshCommand} "$@"`, sshCommand, ...sshArgs];
|
|
30
33
|
}
|
|
34
|
+
export function deriveCheckoutKey(hostname, toplevel) {
|
|
35
|
+
return createHash('sha256').update(`${hostname}\0${toplevel}`).digest('hex').slice(0, 16);
|
|
36
|
+
}
|
|
31
37
|
export function deriveCheckoutPath(hostname, toplevel) {
|
|
32
|
-
|
|
33
|
-
return path.posix.join('.cache/nmr/delegate', key, path.basename(toplevel));
|
|
38
|
+
return path.posix.join(CHECKOUTS_DIR, deriveCheckoutKey(hostname, toplevel), path.basename(toplevel));
|
|
34
39
|
}
|
|
35
40
|
export function formatMisplacedDelegationWarning(command, style) {
|
|
36
41
|
return (`${STATUS_GLYPHS[style].warning.text} ${DELEGATE_FLAG}, ${DELEGATE_TO_FLAG}, and ${NO_DELEGATE_FLAG} after the ` +
|
|
@@ -44,15 +49,30 @@ export function readLastLine(output) {
|
|
|
44
49
|
.findLast((line) => line !== '') ?? '');
|
|
45
50
|
}
|
|
46
51
|
export async function runRemotely(options) {
|
|
47
|
-
const { command, cwd, env, host, monorepoRoot, nmrArgs, remoteEnv, snapshot, stderr, stdout } = options;
|
|
52
|
+
const { command, cwd, env, host, monorepoRoot, nmrArgs, remoteEnv, removeCheckoutsAfterMs, snapshot, stderr, stdout, } = options;
|
|
48
53
|
const checkoutPath = deriveCheckoutPath(os.hostname(), snapshot.toplevel);
|
|
49
54
|
const checkoutDir = `"$HOME"/${quoteForShell(checkoutPath)}`;
|
|
50
|
-
|
|
55
|
+
function runOnHost(script) {
|
|
56
|
+
return runSsh(host, script, env);
|
|
57
|
+
}
|
|
58
|
+
const removal = composeCheckoutRemovalScript({
|
|
59
|
+
checkoutsDir: CHECKOUTS_DIR,
|
|
60
|
+
lockPath: CHECKOUT_LOCK_PATH,
|
|
61
|
+
ownKey: deriveCheckoutKey(os.hostname(), snapshot.toplevel),
|
|
62
|
+
removeAfterMs: removeCheckoutsAfterMs,
|
|
63
|
+
staleAfterMs: STALE_AFTER_MS,
|
|
64
|
+
stampPath: CHECKOUT_STAMP_PATH,
|
|
65
|
+
});
|
|
66
|
+
const prepare = await runSsh(host, `{ ${removal} || :; }; mkdir -p ${checkoutDir} && cd ${checkoutDir} && { [ -d .git ] || git init --quiet; } && ` +
|
|
67
|
+
`{ touch ${quoteForShell(CHECKOUT_STAMP_PATH)} || :; }`, env);
|
|
51
68
|
if (prepare.exitCode !== 0) {
|
|
69
|
+
const isUnreachable = prepare.exitCode === DELEGATION_FAILURE_EXIT_CODE;
|
|
52
70
|
return {
|
|
53
71
|
ok: false,
|
|
54
|
-
isUnreachable
|
|
55
|
-
message:
|
|
72
|
+
isUnreachable,
|
|
73
|
+
message: isUnreachable
|
|
74
|
+
? `could not prepare ${host}`
|
|
75
|
+
: await appendFilesystemReason(`could not prepare ${host}`, prepare.output, runOnHost),
|
|
56
76
|
output: prepare.output,
|
|
57
77
|
phase: 'prepare',
|
|
58
78
|
waitedMs: 0,
|
|
@@ -69,7 +89,8 @@ export async function runRemotely(options) {
|
|
|
69
89
|
`${snapshot.sha}:${SNAPSHOT_REF}`,
|
|
70
90
|
], { cwd: snapshot.toplevel, env: { ...env, [SSH_COMMAND_ENV_VAR]: composeGitSshCommand(env) } });
|
|
71
91
|
if (push.exitCode !== 0) {
|
|
72
|
-
|
|
92
|
+
const message = await appendFilesystemReason(`could not push to ${host}`, push.output, runOnHost);
|
|
93
|
+
return { ok: false, message, output: push.output, phase: 'push', waitedMs: 0 };
|
|
73
94
|
}
|
|
74
95
|
const nonce = randomUUID();
|
|
75
96
|
const marker = `__NMR_DELEGATE_${nonce}__`;
|
|
@@ -127,7 +148,8 @@ export async function runRemotely(options) {
|
|
|
127
148
|
demultiplexer.flush();
|
|
128
149
|
if (failure !== undefined) {
|
|
129
150
|
const output = [demultiplexer.readOutput(), failure.output].filter((text) => text !== '').join('\n');
|
|
130
|
-
|
|
151
|
+
const message = await appendFilesystemReason(describeFailedPhase(failure, host), output, runOnHost);
|
|
152
|
+
return { ok: false, message, output, phase: failure.phase, waitedMs };
|
|
131
153
|
}
|
|
132
154
|
if (!demultiplexer.hasStarted()) {
|
|
133
155
|
return {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The file whose mtime records a checkout's last use, relative to the checkout. */
|
|
2
|
+
export declare const CHECKOUT_STAMP_PATH = ".git/nmr-delegate-used";
|
|
3
|
+
/** What the removal needs, sent to it as JSON. */
|
|
4
|
+
export interface CheckoutRemovalParams {
|
|
5
|
+
/** The directory that contains one directory per checkout key, relative to the host's home directory. */
|
|
6
|
+
checkoutsDir: string;
|
|
7
|
+
/** The checkout's lock file, relative to the checkout. */
|
|
8
|
+
lockPath: string;
|
|
9
|
+
/** The key of the delegation's own checkout, which is never removed. */
|
|
10
|
+
ownKey: string;
|
|
11
|
+
/** Age past which an unused checkout is removed, or `null` to keep every checkout. */
|
|
12
|
+
removeAfterMs: number | null;
|
|
13
|
+
/** Age at which an unrefreshed lock file is abandoned. */
|
|
14
|
+
staleAfterMs: number;
|
|
15
|
+
/** The file whose mtime records the checkout's last use, relative to the checkout. */
|
|
16
|
+
stampPath: string;
|
|
17
|
+
}
|
|
18
|
+
/** Composes the shell command that runs `removeStaleCheckouts` on the host. */
|
|
19
|
+
export declare function composeCheckoutRemovalScript(params: CheckoutRemovalParams): string;
|
|
20
|
+
/**
|
|
21
|
+
* Runs on the host and removes every checkout other than the delegation's own that has not been used for longer than
|
|
22
|
+
* `removeAfterMs` and whose lock does not have a live holder. A checkout's last use is the newest mtime of its stamp,
|
|
23
|
+
* `.git/index`, and `.git/HEAD`, so that a checkout made by an nmr that does not write the stamp still has one.
|
|
24
|
+
*
|
|
25
|
+
* Each checkout is renamed at once to a `.removing-` sibling, which needs no new inode, so that removal works on a
|
|
26
|
+
* filesystem without any free inodes and a delegation that starts later creates a fresh checkout. A detached `rm`
|
|
27
|
+
* then deletes every `.removing-` sibling, including those that an interrupted removal left, so that the delegation
|
|
28
|
+
* does not wait for it. An entry that cannot be read or moved is skipped. It is sent as source, so it refers to
|
|
29
|
+
* nothing outside its own body.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export declare function removeStaleCheckouts(params: CheckoutRemovalParams): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { quoteForShell } from "../helpers/quoteForShell.js";
|
|
2
|
+
export const CHECKOUT_STAMP_PATH = '.git/nmr-delegate-used';
|
|
3
|
+
export function composeCheckoutRemovalScript(params) {
|
|
4
|
+
const source = `(${removeStaleCheckouts.toString()})(JSON.parse(process.argv[1]));`;
|
|
5
|
+
return `node -e ${quoteForShell(source)} ${quoteForShell(JSON.stringify(params))}`;
|
|
6
|
+
}
|
|
7
|
+
export function removeStaleCheckouts(params) {
|
|
8
|
+
const fs = process.getBuiltinModule('node:fs');
|
|
9
|
+
const childProcess = process.getBuiltinModule('node:child_process');
|
|
10
|
+
const crypto = process.getBuiltinModule('node:crypto');
|
|
11
|
+
const os = process.getBuiltinModule('node:os');
|
|
12
|
+
const path = process.getBuiltinModule('node:path');
|
|
13
|
+
const removeAfterMs = params.removeAfterMs;
|
|
14
|
+
if (removeAfterMs === null)
|
|
15
|
+
return;
|
|
16
|
+
const rootDir = path.join(os.homedir(), params.checkoutsDir);
|
|
17
|
+
const REMOVING_PREFIX = '.removing-';
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
for (const key of readNames(rootDir)) {
|
|
20
|
+
if (key.startsWith('.') || key === params.ownKey)
|
|
21
|
+
continue;
|
|
22
|
+
const keyDir = path.join(rootDir, key);
|
|
23
|
+
const checkouts = readNames(keyDir);
|
|
24
|
+
if (checkouts.length !== 1)
|
|
25
|
+
continue;
|
|
26
|
+
const checkoutDir = path.join(keyDir, checkouts[0] ?? '');
|
|
27
|
+
const lastUsedMs = readLastUsedMs(checkoutDir);
|
|
28
|
+
if (lastUsedMs === undefined || now - lastUsedMs <= removeAfterMs)
|
|
29
|
+
continue;
|
|
30
|
+
if (isLockHeld(path.join(checkoutDir, params.lockPath)))
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
fs.renameSync(keyDir, path.join(rootDir, `${REMOVING_PREFIX}${crypto.randomUUID()}`));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const moved = readNames(rootDir)
|
|
39
|
+
.filter((name) => name.startsWith(REMOVING_PREFIX))
|
|
40
|
+
.map((name) => path.join(rootDir, name));
|
|
41
|
+
if (moved.length > 0) {
|
|
42
|
+
childProcess.spawn('rm', ['-rf', ...moved], { detached: true, stdio: 'ignore' }).unref();
|
|
43
|
+
}
|
|
44
|
+
function isLockHeld(lockPath) {
|
|
45
|
+
let content;
|
|
46
|
+
let mtimeMs;
|
|
47
|
+
try {
|
|
48
|
+
content = fs.readFileSync(lockPath, 'utf8');
|
|
49
|
+
({ mtimeMs } = fs.statSync(lockPath));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (Date.now() - mtimeMs > params.staleAfterMs)
|
|
55
|
+
return false;
|
|
56
|
+
const pid = readHolderPid(content);
|
|
57
|
+
return pid === undefined || !isProcessGone(pid);
|
|
58
|
+
}
|
|
59
|
+
function isProcessGone(pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ESRCH';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function readHolderPid(content) {
|
|
69
|
+
try {
|
|
70
|
+
const value = JSON.parse(content);
|
|
71
|
+
if (typeof value !== 'object' || value === null || !('pid' in value))
|
|
72
|
+
return undefined;
|
|
73
|
+
return typeof value.pid === 'number' ? value.pid : undefined;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function readLastUsedMs(checkoutDir) {
|
|
80
|
+
let newestMs;
|
|
81
|
+
for (const file of [params.stampPath, '.git/index', '.git/HEAD']) {
|
|
82
|
+
try {
|
|
83
|
+
newestMs = Math.max(newestMs ?? 0, fs.statSync(path.join(checkoutDir, file)).mtimeMs);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return newestMs;
|
|
89
|
+
}
|
|
90
|
+
function readNames(dir) {
|
|
91
|
+
try {
|
|
92
|
+
return fs.readdirSync(dir);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { HostList } from './remote-run.js';
|
|
2
2
|
/** The values that `delegate.when` accepts, each naming the runs that delegate without being asked. */
|
|
3
|
-
|
|
3
|
+
declare const DELEGATE_WHEN_VALUES: readonly ["agent", "always"];
|
|
4
4
|
export type DelegateWhen = (typeof DELEGATE_WHEN_VALUES)[number];
|
|
5
|
+
/** The age, in days, past which a delegation removes a host's unused checkouts when the user config does not set one. */
|
|
6
|
+
export declare const DEFAULT_REMOVE_CHECKOUTS_AFTER_DAYS = 7;
|
|
5
7
|
/**
|
|
6
8
|
* Renders a path for a message, with `~` standing for the home directory that `env` names when the path lies under
|
|
7
9
|
* it.
|
|
@@ -30,5 +32,8 @@ export interface UserConfig {
|
|
|
30
32
|
/** This machine's delegation hosts, and which runs delegate to them without being asked. */
|
|
31
33
|
export interface UserDelegateConfig {
|
|
32
34
|
hosts: HostList;
|
|
35
|
+
/** The age, in days, past which a delegation removes the host's unused checkouts; `false` keeps them. */
|
|
36
|
+
removeCheckoutsAfterDays?: number | false;
|
|
33
37
|
when?: DelegateWhen;
|
|
34
38
|
}
|
|
39
|
+
export {};
|