@thegitai/cli 1.0.0-preview.43 → 1.0.0-preview.45
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 +17 -2
- 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 +25 -0
- package/dist/src/ui/tui/build-frame.js +8 -4
- package/dist/src/ui/tui/user-input.js +38 -13
- 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
|
|
@@ -69,12 +78,18 @@ The agent can pause its current turn to ask up to four related questions in one
|
|
|
69
78
|
form. Use **↑ / ↓**, the displayed option number, or **Enter** to choose, and
|
|
70
79
|
**← / →** to move between questions. **Something else / add details** is the
|
|
71
80
|
numbered final option; focus it and type, paste, or press **Enter** to add a
|
|
72
|
-
single-line note
|
|
73
|
-
|
|
81
|
+
single-line note; **Enter** commits it and moves on the way any other choice
|
|
82
|
+
does — to the next question, or to **Submit answers** on the last one — without
|
|
83
|
+
submitting the form. On single-select questions it is mutually exclusive with
|
|
84
|
+
the listed choices, and **↑ / ↓** leaves its editor when the note is empty.
|
|
74
85
|
Multi-select questions toggle choices. The last question has an explicit
|
|
75
86
|
**Submit answers** row. **Esc** closes an open note first and otherwise cancels
|
|
76
87
|
the form; **Ctrl+C** cancels the whole turn.
|
|
77
88
|
|
|
89
|
+
Only the option you are on shows its description, so the whole form — every
|
|
90
|
+
option, the note row, **Submit answers**, and the key hints — stays on screen
|
|
91
|
+
without scrolling. Move between options to read the others.
|
|
92
|
+
|
|
78
93
|
Answers return to the same turn, and completed question/answer records remain
|
|
79
94
|
readable when the session is resumed. Default, Bypass all permissions, and Plan
|
|
80
95
|
modes all support the form; Bypass all permissions does not choose answers for
|
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',
|
|
@@ -90,6 +103,18 @@ const HELP_MARKDOWN = [
|
|
|
90
103
|
? ['- On Mac laptops without dedicated keys, **Home/End** are `Fn+←` / `Fn+→`.']
|
|
91
104
|
: []),
|
|
92
105
|
'',
|
|
106
|
+
'## Questions',
|
|
107
|
+
'',
|
|
108
|
+
'- The agent can pause a turn to ask up to four questions in one form.',
|
|
109
|
+
' **↑/↓** moves, the option number or **Enter** chooses, **←/→** moves',
|
|
110
|
+
' between questions, and the last question has a **Submit answers** row.',
|
|
111
|
+
'- Only the option you are on shows its description, so the whole form fits',
|
|
112
|
+
' on screen. Move to an option to read what it means.',
|
|
113
|
+
'- **Something else / add details** is the numbered final option: focus it',
|
|
114
|
+
' and type. **Enter** commits the note and moves to the next question, or to',
|
|
115
|
+
' Submit on the last one — it never submits the form for you.',
|
|
116
|
+
'- **Esc** closes an open note first, and otherwise cancels the form.',
|
|
117
|
+
'',
|
|
93
118
|
'## Chat commands',
|
|
94
119
|
'',
|
|
95
120
|
'- `/help` — show this help',
|
|
@@ -44,6 +44,7 @@ export function footerNotificationHint(status) {
|
|
|
44
44
|
}
|
|
45
45
|
const FOOTER_CYCLE_HINT = ' (shift+tab to cycle)';
|
|
46
46
|
const OVERLAY_PANEL_MAX_WIDTH = 86;
|
|
47
|
+
const USER_INPUT_PANEL_MAX_WIDTH = 120;
|
|
47
48
|
const OVERLAY_PANEL_MARGIN_LINES = 2;
|
|
48
49
|
const OVERLAY_PANEL_MARGIN_MIN_ROWS = 30;
|
|
49
50
|
const OVERLAY_BORDER_COLOR = 'yellow';
|
|
@@ -817,8 +818,8 @@ function overlayPanelContentBudget(height) {
|
|
|
817
818
|
const margins = overlayPanelMarginLineCount(height) * 2;
|
|
818
819
|
return Math.max(1, Math.floor(height) - margins - 2);
|
|
819
820
|
}
|
|
820
|
-
function buildOverlayPanel(rows, width, color, height = Number.POSITIVE_INFINITY) {
|
|
821
|
-
const panelWidth = Math.max(24, Math.min(width,
|
|
821
|
+
function buildOverlayPanel(rows, width, color, height = Number.POSITIVE_INFINITY, maxWidth = OVERLAY_PANEL_MAX_WIDTH) {
|
|
822
|
+
const panelWidth = Math.max(24, Math.min(width, maxWidth));
|
|
822
823
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
823
824
|
const margin = Array.from({
|
|
824
825
|
length: overlayPanelMarginLineCount(height),
|
|
@@ -1104,6 +1105,9 @@ export function buildWorkingClockLine(state, elapsedSeconds) {
|
|
|
1104
1105
|
export function approvalPanelInnerWidth(width) {
|
|
1105
1106
|
return Math.max(1, Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH)) - 4);
|
|
1106
1107
|
}
|
|
1108
|
+
export function userInputPanelInnerWidth(width) {
|
|
1109
|
+
return Math.max(1, Math.max(24, Math.min(width, USER_INPUT_PANEL_MAX_WIDTH)) - 4);
|
|
1110
|
+
}
|
|
1107
1111
|
export function approvalPaddingEnabled(height) {
|
|
1108
1112
|
return height >= APPROVAL_PADDING_MIN_ROWS;
|
|
1109
1113
|
}
|
|
@@ -1212,7 +1216,7 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
1212
1216
|
if (height < 3) {
|
|
1213
1217
|
return [];
|
|
1214
1218
|
}
|
|
1215
|
-
return buildOverlayPanel(buildUserInputOverlayLines(state.userInputPrompt,
|
|
1219
|
+
return buildOverlayPanel(buildUserInputOverlayLines(state.userInputPrompt, userInputPanelInnerWidth(width), overlayPanelContentBudget(height)), width, USER_INPUT_BORDER_COLOR, height, USER_INPUT_PANEL_MAX_WIDTH);
|
|
1216
1220
|
}
|
|
1217
1221
|
if (state.approvalPrompt) {
|
|
1218
1222
|
const prompt = state.approvalPrompt;
|
|
@@ -1337,7 +1341,7 @@ export function userInputViewportForFrame(state, cols, rows, spinnerFrame, elaps
|
|
|
1337
1341
|
const liveRows = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs).length;
|
|
1338
1342
|
const overlayHeight = Math.max(0, rows - liveRows - composerFooterLines(state, contentWidth).length);
|
|
1339
1343
|
return {
|
|
1340
|
-
width:
|
|
1344
|
+
width: userInputPanelInnerWidth(contentWidth),
|
|
1341
1345
|
maxRows: overlayPanelContentBudget(overlayHeight),
|
|
1342
1346
|
};
|
|
1343
1347
|
}
|
|
@@ -139,9 +139,19 @@ function editOpenNote(state, event) {
|
|
|
139
139
|
const question = currentQuestion(state);
|
|
140
140
|
const current = state.customText[question.id] ?? '';
|
|
141
141
|
const cursor = Math.max(0, Math.min(state.noteCursor, current.length));
|
|
142
|
-
if (event.escape
|
|
142
|
+
if (event.escape) {
|
|
143
143
|
return { ...state, noteOpen: false };
|
|
144
144
|
}
|
|
145
|
+
if (event.returnKey) {
|
|
146
|
+
const closed = { ...state, noteOpen: false };
|
|
147
|
+
if (question.multiSelect || !current.trim()) {
|
|
148
|
+
return closed;
|
|
149
|
+
}
|
|
150
|
+
const index = questionIndex(closed);
|
|
151
|
+
return index < closed.questions.length - 1
|
|
152
|
+
? setQuestion(closed, index + 1)
|
|
153
|
+
: setCursor(closed, question, SUBMIT_CURSOR);
|
|
154
|
+
}
|
|
145
155
|
if (event.tab)
|
|
146
156
|
return state;
|
|
147
157
|
if ((event.upArrow || event.downArrow) && !current.trim()) {
|
|
@@ -433,8 +443,10 @@ function buildUserInputContent(state, width) {
|
|
|
433
443
|
}), ...(recommendation
|
|
434
444
|
? [span(recommendation, { color: 'green' })]
|
|
435
445
|
: [])));
|
|
436
|
-
|
|
437
|
-
|
|
446
|
+
if (focused) {
|
|
447
|
+
for (const description of wrapText(option.description, Math.max(8, width - 5))) {
|
|
448
|
+
lines.push(plainLine(` ${description}`, { color: 'gray' }));
|
|
449
|
+
}
|
|
438
450
|
}
|
|
439
451
|
if (state.contentScrollAnchor === option.id) {
|
|
440
452
|
anchorRange = { start: blockStart, end: lines.length - 1 };
|
|
@@ -490,23 +502,38 @@ function buildUserInputContent(state, width) {
|
|
|
490
502
|
if (state.validationMessage) {
|
|
491
503
|
lines.push(plainLine(state.validationMessage, { color: 'yellow' }));
|
|
492
504
|
}
|
|
493
|
-
|
|
494
|
-
? 'Type a single-line note • ↑/↓ moves when empty • Enter saves • Esc closes
|
|
505
|
+
const footer = plainLine(state.noteOpen
|
|
506
|
+
? 'Type a single-line note • ↑/↓ moves when empty • Enter saves • Esc closes'
|
|
495
507
|
: question.multiSelect
|
|
496
|
-
? `↑/↓ move • 1–${otherNumber} choose • Enter toggles/edits • ←/→ questions •
|
|
497
|
-
: `↑/↓ move • 1–${otherNumber} choose • Enter selects/edits • ←/→ questions •
|
|
508
|
+
? `↑/↓ move • 1–${otherNumber} choose • Enter toggles/edits • ←/→ questions • Esc cancels`
|
|
509
|
+
: `↑/↓ move • 1–${otherNumber} choose • Enter selects/edits • ←/→ questions • Esc cancels`, { color: 'gray' });
|
|
498
510
|
return {
|
|
499
511
|
anchorRange,
|
|
512
|
+
footer: clipLineToWidth(footer, width),
|
|
500
513
|
lines: lines.map((row) => clipLineToWidth(row, width)),
|
|
501
514
|
};
|
|
502
515
|
}
|
|
503
516
|
function buildUserInputWindow(state, width, maxRows) {
|
|
504
517
|
const content = buildUserInputContent(state, width);
|
|
505
|
-
if (!Number.isFinite(maxRows)
|
|
506
|
-
return {
|
|
518
|
+
if (!Number.isFinite(maxRows)) {
|
|
519
|
+
return {
|
|
520
|
+
lines: [...content.lines, content.footer],
|
|
521
|
+
maxOffset: 0,
|
|
522
|
+
offset: 0,
|
|
523
|
+
};
|
|
507
524
|
}
|
|
508
525
|
const rowBudget = Math.max(1, Math.floor(maxRows));
|
|
509
|
-
|
|
526
|
+
if (rowBudget === 1) {
|
|
527
|
+
return { lines: [content.footer], maxOffset: 0, offset: 0 };
|
|
528
|
+
}
|
|
529
|
+
if (content.lines.length <= rowBudget - 1) {
|
|
530
|
+
return {
|
|
531
|
+
lines: [...content.lines, content.footer],
|
|
532
|
+
maxOffset: 0,
|
|
533
|
+
offset: 0,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const visibleRowBudget = Math.max(1, rowBudget - 2);
|
|
510
537
|
const maxOffset = Math.max(0, content.lines.length - visibleRowBudget);
|
|
511
538
|
let offset = Math.max(0, Math.min(state.contentScrollOffset, maxOffset));
|
|
512
539
|
const range = content.anchorRange;
|
|
@@ -527,13 +554,11 @@ function buildUserInputWindow(state, width, maxRows) {
|
|
|
527
554
|
offset = Math.max(0, Math.min(offset, maxOffset));
|
|
528
555
|
}
|
|
529
556
|
const visible = content.lines.slice(offset, offset + visibleRowBudget);
|
|
530
|
-
if (rowBudget === 1) {
|
|
531
|
-
return { lines: visible, maxOffset, offset };
|
|
532
|
-
}
|
|
533
557
|
return {
|
|
534
558
|
lines: [
|
|
535
559
|
...visible,
|
|
536
560
|
plainLine(fitText(`Rows ${offset + 1}–${offset + visible.length} of ${content.lines.length} · PgUp/PgDn scroll`, width), { color: 'gray' }),
|
|
561
|
+
content.footer,
|
|
537
562
|
],
|
|
538
563
|
maxOffset,
|
|
539
564
|
offset,
|
|
@@ -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.45",
|
|
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.45",
|
|
48
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.45",
|
|
49
|
+
"@thegitai/tui-linux-arm64": "1.0.0-preview.45",
|
|
50
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.45",
|
|
51
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.45",
|
|
52
52
|
"@vscode/ripgrep": "1.18.0"
|
|
53
53
|
},
|
|
54
54
|
"repository": {
|