mdcontext 0.1.0 → 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 (251) hide show
  1. package/.changeset/config.json +9 -9
  2. package/.claude/settings.local.json +25 -0
  3. package/.github/workflows/claude-code-review.yml +44 -0
  4. package/.github/workflows/claude.yml +85 -0
  5. package/CONTRIBUTING.md +186 -0
  6. package/NOTES/NOTES +44 -0
  7. package/README.md +206 -3
  8. package/biome.json +1 -1
  9. package/dist/chunk-23UPXDNL.js +3044 -0
  10. package/dist/chunk-2W7MO2DL.js +1366 -0
  11. package/dist/chunk-3NUAZGMA.js +1689 -0
  12. package/dist/chunk-7TOWB2XB.js +366 -0
  13. package/dist/chunk-7XOTOADQ.js +3065 -0
  14. package/dist/chunk-AH2PDM2K.js +3042 -0
  15. package/dist/chunk-BNXWSZ63.js +3742 -0
  16. package/dist/chunk-BTL5DJVU.js +3222 -0
  17. package/dist/chunk-HDHYG7E4.js +104 -0
  18. package/dist/chunk-HLR4KZBP.js +3234 -0
  19. package/dist/chunk-IP3FRFEB.js +1045 -0
  20. package/dist/chunk-KHU56VDO.js +3042 -0
  21. package/dist/chunk-KRYIFLQR.js +85 -89
  22. package/dist/chunk-LBSDNLEM.js +287 -0
  23. package/dist/chunk-MNTQ7HCP.js +2643 -0
  24. package/dist/chunk-MUJELQQ6.js +1387 -0
  25. package/dist/chunk-MXJGMSLV.js +2199 -0
  26. package/dist/chunk-N6QJGC3Z.js +2636 -0
  27. package/dist/chunk-OBELGBPM.js +1713 -0
  28. package/dist/chunk-OT7R5XTA.js +3192 -0
  29. package/dist/chunk-P7X4RA2T.js +106 -0
  30. package/dist/chunk-PIDUQNC2.js +3185 -0
  31. package/dist/chunk-POGCDIH4.js +3187 -0
  32. package/dist/chunk-PSIEOQGZ.js +3043 -0
  33. package/dist/chunk-PVRT3IHA.js +3238 -0
  34. package/dist/chunk-QNN4TT23.js +1430 -0
  35. package/dist/chunk-RE3R45RJ.js +3042 -0
  36. package/dist/chunk-S7E6TFX6.js +718 -657
  37. package/dist/chunk-SG6GLU4U.js +1378 -0
  38. package/dist/chunk-SJCDV2ST.js +274 -0
  39. package/dist/chunk-SYE5XLF3.js +104 -0
  40. package/dist/chunk-T5VLYBZD.js +103 -0
  41. package/dist/chunk-TOQB7VWU.js +3238 -0
  42. package/dist/chunk-VFNMZ4ZQ.js +3228 -0
  43. package/dist/chunk-VVTGZNBT.js +1533 -1423
  44. package/dist/chunk-W7Q4RFEV.js +104 -0
  45. package/dist/chunk-XTYYVRLO.js +3190 -0
  46. package/dist/chunk-Y6MDYVJD.js +3063 -0
  47. package/dist/cli/main.js +4072 -629
  48. package/dist/index.d.ts +420 -33
  49. package/dist/index.js +8 -15
  50. package/dist/mcp/server.js +103 -7
  51. package/dist/schema-BAWSG7KY.js +22 -0
  52. package/dist/schema-E3QUPL26.js +20 -0
  53. package/dist/schema-EHL7WUT6.js +20 -0
  54. package/docs/019-USAGE.md +44 -5
  55. package/docs/020-current-implementation.md +8 -8
  56. package/docs/021-DOGFOODING-FINDINGS.md +1 -1
  57. package/docs/CONFIG.md +1123 -0
  58. package/docs/ERRORS.md +383 -0
  59. package/docs/summarization.md +320 -0
  60. package/justfile +40 -0
  61. package/package.json +39 -33
  62. package/research/INDEX.md +315 -0
  63. package/research/code-review/README.md +90 -0
  64. package/research/code-review/cli-error-handling-review.md +979 -0
  65. package/research/code-review/code-review-validation-report.md +464 -0
  66. package/research/code-review/main-ts-review.md +1128 -0
  67. package/research/config-docs/SUMMARY.md +357 -0
  68. package/research/config-docs/TEST-RESULTS.md +776 -0
  69. package/research/config-docs/TODO.md +542 -0
  70. package/research/config-docs/analysis.md +744 -0
  71. package/research/config-docs/fix-validation.md +502 -0
  72. package/research/config-docs/help-audit.md +264 -0
  73. package/research/config-docs/help-system-analysis.md +890 -0
  74. package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
  75. package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
  76. package/research/issue-review.md +603 -0
  77. package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
  78. package/research/llm-summarization/alternative-providers-2026.md +1428 -0
  79. package/research/llm-summarization/anthropic-2026.md +367 -0
  80. package/research/llm-summarization/claude-cli-integration.md +1706 -0
  81. package/research/llm-summarization/cli-integration-patterns.md +3155 -0
  82. package/research/llm-summarization/openai-2026.md +473 -0
  83. package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
  84. package/research/llm-summarization/opencode-cli-integration.md +1552 -0
  85. package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
  86. package/research/llm-summarization/prototype-results.md +56 -0
  87. package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
  88. package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
  89. package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
  90. package/research/mdcontext-pudding/01-index-embed.md +956 -0
  91. package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
  92. package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
  93. package/research/mdcontext-pudding/02-search.md +970 -0
  94. package/research/mdcontext-pudding/03-context.md +779 -0
  95. package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
  96. package/research/mdcontext-pudding/04-tree.md +704 -0
  97. package/research/mdcontext-pudding/05-config.md +1038 -0
  98. package/research/mdcontext-pudding/06-links-summary.txt +87 -0
  99. package/research/mdcontext-pudding/06-links.md +679 -0
  100. package/research/mdcontext-pudding/07-stats.md +693 -0
  101. package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
  102. package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
  103. package/research/mdcontext-pudding/README.md +168 -0
  104. package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
  105. package/research/research-quality-review.md +834 -0
  106. package/research/semantic-search/embedding-text-analysis.md +156 -0
  107. package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
  108. package/research/semantic-search/query-processing-analysis.md +207 -0
  109. package/research/semantic-search/root-cause-and-solution.md +114 -0
  110. package/research/semantic-search/threshold-validation-report.md +69 -0
  111. package/research/semantic-search/vector-search-analysis.md +63 -0
  112. package/research/test-path-issues.md +276 -0
  113. package/review/ALP-76/1-error-type-design.md +962 -0
  114. package/review/ALP-76/2-error-handling-patterns.md +906 -0
  115. package/review/ALP-76/3-error-presentation.md +624 -0
  116. package/review/ALP-76/4-test-coverage.md +625 -0
  117. package/review/ALP-76/5-migration-completeness.md +440 -0
  118. package/review/ALP-76/6-effect-best-practices.md +755 -0
  119. package/scripts/apply-branch-protection.sh +47 -0
  120. package/scripts/branch-protection-templates.json +79 -0
  121. package/scripts/prototype-summarization.ts +346 -0
  122. package/scripts/rebuild-hnswlib.js +32 -37
  123. package/scripts/setup-branch-protection.sh +64 -0
  124. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
  125. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
  126. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
  127. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
  128. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  129. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  130. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
  131. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
  132. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
  133. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
  134. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
  135. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
  136. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
  137. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
  138. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
  139. package/src/cli/argv-preprocessor.test.ts +2 -2
  140. package/src/cli/cli.test.ts +230 -33
  141. package/src/cli/commands/config-cmd.ts +642 -0
  142. package/src/cli/commands/context.ts +97 -9
  143. package/src/cli/commands/duplicates.ts +122 -0
  144. package/src/cli/commands/embeddings.ts +529 -0
  145. package/src/cli/commands/index-cmd.ts +210 -30
  146. package/src/cli/commands/index.ts +3 -0
  147. package/src/cli/commands/search.ts +894 -64
  148. package/src/cli/commands/stats.ts +3 -0
  149. package/src/cli/commands/tree.ts +26 -5
  150. package/src/cli/config-layer.ts +176 -0
  151. package/src/cli/error-handler.test.ts +235 -0
  152. package/src/cli/error-handler.ts +655 -0
  153. package/src/cli/flag-schemas.ts +66 -0
  154. package/src/cli/help.ts +209 -7
  155. package/src/cli/main.ts +348 -58
  156. package/src/cli/options.ts +10 -0
  157. package/src/cli/shared-error-handling.ts +199 -0
  158. package/src/cli/utils.ts +150 -17
  159. package/src/config/file-provider.test.ts +320 -0
  160. package/src/config/file-provider.ts +273 -0
  161. package/src/config/index.ts +72 -0
  162. package/src/config/integration.test.ts +667 -0
  163. package/src/config/precedence.test.ts +277 -0
  164. package/src/config/precedence.ts +451 -0
  165. package/src/config/schema.test.ts +414 -0
  166. package/src/config/schema.ts +603 -0
  167. package/src/config/service.test.ts +320 -0
  168. package/src/config/service.ts +243 -0
  169. package/src/config/testing.test.ts +264 -0
  170. package/src/config/testing.ts +110 -0
  171. package/src/core/types.ts +6 -33
  172. package/src/duplicates/detector.test.ts +183 -0
  173. package/src/duplicates/detector.ts +414 -0
  174. package/src/duplicates/index.ts +18 -0
  175. package/src/embeddings/embedding-namespace.test.ts +300 -0
  176. package/src/embeddings/embedding-namespace.ts +947 -0
  177. package/src/embeddings/heading-boost.test.ts +222 -0
  178. package/src/embeddings/hnsw-build-options.test.ts +198 -0
  179. package/src/embeddings/hyde.test.ts +272 -0
  180. package/src/embeddings/hyde.ts +264 -0
  181. package/src/embeddings/index.ts +2 -0
  182. package/src/embeddings/openai-provider.ts +332 -83
  183. package/src/embeddings/pricing.json +22 -0
  184. package/src/embeddings/provider-constants.ts +204 -0
  185. package/src/embeddings/provider-errors.test.ts +967 -0
  186. package/src/embeddings/provider-errors.ts +565 -0
  187. package/src/embeddings/provider-factory.test.ts +240 -0
  188. package/src/embeddings/provider-factory.ts +225 -0
  189. package/src/embeddings/provider-integration.test.ts +788 -0
  190. package/src/embeddings/query-preprocessing.test.ts +187 -0
  191. package/src/embeddings/semantic-search-threshold.test.ts +508 -0
  192. package/src/embeddings/semantic-search.ts +780 -93
  193. package/src/embeddings/types.ts +293 -16
  194. package/src/embeddings/vector-store.ts +486 -77
  195. package/src/embeddings/voyage-provider.ts +313 -0
  196. package/src/errors/errors.test.ts +845 -0
  197. package/src/errors/index.ts +533 -0
  198. package/src/index/ignore-patterns.test.ts +354 -0
  199. package/src/index/ignore-patterns.ts +305 -0
  200. package/src/index/indexer.ts +286 -48
  201. package/src/index/storage.ts +94 -30
  202. package/src/index/types.ts +40 -2
  203. package/src/index/watcher.ts +67 -9
  204. package/src/index.ts +22 -0
  205. package/src/integration/search-keyword.test.ts +678 -0
  206. package/src/mcp/server.ts +135 -6
  207. package/src/parser/parser.ts +18 -19
  208. package/src/parser/section-filter.test.ts +277 -0
  209. package/src/parser/section-filter.ts +125 -3
  210. package/src/search/__tests__/hybrid-search.test.ts +650 -0
  211. package/src/search/bm25-store.ts +366 -0
  212. package/src/search/cross-encoder.test.ts +253 -0
  213. package/src/search/cross-encoder.ts +406 -0
  214. package/src/search/fuzzy-search.test.ts +419 -0
  215. package/src/search/fuzzy-search.ts +273 -0
  216. package/src/search/hybrid-search.ts +448 -0
  217. package/src/search/path-matcher.test.ts +276 -0
  218. package/src/search/path-matcher.ts +33 -0
  219. package/src/search/searcher.test.ts +99 -1
  220. package/src/search/searcher.ts +189 -67
  221. package/src/search/wink-bm25.d.ts +30 -0
  222. package/src/summarization/cli-providers/claude.ts +202 -0
  223. package/src/summarization/cli-providers/detection.test.ts +273 -0
  224. package/src/summarization/cli-providers/detection.ts +118 -0
  225. package/src/summarization/cli-providers/index.ts +8 -0
  226. package/src/summarization/cost.test.ts +139 -0
  227. package/src/summarization/cost.ts +102 -0
  228. package/src/summarization/error-handler.test.ts +127 -0
  229. package/src/summarization/error-handler.ts +111 -0
  230. package/src/summarization/index.ts +102 -0
  231. package/src/summarization/pipeline.test.ts +498 -0
  232. package/src/summarization/pipeline.ts +231 -0
  233. package/src/summarization/prompts.test.ts +269 -0
  234. package/src/summarization/prompts.ts +133 -0
  235. package/src/summarization/provider-factory.test.ts +396 -0
  236. package/src/summarization/provider-factory.ts +178 -0
  237. package/src/summarization/types.ts +184 -0
  238. package/src/summarize/summarizer.ts +104 -35
  239. package/src/types/huggingface-transformers.d.ts +66 -0
  240. package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
  241. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  242. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  243. package/tests/fixtures/cli/.mdcontext/indexes/documents.json +4 -4
  244. package/tests/fixtures/cli/.mdcontext/indexes/sections.json +14 -0
  245. package/tests/integration/embed-index.test.ts +712 -0
  246. package/tests/integration/search-context.test.ts +469 -0
  247. package/tests/integration/search-semantic.test.ts +522 -0
  248. package/vitest.config.ts +1 -6
  249. package/AGENTS.md +0 -46
  250. package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
  251. package/tests/fixtures/cli/.mdcontext/vectors.meta.json +0 -1264
@@ -0,0 +1,3155 @@
1
+ # CLI Integration Patterns for TypeScript/Node.js (2026)
2
+
3
+ **Last Updated:** January 2026
4
+ **Purpose:** Best practices for integrating with AI CLI tools programmatically in TypeScript/Node.js for the mdcontext project
5
+
6
+ ## Table of Contents
7
+
8
+ 1. [Process Spawning Patterns](#process-spawning-patterns)
9
+ 2. [Output Parsing Strategies](#output-parsing-strategies)
10
+ 3. [Error Detection & Handling](#error-detection--handling)
11
+ 4. [Timeout Handling](#timeout-handling)
12
+ 5. [Shell Escaping & Security](#shell-escaping--security)
13
+ 6. [Buffer Management](#buffer-management)
14
+ 7. [Exit Code Handling](#exit-code-handling)
15
+ 8. [Environment Variables](#environment-variables)
16
+ 9. [Testing Strategies](#testing-strategies)
17
+ 10. [Production Examples](#production-examples)
18
+ 11. [Common Pitfalls](#common-pitfalls)
19
+ 12. [Platform Differences](#platform-differences)
20
+
21
+ ---
22
+
23
+ ## Process Spawning Patterns
24
+
25
+ ### Overview: exec vs spawn vs execFile
26
+
27
+ Node.js provides three primary methods for spawning child processes, each with distinct use cases:
28
+
29
+ | Method | Use Case | Shell | Buffering | Best For |
30
+ |--------|----------|-------|-----------|----------|
31
+ | `exec()` | Simple shell commands | Yes | Buffered | Small output, shell features needed |
32
+ | `execFile()` | Direct binary execution | No | Buffered | Security-critical, known binaries |
33
+ | `spawn()` | Streaming processes | Optional | Streaming | Large output, real-time data |
34
+
35
+ ### When to Use Each Method
36
+
37
+ **Use `exec()` when:**
38
+ - You need shell features (pipes, redirection, wildcards)
39
+ - Output size is guaranteed small (< 1MB)
40
+ - You're running simple, trusted commands
41
+ - You need synchronous execution (with `execSync()`)
42
+
43
+ **Use `execFile()` when:**
44
+ - Security is paramount (no shell injection risk)
45
+ - Executing a known binary directly
46
+ - You want better performance (no shell overhead)
47
+ - Arguments are dynamic/user-controlled
48
+
49
+ **Use `spawn()` when:**
50
+ - Output may be large or unbounded
51
+ - You need real-time streaming output
52
+ - Long-running processes
53
+ - Fine-grained control over stdio streams
54
+
55
+ ### TypeScript Examples
56
+
57
+ #### Pattern 1: Using spawn() for AI CLI Tools (Recommended)
58
+
59
+ ```typescript
60
+ import { spawn, type ChildProcess } from 'child_process';
61
+ import { EventEmitter } from 'events';
62
+
63
+ interface CLIResult {
64
+ stdout: string;
65
+ stderr: string;
66
+ exitCode: number | null;
67
+ signal: NodeJS.Signals | null;
68
+ }
69
+
70
+ interface SpawnOptions {
71
+ timeout?: number;
72
+ maxBuffer?: number;
73
+ env?: NodeJS.ProcessEnv;
74
+ cwd?: string;
75
+ }
76
+
77
+ /**
78
+ * Spawn a CLI process with proper error handling and streaming support
79
+ * Recommended for AI CLI tools like claude, gh, or git
80
+ */
81
+ async function spawnCLI(
82
+ command: string,
83
+ args: string[],
84
+ options: SpawnOptions = {}
85
+ ): Promise<CLIResult> {
86
+ return new Promise((resolve, reject) => {
87
+ const {
88
+ timeout = 120000, // 2 minutes default
89
+ maxBuffer = 10 * 1024 * 1024, // 10MB default
90
+ env = process.env,
91
+ cwd = process.cwd(),
92
+ } = options;
93
+
94
+ let stdout = '';
95
+ let stderr = '';
96
+ let stdoutBytes = 0;
97
+ let stderrBytes = 0;
98
+ let timedOut = false;
99
+
100
+ // Spawn with explicit argument array (prevents shell injection)
101
+ const child: ChildProcess = spawn(command, args, {
102
+ cwd,
103
+ env,
104
+ // Do NOT use shell: true unless absolutely necessary
105
+ shell: false,
106
+ // Ensure stdio is piped for capture
107
+ stdio: ['pipe', 'pipe', 'pipe'],
108
+ });
109
+
110
+ // Timeout handler
111
+ const timeoutId = setTimeout(() => {
112
+ timedOut = true;
113
+ child.kill('SIGTERM');
114
+
115
+ // Escalate to SIGKILL if not dead after 5 seconds
116
+ setTimeout(() => {
117
+ if (child.exitCode === null && child.signalCode === null) {
118
+ child.kill('SIGKILL');
119
+ }
120
+ }, 5000);
121
+ }, timeout);
122
+
123
+ // Capture stdout with buffer protection
124
+ child.stdout?.on('data', (chunk: Buffer) => {
125
+ stdoutBytes += chunk.length;
126
+ if (stdoutBytes > maxBuffer) {
127
+ child.kill('SIGTERM');
128
+ reject(new Error(`stdout exceeded maxBuffer of ${maxBuffer} bytes`));
129
+ return;
130
+ }
131
+ stdout += chunk.toString('utf8');
132
+ });
133
+
134
+ // Capture stderr with buffer protection
135
+ child.stderr?.on('data', (chunk: Buffer) => {
136
+ stderrBytes += chunk.length;
137
+ if (stderrBytes > maxBuffer) {
138
+ child.kill('SIGTERM');
139
+ reject(new Error(`stderr exceeded maxBuffer of ${maxBuffer} bytes`));
140
+ return;
141
+ }
142
+ stderr += chunk.toString('utf8');
143
+ });
144
+
145
+ // Handle process exit
146
+ child.on('close', (exitCode: number | null, signal: NodeJS.Signals | null) => {
147
+ clearTimeout(timeoutId);
148
+
149
+ if (timedOut) {
150
+ reject(new Error(`Process timed out after ${timeout}ms`));
151
+ return;
152
+ }
153
+
154
+ resolve({
155
+ stdout: stdout.trim(),
156
+ stderr: stderr.trim(),
157
+ exitCode,
158
+ signal,
159
+ });
160
+ });
161
+
162
+ // Handle spawn errors (command not found, permission denied, etc.)
163
+ child.on('error', (err: Error) => {
164
+ clearTimeout(timeoutId);
165
+ reject(new Error(`Failed to spawn process: ${err.message}`));
166
+ });
167
+ });
168
+ }
169
+
170
+ // Usage example for AI CLI
171
+ async function callClaudeCLI(prompt: string): Promise<string> {
172
+ try {
173
+ const result = await spawnCLI('claude', ['--prompt', prompt], {
174
+ timeout: 300000, // 5 minutes for AI responses
175
+ maxBuffer: 50 * 1024 * 1024, // 50MB for large responses
176
+ });
177
+
178
+ if (result.exitCode !== 0) {
179
+ throw new Error(`Claude CLI failed with exit code ${result.exitCode}: ${result.stderr}`);
180
+ }
181
+
182
+ return result.stdout;
183
+ } catch (error) {
184
+ if (error instanceof Error) {
185
+ throw new Error(`Claude CLI error: ${error.message}`);
186
+ }
187
+ throw error;
188
+ }
189
+ }
190
+ ```
191
+
192
+ #### Pattern 2: Using execFile() for Security-Critical Operations
193
+
194
+ ```typescript
195
+ import { execFile } from 'child_process';
196
+ import { promisify } from 'util';
197
+
198
+ const execFileAsync = promisify(execFile);
199
+
200
+ /**
201
+ * Execute a binary directly without shell (more secure)
202
+ * Use this when arguments may come from user input
203
+ */
204
+ async function safeExecute(
205
+ command: string,
206
+ args: string[],
207
+ options: {
208
+ timeout?: number;
209
+ maxBuffer?: number;
210
+ cwd?: string;
211
+ } = {}
212
+ ): Promise<{ stdout: string; stderr: string }> {
213
+ try {
214
+ const { stdout, stderr } = await execFileAsync(command, args, {
215
+ timeout: options.timeout || 120000,
216
+ maxBuffer: options.maxBuffer || 10 * 1024 * 1024,
217
+ cwd: options.cwd || process.cwd(),
218
+ // Note: execFile does NOT use a shell by default
219
+ // This prevents shell injection attacks
220
+ });
221
+
222
+ return {
223
+ stdout: stdout.trim(),
224
+ stderr: stderr.trim(),
225
+ };
226
+ } catch (error: any) {
227
+ // Handle specific error types
228
+ if (error.killed && error.signal === 'SIGTERM') {
229
+ throw new Error(`Process timed out after ${options.timeout}ms`);
230
+ }
231
+ if (error.code === 'ENOENT') {
232
+ throw new Error(`Command not found: ${command}`);
233
+ }
234
+ if (error.code === 'EACCES') {
235
+ throw new Error(`Permission denied: ${command}`);
236
+ }
237
+ throw error;
238
+ }
239
+ }
240
+
241
+ // Usage example
242
+ async function runGitCommand(args: string[]): Promise<string> {
243
+ const { stdout } = await safeExecute('git', args, {
244
+ timeout: 30000, // 30 seconds
245
+ });
246
+ return stdout;
247
+ }
248
+ ```
249
+
250
+ #### Pattern 3: Streaming Large Outputs
251
+
252
+ ```typescript
253
+ import { spawn } from 'child_process';
254
+ import { EventEmitter } from 'events';
255
+
256
+ interface StreamOptions {
257
+ onStdout?: (chunk: string) => void;
258
+ onStderr?: (chunk: string) => void;
259
+ onProgress?: (bytesRead: number) => void;
260
+ }
261
+
262
+ /**
263
+ * Spawn process with streaming callbacks for real-time processing
264
+ * Ideal for long-running AI generation or large file operations
265
+ */
266
+ async function spawnWithStreaming(
267
+ command: string,
268
+ args: string[],
269
+ options: StreamOptions = {}
270
+ ): Promise<void> {
271
+ return new Promise((resolve, reject) => {
272
+ const child = spawn(command, args, {
273
+ stdio: ['pipe', 'pipe', 'pipe'],
274
+ shell: false,
275
+ });
276
+
277
+ let totalBytesRead = 0;
278
+
279
+ // Stream stdout chunks in real-time
280
+ child.stdout?.on('data', (chunk: Buffer) => {
281
+ totalBytesRead += chunk.length;
282
+ const text = chunk.toString('utf8');
283
+
284
+ options.onStdout?.(text);
285
+ options.onProgress?.(totalBytesRead);
286
+ });
287
+
288
+ // Stream stderr chunks in real-time
289
+ child.stderr?.on('data', (chunk: Buffer) => {
290
+ const text = chunk.toString('utf8');
291
+ options.onStderr?.(text);
292
+ });
293
+
294
+ child.on('close', (exitCode) => {
295
+ if (exitCode === 0) {
296
+ resolve();
297
+ } else {
298
+ reject(new Error(`Process exited with code ${exitCode}`));
299
+ }
300
+ });
301
+
302
+ child.on('error', reject);
303
+ });
304
+ }
305
+
306
+ // Usage example for streaming AI responses
307
+ async function streamClaudeResponse(prompt: string) {
308
+ let response = '';
309
+
310
+ await spawnWithStreaming('claude', ['--stream', '--prompt', prompt], {
311
+ onStdout: (chunk) => {
312
+ response += chunk;
313
+ process.stdout.write(chunk); // Display in real-time
314
+ },
315
+ onStderr: (chunk) => {
316
+ console.error('Error:', chunk);
317
+ },
318
+ onProgress: (bytes) => {
319
+ console.log(`Received ${bytes} bytes...`);
320
+ },
321
+ });
322
+
323
+ return response;
324
+ }
325
+ ```
326
+
327
+ ---
328
+
329
+ ## Output Parsing Strategies
330
+
331
+ ### Handling Different Output Formats
332
+
333
+ AI CLI tools typically output in various formats. Here are robust parsing strategies:
334
+
335
+ #### Strategy 1: JSON Output
336
+
337
+ ```typescript
338
+ import { z } from 'zod';
339
+
340
+ /**
341
+ * Parse and validate JSON output from CLI tools
342
+ * Many modern CLI tools support --json flags
343
+ */
344
+ async function parseJSONOutput<T>(
345
+ command: string,
346
+ args: string[],
347
+ schema: z.ZodSchema<T>
348
+ ): Promise<T> {
349
+ const result = await spawnCLI(command, args);
350
+
351
+ if (result.exitCode !== 0) {
352
+ throw new Error(`Command failed: ${result.stderr}`);
353
+ }
354
+
355
+ try {
356
+ // Parse JSON
357
+ const parsed = JSON.parse(result.stdout);
358
+
359
+ // Validate with Zod schema
360
+ return schema.parse(parsed);
361
+ } catch (error) {
362
+ if (error instanceof z.ZodError) {
363
+ throw new Error(`Invalid JSON schema: ${error.message}`);
364
+ }
365
+ if (error instanceof SyntaxError) {
366
+ throw new Error(`Invalid JSON output: ${error.message}`);
367
+ }
368
+ throw error;
369
+ }
370
+ }
371
+
372
+ // Example: Parse GitHub CLI output
373
+ const IssueSchema = z.object({
374
+ number: z.number(),
375
+ title: z.string(),
376
+ state: z.enum(['open', 'closed']),
377
+ author: z.string(),
378
+ });
379
+
380
+ type Issue = z.infer<typeof IssueSchema>;
381
+
382
+ async function getGitHubIssue(issueNumber: number): Promise<Issue> {
383
+ return parseJSONOutput(
384
+ 'gh',
385
+ ['issue', 'view', issueNumber.toString(), '--json', 'number,title,state,author'],
386
+ IssueSchema
387
+ );
388
+ }
389
+ ```
390
+
391
+ #### Strategy 2: NDJSON (Newline-Delimited JSON) Streaming
392
+
393
+ ```typescript
394
+ import { spawn } from 'child_process';
395
+ import { Transform } from 'stream';
396
+ import split2 from 'split2'; // npm: split2
397
+
398
+ /**
399
+ * Parse streaming NDJSON output line-by-line
400
+ * Efficient for large datasets or continuous output
401
+ */
402
+ async function parseNDJSONStream<T>(
403
+ command: string,
404
+ args: string[],
405
+ schema: z.ZodSchema<T>,
406
+ onItem: (item: T) => void | Promise<void>
407
+ ): Promise<void> {
408
+ return new Promise((resolve, reject) => {
409
+ const child = spawn(command, args, {
410
+ stdio: ['pipe', 'pipe', 'pipe'],
411
+ shell: false,
412
+ });
413
+
414
+ let lineNumber = 0;
415
+
416
+ // Split stdout by newlines and parse each line as JSON
417
+ child.stdout
418
+ ?.pipe(split2()) // Split on newlines
419
+ .pipe(
420
+ new Transform({
421
+ objectMode: true,
422
+ async transform(line: string, encoding, callback) {
423
+ lineNumber++;
424
+
425
+ if (!line.trim()) {
426
+ callback();
427
+ return;
428
+ }
429
+
430
+ try {
431
+ const parsed = JSON.parse(line);
432
+ const validated = schema.parse(parsed);
433
+ await onItem(validated);
434
+ callback();
435
+ } catch (error) {
436
+ callback(new Error(`Line ${lineNumber}: ${error}`));
437
+ }
438
+ },
439
+ })
440
+ )
441
+ .on('error', reject)
442
+ .on('finish', resolve);
443
+
444
+ child.on('error', reject);
445
+ });
446
+ }
447
+
448
+ // Example: Stream log entries
449
+ const LogEntrySchema = z.object({
450
+ timestamp: z.string(),
451
+ level: z.enum(['info', 'warn', 'error']),
452
+ message: z.string(),
453
+ });
454
+
455
+ type LogEntry = z.infer<typeof LogEntrySchema>;
456
+
457
+ async function streamLogs() {
458
+ await parseNDJSONStream(
459
+ 'some-logging-cli',
460
+ ['stream', '--format', 'ndjson'],
461
+ LogEntrySchema,
462
+ async (entry) => {
463
+ console.log(`[${entry.level.toUpperCase()}] ${entry.message}`);
464
+ }
465
+ );
466
+ }
467
+ ```
468
+
469
+ #### Strategy 3: Unstructured Text Parsing
470
+
471
+ ```typescript
472
+ /**
473
+ * Parse unstructured CLI output with regex patterns
474
+ * Use when JSON output is not available
475
+ */
476
+ interface ParsePattern<T> {
477
+ pattern: RegExp;
478
+ transform: (matches: RegExpMatchArray) => T;
479
+ }
480
+
481
+ async function parseTextOutput<T>(
482
+ command: string,
483
+ args: string[],
484
+ patterns: ParsePattern<T>[]
485
+ ): Promise<T[]> {
486
+ const result = await spawnCLI(command, args);
487
+
488
+ if (result.exitCode !== 0) {
489
+ throw new Error(`Command failed: ${result.stderr}`);
490
+ }
491
+
492
+ const results: T[] = [];
493
+
494
+ for (const { pattern, transform } of patterns) {
495
+ const matches = result.stdout.matchAll(pattern);
496
+ for (const match of matches) {
497
+ results.push(transform(match));
498
+ }
499
+ }
500
+
501
+ return results;
502
+ }
503
+
504
+ // Example: Parse git log output
505
+ interface GitCommit {
506
+ hash: string;
507
+ author: string;
508
+ date: string;
509
+ message: string;
510
+ }
511
+
512
+ async function getGitLog(): Promise<GitCommit[]> {
513
+ const result = await spawnCLI('git', [
514
+ 'log',
515
+ '--pretty=format:%H|%an|%ad|%s',
516
+ '--date=iso',
517
+ '-n',
518
+ '10',
519
+ ]);
520
+
521
+ return result.stdout.split('\n').map((line) => {
522
+ const [hash, author, date, message] = line.split('|');
523
+ return { hash, author, date, message };
524
+ });
525
+ }
526
+ ```
527
+
528
+ #### Strategy 4: Handling Mixed Output (JSON + Progress)
529
+
530
+ ```typescript
531
+ /**
532
+ * Some CLI tools output progress to stderr and JSON to stdout
533
+ * Handle this pattern by separating the streams
534
+ */
535
+ async function parseWithProgress<T>(
536
+ command: string,
537
+ args: string[],
538
+ schema: z.ZodSchema<T>,
539
+ onProgress?: (message: string) => void
540
+ ): Promise<T> {
541
+ const result = await new Promise<CLIResult>((resolve, reject) => {
542
+ const child = spawn(command, args, {
543
+ stdio: ['pipe', 'pipe', 'pipe'],
544
+ shell: false,
545
+ });
546
+
547
+ let stdout = '';
548
+ let stderr = '';
549
+
550
+ child.stdout?.on('data', (chunk) => {
551
+ stdout += chunk.toString('utf8');
552
+ });
553
+
554
+ child.stderr?.on('data', (chunk) => {
555
+ const message = chunk.toString('utf8').trim();
556
+ stderr += message + '\n';
557
+
558
+ // Many CLI tools output progress to stderr
559
+ onProgress?.(message);
560
+ });
561
+
562
+ child.on('close', (exitCode, signal) => {
563
+ resolve({ stdout, stderr, exitCode, signal });
564
+ });
565
+
566
+ child.on('error', reject);
567
+ });
568
+
569
+ if (result.exitCode !== 0) {
570
+ throw new Error(`Command failed: ${result.stderr}`);
571
+ }
572
+
573
+ const parsed = JSON.parse(result.stdout);
574
+ return schema.parse(parsed);
575
+ }
576
+
577
+ // Example usage
578
+ async function generateWithProgress() {
579
+ const result = await parseWithProgress(
580
+ 'some-ai-cli',
581
+ ['generate', '--output', 'json'],
582
+ z.object({ content: z.string(), tokens: z.number() }),
583
+ (progress) => {
584
+ console.log('Progress:', progress);
585
+ }
586
+ );
587
+
588
+ return result;
589
+ }
590
+ ```
591
+
592
+ ---
593
+
594
+ ## Error Detection & Handling
595
+
596
+ ### Comprehensive Error Classification
597
+
598
+ ```typescript
599
+ /**
600
+ * Error types for CLI integration
601
+ */
602
+ export enum CLIErrorType {
603
+ // Process errors
604
+ COMMAND_NOT_FOUND = 'COMMAND_NOT_FOUND',
605
+ PERMISSION_DENIED = 'PERMISSION_DENIED',
606
+ SPAWN_FAILED = 'SPAWN_FAILED',
607
+
608
+ // Execution errors
609
+ TIMEOUT = 'TIMEOUT',
610
+ BUFFER_OVERFLOW = 'BUFFER_OVERFLOW',
611
+ NON_ZERO_EXIT = 'NON_ZERO_EXIT',
612
+ SIGNAL_TERMINATED = 'SIGNAL_TERMINATED',
613
+
614
+ // Authentication errors
615
+ AUTH_REQUIRED = 'AUTH_REQUIRED',
616
+ AUTH_EXPIRED = 'AUTH_EXPIRED',
617
+ INVALID_TOKEN = 'INVALID_TOKEN',
618
+
619
+ // Rate limiting
620
+ RATE_LIMITED = 'RATE_LIMITED',
621
+ QUOTA_EXCEEDED = 'QUOTA_EXCEEDED',
622
+
623
+ // Network errors
624
+ NETWORK_ERROR = 'NETWORK_ERROR',
625
+ CONNECTION_TIMEOUT = 'CONNECTION_TIMEOUT',
626
+ DNS_RESOLUTION_FAILED = 'DNS_RESOLUTION_FAILED',
627
+
628
+ // Data errors
629
+ INVALID_OUTPUT = 'INVALID_OUTPUT',
630
+ MALFORMED_JSON = 'MALFORMED_JSON',
631
+ SCHEMA_VALIDATION_FAILED = 'SCHEMA_VALIDATION_FAILED',
632
+ }
633
+
634
+ export class CLIError extends Error {
635
+ constructor(
636
+ public type: CLIErrorType,
637
+ message: string,
638
+ public details?: {
639
+ command?: string;
640
+ args?: string[];
641
+ exitCode?: number | null;
642
+ signal?: NodeJS.Signals | null;
643
+ stdout?: string;
644
+ stderr?: string;
645
+ originalError?: Error;
646
+ }
647
+ ) {
648
+ super(message);
649
+ this.name = 'CLIError';
650
+ }
651
+
652
+ /**
653
+ * Check if error is retryable
654
+ */
655
+ isRetryable(): boolean {
656
+ return [
657
+ CLIErrorType.NETWORK_ERROR,
658
+ CLIErrorType.CONNECTION_TIMEOUT,
659
+ CLIErrorType.RATE_LIMITED,
660
+ CLIErrorType.TIMEOUT,
661
+ ].includes(this.type);
662
+ }
663
+
664
+ /**
665
+ * Check if error requires user intervention
666
+ */
667
+ requiresUserAction(): boolean {
668
+ return [
669
+ CLIErrorType.AUTH_REQUIRED,
670
+ CLIErrorType.AUTH_EXPIRED,
671
+ CLIErrorType.INVALID_TOKEN,
672
+ CLIErrorType.COMMAND_NOT_FOUND,
673
+ CLIErrorType.PERMISSION_DENIED,
674
+ ].includes(this.type);
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Detect and classify CLI errors
680
+ */
681
+ function detectErrorType(error: any, result?: CLIResult): CLIError {
682
+ // Process spawn errors
683
+ if (error.code === 'ENOENT') {
684
+ return new CLIError(
685
+ CLIErrorType.COMMAND_NOT_FOUND,
686
+ `Command not found: ${error.path}`,
687
+ { originalError: error }
688
+ );
689
+ }
690
+
691
+ if (error.code === 'EACCES') {
692
+ return new CLIError(
693
+ CLIErrorType.PERMISSION_DENIED,
694
+ `Permission denied: ${error.path}`,
695
+ { originalError: error }
696
+ );
697
+ }
698
+
699
+ // Timeout errors
700
+ if (error.killed && error.signal === 'SIGTERM') {
701
+ return new CLIError(
702
+ CLIErrorType.TIMEOUT,
703
+ 'Process timed out',
704
+ { signal: error.signal, originalError: error }
705
+ );
706
+ }
707
+
708
+ // Check stderr for common error patterns
709
+ const stderr = result?.stderr?.toLowerCase() || '';
710
+
711
+ // Authentication errors
712
+ if (
713
+ stderr.includes('authentication') ||
714
+ stderr.includes('not logged in') ||
715
+ stderr.includes('please login') ||
716
+ stderr.includes('401 unauthorized')
717
+ ) {
718
+ return new CLIError(
719
+ CLIErrorType.AUTH_REQUIRED,
720
+ 'Authentication required',
721
+ { stderr: result?.stderr }
722
+ );
723
+ }
724
+
725
+ if (stderr.includes('token expired') || stderr.includes('token invalid')) {
726
+ return new CLIError(
727
+ CLIErrorType.AUTH_EXPIRED,
728
+ 'Authentication token expired',
729
+ { stderr: result?.stderr }
730
+ );
731
+ }
732
+
733
+ // Rate limiting
734
+ if (
735
+ stderr.includes('rate limit') ||
736
+ stderr.includes('too many requests') ||
737
+ stderr.includes('429')
738
+ ) {
739
+ return new CLIError(
740
+ CLIErrorType.RATE_LIMITED,
741
+ 'Rate limit exceeded',
742
+ { stderr: result?.stderr }
743
+ );
744
+ }
745
+
746
+ // Network errors
747
+ if (
748
+ stderr.includes('network') ||
749
+ stderr.includes('connection') ||
750
+ stderr.includes('timeout') ||
751
+ stderr.includes('econnrefused') ||
752
+ stderr.includes('enotfound')
753
+ ) {
754
+ return new CLIError(
755
+ CLIErrorType.NETWORK_ERROR,
756
+ 'Network error occurred',
757
+ { stderr: result?.stderr }
758
+ );
759
+ }
760
+
761
+ // Generic non-zero exit
762
+ if (result?.exitCode !== null && result.exitCode !== 0) {
763
+ return new CLIError(
764
+ CLIErrorType.NON_ZERO_EXIT,
765
+ `Process exited with code ${result.exitCode}`,
766
+ {
767
+ exitCode: result.exitCode,
768
+ stderr: result.stderr,
769
+ stdout: result.stdout,
770
+ }
771
+ );
772
+ }
773
+
774
+ // Default error
775
+ return new CLIError(
776
+ CLIErrorType.SPAWN_FAILED,
777
+ error.message || 'Unknown error',
778
+ { originalError: error }
779
+ );
780
+ }
781
+
782
+ /**
783
+ * Execute CLI with comprehensive error handling
784
+ */
785
+ async function executeWithErrorHandling(
786
+ command: string,
787
+ args: string[],
788
+ options: SpawnOptions = {}
789
+ ): Promise<CLIResult> {
790
+ try {
791
+ const result = await spawnCLI(command, args, options);
792
+
793
+ // Check for errors even on zero exit code
794
+ // Some CLIs output errors to stderr but still exit 0
795
+ if (result.stderr && !isExpectedStderr(result.stderr)) {
796
+ console.warn(`Warning from ${command}:`, result.stderr);
797
+ }
798
+
799
+ return result;
800
+ } catch (error) {
801
+ const cliError = detectErrorType(error);
802
+
803
+ // Log structured error for debugging
804
+ console.error('CLI Error:', {
805
+ type: cliError.type,
806
+ message: cliError.message,
807
+ retryable: cliError.isRetryable(),
808
+ requiresAction: cliError.requiresUserAction(),
809
+ details: cliError.details,
810
+ });
811
+
812
+ throw cliError;
813
+ }
814
+ }
815
+
816
+ /**
817
+ * Determine if stderr output is expected/informational
818
+ */
819
+ function isExpectedStderr(stderr: string): boolean {
820
+ const lowerStderr = stderr.toLowerCase();
821
+
822
+ // Common informational messages
823
+ const expectedPatterns = [
824
+ 'warning:',
825
+ 'note:',
826
+ 'info:',
827
+ 'downloading',
828
+ 'progress:',
829
+ 'fetching',
830
+ ];
831
+
832
+ return expectedPatterns.some((pattern) => lowerStderr.includes(pattern));
833
+ }
834
+ ```
835
+
836
+ ### Retry Logic with Exponential Backoff
837
+
838
+ ```typescript
839
+ interface RetryOptions {
840
+ maxRetries?: number;
841
+ initialDelay?: number;
842
+ maxDelay?: number;
843
+ backoffFactor?: number;
844
+ shouldRetry?: (error: CLIError) => boolean;
845
+ }
846
+
847
+ /**
848
+ * Execute CLI command with retry logic
849
+ */
850
+ async function executeWithRetry(
851
+ command: string,
852
+ args: string[],
853
+ spawnOptions: SpawnOptions = {},
854
+ retryOptions: RetryOptions = {}
855
+ ): Promise<CLIResult> {
856
+ const {
857
+ maxRetries = 3,
858
+ initialDelay = 1000,
859
+ maxDelay = 30000,
860
+ backoffFactor = 2,
861
+ shouldRetry = (error) => error.isRetryable(),
862
+ } = retryOptions;
863
+
864
+ let lastError: CLIError;
865
+ let delay = initialDelay;
866
+
867
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
868
+ try {
869
+ return await executeWithErrorHandling(command, args, spawnOptions);
870
+ } catch (error) {
871
+ if (!(error instanceof CLIError)) {
872
+ throw error;
873
+ }
874
+
875
+ lastError = error;
876
+
877
+ // Don't retry if not retryable or max retries reached
878
+ if (!shouldRetry(error) || attempt === maxRetries) {
879
+ break;
880
+ }
881
+
882
+ console.log(`Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`);
883
+ await new Promise((resolve) => setTimeout(resolve, delay));
884
+
885
+ // Exponential backoff
886
+ delay = Math.min(delay * backoffFactor, maxDelay);
887
+ }
888
+ }
889
+
890
+ throw lastError!;
891
+ }
892
+
893
+ // Example usage
894
+ async function callResilientCLI() {
895
+ return executeWithRetry(
896
+ 'some-ai-cli',
897
+ ['generate', '--prompt', 'Hello'],
898
+ {
899
+ timeout: 60000,
900
+ },
901
+ {
902
+ maxRetries: 3,
903
+ initialDelay: 2000,
904
+ shouldRetry: (error) => {
905
+ // Retry on network errors and rate limits
906
+ return [
907
+ CLIErrorType.NETWORK_ERROR,
908
+ CLIErrorType.RATE_LIMITED,
909
+ CLIErrorType.CONNECTION_TIMEOUT,
910
+ ].includes(error.type);
911
+ },
912
+ }
913
+ );
914
+ }
915
+ ```
916
+
917
+ ---
918
+
919
+ ## Timeout Handling
920
+
921
+ ### Production-Ready Timeout Patterns
922
+
923
+ ```typescript
924
+ /**
925
+ * Timeout handler with graceful degradation
926
+ */
927
+ class TimeoutHandler {
928
+ private timeoutId?: NodeJS.Timeout;
929
+ private killTimeoutId?: NodeJS.Timeout;
930
+ private child?: ChildProcess;
931
+
932
+ constructor(
933
+ private readonly process: ChildProcess,
934
+ private readonly timeoutMs: number,
935
+ private readonly gracePeriodMs: number = 5000
936
+ ) {
937
+ this.child = process;
938
+ this.start();
939
+ }
940
+
941
+ private start(): void {
942
+ this.timeoutId = setTimeout(() => {
943
+ this.handleTimeout();
944
+ }, this.timeoutMs);
945
+ }
946
+
947
+ private handleTimeout(): void {
948
+ if (!this.child) return;
949
+
950
+ console.warn(`Process timeout after ${this.timeoutMs}ms, sending SIGTERM`);
951
+
952
+ // First, try graceful termination
953
+ this.child.kill('SIGTERM');
954
+
955
+ // If process doesn't exit within grace period, force kill
956
+ this.killTimeoutId = setTimeout(() => {
957
+ if (this.child && this.child.exitCode === null) {
958
+ console.error('Process did not exit gracefully, sending SIGKILL');
959
+ this.child.kill('SIGKILL');
960
+ }
961
+ }, this.gracePeriodMs);
962
+ }
963
+
964
+ cancel(): void {
965
+ if (this.timeoutId) {
966
+ clearTimeout(this.timeoutId);
967
+ this.timeoutId = undefined;
968
+ }
969
+ if (this.killTimeoutId) {
970
+ clearTimeout(this.killTimeoutId);
971
+ this.killTimeoutId = undefined;
972
+ }
973
+ }
974
+ }
975
+
976
+ /**
977
+ * Spawn with advanced timeout handling
978
+ */
979
+ async function spawnWithAdvancedTimeout(
980
+ command: string,
981
+ args: string[],
982
+ options: {
983
+ timeout: number;
984
+ gracePeriod?: number;
985
+ onTimeout?: () => void;
986
+ }
987
+ ): Promise<CLIResult> {
988
+ return new Promise((resolve, reject) => {
989
+ const child = spawn(command, args, {
990
+ stdio: ['pipe', 'pipe', 'pipe'],
991
+ shell: false,
992
+ });
993
+
994
+ let stdout = '';
995
+ let stderr = '';
996
+ let timedOut = false;
997
+
998
+ // Create timeout handler
999
+ const timeoutHandler = new TimeoutHandler(
1000
+ child,
1001
+ options.timeout,
1002
+ options.gracePeriod
1003
+ );
1004
+
1005
+ child.stdout?.on('data', (chunk) => {
1006
+ stdout += chunk.toString('utf8');
1007
+ });
1008
+
1009
+ child.stderr?.on('data', (chunk) => {
1010
+ stderr += chunk.toString('utf8');
1011
+ });
1012
+
1013
+ child.on('close', (exitCode, signal) => {
1014
+ timeoutHandler.cancel();
1015
+
1016
+ if (timedOut) {
1017
+ reject(
1018
+ new CLIError(
1019
+ CLIErrorType.TIMEOUT,
1020
+ `Process timed out after ${options.timeout}ms`,
1021
+ { exitCode, signal, stdout, stderr }
1022
+ )
1023
+ );
1024
+ return;
1025
+ }
1026
+
1027
+ resolve({ stdout, stderr, exitCode, signal });
1028
+ });
1029
+
1030
+ child.on('error', (error) => {
1031
+ timeoutHandler.cancel();
1032
+ reject(error);
1033
+ });
1034
+
1035
+ // Mark as timed out if SIGTERM was sent
1036
+ child.once('SIGTERM', () => {
1037
+ timedOut = true;
1038
+ options.onTimeout?.();
1039
+ });
1040
+ });
1041
+ }
1042
+ ```
1043
+
1044
+ ### Adaptive Timeouts Based on Operation Type
1045
+
1046
+ ```typescript
1047
+ /**
1048
+ * Calculate adaptive timeout based on operation type and context
1049
+ */
1050
+ function getAdaptiveTimeout(operation: {
1051
+ type: 'read' | 'write' | 'generate' | 'analyze';
1052
+ estimatedTokens?: number;
1053
+ complexity?: 'low' | 'medium' | 'high';
1054
+ }): number {
1055
+ const baseTimeouts = {
1056
+ read: 30000, // 30 seconds
1057
+ write: 60000, // 1 minute
1058
+ generate: 120000, // 2 minutes
1059
+ analyze: 180000, // 3 minutes
1060
+ };
1061
+
1062
+ let timeout = baseTimeouts[operation.type];
1063
+
1064
+ // Adjust for token count (AI operations)
1065
+ if (operation.estimatedTokens) {
1066
+ // ~100ms per 100 tokens (rough estimate)
1067
+ timeout += (operation.estimatedTokens / 100) * 100;
1068
+ }
1069
+
1070
+ // Adjust for complexity
1071
+ const complexityMultipliers = {
1072
+ low: 1,
1073
+ medium: 1.5,
1074
+ high: 2,
1075
+ };
1076
+
1077
+ if (operation.complexity) {
1078
+ timeout *= complexityMultipliers[operation.complexity];
1079
+ }
1080
+
1081
+ // Cap at 10 minutes
1082
+ return Math.min(timeout, 600000);
1083
+ }
1084
+
1085
+ // Example usage
1086
+ async function adaptiveExecute(prompt: string) {
1087
+ const timeout = getAdaptiveTimeout({
1088
+ type: 'generate',
1089
+ estimatedTokens: prompt.length * 2, // Rough estimate
1090
+ complexity: 'high',
1091
+ });
1092
+
1093
+ return spawnCLI('claude', ['--prompt', prompt], { timeout });
1094
+ }
1095
+ ```
1096
+
1097
+ ---
1098
+
1099
+ ## Shell Escaping & Security
1100
+
1101
+ ### Critical Security Principles
1102
+
1103
+ **🔴 NEVER use `exec()` with user input**
1104
+ **🟢 ALWAYS use `spawn()` or `execFile()` with argument arrays**
1105
+
1106
+ ### Safe Argument Handling
1107
+
1108
+ ```typescript
1109
+ /**
1110
+ * Validate and sanitize CLI arguments
1111
+ * NEVER trust user input directly
1112
+ */
1113
+ class ArgumentValidator {
1114
+ /**
1115
+ * Validate argument against whitelist pattern
1116
+ */
1117
+ static validate(
1118
+ arg: string,
1119
+ rules: {
1120
+ pattern?: RegExp;
1121
+ maxLength?: number;
1122
+ allowedChars?: RegExp;
1123
+ blockedPatterns?: RegExp[];
1124
+ }
1125
+ ): boolean {
1126
+ // Check length
1127
+ if (rules.maxLength && arg.length > rules.maxLength) {
1128
+ return false;
1129
+ }
1130
+
1131
+ // Check against whitelist pattern
1132
+ if (rules.pattern && !rules.pattern.test(arg)) {
1133
+ return false;
1134
+ }
1135
+
1136
+ // Check allowed characters
1137
+ if (rules.allowedChars && !rules.allowedChars.test(arg)) {
1138
+ return false;
1139
+ }
1140
+
1141
+ // Check blocked patterns
1142
+ if (rules.blockedPatterns) {
1143
+ for (const blocked of rules.blockedPatterns) {
1144
+ if (blocked.test(arg)) {
1145
+ return false;
1146
+ }
1147
+ }
1148
+ }
1149
+
1150
+ return true;
1151
+ }
1152
+
1153
+ /**
1154
+ * Sanitize filename for use in CLI
1155
+ */
1156
+ static sanitizeFilename(filename: string): string {
1157
+ // Remove directory traversal attempts
1158
+ const cleaned = filename.replace(/\.\.\//g, '').replace(/\.\.\\/g, '');
1159
+
1160
+ // Allow only alphanumeric, dash, underscore, dot
1161
+ const sanitized = cleaned.replace(/[^a-zA-Z0-9._-]/g, '_');
1162
+
1163
+ // Limit length
1164
+ return sanitized.slice(0, 255);
1165
+ }
1166
+
1167
+ /**
1168
+ * Validate that path is within allowed directory
1169
+ */
1170
+ static isPathSafe(filePath: string, baseDir: string): boolean {
1171
+ const { resolve, relative } = require('path');
1172
+
1173
+ const resolvedBase = resolve(baseDir);
1174
+ const resolvedPath = resolve(baseDir, filePath);
1175
+
1176
+ // Check if resolved path is within base directory
1177
+ const relativePath = relative(resolvedBase, resolvedPath);
1178
+
1179
+ return (
1180
+ !relativePath.startsWith('..') &&
1181
+ !relativePath.startsWith('/') &&
1182
+ resolvedPath.startsWith(resolvedBase)
1183
+ );
1184
+ }
1185
+ }
1186
+
1187
+ /**
1188
+ * Example: Safe file operation
1189
+ */
1190
+ async function safeFileOperation(
1191
+ userProvidedFilename: string,
1192
+ baseDir: string
1193
+ ): Promise<void> {
1194
+ // Sanitize filename
1195
+ const sanitized = ArgumentValidator.sanitizeFilename(userProvidedFilename);
1196
+
1197
+ // Verify path is safe
1198
+ if (!ArgumentValidator.isPathSafe(sanitized, baseDir)) {
1199
+ throw new CLIError(
1200
+ CLIErrorType.PERMISSION_DENIED,
1201
+ 'Invalid file path: directory traversal attempt detected'
1202
+ );
1203
+ }
1204
+
1205
+ // Use execFile with explicit arguments (safe)
1206
+ await safeExecute('cat', [sanitized], { cwd: baseDir });
1207
+ }
1208
+ ```
1209
+
1210
+ ### Shell Injection Prevention
1211
+
1212
+ ```typescript
1213
+ /**
1214
+ * Common shell metacharacters that should raise red flags
1215
+ */
1216
+ const SHELL_METACHARACTERS = [
1217
+ ';', // Command separator
1218
+ '&', // Background execution
1219
+ '|', // Pipe
1220
+ '$', // Variable expansion
1221
+ '`', // Command substitution
1222
+ '(', ')', // Subshell
1223
+ '<', '>', // Redirection
1224
+ '\n', '\r', // Newlines
1225
+ '*', '?', '[', ']', // Globbing
1226
+ ];
1227
+
1228
+ /**
1229
+ * Detect potential shell injection attempts
1230
+ */
1231
+ function detectShellInjection(input: string): boolean {
1232
+ return SHELL_METACHARACTERS.some((char) => input.includes(char));
1233
+ }
1234
+
1235
+ /**
1236
+ * Safe CLI execution wrapper
1237
+ */
1238
+ async function executeSafely(
1239
+ command: string,
1240
+ args: string[],
1241
+ options: {
1242
+ allowShellMetacharacters?: boolean;
1243
+ validateArgs?: boolean;
1244
+ } = {}
1245
+ ): Promise<CLIResult> {
1246
+ const { allowShellMetacharacters = false, validateArgs = true } = options;
1247
+
1248
+ // Validate command name
1249
+ if (detectShellInjection(command)) {
1250
+ throw new CLIError(
1251
+ CLIErrorType.PERMISSION_DENIED,
1252
+ 'Shell metacharacters detected in command name'
1253
+ );
1254
+ }
1255
+
1256
+ // Validate arguments
1257
+ if (validateArgs && !allowShellMetacharacters) {
1258
+ for (const arg of args) {
1259
+ if (detectShellInjection(arg)) {
1260
+ throw new CLIError(
1261
+ CLIErrorType.PERMISSION_DENIED,
1262
+ `Shell metacharacters detected in argument: ${arg}`
1263
+ );
1264
+ }
1265
+ }
1266
+ }
1267
+
1268
+ // Use spawn with explicit args (never shell)
1269
+ return spawnCLI(command, args, { ...options });
1270
+ }
1271
+
1272
+ // Example: Unsafe vs Safe
1273
+ async function unsafeExample(userInput: string) {
1274
+ // 🔴 DANGEROUS - Never do this!
1275
+ // const { exec } = require('child_process');
1276
+ // exec(`echo ${userInput}`); // Shell injection vulnerability!
1277
+
1278
+ // 🟢 SAFE - Use spawn with argument array
1279
+ return executeSafely('echo', [userInput], {
1280
+ validateArgs: true,
1281
+ });
1282
+ }
1283
+ ```
1284
+
1285
+ ### Secure Environment Variable Handling
1286
+
1287
+ ```typescript
1288
+ /**
1289
+ * Create sanitized environment for child processes
1290
+ * Following principle of least privilege
1291
+ */
1292
+ function createSecureEnv(
1293
+ additionalEnv: Record<string, string> = {}
1294
+ ): NodeJS.ProcessEnv {
1295
+ // Start with minimal environment
1296
+ const secureEnv: NodeJS.ProcessEnv = {
1297
+ // Keep essential vars
1298
+ PATH: process.env.PATH,
1299
+ HOME: process.env.HOME,
1300
+ USER: process.env.USER,
1301
+ // Add any required vars
1302
+ ...additionalEnv,
1303
+ };
1304
+
1305
+ // NEVER pass these sensitive vars to child processes
1306
+ const BLOCKED_VARS = [
1307
+ /^AWS_/,
1308
+ /^AZURE_/,
1309
+ /^GCP_/,
1310
+ /_SECRET$/,
1311
+ /_KEY$/,
1312
+ /_TOKEN$/,
1313
+ /_PASSWORD$/,
1314
+ /^DATABASE_/,
1315
+ /^DB_/,
1316
+ ];
1317
+
1318
+ // Remove any accidentally included sensitive vars
1319
+ for (const key of Object.keys(secureEnv)) {
1320
+ if (BLOCKED_VARS.some((pattern) => pattern.test(key))) {
1321
+ delete secureEnv[key];
1322
+ console.warn(`Removed sensitive env var from child process: ${key}`);
1323
+ }
1324
+ }
1325
+
1326
+ return secureEnv;
1327
+ }
1328
+
1329
+ /**
1330
+ * Execute with minimal environment
1331
+ */
1332
+ async function executeWithMinimalEnv(
1333
+ command: string,
1334
+ args: string[],
1335
+ requiredEnv: Record<string, string> = {}
1336
+ ): Promise<CLIResult> {
1337
+ const env = createSecureEnv(requiredEnv);
1338
+
1339
+ return spawnCLI(command, args, { env });
1340
+ }
1341
+
1342
+ // Example usage
1343
+ async function callGitHubCLI() {
1344
+ // Only pass the specific token needed, not entire env
1345
+ return executeWithMinimalEnv('gh', ['api', 'user'], {
1346
+ GITHUB_TOKEN: process.env.GITHUB_TOKEN!, // Explicitly allowed
1347
+ });
1348
+ }
1349
+ ```
1350
+
1351
+ ---
1352
+
1353
+ ## Buffer Management
1354
+
1355
+ ### Handling Large Outputs
1356
+
1357
+ ```typescript
1358
+ /**
1359
+ * Stream-based approach for large outputs
1360
+ * Avoids maxBuffer limitations by processing data incrementally
1361
+ */
1362
+ async function handleLargeOutput(
1363
+ command: string,
1364
+ args: string[],
1365
+ processor: (chunk: string) => void | Promise<void>
1366
+ ): Promise<void> {
1367
+ return new Promise((resolve, reject) => {
1368
+ const child = spawn(command, args, {
1369
+ stdio: ['pipe', 'pipe', 'pipe'],
1370
+ shell: false,
1371
+ });
1372
+
1373
+ // Process stdout chunks as they arrive
1374
+ // No buffer limits - data is processed and discarded
1375
+ child.stdout?.on('data', async (chunk: Buffer) => {
1376
+ const text = chunk.toString('utf8');
1377
+ try {
1378
+ await processor(text);
1379
+ } catch (error) {
1380
+ child.kill('SIGTERM');
1381
+ reject(error);
1382
+ }
1383
+ });
1384
+
1385
+ child.stderr?.on('data', (chunk: Buffer) => {
1386
+ console.error('stderr:', chunk.toString('utf8'));
1387
+ });
1388
+
1389
+ child.on('close', (exitCode) => {
1390
+ if (exitCode === 0) {
1391
+ resolve();
1392
+ } else {
1393
+ reject(new Error(`Process exited with code ${exitCode}`));
1394
+ }
1395
+ });
1396
+
1397
+ child.on('error', reject);
1398
+ });
1399
+ }
1400
+
1401
+ // Example: Process large file line by line
1402
+ async function processLargeFile(filePath: string) {
1403
+ let lineCount = 0;
1404
+
1405
+ await handleLargeOutput('cat', [filePath], (chunk) => {
1406
+ const lines = chunk.split('\n');
1407
+ lineCount += lines.length;
1408
+
1409
+ // Process each line without storing all in memory
1410
+ for (const line of lines) {
1411
+ if (line.trim()) {
1412
+ console.log(`Processing line ${lineCount}: ${line.slice(0, 50)}...`);
1413
+ }
1414
+ }
1415
+ });
1416
+
1417
+ console.log(`Processed ${lineCount} lines`);
1418
+ }
1419
+ ```
1420
+
1421
+ ### Dynamic Buffer Sizing
1422
+
1423
+ ```typescript
1424
+ /**
1425
+ * Calculate appropriate buffer size based on operation
1426
+ */
1427
+ function calculateBufferSize(options: {
1428
+ operationType: 'read' | 'write' | 'generate';
1429
+ estimatedSize?: number;
1430
+ safetyFactor?: number;
1431
+ }): number {
1432
+ const {
1433
+ operationType,
1434
+ estimatedSize,
1435
+ safetyFactor = 2, // 2x safety margin
1436
+ } = options;
1437
+
1438
+ // Base buffer sizes
1439
+ const baseBuffers = {
1440
+ read: 1024 * 1024, // 1MB
1441
+ write: 512 * 1024, // 512KB
1442
+ generate: 10 * 1024 * 1024, // 10MB for AI generation
1443
+ };
1444
+
1445
+ let bufferSize = baseBuffers[operationType];
1446
+
1447
+ // Adjust based on estimated size
1448
+ if (estimatedSize) {
1449
+ bufferSize = Math.max(bufferSize, estimatedSize * safetyFactor);
1450
+ }
1451
+
1452
+ // Cap at 100MB to prevent excessive memory usage
1453
+ return Math.min(bufferSize, 100 * 1024 * 1024);
1454
+ }
1455
+
1456
+ /**
1457
+ * Execute with dynamic buffer sizing
1458
+ */
1459
+ async function executeDynamic(
1460
+ command: string,
1461
+ args: string[],
1462
+ options: {
1463
+ operationType: 'read' | 'write' | 'generate';
1464
+ estimatedSize?: number;
1465
+ }
1466
+ ): Promise<CLIResult> {
1467
+ const maxBuffer = calculateBufferSize(options);
1468
+
1469
+ console.log(`Using buffer size: ${(maxBuffer / 1024 / 1024).toFixed(2)}MB`);
1470
+
1471
+ return spawnCLI(command, args, { maxBuffer });
1472
+ }
1473
+ ```
1474
+
1475
+ ### Write Stream to File Instead of Memory
1476
+
1477
+ ```typescript
1478
+ import { createWriteStream } from 'fs';
1479
+ import { pipeline } from 'stream/promises';
1480
+
1481
+ /**
1482
+ * Pipe large CLI output directly to file
1483
+ * Avoids memory issues entirely
1484
+ */
1485
+ async function pipeOutputToFile(
1486
+ command: string,
1487
+ args: string[],
1488
+ outputPath: string
1489
+ ): Promise<void> {
1490
+ const child = spawn(command, args, {
1491
+ stdio: ['pipe', 'pipe', 'pipe'],
1492
+ shell: false,
1493
+ });
1494
+
1495
+ const writeStream = createWriteStream(outputPath);
1496
+
1497
+ try {
1498
+ // Pipe stdout directly to file
1499
+ await pipeline(child.stdout!, writeStream);
1500
+ } catch (error) {
1501
+ throw new Error(`Failed to pipe output to file: ${error}`);
1502
+ }
1503
+
1504
+ return new Promise((resolve, reject) => {
1505
+ child.on('close', (exitCode) => {
1506
+ if (exitCode === 0) {
1507
+ resolve();
1508
+ } else {
1509
+ reject(new Error(`Process exited with code ${exitCode}`));
1510
+ }
1511
+ });
1512
+
1513
+ child.on('error', reject);
1514
+ });
1515
+ }
1516
+
1517
+ // Example: Download large dataset
1518
+ async function downloadDataset(url: string, outputPath: string) {
1519
+ await pipeOutputToFile('curl', ['-L', url], outputPath);
1520
+ console.log(`Downloaded to ${outputPath}`);
1521
+ }
1522
+ ```
1523
+
1524
+ ---
1525
+
1526
+ ## Exit Code Handling
1527
+
1528
+ ### Standard Unix Exit Codes
1529
+
1530
+ ```typescript
1531
+ /**
1532
+ * Standard POSIX exit codes
1533
+ */
1534
+ export enum ExitCode {
1535
+ SUCCESS = 0,
1536
+ GENERAL_ERROR = 1,
1537
+ MISUSE_OF_SHELL_BUILTIN = 2,
1538
+
1539
+ // sysexits.h codes (64-78)
1540
+ USAGE_ERROR = 64, // Command line usage error
1541
+ DATA_ERROR = 65, // Data format error
1542
+ NO_INPUT = 66, // Cannot open input
1543
+ NO_USER = 67, // Addressee unknown
1544
+ NO_HOST = 68, // Host name unknown
1545
+ UNAVAILABLE = 69, // Service unavailable
1546
+ SOFTWARE_ERROR = 70, // Internal software error
1547
+ OS_ERROR = 71, // System error (e.g., can't fork)
1548
+ OS_FILE_ERROR = 72, // Critical OS file missing
1549
+ CANT_CREATE = 73, // Can't create (user) output file
1550
+ IO_ERROR = 74, // Input/output error
1551
+ TEMP_FAIL = 75, // Temp failure; user is invited to retry
1552
+ PROTOCOL_ERROR = 76, // Remote error in protocol
1553
+ NO_PERMISSION = 77, // Permission denied
1554
+ CONFIG_ERROR = 78, // Configuration error
1555
+
1556
+ // 126-165 are reserved
1557
+ COMMAND_NOT_EXECUTABLE = 126,
1558
+ COMMAND_NOT_FOUND = 127,
1559
+
1560
+ // 128+N = terminated by signal N
1561
+ TERMINATED_BY_SIGNAL_BASE = 128,
1562
+
1563
+ // Common signal terminations
1564
+ SIGINT = 130, // 128 + 2 (Ctrl+C)
1565
+ SIGTERM = 143, // 128 + 15
1566
+ SIGKILL = 137, // 128 + 9
1567
+ }
1568
+
1569
+ /**
1570
+ * Interpret exit code and provide human-readable message
1571
+ */
1572
+ function interpretExitCode(exitCode: number, signal: NodeJS.Signals | null): {
1573
+ type: 'success' | 'error' | 'signal';
1574
+ message: string;
1575
+ retryable: boolean;
1576
+ } {
1577
+ // Success
1578
+ if (exitCode === ExitCode.SUCCESS) {
1579
+ return {
1580
+ type: 'success',
1581
+ message: 'Command completed successfully',
1582
+ retryable: false,
1583
+ };
1584
+ }
1585
+
1586
+ // Signal termination
1587
+ if (signal) {
1588
+ return {
1589
+ type: 'signal',
1590
+ message: `Process terminated by signal: ${signal}`,
1591
+ retryable: signal === 'SIGTERM' || signal === 'SIGINT',
1592
+ };
1593
+ }
1594
+
1595
+ // Exit code >= 128 indicates signal termination
1596
+ if (exitCode >= ExitCode.TERMINATED_BY_SIGNAL_BASE) {
1597
+ const signalNumber = exitCode - ExitCode.TERMINATED_BY_SIGNAL_BASE;
1598
+ return {
1599
+ type: 'signal',
1600
+ message: `Process terminated by signal ${signalNumber}`,
1601
+ retryable: true,
1602
+ };
1603
+ }
1604
+
1605
+ // Specific exit codes
1606
+ switch (exitCode) {
1607
+ case ExitCode.COMMAND_NOT_FOUND:
1608
+ return {
1609
+ type: 'error',
1610
+ message: 'Command not found',
1611
+ retryable: false,
1612
+ };
1613
+
1614
+ case ExitCode.COMMAND_NOT_EXECUTABLE:
1615
+ return {
1616
+ type: 'error',
1617
+ message: 'Command not executable (permission denied)',
1618
+ retryable: false,
1619
+ };
1620
+
1621
+ case ExitCode.USAGE_ERROR:
1622
+ return {
1623
+ type: 'error',
1624
+ message: 'Invalid command usage',
1625
+ retryable: false,
1626
+ };
1627
+
1628
+ case ExitCode.DATA_ERROR:
1629
+ return {
1630
+ type: 'error',
1631
+ message: 'Invalid data format',
1632
+ retryable: false,
1633
+ };
1634
+
1635
+ case ExitCode.TEMP_FAIL:
1636
+ return {
1637
+ type: 'error',
1638
+ message: 'Temporary failure (retry recommended)',
1639
+ retryable: true,
1640
+ };
1641
+
1642
+ case ExitCode.UNAVAILABLE:
1643
+ return {
1644
+ type: 'error',
1645
+ message: 'Service unavailable',
1646
+ retryable: true,
1647
+ };
1648
+
1649
+ case ExitCode.NO_PERMISSION:
1650
+ return {
1651
+ type: 'error',
1652
+ message: 'Permission denied',
1653
+ retryable: false,
1654
+ };
1655
+
1656
+ default:
1657
+ return {
1658
+ type: 'error',
1659
+ message: `Command failed with exit code ${exitCode}`,
1660
+ retryable: exitCode === ExitCode.GENERAL_ERROR, // Maybe retryable
1661
+ };
1662
+ }
1663
+ }
1664
+
1665
+ /**
1666
+ * Execute and handle exit codes intelligently
1667
+ */
1668
+ async function executeWithExitCodeHandling(
1669
+ command: string,
1670
+ args: string[]
1671
+ ): Promise<CLIResult> {
1672
+ const result = await spawnCLI(command, args);
1673
+
1674
+ const interpretation = interpretExitCode(result.exitCode, result.signal);
1675
+
1676
+ if (interpretation.type !== 'success') {
1677
+ throw new CLIError(
1678
+ interpretation.retryable ? CLIErrorType.TEMP_FAIL : CLIErrorType.NON_ZERO_EXIT,
1679
+ interpretation.message,
1680
+ {
1681
+ command,
1682
+ args,
1683
+ exitCode: result.exitCode,
1684
+ signal: result.signal,
1685
+ stdout: result.stdout,
1686
+ stderr: result.stderr,
1687
+ }
1688
+ );
1689
+ }
1690
+
1691
+ return result;
1692
+ }
1693
+ ```
1694
+
1695
+ ### CLI-Specific Exit Code Mappings
1696
+
1697
+ ```typescript
1698
+ /**
1699
+ * Map CLI-specific exit codes to standard errors
1700
+ * Many CLI tools use custom exit code ranges
1701
+ */
1702
+ const CLI_EXIT_CODE_MAPS: Record<string, Record<number, CLIErrorType>> = {
1703
+ // GitHub CLI
1704
+ gh: {
1705
+ 1: CLIErrorType.GENERAL_ERROR,
1706
+ 2: CLIErrorType.AUTH_REQUIRED,
1707
+ 4: CLIErrorType.RATE_LIMITED,
1708
+ },
1709
+
1710
+ // Git
1711
+ git: {
1712
+ 1: CLIErrorType.GENERAL_ERROR,
1713
+ 128: CLIErrorType.INVALID_OUTPUT, // Git specific: invalid argument
1714
+ 129: CLIErrorType.SIGNAL_TERMINATED, // SIGHUP
1715
+ },
1716
+
1717
+ // Claude CLI (example)
1718
+ claude: {
1719
+ 1: CLIErrorType.GENERAL_ERROR,
1720
+ 2: CLIErrorType.AUTH_REQUIRED,
1721
+ 3: CLIErrorType.RATE_LIMITED,
1722
+ 4: CLIErrorType.QUOTA_EXCEEDED,
1723
+ 5: CLIErrorType.INVALID_OUTPUT,
1724
+ },
1725
+ };
1726
+
1727
+ /**
1728
+ * Get error type based on CLI-specific exit code
1729
+ */
1730
+ function getErrorTypeFromExitCode(
1731
+ command: string,
1732
+ exitCode: number
1733
+ ): CLIErrorType {
1734
+ const cliName = command.split('/').pop() || command;
1735
+ const cliMap = CLI_EXIT_CODE_MAPS[cliName];
1736
+
1737
+ if (cliMap && cliMap[exitCode]) {
1738
+ return cliMap[exitCode];
1739
+ }
1740
+
1741
+ // Default mapping
1742
+ if (exitCode === 0) {
1743
+ return CLIErrorType.NON_ZERO_EXIT; // Should never happen
1744
+ }
1745
+
1746
+ return CLIErrorType.GENERAL_ERROR;
1747
+ }
1748
+ ```
1749
+
1750
+ ---
1751
+
1752
+ ## Environment Variables
1753
+
1754
+ ### Secure Environment Variable Passing
1755
+
1756
+ ```typescript
1757
+ /**
1758
+ * Environment variable manager for CLI tools
1759
+ * Implements principle of least privilege
1760
+ */
1761
+ class EnvironmentManager {
1762
+ private static readonly SENSITIVE_PATTERNS = [
1763
+ /^AWS_/,
1764
+ /^AZURE_/,
1765
+ /^GCP_/,
1766
+ /_SECRET$/,
1767
+ /_KEY$/,
1768
+ /_TOKEN$/,
1769
+ /_PASSWORD$/,
1770
+ /^DATABASE_/,
1771
+ /^DB_/,
1772
+ /^OPENAI_/,
1773
+ /^ANTHROPIC_/,
1774
+ ];
1775
+
1776
+ /**
1777
+ * Create minimal environment for command
1778
+ */
1779
+ static createMinimalEnv(
1780
+ allowedVars: string[] = []
1781
+ ): NodeJS.ProcessEnv {
1782
+ const minimalEnv: NodeJS.ProcessEnv = {
1783
+ // Essential system vars
1784
+ PATH: process.env.PATH,
1785
+ HOME: process.env.HOME,
1786
+ USER: process.env.USER,
1787
+ TMPDIR: process.env.TMPDIR,
1788
+ };
1789
+
1790
+ // Add explicitly allowed vars
1791
+ for (const varName of allowedVars) {
1792
+ if (process.env[varName]) {
1793
+ minimalEnv[varName] = process.env[varName];
1794
+ }
1795
+ }
1796
+
1797
+ return minimalEnv;
1798
+ }
1799
+
1800
+ /**
1801
+ * Create env with specific CLI tool requirements
1802
+ */
1803
+ static createCLIEnv(
1804
+ cli: 'gh' | 'git' | 'claude' | 'openai',
1805
+ additionalVars: Record<string, string> = {}
1806
+ ): NodeJS.ProcessEnv {
1807
+ const cliRequirements: Record<string, string[]> = {
1808
+ gh: ['GITHUB_TOKEN', 'GH_TOKEN'],
1809
+ git: ['GIT_AUTHOR_NAME', 'GIT_AUTHOR_EMAIL', 'GIT_COMMITTER_NAME', 'GIT_COMMITTER_EMAIL'],
1810
+ claude: ['ANTHROPIC_API_KEY', 'CLAUDE_API_KEY'],
1811
+ openai: ['OPENAI_API_KEY'],
1812
+ };
1813
+
1814
+ const allowedVars = cliRequirements[cli] || [];
1815
+ const env = this.createMinimalEnv(allowedVars);
1816
+
1817
+ // Add additional vars
1818
+ Object.assign(env, additionalVars);
1819
+
1820
+ return env;
1821
+ }
1822
+
1823
+ /**
1824
+ * Validate that no sensitive vars are accidentally exposed
1825
+ */
1826
+ static validateEnv(env: NodeJS.ProcessEnv): void {
1827
+ for (const key of Object.keys(env)) {
1828
+ if (this.isSensitiveVar(key)) {
1829
+ console.warn(`Warning: Sensitive env var passed to child process: ${key}`);
1830
+ }
1831
+ }
1832
+ }
1833
+
1834
+ /**
1835
+ * Check if var name matches sensitive patterns
1836
+ */
1837
+ static isSensitiveVar(varName: string): boolean {
1838
+ return this.SENSITIVE_PATTERNS.some((pattern) => pattern.test(varName));
1839
+ }
1840
+
1841
+ /**
1842
+ * Redact sensitive values for logging
1843
+ */
1844
+ static redactForLogging(env: NodeJS.ProcessEnv): Record<string, string> {
1845
+ const redacted: Record<string, string> = {};
1846
+
1847
+ for (const [key, value] of Object.entries(env)) {
1848
+ if (this.isSensitiveVar(key)) {
1849
+ redacted[key] = '***REDACTED***';
1850
+ } else {
1851
+ redacted[key] = value || '';
1852
+ }
1853
+ }
1854
+
1855
+ return redacted;
1856
+ }
1857
+ }
1858
+
1859
+ /**
1860
+ * Execute with managed environment
1861
+ */
1862
+ async function executeWithManagedEnv(
1863
+ command: string,
1864
+ args: string[],
1865
+ options: {
1866
+ cli?: 'gh' | 'git' | 'claude' | 'openai';
1867
+ additionalEnv?: Record<string, string>;
1868
+ allowedVars?: string[];
1869
+ } = {}
1870
+ ): Promise<CLIResult> {
1871
+ let env: NodeJS.ProcessEnv;
1872
+
1873
+ if (options.cli) {
1874
+ env = EnvironmentManager.createCLIEnv(options.cli, options.additionalEnv);
1875
+ } else {
1876
+ env = EnvironmentManager.createMinimalEnv(options.allowedVars);
1877
+ Object.assign(env, options.additionalEnv);
1878
+ }
1879
+
1880
+ // Validate before execution
1881
+ EnvironmentManager.validateEnv(env);
1882
+
1883
+ // Log redacted environment (for debugging)
1884
+ console.debug('Executing with env:', EnvironmentManager.redactForLogging(env));
1885
+
1886
+ return spawnCLI(command, args, { env });
1887
+ }
1888
+
1889
+ // Example usage
1890
+ async function callGitHubCLI() {
1891
+ return executeWithManagedEnv('gh', ['api', 'user'], {
1892
+ cli: 'gh',
1893
+ });
1894
+ }
1895
+
1896
+ async function callCustomCLI() {
1897
+ return executeWithManagedEnv('custom-cli', ['process'], {
1898
+ allowedVars: ['CUSTOM_API_KEY'],
1899
+ additionalEnv: {
1900
+ LOG_LEVEL: 'debug',
1901
+ },
1902
+ });
1903
+ }
1904
+ ```
1905
+
1906
+ ### Alternative: File-Based Secrets
1907
+
1908
+ ```typescript
1909
+ import { readFile } from 'fs/promises';
1910
+ import { join } from 'path';
1911
+
1912
+ /**
1913
+ * Load secrets from file instead of environment
1914
+ * More secure than env vars for sensitive data
1915
+ */
1916
+ class SecretManager {
1917
+ private static cache = new Map<string, string>();
1918
+
1919
+ /**
1920
+ * Load secret from file
1921
+ */
1922
+ static async loadSecret(secretName: string): Promise<string> {
1923
+ if (this.cache.has(secretName)) {
1924
+ return this.cache.get(secretName)!;
1925
+ }
1926
+
1927
+ const secretPath = join(process.env.HOME!, '.config', 'mdcontext', 'secrets', secretName);
1928
+
1929
+ try {
1930
+ const secret = await readFile(secretPath, 'utf-8');
1931
+ this.cache.set(secretName, secret.trim());
1932
+ return secret.trim();
1933
+ } catch (error) {
1934
+ throw new Error(`Failed to load secret '${secretName}': ${error}`);
1935
+ }
1936
+ }
1937
+
1938
+ /**
1939
+ * Pass secret to CLI via stdin instead of env
1940
+ */
1941
+ static async executeWithStdinSecret(
1942
+ command: string,
1943
+ args: string[],
1944
+ secretName: string
1945
+ ): Promise<CLIResult> {
1946
+ const secret = await this.loadSecret(secretName);
1947
+
1948
+ return new Promise((resolve, reject) => {
1949
+ const child = spawn(command, args, {
1950
+ stdio: ['pipe', 'pipe', 'pipe'],
1951
+ shell: false,
1952
+ });
1953
+
1954
+ // Write secret to stdin
1955
+ child.stdin?.write(secret);
1956
+ child.stdin?.end();
1957
+
1958
+ let stdout = '';
1959
+ let stderr = '';
1960
+
1961
+ child.stdout?.on('data', (chunk) => {
1962
+ stdout += chunk.toString('utf8');
1963
+ });
1964
+
1965
+ child.stderr?.on('data', (chunk) => {
1966
+ stderr += chunk.toString('utf8');
1967
+ });
1968
+
1969
+ child.on('close', (exitCode, signal) => {
1970
+ resolve({ stdout, stderr, exitCode, signal });
1971
+ });
1972
+
1973
+ child.on('error', reject);
1974
+ });
1975
+ }
1976
+
1977
+ /**
1978
+ * Clear secret cache (for security)
1979
+ */
1980
+ static clearCache(): void {
1981
+ this.cache.clear();
1982
+ }
1983
+ }
1984
+
1985
+ // Example usage
1986
+ async function authenticatedCLICall() {
1987
+ // Secret is read from file, not env var
1988
+ return SecretManager.executeWithStdinSecret(
1989
+ 'some-cli',
1990
+ ['authenticate', '--stdin'],
1991
+ 'api_token'
1992
+ );
1993
+ }
1994
+ ```
1995
+
1996
+ ---
1997
+
1998
+ ## Testing Strategies
1999
+
2000
+ ### Mocking Child Processes with Jest
2001
+
2002
+ ```typescript
2003
+ // __tests__/cli-integration.test.ts
2004
+ import { EventEmitter } from 'events';
2005
+ import type { ChildProcess } from 'child_process';
2006
+
2007
+ /**
2008
+ * Create a mock child process for testing
2009
+ */
2010
+ function createMockChildProcess(): ChildProcess {
2011
+ const mockProcess = new EventEmitter() as ChildProcess;
2012
+
2013
+ // Mock stdio streams
2014
+ mockProcess.stdout = new EventEmitter() as any;
2015
+ mockProcess.stderr = new EventEmitter() as any;
2016
+ mockProcess.stdin = new EventEmitter() as any;
2017
+ mockProcess.stdin.write = jest.fn();
2018
+ mockProcess.stdin.end = jest.fn();
2019
+
2020
+ // Mock methods
2021
+ mockProcess.kill = jest.fn().mockReturnValue(true);
2022
+ mockProcess.pid = 12345;
2023
+ mockProcess.exitCode = null;
2024
+ mockProcess.signalCode = null;
2025
+
2026
+ return mockProcess;
2027
+ }
2028
+
2029
+ /**
2030
+ * Test suite for CLI integration
2031
+ */
2032
+ describe('CLI Integration', () => {
2033
+ let mockSpawn: jest.Mock;
2034
+ let mockChildProcess: ChildProcess;
2035
+
2036
+ beforeEach(() => {
2037
+ // Reset mocks
2038
+ jest.clearAllMocks();
2039
+
2040
+ // Create mock child process
2041
+ mockChildProcess = createMockChildProcess();
2042
+
2043
+ // Mock spawn function
2044
+ mockSpawn = jest.fn().mockReturnValue(mockChildProcess);
2045
+
2046
+ // Replace child_process.spawn with mock
2047
+ jest.mock('child_process', () => ({
2048
+ spawn: mockSpawn,
2049
+ }));
2050
+ });
2051
+
2052
+ afterEach(() => {
2053
+ jest.restoreAllMocks();
2054
+ });
2055
+
2056
+ test('should execute command successfully', async () => {
2057
+ // Arrange
2058
+ const expectedOutput = 'Hello, World!';
2059
+
2060
+ // Act
2061
+ const resultPromise = spawnCLI('echo', ['Hello, World!']);
2062
+
2063
+ // Simulate process execution
2064
+ setImmediate(() => {
2065
+ mockChildProcess.stdout?.emit('data', Buffer.from(expectedOutput));
2066
+ mockChildProcess.emit('close', 0, null);
2067
+ });
2068
+
2069
+ const result = await resultPromise;
2070
+
2071
+ // Assert
2072
+ expect(mockSpawn).toHaveBeenCalledWith('echo', ['Hello, World!'], expect.any(Object));
2073
+ expect(result.stdout).toBe(expectedOutput);
2074
+ expect(result.exitCode).toBe(0);
2075
+ });
2076
+
2077
+ test('should handle timeout', async () => {
2078
+ // Arrange
2079
+ jest.useFakeTimers();
2080
+
2081
+ // Act
2082
+ const resultPromise = spawnCLI('slow-command', [], { timeout: 1000 });
2083
+
2084
+ // Fast-forward time
2085
+ jest.advanceTimersByTime(1000);
2086
+
2087
+ // Assert
2088
+ await expect(resultPromise).rejects.toThrow('Process timed out');
2089
+ expect(mockChildProcess.kill).toHaveBeenCalledWith('SIGTERM');
2090
+
2091
+ jest.useRealTimers();
2092
+ });
2093
+
2094
+ test('should handle spawn errors', async () => {
2095
+ // Arrange
2096
+ const error = new Error('Command not found');
2097
+ (error as any).code = 'ENOENT';
2098
+
2099
+ // Act
2100
+ const resultPromise = spawnCLI('nonexistent-command', []);
2101
+
2102
+ // Simulate spawn error
2103
+ setImmediate(() => {
2104
+ mockChildProcess.emit('error', error);
2105
+ });
2106
+
2107
+ // Assert
2108
+ await expect(resultPromise).rejects.toThrow('Failed to spawn process');
2109
+ });
2110
+
2111
+ test('should handle non-zero exit codes', async () => {
2112
+ // Arrange
2113
+ const stderr = 'Error: Something went wrong';
2114
+
2115
+ // Act
2116
+ const resultPromise = spawnCLI('failing-command', []);
2117
+
2118
+ // Simulate process failure
2119
+ setImmediate(() => {
2120
+ mockChildProcess.stderr?.emit('data', Buffer.from(stderr));
2121
+ mockChildProcess.emit('close', 1, null);
2122
+ });
2123
+
2124
+ const result = await resultPromise;
2125
+
2126
+ // Assert
2127
+ expect(result.exitCode).toBe(1);
2128
+ expect(result.stderr).toBe(stderr);
2129
+ });
2130
+
2131
+ test('should handle buffer overflow', async () => {
2132
+ // Arrange
2133
+ const largeOutput = 'x'.repeat(11 * 1024 * 1024); // 11MB
2134
+
2135
+ // Act
2136
+ const resultPromise = spawnCLI('large-output-command', [], {
2137
+ maxBuffer: 10 * 1024 * 1024, // 10MB
2138
+ });
2139
+
2140
+ // Simulate large output
2141
+ setImmediate(() => {
2142
+ mockChildProcess.stdout?.emit('data', Buffer.from(largeOutput));
2143
+ });
2144
+
2145
+ // Assert
2146
+ await expect(resultPromise).rejects.toThrow('exceeded maxBuffer');
2147
+ expect(mockChildProcess.kill).toHaveBeenCalled();
2148
+ });
2149
+ });
2150
+ ```
2151
+
2152
+ ### Integration Testing with Real CLI Tools
2153
+
2154
+ ```typescript
2155
+ // __tests__/integration/git-cli.integration.test.ts
2156
+
2157
+ /**
2158
+ * Integration tests with real git CLI
2159
+ * Use a temporary directory for isolation
2160
+ */
2161
+ describe('Git CLI Integration', () => {
2162
+ let tempDir: string;
2163
+
2164
+ beforeEach(async () => {
2165
+ // Create temporary directory
2166
+ const { mkdtemp } = require('fs/promises');
2167
+ const { tmpdir } = require('os');
2168
+ const { join } = require('path');
2169
+
2170
+ tempDir = await mkdtemp(join(tmpdir(), 'git-test-'));
2171
+
2172
+ // Initialize git repo
2173
+ await safeExecute('git', ['init'], { cwd: tempDir });
2174
+ await safeExecute('git', ['config', 'user.name', 'Test User'], { cwd: tempDir });
2175
+ await safeExecute('git', ['config', 'user.email', 'test@example.com'], { cwd: tempDir });
2176
+ });
2177
+
2178
+ afterEach(async () => {
2179
+ // Clean up temporary directory
2180
+ const { rm } = require('fs/promises');
2181
+ await rm(tempDir, { recursive: true, force: true });
2182
+ });
2183
+
2184
+ test('should create commit successfully', async () => {
2185
+ // Arrange
2186
+ const { writeFile } = require('fs/promises');
2187
+ const { join } = require('path');
2188
+
2189
+ const testFile = join(tempDir, 'test.txt');
2190
+ await writeFile(testFile, 'Hello, World!');
2191
+
2192
+ // Act
2193
+ await safeExecute('git', ['add', 'test.txt'], { cwd: tempDir });
2194
+ await safeExecute('git', ['commit', '-m', 'Initial commit'], { cwd: tempDir });
2195
+
2196
+ // Assert
2197
+ const { stdout } = await safeExecute('git', ['log', '--oneline'], { cwd: tempDir });
2198
+ expect(stdout).toContain('Initial commit');
2199
+ });
2200
+
2201
+ test('should handle git errors gracefully', async () => {
2202
+ // Act & Assert
2203
+ await expect(
2204
+ safeExecute('git', ['commit', '-m', 'No changes'], { cwd: tempDir })
2205
+ ).rejects.toThrow();
2206
+ });
2207
+ });
2208
+ ```
2209
+
2210
+ ### Snapshot Testing for CLI Outputs
2211
+
2212
+ ```typescript
2213
+ // __tests__/snapshots/cli-outputs.test.ts
2214
+
2215
+ /**
2216
+ * Snapshot testing for consistent CLI outputs
2217
+ */
2218
+ describe('CLI Output Snapshots', () => {
2219
+ test('gh issue view output matches snapshot', async () => {
2220
+ // Mock the CLI response
2221
+ const mockOutput = {
2222
+ number: 123,
2223
+ title: 'Example Issue',
2224
+ state: 'open',
2225
+ author: 'octocat',
2226
+ };
2227
+
2228
+ jest.spyOn(JSON, 'parse').mockReturnValue(mockOutput);
2229
+
2230
+ // Execute
2231
+ const result = await parseJSONOutput(
2232
+ 'gh',
2233
+ ['issue', 'view', '123', '--json', 'number,title,state,author'],
2234
+ z.any()
2235
+ );
2236
+
2237
+ // Assert against snapshot
2238
+ expect(result).toMatchSnapshot();
2239
+ });
2240
+ });
2241
+ ```
2242
+
2243
+ ### Property-Based Testing
2244
+
2245
+ ```typescript
2246
+ import * as fc from 'fast-check';
2247
+
2248
+ /**
2249
+ * Property-based tests for argument validation
2250
+ */
2251
+ describe('Argument Validation Properties', () => {
2252
+ test('sanitizeFilename should never contain directory traversal', () => {
2253
+ fc.assert(
2254
+ fc.property(fc.string(), (input) => {
2255
+ const sanitized = ArgumentValidator.sanitizeFilename(input);
2256
+
2257
+ // Should not contain ..
2258
+ expect(sanitized).not.toContain('..');
2259
+
2260
+ // Should only contain safe characters
2261
+ expect(sanitized).toMatch(/^[a-zA-Z0-9._-]*$/);
2262
+
2263
+ // Should not exceed 255 characters
2264
+ expect(sanitized.length).toBeLessThanOrEqual(255);
2265
+ })
2266
+ );
2267
+ });
2268
+
2269
+ test('detectShellInjection should catch all metacharacters', () => {
2270
+ fc.assert(
2271
+ fc.property(
2272
+ fc.constantFrom(...SHELL_METACHARACTERS),
2273
+ fc.string(),
2274
+ fc.string(),
2275
+ (metachar, before, after) => {
2276
+ const testString = before + metachar + after;
2277
+ expect(detectShellInjection(testString)).toBe(true);
2278
+ }
2279
+ )
2280
+ );
2281
+ });
2282
+ });
2283
+ ```
2284
+
2285
+ ---
2286
+
2287
+ ## Production Examples
2288
+
2289
+ ### GitHub CLI Integration
2290
+
2291
+ ```typescript
2292
+ /**
2293
+ * Production-ready GitHub CLI wrapper
2294
+ */
2295
+ class GitHubCLI {
2296
+ /**
2297
+ * Check if gh CLI is installed and authenticated
2298
+ */
2299
+ static async checkAvailability(): Promise<boolean> {
2300
+ try {
2301
+ await safeExecute('gh', ['auth', 'status'], { timeout: 5000 });
2302
+ return true;
2303
+ } catch (error) {
2304
+ if (error instanceof CLIError) {
2305
+ if (error.type === CLIErrorType.COMMAND_NOT_FOUND) {
2306
+ throw new Error('GitHub CLI (gh) is not installed. Install from https://cli.github.com');
2307
+ }
2308
+ if (error.type === CLIErrorType.AUTH_REQUIRED) {
2309
+ throw new Error('GitHub CLI is not authenticated. Run: gh auth login');
2310
+ }
2311
+ }
2312
+ return false;
2313
+ }
2314
+ }
2315
+
2316
+ /**
2317
+ * Create an issue
2318
+ */
2319
+ static async createIssue(options: {
2320
+ title: string;
2321
+ body: string;
2322
+ labels?: string[];
2323
+ assignees?: string[];
2324
+ }): Promise<{ number: number; url: string }> {
2325
+ const args = [
2326
+ 'issue',
2327
+ 'create',
2328
+ '--title',
2329
+ options.title,
2330
+ '--body',
2331
+ options.body,
2332
+ '--json',
2333
+ 'number,url',
2334
+ ];
2335
+
2336
+ if (options.labels) {
2337
+ args.push('--label', options.labels.join(','));
2338
+ }
2339
+
2340
+ if (options.assignees) {
2341
+ args.push('--assignee', options.assignees.join(','));
2342
+ }
2343
+
2344
+ const result = await executeWithRetry('gh', args, {}, {
2345
+ maxRetries: 3,
2346
+ shouldRetry: (error) => error.type === CLIErrorType.RATE_LIMITED,
2347
+ });
2348
+
2349
+ const parsed = JSON.parse(result.stdout);
2350
+ return {
2351
+ number: parsed.number,
2352
+ url: parsed.url,
2353
+ };
2354
+ }
2355
+
2356
+ /**
2357
+ * List pull requests
2358
+ */
2359
+ static async listPullRequests(options: {
2360
+ state?: 'open' | 'closed' | 'all';
2361
+ limit?: number;
2362
+ } = {}): Promise<Array<{ number: number; title: string; state: string }>> {
2363
+ const args = [
2364
+ 'pr',
2365
+ 'list',
2366
+ '--json',
2367
+ 'number,title,state',
2368
+ '--limit',
2369
+ (options.limit || 30).toString(),
2370
+ ];
2371
+
2372
+ if (options.state) {
2373
+ args.push('--state', options.state);
2374
+ }
2375
+
2376
+ const result = await executeWithErrorHandling('gh', args);
2377
+ return JSON.parse(result.stdout);
2378
+ }
2379
+ }
2380
+
2381
+ // Example usage
2382
+ async function exampleGitHubWorkflow() {
2383
+ // Check availability
2384
+ await GitHubCLI.checkAvailability();
2385
+
2386
+ // Create issue
2387
+ const issue = await GitHubCLI.createIssue({
2388
+ title: 'Bug: CLI integration failing',
2389
+ body: 'Detailed description of the bug...',
2390
+ labels: ['bug', 'cli'],
2391
+ assignees: ['octocat'],
2392
+ });
2393
+
2394
+ console.log(`Created issue #${issue.number}: ${issue.url}`);
2395
+
2396
+ // List PRs
2397
+ const prs = await GitHubCLI.listPullRequests({ state: 'open', limit: 10 });
2398
+ console.log(`Found ${prs.length} open PRs`);
2399
+ }
2400
+ ```
2401
+
2402
+ ### Git CLI Integration
2403
+
2404
+ ```typescript
2405
+ /**
2406
+ * Production-ready Git CLI wrapper
2407
+ */
2408
+ class GitCLI {
2409
+ constructor(private readonly repoPath: string) {}
2410
+
2411
+ /**
2412
+ * Get current branch name
2413
+ */
2414
+ async getCurrentBranch(): Promise<string> {
2415
+ const result = await safeExecute('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
2416
+ cwd: this.repoPath,
2417
+ });
2418
+ return result.stdout.trim();
2419
+ }
2420
+
2421
+ /**
2422
+ * Get commit history
2423
+ */
2424
+ async getLog(options: { limit?: number; format?: string } = {}): Promise<GitCommit[]> {
2425
+ const args = [
2426
+ 'log',
2427
+ '--pretty=format:%H|%an|%ae|%ad|%s',
2428
+ '--date=iso',
2429
+ ];
2430
+
2431
+ if (options.limit) {
2432
+ args.push('-n', options.limit.toString());
2433
+ }
2434
+
2435
+ const result = await safeExecute('git', args, {
2436
+ cwd: this.repoPath,
2437
+ });
2438
+
2439
+ return result.stdout.split('\n').map((line) => {
2440
+ const [hash, authorName, authorEmail, date, message] = line.split('|');
2441
+ return { hash, authorName, authorEmail, date, message };
2442
+ });
2443
+ }
2444
+
2445
+ /**
2446
+ * Create commit
2447
+ */
2448
+ async commit(message: string): Promise<string> {
2449
+ const result = await safeExecute('git', ['commit', '-m', message], {
2450
+ cwd: this.repoPath,
2451
+ });
2452
+
2453
+ // Extract commit hash from output
2454
+ const match = result.stdout.match(/\[.+ ([a-f0-9]+)\]/);
2455
+ return match ? match[1] : '';
2456
+ }
2457
+
2458
+ /**
2459
+ * Check if repo is clean
2460
+ */
2461
+ async isClean(): Promise<boolean> {
2462
+ const result = await safeExecute('git', ['status', '--porcelain'], {
2463
+ cwd: this.repoPath,
2464
+ });
2465
+ return result.stdout.trim() === '';
2466
+ }
2467
+ }
2468
+ ```
2469
+
2470
+ ### Claude CLI Integration (Example)
2471
+
2472
+ ```typescript
2473
+ /**
2474
+ * Example Claude CLI wrapper
2475
+ * (Adjust based on actual Claude CLI interface)
2476
+ */
2477
+ class ClaudeCLI {
2478
+ /**
2479
+ * Generate text completion
2480
+ */
2481
+ static async generateCompletion(options: {
2482
+ prompt: string;
2483
+ model?: string;
2484
+ maxTokens?: number;
2485
+ temperature?: number;
2486
+ }): Promise<{ content: string; tokens: number }> {
2487
+ const args = ['generate', '--prompt', options.prompt];
2488
+
2489
+ if (options.model) {
2490
+ args.push('--model', options.model);
2491
+ }
2492
+
2493
+ if (options.maxTokens) {
2494
+ args.push('--max-tokens', options.maxTokens.toString());
2495
+ }
2496
+
2497
+ if (options.temperature) {
2498
+ args.push('--temperature', options.temperature.toString());
2499
+ }
2500
+
2501
+ // Add JSON output flag
2502
+ args.push('--output', 'json');
2503
+
2504
+ // Use longer timeout for AI generation
2505
+ const timeout = getAdaptiveTimeout({
2506
+ type: 'generate',
2507
+ estimatedTokens: options.maxTokens || 1000,
2508
+ complexity: 'high',
2509
+ });
2510
+
2511
+ const result = await executeWithRetry(
2512
+ 'claude',
2513
+ args,
2514
+ { timeout },
2515
+ {
2516
+ maxRetries: 2,
2517
+ shouldRetry: (error) =>
2518
+ error.type === CLIErrorType.RATE_LIMITED ||
2519
+ error.type === CLIErrorType.NETWORK_ERROR,
2520
+ }
2521
+ );
2522
+
2523
+ const parsed = JSON.parse(result.stdout);
2524
+ return {
2525
+ content: parsed.content,
2526
+ tokens: parsed.usage.total_tokens,
2527
+ };
2528
+ }
2529
+
2530
+ /**
2531
+ * Stream generation with real-time output
2532
+ */
2533
+ static async streamCompletion(
2534
+ prompt: string,
2535
+ onChunk: (chunk: string) => void
2536
+ ): Promise<void> {
2537
+ await spawnWithStreaming(
2538
+ 'claude',
2539
+ ['generate', '--stream', '--prompt', prompt],
2540
+ {
2541
+ onStdout: onChunk,
2542
+ onStderr: (error) => {
2543
+ console.error('Claude error:', error);
2544
+ },
2545
+ }
2546
+ );
2547
+ }
2548
+ }
2549
+ ```
2550
+
2551
+ ---
2552
+
2553
+ ## Common Pitfalls
2554
+
2555
+ ### 1. Shell Injection (CRITICAL)
2556
+
2557
+ ```typescript
2558
+ // ❌ DANGEROUS - Never do this!
2559
+ async function dangerousExample(userInput: string) {
2560
+ const { exec } = require('child_process');
2561
+
2562
+ // User could input: "; rm -rf /"
2563
+ exec(`echo ${userInput}`);
2564
+ }
2565
+
2566
+ // ✅ SAFE - Use spawn with argument array
2567
+ async function safeExample(userInput: string) {
2568
+ await spawnCLI('echo', [userInput]);
2569
+ }
2570
+ ```
2571
+
2572
+ ### 2. Buffer Overflow
2573
+
2574
+ ```typescript
2575
+ // ❌ WRONG - Default maxBuffer (1MB) may be too small
2576
+ async function bufferOverflow() {
2577
+ await spawnCLI('cat', ['large-file.txt']); // May crash!
2578
+ }
2579
+
2580
+ // ✅ CORRECT - Use streaming or increase buffer
2581
+ async function handleLargeFile() {
2582
+ // Option 1: Stream the output
2583
+ await handleLargeOutput('cat', ['large-file.txt'], (chunk) => {
2584
+ console.log(chunk);
2585
+ });
2586
+
2587
+ // Option 2: Increase buffer
2588
+ await spawnCLI('cat', ['large-file.txt'], {
2589
+ maxBuffer: 100 * 1024 * 1024, // 100MB
2590
+ });
2591
+ }
2592
+ ```
2593
+
2594
+ ### 3. Hanging Processes
2595
+
2596
+ ```typescript
2597
+ // ❌ WRONG - No timeout, process may hang forever
2598
+ async function hangingProcess() {
2599
+ await spawnCLI('some-unreliable-command', []);
2600
+ }
2601
+
2602
+ // ✅ CORRECT - Always set timeout
2603
+ async function timeoutProtected() {
2604
+ await spawnCLI('some-unreliable-command', [], {
2605
+ timeout: 30000, // 30 seconds
2606
+ });
2607
+ }
2608
+ ```
2609
+
2610
+ ### 4. Environment Variable Leakage
2611
+
2612
+ ```typescript
2613
+ // ❌ DANGEROUS - Exposes all env vars to child process
2614
+ async function envLeakage() {
2615
+ await spawnCLI('untrusted-command', [], {
2616
+ env: process.env, // All secrets exposed!
2617
+ });
2618
+ }
2619
+
2620
+ // ✅ SAFE - Minimal environment
2621
+ async function minimalEnv() {
2622
+ await executeWithManagedEnv('untrusted-command', [], {
2623
+ allowedVars: ['PATH'],
2624
+ });
2625
+ }
2626
+ ```
2627
+
2628
+ ### 5. Race Conditions
2629
+
2630
+ ```typescript
2631
+ // ❌ WRONG - Multiple writes may race
2632
+ async function racyWrites() {
2633
+ const child = spawn('command', []);
2634
+ child.stdin?.write('first\n');
2635
+ child.stdin?.write('second\n'); // May arrive out of order
2636
+ child.stdin?.end();
2637
+ }
2638
+
2639
+ // ✅ CORRECT - Wait for drain events
2640
+ async function sequentialWrites() {
2641
+ const child = spawn('command', []);
2642
+
2643
+ await new Promise<void>((resolve) => {
2644
+ child.stdin?.write('first\n', () => {
2645
+ child.stdin?.write('second\n', () => {
2646
+ child.stdin?.end();
2647
+ resolve();
2648
+ });
2649
+ });
2650
+ });
2651
+ }
2652
+ ```
2653
+
2654
+ ### 6. Platform-Specific Behavior
2655
+
2656
+ ```typescript
2657
+ // ❌ WRONG - Assumes Unix paths
2658
+ async function unixOnly() {
2659
+ await spawnCLI('cat', ['/tmp/file.txt']); // Breaks on Windows
2660
+ }
2661
+
2662
+ // ✅ CORRECT - Use path module for cross-platform paths
2663
+ import { join, resolve } from 'path';
2664
+ import { tmpdir } from 'os';
2665
+
2666
+ async function crossPlatform() {
2667
+ const filePath = join(tmpdir(), 'file.txt');
2668
+ await spawnCLI('cat', [filePath]);
2669
+ }
2670
+ ```
2671
+
2672
+ ### 7. Ignoring stderr
2673
+
2674
+ ```typescript
2675
+ // ❌ WRONG - Only checking stdout
2676
+ async function ignoringErrors() {
2677
+ const result = await spawnCLI('command', []);
2678
+ console.log(result.stdout); // stderr may contain important warnings!
2679
+ }
2680
+
2681
+ // ✅ CORRECT - Check both stdout and stderr
2682
+ async function checkingBoth() {
2683
+ const result = await spawnCLI('command', []);
2684
+
2685
+ if (result.stderr) {
2686
+ console.warn('Warnings/Errors:', result.stderr);
2687
+ }
2688
+
2689
+ console.log('Output:', result.stdout);
2690
+ }
2691
+ ```
2692
+
2693
+ ---
2694
+
2695
+ ## Platform Differences
2696
+
2697
+ ### Windows vs Unix/Linux/macOS
2698
+
2699
+ ```typescript
2700
+ /**
2701
+ * Platform detection and handling
2702
+ */
2703
+ class PlatformAdapter {
2704
+ static readonly isWindows = process.platform === 'win32';
2705
+ static readonly isMac = process.platform === 'darwin';
2706
+ static readonly isLinux = process.platform === 'linux';
2707
+
2708
+ /**
2709
+ * Get shell for current platform
2710
+ */
2711
+ static getShell(): string {
2712
+ if (this.isWindows) {
2713
+ return process.env.COMSPEC || 'cmd.exe';
2714
+ }
2715
+ return process.env.SHELL || '/bin/sh';
2716
+ }
2717
+
2718
+ /**
2719
+ * Execute script with appropriate shell
2720
+ */
2721
+ static async executeScript(
2722
+ scriptContent: string,
2723
+ extension: '.sh' | '.bat' | '.ps1'
2724
+ ): Promise<CLIResult> {
2725
+ if (this.isWindows) {
2726
+ if (extension === '.ps1') {
2727
+ // PowerShell script
2728
+ return spawnCLI('powershell.exe', ['-Command', scriptContent]);
2729
+ } else {
2730
+ // Batch script
2731
+ return spawnCLI('cmd.exe', ['/c', scriptContent]);
2732
+ }
2733
+ } else {
2734
+ // Unix shell script
2735
+ return spawnCLI('/bin/sh', ['-c', scriptContent]);
2736
+ }
2737
+ }
2738
+
2739
+ /**
2740
+ * Convert command for platform
2741
+ */
2742
+ static adaptCommand(command: string): { cmd: string; args: string[] } {
2743
+ if (this.isWindows) {
2744
+ // Windows may need .exe extension
2745
+ const windowsCommands: Record<string, string> = {
2746
+ node: 'node.exe',
2747
+ npm: 'npm.cmd',
2748
+ git: 'git.exe',
2749
+ };
2750
+
2751
+ const adapted = windowsCommands[command] || command;
2752
+ return { cmd: adapted, args: [] };
2753
+ }
2754
+
2755
+ return { cmd: command, args: [] };
2756
+ }
2757
+
2758
+ /**
2759
+ * Get appropriate path separator
2760
+ */
2761
+ static getPathSeparator(): string {
2762
+ return this.isWindows ? ';' : ':';
2763
+ }
2764
+
2765
+ /**
2766
+ * Normalize path for platform
2767
+ */
2768
+ static normalizePath(path: string): string {
2769
+ if (this.isWindows) {
2770
+ return path.replace(/\//g, '\\');
2771
+ }
2772
+ return path.replace(/\\/g, '/');
2773
+ }
2774
+ }
2775
+
2776
+ /**
2777
+ * Cross-platform command execution
2778
+ */
2779
+ async function executeCrossPlatform(
2780
+ command: string,
2781
+ args: string[]
2782
+ ): Promise<CLIResult> {
2783
+ const { cmd } = PlatformAdapter.adaptCommand(command);
2784
+
2785
+ // Use cross-spawn for better Windows compatibility
2786
+ const crossSpawn = require('cross-spawn');
2787
+
2788
+ return new Promise((resolve, reject) => {
2789
+ const child = crossSpawn(cmd, args, {
2790
+ stdio: ['pipe', 'pipe', 'pipe'],
2791
+ });
2792
+
2793
+ let stdout = '';
2794
+ let stderr = '';
2795
+
2796
+ child.stdout?.on('data', (chunk: Buffer) => {
2797
+ stdout += chunk.toString('utf8');
2798
+ });
2799
+
2800
+ child.stderr?.on('data', (chunk: Buffer) => {
2801
+ stderr += chunk.toString('utf8');
2802
+ });
2803
+
2804
+ child.on('close', (exitCode: number, signal: NodeJS.Signals) => {
2805
+ resolve({ stdout, stderr, exitCode, signal });
2806
+ });
2807
+
2808
+ child.on('error', reject);
2809
+ });
2810
+ }
2811
+ ```
2812
+
2813
+ ### Handling Line Endings
2814
+
2815
+ ```typescript
2816
+ /**
2817
+ * Normalize line endings across platforms
2818
+ */
2819
+ function normalizeLineEndings(text: string): string {
2820
+ // Convert all line endings to \n
2821
+ return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
2822
+ }
2823
+
2824
+ /**
2825
+ * Convert to platform-specific line endings
2826
+ */
2827
+ function toPlatformLineEndings(text: string): string {
2828
+ const normalized = normalizeLineEndings(text);
2829
+
2830
+ if (PlatformAdapter.isWindows) {
2831
+ return normalized.replace(/\n/g, '\r\n');
2832
+ }
2833
+
2834
+ return normalized;
2835
+ }
2836
+ ```
2837
+
2838
+ ---
2839
+
2840
+ ## Performance Optimization
2841
+
2842
+ ### Process Pooling for Repeated Calls
2843
+
2844
+ ```typescript
2845
+ /**
2846
+ * Process pool for reusing long-running processes
2847
+ * Useful for CLI tools with high startup cost
2848
+ */
2849
+ class ProcessPool {
2850
+ private processes: ChildProcess[] = [];
2851
+ private queue: Array<{
2852
+ args: string[];
2853
+ resolve: (result: CLIResult) => void;
2854
+ reject: (error: Error) => void;
2855
+ }> = [];
2856
+
2857
+ constructor(
2858
+ private readonly command: string,
2859
+ private readonly poolSize: number = 3
2860
+ ) {}
2861
+
2862
+ /**
2863
+ * Execute command using pooled process
2864
+ */
2865
+ async execute(args: string[]): Promise<CLIResult> {
2866
+ return new Promise((resolve, reject) => {
2867
+ this.queue.push({ args, resolve, reject });
2868
+ this.processQueue();
2869
+ });
2870
+ }
2871
+
2872
+ private processQueue(): void {
2873
+ if (this.queue.length === 0) return;
2874
+ if (this.processes.length >= this.poolSize) return;
2875
+
2876
+ const job = this.queue.shift();
2877
+ if (!job) return;
2878
+
2879
+ const child = spawn(this.command, job.args, {
2880
+ stdio: ['pipe', 'pipe', 'pipe'],
2881
+ shell: false,
2882
+ });
2883
+
2884
+ this.processes.push(child);
2885
+
2886
+ let stdout = '';
2887
+ let stderr = '';
2888
+
2889
+ child.stdout?.on('data', (chunk) => {
2890
+ stdout += chunk.toString('utf8');
2891
+ });
2892
+
2893
+ child.stderr?.on('data', (chunk) => {
2894
+ stderr += chunk.toString('utf8');
2895
+ });
2896
+
2897
+ child.on('close', (exitCode, signal) => {
2898
+ // Remove from pool
2899
+ this.processes = this.processes.filter((p) => p !== child);
2900
+
2901
+ job.resolve({ stdout, stderr, exitCode, signal });
2902
+
2903
+ // Process next job
2904
+ this.processQueue();
2905
+ });
2906
+
2907
+ child.on('error', (error) => {
2908
+ this.processes = this.processes.filter((p) => p !== child);
2909
+ job.reject(error);
2910
+ this.processQueue();
2911
+ });
2912
+ }
2913
+
2914
+ /**
2915
+ * Cleanup all processes
2916
+ */
2917
+ async cleanup(): Promise<void> {
2918
+ for (const process of this.processes) {
2919
+ process.kill('SIGTERM');
2920
+ }
2921
+ this.processes = [];
2922
+ this.queue = [];
2923
+ }
2924
+ }
2925
+
2926
+ // Example usage
2927
+ const gitPool = new ProcessPool('git', 5);
2928
+
2929
+ async function parallelGitOperations() {
2930
+ const results = await Promise.all([
2931
+ gitPool.execute(['status']),
2932
+ gitPool.execute(['log', '-1']),
2933
+ gitPool.execute(['branch', '-a']),
2934
+ ]);
2935
+
2936
+ console.log('All git operations completed:', results);
2937
+ }
2938
+ ```
2939
+
2940
+ ### Caching CLI Results
2941
+
2942
+ ```typescript
2943
+ /**
2944
+ * Cache CLI results to avoid redundant executions
2945
+ */
2946
+ class CLICache {
2947
+ private cache = new Map<string, { result: CLIResult; timestamp: number }>();
2948
+
2949
+ constructor(private readonly ttlMs: number = 60000) {} // 1 minute default
2950
+
2951
+ /**
2952
+ * Get cache key for command
2953
+ */
2954
+ private getCacheKey(command: string, args: string[]): string {
2955
+ return `${command}:${args.join(':')}`;
2956
+ }
2957
+
2958
+ /**
2959
+ * Execute with caching
2960
+ */
2961
+ async execute(
2962
+ command: string,
2963
+ args: string[],
2964
+ options: SpawnOptions = {}
2965
+ ): Promise<CLIResult> {
2966
+ const key = this.getCacheKey(command, args);
2967
+ const cached = this.cache.get(key);
2968
+
2969
+ // Return cached if still valid
2970
+ if (cached && Date.now() - cached.timestamp < this.ttlMs) {
2971
+ console.debug('Cache hit:', key);
2972
+ return cached.result;
2973
+ }
2974
+
2975
+ // Execute and cache
2976
+ console.debug('Cache miss:', key);
2977
+ const result = await spawnCLI(command, args, options);
2978
+
2979
+ this.cache.set(key, {
2980
+ result,
2981
+ timestamp: Date.now(),
2982
+ });
2983
+
2984
+ return result;
2985
+ }
2986
+
2987
+ /**
2988
+ * Clear cache
2989
+ */
2990
+ clear(): void {
2991
+ this.cache.clear();
2992
+ }
2993
+
2994
+ /**
2995
+ * Invalidate specific command
2996
+ */
2997
+ invalidate(command: string, args?: string[]): void {
2998
+ if (args) {
2999
+ const key = this.getCacheKey(command, args);
3000
+ this.cache.delete(key);
3001
+ } else {
3002
+ // Invalidate all for this command
3003
+ for (const key of this.cache.keys()) {
3004
+ if (key.startsWith(`${command}:`)) {
3005
+ this.cache.delete(key);
3006
+ }
3007
+ }
3008
+ }
3009
+ }
3010
+ }
3011
+
3012
+ // Example usage
3013
+ const cache = new CLICache(5 * 60 * 1000); // 5 minute TTL
3014
+
3015
+ async function cachedGitStatus() {
3016
+ // First call executes git
3017
+ const result1 = await cache.execute('git', ['status']);
3018
+
3019
+ // Second call returns cached result
3020
+ const result2 = await cache.execute('git', ['status']);
3021
+
3022
+ // After mutation, invalidate cache
3023
+ await spawnCLI('git', ['add', 'file.txt']);
3024
+ cache.invalidate('git', ['status']);
3025
+ }
3026
+ ```
3027
+
3028
+ ---
3029
+
3030
+ ## Best Practices Summary
3031
+
3032
+ ### Security Checklist
3033
+
3034
+ - ✅ **ALWAYS** use `spawn()` or `execFile()` with argument arrays
3035
+ - ✅ **NEVER** use `exec()` with user input
3036
+ - ✅ **ALWAYS** validate and sanitize user input
3037
+ - ✅ **NEVER** trust user input in command construction
3038
+ - ✅ **ALWAYS** use minimal environment variables
3039
+ - ✅ **NEVER** pass sensitive env vars to untrusted processes
3040
+ - ✅ **ALWAYS** validate file paths against directory traversal
3041
+ - ✅ **NEVER** concatenate user input into commands
3042
+
3043
+ ### Reliability Checklist
3044
+
3045
+ - ✅ **ALWAYS** set timeouts for CLI operations
3046
+ - ✅ **ALWAYS** handle buffer overflow scenarios
3047
+ - ✅ **ALWAYS** implement retry logic for transient errors
3048
+ - ✅ **ALWAYS** classify errors (auth, network, etc.)
3049
+ - ✅ **ALWAYS** check exit codes and stderr
3050
+ - ✅ **ALWAYS** clean up child processes on exit
3051
+ - ✅ **ALWAYS** test cross-platform compatibility
3052
+
3053
+ ### Performance Checklist
3054
+
3055
+ - ✅ Use streaming for large outputs
3056
+ - ✅ Implement caching for repeated operations
3057
+ - ✅ Use process pooling for frequent calls
3058
+ - ✅ Set appropriate buffer sizes
3059
+ - ✅ Use adaptive timeouts based on operation type
3060
+
3061
+ ---
3062
+
3063
+ ## References & Sources
3064
+
3065
+ ### Node.js Child Process Documentation
3066
+
3067
+ - [Child process | Node.js v25.3.0 Documentation](https://nodejs.org/api/child_process.html)
3068
+ - [How To Launch Child Processes in Node.js | DigitalOcean](https://www.digitalocean.com/community/tutorials/how-to-launch-child-processes-in-node-js)
3069
+ - [Node.js Child Processes: Everything you need to know](https://www.freecodecamp.org/news/node-js-child-processes-everything-you-need-to-know-e69498fe970a/)
3070
+
3071
+ ### CLI Integration Patterns
3072
+
3073
+ - [Building CLI apps with TypeScript in 2026 - DEV Community](https://dev.to/hongminhee/building-cli-apps-with-typescript-in-2026-5c9d)
3074
+ - [CLI patterns cookbook | Optique](https://optique.dev/cookbook)
3075
+ - [Streamlining CLI Input with Async Generators](https://typescript.tv/hands-on/streamlining-cli-input-with-async-generators/)
3076
+
3077
+ ### GitHub CLI Integration
3078
+
3079
+ - [GitHub CLI | Take GitHub to the command line](https://cli.github.com/manual/gh)
3080
+ - [Creating GitHub CLI extensions - GitHub Docs](https://docs.github.com/en/github-cli/github-cli/creating-github-cli-extensions)
3081
+ - [GitHub - cli/cli: GitHub's official command line tool](https://github.com/cli/cli)
3082
+
3083
+ ### Security Best Practices
3084
+
3085
+ - [Preventing Command Injection Attacks in Node.js Apps](https://auth0.com/blog/preventing-command-injection-attacks-in-node-js-apps/)
3086
+ - [Secure JavaScript Coding Practices Against Command Injection Vulnerabilities](https://www.nodejs-security.com/blog/secure-javascript-coding-practices-against-command-injection-vulnerabilities)
3087
+ - [NodeJS Command Injection Guide: Examples and Prevention](https://www.stackhawk.com/blog/nodejs-command-injection-examples-and-prevention/)
3088
+ - [Do not use secrets in environment variables and here's how to do it better](https://www.nodejs-security.com/blog/do-not-use-secrets-in-environment-variables-and-here-is-how-to-do-it-better)
3089
+
3090
+ ### Buffer & Timeout Handling
3091
+
3092
+ - [Handling Large Output in Node.js: Avoiding ERR_CHILD_PROCESS_STDIO_MAXBUFFER](https://runebook.dev/en/articles/node/errors/err_child_process_stdio_maxbuffer)
3093
+ - [maxBuffer default too small · Issue #9829 · nodejs/node](https://github.com/nodejs/node/issues/9829)
3094
+
3095
+ ### Exit Codes
3096
+
3097
+ - [Standard Exit Status Codes in Linux | Baeldung on Linux](https://www.baeldung.com/linux/status-codes)
3098
+ - [Bash command line exit codes demystified](https://www.redhat.com/en/blog/exit-codes-demystified)
3099
+ - [Exit status - Wikipedia](https://en.wikipedia.org/wiki/Exit_status)
3100
+
3101
+ ### Cross-Platform Development
3102
+
3103
+ - [Creating cross-platform shell scripts • Shell scripting with Node.js](https://exploringjs.com/nodejs-shell-scripting/ch_creating-shell-scripts.html)
3104
+ - [GitHub - bcoe/awesome-cross-platform-nodejs](https://github.com/bcoe/awesome-cross-platform-nodejs)
3105
+ - [Writing cross-platform Node.js | George Ornbo](https://shapeshed.com/writing-cross-platform-node/)
3106
+
3107
+ ### Git CLI Integration
3108
+
3109
+ - [GitHub - steveukx/git-js: A light weight interface for running git commands](https://github.com/steveukx/git-js)
3110
+ - [isomorphic-git · A pure JavaScript implementation of git](https://isomorphic-git.org/)
3111
+
3112
+ ### Streaming JSON
3113
+
3114
+ - [Process streaming JSON with Node.js | by Jake Burden | Medium](https://medium.com/@Jekrb/process-streaming-json-with-node-js-d6530cde72e9)
3115
+ - [GitHub - uhop/stream-json](https://github.com/uhop/stream-json)
3116
+ - [GitHub - max-mapper/ndjson](https://github.com/max-mapper/ndjson)
3117
+
3118
+ ### Process Management
3119
+
3120
+ - [Killing process families with node | by Almenon | Medium](https://medium.com/@almenon214/killing-processes-with-node-772ffdd19aad)
3121
+ - [Handling signals/terminating child processes in Node.js](https://colinchjs.github.io/2023-10-10/08-49-38-631116-handling-signalsterminating-child-processes-in-nodejs/)
3122
+ - [Graceful Shutdown in Node.js | Dmitry Trunin](https://dtrunin.github.io//2022/04/05/nodejs-graceful-shutdown.html)
3123
+
3124
+ ### Testing
3125
+
3126
+ - [Unit-testing a child process in a Node.js\\Typescript app | by Tzafrir Ben Ami | Medium](https://unhandledexception.dev/unit-testing-a-child-process-in-a-node-js-typescript-app-b7d89615e8e0)
3127
+ - [GitHub - gotwarlost/mock-spawn: Easy to use mock for child_process.spawn](https://github.com/gotwarlost/mock-spawn)
3128
+ - [Mocking node:child_process.spawn() using Jest + TypeScript · GitHub](https://gist.github.com/manekinekko/0aae4bbfdec4e47883f7c04310c40fa1)
3129
+
3130
+ ### AI Integration
3131
+
3132
+ - [Vercel AI SDK by Vercel](https://ai-sdk.dev/docs/introduction)
3133
+ - [GitHub - ben-vargas/ai-sdk-provider-claude-code](https://github.com/ben-vargas/ai-sdk-provider-claude-code)
3134
+ - [Using Vercel Sandbox to run Claude's Agent SDK](https://vercel.com/kb/guide/using-vercel-sandbox-claude-agent-sdk)
3135
+
3136
+ ---
3137
+
3138
+ ## Conclusion
3139
+
3140
+ This document provides production-ready patterns for integrating with AI CLI tools in TypeScript/Node.js. The key principles are:
3141
+
3142
+ 1. **Security First**: Never trust user input, always use argument arrays
3143
+ 2. **Reliability**: Implement timeouts, retries, and comprehensive error handling
3144
+ 3. **Performance**: Use streaming for large outputs, caching for repeated operations
3145
+ 4. **Cross-Platform**: Test on Windows, macOS, and Linux
3146
+ 5. **Testing**: Mock child processes, use integration tests with real CLI tools
3147
+
3148
+ For the **mdcontext** project, prioritize:
3149
+ - Using `spawn()` with explicit argument arrays for security
3150
+ - Implementing streaming for large codebase processing
3151
+ - Comprehensive error classification for AI CLI interactions
3152
+ - Adaptive timeouts based on operation complexity
3153
+ - Secure environment variable management
3154
+
3155
+ Remember: **Shell injection is the #1 vulnerability in CLI integration. Always use argument arrays, never string concatenation.**