@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 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,
@@ -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
  }
@@ -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
@@ -1,3 +1,7 @@
1
+ export function isAbortError(error) {
2
+ const err = error;
3
+ return err?.name === 'AbortError' || err?.code === 'ABORT_ERR';
4
+ }
1
5
  export function cosineSimilarity(a, b) {
2
6
  let dot = 0;
3
7
  let magA = 0;
@@ -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 UNKNOWN_VERSION = '0.0.0';
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 UNKNOWN_VERSION;
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.43",
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.43",
48
- "@thegitai/tui-darwin-x64": "1.0.0-preview.43",
49
- "@thegitai/tui-linux-arm64": "1.0.0-preview.43",
50
- "@thegitai/tui-linux-x64": "1.0.0-preview.43",
51
- "@thegitai/tui-win32-x64": "1.0.0-preview.43",
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": {