@thegitai/cli 1.0.0-preview.26 → 1.0.0-preview.27
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 +9 -4
- package/dist/bin/ai.js +38 -54
- package/dist/src/api/browser-login.js +80 -34
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +2 -2
- package/dist/src/cli-args.js +19 -5
- package/dist/src/help-text.js +12 -5
- package/dist/src/signin.js +56 -0
- package/dist/src/ui/repl.js +23 -1
- package/dist/src/ui/tui/build-frame.js +1 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -17,9 +17,9 @@ Requires Node.js 24 or newer.
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
19
|
```text
|
|
20
|
-
ai start
|
|
20
|
+
ai sign in if needed, then start a session in the current repo
|
|
21
21
|
ai "<request>" start a session with <request> as the first message
|
|
22
|
-
ai login
|
|
22
|
+
ai login the same as `ai`; kept for muscle memory
|
|
23
23
|
ai whoami show the signed-in account
|
|
24
24
|
ai --usage show account usage and reset times
|
|
25
25
|
ai logout sign out
|
|
@@ -34,9 +34,14 @@ always be viewed or resumed from that computer. Continuing it through the
|
|
|
34
34
|
service requires the TheGitAI account used for that session. Local sessions can
|
|
35
35
|
also be listed while signed out or offline.
|
|
36
36
|
|
|
37
|
+
Signing in opens your browser. The same screen also prints a URL you can open on
|
|
38
|
+
any other device and a box for an authorization code, so SSH and headless
|
|
39
|
+
machines need no separate command: open the URL wherever you have a browser,
|
|
40
|
+
choose "Not on this machine?" on that page, and paste the code it shows.
|
|
41
|
+
|
|
37
42
|
CLI login tokens use a rolling 48-hour inactivity timeout. If one expires during
|
|
38
|
-
any server request, the CLI
|
|
39
|
-
|
|
43
|
+
any server request, the CLI removes the expired credential and signs you in
|
|
44
|
+
again on the next run.
|
|
40
45
|
|
|
41
46
|
## Structured questions
|
|
42
47
|
|
package/dist/bin/ai.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from '../src/colors.js';
|
|
3
|
-
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
-
import readline from 'node:readline/promises';
|
|
5
3
|
import { ServerApi } from '../src/api/index.js';
|
|
6
|
-
import {
|
|
4
|
+
import { DEFAULT_THEGITAI_HOST } from '../src/api/default-host.js';
|
|
5
|
+
import { isSignInCancelled } from '../src/api/browser-login.js';
|
|
6
|
+
import { runSignIn } from '../src/signin.js';
|
|
7
7
|
import { STARTUP_RETRY_BUDGET, authenticationErrorMessage, isAuthenticationError, isTransientNetworkError, } from '../src/api/http.js';
|
|
8
8
|
import { formatCliHelpText } from '../src/help-text.js';
|
|
9
9
|
import { createIndex } from '../src/project-index.js';
|
|
@@ -15,7 +15,6 @@ import { formatSessionExitNotice } from '../src/session-exit.js';
|
|
|
15
15
|
import { formatUsageText } from '../src/usage.js';
|
|
16
16
|
import { formatVersionLine } from '../src/version.js';
|
|
17
17
|
import { parseArgs } from '../src/cli-args.js';
|
|
18
|
-
const DEFAULT_SERVER_URL = 'https://thegit.ai';
|
|
19
18
|
const { auth, chat, models, sessions } = ServerApi;
|
|
20
19
|
function printUsage() {
|
|
21
20
|
console.log(formatCliHelpText({ color: process.stdout.isTTY === true }));
|
|
@@ -34,49 +33,17 @@ function unreachableReason(error) {
|
|
|
34
33
|
return 'connection reset';
|
|
35
34
|
return err?.message ? String(err.message) : 'network error';
|
|
36
35
|
}
|
|
37
|
-
async function promptText(question, fallback = null) {
|
|
38
|
-
const rl = readline.createInterface({ input, output });
|
|
39
|
-
try {
|
|
40
|
-
const suffix = fallback ? ` (${fallback})` : '';
|
|
41
|
-
const answer = await rl.question(`${question}${suffix}: `);
|
|
42
|
-
return answer.trim() || fallback || '';
|
|
43
|
-
}
|
|
44
|
-
finally {
|
|
45
|
-
rl.close();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
36
|
function appendPromptHistory(prompt, env = process.env) {
|
|
49
37
|
appendPromptToFile(prompt, env);
|
|
50
38
|
}
|
|
51
|
-
async function runAuthCommand(command
|
|
52
|
-
if (command === 'login') {
|
|
53
|
-
const serverUrl = DEFAULT_SERVER_URL;
|
|
54
|
-
const noBrowser = args.includes('--no-browser');
|
|
55
|
-
console.log(chalk.dim(noBrowser
|
|
56
|
-
? 'Sign in on the website, then paste the authorization code here.'
|
|
57
|
-
: 'Opening your browser to sign in…'));
|
|
58
|
-
const result = await loginViaBrowser({
|
|
59
|
-
serverUrl,
|
|
60
|
-
noBrowser,
|
|
61
|
-
onUrl: (url) => {
|
|
62
|
-
console.log(chalk.dim(noBrowser ? 'Open this URL to sign in:' : 'If your browser did not open, visit:'));
|
|
63
|
-
console.log(` ${url}`);
|
|
64
|
-
},
|
|
65
|
-
onWaiting: () => console.log(chalk.dim('Waiting for you to finish signing in…')),
|
|
66
|
-
promptCode: noBrowser
|
|
67
|
-
? () => promptText('Paste the authorization code')
|
|
68
|
-
: undefined,
|
|
69
|
-
deviceName: process.env.THEGITAI_DEVICE_NAME?.trim() || undefined,
|
|
70
|
-
});
|
|
71
|
-
auth.writeCliAuthConfig(result);
|
|
72
|
-
console.log(chalk.green(`✓ Logged in as ${result.customer.email}`));
|
|
73
|
-
console.log(chalk.dim(`Server: ${result.serverUrl}`));
|
|
74
|
-
console.log(chalk.dim('You can close the browser tab. Run `ai` in a repo to start.'));
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
39
|
+
async function runAuthCommand(command) {
|
|
77
40
|
const config = auth.readCliAuthConfig();
|
|
78
41
|
if (!config) {
|
|
79
|
-
|
|
42
|
+
if (command === 'logout') {
|
|
43
|
+
console.log(chalk.green('Already signed out.'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
throw new Error('Not signed in. Run `ai` to sign in.');
|
|
80
47
|
}
|
|
81
48
|
if (command === 'whoami') {
|
|
82
49
|
const customer = await auth.fetchWhoami({ config });
|
|
@@ -84,19 +51,25 @@ async function runAuthCommand(command, args) {
|
|
|
84
51
|
return;
|
|
85
52
|
}
|
|
86
53
|
if (command === 'logout') {
|
|
87
|
-
|
|
54
|
+
try {
|
|
55
|
+
await auth.logoutFromServer({ config });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
}
|
|
88
59
|
auth.clearCliAuthConfig();
|
|
89
60
|
console.log(chalk.green('Logged out.'));
|
|
90
61
|
return;
|
|
91
62
|
}
|
|
92
63
|
throw new Error(`Unknown auth command: ${command}`);
|
|
93
64
|
}
|
|
94
|
-
function
|
|
65
|
+
async function ensureCliAuthConfig() {
|
|
95
66
|
const config = auth.readCliAuthConfig();
|
|
96
|
-
if (
|
|
97
|
-
|
|
67
|
+
if (config)
|
|
68
|
+
return config;
|
|
69
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
70
|
+
throw new Error('Not signed in. Run `ai login` on a terminal to sign in.');
|
|
98
71
|
}
|
|
99
|
-
return
|
|
72
|
+
return await runSignIn();
|
|
100
73
|
}
|
|
101
74
|
function formatSessionName(name) {
|
|
102
75
|
return name ? `"${name}"` : '(unnamed)';
|
|
@@ -154,7 +127,7 @@ function requireInteractiveTerminal() {
|
|
|
154
127
|
return true;
|
|
155
128
|
}
|
|
156
129
|
export async function main() {
|
|
157
|
-
const { autoYes, help, version, usage, command,
|
|
130
|
+
const { autoYes, help, version, usage, command, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
|
|
158
131
|
if (version) {
|
|
159
132
|
console.log(formatVersionLine());
|
|
160
133
|
return;
|
|
@@ -169,18 +142,18 @@ export async function main() {
|
|
|
169
142
|
process.exitCode = 2;
|
|
170
143
|
return;
|
|
171
144
|
}
|
|
172
|
-
if (command) {
|
|
173
|
-
await runAuthCommand(command
|
|
145
|
+
if (command && command !== 'login') {
|
|
146
|
+
await runAuthCommand(command);
|
|
174
147
|
return;
|
|
175
148
|
}
|
|
176
149
|
if (usage) {
|
|
177
|
-
const authConfig =
|
|
150
|
+
const authConfig = await ensureCliAuthConfig();
|
|
178
151
|
console.log(formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })));
|
|
179
152
|
return;
|
|
180
153
|
}
|
|
181
154
|
const rootDir = process.cwd();
|
|
182
155
|
if (listSessions) {
|
|
183
|
-
const activeServerUrl = auth.readCliAuthConfig()?.serverUrl ??
|
|
156
|
+
const activeServerUrl = auth.readCliAuthConfig()?.serverUrl ?? DEFAULT_THEGITAI_HOST;
|
|
184
157
|
printSessionList(rootDir, listSessionMetadata(rootDir), models.selectCacheForServer(models.readCachedServerModels(), activeServerUrl));
|
|
185
158
|
return;
|
|
186
159
|
}
|
|
@@ -195,7 +168,7 @@ export async function main() {
|
|
|
195
168
|
if (!requireInteractiveTerminal()) {
|
|
196
169
|
return;
|
|
197
170
|
}
|
|
198
|
-
const authConfig =
|
|
171
|
+
const authConfig = await ensureCliAuthConfig();
|
|
199
172
|
const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
|
|
200
173
|
const cachedModels = models.selectCacheForServer(models.readCachedServerModels(), authConfig.serverUrl);
|
|
201
174
|
const [modelsOutcome, whoamiOutcome] = await Promise.allSettled([
|
|
@@ -279,7 +252,7 @@ export async function main() {
|
|
|
279
252
|
},
|
|
280
253
|
});
|
|
281
254
|
const initialPrompt = prompt || undefined;
|
|
282
|
-
await runClientInteractive({
|
|
255
|
+
const outcome = await runClientInteractive({
|
|
283
256
|
appendPromptHistory: (value) => appendPromptHistory(value, session.env),
|
|
284
257
|
authConfig,
|
|
285
258
|
debugUi: whoami.debugUi,
|
|
@@ -290,10 +263,21 @@ export async function main() {
|
|
|
290
263
|
initialPrompt,
|
|
291
264
|
usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
|
|
292
265
|
});
|
|
266
|
+
if (outcome.signedOut) {
|
|
267
|
+
if (sessionHasUserMessage(session)) {
|
|
268
|
+
saveSessionState(session);
|
|
269
|
+
}
|
|
270
|
+
console.log(chalk.green('\n✓ Logged out.\n'));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
293
273
|
await saveSessionBoth({ session, serverSessionClient });
|
|
294
274
|
printSessionExit(session);
|
|
295
275
|
}
|
|
296
276
|
main().catch((error) => {
|
|
277
|
+
if (isSignInCancelled(error)) {
|
|
278
|
+
console.error(chalk.dim('\nSign-in cancelled. Nothing was saved.\n'));
|
|
279
|
+
process.exit(130);
|
|
280
|
+
}
|
|
297
281
|
if (isAuthenticationError(error)) {
|
|
298
282
|
auth.clearCliAuthConfig();
|
|
299
283
|
console.error(chalk.red(`\n✖ Error: ${authenticationErrorMessage(error)}\n`));
|
|
@@ -5,15 +5,27 @@ import http from 'node:http';
|
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { openUrl } from '../core/open-url.js';
|
|
7
7
|
import { ServerApiError, createTraceContext, failureCode, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
8
|
+
import { DEFAULT_THEGITAI_HOST } from './default-host.js';
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
11
10
|
function shutDownServer(server) {
|
|
12
11
|
server.closeAllConnections?.();
|
|
13
12
|
server.close();
|
|
14
13
|
}
|
|
14
|
+
export class SignInCancelledError extends Error {
|
|
15
|
+
constructor() {
|
|
16
|
+
super('Sign-in cancelled.');
|
|
17
|
+
this.name = 'SignInCancelledError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function isSignInCancelled(error) {
|
|
21
|
+
return error instanceof SignInCancelledError;
|
|
22
|
+
}
|
|
23
|
+
function isAbortError(error) {
|
|
24
|
+
const err = error;
|
|
25
|
+
return err?.name === 'AbortError' || err?.code === 'ABORT_ERR';
|
|
26
|
+
}
|
|
15
27
|
export function resolveWebsiteUrl() {
|
|
16
|
-
return
|
|
28
|
+
return DEFAULT_THEGITAI_HOST.replace(/\/+$/, '');
|
|
17
29
|
}
|
|
18
30
|
function defaultDeviceName() {
|
|
19
31
|
try {
|
|
@@ -102,12 +114,8 @@ function buildAuthUrl(websiteUrl, params) {
|
|
|
102
114
|
const url = new URL(`${websiteUrl}/cli-auth`);
|
|
103
115
|
url.searchParams.set('code_challenge', params.codeChallenge);
|
|
104
116
|
url.searchParams.set('device_name', params.deviceName);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (params.state)
|
|
108
|
-
url.searchParams.set('state', params.state);
|
|
109
|
-
if (params.paste)
|
|
110
|
-
url.searchParams.set('mode', 'paste');
|
|
117
|
+
url.searchParams.set('redirect_uri', params.redirectUri);
|
|
118
|
+
url.searchParams.set('state', params.state);
|
|
111
119
|
return url.toString();
|
|
112
120
|
}
|
|
113
121
|
const RESULT_PAGE = (heading, detail) => `<!doctype html><html><head><meta charset="utf-8"><title>TheGitAI CLI</title>` +
|
|
@@ -140,36 +148,38 @@ async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl,
|
|
|
140
148
|
customer,
|
|
141
149
|
};
|
|
142
150
|
}
|
|
151
|
+
async function readPastedResult({ promptCode, signal, serverUrl, codeVerifier, fetchImpl, onPasteRejected, }) {
|
|
152
|
+
while (!signal.aborted) {
|
|
153
|
+
const code = (await promptCode(signal)).trim();
|
|
154
|
+
if (code) {
|
|
155
|
+
try {
|
|
156
|
+
return await exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl });
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (signal.aborted)
|
|
160
|
+
break;
|
|
161
|
+
onPasteRejected?.(error.message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
165
|
+
}
|
|
166
|
+
return await new Promise(() => { });
|
|
167
|
+
}
|
|
143
168
|
export async function loginViaBrowser(options) {
|
|
144
|
-
const serverUrl = normalizeServerUrl(options.serverUrl ??
|
|
169
|
+
const serverUrl = normalizeServerUrl(options.serverUrl ?? DEFAULT_THEGITAI_HOST);
|
|
145
170
|
const websiteUrl = resolveWebsiteUrl();
|
|
146
171
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
147
172
|
const openBrowser = options.openBrowser ?? openUrl;
|
|
148
173
|
const onUrl = options.onUrl ?? (() => { });
|
|
149
174
|
const deviceName = withOperatingSystemInfo(options.deviceName ?? defaultDeviceName());
|
|
150
175
|
const { verifier, challenge } = generatePkce();
|
|
151
|
-
if (options.noBrowser) {
|
|
152
|
-
const authUrl = buildAuthUrl(websiteUrl, {
|
|
153
|
-
codeChallenge: challenge,
|
|
154
|
-
deviceName,
|
|
155
|
-
paste: true,
|
|
156
|
-
});
|
|
157
|
-
onUrl(authUrl);
|
|
158
|
-
if (!options.promptCode) {
|
|
159
|
-
throw new Error('No way to read the authorization code in this context.');
|
|
160
|
-
}
|
|
161
|
-
const code = (await options.promptCode()).trim();
|
|
162
|
-
if (!code) {
|
|
163
|
-
throw new Error('No authorization code was entered.');
|
|
164
|
-
}
|
|
165
|
-
return exchangeCodeForToken({ serverUrl, code, codeVerifier: verifier, fetchImpl });
|
|
166
|
-
}
|
|
167
176
|
const state = crypto.randomBytes(16).toString('base64url');
|
|
168
177
|
const server = http.createServer();
|
|
178
|
+
let timer;
|
|
169
179
|
const codePromise = new Promise((resolve, reject) => {
|
|
170
|
-
|
|
180
|
+
timer = setTimeout(() => {
|
|
171
181
|
shutDownServer(server);
|
|
172
|
-
reject(new Error('Timed out waiting for the
|
|
182
|
+
reject(new Error('Timed out waiting for the login to complete.'));
|
|
173
183
|
}, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
174
184
|
server.on('request', (req, res) => {
|
|
175
185
|
const requestUrl = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
@@ -182,7 +192,7 @@ export async function loginViaBrowser(options) {
|
|
|
182
192
|
const returnedState = requestUrl.searchParams.get('state') ?? '';
|
|
183
193
|
if (!code || returnedState !== state) {
|
|
184
194
|
res.writeHead(400, { 'content-type': 'text/html', connection: 'close' });
|
|
185
|
-
res.end(RESULT_PAGE('Login failed', 'The request could not be verified. Please run ai
|
|
195
|
+
res.end(RESULT_PAGE('Login failed', 'The request could not be verified. Please run `ai` again.'));
|
|
186
196
|
clearTimeout(timer);
|
|
187
197
|
shutDownServer(server);
|
|
188
198
|
reject(new Error('The login callback could not be verified.'));
|
|
@@ -212,8 +222,44 @@ export async function loginViaBrowser(options) {
|
|
|
212
222
|
state,
|
|
213
223
|
});
|
|
214
224
|
onUrl(authUrl);
|
|
215
|
-
await openBrowser(authUrl).catch(() => false);
|
|
216
|
-
options.
|
|
217
|
-
|
|
218
|
-
|
|
225
|
+
const opened = await openBrowser(authUrl).catch(() => false);
|
|
226
|
+
options.onBrowserOpen?.(opened);
|
|
227
|
+
let raceSettled = false;
|
|
228
|
+
const untilSettled = (promise) => promise.catch((error) => {
|
|
229
|
+
if (raceSettled)
|
|
230
|
+
return new Promise(() => { });
|
|
231
|
+
throw error;
|
|
232
|
+
});
|
|
233
|
+
const pasteAbort = new AbortController();
|
|
234
|
+
const routes = [
|
|
235
|
+
untilSettled(codePromise.then((code) => exchangeCodeForToken({ serverUrl, code, codeVerifier: verifier, fetchImpl }))),
|
|
236
|
+
];
|
|
237
|
+
if (options.promptCode) {
|
|
238
|
+
const pasted = readPastedResult({
|
|
239
|
+
promptCode: options.promptCode,
|
|
240
|
+
signal: pasteAbort.signal,
|
|
241
|
+
serverUrl,
|
|
242
|
+
codeVerifier: verifier,
|
|
243
|
+
fetchImpl,
|
|
244
|
+
onPasteRejected: options.onPasteRejected,
|
|
245
|
+
}).catch((error) => {
|
|
246
|
+
if (!raceSettled && isAbortError(error))
|
|
247
|
+
throw new SignInCancelledError();
|
|
248
|
+
if (!raceSettled) {
|
|
249
|
+
return new Promise(() => { });
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
});
|
|
253
|
+
routes.push(untilSettled(pasted));
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
return await Promise.race(routes);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
raceSettled = true;
|
|
260
|
+
pasteAbort.abort();
|
|
261
|
+
if (timer)
|
|
262
|
+
clearTimeout(timer);
|
|
263
|
+
shutDownServer(server);
|
|
264
|
+
}
|
|
219
265
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_THEGITAI_HOST = 'https://thegit.ai';
|
package/dist/src/api/http.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
|
|
2
|
+
import { DEFAULT_THEGITAI_HOST } from './default-host.js';
|
|
3
3
|
export const TRACE_ID_HEADER = 'x-thegitai-trace-id';
|
|
4
4
|
export const CLIENT_HEADER = 'x-thegitai-client';
|
|
5
5
|
export const CLIENT_PLATFORM_HEADER = 'x-thegitai-client-platform';
|
|
@@ -88,7 +88,7 @@ export async function retryTransient(run, { retries = 2, baseDelayMs = 400, dead
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
export function normalizeServerUrl(serverUrl) {
|
|
91
|
-
const normalized = String(serverUrl ||
|
|
91
|
+
const normalized = String(serverUrl || DEFAULT_THEGITAI_HOST)
|
|
92
92
|
.trim()
|
|
93
93
|
.replace(/\/+$/, '');
|
|
94
94
|
if (!/^https?:\/\//i.test(normalized)) {
|
package/dist/src/cli-args.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
export const AUTH_COMMANDS = new Set(['login', 'whoami', 'logout']);
|
|
2
|
+
function hasPromptWordsAfter(args) {
|
|
3
|
+
for (let i = 1; i < args.length; i++) {
|
|
4
|
+
const arg = args[i];
|
|
5
|
+
if (arg === '--session' || arg === '--resume') {
|
|
6
|
+
i += 1;
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
if (arg.startsWith('-'))
|
|
10
|
+
continue;
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
2
15
|
export function parseArgs(argv) {
|
|
3
16
|
const args = argv.slice(2);
|
|
4
17
|
const firstArg = args[0];
|
|
5
|
-
const command = firstArg && AUTH_COMMANDS.has(firstArg)
|
|
6
|
-
|
|
18
|
+
const command = firstArg && AUTH_COMMANDS.has(firstArg) &&
|
|
19
|
+
(firstArg !== 'login' || !hasPromptWordsAfter(args))
|
|
20
|
+
? firstArg
|
|
21
|
+
: null;
|
|
7
22
|
let autoYes = false;
|
|
8
23
|
let help = false;
|
|
9
24
|
let version = false;
|
|
@@ -12,7 +27,7 @@ export function parseArgs(argv) {
|
|
|
12
27
|
let listSessions = false;
|
|
13
28
|
let unknownOption = null;
|
|
14
29
|
const promptParts = [];
|
|
15
|
-
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
for (let i = command ? 1 : 0; i < args.length; i++) {
|
|
16
31
|
const arg = args[i];
|
|
17
32
|
if (arg === '--yes' || arg === '-y') {
|
|
18
33
|
autoYes = true;
|
|
@@ -39,7 +54,7 @@ export function parseArgs(argv) {
|
|
|
39
54
|
usage = true;
|
|
40
55
|
continue;
|
|
41
56
|
}
|
|
42
|
-
if (
|
|
57
|
+
if (unknownOption === null && /^-/.test(arg)) {
|
|
43
58
|
unknownOption = arg;
|
|
44
59
|
continue;
|
|
45
60
|
}
|
|
@@ -47,7 +62,6 @@ export function parseArgs(argv) {
|
|
|
47
62
|
}
|
|
48
63
|
return {
|
|
49
64
|
command,
|
|
50
|
-
commandArgs,
|
|
51
65
|
autoYes,
|
|
52
66
|
help,
|
|
53
67
|
version,
|
package/dist/src/help-text.js
CHANGED
|
@@ -26,8 +26,12 @@ const HELP_MARKDOWN = [
|
|
|
26
26
|
'',
|
|
27
27
|
'## Auth',
|
|
28
28
|
'',
|
|
29
|
-
'- `ai
|
|
30
|
-
'- `ai login
|
|
29
|
+
'- `ai` — signs you in if you are not already, then starts a session',
|
|
30
|
+
'- `ai login` — the same thing; kept for muscle memory',
|
|
31
|
+
'- Signing in opens your browser. When it cannot open one — over SSH, in a',
|
|
32
|
+
' container, or when the browser you want is on another device — the same',
|
|
33
|
+
' screen prints a URL you can open anywhere and takes the code that page',
|
|
34
|
+
' gives you back. There is no mode to pick and no flag to remember.',
|
|
31
35
|
'- `ai whoami` — show the signed-in account',
|
|
32
36
|
'- `ai --usage` — show account usage percentage and reset times',
|
|
33
37
|
'- `ai logout` — sign out',
|
|
@@ -89,6 +93,7 @@ const HELP_MARKDOWN = [
|
|
|
89
93
|
'- `/jobs output <id>` — print one job\'s full captured output',
|
|
90
94
|
'- `/jobs kill <id>` — stop one background job',
|
|
91
95
|
'- `/new` — start a new conversation; this session remains saved',
|
|
96
|
+
'- `/logout` — sign out and quit',
|
|
92
97
|
'- `/exit` — quit the session',
|
|
93
98
|
'',
|
|
94
99
|
'## Safety & approvals',
|
|
@@ -131,12 +136,14 @@ const HELP_MARKDOWN = [
|
|
|
131
136
|
'',
|
|
132
137
|
'## Troubleshooting',
|
|
133
138
|
'',
|
|
134
|
-
'- "Not logged in" → run `ai login`.',
|
|
135
139
|
'- Auth or permission errors → run `ai whoami` to confirm the signed-in',
|
|
136
140
|
' account.',
|
|
137
141
|
'- Usage or quota errors → run `ai --usage`.',
|
|
138
|
-
'- Signed in with the wrong
|
|
139
|
-
' the account you intended to use.',
|
|
142
|
+
'- Signed in with the wrong account → `/logout` (or `ai logout`), then run',
|
|
143
|
+
' `ai` and sign in as the account you intended to use.',
|
|
144
|
+
'- The browser did not open, or opened on the wrong machine → open the URL',
|
|
145
|
+
" printed on the sign-in screen anywhere you like, choose \"Not on this",
|
|
146
|
+
' machine?" on that page, and paste the code back into the terminal.',
|
|
140
147
|
'- A local session was used with a different sign-in → sign in with the',
|
|
141
148
|
' account you used for that session or start a new session.',
|
|
142
149
|
'- For anything else, re-run the command and report the printed error',
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
+
import readline from 'node:readline/promises';
|
|
3
|
+
import chalk from './colors.js';
|
|
4
|
+
import { auth } from './api/index.js';
|
|
5
|
+
import { DEFAULT_THEGITAI_HOST } from './api/default-host.js';
|
|
6
|
+
import { loginViaBrowser } from './api/browser-login.js';
|
|
7
|
+
function hyperlink(url, label) {
|
|
8
|
+
if (process.stdout.isTTY !== true)
|
|
9
|
+
return label;
|
|
10
|
+
return `\x1b]8;;${url}\x07${label}\x1b]8;;\x07`;
|
|
11
|
+
}
|
|
12
|
+
export function formatSignInScreen({ url }) {
|
|
13
|
+
return [
|
|
14
|
+
'',
|
|
15
|
+
` ${chalk.bold.cyan('TheGitAI')}`,
|
|
16
|
+
'',
|
|
17
|
+
` ${chalk.bold('Sign in to continue.')}`,
|
|
18
|
+
' Your browser should open automatically. If not, copy this URL:',
|
|
19
|
+
'',
|
|
20
|
+
` ${chalk.cyan(url)}`,
|
|
21
|
+
'',
|
|
22
|
+
` ${hyperlink(url, chalk.cyan('→ Click here to authenticate'))}`,
|
|
23
|
+
'',
|
|
24
|
+
chalk.dim(" Signing in on a different machine? Choose \"Not on this machine?\""),
|
|
25
|
+
chalk.dim(' on that page, authorize, and paste the code it gives you below.'),
|
|
26
|
+
'',
|
|
27
|
+
].join('\n');
|
|
28
|
+
}
|
|
29
|
+
async function promptForCode(signal) {
|
|
30
|
+
const rl = readline.createInterface({ input, output });
|
|
31
|
+
try {
|
|
32
|
+
return await rl.question(` ${chalk.bold('Authorization code:')} `, { signal });
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
rl.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function runSignIn({ env = process.env, login = loginViaBrowser, write = auth.writeCliAuthConfig, log = (line) => console.log(line), } = {}) {
|
|
39
|
+
const result = await login({
|
|
40
|
+
serverUrl: DEFAULT_THEGITAI_HOST,
|
|
41
|
+
deviceName: env.THEGITAI_DEVICE_NAME?.trim() || undefined,
|
|
42
|
+
onUrl: (url) => log(formatSignInScreen({ url })),
|
|
43
|
+
onBrowserOpen: (opened) => {
|
|
44
|
+
if (!opened) {
|
|
45
|
+
log(chalk.dim(' (no browser opened — use the URL or the code box above)'));
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
promptCode: promptForCode,
|
|
49
|
+
onPasteRejected: (message) => log(chalk.red(` ✖ ${message}`) + chalk.dim(' Try pasting it again.')),
|
|
50
|
+
});
|
|
51
|
+
write(result, env);
|
|
52
|
+
log('');
|
|
53
|
+
log(chalk.green(` ✓ Signed in as ${result.customer.email}`));
|
|
54
|
+
log('');
|
|
55
|
+
return result;
|
|
56
|
+
}
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -13,7 +13,7 @@ import { setScratchSession } from '../scratch-dir.js';
|
|
|
13
13
|
import { setImageStoreSession } from '../core/session-image-store.js';
|
|
14
14
|
import { cancelActiveCommand } from '../executor.js';
|
|
15
15
|
import { isTurnFailureMarker } from '../turn-failure-marker.js';
|
|
16
|
-
import { clearCliAuthConfig } from '../api/auth.js';
|
|
16
|
+
import { clearCliAuthConfig, logoutFromServer, } from '../api/auth.js';
|
|
17
17
|
import { authenticationErrorMessage, isAuthenticationError, } from '../api/http.js';
|
|
18
18
|
import { createUserInputPromptState, formatUserInputTranscript, } from './tui/user-input.js';
|
|
19
19
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
@@ -105,6 +105,10 @@ export const SLASH_COMMANDS = [
|
|
|
105
105
|
command: '/new',
|
|
106
106
|
description: 'start a new conversation; this session remains saved',
|
|
107
107
|
},
|
|
108
|
+
{
|
|
109
|
+
command: '/logout',
|
|
110
|
+
description: 'Sign out and quit',
|
|
111
|
+
},
|
|
108
112
|
{
|
|
109
113
|
command: '/exit',
|
|
110
114
|
description: 'Quit the current session',
|
|
@@ -1348,6 +1352,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1348
1352
|
throw new Error('stdout is not a terminal');
|
|
1349
1353
|
}
|
|
1350
1354
|
let fatalError = null;
|
|
1355
|
+
let signedOut = false;
|
|
1351
1356
|
await withTuiMode(async () => {
|
|
1352
1357
|
setBackgroundJobSession(session.sessionId);
|
|
1353
1358
|
setScratchSession(session.sessionId);
|
|
@@ -2405,6 +2410,22 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2405
2410
|
requestExit();
|
|
2406
2411
|
return;
|
|
2407
2412
|
}
|
|
2413
|
+
if (input === '/logout') {
|
|
2414
|
+
store.update((current) => ({
|
|
2415
|
+
...current,
|
|
2416
|
+
busy: true,
|
|
2417
|
+
status: 'Signing out...',
|
|
2418
|
+
}));
|
|
2419
|
+
try {
|
|
2420
|
+
await logoutFromServer({ config: authConfig });
|
|
2421
|
+
}
|
|
2422
|
+
catch {
|
|
2423
|
+
}
|
|
2424
|
+
clearCliAuthConfig(session.env);
|
|
2425
|
+
signedOut = true;
|
|
2426
|
+
requestExit();
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2408
2429
|
if (input === '/help') {
|
|
2409
2430
|
appendStaticEntry({
|
|
2410
2431
|
body: formatInteractiveHelpText(),
|
|
@@ -3003,4 +3024,5 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
3003
3024
|
});
|
|
3004
3025
|
if (fatalError)
|
|
3005
3026
|
throw fatalError;
|
|
3027
|
+
return { signedOut };
|
|
3006
3028
|
}
|
|
@@ -181,6 +181,7 @@ const CLIENT_SLASH_COMMANDS = [
|
|
|
181
181
|
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
182
182
|
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
183
183
|
{ command: '/new', description: 'start a new conversation; this session remains saved' },
|
|
184
|
+
{ command: '/logout', description: 'Sign out and quit' },
|
|
184
185
|
{ command: '/exit', description: 'Quit the current session' },
|
|
185
186
|
];
|
|
186
187
|
function buildModelPickerOptions(currentModelId, serverModels) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegitai/cli",
|
|
3
|
-
"version": "1.0.0-preview.
|
|
3
|
+
"version": "1.0.0-preview.27",
|
|
4
4
|
"description": "TheGitAI is an AI coding agent for your terminal. It indexes your repository, writes and edits files, runs commands, and builds features with you.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"@lydell/node-pty-linux-x64": "1.1.0",
|
|
38
38
|
"@lydell/node-pty-win32-arm64": "1.1.0",
|
|
39
39
|
"@lydell/node-pty-win32-x64": "1.1.0",
|
|
40
|
-
"@thegitai/tui-darwin-arm64": "1.0.0-preview.
|
|
41
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
42
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
43
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
40
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.27",
|
|
41
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.27",
|
|
42
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.27",
|
|
43
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.27",
|
|
44
44
|
"@vscode/ripgrep": "1.18.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|