alegra-data-agent 0.4.4 → 0.5.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/README.md +2 -2
- package/dist/api.js +101 -9
- package/dist/auth.js +12 -0
- package/dist/bounce.js +11 -0
- package/dist/colors.js +0 -9
- package/dist/commands/hook.js +2 -1
- package/dist/commands/login.js +8 -0
- package/dist/commands/status.js +23 -2
- package/dist/commands/uninstall.js +0 -16
- package/dist/commands/update.js +74 -15
- package/dist/installer.js +110 -80
- package/dist/lazy-mcp.js +24 -3
- package/dist/lib.js +232 -6
- package/dist/main.js +11 -90
- package/dist/render.js +0 -77
- package/dist/signature.js +27 -19
- package/dist/ui.js +0 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ Solo accesible para cuentas `@alegra.com` (OAuth Google). El CLI te abre el nave
|
|
|
14
14
|
|
|
15
15
|
- Node ≥ 22.6.0
|
|
16
16
|
- pnpm ≥ 11.1.2
|
|
17
|
-
- Uno de: Claude Code, Cursor, OpenCode, Antigravity, Codex
|
|
17
|
+
- Uno de: Claude Code, Cursor, OpenCode, Antigravity, Codex, Hermes Agent
|
|
18
18
|
|
|
19
19
|
## Soporte
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Guía de uso, documentación y soporte para el equipo de Alegra: entra a https://alegra-data-agent.vercel.app con tu cuenta `@alegra.com`.
|
package/dist/api.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { readAuth } from "./auth.js";
|
|
4
|
-
import { COMMAND_NAME, GATEWAY_URL, getManifestCachePath, getPackageVersion } from "./lib.js";
|
|
5
|
-
import { verifyManifestSignature } from "./signature.js";
|
|
3
|
+
import { authRecordFromToken, authRemainingMs, readAuth, writeAuth } from "./auth.js";
|
|
4
|
+
import { COMMAND_NAME, GATEWAY_URL, getManifestCachePath, getPackageVersion, readLockFile, writeLockFile, } from "./lib.js";
|
|
5
|
+
import { manifestTimestamp, verifyManifestSignature } from "./signature.js";
|
|
6
6
|
export class ManifestSignatureError extends Error {
|
|
7
7
|
constructor(reason) {
|
|
8
8
|
super(`Firma del manifest inválida: ${reason}. Posible compromiso del gateway.`);
|
|
@@ -37,8 +37,55 @@ function sleep(ms) {
|
|
|
37
37
|
return new Promise((r) => setTimeout(r, ms));
|
|
38
38
|
}
|
|
39
39
|
let rateLimitCooldownUntil = 0;
|
|
40
|
-
|
|
40
|
+
const REFRESH_AHEAD_MS = 2 * 24 * 3600_000;
|
|
41
|
+
let refreshTriedFor = null;
|
|
42
|
+
export function shouldRefreshAuth() {
|
|
43
|
+
const remaining = authRemainingMs(readAuth());
|
|
44
|
+
return remaining !== null && remaining < REFRESH_AHEAD_MS;
|
|
45
|
+
}
|
|
46
|
+
export async function refreshAuth() {
|
|
41
47
|
const auth = readAuth();
|
|
48
|
+
if (!auth)
|
|
49
|
+
return false;
|
|
50
|
+
refreshTriedFor = auth.token;
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`${GATEWAY_URL}/api/cli/refresh`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${auth.token}`,
|
|
56
|
+
Accept: "application/json",
|
|
57
|
+
"User-Agent": userAgent(),
|
|
58
|
+
"X-Request-Id": newRequestId(),
|
|
59
|
+
},
|
|
60
|
+
signal: AbortSignal.timeout(10_000),
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok)
|
|
63
|
+
return false;
|
|
64
|
+
const body = (await res.json());
|
|
65
|
+
if (typeof body.token !== "string")
|
|
66
|
+
return false;
|
|
67
|
+
const record = authRecordFromToken(body.token);
|
|
68
|
+
if (!record)
|
|
69
|
+
return false;
|
|
70
|
+
writeAuth(record);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function ensureFreshAuth() {
|
|
78
|
+
if (readAuth()?.token !== refreshTriedFor && shouldRefreshAuth())
|
|
79
|
+
await refreshAuth();
|
|
80
|
+
const auth = readAuth();
|
|
81
|
+
if (!auth)
|
|
82
|
+
return false;
|
|
83
|
+
const remaining = authRemainingMs(auth);
|
|
84
|
+
return remaining === null || remaining > 60_000;
|
|
85
|
+
}
|
|
86
|
+
async function authedFetch(path, opts = {}) {
|
|
87
|
+
await ensureFreshAuth();
|
|
88
|
+
let auth = readAuth();
|
|
42
89
|
if (!auth) {
|
|
43
90
|
if (opts.requireAuth === false) {
|
|
44
91
|
return new Response(null, { status: 401 });
|
|
@@ -84,6 +131,10 @@ async function authedFetch(path, opts = {}) {
|
|
|
84
131
|
clearTimeout(timer);
|
|
85
132
|
}
|
|
86
133
|
if (res.status === 401) {
|
|
134
|
+
if (auth.token !== refreshTriedFor && (await refreshAuth())) {
|
|
135
|
+
auth = readAuth() ?? auth;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
87
138
|
if (opts.requireAuth === false)
|
|
88
139
|
return res;
|
|
89
140
|
throw new AuthRequiredError(`Sesión expirada. Agente: ejecutá vos mismo \`pnpm dlx ${COMMAND_NAME} login\` — al usuario solo le toca el click del navegador.`);
|
|
@@ -204,8 +255,49 @@ function writeManifestCache(etag, manifest) {
|
|
|
204
255
|
catch {
|
|
205
256
|
}
|
|
206
257
|
}
|
|
207
|
-
|
|
208
|
-
|
|
258
|
+
export const LOCK_MANIFEST_FLOOR_KEY = "manifestGeneratedAt";
|
|
259
|
+
function lockFloor() {
|
|
260
|
+
try {
|
|
261
|
+
const raw = readLockFile()[LOCK_MANIFEST_FLOOR_KEY];
|
|
262
|
+
return manifestTimestamp({ generatedAt: raw });
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
export function recordManifestFloor(manifest) {
|
|
269
|
+
try {
|
|
270
|
+
const ts = manifestTimestamp(manifest);
|
|
271
|
+
if (ts === null)
|
|
272
|
+
return;
|
|
273
|
+
const current = lockFloor();
|
|
274
|
+
if (current !== null && current >= ts)
|
|
275
|
+
return;
|
|
276
|
+
const lock = readLockFile();
|
|
277
|
+
lock[LOCK_MANIFEST_FLOOR_KEY] = manifest.generatedAt;
|
|
278
|
+
writeLockFile(lock);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function trustedCacheFloor(cached) {
|
|
284
|
+
if (!cached)
|
|
285
|
+
return null;
|
|
286
|
+
try {
|
|
287
|
+
if (!verifyManifestSignature(cached.manifest, { softFail: false }).ok)
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
return manifestTimestamp(cached.manifest);
|
|
294
|
+
}
|
|
295
|
+
function maxFloor(...floors) {
|
|
296
|
+
const valid = floors.filter((f) => typeof f === "number");
|
|
297
|
+
return valid.length > 0 ? Math.max(...valid) : null;
|
|
298
|
+
}
|
|
299
|
+
function verifiedOrThrow(manifest, notOlderThan) {
|
|
300
|
+
const check = verifyManifestSignature(manifest, { softFail: false, notOlderThan });
|
|
209
301
|
if (!check.ok) {
|
|
210
302
|
throw new ManifestSignatureError(check.reason);
|
|
211
303
|
}
|
|
@@ -221,15 +313,15 @@ export async function fetchManifest(opts = {}) {
|
|
|
221
313
|
});
|
|
222
314
|
if (res.status === 304) {
|
|
223
315
|
if (cached)
|
|
224
|
-
return verifiedOrThrow(cached.manifest);
|
|
316
|
+
return verifiedOrThrow(cached.manifest, lockFloor());
|
|
225
317
|
const retry = await authedFetch("/api/manifest", { ...opts, allowNotModified: false });
|
|
226
318
|
if (!retry.ok)
|
|
227
319
|
return null;
|
|
228
|
-
return verifiedOrThrow((await retry.json()));
|
|
320
|
+
return verifiedOrThrow((await retry.json()), lockFloor());
|
|
229
321
|
}
|
|
230
322
|
if (!res.ok)
|
|
231
323
|
return null;
|
|
232
|
-
const manifest = verifiedOrThrow((await res.json()));
|
|
324
|
+
const manifest = verifiedOrThrow((await res.json()), maxFloor(trustedCacheFloor(cached), lockFloor()));
|
|
233
325
|
const etag = res.headers.get("etag");
|
|
234
326
|
if (etag)
|
|
235
327
|
writeManifestCache(etag, manifest);
|
package/dist/auth.js
CHANGED
|
@@ -83,6 +83,18 @@ export function isAuthExpired(record) {
|
|
|
83
83
|
return false;
|
|
84
84
|
return Date.now() >= exp * 1000 - 60_000;
|
|
85
85
|
}
|
|
86
|
+
export function authRemainingMs(record) {
|
|
87
|
+
if (!record)
|
|
88
|
+
return null;
|
|
89
|
+
const exp = decodeJwtExp(record.token);
|
|
90
|
+
return exp === null ? null : exp * 1000 - Date.now();
|
|
91
|
+
}
|
|
92
|
+
export function authRecordFromToken(token) {
|
|
93
|
+
const email = decodeJwtEmail(token);
|
|
94
|
+
if (!email)
|
|
95
|
+
return null;
|
|
96
|
+
return { email, token, obtainedAt: new Date().toISOString() };
|
|
97
|
+
}
|
|
86
98
|
export function hasValidAuth() {
|
|
87
99
|
const record = readAuth();
|
|
88
100
|
return record !== null && !isAuthExpired(record);
|
package/dist/bounce.js
CHANGED
|
@@ -23,6 +23,12 @@ export function shouldBounce(current, latest) {
|
|
|
23
23
|
return false;
|
|
24
24
|
return compareSemver(latest, current) > 0;
|
|
25
25
|
}
|
|
26
|
+
const WIN32_CMD_UNSAFE = /[&|<>^%!"\s]/;
|
|
27
|
+
export function unsafeBounceArgs(argv, platform = process.platform) {
|
|
28
|
+
if (platform !== "win32")
|
|
29
|
+
return [];
|
|
30
|
+
return argv.filter((a) => WIN32_CMD_UNSAFE.test(a));
|
|
31
|
+
}
|
|
26
32
|
async function fetchLatestVersion() {
|
|
27
33
|
try {
|
|
28
34
|
const res = await fetch(REGISTRY_URL, {
|
|
@@ -45,6 +51,11 @@ export async function maybeBounceToLatest(argv) {
|
|
|
45
51
|
const latest = await fetchLatestVersion();
|
|
46
52
|
if (!shouldBounce(current, latest))
|
|
47
53
|
return;
|
|
54
|
+
const unsafe = unsafeBounceArgs(argv);
|
|
55
|
+
if (unsafe.length > 0) {
|
|
56
|
+
process.stderr.write(` ⚠ No reejecuto con ${COMMAND_NAME}@${latest}: estos argumentos tienen caracteres que cmd.exe interpreta (& | < > ^ % ! " o espacios): ${unsafe.map((a) => JSON.stringify(a)).join(", ")}. Sigo con la versión ${current}.\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
48
59
|
const child = spawnSync("pnpm", ["dlx", "--config.minimumReleaseAge=0", `${COMMAND_NAME}@${latest}`, ...argv], {
|
|
49
60
|
stdio: "inherit",
|
|
50
61
|
env: { ...process.env, [NO_BOUNCE_ENV]: "1" },
|
package/dist/colors.js
CHANGED
|
@@ -5,14 +5,8 @@ export const dim = wrap("\x1b[2m", "\x1b[22m");
|
|
|
5
5
|
export const red = wrap("\x1b[31m", "\x1b[39m");
|
|
6
6
|
export const green = wrap("\x1b[32m", "\x1b[39m");
|
|
7
7
|
export const yellow = wrap("\x1b[33m", "\x1b[39m");
|
|
8
|
-
export const blue = wrap("\x1b[34m", "\x1b[39m");
|
|
9
|
-
export const magenta = wrap("\x1b[35m", "\x1b[39m");
|
|
10
8
|
export const cyan = wrap("\x1b[36m", "\x1b[39m");
|
|
11
9
|
export const gray = wrap("\x1b[90m", "\x1b[39m");
|
|
12
|
-
export const HIDE_CURSOR = SUPPORTS_COLOR ? "\x1b[?25l" : "";
|
|
13
|
-
export const SHOW_CURSOR = SUPPORTS_COLOR ? "\x1b[?25h" : "";
|
|
14
|
-
export const CLEAR_LINE = SUPPORTS_COLOR ? "\r\x1b[K" : "\r";
|
|
15
|
-
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
16
10
|
export function write(s) {
|
|
17
11
|
process.stdout.write(s);
|
|
18
12
|
}
|
|
@@ -28,6 +22,3 @@ export function logWarn(s) {
|
|
|
28
22
|
export function logOk(s) {
|
|
29
23
|
process.stdout.write(`${green("✓ ") + s}\n`);
|
|
30
24
|
}
|
|
31
|
-
export function logInfo(s) {
|
|
32
|
-
process.stdout.write(`${cyan("ℹ ") + s}\n`);
|
|
33
|
-
}
|
package/dist/commands/hook.js
CHANGED
|
@@ -9,11 +9,12 @@ function versionRangeForHook(version) {
|
|
|
9
9
|
return "latest";
|
|
10
10
|
return major === 0 ? "0.x" : `^${major}.0.0`;
|
|
11
11
|
}
|
|
12
|
+
export const AUTO_UPDATE_MIN_RELEASE_AGE_MINUTES = 0;
|
|
12
13
|
function hookCommandFor(mode) {
|
|
13
14
|
const v = getPackageVersion();
|
|
14
15
|
const range = versionRangeForHook(v);
|
|
15
16
|
const verb = mode === "auto" ? "update --yes --background" : "check";
|
|
16
|
-
return `pnpm dlx --config.minimumReleaseAge
|
|
17
|
+
return `pnpm dlx --config.minimumReleaseAge=${AUTO_UPDATE_MIN_RELEASE_AGE_MINUTES} ${COMMAND_NAME}@${range} ${verb} --no-bounce`;
|
|
17
18
|
}
|
|
18
19
|
const HOOK_SIGNATURE = COMMAND_NAME;
|
|
19
20
|
const HOOK_TIMEOUT_SECONDS = 120;
|
package/dist/commands/login.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { refreshAuth } from "../api.js";
|
|
1
2
|
import { isAuthExpired, performBrowserLogin, readAuth, writeAuth } from "../auth.js";
|
|
2
3
|
import { bold, dim, green, log, logError, logWarn } from "../colors.js";
|
|
3
4
|
import { GATEWAY_URL, getAuthFilePath } from "../lib.js";
|
|
@@ -13,6 +14,13 @@ export async function runLogin(opts = {}) {
|
|
|
13
14
|
log("");
|
|
14
15
|
return 0;
|
|
15
16
|
}
|
|
17
|
+
if (existing && (await refreshAuth())) {
|
|
18
|
+
const renewed = readAuth();
|
|
19
|
+
log(` ${green("✓")} Sesión renovada como ${bold(renewed?.email ?? existing.email)}.`);
|
|
20
|
+
log(dim(` Token: ${getAuthFilePath()}`));
|
|
21
|
+
log("");
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
16
24
|
if (existing) {
|
|
17
25
|
logWarn(`Tu sesión (${existing.email}) expiró — reabro el login.`);
|
|
18
26
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
4
|
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
5
5
|
import { readAuth } from "../auth.js";
|
|
6
6
|
import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
|
|
@@ -15,7 +15,27 @@ function reportAutoUpdateLog() {
|
|
|
15
15
|
const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
16
16
|
if (lines.length === 0)
|
|
17
17
|
return;
|
|
18
|
-
|
|
18
|
+
const last = lines[lines.length - 1];
|
|
19
|
+
if (/login no se completó/.test(last)) {
|
|
20
|
+
logWarn(`El auto-update necesita volver a iniciar sesión (el login automático no se completó ${lines.length} vez/veces) — última: "${last}". Agente: corré ${bold(`${COMMAND_NAME} update`)}, que abre el login y actualiza en el acto.`);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
logWarn(`El auto-update no pudo correr ${lines.length} vez/veces en un cliente GUI — última: "${last}". Agente: corré ${bold(`${COMMAND_NAME} update`)} (actualiza ya mismo y limpia este aviso); si reaparece, el cliente GUI no encuentra pnpm en su PATH y hay que reinstalar con ${bold(`${COMMAND_NAME} install`)} desde una terminal con pnpm disponible.`);
|
|
24
|
+
}
|
|
25
|
+
log("");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function reportSkillBackups() {
|
|
31
|
+
const backupsLog = join(dirname(getAutoUpdateLogPath()), "backups", "backups.log");
|
|
32
|
+
if (!existsSync(backupsLog))
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
const lines = readFileSync(backupsLog, "utf-8").trim().split("\n").filter(Boolean);
|
|
36
|
+
if (lines.length === 0)
|
|
37
|
+
return;
|
|
38
|
+
logWarn(`Se guardaron ${lines.length} backup(s) de skills que habías modificado antes de actualizarlas — último: "${lines[lines.length - 1]}". Están en ${dirname(backupsLog)}.`);
|
|
19
39
|
log("");
|
|
20
40
|
}
|
|
21
41
|
catch {
|
|
@@ -288,6 +308,7 @@ export async function runStatus() {
|
|
|
288
308
|
logWarn(`${scopeWarning} — los MCPs quedaron en scope usuario.`);
|
|
289
309
|
log("");
|
|
290
310
|
reportAutoUpdateLog();
|
|
311
|
+
reportSkillBackups();
|
|
291
312
|
if (!auth) {
|
|
292
313
|
logWarn(`Inicia sesión con: ${bold(`${COMMAND_NAME} login`)}`);
|
|
293
314
|
log("");
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
1
|
import { removeAutoUpdate } from "../autoupdate.js";
|
|
5
2
|
import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
|
|
6
3
|
import { uninstallAll } from "../installer.js";
|
|
@@ -37,7 +34,6 @@ export async function runUninstall(opts = {}) {
|
|
|
37
34
|
if (!opts.dryRun) {
|
|
38
35
|
for (const t of Object.keys(TARGETS))
|
|
39
36
|
removeAutoUpdate(t);
|
|
40
|
-
removeNpmrcCooldownExclude();
|
|
41
37
|
}
|
|
42
38
|
log("");
|
|
43
39
|
if (opts.dryRun) {
|
|
@@ -48,15 +44,3 @@ export async function runUninstall(opts = {}) {
|
|
|
48
44
|
}
|
|
49
45
|
return 0;
|
|
50
46
|
}
|
|
51
|
-
function removeNpmrcCooldownExclude() {
|
|
52
|
-
const npmrc = join(homedir(), ".npmrc");
|
|
53
|
-
if (!existsSync(npmrc))
|
|
54
|
-
return;
|
|
55
|
-
const EXCLUDE = "minimum-release-age-exclude[]=alegra-data-agent";
|
|
56
|
-
const lines = readFileSync(npmrc, "utf-8").split("\n");
|
|
57
|
-
const kept = lines.filter((l) => l.trim() !== EXCLUDE);
|
|
58
|
-
if (kept.length !== lines.length) {
|
|
59
|
-
writeFileSync(npmrc, kept.join("\n"));
|
|
60
|
-
logOk("Removido minimum-release-age-exclude de ~/.npmrc");
|
|
61
|
-
}
|
|
62
|
-
}
|
package/dist/commands/update.js
CHANGED
|
@@ -1,45 +1,94 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
3
|
+
import { AuthRequiredError, ensureFreshAuth, fetchManifest } from "../api.js";
|
|
4
4
|
import { hasValidAuth } from "../auth.js";
|
|
5
5
|
import { refreshAutoUpdate } from "../autoupdate.js";
|
|
6
6
|
import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
|
|
7
7
|
import { detectDrift, installAll, summarize } from "../installer.js";
|
|
8
|
-
import { getAutoUpdateLogPath, getLastCheckPath } from "../lib.js";
|
|
8
|
+
import { COMMAND_NAME, getAutoUpdateLogPath, getLastCheckPath, getPackageVersion, } from "../lib.js";
|
|
9
9
|
import { banner } from "../ui.js";
|
|
10
10
|
import { runLogin } from "./login.js";
|
|
11
11
|
const BACKGROUND_COOLDOWN_MS = 15 * 60_000;
|
|
12
|
-
const BACKGROUND_LOGIN_TIMEOUT_MS =
|
|
13
|
-
|
|
12
|
+
const BACKGROUND_LOGIN_TIMEOUT_MS = (() => {
|
|
13
|
+
const v = Number(process.env.ALEGRA_DATA_AGENT_BG_LOGIN_TIMEOUT_MS);
|
|
14
|
+
return Number.isFinite(v) && v > 0 ? v : 90_000;
|
|
15
|
+
})();
|
|
16
|
+
export const BACKGROUND_AUTH_FAIL_COOLDOWN_MS = 60 * 60_000;
|
|
17
|
+
function readLastCheck() {
|
|
14
18
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
if (Number.isNaN(at))
|
|
18
|
-
return false;
|
|
19
|
-
const age = Date.now() - at;
|
|
20
|
-
return age >= 0 && age < BACKGROUND_COOLDOWN_MS;
|
|
19
|
+
const parsed = JSON.parse(readFileSync(getLastCheckPath(), "utf-8"));
|
|
20
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
21
21
|
}
|
|
22
22
|
catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function withinWindow(iso, windowMs) {
|
|
27
|
+
const at = Date.parse(iso ?? "");
|
|
28
|
+
if (Number.isNaN(at))
|
|
23
29
|
return false;
|
|
30
|
+
const age = Date.now() - at;
|
|
31
|
+
return age >= 0 && age < windowMs;
|
|
32
|
+
}
|
|
33
|
+
function checkedRecently() {
|
|
34
|
+
return withinWindow(readLastCheck().checkedAt, BACKGROUND_COOLDOWN_MS);
|
|
35
|
+
}
|
|
36
|
+
function authFailedRecently() {
|
|
37
|
+
return withinWindow(readLastCheck().authFailedAt, BACKGROUND_AUTH_FAIL_COOLDOWN_MS);
|
|
38
|
+
}
|
|
39
|
+
function writeLastCheck(data) {
|
|
40
|
+
try {
|
|
41
|
+
const path = getLastCheckPath();
|
|
42
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
43
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
24
46
|
}
|
|
25
47
|
}
|
|
26
48
|
function markChecked() {
|
|
49
|
+
writeLastCheck({ checkedAt: new Date().toISOString() });
|
|
50
|
+
}
|
|
51
|
+
function markAuthFailed() {
|
|
52
|
+
writeLastCheck({ ...readLastCheck(), authFailedAt: new Date().toISOString() });
|
|
53
|
+
}
|
|
54
|
+
function appendHookLog(message) {
|
|
27
55
|
try {
|
|
28
|
-
const path =
|
|
56
|
+
const path = getAutoUpdateLogPath();
|
|
29
57
|
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
-
|
|
58
|
+
appendFileSync(path, `${new Date().toISOString()} ${message}\n`);
|
|
31
59
|
}
|
|
32
60
|
catch {
|
|
33
61
|
}
|
|
34
62
|
}
|
|
63
|
+
async function loginFromBackground(reason, force = false) {
|
|
64
|
+
const code = await runLogin({
|
|
65
|
+
timeoutMs: BACKGROUND_LOGIN_TIMEOUT_MS,
|
|
66
|
+
...(force ? { force: true } : {}),
|
|
67
|
+
});
|
|
68
|
+
if (code === 0)
|
|
69
|
+
return true;
|
|
70
|
+
appendHookLog(`[update --background] ${reason} y el login no se completó. Se reintenta solo en la próxima sesión pasada 1 h (o al correr \`${COMMAND_NAME} update\`).`);
|
|
71
|
+
markAuthFailed();
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
function warnStaleFromBackground() {
|
|
75
|
+
log(yellow(` ⚠ ${COMMAND_NAME} v${getPackageVersion()}: no pude actualizar las skills porque la sesión venció. Agente: corré vos mismo \`pnpm dlx ${COMMAND_NAME} login\` y después \`pnpm dlx ${COMMAND_NAME} update\` — al usuario solo le toca el clic del navegador.`));
|
|
76
|
+
}
|
|
35
77
|
export async function runUpdate(opts = {}) {
|
|
36
78
|
if (opts.background && !opts.dryRun && checkedRecently())
|
|
37
79
|
return 0;
|
|
80
|
+
const fresh = await ensureFreshAuth();
|
|
81
|
+
if (opts.background && !fresh) {
|
|
82
|
+
if (authFailedRecently() || !(await loginFromBackground("sesión vencida o ausente"))) {
|
|
83
|
+
warnStaleFromBackground();
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
38
87
|
banner();
|
|
39
88
|
if (!hasValidAuth()) {
|
|
40
|
-
const code = await runLogin(
|
|
89
|
+
const code = await runLogin({});
|
|
41
90
|
if (code !== 0)
|
|
42
|
-
return
|
|
91
|
+
return code;
|
|
43
92
|
}
|
|
44
93
|
let reauthed = false;
|
|
45
94
|
for (;;) {
|
|
@@ -47,6 +96,14 @@ export async function runUpdate(opts = {}) {
|
|
|
47
96
|
return await runUpdateOnce(opts);
|
|
48
97
|
}
|
|
49
98
|
catch (err) {
|
|
99
|
+
if (err instanceof AuthRequiredError && opts.background && !reauthed) {
|
|
100
|
+
reauthed = true;
|
|
101
|
+
if (!(await loginFromBackground("el gateway rechazó la sesión", true))) {
|
|
102
|
+
warnStaleFromBackground();
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
50
107
|
if (err instanceof AuthRequiredError && !reauthed) {
|
|
51
108
|
reauthed = true;
|
|
52
109
|
log(dim(" La sesión expiró durante la actualización; reabro el login y reintento.\n"));
|
|
@@ -56,6 +113,8 @@ export async function runUpdate(opts = {}) {
|
|
|
56
113
|
continue;
|
|
57
114
|
}
|
|
58
115
|
if (err instanceof AuthRequiredError) {
|
|
116
|
+
if (opts.background)
|
|
117
|
+
return 0;
|
|
59
118
|
logError(err.message);
|
|
60
119
|
return 2;
|
|
61
120
|
}
|