@webpieces/ai-hook-rules 0.0.1 → 0.2.114

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.
Files changed (99) hide show
  1. package/package.json +4 -3
  2. package/src/adapters/claude-code-hook.d.ts +1 -0
  3. package/src/adapters/claude-code-hook.js +112 -0
  4. package/src/adapters/claude-code-hook.js.map +1 -0
  5. package/src/adapters/openclaw-plugin.d.ts +14 -0
  6. package/src/adapters/openclaw-plugin.js +73 -0
  7. package/src/adapters/openclaw-plugin.js.map +1 -0
  8. package/src/core/build-context.d.ts +8 -0
  9. package/src/core/build-context.js +62 -0
  10. package/src/core/build-context.js.map +1 -0
  11. package/src/core/configs/default.d.ts +2 -0
  12. package/src/core/configs/{default.ts → default.js} +6 -3
  13. package/src/core/configs/default.js.map +1 -0
  14. package/src/core/disable-directives.d.ts +9 -0
  15. package/src/core/disable-directives.js +92 -0
  16. package/src/core/disable-directives.js.map +1 -0
  17. package/src/core/instruct-ai-writer.d.ts +1 -0
  18. package/src/core/instruct-ai-writer.js +18 -0
  19. package/src/core/instruct-ai-writer.js.map +1 -0
  20. package/src/core/load-config.d.ts +1 -0
  21. package/src/core/load-config.js +10 -0
  22. package/src/core/load-config.js.map +1 -0
  23. package/src/core/load-rules.d.ts +3 -0
  24. package/src/core/{load-rules.ts → load-rules.js} +33 -32
  25. package/src/core/load-rules.js.map +1 -0
  26. package/src/core/rejection-log.d.ts +2 -0
  27. package/src/core/{rejection-log.ts → rejection-log.js} +34 -51
  28. package/src/core/rejection-log.js.map +1 -0
  29. package/src/core/report.d.ts +2 -0
  30. package/src/core/{report.ts → report.js} +7 -8
  31. package/src/core/report.js.map +1 -0
  32. package/src/core/rules/catch-error-pattern.d.ts +3 -0
  33. package/src/core/rules/{catch-error-pattern.ts → catch-error-pattern.js} +25 -54
  34. package/src/core/rules/catch-error-pattern.js.map +1 -0
  35. package/src/core/rules/file-location.d.ts +3 -0
  36. package/src/core/rules/{file-location.ts → file-location.js} +29 -43
  37. package/src/core/rules/file-location.js.map +1 -0
  38. package/src/core/rules/index.d.ts +1 -0
  39. package/src/core/rules/{index.ts → index.js} +5 -1
  40. package/src/core/rules/index.js.map +1 -0
  41. package/src/core/rules/max-file-lines.d.ts +3 -0
  42. package/src/core/rules/{max-file-lines.ts → max-file-lines.js} +17 -23
  43. package/src/core/rules/max-file-lines.js.map +1 -0
  44. package/src/core/rules/no-any-unknown.d.ts +3 -0
  45. package/src/core/rules/no-any-unknown.js +30 -0
  46. package/src/core/rules/no-any-unknown.js.map +1 -0
  47. package/src/core/rules/no-destructure.d.ts +3 -0
  48. package/src/core/rules/{no-destructure.ts → no-destructure.js} +13 -17
  49. package/src/core/rules/no-destructure.js.map +1 -0
  50. package/src/core/rules/no-implicit-any.d.ts +3 -0
  51. package/src/core/rules/{no-implicit-any.ts → no-implicit-any.js} +32 -30
  52. package/src/core/rules/no-implicit-any.js.map +1 -0
  53. package/src/core/rules/no-shell-substitution.d.ts +3 -0
  54. package/src/core/rules/no-shell-substitution.js +54 -0
  55. package/src/core/rules/no-shell-substitution.js.map +1 -0
  56. package/src/core/rules/no-unmanaged-exceptions.d.ts +3 -0
  57. package/src/core/rules/{no-unmanaged-exceptions.ts → no-unmanaged-exceptions.js} +21 -24
  58. package/src/core/rules/no-unmanaged-exceptions.js.map +1 -0
  59. package/src/core/rules/require-return-type.d.ts +3 -0
  60. package/src/core/rules/{require-return-type.ts → require-return-type.js} +21 -28
  61. package/src/core/rules/require-return-type.js.map +1 -0
  62. package/src/core/runner.d.ts +3 -0
  63. package/src/core/runner.js +181 -0
  64. package/src/core/runner.js.map +1 -0
  65. package/src/core/strip-ts-noise.d.ts +1 -0
  66. package/src/core/strip-ts-noise.js +178 -0
  67. package/src/core/strip-ts-noise.js.map +1 -0
  68. package/src/core/to-error.d.ts +5 -0
  69. package/src/core/{to-error.ts → to-error.js} +7 -4
  70. package/src/core/to-error.js.map +1 -0
  71. package/src/core/types.d.ts +93 -0
  72. package/src/core/types.js +93 -0
  73. package/src/core/types.js.map +1 -0
  74. package/src/index.d.ts +5 -0
  75. package/src/index.js +25 -0
  76. package/src/index.js.map +1 -0
  77. package/LICENSE +0 -373
  78. package/src/adapters/claude-code-hook.ts +0 -117
  79. package/src/adapters/openclaw-plugin.ts +0 -88
  80. package/src/core/__tests__/disable-directives.test.ts +0 -114
  81. package/src/core/__tests__/rules/file-location.test.ts +0 -90
  82. package/src/core/__tests__/rules/max-file-lines.test.ts +0 -53
  83. package/src/core/__tests__/rules/no-any.test.ts +0 -68
  84. package/src/core/__tests__/rules/no-destructure.test.ts +0 -50
  85. package/src/core/__tests__/rules/no-shell-substitution.test.ts +0 -118
  86. package/src/core/__tests__/rules/no-unmanaged-exceptions.test.ts +0 -54
  87. package/src/core/__tests__/rules/require-return-type.test.ts +0 -79
  88. package/src/core/__tests__/runner.test.ts +0 -288
  89. package/src/core/__tests__/strip-ts-noise.test.ts +0 -109
  90. package/src/core/build-context.ts +0 -96
  91. package/src/core/disable-directives.ts +0 -90
  92. package/src/core/instruct-ai-writer.ts +0 -15
  93. package/src/core/load-config.ts +0 -3
  94. package/src/core/rules/no-any-unknown.ts +0 -35
  95. package/src/core/rules/no-shell-substitution.ts +0 -71
  96. package/src/core/runner.ts +0 -205
  97. package/src/core/strip-ts-noise.ts +0 -103
  98. package/src/core/types.ts +0 -196
  99. package/src/index.ts +0 -14
@@ -1,88 +0,0 @@
1
- import * as path from 'path';
2
- import * as fs from 'fs';
3
-
4
- import { run } from '../core/runner';
5
- import { NormalizedToolInput, NormalizedEdit, ToolKind } from '../core/types';
6
- import { toError } from '../core/to-error';
7
-
8
- interface ToolCallEvent {
9
- toolName: string;
10
- // webpieces-disable no-any-unknown -- openclaw SDK passes opaque tool arguments
11
- arguments: Record<string, unknown>;
12
- }
13
-
14
- interface HookContext {
15
- // webpieces-disable no-any-unknown -- openclaw SDK context shape is opaque
16
- [key: string]: unknown;
17
- }
18
-
19
- class OpenclawHandlerResult {
20
- readonly status: 'approved' | 'rejected';
21
- readonly reason: string | undefined;
22
-
23
- constructor(status: 'approved' | 'rejected', reason?: string) {
24
- this.status = status;
25
- this.reason = reason;
26
- }
27
- }
28
-
29
- const TOOL_MAP: Record<string, ToolKind> = {
30
- 'write': 'Write',
31
- 'edit': 'Edit',
32
- };
33
-
34
- function mapToolName(openclawName: string): ToolKind | null {
35
- return TOOL_MAP[openclawName] || null;
36
- }
37
-
38
- // webpieces-disable no-any-unknown -- openclaw SDK passes opaque tool arguments
39
- function mapToolInput(toolName: string, args: Record<string, unknown>): NormalizedToolInput | null {
40
- const filePath = typeof args['path'] === 'string' ? args['path'] as string : null;
41
- if (!filePath) return null;
42
-
43
- if (toolName === 'write') {
44
- const content = typeof args['content'] === 'string' ? args['content'] as string : '';
45
- return new NormalizedToolInput(filePath, [new NormalizedEdit('', content)]);
46
- }
47
- if (toolName === 'edit') {
48
- const oldStr = typeof args['old_string'] === 'string' ? args['old_string'] as string : '';
49
- const newStr = typeof args['new_string'] === 'string' ? args['new_string'] as string : '';
50
- return new NormalizedToolInput(filePath, [new NormalizedEdit(oldStr, newStr)]);
51
- }
52
- return null;
53
- }
54
-
55
- function findWorkspaceRoot(filePath: string): string | null {
56
- let dir = path.dirname(filePath);
57
- while (true) {
58
- if (fs.existsSync(path.join(dir, 'webpieces.ai-hooks.json'))) return dir;
59
- const parent = path.dirname(dir);
60
- if (parent === dir) return null;
61
- dir = parent;
62
- }
63
- }
64
-
65
- export default async function handler(
66
- event: ToolCallEvent,
67
- _context: HookContext,
68
- ): Promise<OpenclawHandlerResult | undefined> {
69
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
70
- try {
71
- const toolKind = mapToolName(event.toolName);
72
- if (!toolKind) return undefined;
73
-
74
- const input = mapToolInput(event.toolName, event.arguments);
75
- if (!input) return undefined;
76
-
77
- const wsRoot = findWorkspaceRoot(input.filePath);
78
- if (!wsRoot) return undefined;
79
-
80
- const result = run(toolKind, input, wsRoot);
81
- if (!result) return new OpenclawHandlerResult('approved');
82
- return new OpenclawHandlerResult('rejected', result.report);
83
- } catch (err: unknown) {
84
- const error = toError(err);
85
- console.error(`[ai-hooks] openclaw adapter crashed (failing open): ${error.message}`);
86
- return undefined;
87
- }
88
- }
@@ -1,114 +0,0 @@
1
- /* eslint-disable @webpieces/max-method-lines -- test describe blocks are inherently large */
2
- import { parseDirectives } from '../disable-directives';
3
-
4
- describe('parseDirectives', () => {
5
- it('inline disable on same line as code', () => {
6
- const d = parseDirectives('const x: any = 1; // ai-hook-disable no-any-unknown -- legacy');
7
- expect(d.isLineDisabled(1, 'no-any-unknown')).toBe(true);
8
- expect(d.isLineDisabled(1, 'no-destructure')).toBe(false);
9
- });
10
-
11
- it('line-above disable affects next non-blank line', () => {
12
- const d = parseDirectives('// ai-hook-disable no-any-unknown -- reason\nconst x: any = 1;');
13
- expect(d.isLineDisabled(2, 'no-any-unknown')).toBe(true);
14
- expect(d.isLineDisabled(1, 'no-any-unknown')).toBe(false);
15
- });
16
-
17
- it('skips blank lines before target', () => {
18
- const d = parseDirectives('// ai-hook-disable no-any-unknown -- reason\n\n\nconst x: any = 1;');
19
- expect(d.isLineDisabled(4, 'no-any-unknown')).toBe(true);
20
- });
21
-
22
- it('handles comma-separated rules', () => {
23
- const d = parseDirectives('const { a } = obj; // ai-hook-disable no-destructure, no-any-unknown -- reason');
24
- expect(d.isLineDisabled(1, 'no-destructure')).toBe(true);
25
- expect(d.isLineDisabled(1, 'no-any-unknown')).toBe(true);
26
- expect(d.isLineDisabled(1, 'other-rule')).toBe(false);
27
- });
28
-
29
- it('ai-hook-disable-next explicit form', () => {
30
- const d = parseDirectives('// ai-hook-disable-next no-any-unknown -- reason\nconst x: any = 1;');
31
- expect(d.isLineDisabled(2, 'no-any-unknown')).toBe(true);
32
- });
33
-
34
- it('ai-hook-disable-file within first 20 lines', () => {
35
- const src = '// ai-hook-disable-file no-any-unknown -- wraps API\nconst a = 1;\nconst b: any = 2;\nconst c: any = 3;';
36
- const d = parseDirectives(src);
37
- expect(d.isLineDisabled(3, 'no-any-unknown')).toBe(true);
38
- expect(d.isLineDisabled(4, 'no-any-unknown')).toBe(true);
39
- expect(d.isLineDisabled(3, 'no-destructure')).toBe(false);
40
- });
41
-
42
- it('ai-hook-disable-file beyond line 20 is ignored', () => {
43
- const filler = Array(25).fill('const a = 1;').join('\n');
44
- const src = filler + '\n// ai-hook-disable-file no-any-unknown -- too late\nconst x: any = 1;';
45
- const d = parseDirectives(src);
46
- expect(d.isLineDisabled(27, 'no-any-unknown')).toBe(false);
47
- });
48
-
49
- it('ai-hook-disable-all on its own line', () => {
50
- const d = parseDirectives('// ai-hook-disable-all -- hack\nconst x: any = 1;');
51
- expect(d.isLineDisabled(2, 'no-any-unknown')).toBe(true);
52
- expect(d.isLineDisabled(2, 'require-return-type')).toBe(true);
53
- });
54
-
55
- it('ai-hook-disable-all inline', () => {
56
- const d = parseDirectives('const x: any = 1; // ai-hook-disable-all -- hack');
57
- expect(d.isLineDisabled(1, 'no-any-unknown')).toBe(true);
58
- expect(d.isLineDisabled(1, 'anything-else')).toBe(true);
59
- });
60
-
61
- it('no directives present', () => {
62
- const d = parseDirectives('const x: any = 1;');
63
- expect(d.isLineDisabled(1, 'no-any-unknown')).toBe(false);
64
- });
65
-
66
- it('star rule name matches anything', () => {
67
- const d = parseDirectives('const x: any = 1; // ai-hook-disable * -- nuclear');
68
- expect(d.isLineDisabled(1, 'no-any-unknown')).toBe(true);
69
- expect(d.isLineDisabled(1, 'require-return-type')).toBe(true);
70
- });
71
-
72
- it('directive without reason still parses', () => {
73
- const d = parseDirectives('const x: any = 1; // ai-hook-disable no-any-unknown');
74
- expect(d.isLineDisabled(1, 'no-any-unknown')).toBe(true);
75
- });
76
-
77
- it('chained disable comments skip each other', () => {
78
- const src = '// ai-hook-disable no-any-unknown -- r1\n// ai-hook-disable no-destructure -- r2\nconst { a }: any = obj;';
79
- const d = parseDirectives(src);
80
- expect(d.isLineDisabled(3, 'no-any-unknown')).toBe(true);
81
- expect(d.isLineDisabled(3, 'no-destructure')).toBe(true);
82
- });
83
-
84
- it('webpieces-disable inline works same as ai-hook-disable', () => {
85
- const d = parseDirectives('const x: any = 1; // webpieces-disable no-any-unknown -- legacy');
86
- expect(d.isLineDisabled(1, 'no-any-unknown')).toBe(true);
87
- expect(d.isLineDisabled(1, 'no-destructure')).toBe(false);
88
- });
89
-
90
- it('webpieces-disable on line above', () => {
91
- const d = parseDirectives('// webpieces-disable no-any-unknown -- reason\nconst x: any = 1;');
92
- expect(d.isLineDisabled(2, 'no-any-unknown')).toBe(true);
93
- expect(d.isLineDisabled(1, 'no-any-unknown')).toBe(false);
94
- });
95
-
96
- it('webpieces-disable-file within first 20 lines', () => {
97
- const src = '// webpieces-disable-file no-any-unknown -- wraps API\nconst a = 1;\nconst b: any = 2;';
98
- const d = parseDirectives(src);
99
- expect(d.isLineDisabled(3, 'no-any-unknown')).toBe(true);
100
- });
101
-
102
- it('webpieces-disable-all suppresses all rules', () => {
103
- const d = parseDirectives('// webpieces-disable-all -- hack\nconst x: any = 1;');
104
- expect(d.isLineDisabled(2, 'no-any-unknown')).toBe(true);
105
- expect(d.isLineDisabled(2, 'require-return-type')).toBe(true);
106
- });
107
-
108
- it('webpieces-disable chained with ai-hook-disable', () => {
109
- const src = '// webpieces-disable no-any-unknown -- r1\n// ai-hook-disable no-destructure -- r2\nconst { a }: any = obj;';
110
- const d = parseDirectives(src);
111
- expect(d.isLineDisabled(3, 'no-any-unknown')).toBe(true);
112
- expect(d.isLineDisabled(3, 'no-destructure')).toBe(true);
113
- });
114
- });
@@ -1,90 +0,0 @@
1
- /* eslint-disable @webpieces/max-method-lines -- test describe blocks are inherently large */
2
- import { run } from '../../runner';
3
- import { NormalizedToolInput, NormalizedEdit } from '../../types';
4
- import * as fs from 'fs';
5
- import * as os from 'os';
6
- import * as path from 'path';
7
-
8
- function ws(): string {
9
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'file-loc-'));
10
- fs.writeFileSync(path.join(dir, 'webpieces.ai-hooks.json'), JSON.stringify({
11
- rules: { 'no-any-unknown': { enabled: false }, 'max-file-lines': { enabled: false },
12
- 'no-destructure': { enabled: false }, 'require-return-type': { enabled: false },
13
- 'no-unmanaged-exceptions': { enabled: false },
14
- 'file-location': { enabled: true, allowedRootFiles: ['jest.setup.ts'], excludePaths: ['scripts', 'tmp'] } },
15
- rulesDir: [],
16
- }));
17
- return dir;
18
- }
19
-
20
- describe('file-location rule', () => {
21
- it('blocks Write to root (orphan)', () => {
22
- const w = ws();
23
- const r = run('Write', new NormalizedToolInput(path.join(w, 'orphan.ts'), [new NormalizedEdit('', 'const x = 1;')]), w);
24
- expect(r).not.toBeNull();
25
- expect(r!.report).toContain('file-location');
26
- expect(r!.report).toContain('not inside any Nx project');
27
- });
28
-
29
- it('allows Write to allowedRootFiles', () => {
30
- const w = ws();
31
- const r = run('Write', new NormalizedToolInput(path.join(w, 'jest.setup.ts'), [new NormalizedEdit('', 'const x = 1;')]), w);
32
- expect(r).toBeNull();
33
- });
34
-
35
- it('allows Write under src/ of a project', () => {
36
- const w = ws();
37
- const projectDir = path.join(w, 'packages', 'mylib');
38
- fs.mkdirSync(path.join(projectDir, 'src'), { recursive: true });
39
- fs.writeFileSync(path.join(projectDir, 'project.json'), '{}');
40
- const r = run('Write', new NormalizedToolInput(
41
- path.join(projectDir, 'src', 'foo.ts'),
42
- [new NormalizedEdit('', 'const x = 1;')],
43
- ), w);
44
- expect(r).toBeNull();
45
- });
46
-
47
- it('blocks Write outside src/ of a project', () => {
48
- const w = ws();
49
- const projectDir = path.join(w, 'packages', 'mylib');
50
- fs.mkdirSync(path.join(projectDir, 'src'), { recursive: true });
51
- fs.writeFileSync(path.join(projectDir, 'project.json'), '{}');
52
- const r = run('Write', new NormalizedToolInput(
53
- path.join(projectDir, 'stray.ts'),
54
- [new NormalizedEdit('', 'const x = 1;')],
55
- ), w);
56
- expect(r).not.toBeNull();
57
- expect(r!.report).toContain('outside its src/ directory');
58
- });
59
-
60
- it('allows jest.config.ts at project root', () => {
61
- const w = ws();
62
- const projectDir = path.join(w, 'packages', 'mylib');
63
- fs.mkdirSync(path.join(projectDir, 'src'), { recursive: true });
64
- fs.writeFileSync(path.join(projectDir, 'project.json'), '{}');
65
- const r = run('Write', new NormalizedToolInput(
66
- path.join(projectDir, 'jest.config.ts'),
67
- [new NormalizedEdit('', 'export default {};')],
68
- ), w);
69
- expect(r).toBeNull();
70
- });
71
-
72
- it('skips excluded top-level dirs', () => {
73
- const w = ws();
74
- const r = run('Write', new NormalizedToolInput(
75
- path.join(w, 'scripts', 'tool.ts'),
76
- [new NormalizedEdit('', 'const x = 1;')],
77
- ), w);
78
- expect(r).toBeNull();
79
- });
80
-
81
- it('does not fire on Edit (file already exists)', () => {
82
- const w = ws();
83
- const target = path.join(w, 'orphan.ts');
84
- fs.writeFileSync(target, 'const old = 1;');
85
- const r = run('Edit', new NormalizedToolInput(target, [
86
- new NormalizedEdit('const old = 1;', 'const updated = 2;'),
87
- ]), w);
88
- expect(r).toBeNull();
89
- });
90
- });
@@ -1,53 +0,0 @@
1
- import { run } from '../../runner';
2
- import { NormalizedToolInput, NormalizedEdit } from '../../types';
3
- import * as fs from 'fs';
4
- import * as os from 'os';
5
- import * as path from 'path';
6
-
7
- function ws(limit: number = 10): string {
8
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'max-file-'));
9
- fs.writeFileSync(path.join(dir, 'webpieces.ai-hooks.json'), JSON.stringify({
10
- rules: { 'no-any-unknown': { enabled: false }, 'file-location': { enabled: false },
11
- 'no-destructure': { enabled: false }, 'require-return-type': { enabled: false },
12
- 'no-unmanaged-exceptions': { enabled: false },
13
- 'max-file-lines': { enabled: true, limit } },
14
- rulesDir: [],
15
- }));
16
- return dir;
17
- }
18
-
19
- describe('max-file-lines rule', () => {
20
- it('blocks a file that exceeds the limit', () => {
21
- const w = ws(5);
22
- const content = Array(10).fill('const x = 1;').join('\n');
23
- const r = run('Write', new NormalizedToolInput(path.join(w, 'big.ts'), [new NormalizedEdit('', content)]), w);
24
- expect(r).not.toBeNull();
25
- expect(r!.report).toContain('max-file-lines');
26
- expect(r!.report).toContain('10 lines');
27
- });
28
-
29
- it('allows a file within the limit', () => {
30
- const w = ws(20);
31
- const content = Array(5).fill('const x = 1;').join('\n');
32
- const r = run('Write', new NormalizedToolInput(path.join(w, 'small.ts'), [new NormalizedEdit('', content)]), w);
33
- expect(r).toBeNull();
34
- });
35
-
36
- it('computes projected lines for Edit (current + added - removed)', () => {
37
- const w = ws(5);
38
- const target = path.join(w, 'existing.ts');
39
- fs.writeFileSync(target, Array(4).fill('const x = 1;').join('\n'));
40
- const r = run('Edit', new NormalizedToolInput(target, [
41
- new NormalizedEdit('const x = 1;', 'const a = 1;\nconst b = 2;\nconst c = 3;'),
42
- ]), w);
43
- expect(r).not.toBeNull();
44
- expect(r!.report).toContain('6 lines');
45
- });
46
-
47
- it('ignores non-ts files', () => {
48
- const w = ws(3);
49
- const content = Array(10).fill('line').join('\n');
50
- const r = run('Write', new NormalizedToolInput(path.join(w, 'big.md'), [new NormalizedEdit('', content)]), w);
51
- expect(r).toBeNull();
52
- });
53
- });
@@ -1,68 +0,0 @@
1
- import { run } from '../../runner';
2
- import { NormalizedToolInput, NormalizedEdit } from '../../types';
3
- import * as fs from 'fs';
4
- import * as os from 'os';
5
- import * as path from 'path';
6
-
7
- function ws(): string {
8
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'no-any-'));
9
- fs.writeFileSync(path.join(dir, 'webpieces.ai-hooks.json'), JSON.stringify({
10
- rules: { 'max-file-lines': { enabled: false }, 'file-location': { enabled: false },
11
- 'no-destructure': { enabled: false }, 'require-return-type': { enabled: false },
12
- 'no-unmanaged-exceptions': { enabled: false } },
13
- rulesDir: [],
14
- }));
15
- return dir;
16
- }
17
-
18
- describe('no-any rule', () => {
19
- it('blocks : any', () => {
20
- const w = ws();
21
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', 'const x: any = 1;')]), w);
22
- expect(r).not.toBeNull();
23
- expect(r!.report).toContain('no-any-unknown');
24
- });
25
-
26
- it('blocks as any', () => {
27
- const w = ws();
28
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', 'const x = y as any;')]), w);
29
- expect(r).not.toBeNull();
30
- });
31
-
32
- it('blocks Array<any>', () => {
33
- const w = ws();
34
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', 'const x: Array<any> = [];')]), w);
35
- expect(r).not.toBeNull();
36
- });
37
-
38
- it('allows any in a string', () => {
39
- const w = ws();
40
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', 'const x = "has any keyword";')]), w);
41
- expect(r).toBeNull();
42
- });
43
-
44
- it('allows any in a comment', () => {
45
- const w = ws();
46
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', '// any is fine here\nconst x = 1;')]), w);
47
- expect(r).toBeNull();
48
- });
49
-
50
- it('respects ai-hook-disable', () => {
51
- const w = ws();
52
- const content = '// ai-hook-disable no-any-unknown -- legacy\nconst x: any = 1;';
53
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', content)]), w);
54
- expect(r).toBeNull();
55
- });
56
-
57
- it('allows unknown keyword', () => {
58
- const w = ws();
59
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', 'const x: unknown = 1;')]), w);
60
- expect(r).toBeNull();
61
- });
62
-
63
- it('ignores non-ts files', () => {
64
- const w = ws();
65
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.md'), [new NormalizedEdit('', 'const x: any = 1;')]), w);
66
- expect(r).toBeNull();
67
- });
68
- });
@@ -1,50 +0,0 @@
1
- import { run } from '../../runner';
2
- import { NormalizedToolInput, NormalizedEdit } from '../../types';
3
- import * as fs from 'fs';
4
- import * as os from 'os';
5
- import * as path from 'path';
6
-
7
- function ws(): string {
8
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'no-destr-'));
9
- fs.writeFileSync(path.join(dir, 'webpieces.ai-hooks.json'), JSON.stringify({
10
- rules: { 'no-any-unknown': { enabled: false }, 'max-file-lines': { enabled: false },
11
- 'file-location': { enabled: false }, 'require-return-type': { enabled: false },
12
- 'no-unmanaged-exceptions': { enabled: false } },
13
- rulesDir: [],
14
- }));
15
- return dir;
16
- }
17
-
18
- describe('no-destructure rule', () => {
19
- it('blocks const { x } = obj', () => {
20
- const w = ws();
21
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', 'const { x } = obj;')]), w);
22
- expect(r).not.toBeNull();
23
- expect(r!.report).toContain('no-destructure');
24
- });
25
-
26
- it('blocks let { x } = obj', () => {
27
- const w = ws();
28
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', 'let { a, b } = obj;')]), w);
29
- expect(r).not.toBeNull();
30
- });
31
-
32
- it('allows const x = obj.x (no destructure)', () => {
33
- const w = ws();
34
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', 'const x = obj.x;')]), w);
35
- expect(r).toBeNull();
36
- });
37
-
38
- it('respects ai-hook-disable', () => {
39
- const w = ws();
40
- const content = '// ai-hook-disable no-destructure -- needed for spread\nconst { a, ...rest } = obj;';
41
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', content)]), w);
42
- expect(r).toBeNull();
43
- });
44
-
45
- it('allows object literal (not destructure)', () => {
46
- const w = ws();
47
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', 'const obj = { x: 1, y: 2 };')]), w);
48
- expect(r).toBeNull();
49
- });
50
- });
@@ -1,118 +0,0 @@
1
- /* eslint-disable @webpieces/max-method-lines -- test describe blocks are inherently large */
2
- import * as fs from 'fs';
3
- import * as os from 'os';
4
- import * as path from 'path';
5
-
6
- import { runBash } from '../../runner';
7
-
8
- function makeWorkspace(): string {
9
- const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-hooks-bash-test-'));
10
- fs.writeFileSync(
11
- path.join(ws, 'webpieces.ai-hooks.json'),
12
- JSON.stringify({
13
- rules: {
14
- 'no-any-unknown': { enabled: false },
15
- 'max-file-lines': { enabled: false },
16
- 'file-location': { enabled: false },
17
- 'no-destructure': { enabled: false },
18
- 'require-return-type': { enabled: false },
19
- 'no-unmanaged-exceptions': { enabled: false },
20
- 'catch-error-pattern': { enabled: false },
21
- 'no-shell-substitution': { enabled: true },
22
- },
23
- rulesDir: [],
24
- }),
25
- );
26
- return ws;
27
- }
28
-
29
- describe('no-shell-substitution', () => {
30
- it('blocks $(...) command substitution', () => {
31
- const ws = makeWorkspace();
32
- const result = runBash('echo $(date)', ws);
33
- expect(result).not.toBeNull();
34
- expect(result!.report).toContain('no-shell-substitution');
35
- expect(result!.report).toContain('$(...)');
36
- });
37
-
38
- it('blocks backtick substitution', () => {
39
- const ws = makeWorkspace();
40
- const result = runBash('echo `date`', ws);
41
- expect(result).not.toBeNull();
42
- expect(result!.report).toContain('backtick');
43
- });
44
-
45
- it('blocks $VAR expansion', () => {
46
- const ws = makeWorkspace();
47
- const result = runBash('echo $HOME', ws);
48
- expect(result).not.toBeNull();
49
- expect(result!.report).toContain('variable expansion');
50
- });
51
-
52
- it('blocks ${VAR} expansion', () => {
53
- const ws = makeWorkspace();
54
- const result = runBash('echo ${PATH}', ws);
55
- expect(result).not.toBeNull();
56
- expect(result!.report).toContain('variable expansion');
57
- });
58
-
59
- it('allows plain commands', () => {
60
- const ws = makeWorkspace();
61
- expect(runBash('git status', ws)).toBeNull();
62
- expect(runBash('ls -la', ws)).toBeNull();
63
- expect(runBash('pnpm nx build config', ws)).toBeNull();
64
- });
65
-
66
- it('allows single-quoted literals containing $ and backticks', () => {
67
- const ws = makeWorkspace();
68
- expect(runBash("grep '$pattern' file.txt", ws)).toBeNull();
69
- expect(runBash("echo 'literal `backtick` here'", ws)).toBeNull();
70
- });
71
-
72
- it('allows escaped $ in double-quoted strings', () => {
73
- const ws = makeWorkspace();
74
- expect(runBash('echo "price is \\$5"', ws)).toBeNull();
75
- });
76
-
77
- it('allows empty-looking commands with $ in non-variable position', () => {
78
- const ws = makeWorkspace();
79
- expect(runBash('echo hello', ws)).toBeNull();
80
- });
81
-
82
- it('returns null when rule is disabled', () => {
83
- const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-hooks-bash-disabled-'));
84
- fs.writeFileSync(
85
- path.join(ws, 'webpieces.ai-hooks.json'),
86
- JSON.stringify({
87
- rules: {
88
- 'no-any-unknown': { enabled: false },
89
- 'max-file-lines': { enabled: false },
90
- 'file-location': { enabled: false },
91
- 'no-destructure': { enabled: false },
92
- 'require-return-type': { enabled: false },
93
- 'no-unmanaged-exceptions': { enabled: false },
94
- 'catch-error-pattern': { enabled: false },
95
- 'no-shell-substitution': { enabled: false },
96
- },
97
- rulesDir: [],
98
- }),
99
- );
100
- const result = runBash('echo $(date)', ws);
101
- expect(result).toBeNull();
102
- });
103
-
104
- it('returns null when no config in tree', () => {
105
- const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-hooks-bash-noconfig-'));
106
- const result = runBash('echo $(date)', ws);
107
- expect(result).toBeNull();
108
- });
109
-
110
- it('reports multiple violation categories for combined command', () => {
111
- const ws = makeWorkspace();
112
- const result = runBash('echo $(date) "$HOME" `whoami`', ws);
113
- expect(result).not.toBeNull();
114
- expect(result!.report).toContain('$(...)');
115
- expect(result!.report).toContain('backtick');
116
- expect(result!.report).toContain('variable expansion');
117
- });
118
- });
@@ -1,54 +0,0 @@
1
- import { run } from '../../runner';
2
- import { NormalizedToolInput, NormalizedEdit } from '../../types';
3
- import * as fs from 'fs';
4
- import * as os from 'os';
5
- import * as path from 'path';
6
-
7
- function ws(): string {
8
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'no-exc-'));
9
- fs.writeFileSync(path.join(dir, 'webpieces.ai-hooks.json'), JSON.stringify({
10
- rules: { 'no-any-unknown': { enabled: false }, 'max-file-lines': { enabled: false },
11
- 'file-location': { enabled: false }, 'no-destructure': { enabled: false },
12
- 'require-return-type': { enabled: false }, 'catch-error-pattern': { enabled: false } },
13
- rulesDir: [],
14
- }));
15
- return dir;
16
- }
17
-
18
- describe('no-unmanaged-exceptions rule', () => {
19
- it('blocks try/catch without disable comment', () => {
20
- const w = ws();
21
- const content = 'try {\n doSomething();\n} catch (e) {\n console.error(e);\n}';
22
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', content)]), w);
23
- expect(r).not.toBeNull();
24
- expect(r!.report).toContain('no-unmanaged-exceptions');
25
- });
26
-
27
- it('allows try with eslint-disable-next-line', () => {
28
- const w = ws();
29
- const content = '// eslint-disable-next-line @webpieces/no-unmanaged-exceptions\ntry {\n doSomething();\n} catch (e) {}';
30
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', content)]), w);
31
- expect(r).toBeNull();
32
- });
33
-
34
- it('allows try with ai-hook-disable', () => {
35
- const w = ws();
36
- const content = '// ai-hook-disable no-unmanaged-exceptions -- tested externally\ntry {\n doSomething();\n} catch (e) {}';
37
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', content)]), w);
38
- expect(r).toBeNull();
39
- });
40
-
41
- it('does not fire on the word try in a string', () => {
42
- const w = ws();
43
- const content = 'const msg = "try again later";';
44
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', content)]), w);
45
- expect(r).toBeNull();
46
- });
47
-
48
- it('does not fire on the word try in a comment', () => {
49
- const w = ws();
50
- const content = '// try this approach instead\nconst x = 1;';
51
- const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', content)]), w);
52
- expect(r).toBeNull();
53
- });
54
- });