@soulbatical/tetra-dev-toolkit 1.1.1 → 1.3.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/bin/cleanup-repos.sh +287 -0
- package/bin/{vca-audit.js → tetra-audit.js} +16 -12
- package/bin/{vca-dev-token.js → tetra-dev-token.js} +7 -7
- package/bin/{vca-setup.js → tetra-setup.js} +54 -20
- package/lib/checks/health/file-organization.js +389 -0
- package/lib/checks/health/index.js +1 -0
- package/lib/checks/health/quality-toolkit.js +12 -8
- package/lib/checks/health/repo-visibility.js +1 -1
- package/lib/checks/health/scanner.js +3 -1
- package/lib/checks/health/stella-integration.js +7 -7
- package/lib/checks/health/types.js +1 -1
- package/lib/checks/hygiene/file-organization.js +105 -0
- package/lib/checks/index.js +4 -0
- package/lib/checks/stability/ci-pipeline.js +1 -1
- package/lib/checks/stability/husky-hooks.js +1 -1
- package/lib/checks/supabase/rpc-param-mismatch.js +453 -0
- package/lib/commands/dev-token.js +4 -4
- package/lib/config.js +16 -12
- package/lib/index.js +3 -3
- package/lib/reporters/terminal.js +1 -1
- package/lib/runner.js +16 -3
- package/package.json +6 -8
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: File Organization (docs, scripts, config & code dir cleanliness)
|
|
3
|
+
*
|
|
4
|
+
* Verifies that:
|
|
5
|
+
* 1. Documentation files (.md) live in /docs/ or repo root
|
|
6
|
+
* 2. Scripts (.sh) live in /scripts/ or allowed dirs
|
|
7
|
+
* 3. Config files (.yml/.yaml) live in allowed locations
|
|
8
|
+
* 4. No nested docs/ directories exist
|
|
9
|
+
* 5. Code directories (backend/, frontend/, etc.) contain only code — no clutter
|
|
10
|
+
*
|
|
11
|
+
* Score: 6pt max
|
|
12
|
+
* - 1pt: No stray .md files outside /docs and root
|
|
13
|
+
* - 1pt: No stray .sh scripts outside allowed dirs
|
|
14
|
+
* - 1pt: No stray .yml/.yaml config files outside allowed dirs
|
|
15
|
+
* - 1pt: No nested docs/ directories
|
|
16
|
+
* - 1pt: No clutter in code directories (no .txt, .log, .pdf, .csv, .env, .bak, .tmp, .orig)
|
|
17
|
+
* - 1pt: No clutter in repo root (no .txt, .log, .pdf, .csv, .png, .jpg, tmp/, logs/, data/, venv/)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readdirSync, readFileSync, existsSync } from 'fs'
|
|
21
|
+
import { join, relative, basename } from 'path'
|
|
22
|
+
import { createCheck } from './types.js'
|
|
23
|
+
|
|
24
|
+
const ALLOWED_ROOT_MD = new Set([
|
|
25
|
+
'readme.md', 'claude.md', 'changelog.md', 'license.md', 'license',
|
|
26
|
+
'prompt.md', 'plan.md', 'contributing.md', 'code_of_conduct.md'
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
const IGNORED_DIRS = new Set([
|
|
30
|
+
'node_modules', 'dist', 'build', '.git', '.next', '.cache', '.turbo',
|
|
31
|
+
'coverage', '.nyc_output', '.playwright', 'test-results'
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
const ALLOWED_SCRIPT_DIRS = new Set([
|
|
35
|
+
'scripts', 'shell', 'hooks', '.husky', '.ralph', '.claude'
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
/** Directories whose .md content is always allowed (tooling config) */
|
|
39
|
+
const ALLOWED_MD_DIRS = new Set([
|
|
40
|
+
'docs', '.ralph', '.claude', '.agents', 'e2e'
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
/** Directories where .yml/.yaml config files are allowed */
|
|
44
|
+
const ALLOWED_CONFIG_DIRS = new Set([
|
|
45
|
+
'.ralph', '.github', '.claude', '.circleci', '.gitlab'
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
/** Code directories that should only contain code */
|
|
49
|
+
const CODE_DIRS = ['backend', 'frontend', 'backend-mcp', 'backend-pdf', 'mcp']
|
|
50
|
+
|
|
51
|
+
/** File extensions that are clutter in code directories */
|
|
52
|
+
const CLUTTER_EXTENSIONS = new Set([
|
|
53
|
+
'.txt', '.log', '.pdf', '.csv', '.bak', '.tmp', '.orig',
|
|
54
|
+
'.patch', '.diff', '.bk', '.old'
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
/** File names that are clutter in code directories (regardless of extension) */
|
|
58
|
+
const CLUTTER_NAMES = new Set([
|
|
59
|
+
'.env', '.env.local', '.env.development', '.env.production', '.env.staging'
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
/** Files allowed in code dirs despite having clutter extensions */
|
|
63
|
+
const ALLOWED_CLUTTER = new Set([
|
|
64
|
+
'robots.txt', 'llms.txt', 'llms-full.txt', 'humans.txt',
|
|
65
|
+
'security.txt', 'ads.txt', 'manifest.txt'
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
/** Subdirectories within code dirs where clutter files are OK */
|
|
69
|
+
const ALLOWED_CLUTTER_SUBDIRS = new Set([
|
|
70
|
+
'public', 'static', 'assets', 'fixtures', 'test-fixtures',
|
|
71
|
+
'supabase', 'migrations', 'prisma'
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
/** File extensions that are clutter in the repo root */
|
|
75
|
+
const ROOT_CLUTTER_EXTENSIONS = new Set([
|
|
76
|
+
'.txt', '.log', '.pdf', '.csv', '.png', '.jpg', '.jpeg', '.gif',
|
|
77
|
+
'.bak', '.tmp', '.orig', '.patch', '.diff', '.bk', '.old', '.py'
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Root files that are allowed despite having clutter extensions.
|
|
82
|
+
* Standard tooling/platform configs that belong in root.
|
|
83
|
+
*/
|
|
84
|
+
const ALLOWED_ROOT_FILES = new Set([
|
|
85
|
+
// Platform configs
|
|
86
|
+
'dockerfile', 'dockerfile.bot', 'dockerfile.dev', 'dockerfile.prod',
|
|
87
|
+
'procfile', 'nixpacks.toml', 'netlify.toml', 'vercel.json', 'railway.json',
|
|
88
|
+
'render.yaml', 'fly.toml',
|
|
89
|
+
// JS/TS tooling
|
|
90
|
+
'components.json', 'ecosystem.config.js', 'ecosystem.config.cjs',
|
|
91
|
+
'.eslintrc.json', '.prettierrc', '.prettierrc.json',
|
|
92
|
+
'vitest.setup.ts', 'vitest.setup.js', 'jest.setup.ts', 'jest.setup.js',
|
|
93
|
+
'commitlint.config.js', 'lint-staged.config.js',
|
|
94
|
+
// Lock files
|
|
95
|
+
'bun.lockb', 'yarn.lock', 'pnpm-lock.yaml', 'deno.lock',
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
/** Root directories that are clutter (should not exist in repo root) */
|
|
99
|
+
const ROOT_CLUTTER_DIRS = new Set([
|
|
100
|
+
'tmp', 'temp', 'logs', 'log', 'data', 'venv', '.venv',
|
|
101
|
+
'reports', 'output', 'out', 'backup', 'backups'
|
|
102
|
+
])
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Recursively find files matching a predicate, respecting ignored dirs
|
|
106
|
+
*/
|
|
107
|
+
function findFiles(dir, predicate, maxDepth = 5, currentDepth = 0) {
|
|
108
|
+
const results = []
|
|
109
|
+
if (currentDepth >= maxDepth) return results
|
|
110
|
+
|
|
111
|
+
let entries
|
|
112
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return results }
|
|
113
|
+
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const name = entry.name
|
|
116
|
+
if (IGNORED_DIRS.has(name)) continue
|
|
117
|
+
|
|
118
|
+
const fullPath = join(dir, name)
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
results.push(...findFiles(fullPath, predicate, maxDepth, currentDepth + 1))
|
|
121
|
+
} else if (entry.isFile() && predicate(name, fullPath)) {
|
|
122
|
+
results.push(fullPath)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return results
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Directories that may contain a docs/ subdirectory (allowed, not flagged) */
|
|
129
|
+
const ALLOWED_NESTED_DOCS_PARENTS = new Set(['.ralph'])
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find directories named "docs" that are not at the repo root,
|
|
133
|
+
* excluding allowed parents like .ralph/docs
|
|
134
|
+
*/
|
|
135
|
+
function findNestedDocsDirs(rootPath, currentDir, maxDepth = 4, currentDepth = 0) {
|
|
136
|
+
const results = []
|
|
137
|
+
if (currentDepth >= maxDepth) return results
|
|
138
|
+
|
|
139
|
+
let entries
|
|
140
|
+
try { entries = readdirSync(currentDir, { withFileTypes: true }) } catch { return results }
|
|
141
|
+
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
const name = entry.name
|
|
144
|
+
if (IGNORED_DIRS.has(name)) continue
|
|
145
|
+
if (!entry.isDirectory()) continue
|
|
146
|
+
|
|
147
|
+
const fullPath = join(currentDir, name)
|
|
148
|
+
const relFromRoot = relative(rootPath, fullPath)
|
|
149
|
+
const topDir = relFromRoot.split('/')[0]
|
|
150
|
+
|
|
151
|
+
if (name === 'docs' && currentDepth > 0) {
|
|
152
|
+
// Skip if parent is an allowed directory (e.g., .ralph/docs)
|
|
153
|
+
if (!ALLOWED_NESTED_DOCS_PARENTS.has(topDir)) {
|
|
154
|
+
results.push(fullPath)
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
results.push(...findNestedDocsDirs(rootPath, fullPath, maxDepth, currentDepth + 1))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return results
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function check(projectPath) {
|
|
164
|
+
const result = createCheck('file-organization', 6, {
|
|
165
|
+
strayMdFiles: [],
|
|
166
|
+
strayScripts: [],
|
|
167
|
+
strayConfigs: [],
|
|
168
|
+
clutterFiles: [],
|
|
169
|
+
rootClutter: [],
|
|
170
|
+
nestedDocsDirs: [],
|
|
171
|
+
totalStrayMd: 0,
|
|
172
|
+
totalStrayScripts: 0,
|
|
173
|
+
totalStrayConfigs: 0,
|
|
174
|
+
totalClutter: 0,
|
|
175
|
+
totalRootClutter: 0,
|
|
176
|
+
totalNestedDocs: 0
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Load .gitignore entries for root clutter check
|
|
180
|
+
const gitignorePath = join(projectPath, '.gitignore')
|
|
181
|
+
const gitignoredDirs = new Set()
|
|
182
|
+
if (existsSync(gitignorePath)) {
|
|
183
|
+
try {
|
|
184
|
+
const lines = readFileSync(gitignorePath, 'utf-8').split('\n')
|
|
185
|
+
.map(l => l.trim()).filter(l => l && !l.startsWith('#'))
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
// Match dir entries like "tmp/" or "tmp"
|
|
188
|
+
const cleaned = line.replace(/\/$/, '')
|
|
189
|
+
if (cleaned) gitignoredDirs.add(cleaned)
|
|
190
|
+
}
|
|
191
|
+
} catch { /* ignore */ }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- Check 1: Stray .md files ---
|
|
195
|
+
const allMdFiles = findFiles(projectPath, (name) => name.endsWith('.md'))
|
|
196
|
+
const strayMd = []
|
|
197
|
+
|
|
198
|
+
for (const file of allMdFiles) {
|
|
199
|
+
const rel = relative(projectPath, file)
|
|
200
|
+
const parts = rel.split('/')
|
|
201
|
+
|
|
202
|
+
// README.md and CLAUDE.md are always allowed, anywhere
|
|
203
|
+
const nameLc = basename(file).toLowerCase()
|
|
204
|
+
if (nameLc === 'readme.md' || nameLc === 'claude.md') continue
|
|
205
|
+
|
|
206
|
+
// Root .md files: only allowed ones
|
|
207
|
+
if (parts.length === 1) {
|
|
208
|
+
if (ALLOWED_ROOT_MD.has(basename(file).toLowerCase())) continue
|
|
209
|
+
strayMd.push(rel)
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Inside allowed top-level directories: always OK
|
|
214
|
+
if (ALLOWED_MD_DIRS.has(parts[0])) continue
|
|
215
|
+
|
|
216
|
+
// Inside shell/templates: OK (Ralph templates)
|
|
217
|
+
if (parts[0] === 'shell' && parts[1] === 'templates') continue
|
|
218
|
+
|
|
219
|
+
// Everything else is stray
|
|
220
|
+
strayMd.push(rel)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
result.details.strayMdFiles = strayMd.slice(0, 20) // Cap at 20 for readability
|
|
224
|
+
result.details.totalStrayMd = strayMd.length
|
|
225
|
+
|
|
226
|
+
// --- Check 2: Stray scripts ---
|
|
227
|
+
const allScripts = findFiles(projectPath, (name) => name.endsWith('.sh'))
|
|
228
|
+
const strayScripts = []
|
|
229
|
+
|
|
230
|
+
for (const file of allScripts) {
|
|
231
|
+
const rel = relative(projectPath, file)
|
|
232
|
+
const parts = rel.split('/')
|
|
233
|
+
const topDir = parts[0]
|
|
234
|
+
|
|
235
|
+
// Root-level scripts: OK
|
|
236
|
+
if (parts.length === 1) continue
|
|
237
|
+
|
|
238
|
+
// Inside allowed script directories: OK
|
|
239
|
+
if (ALLOWED_SCRIPT_DIRS.has(topDir)) continue
|
|
240
|
+
|
|
241
|
+
// Everything else is stray
|
|
242
|
+
strayScripts.push(rel)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
result.details.strayScripts = strayScripts.slice(0, 20)
|
|
246
|
+
result.details.totalStrayScripts = strayScripts.length
|
|
247
|
+
|
|
248
|
+
// --- Check 3: Stray config files (.yml/.yaml) ---
|
|
249
|
+
const allConfigs = findFiles(projectPath, (name) => name.endsWith('.yml') || name.endsWith('.yaml'))
|
|
250
|
+
const strayConfigs = []
|
|
251
|
+
|
|
252
|
+
for (const file of allConfigs) {
|
|
253
|
+
const rel = relative(projectPath, file)
|
|
254
|
+
const parts = rel.split('/')
|
|
255
|
+
|
|
256
|
+
// Root-level configs: always OK (doppler.yaml, .coderabbit.yaml, etc.)
|
|
257
|
+
if (parts.length === 1) continue
|
|
258
|
+
|
|
259
|
+
// Inside allowed config directories: OK
|
|
260
|
+
if (ALLOWED_CONFIG_DIRS.has(parts[0])) continue
|
|
261
|
+
|
|
262
|
+
// Everything else is stray
|
|
263
|
+
strayConfigs.push(rel)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
result.details.strayConfigs = strayConfigs.slice(0, 20)
|
|
267
|
+
result.details.totalStrayConfigs = strayConfigs.length
|
|
268
|
+
|
|
269
|
+
// --- Check 4: Clutter in code directories ---
|
|
270
|
+
const clutterFiles = []
|
|
271
|
+
|
|
272
|
+
for (const codeDir of CODE_DIRS) {
|
|
273
|
+
const codePath = join(projectPath, codeDir)
|
|
274
|
+
let dirExists
|
|
275
|
+
try { readdirSync(codePath); dirExists = true } catch { dirExists = false }
|
|
276
|
+
if (!dirExists) continue
|
|
277
|
+
|
|
278
|
+
const allFiles = findFiles(codePath, () => true)
|
|
279
|
+
for (const file of allFiles) {
|
|
280
|
+
const name = basename(file).toLowerCase()
|
|
281
|
+
const rel = relative(projectPath, file)
|
|
282
|
+
const relFromCodeDir = relative(codePath, file)
|
|
283
|
+
const subParts = relFromCodeDir.split('/')
|
|
284
|
+
|
|
285
|
+
// Check if in an allowed subdirectory (public/, supabase/migrations/, etc.)
|
|
286
|
+
if (subParts.length > 1 && ALLOWED_CLUTTER_SUBDIRS.has(subParts[0])) continue
|
|
287
|
+
|
|
288
|
+
// Check for .env files (secrets!)
|
|
289
|
+
if (CLUTTER_NAMES.has(name)) {
|
|
290
|
+
clutterFiles.push(rel)
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Check for allowed files (robots.txt in public, etc.)
|
|
295
|
+
if (ALLOWED_CLUTTER.has(name)) continue
|
|
296
|
+
|
|
297
|
+
// Check extension
|
|
298
|
+
const ext = name.includes('.') ? '.' + name.split('.').pop() : ''
|
|
299
|
+
if (CLUTTER_EXTENSIONS.has(ext)) {
|
|
300
|
+
// .env.example and .env.test are OK (templates)
|
|
301
|
+
if (name.endsWith('.example') || name.endsWith('.test')) continue
|
|
302
|
+
clutterFiles.push(rel)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
result.details.clutterFiles = clutterFiles.slice(0, 20)
|
|
308
|
+
result.details.totalClutter = clutterFiles.length
|
|
309
|
+
|
|
310
|
+
// --- Check 5: Root clutter (files & directories) ---
|
|
311
|
+
const rootClutter = []
|
|
312
|
+
|
|
313
|
+
let rootEntries
|
|
314
|
+
try { rootEntries = readdirSync(projectPath, { withFileTypes: true }) } catch { rootEntries = [] }
|
|
315
|
+
|
|
316
|
+
for (const entry of rootEntries) {
|
|
317
|
+
const name = entry.name
|
|
318
|
+
const nameLower = name.toLowerCase()
|
|
319
|
+
|
|
320
|
+
if (entry.isDirectory()) {
|
|
321
|
+
// Check for clutter directories in root (skip if already gitignored)
|
|
322
|
+
if (ROOT_CLUTTER_DIRS.has(nameLower) && !gitignoredDirs.has(name) && !gitignoredDirs.has(nameLower)) {
|
|
323
|
+
rootClutter.push(name + '/')
|
|
324
|
+
}
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!entry.isFile()) continue
|
|
329
|
+
|
|
330
|
+
// Skip dotfiles that are standard config (.gitignore, .eslintrc, .env.example, etc.)
|
|
331
|
+
if (name.startsWith('.')) continue
|
|
332
|
+
|
|
333
|
+
// Skip known allowed root files
|
|
334
|
+
if (ALLOWED_ROOT_FILES.has(nameLower)) continue
|
|
335
|
+
|
|
336
|
+
// Skip package manifests, lockfiles, configs (handled by other checks or standard)
|
|
337
|
+
if (nameLower.endsWith('.json') || nameLower.endsWith('.mjs') || nameLower.endsWith('.cjs')) continue
|
|
338
|
+
if (nameLower.endsWith('.toml') || nameLower.endsWith('.lock')) continue
|
|
339
|
+
if (nameLower.endsWith('.md')) continue // Handled by check 1
|
|
340
|
+
if (nameLower.endsWith('.sh')) continue // Handled by check 2
|
|
341
|
+
if (nameLower.endsWith('.yml') || nameLower.endsWith('.yaml')) continue // Handled by check 3
|
|
342
|
+
if (nameLower.endsWith('.ts') || nameLower.endsWith('.js')) continue // Config files like vitest.setup.ts
|
|
343
|
+
|
|
344
|
+
// Check for clutter extensions
|
|
345
|
+
const ext = nameLower.includes('.') ? '.' + nameLower.split('.').pop() : ''
|
|
346
|
+
if (ROOT_CLUTTER_EXTENSIONS.has(ext)) {
|
|
347
|
+
rootClutter.push(name)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
result.details.rootClutter = rootClutter.slice(0, 20)
|
|
352
|
+
result.details.totalRootClutter = rootClutter.length
|
|
353
|
+
|
|
354
|
+
// --- Check 6: Nested docs/ directories ---
|
|
355
|
+
const nestedDocs = findNestedDocsDirs(projectPath, projectPath)
|
|
356
|
+
const nestedDocsRel = nestedDocs.map(d => relative(projectPath, d))
|
|
357
|
+
|
|
358
|
+
result.details.nestedDocsDirs = nestedDocsRel
|
|
359
|
+
result.details.totalNestedDocs = nestedDocsRel.length
|
|
360
|
+
|
|
361
|
+
// --- Scoring ---
|
|
362
|
+
let score = 6
|
|
363
|
+
|
|
364
|
+
if (strayMd.length > 0) score -= 1
|
|
365
|
+
if (strayScripts.length > 0) score -= 1
|
|
366
|
+
if (strayConfigs.length > 0) score -= 1
|
|
367
|
+
if (clutterFiles.length > 0) score -= 1
|
|
368
|
+
if (rootClutter.length > 0) score -= 1
|
|
369
|
+
if (nestedDocsRel.length > 0) score -= 1
|
|
370
|
+
|
|
371
|
+
result.score = score
|
|
372
|
+
|
|
373
|
+
if (score === 6) {
|
|
374
|
+
result.status = 'ok'
|
|
375
|
+
result.details.message = 'Clean repo: docs in /docs, scripts in /scripts, root & code dirs clean'
|
|
376
|
+
} else {
|
|
377
|
+
result.status = score >= 5 ? 'warning' : 'error'
|
|
378
|
+
const issues = []
|
|
379
|
+
if (strayMd.length > 0) issues.push(`${strayMd.length} stray .md`)
|
|
380
|
+
if (strayScripts.length > 0) issues.push(`${strayScripts.length} stray .sh`)
|
|
381
|
+
if (strayConfigs.length > 0) issues.push(`${strayConfigs.length} stray .yml`)
|
|
382
|
+
if (clutterFiles.length > 0) issues.push(`${clutterFiles.length} clutter in code dirs`)
|
|
383
|
+
if (rootClutter.length > 0) issues.push(`${rootClutter.length} root clutter`)
|
|
384
|
+
if (nestedDocsRel.length > 0) issues.push(`${nestedDocsRel.length} nested docs/`)
|
|
385
|
+
result.details.message = issues.join(', ')
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return result
|
|
389
|
+
}
|
|
@@ -24,3 +24,4 @@ export { check as checkStellaIntegration } from './stella-integration.js'
|
|
|
24
24
|
export { check as checkClaudeMd } from './claude-md.js'
|
|
25
25
|
export { check as checkDopplerCompliance } from './doppler-compliance.js'
|
|
26
26
|
export { check as checkInfrastructureYml } from './infrastructure-yml.js'
|
|
27
|
+
export { check as checkFileOrganization } from './file-organization.js'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Health Check: @
|
|
2
|
+
* Health Check: @soulbatical/tetra-dev-toolkit Installation
|
|
3
3
|
*
|
|
4
4
|
* Checks if the quality toolkit is installed and CLI commands available.
|
|
5
5
|
* Score: 0 = not installed, 1 = installed, 2 = all commands available
|
|
@@ -15,7 +15,7 @@ export async function check(projectPath, { getCachedCodeQuality } = {}) {
|
|
|
15
15
|
const result = createCheck('quality-toolkit', 2, {
|
|
16
16
|
installed: false,
|
|
17
17
|
version: null,
|
|
18
|
-
commands: { '
|
|
18
|
+
commands: { 'tetra-audit': false, 'tetra-setup': false, 'tetra-dev-token': false }
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
const packageJsonPath = join(projectPath, 'package.json')
|
|
@@ -28,12 +28,12 @@ export async function check(projectPath, { getCachedCodeQuality } = {}) {
|
|
|
28
28
|
try {
|
|
29
29
|
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
30
30
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
31
|
-
const toolkitDep = allDeps['@vca/dev-toolkit'] || allDeps['@vca/quality-toolkit']
|
|
31
|
+
const toolkitDep = allDeps['@soulbatical/tetra-dev-toolkit'] || allDeps['@vca/dev-toolkit'] || allDeps['@vca/quality-toolkit']
|
|
32
32
|
|
|
33
33
|
if (!toolkitDep) {
|
|
34
34
|
result.status = 'warning'
|
|
35
35
|
result.details.message = 'Not installed'
|
|
36
|
-
result.details.installCommand = 'npm install --save-dev /
|
|
36
|
+
result.details.installCommand = 'npm install --save-dev @soulbatical/tetra-dev-toolkit'
|
|
37
37
|
return result
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -42,8 +42,12 @@ export async function check(projectPath, { getCachedCodeQuality } = {}) {
|
|
|
42
42
|
result.score = 1
|
|
43
43
|
|
|
44
44
|
// Get installed version from node_modules
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const lookupPaths = [
|
|
46
|
+
join(projectPath, 'node_modules', '@soulbatical', 'tetra-dev-toolkit', 'package.json'),
|
|
47
|
+
join(projectPath, 'node_modules', '@vca', 'dev-toolkit', 'package.json'),
|
|
48
|
+
join(projectPath, 'node_modules', '@vca', 'quality-toolkit', 'package.json'),
|
|
49
|
+
]
|
|
50
|
+
for (const toolkitPackagePath of lookupPaths) {
|
|
47
51
|
if (existsSync(toolkitPackagePath)) {
|
|
48
52
|
try {
|
|
49
53
|
result.details.version = JSON.parse(readFileSync(toolkitPackagePath, 'utf-8')).version
|
|
@@ -54,9 +58,9 @@ export async function check(projectPath, { getCachedCodeQuality } = {}) {
|
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
// Check CLI commands
|
|
61
|
+
// Check CLI commands (check new tetra-* names, fall back to legacy vca-*)
|
|
58
62
|
const binPath = join(projectPath, 'node_modules', '.bin')
|
|
59
|
-
const commands = ['
|
|
63
|
+
const commands = ['tetra-audit', 'tetra-setup', 'tetra-dev-token']
|
|
60
64
|
for (const cmd of commands) {
|
|
61
65
|
result.details.commands[cmd] = existsSync(join(binPath, cmd))
|
|
62
66
|
}
|
|
@@ -43,7 +43,7 @@ export async function check(projectPath) {
|
|
|
43
43
|
|
|
44
44
|
try {
|
|
45
45
|
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
46
|
-
headers: { 'User-Agent': '
|
|
46
|
+
headers: { 'User-Agent': 'tetra-health-check' },
|
|
47
47
|
signal: AbortSignal.timeout(5000)
|
|
48
48
|
})
|
|
49
49
|
|
|
@@ -20,6 +20,7 @@ import { check as checkStellaIntegration } from './stella-integration.js'
|
|
|
20
20
|
import { check as checkClaudeMd } from './claude-md.js'
|
|
21
21
|
import { check as checkDopplerCompliance } from './doppler-compliance.js'
|
|
22
22
|
import { check as checkInfrastructureYml } from './infrastructure-yml.js'
|
|
23
|
+
import { check as checkFileOrganization } from './file-organization.js'
|
|
23
24
|
import { calculateHealthStatus } from './types.js'
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -48,7 +49,8 @@ export async function scanProjectHealth(projectPath, projectName, options = {})
|
|
|
48
49
|
checkStellaIntegration(projectPath),
|
|
49
50
|
checkClaudeMd(projectPath),
|
|
50
51
|
checkDopplerCompliance(projectPath),
|
|
51
|
-
checkInfrastructureYml(projectPath)
|
|
52
|
+
checkInfrastructureYml(projectPath),
|
|
53
|
+
checkFileOrganization(projectPath)
|
|
52
54
|
])
|
|
53
55
|
|
|
54
56
|
const totalScore = checks.reduce((sum, c) => sum + c.score, 0)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Health Check: @
|
|
2
|
+
* Health Check: @soulbatical/stella Integration
|
|
3
3
|
*
|
|
4
|
-
* Checks for @
|
|
4
|
+
* Checks for @soulbatical/stella dependency and integration level in MCP server.
|
|
5
5
|
* Score: 0 = not installed, 1 = basic, 2 = full integration
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -14,7 +14,7 @@ export async function check(projectPath) {
|
|
|
14
14
|
installed: false, version: null, integrationLevel: 'none', features: [], packageLocation: null
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
// Search for @
|
|
17
|
+
// Search for @soulbatical/stella in package.json files
|
|
18
18
|
const packageLocations = [
|
|
19
19
|
{ path: join(projectPath, 'package.json'), label: 'root' },
|
|
20
20
|
{ path: join(projectPath, 'mcp', 'package.json'), label: 'mcp/' },
|
|
@@ -29,8 +29,8 @@ export async function check(projectPath) {
|
|
|
29
29
|
try {
|
|
30
30
|
const pkg = JSON.parse(readFileSync(loc.path, 'utf-8'))
|
|
31
31
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
32
|
-
if (allDeps['@
|
|
33
|
-
stellaDep = allDeps['@
|
|
32
|
+
if (allDeps['@soulbatical/stella']) {
|
|
33
|
+
stellaDep = allDeps['@soulbatical/stella']
|
|
34
34
|
stellaLocation = loc.label
|
|
35
35
|
break
|
|
36
36
|
}
|
|
@@ -50,8 +50,8 @@ export async function check(projectPath) {
|
|
|
50
50
|
|
|
51
51
|
// Get installed version
|
|
52
52
|
const nmPaths = [
|
|
53
|
-
join(projectPath, 'node_modules', '@
|
|
54
|
-
join(projectPath, stellaLocation || '', 'node_modules', '@
|
|
53
|
+
join(projectPath, 'node_modules', '@soulbatical', 'stella', 'package.json'),
|
|
54
|
+
join(projectPath, stellaLocation || '', 'node_modules', '@soulbatical', 'stella', 'package.json'),
|
|
55
55
|
]
|
|
56
56
|
for (const nmPath of nmPaths) {
|
|
57
57
|
if (!existsSync(nmPath)) continue
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* @typedef {'plugins'|'mcps'|'git'|'tests'|'secrets'|'quality-toolkit'|'naming-conventions'|'rls-audit'|'gitignore'|'repo-visibility'|'vincifox-widget'|'stella-integration'|'claude-md'|'doppler-compliance'|'infrastructure-yml'} HealthCheckType
|
|
8
|
+
* @typedef {'plugins'|'mcps'|'git'|'tests'|'secrets'|'quality-toolkit'|'naming-conventions'|'rls-audit'|'gitignore'|'repo-visibility'|'vincifox-widget'|'stella-integration'|'claude-md'|'doppler-compliance'|'infrastructure-yml'|'file-organization'} HealthCheckType
|
|
9
9
|
*
|
|
10
10
|
* @typedef {'ok'|'warning'|'error'} HealthStatus
|
|
11
11
|
*
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hygiene Check: File Organization
|
|
3
|
+
*
|
|
4
|
+
* Wraps the health check file-organization as a tetra-audit check.
|
|
5
|
+
* Detects stray docs, scripts, clutter in code dirs, and root mess.
|
|
6
|
+
*
|
|
7
|
+
* Severity: high — messy repos ship messy code
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { check as healthCheck } from '../health/file-organization.js'
|
|
11
|
+
|
|
12
|
+
export const meta = {
|
|
13
|
+
id: 'file-organization',
|
|
14
|
+
name: 'File Organization',
|
|
15
|
+
category: 'hygiene',
|
|
16
|
+
severity: 'high',
|
|
17
|
+
description: 'Checks that docs are in /docs, scripts in /scripts, no clutter in code dirs or repo root'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function run(config, projectRoot) {
|
|
21
|
+
const result = {
|
|
22
|
+
passed: true,
|
|
23
|
+
findings: [],
|
|
24
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
|
|
25
|
+
details: {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const health = await healthCheck(projectRoot)
|
|
29
|
+
result.details = health.details
|
|
30
|
+
|
|
31
|
+
// Convert health check results to findings
|
|
32
|
+
if (health.details.totalStrayMd > 0) {
|
|
33
|
+
result.findings.push({
|
|
34
|
+
type: 'Stray documentation files',
|
|
35
|
+
severity: 'high',
|
|
36
|
+
message: `${health.details.totalStrayMd} .md file(s) outside /docs/ — move to docs/ or delete`,
|
|
37
|
+
files: health.details.strayMdFiles
|
|
38
|
+
})
|
|
39
|
+
result.summary.high++
|
|
40
|
+
result.summary.total++
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (health.details.totalStrayScripts > 0) {
|
|
44
|
+
result.findings.push({
|
|
45
|
+
type: 'Stray shell scripts',
|
|
46
|
+
severity: 'medium',
|
|
47
|
+
message: `${health.details.totalStrayScripts} .sh file(s) outside /scripts/ — move to scripts/`,
|
|
48
|
+
files: health.details.strayScripts
|
|
49
|
+
})
|
|
50
|
+
result.summary.medium++
|
|
51
|
+
result.summary.total++
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (health.details.totalStrayConfigs > 0) {
|
|
55
|
+
result.findings.push({
|
|
56
|
+
type: 'Stray config files',
|
|
57
|
+
severity: 'low',
|
|
58
|
+
message: `${health.details.totalStrayConfigs} .yml/.yaml file(s) outside allowed dirs`,
|
|
59
|
+
files: health.details.strayConfigs
|
|
60
|
+
})
|
|
61
|
+
result.summary.low++
|
|
62
|
+
result.summary.total++
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (health.details.totalClutter > 0) {
|
|
66
|
+
// Clutter in code dirs is high — often includes .env secrets
|
|
67
|
+
const hasEnv = health.details.clutterFiles.some(f => f.includes('.env'))
|
|
68
|
+
const severity = hasEnv ? 'critical' : 'high'
|
|
69
|
+
result.findings.push({
|
|
70
|
+
type: 'Clutter in code directories',
|
|
71
|
+
severity,
|
|
72
|
+
message: `${health.details.totalClutter} clutter file(s) in code dirs (${hasEnv ? 'includes .env!' : '.txt, .log, .bak, etc.'})`,
|
|
73
|
+
files: health.details.clutterFiles
|
|
74
|
+
})
|
|
75
|
+
result.summary[severity]++
|
|
76
|
+
result.summary.total++
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (health.details.totalRootClutter > 0) {
|
|
80
|
+
result.findings.push({
|
|
81
|
+
type: 'Root clutter',
|
|
82
|
+
severity: 'high',
|
|
83
|
+
message: `${health.details.totalRootClutter} clutter item(s) in repo root — ${health.details.rootClutter.join(', ')}`,
|
|
84
|
+
files: health.details.rootClutter
|
|
85
|
+
})
|
|
86
|
+
result.summary.high++
|
|
87
|
+
result.summary.total++
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (health.details.totalNestedDocs > 0) {
|
|
91
|
+
result.findings.push({
|
|
92
|
+
type: 'Nested docs/ directories',
|
|
93
|
+
severity: 'medium',
|
|
94
|
+
message: `${health.details.totalNestedDocs} nested docs/ dir(s) — consolidate into root /docs/`,
|
|
95
|
+
files: health.details.nestedDocsDirs
|
|
96
|
+
})
|
|
97
|
+
result.summary.medium++
|
|
98
|
+
result.summary.total++
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Fail if any high+ findings
|
|
102
|
+
result.passed = result.summary.critical === 0 && result.summary.high === 0
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
}
|
package/lib/checks/index.js
CHANGED
|
@@ -13,5 +13,9 @@ export * as huskyHooks from './stability/husky-hooks.js'
|
|
|
13
13
|
export * as ciPipeline from './stability/ci-pipeline.js'
|
|
14
14
|
export * as npmAudit from './stability/npm-audit.js'
|
|
15
15
|
|
|
16
|
+
// Supabase checks
|
|
17
|
+
export * as rlsPolicyAudit from './supabase/rls-policy-audit.js'
|
|
18
|
+
export * as rpcParamMismatch from './supabase/rpc-param-mismatch.js'
|
|
19
|
+
|
|
16
20
|
// Health checks (project ecosystem)
|
|
17
21
|
export * as health from './health/index.js'
|
|
@@ -91,7 +91,7 @@ export async function run(config, projectRoot) {
|
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
name: 'security-audit',
|
|
94
|
-
patterns: ['npm audit', '
|
|
94
|
+
patterns: ['npm audit', 'tetra-audit', 'security-check', 'snyk', 'CodeQL'],
|
|
95
95
|
severity: 'medium'
|
|
96
96
|
}
|
|
97
97
|
]
|
|
@@ -56,7 +56,7 @@ export async function run(config, projectRoot) {
|
|
|
56
56
|
{ name: 'lint', patterns: ['lint', 'eslint'] },
|
|
57
57
|
{ name: 'type-check', patterns: ['tsc', 'typecheck', 'type-check'] },
|
|
58
58
|
{ name: 'test', patterns: ['test', 'jest', 'vitest'] },
|
|
59
|
-
{ name: 'security', patterns: ['security', 'audit', 'vca-'] }
|
|
59
|
+
{ name: 'security', patterns: ['security', 'audit', 'tetra-', 'vca-'] }
|
|
60
60
|
]
|
|
61
61
|
|
|
62
62
|
const missingChecks = []
|