arbella 0.1.1 → 0.1.3

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/index.js CHANGED
@@ -195,6 +195,15 @@ function toolHomeDir(tool) {
195
195
  return path2.join(homeDir(), ".cursor");
196
196
  }
197
197
  }
198
+ function appUserConfigRoot(targetOS, userHome, env = {}) {
199
+ if (targetOS === "darwin") {
200
+ return path2.join(userHome, "Library", "Application Support");
201
+ }
202
+ if (targetOS === "win32") {
203
+ return env.APPDATA ?? path2.join(userHome, "AppData", "Roaming");
204
+ }
205
+ return env.XDG_CONFIG_HOME ?? path2.join(userHome, ".config");
206
+ }
198
207
  function linuxPackageManagerCandidates() {
199
208
  return LINUX_PM_PROBES;
200
209
  }
@@ -899,6 +908,16 @@ var init_denylist = __esm({
899
908
  }
900
909
  });
901
910
 
911
+ // src/utils/symlink.ts
912
+ function normalizeCapturedSymlinkTarget(target) {
913
+ return target.replace(/\\/g, "/");
914
+ }
915
+ var init_symlink = __esm({
916
+ "src/utils/symlink.ts"() {
917
+ "use strict";
918
+ }
919
+ });
920
+
902
921
  // src/adapters/claude/plugins.ts
903
922
  function isRecord(v) {
904
923
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -1004,7 +1023,7 @@ async function walk(ctx, home4, abs, deny, files, symlinks, warnings) {
1004
1023
  return;
1005
1024
  }
1006
1025
  if (kind === "symlink") {
1007
- const target = await ctx.fs.readLink(abs);
1026
+ const target = normalizeCapturedSymlinkTarget(await ctx.fs.readLink(abs));
1008
1027
  symlinks.push({ repoPath: repoPathFor(rel), target });
1009
1028
  ctx.log.debug(`claude: symlink ${rel} -> ${target}`);
1010
1029
  return;
@@ -1068,7 +1087,7 @@ async function captureSkills(ctx, home4, skillsDir, deny, files, symlinks, skill
1068
1087
  }
1069
1088
  const kind = await ctx.fs.statKind(abs);
1070
1089
  if (kind === "symlink") {
1071
- const target = await ctx.fs.readLink(abs);
1090
+ const target = normalizeCapturedSymlinkTarget(await ctx.fs.readLink(abs));
1072
1091
  symlinks.push({ repoPath: repoPathFor(rel), target });
1073
1092
  skills.push({
1074
1093
  name,
@@ -1166,6 +1185,7 @@ var init_capture = __esm({
1166
1185
  "src/adapters/claude/capture.ts"() {
1167
1186
  "use strict";
1168
1187
  init_denylist();
1188
+ init_symlink();
1169
1189
  init_install();
1170
1190
  init_paths();
1171
1191
  init_plugins();
@@ -1849,7 +1869,7 @@ async function captureFile2(ctx, absPath, rel, out2) {
1849
1869
  async function captureSymlink(ctx, absPath, rel, out2) {
1850
1870
  let target;
1851
1871
  try {
1852
- target = await ctx.fs.readLink(absPath);
1872
+ target = normalizeCapturedSymlinkTarget(await ctx.fs.readLink(absPath));
1853
1873
  } catch {
1854
1874
  out2.warnings.push(`codex: could not read symlink ${rel}; skipped`);
1855
1875
  return;
@@ -2007,6 +2027,7 @@ var init_capture2 = __esm({
2007
2027
  "use strict";
2008
2028
  init_denylist();
2009
2029
  init_install();
2030
+ init_symlink();
2010
2031
  init_paths2();
2011
2032
  init_configToml();
2012
2033
  DENY = denylistFor("codex");
@@ -2259,38 +2280,54 @@ function paths3(overrideHome) {
2259
2280
  return {
2260
2281
  home: base,
2261
2282
  mcpJson: path10.join(base, "mcp.json"),
2262
- skillsDir: path10.join(base, "skills"),
2263
- rulesDir: path10.join(base, "rules")
2283
+ skillsDir: path10.join(base, "skills")
2264
2284
  };
2265
2285
  }
2266
- function sharedRulePath(overrideHome) {
2267
- return path10.join(paths3(overrideHome).rulesDir, SHARED_RULE_FILENAME);
2286
+ function cursorUserPaths(toolHome, os2, env = {}) {
2287
+ const userHome = path10.dirname(toolHome);
2288
+ const userDir = path10.join(appUserConfigRoot(os2, userHome, env), "Cursor", "User");
2289
+ return {
2290
+ userDir,
2291
+ settingsJson: path10.join(userDir, "settings.json"),
2292
+ keybindingsJson: path10.join(userDir, "keybindings.json"),
2293
+ snippetsDir: path10.join(userDir, "snippets")
2294
+ };
2268
2295
  }
2269
- var REPO_PREFIX3, FROZEN_PATHS3, SHARED_INSTRUCTIONS_REPO_PATH, SHARED_RULE_FILENAME;
2296
+ var REPO_PREFIX3, USER_REPO_PREFIX, FROZEN_PATHS3;
2270
2297
  var init_paths3 = __esm({
2271
2298
  "src/adapters/cursor/paths.ts"() {
2272
2299
  "use strict";
2273
2300
  init_os();
2274
2301
  REPO_PREFIX3 = "cursor/files";
2275
- FROZEN_PATHS3 = ["mcp.json"];
2276
- SHARED_INSTRUCTIONS_REPO_PATH = "shared/instructions.md";
2277
- SHARED_RULE_FILENAME = "arbella-shared-instructions.mdc";
2302
+ USER_REPO_PREFIX = "cursor/user";
2303
+ FROZEN_PATHS3 = ["mcp.json", "skills"];
2278
2304
  }
2279
2305
  });
2280
2306
 
2281
2307
  // src/adapters/cursor/index.ts
2282
2308
  import path11 from "path";
2309
+ import process3 from "process";
2310
+ import { execa as execa4 } from "execa";
2283
2311
  function repoPathFor3(rel) {
2284
2312
  return `${REPO_PREFIX3}/${rel}`;
2285
2313
  }
2314
+ function userRepoPathFor(rel) {
2315
+ return `${USER_REPO_PREFIX}/${rel}`;
2316
+ }
2286
2317
  function toRel2(home4, abs) {
2287
2318
  return path11.relative(home4, abs).split(path11.sep).join("/");
2288
2319
  }
2289
- function relFromRepoPath(repoPath) {
2320
+ function targetFromRepoPath(repoPath) {
2290
2321
  const norm = repoPath.replace(/\\/g, "/");
2291
- const prefix = `${REPO_PREFIX3}/`;
2292
- if (!norm.startsWith(prefix)) return void 0;
2293
- return norm.slice(prefix.length);
2322
+ const homePrefix = `${REPO_PREFIX3}/`;
2323
+ if (norm.startsWith(homePrefix)) return { root: "home", rel: norm.slice(homePrefix.length) };
2324
+ const userPrefix = `${USER_REPO_PREFIX}/`;
2325
+ if (norm.startsWith(userPrefix)) return { root: "user", rel: norm.slice(userPrefix.length) };
2326
+ return void 0;
2327
+ }
2328
+ function targetAbsFor2(ctx, target) {
2329
+ const root = target.root === "home" ? ctx.toolHome : cursorUserPaths(ctx.toolHome, ctx.os, ctx.env).userDir;
2330
+ return path11.join(root, ...target.rel.split("/").filter(Boolean));
2294
2331
  }
2295
2332
  function looksBinary2(buf) {
2296
2333
  const n = Math.min(buf.length, 8e3);
@@ -2333,72 +2370,250 @@ function collectMcpSecretRefs(ctx, parsed, source) {
2333
2370
  }
2334
2371
  return refs;
2335
2372
  }
2336
- async function capture4(ctx, _opts) {
2337
- const files = [];
2338
- const symlinks = [];
2339
- const secrets = [];
2340
- const warnings = [];
2341
- const manifest = emptyCursorManifest();
2342
- const p = paths3(ctx.toolHome);
2343
- const deny = denylistFor("cursor");
2344
- if (await ctx.fs.statKind(p.home) !== "dir") {
2345
- warnings.push("cursor: ~/.cursor not found; skipping (Cursor not installed?).");
2346
- return { tool: "cursor", files, symlinks, manifest, secrets, warnings };
2373
+ async function readMode(abs) {
2374
+ try {
2375
+ const { promises: fsp5 } = await import("fs");
2376
+ const st = await fsp5.stat(abs);
2377
+ return st.mode & 511;
2378
+ } catch {
2379
+ return void 0;
2347
2380
  }
2348
- for (const rel of FROZEN_PATHS3) {
2349
- const abs = path11.join(p.home, rel);
2350
- const relPosix = toRel2(p.home, abs);
2351
- if (matchesDeny(relPosix, deny)) {
2352
- ctx.log.debug(`cursor: skip (denylist) ${relPosix}`);
2353
- continue;
2381
+ }
2382
+ async function captureFile3(args) {
2383
+ const { ctx, abs, rel, repoPath, files, secrets, warnings, scanMcpSecrets = false } = args;
2384
+ const mode = await readMode(abs);
2385
+ let bytes;
2386
+ try {
2387
+ bytes = await ctx.fs.readBytes(abs);
2388
+ } catch (err) {
2389
+ warnings.push(`cursor: could not read ${rel}: ${err.message}`);
2390
+ return;
2391
+ }
2392
+ if (looksBinary2(bytes)) {
2393
+ const file2 = {
2394
+ repoPath,
2395
+ content: bytes.toString("base64"),
2396
+ binary: true
2397
+ };
2398
+ if (mode !== void 0) file2.mode = mode;
2399
+ files.push(file2);
2400
+ return;
2401
+ }
2402
+ const raw = bytes.toString("utf8");
2403
+ if (scanMcpSecrets) {
2404
+ try {
2405
+ secrets.push(...collectMcpSecretRefs(ctx, JSON.parse(raw), rel));
2406
+ } catch (err) {
2407
+ warnings.push(`cursor: could not parse ${rel} for secret scan: ${err.message}`);
2408
+ }
2409
+ }
2410
+ let content = raw;
2411
+ if (!ctx.includeSecrets) {
2412
+ const sanitized = ctx.sanitizer.sanitizeFile(raw, "cursor", rel);
2413
+ content = sanitized.content;
2414
+ secrets.push(...sanitized.found);
2415
+ }
2416
+ const templated = ctx.templater.toTemplate(content, ctx.vars);
2417
+ const file = { repoPath, content: templated };
2418
+ if (mode !== void 0) file.mode = mode;
2419
+ files.push(file);
2420
+ }
2421
+ async function walkFrozen(ctx, root, abs, deny, repoPathBuilder, files, symlinks, secrets, warnings) {
2422
+ const kind = await ctx.fs.statKind(abs);
2423
+ if (kind === "missing") return;
2424
+ const rel = toRel2(root, abs);
2425
+ if (rel && matchesDeny(rel, deny)) {
2426
+ ctx.log.debug(`cursor: skip (denylist) ${rel}`);
2427
+ return;
2428
+ }
2429
+ if (kind === "symlink") {
2430
+ const target = normalizeCapturedSymlinkTarget(await ctx.fs.readLink(abs));
2431
+ symlinks.push({ repoPath: repoPathBuilder(rel), target });
2432
+ ctx.log.debug(`cursor: symlink ${rel} -> ${target}`);
2433
+ return;
2434
+ }
2435
+ if (kind === "dir") {
2436
+ const entries = await ctx.fs.list(abs);
2437
+ entries.sort();
2438
+ for (const name of entries) {
2439
+ await walkFrozen(ctx, root, path11.join(abs, name), deny, repoPathBuilder, files, symlinks, secrets, warnings);
2354
2440
  }
2441
+ return;
2442
+ }
2443
+ await captureFile3({
2444
+ ctx,
2445
+ abs,
2446
+ rel,
2447
+ repoPath: repoPathBuilder(rel),
2448
+ files,
2449
+ secrets,
2450
+ warnings,
2451
+ scanMcpSecrets: rel === "mcp.json"
2452
+ });
2453
+ }
2454
+ async function captureSkills3(ctx, home4, skillsDir, deny, files, symlinks, skills, secrets, warnings) {
2455
+ if (await ctx.fs.statKind(skillsDir) !== "dir") return;
2456
+ const entries = await ctx.fs.list(skillsDir);
2457
+ entries.sort();
2458
+ for (const name of entries) {
2459
+ const abs = path11.join(skillsDir, name);
2460
+ const rel = toRel2(home4, abs);
2461
+ if (matchesDeny(rel, deny)) continue;
2355
2462
  const kind = await ctx.fs.statKind(abs);
2356
- if (kind === "missing") {
2357
- ctx.log.debug(`cursor: not present ${relPosix}`);
2463
+ if (kind === "symlink") {
2464
+ const target = normalizeCapturedSymlinkTarget(await ctx.fs.readLink(abs));
2465
+ symlinks.push({ repoPath: repoPathFor3(rel), target });
2466
+ skills.push({
2467
+ name,
2468
+ source: "skills.sh",
2469
+ installCommand: `npx skills add ${name}`,
2470
+ symlinked: true
2471
+ });
2358
2472
  continue;
2359
2473
  }
2360
- if (kind !== "file") {
2361
- ctx.log.debug(`cursor: skip non-file ${relPosix}`);
2474
+ if (kind === "dir") {
2475
+ skills.push({ name, source: "frozen", symlinked: false });
2476
+ await walkFrozen(ctx, home4, abs, deny, repoPathFor3, files, symlinks, secrets, warnings);
2362
2477
  continue;
2363
2478
  }
2364
- let bytes;
2479
+ if (kind === "file") {
2480
+ skills.push({ name, source: "frozen", symlinked: false });
2481
+ await walkFrozen(ctx, home4, abs, deny, repoPathFor3, files, symlinks, secrets, warnings);
2482
+ }
2483
+ }
2484
+ }
2485
+ function parseExtensionId(value) {
2486
+ if (typeof value !== "string") return void 0;
2487
+ const trimmed = value.trim();
2488
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(trimmed)) return void 0;
2489
+ return trimmed;
2490
+ }
2491
+ function parseCursorExtensionEntry(entry) {
2492
+ if (entry === null || typeof entry !== "object") return void 0;
2493
+ const obj = entry;
2494
+ const identifier = obj.identifier;
2495
+ const identifierObj = identifier !== null && typeof identifier === "object" ? identifier : {};
2496
+ const id = parseExtensionId(identifierObj.id) ?? parseExtensionId(obj.id);
2497
+ if (id === void 0) return void 0;
2498
+ const version = typeof obj.version === "string" ? obj.version : void 0;
2499
+ return {
2500
+ id,
2501
+ name: id,
2502
+ version,
2503
+ enabled: true,
2504
+ scope: "user"
2505
+ };
2506
+ }
2507
+ function parseCursorExtensionsJson(raw) {
2508
+ let parsed;
2509
+ try {
2510
+ parsed = JSON.parse(raw);
2511
+ } catch {
2512
+ return [];
2513
+ }
2514
+ if (!Array.isArray(parsed)) return [];
2515
+ const out2 = [];
2516
+ const seen = /* @__PURE__ */ new Set();
2517
+ for (const entry of parsed) {
2518
+ const plugin = parseCursorExtensionEntry(entry);
2519
+ if (plugin === void 0 || seen.has(plugin.id)) continue;
2520
+ seen.add(plugin.id);
2521
+ out2.push(plugin);
2522
+ }
2523
+ return out2;
2524
+ }
2525
+ function parseCursorExtensionLine(line) {
2526
+ const trimmed = line.trim();
2527
+ if (trimmed.length === 0) return void 0;
2528
+ const at = trimmed.lastIndexOf("@");
2529
+ const id = at > 0 ? trimmed.slice(0, at) : trimmed;
2530
+ const version = at > 0 ? trimmed.slice(at + 1) : void 0;
2531
+ const parsedId = parseExtensionId(id);
2532
+ if (parsedId === void 0) return void 0;
2533
+ return {
2534
+ id: parsedId,
2535
+ name: parsedId,
2536
+ version: version && version.length > 0 ? version : void 0,
2537
+ enabled: true,
2538
+ scope: "user"
2539
+ };
2540
+ }
2541
+ async function captureExtensions(ctx, cursorHome, warnings) {
2542
+ const extensionState = path11.join(cursorHome, "extensions", "extensions.json");
2543
+ if (await ctx.fs.statKind(extensionState) === "file") {
2365
2544
  try {
2366
- bytes = await ctx.fs.readBytes(abs);
2545
+ const fromState = parseCursorExtensionsJson(await ctx.fs.read(extensionState));
2546
+ if (fromState.length > 0) return fromState;
2367
2547
  } catch (err) {
2368
- warnings.push(`cursor: could not read ${relPosix}: ${err.message}`);
2369
- continue;
2370
- }
2371
- if (looksBinary2(bytes)) {
2372
- files.push({ repoPath: repoPathFor3(relPosix), content: bytes.toString("base64"), binary: true });
2373
- continue;
2548
+ warnings.push(`cursor: could not read extensions metadata: ${err.message}`);
2374
2549
  }
2375
- const raw = bytes.toString("utf8");
2376
- if (relPosix === "mcp.json") {
2377
- try {
2378
- secrets.push(...collectMcpSecretRefs(ctx, JSON.parse(raw), relPosix));
2379
- } catch (err) {
2380
- warnings.push(`cursor: could not parse mcp.json for secret scan: ${err.message}`);
2550
+ }
2551
+ if (!await which(cliBinaryName("cursor"))) return [];
2552
+ try {
2553
+ const result = await execa4(cliBinaryName("cursor"), ["--list-extensions", "--show-versions"], {
2554
+ stdin: "ignore",
2555
+ stderr: "ignore",
2556
+ stdout: "pipe",
2557
+ reject: false
2558
+ });
2559
+ const plugins = result.stdout.split(/\r?\n/).map(parseCursorExtensionLine).filter((plugin) => plugin !== void 0);
2560
+ const seen = /* @__PURE__ */ new Set();
2561
+ return plugins.filter((plugin) => {
2562
+ if (seen.has(plugin.id)) return false;
2563
+ seen.add(plugin.id);
2564
+ return true;
2565
+ });
2566
+ } catch (err) {
2567
+ warnings.push(`cursor: could not list extensions via CLI: ${err.message}`);
2568
+ return [];
2569
+ }
2570
+ }
2571
+ async function capture4(ctx, _opts) {
2572
+ const files = [];
2573
+ const symlinks = [];
2574
+ const secrets = [];
2575
+ const warnings = [];
2576
+ const manifest = emptyCursorManifest();
2577
+ const p = paths3(ctx.toolHome);
2578
+ const userPaths = cursorUserPaths(p.home, ctx.os, ctx.env);
2579
+ const deny = denylistFor("cursor");
2580
+ const hasToolHome = await ctx.fs.statKind(p.home) === "dir";
2581
+ const hasUserDir = await ctx.fs.statKind(userPaths.userDir) === "dir";
2582
+ if (!hasToolHome && !hasUserDir) {
2583
+ warnings.push("cursor: no ~/.cursor or Cursor User data found; skipping (Cursor not installed?).");
2584
+ return { tool: "cursor", files, symlinks, manifest, secrets, warnings };
2585
+ }
2586
+ if (hasToolHome) {
2587
+ for (const rel of FROZEN_PATHS3) {
2588
+ const abs = path11.join(p.home, rel);
2589
+ if (rel === "skills") {
2590
+ await captureSkills3(ctx, p.home, abs, deny, files, symlinks, manifest.skills, secrets, warnings);
2591
+ } else {
2592
+ await walkFrozen(ctx, p.home, abs, deny, repoPathFor3, files, symlinks, secrets, warnings);
2381
2593
  }
2382
2594
  }
2383
- const content = ctx.includeSecrets ? raw : ctx.sanitizer.sanitizeFile(raw, "cursor", relPosix).content;
2384
- const templated = ctx.templater.toTemplate(content, ctx.vars);
2385
- files.push({ repoPath: repoPathFor3(relPosix), content: templated });
2386
- ctx.log.debug(`cursor: froze ${relPosix}`);
2595
+ manifest.plugins = await captureExtensions(ctx, p.home, warnings);
2387
2596
  }
2388
- if (files.length === 0) {
2389
- warnings.push("cursor: present but nothing portable to capture (no mcp.json).");
2597
+ if (hasUserDir) {
2598
+ const userItems = [userPaths.settingsJson, userPaths.keybindingsJson, userPaths.snippetsDir];
2599
+ for (const abs of userItems) {
2600
+ await walkFrozen(ctx, userPaths.userDir, abs, deny, userRepoPathFor, files, symlinks, secrets, warnings);
2601
+ }
2602
+ }
2603
+ if (files.length === 0 && symlinks.length === 0 && manifest.plugins.length === 0) {
2604
+ warnings.push("cursor: present but nothing portable to capture.");
2390
2605
  }
2391
2606
  return { tool: "cursor", files, symlinks, manifest, secrets, warnings };
2392
2607
  }
2393
2608
  async function writeRestoredFile(ctx, file) {
2394
- const rel = relFromRepoPath(file.repoPath);
2395
- if (rel === void 0) {
2609
+ const target = targetFromRepoPath(file.repoPath);
2610
+ if (target === void 0) {
2396
2611
  ctx.log.debug(`cursor: ignoring foreign repoPath ${file.repoPath}`);
2397
2612
  return;
2398
2613
  }
2399
- const dest = path11.join(ctx.toolHome, ...rel.split("/"));
2614
+ const dest = targetAbsFor2(ctx, target);
2400
2615
  if (ctx.sourceOfTruth === "local" && await ctx.fs.exists(dest)) {
2401
- ctx.log.debug(`cursor: keep local ${rel} (sourceOfTruth=local)`);
2616
+ ctx.log.debug(`cursor: keep local ${target.rel} (sourceOfTruth=local)`);
2402
2617
  return;
2403
2618
  }
2404
2619
  if (file.binary) {
@@ -2408,33 +2623,93 @@ async function writeRestoredFile(ctx, file) {
2408
2623
  const expanded = ctx.templater.fromTemplate(file.content, ctx.vars);
2409
2624
  await ctx.fs.write(dest, expanded, file.mode);
2410
2625
  }
2411
- async function deploySharedRule(ctx) {
2412
- const sharedAbs = path11.join(ctx.repoRoot, ...SHARED_INSTRUCTIONS_REPO_PATH.split("/"));
2413
- if (!await ctx.fs.exists(sharedAbs)) {
2414
- ctx.log.debug("cursor: no shared/instructions.md to deploy as a Cursor rule.");
2626
+ async function writeRestoredSymlink(ctx, link) {
2627
+ const target = targetFromRepoPath(link.repoPath);
2628
+ if (target === void 0) {
2629
+ ctx.log.debug(`cursor: ignoring foreign symlink ${link.repoPath}`);
2415
2630
  return;
2416
2631
  }
2417
- const body = await ctx.fs.read(sharedAbs);
2418
- const rule = `---
2419
- description: Shared agent instructions (managed by arbella)
2420
- alwaysApply: true
2421
- ---
2422
-
2423
- ${body}`;
2424
- const dest = sharedRulePath(ctx.toolHome);
2425
- await ctx.fs.write(dest, rule);
2426
- ctx.log.debug(`cursor: wrote shared-instructions rule -> ${dest}`);
2632
+ const dest = targetAbsFor2(ctx, target);
2633
+ if (ctx.sourceOfTruth === "local" && await ctx.fs.statKind(dest) !== "missing") {
2634
+ ctx.log.debug(`cursor: keep local symlink ${target.rel} (sourceOfTruth=local)`);
2635
+ return;
2636
+ }
2637
+ await ctx.fs.symlink(link.target, dest);
2638
+ }
2639
+ async function reinstallExtensions(ctx, plugins) {
2640
+ const userPlugins = plugins.filter((plugin) => plugin.scope === "user");
2641
+ if (userPlugins.length === 0) return;
2642
+ const cursorBin = cliBinaryName("cursor");
2643
+ if (!await which(cursorBin)) {
2644
+ ctx.log.warn(
2645
+ "cursor CLI not found; extension IDs were restored in manifest.json, but extensions were not installed."
2646
+ );
2647
+ return;
2648
+ }
2649
+ for (const plugin of userPlugins) {
2650
+ try {
2651
+ const result = await execa4(cursorBin, ["--install-extension", plugin.id], {
2652
+ stdin: "ignore",
2653
+ stdout: "ignore",
2654
+ stderr: "pipe",
2655
+ reject: false
2656
+ });
2657
+ if (result.exitCode === 0) {
2658
+ ctx.log.step(`cursor: installed extension ${plugin.id}`);
2659
+ } else {
2660
+ const stderr = result.stderr.trim();
2661
+ ctx.log.warn(
2662
+ `cursor: failed to install extension ${plugin.id}${stderr ? ` (${stderr})` : ""}`
2663
+ );
2664
+ }
2665
+ } catch (err) {
2666
+ ctx.log.warn(`cursor: failed to install extension ${plugin.id}: ${err.message}`);
2667
+ }
2668
+ }
2669
+ }
2670
+ async function planActions3(ctx, data) {
2671
+ const actions = [];
2672
+ for (const file of data.files) {
2673
+ const target = targetFromRepoPath(file.repoPath);
2674
+ if (target === void 0) continue;
2675
+ const targetPath = targetAbsFor2(ctx, target);
2676
+ const overwrites = await ctx.fs.exists(targetPath);
2677
+ if (ctx.sourceOfTruth === "local" && overwrites) continue;
2678
+ actions.push({
2679
+ type: "write-file",
2680
+ tool: "cursor",
2681
+ targetPath,
2682
+ description: `Write ${target.root === "user" ? "User/" : ""}${target.rel}`,
2683
+ overwrites
2684
+ });
2685
+ }
2686
+ for (const link of data.symlinks) {
2687
+ const target = targetFromRepoPath(link.repoPath);
2688
+ if (target === void 0) continue;
2689
+ const targetPath = targetAbsFor2(ctx, target);
2690
+ const overwrites = await ctx.fs.statKind(targetPath) !== "missing";
2691
+ if (ctx.sourceOfTruth === "local" && overwrites) continue;
2692
+ actions.push({
2693
+ type: "write-symlink",
2694
+ tool: "cursor",
2695
+ targetPath,
2696
+ description: `Link ${target.root === "user" ? "User/" : ""}${target.rel} -> ${link.target}`,
2697
+ overwrites
2698
+ });
2699
+ }
2700
+ for (const plugin of data.manifest.plugins.filter((p) => p.scope === "user")) {
2701
+ actions.push({
2702
+ type: "install-plugin",
2703
+ tool: "cursor",
2704
+ description: `cursor --install-extension ${plugin.id}`
2705
+ });
2706
+ }
2707
+ return actions;
2427
2708
  }
2428
2709
  async function restore4(ctx, data) {
2429
2710
  if (ctx.dryRun) {
2430
- for (const file of data.files) {
2431
- const rel = relFromRepoPath(file.repoPath);
2432
- if (rel) ctx.log.step(`cursor: would write ${path11.join(ctx.toolHome, ...rel.split("/"))}`);
2433
- }
2434
- const sharedAbs = path11.join(ctx.repoRoot, ...SHARED_INSTRUCTIONS_REPO_PATH.split("/"));
2435
- if (await ctx.fs.exists(sharedAbs)) {
2436
- ctx.log.step(`cursor: would write shared-instructions rule -> ${sharedRulePath(ctx.toolHome)}`);
2437
- }
2711
+ const actions = await planActions3(ctx, data);
2712
+ for (const action of actions) ctx.log.step(action.description);
2438
2713
  return;
2439
2714
  }
2440
2715
  for (const file of data.files) {
@@ -2444,18 +2719,22 @@ async function restore4(ctx, data) {
2444
2719
  ctx.log.warn(`cursor: failed to write ${file.repoPath}: ${err.message}`);
2445
2720
  }
2446
2721
  }
2447
- try {
2448
- await deploySharedRule(ctx);
2449
- } catch (err) {
2450
- ctx.log.warn(`cursor: failed to deploy shared-instructions rule: ${err.message}`);
2722
+ for (const link of data.symlinks) {
2723
+ try {
2724
+ await writeRestoredSymlink(ctx, link);
2725
+ } catch (err) {
2726
+ ctx.log.warn(`cursor: failed to create symlink ${link.repoPath}: ${err.message}`);
2727
+ }
2451
2728
  }
2729
+ await reinstallExtensions(ctx, data.manifest.plugins);
2452
2730
  }
2453
2731
  async function detect2() {
2454
- return fsExistsDir(paths3().home);
2732
+ const p = paths3();
2733
+ return await fsExistsDir(p.home) || await fsExistsDir(cursorUserPaths(p.home, detectOS(), process3.env).userDir);
2455
2734
  }
2456
2735
  async function fsExistsDir(p) {
2457
- const { fs: fs3 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
2458
- return await fs3.statKind(p) === "dir";
2736
+ const { fs: fs4 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
2737
+ return await fs4.statKind(p) === "dir";
2459
2738
  }
2460
2739
  async function isCliInstalled3() {
2461
2740
  return which(cliBinaryName("cursor"));
@@ -2476,6 +2755,7 @@ var init_cursor = __esm({
2476
2755
  init_denylist();
2477
2756
  init_os();
2478
2757
  init_install();
2758
+ init_symlink();
2479
2759
  init_paths3();
2480
2760
  cursorAdapter = {
2481
2761
  id: "cursor",
@@ -2497,8 +2777,39 @@ var init_cursor = __esm({
2497
2777
  }
2498
2778
  });
2499
2779
 
2500
- // src/core/autobackup/hook.ts
2780
+ // src/core/version.ts
2781
+ import fs2 from "fs";
2501
2782
  import path12 from "path";
2783
+ import { fileURLToPath } from "url";
2784
+ function getPackageVersion() {
2785
+ if (cachedVersion !== void 0) return cachedVersion;
2786
+ const here = path12.dirname(fileURLToPath(import.meta.url));
2787
+ const candidates = [
2788
+ path12.join(here, "..", "package.json"),
2789
+ path12.join(here, "..", "..", "package.json")
2790
+ ];
2791
+ for (const packagePath of candidates) {
2792
+ try {
2793
+ const parsed = JSON.parse(fs2.readFileSync(packagePath, "utf8"));
2794
+ if (typeof parsed.version === "string" && parsed.version.trim() !== "") {
2795
+ cachedVersion = parsed.version;
2796
+ return cachedVersion;
2797
+ }
2798
+ } catch {
2799
+ }
2800
+ }
2801
+ cachedVersion = "0.0.0";
2802
+ return cachedVersion;
2803
+ }
2804
+ var cachedVersion;
2805
+ var init_version = __esm({
2806
+ "src/core/version.ts"() {
2807
+ "use strict";
2808
+ }
2809
+ });
2810
+
2811
+ // src/core/autobackup/hook.ts
2812
+ import path13 from "path";
2502
2813
  function hookCommand() {
2503
2814
  if (detectOS() === "win32") {
2504
2815
  return `start /b "" arbella push --auto >NUL 2>&1 :: ${HOOK_TAG}`;
@@ -2548,7 +2859,7 @@ async function writeJsonObject(file, value) {
2548
2859
  await fs.write(file, JSON.stringify(value, null, 2) + "\n");
2549
2860
  }
2550
2861
  function claudeSettingsPath() {
2551
- return path12.join(toolHomeDir("claude"), "settings.json");
2862
+ return path13.join(toolHomeDir("claude"), "settings.json");
2552
2863
  }
2553
2864
  async function applyClaude(enable) {
2554
2865
  const home4 = toolHomeDir("claude");
@@ -2573,7 +2884,7 @@ async function applyClaude(enable) {
2573
2884
  return true;
2574
2885
  }
2575
2886
  function codexHooksPath() {
2576
- return path12.join(toolHomeDir("codex"), "hooks.json");
2887
+ return path13.join(toolHomeDir("codex"), "hooks.json");
2577
2888
  }
2578
2889
  async function applyCodex(enable) {
2579
2890
  const home4 = toolHomeDir("codex");
@@ -2619,7 +2930,7 @@ var init_hook = __esm({
2619
2930
  });
2620
2931
 
2621
2932
  // src/core/autobackup/throttle.ts
2622
- import path13 from "path";
2933
+ import path14 from "path";
2623
2934
  async function readState() {
2624
2935
  if (!await fs.exists(STAMP_FILE)) {
2625
2936
  return { lastRunIso: null };
@@ -2662,7 +2973,7 @@ var init_throttle = __esm({
2662
2973
  "use strict";
2663
2974
  init_fs();
2664
2975
  init_os();
2665
- STAMP_FILE = path13.join(dataDir(), "autobackup.json");
2976
+ STAMP_FILE = path14.join(dataDir(), "autobackup.json");
2666
2977
  MIN_SESSION_GAP_MS = 5 * 60 * 1e3;
2667
2978
  DAILY_GAP_MS = 24 * 60 * 60 * 1e3;
2668
2979
  }
@@ -2750,9 +3061,9 @@ var init_schema = __esm({
2750
3061
  });
2751
3062
 
2752
3063
  // src/core/config/index.ts
2753
- import path14 from "path";
3064
+ import path15 from "path";
2754
3065
  function configPath() {
2755
- return path14.join(configDir(), "config.json");
3066
+ return path15.join(configDir(), "config.json");
2756
3067
  }
2757
3068
  async function configExists() {
2758
3069
  return fs.exists(configPath());
@@ -2801,7 +3112,7 @@ function defaultConfig() {
2801
3112
  async function saveConfig(config) {
2802
3113
  const valid = arbellaConfigSchema.parse(config);
2803
3114
  const file = configPath();
2804
- await fs.ensureDir(path14.dirname(file));
3115
+ await fs.ensureDir(path15.dirname(file));
2805
3116
  await fs.write(file, serializeConfig(valid));
2806
3117
  }
2807
3118
  function serializeConfig(value) {
@@ -2837,7 +3148,7 @@ var init_config = __esm({
2837
3148
  });
2838
3149
 
2839
3150
  // src/core/auth/cli.ts
2840
- import { execa as execa4 } from "execa";
3151
+ import { execa as execa5 } from "execa";
2841
3152
  function cliForProvider(provider) {
2842
3153
  return CLI_BY_PROVIDER[provider];
2843
3154
  }
@@ -2856,7 +3167,7 @@ async function providerCliAuthStatus(provider, host) {
2856
3167
  let exitCode = 1;
2857
3168
  let combined = "";
2858
3169
  try {
2859
- const res = await execa4(cli.bin, args, {
3170
+ const res = await execa5(cli.bin, args, {
2860
3171
  reject: false,
2861
3172
  // exit code is the signal; never throw on "not logged in".
2862
3173
  stdin: "ignore",
@@ -2899,7 +3210,7 @@ async function providerCliLogin(provider, opts = {}) {
2899
3210
  );
2900
3211
  log.step(`Running: ${cli.bin} ${args.join(" ")}`);
2901
3212
  try {
2902
- await execa4(cli.bin, args, {
3213
+ await execa5(cli.bin, args, {
2903
3214
  // INHERITED stdio: the user interacts with gh/glab directly. This is the
2904
3215
  // whole point — arbella steps out of the way for the actual login.
2905
3216
  stdio: "inherit",
@@ -2942,7 +3253,7 @@ async function providerCliLogout(provider, host) {
2942
3253
  }
2943
3254
  log.step(`Running: ${cli.bin} ${args.join(" ")}`);
2944
3255
  try {
2945
- await execa4(cli.bin, args, { stdio: "inherit", timeout: 6e4 });
3256
+ await execa5(cli.bin, args, { stdio: "inherit", timeout: 6e4 });
2946
3257
  log.success(`${cli.label}: signed out.`);
2947
3258
  return true;
2948
3259
  } catch (err) {
@@ -3194,7 +3505,7 @@ var init_device_flow = __esm({
3194
3505
  });
3195
3506
 
3196
3507
  // src/core/auth/providers.ts
3197
- import process3 from "process";
3508
+ import process4 from "process";
3198
3509
  function isPlaceholderClientId(clientId) {
3199
3510
  return clientId.trim() === "" || clientId === PLACEHOLDER_CLIENT_ID;
3200
3511
  }
@@ -3240,7 +3551,7 @@ function resolveClientId(spec, overrides) {
3240
3551
  if (typeof override === "string" && override.trim() !== "") {
3241
3552
  return override.trim();
3242
3553
  }
3243
- const fromEnv = process3.env[spec.clientIdEnvVar];
3554
+ const fromEnv = process4.env[spec.clientIdEnvVar];
3244
3555
  if (typeof fromEnv === "string" && fromEnv.trim() !== "") {
3245
3556
  return fromEnv.trim();
3246
3557
  }
@@ -3301,9 +3612,9 @@ var init_providers = __esm({
3301
3612
 
3302
3613
  // src/core/auth/store.ts
3303
3614
  import { promises as fsp3 } from "fs";
3304
- import path15 from "path";
3615
+ import path16 from "path";
3305
3616
  function credentialsPath() {
3306
- return path15.join(dataDir(), "credentials.json");
3617
+ return path16.join(dataDir(), "credentials.json");
3307
3618
  }
3308
3619
  async function loadFile() {
3309
3620
  const file = credentialsPath();
@@ -3358,7 +3669,7 @@ function normalizeCredential(host, value) {
3358
3669
  }
3359
3670
  async function saveFile(data) {
3360
3671
  const file = credentialsPath();
3361
- await fs.ensureDir(path15.dirname(file));
3672
+ await fs.ensureDir(path16.dirname(file));
3362
3673
  const json = `${JSON.stringify(data, null, 2)}
3363
3674
  `;
3364
3675
  await fs.write(file, json, CREDENTIALS_MODE);
@@ -3832,13 +4143,13 @@ var init_auth = __esm({
3832
4143
  });
3833
4144
 
3834
4145
  // src/core/repo/git.ts
3835
- import { execa as execa5 } from "execa";
4146
+ import { execa as execa6 } from "execa";
3836
4147
  async function git(cwd, args, opts = {}) {
3837
4148
  const reject = opts.reject ?? true;
3838
4149
  log.debug(`git ${args.map(redactArg).join(" ")} (cwd=${cwd})`);
3839
4150
  let res;
3840
4151
  try {
3841
- res = await execa5("git", args, {
4152
+ res = await execa6("git", args, {
3842
4153
  cwd,
3843
4154
  reject,
3844
4155
  // Keep output as strings; do not inherit stdio so we can capture/redact.
@@ -3921,7 +4232,7 @@ async function hasUpstream(cwd, branch) {
3921
4232
  async function clone(url, dest) {
3922
4233
  log.debug(`git clone <url> -> ${dest}`);
3923
4234
  try {
3924
- await execa5("git", ["clone", url, dest], {
4235
+ await execa6("git", ["clone", url, dest], {
3925
4236
  reject: true,
3926
4237
  stdout: "pipe",
3927
4238
  stderr: "pipe",
@@ -4024,10 +4335,10 @@ var init_generic = __esm({
4024
4335
  });
4025
4336
 
4026
4337
  // src/core/repo/providers/github.ts
4027
- import { execa as execa6 } from "execa";
4338
+ import { execa as execa7 } from "execa";
4028
4339
  async function ghPresent() {
4029
4340
  try {
4030
- await execa6("gh", ["--version"], { reject: true, stdin: "ignore" });
4341
+ await execa7("gh", ["--version"], { reject: true, stdin: "ignore" });
4031
4342
  return true;
4032
4343
  } catch {
4033
4344
  return false;
@@ -4036,7 +4347,7 @@ async function ghPresent() {
4036
4347
  async function gh(args) {
4037
4348
  log.debug(`gh ${args.join(" ")}`);
4038
4349
  try {
4039
- const res = await execa6("gh", args, {
4350
+ const res = await execa7("gh", args, {
4040
4351
  reject: true,
4041
4352
  stdout: "pipe",
4042
4353
  stderr: "pipe",
@@ -4115,10 +4426,10 @@ var init_github = __esm({
4115
4426
  });
4116
4427
 
4117
4428
  // src/core/repo/providers/gitlab.ts
4118
- import { execa as execa7 } from "execa";
4429
+ import { execa as execa8 } from "execa";
4119
4430
  async function glabPresent() {
4120
4431
  try {
4121
- await execa7("glab", ["--version"], { reject: true, stdin: "ignore" });
4432
+ await execa8("glab", ["--version"], { reject: true, stdin: "ignore" });
4122
4433
  return true;
4123
4434
  } catch {
4124
4435
  return false;
@@ -4127,7 +4438,7 @@ async function glabPresent() {
4127
4438
  async function glab(args) {
4128
4439
  log.debug(`glab ${args.join(" ")}`);
4129
4440
  try {
4130
- const res = await execa7("glab", args, {
4441
+ const res = await execa8("glab", args, {
4131
4442
  reject: true,
4132
4443
  stdout: "pipe",
4133
4444
  stderr: "pipe",
@@ -4206,7 +4517,7 @@ var init_gitlab = __esm({
4206
4517
  });
4207
4518
 
4208
4519
  // src/core/repo/index.ts
4209
- import path16 from "path";
4520
+ import path17 from "path";
4210
4521
  async function withRepoAuth(repoUrl, hooks, op) {
4211
4522
  try {
4212
4523
  await op(repoUrl, false);
@@ -4281,7 +4592,7 @@ async function ensureLocalClone(repo, auth) {
4281
4592
  "repo.url is not configured; cannot clone. Run `arbella init` first."
4282
4593
  );
4283
4594
  }
4284
- await fs.ensureDir(path16.dirname(localPath));
4595
+ await fs.ensureDir(path17.dirname(localPath));
4285
4596
  log.step(`Cloning backup repo into ${localPath}`);
4286
4597
  await withRepoAuth(repo.url, auth, async (url, credentialed) => {
4287
4598
  await clone(url, localPath);
@@ -4433,13 +4744,11 @@ function createSanitizer() {
4433
4744
  }
4434
4745
  function sanitizeFile(content, tool, source) {
4435
4746
  if (looksLikeJsonSource(source)) {
4436
- let parsed;
4437
- try {
4438
- parsed = JSON.parse(content);
4439
- } catch {
4747
+ const parsed = parseJsonOrJsonc(content);
4748
+ if (!parsed.ok) {
4440
4749
  return sanitizeText(content, tool, source);
4441
4750
  }
4442
- const { value, found } = sanitizeJson(parsed, tool, source);
4751
+ const { value, found } = sanitizeJson(parsed.value, tool, source);
4443
4752
  const indent = detectJsonIndent(content);
4444
4753
  const serialized = JSON.stringify(value, null, indent);
4445
4754
  return { content: serialized, found, changed: serialized !== content };
@@ -4452,6 +4761,92 @@ function looksLikeJsonSource(source) {
4452
4761
  const base = source.replace(/\\/g, "/").split("/").pop() ?? source;
4453
4762
  return /\.json$/i.test(base);
4454
4763
  }
4764
+ function parseJsonOrJsonc(content) {
4765
+ try {
4766
+ return { ok: true, value: JSON.parse(content) };
4767
+ } catch {
4768
+ }
4769
+ try {
4770
+ return { ok: true, value: JSON.parse(stripJsonc(content)) };
4771
+ } catch {
4772
+ return { ok: false };
4773
+ }
4774
+ }
4775
+ function stripJsonc(content) {
4776
+ return removeTrailingCommas(removeJsonComments(content));
4777
+ }
4778
+ function removeJsonComments(content) {
4779
+ let out2 = "";
4780
+ let inString = false;
4781
+ let escaped = false;
4782
+ for (let i = 0; i < content.length; i++) {
4783
+ const ch = content[i];
4784
+ const next = content[i + 1];
4785
+ if (inString) {
4786
+ out2 += ch;
4787
+ if (escaped) {
4788
+ escaped = false;
4789
+ } else if (ch === "\\") {
4790
+ escaped = true;
4791
+ } else if (ch === '"') {
4792
+ inString = false;
4793
+ }
4794
+ continue;
4795
+ }
4796
+ if (ch === '"') {
4797
+ inString = true;
4798
+ out2 += ch;
4799
+ continue;
4800
+ }
4801
+ if (ch === "/" && next === "/") {
4802
+ while (i < content.length && content[i] !== "\n") i++;
4803
+ if (i < content.length) out2 += "\n";
4804
+ continue;
4805
+ }
4806
+ if (ch === "/" && next === "*") {
4807
+ i += 2;
4808
+ while (i < content.length && !(content[i] === "*" && content[i + 1] === "/")) {
4809
+ if (content[i] === "\n") out2 += "\n";
4810
+ i++;
4811
+ }
4812
+ i++;
4813
+ continue;
4814
+ }
4815
+ out2 += ch;
4816
+ }
4817
+ return out2;
4818
+ }
4819
+ function removeTrailingCommas(content) {
4820
+ let out2 = "";
4821
+ let inString = false;
4822
+ let escaped = false;
4823
+ for (let i = 0; i < content.length; i++) {
4824
+ const ch = content[i];
4825
+ if (inString) {
4826
+ out2 += ch;
4827
+ if (escaped) {
4828
+ escaped = false;
4829
+ } else if (ch === "\\") {
4830
+ escaped = true;
4831
+ } else if (ch === '"') {
4832
+ inString = false;
4833
+ }
4834
+ continue;
4835
+ }
4836
+ if (ch === '"') {
4837
+ inString = true;
4838
+ out2 += ch;
4839
+ continue;
4840
+ }
4841
+ if (ch === ",") {
4842
+ let j = i + 1;
4843
+ while (j < content.length && /\s/.test(content[j])) j++;
4844
+ if (content[j] === "}" || content[j] === "]") continue;
4845
+ }
4846
+ out2 += ch;
4847
+ }
4848
+ return out2;
4849
+ }
4455
4850
  function detectJsonIndent(content) {
4456
4851
  const m = /\n([ \t]+)\S/.exec(content);
4457
4852
  if (!m) return 2;
@@ -4674,6 +5069,7 @@ var init_templater = __esm({
4674
5069
 
4675
5070
  // src/commands/_context.ts
4676
5071
  import { confirm, isCancel, password } from "@clack/prompts";
5072
+ import process5 from "process";
4677
5073
  function buildRepoAuthHooks(args) {
4678
5074
  return {
4679
5075
  interactive: args.interactive ?? true,
@@ -5134,7 +5530,7 @@ function shouldShareInstructions(claudeMd, agentsMd) {
5134
5530
  }
5135
5531
  function buildSharedInstructionsFile(content) {
5136
5532
  return {
5137
- repoPath: SHARED_INSTRUCTIONS_REPO_PATH2,
5533
+ repoPath: SHARED_INSTRUCTIONS_REPO_PATH,
5138
5534
  content
5139
5535
  };
5140
5536
  }
@@ -5144,12 +5540,12 @@ function sharedInstructionsTargets() {
5144
5540
  { tool: "codex", relPath: "AGENTS.md" }
5145
5541
  ];
5146
5542
  }
5147
- var SHARED_INSTRUCTIONS_REPO_PATH2;
5543
+ var SHARED_INSTRUCTIONS_REPO_PATH;
5148
5544
  var init_manifest = __esm({
5149
5545
  "src/core/manifest/index.ts"() {
5150
5546
  "use strict";
5151
5547
  init_schema2();
5152
- SHARED_INSTRUCTIONS_REPO_PATH2 = "shared/instructions.md";
5548
+ SHARED_INSTRUCTIONS_REPO_PATH = "shared/instructions.md";
5153
5549
  }
5154
5550
  });
5155
5551
 
@@ -5159,7 +5555,8 @@ __export(backup_exports, {
5159
5555
  register: () => register2,
5160
5556
  run: () => run2
5161
5557
  });
5162
- import path17 from "path";
5558
+ import path18 from "path";
5559
+ import process6 from "process";
5163
5560
  function register2(program) {
5164
5561
  const configure = (cmd) => cmd.description(
5165
5562
  "Push your AI dev setup to your private repo (snapshot local changes, then commit + push)."
@@ -5190,7 +5587,8 @@ function buildCoreServices(toolHome) {
5190
5587
  sanitizer,
5191
5588
  templater,
5192
5589
  vars: buildVariables(toolHome),
5193
- os: detectOS()
5590
+ os: detectOS(),
5591
+ env: process6.env
5194
5592
  };
5195
5593
  }
5196
5594
  function buildCaptureContext(tool, config, dryRun) {
@@ -5275,9 +5673,9 @@ async function run2(opts = {}) {
5275
5673
  if (sharedContent !== void 0) {
5276
5674
  const shared = buildSharedInstructionsFile(sharedContent);
5277
5675
  await writeCapturedFile2(repoRoot, shared);
5278
- log.step(`Wrote ${SHARED_INSTRUCTIONS_REPO_PATH2} (shared CLAUDE.md == AGENTS.md)`);
5676
+ log.step(`Wrote ${SHARED_INSTRUCTIONS_REPO_PATH} (shared CLAUDE.md == AGENTS.md)`);
5279
5677
  } else {
5280
- await fs.rmrf(repoJoin(repoRoot, SHARED_INSTRUCTIONS_REPO_PATH2));
5678
+ await fs.rmrf(repoJoin(repoRoot, SHARED_INSTRUCTIONS_REPO_PATH));
5281
5679
  }
5282
5680
  const meta = buildArbellaMeta({
5283
5681
  arbellaVersion: ARBELLA_VERSION,
@@ -5309,8 +5707,8 @@ async function decideSharedInstructions(present) {
5309
5707
  if (!present.includes("claude") || !present.includes("codex")) {
5310
5708
  return { share: false };
5311
5709
  }
5312
- const claudeMd = await readIfExists(path17.join(toolHomeDir("claude"), "CLAUDE.md"));
5313
- const agentsMd = await readIfExists(path17.join(toolHomeDir("codex"), "AGENTS.md"));
5710
+ const claudeMd = await readIfExists(path18.join(toolHomeDir("claude"), "CLAUDE.md"));
5711
+ const agentsMd = await readIfExists(path18.join(toolHomeDir("codex"), "AGENTS.md"));
5314
5712
  if (shouldShareInstructions(claudeMd, agentsMd)) {
5315
5713
  return { share: true, content: claudeMd };
5316
5714
  }
@@ -5320,8 +5718,9 @@ function toolFilesPrefix(tool) {
5320
5718
  return `${tool}/files`;
5321
5719
  }
5322
5720
  async function replaceToolFiles(repoRoot, result) {
5323
- const filesDir = repoJoin(repoRoot, toolFilesPrefix(result.tool));
5324
- await fs.rmrf(filesDir);
5721
+ for (const root of toolRepoDataRoots(result.tool)) {
5722
+ await fs.rmrf(repoJoin(repoRoot, root));
5723
+ }
5325
5724
  for (const file of result.files) {
5326
5725
  await writeCapturedFile2(repoRoot, file);
5327
5726
  }
@@ -5332,6 +5731,11 @@ async function replaceToolFiles(repoRoot, result) {
5332
5731
  const manifestPath = repoJoin(repoRoot, `${result.tool}/manifest.json`);
5333
5732
  await fs.write(manifestPath, serialize(result.manifest));
5334
5733
  }
5734
+ function toolRepoDataRoots(tool) {
5735
+ const roots = [toolFilesPrefix(tool)];
5736
+ if (tool === "cursor") roots.push("cursor/user");
5737
+ return roots;
5738
+ }
5335
5739
  async function writeCapturedFile2(repoRoot, file) {
5336
5740
  const dest = repoJoin(repoRoot, file.repoPath);
5337
5741
  if (file.binary === true) {
@@ -5343,7 +5747,7 @@ async function writeCapturedFile2(repoRoot, file) {
5343
5747
  }
5344
5748
  function repoJoin(repoRoot, repoPath) {
5345
5749
  const segments = repoPath.split("/").filter((s) => s.length > 0);
5346
- return path17.join(repoRoot, ...segments);
5750
+ return path18.join(repoRoot, ...segments);
5347
5751
  }
5348
5752
  function toolLabel(tool) {
5349
5753
  switch (tool) {
@@ -5357,7 +5761,8 @@ function toolLabel(tool) {
5357
5761
  }
5358
5762
  function renderRepoReadme(meta, generatedAtIso) {
5359
5763
  const toolList = meta.tools.map((t) => `- ${toolLabel(t)} (\`${t}\`)`).join("\n");
5360
- const sharedLine = meta.sharedInstructions ? "Your `CLAUDE.md` and `AGENTS.md` were identical and are stored once in [`shared/instructions.md`](shared/instructions.md); restore deploys it to both tools.\n" : "";
5764
+ const cursorUserLine = meta.tools.includes("cursor") ? "- `cursor/user/\u2026` \u2014 Cursor application User data such as settings,\n keybindings, and snippets.\n" : "";
5765
+ const sharedLine = meta.sharedInstructions ? "Your `CLAUDE.md` and `AGENTS.md` were identical and are stored once in [`shared/instructions.md`](shared/instructions.md); restore deploys it to Claude Code and Codex.\n" : "";
5361
5766
  return `# arbella backup
5362
5767
 
5363
5768
  This is a **private** backup of an AI coding setup, produced by
@@ -5377,6 +5782,7 @@ Each tool lives under \`<tool>/\`:
5377
5782
 
5378
5783
  - \`<tool>/files/\u2026\` \u2014 frozen config files (paths replaced with \`{{HOME}}\`-style
5379
5784
  placeholders, secret values redacted).
5785
+ ${cursorUserLine}
5380
5786
  - \`<tool>/manifest.json\` \u2014 what to reinstall (plugins, marketplaces, skills,
5381
5787
  npm globals) and which plugins to re-enable.
5382
5788
 
@@ -5391,7 +5797,7 @@ npm install -g arbella
5391
5797
  arbella pull <this-repo-url>
5392
5798
  \`\`\`
5393
5799
 
5394
- arbella will (R6/R14) take a timestamped safety copy of any existing tool homes,
5800
+ arbella will (R6/R14) take a timestamped safety copy of any existing restore targets,
5395
5801
  auto-install missing CLIs, write the frozen files back (re-expanding placeholders
5396
5802
  to this machine's paths), reinstall plugins/marketplaces/skills, and re-enable
5397
5803
  plugins.
@@ -5446,15 +5852,17 @@ function renderRepoGitignore(tools) {
5446
5852
  "*.key"
5447
5853
  ];
5448
5854
  for (const name of secretBasenames) lines.push(name);
5449
- lines.push("", "# Per-tool excluded directories (scoped under <tool>/files/)");
5855
+ lines.push("", "# Per-tool excluded directories (scoped under owned data roots)");
5450
5856
  const seen = /* @__PURE__ */ new Set();
5451
5857
  for (const tool of tools) {
5452
5858
  for (const pattern of denylistFor(tool)) {
5453
5859
  if (!pattern.endsWith("/")) continue;
5454
- const scoped = `${tool}/files/${pattern}`;
5455
- if (seen.has(scoped)) continue;
5456
- seen.add(scoped);
5457
- lines.push(scoped);
5860
+ for (const root of toolRepoDataRoots(tool)) {
5861
+ const scoped = `${root}/${pattern}`;
5862
+ if (seen.has(scoped)) continue;
5863
+ seen.add(scoped);
5864
+ lines.push(scoped);
5865
+ }
5458
5866
  }
5459
5867
  }
5460
5868
  return lines.join("\n") + "\n";
@@ -5475,7 +5883,7 @@ function reportDryRun(results, sharing, config) {
5475
5883
  );
5476
5884
  }
5477
5885
  if (sharing) {
5478
- log.step(`+ ${SHARED_INSTRUCTIONS_REPO_PATH2} (shared CLAUDE.md == AGENTS.md)`);
5886
+ log.step(`+ ${SHARED_INSTRUCTIONS_REPO_PATH} (shared CLAUDE.md == AGENTS.md)`);
5479
5887
  }
5480
5888
  log.step("+ arbella.json, README.md, .gitignore");
5481
5889
  reportSecrets(results.flatMap((r) => r.secrets), config.includeSecrets);
@@ -5575,15 +5983,16 @@ var init_backup = __esm({
5575
5983
  init_capture();
5576
5984
  init_capture2();
5577
5985
  init_cursor();
5578
- ARBELLA_VERSION = "0.1.0";
5986
+ init_version();
5987
+ ARBELLA_VERSION = getPackageVersion();
5579
5988
  }
5580
5989
  });
5581
5990
 
5582
5991
  // src/index.ts
5583
5992
  init_log();
5584
- import fs2 from "fs";
5585
- import path23 from "path";
5586
- import { fileURLToPath } from "url";
5993
+ import fs3 from "fs";
5994
+ import path24 from "path";
5995
+ import { fileURLToPath as fileURLToPath2 } from "url";
5587
5996
  import { Command } from "commander";
5588
5997
  import pc2 from "picocolors";
5589
5998
 
@@ -5604,8 +6013,11 @@ function listAdapters() {
5604
6013
  return ALL_ADAPTERS.map((a) => ({ id: a.id, displayName: a.displayName }));
5605
6014
  }
5606
6015
 
6016
+ // src/index.ts
6017
+ init_version();
6018
+
5607
6019
  // src/commands/init.ts
5608
- import path18 from "path";
6020
+ import path19 from "path";
5609
6021
  import {
5610
6022
  cancel as cancel2,
5611
6023
  confirm as confirm3,
@@ -6023,7 +6435,7 @@ async function ensureProviderSignedIn(provider, yes) {
6023
6435
  }
6024
6436
  }
6025
6437
  function defaultLocalPath() {
6026
- return path18.join(dataDir(), "repo");
6438
+ return path19.join(dataDir(), "repo");
6027
6439
  }
6028
6440
  function displayName(id) {
6029
6441
  switch (id) {
@@ -6239,7 +6651,8 @@ function resolveProviderSpec(raw) {
6239
6651
  init_backup();
6240
6652
 
6241
6653
  // src/commands/restore.ts
6242
- import path19 from "path";
6654
+ import path20 from "path";
6655
+ import process7 from "process";
6243
6656
  init_fs();
6244
6657
  init_log();
6245
6658
  init_os();
@@ -6256,12 +6669,13 @@ init_manifest();
6256
6669
  init_claude();
6257
6670
  init_codex();
6258
6671
  init_cursor();
6672
+ init_paths3();
6259
6673
  init_restore();
6260
6674
  init_restore2();
6261
6675
  var WIRING = [
6262
6676
  { id: "claude", adapter: claudeAdapter, planActions },
6263
6677
  { id: "codex", adapter: codexAdapter, planActions: planActions2 },
6264
- { id: "cursor", adapter: cursorAdapter }
6678
+ { id: "cursor", adapter: cursorAdapter, planActions: planActions3 }
6265
6679
  ];
6266
6680
  function wiringFor(id) {
6267
6681
  const w = WIRING.find((entry) => entry.id === id);
@@ -6275,12 +6689,13 @@ function buildCoreServices2(toolHome, os2) {
6275
6689
  sanitizer: createSanitizer(),
6276
6690
  templater: createTemplater(),
6277
6691
  vars: buildVariables(toolHome),
6278
- os: os2
6692
+ os: os2,
6693
+ env: process7.env
6279
6694
  };
6280
6695
  }
6281
6696
  function buildRestoreContext(args) {
6282
6697
  const toolHome = toolHomeDir(args.toolId);
6283
- const repoToolDir = path19.join(args.repoRoot, args.toolId);
6698
+ const repoToolDir = path20.join(args.repoRoot, args.toolId);
6284
6699
  return {
6285
6700
  ...buildCoreServices2(toolHome, args.os),
6286
6701
  toolHome,
@@ -6298,20 +6713,17 @@ function looksBinary3(buf) {
6298
6713
  return false;
6299
6714
  }
6300
6715
  async function readToolFrozen(repoToolDir, tool) {
6301
- const filesRoot = path19.join(repoToolDir, "files");
6302
6716
  const files = [];
6303
6717
  const symlinks = [];
6304
- if (await fs.statKind(filesRoot) !== "dir") {
6305
- return { files, symlinks };
6306
- }
6307
- async function walk2(absDir, relParts) {
6718
+ const roots = frozenRootsForTool(repoToolDir, tool);
6719
+ async function walk2(absDir, relParts, repoPrefix) {
6308
6720
  const entries = await fs.list(absDir);
6309
6721
  entries.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
6310
6722
  for (const name of entries) {
6311
- const abs = path19.join(absDir, name);
6723
+ const abs = path20.join(absDir, name);
6312
6724
  const nextRel = [...relParts, name];
6313
6725
  const relPosix = nextRel.join("/");
6314
- const repoPath = `${tool}/files/${relPosix}`;
6726
+ const repoPath = `${repoPrefix}/${relPosix}`;
6315
6727
  const kind = await fs.statKind(abs);
6316
6728
  if (kind === "symlink") {
6317
6729
  let target;
@@ -6327,7 +6739,7 @@ async function readToolFrozen(repoToolDir, tool) {
6327
6739
  continue;
6328
6740
  }
6329
6741
  if (kind === "dir") {
6330
- await walk2(abs, nextRel);
6742
+ await walk2(abs, nextRel, repoPrefix);
6331
6743
  continue;
6332
6744
  }
6333
6745
  if (kind !== "file") continue;
@@ -6355,11 +6767,23 @@ async function readToolFrozen(repoToolDir, tool) {
6355
6767
  }
6356
6768
  }
6357
6769
  }
6358
- await walk2(filesRoot, []);
6770
+ for (const root of roots) {
6771
+ if (await fs.statKind(root.absRoot) !== "dir") continue;
6772
+ await walk2(root.absRoot, [], root.repoPrefix);
6773
+ }
6359
6774
  return { files, symlinks };
6360
6775
  }
6776
+ function frozenRootsForTool(repoToolDir, tool) {
6777
+ const roots = [
6778
+ { absRoot: path20.join(repoToolDir, "files"), repoPrefix: `${tool}/files` }
6779
+ ];
6780
+ if (tool === "cursor") {
6781
+ roots.push({ absRoot: path20.join(repoToolDir, "user"), repoPrefix: "cursor/user" });
6782
+ }
6783
+ return roots;
6784
+ }
6361
6785
  async function readToolManifest(repoToolDir, tool) {
6362
- const manifestPath = path19.join(repoToolDir, "manifest.json");
6786
+ const manifestPath = path20.join(repoToolDir, "manifest.json");
6363
6787
  if (!await fs.exists(manifestPath)) {
6364
6788
  log.debug(`restore: no manifest.json for ${tool}; using empty manifest.`);
6365
6789
  return emptyManifest(tool);
@@ -6376,7 +6800,7 @@ async function readToolManifest(repoToolDir, tool) {
6376
6800
  return parseManifest(json);
6377
6801
  }
6378
6802
  async function loadRestoreData(repoRoot, tool) {
6379
- const repoToolDir = path19.join(repoRoot, tool);
6803
+ const repoToolDir = path20.join(repoRoot, tool);
6380
6804
  const manifest = await readToolManifest(repoToolDir, tool);
6381
6805
  const { files, symlinks } = await readToolFrozen(repoToolDir, tool);
6382
6806
  return { manifest, files, symlinks };
@@ -6389,49 +6813,65 @@ function parseToolsFlag(raw) {
6389
6813
  );
6390
6814
  return ids;
6391
6815
  }
6392
- function selectTools(meta, flagTools, configTools) {
6816
+ function selectToolsForRestore(meta, flagTools, _configTools) {
6393
6817
  const captured = new Set(meta.tools);
6394
6818
  let candidate;
6395
6819
  if (flagTools && flagTools.length > 0) {
6396
6820
  candidate = new Set(flagTools);
6397
- } else if (configTools.length > 0) {
6398
- candidate = new Set(configTools);
6399
6821
  } else {
6400
6822
  candidate = new Set(captured);
6401
6823
  }
6402
6824
  return TOOL_IDS.filter((id) => captured.has(id) && candidate.has(id));
6403
6825
  }
6404
- async function safetyBackup(tools, iso) {
6826
+ async function safetyBackup(tools, iso, os2) {
6405
6827
  const stamp = iso.replace(/[:.]/g, "-");
6406
- const backupsRoot = path19.join(dataDir(), "safety-backups");
6407
6828
  const made = [];
6408
6829
  for (const tool of tools) {
6409
- const source = toolHomeDir(tool);
6410
- if (!await fs.exists(source)) {
6411
- log.debug(`restore: no existing ${tool} home to back up (${source}).`);
6412
- continue;
6413
- }
6414
- const dest = path19.join(backupsRoot, `${tool}-${stamp}`);
6415
- try {
6416
- await fs.copy(source, dest);
6417
- made.push({ tool, source, dest });
6418
- log.step(`Safety backup: ${source} -> ${dest}`);
6419
- } catch (err) {
6420
- log.warn(
6421
- `restore: failed to safety-backup ${tool} (${source}): ${errMsg(err)}`
6422
- );
6830
+ for (const entry of safetySourcesForTool(tool, os2, stamp)) {
6831
+ if (!await fs.exists(entry.source)) {
6832
+ log.debug(`restore: no existing ${entry.label} to back up (${entry.source}).`);
6833
+ continue;
6834
+ }
6835
+ try {
6836
+ await fs.copy(entry.source, entry.dest);
6837
+ made.push({ tool, source: entry.source, dest: entry.dest });
6838
+ log.step(`Safety backup: ${entry.source} -> ${entry.dest}`);
6839
+ } catch (err) {
6840
+ log.warn(
6841
+ `restore: failed to safety-backup ${entry.label} (${entry.source}): ${errMsg(err)}`
6842
+ );
6843
+ }
6423
6844
  }
6424
6845
  }
6425
6846
  return made;
6426
6847
  }
6848
+ function safetySourcesForTool(tool, os2, stamp) {
6849
+ const backupsRoot = path20.join(dataDir(), "safety-backups");
6850
+ const toolHome = toolHomeDir(tool);
6851
+ const sources = [
6852
+ {
6853
+ label: `${tool} home`,
6854
+ source: toolHome,
6855
+ dest: path20.join(backupsRoot, `${tool}-${stamp}`)
6856
+ }
6857
+ ];
6858
+ if (tool === "cursor") {
6859
+ sources.push({
6860
+ label: "cursor User data",
6861
+ source: cursorUserPaths(toolHome, os2, process7.env).userDir,
6862
+ dest: path20.join(backupsRoot, `cursor-user-${stamp}`)
6863
+ });
6864
+ }
6865
+ return sources;
6866
+ }
6427
6867
  async function deploySharedInstructions(repoRoot, toolsInScope, dryRun) {
6428
- const sharedAbs = path19.join(
6868
+ const sharedAbs = path20.join(
6429
6869
  repoRoot,
6430
- ...SHARED_INSTRUCTIONS_REPO_PATH2.split("/")
6870
+ ...SHARED_INSTRUCTIONS_REPO_PATH.split("/")
6431
6871
  );
6432
6872
  if (!await fs.exists(sharedAbs)) {
6433
6873
  log.warn(
6434
- `restore: meta.sharedInstructions is set but ${SHARED_INSTRUCTIONS_REPO_PATH2} is missing from the repo; skipping shared-instructions deployment.`
6874
+ `restore: meta.sharedInstructions is set but ${SHARED_INSTRUCTIONS_REPO_PATH} is missing from the repo; skipping shared-instructions deployment.`
6435
6875
  );
6436
6876
  return;
6437
6877
  }
@@ -6439,7 +6879,7 @@ async function deploySharedInstructions(repoRoot, toolsInScope, dryRun) {
6439
6879
  const content = await fs.read(sharedAbs);
6440
6880
  for (const target of sharedInstructionsTargets()) {
6441
6881
  if (!inScope.has(target.tool)) continue;
6442
- const dest = path19.join(toolHomeDir(target.tool), target.relPath);
6882
+ const dest = path20.join(toolHomeDir(target.tool), target.relPath);
6443
6883
  if (dryRun) {
6444
6884
  log.step(`Would deploy shared instructions -> ${dest}`);
6445
6885
  continue;
@@ -6496,7 +6936,7 @@ async function fallbackActions(ctx, tool, data) {
6496
6936
  const out2 = [];
6497
6937
  for (const file of data.files) {
6498
6938
  const rel = stripFilesPrefix(tool, file.repoPath);
6499
- const dest = path19.join(ctx.toolHome, ...rel.split("/"));
6939
+ const dest = path20.join(ctx.toolHome, ...rel.split("/"));
6500
6940
  const overwrites = await ctx.fs.exists(dest);
6501
6941
  if (ctx.sourceOfTruth === "local" && overwrites) continue;
6502
6942
  out2.push({
@@ -6509,7 +6949,7 @@ async function fallbackActions(ctx, tool, data) {
6509
6949
  }
6510
6950
  for (const link of data.symlinks) {
6511
6951
  const rel = stripFilesPrefix(tool, link.repoPath);
6512
- const dest = path19.join(ctx.toolHome, ...rel.split("/"));
6952
+ const dest = path20.join(ctx.toolHome, ...rel.split("/"));
6513
6953
  const overwrites = await ctx.fs.statKind(dest) !== "missing";
6514
6954
  if (ctx.sourceOfTruth === "local" && overwrites) continue;
6515
6955
  out2.push({
@@ -6564,7 +7004,7 @@ function printPlan(plan, meta, l = log) {
6564
7004
  `Tools: ${plan.tools.length > 0 ? plan.tools.join(", ") : "(none)"}`
6565
7005
  );
6566
7006
  l.step(
6567
- `A timestamped safety backup of existing tool homes WILL be taken first (R14).`
7007
+ `A timestamped safety backup of existing restore targets WILL be taken first (R14).`
6568
7008
  );
6569
7009
  if (plan.missingClis.length > 0) {
6570
7010
  l.step(`CLIs to auto-install: ${plan.missingClis.join(", ")}`);
@@ -6631,7 +7071,7 @@ async function resolveRepo(repoUrl, optRepo) {
6631
7071
  if (explicit && explicit.trim() !== "") {
6632
7072
  const url = explicit.trim();
6633
7073
  const sameAsConfig = config.repo.url.trim() !== "" && config.repo.url.trim() === url;
6634
- const localPath2 = sameAsConfig && config.repo.localPath ? config.repo.localPath : path19.join(dataDir(), "restore", slugForUrl(url));
7074
+ const localPath2 = sameAsConfig && config.repo.localPath ? config.repo.localPath : path20.join(dataDir(), "restore", slugForUrl(url));
6635
7075
  return { provider: config.repo.provider, url, localPath: localPath2 };
6636
7076
  }
6637
7077
  if (!config.repo.url || config.repo.url.trim() === "") {
@@ -6639,7 +7079,7 @@ async function resolveRepo(repoUrl, optRepo) {
6639
7079
  "No repo to pull from. Pass a repo URL (`arbella pull <repo-url>`) or run `arbella init` first."
6640
7080
  );
6641
7081
  }
6642
- const localPath = config.repo.localPath && config.repo.localPath.trim() !== "" ? config.repo.localPath : path19.join(dataDir(), "restore", slugForUrl(config.repo.url));
7082
+ const localPath = config.repo.localPath && config.repo.localPath.trim() !== "" ? config.repo.localPath : path20.join(dataDir(), "restore", slugForUrl(config.repo.url));
6643
7083
  return { provider: config.repo.provider, url: config.repo.url, localPath };
6644
7084
  }
6645
7085
  function slugForUrl(url) {
@@ -6676,7 +7116,7 @@ async function run4(repoUrl, opts) {
6676
7116
  });
6677
7117
  await ensureRepoReady(repo, authHooks);
6678
7118
  const repoRoot = repo.localPath;
6679
- const metaPath = path19.join(repoRoot, "arbella.json");
7119
+ const metaPath = path20.join(repoRoot, "arbella.json");
6680
7120
  if (!await fs.exists(metaPath)) {
6681
7121
  throw new Error(
6682
7122
  `Not a arbella backup repo: ${metaPath} is missing. Did you point restore at the right repository?`
@@ -6692,7 +7132,7 @@ async function run4(repoUrl, opts) {
6692
7132
  }
6693
7133
  const config = await loadConfigOrDefault();
6694
7134
  const flagTools = parseToolsFlag(opts.tools);
6695
- const tools = selectTools(meta, flagTools, config.tools);
7135
+ const tools = selectToolsForRestore(meta, flagTools, config.tools);
6696
7136
  if (tools.length === 0) {
6697
7137
  log.warn(
6698
7138
  `No tools to restore (the repo + your selection have no overlap). Repo captured: ${meta.tools.join(", ") || "(none)"}.`
@@ -6716,7 +7156,7 @@ async function run4(repoUrl, opts) {
6716
7156
  }
6717
7157
  const iso = (/* @__PURE__ */ new Date()).toISOString();
6718
7158
  log.info("Creating safety backups of existing tool homes (R14)\u2026");
6719
- const backups = await safetyBackup(tools, iso);
7159
+ const backups = await safetyBackup(tools, iso, os2);
6720
7160
  if (backups.length === 0) {
6721
7161
  log.debug("restore: no existing tool homes needed backing up.");
6722
7162
  }
@@ -6756,7 +7196,7 @@ async function run4(repoUrl, opts) {
6756
7196
  printReauthReminder(tools);
6757
7197
  if (backups.length > 0) {
6758
7198
  log.info(
6759
- `Previous tool homes were safely backed up under ${path19.join(
7199
+ `Previous tool homes were safely backed up under ${path20.join(
6760
7200
  dataDir(),
6761
7201
  "safety-backups"
6762
7202
  )} (restore them manually if anything looks wrong).`
@@ -6816,7 +7256,8 @@ init_cursor();
6816
7256
  init_capture();
6817
7257
  init_capture2();
6818
7258
  init_cursor();
6819
- import path20 from "path";
7259
+ import path21 from "path";
7260
+ import process8 from "process";
6820
7261
  var ADAPTERS = {
6821
7262
  claude: { adapter: claudeAdapter, capture },
6822
7263
  codex: { adapter: codexAdapter, capture: capture2 },
@@ -6834,8 +7275,8 @@ async function run5(opts) {
6834
7275
  await ensureLocalClone(config.repo);
6835
7276
  const repoRoot = config.repo.localPath;
6836
7277
  const repoInitialized = await isRepoInitialized(repoRoot);
6837
- const claudeMd = await readIfExists2(path20.join(toolHomeDir("claude"), "CLAUDE.md"));
6838
- const agentsMd = await readIfExists2(path20.join(toolHomeDir("codex"), "AGENTS.md"));
7278
+ const claudeMd = await readIfExists2(path21.join(toolHomeDir("claude"), "CLAUDE.md"));
7279
+ const agentsMd = await readIfExists2(path21.join(toolHomeDir("codex"), "AGENTS.md"));
6839
7280
  const sharing = shouldShareInstructions(claudeMd, agentsMd);
6840
7281
  const toolStatuses = [];
6841
7282
  for (const tool of config.tools) {
@@ -6897,8 +7338,8 @@ async function run5(opts) {
6897
7338
  const change = await classifyFile(repoRoot, file);
6898
7339
  if (change.kind !== "unchanged") sharedChanges.push(change);
6899
7340
  } else {
6900
- if (await committedFileExists(repoRoot, SHARED_INSTRUCTIONS_REPO_PATH2)) {
6901
- sharedChanges.push({ repoPath: SHARED_INSTRUCTIONS_REPO_PATH2, kind: "removed" });
7341
+ if (await committedFileExists(repoRoot, SHARED_INSTRUCTIONS_REPO_PATH)) {
7342
+ sharedChanges.push({ repoPath: SHARED_INSTRUCTIONS_REPO_PATH, kind: "removed" });
6902
7343
  }
6903
7344
  }
6904
7345
  const clean = sharedChanges.length === 0 && toolStatuses.every(
@@ -6914,7 +7355,7 @@ async function run5(opts) {
6914
7355
  clean
6915
7356
  };
6916
7357
  if (opts.json) {
6917
- process.stdout.write(serialize(report));
7358
+ process8.stdout.write(serialize(report));
6918
7359
  return;
6919
7360
  }
6920
7361
  printHuman(report);
@@ -6928,6 +7369,7 @@ function buildCaptureContext2(tool, includeSecrets, includeMemories) {
6928
7369
  templater,
6929
7370
  vars: buildVariables(toolHome),
6930
7371
  os: detectOS(),
7372
+ env: process8.env,
6931
7373
  toolHome,
6932
7374
  includeSecrets,
6933
7375
  includeMemories,
@@ -7133,7 +7575,7 @@ async function walkRepoFiles(baseAbs, basePosix) {
7133
7575
  const entries = await fs.list(baseAbs);
7134
7576
  for (const name of entries) {
7135
7577
  if (name === ".git") continue;
7136
- const childAbs = path20.join(baseAbs, name);
7578
+ const childAbs = path21.join(baseAbs, name);
7137
7579
  const childPosix = `${basePosix}/${name}`;
7138
7580
  const kind = await fs.statKind(childAbs);
7139
7581
  if (kind === "symlink") {
@@ -7147,7 +7589,7 @@ async function walkRepoFiles(baseAbs, basePosix) {
7147
7589
  return out2;
7148
7590
  }
7149
7591
  function repoAbsPath(repoRoot, repoPath) {
7150
- return path20.join(repoRoot, ...repoPath.split("/"));
7592
+ return path21.join(repoRoot, ...repoPath.split("/"));
7151
7593
  }
7152
7594
  async function committedFileExists(repoRoot, repoPath) {
7153
7595
  const kind = await fs.statKind(repoAbsPath(repoRoot, repoPath));
@@ -7257,8 +7699,37 @@ function renderChangeLine(c) {
7257
7699
  return `${sigil} ${c.repoPath}${linkTag}`;
7258
7700
  }
7259
7701
 
7702
+ // src/commands/update.ts
7703
+ init_version();
7704
+ init_install();
7705
+ init_log();
7706
+ function normalizeVersion(version) {
7707
+ const trimmed = version?.trim();
7708
+ if (!trimmed) return "latest";
7709
+ if (trimmed.startsWith("arbella@")) return trimmed.slice("arbella@".length);
7710
+ return trimmed.replace(/^v(?=\d)/, "");
7711
+ }
7712
+ function packageSpec(version) {
7713
+ return `arbella@${normalizeVersion(version)}`;
7714
+ }
7715
+ function register7(program) {
7716
+ program.command("update").description("Update arbella itself through npm").option("--version <version>", "install a specific arbella version or npm tag instead of latest").option("--dry-run", "show the npm command without running it").action(async (opts) => {
7717
+ await run6(opts);
7718
+ });
7719
+ }
7720
+ async function run6(opts = {}) {
7721
+ const spec = packageSpec(opts.version);
7722
+ if (opts.dryRun) {
7723
+ log.info(`Would run: npm install -g ${spec}`);
7724
+ return;
7725
+ }
7726
+ log.info(`Updating arbella ${getPackageVersion()} -> ${spec}`);
7727
+ await npmInstallGlobal(spec);
7728
+ log.success(`arbella updated (${spec}).`);
7729
+ }
7730
+
7260
7731
  // src/commands/secrets.ts
7261
- import path22 from "path";
7732
+ import path23 from "path";
7262
7733
  import * as clack2 from "@clack/prompts";
7263
7734
 
7264
7735
  // src/core/secrets/index.ts
@@ -7271,7 +7742,7 @@ import {
7271
7742
  scryptSync
7272
7743
  } from "crypto";
7273
7744
  import { promises as fsp4 } from "fs";
7274
- import path21 from "path";
7745
+ import path22 from "path";
7275
7746
  var SCRYPT_PARAMS = {
7276
7747
  N: 1 << 15,
7277
7748
  // 32768
@@ -7366,7 +7837,7 @@ async function gatherSecretRefs(toolId) {
7366
7837
  const refs = [];
7367
7838
  const home4 = toolHomeDir(toolId);
7368
7839
  for (const spec of SECRET_FILES_BY_TOOL[toolId]) {
7369
- const abs = path21.join(home4, spec.relPath);
7840
+ const abs = path22.join(home4, spec.relPath);
7370
7841
  if (await fs.exists(abs)) {
7371
7842
  refs.push({
7372
7843
  tool: toolId,
@@ -7388,10 +7859,10 @@ async function collectSecretFiles(refs, createdAt) {
7388
7859
  const dedupeKey = `${ref.tool}:${relPath}`;
7389
7860
  if (seen.has(dedupeKey)) continue;
7390
7861
  seen.add(dedupeKey);
7391
- const abs = path21.join(toolHomeDir(ref.tool), relPath);
7862
+ const abs = path22.join(toolHomeDir(ref.tool), relPath);
7392
7863
  if (!await fs.exists(abs)) continue;
7393
7864
  const bytes = await fs.readBytes(abs);
7394
- const mode = await readMode(abs);
7865
+ const mode = await readMode2(abs);
7395
7866
  entries.push({
7396
7867
  tool: ref.tool,
7397
7868
  relPath,
@@ -7405,9 +7876,9 @@ async function applySecretBundle(bundle) {
7405
7876
  for (const entry of bundle.entries) {
7406
7877
  const home4 = toolHomeDir(entry.tool);
7407
7878
  const rel = entry.relPath.replace(/\\/g, "/");
7408
- const dest = path21.resolve(home4, rel);
7409
- const homeResolved = path21.resolve(home4);
7410
- const withinHome = dest === homeResolved || dest.startsWith(homeResolved + path21.sep);
7879
+ const dest = path22.resolve(home4, rel);
7880
+ const homeResolved = path22.resolve(home4);
7881
+ const withinHome = dest === homeResolved || dest.startsWith(homeResolved + path22.sep);
7411
7882
  if (!withinHome) {
7412
7883
  throw new Error(
7413
7884
  `Refusing to write secret outside ${entry.tool} home (suspicious path: ${rel}).`
@@ -7418,7 +7889,7 @@ async function applySecretBundle(bundle) {
7418
7889
  await fs.writeBytes(dest, bytes, mode);
7419
7890
  }
7420
7891
  }
7421
- async function readMode(abs) {
7892
+ async function readMode2(abs) {
7422
7893
  try {
7423
7894
  const st = await fsp4.lstat(abs);
7424
7895
  const m = st.mode & 4095;
@@ -7470,7 +7941,7 @@ function assertBundle(value) {
7470
7941
  init_fs();
7471
7942
  init_log();
7472
7943
  var DEFAULT_BLOB_FILE = "arbella-secrets.blob";
7473
- function register7(program) {
7944
+ function register8(program) {
7474
7945
  const secrets = program.command("secrets").description(
7475
7946
  "Move secret files (auth tokens / credentials) between machines via an encrypted, passphrase-protected blob. Never uses git."
7476
7947
  );
@@ -7523,7 +7994,7 @@ async function runExport(opts) {
7523
7994
  return;
7524
7995
  }
7525
7996
  const blob = encryptBundle(bundle, passphrase);
7526
- const outPath = path22.resolve(opts.out);
7997
+ const outPath = path23.resolve(opts.out);
7527
7998
  await fs.write(outPath, blob + "\n", 384);
7528
7999
  clack2.note(
7529
8000
  [
@@ -7532,7 +8003,7 @@ async function runExport(opts) {
7532
8003
  "",
7533
8004
  "This blob is safe to copy between machines (it is encrypted with your",
7534
8005
  "passphrase). It NEVER goes through git. On the target machine run:",
7535
- ` arbella secrets import --in ${path22.basename(outPath)}`,
8006
+ ` arbella secrets import --in ${path23.basename(outPath)}`,
7536
8007
  "",
7537
8008
  "Keep your passphrase safe: without it the blob cannot be decrypted."
7538
8009
  ].join("\n"),
@@ -7542,7 +8013,7 @@ async function runExport(opts) {
7542
8013
  }
7543
8014
  async function runImport(opts) {
7544
8015
  clack2.intro("arbella secrets import");
7545
- const inPath = path22.resolve(opts.in);
8016
+ const inPath = path23.resolve(opts.in);
7546
8017
  if (!await fs.exists(inPath)) {
7547
8018
  clack2.cancel(
7548
8019
  `No blob found at:
@@ -7644,7 +8115,7 @@ function errMessage7(err) {
7644
8115
  }
7645
8116
 
7646
8117
  // src/index.ts
7647
- var VERSION = "0.1.0";
8118
+ var VERSION = getPackageVersion();
7648
8119
  function buildProgram() {
7649
8120
  const program = new Command();
7650
8121
  const supported = listAdapters().map((a) => a.displayName).join(", ");
@@ -7664,6 +8135,7 @@ Supported tools: ${supported}.`
7664
8135
  register5(program);
7665
8136
  register6(program);
7666
8137
  register7(program);
8138
+ register8(program);
7667
8139
  return program;
7668
8140
  }
7669
8141
  async function main(argv = process.argv) {
@@ -7673,11 +8145,11 @@ async function main(argv = process.argv) {
7673
8145
  function isDirectRun() {
7674
8146
  const entry = process.argv[1];
7675
8147
  if (!entry) return false;
7676
- const modulePath = fileURLToPath(import.meta.url);
8148
+ const modulePath = fileURLToPath2(import.meta.url);
7677
8149
  try {
7678
- return fs2.realpathSync(entry) === fs2.realpathSync(modulePath);
8150
+ return fs3.realpathSync(entry) === fs3.realpathSync(modulePath);
7679
8151
  } catch {
7680
- return path23.resolve(entry) === modulePath;
8152
+ return path24.resolve(entry) === modulePath;
7681
8153
  }
7682
8154
  }
7683
8155
  function handleTopLevelError(err) {