@supatest/cli 0.0.3 → 0.0.4

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 (76) hide show
  1. package/dist/index.js +6586 -153
  2. package/package.json +4 -3
  3. package/dist/agent-runner.js +0 -589
  4. package/dist/commands/login.js +0 -392
  5. package/dist/commands/setup.js +0 -234
  6. package/dist/config.js +0 -29
  7. package/dist/core/agent.js +0 -270
  8. package/dist/modes/headless.js +0 -117
  9. package/dist/modes/interactive.js +0 -430
  10. package/dist/presenters/composite.js +0 -32
  11. package/dist/presenters/console.js +0 -163
  12. package/dist/presenters/react.js +0 -220
  13. package/dist/presenters/types.js +0 -1
  14. package/dist/presenters/web.js +0 -78
  15. package/dist/prompts/builder.js +0 -181
  16. package/dist/prompts/fixer.js +0 -148
  17. package/dist/prompts/headless.md +0 -97
  18. package/dist/prompts/index.js +0 -3
  19. package/dist/prompts/interactive.md +0 -43
  20. package/dist/prompts/plan.md +0 -41
  21. package/dist/prompts/planner.js +0 -70
  22. package/dist/prompts/prompts/builder.md +0 -97
  23. package/dist/prompts/prompts/fixer.md +0 -100
  24. package/dist/prompts/prompts/plan.md +0 -41
  25. package/dist/prompts/prompts/planner.md +0 -41
  26. package/dist/services/api-client.js +0 -244
  27. package/dist/services/event-streamer.js +0 -130
  28. package/dist/types.js +0 -1
  29. package/dist/ui/App.js +0 -322
  30. package/dist/ui/components/AuthBanner.js +0 -20
  31. package/dist/ui/components/AuthDialog.js +0 -32
  32. package/dist/ui/components/Banner.js +0 -12
  33. package/dist/ui/components/ExpandableSection.js +0 -17
  34. package/dist/ui/components/Header.js +0 -49
  35. package/dist/ui/components/HelpMenu.js +0 -89
  36. package/dist/ui/components/InputPrompt.js +0 -292
  37. package/dist/ui/components/MessageList.js +0 -42
  38. package/dist/ui/components/QueuedMessageDisplay.js +0 -31
  39. package/dist/ui/components/Scrollable.js +0 -103
  40. package/dist/ui/components/SessionSelector.js +0 -196
  41. package/dist/ui/components/StatusBar.js +0 -45
  42. package/dist/ui/components/messages/AssistantMessage.js +0 -20
  43. package/dist/ui/components/messages/ErrorMessage.js +0 -26
  44. package/dist/ui/components/messages/LoadingMessage.js +0 -28
  45. package/dist/ui/components/messages/ThinkingMessage.js +0 -17
  46. package/dist/ui/components/messages/TodoMessage.js +0 -44
  47. package/dist/ui/components/messages/ToolMessage.js +0 -218
  48. package/dist/ui/components/messages/UserMessage.js +0 -14
  49. package/dist/ui/contexts/KeypressContext.js +0 -527
  50. package/dist/ui/contexts/MouseContext.js +0 -98
  51. package/dist/ui/contexts/SessionContext.js +0 -131
  52. package/dist/ui/hooks/useAnimatedScrollbar.js +0 -83
  53. package/dist/ui/hooks/useBatchedScroll.js +0 -22
  54. package/dist/ui/hooks/useBracketedPaste.js +0 -31
  55. package/dist/ui/hooks/useFocus.js +0 -50
  56. package/dist/ui/hooks/useKeypress.js +0 -26
  57. package/dist/ui/hooks/useModeToggle.js +0 -25
  58. package/dist/ui/types/auth.js +0 -13
  59. package/dist/ui/utils/file-completion.js +0 -56
  60. package/dist/ui/utils/input.js +0 -50
  61. package/dist/ui/utils/markdown.js +0 -376
  62. package/dist/ui/utils/mouse.js +0 -189
  63. package/dist/ui/utils/theme.js +0 -59
  64. package/dist/utils/banner.js +0 -9
  65. package/dist/utils/encryption.js +0 -71
  66. package/dist/utils/events.js +0 -36
  67. package/dist/utils/keychain-storage.js +0 -120
  68. package/dist/utils/logger.js +0 -209
  69. package/dist/utils/node-version.js +0 -89
  70. package/dist/utils/plan-file.js +0 -75
  71. package/dist/utils/project-instructions.js +0 -23
  72. package/dist/utils/rich-logger.js +0 -208
  73. package/dist/utils/stdin.js +0 -25
  74. package/dist/utils/stdio.js +0 -80
  75. package/dist/utils/summary.js +0 -94
  76. package/dist/utils/token-storage.js +0 -242
@@ -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();
@@ -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
- }
@@ -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
- }
@@ -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
- }