vipcode 0.0.0-stage → 0.2.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/dist/update.js ADDED
@@ -0,0 +1,237 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { PACKAGE_NAME, REPO_URL, VERSION } from "./meta.js";
6
+ import { INSTALL_DIR, UPDATE_PATH, ensureHomeDir } from "./paths.js";
7
+ const DAY_MS = 24 * 60 * 60 * 1000;
8
+ // Carpeta donde está instalado vipcode (la que contiene dist/). VIPCODE_INSTALL_ROOT permite fingir otra en las pruebas
9
+ export function packageRoot() {
10
+ return process.env.VIPCODE_INSTALL_ROOT || INSTALL_DIR;
11
+ }
12
+ // "v0.10.1" frente a "0.9.3": positivo si a es más nueva
13
+ export function compareVersions(a, b) {
14
+ const parse = (version) => version
15
+ .replace(/^v/, "")
16
+ .split("-")[0]
17
+ .split(".")
18
+ .map((part) => Number(part) || 0);
19
+ const x = parse(a);
20
+ const y = parse(b);
21
+ for (let i = 0; i < 3; i++) {
22
+ const diff = (x[i] || 0) - (y[i] || 0);
23
+ if (diff !== 0) {
24
+ return diff;
25
+ }
26
+ }
27
+ return 0;
28
+ }
29
+ // API de GitHub del repositorio (VIPCODE_UPDATE_URL permite usar otra, por ejemplo en las pruebas)
30
+ function apiBase() {
31
+ return process.env.VIPCODE_UPDATE_URL || REPO_URL.replace("https://github.com/", "https://api.github.com/repos/");
32
+ }
33
+ // Registro de npm (VIPCODE_REGISTRY_URL permite usar otro, por ejemplo en las pruebas)
34
+ function registryBase() {
35
+ return (process.env.VIPCODE_REGISTRY_URL || "https://registry.npmjs.org").replace(/\/+$/, "");
36
+ }
37
+ // Última versión publicada en npm (null si no está, o si el paquete con ese nombre no es este proyecto)
38
+ async function latestFromNpm() {
39
+ const response = await fetch(`${registryBase()}/${PACKAGE_NAME}/latest`, {
40
+ headers: { accept: "application/json", "user-agent": `vipcode/${VERSION}` },
41
+ signal: AbortSignal.timeout(15000),
42
+ });
43
+ if (response.status === 404) {
44
+ return null;
45
+ }
46
+ if (!response.ok) {
47
+ throw new Error(`npm ha respondido ${response.status}`);
48
+ }
49
+ const json = await response.json();
50
+ const repository = String(json.repository?.url || json.repository || "");
51
+ if (!json.version || !repository.includes(REPO_URL.replace("https://", ""))) {
52
+ return null;
53
+ }
54
+ return { version: String(json.version), spec: `${PACKAGE_NAME}@${json.version}`, source: "npm" };
55
+ }
56
+ // Última release de GitHub con su paquete ya compilado (null si todavía no hay ninguna)
57
+ async function latestFromGitHub() {
58
+ const response = await fetch(`${apiBase()}/releases/latest`, {
59
+ headers: { accept: "application/vnd.github+json", "user-agent": `vipcode/${VERSION}` },
60
+ signal: AbortSignal.timeout(15000),
61
+ });
62
+ if (response.status === 404) {
63
+ return null;
64
+ }
65
+ if (!response.ok) {
66
+ throw new Error(`GitHub ha respondido ${response.status}`);
67
+ }
68
+ const json = await response.json();
69
+ const assets = Array.isArray(json.assets) ? json.assets : [];
70
+ const asset = assets.find((item) => item.name === "vipcode.tgz") || assets.find((item) => /\.tgz$/.test(String(item.name)));
71
+ if (!asset?.browser_download_url) {
72
+ return null;
73
+ }
74
+ return { version: String(json.tag_name || "").replace(/^v/, ""), spec: String(asset.browser_download_url), source: "GitHub" };
75
+ }
76
+ // La más nueva entre npm y GitHub (con la misma versión, la de npm); null si no hay ninguna publicada
77
+ export async function latestRelease() {
78
+ const errors = [];
79
+ const found = (await Promise.all([latestFromNpm, latestFromGitHub].map((source) => source().catch((err) => {
80
+ errors.push(err);
81
+ return null;
82
+ })))).filter((release) => release !== null);
83
+ if (found.length === 0) {
84
+ if (errors.length === 2) {
85
+ throw new Error(`no he podido consultar npm ni GitHub (${errors[0].message})`);
86
+ }
87
+ return null;
88
+ }
89
+ return found.reduce((best, release) => (compareVersions(release.version, best.version) > 0 ? release : best));
90
+ }
91
+ // Una vez al día, en segundo plano: devuelve la versión nueva si la hay (y nunca falla)
92
+ export async function checkForUpdate() {
93
+ let cache = {};
94
+ try {
95
+ cache = JSON.parse(fs.readFileSync(UPDATE_PATH, "utf8"));
96
+ }
97
+ catch {
98
+ // primera vez
99
+ }
100
+ if (!cache.checked || Date.now() - cache.checked > DAY_MS || !cache.latest) {
101
+ try {
102
+ const release = await latestRelease();
103
+ cache = { checked: Date.now(), latest: release?.version || VERSION };
104
+ ensureHomeDir();
105
+ fs.writeFileSync(UPDATE_PATH, JSON.stringify(cache, null, 2));
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ if (cache.latest && compareVersions(cache.latest, VERSION) > 0) {
112
+ return cache.latest;
113
+ }
114
+ return null;
115
+ }
116
+ // Sin las variables que pone npm al ejecutar un script (si vipcode se lanzó así, npm_config_local_prefix
117
+ // apuntaría a otra carpeta y npm trabajaría sobre ella)
118
+ function cleanEnv() {
119
+ const env = { GIT_TERMINAL_PROMPT: "0" };
120
+ for (const [key, value] of Object.entries(process.env)) {
121
+ if (!/^npm_(config_local_prefix|package_|lifecycle_|command$)/i.test(key)) {
122
+ env[key] = value;
123
+ }
124
+ }
125
+ return env;
126
+ }
127
+ function run(command, args, cwd) {
128
+ return new Promise((resolve) => {
129
+ // En Windows npm es un .cmd y necesita la shell
130
+ const child = spawn(command, args, {
131
+ cwd,
132
+ shell: process.platform === "win32",
133
+ stdio: ["ignore", "pipe", "pipe"],
134
+ env: { ...cleanEnv(), GIT_TERMINAL_PROMPT: "0" },
135
+ });
136
+ let output = "";
137
+ child.stdout.on("data", (data) => (output += data.toString()));
138
+ child.stderr.on("data", (data) => (output += data.toString()));
139
+ child.on("error", (err) => resolve({ code: null, output: err.message }));
140
+ child.on("close", (code) => resolve({ code, output }));
141
+ });
142
+ }
143
+ const NPM = process.platform === "win32" ? "npm.cmd" : "npm";
144
+ function lastLines(text, count = 6) {
145
+ return text.trim().split("\n").slice(-count).join("\n");
146
+ }
147
+ function isInside(dir, parent) {
148
+ try {
149
+ const real = fs.realpathSync(dir);
150
+ const base = fs.realpathSync(parent);
151
+ return real === base || real.startsWith(base + path.sep);
152
+ }
153
+ catch {
154
+ return false;
155
+ }
156
+ }
157
+ // Instalado desde el código (con .git): se trae lo último del repositorio y se compila
158
+ async function updateFromGit(root, log) {
159
+ const status = await run("git", ["status", "--porcelain"], root);
160
+ if (status.code !== 0) {
161
+ return { status: "error", message: `No he podido usar git en ${root}: ${lastLines(status.output, 2)}` };
162
+ }
163
+ if (status.output.trim() !== "") {
164
+ return { status: "error", message: `Tienes cambios sin guardar en ${root}: haz commit o descártalos y vuelve a probar. No he tocado nada.` };
165
+ }
166
+ log("Buscando cambios en el repositorio…");
167
+ const before = (await run("git", ["rev-parse", "HEAD"], root)).output.trim();
168
+ const pull = await run("git", ["pull", "--ff-only"], root);
169
+ if (pull.code !== 0) {
170
+ return { status: "error", message: `git pull ha fallado:\n${lastLines(pull.output)}` };
171
+ }
172
+ const after = (await run("git", ["rev-parse", "HEAD"], root)).output.trim();
173
+ if (before === after) {
174
+ return { status: "current", message: `Ya tienes lo último del repositorio (vipcode ${VERSION}).` };
175
+ }
176
+ const count = (await run("git", ["rev-list", "--count", `${before}..${after}`], root)).output.trim();
177
+ const changes = count === "1" ? "1 cambio nuevo" : `${count} cambios nuevos`;
178
+ log(`${changes.replace(/^./, (letter) => letter.toUpperCase())}. Instalando dependencias y compilando…`);
179
+ // npm ci no reescribe package-lock.json (si lo cambiara, la próxima vez habría «cambios sin guardar»)
180
+ const install = fs.existsSync(path.join(root, "package-lock.json")) ? ["ci"] : ["install"];
181
+ for (const args of [install, ["run", "build"]]) {
182
+ const step = await run(NPM, args, root);
183
+ if (step.code !== 0) {
184
+ return { status: "error", message: `npm ${args.join(" ")} ha fallado:\n${lastLines(step.output)}` };
185
+ }
186
+ }
187
+ let version = VERSION;
188
+ try {
189
+ version = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version || VERSION;
190
+ }
191
+ catch {
192
+ // se queda la que había
193
+ }
194
+ return { status: "updated", message: `Actualizado desde el repositorio (${changes}): vipcode ${version}. Cierra y vuelve a abrir vipcode para usarlo.` };
195
+ }
196
+ // Instalado con npm: se instala el paquete de la última versión publicada en GitHub
197
+ async function updateFromRelease(root, log) {
198
+ log("Buscando la última versión publicada en npm y en GitHub…");
199
+ let release;
200
+ try {
201
+ release = await latestRelease();
202
+ }
203
+ catch (err) {
204
+ return { status: "error", message: `No he podido buscar versiones nuevas: ${err.message}` };
205
+ }
206
+ if (!release) {
207
+ return { status: "manual", message: `Todavía no hay ninguna versión publicada (ni en npm ni en ${REPO_URL}/releases).` };
208
+ }
209
+ if (compareVersions(release.version, VERSION) <= 0) {
210
+ return { status: "current", message: `Ya tienes la última versión (${VERSION}).` };
211
+ }
212
+ const prefix = (await run(NPM, ["prefix", "-g"], os.tmpdir())).output.trim();
213
+ const globalDir = process.platform === "win32" ? path.join(prefix, "node_modules") : path.join(prefix, "lib", "node_modules");
214
+ if (!prefix || !isInside(root, globalDir)) {
215
+ return {
216
+ status: "manual",
217
+ message: `Hay una versión nueva (${release.version}, tienes la ${VERSION}), pero este vipcode no está instalado con «npm install -g». Instálala con:\n npm install -g ${release.spec}`,
218
+ };
219
+ }
220
+ if (!/^[\w@.:/+~%-]+$/.test(release.spec) || release.spec.startsWith("-")) {
221
+ return { status: "error", message: `La última versión publicada tiene un nombre de paquete raro (${release.spec}): no la instalo.` };
222
+ }
223
+ log(`Instalando vipcode ${release.version} desde ${release.source}…`);
224
+ // Desde la carpeta temporal, para que no se mezcle con la configuración de npm del proyecto en el que estás
225
+ const install = await run(NPM, ["install", "-g", release.spec], os.tmpdir());
226
+ if (install.code !== 0) {
227
+ return { status: "error", message: `npm install ha fallado:\n${lastLines(install.output)}` };
228
+ }
229
+ return { status: "updated", message: `vipcode ${release.version} instalado (tenías la ${VERSION}). Cierra y vuelve a abrir vipcode para usarlo.` };
230
+ }
231
+ export async function updateVipcode(log) {
232
+ const root = packageRoot();
233
+ if (fs.existsSync(path.join(root, ".git"))) {
234
+ return updateFromGit(root, log);
235
+ }
236
+ return updateFromRelease(root, log);
237
+ }
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "vipcode",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "Agente de programación para la terminal que usa IAs gratuitas y salta a otro proveedor cuando uno se agota",
5
+ "keywords": [
6
+ "ai",
7
+ "agent",
8
+ "coding-agent",
9
+ "cli",
10
+ "tui",
11
+ "llm",
12
+ "free",
13
+ "terminal",
14
+ "opencode",
15
+ "spanish"
16
+ ],
17
+ "homepage": "https://github.com/PabloVip/vipcode#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/PabloVip/vipcode/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/PabloVip/vipcode.git"
24
+ },
25
+ "license": "MIT",
26
+ "author": "Pablo Vicente Juan (https://github.com/PabloVip)",
27
+ "type": "module",
28
+ "bin": {
29
+ "vipcode": "dist/index.js"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "README.en.md",
35
+ "LICENSE"
36
+ ],
37
+ "scripts": {
38
+ "dev": "tsx src/index.ts",
39
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
40
+ "start": "node dist/index.js",
41
+ "test": "npm run build && node --test --test-force-exit --test-timeout=120000 \"test/*.test.mjs\"",
42
+ "prepublishOnly": "npm run build",
43
+ "release": "node scripts/release.mjs"
44
+ },
45
+ "engines": {
46
+ "node": ">=22"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.32.2",
50
+ "fast-glob": "^3.3.3",
51
+ "highlight.js": "^11.12.0",
52
+ "ink": "^8.0.0",
53
+ "marked": "^18.1.2",
54
+ "openai": "^7.45.1",
55
+ "picocolors": "^1.1.1",
56
+ "react": "^19.3.1",
57
+ "typescript": "^7.0.2",
58
+ "yaml": "^2.9.1",
59
+ "zod": "^4.6.7"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^26.6.6",
63
+ "@types/react": "^19.3.0",
64
+ "tsx": "^4.23.15"
65
+ }
66
+ }