alegra-data-agent 0.2.3 → 0.2.5
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/api.js +83 -16
- package/dist/auth.js +23 -8
- package/dist/autoupdate.js +216 -0
- package/dist/commands/hook.js +41 -45
- package/dist/commands/install.js +25 -16
- package/dist/commands/login.js +1 -1
- package/dist/commands/status.js +125 -5
- package/dist/commands/uninstall.js +16 -4
- package/dist/commands/update.js +56 -8
- package/dist/installer.js +247 -72
- package/dist/lib.js +58 -28
- package/dist/main.js +4 -0
- package/dist/render.js +3 -0
- package/dist/signature.js +17 -5
- package/dist/targets.js +10 -7
- package/dist/ui.js +2 -2
- package/package.json +12 -9
package/dist/api.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
1
3
|
import { readAuth } from "./auth.js";
|
|
2
|
-
import { COMMAND_NAME, GATEWAY_URL, getPackageVersion } from "./lib.js";
|
|
4
|
+
import { COMMAND_NAME, GATEWAY_URL, getManifestCachePath, getPackageVersion } from "./lib.js";
|
|
3
5
|
import { verifyManifestSignature } from "./signature.js";
|
|
4
6
|
export class ManifestSignatureError extends Error {
|
|
5
7
|
constructor(reason) {
|
|
@@ -29,11 +31,12 @@ function newRequestId() {
|
|
|
29
31
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
30
32
|
}
|
|
31
33
|
function userAgent() {
|
|
32
|
-
return `${COMMAND_NAME}/${getPackageVersion()} (+https://github.com/
|
|
34
|
+
return `${COMMAND_NAME}/${getPackageVersion()} (+https://github.com/Alegra-Team/data-agent-selfservice)`;
|
|
33
35
|
}
|
|
34
36
|
function sleep(ms) {
|
|
35
37
|
return new Promise((r) => setTimeout(r, ms));
|
|
36
38
|
}
|
|
39
|
+
let rateLimitCooldownUntil = 0;
|
|
37
40
|
async function authedFetch(path, opts = {}) {
|
|
38
41
|
const auth = readAuth();
|
|
39
42
|
if (!auth) {
|
|
@@ -49,6 +52,9 @@ async function authedFetch(path, opts = {}) {
|
|
|
49
52
|
let attempt = 0;
|
|
50
53
|
const delays = [250, 750, 2250];
|
|
51
54
|
while (true) {
|
|
55
|
+
const pending = rateLimitCooldownUntil - Date.now();
|
|
56
|
+
if (pending > 0)
|
|
57
|
+
await sleep(pending);
|
|
52
58
|
const ac = new AbortController();
|
|
53
59
|
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
54
60
|
let res;
|
|
@@ -59,6 +65,7 @@ async function authedFetch(path, opts = {}) {
|
|
|
59
65
|
Accept: "application/json,application/octet-stream",
|
|
60
66
|
"User-Agent": userAgent(),
|
|
61
67
|
"X-Request-Id": reqId,
|
|
68
|
+
...opts.headers,
|
|
62
69
|
},
|
|
63
70
|
signal: ac.signal,
|
|
64
71
|
});
|
|
@@ -86,13 +93,16 @@ async function authedFetch(path, opts = {}) {
|
|
|
86
93
|
"Si saliste de Alegra, esto es esperado. Si no, contacta a #data.");
|
|
87
94
|
}
|
|
88
95
|
if (res.status === 429) {
|
|
96
|
+
const cap = maxRetries + 4;
|
|
89
97
|
const ra = parseRetryAfter(res.headers.get("retry-after"));
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
const wait = ra !== null ? ra : delays[Math.min(attempt, delays.length - 1)] * 2;
|
|
99
|
+
rateLimitCooldownUntil = Math.max(rateLimitCooldownUntil, Date.now() + wait);
|
|
100
|
+
if (attempt < cap) {
|
|
101
|
+
await sleep(wait);
|
|
92
102
|
attempt++;
|
|
93
103
|
continue;
|
|
94
104
|
}
|
|
95
|
-
throw new GatewayError(429, path,
|
|
105
|
+
throw new GatewayError(429, path, `Rate limit del gateway tras ${cap + 1} intentos. Esperá un minuto y reintentá (request-id ${reqId}).`);
|
|
96
106
|
}
|
|
97
107
|
if (res.status >= 500 && res.status < 600) {
|
|
98
108
|
if (attempt < maxRetries) {
|
|
@@ -102,6 +112,8 @@ async function authedFetch(path, opts = {}) {
|
|
|
102
112
|
}
|
|
103
113
|
throw new GatewayError(res.status, path, `Gateway con error ${res.status}. Reintenta en unos segundos. Si persiste, reporta a #data (request-id ${reqId}).`);
|
|
104
114
|
}
|
|
115
|
+
if (res.status === 304 && opts.allowNotModified)
|
|
116
|
+
return res;
|
|
105
117
|
if (!res.ok) {
|
|
106
118
|
throw new GatewayError(res.status, path, `Gateway respondió ${res.status} en ${path}`);
|
|
107
119
|
}
|
|
@@ -151,16 +163,76 @@ function mapNetworkError(err, path, timeoutMs) {
|
|
|
151
163
|
}
|
|
152
164
|
return new GatewayError(0, path, `Error de red al hablar con el gateway: ${err.message}`);
|
|
153
165
|
}
|
|
166
|
+
export function parseManifestCache(raw) {
|
|
167
|
+
try {
|
|
168
|
+
const parsed = JSON.parse(raw);
|
|
169
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
170
|
+
return null;
|
|
171
|
+
const { etag, manifest } = parsed;
|
|
172
|
+
if (typeof etag !== "string" || etag.length === 0)
|
|
173
|
+
return null;
|
|
174
|
+
if (typeof manifest !== "object" || manifest === null)
|
|
175
|
+
return null;
|
|
176
|
+
return { etag, manifest: manifest };
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function readManifestCache() {
|
|
183
|
+
try {
|
|
184
|
+
return parseManifestCache(readFileSync(getManifestCachePath(), "utf-8"));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function writeManifestCache(etag, manifest) {
|
|
191
|
+
const path = getManifestCachePath();
|
|
192
|
+
try {
|
|
193
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
194
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
195
|
+
writeFileSync(tmp, `${JSON.stringify({ etag, manifest })}\n`, { mode: 0o600 });
|
|
196
|
+
try {
|
|
197
|
+
renameSync(tmp, path);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
rmSync(tmp, { force: true });
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function verifiedOrThrow(manifest) {
|
|
208
|
+
const check = verifyManifestSignature(manifest, { softFail: false });
|
|
209
|
+
if (!check.ok) {
|
|
210
|
+
throw new ManifestSignatureError(check.reason);
|
|
211
|
+
}
|
|
212
|
+
return manifest;
|
|
213
|
+
}
|
|
154
214
|
export async function fetchManifest(opts = {}) {
|
|
155
215
|
try {
|
|
156
|
-
const
|
|
216
|
+
const cached = readManifestCache();
|
|
217
|
+
const res = await authedFetch("/api/manifest", {
|
|
218
|
+
...opts,
|
|
219
|
+
allowNotModified: true,
|
|
220
|
+
headers: cached ? { ...opts.headers, "If-None-Match": cached.etag } : opts.headers,
|
|
221
|
+
});
|
|
222
|
+
if (res.status === 304) {
|
|
223
|
+
if (cached)
|
|
224
|
+
return verifiedOrThrow(cached.manifest);
|
|
225
|
+
const retry = await authedFetch("/api/manifest", { ...opts, allowNotModified: false });
|
|
226
|
+
if (!retry.ok)
|
|
227
|
+
return null;
|
|
228
|
+
return verifiedOrThrow((await retry.json()));
|
|
229
|
+
}
|
|
157
230
|
if (!res.ok)
|
|
158
231
|
return null;
|
|
159
|
-
const manifest = (await res.json());
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
}
|
|
232
|
+
const manifest = verifiedOrThrow((await res.json()));
|
|
233
|
+
const etag = res.headers.get("etag");
|
|
234
|
+
if (etag)
|
|
235
|
+
writeManifestCache(etag, manifest);
|
|
164
236
|
return manifest;
|
|
165
237
|
}
|
|
166
238
|
catch (err) {
|
|
@@ -178,8 +250,3 @@ export async function fetchSkillFile(skillName, relPath) {
|
|
|
178
250
|
const buffer = await res.arrayBuffer();
|
|
179
251
|
return new Uint8Array(buffer);
|
|
180
252
|
}
|
|
181
|
-
export async function fetchMcps(opts = {}) {
|
|
182
|
-
const res = await authedFetch("/api/mcps", opts);
|
|
183
|
-
const body = (await res.json());
|
|
184
|
-
return body.mcps ?? {};
|
|
185
|
-
}
|
package/dist/auth.js
CHANGED
|
@@ -25,19 +25,26 @@ function constantTimeStringEq(a, b) {
|
|
|
25
25
|
return false;
|
|
26
26
|
return timingSafeEqual(ab, bb);
|
|
27
27
|
}
|
|
28
|
+
let _authCache = null;
|
|
28
29
|
export function readAuth() {
|
|
30
|
+
if (_authCache)
|
|
31
|
+
return _authCache.value;
|
|
29
32
|
const path = getAuthFilePath();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
let value = null;
|
|
34
|
+
if (existsSync(path)) {
|
|
35
|
+
try {
|
|
36
|
+
value = JSON.parse(readFileSync(path, "utf-8"));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
value = null;
|
|
40
|
+
}
|
|
37
41
|
}
|
|
42
|
+
_authCache = { value };
|
|
43
|
+
return value;
|
|
38
44
|
}
|
|
39
45
|
export function writeAuth(record) {
|
|
40
46
|
const path = getAuthFilePath();
|
|
47
|
+
_authCache = { value: record };
|
|
41
48
|
mkdirSync(dirname(path), { recursive: true });
|
|
42
49
|
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
|
|
43
50
|
try {
|
|
@@ -48,6 +55,7 @@ export function writeAuth(record) {
|
|
|
48
55
|
}
|
|
49
56
|
export function clearAuth() {
|
|
50
57
|
const path = getAuthFilePath();
|
|
58
|
+
_authCache = { value: null };
|
|
51
59
|
if (existsSync(path))
|
|
52
60
|
rmSync(path, { force: true });
|
|
53
61
|
}
|
|
@@ -278,6 +286,13 @@ function htmlPage(kind, subject) {
|
|
|
278
286
|
</body>
|
|
279
287
|
</html>`;
|
|
280
288
|
}
|
|
289
|
+
const HTML_ESCAPES = {
|
|
290
|
+
"&": "&",
|
|
291
|
+
"<": "<",
|
|
292
|
+
">": ">",
|
|
293
|
+
'"': """,
|
|
294
|
+
"'": "'",
|
|
295
|
+
};
|
|
281
296
|
function escapeHtml(s) {
|
|
282
|
-
return s.replace(/[&<>"']/g, (c) =>
|
|
297
|
+
return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c] ?? c);
|
|
283
298
|
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { logWarn } from "./colors.js";
|
|
4
|
+
import { hookCommandFor, runHookInstall, runHookUninstall, } from "./commands/hook.js";
|
|
5
|
+
import { COMMAND_NAME, getClaudeSettingsPath, getCodexHooksPath, getCursorHooksPath, getGeminiSettingsPath, getOpenCodePluginPath, readLockFile, } from "./lib.js";
|
|
6
|
+
function readJsonSafe(path) {
|
|
7
|
+
if (!existsSync(path))
|
|
8
|
+
return {};
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function writeJson(path, data) {
|
|
17
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
18
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
19
|
+
writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
20
|
+
renameSync(tmp, path);
|
|
21
|
+
}
|
|
22
|
+
function entryIsOurs(m) {
|
|
23
|
+
if (!m || typeof m !== "object")
|
|
24
|
+
return false;
|
|
25
|
+
const o = m;
|
|
26
|
+
if (typeof o.command === "string" && o.command.includes(COMMAND_NAME))
|
|
27
|
+
return true;
|
|
28
|
+
if (Array.isArray(o.hooks)) {
|
|
29
|
+
return o.hooks.some((h) => h &&
|
|
30
|
+
typeof h === "object" &&
|
|
31
|
+
typeof h.command === "string" &&
|
|
32
|
+
h.command.includes(COMMAND_NAME));
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
function freshHookList(cfg, key) {
|
|
37
|
+
const hooks = (cfg.hooks && typeof cfg.hooks === "object" ? cfg.hooks : {});
|
|
38
|
+
cfg.hooks = hooks;
|
|
39
|
+
const list = Array.isArray(hooks[key]) ? hooks[key] : [];
|
|
40
|
+
return list.filter((m) => !entryIsOurs(m));
|
|
41
|
+
}
|
|
42
|
+
export function guiResilientCommand(base, opts) {
|
|
43
|
+
const platform = opts.platform ?? process.platform;
|
|
44
|
+
if (platform === "win32")
|
|
45
|
+
return opts.silence ? `${base} >NUL 2>&1` : base;
|
|
46
|
+
const extraDirs = [
|
|
47
|
+
dirname(process.execPath),
|
|
48
|
+
process.env.PNPM_HOME ?? "",
|
|
49
|
+
"$HOME/.local/share/pnpm",
|
|
50
|
+
"$HOME/Library/pnpm",
|
|
51
|
+
"/opt/homebrew/bin",
|
|
52
|
+
"/usr/local/bin",
|
|
53
|
+
].filter(Boolean);
|
|
54
|
+
const run = opts.silence ? `${base} >/dev/null 2>&1` : base;
|
|
55
|
+
const logFile = `"\${XDG_CONFIG_HOME:-$HOME/.config}/${COMMAND_NAME}/autoupdate.log"`;
|
|
56
|
+
return (`PATH="$PATH:${extraDirs.join(":")}"; ` +
|
|
57
|
+
`if command -v pnpm >/dev/null 2>&1; then ${run}; ` +
|
|
58
|
+
`else mkdir -p "$(dirname ${logFile})" && ` +
|
|
59
|
+
`echo "$(date '+%Y-%m-%d %H:%M:%S') [${opts.client}] pnpm no esta en el PATH del cliente; auto-update omitido" >> ${logFile}; fi`);
|
|
60
|
+
}
|
|
61
|
+
function installCursor(command) {
|
|
62
|
+
const path = getCursorHooksPath();
|
|
63
|
+
const cfg = readJsonSafe(path);
|
|
64
|
+
if (cfg === null) {
|
|
65
|
+
logWarn(`${path} no es JSON válido; salteo el auto-update de Cursor.`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (typeof cfg.version !== "number")
|
|
69
|
+
cfg.version = 1;
|
|
70
|
+
const list = freshHookList(cfg, "workspaceOpen");
|
|
71
|
+
list.push({ command });
|
|
72
|
+
cfg.hooks.workspaceOpen = list;
|
|
73
|
+
writeJson(path, cfg);
|
|
74
|
+
}
|
|
75
|
+
function installCodex(command) {
|
|
76
|
+
const path = getCodexHooksPath();
|
|
77
|
+
const cfg = readJsonSafe(path);
|
|
78
|
+
if (cfg === null) {
|
|
79
|
+
logWarn(`${path} no es JSON válido; salteo el auto-update de Codex.`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const list = freshHookList(cfg, "SessionStart");
|
|
83
|
+
list.push({ matcher: "startup|resume", hooks: [{ type: "command", command }] });
|
|
84
|
+
cfg.hooks.SessionStart = list;
|
|
85
|
+
writeJson(path, cfg);
|
|
86
|
+
}
|
|
87
|
+
function installAntigravity(command) {
|
|
88
|
+
const path = getGeminiSettingsPath();
|
|
89
|
+
const cfg = readJsonSafe(path);
|
|
90
|
+
if (cfg === null) {
|
|
91
|
+
logWarn(`${path} no es JSON válido; salteo el auto-update de Antigravity.`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const list = freshHookList(cfg, "SessionStart");
|
|
95
|
+
list.push({ hooks: [{ type: "command", command }] });
|
|
96
|
+
cfg.hooks.SessionStart = list;
|
|
97
|
+
writeJson(path, cfg);
|
|
98
|
+
}
|
|
99
|
+
function installOpenCode(command) {
|
|
100
|
+
const path = getOpenCodePluginPath();
|
|
101
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
102
|
+
const plugin = `// alegra-data-agent: auto-update al iniciar OpenCode. Generado por el instalador.
|
|
103
|
+
// Borrar este archivo desactiva el auto-update. No editar a mano.
|
|
104
|
+
export const AlegraDataAgentAutoUpdate = async ({ $ }) => {
|
|
105
|
+
if (!$) return {}
|
|
106
|
+
try {
|
|
107
|
+
$\`${command}\`.quiet().nothrow().catch(() => {})
|
|
108
|
+
} catch (_e) {}
|
|
109
|
+
return {}
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
writeFileSync(path, plugin, "utf-8");
|
|
113
|
+
}
|
|
114
|
+
function removeJsonHook(path, key) {
|
|
115
|
+
const cfg = readJsonSafe(path);
|
|
116
|
+
if (!cfg)
|
|
117
|
+
return;
|
|
118
|
+
const hooks = cfg.hooks;
|
|
119
|
+
if (!hooks || !Array.isArray(hooks[key]))
|
|
120
|
+
return;
|
|
121
|
+
const list = hooks[key];
|
|
122
|
+
const kept = list.filter((m) => !entryIsOurs(m));
|
|
123
|
+
if (kept.length === list.length)
|
|
124
|
+
return;
|
|
125
|
+
if (kept.length === 0)
|
|
126
|
+
delete hooks[key];
|
|
127
|
+
else
|
|
128
|
+
hooks[key] = kept;
|
|
129
|
+
writeJson(path, cfg);
|
|
130
|
+
}
|
|
131
|
+
export function installAutoUpdate(target, mode = "auto") {
|
|
132
|
+
const effective = target === "claude-code" ? mode : "auto";
|
|
133
|
+
try {
|
|
134
|
+
switch (target) {
|
|
135
|
+
case "claude-code":
|
|
136
|
+
runHookInstall(effective);
|
|
137
|
+
break;
|
|
138
|
+
case "cursor":
|
|
139
|
+
installCursor(guiResilientCommand(hookCommandFor(effective), { client: "cursor" }));
|
|
140
|
+
break;
|
|
141
|
+
case "codex":
|
|
142
|
+
installCodex(hookCommandFor(effective));
|
|
143
|
+
break;
|
|
144
|
+
case "antigravity":
|
|
145
|
+
installAntigravity(guiResilientCommand(hookCommandFor(effective), { client: "antigravity", silence: true }));
|
|
146
|
+
break;
|
|
147
|
+
case "opencode":
|
|
148
|
+
installOpenCode(hookCommandFor(effective));
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
logWarn(`No pude instalar el auto-update de ${target}: ${err instanceof Error ? err.message : String(err)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export function removeAutoUpdate(target) {
|
|
157
|
+
try {
|
|
158
|
+
switch (target) {
|
|
159
|
+
case "claude-code":
|
|
160
|
+
runHookUninstall({ quiet: true });
|
|
161
|
+
break;
|
|
162
|
+
case "cursor":
|
|
163
|
+
removeJsonHook(getCursorHooksPath(), "workspaceOpen");
|
|
164
|
+
break;
|
|
165
|
+
case "codex":
|
|
166
|
+
removeJsonHook(getCodexHooksPath(), "SessionStart");
|
|
167
|
+
break;
|
|
168
|
+
case "antigravity":
|
|
169
|
+
removeJsonHook(getGeminiSettingsPath(), "SessionStart");
|
|
170
|
+
break;
|
|
171
|
+
case "opencode": {
|
|
172
|
+
const p = getOpenCodePluginPath();
|
|
173
|
+
if (existsSync(p))
|
|
174
|
+
rmSync(p, { force: true });
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
logWarn(`No pude quitar el auto-update de ${target}: ${err instanceof Error ? err.message : String(err)}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function jsonHookHasOurs(path, key) {
|
|
184
|
+
const cfg = readJsonSafe(path);
|
|
185
|
+
const hooks = cfg?.hooks;
|
|
186
|
+
if (!hooks || !Array.isArray(hooks[key]))
|
|
187
|
+
return false;
|
|
188
|
+
return hooks[key].some(entryIsOurs);
|
|
189
|
+
}
|
|
190
|
+
export function hasAutoUpdate(target) {
|
|
191
|
+
switch (target) {
|
|
192
|
+
case "claude-code":
|
|
193
|
+
return jsonHookHasOurs(getClaudeSettingsPath(), "SessionStart");
|
|
194
|
+
case "cursor":
|
|
195
|
+
return jsonHookHasOurs(getCursorHooksPath(), "workspaceOpen");
|
|
196
|
+
case "codex":
|
|
197
|
+
return jsonHookHasOurs(getCodexHooksPath(), "SessionStart");
|
|
198
|
+
case "antigravity":
|
|
199
|
+
return jsonHookHasOurs(getGeminiSettingsPath(), "SessionStart");
|
|
200
|
+
case "opencode":
|
|
201
|
+
return existsSync(getOpenCodePluginPath());
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
export function refreshAutoUpdate() {
|
|
205
|
+
const lock = readLockFile();
|
|
206
|
+
const targets = [
|
|
207
|
+
...new Set([
|
|
208
|
+
...Object.values(lock.skills).flatMap((s) => s.targets ?? []),
|
|
209
|
+
...Object.values(lock.mcps ?? {}).flatMap((m) => m.targets ?? []),
|
|
210
|
+
]),
|
|
211
|
+
];
|
|
212
|
+
for (const t of targets) {
|
|
213
|
+
if (hasAutoUpdate(t))
|
|
214
|
+
installAutoUpdate(t, "auto");
|
|
215
|
+
}
|
|
216
|
+
}
|
package/dist/commands/hook.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
2
3
|
import { logError, logOk, logWarn } from "../colors.js";
|
|
3
4
|
import { COMMAND_NAME, PRODUCT_NAME, getClaudeSettingsPath, getPackageVersion } from "../lib.js";
|
|
5
|
+
function versionRangeForHook(version) {
|
|
6
|
+
const [rawMajor] = version.split(".");
|
|
7
|
+
const major = Number.parseInt(rawMajor ?? "0", 10);
|
|
8
|
+
if (!Number.isFinite(major))
|
|
9
|
+
return "latest";
|
|
10
|
+
return major === 0 ? "0.x" : `^${major}.0.0`;
|
|
11
|
+
}
|
|
4
12
|
function hookCommandFor(mode) {
|
|
5
13
|
const v = getPackageVersion();
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const minor = parts[1] ?? "0";
|
|
9
|
-
const range = `~${major}.${minor}.0`;
|
|
10
|
-
const verb = mode === "auto" ? "update --yes" : "check";
|
|
14
|
+
const range = versionRangeForHook(v);
|
|
15
|
+
const verb = mode === "auto" ? "update --yes --background" : "check";
|
|
11
16
|
return `pnpm dlx --config.minimumReleaseAge=0 ${COMMAND_NAME}@${range} ${verb} --no-bounce`;
|
|
12
17
|
}
|
|
13
18
|
const HOOK_SIGNATURE = COMMAND_NAME;
|
|
19
|
+
const HOOK_TIMEOUT_SECONDS = 120;
|
|
14
20
|
function readSettings() {
|
|
15
21
|
const path = getClaudeSettingsPath();
|
|
16
22
|
if (!existsSync(path))
|
|
@@ -20,11 +26,15 @@ function readSettings() {
|
|
|
20
26
|
}
|
|
21
27
|
catch {
|
|
22
28
|
logError(`No se pudo parsear ${path}. Agente: reparalo vos mismo (la skill agent-onboarding tiene la guía) — no le pidas al usuario editar JSON.`);
|
|
23
|
-
return
|
|
29
|
+
return null;
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
function writeSettings(s) {
|
|
27
|
-
|
|
33
|
+
const path = getClaudeSettingsPath();
|
|
34
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
35
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
36
|
+
writeFileSync(tmp, `${JSON.stringify(s, null, 2)}\n`, "utf-8");
|
|
37
|
+
renameSync(tmp, path);
|
|
28
38
|
}
|
|
29
39
|
function isOurMatcher(m) {
|
|
30
40
|
if (!m || typeof m !== "object")
|
|
@@ -39,10 +49,12 @@ function isOurMatcher(m) {
|
|
|
39
49
|
h.command.includes(HOOK_SIGNATURE));
|
|
40
50
|
}
|
|
41
51
|
function buildOurMatcher(command) {
|
|
42
|
-
return { hooks: [{ type: "command", command }] };
|
|
52
|
+
return { hooks: [{ type: "command", command, timeout: HOOK_TIMEOUT_SECONDS }] };
|
|
43
53
|
}
|
|
44
54
|
export function runHookInstall(mode = "auto") {
|
|
45
55
|
const settings = readSettings();
|
|
56
|
+
if (settings === null)
|
|
57
|
+
return 1;
|
|
46
58
|
settings.hooks = settings.hooks ?? {};
|
|
47
59
|
const command = hookCommandFor(mode);
|
|
48
60
|
const existing = settings.hooks.SessionStart;
|
|
@@ -82,63 +94,47 @@ export function runHookInstall(mode = "auto") {
|
|
|
82
94
|
}
|
|
83
95
|
return 0;
|
|
84
96
|
}
|
|
85
|
-
export function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (!settings.hooks)
|
|
89
|
-
return;
|
|
90
|
-
const existing = settings.hooks.SessionStart;
|
|
91
|
-
if (!Array.isArray(existing))
|
|
92
|
-
return;
|
|
93
|
-
const ours = existing.find(isOurMatcher);
|
|
94
|
-
if (!ours)
|
|
95
|
-
return;
|
|
96
|
-
const current = ours.hooks.find((h) => h.type === "command")?.command ?? "";
|
|
97
|
-
const fresh = hookCommandFor("auto");
|
|
98
|
-
if (current === fresh)
|
|
99
|
-
return;
|
|
100
|
-
const others = existing.filter((m) => !isOurMatcher(m));
|
|
101
|
-
others.push(buildOurMatcher(fresh));
|
|
102
|
-
settings.hooks.SessionStart = others;
|
|
103
|
-
writeSettings(settings);
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
export function runHookUninstall() {
|
|
97
|
+
export function runHookUninstall(opts = {}) {
|
|
98
|
+
const say = opts.quiet ? () => { } : logWarn;
|
|
99
|
+
const sayOk = opts.quiet ? () => { } : logOk;
|
|
109
100
|
const settings = readSettings();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
101
|
+
if (settings === null) {
|
|
102
|
+
say("settings.json corrupto — no lo toco. Agente: reparalo vos mismo (la skill agent-onboarding tiene la guía) y re-corré uninstall — no le pidas al usuario editar JSON.");
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
const hooks = settings.hooks;
|
|
106
|
+
const existing = hooks?.SessionStart;
|
|
107
|
+
if (!hooks || existing === undefined) {
|
|
108
|
+
say("No había hook que desinstalar.");
|
|
113
109
|
return 0;
|
|
114
110
|
}
|
|
115
111
|
if (typeof existing === "string") {
|
|
116
112
|
if (!existing.includes(HOOK_SIGNATURE)) {
|
|
117
|
-
|
|
113
|
+
say(`El SessionStart hook actual no es de ${PRODUCT_NAME}. No lo toco.`);
|
|
118
114
|
return 1;
|
|
119
115
|
}
|
|
120
|
-
|
|
116
|
+
hooks.SessionStart = undefined;
|
|
121
117
|
writeSettings(settings);
|
|
122
|
-
|
|
118
|
+
sayOk("Hook removido.");
|
|
123
119
|
return 0;
|
|
124
120
|
}
|
|
125
121
|
if (!Array.isArray(existing)) {
|
|
126
|
-
|
|
122
|
+
say("No había hook que desinstalar.");
|
|
127
123
|
return 0;
|
|
128
124
|
}
|
|
129
125
|
const filtered = existing.filter((m) => !isOurMatcher(m));
|
|
130
126
|
if (filtered.length === existing.length) {
|
|
131
|
-
|
|
127
|
+
say(`No encontré ningún SessionStart hook de ${PRODUCT_NAME}. No lo toco.`);
|
|
132
128
|
return 1;
|
|
133
129
|
}
|
|
134
130
|
if (filtered.length === 0) {
|
|
135
|
-
|
|
131
|
+
hooks.SessionStart = undefined;
|
|
136
132
|
}
|
|
137
133
|
else {
|
|
138
|
-
|
|
134
|
+
hooks.SessionStart = filtered;
|
|
139
135
|
}
|
|
140
136
|
writeSettings(settings);
|
|
141
|
-
|
|
137
|
+
sayOk("Hook removido.");
|
|
142
138
|
return 0;
|
|
143
139
|
}
|
|
144
|
-
export { hookCommandFor };
|
|
140
|
+
export { hookCommandFor, versionRangeForHook };
|