@supatest/cli 0.0.2 → 0.0.3

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 (75) hide show
  1. package/README.md +58 -315
  2. package/dist/agent-runner.js +224 -52
  3. package/dist/commands/login.js +392 -0
  4. package/dist/commands/setup.js +234 -0
  5. package/dist/config.js +29 -0
  6. package/dist/core/agent.js +270 -0
  7. package/dist/index.js +118 -31
  8. package/dist/modes/headless.js +117 -0
  9. package/dist/modes/interactive.js +430 -0
  10. package/dist/presenters/composite.js +32 -0
  11. package/dist/presenters/console.js +163 -0
  12. package/dist/presenters/react.js +220 -0
  13. package/dist/presenters/types.js +1 -0
  14. package/dist/presenters/web.js +78 -0
  15. package/dist/prompts/builder.js +181 -0
  16. package/dist/prompts/fixer.js +148 -0
  17. package/dist/prompts/headless.md +97 -0
  18. package/dist/prompts/index.js +3 -0
  19. package/dist/prompts/interactive.md +43 -0
  20. package/dist/prompts/plan.md +41 -0
  21. package/dist/prompts/planner.js +70 -0
  22. package/dist/prompts/prompts/builder.md +97 -0
  23. package/dist/prompts/prompts/fixer.md +100 -0
  24. package/dist/prompts/prompts/plan.md +41 -0
  25. package/dist/prompts/prompts/planner.md +41 -0
  26. package/dist/services/api-client.js +244 -0
  27. package/dist/services/event-streamer.js +130 -0
  28. package/dist/ui/App.js +322 -0
  29. package/dist/ui/components/AuthBanner.js +20 -0
  30. package/dist/ui/components/AuthDialog.js +32 -0
  31. package/dist/ui/components/Banner.js +12 -0
  32. package/dist/ui/components/ExpandableSection.js +17 -0
  33. package/dist/ui/components/Header.js +49 -0
  34. package/dist/ui/components/HelpMenu.js +89 -0
  35. package/dist/ui/components/InputPrompt.js +292 -0
  36. package/dist/ui/components/MessageList.js +42 -0
  37. package/dist/ui/components/QueuedMessageDisplay.js +31 -0
  38. package/dist/ui/components/Scrollable.js +103 -0
  39. package/dist/ui/components/SessionSelector.js +196 -0
  40. package/dist/ui/components/StatusBar.js +45 -0
  41. package/dist/ui/components/messages/AssistantMessage.js +20 -0
  42. package/dist/ui/components/messages/ErrorMessage.js +26 -0
  43. package/dist/ui/components/messages/LoadingMessage.js +28 -0
  44. package/dist/ui/components/messages/ThinkingMessage.js +17 -0
  45. package/dist/ui/components/messages/TodoMessage.js +44 -0
  46. package/dist/ui/components/messages/ToolMessage.js +218 -0
  47. package/dist/ui/components/messages/UserMessage.js +14 -0
  48. package/dist/ui/contexts/KeypressContext.js +527 -0
  49. package/dist/ui/contexts/MouseContext.js +98 -0
  50. package/dist/ui/contexts/SessionContext.js +131 -0
  51. package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
  52. package/dist/ui/hooks/useBatchedScroll.js +22 -0
  53. package/dist/ui/hooks/useBracketedPaste.js +31 -0
  54. package/dist/ui/hooks/useFocus.js +50 -0
  55. package/dist/ui/hooks/useKeypress.js +26 -0
  56. package/dist/ui/hooks/useModeToggle.js +25 -0
  57. package/dist/ui/types/auth.js +13 -0
  58. package/dist/ui/utils/file-completion.js +56 -0
  59. package/dist/ui/utils/input.js +50 -0
  60. package/dist/ui/utils/markdown.js +376 -0
  61. package/dist/ui/utils/mouse.js +189 -0
  62. package/dist/ui/utils/theme.js +59 -0
  63. package/dist/utils/banner.js +7 -14
  64. package/dist/utils/encryption.js +71 -0
  65. package/dist/utils/events.js +36 -0
  66. package/dist/utils/keychain-storage.js +120 -0
  67. package/dist/utils/logger.js +103 -1
  68. package/dist/utils/node-version.js +1 -3
  69. package/dist/utils/plan-file.js +75 -0
  70. package/dist/utils/project-instructions.js +23 -0
  71. package/dist/utils/rich-logger.js +1 -1
  72. package/dist/utils/stdio.js +80 -0
  73. package/dist/utils/summary.js +1 -5
  74. package/dist/utils/token-storage.js +242 -0
  75. package/package.json +35 -15
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Token encryption utilities using AES-256-GCM with machine-specific key derivation.
3
+ * Inspired by Gemini CLI's file-token-storage implementation.
4
+ */
5
+ import crypto from "node:crypto";
6
+ import { hostname, userInfo } from "node:os";
7
+ const ALGORITHM = "aes-256-gcm";
8
+ const KEY_LENGTH = 32;
9
+ const IV_LENGTH = 16;
10
+ /**
11
+ * Derive an encryption key from machine-specific identifiers.
12
+ * Uses scrypt for key derivation with a salt based on hostname and username.
13
+ * This means tokens encrypted on one machine cannot be decrypted on another.
14
+ */
15
+ function deriveEncryptionKey() {
16
+ const salt = `${hostname()}-${userInfo().username}-supatest-cli`;
17
+ return crypto.scryptSync("supatest-cli-token", salt, KEY_LENGTH);
18
+ }
19
+ // Cache the derived key for performance
20
+ let cachedKey = null;
21
+ function getEncryptionKey() {
22
+ if (!cachedKey) {
23
+ cachedKey = deriveEncryptionKey();
24
+ }
25
+ return cachedKey;
26
+ }
27
+ /**
28
+ * Encrypt plaintext using AES-256-GCM with a machine-derived key.
29
+ * Returns a colon-separated string: iv:authTag:ciphertext (all hex encoded)
30
+ */
31
+ export function encrypt(plaintext) {
32
+ const key = getEncryptionKey();
33
+ const iv = crypto.randomBytes(IV_LENGTH);
34
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
35
+ let encrypted = cipher.update(plaintext, "utf8", "hex");
36
+ encrypted += cipher.final("hex");
37
+ const authTag = cipher.getAuthTag();
38
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
39
+ }
40
+ /**
41
+ * Decrypt ciphertext that was encrypted with the encrypt() function.
42
+ * Expects format: iv:authTag:ciphertext (all hex encoded)
43
+ */
44
+ export function decrypt(encryptedData) {
45
+ const parts = encryptedData.split(":");
46
+ if (parts.length !== 3) {
47
+ throw new Error("Invalid encrypted data format");
48
+ }
49
+ const [ivHex, authTagHex, encrypted] = parts;
50
+ const iv = Buffer.from(ivHex, "hex");
51
+ const authTag = Buffer.from(authTagHex, "hex");
52
+ const key = getEncryptionKey();
53
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
54
+ decipher.setAuthTag(authTag);
55
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
56
+ decrypted += decipher.final("utf8");
57
+ return decrypted;
58
+ }
59
+ /**
60
+ * Check if a string appears to be in the encrypted format.
61
+ * Encrypted format has 3 hex segments separated by colons.
62
+ */
63
+ export function isEncrypted(data) {
64
+ const parts = data.split(":");
65
+ if (parts.length !== 3) {
66
+ return false;
67
+ }
68
+ // Check each part looks like valid hex
69
+ const hexRegex = /^[0-9a-fA-F]+$/;
70
+ return parts.every((p) => p.length > 0 && hexRegex.test(p));
71
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Simple event emitter for app-level events
3
+ */
4
+ export var AppEvent;
5
+ (function (AppEvent) {
6
+ AppEvent["PasteTimeout"] = "paste-timeout";
7
+ AppEvent["OpenDebugConsole"] = "open-debug-console";
8
+ AppEvent["SelectionWarning"] = "selection-warning";
9
+ })(AppEvent || (AppEvent = {}));
10
+ class EventEmitter {
11
+ events = new Map();
12
+ on(event, callback) {
13
+ if (!this.events.has(event)) {
14
+ this.events.set(event, []);
15
+ }
16
+ this.events.get(event).push(callback);
17
+ }
18
+ emit(event, ...args) {
19
+ const callbacks = this.events.get(event);
20
+ if (callbacks) {
21
+ for (const callback of callbacks) {
22
+ callback(...args);
23
+ }
24
+ }
25
+ }
26
+ off(event, callback) {
27
+ const callbacks = this.events.get(event);
28
+ if (callbacks) {
29
+ const index = callbacks.indexOf(callback);
30
+ if (index > -1) {
31
+ callbacks.splice(index, 1);
32
+ }
33
+ }
34
+ }
35
+ }
36
+ export const appEvents = new EventEmitter();
@@ -0,0 +1,120 @@
1
+ /**
2
+ * OS Keychain token storage using keytar.
3
+ * Inspired by Gemini CLI's keychain-token-storage implementation.
4
+ */
5
+ import crypto from "node:crypto";
6
+ const SERVICE_NAME = "supatest-cli";
7
+ const ACCOUNT_NAME = "cli-token";
8
+ const KEYCHAIN_TEST_PREFIX = "__keychain_test__";
9
+ let keytarModule = null;
10
+ let keytarLoadAttempted = false;
11
+ let keychainAvailable = null;
12
+ /**
13
+ * Dynamically load the keytar module.
14
+ * Returns null if keytar is not available (not installed or failed to load).
15
+ */
16
+ async function getKeytar() {
17
+ if (keytarLoadAttempted) {
18
+ return keytarModule;
19
+ }
20
+ keytarLoadAttempted = true;
21
+ try {
22
+ const moduleName = "keytar";
23
+ const mod = await import(moduleName);
24
+ keytarModule = mod.default || mod;
25
+ }
26
+ catch {
27
+ // Keytar is optional, so we silently fall back to file storage
28
+ keytarModule = null;
29
+ }
30
+ return keytarModule;
31
+ }
32
+ /**
33
+ * Check if OS keychain is available via a set/get/delete test cycle.
34
+ * Respects SUPATEST_FORCE_FILE_STORAGE environment variable.
35
+ */
36
+ export async function isKeychainAvailable() {
37
+ if (keychainAvailable !== null) {
38
+ return keychainAvailable;
39
+ }
40
+ if (process.env.SUPATEST_FORCE_FILE_STORAGE === "true") {
41
+ keychainAvailable = false;
42
+ return false;
43
+ }
44
+ try {
45
+ const keytar = await getKeytar();
46
+ if (!keytar) {
47
+ keychainAvailable = false;
48
+ return false;
49
+ }
50
+ // Test with a set/get/delete cycle
51
+ const testAccount = `${KEYCHAIN_TEST_PREFIX}${crypto.randomBytes(8).toString("hex")}`;
52
+ const testPassword = "test";
53
+ await keytar.setPassword(SERVICE_NAME, testAccount, testPassword);
54
+ const retrieved = await keytar.getPassword(SERVICE_NAME, testAccount);
55
+ const deleted = await keytar.deletePassword(SERVICE_NAME, testAccount);
56
+ keychainAvailable = deleted && retrieved === testPassword;
57
+ }
58
+ catch {
59
+ keychainAvailable = false;
60
+ }
61
+ return keychainAvailable;
62
+ }
63
+ /**
64
+ * Save token payload to OS keychain.
65
+ * Returns true if successful, false otherwise.
66
+ */
67
+ export async function saveToKeychain(token, expiresAt) {
68
+ const keytar = await getKeytar();
69
+ if (!keytar) {
70
+ return false;
71
+ }
72
+ const payload = {
73
+ token,
74
+ expiresAt,
75
+ createdAt: new Date().toISOString(),
76
+ };
77
+ try {
78
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(payload));
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ /**
86
+ * Load token payload from OS keychain.
87
+ * Returns null if not found or keychain unavailable.
88
+ */
89
+ export async function loadFromKeychain() {
90
+ const keytar = await getKeytar();
91
+ if (!keytar) {
92
+ return null;
93
+ }
94
+ try {
95
+ const data = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
96
+ if (!data) {
97
+ return null;
98
+ }
99
+ return JSON.parse(data);
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ /**
106
+ * Remove token from OS keychain.
107
+ * Returns true if successfully removed, false otherwise.
108
+ */
109
+ export async function removeFromKeychain() {
110
+ const keytar = await getKeytar();
111
+ if (!keytar) {
112
+ return false;
113
+ }
114
+ try {
115
+ return await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
@@ -1,63 +1,161 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
1
3
  import chalk from "chalk";
2
4
  class Logger {
3
5
  verbose = false;
6
+ silent = false;
7
+ logFile = null;
8
+ isDev = false;
4
9
  setVerbose(enabled) {
5
10
  this.verbose = enabled;
6
11
  }
12
+ setSilent(enabled) {
13
+ this.silent = enabled;
14
+ }
15
+ isSilent() {
16
+ return this.silent;
17
+ }
18
+ /**
19
+ * Enable file logging (dev mode only)
20
+ */
21
+ enableFileLogging(isDev = false) {
22
+ this.isDev = isDev;
23
+ if (!isDev)
24
+ return;
25
+ // Write directly to cli.log in current directory
26
+ this.logFile = path.join(process.cwd(), "cli.log");
27
+ // Add session separator to log file
28
+ const separator = `\n${"=".repeat(80)}\n[${new Date().toISOString()}] New CLI session started\n${"=".repeat(80)}\n`;
29
+ try {
30
+ fs.appendFileSync(this.logFile, separator);
31
+ }
32
+ catch (error) {
33
+ // Silently fail
34
+ }
35
+ }
36
+ /**
37
+ * Write to log file (dev mode only)
38
+ */
39
+ writeToFile(level, message, data) {
40
+ if (!this.isDev || !this.logFile)
41
+ return;
42
+ const timestamp = new Date().toISOString();
43
+ const logEntry = data
44
+ ? `[${timestamp}] [${level}] ${message} ${JSON.stringify(data, null, 2)}\n`
45
+ : `[${timestamp}] [${level}] ${message}\n`;
46
+ try {
47
+ fs.appendFileSync(this.logFile, logEntry);
48
+ }
49
+ catch (error) {
50
+ // Silently fail - don't disrupt CLI operation
51
+ }
52
+ }
53
+ /**
54
+ * Check if an error message is critical and should bypass silent mode
55
+ * Critical errors are those that prevent the CLI from starting or executing
56
+ */
57
+ isCriticalError(message) {
58
+ const criticalPatterns = [
59
+ /api key/i,
60
+ /authentication/i,
61
+ /node.*version/i,
62
+ /missing.*required/i,
63
+ /failed to install/i,
64
+ /fatal/i,
65
+ ];
66
+ return criticalPatterns.some((pattern) => pattern.test(message));
67
+ }
7
68
  info(message) {
69
+ if (this.silent)
70
+ return;
8
71
  console.log(chalk.blue("ℹ"), message);
9
72
  }
10
73
  success(message) {
74
+ if (this.silent)
75
+ return;
11
76
  console.log(chalk.green("✓"), message);
12
77
  }
13
78
  error(message) {
79
+ // Allow critical errors through even in silent mode
80
+ if (this.silent && !this.isCriticalError(message))
81
+ return;
14
82
  console.error(chalk.red("✗"), message);
15
83
  }
16
84
  warn(message) {
85
+ if (this.silent)
86
+ return;
17
87
  console.warn(chalk.yellow("⚠"), message);
18
88
  }
19
- debug(message) {
89
+ debug(message, data) {
90
+ this.writeToFile("DEBUG", message, data);
91
+ if (this.silent)
92
+ return;
20
93
  if (this.verbose) {
21
94
  console.log(chalk.gray("→"), message);
95
+ if (data) {
96
+ console.log(chalk.gray(JSON.stringify(data, null, 2)));
97
+ }
22
98
  }
23
99
  }
24
100
  section(title) {
101
+ if (this.silent)
102
+ return;
25
103
  console.log("\n" + chalk.bold.red(`━━━ ${title} ━━━`));
26
104
  }
27
105
  summary(title) {
106
+ if (this.silent)
107
+ return;
28
108
  console.log("\n" + chalk.bold.cyan(`╔═══ ${title} ═══╗`));
29
109
  }
30
110
  raw(message) {
111
+ if (this.silent)
112
+ return;
31
113
  console.log(message);
32
114
  }
33
115
  stream(chunk) {
116
+ if (this.silent)
117
+ return;
34
118
  process.stdout.write(chalk.dim(chunk));
35
119
  }
36
120
  toolRead(filePath) {
121
+ if (this.silent)
122
+ return;
37
123
  console.log("");
38
124
  console.log(chalk.blue("📖"), chalk.dim("Reading:"), chalk.white(filePath));
39
125
  }
40
126
  toolWrite(filePath) {
127
+ if (this.silent)
128
+ return;
41
129
  console.log("");
42
130
  console.log(chalk.green("✏️"), chalk.dim("Writing:"), chalk.white(filePath));
43
131
  }
44
132
  toolEdit(filePath) {
133
+ if (this.silent)
134
+ return;
45
135
  console.log("");
46
136
  console.log(chalk.yellow("✏️"), chalk.dim("Editing:"), chalk.white(filePath));
47
137
  }
48
138
  toolBash(command) {
139
+ if (this.silent)
140
+ return;
49
141
  console.log("");
50
142
  console.log(chalk.cyan("🔨"), chalk.dim("Running:"), chalk.white(command));
51
143
  }
52
144
  toolSearch(type, pattern) {
145
+ if (this.silent)
146
+ return;
53
147
  console.log("");
54
148
  console.log(chalk.cyan("🔍"), chalk.dim(`Searching ${type}:`), chalk.white(pattern));
55
149
  }
56
150
  toolAgent(agentType) {
151
+ if (this.silent)
152
+ return;
57
153
  console.log("");
58
154
  console.log(chalk.cyan("🤖"), chalk.dim("Launching agent:"), chalk.white(agentType));
59
155
  }
60
156
  todoUpdate(todos) {
157
+ if (this.silent)
158
+ return;
61
159
  const completed = todos.filter((t) => t.status === "completed");
62
160
  const inProgress = todos.filter((t) => t.status === "in_progress");
63
161
  const pending = todos.filter((t) => t.status === "pending");
@@ -88,9 +186,13 @@ class Logger {
88
186
  }
89
187
  }
90
188
  divider() {
189
+ if (this.silent)
190
+ return;
91
191
  console.log(chalk.gray("─".repeat(60)));
92
192
  }
93
193
  box(title) {
194
+ if (this.silent)
195
+ return;
94
196
  const width = 60;
95
197
  const padding = Math.max(0, width - title.length - 2);
96
198
  const leftPad = Math.floor(padding / 2);
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "node:child_process";
2
- import { logger } from "./logger.js";
2
+ import { logger } from "./logger";
3
3
  const MINIMUM_NODE_VERSION = 18;
4
4
  /**
5
5
  * Parse a version string like "v18.17.0" or "18.17.0" into components
@@ -76,8 +76,6 @@ export function checkNodeVersion() {
76
76
  process.exit(1);
77
77
  }
78
78
  // Success - version is adequate
79
- // Silent unless verbose mode is enabled
80
- logger.debug(`✓ Node.js ${nodeVersion.raw} detected (meets minimum requirement of ${MINIMUM_NODE_VERSION}.0.0)`);
81
79
  }
82
80
  /**
83
81
  * Get Node.js version info for display purposes
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Plan file utilities
3
+ * Handles creation and management of plan files for plan mode
4
+ */
5
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ const PLAN_DIR = ".supatest/plans";
8
+ /**
9
+ * Generate a unique plan file name with timestamp
10
+ */
11
+ function generatePlanFileName() {
12
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
13
+ return `plan-${timestamp}.md`;
14
+ }
15
+ /**
16
+ * Create a new plan file in the .supatest/plans directory
17
+ * @param cwd - The current working directory (project root)
18
+ * @returns The path to the created plan file
19
+ */
20
+ export async function createPlanFile(cwd) {
21
+ const planDir = join(cwd, PLAN_DIR);
22
+ // Ensure the plan directory exists
23
+ await mkdir(planDir, { recursive: true });
24
+ const planFileName = generatePlanFileName();
25
+ const planPath = join(planDir, planFileName);
26
+ // Create the initial plan file with a placeholder
27
+ const initialContent = `# Plan
28
+
29
+ _Planning in progress..._
30
+
31
+ ---
32
+
33
+ ## Summary
34
+
35
+ _To be filled by the agent_
36
+
37
+ ## Tasks
38
+
39
+ _To be filled by the agent_
40
+
41
+ ## Files to Modify
42
+
43
+ _To be filled by the agent_
44
+
45
+ ---
46
+
47
+ _Generated by Supatest AI_
48
+ `;
49
+ await writeFile(planPath, initialContent, "utf-8");
50
+ return planPath;
51
+ }
52
+ /**
53
+ * Update the contents of an existing plan file
54
+ * @param planPath - The path to the plan file
55
+ * @param content - The new content to write
56
+ */
57
+ export async function updatePlanFile(planPath, content) {
58
+ await writeFile(planPath, content, "utf-8");
59
+ }
60
+ /**
61
+ * Read the contents of a plan file
62
+ * @param planPath - The path to the plan file
63
+ * @returns The contents of the plan file
64
+ */
65
+ export async function readPlanFile(planPath) {
66
+ return await readFile(planPath, "utf-8");
67
+ }
68
+ /**
69
+ * Get the plan directory path for a given project
70
+ * @param cwd - The current working directory (project root)
71
+ * @returns The path to the plan directory
72
+ */
73
+ export function getPlanDirectory(cwd) {
74
+ return join(cwd, PLAN_DIR);
75
+ }
@@ -0,0 +1,23 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Load project-specific instructions from SUPATEST.md
5
+ * Checks multiple locations in order of precedence
6
+ */
7
+ export function loadProjectInstructions(cwd) {
8
+ const paths = [
9
+ join(cwd, "SUPATEST.md"),
10
+ join(cwd, ".supatest", "SUPATEST.md"),
11
+ ];
12
+ for (const path of paths) {
13
+ if (existsSync(path)) {
14
+ try {
15
+ return readFileSync(path, "utf-8");
16
+ }
17
+ catch {
18
+ // Skip if can't read
19
+ }
20
+ }
21
+ }
22
+ return undefined;
23
+ }
@@ -1,5 +1,5 @@
1
- import chalk from "chalk";
2
1
  import boxen from "boxen";
2
+ import chalk from "chalk";
3
3
  import { highlight } from "cli-highlight";
4
4
  /**
5
5
  * Rich logger optimized for CI/CD environments
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Stdio utilities for writing to stdout/stderr
3
+ * Based on Gemini CLI's stdio handling
4
+ */
5
+ // Capture the original stdout and stderr write methods before any monkey patching occurs.
6
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
7
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
8
+ /**
9
+ * Writes to the real stdout, bypassing any monkey patching on process.stdout.write.
10
+ */
11
+ export function writeToStdout(...args) {
12
+ return originalStdoutWrite(...args);
13
+ }
14
+ /**
15
+ * Writes to the real stderr, bypassing any monkey patching on process.stderr.write.
16
+ */
17
+ export function writeToStderr(...args) {
18
+ return originalStderrWrite(...args);
19
+ }
20
+ /**
21
+ * Monkey patches process.stdout.write and process.stderr.write to suppress output.
22
+ * This prevents stray output from libraries (or the app itself) from corrupting the UI.
23
+ * Returns a cleanup function that restores the original write methods.
24
+ */
25
+ export function patchStdio() {
26
+ const previousStdoutWrite = process.stdout.write;
27
+ const previousStderrWrite = process.stderr.write;
28
+ process.stdout.write = (chunk, encodingOrCb, cb) => {
29
+ // Suppress the output (don't write it anywhere)
30
+ const callback = typeof encodingOrCb === 'function' ? encodingOrCb : cb;
31
+ if (callback) {
32
+ callback();
33
+ }
34
+ return true;
35
+ };
36
+ process.stderr.write = (chunk, encodingOrCb, cb) => {
37
+ // Suppress the output (don't write it anywhere)
38
+ const callback = typeof encodingOrCb === 'function' ? encodingOrCb : cb;
39
+ if (callback) {
40
+ callback();
41
+ }
42
+ return true;
43
+ };
44
+ return () => {
45
+ process.stdout.write = previousStdoutWrite;
46
+ process.stderr.write = previousStderrWrite;
47
+ };
48
+ }
49
+ /**
50
+ * Creates proxies for process.stdout and process.stderr that use the real write methods
51
+ * (writeToStdout and writeToStderr) bypassing any monkey patching.
52
+ * This is used by Ink to render to the real output.
53
+ */
54
+ export function createInkStdio() {
55
+ const inkStdout = new Proxy(process.stdout, {
56
+ get(target, prop, receiver) {
57
+ if (prop === 'write') {
58
+ return writeToStdout;
59
+ }
60
+ const value = Reflect.get(target, prop, receiver);
61
+ if (typeof value === 'function') {
62
+ return value.bind(target);
63
+ }
64
+ return value;
65
+ },
66
+ });
67
+ const inkStderr = new Proxy(process.stderr, {
68
+ get(target, prop, receiver) {
69
+ if (prop === 'write') {
70
+ return writeToStderr;
71
+ }
72
+ const value = Reflect.get(target, prop, receiver);
73
+ if (typeof value === 'function') {
74
+ return value.bind(target);
75
+ }
76
+ return value;
77
+ },
78
+ });
79
+ return { stdout: inkStdout, stderr: inkStderr };
80
+ }
@@ -83,12 +83,8 @@ export function generateSummary(stats, result, verbose = false) {
83
83
  const summaryLines = result.summary.split("\n");
84
84
  let lineCount = 0;
85
85
  for (const line of summaryLines) {
86
- if (lineCount >= 10) {
87
- lines.push(chalk.gray("... (truncated)"));
88
- break;
89
- }
86
+ // Removed truncation limit to show full summary
90
87
  lines.push(line);
91
- lineCount++;
92
88
  }
93
89
  }
94
90
  lines.push("");