tack-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/App.d.ts +5 -0
- package/dist/App.js +17 -0
- package/dist/detectors/admin.d.ts +2 -0
- package/dist/detectors/admin.js +33 -0
- package/dist/detectors/auth.d.ts +2 -0
- package/dist/detectors/auth.js +86 -0
- package/dist/detectors/database.d.ts +2 -0
- package/dist/detectors/database.js +96 -0
- package/dist/detectors/duplicates.d.ts +2 -0
- package/dist/detectors/duplicates.js +23 -0
- package/dist/detectors/exports.d.ts +2 -0
- package/dist/detectors/exports.js +30 -0
- package/dist/detectors/framework.d.ts +2 -0
- package/dist/detectors/framework.js +71 -0
- package/dist/detectors/index.d.ts +12 -0
- package/dist/detectors/index.js +128 -0
- package/dist/detectors/jobs.d.ts +2 -0
- package/dist/detectors/jobs.js +62 -0
- package/dist/detectors/multiuser.d.ts +2 -0
- package/dist/detectors/multiuser.js +55 -0
- package/dist/detectors/payments.d.ts +2 -0
- package/dist/detectors/payments.js +49 -0
- package/dist/detectors/rules/auth.yaml +24 -0
- package/dist/detectors/rules/database.yaml +27 -0
- package/dist/detectors/rules/exports.yaml +28 -0
- package/dist/detectors/rules/framework.yaml +26 -0
- package/dist/detectors/rules/jobs.yaml +23 -0
- package/dist/detectors/rules/payments.yaml +22 -0
- package/dist/detectors/types.d.ts +2 -0
- package/dist/detectors/types.js +1 -0
- package/dist/detectors/yamlRunner.d.ts +31 -0
- package/dist/detectors/yamlRunner.js +128 -0
- package/dist/engine/cleanup.d.ts +12 -0
- package/dist/engine/cleanup.js +101 -0
- package/dist/engine/compaction.d.ts +5 -0
- package/dist/engine/compaction.js +44 -0
- package/dist/engine/compareSpec.d.ts +2 -0
- package/dist/engine/compareSpec.js +74 -0
- package/dist/engine/computeDrift.d.ts +6 -0
- package/dist/engine/computeDrift.js +133 -0
- package/dist/engine/contextPack.d.ts +4 -0
- package/dist/engine/contextPack.js +169 -0
- package/dist/engine/decisions.d.ts +4 -0
- package/dist/engine/decisions.js +21 -0
- package/dist/engine/diff.d.ts +46 -0
- package/dist/engine/diff.js +210 -0
- package/dist/engine/handoff.d.ts +7 -0
- package/dist/engine/handoff.js +469 -0
- package/dist/engine/status.d.ts +10 -0
- package/dist/engine/status.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +299 -0
- package/dist/lib/cli.d.ts +4 -0
- package/dist/lib/cli.js +8 -0
- package/dist/lib/files.d.ts +48 -0
- package/dist/lib/files.js +529 -0
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +96 -0
- package/dist/lib/logger.d.ts +3 -0
- package/dist/lib/logger.js +21 -0
- package/dist/lib/ndjson.d.ts +2 -0
- package/dist/lib/ndjson.js +45 -0
- package/dist/lib/notes.d.ts +8 -0
- package/dist/lib/notes.js +144 -0
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +14 -0
- package/dist/lib/project.d.ts +1 -0
- package/dist/lib/project.js +17 -0
- package/dist/lib/promptSafety.d.ts +1 -0
- package/dist/lib/promptSafety.js +20 -0
- package/dist/lib/signals.d.ts +279 -0
- package/dist/lib/signals.js +55 -0
- package/dist/lib/tty.d.ts +2 -0
- package/dist/lib/tty.js +10 -0
- package/dist/lib/validate.d.ts +9 -0
- package/dist/lib/validate.js +282 -0
- package/dist/lib/yaml.d.ts +4 -0
- package/dist/lib/yaml.js +26 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +259 -0
- package/dist/plain/colors.d.ts +5 -0
- package/dist/plain/colors.js +16 -0
- package/dist/plain/diff.d.ts +1 -0
- package/dist/plain/diff.js +129 -0
- package/dist/plain/handoff.d.ts +1 -0
- package/dist/plain/handoff.js +9 -0
- package/dist/plain/init.d.ts +1 -0
- package/dist/plain/init.js +44 -0
- package/dist/plain/notes.d.ts +5 -0
- package/dist/plain/notes.js +49 -0
- package/dist/plain/status.d.ts +2 -0
- package/dist/plain/status.js +13 -0
- package/dist/plain/watch.d.ts +1 -0
- package/dist/plain/watch.js +78 -0
- package/dist/ui/CleanupPlan.d.ts +5 -0
- package/dist/ui/CleanupPlan.js +8 -0
- package/dist/ui/DetectorSweep.d.ts +6 -0
- package/dist/ui/DetectorSweep.js +54 -0
- package/dist/ui/DriftAlert.d.ts +7 -0
- package/dist/ui/DriftAlert.js +105 -0
- package/dist/ui/Handoff.d.ts +1 -0
- package/dist/ui/Handoff.js +37 -0
- package/dist/ui/Init.d.ts +1 -0
- package/dist/ui/Init.js +117 -0
- package/dist/ui/Logo.d.ts +1 -0
- package/dist/ui/Logo.js +13 -0
- package/dist/ui/SpecSummary.d.ts +8 -0
- package/dist/ui/SpecSummary.js +15 -0
- package/dist/ui/Status.d.ts +1 -0
- package/dist/ui/Status.js +38 -0
- package/dist/ui/Watch.d.ts +1 -0
- package/dist/ui/Watch.js +136 -0
- package/dist/yoga.wasm +0 -0
- package/package.json +50 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createDetectorFromYaml, getRulesDir } from "./yamlRunner.js";
|
|
4
|
+
import { detectMultiuser } from "./multiuser.js";
|
|
5
|
+
import { detectAdmin } from "./admin.js";
|
|
6
|
+
import { detectDuplicates } from "./duplicates.js";
|
|
7
|
+
const rulesDir = getRulesDir();
|
|
8
|
+
function listYamlFiles(dir) {
|
|
9
|
+
try {
|
|
10
|
+
if (!existsSync(dir))
|
|
11
|
+
return [];
|
|
12
|
+
return readdirSync(dir)
|
|
13
|
+
.filter((f) => f.toLowerCase().endsWith(".yaml"))
|
|
14
|
+
.map((f) => join(dir, f));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function loadYamlDetectors() {
|
|
21
|
+
const detectors = [];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const coreRuleNames = ["framework", "auth", "database", "payments", "jobs", "exports"];
|
|
24
|
+
for (const base of coreRuleNames) {
|
|
25
|
+
const rulePath = join(rulesDir, `${base}.yaml`);
|
|
26
|
+
if (!existsSync(rulePath))
|
|
27
|
+
continue;
|
|
28
|
+
const key = rulePath.toLowerCase();
|
|
29
|
+
if (seen.has(key))
|
|
30
|
+
continue;
|
|
31
|
+
seen.add(key);
|
|
32
|
+
detectors.push(createDetectorFromYaml(rulePath));
|
|
33
|
+
}
|
|
34
|
+
for (const file of listYamlFiles(rulesDir)) {
|
|
35
|
+
const key = file.toLowerCase();
|
|
36
|
+
if (seen.has(key))
|
|
37
|
+
continue;
|
|
38
|
+
seen.add(key);
|
|
39
|
+
detectors.push(createDetectorFromYaml(file));
|
|
40
|
+
}
|
|
41
|
+
const tackDetectorsDir = join(process.cwd(), ".tack", "detectors");
|
|
42
|
+
for (const file of listYamlFiles(tackDetectorsDir)) {
|
|
43
|
+
const key = file.toLowerCase();
|
|
44
|
+
if (seen.has(key))
|
|
45
|
+
continue;
|
|
46
|
+
seen.add(key);
|
|
47
|
+
detectors.push(createDetectorFromYaml(file));
|
|
48
|
+
}
|
|
49
|
+
return detectors;
|
|
50
|
+
}
|
|
51
|
+
const YAML_DETECTORS = loadYamlDetectors();
|
|
52
|
+
export const PRIMARY_DETECTORS = [
|
|
53
|
+
...YAML_DETECTORS,
|
|
54
|
+
{ name: "multiuser", displayName: "Scanning for multi-tenant patterns", run: detectMultiuser },
|
|
55
|
+
{ name: "admin", displayName: "Scanning for admin routes", run: detectAdmin },
|
|
56
|
+
];
|
|
57
|
+
export function runAllDetectors() {
|
|
58
|
+
const results = [];
|
|
59
|
+
const allSignals = [];
|
|
60
|
+
for (const detector of PRIMARY_DETECTORS) {
|
|
61
|
+
const result = detector.run();
|
|
62
|
+
results.push(result);
|
|
63
|
+
allSignals.push(...result.signals);
|
|
64
|
+
}
|
|
65
|
+
const dupeResult = detectDuplicates(allSignals);
|
|
66
|
+
results.push(dupeResult);
|
|
67
|
+
allSignals.push(...dupeResult.signals);
|
|
68
|
+
const seen = new Map();
|
|
69
|
+
for (const sig of allSignals) {
|
|
70
|
+
const key = `${sig.category}:${sig.id}:${sig.detail ?? ""}`;
|
|
71
|
+
const existing = seen.get(key);
|
|
72
|
+
if (!existing || sig.confidence > existing.confidence) {
|
|
73
|
+
seen.set(key, sig);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
results,
|
|
78
|
+
signals: Array.from(seen.values()),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function detectorsForFileChange(filepath) {
|
|
82
|
+
const f = filepath.toLowerCase();
|
|
83
|
+
if (f === "package.json" ||
|
|
84
|
+
f.endsWith("package-lock.json") ||
|
|
85
|
+
f.endsWith("bun.lockb") ||
|
|
86
|
+
f.endsWith("yarn.lock") ||
|
|
87
|
+
f.endsWith("pnpm-lock.yaml")) {
|
|
88
|
+
return PRIMARY_DETECTORS;
|
|
89
|
+
}
|
|
90
|
+
const triggered = [];
|
|
91
|
+
const find = (name) => PRIMARY_DETECTORS.find((d) => d.name === name);
|
|
92
|
+
if (f.includes("prisma/schema") ||
|
|
93
|
+
f.includes("drizzle") ||
|
|
94
|
+
f.includes("migrations/") ||
|
|
95
|
+
f.includes("schema.ts") ||
|
|
96
|
+
f.includes("schema.js")) {
|
|
97
|
+
const db = find("database");
|
|
98
|
+
const mu = find("multiuser");
|
|
99
|
+
if (db)
|
|
100
|
+
triggered.push(db);
|
|
101
|
+
if (mu)
|
|
102
|
+
triggered.push(mu);
|
|
103
|
+
}
|
|
104
|
+
if (f.includes("auth") || f.includes("middleware") || f.includes("clerk")) {
|
|
105
|
+
const auth = find("auth");
|
|
106
|
+
if (auth)
|
|
107
|
+
triggered.push(auth);
|
|
108
|
+
}
|
|
109
|
+
if (f.includes("stripe") || f.includes("payment") || f.includes("webhook") || f.includes("billing")) {
|
|
110
|
+
const pay = find("payments");
|
|
111
|
+
if (pay)
|
|
112
|
+
triggered.push(pay);
|
|
113
|
+
}
|
|
114
|
+
if (f.includes("admin")) {
|
|
115
|
+
const admin = find("admin");
|
|
116
|
+
if (admin)
|
|
117
|
+
triggered.push(admin);
|
|
118
|
+
}
|
|
119
|
+
if (f.includes("job") || f.includes("worker") || f.includes("queue") || f.includes("cron")) {
|
|
120
|
+
const jobs = find("jobs");
|
|
121
|
+
if (jobs)
|
|
122
|
+
triggered.push(jobs);
|
|
123
|
+
}
|
|
124
|
+
if (triggered.length === 0) {
|
|
125
|
+
return PRIMARY_DETECTORS;
|
|
126
|
+
}
|
|
127
|
+
return triggered;
|
|
128
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createSignal } from "../lib/signals.js";
|
|
2
|
+
import { readJson, fileExists } from "../lib/files.js";
|
|
3
|
+
const JOB_SYSTEMS = [
|
|
4
|
+
{
|
|
5
|
+
id: "bullmq",
|
|
6
|
+
packages: ["bullmq", "bull"],
|
|
7
|
+
directories: ["src/jobs", "src/workers", "workers", "jobs", "src/queues", "queues"],
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "agenda",
|
|
11
|
+
packages: ["agenda"],
|
|
12
|
+
directories: ["src/jobs", "jobs"],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "cron",
|
|
16
|
+
packages: ["node-cron", "cron"],
|
|
17
|
+
directories: ["src/cron", "cron"],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "temporal",
|
|
21
|
+
packages: ["@temporalio/client", "@temporalio/worker"],
|
|
22
|
+
directories: ["src/workflows", "src/activities"],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "inngest",
|
|
26
|
+
packages: ["inngest"],
|
|
27
|
+
directories: [],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "trigger",
|
|
31
|
+
packages: ["@trigger.dev/sdk"],
|
|
32
|
+
directories: ["src/trigger", "trigger"],
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
export function detectJobs() {
|
|
36
|
+
try {
|
|
37
|
+
const signals = [];
|
|
38
|
+
const pkg = readJson("package.json");
|
|
39
|
+
const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
40
|
+
for (const system of JOB_SYSTEMS) {
|
|
41
|
+
const foundPkgs = system.packages.filter((p) => p in allDeps);
|
|
42
|
+
const foundDirs = system.directories.filter((d) => fileExists(d));
|
|
43
|
+
const sources = [];
|
|
44
|
+
let confidence = 0;
|
|
45
|
+
if (foundPkgs.length > 0) {
|
|
46
|
+
sources.push(`package.json (${foundPkgs.join(", ")})`);
|
|
47
|
+
confidence = 0.8;
|
|
48
|
+
}
|
|
49
|
+
if (foundDirs.length > 0) {
|
|
50
|
+
sources.push(...foundDirs);
|
|
51
|
+
confidence = Math.min(confidence + 0.2, 1);
|
|
52
|
+
}
|
|
53
|
+
if (sources.length > 0) {
|
|
54
|
+
signals.push(createSignal("system", "background_jobs", sources.join(" + "), confidence, system.id));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { name: "jobs", signals };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { name: "jobs", signals: [] };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createSignal } from "../lib/signals.js";
|
|
2
|
+
import { readFile, fileExists, grepFiles, listProjectFiles } from "../lib/files.js";
|
|
3
|
+
const SCHEMA_FILES = [
|
|
4
|
+
"prisma/schema.prisma",
|
|
5
|
+
"src/db/schema.ts",
|
|
6
|
+
"db/schema.ts",
|
|
7
|
+
"src/schema.ts",
|
|
8
|
+
"drizzle/schema.ts",
|
|
9
|
+
];
|
|
10
|
+
const SCHEMA_PATTERNS = [
|
|
11
|
+
/model\s+Organization\b/i,
|
|
12
|
+
/model\s+Org\b/i,
|
|
13
|
+
/model\s+Team\b/i,
|
|
14
|
+
/model\s+Workspace\b/i,
|
|
15
|
+
/model\s+Tenant\b/i,
|
|
16
|
+
/table\s*\(\s*["']organizations["']/i,
|
|
17
|
+
/table\s*\(\s*["']teams["']/i,
|
|
18
|
+
/table\s*\(\s*["']workspaces["']/i,
|
|
19
|
+
/table\s*\(\s*["']tenants["']/i,
|
|
20
|
+
/organizationId|orgId|teamId|workspaceId|tenantId/,
|
|
21
|
+
];
|
|
22
|
+
const ROUTE_PATTERNS = [
|
|
23
|
+
/\/org\/|\/organization\/|\/team\/|\/workspace\//,
|
|
24
|
+
/\[orgId\]|\[teamId\]|\[workspaceId\]/,
|
|
25
|
+
/params\.orgId|params\.teamId|params\.workspaceId/,
|
|
26
|
+
];
|
|
27
|
+
export function detectMultiuser() {
|
|
28
|
+
try {
|
|
29
|
+
const signals = [];
|
|
30
|
+
const projectFiles = listProjectFiles();
|
|
31
|
+
for (const schemaFile of SCHEMA_FILES) {
|
|
32
|
+
if (!fileExists(schemaFile))
|
|
33
|
+
continue;
|
|
34
|
+
const content = readFile(schemaFile);
|
|
35
|
+
if (!content)
|
|
36
|
+
continue;
|
|
37
|
+
for (const pattern of SCHEMA_PATTERNS) {
|
|
38
|
+
const match = content.match(pattern);
|
|
39
|
+
if (match) {
|
|
40
|
+
signals.push(createSignal("scope", "multi_tenant", `${schemaFile} (${match[0]} found)`, 0.7, "Organization/team model in schema"));
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const routeMatches = ROUTE_PATTERNS.flatMap((p) => grepFiles(projectFiles, p, 3));
|
|
46
|
+
if (routeMatches.length > 0) {
|
|
47
|
+
const files = [...new Set(routeMatches.map((m) => m.file))];
|
|
48
|
+
signals.push(createSignal("scope", "multi_tenant", files.join(", "), 0.6, "Org/team route patterns found"));
|
|
49
|
+
}
|
|
50
|
+
return { name: "multiuser", signals };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { name: "multiuser", signals: [] };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createSignal } from "../lib/signals.js";
|
|
2
|
+
import { readJson, grepFiles, listProjectFiles } from "../lib/files.js";
|
|
3
|
+
const PAYMENT_SYSTEMS = [
|
|
4
|
+
{
|
|
5
|
+
id: "stripe",
|
|
6
|
+
packages: ["stripe", "@stripe/stripe-js", "@stripe/react-stripe-js"],
|
|
7
|
+
webhookPatterns: [/stripe\.webhooks\.constructEvent|webhook.*stripe|stripe.*webhook/i],
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "paddle",
|
|
11
|
+
packages: ["@paddle/paddle-js", "@paddle/paddle-node-sdk"],
|
|
12
|
+
webhookPatterns: [/paddle.*webhook|webhook.*paddle/i],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "lemonsqueezy",
|
|
16
|
+
packages: ["@lemonsqueezy/lemonsqueezy.js"],
|
|
17
|
+
webhookPatterns: [/lemonsqueezy.*webhook|webhook.*lemonsqueezy/i],
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
export function detectPayments() {
|
|
21
|
+
try {
|
|
22
|
+
const signals = [];
|
|
23
|
+
const pkg = readJson("package.json");
|
|
24
|
+
const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
25
|
+
const projectFiles = listProjectFiles();
|
|
26
|
+
for (const payment of PAYMENT_SYSTEMS) {
|
|
27
|
+
const foundPkgs = payment.packages.filter((p) => p in allDeps);
|
|
28
|
+
const webhookFiles = payment.webhookPatterns.flatMap((pattern) => grepFiles(projectFiles, pattern, 3));
|
|
29
|
+
const sources = [];
|
|
30
|
+
let confidence = 0;
|
|
31
|
+
if (foundPkgs.length > 0) {
|
|
32
|
+
sources.push(`package.json (${foundPkgs.join(", ")})`);
|
|
33
|
+
confidence = 0.8;
|
|
34
|
+
}
|
|
35
|
+
if (webhookFiles.length > 0) {
|
|
36
|
+
const uniqueFiles = [...new Set(webhookFiles.map((m) => m.file))];
|
|
37
|
+
sources.push(...uniqueFiles);
|
|
38
|
+
confidence = Math.min(confidence + 0.2, 1);
|
|
39
|
+
}
|
|
40
|
+
if (sources.length > 0) {
|
|
41
|
+
signals.push(createSignal("system", "payments", sources.join(" + "), confidence, payment.id));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { name: "payments", signals };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { name: "payments", signals: [] };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: auth
|
|
2
|
+
displayName: "Detecting auth"
|
|
3
|
+
signalId: auth
|
|
4
|
+
category: system
|
|
5
|
+
systems:
|
|
6
|
+
- id: clerk
|
|
7
|
+
packages: ["@clerk/nextjs", "@clerk/clerk-react", "@clerk/express"]
|
|
8
|
+
routePatterns: ["clerkMiddleware", "ClerkProvider", "useAuth", "useUser"]
|
|
9
|
+
- id: nextauth
|
|
10
|
+
packages: ["next-auth", "@auth/core"]
|
|
11
|
+
configFiles: ["auth.ts", "auth.config.ts", "src/auth.ts"]
|
|
12
|
+
routePatterns: ["NextAuth", "getServerSession", "useSession"]
|
|
13
|
+
- id: auth0
|
|
14
|
+
packages: ["@auth0/nextjs-auth0", "@auth0/auth0-react", "auth0"]
|
|
15
|
+
routePatterns: ["Auth0Provider", "useUser", "withPageAuthRequired"]
|
|
16
|
+
- id: supabase-auth
|
|
17
|
+
packages: ["@supabase/auth-helpers-nextjs", "@supabase/ssr"]
|
|
18
|
+
routePatterns: ["createClientComponentClient", "createServerComponentClient"]
|
|
19
|
+
- id: lucia
|
|
20
|
+
packages: ["lucia", "@lucia-auth/adapter-prisma", "@lucia-auth/adapter-drizzle"]
|
|
21
|
+
routePatterns: ["Lucia", "validateSessionCookie"]
|
|
22
|
+
- id: passport
|
|
23
|
+
packages: ["passport", "passport-local", "passport-google-oauth20"]
|
|
24
|
+
routePatterns: ["passport\\.authenticate", "passport\\.use"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: database
|
|
2
|
+
displayName: "Detecting database"
|
|
3
|
+
signalId: db
|
|
4
|
+
category: system
|
|
5
|
+
systems:
|
|
6
|
+
- id: prisma
|
|
7
|
+
packages: ["prisma", "@prisma/client"]
|
|
8
|
+
configFiles: ["prisma/schema.prisma"]
|
|
9
|
+
- id: drizzle
|
|
10
|
+
packages: ["drizzle-orm", "drizzle-kit"]
|
|
11
|
+
configFiles: ["drizzle.config.ts", "drizzle.config.js"]
|
|
12
|
+
- id: typeorm
|
|
13
|
+
packages: ["typeorm"]
|
|
14
|
+
configFiles: ["ormconfig.json", "ormconfig.ts", "ormconfig.js"]
|
|
15
|
+
- id: mongoose
|
|
16
|
+
packages: ["mongoose"]
|
|
17
|
+
- id: knex
|
|
18
|
+
packages: ["knex"]
|
|
19
|
+
configFiles: ["knexfile.js", "knexfile.ts"]
|
|
20
|
+
- id: postgres
|
|
21
|
+
packages: ["pg", "@neondatabase/serverless"]
|
|
22
|
+
- id: mysql
|
|
23
|
+
packages: ["mysql2", "@planetscale/database"]
|
|
24
|
+
- id: sqlite
|
|
25
|
+
packages: ["better-sqlite3", "@libsql/client"]
|
|
26
|
+
- id: mongodb
|
|
27
|
+
packages: ["mongodb"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: exports
|
|
2
|
+
displayName: "Detecting export/PDF systems"
|
|
3
|
+
signalId: exports
|
|
4
|
+
category: system
|
|
5
|
+
systems:
|
|
6
|
+
- id: jspdf
|
|
7
|
+
packages: ["jspdf"]
|
|
8
|
+
- id: pdfkit
|
|
9
|
+
packages: ["pdfkit"]
|
|
10
|
+
- id: react-pdf
|
|
11
|
+
packages: ["@react-pdf/renderer"]
|
|
12
|
+
- id: puppeteer
|
|
13
|
+
packages: ["puppeteer"]
|
|
14
|
+
- id: playwright
|
|
15
|
+
packages: ["playwright"]
|
|
16
|
+
- id: html2canvas
|
|
17
|
+
packages: ["html2canvas"]
|
|
18
|
+
- id: exceljs
|
|
19
|
+
packages: ["exceljs"]
|
|
20
|
+
- id: sheetjs
|
|
21
|
+
packages: ["xlsx"]
|
|
22
|
+
- id: csv-writer
|
|
23
|
+
packages: ["csv-writer"]
|
|
24
|
+
- id: csv-stringify
|
|
25
|
+
packages: ["csv-stringify"]
|
|
26
|
+
- id: json2csv
|
|
27
|
+
packages: ["json2csv"]
|
|
28
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: framework
|
|
2
|
+
displayName: "Detecting framework"
|
|
3
|
+
signalId: framework
|
|
4
|
+
category: system
|
|
5
|
+
systems:
|
|
6
|
+
- id: nextjs
|
|
7
|
+
packages: ["next"]
|
|
8
|
+
configFiles: ["next.config.js", "next.config.mjs", "next.config.ts"]
|
|
9
|
+
- id: remix
|
|
10
|
+
packages: ["@remix-run/node", "@remix-run/react"]
|
|
11
|
+
configFiles: ["remix.config.js"]
|
|
12
|
+
- id: sveltekit
|
|
13
|
+
packages: ["@sveltejs/kit"]
|
|
14
|
+
configFiles: ["svelte.config.js"]
|
|
15
|
+
- id: vite
|
|
16
|
+
packages: ["vite"]
|
|
17
|
+
configFiles: ["vite.config.ts", "vite.config.js"]
|
|
18
|
+
- id: express
|
|
19
|
+
packages: ["express"]
|
|
20
|
+
- id: fastify
|
|
21
|
+
packages: ["fastify"]
|
|
22
|
+
- id: hono
|
|
23
|
+
packages: ["hono"]
|
|
24
|
+
- id: astro
|
|
25
|
+
packages: ["astro"]
|
|
26
|
+
configFiles: ["astro.config.mjs", "astro.config.ts"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: jobs
|
|
2
|
+
displayName: "Detecting background jobs"
|
|
3
|
+
signalId: background_jobs
|
|
4
|
+
category: system
|
|
5
|
+
systems:
|
|
6
|
+
- id: bullmq
|
|
7
|
+
packages: ["bullmq", "bull"]
|
|
8
|
+
directories: ["src/jobs", "src/workers", "workers", "jobs", "src/queues", "queues"]
|
|
9
|
+
- id: agenda
|
|
10
|
+
packages: ["agenda"]
|
|
11
|
+
directories: ["src/jobs", "jobs"]
|
|
12
|
+
- id: cron
|
|
13
|
+
packages: ["node-cron", "cron"]
|
|
14
|
+
directories: ["src/cron", "cron"]
|
|
15
|
+
- id: temporal
|
|
16
|
+
packages: ["@temporalio/client", "@temporalio/worker"]
|
|
17
|
+
directories: ["src/workflows", "src/activities"]
|
|
18
|
+
- id: inngest
|
|
19
|
+
packages: ["inngest"]
|
|
20
|
+
- id: trigger
|
|
21
|
+
packages: ["@trigger.dev/sdk"]
|
|
22
|
+
directories: ["src/trigger", "trigger"]
|
|
23
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: payments
|
|
2
|
+
displayName: "Detecting payments"
|
|
3
|
+
signalId: payments
|
|
4
|
+
category: system
|
|
5
|
+
systems:
|
|
6
|
+
- id: stripe
|
|
7
|
+
packages: ["stripe", "@stripe/stripe-js", "@stripe/react-stripe-js"]
|
|
8
|
+
routePatterns:
|
|
9
|
+
- "stripe\\.webhooks\\.constructEvent"
|
|
10
|
+
- "webhook.*stripe"
|
|
11
|
+
- "stripe.*webhook"
|
|
12
|
+
- id: paddle
|
|
13
|
+
packages: ["@paddle/paddle-js", "@paddle/paddle-node-sdk"]
|
|
14
|
+
routePatterns:
|
|
15
|
+
- "paddle.*webhook"
|
|
16
|
+
- "webhook.*paddle"
|
|
17
|
+
- id: lemonsqueezy
|
|
18
|
+
packages: ["@lemonsqueezy/lemonsqueezy.js"]
|
|
19
|
+
routePatterns:
|
|
20
|
+
- "lemonsqueezy.*webhook"
|
|
21
|
+
- "webhook.*lemonsqueezy"
|
|
22
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type DetectorResult } from "../lib/signals.js";
|
|
2
|
+
import type { SignalCategory } from "../lib/signals.js";
|
|
3
|
+
export interface YamlSystemRule {
|
|
4
|
+
id: string;
|
|
5
|
+
packages?: string[];
|
|
6
|
+
configFiles?: string[];
|
|
7
|
+
routePatterns?: string[];
|
|
8
|
+
directories?: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface YamlDetectorRule {
|
|
11
|
+
name: string;
|
|
12
|
+
displayName: string;
|
|
13
|
+
signalId: string;
|
|
14
|
+
category: SignalCategory;
|
|
15
|
+
systems: YamlSystemRule[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a detector that runs from a YAML rule file.
|
|
19
|
+
* Binary detection: if any source matches, emit one signal with confidence 1.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Create a detector entry that loads and runs a single YAML rule file.
|
|
23
|
+
* Name/displayName are read once at creation; run() re-reads the file so rules are fresh.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createDetectorFromYaml(yamlPath: string): {
|
|
26
|
+
name: string;
|
|
27
|
+
displayName: string;
|
|
28
|
+
run: () => DetectorResult;
|
|
29
|
+
};
|
|
30
|
+
/** Resolve the rules directory for both source (src/detectors/rules) and bundled (dist/detectors/rules). */
|
|
31
|
+
export declare function getRulesDir(): string;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import { createSignal } from "../lib/signals.js";
|
|
6
|
+
import { readJson, fileExists, grepFiles, listProjectFiles, projectRoot } from "../lib/files.js";
|
|
7
|
+
function parseRule(content) {
|
|
8
|
+
const raw = yaml.load(content);
|
|
9
|
+
if (!raw || typeof raw !== "object" || !Array.isArray(raw.systems))
|
|
10
|
+
return null;
|
|
11
|
+
const rule = raw;
|
|
12
|
+
if (typeof rule.name !== "string" ||
|
|
13
|
+
typeof rule.displayName !== "string" ||
|
|
14
|
+
typeof rule.signalId !== "string" ||
|
|
15
|
+
(rule.category !== "system" && rule.category !== "scope" && rule.category !== "risk"))
|
|
16
|
+
return null;
|
|
17
|
+
return rule;
|
|
18
|
+
}
|
|
19
|
+
/** Invalid regex in YAML (e.g. unescaped brackets) skips that pattern instead of throwing. */
|
|
20
|
+
function safeRegex(patternStr) {
|
|
21
|
+
try {
|
|
22
|
+
return new RegExp(patternStr);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a detector that runs from a YAML rule file.
|
|
30
|
+
* Binary detection: if any source matches, emit one signal with confidence 1.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Create a detector entry that loads and runs a single YAML rule file.
|
|
34
|
+
* Name/displayName are read once at creation; run() re-reads the file so rules are fresh.
|
|
35
|
+
*/
|
|
36
|
+
export function createDetectorFromYaml(yamlPath) {
|
|
37
|
+
const ruleName = yamlPath.split(/[/\\]/).pop()?.replace(/\.yaml$/, "") ?? "yaml";
|
|
38
|
+
let name = ruleName;
|
|
39
|
+
let displayName = ruleName;
|
|
40
|
+
try {
|
|
41
|
+
const content = readFileSync(yamlPath, "utf-8");
|
|
42
|
+
const rule = parseRule(content);
|
|
43
|
+
if (rule) {
|
|
44
|
+
name = rule.name;
|
|
45
|
+
displayName = rule.displayName;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// keep defaults
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
name,
|
|
53
|
+
displayName,
|
|
54
|
+
run: () => {
|
|
55
|
+
try {
|
|
56
|
+
const content = readFileSync(yamlPath, "utf-8");
|
|
57
|
+
const rule = parseRule(content);
|
|
58
|
+
if (!rule)
|
|
59
|
+
return { name: ruleName, signals: [] };
|
|
60
|
+
const signals = [];
|
|
61
|
+
const pkg = readJson("package.json");
|
|
62
|
+
const allDeps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
63
|
+
let projectFiles = [];
|
|
64
|
+
try {
|
|
65
|
+
projectFiles = listProjectFiles();
|
|
66
|
+
// Never search inside the directory that contains this rule file (avoids
|
|
67
|
+
// matching rule definitions as if they were project code; fixes false
|
|
68
|
+
// positives for the tack repo and any project that has rule YAML in-tree).
|
|
69
|
+
const base = projectRoot();
|
|
70
|
+
const ruleDirRel = relative(base, dirname(yamlPath)).replace(/\\/g, "/");
|
|
71
|
+
if (ruleDirRel && !ruleDirRel.startsWith("..")) {
|
|
72
|
+
const prefixWithSep = ruleDirRel.endsWith("/") ? ruleDirRel : `${ruleDirRel}/`;
|
|
73
|
+
projectFiles = projectFiles.filter((f) => {
|
|
74
|
+
const n = f.replace(/\\/g, "/");
|
|
75
|
+
return n !== ruleDirRel && !n.startsWith(prefixWithSep);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// non-node or no project root
|
|
81
|
+
}
|
|
82
|
+
for (const system of rule.systems) {
|
|
83
|
+
if (!system?.id || typeof system.id !== "string")
|
|
84
|
+
continue;
|
|
85
|
+
const foundPkgs = (system.packages ?? []).filter((p) => p in allDeps);
|
|
86
|
+
const foundConfigs = (system.configFiles ?? []).filter((f) => fileExists(f));
|
|
87
|
+
const foundDirs = (system.directories ?? []).filter((d) => fileExists(d));
|
|
88
|
+
let routeMatch;
|
|
89
|
+
const routePatterns = system.routePatterns ?? [];
|
|
90
|
+
for (const patternStr of routePatterns) {
|
|
91
|
+
const pattern = safeRegex(patternStr);
|
|
92
|
+
if (!pattern)
|
|
93
|
+
continue;
|
|
94
|
+
const matches = grepFiles(projectFiles, pattern, 1);
|
|
95
|
+
if (matches.length > 0) {
|
|
96
|
+
routeMatch = matches[0].file;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const sources = [];
|
|
101
|
+
if (foundPkgs.length > 0)
|
|
102
|
+
sources.push(`package.json (${foundPkgs.join(", ")})`);
|
|
103
|
+
if (foundConfigs.length > 0)
|
|
104
|
+
sources.push(foundConfigs[0]);
|
|
105
|
+
if (foundDirs.length > 0)
|
|
106
|
+
sources.push(...foundDirs.slice(0, 3));
|
|
107
|
+
if (routeMatch)
|
|
108
|
+
sources.push(routeMatch);
|
|
109
|
+
if (sources.length > 0) {
|
|
110
|
+
signals.push(createSignal(rule.category, rule.signalId, sources.join(" + "), 1, system.id));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { name: rule.name, signals };
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { name: ruleName, signals: [] };
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Resolve the rules directory for both source (src/detectors/rules) and bundled (dist/detectors/rules). */
|
|
122
|
+
export function getRulesDir() {
|
|
123
|
+
const baseDir = dirname(fileURLToPath(import.meta.url));
|
|
124
|
+
const nextToMe = join(baseDir, "rules");
|
|
125
|
+
if (existsSync(nextToMe))
|
|
126
|
+
return nextToMe;
|
|
127
|
+
return join(baseDir, "detectors", "rules");
|
|
128
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type CleanupPlan = {
|
|
2
|
+
system: string;
|
|
3
|
+
packagesToRemove: string[];
|
|
4
|
+
filesToReview: Array<{
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
content: string;
|
|
8
|
+
}>;
|
|
9
|
+
configFilesToCheck: string[];
|
|
10
|
+
summary: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function generateCleanupPlan(systemId: string): CleanupPlan;
|