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.
@@ -0,0 +1,83 @@
1
+ import { detectProjectFacts } from "../core/detector.js";
2
+ import { evaluatePolicies } from "../core/evaluate.js";
3
+ import { computeRiskScore } from "../core/risk-score.js";
4
+ import { deriveInstallDecision } from "../core/install-decision.js";
5
+ import { formatInstallHuman } from "../formatters/human.js";
6
+ import { formatInstallJson } from "../formatters/json.js";
7
+ import { policyCatalogV1 } from "../policies/catalog.v1.js";
8
+ const INSTALL_COMMANDS = {
9
+ npm: "npm install",
10
+ pnpm: "pnpm add",
11
+ yarn: "yarn add",
12
+ bun: "bun add"
13
+ };
14
+ export function getManagerInstallCommand(manager, packageName) {
15
+ const template = INSTALL_COMMANDS[manager];
16
+ if (!template) {
17
+ throw new Error("Cannot install with unknown package manager");
18
+ }
19
+ return `${template} ${packageName}`;
20
+ }
21
+ function buildRemediationCommands(packageManager) {
22
+ const install = packageManager === "pnpm"
23
+ ? "pnpm install --frozen-lockfile"
24
+ : packageManager === "yarn"
25
+ ? "yarn install --immutable"
26
+ : packageManager === "bun"
27
+ ? "bun install --frozen-lockfile"
28
+ : "npm ci";
29
+ return [install, "npm audit --audit-level=critical"];
30
+ }
31
+ const PACKAGE_NAME_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
32
+ function isValidPackageName(name) {
33
+ return PACKAGE_NAME_RE.test(name) && name.length <= 214 && !name.startsWith(".") && !name.startsWith("_");
34
+ }
35
+ export function runInstall(options) {
36
+ const cwd = options.cwd ?? process.cwd();
37
+ const packageName = options.packageName;
38
+ const force = options.force ?? false;
39
+ if (!isValidPackageName(packageName)) {
40
+ throw new Error(`Invalid package name: "${packageName}". Must be a valid npm package name (e.g. "lodash" or "@scope/pkg").`);
41
+ }
42
+ const facts = detectProjectFacts(cwd);
43
+ const findings = evaluatePolicies(facts, policyCatalogV1);
44
+ const score = computeRiskScore(findings);
45
+ const decision = deriveInstallDecision(findings, score);
46
+ // Compute manager command for reporting
47
+ const managerCommand = facts.packageManager !== "unknown"
48
+ ? getManagerInstallCommand(facts.packageManager, packageName)
49
+ : undefined;
50
+ const remediationCommands = buildRemediationCommands(facts.packageManager);
51
+ // Adapter report — adapters are async and not run in sync command flow.
52
+ // Real CLI should use async adapter runner; sync path provides empty array.
53
+ const adapterReport = [];
54
+ // Decision gate
55
+ const shouldBlock = decision === "block" && !force;
56
+ const shouldWarn = decision === "warn" && !force;
57
+ const installProceeded = !shouldBlock && !shouldWarn;
58
+ const result = {
59
+ decision,
60
+ forced: force && decision !== "allow",
61
+ score,
62
+ findings,
63
+ remediationCommands,
64
+ adapterReport,
65
+ installed: installProceeded,
66
+ managerCommand,
67
+ exitCode: installProceeded ? 0 : 1
68
+ };
69
+ const envelope = {
70
+ schemaVersion: "1.0.0",
71
+ command: "install",
72
+ facts,
73
+ result
74
+ };
75
+ const output = options.json
76
+ ? formatInstallJson(envelope)
77
+ : formatInstallHuman(envelope);
78
+ return {
79
+ envelope,
80
+ output,
81
+ exitCode: result.exitCode
82
+ };
83
+ }
@@ -0,0 +1,39 @@
1
+ import { detectProjectFacts } from "../core/detector.js";
2
+ import { evaluatePolicies } from "../core/evaluate.js";
3
+ import { computeRiskScore } from "../core/risk-score.js";
4
+ import { formatScanHuman } from "../formatters/human.js";
5
+ import { formatScanJson } from "../formatters/json.js";
6
+ import { policyCatalogV1 } from "../policies/catalog.v1.js";
7
+ function buildRemediationCommands(packageManager) {
8
+ const install = packageManager === "pnpm"
9
+ ? "pnpm install --frozen-lockfile"
10
+ : packageManager === "yarn"
11
+ ? "yarn install --immutable"
12
+ : packageManager === "bun"
13
+ ? "bun install --frozen-lockfile"
14
+ : packageManager === "npm"
15
+ ? "npm ci"
16
+ : "corepack enable && pnpm install --frozen-lockfile";
17
+ return [install, "npm audit --audit-level=critical"];
18
+ }
19
+ export function runScan(options = {}) {
20
+ const cwd = options.cwd ?? process.cwd();
21
+ const facts = detectProjectFacts(cwd);
22
+ const findings = evaluatePolicies(facts, policyCatalogV1);
23
+ const score = computeRiskScore(findings);
24
+ const envelope = {
25
+ schemaVersion: "1.0.0",
26
+ command: "scan",
27
+ facts,
28
+ result: {
29
+ risk_score: score,
30
+ findings,
31
+ proposed_diff: findings.map((finding) => `policy:${finding.ruleId}`),
32
+ remediation_commands: buildRemediationCommands(facts.packageManager)
33
+ }
34
+ };
35
+ return {
36
+ envelope,
37
+ output: options.json ? formatScanJson(envelope) : formatScanHuman(envelope)
38
+ };
39
+ }
@@ -0,0 +1,353 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { detectProjectFacts } from "../core/detector.js";
4
+ import { applySafePlan, planSafeFile } from "../core/safe-write.js";
5
+ const MANAGERS = ["pnpm", "npm", "yarn", "bun"];
6
+ function quoteYamlString(value) {
7
+ return `"${value.replace(/\\/g, "\\\\").replace(/\"/g, '\\\"')}"`;
8
+ }
9
+ function emptyTrustStore() {
10
+ return {
11
+ allowBuild: { pnpm: [], npm: [], yarn: [], bun: [] },
12
+ ignoreBuild: { pnpm: [], npm: [], yarn: [], bun: [] }
13
+ };
14
+ }
15
+ function readJsonSafe(filePath, fallback) {
16
+ if (!existsSync(filePath))
17
+ return fallback;
18
+ try {
19
+ return JSON.parse(readFileSync(filePath, "utf8"));
20
+ }
21
+ catch {
22
+ return fallback;
23
+ }
24
+ }
25
+ function uniqueSorted(values) {
26
+ return [...new Set(values)].sort();
27
+ }
28
+ function readDepsentinelConfig(cwd) {
29
+ const configPath = path.join(cwd, "depsentinel.json");
30
+ const fallback = {
31
+ schemaVersion: "1.0.0",
32
+ preset: "base",
33
+ policyCatalog: "v1",
34
+ failOn: ["critical"],
35
+ overrides: [],
36
+ trust: emptyTrustStore()
37
+ };
38
+ const raw = readJsonSafe(configPath, fallback);
39
+ const trust = raw.trust ?? emptyTrustStore();
40
+ return {
41
+ ...fallback,
42
+ ...raw,
43
+ trust: {
44
+ allowBuild: {
45
+ pnpm: uniqueSorted(trust.allowBuild?.pnpm ?? []),
46
+ npm: uniqueSorted(trust.allowBuild?.npm ?? []),
47
+ yarn: uniqueSorted(trust.allowBuild?.yarn ?? []),
48
+ bun: uniqueSorted(trust.allowBuild?.bun ?? [])
49
+ },
50
+ ignoreBuild: {
51
+ pnpm: uniqueSorted(trust.ignoreBuild?.pnpm ?? []),
52
+ npm: uniqueSorted(trust.ignoreBuild?.npm ?? []),
53
+ yarn: uniqueSorted(trust.ignoreBuild?.yarn ?? []),
54
+ bun: uniqueSorted(trust.ignoreBuild?.bun ?? [])
55
+ }
56
+ }
57
+ };
58
+ }
59
+ function parsePnpmWorkspace(content) {
60
+ const lines = content.split(/\r?\n/);
61
+ const allowBuilds = new Set();
62
+ const ignoreBuiltDependencies = new Set();
63
+ let inAllowBuilds = false;
64
+ let inIgnoreList = false;
65
+ for (const line of lines) {
66
+ const trimmed = line.trim();
67
+ if (/^allowBuilds\s*:\s*$/.test(trimmed)) {
68
+ inAllowBuilds = true;
69
+ inIgnoreList = false;
70
+ continue;
71
+ }
72
+ if (/^ignoreBuiltDependencies\s*:\s*$/.test(trimmed)) {
73
+ inIgnoreList = true;
74
+ inAllowBuilds = false;
75
+ continue;
76
+ }
77
+ if (/^\S[^:]*\s*:/.test(line)) {
78
+ inAllowBuilds = false;
79
+ inIgnoreList = false;
80
+ }
81
+ if (inAllowBuilds) {
82
+ const match = trimmed.match(/^([@a-zA-Z0-9._\/-]+)\s*:\s*(true|false)\s*$/);
83
+ if (match && match[2] === "true")
84
+ allowBuilds.add(match[1]);
85
+ }
86
+ if (inIgnoreList) {
87
+ const match = trimmed.match(/^-\s+"?([@a-zA-Z0-9._\/-]+)"?\s*$/);
88
+ if (match)
89
+ ignoreBuiltDependencies.add(match[1]);
90
+ }
91
+ }
92
+ return { lines, allowBuilds, ignoreBuiltDependencies };
93
+ }
94
+ function renderPnpmWorkspace(state) {
95
+ const out = [...state.lines];
96
+ const hasAllow = out.some((line) => /^\s*allowBuilds\s*:\s*$/.test(line));
97
+ if (!hasAllow)
98
+ out.push("allowBuilds:");
99
+ const allowIndex = out.findIndex((line) => /^\s*allowBuilds\s*:\s*$/.test(line));
100
+ let allowEnd = allowIndex + 1;
101
+ while (allowEnd < out.length) {
102
+ const trimmed = out[allowEnd].trim();
103
+ if (!trimmed || trimmed.startsWith("#")) {
104
+ allowEnd += 1;
105
+ continue;
106
+ }
107
+ if (/^[a-zA-Z]/.test(trimmed) && !trimmed.startsWith("- "))
108
+ break;
109
+ allowEnd += 1;
110
+ }
111
+ out.splice(allowIndex + 1, allowEnd - (allowIndex + 1));
112
+ out.splice(allowIndex + 1, 0, ...[...state.allowBuilds].sort().map((name) => ` ${name}: true`));
113
+ const hasIgnore = out.some((line) => /^\s*ignoreBuiltDependencies\s*:\s*$/.test(line));
114
+ if (!hasIgnore) {
115
+ if (out.length > 0 && out[out.length - 1].trim() !== "")
116
+ out.push("");
117
+ out.push("ignoreBuiltDependencies:");
118
+ }
119
+ const ignoreIndex = out.findIndex((line) => /^\s*ignoreBuiltDependencies\s*:\s*$/.test(line));
120
+ let ignoreEnd = ignoreIndex + 1;
121
+ while (ignoreEnd < out.length) {
122
+ const trimmed = out[ignoreEnd].trim();
123
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("- ")) {
124
+ ignoreEnd += 1;
125
+ continue;
126
+ }
127
+ if (/^[a-zA-Z]/.test(trimmed))
128
+ break;
129
+ ignoreEnd += 1;
130
+ }
131
+ out.splice(ignoreIndex + 1, ignoreEnd - (ignoreIndex + 1));
132
+ out.splice(ignoreIndex + 1, 0, ...[...state.ignoreBuiltDependencies].sort().map((name) => ` - ${quoteYamlString(name)}`));
133
+ return `${out.join("\n").replace(/\n*$/, "\n")}`;
134
+ }
135
+ function ensurePnpmWorkspace(cwd) {
136
+ const filePath = path.join(cwd, "pnpm-workspace.yaml");
137
+ if (existsSync(filePath))
138
+ return readFileSync(filePath, "utf8");
139
+ return ["packages:", " - \".\"", "", "allowBuilds:", "", "ignoreBuiltDependencies:", ""].join("\n");
140
+ }
141
+ function updateDepsentinelTrust(config, manager, mode, action, pkg) {
142
+ const next = { ...config, trust: config.trust ?? emptyTrustStore() };
143
+ const list = mode === "allow-build" ? next.trust.allowBuild[manager] : next.trust.ignoreBuild[manager];
144
+ const other = mode === "allow-build" ? next.trust.ignoreBuild[manager] : next.trust.allowBuild[manager];
145
+ if (action === "add") {
146
+ if (!list.includes(pkg))
147
+ list.push(pkg);
148
+ const otherIdx = other.indexOf(pkg);
149
+ if (otherIdx >= 0)
150
+ other.splice(otherIdx, 1);
151
+ }
152
+ else if (action === "remove") {
153
+ const idx = list.indexOf(pkg);
154
+ if (idx >= 0)
155
+ list.splice(idx, 1);
156
+ }
157
+ next.trust.allowBuild[manager] = uniqueSorted(next.trust.allowBuild[manager]);
158
+ next.trust.ignoreBuild[manager] = uniqueSorted(next.trust.ignoreBuild[manager]);
159
+ return next;
160
+ }
161
+ function listForManager(cwd, manager) {
162
+ if (manager === "pnpm") {
163
+ const parsed = parsePnpmWorkspace(ensurePnpmWorkspace(cwd));
164
+ return {
165
+ manager,
166
+ allowBuild: [...parsed.allowBuilds].sort(),
167
+ ignoreBuild: [...parsed.ignoreBuiltDependencies].sort(),
168
+ source: { allowBuild: "native", ignoreBuild: "native" },
169
+ entries: {
170
+ allowBuild: [...parsed.allowBuilds].sort().map((packageName) => ({ packageName, source: "native" })),
171
+ ignoreBuild: [...parsed.ignoreBuiltDependencies].sort().map((packageName) => ({ packageName, source: "native" }))
172
+ }
173
+ };
174
+ }
175
+ if (manager === "bun") {
176
+ const pkgPath = path.join(cwd, "package.json");
177
+ const pkg = readJsonSafe(pkgPath, {});
178
+ const config = readDepsentinelConfig(cwd);
179
+ return {
180
+ manager,
181
+ allowBuild: uniqueSorted(pkg.trustedDependencies ?? []),
182
+ ignoreBuild: config.trust?.ignoreBuild.bun ?? [],
183
+ source: { allowBuild: "native", ignoreBuild: "depsentinel" },
184
+ entries: {
185
+ allowBuild: uniqueSorted(pkg.trustedDependencies ?? []).map((packageName) => ({ packageName, source: "native" })),
186
+ ignoreBuild: (config.trust?.ignoreBuild.bun ?? []).map((packageName) => ({
187
+ packageName,
188
+ source: "depsentinel"
189
+ }))
190
+ }
191
+ };
192
+ }
193
+ if (manager === "yarn") {
194
+ const pkgPath = path.join(cwd, "package.json");
195
+ const pkg = readJsonSafe(pkgPath, {});
196
+ const meta = pkg.dependenciesMeta ?? {};
197
+ const allowBuild = Object.entries(meta)
198
+ .filter(([, v]) => v.built === true)
199
+ .map(([name]) => name)
200
+ .sort();
201
+ const ignoreBuild = Object.entries(meta)
202
+ .filter(([, v]) => v.built === false)
203
+ .map(([name]) => name)
204
+ .sort();
205
+ return {
206
+ manager,
207
+ allowBuild,
208
+ ignoreBuild,
209
+ source: { allowBuild: "native", ignoreBuild: "native" },
210
+ entries: {
211
+ allowBuild: allowBuild.map((packageName) => ({ packageName, source: "native" })),
212
+ ignoreBuild: ignoreBuild.map((packageName) => ({ packageName, source: "native" }))
213
+ }
214
+ };
215
+ }
216
+ const config = readDepsentinelConfig(cwd);
217
+ return {
218
+ manager,
219
+ allowBuild: config.trust?.allowBuild.npm ?? [],
220
+ ignoreBuild: config.trust?.ignoreBuild.npm ?? [],
221
+ source: { allowBuild: "depsentinel", ignoreBuild: "depsentinel" },
222
+ entries: {
223
+ allowBuild: (config.trust?.allowBuild.npm ?? []).map((packageName) => ({ packageName, source: "depsentinel" })),
224
+ ignoreBuild: (config.trust?.ignoreBuild.npm ?? []).map((packageName) => ({ packageName, source: "depsentinel" }))
225
+ }
226
+ };
227
+ }
228
+ function toManager(value) {
229
+ if (value === "unknown")
230
+ return null;
231
+ return value;
232
+ }
233
+ export function runTrust(options) {
234
+ const cwd = options.cwd ?? process.cwd();
235
+ const dryRun = options.dryRun ?? true;
236
+ const facts = detectProjectFacts(cwd);
237
+ const resolvedManager = toManager(options.manager ?? facts.packageManager);
238
+ if (!resolvedManager) {
239
+ const output = options.json
240
+ ? JSON.stringify({ command: "trust", error: "could not detect package manager; pass --pm" }, null, 2)
241
+ : "Could not detect package manager. Pass --pm npm|pnpm|yarn|bun.";
242
+ return { output, exitCode: 1 };
243
+ }
244
+ if (options.action === "list") {
245
+ const list = listForManager(cwd, resolvedManager);
246
+ const output = options.json
247
+ ? JSON.stringify({ command: "trust", action: "list", ...list }, null, 2)
248
+ : [
249
+ `depsentinel trust list (${resolvedManager})`,
250
+ `allow-build [${list.source.allowBuild}]: ${list.allowBuild.join(", ") || "none"}`,
251
+ `ignore-build [${list.source.ignoreBuild}]: ${list.ignoreBuild.join(", ") || "none"}`,
252
+ "allow-build entries:",
253
+ ...(list.entries.allowBuild.length > 0
254
+ ? list.entries.allowBuild.map((entry) => `- ${entry.packageName} (${entry.source})`)
255
+ : ["- none"]),
256
+ "ignore-build entries:",
257
+ ...(list.entries.ignoreBuild.length > 0
258
+ ? list.entries.ignoreBuild.map((entry) => `- ${entry.packageName} (${entry.source})`)
259
+ : ["- none"])
260
+ ].join("\n");
261
+ return { output, exitCode: 0 };
262
+ }
263
+ if (!options.packageName || !options.mode) {
264
+ const output = options.json
265
+ ? JSON.stringify({ command: "trust", error: "packageName and --mode are required for add/remove" }, null, 2)
266
+ : "packageName and --mode are required for add/remove";
267
+ return { output, exitCode: 1 };
268
+ }
269
+ const plans = [];
270
+ if (resolvedManager === "pnpm") {
271
+ const state = parsePnpmWorkspace(ensurePnpmWorkspace(cwd));
272
+ if (options.mode === "allow-build") {
273
+ if (options.action === "add") {
274
+ state.allowBuilds.add(options.packageName);
275
+ state.ignoreBuiltDependencies.delete(options.packageName);
276
+ }
277
+ else {
278
+ state.allowBuilds.delete(options.packageName);
279
+ }
280
+ }
281
+ else if (options.action === "add") {
282
+ state.ignoreBuiltDependencies.add(options.packageName);
283
+ state.allowBuilds.delete(options.packageName);
284
+ }
285
+ else {
286
+ state.ignoreBuiltDependencies.delete(options.packageName);
287
+ }
288
+ plans.push(planSafeFile(path.join(cwd, "pnpm-workspace.yaml"), renderPnpmWorkspace(state)));
289
+ }
290
+ if (resolvedManager === "bun") {
291
+ const pkgPath = path.join(cwd, "package.json");
292
+ const pkg = readJsonSafe(pkgPath, {});
293
+ const trusted = new Set(pkg.trustedDependencies ?? []);
294
+ const config = readDepsentinelConfig(cwd);
295
+ const nextConfig = updateDepsentinelTrust(config, "bun", options.mode, options.action, options.packageName);
296
+ if (options.mode === "allow-build") {
297
+ if (options.action === "add")
298
+ trusted.add(options.packageName);
299
+ if (options.action === "remove")
300
+ trusted.delete(options.packageName);
301
+ pkg.trustedDependencies = [...trusted].sort();
302
+ plans.push(planSafeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n"));
303
+ }
304
+ else {
305
+ plans.push(planSafeFile(path.join(cwd, "depsentinel.json"), JSON.stringify(nextConfig, null, 2) + "\n"));
306
+ }
307
+ }
308
+ if (resolvedManager === "yarn") {
309
+ const pkgPath = path.join(cwd, "package.json");
310
+ const pkg = readJsonSafe(pkgPath, {});
311
+ const meta = { ...(pkg.dependenciesMeta ?? {}) };
312
+ if (options.action === "add") {
313
+ meta[options.packageName] = { ...(meta[options.packageName] ?? {}), built: options.mode === "allow-build" };
314
+ }
315
+ else if (meta[options.packageName]) {
316
+ const copy = { ...meta[options.packageName] };
317
+ delete copy.built;
318
+ if (Object.keys(copy).length === 0) {
319
+ delete meta[options.packageName];
320
+ }
321
+ else {
322
+ meta[options.packageName] = copy;
323
+ }
324
+ }
325
+ pkg.dependenciesMeta = meta;
326
+ plans.push(planSafeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n"));
327
+ }
328
+ if (resolvedManager === "npm") {
329
+ const config = readDepsentinelConfig(cwd);
330
+ const nextConfig = updateDepsentinelTrust(config, "npm", options.mode, options.action, options.packageName);
331
+ plans.push(planSafeFile(path.join(cwd, "depsentinel.json"), JSON.stringify(nextConfig, null, 2) + "\n"));
332
+ }
333
+ const applied = applySafePlan(plans, { dryRun });
334
+ const list = listForManager(cwd, resolvedManager);
335
+ const output = options.json
336
+ ? JSON.stringify({
337
+ command: "trust",
338
+ action: options.action,
339
+ manager: resolvedManager,
340
+ mode: options.mode,
341
+ packageName: options.packageName,
342
+ dryRun,
343
+ files: applied.map((p) => ({ path: path.basename(p.path), status: p.status })),
344
+ trust: list
345
+ }, null, 2)
346
+ : [
347
+ `depsentinel trust ${options.action} (${resolvedManager}, ${options.mode})`,
348
+ `package: ${options.packageName}`,
349
+ `mode: ${dryRun ? "dry-run" : "apply"}`,
350
+ ...applied.map((p) => `- ${path.basename(p.path)}: ${p.status}`)
351
+ ].join("\n");
352
+ return { output, exitCode: 0 };
353
+ }
@@ -0,0 +1,54 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ const LOCKFILES = [
4
+ { name: "pnpm-lock.yaml", manager: "pnpm" },
5
+ { name: "package-lock.json", manager: "npm" },
6
+ { name: "yarn.lock", manager: "yarn" },
7
+ { name: "bun.lockb", manager: "bun" }
8
+ ];
9
+ export function detectProjectFacts(rootDir) {
10
+ const packageJsonPath = path.join(rootDir, "package.json");
11
+ let pkg = {};
12
+ try {
13
+ pkg = JSON.parse(existsSync(packageJsonPath) ? readFileSync(packageJsonPath, "utf8") : "{}");
14
+ }
15
+ catch {
16
+ pkg = {};
17
+ }
18
+ const lock = LOCKFILES.find((entry) => existsSync(path.join(rootDir, entry.name))) ?? null;
19
+ const dependencies = {
20
+ ...(pkg.dependencies ?? {}),
21
+ ...(pkg.devDependencies ?? {})
22
+ };
23
+ const framework = detectFrameworkHint(dependencies);
24
+ const isWorkspace = Boolean(pkg.workspaces) || existsSync(path.join(rootDir, "pnpm-workspace.yaml"));
25
+ return {
26
+ rootDir,
27
+ packageManager: lock?.manager ?? "unknown",
28
+ lockfile: lock?.name ?? null,
29
+ isWorkspace,
30
+ framework,
31
+ dependencies
32
+ };
33
+ }
34
+ function detectFrameworkHint(dependencies) {
35
+ if (dependencies.expo) {
36
+ return "expo";
37
+ }
38
+ if (dependencies.next) {
39
+ return "nextjs";
40
+ }
41
+ if (dependencies.react) {
42
+ return "react";
43
+ }
44
+ if (dependencies.vue) {
45
+ return "vue";
46
+ }
47
+ if (dependencies["@angular/core"]) {
48
+ return "angular";
49
+ }
50
+ if (dependencies.svelte) {
51
+ return "svelte";
52
+ }
53
+ return Object.keys(dependencies).length > 0 ? "node" : "unknown";
54
+ }