@hanzlaa/rcode 4.1.1 → 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/AGENTS.md +1 -1
- package/CONTRIBUTING.md +3 -0
- package/README.md +3 -0
- package/cli/agent.js +3 -1
- package/cli/index.js +29 -0
- package/cli/install.js +233 -15
- 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/cli/workflow.js +3 -1
- package/dist/rcode.js +241 -227
- package/package.json +1 -1
- package/rcode/bin/rcode-tools.cjs +15 -6
- package/rcode/commands/scaffold-project.md +2 -2
- package/rcode/skills/actions/2-plan/rcode-create-epics-and-stories/steps/step-04-final-validation.md +1 -1
- package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/README.md +2 -2
- package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/step-09-state-sync.md +1 -1
- package/rcode/skills/actions/4-implementation/rcode-code-review/steps/step-02-review.md +1 -1
- package/rcode/skills/actions/4-implementation/rcode-git-flow/SKILL.md +1 -1
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/SKILL.md +39 -12
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-01-target.md +18 -3
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-02-safety.md +27 -3
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-03-brownfield.md +57 -0
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-03-clone.md +4 -1
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-04-post-setup.md +15 -1
- package/rcode/skills/actions/4-implementation/rcode-trim/SKILL.md +1 -1
- package/rcode/workflows/audit-milestone.md +1 -1
- package/rcode/workflows/discuss-phase.md +1 -1
- package/rcode/workflows/execute-milestone.md +1 -1
- package/rcode/workflows/execute-regression-gates.md +3 -0
- package/rcode/workflows/execute-sprint.md +27 -1
- package/rcode/workflows/execute-waves.md +6 -0
- package/rcode/workflows/execute.md +13 -3
- package/rcode/workflows/new-milestone.md +2 -2
- package/rcode/workflows/new-project.md +4 -0
- package/rcode/workflows/plan-research-validation.md +1 -1
- package/rcode/workflows/plan-spawn-planner.md +2 -2
- package/rcode/workflows/plan.md +34 -15
- package/rcode/workflows/review.md +2 -0
- package/rcode/workflows/scaffold-project.md +5 -1
- package/rcode/workflows/session-report.md +1 -1
- package/rcode/workflows/ship.md +39 -0
- package/rcode/workflows/sprint-planning.md +27 -0
- package/rcode/workflows/status.md +3 -3
- 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/AGENTS.md
CHANGED
|
@@ -24,7 +24,7 @@ If a user says "just keep going" or "don't stop until done", that authorization
|
|
|
24
24
|
|
|
25
25
|
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format: `type(scope): subject`
|
|
26
26
|
- Types allowed: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `perf`, `revert`
|
|
27
|
-
- Scopes allowed: `agents`, `skills`, `workflows`, `templates`, `dashboard`, `docs`, `config`, `github`, `commands`, `memory`, `brand`, `cli`, `ci`, `release`, `meta`, `tasks`, `migrations`, `refs`, `state`, `hooks`, `install`, `parity`, `triggers`, `dogfood`, `namespace`, `planning`, `insights`, `help`, `roadmap`, `session`, `audits`, `execute`, `executor`, `plan`, `planner`, `readme`, `sync`, `sprint`, `agent-exp`, `extensibility`, `lens-audit`, `tiers`, `build`, `council`, `doctor`, `postinstall`, `progress`, `security`, `tools`, `uninstall`, `update`, `test`, `changelog`, `scopes`, `phases`, `references`, `kanban`, `orchestrator`, `orchpanel`, `status`, `bin`, `brain`, `dogfeed`, `new-project`, `package`, `rcode-tools`, `rihal-tools`, `team`, `usp`, `v4`, `observability`, `audit`, `agent-rules`, `cursor`, `i18n`, `phase`, plus numeric phase/sprint scopes (e.g. `docs(15)`, `feat(8.3)`)
|
|
27
|
+
- Scopes allowed: `agents`, `skills`, `workflows`, `templates`, `dashboard`, `docs`, `config`, `github`, `commands`, `memory`, `brand`, `cli`, `ci`, `release`, `meta`, `tasks`, `migrations`, `refs`, `state`, `hooks`, `install`, `parity`, `triggers`, `dogfood`, `namespace`, `planning`, `insights`, `help`, `roadmap`, `session`, `audits`, `execute`, `executor`, `plan`, `planner`, `readme`, `sync`, `sprint`, `agent-exp`, `extensibility`, `lens-audit`, `tiers`, `build`, `council`, `doctor`, `postinstall`, `progress`, `security`, `tools`, `uninstall`, `update`, `test`, `changelog`, `scopes`, `phases`, `references`, `kanban`, `orchestrator`, `orchpanel`, `status`, `bin`, `brain`, `dogfeed`, `new-project`, `package`, `rcode-tools`, `rihal-tools`, `team`, `usp`, `v4`, `observability`, `audit`, `agent-rules`, `cursor`, `i18n`, `phase`, `scaffold`, `campaign`, `ship`, plus numeric phase/sprint scopes (e.g. `docs(15)`, `feat(8.3)`)
|
|
28
28
|
- Subject: lowercase first letter, imperative mood, no trailing period, under 72 chars
|
|
29
29
|
- **NEVER add Claude/AI attribution to commit messages.** No "Generated with Claude Code", no "Co-Authored-By: Claude", no "🤖 Generated". The user does not want this.
|
|
30
30
|
- **NEVER use `--no-verify`** to bypass hooks. If hooks fail, fix the underlying issue.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -349,6 +349,9 @@ We use [Conventional Commits](https://www.conventionalcommits.org/) format. The
|
|
|
349
349
|
- `cursor` — Cursor IDE rules under `.cursor/rules/rcode/`
|
|
350
350
|
- `i18n` — internationalization, brand strings, and user-visible copy
|
|
351
351
|
- `phase` — phase-lifecycle changes (variable renames, phase-loop fixes); use numeric `<phase-id>` for in-phase commits
|
|
352
|
+
- `scaffold` — scaffold-project workflow changes
|
|
353
|
+
- `campaign` — fix-campaign orchestration tracking files
|
|
354
|
+
- `ship` — ship workflow changes
|
|
352
355
|
- `<phase-id>` — numeric phase scope when committing inside a phase (e.g. `docs(15)`, `feat(8.3)`)
|
|
353
356
|
- `<sprint-id>` — numeric sprint scope inside a phase (e.g. `feat(15.1)`)
|
|
354
357
|
|
package/README.md
CHANGED
|
@@ -117,6 +117,9 @@ pnpm dlx @hanzlaa/rcode install
|
|
|
117
117
|
/rcode-init
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
> **Don't have pnpm?** On Node 18+ / npm 11.x we recommend pnpm to avoid `npx` cache issues:
|
|
121
|
+
> `npm install -g pnpm`
|
|
122
|
+
|
|
120
123
|
`/rcode-init` detects your project state (fresh / existing / returning) and routes to the right first action. For a greenfield project it auto-routes to `/rcode-new-project`.
|
|
121
124
|
|
|
122
125
|
### The full loop
|
package/cli/agent.js
CHANGED
|
@@ -24,7 +24,9 @@ module.exports = function agent(args, { packageRoot }) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const name = args[0];
|
|
27
|
-
|
|
27
|
+
// Strip rcode- prefix if already provided so rcode agent rcode-executor works (#882)
|
|
28
|
+
const bare = name.startsWith('rcode-') ? name.slice('rcode-'.length) : name;
|
|
29
|
+
const agentName = `rcode-${bare}`;
|
|
28
30
|
|
|
29
31
|
// Validate agent file exists
|
|
30
32
|
const agentFile = path.join(agentDir, `${agentName}.md`);
|
package/cli/index.js
CHANGED
|
@@ -34,6 +34,10 @@ const COMMANDS = {
|
|
|
34
34
|
agent: require('./agent'),
|
|
35
35
|
doctor: require('./doctor'),
|
|
36
36
|
workflow: require('./workflow'), // lifecycle bridge for non-Claude runtimes
|
|
37
|
+
// Thin lifecycle aliases — delegate to workflow show <name> (#883)
|
|
38
|
+
plan: (args, ctx) => lifecycleAlias('plan', args, ctx),
|
|
39
|
+
execute: (args, ctx) => lifecycleAlias('execute-sprint', args, ctx),
|
|
40
|
+
ship: (args, ctx) => lifecycleAlias('ship', args, ctx),
|
|
37
41
|
'set-profile': require('./set-profile'),
|
|
38
42
|
'set-mode': require('./set-mode'),
|
|
39
43
|
config: require('./config'),
|
|
@@ -70,6 +74,9 @@ Usage:
|
|
|
70
74
|
github-sync Sync .rcode/ phases/epics/stories to GitHub (dry-run default)
|
|
71
75
|
|
|
72
76
|
🔄 LIFECYCLE (Codex / Copilot / Grok bridge)
|
|
77
|
+
plan Print the plan workflow (alias for workflow show plan)
|
|
78
|
+
execute Print the execute-sprint workflow
|
|
79
|
+
ship Print the ship workflow
|
|
73
80
|
workflow list List all lifecycle workflow names
|
|
74
81
|
workflow show <name> Print a workflow's full instructions to stdout
|
|
75
82
|
workflow show new-project → project setup + ROADMAP
|
|
@@ -82,6 +89,7 @@ Usage:
|
|
|
82
89
|
workflow show ship → deploy / release workflow
|
|
83
90
|
|
|
84
91
|
Non-Claude agents: pipe to your agent instead of using slash commands.
|
|
92
|
+
Example: rcode plan | codex run -
|
|
85
93
|
Example: rcode workflow show plan | codex run -
|
|
86
94
|
|
|
87
95
|
👥 TEAM
|
|
@@ -91,6 +99,7 @@ Usage:
|
|
|
91
99
|
rcode agent --list to see available agents
|
|
92
100
|
show-model Show which model each agent uses in the current profile
|
|
93
101
|
dashboard Start the Diwan view-only dashboard (port 7717)
|
|
102
|
+
Starts a view-only dashboard at http://localhost:7717. No write access.
|
|
94
103
|
serve Alias for dashboard
|
|
95
104
|
|
|
96
105
|
⚙️ META
|
|
@@ -111,6 +120,26 @@ Documentation: https://github.com/hanzlahabib/rihal-code
|
|
|
111
120
|
`.trim());
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Lifecycle aliases (plan/execute/ship): show the workflow then print actionable
|
|
125
|
+
* next-step guidance so the user knows how to actually run it.
|
|
126
|
+
*/
|
|
127
|
+
function lifecycleAlias(workflowName, args, ctx) {
|
|
128
|
+
require('./workflow')(['show', workflowName, ...args], ctx);
|
|
129
|
+
|
|
130
|
+
const hasAuto = args.includes('--auto') || args.includes('--run');
|
|
131
|
+
|
|
132
|
+
console.log('\n─────────────────────────────────────────────');
|
|
133
|
+
console.log(`▶ To run: paste the above into Claude Code as /${workflowName === 'execute-sprint' ? 'rcode-execute-sprint' : `rcode-${workflowName}`}`);
|
|
134
|
+
console.log(' or pipe it directly: rcode ' + (workflowName === 'execute-sprint' ? 'execute' : workflowName) + ' | cld --model sonnet');
|
|
135
|
+
|
|
136
|
+
if (hasAuto) {
|
|
137
|
+
console.log('\n AUTO mode detected — in Claude Code run: /rcode-' +
|
|
138
|
+
(workflowName === 'execute-sprint' ? 'execute-sprint' : workflowName) +
|
|
139
|
+
' --auto (applies yolo defaults, skips confirmation prompts)');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
114
143
|
/**
|
|
115
144
|
* npm 10+ suppresses postinstall script output during global installs, so users
|
|
116
145
|
* who run `npm install -g @hanzlaa/rcode` see only "added 1 package" with no
|
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']);
|
|
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
|
|
@@ -197,6 +202,9 @@ function parseArgs(argv) {
|
|
|
197
202
|
silent: false,
|
|
198
203
|
// noPrompt — skip all interactive prompts (used by postinstall auto-run)
|
|
199
204
|
noPrompt: false,
|
|
205
|
+
// dry-run / list-files — preview paths that would be written, then exit
|
|
206
|
+
dryRun: false,
|
|
207
|
+
listFiles: false,
|
|
200
208
|
};
|
|
201
209
|
const positional = [];
|
|
202
210
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -231,6 +239,8 @@ function parseArgs(argv) {
|
|
|
231
239
|
else if (arg === '--global') opts.global = true;
|
|
232
240
|
else if (arg === '--silent') opts.silent = true;
|
|
233
241
|
else if (arg === '--no-prompt') opts.noPrompt = true;
|
|
242
|
+
else if (arg === '--dry-run') opts.dryRun = true;
|
|
243
|
+
else if (arg === '--list-files') opts.listFiles = true;
|
|
234
244
|
else if (!arg.startsWith('--')) positional.push(arg);
|
|
235
245
|
}
|
|
236
246
|
if (positional[0]) {
|
|
@@ -242,7 +252,7 @@ function parseArgs(argv) {
|
|
|
242
252
|
// project directory wrote rcode artifacts to that project, not to the user's
|
|
243
253
|
// home where Claude Code reads global commands from.
|
|
244
254
|
if (opts.global && !opts.targetProvided) {
|
|
245
|
-
opts.target =
|
|
255
|
+
opts.target = homedir();
|
|
246
256
|
}
|
|
247
257
|
// Issue #821/#832: pnpm workspace anchor.
|
|
248
258
|
// When `pnpm add -D @hanzlaa/rcode` runs inside a workspace member,
|
|
@@ -372,7 +382,7 @@ function printInstallHeader(targetVersion) {
|
|
|
372
382
|
* Returns a set like { claude: true, cursor: false, gemini: false }.
|
|
373
383
|
*/
|
|
374
384
|
function detectIdeSignals(target) {
|
|
375
|
-
const signals = { claude: false, cursor: false, gemini: false, vscode: false, antigravity: false, windsurf: false };
|
|
385
|
+
const signals = { claude: false, cursor: false, gemini: false, vscode: false, antigravity: false, windsurf: false, codex: false };
|
|
376
386
|
// 1. Project-local install dirs (strongest signal — they already use one)
|
|
377
387
|
if (fs.existsSync(path.join(target, '.claude'))) signals.claude = true;
|
|
378
388
|
if (fs.existsSync(path.join(target, '.cursor'))) signals.cursor = true;
|
|
@@ -381,7 +391,7 @@ function detectIdeSignals(target) {
|
|
|
381
391
|
if (fs.existsSync(path.join(target, '.antigravity'))) signals.antigravity = true;
|
|
382
392
|
if (fs.existsSync(path.join(target, '.windsurf'))) signals.windsurf = true;
|
|
383
393
|
// 2. User-level config dirs
|
|
384
|
-
const home =
|
|
394
|
+
const home = homedir();
|
|
385
395
|
if (fs.existsSync(path.join(home, '.claude'))) signals.claude = true;
|
|
386
396
|
if (fs.existsSync(path.join(home, '.cursor'))) signals.cursor = true;
|
|
387
397
|
if (fs.existsSync(path.join(home, '.config', 'Cursor'))) signals.cursor = true;
|
|
@@ -396,6 +406,7 @@ function detectIdeSignals(target) {
|
|
|
396
406
|
if (process.env.CLAUDECODE === '1' || process.env.CLAUDE_CODE_ENTRYPOINT) signals.claude = true;
|
|
397
407
|
if (process.env.VSCODE_PID || /vscode/i.test(process.env.TERM_PROGRAM || '')) signals.vscode = true;
|
|
398
408
|
if (/windsurf/i.test(process.env.TERM_PROGRAM || '')) signals.windsurf = true;
|
|
409
|
+
if (process.env.CODEX_ENV || /codex/i.test(process.env.TERM_PROGRAM || '')) signals.codex = true;
|
|
399
410
|
return signals;
|
|
400
411
|
}
|
|
401
412
|
|
|
@@ -442,6 +453,7 @@ async function resolveIde(opts) {
|
|
|
442
453
|
options: [
|
|
443
454
|
{ value: 'claude', label: 'Claude Code', hint: signals.claude ? '(detected)' : undefined },
|
|
444
455
|
{ value: 'cursor', label: 'Cursor', hint: signals.cursor ? '(detected)' : undefined },
|
|
456
|
+
{ value: 'codex', label: 'Codex (OpenAI CLI)', hint: signals.codex ? '(detected)' : '(uses AGENTS.md + workflow bridge)' },
|
|
445
457
|
{ value: 'gemini', label: 'Gemini CLI', hint: signals.gemini ? '(detected)' : '(beta — limited)' },
|
|
446
458
|
{ value: 'vscode', label: 'VS Code', hint: signals.vscode ? '(detected)' : '(via Continue / Copilot extensions)' },
|
|
447
459
|
{ value: 'antigravity', label: 'Antigravity', hint: '(experimental — installs to .antigravity/)' },
|
|
@@ -522,6 +534,8 @@ Options:
|
|
|
522
534
|
--language <lang> set communication_language (default: English)
|
|
523
535
|
--mode <guided|yolo> default mode (default: guided)
|
|
524
536
|
--ide <name> target IDE (claude, cursor, gemini; default: claude)
|
|
537
|
+
--dry-run preview what would be written; exit without writing any files
|
|
538
|
+
--list-files alias for --dry-run
|
|
525
539
|
--help this text
|
|
526
540
|
|
|
527
541
|
Installs (IDE-specific):
|
|
@@ -601,6 +615,33 @@ function getPathsForIde(ide, target) {
|
|
|
601
615
|
referencesDir: path.join(target, '.rcode', 'references'),
|
|
602
616
|
binDir: path.join(target, '.rcode', 'bin'),
|
|
603
617
|
};
|
|
618
|
+
case 'codex':
|
|
619
|
+
// OpenAI Codex CLI reads AGENTS.md from the project root (written by the
|
|
620
|
+
// claude/vscode install paths). We install agent + command files to .claude/
|
|
621
|
+
// so multi-IDE installs share files, and the rcode workflow bridge gives
|
|
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.
|
|
638
|
+
return {
|
|
639
|
+
agentsDir: path.join(target, '.claude', 'agents'),
|
|
640
|
+
commandsDir: path.join(target, '.claude', 'commands'),
|
|
641
|
+
workflowsDir: path.join(target, '.rcode', 'workflows'),
|
|
642
|
+
referencesDir: path.join(target, '.rcode', 'references'),
|
|
643
|
+
binDir: path.join(target, '.rcode', 'bin'),
|
|
644
|
+
};
|
|
604
645
|
default:
|
|
605
646
|
throw new Error(`Unknown IDE: ${ide}. Supported: ${SUPPORTED_IDES.join(', ')}`);
|
|
606
647
|
}
|
|
@@ -1058,7 +1099,7 @@ function installSkills(packageRoot, target, options = {}) {
|
|
|
1058
1099
|
// prefix, Claude Code reads from BOTH global and project, showing every
|
|
1059
1100
|
// /rcode-* twice in the slash picker. Skip the project copy for any rcode-*
|
|
1060
1101
|
// skill that already lives in the global skills dir.
|
|
1061
|
-
const globalSkillsDir = path.join(
|
|
1102
|
+
const globalSkillsDir = path.join(homedir(), '.claude', 'skills');
|
|
1062
1103
|
const globalRcodeSkills = (options.skipGlobalDuplicates && fs.existsSync(globalSkillsDir))
|
|
1063
1104
|
? new Set(fs.readdirSync(globalSkillsDir).filter(n => n.startsWith('rcode-')))
|
|
1064
1105
|
: new Set();
|
|
@@ -1282,7 +1323,7 @@ function buildInstallPlan(ide = 'claude', target = process.cwd()) {
|
|
|
1282
1323
|
const rel = path.relative(path.join(SOURCE_ROOT, 'commands'), f);
|
|
1283
1324
|
const ext = ide === 'cursor' ? '.mdc' : '.md';
|
|
1284
1325
|
const baseName = path.basename(f, '.md');
|
|
1285
|
-
const outName = (ide === 'claude' || ide === 'vscode')
|
|
1326
|
+
const outName = (ide === 'claude' || ide === 'vscode' || ide === 'grok')
|
|
1286
1327
|
? `rcode-${baseName}${ext}`
|
|
1287
1328
|
: baseName + ext;
|
|
1288
1329
|
plan.push({ src: f, rel: path.join(relCommands, path.dirname(rel), outName), ide, cursor: ide === 'cursor' });
|
|
@@ -1431,8 +1472,8 @@ function generateAgentManifest(plan, target) {
|
|
|
1431
1472
|
// Also scan rcode/agents/ in SOURCE_ROOT as a last-resort fallback so the
|
|
1432
1473
|
// manifest is never empty when the package itself ships agent definitions.
|
|
1433
1474
|
const extraScans = [
|
|
1434
|
-
path.join(
|
|
1435
|
-
path.join(
|
|
1475
|
+
path.join(homedir(), '.claude', 'agents'),
|
|
1476
|
+
path.join(homedir(), '.rcode', 'agents'),
|
|
1436
1477
|
];
|
|
1437
1478
|
// Final fallback: scan the package source itself.
|
|
1438
1479
|
try {
|
|
@@ -1880,6 +1921,125 @@ function acquireInstallLock(target) {
|
|
|
1880
1921
|
return { ok: false, pid: 0, lockPath };
|
|
1881
1922
|
}
|
|
1882
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
|
+
|
|
1883
2043
|
async function installInner(opts) {
|
|
1884
2044
|
const pkgVersion = readPackageVersion();
|
|
1885
2045
|
|
|
@@ -1929,10 +2089,12 @@ async function installInner(opts) {
|
|
|
1929
2089
|
console.error(' Currently supported:');
|
|
1930
2090
|
console.error(' claude — Claude Code native (recommended)');
|
|
1931
2091
|
console.error(' cursor — Cursor IDE');
|
|
1932
|
-
console.error('
|
|
2092
|
+
console.error(' codex — Codex CLI');
|
|
2093
|
+
console.error(' gemini — Gemini CLI (planned — not yet implemented)');
|
|
1933
2094
|
console.error(' vscode — VS Code (with Claude Code / Continue / Copilot extension)');
|
|
1934
2095
|
console.error(' windsurf — Windsurf (Codeium)');
|
|
1935
2096
|
console.error(' antigravity — Antigravity (experimental)');
|
|
2097
|
+
console.error(' grok — Grok Build (xAI CLI, Claude-Code-compatible)');
|
|
1936
2098
|
console.error('');
|
|
1937
2099
|
console.error(' Tracked for future:');
|
|
1938
2100
|
console.error(' jetbrains — IntelliJ / PyCharm');
|
|
@@ -1946,6 +2108,11 @@ async function installInner(opts) {
|
|
|
1946
2108
|
console.log(' ' + dim('VS Code → installing to .claude/ paths (read by Claude Code / Continue / Copilot extensions).'));
|
|
1947
2109
|
}
|
|
1948
2110
|
|
|
2111
|
+
// Codex installs to .claude/ and AGENTS.md; lifecycle via rcode workflow bridge.
|
|
2112
|
+
if (opts.ides.includes('codex')) {
|
|
2113
|
+
console.log(' ' + dim('Codex → installing to .claude/ paths + AGENTS.md. Use `rcode workflow show <name>` to feed workflows to Codex.'));
|
|
2114
|
+
}
|
|
2115
|
+
|
|
1949
2116
|
// Gemini IDE support deferred
|
|
1950
2117
|
if (opts.ides.includes('gemini')) {
|
|
1951
2118
|
console.log(`\n⚠️ Gemini CLI install not yet implemented\n`);
|
|
@@ -1994,6 +2161,15 @@ async function installInner(opts) {
|
|
|
1994
2161
|
console.log(` Modules: ${opts.modules.join(', ')}`);
|
|
1995
2162
|
}
|
|
1996
2163
|
|
|
2164
|
+
// Dry run / list-files — list paths that would be written and exit without writing
|
|
2165
|
+
if (opts.dryRun || opts.listFiles) {
|
|
2166
|
+
console.log('DRY RUN: the following paths would be written:');
|
|
2167
|
+
for (const entry of plan) {
|
|
2168
|
+
console.log(' + ' + entry.rel);
|
|
2169
|
+
}
|
|
2170
|
+
return 0;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
1997
2173
|
// Force-overwrite backup — closes #381. Without this, customized
|
|
1998
2174
|
// .claude/agents/rcode-*.md and similar package-managed files were silently
|
|
1999
2175
|
// clobbered with no recovery path. Now every --force-overwrite run creates
|
|
@@ -2262,6 +2438,16 @@ async function installInner(opts) {
|
|
|
2262
2438
|
} catch { /* non-fatal */ }
|
|
2263
2439
|
console.log('');
|
|
2264
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
|
+
}
|
|
2265
2451
|
return 0;
|
|
2266
2452
|
}
|
|
2267
2453
|
|
|
@@ -2269,9 +2455,9 @@ async function installInner(opts) {
|
|
|
2269
2455
|
// skip writing agents/commands to the project's .claude/ directory. Without this,
|
|
2270
2456
|
// running `npx rcode install` in the home dir AND then in a project creates two sets
|
|
2271
2457
|
// of identical files — Claude Code shows both as duplicate slash commands.
|
|
2272
|
-
const globalClaudeCommands = path.join(
|
|
2458
|
+
const globalClaudeCommands = path.join(homedir(), '.claude', 'commands');
|
|
2273
2459
|
const projectClaudeCommands = path.join(opts.target, '.claude', 'commands');
|
|
2274
|
-
const isProjectInstall = opts.target !==
|
|
2460
|
+
const isProjectInstall = opts.target !== homedir();
|
|
2275
2461
|
// Run dedup even when force:true — only forceOverwrite skips it.
|
|
2276
2462
|
if (isProjectInstall && !opts.forceOverwrite) {
|
|
2277
2463
|
try {
|
|
@@ -2453,7 +2639,7 @@ async function installInner(opts) {
|
|
|
2453
2639
|
}
|
|
2454
2640
|
|
|
2455
2641
|
// ~/.rcode/agents/ global agents directory
|
|
2456
|
-
const globalAgentsDir = path.join(
|
|
2642
|
+
const globalAgentsDir = path.join(homedir(), '.rcode', 'agents');
|
|
2457
2643
|
ensureDir(globalAgentsDir);
|
|
2458
2644
|
|
|
2459
2645
|
// Issue #702: files-manifest.csv used to be written here, BEFORE
|
|
@@ -2638,9 +2824,9 @@ async function installInner(opts) {
|
|
|
2638
2824
|
// the project skills folder may have only sidebar stubs while ~/.claude/
|
|
2639
2825
|
// has the real skills — health check should see those.
|
|
2640
2826
|
if (agentCount === 0 || commandCount === 0 || skillsInstalled < 20) {
|
|
2641
|
-
const homeAgents = path.join(
|
|
2642
|
-
const homeCommands = path.join(
|
|
2643
|
-
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');
|
|
2644
2830
|
if (agentCount === 0 && fs.existsSync(homeAgents)) {
|
|
2645
2831
|
// #669 — count both rcode-* and rcode-* prefixes; missing rcode-
|
|
2646
2832
|
// branch produced "Agents: 0" alongside "Skills: 120".
|
|
@@ -2667,6 +2853,32 @@ async function installInner(opts) {
|
|
|
2667
2853
|
console.error('[install] installInner: failed to count installed agents/commands:', err?.message || err);
|
|
2668
2854
|
}
|
|
2669
2855
|
|
|
2856
|
+
// Duplicate-namespace detection: warn when both rcode-* and rihal-* entries exist.
|
|
2857
|
+
// Having both doubles the roster size with near-identical content.
|
|
2858
|
+
try {
|
|
2859
|
+
const skillsDir = path.join(opts.target, '.rcode', 'skills');
|
|
2860
|
+
const claudeCommandsDir = path.join(opts.target, '.claude', 'commands');
|
|
2861
|
+
const dirsToCheck = [skillsDir, claudeCommandsDir];
|
|
2862
|
+
let hasRcode = false, hasRihal = false;
|
|
2863
|
+
for (const dir of dirsToCheck) {
|
|
2864
|
+
if (!fs.existsSync(dir)) continue;
|
|
2865
|
+
const entries = fs.readdirSync(dir);
|
|
2866
|
+
if (entries.some(e => e.startsWith('rcode-'))) hasRcode = true;
|
|
2867
|
+
if (entries.some(e => e.startsWith('rihal-'))) hasRihal = true;
|
|
2868
|
+
}
|
|
2869
|
+
if (hasRcode && hasRihal) {
|
|
2870
|
+
process.stderr.write(pc.yellow('WARNING: rcode-* and rihal-* namespaces both detected — consider removing one to reduce roster size.') + '\n');
|
|
2871
|
+
}
|
|
2872
|
+
} catch { /* non-fatal */ }
|
|
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
|
+
|
|
2670
2882
|
const version = readPackageVersion();
|
|
2671
2883
|
console.log('');
|
|
2672
2884
|
console.log(` ${bold('Version:')} ${pc.cyan('@hanzlaa/rcode@' + version)}`);
|
|
@@ -2915,6 +3127,7 @@ async function runInstallWizard(opts) {
|
|
|
2915
3127
|
options: [
|
|
2916
3128
|
{ value: 'claude', label: 'Claude Code', hint: 'recommended' },
|
|
2917
3129
|
{ value: 'cursor', label: 'Cursor' },
|
|
3130
|
+
{ value: 'codex', label: 'Codex (OpenAI CLI)', hint: 'AGENTS.md + workflow bridge' },
|
|
2918
3131
|
{ value: 'gemini', label: 'Gemini CLI', hint: 'coming soon' },
|
|
2919
3132
|
{ value: 'vscode', label: 'VS Code', hint: 'via Continue / Copilot extensions' },
|
|
2920
3133
|
{ value: 'antigravity', label: 'Antigravity', hint: 'experimental' },
|
|
@@ -3016,3 +3229,8 @@ module.exports.install = install;
|
|
|
3016
3229
|
module.exports.SUPPORTED_IDES = SUPPORTED_IDES;
|
|
3017
3230
|
module.exports.migrateVscodeCommandsLayout = migrateVscodeCommandsLayout;
|
|
3018
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);
|