@alia-codea/cli 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1362 -491
- package/package.json +5 -5
- package/src/app.tsx +281 -0
- package/src/commands/auth.ts +149 -20
- package/src/commands/repl.ts +11 -299
- package/src/commands/run.ts +103 -126
- package/src/commands/sessions.ts +5 -6
- package/src/components/ApprovalPrompt.tsx +60 -0
- package/src/components/Header.tsx +39 -0
- package/src/components/InputBar.tsx +36 -0
- package/src/components/MessageList.tsx +81 -0
- package/src/components/ThinkingIndicator.tsx +28 -0
- package/src/components/ToolCallCard.tsx +68 -0
- package/src/index.ts +31 -11
- package/src/tools/executor.ts +140 -14
- package/src/tools/patch.ts +167 -0
- package/src/utils/api.ts +22 -3
- package/src/utils/approval.ts +31 -0
- package/src/utils/context.ts +65 -4
- package/src/utils/conversation.ts +141 -0
- package/dist/api-X2G5QROW.js +0 -10
- package/dist/chunk-SVPL4GNV.js +0 -230
- package/dist/index.d.ts +0 -1
- package/src/utils/ui.ts +0 -153
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alia-codea/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Codea CLI - AI coding assistant for your terminal by Alia",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsup src/index.ts --format esm --
|
|
11
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
12
12
|
"dev": "tsx src/index.ts",
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
14
|
"lint": "eslint src",
|
|
@@ -29,19 +29,19 @@
|
|
|
29
29
|
"chalk": "^5.3.0",
|
|
30
30
|
"commander": "^12.1.0",
|
|
31
31
|
"conf": "^13.0.1",
|
|
32
|
-
"ink": "^
|
|
32
|
+
"ink": "^6.7.0",
|
|
33
33
|
"ink-spinner": "^5.0.0",
|
|
34
34
|
"ink-text-input": "^6.0.0",
|
|
35
35
|
"marked": "^15.0.4",
|
|
36
36
|
"marked-terminal": "^7.2.1",
|
|
37
37
|
"openai": "^6.16.0",
|
|
38
38
|
"ora": "^8.1.1",
|
|
39
|
-
"react": "
|
|
39
|
+
"react": "19.2.0",
|
|
40
40
|
"simple-git": "^3.27.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^22.10.5",
|
|
44
|
-
"@types/react": "^
|
|
44
|
+
"@types/react": "^19.2.0",
|
|
45
45
|
"tsup": "^8.3.5",
|
|
46
46
|
"tsx": "^4.19.2",
|
|
47
47
|
"typescript": "^5.7.2"
|
package/src/app.tsx
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
3
|
+
import { Header } from './components/Header.js';
|
|
4
|
+
import { MessageList, DisplayMessage } from './components/MessageList.js';
|
|
5
|
+
import { InputBar } from './components/InputBar.js';
|
|
6
|
+
import { ApprovalPrompt } from './components/ApprovalPrompt.js';
|
|
7
|
+
import { processConversation, Message, ToolExecution } from './utils/conversation.js';
|
|
8
|
+
import { buildSystemMessage, getCodebaseContext, loadProjectInstructions } from './utils/context.js';
|
|
9
|
+
import { createSession, saveSession } from './utils/config.js';
|
|
10
|
+
import { ApprovalMode } from './utils/approval.js';
|
|
11
|
+
|
|
12
|
+
export interface AppOptions {
|
|
13
|
+
model: string;
|
|
14
|
+
approvalMode: ApprovalMode;
|
|
15
|
+
context: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let msgCounter = 0;
|
|
19
|
+
function nextId(): string {
|
|
20
|
+
return `msg-${++msgCounter}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function App({ options }: { options: AppOptions }) {
|
|
24
|
+
const { exit } = useApp();
|
|
25
|
+
const [displayMessages, setDisplayMessages] = useState<DisplayMessage[]>([]);
|
|
26
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
27
|
+
const [thinkingLabel, setThinkingLabel] = useState('Thinking');
|
|
28
|
+
const [approvalMode, setApprovalMode] = useState<ApprovalMode>(options.approvalMode);
|
|
29
|
+
const [contextPercent, setContextPercent] = useState(100);
|
|
30
|
+
const [pendingApproval, setPendingApproval] = useState<{
|
|
31
|
+
execution: ToolExecution;
|
|
32
|
+
resolve: (approved: boolean) => void;
|
|
33
|
+
} | null>(null);
|
|
34
|
+
const [ready, setReady] = useState(false);
|
|
35
|
+
const [codebaseContext, setCodebaseContext] = useState('');
|
|
36
|
+
const [instructions, setInstructions] = useState('');
|
|
37
|
+
|
|
38
|
+
const messagesRef = useRef<Message[]>([]);
|
|
39
|
+
const sessionRef = useRef(createSession());
|
|
40
|
+
const activeRef = useRef(true);
|
|
41
|
+
const streamingIdRef = useRef<string | null>(null);
|
|
42
|
+
|
|
43
|
+
// Initialize on mount
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let cancelled = false;
|
|
46
|
+
(async () => {
|
|
47
|
+
let ctx = '';
|
|
48
|
+
let instr = '';
|
|
49
|
+
if (options.context !== false) {
|
|
50
|
+
ctx = await getCodebaseContext();
|
|
51
|
+
if (ctx && !cancelled) {
|
|
52
|
+
setDisplayMessages((prev) => [
|
|
53
|
+
...prev,
|
|
54
|
+
{ id: nextId(), type: 'info', content: `Loaded context from ${ctx.split('\n').length} lines` },
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
instr = await loadProjectInstructions();
|
|
59
|
+
if (instr && !cancelled) {
|
|
60
|
+
const count = instr.split('\n---\n').length;
|
|
61
|
+
setDisplayMessages((prev) => [
|
|
62
|
+
...prev,
|
|
63
|
+
{ id: nextId(), type: 'info', content: `Loaded ${count} CODEA.md instruction file(s)` },
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
if (!cancelled) {
|
|
67
|
+
setCodebaseContext(ctx);
|
|
68
|
+
setInstructions(instr);
|
|
69
|
+
setReady(true);
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
return () => { cancelled = true; };
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
// Handle Ctrl+C
|
|
76
|
+
useInput((_input, key) => {
|
|
77
|
+
if (key.ctrl && (_input === 'c' || _input === 'C')) {
|
|
78
|
+
if (isProcessing) {
|
|
79
|
+
activeRef.current = false;
|
|
80
|
+
setIsProcessing(false);
|
|
81
|
+
setPendingApproval(null);
|
|
82
|
+
setDisplayMessages((prev) => [
|
|
83
|
+
...prev,
|
|
84
|
+
{ id: nextId(), type: 'info', content: 'Cancelled.' },
|
|
85
|
+
]);
|
|
86
|
+
} else {
|
|
87
|
+
exit();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const addMessage = useCallback((msg: DisplayMessage) => {
|
|
93
|
+
setDisplayMessages((prev) => [...prev, msg]);
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
const updateLastAssistant = useCallback((text: string) => {
|
|
97
|
+
setDisplayMessages((prev) => {
|
|
98
|
+
const last = prev[prev.length - 1];
|
|
99
|
+
if (last && last.type === 'assistant' && last.streaming) {
|
|
100
|
+
return [...prev.slice(0, -1), { ...last, content: last.content + text }];
|
|
101
|
+
}
|
|
102
|
+
return prev;
|
|
103
|
+
});
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const finalizeAssistant = useCallback(() => {
|
|
107
|
+
setDisplayMessages((prev) => {
|
|
108
|
+
const last = prev[prev.length - 1];
|
|
109
|
+
if (last && last.type === 'assistant' && last.streaming) {
|
|
110
|
+
return [...prev.slice(0, -1), { ...last, streaming: false }];
|
|
111
|
+
}
|
|
112
|
+
return prev;
|
|
113
|
+
});
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
const handleSubmit = useCallback(async (input: string) => {
|
|
117
|
+
// Handle slash commands
|
|
118
|
+
if (input.startsWith('/')) {
|
|
119
|
+
const [cmd, ...args] = input.slice(1).split(' ');
|
|
120
|
+
switch (cmd.toLowerCase()) {
|
|
121
|
+
case 'help':
|
|
122
|
+
addMessage({
|
|
123
|
+
id: nextId(),
|
|
124
|
+
type: 'info',
|
|
125
|
+
content: 'Commands: /help, /clear, /mode <suggest|auto-edit|full-auto>, /model <name>, /exit',
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
case 'clear':
|
|
129
|
+
messagesRef.current = [];
|
|
130
|
+
setDisplayMessages([]);
|
|
131
|
+
setContextPercent(100);
|
|
132
|
+
return;
|
|
133
|
+
case 'mode':
|
|
134
|
+
if (args[0] && ['suggest', 'auto-edit', 'full-auto'].includes(args[0])) {
|
|
135
|
+
setApprovalMode(args[0] as ApprovalMode);
|
|
136
|
+
addMessage({ id: nextId(), type: 'info', content: `Approval mode: ${args[0]}` });
|
|
137
|
+
} else {
|
|
138
|
+
addMessage({ id: nextId(), type: 'info', content: `Current mode: ${approvalMode}. Options: suggest, auto-edit, full-auto` });
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
case 'model':
|
|
142
|
+
if (args[0]) {
|
|
143
|
+
options.model = args[0].startsWith('alia-') ? args[0] : `alia-v1-${args[0]}`;
|
|
144
|
+
addMessage({ id: nextId(), type: 'info', content: `Model: ${options.model}` });
|
|
145
|
+
} else {
|
|
146
|
+
addMessage({ id: nextId(), type: 'info', content: `Current model: ${options.model}` });
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
case 'exit':
|
|
150
|
+
case 'quit':
|
|
151
|
+
exit();
|
|
152
|
+
return;
|
|
153
|
+
default:
|
|
154
|
+
addMessage({ id: nextId(), type: 'info', content: `Unknown command: /${cmd}` });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Add user message
|
|
160
|
+
addMessage({ id: nextId(), type: 'user', content: input });
|
|
161
|
+
messagesRef.current.push({ role: 'user', content: input });
|
|
162
|
+
|
|
163
|
+
setIsProcessing(true);
|
|
164
|
+
activeRef.current = true;
|
|
165
|
+
streamingIdRef.current = null;
|
|
166
|
+
|
|
167
|
+
const systemMessage = buildSystemMessage(options.model, codebaseContext, instructions);
|
|
168
|
+
|
|
169
|
+
await processConversation({
|
|
170
|
+
messages: messagesRef.current,
|
|
171
|
+
systemMessage,
|
|
172
|
+
model: options.model,
|
|
173
|
+
approvalMode,
|
|
174
|
+
isActive: () => activeRef.current,
|
|
175
|
+
requestApproval: (execution) => {
|
|
176
|
+
return new Promise<boolean>((resolve) => {
|
|
177
|
+
setPendingApproval({ execution, resolve });
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
onEvent: (event) => {
|
|
181
|
+
switch (event.type) {
|
|
182
|
+
case 'thinking':
|
|
183
|
+
setThinkingLabel('Thinking');
|
|
184
|
+
// Start a new streaming assistant message
|
|
185
|
+
streamingIdRef.current = nextId();
|
|
186
|
+
setDisplayMessages((prev) => [
|
|
187
|
+
...prev,
|
|
188
|
+
{ id: streamingIdRef.current!, type: 'assistant', content: '', streaming: true },
|
|
189
|
+
]);
|
|
190
|
+
break;
|
|
191
|
+
case 'content':
|
|
192
|
+
updateLastAssistant(event.text);
|
|
193
|
+
break;
|
|
194
|
+
case 'tool_start':
|
|
195
|
+
// Finalize any streaming text before showing tool
|
|
196
|
+
finalizeAssistant();
|
|
197
|
+
setThinkingLabel(`Running ${event.execution.tool}`);
|
|
198
|
+
addMessage({
|
|
199
|
+
id: nextId(),
|
|
200
|
+
type: 'tool',
|
|
201
|
+
content: '',
|
|
202
|
+
toolExecution: { ...event.execution },
|
|
203
|
+
});
|
|
204
|
+
break;
|
|
205
|
+
case 'tool_done':
|
|
206
|
+
// Update the tool message with result
|
|
207
|
+
setDisplayMessages((prev) => {
|
|
208
|
+
const idx = prev.findLastIndex(
|
|
209
|
+
(m) => m.type === 'tool' && m.toolExecution?.id === event.execution.id
|
|
210
|
+
);
|
|
211
|
+
if (idx >= 0) {
|
|
212
|
+
const updated = [...prev];
|
|
213
|
+
updated[idx] = {
|
|
214
|
+
...updated[idx],
|
|
215
|
+
toolExecution: { ...event.execution },
|
|
216
|
+
};
|
|
217
|
+
return updated;
|
|
218
|
+
}
|
|
219
|
+
return prev;
|
|
220
|
+
});
|
|
221
|
+
break;
|
|
222
|
+
case 'done':
|
|
223
|
+
finalizeAssistant();
|
|
224
|
+
break;
|
|
225
|
+
case 'error':
|
|
226
|
+
finalizeAssistant();
|
|
227
|
+
addMessage({ id: nextId(), type: 'info', content: `Error: ${event.message}` });
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
setIsProcessing(false);
|
|
234
|
+
setPendingApproval(null);
|
|
235
|
+
|
|
236
|
+
// Save session
|
|
237
|
+
const session = sessionRef.current;
|
|
238
|
+
session.messages = messagesRef.current.map((m) => ({ role: m.role, content: m.content }));
|
|
239
|
+
session.title = messagesRef.current[0]?.content.slice(0, 50) || 'New conversation';
|
|
240
|
+
session.updatedAt = Date.now();
|
|
241
|
+
saveSession(session);
|
|
242
|
+
|
|
243
|
+
// Update context estimate
|
|
244
|
+
const totalChars = messagesRef.current.reduce((acc, m) => acc + m.content.length, 0);
|
|
245
|
+
const maxContext = 128000;
|
|
246
|
+
setContextPercent(Math.max(5, 100 - Math.floor((totalChars / maxContext) * 100)));
|
|
247
|
+
}, [approvalMode, codebaseContext, instructions, options]);
|
|
248
|
+
|
|
249
|
+
const handleApprovalResolve = useCallback((approved: boolean) => {
|
|
250
|
+
if (pendingApproval) {
|
|
251
|
+
pendingApproval.resolve(approved);
|
|
252
|
+
setPendingApproval(null);
|
|
253
|
+
}
|
|
254
|
+
}, [pendingApproval]);
|
|
255
|
+
|
|
256
|
+
const modelDisplay = options.model.replace('alia-v1-', '');
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<Box flexDirection="column">
|
|
260
|
+
<Header
|
|
261
|
+
cwd={process.cwd()}
|
|
262
|
+
model={modelDisplay}
|
|
263
|
+
approvalMode={approvalMode}
|
|
264
|
+
contextPercent={contextPercent}
|
|
265
|
+
/>
|
|
266
|
+
<MessageList messages={displayMessages} />
|
|
267
|
+
{pendingApproval ? (
|
|
268
|
+
<ApprovalPrompt
|
|
269
|
+
execution={pendingApproval.execution}
|
|
270
|
+
onResolve={handleApprovalResolve}
|
|
271
|
+
/>
|
|
272
|
+
) : (
|
|
273
|
+
<InputBar
|
|
274
|
+
onSubmit={handleSubmit}
|
|
275
|
+
isProcessing={isProcessing}
|
|
276
|
+
thinkingLabel={thinkingLabel}
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
279
|
+
</Box>
|
|
280
|
+
);
|
|
281
|
+
}
|
package/src/commands/auth.ts
CHANGED
|
@@ -1,18 +1,134 @@
|
|
|
1
1
|
import * as readline from 'readline';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
3
|
+
import * as http from 'http';
|
|
4
|
+
import { exec } from 'child_process';
|
|
2
5
|
import chalk from 'chalk';
|
|
3
6
|
import { config } from '../utils/config.js';
|
|
4
|
-
|
|
7
|
+
function printSuccess(message: string): void {
|
|
8
|
+
console.log(chalk.green('✓ ') + message);
|
|
9
|
+
}
|
|
5
10
|
|
|
6
|
-
|
|
7
|
-
console.log();
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
console.log();
|
|
11
|
+
function printError(message: string): void {
|
|
12
|
+
console.log(chalk.red('✗ Error: ') + message);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function printInfo(message: string): void {
|
|
16
|
+
console.log(chalk.blue('ℹ ') + message);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function openBrowser(url: string): void {
|
|
20
|
+
const cmd =
|
|
21
|
+
process.platform === 'darwin'
|
|
22
|
+
? 'open'
|
|
23
|
+
: process.platform === 'win32'
|
|
24
|
+
? 'start ""'
|
|
25
|
+
: 'xdg-open';
|
|
26
|
+
exec(`${cmd} "${url}"`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function loginWithBrowser(): Promise<boolean> {
|
|
30
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
31
|
+
const codeChallenge = crypto
|
|
32
|
+
.createHash('sha256')
|
|
33
|
+
.update(codeVerifier)
|
|
34
|
+
.digest('base64url');
|
|
35
|
+
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const server = http.createServer(async (req, res) => {
|
|
38
|
+
const url = new URL(req.url!, `http://localhost`);
|
|
39
|
+
if (url.pathname !== '/callback') {
|
|
40
|
+
res.writeHead(404);
|
|
41
|
+
res.end();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const code = url.searchParams.get('code');
|
|
46
|
+
const error = url.searchParams.get('error');
|
|
47
|
+
|
|
48
|
+
if (error || !code) {
|
|
49
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
50
|
+
res.end(
|
|
51
|
+
'<html><body style="font-family:system-ui;text-align:center;padding:60px">' +
|
|
52
|
+
'<h2>Authorization cancelled</h2><p>You can close this window.</p></body></html>',
|
|
53
|
+
);
|
|
54
|
+
server.close();
|
|
55
|
+
resolve(false);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const baseUrl = config.get('apiBaseUrl') || 'https://api.alia.onl';
|
|
61
|
+
const response = await fetch(`${baseUrl}/auth/token`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
grant_type: 'authorization_code',
|
|
66
|
+
code,
|
|
67
|
+
code_verifier: codeVerifier,
|
|
68
|
+
client_id: 'codea',
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const data = (await response.json()) as { token?: string };
|
|
73
|
+
|
|
74
|
+
if (data.token) {
|
|
75
|
+
config.set('apiKey', data.token);
|
|
76
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
77
|
+
res.end(
|
|
78
|
+
'<html><body style="font-family:system-ui;text-align:center;padding:60px">' +
|
|
79
|
+
'<h2>Logged in!</h2><p>You can close this window and return to the terminal.</p></body></html>',
|
|
80
|
+
);
|
|
81
|
+
console.log();
|
|
82
|
+
printSuccess('Logged in successfully!');
|
|
83
|
+
server.close();
|
|
84
|
+
resolve(true);
|
|
85
|
+
} else {
|
|
86
|
+
throw new Error('No token received');
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
90
|
+
res.end(
|
|
91
|
+
'<html><body style="font-family:system-ui;text-align:center;padding:60px">' +
|
|
92
|
+
'<h2>Login failed</h2><p>Please try again.</p></body></html>',
|
|
93
|
+
);
|
|
94
|
+
printError('Failed to exchange authorization code.');
|
|
95
|
+
server.close();
|
|
96
|
+
resolve(false);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
server.listen(0, () => {
|
|
101
|
+
const port = (server.address() as { port: number }).port;
|
|
102
|
+
const callback = encodeURIComponent(`http://localhost:${port}/callback`);
|
|
103
|
+
const authorizeUrl =
|
|
104
|
+
`https://alia.onl/authorize?app=codea` +
|
|
105
|
+
`&callback=${callback}` +
|
|
106
|
+
`&code_challenge=${codeChallenge}` +
|
|
107
|
+
`&code_challenge_method=S256`;
|
|
12
108
|
|
|
109
|
+
printInfo('Opening browser for authorization...');
|
|
110
|
+
openBrowser(authorizeUrl);
|
|
111
|
+
console.log(
|
|
112
|
+
chalk.gray('\nIf the browser doesn\'t open, visit:\n') +
|
|
113
|
+
chalk.cyan(authorizeUrl) +
|
|
114
|
+
'\n',
|
|
115
|
+
);
|
|
116
|
+
console.log(chalk.gray('Waiting for authorization...'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Timeout after 5 minutes
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
server.close();
|
|
122
|
+
printError('Authorization timed out.');
|
|
123
|
+
resolve(false);
|
|
124
|
+
}, 5 * 60 * 1000);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function loginWithApiKey(): Promise<boolean> {
|
|
13
129
|
const rl = readline.createInterface({
|
|
14
130
|
input: process.stdin,
|
|
15
|
-
output: process.stdout
|
|
131
|
+
output: process.stdout,
|
|
16
132
|
});
|
|
17
133
|
|
|
18
134
|
return new Promise((resolve) => {
|
|
@@ -20,46 +136,59 @@ export async function login(): Promise<void> {
|
|
|
20
136
|
rl.close();
|
|
21
137
|
|
|
22
138
|
const trimmedKey = apiKey.trim();
|
|
23
|
-
|
|
24
139
|
if (!trimmedKey) {
|
|
25
140
|
printError('No API key provided.');
|
|
26
|
-
resolve();
|
|
141
|
+
resolve(false);
|
|
27
142
|
return;
|
|
28
143
|
}
|
|
29
144
|
|
|
30
|
-
// Validate the API key by making a test request
|
|
31
145
|
printInfo('Validating API key...');
|
|
32
146
|
|
|
33
147
|
try {
|
|
34
148
|
const baseUrl = config.get('apiBaseUrl') || 'https://api.alia.onl';
|
|
35
149
|
const response = await fetch(`${baseUrl}/codea/me`, {
|
|
36
|
-
headers: {
|
|
37
|
-
'Authorization': `Bearer ${trimmedKey}`
|
|
38
|
-
}
|
|
150
|
+
headers: { Authorization: `Bearer ${trimmedKey}` },
|
|
39
151
|
});
|
|
40
152
|
|
|
41
153
|
if (response.ok) {
|
|
42
|
-
const data = await response.json();
|
|
154
|
+
const data = (await response.json()) as { name?: string };
|
|
43
155
|
config.set('apiKey', trimmedKey);
|
|
44
156
|
console.log();
|
|
45
|
-
printSuccess(
|
|
157
|
+
printSuccess('Logged in successfully!');
|
|
46
158
|
if (data.name) {
|
|
47
159
|
console.log(chalk.gray(`Welcome, ${data.name}!`));
|
|
48
160
|
}
|
|
49
|
-
|
|
50
|
-
console.log(chalk.gray('Run ') + chalk.cyan('codea') + chalk.gray(' to start coding.'));
|
|
161
|
+
resolve(true);
|
|
51
162
|
} else {
|
|
52
163
|
printError('Invalid API key. Please check and try again.');
|
|
164
|
+
resolve(false);
|
|
53
165
|
}
|
|
54
166
|
} catch (error: any) {
|
|
55
167
|
printError(`Could not validate API key: ${error.message}`);
|
|
168
|
+
resolve(false);
|
|
56
169
|
}
|
|
57
|
-
|
|
58
|
-
resolve();
|
|
59
170
|
});
|
|
60
171
|
});
|
|
61
172
|
}
|
|
62
173
|
|
|
174
|
+
export async function login(): Promise<boolean> {
|
|
175
|
+
console.log();
|
|
176
|
+
console.log(chalk.bold('Codea CLI Login'));
|
|
177
|
+
console.log();
|
|
178
|
+
|
|
179
|
+
const success = await loginWithBrowser();
|
|
180
|
+
if (success) return true;
|
|
181
|
+
|
|
182
|
+
// Fallback to manual API key entry
|
|
183
|
+
console.log();
|
|
184
|
+
console.log(
|
|
185
|
+
chalk.gray('Alternatively, paste your API key from: ') +
|
|
186
|
+
chalk.cyan('https://alia.onl/settings/api'),
|
|
187
|
+
);
|
|
188
|
+
console.log();
|
|
189
|
+
return loginWithApiKey();
|
|
190
|
+
}
|
|
191
|
+
|
|
63
192
|
export function logout(): void {
|
|
64
193
|
config.delete('apiKey');
|
|
65
194
|
printSuccess('Logged out successfully.');
|