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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/dist/App.d.ts +5 -0
  4. package/dist/App.js +17 -0
  5. package/dist/detectors/admin.d.ts +2 -0
  6. package/dist/detectors/admin.js +33 -0
  7. package/dist/detectors/auth.d.ts +2 -0
  8. package/dist/detectors/auth.js +86 -0
  9. package/dist/detectors/database.d.ts +2 -0
  10. package/dist/detectors/database.js +96 -0
  11. package/dist/detectors/duplicates.d.ts +2 -0
  12. package/dist/detectors/duplicates.js +23 -0
  13. package/dist/detectors/exports.d.ts +2 -0
  14. package/dist/detectors/exports.js +30 -0
  15. package/dist/detectors/framework.d.ts +2 -0
  16. package/dist/detectors/framework.js +71 -0
  17. package/dist/detectors/index.d.ts +12 -0
  18. package/dist/detectors/index.js +128 -0
  19. package/dist/detectors/jobs.d.ts +2 -0
  20. package/dist/detectors/jobs.js +62 -0
  21. package/dist/detectors/multiuser.d.ts +2 -0
  22. package/dist/detectors/multiuser.js +55 -0
  23. package/dist/detectors/payments.d.ts +2 -0
  24. package/dist/detectors/payments.js +49 -0
  25. package/dist/detectors/rules/auth.yaml +24 -0
  26. package/dist/detectors/rules/database.yaml +27 -0
  27. package/dist/detectors/rules/exports.yaml +28 -0
  28. package/dist/detectors/rules/framework.yaml +26 -0
  29. package/dist/detectors/rules/jobs.yaml +23 -0
  30. package/dist/detectors/rules/payments.yaml +22 -0
  31. package/dist/detectors/types.d.ts +2 -0
  32. package/dist/detectors/types.js +1 -0
  33. package/dist/detectors/yamlRunner.d.ts +31 -0
  34. package/dist/detectors/yamlRunner.js +128 -0
  35. package/dist/engine/cleanup.d.ts +12 -0
  36. package/dist/engine/cleanup.js +101 -0
  37. package/dist/engine/compaction.d.ts +5 -0
  38. package/dist/engine/compaction.js +44 -0
  39. package/dist/engine/compareSpec.d.ts +2 -0
  40. package/dist/engine/compareSpec.js +74 -0
  41. package/dist/engine/computeDrift.d.ts +6 -0
  42. package/dist/engine/computeDrift.js +133 -0
  43. package/dist/engine/contextPack.d.ts +4 -0
  44. package/dist/engine/contextPack.js +169 -0
  45. package/dist/engine/decisions.d.ts +4 -0
  46. package/dist/engine/decisions.js +21 -0
  47. package/dist/engine/diff.d.ts +46 -0
  48. package/dist/engine/diff.js +210 -0
  49. package/dist/engine/handoff.d.ts +7 -0
  50. package/dist/engine/handoff.js +469 -0
  51. package/dist/engine/status.d.ts +10 -0
  52. package/dist/engine/status.js +46 -0
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.js +299 -0
  55. package/dist/lib/cli.d.ts +4 -0
  56. package/dist/lib/cli.js +8 -0
  57. package/dist/lib/files.d.ts +48 -0
  58. package/dist/lib/files.js +529 -0
  59. package/dist/lib/git.d.ts +9 -0
  60. package/dist/lib/git.js +96 -0
  61. package/dist/lib/logger.d.ts +3 -0
  62. package/dist/lib/logger.js +21 -0
  63. package/dist/lib/ndjson.d.ts +2 -0
  64. package/dist/lib/ndjson.js +45 -0
  65. package/dist/lib/notes.d.ts +8 -0
  66. package/dist/lib/notes.js +144 -0
  67. package/dist/lib/notify.d.ts +1 -0
  68. package/dist/lib/notify.js +14 -0
  69. package/dist/lib/project.d.ts +1 -0
  70. package/dist/lib/project.js +17 -0
  71. package/dist/lib/promptSafety.d.ts +1 -0
  72. package/dist/lib/promptSafety.js +20 -0
  73. package/dist/lib/signals.d.ts +279 -0
  74. package/dist/lib/signals.js +55 -0
  75. package/dist/lib/tty.d.ts +2 -0
  76. package/dist/lib/tty.js +10 -0
  77. package/dist/lib/validate.d.ts +9 -0
  78. package/dist/lib/validate.js +282 -0
  79. package/dist/lib/yaml.d.ts +4 -0
  80. package/dist/lib/yaml.js +26 -0
  81. package/dist/mcp.d.ts +1 -0
  82. package/dist/mcp.js +259 -0
  83. package/dist/plain/colors.d.ts +5 -0
  84. package/dist/plain/colors.js +16 -0
  85. package/dist/plain/diff.d.ts +1 -0
  86. package/dist/plain/diff.js +129 -0
  87. package/dist/plain/handoff.d.ts +1 -0
  88. package/dist/plain/handoff.js +9 -0
  89. package/dist/plain/init.d.ts +1 -0
  90. package/dist/plain/init.js +44 -0
  91. package/dist/plain/notes.d.ts +5 -0
  92. package/dist/plain/notes.js +49 -0
  93. package/dist/plain/status.d.ts +2 -0
  94. package/dist/plain/status.js +13 -0
  95. package/dist/plain/watch.d.ts +1 -0
  96. package/dist/plain/watch.js +78 -0
  97. package/dist/ui/CleanupPlan.d.ts +5 -0
  98. package/dist/ui/CleanupPlan.js +8 -0
  99. package/dist/ui/DetectorSweep.d.ts +6 -0
  100. package/dist/ui/DetectorSweep.js +54 -0
  101. package/dist/ui/DriftAlert.d.ts +7 -0
  102. package/dist/ui/DriftAlert.js +105 -0
  103. package/dist/ui/Handoff.d.ts +1 -0
  104. package/dist/ui/Handoff.js +37 -0
  105. package/dist/ui/Init.d.ts +1 -0
  106. package/dist/ui/Init.js +117 -0
  107. package/dist/ui/Logo.d.ts +1 -0
  108. package/dist/ui/Logo.js +13 -0
  109. package/dist/ui/SpecSummary.d.ts +8 -0
  110. package/dist/ui/SpecSummary.js +15 -0
  111. package/dist/ui/Status.d.ts +1 -0
  112. package/dist/ui/Status.js +38 -0
  113. package/dist/ui/Watch.d.ts +1 -0
  114. package/dist/ui/Watch.js +136 -0
  115. package/dist/yoga.wasm +0 -0
  116. 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,2 @@
1
+ import { type DetectorResult } from "../lib/signals.js";
2
+ export declare function detectJobs(): DetectorResult;
@@ -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,2 @@
1
+ import { type DetectorResult } from "../lib/signals.js";
2
+ export declare function detectMultiuser(): DetectorResult;
@@ -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,2 @@
1
+ import { type DetectorResult } from "../lib/signals.js";
2
+ export declare function detectPayments(): DetectorResult;
@@ -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,2 @@
1
+ import type { DetectorResult } from "../lib/signals.js";
2
+ export type DetectorFn = () => DetectorResult;
@@ -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;