ucode-agent 1.61.1 → 1.62.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.
@@ -1,34 +1,34 @@
1
- # Third-party notices
2
-
3
- ucode adapts logic from the following open-source project.
4
-
5
- ## opencode
6
-
7
- Used in `src/tools/fuzzy.js` (the fallback matchers an edit tries when its
8
- text is not in the file as written), `src/core/window.js` (the anchored
9
- summary template), and `src/core/provider.js` (the context-overflow and
10
- transient-error patterns, and tool-name repair).
11
-
12
- ```
13
- MIT License
14
-
15
- Copyright (c) 2025 opencode
16
-
17
- Permission is hereby granted, free of charge, to any person obtaining a copy
18
- of this software and associated documentation files (the "Software"), to deal
19
- in the Software without restriction, including without limitation the rights
20
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
- copies of the Software, and to permit persons to whom the Software is
22
- furnished to do so, subject to the following conditions:
23
-
24
- The above copyright notice and this permission notice shall be included in all
25
- copies or substantial portions of the Software.
26
-
27
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
- SOFTWARE.
34
- ```
1
+ # Third-party notices
2
+
3
+ ucode adapts logic from the following open-source project.
4
+
5
+ ## opencode
6
+
7
+ Used in `src/tools/fuzzy.js` (the fallback matchers an edit tries when its
8
+ text is not in the file as written), `src/core/window.js` (the anchored
9
+ summary template), and `src/core/provider.js` (the context-overflow and
10
+ transient-error patterns, and tool-name repair).
11
+
12
+ ```
13
+ MIT License
14
+
15
+ Copyright (c) 2025 opencode
16
+
17
+ Permission is hereby granted, free of charge, to any person obtaining a copy
18
+ of this software and associated documentation files (the "Software"), to deal
19
+ in the Software without restriction, including without limitation the rights
20
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
+ copies of the Software, and to permit persons to whom the Software is
22
+ furnished to do so, subject to the following conditions:
23
+
24
+ The above copyright notice and this permission notice shall be included in all
25
+ copies or substantial portions of the Software.
26
+
27
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
+ SOFTWARE.
34
+ ```
package/package.json CHANGED
@@ -1,64 +1,64 @@
1
- {
2
- "name": "ucode-agent",
3
- "version": "1.61.1",
4
- "description": "ucode - a terminal coding agent that reads, edits and runs your code, on Google Gemini models.",
5
- "type": "module",
6
- "main": "ucode.js",
7
- "bin": {
8
- "ucode": "ucode.js"
9
- },
10
- "files": [
11
- "ucode.js",
12
- "src/",
13
- "skills/",
14
- "templates/",
15
- "THIRD_PARTY_NOTICES.md"
16
- ],
17
- "scripts": {
18
- "start": "node ucode.js",
19
- "test": "node test/run.js",
20
- "prepublishOnly": "node scripts/no-bundled-key.js && node test/run.js",
21
- "hooks": "node scripts/install-hooks.js"
22
- },
23
- "repository": {
24
- "type": "git",
25
- "url": "git+https://github.com/sppideey/ucode-agent.git"
26
- },
27
- "bugs": {
28
- "url": "https://github.com/sppideey/ucode-agent/issues"
29
- },
30
- "homepage": "https://github.com/sppideey/ucode-agent#readme",
31
- "engines": {
32
- "node": ">=22"
33
- },
34
- "keywords": [
35
- "agent",
36
- "cli",
37
- "terminal",
38
- "coding-agent",
39
- "llm",
40
- "nvidia",
41
- "nemotron",
42
- "cohere",
43
- "nex-agi"
44
- ],
45
- "author": "om dixit",
46
- "license": "MIT",
47
- "dependencies": {
48
- "@babel/parser": "^7.29.8",
49
- "chalk": "^6.0.0",
50
- "dotenv": "^17.4.2",
51
- "jsonrepair": "^3.15.0",
52
- "marked": "^15.0.12",
53
- "marked-terminal": "^7.3.0",
54
- "openai": "^7.4.0",
55
- "playwright-core": "^1.63.0"
56
- },
57
- "devDependencies": {
58
- "@types/node": "^26.5.1",
59
- "typescript": "^5.9.3"
60
- },
61
- "publishConfig": {
62
- "access": "public"
63
- }
64
- }
1
+ {
2
+ "name": "ucode-agent",
3
+ "version": "1.62.1",
4
+ "description": "ucode - a terminal coding agent that reads, edits and runs your code, on Google Gemini models.",
5
+ "type": "module",
6
+ "main": "ucode.js",
7
+ "bin": {
8
+ "ucode": "ucode.js"
9
+ },
10
+ "files": [
11
+ "ucode.js",
12
+ "src/",
13
+ "skills/",
14
+ "templates/",
15
+ "THIRD_PARTY_NOTICES.md"
16
+ ],
17
+ "scripts": {
18
+ "start": "node ucode.js",
19
+ "test": "node test/run.js",
20
+ "prepublishOnly": "node scripts/no-bundled-key.js && node test/run.js",
21
+ "hooks": "node scripts/install-hooks.js"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/sppideey/ucode-agent.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/sppideey/ucode-agent/issues"
29
+ },
30
+ "homepage": "https://github.com/sppideey/ucode-agent#readme",
31
+ "engines": {
32
+ "node": ">=22"
33
+ },
34
+ "keywords": [
35
+ "agent",
36
+ "cli",
37
+ "terminal",
38
+ "coding-agent",
39
+ "llm",
40
+ "nvidia",
41
+ "nemotron",
42
+ "cohere",
43
+ "nex-agi"
44
+ ],
45
+ "author": "om dixit",
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "@babel/parser": "^7.29.8",
49
+ "chalk": "^6.0.0",
50
+ "dotenv": "^17.4.2",
51
+ "jsonrepair": "^3.15.0",
52
+ "marked": "^15.0.12",
53
+ "marked-terminal": "^7.3.0",
54
+ "openai": "^7.4.0",
55
+ "playwright-core": "^1.63.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^26.5.1",
59
+ "typescript": "^5.9.3"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ }
64
+ }
@@ -1,111 +1,111 @@
1
- /**
2
- * doctor.js — `ucode doctor` and /doctor: is everything ucode needs working?
3
- *
4
- * Every check runs at once and each has a short deadline, so the whole report
5
- * takes a few seconds. A problem comes with the one thing that fixes it.
6
- * Secret values are never printed — only whether they are there and accepted.
7
- */
8
-
9
- import { spawnSync } from 'node:child_process';
10
- import { existsSync, readdirSync } from 'node:fs';
11
- import os from 'node:os';
12
- import path from 'node:path';
13
- import { theme, dim, blue } from '../ui/theme.js';
14
- import { VERSION } from './version.js';
15
- import { ENV_FILE, BASE_URL, providerKey } from './provider.js';
16
- import { newer } from './updater.js';
17
-
18
- const DEADLINE = 6000;
19
- const timed = (ms = DEADLINE) => AbortSignal.timeout(ms);
20
-
21
- function version(cmd) {
22
- const r = spawnSync(cmd, ['--version'], { encoding: 'utf8', shell: process.platform === 'win32', timeout: 5000 });
23
- return r.status === 0 ? String(r.stdout).trim().split('\n')[0] : null;
24
- }
25
-
26
- async function checkKey() {
27
- const key = providerKey();
28
- if (!key) return { ok: false, name: 'API key', detail: 'not set', fix: `Add GEMINI_API_KEY=... to ${ENV_FILE} (free at aistudio.google.com/apikey)` };
29
- try {
30
- // Listing models checks the key in under a second; a chat request can queue for half a minute.
31
- const r = await fetch(`${BASE_URL}/models`, { headers: { Authorization: `Bearer ${key}` }, signal: timed() });
32
- if ([400, 401, 403].includes(r.status)) return { ok: false, name: 'API key', detail: 'rejected', fix: 'Check the key for typos, or make a new one' };
33
- if (!r.ok) return { ok: false, name: 'API key', detail: `could not check (HTTP ${r.status})`, fix: 'Try again in a minute' };
34
- return { ok: true, name: 'API key', detail: 'works' };
35
- } catch (err) {
36
- return { ok: false, name: 'API key', detail: `could not check (${err.name === 'TimeoutError' ? 'no answer' : err.message})`, fix: 'Check your internet connection' };
37
- }
38
- }
39
-
40
- async function checkVercel() {
41
- const token = process.env.VERCEL_TOKEN;
42
- if (!token) return { ok: null, name: 'Vercel', detail: 'no token — /deploy needs one', fix: `vercel.com/account/tokens → Create Token, then add VERCEL_TOKEN=... to ${ENV_FILE}` };
43
- try {
44
- const r = await fetch('https://api.vercel.com/v2/user', { headers: { Authorization: `Bearer ${token}` }, signal: timed() });
45
- if (!r.ok) return { ok: false, name: 'Vercel', detail: 'token rejected', fix: 'Make a new token at vercel.com/account/tokens' };
46
- const j = await r.json();
47
- return { ok: true, name: 'Vercel', detail: `ready to deploy as ${j.user?.username ?? 'you'}` };
48
- } catch {
49
- return { ok: false, name: 'Vercel', detail: 'could not reach Vercel', fix: 'Check your internet connection' };
50
- }
51
- }
52
-
53
- async function checkBrowser() {
54
- try {
55
- const { chromium } = await import('playwright-core');
56
- for (const channel of ['msedge', 'chrome']) {
57
- try {
58
- const b = await chromium.launch({ channel, headless: true, timeout: 8000 });
59
- await b.close();
60
- return { ok: true, name: 'Browser', detail: `${channel === 'msedge' ? 'Edge' : 'Chrome'} — look_at_app can see your apps` };
61
- } catch { /* try the next one */ }
62
- }
63
- } catch { /* playwright-core missing */ }
64
- return { ok: false, name: 'Browser', detail: 'no Edge or Chrome found', fix: 'Install Google Chrome or Microsoft Edge' };
65
- }
66
-
67
- async function checkVersion() {
68
- try {
69
- const r = await fetch('https://registry.npmjs.org/ucode-agent/latest', { signal: timed() });
70
- const latest = (await r.json()).version;
71
- return newer(latest, VERSION)
72
- ? { ok: null, name: 'ucode', detail: `${VERSION} · ${latest} is out`, fix: 'It updates itself on the next start, or: npm install -g ucode-agent' }
73
- : { ok: true, name: 'ucode', detail: `${VERSION} · up to date` };
74
- } catch {
75
- return { ok: true, name: 'ucode', detail: VERSION };
76
- }
77
- }
78
-
79
- function checkTools() {
80
- const [major] = process.versions.node.split('.').map(Number);
81
- const out = [
82
- major >= 22
83
- ? { ok: true, name: 'Node', detail: process.versions.node }
84
- : { ok: false, name: 'Node', detail: process.versions.node, fix: 'Install Node 22 or newer from nodejs.org' },
85
- ];
86
- const npm = version('npm');
87
- out.push(npm ? { ok: true, name: 'npm', detail: npm } : { ok: false, name: 'npm', detail: 'not found', fix: 'Reinstall Node, which includes npm' });
88
- const git = version('git');
89
- out.push(git ? { ok: true, name: 'git', detail: git.replace(/^git version /, '') } : { ok: null, name: 'git', detail: 'not found', fix: 'Optional — install git to keep history of your apps' });
90
- const cache = path.join(os.homedir(), '.ucode', 'cache');
91
- if (existsSync(cache)) out.push({ ok: true, name: 'Starter cache', detail: `${readdirSync(cache).length} ready` });
92
- return out;
93
- }
94
-
95
- /** Run every check; returns the lines to print. */
96
- export async function runDoctor() {
97
- const results = [
98
- ...checkTools(),
99
- ...(await Promise.all([checkKey(), checkVercel(), checkBrowser(), checkVersion()])),
100
- ];
101
- const width = Math.max(...results.map((r) => r.name.length));
102
- const lines = ['', ` ${blue('ucode doctor')}`];
103
- for (const r of results) {
104
- const mark = r.ok === true ? theme.ok('✓') : r.ok === false ? theme.error('✗') : theme.warn('!');
105
- lines.push(` ${mark} ${r.name.padEnd(width)} ${r.detail}`);
106
- if (r.fix && r.ok !== true) lines.push(` ${' '.repeat(width)} ${dim(`→ ${r.fix}`)}`);
107
- }
108
- const bad = results.filter((r) => r.ok === false).length;
109
- lines.push('', bad ? ` ${bad} thing${bad === 1 ? '' : 's'} to fix` : ` ${theme.ok('All good')}`, '');
110
- return lines;
111
- }
1
+ /**
2
+ * doctor.js — `ucode doctor` and /doctor: is everything ucode needs working?
3
+ *
4
+ * Every check runs at once and each has a short deadline, so the whole report
5
+ * takes a few seconds. A problem comes with the one thing that fixes it.
6
+ * Secret values are never printed — only whether they are there and accepted.
7
+ */
8
+
9
+ import { spawnSync } from 'node:child_process';
10
+ import { existsSync, readdirSync } from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+ import { theme, dim, blue } from '../ui/theme.js';
14
+ import { VERSION } from './version.js';
15
+ import { ENV_FILE, BASE_URL, providerKey } from './provider.js';
16
+ import { newer } from './updater.js';
17
+
18
+ const DEADLINE = 6000;
19
+ const timed = (ms = DEADLINE) => AbortSignal.timeout(ms);
20
+
21
+ function version(cmd) {
22
+ const r = spawnSync(cmd, ['--version'], { encoding: 'utf8', shell: process.platform === 'win32', timeout: 5000 });
23
+ return r.status === 0 ? String(r.stdout).trim().split('\n')[0] : null;
24
+ }
25
+
26
+ async function checkKey() {
27
+ const key = providerKey();
28
+ if (!key) return { ok: false, name: 'API key', detail: 'not set', fix: `Add GEMINI_API_KEY=... to ${ENV_FILE} (free at aistudio.google.com/apikey)` };
29
+ try {
30
+ // Listing models checks the key in under a second; a chat request can queue for half a minute.
31
+ const r = await fetch(`${BASE_URL}/models`, { headers: { Authorization: `Bearer ${key}` }, signal: timed() });
32
+ if ([400, 401, 403].includes(r.status)) return { ok: false, name: 'API key', detail: 'rejected', fix: 'Check the key for typos, or make a new one' };
33
+ if (!r.ok) return { ok: false, name: 'API key', detail: `could not check (HTTP ${r.status})`, fix: 'Try again in a minute' };
34
+ return { ok: true, name: 'API key', detail: 'works' };
35
+ } catch (err) {
36
+ return { ok: false, name: 'API key', detail: `could not check (${err.name === 'TimeoutError' ? 'no answer' : err.message})`, fix: 'Check your internet connection' };
37
+ }
38
+ }
39
+
40
+ async function checkVercel() {
41
+ const token = process.env.VERCEL_TOKEN;
42
+ if (!token) return { ok: null, name: 'Vercel', detail: 'no token — /deploy needs one', fix: `vercel.com/account/tokens → Create Token, then add VERCEL_TOKEN=... to ${ENV_FILE}` };
43
+ try {
44
+ const r = await fetch('https://api.vercel.com/v2/user', { headers: { Authorization: `Bearer ${token}` }, signal: timed() });
45
+ if (!r.ok) return { ok: false, name: 'Vercel', detail: 'token rejected', fix: 'Make a new token at vercel.com/account/tokens' };
46
+ const j = await r.json();
47
+ return { ok: true, name: 'Vercel', detail: `ready to deploy as ${j.user?.username ?? 'you'}` };
48
+ } catch {
49
+ return { ok: false, name: 'Vercel', detail: 'could not reach Vercel', fix: 'Check your internet connection' };
50
+ }
51
+ }
52
+
53
+ async function checkBrowser() {
54
+ try {
55
+ const { chromium } = await import('playwright-core');
56
+ for (const channel of ['msedge', 'chrome']) {
57
+ try {
58
+ const b = await chromium.launch({ channel, headless: true, timeout: 8000 });
59
+ await b.close();
60
+ return { ok: true, name: 'Browser', detail: `${channel === 'msedge' ? 'Edge' : 'Chrome'} — look_at_app can see your apps` };
61
+ } catch { /* try the next one */ }
62
+ }
63
+ } catch { /* playwright-core missing */ }
64
+ return { ok: false, name: 'Browser', detail: 'no Edge or Chrome found', fix: 'Install Google Chrome or Microsoft Edge' };
65
+ }
66
+
67
+ async function checkVersion() {
68
+ try {
69
+ const r = await fetch('https://registry.npmjs.org/ucode-agent/latest', { signal: timed() });
70
+ const latest = (await r.json()).version;
71
+ return newer(latest, VERSION)
72
+ ? { ok: null, name: 'ucode', detail: `${VERSION} · ${latest} is out`, fix: 'It updates itself on the next start, or: npm install -g ucode-agent' }
73
+ : { ok: true, name: 'ucode', detail: `${VERSION} · up to date` };
74
+ } catch {
75
+ return { ok: true, name: 'ucode', detail: VERSION };
76
+ }
77
+ }
78
+
79
+ function checkTools() {
80
+ const [major] = process.versions.node.split('.').map(Number);
81
+ const out = [
82
+ major >= 22
83
+ ? { ok: true, name: 'Node', detail: process.versions.node }
84
+ : { ok: false, name: 'Node', detail: process.versions.node, fix: 'Install Node 22 or newer from nodejs.org' },
85
+ ];
86
+ const npm = version('npm');
87
+ out.push(npm ? { ok: true, name: 'npm', detail: npm } : { ok: false, name: 'npm', detail: 'not found', fix: 'Reinstall Node, which includes npm' });
88
+ const git = version('git');
89
+ out.push(git ? { ok: true, name: 'git', detail: git.replace(/^git version /, '') } : { ok: null, name: 'git', detail: 'not found', fix: 'Optional — install git to keep history of your apps' });
90
+ const cache = path.join(os.homedir(), '.ucode', 'cache');
91
+ if (existsSync(cache)) out.push({ ok: true, name: 'Starter cache', detail: `${readdirSync(cache).length} ready` });
92
+ return out;
93
+ }
94
+
95
+ /** Run every check; returns the lines to print. */
96
+ export async function runDoctor() {
97
+ const results = [
98
+ ...checkTools(),
99
+ ...(await Promise.all([checkKey(), checkVercel(), checkBrowser(), checkVersion()])),
100
+ ];
101
+ const width = Math.max(...results.map((r) => r.name.length));
102
+ const lines = ['', ` ${blue('ucode doctor')}`];
103
+ for (const r of results) {
104
+ const mark = r.ok === true ? theme.ok('✓') : r.ok === false ? theme.error('✗') : theme.warn('!');
105
+ lines.push(` ${mark} ${r.name.padEnd(width)} ${r.detail}`);
106
+ if (r.fix && r.ok !== true) lines.push(` ${' '.repeat(width)} ${dim(`→ ${r.fix}`)}`);
107
+ }
108
+ const bad = results.filter((r) => r.ok === false).length;
109
+ lines.push('', bad ? ` ${bad} thing${bad === 1 ? '' : 's'} to fix` : ` ${theme.ok('All good')}`, '');
110
+ return lines;
111
+ }
package/src/core/login.js CHANGED
@@ -1,59 +1,59 @@
1
- /**
2
- * login.js — putting the API key somewhere ucode will always find it.
3
- *
4
- * A key in a project's .env is a key you set up again in the next folder, and
5
- * on the next machine. This writes it once to ~/.ucode/.env, which every
6
- * project on the machine reads, so setting ucode up somewhere new — a borrowed
7
- * laptop, a machine you are demonstrating on — is one command.
8
- *
9
- * The file is written with owner-only permissions, and the existing contents
10
- * are kept: a key is replaced in place rather than by rewriting the file.
11
- */
12
-
13
- import { promises as fs } from 'node:fs';
14
- import path from 'node:path';
15
- import { ENV_FILE } from './provider.js';
16
-
17
- const NAME = 'GEMINI_API_KEY';
18
-
19
- /** A plausible Google key (AIza... or the newer AQ.... form), so a typo is caught here. */
20
- export function looksLikeKey(key) {
21
- return typeof key === 'string' && /^(?:AIza[\w-]{30,}|AQ\.[\w.-]{30,})$/.test(key.trim());
22
- }
23
-
24
- /** Put `key` in the machine-wide env file, keeping whatever else is in it. */
25
- export function withKey(existing, key) {
26
- const line = `${NAME}=${key}`;
27
- const lines = String(existing ?? '').split('\n');
28
- let replaced = false;
29
- const out = lines.map((l) => {
30
- if (new RegExp(`^\\s*(?:export\\s+)?${NAME}\\s*=`).test(l)) {
31
- replaced = true;
32
- return line;
33
- }
34
- return l;
35
- });
36
- if (!replaced) {
37
- if (out.length && out[out.length - 1].trim() !== '') out.push('');
38
- out.splice(out.length - 1, 0, line);
39
- }
40
- return out.join('\n').replace(/\n{3,}/g, '\n\n');
41
- }
42
-
43
- export async function saveKey(key) {
44
- const trimmed = String(key ?? '').trim();
45
-
46
- if (!trimmed) {
47
- return ` Usage: ucode login <key>\n\n Get one free at https://aistudio.google.com/apikey\n It is saved to ${ENV_FILE} and used by every project on this machine.`;
48
- }
49
- if (!looksLikeKey(trimmed)) {
50
- return ` That does not look like a Google key — they start with "AIza" or "AQ.".\n Get one at https://aistudio.google.com/apikey`;
51
- }
52
-
53
- const existing = await fs.readFile(ENV_FILE, 'utf8').catch(() => '');
54
- await fs.mkdir(path.dirname(ENV_FILE), { recursive: true });
55
- await fs.writeFile(ENV_FILE, withKey(existing, trimmed), { encoding: 'utf8', mode: 0o600 });
56
- await fs.chmod(ENV_FILE, 0o600).catch(() => {});
57
-
58
- return ` Key saved to ${ENV_FILE}\n Every project on this machine will use it. Run ucode anywhere to start.`;
59
- }
1
+ /**
2
+ * login.js — putting the API key somewhere ucode will always find it.
3
+ *
4
+ * A key in a project's .env is a key you set up again in the next folder, and
5
+ * on the next machine. This writes it once to ~/.ucode/.env, which every
6
+ * project on the machine reads, so setting ucode up somewhere new — a borrowed
7
+ * laptop, a machine you are demonstrating on — is one command.
8
+ *
9
+ * The file is written with owner-only permissions, and the existing contents
10
+ * are kept: a key is replaced in place rather than by rewriting the file.
11
+ */
12
+
13
+ import { promises as fs } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { ENV_FILE } from './provider.js';
16
+
17
+ const NAME = 'GEMINI_API_KEY';
18
+
19
+ /** A plausible Google key (AIza... or the newer AQ.... form), so a typo is caught here. */
20
+ export function looksLikeKey(key) {
21
+ return typeof key === 'string' && /^(?:AIza[\w-]{30,}|AQ\.[\w.-]{30,})$/.test(key.trim());
22
+ }
23
+
24
+ /** Put `key` in the machine-wide env file, keeping whatever else is in it. */
25
+ export function withKey(existing, key) {
26
+ const line = `${NAME}=${key}`;
27
+ const lines = String(existing ?? '').split('\n');
28
+ let replaced = false;
29
+ const out = lines.map((l) => {
30
+ if (new RegExp(`^\\s*(?:export\\s+)?${NAME}\\s*=`).test(l)) {
31
+ replaced = true;
32
+ return line;
33
+ }
34
+ return l;
35
+ });
36
+ if (!replaced) {
37
+ if (out.length && out[out.length - 1].trim() !== '') out.push('');
38
+ out.splice(out.length - 1, 0, line);
39
+ }
40
+ return out.join('\n').replace(/\n{3,}/g, '\n\n');
41
+ }
42
+
43
+ export async function saveKey(key) {
44
+ const trimmed = String(key ?? '').trim();
45
+
46
+ if (!trimmed) {
47
+ return ` Usage: ucode login <key>\n\n Get one free at https://aistudio.google.com/apikey\n It is saved to ${ENV_FILE} and used by every project on this machine.`;
48
+ }
49
+ if (!looksLikeKey(trimmed)) {
50
+ return ` That does not look like a Google key — they start with "AIza" or "AQ.".\n Get one at https://aistudio.google.com/apikey`;
51
+ }
52
+
53
+ const existing = await fs.readFile(ENV_FILE, 'utf8').catch(() => '');
54
+ await fs.mkdir(path.dirname(ENV_FILE), { recursive: true });
55
+ await fs.writeFile(ENV_FILE, withKey(existing, trimmed), { encoding: 'utf8', mode: 0o600 });
56
+ await fs.chmod(ENV_FILE, 0o600).catch(() => {});
57
+
58
+ return ` Key saved to ${ENV_FILE}\n Every project on this machine will use it. Run ucode anywhere to start.`;
59
+ }