@deftai/directive-core 0.99.0 → 0.100.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 +157 -23
- package/dist/authz/decompose-apply.d.ts +67 -0
- package/dist/authz/decompose-apply.js +302 -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/doctor/checks.d.ts +44 -0
- package/dist/doctor/checks.js +428 -2
- package/dist/doctor/main.d.ts +6 -7
- package/dist/doctor/main.js +72 -79
- 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 +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +20 -0
- package/dist/init-deposit/agent-hooks.js +51 -31
- package/dist/intake/issue-ingest.js +29 -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/ceremony-dial.d.ts +67 -11
- package/dist/policy/ceremony-dial.js +174 -23
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- 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/render/framework-commands.js +4 -0
- package/dist/scope/acceptance-evidence.d.ts +76 -0
- package/dist/scope/acceptance-evidence.js +348 -0
- package/dist/scope/coverage-map.d.ts +98 -0
- package/dist/scope/coverage-map.js +558 -0
- package/dist/scope/decompose.js +125 -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/active-cli.d.ts +79 -0
- package/dist/session/active-cli.js +382 -0
- package/dist/session/effort-budget.d.ts +130 -0
- package/dist/session/effort-budget.js +334 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/session-start.d.ts +6 -0
- package/dist/session/session-start.js +53 -2
- 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/xbrief-migrate/migrate-project.d.ts +4 -0
- package/dist/xbrief-migrate/migrate-project.js +92 -3
- package/package.json +7 -3
package/dist/scope/transition.js
CHANGED
|
@@ -3,6 +3,9 @@ import { dirname, join, resolve } from "node:path";
|
|
|
3
3
|
import { InstrumentedVbriefCrud, persistCrudMetrics } from "../eval/crud-telemetry.js";
|
|
4
4
|
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
5
5
|
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
6
|
+
import { evaluateCompletedPlanConsistency } from "../lifecycle/completed-consistency.js";
|
|
7
|
+
import { evaluateLiteralAcceptanceFromPlan } from "../literal-acceptance/index.js";
|
|
8
|
+
import { evaluateAcceptanceEvidenceGate, formatAcceptanceCompletionListing, } from "./acceptance-evidence.js";
|
|
6
9
|
import { append, canonicalLogPath, newDecisionId } from "./audit-log.js";
|
|
7
10
|
import { atomicWriteBrief, formatBriefJson, readBriefForMutation } from "./brief-io.js";
|
|
8
11
|
import { stampCompletionMetadata } from "./capacity-stamp.js";
|
|
@@ -165,16 +168,65 @@ export function runTransition(action, filePath, now = new Date(), options = {})
|
|
|
165
168
|
stampDeliveryProvenance(planObj, gate.provenance);
|
|
166
169
|
}
|
|
167
170
|
}
|
|
171
|
+
// #3240: per-criterion typed evidence or human-origin disposition before auto-advance.
|
|
172
|
+
let acceptanceReports;
|
|
173
|
+
let acceptanceListing = "";
|
|
174
|
+
if (act === "complete" && options.skipAcceptanceEvidenceGate !== true) {
|
|
175
|
+
const acceptanceGate = evaluateAcceptanceEvidenceGate(planObj);
|
|
176
|
+
acceptanceReports = acceptanceGate.reports;
|
|
177
|
+
if (!acceptanceGate.ok) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
message: acceptanceGate.message,
|
|
181
|
+
acceptanceReports: acceptanceGate.reports,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
acceptanceListing = formatAcceptanceCompletionListing(acceptanceGate.reports);
|
|
185
|
+
// #3267: run agent-authored literal AC before complete; re-scan narratives so
|
|
186
|
+
// narrative-only stated commands fail closed (promote required) rather than skip.
|
|
187
|
+
const literalGate = evaluateLiteralAcceptanceFromPlan(planObj, {
|
|
188
|
+
projectRoot,
|
|
189
|
+
captureFromNarratives: true,
|
|
190
|
+
});
|
|
191
|
+
if (!literalGate.ok) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
message: `Literal acceptance-command gate failed before scope:complete (#3267).\n` +
|
|
195
|
+
literalGate.message,
|
|
196
|
+
acceptanceReports: acceptanceGate.reports,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (literalGate.commands.length > 0 || literalGate.message.length > 0) {
|
|
200
|
+
acceptanceListing =
|
|
201
|
+
acceptanceListing.length > 0
|
|
202
|
+
? `${acceptanceListing}\n${literalGate.message}`
|
|
203
|
+
: literalGate.message;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
168
206
|
planObj.status = targetStatus;
|
|
169
207
|
planObj.updated = nowIso;
|
|
170
208
|
// Keep the envelope clock aligned with plan.updated on every mutating transition (#2862).
|
|
171
209
|
stampEnvelopeUpdated(data, nowIso);
|
|
172
210
|
// Reconcile the completing brief's own plan.items (mirrors #1527 / #2566 registry sync) (#2862).
|
|
211
|
+
// On complete, items only reach here when #3240 evidence/disposition gate passed.
|
|
173
212
|
if (OWN_ITEMS_RECONCILE_ACTIONS.has(act)) {
|
|
174
213
|
advanceNonTerminalOwnItems(planObj.items, targetStatus);
|
|
175
214
|
}
|
|
176
215
|
if (act === "complete") {
|
|
177
216
|
stampCompletionMetadata(planObj, projectRoot, nowIso);
|
|
217
|
+
// #3242 / epic #3237 Q4: after reconcile, completed lifecycle must match
|
|
218
|
+
// plan.status=completed and terminal plan.items (compose with #3240).
|
|
219
|
+
const consistency = evaluateCompletedPlanConsistency(planObj, {
|
|
220
|
+
relPath: basename,
|
|
221
|
+
requireStatus: "completed",
|
|
222
|
+
});
|
|
223
|
+
if (!consistency.ok) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
message: consistency.message,
|
|
227
|
+
acceptanceReports,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
178
230
|
}
|
|
179
231
|
const formatted = formatBriefJson(data);
|
|
180
232
|
const crud = new InstrumentedVbriefCrud({ now: () => now });
|
|
@@ -219,12 +271,16 @@ export function runTransition(action, filePath, now = new Date(), options = {})
|
|
|
219
271
|
ok: false,
|
|
220
272
|
message: `${actionLabel} ${basename}: brief moved to ${targetFolder}/ but ` +
|
|
221
273
|
`PROJECT-DEFINITION sync failed: ${pdSyncError}`,
|
|
274
|
+
acceptanceReports,
|
|
222
275
|
};
|
|
223
276
|
}
|
|
224
277
|
syncSpecificationAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
|
|
278
|
+
const moveMsg = `${actionLabel} ${basename}: ${currentFolder}/ -> ${targetFolder}/ (status: ${targetStatus})` +
|
|
279
|
+
(acceptanceListing.length > 0 ? `\n${acceptanceListing}` : "");
|
|
225
280
|
return {
|
|
226
281
|
ok: true,
|
|
227
|
-
message:
|
|
282
|
+
message: moveMsg,
|
|
283
|
+
acceptanceReports,
|
|
228
284
|
};
|
|
229
285
|
}
|
|
230
286
|
const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot, { projectRoot });
|
|
@@ -239,9 +295,12 @@ export function runTransition(action, filePath, now = new Date(), options = {})
|
|
|
239
295
|
/* best-effort telemetry persistence */
|
|
240
296
|
}
|
|
241
297
|
const actionLabel = STAY_LABELS[act] ?? act.charAt(0).toUpperCase() + act.slice(1);
|
|
298
|
+
const stayMsg = `${actionLabel} ${basename}: stays in ${currentFolder}/ (status: ${targetStatus})` +
|
|
299
|
+
(acceptanceListing.length > 0 ? `\n${acceptanceListing}` : "");
|
|
242
300
|
return {
|
|
243
301
|
ok: true,
|
|
244
|
-
message:
|
|
302
|
+
message: stayMsg,
|
|
303
|
+
acceptanceReports,
|
|
245
304
|
};
|
|
246
305
|
}
|
|
247
306
|
export function recordWipCapOverride(filePath, projectRoot, check, now = new Date()) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active CLI path / engine-version check after global upgrade (#3233).
|
|
3
|
+
*
|
|
4
|
+
* A global `npm i -g` can land under one npm prefix while a higher-precedence
|
|
5
|
+
* PATH entry still runs an older `deft` / `directive`. The upgrade looks
|
|
6
|
+
* complete, but bare CLI invocations keep the stale engine.
|
|
7
|
+
*
|
|
8
|
+
* Shell-active binaries may be probed with `--version`. Lower-precedence PATH
|
|
9
|
+
* candidates are inspected via package.json only (never executed) so gated
|
|
10
|
+
* ritual cannot run unselected PATH entries (#3233 Greptile P1 security).
|
|
11
|
+
*/
|
|
12
|
+
/** Bare CLI names operators invoke after upgrade. */
|
|
13
|
+
export declare const ACTIVE_CLI_COMMANDS: readonly ["deft", "directive"];
|
|
14
|
+
export type ActiveCliCommand = (typeof ACTIVE_CLI_COMMANDS)[number];
|
|
15
|
+
export interface CliCandidate {
|
|
16
|
+
readonly command: ActiveCliCommand;
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly version: string | null;
|
|
19
|
+
/** Zero-based PATH precedence within this command (0 = shell-active). */
|
|
20
|
+
readonly precedence: number;
|
|
21
|
+
/** How the version was obtained (for diagnostics / tests). */
|
|
22
|
+
readonly versionSource: "exec" | "package-json" | "none";
|
|
23
|
+
}
|
|
24
|
+
export interface ActiveCliCheckSeams {
|
|
25
|
+
readonly whichAll?: (name: string) => string[];
|
|
26
|
+
/**
|
|
27
|
+
* Optional override for version probing. When omitted, active binaries may
|
|
28
|
+
* spawn `--version`; shadowed binaries use package.json only.
|
|
29
|
+
*/
|
|
30
|
+
readonly probeVersion?: (executablePath: string, precedence: number) => string | null;
|
|
31
|
+
readonly commands?: readonly ActiveCliCommand[];
|
|
32
|
+
/** Override default upgrade-target resolution (tests). */
|
|
33
|
+
readonly resolveDefaultTarget?: (candidates: readonly CliCandidate[]) => string | null;
|
|
34
|
+
/** Inject in-process engine version for default-target resolution. */
|
|
35
|
+
readonly inProcessVersion?: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface ActiveCliCheckResult {
|
|
38
|
+
/** True when no shadowing / target mismatch was detected. */
|
|
39
|
+
readonly ok: boolean;
|
|
40
|
+
/** 0 = ok, 1 = shadow/mismatch (fail closed), 2 = probe config error (unused; reserved). */
|
|
41
|
+
readonly code: number;
|
|
42
|
+
readonly active: CliCandidate | null;
|
|
43
|
+
readonly candidates: readonly CliCandidate[];
|
|
44
|
+
readonly targetVersion: string | null;
|
|
45
|
+
/** Single-line summary for ritual / exit message. */
|
|
46
|
+
readonly message: string;
|
|
47
|
+
/** Multi-line stderr diagnostic (paths, versions, remediation). */
|
|
48
|
+
readonly lines: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve engine version by walking the install tree for package.json (no exec).
|
|
52
|
+
* Follows symlinks and win32 `.cmd` shims that embed a node path.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readEngineVersionFromInstallTree(executablePath: string): string | null;
|
|
55
|
+
/** Probe shell-active CLI only via `--version` (never used for shadowed PATH entries). */
|
|
56
|
+
export declare function probeCliEngineVersion(executablePath: string): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Version probe used by the default collector:
|
|
59
|
+
* - precedence 0 (shell-active): package.json first, then `--version` spawn
|
|
60
|
+
* - precedence > 0: package.json only (never execute shadowed PATH entries)
|
|
61
|
+
*/
|
|
62
|
+
export declare function probeCandidateVersion(executablePath: string, precedence: number): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Default upgrade target when callers omit `targetEngineVersion` (#3233 P1):
|
|
65
|
+
* the max of (1) publishable in-process engine version and (2) versions from
|
|
66
|
+
* **non-active** PATH candidates only. The shell-active CLI must never become
|
|
67
|
+
* its own expected target (lone stale self-match).
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveDefaultActiveCliTarget(candidates: readonly CliCandidate[], inProcessVersion?: string | null): string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Fail-closed check: shell-active `deft`/`directive` must match `targetVersion`
|
|
72
|
+
* (or the resolved default target), and must not be older than another PATH
|
|
73
|
+
* candidate (#3233).
|
|
74
|
+
*
|
|
75
|
+
* When no CLI is on PATH, returns ok=true (absence is covered by verify:tools /
|
|
76
|
+
* engine ladder — this gate only catches shadowing after a multi-prefix install).
|
|
77
|
+
*/
|
|
78
|
+
export declare function checkActiveCliAgainstTarget(targetVersion?: string | null, seams?: ActiveCliCheckSeams): ActiveCliCheckResult;
|
|
79
|
+
//# sourceMappingURL=active-cli.d.ts.map
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active CLI path / engine-version check after global upgrade (#3233).
|
|
3
|
+
*
|
|
4
|
+
* A global `npm i -g` can land under one npm prefix while a higher-precedence
|
|
5
|
+
* PATH entry still runs an older `deft` / `directive`. The upgrade looks
|
|
6
|
+
* complete, but bare CLI invocations keep the stale engine.
|
|
7
|
+
*
|
|
8
|
+
* Shell-active binaries may be probed with `--version`. Lower-precedence PATH
|
|
9
|
+
* candidates are inspected via package.json only (never executed) so gated
|
|
10
|
+
* ritual cannot run unselected PATH entries (#3233 Greptile P1 security).
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { CANONICAL_UPGRADE_COMMAND } from "../doctor/constants.js";
|
|
16
|
+
import { defaultWhichAll } from "../doctor/which.js";
|
|
17
|
+
import { readCorePackageVersion } from "../engine-version.js";
|
|
18
|
+
import { compareSemver } from "../resolution/pin.js";
|
|
19
|
+
import { quoteWin32CommandForShell, shouldUseShellForCommand, } from "../verify-env/command-spawn.js";
|
|
20
|
+
/** Bare CLI names operators invoke after upgrade. */
|
|
21
|
+
export const ACTIVE_CLI_COMMANDS = ["deft", "directive"];
|
|
22
|
+
/** Bounded semver extract — avoids ReDoS on long zero-runs (CodeQL). */
|
|
23
|
+
const SEMVER_IN_TEXT_RE = /\b(\d{1,6}\.\d{1,6}\.\d{1,6})\b/;
|
|
24
|
+
const ENGINE_PKG_NAMES = new Set(["@deftai/directive", "@deftai/directive-core"]);
|
|
25
|
+
function parseVersionFromOutput(out) {
|
|
26
|
+
// Cap scan length so CodeQL polynomial-regex alerts cannot fire on huge text.
|
|
27
|
+
const sample = out.length > 512 ? out.slice(0, 512) : out;
|
|
28
|
+
const match = SEMVER_IN_TEXT_RE.exec(sample);
|
|
29
|
+
return match?.[1] ?? null;
|
|
30
|
+
}
|
|
31
|
+
function isPublishableSemver(version) {
|
|
32
|
+
if (typeof version !== "string" || version.length === 0)
|
|
33
|
+
return false;
|
|
34
|
+
if (version === "0.0.0" || version.includes("0.0.0-dev") || version.includes("dev")) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return SEMVER_IN_TEXT_RE.test(version);
|
|
38
|
+
}
|
|
39
|
+
function tryReadPackageVersion(pkgPath) {
|
|
40
|
+
try {
|
|
41
|
+
const raw = readFileSync(pkgPath, "utf8");
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
if (parsed === null || typeof parsed !== "object")
|
|
44
|
+
return null;
|
|
45
|
+
const rec = parsed;
|
|
46
|
+
const name = typeof rec.name === "string" ? rec.name : "";
|
|
47
|
+
if (!ENGINE_PKG_NAMES.has(name))
|
|
48
|
+
return null;
|
|
49
|
+
// Prefer directive-core dep when the CLI package version is a workspace/dev 0.0.0.
|
|
50
|
+
if (name === "@deftai/directive" && rec.dependencies && typeof rec.dependencies === "object") {
|
|
51
|
+
const core = rec.dependencies["@deftai/directive-core"];
|
|
52
|
+
if (typeof core === "string") {
|
|
53
|
+
const coreVer = parseVersionFromOutput(core);
|
|
54
|
+
if (coreVer !== null && isPublishableSemver(coreVer))
|
|
55
|
+
return coreVer;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return typeof rec.version === "string" && rec.version.length > 0 ? rec.version : null;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve engine version by walking the install tree for package.json (no exec).
|
|
66
|
+
* Follows symlinks and win32 `.cmd` shims that embed a node path.
|
|
67
|
+
*/
|
|
68
|
+
export function readEngineVersionFromInstallTree(executablePath) {
|
|
69
|
+
const seeds = [executablePath];
|
|
70
|
+
try {
|
|
71
|
+
if (existsSync(executablePath)) {
|
|
72
|
+
const st = lstatSync(executablePath);
|
|
73
|
+
if (st.isSymbolicLink()) {
|
|
74
|
+
try {
|
|
75
|
+
const link = readlinkSync(executablePath);
|
|
76
|
+
seeds.push(resolve(dirname(executablePath), link));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
/* ignore */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
seeds.push(realpathSync(executablePath));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* ignore */
|
|
87
|
+
}
|
|
88
|
+
// Win32 npm shims often contain a path to the real package bin.
|
|
89
|
+
if (/\.(cmd|bat)$/i.test(executablePath)) {
|
|
90
|
+
try {
|
|
91
|
+
const text = readFileSync(executablePath, "utf8");
|
|
92
|
+
const m = /%~dp0[\\/](\.\.[\\/].*?node_modules[\\/]@deftai[\\/]directive[\\/][^\s"']+)/i.exec(text) ??
|
|
93
|
+
/([A-Za-z]:\\[^\r\n"']*node_modules[\\/]@deftai[\\/]directive[\\/][^\r\n"']+)/i.exec(text) ??
|
|
94
|
+
/([^\s"']*node_modules[\\/]@deftai[\\/]directive[\\/][^\s"']+)/i.exec(text);
|
|
95
|
+
if (m?.[1]) {
|
|
96
|
+
seeds.push(resolve(dirname(executablePath), m[1].replace(/\//g, "\\")));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* ignore */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
for (const seed of seeds) {
|
|
109
|
+
let dir = dirname(seed);
|
|
110
|
+
for (let i = 0; i < 12; i += 1) {
|
|
111
|
+
const pkgPath = join(dir, "package.json");
|
|
112
|
+
const ver = tryReadPackageVersion(pkgPath);
|
|
113
|
+
if (ver !== null) {
|
|
114
|
+
const parsed = parseVersionFromOutput(ver) ?? ver;
|
|
115
|
+
if (isPublishableSemver(parsed) || SEMVER_IN_TEXT_RE.test(parsed)) {
|
|
116
|
+
return parseVersionFromOutput(parsed) ?? parsed;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// npm global: .../node_modules/@deftai/directive
|
|
120
|
+
const nested = join(dir, "node_modules", "@deftai", "directive", "package.json");
|
|
121
|
+
const nestedVer = tryReadPackageVersion(nested);
|
|
122
|
+
if (nestedVer !== null) {
|
|
123
|
+
return parseVersionFromOutput(nestedVer) ?? nestedVer;
|
|
124
|
+
}
|
|
125
|
+
const nestedCore = join(dir, "node_modules", "@deftai", "directive-core", "package.json");
|
|
126
|
+
const nestedCoreVer = tryReadPackageVersion(nestedCore);
|
|
127
|
+
if (nestedCoreVer !== null) {
|
|
128
|
+
return parseVersionFromOutput(nestedCoreVer) ?? nestedCoreVer;
|
|
129
|
+
}
|
|
130
|
+
const parent = dirname(dir);
|
|
131
|
+
if (parent === dir)
|
|
132
|
+
break;
|
|
133
|
+
dir = parent;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
/** Probe shell-active CLI only via `--version` (never used for shadowed PATH entries). */
|
|
139
|
+
export function probeCliEngineVersion(executablePath) {
|
|
140
|
+
const fromTree = readEngineVersionFromInstallTree(executablePath);
|
|
141
|
+
if (fromTree !== null)
|
|
142
|
+
return fromTree;
|
|
143
|
+
const shell = shouldUseShellForCommand(executablePath);
|
|
144
|
+
const cmd = shell && process.platform === "win32"
|
|
145
|
+
? quoteWin32CommandForShell(executablePath)
|
|
146
|
+
: executablePath;
|
|
147
|
+
try {
|
|
148
|
+
const result = spawnSync(cmd, ["--version"], {
|
|
149
|
+
encoding: "utf8",
|
|
150
|
+
timeout: 5_000,
|
|
151
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
152
|
+
shell,
|
|
153
|
+
windowsHide: true,
|
|
154
|
+
});
|
|
155
|
+
if (result.status !== 0) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : "";
|
|
159
|
+
return parseVersionFromOutput(stdout);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Version probe used by the default collector:
|
|
167
|
+
* - precedence 0 (shell-active): package.json first, then `--version` spawn
|
|
168
|
+
* - precedence > 0: package.json only (never execute shadowed PATH entries)
|
|
169
|
+
*/
|
|
170
|
+
export function probeCandidateVersion(executablePath, precedence) {
|
|
171
|
+
if (precedence > 0) {
|
|
172
|
+
return readEngineVersionFromInstallTree(executablePath);
|
|
173
|
+
}
|
|
174
|
+
return probeCliEngineVersion(executablePath);
|
|
175
|
+
}
|
|
176
|
+
function collectCandidates(seams) {
|
|
177
|
+
const whichAll = seams.whichAll ?? ((name) => defaultWhichAll(name));
|
|
178
|
+
const probe = seams.probeVersion ?? probeCandidateVersion;
|
|
179
|
+
const commands = seams.commands ?? ACTIVE_CLI_COMMANDS;
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const command of commands) {
|
|
182
|
+
const paths = whichAll(command);
|
|
183
|
+
paths.forEach((path, precedence) => {
|
|
184
|
+
const version = probe(path, precedence);
|
|
185
|
+
out.push({
|
|
186
|
+
command,
|
|
187
|
+
path,
|
|
188
|
+
version,
|
|
189
|
+
precedence,
|
|
190
|
+
versionSource: version === null ? "none" : precedence > 0 ? "package-json" : "exec",
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
function formatCandidateLine(c) {
|
|
197
|
+
const ver = (c.version ?? "unknown").replace(/\r?\n/g, " ");
|
|
198
|
+
const path = c.path.replace(/\r?\n/g, " ");
|
|
199
|
+
const command = String(c.command).replace(/\r?\n/g, " ");
|
|
200
|
+
const mark = c.precedence === 0 ? " (active)" : "";
|
|
201
|
+
return ` - ${path} → engine ${ver}${mark} [${command}]`;
|
|
202
|
+
}
|
|
203
|
+
function remediationLines(params) {
|
|
204
|
+
const lines = [
|
|
205
|
+
"[deft session] Remediation (higher-precedence CLI shadows the upgraded install):",
|
|
206
|
+
" 1. Align every global prefix to the same engine version, e.g.:",
|
|
207
|
+
` ${CANONICAL_UPGRADE_COMMAND}`,
|
|
208
|
+
" (or `pnpm add -g @deftai/directive@latest` when pnpm owns the active prefix)",
|
|
209
|
+
];
|
|
210
|
+
if (params.active !== null) {
|
|
211
|
+
lines.push(` 2. Confirm the shell-active binary: re-open the shell, then`, ` \`${params.active.command} --version\` must report` +
|
|
212
|
+
(params.targetVersion !== null
|
|
213
|
+
? ` engine @${params.targetVersion}.`
|
|
214
|
+
: " the upgraded engine version."));
|
|
215
|
+
lines.push(` 3. If PATH still prefers a stale prefix, put the upgraded npm/pnpm bin dir`, ` ahead of ${params.active.path}, or uninstall/upgrade that shadowing install.`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
lines.push(" 2. Ensure the upgraded npm/pnpm global bin directory is on PATH, then re-run", " `deft --version` / `directive --version`.");
|
|
219
|
+
}
|
|
220
|
+
if (params.newerCandidates.length > 0) {
|
|
221
|
+
lines.push(" Competing installs (not shell-active but newer or target-matching):");
|
|
222
|
+
for (const c of params.newerCandidates) {
|
|
223
|
+
lines.push(formatCandidateLine(c));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return lines;
|
|
227
|
+
}
|
|
228
|
+
function maxPublishableVersion(versions) {
|
|
229
|
+
let best = null;
|
|
230
|
+
for (const v of versions) {
|
|
231
|
+
if (!isPublishableSemver(v))
|
|
232
|
+
continue;
|
|
233
|
+
if (best === null || compareSemver(v, best) === 1) {
|
|
234
|
+
best = v;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return best;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Default upgrade target when callers omit `targetEngineVersion` (#3233 P1):
|
|
241
|
+
* the max of (1) publishable in-process engine version and (2) versions from
|
|
242
|
+
* **non-active** PATH candidates only. The shell-active CLI must never become
|
|
243
|
+
* its own expected target (lone stale self-match).
|
|
244
|
+
*/
|
|
245
|
+
export function resolveDefaultActiveCliTarget(candidates, inProcessVersion) {
|
|
246
|
+
let inProcess = inProcessVersion;
|
|
247
|
+
if (inProcess === undefined) {
|
|
248
|
+
try {
|
|
249
|
+
inProcess = readCorePackageVersion();
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
inProcess = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Independent sources only — exclude precedence-0 (active) versions.
|
|
256
|
+
const fromShadows = candidates.filter((c) => c.precedence > 0).map((c) => c.version);
|
|
257
|
+
return maxPublishableVersion([isPublishableSemver(inProcess) ? inProcess : null, ...fromShadows]);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Fail-closed check: shell-active `deft`/`directive` must match `targetVersion`
|
|
261
|
+
* (or the resolved default target), and must not be older than another PATH
|
|
262
|
+
* candidate (#3233).
|
|
263
|
+
*
|
|
264
|
+
* When no CLI is on PATH, returns ok=true (absence is covered by verify:tools /
|
|
265
|
+
* engine ladder — this gate only catches shadowing after a multi-prefix install).
|
|
266
|
+
*/
|
|
267
|
+
export function checkActiveCliAgainstTarget(targetVersion = null, seams = {}) {
|
|
268
|
+
const candidates = collectCandidates(seams);
|
|
269
|
+
if (candidates.length === 0) {
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
code: 0,
|
|
273
|
+
active: null,
|
|
274
|
+
candidates,
|
|
275
|
+
targetVersion,
|
|
276
|
+
message: "active CLI check skipped (no deft/directive on PATH)",
|
|
277
|
+
lines: [],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const resolveTarget = seams.resolveDefaultTarget ??
|
|
281
|
+
((cs) => resolveDefaultActiveCliTarget(cs, seams.inProcessVersion));
|
|
282
|
+
const effectiveTarget = targetVersion !== null && targetVersion.length > 0 ? targetVersion : resolveTarget(candidates);
|
|
283
|
+
// Group by command; each bare name has its own PATH-active binary.
|
|
284
|
+
const byCommand = new Map();
|
|
285
|
+
for (const c of candidates) {
|
|
286
|
+
const list = byCommand.get(c.command) ?? [];
|
|
287
|
+
list.push(c);
|
|
288
|
+
byCommand.set(c.command, list);
|
|
289
|
+
}
|
|
290
|
+
const failures = [];
|
|
291
|
+
const diagnostic = ["[deft session] CLI candidates on PATH (precedence order):"];
|
|
292
|
+
for (const c of candidates) {
|
|
293
|
+
diagnostic.push(formatCandidateLine(c));
|
|
294
|
+
}
|
|
295
|
+
let primaryActive = null;
|
|
296
|
+
const competing = [];
|
|
297
|
+
for (const [command, list] of byCommand) {
|
|
298
|
+
const active = list.find((c) => c.precedence === 0) ?? list[0];
|
|
299
|
+
if (!active)
|
|
300
|
+
continue;
|
|
301
|
+
if (primaryActive === null) {
|
|
302
|
+
primaryActive = active;
|
|
303
|
+
}
|
|
304
|
+
if (effectiveTarget !== null && effectiveTarget.length > 0) {
|
|
305
|
+
if (active.version === null) {
|
|
306
|
+
failures.push(`shell-active ${command} at ${active.path} did not report an engine version ` +
|
|
307
|
+
`(expected ${effectiveTarget})`);
|
|
308
|
+
}
|
|
309
|
+
else if (active.version !== effectiveTarget) {
|
|
310
|
+
// Fail when active is behind the target (or simply unequal for exact match).
|
|
311
|
+
const cmp = compareSemver(active.version, effectiveTarget);
|
|
312
|
+
if (cmp === -1 || cmp === null) {
|
|
313
|
+
failures.push(`shell-active ${command} is engine ${active.version} at ${active.path}, ` +
|
|
314
|
+
`but upgrade target is ${effectiveTarget}`);
|
|
315
|
+
for (const other of list) {
|
|
316
|
+
if (other.precedence === 0)
|
|
317
|
+
continue;
|
|
318
|
+
if (other.version === effectiveTarget ||
|
|
319
|
+
compareSemver(other.version, active.version) === 1) {
|
|
320
|
+
competing.push(other);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Multi-prefix skew: a lower-precedence candidate is strictly newer than active.
|
|
327
|
+
for (const other of list) {
|
|
328
|
+
if (other.precedence === 0)
|
|
329
|
+
continue;
|
|
330
|
+
const cmp = compareSemver(other.version, active.version);
|
|
331
|
+
if (cmp === 1) {
|
|
332
|
+
failures.push(`shell-active ${command} is engine ${active.version ?? "unknown"} at ${active.path}, ` +
|
|
333
|
+
`but a lower-precedence install is newer (${other.version ?? "unknown"} at ${other.path})`);
|
|
334
|
+
competing.push(other);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (failures.length === 0) {
|
|
339
|
+
const activeVer = primaryActive?.version ?? "unknown";
|
|
340
|
+
const activePath = primaryActive?.path ?? "(none)";
|
|
341
|
+
return {
|
|
342
|
+
ok: true,
|
|
343
|
+
code: 0,
|
|
344
|
+
active: primaryActive,
|
|
345
|
+
candidates,
|
|
346
|
+
targetVersion: effectiveTarget,
|
|
347
|
+
message: effectiveTarget !== null
|
|
348
|
+
? `active CLI engine ${activeVer} matches target ${effectiveTarget} (${activePath})`
|
|
349
|
+
: `active CLI engine ${activeVer} is not shadowed (${activePath})`,
|
|
350
|
+
lines: [],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
// Dedup competing by path.
|
|
354
|
+
const seenPath = new Set();
|
|
355
|
+
const uniqueCompeting = competing.filter((c) => {
|
|
356
|
+
if (seenPath.has(c.path))
|
|
357
|
+
return false;
|
|
358
|
+
seenPath.add(c.path);
|
|
359
|
+
return true;
|
|
360
|
+
});
|
|
361
|
+
const summary = `stale higher-precedence CLI after upgrade: ${failures[0]}` +
|
|
362
|
+
(failures.length > 1 ? ` (+${failures.length - 1} more)` : "");
|
|
363
|
+
const lines = [
|
|
364
|
+
`[deft session] FAIL: ${summary}`,
|
|
365
|
+
...diagnostic,
|
|
366
|
+
...remediationLines({
|
|
367
|
+
active: primaryActive,
|
|
368
|
+
targetVersion: effectiveTarget,
|
|
369
|
+
newerCandidates: uniqueCompeting,
|
|
370
|
+
}),
|
|
371
|
+
];
|
|
372
|
+
return {
|
|
373
|
+
ok: false,
|
|
374
|
+
code: 1,
|
|
375
|
+
active: primaryActive,
|
|
376
|
+
candidates,
|
|
377
|
+
targetVersion: effectiveTarget,
|
|
378
|
+
message: summary,
|
|
379
|
+
lines,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
//# sourceMappingURL=active-cli.js.map
|