@simonfestl/husky-cli 1.6.3 → 1.6.5

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.
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { Command } from "commander";
7
7
  import { QdrantClient } from "../../lib/biz/index.js";
8
+ import { requireRole } from "../../lib/permissions.js";
8
9
  export const qdrantCommand = new Command("qdrant")
9
10
  .description("Vector database operations (Qdrant)");
10
11
  // husky biz qdrant collections
@@ -167,4 +168,32 @@ qdrantCommand
167
168
  process.exit(1);
168
169
  }
169
170
  });
171
+ // husky biz qdrant delete-collection <name> (ADMIN ONLY)
172
+ qdrantCommand
173
+ .command("delete-collection <name>")
174
+ .description("Delete an entire collection (ADMIN ONLY)")
175
+ .option("-f, --force", "Skip confirmation")
176
+ .action(async (name, options) => {
177
+ try {
178
+ // Require admin role
179
+ requireRole("admin");
180
+ const client = QdrantClient.fromConfig();
181
+ // Get collection info first
182
+ const info = await client.getCollection(name);
183
+ if (!options.force) {
184
+ console.log(`\n ⚠️ DANGER: About to DELETE collection "${name}"`);
185
+ console.log(` Points: ${info.pointsCount}`);
186
+ console.log(` Vectors: ${info.vectorsCount}`);
187
+ console.log(`\n This action is IRREVERSIBLE!`);
188
+ console.log(` Use --force to confirm deletion\n`);
189
+ process.exit(0);
190
+ }
191
+ await client.deleteCollection(name);
192
+ console.log(`✓ Deleted collection "${name}"`);
193
+ }
194
+ catch (error) {
195
+ console.error("Error:", error.message);
196
+ process.exit(1);
197
+ }
198
+ });
170
199
  export default qdrantCommand;
@@ -135,7 +135,7 @@ ${subcommandCases}
135
135
  return 0
136
136
  ;;
137
137
  --agent)
138
- COMPREPLY=( $(compgen -W "claude-code gemini-cli custom" -- \${cur}) )
138
+ COMPREPLY=( $(compgen -W "claude-code gemini-cli opencode custom" -- \${cur}) )
139
139
  return 0
140
140
  ;;
141
141
  --type)
@@ -268,7 +268,7 @@ complete -c husky -l project -d "Project ID" -r
268
268
  complete -c husky -l status -d "Filter by status" -r -a "backlog in_progress review done pending running completed failed"
269
269
  complete -c husky -l priority -d "Priority level" -r -a "low medium high urgent must should could wont"
270
270
  complete -c husky -l assignee -d "Assignee type" -r -a "human llm unassigned"
271
- complete -c husky -l agent -d "Agent type" -r -a "claude-code gemini-cli custom"
271
+ complete -c husky -l agent -d "Agent type" -r -a "claude-code gemini-cli opencode custom"
272
272
  complete -c husky -l type -d "Type filter" -r -a "global project architecture patterns decisions learnings"
273
273
  complete -c husky -l value-stream -d "Value stream" -r -a "order_to_delivery procure_to_pay returns_management product_lifecycle customer_service marketing_sales finance_accounting hr_operations it_operations general"
274
274
  complete -c husky -l action -d "Action type" -r -a "manual semi_automated fully_automated"
@@ -0,0 +1,11 @@
1
+ /**
2
+ * E2E Testing commands for E2E Agent
3
+ *
4
+ * Commands for browser automation and E2E testing:
5
+ * - husky e2e run <task-id> - Run E2E tests for a task
6
+ * - husky e2e record <url> - Record a browser session with Playwright
7
+ * - husky e2e upload <file> - Upload recording to GCS bucket husky-files-tigerv0
8
+ * - husky e2e screenshot <url> - Take a screenshot of a URL
9
+ */
10
+ import { Command } from "commander";
11
+ export declare const e2eCommand: Command;
@@ -0,0 +1,528 @@
1
+ /**
2
+ * E2E Testing commands for E2E Agent
3
+ *
4
+ * Commands for browser automation and E2E testing:
5
+ * - husky e2e run <task-id> - Run E2E tests for a task
6
+ * - husky e2e record <url> - Record a browser session with Playwright
7
+ * - husky e2e upload <file> - Upload recording to GCS bucket husky-files-tigerv0
8
+ * - husky e2e screenshot <url> - Take a screenshot of a URL
9
+ */
10
+ import { Command } from "commander";
11
+ import { spawnSync } from "child_process";
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import { requireAnyPermission } from "../lib/permissions.js";
15
+ import { getConfig } from "./config.js";
16
+ export const e2eCommand = new Command("e2e")
17
+ .description("E2E testing and browser automation (E2E Agent)");
18
+ // GCS bucket for E2E artifacts
19
+ const GCS_BUCKET = "husky-files-tigerv0";
20
+ // Helper: Ensure API is configured
21
+ function ensureConfig() {
22
+ const config = getConfig();
23
+ if (!config.apiUrl) {
24
+ console.error("Error: API URL not configured. Run: husky config set api-url <url>");
25
+ process.exit(1);
26
+ }
27
+ return config;
28
+ }
29
+ // Helper: Run a command and return result
30
+ function runCommand(cmd, args, options) {
31
+ const result = spawnSync(cmd, args, {
32
+ encoding: "utf-8",
33
+ maxBuffer: 50 * 1024 * 1024,
34
+ timeout: options?.timeout || 300000, // 5 min default
35
+ cwd: options?.cwd,
36
+ });
37
+ return {
38
+ stdout: result.stdout || "",
39
+ stderr: result.stderr || "",
40
+ success: result.status === 0,
41
+ };
42
+ }
43
+ // Helper: Generate timestamp for filenames
44
+ function generateTimestamp() {
45
+ const now = new Date();
46
+ return now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
47
+ }
48
+ // Helper: Upload file to GCS
49
+ function uploadToGCS(localPath, gcsPath) {
50
+ const fullGcsPath = `gs://${GCS_BUCKET}/${gcsPath}`;
51
+ const result = runCommand("gcloud", [
52
+ "storage", "cp", localPath, fullGcsPath,
53
+ ]);
54
+ if (result.success) {
55
+ return {
56
+ success: true,
57
+ url: `https://storage.googleapis.com/${GCS_BUCKET}/${gcsPath}`,
58
+ };
59
+ }
60
+ console.error("GCS upload failed:", result.stderr);
61
+ return { success: false, url: "" };
62
+ }
63
+ // husky e2e run <task-id>
64
+ e2eCommand
65
+ .command("run <taskId>")
66
+ .description("Run E2E tests for a task")
67
+ .option("--headless", "Run in headless mode (default: true)", true)
68
+ .option("--headed", "Run in headed mode with visible browser")
69
+ .option("--browser <browser>", "Browser to use (chromium, firefox, webkit)", "chromium")
70
+ .option("--timeout <ms>", "Test timeout in milliseconds", "60000")
71
+ .option("--output <dir>", "Output directory for results")
72
+ .option("--json", "Output as JSON")
73
+ .action(async (taskId, options) => {
74
+ requireAnyPermission(["task:e2e", "deploy:sandbox", "e2e:*"]);
75
+ const config = ensureConfig();
76
+ const timestamp = generateTimestamp();
77
+ const outputDir = options.output || `/tmp/e2e-${taskId}-${timestamp}`;
78
+ console.log(`\n E2E Tests for Task: ${taskId}\n`);
79
+ console.log(` Output directory: ${outputDir}`);
80
+ console.log(` Browser: ${options.browser}`);
81
+ console.log(` Headless: ${!options.headed}`);
82
+ console.log("");
83
+ // Create output directory
84
+ if (!fs.existsSync(outputDir)) {
85
+ fs.mkdirSync(outputDir, { recursive: true });
86
+ }
87
+ // Fetch task details from API
88
+ let taskUrl;
89
+ let taskName;
90
+ try {
91
+ const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}`, {
92
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
93
+ });
94
+ if (res.ok) {
95
+ const task = await res.json();
96
+ taskName = task.title || task.name;
97
+ // Check if task has a preview URL or target URL
98
+ taskUrl = task.previewUrl || task.targetUrl || task.metadata?.url;
99
+ console.log(` Task: ${taskName}`);
100
+ if (taskUrl) {
101
+ console.log(` URL: ${taskUrl}`);
102
+ }
103
+ }
104
+ }
105
+ catch {
106
+ console.warn(" Warning: Could not fetch task details");
107
+ }
108
+ // Look for test files in current directory or task worktree
109
+ const testPatterns = [
110
+ "e2e/**/*.spec.ts",
111
+ "e2e/**/*.test.ts",
112
+ "tests/e2e/**/*.spec.ts",
113
+ "tests/e2e/**/*.test.ts",
114
+ "*.e2e.ts",
115
+ ];
116
+ console.log("\n Running Playwright tests...\n");
117
+ const playwrightArgs = [
118
+ "test",
119
+ "--reporter=list",
120
+ `--output=${outputDir}`,
121
+ `--timeout=${options.timeout}`,
122
+ ];
123
+ if (!options.headed) {
124
+ playwrightArgs.push("--headed=false");
125
+ }
126
+ else {
127
+ playwrightArgs.push("--headed");
128
+ }
129
+ if (options.browser) {
130
+ playwrightArgs.push(`--project=${options.browser}`);
131
+ }
132
+ const result = runCommand("npx", ["playwright", ...playwrightArgs], {
133
+ timeout: parseInt(options.timeout, 10) * 2, // Allow 2x timeout for test suite
134
+ });
135
+ const testResult = {
136
+ taskId,
137
+ taskName,
138
+ taskUrl,
139
+ success: result.success,
140
+ timestamp,
141
+ outputDir,
142
+ browser: options.browser,
143
+ stdout: result.stdout,
144
+ stderr: result.stderr,
145
+ };
146
+ // Save results to file
147
+ const resultsPath = path.join(outputDir, "results.json");
148
+ fs.writeFileSync(resultsPath, JSON.stringify(testResult, null, 2));
149
+ // Upload results to GCS
150
+ const gcsPath = `e2e/${taskId}/${timestamp}/results.json`;
151
+ const uploadResult = uploadToGCS(resultsPath, gcsPath);
152
+ if (options.json) {
153
+ console.log(JSON.stringify({
154
+ ...testResult,
155
+ gcsUrl: uploadResult.url,
156
+ }, null, 2));
157
+ }
158
+ else {
159
+ console.log(result.stdout);
160
+ if (result.stderr) {
161
+ console.error(result.stderr);
162
+ }
163
+ console.log("\n " + "═".repeat(50));
164
+ console.log(` Result: ${result.success ? "PASSED" : "FAILED"}`);
165
+ console.log(` Output: ${outputDir}`);
166
+ if (uploadResult.success) {
167
+ console.log(` GCS: ${uploadResult.url}`);
168
+ }
169
+ console.log("");
170
+ }
171
+ if (!result.success) {
172
+ process.exit(1);
173
+ }
174
+ });
175
+ // husky e2e record <url>
176
+ e2eCommand
177
+ .command("record <url>")
178
+ .description("Record a browser session with Playwright codegen")
179
+ .option("-o, --output <file>", "Output file for recorded script")
180
+ .option("--browser <browser>", "Browser to use (chromium, firefox, webkit)", "chromium")
181
+ .option("--device <device>", "Emulate device (e.g., 'iPhone 13', 'Pixel 5')")
182
+ .option("--viewport <size>", "Viewport size (e.g., '1280x720')")
183
+ .option("--video", "Record video of the session")
184
+ .option("--json", "Output as JSON")
185
+ .action(async (url, options) => {
186
+ requireAnyPermission(["task:e2e", "deploy:sandbox", "e2e:*"]);
187
+ const timestamp = generateTimestamp();
188
+ const outputFile = options.output || `/tmp/e2e-recording-${timestamp}.ts`;
189
+ const videoDir = `/tmp/e2e-video-${timestamp}`;
190
+ console.log(`\n Recording Browser Session\n`);
191
+ console.log(` URL: ${url}`);
192
+ console.log(` Browser: ${options.browser}`);
193
+ console.log(` Output: ${outputFile}`);
194
+ if (options.device) {
195
+ console.log(` Device: ${options.device}`);
196
+ }
197
+ if (options.viewport) {
198
+ console.log(` Viewport: ${options.viewport}`);
199
+ }
200
+ console.log("");
201
+ const codegenArgs = [
202
+ "playwright", "codegen",
203
+ url,
204
+ `-o`, outputFile,
205
+ `-b`, options.browser,
206
+ ];
207
+ if (options.device) {
208
+ codegenArgs.push("--device", options.device);
209
+ }
210
+ if (options.viewport) {
211
+ const [width, height] = options.viewport.split("x").map(Number);
212
+ codegenArgs.push("--viewport-size", `${width},${height}`);
213
+ }
214
+ if (options.video) {
215
+ if (!fs.existsSync(videoDir)) {
216
+ fs.mkdirSync(videoDir, { recursive: true });
217
+ }
218
+ codegenArgs.push("--save-storage", path.join(videoDir, "storage.json"));
219
+ }
220
+ console.log(" Starting Playwright Codegen...");
221
+ console.log(" Interact with the browser, then close it to save the recording.\n");
222
+ // Run codegen interactively
223
+ const codegen = spawnSync("npx", codegenArgs, {
224
+ stdio: "inherit",
225
+ encoding: "utf-8",
226
+ });
227
+ if (codegen.status === 0 && fs.existsSync(outputFile)) {
228
+ console.log(`\n Recording saved to: ${outputFile}`);
229
+ // Upload to GCS
230
+ const gcsPath = `e2e/recordings/${timestamp}/${path.basename(outputFile)}`;
231
+ const uploadResult = uploadToGCS(outputFile, gcsPath);
232
+ if (options.json) {
233
+ console.log(JSON.stringify({
234
+ success: true,
235
+ url,
236
+ outputFile,
237
+ gcsUrl: uploadResult.url,
238
+ timestamp,
239
+ }, null, 2));
240
+ }
241
+ else {
242
+ if (uploadResult.success) {
243
+ console.log(` Uploaded to: ${uploadResult.url}`);
244
+ }
245
+ console.log("\n To run this recording:");
246
+ console.log(` npx playwright test ${outputFile}`);
247
+ console.log("");
248
+ }
249
+ }
250
+ else {
251
+ console.error("\n Recording failed or was cancelled.");
252
+ process.exit(1);
253
+ }
254
+ });
255
+ // husky e2e upload <file>
256
+ e2eCommand
257
+ .command("upload <file>")
258
+ .description("Upload a file to the E2E GCS bucket")
259
+ .option("--task <taskId>", "Associate with a task ID")
260
+ .option("--type <type>", "File type (screenshot, video, recording, report)", "file")
261
+ .option("--json", "Output as JSON")
262
+ .action(async (file, options) => {
263
+ requireAnyPermission(["task:e2e", "deploy:sandbox", "e2e:*"]);
264
+ if (!fs.existsSync(file)) {
265
+ console.error(`Error: File not found: ${file}`);
266
+ process.exit(1);
267
+ }
268
+ const timestamp = generateTimestamp();
269
+ const filename = path.basename(file);
270
+ const taskPrefix = options.task ? `${options.task}/` : "";
271
+ const gcsPath = `e2e/${taskPrefix}${options.type}s/${timestamp}-${filename}`;
272
+ console.log(`\n Uploading to GCS\n`);
273
+ console.log(` File: ${file}`);
274
+ console.log(` Type: ${options.type}`);
275
+ if (options.task) {
276
+ console.log(` Task: ${options.task}`);
277
+ }
278
+ console.log("");
279
+ const uploadResult = uploadToGCS(file, gcsPath);
280
+ if (options.json) {
281
+ console.log(JSON.stringify({
282
+ success: uploadResult.success,
283
+ file,
284
+ gcsPath,
285
+ url: uploadResult.url,
286
+ timestamp,
287
+ }, null, 2));
288
+ }
289
+ else {
290
+ if (uploadResult.success) {
291
+ console.log(` Uploaded successfully!`);
292
+ console.log(` URL: ${uploadResult.url}`);
293
+ }
294
+ else {
295
+ console.error(` Upload failed.`);
296
+ process.exit(1);
297
+ }
298
+ }
299
+ console.log("");
300
+ });
301
+ // husky e2e screenshot <url>
302
+ e2eCommand
303
+ .command("screenshot <url>")
304
+ .description("Take a screenshot of a URL")
305
+ .option("-o, --output <file>", "Output file path")
306
+ .option("--full-page", "Capture full page (not just viewport)")
307
+ .option("--viewport <size>", "Viewport size (e.g., '1920x1080')", "1920x1080")
308
+ .option("--device <device>", "Emulate device (e.g., 'iPhone 13', 'Pixel 5')")
309
+ .option("--wait <ms>", "Wait time before screenshot in ms", "2000")
310
+ .option("--selector <selector>", "Wait for selector before screenshot")
311
+ .option("--task <taskId>", "Associate with a task ID")
312
+ .option("--upload", "Upload screenshot to GCS")
313
+ .option("--json", "Output as JSON")
314
+ .action(async (url, options) => {
315
+ requireAnyPermission(["task:e2e", "deploy:sandbox", "e2e:*"]);
316
+ const timestamp = generateTimestamp();
317
+ const defaultFilename = `screenshot-${timestamp}.png`;
318
+ const outputFile = options.output || `/tmp/${defaultFilename}`;
319
+ console.log(`\n Taking Screenshot\n`);
320
+ console.log(` URL: ${url}`);
321
+ console.log(` Viewport: ${options.viewport}`);
322
+ console.log(` Full page: ${options.fullPage || false}`);
323
+ if (options.device) {
324
+ console.log(` Device: ${options.device}`);
325
+ }
326
+ console.log(` Output: ${outputFile}`);
327
+ console.log("");
328
+ // Build Playwright script
329
+ const [width, height] = options.viewport.split("x").map(Number);
330
+ const deviceConfig = options.device ? `devices[${JSON.stringify(options.device)}]` : "null";
331
+ // Use JSON.stringify to safely encode user-provided values and prevent injection
332
+ const safeUrl = JSON.stringify(url);
333
+ const safeOutputFile = JSON.stringify(outputFile);
334
+ const safeSelector = options.selector ? JSON.stringify(options.selector) : null;
335
+ const script = `
336
+ const { chromium, devices } = require('playwright');
337
+
338
+ (async () => {
339
+ const browser = await chromium.launch({ headless: true });
340
+ const deviceConfig = ${deviceConfig};
341
+ const contextOptions = deviceConfig ? { ...deviceConfig } : {
342
+ viewport: { width: ${width}, height: ${height} }
343
+ };
344
+ const context = await browser.newContext(contextOptions);
345
+ const page = await context.newPage();
346
+
347
+ await page.goto(${safeUrl}, { waitUntil: 'networkidle' });
348
+
349
+ ${safeSelector ? `await page.waitForSelector(${safeSelector});` : ""}
350
+ await page.waitForTimeout(${options.wait});
351
+
352
+ await page.screenshot({
353
+ path: ${safeOutputFile},
354
+ fullPage: ${options.fullPage || false}
355
+ });
356
+
357
+ await browser.close();
358
+ console.log('Screenshot saved to: ' + ${safeOutputFile});
359
+ })();
360
+ `;
361
+ // Write and execute script
362
+ const scriptPath = `/tmp/screenshot-script-${timestamp}.js`;
363
+ fs.writeFileSync(scriptPath, script);
364
+ console.log(" Capturing screenshot...\n");
365
+ const result = runCommand("node", [scriptPath], { timeout: 60000 });
366
+ // Clean up script
367
+ try {
368
+ fs.unlinkSync(scriptPath);
369
+ }
370
+ catch {
371
+ // Ignore cleanup errors - temp script file may already be deleted
372
+ }
373
+ if (!result.success || !fs.existsSync(outputFile)) {
374
+ console.error(" Screenshot failed:");
375
+ console.error(result.stderr || result.stdout);
376
+ process.exit(1);
377
+ }
378
+ const fileStats = fs.statSync(outputFile);
379
+ let gcsUrl = "";
380
+ // Upload if requested
381
+ if (options.upload) {
382
+ const taskPrefix = options.task ? `${options.task}/` : "";
383
+ const gcsPath = `e2e/${taskPrefix}screenshots/${defaultFilename}`;
384
+ const uploadResult = uploadToGCS(outputFile, gcsPath);
385
+ gcsUrl = uploadResult.url;
386
+ }
387
+ if (options.json) {
388
+ console.log(JSON.stringify({
389
+ success: true,
390
+ url,
391
+ outputFile,
392
+ fileSize: fileStats.size,
393
+ viewport: options.viewport,
394
+ fullPage: options.fullPage || false,
395
+ gcsUrl: gcsUrl || undefined,
396
+ timestamp,
397
+ }, null, 2));
398
+ }
399
+ else {
400
+ console.log(` Screenshot captured successfully!`);
401
+ console.log(` File: ${outputFile}`);
402
+ console.log(` Size: ${(fileStats.size / 1024).toFixed(1)} KB`);
403
+ if (gcsUrl) {
404
+ console.log(` GCS: ${gcsUrl}`);
405
+ }
406
+ console.log("");
407
+ }
408
+ });
409
+ // husky e2e list
410
+ e2eCommand
411
+ .command("list")
412
+ .description("List E2E artifacts in GCS bucket")
413
+ .option("--task <taskId>", "Filter by task ID")
414
+ .option("--type <type>", "Filter by type (screenshots, videos, recordings, reports)")
415
+ .option("--limit <n>", "Limit number of results", "20")
416
+ .option("--json", "Output as JSON")
417
+ .action(async (options) => {
418
+ requireAnyPermission(["task:e2e", "deploy:sandbox", "e2e:*"]);
419
+ let gcsPath = `gs://${GCS_BUCKET}/e2e/`;
420
+ if (options.task) {
421
+ gcsPath += `${options.task}/`;
422
+ }
423
+ if (options.type) {
424
+ gcsPath += `${options.type}/`;
425
+ }
426
+ console.log(`\n E2E Artifacts\n`);
427
+ console.log(` Bucket: ${GCS_BUCKET}`);
428
+ if (options.task) {
429
+ console.log(` Task: ${options.task}`);
430
+ }
431
+ if (options.type) {
432
+ console.log(` Type: ${options.type}`);
433
+ }
434
+ console.log("");
435
+ const result = runCommand("gcloud", [
436
+ "storage", "ls", "-l", gcsPath,
437
+ ]);
438
+ if (!result.success) {
439
+ if (result.stderr.includes("CommandException") || result.stderr.includes("not found")) {
440
+ console.log(" No artifacts found.");
441
+ }
442
+ else {
443
+ console.error("Error listing artifacts:", result.stderr);
444
+ }
445
+ return;
446
+ }
447
+ const lines = result.stdout.trim().split("\n").filter(Boolean);
448
+ const artifacts = lines
449
+ .slice(0, parseInt(options.limit, 10))
450
+ .map((line) => {
451
+ const parts = line.trim().split(/\s+/);
452
+ if (parts.length >= 3) {
453
+ return {
454
+ size: parts[0],
455
+ date: parts[1],
456
+ path: parts[2],
457
+ };
458
+ }
459
+ return null;
460
+ })
461
+ .filter(Boolean);
462
+ if (options.json) {
463
+ console.log(JSON.stringify({ artifacts }, null, 2));
464
+ }
465
+ else {
466
+ console.log(" " + "-".repeat(70));
467
+ console.log(` ${"SIZE".padEnd(12)} ${"DATE".padEnd(20)} PATH`);
468
+ console.log(" " + "-".repeat(70));
469
+ for (const artifact of artifacts) {
470
+ if (artifact) {
471
+ const displayPath = artifact.path.replace(`gs://${GCS_BUCKET}/`, "");
472
+ console.log(` ${artifact.size.padEnd(12)} ${artifact.date.padEnd(20)} ${displayPath}`);
473
+ }
474
+ }
475
+ console.log("");
476
+ }
477
+ });
478
+ // husky e2e clean
479
+ e2eCommand
480
+ .command("clean")
481
+ .description("Clean up local E2E artifacts")
482
+ .option("--all", "Remove all E2E artifacts from /tmp")
483
+ .option("--older-than <days>", "Remove artifacts older than N days", "7")
484
+ .option("--dry-run", "Show what would be deleted without deleting")
485
+ .action(async (options) => {
486
+ requireAnyPermission(["task:e2e", "deploy:sandbox", "e2e:*"]);
487
+ console.log(`\n Cleaning E2E Artifacts\n`);
488
+ const tmpDir = "/tmp";
489
+ const e2ePatterns = ["e2e-", "screenshot-", "recording-"];
490
+ const maxAge = parseInt(options.olderThan, 10) * 24 * 60 * 60 * 1000;
491
+ const now = Date.now();
492
+ let cleaned = 0;
493
+ let totalSize = 0;
494
+ try {
495
+ const files = fs.readdirSync(tmpDir);
496
+ for (const file of files) {
497
+ const isE2E = e2ePatterns.some((p) => file.startsWith(p));
498
+ if (!isE2E)
499
+ continue;
500
+ const filePath = path.join(tmpDir, file);
501
+ const stats = fs.statSync(filePath);
502
+ const age = now - stats.mtime.getTime();
503
+ if (options.all || age > maxAge) {
504
+ if (options.dryRun) {
505
+ console.log(` Would delete: ${file} (${(stats.size / 1024).toFixed(1)} KB)`);
506
+ }
507
+ else {
508
+ if (stats.isDirectory()) {
509
+ fs.rmSync(filePath, { recursive: true });
510
+ }
511
+ else {
512
+ fs.unlinkSync(filePath);
513
+ }
514
+ console.log(` Deleted: ${file}`);
515
+ }
516
+ cleaned++;
517
+ totalSize += stats.size;
518
+ }
519
+ }
520
+ }
521
+ catch (error) {
522
+ console.error("Error cleaning artifacts:", error);
523
+ process.exit(1);
524
+ }
525
+ console.log("");
526
+ console.log(` ${options.dryRun ? "Would clean" : "Cleaned"}: ${cleaned} items (${(totalSize / 1024 / 1024).toFixed(2)} MB)`);
527
+ console.log("");
528
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Infrastructure commands for DevOps agent
3
+ *
4
+ * Commands for monitoring and managing GCP infrastructure:
5
+ * - husky infra status - Show all services status
6
+ * - husky infra vms - List VMs with status
7
+ * - husky infra logs <service> - View recent logs
8
+ * - husky infra scale <pool> --count <n> - Scale VM pool
9
+ */
10
+ import { Command } from "commander";
11
+ export declare const infraCommand: Command;