@verekia/warden 0.0.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/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @verekia/warden
2
+
3
+ Linter that checks repositories share consistent configs and tool versions.
4
+
5
+ Two modes:
6
+
7
+ - **Cross-project** — one warden checkout sits next to several sibling
8
+ repos and lints them all in one pass.
9
+ - **Library** — a repo installs `@verekia/warden` as a devDep and lints
10
+ itself.
11
+
12
+ ## Library mode
13
+
14
+ ```sh
15
+ bun add -D @verekia/warden
16
+ ```
17
+
18
+ Add a `warden` script and wire it into your `all` script:
19
+
20
+ ```jsonc
21
+ {
22
+ "scripts": {
23
+ "warden": "warden",
24
+ "all": "bun run format:check && bun run lint && bun run typecheck && bun run warden",
25
+ },
26
+ "devDependencies": {
27
+ "@verekia/warden": "0.0.0",
28
+ },
29
+ "warden": {
30
+ "checks": {
31
+ "configFilesPresent": {
32
+ "enabled": true,
33
+ "files": [".oxfmtrc.json", ".oxlintrc.json"],
34
+ },
35
+ },
36
+ },
37
+ }
38
+ ```
39
+
40
+ Omit `projects` — warden defaults to checking the cwd.
41
+
42
+ ## Cross-project mode
43
+
44
+ Layout: warden lives next to the repos it checks.
45
+
46
+ ```
47
+ <parent>/
48
+ warden/
49
+ vct7/
50
+ verekia.com/
51
+ ```
52
+
53
+ `package.json` (in the warden checkout) declares projects and check
54
+ options under the `"warden"` key:
55
+
56
+ ```jsonc
57
+ {
58
+ "warden": {
59
+ "projects": ["../vct7", "../verekia.com"],
60
+ "checks": {
61
+ /* … */
62
+ },
63
+ },
64
+ }
65
+ ```
66
+
67
+ Run:
68
+
69
+ ```sh
70
+ bun install
71
+ bun run warden # check every project in the list
72
+ bun run warden vct7 verekia.com # check only the named subset
73
+ ```
74
+
75
+ Positional arguments scope the run — useful in cloud environments where
76
+ only some sibling repos are attached.
77
+
78
+ ## Available checks
79
+
80
+ See [`CLAUDE.md`](./CLAUDE.md) for the full list and the failure → fix
81
+ mapping. Each check has its own block under `"warden".checks` in
82
+ `package.json` and toggles independently.
83
+
84
+ ## Adding a check
85
+
86
+ 1. Create `src/checks/<name>.ts` exporting a function that takes
87
+ `(projects, options)` and returns a `CheckResult`.
88
+ 2. Add its options shape to `WardenConfig['checks']` in `src/types.ts`.
89
+ 3. Wire it into `src/index.ts`.
package/package.json ADDED
@@ -0,0 +1,108 @@
1
+ {
2
+ "name": "@verekia/warden",
3
+ "version": "0.0.0",
4
+ "description": "Linter that checks repositories share consistent configs and tool versions.",
5
+ "bin": {
6
+ "warden": "./src/index.ts"
7
+ },
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "type": "module",
12
+ "scripts": {
13
+ "warden": "bun run src/index.ts",
14
+ "format": "oxfmt .",
15
+ "format:check": "oxfmt --check .",
16
+ "lint": "oxlint .",
17
+ "lint:fix": "oxlint --fix .",
18
+ "typecheck": "tsc --noEmit",
19
+ "all": "bun run format:check && bun run lint && bun run typecheck"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "^1.3.0",
23
+ "oxfmt": "0.48.0",
24
+ "oxlint": "1.63.0",
25
+ "typescript": "^5.6.0"
26
+ },
27
+ "warden": {
28
+ "projects": [
29
+ "../vct7",
30
+ "../verekia.com",
31
+ "../bunext",
32
+ "../pixelize",
33
+ "../gizmo",
34
+ "../gputex",
35
+ "../gradient-normal-textures",
36
+ "../nanothree",
37
+ "../svg-to-tsl"
38
+ ],
39
+ "checks": {
40
+ "configFilesPresent": {
41
+ "enabled": true,
42
+ "files": [
43
+ ".oxfmtrc.json",
44
+ ".oxlintrc.json",
45
+ ".github/workflows/ci.yml"
46
+ ]
47
+ },
48
+ "matchingDependencyVersions": {
49
+ "enabled": true,
50
+ "packages": [
51
+ "oxfmt",
52
+ "oxlint"
53
+ ]
54
+ },
55
+ "exactDependencyVersions": {
56
+ "enabled": true,
57
+ "packages": {
58
+ "oxfmt": "0.48.0",
59
+ "oxlint": "1.63.0"
60
+ }
61
+ },
62
+ "requiredScripts": {
63
+ "enabled": true,
64
+ "scripts": {
65
+ "format": "oxfmt .",
66
+ "format:check": "oxfmt --check .",
67
+ "lint": "oxlint .",
68
+ "lint:fix": "oxlint --fix ."
69
+ }
70
+ },
71
+ "typecheckScript": {
72
+ "enabled": true,
73
+ "modes": {
74
+ "nanothree": "bun-filter-all"
75
+ }
76
+ },
77
+ "testScriptConsistency": {
78
+ "enabled": true,
79
+ "testFilePattern": "**/*.test.{ts,tsx,js,jsx}",
80
+ "testScript": "bun test",
81
+ "allWithoutTests": "bun run format:check && bun run lint && bun run typecheck && bun run warden",
82
+ "allWithTests": "bun run format:check && bun run lint && bun run typecheck && bun run warden && bun run test"
83
+ },
84
+ "tailwindOxfmtConfig": {
85
+ "enabled": true
86
+ },
87
+ "portlessNextDev": {
88
+ "enabled": true,
89
+ "version": "0.13.0"
90
+ },
91
+ "nextConfig": {
92
+ "enabled": true,
93
+ "options": {
94
+ "reactStrictMode": true,
95
+ "reactCompiler": true,
96
+ "output": "export"
97
+ },
98
+ "devDependencies": {
99
+ "babel-plugin-react-compiler": "1.0.0"
100
+ }
101
+ },
102
+ "wardenScript": {
103
+ "enabled": true,
104
+ "package": "@verekia/warden"
105
+ }
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,21 @@
1
+ import type { CheckResult, Project, WardenConfig } from '../types.ts'
2
+
3
+ export const configFilesPresent = async (
4
+ projects: Project[],
5
+ options: NonNullable<WardenConfig['checks']['configFilesPresent']>,
6
+ ): Promise<CheckResult> => {
7
+ const messages: string[] = []
8
+ let passed = true
9
+
10
+ for (const project of projects) {
11
+ for (const file of options.files) {
12
+ const exists = await Bun.file(`${project.path}/${file}`).exists()
13
+ if (!exists) {
14
+ passed = false
15
+ messages.push(`${project.name}: missing ${file}`)
16
+ }
17
+ }
18
+ }
19
+
20
+ return { name: 'configFilesPresent', passed, messages }
21
+ }
@@ -0,0 +1,43 @@
1
+ import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
2
+
3
+ const findVersion = (pkg: PackageJson, name: string): string | undefined =>
4
+ pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? pkg.peerDependencies?.[name]
5
+
6
+ const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/
7
+
8
+ export const exactDependencyVersions = (
9
+ projects: Project[],
10
+ options: NonNullable<WardenConfig['checks']['exactDependencyVersions']>,
11
+ ): CheckResult => {
12
+ const messages: string[] = []
13
+ let passed = true
14
+
15
+ for (const project of projects) {
16
+ if (!project.packageJson) {
17
+ passed = false
18
+ messages.push(`${project.name}: no package.json`)
19
+ continue
20
+ }
21
+ for (const [name, expected] of Object.entries(options.packages)) {
22
+ const declared = findVersion(project.packageJson, name)
23
+ if (declared === undefined) {
24
+ passed = false
25
+ messages.push(`${project.name}: ${name} not declared (expected ${expected})`)
26
+ continue
27
+ }
28
+ if (!EXACT_SEMVER.test(declared)) {
29
+ passed = false
30
+ messages.push(
31
+ `${project.name}: ${name} ${JSON.stringify(declared)} is not an exact version — use "${expected}"`,
32
+ )
33
+ continue
34
+ }
35
+ if (declared !== expected) {
36
+ passed = false
37
+ messages.push(`${project.name}: ${name} ${declared} does not match expected ${expected}`)
38
+ }
39
+ }
40
+ }
41
+
42
+ return { name: 'exactDependencyVersions', passed, messages }
43
+ }
@@ -0,0 +1,43 @@
1
+ import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
2
+
3
+ const findVersion = (pkg: PackageJson, name: string): string | undefined =>
4
+ pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? pkg.peerDependencies?.[name]
5
+
6
+ export const matchingDependencyVersions = (
7
+ projects: Project[],
8
+ options: NonNullable<WardenConfig['checks']['matchingDependencyVersions']>,
9
+ ): CheckResult => {
10
+ const messages: string[] = []
11
+ let passed = true
12
+
13
+ for (const packageName of options.packages) {
14
+ const versionToProjects = new Map<string, string[]>()
15
+ const missing: string[] = []
16
+
17
+ for (const project of projects) {
18
+ const version = project.packageJson ? findVersion(project.packageJson, packageName) : undefined
19
+ if (version === undefined) {
20
+ missing.push(project.name)
21
+ continue
22
+ }
23
+ const existing = versionToProjects.get(version) ?? []
24
+ existing.push(project.name)
25
+ versionToProjects.set(version, existing)
26
+ }
27
+
28
+ if (missing.length > 0) {
29
+ passed = false
30
+ messages.push(`${packageName}: not declared in ${missing.join(', ')}`)
31
+ }
32
+
33
+ if (versionToProjects.size > 1) {
34
+ passed = false
35
+ const detail = [...versionToProjects.entries()]
36
+ .map(([version, names]) => `${version} (${names.join(', ')})`)
37
+ .join(' vs ')
38
+ messages.push(`${packageName}: version mismatch — ${detail}`)
39
+ }
40
+ }
41
+
42
+ return { name: 'matchingDependencyVersions', passed, messages }
43
+ }
@@ -0,0 +1,135 @@
1
+ import { Glob } from 'bun'
2
+ import { dirname, resolve } from 'node:path'
3
+
4
+ import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
5
+
6
+ const NEXT = 'next'
7
+ const CONFIG_VARIANTS = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs'] as const
8
+
9
+ type WorkspaceField = string[] | { packages?: string[] } | undefined
10
+
11
+ const dependsOnNext = (pkg: PackageJson): boolean =>
12
+ NEXT in (pkg.dependencies ?? {}) || NEXT in (pkg.devDependencies ?? {}) || NEXT in (pkg.peerDependencies ?? {})
13
+
14
+ const workspacePatterns = (pkg: PackageJson | null): string[] => {
15
+ const ws = (pkg as { workspaces?: WorkspaceField } | null)?.workspaces
16
+ if (Array.isArray(ws)) return ws
17
+ if (ws && Array.isArray(ws.packages)) return ws.packages
18
+ return []
19
+ }
20
+
21
+ const collectPackageJsons = async (project: Project): Promise<Array<{ relPath: string; pkg: PackageJson }>> => {
22
+ const results: Array<{ relPath: string; pkg: PackageJson }> = []
23
+ if (project.packageJson) {
24
+ results.push({ relPath: 'package.json', pkg: project.packageJson })
25
+ }
26
+ for (const pattern of workspacePatterns(project.packageJson)) {
27
+ const target = `${pattern}/package.json`
28
+ if (pattern.includes('*')) {
29
+ const glob = new Glob(target)
30
+ for await (const path of glob.scan({ cwd: project.path, onlyFiles: true })) {
31
+ if (path.startsWith('node_modules/')) continue
32
+ const pkgFile = Bun.file(resolve(project.path, path))
33
+ if (await pkgFile.exists()) {
34
+ results.push({ relPath: path, pkg: (await pkgFile.json()) as PackageJson })
35
+ }
36
+ }
37
+ } else {
38
+ const pkgFile = Bun.file(resolve(project.path, target))
39
+ if (await pkgFile.exists()) {
40
+ results.push({ relPath: target, pkg: (await pkgFile.json()) as PackageJson })
41
+ }
42
+ }
43
+ }
44
+ return results
45
+ }
46
+
47
+ const equal = (a: unknown, b: unknown): boolean => {
48
+ if (a === b) return true
49
+ if (a && b && typeof a === 'object' && typeof b === 'object') {
50
+ return JSON.stringify(a) === JSON.stringify(b)
51
+ }
52
+ return false
53
+ }
54
+
55
+ const loadConfigObject = async (absPath: string): Promise<Record<string, unknown>> => {
56
+ const mod = (await import(absPath)) as { default?: unknown }
57
+ let value: unknown = mod.default ?? mod
58
+ if (typeof value === 'function') {
59
+ value = await (value as (...args: unknown[]) => unknown)('phase-development-server', { defaultConfig: {} })
60
+ }
61
+ if (!value || typeof value !== 'object') {
62
+ throw new Error(`default export is not an object (got ${typeof value})`)
63
+ }
64
+ return value as Record<string, unknown>
65
+ }
66
+
67
+ export const nextConfig = async (
68
+ projects: Project[],
69
+ options: NonNullable<WardenConfig['checks']['nextConfig']>,
70
+ ): Promise<CheckResult> => {
71
+ const messages: string[] = []
72
+ let passed = true
73
+
74
+ for (const project of projects) {
75
+ const pkgs = await collectPackageJsons(project)
76
+
77
+ for (const { relPath, pkg } of pkgs) {
78
+ if (!dependsOnNext(pkg)) continue
79
+
80
+ const pkgDir = dirname(relPath)
81
+ const dirLabel = pkgDir === '.' ? '' : `${pkgDir}/`
82
+
83
+ let configRel: string | null = null
84
+ let configAbs: string | null = null
85
+ for (const variant of CONFIG_VARIANTS) {
86
+ const abs = resolve(project.path, pkgDir, variant)
87
+ if (await Bun.file(abs).exists()) {
88
+ configRel = `${dirLabel}${variant}`
89
+ configAbs = abs
90
+ break
91
+ }
92
+ }
93
+
94
+ if (configRel === null || configAbs === null) {
95
+ passed = false
96
+ messages.push(
97
+ `${project.name}/${relPath}: declares "next" but no config file found at ${dirLabel || './'} (expected one of ${CONFIG_VARIANTS.join(', ')})`,
98
+ )
99
+ } else {
100
+ let cfg: Record<string, unknown> | null = null
101
+ try {
102
+ cfg = await loadConfigObject(configAbs)
103
+ } catch (err) {
104
+ passed = false
105
+ messages.push(`${project.name}/${configRel}: failed to load — ${(err as Error).message}`)
106
+ }
107
+
108
+ if (cfg) {
109
+ for (const [key, expected] of Object.entries(options.options)) {
110
+ if (!equal(cfg[key], expected)) {
111
+ passed = false
112
+ const actual = key in cfg ? JSON.stringify(cfg[key]) : 'missing'
113
+ messages.push(
114
+ `${project.name}/${configRel}: "${key}" is ${actual} — expected ${JSON.stringify(expected)}`,
115
+ )
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ for (const [name, expected] of Object.entries(options.devDependencies)) {
122
+ const declared = pkg.devDependencies?.[name]
123
+ if (declared !== expected) {
124
+ const actual = declared === undefined ? 'missing' : JSON.stringify(declared)
125
+ passed = false
126
+ messages.push(
127
+ `${project.name}/${relPath}: declares "next" but devDependencies["${name}"] is ${actual} — expected exact "${expected}" (must be declared in this package.json)`,
128
+ )
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ return { name: 'nextConfig', passed, messages }
135
+ }
@@ -0,0 +1,83 @@
1
+ import { Glob } from 'bun'
2
+ import { resolve } from 'node:path'
3
+
4
+ import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
5
+
6
+ const NEXT_DEV_RE = /\bnext dev\b/
7
+ const PORTLESS_WRAP_RE = /\bportless\s+\S+\s+next dev\b/
8
+
9
+ type WorkspaceField = string[] | { packages?: string[] } | undefined
10
+
11
+ const workspacePatterns = (pkg: PackageJson | null): string[] => {
12
+ const ws = (pkg as { workspaces?: WorkspaceField } | null)?.workspaces
13
+ if (Array.isArray(ws)) return ws
14
+ if (ws && Array.isArray(ws.packages)) return ws.packages
15
+ return []
16
+ }
17
+
18
+ const collectPackageJsons = async (project: Project): Promise<Array<{ relPath: string; pkg: PackageJson }>> => {
19
+ const results: Array<{ relPath: string; pkg: PackageJson }> = []
20
+ if (project.packageJson) {
21
+ results.push({ relPath: 'package.json', pkg: project.packageJson })
22
+ }
23
+ for (const pattern of workspacePatterns(project.packageJson)) {
24
+ const target = `${pattern}/package.json`
25
+ if (pattern.includes('*')) {
26
+ const glob = new Glob(target)
27
+ for await (const path of glob.scan({ cwd: project.path, onlyFiles: true })) {
28
+ if (path.startsWith('node_modules/')) continue
29
+ const pkgFile = Bun.file(resolve(project.path, path))
30
+ if (await pkgFile.exists()) {
31
+ results.push({ relPath: path, pkg: (await pkgFile.json()) as PackageJson })
32
+ }
33
+ }
34
+ } else {
35
+ const pkgFile = Bun.file(resolve(project.path, target))
36
+ if (await pkgFile.exists()) {
37
+ results.push({ relPath: target, pkg: (await pkgFile.json()) as PackageJson })
38
+ }
39
+ }
40
+ }
41
+ return results
42
+ }
43
+
44
+ export const portlessNextDev = async (
45
+ projects: Project[],
46
+ options: NonNullable<WardenConfig['checks']['portlessNextDev']>,
47
+ ): Promise<CheckResult> => {
48
+ const messages: string[] = []
49
+ let passed = true
50
+
51
+ for (const project of projects) {
52
+ const pkgs = await collectPackageJsons(project)
53
+
54
+ for (const { relPath, pkg } of pkgs) {
55
+ const scripts = pkg.scripts ?? {}
56
+ let usesNextDev = false
57
+
58
+ for (const [name, value] of Object.entries(scripts)) {
59
+ if (!NEXT_DEV_RE.test(value)) continue
60
+ usesNextDev = true
61
+ if (!PORTLESS_WRAP_RE.test(value)) {
62
+ passed = false
63
+ messages.push(
64
+ `${project.name}/${relPath}: script "${name}" is ${JSON.stringify(value)} — must be wrapped as "portless <app-name> next dev"`,
65
+ )
66
+ }
67
+ }
68
+
69
+ if (usesNextDev) {
70
+ const declared = pkg.devDependencies?.portless
71
+ if (declared !== options.version) {
72
+ const actual = declared === undefined ? 'missing' : JSON.stringify(declared)
73
+ passed = false
74
+ messages.push(
75
+ `${project.name}/${relPath}: uses "next dev" but devDependencies.portless is ${actual} — expected exact "${options.version}" (must be declared in this package.json)`,
76
+ )
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ return { name: 'portlessNextDev', passed, messages }
83
+ }
@@ -0,0 +1,32 @@
1
+ import type { CheckResult, Project, WardenConfig } from '../types.ts'
2
+
3
+ export const requiredScripts = (
4
+ projects: Project[],
5
+ options: NonNullable<WardenConfig['checks']['requiredScripts']>,
6
+ ): CheckResult => {
7
+ const messages: string[] = []
8
+ let passed = true
9
+
10
+ for (const project of projects) {
11
+ const scripts = project.packageJson?.scripts
12
+ if (!scripts) {
13
+ passed = false
14
+ messages.push(`${project.name}: no "scripts" in package.json`)
15
+ continue
16
+ }
17
+ for (const [name, expected] of Object.entries(options.scripts)) {
18
+ const actual = scripts[name]
19
+ if (actual === undefined) {
20
+ passed = false
21
+ messages.push(`${project.name}: missing script "${name}" (expected ${JSON.stringify(expected)})`)
22
+ } else if (actual !== expected) {
23
+ passed = false
24
+ messages.push(
25
+ `${project.name}: script "${name}" is ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`,
26
+ )
27
+ }
28
+ }
29
+ }
30
+
31
+ return { name: 'requiredScripts', passed, messages }
32
+ }
@@ -0,0 +1,41 @@
1
+ import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
2
+
3
+ const TAILWIND = 'tailwindcss'
4
+ const OXFMT_CONFIG = '.oxfmtrc.json'
5
+ const REQUIRED_KEY = 'sortTailwindcss'
6
+
7
+ const dependsOnTailwind = (pkg: PackageJson): boolean =>
8
+ TAILWIND in (pkg.dependencies ?? {}) ||
9
+ TAILWIND in (pkg.devDependencies ?? {}) ||
10
+ TAILWIND in (pkg.peerDependencies ?? {})
11
+
12
+ export const tailwindOxfmtConfig = async (
13
+ projects: Project[],
14
+ _options: NonNullable<WardenConfig['checks']['tailwindOxfmtConfig']>,
15
+ ): Promise<CheckResult> => {
16
+ const messages: string[] = []
17
+ let passed = true
18
+
19
+ for (const project of projects) {
20
+ const oxfmtFile = Bun.file(`${project.path}/${OXFMT_CONFIG}`)
21
+ if (!(await oxfmtFile.exists())) continue
22
+
23
+ const oxfmt = (await oxfmtFile.json()) as Record<string, unknown>
24
+ const present = REQUIRED_KEY in oxfmt
25
+
26
+ if (present && oxfmt[REQUIRED_KEY] !== true) {
27
+ passed = false
28
+ messages.push(
29
+ `${project.name}: ${OXFMT_CONFIG} "${REQUIRED_KEY}" must be \`true\` (got ${JSON.stringify(oxfmt[REQUIRED_KEY])})`,
30
+ )
31
+ continue
32
+ }
33
+
34
+ if (!present && project.packageJson && dependsOnTailwind(project.packageJson)) {
35
+ passed = false
36
+ messages.push(`${project.name}: depends on ${TAILWIND} but ${OXFMT_CONFIG} is missing "${REQUIRED_KEY}": true`)
37
+ }
38
+ }
39
+
40
+ return { name: 'tailwindOxfmtConfig', passed, messages }
41
+ }
@@ -0,0 +1,49 @@
1
+ import { Glob } from 'bun'
2
+
3
+ import type { CheckResult, Project, WardenConfig } from '../types.ts'
4
+
5
+ const projectHasTests = async (projectPath: string, pattern: string): Promise<boolean> => {
6
+ const glob = new Glob(pattern)
7
+ for await (const path of glob.scan({ cwd: projectPath, onlyFiles: true })) {
8
+ if (path.startsWith('node_modules/')) continue
9
+ return true
10
+ }
11
+ return false
12
+ }
13
+
14
+ export const testScriptConsistency = async (
15
+ projects: Project[],
16
+ options: NonNullable<WardenConfig['checks']['testScriptConsistency']>,
17
+ ): Promise<CheckResult> => {
18
+ const messages: string[] = []
19
+ let passed = true
20
+
21
+ for (const project of projects) {
22
+ const scripts = project.packageJson?.scripts ?? {}
23
+ const hasTests = await projectHasTests(project.path, options.testFilePattern)
24
+
25
+ if (hasTests) {
26
+ if (scripts.test !== options.testScript) {
27
+ passed = false
28
+ messages.push(
29
+ `${project.name}: has test files but "test" script is ${JSON.stringify(scripts.test)}, expected ${JSON.stringify(options.testScript)}`,
30
+ )
31
+ }
32
+ if (scripts.all !== options.allWithTests) {
33
+ passed = false
34
+ messages.push(
35
+ `${project.name}: "all" is ${JSON.stringify(scripts.all)}, expected ${JSON.stringify(options.allWithTests)} (project has tests)`,
36
+ )
37
+ }
38
+ } else {
39
+ if (scripts.all !== options.allWithoutTests) {
40
+ passed = false
41
+ messages.push(
42
+ `${project.name}: "all" is ${JSON.stringify(scripts.all)}, expected ${JSON.stringify(options.allWithoutTests)} (no tests detected)`,
43
+ )
44
+ }
45
+ }
46
+ }
47
+
48
+ return { name: 'testScriptConsistency', passed, messages }
49
+ }
@@ -0,0 +1,52 @@
1
+ import type { CheckResult, Project, WardenConfig } from '../types.ts'
2
+
3
+ const SINGLE = 'tsc --noEmit'
4
+ const SOLUTION = 'tsc -b --noEmit'
5
+ const FILTER_ALL = "bun run --filter '*' typecheck"
6
+
7
+ const readTsconfigReferences = async (projectPath: string): Promise<boolean> => {
8
+ const file = Bun.file(`${projectPath}/tsconfig.json`)
9
+ if (!(await file.exists())) return false
10
+ try {
11
+ const tsconfig = (await file.json()) as { references?: unknown[] }
12
+ return Array.isArray(tsconfig.references) && tsconfig.references.length > 0
13
+ } catch {
14
+ return false
15
+ }
16
+ }
17
+
18
+ export const typecheckScript = async (
19
+ projects: Project[],
20
+ options: NonNullable<WardenConfig['checks']['typecheckScript']>,
21
+ ): Promise<CheckResult> => {
22
+ const messages: string[] = []
23
+ let passed = true
24
+
25
+ for (const project of projects) {
26
+ const scripts = project.packageJson?.scripts
27
+ if (!scripts) {
28
+ passed = false
29
+ messages.push(`${project.name}: no "scripts" in package.json`)
30
+ continue
31
+ }
32
+ const mode = options.modes?.[project.name] ?? 'auto'
33
+ const expected =
34
+ mode === 'bun-filter-all' ? FILTER_ALL : (await readTsconfigReferences(project.path)) ? SOLUTION : SINGLE
35
+ const reason =
36
+ mode === 'bun-filter-all'
37
+ ? "mode 'bun-filter-all'"
38
+ : `root tsconfig ${expected === SOLUTION ? 'has' : 'has no'} project references`
39
+ const actual = scripts.typecheck
40
+ if (actual === undefined) {
41
+ passed = false
42
+ messages.push(`${project.name}: missing script "typecheck" (expected ${JSON.stringify(expected)})`)
43
+ } else if (actual !== expected) {
44
+ passed = false
45
+ messages.push(
46
+ `${project.name}: script "typecheck" is ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)} (${reason})`,
47
+ )
48
+ }
49
+ }
50
+
51
+ return { name: 'typecheckScript', passed, messages }
52
+ }
@@ -0,0 +1,33 @@
1
+ import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
2
+
3
+ const dependsOn = (pkg: PackageJson, name: string): boolean =>
4
+ name in (pkg.dependencies ?? {}) || name in (pkg.devDependencies ?? {}) || name in (pkg.peerDependencies ?? {})
5
+
6
+ export const wardenScript = (
7
+ projects: Project[],
8
+ options: NonNullable<WardenConfig['checks']['wardenScript']>,
9
+ ): CheckResult => {
10
+ const messages: string[] = []
11
+ let passed = true
12
+
13
+ for (const project of projects) {
14
+ const pkg = project.packageJson
15
+ if (!pkg || !dependsOn(pkg, options.package)) continue
16
+
17
+ const scripts = pkg.scripts ?? {}
18
+ if (scripts.warden !== 'warden') {
19
+ passed = false
20
+ messages.push(
21
+ `${project.name}: depends on ${options.package} but "warden" script is ${JSON.stringify(scripts.warden)}, expected "warden"`,
22
+ )
23
+ }
24
+ if (!scripts.all || !scripts.all.includes('bun run warden')) {
25
+ passed = false
26
+ messages.push(
27
+ `${project.name}: depends on ${options.package} but "all" script ${JSON.stringify(scripts.all)} is missing "bun run warden"`,
28
+ )
29
+ }
30
+ }
31
+
32
+ return { name: 'wardenScript', passed, messages }
33
+ }
package/src/config.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { basename, resolve } from 'node:path'
2
+
3
+ import type { PackageJson, Project, WardenConfig } from './types.ts'
4
+
5
+ export const loadConfig = async (baseDir: string): Promise<WardenConfig> => {
6
+ const packageJsonPath = `${baseDir}/package.json`
7
+ const packageJsonFile = Bun.file(packageJsonPath)
8
+ if (!(await packageJsonFile.exists())) {
9
+ throw new Error(`No package.json found at ${packageJsonPath}`)
10
+ }
11
+ const pkg = (await packageJsonFile.json()) as PackageJson & { warden?: WardenConfig }
12
+ if (!pkg.warden) {
13
+ throw new Error(`No "warden" key in ${packageJsonPath}`)
14
+ }
15
+ return pkg.warden
16
+ }
17
+
18
+ export const resolveProjects = async (config: WardenConfig, baseDir: string): Promise<Project[]> => {
19
+ const projectPaths = config.projects && config.projects.length > 0 ? config.projects : ['.']
20
+ return Promise.all(
21
+ projectPaths.map(async projectPath => {
22
+ const path = resolve(baseDir, projectPath)
23
+ const packageJsonFile = Bun.file(`${path}/package.json`)
24
+ const packageJson = (await packageJsonFile.exists()) ? ((await packageJsonFile.json()) as PackageJson) : null
25
+ return { name: basename(path), path, packageJson }
26
+ }),
27
+ )
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync } from 'node:fs'
3
+
4
+ import { configFilesPresent } from './checks/config-files-present.ts'
5
+ import { exactDependencyVersions } from './checks/exact-dependency-versions.ts'
6
+ import { matchingDependencyVersions } from './checks/matching-dependency-versions.ts'
7
+ import { nextConfig } from './checks/next-config.ts'
8
+ import { portlessNextDev } from './checks/portless-next-dev.ts'
9
+ import { requiredScripts } from './checks/required-scripts.ts'
10
+ import { tailwindOxfmtConfig } from './checks/tailwind-oxfmt-config.ts'
11
+ import { testScriptConsistency } from './checks/test-script-consistency.ts'
12
+ import { typecheckScript } from './checks/typecheck-script.ts'
13
+ import { wardenScript } from './checks/warden-script.ts'
14
+ import { loadConfig, resolveProjects } from './config.ts'
15
+
16
+ import type { CheckResult } from './types.ts'
17
+
18
+ const baseDir = process.cwd()
19
+
20
+ const config = await loadConfig(baseDir)
21
+ const allProjects = await resolveProjects(config, baseDir)
22
+
23
+ const filterNames = process.argv.slice(2)
24
+ const knownNames = new Set(allProjects.map(p => p.name))
25
+ const unknown = filterNames.filter(n => !knownNames.has(n))
26
+ if (unknown.length > 0) {
27
+ console.error(
28
+ `Unknown project(s): ${unknown.join(', ')}\nConfigured projects: ${allProjects.map(p => p.name).join(', ')}`,
29
+ )
30
+ process.exit(2)
31
+ }
32
+
33
+ const projects = filterNames.length > 0 ? allProjects.filter(p => filterNames.includes(p.name)) : allProjects
34
+
35
+ const missing = projects.filter(p => !existsSync(p.path))
36
+ if (missing.length > 0) {
37
+ const detail = missing.map(p => ` - ${p.name} (expected at ${p.path})`).join('\n')
38
+ const noun = missing.length === 1 ? 'directory is' : 'directories are'
39
+ console.error(
40
+ `Cannot run warden — the following project ${noun} not on disk:\n${detail}\n\nWarden resolves project paths relative to ${baseDir}. In a cloud env, ensure each attached repo is checked out at the expected path and pass only those names as arguments.`,
41
+ )
42
+ process.exit(2)
43
+ }
44
+
45
+ const scopeNote = filterNames.length > 0 ? ` (filtered from ${allProjects.length} configured)` : ''
46
+ console.log(`warden — checking ${projects.length} project(s)${scopeNote}: ${projects.map(p => p.name).join(', ')}\n`)
47
+
48
+ const results: CheckResult[] = []
49
+
50
+ if (config.checks.configFilesPresent?.enabled) {
51
+ results.push(await configFilesPresent(projects, config.checks.configFilesPresent))
52
+ }
53
+ if (config.checks.matchingDependencyVersions?.enabled) {
54
+ results.push(matchingDependencyVersions(projects, config.checks.matchingDependencyVersions))
55
+ }
56
+ if (config.checks.exactDependencyVersions?.enabled) {
57
+ results.push(exactDependencyVersions(projects, config.checks.exactDependencyVersions))
58
+ }
59
+ if (config.checks.requiredScripts?.enabled) {
60
+ results.push(requiredScripts(projects, config.checks.requiredScripts))
61
+ }
62
+ if (config.checks.typecheckScript?.enabled) {
63
+ results.push(await typecheckScript(projects, config.checks.typecheckScript))
64
+ }
65
+ if (config.checks.testScriptConsistency?.enabled) {
66
+ results.push(await testScriptConsistency(projects, config.checks.testScriptConsistency))
67
+ }
68
+ if (config.checks.tailwindOxfmtConfig?.enabled) {
69
+ results.push(await tailwindOxfmtConfig(projects, config.checks.tailwindOxfmtConfig))
70
+ }
71
+ if (config.checks.portlessNextDev?.enabled) {
72
+ results.push(await portlessNextDev(projects, config.checks.portlessNextDev))
73
+ }
74
+ if (config.checks.nextConfig?.enabled) {
75
+ results.push(await nextConfig(projects, config.checks.nextConfig))
76
+ }
77
+ if (config.checks.wardenScript?.enabled) {
78
+ results.push(wardenScript(projects, config.checks.wardenScript))
79
+ }
80
+
81
+ for (const result of results) {
82
+ const tag = result.passed ? '[PASS]' : '[FAIL]'
83
+ console.log(`${tag} ${result.name}`)
84
+ for (const message of result.messages) {
85
+ console.log(` ${message}`)
86
+ }
87
+ }
88
+
89
+ const failed = results.filter(r => !r.passed).length
90
+ console.log(`\n${results.length - failed}/${results.length} checks passed`)
91
+ process.exit(failed === 0 ? 0 : 1)
package/src/types.ts ADDED
@@ -0,0 +1,46 @@
1
+ export type WardenConfig = {
2
+ projects?: string[]
3
+ checks: {
4
+ configFilesPresent?: { enabled: boolean; files: string[] }
5
+ matchingDependencyVersions?: { enabled: boolean; packages: string[] }
6
+ exactDependencyVersions?: { enabled: boolean; packages: Record<string, string> }
7
+ requiredScripts?: { enabled: boolean; scripts: Record<string, string> }
8
+ typecheckScript?: { enabled: boolean; modes?: Record<string, 'auto' | 'bun-filter-all'> }
9
+ testScriptConsistency?: {
10
+ enabled: boolean
11
+ testFilePattern: string
12
+ testScript: string
13
+ allWithoutTests: string
14
+ allWithTests: string
15
+ }
16
+ tailwindOxfmtConfig?: { enabled: boolean }
17
+ portlessNextDev?: { enabled: boolean; version: string }
18
+ nextConfig?: {
19
+ enabled: boolean
20
+ options: Record<string, unknown>
21
+ devDependencies: Record<string, string>
22
+ }
23
+ wardenScript?: { enabled: boolean; package: string }
24
+ }
25
+ }
26
+
27
+ export type PackageJson = {
28
+ name?: string
29
+ version?: string
30
+ scripts?: Record<string, string>
31
+ dependencies?: Record<string, string>
32
+ devDependencies?: Record<string, string>
33
+ peerDependencies?: Record<string, string>
34
+ }
35
+
36
+ export type Project = {
37
+ name: string
38
+ path: string
39
+ packageJson: PackageJson | null
40
+ }
41
+
42
+ export type CheckResult = {
43
+ name: string
44
+ passed: boolean
45
+ messages: string[]
46
+ }