@sentry/warden 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/.agents/skills/find-bugs/SKILL.md +75 -0
  2. package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
  3. package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
  4. package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  5. package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  6. package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  7. package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  8. package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  9. package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  10. package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  11. package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  12. package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  13. package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  14. package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  15. package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  16. package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  17. package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  18. package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  19. package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  20. package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  21. package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  22. package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  23. package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  24. package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  25. package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  26. package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  27. package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  28. package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  29. package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  30. package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  31. package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  32. package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  33. package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  34. package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  35. package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  36. package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  37. package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  38. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  39. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  40. package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  41. package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  42. package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  43. package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  44. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  45. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  46. package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  47. package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  48. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  49. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  50. package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  51. package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  52. package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  53. package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  54. package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  55. package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  56. package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  57. package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  58. package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  59. package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  60. package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  61. package/.claude/settings.json +57 -0
  62. package/.claude/settings.local.json +88 -0
  63. package/.claude/skills/agent-prompt/SKILL.md +54 -0
  64. package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
  65. package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
  66. package/.claude/skills/agent-prompt/references/context-design.md +124 -0
  67. package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
  68. package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
  69. package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
  70. package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
  71. package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
  72. package/.claude/skills/notseer/SKILL.md +131 -0
  73. package/.claude/skills/skill-writer/SKILL.md +140 -0
  74. package/.claude/skills/testing-guidelines/SKILL.md +132 -0
  75. package/.claude/skills/warden-skill/SKILL.md +250 -0
  76. package/.claude/skills/warden-skill/references/config-schema.md +133 -0
  77. package/.dex/config.toml +2 -0
  78. package/.github/workflows/ci.yml +33 -0
  79. package/.github/workflows/release.yml +54 -0
  80. package/.github/workflows/warden.yml +40 -0
  81. package/AGENTS.md +89 -0
  82. package/CONTRIBUTING.md +60 -0
  83. package/LICENSE +105 -0
  84. package/README.md +43 -0
  85. package/SPEC.md +263 -0
  86. package/action.yml +87 -0
  87. package/assets/favicon.png +0 -0
  88. package/assets/warden-icon-bw.svg +5 -0
  89. package/assets/warden-icon-purple.png +0 -0
  90. package/assets/warden-icon-purple.svg +5 -0
  91. package/docs/.claude/settings.local.json +11 -0
  92. package/docs/astro.config.mjs +43 -0
  93. package/docs/package.json +19 -0
  94. package/docs/pnpm-lock.yaml +4000 -0
  95. package/docs/public/favicon.svg +5 -0
  96. package/docs/src/components/Code.astro +141 -0
  97. package/docs/src/components/PackageManagerTabs.astro +183 -0
  98. package/docs/src/components/Terminal.astro +212 -0
  99. package/docs/src/layouts/Base.astro +380 -0
  100. package/docs/src/pages/cli.astro +167 -0
  101. package/docs/src/pages/config.astro +394 -0
  102. package/docs/src/pages/guide.astro +449 -0
  103. package/docs/src/pages/index.astro +490 -0
  104. package/docs/src/styles/global.css +551 -0
  105. package/docs/tsconfig.json +3 -0
  106. package/docs/vercel.json +5 -0
  107. package/eslint.config.js +33 -0
  108. package/package.json +73 -0
  109. package/src/action/index.ts +1 -0
  110. package/src/action/main.ts +868 -0
  111. package/src/cli/args.test.ts +477 -0
  112. package/src/cli/args.ts +415 -0
  113. package/src/cli/commands/add.ts +447 -0
  114. package/src/cli/commands/init.test.ts +136 -0
  115. package/src/cli/commands/init.ts +132 -0
  116. package/src/cli/commands/setup-app/browser.ts +38 -0
  117. package/src/cli/commands/setup-app/credentials.ts +45 -0
  118. package/src/cli/commands/setup-app/manifest.ts +48 -0
  119. package/src/cli/commands/setup-app/server.ts +172 -0
  120. package/src/cli/commands/setup-app.ts +156 -0
  121. package/src/cli/commands/sync.ts +114 -0
  122. package/src/cli/context.ts +131 -0
  123. package/src/cli/files.test.ts +155 -0
  124. package/src/cli/files.ts +89 -0
  125. package/src/cli/fix.test.ts +310 -0
  126. package/src/cli/fix.ts +387 -0
  127. package/src/cli/git.test.ts +119 -0
  128. package/src/cli/git.ts +318 -0
  129. package/src/cli/index.ts +14 -0
  130. package/src/cli/main.ts +672 -0
  131. package/src/cli/output/box.ts +235 -0
  132. package/src/cli/output/formatters.test.ts +187 -0
  133. package/src/cli/output/formatters.ts +269 -0
  134. package/src/cli/output/icons.ts +13 -0
  135. package/src/cli/output/index.ts +44 -0
  136. package/src/cli/output/ink-runner.tsx +337 -0
  137. package/src/cli/output/jsonl.test.ts +347 -0
  138. package/src/cli/output/jsonl.ts +126 -0
  139. package/src/cli/output/reporter.ts +435 -0
  140. package/src/cli/output/tasks.ts +374 -0
  141. package/src/cli/output/tty.test.ts +117 -0
  142. package/src/cli/output/tty.ts +60 -0
  143. package/src/cli/output/verbosity.test.ts +40 -0
  144. package/src/cli/output/verbosity.ts +31 -0
  145. package/src/cli/terminal.test.ts +148 -0
  146. package/src/cli/terminal.ts +301 -0
  147. package/src/config/index.ts +3 -0
  148. package/src/config/loader.test.ts +313 -0
  149. package/src/config/loader.ts +103 -0
  150. package/src/config/schema.ts +168 -0
  151. package/src/config/writer.test.ts +119 -0
  152. package/src/config/writer.ts +84 -0
  153. package/src/diff/classify.test.ts +162 -0
  154. package/src/diff/classify.ts +92 -0
  155. package/src/diff/coalesce.test.ts +208 -0
  156. package/src/diff/coalesce.ts +133 -0
  157. package/src/diff/context.test.ts +226 -0
  158. package/src/diff/context.ts +201 -0
  159. package/src/diff/index.ts +4 -0
  160. package/src/diff/parser.test.ts +212 -0
  161. package/src/diff/parser.ts +149 -0
  162. package/src/event/context.ts +132 -0
  163. package/src/event/index.ts +2 -0
  164. package/src/event/schedule-context.ts +101 -0
  165. package/src/examples/examples.integration.test.ts +66 -0
  166. package/src/examples/index.test.ts +101 -0
  167. package/src/examples/index.ts +122 -0
  168. package/src/examples/setup.ts +25 -0
  169. package/src/index.ts +115 -0
  170. package/src/output/dedup.test.ts +419 -0
  171. package/src/output/dedup.ts +607 -0
  172. package/src/output/github-checks.test.ts +300 -0
  173. package/src/output/github-checks.ts +476 -0
  174. package/src/output/github-issues.ts +329 -0
  175. package/src/output/index.ts +5 -0
  176. package/src/output/issue-renderer.ts +197 -0
  177. package/src/output/renderer.test.ts +727 -0
  178. package/src/output/renderer.ts +217 -0
  179. package/src/output/stale.test.ts +375 -0
  180. package/src/output/stale.ts +155 -0
  181. package/src/output/types.ts +34 -0
  182. package/src/sdk/index.ts +1 -0
  183. package/src/sdk/runner.test.ts +806 -0
  184. package/src/sdk/runner.ts +1232 -0
  185. package/src/skills/index.ts +36 -0
  186. package/src/skills/loader.test.ts +300 -0
  187. package/src/skills/loader.ts +423 -0
  188. package/src/skills/remote.test.ts +704 -0
  189. package/src/skills/remote.ts +604 -0
  190. package/src/triggers/matcher.test.ts +277 -0
  191. package/src/triggers/matcher.ts +152 -0
  192. package/src/types/index.ts +194 -0
  193. package/src/utils/async.ts +18 -0
  194. package/src/utils/index.test.ts +84 -0
  195. package/src/utils/index.ts +50 -0
  196. package/tsconfig.json +25 -0
  197. package/vitest.config.ts +8 -0
  198. package/vitest.integration.config.ts +11 -0
  199. package/warden.toml +19 -0
@@ -0,0 +1,477 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { parseCliArgs, CLIOptionsSchema, detectTargetType, classifyTargets } from './args.js';
4
+
5
+ vi.mock('node:fs', async () => {
6
+ const actual = await vi.importActual('node:fs');
7
+ return {
8
+ ...actual,
9
+ existsSync: vi.fn(() => false),
10
+ };
11
+ });
12
+
13
+ describe('parseCliArgs', () => {
14
+ const originalExit = process.exit;
15
+ const originalError = console.error;
16
+
17
+ beforeEach(() => {
18
+ process.exit = vi.fn() as never;
19
+ console.error = vi.fn();
20
+ });
21
+
22
+ afterEach(() => {
23
+ process.exit = originalExit;
24
+ console.error = originalError;
25
+ });
26
+
27
+ it('parses with no arguments', () => {
28
+ const result = parseCliArgs([]);
29
+ expect(result.command).toBe('run');
30
+ expect(result.options.targets).toBeUndefined();
31
+ });
32
+
33
+ it('parses file target with skill', () => {
34
+ const result = parseCliArgs(['src/auth.ts', '--skill', 'security-review']);
35
+ expect(result.options.targets).toEqual(['src/auth.ts']);
36
+ expect(result.options.skill).toBe('security-review');
37
+ });
38
+
39
+ it('parses multiple file targets', () => {
40
+ const result = parseCliArgs(['file1.ts', 'file2.ts', '--skill', 'security-review']);
41
+ expect(result.options.targets).toEqual(['file1.ts', 'file2.ts']);
42
+ });
43
+
44
+ it('parses glob pattern', () => {
45
+ const result = parseCliArgs(['src/**/*.ts', '--skill', 'security-review']);
46
+ expect(result.options.targets).toEqual(['src/**/*.ts']);
47
+ });
48
+
49
+ it('parses git ref target', () => {
50
+ const result = parseCliArgs(['HEAD~3', '--skill', 'security-review']);
51
+ expect(result.options.targets).toEqual(['HEAD~3']);
52
+ });
53
+
54
+ it('parses git range target', () => {
55
+ const result = parseCliArgs(['main..feature', '--skill', 'security-review']);
56
+ expect(result.options.targets).toEqual(['main..feature']);
57
+ });
58
+
59
+ it('parses --skill option', () => {
60
+ const result = parseCliArgs(['--skill', 'security-review']);
61
+ expect(result.options.skill).toBe('security-review');
62
+ });
63
+
64
+ it('parses --config option', () => {
65
+ const result = parseCliArgs(['--config', './custom.toml']);
66
+ expect(result.options.config).toBe('./custom.toml');
67
+ });
68
+
69
+ it('parses --json flag', () => {
70
+ const result = parseCliArgs(['--json']);
71
+ expect(result.options.json).toBe(true);
72
+ });
73
+
74
+ it('parses --fail-on option', () => {
75
+ const result = parseCliArgs(['--fail-on', 'high']);
76
+ expect(result.options.failOn).toBe('high');
77
+ });
78
+
79
+ it('parses help command', () => {
80
+ const result = parseCliArgs(['help']);
81
+ expect(result.command).toBe('help');
82
+ });
83
+
84
+ it('parses --help flag', () => {
85
+ const result = parseCliArgs(['--help']);
86
+ expect(result.command).toBe('help');
87
+ });
88
+
89
+ it('parses -h flag', () => {
90
+ const result = parseCliArgs(['-h']);
91
+ expect(result.command).toBe('help');
92
+ });
93
+
94
+ it('ignores run command for backward compat', () => {
95
+ const result = parseCliArgs(['run', '--skill', 'security-review']);
96
+ expect(result.options.targets).toBeUndefined();
97
+ expect(result.options.skill).toBe('security-review');
98
+ });
99
+
100
+ it('allows targets without --skill (runs all skills)', () => {
101
+ const result = parseCliArgs(['src/auth.ts']);
102
+ expect(result.options.targets).toEqual(['src/auth.ts']);
103
+ expect(result.options.skill).toBeUndefined();
104
+ });
105
+
106
+ it('parses --parallel option', () => {
107
+ const result = parseCliArgs(['--parallel', '8']);
108
+ expect(result.options.parallel).toBe(8);
109
+ });
110
+
111
+ it('does not set parallel when not provided', () => {
112
+ const result = parseCliArgs([]);
113
+ expect(result.options.parallel).toBeUndefined();
114
+ });
115
+
116
+ it('parses --quiet flag', () => {
117
+ const result = parseCliArgs(['--quiet']);
118
+ expect(result.options.quiet).toBe(true);
119
+ });
120
+
121
+ it('parses single -v flag', () => {
122
+ const result = parseCliArgs(['-v']);
123
+ expect(result.options.verbose).toBe(1);
124
+ });
125
+
126
+ it('parses multiple -v flags', () => {
127
+ const result = parseCliArgs(['-v', '-v']);
128
+ expect(result.options.verbose).toBe(2);
129
+ });
130
+
131
+ it('parses -vv flag', () => {
132
+ const result = parseCliArgs(['-vv']);
133
+ expect(result.options.verbose).toBe(2);
134
+ });
135
+
136
+ it('parses --verbose flag', () => {
137
+ const result = parseCliArgs(['--verbose']);
138
+ expect(result.options.verbose).toBe(1);
139
+ });
140
+
141
+ it('parses --color flag', () => {
142
+ const result = parseCliArgs(['--color']);
143
+ expect(result.options.color).toBe(true);
144
+ });
145
+
146
+ it('parses --no-color flag', () => {
147
+ const result = parseCliArgs(['--no-color']);
148
+ expect(result.options.color).toBe(false);
149
+ });
150
+
151
+ it('--no-color overrides --color when both specified', () => {
152
+ const result = parseCliArgs(['--color', '--no-color']);
153
+ expect(result.options.color).toBe(false);
154
+ });
155
+
156
+ it('defaults quiet to false', () => {
157
+ const result = parseCliArgs([]);
158
+ expect(result.options.quiet).toBe(false);
159
+ });
160
+
161
+ it('defaults verbose to 0', () => {
162
+ const result = parseCliArgs([]);
163
+ expect(result.options.verbose).toBe(0);
164
+ });
165
+
166
+ it('color is undefined by default (auto-detect)', () => {
167
+ const result = parseCliArgs([]);
168
+ expect(result.options.color).toBeUndefined();
169
+ });
170
+
171
+ it('parses --fix flag', () => {
172
+ const result = parseCliArgs(['--fix']);
173
+ expect(result.options.fix).toBe(true);
174
+ });
175
+
176
+ it('defaults fix to false', () => {
177
+ const result = parseCliArgs([]);
178
+ expect(result.options.fix).toBe(false);
179
+ });
180
+
181
+ it('parses add command', () => {
182
+ const result = parseCliArgs(['add']);
183
+ expect(result.command).toBe('add');
184
+ });
185
+
186
+ it('parses add command with skill argument', () => {
187
+ const result = parseCliArgs(['add', 'security-review']);
188
+ expect(result.command).toBe('add');
189
+ expect(result.options.skill).toBe('security-review');
190
+ });
191
+
192
+ it('parses add --list flag', () => {
193
+ const result = parseCliArgs(['add', '--list']);
194
+ expect(result.command).toBe('add');
195
+ expect(result.options.list).toBe(true);
196
+ });
197
+
198
+ it('parses add -l flag', () => {
199
+ const result = parseCliArgs(['add', '-l']);
200
+ expect(result.command).toBe('add');
201
+ expect(result.options.list).toBe(true);
202
+ });
203
+
204
+ it('defaults list to false', () => {
205
+ const result = parseCliArgs([]);
206
+ expect(result.options.list).toBe(false);
207
+ });
208
+
209
+ it('parses --git flag', () => {
210
+ const result = parseCliArgs(['feature', '--git']);
211
+ expect(result.options.git).toBe(true);
212
+ });
213
+
214
+ it('defaults git to false', () => {
215
+ const result = parseCliArgs([]);
216
+ expect(result.options.git).toBe(false);
217
+ });
218
+
219
+ it('parses setup-app command', () => {
220
+ const result = parseCliArgs(['setup-app']);
221
+ expect(result.command).toBe('setup-app');
222
+ expect(result.setupAppOptions).toBeDefined();
223
+ expect(result.setupAppOptions?.port).toBe(3000);
224
+ expect(result.setupAppOptions?.timeout).toBe(300);
225
+ expect(result.setupAppOptions?.open).toBe(true);
226
+ });
227
+
228
+ it('parses setup-app --org option', () => {
229
+ const result = parseCliArgs(['setup-app', '--org', 'myorg']);
230
+ expect(result.command).toBe('setup-app');
231
+ expect(result.setupAppOptions?.org).toBe('myorg');
232
+ });
233
+
234
+ it('parses setup-app --port option', () => {
235
+ const result = parseCliArgs(['setup-app', '--port', '8080']);
236
+ expect(result.command).toBe('setup-app');
237
+ expect(result.setupAppOptions?.port).toBe(8080);
238
+ });
239
+
240
+ it('parses setup-app --timeout option', () => {
241
+ const result = parseCliArgs(['setup-app', '--timeout', '60']);
242
+ expect(result.command).toBe('setup-app');
243
+ expect(result.setupAppOptions?.timeout).toBe(60);
244
+ });
245
+
246
+ it('parses setup-app --name option', () => {
247
+ const result = parseCliArgs(['setup-app', '--name', 'My Custom App']);
248
+ expect(result.command).toBe('setup-app');
249
+ expect(result.setupAppOptions?.name).toBe('My Custom App');
250
+ });
251
+
252
+ it('parses setup-app --no-open flag', () => {
253
+ const result = parseCliArgs(['setup-app', '--no-open']);
254
+ expect(result.command).toBe('setup-app');
255
+ expect(result.setupAppOptions?.open).toBe(false);
256
+ });
257
+
258
+ it('parses add command with --remote flag', () => {
259
+ const result = parseCliArgs(['add', '--remote', 'getsentry/skills', '--skill', 'security-review']);
260
+ expect(result.command).toBe('add');
261
+ expect(result.options.remote).toBe('getsentry/skills');
262
+ expect(result.options.skill).toBe('security-review');
263
+ });
264
+
265
+ it('parses add command with pinned --remote', () => {
266
+ const result = parseCliArgs(['add', '--remote', 'getsentry/skills@abc123', '--skill', 'security-review']);
267
+ expect(result.command).toBe('add');
268
+ expect(result.options.remote).toBe('getsentry/skills@abc123');
269
+ });
270
+
271
+ it('parses add command with --remote and --list', () => {
272
+ const result = parseCliArgs(['add', '--remote', 'getsentry/skills', '--list']);
273
+ expect(result.command).toBe('add');
274
+ expect(result.options.remote).toBe('getsentry/skills');
275
+ expect(result.options.list).toBe(true);
276
+ });
277
+
278
+ it('parses add command with --remote and positional skill name', () => {
279
+ const result = parseCliArgs(['add', '--remote', 'getsentry/skills', 'security-review']);
280
+ expect(result.command).toBe('add');
281
+ expect(result.options.remote).toBe('getsentry/skills');
282
+ expect(result.options.skill).toBe('security-review');
283
+ });
284
+
285
+ it('parses --offline flag', () => {
286
+ const result = parseCliArgs(['--offline']);
287
+ expect(result.options.offline).toBe(true);
288
+ });
289
+
290
+ it('defaults offline to false', () => {
291
+ const result = parseCliArgs([]);
292
+ expect(result.options.offline).toBe(false);
293
+ });
294
+
295
+ it('parses sync command', () => {
296
+ const result = parseCliArgs(['sync']);
297
+ expect(result.command).toBe('sync');
298
+ });
299
+
300
+ it('parses sync command with remote argument', () => {
301
+ const result = parseCliArgs(['sync', 'getsentry/skills']);
302
+ expect(result.command).toBe('sync');
303
+ expect(result.options.remote).toBe('getsentry/skills');
304
+ });
305
+
306
+ it('parses sync command with --remote flag', () => {
307
+ const result = parseCliArgs(['sync', '--remote', 'getsentry/skills']);
308
+ expect(result.command).toBe('sync');
309
+ expect(result.options.remote).toBe('getsentry/skills');
310
+ });
311
+
312
+ it('--remote flag takes precedence over positional in sync', () => {
313
+ const result = parseCliArgs(['sync', 'other/repo', '--remote', 'getsentry/skills']);
314
+ expect(result.command).toBe('sync');
315
+ expect(result.options.remote).toBe('getsentry/skills');
316
+ });
317
+ });
318
+
319
+ describe('CLIOptionsSchema', () => {
320
+ it('validates valid severity levels', () => {
321
+ const severities = ['critical', 'high', 'medium', 'low', 'info'];
322
+ for (const severity of severities) {
323
+ const result = CLIOptionsSchema.safeParse({ failOn: severity });
324
+ expect(result.success).toBe(true);
325
+ }
326
+ });
327
+
328
+ it('rejects invalid severity levels', () => {
329
+ const result = CLIOptionsSchema.safeParse({ failOn: 'invalid' });
330
+ expect(result.success).toBe(false);
331
+ });
332
+
333
+ it('defaults json to false', () => {
334
+ const result = CLIOptionsSchema.parse({});
335
+ expect(result.json).toBe(false);
336
+ });
337
+
338
+ it('validates positive integer for parallel', () => {
339
+ const result = CLIOptionsSchema.safeParse({ parallel: 4 });
340
+ expect(result.success).toBe(true);
341
+ });
342
+
343
+ it('rejects non-positive parallel values', () => {
344
+ const result = CLIOptionsSchema.safeParse({ parallel: 0 });
345
+ expect(result.success).toBe(false);
346
+
347
+ const result2 = CLIOptionsSchema.safeParse({ parallel: -1 });
348
+ expect(result2.success).toBe(false);
349
+ });
350
+
351
+ it('rejects non-integer parallel values', () => {
352
+ const result = CLIOptionsSchema.safeParse({ parallel: 1.5 });
353
+ expect(result.success).toBe(false);
354
+ });
355
+ });
356
+
357
+ describe('detectTargetType', () => {
358
+ const mockExistsSync = existsSync as ReturnType<typeof vi.fn>;
359
+
360
+ beforeEach(() => {
361
+ mockExistsSync.mockReturnValue(false);
362
+ });
363
+
364
+ afterEach(() => {
365
+ mockExistsSync.mockReset();
366
+ });
367
+
368
+ it('detects git range syntax', () => {
369
+ expect(detectTargetType('main..feature')).toBe('git');
370
+ expect(detectTargetType('HEAD~3..HEAD')).toBe('git');
371
+ expect(detectTargetType('abc123..def456')).toBe('git');
372
+ });
373
+
374
+ it('detects relative refs', () => {
375
+ expect(detectTargetType('HEAD~3')).toBe('git');
376
+ expect(detectTargetType('main^2')).toBe('git');
377
+ expect(detectTargetType('feature~')).toBe('git');
378
+ });
379
+
380
+ it('detects common git refs', () => {
381
+ expect(detectTargetType('HEAD')).toBe('git');
382
+ expect(detectTargetType('FETCH_HEAD')).toBe('git');
383
+ expect(detectTargetType('ORIG_HEAD')).toBe('git');
384
+ });
385
+
386
+ it('detects file paths', () => {
387
+ expect(detectTargetType('src/auth.ts')).toBe('file');
388
+ expect(detectTargetType('./file.ts')).toBe('file');
389
+ expect(detectTargetType('path/to/file.js')).toBe('file');
390
+ });
391
+
392
+ it('detects file extensions', () => {
393
+ expect(detectTargetType('file.ts')).toBe('file');
394
+ expect(detectTargetType('file.js')).toBe('file');
395
+ expect(detectTargetType('README.md')).toBe('file');
396
+ });
397
+
398
+ it('detects glob patterns', () => {
399
+ expect(detectTargetType('*.ts')).toBe('file');
400
+ expect(detectTargetType('src/**/*.ts')).toBe('file');
401
+ expect(detectTargetType('file?.ts')).toBe('file');
402
+ });
403
+
404
+ it('defaults to git for ambiguous targets when path does not exist', () => {
405
+ mockExistsSync.mockReturnValue(false);
406
+ expect(detectTargetType('main')).toBe('git');
407
+ expect(detectTargetType('feature')).toBe('git');
408
+ });
409
+
410
+ it('prefers file when ambiguous target exists as file/directory', () => {
411
+ mockExistsSync.mockReturnValue(true);
412
+ expect(detectTargetType('feature')).toBe('file');
413
+ expect(detectTargetType('docs')).toBe('file');
414
+ });
415
+
416
+ it('uses cwd option for filesystem check', () => {
417
+ mockExistsSync.mockImplementation((path: string) => path === '/custom/path/feature');
418
+ expect(detectTargetType('feature', { cwd: '/custom/path' })).toBe('file');
419
+ expect(mockExistsSync).toHaveBeenCalledWith('/custom/path/feature');
420
+ });
421
+
422
+ it('forceGit option overrides filesystem check', () => {
423
+ mockExistsSync.mockReturnValue(true);
424
+ expect(detectTargetType('feature', { forceGit: true })).toBe('git');
425
+ });
426
+
427
+ it('forceGit does not affect unambiguous targets', () => {
428
+ // Git range syntax is still git
429
+ expect(detectTargetType('main..feature', { forceGit: true })).toBe('git');
430
+ // File paths are still file
431
+ expect(detectTargetType('src/auth.ts', { forceGit: true })).toBe('file');
432
+ });
433
+ });
434
+
435
+ describe('classifyTargets', () => {
436
+ const mockExistsSync = existsSync as ReturnType<typeof vi.fn>;
437
+
438
+ beforeEach(() => {
439
+ mockExistsSync.mockReturnValue(false);
440
+ });
441
+
442
+ afterEach(() => {
443
+ mockExistsSync.mockReset();
444
+ });
445
+
446
+ it('classifies file targets', () => {
447
+ const { gitRefs, filePatterns } = classifyTargets(['src/auth.ts', 'file.js']);
448
+ expect(gitRefs).toEqual([]);
449
+ expect(filePatterns).toEqual(['src/auth.ts', 'file.js']);
450
+ });
451
+
452
+ it('classifies git targets', () => {
453
+ const { gitRefs, filePatterns } = classifyTargets(['HEAD~3', 'main..feature']);
454
+ expect(gitRefs).toEqual(['HEAD~3', 'main..feature']);
455
+ expect(filePatterns).toEqual([]);
456
+ });
457
+
458
+ it('classifies mixed targets', () => {
459
+ const { gitRefs, filePatterns } = classifyTargets(['HEAD~3', 'src/auth.ts']);
460
+ expect(gitRefs).toEqual(['HEAD~3']);
461
+ expect(filePatterns).toEqual(['src/auth.ts']);
462
+ });
463
+
464
+ it('classifies ambiguous target as file when path exists', () => {
465
+ mockExistsSync.mockReturnValue(true);
466
+ const { gitRefs, filePatterns } = classifyTargets(['feature']);
467
+ expect(gitRefs).toEqual([]);
468
+ expect(filePatterns).toEqual(['feature']);
469
+ });
470
+
471
+ it('forceGit option forces ambiguous targets to git', () => {
472
+ mockExistsSync.mockReturnValue(true);
473
+ const { gitRefs, filePatterns } = classifyTargets(['feature'], { forceGit: true });
474
+ expect(gitRefs).toEqual(['feature']);
475
+ expect(filePatterns).toEqual([]);
476
+ });
477
+ });