@thegitai/cli 1.0.0-preview.43 → 1.0.0-preview.44
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 -0
- package/dist/bin/ai.js +13 -1
- package/dist/src/api/auth.js +4 -0
- package/dist/src/api/browser-login.js +1 -4
- package/dist/src/help-text.js +13 -0
- package/dist/src/update-check.js +121 -0
- package/dist/src/utils.js +4 -0
- package/dist/src/version.js +52 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -14,6 +14,15 @@ npm i -g @thegitai/cli
|
|
|
14
14
|
|
|
15
15
|
Supports Node.js 22 and newer. Node.js 24 is recommended.
|
|
16
16
|
|
|
17
|
+
## Updates
|
|
18
|
+
|
|
19
|
+
`ai` checks for a newer release each time it starts, and offers to install it
|
|
20
|
+
for you. TheGitAI serves the current release only, so an older CLI can fail
|
|
21
|
+
partway through a session. Press Enter to update — the CLI installs it and asks
|
|
22
|
+
you to start `ai` again, so the whole session runs one version — or choose
|
|
23
|
+
**Skip** to go on with the version you have. Skipping lasts for that session
|
|
24
|
+
only; the next start asks again. Ctrl+C quits without installing anything.
|
|
25
|
+
|
|
17
26
|
## Usage
|
|
18
27
|
|
|
19
28
|
```text
|
package/dist/bin/ai.js
CHANGED
|
@@ -13,7 +13,8 @@ import { startBrowserDiscoveryInBackground } from '../src/browser/session-bridge
|
|
|
13
13
|
import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
|
|
14
14
|
import { formatSessionExitNotice } from '../src/session-exit.js';
|
|
15
15
|
import { formatUsageText } from '../src/usage.js';
|
|
16
|
-
import { formatVersionLine } from '../src/version.js';
|
|
16
|
+
import { getCliVersion, formatVersionLine } from '../src/version.js';
|
|
17
|
+
import { offerUpdateAtStartup } from '../src/update-check.js';
|
|
17
18
|
import { parseArgs } from '../src/cli-args.js';
|
|
18
19
|
const { auth, chat, models, sessions } = ServerApi;
|
|
19
20
|
function printUsage() {
|
|
@@ -220,6 +221,17 @@ export async function main() {
|
|
|
220
221
|
if (offlineNotice) {
|
|
221
222
|
console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${offlineNotice}). Starting with cached settings — it will reconnect on your next message.`));
|
|
222
223
|
}
|
|
224
|
+
const updateOutcome = await offerUpdateAtStartup({
|
|
225
|
+
currentVersion: getCliVersion(),
|
|
226
|
+
latestVersion: whoami.release?.latestVersion ?? null,
|
|
227
|
+
});
|
|
228
|
+
if (updateOutcome === 'updated') {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (updateOutcome === 'cancelled') {
|
|
232
|
+
process.exitCode = 130;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
223
235
|
const selectedModelId = models.selectServerModel({
|
|
224
236
|
requestedModelId: sourceSnapshot?.modelId ?? null,
|
|
225
237
|
cached: cachedModels,
|
package/dist/src/api/auth.js
CHANGED
|
@@ -61,11 +61,15 @@ export async function fetchWhoamiResponse({ config, fetchImpl = globalThis.fetch
|
|
|
61
61
|
if (!data?.customer?.email) {
|
|
62
62
|
throw new Error('Server returned an invalid whoami response.');
|
|
63
63
|
}
|
|
64
|
+
const latestVersion = data.release?.latestVersion;
|
|
64
65
|
return {
|
|
65
66
|
customer: data.customer,
|
|
66
67
|
debugUi: {
|
|
67
68
|
showSessionId: data.debugUi?.showSessionId === true,
|
|
68
69
|
},
|
|
70
|
+
...(typeof latestVersion === 'string' && latestVersion
|
|
71
|
+
? { release: { latestVersion } }
|
|
72
|
+
: {}),
|
|
69
73
|
...(data.usage ? { usage: data.usage } : {}),
|
|
70
74
|
};
|
|
71
75
|
}
|
|
@@ -6,6 +6,7 @@ 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
8
|
import { DEFAULT_THEGITAI_HOST } from './default-host.js';
|
|
9
|
+
import { isAbortError } from '../utils.js';
|
|
9
10
|
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
10
11
|
function shutDownServer(server) {
|
|
11
12
|
server.closeAllConnections?.();
|
|
@@ -20,10 +21,6 @@ export class SignInCancelledError extends Error {
|
|
|
20
21
|
export function isSignInCancelled(error) {
|
|
21
22
|
return error instanceof SignInCancelledError;
|
|
22
23
|
}
|
|
23
|
-
function isAbortError(error) {
|
|
24
|
-
const err = error;
|
|
25
|
-
return err?.name === 'AbortError' || err?.code === 'ABORT_ERR';
|
|
26
|
-
}
|
|
27
24
|
export function resolveWebsiteUrl() {
|
|
28
25
|
return DEFAULT_THEGITAI_HOST.replace(/\/+$/, '');
|
|
29
26
|
}
|
package/dist/src/help-text.js
CHANGED
|
@@ -43,6 +43,19 @@ const HELP_MARKDOWN = [
|
|
|
43
43
|
'- Sessions are stored locally and can be listed or resumed in the same repo.',
|
|
44
44
|
' Continuing one requires the TheGitAI account used for that session.',
|
|
45
45
|
'',
|
|
46
|
+
'## Updates',
|
|
47
|
+
'',
|
|
48
|
+
'- On start, `ai` says when a newer release is published and offers to install',
|
|
49
|
+
' it. TheGitAI serves the current release only, so an older CLI can fail',
|
|
50
|
+
' partway through a session, in ways the transcript cannot explain.',
|
|
51
|
+
'- Enter takes the update; the CLI installs it and asks you to start `ai`',
|
|
52
|
+
' again, so the whole session runs one version.',
|
|
53
|
+
'- Skipping starts the session on the version you have and asks again next',
|
|
54
|
+
' time. A release cannot be silenced for good — that is what keeps a broken',
|
|
55
|
+
' combination from lasting.',
|
|
56
|
+
'- **Ctrl+C** at the prompt quits without installing anything and without',
|
|
57
|
+
' starting the session.',
|
|
58
|
+
'',
|
|
46
59
|
'## Options',
|
|
47
60
|
'',
|
|
48
61
|
'- `-y, --yes` — start in Bypass all permissions mode',
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
+
import readline from 'node:readline/promises';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import chalk from './colors.js';
|
|
7
|
+
import { isAbortError } from './utils.js';
|
|
8
|
+
import { compareCliVersions, UNKNOWN_CLI_VERSION } from './version.js';
|
|
9
|
+
const PACKAGE_NAME = '@thegitai/cli';
|
|
10
|
+
export function resolveInstallCommand(version, moduleDir = path.dirname(fileURLToPath(import.meta.url))) {
|
|
11
|
+
const spec = `${PACKAGE_NAME}@${version}`;
|
|
12
|
+
const location = moduleDir.replace(/\\/g, '/').toLowerCase();
|
|
13
|
+
if (location.includes('/pnpm/global/')) {
|
|
14
|
+
return { program: 'pnpm', args: ['add', '-g', spec] };
|
|
15
|
+
}
|
|
16
|
+
if (location.includes('/.bun/install/global/')) {
|
|
17
|
+
return { program: 'bun', args: ['add', '-g', spec] };
|
|
18
|
+
}
|
|
19
|
+
return { program: 'npm', args: ['install', '-g', spec] };
|
|
20
|
+
}
|
|
21
|
+
export function buildUpdateOffer({ currentVersion, latestVersion, moduleDir, }) {
|
|
22
|
+
if (!latestVersion)
|
|
23
|
+
return null;
|
|
24
|
+
if (currentVersion === UNKNOWN_CLI_VERSION)
|
|
25
|
+
return null;
|
|
26
|
+
const ordering = compareCliVersions(currentVersion, latestVersion);
|
|
27
|
+
if (ordering === null || ordering >= 0)
|
|
28
|
+
return null;
|
|
29
|
+
const { program, args } = resolveInstallCommand(latestVersion, moduleDir);
|
|
30
|
+
return {
|
|
31
|
+
currentVersion,
|
|
32
|
+
latestVersion,
|
|
33
|
+
program,
|
|
34
|
+
args,
|
|
35
|
+
display: [program, ...args].join(' '),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function formatUpdateScreen(offer) {
|
|
39
|
+
return [
|
|
40
|
+
'',
|
|
41
|
+
` ${chalk.yellow('✨ Update available')} — ${chalk.dim(offer.currentVersion)} → ${chalk.bold(offer.latestVersion)}`,
|
|
42
|
+
'',
|
|
43
|
+
' TheGitAI supports the current release only. On an older CLI a session',
|
|
44
|
+
' can fail partway through, in ways this terminal cannot explain.',
|
|
45
|
+
'',
|
|
46
|
+
` ${chalk.cyan('1.')} Update now ${chalk.dim(`(runs \`${offer.display}\`)`)}`,
|
|
47
|
+
` ${chalk.cyan('2.')} Skip ${chalk.dim('(Agent may not work optimally)')}`,
|
|
48
|
+
'',
|
|
49
|
+
` ${chalk.dim('Press Enter to update, or 2 to skip.')}`,
|
|
50
|
+
'',
|
|
51
|
+
].join('\n');
|
|
52
|
+
}
|
|
53
|
+
export function readUpdateChoice(answer) {
|
|
54
|
+
const value = answer.trim().toLowerCase();
|
|
55
|
+
if (value === '' || value === '1' || value === 'y' || value === 'yes') {
|
|
56
|
+
return 'update';
|
|
57
|
+
}
|
|
58
|
+
return 'skip';
|
|
59
|
+
}
|
|
60
|
+
export async function resolveUpdateAnswer(question) {
|
|
61
|
+
try {
|
|
62
|
+
return readUpdateChoice(await question());
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (isAbortError(error))
|
|
66
|
+
return 'cancel';
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function askUpdateChoice() {
|
|
71
|
+
const rl = readline.createInterface({ input, output });
|
|
72
|
+
try {
|
|
73
|
+
return await resolveUpdateAnswer(() => rl.question(' ▸ '));
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
rl.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function runUpdate(offer) {
|
|
80
|
+
return await new Promise((resolve) => {
|
|
81
|
+
const child = spawn(offer.program, offer.args, {
|
|
82
|
+
stdio: 'inherit',
|
|
83
|
+
shell: process.platform === 'win32',
|
|
84
|
+
});
|
|
85
|
+
child.on('error', () => resolve(false));
|
|
86
|
+
child.on('close', (code) => resolve(code === 0));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export async function offerUpdateAtStartup({ currentVersion, latestVersion, ask = askUpdateChoice, install = runUpdate, write = (text) => {
|
|
90
|
+
process.stdout.write(text);
|
|
91
|
+
}, }) {
|
|
92
|
+
const offer = buildUpdateOffer({ currentVersion, latestVersion });
|
|
93
|
+
if (!offer)
|
|
94
|
+
return 'none';
|
|
95
|
+
write(`${formatUpdateScreen(offer)}\n`);
|
|
96
|
+
const answer = await ask();
|
|
97
|
+
if (answer === 'cancel') {
|
|
98
|
+
write(`\n ${chalk.dim('Cancelled. Nothing was installed.')}\n\n`);
|
|
99
|
+
return 'cancelled';
|
|
100
|
+
}
|
|
101
|
+
if (answer === 'skip') {
|
|
102
|
+
write(`\n ${chalk.yellow(`Continuing on ${offer.currentVersion} — you'll be asked again next time.`)}\n\n`);
|
|
103
|
+
return 'skipped';
|
|
104
|
+
}
|
|
105
|
+
write(`\n ${chalk.dim(`$ ${offer.display}`)}\n\n`);
|
|
106
|
+
if (await install(offer)) {
|
|
107
|
+
write(`\n ${chalk.green(`✓ Updated to ${offer.latestVersion}.`)} Run ${chalk.bold('ai')} again to start.\n\n`);
|
|
108
|
+
return 'updated';
|
|
109
|
+
}
|
|
110
|
+
write([
|
|
111
|
+
'',
|
|
112
|
+
` ${chalk.red('✖ The update did not complete.')} Run this yourself, then start ${chalk.bold('ai')} again:`,
|
|
113
|
+
'',
|
|
114
|
+
` ${chalk.bold(offer.display)}`,
|
|
115
|
+
'',
|
|
116
|
+
` ${chalk.dim(`Starting on ${offer.currentVersion} for now.`)}`,
|
|
117
|
+
'',
|
|
118
|
+
'',
|
|
119
|
+
].join('\n'));
|
|
120
|
+
return 'failed';
|
|
121
|
+
}
|
package/dist/src/utils.js
CHANGED
package/dist/src/version.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
const PACKAGE_NAME = '@thegitai/cli';
|
|
5
|
-
const
|
|
5
|
+
export const UNKNOWN_CLI_VERSION = '0.0.0';
|
|
6
6
|
export function getCliVersion() {
|
|
7
7
|
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
for (let depth = 0; depth < 6; depth++) {
|
|
@@ -19,7 +19,7 @@ export function getCliVersion() {
|
|
|
19
19
|
break;
|
|
20
20
|
dir = parent;
|
|
21
21
|
}
|
|
22
|
-
return
|
|
22
|
+
return UNKNOWN_CLI_VERSION;
|
|
23
23
|
}
|
|
24
24
|
export function getPlatformTag() {
|
|
25
25
|
return `${process.platform}-${process.arch}`;
|
|
@@ -27,3 +27,53 @@ export function getPlatformTag() {
|
|
|
27
27
|
export function formatVersionLine() {
|
|
28
28
|
return `ai ${getCliVersion()} (${getPlatformTag()}, node ${process.version})`;
|
|
29
29
|
}
|
|
30
|
+
const VERSION_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
31
|
+
function parseVersion(value) {
|
|
32
|
+
const match = VERSION_PATTERN.exec(String(value ?? '').trim());
|
|
33
|
+
if (!match)
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
37
|
+
prerelease: match[4] ? match[4].split('.') : [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function comparePrereleaseIdentifier(left, right) {
|
|
41
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
42
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
43
|
+
if (leftNumeric && rightNumeric) {
|
|
44
|
+
const a = Number(left);
|
|
45
|
+
const b = Number(right);
|
|
46
|
+
return a === b ? 0 : a < b ? -1 : 1;
|
|
47
|
+
}
|
|
48
|
+
if (leftNumeric !== rightNumeric)
|
|
49
|
+
return leftNumeric ? -1 : 1;
|
|
50
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
51
|
+
}
|
|
52
|
+
export function isReadableCliVersion(value) {
|
|
53
|
+
return parseVersion(value) !== null;
|
|
54
|
+
}
|
|
55
|
+
export function compareCliVersions(left, right) {
|
|
56
|
+
const a = parseVersion(left);
|
|
57
|
+
const b = parseVersion(right);
|
|
58
|
+
if (!a || !b)
|
|
59
|
+
return null;
|
|
60
|
+
for (let index = 0; index < 3; index++) {
|
|
61
|
+
if (a.core[index] !== b.core[index]) {
|
|
62
|
+
return a.core[index] < b.core[index] ? -1 : 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!a.prerelease.length || !b.prerelease.length) {
|
|
66
|
+
if (a.prerelease.length === b.prerelease.length)
|
|
67
|
+
return 0;
|
|
68
|
+
return a.prerelease.length ? -1 : 1;
|
|
69
|
+
}
|
|
70
|
+
const shared = Math.min(a.prerelease.length, b.prerelease.length);
|
|
71
|
+
for (let index = 0; index < shared; index++) {
|
|
72
|
+
const ordering = comparePrereleaseIdentifier(a.prerelease[index], b.prerelease[index]);
|
|
73
|
+
if (ordering !== 0)
|
|
74
|
+
return ordering;
|
|
75
|
+
}
|
|
76
|
+
if (a.prerelease.length === b.prerelease.length)
|
|
77
|
+
return 0;
|
|
78
|
+
return a.prerelease.length < b.prerelease.length ? -1 : 1;
|
|
79
|
+
}
|
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.44",
|
|
4
4
|
"description": "TheGitAI is an agentic AI coding tool for your terminal. It reads and searches your repository, writes and edits files, runs your tests, and verifies the change before handing it back.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentic-ai",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"@lydell/node-pty-linux-x64": "1.1.0",
|
|
45
45
|
"@lydell/node-pty-win32-arm64": "1.1.0",
|
|
46
46
|
"@lydell/node-pty-win32-x64": "1.1.0",
|
|
47
|
-
"@thegitai/tui-darwin-arm64": "1.0.0-preview.
|
|
48
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
49
|
-
"@thegitai/tui-linux-arm64": "1.0.0-preview.
|
|
50
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
51
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
47
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.44",
|
|
48
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.44",
|
|
49
|
+
"@thegitai/tui-linux-arm64": "1.0.0-preview.44",
|
|
50
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.44",
|
|
51
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.44",
|
|
52
52
|
"@vscode/ripgrep": "1.18.0"
|
|
53
53
|
},
|
|
54
54
|
"repository": {
|