@supatest/cli 0.0.5 → 0.0.6
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.
- package/dist/index.js +9485 -157
- package/package.json +8 -5
- package/dist/commands/login.js +0 -392
- package/dist/commands/setup.js +0 -234
- package/dist/config.js +0 -29
- package/dist/core/agent.js +0 -259
- package/dist/modes/headless.js +0 -117
- package/dist/modes/interactive.js +0 -418
- package/dist/presenters/composite.js +0 -32
- package/dist/presenters/console.js +0 -163
- package/dist/presenters/react.js +0 -217
- package/dist/presenters/types.js +0 -1
- package/dist/presenters/web.js +0 -78
- package/dist/prompts/builder.js +0 -181
- package/dist/prompts/fixer.js +0 -148
- package/dist/prompts/index.js +0 -3
- package/dist/prompts/planner.js +0 -70
- package/dist/services/api-client.js +0 -244
- package/dist/services/event-streamer.js +0 -130
- package/dist/types.js +0 -1
- package/dist/ui/App.js +0 -322
- package/dist/ui/components/AuthBanner.js +0 -24
- package/dist/ui/components/AuthDialog.js +0 -32
- package/dist/ui/components/Banner.js +0 -12
- package/dist/ui/components/ExpandableSection.js +0 -17
- package/dist/ui/components/Header.js +0 -51
- package/dist/ui/components/HelpMenu.js +0 -89
- package/dist/ui/components/InputPrompt.js +0 -286
- package/dist/ui/components/MessageList.js +0 -42
- package/dist/ui/components/QueuedMessageDisplay.js +0 -31
- package/dist/ui/components/Scrollable.js +0 -103
- package/dist/ui/components/SessionSelector.js +0 -196
- package/dist/ui/components/StatusBar.js +0 -34
- package/dist/ui/components/messages/AssistantMessage.js +0 -20
- package/dist/ui/components/messages/ErrorMessage.js +0 -26
- package/dist/ui/components/messages/LoadingMessage.js +0 -28
- package/dist/ui/components/messages/ThinkingMessage.js +0 -17
- package/dist/ui/components/messages/TodoMessage.js +0 -44
- package/dist/ui/components/messages/ToolMessage.js +0 -218
- package/dist/ui/components/messages/UserMessage.js +0 -14
- package/dist/ui/contexts/KeypressContext.js +0 -527
- package/dist/ui/contexts/MouseContext.js +0 -98
- package/dist/ui/contexts/SessionContext.js +0 -129
- package/dist/ui/hooks/useAnimatedScrollbar.js +0 -83
- package/dist/ui/hooks/useBatchedScroll.js +0 -22
- package/dist/ui/hooks/useBracketedPaste.js +0 -31
- package/dist/ui/hooks/useFocus.js +0 -50
- package/dist/ui/hooks/useKeypress.js +0 -26
- package/dist/ui/hooks/useModeToggle.js +0 -25
- package/dist/ui/types/auth.js +0 -13
- package/dist/ui/utils/file-completion.js +0 -56
- package/dist/ui/utils/input.js +0 -50
- package/dist/ui/utils/markdown.js +0 -376
- package/dist/ui/utils/mouse.js +0 -189
- package/dist/ui/utils/theme.js +0 -59
- package/dist/utils/banner.js +0 -9
- package/dist/utils/encryption.js +0 -71
- package/dist/utils/events.js +0 -36
- package/dist/utils/keychain-storage.js +0 -120
- package/dist/utils/logger.js +0 -209
- package/dist/utils/node-version.js +0 -89
- package/dist/utils/plan-file.js +0 -75
- package/dist/utils/project-instructions.js +0 -23
- package/dist/utils/rich-logger.js +0 -208
- package/dist/utils/stdin.js +0 -25
- package/dist/utils/stdio.js +0 -80
- package/dist/utils/summary.js +0 -94
- package/dist/utils/token-storage.js +0 -242
- package/dist/version.js +0 -6
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import boxen from "boxen";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import { highlight } from "cli-highlight";
|
|
4
|
-
/**
|
|
5
|
-
* Rich logger optimized for CI/CD environments
|
|
6
|
-
* Provides structured, timestamped, grep-able output
|
|
7
|
-
*/
|
|
8
|
-
export class RichLogger {
|
|
9
|
-
startTime = Date.now();
|
|
10
|
-
verbose = false;
|
|
11
|
-
setVerbose(enabled) {
|
|
12
|
-
this.verbose = enabled;
|
|
13
|
-
}
|
|
14
|
-
timestamp() {
|
|
15
|
-
const now = new Date();
|
|
16
|
-
return chalk.gray(`[${now.toISOString()}]`);
|
|
17
|
-
}
|
|
18
|
-
elapsed() {
|
|
19
|
-
const ms = Date.now() - this.startTime;
|
|
20
|
-
const seconds = (ms / 1000).toFixed(1);
|
|
21
|
-
return chalk.dim(`(+${seconds}s)`);
|
|
22
|
-
}
|
|
23
|
-
/** Print major section header */
|
|
24
|
-
section(title) {
|
|
25
|
-
console.log("");
|
|
26
|
-
console.log(chalk.bold.cyan("═".repeat(60)));
|
|
27
|
-
console.log(chalk.bold.cyan(` ${title}`));
|
|
28
|
-
console.log(chalk.bold.cyan("═".repeat(60)));
|
|
29
|
-
}
|
|
30
|
-
/** Print subsection divider */
|
|
31
|
-
subsection(title) {
|
|
32
|
-
console.log("");
|
|
33
|
-
console.log(chalk.cyan("─".repeat(60)));
|
|
34
|
-
console.log(chalk.cyan(` ${title}`));
|
|
35
|
-
console.log(chalk.cyan("─".repeat(60)));
|
|
36
|
-
}
|
|
37
|
-
/** Agent task started */
|
|
38
|
-
taskStart(task, model, maxIterations) {
|
|
39
|
-
this.startTime = Date.now();
|
|
40
|
-
this.section("Supatest AI Agent - Task Started");
|
|
41
|
-
console.log(`${this.timestamp()} ${chalk.bold("Task:")} ${task}`);
|
|
42
|
-
if (model) {
|
|
43
|
-
console.log(`${this.timestamp()} ${chalk.bold("Model:")} ${model}`);
|
|
44
|
-
}
|
|
45
|
-
if (maxIterations) {
|
|
46
|
-
console.log(`${this.timestamp()} ${chalk.bold("Max Iterations:")} ${maxIterations}`);
|
|
47
|
-
}
|
|
48
|
-
console.log(`${this.timestamp()} ${chalk.bold("Started:")} ${new Date().toISOString()}`);
|
|
49
|
-
}
|
|
50
|
-
/** Start of iteration */
|
|
51
|
-
iteration(current, total) {
|
|
52
|
-
this.subsection(`Iteration ${current}/${total}`);
|
|
53
|
-
}
|
|
54
|
-
/** Agent thinking/response text */
|
|
55
|
-
agentText(text) {
|
|
56
|
-
console.log(`${this.timestamp()} ${chalk.blue("🤖 Agent:")} ${text}`);
|
|
57
|
-
}
|
|
58
|
-
/** Tool call started */
|
|
59
|
-
toolCall(toolName, input, verbose = false) {
|
|
60
|
-
console.log("");
|
|
61
|
-
console.log(`${this.timestamp()} ${chalk.yellow("🔧 Tool Call:")} ${chalk.bold(toolName)}`);
|
|
62
|
-
if (verbose || this.verbose) {
|
|
63
|
-
// Show formatted input
|
|
64
|
-
const inputStr = JSON.stringify(input, null, 2);
|
|
65
|
-
console.log(chalk.dim(boxen(inputStr, {
|
|
66
|
-
padding: 0,
|
|
67
|
-
margin: { left: 11 },
|
|
68
|
-
borderStyle: "round",
|
|
69
|
-
borderColor: "gray",
|
|
70
|
-
title: "Input",
|
|
71
|
-
titleAlignment: "left",
|
|
72
|
-
})));
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
// Show compact input
|
|
76
|
-
for (const [key, value] of Object.entries(input)) {
|
|
77
|
-
console.log(`${" ".repeat(11)} ${chalk.dim(key + ":")} ${String(value).substring(0, 80)}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
/** Tool call completed successfully */
|
|
82
|
-
toolSuccess(toolName, duration) {
|
|
83
|
-
const durationStr = duration ? chalk.dim(` (${duration.toFixed(2)}s)`) : "";
|
|
84
|
-
console.log(`${this.timestamp()} ${chalk.green("✓")} ${chalk.bold(toolName)} completed${durationStr}`);
|
|
85
|
-
}
|
|
86
|
-
/** Tool call failed */
|
|
87
|
-
toolError(toolName, error) {
|
|
88
|
-
console.log(`${this.timestamp()} ${chalk.red("✗")} ${chalk.bold(toolName)} failed`);
|
|
89
|
-
console.log(chalk.red(boxen(error, {
|
|
90
|
-
padding: { left: 1, right: 1 },
|
|
91
|
-
margin: { left: 11 },
|
|
92
|
-
borderStyle: "round",
|
|
93
|
-
borderColor: "red",
|
|
94
|
-
title: "Error",
|
|
95
|
-
})));
|
|
96
|
-
}
|
|
97
|
-
/** Show tool output (file contents, command output, etc) */
|
|
98
|
-
toolOutput(content, language) {
|
|
99
|
-
// Limit output length for readability
|
|
100
|
-
const maxLength = this.verbose ? 5000 : 500;
|
|
101
|
-
const truncated = content.length > maxLength;
|
|
102
|
-
const displayContent = truncated
|
|
103
|
-
? content.substring(0, maxLength) + "\n... (truncated)"
|
|
104
|
-
: content;
|
|
105
|
-
try {
|
|
106
|
-
// Try syntax highlighting
|
|
107
|
-
const highlighted = language && displayContent.length < 2000
|
|
108
|
-
? highlight(displayContent, { language })
|
|
109
|
-
: displayContent;
|
|
110
|
-
console.log(boxen(highlighted, {
|
|
111
|
-
padding: { left: 1, right: 1 },
|
|
112
|
-
margin: { left: 11 },
|
|
113
|
-
borderStyle: "round",
|
|
114
|
-
borderColor: "gray",
|
|
115
|
-
title: language ? `Output (${language})` : "Output",
|
|
116
|
-
}));
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// Fallback to plain output if highlighting fails
|
|
120
|
-
console.log(boxen(displayContent, {
|
|
121
|
-
padding: { left: 1, right: 1 },
|
|
122
|
-
margin: { left: 11 },
|
|
123
|
-
borderStyle: "round",
|
|
124
|
-
borderColor: "gray",
|
|
125
|
-
title: "Output",
|
|
126
|
-
}));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
/** Show bash command being executed */
|
|
130
|
-
bashCommand(command) {
|
|
131
|
-
console.log(`${this.timestamp()} ${chalk.yellow("🔧 Running:")} ${chalk.bold(command)}`);
|
|
132
|
-
}
|
|
133
|
-
/** Show bash command output */
|
|
134
|
-
bashOutput(output) {
|
|
135
|
-
if (output.trim()) {
|
|
136
|
-
this.toolOutput(output, "bash");
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/** File being read */
|
|
140
|
-
fileRead(path) {
|
|
141
|
-
console.log(`${this.timestamp()} ${chalk.blue("📖 Reading:")} ${chalk.underline(path)}`);
|
|
142
|
-
}
|
|
143
|
-
/** File being written */
|
|
144
|
-
fileWrite(path) {
|
|
145
|
-
console.log(`${this.timestamp()} ${chalk.green("✏️ Writing:")} ${chalk.underline(path)}`);
|
|
146
|
-
}
|
|
147
|
-
/** General info message */
|
|
148
|
-
info(message) {
|
|
149
|
-
console.log(`${this.timestamp()} ${chalk.blue("ℹ")} ${message}`);
|
|
150
|
-
}
|
|
151
|
-
/** Success message */
|
|
152
|
-
success(message) {
|
|
153
|
-
console.log(`${this.timestamp()} ${chalk.green("✓")} ${message}`);
|
|
154
|
-
}
|
|
155
|
-
/** Error message */
|
|
156
|
-
error(message) {
|
|
157
|
-
console.log(`${this.timestamp()} ${chalk.red("✗")} ${message}`);
|
|
158
|
-
}
|
|
159
|
-
/** Warning message */
|
|
160
|
-
warn(message) {
|
|
161
|
-
console.log(`${this.timestamp()} ${chalk.yellow("⚠")} ${message}`);
|
|
162
|
-
}
|
|
163
|
-
/** Debug message (only shown in verbose mode) */
|
|
164
|
-
debug(message) {
|
|
165
|
-
if (this.verbose) {
|
|
166
|
-
console.log(`${this.timestamp()} ${chalk.gray("→")} ${message}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
/** Final summary */
|
|
170
|
-
summary(data) {
|
|
171
|
-
this.section("Summary");
|
|
172
|
-
const status = data.success
|
|
173
|
-
? chalk.green("✓ Success")
|
|
174
|
-
: chalk.red("✗ Failed");
|
|
175
|
-
console.log(`${this.timestamp()} ${chalk.bold("Status:")} ${status}`);
|
|
176
|
-
console.log(`${this.timestamp()} ${chalk.bold("Duration:")} ${(data.duration / 1000).toFixed(1)}s`);
|
|
177
|
-
console.log(`${this.timestamp()} ${chalk.bold("Iterations:")} ${data.iterations}`);
|
|
178
|
-
if (data.filesModified.length > 0) {
|
|
179
|
-
console.log("");
|
|
180
|
-
console.log(`${this.timestamp()} ${chalk.bold("Files Modified:")}`);
|
|
181
|
-
for (const file of data.filesModified) {
|
|
182
|
-
console.log(`${" ".repeat(11)} ${chalk.cyan("→")} ${file}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (data.commandsRun.length > 0) {
|
|
186
|
-
console.log("");
|
|
187
|
-
console.log(`${this.timestamp()} ${chalk.bold("Commands Executed:")}`);
|
|
188
|
-
for (const cmd of data.commandsRun) {
|
|
189
|
-
console.log(`${" ".repeat(11)} ${chalk.gray("$")} ${cmd}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (data.errors && data.errors.length > 0) {
|
|
193
|
-
console.log("");
|
|
194
|
-
console.log(`${this.timestamp()} ${chalk.bold.red("Errors Encountered:")}`);
|
|
195
|
-
for (const error of data.errors) {
|
|
196
|
-
console.log(`${" ".repeat(11)} ${chalk.red("✗")} ${error}`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
console.log(chalk.cyan("═".repeat(60)));
|
|
200
|
-
console.log("");
|
|
201
|
-
}
|
|
202
|
-
/** Raw output without formatting */
|
|
203
|
-
raw(text) {
|
|
204
|
-
console.log(text);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// Export singleton instance
|
|
208
|
-
export const richLogger = new RichLogger();
|
package/dist/utils/stdin.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export async function readStdin() {
|
|
2
|
-
return new Promise((resolve, reject) => {
|
|
3
|
-
const chunks = [];
|
|
4
|
-
// Check if stdin is connected (not a TTY)
|
|
5
|
-
if (process.stdin.isTTY) {
|
|
6
|
-
resolve("");
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
|
-
process.stdin.on("data", (chunk) => {
|
|
10
|
-
chunks.push(chunk);
|
|
11
|
-
});
|
|
12
|
-
process.stdin.on("end", () => {
|
|
13
|
-
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
14
|
-
});
|
|
15
|
-
process.stdin.on("error", (error) => {
|
|
16
|
-
reject(error);
|
|
17
|
-
});
|
|
18
|
-
// Set a timeout to avoid hanging
|
|
19
|
-
setTimeout(() => {
|
|
20
|
-
if (chunks.length === 0) {
|
|
21
|
-
resolve("");
|
|
22
|
-
}
|
|
23
|
-
}, 100);
|
|
24
|
-
});
|
|
25
|
-
}
|
package/dist/utils/stdio.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/utils/summary.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
export function generateSummary(stats, result, verbose = false) {
|
|
3
|
-
const duration = stats.endTime
|
|
4
|
-
? ((stats.endTime - stats.startTime) / 1000).toFixed(2)
|
|
5
|
-
: "N/A";
|
|
6
|
-
const lines = [];
|
|
7
|
-
lines.push("");
|
|
8
|
-
lines.push(chalk.bold.cyan("AGENT EXECUTION SUMMARY"));
|
|
9
|
-
lines.push(chalk.gray("─".repeat(60)));
|
|
10
|
-
lines.push("");
|
|
11
|
-
// Status with colored indicator
|
|
12
|
-
const statusIcon = result.success ? chalk.green("●") : chalk.red("●");
|
|
13
|
-
const statusText = result.success
|
|
14
|
-
? chalk.green.bold("SUCCESS")
|
|
15
|
-
: chalk.red.bold("FAILED");
|
|
16
|
-
lines.push(statusIcon +
|
|
17
|
-
" " +
|
|
18
|
-
chalk.white.bold("Status:") +
|
|
19
|
-
" ".repeat(11) +
|
|
20
|
-
statusText);
|
|
21
|
-
// Duration with clock icon
|
|
22
|
-
lines.push(chalk.blue("◷") +
|
|
23
|
-
" " +
|
|
24
|
-
chalk.white.bold("Duration:") +
|
|
25
|
-
" ".repeat(9) +
|
|
26
|
-
chalk.white(`${duration}s`));
|
|
27
|
-
// Iterations with counter icon (verbose only)
|
|
28
|
-
if (verbose) {
|
|
29
|
-
lines.push(chalk.cyan("🔄") +
|
|
30
|
-
" " +
|
|
31
|
-
chalk.white.bold("Iterations:") +
|
|
32
|
-
" ".repeat(7) +
|
|
33
|
-
chalk.white(`${stats.iterations}`));
|
|
34
|
-
}
|
|
35
|
-
// Files modified section
|
|
36
|
-
if (stats.filesModified.size > 0) {
|
|
37
|
-
lines.push("");
|
|
38
|
-
lines.push(chalk.green("📝") +
|
|
39
|
-
" " +
|
|
40
|
-
chalk.white.bold(`Files Modified (${stats.filesModified.size})`));
|
|
41
|
-
let count = 0;
|
|
42
|
-
for (const file of stats.filesModified) {
|
|
43
|
-
if (count >= 5) {
|
|
44
|
-
const remaining = stats.filesModified.size - 5;
|
|
45
|
-
lines.push(chalk.gray(` ... and ${remaining} more`));
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
lines.push(chalk.yellow(" →") + " " + chalk.white(file));
|
|
49
|
-
count++;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
// Commands executed section (verbose only)
|
|
53
|
-
if (verbose && stats.commandsRun.length > 0) {
|
|
54
|
-
lines.push("");
|
|
55
|
-
lines.push(chalk.cyan("🔨") +
|
|
56
|
-
" " +
|
|
57
|
-
chalk.white.bold(`Commands Executed (${stats.commandsRun.length})`));
|
|
58
|
-
let count = 0;
|
|
59
|
-
for (const cmd of stats.commandsRun) {
|
|
60
|
-
if (count >= 3) {
|
|
61
|
-
const remaining = stats.commandsRun.length - 3;
|
|
62
|
-
lines.push(chalk.gray(` ... and ${remaining} more`));
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
lines.push(chalk.gray(" $") + " " + chalk.white(cmd));
|
|
66
|
-
count++;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// Errors section
|
|
70
|
-
if (stats.errors.length > 0) {
|
|
71
|
-
lines.push("");
|
|
72
|
-
lines.push(chalk.red("❌") +
|
|
73
|
-
" " +
|
|
74
|
-
chalk.red.bold(`Errors Encountered (${stats.errors.length})`));
|
|
75
|
-
for (const error of stats.errors.slice(0, 3)) {
|
|
76
|
-
lines.push(chalk.red(" ✗") + " " + chalk.white(error));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// Summary section (agent's output already includes its own header)
|
|
80
|
-
if (result.summary && result.summary.trim()) {
|
|
81
|
-
lines.push("");
|
|
82
|
-
// Split summary into lines
|
|
83
|
-
const summaryLines = result.summary.split("\n");
|
|
84
|
-
let lineCount = 0;
|
|
85
|
-
for (const line of summaryLines) {
|
|
86
|
-
// Removed truncation limit to show full summary
|
|
87
|
-
lines.push(line);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
lines.push("");
|
|
91
|
-
lines.push(chalk.gray("─".repeat(60)));
|
|
92
|
-
lines.push("");
|
|
93
|
-
return lines.join("\n");
|
|
94
|
-
}
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI token storage with encryption and keychain support.
|
|
3
|
-
* Inspired by Gemini CLI's hybrid-token-storage pattern.
|
|
4
|
-
*
|
|
5
|
-
* Storage priority:
|
|
6
|
-
* 1. OS Keychain (if available)
|
|
7
|
-
* 2. Encrypted file (~/.supatest/token.json)
|
|
8
|
-
*/
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { homedir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import { decrypt, encrypt } from "./encryption";
|
|
13
|
-
import { isKeychainAvailable, loadFromKeychain, removeFromKeychain, saveToKeychain, } from "./keychain-storage";
|
|
14
|
-
const CONFIG_DIR = join(homedir(), ".supatest");
|
|
15
|
-
const TOKEN_FILE = join(CONFIG_DIR, "token.json");
|
|
16
|
-
const STORAGE_VERSION = 2;
|
|
17
|
-
export { CONFIG_DIR, TOKEN_FILE };
|
|
18
|
-
export var StorageType;
|
|
19
|
-
(function (StorageType) {
|
|
20
|
-
StorageType["KEYCHAIN"] = "keychain";
|
|
21
|
-
StorageType["ENCRYPTED_FILE"] = "encrypted_file";
|
|
22
|
-
})(StorageType || (StorageType = {}));
|
|
23
|
-
// Track which storage is being used
|
|
24
|
-
let activeStorageType = null;
|
|
25
|
-
let storageInitPromise = null;
|
|
26
|
-
function isV2Format(stored) {
|
|
27
|
-
return "version" in stored && stored.version === 2;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Ensure the config directory exists
|
|
31
|
-
*/
|
|
32
|
-
function ensureConfigDir() {
|
|
33
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
34
|
-
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Initialize storage and determine which backend to use.
|
|
39
|
-
* Uses a singleton promise to avoid race conditions.
|
|
40
|
-
*/
|
|
41
|
-
async function initializeStorage() {
|
|
42
|
-
if (await isKeychainAvailable()) {
|
|
43
|
-
activeStorageType = StorageType.KEYCHAIN;
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
activeStorageType = StorageType.ENCRYPTED_FILE;
|
|
47
|
-
}
|
|
48
|
-
return activeStorageType;
|
|
49
|
-
}
|
|
50
|
-
async function getStorageType() {
|
|
51
|
-
if (activeStorageType !== null) {
|
|
52
|
-
return activeStorageType;
|
|
53
|
-
}
|
|
54
|
-
if (!storageInitPromise) {
|
|
55
|
-
storageInitPromise = initializeStorage();
|
|
56
|
-
}
|
|
57
|
-
return storageInitPromise;
|
|
58
|
-
}
|
|
59
|
-
// ============================================================================
|
|
60
|
-
// File-based storage (encrypted)
|
|
61
|
-
// ============================================================================
|
|
62
|
-
/**
|
|
63
|
-
* Save CLI token to encrypted file
|
|
64
|
-
*/
|
|
65
|
-
function saveTokenToFile(token, expiresAt) {
|
|
66
|
-
ensureConfigDir();
|
|
67
|
-
const payload = {
|
|
68
|
-
token,
|
|
69
|
-
expiresAt,
|
|
70
|
-
createdAt: new Date().toISOString(),
|
|
71
|
-
};
|
|
72
|
-
const stored = {
|
|
73
|
-
version: STORAGE_VERSION,
|
|
74
|
-
encryptedData: encrypt(JSON.stringify(payload)),
|
|
75
|
-
};
|
|
76
|
-
writeFileSync(TOKEN_FILE, JSON.stringify(stored, null, 2), { mode: 0o600 });
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Load CLI token from file, decrypting if necessary
|
|
80
|
-
*/
|
|
81
|
-
function loadTokenFromFile() {
|
|
82
|
-
if (!existsSync(TOKEN_FILE)) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
try {
|
|
86
|
-
const data = readFileSync(TOKEN_FILE, "utf8");
|
|
87
|
-
const stored = JSON.parse(data);
|
|
88
|
-
if (isV2Format(stored)) {
|
|
89
|
-
return JSON.parse(decrypt(stored.encryptedData));
|
|
90
|
-
}
|
|
91
|
-
// Legacy plaintext format (version 1 or no version)
|
|
92
|
-
return stored;
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
const err = error;
|
|
96
|
-
if (err.message?.includes("Invalid encrypted data format") ||
|
|
97
|
-
err.message?.includes("Unsupported state or unable to authenticate data")) {
|
|
98
|
-
// Token file corrupted, remove it
|
|
99
|
-
try {
|
|
100
|
-
unlinkSync(TOKEN_FILE);
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
// Ignore
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Remove token file
|
|
111
|
-
*/
|
|
112
|
-
function removeTokenFile() {
|
|
113
|
-
if (existsSync(TOKEN_FILE)) {
|
|
114
|
-
unlinkSync(TOKEN_FILE);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// ============================================================================
|
|
118
|
-
// Synchronous API (file-only, for backward compatibility)
|
|
119
|
-
// ============================================================================
|
|
120
|
-
/**
|
|
121
|
-
* Save CLI token to disk with AES-256-GCM encryption (sync, file-only)
|
|
122
|
-
*/
|
|
123
|
-
export function saveToken(token, expiresAt) {
|
|
124
|
-
saveTokenToFile(token, expiresAt);
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Load CLI token from disk (sync, file-only)
|
|
128
|
-
*/
|
|
129
|
-
export function loadToken() {
|
|
130
|
-
const payload = loadTokenFromFile();
|
|
131
|
-
if (!payload) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
if (payload.expiresAt) {
|
|
135
|
-
const expiresAt = new Date(payload.expiresAt);
|
|
136
|
-
if (expiresAt < new Date()) {
|
|
137
|
-
console.warn("CLI token has expired. Please run 'supatest login' again.");
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return payload.token;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Remove stored token (sync, file-only)
|
|
145
|
-
*/
|
|
146
|
-
export function removeToken() {
|
|
147
|
-
removeTokenFile();
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Check if user is logged in (sync, file-only)
|
|
151
|
-
*/
|
|
152
|
-
export function isLoggedIn() {
|
|
153
|
-
return loadToken() !== null;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Get token info for display (sync, file-only)
|
|
157
|
-
*/
|
|
158
|
-
export function getTokenInfo() {
|
|
159
|
-
const payload = loadTokenFromFile();
|
|
160
|
-
if (!payload) {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
createdAt: payload.createdAt,
|
|
165
|
-
expiresAt: payload.expiresAt,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
// ============================================================================
|
|
169
|
-
// Async API (hybrid: keychain with file fallback)
|
|
170
|
-
// ============================================================================
|
|
171
|
-
/**
|
|
172
|
-
* Save CLI token using best available storage (async, hybrid)
|
|
173
|
-
* Priority: Keychain > Encrypted File
|
|
174
|
-
*/
|
|
175
|
-
export async function saveTokenAsync(token, expiresAt) {
|
|
176
|
-
const storageType = await getStorageType();
|
|
177
|
-
if (storageType === StorageType.KEYCHAIN) {
|
|
178
|
-
const saved = await saveToKeychain(token, expiresAt);
|
|
179
|
-
if (saved) {
|
|
180
|
-
// Remove file-based token if keychain succeeds
|
|
181
|
-
removeTokenFile();
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
// Fall through to file storage if keychain save fails
|
|
185
|
-
}
|
|
186
|
-
saveTokenToFile(token, expiresAt);
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Load CLI token from best available storage (async, hybrid)
|
|
190
|
-
* Priority: Keychain > Encrypted File
|
|
191
|
-
*/
|
|
192
|
-
export async function loadTokenAsync() {
|
|
193
|
-
const storageType = await getStorageType();
|
|
194
|
-
// Try keychain first
|
|
195
|
-
if (storageType === StorageType.KEYCHAIN) {
|
|
196
|
-
const payload = await loadFromKeychain();
|
|
197
|
-
if (payload) {
|
|
198
|
-
if (payload.expiresAt && new Date(payload.expiresAt) < new Date()) {
|
|
199
|
-
console.warn("CLI token has expired. Please run 'supatest login' again.");
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
return payload.token;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// Fallback to file
|
|
206
|
-
return loadToken();
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Remove token from all storage locations (async, hybrid)
|
|
210
|
-
*/
|
|
211
|
-
export async function removeTokenAsync() {
|
|
212
|
-
await removeFromKeychain();
|
|
213
|
-
removeTokenFile();
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Check if user is logged in (async, hybrid)
|
|
217
|
-
*/
|
|
218
|
-
export async function isLoggedInAsync() {
|
|
219
|
-
return (await loadTokenAsync()) !== null;
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Get token info for display (async, hybrid)
|
|
223
|
-
*/
|
|
224
|
-
export async function getTokenInfoAsync() {
|
|
225
|
-
const storageType = await getStorageType();
|
|
226
|
-
if (storageType === StorageType.KEYCHAIN) {
|
|
227
|
-
const payload = await loadFromKeychain();
|
|
228
|
-
if (payload) {
|
|
229
|
-
return {
|
|
230
|
-
createdAt: payload.createdAt,
|
|
231
|
-
expiresAt: payload.expiresAt,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return getTokenInfo();
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Get the currently active storage type
|
|
239
|
-
*/
|
|
240
|
-
export async function getActiveStorageType() {
|
|
241
|
-
return getStorageType();
|
|
242
|
-
}
|