tribunal-kit 4.4.0 → 4.4.2
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/.agent/agents/api-architect.md +66 -66
- package/.agent/agents/db-latency-auditor.md +216 -216
- package/.agent/agents/precedence-reviewer.md +250 -250
- package/.agent/agents/resilience-reviewer.md +88 -88
- package/.agent/agents/schema-reviewer.md +67 -67
- package/.agent/agents/throughput-optimizer.md +299 -299
- package/.agent/agents/ui-ux-auditor.md +292 -292
- package/.agent/agents/vitals-reviewer.md +223 -223
- package/.agent/history/architecture-graph.yaml +32 -1
- package/.agent/history/graph-cache.json +66 -19
- package/.agent/history/snapshots/bin__tribunal-kit.js.json +19 -0
- package/.agent/history/snapshots/eslint.config.js.json +9 -0
- package/.agent/history/snapshots/migrate_refs.js.json +3 -3
- package/.agent/history/snapshots/scripts__changelog.js.json +2 -1
- package/.agent/history/snapshots/scripts__sync-version.js.json +2 -1
- package/.agent/history/snapshots/scripts__validate-payload.js.json +1 -0
- package/.agent/history/snapshots/test__integration__bridges.test.js.json +2 -1
- package/.agent/history/snapshots/test__integration__init.test.js.json +1 -0
- package/.agent/history/snapshots/test__integration__routing.test.js.json +1 -0
- package/.agent/history/snapshots/test__integration__swarm_dispatcher.test.js.json +2 -1
- package/.agent/history/snapshots/test__integration__wave2.test.js.json +2 -1
- package/.agent/history/snapshots/test__unit__args.test.js.json +11 -1
- package/.agent/history/snapshots/test__unit__case_law_manager.test.js.json +1 -0
- package/.agent/history/snapshots/test__unit__context_broker.test.js.json +11 -0
- package/.agent/history/snapshots/test__unit__copyDir.test.js.json +11 -1
- package/.agent/history/snapshots/test__unit__graph_tools.test.js.json +1 -0
- package/.agent/history/snapshots/test__unit__inner_loop_validator.test.js.json +11 -0
- package/.agent/history/snapshots/test__unit__selfInstall.test.js.json +11 -1
- package/.agent/history/snapshots/test__unit__semver.test.js.json +11 -1
- package/.agent/history/snapshots/test__unit__swarm_dispatcher.test.js.json +1 -0
- package/.agent/scripts/_colors.js +154 -2
- package/.agent/scripts/_utils.js +205 -3
- package/.agent/scripts/append_flow.js +72 -72
- package/.agent/scripts/auto_preview.js +197 -197
- package/.agent/scripts/bundle_analyzer.js +90 -119
- package/.agent/scripts/case_law_manager.js +18 -13
- package/.agent/scripts/checklist.js +100 -88
- package/.agent/scripts/colors.js +7 -13
- package/.agent/scripts/compress_skills.js +141 -141
- package/.agent/scripts/consolidate_skills.js +149 -149
- package/.agent/scripts/context_broker.js +605 -609
- package/.agent/scripts/deep_compress.js +150 -150
- package/.agent/scripts/dependency_analyzer.js +68 -106
- package/.agent/scripts/graph_builder.js +341 -311
- package/.agent/scripts/graph_visualizer.js +390 -384
- package/.agent/scripts/graph_zoom.js +6 -4
- package/.agent/scripts/inner_loop_validator.js +445 -465
- package/.agent/scripts/lint_runner.js +27 -28
- package/.agent/scripts/minify_context.js +100 -100
- package/.agent/scripts/mutation_runner.js +280 -280
- package/.agent/scripts/patch_skills_meta.js +156 -156
- package/.agent/scripts/patch_skills_output.js +244 -244
- package/.agent/scripts/schema_validator.js +280 -297
- package/.agent/scripts/security_scan.js +37 -64
- package/.agent/scripts/session_manager.js +270 -276
- package/.agent/scripts/skill_evolution.js +637 -644
- package/.agent/scripts/skill_integrator.js +307 -313
- package/.agent/scripts/strengthen_skills.js +193 -193
- package/.agent/scripts/strip_tribunal.js +47 -47
- package/.agent/scripts/swarm_dispatcher.js +360 -360
- package/.agent/scripts/test_runner.js +32 -39
- package/.agent/scripts/utils.js +10 -25
- package/.agent/scripts/verify_all.js +84 -92
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +1 -1
- package/.agent/skills/doc.md +1 -1
- package/.agent/skills/knowledge-graph/SKILL.md +52 -52
- package/.agent/skills/ui-ux-pro-max/SKILL.md +562 -562
- package/.agent/workflows/generate.md +183 -183
- package/.agent/workflows/tribunal-speed.md +183 -183
- package/README.md +1 -1
- package/bin/tribunal-kit.js +76 -87
- package/package.json +6 -3
- package/scripts/changelog.js +167 -167
- package/scripts/sync-version.js +81 -81
- package/.agent/history/architecture-explorer.html +0 -352
- package/.agent/scripts/__pycache__/_colors.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/_utils.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/case_law_manager.cpython-311.pyc +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/unit/context_broker.test.js",
|
|
3
|
+
"hash": "5fa154b5885a89376a937bd4825b1ba5643c92f6",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"../../.agent/scripts/context_broker": []
|
|
8
|
+
},
|
|
9
|
+
"dependents": [],
|
|
10
|
+
"content": "/**\n * context_broker.test.js — Unit tests for the Context Density Broker\n * Tests: tokenize, scoreSkill, selectSkills\n */\n\nconst {\n scoreSkill,\n selectSkills,\n tokenize\n} = require('../../.agent/scripts/context_broker');\n\ndescribe('context_broker.js', () => {\n\n // ── tokenize ──────────────────────────────────────────────────────────\n\n describe('tokenize()', () => {\n it('should extract words 3+ characters, lowercased', () => {\n const tokens = tokenize('Build a JWT authentication API');\n expect(tokens).toContain('build');\n expect(tokens).toContain('jwt');\n expect(tokens).toContain('authentication');\n expect(tokens).toContain('api');\n });\n\n it('should ignore 1-2 character words', () => {\n const tokens = tokenize('I am ok to go');\n // \"ok\" is only 2 chars, \"am\" is 2 chars, \"I\" is 1 char, \"to\" is 2 chars, \"go\" is 2 chars\n expect(tokens).toEqual([]);\n });\n\n it('should handle empty string', () => {\n expect(tokenize('')).toEqual([]);\n });\n\n it('should handle underscored identifiers', () => {\n const tokens = tokenize('use_case my_component');\n expect(tokens).toContain('use_case');\n expect(tokens).toContain('my_component');\n });\n });\n\n // ── scoreSkill ────────────────────────────────────────────────────────\n\n describe('scoreSkill()', () => {\n const makeSkill = (name, description = '') => ({\n name,\n description,\n content: ''\n });\n\n it('should give higher scores for domain-matched skills', () => {\n const taskTokens = tokenize('Build a React component with hooks');\n const reactSkill = makeSkill('react-specialist', 'React 19+ specialist. Components, hooks, state.');\n const sqlSkill = makeSkill('sql-pro', 'Advanced SQL queries and optimization');\n\n const reactScore = scoreSkill(reactSkill, 'Build a React component with hooks', [], taskTokens);\n const sqlScore = scoreSkill(sqlSkill, 'Build a React component with hooks', [], taskTokens);\n\n expect(reactScore).toBeGreaterThan(sqlScore);\n });\n\n it('should boost score for file extension affinity', () => {\n const taskTokens = tokenize('Fix the login page');\n const reactSkill = makeSkill('react-specialist', 'React components and hooks');\n\n const withoutExt = scoreSkill(reactSkill, 'Fix the login page', [], taskTokens);\n const withExt = scoreSkill(reactSkill, 'Fix the login page', ['.tsx'], taskTokens);\n\n expect(withExt).toBeGreaterThanOrEqual(withoutExt);\n });\n\n it('should return 0 or low score for completely unrelated skills', () => {\n const taskTokens = tokenize('Deploy to Kubernetes');\n const gameSkill = makeSkill('game-design-expert', 'Game design and player experience');\n\n const score = scoreSkill(gameSkill, 'Deploy to Kubernetes', [], taskTokens);\n expect(score).toBeLessThanOrEqual(1);\n });\n\n it('should boost baseline skills', () => {\n const taskTokens = tokenize('random unrelated task');\n // clean-code is a baseline skill\n const cleanCode = makeSkill('clean-code', 'Clean code mastery');\n\n const score = scoreSkill(cleanCode, 'random unrelated task', [], taskTokens);\n // Baseline skills get +1 regardless of match\n expect(score).toBeGreaterThanOrEqual(1);\n });\n });\n\n // ── selectSkills ──────────────────────────────────────────────────────\n\n describe('selectSkills()', () => {\n // Create minimal mock skills array for testing\n const mockSkills = [\n { name: 'react-specialist', description: 'React 19+ specialist. Components, hooks, state management.', content: '' },\n { name: 'sql-pro', description: 'SQL queries, PostgreSQL, MySQL, optimization', content: '' },\n { name: 'clean-code', description: 'Clean code mastery. Naming, SOLID, refactoring.', content: '' },\n { name: 'testing-patterns', description: 'Testing mastery. Jest, Vitest, Playwright, mocking.', content: '' },\n { name: 'vulnerability-scanner', description: 'Security vulnerability analysis. OWASP, injection.', content: '' },\n ];\n\n it('should return essential, supplementary, and available arrays', () => {\n const result = selectSkills('Build a React component', [], 'large', mockSkills);\n\n expect(result).toHaveProperty('essential');\n expect(result).toHaveProperty('supplementary');\n expect(result).toHaveProperty('available');\n expect(result).toHaveProperty('scores');\n expect(Array.isArray(result.essential)).toBe(true);\n expect(Array.isArray(result.supplementary)).toBe(true);\n expect(Array.isArray(result.available)).toBe(true);\n });\n\n it('should prioritize relevant skills in essential tier', () => {\n const result = selectSkills('Write a SQL query for user analytics', [], 'large', mockSkills);\n const essentialNames = result.essential.map(s => s.name);\n // sql-pro should be ranked highly for a SQL task\n if (essentialNames.length > 0) {\n expect(essentialNames).toContain('sql-pro');\n }\n });\n\n it('should limit essential to 6 for small models', () => {\n const result = selectSkills('Build something', [], 'small', mockSkills);\n expect(result.essential.length).toBeLessThanOrEqual(6);\n // Small model should have empty supplementary\n expect(result.supplementary).toEqual([]);\n });\n\n it('should collapse supplementary into available for small models', () => {\n const result = selectSkills('Build a full-stack app', [], 'small', mockSkills);\n expect(result.supplementary).toEqual([]);\n // Available should contain things that would have been supplementary\n expect(result.available.length).toBeGreaterThanOrEqual(0);\n });\n\n it('should limit essential to 10 for large models', () => {\n const result = selectSkills('Build everything imaginable', [], 'large', mockSkills);\n expect(result.essential.length).toBeLessThanOrEqual(10);\n });\n\n it('should return a scores Map', () => {\n const result = selectSkills('Test something', [], 'large', mockSkills);\n expect(result.scores).toBeInstanceOf(Map);\n expect(result.scores.size).toBe(mockSkills.length);\n });\n });\n});\n"
|
|
11
|
+
}
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"file": "test/unit/copyDir.test.js",
|
|
3
|
+
"hash": "1f3111ef91650adcb45b728779000c03b9bdcb3f",
|
|
3
4
|
"riskScore": "Low",
|
|
4
5
|
"blastRadius": 0,
|
|
5
6
|
"imports": {
|
|
6
7
|
"fs": [],
|
|
7
8
|
"path": [],
|
|
8
9
|
"os": [],
|
|
9
|
-
"../../bin/tribunal-kit": [
|
|
10
|
+
"../../bin/tribunal-kit": [
|
|
11
|
+
"parseArgs",
|
|
12
|
+
"compareSemver",
|
|
13
|
+
"copyDir",
|
|
14
|
+
"countDir",
|
|
15
|
+
"isSelfInstall",
|
|
16
|
+
"CORE_AGENTS",
|
|
17
|
+
"CORE_SKILLS",
|
|
18
|
+
"generateIDEBridges"
|
|
19
|
+
]
|
|
10
20
|
},
|
|
11
21
|
"dependents": [],
|
|
12
22
|
"content": "'use strict';\r\n\r\nconst fs = require('fs');\r\nconst path = require('path');\r\nconst os = require('os');\r\nconst { copyDir, countDir } = require('../../bin/tribunal-kit');\r\n\r\nfunction makeTempDir(prefix = 'tk-test-') {\r\n return fs.mkdtempSync(path.join(os.tmpdir(), prefix));\r\n}\r\n\r\ndescribe('countDir', () => {\r\n let tmpDir;\r\n\r\n beforeEach(() => {\r\n tmpDir = makeTempDir();\r\n });\r\n\r\n afterEach(() => {\r\n fs.rmSync(tmpDir, { recursive: true, force: true });\r\n });\r\n\r\n test('returns 0 for empty directory', () => {\r\n expect(countDir(tmpDir)).toBe(0);\r\n });\r\n\r\n test('counts files at top level', () => {\r\n fs.writeFileSync(path.join(tmpDir, 'a.txt'), 'a');\r\n fs.writeFileSync(path.join(tmpDir, 'b.txt'), 'b');\r\n expect(countDir(tmpDir)).toBe(2);\r\n });\r\n\r\n test('counts files recursively in subdirectories', () => {\r\n const sub = path.join(tmpDir, 'sub');\r\n fs.mkdirSync(sub);\r\n fs.writeFileSync(path.join(tmpDir, 'top.txt'), 'top');\r\n fs.writeFileSync(path.join(sub, 'nested.txt'), 'nested');\r\n expect(countDir(tmpDir)).toBe(2);\r\n });\r\n});\r\n\r\ndescribe('copyDir', () => {\r\n let srcDir;\r\n let destDir;\r\n\r\n beforeEach(() => {\r\n srcDir = makeTempDir('tk-src-');\r\n destDir = makeTempDir('tk-dest-');\r\n // Remove destDir so copyDir can create it fresh\r\n fs.rmSync(destDir, { recursive: true, force: true });\r\n });\r\n\r\n afterEach(() => {\r\n fs.rmSync(srcDir, { recursive: true, force: true });\r\n fs.rmSync(destDir, { recursive: true, force: true });\r\n });\r\n\r\n test('copies files to destination', () => {\r\n fs.writeFileSync(path.join(srcDir, 'hello.txt'), 'hello');\r\n copyDir(srcDir, destDir);\r\n expect(fs.existsSync(path.join(destDir, 'hello.txt'))).toBe(true);\r\n expect(fs.readFileSync(path.join(destDir, 'hello.txt'), 'utf8')).toBe('hello');\r\n });\r\n\r\n test('copies nested directories recursively', () => {\r\n const sub = path.join(srcDir, 'sub');\r\n fs.mkdirSync(sub);\r\n fs.writeFileSync(path.join(sub, 'nested.txt'), 'nested');\r\n copyDir(srcDir, destDir);\r\n expect(fs.existsSync(path.join(destDir, 'sub', 'nested.txt'))).toBe(true);\r\n });\r\n\r\n test('returns total file count copied', () => {\r\n fs.writeFileSync(path.join(srcDir, 'a.txt'), 'a');\r\n fs.writeFileSync(path.join(srcDir, 'b.txt'), 'b');\r\n const count = copyDir(srcDir, destDir);\r\n expect(count).toBe(2);\r\n });\r\n\r\n test('dry-run does NOT create destination files', () => {\r\n fs.writeFileSync(path.join(srcDir, 'hello.txt'), 'hello');\r\n const destDryRun = path.join(os.tmpdir(), 'tk-dryrun-' + Date.now());\r\n copyDir(srcDir, destDryRun, true);\r\n expect(fs.existsSync(destDryRun)).toBe(false);\r\n });\r\n\r\n test('dry-run still returns correct file count', () => {\r\n fs.writeFileSync(path.join(srcDir, 'a.txt'), 'a');\r\n fs.writeFileSync(path.join(srcDir, 'b.txt'), 'b');\r\n const destDryRun = path.join(os.tmpdir(), 'tk-dryrun2-' + Date.now());\r\n const count = copyDir(srcDir, destDryRun, true);\r\n expect(count).toBe(2);\r\n });\r\n});\r\n"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/unit/inner_loop_validator.test.js",
|
|
3
|
+
"hash": "48487187be036a1448dcb2089cb17d01258f964f",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"../../.agent/scripts/inner_loop_validator": []
|
|
8
|
+
},
|
|
9
|
+
"dependents": [],
|
|
10
|
+
"content": "/**\n * inner_loop_validator.test.js — Unit tests for the Inner-Loop Validator\n * Tests: scanCode, computeVerdict, buildSelfHealingInstructions, validate\n */\n\nconst {\n scanCode,\n computeVerdict,\n buildSelfHealingInstructions,\n validate\n} = require('../../.agent/scripts/inner_loop_validator');\n\ndescribe('inner_loop_validator.js', () => {\n\n // ── scanCode ──────────────────────────────────────────────────────────\n\n describe('scanCode()', () => {\n it('should return empty array for clean code', () => {\n const findings = scanCode('const x = 1;\\nconst y = x + 2;\\n');\n expect(findings).toEqual([]);\n });\n\n it('should detect eval() as high severity', () => {\n const findings = scanCode('const result = eval(userInput);');\n const evalFinding = findings.find(f => f.category === 'Code Injection');\n expect(evalFinding).toBeDefined();\n expect(evalFinding.severity).toBe('high');\n expect(evalFinding.line).toBe(1);\n expect(evalFinding.source).toBe('security_scan');\n });\n\n it('should detect innerHTML as high severity XSS', () => {\n const findings = scanCode('el.innerHTML = userInput;');\n const xssFinding = findings.find(f => f.category === 'XSS');\n expect(xssFinding).toBeDefined();\n expect(xssFinding.severity).toBe('high');\n });\n\n it('should detect hardcoded passwords as critical', () => {\n const findings = scanCode('const password = \"supersecret123\";');\n const secretFinding = findings.find(f => f.category === 'Hardcoded Secret');\n expect(secretFinding).toBeDefined();\n expect(secretFinding.severity).toBe('critical');\n });\n\n it('should detect empty catch blocks as medium severity', () => {\n const findings = scanCode('try { foo(); } catch (e) {}');\n const catchFinding = findings.find(f => f.category === 'Error Handling');\n expect(catchFinding).toBeDefined();\n expect(catchFinding.severity).toBe('medium');\n });\n\n it('should detect throw string as low severity', () => {\n const findings = scanCode('throw \"something went wrong\";');\n const throwFinding = findings.find(f => f.category === 'Error Quality');\n expect(throwFinding).toBeDefined();\n expect(throwFinding.severity).toBe('low');\n });\n\n it('should skip lines that are pure comments', () => {\n const code = '// eval(dangerousInput);\\nconst safe = 1;';\n const findings = scanCode(code);\n // eval is in a comment, should not be flagged\n const evalFinding = findings.find(f => f.category === 'Code Injection');\n expect(evalFinding).toBeUndefined();\n });\n\n it('should report correct line numbers', () => {\n const code = 'const a = 1;\\nconst b = 2;\\nconst r = eval(\"bad\");';\n const findings = scanCode(code);\n const evalFinding = findings.find(f => f.category === 'Code Injection');\n expect(evalFinding).toBeDefined();\n expect(evalFinding.line).toBe(3);\n });\n\n it('should detect VERIFY flags as low/informational', () => {\n const findings = scanCode('const x = someApi(); // VERIFY: does this exist?');\n const verifyFinding = findings.find(f => f.category === 'Verification Flag');\n expect(verifyFinding).toBeDefined();\n expect(verifyFinding.severity).toBe('low');\n });\n\n it('should detect browser globals in Node context', () => {\n const findings = scanCode('const w = window.innerWidth;');\n const browserFinding = findings.find(f => f.category === 'Environment Check');\n expect(browserFinding).toBeDefined();\n });\n });\n\n // ── computeVerdict ────────────────────────────────────────────────────\n\n describe('computeVerdict()', () => {\n it('should return APPROVED for no findings', () => {\n const result = computeVerdict([]);\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n });\n\n it('should return APPROVED for low-only findings', () => {\n const findings = [\n { severity: 'low', category: 'Error Quality', line: 1, message: 'test' }\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n });\n\n it('should return WARNING for medium findings', () => {\n const findings = [\n { severity: 'medium', category: 'Error Handling', line: 1, message: 'test' }\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('WARNING');\n expect(result.passed).toBe(true);\n });\n\n it('should return REJECTED for high findings', () => {\n const findings = [\n { severity: 'high', category: 'Code Injection', line: 1, message: 'test' }\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('REJECTED');\n expect(result.passed).toBe(false);\n });\n\n it('should return REJECTED for critical findings', () => {\n const findings = [\n { severity: 'critical', category: 'Hardcoded Secret', line: 1, message: 'test' }\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('REJECTED');\n expect(result.passed).toBe(false);\n });\n\n it('should use worst severity when mixed findings exist', () => {\n const findings = [\n { severity: 'low', category: 'Info', line: 1, message: 'ok' },\n { severity: 'critical', category: 'Secret', line: 2, message: 'bad' },\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('REJECTED');\n expect(result.passed).toBe(false);\n });\n\n it('should not let Verification Flags block approval', () => {\n const findings = [\n { severity: 'low', category: 'Verification Flag', line: 1, message: 'verify' }\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n });\n });\n\n // ── buildSelfHealingInstructions ───────────────────────────────────────\n\n describe('buildSelfHealingInstructions()', () => {\n it('should return null when no blocking findings', () => {\n const result = buildSelfHealingInstructions([\n { severity: 'low', category: 'Info', line: 1, message: 'test', fix: 'do nothing' }\n ]);\n expect(result).toBeNull();\n });\n\n it('should return null for empty array', () => {\n expect(buildSelfHealingInstructions([])).toBeNull();\n });\n\n it('should generate instructions for critical findings', () => {\n const result = buildSelfHealingInstructions([\n { severity: 'critical', category: 'Hardcoded Secret', line: 5, message: 'password hardcoded', fix: 'Use env vars' }\n ]);\n expect(result).toContain('CRITICAL');\n expect(result).toContain('Line 5');\n expect(result).toContain('Hardcoded Secret');\n expect(result).toContain('Use env vars');\n });\n\n it('should generate instructions for high findings', () => {\n const result = buildSelfHealingInstructions([\n { severity: 'high', category: 'Code Injection', line: 3, message: 'eval used', fix: 'Remove eval' }\n ]);\n expect(result).toContain('HIGH');\n expect(result).toContain('Line 3');\n });\n\n it('should skip medium findings in instructions', () => {\n const result = buildSelfHealingInstructions([\n { severity: 'medium', category: 'Error Handling', line: 1, message: 'empty catch', fix: 'Add handler' }\n ]);\n expect(result).toBeNull();\n });\n });\n\n // ── validate (integration of scan + verdict + healing) ────────────────\n\n describe('validate()', () => {\n it('should return APPROVED for clean code', () => {\n const result = validate('const x = 1;\\nconst y = x + 2;');\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n expect(result.issues).toEqual([]);\n expect(result.self_healing_instructions).toBeNull();\n });\n\n it('should return REJECTED for code with eval', () => {\n const result = validate('const x = eval(input);');\n expect(result.verdict).toBe('REJECTED');\n expect(result.passed).toBe(false);\n expect(result.issues.length).toBeGreaterThan(0);\n expect(result.self_healing_instructions).not.toBeNull();\n });\n\n it('should handle empty/null input gracefully', () => {\n const result = validate('');\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n expect(result.summary).toContain('No code provided');\n });\n\n it('should handle null input', () => {\n const result = validate(null);\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n });\n\n it('should include meta with line count and timestamp', () => {\n const result = validate('const a = 1;\\nconst b = 2;\\nconst c = 3;');\n expect(result.meta).toBeDefined();\n expect(result.meta.lines_scanned).toBe(3);\n expect(result.meta.lang).toBe('js');\n expect(result.meta.timestamp).toBeDefined();\n });\n\n it('should sort issues by severity (critical first)', () => {\n const code = [\n 'throw \"bad\";', // low: Error Quality\n 'const password = \"secret123\";', // critical: Hardcoded Secret\n 'const x = eval(input);', // high: Code Injection\n ].join('\\n');\n const result = validate(code);\n const severities = result.issues.map(i => i.severity);\n const rankOrder = { critical: 0, high: 1, medium: 2, low: 3 };\n for (let i = 1; i < severities.length; i++) {\n expect(rankOrder[severities[i]]).toBeGreaterThanOrEqual(rankOrder[severities[i - 1]]);\n }\n });\n\n it('should generate summary with issue counts', () => {\n const result = validate('const password = \"secret\";');\n expect(result.summary).toBeTruthy();\n expect(result.summary).toContain('critical');\n });\n });\n});\n"
|
|
11
|
+
}
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"file": "test/unit/selfInstall.test.js",
|
|
3
|
+
"hash": "65b099af8b884c6c1d174f7f70c6d836c27e02ea",
|
|
3
4
|
"riskScore": "Low",
|
|
4
5
|
"blastRadius": 0,
|
|
5
6
|
"imports": {
|
|
6
7
|
"path": [],
|
|
7
8
|
"fs": [],
|
|
8
9
|
"os": [],
|
|
9
|
-
"../../bin/tribunal-kit": [
|
|
10
|
+
"../../bin/tribunal-kit": [
|
|
11
|
+
"parseArgs",
|
|
12
|
+
"compareSemver",
|
|
13
|
+
"copyDir",
|
|
14
|
+
"countDir",
|
|
15
|
+
"isSelfInstall",
|
|
16
|
+
"CORE_AGENTS",
|
|
17
|
+
"CORE_SKILLS",
|
|
18
|
+
"generateIDEBridges"
|
|
19
|
+
]
|
|
10
20
|
},
|
|
11
21
|
"dependents": [],
|
|
12
22
|
"content": "'use strict';\r\n\r\nconst path = require('path');\r\nconst fs = require('fs');\r\nconst os = require('os');\r\nconst { isSelfInstall } = require('../../bin/tribunal-kit');\r\n\r\ndescribe('isSelfInstall', () => {\r\n test('returns true when target is the kit root (package root)', () => {\r\n // The kit root is one level above bin/\r\n const kitRoot = path.resolve(__dirname, '../../');\r\n expect(isSelfInstall(kitRoot)).toBe(true);\r\n });\r\n\r\n test('returns false for an unrelated directory', () => {\r\n expect(isSelfInstall(os.tmpdir())).toBe(false);\r\n });\r\n\r\n test('returns true when target package.json has name \"tribunal-kit\"', () => {\r\n // Create a temp dir with a fake package.json mimicking the kit\r\n const fakeKitDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-tk-'));\r\n fs.writeFileSync(\r\n path.join(fakeKitDir, 'package.json'),\r\n JSON.stringify({ name: 'tribunal-kit', version: '1.0.0' })\r\n );\r\n expect(isSelfInstall(fakeKitDir)).toBe(true);\r\n fs.rmSync(fakeKitDir, { recursive: true, force: true });\r\n });\r\n\r\n test('returns false when target package.json has a different name', () => {\r\n const fakeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'other-pkg-'));\r\n fs.writeFileSync(\r\n path.join(fakeDir, 'package.json'),\r\n JSON.stringify({ name: 'my-other-project', version: '1.0.0' })\r\n );\r\n expect(isSelfInstall(fakeDir)).toBe(false);\r\n fs.rmSync(fakeDir, { recursive: true, force: true });\r\n });\r\n\r\n test('returns false when target has no package.json', () => {\r\n const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'no-pkg-'));\r\n expect(isSelfInstall(emptyDir)).toBe(false);\r\n fs.rmSync(emptyDir, { recursive: true, force: true });\r\n });\r\n});\r\n"
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"file": "test/unit/semver.test.js",
|
|
3
|
+
"hash": "d3b4e92656e66f496f034174abd53012ed00b33c",
|
|
3
4
|
"riskScore": "Low",
|
|
4
5
|
"blastRadius": 0,
|
|
5
6
|
"imports": {
|
|
6
|
-
"../../bin/tribunal-kit": [
|
|
7
|
+
"../../bin/tribunal-kit": [
|
|
8
|
+
"parseArgs",
|
|
9
|
+
"compareSemver",
|
|
10
|
+
"copyDir",
|
|
11
|
+
"countDir",
|
|
12
|
+
"isSelfInstall",
|
|
13
|
+
"CORE_AGENTS",
|
|
14
|
+
"CORE_SKILLS",
|
|
15
|
+
"generateIDEBridges"
|
|
16
|
+
]
|
|
7
17
|
},
|
|
8
18
|
"dependents": [],
|
|
9
19
|
"content": "'use strict';\r\n\r\nconst { compareSemver } = require('../../bin/tribunal-kit');\r\n\r\ndescribe('compareSemver', () => {\r\n test('returns 1 when a > b (patch)', () => {\r\n expect(compareSemver('2.4.2', '2.4.1')).toBe(1);\r\n });\r\n\r\n test('returns 1 when a > b (minor)', () => {\r\n expect(compareSemver('2.5.0', '2.4.9')).toBe(1);\r\n });\r\n\r\n test('returns 1 when a > b (major)', () => {\r\n expect(compareSemver('3.0.0', '2.9.9')).toBe(1);\r\n });\r\n\r\n test('returns 0 when a === b', () => {\r\n expect(compareSemver('2.4.2', '2.4.2')).toBe(0);\r\n });\r\n\r\n test('returns -1 when a < b (patch)', () => {\r\n expect(compareSemver('2.4.1', '2.4.2')).toBe(-1);\r\n });\r\n\r\n test('returns -1 when a < b (minor)', () => {\r\n expect(compareSemver('2.4.9', '2.5.0')).toBe(-1);\r\n });\r\n\r\n test('strips leading \"v\" prefix from a', () => {\r\n expect(compareSemver('v2.4.2', '2.4.2')).toBe(0);\r\n });\r\n\r\n test('strips leading \"v\" prefix from b', () => {\r\n expect(compareSemver('2.4.2', 'v2.4.2')).toBe(0);\r\n });\r\n\r\n test('strips leading \"v\" from both', () => {\r\n expect(compareSemver('v2.4.3', 'v2.4.2')).toBe(1);\r\n });\r\n\r\n test('handles missing patch segment (treats as 0)', () => {\r\n expect(compareSemver('2.4', '2.4.0')).toBe(0);\r\n });\r\n});\r\n"
|
|
@@ -1,18 +1,170 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* _colors.js — Tribunal Kit ANSI Color System
|
|
3
|
+
* ════════════════════════════════════════════════════════════════
|
|
4
|
+
* Single source of truth for all terminal color constants.
|
|
3
5
|
* Import this module instead of duplicating color codes.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { RED, GREEN, BOLD, RESET } = require('./_colors');
|
|
9
|
+
* const C = require('./_colors'); // C.RED, C.banner('Title'), etc.
|
|
4
10
|
*/
|
|
5
11
|
|
|
6
12
|
'use strict';
|
|
7
13
|
|
|
14
|
+
// ── Core ANSI Escape Codes ──────────────────────────────────────────────────
|
|
8
15
|
const GREEN = '\x1b[92m';
|
|
9
16
|
const YELLOW = '\x1b[93m';
|
|
10
17
|
const CYAN = '\x1b[96m';
|
|
11
18
|
const RED = '\x1b[91m';
|
|
12
19
|
const BLUE = '\x1b[94m';
|
|
13
20
|
const MAGENTA = '\x1b[95m';
|
|
21
|
+
const WHITE = '\x1b[97m';
|
|
22
|
+
const GRAY = '\x1b[90m';
|
|
14
23
|
const BOLD = '\x1b[1m';
|
|
15
24
|
const DIM = '\x1b[2m';
|
|
25
|
+
const ITALIC = '\x1b[3m';
|
|
26
|
+
const UNDERLINE = '\x1b[4m';
|
|
16
27
|
const RESET = '\x1b[0m';
|
|
17
28
|
|
|
18
|
-
|
|
29
|
+
// ── Box Drawing Characters ──────────────────────────────────────────────────
|
|
30
|
+
const BOX = {
|
|
31
|
+
TL: '╔', TR: '╗', BL: '╚', BR: '╝',
|
|
32
|
+
H: '═', V: '║',
|
|
33
|
+
tl: '┌', tr: '┐', bl: '└', br: '┘',
|
|
34
|
+
h: '─', v: '│',
|
|
35
|
+
cross: '┼', teeL: '├', teeR: '┤', teeT: '┬', teeB: '┴',
|
|
36
|
+
bullet: '●', bulletEmpty: '○',
|
|
37
|
+
arrow: '→', arrowLeft: '←',
|
|
38
|
+
check: '✔', cross_mark: '✖',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ── Branded Output Helpers ──────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate a branded Tribunal banner box.
|
|
45
|
+
* @param {string} title - Script title (e.g., "verify_all.js")
|
|
46
|
+
* @param {object} [meta] - Optional metadata lines { key: value }
|
|
47
|
+
* @returns {string} Formatted banner string
|
|
48
|
+
*/
|
|
49
|
+
function banner(title, meta = {}) {
|
|
50
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
51
|
+
const lines = [
|
|
52
|
+
` ${BOLD}${CYAN}⚖️ TRIBUNAL KIT${RESET} ${DIM}— ${title}${RESET}`,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const metaEntries = Object.entries(meta);
|
|
56
|
+
if (metaEntries.length > 0) {
|
|
57
|
+
for (const [key, value] of metaEntries) {
|
|
58
|
+
lines.push(` ${DIM}${key}:${RESET} ${value}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
lines.push(` ${DIM}${timestamp}${RESET}`);
|
|
62
|
+
|
|
63
|
+
const divider = `${CYAN}${'━'.repeat(56)}${RESET}`;
|
|
64
|
+
return `\n${divider}\n${lines.join('\n')}\n${divider}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate a section header with optional timing.
|
|
69
|
+
* @param {string} title - Section title
|
|
70
|
+
* @param {number} [index] - Step number (1-based)
|
|
71
|
+
* @param {number} [ms] - Execution time in ms
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function sectionHeader(title, index, ms) {
|
|
75
|
+
const num = index != null ? `${index} — ` : '';
|
|
76
|
+
const timing = ms != null ? `${DIM} (${formatMs(ms)})${RESET}` : '';
|
|
77
|
+
return `\n${BOLD}${BLUE}━━━ ${num}${title} ━━━${RESET}${timing}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Format milliseconds into a human-readable string.
|
|
82
|
+
* @param {number} ms
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function formatMs(ms) {
|
|
86
|
+
if (ms < 1000) return `${ms}ms`;
|
|
87
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
88
|
+
return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Status Indicator Helpers ────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** @param {string} msg */
|
|
94
|
+
function ok(msg) { console.log(` ${GREEN}✅ ${msg}${RESET}`); }
|
|
95
|
+
|
|
96
|
+
/** @param {string} msg */
|
|
97
|
+
function fail(msg) { console.log(` ${RED}❌ ${msg}${RESET}`); }
|
|
98
|
+
|
|
99
|
+
/** @param {string} msg */
|
|
100
|
+
function warn(msg) { console.log(` ${YELLOW}⚠️ ${msg}${RESET}`); }
|
|
101
|
+
|
|
102
|
+
/** @param {string} msg */
|
|
103
|
+
function skip(msg) { console.log(` ${YELLOW}⏭️ ${msg}${RESET}`); }
|
|
104
|
+
|
|
105
|
+
/** @param {string} msg */
|
|
106
|
+
function info(msg) { console.log(` ${BLUE}ℹ️ ${msg}${RESET}`); }
|
|
107
|
+
|
|
108
|
+
// ── Summary Table Builder ───────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build and print a box-drawing summary table.
|
|
112
|
+
* @param {Array<{name: string, status: 'pass'|'fail'|'skip'|'warn', ms?: number}>} rows
|
|
113
|
+
*/
|
|
114
|
+
function summaryTable(rows) {
|
|
115
|
+
const maxName = Math.max(...rows.map(r => r.name.length), 18);
|
|
116
|
+
const colW = maxName + 2;
|
|
117
|
+
|
|
118
|
+
const statusIcon = (s) => {
|
|
119
|
+
if (s === 'pass') return `${GREEN} ✅ ${RESET}`;
|
|
120
|
+
if (s === 'fail') return `${RED} ❌ ${RESET}`;
|
|
121
|
+
if (s === 'skip') return `${YELLOW} ⏭️ ${RESET}`;
|
|
122
|
+
if (s === 'warn') return `${YELLOW} ⚠️ ${RESET}`;
|
|
123
|
+
return `${DIM} — ${RESET}`;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const pad = (str, len) => str + ' '.repeat(Math.max(0, len - str.length));
|
|
127
|
+
const top = ` ${BOX.tl}${BOX.h.repeat(colW)}${BOX.teeT}${BOX.h.repeat(6)}${BOX.teeT}${BOX.h.repeat(10)}${BOX.tr}`;
|
|
128
|
+
const mid = ` ${BOX.teeL}${BOX.h.repeat(colW)}${BOX.cross}${BOX.h.repeat(6)}${BOX.cross}${BOX.h.repeat(10)}${BOX.teeR}`;
|
|
129
|
+
const bot = ` ${BOX.bl}${BOX.h.repeat(colW)}${BOX.teeB}${BOX.h.repeat(6)}${BOX.teeB}${BOX.h.repeat(10)}${BOX.br}`;
|
|
130
|
+
const hdr = ` ${BOX.v} ${BOLD}${pad('Check', colW - 2)}${RESET} ${BOX.v}${BOLD} Res ${RESET}${BOX.v} ${BOLD}${pad('Time', 8)}${RESET}${BOX.v}`;
|
|
131
|
+
|
|
132
|
+
console.log(top);
|
|
133
|
+
console.log(hdr);
|
|
134
|
+
console.log(mid);
|
|
135
|
+
|
|
136
|
+
for (const row of rows) {
|
|
137
|
+
const time = row.ms != null ? formatMs(row.ms) : '—';
|
|
138
|
+
const line = ` ${BOX.v} ${pad(row.name, colW - 2)} ${BOX.v}${statusIcon(row.status)}${BOX.v} ${pad(time, 8)}${BOX.v}`;
|
|
139
|
+
console.log(line);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(bot);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── High-Precision Timer ────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create a timer. Call returned function to get elapsed ms.
|
|
149
|
+
* @returns {function(): number}
|
|
150
|
+
*/
|
|
151
|
+
function timer() {
|
|
152
|
+
const start = process.hrtime.bigint();
|
|
153
|
+
return () => Number(process.hrtime.bigint() - start) / 1e6;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
// Core colors
|
|
158
|
+
GREEN, YELLOW, CYAN, RED, BLUE, MAGENTA, WHITE, GRAY,
|
|
159
|
+
BOLD, DIM, ITALIC, UNDERLINE, RESET,
|
|
160
|
+
// Box drawing
|
|
161
|
+
BOX,
|
|
162
|
+
// Branded helpers
|
|
163
|
+
banner, sectionHeader, formatMs,
|
|
164
|
+
// Status indicators
|
|
165
|
+
ok, fail, warn, skip, info,
|
|
166
|
+
// Summary
|
|
167
|
+
summaryTable,
|
|
168
|
+
// Timing
|
|
169
|
+
timer,
|
|
170
|
+
};
|
package/.agent/scripts/_utils.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* _utils.js — Tribunal Kit Shared Utilities
|
|
3
|
+
* ════════════════════════════════════════════════════════════════
|
|
4
|
+
* Single source of truth for all shared utility functions.
|
|
5
|
+
* Import this module instead of duplicating helpers.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { findAgentDir, walkDir, loadJson } = require('./_utils');
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
'use strict';
|
|
@@ -9,6 +14,22 @@ const fs = require('fs');
|
|
|
9
14
|
const path = require('path');
|
|
10
15
|
const { RED, RESET } = require('./_colors');
|
|
11
16
|
|
|
17
|
+
// ── Default Skip Directories ────────────────────────────────────────────────
|
|
18
|
+
const DEFAULT_SKIP_DIRS = new Set([
|
|
19
|
+
'node_modules', '.git', 'dist', 'build', '.next', '.agent',
|
|
20
|
+
'__pycache__', '.venv', 'venv', 'coverage', '.turbo',
|
|
21
|
+
'.svelte-kit', '.nuxt', '.output',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
// ── Default Source Extensions ───────────────────────────────────────────────
|
|
25
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
26
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
27
|
+
'.py', '.rs', '.go', '.java', '.cs', '.rb',
|
|
28
|
+
'.vue', '.svelte',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// ── Agent Directory Discovery ───────────────────────────────────────────────
|
|
32
|
+
|
|
12
33
|
/**
|
|
13
34
|
* Walk up the directory tree to find the nearest .agent/ folder.
|
|
14
35
|
* @param {string} [startDir] - Directory to start searching from (defaults to cwd).
|
|
@@ -30,6 +51,8 @@ function findAgentDir(startDir) {
|
|
|
30
51
|
process.exit(1);
|
|
31
52
|
}
|
|
32
53
|
|
|
54
|
+
// ── Package.json Helpers ────────────────────────────────────────────────────
|
|
55
|
+
|
|
33
56
|
/**
|
|
34
57
|
* Check if a package.json exists in the given directory.
|
|
35
58
|
* @param {string} dir - Directory to check.
|
|
@@ -39,4 +62,183 @@ function hasNpm(dir) {
|
|
|
39
62
|
return fs.existsSync(path.join(dir, 'package.json'));
|
|
40
63
|
}
|
|
41
64
|
|
|
42
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Load and parse a JSON file safely. Returns null on failure.
|
|
67
|
+
* @param {string} filePath - Absolute path to the JSON file.
|
|
68
|
+
* @returns {object|null}
|
|
69
|
+
*/
|
|
70
|
+
function loadJson(filePath) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Filesystem Walking ──────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Recursively walk a directory tree, yielding file paths.
|
|
82
|
+
* This is the consolidated walker used by all scanning scripts.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} dir - Root directory to walk.
|
|
85
|
+
* @param {object} [opts] - Options.
|
|
86
|
+
* @param {Set<string>} [opts.skipDirs] - Directory names to skip (default: DEFAULT_SKIP_DIRS).
|
|
87
|
+
* @param {Set<string>} [opts.extensions] - Only yield files with these extensions. Null = all files.
|
|
88
|
+
* @param {function(string): boolean} [opts.filter] - Custom filter predicate for file paths.
|
|
89
|
+
* @returns {string[]} Array of absolute file paths.
|
|
90
|
+
*/
|
|
91
|
+
function walkDir(dir, opts = {}) {
|
|
92
|
+
const skipDirs = opts.skipDirs || DEFAULT_SKIP_DIRS;
|
|
93
|
+
const extensions = opts.extensions || null;
|
|
94
|
+
const filter = opts.filter || null;
|
|
95
|
+
const results = [];
|
|
96
|
+
|
|
97
|
+
function _walk(currentDir) {
|
|
98
|
+
let entries;
|
|
99
|
+
try {
|
|
100
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
101
|
+
} catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
107
|
+
|
|
108
|
+
if (entry.isDirectory()) {
|
|
109
|
+
if (!skipDirs.has(entry.name)) {
|
|
110
|
+
_walk(fullPath);
|
|
111
|
+
}
|
|
112
|
+
} else if (entry.isFile()) {
|
|
113
|
+
if (extensions) {
|
|
114
|
+
const ext = path.extname(entry.name);
|
|
115
|
+
if (!extensions.has(ext)) continue;
|
|
116
|
+
}
|
|
117
|
+
if (filter && !filter(fullPath)) continue;
|
|
118
|
+
results.push(fullPath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_walk(dir);
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Count files in a directory recursively (fast — no file content reads).
|
|
129
|
+
* @param {string} dir
|
|
130
|
+
* @param {Set<string>} [skipDirs]
|
|
131
|
+
* @returns {number}
|
|
132
|
+
*/
|
|
133
|
+
function countFiles(dir, skipDirs = DEFAULT_SKIP_DIRS) {
|
|
134
|
+
let count = 0;
|
|
135
|
+
function _count(d) {
|
|
136
|
+
let entries;
|
|
137
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
138
|
+
for (const e of entries) {
|
|
139
|
+
if (e.isDirectory() && !skipDirs.has(e.name)) _count(path.join(d, e.name));
|
|
140
|
+
else if (e.isFile()) count++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
_count(dir);
|
|
144
|
+
return count;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── CLI Argument Parsing ────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parse command-line arguments into a structured object.
|
|
151
|
+
* Supports --flag, --key value, and positional arguments.
|
|
152
|
+
*
|
|
153
|
+
* @param {string[]} argv - process.argv.slice(2)
|
|
154
|
+
* @param {object} [schema] - Flag definitions: { flag: { type: 'boolean'|'string'|'number', default: any } }
|
|
155
|
+
* @returns {{ flags: object, positional: string[] }}
|
|
156
|
+
*/
|
|
157
|
+
function parseArgs(argv, schema = {}) {
|
|
158
|
+
const flags = {};
|
|
159
|
+
const positional = [];
|
|
160
|
+
|
|
161
|
+
// Set defaults
|
|
162
|
+
for (const [key, def] of Object.entries(schema)) {
|
|
163
|
+
flags[key] = def.default ?? (def.type === 'boolean' ? false : null);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < argv.length; i++) {
|
|
167
|
+
const arg = argv[i];
|
|
168
|
+
|
|
169
|
+
if (arg === '-h' || arg === '--help') {
|
|
170
|
+
flags.help = true;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (arg.startsWith('--')) {
|
|
175
|
+
const flagName = arg.slice(2);
|
|
176
|
+
const schemaDef = schema[flagName];
|
|
177
|
+
|
|
178
|
+
if (schemaDef && schemaDef.type === 'boolean') {
|
|
179
|
+
flags[flagName] = true;
|
|
180
|
+
} else if (schemaDef && i + 1 < argv.length) {
|
|
181
|
+
const val = argv[++i];
|
|
182
|
+
flags[flagName] = schemaDef.type === 'number' ? Number(val) : val;
|
|
183
|
+
} else {
|
|
184
|
+
// Unknown flag, store as boolean
|
|
185
|
+
flags[flagName] = true;
|
|
186
|
+
}
|
|
187
|
+
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
188
|
+
// Short flag — treat as boolean
|
|
189
|
+
flags[arg.slice(1)] = true;
|
|
190
|
+
} else {
|
|
191
|
+
positional.push(arg);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { flags, positional };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Command Runner ──────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Run a command synchronously and return a structured result.
|
|
202
|
+
* Handles Windows .cmd shims automatically.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} cmd - Command to run
|
|
205
|
+
* @param {string[]} args - Arguments
|
|
206
|
+
* @param {object} [opts] - spawnSync options (cwd, timeout, etc.)
|
|
207
|
+
* @returns {{ status: number, stdout: string, stderr: string, ok: boolean }}
|
|
208
|
+
*/
|
|
209
|
+
function runCommand(cmd, args = [], opts = {}) {
|
|
210
|
+
const { spawnSync } = require('child_process');
|
|
211
|
+
const executable = process.platform === 'win32' && !cmd.endsWith('.cmd') && !cmd.includes(path.sep)
|
|
212
|
+
? `${cmd}.cmd`
|
|
213
|
+
: cmd;
|
|
214
|
+
|
|
215
|
+
const result = spawnSync(executable, args, {
|
|
216
|
+
encoding: 'utf8',
|
|
217
|
+
timeout: opts.timeout || 120000,
|
|
218
|
+
cwd: opts.cwd || process.cwd(),
|
|
219
|
+
shell: process.platform === 'win32',
|
|
220
|
+
stdio: opts.stdio || 'pipe',
|
|
221
|
+
...opts,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
status: result.status ?? 1,
|
|
226
|
+
stdout: (result.stdout || '').toString(),
|
|
227
|
+
stderr: (result.stderr || '').toString(),
|
|
228
|
+
ok: result.status === 0,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
// Agent discovery
|
|
234
|
+
findAgentDir,
|
|
235
|
+
// Package.json
|
|
236
|
+
hasNpm, loadJson,
|
|
237
|
+
// Filesystem
|
|
238
|
+
walkDir, countFiles,
|
|
239
|
+
DEFAULT_SKIP_DIRS, SOURCE_EXTENSIONS,
|
|
240
|
+
// CLI
|
|
241
|
+
parseArgs,
|
|
242
|
+
// Commands
|
|
243
|
+
runCommand,
|
|
244
|
+
};
|