@enactprotocol/cli 1.2.13 → 2.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 (73) hide show
  1. package/README.md +88 -0
  2. package/package.json +34 -38
  3. package/src/commands/auth/index.ts +940 -0
  4. package/src/commands/cache/index.ts +361 -0
  5. package/src/commands/config/README.md +239 -0
  6. package/src/commands/config/index.ts +164 -0
  7. package/src/commands/env/README.md +197 -0
  8. package/src/commands/env/index.ts +392 -0
  9. package/src/commands/exec/README.md +110 -0
  10. package/src/commands/exec/index.ts +195 -0
  11. package/src/commands/get/index.ts +198 -0
  12. package/src/commands/index.ts +30 -0
  13. package/src/commands/inspect/index.ts +264 -0
  14. package/src/commands/install/README.md +146 -0
  15. package/src/commands/install/index.ts +682 -0
  16. package/src/commands/list/README.md +115 -0
  17. package/src/commands/list/index.ts +138 -0
  18. package/src/commands/publish/index.ts +350 -0
  19. package/src/commands/report/index.ts +366 -0
  20. package/src/commands/run/README.md +124 -0
  21. package/src/commands/run/index.ts +686 -0
  22. package/src/commands/search/index.ts +368 -0
  23. package/src/commands/setup/index.ts +274 -0
  24. package/src/commands/sign/index.ts +652 -0
  25. package/src/commands/trust/README.md +214 -0
  26. package/src/commands/trust/index.ts +453 -0
  27. package/src/commands/unyank/index.ts +107 -0
  28. package/src/commands/yank/index.ts +143 -0
  29. package/src/index.ts +96 -0
  30. package/src/types.ts +81 -0
  31. package/src/utils/errors.ts +409 -0
  32. package/src/utils/exit-codes.ts +159 -0
  33. package/src/utils/ignore.ts +147 -0
  34. package/src/utils/index.ts +107 -0
  35. package/src/utils/output.ts +242 -0
  36. package/src/utils/spinner.ts +214 -0
  37. package/tests/commands/auth.test.ts +217 -0
  38. package/tests/commands/cache.test.ts +286 -0
  39. package/tests/commands/config.test.ts +277 -0
  40. package/tests/commands/env.test.ts +293 -0
  41. package/tests/commands/exec.test.ts +112 -0
  42. package/tests/commands/get.test.ts +179 -0
  43. package/tests/commands/inspect.test.ts +201 -0
  44. package/tests/commands/install-integration.test.ts +343 -0
  45. package/tests/commands/install.test.ts +288 -0
  46. package/tests/commands/list.test.ts +160 -0
  47. package/tests/commands/publish.test.ts +186 -0
  48. package/tests/commands/report.test.ts +194 -0
  49. package/tests/commands/run.test.ts +231 -0
  50. package/tests/commands/search.test.ts +131 -0
  51. package/tests/commands/sign.test.ts +164 -0
  52. package/tests/commands/trust.test.ts +236 -0
  53. package/tests/commands/unyank.test.ts +114 -0
  54. package/tests/commands/yank.test.ts +154 -0
  55. package/tests/e2e.test.ts +554 -0
  56. package/tests/fixtures/calculator/enact.yaml +34 -0
  57. package/tests/fixtures/echo-tool/enact.md +31 -0
  58. package/tests/fixtures/env-tool/enact.yaml +19 -0
  59. package/tests/fixtures/greeter/enact.yaml +18 -0
  60. package/tests/fixtures/invalid-tool/enact.yaml +4 -0
  61. package/tests/index.test.ts +8 -0
  62. package/tests/types.test.ts +84 -0
  63. package/tests/utils/errors.test.ts +303 -0
  64. package/tests/utils/exit-codes.test.ts +189 -0
  65. package/tests/utils/ignore.test.ts +461 -0
  66. package/tests/utils/output.test.ts +126 -0
  67. package/tsconfig.json +17 -0
  68. package/tsconfig.tsbuildinfo +1 -0
  69. package/dist/index.js +0 -231612
  70. package/dist/index.js.bak +0 -231611
  71. package/dist/web/static/app.js +0 -663
  72. package/dist/web/static/index.html +0 -117
  73. package/dist/web/static/style.css +0 -291
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Error handling utilities for CLI
3
+ *
4
+ * Provides consistent error handling with helpful suggestions
5
+ */
6
+
7
+ import {
8
+ EXIT_AUTH_ERROR,
9
+ EXIT_CONFIG,
10
+ EXIT_CONTAINER_ERROR,
11
+ EXIT_EXECUTION_ERROR,
12
+ EXIT_FAILURE,
13
+ EXIT_MANIFEST_ERROR,
14
+ EXIT_NETWORK_ERROR,
15
+ EXIT_NOINPUT,
16
+ EXIT_NOPERM,
17
+ EXIT_REGISTRY_ERROR,
18
+ EXIT_TIMEOUT,
19
+ EXIT_TOOL_NOT_FOUND,
20
+ EXIT_TRUST_ERROR,
21
+ EXIT_USAGE,
22
+ EXIT_VALIDATION_ERROR,
23
+ } from "./exit-codes";
24
+ import { colors, dim, newline, error as printError, suggest } from "./output";
25
+
26
+ // ============================================================================
27
+ // Error Classes
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Base CLI error with exit code
32
+ */
33
+ export class CliError extends Error {
34
+ constructor(
35
+ message: string,
36
+ public readonly exitCode: number = EXIT_FAILURE,
37
+ public readonly suggestion?: string
38
+ ) {
39
+ super(message);
40
+ this.name = "CliError";
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Tool not found error
46
+ */
47
+ export class ToolNotFoundError extends CliError {
48
+ constructor(toolName: string) {
49
+ super(
50
+ `Tool not found: ${toolName}`,
51
+ EXIT_TOOL_NOT_FOUND,
52
+ "Check the tool name or provide a path to a local tool.\nFor registry tools, use the format: owner/namespace/tool[@version]"
53
+ );
54
+ this.name = "ToolNotFoundError";
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Manifest error
60
+ */
61
+ export class ManifestError extends CliError {
62
+ constructor(message: string, path?: string) {
63
+ const fullMessage = path ? `${message} in ${path}` : message;
64
+ super(
65
+ fullMessage,
66
+ EXIT_MANIFEST_ERROR,
67
+ "Ensure the directory contains a valid enact.yaml or enact.md file."
68
+ );
69
+ this.name = "ManifestError";
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Validation error
75
+ */
76
+ export class ValidationError extends CliError {
77
+ constructor(message: string, field?: string) {
78
+ const fullMessage = field ? `Invalid ${field}: ${message}` : message;
79
+ super(fullMessage, EXIT_VALIDATION_ERROR);
80
+ this.name = "ValidationError";
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Authentication error
86
+ */
87
+ export class AuthError extends CliError {
88
+ constructor(message: string) {
89
+ super(message, EXIT_AUTH_ERROR, "Run 'enact auth login' to authenticate.");
90
+ this.name = "AuthError";
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Network error
96
+ */
97
+ export class NetworkError extends CliError {
98
+ constructor(message: string) {
99
+ super(message, EXIT_NETWORK_ERROR, "Check your network connection and try again.");
100
+ this.name = "NetworkError";
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Registry error
106
+ */
107
+ export class RegistryError extends CliError {
108
+ constructor(message: string) {
109
+ super(
110
+ message,
111
+ EXIT_REGISTRY_ERROR,
112
+ "The registry may be temporarily unavailable. Try again later."
113
+ );
114
+ this.name = "RegistryError";
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Trust verification error
120
+ */
121
+ export class TrustError extends CliError {
122
+ constructor(message: string) {
123
+ super(
124
+ message,
125
+ EXIT_TRUST_ERROR,
126
+ "Verify the tool's attestations with 'enact trust check <tool>'"
127
+ );
128
+ this.name = "TrustError";
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Timeout error
134
+ */
135
+ export class TimeoutError extends CliError {
136
+ constructor(operation: string, timeoutMs: number) {
137
+ const timeoutSec = Math.round(timeoutMs / 1000);
138
+ super(
139
+ `${operation} timed out after ${timeoutSec}s`,
140
+ EXIT_TIMEOUT,
141
+ "Try increasing the timeout with --timeout option."
142
+ );
143
+ this.name = "TimeoutError";
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Execution error
149
+ */
150
+ export class ExecutionError extends CliError {
151
+ constructor(
152
+ message: string,
153
+ public readonly stderr?: string
154
+ ) {
155
+ super(message, EXIT_EXECUTION_ERROR);
156
+ this.name = "ExecutionError";
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Container error
162
+ */
163
+ export class ContainerError extends CliError {
164
+ constructor(message: string) {
165
+ super(message, EXIT_CONTAINER_ERROR, "Ensure Docker or another container runtime is running.");
166
+ this.name = "ContainerError";
167
+ }
168
+ }
169
+
170
+ /**
171
+ * File not found error
172
+ */
173
+ export class FileNotFoundError extends CliError {
174
+ constructor(path: string) {
175
+ super(`File not found: ${path}`, EXIT_NOINPUT);
176
+ this.name = "FileNotFoundError";
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Permission error
182
+ */
183
+ export class PermissionError extends CliError {
184
+ constructor(path: string) {
185
+ super(`Permission denied: ${path}`, EXIT_NOPERM);
186
+ this.name = "PermissionError";
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Configuration error
192
+ */
193
+ export class ConfigError extends CliError {
194
+ constructor(message: string) {
195
+ super(message, EXIT_CONFIG, "Check your configuration with 'enact config list'.");
196
+ this.name = "ConfigError";
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Usage error (invalid arguments)
202
+ */
203
+ export class UsageError extends CliError {
204
+ constructor(message: string) {
205
+ super(message, EXIT_USAGE);
206
+ this.name = "UsageError";
207
+ }
208
+ }
209
+
210
+ // ============================================================================
211
+ // Error Handling
212
+ // ============================================================================
213
+
214
+ /**
215
+ * Handle an error and exit with appropriate code
216
+ */
217
+ export function handleError(err: unknown, options?: { verbose: boolean }): never {
218
+ if (err instanceof CliError) {
219
+ printError(err.message);
220
+ if (err.suggestion) {
221
+ suggest(err.suggestion);
222
+ }
223
+ if (options?.verbose && err.stack) {
224
+ newline();
225
+ dim(err.stack);
226
+ }
227
+ process.exit(err.exitCode);
228
+ }
229
+
230
+ // Handle standard errors
231
+ if (err instanceof Error) {
232
+ printError(err.message);
233
+ if (options?.verbose && err.stack) {
234
+ newline();
235
+ dim(err.stack);
236
+ }
237
+ process.exit(EXIT_FAILURE);
238
+ }
239
+
240
+ // Handle unknown errors
241
+ printError(String(err));
242
+ process.exit(EXIT_FAILURE);
243
+ }
244
+
245
+ /**
246
+ * Wrap an async function with error handling
247
+ */
248
+ export function withErrorHandling<T extends unknown[]>(
249
+ fn: (...args: T) => Promise<void>,
250
+ options?: { verbose: boolean }
251
+ ): (...args: T) => Promise<void> {
252
+ return async (...args: T) => {
253
+ try {
254
+ await fn(...args);
255
+ } catch (err) {
256
+ handleError(err, options);
257
+ }
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Categorize an error and return appropriate CliError
263
+ */
264
+ export function categorizeError(err: unknown): CliError {
265
+ if (err instanceof CliError) {
266
+ return err;
267
+ }
268
+
269
+ if (err instanceof Error) {
270
+ const message = err.message.toLowerCase();
271
+
272
+ // Network errors
273
+ if (
274
+ message.includes("network") ||
275
+ message.includes("econnrefused") ||
276
+ message.includes("enotfound") ||
277
+ message.includes("etimedout") ||
278
+ message.includes("fetch failed")
279
+ ) {
280
+ return new NetworkError(err.message);
281
+ }
282
+
283
+ // Permission errors
284
+ if (
285
+ message.includes("eacces") ||
286
+ message.includes("permission denied") ||
287
+ message.includes("eperm")
288
+ ) {
289
+ return new PermissionError(err.message);
290
+ }
291
+
292
+ // File not found
293
+ if (message.includes("enoent") || message.includes("no such file")) {
294
+ return new FileNotFoundError(err.message);
295
+ }
296
+
297
+ // Timeout
298
+ if (message.includes("timeout") || message.includes("timed out")) {
299
+ return new TimeoutError("Operation", 30000);
300
+ }
301
+
302
+ // Authentication
303
+ if (
304
+ message.includes("unauthorized") ||
305
+ message.includes("401") ||
306
+ message.includes("authentication")
307
+ ) {
308
+ return new AuthError(err.message);
309
+ }
310
+
311
+ // Return generic CliError
312
+ return new CliError(err.message);
313
+ }
314
+
315
+ return new CliError(String(err));
316
+ }
317
+
318
+ // ============================================================================
319
+ // Error Messages with Actions
320
+ // ============================================================================
321
+
322
+ /**
323
+ * Common error messages with actionable suggestions
324
+ */
325
+ export const ErrorMessages = {
326
+ toolNotFound: (name: string) => ({
327
+ message: `Tool "${name}" not found`,
328
+ suggestions: [
329
+ "Check the tool name spelling",
330
+ `Search for tools: ${colors.command("enact search <query>")}`,
331
+ `Install from path: ${colors.command("enact install ./path/to/tool")}`,
332
+ ],
333
+ }),
334
+
335
+ notAuthenticated: () => ({
336
+ message: "You are not authenticated",
337
+ suggestions: [
338
+ `Log in: ${colors.command("enact auth login")}`,
339
+ `Check status: ${colors.command("enact auth status")}`,
340
+ ],
341
+ }),
342
+
343
+ manifestNotFound: (dir: string) => ({
344
+ message: `No manifest found in ${dir}`,
345
+ suggestions: [
346
+ `Create a manifest: ${colors.command("enact init")}`,
347
+ "Ensure the directory contains enact.yaml or enact.md",
348
+ ],
349
+ }),
350
+
351
+ invalidManifest: (errors: string[]) => ({
352
+ message: "Invalid manifest",
353
+ suggestions: [
354
+ "Fix the following errors:",
355
+ ...errors.map((e) => ` ${colors.error("•")} ${e}`),
356
+ `Validate manifest: ${colors.command("enact validate")}`,
357
+ ],
358
+ }),
359
+
360
+ registryUnavailable: () => ({
361
+ message: "Registry is unavailable",
362
+ suggestions: [
363
+ "Check your internet connection",
364
+ `Verify registry URL: ${colors.command("enact config get registry.url")}`,
365
+ "Try again later",
366
+ ],
367
+ }),
368
+
369
+ containerRuntimeNotFound: () => ({
370
+ message: "No container runtime found",
371
+ suggestions: [
372
+ "Install Docker: https://docs.docker.com/get-docker/",
373
+ "Or install Podman: https://podman.io/getting-started/installation",
374
+ "Ensure the runtime is running",
375
+ ],
376
+ }),
377
+
378
+ trustVerificationFailed: (tool: string) => ({
379
+ message: `Trust verification failed for "${tool}"`,
380
+ suggestions: [
381
+ "The tool has no valid attestations",
382
+ `Add a trusted publisher: ${colors.command("enact trust add <identity>")}`,
383
+ `Check attestations: ${colors.command(`enact trust check ${tool}`)}`,
384
+ ],
385
+ }),
386
+
387
+ executionFailed: (tool: string, exitCode: number) => ({
388
+ message: `Tool "${tool}" failed with exit code ${exitCode}`,
389
+ suggestions: [
390
+ "Check tool logs for details",
391
+ "Verify input parameters",
392
+ `Run with verbose output: ${colors.command("enact run --verbose")}`,
393
+ ],
394
+ }),
395
+ };
396
+
397
+ /**
398
+ * Print an error with actionable suggestions
399
+ */
400
+ export function printErrorWithSuggestions(errorInfo: {
401
+ message: string;
402
+ suggestions: string[];
403
+ }): void {
404
+ printError(errorInfo.message);
405
+ newline();
406
+ for (const suggestion of errorInfo.suggestions) {
407
+ console.log(` ${colors.dim("•")} ${suggestion}`);
408
+ }
409
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Exit codes for CLI commands
3
+ *
4
+ * Standardized exit codes following Unix conventions:
5
+ * - 0: Success
6
+ * - 1: General error
7
+ * - 2: Misuse of shell command (invalid args)
8
+ * - 64-78: BSD sysexits.h codes
9
+ * - 128+: Signal-based exits
10
+ *
11
+ * @see https://www.freebsd.org/cgi/man.cgi?query=sysexits
12
+ */
13
+
14
+ /** Success - command completed successfully */
15
+ export const EXIT_SUCCESS = 0;
16
+
17
+ /** General error - unspecified failure */
18
+ export const EXIT_FAILURE = 1;
19
+
20
+ /** Usage error - invalid command line arguments */
21
+ export const EXIT_USAGE = 2;
22
+
23
+ /** Data error - input data was incorrect */
24
+ export const EXIT_DATAERR = 65;
25
+
26
+ /** No input - input file did not exist or was not readable */
27
+ export const EXIT_NOINPUT = 66;
28
+
29
+ /** No user - user does not exist */
30
+ export const EXIT_NOUSER = 67;
31
+
32
+ /** No host - host does not exist */
33
+ export const EXIT_NOHOST = 68;
34
+
35
+ /** Service unavailable - a required service is unavailable */
36
+ export const EXIT_UNAVAILABLE = 69;
37
+
38
+ /** Software error - internal error */
39
+ export const EXIT_SOFTWARE = 70;
40
+
41
+ /** OS error - system error */
42
+ export const EXIT_OSERR = 71;
43
+
44
+ /** OS file - system file missing or not creatable */
45
+ export const EXIT_OSFILE = 72;
46
+
47
+ /** Can't create - output file cannot be created */
48
+ export const EXIT_CANTCREAT = 73;
49
+
50
+ /** I/O error - input/output error */
51
+ export const EXIT_IOERR = 74;
52
+
53
+ /** Temp failure - temporary failure, try again */
54
+ export const EXIT_TEMPFAIL = 75;
55
+
56
+ /** Protocol error - remote error in protocol */
57
+ export const EXIT_PROTOCOL = 76;
58
+
59
+ /** No permission - permission denied */
60
+ export const EXIT_NOPERM = 77;
61
+
62
+ /** Configuration error - configuration error */
63
+ export const EXIT_CONFIG = 78;
64
+
65
+ // ============================================================================
66
+ // Enact-specific exit codes (100-119)
67
+ // ============================================================================
68
+
69
+ /** Tool not found */
70
+ export const EXIT_TOOL_NOT_FOUND = 100;
71
+
72
+ /** Manifest error - invalid or missing manifest */
73
+ export const EXIT_MANIFEST_ERROR = 101;
74
+
75
+ /** Execution error - tool execution failed */
76
+ export const EXIT_EXECUTION_ERROR = 102;
77
+
78
+ /** Timeout error - operation timed out */
79
+ export const EXIT_TIMEOUT = 103;
80
+
81
+ /** Trust error - trust verification failed */
82
+ export const EXIT_TRUST_ERROR = 104;
83
+
84
+ /** Registry error - registry communication failed */
85
+ export const EXIT_REGISTRY_ERROR = 105;
86
+
87
+ /** Authentication error - not authenticated or token expired */
88
+ export const EXIT_AUTH_ERROR = 106;
89
+
90
+ /** Validation error - input validation failed */
91
+ export const EXIT_VALIDATION_ERROR = 107;
92
+
93
+ /** Network error - network communication failed */
94
+ export const EXIT_NETWORK_ERROR = 108;
95
+
96
+ /** Container error - container runtime error */
97
+ export const EXIT_CONTAINER_ERROR = 109;
98
+
99
+ // ============================================================================
100
+ // Exit code descriptions
101
+ // ============================================================================
102
+
103
+ const EXIT_CODE_DESCRIPTIONS: Record<number, string> = {
104
+ [EXIT_SUCCESS]: "Success",
105
+ [EXIT_FAILURE]: "General error",
106
+ [EXIT_USAGE]: "Invalid command line arguments",
107
+ [EXIT_DATAERR]: "Input data was incorrect",
108
+ [EXIT_NOINPUT]: "Input file not found or not readable",
109
+ [EXIT_NOUSER]: "User not found",
110
+ [EXIT_NOHOST]: "Host not found",
111
+ [EXIT_UNAVAILABLE]: "Service unavailable",
112
+ [EXIT_SOFTWARE]: "Internal software error",
113
+ [EXIT_OSERR]: "System error",
114
+ [EXIT_OSFILE]: "System file missing",
115
+ [EXIT_CANTCREAT]: "Cannot create output file",
116
+ [EXIT_IOERR]: "I/O error",
117
+ [EXIT_TEMPFAIL]: "Temporary failure, try again",
118
+ [EXIT_PROTOCOL]: "Protocol error",
119
+ [EXIT_NOPERM]: "Permission denied",
120
+ [EXIT_CONFIG]: "Configuration error",
121
+ [EXIT_TOOL_NOT_FOUND]: "Tool not found",
122
+ [EXIT_MANIFEST_ERROR]: "Manifest error",
123
+ [EXIT_EXECUTION_ERROR]: "Tool execution failed",
124
+ [EXIT_TIMEOUT]: "Operation timed out",
125
+ [EXIT_TRUST_ERROR]: "Trust verification failed",
126
+ [EXIT_REGISTRY_ERROR]: "Registry error",
127
+ [EXIT_AUTH_ERROR]: "Authentication error",
128
+ [EXIT_VALIDATION_ERROR]: "Validation error",
129
+ [EXIT_NETWORK_ERROR]: "Network error",
130
+ [EXIT_CONTAINER_ERROR]: "Container runtime error",
131
+ };
132
+
133
+ /**
134
+ * Get description for an exit code
135
+ */
136
+ export function getExitCodeDescription(code: number): string {
137
+ return EXIT_CODE_DESCRIPTIONS[code] ?? `Unknown error (code ${code})`;
138
+ }
139
+
140
+ /**
141
+ * Exit with a specific code
142
+ */
143
+ export function exit(code: number): never {
144
+ process.exit(code);
145
+ }
146
+
147
+ /**
148
+ * Exit with success
149
+ */
150
+ export function exitSuccess(): never {
151
+ process.exit(EXIT_SUCCESS);
152
+ }
153
+
154
+ /**
155
+ * Exit with failure
156
+ */
157
+ export function exitFailure(): never {
158
+ process.exit(EXIT_FAILURE);
159
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * File ignore utilities for bundling
3
+ *
4
+ * Provides gitignore-style pattern matching and default ignore lists
5
+ * to prevent sensitive files from being included in tool bundles.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ /** Files/patterns that should never be included in bundles */
12
+ export const ALWAYS_IGNORE = [
13
+ // Environment and secrets
14
+ ".env",
15
+ ".env.local",
16
+ ".env.development",
17
+ ".env.production",
18
+ ".env.*.local",
19
+ "*.pem",
20
+ "*.key",
21
+ "*.p12",
22
+ "*.pfx",
23
+
24
+ // Version control
25
+ ".git",
26
+ ".gitignore",
27
+ ".gitattributes",
28
+
29
+ // IDE/Editor
30
+ ".vscode",
31
+ ".idea",
32
+ "*.swp",
33
+ "*.swo",
34
+ "*~",
35
+
36
+ // OS files
37
+ ".DS_Store",
38
+ "Thumbs.db",
39
+ "desktop.ini",
40
+
41
+ // Dependencies
42
+ "node_modules",
43
+ "vendor",
44
+ "__pycache__",
45
+ "*.pyc",
46
+ ".venv",
47
+ "venv",
48
+
49
+ // Build artifacts
50
+ "dist",
51
+ "build",
52
+ "out",
53
+ "target",
54
+ "*.log",
55
+ ];
56
+
57
+ /**
58
+ * Load and parse a .gitignore file from a directory
59
+ */
60
+ export function loadGitignore(toolDir: string): string[] {
61
+ const gitignorePath = join(toolDir, ".gitignore");
62
+ if (!existsSync(gitignorePath)) {
63
+ return [];
64
+ }
65
+
66
+ const content = readFileSync(gitignorePath, "utf-8");
67
+ return parseGitignoreContent(content);
68
+ }
69
+
70
+ /**
71
+ * Parse gitignore content string into patterns
72
+ */
73
+ export function parseGitignoreContent(content: string): string[] {
74
+ return content
75
+ .split("\n")
76
+ .map((line) => line.trim())
77
+ .filter((line) => line && !line.startsWith("#"));
78
+ }
79
+
80
+ /**
81
+ * Check if a path matches a gitignore-style pattern
82
+ */
83
+ export function matchesPattern(relativePath: string, pattern: string): boolean {
84
+ // Handle negation patterns (we don't support re-including for now)
85
+ if (pattern.startsWith("!")) {
86
+ return false;
87
+ }
88
+
89
+ // Normalize pattern
90
+ let normalizedPattern = pattern;
91
+
92
+ // Handle directory-specific patterns (ending with /)
93
+ const isDirPattern = pattern.endsWith("/");
94
+ if (isDirPattern) {
95
+ normalizedPattern = pattern.slice(0, -1);
96
+ }
97
+
98
+ // Handle patterns starting with /
99
+ const isRooted = pattern.startsWith("/");
100
+ if (isRooted) {
101
+ normalizedPattern = normalizedPattern.slice(1);
102
+ }
103
+
104
+ // Simple glob matching
105
+ const regexPattern = normalizedPattern
106
+ .replace(/\./g, "\\.")
107
+ .replace(/\*\*/g, "<<<GLOBSTAR>>>")
108
+ .replace(/\*/g, "[^/]*")
109
+ .replace(/<<<GLOBSTAR>>>/g, ".*")
110
+ .replace(/\?/g, ".");
111
+
112
+ const regex = isRooted
113
+ ? new RegExp(`^${regexPattern}($|/)`)
114
+ : new RegExp(`(^|/)${regexPattern}($|/)`);
115
+
116
+ return regex.test(relativePath);
117
+ }
118
+
119
+ /**
120
+ * Check if a file should be ignored based on patterns
121
+ */
122
+ export function shouldIgnore(
123
+ relativePath: string,
124
+ fileName: string,
125
+ ignorePatterns: string[] = []
126
+ ): boolean {
127
+ // Always skip hidden files (starting with .)
128
+ if (fileName.startsWith(".")) {
129
+ return true;
130
+ }
131
+
132
+ // Check against always-ignore list
133
+ for (const pattern of ALWAYS_IGNORE) {
134
+ if (matchesPattern(relativePath, pattern) || matchesPattern(fileName, pattern)) {
135
+ return true;
136
+ }
137
+ }
138
+
139
+ // Check against gitignore patterns
140
+ for (const pattern of ignorePatterns) {
141
+ if (matchesPattern(relativePath, pattern)) {
142
+ return true;
143
+ }
144
+ }
145
+
146
+ return false;
147
+ }