depsentinel 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -53,13 +53,13 @@ export function createCli() {
53
53
  const context = options.json
54
54
  ? {
55
55
  publishesToNpm: true,
56
- publishFromCi: true,
57
- usesOidcTrustedPublisher: false
56
+ usesOidcTrustedPublisher: false,
57
+ usesDevContainer: false
58
58
  }
59
59
  : {
60
60
  publishesToNpm: await askYesNo("Does this project publish packages to npm?", false),
61
- publishFromCi: await askYesNo("Does this project publish from CI?", true),
62
- usesOidcTrustedPublisher: await askYesNo("Does npm publish use OIDC Trusted Publisher?", false)
61
+ usesOidcTrustedPublisher: await askYesNo("Does npm publish use OIDC Trusted Publisher?", false),
62
+ usesDevContainer: await askYesNo("Does this project use Dev Containers for local development?", false)
63
63
  };
64
64
  const result = runInit({
65
65
  preset: options.preset,
@@ -56,7 +56,7 @@ function collectFixPlans(cwd) {
56
56
  pkgDirty = true;
57
57
  }
58
58
  if (pkgDirty) {
59
- plans.push(planSafeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n"));
59
+ plans.push(planSafeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", { backupOnUpdate: false }));
60
60
  }
61
61
  // Bunfig
62
62
  if (facts.packageManager === "bun") {
@@ -1,3 +1,4 @@
1
+ import { existsSync, readFileSync } from "node:fs";
1
2
  import path from "node:path";
2
3
  import { applySafePlan, planSafeFile } from "../core/safe-write.js";
3
4
  import { detectProjectFacts } from "../core/detector.js";
@@ -20,6 +21,26 @@ function buildNpmRc() {
20
21
  ""
21
22
  ].join("\n");
22
23
  }
24
+ function mergeNpmRc(existing) {
25
+ const lines = existing.split(/\r?\n/);
26
+ const keys = new Map();
27
+ for (let i = 0; i < lines.length; i += 1) {
28
+ const m = lines[i]?.match(/^\s*([a-zA-Z0-9-]+)\s*=\s*(.*)\s*$/);
29
+ if (m)
30
+ keys.set(m[1], i);
31
+ }
32
+ const required = [
33
+ ["ignore-scripts", "true"],
34
+ ["allow-git", "none"],
35
+ ["min-release-age", "3"]
36
+ ];
37
+ for (const [key, value] of required) {
38
+ if (!keys.has(key)) {
39
+ lines.push(`${key}=${value}`);
40
+ }
41
+ }
42
+ return `${lines.join("\n").replace(/\n*$/, "\n")}`;
43
+ }
23
44
  function buildPnpmWorkspace() {
24
45
  return [
25
46
  "packages:",
@@ -37,6 +58,36 @@ function buildPnpmWorkspace() {
37
58
  ""
38
59
  ].join("\n");
39
60
  }
61
+ function mergePnpmWorkspace(existing) {
62
+ const lines = existing.split(/\r?\n/);
63
+ const has = (k) => lines.some((line) => line.trim().startsWith(`${k}:`));
64
+ if (!has("minimumReleaseAge"))
65
+ lines.push("minimumReleaseAge: 43200");
66
+ if (!has("trustPolicy"))
67
+ lines.push("trustPolicy: no-downgrade");
68
+ if (!has("blockExoticSubdeps"))
69
+ lines.push("blockExoticSubdeps: true");
70
+ if (!has("strictDepBuilds"))
71
+ lines.push("strictDepBuilds: true");
72
+ const allowIdx = lines.findIndex((line) => line.trim() === "allowBuilds:");
73
+ if (allowIdx === -1) {
74
+ lines.push("allowBuilds:");
75
+ lines.push(" esbuild: true");
76
+ lines.push(" rolldown: true");
77
+ lines.push(" unrs-resolver: true");
78
+ }
79
+ else {
80
+ const addAllow = (pkg) => {
81
+ const exists = lines.some((line) => line.trim().startsWith(`${pkg}:`));
82
+ if (!exists)
83
+ lines.splice(allowIdx + 1, 0, ` ${pkg}: true`);
84
+ };
85
+ addAllow("esbuild");
86
+ addAllow("rolldown");
87
+ addAllow("unrs-resolver");
88
+ }
89
+ return `${lines.join("\n").replace(/\n*$/, "\n")}`;
90
+ }
40
91
  function buildBunfig() {
41
92
  return [
42
93
  "[install]",
@@ -106,17 +157,19 @@ export function runInit(options = {}) {
106
157
  const facts = detectProjectFacts(cwd);
107
158
  const context = options.context ?? {
108
159
  publishesToNpm: true,
109
- publishFromCi: true,
110
- usesOidcTrustedPublisher: false
160
+ usesOidcTrustedPublisher: false,
161
+ usesDevContainer: false
111
162
  };
112
163
  const planned = [
113
164
  planSafeFile(path.join(cwd, "depsentinel.json"), `${buildDepsentinelConfig(preset, context)}\n`),
114
- planSafeFile(path.join(cwd, ".npmrc"), buildNpmRc()),
165
+ planSafeFile(path.join(cwd, ".npmrc"), existsSync(path.join(cwd, ".npmrc")) ? mergeNpmRc(readFileSync(path.join(cwd, ".npmrc"), "utf8")) : buildNpmRc(), { backupOnUpdate: false }),
115
166
  planSafeFile(path.join(cwd, ".npmignore"), buildNpmIgnore()),
116
167
  planSafeFile(path.join(cwd, ".github", "workflows", "depsentinel-ci.yml"), `${buildCiWorkflow()}\n`)
117
168
  ];
118
169
  if (facts.packageManager === "pnpm" || facts.packageManager === "unknown") {
119
- planned.push(planSafeFile(path.join(cwd, "pnpm-workspace.yaml"), buildPnpmWorkspace()));
170
+ planned.push(planSafeFile(path.join(cwd, "pnpm-workspace.yaml"), existsSync(path.join(cwd, "pnpm-workspace.yaml"))
171
+ ? mergePnpmWorkspace(readFileSync(path.join(cwd, "pnpm-workspace.yaml"), "utf8"))
172
+ : buildPnpmWorkspace(), { backupOnUpdate: false }));
120
173
  }
121
174
  if (facts.packageManager === "bun") {
122
175
  planned.push(planSafeFile(path.join(cwd, "bunfig.toml"), buildBunfig()));
@@ -44,7 +44,7 @@ export function collectDiagnoses(rootDir, facts) {
44
44
  checkEnvPlaintext(rootDir),
45
45
  checkNpxHardening(),
46
46
  checkNpm2fa(context),
47
- checkDevContainer(rootDir),
47
+ checkDevContainer(rootDir, context),
48
48
  checkNodeModulesGitignored(rootDir)
49
49
  ];
50
50
  }
@@ -77,12 +77,12 @@ function checkMixedLockfiles(rootDir, facts) {
77
77
  function readProjectContext(rootDir) {
78
78
  const configPath = path.join(rootDir, "depsentinel.json");
79
79
  const parsed = readJsonSafe(configPath, {
80
- context: { publishesToNpm: true, publishFromCi: true, usesOidcTrustedPublisher: false }
80
+ context: { publishesToNpm: true, usesOidcTrustedPublisher: false, usesDevContainer: false }
81
81
  });
82
82
  return {
83
83
  publishesToNpm: parsed.context?.publishesToNpm ?? true,
84
- publishFromCi: parsed.context?.publishFromCi ?? true,
85
- usesOidcTrustedPublisher: parsed.context?.usesOidcTrustedPublisher ?? false
84
+ usesOidcTrustedPublisher: parsed.context?.usesOidcTrustedPublisher ?? false,
85
+ usesDevContainer: parsed.context?.usesDevContainer ?? false
86
86
  };
87
87
  }
88
88
  function checkNpmRc(rootDir) {
@@ -173,8 +173,8 @@ function checkLockfileCommitted(rootDir) {
173
173
  return skip("ci.lockfile-committed.manual", "ci", "Verify lockfile committed", "Use `git ls-files package-lock.json` to confirm your lockfile is committed.", "Run `git add package-lock.json && git commit -m \"chore: add lockfile\"`.");
174
174
  }
175
175
  function checkCiProvenance(rootDir, context) {
176
- if (!context.publishesToNpm || !context.publishFromCi) {
177
- return skip("ci.provenance.not-applicable", "ci", "Publish provenance not required", "Project context says npm publishing from CI is disabled.", "Set `context.publishesToNpm=true` and `context.publishFromCi=true` if you start publishing from CI.");
176
+ if (!context.publishesToNpm) {
177
+ return skip("ci.provenance.not-applicable", "ci", "Publish provenance not required", "Project context says npm publishing is disabled.", "Set `context.publishesToNpm=true` in depsentinel.json if this changes.");
178
178
  }
179
179
  const workflowsDir = path.join(rootDir, ".github", "workflows");
180
180
  if (!existsSync(workflowsDir))
@@ -238,7 +238,10 @@ function checkNpm2fa(context) {
238
238
  }
239
239
  return skip("maintainer.2fa.manual", "maintainer", "Verify npm account 2FA", "Accounts without 2FA are vulnerable to credential theft and package takeover.", "Run `npm profile enable-2fa auth-and-writes` to enable 2FA for your npm account.");
240
240
  }
241
- function checkDevContainer(rootDir) {
241
+ function checkDevContainer(rootDir, context) {
242
+ if (!context.usesDevContainer) {
243
+ return skip("maintainer.devcontainer.not-required", "maintainer", "Dev Container not required by project context", "Project context says Dev Containers are not part of local development workflow.", "Set `context.usesDevContainer=true` in depsentinel.json if you adopt Dev Containers.");
244
+ }
242
245
  const devContainerPath = path.join(rootDir, ".devcontainer", "devcontainer.json");
243
246
  if (existsSync(devContainerPath))
244
247
  return pass("maintainer.devcontainer.present", "maintainer", "Dev Container configured");
@@ -9,7 +9,7 @@ function nextBackupPath(filePath) {
9
9
  }
10
10
  return candidate;
11
11
  }
12
- export function planSafeFile(filePath, content) {
12
+ export function planSafeFile(filePath, content, options = {}) {
13
13
  if (!existsSync(filePath)) {
14
14
  return { path: filePath, content, status: "create" };
15
15
  }
@@ -21,7 +21,7 @@ export function planSafeFile(filePath, content) {
21
21
  path: filePath,
22
22
  content,
23
23
  status: "update",
24
- backupPath: nextBackupPath(filePath)
24
+ backupPath: options.backupOnUpdate === false ? undefined : nextBackupPath(filePath)
25
25
  };
26
26
  }
27
27
  export function applySafePlan(plan, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depsentinel",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "JS/TS supply-chain hardening CLI — scan, secure, and enforce dependency policies",
5
5
  "license": "MIT",
6
6
  "author": "depsentinel contributors",