progressive-explainer-cli 0.0.0-stage → 0.1.0
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.snippet.md +7 -0
- package/LICENSE +21 -0
- package/README.md +95 -3
- package/bin/progressive-explainer.js +9 -0
- package/package.json +32 -6
- package/skills/progressive-explainer/SKILL.md +108 -0
- package/skills/progressive-explainer/references/example.md +72 -0
- package/src/cli.js +50 -0
- package/src/doctor.js +57 -0
- package/src/init.js +72 -0
- package/src/paths.js +16 -0
- package/src/skills.js +67 -0
- package/src/update.js +33 -0
- package/src/writers.js +28 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## progressive-explainer
|
|
2
|
+
|
|
3
|
+
When the user asks you to explain a topic, asks how something works, or asks for a document they will read to understand something, invoke the `progressive-explainer` skill and follow it: short first document, one running analogy, progress checklist at the end.
|
|
4
|
+
|
|
5
|
+
When the user replies with a number from a progress checklist, invoke `progressive-explainer` again and cover that subtopic.
|
|
6
|
+
|
|
7
|
+
Code changes, commits, pull requests and quick factual answers do not use this skill.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Grégory Babonaux
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,95 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# 🧭 progressive-explainer
|
|
2
|
+
|
|
3
|
+
**English** · [Français](README.fr.md) · [Español](README.es.md) · [Deutsch](README.de.md) · [Português](README.pt-BR.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [한국어](README.ko.md)
|
|
4
|
+
|
|
5
|
+
🌐 [Website](https://gregorybabonaux.github.io/progressive-explainer/)
|
|
6
|
+
|
|
7
|
+
**Explanations you can actually follow: a short overview first, then you pick what to dig into.**
|
|
8
|
+
|
|
9
|
+
progressive-explainer is a skill for AI agents (Claude, Claude Code, Codex, Cursor and others). It changes how the agent explains things, not what it knows.
|
|
10
|
+
|
|
11
|
+
## The problem
|
|
12
|
+
|
|
13
|
+
Ask an AI agent how something works and you often get a wall of text: correct, complete, and impossible to follow if you have trouble with attention or focus. You lose the thread halfway, and the answer is wasted.
|
|
14
|
+
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
The agent answers in two stages.
|
|
18
|
+
|
|
19
|
+
**1. A short first document (250 to 400 words)**, always in the same order:
|
|
20
|
+
|
|
21
|
+
| Part | What it gives you |
|
|
22
|
+
|---|---|
|
|
23
|
+
| One-sentence answer | The point, in under 20 words, before anything else |
|
|
24
|
+
| The frame | What this is about, why it matters, who decides |
|
|
25
|
+
| How it works | 2 to 4 short sections, with one analogy running through all of them |
|
|
26
|
+
| A diagram, if useful | 5 elements at most, every arrow labelled |
|
|
27
|
+
| One-sentence recap | Something you could repeat to someone else |
|
|
28
|
+
|
|
29
|
+
**2. A progress checklist** that replaces the usual "want me to go deeper?":
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
**Go further**
|
|
33
|
+
|
|
34
|
+
🟢 easy · 🟡 intermediate · 🔴 advanced
|
|
35
|
+
|
|
36
|
+
- [x] **1. Overview**: one thread for JS, slow work handed off, blocking 🟢
|
|
37
|
+
- [ ] **2. What actually blocks**: which functions and patterns freeze the server? 🟢
|
|
38
|
+
- [ ] **3. The `await` trap**: why can an `async` function still block everything? 🟢
|
|
39
|
+
- [ ] **4. Execution order**: who goes first between Promise, `nextTick` and `setTimeout`? 🟡
|
|
40
|
+
- [ ] **5. Worker threads**: how do you move a heavy computation off the main thread? 🟡
|
|
41
|
+
- [ ] **6. Configuring the thread pool**: where and how do you add cooks? 🟡
|
|
42
|
+
|
|
43
|
+
Reply with a number (or several).
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Reply with a number: the agent covers that subtopic with the same rules and the same analogy, then ticks the box. You see your progress.
|
|
47
|
+
|
|
48
|
+
A complete example: [`skills/progressive-explainer/references/example.md`](skills/progressive-explainer/references/example.md).
|
|
49
|
+
|
|
50
|
+
## The main rules
|
|
51
|
+
|
|
52
|
+
- **The conclusion comes first**, in every section, in bold. Reading only the headings and the bold text gives you the essentials.
|
|
53
|
+
- **One analogy for the whole topic.** No juggling between three metaphors.
|
|
54
|
+
- **No setting without its location.** If a setting is mentioned, the agent says which file or tool it lives in, and how to check it works.
|
|
55
|
+
- **One number per point.** Names and sources go at the end, not in the middle of a sentence.
|
|
56
|
+
- **Casual tone in the body, neutral headings.** No hook, no suspense, no filler.
|
|
57
|
+
- **Your language.** The agent answers in the language you write in.
|
|
58
|
+
|
|
59
|
+
The full rules are in [`SKILL.md`](skills/progressive-explainer/SKILL.md). Edit it to tune them.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
**Step 1: install the skill (pick one)**
|
|
64
|
+
|
|
65
|
+
| Where | Command | What you get |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| **Claude Code** (recommended) | `/plugin marketplace add GregoryBabonaux/progressive-explainer`, then `/plugin install progressive-explainer@progressive-explainer` | The skill and an `/explain <topic>` command |
|
|
68
|
+
| **Any agent** (Codex, Cursor, Gemini CLI, Copilot, OpenCode…) | `npx skills add GregoryBabonaux/progressive-explainer` | The skill, in the project or globally with `-g` |
|
|
69
|
+
| **No skills.sh** | `npx progressive-explainer-cli skills --yes` | The skill copied to `~/.agents/skills` and `~/.claude/skills` |
|
|
70
|
+
| **Claude app** (claude.ai, desktop) | Download the [`skills/progressive-explainer`](skills/progressive-explainer) folder as a zip, then upload it in the Skills section of Claude's settings | The skill, available in every chat |
|
|
71
|
+
|
|
72
|
+
**Step 2 (optional): use it for every explanation in a project**
|
|
73
|
+
|
|
74
|
+
By default you call the skill when you want it (`/explain`, or "explain this progressively"). To make the agent use it whenever you ask for an explanation:
|
|
75
|
+
|
|
76
|
+
- `npx progressive-explainer-cli init` shows what it will write. Nothing is written until you confirm.
|
|
77
|
+
- `npx progressive-explainer-cli init --yes` adds the rule to `AGENTS.md` (and `CLAUDE.md` or `GEMINI.md` if your project uses them), between markers, without touching the rest of the file.
|
|
78
|
+
|
|
79
|
+
**Step 3: check**
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
npx progressive-explainer-cli doctor
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
To update later: `npx progressive-explainer-cli update`, `claude plugin update progressive-explainer@progressive-explainer` or `npx skills update`, depending on how you installed it.
|
|
86
|
+
|
|
87
|
+
## Credits
|
|
88
|
+
|
|
89
|
+
Built from tests on real explanations, then compared with existing skills:
|
|
90
|
+
- [patrick204nqh/skills](https://github.com/patrick204nqh/skills): the one-sentence answer, the single analogy and the recap come from `eli5`, the diagram rules from `diagram-it`.
|
|
91
|
+
- [adhd-friendly-documentation](https://skillsmp.com/skills/rghamilton3-dotfiles-dot-claude-skills-adhd-friendly-documentation-skill-md): short paragraphs and the check before sending.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT. Made by Grégory Babonaux.
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "progressive-explainer-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "progressive-explainer-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Installs progressive-explainer: the skill that turns AI explanations into a short overview plus a checklist to go deeper.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"progressive-explainer": "bin/progressive-explainer.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"skills",
|
|
13
|
+
"AGENTS.snippet.md",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test \"test/*.test.js\"",
|
|
22
|
+
"prepack": "node ../scripts/bundle.mjs"
|
|
23
|
+
},
|
|
24
|
+
"keywords": ["skills", "agent-skills", "claude-code", "codex", "cursor", "explain", "adhd", "accessibility"],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/GregoryBabonaux/progressive-explainer.git",
|
|
29
|
+
"directory": "cli"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/GregoryBabonaux/progressive-explainer#readme"
|
|
32
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: progressive-explainer
|
|
3
|
+
description: Explains a topic, or writes a document someone has to understand, for a reader who loses focus in long or dense text. Short overview first, one running analogy, then a progress checklist the reader picks from to go deeper. Use when the user invokes /explain or progressive-explainer, asks for an explanation they can actually follow, or picks a number from a progress checklist this skill produced.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Progressive explainer
|
|
10
|
+
|
|
11
|
+
The reader has trouble with attention and focus. A long or dense document makes them lose the thread, even when every sentence is correct. The goal: they understand the topic on the first read, then choose for themselves what to dig into.
|
|
12
|
+
|
|
13
|
+
Principle: a short first document gives the big picture. Details come afterwards, one piece at a time, on request, through a progress checklist.
|
|
14
|
+
|
|
15
|
+
Write in the language the user writes in. Address the reader informally (for example "tu" in French, "du" in German) unless the context clearly calls for formality.
|
|
16
|
+
|
|
17
|
+
See `references/example.md` for a complete first document.
|
|
18
|
+
|
|
19
|
+
## Before writing
|
|
20
|
+
|
|
21
|
+
1. **Pick the analogy.** One only, from everyday life, that covers the whole topic. If none fits cleanly, use one concrete example instead.
|
|
22
|
+
2. **List the subtopics** that will go into the final checklist, and sort them by difficulty.
|
|
23
|
+
|
|
24
|
+
## Structure of the first document
|
|
25
|
+
|
|
26
|
+
In this order:
|
|
27
|
+
|
|
28
|
+
1. **The answer in one sentence.** Under 20 words, no jargon, in bold.
|
|
29
|
+
2. **The frame.** Three short points: what this is about, why it matters, who decides or who uses it.
|
|
30
|
+
3. **How it works**, in 2 to 4 sections. The analogy runs from start to finish.
|
|
31
|
+
4. **A diagram**, only if a relationship is the heart of the topic (see the diagram rules below).
|
|
32
|
+
5. **The one-sentence recap**, something the reader could repeat to someone else.
|
|
33
|
+
6. **The progress checklist.**
|
|
34
|
+
7. **Sources**, at the very end.
|
|
35
|
+
|
|
36
|
+
Target length: 250 to 400 words, excluding code and the checklist. If the topic needs more, put the extra into the checklist instead of writing it.
|
|
37
|
+
|
|
38
|
+
## Progress checklist
|
|
39
|
+
|
|
40
|
+
It always ends the document.
|
|
41
|
+
|
|
42
|
+
```markdown
|
|
43
|
+
**Go further**
|
|
44
|
+
|
|
45
|
+
🟢 easy · 🟡 intermediate · 🔴 advanced
|
|
46
|
+
|
|
47
|
+
- [x] **1. Overview**: <what was just covered> 🟢
|
|
48
|
+
- [ ] **2. <Title>**: <the question this subtopic answers> 🟢
|
|
49
|
+
- [ ] **3. <Title>**: <question> 🟡
|
|
50
|
+
- [ ] **4. <Title>**: <question> 🔴
|
|
51
|
+
|
|
52
|
+
Reply with a number (or several).
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Rules:
|
|
56
|
+
- Sort from easiest to most advanced.
|
|
57
|
+
- One line per subtopic: a bold title, then the question it answers.
|
|
58
|
+
- Tick what has already been covered.
|
|
59
|
+
- Include a "configuration" subtopic (where and how it is set) and a "diagnosis" subtopic (how to check it works) whenever the topic is technical and operational.
|
|
60
|
+
- Never end with a yes/no question like "want me to dig into X?". The checklist replaces that question.
|
|
61
|
+
|
|
62
|
+
## When the reader picks a number
|
|
63
|
+
|
|
64
|
+
- Cover that subtopic with the same rules: one-sentence answer, conclusion first in each section, same analogy, same target length.
|
|
65
|
+
- If several numbers are picked, cover them in order, each under its own heading.
|
|
66
|
+
- End with the updated checklist, with the new boxes ticked. Add subtopics if the deeper dive reveals new ones.
|
|
67
|
+
|
|
68
|
+
## Content rules
|
|
69
|
+
|
|
70
|
+
- **One analogy, running through.** Do not switch analogies midway and do not stack several.
|
|
71
|
+
- **Never mention a setting without saying where it is set.** Name the file, tool or screen, and explain how to check it works (command, metric, tab).
|
|
72
|
+
- **One number per point at most**, the one that carries the message.
|
|
73
|
+
- **Names and sources go at the end**, never in the body. They are there to source, not to understand.
|
|
74
|
+
- **Short code examples** when something is configured, rather than a description.
|
|
75
|
+
- **Unavoidable jargon is defined** in a few words the first time it appears. Technical terms stay in English (event loop, rollback, heartbeat...), even when writing in another language.
|
|
76
|
+
|
|
77
|
+
## Diagram rules
|
|
78
|
+
|
|
79
|
+
A diagram only if a relationship (flow, sequence, hierarchy, state transitions) is the heart of the explanation. Otherwise a sentence or a list is enough.
|
|
80
|
+
|
|
81
|
+
- 5 elements at most, one idea per diagram.
|
|
82
|
+
- Every arrow has a label.
|
|
83
|
+
- The heading above the diagram states the conclusion, not the topic.
|
|
84
|
+
- Mermaid by default, ASCII if rendering is not available.
|
|
85
|
+
- 2 to 4 lines of text after the diagram say what it does not show.
|
|
86
|
+
- No diagram for a comparison: use a table.
|
|
87
|
+
|
|
88
|
+
## Form rules
|
|
89
|
+
|
|
90
|
+
- **Informative, neutral headings.** "The 5 classic mistakes", not "The hall of horrors". The reader must be able to skim the document by its headings.
|
|
91
|
+
- **The conclusion is the first sentence of each section**, in bold, then the explanation.
|
|
92
|
+
- **Paragraphs of 2 or 3 lines**, one idea per sentence.
|
|
93
|
+
- **Bullets start with a bold phrase.**
|
|
94
|
+
- **Casual tone in the body**: informal address, spoken turns of phrase, a touch of humor when it helps. Headings stay neutral.
|
|
95
|
+
- No hook, no suspense, no empty transition.
|
|
96
|
+
- No condescension, no filler, no repeated "this is a simplification".
|
|
97
|
+
- No em dash (—): use a comma, a period or a colon instead.
|
|
98
|
+
|
|
99
|
+
## Check before sending
|
|
100
|
+
|
|
101
|
+
- [ ] The first line answers the question in under 20 words.
|
|
102
|
+
- [ ] The frame says what this is about, why it matters, who decides.
|
|
103
|
+
- [ ] One analogy, present from start to finish.
|
|
104
|
+
- [ ] Reading only the headings and the bold text gives the essentials.
|
|
105
|
+
- [ ] Every setting mentioned says where it is set and how to check it.
|
|
106
|
+
- [ ] No names in the body.
|
|
107
|
+
- [ ] The document ends with the recap, the checklist, then the sources.
|
|
108
|
+
- [ ] Under 400 words, excluding code and the checklist.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Example: first document for "Node.js single thread"
|
|
2
|
+
|
|
3
|
+
This is what a first document looks like when every rule is applied. Use it as a reference for shape and length, not as a template to copy word for word.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**Node runs your JavaScript on a single thread, and hands all the slow work to someone else.**
|
|
8
|
+
|
|
9
|
+
## What this is about
|
|
10
|
+
|
|
11
|
+
- **What:** the Node.js execution model. One single thread runs your code. A **thread** is a line of instructions the processor runs one after the other.
|
|
12
|
+
- **Why it matters:** one greedy function is enough to freeze your whole server, along with every request in progress.
|
|
13
|
+
- **Who decides:** Node manages the thread and the loop that feeds it, the **event loop**. You decide what you give it to run.
|
|
14
|
+
|
|
15
|
+
## Your code goes through a single waiter
|
|
16
|
+
|
|
17
|
+
**Only one line of your JS runs at any given moment.**
|
|
18
|
+
|
|
19
|
+
Picture a restaurant with a single waiter. They take an order, pass it to the kitchen and move on to the next table without waiting for the dish. One waiter still serves the whole room, as long as they don't get stuck at one table.
|
|
20
|
+
|
|
21
|
+
## Slow work goes to the kitchen
|
|
22
|
+
|
|
23
|
+
**Network, disk and crypto do not go through your thread.**
|
|
24
|
+
|
|
25
|
+
- **The network** (HTTP, database) is handled by the operating system.
|
|
26
|
+
- **Files, DNS and crypto** go to the **thread pool**, a kitchen crew of 4 threads by default.
|
|
27
|
+
|
|
28
|
+
So "single thread" only applies to your JS. Node itself uses several threads behind the scenes.
|
|
29
|
+
|
|
30
|
+
**A finished dish waits until the waiter's hands are free.**
|
|
31
|
+
|
|
32
|
+
```mermaid
|
|
33
|
+
flowchart LR
|
|
34
|
+
R[Request] -->|arrives| T[JS thread]
|
|
35
|
+
T -->|hands off| C[Kitchen: OS and thread pool]
|
|
36
|
+
C -->|dish ready: callback queued| E[Event loop]
|
|
37
|
+
E -->|gives back to| T
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The diagram does not show the priority order between waiting callbacks. Some jump the queue, and that is a topic of its own.
|
|
41
|
+
|
|
42
|
+
## A long computation blocks the whole room
|
|
43
|
+
|
|
44
|
+
**While your thread computes, no other request moves.**
|
|
45
|
+
|
|
46
|
+
If the waiter starts peeling potatoes themselves, nobody gets served. In Node, that is a big `JSON.parse`, a loop over hundreds of thousands of items, or an `fs.readFileSync`.
|
|
47
|
+
|
|
48
|
+
## In one sentence
|
|
49
|
+
|
|
50
|
+
One waiter for your code, a kitchen for the slow work, and the whole room waits as soon as the waiter starts cooking.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
**Go further**
|
|
55
|
+
|
|
56
|
+
🟢 easy · 🟡 intermediate · 🔴 advanced
|
|
57
|
+
|
|
58
|
+
- [x] **1. Overview**: one thread for JS, slow work handed off, blocking 🟢
|
|
59
|
+
- [ ] **2. What actually blocks**: which functions and patterns freeze the server? 🟢
|
|
60
|
+
- [ ] **3. The `await` trap**: why can an `async` function still block everything? 🟢
|
|
61
|
+
- [ ] **4. Execution order**: who goes first between Promise, `nextTick` and `setTimeout`? 🟡
|
|
62
|
+
- [ ] **5. Worker threads**: how do you move a heavy computation off the main thread? 🟡
|
|
63
|
+
- [ ] **6. Configuring the thread pool**: where and how do you add cooks? 🟡
|
|
64
|
+
- [ ] **7. Diagnosing a block**: how do you measure event loop delay and find the culprit? 🟡
|
|
65
|
+
- [ ] **8. Several processes**: `cluster` and PM2 to use every core of the machine 🔴
|
|
66
|
+
- [ ] **9. The starved loop**: how can microtasks stop I/O from getting through? 🔴
|
|
67
|
+
|
|
68
|
+
Reply with a number (or several).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
Sources: official Node.js documentation (event loop, libuv thread pool).
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { runSkills } from './skills.js';
|
|
3
|
+
import { runInit } from './init.js';
|
|
4
|
+
import { runDoctor } from './doctor.js';
|
|
5
|
+
import { runUpdate } from './update.js';
|
|
6
|
+
|
|
7
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
8
|
+
export const VERSION = pkg.version;
|
|
9
|
+
|
|
10
|
+
export function parseOptions(args) {
|
|
11
|
+
const o = { yes: false, dryRun: false, quiet: false, target: null };
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const a = args[i];
|
|
14
|
+
if (a === '--yes' || a === '-y') o.yes = true;
|
|
15
|
+
else if (a === '--dry-run') o.dryRun = true;
|
|
16
|
+
else if (a === '--quiet') o.quiet = true;
|
|
17
|
+
else if (a === '--target') o.target = args[++i] ?? null;
|
|
18
|
+
}
|
|
19
|
+
return o;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const HELP = `progressive-explainer ${VERSION}, explanations you can follow (short overview, one analogy, a checklist to go deeper)
|
|
23
|
+
|
|
24
|
+
Usage: progressive-explainer <command> [options]
|
|
25
|
+
|
|
26
|
+
skills install the skill into ~/.agents/skills and ~/.claude/skills [--target <dir>] [--yes]
|
|
27
|
+
init add the explainer rule to AGENTS.md (and CLAUDE.md / GEMINI.md if the project uses them) [--yes]
|
|
28
|
+
doctor check the installation [--quiet]
|
|
29
|
+
update refresh skill copies and explainer rules installed by this CLI
|
|
30
|
+
|
|
31
|
+
Nothing is written without --yes: without it, commands show what they would change.
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
export async function run(argv, io) {
|
|
35
|
+
const [cmd, ...rest] = argv;
|
|
36
|
+
const opts = parseOptions(rest);
|
|
37
|
+
if (cmd === '--version' || cmd === '-v') { io.stdout.write(`${VERSION}\n`); return 0; }
|
|
38
|
+
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { io.stdout.write(HELP); return 0; }
|
|
39
|
+
try {
|
|
40
|
+
if (cmd === 'skills') return runSkills(io, opts, VERSION);
|
|
41
|
+
if (cmd === 'init') return runInit(io, opts, VERSION);
|
|
42
|
+
if (cmd === 'doctor') return runDoctor(io, opts, VERSION);
|
|
43
|
+
if (cmd === 'update') return runUpdate(io, opts, VERSION);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
io.stderr.write(`progressive-explainer: ${err.message}\n`);
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
io.stderr.write(`progressive-explainer: unknown command "${cmd}". Commands: skills, init, doctor, update\n`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { defaultTargets, STAMP } from './skills.js';
|
|
4
|
+
import { gitRoot, TARGETS } from './init.js';
|
|
5
|
+
import { hasBlock, blockVersion } from './writers.js';
|
|
6
|
+
|
|
7
|
+
export function diagnose({ root, env, version }) {
|
|
8
|
+
const out = [];
|
|
9
|
+
const ok = (message) => out.push({ level: 'ok', message });
|
|
10
|
+
const warn = (message) => out.push({ level: 'warn', message });
|
|
11
|
+
ok(`progressive-explainer-cli ${version}`);
|
|
12
|
+
|
|
13
|
+
let skillFound = false;
|
|
14
|
+
for (const target of defaultTargets(env)) {
|
|
15
|
+
const dir = path.join(target, 'progressive-explainer');
|
|
16
|
+
if (!fs.existsSync(path.join(dir, 'SKILL.md'))) continue;
|
|
17
|
+
skillFound = true;
|
|
18
|
+
const stampFile = path.join(dir, STAMP);
|
|
19
|
+
if (!fs.existsSync(stampFile)) ok(`skill in ${target} (managed by another channel: plugin or skills.sh)`);
|
|
20
|
+
else {
|
|
21
|
+
const v = fs.readFileSync(stampFile, 'utf8').trim();
|
|
22
|
+
if (v === version) ok(`skill ${v} in ${target}`);
|
|
23
|
+
else warn(`skill ${v} in ${target}, CLI is ${version}: run progressive-explainer update`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!skillFound) warn('no progressive-explainer skill in ~/.agents/skills or ~/.claude/skills: fine if installed in the project or by the Claude Code plugin, otherwise run progressive-explainer skills --yes');
|
|
27
|
+
|
|
28
|
+
if (!root) { warn('not inside a git repository: project checks skipped'); return out; }
|
|
29
|
+
for (const projectSkills of ['.agents/skills', '.claude/skills', '.cursor/skills', '.github/skills']) {
|
|
30
|
+
if (fs.existsSync(path.join(root, projectSkills, 'progressive-explainer', 'SKILL.md'))) ok(`skill in the project (${projectSkills})`);
|
|
31
|
+
}
|
|
32
|
+
let ruleFound = false;
|
|
33
|
+
for (const t of TARGETS) {
|
|
34
|
+
const file = path.join(root, t.file);
|
|
35
|
+
if (!fs.existsSync(file)) continue;
|
|
36
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
37
|
+
if (!hasBlock(text)) {
|
|
38
|
+
if (t.always) ok(`${t.file} has no explainer rule (optional: run progressive-explainer init to use the skill for every explanation)`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
ruleFound = true;
|
|
42
|
+
const v = blockVersion(text);
|
|
43
|
+
if (v === version) ok(`explainer rule ${v} in ${t.file}`);
|
|
44
|
+
else warn(`explainer rule ${v ?? '(no version)'} in ${t.file}, CLI is ${version}: run progressive-explainer update`);
|
|
45
|
+
}
|
|
46
|
+
if (!ruleFound && !fs.existsSync(path.join(root, 'AGENTS.md'))) ok('no explainer rule in AGENTS.md (optional: run progressive-explainer init to use the skill for every explanation)');
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function runDoctor(io, opts, version) {
|
|
51
|
+
const results = diagnose({ root: gitRoot(io.cwd), env: io.env ?? process.env, version });
|
|
52
|
+
for (const r of results) {
|
|
53
|
+
if (r.level === 'ok' && opts.quiet) continue;
|
|
54
|
+
io.stdout.write(`${r.level === 'ok' ? '✓' : '!'} ${r.message}\n`);
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
package/src/init.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { snippetSource } from './paths.js';
|
|
5
|
+
import { upsertBlock, toLf, matchEol } from './writers.js';
|
|
6
|
+
|
|
7
|
+
const readOrNull = (p) => (fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null);
|
|
8
|
+
|
|
9
|
+
export function gitRoot(cwd) {
|
|
10
|
+
try { return execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return null; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Instruction files the explainer rule goes into. AGENTS.md is read by Codex, Cursor, Copilot,
|
|
14
|
+
// OpenCode and most agents; CLAUDE.md and GEMINI.md only when the project already uses them.
|
|
15
|
+
export const TARGETS = [
|
|
16
|
+
{ file: 'AGENTS.md', always: true },
|
|
17
|
+
{ file: 'CLAUDE.md', detect: ['CLAUDE.md', '.claude'] },
|
|
18
|
+
{ file: 'GEMINI.md', detect: ['GEMINI.md', '.gemini'] },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function ruleBody(version) {
|
|
22
|
+
const src = snippetSource();
|
|
23
|
+
if (!src) throw new Error('no bundled AGENTS.snippet.md found');
|
|
24
|
+
return `<!-- progressive-explainer ${version} -->\n${toLf(fs.readFileSync(src, 'utf8')).trim()}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function selectTargets(root, only) {
|
|
28
|
+
return TARGETS.filter((t) => (only ? only.includes(t.file) : t.always || t.detect.some((p) => fs.existsSync(path.join(root, p)))));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// `only`: restrict to these files (used by update, which never adds new files).
|
|
32
|
+
export function planInit(root, { version, only = null }) {
|
|
33
|
+
const body = ruleBody(version);
|
|
34
|
+
const changes = [];
|
|
35
|
+
for (const t of selectTargets(root, only)) {
|
|
36
|
+
const before = readOrNull(path.join(root, t.file));
|
|
37
|
+
const after = upsertBlock(before === null ? '' : toLf(before), body);
|
|
38
|
+
if (before !== null && toLf(before) === after) continue;
|
|
39
|
+
changes.push({ path: t.file, before, after: matchEol(before, after) });
|
|
40
|
+
}
|
|
41
|
+
return changes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Backups live under .git/ so they are never committed by accident.
|
|
45
|
+
export function applyChanges(root, changes) {
|
|
46
|
+
const backupDir = path.join(root, '.git', 'progressive-explainer-backup');
|
|
47
|
+
for (const c of changes) {
|
|
48
|
+
if (c.before !== null) {
|
|
49
|
+
const backup = path.join(backupDir, c.path);
|
|
50
|
+
if (!fs.existsSync(backup)) {
|
|
51
|
+
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
52
|
+
fs.writeFileSync(backup, c.before);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
fs.writeFileSync(path.join(root, c.path), c.after);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function runInit(io, opts, version) {
|
|
60
|
+
const root = gitRoot(io.cwd);
|
|
61
|
+
if (!root) { io.stderr.write('progressive-explainer: init must run inside a git repository\n'); return 1; }
|
|
62
|
+
const changes = planInit(root, { version });
|
|
63
|
+
if (!changes.length) { io.stdout.write('progressive-explainer: explainer rule already in place\n'); return 0; }
|
|
64
|
+
for (const c of changes) io.stdout.write(`${c.before === null ? '+ create' : '~ update'} ${c.path} (explainer rule between progressive-explainer markers)\n`);
|
|
65
|
+
if (!opts.yes || opts.dryRun) { io.stdout.write('progressive-explainer: nothing written yet, re-run with --yes to apply\n'); return 0; }
|
|
66
|
+
applyChanges(root, changes);
|
|
67
|
+
io.stdout.write(`progressive-explainer: explainer rule installed in ${changes.map((c) => c.path).join(', ')}\n`);
|
|
68
|
+
if (fs.existsSync(path.join(root, '.claude')) || fs.existsSync(path.join(root, 'CLAUDE.md'))) {
|
|
69
|
+
io.stdout.write('progressive-explainer: on Claude Code, the plugin also adds the /explain command: /plugin marketplace add GregoryBabonaux/progressive-explainer\n');
|
|
70
|
+
}
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
// In the repository (cli/ next to skills/), the live files win over a stale bundled copy.
|
|
5
|
+
// In the npm package, prepack has bundled skills/ and AGENTS.snippet.md into cli/.
|
|
6
|
+
const inRepo = () => fs.existsSync(fileURLToPath(new URL('../../.claude-plugin/plugin.json', import.meta.url)));
|
|
7
|
+
|
|
8
|
+
function pick(repoRel, bundledRel) {
|
|
9
|
+
const repo = fileURLToPath(new URL(repoRel, import.meta.url));
|
|
10
|
+
const bundled = fileURLToPath(new URL(bundledRel, import.meta.url));
|
|
11
|
+
const candidates = inRepo() ? [repo, bundled] : [bundled, repo];
|
|
12
|
+
return candidates.find((p) => fs.existsSync(p)) ?? null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const skillsSource = () => pick('../../skills', '../skills');
|
|
16
|
+
export const snippetSource = () => pick('../../AGENTS.snippet.md', '../AGENTS.snippet.md');
|
package/src/skills.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { skillsSource } from './paths.js';
|
|
5
|
+
|
|
6
|
+
export const STAMP = '.progressive-explainer-version';
|
|
7
|
+
|
|
8
|
+
export function listSkills(dir) {
|
|
9
|
+
return fs.readdirSync(dir).filter((d) => fs.existsSync(path.join(dir, d, 'SKILL.md'))).sort();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ~/.agents/skills is read by Codex, Copilot CLI, Gemini CLI, Cursor and OpenCode; ~/.claude/skills by Claude Code.
|
|
13
|
+
export function defaultTargets(env) {
|
|
14
|
+
const home = env.PROGRESSIVE_EXPLAINER_HOME || os.homedir();
|
|
15
|
+
return [path.join(home, '.agents', 'skills'), path.join(home, '.claude', 'skills')];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// install: not there yet; update: installed by this CLI, older version; skip: managed by another channel.
|
|
19
|
+
export function planSkills({ source, targets, version }) {
|
|
20
|
+
const actions = [];
|
|
21
|
+
for (const target of targets) {
|
|
22
|
+
for (const skill of listSkills(source)) {
|
|
23
|
+
const dest = path.join(target, skill);
|
|
24
|
+
const stampFile = path.join(dest, STAMP);
|
|
25
|
+
if (!fs.existsSync(dest)) actions.push({ skill, target, dest, action: 'install' });
|
|
26
|
+
else if (!fs.existsSync(stampFile)) actions.push({ skill, target, dest, action: 'skip' });
|
|
27
|
+
else if (fs.readFileSync(stampFile, 'utf8').trim() !== version) actions.push({ skill, target, dest, action: 'update' });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return actions;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function applySkills(actions, source, version) {
|
|
34
|
+
for (const a of actions) {
|
|
35
|
+
if (a.action === 'skip') continue;
|
|
36
|
+
fs.rmSync(a.dest, { recursive: true, force: true });
|
|
37
|
+
fs.mkdirSync(path.dirname(a.dest), { recursive: true });
|
|
38
|
+
fs.cpSync(path.join(source, a.skill), a.dest, { recursive: true });
|
|
39
|
+
fs.writeFileSync(path.join(a.dest, STAMP), `${version}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function runSkills(io, opts, version) {
|
|
44
|
+
const source = skillsSource();
|
|
45
|
+
if (!source) { io.stderr.write('progressive-explainer: no bundled skills found\n'); return 1; }
|
|
46
|
+
const targets = opts.target ? [opts.target] : defaultTargets(io.env ?? process.env);
|
|
47
|
+
const actions = planSkills({ source, targets, version });
|
|
48
|
+
for (const a of actions.filter((x) => x.action === 'skip')) {
|
|
49
|
+
io.stderr.write(`progressive-explainer: ${a.skill} in ${a.target} is managed elsewhere (no ${STAMP}), left untouched\n`);
|
|
50
|
+
}
|
|
51
|
+
const work = actions.filter((a) => a.action !== 'skip');
|
|
52
|
+
if (!work.length) { io.stdout.write('progressive-explainer: skills already up to date\n'); return 0; }
|
|
53
|
+
for (const a of work) io.stdout.write(`${a.action === 'install' ? '+ install' : '~ update'} ${a.skill} → ${a.target}\n`);
|
|
54
|
+
if (!opts.yes || opts.dryRun) { io.stdout.write('progressive-explainer: re-run with --yes to apply\n'); return 0; }
|
|
55
|
+
applySkills(work, source, version);
|
|
56
|
+
io.stdout.write(`progressive-explainer: installed ${work.length} skill cop${work.length === 1 ? 'y' : 'ies'}\n`);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Used by `update`: refresh only copies this CLI installed (stamped), never install new ones.
|
|
61
|
+
export function refreshStampedSkills(env, version) {
|
|
62
|
+
const source = skillsSource();
|
|
63
|
+
if (!env || !source) return 0;
|
|
64
|
+
const updates = planSkills({ source, targets: defaultTargets(env), version }).filter((a) => a.action === 'update');
|
|
65
|
+
applySkills(updates, source, version);
|
|
66
|
+
return updates.length;
|
|
67
|
+
}
|
package/src/update.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { refreshStampedSkills } from './skills.js';
|
|
4
|
+
import { gitRoot, planInit, applyChanges, TARGETS } from './init.js';
|
|
5
|
+
import { hasBlock } from './writers.js';
|
|
6
|
+
|
|
7
|
+
const CHANNELS = [
|
|
8
|
+
'progressive-explainer: copies installed by another channel are updated there:\n',
|
|
9
|
+
' claude plugin update progressive-explainer@progressive-explainer (Claude Code plugin)\n',
|
|
10
|
+
' npx skills update (skills.sh)\n',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function runUpdate(io, opts, version) {
|
|
14
|
+
// skill copies first: they live in the home directory and need no repository
|
|
15
|
+
const refreshed = refreshStampedSkills(io.env ?? process.env, version);
|
|
16
|
+
if (refreshed) io.stdout.write(`progressive-explainer: refreshed ${refreshed} skill cop${refreshed === 1 ? 'y' : 'ies'}\n`);
|
|
17
|
+
|
|
18
|
+
const root = gitRoot(io.cwd);
|
|
19
|
+
// only refresh files that already carry the block: never install by surprise
|
|
20
|
+
const only = root ? TARGETS.map((t) => t.file).filter((f) => {
|
|
21
|
+
const p = path.join(root, f);
|
|
22
|
+
return fs.existsSync(p) && hasBlock(fs.readFileSync(p, 'utf8'));
|
|
23
|
+
}) : [];
|
|
24
|
+
if (!only.length) {
|
|
25
|
+
io.stdout.write('progressive-explainer: no explainer rule in this repository, run progressive-explainer init to add one\n');
|
|
26
|
+
} else {
|
|
27
|
+
const changes = planInit(root, { version, only });
|
|
28
|
+
applyChanges(root, changes);
|
|
29
|
+
io.stdout.write(`progressive-explainer: explainer rule ${changes.length ? `updated in ${changes.map((c) => c.path).join(', ')}` : 'already up to date'}\n`);
|
|
30
|
+
}
|
|
31
|
+
for (const line of CHANNELS) io.stdout.write(line);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
package/src/writers.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const toLf = (text) => text.replace(/\r\n/g, '\n');
|
|
2
|
+
|
|
3
|
+
// Write `after` with the line endings `before` already uses (a CRLF checkout stays CRLF).
|
|
4
|
+
export function matchEol(before, after) {
|
|
5
|
+
return before !== null && before.includes('\r\n') ? toLf(after).replace(/\n/g, '\r\n') : after;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const START = '<!-- progressive-explainer:start -->';
|
|
9
|
+
export const END = '<!-- progressive-explainer:end -->';
|
|
10
|
+
|
|
11
|
+
export function hasBlock(text) {
|
|
12
|
+
const i = text.indexOf(START);
|
|
13
|
+
return i !== -1 && text.indexOf(END, i + 1) !== -1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Replace the managed block if present, otherwise append it. Text outside the markers is never touched.
|
|
17
|
+
export function upsertBlock(text, body) {
|
|
18
|
+
const block = `${START}\n${body}\n${END}`;
|
|
19
|
+
const i = text.indexOf(START);
|
|
20
|
+
const j = i === -1 ? -1 : text.indexOf(END, i + 1);
|
|
21
|
+
if (i !== -1 && j !== -1) return text.slice(0, i) + block + text.slice(j + END.length);
|
|
22
|
+
if (!text) return `${block}\n`;
|
|
23
|
+
return `${text}${text.endsWith('\n') ? '' : '\n'}\n${block}\n`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function blockVersion(text) {
|
|
27
|
+
return text.match(/<!-- progressive-explainer (\d+\.\d+\.\d+) -->/)?.[1] ?? null;
|
|
28
|
+
}
|