@hanzlaa/rcode 3.1.0 → 3.2.0

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.
Files changed (3) hide show
  1. package/cli/install.js +145 -47
  2. package/dist/rcode.js +134 -43
  3. package/package.json +2 -2
package/cli/install.js CHANGED
@@ -268,20 +268,26 @@ function printInstallHeader(targetVersion) {
268
268
  * Returns a set like { claude: true, cursor: false, gemini: false }.
269
269
  */
270
270
  function detectIdeSignals(target) {
271
- const signals = { claude: false, cursor: false, gemini: false };
271
+ const signals = { claude: false, cursor: false, gemini: false, vscode: false, antigravity: false };
272
272
  // 1. Project-local install dirs (strongest signal — they already use one)
273
273
  if (fs.existsSync(path.join(target, '.claude'))) signals.claude = true;
274
274
  if (fs.existsSync(path.join(target, '.cursor'))) signals.cursor = true;
275
275
  if (fs.existsSync(path.join(target, '.gemini'))) signals.gemini = true;
276
+ if (fs.existsSync(path.join(target, '.vscode'))) signals.vscode = true;
277
+ if (fs.existsSync(path.join(target, '.antigravity'))) signals.antigravity = true;
276
278
  // 2. User-level config dirs
277
279
  const home = os.homedir();
278
280
  if (fs.existsSync(path.join(home, '.claude'))) signals.claude = true;
279
281
  if (fs.existsSync(path.join(home, '.cursor'))) signals.cursor = true;
280
282
  if (fs.existsSync(path.join(home, '.config', 'Cursor'))) signals.cursor = true;
281
283
  if (fs.existsSync(path.join(home, '.gemini'))) signals.gemini = true;
284
+ if (fs.existsSync(path.join(home, '.vscode'))) signals.vscode = true;
285
+ if (fs.existsSync(path.join(home, '.config', 'Code'))) signals.vscode = true;
286
+ if (fs.existsSync(path.join(home, '.antigravity'))) signals.antigravity = true;
282
287
  // 3. Env vars commonly set by editor terminals
283
288
  if (process.env.CURSOR_TRACE_ID || /cursor/i.test(process.env.TERM_PROGRAM || '')) signals.cursor = true;
284
289
  if (process.env.CLAUDECODE === '1' || process.env.CLAUDE_CODE_ENTRYPOINT) signals.claude = true;
290
+ if (process.env.VSCODE_PID || /vscode/i.test(process.env.TERM_PROGRAM || '')) signals.vscode = true;
285
291
  return signals;
286
292
  }
287
293
 
@@ -297,43 +303,32 @@ async function resolveIde(opts) {
297
303
  if (opts.yes || !process.stdin.isTTY) return opts.ide || 'claude';
298
304
 
299
305
  const signals = detectIdeSignals(opts.target);
300
- const detected = ['claude', 'cursor', 'gemini'].filter(k => signals[k]);
301
-
302
- // Build the menu — detected IDEs marked with a hint
303
- const choices = [
304
- { key: '1', value: 'claude', label: 'Claude Code', hint: signals.claude ? dim('(detected)') : '' },
305
- { key: '2', value: 'cursor', label: 'Cursor', hint: signals.cursor ? dim('(detected)') : '' },
306
- { key: '3', value: 'gemini', label: 'Gemini CLI', hint: signals.gemini ? dim('(detected)') : dim('(beta — limited)') },
307
- ];
306
+ const detected = ['claude', 'cursor', 'gemini', 'vscode'].filter(k => signals[k]);
308
307
 
309
308
  // Pick a default: prefer the single detected IDE; otherwise claude
310
309
  let defaultValue = 'claude';
311
310
  if (detected.length === 1) defaultValue = detected[0];
312
311
 
313
- const readline = require('readline');
314
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
315
- const prompt = (q) => new Promise(r => rl.question(q, a => r(a)));
312
+ // Use @clack/prompts for arrow-key navigation. Closes #449 / #450.
313
+ const choice = await clack.select({
314
+ message: '🎯 Which editor will you use rcode with?',
315
+ initialValue: defaultValue,
316
+ options: [
317
+ { value: 'claude', label: 'Claude Code', hint: signals.claude ? '(detected)' : undefined },
318
+ { value: 'cursor', label: 'Cursor', hint: signals.cursor ? '(detected)' : undefined },
319
+ { value: 'gemini', label: 'Gemini CLI', hint: signals.gemini ? '(detected)' : '(beta — limited)' },
320
+ { value: 'vscode', label: 'VS Code', hint: signals.vscode ? '(detected)' : '(via Continue / Copilot extensions)' },
321
+ { value: 'antigravity', label: 'Antigravity', hint: '(experimental — installs to .antigravity/)' },
322
+ ],
323
+ });
316
324
 
317
- console.log(pc.bold('🎯 Which editor will you use Rihal with?'));
318
- console.log('');
319
- for (const c of choices) {
320
- const marker = c.value === defaultValue ? pc.green('●') : dim('○');
321
- const label = c.value === defaultValue ? pc.bold(c.label) : c.label;
322
- console.log(` ${marker} ${pc.cyan('[' + c.key + ']')} ${label} ${c.hint}`);
325
+ // Handle Ctrl-C cleanly
326
+ if (clack.isCancel(choice)) {
327
+ clack.cancel('Install cancelled.');
328
+ process.exit(0);
323
329
  }
324
- console.log('');
325
- const defaultKey = choices.find(c => c.value === defaultValue).key;
326
- const answer = (await prompt(` Pick an editor [${defaultKey}]: `)).trim().toLowerCase();
327
- rl.close();
328
-
329
- if (!answer) return defaultValue;
330
- // Accept either the number key or the name
331
- const byKey = choices.find(c => c.key === answer);
332
- if (byKey) return byKey.value;
333
- const byName = choices.find(c => c.value === answer || c.label.toLowerCase().startsWith(answer));
334
- if (byName) return byName.value;
335
- console.log(dim(` Unrecognised choice "${answer}" — falling back to ${defaultValue}.`));
336
- return defaultValue;
330
+
331
+ return choice;
337
332
  }
338
333
 
339
334
  /**
@@ -345,19 +340,21 @@ async function resolveCommitPlanning(opts) {
345
340
  if (opts.commitPlanning !== null) return opts.commitPlanning;
346
341
  if (opts.yes || !process.stdin.isTTY) return true; // non-interactive default
347
342
 
348
- const readline = require('readline');
349
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
350
- const prompt = (q) => new Promise(r => rl.question(q, a => r(a)));
351
- console.log('');
352
- console.log('📋 .planning/ holds PRDs, roadmaps, sprints, SUMMARY files.');
353
- console.log(' Commit them to git, or keep them local?');
354
- console.log('');
355
- console.log(' [Y] Commit — collaborators see the same plans (default, recommended)');
356
- console.log(' [n] Gitignore — planning stays local (good for sensitive PRDs)');
357
- console.log('');
358
- const answer = (await prompt(' Commit planning artifacts? [Y/n]: ')).trim().toLowerCase();
359
- rl.close();
360
- return !(answer === 'n' || answer === 'no');
343
+ const choice = await clack.select({
344
+ message: '📋 .planning/ holds PRDs, roadmaps, sprints, SUMMARY files. How should they be tracked?',
345
+ initialValue: 'commit',
346
+ options: [
347
+ { value: 'commit', label: 'Commit', hint: 'collaborators see the same plans (recommended)' },
348
+ { value: 'gitignore', label: 'Gitignore', hint: 'planning stays local (good for sensitive PRDs)' },
349
+ ],
350
+ });
351
+
352
+ if (clack.isCancel(choice)) {
353
+ clack.cancel('Install cancelled.');
354
+ process.exit(0);
355
+ }
356
+
357
+ return choice === 'commit';
361
358
  }
362
359
 
363
360
  function printHelp() {
@@ -1342,6 +1339,7 @@ async function install(opts) {
1342
1339
  let preserved = 0;
1343
1340
  const preservedFiles = [];
1344
1341
  const preservedDiffs = []; // { rel, insertions, deletions, patch } for #251
1342
+ const conflictedFiles = []; // { rel, src, destPath, existingContent, sourceContent } for #451 / #453
1345
1343
  const spinner = createSpinner(dim(`Installing ${plan.length} files…`), { color: 'cyan' }).start();
1346
1344
 
1347
1345
  for (const entry of plan) {
@@ -1381,9 +1379,15 @@ async function install(opts) {
1381
1379
  const sourceHash = sha256(fs.readFileSync(entry.src));
1382
1380
  if (existingHash === sourceHash) { skipped++; continue; }
1383
1381
  if (!opts.yes && !opts.nonDestructive) {
1384
- spinner.stop();
1385
- console.warn(' ' + warn(`${entry.rel} differs from package version use --force-overwrite to overwrite`));
1386
- spinner.start();
1382
+ // Buffer the conflict instead of spamming a warning per file (#451).
1383
+ // Surfaced as a categorised summary post-install + interactive offer (#453).
1384
+ conflictedFiles.push({
1385
+ rel: relForward,
1386
+ src: entry.src,
1387
+ destPath,
1388
+ existingContent: fs.readFileSync(destPath, 'utf8'),
1389
+ sourceContent: fs.readFileSync(entry.src, 'utf8'),
1390
+ });
1387
1391
  skipped++;
1388
1392
  continue;
1389
1393
  }
@@ -1406,6 +1410,100 @@ async function install(opts) {
1406
1410
 
1407
1411
  spinner.success({ text: ok(`${copied} files installed`) });
1408
1412
 
1413
+ // Categorised conflict summary (#451) + interactive resolution offer (#453).
1414
+ // Replaces the per-file 'differs from package version' warning spam.
1415
+ if (conflictedFiles.length > 0) {
1416
+ const byCategory = { workflows: [], agents: [], commands: [], skills: [], references: [], other: [] };
1417
+ for (const c of conflictedFiles) {
1418
+ if (c.rel.includes('/workflows/')) byCategory.workflows.push(c);
1419
+ else if (c.rel.includes('/agents/')) byCategory.agents.push(c);
1420
+ else if (c.rel.includes('/commands/')) byCategory.commands.push(c);
1421
+ else if (c.rel.includes('/skills/')) byCategory.skills.push(c);
1422
+ else if (c.rel.includes('/references/')) byCategory.references.push(c);
1423
+ else byCategory.other.push(c);
1424
+ }
1425
+ console.log('');
1426
+ console.log(' ' + warn(`${conflictedFiles.length} file${conflictedFiles.length === 1 ? '' : 's'} have local edits AND v${readPackageVersion()} updates:`));
1427
+ for (const [cat, list] of Object.entries(byCategory)) {
1428
+ if (list.length === 0) continue;
1429
+ console.log(' ' + dim(`${list.length} ${cat}`));
1430
+ }
1431
+ console.log('');
1432
+
1433
+ if (!opts.yes && process.stdin.isTTY) {
1434
+ const action = await clack.select({
1435
+ message: 'How should we handle these?',
1436
+ initialValue: 'review',
1437
+ options: [
1438
+ { value: 'review', label: 'Review each one', hint: 'see the diff, decide per file' },
1439
+ { value: 'upstream', label: 'Take v' + readPackageVersion() + ' for all', hint: 'lose local edits, get all bug fixes' },
1440
+ { value: 'keep', label: 'Keep my local edits', hint: 'skip v' + readPackageVersion() + ' updates for these files (current behaviour)' },
1441
+ ],
1442
+ });
1443
+ if (clack.isCancel(action)) {
1444
+ clack.note('Skipped — local edits preserved.');
1445
+ } else if (action === 'upstream') {
1446
+ let applied = 0;
1447
+ for (const c of conflictedFiles) {
1448
+ fs.writeFileSync(c.destPath, c.sourceContent, 'utf8');
1449
+ applied++;
1450
+ }
1451
+ console.log(' ' + ok(`Applied v${readPackageVersion()} to ${applied} file${applied === 1 ? '' : 's'}.`));
1452
+ } else if (action === 'review') {
1453
+ let applied = 0, kept = 0;
1454
+ for (const c of conflictedFiles) {
1455
+ const patch = createTwoFilesPatch(c.rel, c.rel, c.existingContent, c.sourceContent, 'local', 'v' + readPackageVersion());
1456
+ let ins = 0, del = 0;
1457
+ for (const line of patch.split('\n')) {
1458
+ if (line.startsWith('+') && !line.startsWith('+++')) ins++;
1459
+ if (line.startsWith('-') && !line.startsWith('---')) del++;
1460
+ }
1461
+ console.log('');
1462
+ console.log(' ' + pc.bold(c.rel) + dim(' ') + pc.green(`+${ins}`) + ' ' + pc.red(`-${del}`));
1463
+ const decision = await clack.select({
1464
+ message: 'Take upstream, keep local, or view diff?',
1465
+ initialValue: 'view',
1466
+ options: [
1467
+ { value: 'upstream', label: 'Take v' + readPackageVersion() },
1468
+ { value: 'keep', label: 'Keep local' },
1469
+ { value: 'view', label: 'View diff first' },
1470
+ ],
1471
+ });
1472
+ let finalAction = decision;
1473
+ if (clack.isCancel(decision) || decision === 'view') {
1474
+ for (const line of patch.split('\n').slice(4)) {
1475
+ if (line.startsWith('+')) process.stdout.write(pc.green(line) + '\n');
1476
+ else if (line.startsWith('-')) process.stdout.write(pc.red(line) + '\n');
1477
+ else if (line.startsWith('@')) process.stdout.write(pc.cyan(line) + '\n');
1478
+ else process.stdout.write(dim(line) + '\n');
1479
+ }
1480
+ const after = await clack.select({
1481
+ message: 'Now: take upstream or keep local?',
1482
+ initialValue: 'keep',
1483
+ options: [
1484
+ { value: 'upstream', label: 'Take v' + readPackageVersion() },
1485
+ { value: 'keep', label: 'Keep local' },
1486
+ ],
1487
+ });
1488
+ finalAction = clack.isCancel(after) ? 'keep' : after;
1489
+ }
1490
+ if (finalAction === 'upstream') {
1491
+ fs.writeFileSync(c.destPath, c.sourceContent, 'utf8');
1492
+ applied++;
1493
+ } else {
1494
+ kept++;
1495
+ }
1496
+ }
1497
+ console.log(' ' + ok(`Review complete: ${applied} applied, ${kept} kept local.`));
1498
+ } else {
1499
+ console.log(' ' + dim(`${conflictedFiles.length} file${conflictedFiles.length === 1 ? '' : 's'} kept local. Re-run with --force-overwrite or 'rcode update' anytime.`));
1500
+ }
1501
+ } else {
1502
+ console.log(' ' + dim(`Re-run with --force-overwrite to apply v${readPackageVersion()} updates, or pipe through an interactive shell to resolve per-file.`));
1503
+ }
1504
+ console.log('');
1505
+ }
1506
+
1409
1507
  // Write .rihal/_config/manifest.yaml + agent-manifest.csv + files-manifest.csv
1410
1508
  const configDir = path.join(opts.target, '.rihal', '_config');
1411
1509
  ensureDir(configDir);
package/dist/rcode.js CHANGED
@@ -15952,69 +15952,65 @@ var require_install = __commonJS({
15952
15952
  console.log(lines.join("\n"));
15953
15953
  }
15954
15954
  function detectIdeSignals(target) {
15955
- const signals = { claude: false, cursor: false, gemini: false };
15955
+ const signals = { claude: false, cursor: false, gemini: false, vscode: false, antigravity: false };
15956
15956
  if (fs2.existsSync(path2.join(target, ".claude"))) signals.claude = true;
15957
15957
  if (fs2.existsSync(path2.join(target, ".cursor"))) signals.cursor = true;
15958
15958
  if (fs2.existsSync(path2.join(target, ".gemini"))) signals.gemini = true;
15959
+ if (fs2.existsSync(path2.join(target, ".vscode"))) signals.vscode = true;
15960
+ if (fs2.existsSync(path2.join(target, ".antigravity"))) signals.antigravity = true;
15959
15961
  const home = os.homedir();
15960
15962
  if (fs2.existsSync(path2.join(home, ".claude"))) signals.claude = true;
15961
15963
  if (fs2.existsSync(path2.join(home, ".cursor"))) signals.cursor = true;
15962
15964
  if (fs2.existsSync(path2.join(home, ".config", "Cursor"))) signals.cursor = true;
15963
15965
  if (fs2.existsSync(path2.join(home, ".gemini"))) signals.gemini = true;
15966
+ if (fs2.existsSync(path2.join(home, ".vscode"))) signals.vscode = true;
15967
+ if (fs2.existsSync(path2.join(home, ".config", "Code"))) signals.vscode = true;
15968
+ if (fs2.existsSync(path2.join(home, ".antigravity"))) signals.antigravity = true;
15964
15969
  if (process.env.CURSOR_TRACE_ID || /cursor/i.test(process.env.TERM_PROGRAM || "")) signals.cursor = true;
15965
15970
  if (process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE_ENTRYPOINT) signals.claude = true;
15971
+ if (process.env.VSCODE_PID || /vscode/i.test(process.env.TERM_PROGRAM || "")) signals.vscode = true;
15966
15972
  return signals;
15967
15973
  }
15968
15974
  async function resolveIde(opts) {
15969
15975
  if (opts.ideProvided) return opts.ide;
15970
15976
  if (opts.yes || !process.stdin.isTTY) return opts.ide || "claude";
15971
15977
  const signals = detectIdeSignals(opts.target);
15972
- const detected = ["claude", "cursor", "gemini"].filter((k) => signals[k]);
15973
- const choices = [
15974
- { key: "1", value: "claude", label: "Claude Code", hint: signals.claude ? dim("(detected)") : "" },
15975
- { key: "2", value: "cursor", label: "Cursor", hint: signals.cursor ? dim("(detected)") : "" },
15976
- { key: "3", value: "gemini", label: "Gemini CLI", hint: signals.gemini ? dim("(detected)") : dim("(beta \u2014 limited)") }
15977
- ];
15978
+ const detected = ["claude", "cursor", "gemini", "vscode"].filter((k) => signals[k]);
15978
15979
  let defaultValue = "claude";
15979
15980
  if (detected.length === 1) defaultValue = detected[0];
15980
- const readline = require("readline");
15981
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
15982
- const prompt = (q) => new Promise((r) => rl.question(q, (a) => r(a)));
15983
- console.log(pc.bold("\u{1F3AF} Which editor will you use Rihal with?"));
15984
- console.log("");
15985
- for (const c of choices) {
15986
- const marker = c.value === defaultValue ? pc.green("\u25CF") : dim("\u25CB");
15987
- const label = c.value === defaultValue ? pc.bold(c.label) : c.label;
15988
- console.log(` ${marker} ${pc.cyan("[" + c.key + "]")} ${label} ${c.hint}`);
15981
+ const choice = await clack.select({
15982
+ message: "\u{1F3AF} Which editor will you use rcode with?",
15983
+ initialValue: defaultValue,
15984
+ options: [
15985
+ { value: "claude", label: "Claude Code", hint: signals.claude ? "(detected)" : void 0 },
15986
+ { value: "cursor", label: "Cursor", hint: signals.cursor ? "(detected)" : void 0 },
15987
+ { value: "gemini", label: "Gemini CLI", hint: signals.gemini ? "(detected)" : "(beta \u2014 limited)" },
15988
+ { value: "vscode", label: "VS Code", hint: signals.vscode ? "(detected)" : "(via Continue / Copilot extensions)" },
15989
+ { value: "antigravity", label: "Antigravity", hint: "(experimental \u2014 installs to .antigravity/)" }
15990
+ ]
15991
+ });
15992
+ if (clack.isCancel(choice)) {
15993
+ clack.cancel("Install cancelled.");
15994
+ process.exit(0);
15989
15995
  }
15990
- console.log("");
15991
- const defaultKey = choices.find((c) => c.value === defaultValue).key;
15992
- const answer = (await prompt(` Pick an editor [${defaultKey}]: `)).trim().toLowerCase();
15993
- rl.close();
15994
- if (!answer) return defaultValue;
15995
- const byKey = choices.find((c) => c.key === answer);
15996
- if (byKey) return byKey.value;
15997
- const byName = choices.find((c) => c.value === answer || c.label.toLowerCase().startsWith(answer));
15998
- if (byName) return byName.value;
15999
- console.log(dim(` Unrecognised choice "${answer}" \u2014 falling back to ${defaultValue}.`));
16000
- return defaultValue;
15996
+ return choice;
16001
15997
  }
16002
15998
  async function resolveCommitPlanning(opts) {
16003
15999
  if (opts.commitPlanning !== null) return opts.commitPlanning;
16004
16000
  if (opts.yes || !process.stdin.isTTY) return true;
16005
- const readline = require("readline");
16006
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
16007
- const prompt = (q) => new Promise((r) => rl.question(q, (a) => r(a)));
16008
- console.log("");
16009
- console.log("\u{1F4CB} .planning/ holds PRDs, roadmaps, sprints, SUMMARY files.");
16010
- console.log(" Commit them to git, or keep them local?");
16011
- console.log("");
16012
- console.log(" [Y] Commit \u2014 collaborators see the same plans (default, recommended)");
16013
- console.log(" [n] Gitignore \u2014 planning stays local (good for sensitive PRDs)");
16014
- console.log("");
16015
- const answer = (await prompt(" Commit planning artifacts? [Y/n]: ")).trim().toLowerCase();
16016
- rl.close();
16017
- return !(answer === "n" || answer === "no");
16001
+ const choice = await clack.select({
16002
+ message: "\u{1F4CB} .planning/ holds PRDs, roadmaps, sprints, SUMMARY files. How should they be tracked?",
16003
+ initialValue: "commit",
16004
+ options: [
16005
+ { value: "commit", label: "Commit", hint: "collaborators see the same plans (recommended)" },
16006
+ { value: "gitignore", label: "Gitignore", hint: "planning stays local (good for sensitive PRDs)" }
16007
+ ]
16008
+ });
16009
+ if (clack.isCancel(choice)) {
16010
+ clack.cancel("Install cancelled.");
16011
+ process.exit(0);
16012
+ }
16013
+ return choice === "commit";
16018
16014
  }
16019
16015
  function printHelp2() {
16020
16016
  console.log(`
@@ -16791,6 +16787,7 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
16791
16787
  let preserved = 0;
16792
16788
  const preservedFiles = [];
16793
16789
  const preservedDiffs = [];
16790
+ const conflictedFiles = [];
16794
16791
  const spinner = createSpinner(dim(`Installing ${plan.length} files\u2026`), { color: "cyan" }).start();
16795
16792
  for (const entry of plan) {
16796
16793
  const destPath = path2.join(opts.target, entry.rel);
@@ -16825,9 +16822,13 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
16825
16822
  continue;
16826
16823
  }
16827
16824
  if (!opts.yes && !opts.nonDestructive) {
16828
- spinner.stop();
16829
- console.warn(" " + warn(`${entry.rel} differs from package version \u2014 use --force-overwrite to overwrite`));
16830
- spinner.start();
16825
+ conflictedFiles.push({
16826
+ rel: relForward,
16827
+ src: entry.src,
16828
+ destPath,
16829
+ existingContent: fs2.readFileSync(destPath, "utf8"),
16830
+ sourceContent: fs2.readFileSync(entry.src, "utf8")
16831
+ });
16831
16832
  skipped++;
16832
16833
  continue;
16833
16834
  }
@@ -16846,6 +16847,96 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
16846
16847
  copied++;
16847
16848
  }
16848
16849
  spinner.success({ text: ok(`${copied} files installed`) });
16850
+ if (conflictedFiles.length > 0) {
16851
+ const byCategory = { workflows: [], agents: [], commands: [], skills: [], references: [], other: [] };
16852
+ for (const c of conflictedFiles) {
16853
+ if (c.rel.includes("/workflows/")) byCategory.workflows.push(c);
16854
+ else if (c.rel.includes("/agents/")) byCategory.agents.push(c);
16855
+ else if (c.rel.includes("/commands/")) byCategory.commands.push(c);
16856
+ else if (c.rel.includes("/skills/")) byCategory.skills.push(c);
16857
+ else if (c.rel.includes("/references/")) byCategory.references.push(c);
16858
+ else byCategory.other.push(c);
16859
+ }
16860
+ console.log("");
16861
+ console.log(" " + warn(`${conflictedFiles.length} file${conflictedFiles.length === 1 ? "" : "s"} have local edits AND v${readPackageVersion()} updates:`));
16862
+ for (const [cat, list] of Object.entries(byCategory)) {
16863
+ if (list.length === 0) continue;
16864
+ console.log(" " + dim(`${list.length} ${cat}`));
16865
+ }
16866
+ console.log("");
16867
+ if (!opts.yes && process.stdin.isTTY) {
16868
+ const action = await clack.select({
16869
+ message: "How should we handle these?",
16870
+ initialValue: "review",
16871
+ options: [
16872
+ { value: "review", label: "Review each one", hint: "see the diff, decide per file" },
16873
+ { value: "upstream", label: "Take v" + readPackageVersion() + " for all", hint: "lose local edits, get all bug fixes" },
16874
+ { value: "keep", label: "Keep my local edits", hint: "skip v" + readPackageVersion() + " updates for these files (current behaviour)" }
16875
+ ]
16876
+ });
16877
+ if (clack.isCancel(action)) {
16878
+ clack.note("Skipped \u2014 local edits preserved.");
16879
+ } else if (action === "upstream") {
16880
+ let applied = 0;
16881
+ for (const c of conflictedFiles) {
16882
+ fs2.writeFileSync(c.destPath, c.sourceContent, "utf8");
16883
+ applied++;
16884
+ }
16885
+ console.log(" " + ok(`Applied v${readPackageVersion()} to ${applied} file${applied === 1 ? "" : "s"}.`));
16886
+ } else if (action === "review") {
16887
+ let applied = 0, kept = 0;
16888
+ for (const c of conflictedFiles) {
16889
+ const patch = createTwoFilesPatch(c.rel, c.rel, c.existingContent, c.sourceContent, "local", "v" + readPackageVersion());
16890
+ let ins = 0, del = 0;
16891
+ for (const line of patch.split("\n")) {
16892
+ if (line.startsWith("+") && !line.startsWith("+++")) ins++;
16893
+ if (line.startsWith("-") && !line.startsWith("---")) del++;
16894
+ }
16895
+ console.log("");
16896
+ console.log(" " + pc.bold(c.rel) + dim(" ") + pc.green(`+${ins}`) + " " + pc.red(`-${del}`));
16897
+ const decision = await clack.select({
16898
+ message: "Take upstream, keep local, or view diff?",
16899
+ initialValue: "view",
16900
+ options: [
16901
+ { value: "upstream", label: "Take v" + readPackageVersion() },
16902
+ { value: "keep", label: "Keep local" },
16903
+ { value: "view", label: "View diff first" }
16904
+ ]
16905
+ });
16906
+ let finalAction = decision;
16907
+ if (clack.isCancel(decision) || decision === "view") {
16908
+ for (const line of patch.split("\n").slice(4)) {
16909
+ if (line.startsWith("+")) process.stdout.write(pc.green(line) + "\n");
16910
+ else if (line.startsWith("-")) process.stdout.write(pc.red(line) + "\n");
16911
+ else if (line.startsWith("@")) process.stdout.write(pc.cyan(line) + "\n");
16912
+ else process.stdout.write(dim(line) + "\n");
16913
+ }
16914
+ const after = await clack.select({
16915
+ message: "Now: take upstream or keep local?",
16916
+ initialValue: "keep",
16917
+ options: [
16918
+ { value: "upstream", label: "Take v" + readPackageVersion() },
16919
+ { value: "keep", label: "Keep local" }
16920
+ ]
16921
+ });
16922
+ finalAction = clack.isCancel(after) ? "keep" : after;
16923
+ }
16924
+ if (finalAction === "upstream") {
16925
+ fs2.writeFileSync(c.destPath, c.sourceContent, "utf8");
16926
+ applied++;
16927
+ } else {
16928
+ kept++;
16929
+ }
16930
+ }
16931
+ console.log(" " + ok(`Review complete: ${applied} applied, ${kept} kept local.`));
16932
+ } else {
16933
+ console.log(" " + dim(`${conflictedFiles.length} file${conflictedFiles.length === 1 ? "" : "s"} kept local. Re-run with --force-overwrite or 'rcode update' anytime.`));
16934
+ }
16935
+ } else {
16936
+ console.log(" " + dim(`Re-run with --force-overwrite to apply v${readPackageVersion()} updates, or pipe through an interactive shell to resolve per-file.`));
16937
+ }
16938
+ console.log("");
16939
+ }
16849
16940
  const configDir = path2.join(opts.target, ".rihal", "_config");
16850
16941
  ensureDir(configDir);
16851
16942
  fs2.writeFileSync(path2.join(configDir, "manifest.yaml"), generateInstallManifest(opts));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "3.1.0",
4
- "description": "rcode — the memory bank for AI-driven SaaS teams. Persistent project context, distinctive engineering personas, and phase-based workflows. Built by Rihal. Works in Claude Code, Cursor, and Gemini.",
3
+ "version": "3.2.0",
4
+ "description": "rcode — the memory bank for AI-driven SaaS teams. Persistent project context, distinctive engineering personas, and phase-based workflows. Built by Rihal. Works in Claude Code, Cursor, Gemini, VS Code, and Antigravity.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {
7
7
  "rcode": "dist/rcode.js",