@hanzlaa/rcode 3.4.21 → 3.4.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/generate-command-skills.cjs +33 -2
- package/cli/index.js +8 -8
- package/cli/install.js +115 -11
- package/cli/set-profile.js +5 -5
- package/cli/show-model.js +3 -3
- package/cli/uninstall.js +61 -9
- package/dist/rcode.js +123 -27
- package/package.json +2 -1
- package/rihal/bin/rihal-tools.cjs +37 -0
|
@@ -149,7 +149,7 @@ Identical to \`/rihal-${cmdName}\`. See the workflow file for the canonical outp
|
|
|
149
149
|
`;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function main(packageRoot, targetSkillsDir, version) {
|
|
152
|
+
function main(packageRoot, targetSkillsDir, version, options = {}) {
|
|
153
153
|
if (!fs.existsSync(targetSkillsDir)) {
|
|
154
154
|
fs.mkdirSync(targetSkillsDir, { recursive: true });
|
|
155
155
|
}
|
|
@@ -158,11 +158,20 @@ function main(packageRoot, targetSkillsDir, version) {
|
|
|
158
158
|
const commandsDir = path.join(packageRoot, 'rihal', 'commands');
|
|
159
159
|
if (!fs.existsSync(commandsDir)) {
|
|
160
160
|
console.warn(`[generate-command-skills] commands dir not found: ${commandsDir}`);
|
|
161
|
-
return { generated: 0, skipped: 0 };
|
|
161
|
+
return { generated: 0, skipped: 0, skippedGlobal: 0 };
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// Issue #679: skip stubs whose name already exists in ~/.claude/skills/.
|
|
165
|
+
// Otherwise the slash picker shows the same /rihal-* twice.
|
|
166
|
+
const os = require('os');
|
|
167
|
+
const globalSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
168
|
+
const globalSkills = (options.skipGlobalDuplicates && fs.existsSync(globalSkillsDir))
|
|
169
|
+
? new Set(fs.readdirSync(globalSkillsDir).filter(n => n.startsWith('rihal-')))
|
|
170
|
+
: new Set();
|
|
171
|
+
|
|
164
172
|
let generated = 0;
|
|
165
173
|
let skipped = 0;
|
|
174
|
+
let skippedGlobal = 0;
|
|
166
175
|
|
|
167
176
|
for (const file of fs.readdirSync(commandsDir)) {
|
|
168
177
|
if (!file.endsWith('.md')) continue;
|
|
@@ -177,6 +186,28 @@ function main(packageRoot, targetSkillsDir, version) {
|
|
|
177
186
|
continue;
|
|
178
187
|
}
|
|
179
188
|
|
|
189
|
+
if (globalSkills.has(skillName)) {
|
|
190
|
+
// Global skill of same name exists — installing a sidebar stub here would
|
|
191
|
+
// duplicate the entry in Claude Code's slash picker. Also clean up any
|
|
192
|
+
// previously-generated stub with the same name.
|
|
193
|
+
const existingStub = path.join(targetSkillsDir, skillName);
|
|
194
|
+
if (fs.existsSync(existingStub)) {
|
|
195
|
+
try {
|
|
196
|
+
const existingFile = path.join(existingStub, 'SKILL.md');
|
|
197
|
+
if (fs.existsSync(existingFile)) {
|
|
198
|
+
const text = fs.readFileSync(existingFile, 'utf8');
|
|
199
|
+
// Only delete previously-generated stubs (marker present) — never
|
|
200
|
+
// user customizations.
|
|
201
|
+
if (/^generated:\s*true/m.test(text)) {
|
|
202
|
+
fs.rmSync(existingStub, { recursive: true, force: true });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch { /* non-fatal */ }
|
|
206
|
+
}
|
|
207
|
+
skippedGlobal++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
180
211
|
const cmdPath = path.join(commandsDir, file);
|
|
181
212
|
const cmdText = fs.readFileSync(cmdPath, 'utf8');
|
|
182
213
|
const cmdFm = parseFrontmatter(cmdText);
|
package/cli/index.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* Rihal Code CLI
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* npx @
|
|
7
|
-
* npx @
|
|
8
|
-
* npx @
|
|
9
|
-
* npx @
|
|
10
|
-
* npx @
|
|
11
|
-
* npx @
|
|
12
|
-
* npx @
|
|
13
|
-
* npx @
|
|
6
|
+
* npx @hanzlaa/rcode init → scaffold .rihal/ in current project
|
|
7
|
+
* npx @hanzlaa/rcode dashboard → start the Diwan view-only dashboard
|
|
8
|
+
* npx @hanzlaa/rcode serve → alias for dashboard
|
|
9
|
+
* npx @hanzlaa/rcode digest → print compact agent digests
|
|
10
|
+
* npx @hanzlaa/rcode team → list the team roster
|
|
11
|
+
* npx @hanzlaa/rcode doctor → compliance check
|
|
12
|
+
* npx @hanzlaa/rcode version → print version
|
|
13
|
+
* npx @hanzlaa/rcode help → this message
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const path = require('path');
|
package/cli/install.js
CHANGED
|
@@ -370,11 +370,32 @@ async function resolveIde(opts) {
|
|
|
370
370
|
async function resolveCommitPlanning(opts) {
|
|
371
371
|
if (opts.commitPlanning !== null) return opts.commitPlanning;
|
|
372
372
|
if (opts.noPrompt || opts.global) return false; // global install: no planning artifacts
|
|
373
|
-
if (opts.yes || !process.stdin.isTTY) return true; // non-interactive default
|
|
374
373
|
|
|
374
|
+
// Issue #685: on re-install, read the existing .rihal/config.yaml and use
|
|
375
|
+
// its commit_planning value as the default. Otherwise the new prompt
|
|
376
|
+
// answer overwrites .gitignore but NOT config.yaml, leaving two sources of
|
|
377
|
+
// truth that silently diverge. Users on re-install almost always want to
|
|
378
|
+
// KEEP their existing setting unless they explicitly pass --commit-planning.
|
|
379
|
+
let existingValue = null;
|
|
380
|
+
try {
|
|
381
|
+
const cfgPath = path.join(opts.target, '.rihal', 'config.yaml');
|
|
382
|
+
if (fs.existsSync(cfgPath)) {
|
|
383
|
+
const cfg = fs.readFileSync(cfgPath, 'utf8');
|
|
384
|
+
const m = cfg.match(/^commit_planning:\s*(true|false)\s*$/m);
|
|
385
|
+
if (m) existingValue = m[1] === 'true';
|
|
386
|
+
}
|
|
387
|
+
} catch { /* fall through to prompt */ }
|
|
388
|
+
|
|
389
|
+
if (opts.yes || !process.stdin.isTTY) {
|
|
390
|
+
return existingValue !== null ? existingValue : true; // honor existing on re-install
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const initialValue = existingValue === false ? 'gitignore' : 'commit';
|
|
375
394
|
const choice = await clack.select({
|
|
376
|
-
message:
|
|
377
|
-
|
|
395
|
+
message: existingValue !== null
|
|
396
|
+
? '📋 .planning/ tracking — current setting preserved unless you change it.'
|
|
397
|
+
: '📋 .planning/ holds PRDs, roadmaps, sprints, SUMMARY files. How should they be tracked?',
|
|
398
|
+
initialValue,
|
|
378
399
|
options: [
|
|
379
400
|
{ value: 'commit', label: 'Commit', hint: 'collaborators see the same plans (recommended)' },
|
|
380
401
|
{ value: 'gitignore', label: 'Gitignore', hint: 'planning stays local (good for sensitive PRDs)' },
|
|
@@ -873,16 +894,26 @@ function installBrainScaffold(packageRoot, target) {
|
|
|
873
894
|
*
|
|
874
895
|
* A skill is marked internal by adding `internal: true` to its SKILL.md frontmatter.
|
|
875
896
|
*/
|
|
876
|
-
function installSkills(packageRoot, target) {
|
|
897
|
+
function installSkills(packageRoot, target, options = {}) {
|
|
877
898
|
const skillsSource = path.join(packageRoot, 'rihal/skills');
|
|
878
899
|
const skillsDest = path.join(target, '.claude/skills');
|
|
879
900
|
const internalDest = path.join(target, '.rihal/skills');
|
|
880
901
|
|
|
881
|
-
if (!fs.existsSync(skillsSource)) return 0;
|
|
902
|
+
if (!fs.existsSync(skillsSource)) return { count: 0, skippedGlobal: 0 };
|
|
882
903
|
fs.mkdirSync(skillsDest, { recursive: true });
|
|
883
904
|
fs.mkdirSync(internalDest, { recursive: true });
|
|
884
905
|
|
|
906
|
+
// Issue #679: when ~/.claude/skills/<name>/ already exists with the rihal-
|
|
907
|
+
// prefix, Claude Code reads from BOTH global and project, showing every
|
|
908
|
+
// /rihal-* twice in the slash picker. Skip the project copy for any rihal-*
|
|
909
|
+
// skill that already lives in the global skills dir.
|
|
910
|
+
const globalSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
911
|
+
const globalRihalSkills = (options.skipGlobalDuplicates && fs.existsSync(globalSkillsDir))
|
|
912
|
+
? new Set(fs.readdirSync(globalSkillsDir).filter(n => n.startsWith('rihal-')))
|
|
913
|
+
: new Set();
|
|
914
|
+
|
|
885
915
|
let count = 0;
|
|
916
|
+
let skippedGlobal = 0;
|
|
886
917
|
|
|
887
918
|
function isInternalSkill(skillDir) {
|
|
888
919
|
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
@@ -891,6 +922,13 @@ function installSkills(packageRoot, target) {
|
|
|
891
922
|
return /^internal:\s*true\s*$/m.test(text);
|
|
892
923
|
}
|
|
893
924
|
|
|
925
|
+
function hasLocalOverride(destDir) {
|
|
926
|
+
if (!fs.existsSync(destDir)) return false;
|
|
927
|
+
try {
|
|
928
|
+
return fs.readdirSync(destDir).some(f => f.endsWith('.local.md'));
|
|
929
|
+
} catch { return false; }
|
|
930
|
+
}
|
|
931
|
+
|
|
894
932
|
function walkForSkills(dir) {
|
|
895
933
|
if (!fs.existsSync(dir)) return;
|
|
896
934
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -901,9 +939,23 @@ function installSkills(packageRoot, target) {
|
|
|
901
939
|
const destName = entry.name.startsWith('rihal-')
|
|
902
940
|
? entry.name
|
|
903
941
|
: `rihal-${entry.name}`;
|
|
904
|
-
const
|
|
942
|
+
const internal = isInternalSkill(src);
|
|
943
|
+
const dest = internal
|
|
905
944
|
? path.join(internalDest, destName) // internal → .rihal/skills/
|
|
906
945
|
: path.join(skillsDest, destName); // user-facing → .claude/skills/
|
|
946
|
+
|
|
947
|
+
// Skip user-facing (non-internal) rihal-* skills when the same name
|
|
948
|
+
// exists globally — UNLESS the user has a *.local.md override on the
|
|
949
|
+
// project copy, in which case we always preserve their customization.
|
|
950
|
+
if (!internal && globalRihalSkills.has(destName) && !hasLocalOverride(dest)) {
|
|
951
|
+
// Also remove the existing project copy (left over from previous
|
|
952
|
+
// installs that didn't dedup) so it stops showing in the picker.
|
|
953
|
+
if (fs.existsSync(dest)) {
|
|
954
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch { /* non-fatal */ }
|
|
955
|
+
}
|
|
956
|
+
skippedGlobal++;
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
907
959
|
copyDirRecursive(src, dest);
|
|
908
960
|
count++;
|
|
909
961
|
} else {
|
|
@@ -916,7 +968,7 @@ function installSkills(packageRoot, target) {
|
|
|
916
968
|
walkForSkills(path.join(skillsSource, bucket));
|
|
917
969
|
}
|
|
918
970
|
|
|
919
|
-
return count;
|
|
971
|
+
return { count, skippedGlobal };
|
|
920
972
|
}
|
|
921
973
|
|
|
922
974
|
/**
|
|
@@ -1425,6 +1477,18 @@ function convertToCursorMdc(sourceText) {
|
|
|
1425
1477
|
async function install(opts) {
|
|
1426
1478
|
if (opts.help) { printHelp(); return 0; }
|
|
1427
1479
|
|
|
1480
|
+
// Issue #680: --reset alone is a footgun — silently does nothing. Fail
|
|
1481
|
+
// fast with a clear message before any work happens.
|
|
1482
|
+
if (opts.reset && !opts.force) {
|
|
1483
|
+
console.log('');
|
|
1484
|
+
console.log(' ' + warn('--reset has no effect without --force.'));
|
|
1485
|
+
console.log(' ' + dim(' --reset wipes config.yaml and state.json. To prevent accidental data loss,'));
|
|
1486
|
+
console.log(' ' + dim(' it must be paired with --force. Re-run as:'));
|
|
1487
|
+
console.log(' ' + dim(' rcode install --reset --force'));
|
|
1488
|
+
console.log('');
|
|
1489
|
+
return 2;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1428
1492
|
const pkgVersion = readPackageVersion();
|
|
1429
1493
|
|
|
1430
1494
|
// Header banner — only shown for interactive runs to keep CI/non-TTY logs terse.
|
|
@@ -1754,8 +1818,10 @@ async function install(opts) {
|
|
|
1754
1818
|
const configDir = path.join(opts.target, '.rihal', '_config');
|
|
1755
1819
|
ensureDir(configDir);
|
|
1756
1820
|
fs.writeFileSync(path.join(configDir, 'manifest.yaml'), generateInstallManifest(opts));
|
|
1757
|
-
// Install skills + sidebar stubs globally
|
|
1758
|
-
|
|
1821
|
+
// Install skills + sidebar stubs globally — never dedup against globals,
|
|
1822
|
+
// because in --global mode the target IS the global dir.
|
|
1823
|
+
const skillsResult = installSkills(PACKAGE_ROOT, opts.target);
|
|
1824
|
+
let skillsInstalled = skillsResult.count;
|
|
1759
1825
|
try {
|
|
1760
1826
|
const { main: generateCommandSkills } = require(path.join(PACKAGE_ROOT, 'cli', 'generate-command-skills.cjs'));
|
|
1761
1827
|
const stubsDir = path.join(opts.target, '.claude', 'skills');
|
|
@@ -1831,6 +1897,7 @@ async function install(opts) {
|
|
|
1831
1897
|
plan.length = 0;
|
|
1832
1898
|
filtered.forEach(e => plan.push(e));
|
|
1833
1899
|
}
|
|
1900
|
+
|
|
1834
1901
|
} catch { /* non-fatal — skip detection on permission errors */ }
|
|
1835
1902
|
}
|
|
1836
1903
|
|
|
@@ -1855,11 +1922,34 @@ async function install(opts) {
|
|
|
1855
1922
|
} else if (opts.force && (fs.existsSync(configPath) || fs.existsSync(stateDest))) {
|
|
1856
1923
|
existedBefore = true;
|
|
1857
1924
|
}
|
|
1925
|
+
// Note: --reset without --force is rejected at the top of install() (#680).
|
|
1858
1926
|
|
|
1859
1927
|
// Write .rihal/config.yaml (user_name, project_name, language, mode)
|
|
1860
1928
|
// Note: config.yaml is user data and should NOT be overwritten on --force (unless --reset)
|
|
1861
1929
|
if (!fs.existsSync(configPath)) {
|
|
1862
1930
|
fs.writeFileSync(configPath, generateConfigYaml(opts));
|
|
1931
|
+
} else {
|
|
1932
|
+
// Issue #685: re-install path. config.yaml is preserved BUT if the user
|
|
1933
|
+
// just changed commit_planning via the prompt/flag, .gitignore will be
|
|
1934
|
+
// rewritten with the new value while config.yaml keeps the old one,
|
|
1935
|
+
// creating a silent drift. Update only commit_planning in-place
|
|
1936
|
+
// (preserve everything else the user may have customized).
|
|
1937
|
+
try {
|
|
1938
|
+
const before = fs.readFileSync(configPath, 'utf8');
|
|
1939
|
+
const desired = opts.commitPlanning !== false;
|
|
1940
|
+
const re = /^commit_planning:\s*(true|false)\s*$/m;
|
|
1941
|
+
const match = before.match(re);
|
|
1942
|
+
const currentInFile = match ? match[1] === 'true' : null;
|
|
1943
|
+
if (match && currentInFile !== desired) {
|
|
1944
|
+
const updated = before.replace(re, `commit_planning: ${desired}`);
|
|
1945
|
+
fs.writeFileSync(configPath, updated);
|
|
1946
|
+
console.log(' ' + dim(`Updated commit_planning in config.yaml (${currentInFile} → ${desired}) — closes #685.`));
|
|
1947
|
+
} else if (!match) {
|
|
1948
|
+
// Older config without the key — append it so the next read finds it.
|
|
1949
|
+
const appended = before.replace(/\n*$/, '') + `\ncommit_planning: ${desired}\n`;
|
|
1950
|
+
fs.writeFileSync(configPath, appended);
|
|
1951
|
+
}
|
|
1952
|
+
} catch { /* best-effort — never fail install on this */ }
|
|
1863
1953
|
}
|
|
1864
1954
|
// Validate config.yaml with zod schema (#250) — warn but never block install.
|
|
1865
1955
|
try {
|
|
@@ -1917,7 +2007,16 @@ async function install(opts) {
|
|
|
1917
2007
|
|
|
1918
2008
|
// Install v1-style phrase-activated skills (scaffold-project, create-prd,
|
|
1919
2009
|
// retrospective, etc.) into .claude/skills/ alongside the v2 agents/commands.
|
|
1920
|
-
|
|
2010
|
+
// Issue #679: skip rihal-* skills that already exist in ~/.claude/skills/
|
|
2011
|
+
// (global precedence) so the slash picker doesn't show every command twice.
|
|
2012
|
+
// Reuse the isProjectInstall flag declared earlier in this scope.
|
|
2013
|
+
const skillsResult = installSkills(PACKAGE_ROOT, opts.target, {
|
|
2014
|
+
skipGlobalDuplicates: isProjectInstall,
|
|
2015
|
+
});
|
|
2016
|
+
let skillsInstalled = skillsResult.count;
|
|
2017
|
+
if (skillsResult.skippedGlobal > 0) {
|
|
2018
|
+
console.log(' ' + dim(`Skipped ${skillsResult.skippedGlobal} project-level rihal skills (global ones in ~/.claude/skills/ take precedence) — closes #679.`));
|
|
2019
|
+
}
|
|
1921
2020
|
|
|
1922
2021
|
// Generate install-time skill stubs that mirror sidebar-worthy slash commands.
|
|
1923
2022
|
// Source codebase stays clean — these stubs only exist at the install
|
|
@@ -1926,11 +2025,16 @@ async function install(opts) {
|
|
|
1926
2025
|
try {
|
|
1927
2026
|
const { main: generateCommandSkills } = require(path.join(PACKAGE_ROOT, 'cli', 'generate-command-skills.cjs'));
|
|
1928
2027
|
const stubsDir = path.join(opts.target, '.claude', 'skills');
|
|
1929
|
-
const result = generateCommandSkills(PACKAGE_ROOT, stubsDir, readPackageVersion()
|
|
2028
|
+
const result = generateCommandSkills(PACKAGE_ROOT, stubsDir, readPackageVersion(), {
|
|
2029
|
+
skipGlobalDuplicates: isProjectInstall,
|
|
2030
|
+
});
|
|
1930
2031
|
if (result.generated > 0) {
|
|
1931
2032
|
console.log(' ' + dim(`${result.generated} sidebar skill stub${result.generated === 1 ? '' : 's'} generated for command discoverability`));
|
|
1932
2033
|
skillsInstalled += result.generated;
|
|
1933
2034
|
}
|
|
2035
|
+
if (result.skippedGlobal > 0) {
|
|
2036
|
+
console.log(' ' + dim(`Skipped ${result.skippedGlobal} sidebar stub${result.skippedGlobal === 1 ? '' : 's'} that duplicate global ~/.claude/skills/ — closes #679.`));
|
|
2037
|
+
}
|
|
1934
2038
|
} catch (err) {
|
|
1935
2039
|
// Non-fatal: install succeeds without sidebar stubs
|
|
1936
2040
|
console.log(' ' + dim(`(sidebar stub generation skipped: ${err.message})`));
|
package/cli/set-profile.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* rihal-code set-profile — change the model profile for the current project
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* npx @
|
|
6
|
-
* npx @
|
|
7
|
-
* npx @
|
|
8
|
-
* npx @
|
|
9
|
-
* npx @
|
|
5
|
+
* npx @hanzlaa/rcode set-profile balanced
|
|
6
|
+
* npx @hanzlaa/rcode set-profile quality
|
|
7
|
+
* npx @hanzlaa/rcode set-profile budget
|
|
8
|
+
* npx @hanzlaa/rcode set-profile inherit
|
|
9
|
+
* npx @hanzlaa/rcode set-profile # show current
|
|
10
10
|
*
|
|
11
11
|
* This is a thin wrapper over `rihal-code config model_profile <name>`.
|
|
12
12
|
* All config read/write goes through cli/lib/config.cjs, which handles
|
package/cli/show-model.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* rihal-code show-model — print the resolved model for an agent (or all agents)
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* npx @
|
|
6
|
-
* npx @
|
|
7
|
-
* npx @
|
|
5
|
+
* npx @hanzlaa/rcode show-model # all agents in current profile
|
|
6
|
+
* npx @hanzlaa/rcode show-model waleed # single agent
|
|
7
|
+
* npx @hanzlaa/rcode show-model --profile=quality # different profile
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const {
|
package/cli/uninstall.js
CHANGED
|
@@ -250,8 +250,15 @@ function isKnownSkillName(name) {
|
|
|
250
250
|
/**
|
|
251
251
|
* Build the list of files/dirs (relative to cwd) that the uninstall plan
|
|
252
252
|
* will delete or mutate. Used to feed `tar --files-from=-`.
|
|
253
|
+
*
|
|
254
|
+
* @param {object} plan — uninstall plan
|
|
255
|
+
* @param {string} cwd — project root
|
|
256
|
+
* @param {object} [options]
|
|
257
|
+
* @param {boolean} [options.purge=false] — when true, also include .rihal/
|
|
258
|
+
* and .planning/ in the backup so --purge users can recover state.json,
|
|
259
|
+
* decisions, and planning artifacts. Issue #683.
|
|
253
260
|
*/
|
|
254
|
-
function planToPathList(plan, cwd) {
|
|
261
|
+
function planToPathList(plan, cwd, options = {}) {
|
|
255
262
|
const paths = [];
|
|
256
263
|
|
|
257
264
|
for (const name of plan.claude.skills) {
|
|
@@ -278,6 +285,26 @@ function planToPathList(plan, cwd) {
|
|
|
278
285
|
paths.push('AGENTS.md');
|
|
279
286
|
}
|
|
280
287
|
|
|
288
|
+
// Issue #683: --purge wipes .rihal/ AND .planning/ but the backup never
|
|
289
|
+
// included them. User loses state.json, decisions, planning artifacts with
|
|
290
|
+
// no recovery. Add them when purging — but EXCLUDE .rihal/backups/ itself
|
|
291
|
+
// (we'd be writing into the dir we're tar-ing).
|
|
292
|
+
if (options.purge) {
|
|
293
|
+
const rihalDir = path.join(cwd, '.rihal');
|
|
294
|
+
if (fs.existsSync(rihalDir)) {
|
|
295
|
+
// Walk one level deep and add everything except backups/
|
|
296
|
+
try {
|
|
297
|
+
for (const entry of fs.readdirSync(rihalDir)) {
|
|
298
|
+
if (entry === 'backups') continue;
|
|
299
|
+
paths.push(path.join('.rihal', entry));
|
|
300
|
+
}
|
|
301
|
+
} catch { /* fall through; ok=false from tar will warn */ }
|
|
302
|
+
}
|
|
303
|
+
if (fs.existsSync(path.join(cwd, '.planning'))) {
|
|
304
|
+
paths.push('.planning');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
281
308
|
return paths;
|
|
282
309
|
}
|
|
283
310
|
|
|
@@ -288,8 +315,8 @@ function planToPathList(plan, cwd) {
|
|
|
288
315
|
* (tar missing, no paths, etc.); the caller should warn the user but may
|
|
289
316
|
* still proceed since the user already confirmed the destructive action.
|
|
290
317
|
*/
|
|
291
|
-
function createBackup(cwd, plan) {
|
|
292
|
-
const paths = planToPathList(plan, cwd);
|
|
318
|
+
function createBackup(cwd, plan, options = {}) {
|
|
319
|
+
const paths = planToPathList(plan, cwd, { purge: options.purge === true });
|
|
293
320
|
if (paths.length === 0) {
|
|
294
321
|
return { ok: false, warning: 'nothing to back up' };
|
|
295
322
|
}
|
|
@@ -301,11 +328,17 @@ function createBackup(cwd, plan) {
|
|
|
301
328
|
return { ok: false, warning: 'tar not available on this system' };
|
|
302
329
|
}
|
|
303
330
|
|
|
304
|
-
|
|
331
|
+
// Issue #683: when --purge wipes .rihal/, a backup written into
|
|
332
|
+
// .rihal/backups/ would be deleted moments later. Write to a sibling
|
|
333
|
+
// .rihal-backups/ at the project root instead so the backup survives.
|
|
334
|
+
// For non-purge runs, keep the historical .rihal/backups/ location.
|
|
335
|
+
const backupsDir = options.purge
|
|
336
|
+
? path.join(cwd, '.rihal-backups')
|
|
337
|
+
: path.join(cwd, '.rihal/backups');
|
|
305
338
|
try {
|
|
306
339
|
fs.mkdirSync(backupsDir, { recursive: true });
|
|
307
340
|
} catch (err) {
|
|
308
|
-
return { ok: false, warning: `could not create .
|
|
341
|
+
return { ok: false, warning: `could not create ${path.relative(cwd, backupsDir)}/: ${err.message}` };
|
|
309
342
|
}
|
|
310
343
|
|
|
311
344
|
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
@@ -489,10 +522,15 @@ async function runUninstall(args) {
|
|
|
489
522
|
|
|
490
523
|
// Create a timestamped backup before doing anything destructive.
|
|
491
524
|
// Non-fatal on failure — the user already confirmed, we just warn.
|
|
525
|
+
// Issue #683: --purge backs up .rihal/ and .planning/ too so users can
|
|
526
|
+
// recover state.json, decisions log, and planning artifacts.
|
|
492
527
|
console.log();
|
|
493
|
-
const backup = createBackup(cwd, plan);
|
|
528
|
+
const backup = createBackup(cwd, plan, { purge: opts.purge === true });
|
|
494
529
|
if (backup.ok) {
|
|
495
530
|
console.log(` 💾 backup created: ${backup.path}`);
|
|
531
|
+
if (opts.purge) {
|
|
532
|
+
console.log(' includes .rihal/ and .planning/ (state, decisions, planning artifacts)');
|
|
533
|
+
}
|
|
496
534
|
} else {
|
|
497
535
|
console.log(` ⚠ no backup created (${backup.warning}) — continuing anyway`);
|
|
498
536
|
}
|
|
@@ -649,15 +687,29 @@ async function runUninstall(args) {
|
|
|
649
687
|
|
|
650
688
|
// Strip the rcode-managed block from .gitignore. The installer writes
|
|
651
689
|
// a fenced block; we remove it cleanly without touching user lines.
|
|
690
|
+
//
|
|
691
|
+
// Issue #684: previous regex `/\n?# rcode[\s\S]*?(?=\n\n|\n$|$)/g` was a
|
|
692
|
+
// footgun — it matched ANY user line starting with "# rcode" (e.g.
|
|
693
|
+
// "# rcode notes", "# rcode is great") and greedily consumed everything
|
|
694
|
+
// up to the next blank line, silently nuking user content.
|
|
695
|
+
//
|
|
696
|
+
// Three shapes have ever shipped:
|
|
697
|
+
// 1. Current (install.js:653-654): "# ===== rcode-managed gitignore block ... =====" ... "# ===== end rcode-managed gitignore block ====="
|
|
698
|
+
// 2. Old fenced markers: "# >>> rihal-code >>>" ... "# <<< rihal-code <<<"
|
|
699
|
+
// 3. Hypothetical legacy single-line "# rcode" — never actually
|
|
700
|
+
// committed by any installer version we can find. Removed.
|
|
701
|
+
//
|
|
702
|
+
// Both kept patterns require BOTH sentinel markers to be present —
|
|
703
|
+
// user content with "# rcode" prefix is now safe.
|
|
652
704
|
const gitignorePath = path.join(cwd, '.gitignore');
|
|
653
705
|
if (fs.existsSync(gitignorePath)) {
|
|
654
706
|
try {
|
|
655
707
|
const before = fs.readFileSync(gitignorePath, 'utf8');
|
|
656
|
-
// Match either fenced markers or the legacy "# rcode" header through to
|
|
657
|
-
// the next blank line — both shapes the installer has used historically.
|
|
658
708
|
const stripped = before
|
|
709
|
+
// Current shape (install.js BEGIN/END markers — exact match).
|
|
710
|
+
.replace(/\n?# ===== rcode-managed gitignore block[\s\S]*?# ===== end rcode-managed gitignore block =====\n?/g, '\n')
|
|
711
|
+
// Legacy >>> / <<< fenced shape.
|
|
659
712
|
.replace(/\n?# >>> rihal-code >>>[\s\S]*?# <<< rihal-code <<<\n?/g, '\n')
|
|
660
|
-
.replace(/\n?# rcode[\s\S]*?(?=\n\n|\n$|$)/g, '\n')
|
|
661
713
|
.replace(/\n{3,}/g, '\n\n');
|
|
662
714
|
if (stripped !== before) {
|
|
663
715
|
fs.writeFileSync(gitignorePath, stripped);
|
package/dist/rcode.js
CHANGED
|
@@ -15188,10 +15188,23 @@ var require_install = __commonJS({
|
|
|
15188
15188
|
async function resolveCommitPlanning(opts) {
|
|
15189
15189
|
if (opts.commitPlanning !== null) return opts.commitPlanning;
|
|
15190
15190
|
if (opts.noPrompt || opts.global) return false;
|
|
15191
|
-
|
|
15191
|
+
let existingValue = null;
|
|
15192
|
+
try {
|
|
15193
|
+
const cfgPath = path2.join(opts.target, ".rihal", "config.yaml");
|
|
15194
|
+
if (fs2.existsSync(cfgPath)) {
|
|
15195
|
+
const cfg = fs2.readFileSync(cfgPath, "utf8");
|
|
15196
|
+
const m = cfg.match(/^commit_planning:\s*(true|false)\s*$/m);
|
|
15197
|
+
if (m) existingValue = m[1] === "true";
|
|
15198
|
+
}
|
|
15199
|
+
} catch {
|
|
15200
|
+
}
|
|
15201
|
+
if (opts.yes || !process.stdin.isTTY) {
|
|
15202
|
+
return existingValue !== null ? existingValue : true;
|
|
15203
|
+
}
|
|
15204
|
+
const initialValue = existingValue === false ? "gitignore" : "commit";
|
|
15192
15205
|
const choice = await clack.select({
|
|
15193
|
-
message: "\u{1F4CB} .planning/ holds PRDs, roadmaps, sprints, SUMMARY files. How should they be tracked?",
|
|
15194
|
-
initialValue
|
|
15206
|
+
message: existingValue !== null ? "\u{1F4CB} .planning/ tracking \u2014 current setting preserved unless you change it." : "\u{1F4CB} .planning/ holds PRDs, roadmaps, sprints, SUMMARY files. How should they be tracked?",
|
|
15207
|
+
initialValue,
|
|
15195
15208
|
options: [
|
|
15196
15209
|
{ value: "commit", label: "Commit", hint: "collaborators see the same plans (recommended)" },
|
|
15197
15210
|
{ value: "gitignore", label: "Gitignore", hint: "planning stays local (good for sensitive PRDs)" }
|
|
@@ -15312,9 +15325,13 @@ Installs (IDE-specific):
|
|
|
15312
15325
|
fs2.mkdirSync(planningDir, { recursive: true });
|
|
15313
15326
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
15314
15327
|
const name = projectName || path2.basename(target);
|
|
15328
|
+
const STUB_BANNER = `<!-- INSTALL STUB \u2014 overwritten by /rihal-new-project. Delete this file or run
|
|
15329
|
+
/rihal-new-project before committing. See https://github.com/hanzlahabib/rihal-code/issues/670 -->
|
|
15330
|
+
|
|
15331
|
+
`;
|
|
15315
15332
|
fs2.writeFileSync(
|
|
15316
15333
|
projectPath,
|
|
15317
|
-
`# ${name}
|
|
15334
|
+
STUB_BANNER + `# ${name}
|
|
15318
15335
|
|
|
15319
15336
|
**One-line:** Describe what this project is in one sentence.
|
|
15320
15337
|
|
|
@@ -15331,7 +15348,7 @@ What this project delivers and who it serves.
|
|
|
15331
15348
|
);
|
|
15332
15349
|
fs2.writeFileSync(
|
|
15333
15350
|
roadmapPath,
|
|
15334
|
-
`# ${name} \u2014 Roadmap
|
|
15351
|
+
STUB_BANNER + `# ${name} \u2014 Roadmap
|
|
15335
15352
|
|
|
15336
15353
|
**Milestone: M1 \u2014 Initial Delivery** (v1.0)
|
|
15337
15354
|
Started: ${today} \xB7 Current
|
|
@@ -15355,7 +15372,7 @@ Ideas and future phases go here.
|
|
|
15355
15372
|
);
|
|
15356
15373
|
fs2.writeFileSync(
|
|
15357
15374
|
statePath,
|
|
15358
|
-
`# ${name} \u2014 State
|
|
15375
|
+
STUB_BANNER + `# ${name} \u2014 State
|
|
15359
15376
|
|
|
15360
15377
|
**Last updated:** ${today}
|
|
15361
15378
|
**Milestone:** M1 \u2014 Initial Delivery
|
|
@@ -15374,7 +15391,7 @@ _None._
|
|
|
15374
15391
|
|
|
15375
15392
|
## Next Action
|
|
15376
15393
|
|
|
15377
|
-
|
|
15394
|
+
Run \`/rihal-new-project <description>\` to bootstrap, or \`/rihal-sprint-planning\` once a real phase exists.
|
|
15378
15395
|
`
|
|
15379
15396
|
);
|
|
15380
15397
|
const rihalStateJson = path2.join(target, ".rihal", "state.json");
|
|
@@ -15382,16 +15399,15 @@ Say "plan a sprint" or run \`/rihal-sprint-planning\` to break Phase 01 into sto
|
|
|
15382
15399
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15383
15400
|
const state = {
|
|
15384
15401
|
version: "1",
|
|
15385
|
-
project:
|
|
15402
|
+
project: null,
|
|
15403
|
+
_seeded_stub: true,
|
|
15386
15404
|
created: now,
|
|
15387
15405
|
updated: now,
|
|
15388
|
-
current_phase:
|
|
15406
|
+
current_phase: null,
|
|
15389
15407
|
current_plan: 0,
|
|
15390
15408
|
current_sprint: null,
|
|
15391
|
-
milestone:
|
|
15392
|
-
phases: [
|
|
15393
|
-
{ id: "01", name: "Setup & Scaffolding", status: "planned" }
|
|
15394
|
-
],
|
|
15409
|
+
milestone: null,
|
|
15410
|
+
phases: [],
|
|
15395
15411
|
executions: [],
|
|
15396
15412
|
decisions: [],
|
|
15397
15413
|
blockers: [],
|
|
@@ -15584,20 +15600,31 @@ ${BLOCK}`);
|
|
|
15584
15600
|
}
|
|
15585
15601
|
return copied;
|
|
15586
15602
|
}
|
|
15587
|
-
function installSkills(packageRoot, target) {
|
|
15603
|
+
function installSkills(packageRoot, target, options = {}) {
|
|
15588
15604
|
const skillsSource = path2.join(packageRoot, "rihal/skills");
|
|
15589
15605
|
const skillsDest = path2.join(target, ".claude/skills");
|
|
15590
15606
|
const internalDest = path2.join(target, ".rihal/skills");
|
|
15591
|
-
if (!fs2.existsSync(skillsSource)) return 0;
|
|
15607
|
+
if (!fs2.existsSync(skillsSource)) return { count: 0, skippedGlobal: 0 };
|
|
15592
15608
|
fs2.mkdirSync(skillsDest, { recursive: true });
|
|
15593
15609
|
fs2.mkdirSync(internalDest, { recursive: true });
|
|
15610
|
+
const globalSkillsDir = path2.join(os.homedir(), ".claude", "skills");
|
|
15611
|
+
const globalRihalSkills = options.skipGlobalDuplicates && fs2.existsSync(globalSkillsDir) ? new Set(fs2.readdirSync(globalSkillsDir).filter((n) => n.startsWith("rihal-"))) : /* @__PURE__ */ new Set();
|
|
15594
15612
|
let count = 0;
|
|
15613
|
+
let skippedGlobal = 0;
|
|
15595
15614
|
function isInternalSkill(skillDir) {
|
|
15596
15615
|
const skillMd = path2.join(skillDir, "SKILL.md");
|
|
15597
15616
|
if (!fs2.existsSync(skillMd)) return false;
|
|
15598
15617
|
const text = fs2.readFileSync(skillMd, "utf8");
|
|
15599
15618
|
return /^internal:\s*true\s*$/m.test(text);
|
|
15600
15619
|
}
|
|
15620
|
+
function hasLocalOverride(destDir) {
|
|
15621
|
+
if (!fs2.existsSync(destDir)) return false;
|
|
15622
|
+
try {
|
|
15623
|
+
return fs2.readdirSync(destDir).some((f) => f.endsWith(".local.md"));
|
|
15624
|
+
} catch {
|
|
15625
|
+
return false;
|
|
15626
|
+
}
|
|
15627
|
+
}
|
|
15601
15628
|
function walkForSkills(dir) {
|
|
15602
15629
|
if (!fs2.existsSync(dir)) return;
|
|
15603
15630
|
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -15606,7 +15633,18 @@ ${BLOCK}`);
|
|
|
15606
15633
|
const hasSkillMd = fs2.existsSync(path2.join(src, "SKILL.md"));
|
|
15607
15634
|
if (hasSkillMd) {
|
|
15608
15635
|
const destName = entry.name.startsWith("rihal-") ? entry.name : `rihal-${entry.name}`;
|
|
15609
|
-
const
|
|
15636
|
+
const internal = isInternalSkill(src);
|
|
15637
|
+
const dest = internal ? path2.join(internalDest, destName) : path2.join(skillsDest, destName);
|
|
15638
|
+
if (!internal && globalRihalSkills.has(destName) && !hasLocalOverride(dest)) {
|
|
15639
|
+
if (fs2.existsSync(dest)) {
|
|
15640
|
+
try {
|
|
15641
|
+
fs2.rmSync(dest, { recursive: true, force: true });
|
|
15642
|
+
} catch {
|
|
15643
|
+
}
|
|
15644
|
+
}
|
|
15645
|
+
skippedGlobal++;
|
|
15646
|
+
continue;
|
|
15647
|
+
}
|
|
15610
15648
|
copyDirRecursive(src, dest);
|
|
15611
15649
|
count++;
|
|
15612
15650
|
} else {
|
|
@@ -15617,7 +15655,7 @@ ${BLOCK}`);
|
|
|
15617
15655
|
for (const bucket of ["agents", "actions", "core"]) {
|
|
15618
15656
|
walkForSkills(path2.join(skillsSource, bucket));
|
|
15619
15657
|
}
|
|
15620
|
-
return count;
|
|
15658
|
+
return { count, skippedGlobal };
|
|
15621
15659
|
}
|
|
15622
15660
|
function parseFrontmatter(text) {
|
|
15623
15661
|
if (!text.startsWith("---\n")) return { frontmatter: {}, body: text };
|
|
@@ -16001,6 +16039,15 @@ ${BLOCK}`);
|
|
|
16001
16039
|
printHelp2();
|
|
16002
16040
|
return 0;
|
|
16003
16041
|
}
|
|
16042
|
+
if (opts.reset && !opts.force) {
|
|
16043
|
+
console.log("");
|
|
16044
|
+
console.log(" " + warn("--reset has no effect without --force."));
|
|
16045
|
+
console.log(" " + dim(" --reset wipes config.yaml and state.json. To prevent accidental data loss,"));
|
|
16046
|
+
console.log(" " + dim(" it must be paired with --force. Re-run as:"));
|
|
16047
|
+
console.log(" " + dim(" rcode install --reset --force"));
|
|
16048
|
+
console.log("");
|
|
16049
|
+
return 2;
|
|
16050
|
+
}
|
|
16004
16051
|
const pkgVersion = readPackageVersion();
|
|
16005
16052
|
const isInteractive = process.stdin.isTTY && !opts.yes;
|
|
16006
16053
|
if (isInteractive) printInstallHeader(pkgVersion);
|
|
@@ -16273,7 +16320,8 @@ ${BLOCK}`);
|
|
|
16273
16320
|
const configDir2 = path2.join(opts.target, ".rihal", "_config");
|
|
16274
16321
|
ensureDir(configDir2);
|
|
16275
16322
|
fs2.writeFileSync(path2.join(configDir2, "manifest.yaml"), generateInstallManifest(opts));
|
|
16276
|
-
|
|
16323
|
+
const skillsResult2 = installSkills(PACKAGE_ROOT2, opts.target);
|
|
16324
|
+
let skillsInstalled2 = skillsResult2.count;
|
|
16277
16325
|
try {
|
|
16278
16326
|
const { main: generateCommandSkills } = require(path2.join(PACKAGE_ROOT2, "cli", "generate-command-skills.cjs"));
|
|
16279
16327
|
const stubsDir = path2.join(opts.target, ".claude", "skills");
|
|
@@ -16353,6 +16401,25 @@ ${BLOCK}`);
|
|
|
16353
16401
|
}
|
|
16354
16402
|
if (!fs2.existsSync(configPath)) {
|
|
16355
16403
|
fs2.writeFileSync(configPath, generateConfigYaml(opts));
|
|
16404
|
+
} else {
|
|
16405
|
+
try {
|
|
16406
|
+
const before = fs2.readFileSync(configPath, "utf8");
|
|
16407
|
+
const desired = opts.commitPlanning !== false;
|
|
16408
|
+
const re = /^commit_planning:\s*(true|false)\s*$/m;
|
|
16409
|
+
const match = before.match(re);
|
|
16410
|
+
const currentInFile = match ? match[1] === "true" : null;
|
|
16411
|
+
if (match && currentInFile !== desired) {
|
|
16412
|
+
const updated = before.replace(re, `commit_planning: ${desired}`);
|
|
16413
|
+
fs2.writeFileSync(configPath, updated);
|
|
16414
|
+
console.log(" " + dim(`Updated commit_planning in config.yaml (${currentInFile} \u2192 ${desired}) \u2014 closes #685.`));
|
|
16415
|
+
} else if (!match) {
|
|
16416
|
+
const appended = before.replace(/\n*$/, "") + `
|
|
16417
|
+
commit_planning: ${desired}
|
|
16418
|
+
`;
|
|
16419
|
+
fs2.writeFileSync(configPath, appended);
|
|
16420
|
+
}
|
|
16421
|
+
} catch {
|
|
16422
|
+
}
|
|
16356
16423
|
}
|
|
16357
16424
|
try {
|
|
16358
16425
|
const configText = fs2.readFileSync(configPath, "utf8");
|
|
@@ -16392,15 +16459,26 @@ ${BLOCK}`);
|
|
|
16392
16459
|
path2.join(configDir, "files-manifest.csv"),
|
|
16393
16460
|
generateFilesManifest(plan, opts.target, { mergeExistingManifest: !opts.force })
|
|
16394
16461
|
);
|
|
16395
|
-
|
|
16462
|
+
const skillsResult = installSkills(PACKAGE_ROOT2, opts.target, {
|
|
16463
|
+
skipGlobalDuplicates: isProjectInstall
|
|
16464
|
+
});
|
|
16465
|
+
let skillsInstalled = skillsResult.count;
|
|
16466
|
+
if (skillsResult.skippedGlobal > 0) {
|
|
16467
|
+
console.log(" " + dim(`Skipped ${skillsResult.skippedGlobal} project-level rihal skills (global ones in ~/.claude/skills/ take precedence) \u2014 closes #679.`));
|
|
16468
|
+
}
|
|
16396
16469
|
try {
|
|
16397
16470
|
const { main: generateCommandSkills } = require(path2.join(PACKAGE_ROOT2, "cli", "generate-command-skills.cjs"));
|
|
16398
16471
|
const stubsDir = path2.join(opts.target, ".claude", "skills");
|
|
16399
|
-
const result = generateCommandSkills(PACKAGE_ROOT2, stubsDir, readPackageVersion()
|
|
16472
|
+
const result = generateCommandSkills(PACKAGE_ROOT2, stubsDir, readPackageVersion(), {
|
|
16473
|
+
skipGlobalDuplicates: isProjectInstall
|
|
16474
|
+
});
|
|
16400
16475
|
if (result.generated > 0) {
|
|
16401
16476
|
console.log(" " + dim(`${result.generated} sidebar skill stub${result.generated === 1 ? "" : "s"} generated for command discoverability`));
|
|
16402
16477
|
skillsInstalled += result.generated;
|
|
16403
16478
|
}
|
|
16479
|
+
if (result.skippedGlobal > 0) {
|
|
16480
|
+
console.log(" " + dim(`Skipped ${result.skippedGlobal} sidebar stub${result.skippedGlobal === 1 ? "" : "s"} that duplicate global ~/.claude/skills/ \u2014 closes #679.`));
|
|
16481
|
+
}
|
|
16404
16482
|
} catch (err) {
|
|
16405
16483
|
console.log(" " + dim(`(sidebar stub generation skipped: ${err.message})`));
|
|
16406
16484
|
}
|
|
@@ -17666,7 +17744,7 @@ var require_uninstall = __commonJS({
|
|
|
17666
17744
|
function isKnownSkillName(name) {
|
|
17667
17745
|
return KNOWN_ACTION_SKILLS.includes(name);
|
|
17668
17746
|
}
|
|
17669
|
-
function planToPathList(plan, cwd) {
|
|
17747
|
+
function planToPathList(plan, cwd, options = {}) {
|
|
17670
17748
|
const paths = [];
|
|
17671
17749
|
for (const name of plan.claude.skills) {
|
|
17672
17750
|
paths.push(path2.join(".claude/skills", name));
|
|
@@ -17689,10 +17767,25 @@ var require_uninstall = __commonJS({
|
|
|
17689
17767
|
if (plan.agentsMd && fs2.existsSync(path2.join(cwd, "AGENTS.md"))) {
|
|
17690
17768
|
paths.push("AGENTS.md");
|
|
17691
17769
|
}
|
|
17770
|
+
if (options.purge) {
|
|
17771
|
+
const rihalDir = path2.join(cwd, ".rihal");
|
|
17772
|
+
if (fs2.existsSync(rihalDir)) {
|
|
17773
|
+
try {
|
|
17774
|
+
for (const entry of fs2.readdirSync(rihalDir)) {
|
|
17775
|
+
if (entry === "backups") continue;
|
|
17776
|
+
paths.push(path2.join(".rihal", entry));
|
|
17777
|
+
}
|
|
17778
|
+
} catch {
|
|
17779
|
+
}
|
|
17780
|
+
}
|
|
17781
|
+
if (fs2.existsSync(path2.join(cwd, ".planning"))) {
|
|
17782
|
+
paths.push(".planning");
|
|
17783
|
+
}
|
|
17784
|
+
}
|
|
17692
17785
|
return paths;
|
|
17693
17786
|
}
|
|
17694
|
-
function createBackup(cwd, plan) {
|
|
17695
|
-
const paths = planToPathList(plan, cwd);
|
|
17787
|
+
function createBackup(cwd, plan, options = {}) {
|
|
17788
|
+
const paths = planToPathList(plan, cwd, { purge: options.purge === true });
|
|
17696
17789
|
if (paths.length === 0) {
|
|
17697
17790
|
return { ok: false, warning: "nothing to back up" };
|
|
17698
17791
|
}
|
|
@@ -17700,11 +17793,11 @@ var require_uninstall = __commonJS({
|
|
|
17700
17793
|
if (tarCheck.status !== 0) {
|
|
17701
17794
|
return { ok: false, warning: "tar not available on this system" };
|
|
17702
17795
|
}
|
|
17703
|
-
const backupsDir = path2.join(cwd, ".rihal/backups");
|
|
17796
|
+
const backupsDir = options.purge ? path2.join(cwd, ".rihal-backups") : path2.join(cwd, ".rihal/backups");
|
|
17704
17797
|
try {
|
|
17705
17798
|
fs2.mkdirSync(backupsDir, { recursive: true });
|
|
17706
17799
|
} catch (err) {
|
|
17707
|
-
return { ok: false, warning: `could not create .
|
|
17800
|
+
return { ok: false, warning: `could not create ${path2.relative(cwd, backupsDir)}/: ${err.message}` };
|
|
17708
17801
|
}
|
|
17709
17802
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
17710
17803
|
const backupFile = path2.join(backupsDir, `uninstall-${ts}.tgz`);
|
|
@@ -17845,9 +17938,12 @@ var require_uninstall = __commonJS({
|
|
|
17845
17938
|
}
|
|
17846
17939
|
}
|
|
17847
17940
|
console.log();
|
|
17848
|
-
const backup = createBackup(cwd, plan);
|
|
17941
|
+
const backup = createBackup(cwd, plan, { purge: opts.purge === true });
|
|
17849
17942
|
if (backup.ok) {
|
|
17850
17943
|
console.log(` \u{1F4BE} backup created: ${backup.path}`);
|
|
17944
|
+
if (opts.purge) {
|
|
17945
|
+
console.log(" includes .rihal/ and .planning/ (state, decisions, planning artifacts)");
|
|
17946
|
+
}
|
|
17851
17947
|
} else {
|
|
17852
17948
|
console.log(` \u26A0 no backup created (${backup.warning}) \u2014 continuing anyway`);
|
|
17853
17949
|
}
|
|
@@ -17977,7 +18073,7 @@ var require_uninstall = __commonJS({
|
|
|
17977
18073
|
if (fs2.existsSync(gitignorePath)) {
|
|
17978
18074
|
try {
|
|
17979
18075
|
const before = fs2.readFileSync(gitignorePath, "utf8");
|
|
17980
|
-
const stripped = before.replace(/\n?#
|
|
18076
|
+
const stripped = before.replace(/\n?# ===== rcode-managed gitignore block[\s\S]*?# ===== end rcode-managed gitignore block =====\n?/g, "\n").replace(/\n?# >>> rihal-code >>>[\s\S]*?# <<< rihal-code <<<\n?/g, "\n").replace(/\n{3,}/g, "\n\n");
|
|
17981
18077
|
if (stripped !== before) {
|
|
17982
18078
|
fs2.writeFileSync(gitignorePath, stripped);
|
|
17983
18079
|
console.log(` \u2713 stripped rcode block from .gitignore (--purge)`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.23",
|
|
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": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"postinstall": "node cli/postinstall.js",
|
|
16
16
|
"build:cli": "node scripts/build.cjs",
|
|
17
17
|
"build": "node scripts/build.cjs",
|
|
18
|
+
"prepack": "node scripts/build.cjs",
|
|
18
19
|
"dogfood": "bash scripts/dogfood-check.sh"
|
|
19
20
|
},
|
|
20
21
|
"files": [
|
|
@@ -985,6 +985,26 @@ function cmdState(subArgs) {
|
|
|
985
985
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
986
986
|
}
|
|
987
987
|
|
|
988
|
+
// Issue #681: auto-clear the install-time _seeded_stub marker once the
|
|
989
|
+
// state has graduated to a real project (project field set + at least one
|
|
990
|
+
// real phase OR REQUIREMENTS.md present). project-status (#675) reads
|
|
991
|
+
// _seeded_stub; if no writer ever clears it, every project stays "stub"
|
|
992
|
+
// forever and downstream workflows misroute.
|
|
993
|
+
if (state._seeded_stub === true) {
|
|
994
|
+
const phases = Array.isArray(state.phases) ? state.phases : [];
|
|
995
|
+
const firstPhaseName = phases[0]?.name || '';
|
|
996
|
+
const hasRealPhase = phases.length > 1 ||
|
|
997
|
+
(firstPhaseName && firstPhaseName !== 'Setup & Scaffolding');
|
|
998
|
+
const hasRequirements = (() => {
|
|
999
|
+
try {
|
|
1000
|
+
return fs.existsSync(path.join(PROJECT_ROOT, '.planning', 'REQUIREMENTS.md'));
|
|
1001
|
+
} catch { return false; }
|
|
1002
|
+
})();
|
|
1003
|
+
if ((state.project && hasRealPhase) || hasRequirements) {
|
|
1004
|
+
delete state._seeded_stub;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
988
1008
|
state.updated = new Date().toISOString();
|
|
989
1009
|
fs.mkdirSync(RIHAL_DIR, { recursive: true });
|
|
990
1010
|
const lockPath = statePath + '.lock';
|
|
@@ -1069,6 +1089,23 @@ function cmdState(subArgs) {
|
|
|
1069
1089
|
return state;
|
|
1070
1090
|
}
|
|
1071
1091
|
|
|
1092
|
+
// --- clear-stub --- (issue #681)
|
|
1093
|
+
// Explicit way to flip _seeded_stub off. Useful for /rihal-new-project once
|
|
1094
|
+
// PROJECT.md / REQUIREMENTS.md / ROADMAP.md are committed. The auto-clear in
|
|
1095
|
+
// writeState() also handles this, but having an explicit subcommand lets
|
|
1096
|
+
// workflows be self-documenting and idempotent.
|
|
1097
|
+
if (sub === 'clear-stub') {
|
|
1098
|
+
if (!fs.existsSync(statePath)) {
|
|
1099
|
+
return { ok: false, error: 'No state.json — nothing to clear.' };
|
|
1100
|
+
}
|
|
1101
|
+
const state = readState();
|
|
1102
|
+
if (!state) return { ok: false, error: 'state.json unreadable' };
|
|
1103
|
+
const wasStub = state._seeded_stub === true;
|
|
1104
|
+
if (wasStub) delete state._seeded_stub;
|
|
1105
|
+
writeState(state);
|
|
1106
|
+
return { ok: true, was_stub: wasStub, project: state.project || null };
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1072
1109
|
// --- init ---
|
|
1073
1110
|
if (sub === 'init') {
|
|
1074
1111
|
let existing;
|