depsentinel 0.1.2

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/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # depsentinel
2
+
3
+ **JS/TS supply-chain hardening CLI.** Detect your package manager, evaluate risk, and apply secure defaults — in one command.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npx depsentinel scan # diagnose your project
9
+ npx depsentinel init --write # generate secure baseline
10
+ npx depsentinel ci --json # fail CI if critical policies violated
11
+ npx depsentinel install express # check package safety before adding it
12
+ npx depsentinel doctor # full 26-point security diagnosis
13
+ ```
14
+
15
+ ## Commands
16
+
17
+ | Command | Purpose |
18
+ |---|---|
19
+ | `scan` | Detect PM, lockfile, framework; return `risk_score` + `remediation_commands` |
20
+ | `init --write` | Generate `.npmrc`, `.npmignore`, CI workflow, PM-specific configs |
21
+ | `ci --json` | Policy gate for CI pipelines; exits non-zero on critical findings |
22
+ | `install <pkg>` | Preflight check before adding a dependency (`allow\|warn\|block`) |
23
+ | `doctor` | Diagnose project against 26 npm security best practices |
24
+ | `fix --write` | Auto-apply known remediations (`.npmrc`, scripts, configs) |
25
+ | `trust add\|remove\|list` | Manage allow/ignore build-script trust per package manager |
26
+ | `override add\|remove\|list` | Manage policy exceptions with reason and expiration |
27
+
28
+ ## What it enforces
29
+
30
+ - **Disable post-install scripts** — `.npmrc` `ignore-scripts=true`
31
+ - **Block git-based dependencies** — `.npmrc` `allow-git=none`
32
+ - **Package cooldown** — `.npmrc` `min-release-age=3` (3 days)
33
+ - **pnpm hardening** — `minimumReleaseAge`, `trustPolicy: no-downgrade`, `blockExoticSubdeps`, `strictDepBuilds`
34
+ - **Deterministic CI installs** — frozen lockfile per package manager
35
+ - **Advisory checks** — critical vulnerability matching
36
+ - **Tool adapters** — optional `npq`, `sfw`, `lockfile-lint` integration
37
+ - **Override system** — time-bound exceptions with documented reasons
38
+
39
+ Supports **npm**, **pnpm**, **yarn**, and **bun** — auto-detected. Unknown frameworks degrade gracefully to universal JS/TS baseline.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ npm install -g depsentinel
45
+ # or
46
+ pnpm add -g depsentinel
47
+ ```
48
+
49
+ ## Docs
50
+
51
+ Full security best-practices guide with 26-point coverage matrix: [`docs/security-best-practices.md`](docs/security-best-practices.md)
52
+
53
+ ## License
54
+
55
+ MIT
@@ -0,0 +1,25 @@
1
+ const FIXTURE_CRITICAL_ADVISORIES = [
2
+ {
3
+ packageName: "vulnerable-lib",
4
+ affectedVersion: "1.0.0",
5
+ advisoryId: "DSA-2026-0001",
6
+ title: "Remote code execution in vulnerable-lib"
7
+ }
8
+ ];
9
+ function matchFixture(facts, fixtures) {
10
+ for (const fixture of fixtures) {
11
+ const dependencyVersion = facts.dependencies[fixture.packageName];
12
+ if (dependencyVersion === fixture.affectedVersion) {
13
+ return {
14
+ packageName: fixture.packageName,
15
+ affectedVersion: fixture.affectedVersion,
16
+ advisoryId: fixture.advisoryId,
17
+ title: fixture.title
18
+ };
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ export const fixtureAdvisorySource = {
24
+ findCriticalMatch: (facts) => matchFixture(facts, FIXTURE_CRITICAL_ADVISORIES)
25
+ };
@@ -0,0 +1,83 @@
1
+ const DEFAULT_TTL_MS = 60 * 60 * 1000;
2
+ const GRACE_MS = 15 * 60 * 1000;
3
+ export function createLiveAdvisorySource(options) {
4
+ const ttl = options.ttlMs ?? DEFAULT_TTL_MS;
5
+ const fallback = options.fallbackSource ?? null;
6
+ let cache = null;
7
+ let refreshInFlight = null;
8
+ function isFresh() {
9
+ if (!cache)
10
+ return false;
11
+ return Date.now() - cache.fetchedAt < ttl;
12
+ }
13
+ function isGraceStale() {
14
+ if (!cache)
15
+ return false;
16
+ return Date.now() - cache.fetchedAt < ttl + GRACE_MS;
17
+ }
18
+ async function refresh() {
19
+ if (refreshInFlight)
20
+ return refreshInFlight;
21
+ refreshInFlight = (async () => {
22
+ try {
23
+ const controller = new AbortController();
24
+ const to = setTimeout(() => controller.abort(), 10000);
25
+ const res = await fetch(options.endpoint, { signal: controller.signal });
26
+ clearTimeout(to);
27
+ if (!res.ok)
28
+ return false;
29
+ const data = (await res.json());
30
+ cache = { advisories: data.advisories ?? [], fetchedAt: Date.now() };
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ finally {
37
+ refreshInFlight = null;
38
+ }
39
+ })();
40
+ return refreshInFlight;
41
+ }
42
+ function findCriticalMatch(facts) {
43
+ if (cache && isFresh()) {
44
+ return matchAdvisories(facts, cache.advisories);
45
+ }
46
+ if (cache && isGraceStale()) {
47
+ void refresh();
48
+ return matchAdvisories(facts, cache.advisories);
49
+ }
50
+ if (fallback) {
51
+ const match = fallback.findCriticalMatch(facts);
52
+ void refresh();
53
+ return match;
54
+ }
55
+ void refresh();
56
+ return null;
57
+ }
58
+ function getStatus() {
59
+ return {
60
+ source: options.endpoint,
61
+ lastFetched: cache?.fetchedAt ?? null,
62
+ cachedCount: cache?.advisories.length ?? 0,
63
+ ttlMs: ttl
64
+ };
65
+ }
66
+ return { findCriticalMatch, refresh, getStatus };
67
+ }
68
+ function matchAdvisories(facts, advisories) {
69
+ for (const adv of advisories) {
70
+ if (adv.severity !== "critical")
71
+ continue;
72
+ const version = facts.dependencies[adv.packageName];
73
+ if (version && version === adv.affectedVersion) {
74
+ return {
75
+ packageName: adv.packageName,
76
+ affectedVersion: adv.affectedVersion,
77
+ advisoryId: adv.advisoryId,
78
+ title: adv.title
79
+ };
80
+ }
81
+ }
82
+ return null;
83
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ import { cac } from "cac";
3
+ import { runCi } from "./commands/ci.js";
4
+ import { runDoctor } from "./commands/doctor.js";
5
+ import { runFix } from "./commands/fix.js";
6
+ import { runInit } from "./commands/init.js";
7
+ import { runInstall } from "./commands/install.js";
8
+ import { runScan } from "./commands/scan.js";
9
+ import { runTrust } from "./commands/trust.js";
10
+ import { overrideAdd, overrideList, overrideRemove } from "./core/overrides.js";
11
+ export function createCli() {
12
+ const cli = cac("depsentinel");
13
+ cli
14
+ .command("scan", "Run dependency risk scan")
15
+ .option("--json", "Emit machine-readable JSON")
16
+ .action((options) => {
17
+ const result = runScan({ json: Boolean(options.json) });
18
+ process.stdout.write(`${result.output}\n`);
19
+ const hasCritical = result.envelope.result.findings.some((finding) => finding.severity === "critical");
20
+ if (hasCritical) {
21
+ process.exitCode = 1;
22
+ }
23
+ });
24
+ cli
25
+ .command("ci", "Run CI policy gate")
26
+ .option("--json", "Emit machine-readable JSON")
27
+ .action((options) => {
28
+ const result = runCi({ json: Boolean(options.json) });
29
+ process.stdout.write(`${result.output}\n`);
30
+ if (result.shouldFail) {
31
+ process.exitCode = 1;
32
+ }
33
+ });
34
+ cli
35
+ .command("init", "Initialize depsentinel policy artifacts")
36
+ .option("--preset <preset>", "Preset to initialize (base|expo)")
37
+ .option("--write", "Apply changes (default is dry-run)")
38
+ .option("--json", "Emit machine-readable JSON")
39
+ .action((options) => {
40
+ const result = runInit({
41
+ preset: options.preset,
42
+ dryRun: !Boolean(options.write),
43
+ json: Boolean(options.json)
44
+ });
45
+ process.stdout.write(`${result.output}\n`);
46
+ });
47
+ cli
48
+ .command("install <package>", "Evaluate and execute safe dependency install")
49
+ .option("--force", "Override block/warn decision")
50
+ .option("--json", "Emit machine-readable JSON")
51
+ .action((pkg, options) => {
52
+ const result = runInstall({
53
+ packageName: pkg,
54
+ force: Boolean(options.force),
55
+ json: Boolean(options.json)
56
+ });
57
+ process.stdout.write(`${result.output}\n`);
58
+ process.exitCode = result.exitCode;
59
+ });
60
+ cli
61
+ .command("doctor", "Diagnose project against 26 security best practices")
62
+ .option("--json", "Emit machine-readable JSON")
63
+ .action((options) => {
64
+ const result = runDoctor({ json: Boolean(options.json) });
65
+ process.stdout.write(`${result.output}\n`);
66
+ if (result.envelope.result.failed > 0) {
67
+ process.exitCode = 1;
68
+ }
69
+ });
70
+ cli
71
+ .command("fix", "Apply known remediations for detected gaps (dry-run by default)")
72
+ .option("--write", "Apply changes")
73
+ .option("--json", "Emit machine-readable JSON")
74
+ .action((options) => {
75
+ const result = runFix({ dryRun: !options.write, json: Boolean(options.json) });
76
+ process.stdout.write(`${result.output}\n`);
77
+ });
78
+ cli
79
+ .command("trust <action> [package]", "Manage package-manager trust lists")
80
+ .option("--mode <mode>", "Mode: allow-build | ignore-build")
81
+ .option("--pm <pm>", "Package manager override")
82
+ .option("--write", "Apply changes (default is dry-run)")
83
+ .option("--json", "Emit machine-readable JSON")
84
+ .action((action, packageName, options) => {
85
+ const result = runTrust({
86
+ action,
87
+ packageName,
88
+ mode: options.mode,
89
+ manager: options.pm,
90
+ dryRun: !Boolean(options.write),
91
+ json: Boolean(options.json)
92
+ });
93
+ process.stdout.write(`${result.output}\n`);
94
+ process.exitCode = result.exitCode;
95
+ });
96
+ cli
97
+ .command("override <action> [ruleId]", "Manage policy overrides (add | remove | list)")
98
+ .option("--reason <reason>", "Reason for the override")
99
+ .option("--expires <date>", "Expiration date (YYYY-MM-DD)")
100
+ .action((action, ruleId, options) => {
101
+ if (action === "list") {
102
+ const store = overrideList();
103
+ process.stdout.write(JSON.stringify(store, null, 2) + "\n");
104
+ return;
105
+ }
106
+ if (!ruleId) {
107
+ process.stderr.write("ruleId is required for add/remove\n");
108
+ process.exitCode = 1;
109
+ return;
110
+ }
111
+ if (action === "remove") {
112
+ const store = overrideRemove(ruleId);
113
+ process.stdout.write(JSON.stringify(store, null, 2) + "\n");
114
+ return;
115
+ }
116
+ if (action === "add") {
117
+ if (!options.reason || !options.expires) {
118
+ process.stderr.write("--reason and --expires are required for add\n");
119
+ process.exitCode = 1;
120
+ return;
121
+ }
122
+ const store = overrideAdd({ ruleId, reason: options.reason, expires: options.expires });
123
+ process.stdout.write(JSON.stringify(store, null, 2) + "\n");
124
+ return;
125
+ }
126
+ process.stderr.write(`Unknown action: ${action}\n`);
127
+ process.exitCode = 1;
128
+ });
129
+ return cli;
130
+ }
131
+ const cli = createCli();
132
+ cli.help();
133
+ cli.parse();
@@ -0,0 +1,59 @@
1
+ import { formatScanJson } from "../formatters/json.js";
2
+ import { runScan } from "./scan.js";
3
+ function buildDiagnostics(envelope) {
4
+ const diagnostics = [];
5
+ if (envelope.facts.packageManager === "unknown") {
6
+ diagnostics.push("No lockfile detected. Add one of: package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lockb.");
7
+ }
8
+ if (envelope.facts.framework === "unknown") {
9
+ diagnostics.push("Framework not detected. Running universal JS/TS baseline checks only.");
10
+ }
11
+ return diagnostics;
12
+ }
13
+ function formatCiHuman(envelope) {
14
+ const findingLines = envelope.result.findings.length
15
+ ? envelope.result.findings.map((finding) => `- [${finding.severity}] ${finding.ruleId}: ${finding.message}`).join("\n")
16
+ : "- none";
17
+ const remediationLines = envelope.result.remediation_commands.length
18
+ ? envelope.result.remediation_commands.map((command) => `- ${command}`).join("\n")
19
+ : "- none";
20
+ const diagnosticLines = envelope.result.diagnostics.length
21
+ ? envelope.result.diagnostics.map((message) => `- ${message}`).join("\n")
22
+ : "- none";
23
+ return [
24
+ "depsentinel ci",
25
+ `risk score: ${envelope.result.risk_score}`,
26
+ `gate: ${envelope.result.failed_critical_policy ? "fail" : "pass"}`,
27
+ `package manager: ${envelope.facts.packageManager}`,
28
+ `framework hint: ${envelope.facts.framework}`,
29
+ "findings:",
30
+ findingLines,
31
+ "remediation:",
32
+ remediationLines,
33
+ "diagnostics:",
34
+ diagnosticLines
35
+ ].join("\n");
36
+ }
37
+ export function runCi(options = {}) {
38
+ const scan = runScan({ cwd: options.cwd, json: true });
39
+ const failedCriticalPolicy = scan.envelope.result.findings.some((finding) => finding.severity === "critical");
40
+ const envelope = {
41
+ schemaVersion: "1.0.0",
42
+ command: "ci",
43
+ facts: scan.envelope.facts,
44
+ result: {
45
+ risk_score: scan.envelope.result.risk_score,
46
+ proposed_diff: scan.envelope.result.proposed_diff,
47
+ remediation_commands: scan.envelope.result.remediation_commands,
48
+ findings: scan.envelope.result.findings,
49
+ failed_critical_policy: failedCriticalPolicy,
50
+ diagnostics: []
51
+ }
52
+ };
53
+ envelope.result.diagnostics = buildDiagnostics(envelope);
54
+ return {
55
+ envelope,
56
+ output: options.json ? formatScanJson(envelope) : formatCiHuman(envelope),
57
+ shouldFail: failedCriticalPolicy
58
+ };
59
+ }
@@ -0,0 +1,79 @@
1
+ import { detectProjectFacts } from "../core/detector.js";
2
+ import { collectDiagnoses } from "../core/doctor-checks.js";
3
+ import { computeRiskScore } from "../core/risk-score.js";
4
+ function buildRemediationCommands(facts) {
5
+ const install = facts.packageManager === "pnpm"
6
+ ? "pnpm install --frozen-lockfile"
7
+ : facts.packageManager === "yarn"
8
+ ? "yarn install --immutable"
9
+ : facts.packageManager === "bun"
10
+ ? "bun install --frozen-lockfile"
11
+ : facts.packageManager === "npm"
12
+ ? "npm ci"
13
+ : "corepack enable && pnpm install --frozen-lockfile";
14
+ return [install, "depsentinel init --write", "depsentinel ci --json"];
15
+ }
16
+ function formatDoctorHuman(facts, diagnoses) {
17
+ const byCategory = new Map();
18
+ for (const d of diagnoses) {
19
+ const list = byCategory.get(d.category) ?? [];
20
+ list.push(d);
21
+ byCategory.set(d.category, list);
22
+ }
23
+ const statusIcon = (s) => (s === "pass" ? "PASS" : s === "fail" ? "FAIL" : "SKIP");
24
+ const severityFlag = (sv) => (sv === "critical" ? "!! " : sv === "high" ? "! " : "");
25
+ const lines = ["depsentinel doctor", `package manager: ${facts.packageManager}`, `framework hint: ${facts.framework}`, ""];
26
+ const order = ["config", "dependencies", "ci", "maintainer"];
27
+ for (const cat of order) {
28
+ const items = byCategory.get(cat) ?? [];
29
+ if (items.length === 0)
30
+ continue;
31
+ const fails = items.filter((d) => d.status === "fail").length;
32
+ const passes = items.filter((d) => d.status === "pass").length;
33
+ const skips = items.filter((d) => d.status === "skipped").length;
34
+ lines.push(`--- ${cat.toUpperCase()} (${passes} pass, ${fails} fail, ${skips} skip) ---`);
35
+ for (const d of items) {
36
+ lines.push(` [${statusIcon(d.status)}] ${severityFlag(d.severity)}${d.title}`);
37
+ if (d.status === "fail") {
38
+ lines.push(` Why: ${d.detail}`);
39
+ lines.push(` Fix: ${d.remediation}`);
40
+ }
41
+ if (d.status === "skipped") {
42
+ lines.push(` Note: ${d.detail}`);
43
+ lines.push(` Action: ${d.remediation}`);
44
+ }
45
+ }
46
+ lines.push("");
47
+ }
48
+ return lines.join("\n");
49
+ }
50
+ export function runDoctor(options = {}) {
51
+ const cwd = options.cwd ?? process.cwd();
52
+ const facts = detectProjectFacts(cwd);
53
+ const diagnoses = collectDiagnoses(cwd, facts);
54
+ const passed = diagnoses.filter((d) => d.status === "pass").length;
55
+ const failed = diagnoses.filter((d) => d.status === "fail").length;
56
+ const skipped = diagnoses.filter((d) => d.status === "skipped").length;
57
+ const riskFindings = diagnoses.filter((d) => d.status === "fail").map((d) => ({
58
+ ruleId: d.id,
59
+ severity: d.severity,
60
+ message: d.title
61
+ }));
62
+ const score = computeRiskScore(riskFindings);
63
+ const envelope = {
64
+ schemaVersion: "1.0.0",
65
+ command: "doctor",
66
+ facts,
67
+ result: {
68
+ risk_score: score,
69
+ diagnoses,
70
+ passed,
71
+ failed,
72
+ skipped
73
+ }
74
+ };
75
+ return {
76
+ envelope,
77
+ output: options.json ? JSON.stringify(envelope, null, 2) : formatDoctorHuman(facts, diagnoses)
78
+ };
79
+ }
@@ -0,0 +1,101 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { detectProjectFacts } from "../core/detector.js";
4
+ import { planSafeFile, applySafePlan } from "../core/safe-write.js";
5
+ function readJsonSafe(filePath) {
6
+ if (!existsSync(filePath))
7
+ return {};
8
+ try {
9
+ return JSON.parse(readFileSync(filePath, "utf8"));
10
+ }
11
+ catch {
12
+ return {};
13
+ }
14
+ }
15
+ function collectFixPlans(cwd) {
16
+ const facts = detectProjectFacts(cwd);
17
+ const plans = [];
18
+ // .npmrc
19
+ const npmrcPath = path.join(cwd, ".npmrc");
20
+ const npmrcContent = [
21
+ "# depsentinel npm security baseline",
22
+ "ignore-scripts=true",
23
+ "allow-git=none",
24
+ "min-release-age=3",
25
+ ""
26
+ ].join("\n");
27
+ plans.push(planSafeFile(npmrcPath, npmrcContent));
28
+ // .npmignore
29
+ const npmignorePath = path.join(cwd, ".npmignore");
30
+ const npmignoreContent = "# depsentinel secure npmignore\n.env\n*.log\ncoverage/\nnode_modules/\n";
31
+ plans.push(planSafeFile(npmignorePath, npmignoreContent));
32
+ // .gitignore
33
+ const gitignorePath = path.join(cwd, ".gitignore");
34
+ if (!existsSync(gitignorePath)) {
35
+ plans.push(planSafeFile(gitignorePath, "# depsentinel\ndist/\nnode_modules/\n.env\n*.log\n"));
36
+ }
37
+ // Accumulate package.json changes in memory
38
+ const pkgPath = path.join(cwd, "package.json");
39
+ const pkg = readJsonSafe(pkgPath);
40
+ let pkgDirty = false;
41
+ // files allowlist
42
+ if (pkg.name && !pkg.private && !pkg.files) {
43
+ pkg.files = ["dist"];
44
+ pkgDirty = true;
45
+ }
46
+ // lint:lockfile script
47
+ if (!pkg.scripts)
48
+ pkg.scripts = {};
49
+ if (!pkg.scripts["lint:lockfile"]) {
50
+ pkg.scripts["lint:lockfile"] = "lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https";
51
+ pkgDirty = true;
52
+ }
53
+ // sbom script
54
+ if (!pkg.scripts["sbom"] && !pkg.scripts["generate:sbom"]) {
55
+ pkg.scripts["sbom"] = "npx @cyclonedx/cyclonedx-npm --validate > sbom.json";
56
+ pkgDirty = true;
57
+ }
58
+ if (pkgDirty) {
59
+ plans.push(planSafeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n"));
60
+ }
61
+ // Bunfig
62
+ if (facts.packageManager === "bun") {
63
+ const bunfigPath = path.join(cwd, "bunfig.toml");
64
+ const bunfigContent = "[install]\nminimumReleaseAge = 259200\n";
65
+ plans.push(planSafeFile(bunfigPath, bunfigContent));
66
+ }
67
+ // Yarnrc
68
+ if (facts.packageManager === "yarn") {
69
+ const yarnrcPath = path.join(cwd, ".yarnrc.yml");
70
+ const yarnrcContent = "npmMinimalAgeGate: \"3d\"\n";
71
+ plans.push(planSafeFile(yarnrcPath, yarnrcContent));
72
+ }
73
+ return { plans, facts };
74
+ }
75
+ function buildEntries(plans) {
76
+ return plans.map((p) => ({
77
+ path: path.basename(p.path),
78
+ status: (p.status === "noop" ? "noop" : p.status === "create" ? "created" : "applied"),
79
+ backupPath: p.backupPath ? path.basename(p.backupPath) : undefined,
80
+ reason: p.status === "update" ? "content updated" : p.status === "create" ? "new file" : "already current"
81
+ }));
82
+ }
83
+ export function runFix(options = {}) {
84
+ const cwd = options.cwd ?? process.cwd();
85
+ const dryRun = options.dryRun ?? true;
86
+ const { plans } = collectFixPlans(cwd);
87
+ const applied = applySafePlan(plans, { dryRun });
88
+ const entries = buildEntries(plans);
89
+ const outputLines = [
90
+ "depsentinel fix",
91
+ `Mode: ${dryRun ? "dry-run" : "apply"}`,
92
+ ...entries.map((e) => {
93
+ const backup = e.backupPath ? ` (backup: ${e.backupPath})` : "";
94
+ return `- ${e.path}: ${e.status} — ${e.reason}${backup}`;
95
+ })
96
+ ];
97
+ const output = options.json
98
+ ? JSON.stringify({ command: "fix", dryRun, entries }, null, 2)
99
+ : outputLines.join("\n");
100
+ return { entries, dryRun, output };
101
+ }
@@ -0,0 +1,143 @@
1
+ import path from "node:path";
2
+ import { applySafePlan, planSafeFile } from "../core/safe-write.js";
3
+ import { detectProjectFacts } from "../core/detector.js";
4
+ function buildDepsentinelConfig(preset) {
5
+ return JSON.stringify({
6
+ schemaVersion: "1.0.0",
7
+ preset,
8
+ policyCatalog: "v1",
9
+ failOn: ["critical"],
10
+ overrides: []
11
+ }, null, 2);
12
+ }
13
+ function buildNpmRc() {
14
+ return [
15
+ "# depsentinel npm security baseline",
16
+ "ignore-scripts=true",
17
+ "allow-git=none",
18
+ "min-release-age=3",
19
+ ""
20
+ ].join("\n");
21
+ }
22
+ function buildPnpmWorkspace() {
23
+ return [
24
+ "packages:",
25
+ " - \".\"",
26
+ "",
27
+ "# depsentinel pnpm security baseline",
28
+ "minimumReleaseAge: 43200",
29
+ "trustPolicy: no-downgrade",
30
+ "blockExoticSubdeps: true",
31
+ "strictDepBuilds: true",
32
+ "allowBuilds:",
33
+ " esbuild: true",
34
+ " rolldown: true",
35
+ " unrs-resolver: true",
36
+ ""
37
+ ].join("\n");
38
+ }
39
+ function buildBunfig() {
40
+ return [
41
+ "[install]",
42
+ "minimumReleaseAge = 259200",
43
+ ""
44
+ ].join("\n");
45
+ }
46
+ function buildYarnRc() {
47
+ return [
48
+ "npmMinimalAgeGate: \"3d\"",
49
+ ""
50
+ ].join("\n");
51
+ }
52
+ function buildNpmIgnore() {
53
+ return [
54
+ "# depsentinel secure npmignore",
55
+ ".env",
56
+ "*.log",
57
+ "coverage/",
58
+ "node_modules/",
59
+ ""
60
+ ].join("\n");
61
+ }
62
+ function buildCiWorkflow() {
63
+ return [
64
+ "name: depsentinel-ci",
65
+ "",
66
+ "on:",
67
+ " pull_request:",
68
+ " push:",
69
+ " branches: [main]",
70
+ "",
71
+ "jobs:",
72
+ " policy-gate:",
73
+ " runs-on: ubuntu-latest",
74
+ " steps:",
75
+ " - uses: actions/checkout@v4",
76
+ " - uses: pnpm/action-setup@v4",
77
+ " - uses: actions/setup-node@v4",
78
+ " with:",
79
+ " node-version: 22",
80
+ " cache: pnpm",
81
+ " - name: Install dependencies",
82
+ " run: |",
83
+ " if [ -f pnpm-lock.yaml ]; then corepack enable && pnpm install --frozen-lockfile;",
84
+ " elif [ -f yarn.lock ]; then corepack enable && yarn install --immutable;",
85
+ " elif [ -f bun.lockb ]; then bun install --frozen-lockfile;",
86
+ " elif [ -f package-lock.json ]; then npm ci;",
87
+ " else corepack enable && pnpm install; fi",
88
+ " - name: Build depsentinel CLI",
89
+ " run: pnpm build",
90
+ " - name: Run depsentinel CI gate",
91
+ " run: node dist/cli.js ci --json"
92
+ ].join("\n");
93
+ }
94
+ function toFilePlan(result) {
95
+ return {
96
+ path: path.basename(result.path),
97
+ status: result.status,
98
+ backupPath: result.backupPath ? path.basename(result.backupPath) : undefined
99
+ };
100
+ }
101
+ export function runInit(options = {}) {
102
+ const cwd = options.cwd ?? process.cwd();
103
+ const preset = options.preset ?? "base";
104
+ const dryRun = options.dryRun ?? true;
105
+ const facts = detectProjectFacts(cwd);
106
+ const planned = [
107
+ planSafeFile(path.join(cwd, "depsentinel.json"), `${buildDepsentinelConfig(preset)}\n`),
108
+ planSafeFile(path.join(cwd, ".npmrc"), buildNpmRc()),
109
+ planSafeFile(path.join(cwd, ".npmignore"), buildNpmIgnore()),
110
+ planSafeFile(path.join(cwd, ".github", "workflows", "depsentinel-ci.yml"), `${buildCiWorkflow()}\n`)
111
+ ];
112
+ if (facts.packageManager === "pnpm" || facts.packageManager === "unknown") {
113
+ planned.push(planSafeFile(path.join(cwd, "pnpm-workspace.yaml"), buildPnpmWorkspace()));
114
+ }
115
+ if (facts.packageManager === "bun") {
116
+ planned.push(planSafeFile(path.join(cwd, "bunfig.toml"), buildBunfig()));
117
+ }
118
+ if (facts.packageManager === "yarn") {
119
+ planned.push(planSafeFile(path.join(cwd, ".yarnrc.yml"), buildYarnRc()));
120
+ }
121
+ const applied = applySafePlan(planned, { dryRun });
122
+ const files = applied.map(toFilePlan);
123
+ const envelope = {
124
+ schemaVersion: "1.0.0",
125
+ command: "init",
126
+ result: {
127
+ preset,
128
+ dryRun,
129
+ files
130
+ }
131
+ };
132
+ const output = options.json
133
+ ? JSON.stringify(envelope, null, 2)
134
+ : [
135
+ `Init preset: ${preset}`,
136
+ `Mode: ${dryRun ? "dry-run" : "apply"}`,
137
+ ...files.map((file) => {
138
+ const backup = file.backupPath ? ` (backup: ${file.backupPath})` : "";
139
+ return `- ${file.path}: ${file.status}${backup}`;
140
+ })
141
+ ].join("\n");
142
+ return { envelope, output };
143
+ }