@ssheleg/sheleg-dev 0.10.4 → 0.10.5

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/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@ All notable changes to this project are documented here.
4
4
  Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
  Versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.10.5 — the installers refuse the shadow they used to write
8
+
9
+ Both install channels — `npx @ssheleg/sheleg-dev` and `install.sh` — now consult the
10
+ target home's `~/.claude/plugins/installed_plugins.json` before writing anything under
11
+ `~/.claude/skills/`. If the sheleg-dev **plugin** is installed there, the install is
12
+ **refused with exit 3**: a plain copy beside a plugin shadows the plugin's skill of the
13
+ same name and serves the frozen version forever, and until this release neither channel
14
+ looked at all. Reproduced live in the family on 2026-08-29: a bare
15
+ `npx @ssheleg/telegram-dev` shipped three shadows while the plugin was enabled.
16
+
17
+ - `installed_plugins.json` is the signal, because it is the record of what is actually
18
+ installed. A check keyed on the `plugins/marketplaces/<name>` directory alone — the
19
+ shape other family members carried — is the fail-open class: a marketplace added from
20
+ a local `directory` source has no dir there, and plugin names differ from marketplace
21
+ names. The directory is kept only as a fallback signal.
22
+ - The refusal names its remedy with the **real spec read from the JSON**
23
+ (`claude plugin marketplace update sheleg-dev`, `claude plugin update <spec>`), plus
24
+ the family launcher line, and offers `--force` as the explicit override for running
25
+ two channels deliberately.
26
+ - An absent or corrupt `installed_plugins.json` reads as "no plugin": the check fails
27
+ open and never crashes an install — the fresh HOME is the common case.
28
+ - New suite `test/installer_test.js` (11 cases, both channels, throwaway HOMEs), wired
29
+ into `npm test` and CI. **Watched failing first**: run against the pre-refusal
30
+ installers it went red on 7 of 11 cases, each showing the shadow landing at exit 0.
31
+ - Both success paths now end by saying how the next version arrives — the update line —
32
+ and `install.sh` gained `--force`/usage parsing (unknown argument exits 2) to carry
33
+ the same contract as the npm channel.
34
+ - `.claude-plugin/marketplace.json` still said "Six integration skills" while seven
35
+ ship (the validator counts 7; README and SECURITY say seven). It now says seven and
36
+ names error tracking — the second hand-written count this repository has caught after
37
+ the fact, which is why the new suite derives the roster from the tree.
38
+ - `SECURITY.md`'s installer contract moved with the behaviour: the read surface now
39
+ includes the two read-only looks at the target home, and the I/O-surface grep line
40
+ count was re-measured (twelve, and it was already twelve before this change — the
41
+ "eleven" had rotted when a comment naming `rm -rf` arrived).
42
+
7
43
  ## v0.10.4 — the channel that sends the installs, on npm too
8
44
 
9
45
  - The `skills.sh` badge and the canonical `homepage` reached GitHub in the previous cycle and stopped
package/README.md CHANGED
@@ -118,6 +118,12 @@ a Lighthouse score.
118
118
  npx @ssheleg/sheleg-dev
119
119
  ```
120
120
 
121
+ If the sheleg-dev **plugin** is already installed in that home, the npm installer
122
+ and `install.sh` refuse with exit 3 instead of writing plain copies — a plain
123
+ copy shadows the plugin's skill of the same name and serves the frozen version
124
+ forever. The refusal prints the plugin-channel update commands; `--force`
125
+ overrides it, deliberately.
126
+
121
127
  **Any of 70+ agents:**
122
128
 
123
129
  ```bash
package/SECURITY.md CHANGED
@@ -33,7 +33,7 @@ no socket; the greps below prove all three.
33
33
  | `plugins/sheleg-dev/hooks/money-gate.js` | 1 | Runs on a tool call, when the plugin is enabled. Requires `path` and its own decision module — nothing else. Reads stdin, writes one JSON line, exits 0 on every path. |
34
34
  | `plugins/sheleg-dev/hooks/lib/moneygate.js` | 1 | Pure. Payload and environment in, verdict out. No `require` at all, no filesystem, no clock. |
35
35
  | `bin/sheleg-dev.js` — the npm installer | 1 | Runs only when you invoke it. Node built-ins only: `fs`, `path`, `os`. |
36
- | `install.sh` — the shell installer | 1 | **Not in the tarball** — `files` ships `bin/` and `plugins/`, so this one reaches you only through a clone. Runs when you invoke it. Coreutils only: `cd`, `pwd`, `dirname`, `basename`, `mkdir`, `rm`, `cp`, `echo`. It is the destructive channel: `rm -rf` per skill, then `cp -R`. |
36
+ | `install.sh` — the shell installer | 1 | **Not in the tarball** — `files` ships `bin/` and `plugins/`, so this one reaches you only through a clone. Runs when you invoke it. Shell tools only: `cd`, `pwd`, `dirname`, `basename`, `mkdir`, `rm`, `cp`, `echo`, plus `sed` and `head` to read the plugin registry it refuses to shadow. It is the destructive channel: `rm -rf` per skill, then `cp -R`. |
37
37
  | `plugins/sheleg-dev/skills/*/fixtures/*.json` — webhook payloads | 16 | Data. Placeholder ids only; no key, token, signing secret or real customer id. |
38
38
  | `plugins/sheleg-dev/skills/*/fixtures/*.mjs` — the assertion packs and their reference implementations | 4 | Runs only when you invoke it. Reads the JSON beside it with `readFileSync` and nothing else: no `child_process`, no `fetch`, no write, no `process.env`. |
39
39
  | `plugins/sheleg-dev/skills/*/fixtures/manifest.json` and its `README.md` | 4 | Text. The invariant-to-fixture-to-document map, which `test/validate.py` reads. |
@@ -61,24 +61,32 @@ nothing but the CLI you typed.
61
61
  ## What the installers read and write
62
62
 
63
63
  Both do one thing: copy the seven skill directories out of the package into
64
- `~/.claude/skills/<name>`.
65
-
66
- - **Read:** only files inside the package.
64
+ `~/.claude/skills/<name>` — unless the sheleg-dev **plugin** is already installed
65
+ in that home, in which case both refuse with exit 3 rather than write plain
66
+ copies that would shadow the plugin's skills and serve this frozen version
67
+ forever. `--force` overrides the refusal, deliberately.
68
+
69
+ - **Read:** files inside the package, plus two read-only looks at the target
70
+ home to decide the refusal above: `~/.claude/plugins/installed_plugins.json`
71
+ (the record of installed plugins; a missing or unparsable file reads as "no
72
+ plugin", so the check fails open and never crashes an install) and whether
73
+ `~/.claude/plugins/marketplaces/sheleg-dev` exists, kept only as a fallback
74
+ signal.
67
75
  - **Write:** only `~/.claude/skills/` — seven directories, named for the seven skills.
68
76
  Nothing else on the filesystem, nothing outside `$HOME`.
69
77
  - **Network: none.** Neither file opens a socket or resolves a hostname.
70
78
  `bin/sheleg-dev.js` requires `fs`, `path` and `os` and nothing else — no
71
79
  `child_process`, no `fetch`, no `http` — so it spawns no process at all;
72
- `install.sh` runs only the coreutils named above. The grep below prints the
73
- whole surface of both files in eleven lines.
80
+ `install.sh` runs only the shell tools named above. The grep below prints the
81
+ whole surface of both files in twelve lines.
74
82
  - **No telemetry, no analytics, no phone-home.** Nothing here has anywhere to
75
83
  send anything.
76
84
 
77
85
  **One destructive difference between the two installers.** `bin/sheleg-dev.js`
78
86
  skips a skill that is already installed and says so, unless you pass `--force`
79
- (`bin/sheleg-dev.js:45-46`). `install.sh` does not ask: for each of the seven names
87
+ (`bin/sheleg-dev.js:89-90`). `install.sh` does not ask: for each of the seven names
80
88
  it runs `rm -rf` on the destination and re-copies, every time
81
- (`install.sh:21-22`). If you have hand-edited an installed skill, the shell
89
+ (`install.sh:62-63`). If you have hand-edited an installed skill, the shell
82
90
  installer deletes your edits and the node installer does not.
83
91
 
84
92
  ## Credentials
@@ -193,14 +201,22 @@ node test/moneygate_test.js
193
201
  # The command prints its own count.
194
202
  node test/fixtures_test.js
195
203
 
204
+ # Both installers against throwaway HOMEs: fresh install, rerun, --force, and the
205
+ # plugin-present refusal — exit 3, the remedy naming the real plugin spec, nothing
206
+ # written; a corrupt ~/.claude/plugins/installed_plugins.json fails open. The command
207
+ # prints its own count.
208
+ node test/installer_test.js
209
+
196
210
  # What the assertion packs cannot reach: NO OUTPUT, and grep exits 1 because it
197
211
  # matched nothing. `readFileSync` of the fixtures beside them is their whole I/O.
198
212
  grep -rnE "child_process|\bfetch\(|require\('(http|https|net|dns|tls)'\)|spawn\(|execFile|writeFileSync|appendFileSync|unlinkSync|process\.env\." \
199
213
  plugins/sheleg-dev/skills/*/fixtures/*.mjs
200
214
 
201
- # The entire I/O surface of the only two executable files: eleven lines. Three
202
- # built-in requires, the copy/mkdir/remove calls, homedir, and install.sh's
203
- # rm -rf + cp -R. No process, no socket, no network.
215
+ # The entire I/O surface of the only two executable files: twelve lines. Three
216
+ # built-in requires, the copy/mkdir/remove calls, homedir, install.sh's
217
+ # rm -rf + cp -R, and one comment naming rm -rf. No process, no socket, no
218
+ # network — the plugin-registry read that decides the shadow refusal is
219
+ # readFileSync in the node file and sed in the shell one.
204
220
  grep -nE "require|child_process|exec|spawn|fetch|socket|rm -rf|cp -R|copyFileSync|mkdirSync|rmSync|homedir" bin/sheleg-dev.js install.sh
205
221
 
206
222
  # Live-key shapes across the skill payload: one line, and it is a placeholder.
package/bin/sheleg-dev.js CHANGED
@@ -17,6 +17,44 @@ const os = require('os');
17
17
  const ROOT = path.resolve(__dirname, '..');
18
18
  const REPO = 'ssheleg/sheleg-dev';
19
19
 
20
+ // Exit codes are the contract: 0 installed or skipped, 1 corrupted package,
21
+ // 2 usage error, 3 refused — the plugin channel owns this agent (--force overrides).
22
+ const EXIT_PLUGIN_PRESENT = 3;
23
+
24
+ /**
25
+ * The plugin spec (`<name>@<marketplace>`) installed for `name` in this home,
26
+ * or null.
27
+ *
28
+ * `installed_plugins.json` is the record of what is actually installed. The
29
+ * `plugins/marketplaces/<name>` directory — the check other members of this
30
+ * family keyed on until 2026-08-29 — under-reports: a marketplace added from a
31
+ * local `directory` source has no dir there at all, and plugin names differ
32
+ * from marketplace names, so a check keyed on it stays green while the shadow
33
+ * lands. Absence and corruption both read as "no plugin": the fresh HOME is
34
+ * the common case, and an installer that crashes on a parse error refuses the
35
+ * machines that need it most.
36
+ */
37
+ function installedPluginSpec(home, name) {
38
+ try {
39
+ const raw = fs.readFileSync(
40
+ path.join(home, '.claude', 'plugins', 'installed_plugins.json'), 'utf8');
41
+ const parsed = JSON.parse(raw);
42
+ const plugins =
43
+ parsed && typeof parsed === 'object' &&
44
+ parsed.plugins && typeof parsed.plugins === 'object'
45
+ ? parsed.plugins
46
+ : parsed;
47
+ if (!plugins || typeof plugins !== 'object') return null;
48
+ for (const spec of Object.keys(plugins)) {
49
+ if (spec === name) return `${name}@${name}`;
50
+ if (spec.startsWith(name + '@')) return spec;
51
+ }
52
+ } catch {
53
+ // missing or corrupt = no plugin — fail open on absence, never crash
54
+ }
55
+ return null;
56
+ }
57
+
20
58
  function usage() {
21
59
  console.log(`sheleg-dev installer
22
60
 
@@ -25,6 +63,12 @@ Usage:
25
63
  into ~/.claude (skip existing unless --force)
26
64
  npx @ssheleg/sheleg-dev --help
27
65
 
66
+ Exit codes:
67
+ 0 installed or skipped 2 usage error
68
+ 1 corrupted package 3 refused: the sheleg-dev PLUGIN is installed in
69
+ this home — plain copies would shadow its skills
70
+ (pass --force to write them anyway)
71
+
28
72
  Other install paths:
29
73
  Claude Code plugin: /plugin marketplace add ${REPO}
30
74
  /plugin install sheleg-dev@sheleg-dev
@@ -84,6 +128,39 @@ function main(argv) {
84
128
  }
85
129
 
86
130
  const home = os.homedir();
131
+
132
+ // One channel per agent. Plain copies under ~/.claude/skills/ beside the
133
+ // installed sheleg-dev plugin are a second listing of the same skills, and
134
+ // the stale copies win — the exact shadow this family's canon forbids.
135
+ // Refuse rather than create them, and refuse LOUDLY: a marketplaces/-dir
136
+ // check alone is the fail-open class (a directory-sourced marketplace has no
137
+ // dir there, plugin names differ from marketplace names), and a refusal that
138
+ // exits 0 reads as success to every script above it. Reproduced live
139
+ // 2026-08-29: a bare `npx @ssheleg/telegram-dev` shipped three shadows past
140
+ // exactly that hole while the plugin was enabled.
141
+ const spec = installedPluginSpec(home, 'sheleg-dev');
142
+ const marketplace = path.join(home, '.claude', 'plugins', 'marketplaces', 'sheleg-dev');
143
+ const viaMarketplaceDir = !spec && fs.existsSync(marketplace);
144
+ if ((spec || viaMarketplaceDir) && !force) {
145
+ const found = spec
146
+ ? `installed as the Claude Code plugin ${spec}\n` +
147
+ ' (declared in ~/.claude/plugins/installed_plugins.json)'
148
+ : `registered as a Claude Code marketplace\n (${marketplace})`;
149
+ console.error(
150
+ `refused: sheleg-dev is already ${found}.\n` +
151
+ ` Plain copies in ~/.claude/skills/ would shadow the plugin's\n` +
152
+ ` ${names.length} skills and serve this frozen version forever. Update the\n` +
153
+ ' plugin channel instead:\n' +
154
+ ' claude plugin marketplace update sheleg-dev\n' +
155
+ ` claude plugin update ${spec || 'sheleg-dev@sheleg-dev'}\n` +
156
+ ' Family launcher (updates every member, prunes shadow copies):\n' +
157
+ ' npx --yes sshlg-skills@latest update\n' +
158
+ ' Pass --force to write the plain copies anyway — a deliberate choice\n' +
159
+ ' to run two channels, where the stale one wins.'
160
+ );
161
+ return EXIT_PLUGIN_PRESENT;
162
+ }
163
+
87
164
  for (const name of names) {
88
165
  installOne(
89
166
  `${name} skill`,
@@ -111,6 +188,15 @@ function main(argv) {
111
188
  console.log('skills copy. To get it here, register it yourself — README.md, section');
112
189
  console.log('"The manual gate", has the settings snippet. Nothing enforces this step.');
113
190
  }
191
+ // The last line says how the next version arrives — "Installed" is not a
192
+ // complete sentence. Auto-update is off on purpose: this member composes
193
+ // with its family, and per-marketplace autoUpdate moves each member on its
194
+ // own clock, into combinations nobody tested together.
195
+ console.log(
196
+ '\nUpdates: rerun `npx @ssheleg/sheleg-dev@latest --force`, or refresh the\n' +
197
+ 'whole family with `npx --yes sshlg-skills@latest update` (every channel,\n' +
198
+ 'and it prunes plain copies that would shadow a plugin).'
199
+ );
114
200
  return 0;
115
201
  }
116
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssheleg/sheleg-dev",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "description": "Seven integration skills for the layer a product reaches once it has users: Stripe subscription billing and cancellation retention, crypto payments, ad and conversion tracking under Consent Mode v2, Google sign-in and the wider Google auth surface, error tracking, and Core Web Vitals. This package is the installer CLI.",
5
5
  "bin": {
6
6
  "sheleg-dev": "bin/sheleg-dev.js"
@@ -43,8 +43,8 @@
43
43
  "skill"
44
44
  ],
45
45
  "scripts": {
46
- "test": "python3 test/validate.py && node test/moneygate_test.js && node test/fixtures_test.js",
46
+ "test": "python3 test/validate.py && node test/moneygate_test.js && node test/fixtures_test.js && node test/installer_test.js",
47
47
  "test:negatives": "python3 test/negatives.py",
48
- "test:all": "python3 test/validate.py && node test/moneygate_test.js && node test/fixtures_test.js && python3 test/negatives.py"
48
+ "test:all": "python3 test/validate.py && node test/moneygate_test.js && node test/fixtures_test.js && node test/installer_test.js && python3 test/negatives.py"
49
49
  }
50
50
  }
@@ -3,7 +3,7 @@
3
3
  "name": "sheleg-dev",
4
4
  "displayName": "Sheleg Dev",
5
5
  "description": "The integration layer a product needs once it has users: Stripe subscription billing reconciled into your own database, crypto payments that survive over-payment and duplicate webhooks, ad and conversion tracking under Consent Mode v2, Google sign-in with the account-linking guards, the wider Google auth surface, and Core Web Vitals work that moves the score, and Sentry error tracking wired so it does not forward your own credentials to a third party and so a stack trace names the commit that caused it.",
6
- "version": "0.10.4",
6
+ "version": "0.10.5",
7
7
  "author": {
8
8
  "name": "ssheleg",
9
9
  "url": "https://x.com/sshlg93"