@thegitai/cli 1.0.0-preview.42 → 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 +19 -0
- package/dist/src/preferences.js +39 -0
- package/dist/src/ui/repl.js +75 -1
- package/dist/src/ui/tui/attention.js +25 -0
- package/dist/src/ui/tui/bridge.js +31 -5
- package/dist/src/ui/tui/build-frame.js +21 -1
- package/dist/src/ui/tui/desktop-notification.js +118 -0
- package/dist/src/ui/tui/shell-input.js +4 -0
- package/dist/src/ui/tui/shortcuts.js +1 -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',
|
|
@@ -77,6 +90,11 @@ const HELP_MARKDOWN = [
|
|
|
77
90
|
' back for editing and **Esc**/**Ctrl+C** discards it; both unlock the',
|
|
78
91
|
' composer for the next message. **Ctrl+V** pastes a screenshot onto the',
|
|
79
92
|
' queued message, and it is delivered into the running turn with the text.',
|
|
93
|
+
'- **Ctrl+G** turns desktop notifications on or off. One is raised when a turn',
|
|
94
|
+
' ends and when a permission prompt or a question is waiting for you, but',
|
|
95
|
+
' only while the terminal is in the background — if you are looking at the',
|
|
96
|
+
' session you are told nothing. Coming back clears it. The choice is',
|
|
97
|
+
' remembered between sessions.',
|
|
80
98
|
`- **Paste** into the composer with your terminal's paste shortcut (\`${PASTE_SHORTCUT}\``,
|
|
81
99
|
' on this system) or by right-clicking the composer.',
|
|
82
100
|
'- **Copy** from the transcript by dragging to select; double-click copies a',
|
|
@@ -101,6 +119,7 @@ const HELP_MARKDOWN = [
|
|
|
101
119
|
' is working; press it again or Esc to close without touching the turn',
|
|
102
120
|
'- `/jobs output <id>` — print one job\'s full captured output',
|
|
103
121
|
'- `/jobs kill <id>` — stop one background job',
|
|
122
|
+
'- `/notify` — turn desktop notifications on or off (same as Ctrl+G)',
|
|
104
123
|
'- `/new` — start a new conversation; this session remains saved',
|
|
105
124
|
'- `/logout` — sign out and quit',
|
|
106
125
|
'- `/exit` — quit the session',
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getClientStateDir } from './client-state.js';
|
|
4
|
+
export const DEFAULT_PREFERENCES = {
|
|
5
|
+
notifications: true,
|
|
6
|
+
};
|
|
7
|
+
export function getPreferencesPath(env = process.env) {
|
|
8
|
+
return path.join(getClientStateDir(env), 'prefs.json');
|
|
9
|
+
}
|
|
10
|
+
export function readPreferences(env = process.env) {
|
|
11
|
+
const filePath = getPreferencesPath(env);
|
|
12
|
+
if (!existsSync(filePath))
|
|
13
|
+
return { ...DEFAULT_PREFERENCES };
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
16
|
+
return {
|
|
17
|
+
notifications: parsed.notifications === false ? false : DEFAULT_PREFERENCES.notifications,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { ...DEFAULT_PREFERENCES };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function writePreferences(preferences, env = process.env) {
|
|
25
|
+
const filePath = getPreferencesPath(env);
|
|
26
|
+
mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
27
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
28
|
+
try {
|
|
29
|
+
writeFileSync(tempPath, `${JSON.stringify(preferences, null, 2)}\n`, {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
mode: 0o600,
|
|
32
|
+
});
|
|
33
|
+
renameSync(tempPath, filePath);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
rmSync(tempPath, { force: true });
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
1
2
|
import { createRatatuiBridge } from './tui/bridge.js';
|
|
2
3
|
import { bucketActionLabel, } from '../permissions.js';
|
|
3
4
|
import { AGENT_MODE_LOCKED_STATUS, approvalScrollLimit, buildTuiFrame, formatJobElapsed, formatTodoProgress, pickThinkingFallbackPhrase, renderTranscriptEntryLines, THINKING_FALLBACK_PHRASES, userInputViewportForFrame, } from './tui/build-frame.js';
|
|
5
|
+
import { createAttentionController, INITIAL_TERMINAL_FOCUS, } from './tui/attention.js';
|
|
6
|
+
import { backendFor, createNotificationQueue, } from './tui/desktop-notification.js';
|
|
7
|
+
import { readPreferences, writePreferences } from '../preferences.js';
|
|
4
8
|
import { createTerminalTitleController } from './tui/terminal-title.js';
|
|
5
9
|
import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
|
|
6
10
|
export { getSlashCommandSuggestions } from './tui/build-frame.js';
|
|
@@ -135,6 +139,10 @@ export const SLASH_COMMANDS = [
|
|
|
135
139
|
command: '/jobs',
|
|
136
140
|
description: 'Background jobs: pick to view output or kill',
|
|
137
141
|
},
|
|
142
|
+
{
|
|
143
|
+
command: '/notify',
|
|
144
|
+
description: 'Toggle desktop notifications when a turn ends (Ctrl+G)',
|
|
145
|
+
},
|
|
138
146
|
{
|
|
139
147
|
command: '/new',
|
|
140
148
|
description: 'start a new conversation; this session remains saved',
|
|
@@ -1100,6 +1108,8 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
1100
1108
|
exiting: false,
|
|
1101
1109
|
input: '',
|
|
1102
1110
|
maxToolSteps: session.maxToolSteps,
|
|
1111
|
+
notificationsEnabled: readPreferences(session.env).notifications,
|
|
1112
|
+
terminalFocused: INITIAL_TERMINAL_FOCUS,
|
|
1103
1113
|
jobsPickerExpandedId: null,
|
|
1104
1114
|
jobsPickerIndex: 0,
|
|
1105
1115
|
jobsPickerOpen: false,
|
|
@@ -1557,11 +1567,42 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1557
1567
|
busy: state.busy,
|
|
1558
1568
|
});
|
|
1559
1569
|
};
|
|
1570
|
+
const notifier = backendFor();
|
|
1571
|
+
let notificationHandle = null;
|
|
1572
|
+
const projectLabel = basename(session.rootDir) || 'TheGitAI';
|
|
1573
|
+
const notifications = createNotificationQueue();
|
|
1574
|
+
const attention = createAttentionController({
|
|
1575
|
+
notify: (kind) => {
|
|
1576
|
+
notifications.run(async () => {
|
|
1577
|
+
notificationHandle = await notifier.post({
|
|
1578
|
+
title: projectLabel,
|
|
1579
|
+
body: kind === 'blocked'
|
|
1580
|
+
? 'Waiting for your answer'
|
|
1581
|
+
: 'Finished \u2014 your turn',
|
|
1582
|
+
}, notificationHandle);
|
|
1583
|
+
});
|
|
1584
|
+
},
|
|
1585
|
+
withdraw: () => {
|
|
1586
|
+
notifications.run(async () => {
|
|
1587
|
+
notifier.withdraw(notificationHandle);
|
|
1588
|
+
notificationHandle = null;
|
|
1589
|
+
});
|
|
1590
|
+
},
|
|
1591
|
+
});
|
|
1592
|
+
const syncAttention = () => {
|
|
1593
|
+
const state = store.getState();
|
|
1594
|
+
attention.sync({
|
|
1595
|
+
awaitingAnswer: Boolean(state.approvalPrompt || state.sudoPrompt || state.userInputPrompt),
|
|
1596
|
+
busy: state.busy,
|
|
1597
|
+
focused: state.terminalFocused,
|
|
1598
|
+
}, state.notificationsEnabled);
|
|
1599
|
+
};
|
|
1560
1600
|
const renderCurrentFrame = () => {
|
|
1561
1601
|
if (!tuiReady)
|
|
1562
1602
|
return;
|
|
1563
1603
|
const state = store.getState();
|
|
1564
1604
|
syncTerminalTitle();
|
|
1605
|
+
syncAttention();
|
|
1565
1606
|
const elapsedSeconds = busyElapsedSeconds(state, Date.now());
|
|
1566
1607
|
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1567
1608
|
};
|
|
@@ -2107,6 +2148,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2107
2148
|
title: 'Model',
|
|
2108
2149
|
});
|
|
2109
2150
|
};
|
|
2151
|
+
const toggleNotifications = () => {
|
|
2152
|
+
const enabled = !store.getState().notificationsEnabled;
|
|
2153
|
+
store.update((current) => ({ ...current, notificationsEnabled: enabled }));
|
|
2154
|
+
try {
|
|
2155
|
+
writePreferences({ ...readPreferences(session.env), notifications: enabled }, session.env);
|
|
2156
|
+
}
|
|
2157
|
+
catch {
|
|
2158
|
+
}
|
|
2159
|
+
showTransientStatus(enabled ? 'Notifications on' : 'Notifications off');
|
|
2160
|
+
};
|
|
2110
2161
|
const openJobsPicker = () => {
|
|
2111
2162
|
if (store.getState().jobsPickerOpen)
|
|
2112
2163
|
return;
|
|
@@ -2513,8 +2564,19 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2513
2564
|
openJobsPicker();
|
|
2514
2565
|
return;
|
|
2515
2566
|
}
|
|
2567
|
+
if (input === '/notify') {
|
|
2568
|
+
store.update((current) => ({
|
|
2569
|
+
...current,
|
|
2570
|
+
cursor: 0,
|
|
2571
|
+
input: '',
|
|
2572
|
+
pastedChunks: [],
|
|
2573
|
+
}));
|
|
2574
|
+
toggleNotifications();
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2516
2577
|
appendTurnAwareEntry({
|
|
2517
|
-
body: "Slash commands can't be queued while a turn is running.
|
|
2578
|
+
body: "Slash commands can't be queued while a turn is running. " +
|
|
2579
|
+
'Ctrl+B opens background jobs, Ctrl+G toggles notifications.',
|
|
2518
2580
|
kind: 'system',
|
|
2519
2581
|
title: 'Queued',
|
|
2520
2582
|
});
|
|
@@ -2599,6 +2661,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2599
2661
|
appendStaticEntries(buildReportTranscriptEntries(opened));
|
|
2600
2662
|
return;
|
|
2601
2663
|
}
|
|
2664
|
+
if (input === '/notify') {
|
|
2665
|
+
toggleNotifications();
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2602
2668
|
if (input === '/jobs' || input.startsWith('/jobs ')) {
|
|
2603
2669
|
const jobsArgs = input.slice('/jobs'.length).trim();
|
|
2604
2670
|
if (!jobsArgs) {
|
|
@@ -3124,6 +3190,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
3124
3190
|
onSelectionCopy: handleAppSelectionCopy,
|
|
3125
3191
|
onSelectModel: handleInlineModelSelection,
|
|
3126
3192
|
onOpenJobsPicker: openJobsPicker,
|
|
3193
|
+
onToggleNotifications: toggleNotifications,
|
|
3127
3194
|
onJobsPickerOutput: handleJobsPickerOutput,
|
|
3128
3195
|
onJobsPickerKill: handleJobsPickerKill,
|
|
3129
3196
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
@@ -3167,6 +3234,13 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
3167
3234
|
return;
|
|
3168
3235
|
}
|
|
3169
3236
|
if (message.op === 'event') {
|
|
3237
|
+
if (message.kind === 'focus') {
|
|
3238
|
+
store.update((current) => current.terminalFocused === message.focused
|
|
3239
|
+
? current
|
|
3240
|
+
: { ...current, terminalFocused: message.focused });
|
|
3241
|
+
renderCurrentFrame();
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3170
3244
|
if (message.kind === 'resize') {
|
|
3171
3245
|
terminalCols = message.cols;
|
|
3172
3246
|
terminalRows = message.rows;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const INITIAL_TERMINAL_FOCUS = true;
|
|
2
|
+
export function createAttentionController(options) {
|
|
3
|
+
let previous = null;
|
|
4
|
+
let outstanding = false;
|
|
5
|
+
return {
|
|
6
|
+
sync(state, enabled) {
|
|
7
|
+
const last = previous;
|
|
8
|
+
previous = { ...state };
|
|
9
|
+
if (last && !last.focused && state.focused && outstanding) {
|
|
10
|
+
outstanding = false;
|
|
11
|
+
options.withdraw();
|
|
12
|
+
}
|
|
13
|
+
if (!last || !enabled)
|
|
14
|
+
return;
|
|
15
|
+
if (state.focused)
|
|
16
|
+
return;
|
|
17
|
+
const turnFinished = last.busy && !state.busy;
|
|
18
|
+
const nowBlocked = !last.awaitingAnswer && state.awaitingAnswer;
|
|
19
|
+
if (nowBlocked || turnFinished) {
|
|
20
|
+
outstanding = true;
|
|
21
|
+
options.notify(nowBlocked ? 'blocked' : 'finished');
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { existsSync, statSync } from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -51,6 +51,9 @@ function normalizeChildMessage(raw) {
|
|
|
51
51
|
url: String(raw.url ?? ''),
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
if (raw.kind === 'focus') {
|
|
55
|
+
return { op: 'event', kind: 'focus', focused: raw.focused === true };
|
|
56
|
+
}
|
|
54
57
|
if (raw.kind === 'contextMenu') {
|
|
55
58
|
return { op: 'event', kind: 'contextMenu' };
|
|
56
59
|
}
|
|
@@ -95,13 +98,18 @@ function normalizeChildMessage(raw) {
|
|
|
95
98
|
pageDown: bool(raw.pageDown ?? raw.page_down),
|
|
96
99
|
};
|
|
97
100
|
}
|
|
101
|
+
function isRepoCheckout(moduleDir) {
|
|
102
|
+
return [
|
|
103
|
+
path.join(moduleDir, '../../../../thegitai-tui/Cargo.toml'),
|
|
104
|
+
path.join(moduleDir, '../../../../../thegitai-tui/Cargo.toml'),
|
|
105
|
+
].some((candidate) => existsSync(candidate));
|
|
106
|
+
}
|
|
98
107
|
export function resolveTuiBinaryPath() {
|
|
99
108
|
const binaryName = process.platform === 'win32' ? 'thegitai-tui.exe' : 'thegitai-tui';
|
|
100
109
|
const platformPackage = `@thegitai/tui-${process.platform}-${process.arch}`;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
catch {
|
|
110
|
+
const override = String(process.env.THEGITAI_TUI_BINARY ?? '').trim();
|
|
111
|
+
if (override && existsSync(override)) {
|
|
112
|
+
return override;
|
|
105
113
|
}
|
|
106
114
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
107
115
|
const devCandidates = [
|
|
@@ -109,6 +117,24 @@ export function resolveTuiBinaryPath() {
|
|
|
109
117
|
path.join(moduleDir, '../../../../bin', binaryName),
|
|
110
118
|
];
|
|
111
119
|
const devBinary = devCandidates.find((candidate) => existsSync(candidate));
|
|
120
|
+
let publishedBinary = null;
|
|
121
|
+
try {
|
|
122
|
+
publishedBinary = requireFromHere.resolve(`${platformPackage}/${binaryName}`);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
}
|
|
126
|
+
if (publishedBinary && devBinary && isRepoCheckout(moduleDir)) {
|
|
127
|
+
try {
|
|
128
|
+
if (statSync(devBinary).mtimeMs > statSync(publishedBinary).mtimeMs) {
|
|
129
|
+
return devBinary;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (publishedBinary) {
|
|
136
|
+
return publishedBinary;
|
|
137
|
+
}
|
|
112
138
|
if (devBinary) {
|
|
113
139
|
return devBinary;
|
|
114
140
|
}
|
|
@@ -32,6 +32,16 @@ const COMPOSER_INPUT_MAX_ROWS = 6;
|
|
|
32
32
|
const FOOTER_HINT_MIN_GAP = 2;
|
|
33
33
|
const FOOTER_HELP_HINT = '? for shortcuts';
|
|
34
34
|
const FOOTER_CANCEL_HINT = 'esc cancels turn';
|
|
35
|
+
const FOOTER_NOTIFICATIONS_ON = 'Notifications ON';
|
|
36
|
+
const FOOTER_NOTIFICATIONS_OFF = 'Notifications OFF';
|
|
37
|
+
export function footerNotificationHint(status) {
|
|
38
|
+
const text = String(status ?? '').trim();
|
|
39
|
+
if (text === 'Notifications on')
|
|
40
|
+
return FOOTER_NOTIFICATIONS_ON;
|
|
41
|
+
if (text === 'Notifications off')
|
|
42
|
+
return FOOTER_NOTIFICATIONS_OFF;
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
35
45
|
const FOOTER_CYCLE_HINT = ' (shift+tab to cycle)';
|
|
36
46
|
const OVERLAY_PANEL_MAX_WIDTH = 86;
|
|
37
47
|
const OVERLAY_PANEL_MARGIN_LINES = 2;
|
|
@@ -180,10 +190,12 @@ export function formatPromptSessionIdLabel(showSessionId, sessionId) {
|
|
|
180
190
|
}
|
|
181
191
|
const CLIENT_SLASH_COMMANDS = [
|
|
182
192
|
{ command: '/help', description: 'Show available chat commands' },
|
|
193
|
+
{ command: '/about', description: 'Show version and platform info' },
|
|
183
194
|
{ command: '/usage', description: 'Show account usage percentage and reset times' },
|
|
184
195
|
{ command: '/model', description: 'Switch the active model' },
|
|
185
196
|
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
186
197
|
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
198
|
+
{ command: '/notify', description: 'Toggle desktop notifications when a turn ends (Ctrl+G)' },
|
|
187
199
|
{ command: '/report', description: 'Report a bug privately on the TheGitAI website' },
|
|
188
200
|
{ command: '/new', description: 'start a new conversation; this session remains saved' },
|
|
189
201
|
{ command: '/logout', description: 'Sign out and quit' },
|
|
@@ -491,7 +503,15 @@ function composerFooterRightHints(state) {
|
|
|
491
503
|
? FOOTER_CANCEL_HINT
|
|
492
504
|
: '';
|
|
493
505
|
const helpHint = shortcutsPanelAvailable(state) ? FOOTER_HELP_HINT : '';
|
|
506
|
+
const notifyHint = footerNotificationHint(state.transientStatus);
|
|
507
|
+
const join = (...parts) => parts.filter(Boolean).join(' · ');
|
|
494
508
|
const tiers = [];
|
|
509
|
+
if (notifyHint) {
|
|
510
|
+
tiers.push(join(cancelHint, notifyHint, helpHint));
|
|
511
|
+
tiers.push(join(notifyHint, helpHint));
|
|
512
|
+
tiers.push(join(cancelHint, notifyHint));
|
|
513
|
+
tiers.push(notifyHint);
|
|
514
|
+
}
|
|
495
515
|
if (cancelHint && helpHint)
|
|
496
516
|
tiers.push(`${cancelHint} · ${helpHint}`);
|
|
497
517
|
if (cancelHint)
|
|
@@ -499,7 +519,7 @@ function composerFooterRightHints(state) {
|
|
|
499
519
|
if (helpHint)
|
|
500
520
|
tiers.push(helpHint);
|
|
501
521
|
tiers.push('');
|
|
502
|
-
return tiers;
|
|
522
|
+
return tiers.filter((tier, index) => tiers.indexOf(tier) === index);
|
|
503
523
|
}
|
|
504
524
|
export function fitComposerFooterRow({ modeLabels, modeLabelFallback, cycleHint, rightHints, leadWidth, tailWidth, contentWidth, }) {
|
|
505
525
|
const candidates = rightHints.flatMap((rightHint) => (cycleHint ? [cycleHint, ''] : ['']).flatMap((cycle) => modeLabels.map((modeLabel) => ({ modeLabel, cycleHint: cycle, rightHint }))));
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
function run(command, args, { capture = false } = {}) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
let child;
|
|
5
|
+
try {
|
|
6
|
+
child = spawn(command, args, {
|
|
7
|
+
stdio: ['ignore', capture ? 'pipe' : 'ignore', 'ignore'],
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
resolve('');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
let out = '';
|
|
15
|
+
child.stdout?.on('data', (chunk) => {
|
|
16
|
+
out += chunk.toString('utf8');
|
|
17
|
+
});
|
|
18
|
+
child.on('error', () => resolve(''));
|
|
19
|
+
child.on('close', () => resolve(out.trim()));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const linuxBackend = {
|
|
23
|
+
canWithdraw: true,
|
|
24
|
+
async post(request, previous) {
|
|
25
|
+
const args = [
|
|
26
|
+
'--print-id',
|
|
27
|
+
'--app-name=TheGitAI',
|
|
28
|
+
'--icon=utilities-terminal',
|
|
29
|
+
'--urgency=normal',
|
|
30
|
+
];
|
|
31
|
+
if (previous)
|
|
32
|
+
args.push(`--replace-id=${previous}`);
|
|
33
|
+
args.push(request.title, request.body);
|
|
34
|
+
const id = await run('notify-send', args, { capture: true });
|
|
35
|
+
return /^\d+$/.test(id) ? id : null;
|
|
36
|
+
},
|
|
37
|
+
withdraw(handle) {
|
|
38
|
+
if (!handle)
|
|
39
|
+
return;
|
|
40
|
+
void run('gdbus', [
|
|
41
|
+
'call',
|
|
42
|
+
'--session',
|
|
43
|
+
'--dest',
|
|
44
|
+
'org.freedesktop.Notifications',
|
|
45
|
+
'--object-path',
|
|
46
|
+
'/org/freedesktop/Notifications',
|
|
47
|
+
'--method',
|
|
48
|
+
'org.freedesktop.Notifications.CloseNotification',
|
|
49
|
+
handle,
|
|
50
|
+
]);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
function appleScriptString(value) {
|
|
54
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
55
|
+
}
|
|
56
|
+
const macBackend = {
|
|
57
|
+
canWithdraw: false,
|
|
58
|
+
async post(request) {
|
|
59
|
+
await run('osascript', [
|
|
60
|
+
'-e',
|
|
61
|
+
`display notification "${appleScriptString(request.body)}" with title "${appleScriptString(request.title)}"`,
|
|
62
|
+
]);
|
|
63
|
+
return null;
|
|
64
|
+
},
|
|
65
|
+
withdraw() {
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
function powerShellString(value) {
|
|
69
|
+
return value.replace(/'/g, "''");
|
|
70
|
+
}
|
|
71
|
+
const WINDOWS_AUMID = 'TheGitAI';
|
|
72
|
+
const WINDOWS_TAG = 'thegitai-attention';
|
|
73
|
+
const WINDOWS_GROUP = 'thegitai';
|
|
74
|
+
const windowsBackend = {
|
|
75
|
+
canWithdraw: true,
|
|
76
|
+
async post(request) {
|
|
77
|
+
const script = [
|
|
78
|
+
'[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] > $null',
|
|
79
|
+
'[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType=WindowsRuntime] > $null',
|
|
80
|
+
'$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)',
|
|
81
|
+
'$n = $t.GetElementsByTagName("text")',
|
|
82
|
+
`$n.Item(0).AppendChild($t.CreateTextNode('${powerShellString(request.title)}')) > $null`,
|
|
83
|
+
`$n.Item(1).AppendChild($t.CreateTextNode('${powerShellString(request.body)}')) > $null`,
|
|
84
|
+
'$toast = [Windows.UI.Notifications.ToastNotification]::new($t)',
|
|
85
|
+
`$toast.Tag = '${WINDOWS_TAG}'`,
|
|
86
|
+
`$toast.Group = '${WINDOWS_GROUP}'`,
|
|
87
|
+
`[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${WINDOWS_AUMID}').Show($toast)`,
|
|
88
|
+
].join('; ');
|
|
89
|
+
await run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
90
|
+
return WINDOWS_TAG;
|
|
91
|
+
},
|
|
92
|
+
withdraw(handle) {
|
|
93
|
+
if (!handle)
|
|
94
|
+
return;
|
|
95
|
+
const script = [
|
|
96
|
+
'[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] > $null',
|
|
97
|
+
`[Windows.UI.Notifications.ToastNotificationManager]::History.Remove('${powerShellString(handle)}', '${WINDOWS_GROUP}', '${WINDOWS_AUMID}')`,
|
|
98
|
+
].join('; ');
|
|
99
|
+
void run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
export function createNotificationQueue() {
|
|
103
|
+
let tail = Promise.resolve();
|
|
104
|
+
return {
|
|
105
|
+
idle: () => tail,
|
|
106
|
+
run(step) {
|
|
107
|
+
tail = tail.then(step).catch(() => {
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function backendFor(platform = process.platform) {
|
|
113
|
+
if (platform === 'darwin')
|
|
114
|
+
return macBackend;
|
|
115
|
+
if (platform === 'win32')
|
|
116
|
+
return windowsBackend;
|
|
117
|
+
return linuxBackend;
|
|
118
|
+
}
|
|
@@ -429,6 +429,10 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
429
429
|
}
|
|
430
430
|
return;
|
|
431
431
|
}
|
|
432
|
+
if (key.ctrl && key.input === 'g' && !key.meta && !key.shift) {
|
|
433
|
+
handlers.onToggleNotifications?.();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
432
436
|
if (key.input === '?' && !key.ctrl && !key.meta && shortcutsPanelAvailable(state)) {
|
|
433
437
|
handlers.onLiveFrameShapeChange();
|
|
434
438
|
store.update((current) => ({ ...current, shortcutsOpen: true }));
|
|
@@ -26,6 +26,7 @@ export function buildShortcutGroups(platform = process.platform) {
|
|
|
26
26
|
{ keys: '↑ / ↓', description: 'Earlier prompts' },
|
|
27
27
|
{ keys: 'Esc', description: 'Clear the composer' },
|
|
28
28
|
{ keys: 'Ctrl+C', description: 'Clear the composer, or quit when empty' },
|
|
29
|
+
{ keys: 'Ctrl+G', description: 'Desktop notifications — toggle on or off' },
|
|
29
30
|
],
|
|
30
31
|
},
|
|
31
32
|
{
|
|
@@ -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": {
|