@hanzlaa/rcode 3.4.27 → 3.4.29
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 +10 -0
- package/cli/install.js +103 -18
- package/cli/lib/manifest.cjs +13 -7
- package/cli/uninstall.js +73 -1
- package/cli/update.js +94 -14
- package/dist/rcode.js +179 -34
- package/package.json +1 -1
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`, 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`, 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
|
@@ -298,6 +298,16 @@ We use [Conventional Commits](https://www.conventionalcommits.org/) format. The
|
|
|
298
298
|
- `extensibility` — extensibility and plugin hooks
|
|
299
299
|
- `lens-audit` — 15-lens audit system and lenses
|
|
300
300
|
- `tiers` — TIERS.md and tier-related documentation
|
|
301
|
+
- `build` — `scripts/build.cjs`, esbuild config, bundle artifacts
|
|
302
|
+
- `council` — `/rihal-council` workflow + spawning logic
|
|
303
|
+
- `doctor` — `cli/doctor.js` health checks
|
|
304
|
+
- `postinstall` — `cli/postinstall.js` lifecycle hook
|
|
305
|
+
- `progress` — `/rihal-progress` workflow
|
|
306
|
+
- `security` — security guardrails (symlink guards, integrity checks)
|
|
307
|
+
- `test` — test files under `test/` (test-only changes)
|
|
308
|
+
- `tools` — `rihal/bin/rihal-tools.cjs` subcommands
|
|
309
|
+
- `uninstall` — `cli/uninstall.js` flow
|
|
310
|
+
- `update` — `cli/update.js` flow
|
|
301
311
|
- `<phase-id>` — numeric phase scope when committing inside a phase (e.g. `docs(15)`, `feat(8.3)`)
|
|
302
312
|
- `<sprint-id>` — numeric sprint scope inside a phase (e.g. `feat(15.1)`)
|
|
303
313
|
|
package/cli/install.js
CHANGED
|
@@ -649,13 +649,28 @@ function seedStarterPlanning(target, projectName) {
|
|
|
649
649
|
// - sprint tools that previously relied on phase 01 will surface a clear
|
|
650
650
|
// "no phases yet — run /rihal-new-project first" error instead of
|
|
651
651
|
// silently operating on a fake phase
|
|
652
|
+
//
|
|
653
|
+
// Issue #705: only mark _seeded_stub when the planning ROADMAP is also
|
|
654
|
+
// a stub. If the user manually deletes state.json but has real
|
|
655
|
+
// .planning/ROADMAP.md (no INSTALL STUB banner), seeding _seeded_stub
|
|
656
|
+
// would mis-classify a real project as fresh and let /rihal-new-project
|
|
657
|
+
// overwrite it. Guard with the banner check.
|
|
652
658
|
const rihalStateJson = path.join(target, '.rihal', 'state.json');
|
|
659
|
+
function planningRoadmapIsStub() {
|
|
660
|
+
const rmPath = path.join(target, '.planning', 'ROADMAP.md');
|
|
661
|
+
if (!fs.existsSync(rmPath)) return true; // missing → fresh install case
|
|
662
|
+
try {
|
|
663
|
+
const text = fs.readFileSync(rmPath, 'utf8');
|
|
664
|
+
return text.includes('<!-- INSTALL STUB');
|
|
665
|
+
} catch { return true; }
|
|
666
|
+
}
|
|
653
667
|
if (!fs.existsSync(rihalStateJson)) {
|
|
654
668
|
const now = new Date().toISOString();
|
|
669
|
+
const isStubProject = planningRoadmapIsStub();
|
|
655
670
|
const state = {
|
|
656
671
|
version: '1',
|
|
657
672
|
project: null,
|
|
658
|
-
_seeded_stub: true,
|
|
673
|
+
...(isStubProject ? { _seeded_stub: true } : {}),
|
|
659
674
|
created: now,
|
|
660
675
|
updated: now,
|
|
661
676
|
current_phase: null,
|
|
@@ -1260,7 +1275,7 @@ function generateAgentManifest(plan, target) {
|
|
|
1260
1275
|
* Generate files-manifest.csv with SHA256 per installed file. Used by
|
|
1261
1276
|
* update/doctor to detect drift. Columns: rel, sha256, size.
|
|
1262
1277
|
*/
|
|
1263
|
-
function generateFilesManifest(plan, target, { mergeExistingManifest = false } = {}) {
|
|
1278
|
+
function generateFilesManifest(plan, target, { mergeExistingManifest = false, extraScanDirs = [] } = {}) {
|
|
1264
1279
|
const rows = [['rel', 'sha256', 'size']];
|
|
1265
1280
|
const newRels = new Set();
|
|
1266
1281
|
|
|
@@ -1273,6 +1288,34 @@ function generateFilesManifest(plan, target, { mergeExistingManifest = false } =
|
|
|
1273
1288
|
newRels.add(rel);
|
|
1274
1289
|
}
|
|
1275
1290
|
|
|
1291
|
+
// Issue #702: skills installed via installSkills() and sidebar stubs
|
|
1292
|
+
// generated by cli/generate-command-skills.cjs are NOT in the install plan
|
|
1293
|
+
// (they're walked from rihal/skills/ separately and copied directly).
|
|
1294
|
+
// Without this scan, files-manifest.csv was missing the largest category
|
|
1295
|
+
// of installed files — orphan sweep + doctor drift detection were blind
|
|
1296
|
+
// to renamed/removed skills.
|
|
1297
|
+
function walkScanDir(absDir) {
|
|
1298
|
+
if (!fs.existsSync(absDir)) return;
|
|
1299
|
+
for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
|
|
1300
|
+
const full = path.join(absDir, entry.name);
|
|
1301
|
+
if (entry.isDirectory()) {
|
|
1302
|
+
walkScanDir(full);
|
|
1303
|
+
} else if (entry.isFile()) {
|
|
1304
|
+
const rel = path.relative(target, full).split(path.sep).join('/');
|
|
1305
|
+
if (newRels.has(rel)) continue; // already in plan
|
|
1306
|
+
// Skip files outside the project root (defense-in-depth — extraScanDirs
|
|
1307
|
+
// is a code-controlled set, but cheap to verify).
|
|
1308
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) continue;
|
|
1309
|
+
try {
|
|
1310
|
+
const buf = fs.readFileSync(full);
|
|
1311
|
+
rows.push([rel, sha256(buf), String(buf.length)]);
|
|
1312
|
+
newRels.add(rel);
|
|
1313
|
+
} catch { /* unreadable file — skip */ }
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
for (const scan of extraScanDirs) walkScanDir(scan);
|
|
1318
|
+
|
|
1276
1319
|
// Merge old manifest entries that are still on disk but not in the current
|
|
1277
1320
|
// plan — this keeps orphaned files traceable by doctor/uninstall even when
|
|
1278
1321
|
// --force sweep was not run. Without this, a re-install without --force
|
|
@@ -1336,20 +1379,26 @@ function sweepStaleInstalledFiles(target, newPlan) {
|
|
|
1336
1379
|
|
|
1337
1380
|
let removed = 0;
|
|
1338
1381
|
const emptyCandidateDirs = new Set();
|
|
1382
|
+
// Issue #703: a tampered or malformed CSV could contain a rel like
|
|
1383
|
+
// '../../etc/passwd'. path.join collapses '..' segments and could escape
|
|
1384
|
+
// the project root. Use safeRmSync's project-root containment check —
|
|
1385
|
+
// any rel whose realpath escapes target is refused with reason='outside-root'.
|
|
1386
|
+
const targetRoot = path.resolve(target);
|
|
1339
1387
|
for (const rel of oldRels) {
|
|
1340
1388
|
if (newRelsSet.has(rel)) continue;
|
|
1341
1389
|
if (neverSweep.test(rel)) continue;
|
|
1342
1390
|
if (isLocalOverride(rel)) continue; // #382 — never sweep user-owned overrides
|
|
1391
|
+
// Reject relative paths that obviously try to escape before even hitting fs.
|
|
1392
|
+
if (rel.includes('..') || path.isAbsolute(rel)) continue;
|
|
1343
1393
|
const full = path.join(target, rel);
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
}
|
|
1350
|
-
} catch {
|
|
1351
|
-
// ignore individual failures — sweep is best-effort
|
|
1394
|
+
if (!fs.existsSync(full)) continue;
|
|
1395
|
+
const result = safeRmSync(full, targetRoot);
|
|
1396
|
+
if (result.ok) {
|
|
1397
|
+
emptyCandidateDirs.add(path.dirname(full));
|
|
1398
|
+
removed += 1;
|
|
1352
1399
|
}
|
|
1400
|
+
// outside-root / lstat / unlink failures are silently skipped — sweep is
|
|
1401
|
+
// best-effort and we never want to abort the install on a single bad row.
|
|
1353
1402
|
}
|
|
1354
1403
|
|
|
1355
1404
|
// Remove any now-empty parent dirs (bottom-up, so nested emptiness cascades).
|
|
@@ -2073,9 +2122,28 @@ async function installInner(opts) {
|
|
|
2073
2122
|
const stateSrc = path.join(SOURCE_ROOT, 'state.json');
|
|
2074
2123
|
if (fs.existsSync(stateSrc)) {
|
|
2075
2124
|
const now = new Date().toISOString();
|
|
2076
|
-
|
|
2125
|
+
let stateContent = fs.readFileSync(stateSrc, 'utf8')
|
|
2077
2126
|
.replace(/__PROJECT_NAME__/g, opts.projectName)
|
|
2078
2127
|
.replace(/__INSTALL_DATE__/g, now);
|
|
2128
|
+
|
|
2129
|
+
// Issue #705: the template ships with _seeded_stub:true. If the user
|
|
2130
|
+
// already has a real planning ROADMAP (no INSTALL STUB banner) but
|
|
2131
|
+
// state.json is missing (manually deleted), restoring with the stub
|
|
2132
|
+
// marker would mis-classify a real project as fresh. Strip the marker
|
|
2133
|
+
// when ROADMAP exists and isn't itself a stub.
|
|
2134
|
+
const rmPath = path.join(opts.target, '.planning', 'ROADMAP.md');
|
|
2135
|
+
if (fs.existsSync(rmPath)) {
|
|
2136
|
+
try {
|
|
2137
|
+
const rm = fs.readFileSync(rmPath, 'utf8');
|
|
2138
|
+
if (!rm.includes('<!-- INSTALL STUB')) {
|
|
2139
|
+
// Remove "_seeded_stub": true, line. JSON is small + flat enough
|
|
2140
|
+
// to do this with a regex; matches whether the field is followed
|
|
2141
|
+
// by a comma or sits as the last key.
|
|
2142
|
+
stateContent = stateContent.replace(/^\s*"_seeded_stub":\s*true,?\s*\n/m, '');
|
|
2143
|
+
}
|
|
2144
|
+
} catch { /* fall through with stub marker — safe default */ }
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2079
2147
|
ensureDir(path.dirname(stateDest));
|
|
2080
2148
|
writeFileAtomic(stateDest, stateContent);
|
|
2081
2149
|
}
|
|
@@ -2101,13 +2169,12 @@ async function installInner(opts) {
|
|
|
2101
2169
|
const globalAgentsDir = path.join(os.homedir(), '.rihal', 'agents');
|
|
2102
2170
|
ensureDir(globalAgentsDir);
|
|
2103
2171
|
|
|
2104
|
-
// files-manifest.csv
|
|
2105
|
-
//
|
|
2106
|
-
//
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
);
|
|
2172
|
+
// Issue #702: files-manifest.csv used to be written here, BEFORE
|
|
2173
|
+
// installSkills + generateCommandSkills ran. The 100+ skill files those
|
|
2174
|
+
// functions install were therefore invisible to sweepStaleInstalledFiles
|
|
2175
|
+
// and doctor's drift detection. Manifest generation moved below to AFTER
|
|
2176
|
+
// all skill installations complete, with extraScanDirs covering both
|
|
2177
|
+
// .claude/skills/ and .rihal/skills/ on disk.
|
|
2111
2178
|
|
|
2112
2179
|
// Install v1-style phrase-activated skills (scaffold-project, create-prd,
|
|
2113
2180
|
// retrospective, etc.) into .claude/skills/ alongside the v2 agents/commands.
|
|
@@ -2144,6 +2211,20 @@ async function installInner(opts) {
|
|
|
2144
2211
|
console.log(' ' + dim(`(sidebar stub generation skipped: ${err.message})`));
|
|
2145
2212
|
}
|
|
2146
2213
|
|
|
2214
|
+
// Issue #702: write files-manifest.csv NOW, after all installs complete.
|
|
2215
|
+
// extraScanDirs picks up the skills + sidebar stubs that aren't in the
|
|
2216
|
+
// plan array.
|
|
2217
|
+
fs.writeFileSync(
|
|
2218
|
+
path.join(configDir, 'files-manifest.csv'),
|
|
2219
|
+
generateFilesManifest(plan, opts.target, {
|
|
2220
|
+
mergeExistingManifest: !opts.force,
|
|
2221
|
+
extraScanDirs: [
|
|
2222
|
+
path.join(opts.target, '.claude', 'skills'),
|
|
2223
|
+
path.join(opts.target, '.rihal', 'skills'),
|
|
2224
|
+
],
|
|
2225
|
+
}),
|
|
2226
|
+
);
|
|
2227
|
+
|
|
2147
2228
|
// Seed .planning/ with starter ROADMAP + STATE so workflows work immediately
|
|
2148
2229
|
const starterSeeded = seedStarterPlanning(opts.target, opts.projectName);
|
|
2149
2230
|
|
|
@@ -2167,10 +2248,14 @@ async function installInner(opts) {
|
|
|
2167
2248
|
const { execFileSync } = require('child_process');
|
|
2168
2249
|
const toolsPath = path.join(opts.target, '.rihal', 'bin', 'rihal-tools.cjs');
|
|
2169
2250
|
if (fs.existsSync(toolsPath)) {
|
|
2251
|
+
// Issue #706: 60s timeout — without it, a slow upstream URL hangs the
|
|
2252
|
+
// entire install indefinitely. Brain pull is best-effort, so a timeout
|
|
2253
|
+
// failure is treated identically to any other pull failure (caught below).
|
|
2170
2254
|
const out = execFileSync('node', [toolsPath, 'brain', 'pull'], {
|
|
2171
2255
|
cwd: opts.target,
|
|
2172
2256
|
encoding: 'utf8',
|
|
2173
2257
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2258
|
+
timeout: 60_000,
|
|
2174
2259
|
});
|
|
2175
2260
|
try { brainReport = JSON.parse(out); } catch {}
|
|
2176
2261
|
}
|
package/cli/lib/manifest.cjs
CHANGED
|
@@ -109,7 +109,12 @@ function diffSet(editor, kind, expected, installed) {
|
|
|
109
109
|
* makes doctor report drift like "agents 119/23" when nothing is wrong.
|
|
110
110
|
* That's why the agent count comes from .claude/agents/, not .claude/skills/.
|
|
111
111
|
*/
|
|
112
|
-
function verifyClaudeInstall(cwd, packageRoot) {
|
|
112
|
+
function verifyClaudeInstall(cwd, packageRoot, options = {}) {
|
|
113
|
+
// Issue #698: tests assert against an isolated tempdir cwd. The global
|
|
114
|
+
// fallback (#664) makes that impossible because it reads the contributor's
|
|
115
|
+
// real ~/.claude/. Tests can pass { globalFallback: false } to disable it.
|
|
116
|
+
// Default remains true to preserve the runtime behavior introduced in #664.
|
|
117
|
+
const globalFallback = options.globalFallback !== false;
|
|
113
118
|
const pkg = readPackageManifest(packageRoot);
|
|
114
119
|
const agentsDir = path.join(cwd, '.claude/agents');
|
|
115
120
|
const skillsDir = path.join(cwd, '.claude/skills');
|
|
@@ -129,7 +134,7 @@ function verifyClaudeInstall(cwd, packageRoot) {
|
|
|
129
134
|
// level .claude/agents/rihal-*.md when the user's ~/.claude/ already has
|
|
130
135
|
// them, to avoid duplicate commands. Without this fallback the verifier
|
|
131
136
|
// reports 0 agents on every successful install in that scenario.
|
|
132
|
-
if (installedAgents.size === 0) {
|
|
137
|
+
if (installedAgents.size === 0 && globalFallback) {
|
|
133
138
|
try {
|
|
134
139
|
const os = require('os');
|
|
135
140
|
const globalAgentsDir = path.join(os.homedir(), '.claude/agents');
|
|
@@ -143,12 +148,13 @@ function verifyClaudeInstall(cwd, packageRoot) {
|
|
|
143
148
|
} catch { /* non-fatal — permission errors etc. */ }
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
// Actions: .claude/skills
|
|
147
|
-
//
|
|
151
|
+
// Actions: .claude/skills/rihal-<name>/. installSkills (cli/install.js)
|
|
152
|
+
// prefixes every action with rihal-, and readPackageManifest does the
|
|
153
|
+
// same — so both sides are normalized. The previous version filtered OUT
|
|
154
|
+
// rihal-* dirs which excluded ALL real actions and made the diff always
|
|
155
|
+
// report "everything missing." Compare directly against the prefixed set.
|
|
148
156
|
const allInstalled = readInstalledDirs(skillsDir);
|
|
149
|
-
const actionsInstalled = new Set(
|
|
150
|
-
[...allInstalled].filter((n) => !n.startsWith('rihal-'))
|
|
151
|
-
);
|
|
157
|
+
const actionsInstalled = new Set([...allInstalled].filter((n) => pkg.actions.has(n)));
|
|
152
158
|
|
|
153
159
|
return [
|
|
154
160
|
diffSet('claude', 'agents', pkg.agents, installedAgents),
|
package/cli/uninstall.js
CHANGED
|
@@ -152,11 +152,22 @@ function buildPlan(cwd, editors) {
|
|
|
152
152
|
cursor: [],
|
|
153
153
|
windsurf: [],
|
|
154
154
|
antigravity: [],
|
|
155
|
+
gemini: [], // #706 — added when --editor=gemini or --editor=all
|
|
156
|
+
vscode: [], // #706 — vscode marker dir cleanup (commands share .claude/)
|
|
155
157
|
agentsMd: null, // null = no section; 'present' = section present
|
|
156
158
|
stateDir: null, // null = missing; { files: N } = present
|
|
157
159
|
planningDir: null, // null = missing; { files: N } = present
|
|
158
160
|
};
|
|
159
161
|
|
|
162
|
+
// Issue #706: vscode and gemini are in SUPPORTED_IDES but uninstall.js had
|
|
163
|
+
// no branches for them. vscode shares .claude/ for commands+agents+skills
|
|
164
|
+
// — fold into the claude branch. gemini has its own .gemini/rihal/ tree.
|
|
165
|
+
if (editors.includes('vscode')) {
|
|
166
|
+
if (!editors.includes('claude')) editors.push('claude'); // share scan
|
|
167
|
+
const markerDir = path.join(cwd, '.vscode/rihal');
|
|
168
|
+
if (fs.existsSync(markerDir)) plan.vscode.push('.vscode/rihal');
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
if (editors.includes('claude')) {
|
|
161
172
|
const skillsDir = path.join(cwd, '.claude/skills');
|
|
162
173
|
if (fs.existsSync(skillsDir)) {
|
|
@@ -212,6 +223,20 @@ function buildPlan(cwd, editors) {
|
|
|
212
223
|
}
|
|
213
224
|
}
|
|
214
225
|
|
|
226
|
+
if (editors.includes('gemini')) {
|
|
227
|
+
// #706 — gemini installs to .gemini/rihal/{agents,commands}
|
|
228
|
+
for (const sub of ['agents', 'commands']) {
|
|
229
|
+
const dir = path.join(cwd, '.gemini', 'rihal', sub);
|
|
230
|
+
if (fs.existsSync(dir)) {
|
|
231
|
+
for (const name of fs.readdirSync(dir)) {
|
|
232
|
+
if (name.startsWith('rihal-') || name.endsWith('.md')) {
|
|
233
|
+
plan.gemini.push(path.join('.gemini/rihal', sub, name));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
215
240
|
// Check AGENTS.md for Rihal section
|
|
216
241
|
const agentsMdPath = path.join(cwd, 'AGENTS.md');
|
|
217
242
|
if (fs.existsSync(agentsMdPath)) {
|
|
@@ -284,7 +309,23 @@ function planToPathList(plan, cwd, options = {}) {
|
|
|
284
309
|
for (const name of plan.claude.skills) {
|
|
285
310
|
paths.push(path.join('.claude/skills', name));
|
|
286
311
|
}
|
|
287
|
-
|
|
312
|
+
// Issue #704: claude IDE installs slash commands as flat
|
|
313
|
+
// .claude/commands/rihal-*.md files (post-#697 layout). The previous
|
|
314
|
+
// backup only added the legacy '.claude/commands/rihal' subdir, so on
|
|
315
|
+
// any modern claude install the tarball was missing every slash command.
|
|
316
|
+
// Add each flat file individually if present, plus the legacy subdir.
|
|
317
|
+
for (const name of plan.claude.commands) {
|
|
318
|
+
// plan.claude.commands holds entries from BOTH layouts:
|
|
319
|
+
// - 'rihal-foo.md' (claude flat)
|
|
320
|
+
// - 'foo.md' (vscode subdir)
|
|
321
|
+
// Disambiguate by the rihal- prefix.
|
|
322
|
+
if (name.startsWith('rihal-') && name.endsWith('.md')) {
|
|
323
|
+
paths.push(path.join('.claude/commands', name));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Legacy vscode-style subdir is added once if any subdir entries exist.
|
|
327
|
+
const hasSubdirCommand = plan.claude.commands.some(n => !n.startsWith('rihal-'));
|
|
328
|
+
if (hasSubdirCommand) {
|
|
288
329
|
paths.push('.claude/commands/rihal');
|
|
289
330
|
}
|
|
290
331
|
for (const name of plan.claude.agents) {
|
|
@@ -299,6 +340,13 @@ function planToPathList(plan, cwd, options = {}) {
|
|
|
299
340
|
for (const name of plan.antigravity) {
|
|
300
341
|
paths.push(path.join('.antigravity/agents', name));
|
|
301
342
|
}
|
|
343
|
+
// #706 — gemini paths are already relative (built that way in buildPlan).
|
|
344
|
+
if (Array.isArray(plan.gemini)) {
|
|
345
|
+
for (const rel of plan.gemini) paths.push(rel);
|
|
346
|
+
}
|
|
347
|
+
if (Array.isArray(plan.vscode)) {
|
|
348
|
+
for (const rel of plan.vscode) paths.push(rel);
|
|
349
|
+
}
|
|
302
350
|
// AGENTS.md is mutated (stripped), not deleted — but we back it up so the
|
|
303
351
|
// user can restore the stripped content.
|
|
304
352
|
if (plan.agentsMd && fs.existsSync(path.join(cwd, 'AGENTS.md'))) {
|
|
@@ -638,6 +686,30 @@ async function runUninstall(args) {
|
|
|
638
686
|
if (n > 0) console.log(` ✓ removed ${n} Antigravity agents`);
|
|
639
687
|
}
|
|
640
688
|
|
|
689
|
+
// #706 — gemini removal (.gemini/rihal/{agents,commands})
|
|
690
|
+
if (editors.includes('gemini')) {
|
|
691
|
+
let n = 0;
|
|
692
|
+
for (const sub of ['agents', 'commands']) {
|
|
693
|
+
const dir = path.join(cwd, '.gemini', 'rihal', sub);
|
|
694
|
+
n += removeMatching(dir, (name) => name.startsWith('rihal-') || name.endsWith('.md'));
|
|
695
|
+
}
|
|
696
|
+
removed += n;
|
|
697
|
+
if (n > 0) console.log(` ✓ removed ${n} Gemini files`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// #706 — vscode marker dir cleanup. Commands+skills+agents share .claude/
|
|
701
|
+
// and were already removed under the claude branch.
|
|
702
|
+
if (editors.includes('vscode')) {
|
|
703
|
+
const markerDir = path.join(cwd, '.vscode/rihal');
|
|
704
|
+
if (fs.existsSync(markerDir)) {
|
|
705
|
+
const r = safeRmSync(markerDir, path.resolve(cwd));
|
|
706
|
+
if (r.ok) {
|
|
707
|
+
removed += 1;
|
|
708
|
+
console.log(` ✓ removed .vscode/rihal/ marker`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
641
713
|
// Strip AGENTS.md section
|
|
642
714
|
if (plan.agentsMd) {
|
|
643
715
|
const agentsMdPath = path.join(cwd, 'AGENTS.md');
|
package/cli/update.js
CHANGED
|
@@ -33,10 +33,51 @@ const path = require('path');
|
|
|
33
33
|
const { spawnSync } = require('child_process');
|
|
34
34
|
const clack = require('@clack/prompts');
|
|
35
35
|
const { PromptAbortError } = require('./lib/prompts.cjs');
|
|
36
|
-
const {
|
|
36
|
+
const { writeFileAtomic } = require('./lib/fsutil.cjs');
|
|
37
37
|
const { verifyInstall, formatReport } = require('./lib/manifest.cjs');
|
|
38
38
|
const install = require('./install');
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Issue #701: update.js used to read .rihal/config.json with JSON.parse, but
|
|
42
|
+
* the installer writes .rihal/config.yaml. So `rcode update` errored on
|
|
43
|
+
* every real install. Read config.yaml with the same minimal parser the
|
|
44
|
+
* installer uses, and write it back as YAML preserving every other field.
|
|
45
|
+
*/
|
|
46
|
+
function readConfigYaml(configPath) {
|
|
47
|
+
const text = fs.readFileSync(configPath, 'utf8');
|
|
48
|
+
const obj = {};
|
|
49
|
+
for (const raw of text.split('\n')) {
|
|
50
|
+
const line = raw.replace(/#.*$/, '').trimEnd();
|
|
51
|
+
if (!line) continue;
|
|
52
|
+
if (line.startsWith(' ')) continue; // ignore nested keys (rare; preserved on disk via raw text path)
|
|
53
|
+
const colonAt = line.indexOf(':');
|
|
54
|
+
if (colonAt === -1) continue;
|
|
55
|
+
const key = line.slice(0, colonAt).trim();
|
|
56
|
+
let val = line.slice(colonAt + 1).trim();
|
|
57
|
+
if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
|
|
58
|
+
if (val === 'true') val = true;
|
|
59
|
+
else if (val === 'false') val = false;
|
|
60
|
+
obj[key] = val;
|
|
61
|
+
}
|
|
62
|
+
return { obj, raw: text };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Surgically update a single key in a YAML config without rewriting the
|
|
67
|
+
* whole file (preserves comments, ordering, and nested keys we don't parse).
|
|
68
|
+
* If the key doesn't exist, append it.
|
|
69
|
+
*/
|
|
70
|
+
function setYamlKey(rawText, key, value) {
|
|
71
|
+
const re = new RegExp(`^${key}:\\s*.*$`, 'm');
|
|
72
|
+
const replacement = typeof value === 'string'
|
|
73
|
+
? `${key}: "${value.replace(/"/g, '\\"')}"`
|
|
74
|
+
: `${key}: ${value}`;
|
|
75
|
+
if (re.test(rawText)) {
|
|
76
|
+
return rawText.replace(re, replacement);
|
|
77
|
+
}
|
|
78
|
+
return rawText.replace(/\n*$/, '') + `\n${replacement}\n`;
|
|
79
|
+
}
|
|
80
|
+
|
|
40
81
|
function parseArgs(args) {
|
|
41
82
|
const opts = { yes: false };
|
|
42
83
|
for (const arg of args) {
|
|
@@ -51,12 +92,29 @@ function parseArgs(args) {
|
|
|
51
92
|
*/
|
|
52
93
|
function detectInstalledEditors(cwd) {
|
|
53
94
|
const editors = [];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
95
|
+
|
|
96
|
+
// Issue #701: post-#679 dedup leaves .claude/skills/ empty when ~/.claude/
|
|
97
|
+
// already has the rihal-* set. Detect "claude install" by looking at
|
|
98
|
+
// .rihal/config.yaml as the canonical signal — if config exists, the
|
|
99
|
+
// project ran rcode install at least once for claude. The presence of
|
|
100
|
+
// any commands/agents/skills then becomes secondary evidence.
|
|
101
|
+
const os = require('os');
|
|
102
|
+
const homeSkills = path.join(os.homedir(), '.claude/skills');
|
|
103
|
+
const projectClaude = (
|
|
104
|
+
(fs.existsSync(path.join(cwd, '.claude/skills')) &&
|
|
105
|
+
fs.readdirSync(path.join(cwd, '.claude/skills')).some(n => n.startsWith('rihal-'))) ||
|
|
106
|
+
(fs.existsSync(path.join(cwd, '.claude/agents')) &&
|
|
107
|
+
fs.readdirSync(path.join(cwd, '.claude/agents')).some(n => n.startsWith('rihal-'))) ||
|
|
108
|
+
(fs.existsSync(path.join(cwd, '.claude/commands')) &&
|
|
109
|
+
fs.readdirSync(path.join(cwd, '.claude/commands')).some(n => n.startsWith('rihal-')))
|
|
110
|
+
);
|
|
111
|
+
const globalClaude = (
|
|
112
|
+
fs.existsSync(homeSkills) &&
|
|
113
|
+
fs.readdirSync(homeSkills).some(n => n.startsWith('rihal-'))
|
|
114
|
+
);
|
|
115
|
+
const configIndicatesClaude = fs.existsSync(path.join(cwd, '.rihal/config.yaml'));
|
|
116
|
+
if (projectClaude || (configIndicatesClaude && globalClaude)) editors.push('claude');
|
|
117
|
+
|
|
60
118
|
if (fs.existsSync(path.join(cwd, '.cursor/rules'))) {
|
|
61
119
|
const hasRihal = fs
|
|
62
120
|
.readdirSync(path.join(cwd, '.cursor/rules'))
|
|
@@ -241,7 +299,13 @@ async function runUpdate(args, { packageRoot, packageJson }) {
|
|
|
241
299
|
const packageVersion = packageJson?.version || '0.0.0';
|
|
242
300
|
|
|
243
301
|
// ------ Sanity: must be installed ------
|
|
244
|
-
|
|
302
|
+
// Issue #701: installer writes .rihal/config.yaml, not config.json.
|
|
303
|
+
// Tolerate both shapes for users who upgrade across the rename window,
|
|
304
|
+
// but the canonical path is YAML.
|
|
305
|
+
const configYamlPath = path.join(cwd, '.rihal/config.yaml');
|
|
306
|
+
const configJsonPath = path.join(cwd, '.rihal/config.json');
|
|
307
|
+
const configPath = fs.existsSync(configYamlPath) ? configYamlPath : configJsonPath;
|
|
308
|
+
|
|
245
309
|
if (!fs.existsSync(configPath)) {
|
|
246
310
|
console.error(`\n❌ Rihal Code is not installed in this directory.`);
|
|
247
311
|
console.error(` To install: rcode install\n`);
|
|
@@ -249,10 +313,18 @@ async function runUpdate(args, { packageRoot, packageJson }) {
|
|
|
249
313
|
}
|
|
250
314
|
|
|
251
315
|
let config;
|
|
316
|
+
let configRaw = '';
|
|
317
|
+
const isYaml = configPath.endsWith('.yaml');
|
|
252
318
|
try {
|
|
253
|
-
|
|
319
|
+
if (isYaml) {
|
|
320
|
+
const parsed = readConfigYaml(configPath);
|
|
321
|
+
config = parsed.obj;
|
|
322
|
+
configRaw = parsed.raw;
|
|
323
|
+
} else {
|
|
324
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
325
|
+
}
|
|
254
326
|
} catch (err) {
|
|
255
|
-
console.error(`\n❌ .
|
|
327
|
+
console.error(`\n❌ ${path.basename(configPath)} is not parseable: ${err.message}\n`);
|
|
256
328
|
process.exit(1);
|
|
257
329
|
}
|
|
258
330
|
|
|
@@ -356,10 +428,18 @@ async function runUpdate(args, { packageRoot, packageJson }) {
|
|
|
356
428
|
console.log(` ✓ [${supportedIdes.join(', ')}] → refreshed via install.js`);
|
|
357
429
|
}
|
|
358
430
|
|
|
359
|
-
// ------ Update installed_version in config
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
431
|
+
// ------ Update installed_version in config (atomic) ------
|
|
432
|
+
// Issue #701: write back in the same format we read. YAML uses surgical
|
|
433
|
+
// single-key replace so other fields, comments, ordering survive.
|
|
434
|
+
if (isYaml) {
|
|
435
|
+
const updated = setYamlKey(configRaw, 'installed_version', packageVersion);
|
|
436
|
+
writeFileAtomic(configPath, updated);
|
|
437
|
+
} else {
|
|
438
|
+
config.installed_version = packageVersion;
|
|
439
|
+
const { writeJsonAtomic } = require('./lib/fsutil.cjs');
|
|
440
|
+
writeJsonAtomic(configPath, config);
|
|
441
|
+
}
|
|
442
|
+
console.log(` ✓ ${path.relative(cwd, configPath)} → installed_version: ${packageVersion}`);
|
|
363
443
|
|
|
364
444
|
// ------ Verify manifest ------
|
|
365
445
|
console.log();
|
package/dist/rcode.js
CHANGED
|
@@ -15073,7 +15073,8 @@ var require_manifest = __commonJS({
|
|
|
15073
15073
|
extra
|
|
15074
15074
|
};
|
|
15075
15075
|
}
|
|
15076
|
-
function verifyClaudeInstall(cwd, packageRoot) {
|
|
15076
|
+
function verifyClaudeInstall(cwd, packageRoot, options = {}) {
|
|
15077
|
+
const globalFallback = options.globalFallback !== false;
|
|
15077
15078
|
const pkg = readPackageManifest(packageRoot);
|
|
15078
15079
|
const agentsDir = path2.join(cwd, ".claude/agents");
|
|
15079
15080
|
const skillsDir = path2.join(cwd, ".claude/skills");
|
|
@@ -15085,7 +15086,7 @@ var require_manifest = __commonJS({
|
|
|
15085
15086
|
}
|
|
15086
15087
|
}
|
|
15087
15088
|
}
|
|
15088
|
-
if (installedAgents.size === 0) {
|
|
15089
|
+
if (installedAgents.size === 0 && globalFallback) {
|
|
15089
15090
|
try {
|
|
15090
15091
|
const os = require("os");
|
|
15091
15092
|
const globalAgentsDir = path2.join(os.homedir(), ".claude/agents");
|
|
@@ -15100,9 +15101,7 @@ var require_manifest = __commonJS({
|
|
|
15100
15101
|
}
|
|
15101
15102
|
}
|
|
15102
15103
|
const allInstalled = readInstalledDirs(skillsDir);
|
|
15103
|
-
const actionsInstalled = new Set(
|
|
15104
|
-
[...allInstalled].filter((n) => !n.startsWith("rihal-"))
|
|
15105
|
-
);
|
|
15104
|
+
const actionsInstalled = new Set([...allInstalled].filter((n) => pkg.actions.has(n)));
|
|
15106
15105
|
return [
|
|
15107
15106
|
diffSet("claude", "agents", pkg.agents, installedAgents),
|
|
15108
15107
|
diffSet("claude", "actions", pkg.actions, actionsInstalled)
|
|
@@ -15654,12 +15653,23 @@ Run \`/rihal-new-project <description>\` to bootstrap, or \`/rihal-sprint-planni
|
|
|
15654
15653
|
`
|
|
15655
15654
|
);
|
|
15656
15655
|
const rihalStateJson = path2.join(target, ".rihal", "state.json");
|
|
15656
|
+
function planningRoadmapIsStub() {
|
|
15657
|
+
const rmPath = path2.join(target, ".planning", "ROADMAP.md");
|
|
15658
|
+
if (!fs2.existsSync(rmPath)) return true;
|
|
15659
|
+
try {
|
|
15660
|
+
const text = fs2.readFileSync(rmPath, "utf8");
|
|
15661
|
+
return text.includes("<!-- INSTALL STUB");
|
|
15662
|
+
} catch {
|
|
15663
|
+
return true;
|
|
15664
|
+
}
|
|
15665
|
+
}
|
|
15657
15666
|
if (!fs2.existsSync(rihalStateJson)) {
|
|
15658
15667
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15668
|
+
const isStubProject = planningRoadmapIsStub();
|
|
15659
15669
|
const state = {
|
|
15660
15670
|
version: "1",
|
|
15661
15671
|
project: null,
|
|
15662
|
-
_seeded_stub: true,
|
|
15672
|
+
...isStubProject ? { _seeded_stub: true } : {},
|
|
15663
15673
|
created: now,
|
|
15664
15674
|
updated: now,
|
|
15665
15675
|
current_phase: null,
|
|
@@ -16103,7 +16113,7 @@ ${BLOCK}`, { mode: 493 });
|
|
|
16103
16113
|
}
|
|
16104
16114
|
return rows.map((r) => r.join(",")).join("\n") + "\n";
|
|
16105
16115
|
}
|
|
16106
|
-
function generateFilesManifest(plan, target, { mergeExistingManifest = false } = {}) {
|
|
16116
|
+
function generateFilesManifest(plan, target, { mergeExistingManifest = false, extraScanDirs = [] } = {}) {
|
|
16107
16117
|
const rows = [["rel", "sha256", "size"]];
|
|
16108
16118
|
const newRels = /* @__PURE__ */ new Set();
|
|
16109
16119
|
for (const entry of plan) {
|
|
@@ -16114,6 +16124,26 @@ ${BLOCK}`, { mode: 493 });
|
|
|
16114
16124
|
rows.push([rel, sha256(buf), String(buf.length)]);
|
|
16115
16125
|
newRels.add(rel);
|
|
16116
16126
|
}
|
|
16127
|
+
function walkScanDir(absDir) {
|
|
16128
|
+
if (!fs2.existsSync(absDir)) return;
|
|
16129
|
+
for (const entry of fs2.readdirSync(absDir, { withFileTypes: true })) {
|
|
16130
|
+
const full = path2.join(absDir, entry.name);
|
|
16131
|
+
if (entry.isDirectory()) {
|
|
16132
|
+
walkScanDir(full);
|
|
16133
|
+
} else if (entry.isFile()) {
|
|
16134
|
+
const rel = path2.relative(target, full).split(path2.sep).join("/");
|
|
16135
|
+
if (newRels.has(rel)) continue;
|
|
16136
|
+
if (rel.startsWith("..") || path2.isAbsolute(rel)) continue;
|
|
16137
|
+
try {
|
|
16138
|
+
const buf = fs2.readFileSync(full);
|
|
16139
|
+
rows.push([rel, sha256(buf), String(buf.length)]);
|
|
16140
|
+
newRels.add(rel);
|
|
16141
|
+
} catch {
|
|
16142
|
+
}
|
|
16143
|
+
}
|
|
16144
|
+
}
|
|
16145
|
+
}
|
|
16146
|
+
for (const scan of extraScanDirs) walkScanDir(scan);
|
|
16117
16147
|
if (mergeExistingManifest) {
|
|
16118
16148
|
const manifestPath = path2.join(target, ".rihal", "_config", "files-manifest.csv");
|
|
16119
16149
|
if (fs2.existsSync(manifestPath)) {
|
|
@@ -16149,18 +16179,18 @@ ${BLOCK}`, { mode: 493 });
|
|
|
16149
16179
|
const isLocalOverride = (rel) => /\.local\.(md|mdc|json|yaml|yml|toml|js|ts)$/.test(rel);
|
|
16150
16180
|
let removed = 0;
|
|
16151
16181
|
const emptyCandidateDirs = /* @__PURE__ */ new Set();
|
|
16182
|
+
const targetRoot = path2.resolve(target);
|
|
16152
16183
|
for (const rel of oldRels) {
|
|
16153
16184
|
if (newRelsSet.has(rel)) continue;
|
|
16154
16185
|
if (neverSweep.test(rel)) continue;
|
|
16155
16186
|
if (isLocalOverride(rel)) continue;
|
|
16187
|
+
if (rel.includes("..") || path2.isAbsolute(rel)) continue;
|
|
16156
16188
|
const full = path2.join(target, rel);
|
|
16157
|
-
|
|
16158
|
-
|
|
16159
|
-
|
|
16160
|
-
|
|
16161
|
-
|
|
16162
|
-
}
|
|
16163
|
-
} catch {
|
|
16189
|
+
if (!fs2.existsSync(full)) continue;
|
|
16190
|
+
const result = safeRmSync(full, targetRoot);
|
|
16191
|
+
if (result.ok) {
|
|
16192
|
+
emptyCandidateDirs.add(path2.dirname(full));
|
|
16193
|
+
removed += 1;
|
|
16164
16194
|
}
|
|
16165
16195
|
}
|
|
16166
16196
|
const dirsSortedDeep = Array.from(emptyCandidateDirs).sort((a, b) => b.length - a.length);
|
|
@@ -16764,7 +16794,17 @@ commit_planning: ${desired}
|
|
|
16764
16794
|
const stateSrc = path2.join(SOURCE_ROOT, "state.json");
|
|
16765
16795
|
if (fs2.existsSync(stateSrc)) {
|
|
16766
16796
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16767
|
-
|
|
16797
|
+
let stateContent = fs2.readFileSync(stateSrc, "utf8").replace(/__PROJECT_NAME__/g, opts.projectName).replace(/__INSTALL_DATE__/g, now);
|
|
16798
|
+
const rmPath = path2.join(opts.target, ".planning", "ROADMAP.md");
|
|
16799
|
+
if (fs2.existsSync(rmPath)) {
|
|
16800
|
+
try {
|
|
16801
|
+
const rm = fs2.readFileSync(rmPath, "utf8");
|
|
16802
|
+
if (!rm.includes("<!-- INSTALL STUB")) {
|
|
16803
|
+
stateContent = stateContent.replace(/^\s*"_seeded_stub":\s*true,?\s*\n/m, "");
|
|
16804
|
+
}
|
|
16805
|
+
} catch {
|
|
16806
|
+
}
|
|
16807
|
+
}
|
|
16768
16808
|
ensureDir(path2.dirname(stateDest));
|
|
16769
16809
|
writeFileAtomic(stateDest, stateContent);
|
|
16770
16810
|
}
|
|
@@ -16782,10 +16822,6 @@ commit_planning: ${desired}
|
|
|
16782
16822
|
}
|
|
16783
16823
|
const globalAgentsDir = path2.join(os.homedir(), ".rihal", "agents");
|
|
16784
16824
|
ensureDir(globalAgentsDir);
|
|
16785
|
-
fs2.writeFileSync(
|
|
16786
|
-
path2.join(configDir, "files-manifest.csv"),
|
|
16787
|
-
generateFilesManifest(plan, opts.target, { mergeExistingManifest: !opts.force })
|
|
16788
|
-
);
|
|
16789
16825
|
const skillsResult = installSkills(PACKAGE_ROOT2, opts.target, {
|
|
16790
16826
|
skipGlobalDuplicates: isProjectInstall
|
|
16791
16827
|
});
|
|
@@ -16809,6 +16845,16 @@ commit_planning: ${desired}
|
|
|
16809
16845
|
} catch (err) {
|
|
16810
16846
|
console.log(" " + dim(`(sidebar stub generation skipped: ${err.message})`));
|
|
16811
16847
|
}
|
|
16848
|
+
fs2.writeFileSync(
|
|
16849
|
+
path2.join(configDir, "files-manifest.csv"),
|
|
16850
|
+
generateFilesManifest(plan, opts.target, {
|
|
16851
|
+
mergeExistingManifest: !opts.force,
|
|
16852
|
+
extraScanDirs: [
|
|
16853
|
+
path2.join(opts.target, ".claude", "skills"),
|
|
16854
|
+
path2.join(opts.target, ".rihal", "skills")
|
|
16855
|
+
]
|
|
16856
|
+
})
|
|
16857
|
+
);
|
|
16812
16858
|
const starterSeeded = seedStarterPlanning(opts.target, opts.projectName);
|
|
16813
16859
|
installBrainScaffold(PACKAGE_ROOT2, opts.target);
|
|
16814
16860
|
const gitignoreReport = ensureRcodeGitignore(opts.target, { commitPlanning: opts.commitPlanning });
|
|
@@ -16821,7 +16867,8 @@ commit_planning: ${desired}
|
|
|
16821
16867
|
const out = execFileSync("node", [toolsPath, "brain", "pull"], {
|
|
16822
16868
|
cwd: opts.target,
|
|
16823
16869
|
encoding: "utf8",
|
|
16824
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
16870
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
16871
|
+
timeout: 6e4
|
|
16825
16872
|
});
|
|
16826
16873
|
try {
|
|
16827
16874
|
brainReport = JSON.parse(out);
|
|
@@ -17420,9 +17467,37 @@ var require_update = __commonJS({
|
|
|
17420
17467
|
var { spawnSync } = require("child_process");
|
|
17421
17468
|
var clack = require_dist3();
|
|
17422
17469
|
var { PromptAbortError } = require_prompts();
|
|
17423
|
-
var {
|
|
17470
|
+
var { writeFileAtomic } = require_fsutil();
|
|
17424
17471
|
var { verifyInstall, formatReport } = require_manifest();
|
|
17425
17472
|
var install = require_install();
|
|
17473
|
+
function readConfigYaml(configPath) {
|
|
17474
|
+
const text = fs2.readFileSync(configPath, "utf8");
|
|
17475
|
+
const obj = {};
|
|
17476
|
+
for (const raw of text.split("\n")) {
|
|
17477
|
+
const line = raw.replace(/#.*$/, "").trimEnd();
|
|
17478
|
+
if (!line) continue;
|
|
17479
|
+
if (line.startsWith(" ")) continue;
|
|
17480
|
+
const colonAt = line.indexOf(":");
|
|
17481
|
+
if (colonAt === -1) continue;
|
|
17482
|
+
const key = line.slice(0, colonAt).trim();
|
|
17483
|
+
let val = line.slice(colonAt + 1).trim();
|
|
17484
|
+
if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
|
|
17485
|
+
if (val === "true") val = true;
|
|
17486
|
+
else if (val === "false") val = false;
|
|
17487
|
+
obj[key] = val;
|
|
17488
|
+
}
|
|
17489
|
+
return { obj, raw: text };
|
|
17490
|
+
}
|
|
17491
|
+
function setYamlKey(rawText, key, value) {
|
|
17492
|
+
const re = new RegExp(`^${key}:\\s*.*$`, "m");
|
|
17493
|
+
const replacement = typeof value === "string" ? `${key}: "${value.replace(/"/g, '\\"')}"` : `${key}: ${value}`;
|
|
17494
|
+
if (re.test(rawText)) {
|
|
17495
|
+
return rawText.replace(re, replacement);
|
|
17496
|
+
}
|
|
17497
|
+
return rawText.replace(/\n*$/, "") + `
|
|
17498
|
+
${replacement}
|
|
17499
|
+
`;
|
|
17500
|
+
}
|
|
17426
17501
|
function parseArgs(args) {
|
|
17427
17502
|
const opts = { yes: false };
|
|
17428
17503
|
for (const arg of args) {
|
|
@@ -17432,10 +17507,12 @@ var require_update = __commonJS({
|
|
|
17432
17507
|
}
|
|
17433
17508
|
function detectInstalledEditors(cwd) {
|
|
17434
17509
|
const editors = [];
|
|
17435
|
-
|
|
17436
|
-
|
|
17437
|
-
|
|
17438
|
-
|
|
17510
|
+
const os = require("os");
|
|
17511
|
+
const homeSkills = path2.join(os.homedir(), ".claude/skills");
|
|
17512
|
+
const projectClaude = fs2.existsSync(path2.join(cwd, ".claude/skills")) && fs2.readdirSync(path2.join(cwd, ".claude/skills")).some((n) => n.startsWith("rihal-")) || fs2.existsSync(path2.join(cwd, ".claude/agents")) && fs2.readdirSync(path2.join(cwd, ".claude/agents")).some((n) => n.startsWith("rihal-")) || fs2.existsSync(path2.join(cwd, ".claude/commands")) && fs2.readdirSync(path2.join(cwd, ".claude/commands")).some((n) => n.startsWith("rihal-"));
|
|
17513
|
+
const globalClaude = fs2.existsSync(homeSkills) && fs2.readdirSync(homeSkills).some((n) => n.startsWith("rihal-"));
|
|
17514
|
+
const configIndicatesClaude = fs2.existsSync(path2.join(cwd, ".rihal/config.yaml"));
|
|
17515
|
+
if (projectClaude || configIndicatesClaude && globalClaude) editors.push("claude");
|
|
17439
17516
|
if (fs2.existsSync(path2.join(cwd, ".cursor/rules"))) {
|
|
17440
17517
|
const hasRihal = fs2.readdirSync(path2.join(cwd, ".cursor/rules")).some((n) => n.startsWith("rihal-") && n.endsWith(".mdc"));
|
|
17441
17518
|
if (hasRihal) editors.push("cursor");
|
|
@@ -17561,8 +17638,8 @@ var require_update = __commonJS({
|
|
|
17561
17638
|
}
|
|
17562
17639
|
if (changed) {
|
|
17563
17640
|
content = content.replace(/\n---\n+$/, "\n");
|
|
17564
|
-
const { writeFileAtomic } = require_fsutil();
|
|
17565
|
-
|
|
17641
|
+
const { writeFileAtomic: writeFileAtomic2 } = require_fsutil();
|
|
17642
|
+
writeFileAtomic2(agentsMdPath, content);
|
|
17566
17643
|
}
|
|
17567
17644
|
}
|
|
17568
17645
|
module2.exports = async function update(args, { packageRoot, packageJson }) {
|
|
@@ -17581,7 +17658,9 @@ var require_update = __commonJS({
|
|
|
17581
17658
|
const cwd = process.cwd();
|
|
17582
17659
|
const opts = parseArgs(args);
|
|
17583
17660
|
const packageVersion = packageJson?.version || "0.0.0";
|
|
17584
|
-
const
|
|
17661
|
+
const configYamlPath = path2.join(cwd, ".rihal/config.yaml");
|
|
17662
|
+
const configJsonPath = path2.join(cwd, ".rihal/config.json");
|
|
17663
|
+
const configPath = fs2.existsSync(configYamlPath) ? configYamlPath : configJsonPath;
|
|
17585
17664
|
if (!fs2.existsSync(configPath)) {
|
|
17586
17665
|
console.error(`
|
|
17587
17666
|
\u274C Rihal Code is not installed in this directory.`);
|
|
@@ -17590,11 +17669,19 @@ var require_update = __commonJS({
|
|
|
17590
17669
|
process.exit(1);
|
|
17591
17670
|
}
|
|
17592
17671
|
let config;
|
|
17672
|
+
let configRaw = "";
|
|
17673
|
+
const isYaml = configPath.endsWith(".yaml");
|
|
17593
17674
|
try {
|
|
17594
|
-
|
|
17675
|
+
if (isYaml) {
|
|
17676
|
+
const parsed = readConfigYaml(configPath);
|
|
17677
|
+
config = parsed.obj;
|
|
17678
|
+
configRaw = parsed.raw;
|
|
17679
|
+
} else {
|
|
17680
|
+
config = JSON.parse(fs2.readFileSync(configPath, "utf8"));
|
|
17681
|
+
}
|
|
17595
17682
|
} catch (err) {
|
|
17596
17683
|
console.error(`
|
|
17597
|
-
\u274C .
|
|
17684
|
+
\u274C ${path2.basename(configPath)} is not parseable: ${err.message}
|
|
17598
17685
|
`);
|
|
17599
17686
|
process.exit(1);
|
|
17600
17687
|
}
|
|
@@ -17682,9 +17769,15 @@ var require_update = __commonJS({
|
|
|
17682
17769
|
});
|
|
17683
17770
|
console.log(` \u2713 [${supportedIdes.join(", ")}] \u2192 refreshed via install.js`);
|
|
17684
17771
|
}
|
|
17685
|
-
|
|
17686
|
-
|
|
17687
|
-
|
|
17772
|
+
if (isYaml) {
|
|
17773
|
+
const updated = setYamlKey(configRaw, "installed_version", packageVersion);
|
|
17774
|
+
writeFileAtomic(configPath, updated);
|
|
17775
|
+
} else {
|
|
17776
|
+
config.installed_version = packageVersion;
|
|
17777
|
+
const { writeJsonAtomic } = require_fsutil();
|
|
17778
|
+
writeJsonAtomic(configPath, config);
|
|
17779
|
+
}
|
|
17780
|
+
console.log(` \u2713 ${path2.relative(cwd, configPath)} \u2192 installed_version: ${packageVersion}`);
|
|
17688
17781
|
console.log();
|
|
17689
17782
|
const { reports, hasDrift } = verifyInstall(cwd, packageRoot, editors);
|
|
17690
17783
|
if (hasDrift) {
|
|
@@ -17791,6 +17884,10 @@ var require_uninstall = __commonJS({
|
|
|
17791
17884
|
cursor: [],
|
|
17792
17885
|
windsurf: [],
|
|
17793
17886
|
antigravity: [],
|
|
17887
|
+
gemini: [],
|
|
17888
|
+
// #706 — added when --editor=gemini or --editor=all
|
|
17889
|
+
vscode: [],
|
|
17890
|
+
// #706 — vscode marker dir cleanup (commands share .claude/)
|
|
17794
17891
|
agentsMd: null,
|
|
17795
17892
|
// null = no section; 'present' = section present
|
|
17796
17893
|
stateDir: null,
|
|
@@ -17798,6 +17895,11 @@ var require_uninstall = __commonJS({
|
|
|
17798
17895
|
planningDir: null
|
|
17799
17896
|
// null = missing; { files: N } = present
|
|
17800
17897
|
};
|
|
17898
|
+
if (editors.includes("vscode")) {
|
|
17899
|
+
if (!editors.includes("claude")) editors.push("claude");
|
|
17900
|
+
const markerDir = path2.join(cwd, ".vscode/rihal");
|
|
17901
|
+
if (fs2.existsSync(markerDir)) plan.vscode.push(".vscode/rihal");
|
|
17902
|
+
}
|
|
17801
17903
|
if (editors.includes("claude")) {
|
|
17802
17904
|
const skillsDir = path2.join(cwd, ".claude/skills");
|
|
17803
17905
|
if (fs2.existsSync(skillsDir)) {
|
|
@@ -17835,6 +17937,18 @@ var require_uninstall = __commonJS({
|
|
|
17835
17937
|
plan.antigravity = fs2.readdirSync(agDir).filter((name) => name.startsWith("rihal-"));
|
|
17836
17938
|
}
|
|
17837
17939
|
}
|
|
17940
|
+
if (editors.includes("gemini")) {
|
|
17941
|
+
for (const sub of ["agents", "commands"]) {
|
|
17942
|
+
const dir = path2.join(cwd, ".gemini", "rihal", sub);
|
|
17943
|
+
if (fs2.existsSync(dir)) {
|
|
17944
|
+
for (const name of fs2.readdirSync(dir)) {
|
|
17945
|
+
if (name.startsWith("rihal-") || name.endsWith(".md")) {
|
|
17946
|
+
plan.gemini.push(path2.join(".gemini/rihal", sub, name));
|
|
17947
|
+
}
|
|
17948
|
+
}
|
|
17949
|
+
}
|
|
17950
|
+
}
|
|
17951
|
+
}
|
|
17838
17952
|
const agentsMdPath = path2.join(cwd, "AGENTS.md");
|
|
17839
17953
|
if (fs2.existsSync(agentsMdPath)) {
|
|
17840
17954
|
const content = fs2.readFileSync(agentsMdPath, "utf8");
|
|
@@ -17881,7 +17995,13 @@ var require_uninstall = __commonJS({
|
|
|
17881
17995
|
for (const name of plan.claude.skills) {
|
|
17882
17996
|
paths.push(path2.join(".claude/skills", name));
|
|
17883
17997
|
}
|
|
17884
|
-
|
|
17998
|
+
for (const name of plan.claude.commands) {
|
|
17999
|
+
if (name.startsWith("rihal-") && name.endsWith(".md")) {
|
|
18000
|
+
paths.push(path2.join(".claude/commands", name));
|
|
18001
|
+
}
|
|
18002
|
+
}
|
|
18003
|
+
const hasSubdirCommand = plan.claude.commands.some((n) => !n.startsWith("rihal-"));
|
|
18004
|
+
if (hasSubdirCommand) {
|
|
17885
18005
|
paths.push(".claude/commands/rihal");
|
|
17886
18006
|
}
|
|
17887
18007
|
for (const name of plan.claude.agents) {
|
|
@@ -17896,6 +18016,12 @@ var require_uninstall = __commonJS({
|
|
|
17896
18016
|
for (const name of plan.antigravity) {
|
|
17897
18017
|
paths.push(path2.join(".antigravity/agents", name));
|
|
17898
18018
|
}
|
|
18019
|
+
if (Array.isArray(plan.gemini)) {
|
|
18020
|
+
for (const rel of plan.gemini) paths.push(rel);
|
|
18021
|
+
}
|
|
18022
|
+
if (Array.isArray(plan.vscode)) {
|
|
18023
|
+
for (const rel of plan.vscode) paths.push(rel);
|
|
18024
|
+
}
|
|
17899
18025
|
if (plan.agentsMd && fs2.existsSync(path2.join(cwd, "AGENTS.md"))) {
|
|
17900
18026
|
paths.push("AGENTS.md");
|
|
17901
18027
|
}
|
|
@@ -18150,6 +18276,25 @@ var require_uninstall = __commonJS({
|
|
|
18150
18276
|
removed += n;
|
|
18151
18277
|
if (n > 0) console.log(` \u2713 removed ${n} Antigravity agents`);
|
|
18152
18278
|
}
|
|
18279
|
+
if (editors.includes("gemini")) {
|
|
18280
|
+
let n = 0;
|
|
18281
|
+
for (const sub of ["agents", "commands"]) {
|
|
18282
|
+
const dir = path2.join(cwd, ".gemini", "rihal", sub);
|
|
18283
|
+
n += removeMatching(dir, (name) => name.startsWith("rihal-") || name.endsWith(".md"));
|
|
18284
|
+
}
|
|
18285
|
+
removed += n;
|
|
18286
|
+
if (n > 0) console.log(` \u2713 removed ${n} Gemini files`);
|
|
18287
|
+
}
|
|
18288
|
+
if (editors.includes("vscode")) {
|
|
18289
|
+
const markerDir = path2.join(cwd, ".vscode/rihal");
|
|
18290
|
+
if (fs2.existsSync(markerDir)) {
|
|
18291
|
+
const r = safeRmSync(markerDir, path2.resolve(cwd));
|
|
18292
|
+
if (r.ok) {
|
|
18293
|
+
removed += 1;
|
|
18294
|
+
console.log(` \u2713 removed .vscode/rihal/ marker`);
|
|
18295
|
+
}
|
|
18296
|
+
}
|
|
18297
|
+
}
|
|
18153
18298
|
if (plan.agentsMd) {
|
|
18154
18299
|
const agentsMdPath = path2.join(cwd, "AGENTS.md");
|
|
18155
18300
|
const stripped = stripRihalFromAgentsMd(agentsMdPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.29",
|
|
4
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": {
|