@deftai/directive-content 0.82.0 → 0.83.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/commands.md +3 -1
- package/docs/product-signal.md +6 -2
- package/package.json +1 -1
- package/packs/skills/skills-pack-0.1.json +1 -1
- package/skills/deft-directive-product-signal/SKILL.md +3 -1
- package/tasks/engine-pm-run.cjs +269 -0
- package/tasks/engine-pm-run.test.cjs +201 -0
- package/tasks/engine.yml +2 -135
package/commands.md
CHANGED
|
@@ -187,12 +187,14 @@ Use `task --list` for the exact current verify namespace.
|
|
|
187
187
|
|
|
188
188
|
### Agent-host direct-write hooks (#2438, #2596)
|
|
189
189
|
|
|
190
|
-
`directive init` and `deft update` idempotently merge Directive-owned entries into `.claude/settings.json`, `.grok/hooks/deft.json`, `.cursor/hooks.json`, and `.codex/hooks.json` while preserving unrelated settings. `SessionStart` refreshes resume bookkeeping on a non-blocking path. `PreToolUse` covers direct edit/write tools and denies them until both existing gates pass: a fresh gated session ritual and an active/running xBRIEF accepted by canonical preflight. A second `PreToolUse` matcher covers spawn/Task tools (`Task`, `SubagentStart`, `spawn_subagent`, `start_agent`, `CreateAgent`) with the same pre-`start_agent` gate stack; explore spawns (`subagent_type: explore`) pass without implementation gates.
|
|
190
|
+
`directive init` and `deft update` idempotently merge Directive-owned entries into `.claude/settings.json`, `.grok/hooks/deft.json`, `.cursor/hooks.json`, and `.codex/hooks.json` while preserving unrelated settings. `SessionStart` refreshes resume bookkeeping on a non-blocking path. `PreToolUse` covers direct edit/write tools and denies them until both existing gates pass: a fresh gated session ritual and an active/running xBRIEF accepted by canonical preflight. Cursor `preToolUse` deposits set `failClosed: true`, so allow decisions emit `{"permission":"allow"}` — empty stdout is treated as hook failure and would block Write tools. A second `PreToolUse` matcher covers spawn/Task tools (`Task`, `SubagentStart`, `spawn_subagent`, `start_agent`, `CreateAgent`) with the same pre-`start_agent` gate stack; explore spawns (`subagent_type: explore`) pass without implementation gates.
|
|
191
191
|
|
|
192
192
|
- **Read-only explore (#1185):** Prefer Grok role deposit `default_capability_mode = "read-only"` (see [issue #1185](https://github.com/deftai/directive/issues/1185)). Hooks also deny direct writes when `DEFT_HOOK_READ_ONLY=1` or the host payload signals read-only capability. Implementation spawns remain blocked in read-only posture unless explicitly marked explore.
|
|
193
193
|
|
|
194
194
|
- Verify registration: `deft verify:hooks-installed --scope=agent` (or `--scope=all` for git + agent hooks).
|
|
195
195
|
- Repair missing/drifted entries: `deft update`.
|
|
196
|
+
- **Opt-out (#2752):** Set `plan.policy.hostHooks.<host>` to `false` in `xbrief/PROJECT-DEFINITION.xbrief.json` for any of the four deposited hosts (`claude`, `cursor`, `grok`, `codex`). Unset or `true` keeps the current fail-closed deposit. When a host is opted out, `deft update` / `directive init` skip creating or re-merging Directive-managed hook entries for that host; if a prior deposit left managed entries in the file, the next update strips only those entries and preserves unrelated settings. Inspect with `deft policy:show --field=hostHooks`. Doctor and `verify:hooks-installed --scope=agent` treat opted-out hosts as healthy — they do not recommend `deft update` to repair them.
|
|
197
|
+
- **Claude matcher scope:** Once `.claude/settings.json` hooks are loaded, Claude's `PreToolUse` matcher keys on tool names (`Edit`, `Write`, …), not target paths — matched tools can be gated for the whole session, including writes outside the project tree. Opt out of Claude hook deposit when that posture is unwanted.
|
|
196
198
|
- **Compact re-arm (#2113):** Cursor `preCompact` and Claude/Grok `PreCompact`/`PostCompact` call `deft hook:dispatch --event session.compact` to mark the gated session ritual stale after context compaction/resume; the existing PreToolUse gate then denies direct writes until `deft session:start` and `deft verify:session-ritual -- --tier=gated`. Codex has no native compact hook — operators must re-run the mutation ritual manually after compaction.
|
|
197
199
|
- Codex project hooks are trust-gated by Codex. Directive verifies only that the registrations are structurally current; after an install or changed hook hash, open `/hooks` in Codex and review/approve the project hook commands. Runtime trust cannot be inferred from the file alone.
|
|
198
200
|
- Directive writes only `.codex/hooks.json`; it does not parse or modify `.codex/config.toml`. Codex can also load inline hooks from `config.toml`, so avoid defining duplicate Directive commands there or they may run more than once. See the [Codex hooks documentation](https://learn.chatgpt.com/docs/hooks).
|
package/docs/product-signal.md
CHANGED
|
@@ -15,13 +15,15 @@ Capability-cost disclosure prints before `--confirm` applies `plan.policy.produc
|
|
|
15
15
|
|
|
16
16
|
Consent file: `%APPDATA%\\deft\\product-signal-consent.json` (Windows) or `~/.config/deft/product-signal-consent.json` (Unix).
|
|
17
17
|
|
|
18
|
+
Schema **v2** records the normalized `owner/repo` sink you authorize (#2767). Legacy v1 consent authorizes only the baked-in default `deftai/product-signal`; a custom `plan.policy.productSignal.sinkRepo` requires re-consent after grant.
|
|
19
|
+
|
|
18
20
|
```bash
|
|
19
|
-
task product-signal:consent -- --grant
|
|
21
|
+
task product-signal:consent -- --grant [--project-root .]
|
|
20
22
|
task product-signal:consent -- --revoke
|
|
21
23
|
task product-signal:status
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
Outbound requires **both** enable and consent.
|
|
26
|
+
`task product-signal:status` shows configured sink, consented sink, and whether they match. Outbound requires **both** enable and consent, and the configured sink must match the consented destination (including dry-run).
|
|
25
27
|
|
|
26
28
|
## Submit (ops / tests)
|
|
27
29
|
|
|
@@ -29,6 +31,8 @@ Outbound requires **both** enable and consent.
|
|
|
29
31
|
task product-signal:submit -- --surface pulse|portrait [--dry-run] [--json] [--nps 0-10]
|
|
30
32
|
```
|
|
31
33
|
|
|
34
|
+
Changing `sinkRepo` after consent soft-skips with `sink-unconsented` until you re-run consent for the new destination. Destination authorization cannot be bypassed with `skipGates`.
|
|
35
|
+
|
|
32
36
|
## Sink bootstrap (maintainers)
|
|
33
37
|
|
|
34
38
|
Internal (org) inbox: `deftai/product-signal` (overridable via `plan.policy.productSignal.sinkRepo`). Not public.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-content",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.83.0",
|
|
4
4
|
"description": "Shippable Directive framework content in the consumer .deft/core/ layout (C1 flatten), plus the engine surfaces (.githooks/, Taskfile.yml, tasks/) the deposit wires. Python-free per #2022 Phase 3. Refs #11, #1669, #1967.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
],
|
|
188
188
|
"path": "skills/deft-directive-product-signal/SKILL.md",
|
|
189
189
|
"version": "0.1",
|
|
190
|
-
"body": "# Deft Directive Product Signal — consented check-in (#2693)\n\nAgent-driven qualitative product-improvement signal for partner/trial installs. Lightweight interview, minimized local summaries (value/health/helped), optional skillsSummary hook — submits to private `deftai/product-signal` when enable + consent gates pass.\n\nLegend (from RFC2119): !=MUST, ~=SHOULD, ≉=SHOULD NOT, ⊗=MUST NOT, ?=MAY.\n\n## When to Use\n\n- Operator says `product check-in`, `product pulse`, `partner feedback`, or `product signal`\n- Partner onboarding after ops enables `plan.policy.productSignal`\n- ⊗ Do NOT invoke from session:start or ambient rituals when disabled (#2693 D3)\n\n## Preconditions\n\n- ! `plan.policy.productSignal.enabled` MUST be true (`task policy:show --field=productSignal`)\n- ! Install-level consent MUST be recorded before outbound submit (`task product-signal:status`)\n- ⊗ Prompt or submit when `enabled` is false\n- ⊗ Block story work on sink errors — fail open (#2693 D16/D18)\n\n## Phase 1 — Gate\n\n```bash\ntask product-signal:status\ntask policy:show -- --field=productSignal\n```\n\n- ! If disabled: soft-skip with one line; exit skill\n- ! If headless/CI: fail open — no consent/interview prompts (#2693 D16)\n\n## Phase 2 — First-time consent (interactive only, D17)\n\nWhen enabled, interactive, and consent missing/revoked, ask:\n\n> May we collect usage metrics and related session signal from this install to improve Directive? This can include a short check-in plus minimized local summaries (value/health and related ledgers). Nothing is sent while this path is off or without your consent.\n>\n> If you are unsure, please check with your company before saying no — they are a Directive partner and may already expect this signal to be shared.\n>\n> Reply **yes** to consent, or **no** to decline.\n\n- **yes** → `task product-signal:consent -- --grant
|
|
190
|
+
"body": "# Deft Directive Product Signal — consented check-in (#2693)\n\nAgent-driven qualitative product-improvement signal for partner/trial installs. Lightweight interview, minimized local summaries (value/health/helped), optional skillsSummary hook — submits to private `deftai/product-signal` when enable + consent gates pass.\n\nLegend (from RFC2119): !=MUST, ~=SHOULD, ≉=SHOULD NOT, ⊗=MUST NOT, ?=MAY.\n\n## When to Use\n\n- Operator says `product check-in`, `product pulse`, `partner feedback`, or `product signal`\n- Partner onboarding after ops enables `plan.policy.productSignal`\n- ⊗ Do NOT invoke from session:start or ambient rituals when disabled (#2693 D3)\n\n## Preconditions\n\n- ! `plan.policy.productSignal.enabled` MUST be true (`task policy:show --field=productSignal`)\n- ! Install-level consent MUST be recorded before outbound submit (`task product-signal:status`)\n- ! Consent schema v2 binds to the configured sinkRepo; legacy v1 consent authorizes only `deftai/product-signal` (#2767)\n- ! Re-consent after changing `plan.policy.productSignal.sinkRepo`; mismatch soft-skips as sink-unconsented\n- ⊗ Prompt or submit when `enabled` is false\n- ⊗ Block story work on sink errors — fail open (#2693 D16/D18)\n\n## Phase 1 — Gate\n\n```bash\ntask product-signal:status\ntask policy:show -- --field=productSignal\n```\n\n- ! If disabled: soft-skip with one line; exit skill\n- ! If headless/CI: fail open — no consent/interview prompts (#2693 D16)\n\n## Phase 2 — First-time consent (interactive only, D17)\n\nWhen enabled, interactive, and consent missing/revoked, ask:\n\n> May we collect usage metrics and related session signal from this install to improve Directive? This can include a short check-in plus minimized local summaries (value/health and related ledgers). Nothing is sent while this path is off or without your consent.\n>\n> If you are unsure, please check with your company before saying no — they are a Directive partner and may already expect this signal to be shared.\n>\n> Reply **yes** to consent, or **no** to decline.\n\n- **yes** → `task product-signal:consent -- --grant` (pass `--project-root` when using a custom sinkRepo); continue\n- **no** → soft-skip; do not badger every turn\n\n## Phase 3 — Interview (≤3 answers + optional NPS)\n\n- ! Ask at most three short questions plus optional NPS (0–10)\n- ~ Target ≤30s human time beyond first-time consent\n- ⊗ Paste secrets, paths, raw logs, or full USER.md into answers\n\n## Phase 4 — Assemble + validate + submit\n\n```bash\ntask product-signal:submit -- --surface pulse --dry-run\ntask product-signal:submit -- --surface pulse --nps 9\n```\n\n- ! On validation failure: show reason; no partial write\n- ! On sink ACL/network errors: soft-skip with hint; do not fail caller (#2693 D18)\n- ! Report issue URL on success\n\n## Private gaps (D19)\n\n- ! Record bugs/gaps as `Gap:` comments on the standing **pulse** thread (enable+consent sufficient)\n- ! Public promote to `deftai/directive` remains hard-confirm via `deft-directive-feedback` / `task feedback:file`\n\n## Revoke\n\n```bash\ntask product-signal:consent -- --revoke\n# and/or disable: set plan.policy.productSignal.enabled false\n```\n\n## Anti-Patterns\n\n- ⊗ Ambient consent nag when disabled\n- ⊗ Fail-closed headless behavior\n- ⊗ Sticky forever-fail when sink is down\n- ⊗ Auto-file public framework-gap issues from pulses\n\n## Exit\n\ndeft-directive-product-signal complete — exiting skill.\n",
|
|
191
191
|
"frontmatter_extra": null
|
|
192
192
|
},
|
|
193
193
|
{
|
|
@@ -27,6 +27,8 @@ Legend (from RFC2119): !=MUST, ~=SHOULD, ≉=SHOULD NOT, ⊗=MUST NOT, ?=MAY.
|
|
|
27
27
|
|
|
28
28
|
- ! `plan.policy.productSignal.enabled` MUST be true (`task policy:show --field=productSignal`)
|
|
29
29
|
- ! Install-level consent MUST be recorded before outbound submit (`task product-signal:status`)
|
|
30
|
+
- ! Consent schema v2 binds to the configured sinkRepo; legacy v1 consent authorizes only `deftai/product-signal` (#2767)
|
|
31
|
+
- ! Re-consent after changing `plan.policy.productSignal.sinkRepo`; mismatch soft-skips as sink-unconsented
|
|
30
32
|
- ⊗ Prompt or submit when `enabled` is false
|
|
31
33
|
- ⊗ Block story work on sink errors — fail open (#2693 D16/D18)
|
|
32
34
|
|
|
@@ -50,7 +52,7 @@ When enabled, interactive, and consent missing/revoked, ask:
|
|
|
50
52
|
>
|
|
51
53
|
> Reply **yes** to consent, or **no** to decline.
|
|
52
54
|
|
|
53
|
-
- **yes** → `task product-signal:consent -- --grant
|
|
55
|
+
- **yes** → `task product-signal:consent -- --grant` (pass `--project-root` when using a custom sinkRepo); continue
|
|
54
56
|
- **no** → soft-skip; do not badger every turn
|
|
55
57
|
|
|
56
58
|
## Phase 3 — Interview (≤3 answers + optional NPS)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run package.json scripts via pnpm / Corepack without shell-interpolating
|
|
6
|
+
* repository-controlled packageManager pins (#2765 / #2761).
|
|
7
|
+
*
|
|
8
|
+
* Consumed by tasks/engine.yml `:engine:pm-run` and `:engine:_ts-build`.
|
|
9
|
+
* Lives under tasks/ so @deftai/directive-content prepack ships it beside
|
|
10
|
+
* tasks/engine.yml (#2022 Phase 3).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { execFileSync } = require("node:child_process");
|
|
14
|
+
const fs = require("node:fs");
|
|
15
|
+
const path = require("node:path");
|
|
16
|
+
|
|
17
|
+
const WIN32_ALLOWLIST = new Set(["pnpm", "corepack", "npm"]);
|
|
18
|
+
const SHELL_METACHAR_RE = /[;&|`$<>()\\'"!#\n\r\t]/;
|
|
19
|
+
const SEMVER_RE =
|
|
20
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
21
|
+
const SCRIPT_NAME_RE = /^[A-Za-z0-9:_-]+$/;
|
|
22
|
+
|
|
23
|
+
/** @param {string | null | undefined} version */
|
|
24
|
+
function isValidSemVer(version) {
|
|
25
|
+
return typeof version === "string" && SEMVER_RE.test(version);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {unknown} raw
|
|
30
|
+
* @returns {{ ok: true, semver: string | null, pin: string | null } | { ok: false, reason: string }}
|
|
31
|
+
*/
|
|
32
|
+
function parsePnpmPin(raw) {
|
|
33
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
34
|
+
return { ok: true, semver: null, pin: null };
|
|
35
|
+
}
|
|
36
|
+
const str = String(raw);
|
|
37
|
+
if (str !== str.trim()) {
|
|
38
|
+
return { ok: false, reason: "packageManager has leading/trailing whitespace" };
|
|
39
|
+
}
|
|
40
|
+
if (/\s/.test(str)) {
|
|
41
|
+
return { ok: false, reason: "packageManager contains whitespace" };
|
|
42
|
+
}
|
|
43
|
+
if (SHELL_METACHAR_RE.test(str)) {
|
|
44
|
+
return { ok: false, reason: "packageManager contains shell metacharacters" };
|
|
45
|
+
}
|
|
46
|
+
if (!str.startsWith("pnpm@")) {
|
|
47
|
+
return { ok: false, reason: "packageManager must be pnpm@<semver> when set" };
|
|
48
|
+
}
|
|
49
|
+
const semver = str.slice("pnpm@".length);
|
|
50
|
+
if (!semver || !isValidSemVer(semver)) {
|
|
51
|
+
return { ok: false, reason: "packageManager semver is invalid" };
|
|
52
|
+
}
|
|
53
|
+
if (str !== `pnpm@${semver}`) {
|
|
54
|
+
return { ok: false, reason: "packageManager pin malformed" };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, semver, pin: str };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {unknown} name
|
|
61
|
+
* @param {Record<string, unknown> | undefined} scripts
|
|
62
|
+
*/
|
|
63
|
+
function validateScriptName(name, scripts) {
|
|
64
|
+
if (typeof name !== "string" || !name || !SCRIPT_NAME_RE.test(name)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return !!scripts && Object.prototype.hasOwnProperty.call(scripts, name);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {string} arg */
|
|
71
|
+
function quoteWin32Arg(arg) {
|
|
72
|
+
const s = String(arg);
|
|
73
|
+
if (!/[\s"]/.test(s)) {
|
|
74
|
+
return s;
|
|
75
|
+
}
|
|
76
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {typeof execFileSync} execFn
|
|
81
|
+
* @param {string} name
|
|
82
|
+
*/
|
|
83
|
+
function hasCmd(execFn, name) {
|
|
84
|
+
const spawnOpts = (/** @type {Record<string, unknown>} */ extra) => ({
|
|
85
|
+
stdio: "ignore",
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
...extra,
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
execFn(name, ["--version"], spawnOpts({}));
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
try {
|
|
94
|
+
execFn(name, ["--version"], spawnOpts({ shell: true }));
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {typeof execFileSync} execFn
|
|
104
|
+
* @param {string} cmd
|
|
105
|
+
* @param {string[]} args
|
|
106
|
+
* @param {{ cwd: string, spawn?: Record<string, unknown> }} opts
|
|
107
|
+
*/
|
|
108
|
+
function executeAllowlisted(execFn, cmd, args, opts) {
|
|
109
|
+
if (!WIN32_ALLOWLIST.has(cmd)) {
|
|
110
|
+
throw new Error(`deft: allowlisted shim required, got ${JSON.stringify(cmd)}`);
|
|
111
|
+
}
|
|
112
|
+
const spawnOverride = opts.spawn && typeof opts.spawn === "object" ? opts.spawn : {};
|
|
113
|
+
const base = {
|
|
114
|
+
cwd: opts.cwd,
|
|
115
|
+
stdio: "inherit",
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
shell: false,
|
|
118
|
+
...spawnOverride,
|
|
119
|
+
shell: false,
|
|
120
|
+
};
|
|
121
|
+
if (process.platform === "win32") {
|
|
122
|
+
const commandLine = [quoteWin32Arg(cmd), ...args.map(quoteWin32Arg)].join(" ");
|
|
123
|
+
execFn("cmd.exe", ["/d", "/s", "/c", commandLine], base);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
execFn(cmd, args, base);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {{ hasPnpm: boolean, hasCorepack: boolean, semver: string | null, script: string }} input
|
|
131
|
+
*/
|
|
132
|
+
function buildDispatchSteps(input) {
|
|
133
|
+
const steps = [];
|
|
134
|
+
if (input.hasPnpm) {
|
|
135
|
+
steps.push({ cmd: "pnpm", args: ["run", input.script] });
|
|
136
|
+
}
|
|
137
|
+
if (input.hasCorepack && input.semver) {
|
|
138
|
+
steps.push({ cmd: "corepack", args: [`pnpm@${input.semver}`, "run", input.script] });
|
|
139
|
+
}
|
|
140
|
+
if (input.hasCorepack) {
|
|
141
|
+
steps.push({ cmd: "corepack", args: ["pnpm", "run", input.script] });
|
|
142
|
+
}
|
|
143
|
+
return steps;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** @param {string} root */
|
|
147
|
+
function markWarm(root) {
|
|
148
|
+
try {
|
|
149
|
+
const dist = path.join(root, "packages", "cli", "dist");
|
|
150
|
+
fs.mkdirSync(dist, { recursive: true });
|
|
151
|
+
fs.writeFileSync(path.join(dist, ".deft-ts-build-stamp"), new Date().toISOString());
|
|
152
|
+
} catch {
|
|
153
|
+
// best-effort warm marker
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {string} root
|
|
159
|
+
* @param {string} script
|
|
160
|
+
* @param {{ execFileSync?: typeof execFileSync, markWarm?: boolean }} [deps]
|
|
161
|
+
*/
|
|
162
|
+
function runPackageScript(root, script, deps = {}) {
|
|
163
|
+
const execFn = deps.execFileSync || execFileSync;
|
|
164
|
+
const pkgPath = path.join(root, "package.json");
|
|
165
|
+
if (!fs.existsSync(pkgPath)) {
|
|
166
|
+
console.error(`deft: package.json missing at ${root}`);
|
|
167
|
+
return 2;
|
|
168
|
+
}
|
|
169
|
+
let pkg;
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
172
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
173
|
+
console.error(`deft: package.json at ${root} is not a JSON object`);
|
|
174
|
+
return 2;
|
|
175
|
+
}
|
|
176
|
+
pkg = parsed;
|
|
177
|
+
} catch {
|
|
178
|
+
console.error(`deft: package.json at ${root} is not valid JSON`);
|
|
179
|
+
return 2;
|
|
180
|
+
}
|
|
181
|
+
if (!validateScriptName(script, pkg.scripts)) {
|
|
182
|
+
console.error(`deft: package.json has no script ${JSON.stringify(script)}`);
|
|
183
|
+
return 2;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const pinResult = parsePnpmPin(pkg.packageManager);
|
|
187
|
+
if (!pinResult.ok) {
|
|
188
|
+
console.error(`deft: invalid packageManager pin — ${pinResult.reason}`);
|
|
189
|
+
return 3;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const envPm = String(process.env.DEFT_PACKAGE_MANAGER || "")
|
|
193
|
+
.trim()
|
|
194
|
+
.toLowerCase();
|
|
195
|
+
if (envPm === "npm") {
|
|
196
|
+
try {
|
|
197
|
+
executeAllowlisted(execFn, "npm", ["run", script], { cwd: root });
|
|
198
|
+
if (deps.markWarm) {
|
|
199
|
+
markWarm(root);
|
|
200
|
+
}
|
|
201
|
+
return 0;
|
|
202
|
+
} catch {
|
|
203
|
+
console.error(`deft: npm run ${JSON.stringify(script)} failed`);
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const hasPnpm = hasCmd(execFn, "pnpm");
|
|
209
|
+
const hasCorepack = hasCmd(execFn, "corepack");
|
|
210
|
+
const steps = buildDispatchSteps({
|
|
211
|
+
hasPnpm,
|
|
212
|
+
hasCorepack,
|
|
213
|
+
semver: pinResult.semver,
|
|
214
|
+
script,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
for (const step of steps) {
|
|
218
|
+
try {
|
|
219
|
+
executeAllowlisted(execFn, step.cmd, step.args, { cwd: root });
|
|
220
|
+
if (deps.markWarm) {
|
|
221
|
+
markWarm(root);
|
|
222
|
+
}
|
|
223
|
+
return 0;
|
|
224
|
+
} catch {
|
|
225
|
+
// fall through to Corepack / next resolver
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.error(`deft: neither pnpm nor corepack is available to run ${JSON.stringify(script)}.`);
|
|
230
|
+
if (pinResult.pin) {
|
|
231
|
+
console.error(
|
|
232
|
+
` Enable Corepack for the pinned manager: corepack enable && corepack prepare ${pinResult.pin} --activate`,
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
console.error(" Install pnpm or enable Corepack (see package.json#packageManager).");
|
|
236
|
+
}
|
|
237
|
+
console.error(" Or set DEFT_PACKAGE_MANAGER=npm for an explicit npm build path.");
|
|
238
|
+
return 127;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function main() {
|
|
242
|
+
const root = process.argv[2];
|
|
243
|
+
const script = process.argv[3];
|
|
244
|
+
const markWarmFlag = process.argv.includes("--mark-warm");
|
|
245
|
+
if (!root || !script) {
|
|
246
|
+
console.error("deft: engine-pm-run usage: engine-pm-run.cjs <DEFT_ROOT> <script> [--mark-warm]");
|
|
247
|
+
process.exit(2);
|
|
248
|
+
}
|
|
249
|
+
const code = runPackageScript(root, script, { markWarm: markWarmFlag });
|
|
250
|
+
process.exit(code);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (require.main === module) {
|
|
254
|
+
main();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
SHELL_METACHAR_RE,
|
|
259
|
+
WIN32_ALLOWLIST,
|
|
260
|
+
buildDispatchSteps,
|
|
261
|
+
executeAllowlisted,
|
|
262
|
+
hasCmd,
|
|
263
|
+
isValidSemVer,
|
|
264
|
+
markWarm,
|
|
265
|
+
parsePnpmPin,
|
|
266
|
+
quoteWin32Arg,
|
|
267
|
+
runPackageScript,
|
|
268
|
+
validateScriptName,
|
|
269
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const assert = require("node:assert/strict");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const { describe, it } = require("node:test");
|
|
9
|
+
const {
|
|
10
|
+
buildDispatchSteps,
|
|
11
|
+
executeAllowlisted,
|
|
12
|
+
hasCmd,
|
|
13
|
+
parsePnpmPin,
|
|
14
|
+
runPackageScript,
|
|
15
|
+
validateScriptName,
|
|
16
|
+
} = require("./engine-pm-run.cjs");
|
|
17
|
+
|
|
18
|
+
describe("parsePnpmPin", () => {
|
|
19
|
+
it("accepts stable, prerelease, and build-metadata pins", () => {
|
|
20
|
+
for (const pin of ["pnpm@11.8.0", "pnpm@1.0.0-alpha.1", "pnpm@1.0.0+build.1"]) {
|
|
21
|
+
const result = parsePnpmPin(pin);
|
|
22
|
+
assert.equal(result.ok, true);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("accepts missing/empty pins for unpinned fallback", () => {
|
|
27
|
+
assert.deepEqual(parsePnpmPin(undefined), { ok: true, semver: null, pin: null });
|
|
28
|
+
assert.deepEqual(parsePnpmPin(""), { ok: true, semver: null, pin: null });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("rejects shell metacharacters and malformed pins before spawn", () => {
|
|
32
|
+
for (const pin of [
|
|
33
|
+
"pnpm@9.0.0; echo pwned",
|
|
34
|
+
"pnpm@9.0.0 & echo pwned",
|
|
35
|
+
"pnpm@^1.0.0",
|
|
36
|
+
"npm@1.0.0",
|
|
37
|
+
"pnpm@1.0.0 extra",
|
|
38
|
+
" pnpm@1.0.0",
|
|
39
|
+
"pnpm@1.0.0 ",
|
|
40
|
+
"pnpm@1.0.0\n",
|
|
41
|
+
]) {
|
|
42
|
+
const result = parsePnpmPin(pin);
|
|
43
|
+
assert.equal(result.ok, false, pin);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("validateScriptName", () => {
|
|
49
|
+
it("accepts only declared script keys with safe names", () => {
|
|
50
|
+
const scripts = { build: "tsc", "test:unit": "vitest" };
|
|
51
|
+
assert.equal(validateScriptName("build", scripts), true);
|
|
52
|
+
assert.equal(validateScriptName("test:unit", scripts), true);
|
|
53
|
+
assert.equal(validateScriptName("missing", scripts), false);
|
|
54
|
+
assert.equal(validateScriptName("build;rm", scripts), false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("buildDispatchSteps", () => {
|
|
59
|
+
it("preserves installed pnpm -> pinned corepack -> unpinned corepack order", () => {
|
|
60
|
+
const steps = buildDispatchSteps({
|
|
61
|
+
hasPnpm: true,
|
|
62
|
+
hasCorepack: true,
|
|
63
|
+
semver: "11.8.0",
|
|
64
|
+
script: "build",
|
|
65
|
+
});
|
|
66
|
+
assert.deepEqual(steps, [
|
|
67
|
+
{ cmd: "pnpm", args: ["run", "build"] },
|
|
68
|
+
{ cmd: "corepack", args: ["pnpm@11.8.0", "run", "build"] },
|
|
69
|
+
{ cmd: "corepack", args: ["pnpm", "run", "build"] },
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("runPackageScript security", () => {
|
|
75
|
+
function mkFixture(/** @type {Record<string, unknown>} */ pkgExtra) {
|
|
76
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "deft-engine-pm-run-"));
|
|
77
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(pkgExtra, null, 2), "utf8");
|
|
78
|
+
return dir;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
it("rejects malicious pins before any execution call", () => {
|
|
82
|
+
const dir = mkFixture({
|
|
83
|
+
packageManager: "pnpm@9.0.0; echo pwned",
|
|
84
|
+
scripts: { build: "node -e \"\"" },
|
|
85
|
+
});
|
|
86
|
+
const sentinel = path.join(dir, "sentinel.txt");
|
|
87
|
+
let execCalls = 0;
|
|
88
|
+
const code = runPackageScript(dir, "build", {
|
|
89
|
+
execFileSync() {
|
|
90
|
+
execCalls += 1;
|
|
91
|
+
fs.writeFileSync(sentinel, "pwned", "utf8");
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
assert.equal(code, 3);
|
|
95
|
+
assert.equal(execCalls, 0);
|
|
96
|
+
assert.equal(fs.existsSync(sentinel), false);
|
|
97
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("records exact argv on execution without shell:true", () => {
|
|
101
|
+
const dir = mkFixture({
|
|
102
|
+
packageManager: "pnpm@11.8.0",
|
|
103
|
+
scripts: { build: "tsc" },
|
|
104
|
+
});
|
|
105
|
+
/** @type {Array<{ cmd: string, args: string[], shell?: boolean, stdio?: string }>} */
|
|
106
|
+
const calls = [];
|
|
107
|
+
const code = runPackageScript(dir, "build", {
|
|
108
|
+
execFileSync(cmd, args, opts) {
|
|
109
|
+
calls.push({ cmd, args: [...args], shell: opts?.shell, stdio: opts?.stdio });
|
|
110
|
+
if (opts?.stdio === "inherit") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
assert.equal(code, 0);
|
|
116
|
+
const execCalls = calls.filter((c) => c.stdio === "inherit");
|
|
117
|
+
assert.equal(execCalls.length, 1);
|
|
118
|
+
assert.equal(execCalls[0].shell, false);
|
|
119
|
+
assert.equal(execCalls[0].stdio, "inherit");
|
|
120
|
+
if (process.platform === "win32") {
|
|
121
|
+
assert.equal(execCalls[0].cmd, "cmd.exe");
|
|
122
|
+
assert.deepEqual(execCalls[0].args.slice(0, 3), ["/d", "/s", "/c"]);
|
|
123
|
+
assert.match(execCalls[0].args[3], /pnpm.*run.*build/);
|
|
124
|
+
} else {
|
|
125
|
+
assert.deepEqual(execCalls[0], {
|
|
126
|
+
cmd: "pnpm",
|
|
127
|
+
args: ["run", "build"],
|
|
128
|
+
shell: false,
|
|
129
|
+
stdio: "inherit",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("allows probe hasCmd to use shell:true separately from execution", () => {
|
|
136
|
+
/** @type {Array<{ shell?: boolean, stdio?: string }>} */
|
|
137
|
+
const calls = [];
|
|
138
|
+
let probeAttempts = 0;
|
|
139
|
+
const execFn = (/** @type {string} */ _cmd, /** @type {string[]} */ _args, /** @type {{ shell?: boolean, stdio?: string }} */ opts) => {
|
|
140
|
+
if (opts?.stdio === "ignore") {
|
|
141
|
+
probeAttempts += 1;
|
|
142
|
+
if (probeAttempts === 1 && !opts.shell) {
|
|
143
|
+
throw new Error("probe without shell failed");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
calls.push({ shell: opts?.shell, stdio: opts?.stdio });
|
|
147
|
+
};
|
|
148
|
+
assert.equal(hasCmd(execFn, "pnpm"), true);
|
|
149
|
+
assert.ok(calls.some((c) => c.shell === true && c.stdio === "ignore"));
|
|
150
|
+
const execCalls = calls.filter((c) => c.stdio === "inherit");
|
|
151
|
+
assert.equal(execCalls.length, 0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("rejects invalid package.json before probing PATH", () => {
|
|
155
|
+
const dir = mkFixture({ packageManager: "pnpm@11.8.0", scripts: { build: "tsc" } });
|
|
156
|
+
fs.writeFileSync(path.join(dir, "package.json"), "null", "utf8");
|
|
157
|
+
let execCalls = 0;
|
|
158
|
+
const code = runPackageScript(dir, "build", {
|
|
159
|
+
execFileSync() {
|
|
160
|
+
execCalls += 1;
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
assert.equal(code, 2);
|
|
164
|
+
assert.equal(execCalls, 0);
|
|
165
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("rejects unknown scripts before probing PATH", () => {
|
|
169
|
+
const dir = mkFixture({ packageManager: "pnpm@11.8.0", scripts: { build: "tsc" } });
|
|
170
|
+
let execCalls = 0;
|
|
171
|
+
const code = runPackageScript(dir, "lint", {
|
|
172
|
+
execFileSync() {
|
|
173
|
+
execCalls += 1;
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
assert.equal(code, 2);
|
|
177
|
+
assert.equal(execCalls, 0);
|
|
178
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("uses cmd.exe on win32 without shell:true on the Node spawn", () => {
|
|
182
|
+
if (process.platform !== "win32") {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
/** @type {{ cmd?: string, args?: string[], shell?: boolean } | null} */
|
|
186
|
+
let recorded = null;
|
|
187
|
+
executeAllowlisted(
|
|
188
|
+
(cmd, args, opts) => {
|
|
189
|
+
recorded = { cmd, args: [...args], shell: opts?.shell };
|
|
190
|
+
},
|
|
191
|
+
"pnpm",
|
|
192
|
+
["run", "build"],
|
|
193
|
+
{ cwd: process.cwd() },
|
|
194
|
+
);
|
|
195
|
+
assert.ok(recorded);
|
|
196
|
+
assert.equal(recorded.cmd, "cmd.exe");
|
|
197
|
+
assert.equal(recorded.args[0], "/d");
|
|
198
|
+
assert.equal(recorded.shell, false);
|
|
199
|
+
assert.match(recorded.args[3], /^pnpm run build$|^"pnpm" "run" "build"$/);
|
|
200
|
+
});
|
|
201
|
+
});
|
package/tasks/engine.yml
CHANGED
|
@@ -20,71 +20,7 @@ tasks:
|
|
|
20
20
|
cmds:
|
|
21
21
|
- |
|
|
22
22
|
set -eu
|
|
23
|
-
node -
|
|
24
|
-
const {execFileSync}=require('child_process');
|
|
25
|
-
const fs=require('fs');
|
|
26
|
-
const root=process.argv[1];
|
|
27
|
-
const script=process.argv[2];
|
|
28
|
-
const pkgPath=root+'/package.json';
|
|
29
|
-
if(!fs.existsSync(pkgPath)){
|
|
30
|
-
console.error('deft: package.json missing at '+root);
|
|
31
|
-
process.exit(2);
|
|
32
|
-
}
|
|
33
|
-
const pkg=JSON.parse(fs.readFileSync(pkgPath,'utf8'));
|
|
34
|
-
// Cross-platform probe (#2415): do NOT use Unix `sh -c 'command -v'` —
|
|
35
|
-
// Windows native Task often has no `sh` on PATH, so Corepack.cmd was
|
|
36
|
-
// invisible after #2411. Prefer a direct spawn (POSIX / real binaries);
|
|
37
|
-
// fall back to shell:true so PATHEXT resolves .cmd/.exe on win32.
|
|
38
|
-
// windowsHide (#2563): CREATE_NO_WINDOW so Cursor Task shells do not
|
|
39
|
-
// flood visible cmd.exe/conhost windows on every probe/build.
|
|
40
|
-
const spawnOpts=(extra)=>({stdio:'ignore',windowsHide:true,...extra});
|
|
41
|
-
const hasCmd=(name)=>{
|
|
42
|
-
try{
|
|
43
|
-
execFileSync(name,['--version'],spawnOpts());
|
|
44
|
-
return true;
|
|
45
|
-
}catch{
|
|
46
|
-
try{
|
|
47
|
-
execFileSync(name,['--version'],spawnOpts({shell:true}));
|
|
48
|
-
return true;
|
|
49
|
-
}catch{
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
const run=(cmd,args)=>{
|
|
55
|
-
execFileSync(cmd,args,{cwd:root,stdio:'inherit',shell:true,windowsHide:true});
|
|
56
|
-
};
|
|
57
|
-
const trySteps=(steps)=>{
|
|
58
|
-
for(const [cmd,args] of steps){
|
|
59
|
-
try{
|
|
60
|
-
run(cmd,args);
|
|
61
|
-
process.exit(0);
|
|
62
|
-
}catch{
|
|
63
|
-
// fall through to Corepack / next resolver
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
const envPm=String(process.env.DEFT_PACKAGE_MANAGER||'').trim().toLowerCase();
|
|
68
|
-
if(envPm==='npm'){
|
|
69
|
-
run('npm',['run',script]);
|
|
70
|
-
process.exit(0);
|
|
71
|
-
}
|
|
72
|
-
const pin=String(pkg.packageManager||'').trim();
|
|
73
|
-
const match=pin.match(/^pnpm@(.+)$/);
|
|
74
|
-
const steps=[];
|
|
75
|
-
if(hasCmd('pnpm')) steps.push(['pnpm',['run',script]]);
|
|
76
|
-
if(hasCmd('corepack')&&match) steps.push(['corepack',['pnpm@'+match[1],'run',script]]);
|
|
77
|
-
if(hasCmd('corepack')) steps.push(['corepack',['pnpm','run',script]]);
|
|
78
|
-
trySteps(steps);
|
|
79
|
-
console.error('deft: neither pnpm nor corepack is available to run \"'+script+'\".');
|
|
80
|
-
if(pin){
|
|
81
|
-
console.error(' Enable Corepack for the pinned manager: corepack enable && corepack prepare '+pin+' --activate');
|
|
82
|
-
}else{
|
|
83
|
-
console.error(' Install pnpm or enable Corepack (see package.json#packageManager).');
|
|
84
|
-
}
|
|
85
|
-
console.error(' Or set DEFT_PACKAGE_MANAGER=npm for an explicit npm build path.');
|
|
86
|
-
process.exit(127);
|
|
87
|
-
" "{{.DEFT_ROOT}}" "{{.PM_SCRIPT}}"
|
|
23
|
+
node "{{.TASKFILE_DIR}}/engine-pm-run.cjs" "{{.DEFT_ROOT}}" "{{.PM_SCRIPT}}"
|
|
88
24
|
|
|
89
25
|
_ts-build:
|
|
90
26
|
internal: true
|
|
@@ -115,76 +51,7 @@ tasks:
|
|
|
115
51
|
if node "{{.TASKFILE_DIR}}/ts-build-fresh.cjs" "{{.DEFT_ROOT}}"; then
|
|
116
52
|
exit 0
|
|
117
53
|
fi
|
|
118
|
-
node -
|
|
119
|
-
const {execFileSync}=require('child_process');
|
|
120
|
-
const fs=require('fs');
|
|
121
|
-
const root=process.argv[1];
|
|
122
|
-
const script='build';
|
|
123
|
-
const pkgPath=root+'/package.json';
|
|
124
|
-
const pkg=JSON.parse(fs.readFileSync(pkgPath,'utf8'));
|
|
125
|
-
// Cross-platform probe (#2415): do NOT use Unix `sh -c 'command -v'` —
|
|
126
|
-
// Windows native Task often has no `sh` on PATH, so Corepack.cmd was
|
|
127
|
-
// invisible after #2411. Prefer a direct spawn (POSIX / real binaries);
|
|
128
|
-
// fall back to shell:true so PATHEXT resolves .cmd/.exe on win32.
|
|
129
|
-
// windowsHide (#2563): CREATE_NO_WINDOW so Cursor Task shells do not
|
|
130
|
-
// flood visible cmd.exe/conhost windows on every probe/build.
|
|
131
|
-
const spawnOpts=(extra)=>({stdio:'ignore',windowsHide:true,...extra});
|
|
132
|
-
const hasCmd=(name)=>{
|
|
133
|
-
try{
|
|
134
|
-
execFileSync(name,['--version'],spawnOpts());
|
|
135
|
-
return true;
|
|
136
|
-
}catch{
|
|
137
|
-
try{
|
|
138
|
-
execFileSync(name,['--version'],spawnOpts({shell:true}));
|
|
139
|
-
return true;
|
|
140
|
-
}catch{
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
const run=(cmd,args)=>{
|
|
146
|
-
execFileSync(cmd,args,{cwd:root,stdio:'inherit',shell:true,windowsHide:true});
|
|
147
|
-
};
|
|
148
|
-
const markWarm=()=>{
|
|
149
|
-
try{
|
|
150
|
-
const dist=root+'/packages/cli/dist';
|
|
151
|
-
fs.mkdirSync(dist,{recursive:true});
|
|
152
|
-
fs.writeFileSync(dist+'/.deft-ts-build-stamp',new Date().toISOString());
|
|
153
|
-
}catch{}
|
|
154
|
-
};
|
|
155
|
-
const trySteps=(steps)=>{
|
|
156
|
-
for(const [cmd,args] of steps){
|
|
157
|
-
try{
|
|
158
|
-
run(cmd,args);
|
|
159
|
-
markWarm();
|
|
160
|
-
process.exit(0);
|
|
161
|
-
}catch{
|
|
162
|
-
// fall through to Corepack / next resolver
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
const envPm=String(process.env.DEFT_PACKAGE_MANAGER||'').trim().toLowerCase();
|
|
167
|
-
if(envPm==='npm'){
|
|
168
|
-
run('npm',['run',script]);
|
|
169
|
-
markWarm();
|
|
170
|
-
process.exit(0);
|
|
171
|
-
}
|
|
172
|
-
const pin=String(pkg.packageManager||'').trim();
|
|
173
|
-
const match=pin.match(/^pnpm@(.+)$/);
|
|
174
|
-
const steps=[];
|
|
175
|
-
if(hasCmd('pnpm')) steps.push(['pnpm',['run',script]]);
|
|
176
|
-
if(hasCmd('corepack')&&match) steps.push(['corepack',['pnpm@'+match[1],'run',script]]);
|
|
177
|
-
if(hasCmd('corepack')) steps.push(['corepack',['pnpm','run',script]]);
|
|
178
|
-
trySteps(steps);
|
|
179
|
-
console.error('deft: neither pnpm nor corepack is available to run \"'+script+'\".');
|
|
180
|
-
if(pin){
|
|
181
|
-
console.error(' Enable Corepack for the pinned manager: corepack enable && corepack prepare '+pin+' --activate');
|
|
182
|
-
}else{
|
|
183
|
-
console.error(' Install pnpm or enable Corepack (see package.json#packageManager).');
|
|
184
|
-
}
|
|
185
|
-
console.error(' Or set DEFT_PACKAGE_MANAGER=npm for an explicit npm build path.');
|
|
186
|
-
process.exit(127);
|
|
187
|
-
" "{{.DEFT_ROOT}}"
|
|
54
|
+
node "{{.TASKFILE_DIR}}/engine-pm-run.cjs" "{{.DEFT_ROOT}}" build --mark-warm
|
|
188
55
|
fi
|
|
189
56
|
|
|
190
57
|
invoke:
|