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/dist/installer.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { chmodSync, copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
+ import { appendFileSync, chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
4
4
  import { basename, dirname, join, resolve, sep } from "node:path";
5
- import { AuthRequiredError, fetchManifest, fetchSkillFile } from "./api.js";
6
- import { dim, gray, green, logError, red, yellow } from "./colors.js";
5
+ import { AuthRequiredError, fetchManifest, fetchSkillFile, recordManifestFloor } from "./api.js";
6
+ import { dim, gray, green, logError, logWarn, red, yellow } from "./colors.js";
7
7
  import { ensureLazyProxy, lazyMcpDir, lazyProxyPath, lazyToolsPath, stdioLaunch, } from "./lazy-mcp.js";
8
- import { ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getOpenCodeLegacyPromptsDir, getPackageVersion, getUserScopeMcpConfigPath, isMcpScopeEnvOverride, readLockFile, sha256File, writeLockFile, } from "./lib.js";
9
- import { inlineReferences, renderHermesMcpOps, renderMcpForTarget, } from "./render.js";
8
+ import { computeBundleHash, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getConfigDir, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getOpenCodeLegacyPromptsDir, getPackageVersion, getUserScopeMcpConfigPath, isMcpScopeEnvOverride, readLockFile, sha256File, walkSkillFiles, writeLockFile, } from "./lib.js";
9
+ import { renderHermesMcpOps, renderMcpForTarget } from "./render.js";
10
10
  import { TARGETS, detectInstalledTargets } from "./targets.js";
11
11
  const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
12
12
  const MCP_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
@@ -120,66 +120,100 @@ async function stageSkill(skillName, entry) {
120
120
  function skillDirsFor(spec) {
121
121
  return [spec.skillsDir, ...(spec.mirrorSkillsDirs ?? [])];
122
122
  }
123
- function writeSkillToTarget(skillName, staged, spec) {
124
- validateSkillName(skillName);
125
- if (spec.skillFormat === "folder") {
126
- for (const base of skillDirsFor(spec)) {
127
- const dst = join(base, skillName);
128
- if (existsSync(dst))
129
- withFsRetry(() => rmSync(dst, { recursive: true, force: true }));
130
- ensureDir(dst);
131
- for (const f of staged) {
132
- const dstFile = safeJoinSkillFile(dst, f.rel);
133
- ensureDir(dirname(dstFile));
134
- withFsRetry(() => writeFileSync(dstFile, f.data));
135
- }
123
+ const OS_JUNK_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
124
+ function diskBundleHash(dir) {
125
+ const shas = {};
126
+ for (const rel of walkSkillFiles(dir)) {
127
+ if (OS_JUNK_FILES.has(basename(rel)))
128
+ continue;
129
+ shas[rel] = sha256File(join(dir, rel));
130
+ }
131
+ return computeBundleHash(shas);
132
+ }
133
+ function localDateStamp(d = new Date()) {
134
+ const p = (n) => String(n).padStart(2, "0");
135
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
136
+ }
137
+ export function skillBackupRoot(target) {
138
+ return join(getConfigDir(), "backups", target);
139
+ }
140
+ export function skillBackupLogPath() {
141
+ return join(getConfigDir(), "backups", "backups.log");
142
+ }
143
+ function moveToBackup(dir, skillName, backupRoot) {
144
+ mkdirSync(backupRoot, { recursive: true });
145
+ const base = join(backupRoot, `${skillName}-${localDateStamp()}`);
146
+ let dst = base;
147
+ for (let n = 2; existsSync(dst); n++)
148
+ dst = `${base}-${n}`;
149
+ try {
150
+ withFsRetry(() => renameSync(dir, dst));
151
+ }
152
+ catch (err) {
153
+ if (err.code !== "EXDEV")
154
+ throw err;
155
+ cpSync(dir, dst, { recursive: true });
156
+ withFsRetry(() => rmSync(dir, { recursive: true, force: true }));
157
+ }
158
+ return dst;
159
+ }
160
+ function noteSkillBackup(skillName, from, to) {
161
+ const line = `${skillName}: tenías cambios locales en ${from}; los guardé en ${to} antes de actualizar.`;
162
+ logWarn(line);
163
+ try {
164
+ mkdirSync(dirname(skillBackupLogPath()), { recursive: true });
165
+ appendFileSync(skillBackupLogPath(), `${new Date().toISOString()} ${line}\n`);
166
+ }
167
+ catch {
168
+ }
169
+ }
170
+ export function replaceSkillDir(dst, skillName, staged, opts) {
171
+ let backup = null;
172
+ if (existsSync(dst)) {
173
+ const known = new Set(opts.knownBundleHashes.filter((h) => !!h));
174
+ if (!known.has(diskBundleHash(dst))) {
175
+ backup = moveToBackup(dst, skillName, opts.backupRoot);
176
+ noteSkillBackup(skillName, dst, backup);
177
+ }
178
+ else {
179
+ withFsRetry(() => rmSync(dst, { recursive: true, force: true }));
136
180
  }
137
- return;
138
181
  }
139
- const entry = staged.find((f) => f.rel === "SKILL.md") ?? staged.find((f) => f.rel === "README.md");
140
- if (!entry) {
141
- throw new Error(`Para target ${spec.id} se requiere SKILL.md o README.md en la skill`);
142
- }
143
- const references = staged
144
- .filter((f) => f.rel !== "SKILL.md" && f.rel !== "README.md")
145
- .map((f) => ({ rel: f.rel, text: new TextDecoder().decode(f.data) }));
146
- const content = new TextDecoder().decode(entry.data);
147
- const rendered = renderSkillForTargetFromContent(content, skillName, spec.skillFormat, references);
148
- ensureDir(spec.skillsDir);
149
- const dst = join(spec.skillsDir, rendered.filename);
150
- atomicWriteFile(dst, rendered.content);
151
- }
152
- function renderSkillForTargetFromContent(content, skillName, format, references = []) {
153
- if (format === "folder") {
154
- throw new Error("renderSkillForTargetFromContent no soporta 'folder'");
155
- }
156
- const stripped = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").replace(/^\s+/, "");
157
- const body = stripped + inlineReferences(references);
158
- const fm = parseFrontmatterMinimal(content);
159
- if (format === "cursor-mdc") {
160
- const desc = fm.description ?? `Skill ${skillName}`;
161
- const cursorDesc = desc.startsWith("USE WHEN") ? desc : `USE WHEN ${desc}`;
162
- const safeDesc = cursorDesc.replace(/\n/g, " ").replace(/"/g, '\\"');
163
- return {
164
- filename: `${skillName}.mdc`,
165
- content: `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${body}`,
166
- };
182
+ ensureDir(dst);
183
+ for (const f of staged) {
184
+ const dstFile = safeJoinSkillFile(dst, f.rel);
185
+ ensureDir(dirname(dstFile));
186
+ withFsRetry(() => writeFileSync(dstFile, f.data));
167
187
  }
168
- return { filename: `${skillName}.md`, content: body };
188
+ return backup;
169
189
  }
170
- function parseFrontmatterMinimal(content) {
171
- const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
172
- if (!m)
173
- return {};
174
- const out = {};
175
- for (const line of m[1].split(/\r?\n/)) {
176
- const kv = line.match(/^description:\s*(.+)$/);
177
- if (kv) {
178
- out.description = kv[1].replace(/^["']|["']$/g, "").trim();
179
- return out;
190
+ function assertFolderFormat(spec) {
191
+ if (spec.skillFormat !== "folder") {
192
+ throw new Error(`formato de skill no soportado para ${spec.id}: ${spec.skillFormat}`);
193
+ }
194
+ }
195
+ function writeSkillToTarget(skillName, staged, spec, knownBundleHashes) {
196
+ validateSkillName(skillName);
197
+ assertFolderFormat(spec);
198
+ for (const base of skillDirsFor(spec)) {
199
+ replaceSkillDir(join(base, skillName), skillName, staged, {
200
+ knownBundleHashes,
201
+ backupRoot: skillBackupRoot(spec.id),
202
+ });
203
+ }
204
+ }
205
+ function brokenSkillDirs(name, entry, specs) {
206
+ const broken = [];
207
+ for (const spec of specs) {
208
+ if (spec.skillFormat !== "folder")
209
+ continue;
210
+ for (const base of skillDirsFor(spec)) {
211
+ const dir = join(base, name);
212
+ if (!verifyInstalledFiles(dir, entry).ok)
213
+ broken.push(dir);
180
214
  }
181
215
  }
182
- return out;
216
+ return broken;
183
217
  }
184
218
  function sleepSync(ms) {
185
219
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
@@ -885,14 +919,7 @@ export async function installSkill(name, manifest, opts = {}) {
885
919
  prev.targets.length === targetIds.length &&
886
920
  prev.targets.every((t) => targetIds.includes(t));
887
921
  if (prev && prev.bundleHash === entry.bundleHash && targetsMatch && !opts.force) {
888
- const folderTarget = targets.find((t) => t.skillFormat === "folder");
889
- if (folderTarget) {
890
- const dst = join(folderTarget.skillsDir, name);
891
- const drift = verifyInstalledFiles(dst, entry);
892
- if (drift.ok)
893
- return { name, kind: "skill", status: "unchanged", targets: targetIds };
894
- }
895
- else {
922
+ if (brokenSkillDirs(name, entry, targets).length === 0) {
896
923
  return { name, kind: "skill", status: "unchanged", targets: targetIds };
897
924
  }
898
925
  }
@@ -923,7 +950,7 @@ export async function installSkill(name, manifest, opts = {}) {
923
950
  const failures = [];
924
951
  for (const t of targets) {
925
952
  try {
926
- writeSkillToTarget(name, staged, t);
953
+ writeSkillToTarget(name, staged, t, [prev?.bundleHash, entry.bundleHash]);
927
954
  okTargets.push(t.id);
928
955
  }
929
956
  catch (err) {
@@ -1193,6 +1220,8 @@ export async function installAll(opts = {}) {
1193
1220
  }
1194
1221
  results.push(installMcp(mcp, { ...opts, targets }));
1195
1222
  }
1223
+ if (!opts.dryRun)
1224
+ recordManifestFloor(manifest);
1196
1225
  return { manifest, results, targets };
1197
1226
  }
1198
1227
  export function uninstallSkill(name, opts = {}) {
@@ -1216,18 +1245,10 @@ export function uninstallSkill(name, opts = {}) {
1216
1245
  if (!opts.dryRun) {
1217
1246
  for (const tId of targetIds) {
1218
1247
  const spec = TARGETS[tId];
1219
- if (spec.skillFormat === "folder") {
1220
- for (const base of skillDirsFor(spec)) {
1221
- const dst = join(base, name);
1222
- if (existsSync(dst))
1223
- rmSync(dst, { recursive: true, force: true });
1224
- }
1225
- }
1226
- else {
1227
- const ext = spec.skillFormat === "cursor-mdc" ? ".mdc" : ".md";
1228
- const dst = join(spec.skillsDir, `${name}${ext}`);
1248
+ for (const base of skillDirsFor(spec)) {
1249
+ const dst = join(base, name);
1229
1250
  if (existsSync(dst))
1230
- rmSync(dst, { force: true });
1251
+ rmSync(dst, { recursive: true, force: true });
1231
1252
  }
1232
1253
  }
1233
1254
  delete lock.skills[name];
@@ -1316,13 +1337,22 @@ export function detectDrift(manifest) {
1316
1337
  const toUpdate = [];
1317
1338
  const unchanged = [];
1318
1339
  const orphaned = [];
1340
+ const detected = new Set(detectInstalledTargets());
1319
1341
  for (const name of Object.keys(manifest.skills)) {
1320
1342
  const prev = lock.skills[name];
1321
1343
  if (!prev) {
1322
1344
  toInstall.push(name);
1323
1345
  continue;
1324
1346
  }
1325
- if (prev.bundleHash !== manifest.skills[name].bundleHash) {
1347
+ const entry = manifest.skills[name];
1348
+ if (prev.bundleHash !== entry.bundleHash) {
1349
+ toUpdate.push(name);
1350
+ continue;
1351
+ }
1352
+ const specs = (prev.targets ?? ["claude-code"])
1353
+ .filter((t) => detected.has(t) && TARGETS[t])
1354
+ .map((t) => TARGETS[t]);
1355
+ if (brokenSkillDirs(name, entry, specs).length > 0) {
1326
1356
  toUpdate.push(name);
1327
1357
  }
1328
1358
  else {
package/dist/lazy-mcp.js CHANGED
@@ -58,6 +58,8 @@ const spec = JSON.parse(readFileSync(argv[0], "utf8"));
58
58
  const [cmd, ...args] = argv.slice(2);
59
59
  const INIT_ID = "__alegra_lazy_init__";
60
60
  const START_TIMEOUT_MS = 10 * 60 * 1000;
61
+ const LEVELS = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"];
62
+ let logLevel = "info";
61
63
 
62
64
  const send = (m) => process.stdout.write(JSON.stringify(m) + "\\n");
63
65
  const key = (id) => JSON.stringify(id);
@@ -102,8 +104,23 @@ function forward(m) {
102
104
  child.stdin.write(JSON.stringify(m) + "\\n");
103
105
  }
104
106
 
105
- function start() {
107
+ function notifyStarting(trigger) {
108
+ const text =
109
+ "Preparando " + spec.name + ": la primera vez tarda 1-2 min mientras se descarga " +
110
+ "(las siguientes llamadas de la sesión son inmediatas).";
111
+ process.stderr.write("[" + spec.name + "] " + text + "\\n");
112
+ if (LEVELS.indexOf(logLevel) <= LEVELS.indexOf("info")) {
113
+ send({ jsonrpc: "2.0", method: "notifications/message", params: { level: "info", logger: spec.name, data: text } });
114
+ }
115
+ const token = trigger && trigger.params && trigger.params._meta && trigger.params._meta.progressToken;
116
+ if (token !== undefined && token !== null) {
117
+ send({ jsonrpc: "2.0", method: "notifications/progress", params: { progressToken: token, progress: 0, message: text } });
118
+ }
119
+ }
120
+
121
+ function start(trigger) {
106
122
  if (child || dead) return;
123
+ notifyStarting(trigger);
107
124
  child = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"] });
108
125
  startTimer = setTimeout(
109
126
  () => die("El MCP " + spec.name + " tardó más de 10 min en arrancar (¿se cortó la descarga?). Reintentá en una sesión nueva."),
@@ -152,7 +169,7 @@ createInterface({ input: process.stdin }).on("line", (line) => {
152
169
  id: m.id,
153
170
  result: {
154
171
  protocolVersion: spec.protocolVersion ?? initParams.protocolVersion ?? "2025-06-18",
155
- capabilities: { tools: {} },
172
+ capabilities: { tools: {}, logging: {} },
156
173
  serverInfo: { name: spec.name, version: "lazy" },
157
174
  },
158
175
  });
@@ -160,6 +177,10 @@ createInterface({ input: process.stdin }).on("line", (line) => {
160
177
  case "ping":
161
178
  send({ jsonrpc: "2.0", id: m.id, result: {} });
162
179
  return;
180
+ case "logging/setLevel":
181
+ if (LEVELS.includes(m.params?.level)) logLevel = m.params.level;
182
+ send({ jsonrpc: "2.0", id: m.id, result: {} });
183
+ return;
163
184
  case "tools/list":
164
185
  send({ jsonrpc: "2.0", id: m.id, result: { tools: spec.tools } });
165
186
  return;
@@ -185,7 +206,7 @@ createInterface({ input: process.stdin }).on("line", (line) => {
185
206
  return;
186
207
  }
187
208
  queue.push(m);
188
- start();
209
+ start(m);
189
210
  });
190
211
 
191
212
  function shutdown() {
package/dist/lib.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync, } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -7,7 +7,34 @@ export const PRODUCT_NAME = "alegra-data-agent";
7
7
  export const COMMAND_NAME = "alegra-data-agent";
8
8
  export const REPO_SLUG = "Alegra-Team/data-agent-selfservice";
9
9
  export const REPO_DEFAULT_BRANCH = "main";
10
- export const GATEWAY_URL = process.env.ALEGRA_DATA_AGENT_GATEWAY_URL ?? "https://alegra-data-agent.vercel.app";
10
+ export const DEFAULT_GATEWAY_URL = "https://alegra-data-agent.vercel.app";
11
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost"]);
12
+ export function resolveGatewayUrl(raw) {
13
+ if (raw === undefined || raw.trim() === "")
14
+ return { url: DEFAULT_GATEWAY_URL };
15
+ let parsed;
16
+ try {
17
+ parsed = new URL(raw);
18
+ }
19
+ catch {
20
+ return {
21
+ url: DEFAULT_GATEWAY_URL,
22
+ warning: `ALEGRA_DATA_AGENT_GATEWAY_URL no es una URL válida ('${raw}'); uso ${DEFAULT_GATEWAY_URL}.`,
23
+ };
24
+ }
25
+ if (parsed.protocol === "https:")
26
+ return { url: raw };
27
+ if (parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname))
28
+ return { url: raw };
29
+ return {
30
+ url: DEFAULT_GATEWAY_URL,
31
+ warning: `ALEGRA_DATA_AGENT_GATEWAY_URL debe ser https:// (http:// solo para 127.0.0.1/localhost); ignoro '${raw}' y uso ${DEFAULT_GATEWAY_URL}.`,
32
+ };
33
+ }
34
+ const _gateway = resolveGatewayUrl(process.env.ALEGRA_DATA_AGENT_GATEWAY_URL);
35
+ if (_gateway.warning)
36
+ process.stderr.write(` ⚠ ${_gateway.warning}\n`);
37
+ export const GATEWAY_URL = _gateway.url;
11
38
  export const ALL_TARGETS = [
12
39
  "claude-code",
13
40
  "cursor",
@@ -278,27 +305,226 @@ let _lockCache = null;
278
305
  function emptyLock() {
279
306
  return { schemaVersion: 1, productName: PRODUCT_NAME, skills: {} };
280
307
  }
308
+ function warn(message) {
309
+ process.stderr.write(` ⚠ ${message}\n`);
310
+ }
311
+ function isLockShape(v) {
312
+ if (!v || typeof v !== "object" || Array.isArray(v))
313
+ return false;
314
+ const skills = v.skills;
315
+ return !!skills && typeof skills === "object" && !Array.isArray(skills);
316
+ }
281
317
  export function readLockFile() {
282
318
  if (_lockCache)
283
319
  return _lockCache;
284
320
  const path = getLockFilePath();
285
321
  let lock = emptyLock();
286
322
  if (existsSync(path)) {
323
+ let parsed;
324
+ let ok = false;
287
325
  try {
288
- lock = JSON.parse(readFileSync(path, "utf-8"));
326
+ parsed = JSON.parse(readFileSync(path, "utf-8"));
327
+ ok = isLockShape(parsed);
289
328
  }
290
329
  catch {
291
- lock = emptyLock();
330
+ ok = false;
331
+ }
332
+ if (ok) {
333
+ lock = parsed;
334
+ }
335
+ else {
336
+ const backup = `${path}.corrupt-${new Date().toISOString().replace(/[:.]/g, "-")}`;
337
+ try {
338
+ renameSync(path, backup);
339
+ warn(`El lock del agente (${path}) estaba corrupto: lo aparté en ${backup} y sigo como si no hubiera nada instalado. Un \`install\` lo regenera.`);
340
+ }
341
+ catch {
342
+ warn(`El lock del agente (${path}) está corrupto y no pude apartarlo; lo leo como vacío.`);
343
+ }
292
344
  }
293
345
  }
294
346
  _lockCache = lock;
295
347
  return lock;
296
348
  }
297
- export function writeLockFile(lock) {
349
+ export const LOCKFILE_STALE_MS = 60_000;
350
+ export const LOCKFILE_MAX_WAIT_MS = 10_000;
351
+ function sleepSync(ms) {
352
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
353
+ }
354
+ function tryCreateLock(lockPath) {
355
+ let fd;
356
+ try {
357
+ fd = openSync(lockPath, "wx");
358
+ }
359
+ catch (err) {
360
+ if (err.code === "EEXIST")
361
+ return false;
362
+ throw err;
363
+ }
364
+ try {
365
+ writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
366
+ }
367
+ finally {
368
+ closeSync(fd);
369
+ }
370
+ return true;
371
+ }
372
+ function pidAlive(pid) {
373
+ try {
374
+ process.kill(pid, 0);
375
+ return true;
376
+ }
377
+ catch (err) {
378
+ return err.code === "EPERM";
379
+ }
380
+ }
381
+ function lockIsStale(lockPath) {
382
+ let at;
383
+ let pid;
384
+ try {
385
+ const raw = JSON.parse(readFileSync(lockPath, "utf-8"));
386
+ if (typeof raw.at === "number")
387
+ at = raw.at;
388
+ if (typeof raw.pid === "number")
389
+ pid = raw.pid;
390
+ }
391
+ catch {
392
+ }
393
+ if (at === undefined) {
394
+ try {
395
+ at = statSync(lockPath).mtimeMs;
396
+ }
397
+ catch {
398
+ return false;
399
+ }
400
+ }
401
+ const age = Date.now() - at;
402
+ if (age < 0 || age > LOCKFILE_STALE_MS)
403
+ return true;
404
+ if (pid !== undefined && pid !== process.pid && !pidAlive(pid))
405
+ return true;
406
+ return false;
407
+ }
408
+ function acquireWriteLock(lockPath, maxWaitMs) {
409
+ const deadline = Date.now() + maxWaitMs;
410
+ let backoff = 25;
411
+ for (;;) {
412
+ if (tryCreateLock(lockPath))
413
+ return true;
414
+ if (lockIsStale(lockPath)) {
415
+ const grave = `${lockPath}.stale-${process.pid}-${Date.now()}`;
416
+ try {
417
+ renameSync(lockPath, grave);
418
+ rmSync(grave, { force: true });
419
+ }
420
+ catch {
421
+ }
422
+ continue;
423
+ }
424
+ if (Date.now() >= deadline)
425
+ return false;
426
+ sleepSync(Math.min(backoff, Math.max(1, deadline - Date.now())));
427
+ backoff = Math.min(backoff * 2, 500);
428
+ }
429
+ }
430
+ export function writeLockFile(lock, opts = {}) {
298
431
  const path = getLockFilePath();
299
432
  _lockCache = lock;
300
433
  mkdirSync(dirname(path), { recursive: true });
301
- writeFileSync(path, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
434
+ const lockPath = `${path}.lock`;
435
+ if (!acquireWriteLock(lockPath, opts.maxWaitMs ?? LOCKFILE_MAX_WAIT_MS)) {
436
+ warn(`Otro proceso del agente está escribiendo ${path} (${lockPath}); no lo pisé. El próximo \`update\` lo reconcilia.`);
437
+ return false;
438
+ }
439
+ try {
440
+ const tmp = `${path}.tmp-${process.pid}`;
441
+ writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
442
+ renameSync(tmp, path);
443
+ }
444
+ finally {
445
+ rmSync(lockPath, { force: true });
446
+ }
447
+ return true;
448
+ }
449
+ export function parseTargetList(raw) {
450
+ const out = [];
451
+ for (const piece of raw
452
+ .split(",")
453
+ .map((s) => s.trim())
454
+ .filter(Boolean)) {
455
+ if (ALL_TARGETS.includes(piece)) {
456
+ out.push(piece);
457
+ }
458
+ else {
459
+ throw new Error(`Target desconocido: '${piece}'. Válidos: ${ALL_TARGETS.join(", ")}`);
460
+ }
461
+ }
462
+ return out;
463
+ }
464
+ export function parseArgs(argv) {
465
+ const flags = {
466
+ yes: false,
467
+ dryRun: false,
468
+ force: false,
469
+ help: false,
470
+ version: false,
471
+ noHook: false,
472
+ noBounce: false,
473
+ autoUpdate: false,
474
+ background: false,
475
+ };
476
+ const positional = [];
477
+ const only = [];
478
+ const targets = [];
479
+ let i = 0;
480
+ while (i < argv.length) {
481
+ const a = argv[i];
482
+ if (a === "-y" || a === "--yes")
483
+ flags.yes = true;
484
+ else if (a === "--dry-run")
485
+ flags.dryRun = true;
486
+ else if (a === "--force")
487
+ flags.force = true;
488
+ else if (a === "--no-hook")
489
+ flags.noHook = true;
490
+ else if (a === "--no-bounce")
491
+ flags.noBounce = true;
492
+ else if (a === "--auto-update")
493
+ flags.autoUpdate = true;
494
+ else if (a === "--background")
495
+ flags.background = true;
496
+ else if (a === "-h" || a === "--help")
497
+ flags.help = true;
498
+ else if (a === "-v" || a === "--version")
499
+ flags.version = true;
500
+ else if (a === "--only") {
501
+ i++;
502
+ if (argv[i])
503
+ only.push(...argv[i].split(","));
504
+ }
505
+ else if (a.startsWith("--only=")) {
506
+ only.push(...a.slice("--only=".length).split(","));
507
+ }
508
+ else if (a === "--target") {
509
+ i++;
510
+ if (argv[i])
511
+ targets.push(...parseTargetList(argv[i]));
512
+ }
513
+ else if (a.startsWith("--target=")) {
514
+ targets.push(...parseTargetList(a.slice("--target=".length)));
515
+ }
516
+ else {
517
+ positional.push(a);
518
+ }
519
+ i++;
520
+ }
521
+ return {
522
+ command: positional[0] ?? "install",
523
+ subcommand: positional[1],
524
+ flags,
525
+ only,
526
+ targets,
527
+ };
302
528
  }
303
529
  export function sha256(buf) {
304
530
  return createHash("sha256").update(buf).digest("hex");