jaitus 0.3.0 → 0.3.1
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/package.json +2 -2
- package/src/agent/loop.js +66 -3
- package/src/commands/login.js +50 -32
- package/src/commands/update.js +167 -9
- package/src/errors.js +2 -2
- package/src/index.js +34 -6
- package/src/tui/mouse.js +150 -125
- package/src/tui-ink.js +1998 -1927
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jaitus",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Jaitus — a coding agent in your terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"node": ">=20"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "node tests/source-sanity.js && node tests/rules.js && node tests/run.js && node tests/commands.js && node tests/login.js && node tests/session.js && node tests/openUrl.js && node tests/ink-tui.js && node tests/tui-render.js",
|
|
19
|
+
"test": "node tests/source-sanity.js && node tests/rules.js && node tests/run.js && node tests/commands.js && node tests/login.js && node tests/session.js && node tests/openUrl.js && node tests/ink-tui.js && node tests/tui-render.js && node tests/mouse-e2e.js && node tests/environment.js && node tests/update.js",
|
|
20
20
|
"preview": "node scripts/tui-preview.js",
|
|
21
21
|
"prepublishOnly": "npm test"
|
|
22
22
|
},
|
package/src/agent/loop.js
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
import { byName, toolSchemas, ToolError, readImageForModel } from './tools.js';
|
|
22
22
|
import { PermissionDenied } from './permissions.js';
|
|
23
23
|
import path from 'node:path';
|
|
24
|
-
import
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
import { loadContext, loadIgnore, CONTEXT_FILE } from '../project.js';
|
|
25
27
|
import { c, out, write, spinner, glyph } from '../ui.js';
|
|
26
28
|
import { discoverMcpTools } from '../mcp.js';
|
|
27
29
|
import { unifiedDiff } from '../diff.js';
|
|
@@ -43,6 +45,11 @@ const toolMark = (name) => {
|
|
|
43
45
|
|
|
44
46
|
export const SYSTEM_PROMPT = `You are Jaitus, a coding agent running in a developer's terminal, inside their project directory.
|
|
45
47
|
|
|
48
|
+
You work directly on this computer. Your tools read, search, create and edit the files in
|
|
49
|
+
the project and run shell commands in it, and you can fetch web pages. When the user asks
|
|
50
|
+
about their files, their code or their machine, use the tools and answer from what you
|
|
51
|
+
find — never say you cannot access local files, because you can.
|
|
52
|
+
|
|
46
53
|
How to work:
|
|
47
54
|
- Investigate before you change anything. Read the files you are about to edit; do not guess at their contents.
|
|
48
55
|
- Prefer edit_file over write_file for existing files. Keep changes minimal and in the style of the surrounding code.
|
|
@@ -65,10 +72,66 @@ How to talk:
|
|
|
65
72
|
* Fenced and labelled rather than concatenated, so instructions written by whoever owns
|
|
66
73
|
* the repo are visibly the repo's and cannot be mistaken for part of Jaitus's own rules.
|
|
67
74
|
*/
|
|
75
|
+
/**
|
|
76
|
+
* Where the agent is: the facts it would otherwise guess at, and guess wrong.
|
|
77
|
+
*
|
|
78
|
+
* Without these, the model did not know which folder it was in, which OS its commands
|
|
79
|
+
* would run on (cmd.exe and /bin/sh disagree about almost everything), or today's date —
|
|
80
|
+
* and a model that does not know where it is tends to say it cannot see your files. The
|
|
81
|
+
* listing is the top level only, names not contents, and respects .jaitusignore.
|
|
82
|
+
*/
|
|
83
|
+
export function environmentBlock(cwd, { now = new Date(), platform = process.platform } = {}) {
|
|
84
|
+
if (!cwd) return '';
|
|
85
|
+
const osName = { win32: 'Windows', darwin: 'macOS', linux: 'Linux' }[platform] || platform;
|
|
86
|
+
const shell = platform === 'win32' ? 'cmd.exe (run_command uses it)' : '/bin/sh (run_command uses it)';
|
|
87
|
+
// The repository may start above the working directory, and .git may be a file that
|
|
88
|
+
// points elsewhere (a worktree or submodule).
|
|
89
|
+
let branch = null;
|
|
90
|
+
for (let dir = cwd, i = 0; i < 40; i++) {
|
|
91
|
+
const dotGit = path.join(dir, '.git');
|
|
92
|
+
try {
|
|
93
|
+
let gitDir = dotGit;
|
|
94
|
+
if (fs.statSync(dotGit).isFile()) {
|
|
95
|
+
const m = /^gitdir:\s*(.+)$/m.exec(fs.readFileSync(dotGit, 'utf8'));
|
|
96
|
+
if (m) gitDir = path.resolve(dir, m[1].trim());
|
|
97
|
+
}
|
|
98
|
+
const head = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
|
|
99
|
+
branch = head.startsWith('ref: refs/heads/') ? head.slice(16) : `detached at ${head.slice(0, 7)}`;
|
|
100
|
+
break;
|
|
101
|
+
} catch { /* not here; look one level up */ }
|
|
102
|
+
const parent = path.dirname(dir);
|
|
103
|
+
if (parent === dir) break;
|
|
104
|
+
dir = parent;
|
|
105
|
+
}
|
|
106
|
+
let listing = '';
|
|
107
|
+
try {
|
|
108
|
+
const ignore = loadIgnore(cwd);
|
|
109
|
+
const SKIP = new Set(['.git', 'node_modules']);
|
|
110
|
+
const names = fs.readdirSync(cwd, { withFileTypes: true })
|
|
111
|
+
.filter((d) => !SKIP.has(d.name) && !ignore.matches(d.isDirectory() ? `${d.name}/` : d.name) && !ignore.matches(d.name))
|
|
112
|
+
.map((d) => (d.isDirectory() ? `${d.name}/` : d.name))
|
|
113
|
+
.sort((a, b) => (b.endsWith('/') - a.endsWith('/')) || a.localeCompare(b));
|
|
114
|
+
const MAX = 60;
|
|
115
|
+
listing = names.slice(0, MAX).join('\n') + (names.length > MAX ? `\n… and ${names.length - MAX} more` : '');
|
|
116
|
+
} catch { /* unreadable: the tools will say why */ }
|
|
117
|
+
return `
|
|
118
|
+
|
|
119
|
+
<environment>
|
|
120
|
+
Working directory: ${cwd}
|
|
121
|
+
Platform: ${osName} (${os.release()})
|
|
122
|
+
Shell: ${shell}
|
|
123
|
+
Today: ${now.toISOString().slice(0, 10)}
|
|
124
|
+
Git: ${branch ? `yes, branch ${branch}` : 'no repository at this folder'}
|
|
125
|
+
Top level of the working directory:
|
|
126
|
+
${listing || '(empty)'}
|
|
127
|
+
</environment>`;
|
|
128
|
+
}
|
|
129
|
+
|
|
68
130
|
export function systemPrompt(cwd) {
|
|
69
131
|
const project = cwd ? loadContext(cwd) : null;
|
|
70
|
-
|
|
71
|
-
return
|
|
132
|
+
const base = `${SYSTEM_PROMPT}${environmentBlock(cwd)}`;
|
|
133
|
+
if (!project) return base;
|
|
134
|
+
return `${base}
|
|
72
135
|
|
|
73
136
|
The project you are working in ships a ${CONTEXT_FILE} with notes for you. Follow it where
|
|
74
137
|
it applies, and prefer what it says over your own assumptions about this codebase.
|
package/src/commands/login.js
CHANGED
|
@@ -104,16 +104,15 @@ function awaitCallback(server, expectedState) {
|
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
/**
|
|
108
|
+
* The browser sign-in, with no output of its own.
|
|
109
|
+
*
|
|
110
|
+
* The command below prints it to the terminal; the app view shows the same steps in its
|
|
111
|
+
* transcript. Reports progress through `onUrl` (the link to open, shown in case the
|
|
112
|
+
* browser does not) and `onStatus`. Resolves to { ok: true, user } or { ok: false, message }.
|
|
113
|
+
*/
|
|
114
|
+
export async function signIn({ onUrl = () => {}, onStatus = () => {} } = {}) {
|
|
108
115
|
const { baseUrl, siteUrl } = endpoints();
|
|
109
|
-
|
|
110
|
-
const existing = loadCredentials();
|
|
111
|
-
if (existing?.refreshToken && !argv.includes('--force')) {
|
|
112
|
-
out(`Already signed in as ${c.bold(existing.user?.email || 'your account')}.`);
|
|
113
|
-
const again = await ask(`${c.dim('Sign in again? [y/N] ')}`);
|
|
114
|
-
if (again.toLowerCase() !== 'y') return 0;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
116
|
const state = crypto.randomBytes(24).toString('base64url');
|
|
118
117
|
// PKCE (S256): the challenge goes out through the browser, the verifier stays here and
|
|
119
118
|
// is only ever sent in the final exchange.
|
|
@@ -140,27 +139,18 @@ export default async function login(argv) {
|
|
|
140
139
|
url.searchParams.set('device', device);
|
|
141
140
|
url.searchParams.set('platform', process.platform);
|
|
142
141
|
|
|
143
|
-
|
|
144
|
-
out(` ${c.brand('Jaitus')} — sign in`);
|
|
145
|
-
out('');
|
|
146
|
-
out(` Opening ${c.cyan(siteUrl + '/cli-auth')} in your browser.`);
|
|
147
|
-
out(` ${c.dim('If it does not open, paste this link:')}`);
|
|
148
|
-
out(` ${c.dim(url.toString())}`);
|
|
149
|
-
out('');
|
|
150
|
-
|
|
142
|
+
onUrl(url.toString(), `${siteUrl}/cli-auth`);
|
|
151
143
|
openBrowser(url.toString());
|
|
152
|
-
|
|
144
|
+
onStatus('waiting');
|
|
153
145
|
|
|
154
146
|
let code;
|
|
155
147
|
try {
|
|
156
148
|
({ code } = await awaitCallback(server, state));
|
|
157
149
|
} catch (e) {
|
|
158
|
-
spin.stop();
|
|
159
150
|
server.close();
|
|
160
|
-
|
|
161
|
-
return 1;
|
|
151
|
+
return { ok: false, message: e.message };
|
|
162
152
|
}
|
|
163
|
-
|
|
153
|
+
onStatus('finishing');
|
|
164
154
|
|
|
165
155
|
try {
|
|
166
156
|
const res = await fetch(`${baseUrl}/api/auth/cli/exchange`, {
|
|
@@ -169,13 +159,11 @@ export default async function login(argv) {
|
|
|
169
159
|
body: JSON.stringify({ code, verifier }),
|
|
170
160
|
});
|
|
171
161
|
const body = await res.json().catch(() => ({}));
|
|
172
|
-
spin.stop();
|
|
173
162
|
server.close();
|
|
174
163
|
|
|
175
164
|
if (!res.ok || !body?.data?.accessToken || !body?.data?.refreshToken) {
|
|
176
165
|
// Our own sign-in endpoint's wording, or a plain fallback — never a raw body.
|
|
177
|
-
|
|
178
|
-
return 1;
|
|
166
|
+
return { ok: false, message: typeof body?.message === 'string' && body.message ? body.message : 'Sign-in did not complete. Try again.' };
|
|
179
167
|
}
|
|
180
168
|
|
|
181
169
|
saveCredentials({
|
|
@@ -188,14 +176,44 @@ export default async function login(argv) {
|
|
|
188
176
|
createdAt: new Date().toISOString(),
|
|
189
177
|
});
|
|
190
178
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
info(`Run ${c.bold('jaitus')} in a project to start, or ${c.bold('jaitus --help')} to see everything.`);
|
|
194
|
-
return 0;
|
|
195
|
-
} catch (e) {
|
|
196
|
-
spin.stop();
|
|
179
|
+
return { ok: true, user: body.data.user || null };
|
|
180
|
+
} catch {
|
|
197
181
|
server.close();
|
|
198
|
-
|
|
182
|
+
return { ok: false, message: 'Could not reach Jaitus. Check your connection and try again.' };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export default async function login(argv, { then = 'hint' } = {}) {
|
|
187
|
+
const existing = loadCredentials();
|
|
188
|
+
if (existing?.refreshToken && !argv.includes('--force')) {
|
|
189
|
+
out(`Already signed in as ${c.bold(existing.user?.email || 'your account')}.`);
|
|
190
|
+
const again = await ask(`${c.dim('Sign in again? [y/N] ')}`);
|
|
191
|
+
if (again.toLowerCase() !== 'y') return 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let spin = null;
|
|
195
|
+
const result = await signIn({
|
|
196
|
+
onUrl: (url, shown) => {
|
|
197
|
+
out('');
|
|
198
|
+
out(` ${c.brand('Jaitus')} — sign in`);
|
|
199
|
+
out('');
|
|
200
|
+
out(` Opening ${c.cyan(shown)} in your browser.`);
|
|
201
|
+
out(` ${c.dim('If it does not open, paste this link:')}`);
|
|
202
|
+
out(` ${c.dim(url)}`);
|
|
203
|
+
out('');
|
|
204
|
+
},
|
|
205
|
+
onStatus: (s) => {
|
|
206
|
+
if (s === 'waiting') spin = spinner(glyph('Waiting for you to authorise in the browser…', 'Waiting for you to authorise in the browser...'));
|
|
207
|
+
else spin?.update(glyph('Finishing sign-in…', 'Finishing sign-in...'));
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
spin?.stop();
|
|
211
|
+
if (!result.ok) {
|
|
212
|
+
fail(`${result.message}${/login/.test(result.message) ? '' : ` Run ${c.bold('jaitus login')} to try again.`}`);
|
|
199
213
|
return 1;
|
|
200
214
|
}
|
|
215
|
+
const who = result.user;
|
|
216
|
+
ok(`Signed in as ${c.bold(who?.email || 'your account')}${who?.isPaid ? '' : c.dim(' (free plan)')}`);
|
|
217
|
+
if (then === 'hint') info(`Run ${c.bold('jaitus')} in a project to start, or ${c.bold('jaitus --help')} to see everything.`);
|
|
218
|
+
return 0;
|
|
201
219
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
// ─── jaitus update ──────────────────────────────────────────────────────────────
|
|
2
2
|
//
|
|
3
|
-
// Checks the registry and
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Checks the registry, and installs the new version when it can tell HOW this copy was
|
|
4
|
+
// installed: a global npm install is upgraded with the same npm, after asking. Anything
|
|
5
|
+
// else — npx, a version manager, a copy vendored into a repo — gets the command to run
|
|
6
|
+
// instead, because a tool that guesses wrong there either fails confusingly or modifies
|
|
7
|
+
// something it was never asked to modify.
|
|
8
|
+
//
|
|
9
|
+
// A global install never updates itself, so the app also checks once a day, in the
|
|
10
|
+
// background, and says when a newer version is out (the update-notifier pattern npm and
|
|
11
|
+
// gh use). JAITUS_NO_UPDATE_CHECK=1, NO_UPDATE_NOTIFIER or CI turns that off.
|
|
7
12
|
|
|
8
|
-
import
|
|
9
|
-
import
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { c, out, ok, info, warn, fail, present, askKey } from '../ui.js';
|
|
18
|
+
import { version, HOME } from '../config.js';
|
|
10
19
|
|
|
11
20
|
const REGISTRY = process.env.JAITUS_REGISTRY || 'https://registry.npmjs.org';
|
|
12
21
|
const TIMEOUT_MS = 5000;
|
|
@@ -46,6 +55,74 @@ export async function latestVersion() {
|
|
|
46
55
|
return (await checkRegistry()).version;
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
const CHECK_FILE = path.join(HOME, 'update-check.json');
|
|
59
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
60
|
+
|
|
61
|
+
/** Whether the background check is allowed to run at all. */
|
|
62
|
+
export function updateCheckEnabled(env = process.env) {
|
|
63
|
+
return !(env.JAITUS_NO_UPDATE_CHECK || env.NO_UPDATE_NOTIFIER || env.CI);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The newest published version, asked of the registry at most once a day.
|
|
68
|
+
*
|
|
69
|
+
* Cached so opening the app is never slowed by, or dependent on, the network: a failed
|
|
70
|
+
* check is remembered too, and simply tried again tomorrow.
|
|
71
|
+
*/
|
|
72
|
+
export async function cachedLatest({ now = Date.now(), file = CHECK_FILE, check = checkRegistry } = {}) {
|
|
73
|
+
try {
|
|
74
|
+
const cached = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
75
|
+
if (cached && now - Number(cached.checkedAt) < DAY_MS) return cached.latest || null;
|
|
76
|
+
} catch { /* first run, or unreadable: ask */ }
|
|
77
|
+
const { version: latest } = await check();
|
|
78
|
+
try {
|
|
79
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
80
|
+
fs.writeFileSync(file, JSON.stringify({ checkedAt: now, latest: latest || null }));
|
|
81
|
+
} catch { /* read-only home: check again next time */ }
|
|
82
|
+
return latest || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** One line for the app to show when a newer version is out, or null. */
|
|
86
|
+
export async function updateNotice(opts = {}) {
|
|
87
|
+
if (!updateCheckEnabled(opts.env)) return null;
|
|
88
|
+
const current = opts.current || version();
|
|
89
|
+
const latest = await cachedLatest(opts).catch(() => null);
|
|
90
|
+
if (!latest || !isNewer(latest, current)) return null;
|
|
91
|
+
return `Jaitus v${latest} is out — you have v${current}. Quit and run \`jaitus update\` to install it.`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function run(cmd, args, { capture = false } = {}) {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
// npm is npm.cmd on Windows, which spawn cannot start without a shell. The arguments
|
|
97
|
+
// here are fixed strings (a version number from the registry at most), so they are
|
|
98
|
+
// joined into one command line rather than passed alongside shell: true, which Node
|
|
99
|
+
// deprecates because it does not escape them.
|
|
100
|
+
const stdio = capture ? ['ignore', 'pipe', 'ignore'] : 'inherit';
|
|
101
|
+
const child = process.platform === 'win32'
|
|
102
|
+
? spawn([cmd, ...args].join(' '), { shell: true, stdio })
|
|
103
|
+
: spawn(cmd, args, { stdio });
|
|
104
|
+
let stdout = '';
|
|
105
|
+
child.stdout?.on('data', (d) => { stdout += d; });
|
|
106
|
+
child.on('error', () => resolve({ code: -1, stdout }));
|
|
107
|
+
child.on('close', (code) => resolve({ code, stdout }));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* How this copy was installed: 'npm-global', 'npx', or 'other'.
|
|
113
|
+
*
|
|
114
|
+
* Decided by where this file actually lives, compared with npm's own global folder —
|
|
115
|
+
* not by guessing from environment variables.
|
|
116
|
+
*/
|
|
117
|
+
export async function installMethod({ here = fileURLToPath(import.meta.url), npmRoot = null } = {}) {
|
|
118
|
+
const real = (() => { try { return fs.realpathSync(here); } catch { return here; } })();
|
|
119
|
+
const norm = (p) => path.resolve(p).toLowerCase();
|
|
120
|
+
if (/[\\/]_npx[\\/]/.test(real)) return 'npx';
|
|
121
|
+
const root = npmRoot ?? (await run('npm', ['root', '-g'], { capture: true })).stdout.trim();
|
|
122
|
+
if (root && norm(real).startsWith(norm(path.join(root, 'jaitus')) + path.sep.toLowerCase())) return 'npm-global';
|
|
123
|
+
return 'other';
|
|
124
|
+
}
|
|
125
|
+
|
|
49
126
|
/**
|
|
50
127
|
* Plain semver comparison — is `a` newer than `b`?
|
|
51
128
|
* Prerelease tags are treated as older than the release they lead to, which is what
|
|
@@ -67,7 +144,61 @@ export function isNewer(a, b) {
|
|
|
67
144
|
return false;
|
|
68
145
|
}
|
|
69
146
|
|
|
70
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Update on launch, then reopen: every time `jaitus` starts, the registry is asked
|
|
149
|
+
* (with a short timeout, so a slow network costs at most that); when a newer version is
|
|
150
|
+
* out and this copy is a global npm install, it is installed and the same command is
|
|
151
|
+
* started again on the new version.
|
|
152
|
+
*
|
|
153
|
+
* It never blocks the session on a failure. A failed install (commonly: a system-wide
|
|
154
|
+
* Node on macOS or Linux, where npm -g needs sudo) says so once and is not retried for a
|
|
155
|
+
* day, so a machine that cannot update does not pay for trying on every start.
|
|
156
|
+
*
|
|
157
|
+
* Off with `jaitus config autoUpdate off`, JAITUS_NO_AUTOUPDATE=1, NO_UPDATE_NOTIFIER or CI.
|
|
158
|
+
* Returns the child's exit code when it relaunched, or null to carry on in this process.
|
|
159
|
+
*/
|
|
160
|
+
export async function autoUpdate({ argv = [], settings = {}, env = process.env, current = version(), log = out,
|
|
161
|
+
check = () => checkRegistry(), method = () => installMethod(), install = (v) => run('npm', ['install', '-g', `jaitus@${v}`], { capture: true }),
|
|
162
|
+
relaunch = defaultRelaunch, now = Date.now(), failFile = path.join(HOME, 'update-failed.json') } = {}) {
|
|
163
|
+
if (env.JAITUS_UPDATED_FROM) return null; // just relaunched: never loop
|
|
164
|
+
if (!updateCheckEnabled(env) || env.JAITUS_NO_AUTOUPDATE || settings.autoUpdate === false) return null;
|
|
165
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) return null; // scripts and pipes get a stable version
|
|
166
|
+
const first = argv.find((a) => !a.startsWith('-'));
|
|
167
|
+
if (['update', 'completion', 'help', 'logout'].includes(first) || argv.some((a) => ['--version', '-v', '--help', '-h', '--print', '-p'].includes(a))) return null;
|
|
168
|
+
try {
|
|
169
|
+
const failed = JSON.parse(fs.readFileSync(failFile, 'utf8'));
|
|
170
|
+
if (now - Number(failed.at) < DAY_MS) return null;
|
|
171
|
+
} catch { /* no recent failure */ }
|
|
172
|
+
|
|
173
|
+
const { version: latest } = await Promise.race([
|
|
174
|
+
check(),
|
|
175
|
+
// unref: a quick command must not sit waiting for this timer after the answer came.
|
|
176
|
+
new Promise((resolve) => setTimeout(() => resolve({ version: null }), 1500).unref()),
|
|
177
|
+
]);
|
|
178
|
+
if (!latest || !isNewer(latest, current)) return null;
|
|
179
|
+
if ((await method()) !== 'npm-global') return null; // the in-app notice covers these
|
|
180
|
+
|
|
181
|
+
log(` ${c.brand('Jaitus')} ${c.dim(`v${current} →`)} ${c.bold(`v${latest}`)} ${c.dim('— updating…')}`);
|
|
182
|
+
const { code } = await install(latest);
|
|
183
|
+
if (code !== 0) {
|
|
184
|
+
try { fs.mkdirSync(path.dirname(failFile), { recursive: true }); fs.writeFileSync(failFile, JSON.stringify({ at: now, version: latest })); } catch { /* best effort */ }
|
|
185
|
+
log(` ${c.yellow('!')} ${c.dim('Could not update automatically — continuing on')} v${current}${c.dim('. To update:')} ${c.bold(process.platform === 'win32' ? 'npm install -g jaitus@latest' : 'sudo npm install -g jaitus@latest')}`);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
log(` ${c.accent('✓')} ${c.dim('Updated — reopening')}`);
|
|
189
|
+
return relaunch({ ...env, JAITUS_UPDATED_FROM: current });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Start the same command again on the freshly installed version, in this terminal. */
|
|
193
|
+
function defaultRelaunch(env) {
|
|
194
|
+
return new Promise((resolve) => {
|
|
195
|
+
const child = spawn(process.execPath, [...process.execArgv, process.argv[1], ...process.argv.slice(2)], { stdio: 'inherit', env });
|
|
196
|
+
child.on('error', () => resolve(null));
|
|
197
|
+
child.on('exit', (code, signal) => resolve(signal ? 1 : (code ?? 0)));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default async function update(argv = [], { json = false, interactive = true } = {}) {
|
|
71
202
|
const current = version();
|
|
72
203
|
const { version: latest, status } = await checkRegistry();
|
|
73
204
|
|
|
@@ -89,9 +220,36 @@ export default async function update(argv = [], { json = false } = {}) {
|
|
|
89
220
|
out('');
|
|
90
221
|
out(` ${c.bold(`v${latest}`)} is available ${c.dim(`— you have v${current}`)}`);
|
|
91
222
|
out('');
|
|
92
|
-
|
|
223
|
+
});
|
|
224
|
+
if (!available || json) return 0;
|
|
225
|
+
|
|
226
|
+
const manual = () => {
|
|
227
|
+
out(` ${c.dim('global install')} ${c.bold('npm install -g jaitus@latest')}`);
|
|
93
228
|
out(` ${c.dim('no install')} ${c.bold('npx jaitus@latest')}`);
|
|
94
229
|
out('');
|
|
95
|
-
}
|
|
230
|
+
};
|
|
231
|
+
// Inside the app the running copy cannot be swapped out from under itself.
|
|
232
|
+
if (!interactive) { info('Quit Jaitus, then run it from your shell:'); manual(); return 0; }
|
|
233
|
+
|
|
234
|
+
const method = await installMethod();
|
|
235
|
+
if (method !== 'npm-global') {
|
|
236
|
+
info(method === 'npx' ? 'You are running it through npx — use @latest to get the new version:' : 'This copy was not installed with npm -g, so update it the way you installed it, or:');
|
|
237
|
+
manual();
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
const yes = argv.includes('--yes') || argv.includes('-y');
|
|
241
|
+
if (!yes) {
|
|
242
|
+
if (!process.stdin.isTTY) { manual(); return 0; }
|
|
243
|
+
const answer = await askKey(` Install v${latest} now? ${c.dim('[Y/n]')} `, ['y', 'n', '\r', '\n']);
|
|
244
|
+
if (answer === 'n') { manual(); return 0; }
|
|
245
|
+
}
|
|
246
|
+
out(` ${c.dim('running')} npm install -g jaitus@${latest}`);
|
|
247
|
+
const { code } = await run('npm', ['install', '-g', `jaitus@${latest}`]);
|
|
248
|
+
if (code !== 0) {
|
|
249
|
+
fail(`npm could not install it (exit ${code}). Try it yourself — on macOS or Linux it may need sudo, or a Node version manager:`);
|
|
250
|
+
manual();
|
|
251
|
+
return 1;
|
|
252
|
+
}
|
|
253
|
+
ok(`Updated to v${latest}. Start ${c.bold('jaitus')} again to use it.`);
|
|
96
254
|
return 0;
|
|
97
255
|
}
|
package/src/errors.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// recognise, because the only text that is safe to show is text we wrote.
|
|
9
9
|
|
|
10
10
|
const CATALOG = {
|
|
11
|
-
auth_required: { title: 'Not signed in', hint: '
|
|
12
|
-
session_expired: { title: 'Signed out', hint: '
|
|
11
|
+
auth_required: { title: 'Not signed in', hint: 'Type /login to sign in (or run `jaitus login`).' },
|
|
12
|
+
session_expired: { title: 'Signed out', hint: 'Type /login to sign in again (or run `jaitus login`).' },
|
|
13
13
|
account_suspended: { title: 'Account suspended', hint: 'Contact support if you think this is a mistake.' },
|
|
14
14
|
email_unverified: { title: 'Email not verified', hint: 'Open the verification link in your inbox, then retry.' },
|
|
15
15
|
free_limit_reached: { title: 'Free prompts used up', hint: 'Subscribe on the website, then retry this turn.' },
|
package/src/index.js
CHANGED
|
@@ -41,7 +41,7 @@ const createTui = process.env.JAITUS_RENDERER === 'classic' ? createClassicTui :
|
|
|
41
41
|
import login from './commands/login.js';
|
|
42
42
|
import logout from './commands/logout.js';
|
|
43
43
|
import doctor from './commands/doctor.js';
|
|
44
|
-
import update from './commands/update.js';
|
|
44
|
+
import update, { updateNotice, autoUpdate } from './commands/update.js';
|
|
45
45
|
import init from './commands/init.js';
|
|
46
46
|
import research from './commands/research.js';
|
|
47
47
|
import explain from './commands/explain.js';
|
|
@@ -522,6 +522,12 @@ export function ensureUtf8Codepage() {
|
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
async function session({ flags }) {
|
|
525
|
+
// Signed out: sign in here, in this window, and go straight on into the session —
|
|
526
|
+
// rather than stopping to tell someone to run another command first.
|
|
527
|
+
if (!signedIn() && process.stdin.isTTY && process.stdout.isTTY) {
|
|
528
|
+
const code = await login([], { then: 'continue' });
|
|
529
|
+
if (code !== 0) return code;
|
|
530
|
+
}
|
|
525
531
|
const client = requireAuth();
|
|
526
532
|
// Classic conhost speaks an OEM codepage by default, so every ›•─│ the CLI
|
|
527
533
|
// paints arrives as mojibake (the whole UI looks broken, not old). One
|
|
@@ -1039,6 +1045,9 @@ async function session({ flags }) {
|
|
|
1039
1045
|
: true;
|
|
1040
1046
|
if (process.stdout.isTTY && wantFullscreen) {
|
|
1041
1047
|
tui.start({ onLine: handleLine, history: sessionHistory() });
|
|
1048
|
+
// A global install never updates itself; say so when there is something newer.
|
|
1049
|
+
// Background and cached for a day — the app never waits on the network for this.
|
|
1050
|
+
void updateNotice().then((line) => { if (line) tui.notice(line, 'info'); }).catch(() => {});
|
|
1042
1051
|
// Fresh sessions open on the home moment, opencode-style: the centered
|
|
1043
1052
|
// two-tone mark, then a tip — the composer below is the whole welcome.
|
|
1044
1053
|
// The mark is vertically centered in the free space above the pinned
|
|
@@ -1992,9 +2001,24 @@ async function slashCommand(verb, args, ctx) {
|
|
|
1992
2001
|
return null;
|
|
1993
2002
|
}
|
|
1994
2003
|
|
|
1995
|
-
case '/login':
|
|
1996
|
-
|
|
2004
|
+
case '/login': {
|
|
2005
|
+
// In the app view the steps go to the transcript; the session carries on after,
|
|
2006
|
+
// on the new account — every request reads the saved sign-in fresh.
|
|
2007
|
+
const t = ctx.tui;
|
|
2008
|
+
if (!t) { await login(['--force'], { then: 'none' }); return null; }
|
|
2009
|
+
const { signIn } = await import('./commands/login.js');
|
|
2010
|
+
const result = await signIn({
|
|
2011
|
+
onUrl: (url) => {
|
|
2012
|
+
t.notice('Opening your browser to sign in. If it does not open, paste this link:', 'info');
|
|
2013
|
+
t.notice(url, 'dim');
|
|
2014
|
+
},
|
|
2015
|
+
onStatus: (s) => t.setStatus(s === 'waiting' ? 'Waiting for you to approve in the browser… (Esc does not cancel; close the tab to give up)' : 'Finishing sign-in…'),
|
|
2016
|
+
});
|
|
2017
|
+
t.clearStatus?.();
|
|
2018
|
+
if (result.ok) t.notice(`Signed in as ${result.user?.email || 'your account'}${result.user?.isPaid ? '' : ' (free plan)'}.`, 'ok');
|
|
2019
|
+
else t.notice(result.message, 'warn');
|
|
1997
2020
|
return null;
|
|
2021
|
+
}
|
|
1998
2022
|
|
|
1999
2023
|
case '/logout': {
|
|
2000
2024
|
const { default: logout } = await import('./commands/logout.js');
|
|
@@ -2031,7 +2055,7 @@ async function slashCommand(verb, args, ctx) {
|
|
|
2031
2055
|
return null;
|
|
2032
2056
|
|
|
2033
2057
|
case '/update':
|
|
2034
|
-
await
|
|
2058
|
+
await update([], { json: false, interactive: false });
|
|
2035
2059
|
return null;
|
|
2036
2060
|
|
|
2037
2061
|
case '/banner': {
|
|
@@ -2288,7 +2312,7 @@ const commands = {
|
|
|
2288
2312
|
run: (argv, { json }) => {
|
|
2289
2313
|
const [key, value] = argv;
|
|
2290
2314
|
const settings = loadSettings();
|
|
2291
|
-
const ONOFF = ['copyOnSelect', 'wheelAccel', 'autoScroll', 'animations', 'autoCompact', 'mouse'];
|
|
2315
|
+
const ONOFF = ['copyOnSelect', 'wheelAccel', 'autoScroll', 'animations', 'autoCompact', 'mouse', 'autoUpdate'];
|
|
2292
2316
|
const SECRET = ['braveSearchKey', 'tavilyKey', 'serperKey'];
|
|
2293
2317
|
if (!key) {
|
|
2294
2318
|
const e = endpoints();
|
|
@@ -2321,7 +2345,7 @@ const commands = {
|
|
|
2321
2345
|
['Auto-scroll', data.autoScroll ? 'on' : 'off'],
|
|
2322
2346
|
['Animations', data.animations ? 'on' : 'off'],
|
|
2323
2347
|
['Auto-compact', data.autoCompact ? 'on (summarises near the context limit)' : 'off'],
|
|
2324
|
-
['Mouse', data.mouse ? 'on (wheel scrolls, clicks act
|
|
2348
|
+
['Mouse', data.mouse ? 'on (wheel scrolls, clicks act, drag or double-click copies)' : 'off (the terminal selects text itself)'],
|
|
2325
2349
|
['Search API', data.searchApi ? `${data.searchApi} (set)` : 'none — using free engines + npm/GitHub/MDN/Stack Overflow'],
|
|
2326
2350
|
['Status line', data.statusLine || '(unset — set a command to render one)'],
|
|
2327
2351
|
['Config', data.configDir],
|
|
@@ -2456,6 +2480,10 @@ export default async function main(argv) {
|
|
|
2456
2480
|
|
|
2457
2481
|
if (flags.version) { out(VERSION); return 0; }
|
|
2458
2482
|
|
|
2483
|
+
// Always on the newest version: install it and reopen before doing anything else.
|
|
2484
|
+
const relaunched = await autoUpdate({ argv, settings: loadSettings() }).catch(() => null);
|
|
2485
|
+
if (relaunched !== null) return relaunched;
|
|
2486
|
+
|
|
2459
2487
|
const [first, ...others] = rest;
|
|
2460
2488
|
const cmd = commands[first];
|
|
2461
2489
|
|