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 +4 -4
- package/dist/commands/fix.js +1 -1
- package/dist/commands/init.js +57 -4
- package/dist/core/doctor-checks.js +10 -7
- package/dist/core/safe-write.js +2 -2
- package/package.json +1 -1
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
|
-
|
|
57
|
-
|
|
56
|
+
usesOidcTrustedPublisher: false,
|
|
57
|
+
usesDevContainer: false
|
|
58
58
|
}
|
|
59
59
|
: {
|
|
60
60
|
publishesToNpm: await askYesNo("Does this project publish packages to npm?", false),
|
|
61
|
-
|
|
62
|
-
|
|
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,
|
package/dist/commands/fix.js
CHANGED
|
@@ -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") {
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
110
|
-
|
|
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"),
|
|
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,
|
|
80
|
+
context: { publishesToNpm: true, usesOidcTrustedPublisher: false, usesDevContainer: false }
|
|
81
81
|
});
|
|
82
82
|
return {
|
|
83
83
|
publishesToNpm: parsed.context?.publishesToNpm ?? true,
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
177
|
-
return skip("ci.provenance.not-applicable", "ci", "Publish provenance not required", "Project context says npm publishing
|
|
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");
|
package/dist/core/safe-write.js
CHANGED
|
@@ -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 = {}) {
|