@deftai/directive-core 0.99.0 → 0.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authz/actions.d.ts +6 -0
- package/dist/authz/actions.js +3 -0
- package/dist/authz/classify.js +241 -30
- package/dist/authz/decompose-apply.d.ts +81 -0
- package/dist/authz/decompose-apply.js +377 -0
- package/dist/authz/index.d.ts +3 -1
- package/dist/authz/index.js +3 -1
- package/dist/authz/store.d.ts +53 -0
- package/dist/authz/store.js +219 -1
- package/dist/authz/types.d.ts +19 -2
- package/dist/authz/types.js +5 -1
- package/dist/check/cached-orchestrator.d.ts +26 -5
- package/dist/check/cached-orchestrator.js +228 -6
- package/dist/check/gate-lists.d.ts +10 -4
- package/dist/check/gate-lists.js +19 -5
- package/dist/check/index.d.ts +3 -2
- package/dist/check/index.js +2 -1
- package/dist/check/named-cause.d.ts +45 -0
- package/dist/check/named-cause.js +121 -0
- package/dist/check/orchestrator.js +6 -1
- package/dist/doctor/checks.d.ts +44 -0
- package/dist/doctor/checks.js +428 -2
- package/dist/doctor/help.d.ts +5 -0
- package/dist/doctor/help.js +31 -0
- package/dist/doctor/index.d.ts +2 -0
- package/dist/doctor/index.js +2 -0
- package/dist/doctor/main.d.ts +6 -7
- package/dist/doctor/main.js +130 -79
- package/dist/doctor/session-coda.d.ts +82 -0
- package/dist/doctor/session-coda.js +151 -0
- package/dist/doctor/types.d.ts +16 -0
- package/dist/doctor/which.d.ts +20 -1
- package/dist/doctor/which.js +67 -1
- package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
- package/dist/finish-loop/pr-finish-loop.js +116 -5
- package/dist/finish-loop/types.d.ts +1 -1
- package/dist/hooks/dispatcher.d.ts +9 -5
- package/dist/hooks/dispatcher.js +23 -15
- package/dist/hooks/readonly.d.ts +6 -3
- package/dist/hooks/readonly.js +44 -25
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/init-deposit/agent-hooks.d.ts +20 -0
- package/dist/init-deposit/agent-hooks.js +51 -31
- package/dist/init-deposit/gitignore.js +2 -0
- package/dist/intake/issue-ingest.js +36 -0
- package/dist/lifecycle/completed-consistency.d.ts +60 -0
- package/dist/lifecycle/completed-consistency.js +443 -0
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/literal-acceptance/capture.d.ts +52 -0
- package/dist/literal-acceptance/capture.js +658 -0
- package/dist/literal-acceptance/evaluate.d.ts +47 -0
- package/dist/literal-acceptance/evaluate.js +193 -0
- package/dist/literal-acceptance/index.d.ts +12 -0
- package/dist/literal-acceptance/index.js +12 -0
- package/dist/literal-acceptance/run.d.ts +38 -0
- package/dist/literal-acceptance/run.js +216 -0
- package/dist/literal-acceptance/safety.d.ts +24 -0
- package/dist/literal-acceptance/safety.js +251 -0
- package/dist/literal-acceptance/types.d.ts +82 -0
- package/dist/literal-acceptance/types.js +26 -0
- package/dist/policy/ac-pass-banking.d.ts +58 -0
- package/dist/policy/ac-pass-banking.js +150 -0
- package/dist/policy/ceremony-dial.d.ts +96 -11
- package/dist/policy/ceremony-dial.js +250 -23
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +16 -1
- package/dist/policy/merge-approval-head.d.ts +129 -0
- package/dist/policy/merge-approval-head.js +461 -0
- package/dist/policy/require-human-merge.d.ts +5 -0
- package/dist/policy/require-human-merge.js +5 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
- package/dist/pr-merge-readiness/ci-gate.js +65 -5
- package/dist/pr-merge-readiness/compute.d.ts +13 -0
- package/dist/pr-merge-readiness/compute.js +99 -14
- package/dist/pr-merge-readiness/gh.d.ts +49 -0
- package/dist/pr-merge-readiness/gh.js +268 -0
- package/dist/pr-merge-readiness/index.d.ts +2 -2
- package/dist/pr-merge-readiness/index.js +2 -2
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
- package/dist/pr-wait-mergeable/cascade.js +115 -1
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +4 -0
- package/dist/pr-wait-mergeable/types.d.ts +3 -1
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
- package/dist/pr-wait-mergeable/wrappers.js +7 -1
- package/dist/preflight/evaluate.d.ts +15 -1
- package/dist/preflight/evaluate.js +33 -2
- package/dist/preflight-cache/evaluate.d.ts +3 -0
- package/dist/preflight-cache/evaluate.js +3 -0
- package/dist/product-first-done-gate/acceptance.d.ts +25 -0
- package/dist/product-first-done-gate/acceptance.js +247 -0
- package/dist/product-first-done-gate/check-mode.d.ts +45 -0
- package/dist/product-first-done-gate/check-mode.js +148 -0
- package/dist/product-first-done-gate/evaluate.d.ts +50 -0
- package/dist/product-first-done-gate/evaluate.js +255 -0
- package/dist/product-first-done-gate/index.d.ts +11 -0
- package/dist/product-first-done-gate/index.js +11 -0
- package/dist/product-first-done-gate/types.d.ts +64 -0
- package/dist/product-first-done-gate/types.js +61 -0
- package/dist/render/framework-commands.js +4 -0
- package/dist/run-summary/emit.d.ts +56 -0
- package/dist/run-summary/emit.js +171 -0
- package/dist/run-summary/index.d.ts +4 -0
- package/dist/run-summary/index.js +4 -0
- package/dist/run-summary/path.d.ts +25 -0
- package/dist/run-summary/path.js +78 -0
- package/dist/run-summary/types.d.ts +76 -0
- package/dist/run-summary/types.js +21 -0
- package/dist/scope/acceptance-evidence.d.ts +110 -0
- package/dist/scope/acceptance-evidence.js +422 -0
- package/dist/scope/coverage-map.d.ts +98 -0
- package/dist/scope/coverage-map.js +558 -0
- package/dist/scope/decompose.d.ts +5 -0
- package/dist/scope/decompose.js +134 -11
- package/dist/scope/index.d.ts +3 -0
- package/dist/scope/index.js +3 -0
- package/dist/scope/parent-lineage.d.ts +125 -0
- package/dist/scope/parent-lineage.js +626 -0
- package/dist/scope/transition.d.ts +8 -0
- package/dist/scope/transition.js +61 -2
- package/dist/session/ac-pass-banking.d.ts +221 -0
- package/dist/session/ac-pass-banking.js +761 -0
- package/dist/session/active-cli.d.ts +79 -0
- package/dist/session/active-cli.js +382 -0
- package/dist/session/deposit-sha.d.ts +49 -0
- package/dist/session/deposit-sha.js +121 -0
- package/dist/session/effort-budget.d.ts +136 -0
- package/dist/session/effort-budget.js +352 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +7 -0
- package/dist/session/orientation-compression.d.ts +127 -0
- package/dist/session/orientation-compression.js +425 -0
- package/dist/session/orientation-state.d.ts +29 -0
- package/dist/session/orientation-state.js +85 -0
- package/dist/session/session-start.d.ts +31 -0
- package/dist/session/session-start.js +228 -2
- package/dist/session/toolchain-preflight.d.ts +64 -0
- package/dist/session/toolchain-preflight.js +151 -0
- package/dist/session/verify-session-ritual.d.ts +14 -0
- package/dist/session/verify-session-ritual.js +33 -0
- package/dist/story-ready/evaluate.d.ts +11 -0
- package/dist/story-ready/evaluate.js +41 -3
- package/dist/triage/actions/candidates-log.d.ts +1 -2
- package/dist/triage/actions/candidates-log.js +37 -6
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +8 -2
- package/dist/triage/bootstrap/index.js +15 -3
- package/dist/triage/classify/index.d.ts +1 -0
- package/dist/triage/classify/index.js +2 -0
- package/dist/triage/classify/label-mirror.js +16 -0
- package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
- package/dist/triage/classify/mirror-discovery-tip.js +234 -0
- package/dist/triage/reconcile/reconcile.js +28 -6
- package/dist/triage/summary/index.js +12 -4
- package/dist/triage/welcome/default-mode.js +16 -0
- package/dist/ts-check-lane/run-lane.js +21 -4
- package/dist/vbrief-validate/conformance.js +7 -0
- package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
- package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
- package/dist/xbrief-migrate/migrate-project.js +92 -3
- package/package.json +15 -3
package/dist/doctor/main.js
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { VBRIEF_VERSION } from "@deftai/directive-types";
|
|
4
3
|
import { evaluate as evaluateAgentsMdAdvisory } from "../agents-md-advisory/evaluate.js";
|
|
5
4
|
import { evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, } from "../check/consumer-gate-integrity.js";
|
|
6
5
|
import { contentRoot } from "../content-root.js";
|
|
7
|
-
import { resolveProjectDefinitionPath } from "../layout/resolve.js";
|
|
8
6
|
import { DEFT_DIRECTIVE_DISABLE_FLAG_NAME, DEFT_DIRECTIVE_DISABLE_STATUS, DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING, detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
|
|
9
7
|
import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
|
|
10
8
|
import { describeShadowedPlanExtension, detectShadowedPlanExtensions, } from "../policy/plan-extensions.js";
|
|
11
9
|
import { loadProjectDefinition } from "../policy/resolve.js";
|
|
12
10
|
import { checkLocalEngineIntegrity, classify, detectPackageManager, evaluateSkew, reconcileVersions, plan as resolvePlan, } from "../resolution/index.js";
|
|
13
|
-
import { classifyXbriefSchemaDistance } from "../staleness-tickler/probe-xbrief.js";
|
|
14
11
|
import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
|
|
15
12
|
import { evaluateAgentHooks } from "../verify-env/agent-hooks.js";
|
|
16
13
|
import { probeAgentHooksLive } from "../verify-env/agent-hooks-live-probe.js";
|
|
17
|
-
import { readDeclaredArtifactVersion } from "../xbrief-migrate/transforms.js";
|
|
18
14
|
import { agentsRefreshPlan, hasV3ManagedMarker } from "./agents-md.js";
|
|
19
|
-
import { runChecks } from "./checks.js";
|
|
15
|
+
import { checkXbriefEnvelopeMajorVersion, runChecks, XBRIEF_ENVELOPE_MAJOR_CHECK, XBRIEF_ENVELOPE_MIGRATE_COMMAND, } from "./checks.js";
|
|
20
16
|
import { CONSUMER_FRAMEWORK_DIRS, EXPECTED_CONTENT_DIRS, EXPECTED_FRAMEWORK_DIRS, NETWORK_DISCLOSURE_LINE, PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE, TASKFILE_INCLUDE_SNIPPET, UV_INSTALL_URL, } from "./constants.js";
|
|
21
17
|
import { decideThrottle, formatIsoZ, readState, renderDoctorStatusLine, writeState, } from "./doctor-state.js";
|
|
22
18
|
import { formatAllowedFlagsHint, formatUnknownFlagsError, parseDoctorFlags } from "./flags.js";
|
|
19
|
+
import { formatDoctorHelp } from "./help.js";
|
|
23
20
|
import { pythonJsonDump } from "./json.js";
|
|
24
21
|
import { parseInstallRootFromAgentsMd } from "./manifest.js";
|
|
25
22
|
import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
|
|
@@ -29,12 +26,11 @@ import { runOpenClawSoftRebindCheck } from "./openclaw-soft-rebind.js";
|
|
|
29
26
|
import { createPlainSink } from "./output.js";
|
|
30
27
|
import { readTextSafe, resolveFrameworkRootForProject, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
|
|
31
28
|
import { runPayloadStalenessCheck } from "./payload-staleness.js";
|
|
29
|
+
import { loadSessionCodas, projectRootRealpath, resolveSessionCodaLine, SESSION_CODA_ENV, sessionCodaMode, shouldEmitSessionCodaLine, utcDateYmd, } from "./session-coda.js";
|
|
32
30
|
import { runLocalSignpostChecks } from "./signpost-checks.js";
|
|
33
31
|
import { classifyTaskfileInclude, formatGatesSurfaceDualRemediation, formatMissingIncludeSnippet, GATES_SURFACE_DEFT_REMEDIATION, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
|
|
34
32
|
import { defaultWhich } from "./which.js";
|
|
35
33
|
const DEFAULT_RESOLUTION_PLATFORMS = ["linux", "darwin", "win32"];
|
|
36
|
-
/** Next-action command for project-envelope behind-major (#2971). */
|
|
37
|
-
const XBRIEF_ENVELOPE_MIGRATE_COMMAND = "deft migrate:xbrief";
|
|
38
34
|
/**
|
|
39
35
|
* Read the `packageManager` field (Corepack) from a project package.json, or
|
|
40
36
|
* null when absent/unreadable. Lets the doctor detect a pnpm project that has
|
|
@@ -64,6 +60,10 @@ export function cmdDoctor(args, seams = {}) {
|
|
|
64
60
|
sink.info(formatAllowedFlagsHint());
|
|
65
61
|
return 2;
|
|
66
62
|
}
|
|
63
|
+
if (flags.help) {
|
|
64
|
+
process.stdout.write(formatDoctorHelp());
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
67
|
const sessionMode = flags.session;
|
|
68
68
|
const fixMode = flags.fix && !sessionMode;
|
|
69
69
|
const jsonMode = flags.json;
|
|
@@ -495,6 +495,15 @@ export function cmdDoctor(args, seams = {}) {
|
|
|
495
495
|
sink.blank();
|
|
496
496
|
if (errorCount === 0 && warningCount === 0) {
|
|
497
497
|
sink.finalSuccess("System check passed!");
|
|
498
|
+
emitSessionCodaAfterFinalSuccess({
|
|
499
|
+
projectRoot,
|
|
500
|
+
frameworkRoot,
|
|
501
|
+
jsonMode,
|
|
502
|
+
exitOk: true,
|
|
503
|
+
now: nowFn(),
|
|
504
|
+
seams,
|
|
505
|
+
sink,
|
|
506
|
+
});
|
|
498
507
|
return 0;
|
|
499
508
|
}
|
|
500
509
|
if (errorCount) {
|
|
@@ -504,8 +513,51 @@ export function cmdDoctor(args, seams = {}) {
|
|
|
504
513
|
return 1;
|
|
505
514
|
}
|
|
506
515
|
sink.finalWarn(`System check completed with ${warningCount} warning(s).`);
|
|
516
|
+
emitSessionCodaAfterFinalSuccess({
|
|
517
|
+
projectRoot,
|
|
518
|
+
frameworkRoot,
|
|
519
|
+
jsonMode,
|
|
520
|
+
exitOk: true,
|
|
521
|
+
now: nowFn(),
|
|
522
|
+
seams,
|
|
523
|
+
sink,
|
|
524
|
+
});
|
|
507
525
|
return 0;
|
|
508
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* After the final human success footer only (D2): optional off-hint or ✦ coda.
|
|
529
|
+
* Never mid-run Next command: block; never JSON; never hard-fail path (D1).
|
|
530
|
+
*/
|
|
531
|
+
function emitSessionCodaAfterFinalSuccess(input) {
|
|
532
|
+
const stdoutIsTty = input.seams.stdoutIsTty ?? (() => process.stdout.isTTY === true);
|
|
533
|
+
// `in` so tests can force unset via `{ sessionCodaEnv: undefined }` without
|
|
534
|
+
// leaking the host process env.
|
|
535
|
+
const envRaw = "sessionCodaEnv" in input.seams ? input.seams.sessionCodaEnv : process.env[SESSION_CODA_ENV];
|
|
536
|
+
const ciRaw = "ciEnv" in input.seams ? input.seams.ciEnv : process.env.CI;
|
|
537
|
+
const ci = Boolean(ciRaw && ciRaw !== "0" && String(ciRaw).toLowerCase() !== "false");
|
|
538
|
+
const shouldEmit = shouldEmitSessionCodaLine({
|
|
539
|
+
tty: stdoutIsTty(),
|
|
540
|
+
ci,
|
|
541
|
+
json: input.jsonMode,
|
|
542
|
+
exitOk: input.exitOk,
|
|
543
|
+
});
|
|
544
|
+
const mode = sessionCodaMode(envRaw);
|
|
545
|
+
const codas = mode === "on"
|
|
546
|
+
? (input.seams.loadSessionCodas?.(input.frameworkRoot) ??
|
|
547
|
+
loadSessionCodas(input.frameworkRoot, input.seams.readText))
|
|
548
|
+
: [];
|
|
549
|
+
const line = resolveSessionCodaLine({
|
|
550
|
+
mode,
|
|
551
|
+
shouldEmit,
|
|
552
|
+
date: utcDateYmd(input.now),
|
|
553
|
+
projectRoot: projectRootRealpath(input.projectRoot),
|
|
554
|
+
codas,
|
|
555
|
+
});
|
|
556
|
+
if (line === null)
|
|
557
|
+
return;
|
|
558
|
+
input.sink.blank();
|
|
559
|
+
input.sink.raw(line);
|
|
560
|
+
}
|
|
509
561
|
export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams) {
|
|
510
562
|
const checkName = "agent-hooks-registration";
|
|
511
563
|
if (!consumerContext) {
|
|
@@ -653,6 +705,11 @@ function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
|
|
|
653
705
|
const name = String(entry.name ?? "install-integrity");
|
|
654
706
|
const status = String(entry.status ?? "");
|
|
655
707
|
const detail = String(entry.detail ?? "");
|
|
708
|
+
// Authoritative envelope-major emission is runXbriefEnvelopeVersionCheck
|
|
709
|
+
// (runs for framework repo + consumers). Skip install-integrity duplicate.
|
|
710
|
+
if (name === XBRIEF_ENVELOPE_MAJOR_CHECK) {
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
656
713
|
if (status === "pass") {
|
|
657
714
|
sink.success(`${name}: pass`);
|
|
658
715
|
continue;
|
|
@@ -954,105 +1011,99 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
|
|
|
954
1011
|
});
|
|
955
1012
|
}
|
|
956
1013
|
/**
|
|
957
|
-
* Fail closed when
|
|
958
|
-
*
|
|
959
|
-
*
|
|
960
|
-
* (
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
* must NOT route to migrate:xbrief when only framework schema files are stale.
|
|
1014
|
+
* Fail closed when any scanned project xbrief lifecycle `*.xbrief.json` envelope
|
|
1015
|
+
* declares a schema major behind the framework target (#2971 / #3243). Greenfield
|
|
1016
|
+
* (no envelopes) skips. Behind-major records `deft migrate:xbrief` as the next
|
|
1017
|
+
* action (hybrid in-place rewrite on already-xbrief trees — #3236). Distinct from
|
|
1018
|
+
* deposited-schema (`stale-xbrief-schema-deposit`) which must NOT route to
|
|
1019
|
+
* migrate:xbrief when only framework schema files are stale.
|
|
964
1020
|
*/
|
|
965
1021
|
export function runXbriefEnvelopeVersionCheck(projectRoot, sink, addFinding, seams) {
|
|
966
|
-
const checkName =
|
|
967
|
-
const isFile = seams.isFile ?? ((p) => existsSync(p));
|
|
968
|
-
const readText = seams.readText ?? readTextSafe;
|
|
969
|
-
const targetVersion = VBRIEF_VERSION;
|
|
1022
|
+
const checkName = XBRIEF_ENVELOPE_MAJOR_CHECK;
|
|
970
1023
|
if (seams.probeXbriefEnvelope) {
|
|
971
1024
|
const probe = seams.probeXbriefEnvelope(projectRoot);
|
|
972
1025
|
emitXbriefEnvelopeFinding(checkName, probe, sink, addFinding);
|
|
973
1026
|
return;
|
|
974
1027
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
}
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
if (!isFile(definitionPath)) {
|
|
993
|
-
const skipMessage = `${checkName}: skip -- no PROJECT-DEFINITION yet (greenfield)`;
|
|
994
|
-
sink.info(skipMessage);
|
|
995
|
-
addFinding({
|
|
996
|
-
severity: "skip",
|
|
997
|
-
message: skipMessage,
|
|
998
|
-
check: checkName,
|
|
999
|
-
status: "skip",
|
|
1000
|
-
reason: "no-project-definition",
|
|
1001
|
-
});
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
const text = readText(definitionPath);
|
|
1005
|
-
if (text === null) {
|
|
1006
|
-
// Unreadable path (permissions or injectable seams) is not proof of 0.6.
|
|
1007
|
-
const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION unreadable`;
|
|
1028
|
+
const result = checkXbriefEnvelopeMajorVersion(projectRoot, {
|
|
1029
|
+
...(seams.readText ? { readText: seams.readText } : {}),
|
|
1030
|
+
...(seams.isFile ? { isFile: seams.isFile } : {}),
|
|
1031
|
+
...(seams.isDir ? { isDir: seams.isDir } : {}),
|
|
1032
|
+
});
|
|
1033
|
+
const data = (result.data ?? {});
|
|
1034
|
+
const targetVersion = typeof data.target_version === "string"
|
|
1035
|
+
? data.target_version
|
|
1036
|
+
: String(data.target_version ?? "");
|
|
1037
|
+
const declaredFromData = data.declared_versions;
|
|
1038
|
+
const declaredVersion = Array.isArray(declaredFromData)
|
|
1039
|
+
? (declaredFromData.find((v) => typeof v === "string") ?? null)
|
|
1040
|
+
: null;
|
|
1041
|
+
if (result.status === "skip") {
|
|
1042
|
+
const skipMessage = `${checkName}: skip -- ${result.detail}`;
|
|
1008
1043
|
sink.info(skipMessage);
|
|
1009
1044
|
addFinding({
|
|
1010
1045
|
severity: "skip",
|
|
1011
1046
|
message: skipMessage,
|
|
1012
1047
|
check: checkName,
|
|
1013
1048
|
status: "skip",
|
|
1014
|
-
reason: "
|
|
1049
|
+
reason: typeof data.reason === "string" ? data.reason : "skip",
|
|
1050
|
+
target_version: targetVersion || undefined,
|
|
1015
1051
|
});
|
|
1016
1052
|
return;
|
|
1017
1053
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION is not a JSON object`;
|
|
1026
|
-
sink.info(skipMessage);
|
|
1054
|
+
if (result.status === "pass") {
|
|
1055
|
+
const status = typeof data.status === "string" ? data.status : "current";
|
|
1056
|
+
if (status === "behind-minor") {
|
|
1057
|
+
// Major-only check (#3243) does not fail closed, but surface an advisory
|
|
1058
|
+
// when the shared scan observed behind-minor distance.
|
|
1059
|
+
const warnMessage = `${checkName}: behind-minor -- ${result.detail}. Prefer \`${XBRIEF_ENVELOPE_MIGRATE_COMMAND}\` when convenient.`;
|
|
1060
|
+
sink.warn(warnMessage);
|
|
1027
1061
|
addFinding({
|
|
1028
|
-
severity: "
|
|
1029
|
-
message:
|
|
1062
|
+
severity: "warning",
|
|
1063
|
+
message: warnMessage,
|
|
1030
1064
|
check: checkName,
|
|
1031
|
-
status: "
|
|
1032
|
-
|
|
1065
|
+
status: "behind-minor",
|
|
1066
|
+
suggestion: XBRIEF_ENVELOPE_MIGRATE_COMMAND,
|
|
1067
|
+
target_version: targetVersion || undefined,
|
|
1068
|
+
next_command: XBRIEF_ENVELOPE_MIGRATE_COMMAND,
|
|
1033
1069
|
});
|
|
1034
1070
|
return;
|
|
1035
1071
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION JSON parse failed`;
|
|
1039
|
-
sink.info(skipMessage);
|
|
1072
|
+
const okMessage = `${checkName}: ${result.detail}`;
|
|
1073
|
+
sink.success(okMessage);
|
|
1040
1074
|
addFinding({
|
|
1041
1075
|
severity: "skip",
|
|
1042
|
-
message:
|
|
1076
|
+
message: okMessage,
|
|
1043
1077
|
check: checkName,
|
|
1044
|
-
status: "
|
|
1045
|
-
|
|
1078
|
+
status: "current",
|
|
1079
|
+
target_version: targetVersion || undefined,
|
|
1046
1080
|
});
|
|
1047
1081
|
return;
|
|
1048
1082
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1083
|
+
// fail / error → behind-major fail closed (migratable 0.6 or non-migratable)
|
|
1084
|
+
const status = typeof data.status === "string" ? data.status : "behind-major";
|
|
1085
|
+
const nextCommand = typeof data.next_command === "string"
|
|
1086
|
+
? data.next_command
|
|
1087
|
+
: data.next_command === null
|
|
1088
|
+
? null
|
|
1089
|
+
: XBRIEF_ENVELOPE_MIGRATE_COMMAND;
|
|
1090
|
+
const suggestion = typeof data.suggestion === "string"
|
|
1091
|
+
? data.suggestion
|
|
1092
|
+
: (nextCommand ?? `set xBRIEFInfo.version to framework target`);
|
|
1093
|
+
const message = `${checkName}: ${result.detail}`;
|
|
1094
|
+
sink.error(message);
|
|
1095
|
+
addFinding({
|
|
1096
|
+
severity: "error",
|
|
1097
|
+
message,
|
|
1098
|
+
check: checkName,
|
|
1099
|
+
status,
|
|
1100
|
+
suggestion,
|
|
1101
|
+
declared_version: declaredVersion,
|
|
1102
|
+
target_version: targetVersion || undefined,
|
|
1103
|
+
next_command: nextCommand,
|
|
1104
|
+
sample_paths: data.sample_paths,
|
|
1105
|
+
behind_major_count: data.behind_major_count,
|
|
1106
|
+
});
|
|
1056
1107
|
}
|
|
1057
1108
|
function emitXbriefEnvelopeFinding(checkName, probe, sink, addFinding) {
|
|
1058
1109
|
if (probe.distance === "current") {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional deterministic session coda for interactive doctor success (#2712).
|
|
3
|
+
*
|
|
4
|
+
* Human TTY affordance only: never JSON, never AGENTS/skills, never exit-code
|
|
5
|
+
* changes. Env three-state DEFT_SESSION_CODA (unset / 1 / 0). Copy lives in
|
|
6
|
+
* content/doctor/session-coda.json — not embedded as an engine literal.
|
|
7
|
+
*/
|
|
8
|
+
/** Env var name (env-only v1; no USER.md key). */
|
|
9
|
+
export declare const SESSION_CODA_ENV = "DEFT_SESSION_CODA";
|
|
10
|
+
/**
|
|
11
|
+
* Fixed discoverability line when env is unset and gates pass (D3).
|
|
12
|
+
* Snapshot-stable; not selected from the pack; no ✦ prefix.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SESSION_CODA_OFF_HINT = "Session coda: off (set DEFT_SESSION_CODA=1 to enable)";
|
|
15
|
+
/** U+2726 FOUR RAYED STAR + space — stable; do not localize. */
|
|
16
|
+
export declare const SESSION_CODA_STAR_PREFIX = "\u2726 ";
|
|
17
|
+
/** Max body length; longer pack entries are skipped (fail-open). */
|
|
18
|
+
export declare const SESSION_CODA_MAX_CHARS = 100;
|
|
19
|
+
/** Relative path under contentRoot (source: content/doctor/…; deposit: doctor/…). */
|
|
20
|
+
export declare const SESSION_CODA_REL_PATH: string;
|
|
21
|
+
export type SessionCodaMode = "unset" | "on" | "off";
|
|
22
|
+
/**
|
|
23
|
+
* Map DEFT_SESSION_CODA to the three-state mode (D3).
|
|
24
|
+
* - undefined / "" → unset (discoverability off-hint)
|
|
25
|
+
* - "1" → on (✦ coda)
|
|
26
|
+
* - "0" → off (silent)
|
|
27
|
+
* - any other non-empty value → unset (show correct enable form)
|
|
28
|
+
*/
|
|
29
|
+
export declare function sessionCodaMode(envValue: string | undefined): SessionCodaMode;
|
|
30
|
+
/**
|
|
31
|
+
* Whether any extra line (off-hint or coda) is allowed (D1 + surface gates).
|
|
32
|
+
* TTY = stdout.isTTY; CI truthy blocks; json blocks; exitOk = errorCount === 0.
|
|
33
|
+
*/
|
|
34
|
+
export declare function shouldEmitSessionCodaLine(input: {
|
|
35
|
+
readonly tty: boolean;
|
|
36
|
+
readonly ci: boolean;
|
|
37
|
+
readonly json: boolean;
|
|
38
|
+
readonly exitOk: boolean;
|
|
39
|
+
}): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* FNV-1a 32-bit (#2712). Documented choice for hash_u32(date + "\\0" + root).
|
|
42
|
+
* No Math.random; same inputs ⇒ same u32 on every platform.
|
|
43
|
+
*/
|
|
44
|
+
export declare function hashU32(input: string): number;
|
|
45
|
+
/** UTC calendar date yyyy-mm-dd from a Date (stable for 24h across a team day). */
|
|
46
|
+
export declare function utcDateYmd(date: Date): string;
|
|
47
|
+
/** Best-effort realpath; falls back to the given path when realpath fails. */
|
|
48
|
+
export declare function projectRootRealpath(projectRoot: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Select one coda body from the pack (enabled path only).
|
|
51
|
+
* index = hash_u32(utc_date + "\\0" + project_root_realpath) % len(codas)
|
|
52
|
+
* Empty list → null (omit ✦; doctor still 0).
|
|
53
|
+
*/
|
|
54
|
+
export declare function selectSessionCoda(input: {
|
|
55
|
+
readonly date: string;
|
|
56
|
+
readonly projectRoot: string;
|
|
57
|
+
readonly codas: readonly string[];
|
|
58
|
+
}): string | null;
|
|
59
|
+
/** Skip non-strings and bodies over SESSION_CODA_MAX_CHARS. */
|
|
60
|
+
export declare function filterCodaEntries(raw: unknown): string[];
|
|
61
|
+
/**
|
|
62
|
+
* Load + filter coda lines from content. Fail-open: missing/unreadable/invalid
|
|
63
|
+
* / empty after filter → [] (omit ✦; no error noise).
|
|
64
|
+
*/
|
|
65
|
+
export declare function loadSessionCodas(frameworkRoot: string, readText?: (path: string) => string | null): string[];
|
|
66
|
+
/** Format enabled coda line: ✦ + space + body. */
|
|
67
|
+
export declare function formatSessionCodaEnabledLine(body: string): string;
|
|
68
|
+
export interface ResolveSessionCodaLineInput {
|
|
69
|
+
readonly mode: SessionCodaMode;
|
|
70
|
+
readonly shouldEmit: boolean;
|
|
71
|
+
readonly date: string;
|
|
72
|
+
readonly projectRoot: string;
|
|
73
|
+
readonly codas: readonly string[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the single extra line (or null) after final success footer.
|
|
77
|
+
* Does not print; caller places after blank line post footer (D2).
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolveSessionCodaLine(input: ResolveSessionCodaLineInput): string | null;
|
|
80
|
+
/** Help / env-docs fragment for DEFT_SESSION_CODA three-state (#2712). */
|
|
81
|
+
export declare function sessionCodaHelpText(): string;
|
|
82
|
+
//# sourceMappingURL=session-coda.d.ts.map
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional deterministic session coda for interactive doctor success (#2712).
|
|
3
|
+
*
|
|
4
|
+
* Human TTY affordance only: never JSON, never AGENTS/skills, never exit-code
|
|
5
|
+
* changes. Env three-state DEFT_SESSION_CODA (unset / 1 / 0). Copy lives in
|
|
6
|
+
* content/doctor/session-coda.json — not embedded as an engine literal.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { contentRoot } from "../content-root.js";
|
|
11
|
+
/** Env var name (env-only v1; no USER.md key). */
|
|
12
|
+
export const SESSION_CODA_ENV = "DEFT_SESSION_CODA";
|
|
13
|
+
/**
|
|
14
|
+
* Fixed discoverability line when env is unset and gates pass (D3).
|
|
15
|
+
* Snapshot-stable; not selected from the pack; no ✦ prefix.
|
|
16
|
+
*/
|
|
17
|
+
export const SESSION_CODA_OFF_HINT = "Session coda: off (set DEFT_SESSION_CODA=1 to enable)";
|
|
18
|
+
/** U+2726 FOUR RAYED STAR + space — stable; do not localize. */
|
|
19
|
+
export const SESSION_CODA_STAR_PREFIX = "\u2726 ";
|
|
20
|
+
/** Max body length; longer pack entries are skipped (fail-open). */
|
|
21
|
+
export const SESSION_CODA_MAX_CHARS = 100;
|
|
22
|
+
/** Relative path under contentRoot (source: content/doctor/…; deposit: doctor/…). */
|
|
23
|
+
export const SESSION_CODA_REL_PATH = join("doctor", "session-coda.json");
|
|
24
|
+
/**
|
|
25
|
+
* Map DEFT_SESSION_CODA to the three-state mode (D3).
|
|
26
|
+
* - undefined / "" → unset (discoverability off-hint)
|
|
27
|
+
* - "1" → on (✦ coda)
|
|
28
|
+
* - "0" → off (silent)
|
|
29
|
+
* - any other non-empty value → unset (show correct enable form)
|
|
30
|
+
*/
|
|
31
|
+
export function sessionCodaMode(envValue) {
|
|
32
|
+
if (envValue === undefined || envValue === "")
|
|
33
|
+
return "unset";
|
|
34
|
+
if (envValue === "1")
|
|
35
|
+
return "on";
|
|
36
|
+
if (envValue === "0")
|
|
37
|
+
return "off";
|
|
38
|
+
return "unset";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether any extra line (off-hint or coda) is allowed (D1 + surface gates).
|
|
42
|
+
* TTY = stdout.isTTY; CI truthy blocks; json blocks; exitOk = errorCount === 0.
|
|
43
|
+
*/
|
|
44
|
+
export function shouldEmitSessionCodaLine(input) {
|
|
45
|
+
return input.exitOk === true && input.tty === true && input.ci !== true && input.json !== true;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* FNV-1a 32-bit (#2712). Documented choice for hash_u32(date + "\\0" + root).
|
|
49
|
+
* No Math.random; same inputs ⇒ same u32 on every platform.
|
|
50
|
+
*/
|
|
51
|
+
export function hashU32(input) {
|
|
52
|
+
let hash = 0x811c9dc5;
|
|
53
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
54
|
+
hash ^= input.charCodeAt(i);
|
|
55
|
+
hash = Math.imul(hash, 0x01000193);
|
|
56
|
+
}
|
|
57
|
+
return hash >>> 0;
|
|
58
|
+
}
|
|
59
|
+
/** UTC calendar date yyyy-mm-dd from a Date (stable for 24h across a team day). */
|
|
60
|
+
export function utcDateYmd(date) {
|
|
61
|
+
return date.toISOString().slice(0, 10);
|
|
62
|
+
}
|
|
63
|
+
/** Best-effort realpath; falls back to the given path when realpath fails. */
|
|
64
|
+
export function projectRootRealpath(projectRoot) {
|
|
65
|
+
try {
|
|
66
|
+
return realpathSync(projectRoot);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return projectRoot;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Select one coda body from the pack (enabled path only).
|
|
74
|
+
* index = hash_u32(utc_date + "\\0" + project_root_realpath) % len(codas)
|
|
75
|
+
* Empty list → null (omit ✦; doctor still 0).
|
|
76
|
+
*/
|
|
77
|
+
export function selectSessionCoda(input) {
|
|
78
|
+
const { date, projectRoot, codas } = input;
|
|
79
|
+
if (codas.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
const key = `${date}\0${projectRoot}`;
|
|
82
|
+
const index = hashU32(key) % codas.length;
|
|
83
|
+
return codas[index] ?? null;
|
|
84
|
+
}
|
|
85
|
+
/** Skip non-strings and bodies over SESSION_CODA_MAX_CHARS. */
|
|
86
|
+
export function filterCodaEntries(raw) {
|
|
87
|
+
if (!Array.isArray(raw))
|
|
88
|
+
return [];
|
|
89
|
+
const out = [];
|
|
90
|
+
for (const entry of raw) {
|
|
91
|
+
if (typeof entry !== "string")
|
|
92
|
+
continue;
|
|
93
|
+
if (entry.length === 0 || entry.length > SESSION_CODA_MAX_CHARS)
|
|
94
|
+
continue;
|
|
95
|
+
out.push(entry);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Load + filter coda lines from content. Fail-open: missing/unreadable/invalid
|
|
101
|
+
* / empty after filter → [] (omit ✦; no error noise).
|
|
102
|
+
*/
|
|
103
|
+
export function loadSessionCodas(frameworkRoot, readText) {
|
|
104
|
+
const packPath = join(contentRoot(frameworkRoot), SESSION_CODA_REL_PATH);
|
|
105
|
+
try {
|
|
106
|
+
const text = readText !== undefined ? readText(packPath) : readFileSync(packPath, { encoding: "utf8" });
|
|
107
|
+
if (text === null || text === "")
|
|
108
|
+
return [];
|
|
109
|
+
return filterCodaEntries(JSON.parse(text));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Format enabled coda line: ✦ + space + body. */
|
|
116
|
+
export function formatSessionCodaEnabledLine(body) {
|
|
117
|
+
return `${SESSION_CODA_STAR_PREFIX}${body}`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resolve the single extra line (or null) after final success footer.
|
|
121
|
+
* Does not print; caller places after blank line post footer (D2).
|
|
122
|
+
*/
|
|
123
|
+
export function resolveSessionCodaLine(input) {
|
|
124
|
+
if (!input.shouldEmit)
|
|
125
|
+
return null;
|
|
126
|
+
if (input.mode === "off")
|
|
127
|
+
return null;
|
|
128
|
+
if (input.mode === "unset")
|
|
129
|
+
return SESSION_CODA_OFF_HINT;
|
|
130
|
+
const body = selectSessionCoda({
|
|
131
|
+
date: input.date,
|
|
132
|
+
projectRoot: input.projectRoot,
|
|
133
|
+
codas: input.codas,
|
|
134
|
+
});
|
|
135
|
+
if (body === null)
|
|
136
|
+
return null;
|
|
137
|
+
return formatSessionCodaEnabledLine(body);
|
|
138
|
+
}
|
|
139
|
+
/** Help / env-docs fragment for DEFT_SESSION_CODA three-state (#2712). */
|
|
140
|
+
export function sessionCodaHelpText() {
|
|
141
|
+
return [
|
|
142
|
+
"Environment:",
|
|
143
|
+
` ${SESSION_CODA_ENV} Session coda after interactive success (exit 0, TTY, not CI, not --json):`,
|
|
144
|
+
" unset Print discoverability line:",
|
|
145
|
+
` ${SESSION_CODA_OFF_HINT}`,
|
|
146
|
+
" =1 Print one curated line, e.g.:",
|
|
147
|
+
` ${formatSessionCodaEnabledLine("Doctor first. Then continue.")}`,
|
|
148
|
+
" =0 Silent (no hint, no coda)",
|
|
149
|
+
].join("\n");
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=session-coda.js.map
|
package/dist/doctor/types.d.ts
CHANGED
|
@@ -123,6 +123,22 @@ export interface DoctorSeams {
|
|
|
123
123
|
now?: Date;
|
|
124
124
|
}) => string | null;
|
|
125
125
|
readonly isTty?: () => boolean;
|
|
126
|
+
/**
|
|
127
|
+
* stdout TTY probe for session coda (#2712). Defaults to
|
|
128
|
+
* `process.stdout.isTTY === true` (not stdin). Separate from `isTty` (stdin
|
|
129
|
+
* used by --fix prompts).
|
|
130
|
+
*/
|
|
131
|
+
readonly stdoutIsTty?: () => boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Override DEFT_SESSION_CODA for tests (#2712). `undefined` means read
|
|
134
|
+
* process.env; pass `""` or omit via explicit nullish env simulation with
|
|
135
|
+
* `undefined` from a seam that returns undefined.
|
|
136
|
+
*/
|
|
137
|
+
readonly sessionCodaEnv?: string | undefined;
|
|
138
|
+
/** Override CI env string for session coda gates (#2712). */
|
|
139
|
+
readonly ciEnv?: string | undefined;
|
|
140
|
+
/** Override session-coda pack loader for fail-open tests (#2712). */
|
|
141
|
+
readonly loadSessionCodas?: (frameworkRoot: string) => string[];
|
|
126
142
|
readonly readYn?: (prompt: string, defaultYes: boolean) => boolean;
|
|
127
143
|
readonly writeText?: (path: string, content: string) => void;
|
|
128
144
|
readonly now?: () => Date;
|
package/dist/doctor/which.d.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
export interface WhichAllOptions {
|
|
2
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
3
|
+
readonly platform?: NodeJS.Platform;
|
|
4
|
+
readonly exists?: (path: string) => boolean;
|
|
5
|
+
/** Optional executable predicate (defaults to file + X_OK on posix). */
|
|
6
|
+
readonly isExecutable?: (path: string) => boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Enumerate every PATH match for `name` (PATH order, first = highest precedence).
|
|
10
|
+
* Pure PATH scan — no shell locator — so tests and win32/posix stay hermetic and
|
|
11
|
+
* gated ritual never executes a PATH-substituted `which`/`where` (#3233).
|
|
12
|
+
* Only files (posix: executable bit) are accepted — not bare directories.
|
|
13
|
+
*/
|
|
14
|
+
export declare function whichAllFromPath(name: string, options?: WhichAllOptions): string[];
|
|
15
|
+
/**
|
|
16
|
+
* All PATH matches for `name` without shelling out (#3233 security).
|
|
17
|
+
* Alias of {@link whichAllFromPath} — never runs bare `which` / `where`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function defaultWhichAll(name: string, options?: WhichAllOptions): string[];
|
|
20
|
+
/** Default PATH lookup mirroring Python `shutil.which` (first match only). */
|
|
2
21
|
export declare function defaultWhich(name: string): string | null;
|
|
3
22
|
//# sourceMappingURL=which.d.ts.map
|
package/dist/doctor/which.js
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
|
|
2
|
+
import { accessSync, constants, existsSync, statSync } from "node:fs";
|
|
3
|
+
import { posix, win32 } from "node:path";
|
|
4
|
+
function defaultIsExecutable(path, platform) {
|
|
5
|
+
try {
|
|
6
|
+
const st = statSync(path);
|
|
7
|
+
// Directories named `deft` must not shadow later executables (#3233 Greptile).
|
|
8
|
+
if (st.isDirectory())
|
|
9
|
+
return false;
|
|
10
|
+
if (!st.isFile() && !st.isSymbolicLink())
|
|
11
|
+
return false;
|
|
12
|
+
if (platform === "win32") {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
accessSync(path, constants.X_OK);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Enumerate every PATH match for `name` (PATH order, first = highest precedence).
|
|
24
|
+
* Pure PATH scan — no shell locator — so tests and win32/posix stay hermetic and
|
|
25
|
+
* gated ritual never executes a PATH-substituted `which`/`where` (#3233).
|
|
26
|
+
* Only files (posix: executable bit) are accepted — not bare directories.
|
|
27
|
+
*/
|
|
28
|
+
export function whichAllFromPath(name, options = {}) {
|
|
29
|
+
const env = options.env ?? process.env;
|
|
30
|
+
const platform = options.platform ?? process.platform;
|
|
31
|
+
const exists = options.exists ?? existsSync;
|
|
32
|
+
const isExecutable = options.isExecutable ?? ((p) => defaultIsExecutable(p, platform));
|
|
33
|
+
const pathValue = env.PATH ?? env.Path ?? "";
|
|
34
|
+
if (pathValue === "") {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const isWindows = platform === "win32";
|
|
38
|
+
const exts = isWindows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";") : [""];
|
|
39
|
+
const sep = isWindows ? ";" : ":";
|
|
40
|
+
const joinPath = isWindows ? win32.join : posix.join;
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const dir of pathValue.split(sep)) {
|
|
44
|
+
if (dir === "")
|
|
45
|
+
continue;
|
|
46
|
+
for (const ext of exts) {
|
|
47
|
+
const candidate = joinPath(dir, `${name}${ext}`);
|
|
48
|
+
const key = isWindows ? candidate.toLowerCase() : candidate;
|
|
49
|
+
if (seen.has(key))
|
|
50
|
+
continue;
|
|
51
|
+
if (exists(candidate) && isExecutable(candidate)) {
|
|
52
|
+
seen.add(key);
|
|
53
|
+
out.push(candidate);
|
|
54
|
+
// One match per PATH directory (first PATHEXT hit wins on win32).
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* All PATH matches for `name` without shelling out (#3233 security).
|
|
63
|
+
* Alias of {@link whichAllFromPath} — never runs bare `which` / `where`.
|
|
64
|
+
*/
|
|
65
|
+
export function defaultWhichAll(name, options = {}) {
|
|
66
|
+
return whichAllFromPath(name, options);
|
|
67
|
+
}
|
|
68
|
+
/** Default PATH lookup mirroring Python `shutil.which` (first match only). */
|
|
3
69
|
export function defaultWhich(name) {
|
|
4
70
|
const locator = process.platform === "win32" ? "where" : "which";
|
|
5
71
|
try {
|