@vertaaux/cli 0.2.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 (198) hide show
  1. package/README.md +345 -0
  2. package/dist/auth/ci-token.d.ts +49 -0
  3. package/dist/auth/ci-token.d.ts.map +1 -0
  4. package/dist/auth/ci-token.js +83 -0
  5. package/dist/auth/device-flow.d.ts +66 -0
  6. package/dist/auth/device-flow.d.ts.map +1 -0
  7. package/dist/auth/device-flow.js +156 -0
  8. package/dist/auth/token-store.d.ts +53 -0
  9. package/dist/auth/token-store.d.ts.map +1 -0
  10. package/dist/auth/token-store.js +78 -0
  11. package/dist/baseline/diff.d.ts +57 -0
  12. package/dist/baseline/diff.d.ts.map +1 -0
  13. package/dist/baseline/diff.js +152 -0
  14. package/dist/baseline/hash.d.ts +54 -0
  15. package/dist/baseline/hash.d.ts.map +1 -0
  16. package/dist/baseline/hash.js +66 -0
  17. package/dist/baseline/manager.d.ts +89 -0
  18. package/dist/baseline/manager.d.ts.map +1 -0
  19. package/dist/baseline/manager.js +157 -0
  20. package/dist/cache/index.d.ts +8 -0
  21. package/dist/cache/index.d.ts.map +1 -0
  22. package/dist/cache/index.js +7 -0
  23. package/dist/cache/route-cache.d.ts +119 -0
  24. package/dist/cache/route-cache.d.ts.map +1 -0
  25. package/dist/cache/route-cache.js +213 -0
  26. package/dist/ci/changed-routes.d.ts +95 -0
  27. package/dist/ci/changed-routes.d.ts.map +1 -0
  28. package/dist/ci/changed-routes.js +304 -0
  29. package/dist/ci/github-api.d.ts +68 -0
  30. package/dist/ci/github-api.d.ts.map +1 -0
  31. package/dist/ci/github-api.js +138 -0
  32. package/dist/ci/gitlab-api.d.ts +75 -0
  33. package/dist/ci/gitlab-api.d.ts.map +1 -0
  34. package/dist/ci/gitlab-api.js +180 -0
  35. package/dist/ci/index.d.ts +6 -0
  36. package/dist/ci/index.d.ts.map +1 -0
  37. package/dist/ci/index.js +4 -0
  38. package/dist/commands/audit.d.ts +58 -0
  39. package/dist/commands/audit.d.ts.map +1 -0
  40. package/dist/commands/audit.js +862 -0
  41. package/dist/commands/baseline.d.ts +22 -0
  42. package/dist/commands/baseline.d.ts.map +1 -0
  43. package/dist/commands/baseline.js +210 -0
  44. package/dist/commands/comment.d.ts +14 -0
  45. package/dist/commands/comment.d.ts.map +1 -0
  46. package/dist/commands/comment.js +363 -0
  47. package/dist/commands/diff.d.ts +24 -0
  48. package/dist/commands/diff.d.ts.map +1 -0
  49. package/dist/commands/diff.js +196 -0
  50. package/dist/commands/doctor.d.ts +58 -0
  51. package/dist/commands/doctor.d.ts.map +1 -0
  52. package/dist/commands/doctor.js +338 -0
  53. package/dist/commands/download.d.ts +12 -0
  54. package/dist/commands/download.d.ts.map +1 -0
  55. package/dist/commands/download.js +183 -0
  56. package/dist/commands/explain.d.ts +62 -0
  57. package/dist/commands/explain.d.ts.map +1 -0
  58. package/dist/commands/explain.js +302 -0
  59. package/dist/commands/init.d.ts +12 -0
  60. package/dist/commands/init.d.ts.map +1 -0
  61. package/dist/commands/init.js +212 -0
  62. package/dist/commands/login.d.ts +14 -0
  63. package/dist/commands/login.d.ts.map +1 -0
  64. package/dist/commands/login.js +222 -0
  65. package/dist/commands/policy.d.ts +13 -0
  66. package/dist/commands/policy.d.ts.map +1 -0
  67. package/dist/commands/policy.js +347 -0
  68. package/dist/commands/upload.d.ts +12 -0
  69. package/dist/commands/upload.d.ts.map +1 -0
  70. package/dist/commands/upload.js +158 -0
  71. package/dist/config/defaults.d.ts +21 -0
  72. package/dist/config/defaults.d.ts.map +1 -0
  73. package/dist/config/defaults.js +49 -0
  74. package/dist/config/loader.d.ts +66 -0
  75. package/dist/config/loader.d.ts.map +1 -0
  76. package/dist/config/loader.js +167 -0
  77. package/dist/config/schema.d.ts +55 -0
  78. package/dist/config/schema.d.ts.map +1 -0
  79. package/dist/config/schema.js +6 -0
  80. package/dist/index.d.ts +9 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +1090 -0
  83. package/dist/interactive/fix-wizard.d.ts +44 -0
  84. package/dist/interactive/fix-wizard.d.ts.map +1 -0
  85. package/dist/interactive/fix-wizard.js +286 -0
  86. package/dist/interactive/init-wizard.d.ts +32 -0
  87. package/dist/interactive/init-wizard.d.ts.map +1 -0
  88. package/dist/interactive/init-wizard.js +193 -0
  89. package/dist/interactive/prompts.d.ts +62 -0
  90. package/dist/interactive/prompts.d.ts.map +1 -0
  91. package/dist/interactive/prompts.js +78 -0
  92. package/dist/monorepo/detector.d.ts +70 -0
  93. package/dist/monorepo/detector.d.ts.map +1 -0
  94. package/dist/monorepo/detector.js +278 -0
  95. package/dist/monorepo/index.d.ts +9 -0
  96. package/dist/monorepo/index.d.ts.map +1 -0
  97. package/dist/monorepo/index.js +8 -0
  98. package/dist/monorepo/workspace.d.ts +142 -0
  99. package/dist/monorepo/workspace.d.ts.map +1 -0
  100. package/dist/monorepo/workspace.js +171 -0
  101. package/dist/output/envelope.d.ts +21 -0
  102. package/dist/output/envelope.d.ts.map +1 -0
  103. package/dist/output/envelope.js +27 -0
  104. package/dist/output/factory.d.ts +73 -0
  105. package/dist/output/factory.d.ts.map +1 -0
  106. package/dist/output/factory.js +60 -0
  107. package/dist/output/formats.d.ts +11 -0
  108. package/dist/output/formats.d.ts.map +1 -0
  109. package/dist/output/formats.js +41 -0
  110. package/dist/output/html.d.ts +45 -0
  111. package/dist/output/html.d.ts.map +1 -0
  112. package/dist/output/html.js +607 -0
  113. package/dist/output/human.d.ts +41 -0
  114. package/dist/output/human.d.ts.map +1 -0
  115. package/dist/output/human.js +274 -0
  116. package/dist/output/json.d.ts +42 -0
  117. package/dist/output/json.d.ts.map +1 -0
  118. package/dist/output/json.js +37 -0
  119. package/dist/output/junit.d.ts +56 -0
  120. package/dist/output/junit.d.ts.map +1 -0
  121. package/dist/output/junit.js +135 -0
  122. package/dist/output/markdown.d.ts +77 -0
  123. package/dist/output/markdown.d.ts.map +1 -0
  124. package/dist/output/markdown.js +411 -0
  125. package/dist/output/sarif.d.ts +160 -0
  126. package/dist/output/sarif.d.ts.map +1 -0
  127. package/dist/output/sarif.js +207 -0
  128. package/dist/policy/evaluator.d.ts +111 -0
  129. package/dist/policy/evaluator.d.ts.map +1 -0
  130. package/dist/policy/evaluator.js +362 -0
  131. package/dist/policy/index.d.ts +15 -0
  132. package/dist/policy/index.d.ts.map +1 -0
  133. package/dist/policy/index.js +11 -0
  134. package/dist/policy/loader.d.ts +97 -0
  135. package/dist/policy/loader.d.ts.map +1 -0
  136. package/dist/policy/loader.js +281 -0
  137. package/dist/policy/schema.d.ts +297 -0
  138. package/dist/policy/schema.d.ts.map +1 -0
  139. package/dist/policy/schema.js +230 -0
  140. package/dist/quality-gate/evaluator.d.ts +58 -0
  141. package/dist/quality-gate/evaluator.d.ts.map +1 -0
  142. package/dist/quality-gate/evaluator.js +274 -0
  143. package/dist/quality-gate/index.d.ts +10 -0
  144. package/dist/quality-gate/index.d.ts.map +1 -0
  145. package/dist/quality-gate/index.js +7 -0
  146. package/dist/quality-gate/types.d.ts +103 -0
  147. package/dist/quality-gate/types.d.ts.map +1 -0
  148. package/dist/quality-gate/types.js +23 -0
  149. package/dist/templates/azure-devops.d.ts +25 -0
  150. package/dist/templates/azure-devops.d.ts.map +1 -0
  151. package/dist/templates/azure-devops.js +109 -0
  152. package/dist/templates/circleci.d.ts +28 -0
  153. package/dist/templates/circleci.d.ts.map +1 -0
  154. package/dist/templates/circleci.js +86 -0
  155. package/dist/templates/github-actions.d.ts +81 -0
  156. package/dist/templates/github-actions.d.ts.map +1 -0
  157. package/dist/templates/github-actions.js +393 -0
  158. package/dist/templates/gitlab-ci.d.ts +26 -0
  159. package/dist/templates/gitlab-ci.d.ts.map +1 -0
  160. package/dist/templates/gitlab-ci.js +70 -0
  161. package/dist/templates/index.d.ts +72 -0
  162. package/dist/templates/index.d.ts.map +1 -0
  163. package/dist/templates/index.js +112 -0
  164. package/dist/templates/jenkins.d.ts +26 -0
  165. package/dist/templates/jenkins.d.ts.map +1 -0
  166. package/dist/templates/jenkins.js +110 -0
  167. package/dist/ui/banner.d.ts +31 -0
  168. package/dist/ui/banner.d.ts.map +1 -0
  169. package/dist/ui/banner.js +84 -0
  170. package/dist/ui/diagnostics.d.ts +39 -0
  171. package/dist/ui/diagnostics.d.ts.map +1 -0
  172. package/dist/ui/diagnostics.js +153 -0
  173. package/dist/ui/spinner.d.ts +61 -0
  174. package/dist/ui/spinner.d.ts.map +1 -0
  175. package/dist/ui/spinner.js +101 -0
  176. package/dist/ui/table.d.ts +63 -0
  177. package/dist/ui/table.d.ts.map +1 -0
  178. package/dist/ui/table.js +236 -0
  179. package/dist/utils/client.d.ts +82 -0
  180. package/dist/utils/client.d.ts.map +1 -0
  181. package/dist/utils/client.js +128 -0
  182. package/dist/utils/detect-env.d.ts +59 -0
  183. package/dist/utils/detect-env.d.ts.map +1 -0
  184. package/dist/utils/detect-env.js +115 -0
  185. package/dist/utils/exit-codes.d.ts +47 -0
  186. package/dist/utils/exit-codes.d.ts.map +1 -0
  187. package/dist/utils/exit-codes.js +61 -0
  188. package/dist/utils/logger.d.ts +87 -0
  189. package/dist/utils/logger.d.ts.map +1 -0
  190. package/dist/utils/logger.js +185 -0
  191. package/dist/utils/sanitize.d.ts +36 -0
  192. package/dist/utils/sanitize.d.ts.map +1 -0
  193. package/dist/utils/sanitize.js +64 -0
  194. package/dist/utils/validators.d.ts +41 -0
  195. package/dist/utils/validators.d.ts.map +1 -0
  196. package/dist/utils/validators.js +123 -0
  197. package/package.json +63 -0
  198. package/schemas/vertaaux.config.schema.json +103 -0
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Detect routes changed in PR for incremental auditing.
3
+ *
4
+ * Uses git diff to find changed files, then maps them to routes.
5
+ * This enables efficient CI by only auditing what changed.
6
+ *
7
+ * Implements CICD-07: Incremental mode audits only routes touched in PR.
8
+ */
9
+ import { execSync, execFileSync } from "child_process";
10
+ import { validateBranchName } from "../utils/sanitize.js";
11
+ /**
12
+ * Default route patterns for common frameworks.
13
+ *
14
+ * These patterns identify files that typically map to routes.
15
+ */
16
+ const DEFAULT_ROUTE_PATTERNS = [
17
+ // Next.js App Router
18
+ "src/app/**/page.tsx",
19
+ "src/app/**/page.ts",
20
+ "src/app/**/page.jsx",
21
+ "src/app/**/page.js",
22
+ // Next.js Pages Router
23
+ "src/pages/**/*.tsx",
24
+ "src/pages/**/*.ts",
25
+ "src/pages/**/*.jsx",
26
+ "src/pages/**/*.js",
27
+ "pages/**/*.tsx",
28
+ "pages/**/*.ts",
29
+ "pages/**/*.jsx",
30
+ "pages/**/*.js",
31
+ // SvelteKit
32
+ "src/routes/**/+page.svelte",
33
+ "src/routes/**/+page.ts",
34
+ "src/routes/**/+page.js",
35
+ // Remix
36
+ "app/routes/**/*.tsx",
37
+ "app/routes/**/*.ts",
38
+ "app/routes/**/*.jsx",
39
+ "app/routes/**/*.js",
40
+ // Nuxt
41
+ "pages/**/*.vue",
42
+ // Astro
43
+ "src/pages/**/*.astro",
44
+ ];
45
+ /**
46
+ * Convert a file path to a route URL.
47
+ *
48
+ * Supports common framework conventions:
49
+ * - Next.js App Router: src/app/about/page.tsx -> /about
50
+ * - Next.js Pages Router: src/pages/blog/index.tsx -> /blog
51
+ * - SvelteKit: src/routes/blog/+page.svelte -> /blog
52
+ *
53
+ * @param filePath - The changed file path
54
+ * @returns The route URL or null if not a route file
55
+ */
56
+ export function filePathToRoute(filePath) {
57
+ // Next.js App Router: src/app/about/page.tsx -> /about
58
+ const appRouterMatch = filePath.match(/^(?:src\/)?app\/(.*)\/page\.(tsx?|jsx?|mdx?)$/);
59
+ if (appRouterMatch) {
60
+ const route = appRouterMatch[1];
61
+ // Handle root route
62
+ if (route === "")
63
+ return "/";
64
+ // Handle dynamic segments: [id] -> :id (for display, keep as-is for audit)
65
+ return `/${route}`;
66
+ }
67
+ // Next.js Pages Router: src/pages/blog/index.tsx -> /blog
68
+ const pagesRouterMatch = filePath.match(/^(?:src\/)?pages\/(.+)\.(tsx?|jsx?|mdx?)$/);
69
+ if (pagesRouterMatch) {
70
+ let route = pagesRouterMatch[1];
71
+ // Remove index suffix
72
+ route = route.replace(/\/index$/, "");
73
+ route = route.replace(/^index$/, "");
74
+ // Handle root route
75
+ if (route === "")
76
+ return "/";
77
+ return `/${route}`;
78
+ }
79
+ // SvelteKit: src/routes/blog/+page.svelte -> /blog
80
+ const sveltekitMatch = filePath.match(/^src\/routes\/(.*)\/\+page\.(svelte|ts|js)$/);
81
+ if (sveltekitMatch) {
82
+ const route = sveltekitMatch[1];
83
+ if (route === "")
84
+ return "/";
85
+ return `/${route}`;
86
+ }
87
+ // Remix: app/routes/blog.tsx -> /blog
88
+ const remixMatch = filePath.match(/^app\/routes\/(.+)\.(tsx?|jsx?)$/);
89
+ if (remixMatch) {
90
+ let route = remixMatch[1];
91
+ // Remix uses . for nested routes: blog.post -> blog/post
92
+ route = route.replace(/\./g, "/");
93
+ // Handle _index (root)
94
+ route = route.replace(/\/_index$/, "");
95
+ route = route.replace(/^_index$/, "");
96
+ if (route === "")
97
+ return "/";
98
+ return `/${route}`;
99
+ }
100
+ // Nuxt: pages/about.vue -> /about
101
+ const nuxtMatch = filePath.match(/^pages\/(.+)\.vue$/);
102
+ if (nuxtMatch) {
103
+ let route = nuxtMatch[1];
104
+ route = route.replace(/\/index$/, "");
105
+ route = route.replace(/^index$/, "");
106
+ if (route === "")
107
+ return "/";
108
+ return `/${route}`;
109
+ }
110
+ // Astro: src/pages/about.astro -> /about
111
+ const astroMatch = filePath.match(/^src\/pages\/(.+)\.astro$/);
112
+ if (astroMatch) {
113
+ let route = astroMatch[1];
114
+ route = route.replace(/\/index$/, "");
115
+ route = route.replace(/^index$/, "");
116
+ if (route === "")
117
+ return "/";
118
+ return `/${route}`;
119
+ }
120
+ return null;
121
+ }
122
+ /**
123
+ * Check if a file path matches any of the route patterns.
124
+ */
125
+ function matchesRoutePattern(filePath, patterns) {
126
+ for (const pattern of patterns) {
127
+ // Convert glob pattern to regex
128
+ const regexPattern = pattern
129
+ .replace(/\*\*/g, "DOUBLESTAR")
130
+ .replace(/\*/g, "[^/]*")
131
+ .replace(/DOUBLESTAR/g, ".*")
132
+ .replace(/\./g, "\\.");
133
+ const regex = new RegExp(`^${regexPattern}$`);
134
+ if (regex.test(filePath)) {
135
+ return true;
136
+ }
137
+ }
138
+ return false;
139
+ }
140
+ /**
141
+ * Get the list of files changed between branches.
142
+ *
143
+ * @param baseBranch - The base branch to compare against
144
+ * @returns Array of changed file paths
145
+ */
146
+ function getChangedFiles(baseBranch) {
147
+ const validBranch = validateBranchName(baseBranch);
148
+ try {
149
+ // Try with origin/ prefix first (most common in CI)
150
+ const result = execFileSync("git", ["diff", "--name-only", `origin/${validBranch}...HEAD`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
151
+ return result
152
+ .split("\n")
153
+ .map((line) => line.trim())
154
+ .filter(Boolean);
155
+ }
156
+ catch {
157
+ try {
158
+ // Fall back to without origin/ prefix (local branches)
159
+ const result = execFileSync("git", ["diff", "--name-only", `${validBranch}...HEAD`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
160
+ return result
161
+ .split("\n")
162
+ .map((line) => line.trim())
163
+ .filter(Boolean);
164
+ }
165
+ catch {
166
+ // If git diff fails, return empty array
167
+ console.error(`Warning: Could not get changed files from git (base: ${validBranch})`);
168
+ return [];
169
+ }
170
+ }
171
+ }
172
+ /**
173
+ * Detect base branch from CI environment variables.
174
+ *
175
+ * Supports:
176
+ * - GitHub Actions: GITHUB_BASE_REF
177
+ * - GitLab CI: CI_MERGE_REQUEST_TARGET_BRANCH_NAME
178
+ * - Azure DevOps: SYSTEM_PULLREQUEST_TARGETBRANCH
179
+ * - Jenkins: CHANGE_TARGET
180
+ * - CircleCI: CIRCLE_BRANCH (for base, uses main as fallback)
181
+ *
182
+ * @returns Detected base branch or "main" as default
183
+ */
184
+ export function detectBaseBranch() {
185
+ let detected;
186
+ // GitHub Actions
187
+ if (process.env.GITHUB_BASE_REF) {
188
+ detected = process.env.GITHUB_BASE_REF;
189
+ }
190
+ // GitLab CI
191
+ if (!detected && process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME) {
192
+ detected = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
193
+ }
194
+ // Azure DevOps
195
+ if (!detected && process.env.SYSTEM_PULLREQUEST_TARGETBRANCH) {
196
+ // Azure uses refs/heads/main format
197
+ detected = process.env.SYSTEM_PULLREQUEST_TARGETBRANCH.replace(/^refs\/heads\//, "");
198
+ }
199
+ // Jenkins
200
+ if (!detected && process.env.CHANGE_TARGET) {
201
+ detected = process.env.CHANGE_TARGET;
202
+ }
203
+ // Try to detect main vs master
204
+ if (!detected) {
205
+ try {
206
+ execSync("git rev-parse --verify origin/main", {
207
+ stdio: ["pipe", "pipe", "pipe"],
208
+ });
209
+ detected = "main";
210
+ }
211
+ catch {
212
+ try {
213
+ execSync("git rev-parse --verify origin/master", {
214
+ stdio: ["pipe", "pipe", "pipe"],
215
+ });
216
+ detected = "master";
217
+ }
218
+ catch {
219
+ // Default to main
220
+ detected = "main";
221
+ }
222
+ }
223
+ }
224
+ // Defense in depth: validate the detected branch name.
225
+ // If a CI env var contained malicious characters, fall back to "main".
226
+ try {
227
+ return validateBranchName(detected);
228
+ }
229
+ catch {
230
+ return "main";
231
+ }
232
+ }
233
+ /**
234
+ * Get list of routes affected by changed files.
235
+ *
236
+ * Uses git diff to find changed files, then maps to routes.
237
+ *
238
+ * @param options - Detection options
239
+ * @returns Changed routes result
240
+ */
241
+ export async function getChangedRoutes(options) {
242
+ const { baseBranch, routePatterns = DEFAULT_ROUTE_PATTERNS, fileToRouteMap } = options;
243
+ // Get changed files from git
244
+ const changedFiles = getChangedFiles(baseBranch);
245
+ if (changedFiles.length === 0) {
246
+ return {
247
+ routes: [],
248
+ changedFiles: [],
249
+ hasChanges: false,
250
+ };
251
+ }
252
+ // Collect routes from changed files
253
+ const routeSet = new Set();
254
+ for (const filePath of changedFiles) {
255
+ // First, check custom mapping
256
+ if (fileToRouteMap) {
257
+ for (const [pattern, routes] of Object.entries(fileToRouteMap)) {
258
+ const regexPattern = pattern
259
+ .replace(/\*\*/g, "DOUBLESTAR")
260
+ .replace(/\*/g, "[^/]*")
261
+ .replace(/DOUBLESTAR/g, ".*")
262
+ .replace(/\./g, "\\.");
263
+ const regex = new RegExp(`^${regexPattern}$`);
264
+ if (regex.test(filePath)) {
265
+ for (const route of routes) {
266
+ routeSet.add(route);
267
+ }
268
+ }
269
+ }
270
+ }
271
+ // Check if file matches route patterns
272
+ if (matchesRoutePattern(filePath, routePatterns)) {
273
+ const route = filePathToRoute(filePath);
274
+ if (route) {
275
+ routeSet.add(route);
276
+ }
277
+ }
278
+ }
279
+ const routes = Array.from(routeSet).sort();
280
+ return {
281
+ routes,
282
+ changedFiles,
283
+ hasChanges: routes.length > 0,
284
+ };
285
+ }
286
+ /**
287
+ * Budget modes for controlling audit scope.
288
+ *
289
+ * CICD-13: Default mode runs under sane budget; full scan is opt-in.
290
+ */
291
+ export const BUDGET_MODES = {
292
+ /** Quick scan: 5 pages, 30s timeout, 2 concurrent */
293
+ quick: { maxPages: 5, maxTime: 30000, concurrency: 2 },
294
+ /** Standard scan: 20 pages, 2min timeout, 4 concurrent */
295
+ standard: { maxPages: 20, maxTime: 120000, concurrency: 4 },
296
+ /** Full scan: unlimited pages, 10min timeout, 8 concurrent */
297
+ full: { maxPages: Infinity, maxTime: 600000, concurrency: 8 },
298
+ };
299
+ /**
300
+ * Get budget configuration for a mode.
301
+ */
302
+ export function getBudgetConfig(mode) {
303
+ return BUDGET_MODES[mode] || BUDGET_MODES.standard;
304
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * GitHub API wrapper for PR comment operations.
3
+ *
4
+ * Provides utilities for posting and updating PR comments
5
+ * with sticky comment semantics (find existing by hidden header).
6
+ *
7
+ * Uses native fetch (Node 18+).
8
+ */
9
+ /**
10
+ * Options for GitHub comment operations.
11
+ */
12
+ export interface GitHubCommentOptions {
13
+ /** GitHub token (from GITHUB_TOKEN env var) */
14
+ token: string;
15
+ /** Repository owner */
16
+ owner: string;
17
+ /** Repository name */
18
+ repo: string;
19
+ /** PR number */
20
+ prNumber: number;
21
+ /** Comment body (markdown) */
22
+ body: string;
23
+ /** Header identifier for finding existing comment */
24
+ header: string;
25
+ }
26
+ /**
27
+ * Find existing comment by hidden header identifier.
28
+ *
29
+ * Searches PR comments for `<!-- {header} -->` pattern in body.
30
+ * Handles pagination for repos with many comments.
31
+ *
32
+ * @param options - Comment search options (token, owner, repo, prNumber, header)
33
+ * @returns Comment ID if found, null otherwise
34
+ */
35
+ export declare function findExistingComment(options: Omit<GitHubCommentOptions, "body">): Promise<number | null>;
36
+ /**
37
+ * Post new comment or update existing one.
38
+ *
39
+ * Uses header identifier to find existing comment for update semantics.
40
+ * This prevents comment spam and ensures only one VertaaUX comment per PR.
41
+ *
42
+ * @param options - Comment options (token, owner, repo, prNumber, body, header)
43
+ * @returns Created/updated comment with ID and URL
44
+ */
45
+ export declare function postOrUpdateGitHubComment(options: GitHubCommentOptions): Promise<{
46
+ id: number;
47
+ html_url: string;
48
+ }>;
49
+ /**
50
+ * Parse repository string into owner and repo.
51
+ *
52
+ * @param repository - Repository string in "owner/repo" format
53
+ * @returns Parsed owner and repo, or null if invalid
54
+ */
55
+ export declare function parseRepository(repository: string): {
56
+ owner: string;
57
+ repo: string;
58
+ } | null;
59
+ /**
60
+ * Extract PR number from GitHub Actions environment.
61
+ *
62
+ * Checks GITHUB_REF_NAME for PR merge ref format (e.g., "123/merge")
63
+ * and falls back to GITHUB_EVENT_NUMBER.
64
+ *
65
+ * @returns PR number or null if not in PR context
66
+ */
67
+ export declare function extractPRNumber(): number | null;
68
+ //# sourceMappingURL=github-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-api.d.ts","sourceRoot":"","sources":["../../src/ci/github-api.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;CAChB;AA8DD;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,MAAM,CAAC,GAC1C,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA4BxB;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAqB3C;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,GACjB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAIxC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAqB/C"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * GitHub API wrapper for PR comment operations.
3
+ *
4
+ * Provides utilities for posting and updating PR comments
5
+ * with sticky comment semantics (find existing by hidden header).
6
+ *
7
+ * Uses native fetch (Node 18+).
8
+ */
9
+ const GITHUB_API_BASE = "https://api.github.com";
10
+ /**
11
+ * Make an authenticated GitHub API request.
12
+ */
13
+ async function githubRequest(method, url, token, body) {
14
+ const headers = {
15
+ Accept: "application/vnd.github+json",
16
+ Authorization: `Bearer ${token}`,
17
+ "X-GitHub-Api-Version": "2022-11-28",
18
+ "User-Agent": "VertaaUX-CLI",
19
+ };
20
+ if (body) {
21
+ headers["Content-Type"] = "application/json";
22
+ }
23
+ const response = await fetch(url, {
24
+ method,
25
+ headers,
26
+ body: body ? JSON.stringify(body) : undefined,
27
+ });
28
+ if (!response.ok) {
29
+ const errorData = (await response.json().catch(() => ({})));
30
+ throw new Error(`GitHub API error (${response.status}): ${errorData.message || response.statusText}`);
31
+ }
32
+ // Handle no content responses
33
+ if (response.status === 204) {
34
+ return {};
35
+ }
36
+ return response.json();
37
+ }
38
+ /**
39
+ * Find existing comment by hidden header identifier.
40
+ *
41
+ * Searches PR comments for `<!-- {header} -->` pattern in body.
42
+ * Handles pagination for repos with many comments.
43
+ *
44
+ * @param options - Comment search options (token, owner, repo, prNumber, header)
45
+ * @returns Comment ID if found, null otherwise
46
+ */
47
+ export async function findExistingComment(options) {
48
+ const { token, owner, repo, prNumber, header } = options;
49
+ const headerPattern = `<!-- ${header} -->`;
50
+ let page = 1;
51
+ const perPage = 100;
52
+ // Paginate through comments until we find one or run out
53
+ while (true) {
54
+ const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/issues/${prNumber}/comments?page=${page}&per_page=${perPage}`;
55
+ const comments = await githubRequest("GET", url, token);
56
+ for (const comment of comments) {
57
+ if (comment.body?.includes(headerPattern)) {
58
+ return comment.id;
59
+ }
60
+ }
61
+ // No more pages
62
+ if (comments.length < perPage) {
63
+ break;
64
+ }
65
+ page++;
66
+ }
67
+ return null;
68
+ }
69
+ /**
70
+ * Post new comment or update existing one.
71
+ *
72
+ * Uses header identifier to find existing comment for update semantics.
73
+ * This prevents comment spam and ensures only one VertaaUX comment per PR.
74
+ *
75
+ * @param options - Comment options (token, owner, repo, prNumber, body, header)
76
+ * @returns Created/updated comment with ID and URL
77
+ */
78
+ export async function postOrUpdateGitHubComment(options) {
79
+ const { token, owner, repo, prNumber, body, header } = options;
80
+ // Try to find existing comment
81
+ const existingId = await findExistingComment({
82
+ token,
83
+ owner,
84
+ repo,
85
+ prNumber,
86
+ header,
87
+ });
88
+ if (existingId) {
89
+ // Update existing comment
90
+ const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/issues/comments/${existingId}`;
91
+ return githubRequest("PATCH", url, token, { body });
92
+ }
93
+ else {
94
+ // Create new comment
95
+ const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/issues/${prNumber}/comments`;
96
+ return githubRequest("POST", url, token, { body });
97
+ }
98
+ }
99
+ /**
100
+ * Parse repository string into owner and repo.
101
+ *
102
+ * @param repository - Repository string in "owner/repo" format
103
+ * @returns Parsed owner and repo, or null if invalid
104
+ */
105
+ export function parseRepository(repository) {
106
+ const match = repository.match(/^([^/]+)\/([^/]+)$/);
107
+ if (!match)
108
+ return null;
109
+ return { owner: match[1], repo: match[2] };
110
+ }
111
+ /**
112
+ * Extract PR number from GitHub Actions environment.
113
+ *
114
+ * Checks GITHUB_REF_NAME for PR merge ref format (e.g., "123/merge")
115
+ * and falls back to GITHUB_EVENT_NUMBER.
116
+ *
117
+ * @returns PR number or null if not in PR context
118
+ */
119
+ export function extractPRNumber() {
120
+ // From PR merge ref: refs/pull/123/merge -> GITHUB_REF_NAME = "123/merge"
121
+ const refName = process.env.GITHUB_REF_NAME;
122
+ if (refName) {
123
+ const match = refName.match(/^(\d+)\/merge$/);
124
+ if (match) {
125
+ return parseInt(match[1], 10);
126
+ }
127
+ }
128
+ // From workflow context
129
+ const eventNumber = process.env.GITHUB_EVENT_NUMBER;
130
+ if (eventNumber) {
131
+ const num = parseInt(eventNumber, 10);
132
+ if (!isNaN(num))
133
+ return num;
134
+ }
135
+ // From pull_request event payload (parsed from GITHUB_EVENT_PATH)
136
+ // This requires reading the event file, skipping for simplicity
137
+ return null;
138
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * GitLab API wrapper for MR note operations.
3
+ *
4
+ * Provides utilities for posting and updating MR notes (comments)
5
+ * with sticky note semantics (find existing by hidden header).
6
+ *
7
+ * Uses native fetch (Node 18+).
8
+ */
9
+ /**
10
+ * Options for GitLab note operations.
11
+ */
12
+ export interface GitLabNoteOptions {
13
+ /** GitLab token (from GITLAB_TOKEN or CI_JOB_TOKEN) */
14
+ token: string;
15
+ /** GitLab API URL (from CI_API_V4_URL or https://gitlab.com/api/v4) */
16
+ apiUrl: string;
17
+ /** Project ID or URL-encoded path */
18
+ projectId: string;
19
+ /** MR IID (internal ID within project) */
20
+ mrIid: number;
21
+ /** Note body (markdown) */
22
+ body: string;
23
+ /** Header identifier for finding existing note */
24
+ header: string;
25
+ }
26
+ /**
27
+ * URL-encode a project path for GitLab API.
28
+ *
29
+ * Project ID can be numeric or a path like "group/project".
30
+ * Numeric IDs pass through unchanged, paths are URL-encoded.
31
+ *
32
+ * @param projectId - Project ID or path
33
+ * @returns URL-safe project identifier
34
+ */
35
+ export declare function encodeProjectId(projectId: string): string;
36
+ /**
37
+ * Find existing note by hidden header identifier.
38
+ *
39
+ * Searches MR notes for `<!-- {header} -->` pattern in body.
40
+ * Handles pagination for MRs with many notes.
41
+ *
42
+ * @param options - Note search options
43
+ * @returns Note ID if found, null otherwise
44
+ */
45
+ export declare function findExistingNote(options: Omit<GitLabNoteOptions, "body">): Promise<number | null>;
46
+ /**
47
+ * Post new note or update existing one.
48
+ *
49
+ * Uses header identifier to find existing note for update semantics.
50
+ * This prevents note spam and ensures only one VertaaUX note per MR.
51
+ *
52
+ * @param options - Note options
53
+ * @returns Created/updated note with ID and web URL
54
+ */
55
+ export declare function postOrUpdateGitLabNote(options: GitLabNoteOptions): Promise<{
56
+ id: number;
57
+ web_url: string;
58
+ }>;
59
+ /**
60
+ * Extract MR IID from GitLab CI environment.
61
+ *
62
+ * @returns MR IID or null if not in MR context
63
+ */
64
+ export declare function extractMRIid(): number | null;
65
+ /**
66
+ * Get GitLab API configuration from environment.
67
+ *
68
+ * @returns API configuration or null if not in GitLab CI
69
+ */
70
+ export declare function getGitLabConfig(): {
71
+ token: string;
72
+ apiUrl: string;
73
+ projectId: string;
74
+ } | null;
75
+ //# sourceMappingURL=gitlab-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab-api.d.ts","sourceRoot":"","sources":["../../src/ci/gitlab-api.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;CAChB;AAwED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAOzD;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,GACvC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgCxB;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA6B1C;AAyBD;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAO5C;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,IAAI,CAeP"}