@deftai/directive-core 0.84.0 → 0.85.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/cache/fetch.d.ts +17 -0
- package/dist/cache/fetch.js +50 -0
- package/dist/doctor/checks.js +16 -1
- package/dist/doctor/constants.d.ts +2 -0
- package/dist/doctor/constants.js +8 -4
- package/dist/doctor/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.js +13 -4
- package/dist/doctor/npm-registry.d.ts +23 -0
- package/dist/doctor/npm-registry.js +128 -0
- package/dist/doctor/npm-view.d.ts +10 -0
- package/dist/doctor/npm-view.js +31 -0
- package/dist/doctor/payload-staleness.js +2 -9
- package/dist/doctor/signpost-checks.d.ts +2 -1
- package/dist/doctor/signpost-checks.js +2 -0
- package/dist/doctor/types.d.ts +9 -0
- package/dist/eval-health-relocation/evaluate.js +15 -1
- package/dist/fs/projection-containment.js +1 -1
- package/dist/hooks/cursor-hooks.d.ts +6 -20
- package/dist/hooks/cursor-hooks.js +7 -92
- package/dist/init-deposit/agent-hooks.d.ts +2 -2
- package/dist/init-deposit/agent-hooks.js +15 -37
- package/dist/init-deposit/hygiene.d.ts +22 -0
- package/dist/init-deposit/hygiene.js +136 -4
- package/dist/init-deposit/legacy-detect.d.ts +25 -1
- package/dist/init-deposit/legacy-detect.js +143 -11
- package/dist/init-deposit/refresh.d.ts +2 -0
- package/dist/init-deposit/refresh.js +18 -5
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +2 -0
- package/dist/policy/org-force-on-migration.d.ts +44 -0
- package/dist/policy/org-force-on-migration.js +240 -0
- package/dist/policy/product-signal.d.ts +3 -1
- package/dist/policy/product-signal.js +15 -4
- package/dist/policy/value-feedback.d.ts +18 -1
- package/dist/policy/value-feedback.js +78 -5
- package/dist/preflight-cache/evaluate.js +12 -1
- package/dist/product-signal/consent-prompt.d.ts +13 -0
- package/dist/product-signal/consent-prompt.js +30 -0
- package/dist/product-signal/submit.js +22 -1
- package/dist/render/index.d.ts +1 -0
- package/dist/render/index.js +1 -0
- package/dist/render/rule-map-template.d.ts +2 -0
- package/dist/render/rule-map-template.js +407 -0
- package/dist/render/rule-map.d.ts +2 -0
- package/dist/render/rule-map.js +703 -0
- package/dist/review-monitor/constants.d.ts +7 -1
- package/dist/review-monitor/constants.js +40 -14
- package/dist/review-monitor/github-lease.d.ts +45 -0
- package/dist/review-monitor/github-lease.js +106 -0
- package/dist/review-monitor/index.d.ts +2 -0
- package/dist/review-monitor/index.js +2 -0
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +152 -0
- package/dist/review-monitor/record.d.ts +61 -13
- package/dist/review-monitor/record.js +379 -132
- package/dist/review-monitor/verify.d.ts +2 -0
- package/dist/review-monitor/verify.js +32 -13
- package/dist/scm/gh-rest.d.ts +7 -1
- package/dist/scm/gh-rest.js +35 -0
- package/dist/session/session-start.js +5 -0
- package/dist/task-surface/index.js +53 -13
- package/dist/xbrief-migrate/constants.d.ts +6 -0
- package/dist/xbrief-migrate/constants.js +6 -0
- package/dist/xbrief-migrate/index.d.ts +2 -2
- package/dist/xbrief-migrate/index.js +2 -2
- package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
- package/dist/xbrief-migrate/migrate-project.js +18 -1
- package/package.json +3 -3
package/dist/cache/fetch.d.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { type RunGhApiFn } from "../scm/gh-rest.js";
|
|
2
|
+
import { type Clock } from "./time.js";
|
|
2
3
|
import type { FetchAllReport, StateRefreshReport } from "./types.js";
|
|
4
|
+
/** Repo-level stamp proving a successful open-only enumeration (#2826). */
|
|
5
|
+
export declare const OPEN_INVENTORY_STAMP = "open-inventory.json";
|
|
6
|
+
export interface OpenInventoryStamp {
|
|
7
|
+
readonly fetched_at: string;
|
|
8
|
+
readonly open_count: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function openInventoryStampPath(cacheRoot: string, source: string, repo: string): string;
|
|
11
|
+
export declare function readOpenInventoryStamp(cacheRoot: string, source: string, repo: string): OpenInventoryStamp | null;
|
|
12
|
+
export declare function writeOpenInventoryStamp(options: {
|
|
13
|
+
cacheRoot: string;
|
|
14
|
+
source: string;
|
|
15
|
+
repo: string;
|
|
16
|
+
openCount: number;
|
|
17
|
+
fetchedAt?: Date;
|
|
18
|
+
clock?: Clock;
|
|
19
|
+
}): void;
|
|
3
20
|
export declare const REST_MAX_PER_PAGE = 100;
|
|
4
21
|
export declare const REST_PAGINATION_MAX_PAGES = 100;
|
|
5
22
|
export declare const DEFAULT_RETRY_AFTER_FALLBACK_S = 60;
|
package/dist/cache/fetch.js
CHANGED
|
@@ -4,9 +4,49 @@ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/project
|
|
|
4
4
|
import { GhRestError, InvalidRepoError, restIssueView, runGhApi, } from "../scm/gh-rest.js";
|
|
5
5
|
import { DEFAULT_BATCH_SIZE, DEFAULT_DELAY_MS } from "./constants.js";
|
|
6
6
|
import { CacheError, CacheFetchError } from "./errors.js";
|
|
7
|
+
import { atomicWriteText } from "./io.js";
|
|
7
8
|
import { pythonJsonLine } from "./json.js";
|
|
8
9
|
import { cachePut, isFresh, validateRepo } from "./operations.js";
|
|
9
10
|
import { entryDir } from "./paths.js";
|
|
11
|
+
import { systemClock, utcIso } from "./time.js";
|
|
12
|
+
/** Repo-level stamp proving a successful open-only enumeration (#2826). */
|
|
13
|
+
export const OPEN_INVENTORY_STAMP = "open-inventory.json";
|
|
14
|
+
export function openInventoryStampPath(cacheRoot, source, repo) {
|
|
15
|
+
const [owner, name] = repo.split("/", 2);
|
|
16
|
+
return join(cacheRoot, source, owner, name, OPEN_INVENTORY_STAMP);
|
|
17
|
+
}
|
|
18
|
+
export function readOpenInventoryStamp(cacheRoot, source, repo) {
|
|
19
|
+
const path = openInventoryStampPath(cacheRoot, source, repo);
|
|
20
|
+
if (!existsSync(path))
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
24
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
25
|
+
return null;
|
|
26
|
+
const fetchedAt = raw.fetched_at;
|
|
27
|
+
const openCount = raw.open_count;
|
|
28
|
+
if (typeof fetchedAt !== "string")
|
|
29
|
+
return null;
|
|
30
|
+
if (typeof openCount !== "number" || openCount !== 0)
|
|
31
|
+
return null;
|
|
32
|
+
return { fetched_at: fetchedAt, open_count: openCount };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function writeOpenInventoryStamp(options) {
|
|
39
|
+
if (options.openCount !== 0)
|
|
40
|
+
return;
|
|
41
|
+
validateRepo(options.repo);
|
|
42
|
+
const clock = options.clock ?? systemClock;
|
|
43
|
+
const fetchedAt = options.fetchedAt ?? clock.now();
|
|
44
|
+
const payload = {
|
|
45
|
+
fetched_at: utcIso(clock, fetchedAt),
|
|
46
|
+
open_count: 0,
|
|
47
|
+
};
|
|
48
|
+
atomicWriteText(openInventoryStampPath(options.cacheRoot, options.source, options.repo), `${JSON.stringify(payload)}\n`);
|
|
49
|
+
}
|
|
10
50
|
export const REST_MAX_PER_PAGE = 100;
|
|
11
51
|
export const REST_PAGINATION_MAX_PAGES = 100;
|
|
12
52
|
export const DEFAULT_RETRY_AFTER_FALLBACK_S = 60;
|
|
@@ -308,6 +348,16 @@ export function runFetchAll(options) {
|
|
|
308
348
|
if (processed % batchSize === 0)
|
|
309
349
|
maybeSleep(delayMs);
|
|
310
350
|
}
|
|
351
|
+
// #2826: zero open issues is a successful inventory answer — persist freshness
|
|
352
|
+
// metadata so preflight-cache does not treat minFetchedAt=null as Infinity stale.
|
|
353
|
+
if (total === 0 && report.issuesFailed === 0) {
|
|
354
|
+
writeOpenInventoryStamp({
|
|
355
|
+
cacheRoot,
|
|
356
|
+
source,
|
|
357
|
+
repo: options.repo,
|
|
358
|
+
openCount: 0,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
311
361
|
return report;
|
|
312
362
|
}
|
|
313
363
|
export function cacheFetchAll(options) {
|
package/dist/doctor/checks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { CANONICAL_GITIGNORE_BASELINE } from "../init-deposit/gitignore.js";
|
|
3
|
-
import { detectLegacyLayout, legacyLayoutSignpostLine, } from "../init-deposit/legacy-detect.js";
|
|
3
|
+
import { detectDualLayout, detectLegacyLayout, dualLayoutSignpostLine, legacyLayoutSignpostLine, } from "../init-deposit/legacy-detect.js";
|
|
4
4
|
import { detectCanonicalVendoredManifest, isNpmManaged, NPM_MANAGED_SENTINEL_KEY, NPM_MANAGED_SENTINEL_VALUE, } from "../init-deposit/migrate.js";
|
|
5
5
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
6
6
|
import { findSkillPathsInText } from "../text/redos-safe.js";
|
|
@@ -300,6 +300,21 @@ export function checkLegacyLayout(projectRoot, seams = {}) {
|
|
|
300
300
|
...(seams.isFile ? { isFile: seams.isFile } : {}),
|
|
301
301
|
...(seams.isDir ? { isDir: seams.isDir } : {}),
|
|
302
302
|
};
|
|
303
|
+
const dualLayout = detectDualLayout(projectRoot, legacySeams);
|
|
304
|
+
if (dualLayout !== null) {
|
|
305
|
+
return {
|
|
306
|
+
name: "legacy-layout",
|
|
307
|
+
status: "fail",
|
|
308
|
+
detail: dualLayoutSignpostLine(dualLayout),
|
|
309
|
+
data: {
|
|
310
|
+
legacy_layout: true,
|
|
311
|
+
legacy_layout_kind: dualLayout.kind,
|
|
312
|
+
evidence: [...dualLayout.evidence],
|
|
313
|
+
upgrading_doc_url: UPGRADING_DOC_URL,
|
|
314
|
+
go_bridge_releases_url: GO_BRIDGE_RELEASES_URL,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
303
318
|
const detection = detectLegacyLayout(projectRoot, legacySeams);
|
|
304
319
|
if (!detection.legacy) {
|
|
305
320
|
return {
|
|
@@ -10,6 +10,8 @@ export declare const TASKFILE_INCLUDE_SNIPPET = "version: '3'\n\nincludes:\n de
|
|
|
10
10
|
export declare const DOCTOR_ALLOWED_FLAGS: readonly ["--session", "--fix", "--repair", "--repair-taskfile", "--json", "--quiet", "--full", "--network", "--project-root", "-h", "--help"];
|
|
11
11
|
/** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
|
|
12
12
|
export declare const NPM_PACKAGE_NAME = "@deftai/directive";
|
|
13
|
+
export declare const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
14
|
+
export declare const NPM_REGISTRY_MIRROR_DOC_URL = "https://github.com/deftai/directive/blob/master/content/UPGRADING.md#corporate-or-mirrored-npm-registry";
|
|
13
15
|
export declare const NETWORK_DISCLOSURE_LINE: string;
|
|
14
16
|
export declare const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE: string;
|
|
15
17
|
export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "vbrief"];
|
package/dist/doctor/constants.js
CHANGED
|
@@ -28,12 +28,16 @@ export const DOCTOR_ALLOWED_FLAGS = [
|
|
|
28
28
|
];
|
|
29
29
|
/** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
|
|
30
30
|
export const NPM_PACKAGE_NAME = "@deftai/directive";
|
|
31
|
+
export const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
32
|
+
export const NPM_REGISTRY_MIRROR_DOC_URL = `${UPGRADING_DOC_URL}#corporate-or-mirrored-npm-registry`;
|
|
31
33
|
// #2182: payload-staleness is the only doctor check that can reach a network
|
|
32
34
|
// endpoint (git ls-remote verifies the pinned ref and `npm view` compares a
|
|
33
|
-
// release-tag install with the latest stable package).
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
35
|
+
// release-tag install with the latest stable package). The #2808 baseline
|
|
36
|
+
// registry-routing check uses only offline `npm config get` reads.
|
|
37
|
+
// Payload-staleness is OFF by default (offline tier) and requires the explicit
|
|
38
|
+
// `--network` flag; this line discloses exactly which tool + registry class it
|
|
39
|
+
// may contact BEFORE the check runs, and the skip line tells an offline run how
|
|
40
|
+
// to opt in.
|
|
37
41
|
export const NETWORK_DISCLOSURE_LINE = "[deft doctor] --network: this run may contact your git remote (framework " +
|
|
38
42
|
"repo host) and the npm registry (registry.npmjs.org) to " +
|
|
39
43
|
`look up the latest ${NPM_PACKAGE_NAME} version.`;
|
package/dist/doctor/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./flags.js";
|
|
|
6
6
|
export * from "./json.js";
|
|
7
7
|
export * from "./main.js";
|
|
8
8
|
export * from "./manifest.js";
|
|
9
|
+
export * from "./npm-registry.js";
|
|
9
10
|
export * from "./paths.js";
|
|
10
11
|
export * from "./payload-staleness.js";
|
|
11
12
|
export * from "./release-availability.js";
|
package/dist/doctor/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./flags.js";
|
|
|
6
6
|
export * from "./json.js";
|
|
7
7
|
export * from "./main.js";
|
|
8
8
|
export * from "./manifest.js";
|
|
9
|
+
export * from "./npm-registry.js";
|
|
9
10
|
export * from "./paths.js";
|
|
10
11
|
export * from "./payload-staleness.js";
|
|
11
12
|
export * from "./release-availability.js";
|
package/dist/doctor/main.js
CHANGED
|
@@ -14,6 +14,7 @@ import { decideThrottle, formatIsoZ, readState, renderDoctorStatusLine, writeSta
|
|
|
14
14
|
import { formatAllowedFlagsHint, formatUnknownFlagsError, parseDoctorFlags } from "./flags.js";
|
|
15
15
|
import { pythonJsonDump } from "./json.js";
|
|
16
16
|
import { parseInstallRootFromAgentsMd } from "./manifest.js";
|
|
17
|
+
import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
|
|
17
18
|
import { createPlainSink } from "./output.js";
|
|
18
19
|
import { readTextSafe, resolveDefaultFrameworkRoot, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
|
|
19
20
|
import { runPayloadStalenessCheck } from "./payload-staleness.js";
|
|
@@ -90,7 +91,7 @@ export function cmdDoctor(args, seams = {}) {
|
|
|
90
91
|
}
|
|
91
92
|
const signpostWarnings = throttleFindings.filter((f) => f.severity === "warning").length;
|
|
92
93
|
if (signpostWarnings > 0 && !jsonMode) {
|
|
93
|
-
throttleSink.finalWarn(`Signpost advisory: ${signpostWarnings} local
|
|
94
|
+
throttleSink.finalWarn(`Signpost advisory: ${signpostWarnings} local configuration / layout note(s) above (throttle-skipped full probe).`);
|
|
94
95
|
}
|
|
95
96
|
return decision.dirty ? 1 : 0;
|
|
96
97
|
}
|
|
@@ -165,12 +166,20 @@ export function cmdDoctor(args, seams = {}) {
|
|
|
165
166
|
}
|
|
166
167
|
sink.info("Checking agent-host hook registration...");
|
|
167
168
|
runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams);
|
|
169
|
+
if (consumerContext) {
|
|
170
|
+
if (!jsonMode) {
|
|
171
|
+
sink.blank();
|
|
172
|
+
}
|
|
173
|
+
sink.info("Checking npm registry routing...");
|
|
174
|
+
runNpmRegistryMirrorCheck(projectRoot, sink, addFinding, seams);
|
|
175
|
+
}
|
|
168
176
|
if (!jsonMode) {
|
|
169
177
|
sink.blank();
|
|
170
178
|
}
|
|
171
|
-
// #2182: payload-staleness is the only doctor check that can reach a
|
|
172
|
-
//
|
|
173
|
-
//
|
|
179
|
+
// #2182: payload-staleness is the only doctor check that can reach a network
|
|
180
|
+
// endpoint (git verifies the pin; npm compares stable release availability).
|
|
181
|
+
// The baseline #2808 registry-routing diagnostic above is an offline
|
|
182
|
+
// `npm config get` read. Payload-staleness stays in the OFFLINE
|
|
174
183
|
// tier (skipped) unless the operator explicitly opts into the NETWORK tier
|
|
175
184
|
// via `--network`, and the tool + registry class is disclosed BEFORE the
|
|
176
185
|
// check runs -- never silently, never as a side effect of a read-only
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OutputSink } from "./output.js";
|
|
2
|
+
import type { Finding, NpmConfigGet, NpmConfigGetResult } from "./types.js";
|
|
3
|
+
/** Seams for the read-only npm registry routing diagnostic. */
|
|
4
|
+
export interface NpmRegistryMirrorSeams {
|
|
5
|
+
readonly runNpmConfigGet?: NpmConfigGet;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Read one npm configuration key without a shell.
|
|
9
|
+
*
|
|
10
|
+
* `npm config get` reads local/user configuration only; it does not resolve a
|
|
11
|
+
* package or contact a registry. Failures are returned explicitly so doctor can
|
|
12
|
+
* skip this advisory without turning a missing npm binary into a hard error.
|
|
13
|
+
*/
|
|
14
|
+
export declare function defaultNpmConfigGet(key: string, cwd: string): NpmConfigGetResult;
|
|
15
|
+
/**
|
|
16
|
+
* Warn when `@deftai/*` resolves through a non-public npm registry.
|
|
17
|
+
*
|
|
18
|
+
* The finding intentionally records only whether the scoped or default key won
|
|
19
|
+
* precedence. It never includes the configured URL, which may contain internal
|
|
20
|
+
* hostnames or credentials.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runNpmRegistryMirrorCheck(projectRoot: string, sink: OutputSink, addFinding: (finding: Finding) => void, seams?: NpmRegistryMirrorSeams): void;
|
|
23
|
+
//# sourceMappingURL=npm-registry.d.ts.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { NPM_REGISTRY_MIRROR_DOC_URL, PUBLIC_NPM_REGISTRY } from "./constants.js";
|
|
3
|
+
const CHECK_NAME = "npm-registry-mirror";
|
|
4
|
+
const DEFT_SCOPE_REGISTRY_KEY = "@deftai:registry";
|
|
5
|
+
const DEFAULT_REGISTRY_KEY = "registry";
|
|
6
|
+
const UNSET_NPM_VALUES = new Set(["", "null", "undefined"]);
|
|
7
|
+
/**
|
|
8
|
+
* Read one npm configuration key without a shell.
|
|
9
|
+
*
|
|
10
|
+
* `npm config get` reads local/user configuration only; it does not resolve a
|
|
11
|
+
* package or contact a registry. Failures are returned explicitly so doctor can
|
|
12
|
+
* skip this advisory without turning a missing npm binary into a hard error.
|
|
13
|
+
*/
|
|
14
|
+
export function defaultNpmConfigGet(key, cwd) {
|
|
15
|
+
try {
|
|
16
|
+
const proc = spawnSync("npm", ["config", "get", key], {
|
|
17
|
+
cwd,
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
shell: false,
|
|
20
|
+
timeout: 5_000,
|
|
21
|
+
windowsHide: true,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
ok: proc.status === 0,
|
|
25
|
+
value: typeof proc.stdout === "string" ? proc.stdout.trim() : "",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { ok: false, value: "" };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function configuredValue(value) {
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
return UNSET_NPM_VALUES.has(trimmed.toLowerCase()) ? null : trimmed;
|
|
35
|
+
}
|
|
36
|
+
function effectiveRegistry(runConfigGet, projectRoot) {
|
|
37
|
+
let scoped;
|
|
38
|
+
try {
|
|
39
|
+
scoped = runConfigGet(DEFT_SCOPE_REGISTRY_KEY, projectRoot);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (!scoped.ok) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const scopedValue = configuredValue(scoped.value);
|
|
48
|
+
if (scopedValue !== null) {
|
|
49
|
+
return { source: "scoped", value: scopedValue };
|
|
50
|
+
}
|
|
51
|
+
let fallback;
|
|
52
|
+
try {
|
|
53
|
+
fallback = runConfigGet(DEFAULT_REGISTRY_KEY, projectRoot);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (!fallback.ok) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const fallbackValue = configuredValue(fallback.value);
|
|
62
|
+
return fallbackValue === null ? null : { source: "default", value: fallbackValue };
|
|
63
|
+
}
|
|
64
|
+
function isValidRegistryUrl(value) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = new URL(value);
|
|
67
|
+
return (parsed.protocol === "https:" || parsed.protocol === "http:") && parsed.hostname !== "";
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function isCanonicalPublicRegistry(value) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = new URL(value);
|
|
76
|
+
return (parsed.protocol === "https:" &&
|
|
77
|
+
parsed.hostname === "registry.npmjs.org" &&
|
|
78
|
+
parsed.port === "" &&
|
|
79
|
+
(parsed.pathname === "" || parsed.pathname === "/") &&
|
|
80
|
+
parsed.username === "" &&
|
|
81
|
+
parsed.password === "" &&
|
|
82
|
+
parsed.search === "" &&
|
|
83
|
+
parsed.hash === "");
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function skipFinding(addFinding) {
|
|
90
|
+
addFinding({
|
|
91
|
+
severity: "skip",
|
|
92
|
+
message: `${CHECK_NAME}: skip -- npm registry configuration is unavailable or invalid`,
|
|
93
|
+
check: CHECK_NAME,
|
|
94
|
+
status: "skip",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Warn when `@deftai/*` resolves through a non-public npm registry.
|
|
99
|
+
*
|
|
100
|
+
* The finding intentionally records only whether the scoped or default key won
|
|
101
|
+
* precedence. It never includes the configured URL, which may contain internal
|
|
102
|
+
* hostnames or credentials.
|
|
103
|
+
*/
|
|
104
|
+
export function runNpmRegistryMirrorCheck(projectRoot, sink, addFinding, seams = {}) {
|
|
105
|
+
const selected = effectiveRegistry(seams.runNpmConfigGet ?? defaultNpmConfigGet, projectRoot);
|
|
106
|
+
if (selected === null || !isValidRegistryUrl(selected.value)) {
|
|
107
|
+
skipFinding(addFinding);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (isCanonicalPublicRegistry(selected.value)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const message = `${CHECK_NAME}: @deftai packages resolve through a non-public npm registry. ` +
|
|
114
|
+
"Corporate mirrors can return E404/ETARGET or silently serve stale @latest metadata. " +
|
|
115
|
+
`Where organization policy permits, retry with \`npm i -g @deftai/directive@latest --registry=${PUBLIC_NPM_REGISTRY}\` ` +
|
|
116
|
+
`or add \`@deftai:registry=${PUBLIC_NPM_REGISTRY}\` to \`.npmrc\`. ` +
|
|
117
|
+
`Otherwise ask your registry administrator to sync the @deftai packages. See ${NPM_REGISTRY_MIRROR_DOC_URL}.`;
|
|
118
|
+
sink.warn(message);
|
|
119
|
+
addFinding({
|
|
120
|
+
severity: "warning",
|
|
121
|
+
message,
|
|
122
|
+
check: CHECK_NAME,
|
|
123
|
+
status: "non-public",
|
|
124
|
+
registry_source: selected.source,
|
|
125
|
+
suggestion: NPM_REGISTRY_MIRROR_DOC_URL,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=npm-registry.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface NpmViewVersionResult {
|
|
2
|
+
readonly ok: boolean;
|
|
3
|
+
readonly version: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Query the canonical public registry for the latest published Directive
|
|
7
|
+
* version, independent of the consumer's configured corporate mirror.
|
|
8
|
+
*/
|
|
9
|
+
export declare function defaultNpmViewVersion(): NpmViewVersionResult;
|
|
10
|
+
//# sourceMappingURL=npm-view.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { NPM_PACKAGE_NAME, PUBLIC_NPM_REGISTRY } from "./constants.js";
|
|
3
|
+
/**
|
|
4
|
+
* Query the canonical public registry for the latest published Directive
|
|
5
|
+
* version, independent of the consumer's configured corporate mirror.
|
|
6
|
+
*/
|
|
7
|
+
export function defaultNpmViewVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const proc = spawnSync("npm", [
|
|
10
|
+
"view",
|
|
11
|
+
NPM_PACKAGE_NAME,
|
|
12
|
+
"version",
|
|
13
|
+
`--registry=${PUBLIC_NPM_REGISTRY}`,
|
|
14
|
+
"--ignore-scripts",
|
|
15
|
+
], {
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
shell: false,
|
|
18
|
+
timeout: 15_000,
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
});
|
|
21
|
+
if (proc.error !== undefined || proc.status !== 0) {
|
|
22
|
+
return { ok: false, version: "" };
|
|
23
|
+
}
|
|
24
|
+
const version = (typeof proc.stdout === "string" ? proc.stdout : "").trim().split("\n")[0]?.trim() ?? "";
|
|
25
|
+
return { ok: version.length > 0, version };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { ok: false, version: "" };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=npm-view.js.map
|
|
@@ -2,8 +2,9 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { detectCanonicalVendoredManifest, isNpmManaged } from "../init-deposit/migrate.js";
|
|
4
4
|
import { isPublishable } from "../release/version.js";
|
|
5
|
-
import { CANONICAL_UPGRADE_COMMAND,
|
|
5
|
+
import { CANONICAL_UPGRADE_COMMAND, upgradeCommandFor, VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND, } from "./constants.js";
|
|
6
6
|
import { locateManifest, parseInstallManifest } from "./manifest.js";
|
|
7
|
+
import { defaultNpmViewVersion } from "./npm-view.js";
|
|
7
8
|
import { readTextSafe, resolveDefaultFrameworkRoot } from "./paths.js";
|
|
8
9
|
import { evaluateReleaseAvailability } from "./release-availability.js";
|
|
9
10
|
function isDeftFrameworkRepo(projectRoot, readText = readTextSafe) {
|
|
@@ -40,14 +41,6 @@ function parseRemoteSha(stdout) {
|
|
|
40
41
|
const firstLine = stdout.split("\n").find((ln) => ln.trim()) ?? "";
|
|
41
42
|
return firstLine.trim().split(/\s+/)[0] ?? "";
|
|
42
43
|
}
|
|
43
|
-
function defaultNpmViewVersion() {
|
|
44
|
-
const proc = spawnSync("npm", ["view", NPM_PACKAGE_NAME, "version"], {
|
|
45
|
-
encoding: "utf8",
|
|
46
|
-
timeout: 15_000,
|
|
47
|
-
});
|
|
48
|
-
const version = (proc.stdout ?? "").trim().split("\n")[0]?.trim() ?? "";
|
|
49
|
-
return { ok: proc.status === 0 && version.length > 0, version };
|
|
50
|
-
}
|
|
51
44
|
function emitReleaseAvailable(checkName, installedVersion, latestVersion, ref, upgradeCommand, sink, addFinding) {
|
|
52
45
|
const msg = `Newer framework release available (installed v${installedVersion}; ` +
|
|
53
46
|
`latest v${latestVersion} from npm registry). ` +
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type CheckSeams } from "./checks.js";
|
|
2
|
+
import { type NpmRegistryMirrorSeams } from "./npm-registry.js";
|
|
2
3
|
import type { OutputSink } from "./output.js";
|
|
3
4
|
import type { Finding } from "./types.js";
|
|
4
|
-
export interface LocalSignpostSeams extends CheckSeams {
|
|
5
|
+
export interface LocalSignpostSeams extends CheckSeams, NpmRegistryMirrorSeams {
|
|
5
6
|
readonly runningInsideDeftRepo?: (root: string) => boolean;
|
|
6
7
|
}
|
|
7
8
|
/** Lightweight, local-only signpost probes for the throttle-skip path (#1997). */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { checkCanonicalVendoredNpmSignpost, checkLegacyLayout } from "./checks.js";
|
|
2
|
+
import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
|
|
2
3
|
import { runningInsideDeftRepo } from "./paths.js";
|
|
3
4
|
/** Lightweight, local-only signpost probes for the throttle-skip path (#1997). */
|
|
4
5
|
export function runLocalSignpostChecks(projectRoot, sink, addFinding, seams = {}) {
|
|
@@ -6,6 +7,7 @@ export function runLocalSignpostChecks(projectRoot, sink, addFinding, seams = {}
|
|
|
6
7
|
if (insideDeft) {
|
|
7
8
|
return;
|
|
8
9
|
}
|
|
10
|
+
runNpmRegistryMirrorCheck(projectRoot, sink, addFinding, seams);
|
|
9
11
|
for (const result of [
|
|
10
12
|
checkLegacyLayout(projectRoot, seams),
|
|
11
13
|
checkCanonicalVendoredNpmSignpost(projectRoot, seams),
|
package/dist/doctor/types.d.ts
CHANGED
|
@@ -30,6 +30,13 @@ export interface Finding {
|
|
|
30
30
|
readonly status?: string;
|
|
31
31
|
readonly [key: string]: unknown;
|
|
32
32
|
}
|
|
33
|
+
/** Result of an offline `npm config get` query. */
|
|
34
|
+
export interface NpmConfigGetResult {
|
|
35
|
+
readonly ok: boolean;
|
|
36
|
+
readonly value: string;
|
|
37
|
+
}
|
|
38
|
+
/** Injectable npm-config reader used by doctor and its throttle-safe signposts. */
|
|
39
|
+
export type NpmConfigGet = (key: string, cwd: string) => NpmConfigGetResult;
|
|
33
40
|
/**
|
|
34
41
|
* The single read-only decision surface derived from the shared keystone
|
|
35
42
|
* `plan()` (#2267 / epic #2203). `plan()` is the ONE classifier: doctor never
|
|
@@ -99,6 +106,8 @@ export interface DoctorSeams {
|
|
|
99
106
|
ok: boolean;
|
|
100
107
|
version: string;
|
|
101
108
|
};
|
|
109
|
+
/** Offline npm configuration reader for effective-registry diagnostics (#2808). */
|
|
110
|
+
readonly runNpmConfigGet?: NpmConfigGet;
|
|
102
111
|
readonly agentsRefreshPlan?: (projectRoot: string) => Record<string, unknown>;
|
|
103
112
|
readonly agentsMdAdvisoryEvaluate?: (projectRoot: string) => AdvisoryEvaluateResult;
|
|
104
113
|
readonly readState?: (projectRoot: string) => DoctorState | null;
|
|
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { evaluateHealth } from "../eval/health.js";
|
|
5
|
+
import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
5
6
|
import { resolveEvalPath } from "../layout/resolve.js";
|
|
6
7
|
import { matchAny } from "../orchestration/pathspec.js";
|
|
7
8
|
/** Glob patterns that classify a change as epic #2369 rule-relocation (#2373). */
|
|
@@ -57,6 +58,7 @@ export function readHealthBaseline(projectRoot) {
|
|
|
57
58
|
/** Persist a health report as the committed baseline (#2373 Wave 2 seed). */
|
|
58
59
|
export function writeHealthBaseline(projectRoot, report) {
|
|
59
60
|
const path = healthBaselinePath(projectRoot);
|
|
61
|
+
assertWriteTargetSafe(projectRoot, path);
|
|
60
62
|
mkdirSync(dirname(path), { recursive: true });
|
|
61
63
|
writeFileSync(path, `${JSON.stringify(report)}\n`, "utf8");
|
|
62
64
|
}
|
|
@@ -170,7 +172,19 @@ export function evaluate(options = {}) {
|
|
|
170
172
|
stream: "stderr",
|
|
171
173
|
};
|
|
172
174
|
}
|
|
173
|
-
|
|
175
|
+
try {
|
|
176
|
+
writeHealthBaseline(projectRoot, report);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
if (err instanceof ProjectionContainmentError) {
|
|
180
|
+
return {
|
|
181
|
+
code: 2,
|
|
182
|
+
message: `❌ verify:eval-health-relocation: ${err.message}`,
|
|
183
|
+
stream: "stderr",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
174
188
|
return {
|
|
175
189
|
code: 0,
|
|
176
190
|
message: `✓ verify:eval-health-relocation: seeded baseline at ${HEALTH_BASELINE_REL} ` +
|
|
@@ -91,7 +91,7 @@ export function assertProjectionContained(projectDir, targetPath) {
|
|
|
91
91
|
try {
|
|
92
92
|
deepestExistingReal = realpathSync(current);
|
|
93
93
|
}
|
|
94
|
-
catch
|
|
94
|
+
catch {
|
|
95
95
|
throw new ProjectionContainmentError(`projection write refused: could not resolve ${current} on the projection path`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: current });
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
/** Cursor ApplyPatch
|
|
1
|
+
/** Cursor's ApplyPatch payload is parsed directly by the shared hook executable. */
|
|
2
2
|
export declare const APPLY_PATCH_TOOL_NAMES: readonly ["ApplyPatch", "apply_patch"];
|
|
3
3
|
export declare const APPLY_PATCH_HOOK_MATCHER: string;
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export declare const
|
|
9
|
-
export declare const CURSOR_APPLY_PATCH_ADAPTER_COMMAND = "node .cursor/hooks/deft-cursor-hook-adapter.mjs ApplyPatch";
|
|
10
|
-
/** Deposited adapter forwards free-form ApplyPatch stdin to hook:dispatch with explicit project root. */
|
|
11
|
-
export declare const CURSOR_APPLY_PATCH_ADAPTER_SOURCE = "#!/usr/bin/env node\nimport { spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nconst projectRoot = resolve(process.cwd());\nconst stdin = readFileSync(0, \"utf8\");\n\nfunction deftCommand() {\n for (const candidate of [\"deft\", \"directive\"]) {\n const probe = spawnSync(candidate, [\"--version\"], {\n encoding: \"utf8\",\n stdio: \"ignore\",\n shell: process.platform === \"win32\",\n windowsHide: true,\n });\n if (probe.error === undefined && probe.status === 0) return candidate;\n }\n process.stderr.write(\n \"Directive ApplyPatch adapter: neither deft nor directive is on PATH.\\n\",\n );\n process.exit(2);\n}\n\nconst cli = deftCommand();\nconst result = spawnSync(\n cli,\n [\n \"hook:dispatch\",\n \"--host\",\n \"cursor\",\n \"--event\",\n \"tool.before\",\n \"--project-root\",\n projectRoot,\n ],\n {\n input: stdin,\n encoding: \"utf8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n shell: process.platform === \"win32\",\n windowsHide: true,\n },\n);\n\nif (result.stderr) process.stderr.write(result.stderr);\nif (result.error) {\n process.stderr.write(String(result.error));\n process.exit(2);\n}\nif (result.status !== 0 && result.status !== null) {\n if (result.stdout) process.stdout.write(result.stdout);\n process.exit(result.status);\n}\n// Cursor failClosed treats empty stdout as failure \u2014 normalize allow.\nconst out = (result.stdout ?? \"\").trim();\nprocess.stdout.write((out.length > 0 ? out : '{\"permission\":\"allow\"}') + \"\\n\");\nprocess.exit(0);\n";
|
|
12
|
-
export interface CursorPreToolUseEntry {
|
|
13
|
-
readonly command: string;
|
|
14
|
-
readonly matcher?: string;
|
|
15
|
-
readonly failClosed?: boolean;
|
|
16
|
-
readonly timeout?: number;
|
|
17
|
-
}
|
|
18
|
-
export declare function cursorApplyPatchAdapterEntry(): CursorPreToolUseEntry;
|
|
19
|
-
/** True when generic and adapter matchers share no tool tokens. */
|
|
20
|
-
export declare function cursorApplyPatchMatchersDisjoint(): boolean;
|
|
21
|
-
/** Fail closed when Cursor hook projection would double-dispatch ApplyPatch (#2764). */
|
|
22
|
-
export declare function assertCursorApplyPatchMatchersDisjoint(): void;
|
|
4
|
+
/**
|
|
5
|
+
* Cursor uses one disjoint direct-write registration: hook-dispatch recognizes
|
|
6
|
+
* JSON and free-form ApplyPatch payloads, avoiding an adapter process (#2790).
|
|
7
|
+
*/
|
|
8
|
+
export declare const CURSOR_DIRECT_WRITE_HOOK_MATCHER: string;
|
|
23
9
|
//# sourceMappingURL=cursor-hooks.d.ts.map
|