faberun 0.27.0 → 0.28.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 +9 -3
- package/package.json +1 -1
- package/skills/faberun/SKILL.md +3 -3
- package/skills/faberun/references/contract.md +9 -9
- package/skills/faberun/references/local-env.md +11 -0
- package/skills/faberun/references/operations.md +26 -33
- package/src/campaign/brief.mjs +41 -9
- package/src/campaign/campaign-brief.mjs +18 -3
- package/src/campaign/index.mjs +22 -9
- package/src/campaign/ledger.mjs +72 -4
- package/src/campaign/metrics.mjs +29 -2
- package/src/campaign/record.mjs +34 -0
- package/src/cli/campaign.mjs +35 -12
- package/src/cli/contract.mjs +11 -1
- package/src/cli/launch.mjs +12 -2
- package/src/cli/manual.mjs +24 -8
- package/src/cli/setup.mjs +5 -0
- package/src/cli/skills.mjs +18 -7
- package/src/cli/uninstall.mjs +542 -0
- package/src/cli/walkthrough.mjs +462 -0
- package/src/cli.mjs +67 -7
- package/src/contract/articles.mjs +30 -0
- package/src/contract/definition-of-done.mjs +109 -0
- package/src/contract/index.mjs +7 -4
- package/src/contract/judge-independence.mjs +18 -0
- package/src/contract/runtime.mjs +5 -1
- package/src/contract/scope-findings.mjs +120 -0
- package/src/contract/snapshot.mjs +19 -5
- package/src/contract/task-packet.mjs +49 -1
- package/src/contract/worker-result.mjs +10 -0
- package/src/engine/assignment.mjs +45 -0
- package/src/engine/detach.mjs +18 -0
- package/src/engine/gate.mjs +10 -2
- package/src/engine/judge-gate.mjs +24 -9
- package/src/engine/live-preflight.mjs +114 -18
- package/src/engine/prompts.mjs +41 -3
- package/src/engine/resume.mjs +119 -12
- package/src/engine/retry.mjs +11 -11
- package/src/engine/run-command.mjs +80 -4
- package/src/engine/run-identity.mjs +27 -14
- package/src/engine/runtime-discovery.mjs +15 -9
- package/src/engine/worker-env.mjs +179 -0
- package/src/harnesses/agy/index.mjs +11 -0
- package/src/harnesses/catalogue.mjs +29 -0
- package/src/harnesses/claude/index.mjs +98 -1
- package/src/harnesses/codex/index.mjs +15 -0
- package/src/harnesses/dsh/index.mjs +14 -0
- package/src/harnesses/exec-jsonl/index.mjs +12 -0
- package/src/harnesses/fx/index.mjs +13 -0
- package/src/harnesses/index.mjs +21 -10
- package/src/harnesses/replay/index.mjs +12 -0
- package/src/harnesses/zcode/index.mjs +19 -0
- package/src/host/config.mjs +8 -2
- package/src/host/environment-report.mjs +85 -0
- package/src/host/home.mjs +27 -0
- package/src/host/install-registry.mjs +511 -0
- package/src/host/preflight.mjs +32 -9
- package/src/host/uninstall-roots.mjs +196 -0
- package/src/plan/proof-check.mjs +15 -0
- package/src/plan/repo-facts.mjs +95 -11
- package/src/plan/rounds.mjs +27 -0
- package/src/repo/declared-paths.mjs +74 -6
- package/src/repo/integrate.mjs +1 -1
- package/src/repo/scope-closure.mjs +59 -0
- package/src/repo/signal-block.mjs +26 -0
- package/src/repo/signal.mjs +9 -24
- package/src/repo/workspace.mjs +189 -14
- package/src/report/campaign-brief.mjs +4 -0
- package/src/report/message.mjs +37 -6
- package/src/report/progress.mjs +13 -2
- package/src/run/paths.mjs +14 -1
- package/src/run/usage.mjs +66 -1
- package/src/util.mjs +18 -2
|
@@ -3,6 +3,20 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { parseVersion } from "../protocol.mjs";
|
|
4
4
|
import { normalizeRunnerTranscript, runnerEnvironmentOverlay, withSchema } from "../runner-transcript.mjs";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The environment names the dsh adapter reads to authenticate and configure the
|
|
8
|
+
* harness: the provider credential the dsh CLI resolves for the declared
|
|
9
|
+
* provider and the binary override `executable()` honours. The environment
|
|
10
|
+
* overlay's `GIT_CONFIG_*` scrub is a generic pass over ambient keys, not a
|
|
11
|
+
* declared dependency. Values never travel here.
|
|
12
|
+
*
|
|
13
|
+
* @type {readonly string[]}
|
|
14
|
+
*/
|
|
15
|
+
export const declaredEnvironment = Object.freeze([
|
|
16
|
+
"DEEPSEEK_API_KEY",
|
|
17
|
+
"FABERUN_DSH_BIN",
|
|
18
|
+
]);
|
|
19
|
+
|
|
6
20
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
7
21
|
|
|
8
22
|
/**
|
|
@@ -31,6 +31,18 @@ import { finite } from "../../util.mjs";
|
|
|
31
31
|
|
|
32
32
|
export { HARNESS_OUTPUT_LIMIT_BYTES } from "../protocol.mjs";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The environment names the exec-jsonl adapter reads to authenticate and
|
|
36
|
+
* configure the wrapper: only the binary override `executable()` honours.
|
|
37
|
+
* Credentials belong to the wrapper the contract names, so the adapter reads
|
|
38
|
+
* none of them. Values never travel here.
|
|
39
|
+
*
|
|
40
|
+
* @type {readonly string[]}
|
|
41
|
+
*/
|
|
42
|
+
export const declaredEnvironment = Object.freeze([
|
|
43
|
+
"FABERUN_EXEC_JSONL_BIN",
|
|
44
|
+
]);
|
|
45
|
+
|
|
34
46
|
export const EXEC_JSONL_PROTOCOL = Object.freeze({
|
|
35
47
|
schemaVersion: 1,
|
|
36
48
|
requestType: "run.request",
|
|
@@ -10,6 +10,19 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { parseVersion } from "../protocol.mjs";
|
|
11
11
|
import { normalizeRunnerTranscript, runnerEnvironmentOverlay, withSchema } from "../runner-transcript.mjs";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The environment names this adapter reads outside the base set and the
|
|
15
|
+
* runtime's own `*.env_key` names: the provider key the client falls back to
|
|
16
|
+
* when a runtime names none, and the binary override `executable()` honours.
|
|
17
|
+
* Values never travel here.
|
|
18
|
+
*
|
|
19
|
+
* @type {readonly string[]}
|
|
20
|
+
*/
|
|
21
|
+
export const declaredEnvironment = Object.freeze([
|
|
22
|
+
"DEEPSEEK_API_KEY",
|
|
23
|
+
"FABERUN_FX_BIN",
|
|
24
|
+
]);
|
|
25
|
+
|
|
13
26
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
27
|
|
|
15
28
|
/**
|
package/src/harnesses/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { fxHarness } from "./fx/index.mjs";
|
|
|
7
7
|
import { zcodeHarness } from "./zcode/index.mjs";
|
|
8
8
|
import { execJsonlHarness } from "./exec-jsonl/index.mjs";
|
|
9
9
|
import { replayHarness } from "./replay/index.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { passthroughEnvironment, workerEnvironment } from "../engine/worker-env.mjs";
|
|
11
11
|
import { spawnInvocation } from "../host/platform.mjs";
|
|
12
12
|
|
|
13
13
|
/** Current wire-contract version for runner protocol artifacts. */
|
|
@@ -72,7 +72,7 @@ const CAPABILITY_NAMES = new Set([
|
|
|
72
72
|
* `config["replay.recording"]` for commands, and dsh requires
|
|
73
73
|
* `config.provider` for the provider route every attempt runs on.
|
|
74
74
|
*
|
|
75
|
-
* @typedef {{id?: string, harness: string, model: string, reasoning?: string, sandbox?: string, permissionMode?: string, config?: Record<string, unknown>, printTimeout?: string, tools?: string[], executable?: string, args?: string[], versionArgs?: string[], maxArgvPromptBytes?: number, requiredCapabilities?: CapabilityRequirements, tier?: number|string, vendor?: string}} HarnessRuntime
|
|
75
|
+
* @typedef {{id?: string, harness: string, model: string, reasoning?: string, sandbox?: string, permissionMode?: string, config?: Record<string, unknown>, printTimeout?: string, tools?: string[], executable?: string, args?: string[], versionArgs?: string[], maxArgvPromptBytes?: number, requiredCapabilities?: CapabilityRequirements, envPassthrough?: string[], tier?: number|string, vendor?: string}} HarnessRuntime
|
|
76
76
|
*/
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -216,10 +216,14 @@ export function resolveVendor(runtime) {
|
|
|
216
216
|
|
|
217
217
|
/**
|
|
218
218
|
* Build one provider invocation. Prompt transport is explicit in the result:
|
|
219
|
-
* stdin adapters return `input`, while argv adapters append the prompt.
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
219
|
+
* stdin adapters return `input`, while argv adapters append the prompt.
|
|
220
|
+
*
|
|
221
|
+
* The returned `env` is the runtime-specific allowlist overlay (adapter
|
|
222
|
+
* declarations, `*.env_key` names, and the runtime's `envPassthrough`) merged
|
|
223
|
+
* over the adapter's own overlay; a null value removes the ambient variable.
|
|
224
|
+
* The spawn gate unions it with the base operating-system names, so no other
|
|
225
|
+
* controller variable reaches the child. A caller-supplied `options.env`
|
|
226
|
+
* merges over everything here, once, for every harness.
|
|
223
227
|
*
|
|
224
228
|
* @param {HarnessRuntime} runtime
|
|
225
229
|
* @param {string} prompt
|
|
@@ -229,6 +233,12 @@ export function resolveVendor(runtime) {
|
|
|
229
233
|
export function providerCommand(runtime, prompt, options = {}) {
|
|
230
234
|
const harness = getHarness(runtime.harness);
|
|
231
235
|
const command = harness.command(runtime, prompt, options);
|
|
236
|
+
// The runtime-specific allowlist travels to the gate as an overlay. The gate
|
|
237
|
+
// unions it with the base operating-system names and nothing else, so a
|
|
238
|
+
// controller variable no adapter, `*.env_key`, or envPassthrough named never
|
|
239
|
+
// reaches the child. It is deliberately not the whole environment: the
|
|
240
|
+
// caller's overlay stays an overlay, and the base set stays the gate's job.
|
|
241
|
+
command.env = { ...passthroughEnvironment(runtime, undefined, process.env), ...command.env };
|
|
232
242
|
if (options.env) command.env = { ...command.env, ...options.env };
|
|
233
243
|
if (command.promptTransport === "argv") {
|
|
234
244
|
const limit = runtime.maxArgvPromptBytes ?? harness.capabilities.maxArgvPromptBytes;
|
|
@@ -507,10 +517,11 @@ export function probeRuntime(runtime, options = {}) {
|
|
|
507
517
|
const invocation = spawnInvocation(executable, args, { cwd: options.cwd });
|
|
508
518
|
child = spawn(invocation.command, invocation.args, {
|
|
509
519
|
cwd: options.cwd,
|
|
510
|
-
// A
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
|
|
520
|
+
// A probe is a worker-class spawn: it receives only the base names,
|
|
521
|
+
// the adapter's declared names, the runtime's `*.env_key` names, and
|
|
522
|
+
// any envPassthrough the runtime carries. It never delivers a
|
|
523
|
+
// notification; the controller does.
|
|
524
|
+
env: workerEnvironment(runtime),
|
|
514
525
|
stdio: ["ignore", "pipe", "pipe"],
|
|
515
526
|
...invocation.options,
|
|
516
527
|
});
|
|
@@ -2,6 +2,18 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { parseVersion } from "../protocol.mjs";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The environment names the replay adapter reads to authenticate and configure
|
|
7
|
+
* the recording: only the binary override `executable()` honours. A recording
|
|
8
|
+
* carries no credential of its own, and `replay/bin.mjs` reads no environment
|
|
9
|
+
* name at all. Values never travel here.
|
|
10
|
+
*
|
|
11
|
+
* @type {readonly string[]}
|
|
12
|
+
*/
|
|
13
|
+
export const declaredEnvironment = Object.freeze([
|
|
14
|
+
"FABERUN_REPLAY_BIN",
|
|
15
|
+
]);
|
|
16
|
+
|
|
5
17
|
/** Envelope statuses a recording may carry. */
|
|
6
18
|
const REPLAY_STATUSES = Object.freeze(new Set([
|
|
7
19
|
"done",
|
|
@@ -3,6 +3,25 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { delimiter, join } from "node:path";
|
|
4
4
|
import { normalizeZcodeResult, parseVersion } from "../protocol.mjs";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The environment names the zcode adapter reads to authenticate and configure
|
|
8
|
+
* the CLI: the credential the CLI resolves (`auth_token.env_key`, default
|
|
9
|
+
* `ZAI_API_KEY`, with `ANTHROPIC_AUTH_TOKEN` as its fallback), the app install
|
|
10
|
+
* directory `zcodeBundle()` reads, and the binary override `executable()`
|
|
11
|
+
* honours. The `ZCODE_MODEL`, `ZCODE_BASE_URL`, `ANTHROPIC_API_KEY` and
|
|
12
|
+
* `ZCODE_LOG_*` names this adapter writes into the child environment are
|
|
13
|
+
* configuration it supplies, not dependencies it reads. Values never travel
|
|
14
|
+
* here.
|
|
15
|
+
*
|
|
16
|
+
* @type {readonly string[]}
|
|
17
|
+
*/
|
|
18
|
+
export const declaredEnvironment = Object.freeze([
|
|
19
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
20
|
+
"FABERUN_ZCODE_APP_DIR",
|
|
21
|
+
"FABERUN_ZCODE_BIN",
|
|
22
|
+
"ZAI_API_KEY",
|
|
23
|
+
]);
|
|
24
|
+
|
|
6
25
|
/** Default Z.ai Anthropic-compatible endpoint serving GLM models. */
|
|
7
26
|
const ZCODE_DEFAULT_BASE_URL = "https://api.z.ai/api/anthropic";
|
|
8
27
|
|
package/src/host/config.mjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* enabled, which runtime is the default worker and judge, the machine's own
|
|
4
4
|
* ordered `judges` list (R18) -- read only when a contract declares none --
|
|
5
5
|
* and its ordered `reviewers` list (R19), `faberun plan`'s own machine
|
|
6
|
-
* default, read only when `--reviewers` names none.
|
|
6
|
+
* default, read only when `--reviewers` names none. Its `judgeIndependence`
|
|
7
|
+
* (R36) is the machine-wide same-vendor opt-in, read only when a contract
|
|
8
|
+
* declares none.
|
|
7
9
|
*
|
|
8
10
|
* It is separate from `host/home.mjs`, which owns the path, because reading a
|
|
9
11
|
* config is a validation problem and writing it is an atomic-write problem;
|
|
@@ -13,8 +15,9 @@
|
|
|
13
15
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
14
16
|
import { dirname } from "node:path";
|
|
15
17
|
import { configPath, faberunHome } from "./home.mjs";
|
|
18
|
+
import { SAME_VENDOR_REVIEW_MODE } from "../contract/judge-independence.mjs";
|
|
16
19
|
|
|
17
|
-
/** @typedef {{schemaVersion: 1, harnesses: string[], worker?: string, judge?: string, judges?: string[], reviewers?: string[], updatedAt: string}} UserConfig */
|
|
20
|
+
/** @typedef {{schemaVersion: 1, harnesses: string[], worker?: string, judge?: string, judges?: string[], reviewers?: string[], judgeIndependence?: "same-vendor", updatedAt: string}} UserConfig */
|
|
18
21
|
|
|
19
22
|
/** Paths already reported malformed, so a process that reads twice warns once. */
|
|
20
23
|
const warned = new Set();
|
|
@@ -88,5 +91,8 @@ function isUserConfig(value) {
|
|
|
88
91
|
if (record.judge !== undefined && typeof record.judge !== "string") return false;
|
|
89
92
|
if (record.judges !== undefined && (!Array.isArray(record.judges) || !record.judges.every((id) => typeof id === "string"))) return false;
|
|
90
93
|
if (record.reviewers !== undefined && (!Array.isArray(record.reviewers) || !record.reviewers.every((id) => typeof id === "string"))) return false;
|
|
94
|
+
// The machine's same-vendor opt-in shares the contract field's single
|
|
95
|
+
// accepted value; anything else is malformed, not silently ignored.
|
|
96
|
+
if (record.judgeIndependence !== undefined && record.judgeIndependence !== SAME_VENDOR_REVIEW_MODE) return false;
|
|
91
97
|
return typeof record.updatedAt === "string";
|
|
92
98
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `doctor --env` report: which controller variables each runtime's child
|
|
3
|
+
* would carry, which it would not, and which excluded names look like
|
|
4
|
+
* credentials. Only names cross this boundary, never values. Separate from
|
|
5
|
+
* `preflight.mjs` because it is a listing built from the worker environment
|
|
6
|
+
* builder, not a readiness check, and it took that module past its 800-line
|
|
7
|
+
* ceiling (measured 2026-09-27: 840 lines).
|
|
8
|
+
*/
|
|
9
|
+
import { DISCOVERY_RUNTIME_DEFINITIONS } from "../engine/runtime-discovery.mjs";
|
|
10
|
+
import { workerEnvironment } from "../engine/worker-env.mjs";
|
|
11
|
+
|
|
12
|
+
/** @typedef {import("../contract/index.mjs").ValidatedContract} ValidatedContract */
|
|
13
|
+
/** @typedef {import("./preflight.mjs").ReachableRuntimes} ReachableRuntimes */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The name shapes that mark a controller variable as a credential. An excluded
|
|
17
|
+
* name matching one of these is reported as retained: the operator reads that
|
|
18
|
+
* the secret exists on the controller and is kept out of every worker, and
|
|
19
|
+
* never reads its value.
|
|
20
|
+
*
|
|
21
|
+
* `*_KEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD`, `AWS_*` and `GITHUB_*` are the
|
|
22
|
+
* six shapes R3 names; they are a warning about the name, not a claim about the
|
|
23
|
+
* value, so a `GITHUB_ACTIONS` is retained too.
|
|
24
|
+
*/
|
|
25
|
+
const SECRET_ENV_NAME_PATTERNS = Object.freeze([
|
|
26
|
+
/_KEY$/u,
|
|
27
|
+
/_TOKEN$/u,
|
|
28
|
+
/_SECRET$/u,
|
|
29
|
+
/_PASSWORD$/u,
|
|
30
|
+
/^AWS_/u,
|
|
31
|
+
/^GITHUB_/u,
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/** @param {string} name @returns {boolean} */
|
|
35
|
+
export function isSecretEnvName(name) {
|
|
36
|
+
return SECRET_ENV_NAME_PATTERNS.some((pattern) => pattern.test(name));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {Record<string, string|undefined>} source
|
|
41
|
+
* @returns {string[]}
|
|
42
|
+
*/
|
|
43
|
+
function presentEnvironmentNames(source) {
|
|
44
|
+
return Object.keys(source).filter((name) => source[name] !== undefined).sort((left, right) => left.localeCompare(right));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One runtime's side of the `doctor --env` report: the controller variable
|
|
49
|
+
* names it would carry and the names it would not, with every excluded
|
|
50
|
+
* credential-shaped name also listed under `retained`.
|
|
51
|
+
*
|
|
52
|
+
* Values never cross this boundary. The allowlist is computed from the same
|
|
53
|
+
* `workerEnvironment` the dispatch gate spawns with, and only its keys are
|
|
54
|
+
* read; the source is consulted for presence, never copied into the result.
|
|
55
|
+
*
|
|
56
|
+
* @param {{id?: string, harness?: string, config?: Record<string, unknown>, envPassthrough?: string[]}|null|undefined} runtime
|
|
57
|
+
* @param {{envPassthrough?: string[]}|null|undefined} [contract]
|
|
58
|
+
* @param {Record<string, string|undefined>} [source]
|
|
59
|
+
* @returns {{runtime: string, harness: string, passed: string[], excluded: string[], retained: string[]}}
|
|
60
|
+
*/
|
|
61
|
+
export function runtimeEnvironmentReport(runtime, contract, source = process.env) {
|
|
62
|
+
const allowed = new Set(Object.keys(workerEnvironment(runtime, contract, source)));
|
|
63
|
+
const present = presentEnvironmentNames(source);
|
|
64
|
+
const passed = present.filter((name) => allowed.has(name));
|
|
65
|
+
const excluded = present.filter((name) => !allowed.has(name));
|
|
66
|
+
const retained = excluded.filter((name) => isSecretEnvName(name));
|
|
67
|
+
return { runtime: runtime?.id ?? "", harness: runtime?.harness ?? "", passed, excluded, retained };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The per-runtime listings behind `doctor --env`. A contract narrows the report
|
|
72
|
+
* to the reachable runtimes the run committed to; without one the report covers
|
|
73
|
+
* the discovery catalogue, because there is no routed runtime to name.
|
|
74
|
+
*
|
|
75
|
+
* @param {ReachableRuntimes} [runtimes]
|
|
76
|
+
* @param {ValidatedContract} [contract]
|
|
77
|
+
* @param {Record<string, string|undefined>} [source]
|
|
78
|
+
* @returns {{runtime: string, harness: string, passed: string[], excluded: string[], retained: string[]}[]}
|
|
79
|
+
*/
|
|
80
|
+
export function environmentListings(runtimes, contract, source = process.env) {
|
|
81
|
+
const entries = runtimes && runtimes.size
|
|
82
|
+
? [...runtimes.entries()].map(([id, { runtime }]) => ({ ...runtime, id }))
|
|
83
|
+
: Object.entries(DISCOVERY_RUNTIME_DEFINITIONS).map(([id, runtime]) => ({ ...runtime, id }));
|
|
84
|
+
return entries.map((runtime) => runtimeEnvironmentReport(runtime, contract, source));
|
|
85
|
+
}
|
package/src/host/home.mjs
CHANGED
|
@@ -27,6 +27,33 @@ export function faberunHome(env = process.env) {
|
|
|
27
27
|
return join(homedir(), ".faberun");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The home directory the *injected* environment names, or null when it names
|
|
32
|
+
* none. Unlike `faberunHome`, this never consults `os.homedir()`.
|
|
33
|
+
*
|
|
34
|
+
* The distinction matters for exactly one caller: `faberun uninstall` derives
|
|
35
|
+
* the roots it may delete from the environment it was handed, and
|
|
36
|
+
* `src/host/uninstall-roots.mjs` refuses every removal when that environment
|
|
37
|
+
* names neither root. A fallback to the operator's real home would aim a delete
|
|
38
|
+
* at it when the caller meant to scope the operation to a throwaway one, which
|
|
39
|
+
* is the accident the isolation requirement exists to stop. `HOME` is the POSIX
|
|
40
|
+
* name and `USERPROFILE` the Windows one; `HOMEDRIVE`+`HOMEPATH` is the last
|
|
41
|
+
* spelling `os.homedir()` itself accepts there.
|
|
42
|
+
*
|
|
43
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
44
|
+
* @returns {string|null}
|
|
45
|
+
*/
|
|
46
|
+
export function effectiveHome(env = process.env) {
|
|
47
|
+
for (const name of ["HOME", "USERPROFILE"]) {
|
|
48
|
+
const value = env[name];
|
|
49
|
+
if (typeof value === "string" && value) return value;
|
|
50
|
+
}
|
|
51
|
+
const drive = env.HOMEDRIVE;
|
|
52
|
+
const path = env.HOMEPATH;
|
|
53
|
+
if (typeof drive === "string" && drive && typeof path === "string" && path) return `${drive}${path}`;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
/** @param {string} home @returns {string} */
|
|
31
58
|
export function versionsDir(home) {
|
|
32
59
|
return join(home, "versions");
|