@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.
Files changed (89) hide show
  1. package/README.md +123 -37
  2. package/dist/commands/backups.d.ts +4 -0
  3. package/dist/commands/backups.js +291 -0
  4. package/dist/commands/edit.js +16 -8
  5. package/dist/commands/install.d.ts +4 -0
  6. package/dist/commands/install.js +21 -3
  7. package/dist/commands/login.d.ts +7 -0
  8. package/dist/commands/login.js +240 -75
  9. package/dist/commands/sync.d.ts +16 -0
  10. package/dist/commands/sync.js +337 -25
  11. package/dist/commands/team.d.ts +2 -0
  12. package/dist/commands/team.js +251 -0
  13. package/dist/commands/undo.d.ts +19 -0
  14. package/dist/commands/undo.js +88 -0
  15. package/dist/commands/views/conflict-prompt.d.ts +24 -0
  16. package/dist/commands/views/conflict-prompt.js +19 -0
  17. package/dist/commands/views/login-app.d.ts +30 -0
  18. package/dist/commands/views/login-app.js +57 -0
  19. package/dist/commands/views/sync-app.d.ts +27 -0
  20. package/dist/commands/views/sync-app.js +147 -0
  21. package/dist/commands/views/sync-state.d.ts +84 -0
  22. package/dist/commands/views/sync-state.js +93 -0
  23. package/dist/commands/views/theme.d.ts +16 -0
  24. package/dist/commands/views/theme.js +16 -0
  25. package/dist/commands/whoami.js +13 -13
  26. package/dist/index.js +44 -38
  27. package/dist/lib/api.d.ts +2 -3
  28. package/dist/lib/api.js +14 -7
  29. package/dist/lib/auto-detect.js +46 -0
  30. package/dist/lib/backup.d.ts +121 -0
  31. package/dist/lib/backup.js +387 -0
  32. package/dist/lib/canonical.d.ts +1 -1
  33. package/dist/lib/canonical.js +43 -1
  34. package/dist/lib/config.d.ts +8 -1
  35. package/dist/lib/config.js +18 -9
  36. package/dist/lib/lockfile.d.ts +9 -0
  37. package/dist/lib/prompts.d.ts +10 -0
  38. package/dist/lib/prompts.js +36 -12
  39. package/package.json +27 -7
  40. package/templates/agents.md +60 -47
  41. package/templates/ecosystem-prompts/compile-antigravity.md +14 -0
  42. package/templates/ecosystem-prompts/compile-copilot.md +14 -0
  43. package/templates/ecosystem-prompts/compile-gemini.md +14 -0
  44. package/templates/ecosystem-prompts/compile-opencode.md +13 -0
  45. package/templates/ecosystem-prompts/compile-windsurf.md +13 -0
  46. package/dist/commands/check-updates.test.d.ts +0 -1
  47. package/dist/commands/check-updates.test.js +0 -128
  48. package/dist/commands/clone.d.ts +0 -3
  49. package/dist/commands/clone.js +0 -70
  50. package/dist/commands/compile.test.d.ts +0 -1
  51. package/dist/commands/compile.test.js +0 -110
  52. package/dist/commands/diff.d.ts +0 -3
  53. package/dist/commands/diff.js +0 -65
  54. package/dist/commands/edit.test.d.ts +0 -1
  55. package/dist/commands/edit.test.js +0 -102
  56. package/dist/commands/endorse.d.ts +0 -7
  57. package/dist/commands/endorse.js +0 -70
  58. package/dist/commands/ingest.test.d.ts +0 -1
  59. package/dist/commands/ingest.test.js +0 -109
  60. package/dist/commands/install.test.d.ts +0 -1
  61. package/dist/commands/install.test.js +0 -253
  62. package/dist/commands/list.test.d.ts +0 -1
  63. package/dist/commands/list.test.js +0 -51
  64. package/dist/commands/publish.test.d.ts +0 -1
  65. package/dist/commands/publish.test.js +0 -138
  66. package/dist/commands/pull.d.ts +0 -3
  67. package/dist/commands/pull.js +0 -78
  68. package/dist/commands/sync.test.d.ts +0 -1
  69. package/dist/commands/sync.test.js +0 -263
  70. package/dist/commands/uninstall.test.d.ts +0 -1
  71. package/dist/commands/uninstall.test.js +0 -67
  72. package/dist/lib/auto-detect.test.d.ts +0 -1
  73. package/dist/lib/auto-detect.test.js +0 -58
  74. package/dist/lib/canonical.test.d.ts +0 -1
  75. package/dist/lib/canonical.test.js +0 -48
  76. package/dist/lib/diff.test.d.ts +0 -1
  77. package/dist/lib/diff.test.js +0 -28
  78. package/dist/lib/library-sync.test.d.ts +0 -1
  79. package/dist/lib/library-sync.test.js +0 -63
  80. package/dist/lib/llm.test.d.ts +0 -1
  81. package/dist/lib/llm.test.js +0 -72
  82. package/dist/lib/lockfile.test.d.ts +0 -1
  83. package/dist/lib/lockfile.test.js +0 -99
  84. package/dist/lib/manifest.test.d.ts +0 -1
  85. package/dist/lib/manifest.test.js +0 -72
  86. package/dist/lib/shell-hook.test.d.ts +0 -1
  87. package/dist/lib/shell-hook.test.js +0 -68
  88. package/dist/test-utils.d.ts +0 -43
  89. package/dist/test-utils.js +0 -101
@@ -1,26 +1,50 @@
1
- import { confirm, select } from '@inquirer/prompts';
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
- return select({
17
+ const result = await p.select({
4
18
  message: `Apply update for ${label}?`,
5
- choices: [
6
- { name: 'apply', value: 'apply' },
7
- { name: 'skip', value: 'skip' },
8
- { name: 'diff (show before deciding)', value: 'diff' },
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
- return select({
32
+ const result = await p.select({
14
33
  message: `${label}: your local copy differs from upstream`,
15
- choices: [
16
- { name: 'skip this update (keep your local)', value: 'skip' },
17
- { name: 'overwrite local with upstream', value: 'overwrite' },
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
- return confirm({
45
+ const result = await p.confirm({
23
46
  message: `${label}: this will REPLACE your local edits with the upstream version. Are you sure?`,
24
- default: false,
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.3.2",
4
- "description": "CLI for BotDocs — clone, search, publish, and endorse concept specifications.",
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
- "specs",
10
- "specification",
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
- "@inquirer/prompts": "^8.4.2",
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
- "openai": "^6.35.0"
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",
@@ -1,46 +1,56 @@
1
1
  ## BotDocs CLI
2
2
 
3
- [BotDocs](https://botdocs.ai) is a registry of concept specificationsdocs
4
- an AI agent can build from. The `botdocs` CLI is the interface to that
5
- registry.
3
+ [BotDocs](https://botdocs.ai) is a registry of agent skillsthe
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's a GitHub device-code flow). All commands
9
- accept `--json` for machine-readable output.
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 spec for what the user wants to build**
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
- Pick the most relevant result and clone it:
20
+ **Install a team's shared skills**
20
21
 
21
22
  ```bash
22
- botdocs clone @user/slug
23
+ botdocs install @teamco/eng-skills
23
24
  ```
24
25
 
25
- Files land in `./slug/`. Read `index.md` first that's always the entry point.
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
- **Build something on top of a cloned spec, then report back**
28
-
29
- After delivering something the user is happy with, run:
34
+ **Stay current**
30
35
 
31
36
  ```bash
32
- botdocs endorse @user/slug --rating positive \
33
- --comment "Built X in Y minutes."
37
+ botdocs sync
34
38
  ```
35
39
 
36
- Ratings: `positive` | `neutral` | `negative`.
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
- If the server returns a 403 with "clone before endorsing", that means the
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
- **Help the user publish their own spec**
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
- **Update a previously cloned spec**
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
- **Install a team's shared skills**
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 install @teamco/eng-skills
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
- Files land in `~/.claude/skills/teamco/...` (Claude skills) and
70
- `./.cursor/rules/...` (Cursor rules) for project-local files.
71
-
72
- **Stay current**
80
+ Admins also have:
73
81
 
74
82
  ```bash
75
- botdocs sync
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
- Walks the lockfile, applies clean updates, prompts before
79
- overwriting any local edits (with a double confirmation).
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 BotDoc directory. |
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
- | `diff <user/slug>` | Preview remote changes before pulling. |
121
- | `pull <user/slug>` | Apply remote updates. |
122
- | `endorse <user/slug>` | Rate a spec after building from it. |
123
- | `login` / `whoami` | Auth via GitHub device code; show current user. |
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
- });
@@ -1,3 +0,0 @@
1
- export declare function clone(ref: string, options?: {
2
- json?: boolean;
3
- }): Promise<void>;
@@ -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 {};