arbella 0.1.0 → 0.1.2

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);
@@ -908,7 +927,7 @@ function splitPluginId(id) {
908
927
  if (at <= 0) return { name: id };
909
928
  return { name: id.slice(0, at), marketplace: id.slice(at + 1) };
910
929
  }
911
- function parseInstalledPlugins(json) {
930
+ function parseInstalledPlugins(json, foldPath = (p) => p) {
912
931
  if (!isRecord(json)) return [];
913
932
  const plugins = json.plugins;
914
933
  if (!isRecord(plugins)) return [];
@@ -921,7 +940,7 @@ function parseInstalledPlugins(json) {
921
940
  if (!isRecord(rec)) continue;
922
941
  const scope = rec.scope === "project" ? "project" : "user";
923
942
  const version = typeof rec.version === "string" ? rec.version : void 0;
924
- const projectPath = typeof rec.projectPath === "string" ? rec.projectPath : void 0;
943
+ const projectPath = typeof rec.projectPath === "string" ? foldPath(rec.projectPath) : void 0;
925
944
  const entry = {
926
945
  id,
927
946
  name,
@@ -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,
@@ -1119,7 +1138,10 @@ async function capture(ctx, opts) {
1119
1138
  const installedJson = await readJson(ctx, p.installedPlugins, warnings, "installed_plugins.json");
1120
1139
  const marketplacesJson = await readJson(ctx, p.knownMarketplaces, warnings, "known_marketplaces.json");
1121
1140
  const settingsJson = await readJson(ctx, p.settings, warnings, "settings.json");
1122
- const plugins = parseInstalledPlugins(installedJson);
1141
+ const plugins = parseInstalledPlugins(
1142
+ installedJson,
1143
+ (p2) => ctx.templater.toTemplate(p2, ctx.vars)
1144
+ );
1123
1145
  const marketplaces = parseKnownMarketplaces(marketplacesJson);
1124
1146
  const enabledPlugins = extractEnabledPlugins(settingsJson);
1125
1147
  for (const entry of plugins) {
@@ -1163,6 +1185,7 @@ var init_capture = __esm({
1163
1185
  "src/adapters/claude/capture.ts"() {
1164
1186
  "use strict";
1165
1187
  init_denylist();
1188
+ init_symlink();
1166
1189
  init_install();
1167
1190
  init_paths();
1168
1191
  init_plugins();
@@ -1846,7 +1869,7 @@ async function captureFile2(ctx, absPath, rel, out2) {
1846
1869
  async function captureSymlink(ctx, absPath, rel, out2) {
1847
1870
  let target;
1848
1871
  try {
1849
- target = await ctx.fs.readLink(absPath);
1872
+ target = normalizeCapturedSymlinkTarget(await ctx.fs.readLink(absPath));
1850
1873
  } catch {
1851
1874
  out2.warnings.push(`codex: could not read symlink ${rel}; skipped`);
1852
1875
  return;
@@ -2004,6 +2027,7 @@ var init_capture2 = __esm({
2004
2027
  "use strict";
2005
2028
  init_denylist();
2006
2029
  init_install();
2030
+ init_symlink();
2007
2031
  init_paths2();
2008
2032
  init_configToml();
2009
2033
  DENY = denylistFor("codex");
@@ -2060,8 +2084,11 @@ async function planActions2(ctx, data) {
2060
2084
  description: `Register marketplace ${m.id} (${m.source})`
2061
2085
  });
2062
2086
  }
2063
- for (const plugin of data.manifest.plugins) {
2064
- if (plugin.scope !== "user") continue;
2087
+ const { installable } = partitionPluginsForRestore(
2088
+ data.manifest.marketplaces,
2089
+ data.manifest.plugins.filter((p) => p.scope === "user")
2090
+ );
2091
+ for (const plugin of installable) {
2065
2092
  actions.push({
2066
2093
  type: "install-plugin",
2067
2094
  tool: "codex",
@@ -2117,6 +2144,19 @@ async function writeCapturedFile(ctx, file, overwriteAllowed) {
2117
2144
  const content = isConfigToml(rel) ? rehydrateConfigToml(file.content, ctx.templater, ctx.vars) : ctx.templater.fromTemplate(file.content, ctx.vars);
2118
2145
  await ctx.fs.write(targetPath, content, file.mode);
2119
2146
  }
2147
+ function partitionPluginsForRestore(marketplaces, userPlugins) {
2148
+ const known = new Set(marketplaces.map((m) => m.id));
2149
+ const installable = [];
2150
+ const deferred = [];
2151
+ for (const p of userPlugins) {
2152
+ if (p.marketplace !== void 0 && !known.has(p.marketplace)) {
2153
+ deferred.push(p);
2154
+ } else {
2155
+ installable.push(p);
2156
+ }
2157
+ }
2158
+ return { installable, deferred };
2159
+ }
2120
2160
  async function reinstallPluginsAndMarketplaces(ctx, marketplaces, plugins) {
2121
2161
  const userPlugins = plugins.filter((p) => p.scope === "user");
2122
2162
  if (marketplaces.length === 0 && userPlugins.length === 0) return;
@@ -2137,7 +2177,13 @@ async function reinstallPluginsAndMarketplaces(ctx, marketplaces, plugins) {
2137
2177
  ctx.log.warn(`codex: 'codex ${args.join(" ")}' failed (${msg}); config.toml retains it.`);
2138
2178
  }
2139
2179
  }
2140
- for (const plugin of userPlugins) {
2180
+ const { installable, deferred } = partitionPluginsForRestore(marketplaces, userPlugins);
2181
+ for (const plugin of deferred) {
2182
+ ctx.log.step(
2183
+ `codex: ${plugin.id} uses a built-in marketplace (${plugin.marketplace}); left to config.toml for Codex to re-sync.`
2184
+ );
2185
+ }
2186
+ for (const plugin of installable) {
2141
2187
  const args = pluginInstallArgs2(plugin);
2142
2188
  try {
2143
2189
  await execa3("codex", args, { stdout: "ignore", stderr: "ignore", stdin: "ignore" });
@@ -2169,7 +2215,7 @@ function marketplaceAddArgs2(m) {
2169
2215
  return ["plugin", "marketplace", "add", m.source];
2170
2216
  }
2171
2217
  function pluginInstallArgs2(p) {
2172
- return ["plugin", "install", p.id];
2218
+ return ["plugin", "add", p.id];
2173
2219
  }
2174
2220
  var PREFIX_WITH_SLASH;
2175
2221
  var init_restore2 = __esm({
@@ -2234,38 +2280,54 @@ function paths3(overrideHome) {
2234
2280
  return {
2235
2281
  home: base,
2236
2282
  mcpJson: path10.join(base, "mcp.json"),
2237
- skillsDir: path10.join(base, "skills"),
2238
- rulesDir: path10.join(base, "rules")
2283
+ skillsDir: path10.join(base, "skills")
2239
2284
  };
2240
2285
  }
2241
- function sharedRulePath(overrideHome) {
2242
- 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
+ };
2243
2295
  }
2244
- var REPO_PREFIX3, FROZEN_PATHS3, SHARED_INSTRUCTIONS_REPO_PATH, SHARED_RULE_FILENAME;
2296
+ var REPO_PREFIX3, USER_REPO_PREFIX, FROZEN_PATHS3;
2245
2297
  var init_paths3 = __esm({
2246
2298
  "src/adapters/cursor/paths.ts"() {
2247
2299
  "use strict";
2248
2300
  init_os();
2249
2301
  REPO_PREFIX3 = "cursor/files";
2250
- FROZEN_PATHS3 = ["mcp.json"];
2251
- SHARED_INSTRUCTIONS_REPO_PATH = "shared/instructions.md";
2252
- SHARED_RULE_FILENAME = "arbella-shared-instructions.mdc";
2302
+ USER_REPO_PREFIX = "cursor/user";
2303
+ FROZEN_PATHS3 = ["mcp.json", "skills"];
2253
2304
  }
2254
2305
  });
2255
2306
 
2256
2307
  // src/adapters/cursor/index.ts
2257
2308
  import path11 from "path";
2309
+ import process3 from "process";
2310
+ import { execa as execa4 } from "execa";
2258
2311
  function repoPathFor3(rel) {
2259
2312
  return `${REPO_PREFIX3}/${rel}`;
2260
2313
  }
2314
+ function userRepoPathFor(rel) {
2315
+ return `${USER_REPO_PREFIX}/${rel}`;
2316
+ }
2261
2317
  function toRel2(home4, abs) {
2262
2318
  return path11.relative(home4, abs).split(path11.sep).join("/");
2263
2319
  }
2264
- function relFromRepoPath(repoPath) {
2320
+ function targetFromRepoPath(repoPath) {
2265
2321
  const norm = repoPath.replace(/\\/g, "/");
2266
- const prefix = `${REPO_PREFIX3}/`;
2267
- if (!norm.startsWith(prefix)) return void 0;
2268
- 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));
2269
2331
  }
2270
2332
  function looksBinary2(buf) {
2271
2333
  const n = Math.min(buf.length, 8e3);
@@ -2308,72 +2370,250 @@ function collectMcpSecretRefs(ctx, parsed, source) {
2308
2370
  }
2309
2371
  return refs;
2310
2372
  }
2311
- async function capture4(ctx, _opts) {
2312
- const files = [];
2313
- const symlinks = [];
2314
- const secrets = [];
2315
- const warnings = [];
2316
- const manifest = emptyCursorManifest();
2317
- const p = paths3(ctx.toolHome);
2318
- const deny = denylistFor("cursor");
2319
- if (await ctx.fs.statKind(p.home) !== "dir") {
2320
- warnings.push("cursor: ~/.cursor not found; skipping (Cursor not installed?).");
2321
- 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;
2322
2380
  }
2323
- for (const rel of FROZEN_PATHS3) {
2324
- const abs = path11.join(p.home, rel);
2325
- const relPosix = toRel2(p.home, abs);
2326
- if (matchesDeny(relPosix, deny)) {
2327
- ctx.log.debug(`cursor: skip (denylist) ${relPosix}`);
2328
- 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);
2329
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;
2330
2462
  const kind = await ctx.fs.statKind(abs);
2331
- if (kind === "missing") {
2332
- 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
+ });
2333
2472
  continue;
2334
2473
  }
2335
- if (kind !== "file") {
2336
- 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);
2337
2477
  continue;
2338
2478
  }
2339
- 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") {
2340
2544
  try {
2341
- bytes = await ctx.fs.readBytes(abs);
2545
+ const fromState = parseCursorExtensionsJson(await ctx.fs.read(extensionState));
2546
+ if (fromState.length > 0) return fromState;
2342
2547
  } catch (err) {
2343
- warnings.push(`cursor: could not read ${relPosix}: ${err.message}`);
2344
- continue;
2345
- }
2346
- if (looksBinary2(bytes)) {
2347
- files.push({ repoPath: repoPathFor3(relPosix), content: bytes.toString("base64"), binary: true });
2348
- continue;
2548
+ warnings.push(`cursor: could not read extensions metadata: ${err.message}`);
2349
2549
  }
2350
- const raw = bytes.toString("utf8");
2351
- if (relPosix === "mcp.json") {
2352
- try {
2353
- secrets.push(...collectMcpSecretRefs(ctx, JSON.parse(raw), relPosix));
2354
- } catch (err) {
2355
- 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);
2356
2593
  }
2357
2594
  }
2358
- const content = ctx.includeSecrets ? raw : ctx.sanitizer.sanitizeFile(raw, "cursor", relPosix).content;
2359
- const templated = ctx.templater.toTemplate(content, ctx.vars);
2360
- files.push({ repoPath: repoPathFor3(relPosix), content: templated });
2361
- ctx.log.debug(`cursor: froze ${relPosix}`);
2595
+ manifest.plugins = await captureExtensions(ctx, p.home, warnings);
2362
2596
  }
2363
- if (files.length === 0) {
2364
- 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.");
2365
2605
  }
2366
2606
  return { tool: "cursor", files, symlinks, manifest, secrets, warnings };
2367
2607
  }
2368
2608
  async function writeRestoredFile(ctx, file) {
2369
- const rel = relFromRepoPath(file.repoPath);
2370
- if (rel === void 0) {
2609
+ const target = targetFromRepoPath(file.repoPath);
2610
+ if (target === void 0) {
2371
2611
  ctx.log.debug(`cursor: ignoring foreign repoPath ${file.repoPath}`);
2372
2612
  return;
2373
2613
  }
2374
- const dest = path11.join(ctx.toolHome, ...rel.split("/"));
2614
+ const dest = targetAbsFor2(ctx, target);
2375
2615
  if (ctx.sourceOfTruth === "local" && await ctx.fs.exists(dest)) {
2376
- ctx.log.debug(`cursor: keep local ${rel} (sourceOfTruth=local)`);
2616
+ ctx.log.debug(`cursor: keep local ${target.rel} (sourceOfTruth=local)`);
2377
2617
  return;
2378
2618
  }
2379
2619
  if (file.binary) {
@@ -2383,33 +2623,93 @@ async function writeRestoredFile(ctx, file) {
2383
2623
  const expanded = ctx.templater.fromTemplate(file.content, ctx.vars);
2384
2624
  await ctx.fs.write(dest, expanded, file.mode);
2385
2625
  }
2386
- async function deploySharedRule(ctx) {
2387
- const sharedAbs = path11.join(ctx.repoRoot, ...SHARED_INSTRUCTIONS_REPO_PATH.split("/"));
2388
- if (!await ctx.fs.exists(sharedAbs)) {
2389
- 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}`);
2390
2630
  return;
2391
2631
  }
2392
- const body = await ctx.fs.read(sharedAbs);
2393
- const rule = `---
2394
- description: Shared agent instructions (managed by arbella)
2395
- alwaysApply: true
2396
- ---
2397
-
2398
- ${body}`;
2399
- const dest = sharedRulePath(ctx.toolHome);
2400
- await ctx.fs.write(dest, rule);
2401
- 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;
2402
2708
  }
2403
2709
  async function restore4(ctx, data) {
2404
2710
  if (ctx.dryRun) {
2405
- for (const file of data.files) {
2406
- const rel = relFromRepoPath(file.repoPath);
2407
- if (rel) ctx.log.step(`cursor: would write ${path11.join(ctx.toolHome, ...rel.split("/"))}`);
2408
- }
2409
- const sharedAbs = path11.join(ctx.repoRoot, ...SHARED_INSTRUCTIONS_REPO_PATH.split("/"));
2410
- if (await ctx.fs.exists(sharedAbs)) {
2411
- ctx.log.step(`cursor: would write shared-instructions rule -> ${sharedRulePath(ctx.toolHome)}`);
2412
- }
2711
+ const actions = await planActions3(ctx, data);
2712
+ for (const action of actions) ctx.log.step(action.description);
2413
2713
  return;
2414
2714
  }
2415
2715
  for (const file of data.files) {
@@ -2419,18 +2719,22 @@ async function restore4(ctx, data) {
2419
2719
  ctx.log.warn(`cursor: failed to write ${file.repoPath}: ${err.message}`);
2420
2720
  }
2421
2721
  }
2422
- try {
2423
- await deploySharedRule(ctx);
2424
- } catch (err) {
2425
- 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
+ }
2426
2728
  }
2729
+ await reinstallExtensions(ctx, data.manifest.plugins);
2427
2730
  }
2428
2731
  async function detect2() {
2429
- return fsExistsDir(paths3().home);
2732
+ const p = paths3();
2733
+ return await fsExistsDir(p.home) || await fsExistsDir(cursorUserPaths(p.home, detectOS(), process3.env).userDir);
2430
2734
  }
2431
2735
  async function fsExistsDir(p) {
2432
- const { fs: fs3 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
2433
- 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";
2434
2738
  }
2435
2739
  async function isCliInstalled3() {
2436
2740
  return which(cliBinaryName("cursor"));
@@ -2451,6 +2755,7 @@ var init_cursor = __esm({
2451
2755
  init_denylist();
2452
2756
  init_os();
2453
2757
  init_install();
2758
+ init_symlink();
2454
2759
  init_paths3();
2455
2760
  cursorAdapter = {
2456
2761
  id: "cursor",
@@ -2472,8 +2777,39 @@ var init_cursor = __esm({
2472
2777
  }
2473
2778
  });
2474
2779
 
2475
- // src/core/autobackup/hook.ts
2780
+ // src/core/version.ts
2781
+ import fs2 from "fs";
2476
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";
2477
2813
  function hookCommand() {
2478
2814
  if (detectOS() === "win32") {
2479
2815
  return `start /b "" arbella push --auto >NUL 2>&1 :: ${HOOK_TAG}`;
@@ -2523,7 +2859,7 @@ async function writeJsonObject(file, value) {
2523
2859
  await fs.write(file, JSON.stringify(value, null, 2) + "\n");
2524
2860
  }
2525
2861
  function claudeSettingsPath() {
2526
- return path12.join(toolHomeDir("claude"), "settings.json");
2862
+ return path13.join(toolHomeDir("claude"), "settings.json");
2527
2863
  }
2528
2864
  async function applyClaude(enable) {
2529
2865
  const home4 = toolHomeDir("claude");
@@ -2548,7 +2884,7 @@ async function applyClaude(enable) {
2548
2884
  return true;
2549
2885
  }
2550
2886
  function codexHooksPath() {
2551
- return path12.join(toolHomeDir("codex"), "hooks.json");
2887
+ return path13.join(toolHomeDir("codex"), "hooks.json");
2552
2888
  }
2553
2889
  async function applyCodex(enable) {
2554
2890
  const home4 = toolHomeDir("codex");
@@ -2594,7 +2930,7 @@ var init_hook = __esm({
2594
2930
  });
2595
2931
 
2596
2932
  // src/core/autobackup/throttle.ts
2597
- import path13 from "path";
2933
+ import path14 from "path";
2598
2934
  async function readState() {
2599
2935
  if (!await fs.exists(STAMP_FILE)) {
2600
2936
  return { lastRunIso: null };
@@ -2637,7 +2973,7 @@ var init_throttle = __esm({
2637
2973
  "use strict";
2638
2974
  init_fs();
2639
2975
  init_os();
2640
- STAMP_FILE = path13.join(dataDir(), "autobackup.json");
2976
+ STAMP_FILE = path14.join(dataDir(), "autobackup.json");
2641
2977
  MIN_SESSION_GAP_MS = 5 * 60 * 1e3;
2642
2978
  DAILY_GAP_MS = 24 * 60 * 60 * 1e3;
2643
2979
  }
@@ -2725,9 +3061,9 @@ var init_schema = __esm({
2725
3061
  });
2726
3062
 
2727
3063
  // src/core/config/index.ts
2728
- import path14 from "path";
3064
+ import path15 from "path";
2729
3065
  function configPath() {
2730
- return path14.join(configDir(), "config.json");
3066
+ return path15.join(configDir(), "config.json");
2731
3067
  }
2732
3068
  async function configExists() {
2733
3069
  return fs.exists(configPath());
@@ -2776,7 +3112,7 @@ function defaultConfig() {
2776
3112
  async function saveConfig(config) {
2777
3113
  const valid = arbellaConfigSchema.parse(config);
2778
3114
  const file = configPath();
2779
- await fs.ensureDir(path14.dirname(file));
3115
+ await fs.ensureDir(path15.dirname(file));
2780
3116
  await fs.write(file, serializeConfig(valid));
2781
3117
  }
2782
3118
  function serializeConfig(value) {
@@ -2812,7 +3148,7 @@ var init_config = __esm({
2812
3148
  });
2813
3149
 
2814
3150
  // src/core/auth/cli.ts
2815
- import { execa as execa4 } from "execa";
3151
+ import { execa as execa5 } from "execa";
2816
3152
  function cliForProvider(provider) {
2817
3153
  return CLI_BY_PROVIDER[provider];
2818
3154
  }
@@ -2831,7 +3167,7 @@ async function providerCliAuthStatus(provider, host) {
2831
3167
  let exitCode = 1;
2832
3168
  let combined = "";
2833
3169
  try {
2834
- const res = await execa4(cli.bin, args, {
3170
+ const res = await execa5(cli.bin, args, {
2835
3171
  reject: false,
2836
3172
  // exit code is the signal; never throw on "not logged in".
2837
3173
  stdin: "ignore",
@@ -2874,7 +3210,7 @@ async function providerCliLogin(provider, opts = {}) {
2874
3210
  );
2875
3211
  log.step(`Running: ${cli.bin} ${args.join(" ")}`);
2876
3212
  try {
2877
- await execa4(cli.bin, args, {
3213
+ await execa5(cli.bin, args, {
2878
3214
  // INHERITED stdio: the user interacts with gh/glab directly. This is the
2879
3215
  // whole point — arbella steps out of the way for the actual login.
2880
3216
  stdio: "inherit",
@@ -2917,7 +3253,7 @@ async function providerCliLogout(provider, host) {
2917
3253
  }
2918
3254
  log.step(`Running: ${cli.bin} ${args.join(" ")}`);
2919
3255
  try {
2920
- await execa4(cli.bin, args, { stdio: "inherit", timeout: 6e4 });
3256
+ await execa5(cli.bin, args, { stdio: "inherit", timeout: 6e4 });
2921
3257
  log.success(`${cli.label}: signed out.`);
2922
3258
  return true;
2923
3259
  } catch (err) {
@@ -3169,7 +3505,7 @@ var init_device_flow = __esm({
3169
3505
  });
3170
3506
 
3171
3507
  // src/core/auth/providers.ts
3172
- import process3 from "process";
3508
+ import process4 from "process";
3173
3509
  function isPlaceholderClientId(clientId) {
3174
3510
  return clientId.trim() === "" || clientId === PLACEHOLDER_CLIENT_ID;
3175
3511
  }
@@ -3215,7 +3551,7 @@ function resolveClientId(spec, overrides) {
3215
3551
  if (typeof override === "string" && override.trim() !== "") {
3216
3552
  return override.trim();
3217
3553
  }
3218
- const fromEnv = process3.env[spec.clientIdEnvVar];
3554
+ const fromEnv = process4.env[spec.clientIdEnvVar];
3219
3555
  if (typeof fromEnv === "string" && fromEnv.trim() !== "") {
3220
3556
  return fromEnv.trim();
3221
3557
  }
@@ -3276,9 +3612,9 @@ var init_providers = __esm({
3276
3612
 
3277
3613
  // src/core/auth/store.ts
3278
3614
  import { promises as fsp3 } from "fs";
3279
- import path15 from "path";
3615
+ import path16 from "path";
3280
3616
  function credentialsPath() {
3281
- return path15.join(dataDir(), "credentials.json");
3617
+ return path16.join(dataDir(), "credentials.json");
3282
3618
  }
3283
3619
  async function loadFile() {
3284
3620
  const file = credentialsPath();
@@ -3333,7 +3669,7 @@ function normalizeCredential(host, value) {
3333
3669
  }
3334
3670
  async function saveFile(data) {
3335
3671
  const file = credentialsPath();
3336
- await fs.ensureDir(path15.dirname(file));
3672
+ await fs.ensureDir(path16.dirname(file));
3337
3673
  const json = `${JSON.stringify(data, null, 2)}
3338
3674
  `;
3339
3675
  await fs.write(file, json, CREDENTIALS_MODE);
@@ -3807,13 +4143,13 @@ var init_auth = __esm({
3807
4143
  });
3808
4144
 
3809
4145
  // src/core/repo/git.ts
3810
- import { execa as execa5 } from "execa";
4146
+ import { execa as execa6 } from "execa";
3811
4147
  async function git(cwd, args, opts = {}) {
3812
4148
  const reject = opts.reject ?? true;
3813
4149
  log.debug(`git ${args.map(redactArg).join(" ")} (cwd=${cwd})`);
3814
4150
  let res;
3815
4151
  try {
3816
- res = await execa5("git", args, {
4152
+ res = await execa6("git", args, {
3817
4153
  cwd,
3818
4154
  reject,
3819
4155
  // Keep output as strings; do not inherit stdio so we can capture/redact.
@@ -3896,7 +4232,7 @@ async function hasUpstream(cwd, branch) {
3896
4232
  async function clone(url, dest) {
3897
4233
  log.debug(`git clone <url> -> ${dest}`);
3898
4234
  try {
3899
- await execa5("git", ["clone", url, dest], {
4235
+ await execa6("git", ["clone", url, dest], {
3900
4236
  reject: true,
3901
4237
  stdout: "pipe",
3902
4238
  stderr: "pipe",
@@ -3999,10 +4335,10 @@ var init_generic = __esm({
3999
4335
  });
4000
4336
 
4001
4337
  // src/core/repo/providers/github.ts
4002
- import { execa as execa6 } from "execa";
4338
+ import { execa as execa7 } from "execa";
4003
4339
  async function ghPresent() {
4004
4340
  try {
4005
- await execa6("gh", ["--version"], { reject: true, stdin: "ignore" });
4341
+ await execa7("gh", ["--version"], { reject: true, stdin: "ignore" });
4006
4342
  return true;
4007
4343
  } catch {
4008
4344
  return false;
@@ -4011,7 +4347,7 @@ async function ghPresent() {
4011
4347
  async function gh(args) {
4012
4348
  log.debug(`gh ${args.join(" ")}`);
4013
4349
  try {
4014
- const res = await execa6("gh", args, {
4350
+ const res = await execa7("gh", args, {
4015
4351
  reject: true,
4016
4352
  stdout: "pipe",
4017
4353
  stderr: "pipe",
@@ -4090,10 +4426,10 @@ var init_github = __esm({
4090
4426
  });
4091
4427
 
4092
4428
  // src/core/repo/providers/gitlab.ts
4093
- import { execa as execa7 } from "execa";
4429
+ import { execa as execa8 } from "execa";
4094
4430
  async function glabPresent() {
4095
4431
  try {
4096
- await execa7("glab", ["--version"], { reject: true, stdin: "ignore" });
4432
+ await execa8("glab", ["--version"], { reject: true, stdin: "ignore" });
4097
4433
  return true;
4098
4434
  } catch {
4099
4435
  return false;
@@ -4102,7 +4438,7 @@ async function glabPresent() {
4102
4438
  async function glab(args) {
4103
4439
  log.debug(`glab ${args.join(" ")}`);
4104
4440
  try {
4105
- const res = await execa7("glab", args, {
4441
+ const res = await execa8("glab", args, {
4106
4442
  reject: true,
4107
4443
  stdout: "pipe",
4108
4444
  stderr: "pipe",
@@ -4181,7 +4517,7 @@ var init_gitlab = __esm({
4181
4517
  });
4182
4518
 
4183
4519
  // src/core/repo/index.ts
4184
- import path16 from "path";
4520
+ import path17 from "path";
4185
4521
  async function withRepoAuth(repoUrl, hooks, op) {
4186
4522
  try {
4187
4523
  await op(repoUrl, false);
@@ -4256,7 +4592,7 @@ async function ensureLocalClone(repo, auth) {
4256
4592
  "repo.url is not configured; cannot clone. Run `arbella init` first."
4257
4593
  );
4258
4594
  }
4259
- await fs.ensureDir(path16.dirname(localPath));
4595
+ await fs.ensureDir(path17.dirname(localPath));
4260
4596
  log.step(`Cloning backup repo into ${localPath}`);
4261
4597
  await withRepoAuth(repo.url, auth, async (url, credentialed) => {
4262
4598
  await clone(url, localPath);
@@ -4408,13 +4744,11 @@ function createSanitizer() {
4408
4744
  }
4409
4745
  function sanitizeFile(content, tool, source) {
4410
4746
  if (looksLikeJsonSource(source)) {
4411
- let parsed;
4412
- try {
4413
- parsed = JSON.parse(content);
4414
- } catch {
4747
+ const parsed = parseJsonOrJsonc(content);
4748
+ if (!parsed.ok) {
4415
4749
  return sanitizeText(content, tool, source);
4416
4750
  }
4417
- const { value, found } = sanitizeJson(parsed, tool, source);
4751
+ const { value, found } = sanitizeJson(parsed.value, tool, source);
4418
4752
  const indent = detectJsonIndent(content);
4419
4753
  const serialized = JSON.stringify(value, null, indent);
4420
4754
  return { content: serialized, found, changed: serialized !== content };
@@ -4427,6 +4761,92 @@ function looksLikeJsonSource(source) {
4427
4761
  const base = source.replace(/\\/g, "/").split("/").pop() ?? source;
4428
4762
  return /\.json$/i.test(base);
4429
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
+ }
4430
4850
  function detectJsonIndent(content) {
4431
4851
  const m = /\n([ \t]+)\S/.exec(content);
4432
4852
  if (!m) return 2;
@@ -4649,6 +5069,7 @@ var init_templater = __esm({
4649
5069
 
4650
5070
  // src/commands/_context.ts
4651
5071
  import { confirm, isCancel, password } from "@clack/prompts";
5072
+ import process5 from "process";
4652
5073
  function buildRepoAuthHooks(args) {
4653
5074
  return {
4654
5075
  interactive: args.interactive ?? true,
@@ -5109,7 +5530,7 @@ function shouldShareInstructions(claudeMd, agentsMd) {
5109
5530
  }
5110
5531
  function buildSharedInstructionsFile(content) {
5111
5532
  return {
5112
- repoPath: SHARED_INSTRUCTIONS_REPO_PATH2,
5533
+ repoPath: SHARED_INSTRUCTIONS_REPO_PATH,
5113
5534
  content
5114
5535
  };
5115
5536
  }
@@ -5119,12 +5540,12 @@ function sharedInstructionsTargets() {
5119
5540
  { tool: "codex", relPath: "AGENTS.md" }
5120
5541
  ];
5121
5542
  }
5122
- var SHARED_INSTRUCTIONS_REPO_PATH2;
5543
+ var SHARED_INSTRUCTIONS_REPO_PATH;
5123
5544
  var init_manifest = __esm({
5124
5545
  "src/core/manifest/index.ts"() {
5125
5546
  "use strict";
5126
5547
  init_schema2();
5127
- SHARED_INSTRUCTIONS_REPO_PATH2 = "shared/instructions.md";
5548
+ SHARED_INSTRUCTIONS_REPO_PATH = "shared/instructions.md";
5128
5549
  }
5129
5550
  });
5130
5551
 
@@ -5134,7 +5555,8 @@ __export(backup_exports, {
5134
5555
  register: () => register2,
5135
5556
  run: () => run2
5136
5557
  });
5137
- import path17 from "path";
5558
+ import path18 from "path";
5559
+ import process6 from "process";
5138
5560
  function register2(program) {
5139
5561
  const configure = (cmd) => cmd.description(
5140
5562
  "Push your AI dev setup to your private repo (snapshot local changes, then commit + push)."
@@ -5165,7 +5587,8 @@ function buildCoreServices(toolHome) {
5165
5587
  sanitizer,
5166
5588
  templater,
5167
5589
  vars: buildVariables(toolHome),
5168
- os: detectOS()
5590
+ os: detectOS(),
5591
+ env: process6.env
5169
5592
  };
5170
5593
  }
5171
5594
  function buildCaptureContext(tool, config, dryRun) {
@@ -5250,9 +5673,9 @@ async function run2(opts = {}) {
5250
5673
  if (sharedContent !== void 0) {
5251
5674
  const shared = buildSharedInstructionsFile(sharedContent);
5252
5675
  await writeCapturedFile2(repoRoot, shared);
5253
- 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)`);
5254
5677
  } else {
5255
- await fs.rmrf(repoJoin(repoRoot, SHARED_INSTRUCTIONS_REPO_PATH2));
5678
+ await fs.rmrf(repoJoin(repoRoot, SHARED_INSTRUCTIONS_REPO_PATH));
5256
5679
  }
5257
5680
  const meta = buildArbellaMeta({
5258
5681
  arbellaVersion: ARBELLA_VERSION,
@@ -5284,8 +5707,8 @@ async function decideSharedInstructions(present) {
5284
5707
  if (!present.includes("claude") || !present.includes("codex")) {
5285
5708
  return { share: false };
5286
5709
  }
5287
- const claudeMd = await readIfExists(path17.join(toolHomeDir("claude"), "CLAUDE.md"));
5288
- 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"));
5289
5712
  if (shouldShareInstructions(claudeMd, agentsMd)) {
5290
5713
  return { share: true, content: claudeMd };
5291
5714
  }
@@ -5295,8 +5718,9 @@ function toolFilesPrefix(tool) {
5295
5718
  return `${tool}/files`;
5296
5719
  }
5297
5720
  async function replaceToolFiles(repoRoot, result) {
5298
- const filesDir = repoJoin(repoRoot, toolFilesPrefix(result.tool));
5299
- await fs.rmrf(filesDir);
5721
+ for (const root of toolRepoDataRoots(result.tool)) {
5722
+ await fs.rmrf(repoJoin(repoRoot, root));
5723
+ }
5300
5724
  for (const file of result.files) {
5301
5725
  await writeCapturedFile2(repoRoot, file);
5302
5726
  }
@@ -5307,6 +5731,11 @@ async function replaceToolFiles(repoRoot, result) {
5307
5731
  const manifestPath = repoJoin(repoRoot, `${result.tool}/manifest.json`);
5308
5732
  await fs.write(manifestPath, serialize(result.manifest));
5309
5733
  }
5734
+ function toolRepoDataRoots(tool) {
5735
+ const roots = [toolFilesPrefix(tool)];
5736
+ if (tool === "cursor") roots.push("cursor/user");
5737
+ return roots;
5738
+ }
5310
5739
  async function writeCapturedFile2(repoRoot, file) {
5311
5740
  const dest = repoJoin(repoRoot, file.repoPath);
5312
5741
  if (file.binary === true) {
@@ -5318,7 +5747,7 @@ async function writeCapturedFile2(repoRoot, file) {
5318
5747
  }
5319
5748
  function repoJoin(repoRoot, repoPath) {
5320
5749
  const segments = repoPath.split("/").filter((s) => s.length > 0);
5321
- return path17.join(repoRoot, ...segments);
5750
+ return path18.join(repoRoot, ...segments);
5322
5751
  }
5323
5752
  function toolLabel(tool) {
5324
5753
  switch (tool) {
@@ -5332,7 +5761,8 @@ function toolLabel(tool) {
5332
5761
  }
5333
5762
  function renderRepoReadme(meta, generatedAtIso) {
5334
5763
  const toolList = meta.tools.map((t) => `- ${toolLabel(t)} (\`${t}\`)`).join("\n");
5335
- 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" : "";
5336
5766
  return `# arbella backup
5337
5767
 
5338
5768
  This is a **private** backup of an AI coding setup, produced by
@@ -5352,6 +5782,7 @@ Each tool lives under \`<tool>/\`:
5352
5782
 
5353
5783
  - \`<tool>/files/\u2026\` \u2014 frozen config files (paths replaced with \`{{HOME}}\`-style
5354
5784
  placeholders, secret values redacted).
5785
+ ${cursorUserLine}
5355
5786
  - \`<tool>/manifest.json\` \u2014 what to reinstall (plugins, marketplaces, skills,
5356
5787
  npm globals) and which plugins to re-enable.
5357
5788
 
@@ -5366,7 +5797,7 @@ npm install -g arbella
5366
5797
  arbella pull <this-repo-url>
5367
5798
  \`\`\`
5368
5799
 
5369
- 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,
5370
5801
  auto-install missing CLIs, write the frozen files back (re-expanding placeholders
5371
5802
  to this machine's paths), reinstall plugins/marketplaces/skills, and re-enable
5372
5803
  plugins.
@@ -5421,15 +5852,17 @@ function renderRepoGitignore(tools) {
5421
5852
  "*.key"
5422
5853
  ];
5423
5854
  for (const name of secretBasenames) lines.push(name);
5424
- lines.push("", "# Per-tool excluded directories (scoped under <tool>/files/)");
5855
+ lines.push("", "# Per-tool excluded directories (scoped under owned data roots)");
5425
5856
  const seen = /* @__PURE__ */ new Set();
5426
5857
  for (const tool of tools) {
5427
5858
  for (const pattern of denylistFor(tool)) {
5428
5859
  if (!pattern.endsWith("/")) continue;
5429
- const scoped = `${tool}/files/${pattern}`;
5430
- if (seen.has(scoped)) continue;
5431
- seen.add(scoped);
5432
- 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
+ }
5433
5866
  }
5434
5867
  }
5435
5868
  return lines.join("\n") + "\n";
@@ -5450,7 +5883,7 @@ function reportDryRun(results, sharing, config) {
5450
5883
  );
5451
5884
  }
5452
5885
  if (sharing) {
5453
- log.step(`+ ${SHARED_INSTRUCTIONS_REPO_PATH2} (shared CLAUDE.md == AGENTS.md)`);
5886
+ log.step(`+ ${SHARED_INSTRUCTIONS_REPO_PATH} (shared CLAUDE.md == AGENTS.md)`);
5454
5887
  }
5455
5888
  log.step("+ arbella.json, README.md, .gitignore");
5456
5889
  reportSecrets(results.flatMap((r) => r.secrets), config.includeSecrets);
@@ -5550,15 +5983,16 @@ var init_backup = __esm({
5550
5983
  init_capture();
5551
5984
  init_capture2();
5552
5985
  init_cursor();
5553
- ARBELLA_VERSION = "0.1.0";
5986
+ init_version();
5987
+ ARBELLA_VERSION = getPackageVersion();
5554
5988
  }
5555
5989
  });
5556
5990
 
5557
5991
  // src/index.ts
5558
5992
  init_log();
5559
- import fs2 from "fs";
5560
- import path23 from "path";
5561
- import { fileURLToPath } from "url";
5993
+ import fs3 from "fs";
5994
+ import path24 from "path";
5995
+ import { fileURLToPath as fileURLToPath2 } from "url";
5562
5996
  import { Command } from "commander";
5563
5997
  import pc2 from "picocolors";
5564
5998
 
@@ -5579,8 +6013,11 @@ function listAdapters() {
5579
6013
  return ALL_ADAPTERS.map((a) => ({ id: a.id, displayName: a.displayName }));
5580
6014
  }
5581
6015
 
6016
+ // src/index.ts
6017
+ init_version();
6018
+
5582
6019
  // src/commands/init.ts
5583
- import path18 from "path";
6020
+ import path19 from "path";
5584
6021
  import {
5585
6022
  cancel as cancel2,
5586
6023
  confirm as confirm3,
@@ -5998,7 +6435,7 @@ async function ensureProviderSignedIn(provider, yes) {
5998
6435
  }
5999
6436
  }
6000
6437
  function defaultLocalPath() {
6001
- return path18.join(dataDir(), "repo");
6438
+ return path19.join(dataDir(), "repo");
6002
6439
  }
6003
6440
  function displayName(id) {
6004
6441
  switch (id) {
@@ -6214,7 +6651,8 @@ function resolveProviderSpec(raw) {
6214
6651
  init_backup();
6215
6652
 
6216
6653
  // src/commands/restore.ts
6217
- import path19 from "path";
6654
+ import path20 from "path";
6655
+ import process7 from "process";
6218
6656
  init_fs();
6219
6657
  init_log();
6220
6658
  init_os();
@@ -6231,12 +6669,13 @@ init_manifest();
6231
6669
  init_claude();
6232
6670
  init_codex();
6233
6671
  init_cursor();
6672
+ init_paths3();
6234
6673
  init_restore();
6235
6674
  init_restore2();
6236
6675
  var WIRING = [
6237
6676
  { id: "claude", adapter: claudeAdapter, planActions },
6238
6677
  { id: "codex", adapter: codexAdapter, planActions: planActions2 },
6239
- { id: "cursor", adapter: cursorAdapter }
6678
+ { id: "cursor", adapter: cursorAdapter, planActions: planActions3 }
6240
6679
  ];
6241
6680
  function wiringFor(id) {
6242
6681
  const w = WIRING.find((entry) => entry.id === id);
@@ -6250,12 +6689,13 @@ function buildCoreServices2(toolHome, os2) {
6250
6689
  sanitizer: createSanitizer(),
6251
6690
  templater: createTemplater(),
6252
6691
  vars: buildVariables(toolHome),
6253
- os: os2
6692
+ os: os2,
6693
+ env: process7.env
6254
6694
  };
6255
6695
  }
6256
6696
  function buildRestoreContext(args) {
6257
6697
  const toolHome = toolHomeDir(args.toolId);
6258
- const repoToolDir = path19.join(args.repoRoot, args.toolId);
6698
+ const repoToolDir = path20.join(args.repoRoot, args.toolId);
6259
6699
  return {
6260
6700
  ...buildCoreServices2(toolHome, args.os),
6261
6701
  toolHome,
@@ -6273,20 +6713,17 @@ function looksBinary3(buf) {
6273
6713
  return false;
6274
6714
  }
6275
6715
  async function readToolFrozen(repoToolDir, tool) {
6276
- const filesRoot = path19.join(repoToolDir, "files");
6277
6716
  const files = [];
6278
6717
  const symlinks = [];
6279
- if (await fs.statKind(filesRoot) !== "dir") {
6280
- return { files, symlinks };
6281
- }
6282
- async function walk2(absDir, relParts) {
6718
+ const roots = frozenRootsForTool(repoToolDir, tool);
6719
+ async function walk2(absDir, relParts, repoPrefix) {
6283
6720
  const entries = await fs.list(absDir);
6284
6721
  entries.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
6285
6722
  for (const name of entries) {
6286
- const abs = path19.join(absDir, name);
6723
+ const abs = path20.join(absDir, name);
6287
6724
  const nextRel = [...relParts, name];
6288
6725
  const relPosix = nextRel.join("/");
6289
- const repoPath = `${tool}/files/${relPosix}`;
6726
+ const repoPath = `${repoPrefix}/${relPosix}`;
6290
6727
  const kind = await fs.statKind(abs);
6291
6728
  if (kind === "symlink") {
6292
6729
  let target;
@@ -6302,7 +6739,7 @@ async function readToolFrozen(repoToolDir, tool) {
6302
6739
  continue;
6303
6740
  }
6304
6741
  if (kind === "dir") {
6305
- await walk2(abs, nextRel);
6742
+ await walk2(abs, nextRel, repoPrefix);
6306
6743
  continue;
6307
6744
  }
6308
6745
  if (kind !== "file") continue;
@@ -6330,11 +6767,23 @@ async function readToolFrozen(repoToolDir, tool) {
6330
6767
  }
6331
6768
  }
6332
6769
  }
6333
- 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
+ }
6334
6774
  return { files, symlinks };
6335
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
+ }
6336
6785
  async function readToolManifest(repoToolDir, tool) {
6337
- const manifestPath = path19.join(repoToolDir, "manifest.json");
6786
+ const manifestPath = path20.join(repoToolDir, "manifest.json");
6338
6787
  if (!await fs.exists(manifestPath)) {
6339
6788
  log.debug(`restore: no manifest.json for ${tool}; using empty manifest.`);
6340
6789
  return emptyManifest(tool);
@@ -6351,7 +6800,7 @@ async function readToolManifest(repoToolDir, tool) {
6351
6800
  return parseManifest(json);
6352
6801
  }
6353
6802
  async function loadRestoreData(repoRoot, tool) {
6354
- const repoToolDir = path19.join(repoRoot, tool);
6803
+ const repoToolDir = path20.join(repoRoot, tool);
6355
6804
  const manifest = await readToolManifest(repoToolDir, tool);
6356
6805
  const { files, symlinks } = await readToolFrozen(repoToolDir, tool);
6357
6806
  return { manifest, files, symlinks };
@@ -6364,49 +6813,65 @@ function parseToolsFlag(raw) {
6364
6813
  );
6365
6814
  return ids;
6366
6815
  }
6367
- function selectTools(meta, flagTools, configTools) {
6816
+ function selectToolsForRestore(meta, flagTools, _configTools) {
6368
6817
  const captured = new Set(meta.tools);
6369
6818
  let candidate;
6370
6819
  if (flagTools && flagTools.length > 0) {
6371
6820
  candidate = new Set(flagTools);
6372
- } else if (configTools.length > 0) {
6373
- candidate = new Set(configTools);
6374
6821
  } else {
6375
6822
  candidate = new Set(captured);
6376
6823
  }
6377
6824
  return TOOL_IDS.filter((id) => captured.has(id) && candidate.has(id));
6378
6825
  }
6379
- async function safetyBackup(tools, iso) {
6826
+ async function safetyBackup(tools, iso, os2) {
6380
6827
  const stamp = iso.replace(/[:.]/g, "-");
6381
- const backupsRoot = path19.join(dataDir(), "safety-backups");
6382
6828
  const made = [];
6383
6829
  for (const tool of tools) {
6384
- const source = toolHomeDir(tool);
6385
- if (!await fs.exists(source)) {
6386
- log.debug(`restore: no existing ${tool} home to back up (${source}).`);
6387
- continue;
6388
- }
6389
- const dest = path19.join(backupsRoot, `${tool}-${stamp}`);
6390
- try {
6391
- await fs.copy(source, dest);
6392
- made.push({ tool, source, dest });
6393
- log.step(`Safety backup: ${source} -> ${dest}`);
6394
- } catch (err) {
6395
- log.warn(
6396
- `restore: failed to safety-backup ${tool} (${source}): ${errMsg(err)}`
6397
- );
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
+ }
6398
6844
  }
6399
6845
  }
6400
6846
  return made;
6401
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
+ }
6402
6867
  async function deploySharedInstructions(repoRoot, toolsInScope, dryRun) {
6403
- const sharedAbs = path19.join(
6868
+ const sharedAbs = path20.join(
6404
6869
  repoRoot,
6405
- ...SHARED_INSTRUCTIONS_REPO_PATH2.split("/")
6870
+ ...SHARED_INSTRUCTIONS_REPO_PATH.split("/")
6406
6871
  );
6407
6872
  if (!await fs.exists(sharedAbs)) {
6408
6873
  log.warn(
6409
- `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.`
6410
6875
  );
6411
6876
  return;
6412
6877
  }
@@ -6414,7 +6879,7 @@ async function deploySharedInstructions(repoRoot, toolsInScope, dryRun) {
6414
6879
  const content = await fs.read(sharedAbs);
6415
6880
  for (const target of sharedInstructionsTargets()) {
6416
6881
  if (!inScope.has(target.tool)) continue;
6417
- const dest = path19.join(toolHomeDir(target.tool), target.relPath);
6882
+ const dest = path20.join(toolHomeDir(target.tool), target.relPath);
6418
6883
  if (dryRun) {
6419
6884
  log.step(`Would deploy shared instructions -> ${dest}`);
6420
6885
  continue;
@@ -6471,7 +6936,7 @@ async function fallbackActions(ctx, tool, data) {
6471
6936
  const out2 = [];
6472
6937
  for (const file of data.files) {
6473
6938
  const rel = stripFilesPrefix(tool, file.repoPath);
6474
- const dest = path19.join(ctx.toolHome, ...rel.split("/"));
6939
+ const dest = path20.join(ctx.toolHome, ...rel.split("/"));
6475
6940
  const overwrites = await ctx.fs.exists(dest);
6476
6941
  if (ctx.sourceOfTruth === "local" && overwrites) continue;
6477
6942
  out2.push({
@@ -6484,7 +6949,7 @@ async function fallbackActions(ctx, tool, data) {
6484
6949
  }
6485
6950
  for (const link of data.symlinks) {
6486
6951
  const rel = stripFilesPrefix(tool, link.repoPath);
6487
- const dest = path19.join(ctx.toolHome, ...rel.split("/"));
6952
+ const dest = path20.join(ctx.toolHome, ...rel.split("/"));
6488
6953
  const overwrites = await ctx.fs.statKind(dest) !== "missing";
6489
6954
  if (ctx.sourceOfTruth === "local" && overwrites) continue;
6490
6955
  out2.push({
@@ -6539,7 +7004,7 @@ function printPlan(plan, meta, l = log) {
6539
7004
  `Tools: ${plan.tools.length > 0 ? plan.tools.join(", ") : "(none)"}`
6540
7005
  );
6541
7006
  l.step(
6542
- `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).`
6543
7008
  );
6544
7009
  if (plan.missingClis.length > 0) {
6545
7010
  l.step(`CLIs to auto-install: ${plan.missingClis.join(", ")}`);
@@ -6606,7 +7071,7 @@ async function resolveRepo(repoUrl, optRepo) {
6606
7071
  if (explicit && explicit.trim() !== "") {
6607
7072
  const url = explicit.trim();
6608
7073
  const sameAsConfig = config.repo.url.trim() !== "" && config.repo.url.trim() === url;
6609
- 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));
6610
7075
  return { provider: config.repo.provider, url, localPath: localPath2 };
6611
7076
  }
6612
7077
  if (!config.repo.url || config.repo.url.trim() === "") {
@@ -6614,7 +7079,7 @@ async function resolveRepo(repoUrl, optRepo) {
6614
7079
  "No repo to pull from. Pass a repo URL (`arbella pull <repo-url>`) or run `arbella init` first."
6615
7080
  );
6616
7081
  }
6617
- 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));
6618
7083
  return { provider: config.repo.provider, url: config.repo.url, localPath };
6619
7084
  }
6620
7085
  function slugForUrl(url) {
@@ -6651,7 +7116,7 @@ async function run4(repoUrl, opts) {
6651
7116
  });
6652
7117
  await ensureRepoReady(repo, authHooks);
6653
7118
  const repoRoot = repo.localPath;
6654
- const metaPath = path19.join(repoRoot, "arbella.json");
7119
+ const metaPath = path20.join(repoRoot, "arbella.json");
6655
7120
  if (!await fs.exists(metaPath)) {
6656
7121
  throw new Error(
6657
7122
  `Not a arbella backup repo: ${metaPath} is missing. Did you point restore at the right repository?`
@@ -6667,7 +7132,7 @@ async function run4(repoUrl, opts) {
6667
7132
  }
6668
7133
  const config = await loadConfigOrDefault();
6669
7134
  const flagTools = parseToolsFlag(opts.tools);
6670
- const tools = selectTools(meta, flagTools, config.tools);
7135
+ const tools = selectToolsForRestore(meta, flagTools, config.tools);
6671
7136
  if (tools.length === 0) {
6672
7137
  log.warn(
6673
7138
  `No tools to restore (the repo + your selection have no overlap). Repo captured: ${meta.tools.join(", ") || "(none)"}.`
@@ -6691,7 +7156,7 @@ async function run4(repoUrl, opts) {
6691
7156
  }
6692
7157
  const iso = (/* @__PURE__ */ new Date()).toISOString();
6693
7158
  log.info("Creating safety backups of existing tool homes (R14)\u2026");
6694
- const backups = await safetyBackup(tools, iso);
7159
+ const backups = await safetyBackup(tools, iso, os2);
6695
7160
  if (backups.length === 0) {
6696
7161
  log.debug("restore: no existing tool homes needed backing up.");
6697
7162
  }
@@ -6731,7 +7196,7 @@ async function run4(repoUrl, opts) {
6731
7196
  printReauthReminder(tools);
6732
7197
  if (backups.length > 0) {
6733
7198
  log.info(
6734
- `Previous tool homes were safely backed up under ${path19.join(
7199
+ `Previous tool homes were safely backed up under ${path20.join(
6735
7200
  dataDir(),
6736
7201
  "safety-backups"
6737
7202
  )} (restore them manually if anything looks wrong).`
@@ -6791,7 +7256,8 @@ init_cursor();
6791
7256
  init_capture();
6792
7257
  init_capture2();
6793
7258
  init_cursor();
6794
- import path20 from "path";
7259
+ import path21 from "path";
7260
+ import process8 from "process";
6795
7261
  var ADAPTERS = {
6796
7262
  claude: { adapter: claudeAdapter, capture },
6797
7263
  codex: { adapter: codexAdapter, capture: capture2 },
@@ -6809,8 +7275,8 @@ async function run5(opts) {
6809
7275
  await ensureLocalClone(config.repo);
6810
7276
  const repoRoot = config.repo.localPath;
6811
7277
  const repoInitialized = await isRepoInitialized(repoRoot);
6812
- const claudeMd = await readIfExists2(path20.join(toolHomeDir("claude"), "CLAUDE.md"));
6813
- 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"));
6814
7280
  const sharing = shouldShareInstructions(claudeMd, agentsMd);
6815
7281
  const toolStatuses = [];
6816
7282
  for (const tool of config.tools) {
@@ -6872,8 +7338,8 @@ async function run5(opts) {
6872
7338
  const change = await classifyFile(repoRoot, file);
6873
7339
  if (change.kind !== "unchanged") sharedChanges.push(change);
6874
7340
  } else {
6875
- if (await committedFileExists(repoRoot, SHARED_INSTRUCTIONS_REPO_PATH2)) {
6876
- 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" });
6877
7343
  }
6878
7344
  }
6879
7345
  const clean = sharedChanges.length === 0 && toolStatuses.every(
@@ -6889,7 +7355,7 @@ async function run5(opts) {
6889
7355
  clean
6890
7356
  };
6891
7357
  if (opts.json) {
6892
- process.stdout.write(serialize(report));
7358
+ process8.stdout.write(serialize(report));
6893
7359
  return;
6894
7360
  }
6895
7361
  printHuman(report);
@@ -6903,6 +7369,7 @@ function buildCaptureContext2(tool, includeSecrets, includeMemories) {
6903
7369
  templater,
6904
7370
  vars: buildVariables(toolHome),
6905
7371
  os: detectOS(),
7372
+ env: process8.env,
6906
7373
  toolHome,
6907
7374
  includeSecrets,
6908
7375
  includeMemories,
@@ -7108,7 +7575,7 @@ async function walkRepoFiles(baseAbs, basePosix) {
7108
7575
  const entries = await fs.list(baseAbs);
7109
7576
  for (const name of entries) {
7110
7577
  if (name === ".git") continue;
7111
- const childAbs = path20.join(baseAbs, name);
7578
+ const childAbs = path21.join(baseAbs, name);
7112
7579
  const childPosix = `${basePosix}/${name}`;
7113
7580
  const kind = await fs.statKind(childAbs);
7114
7581
  if (kind === "symlink") {
@@ -7122,7 +7589,7 @@ async function walkRepoFiles(baseAbs, basePosix) {
7122
7589
  return out2;
7123
7590
  }
7124
7591
  function repoAbsPath(repoRoot, repoPath) {
7125
- return path20.join(repoRoot, ...repoPath.split("/"));
7592
+ return path21.join(repoRoot, ...repoPath.split("/"));
7126
7593
  }
7127
7594
  async function committedFileExists(repoRoot, repoPath) {
7128
7595
  const kind = await fs.statKind(repoAbsPath(repoRoot, repoPath));
@@ -7233,7 +7700,7 @@ function renderChangeLine(c) {
7233
7700
  }
7234
7701
 
7235
7702
  // src/commands/secrets.ts
7236
- import path22 from "path";
7703
+ import path23 from "path";
7237
7704
  import * as clack2 from "@clack/prompts";
7238
7705
 
7239
7706
  // src/core/secrets/index.ts
@@ -7246,7 +7713,7 @@ import {
7246
7713
  scryptSync
7247
7714
  } from "crypto";
7248
7715
  import { promises as fsp4 } from "fs";
7249
- import path21 from "path";
7716
+ import path22 from "path";
7250
7717
  var SCRYPT_PARAMS = {
7251
7718
  N: 1 << 15,
7252
7719
  // 32768
@@ -7341,7 +7808,7 @@ async function gatherSecretRefs(toolId) {
7341
7808
  const refs = [];
7342
7809
  const home4 = toolHomeDir(toolId);
7343
7810
  for (const spec of SECRET_FILES_BY_TOOL[toolId]) {
7344
- const abs = path21.join(home4, spec.relPath);
7811
+ const abs = path22.join(home4, spec.relPath);
7345
7812
  if (await fs.exists(abs)) {
7346
7813
  refs.push({
7347
7814
  tool: toolId,
@@ -7363,10 +7830,10 @@ async function collectSecretFiles(refs, createdAt) {
7363
7830
  const dedupeKey = `${ref.tool}:${relPath}`;
7364
7831
  if (seen.has(dedupeKey)) continue;
7365
7832
  seen.add(dedupeKey);
7366
- const abs = path21.join(toolHomeDir(ref.tool), relPath);
7833
+ const abs = path22.join(toolHomeDir(ref.tool), relPath);
7367
7834
  if (!await fs.exists(abs)) continue;
7368
7835
  const bytes = await fs.readBytes(abs);
7369
- const mode = await readMode(abs);
7836
+ const mode = await readMode2(abs);
7370
7837
  entries.push({
7371
7838
  tool: ref.tool,
7372
7839
  relPath,
@@ -7380,9 +7847,9 @@ async function applySecretBundle(bundle) {
7380
7847
  for (const entry of bundle.entries) {
7381
7848
  const home4 = toolHomeDir(entry.tool);
7382
7849
  const rel = entry.relPath.replace(/\\/g, "/");
7383
- const dest = path21.resolve(home4, rel);
7384
- const homeResolved = path21.resolve(home4);
7385
- const withinHome = dest === homeResolved || dest.startsWith(homeResolved + path21.sep);
7850
+ const dest = path22.resolve(home4, rel);
7851
+ const homeResolved = path22.resolve(home4);
7852
+ const withinHome = dest === homeResolved || dest.startsWith(homeResolved + path22.sep);
7386
7853
  if (!withinHome) {
7387
7854
  throw new Error(
7388
7855
  `Refusing to write secret outside ${entry.tool} home (suspicious path: ${rel}).`
@@ -7393,7 +7860,7 @@ async function applySecretBundle(bundle) {
7393
7860
  await fs.writeBytes(dest, bytes, mode);
7394
7861
  }
7395
7862
  }
7396
- async function readMode(abs) {
7863
+ async function readMode2(abs) {
7397
7864
  try {
7398
7865
  const st = await fsp4.lstat(abs);
7399
7866
  const m = st.mode & 4095;
@@ -7498,7 +7965,7 @@ async function runExport(opts) {
7498
7965
  return;
7499
7966
  }
7500
7967
  const blob = encryptBundle(bundle, passphrase);
7501
- const outPath = path22.resolve(opts.out);
7968
+ const outPath = path23.resolve(opts.out);
7502
7969
  await fs.write(outPath, blob + "\n", 384);
7503
7970
  clack2.note(
7504
7971
  [
@@ -7507,7 +7974,7 @@ async function runExport(opts) {
7507
7974
  "",
7508
7975
  "This blob is safe to copy between machines (it is encrypted with your",
7509
7976
  "passphrase). It NEVER goes through git. On the target machine run:",
7510
- ` arbella secrets import --in ${path22.basename(outPath)}`,
7977
+ ` arbella secrets import --in ${path23.basename(outPath)}`,
7511
7978
  "",
7512
7979
  "Keep your passphrase safe: without it the blob cannot be decrypted."
7513
7980
  ].join("\n"),
@@ -7517,7 +7984,7 @@ async function runExport(opts) {
7517
7984
  }
7518
7985
  async function runImport(opts) {
7519
7986
  clack2.intro("arbella secrets import");
7520
- const inPath = path22.resolve(opts.in);
7987
+ const inPath = path23.resolve(opts.in);
7521
7988
  if (!await fs.exists(inPath)) {
7522
7989
  clack2.cancel(
7523
7990
  `No blob found at:
@@ -7619,7 +8086,7 @@ function errMessage7(err) {
7619
8086
  }
7620
8087
 
7621
8088
  // src/index.ts
7622
- var VERSION = "0.1.0";
8089
+ var VERSION = getPackageVersion();
7623
8090
  function buildProgram() {
7624
8091
  const program = new Command();
7625
8092
  const supported = listAdapters().map((a) => a.displayName).join(", ");
@@ -7648,11 +8115,11 @@ async function main(argv = process.argv) {
7648
8115
  function isDirectRun() {
7649
8116
  const entry = process.argv[1];
7650
8117
  if (!entry) return false;
7651
- const modulePath = fileURLToPath(import.meta.url);
8118
+ const modulePath = fileURLToPath2(import.meta.url);
7652
8119
  try {
7653
- return fs2.realpathSync(entry) === fs2.realpathSync(modulePath);
8120
+ return fs3.realpathSync(entry) === fs3.realpathSync(modulePath);
7654
8121
  } catch {
7655
- return path23.resolve(entry) === modulePath;
8122
+ return path24.resolve(entry) === modulePath;
7656
8123
  }
7657
8124
  }
7658
8125
  function handleTopLevelError(err) {