@ericrisco/rsc 1.0.29 → 1.0.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/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.29",
2
+ "version": "1.0.30",
3
3
  "counts": {
4
4
  "skills": 276
5
5
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "1.0.29",
3
+ "version": "1.0.30",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,11 +54,62 @@ if (typeof cmd !== 'string' || !cmd) allow();
54
54
  // High-signal, low-false-positive. Each rule: what it catches and the plain why.
55
55
  const noWhere = (verbRe) => verbRe.test(cmd) && !/\bwhere\b/i.test(cmd);
56
56
 
57
+ // A flag is a whitespace-separated token that BEGINS with a dash. That sounds obvious, and the
58
+ // first version of this rule got it wrong in a way that cost real trust: it tested
59
+ // /-[a-z]*r[a-z]*\b/ and /-[a-z]*f[a-z]*\b/ against the ENTIRE command string, so any hyphenated
60
+ // path segment carrying an "r" and an "f" read as the flags -r and -f. The segment `-fixture` alone
61
+ // satisfies both (f-i-x-t-u-r-e), so `rm skills/zz-case-fixture/SKILL.md` and `rm my-draft.md` were
62
+ // DENIED with the reason "rm with -r and -f" — a claim that was false about the command in front of
63
+ // it. It also denied commands that merely QUOTED a dangerous one, which is the same mistake the
64
+ // gitmoji guard had to fix: blocking the conversation about a command, not the command.
65
+ //
66
+ // That is worse than a nuisance. The deny message ends by telling the user how to switch this guard
67
+ // OFF, so crying wolf on a single-file delete is an active instruction to disable the thing that
68
+ // stops a real recursive wipe. Friction has to stay proportional to risk or the protection gets
69
+ // turned off (P7), and a denial's stated reason has to be true (P6).
70
+ //
71
+ // Flags are also scoped to the segment that actually runs rm: `rm notes.md && ls -lrtF` carries an
72
+ // r and an F, but they belong to ls.
73
+ function rmFlagsIn(segment) {
74
+ return segment.split(/\s+/).filter((t) => /^--?[A-Za-z]/.test(t));
75
+ }
76
+
77
+ // Wrappers that stand in front of the real command without being it. A shell with -c is included
78
+ // deliberately: `bash -c "rm -rf /"` DOES run the delete, and dropping it would have been the hole
79
+ // this rewrite opened while fixing the over-fire.
80
+ const WRAPPERS = /^(sudo|doas|env|command|time|nohup|xargs)$/i;
81
+ const SHELLS = /^(bash|sh|zsh|dash|ksh)$/i;
82
+
83
+ // `rm` has to be the command being EXECUTED, not a word inside someone else's argument. Without
84
+ // this, `echo "never run rm -rf /"` was denied — the guard blocking the conversation about a
85
+ // command instead of the command, exactly the defect the gitmoji guard had to fix with its own
86
+ // RUNS_GIT check. A guard that argues with you about a sentence is a guard you turn off.
87
+ function runsRm(segment) {
88
+ const tokens = segment.trim().split(/\s+/).filter(Boolean);
89
+ let i = 0;
90
+ while (i < tokens.length) {
91
+ const bare = tokens[i].replace(/^['"]/, '');
92
+ if (WRAPPERS.test(bare)) { i += 1; continue; }
93
+ if (SHELLS.test(bare)) {
94
+ i += 1;
95
+ while (i < tokens.length && /^-/.test(tokens[i])) i += 1; // skip -c and friends
96
+ continue;
97
+ }
98
+ return bare === 'rm' || bare.endsWith('/rm');
99
+ }
100
+ return false;
101
+ }
102
+
57
103
  function isRmRecursiveForce() {
58
104
  if (!/\brm\b/.test(cmd)) return false;
59
- const hasR = /(-[a-z]*r[a-z]*|--recursive)\b/i.test(cmd);
60
- const hasF = /(-[a-z]*f[a-z]*|--force)\b/i.test(cmd);
61
- return hasR && hasF; // -rf / -fr / -r -f / --recursive --force
105
+ for (const segment of cmd.split(/\|\||&&|[|;&]/)) {
106
+ if (!runsRm(segment)) continue;
107
+ const flags = rmFlagsIn(segment);
108
+ const hasR = flags.some((f) => /^--recursive$/i.test(f) || /^-[A-Za-z]*r/i.test(f));
109
+ const hasF = flags.some((f) => /^--force$/i.test(f) || /^-[A-Za-z]*f/i.test(f));
110
+ if (hasR && hasF) return true; // -rf / -fr / -Rf / -r -f / --recursive --force
111
+ }
112
+ return false;
62
113
  }
63
114
 
64
115
  const RULES = [