@deftai/directive-core 0.68.1 → 0.70.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/deposit/contain.d.ts +41 -0
- package/dist/deposit/contain.js +113 -0
- package/dist/deposit/copy-tree.js +8 -2
- package/dist/doctor/checks.d.ts +11 -0
- package/dist/doctor/checks.js +75 -2
- package/dist/doctor/constants.d.ts +9 -0
- package/dist/doctor/constants.js +11 -0
- package/dist/doctor/main.d.ts +47 -1
- package/dist/doctor/main.js +406 -3
- package/dist/doctor/manifest.d.ts +20 -0
- package/dist/doctor/manifest.js +22 -0
- package/dist/doctor/payload-staleness.d.ts +8 -0
- package/dist/doctor/payload-staleness.js +11 -8
- package/dist/doctor/types.d.ts +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/gitignore.d.ts +18 -0
- package/dist/init-deposit/gitignore.js +94 -15
- package/dist/init-deposit/headless-manifest.d.ts +87 -0
- package/dist/init-deposit/headless-manifest.js +261 -0
- package/dist/init-deposit/index.d.ts +3 -0
- package/dist/init-deposit/index.js +3 -0
- package/dist/init-deposit/init-deposit.js +6 -0
- package/dist/init-deposit/init-dispatch.d.ts +90 -0
- package/dist/init-deposit/init-dispatch.js +170 -0
- package/dist/init-deposit/refresh.d.ts +79 -2
- package/dist/init-deposit/refresh.js +182 -5
- package/dist/init-deposit/scaffold.d.ts +25 -0
- package/dist/init-deposit/scaffold.js +57 -0
- package/dist/init-deposit/untrack-core.d.ts +84 -0
- package/dist/init-deposit/untrack-core.js +150 -0
- package/dist/intake/issue-ingest.d.ts +11 -0
- package/dist/intake/issue-ingest.js +86 -11
- package/dist/policy/plan-extensions.d.ts +31 -0
- package/dist/policy/plan-extensions.js +45 -0
- package/dist/policy/wip.d.ts +2 -2
- package/dist/policy/wip.js +2 -2
- package/dist/resolution/classify.d.ts +41 -0
- package/dist/resolution/classify.js +138 -0
- package/dist/resolution/engine-ladder.d.ts +98 -0
- package/dist/resolution/engine-ladder.js +185 -0
- package/dist/resolution/index.d.ts +19 -0
- package/dist/resolution/index.js +19 -0
- package/dist/resolution/integrity.d.ts +48 -0
- package/dist/resolution/integrity.js +80 -0
- package/dist/resolution/package-manager.d.ts +77 -0
- package/dist/resolution/package-manager.js +103 -0
- package/dist/resolution/pin.d.ts +73 -0
- package/dist/resolution/pin.js +169 -0
- package/dist/resolution/plan.d.ts +66 -0
- package/dist/resolution/plan.js +219 -0
- package/dist/resolution/skew-policy.d.ts +46 -0
- package/dist/resolution/skew-policy.js +120 -0
- package/dist/session/session-start.d.ts +2 -0
- package/dist/session/session-start.js +28 -1
- package/dist/swarm/routing.js +10 -3
- package/dist/triage/bulk/index.d.ts +11 -2
- package/dist/triage/bulk/index.js +41 -4
- package/dist/triage/help/registry-data.d.ts +5 -5
- package/dist/triage/help/registry-data.js +18 -5
- package/dist/triage/scope/cli.d.ts +2 -1
- package/dist/triage/scope/cli.js +30 -6
- package/dist/triage/scope/mutations.d.ts +10 -0
- package/dist/triage/scope/mutations.js +22 -0
- package/dist/triage/welcome/constants.d.ts +1 -2
- package/dist/triage/welcome/constants.js +1 -2
- package/dist/triage/welcome/index.d.ts +1 -0
- package/dist/triage/welcome/index.js +1 -0
- package/dist/triage/welcome/onboard.d.ts +35 -0
- package/dist/triage/welcome/onboard.js +94 -0
- package/dist/umbrella-current-shape/index.d.ts +25 -1
- package/dist/umbrella-current-shape/index.js +84 -4
- package/dist/user-config/index.d.ts +2 -0
- package/dist/user-config/index.js +2 -0
- package/dist/user-config/resolve-user-md.d.ts +76 -0
- package/dist/user-config/resolve-user-md.js +120 -0
- package/dist/vbrief-build/project-definition-io.d.ts +6 -0
- package/dist/vbrief-build/project-definition-io.js +7 -2
- package/dist/vbrief-validate/precutover.js +7 -3
- package/dist/xbrief-migrate/constants.d.ts +10 -0
- package/dist/xbrief-migrate/constants.js +19 -0
- package/dist/xbrief-migrate/detect.d.ts +34 -0
- package/dist/xbrief-migrate/detect.js +33 -0
- package/dist/xbrief-migrate/fs-helpers.d.ts +13 -0
- package/dist/xbrief-migrate/fs-helpers.js +46 -1
- package/dist/xbrief-migrate/index.d.ts +4 -2
- package/dist/xbrief-migrate/index.js +4 -2
- package/dist/xbrief-migrate/migrate-project.d.ts +23 -1
- package/dist/xbrief-migrate/migrate-project.js +97 -8
- package/dist/xbrief-migrate/signpost.d.ts +7 -1
- package/dist/xbrief-migrate/signpost.js +18 -3
- package/package.json +7 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contain.ts — deposit-boundary symlink-escape guard (#2305).
|
|
3
|
+
*
|
|
4
|
+
* The framework deposit derives its destination as
|
|
5
|
+
* `deftDir = join(projectDir, ".deft/core")` and copies the (trusted) content
|
|
6
|
+
* payload into it. If a malicious project repo commits `.deft` (or `.deft/core`,
|
|
7
|
+
* or a parent) as a symlink that escapes the tree — e.g. `.deft -> ../../evil` —
|
|
8
|
+
* the deposit would write/replace framework content THROUGH that symlink,
|
|
9
|
+
* outside the resolved project tree, under the victim's account.
|
|
10
|
+
*
|
|
11
|
+
* `assertDepositContained` refuses that: it anchors on `realpath(projectDir)`,
|
|
12
|
+
* walks each existing component from the project root down to `deftDir`, rejects
|
|
13
|
+
* any component that is a symlink escaping the project tree, and asserts the
|
|
14
|
+
* deepest existing ancestor of `deftDir` resolves back UNDER the project tree
|
|
15
|
+
* using path-SEGMENT containment (not string `startsWith`).
|
|
16
|
+
*
|
|
17
|
+
* Callers MUST invoke it BEFORE the first copy/mkdir/reconstitute so a refusal
|
|
18
|
+
* deposits nothing.
|
|
19
|
+
*
|
|
20
|
+
* Refs #2305.
|
|
21
|
+
*/
|
|
22
|
+
/** Non-zero exit code for a deposit-containment refusal (needs-action). */
|
|
23
|
+
export declare const DEPOSIT_CONTAINMENT_REFUSED_EXIT_CODE = 2;
|
|
24
|
+
/** Thrown by the deposit/refresh path when the destination escapes the tree. */
|
|
25
|
+
export declare class DepositContainmentError extends Error {
|
|
26
|
+
readonly projectDir: string;
|
|
27
|
+
readonly deftDir: string;
|
|
28
|
+
readonly offendingPath: string;
|
|
29
|
+
constructor(message: string, details: {
|
|
30
|
+
projectDir: string;
|
|
31
|
+
deftDir: string;
|
|
32
|
+
offendingPath: string;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Refuse the deposit when `.deft` / `.deft/core` (or a parent) is a symlink that
|
|
37
|
+
* escapes the resolved project tree. Throws {@link DepositContainmentError};
|
|
38
|
+
* MUST be called BEFORE any deposit write so a refusal deposits nothing.
|
|
39
|
+
*/
|
|
40
|
+
export declare function assertDepositContained(projectDir: string, deftDir: string): void;
|
|
41
|
+
//# sourceMappingURL=contain.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contain.ts — deposit-boundary symlink-escape guard (#2305).
|
|
3
|
+
*
|
|
4
|
+
* The framework deposit derives its destination as
|
|
5
|
+
* `deftDir = join(projectDir, ".deft/core")` and copies the (trusted) content
|
|
6
|
+
* payload into it. If a malicious project repo commits `.deft` (or `.deft/core`,
|
|
7
|
+
* or a parent) as a symlink that escapes the tree — e.g. `.deft -> ../../evil` —
|
|
8
|
+
* the deposit would write/replace framework content THROUGH that symlink,
|
|
9
|
+
* outside the resolved project tree, under the victim's account.
|
|
10
|
+
*
|
|
11
|
+
* `assertDepositContained` refuses that: it anchors on `realpath(projectDir)`,
|
|
12
|
+
* walks each existing component from the project root down to `deftDir`, rejects
|
|
13
|
+
* any component that is a symlink escaping the project tree, and asserts the
|
|
14
|
+
* deepest existing ancestor of `deftDir` resolves back UNDER the project tree
|
|
15
|
+
* using path-SEGMENT containment (not string `startsWith`).
|
|
16
|
+
*
|
|
17
|
+
* Callers MUST invoke it BEFORE the first copy/mkdir/reconstitute so a refusal
|
|
18
|
+
* deposits nothing.
|
|
19
|
+
*
|
|
20
|
+
* Refs #2305.
|
|
21
|
+
*/
|
|
22
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
23
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
24
|
+
/** Non-zero exit code for a deposit-containment refusal (needs-action). */
|
|
25
|
+
export const DEPOSIT_CONTAINMENT_REFUSED_EXIT_CODE = 2;
|
|
26
|
+
/** Thrown by the deposit/refresh path when the destination escapes the tree. */
|
|
27
|
+
export class DepositContainmentError extends Error {
|
|
28
|
+
projectDir;
|
|
29
|
+
deftDir;
|
|
30
|
+
offendingPath;
|
|
31
|
+
constructor(message, details) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "DepositContainmentError";
|
|
34
|
+
this.projectDir = details.projectDir;
|
|
35
|
+
this.deftDir = details.deftDir;
|
|
36
|
+
this.offendingPath = details.offendingPath;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Path-SEGMENT containment: is `child` equal to `parent` or nested under it?
|
|
41
|
+
* Uses `path.relative` so `/foo` is NOT treated as containing `/foobar`
|
|
42
|
+
* (`relative("/foo", "/foobar")` is `"../foobar"`, which is rejected).
|
|
43
|
+
*/
|
|
44
|
+
function isContained(parent, child) {
|
|
45
|
+
if (parent === child) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const rel = relative(parent, child);
|
|
49
|
+
return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Refuse the deposit when `.deft` / `.deft/core` (or a parent) is a symlink that
|
|
53
|
+
* escapes the resolved project tree. Throws {@link DepositContainmentError};
|
|
54
|
+
* MUST be called BEFORE any deposit write so a refusal deposits nothing.
|
|
55
|
+
*/
|
|
56
|
+
export function assertDepositContained(projectDir, deftDir) {
|
|
57
|
+
const projectAbs = resolve(projectDir);
|
|
58
|
+
let projectReal;
|
|
59
|
+
try {
|
|
60
|
+
projectReal = realpathSync(projectAbs);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// The project directory does not exist yet (e.g. `directive init <new-path>`).
|
|
64
|
+
// Nothing under it can exist, so there is no pre-existing symlink to escape
|
|
65
|
+
// through -- the downstream init flow creates fresh, clean directories. Let
|
|
66
|
+
// the deposit proceed; a missing content payload / mkdir failure surfaces its
|
|
67
|
+
// own error later.
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const deftAbs = resolve(deftDir);
|
|
71
|
+
const rel = relative(projectAbs, deftAbs);
|
|
72
|
+
if (rel.length === 0 || rel.startsWith("..") || isAbsolute(rel)) {
|
|
73
|
+
throw new DepositContainmentError(`deposit refused: deposit target ${deftAbs} is not nested under the project tree ${projectAbs}`, { projectDir: projectAbs, deftDir: deftAbs, offendingPath: deftAbs });
|
|
74
|
+
}
|
|
75
|
+
const segments = rel.split(/[\\/]+/).filter((segment) => segment.length > 0);
|
|
76
|
+
let current = projectAbs;
|
|
77
|
+
let deepestExistingReal = projectReal;
|
|
78
|
+
for (const segment of segments) {
|
|
79
|
+
current = join(current, segment);
|
|
80
|
+
let info;
|
|
81
|
+
try {
|
|
82
|
+
info = lstatSync(current);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// This component does not exist yet; nothing below it can exist either.
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (info.isSymbolicLink()) {
|
|
89
|
+
let linkReal;
|
|
90
|
+
try {
|
|
91
|
+
linkReal = realpathSync(current);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new DepositContainmentError(`deposit refused: ${current} is a broken/dangling symlink on the deposit path`, { projectDir: projectAbs, deftDir: deftAbs, offendingPath: current });
|
|
95
|
+
}
|
|
96
|
+
if (!isContained(projectReal, linkReal)) {
|
|
97
|
+
throw new DepositContainmentError(`deposit refused: ${current} is a symlink escaping the project tree ` +
|
|
98
|
+
`(resolves to ${linkReal}, outside ${projectReal})`, { projectDir: projectAbs, deftDir: deftAbs, offendingPath: current });
|
|
99
|
+
}
|
|
100
|
+
deepestExistingReal = linkReal;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
deepestExistingReal = realpathSync(current);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Defense-in-depth: the deepest EXISTING ancestor of deftDir must resolve back
|
|
107
|
+
// under the project tree (segment containment, not string startsWith).
|
|
108
|
+
if (!isContained(projectReal, deepestExistingReal)) {
|
|
109
|
+
throw new DepositContainmentError(`deposit refused: deposit path escapes the project tree ` +
|
|
110
|
+
`(${deepestExistingReal} is outside ${projectReal})`, { projectDir: projectAbs, deftDir: deftAbs, offendingPath: deepestExistingReal });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=contain.js.map
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Refs #1942 S1, #1477.
|
|
9
9
|
*/
|
|
10
10
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
11
|
-
import { chmod, mkdir, readdir, stat } from "node:fs/promises";
|
|
11
|
+
import { chmod, lstat, mkdir, readdir, stat } from "node:fs/promises";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import { pipeline } from "node:stream/promises";
|
|
14
14
|
const DEFAULT_FILE_MODE = 0o644;
|
|
@@ -34,7 +34,13 @@ async function copyDirContents(src, dst) {
|
|
|
34
34
|
for (const entry of entries) {
|
|
35
35
|
const srcPath = join(src, entry.name);
|
|
36
36
|
const dstPath = join(dst, entry.name);
|
|
37
|
-
|
|
37
|
+
// #2305: lstat (not stat) so a symlinked entry is NOT followed silently.
|
|
38
|
+
// The deposit copies regular files/dirs only; a symlink in the (trusted)
|
|
39
|
+
// payload is skipped rather than dereferenced.
|
|
40
|
+
const srcStat = await lstat(srcPath);
|
|
41
|
+
if (srcStat.isSymbolicLink()) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
38
44
|
if (srcStat.isDirectory()) {
|
|
39
45
|
await copyDirContents(srcPath, dstPath);
|
|
40
46
|
}
|
package/dist/doctor/checks.d.ts
CHANGED
|
@@ -19,6 +19,17 @@ export declare function checkLegacyLayout(projectRoot: string, seams?: CheckSeam
|
|
|
19
19
|
* been stamped npm-managed. Local-only (no network).
|
|
20
20
|
*/
|
|
21
21
|
export declare function checkCanonicalVendoredNpmSignpost(projectRoot: string, seams?: CheckSeams): CheckResult;
|
|
22
|
+
/**
|
|
23
|
+
* #2294: report whether the located install manifest can surface a version.
|
|
24
|
+
* A legacy `deft-install` deposit made without a release pin writes empty
|
|
25
|
+
* `tag`/`ref` and only a short `sha`, leaving the framework version silently
|
|
26
|
+
* unreportable. Since the Go installer is a frozen legacy bridge (#1912) and
|
|
27
|
+
* the Python/Go rails are retired (#1933/#2022/#2068), the fix is doctor-side:
|
|
28
|
+
* turn the silent blank into a visible, actionable finding. Advisory only --
|
|
29
|
+
* exit-code-exempt like `canonical-vendored-npm-signpost` -- because a sha-only
|
|
30
|
+
* deposit is still a working install, just an unpinned one.
|
|
31
|
+
*/
|
|
32
|
+
export declare function checkManifestVersionReportable(projectRoot: string, installRoot: string | null, seams?: CheckSeams): CheckResult;
|
|
22
33
|
export declare function deriveExitCode(checks: readonly CheckResult[], errors: readonly string[]): number;
|
|
23
34
|
export declare function runChecksImpl(projectRoot: string, seams?: CheckSeams & {
|
|
24
35
|
isDir?: (p: string) => boolean;
|
package/dist/doctor/checks.js
CHANGED
|
@@ -4,7 +4,7 @@ import { detectCanonicalVendoredManifest, isNpmManaged, NPM_MANAGED_SENTINEL_KEY
|
|
|
4
4
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
5
5
|
import { findSkillPathsInText } from "../text/redos-safe.js";
|
|
6
6
|
import { CANONICAL_UPGRADE_COMMAND, GO_BRIDGE_RELEASES_URL, UPGRADING_DOC_URL, } from "./constants.js";
|
|
7
|
-
import { isDeprecationRedirectStub, locateManifest, manifestCandidatePaths, manifestTagToVersion, parseInstallManifest, parseInstallRootFromAgentsMd, parseManifest, } from "./manifest.js";
|
|
7
|
+
import { isDeprecationRedirectStub, locateManifest, manifestCandidatePaths, manifestReportableVersion, manifestTagToVersion, parseInstallManifest, parseInstallRootFromAgentsMd, parseManifest, } from "./manifest.js";
|
|
8
8
|
import { readTextSafe } from "./paths.js";
|
|
9
9
|
function readText(path, seams) {
|
|
10
10
|
return (seams.readText ?? readTextSafe)(path);
|
|
@@ -366,11 +366,82 @@ export function checkCanonicalVendoredNpmSignpost(projectRoot, seams = {}) {
|
|
|
366
366
|
},
|
|
367
367
|
};
|
|
368
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* #2294: report whether the located install manifest can surface a version.
|
|
371
|
+
* A legacy `deft-install` deposit made without a release pin writes empty
|
|
372
|
+
* `tag`/`ref` and only a short `sha`, leaving the framework version silently
|
|
373
|
+
* unreportable. Since the Go installer is a frozen legacy bridge (#1912) and
|
|
374
|
+
* the Python/Go rails are retired (#1933/#2022/#2068), the fix is doctor-side:
|
|
375
|
+
* turn the silent blank into a visible, actionable finding. Advisory only --
|
|
376
|
+
* exit-code-exempt like `canonical-vendored-npm-signpost` -- because a sha-only
|
|
377
|
+
* deposit is still a working install, just an unpinned one.
|
|
378
|
+
*/
|
|
379
|
+
export function checkManifestVersionReportable(projectRoot, installRoot, seams = {}) {
|
|
380
|
+
const isFile = seams.isFile ?? ((p) => readText(p, seams) !== null);
|
|
381
|
+
const manifestPath = locateManifest(projectRoot, installRoot, isFile);
|
|
382
|
+
if (manifestPath === null) {
|
|
383
|
+
return {
|
|
384
|
+
name: "manifest-version-reportable",
|
|
385
|
+
status: "skip",
|
|
386
|
+
detail: "No install manifest found; no framework version to report.",
|
|
387
|
+
data: { manifest_path: null },
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const text = readText(manifestPath, seams);
|
|
391
|
+
if (text === null) {
|
|
392
|
+
return {
|
|
393
|
+
name: "manifest-version-reportable",
|
|
394
|
+
status: "skip",
|
|
395
|
+
detail: `Install manifest at ${manifestPath} is unreadable.`,
|
|
396
|
+
data: { manifest_path: manifestPath },
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const manifest = parseInstallManifest(text);
|
|
400
|
+
const reportable = manifestReportableVersion(manifest);
|
|
401
|
+
if (reportable.version !== null) {
|
|
402
|
+
return {
|
|
403
|
+
name: "manifest-version-reportable",
|
|
404
|
+
status: "pass",
|
|
405
|
+
detail: `Framework version resolves to ${reportable.version} (from manifest ${reportable.source}) at ${manifestPath}.`,
|
|
406
|
+
data: {
|
|
407
|
+
manifest_path: manifestPath,
|
|
408
|
+
version: reportable.version,
|
|
409
|
+
source: reportable.source,
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (reportable.sha !== null) {
|
|
414
|
+
const shortSha = reportable.sha.slice(0, 8);
|
|
415
|
+
return {
|
|
416
|
+
name: "manifest-version-reportable",
|
|
417
|
+
status: "fail",
|
|
418
|
+
detail: `Install manifest at ${manifestPath} carries no semver tag/ref (sha ${shortSha} only), ` +
|
|
419
|
+
"so the framework version is unreportable. This happens on legacy `deft-install` deposits " +
|
|
420
|
+
`made without a release pin (#2294). Run \`${CANONICAL_UPGRADE_COMMAND}\` then \`directive update\` ` +
|
|
421
|
+
`to obtain a pinned npm-managed manifest. See ${UPGRADING_DOC_URL}.`,
|
|
422
|
+
data: {
|
|
423
|
+
manifest_path: manifestPath,
|
|
424
|
+
version: null,
|
|
425
|
+
source: reportable.source,
|
|
426
|
+
sha: reportable.sha,
|
|
427
|
+
upgrade_command: CANONICAL_UPGRADE_COMMAND,
|
|
428
|
+
upgrading_doc_url: UPGRADING_DOC_URL,
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
name: "manifest-version-reportable",
|
|
434
|
+
status: "skip",
|
|
435
|
+
detail: `Install manifest at ${manifestPath} has neither a semver tag/ref nor a sha; no provenance to report.`,
|
|
436
|
+
data: { manifest_path: manifestPath, version: null, sha: null, source: reportable.source },
|
|
437
|
+
};
|
|
438
|
+
}
|
|
369
439
|
export function deriveExitCode(checks, errors) {
|
|
440
|
+
const exitExempt = new Set(["canonical-vendored-npm-signpost", "manifest-version-reportable"]);
|
|
370
441
|
if (errors.length > 0 || checks.some((c) => c.status === "error")) {
|
|
371
442
|
return 2;
|
|
372
443
|
}
|
|
373
|
-
if (checks.some((c) => c.status === "fail" && c.name
|
|
444
|
+
if (checks.some((c) => c.status === "fail" && !exitExempt.has(c.name))) {
|
|
374
445
|
return 1;
|
|
375
446
|
}
|
|
376
447
|
return 0;
|
|
@@ -402,6 +473,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
|
|
|
402
473
|
data: { agents_md_path: agentsMdPath },
|
|
403
474
|
});
|
|
404
475
|
checks.push(checkManifestAgreement(projectRoot, null, seams));
|
|
476
|
+
checks.push(checkManifestVersionReportable(projectRoot, null, seams));
|
|
405
477
|
checks.push(checkLegacyLayout(projectRoot, seams));
|
|
406
478
|
checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
|
|
407
479
|
return {
|
|
@@ -415,6 +487,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
|
|
|
415
487
|
checks.push(checkQuickStartResolves(projectRoot, installRoot, seams));
|
|
416
488
|
checks.push(checkSkillPathsResolve(projectRoot, agentsMdText, seams));
|
|
417
489
|
checks.push(checkManifestAgreement(projectRoot, installRoot, seams));
|
|
490
|
+
checks.push(checkManifestVersionReportable(projectRoot, installRoot, seams));
|
|
418
491
|
checks.push(checkInstallPathConsistency(projectRoot, installRoot, seams));
|
|
419
492
|
checks.push(checkLegacyLayout(projectRoot, seams));
|
|
420
493
|
checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PackageManager } from "../resolution/package-manager.js";
|
|
1
2
|
export declare const UV_INSTALL_URL = "https://docs.astral.sh/uv/";
|
|
2
3
|
export declare const UPGRADING_DOC_URL = "https://github.com/deftai/directive/blob/master/content/UPGRADING.md";
|
|
3
4
|
export declare const GO_BRIDGE_RELEASES_URL = "https://github.com/deftai/directive/releases";
|
|
@@ -18,6 +19,14 @@ export declare const DEFT_REPO_POSITIVE_MARKERS: readonly ["content/templates/ag
|
|
|
18
19
|
export declare const EXPECTED_CONTENT_DIRS: readonly ["languages", "strategies", "skills", "templates"];
|
|
19
20
|
/** Post-freeze canonical upgrade path (#1997 / #2003 / #1912). */
|
|
20
21
|
export declare const CANONICAL_UPGRADE_COMMAND = "npm i -g @deftai/directive@latest";
|
|
22
|
+
/**
|
|
23
|
+
* Render the canonical upgrade one-liner for the active package manager (#2197).
|
|
24
|
+
* Defaults to npm (`CANONICAL_UPGRADE_COMMAND`) so existing callers are
|
|
25
|
+
* unchanged; pass `pnpm` to emit the pnpm form
|
|
26
|
+
* (`pnpm add -g @deftai/directive@latest`). Consumed by the doctor
|
|
27
|
+
* payload-staleness recommendation (`payload-staleness.ts`).
|
|
28
|
+
*/
|
|
29
|
+
export declare function upgradeCommandFor(pm?: PackageManager): string;
|
|
21
30
|
/** Vendored npm-managed deposit: global bump plus in-place `.deft/core/` refresh (#2115). */
|
|
22
31
|
export declare const VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND = "npm i -g @deftai/directive@latest && deft update";
|
|
23
32
|
export declare const CLEAN_WINDOW_HOURS = 24;
|
package/dist/doctor/constants.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ENGINE_PACKAGE, renderGlobalInstall, } from "../resolution/package-manager.js";
|
|
1
2
|
export const UV_INSTALL_URL = "https://docs.astral.sh/uv/";
|
|
2
3
|
// Stable, version-neutral upgrade signposts (#1912). Core principle: never bake
|
|
3
4
|
// the upgrade command/version into the artifact being upgraded -- bake in a
|
|
@@ -60,6 +61,16 @@ export const DEFT_REPO_POSITIVE_MARKERS = [
|
|
|
60
61
|
export const EXPECTED_CONTENT_DIRS = ["languages", "strategies", "skills", "templates"];
|
|
61
62
|
/** Post-freeze canonical upgrade path (#1997 / #2003 / #1912). */
|
|
62
63
|
export const CANONICAL_UPGRADE_COMMAND = "npm i -g @deftai/directive@latest";
|
|
64
|
+
/**
|
|
65
|
+
* Render the canonical upgrade one-liner for the active package manager (#2197).
|
|
66
|
+
* Defaults to npm (`CANONICAL_UPGRADE_COMMAND`) so existing callers are
|
|
67
|
+
* unchanged; pass `pnpm` to emit the pnpm form
|
|
68
|
+
* (`pnpm add -g @deftai/directive@latest`). Consumed by the doctor
|
|
69
|
+
* payload-staleness recommendation (`payload-staleness.ts`).
|
|
70
|
+
*/
|
|
71
|
+
export function upgradeCommandFor(pm = "npm") {
|
|
72
|
+
return renderGlobalInstall(pm, `${ENGINE_PACKAGE}@latest`);
|
|
73
|
+
}
|
|
63
74
|
/** Vendored npm-managed deposit: global bump plus in-place `.deft/core/` refresh (#2115). */
|
|
64
75
|
export const VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND = `${CANONICAL_UPGRADE_COMMAND} && deft update`;
|
|
65
76
|
export const CLEAN_WINDOW_HOURS = 24;
|
package/dist/doctor/main.d.ts
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ResolutionFacts } from "../resolution/index.js";
|
|
2
|
+
import { createPlainSink } from "./output.js";
|
|
3
|
+
import type { DoctorSeams, Finding, ResolutionSummary } from "./types.js";
|
|
2
4
|
export declare function cmdDoctor(args: readonly string[], seams?: DoctorSeams): number;
|
|
5
|
+
/**
|
|
6
|
+
* Never emit a bare `task ...` remediation in a project without Taskfile wiring
|
|
7
|
+
* (#2267). The `directive` surface always works; `task deft:X` is optional and
|
|
8
|
+
* only present when the consumer wired the include. `plan()` already emits the
|
|
9
|
+
* `directive` / `npx` / `npm` surface, so this guard is a defensive invariant:
|
|
10
|
+
* any `task`-prefixed command is rewritten to the `directive` surface unless the
|
|
11
|
+
* project actually has the include.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Detect Taskfile wiring through the injected seam (#2267). Mirrors
|
|
15
|
+
* `classifyTaskfileInclude` but routes the filesystem read through
|
|
16
|
+
* `seams.readText` so the resolution decision stays deterministic + injectable
|
|
17
|
+
* in tests -- unlike a direct `classifyTaskfileInclude(projectRoot)` call, which
|
|
18
|
+
* bypasses every other seam-flowed probe in `runResolutionDecision`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveTaskfileWiring(projectRoot: string, seams: DoctorSeams): boolean;
|
|
21
|
+
export declare function enforceDirectiveSurface(command: string | null, hasTaskfileWiring: boolean): string | null;
|
|
22
|
+
/** Human-facing operating mode derived from the orthogonal fact-set. */
|
|
23
|
+
export declare function resolveOperatingMode(facts: ResolutionFacts): string;
|
|
24
|
+
/**
|
|
25
|
+
* Single engine/pin/VERSION reconciliation line. `reconcileVersions` flags
|
|
26
|
+
* content-behind/ahead and engine-behind; engine-ahead skew is folded in here so
|
|
27
|
+
* the line is the one version-state summary the operator reads.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveReconciliationLine(facts: ResolutionFacts): string;
|
|
30
|
+
/**
|
|
31
|
+
* Probe `.deft/.cli/<platform>` across platforms and report presence + skew. A
|
|
32
|
+
* partial install, or a mix of present + absent platforms, is engine skew: a
|
|
33
|
+
* gate can pass on one platform and fail on another. Read-only.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolvePlatformSkew(projectRoot: string, seams: DoctorSeams): {
|
|
36
|
+
line: string;
|
|
37
|
+
skewDetected: boolean;
|
|
38
|
+
findings: Finding[];
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The single read-only decision surface (#2267). Reuses the shared keystone
|
|
42
|
+
* `classify()` + `plan()` (the ONE classifier) to emit the operating mode, the
|
|
43
|
+
* engine/pin/VERSION reconciliation, cross-platform skew, and exactly ONE
|
|
44
|
+
* primary `Next command:` with a root-cause + remediation rationale. Secondary
|
|
45
|
+
* migration advice (`plan()` warnings) is suppressed until the primary blocker
|
|
46
|
+
* clears. Mutates nothing.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runResolutionDecision(projectRoot: string, jsonMode: boolean, sink: ReturnType<typeof createPlainSink>, addFinding: (f: Finding) => void, seams: DoctorSeams): ResolutionSummary;
|
|
3
49
|
//# sourceMappingURL=main.d.ts.map
|