@deftai/directive 0.119.7 → 0.119.9
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/cli-package-version.d.ts +3 -0
- package/dist/cli-package-version.js +23 -0
- package/dist/cli-router/route-argv.js +6 -0
- package/dist/dispatch.d.ts +2 -2
- package/dist/dispatch.js +27 -3
- package/dist/identity-banner.d.ts +13 -0
- package/dist/identity-banner.js +19 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -5
- package/dist/policy.d.ts +1 -1
- package/dist/policy.js +69 -15
- package/dist/preflight-gh.d.ts +1 -0
- package/dist/preflight-gh.js +16 -74
- package/dist/scope-record-intent-constraint.d.ts +1 -0
- package/dist/scope-record-intent-constraint.js +28 -4
- package/dist/scope-record-observable-scope.d.ts +1 -0
- package/dist/scope-record-observable-scope.js +28 -4
- package/dist/verify-class-checks.d.ts +11 -0
- package/dist/verify-class-checks.js +71 -0
- package/dist/verify-review-monitor.d.ts +5 -0
- package/dist/verify-review-monitor.js +82 -2
- package/dist/verify-session-ritual.js +5 -3
- package/dist/windows-bin-ps1.d.ts +39 -0
- package/dist/windows-bin-ps1.js +224 -0
- package/package.json +7 -5
- package/scripts/remove-windows-ps1-shims.mjs +26 -0
|
@@ -11,12 +11,18 @@ import { buildObservableScopeRecord, extractObservableChangeFromPlan, parseObser
|
|
|
11
11
|
import { isDirectEntrypoint } from "./entrypoint.js";
|
|
12
12
|
import { refuseMintWhileUatActive, refuseNonInteractiveMint, resolveHumanPresenceMintSeams, } from "./human-presence-mint.js";
|
|
13
13
|
import { isPathInsideRoot } from "./scope-record-approved-scope.js";
|
|
14
|
+
const DEFAULT_MINTED_VIA = "scope:record-observable-scope";
|
|
15
|
+
const ALLOWED_MINTED_VIA = new Set([DEFAULT_MINTED_VIA, "in-harness-ask"]);
|
|
14
16
|
function usage() {
|
|
15
17
|
return ("usage: scope:record-observable-scope -- <xbrief-path> --actor <name> --confirm " +
|
|
16
|
-
"[--kind operator] [--
|
|
18
|
+
"[--kind operator] [--minted-via in-harness-ask|scope:record-observable-scope] " +
|
|
19
|
+
"[--project-root <dir>] [--quiet]\n" +
|
|
17
20
|
' Writes .deft/observable-scope/<plan-id>.json from plan["x-directive/observableChange"].\n' +
|
|
18
|
-
"
|
|
19
|
-
"
|
|
21
|
+
" Attended mid-build (#5010): ask in parent chat; on yes the human runs this verb on a real TTY " +
|
|
22
|
+
"with --minted-via=in-harness-ask (attestation is not an agent-shell bypass). Commit the record on " +
|
|
23
|
+
"the delivery-branch merge base, then rebase the implementation branch — same-PR working-tree-only " +
|
|
24
|
+
"records are not authoritative. Unattended/C1: rewrite or park. --actor is display only. Mint still " +
|
|
25
|
+
"requires a real TTY, controlling terminal, --confirm, and typed phrase mint (#3110). Agent/CI shells refuse.");
|
|
20
26
|
}
|
|
21
27
|
function extractPlanId(payload) {
|
|
22
28
|
if (payload === null || typeof payload !== "object" || Array.isArray(payload))
|
|
@@ -33,6 +39,7 @@ export function parseArgs(argv) {
|
|
|
33
39
|
xbriefPath: "",
|
|
34
40
|
actor: "",
|
|
35
41
|
kind: "operator",
|
|
42
|
+
mintedVia: DEFAULT_MINTED_VIA,
|
|
36
43
|
quiet: false,
|
|
37
44
|
confirm: false,
|
|
38
45
|
};
|
|
@@ -77,6 +84,16 @@ export function parseArgs(argv) {
|
|
|
77
84
|
else if (arg?.startsWith("--kind=")) {
|
|
78
85
|
parsed.kind = arg.slice("--kind=".length);
|
|
79
86
|
}
|
|
87
|
+
else if (arg === "--minted-via") {
|
|
88
|
+
const value = argv[i + 1];
|
|
89
|
+
if (value === undefined)
|
|
90
|
+
return { ...parsed, error: "argument --minted-via: expected one argument" };
|
|
91
|
+
parsed.mintedVia = value;
|
|
92
|
+
i += 1;
|
|
93
|
+
}
|
|
94
|
+
else if (arg?.startsWith("--minted-via=")) {
|
|
95
|
+
parsed.mintedVia = arg.slice("--minted-via=".length);
|
|
96
|
+
}
|
|
80
97
|
else if (arg?.startsWith("-")) {
|
|
81
98
|
return { ...parsed, error: `unrecognized argument: ${arg}` };
|
|
82
99
|
}
|
|
@@ -98,6 +115,13 @@ export function parseArgs(argv) {
|
|
|
98
115
|
error: `argument --actor is required (human operator identity)\n${usage()}`,
|
|
99
116
|
};
|
|
100
117
|
}
|
|
118
|
+
if (!ALLOWED_MINTED_VIA.has(parsed.mintedVia.trim())) {
|
|
119
|
+
return {
|
|
120
|
+
...parsed,
|
|
121
|
+
error: `argument --minted-via must be one of: ${[...ALLOWED_MINTED_VIA].join(", ")}\n${usage()}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
parsed.mintedVia = parsed.mintedVia.trim();
|
|
101
125
|
return parsed;
|
|
102
126
|
}
|
|
103
127
|
export function run(argv, seams = {}) {
|
|
@@ -161,7 +185,7 @@ export function run(argv, seams = {}) {
|
|
|
161
185
|
kind: args.kind,
|
|
162
186
|
actor: args.actor,
|
|
163
187
|
mintedAt: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
164
|
-
mintedVia:
|
|
188
|
+
mintedVia: args.mintedVia,
|
|
165
189
|
},
|
|
166
190
|
});
|
|
167
191
|
if ("error" in rec) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
interface ParsedArgs {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
baseRef: string | null;
|
|
5
|
+
quiet: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
9
|
+
export declare function run(argv: string[]): number;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=verify-class-checks.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI for verify:class-checks (#4980 / #3145 class).
|
|
4
|
+
*/
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { classChecks } from "@deftai/directive-core";
|
|
7
|
+
import { isDirectEntrypoint } from "./entrypoint.js";
|
|
8
|
+
export function parseArgs(argv) {
|
|
9
|
+
const parsed = {
|
|
10
|
+
projectRoot: ".",
|
|
11
|
+
baseRef: null,
|
|
12
|
+
quiet: false,
|
|
13
|
+
};
|
|
14
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
15
|
+
const arg = argv[i];
|
|
16
|
+
if (arg === "--quiet") {
|
|
17
|
+
parsed.quiet = true;
|
|
18
|
+
}
|
|
19
|
+
else if (arg === "--project-root") {
|
|
20
|
+
const value = argv[i + 1];
|
|
21
|
+
if (value === undefined) {
|
|
22
|
+
return { ...parsed, error: "argument --project-root: expected one argument" };
|
|
23
|
+
}
|
|
24
|
+
parsed.projectRoot = value;
|
|
25
|
+
i += 1;
|
|
26
|
+
}
|
|
27
|
+
else if (arg?.startsWith("--project-root=")) {
|
|
28
|
+
parsed.projectRoot = arg.slice("--project-root=".length);
|
|
29
|
+
}
|
|
30
|
+
else if (arg === "--base-ref") {
|
|
31
|
+
const value = argv[i + 1];
|
|
32
|
+
if (value === undefined) {
|
|
33
|
+
return { ...parsed, error: "argument --base-ref: expected one argument" };
|
|
34
|
+
}
|
|
35
|
+
parsed.baseRef = value;
|
|
36
|
+
i += 1;
|
|
37
|
+
}
|
|
38
|
+
else if (arg?.startsWith("--base-ref=")) {
|
|
39
|
+
parsed.baseRef = arg.slice("--base-ref=".length);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return { ...parsed, error: `unrecognized argument: ${arg}` };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
export function run(argv) {
|
|
48
|
+
const args = parseArgs(argv);
|
|
49
|
+
if (args.error !== undefined) {
|
|
50
|
+
process.stderr.write(`verify_class_checks: ${args.error}\n`);
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
const projectRoot = resolve(args.projectRoot);
|
|
54
|
+
const result = classChecks.evaluateClassChecks(projectRoot, {
|
|
55
|
+
baseRef: args.baseRef,
|
|
56
|
+
quiet: args.quiet,
|
|
57
|
+
});
|
|
58
|
+
if (result.exitCode === 0) {
|
|
59
|
+
if (!args.quiet) {
|
|
60
|
+
process.stdout.write(`${result.message}\n`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
process.stderr.write(`${result.message}\n`);
|
|
65
|
+
}
|
|
66
|
+
return result.exitCode;
|
|
67
|
+
}
|
|
68
|
+
if (isDirectEntrypoint(import.meta.url)) {
|
|
69
|
+
process.exit(run(process.argv.slice(2)));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=verify-class-checks.js.map
|
|
@@ -9,6 +9,11 @@ interface ParsedArgs {
|
|
|
9
9
|
approach3: boolean;
|
|
10
10
|
approach3Warned: boolean;
|
|
11
11
|
emitJson: boolean;
|
|
12
|
+
/** When true, also run the #4882 merge-path arm observer (attested flags). */
|
|
13
|
+
mergePathArm: boolean;
|
|
14
|
+
liveWait: boolean;
|
|
15
|
+
explicitFinish: boolean;
|
|
16
|
+
stickyLease: boolean;
|
|
12
17
|
help: boolean;
|
|
13
18
|
error?: string;
|
|
14
19
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import { evaluateMergePathArm, } from "@deftai/directive-core/dist/pr-watch/main.js";
|
|
5
|
+
import { EXIT_CONFIG_ERROR, EXIT_NOT_READY, evaluateReviewMonitorGate, isTier1, REVIEW_MONITOR_HELP, verifyResultToJson, } from "@deftai/directive-core/review-monitor";
|
|
5
6
|
const CALL_SITES = new Set([
|
|
6
7
|
"solo",
|
|
7
8
|
"swarm-phase5-6",
|
|
@@ -18,6 +19,10 @@ export function parseVerifyReviewMonitorArgs(argv) {
|
|
|
18
19
|
approach3: false,
|
|
19
20
|
approach3Warned: false,
|
|
20
21
|
emitJson: false,
|
|
22
|
+
mergePathArm: false,
|
|
23
|
+
liveWait: false,
|
|
24
|
+
explicitFinish: false,
|
|
25
|
+
stickyLease: false,
|
|
21
26
|
help: false,
|
|
22
27
|
};
|
|
23
28
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -28,6 +33,18 @@ export function parseVerifyReviewMonitorArgs(argv) {
|
|
|
28
33
|
if (arg === "--json") {
|
|
29
34
|
acc.emitJson = true;
|
|
30
35
|
}
|
|
36
|
+
else if (arg === "--merge-path-arm") {
|
|
37
|
+
acc.mergePathArm = true;
|
|
38
|
+
}
|
|
39
|
+
else if (arg === "--live-wait") {
|
|
40
|
+
acc.liveWait = true;
|
|
41
|
+
}
|
|
42
|
+
else if (arg === "--explicit-finish") {
|
|
43
|
+
acc.explicitFinish = true;
|
|
44
|
+
}
|
|
45
|
+
else if (arg === "--sticky-lease") {
|
|
46
|
+
acc.stickyLease = true;
|
|
47
|
+
}
|
|
31
48
|
else if (arg === "--approach3") {
|
|
32
49
|
acc.approach3 = true;
|
|
33
50
|
}
|
|
@@ -117,6 +134,13 @@ export function run(argv) {
|
|
|
117
134
|
const args = parseVerifyReviewMonitorArgs(argv);
|
|
118
135
|
if (args.help) {
|
|
119
136
|
process.stdout.write(REVIEW_MONITOR_HELP);
|
|
137
|
+
process.stdout.write("\n#4882 merge-path arm observer (optional):\n" +
|
|
138
|
+
" --merge-path-arm Fail closed when neither live wait nor explicit finish\n" +
|
|
139
|
+
" --live-wait Attest a still-running phase-correct wait for this PR\n" +
|
|
140
|
+
" (Tier 1: bound to gate lease evidence for --pr)\n" +
|
|
141
|
+
" --explicit-finish Attest option-C BLOCKED/FAILED finish for this PR\n" +
|
|
142
|
+
" --sticky-lease Attest a fresh sticky lease (not sufficient alone)\n" +
|
|
143
|
+
" Prefer Approach 1 / native pr:watch; homemade line-parsed --json is not an arm.\n");
|
|
120
144
|
return 0;
|
|
121
145
|
}
|
|
122
146
|
if (args.error !== undefined) {
|
|
@@ -129,6 +153,8 @@ export function run(argv) {
|
|
|
129
153
|
process.stderr.write("Try: task verify:review-monitor -- --help\n");
|
|
130
154
|
return 2;
|
|
131
155
|
}
|
|
156
|
+
// Always evaluate the gate first so --project-root / repo config errors stay
|
|
157
|
+
// exit 2 even when --merge-path-arm would otherwise fail closed as unarmed.
|
|
132
158
|
const result = evaluateReviewMonitorGate({
|
|
133
159
|
pr: args.pr,
|
|
134
160
|
projectRoot: resolve(args.projectRoot),
|
|
@@ -139,15 +165,69 @@ export function run(argv) {
|
|
|
139
165
|
approach3Warned: args.approach3Warned,
|
|
140
166
|
environ: process.env,
|
|
141
167
|
});
|
|
168
|
+
let arm = null;
|
|
169
|
+
if (args.mergePathArm) {
|
|
170
|
+
// Bind --live-wait to gate-observed lease evidence on Tier 1 for this PR.
|
|
171
|
+
// Bare flags must not arm when Tier 1 requires a lease and none is present.
|
|
172
|
+
const leaseEvidence = result.monitorRecord !== null;
|
|
173
|
+
const liveBound = args.liveWait && (!isTier1(result.tier) || leaseEvidence);
|
|
174
|
+
arm = evaluateMergePathArm({
|
|
175
|
+
livePhaseCorrectWait: liveBound,
|
|
176
|
+
explicitFinish: args.explicitFinish,
|
|
177
|
+
stickyLeaseActive: args.stickyLease || leaseEvidence,
|
|
178
|
+
});
|
|
179
|
+
if (args.liveWait && !liveBound && !args.explicitFinish && !arm.armed) {
|
|
180
|
+
arm = {
|
|
181
|
+
armed: false,
|
|
182
|
+
reason: "unarmed_stand_down",
|
|
183
|
+
message: `unarmed stand-down: --live-wait attestation unbound to lease evidence ` +
|
|
184
|
+
`for PR #${args.pr} (Tier 1); sticky lease alone is not a live arm (#4882)`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
142
188
|
if (args.emitJson) {
|
|
143
|
-
|
|
189
|
+
const payload = verifyResultToJson(result);
|
|
190
|
+
if (arm !== null) {
|
|
191
|
+
payload.merge_path_arm = {
|
|
192
|
+
armed: arm.armed,
|
|
193
|
+
reason: arm.reason,
|
|
194
|
+
message: arm.message,
|
|
195
|
+
live_wait: args.liveWait,
|
|
196
|
+
explicit_finish: args.explicitFinish,
|
|
197
|
+
sticky_lease: args.stickyLease,
|
|
198
|
+
lease_evidence: result.monitorRecord !== null,
|
|
199
|
+
};
|
|
200
|
+
// Combined gate+arm: unarmed fails closed even when the monitor gate is ready.
|
|
201
|
+
if (!arm.armed && result.exitCode !== EXIT_CONFIG_ERROR) {
|
|
202
|
+
payload.ready = false;
|
|
203
|
+
payload.exit_code = EXIT_NOT_READY;
|
|
204
|
+
// Gate-ready → arm diagnosis; both failing → keep both messages.
|
|
205
|
+
payload.message = result.exitCode === 0 ? arm.message : `${result.message}\n${arm.message}`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
209
|
+
}
|
|
210
|
+
else if (result.exitCode === EXIT_CONFIG_ERROR) {
|
|
211
|
+
process.stderr.write(`${result.message}\n`);
|
|
212
|
+
}
|
|
213
|
+
else if (arm !== null && !arm.armed) {
|
|
214
|
+
process.stderr.write(`${arm.message}\n`);
|
|
144
215
|
}
|
|
145
216
|
else if (result.exitCode === 0) {
|
|
146
217
|
process.stdout.write(`${result.message}\n`);
|
|
218
|
+
if (arm !== null) {
|
|
219
|
+
process.stdout.write(`${arm.message}\n`);
|
|
220
|
+
}
|
|
147
221
|
}
|
|
148
222
|
else {
|
|
149
223
|
process.stderr.write(`${result.message}\n`);
|
|
150
224
|
}
|
|
225
|
+
if (result.exitCode === EXIT_CONFIG_ERROR) {
|
|
226
|
+
return EXIT_CONFIG_ERROR;
|
|
227
|
+
}
|
|
228
|
+
if (arm !== null && !arm.armed) {
|
|
229
|
+
return EXIT_NOT_READY;
|
|
230
|
+
}
|
|
151
231
|
return result.exitCode;
|
|
152
232
|
}
|
|
153
233
|
if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { emitBypassWarning, emitVerifyJson, formatCacheFreshDeferSoftPath,
|
|
4
|
+
import { emitBypassWarning, emitVerifyJson, formatCacheFreshDeferSoftPath, occupancyAwareDenialMessage, verifySessionRitual, } from "@deftai/directive-core/session";
|
|
5
5
|
function parsePosture(value) {
|
|
6
6
|
if (value === "read-only")
|
|
7
7
|
return "read-only";
|
|
@@ -121,8 +121,10 @@ export function run(argv, deps = {}) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
else if (result.code === 1) {
|
|
124
|
-
const
|
|
125
|
-
const lines = [
|
|
124
|
+
const recoveryTier = result.recoveryTier ?? "cold";
|
|
125
|
+
const lines = [
|
|
126
|
+
occupancyAwareDenialMessage(projectRoot, result.message, { env: process.env }, recoveryTier),
|
|
127
|
+
];
|
|
126
128
|
if (isCacheFreshFailureMessage(result.message)) {
|
|
127
129
|
lines.push(formatCacheFreshDeferSoftPath());
|
|
128
130
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Fallback when package.json cannot be read. Keep aligned with the bin map. */
|
|
2
|
+
export declare const PACKAGE_BIN_NAMES: readonly ["directive", "deft", "deft-ts", "deft-hook", "deft-verify-encoding"];
|
|
3
|
+
/** Node owns these shims. This package must not delete them. */
|
|
4
|
+
export declare const NODE_OWNED_SHIM_NAMES: readonly ["npx", "npm"];
|
|
5
|
+
export interface Ps1ShimRemoval {
|
|
6
|
+
readonly removed: readonly string[];
|
|
7
|
+
readonly failed: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function packageIsUnderNodeModules(pkgDir: string): boolean;
|
|
10
|
+
/** bin-links layout. Valid only when {@link packageIsUnderNodeModules} is true. */
|
|
11
|
+
export declare function nodeModulesDir(pkgDir: string): string;
|
|
12
|
+
/** Same directory bin-links uses for this package root. */
|
|
13
|
+
export declare function linkerBinDir(opts: {
|
|
14
|
+
readonly pkgDir: string;
|
|
15
|
+
readonly global: boolean;
|
|
16
|
+
readonly platform: string;
|
|
17
|
+
}): string;
|
|
18
|
+
export declare function isGlobalInstall(env: Readonly<Record<string, string | undefined>>): boolean;
|
|
19
|
+
export declare function candidateBinDirs(opts: {
|
|
20
|
+
readonly pkgDir: string;
|
|
21
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
22
|
+
readonly platform: string;
|
|
23
|
+
}): string[];
|
|
24
|
+
export declare function removeGeneratedPs1Shims(opts: {
|
|
25
|
+
readonly binDir: string;
|
|
26
|
+
readonly binNames: readonly string[];
|
|
27
|
+
readonly platform: string;
|
|
28
|
+
readonly protectedNames?: readonly string[];
|
|
29
|
+
}): Ps1ShimRemoval;
|
|
30
|
+
/** True when cmd-shim text records a target path inside this install. */
|
|
31
|
+
export declare function cmdShimTargetsPackage(cmdText: string, binDir: string, pkgDir: string): boolean;
|
|
32
|
+
export declare function readPackageBinNames(packageJsonText: string): string[];
|
|
33
|
+
export declare function removeInstalledWindowsPs1Shims(opts: {
|
|
34
|
+
readonly pkgDir: string;
|
|
35
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
36
|
+
readonly platform: string;
|
|
37
|
+
readonly readText?: (path: string) => string;
|
|
38
|
+
}): Ps1ShimRemoval;
|
|
39
|
+
//# sourceMappingURL=windows-bin-ps1.d.ts.map
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm's Windows bin linker (cmd-shim) always writes a .ps1 beside the .cmd.
|
|
3
|
+
* PowerShell selects that .ps1 and stops when script execution is refused, so
|
|
4
|
+
* the bare name never reaches the .cmd. This runs from postinstall, after
|
|
5
|
+
* that link, and removes only this package's generated .ps1 files.
|
|
6
|
+
* A .ps1 is removed only when its sibling .cmd records a target inside this
|
|
7
|
+
* install, so an ambient PNPM_HOME from another install is left alone.
|
|
8
|
+
* It does not change npx.ps1, npm.ps1, or the operator's execution policy.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
|
|
11
|
+
import { basename, dirname, join, win32 } from "node:path";
|
|
12
|
+
/** Fallback when package.json cannot be read. Keep aligned with the bin map. */
|
|
13
|
+
export const PACKAGE_BIN_NAMES = [
|
|
14
|
+
"directive",
|
|
15
|
+
"deft",
|
|
16
|
+
"deft-ts",
|
|
17
|
+
"deft-hook",
|
|
18
|
+
"deft-verify-encoding",
|
|
19
|
+
];
|
|
20
|
+
/** Node owns these shims. This package must not delete them. */
|
|
21
|
+
export const NODE_OWNED_SHIM_NAMES = ["npx", "npm"];
|
|
22
|
+
export function packageIsUnderNodeModules(pkgDir) {
|
|
23
|
+
const parent = dirname(pkgDir);
|
|
24
|
+
if (basename(parent) === "node_modules")
|
|
25
|
+
return true;
|
|
26
|
+
return basename(dirname(parent)) === "node_modules";
|
|
27
|
+
}
|
|
28
|
+
/** bin-links layout. Valid only when {@link packageIsUnderNodeModules} is true. */
|
|
29
|
+
export function nodeModulesDir(pkgDir) {
|
|
30
|
+
const scopeOrNm = dirname(pkgDir);
|
|
31
|
+
if (basename(scopeOrNm) === "node_modules")
|
|
32
|
+
return scopeOrNm;
|
|
33
|
+
return dirname(scopeOrNm);
|
|
34
|
+
}
|
|
35
|
+
/** Same directory bin-links uses for this package root. */
|
|
36
|
+
export function linkerBinDir(opts) {
|
|
37
|
+
const nm = nodeModulesDir(opts.pkgDir);
|
|
38
|
+
if (!opts.global)
|
|
39
|
+
return join(nm, ".bin");
|
|
40
|
+
const prefix = dirname(nm);
|
|
41
|
+
if (opts.platform === "win32")
|
|
42
|
+
return prefix;
|
|
43
|
+
return join(prefix, "bin");
|
|
44
|
+
}
|
|
45
|
+
export function isGlobalInstall(env) {
|
|
46
|
+
return env.npm_config_global === "true";
|
|
47
|
+
}
|
|
48
|
+
function dirKey(dir, platform) {
|
|
49
|
+
const norm = dir.replace(/[\\/]+/g, "/");
|
|
50
|
+
if (platform === "win32")
|
|
51
|
+
return norm.toLowerCase();
|
|
52
|
+
return norm;
|
|
53
|
+
}
|
|
54
|
+
function ancestorBinDirs(pkgDir, stopDir, platform) {
|
|
55
|
+
const stop = dirKey(stopDir, platform);
|
|
56
|
+
const start = dirKey(pkgDir, platform);
|
|
57
|
+
const stopPrefix = stop.endsWith("/") ? stop : `${stop}/`;
|
|
58
|
+
if (start !== stop && !start.startsWith(stopPrefix)) {
|
|
59
|
+
return [join(stopDir, "node_modules", ".bin")];
|
|
60
|
+
}
|
|
61
|
+
const out = [];
|
|
62
|
+
let dir = pkgDir;
|
|
63
|
+
for (;;) {
|
|
64
|
+
out.push(join(dir, "node_modules", ".bin"));
|
|
65
|
+
if (dirKey(dir, platform) === stop)
|
|
66
|
+
break;
|
|
67
|
+
const parent = dirname(dir);
|
|
68
|
+
if (parent === dir)
|
|
69
|
+
break;
|
|
70
|
+
dir = parent;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
export function candidateBinDirs(opts) {
|
|
75
|
+
const global = isGlobalInstall(opts.env);
|
|
76
|
+
const dirs = [];
|
|
77
|
+
if (packageIsUnderNodeModules(opts.pkgDir)) {
|
|
78
|
+
dirs.push(linkerBinDir({ pkgDir: opts.pkgDir, global, platform: opts.platform }));
|
|
79
|
+
}
|
|
80
|
+
if (global) {
|
|
81
|
+
const prefix = opts.env.npm_config_prefix;
|
|
82
|
+
if (prefix !== undefined && prefix.length > 0) {
|
|
83
|
+
dirs.push(opts.platform === "win32" ? prefix : join(prefix, "bin"));
|
|
84
|
+
}
|
|
85
|
+
// Candidate only. Removal still requires the .cmd to target this install.
|
|
86
|
+
const pnpmHome = opts.env.PNPM_HOME;
|
|
87
|
+
if (pnpmHome !== undefined && pnpmHome.length > 0)
|
|
88
|
+
dirs.push(pnpmHome);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const localPrefix = opts.env.npm_config_local_prefix;
|
|
92
|
+
if (localPrefix !== undefined && localPrefix.length > 0) {
|
|
93
|
+
dirs.push(...ancestorBinDirs(opts.pkgDir, localPrefix, opts.platform));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const dir of dirs) {
|
|
99
|
+
const key = dirKey(dir, opts.platform);
|
|
100
|
+
if (seen.has(key))
|
|
101
|
+
continue;
|
|
102
|
+
seen.add(key);
|
|
103
|
+
out.push(dir);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function isSafeBinName(name) {
|
|
108
|
+
if (name.length === 0)
|
|
109
|
+
return false;
|
|
110
|
+
if (name.includes("/") || name.includes("\\") || name.includes(".."))
|
|
111
|
+
return false;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
export function removeGeneratedPs1Shims(opts) {
|
|
115
|
+
if (opts.platform !== "win32")
|
|
116
|
+
return { removed: [], failed: [] };
|
|
117
|
+
const protect = new Set((opts.protectedNames ?? NODE_OWNED_SHIM_NAMES).map((name) => name.toLowerCase()));
|
|
118
|
+
const removed = [];
|
|
119
|
+
const failed = [];
|
|
120
|
+
for (const name of opts.binNames) {
|
|
121
|
+
if (!isSafeBinName(name) || protect.has(name.toLowerCase()))
|
|
122
|
+
continue;
|
|
123
|
+
const ps1 = join(opts.binDir, `${name}.ps1`);
|
|
124
|
+
const cmd = join(opts.binDir, `${name}.cmd`);
|
|
125
|
+
if (!existsSync(cmd) || !existsSync(ps1))
|
|
126
|
+
continue;
|
|
127
|
+
try {
|
|
128
|
+
unlinkSync(ps1);
|
|
129
|
+
removed.push(ps1);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
failed.push(ps1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { removed, failed };
|
|
136
|
+
}
|
|
137
|
+
function normalizedWinPath(p) {
|
|
138
|
+
const norm = win32.normalize(p).replace(/[\\/]+/g, "\\");
|
|
139
|
+
const trimmed = norm.length > 3 && norm.endsWith("\\") ? norm.slice(0, -1) : norm;
|
|
140
|
+
return trimmed.toLowerCase();
|
|
141
|
+
}
|
|
142
|
+
function pathIsInside(root, target) {
|
|
143
|
+
const rootKey = normalizedWinPath(root);
|
|
144
|
+
const targetKey = normalizedWinPath(target);
|
|
145
|
+
return targetKey === rootKey || targetKey.startsWith(`${rootKey}\\`);
|
|
146
|
+
}
|
|
147
|
+
/** cmd-shim text is Windows-shaped. realpath needs host separators or a junction never matches. */
|
|
148
|
+
function hostPath(p) {
|
|
149
|
+
return p.replace(/\\/g, "/");
|
|
150
|
+
}
|
|
151
|
+
function realpathIsInside(root, target) {
|
|
152
|
+
try {
|
|
153
|
+
return pathIsInside(realpathSync(hostPath(root)), realpathSync(hostPath(target)));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** True when cmd-shim text records a target path inside this install. */
|
|
160
|
+
export function cmdShimTargetsPackage(cmdText, binDir, pkgDir) {
|
|
161
|
+
const re = /%(?:dp0%|~dp0)(?:\\+|\/+)([^"\r\n\s]+)/gi;
|
|
162
|
+
for (const match of cmdText.matchAll(re)) {
|
|
163
|
+
const rel = match[1];
|
|
164
|
+
if (rel === undefined || rel.length === 0)
|
|
165
|
+
continue;
|
|
166
|
+
const resolved = win32.normalize(win32.join(binDir, rel.replace(/\//g, "\\")));
|
|
167
|
+
if (pathIsInside(pkgDir, resolved) || realpathIsInside(pkgDir, resolved))
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
function cmdLinksCurrentInstall(binDir, name, pkgDir) {
|
|
173
|
+
if (!isSafeBinName(name))
|
|
174
|
+
return false;
|
|
175
|
+
let text;
|
|
176
|
+
try {
|
|
177
|
+
text = readFileSync(join(binDir, `${name}.cmd`), "utf8");
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return cmdShimTargetsPackage(text, binDir, pkgDir);
|
|
183
|
+
}
|
|
184
|
+
export function readPackageBinNames(packageJsonText) {
|
|
185
|
+
let parsed;
|
|
186
|
+
try {
|
|
187
|
+
parsed = JSON.parse(packageJsonText);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
193
|
+
return [];
|
|
194
|
+
const bin = parsed.bin;
|
|
195
|
+
if (bin === null || typeof bin !== "object" || Array.isArray(bin))
|
|
196
|
+
return [];
|
|
197
|
+
return Object.keys(bin);
|
|
198
|
+
}
|
|
199
|
+
export function removeInstalledWindowsPs1Shims(opts) {
|
|
200
|
+
if (opts.platform !== "win32")
|
|
201
|
+
return { removed: [], failed: [] };
|
|
202
|
+
const readText = opts.readText ?? ((path) => readFileSync(path, "utf8"));
|
|
203
|
+
let names;
|
|
204
|
+
try {
|
|
205
|
+
names = readPackageBinNames(readText(join(opts.pkgDir, "package.json")));
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
names = PACKAGE_BIN_NAMES;
|
|
209
|
+
}
|
|
210
|
+
const removed = [];
|
|
211
|
+
const failed = [];
|
|
212
|
+
for (const binDir of candidateBinDirs(opts)) {
|
|
213
|
+
const linked = names.filter((name) => cmdLinksCurrentInstall(binDir, name, opts.pkgDir));
|
|
214
|
+
const result = removeGeneratedPs1Shims({
|
|
215
|
+
binDir,
|
|
216
|
+
binNames: linked,
|
|
217
|
+
platform: opts.platform,
|
|
218
|
+
});
|
|
219
|
+
removed.push(...result.removed);
|
|
220
|
+
failed.push(...result.failed);
|
|
221
|
+
}
|
|
222
|
+
return { removed, failed };
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=windows-bin-ps1.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive",
|
|
3
|
-
"version": "0.119.
|
|
3
|
+
"version": "0.119.9",
|
|
4
4
|
"description": "Directive CLI — npm install path for the Deft Directive framework.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://deftai.github.io/directive/",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist/**/*.js",
|
|
25
|
-
"dist/**/*.d.ts"
|
|
25
|
+
"dist/**/*.d.ts",
|
|
26
|
+
"scripts/remove-windows-ps1-shims.mjs"
|
|
26
27
|
],
|
|
27
28
|
"repository": {
|
|
28
29
|
"type": "git",
|
|
@@ -34,10 +35,11 @@
|
|
|
34
35
|
"provenance": true
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@deftai/directive-core": "^0.119.
|
|
38
|
-
"@deftai/directive-content": "^0.119.
|
|
38
|
+
"@deftai/directive-core": "^0.119.9",
|
|
39
|
+
"@deftai/directive-content": "^0.119.9"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
|
-
"build": "tsc -b"
|
|
42
|
+
"build": "tsc -b",
|
|
43
|
+
"postinstall": "node ./scripts/remove-windows-ps1-shims.mjs"
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* postinstall entry (#4654). The published tarball includes dist/.
|
|
3
|
+
* A workspace install before tsc has no linked shims yet, so a missing
|
|
4
|
+
* dist file is a no-op rather than a failed install.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
+
|
|
10
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
const entry = join(pkgDir, "dist", "windows-bin-ps1.js");
|
|
12
|
+
|
|
13
|
+
if (existsSync(entry)) {
|
|
14
|
+
const mod = await import(pathToFileURL(entry).href);
|
|
15
|
+
const result = mod.removeInstalledWindowsPs1Shims({
|
|
16
|
+
pkgDir,
|
|
17
|
+
env: process.env,
|
|
18
|
+
platform: process.platform,
|
|
19
|
+
});
|
|
20
|
+
if (result.failed.length > 0) {
|
|
21
|
+
process.stderr.write(
|
|
22
|
+
`remove-windows-ps1-shims: could not remove ${result.failed.join(", ")}\n`,
|
|
23
|
+
);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
}
|