@hanzlaa/rcode 3.4.28 → 3.4.30
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 +1 -0
- package/cli/install.js +103 -18
- package/cli/uninstall.js +73 -1
- package/cli/update.js +94 -14
- package/dist/rcode.js +175 -29
- 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`, `build`, `council`, `doctor`, `postinstall`, `progress`, `security`, `tools`, `uninstall`, `test`, 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
|
@@ -307,6 +307,7 @@ We use [Conventional Commits](https://www.conventionalcommits.org/) format. The
|
|
|
307
307
|
- `test` — test files under `test/` (test-only changes)
|
|
308
308
|
- `tools` — `rihal/bin/rihal-tools.cjs` subcommands
|
|
309
309
|
- `uninstall` — `cli/uninstall.js` flow
|
|
310
|
+
- `update` — `cli/update.js` flow
|
|
310
311
|
- `<phase-id>` — numeric phase scope when committing inside a phase (e.g. `docs(15)`, `feat(8.3)`)
|
|
311
312
|
- `<sprint-id>` — numeric sprint scope inside a phase (e.g. `feat(15.1)`)
|
|
312
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/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
|
@@ -15653,12 +15653,23 @@ Run \`/rihal-new-project <description>\` to bootstrap, or \`/rihal-sprint-planni
|
|
|
15653
15653
|
`
|
|
15654
15654
|
);
|
|
15655
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
|
+
}
|
|
15656
15666
|
if (!fs2.existsSync(rihalStateJson)) {
|
|
15657
15667
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15668
|
+
const isStubProject = planningRoadmapIsStub();
|
|
15658
15669
|
const state = {
|
|
15659
15670
|
version: "1",
|
|
15660
15671
|
project: null,
|
|
15661
|
-
_seeded_stub: true,
|
|
15672
|
+
...isStubProject ? { _seeded_stub: true } : {},
|
|
15662
15673
|
created: now,
|
|
15663
15674
|
updated: now,
|
|
15664
15675
|
current_phase: null,
|
|
@@ -16102,7 +16113,7 @@ ${BLOCK}`, { mode: 493 });
|
|
|
16102
16113
|
}
|
|
16103
16114
|
return rows.map((r) => r.join(",")).join("\n") + "\n";
|
|
16104
16115
|
}
|
|
16105
|
-
function generateFilesManifest(plan, target, { mergeExistingManifest = false } = {}) {
|
|
16116
|
+
function generateFilesManifest(plan, target, { mergeExistingManifest = false, extraScanDirs = [] } = {}) {
|
|
16106
16117
|
const rows = [["rel", "sha256", "size"]];
|
|
16107
16118
|
const newRels = /* @__PURE__ */ new Set();
|
|
16108
16119
|
for (const entry of plan) {
|
|
@@ -16113,6 +16124,26 @@ ${BLOCK}`, { mode: 493 });
|
|
|
16113
16124
|
rows.push([rel, sha256(buf), String(buf.length)]);
|
|
16114
16125
|
newRels.add(rel);
|
|
16115
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);
|
|
16116
16147
|
if (mergeExistingManifest) {
|
|
16117
16148
|
const manifestPath = path2.join(target, ".rihal", "_config", "files-manifest.csv");
|
|
16118
16149
|
if (fs2.existsSync(manifestPath)) {
|
|
@@ -16148,18 +16179,18 @@ ${BLOCK}`, { mode: 493 });
|
|
|
16148
16179
|
const isLocalOverride = (rel) => /\.local\.(md|mdc|json|yaml|yml|toml|js|ts)$/.test(rel);
|
|
16149
16180
|
let removed = 0;
|
|
16150
16181
|
const emptyCandidateDirs = /* @__PURE__ */ new Set();
|
|
16182
|
+
const targetRoot = path2.resolve(target);
|
|
16151
16183
|
for (const rel of oldRels) {
|
|
16152
16184
|
if (newRelsSet.has(rel)) continue;
|
|
16153
16185
|
if (neverSweep.test(rel)) continue;
|
|
16154
16186
|
if (isLocalOverride(rel)) continue;
|
|
16187
|
+
if (rel.includes("..") || path2.isAbsolute(rel)) continue;
|
|
16155
16188
|
const full = path2.join(target, rel);
|
|
16156
|
-
|
|
16157
|
-
|
|
16158
|
-
|
|
16159
|
-
|
|
16160
|
-
|
|
16161
|
-
}
|
|
16162
|
-
} 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;
|
|
16163
16194
|
}
|
|
16164
16195
|
}
|
|
16165
16196
|
const dirsSortedDeep = Array.from(emptyCandidateDirs).sort((a, b) => b.length - a.length);
|
|
@@ -16763,7 +16794,17 @@ commit_planning: ${desired}
|
|
|
16763
16794
|
const stateSrc = path2.join(SOURCE_ROOT, "state.json");
|
|
16764
16795
|
if (fs2.existsSync(stateSrc)) {
|
|
16765
16796
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16766
|
-
|
|
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
|
+
}
|
|
16767
16808
|
ensureDir(path2.dirname(stateDest));
|
|
16768
16809
|
writeFileAtomic(stateDest, stateContent);
|
|
16769
16810
|
}
|
|
@@ -16781,10 +16822,6 @@ commit_planning: ${desired}
|
|
|
16781
16822
|
}
|
|
16782
16823
|
const globalAgentsDir = path2.join(os.homedir(), ".rihal", "agents");
|
|
16783
16824
|
ensureDir(globalAgentsDir);
|
|
16784
|
-
fs2.writeFileSync(
|
|
16785
|
-
path2.join(configDir, "files-manifest.csv"),
|
|
16786
|
-
generateFilesManifest(plan, opts.target, { mergeExistingManifest: !opts.force })
|
|
16787
|
-
);
|
|
16788
16825
|
const skillsResult = installSkills(PACKAGE_ROOT2, opts.target, {
|
|
16789
16826
|
skipGlobalDuplicates: isProjectInstall
|
|
16790
16827
|
});
|
|
@@ -16808,6 +16845,16 @@ commit_planning: ${desired}
|
|
|
16808
16845
|
} catch (err) {
|
|
16809
16846
|
console.log(" " + dim(`(sidebar stub generation skipped: ${err.message})`));
|
|
16810
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
|
+
);
|
|
16811
16858
|
const starterSeeded = seedStarterPlanning(opts.target, opts.projectName);
|
|
16812
16859
|
installBrainScaffold(PACKAGE_ROOT2, opts.target);
|
|
16813
16860
|
const gitignoreReport = ensureRcodeGitignore(opts.target, { commitPlanning: opts.commitPlanning });
|
|
@@ -16820,7 +16867,8 @@ commit_planning: ${desired}
|
|
|
16820
16867
|
const out = execFileSync("node", [toolsPath, "brain", "pull"], {
|
|
16821
16868
|
cwd: opts.target,
|
|
16822
16869
|
encoding: "utf8",
|
|
16823
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
16870
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
16871
|
+
timeout: 6e4
|
|
16824
16872
|
});
|
|
16825
16873
|
try {
|
|
16826
16874
|
brainReport = JSON.parse(out);
|
|
@@ -17419,9 +17467,37 @@ var require_update = __commonJS({
|
|
|
17419
17467
|
var { spawnSync } = require("child_process");
|
|
17420
17468
|
var clack = require_dist3();
|
|
17421
17469
|
var { PromptAbortError } = require_prompts();
|
|
17422
|
-
var {
|
|
17470
|
+
var { writeFileAtomic } = require_fsutil();
|
|
17423
17471
|
var { verifyInstall, formatReport } = require_manifest();
|
|
17424
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
|
+
}
|
|
17425
17501
|
function parseArgs(args) {
|
|
17426
17502
|
const opts = { yes: false };
|
|
17427
17503
|
for (const arg of args) {
|
|
@@ -17431,10 +17507,12 @@ var require_update = __commonJS({
|
|
|
17431
17507
|
}
|
|
17432
17508
|
function detectInstalledEditors(cwd) {
|
|
17433
17509
|
const editors = [];
|
|
17434
|
-
|
|
17435
|
-
|
|
17436
|
-
|
|
17437
|
-
|
|
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");
|
|
17438
17516
|
if (fs2.existsSync(path2.join(cwd, ".cursor/rules"))) {
|
|
17439
17517
|
const hasRihal = fs2.readdirSync(path2.join(cwd, ".cursor/rules")).some((n) => n.startsWith("rihal-") && n.endsWith(".mdc"));
|
|
17440
17518
|
if (hasRihal) editors.push("cursor");
|
|
@@ -17560,8 +17638,8 @@ var require_update = __commonJS({
|
|
|
17560
17638
|
}
|
|
17561
17639
|
if (changed) {
|
|
17562
17640
|
content = content.replace(/\n---\n+$/, "\n");
|
|
17563
|
-
const { writeFileAtomic } = require_fsutil();
|
|
17564
|
-
|
|
17641
|
+
const { writeFileAtomic: writeFileAtomic2 } = require_fsutil();
|
|
17642
|
+
writeFileAtomic2(agentsMdPath, content);
|
|
17565
17643
|
}
|
|
17566
17644
|
}
|
|
17567
17645
|
module2.exports = async function update(args, { packageRoot, packageJson }) {
|
|
@@ -17580,7 +17658,9 @@ var require_update = __commonJS({
|
|
|
17580
17658
|
const cwd = process.cwd();
|
|
17581
17659
|
const opts = parseArgs(args);
|
|
17582
17660
|
const packageVersion = packageJson?.version || "0.0.0";
|
|
17583
|
-
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;
|
|
17584
17664
|
if (!fs2.existsSync(configPath)) {
|
|
17585
17665
|
console.error(`
|
|
17586
17666
|
\u274C Rihal Code is not installed in this directory.`);
|
|
@@ -17589,11 +17669,19 @@ var require_update = __commonJS({
|
|
|
17589
17669
|
process.exit(1);
|
|
17590
17670
|
}
|
|
17591
17671
|
let config;
|
|
17672
|
+
let configRaw = "";
|
|
17673
|
+
const isYaml = configPath.endsWith(".yaml");
|
|
17592
17674
|
try {
|
|
17593
|
-
|
|
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
|
+
}
|
|
17594
17682
|
} catch (err) {
|
|
17595
17683
|
console.error(`
|
|
17596
|
-
\u274C .
|
|
17684
|
+
\u274C ${path2.basename(configPath)} is not parseable: ${err.message}
|
|
17597
17685
|
`);
|
|
17598
17686
|
process.exit(1);
|
|
17599
17687
|
}
|
|
@@ -17681,9 +17769,15 @@ var require_update = __commonJS({
|
|
|
17681
17769
|
});
|
|
17682
17770
|
console.log(` \u2713 [${supportedIdes.join(", ")}] \u2192 refreshed via install.js`);
|
|
17683
17771
|
}
|
|
17684
|
-
|
|
17685
|
-
|
|
17686
|
-
|
|
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}`);
|
|
17687
17781
|
console.log();
|
|
17688
17782
|
const { reports, hasDrift } = verifyInstall(cwd, packageRoot, editors);
|
|
17689
17783
|
if (hasDrift) {
|
|
@@ -17790,6 +17884,10 @@ var require_uninstall = __commonJS({
|
|
|
17790
17884
|
cursor: [],
|
|
17791
17885
|
windsurf: [],
|
|
17792
17886
|
antigravity: [],
|
|
17887
|
+
gemini: [],
|
|
17888
|
+
// #706 — added when --editor=gemini or --editor=all
|
|
17889
|
+
vscode: [],
|
|
17890
|
+
// #706 — vscode marker dir cleanup (commands share .claude/)
|
|
17793
17891
|
agentsMd: null,
|
|
17794
17892
|
// null = no section; 'present' = section present
|
|
17795
17893
|
stateDir: null,
|
|
@@ -17797,6 +17895,11 @@ var require_uninstall = __commonJS({
|
|
|
17797
17895
|
planningDir: null
|
|
17798
17896
|
// null = missing; { files: N } = present
|
|
17799
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
|
+
}
|
|
17800
17903
|
if (editors.includes("claude")) {
|
|
17801
17904
|
const skillsDir = path2.join(cwd, ".claude/skills");
|
|
17802
17905
|
if (fs2.existsSync(skillsDir)) {
|
|
@@ -17834,6 +17937,18 @@ var require_uninstall = __commonJS({
|
|
|
17834
17937
|
plan.antigravity = fs2.readdirSync(agDir).filter((name) => name.startsWith("rihal-"));
|
|
17835
17938
|
}
|
|
17836
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
|
+
}
|
|
17837
17952
|
const agentsMdPath = path2.join(cwd, "AGENTS.md");
|
|
17838
17953
|
if (fs2.existsSync(agentsMdPath)) {
|
|
17839
17954
|
const content = fs2.readFileSync(agentsMdPath, "utf8");
|
|
@@ -17880,7 +17995,13 @@ var require_uninstall = __commonJS({
|
|
|
17880
17995
|
for (const name of plan.claude.skills) {
|
|
17881
17996
|
paths.push(path2.join(".claude/skills", name));
|
|
17882
17997
|
}
|
|
17883
|
-
|
|
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) {
|
|
17884
18005
|
paths.push(".claude/commands/rihal");
|
|
17885
18006
|
}
|
|
17886
18007
|
for (const name of plan.claude.agents) {
|
|
@@ -17895,6 +18016,12 @@ var require_uninstall = __commonJS({
|
|
|
17895
18016
|
for (const name of plan.antigravity) {
|
|
17896
18017
|
paths.push(path2.join(".antigravity/agents", name));
|
|
17897
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
|
+
}
|
|
17898
18025
|
if (plan.agentsMd && fs2.existsSync(path2.join(cwd, "AGENTS.md"))) {
|
|
17899
18026
|
paths.push("AGENTS.md");
|
|
17900
18027
|
}
|
|
@@ -18149,6 +18276,25 @@ var require_uninstall = __commonJS({
|
|
|
18149
18276
|
removed += n;
|
|
18150
18277
|
if (n > 0) console.log(` \u2713 removed ${n} Antigravity agents`);
|
|
18151
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
|
+
}
|
|
18152
18298
|
if (plan.agentsMd) {
|
|
18153
18299
|
const agentsMdPath = path2.join(cwd, "AGENTS.md");
|
|
18154
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.30",
|
|
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": {
|