@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.
- package/README.md +58 -315
- package/dist/agent-runner.js +224 -52
- package/dist/commands/login.js +392 -0
- package/dist/commands/setup.js +234 -0
- package/dist/config.js +29 -0
- package/dist/core/agent.js +270 -0
- package/dist/index.js +118 -31
- package/dist/modes/headless.js +117 -0
- package/dist/modes/interactive.js +430 -0
- package/dist/presenters/composite.js +32 -0
- package/dist/presenters/console.js +163 -0
- package/dist/presenters/react.js +220 -0
- package/dist/presenters/types.js +1 -0
- package/dist/presenters/web.js +78 -0
- package/dist/prompts/builder.js +181 -0
- package/dist/prompts/fixer.js +148 -0
- package/dist/prompts/headless.md +97 -0
- package/dist/prompts/index.js +3 -0
- package/dist/prompts/interactive.md +43 -0
- package/dist/prompts/plan.md +41 -0
- package/dist/prompts/planner.js +70 -0
- package/dist/prompts/prompts/builder.md +97 -0
- package/dist/prompts/prompts/fixer.md +100 -0
- package/dist/prompts/prompts/plan.md +41 -0
- package/dist/prompts/prompts/planner.md +41 -0
- package/dist/services/api-client.js +244 -0
- package/dist/services/event-streamer.js +130 -0
- package/dist/ui/App.js +322 -0
- package/dist/ui/components/AuthBanner.js +20 -0
- package/dist/ui/components/AuthDialog.js +32 -0
- package/dist/ui/components/Banner.js +12 -0
- package/dist/ui/components/ExpandableSection.js +17 -0
- package/dist/ui/components/Header.js +49 -0
- package/dist/ui/components/HelpMenu.js +89 -0
- package/dist/ui/components/InputPrompt.js +292 -0
- package/dist/ui/components/MessageList.js +42 -0
- package/dist/ui/components/QueuedMessageDisplay.js +31 -0
- package/dist/ui/components/Scrollable.js +103 -0
- package/dist/ui/components/SessionSelector.js +196 -0
- package/dist/ui/components/StatusBar.js +45 -0
- package/dist/ui/components/messages/AssistantMessage.js +20 -0
- package/dist/ui/components/messages/ErrorMessage.js +26 -0
- package/dist/ui/components/messages/LoadingMessage.js +28 -0
- package/dist/ui/components/messages/ThinkingMessage.js +17 -0
- package/dist/ui/components/messages/TodoMessage.js +44 -0
- package/dist/ui/components/messages/ToolMessage.js +218 -0
- package/dist/ui/components/messages/UserMessage.js +14 -0
- package/dist/ui/contexts/KeypressContext.js +527 -0
- package/dist/ui/contexts/MouseContext.js +98 -0
- package/dist/ui/contexts/SessionContext.js +131 -0
- package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
- package/dist/ui/hooks/useBatchedScroll.js +22 -0
- package/dist/ui/hooks/useBracketedPaste.js +31 -0
- package/dist/ui/hooks/useFocus.js +50 -0
- package/dist/ui/hooks/useKeypress.js +26 -0
- package/dist/ui/hooks/useModeToggle.js +25 -0
- package/dist/ui/types/auth.js +13 -0
- package/dist/ui/utils/file-completion.js +56 -0
- package/dist/ui/utils/input.js +50 -0
- package/dist/ui/utils/markdown.js +376 -0
- package/dist/ui/utils/mouse.js +189 -0
- package/dist/ui/utils/theme.js +59 -0
- package/dist/utils/banner.js +7 -14
- package/dist/utils/encryption.js +71 -0
- package/dist/utils/events.js +36 -0
- package/dist/utils/keychain-storage.js +120 -0
- package/dist/utils/logger.js +103 -1
- package/dist/utils/node-version.js +1 -3
- package/dist/utils/plan-file.js +75 -0
- package/dist/utils/project-instructions.js +23 -0
- package/dist/utils/rich-logger.js +1 -1
- package/dist/utils/stdio.js +80 -0
- package/dist/utils/summary.js +1 -5
- package/dist/utils/token-storage.js +242 -0
- package/package.json +35 -15
|
@@ -0,0 +1,242 @@
|
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supatest/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Supatest CLI - AI-powered task automation for CI/CD",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,16 +11,6 @@
|
|
|
11
11
|
"README.md",
|
|
12
12
|
"LICENSE"
|
|
13
13
|
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"dev": "tsx src/index.ts",
|
|
16
|
-
"dev:bun": "bun run src/index.ts",
|
|
17
|
-
"build": "tsc && node scripts/make-executable.js",
|
|
18
|
-
"build:bun": "bun build src/index.ts --compile --external=@anthropic-ai/claude-agent-sdk --outfile dist/supatest-ai && node scripts/copy-claude-cli.js",
|
|
19
|
-
"type-check": "tsc --noEmit",
|
|
20
|
-
"clean:bundle": "rimraf dist",
|
|
21
|
-
"clean:node_modules": "rimraf node_modules",
|
|
22
|
-
"prepublishOnly": "pnpm build"
|
|
23
|
-
},
|
|
24
14
|
"keywords": [
|
|
25
15
|
"cli",
|
|
26
16
|
"ai",
|
|
@@ -48,24 +38,54 @@
|
|
|
48
38
|
"access": "public"
|
|
49
39
|
},
|
|
50
40
|
"dependencies": {
|
|
51
|
-
"@anthropic-ai/claude-agent-sdk": "^0.1.
|
|
52
|
-
"@anthropic-ai/sdk": "^0.
|
|
41
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.56",
|
|
42
|
+
"@anthropic-ai/sdk": "^0.71.0",
|
|
43
|
+
"@types/inquirer": "^9.0.0",
|
|
44
|
+
"ansi-escapes": "^7.0.0",
|
|
53
45
|
"boxen": "^8.0.1",
|
|
54
46
|
"chalk": "^5.3.0",
|
|
55
47
|
"cli-highlight": "^2.1.11",
|
|
56
48
|
"commander": "^12.1.0",
|
|
49
|
+
"diff": "^8.0.2",
|
|
50
|
+
"dotenv": "^16.6.1",
|
|
51
|
+
"highlight.js": "^11.11.1",
|
|
52
|
+
"ink": "npm:@jrichman/ink@6.4.5",
|
|
53
|
+
"ink-gradient": "^3.0.0",
|
|
54
|
+
"ink-spinner": "^5.0.0",
|
|
55
|
+
"inquirer": "^10.0.0",
|
|
56
|
+
"lowlight": "^3.3.0",
|
|
57
|
+
"marked": "^15.0.0",
|
|
57
58
|
"marked-terminal": "^7.3.0",
|
|
58
59
|
"ora": "^8.1.1",
|
|
59
60
|
"patch-package": "^8.0.1",
|
|
60
61
|
"postinstall-postinstall": "^2.1.0",
|
|
61
|
-
"
|
|
62
|
+
"react": "^19.0.0",
|
|
63
|
+
"string-width": "^8.1.0",
|
|
64
|
+
"strip-ansi": "^7.1.2",
|
|
65
|
+
"wrap-ansi": "^9.0.2",
|
|
66
|
+
"shared": "0.0.1"
|
|
62
67
|
},
|
|
63
68
|
"devDependencies": {
|
|
64
69
|
"@types/node": "^20.12.12",
|
|
70
|
+
"@types/react": "^19.0.0",
|
|
71
|
+
"nodemon": "^3.1.11",
|
|
65
72
|
"tsx": "^4.10.0",
|
|
66
73
|
"typescript": "^5.4.5"
|
|
67
74
|
},
|
|
68
75
|
"engines": {
|
|
69
76
|
"node": ">=18.0.0"
|
|
77
|
+
},
|
|
78
|
+
"optionalDependencies": {
|
|
79
|
+
"keytar": "^7.9.0"
|
|
80
|
+
},
|
|
81
|
+
"scripts": {
|
|
82
|
+
"dev": "NODE_ENV=development tsx src/index.ts",
|
|
83
|
+
"dev:watch": "nodemon",
|
|
84
|
+
"dev:bun": "bun run src/index.ts",
|
|
85
|
+
"build": "tsc && node scripts/make-executable.js",
|
|
86
|
+
"build:bun": "bun build src/index.ts --compile --external=@anthropic-ai/claude-agent-sdk --outfile dist/supatest-ai && node scripts/copy-claude-cli.js",
|
|
87
|
+
"type-check": "tsc --noEmit",
|
|
88
|
+
"clean:bundle": "rimraf dist",
|
|
89
|
+
"clean:node_modules": "rimraf node_modules"
|
|
70
90
|
}
|
|
71
|
-
}
|
|
91
|
+
}
|