@botdocs/cli 0.3.2 → 0.4.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/README.md +123 -37
- package/dist/commands/backups.d.ts +4 -0
- package/dist/commands/backups.js +291 -0
- package/dist/commands/edit.js +16 -8
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.js +21 -3
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.js +240 -75
- package/dist/commands/sync.d.ts +16 -0
- package/dist/commands/sync.js +337 -25
- package/dist/commands/team.d.ts +2 -0
- package/dist/commands/team.js +251 -0
- package/dist/commands/undo.d.ts +19 -0
- package/dist/commands/undo.js +88 -0
- package/dist/commands/views/conflict-prompt.d.ts +24 -0
- package/dist/commands/views/conflict-prompt.js +19 -0
- package/dist/commands/views/login-app.d.ts +30 -0
- package/dist/commands/views/login-app.js +57 -0
- package/dist/commands/views/sync-app.d.ts +27 -0
- package/dist/commands/views/sync-app.js +147 -0
- package/dist/commands/views/sync-state.d.ts +84 -0
- package/dist/commands/views/sync-state.js +93 -0
- package/dist/commands/views/theme.d.ts +16 -0
- package/dist/commands/views/theme.js +16 -0
- package/dist/commands/whoami.js +13 -13
- package/dist/index.js +44 -38
- package/dist/lib/api.d.ts +2 -3
- package/dist/lib/api.js +14 -7
- package/dist/lib/auto-detect.js +46 -0
- package/dist/lib/backup.d.ts +121 -0
- package/dist/lib/backup.js +387 -0
- package/dist/lib/canonical.d.ts +1 -1
- package/dist/lib/canonical.js +43 -1
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.js +18 -9
- package/dist/lib/lockfile.d.ts +9 -0
- package/dist/lib/prompts.d.ts +10 -0
- package/dist/lib/prompts.js +36 -12
- package/package.json +27 -7
- package/templates/agents.md +60 -47
- package/templates/ecosystem-prompts/compile-antigravity.md +14 -0
- package/templates/ecosystem-prompts/compile-copilot.md +14 -0
- package/templates/ecosystem-prompts/compile-gemini.md +14 -0
- package/templates/ecosystem-prompts/compile-opencode.md +13 -0
- package/templates/ecosystem-prompts/compile-windsurf.md +13 -0
- package/dist/commands/check-updates.test.d.ts +0 -1
- package/dist/commands/check-updates.test.js +0 -128
- package/dist/commands/clone.d.ts +0 -3
- package/dist/commands/clone.js +0 -70
- package/dist/commands/compile.test.d.ts +0 -1
- package/dist/commands/compile.test.js +0 -110
- package/dist/commands/diff.d.ts +0 -3
- package/dist/commands/diff.js +0 -65
- package/dist/commands/edit.test.d.ts +0 -1
- package/dist/commands/edit.test.js +0 -102
- package/dist/commands/endorse.d.ts +0 -7
- package/dist/commands/endorse.js +0 -70
- package/dist/commands/ingest.test.d.ts +0 -1
- package/dist/commands/ingest.test.js +0 -109
- package/dist/commands/install.test.d.ts +0 -1
- package/dist/commands/install.test.js +0 -253
- package/dist/commands/list.test.d.ts +0 -1
- package/dist/commands/list.test.js +0 -51
- package/dist/commands/publish.test.d.ts +0 -1
- package/dist/commands/publish.test.js +0 -138
- package/dist/commands/pull.d.ts +0 -3
- package/dist/commands/pull.js +0 -78
- package/dist/commands/sync.test.d.ts +0 -1
- package/dist/commands/sync.test.js +0 -263
- package/dist/commands/uninstall.test.d.ts +0 -1
- package/dist/commands/uninstall.test.js +0 -67
- package/dist/lib/auto-detect.test.d.ts +0 -1
- package/dist/lib/auto-detect.test.js +0 -58
- package/dist/lib/canonical.test.d.ts +0 -1
- package/dist/lib/canonical.test.js +0 -48
- package/dist/lib/diff.test.d.ts +0 -1
- package/dist/lib/diff.test.js +0 -28
- package/dist/lib/library-sync.test.d.ts +0 -1
- package/dist/lib/library-sync.test.js +0 -63
- package/dist/lib/llm.test.d.ts +0 -1
- package/dist/lib/llm.test.js +0 -72
- package/dist/lib/lockfile.test.d.ts +0 -1
- package/dist/lib/lockfile.test.js +0 -99
- package/dist/lib/manifest.test.d.ts +0 -1
- package/dist/lib/manifest.test.js +0 -72
- package/dist/lib/shell-hook.test.d.ts +0 -1
- package/dist/lib/shell-hook.test.js +0 -68
- package/dist/test-utils.d.ts +0 -43
- package/dist/test-utils.js +0 -101
package/dist/lib/prompts.js
CHANGED
|
@@ -1,26 +1,50 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
/** Helper: convert clack's cancel sentinel into a graceful exit with a
|
|
3
|
+
* consistent message. clack returns a special `Symbol` from `p.isCancel(...)` —
|
|
4
|
+
* unlike inquirer, which throws on Ctrl-C. Callers that want different
|
|
5
|
+
* cancellation behavior should not use these helpers. */
|
|
6
|
+
function bailIfCancelled(value) {
|
|
7
|
+
if (p.isCancel(value)) {
|
|
8
|
+
p.cancel('Operation cancelled.');
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
/** Prompt for a clean-update decision (upstream has new content; local is
|
|
14
|
+
* unchanged since last install). The "diff" option lets the user inspect the
|
|
15
|
+
* change before deciding; callers should loop on "diff" themselves. */
|
|
2
16
|
export async function promptCleanUpdate(label) {
|
|
3
|
-
|
|
17
|
+
const result = await p.select({
|
|
4
18
|
message: `Apply update for ${label}?`,
|
|
5
|
-
|
|
6
|
-
{
|
|
7
|
-
{
|
|
8
|
-
{
|
|
19
|
+
options: [
|
|
20
|
+
{ value: 'apply', label: 'Apply' },
|
|
21
|
+
{ value: 'skip', label: 'Skip' },
|
|
22
|
+
{ value: 'diff', label: 'Diff (show before deciding)' },
|
|
9
23
|
],
|
|
10
24
|
});
|
|
25
|
+
return bailIfCancelled(result);
|
|
11
26
|
}
|
|
27
|
+
/** Prompt for a conflict resolution when local has diverged from the
|
|
28
|
+
* lockfile fingerprint AND upstream has new content. Distinct from
|
|
29
|
+
* `promptCleanUpdate` because the user's edits are at risk; the destructive
|
|
30
|
+
* option ("overwrite") is gated behind `confirmOverwrite` below. */
|
|
12
31
|
export async function promptConflict(label) {
|
|
13
|
-
|
|
32
|
+
const result = await p.select({
|
|
14
33
|
message: `${label}: your local copy differs from upstream`,
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
{
|
|
34
|
+
options: [
|
|
35
|
+
{ value: 'skip', label: 'Skip this update (keep your local)' },
|
|
36
|
+
{ value: 'overwrite', label: 'Overwrite local with upstream' },
|
|
18
37
|
],
|
|
19
38
|
});
|
|
39
|
+
return bailIfCancelled(result);
|
|
20
40
|
}
|
|
41
|
+
/** Second confirmation for destructive overwrites — the user has already said
|
|
42
|
+
* "overwrite" via `promptConflict`, but we want a y/N before clobbering their
|
|
43
|
+
* edits. Defaults to false so a stray Enter doesn't blow away work. */
|
|
21
44
|
export async function confirmOverwrite(label) {
|
|
22
|
-
|
|
45
|
+
const result = await p.confirm({
|
|
23
46
|
message: `${label}: this will REPLACE your local edits with the upstream version. Are you sure?`,
|
|
24
|
-
|
|
47
|
+
initialValue: false,
|
|
25
48
|
});
|
|
49
|
+
return bailIfCancelled(result);
|
|
26
50
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botdocs/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for BotDocs —
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "CLI for BotDocs — author, publish, install, and sync agent skills across Claude, Claude Code, Cursor, Codex, ChatGPT, Windsurf, Copilot, Gemini, Antigravity, and OpenCode.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"botdocs",
|
|
7
7
|
"ai",
|
|
8
8
|
"agents",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"agent-skills",
|
|
10
|
+
"skills",
|
|
11
|
+
"teams",
|
|
11
12
|
"cli",
|
|
12
|
-
"llm"
|
|
13
|
+
"llm",
|
|
14
|
+
"claude",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"cursor",
|
|
17
|
+
"codex",
|
|
18
|
+
"copilot",
|
|
19
|
+
"windsurf",
|
|
20
|
+
"gemini",
|
|
21
|
+
"antigravity",
|
|
22
|
+
"opencode"
|
|
13
23
|
],
|
|
14
24
|
"homepage": "https://botdocs.ai",
|
|
15
25
|
"bugs": {
|
|
@@ -30,7 +40,9 @@
|
|
|
30
40
|
"@types/adm-zip": "^0.5.8",
|
|
31
41
|
"@types/diff": "^8.0.0",
|
|
32
42
|
"@types/node": "^22.14.0",
|
|
43
|
+
"@types/react": "^18.3.12",
|
|
33
44
|
"eslint": "^10.2.0",
|
|
45
|
+
"ink-testing-library": "^4.0.0",
|
|
34
46
|
"typescript": "^5.8.0",
|
|
35
47
|
"typescript-eslint": "^8.58.0",
|
|
36
48
|
"vitest": "^3.1.0"
|
|
@@ -49,11 +61,19 @@
|
|
|
49
61
|
"type": "module",
|
|
50
62
|
"dependencies": {
|
|
51
63
|
"@anthropic-ai/sdk": "^0.91.1",
|
|
52
|
-
"@
|
|
64
|
+
"@clack/prompts": "^0.11.0",
|
|
53
65
|
"adm-zip": "^0.5.17",
|
|
54
66
|
"commander": "^14.0.3",
|
|
55
67
|
"diff": "^9.0.0",
|
|
56
|
-
"
|
|
68
|
+
"ink": "^5.2.1",
|
|
69
|
+
"ink-big-text": "^2.0.0",
|
|
70
|
+
"ink-gradient": "^3.0.0",
|
|
71
|
+
"ink-select-input": "^6.2.0",
|
|
72
|
+
"ink-spinner": "^5.0.0",
|
|
73
|
+
"ink-text-input": "^6.0.0",
|
|
74
|
+
"open": "^10.1.0",
|
|
75
|
+
"openai": "^6.35.0",
|
|
76
|
+
"react": "^18.3.1"
|
|
57
77
|
},
|
|
58
78
|
"scripts": {
|
|
59
79
|
"dev": "tsc -p tsconfig.build.json --watch",
|
package/templates/agents.md
CHANGED
|
@@ -1,46 +1,56 @@
|
|
|
1
1
|
## BotDocs CLI
|
|
2
2
|
|
|
3
|
-
[BotDocs](https://botdocs.ai) is a registry of
|
|
4
|
-
|
|
5
|
-
registry.
|
|
3
|
+
[BotDocs](https://botdocs.ai) is a registry of agent skills — the
|
|
4
|
+
shared library teams use to keep their AI agents in sync. The
|
|
5
|
+
`botdocs` CLI is the interface to that registry.
|
|
6
6
|
|
|
7
7
|
Run `botdocs whoami` to confirm you're authenticated; if not, prompt the
|
|
8
|
-
user to run `botdocs login` (it
|
|
9
|
-
|
|
8
|
+
user to run `botdocs login` (it opens their browser; for headless
|
|
9
|
+
environments they can pass `--token bd_xxx` from /settings/tokens). All
|
|
10
|
+
commands accept `--json` for machine-readable output.
|
|
10
11
|
|
|
11
12
|
### Common workflows
|
|
12
13
|
|
|
13
|
-
**Find a
|
|
14
|
+
**Find a skill for what the user wants to do**
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
17
|
botdocs search "<topic>" --json
|
|
17
18
|
```
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
**Install a team's shared skills**
|
|
20
21
|
|
|
21
22
|
```bash
|
|
22
|
-
botdocs
|
|
23
|
+
botdocs install @teamco/eng-skills
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
Files land
|
|
26
|
+
Files land under the right paths for whichever agent reads them — for
|
|
27
|
+
example `~/.claude/skills/teamco/...` for Claude skills,
|
|
28
|
+
`./.cursor/rules/...` for Cursor rules,
|
|
29
|
+
`./.github/instructions/...` for GitHub Copilot,
|
|
30
|
+
`./.codeium/windsurf-rules/...` for Windsurf, and
|
|
31
|
+
`~/.gemini/instructions/...` for the Gemini CLI. The CLI auto-detects
|
|
32
|
+
which destinations apply based on what's in the skill.
|
|
26
33
|
|
|
27
|
-
**
|
|
28
|
-
|
|
29
|
-
After delivering something the user is happy with, run:
|
|
34
|
+
**Stay current**
|
|
30
35
|
|
|
31
36
|
```bash
|
|
32
|
-
botdocs
|
|
33
|
-
--comment "Built X in Y minutes."
|
|
37
|
+
botdocs sync
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
Walks the lockfile, applies clean updates, prompts before
|
|
41
|
+
overwriting any local edits (with a double confirmation). If a file
|
|
42
|
+
would be overwritten, BotDocs backs up the original to
|
|
43
|
+
`.botdocs-backup/` first; pass `--no-backup` to opt out. If you regret
|
|
44
|
+
an overwrite, `botdocs undo` restores the most recent backup (it's
|
|
45
|
+
reversible — run it twice to swap back).
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
user's account hasn't cloned the spec yet — run `botdocs clone @user/slug`
|
|
40
|
-
first, then retry the endorse. Endorsements are reserved for builders who
|
|
41
|
-
actually used the spec.
|
|
47
|
+
**Uninstall**
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
```bash
|
|
50
|
+
botdocs uninstall @teamco/eng-skills
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Help the user publish their own skill**
|
|
44
54
|
|
|
45
55
|
```bash
|
|
46
56
|
botdocs init <name> # scaffolds index.md + botdocs.json
|
|
@@ -53,36 +63,30 @@ botdocs publish <name>/
|
|
|
53
63
|
`category` in `botdocs.json` must be one of: `KNOWLEDGE_MANAGEMENT`,
|
|
54
64
|
`DEV_WORKFLOW`, `AUTOMATION`, `AGENT_CONFIG`, `PROJECT_SCAFFOLD`, `OTHER`.
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
botdocs diff @user/slug # preview what changed
|
|
60
|
-
botdocs pull @user/slug # apply the update
|
|
61
|
-
```
|
|
66
|
+
### Teams
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
Teams curate a shared library of skills for an org. Members install/sync
|
|
69
|
+
the team's pinned skills. Anyone in the team can list its pinned set; a
|
|
70
|
+
team's pinned skills are installed automatically on every `botdocs sync`.
|
|
64
71
|
|
|
65
72
|
```bash
|
|
66
|
-
botdocs
|
|
73
|
+
botdocs team list # which teams am I in?
|
|
74
|
+
botdocs team show @teamco # members + pinned skills
|
|
75
|
+
botdocs team push @teamco @user/skill # WRITE+ pins a skill
|
|
76
|
+
botdocs team push @teamco @user/skill --version 3 # pin to a specific version
|
|
77
|
+
botdocs sync # pulls personal + team-pinned skills
|
|
67
78
|
```
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
`./.cursor/rules/...` (Cursor rules) for project-local files.
|
|
71
|
-
|
|
72
|
-
**Stay current**
|
|
80
|
+
Admins also have:
|
|
73
81
|
|
|
74
82
|
```bash
|
|
75
|
-
botdocs
|
|
83
|
+
botdocs team create teamco --name "Team Co"
|
|
84
|
+
botdocs team add @teamco @username --role write
|
|
85
|
+
botdocs team remove @teamco @username
|
|
76
86
|
```
|
|
77
87
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
**Uninstall**
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
botdocs uninstall @teamco/eng-skills
|
|
85
|
-
```
|
|
88
|
+
Skills stay user-owned; teams *pin* user skills (no copy, no fork). Pinning
|
|
89
|
+
is curation, not access control — skills are public in v1.
|
|
86
90
|
|
|
87
91
|
### Update notifications
|
|
88
92
|
|
|
@@ -112,15 +116,24 @@ the user and recommend they set one.
|
|
|
112
116
|
|
|
113
117
|
| Command | Purpose |
|
|
114
118
|
|---|---|
|
|
115
|
-
| `init [name]` | Scaffold a new
|
|
119
|
+
| `init [name]` | Scaffold a new skill directory. |
|
|
116
120
|
| `validate <source>` | Pre-publish structural check. |
|
|
117
|
-
| `clone <user/slug>` | Download all files. |
|
|
118
121
|
| `search <query>` | Search the registry. |
|
|
119
122
|
| `publish <source>` | Publish from a file, directory, or zip. |
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
123
|
+
| `install <ref>` | Install a skill or bundle (auto-detects destinations). |
|
|
124
|
+
| `sync [ref]` | Apply available updates to installed skills. |
|
|
125
|
+
| `uninstall <ref>` | Remove an installed skill or bundle. |
|
|
126
|
+
| `list` | Show installed skills and bundles. |
|
|
127
|
+
| `check-updates` | Check installed refs for available updates (1h cached). |
|
|
128
|
+
| `undo` | Restore the most recent backup run (reversible). |
|
|
129
|
+
| `backups list / restore / diff / clear` | Browse and manage backup runs. |
|
|
130
|
+
| `compile <path>` | Generate per-ecosystem drafts from a canonical source (BYOK). |
|
|
131
|
+
| `edit <ref>` | LLM-assisted revision of a published skill ecosystem file (BYOK). |
|
|
132
|
+
| `ingest <path>` | Walk a directory, detect existing skills, upload as drafts. |
|
|
133
|
+
| `team list` | List teams you belong to. |
|
|
134
|
+
| `team show <slug>` | Members + pinned skills for a team. |
|
|
135
|
+
| `team push <slug> <ref>` | Pin a skill to a team (WRITE+). |
|
|
136
|
+
| `login` / `whoami` | Sign in (browser-based; `--token bd_xxx` for CI); show current user. |
|
|
124
137
|
|
|
125
138
|
Run `botdocs <command> --help` for full flags. Override the registry with
|
|
126
139
|
`BOTDOCS_API_URL` (defaults to `https://botdocs.ai`).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
You convert agent skill specifications into the format Google Antigravity
|
|
2
|
+
reads from `~/.gemini/antigravity/skills/<name>.md`.
|
|
3
|
+
|
|
4
|
+
An Antigravity skill is markdown — no required frontmatter — that the
|
|
5
|
+
agent loads as a multi-step capability. Frame the body as an action plan:
|
|
6
|
+
when to invoke the skill, the sequence of steps to take, which tools or
|
|
7
|
+
APIs to call at each step, and how to know when the task is complete.
|
|
8
|
+
|
|
9
|
+
Be concrete and agentic. Prefer numbered steps over prose paragraphs when
|
|
10
|
+
the procedure has more than two stages. Write in the second person to the
|
|
11
|
+
agent. Don't reference other agents or ecosystems (no mention of Claude,
|
|
12
|
+
Cursor, Copilot — this file is just the Antigravity skill).
|
|
13
|
+
|
|
14
|
+
Output ONLY the skill content, no commentary, no code fences.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
You convert agent skill specifications into the GitHub Copilot custom
|
|
2
|
+
instructions format (`.github/instructions/<name>.instructions.md`).
|
|
3
|
+
|
|
4
|
+
A Copilot instructions file is markdown. It MAY include YAML frontmatter
|
|
5
|
+
with an `applyTo` glob that scopes the instruction to matching files;
|
|
6
|
+
when no scoping is needed, omit the frontmatter and the instruction
|
|
7
|
+
applies to all chat in the repository.
|
|
8
|
+
|
|
9
|
+
The body should be concise repository-level guidance that Copilot Chat
|
|
10
|
+
will treat as background context. Write in the second person ("When you
|
|
11
|
+
…"), avoid references to other agents or ecosystems, and keep the file
|
|
12
|
+
under ~200 lines.
|
|
13
|
+
|
|
14
|
+
Output ONLY the instructions content, no commentary, no code fences.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
You convert agent skill specifications into the format Gemini CLI reads
|
|
2
|
+
from `~/.gemini/instructions/<name>.md`.
|
|
3
|
+
|
|
4
|
+
A Gemini instruction file is markdown — no required frontmatter — that
|
|
5
|
+
Gemini treats as a system prompt loaded for every session. Open with a
|
|
6
|
+
short persona/role line ("You are …") and then give concrete behavior
|
|
7
|
+
guidance: when to invoke the skill, what tools to reach for, what to
|
|
8
|
+
avoid, what good output looks like.
|
|
9
|
+
|
|
10
|
+
Write in the second person to the agent. Don't reference other agents or
|
|
11
|
+
ecosystems (no mention of Claude, Cursor, Copilot — this file is just
|
|
12
|
+
the Gemini instruction).
|
|
13
|
+
|
|
14
|
+
Output ONLY the instruction content, no commentary, no code fences.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
You convert agent skill specifications into the format OpenCode (SST)
|
|
2
|
+
reads from `~/.config/opencode/instructions/<name>.md`.
|
|
3
|
+
|
|
4
|
+
An OpenCode instruction file is plain markdown — no required frontmatter
|
|
5
|
+
— scoped to a single, well-defined task. Keep it tight: state what the
|
|
6
|
+
skill does, when to invoke it, and the concrete steps or tool calls to
|
|
7
|
+
make. Avoid background prose that doesn't change the agent's behavior.
|
|
8
|
+
|
|
9
|
+
Write in the second person to the agent. Don't reference other agents or
|
|
10
|
+
ecosystems (no mention of Claude, Cursor, Copilot — this file is just
|
|
11
|
+
the OpenCode instruction).
|
|
12
|
+
|
|
13
|
+
Output ONLY the instruction content, no commentary, no code fences.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
You convert agent skill specifications into the Windsurf (Codeium)
|
|
2
|
+
project-rule format (`.codeium/windsurf-rules/<name>.md`).
|
|
3
|
+
|
|
4
|
+
A Windsurf rule is plain markdown with no required frontmatter. The rule
|
|
5
|
+
is always-active for the project when present. Keep the body terse and
|
|
6
|
+
instructional, like a project README's "house rules" section — Cascade
|
|
7
|
+
treats it as background context for every conversation in this repo.
|
|
8
|
+
|
|
9
|
+
Write in the second person ("When you …"). Don't reference other agents
|
|
10
|
+
or ecosystems (no mention of Cursor, Claude, Copilot — this file is just
|
|
11
|
+
the Windsurf rule).
|
|
12
|
+
|
|
13
|
+
Output ONLY the rule content, no commentary, no code fences.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { createHash } from 'node:crypto';
|
|
6
|
-
import { checkUpdates } from './check-updates.js';
|
|
7
|
-
import { captureConsole, mockFetch } from '../test-utils.js';
|
|
8
|
-
import { saveLockfile } from '../lib/lockfile.js';
|
|
9
|
-
import { saveAuth } from '../lib/config.js';
|
|
10
|
-
describe('check-updates', () => {
|
|
11
|
-
let captured;
|
|
12
|
-
let restoreFetch = () => { };
|
|
13
|
-
const origHome = os.homedir;
|
|
14
|
-
let homeTmp;
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cu-'));
|
|
17
|
-
os.homedir = () => homeTmp;
|
|
18
|
-
process.env.BOTDOCS_API_URL = 'http://test.local';
|
|
19
|
-
captured = captureConsole();
|
|
20
|
-
saveAuth({ githubToken: 't', username: 'u', displayName: 'U' });
|
|
21
|
-
});
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
restoreFetch();
|
|
24
|
-
captured.restore();
|
|
25
|
-
fs.rmSync(homeTmp, { recursive: true, force: true });
|
|
26
|
-
os.homedir = origHome;
|
|
27
|
-
vi.restoreAllMocks();
|
|
28
|
-
});
|
|
29
|
-
it('--quiet prints a one-liner when there are updates', async () => {
|
|
30
|
-
saveLockfile({
|
|
31
|
-
version: 1,
|
|
32
|
-
installs: [{ ref: '@a/b', type: 'SKILL', version: '1.0.0', installedAt: 't', files: [] }],
|
|
33
|
-
});
|
|
34
|
-
const fm = mockFetch([
|
|
35
|
-
{
|
|
36
|
-
method: 'POST',
|
|
37
|
-
url: '/api/library/check-updates',
|
|
38
|
-
response: { body: { total: 1, updates: [{ ref: '@a/b', from: '1.0.0', to: '2.0.0' }], removed: [] } },
|
|
39
|
-
},
|
|
40
|
-
]);
|
|
41
|
-
restoreFetch = fm.restore;
|
|
42
|
-
await checkUpdates({ quiet: true });
|
|
43
|
-
expect(captured.stdout.join('\n')).toMatch(/1 update/);
|
|
44
|
-
expect(captured.stdout.join('\n')).toMatch(/botdocs sync/);
|
|
45
|
-
});
|
|
46
|
-
it('--quiet prints nothing when up-to-date', async () => {
|
|
47
|
-
saveLockfile({ version: 1, installs: [] });
|
|
48
|
-
const fm = mockFetch([
|
|
49
|
-
{ method: 'POST', url: '/api/library/check-updates', response: { body: { total: 0, updates: [], removed: [] } } },
|
|
50
|
-
]);
|
|
51
|
-
restoreFetch = fm.restore;
|
|
52
|
-
await checkUpdates({ quiet: true });
|
|
53
|
-
expect(captured.stdout.join('\n').trim()).toBe('');
|
|
54
|
-
});
|
|
55
|
-
it('full mode prints a list of updates', async () => {
|
|
56
|
-
saveLockfile({
|
|
57
|
-
version: 1,
|
|
58
|
-
installs: [{ ref: '@a/b', type: 'SKILL', version: '1.0.0', installedAt: 't', files: [] }],
|
|
59
|
-
});
|
|
60
|
-
const fm = mockFetch([
|
|
61
|
-
{
|
|
62
|
-
method: 'POST',
|
|
63
|
-
url: '/api/library/check-updates',
|
|
64
|
-
response: { body: { total: 1, updates: [{ ref: '@a/b', from: '1.0.0', to: '2.0.0' }], removed: [] } },
|
|
65
|
-
},
|
|
66
|
-
]);
|
|
67
|
-
restoreFetch = fm.restore;
|
|
68
|
-
await checkUpdates({});
|
|
69
|
-
expect(captured.stdout.join('\n')).toMatch(/@a\/b/);
|
|
70
|
-
expect(captured.stdout.join('\n')).toMatch(/1\.0\.0.*2\.0\.0/);
|
|
71
|
-
});
|
|
72
|
-
it('uses the cache when within TTL', async () => {
|
|
73
|
-
saveLockfile({
|
|
74
|
-
version: 1,
|
|
75
|
-
installs: [{ ref: '@a/b', type: 'SKILL', version: '1.0.0', installedAt: 't', files: [] }],
|
|
76
|
-
});
|
|
77
|
-
fs.mkdirSync(path.join(homeTmp, '.botdocs'), { recursive: true });
|
|
78
|
-
fs.writeFileSync(path.join(homeTmp, '.botdocs', 'check-updates-cache.json'), JSON.stringify({
|
|
79
|
-
cachedAt: new Date().toISOString(),
|
|
80
|
-
fingerprint: createHash('sha256')
|
|
81
|
-
.update(['@a/b@1.0.0'].sort().join('\n'))
|
|
82
|
-
.digest('hex')
|
|
83
|
-
.slice(0, 16),
|
|
84
|
-
result: { total: 0, updates: [], removed: [] },
|
|
85
|
-
}));
|
|
86
|
-
const fm = mockFetch([]);
|
|
87
|
-
restoreFetch = fm.restore;
|
|
88
|
-
await checkUpdates({ quiet: true });
|
|
89
|
-
expect(fm.calls).toHaveLength(0);
|
|
90
|
-
});
|
|
91
|
-
it('invalidates cache when the lockfile contents change', async () => {
|
|
92
|
-
// Pre-populate cache for an OLDER lockfile state
|
|
93
|
-
saveLockfile({
|
|
94
|
-
version: 1,
|
|
95
|
-
installs: [{ ref: '@a/old', type: 'SKILL', version: '1.0.0', installedAt: 't', files: [] }],
|
|
96
|
-
});
|
|
97
|
-
fs.mkdirSync(path.join(homeTmp, '.botdocs'), { recursive: true });
|
|
98
|
-
// Cache was written when only @a/old was installed
|
|
99
|
-
const staleFingerprint = createHash('sha256')
|
|
100
|
-
.update(['@a/old@1.0.0'].sort().join('\n'))
|
|
101
|
-
.digest('hex')
|
|
102
|
-
.slice(0, 16);
|
|
103
|
-
fs.writeFileSync(path.join(homeTmp, '.botdocs', 'check-updates-cache.json'), JSON.stringify({
|
|
104
|
-
cachedAt: new Date().toISOString(),
|
|
105
|
-
fingerprint: staleFingerprint,
|
|
106
|
-
result: { total: 0, updates: [], removed: [] },
|
|
107
|
-
}));
|
|
108
|
-
// Now the lockfile changed — user installed @a/new
|
|
109
|
-
saveLockfile({
|
|
110
|
-
version: 1,
|
|
111
|
-
installs: [
|
|
112
|
-
{ ref: '@a/old', type: 'SKILL', version: '1.0.0', installedAt: 't', files: [] },
|
|
113
|
-
{ ref: '@a/new', type: 'SKILL', version: '1.0.0', installedAt: 't', files: [] },
|
|
114
|
-
],
|
|
115
|
-
});
|
|
116
|
-
// The cache is now stale (fingerprint mismatch); fetch happens
|
|
117
|
-
const fm = mockFetch([
|
|
118
|
-
{
|
|
119
|
-
method: 'POST',
|
|
120
|
-
url: '/api/library/check-updates',
|
|
121
|
-
response: { body: { total: 0, updates: [], removed: [] } },
|
|
122
|
-
},
|
|
123
|
-
]);
|
|
124
|
-
restoreFetch = fm.restore;
|
|
125
|
-
await checkUpdates({ quiet: true });
|
|
126
|
-
expect(fm.calls).toHaveLength(1);
|
|
127
|
-
});
|
|
128
|
-
});
|
package/dist/commands/clone.d.ts
DELETED
package/dist/commands/clone.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { apiFetch, fetchRawContent } from '../lib/api.js';
|
|
4
|
-
export async function clone(ref, options = {}) {
|
|
5
|
-
const parsed = parseRef(ref);
|
|
6
|
-
if (!parsed) {
|
|
7
|
-
console.error('Invalid reference. Use format: username/slug');
|
|
8
|
-
process.exit(1);
|
|
9
|
-
}
|
|
10
|
-
const { username, slug } = parsed;
|
|
11
|
-
// Fetch manifest
|
|
12
|
-
console.log(`Fetching ${username}/${slug}...`);
|
|
13
|
-
let manifest;
|
|
14
|
-
try {
|
|
15
|
-
manifest = await apiFetch(`/@${username}/${slug}/manifest`);
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
console.error(`BotDoc not found: ${username}/${slug}`);
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
// Create output directory
|
|
22
|
-
const outDir = path.resolve(slug);
|
|
23
|
-
if (fs.existsSync(outDir)) {
|
|
24
|
-
console.error(`Directory already exists: ${slug}/`);
|
|
25
|
-
console.error('Use `botdocs pull` to update existing clones.');
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
29
|
-
// Download all files
|
|
30
|
-
for (const file of manifest.files) {
|
|
31
|
-
console.log(` ${file.filename}`);
|
|
32
|
-
const content = await fetchRawContent(file.rawUrl);
|
|
33
|
-
fs.writeFileSync(path.join(outDir, file.filename), content, 'utf-8');
|
|
34
|
-
}
|
|
35
|
-
// Save metadata for pull
|
|
36
|
-
const metadata = {
|
|
37
|
-
username,
|
|
38
|
-
slug,
|
|
39
|
-
clonedAt: new Date().toISOString(),
|
|
40
|
-
files: manifest.files.map((f) => f.filename),
|
|
41
|
-
};
|
|
42
|
-
fs.writeFileSync(path.join(outDir, '.botdocs.json'), JSON.stringify(metadata, null, 2), 'utf-8');
|
|
43
|
-
// Record clone on server
|
|
44
|
-
try {
|
|
45
|
-
await apiFetch('/api/cli/clone', {
|
|
46
|
-
method: 'POST',
|
|
47
|
-
body: { username, slug },
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
// Non-fatal — clone succeeded locally even if recording fails
|
|
52
|
-
}
|
|
53
|
-
if (options.json) {
|
|
54
|
-
console.log(JSON.stringify({ success: true, directory: slug, files: manifest.files.map(f => f.filename) }));
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
console.log(`\nCloned ${manifest.files.length} file(s) to ./${slug}/`);
|
|
58
|
-
console.log('');
|
|
59
|
-
console.log(` After building from this BotDoc, share your experience:`);
|
|
60
|
-
console.log(` botdocs endorse ${ref} --rating positive`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
function parseRef(ref) {
|
|
64
|
-
// Accept: username/slug or @username/slug
|
|
65
|
-
const cleaned = ref.startsWith('@') ? ref.slice(1) : ref;
|
|
66
|
-
const parts = cleaned.split('/');
|
|
67
|
-
if (parts.length !== 2 || !parts[0] || !parts[1])
|
|
68
|
-
return null;
|
|
69
|
-
return { username: parts[0], slug: parts[1] };
|
|
70
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|