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/LICENSE +21 -0
- package/README.en.md +60 -0
- package/README.md +401 -2
- package/dist/agent.js +502 -0
- package/dist/agents.js +174 -0
- package/dist/app.js +1025 -0
- package/dist/auth.js +108 -0
- package/dist/catalog.js +487 -0
- package/dist/commands.js +121 -0
- package/dist/config.js +230 -0
- package/dist/debug.js +32 -0
- package/dist/discovery.js +277 -0
- package/dist/frontmatter.js +45 -0
- package/dist/images.js +81 -0
- package/dist/index.js +623 -0
- package/dist/login.js +157 -0
- package/dist/lsp.js +480 -0
- package/dist/markdown.js +262 -0
- package/dist/match.js +8 -0
- package/dist/mcp.js +225 -0
- package/dist/meta.js +7 -0
- package/dist/paths.js +53 -0
- package/dist/permissions.js +144 -0
- package/dist/prompt.js +123 -0
- package/dist/ranking.js +230 -0
- package/dist/router.js +726 -0
- package/dist/sanitize.js +38 -0
- package/dist/session.js +219 -0
- package/dist/skills.js +374 -0
- package/dist/snapshot.js +199 -0
- package/dist/tools.js +1311 -0
- package/dist/trust.js +171 -0
- package/dist/tui/app.js +1019 -0
- package/dist/tui/dialogs.js +197 -0
- package/dist/tui/editor.js +120 -0
- package/dist/ui.js +184 -0
- package/dist/update.js +237 -0
- package/package.json +64 -4
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { debugLog } from "./debug.js";
|
|
7
|
+
import { SNAPSHOTS_DIR } from "./paths.js";
|
|
8
|
+
// Carpetas que no se guardan en las instantáneas (además de lo que diga el .gitignore del proyecto)
|
|
9
|
+
const DEFAULT_EXCLUDES = [
|
|
10
|
+
"node_modules/",
|
|
11
|
+
".venv/",
|
|
12
|
+
"venv/",
|
|
13
|
+
"__pycache__/",
|
|
14
|
+
".next/",
|
|
15
|
+
".nuxt/",
|
|
16
|
+
".turbo/",
|
|
17
|
+
".cache/",
|
|
18
|
+
"target/",
|
|
19
|
+
"dist/",
|
|
20
|
+
"build/",
|
|
21
|
+
"coverage/",
|
|
22
|
+
".DS_Store",
|
|
23
|
+
"*.log",
|
|
24
|
+
];
|
|
25
|
+
// Instantáneas del proyecto en un repositorio git aparte (~/.vipcode/snapshots), sin tocar tu .git
|
|
26
|
+
export class Snapshots {
|
|
27
|
+
root;
|
|
28
|
+
enabled;
|
|
29
|
+
gitDir;
|
|
30
|
+
ready = null;
|
|
31
|
+
disabledReason = "";
|
|
32
|
+
constructor(root, enabled) {
|
|
33
|
+
this.root = root;
|
|
34
|
+
this.enabled = enabled;
|
|
35
|
+
const hash = crypto.createHash("sha1").update(root).digest("hex").slice(0, 16);
|
|
36
|
+
this.gitDir = path.join(SNAPSHOTS_DIR, hash);
|
|
37
|
+
}
|
|
38
|
+
git(args, timeoutMs = 30000) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
execFile("git", ["--git-dir", this.gitDir, "--work-tree", this.root, "-c", "core.autocrlf=false", "-c", "core.quotepath=false", ...args], {
|
|
41
|
+
cwd: this.root,
|
|
42
|
+
timeout: timeoutMs,
|
|
43
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
44
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0" },
|
|
45
|
+
}, (err, stdout, stderr) => {
|
|
46
|
+
let code = 0;
|
|
47
|
+
if (err) {
|
|
48
|
+
code = typeof err.code === "number" ? err.code : 1;
|
|
49
|
+
}
|
|
50
|
+
resolve({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
available() {
|
|
55
|
+
if (!this.ready) {
|
|
56
|
+
this.ready = this.init();
|
|
57
|
+
}
|
|
58
|
+
return this.ready;
|
|
59
|
+
}
|
|
60
|
+
async init() {
|
|
61
|
+
if (!this.enabled) {
|
|
62
|
+
this.disabledReason = "desactivadas en la configuración (snapshot: false)";
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const resolved = path.resolve(this.root);
|
|
66
|
+
if (resolved === path.resolve(os.homedir()) || resolved === path.parse(resolved).root) {
|
|
67
|
+
this.disabledReason = "la carpeta es demasiado grande (tu carpeta personal o la raíz)";
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const version = await new Promise((resolve) => {
|
|
71
|
+
execFile("git", ["--version"], (err) => resolve(!err));
|
|
72
|
+
});
|
|
73
|
+
if (!version) {
|
|
74
|
+
this.disabledReason = "git no está instalado";
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (!fs.existsSync(path.join(this.gitDir, "HEAD"))) {
|
|
78
|
+
fs.mkdirSync(this.gitDir, { recursive: true });
|
|
79
|
+
const result = await this.git(["init", "--quiet"]);
|
|
80
|
+
if (result.code !== 0) {
|
|
81
|
+
this.disabledReason = `no se pudo crear el repositorio de instantáneas: ${result.stderr.trim()}`;
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
fs.mkdirSync(path.join(this.gitDir, "info"), { recursive: true });
|
|
85
|
+
fs.writeFileSync(path.join(this.gitDir, "info", "exclude"), `${DEFAULT_EXCLUDES.join("\n")}\n`);
|
|
86
|
+
fs.writeFileSync(path.join(this.gitDir, "vipcode-root"), this.root);
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// Guarda el estado actual de los ficheros; devuelve el id del árbol
|
|
91
|
+
async track() {
|
|
92
|
+
if (!(await this.available())) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const add = await this.git(["add", "--all", "--", "."], 60000);
|
|
96
|
+
if (add.code !== 0) {
|
|
97
|
+
debugLog("snapshot add", add.stderr);
|
|
98
|
+
if (/timed out|ETIMEDOUT/i.test(add.stderr) || add.code === 143) {
|
|
99
|
+
this.disabledReason = "el proyecto es demasiado grande para hacer instantáneas rápido";
|
|
100
|
+
this.ready = Promise.resolve(false);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const tree = await this.git(["write-tree"]);
|
|
105
|
+
if (tree.code !== 0) {
|
|
106
|
+
debugLog("snapshot write-tree", tree.stderr);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return tree.stdout.trim();
|
|
110
|
+
}
|
|
111
|
+
// Ficheros que cambian entre dos instantáneas: A añadido, M modificado, D borrado
|
|
112
|
+
async changes(from, to) {
|
|
113
|
+
const result = await this.git(["diff-tree", "-r", "--name-status", "--no-renames", "-z", from, to]);
|
|
114
|
+
if (result.code !== 0) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const parts = result.stdout.split("\0").filter((part) => part.length > 0);
|
|
118
|
+
const changes = [];
|
|
119
|
+
for (let i = 0; i + 1 < parts.length; i += 2) {
|
|
120
|
+
changes.push({ status: parts[i], file: parts[i + 1] });
|
|
121
|
+
}
|
|
122
|
+
return changes;
|
|
123
|
+
}
|
|
124
|
+
// Deja los ficheros como estaban en la instantánea; devuelve los que han cambiado
|
|
125
|
+
async restore(tree) {
|
|
126
|
+
const current = await this.track();
|
|
127
|
+
if (!current) {
|
|
128
|
+
throw new Error(`No se pueden restaurar las instantáneas: ${this.disabledReason || "error de git"}`);
|
|
129
|
+
}
|
|
130
|
+
const changes = await this.changes(tree, current);
|
|
131
|
+
if (changes.length === 0) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const toCheckout = [];
|
|
135
|
+
for (const change of changes) {
|
|
136
|
+
if (change.status === "A") {
|
|
137
|
+
// No existía en la instantánea: se borra
|
|
138
|
+
const abs = path.join(this.root, change.file);
|
|
139
|
+
fs.rmSync(abs, { force: true });
|
|
140
|
+
this.removeEmptyParents(path.dirname(abs));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
toCheckout.push(change.file);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// De 200 en 200 para no pasarnos del límite de argumentos
|
|
147
|
+
for (let i = 0; i < toCheckout.length; i += 200) {
|
|
148
|
+
const result = await this.git(["checkout", tree, "--", ...toCheckout.slice(i, i + 200)]);
|
|
149
|
+
if (result.code !== 0) {
|
|
150
|
+
throw new Error(`git checkout falló: ${result.stderr.trim()}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return changes.map((change) => change.file);
|
|
154
|
+
}
|
|
155
|
+
removeEmptyParents(dir) {
|
|
156
|
+
let current = dir;
|
|
157
|
+
while (current.startsWith(this.root + path.sep)) {
|
|
158
|
+
try {
|
|
159
|
+
if (fs.readdirSync(current).length > 0) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
fs.rmdirSync(current);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
current = path.dirname(current);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Diff entre una instantánea y otra (o el estado actual)
|
|
171
|
+
async diff(from, to) {
|
|
172
|
+
let target = to;
|
|
173
|
+
if (!target) {
|
|
174
|
+
target = (await this.track()) || undefined;
|
|
175
|
+
}
|
|
176
|
+
if (!target) {
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
const result = await this.git(["diff", "--no-color", "--no-ext-diff", "--stat", "--patch", from, target]);
|
|
180
|
+
return result.stdout;
|
|
181
|
+
}
|
|
182
|
+
// Limpia objetos viejos de vez en cuando para que no crezca sin fin
|
|
183
|
+
async cleanup() {
|
|
184
|
+
if (!(await this.available())) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const stamp = path.join(this.gitDir, "vipcode-gc");
|
|
188
|
+
try {
|
|
189
|
+
if (Date.now() - fs.statSync(stamp).mtimeMs < 24 * 60 * 60 * 1000) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// primera vez
|
|
195
|
+
}
|
|
196
|
+
fs.writeFileSync(stamp, String(Date.now()));
|
|
197
|
+
await this.git(["gc", "--quiet", "--prune=7.days.ago"], 120000);
|
|
198
|
+
}
|
|
199
|
+
}
|