@hanzlaa/rcode 3.1.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/cli/install.js +205 -69
  2. package/dist/rcode.js +178 -64
  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() {
@@ -418,8 +415,33 @@ function getPathsForIde(ide, target) {
418
415
  referencesDir: path.join(target, '.rihal', 'references'),
419
416
  binDir: path.join(target, '.rihal', 'bin'),
420
417
  };
418
+ case 'vscode':
419
+ // VS Code's Claude Code / Continue / Copilot extensions all read from
420
+ // .claude/ (Claude Code's canonical paths). We install there directly
421
+ // and additionally write a .vscode/rihal/ marker so VS Code workspace
422
+ // settings can pin behaviour.
423
+ return {
424
+ agentsDir: path.join(target, '.claude', 'agents'),
425
+ commandsDir: path.join(target, '.claude', 'commands', 'rihal'),
426
+ workflowsDir: path.join(target, '.rihal', 'workflows'),
427
+ referencesDir: path.join(target, '.rihal', 'references'),
428
+ binDir: path.join(target, '.rihal', 'bin'),
429
+ markerDir: path.join(target, '.vscode', 'rihal'),
430
+ };
431
+ case 'antigravity':
432
+ // Antigravity (Google's agentic IDE) — install to .antigravity/ mirroring
433
+ // the .gemini/ structure. Antigravity's plugin protocol is still firming
434
+ // up; the user can adjust paths via .rihal/config.yaml's `extra_install_paths`
435
+ // if Antigravity expects different routing.
436
+ return {
437
+ agentsDir: path.join(target, '.antigravity', 'rihal', 'agents'),
438
+ commandsDir: path.join(target, '.antigravity', 'rihal', 'commands'),
439
+ workflowsDir: path.join(target, '.rihal', 'workflows'),
440
+ referencesDir: path.join(target, '.rihal', 'references'),
441
+ binDir: path.join(target, '.rihal', 'bin'),
442
+ };
421
443
  default:
422
- throw new Error(`Unknown IDE: ${ide}. Supported: claude, cursor, gemini`);
444
+ throw new Error(`Unknown IDE: ${ide}. Supported: claude, cursor, gemini, vscode, antigravity`);
423
445
  }
424
446
  }
425
447
 
@@ -1234,27 +1256,28 @@ async function install(opts) {
1234
1256
  }
1235
1257
 
1236
1258
  // Validate IDE — structured error for unsupported editors (#197).
1237
- if (!['claude', 'cursor', 'gemini'].includes(opts.ide)) {
1259
+ if (!['claude', 'cursor', 'gemini', 'vscode', 'antigravity'].includes(opts.ide)) {
1238
1260
  console.error(`✖ --ide ${opts.ide} is not supported in v${readPackageVersion()}.`);
1239
1261
  console.error('');
1240
1262
  console.error(' Currently supported:');
1241
- console.error(' claude — Claude Code native (recommended)');
1242
- console.error(' cursor — Cursor IDE');
1243
- console.error(' gemini — Gemini CLI');
1263
+ console.error(' claude — Claude Code native (recommended)');
1264
+ console.error(' cursor — Cursor IDE');
1265
+ console.error(' gemini — Gemini CLI');
1266
+ console.error(' vscode — VS Code (with Claude Code / Continue / Copilot extension)');
1267
+ console.error(' antigravity — Antigravity (experimental)');
1244
1268
  console.error('');
1245
- console.error(' Tracked for v3.0 (see issue #182):');
1246
- console.error(' vscodeVS Code native extension');
1247
- console.error(' jetbrains IntelliJ / PyCharm');
1248
- console.error(' zed — Zed editor');
1269
+ console.error(' Tracked for future:');
1270
+ console.error(' jetbrainsIntelliJ / PyCharm');
1271
+ console.error(' zed Zed editor');
1249
1272
  console.error('');
1250
- if (/^(vscode|vs-code|code)$/i.test(opts.ide)) {
1251
- console.error(' Workaround: if you use VS Code WITH the Claude Code extension,');
1252
- console.error(' run `--ide claude` — the extension reads from .claude/ too.');
1253
- console.error('');
1254
- }
1255
1273
  return 1;
1256
1274
  }
1257
1275
 
1276
+ // VS Code installs to .claude/ paths (extension reads from there). Inform the user.
1277
+ if (opts.ide === 'vscode') {
1278
+ console.log(' ' + dim('VS Code → installing to .claude/ paths (read by Claude Code / Continue / Copilot extensions).'));
1279
+ }
1280
+
1258
1281
  // Gemini IDE support deferred
1259
1282
  if (opts.ide === 'gemini') {
1260
1283
  console.log(`\n⚠️ Gemini CLI install not yet implemented\n`);
@@ -1264,6 +1287,12 @@ async function install(opts) {
1264
1287
  return 1;
1265
1288
  }
1266
1289
 
1290
+ // Antigravity install is experimental — best-effort path, user may need to adjust
1291
+ if (opts.ide === 'antigravity') {
1292
+ console.log(' ' + warn('Antigravity install is experimental. Files land at .antigravity/rihal/{agents,commands}/.'));
1293
+ console.log(' ' + dim('If Antigravity expects a different path, adjust .rihal/config.yaml and re-run.'));
1294
+ }
1295
+
1267
1296
  // Validate requested modules exist
1268
1297
  if (opts.modules.length > 0) {
1269
1298
  const available = listAvailableModules();
@@ -1342,6 +1371,7 @@ async function install(opts) {
1342
1371
  let preserved = 0;
1343
1372
  const preservedFiles = [];
1344
1373
  const preservedDiffs = []; // { rel, insertions, deletions, patch } for #251
1374
+ const conflictedFiles = []; // { rel, src, destPath, existingContent, sourceContent } for #451 / #453
1345
1375
  const spinner = createSpinner(dim(`Installing ${plan.length} files…`), { color: 'cyan' }).start();
1346
1376
 
1347
1377
  for (const entry of plan) {
@@ -1381,9 +1411,15 @@ async function install(opts) {
1381
1411
  const sourceHash = sha256(fs.readFileSync(entry.src));
1382
1412
  if (existingHash === sourceHash) { skipped++; continue; }
1383
1413
  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();
1414
+ // Buffer the conflict instead of spamming a warning per file (#451).
1415
+ // Surfaced as a categorised summary post-install + interactive offer (#453).
1416
+ conflictedFiles.push({
1417
+ rel: relForward,
1418
+ src: entry.src,
1419
+ destPath,
1420
+ existingContent: fs.readFileSync(destPath, 'utf8'),
1421
+ sourceContent: fs.readFileSync(entry.src, 'utf8'),
1422
+ });
1387
1423
  skipped++;
1388
1424
  continue;
1389
1425
  }
@@ -1406,6 +1442,100 @@ async function install(opts) {
1406
1442
 
1407
1443
  spinner.success({ text: ok(`${copied} files installed`) });
1408
1444
 
1445
+ // Categorised conflict summary (#451) + interactive resolution offer (#453).
1446
+ // Replaces the per-file 'differs from package version' warning spam.
1447
+ if (conflictedFiles.length > 0) {
1448
+ const byCategory = { workflows: [], agents: [], commands: [], skills: [], references: [], other: [] };
1449
+ for (const c of conflictedFiles) {
1450
+ if (c.rel.includes('/workflows/')) byCategory.workflows.push(c);
1451
+ else if (c.rel.includes('/agents/')) byCategory.agents.push(c);
1452
+ else if (c.rel.includes('/commands/')) byCategory.commands.push(c);
1453
+ else if (c.rel.includes('/skills/')) byCategory.skills.push(c);
1454
+ else if (c.rel.includes('/references/')) byCategory.references.push(c);
1455
+ else byCategory.other.push(c);
1456
+ }
1457
+ console.log('');
1458
+ console.log(' ' + warn(`${conflictedFiles.length} file${conflictedFiles.length === 1 ? '' : 's'} have local edits AND v${readPackageVersion()} updates:`));
1459
+ for (const [cat, list] of Object.entries(byCategory)) {
1460
+ if (list.length === 0) continue;
1461
+ console.log(' ' + dim(`${list.length} ${cat}`));
1462
+ }
1463
+ console.log('');
1464
+
1465
+ if (!opts.yes && process.stdin.isTTY) {
1466
+ const action = await clack.select({
1467
+ message: 'How should we handle these?',
1468
+ initialValue: 'review',
1469
+ options: [
1470
+ { value: 'review', label: 'Review each one', hint: 'see the diff, decide per file' },
1471
+ { value: 'upstream', label: 'Take v' + readPackageVersion() + ' for all', hint: 'lose local edits, get all bug fixes' },
1472
+ { value: 'keep', label: 'Keep my local edits', hint: 'skip v' + readPackageVersion() + ' updates for these files (current behaviour)' },
1473
+ ],
1474
+ });
1475
+ if (clack.isCancel(action)) {
1476
+ clack.note('Skipped — local edits preserved.');
1477
+ } else if (action === 'upstream') {
1478
+ let applied = 0;
1479
+ for (const c of conflictedFiles) {
1480
+ fs.writeFileSync(c.destPath, c.sourceContent, 'utf8');
1481
+ applied++;
1482
+ }
1483
+ console.log(' ' + ok(`Applied v${readPackageVersion()} to ${applied} file${applied === 1 ? '' : 's'}.`));
1484
+ } else if (action === 'review') {
1485
+ let applied = 0, kept = 0;
1486
+ for (const c of conflictedFiles) {
1487
+ const patch = createTwoFilesPatch(c.rel, c.rel, c.existingContent, c.sourceContent, 'local', 'v' + readPackageVersion());
1488
+ let ins = 0, del = 0;
1489
+ for (const line of patch.split('\n')) {
1490
+ if (line.startsWith('+') && !line.startsWith('+++')) ins++;
1491
+ if (line.startsWith('-') && !line.startsWith('---')) del++;
1492
+ }
1493
+ console.log('');
1494
+ console.log(' ' + pc.bold(c.rel) + dim(' ') + pc.green(`+${ins}`) + ' ' + pc.red(`-${del}`));
1495
+ const decision = await clack.select({
1496
+ message: 'Take upstream, keep local, or view diff?',
1497
+ initialValue: 'view',
1498
+ options: [
1499
+ { value: 'upstream', label: 'Take v' + readPackageVersion() },
1500
+ { value: 'keep', label: 'Keep local' },
1501
+ { value: 'view', label: 'View diff first' },
1502
+ ],
1503
+ });
1504
+ let finalAction = decision;
1505
+ if (clack.isCancel(decision) || decision === 'view') {
1506
+ for (const line of patch.split('\n').slice(4)) {
1507
+ if (line.startsWith('+')) process.stdout.write(pc.green(line) + '\n');
1508
+ else if (line.startsWith('-')) process.stdout.write(pc.red(line) + '\n');
1509
+ else if (line.startsWith('@')) process.stdout.write(pc.cyan(line) + '\n');
1510
+ else process.stdout.write(dim(line) + '\n');
1511
+ }
1512
+ const after = await clack.select({
1513
+ message: 'Now: take upstream or keep local?',
1514
+ initialValue: 'keep',
1515
+ options: [
1516
+ { value: 'upstream', label: 'Take v' + readPackageVersion() },
1517
+ { value: 'keep', label: 'Keep local' },
1518
+ ],
1519
+ });
1520
+ finalAction = clack.isCancel(after) ? 'keep' : after;
1521
+ }
1522
+ if (finalAction === 'upstream') {
1523
+ fs.writeFileSync(c.destPath, c.sourceContent, 'utf8');
1524
+ applied++;
1525
+ } else {
1526
+ kept++;
1527
+ }
1528
+ }
1529
+ console.log(' ' + ok(`Review complete: ${applied} applied, ${kept} kept local.`));
1530
+ } else {
1531
+ console.log(' ' + dim(`${conflictedFiles.length} file${conflictedFiles.length === 1 ? '' : 's'} kept local. Re-run with --force-overwrite or 'rcode update' anytime.`));
1532
+ }
1533
+ } else {
1534
+ console.log(' ' + dim(`Re-run with --force-overwrite to apply v${readPackageVersion()} updates, or pipe through an interactive shell to resolve per-file.`));
1535
+ }
1536
+ console.log('');
1537
+ }
1538
+
1409
1539
  // Write .rihal/_config/manifest.yaml + agent-manifest.csv + files-manifest.csv
1410
1540
  const configDir = path.join(opts.target, '.rihal', '_config');
1411
1541
  ensureDir(configDir);
@@ -1569,16 +1699,19 @@ async function install(opts) {
1569
1699
  console.log('');
1570
1700
  }
1571
1701
 
1572
- // Count installed agents + commands dynamically (#190).
1573
- const agentsDir = path.join(opts.target, '.claude', 'agents');
1574
- const commandsDir = path.join(opts.target, '.claude', 'commands', 'rihal');
1702
+ // Count installed agents + commands dynamically (#190). Reads from the
1703
+ // IDE-specific install paths so cursor/gemini/vscode/antigravity don't
1704
+ // false-fail the health check.
1705
+ const idePaths = getPathsForIde(opts.ide, opts.target);
1706
+ const agentsDir = idePaths.agentsDir;
1707
+ const commandsDir = idePaths.commandsDir;
1575
1708
  let agentCount = 0, commandCount = 0;
1576
1709
  try {
1577
1710
  if (fs.existsSync(agentsDir)) {
1578
- agentCount = fs.readdirSync(agentsDir).filter(f => f.startsWith('rihal-') && f.endsWith('.md')).length;
1711
+ agentCount = fs.readdirSync(agentsDir).filter(f => (f.startsWith('rihal-') || f.startsWith('rcode-')) && (f.endsWith('.md') || f.endsWith('.mdc'))).length;
1579
1712
  }
1580
1713
  if (fs.existsSync(commandsDir)) {
1581
- commandCount = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md')).length;
1714
+ commandCount = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md') || f.endsWith('.mdc')).length;
1582
1715
  }
1583
1716
  } catch {}
1584
1717
 
@@ -1590,9 +1723,12 @@ async function install(opts) {
1590
1723
  console.log(` ${bold('Mode:')} ${opts.mode} ${dim('(guided=confirm at gates, yolo=autonomous)')}`);
1591
1724
  console.log(` ${bold('Planning:')} ${opts.commitPlanning !== false ? 'committed' : 'gitignored'} ${dim('(flip: rihal-tools gitignore refresh)')}`);
1592
1725
  console.log('');
1593
- console.log(` ${bold('Agents:')} ${pc.green(String(agentCount))} in .claude/agents/`);
1594
- console.log(` ${bold('Commands:')} ${pc.green(String(commandCount))} slash commands in .claude/commands/rihal/`);
1595
- if (skillsInstalled > 0) console.log(` ${bold('Skills:')} ${pc.green(String(skillsInstalled))} phrase-activated in .claude/skills/`);
1726
+ // Show the actual install paths so cursor/gemini/antigravity output is accurate
1727
+ const relAgents = path.relative(opts.target, idePaths.agentsDir) || idePaths.agentsDir;
1728
+ const relCommands = path.relative(opts.target, idePaths.commandsDir) || idePaths.commandsDir;
1729
+ console.log(` ${bold('Agents:')} ${pc.green(String(agentCount))} in ${relAgents}/`);
1730
+ console.log(` ${bold('Commands:')} ${pc.green(String(commandCount))} slash commands in ${relCommands}/`);
1731
+ if (skillsInstalled > 0) console.log(` ${bold('Skills:')} ${pc.green(String(skillsInstalled))} phrase-activated`);
1596
1732
  console.log('');
1597
1733
  if (starterSeeded) {
1598
1734
  console.log(' ' + ok('Starter planning scaffolded in .planning/ (ROADMAP, STATE, PROJECT)'));
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(`
@@ -16069,8 +16065,25 @@ Installs (IDE-specific):
16069
16065
  referencesDir: path2.join(target, ".rihal", "references"),
16070
16066
  binDir: path2.join(target, ".rihal", "bin")
16071
16067
  };
16068
+ case "vscode":
16069
+ return {
16070
+ agentsDir: path2.join(target, ".claude", "agents"),
16071
+ commandsDir: path2.join(target, ".claude", "commands", "rihal"),
16072
+ workflowsDir: path2.join(target, ".rihal", "workflows"),
16073
+ referencesDir: path2.join(target, ".rihal", "references"),
16074
+ binDir: path2.join(target, ".rihal", "bin"),
16075
+ markerDir: path2.join(target, ".vscode", "rihal")
16076
+ };
16077
+ case "antigravity":
16078
+ return {
16079
+ agentsDir: path2.join(target, ".antigravity", "rihal", "agents"),
16080
+ commandsDir: path2.join(target, ".antigravity", "rihal", "commands"),
16081
+ workflowsDir: path2.join(target, ".rihal", "workflows"),
16082
+ referencesDir: path2.join(target, ".rihal", "references"),
16083
+ binDir: path2.join(target, ".rihal", "bin")
16084
+ };
16072
16085
  default:
16073
- throw new Error(`Unknown IDE: ${ide}. Supported: claude, cursor, gemini`);
16086
+ throw new Error(`Unknown IDE: ${ide}. Supported: claude, cursor, gemini, vscode, antigravity`);
16074
16087
  }
16075
16088
  }
16076
16089
  function walkFiles(dir, extraIgnore = []) {
@@ -16704,26 +16717,25 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
16704
16717
  console.error(`\u2716 Source tree not found at ${SOURCE_ROOT}. Running from wrong dir?`);
16705
16718
  return 1;
16706
16719
  }
16707
- if (!["claude", "cursor", "gemini"].includes(opts.ide)) {
16720
+ if (!["claude", "cursor", "gemini", "vscode", "antigravity"].includes(opts.ide)) {
16708
16721
  console.error(`\u2716 --ide ${opts.ide} is not supported in v${readPackageVersion()}.`);
16709
16722
  console.error("");
16710
16723
  console.error(" Currently supported:");
16711
- console.error(" claude \u2014 Claude Code native (recommended)");
16712
- console.error(" cursor \u2014 Cursor IDE");
16713
- console.error(" gemini \u2014 Gemini CLI");
16724
+ console.error(" claude \u2014 Claude Code native (recommended)");
16725
+ console.error(" cursor \u2014 Cursor IDE");
16726
+ console.error(" gemini \u2014 Gemini CLI");
16727
+ console.error(" vscode \u2014 VS Code (with Claude Code / Continue / Copilot extension)");
16728
+ console.error(" antigravity \u2014 Antigravity (experimental)");
16714
16729
  console.error("");
16715
- console.error(" Tracked for v3.0 (see issue #182):");
16716
- console.error(" vscode \u2014 VS Code native extension");
16717
- console.error(" jetbrains \u2014 IntelliJ / PyCharm");
16718
- console.error(" zed \u2014 Zed editor");
16730
+ console.error(" Tracked for future:");
16731
+ console.error(" jetbrains \u2014 IntelliJ / PyCharm");
16732
+ console.error(" zed \u2014 Zed editor");
16719
16733
  console.error("");
16720
- if (/^(vscode|vs-code|code)$/i.test(opts.ide)) {
16721
- console.error(" Workaround: if you use VS Code WITH the Claude Code extension,");
16722
- console.error(" run `--ide claude` \u2014 the extension reads from .claude/ too.");
16723
- console.error("");
16724
- }
16725
16734
  return 1;
16726
16735
  }
16736
+ if (opts.ide === "vscode") {
16737
+ console.log(" " + dim("VS Code \u2192 installing to .claude/ paths (read by Claude Code / Continue / Copilot extensions)."));
16738
+ }
16727
16739
  if (opts.ide === "gemini") {
16728
16740
  console.log(`
16729
16741
  \u26A0\uFE0F Gemini CLI install not yet implemented
@@ -16735,6 +16747,10 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
16735
16747
  `);
16736
16748
  return 1;
16737
16749
  }
16750
+ if (opts.ide === "antigravity") {
16751
+ console.log(" " + warn("Antigravity install is experimental. Files land at .antigravity/rihal/{agents,commands}/."));
16752
+ console.log(" " + dim("If Antigravity expects a different path, adjust .rihal/config.yaml and re-run."));
16753
+ }
16738
16754
  if (opts.modules.length > 0) {
16739
16755
  const available = listAvailableModules();
16740
16756
  const unknownModules = opts.modules.filter((m) => !available.includes(m));
@@ -16791,6 +16807,7 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
16791
16807
  let preserved = 0;
16792
16808
  const preservedFiles = [];
16793
16809
  const preservedDiffs = [];
16810
+ const conflictedFiles = [];
16794
16811
  const spinner = createSpinner(dim(`Installing ${plan.length} files\u2026`), { color: "cyan" }).start();
16795
16812
  for (const entry of plan) {
16796
16813
  const destPath = path2.join(opts.target, entry.rel);
@@ -16825,9 +16842,13 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
16825
16842
  continue;
16826
16843
  }
16827
16844
  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();
16845
+ conflictedFiles.push({
16846
+ rel: relForward,
16847
+ src: entry.src,
16848
+ destPath,
16849
+ existingContent: fs2.readFileSync(destPath, "utf8"),
16850
+ sourceContent: fs2.readFileSync(entry.src, "utf8")
16851
+ });
16831
16852
  skipped++;
16832
16853
  continue;
16833
16854
  }
@@ -16846,6 +16867,96 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
16846
16867
  copied++;
16847
16868
  }
16848
16869
  spinner.success({ text: ok(`${copied} files installed`) });
16870
+ if (conflictedFiles.length > 0) {
16871
+ const byCategory = { workflows: [], agents: [], commands: [], skills: [], references: [], other: [] };
16872
+ for (const c of conflictedFiles) {
16873
+ if (c.rel.includes("/workflows/")) byCategory.workflows.push(c);
16874
+ else if (c.rel.includes("/agents/")) byCategory.agents.push(c);
16875
+ else if (c.rel.includes("/commands/")) byCategory.commands.push(c);
16876
+ else if (c.rel.includes("/skills/")) byCategory.skills.push(c);
16877
+ else if (c.rel.includes("/references/")) byCategory.references.push(c);
16878
+ else byCategory.other.push(c);
16879
+ }
16880
+ console.log("");
16881
+ console.log(" " + warn(`${conflictedFiles.length} file${conflictedFiles.length === 1 ? "" : "s"} have local edits AND v${readPackageVersion()} updates:`));
16882
+ for (const [cat, list] of Object.entries(byCategory)) {
16883
+ if (list.length === 0) continue;
16884
+ console.log(" " + dim(`${list.length} ${cat}`));
16885
+ }
16886
+ console.log("");
16887
+ if (!opts.yes && process.stdin.isTTY) {
16888
+ const action = await clack.select({
16889
+ message: "How should we handle these?",
16890
+ initialValue: "review",
16891
+ options: [
16892
+ { value: "review", label: "Review each one", hint: "see the diff, decide per file" },
16893
+ { value: "upstream", label: "Take v" + readPackageVersion() + " for all", hint: "lose local edits, get all bug fixes" },
16894
+ { value: "keep", label: "Keep my local edits", hint: "skip v" + readPackageVersion() + " updates for these files (current behaviour)" }
16895
+ ]
16896
+ });
16897
+ if (clack.isCancel(action)) {
16898
+ clack.note("Skipped \u2014 local edits preserved.");
16899
+ } else if (action === "upstream") {
16900
+ let applied = 0;
16901
+ for (const c of conflictedFiles) {
16902
+ fs2.writeFileSync(c.destPath, c.sourceContent, "utf8");
16903
+ applied++;
16904
+ }
16905
+ console.log(" " + ok(`Applied v${readPackageVersion()} to ${applied} file${applied === 1 ? "" : "s"}.`));
16906
+ } else if (action === "review") {
16907
+ let applied = 0, kept = 0;
16908
+ for (const c of conflictedFiles) {
16909
+ const patch = createTwoFilesPatch(c.rel, c.rel, c.existingContent, c.sourceContent, "local", "v" + readPackageVersion());
16910
+ let ins = 0, del = 0;
16911
+ for (const line of patch.split("\n")) {
16912
+ if (line.startsWith("+") && !line.startsWith("+++")) ins++;
16913
+ if (line.startsWith("-") && !line.startsWith("---")) del++;
16914
+ }
16915
+ console.log("");
16916
+ console.log(" " + pc.bold(c.rel) + dim(" ") + pc.green(`+${ins}`) + " " + pc.red(`-${del}`));
16917
+ const decision = await clack.select({
16918
+ message: "Take upstream, keep local, or view diff?",
16919
+ initialValue: "view",
16920
+ options: [
16921
+ { value: "upstream", label: "Take v" + readPackageVersion() },
16922
+ { value: "keep", label: "Keep local" },
16923
+ { value: "view", label: "View diff first" }
16924
+ ]
16925
+ });
16926
+ let finalAction = decision;
16927
+ if (clack.isCancel(decision) || decision === "view") {
16928
+ for (const line of patch.split("\n").slice(4)) {
16929
+ if (line.startsWith("+")) process.stdout.write(pc.green(line) + "\n");
16930
+ else if (line.startsWith("-")) process.stdout.write(pc.red(line) + "\n");
16931
+ else if (line.startsWith("@")) process.stdout.write(pc.cyan(line) + "\n");
16932
+ else process.stdout.write(dim(line) + "\n");
16933
+ }
16934
+ const after = await clack.select({
16935
+ message: "Now: take upstream or keep local?",
16936
+ initialValue: "keep",
16937
+ options: [
16938
+ { value: "upstream", label: "Take v" + readPackageVersion() },
16939
+ { value: "keep", label: "Keep local" }
16940
+ ]
16941
+ });
16942
+ finalAction = clack.isCancel(after) ? "keep" : after;
16943
+ }
16944
+ if (finalAction === "upstream") {
16945
+ fs2.writeFileSync(c.destPath, c.sourceContent, "utf8");
16946
+ applied++;
16947
+ } else {
16948
+ kept++;
16949
+ }
16950
+ }
16951
+ console.log(" " + ok(`Review complete: ${applied} applied, ${kept} kept local.`));
16952
+ } else {
16953
+ console.log(" " + dim(`${conflictedFiles.length} file${conflictedFiles.length === 1 ? "" : "s"} kept local. Re-run with --force-overwrite or 'rcode update' anytime.`));
16954
+ }
16955
+ } else {
16956
+ console.log(" " + dim(`Re-run with --force-overwrite to apply v${readPackageVersion()} updates, or pipe through an interactive shell to resolve per-file.`));
16957
+ }
16958
+ console.log("");
16959
+ }
16849
16960
  const configDir = path2.join(opts.target, ".rihal", "_config");
16850
16961
  ensureDir(configDir);
16851
16962
  fs2.writeFileSync(path2.join(configDir, "manifest.yaml"), generateInstallManifest(opts));
@@ -16968,15 +17079,16 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
16968
17079
  console.log(dim(" To overwrite: re-run with --force-overwrite | To see full diffs: --show-diff"));
16969
17080
  console.log("");
16970
17081
  }
16971
- const agentsDir = path2.join(opts.target, ".claude", "agents");
16972
- const commandsDir = path2.join(opts.target, ".claude", "commands", "rihal");
17082
+ const idePaths = getPathsForIde(opts.ide, opts.target);
17083
+ const agentsDir = idePaths.agentsDir;
17084
+ const commandsDir = idePaths.commandsDir;
16973
17085
  let agentCount = 0, commandCount = 0;
16974
17086
  try {
16975
17087
  if (fs2.existsSync(agentsDir)) {
16976
- agentCount = fs2.readdirSync(agentsDir).filter((f) => f.startsWith("rihal-") && f.endsWith(".md")).length;
17088
+ agentCount = fs2.readdirSync(agentsDir).filter((f) => (f.startsWith("rihal-") || f.startsWith("rcode-")) && (f.endsWith(".md") || f.endsWith(".mdc"))).length;
16977
17089
  }
16978
17090
  if (fs2.existsSync(commandsDir)) {
16979
- commandCount = fs2.readdirSync(commandsDir).filter((f) => f.endsWith(".md")).length;
17091
+ commandCount = fs2.readdirSync(commandsDir).filter((f) => f.endsWith(".md") || f.endsWith(".mdc")).length;
16980
17092
  }
16981
17093
  } catch {
16982
17094
  }
@@ -16988,9 +17100,11 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
16988
17100
  console.log(` ${bold("Mode:")} ${opts.mode} ${dim("(guided=confirm at gates, yolo=autonomous)")}`);
16989
17101
  console.log(` ${bold("Planning:")} ${opts.commitPlanning !== false ? "committed" : "gitignored"} ${dim("(flip: rihal-tools gitignore refresh)")}`);
16990
17102
  console.log("");
16991
- console.log(` ${bold("Agents:")} ${pc.green(String(agentCount))} in .claude/agents/`);
16992
- console.log(` ${bold("Commands:")} ${pc.green(String(commandCount))} slash commands in .claude/commands/rihal/`);
16993
- if (skillsInstalled > 0) console.log(` ${bold("Skills:")} ${pc.green(String(skillsInstalled))} phrase-activated in .claude/skills/`);
17103
+ const relAgents = path2.relative(opts.target, idePaths.agentsDir) || idePaths.agentsDir;
17104
+ const relCommands = path2.relative(opts.target, idePaths.commandsDir) || idePaths.commandsDir;
17105
+ console.log(` ${bold("Agents:")} ${pc.green(String(agentCount))} in ${relAgents}/`);
17106
+ console.log(` ${bold("Commands:")} ${pc.green(String(commandCount))} slash commands in ${relCommands}/`);
17107
+ if (skillsInstalled > 0) console.log(` ${bold("Skills:")} ${pc.green(String(skillsInstalled))} phrase-activated`);
16994
17108
  console.log("");
16995
17109
  if (starterSeeded) {
16996
17110
  console.log(" " + ok("Starter planning scaffolded in .planning/ (ROADMAP, STATE, PROJECT)"));
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.1",
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",