code-warden 3.1.1 → 3.3.1
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 +205 -137
- package/bin/code-warden.js +82 -0
- package/package.json +62 -19
- package/templates/ci/github-actions.yml +83 -66
- package/tools/governance-report.js +302 -0
- package/tools/lib/file-collection.js +75 -72
package/README.md
CHANGED
|
@@ -1,137 +1,205 @@
|
|
|
1
|
-
# code-warden
|
|
2
|
-
|
|
3
|
-
> Portable AI Coding Governance Layer
|
|
4
|
-
|
|
5
|
-
Code-Warden
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
|
17
|
-
|
|
18
|
-
| **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
node install.js
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
1
|
+
# code-warden
|
|
2
|
+
|
|
3
|
+
> Portable AI Coding Governance Layer
|
|
4
|
+
|
|
5
|
+
Code-Warden provides verifiable governance for AI-assisted development.
|
|
6
|
+
It does not just ask agents to follow rules — it adds Scope Gates, Plan Gates,
|
|
7
|
+
local checks, CI enforcement, runtime hooks where supported, and governance
|
|
8
|
+
artifacts that show what was checked before code was accepted.
|
|
9
|
+
|
|
10
|
+
## Four Layers
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="../logo/layers-diagram.png" alt="Code-Warden Four Layers" width="100%" />
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
| Layer | What it does |
|
|
17
|
+
|-------|-------------|
|
|
18
|
+
| **Skill governance** | Scope Gate, Plan Gate, blast-radius checks, patch-first editing, research gates, drift signals, verification evidence |
|
|
19
|
+
| **Local verification** | `warden-lint`, `verify-secrets`, `get-context` — directory-aware, no external deps |
|
|
20
|
+
| **Installer and health** | Cross-app auto-installer, manifest-backed installs, `--doctor`, `--verify-target`, Windsurf adapter |
|
|
21
|
+
| **Hard enforcement** | Claude Code `PreToolUse` hooks — block oversized writes and hardcoded secrets before the file system is touched |
|
|
22
|
+
|
|
23
|
+
## Governance Evidence
|
|
24
|
+
|
|
25
|
+
Generate a machine-readable governance report that can be stored in CI, attached to PRs, or used as audit evidence:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
node tools/governance-report.js . # write .code-warden-report.json + summary
|
|
29
|
+
node tools/governance-report.js . --format=json # JSON to stdout
|
|
30
|
+
node tools/governance-report.js . --format=md # Markdown to stdout
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The report runs all checks in a single pass (file length, secrets, behavioral tests, source integrity) and produces a structured artifact:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"tool": "code-warden",
|
|
38
|
+
"version": "3.2.0",
|
|
39
|
+
"checks": {
|
|
40
|
+
"fileLength": { "status": "pass", "filesScanned": 34, "violations": 0 },
|
|
41
|
+
"secrets": { "status": "pass", "filesScanned": 34, "violations": 0 },
|
|
42
|
+
"behavioralTests": { "status": "pass", "tests": 8, "failures": 0 },
|
|
43
|
+
"installHealth": { "status": "pass" }
|
|
44
|
+
},
|
|
45
|
+
"result": "pass"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
In CI, the Markdown format pipes directly into `$GITHUB_STEP_SUMMARY` for PR-visible evidence:
|
|
50
|
+
|
|
51
|
+
| Check | Result | Details |
|
|
52
|
+
|-------|--------|---------|
|
|
53
|
+
| File length | PASS | 34 files scanned, 0 violations |
|
|
54
|
+
| Hardcoded credentials | PASS | 34 files scanned, 0 violations |
|
|
55
|
+
| Behavioral tests | PASS | 8 tests, 0 failures |
|
|
56
|
+
| Install health | PASS | All source files present |
|
|
57
|
+
|
|
58
|
+
See [`templates/ci/github-actions.yml`](templates/ci/github-actions.yml) for the full CI template with artifact upload.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx code-warden init
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or install globally:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install -g code-warden
|
|
70
|
+
code-warden init
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### CLI commands
|
|
74
|
+
|
|
75
|
+
| Command | Purpose |
|
|
76
|
+
|---------|---------|
|
|
77
|
+
| `code-warden init` | Install to all detected AI runtimes |
|
|
78
|
+
| `code-warden report` | Generate governance report |
|
|
79
|
+
| `code-warden report --format=md` | Markdown output for PR summaries |
|
|
80
|
+
| `code-warden doctor` | Verify source integrity + install health |
|
|
81
|
+
| `code-warden list` | Show detected runtimes |
|
|
82
|
+
| `code-warden hooks claude` | Install Claude Code PreToolUse hooks |
|
|
83
|
+
| `code-warden hooks codex` | Install Codex PreToolUse hooks (partial) |
|
|
84
|
+
| `code-warden uninstall-hooks claude` | Remove Claude Code hooks |
|
|
85
|
+
| `code-warden uninstall-hooks codex` | Remove Codex hooks |
|
|
86
|
+
|
|
87
|
+
### Direct installer commands
|
|
88
|
+
|
|
89
|
+
| Command | Purpose |
|
|
90
|
+
|---------|---------|
|
|
91
|
+
| `node install.js` | Scan, prompt, install to detected apps |
|
|
92
|
+
| `node install.js --all` | Install without prompt |
|
|
93
|
+
| `node install.js --dry-run` | Preview installs, write nothing |
|
|
94
|
+
| `node install.js --list` | Show detected apps and detection method |
|
|
95
|
+
| `node install.js --doctor` | Verify source integrity + per-target install health |
|
|
96
|
+
| `node install.js --target=claude,cursor` | Force specific targets (warns if not detected) |
|
|
97
|
+
| `node install.js --verify-target=claude` | Strict health check — exits nonzero if not installed |
|
|
98
|
+
| `node install.js --hooks=claude` | Install PreToolUse hooks into `~/.claude/settings.json` |
|
|
99
|
+
| `node install.js --uninstall-hooks=claude` | Remove code-warden hook entries from settings |
|
|
100
|
+
|
|
101
|
+
Supported targets: **Claude Code**, **Cursor**, **Warp**, **OpenAI Codex**, **Windsurf**, **Generic Agents**.
|
|
102
|
+
|
|
103
|
+
Each install writes a `.code-warden-install.json` manifest (version, target, format, timestamp).
|
|
104
|
+
|
|
105
|
+
### npm scripts
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm run lint # warden-lint on full project tree
|
|
109
|
+
npm run check-secrets # verify-secrets on full project tree
|
|
110
|
+
npm run report # governance report, writes .code-warden-report.json
|
|
111
|
+
npm run report:json # governance report as JSON to stdout
|
|
112
|
+
npm run report:md # governance report as Markdown to stdout
|
|
113
|
+
npm run install-auto # node install.js
|
|
114
|
+
npm run install-dry-run # node install.js --dry-run
|
|
115
|
+
npm run install-list # node install.js --list
|
|
116
|
+
npm run install-doctor # node install.js --doctor
|
|
117
|
+
npm run test # behavioral tests (8 scanner/hook pass/fail cases)
|
|
118
|
+
npm run ci # lint + secrets + test + doctor
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Usage
|
|
122
|
+
|
|
123
|
+
Load at the start of any coding session. Trigger phrases:
|
|
124
|
+
|
|
125
|
+
- `"load code-warden"` / `"load protocol"`
|
|
126
|
+
- `"begin coding"` / `"new session"` / `"governance check"`
|
|
127
|
+
- `"start a new module"` / `"review this before we write"`
|
|
128
|
+
|
|
129
|
+
The session sequence is enforced before any implementation:
|
|
130
|
+
|
|
131
|
+
<p align="center">
|
|
132
|
+
<img src="../logo/session-flow.png" alt="Code-Warden Session Start Sequence" width="100%" />
|
|
133
|
+
</p>
|
|
134
|
+
|
|
135
|
+
1. Architecture State (Re-injection Rule)
|
|
136
|
+
2. Session Scope (Session Scoping Rule)
|
|
137
|
+
3. Reference Files (Blueprint Rule)
|
|
138
|
+
4. **Scope Gate** — goal, non-goals, files in/out, verify commands, rollback
|
|
139
|
+
5. **Plan Gate** — patch order, blast radius class, post-patch checks
|
|
140
|
+
|
|
141
|
+
See [`examples/governed-session.md`](examples/governed-session.md) for an annotated example.
|
|
142
|
+
|
|
143
|
+
## Optional Claude Code Hooks
|
|
144
|
+
|
|
145
|
+
<p align="center">
|
|
146
|
+
<img src="../logo/hook-flow.png" alt="Code-Warden Hook Enforcement Flow" width="100%" />
|
|
147
|
+
</p>
|
|
148
|
+
|
|
149
|
+
Install hard enforcement that runs at the `PreToolUse` level — before writes happen:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Requires Claude Code target to be installed first
|
|
153
|
+
node install.js --hooks=claude
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Hook | Trigger | Policy |
|
|
157
|
+
|------|---------|--------|
|
|
158
|
+
| `warden-lint-hook.js` | `Write` or `Edit` | Blocks if resulting file exceeds line limit |
|
|
159
|
+
| `warden-secrets-hook.js` | `Write` or `Edit` | Hardcoded credential scanner — blocks if content matches any secret pattern |
|
|
160
|
+
|
|
161
|
+
Both hooks use exec form (`node /path/to/hook.js`) — no shell differences across platforms.
|
|
162
|
+
|
|
163
|
+
Thresholds are read from `codewarden.json` in the installed skill directory.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
node install.js --uninstall-hooks=claude # remove hook entries from settings.json
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Doctor and `--verify-target=claude` validate hook script paths when hooks are registered.
|
|
170
|
+
|
|
171
|
+
## Configuration
|
|
172
|
+
|
|
173
|
+
All thresholds in [`codewarden.json`](codewarden.json):
|
|
174
|
+
|
|
175
|
+
| Setting | Default | What it controls |
|
|
176
|
+
|---------|---------|-----------------|
|
|
177
|
+
| `thresholds.max_file_length` | 400 | Lines before `warden-lint.js` flags a file |
|
|
178
|
+
| `thresholds.pre_flight_trigger_lines` | 150 | Lines before a pre-flight manifest is required |
|
|
179
|
+
| `thresholds.human_checkpoint_files` | 2 | Files touched before `[AWAITING CONFIRMATION]` is required |
|
|
180
|
+
| `safety.exempt_from_blast_radius` | `tests/`, `docs/`, `scripts/` | Paths excluded from rollback-plan rule |
|
|
181
|
+
|
|
182
|
+
See [`CONFIGURE.md`](CONFIGURE.md) for team-size profiles and tuning rationale.
|
|
183
|
+
|
|
184
|
+
## Reference Files
|
|
185
|
+
|
|
186
|
+
| File | Domain |
|
|
187
|
+
|------|--------|
|
|
188
|
+
| `references/planning-gates.md` | Scope Gate and Plan Gate contracts |
|
|
189
|
+
| `references/architecture.md` | Blueprint Rule, Re-injection, State Update |
|
|
190
|
+
| `references/safety.md` | Blast Radius, Patch-First, Zero-Trust, Dependency Freeze |
|
|
191
|
+
| `references/cognition.md` | Think Before Coding, Don't Guess Syntax, Human Checkpoint |
|
|
192
|
+
| `references/cleanup.md` | Tech Debt format, Test Contract, Decision Log |
|
|
193
|
+
| `references/anti-drift.md` | Anchor Check, Session Scoping, Drift Trigger Protocol |
|
|
194
|
+
| `references/operations.md` | Verification, source-control hygiene, dependency control |
|
|
195
|
+
| `references/research-and-fit.md` | Live research gate, stack fit, product-shape guardrails |
|
|
196
|
+
|
|
197
|
+
## Note for contributors
|
|
198
|
+
|
|
199
|
+
> If testing `npx code-warden` from inside the Code-Warden source checkout,
|
|
200
|
+
> npm may prefer the local package context. Test from a separate directory for
|
|
201
|
+
> the same behavior users will see.
|
|
202
|
+
|
|
203
|
+
## Author
|
|
204
|
+
|
|
205
|
+
Justin Davis — MIT License
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const ROOT = path.join(__dirname, '..');
|
|
8
|
+
|
|
9
|
+
const COMMANDS = {
|
|
10
|
+
init: { desc: 'Install Code-Warden to detected AI runtimes', run: ['install.js', '--all'] },
|
|
11
|
+
doctor: { desc: 'Verify source integrity and install health', run: ['install.js', '--doctor'] },
|
|
12
|
+
report: { desc: 'Generate governance report (.code-warden-report.json)', run: ['tools/governance-report.js', '.'] },
|
|
13
|
+
list: { desc: 'Show detected AI runtimes', run: ['install.js', '--list'] },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const HOOK_TARGETS = ['claude', 'codex'];
|
|
17
|
+
|
|
18
|
+
function usage() {
|
|
19
|
+
console.log('Usage: code-warden <command> [options]\n');
|
|
20
|
+
console.log('Commands:');
|
|
21
|
+
for (const [name, { desc }] of Object.entries(COMMANDS)) {
|
|
22
|
+
console.log(` ${name.padEnd(22)} ${desc}`);
|
|
23
|
+
}
|
|
24
|
+
console.log(` ${'hooks <target>'.padEnd(22)} Install PreToolUse hooks (${HOOK_TARGETS.join(', ')})`);
|
|
25
|
+
console.log(` ${'uninstall-hooks <target>'.padEnd(22)} Remove PreToolUse hooks`);
|
|
26
|
+
console.log(`\nExamples:`);
|
|
27
|
+
console.log(` npx code-warden init`);
|
|
28
|
+
console.log(` npx code-warden report`);
|
|
29
|
+
console.log(` npx code-warden report --format=md`);
|
|
30
|
+
console.log(` npx code-warden hooks claude`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function run(scriptPath, args) {
|
|
34
|
+
const result = spawnSync(process.execPath, [path.join(ROOT, scriptPath), ...args], {
|
|
35
|
+
stdio: 'inherit',
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
});
|
|
38
|
+
process.exit(result.status ?? 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
const command = args[0];
|
|
43
|
+
const rest = args.slice(1);
|
|
44
|
+
|
|
45
|
+
if (!command || command === '--help' || command === '-h') {
|
|
46
|
+
usage();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (command === '--version' || command === '-v') {
|
|
51
|
+
const pkg = require(path.join(ROOT, 'package.json'));
|
|
52
|
+
console.log(pkg.version);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (COMMANDS[command]) {
|
|
57
|
+
const entry = COMMANDS[command];
|
|
58
|
+
const scriptArgs = [...entry.run.slice(1), ...rest];
|
|
59
|
+
run(entry.run[0], scriptArgs);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (command === 'hooks') {
|
|
63
|
+
const target = rest[0];
|
|
64
|
+
if (!target || !HOOK_TARGETS.includes(target)) {
|
|
65
|
+
console.error(`Usage: code-warden hooks <${HOOK_TARGETS.join('|')}>`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
run('install.js', [`--hooks=${target}`]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (command === 'uninstall-hooks') {
|
|
72
|
+
const target = rest[0];
|
|
73
|
+
if (!target || !HOOK_TARGETS.includes(target)) {
|
|
74
|
+
console.error(`Usage: code-warden uninstall-hooks <${HOOK_TARGETS.join('|')}>`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
run('install.js', [`--uninstall-hooks=${target}`]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.error(`Unknown command: ${command}\n`);
|
|
81
|
+
usage();
|
|
82
|
+
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,19 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "code-warden",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "SKILL.md",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "code-warden",
|
|
3
|
+
"version": "3.3.1",
|
|
4
|
+
"description": "Verifiable governance for AI-assisted development — checks, hooks, and evidence.",
|
|
5
|
+
"main": "SKILL.md",
|
|
6
|
+
"bin": {
|
|
7
|
+
"code-warden": "bin/code-warden.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"tools/",
|
|
12
|
+
"references/",
|
|
13
|
+
"templates/",
|
|
14
|
+
"examples/",
|
|
15
|
+
"SKILL.md",
|
|
16
|
+
"CONFIGURE.md",
|
|
17
|
+
"DECISIONS.md",
|
|
18
|
+
"README.md",
|
|
19
|
+
"codewarden.json",
|
|
20
|
+
"install.js",
|
|
21
|
+
"install.ps1",
|
|
22
|
+
"install.sh"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"lint": "node tools/warden-lint.js .",
|
|
26
|
+
"check-secrets": "node tools/verify-secrets.js .",
|
|
27
|
+
"get-context": "node tools/get-context.js",
|
|
28
|
+
"report": "node tools/governance-report.js .",
|
|
29
|
+
"report:json": "node tools/governance-report.js . --format=json",
|
|
30
|
+
"report:md": "node tools/governance-report.js . --format=md",
|
|
31
|
+
"install-auto": "node install.js",
|
|
32
|
+
"install-dry-run": "node install.js --dry-run",
|
|
33
|
+
"install-list": "node install.js --list",
|
|
34
|
+
"install-doctor": "node install.js --doctor",
|
|
35
|
+
"test": "node tools/tests/run-tests.js",
|
|
36
|
+
"ci": "npm run lint && npm run check-secrets && npm run test && node install.js --doctor"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"ai",
|
|
43
|
+
"governance",
|
|
44
|
+
"claude",
|
|
45
|
+
"codex",
|
|
46
|
+
"cursor",
|
|
47
|
+
"windsurf",
|
|
48
|
+
"code-review",
|
|
49
|
+
"linter",
|
|
50
|
+
"secrets",
|
|
51
|
+
"ci",
|
|
52
|
+
"hooks"
|
|
53
|
+
],
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "https://github.com/Kodaxadev/Code-Warden.git"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/Kodaxadev/Code-Warden",
|
|
59
|
+
"bugs": "https://github.com/Kodaxadev/Code-Warden/issues",
|
|
60
|
+
"author": "Justin Davis",
|
|
61
|
+
"license": "MIT"
|
|
62
|
+
}
|
|
@@ -1,66 +1,83 @@
|
|
|
1
|
-
# Code-Warden Quality Gate — Project Template
|
|
2
|
-
# https://github.com/Kodaxadev/Code-Warden
|
|
3
|
-
#
|
|
4
|
-
# Copy this file to .github/workflows/code-warden.yml in your project.
|
|
5
|
-
#
|
|
6
|
-
# What it enforces:
|
|
7
|
-
# - File length limits (
|
|
8
|
-
# - Zero-trust secrets (
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- name:
|
|
63
|
-
run:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
1
|
+
# Code-Warden Quality Gate — Project Template
|
|
2
|
+
# https://github.com/Kodaxadev/Code-Warden
|
|
3
|
+
#
|
|
4
|
+
# Copy this file to .github/workflows/code-warden.yml in your project.
|
|
5
|
+
#
|
|
6
|
+
# What it enforces:
|
|
7
|
+
# - File length limits (default 400 lines per codewarden.json)
|
|
8
|
+
# - Zero-trust secrets (hardcoded-credential patterns)
|
|
9
|
+
# - Behavioral tests (scanner and hook pass/fail verification)
|
|
10
|
+
# - Source integrity (required files present)
|
|
11
|
+
#
|
|
12
|
+
# What it produces:
|
|
13
|
+
# - .code-warden-report.json — machine-readable governance artifact
|
|
14
|
+
# - Markdown summary on the workflow run / PR (via GITHUB_STEP_SUMMARY)
|
|
15
|
+
# - Uploaded artifact for audit trail (90-day retention)
|
|
16
|
+
#
|
|
17
|
+
# How code-warden is made available in CI (choose one):
|
|
18
|
+
#
|
|
19
|
+
# Option A — Download from release (recommended, no files to commit)
|
|
20
|
+
# Set CODE_WARDEN_VERSION below to pin a specific release.
|
|
21
|
+
# The "Install Code-Warden" step downloads and extracts automatically.
|
|
22
|
+
#
|
|
23
|
+
# Option B — Commit to your repo
|
|
24
|
+
# Run: node /path/to/code-warden/install.js --target=claude
|
|
25
|
+
# Add .claude/skills/code-warden/ to git tracking.
|
|
26
|
+
# Set CODE_WARDEN_PATH: .claude/skills/code-warden
|
|
27
|
+
# Remove the "Install Code-Warden" step.
|
|
28
|
+
#
|
|
29
|
+
# Customise thresholds in codewarden.json after install:
|
|
30
|
+
# max_file_length (default 400 lines)
|
|
31
|
+
# pre_flight_trigger_lines (default 150 lines)
|
|
32
|
+
# human_checkpoint_files (default 2 files)
|
|
33
|
+
|
|
34
|
+
name: Code-Warden Quality Gate
|
|
35
|
+
|
|
36
|
+
on:
|
|
37
|
+
push:
|
|
38
|
+
branches: [main, master]
|
|
39
|
+
pull_request:
|
|
40
|
+
branches: [main, master]
|
|
41
|
+
|
|
42
|
+
env:
|
|
43
|
+
CODE_WARDEN_VERSION: v3.2.0
|
|
44
|
+
CODE_WARDEN_PATH: .code-warden-ci
|
|
45
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
46
|
+
|
|
47
|
+
jobs:
|
|
48
|
+
code-warden:
|
|
49
|
+
name: Code-Warden Quality Gate
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
|
|
52
|
+
steps:
|
|
53
|
+
- name: Checkout
|
|
54
|
+
uses: actions/checkout@v4
|
|
55
|
+
|
|
56
|
+
- name: Setup Node.js
|
|
57
|
+
uses: actions/setup-node@v4
|
|
58
|
+
with:
|
|
59
|
+
node-version: '24'
|
|
60
|
+
|
|
61
|
+
# Option A: download from GitHub release (remove if using Option B)
|
|
62
|
+
- name: Install Code-Warden
|
|
63
|
+
run: |
|
|
64
|
+
curl -fsSL -o cw.zip \
|
|
65
|
+
"https://github.com/Kodaxadev/Code-Warden/releases/download/${{ env.CODE_WARDEN_VERSION }}/code-warden-${{ env.CODE_WARDEN_VERSION }}.zip"
|
|
66
|
+
mkdir -p ${{ env.CODE_WARDEN_PATH }}
|
|
67
|
+
unzip -q cw.zip -d ${{ env.CODE_WARDEN_PATH }}
|
|
68
|
+
|
|
69
|
+
- name: Governance report
|
|
70
|
+
run: node ${{ env.CODE_WARDEN_PATH }}/tools/governance-report.js .
|
|
71
|
+
|
|
72
|
+
- name: Publish governance summary
|
|
73
|
+
if: always()
|
|
74
|
+
run: node ${{ env.CODE_WARDEN_PATH }}/tools/governance-report.js . --format=md >> $GITHUB_STEP_SUMMARY
|
|
75
|
+
|
|
76
|
+
- name: Upload governance artifact
|
|
77
|
+
if: always()
|
|
78
|
+
uses: actions/upload-artifact@v4
|
|
79
|
+
with:
|
|
80
|
+
name: code-warden-report
|
|
81
|
+
path: .code-warden-report.json
|
|
82
|
+
if-no-files-found: ignore
|
|
83
|
+
retention-days: 90
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const { countLines } = require('./lib/line-count');
|
|
9
|
+
const { collectFiles } = require('./lib/file-collection');
|
|
10
|
+
const { scanForSecrets } = require('./lib/secret-patterns');
|
|
11
|
+
const { loadConfig } = require('./lib/config');
|
|
12
|
+
|
|
13
|
+
const ROOT = path.join(__dirname, '..');
|
|
14
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
15
|
+
const VERSION = PKG.version;
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// CLI
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const args = argv.slice(2);
|
|
23
|
+
const formatArg = args.find(a => a.startsWith('--format='));
|
|
24
|
+
const format = formatArg ? formatArg.split('=')[1] : null;
|
|
25
|
+
const scanPath = args.find(a => !a.startsWith('--')) || '.';
|
|
26
|
+
return { format, scanPath };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Git metadata
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function gitInfo() {
|
|
34
|
+
const run = (gitArgs) => {
|
|
35
|
+
const r = spawnSync('git', gitArgs, { encoding: 'utf8', timeout: 5000 });
|
|
36
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
branch: run(['rev-parse', '--abbrev-ref', 'HEAD']),
|
|
40
|
+
commit: run(['rev-parse', '--short', 'HEAD']),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// File length + secrets (single pass over all files)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
function runScans(scanPath) {
|
|
49
|
+
const { maxFileLength } = loadConfig();
|
|
50
|
+
const resolved = path.resolve(scanPath);
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(resolved)) {
|
|
53
|
+
console.error(`[CodeWarden] Error: scan path not found: ${scanPath}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const files = [];
|
|
58
|
+
if (fs.statSync(resolved).isDirectory()) {
|
|
59
|
+
collectFiles(resolved, files);
|
|
60
|
+
} else {
|
|
61
|
+
files.push(resolved);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const lengthViolations = [];
|
|
65
|
+
const secretViolations = [];
|
|
66
|
+
|
|
67
|
+
for (const f of files) {
|
|
68
|
+
let content;
|
|
69
|
+
try { content = fs.readFileSync(f, 'utf8'); } catch { continue; }
|
|
70
|
+
|
|
71
|
+
const rel = path.relative(resolved, f);
|
|
72
|
+
|
|
73
|
+
const lineCount = countLines(content);
|
|
74
|
+
if (lineCount > maxFileLength) {
|
|
75
|
+
lengthViolations.push({ file: rel, lines: lineCount, limit: maxFileLength });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const hit = scanForSecrets(content);
|
|
79
|
+
if (hit) {
|
|
80
|
+
secretViolations.push({ file: rel, pattern: hit.label });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
fileLength: {
|
|
86
|
+
status: lengthViolations.length === 0 ? 'pass' : 'fail',
|
|
87
|
+
filesScanned: files.length,
|
|
88
|
+
violations: lengthViolations.length,
|
|
89
|
+
details: lengthViolations.length > 0 ? lengthViolations : undefined,
|
|
90
|
+
},
|
|
91
|
+
secrets: {
|
|
92
|
+
status: secretViolations.length === 0 ? 'pass' : 'fail',
|
|
93
|
+
filesScanned: files.length,
|
|
94
|
+
violations: secretViolations.length,
|
|
95
|
+
details: secretViolations.length > 0 ? secretViolations : undefined,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Behavioral tests
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
function checkTests() {
|
|
105
|
+
const testScript = path.join(__dirname, 'tests', 'run-tests.js');
|
|
106
|
+
if (!fs.existsSync(testScript)) {
|
|
107
|
+
return { status: 'skip', tests: 0, failures: 0 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const r = spawnSync(process.execPath, [testScript], {
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
timeout: 30000,
|
|
113
|
+
cwd: ROOT,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
117
|
+
const passMatch = out.match(/pass\s+(\d+)/);
|
|
118
|
+
const failMatch = out.match(/fail\s+(\d+)/);
|
|
119
|
+
|
|
120
|
+
let passed, failed;
|
|
121
|
+
if (passMatch || failMatch) {
|
|
122
|
+
passed = parseInt(passMatch?.[1] || '0', 10);
|
|
123
|
+
failed = parseInt(failMatch?.[1] || '0', 10);
|
|
124
|
+
} else {
|
|
125
|
+
passed = (out.match(/^(?:ok \d+|✔)/gm) || []).length;
|
|
126
|
+
failed = (out.match(/^(?:not ok \d+|✖)/gm) || []).length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
status: r.status === 0 ? 'pass' : 'fail',
|
|
131
|
+
tests: passed + failed,
|
|
132
|
+
failures: failed,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Source integrity
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
function checkInstallHealth() {
|
|
141
|
+
const required = [
|
|
142
|
+
'SKILL.md',
|
|
143
|
+
'references',
|
|
144
|
+
'tools/warden-lint.js',
|
|
145
|
+
'tools/verify-secrets.js',
|
|
146
|
+
'tools/get-context.js',
|
|
147
|
+
];
|
|
148
|
+
const missing = required.filter(f => !fs.existsSync(path.join(ROOT, f)));
|
|
149
|
+
return {
|
|
150
|
+
status: missing.length === 0 ? 'pass' : 'fail',
|
|
151
|
+
missing: missing.length > 0 ? missing : undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Runtime hook detection
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
function checkRuntimeHooks() {
|
|
160
|
+
const home = os.homedir();
|
|
161
|
+
const result = {};
|
|
162
|
+
|
|
163
|
+
const claudeSettings = path.join(home, '.claude', 'settings.json');
|
|
164
|
+
if (fs.existsSync(claudeSettings)) {
|
|
165
|
+
try {
|
|
166
|
+
const s = JSON.parse(fs.readFileSync(claudeSettings, 'utf8'));
|
|
167
|
+
const hooks = (s?.hooks?.PreToolUse || [])
|
|
168
|
+
.flatMap(m => m.hooks || [])
|
|
169
|
+
.filter(h => String(h.description || '').startsWith('code-warden:'));
|
|
170
|
+
if (hooks.length > 0) {
|
|
171
|
+
const valid = hooks.every(h => h.args?.[0] && fs.existsSync(h.args[0]));
|
|
172
|
+
result.claude = valid ? 'registered' : 'registered_broken';
|
|
173
|
+
} else {
|
|
174
|
+
result.claude = 'not_registered';
|
|
175
|
+
}
|
|
176
|
+
} catch { result.claude = 'error'; }
|
|
177
|
+
} else {
|
|
178
|
+
result.claude = 'not_configured';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const codexHooksPath = path.join(home, '.codex', 'hooks.json');
|
|
182
|
+
if (fs.existsSync(codexHooksPath)) {
|
|
183
|
+
try {
|
|
184
|
+
const h = JSON.parse(fs.readFileSync(codexHooksPath, 'utf8'));
|
|
185
|
+
const cw = (h?.PreToolUse || [])
|
|
186
|
+
.filter(e => String(e.description || '').startsWith('code-warden:'));
|
|
187
|
+
if (cw.length > 0) {
|
|
188
|
+
const valid = cw.every(e => e.args?.[0] && fs.existsSync(e.args[0]));
|
|
189
|
+
result.codex = valid ? 'registered' : 'registered_broken';
|
|
190
|
+
} else {
|
|
191
|
+
result.codex = 'not_registered';
|
|
192
|
+
}
|
|
193
|
+
} catch { result.codex = 'error'; }
|
|
194
|
+
} else {
|
|
195
|
+
result.codex = 'not_configured';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Report assembly
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
function generateReport(scanPath) {
|
|
206
|
+
const repo = gitInfo();
|
|
207
|
+
const { fileLength, secrets } = runScans(scanPath);
|
|
208
|
+
const behavioralTests = checkTests();
|
|
209
|
+
const installHealth = checkInstallHealth();
|
|
210
|
+
const runtimeHooks = checkRuntimeHooks();
|
|
211
|
+
|
|
212
|
+
const checks = { fileLength, secrets, behavioralTests, installHealth };
|
|
213
|
+
const result = Object.values(checks).every(c => c.status === 'pass' || c.status === 'skip')
|
|
214
|
+
? 'pass' : 'fail';
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
tool: 'code-warden',
|
|
218
|
+
version: VERSION,
|
|
219
|
+
timestamp: new Date().toISOString(),
|
|
220
|
+
repository: { branch: repo.branch, commit: repo.commit },
|
|
221
|
+
checks,
|
|
222
|
+
governance: {
|
|
223
|
+
scopeGate: 'session_only',
|
|
224
|
+
planGate: 'session_only',
|
|
225
|
+
runtimeHooks,
|
|
226
|
+
},
|
|
227
|
+
result,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Markdown formatter
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
function formatMarkdown(report) {
|
|
236
|
+
const badge = s => s === 'pass' ? 'PASS' : s === 'skip' ? 'SKIP' : 'FAIL';
|
|
237
|
+
const hookLabel = (id) => {
|
|
238
|
+
const s = report.governance.runtimeHooks[id];
|
|
239
|
+
if (s === 'registered') return 'verified';
|
|
240
|
+
if (s === 'registered_broken') return 'broken';
|
|
241
|
+
if (s === 'not_registered') return 'none';
|
|
242
|
+
return 'n/a';
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const healthDetail = report.checks.installHealth.missing
|
|
246
|
+
? 'Missing: ' + report.checks.installHealth.missing.join(', ')
|
|
247
|
+
: 'All source files present';
|
|
248
|
+
|
|
249
|
+
const lines = [
|
|
250
|
+
'## Code-Warden Governance Report',
|
|
251
|
+
'',
|
|
252
|
+
'| Check | Result | Details |',
|
|
253
|
+
'|-------|--------|---------|',
|
|
254
|
+
`| File length | ${badge(report.checks.fileLength.status)} | ${report.checks.fileLength.filesScanned} files scanned, ${report.checks.fileLength.violations} violations |`,
|
|
255
|
+
`| Hardcoded credentials | ${badge(report.checks.secrets.status)} | ${report.checks.secrets.filesScanned} files scanned, ${report.checks.secrets.violations} violations |`,
|
|
256
|
+
`| Behavioral tests | ${badge(report.checks.behavioralTests.status)} | ${report.checks.behavioralTests.tests} tests, ${report.checks.behavioralTests.failures} failures |`,
|
|
257
|
+
`| Install health | ${badge(report.checks.installHealth.status)} | ${healthDetail} |`,
|
|
258
|
+
`| Runtime hooks | — | Claude: ${hookLabel('claude')} / Codex: ${hookLabel('codex')} |`,
|
|
259
|
+
'',
|
|
260
|
+
`**Result:** ${report.result === 'pass' ? 'All governed checks passed.' : 'One or more checks failed.'}`,
|
|
261
|
+
'',
|
|
262
|
+
`> Generated by Code-Warden v${report.version} at ${report.timestamp}`,
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// One-line summary (default mode stdout)
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
function formatSummary(report) {
|
|
273
|
+
const c = report.checks;
|
|
274
|
+
const parts = [
|
|
275
|
+
`lint:${c.fileLength.status}`,
|
|
276
|
+
`secrets:${c.secrets.status}`,
|
|
277
|
+
`tests:${c.behavioralTests.status}`,
|
|
278
|
+
`health:${c.installHealth.status}`,
|
|
279
|
+
];
|
|
280
|
+
return `[CodeWarden] Governance report: ${report.result.toUpperCase()} (${parts.join(', ')})`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Main
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
const { format, scanPath } = parseArgs(process.argv);
|
|
288
|
+
const report = generateReport(scanPath);
|
|
289
|
+
|
|
290
|
+
if (format === 'md') {
|
|
291
|
+
console.log(formatMarkdown(report));
|
|
292
|
+
} else if (format === 'json') {
|
|
293
|
+
console.log(JSON.stringify(report, null, 2));
|
|
294
|
+
} else {
|
|
295
|
+
const json = JSON.stringify(report, null, 2);
|
|
296
|
+
const outPath = path.resolve('.code-warden-report.json');
|
|
297
|
+
fs.writeFileSync(outPath, json, 'utf8');
|
|
298
|
+
console.log(formatSummary(report));
|
|
299
|
+
console.log(`[CodeWarden] Report written to ${outPath}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
process.exit(report.result === 'pass' ? 0 : 1);
|
|
@@ -1,72 +1,75 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* file-collection.js
|
|
6
|
-
* Shared file traversal helpers for warden-lint and verify-secrets CLI tools.
|
|
7
|
-
*
|
|
8
|
-
* Previously each CLI tool duplicated identical SKIP_DIRS, SKIP_EXTS,
|
|
9
|
-
* collectFiles, and expandPaths logic — any change had to be made twice.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
|
|
15
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', 'target', 'dist', '.next']);
|
|
16
|
-
|
|
17
|
-
const SKIP_EXTS = new Set([
|
|
18
|
-
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp',
|
|
19
|
-
'.zip', '.tar', '.gz', '.7z', '.rar',
|
|
20
|
-
'.dll', '.exe', '.bin', '.so', '.dylib',
|
|
21
|
-
'.pdf', '.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
22
|
-
'.mp4', '.mp3', '.wav', '.ogg', '.avi', '.mov',
|
|
23
|
-
'.map', '.lock',
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Recursively collect all scannable files under a directory.
|
|
28
|
-
*
|
|
29
|
-
* @param {string} dir
|
|
30
|
-
* @param {string[]} results - accumulator (mutated)
|
|
31
|
-
*/
|
|
32
|
-
function collectFiles(dir, results) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* file-collection.js
|
|
6
|
+
* Shared file traversal helpers for warden-lint and verify-secrets CLI tools.
|
|
7
|
+
*
|
|
8
|
+
* Previously each CLI tool duplicated identical SKIP_DIRS, SKIP_EXTS,
|
|
9
|
+
* collectFiles, and expandPaths logic — any change had to be made twice.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'target', 'dist', '.next']);
|
|
16
|
+
|
|
17
|
+
const SKIP_EXTS = new Set([
|
|
18
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp',
|
|
19
|
+
'.zip', '.tar', '.gz', '.7z', '.rar',
|
|
20
|
+
'.dll', '.exe', '.bin', '.so', '.dylib',
|
|
21
|
+
'.pdf', '.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
22
|
+
'.mp4', '.mp3', '.wav', '.ogg', '.avi', '.mov',
|
|
23
|
+
'.map', '.lock',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Recursively collect all scannable files under a directory.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} dir
|
|
30
|
+
* @param {string[]} results - accumulator (mutated)
|
|
31
|
+
*/
|
|
32
|
+
function collectFiles(dir, results) {
|
|
33
|
+
let entries;
|
|
34
|
+
try { entries = fs.readdirSync(dir); } catch { return; }
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
37
|
+
const full = path.join(dir, entry);
|
|
38
|
+
let stat;
|
|
39
|
+
try { stat = fs.statSync(full); } catch { continue; }
|
|
40
|
+
if (stat.isDirectory()) {
|
|
41
|
+
collectFiles(full, results);
|
|
42
|
+
} else if (!SKIP_EXTS.has(path.extname(entry).toLowerCase())) {
|
|
43
|
+
results.push(full);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Expand a list of CLI path arguments into a flat array of file paths.
|
|
50
|
+
* Directories are walked recursively; individual files are included as-is.
|
|
51
|
+
* Exits with usage error if no args provided or a path is not found.
|
|
52
|
+
*
|
|
53
|
+
* @param {string[]} args - process.argv slice
|
|
54
|
+
* @param {string} toolName - used in the usage message
|
|
55
|
+
* @returns {string[]}
|
|
56
|
+
*/
|
|
57
|
+
function expandPaths(args, toolName) {
|
|
58
|
+
if (args.length === 0) {
|
|
59
|
+
console.log(`Usage: ${toolName} <file|dir> [file|dir] ...`);
|
|
60
|
+
console.log(` node tools/${toolName} . # scan entire project`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const files = [];
|
|
64
|
+
for (const arg of args) {
|
|
65
|
+
if (!fs.existsSync(arg)) {
|
|
66
|
+
console.error(`Error: path not found: ${arg}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (fs.statSync(arg).isDirectory()) collectFiles(arg, files);
|
|
70
|
+
else files.push(arg);
|
|
71
|
+
}
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { collectFiles, expandPaths, SKIP_DIRS, SKIP_EXTS };
|