@ngxtm/devkit 3.0.0 → 3.0.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 +57 -197
- package/hooks/lib/__tests__/ck-config-utils.test.cjs +10 -0
- package/hooks/lib/__tests__/statusline-integration.test.cjs +46 -75
- package/hooks/scout-block/tests/test-monorepo-scenarios.cjs +2 -2
- package/hooks/tests/test-ckignore.cjs +7 -2
- package/package.json +1 -1
- package/hooks/tests/test-modularization-hook.cjs +0 -126
package/README.md
CHANGED
|
@@ -1,236 +1,96 @@
|
|
|
1
|
-
# Devkit
|
|
1
|
+
# Devkit
|
|
2
2
|
|
|
3
|
-
> Unified multi-agent system
|
|
3
|
+
> Unified multi-agent skill system for AI coding assistants
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@ngxtm/devkit)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
+
Supercharge your AI coding assistant with **414+ skills**, **38 agents**, and **57 commands**. Works with Claude Code, Cursor, GitHub Copilot, and Gemini.
|
|
9
|
+
|
|
8
10
|
## Features
|
|
9
11
|
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **9 Hooks** - Session management, privacy, context awareness (claudekit)
|
|
14
|
-
- **6 Output Styles** - Coding levels from ELI5 to God mode (claudekit)
|
|
15
|
-
- **10 Rules** - Coding standards for TypeScript, React, NestJS, etc.
|
|
16
|
-
- **Matrix Skill Discovery** - Intelligent skill injection based on context
|
|
12
|
+
- **Smart Tech Detection** - Automatically detects your project stack and loads relevant skills
|
|
13
|
+
- **Per-Project Installation** - Install only what you need, where you need it
|
|
14
|
+
- **Context-Optimized** - Index-only mode reduces context usage by 99.95%
|
|
17
15
|
- **Auto-Sync** - Daily updates from upstream sources via GitHub Actions
|
|
16
|
+
- **Multi-Tool Support** - Works with Claude, Cursor, Copilot, and Gemini
|
|
18
17
|
|
|
19
|
-
##
|
|
20
|
-
|
|
21
|
-
| Source | What's Included |
|
|
22
|
-
|--------|-----------------|
|
|
23
|
-
| [antigravity-awesome-skills](https://github.com/sickn33/antigravity-awesome-skills) | 256+ skills (primary, frequently updated) |
|
|
24
|
-
| [agent-assistant](https://github.com/hainamchung/agent-assistant) | 310+ skills, Matrix system, 20 agents, 25 commands |
|
|
25
|
-
| [claudekit](https://github.com/anthropics/claudekit) | 45 skills, 18 agents, 32 commands, hooks, output styles |
|
|
26
|
-
| [skill-rule](https://github.com/ngxtm/skill-rule) | Coding standards for 10 frameworks |
|
|
27
|
-
|
|
28
|
-
## Installation
|
|
18
|
+
## Quick Start
|
|
29
19
|
|
|
30
20
|
```bash
|
|
31
21
|
npm install -g @ngxtm/devkit
|
|
32
22
|
devkit install
|
|
33
23
|
```
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| Mode | Command | Skills Size | Description |
|
|
38
|
-
|------|---------|-------------|-------------|
|
|
39
|
-
| **Index-only** (default) | `devkit install` | ~30KB | Only installs skills index, Claude loads skills on-demand |
|
|
40
|
-
| Minimal | `devkit install --minimal` | ~2MB | Installs ~20 core skills |
|
|
41
|
-
| Category | `devkit install -c=react` | varies | Installs specific categories |
|
|
42
|
-
| Full | `devkit install --full` | ~59MB | Installs all 413+ skills (may cause context limit) |
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
# Recommended: Index-only (default)
|
|
46
|
-
devkit install # Best for avoiding context limits
|
|
47
|
-
devkit install claude # Claude Code only
|
|
48
|
-
|
|
49
|
-
# Alternative modes
|
|
50
|
-
devkit install --minimal # ~20 core skills
|
|
51
|
-
devkit install --category=react,ts # Specific categories
|
|
52
|
-
devkit install --full # All skills (large)
|
|
53
|
-
devkit install --interactive # Choose interactively
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### How Index-Only Mode Works
|
|
57
|
-
|
|
58
|
-
Instead of installing 3,500+ files (59MB), devkit installs:
|
|
59
|
-
- `SKILLS_INDEX.md` - 30KB summary of all 411 skills
|
|
60
|
-
- All commands (`/plan`, `/cook`, `/brainstorm`, etc.)
|
|
61
|
-
- All agents (planner, debugger, reviewer, etc.)
|
|
62
|
-
- Hooks, rules, output-styles
|
|
63
|
-
|
|
64
|
-
When Claude needs a specific skill, it reads the index to find it, then loads the full skill on-demand. This reduces context usage by **99.95%**.
|
|
65
|
-
|
|
66
|
-
### Available Categories
|
|
25
|
+
## Installation Modes
|
|
67
26
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
27
|
+
| Mode | Command | Size | Description |
|
|
28
|
+
|------|---------|------|-------------|
|
|
29
|
+
| **Index-only** | `devkit install` | ~30KB | On-demand skill loading (recommended) |
|
|
30
|
+
| Minimal | `devkit install --minimal` | ~2MB | ~20 core skills |
|
|
31
|
+
| Category | `devkit install -c=react,ts` | varies | Specific categories only |
|
|
32
|
+
| Full | `devkit install --full` | ~59MB | All 414+ skills |
|
|
71
33
|
|
|
72
|
-
|
|
73
|
-
|----------|--------|-------------|
|
|
74
|
-
| react | 9 | React, Next.js, Remix |
|
|
75
|
-
| typescript | 4 | TypeScript patterns |
|
|
76
|
-
| node | 6 | Node.js, NestJS, Express |
|
|
77
|
-
| database | 5 | PostgreSQL, MongoDB, Redis |
|
|
78
|
-
| devops | 7 | Docker, K8s, CI/CD, AWS |
|
|
79
|
-
| testing | 6 | Jest, Playwright, Vitest |
|
|
80
|
-
| security | 5 | OWASP, Auth, API security |
|
|
81
|
-
| ai | 6 | AI agents, MCP, prompts |
|
|
82
|
-
| mobile | 5 | React Native, Flutter |
|
|
83
|
-
| frontend | 6 | CSS, Tailwind, a11y |
|
|
84
|
-
| backend | 6 | APIs, microservices |
|
|
85
|
-
| tools | 6 | Git, debugging, docs |
|
|
86
|
-
|
|
87
|
-
## Uninstallation
|
|
34
|
+
## Commands
|
|
88
35
|
|
|
89
36
|
```bash
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
37
|
+
# Planning & Building
|
|
38
|
+
/plan # Create implementation strategy
|
|
39
|
+
/cook # Build feature with full workflow
|
|
40
|
+
/code # Code with workflow
|
|
41
|
+
|
|
42
|
+
# Development
|
|
43
|
+
/fix # Fix bugs
|
|
44
|
+
/test # Run tests
|
|
45
|
+
/review # Code review
|
|
46
|
+
/scout # Explore codebase
|
|
47
|
+
|
|
48
|
+
# Setup
|
|
49
|
+
/bootstrap # Setup new project
|
|
50
|
+
/coding-level # Set output style (eli5 → god)
|
|
51
|
+
/kanban # Manage tasks
|
|
96
52
|
```
|
|
97
53
|
|
|
98
|
-
##
|
|
54
|
+
## Coding Levels
|
|
99
55
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
│ ├── matrix-skills/ # Skill discovery system
|
|
109
|
-
│ └── claudekit/ # Claudekit components
|
|
110
|
-
│ ├── agents/ # 18 agents
|
|
111
|
-
│ └── commands/ # 32 commands
|
|
112
|
-
├── rules/ # Coding standards
|
|
113
|
-
├── hooks/ # 9 hooks
|
|
114
|
-
├── output-styles/ # 6 coding levels
|
|
115
|
-
├── workflows/ # Workflow definitions
|
|
116
|
-
└── settings.json # Claude settings
|
|
117
|
-
```
|
|
56
|
+
| Level | Command | Style |
|
|
57
|
+
|-------|---------|-------|
|
|
58
|
+
| 0 | `/coding-level 0` | ELI5 - Learning friendly |
|
|
59
|
+
| 1 | `/coding-level 1` | Junior - Detailed comments |
|
|
60
|
+
| 2 | `/coding-level 2` | Mid - Balanced |
|
|
61
|
+
| 3 | `/coding-level 3` | Senior - Concise |
|
|
62
|
+
| 4 | `/coding-level 4` | Lead - Architecture focus |
|
|
63
|
+
| 5 | `/coding-level 5` | God - Maximum efficiency |
|
|
118
64
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
- **Cursor**: Skills, agents, commands, rules
|
|
122
|
-
- **GitHub Copilot**: Skills, rules
|
|
123
|
-
- **Gemini/Antigravity**: Skills, rules
|
|
124
|
-
|
|
125
|
-
## Quick Start Commands
|
|
126
|
-
|
|
127
|
-
After installation, use these commands in your AI coding tool:
|
|
128
|
-
|
|
129
|
-
### From agent-assistant
|
|
130
|
-
```
|
|
131
|
-
/cook - Build a feature with full workflow
|
|
132
|
-
/plan - Plan implementation strategy
|
|
133
|
-
/review - Code review
|
|
134
|
-
/test - Run tests
|
|
135
|
-
/fix - Fix bugs
|
|
136
|
-
```
|
|
65
|
+
## Categories
|
|
137
66
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
/bootstrap - Setup new project
|
|
141
|
-
/coding-level - Set output style (eli5/junior/mid/senior/lead/god)
|
|
142
|
-
/code - Code with workflow
|
|
143
|
-
/scout - Explore codebase
|
|
144
|
-
/kanban - Manage tasks
|
|
67
|
+
```bash
|
|
68
|
+
devkit categories # List all categories
|
|
145
69
|
```
|
|
146
70
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
|
152
|
-
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
| `/coding-level 2` | Mid | Balanced, best practices |
|
|
156
|
-
| `/coding-level 3` | Senior | Concise, optimized |
|
|
157
|
-
| `/coding-level 4` | Lead | Architecture focus |
|
|
158
|
-
| `/coding-level 5` | God | Minimal, maximum efficiency |
|
|
159
|
-
|
|
160
|
-
## Hooks (Claude Code only)
|
|
161
|
-
|
|
162
|
-
Claudekit hooks provide:
|
|
163
|
-
- **session-init** - Initialize session context
|
|
164
|
-
- **privacy-block** - Protect sensitive files
|
|
165
|
-
- **scout-block** - Control exploration scope
|
|
166
|
-
- **dev-rules-reminder** - Enforce coding standards
|
|
167
|
-
- **usage-context-awareness** - Smart context management
|
|
168
|
-
- And more...
|
|
169
|
-
|
|
170
|
-
## Auto-Sync
|
|
171
|
-
|
|
172
|
-
Skills and rules are automatically updated via GitHub Actions:
|
|
173
|
-
|
|
174
|
-
- **Skills**: Daily sync from antigravity + agent-assistant
|
|
175
|
-
- **Rules**: Weekly sync from skill-rule
|
|
176
|
-
- **Hooks/Claudekit**: Manual sync (local source)
|
|
177
|
-
|
|
178
|
-
## Configuration
|
|
71
|
+
| Category | Skills | Category | Skills |
|
|
72
|
+
|----------|--------|----------|--------|
|
|
73
|
+
| react | 9 | database | 5 |
|
|
74
|
+
| typescript | 4 | devops | 7 |
|
|
75
|
+
| node | 6 | testing | 6 |
|
|
76
|
+
| security | 5 | ai | 6 |
|
|
77
|
+
| mobile | 5 | frontend | 6 |
|
|
78
|
+
| backend | 6 | tools | 6 |
|
|
179
79
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
```yaml
|
|
183
|
-
skill_sources:
|
|
184
|
-
- name: antigravity-awesome-skills
|
|
185
|
-
repo: sickn33/antigravity-awesome-skills
|
|
186
|
-
enabled: true
|
|
187
|
-
priority: 1
|
|
188
|
-
|
|
189
|
-
- name: agent-assistant
|
|
190
|
-
repo: hainamchung/agent-assistant
|
|
191
|
-
enabled: true
|
|
192
|
-
priority: 2
|
|
193
|
-
|
|
194
|
-
sync:
|
|
195
|
-
merge_strategy: prefer-primary
|
|
196
|
-
schedule: "0 0 * * *"
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
## Development
|
|
80
|
+
## Uninstall
|
|
200
81
|
|
|
201
82
|
```bash
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
cd devkit-assistant
|
|
205
|
-
|
|
206
|
-
# Install dependencies
|
|
207
|
-
npm install
|
|
208
|
-
|
|
209
|
-
# Run initial sync (first time only)
|
|
210
|
-
# Windows:
|
|
211
|
-
initial-sync.bat
|
|
212
|
-
|
|
213
|
-
# Linux/Mac:
|
|
214
|
-
./initial-sync.sh
|
|
215
|
-
|
|
216
|
-
# Test install locally
|
|
217
|
-
node cli/install.js
|
|
83
|
+
devkit uninstall # Remove from all tools
|
|
84
|
+
devkit uninstall claude # Remove from specific tool
|
|
218
85
|
```
|
|
219
86
|
|
|
220
87
|
## Contributing
|
|
221
88
|
|
|
222
89
|
1. Fork the repository
|
|
223
|
-
2. Add
|
|
90
|
+
2. Add skills to `skills/your-skill-name/SKILL.md`
|
|
224
91
|
3. Run `python scripts/update_matrix.py`
|
|
225
92
|
4. Submit a pull request
|
|
226
93
|
|
|
227
94
|
## License
|
|
228
95
|
|
|
229
|
-
MIT
|
|
230
|
-
|
|
231
|
-
## Credits
|
|
232
|
-
|
|
233
|
-
- [antigravity-awesome-skills](https://github.com/sickn33/antigravity-awesome-skills) by sickn33
|
|
234
|
-
- [agent-assistant](https://github.com/hainamchung/agent-assistant) by hainamchung
|
|
235
|
-
- [claudekit](https://github.com/anthropics/claudekit) by Anthropic
|
|
236
|
-
- [skill-rule](https://github.com/ngxtm/skill-rule) by ngxtm
|
|
96
|
+
MIT
|
|
@@ -505,6 +505,11 @@ test('getGitRoot from nested subdir returns correct root', () => {
|
|
|
505
505
|
console.log('\n=== Symlinked directory tests ===\n');
|
|
506
506
|
|
|
507
507
|
test('getGitRoot resolves through symlink to git repo', () => {
|
|
508
|
+
// Skip on Windows - symlinks require elevated privileges
|
|
509
|
+
if (process.platform === 'win32') {
|
|
510
|
+
console.log(' → Skipped: Symlinks require elevated privileges on Windows');
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
508
513
|
const realDir = path.join(os.tmpdir(), 'ck-test-real-' + Date.now());
|
|
509
514
|
const linkDir = path.join(os.tmpdir(), 'ck-test-link-' + Date.now());
|
|
510
515
|
fs.mkdirSync(realDir, { recursive: true });
|
|
@@ -529,6 +534,11 @@ test('getGitRoot resolves through symlink to git repo', () => {
|
|
|
529
534
|
});
|
|
530
535
|
|
|
531
536
|
test('getGitRoot with symlinked subdirectory', () => {
|
|
537
|
+
// Skip on Windows - symlinks require elevated privileges
|
|
538
|
+
if (process.platform === 'win32') {
|
|
539
|
+
console.log(' → Skipped: Symlinks require elevated privileges on Windows');
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
532
542
|
const realDir = path.join(os.tmpdir(), 'ck-test-real-sub-' + Date.now());
|
|
533
543
|
const subDir = path.join(realDir, 'subdir');
|
|
534
544
|
const linkToSub = path.join(os.tmpdir(), 'ck-test-link-sub-' + Date.now());
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Integration Tests for Statusline Main Script
|
|
6
6
|
* Tests the complete statusline.cjs with sample JSON input
|
|
7
|
-
* Run: node
|
|
7
|
+
* Run: node hooks/lib/__tests__/statusline-integration.test.cjs
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
@@ -12,6 +12,12 @@ const path = require('path');
|
|
|
12
12
|
const os = require('os');
|
|
13
13
|
const { execSync } = require('child_process');
|
|
14
14
|
|
|
15
|
+
// Determine the correct statusline.cjs path (at project root)
|
|
16
|
+
const statuslinePath = path.resolve(__dirname, '..', '..', '..', 'statusline.cjs');
|
|
17
|
+
|
|
18
|
+
// Check if running on Windows
|
|
19
|
+
const isWindows = process.platform === 'win32';
|
|
20
|
+
|
|
15
21
|
let passed = 0;
|
|
16
22
|
let failed = 0;
|
|
17
23
|
const failures = [];
|
|
@@ -47,6 +53,29 @@ function assertContains(actual, search, msg = '') {
|
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Cross-platform helper to run statusline with JSON input
|
|
58
|
+
* Uses temp file approach to avoid shell quoting issues on Windows
|
|
59
|
+
*/
|
|
60
|
+
function runStatusline(jsonInput, extraEnv = {}, options = {}) {
|
|
61
|
+
const tmpFile = path.join(os.tmpdir(), `statusline-test-${Date.now()}-${Math.random().toString(36).substring(7)}.json`);
|
|
62
|
+
try {
|
|
63
|
+
fs.writeFileSync(tmpFile, jsonInput);
|
|
64
|
+
const cmd = isWindows
|
|
65
|
+
? `type "${tmpFile}" | node "${statuslinePath}"`
|
|
66
|
+
: `cat "${tmpFile}" | node "${statuslinePath}"`;
|
|
67
|
+
return execSync(cmd, {
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
70
|
+
env: { ...process.env, ...extraEnv },
|
|
71
|
+
shell: true,
|
|
72
|
+
...options
|
|
73
|
+
});
|
|
74
|
+
} finally {
|
|
75
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
console.log('\n═══════════════════════════════════════════════════════');
|
|
51
80
|
console.log('STATUSLINE INTEGRATION TESTS');
|
|
52
81
|
console.log('═══════════════════════════════════════════════════════\n');
|
|
@@ -64,10 +93,7 @@ const minimalInput = JSON.stringify({
|
|
|
64
93
|
});
|
|
65
94
|
|
|
66
95
|
try {
|
|
67
|
-
const result =
|
|
68
|
-
encoding: 'utf8',
|
|
69
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
70
|
-
});
|
|
96
|
+
const result = runStatusline(minimalInput);
|
|
71
97
|
|
|
72
98
|
test('Minimal input produces output', () => {
|
|
73
99
|
assertTrue(result.length > 0, 'Should produce some output');
|
|
@@ -109,11 +135,7 @@ try {
|
|
|
109
135
|
context_window: { context_window_size: 200000 }
|
|
110
136
|
});
|
|
111
137
|
|
|
112
|
-
const gitResult =
|
|
113
|
-
encoding: 'utf8',
|
|
114
|
-
cwd: tmpDir,
|
|
115
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
116
|
-
});
|
|
138
|
+
const gitResult = runStatusline(gitInput);
|
|
117
139
|
|
|
118
140
|
test('Git input processed without error', () => {
|
|
119
141
|
assertTrue(gitResult.length > 0, 'Should produce output for git repo');
|
|
@@ -153,10 +175,7 @@ const contextInput = JSON.stringify({
|
|
|
153
175
|
});
|
|
154
176
|
|
|
155
177
|
try {
|
|
156
|
-
const contextResult =
|
|
157
|
-
encoding: 'utf8',
|
|
158
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
159
|
-
});
|
|
178
|
+
const contextResult = runStatusline(contextInput);
|
|
160
179
|
|
|
161
180
|
test('Context window data processed', () => {
|
|
162
181
|
assertTrue(contextResult.length > 0, 'Should process context window data');
|
|
@@ -180,8 +199,6 @@ try {
|
|
|
180
199
|
|
|
181
200
|
console.log('\nTEST 4: JSON with Cost Info\n');
|
|
182
201
|
|
|
183
|
-
process.env.CLAUDE_BILLING_MODE = 'api';
|
|
184
|
-
|
|
185
202
|
const costInput = JSON.stringify({
|
|
186
203
|
model: { display_name: 'Claude' },
|
|
187
204
|
workspace: { current_dir: '/home/user' },
|
|
@@ -194,11 +211,7 @@ const costInput = JSON.stringify({
|
|
|
194
211
|
});
|
|
195
212
|
|
|
196
213
|
try {
|
|
197
|
-
const costResult =
|
|
198
|
-
encoding: 'utf8',
|
|
199
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
200
|
-
env: { ...process.env, CLAUDE_BILLING_MODE: 'api' }
|
|
201
|
-
});
|
|
214
|
+
const costResult = runStatusline(costInput, { CLAUDE_BILLING_MODE: 'api' });
|
|
202
215
|
|
|
203
216
|
test('Cost info displayed in API mode', () => {
|
|
204
217
|
assertTrue(costResult.length > 0, 'Should display cost info');
|
|
@@ -222,10 +235,7 @@ try {
|
|
|
222
235
|
console.log('\nTEST 5: Invalid JSON Handling\n');
|
|
223
236
|
|
|
224
237
|
try {
|
|
225
|
-
const invalidResult =
|
|
226
|
-
encoding: 'utf8',
|
|
227
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
228
|
-
});
|
|
238
|
+
const invalidResult = runStatusline('not valid json');
|
|
229
239
|
|
|
230
240
|
test('Invalid JSON produces fallback output', () => {
|
|
231
241
|
assertTrue(invalidResult.length > 0, 'Should produce fallback output');
|
|
@@ -247,10 +257,7 @@ try {
|
|
|
247
257
|
console.log('\nTEST 6: Empty Input Handling\n');
|
|
248
258
|
|
|
249
259
|
try {
|
|
250
|
-
const emptyResult =
|
|
251
|
-
encoding: 'utf8',
|
|
252
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
253
|
-
});
|
|
260
|
+
const emptyResult = runStatusline('');
|
|
254
261
|
|
|
255
262
|
test('Empty input handled', () => {
|
|
256
263
|
// Should either error gracefully or produce fallback
|
|
@@ -281,10 +288,7 @@ const multilineInput = JSON.stringify({
|
|
|
281
288
|
});
|
|
282
289
|
|
|
283
290
|
try {
|
|
284
|
-
const multilineResult =
|
|
285
|
-
encoding: 'utf8',
|
|
286
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
287
|
-
});
|
|
291
|
+
const multilineResult = runStatusline(multilineInput);
|
|
288
292
|
|
|
289
293
|
test('Multi-line output generates content', () => {
|
|
290
294
|
assertTrue(multilineResult.length > 0, 'Should generate output');
|
|
@@ -322,10 +326,7 @@ const expandInput = JSON.stringify({
|
|
|
322
326
|
});
|
|
323
327
|
|
|
324
328
|
try {
|
|
325
|
-
const expandResult =
|
|
326
|
-
encoding: 'utf8',
|
|
327
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
328
|
-
});
|
|
329
|
+
const expandResult = runStatusline(expandInput);
|
|
329
330
|
|
|
330
331
|
test('Home directory expanded to tilde', () => {
|
|
331
332
|
assertTrue(expandResult.includes('~') || expandResult.includes('projects'), 'Should expand or contain path');
|
|
@@ -352,11 +353,7 @@ const colorInput = JSON.stringify({
|
|
|
352
353
|
|
|
353
354
|
try {
|
|
354
355
|
// Test with NO_COLOR=1
|
|
355
|
-
const noColorResult =
|
|
356
|
-
encoding: 'utf8',
|
|
357
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
358
|
-
env: { ...process.env, NO_COLOR: '1' }
|
|
359
|
-
});
|
|
356
|
+
const noColorResult = runStatusline(colorInput, { NO_COLOR: '1' });
|
|
360
357
|
|
|
361
358
|
test('NO_COLOR=1 produces output', () => {
|
|
362
359
|
assertTrue(noColorResult.length > 0, 'Should produce output with NO_COLOR=1');
|
|
@@ -390,11 +387,7 @@ const wideInput = JSON.stringify({
|
|
|
390
387
|
|
|
391
388
|
let wideLines = 0;
|
|
392
389
|
try {
|
|
393
|
-
const wideResult =
|
|
394
|
-
encoding: 'utf8',
|
|
395
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
396
|
-
env: { ...process.env, COLUMNS: '160' }
|
|
397
|
-
});
|
|
390
|
+
const wideResult = runStatusline(wideInput, { COLUMNS: '160' });
|
|
398
391
|
wideLines = wideResult.trim().split('\n').length;
|
|
399
392
|
|
|
400
393
|
test('Wide terminal (160 cols) produces output', () => {
|
|
@@ -425,11 +418,7 @@ const narrowInput = JSON.stringify({
|
|
|
425
418
|
});
|
|
426
419
|
|
|
427
420
|
try {
|
|
428
|
-
const narrowResult =
|
|
429
|
-
encoding: 'utf8',
|
|
430
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
431
|
-
env: { ...process.env, COLUMNS: '80' }
|
|
432
|
-
});
|
|
421
|
+
const narrowResult = runStatusline(narrowInput, { COLUMNS: '80' });
|
|
433
422
|
const narrowLines = narrowResult.trim().split('\n').length;
|
|
434
423
|
|
|
435
424
|
test('Narrow terminal (80 cols) produces output', () => {
|
|
@@ -463,11 +452,7 @@ const longPathInput = JSON.stringify({
|
|
|
463
452
|
});
|
|
464
453
|
|
|
465
454
|
try {
|
|
466
|
-
const longPathResult =
|
|
467
|
-
encoding: 'utf8',
|
|
468
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
469
|
-
env: { ...process.env, COLUMNS: '100' }
|
|
470
|
-
});
|
|
455
|
+
const longPathResult = runStatusline(longPathInput, { COLUMNS: '100' });
|
|
471
456
|
|
|
472
457
|
test('Long path produces output without crash', () => {
|
|
473
458
|
assertTrue(longPathResult.length > 0, 'Should produce output');
|
|
@@ -497,11 +482,7 @@ const longModelInput = JSON.stringify({
|
|
|
497
482
|
});
|
|
498
483
|
|
|
499
484
|
try {
|
|
500
|
-
const longModelResult =
|
|
501
|
-
encoding: 'utf8',
|
|
502
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
503
|
-
env: { ...process.env, COLUMNS: '100' }
|
|
504
|
-
});
|
|
485
|
+
const longModelResult = runStatusline(longModelInput, { COLUMNS: '100' });
|
|
505
486
|
|
|
506
487
|
test('Long model name produces output', () => {
|
|
507
488
|
assertTrue(longModelResult.length > 0, 'Should produce output');
|
|
@@ -580,10 +561,7 @@ const agentTodoInput = JSON.stringify({
|
|
|
580
561
|
});
|
|
581
562
|
|
|
582
563
|
try {
|
|
583
|
-
const agentTodoResult =
|
|
584
|
-
encoding: 'utf8',
|
|
585
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
586
|
-
});
|
|
564
|
+
const agentTodoResult = runStatusline(agentTodoInput);
|
|
587
565
|
|
|
588
566
|
test('Agent/Todo tracking produces output', () => {
|
|
589
567
|
assertTrue(agentTodoResult.length > 0, 'Should produce output');
|
|
@@ -624,10 +602,7 @@ const zeroContextInput = JSON.stringify({
|
|
|
624
602
|
});
|
|
625
603
|
|
|
626
604
|
try {
|
|
627
|
-
const zeroResult =
|
|
628
|
-
encoding: 'utf8',
|
|
629
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
630
|
-
});
|
|
605
|
+
const zeroResult = runStatusline(zeroContextInput);
|
|
631
606
|
|
|
632
607
|
test('Zero context produces output', () => {
|
|
633
608
|
assertTrue(zeroResult.length > 0, 'Should produce output');
|
|
@@ -638,11 +613,7 @@ try {
|
|
|
638
613
|
|
|
639
614
|
// Test with very small terminal
|
|
640
615
|
try {
|
|
641
|
-
const tinyResult =
|
|
642
|
-
encoding: 'utf8',
|
|
643
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
644
|
-
env: { ...process.env, COLUMNS: '40' }
|
|
645
|
-
});
|
|
616
|
+
const tinyResult = runStatusline(wideInput, { COLUMNS: '40' });
|
|
646
617
|
|
|
647
618
|
test('Very narrow terminal (40 cols) handles gracefully', () => {
|
|
648
619
|
assertTrue(tinyResult.length > 0, 'Should produce output even at 40 cols');
|
|
@@ -138,9 +138,9 @@ const scenarios = [
|
|
|
138
138
|
desc: 'Grep in src'
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
|
-
input: { tool_name: 'Glob', tool_input: { pattern: '
|
|
141
|
+
input: { tool_name: 'Glob', tool_input: { pattern: 'src/**/*.ts' } },
|
|
142
142
|
expected: 'ALLOWED',
|
|
143
|
-
desc: 'Glob
|
|
143
|
+
desc: 'Glob scoped .ts files in src'
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
input: { tool_name: 'Bash', tool_input: { command: 'find packages -name "*.json" | head' } },
|
|
@@ -9,7 +9,7 @@ const { execSync } = require('child_process');
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
12
|
-
const scriptPath = path.join(__dirname, '..', 'scout-block
|
|
12
|
+
const scriptPath = path.join(__dirname, '..', 'scout-block.cjs');
|
|
13
13
|
const ckignorePath = path.join(__dirname, '..', '..', '.ckignore');
|
|
14
14
|
const ckignoreBackupPath = ckignorePath + '.backup';
|
|
15
15
|
|
|
@@ -23,7 +23,7 @@ if (fs.existsSync(ckignorePath)) {
|
|
|
23
23
|
function runTest(name, input, expected) {
|
|
24
24
|
try {
|
|
25
25
|
const inputJson = JSON.stringify(input);
|
|
26
|
-
execSync(`
|
|
26
|
+
execSync(`node "${scriptPath}"`, {
|
|
27
27
|
input: inputJson,
|
|
28
28
|
encoding: 'utf-8',
|
|
29
29
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
@@ -48,6 +48,11 @@ function restoreCkignore() {
|
|
|
48
48
|
if (fs.existsSync(ckignoreBackupPath)) {
|
|
49
49
|
fs.unlinkSync(ckignoreBackupPath);
|
|
50
50
|
}
|
|
51
|
+
} else {
|
|
52
|
+
// If there was no original .ckignore, remove the test-created one
|
|
53
|
+
if (fs.existsSync(ckignorePath)) {
|
|
54
|
+
fs.unlinkSync(ckignorePath);
|
|
55
|
+
}
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
|
package/package.json
CHANGED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Test script for modularization-hook.js (PostToolUse hook)
|
|
5
|
-
* Tests if the hook correctly identifies files exceeding LOC threshold
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { execSync } = require('child_process');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const os = require('os');
|
|
12
|
-
|
|
13
|
-
// Create a temporary test file
|
|
14
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'modularization-test-'));
|
|
15
|
-
const testFilePath = path.join(tempDir, 'test-file.js');
|
|
16
|
-
|
|
17
|
-
// Create file with 250 lines (exceeds 200 LOC threshold)
|
|
18
|
-
const longContent = Array(250).fill('// Test line').join('\n');
|
|
19
|
-
fs.writeFileSync(testFilePath, longContent);
|
|
20
|
-
|
|
21
|
-
// Create file with 50 lines (under threshold)
|
|
22
|
-
const shortFilePath = path.join(tempDir, 'short-file.js');
|
|
23
|
-
const shortContent = Array(50).fill('// Test line').join('\n');
|
|
24
|
-
fs.writeFileSync(shortFilePath, shortContent);
|
|
25
|
-
|
|
26
|
-
const testCases = [
|
|
27
|
-
{
|
|
28
|
-
name: 'Write tool with file exceeding LOC threshold',
|
|
29
|
-
input: {
|
|
30
|
-
tool_name: 'Write',
|
|
31
|
-
tool_input: {
|
|
32
|
-
file_path: testFilePath,
|
|
33
|
-
content: longContent
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
expectSuggestion: true
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: 'Edit tool with file exceeding LOC threshold',
|
|
40
|
-
input: {
|
|
41
|
-
tool_name: 'Edit',
|
|
42
|
-
tool_input: {
|
|
43
|
-
file_path: testFilePath,
|
|
44
|
-
old_string: '// Test line',
|
|
45
|
-
new_string: '// Modified line'
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
expectSuggestion: true
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: 'Write tool with short file (under threshold)',
|
|
52
|
-
input: {
|
|
53
|
-
tool_name: 'Write',
|
|
54
|
-
tool_input: {
|
|
55
|
-
file_path: shortFilePath,
|
|
56
|
-
content: shortContent
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
expectSuggestion: false
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
name: 'Write tool with non-existent file',
|
|
63
|
-
input: {
|
|
64
|
-
tool_name: 'Write',
|
|
65
|
-
tool_input: {
|
|
66
|
-
file_path: '/tmp/non-existent-file.js',
|
|
67
|
-
content: 'test'
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
expectSuggestion: false
|
|
71
|
-
}
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
console.log('Testing modularization-hook.js...\n');
|
|
75
|
-
|
|
76
|
-
const scriptPath = path.join(__dirname, '..', 'modularization-hook.js');
|
|
77
|
-
let passed = 0;
|
|
78
|
-
let failed = 0;
|
|
79
|
-
|
|
80
|
-
for (const test of testCases) {
|
|
81
|
-
try {
|
|
82
|
-
const input = JSON.stringify(test.input);
|
|
83
|
-
const result = execSync(`node "${scriptPath}"`, {
|
|
84
|
-
input,
|
|
85
|
-
encoding: 'utf-8',
|
|
86
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
87
|
-
env: { ...process.env, MODULARIZATION_HOOK_DEBUG: 'false' }
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
let hasSuggestion = false;
|
|
91
|
-
if (result && result.trim()) {
|
|
92
|
-
try {
|
|
93
|
-
const output = JSON.parse(result.trim());
|
|
94
|
-
hasSuggestion = output.hookSpecificOutput?.additionalContext?.includes('LOC');
|
|
95
|
-
} catch (e) {
|
|
96
|
-
// Not valid JSON or no output
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const success = hasSuggestion === test.expectSuggestion;
|
|
101
|
-
|
|
102
|
-
if (success) {
|
|
103
|
-
console.log(`✓ ${test.name}: ${hasSuggestion ? 'suggestion shown' : 'no suggestion'}`);
|
|
104
|
-
passed++;
|
|
105
|
-
} else {
|
|
106
|
-
console.log(`✗ ${test.name}: expected ${test.expectSuggestion ? 'suggestion' : 'no suggestion'}, got ${hasSuggestion ? 'suggestion' : 'no suggestion'}`);
|
|
107
|
-
if (result) {
|
|
108
|
-
console.log(` Output: ${result.trim()}`);
|
|
109
|
-
}
|
|
110
|
-
failed++;
|
|
111
|
-
}
|
|
112
|
-
} catch (error) {
|
|
113
|
-
console.log(`✗ ${test.name}: error executing hook`);
|
|
114
|
-
console.log(` Error: ${error.message}`);
|
|
115
|
-
if (error.stderr) {
|
|
116
|
-
console.log(` Stderr: ${error.stderr.toString()}`);
|
|
117
|
-
}
|
|
118
|
-
failed++;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Cleanup
|
|
123
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
124
|
-
|
|
125
|
-
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
126
|
-
process.exit(failed > 0 ? 1 : 0);
|