@hanzlaa/rcode 4.1.2 → 4.3.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.
- package/cli/install.js +176 -13
- package/cli/lib/config.cjs +4 -2
- package/cli/lib/fsutil.cjs +13 -2
- package/cli/lib/homedir.cjs +21 -0
- package/cli/lib/schemas.cjs +6 -1
- package/cli/nuke.js +13 -8
- package/cli/postinstall.js +14 -4
- package/cli/rcode-slash-router.cjs +118 -0
- package/cli/uninstall.js +59 -1
- package/cli/update.js +10 -5
- package/dist/rcode.js +234 -230
- package/package.json +1 -1
- package/server/dashboard.js +26 -7
- package/server/lib/api.js +62 -4
- package/server/lib/html/client/agents-data.js +22 -18
- package/server/lib/html/client/app.js +3 -0
- package/server/lib/html/client/components/AgentCard.js +127 -0
- package/server/lib/html/client/components/App.js +104 -39
- package/server/lib/html/client/components/CommandPalette.js +133 -0
- package/server/lib/html/client/components/FileReader.js +116 -0
- package/server/lib/html/client/components/FilterChips.js +94 -0
- package/server/lib/html/client/components/NotifyCenter.js +117 -0
- package/server/lib/html/client/components/OrchPanel.js +80 -52
- package/server/lib/html/client/components/PhaseGraph.js +300 -0
- package/server/lib/html/client/components/RejectDialog.js +78 -0
- package/server/lib/html/client/components/RunnerPicker.js +190 -0
- package/server/lib/html/client/components/Sidebar.js +106 -61
- package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
- package/server/lib/html/client/components/TaskPipeline.js +83 -0
- package/server/lib/html/client/components/Topbar.js +86 -39
- package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
- package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
- package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
- package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
- package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
- package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
- package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
- package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
- package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
- package/server/lib/html/client/components/shared.js +47 -11
- package/server/lib/html/client/filter-state.js +72 -0
- package/server/lib/html/client/icons-client.js +7 -0
- package/server/lib/html/client/notify.js +75 -0
- package/server/lib/html/client/orchestrator.js +168 -41
- package/server/lib/html/client/preact.js +13 -8
- package/server/lib/html/client/store.js +70 -6
- package/server/lib/html/client/util.js +78 -0
- package/server/lib/html/client/vendor/htm.js +1 -0
- package/server/lib/html/client/vendor/preact-hooks.js +2 -0
- package/server/lib/html/client/vendor/preact.js +2 -0
- package/server/lib/html/client/views/AgentsView.js +144 -51
- package/server/lib/html/client/views/FilesView.js +20 -103
- package/server/lib/html/client/views/KanbanView.js +40 -21
- package/server/lib/html/client/views/MemoryView.js +26 -9
- package/server/lib/html/client/views/MilestonesView.js +4 -4
- package/server/lib/html/client/views/OrchestrationView.js +154 -19
- package/server/lib/html/client/views/OverviewView.js +47 -239
- package/server/lib/html/client/views/PhasesView.js +50 -6
- package/server/lib/html/client/views/RoadmapView.js +6 -3
- package/server/lib/html/client/views/SprintsView.js +50 -6
- package/server/lib/html/client/views/TasksView.js +4 -3
- package/server/lib/html/client.js +21 -4
- package/server/lib/html/css.js +2761 -8
- package/server/lib/html/icons.js +7 -0
- package/server/lib/html/shell.js +10 -3
- package/server/lib/scanner.js +376 -39
- package/server/orchestrator.js +329 -5
package/cli/install.js
CHANGED
|
@@ -59,6 +59,11 @@ const os = require('os');
|
|
|
59
59
|
// Ctrl+C mid-write and malicious symlink-traversal during dedup/cleanup.
|
|
60
60
|
const { writeFileAtomic, safeRmSync } = require('./lib/fsutil.cjs');
|
|
61
61
|
|
|
62
|
+
// HOME-aware home resolution (#889) — os.homedir() ignores a stubbed HOME on
|
|
63
|
+
// Windows (it reads USERPROFILE), so HOME-isolated tests and CI leaked global
|
|
64
|
+
// installs (~/.rcode, ~/.codex, ~/.gemini) into the real profile dir there.
|
|
65
|
+
const { homedir } = require('./lib/homedir.cjs');
|
|
66
|
+
|
|
62
67
|
// Bundled packages — devDeps inlined by esbuild, loaded from node_modules in dev.
|
|
63
68
|
const pc = require('picocolors');
|
|
64
69
|
const { createSpinner } = require('nanospinner');
|
|
@@ -89,7 +94,7 @@ const SOURCE_ROOT = path.join(PACKAGE_ROOT, 'rcode');
|
|
|
89
94
|
* detectIdeSignals, plus a row to runInstallWizard's multiselect — three
|
|
90
95
|
* sites instead of ten.
|
|
91
96
|
*/
|
|
92
|
-
const SUPPORTED_IDES = Object.freeze(['claude', 'cursor', 'gemini', 'vscode', 'antigravity', 'windsurf', 'codex']);
|
|
97
|
+
const SUPPORTED_IDES = Object.freeze(['claude', 'cursor', 'gemini', 'vscode', 'antigravity', 'windsurf', 'codex', 'grok']);
|
|
93
98
|
|
|
94
99
|
/**
|
|
95
100
|
* Resolve the stable on-disk location of this package so config.yaml
|
|
@@ -247,7 +252,7 @@ function parseArgs(argv) {
|
|
|
247
252
|
// project directory wrote rcode artifacts to that project, not to the user's
|
|
248
253
|
// home where Claude Code reads global commands from.
|
|
249
254
|
if (opts.global && !opts.targetProvided) {
|
|
250
|
-
opts.target =
|
|
255
|
+
opts.target = homedir();
|
|
251
256
|
}
|
|
252
257
|
// Issue #821/#832: pnpm workspace anchor.
|
|
253
258
|
// When `pnpm add -D @hanzlaa/rcode` runs inside a workspace member,
|
|
@@ -386,7 +391,7 @@ function detectIdeSignals(target) {
|
|
|
386
391
|
if (fs.existsSync(path.join(target, '.antigravity'))) signals.antigravity = true;
|
|
387
392
|
if (fs.existsSync(path.join(target, '.windsurf'))) signals.windsurf = true;
|
|
388
393
|
// 2. User-level config dirs
|
|
389
|
-
const home =
|
|
394
|
+
const home = homedir();
|
|
390
395
|
if (fs.existsSync(path.join(home, '.claude'))) signals.claude = true;
|
|
391
396
|
if (fs.existsSync(path.join(home, '.cursor'))) signals.cursor = true;
|
|
392
397
|
if (fs.existsSync(path.join(home, '.config', 'Cursor'))) signals.cursor = true;
|
|
@@ -615,6 +620,21 @@ function getPathsForIde(ide, target) {
|
|
|
615
620
|
// claude/vscode install paths). We install agent + command files to .claude/
|
|
616
621
|
// so multi-IDE installs share files, and the rcode workflow bridge gives
|
|
617
622
|
// Codex access to lifecycle workflows via `rcode workflow show <name>` (#883).
|
|
623
|
+
// NOTE: Codex surfaces native /slash commands ONLY from ~/.codex/prompts/*.md
|
|
624
|
+
// (home, startup-loaded) — installed separately by installNativeHomeSlashCommands()
|
|
625
|
+
// under the opt-in --global flag, since .claude/commands is invisible to Codex.
|
|
626
|
+
return {
|
|
627
|
+
agentsDir: path.join(target, '.claude', 'agents'),
|
|
628
|
+
commandsDir: path.join(target, '.claude', 'commands'),
|
|
629
|
+
workflowsDir: path.join(target, '.rcode', 'workflows'),
|
|
630
|
+
referencesDir: path.join(target, '.rcode', 'references'),
|
|
631
|
+
binDir: path.join(target, '.rcode', 'bin'),
|
|
632
|
+
};
|
|
633
|
+
case 'grok':
|
|
634
|
+
// Grok Build (xAI CLI) is Claude-Code-compatible: it reads slash commands
|
|
635
|
+
// from .claude/commands/*.md (project) and ~/.claude/commands (global), same
|
|
636
|
+
// as Claude Code. So grok maps to the identical .claude/ layout — verified
|
|
637
|
+
// live: `/rcode-add-phase` surfaces in grok from these dirs.
|
|
618
638
|
return {
|
|
619
639
|
agentsDir: path.join(target, '.claude', 'agents'),
|
|
620
640
|
commandsDir: path.join(target, '.claude', 'commands'),
|
|
@@ -1079,7 +1099,7 @@ function installSkills(packageRoot, target, options = {}) {
|
|
|
1079
1099
|
// prefix, Claude Code reads from BOTH global and project, showing every
|
|
1080
1100
|
// /rcode-* twice in the slash picker. Skip the project copy for any rcode-*
|
|
1081
1101
|
// skill that already lives in the global skills dir.
|
|
1082
|
-
const globalSkillsDir = path.join(
|
|
1102
|
+
const globalSkillsDir = path.join(homedir(), '.claude', 'skills');
|
|
1083
1103
|
const globalRcodeSkills = (options.skipGlobalDuplicates && fs.existsSync(globalSkillsDir))
|
|
1084
1104
|
? new Set(fs.readdirSync(globalSkillsDir).filter(n => n.startsWith('rcode-')))
|
|
1085
1105
|
: new Set();
|
|
@@ -1303,7 +1323,7 @@ function buildInstallPlan(ide = 'claude', target = process.cwd()) {
|
|
|
1303
1323
|
const rel = path.relative(path.join(SOURCE_ROOT, 'commands'), f);
|
|
1304
1324
|
const ext = ide === 'cursor' ? '.mdc' : '.md';
|
|
1305
1325
|
const baseName = path.basename(f, '.md');
|
|
1306
|
-
const outName = (ide === 'claude' || ide === 'vscode')
|
|
1326
|
+
const outName = (ide === 'claude' || ide === 'vscode' || ide === 'grok')
|
|
1307
1327
|
? `rcode-${baseName}${ext}`
|
|
1308
1328
|
: baseName + ext;
|
|
1309
1329
|
plan.push({ src: f, rel: path.join(relCommands, path.dirname(rel), outName), ide, cursor: ide === 'cursor' });
|
|
@@ -1452,8 +1472,8 @@ function generateAgentManifest(plan, target) {
|
|
|
1452
1472
|
// Also scan rcode/agents/ in SOURCE_ROOT as a last-resort fallback so the
|
|
1453
1473
|
// manifest is never empty when the package itself ships agent definitions.
|
|
1454
1474
|
const extraScans = [
|
|
1455
|
-
path.join(
|
|
1456
|
-
path.join(
|
|
1475
|
+
path.join(homedir(), '.claude', 'agents'),
|
|
1476
|
+
path.join(homedir(), '.rcode', 'agents'),
|
|
1457
1477
|
];
|
|
1458
1478
|
// Final fallback: scan the package source itself.
|
|
1459
1479
|
try {
|
|
@@ -1901,6 +1921,125 @@ function acquireInstallLock(target) {
|
|
|
1901
1921
|
return { ok: false, pid: 0, lockPath };
|
|
1902
1922
|
}
|
|
1903
1923
|
|
|
1924
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1925
|
+
// Native home-dir slash-command install.
|
|
1926
|
+
//
|
|
1927
|
+
// Some agentic CLIs surface their `/slash` command menu ONLY from a fixed
|
|
1928
|
+
// home directory (not from project dirs the way Claude Code / Grok do):
|
|
1929
|
+
// • Codex → ~/.codex/prompts/<name>.md (flat prompt files)
|
|
1930
|
+
// • Antigravity→ ~/.gemini/antigravity/skills/<name>/SKILL.md (skill dirs)
|
|
1931
|
+
// For those tools the normal project install writes files the CLI never reads,
|
|
1932
|
+
// so `/rcode-*` never appears. This installs the commands in each CLI's NATIVE
|
|
1933
|
+
// format into its NATIVE home dir, gated behind the opt-in `--global` flag.
|
|
1934
|
+
//
|
|
1935
|
+
// IDEs that read project dirs (claude, grok, cursor, vscode, windsurf) are a
|
|
1936
|
+
// no-op here — they already work via getPathsForIde().
|
|
1937
|
+
//
|
|
1938
|
+
// Each tool's writer lives in its own helper; the dispatcher routes by ide.
|
|
1939
|
+
// SOURCE_ROOT/commands/*.md is the canonical command source for all writers.
|
|
1940
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1941
|
+
|
|
1942
|
+
// Codex + Antigravity surface NO file-based slash commands (verified live),
|
|
1943
|
+
// but BOTH support a prompt-submit hook (UserPromptSubmit / UserPrompt) that
|
|
1944
|
+
// can inject context. We install a hook ROUTER (cli/rcode-slash-router.cjs)
|
|
1945
|
+
// into each, plus a home-dir copy of every command body the router reads.
|
|
1946
|
+
// See cli/rcode-slash-router.cjs for the runtime contract.
|
|
1947
|
+
|
|
1948
|
+
// Shared: copy every command body to ~/.rcode/slash-commands/<name>.md and the
|
|
1949
|
+
// router script to ~/.rcode/bin/. A fixed home-dir location lets the hook read
|
|
1950
|
+
// commands regardless of the user's cwd. Idempotent (plain overwrite).
|
|
1951
|
+
function installSlashRouterCommands(opts) {
|
|
1952
|
+
const home = homedir();
|
|
1953
|
+
const cmdDestDir = path.join(home, '.rcode', 'slash-commands');
|
|
1954
|
+
const binDestDir = path.join(home, '.rcode', 'bin');
|
|
1955
|
+
ensureDir(cmdDestDir);
|
|
1956
|
+
ensureDir(binDestDir);
|
|
1957
|
+
|
|
1958
|
+
const srcCmdDir = path.join(SOURCE_ROOT, 'commands');
|
|
1959
|
+
let copied = 0;
|
|
1960
|
+
for (const file of fs.readdirSync(srcCmdDir)) {
|
|
1961
|
+
if (!file.endsWith('.md')) continue;
|
|
1962
|
+
fs.copyFileSync(path.join(srcCmdDir, file), path.join(cmdDestDir, file));
|
|
1963
|
+
copied++;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
const routerSrc = path.join(PACKAGE_ROOT, 'cli', 'rcode-slash-router.cjs');
|
|
1967
|
+
const routerDest = path.join(binDestDir, 'rcode-slash-router.cjs');
|
|
1968
|
+
fs.copyFileSync(routerSrc, routerDest);
|
|
1969
|
+
|
|
1970
|
+
if (opts && opts.global !== 'silent') {
|
|
1971
|
+
console.log(' ' + ok(`Slash-router: ${copied} command bodies → ~/.rcode/slash-commands/ + router → ~/.rcode/bin/`));
|
|
1972
|
+
}
|
|
1973
|
+
return routerDest;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// The absolute command a hook entry runs. Matched by substring for idempotency
|
|
1977
|
+
// and for removal on uninstall — keep the basename stable.
|
|
1978
|
+
function slashRouterHookCommand() {
|
|
1979
|
+
return `node "${path.join(homedir(), '.rcode', 'bin', 'rcode-slash-router.cjs')}"`;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// Merge a prompt-submit hook entry into an existing CLI hooks JSON file without
|
|
1983
|
+
// disturbing any pre-existing entries (e.g. herdr's). `eventKey` is the hook
|
|
1984
|
+
// event name that CLI uses (codex: UserPromptSubmit, antigravity: UserPrompt).
|
|
1985
|
+
// Idempotent: re-running detects the router by command substring and no-ops.
|
|
1986
|
+
function mergeSlashRouterHook(jsonPath, eventKey, command, label) {
|
|
1987
|
+
let root = {};
|
|
1988
|
+
if (fs.existsSync(jsonPath)) {
|
|
1989
|
+
try {
|
|
1990
|
+
root = JSON.parse(fs.readFileSync(jsonPath, 'utf8')) || {};
|
|
1991
|
+
} catch {
|
|
1992
|
+
// Unparseable file — don't clobber the user's config; bail loudly.
|
|
1993
|
+
console.log(' ' + warn(`${label}: ${jsonPath} is not valid JSON — skipped slash-router wiring.`));
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
if (!root.hooks || typeof root.hooks !== 'object') root.hooks = {};
|
|
1998
|
+
if (!Array.isArray(root.hooks[eventKey])) root.hooks[eventKey] = [];
|
|
1999
|
+
|
|
2000
|
+
const already = root.hooks[eventKey].some(group =>
|
|
2001
|
+
Array.isArray(group?.hooks) &&
|
|
2002
|
+
group.hooks.some(h => typeof h?.command === 'string' && h.command.includes('rcode-slash-router.cjs')),
|
|
2003
|
+
);
|
|
2004
|
+
if (already) {
|
|
2005
|
+
console.log(' ' + ok(`${label}: slash-router hook already present (idempotent).`));
|
|
2006
|
+
return false;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
root.hooks[eventKey].push({ hooks: [{ type: 'command', command, timeout: 10 }] });
|
|
2010
|
+
ensureDir(path.dirname(jsonPath));
|
|
2011
|
+
fs.writeFileSync(jsonPath, JSON.stringify(root, null, 2) + '\n');
|
|
2012
|
+
console.log(' ' + ok(`${label}: wired slash-router into ${eventKey} hook (existing hooks preserved).`));
|
|
2013
|
+
return true;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// Codex: ~/.codex/hooks.json, event UserPromptSubmit.
|
|
2017
|
+
function installCodexSlashRouterHook(opts) {
|
|
2018
|
+
installSlashRouterCommands(opts);
|
|
2019
|
+
const jsonPath = path.join(homedir(), '.codex', 'hooks.json');
|
|
2020
|
+
mergeSlashRouterHook(jsonPath, 'UserPromptSubmit', slashRouterHookCommand(), 'Codex');
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// Antigravity: ~/.gemini/antigravity/settings.json, event UserPrompt.
|
|
2024
|
+
function installAntigravitySlashRouterHook(opts) {
|
|
2025
|
+
installSlashRouterCommands(opts);
|
|
2026
|
+
const jsonPath = path.join(homedir(), '.gemini', 'antigravity', 'settings.json');
|
|
2027
|
+
mergeSlashRouterHook(jsonPath, 'UserPrompt', slashRouterHookCommand(), 'Antigravity');
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
function installNativeHomeSlashCommands(opts) {
|
|
2031
|
+
if (!opts || !opts.global) return;
|
|
2032
|
+
const ides = Array.isArray(opts.ides) ? opts.ides : [opts.ide].filter(Boolean);
|
|
2033
|
+
for (const ide of ides) {
|
|
2034
|
+
switch (ide) {
|
|
2035
|
+
case 'codex': installCodexSlashRouterHook(opts); break;
|
|
2036
|
+
case 'antigravity': installAntigravitySlashRouterHook(opts); break;
|
|
2037
|
+
default:
|
|
2038
|
+
break;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1904
2043
|
async function installInner(opts) {
|
|
1905
2044
|
const pkgVersion = readPackageVersion();
|
|
1906
2045
|
|
|
@@ -1955,6 +2094,7 @@ async function installInner(opts) {
|
|
|
1955
2094
|
console.error(' vscode — VS Code (with Claude Code / Continue / Copilot extension)');
|
|
1956
2095
|
console.error(' windsurf — Windsurf (Codeium)');
|
|
1957
2096
|
console.error(' antigravity — Antigravity (experimental)');
|
|
2097
|
+
console.error(' grok — Grok Build (xAI CLI, Claude-Code-compatible)');
|
|
1958
2098
|
console.error('');
|
|
1959
2099
|
console.error(' Tracked for future:');
|
|
1960
2100
|
console.error(' jetbrains — IntelliJ / PyCharm');
|
|
@@ -2298,6 +2438,16 @@ async function installInner(opts) {
|
|
|
2298
2438
|
} catch { /* non-fatal */ }
|
|
2299
2439
|
console.log('');
|
|
2300
2440
|
console.log(` ${dim(`${skillsInstalled} skills installed globally`)}`);
|
|
2441
|
+
|
|
2442
|
+
// Native home-dir slash commands for CLIs that can't surface file-based
|
|
2443
|
+
// /commands (codex, antigravity) but DO support a prompt-submit hook.
|
|
2444
|
+
// This MUST run inside the --global block: the global path returns here,
|
|
2445
|
+
// before the non-global call site below. Gated on opts.global inside.
|
|
2446
|
+
try {
|
|
2447
|
+
installNativeHomeSlashCommands(opts);
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
process.stderr.write(pc.yellow(`WARNING: native slash-command install skipped: ${err?.message || err}`) + '\n');
|
|
2450
|
+
}
|
|
2301
2451
|
return 0;
|
|
2302
2452
|
}
|
|
2303
2453
|
|
|
@@ -2305,9 +2455,9 @@ async function installInner(opts) {
|
|
|
2305
2455
|
// skip writing agents/commands to the project's .claude/ directory. Without this,
|
|
2306
2456
|
// running `npx rcode install` in the home dir AND then in a project creates two sets
|
|
2307
2457
|
// of identical files — Claude Code shows both as duplicate slash commands.
|
|
2308
|
-
const globalClaudeCommands = path.join(
|
|
2458
|
+
const globalClaudeCommands = path.join(homedir(), '.claude', 'commands');
|
|
2309
2459
|
const projectClaudeCommands = path.join(opts.target, '.claude', 'commands');
|
|
2310
|
-
const isProjectInstall = opts.target !==
|
|
2460
|
+
const isProjectInstall = opts.target !== homedir();
|
|
2311
2461
|
// Run dedup even when force:true — only forceOverwrite skips it.
|
|
2312
2462
|
if (isProjectInstall && !opts.forceOverwrite) {
|
|
2313
2463
|
try {
|
|
@@ -2489,7 +2639,7 @@ async function installInner(opts) {
|
|
|
2489
2639
|
}
|
|
2490
2640
|
|
|
2491
2641
|
// ~/.rcode/agents/ global agents directory
|
|
2492
|
-
const globalAgentsDir = path.join(
|
|
2642
|
+
const globalAgentsDir = path.join(homedir(), '.rcode', 'agents');
|
|
2493
2643
|
ensureDir(globalAgentsDir);
|
|
2494
2644
|
|
|
2495
2645
|
// Issue #702: files-manifest.csv used to be written here, BEFORE
|
|
@@ -2674,9 +2824,9 @@ async function installInner(opts) {
|
|
|
2674
2824
|
// the project skills folder may have only sidebar stubs while ~/.claude/
|
|
2675
2825
|
// has the real skills — health check should see those.
|
|
2676
2826
|
if (agentCount === 0 || commandCount === 0 || skillsInstalled < 20) {
|
|
2677
|
-
const homeAgents = path.join(
|
|
2678
|
-
const homeCommands = path.join(
|
|
2679
|
-
const homeSkills = path.join(
|
|
2827
|
+
const homeAgents = path.join(homedir(), '.claude/agents');
|
|
2828
|
+
const homeCommands = path.join(homedir(), '.claude/commands');
|
|
2829
|
+
const homeSkills = path.join(homedir(), '.claude/skills');
|
|
2680
2830
|
if (agentCount === 0 && fs.existsSync(homeAgents)) {
|
|
2681
2831
|
// #669 — count both rcode-* and rcode-* prefixes; missing rcode-
|
|
2682
2832
|
// branch produced "Agents: 0" alongside "Skills: 120".
|
|
@@ -2721,6 +2871,14 @@ async function installInner(opts) {
|
|
|
2721
2871
|
}
|
|
2722
2872
|
} catch { /* non-fatal */ }
|
|
2723
2873
|
|
|
2874
|
+
// Native home-dir slash commands for CLIs that ONLY surface /commands from
|
|
2875
|
+
// their own home dir (not project dirs). Opt-in via --global. See the fn def.
|
|
2876
|
+
try {
|
|
2877
|
+
installNativeHomeSlashCommands(opts);
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
process.stderr.write(pc.yellow(`WARNING: native slash-command install skipped: ${err?.message || err}`) + '\n');
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2724
2882
|
const version = readPackageVersion();
|
|
2725
2883
|
console.log('');
|
|
2726
2884
|
console.log(` ${bold('Version:')} ${pc.cyan('@hanzlaa/rcode@' + version)}`);
|
|
@@ -3071,3 +3229,8 @@ module.exports.install = install;
|
|
|
3071
3229
|
module.exports.SUPPORTED_IDES = SUPPORTED_IDES;
|
|
3072
3230
|
module.exports.migrateVscodeCommandsLayout = migrateVscodeCommandsLayout;
|
|
3073
3231
|
module.exports.getPathsForIde = getPathsForIde;
|
|
3232
|
+
// Slash-router (hook-based /rcode-* support for codex + antigravity).
|
|
3233
|
+
module.exports.installSlashRouterCommands = installSlashRouterCommands;
|
|
3234
|
+
module.exports.installCodexSlashRouterHook = installCodexSlashRouterHook;
|
|
3235
|
+
module.exports.installAntigravitySlashRouterHook = installAntigravitySlashRouterHook;
|
|
3236
|
+
module.exports.installNativeHomeSlashCommands = installNativeHomeSlashCommands;
|
package/cli/lib/config.cjs
CHANGED
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
const fs = require('fs');
|
|
21
|
-
const os = require('os');
|
|
22
21
|
const path = require('path');
|
|
23
22
|
const { writeJsonAtomic } = require('./fsutil.cjs');
|
|
23
|
+
// HOME-aware home resolution (#889) — os.homedir() ignores a stubbed HOME
|
|
24
|
+
// on Windows, which broke user-level defaults isolation in tests.
|
|
25
|
+
const { homedir } = require('./homedir.cjs');
|
|
24
26
|
|
|
25
27
|
// ---------- Schema ----------
|
|
26
28
|
|
|
@@ -102,7 +104,7 @@ const VALID_COMMUNICATION_MODES = new Set(['guided', 'yolo']);
|
|
|
102
104
|
// ---------- Paths ----------
|
|
103
105
|
|
|
104
106
|
function userLevelPath() {
|
|
105
|
-
return path.join(
|
|
107
|
+
return path.join(homedir(), '.rcode', 'defaults.json');
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
function projectLevelPath(cwd) {
|
package/cli/lib/fsutil.cjs
CHANGED
|
@@ -115,8 +115,19 @@ function safeRmSync(targetPath, projectRoot) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
// Real path must stay inside the project root.
|
|
119
|
-
|
|
118
|
+
// Real path must stay inside the project root. The root must be
|
|
119
|
+
// realpathed too: on macOS os.tmpdir() lives behind a symlink
|
|
120
|
+
// (/tmp → /private/tmp, /var → /private/var), so comparing a realpathed
|
|
121
|
+
// target against a merely-resolved root misreports anything under /tmp
|
|
122
|
+
// as outside-root.
|
|
123
|
+
let root;
|
|
124
|
+
try {
|
|
125
|
+
root = fs.realpathSync(projectRoot);
|
|
126
|
+
} catch {
|
|
127
|
+
// Root missing/unreadable — fall back to a lexical resolve; the
|
|
128
|
+
// containment check below then fails closed for an existing target.
|
|
129
|
+
root = path.resolve(projectRoot);
|
|
130
|
+
}
|
|
120
131
|
let resolved;
|
|
121
132
|
try {
|
|
122
133
|
resolved = fs.realpathSync(targetPath);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/lib/homedir.cjs — user home directory resolution (#889).
|
|
3
|
+
*
|
|
4
|
+
* process.env.HOME wins over os.homedir() so a single env var redirects
|
|
5
|
+
* every home-relative read/write on EVERY platform. os.homedir() ignores
|
|
6
|
+
* HOME on Windows (it reads USERPROFILE), which made HOME-stubbed tests
|
|
7
|
+
* silently escape to the real profile dir on Windows CI — installs leaked
|
|
8
|
+
* ~/.codex / ~/.gemini / ~/.rcode into the runner's real home and broke
|
|
9
|
+
* unrelated tests. Honoring HOME also matches git/npm behavior on Windows
|
|
10
|
+
* (git-bash sets HOME), so real users get consistent paths across shells.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
function homedir() {
|
|
18
|
+
return process.env.HOME || os.homedir();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { homedir };
|
package/cli/lib/schemas.cjs
CHANGED
|
@@ -35,9 +35,14 @@ const { z } = require('zod');
|
|
|
35
35
|
* @returns {{ frontmatter: object, body: string }}
|
|
36
36
|
*/
|
|
37
37
|
function parseFrontmatter(text) {
|
|
38
|
-
if (typeof text !== 'string'
|
|
38
|
+
if (typeof text !== 'string') {
|
|
39
39
|
return { frontmatter: {}, body: text || '' };
|
|
40
40
|
}
|
|
41
|
+
// CRLF tolerance (#889): Windows checkouts/user files may use \r\n.
|
|
42
|
+
text = text.replace(/\r\n/g, '\n');
|
|
43
|
+
if (!text.startsWith('---\n')) {
|
|
44
|
+
return { frontmatter: {}, body: text };
|
|
45
|
+
}
|
|
41
46
|
const end = text.indexOf('\n---\n', 4);
|
|
42
47
|
if (end === -1) return { frontmatter: {}, body: text };
|
|
43
48
|
const block = text.slice(4, end);
|
package/cli/nuke.js
CHANGED
|
@@ -20,9 +20,12 @@
|
|
|
20
20
|
'use strict';
|
|
21
21
|
|
|
22
22
|
const fs = require('fs');
|
|
23
|
-
const os = require('os');
|
|
24
23
|
const path = require('path');
|
|
25
24
|
const { spawnSync } = require('child_process');
|
|
25
|
+
// HOME-aware home resolution (#889) — os.homedir() ignores a stubbed HOME
|
|
26
|
+
// on Windows, so tests pointing HOME at a temp dir still scanned the real
|
|
27
|
+
// profile dir there (and tripped over real ~/.rcode state).
|
|
28
|
+
const { homedir } = require('./lib/homedir.cjs');
|
|
26
29
|
|
|
27
30
|
function exists(p) {
|
|
28
31
|
try { fs.accessSync(p); return true; } catch { return false; }
|
|
@@ -37,7 +40,7 @@ function readDirSafe(p) {
|
|
|
37
40
|
* Returns a list of { manager, dir } — dir may not exist.
|
|
38
41
|
*/
|
|
39
42
|
function getGlobalNodeModulesDirs() {
|
|
40
|
-
const home =
|
|
43
|
+
const home = homedir();
|
|
41
44
|
const candidates = [];
|
|
42
45
|
|
|
43
46
|
// npm — npm root -g resolves to the active node version's lib/node_modules.
|
|
@@ -106,7 +109,7 @@ function findRcodePackages(globalNodeModules) {
|
|
|
106
109
|
* Resolve global bin directories where rcode/rcode/rcode may live.
|
|
107
110
|
*/
|
|
108
111
|
function getGlobalBinDirs() {
|
|
109
|
-
const home =
|
|
112
|
+
const home = homedir();
|
|
110
113
|
const dirs = new Set();
|
|
111
114
|
|
|
112
115
|
// npm prefix bin
|
|
@@ -214,7 +217,7 @@ function findClaudeArtifacts(claudeDir) {
|
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
function buildPlan({ includePlanning }) {
|
|
217
|
-
const home =
|
|
220
|
+
const home = homedir();
|
|
218
221
|
const cwd = process.cwd();
|
|
219
222
|
const plan = {
|
|
220
223
|
packages: [],
|
|
@@ -244,13 +247,15 @@ function buildPlan({ includePlanning }) {
|
|
|
244
247
|
plan.globalClaude = findClaudeArtifacts(path.join(home, '.claude'));
|
|
245
248
|
|
|
246
249
|
// Global state (~/.rcode/)
|
|
250
|
+
// #889: was `= globalRcode` (undefined) — a ReferenceError that only fired
|
|
251
|
+
// when ~/.rcode existed, crashing every dry-run on machines with global state.
|
|
247
252
|
const globalrcode = path.join(home, '.rcode');
|
|
248
|
-
if (exists(globalrcode)) plan.globalrcode =
|
|
253
|
+
if (exists(globalrcode)) plan.globalrcode = globalrcode;
|
|
249
254
|
|
|
250
255
|
// Project-level (CWD only — never recurse, user may have many projects)
|
|
251
256
|
plan.projectClaude = findClaudeArtifacts(path.join(cwd, '.claude'));
|
|
252
257
|
const projectrcode = path.join(cwd, '.rcode');
|
|
253
|
-
if (exists(projectrcode) && cwd !== home) plan.projectrcode =
|
|
258
|
+
if (exists(projectrcode) && cwd !== home) plan.projectrcode = projectrcode;
|
|
254
259
|
|
|
255
260
|
if (includePlanning) {
|
|
256
261
|
const projectPlanning = path.join(cwd, '.planning');
|
|
@@ -351,7 +356,7 @@ function executePlan(plan) {
|
|
|
351
356
|
if (rmrf(a.path)) { console.log(` ✓ removed ${a.path}`); removed++; }
|
|
352
357
|
}
|
|
353
358
|
if (plan.globalrcode && rmrf(plan.globalrcode)) {
|
|
354
|
-
console.log(` ✓ removed ${plan.
|
|
359
|
+
console.log(` ✓ removed ${plan.globalrcode}`); removed++;
|
|
355
360
|
}
|
|
356
361
|
|
|
357
362
|
// Claude artifacts (project)
|
|
@@ -359,7 +364,7 @@ function executePlan(plan) {
|
|
|
359
364
|
if (rmrf(a.path)) { console.log(` ✓ removed ${a.path}`); removed++; }
|
|
360
365
|
}
|
|
361
366
|
if (plan.projectrcode && rmrf(plan.projectrcode)) {
|
|
362
|
-
console.log(` ✓ removed ${plan.
|
|
367
|
+
console.log(` ✓ removed ${plan.projectrcode}`); removed++;
|
|
363
368
|
}
|
|
364
369
|
if (plan.projectPlanning && rmrf(plan.projectPlanning)) {
|
|
365
370
|
console.log(` ✓ removed ${plan.projectPlanning}`); removed++;
|
package/cli/postinstall.js
CHANGED
|
@@ -13,6 +13,17 @@
|
|
|
13
13
|
const os = require('os');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Path containment check that survives Windows: path.relative normalizes
|
|
18
|
+
* separators (/ vs \) and compares drive letters case-insensitively, which
|
|
19
|
+
* a raw startsWith prefix compare does not.
|
|
20
|
+
*/
|
|
21
|
+
function isPathInside(child, parent) {
|
|
22
|
+
const rel = path.relative(parent, child);
|
|
23
|
+
if (rel === '') return true;
|
|
24
|
+
return rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
/**
|
|
17
28
|
* Decide whether the current postinstall invocation represents a GLOBAL
|
|
18
29
|
* `npm install -g @hanzlaa/rcode` (true) or a transitive devDep install
|
|
@@ -28,17 +39,16 @@ const path = require('path');
|
|
|
28
39
|
function isGlobalInstall(env, dirname, cwd) {
|
|
29
40
|
try {
|
|
30
41
|
if (env.npm_config_global === 'true') return true;
|
|
31
|
-
if (env.PNPM_HOME && dirname
|
|
42
|
+
if (env.PNPM_HOME && isPathInside(dirname, env.PNPM_HOME)) return true;
|
|
32
43
|
const globalPatterns = [
|
|
33
|
-
|
|
44
|
+
/[/\\]node_modules[/\\]@hanzlaa[/\\]rcode/,
|
|
34
45
|
/[/\\]lib[/\\]node_modules[/\\]/,
|
|
35
46
|
/\.nvm[/\\]versions[/\\]/,
|
|
36
47
|
/\.pnpm[/\\]/,
|
|
37
48
|
/\.yarn[/\\]global/,
|
|
38
49
|
];
|
|
39
50
|
if (globalPatterns.some((re) => re.test(dirname))) return true;
|
|
40
|
-
|
|
41
|
-
if (!dirname.startsWith(localNodeModules)) return true;
|
|
51
|
+
if (!isPathInside(dirname, path.join(cwd, 'node_modules'))) return true;
|
|
42
52
|
return false;
|
|
43
53
|
} catch {
|
|
44
54
|
return false;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// rcode slash-command hook router.
|
|
5
|
+
//
|
|
6
|
+
// WHY this exists: Codex CLI and Antigravity CLI do NOT surface file-based
|
|
7
|
+
// `/slash` commands the way Claude Code / Grok do (verified live). They DO,
|
|
8
|
+
// however, support a prompt-submit hook (`UserPromptSubmit` / `UserPrompt`)
|
|
9
|
+
// that can inject extra context into the model's turn. This router is wired
|
|
10
|
+
// into that hook by the installer. When the user types `/rcode-<name> [args]`,
|
|
11
|
+
// the router loads the matching command body and injects it as additional
|
|
12
|
+
// context so the model executes that command — the closest thing to a native
|
|
13
|
+
// slash command those CLIs allow.
|
|
14
|
+
//
|
|
15
|
+
// Dependency-free (Node stdlib only) so it can run from a stable home dir
|
|
16
|
+
// (~/.rcode/bin/) without an install step. NEVER throws to the host CLI: any
|
|
17
|
+
// error exits 0 with no output so a malfunctioning router can never break or
|
|
18
|
+
// swallow the user's real prompt.
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// Command bodies are copied here by the installer (installSlashRouterCommands).
|
|
25
|
+
// A fixed home-dir location means the hook can always read them regardless of
|
|
26
|
+
// the user's current working directory.
|
|
27
|
+
// HOME wins over os.homedir() (#889): os.homedir() ignores HOME on Windows
|
|
28
|
+
// (it reads USERPROFILE), so HOME-redirected runs (tests, git-bash) would read
|
|
29
|
+
// the wrong profile dir. Inlined — this script is copied standalone to
|
|
30
|
+
// ~/.rcode/bin/ and must stay dependency-free (no ./lib requires).
|
|
31
|
+
const COMMANDS_DIR = path.join(process.env.HOME || os.homedir(), '.rcode', 'slash-commands');
|
|
32
|
+
|
|
33
|
+
// Matches `/rcode-<name>` at the very start, optional whitespace, then the
|
|
34
|
+
// rest of the line(s) as arguments. `\b` ends the command name so trailing
|
|
35
|
+
// punctuation/args don't leak into <name>.
|
|
36
|
+
const SLASH_RE = /^\/rcode-([a-z0-9-]+)\b[ \t]*([\s\S]*)$/;
|
|
37
|
+
|
|
38
|
+
function readStdin() {
|
|
39
|
+
try {
|
|
40
|
+
return fs.readFileSync(0, 'utf8');
|
|
41
|
+
} catch {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Strip a leading YAML frontmatter block (`---\n...\n---`). The frontmatter is
|
|
47
|
+
// CLI-tooling metadata (name/description/allowed-tools) that only confuses the
|
|
48
|
+
// model — we want the executable command body injected, not its header.
|
|
49
|
+
// \r?\n because Windows checkouts may deliver CRLF command bodies (#889).
|
|
50
|
+
function stripFrontmatter(text) {
|
|
51
|
+
return text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function emit(hookEventName, additionalContext) {
|
|
55
|
+
const payload = {
|
|
56
|
+
hookSpecificOutput: {
|
|
57
|
+
hookEventName: hookEventName || 'UserPromptSubmit',
|
|
58
|
+
additionalContext,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
process.stdout.write(JSON.stringify(payload));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function main() {
|
|
65
|
+
const raw = readStdin();
|
|
66
|
+
if (!raw.trim()) return;
|
|
67
|
+
|
|
68
|
+
let data;
|
|
69
|
+
try {
|
|
70
|
+
data = JSON.parse(raw);
|
|
71
|
+
} catch {
|
|
72
|
+
return; // not JSON we understand → pass-through (no output)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Field names vary across CLIs; accept the common spellings.
|
|
76
|
+
const prompt =
|
|
77
|
+
data.prompt ??
|
|
78
|
+
data.user_prompt ??
|
|
79
|
+
data.userPrompt ??
|
|
80
|
+
data.message ??
|
|
81
|
+
data.input ??
|
|
82
|
+
'';
|
|
83
|
+
const hookEventName = data.hook_event_name || data.hookEventName || 'UserPromptSubmit';
|
|
84
|
+
|
|
85
|
+
if (typeof prompt !== 'string') return;
|
|
86
|
+
|
|
87
|
+
const match = prompt.replace(/^\s+/, '').match(SLASH_RE);
|
|
88
|
+
if (!match) return; // not an rcode command → pass-through (no output)
|
|
89
|
+
|
|
90
|
+
const name = match[1];
|
|
91
|
+
const args = (match[2] || '').trim();
|
|
92
|
+
|
|
93
|
+
const cmdFile = path.join(COMMANDS_DIR, `${name}.md`);
|
|
94
|
+
if (!fs.existsSync(cmdFile)) {
|
|
95
|
+
// Unknown command: inject a short note rather than silently doing nothing,
|
|
96
|
+
// so the user learns the command name didn't resolve.
|
|
97
|
+
emit(
|
|
98
|
+
hookEventName,
|
|
99
|
+
`Unknown rcode command: /rcode-${name}. No matching command body was found in ${COMMANDS_DIR}.`,
|
|
100
|
+
);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let body = stripFrontmatter(fs.readFileSync(cmdFile, 'utf8')).trim();
|
|
105
|
+
if (args) {
|
|
106
|
+
// Surface user-supplied args the way the command bodies expect ($ARGUMENTS).
|
|
107
|
+
body += `\n\nArguments: ${args}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
emit(hookEventName, body);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
main();
|
|
115
|
+
} catch {
|
|
116
|
+
// Never break the host CLI's prompt — fail open, silently.
|
|
117
|
+
}
|
|
118
|
+
process.exit(0);
|