thumbgate 1.35.0 → 1.37.1
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/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +336 -3
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +581 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +9 -3
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +6 -0
- package/config/model-candidates.json +312 -29
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +390 -148
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-schema.js +163 -1
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard.js +251 -32
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gates-engine.js +810 -63
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +724 -153
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- package/src/security-questionnaire.js +195 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* override-audit.js — typed, first-class audit records for gate overrides.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS
|
|
7
|
+
* ---------------
|
|
8
|
+
* Before this module, an override left one of two traces:
|
|
9
|
+
*
|
|
10
|
+
* 1. Via the MCP tool -> a generic tool-call row in audit-trail.jsonl with
|
|
11
|
+
* toolName "satisfy_gate", indistinguishable by TYPE from every Bash call.
|
|
12
|
+
* 2. Via the CLI -> `satisfyCondition()` wrote ONLY to the gate state
|
|
13
|
+
* store. Nothing reached the audit trail at all. The override was invisible.
|
|
14
|
+
*
|
|
15
|
+
* Path 2 is the dangerous one: it is exactly the path an agent falls back to
|
|
16
|
+
* when the MCP tool is unavailable, so the least-supervised override was also
|
|
17
|
+
* the least-recorded one. An override log that misses overrides is worse than
|
|
18
|
+
* none, because it reads as completeness.
|
|
19
|
+
*
|
|
20
|
+
* This module gives overrides a typed shape (decision "override") that can be
|
|
21
|
+
* filtered, counted, and rendered, no matter which path produced them.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
|
|
27
|
+
const { recordAuditEvent, getAuditLogPath } = require('./audit-trail');
|
|
28
|
+
|
|
29
|
+
/** Marker written into every override record. */
|
|
30
|
+
const OVERRIDE_DECISION = 'override';
|
|
31
|
+
const OVERRIDE_TOOL_NAME = 'gate-override';
|
|
32
|
+
|
|
33
|
+
/** Where the override came from. Unknown callers are recorded, never dropped. */
|
|
34
|
+
const SOURCES = Object.freeze(['mcp', 'cli', 'break-glass', 'api', 'unknown']);
|
|
35
|
+
|
|
36
|
+
function normalizeSource(source) {
|
|
37
|
+
if (typeof source !== 'string') return 'unknown';
|
|
38
|
+
const lower = source.trim().toLowerCase();
|
|
39
|
+
return SOURCES.includes(lower) ? lower : 'unknown';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Truncate free text so a pathological evidence blob cannot bloat the log.
|
|
44
|
+
* Truncation is marked so a reader never mistakes a cut string for the whole.
|
|
45
|
+
*/
|
|
46
|
+
function clip(value, max = 2000) {
|
|
47
|
+
if (typeof value !== 'string') return null;
|
|
48
|
+
if (value.length <= max) return value;
|
|
49
|
+
return `${value.slice(0, max)}…[truncated ${value.length - max} chars]`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeReasoning(reasoning) {
|
|
53
|
+
if (!reasoning || typeof reasoning !== 'object' || Array.isArray(reasoning)) return null;
|
|
54
|
+
const out = {
|
|
55
|
+
premise: clip(reasoning.premise, 1000),
|
|
56
|
+
evidence: clip(reasoning.evidence, 1000),
|
|
57
|
+
risk: clip(reasoning.risk, 1000),
|
|
58
|
+
conclusion: clip(reasoning.conclusion, 1000),
|
|
59
|
+
};
|
|
60
|
+
// An object of four nulls is noise, not reasoning.
|
|
61
|
+
return Object.values(out).some((v) => v !== null) ? out : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Record one gate override.
|
|
66
|
+
*
|
|
67
|
+
* Never throws: an audit failure must not also break the caller's control flow,
|
|
68
|
+
* or a broken log would become a reason to skip recording entirely.
|
|
69
|
+
*
|
|
70
|
+
* @returns {object|null} the stored record, or null if recording failed
|
|
71
|
+
*/
|
|
72
|
+
function recordOverride(params = {}) {
|
|
73
|
+
const gateId = typeof params.gateId === 'string' && params.gateId.trim() ? params.gateId.trim() : null;
|
|
74
|
+
if (!gateId) return null; // an override with no gate is not attributable
|
|
75
|
+
|
|
76
|
+
const override = {
|
|
77
|
+
gateId,
|
|
78
|
+
source: normalizeSource(params.source),
|
|
79
|
+
actor: clip(params.actor, 200) || 'agent',
|
|
80
|
+
reason: clip(params.reason),
|
|
81
|
+
evidence: clip(params.evidence),
|
|
82
|
+
structuredReasoning: normalizeReasoning(params.structuredReasoning),
|
|
83
|
+
// A reasoned override is materially different from a bare one. Surface it
|
|
84
|
+
// as a first-class field so the panel can rank unreasoned overrides first.
|
|
85
|
+
reasoned: Boolean(normalizeReasoning(params.structuredReasoning)),
|
|
86
|
+
ttlMs: Number.isFinite(params.ttlMs) ? params.ttlMs : null,
|
|
87
|
+
pathGlobs: Array.isArray(params.pathGlobs) ? params.pathGlobs.slice(0, 50).map(String) : null,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
return recordAuditEvent({
|
|
92
|
+
toolName: OVERRIDE_TOOL_NAME,
|
|
93
|
+
toolInput: { gateId, source: override.source },
|
|
94
|
+
decision: OVERRIDE_DECISION,
|
|
95
|
+
gateId,
|
|
96
|
+
message: override.reason || `Override of ${gateId}`,
|
|
97
|
+
severity: 'high',
|
|
98
|
+
source: 'override-audit',
|
|
99
|
+
override,
|
|
100
|
+
});
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveLogPath(logPath) {
|
|
107
|
+
if (logPath) return logPath;
|
|
108
|
+
try {
|
|
109
|
+
return getAuditLogPath();
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read typed override records, newest first.
|
|
117
|
+
*
|
|
118
|
+
* Reads the tail of the file rather than the whole thing: audit logs reach
|
|
119
|
+
* hundreds of MB and a panel must not allocate the entire history to show 50 rows.
|
|
120
|
+
*/
|
|
121
|
+
function readOverrides(options = {}) {
|
|
122
|
+
const logPath = resolveLogPath(options.logPath);
|
|
123
|
+
if (!logPath || !fs.existsSync(logPath)) return [];
|
|
124
|
+
|
|
125
|
+
const limit = Number.isFinite(options.limit) ? Math.max(0, options.limit) : 100;
|
|
126
|
+
if (limit === 0) return [];
|
|
127
|
+
const sinceMs = Number.isFinite(options.sinceMs) ? options.sinceMs : null;
|
|
128
|
+
const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 8 * 1024 * 1024;
|
|
129
|
+
|
|
130
|
+
let text = '';
|
|
131
|
+
let fd;
|
|
132
|
+
try {
|
|
133
|
+
const { size } = fs.statSync(logPath);
|
|
134
|
+
if (size === 0) return [];
|
|
135
|
+
const readBytes = Math.min(size, maxBytes);
|
|
136
|
+
const buf = Buffer.alloc(readBytes);
|
|
137
|
+
fd = fs.openSync(logPath, 'r');
|
|
138
|
+
fs.readSync(fd, buf, 0, readBytes, size - readBytes);
|
|
139
|
+
text = buf.toString('utf8');
|
|
140
|
+
if (readBytes < size) {
|
|
141
|
+
const nl = text.indexOf('\n');
|
|
142
|
+
text = nl === -1 ? '' : text.slice(nl + 1); // drop the partial first record
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
return [];
|
|
146
|
+
} finally {
|
|
147
|
+
if (fd !== undefined) {
|
|
148
|
+
try { fs.closeSync(fd); } catch { /* already closed */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const out = [];
|
|
153
|
+
const lines = text.split('\n');
|
|
154
|
+
for (let i = lines.length - 1; i >= 0 && out.length < limit; i -= 1) {
|
|
155
|
+
const line = lines[i];
|
|
156
|
+
if (!line) continue;
|
|
157
|
+
// Cheap reject before JSON.parse — most rows are not overrides.
|
|
158
|
+
if (!line.includes(`"${OVERRIDE_DECISION}"`)) continue;
|
|
159
|
+
let rec;
|
|
160
|
+
try {
|
|
161
|
+
rec = JSON.parse(line);
|
|
162
|
+
} catch {
|
|
163
|
+
continue; // a corrupt line must not abort the scan
|
|
164
|
+
}
|
|
165
|
+
if (!rec || rec.decision !== OVERRIDE_DECISION || !rec.override) continue;
|
|
166
|
+
if (sinceMs !== null) {
|
|
167
|
+
const ts = Date.parse(rec.timestamp);
|
|
168
|
+
if (Number.isFinite(ts) && ts < sinceMs) continue;
|
|
169
|
+
}
|
|
170
|
+
out.push(rec);
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Aggregate overrides for the dashboard panel.
|
|
177
|
+
* Unreasoned overrides are counted separately because they are the ones a
|
|
178
|
+
* reviewer should look at first.
|
|
179
|
+
*/
|
|
180
|
+
function summarizeOverrides(options = {}) {
|
|
181
|
+
const records = readOverrides({ ...options, limit: options.limit ?? 500 });
|
|
182
|
+
const byGate = {};
|
|
183
|
+
const bySource = {};
|
|
184
|
+
const byActor = {};
|
|
185
|
+
let unreasoned = 0;
|
|
186
|
+
|
|
187
|
+
for (const rec of records) {
|
|
188
|
+
const o = rec.override || {};
|
|
189
|
+
byGate[o.gateId] = (byGate[o.gateId] || 0) + 1;
|
|
190
|
+
bySource[o.source] = (bySource[o.source] || 0) + 1;
|
|
191
|
+
byActor[o.actor] = (byActor[o.actor] || 0) + 1;
|
|
192
|
+
if (!o.reasoned) unreasoned += 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
total: records.length,
|
|
197
|
+
unreasoned,
|
|
198
|
+
byGate,
|
|
199
|
+
bySource,
|
|
200
|
+
byActor,
|
|
201
|
+
mostRecent: records[0] || null,
|
|
202
|
+
records,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
recordOverride,
|
|
208
|
+
readOverrides,
|
|
209
|
+
summarizeOverrides,
|
|
210
|
+
OVERRIDE_DECISION,
|
|
211
|
+
OVERRIDE_TOOL_NAME,
|
|
212
|
+
SOURCES,
|
|
213
|
+
};
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Package-manager honesty doctor (InfoQ pnpm 12 process steal).
|
|
6
|
+
*
|
|
7
|
+
* Source: https://www.infoq.com/news/2026/09/pnpm-12-rust/
|
|
8
|
+
*
|
|
9
|
+
* Transfers:
|
|
10
|
+
* - Single lockfile + packageManager pin honesty
|
|
11
|
+
* - CI install command must match the lockfile manager
|
|
12
|
+
* - Lifecycle scripts are spicy by default on npm — prefer --ignore-scripts
|
|
13
|
+
* evidence for agent/untrusted installs
|
|
14
|
+
* - Fail-closed when an agent proposes switching managers without CI rewrite
|
|
15
|
+
* - pnpm 12 compat checklist (resolution-only → peers check, etc.) when
|
|
16
|
+
* switching TO pnpm — without migrating this repo off npm
|
|
17
|
+
*
|
|
18
|
+
* Does NOT migrate ThumbGate from npm → pnpm. CI stays on npm ci.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
|
|
24
|
+
const SOURCE_URL = 'https://www.infoq.com/news/2026/09/pnpm-12-rust/';
|
|
25
|
+
const LOCKFILES = [
|
|
26
|
+
{ file: 'package-lock.json', manager: 'npm' },
|
|
27
|
+
{ file: 'pnpm-lock.yaml', manager: 'pnpm' },
|
|
28
|
+
{ file: 'yarn.lock', manager: 'yarn' },
|
|
29
|
+
{ file: 'bun.lockb', manager: 'bun' },
|
|
30
|
+
{ file: 'bun.lock', manager: 'bun' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const PNPM12_COMPAT_CHECKS = [
|
|
34
|
+
{
|
|
35
|
+
id: 'resolution-only-removed',
|
|
36
|
+
legacy: 'pnpm install --resolution-only',
|
|
37
|
+
replacement: 'pnpm peers check',
|
|
38
|
+
note: 'pnpm 12 removed --resolution-only; CI that still uses it will break.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'workspace-unknown-keys',
|
|
42
|
+
legacy: 'unknown keys silently ignored in pnpm-workspace.yaml',
|
|
43
|
+
replacement: 'unknown keys reported as errors',
|
|
44
|
+
note: 'Audit pnpm-workspace.yaml before upgrading; unknown keys now fail closed.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'git-canonical-https',
|
|
48
|
+
legacy: 'git+ssh://github.com/... style git deps',
|
|
49
|
+
replacement: 'canonical HTTPS + Git URL rewrite for private SSH',
|
|
50
|
+
note: 'GitHub/GitLab/Bitbucket git deps resolve via HTTPS; configure URL rewriting for private SSH.',
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function normalizeBoolean(value) {
|
|
55
|
+
if (value === true) return true;
|
|
56
|
+
if (value === false || value === undefined || value === null) return false;
|
|
57
|
+
return /^(1|true|yes|on)$/i.test(String(value).trim());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function detectLockfiles(rootDir) {
|
|
61
|
+
return LOCKFILES
|
|
62
|
+
.filter((entry) => fs.existsSync(path.join(rootDir, entry.file)))
|
|
63
|
+
.map((entry) => ({ ...entry, path: path.join(rootDir, entry.file) }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readPackageManagerPin(rootDir) {
|
|
67
|
+
const pkgPath = path.join(rootDir, 'package.json');
|
|
68
|
+
if (!fs.existsSync(pkgPath)) return { path: pkgPath, pin: null, raw: null };
|
|
69
|
+
let pkg;
|
|
70
|
+
try {
|
|
71
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
72
|
+
} catch {
|
|
73
|
+
return { path: pkgPath, pin: null, raw: null, parseError: true };
|
|
74
|
+
}
|
|
75
|
+
const raw = pkg.packageManager ? String(pkg.packageManager) : null;
|
|
76
|
+
if (!raw) return { path: pkgPath, pin: null, raw: null };
|
|
77
|
+
const m = /^(npm|pnpm|yarn|bun)@/.exec(raw);
|
|
78
|
+
return { path: pkgPath, pin: m ? m[1] : raw.split('@')[0], raw };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function listWorkflowFiles(workflowsDir) {
|
|
82
|
+
if (!fs.existsSync(workflowsDir)) return [];
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const name of fs.readdirSync(workflowsDir)) {
|
|
85
|
+
if (!/\.ya?ml$/i.test(name)) continue;
|
|
86
|
+
out.push(path.join(workflowsDir, name));
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract install-ish commands from workflow YAML text (line-oriented, not a YAML parser).
|
|
93
|
+
* Only inspects `run:` steps (including folded `|` / `>` blocks) and splits shell
|
|
94
|
+
* chains (`&&`, `;`, `|`) so each manager+flags pair is recorded separately.
|
|
95
|
+
*/
|
|
96
|
+
function extractInstallCommands(text, filePath) {
|
|
97
|
+
const lines = String(text).split(/\r?\n/);
|
|
98
|
+
const commands = [];
|
|
99
|
+
const installRe = /\b(npm\s+ci|npm\s+install|npm\s+i\b|pnpm\s+(?:i|install|fetch|import)|yarn\s+(?:install|immutable)|bun\s+install)\b/i;
|
|
100
|
+
const runStartRe = /^(\s*)-\s*run:\s*(.*)$/;
|
|
101
|
+
|
|
102
|
+
function recordCommand(cmdText, lineNo) {
|
|
103
|
+
const trimmed = String(cmdText || '').trim();
|
|
104
|
+
if (!trimmed || trimmed.startsWith('#') || !installRe.test(trimmed)) return;
|
|
105
|
+
let manager = 'npm';
|
|
106
|
+
if (/\bpnpm\b/i.test(trimmed)) manager = 'pnpm';
|
|
107
|
+
else if (/\byarn\b/i.test(trimmed)) manager = 'yarn';
|
|
108
|
+
else if (/\bbun\b/i.test(trimmed)) manager = 'bun';
|
|
109
|
+
commands.push({
|
|
110
|
+
file: filePath,
|
|
111
|
+
line: lineNo,
|
|
112
|
+
text: trimmed,
|
|
113
|
+
manager,
|
|
114
|
+
ignoreScripts: /--ignore-scripts\b/.test(trimmed),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function splitShellChain(raw) {
|
|
119
|
+
// Split on &&, ;, and bare | (not ||). Keep install segments intact.
|
|
120
|
+
return String(raw)
|
|
121
|
+
.split(/(?:&&|;|(?<!\|)\|(?!\|))/)
|
|
122
|
+
.map((s) => s.trim())
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
127
|
+
const match = runStartRe.exec(lines[i]);
|
|
128
|
+
if (!match) continue;
|
|
129
|
+
const indent = match[1].length;
|
|
130
|
+
let rest = match[2].trim();
|
|
131
|
+
let startLine = i + 1;
|
|
132
|
+
|
|
133
|
+
if (rest === '|' || rest === '>' || rest === '|-' || rest === '>-') {
|
|
134
|
+
// Multiline run block: each deeper-indented line is its own shell statement
|
|
135
|
+
// (and may itself contain && / ; chains).
|
|
136
|
+
let j = i + 1;
|
|
137
|
+
while (j < lines.length) {
|
|
138
|
+
const next = lines[j];
|
|
139
|
+
if (!next.trim()) { j += 1; continue; }
|
|
140
|
+
const nextIndent = next.match(/^(\s*)/)[1].length;
|
|
141
|
+
if (nextIndent <= indent) break;
|
|
142
|
+
const lineNo = j + 1;
|
|
143
|
+
for (const segment of splitShellChain(next.trim())) {
|
|
144
|
+
recordCommand(segment, lineNo);
|
|
145
|
+
}
|
|
146
|
+
j += 1;
|
|
147
|
+
}
|
|
148
|
+
i = j - 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!rest) continue;
|
|
152
|
+
|
|
153
|
+
for (const segment of splitShellChain(rest)) {
|
|
154
|
+
recordCommand(segment, startLine);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return commands;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function scanWorkflowInstalls(rootDir) {
|
|
161
|
+
const workflowsDir = path.join(rootDir, '.github', 'workflows');
|
|
162
|
+
const files = listWorkflowFiles(workflowsDir);
|
|
163
|
+
const commands = [];
|
|
164
|
+
for (const file of files) {
|
|
165
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
166
|
+
commands.push(...extractInstallCommands(text, path.relative(rootDir, file)));
|
|
167
|
+
}
|
|
168
|
+
return commands;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function inferCanonicalManager(lockfiles, pin) {
|
|
172
|
+
if (lockfiles.length === 1) return lockfiles[0].manager;
|
|
173
|
+
if (pin && lockfiles.some((l) => l.manager === pin)) return pin;
|
|
174
|
+
if (lockfiles.some((l) => l.manager === 'npm')) return 'npm';
|
|
175
|
+
return lockfiles[0] ? lockfiles[0].manager : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildFindings({
|
|
179
|
+
lockfiles,
|
|
180
|
+
pinInfo,
|
|
181
|
+
installs,
|
|
182
|
+
canonical,
|
|
183
|
+
proposeSwitch,
|
|
184
|
+
allowIgnoreScriptsGaps,
|
|
185
|
+
}) {
|
|
186
|
+
const findings = [];
|
|
187
|
+
|
|
188
|
+
if (lockfiles.length === 0) {
|
|
189
|
+
findings.push({
|
|
190
|
+
id: 'no_lockfile',
|
|
191
|
+
severity: 'fail',
|
|
192
|
+
gateId: 'require-package-manager-lockfile-ci-parity',
|
|
193
|
+
message: 'No package-lock.json / pnpm-lock.yaml / yarn.lock / bun.lock found. Installs are not reproducible.',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (lockfiles.length > 1) {
|
|
198
|
+
findings.push({
|
|
199
|
+
id: 'multiple_lockfiles',
|
|
200
|
+
severity: 'fail',
|
|
201
|
+
gateId: 'require-package-manager-lockfile-ci-parity',
|
|
202
|
+
message: `Multiple lockfiles present (${lockfiles.map((l) => l.file).join(', ')}). Keep exactly one manager+lockfile pair.`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (pinInfo.pin && canonical && pinInfo.pin !== canonical) {
|
|
207
|
+
findings.push({
|
|
208
|
+
id: 'package_manager_pin_mismatch',
|
|
209
|
+
severity: 'fail',
|
|
210
|
+
gateId: 'require-package-manager-lockfile-ci-parity',
|
|
211
|
+
message: `package.json packageManager=${pinInfo.raw} disagrees with lockfile manager=${canonical}.`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (pinInfo.parseError) {
|
|
216
|
+
findings.push({
|
|
217
|
+
id: 'package_json_parse_error',
|
|
218
|
+
severity: 'fail',
|
|
219
|
+
gateId: 'require-package-manager-lockfile-ci-parity',
|
|
220
|
+
message: 'package.json could not be parsed. Fix JSON before trusting packageManager pin / install commands.',
|
|
221
|
+
});
|
|
222
|
+
} else if (!pinInfo.pin && canonical) {
|
|
223
|
+
findings.push({
|
|
224
|
+
id: 'package_manager_pin_missing',
|
|
225
|
+
severity: 'info',
|
|
226
|
+
gateId: null,
|
|
227
|
+
message: `No packageManager pin in package.json. Canonical lockfile manager is ${canonical}. Pinning is optional for npm-default repos but required before switching managers.`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const mismatchedCi = installs.filter((c) => canonical && c.manager !== canonical);
|
|
232
|
+
if (mismatchedCi.length > 0) {
|
|
233
|
+
const sample = mismatchedCi.slice(0, 3)
|
|
234
|
+
.map((c) => `${c.file}:${c.line} (${c.manager})`)
|
|
235
|
+
.join('; ');
|
|
236
|
+
findings.push({
|
|
237
|
+
id: 'ci_install_manager_mismatch',
|
|
238
|
+
severity: 'fail',
|
|
239
|
+
gateId: 'require-package-manager-lockfile-ci-parity',
|
|
240
|
+
message: `${mismatchedCi.length} CI install command(s) use a different manager than lockfile ${canonical}. Examples: ${sample}`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const withoutIgnore = installs.filter((c) => !c.ignoreScripts);
|
|
245
|
+
if (withoutIgnore.length > 0 && !allowIgnoreScriptsGaps) {
|
|
246
|
+
findings.push({
|
|
247
|
+
id: 'ci_install_without_ignore_scripts',
|
|
248
|
+
severity: 'warn',
|
|
249
|
+
gateId: 'block-package-lifecycle-secret-harvest',
|
|
250
|
+
message: `${withoutIgnore.length}/${installs.length} CI install lines lack --ignore-scripts. npm runs lifecycle scripts by default (spicy); prefer --ignore-scripts unless prepare/build scripts are required and reviewed.`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (proposeSwitch) {
|
|
255
|
+
const target = String(proposeSwitch).trim().toLowerCase();
|
|
256
|
+
if (!['npm', 'pnpm', 'yarn', 'bun'].includes(target)) {
|
|
257
|
+
findings.push({
|
|
258
|
+
id: 'unknown_switch_target',
|
|
259
|
+
severity: 'fail',
|
|
260
|
+
gateId: 'checkpoint-package-manager-switch',
|
|
261
|
+
message: `Unknown --propose-switch=${proposeSwitch}. Expected npm|pnpm|yarn|bun.`,
|
|
262
|
+
});
|
|
263
|
+
} else if (canonical && target === canonical) {
|
|
264
|
+
findings.push({
|
|
265
|
+
id: 'switch_noop',
|
|
266
|
+
severity: 'info',
|
|
267
|
+
gateId: null,
|
|
268
|
+
message: `Proposed switch target ${target} already matches canonical manager.`,
|
|
269
|
+
});
|
|
270
|
+
} else {
|
|
271
|
+
findings.push({
|
|
272
|
+
id: 'switch_without_migration_plan',
|
|
273
|
+
severity: 'fail',
|
|
274
|
+
gateId: 'checkpoint-package-manager-switch',
|
|
275
|
+
message: `Refuse silent switch ${canonical || 'unknown'} → ${target}. Require: single new lockfile, packageManager pin, rewrite every CI install, delete the old lockfile, and re-run npm pack/CI green. ThumbGate stays on npm unless that plan is complete.`,
|
|
276
|
+
});
|
|
277
|
+
if (target === 'pnpm') {
|
|
278
|
+
for (const check of PNPM12_COMPAT_CHECKS) {
|
|
279
|
+
findings.push({
|
|
280
|
+
id: `pnpm12_${check.id}`,
|
|
281
|
+
severity: 'warn',
|
|
282
|
+
gateId: 'checkpoint-package-manager-switch',
|
|
283
|
+
message: `pnpm 12 compat: replace "${check.legacy}" with "${check.replacement}". ${check.note}`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return findings;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function normalizeOptions(options = {}) {
|
|
294
|
+
const rootDir = path.resolve(
|
|
295
|
+
String(options.root || options.cwd || options['root-dir'] || process.cwd())
|
|
296
|
+
);
|
|
297
|
+
return {
|
|
298
|
+
rootDir,
|
|
299
|
+
proposeSwitch: options['propose-switch'] || options.switch || options.propose || null,
|
|
300
|
+
allowIgnoreScriptsGaps: normalizeBoolean(options['allow-ignore-scripts-gaps']),
|
|
301
|
+
strict: normalizeBoolean(options.strict),
|
|
302
|
+
json: normalizeBoolean(options.json),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function buildPackageManagerHonestyReport(rawOptions = {}) {
|
|
307
|
+
const options = normalizeOptions(rawOptions);
|
|
308
|
+
const lockfiles = detectLockfiles(options.rootDir);
|
|
309
|
+
const pinInfo = readPackageManagerPin(options.rootDir);
|
|
310
|
+
const installs = scanWorkflowInstalls(options.rootDir);
|
|
311
|
+
const canonical = inferCanonicalManager(lockfiles, pinInfo.pin);
|
|
312
|
+
const findings = buildFindings({
|
|
313
|
+
lockfiles,
|
|
314
|
+
pinInfo,
|
|
315
|
+
installs,
|
|
316
|
+
canonical,
|
|
317
|
+
proposeSwitch: options.proposeSwitch,
|
|
318
|
+
allowIgnoreScriptsGaps: options.allowIgnoreScriptsGaps,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const failCount = findings.filter((f) => f.severity === 'fail').length;
|
|
322
|
+
const warnCount = findings.filter((f) => f.severity === 'warn').length;
|
|
323
|
+
let status = 'ready';
|
|
324
|
+
if (failCount > 0) status = 'fail';
|
|
325
|
+
else if (warnCount > 0) status = 'actionable';
|
|
326
|
+
|
|
327
|
+
const ignoreScriptsCount = installs.filter((c) => c.ignoreScripts).length;
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
name: 'thumbgate-package-manager-honesty-doctor',
|
|
331
|
+
status,
|
|
332
|
+
source: SOURCE_URL,
|
|
333
|
+
disclaimer:
|
|
334
|
+
'Process steal from InfoQ pnpm 12 coverage (Rust rewrite, lockfile honesty, lifecycle defaults). Does not migrate this repo to pnpm and is not affiliated with pnpm.',
|
|
335
|
+
rootDir: options.rootDir,
|
|
336
|
+
metrics: {
|
|
337
|
+
lockfileCount: lockfiles.length,
|
|
338
|
+
lockfiles: lockfiles.map((l) => l.file),
|
|
339
|
+
canonicalManager: canonical,
|
|
340
|
+
packageManagerPin: pinInfo.raw,
|
|
341
|
+
ciInstallCount: installs.length,
|
|
342
|
+
ciIgnoreScriptsCount: ignoreScriptsCount,
|
|
343
|
+
ciIgnoreScriptsRatio: installs.length
|
|
344
|
+
? Number((ignoreScriptsCount / installs.length).toFixed(4))
|
|
345
|
+
: null,
|
|
346
|
+
proposeSwitch: options.proposeSwitch || null,
|
|
347
|
+
},
|
|
348
|
+
findings,
|
|
349
|
+
summary: {
|
|
350
|
+
failCount,
|
|
351
|
+
warnCount,
|
|
352
|
+
findingCount: findings.length,
|
|
353
|
+
recommendedGateCount: [...new Set(findings.map((f) => f.gateId).filter(Boolean))].length,
|
|
354
|
+
},
|
|
355
|
+
recommendedGates: [...new Set(findings.map((f) => f.gateId).filter(Boolean))],
|
|
356
|
+
nextActions: [
|
|
357
|
+
'Keep exactly one lockfile; do not add pnpm-lock.yaml beside package-lock.json.',
|
|
358
|
+
'CI install manager must match the lockfile (ThumbGate: npm ci).',
|
|
359
|
+
'Prefer --ignore-scripts on agent/untrusted installs; npm runs lifecycle scripts by default.',
|
|
360
|
+
'Refuse package-manager switches without pin + CI rewrite + old lockfile deletion.',
|
|
361
|
+
'If evaluating pnpm 12: use peers check (not --resolution-only) and audit workspace unknown keys — do not rewrite this public npm package casually.',
|
|
362
|
+
],
|
|
363
|
+
exampleCommand:
|
|
364
|
+
'npx thumbgate package-manager-honesty-doctor --propose-switch=pnpm --json',
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function formatPackageManagerHonestyReport(report) {
|
|
369
|
+
const lines = [
|
|
370
|
+
'',
|
|
371
|
+
'ThumbGate Package-Manager Honesty Doctor',
|
|
372
|
+
'-'.repeat(40),
|
|
373
|
+
`Status : ${report.status}`,
|
|
374
|
+
`Root : ${report.rootDir}`,
|
|
375
|
+
`Canonical: ${report.metrics.canonicalManager || 'none'}`,
|
|
376
|
+
`Lockfiles: ${report.metrics.lockfiles.join(', ') || 'none'}`,
|
|
377
|
+
`Pin : ${report.metrics.packageManagerPin || '(unset)'}`,
|
|
378
|
+
`CI installs: ${report.metrics.ciInstallCount} (ignore-scripts ${report.metrics.ciIgnoreScriptsCount})`,
|
|
379
|
+
`Findings : ${report.summary.findingCount} (fail=${report.summary.failCount}, warn=${report.summary.warnCount})`,
|
|
380
|
+
`Source : ${report.source}`,
|
|
381
|
+
];
|
|
382
|
+
if (report.findings.length) {
|
|
383
|
+
lines.push('', 'Findings:');
|
|
384
|
+
for (const f of report.findings) {
|
|
385
|
+
const gate = f.gateId ? ` [${f.gateId}]` : '';
|
|
386
|
+
lines.push(` - [${f.severity}] ${f.id}${gate}`);
|
|
387
|
+
lines.push(` ${f.message}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
lines.push('', 'Next actions:');
|
|
391
|
+
for (const a of report.nextActions) lines.push(` - ${a}`);
|
|
392
|
+
lines.push('', `Example: ${report.exampleCommand}`);
|
|
393
|
+
lines.push(`Note: ${report.disclaimer}`, '');
|
|
394
|
+
return `${lines.join('\n')}\n`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function parseCliArgs(argv) {
|
|
398
|
+
const options = {};
|
|
399
|
+
for (const arg of argv) {
|
|
400
|
+
if (arg === '--json') { options.json = true; continue; }
|
|
401
|
+
if (arg === '--strict') { options.strict = true; continue; }
|
|
402
|
+
if (arg === '--help' || arg === '-h') { options.help = true; continue; }
|
|
403
|
+
const m = /^--([^=]+)(?:=(.*))?$/.exec(arg);
|
|
404
|
+
if (!m) continue;
|
|
405
|
+
options[m[1]] = m[2] === undefined ? true : m[2];
|
|
406
|
+
}
|
|
407
|
+
return options;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function printHelp() {
|
|
411
|
+
process.stdout.write(`Usage: node scripts/package-manager-honesty-doctor.js [flags]
|
|
412
|
+
|
|
413
|
+
Flags:
|
|
414
|
+
--root=DIR Repo root to scan (default: cwd)
|
|
415
|
+
--propose-switch=pnpm|yarn|bun|npm
|
|
416
|
+
Fail-closed checklist for a manager switch
|
|
417
|
+
--allow-ignore-scripts-gaps
|
|
418
|
+
Downgrade missing --ignore-scripts to silence
|
|
419
|
+
--strict Exit 1 on fail/actionable
|
|
420
|
+
--json
|
|
421
|
+
|
|
422
|
+
Source: ${SOURCE_URL}
|
|
423
|
+
`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function runCli(argv = process.argv.slice(2)) {
|
|
427
|
+
const args = parseCliArgs(argv);
|
|
428
|
+
if (args.help) {
|
|
429
|
+
printHelp();
|
|
430
|
+
return 0;
|
|
431
|
+
}
|
|
432
|
+
const report = buildPackageManagerHonestyReport(args);
|
|
433
|
+
if (args.json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
434
|
+
else process.stdout.write(formatPackageManagerHonestyReport(report));
|
|
435
|
+
if (args.strict && report.status !== 'ready') return 1;
|
|
436
|
+
if (report.status === 'fail') return 1;
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
module.exports = {
|
|
441
|
+
SOURCE_URL,
|
|
442
|
+
LOCKFILES,
|
|
443
|
+
PNPM12_COMPAT_CHECKS,
|
|
444
|
+
detectLockfiles,
|
|
445
|
+
readPackageManagerPin,
|
|
446
|
+
extractInstallCommands,
|
|
447
|
+
scanWorkflowInstalls,
|
|
448
|
+
inferCanonicalManager,
|
|
449
|
+
buildPackageManagerHonestyReport,
|
|
450
|
+
formatPackageManagerHonestyReport,
|
|
451
|
+
normalizeOptions,
|
|
452
|
+
parseCliArgs,
|
|
453
|
+
runCli,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) {
|
|
457
|
+
process.exitCode = runCli(process.argv.slice(2));
|
|
458
|
+
}
|