@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,253 +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 { install } from './install.js';
6
- import { captureConsole, mockFetch, withTempDir } from '../test-utils.js';
7
- import { loadLockfile } from '../lib/lockfile.js';
8
- vi.mock('../lib/library-sync.js', () => ({
9
- syncLibrary: vi.fn(),
10
- }));
11
- import * as librarySync from '../lib/library-sync.js';
12
- describe('install', () => {
13
- let tmp;
14
- let captured;
15
- let restoreFetch = () => { };
16
- const origHome = os.homedir;
17
- let homeTmp;
18
- beforeEach(() => {
19
- tmp = withTempDir();
20
- homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'install-home-'));
21
- os.homedir = () => homeTmp;
22
- process.env.BOTDOCS_API_URL = 'http://test.local';
23
- captured = captureConsole();
24
- });
25
- afterEach(() => {
26
- restoreFetch();
27
- captured.restore();
28
- tmp.cleanup();
29
- fs.rmSync(homeTmp, { recursive: true, force: true });
30
- os.homedir = origHome;
31
- vi.restoreAllMocks();
32
- });
33
- it('installs a SKILL bundle to the right destinations', async () => {
34
- const fm = mockFetch([
35
- {
36
- url: '/api/skills/alice/code-review/manifest',
37
- response: {
38
- body: {
39
- ref: { username: 'alice', slug: 'code-review' },
40
- type: 'SKILL',
41
- version: '1.0.0',
42
- sourceEcosystem: 'claude-code',
43
- files: [
44
- { filename: 'claude/SKILL.md', rawUrl: 'http://test.local/raw/skill' },
45
- { filename: 'cursor/rules/code-review.mdc', rawUrl: 'http://test.local/raw/cursor' },
46
- ],
47
- },
48
- },
49
- },
50
- { url: '/raw/skill', response: { body: '# Claude SKILL\n\nbody', contentType: 'text/plain' } },
51
- { url: '/raw/cursor', response: { body: '# Cursor rule', contentType: 'text/plain' } },
52
- ]);
53
- restoreFetch = fm.restore;
54
- await install('@alice/code-review', { project: tmp.dir });
55
- const claudePath = path.join(homeTmp, '.claude', 'skills', 'alice', 'code-review', 'SKILL.md');
56
- const cursorPath = path.join(tmp.dir, '.cursor', 'rules', 'code-review.mdc');
57
- expect(fs.existsSync(claudePath)).toBe(true);
58
- expect(fs.existsSync(cursorPath)).toBe(true);
59
- const lf = loadLockfile();
60
- expect(lf.installs).toHaveLength(1);
61
- expect(lf.installs[0].ref).toBe('@alice/code-review');
62
- expect(lf.installs[0].files).toHaveLength(2);
63
- });
64
- it('installs a BUNDLE by recursively installing each skill', async () => {
65
- const fm = mockFetch([
66
- {
67
- url: '/api/skills/teamco/eng-skills/manifest',
68
- response: {
69
- body: {
70
- ref: { username: 'teamco', slug: 'eng-skills' },
71
- type: 'BUNDLE',
72
- version: '1.0.0',
73
- skills: [
74
- {
75
- ref: { username: 'teamco', slug: 'code-review' },
76
- version: '1.0.0',
77
- sourceEcosystem: 'claude-code',
78
- files: [{ filename: 'claude/SKILL.md', rawUrl: 'http://test.local/raw/cr' }],
79
- },
80
- {
81
- ref: { username: 'teamco', slug: 'pr-summary' },
82
- version: '1.0.0',
83
- sourceEcosystem: 'claude-code',
84
- files: [{ filename: 'claude/SKILL.md', rawUrl: 'http://test.local/raw/ps' }],
85
- },
86
- ],
87
- },
88
- },
89
- },
90
- { url: '/raw/cr', response: { body: '# CR', contentType: 'text/plain' } },
91
- { url: '/raw/ps', response: { body: '# PS', contentType: 'text/plain' } },
92
- ]);
93
- restoreFetch = fm.restore;
94
- await install('@teamco/eng-skills', { project: tmp.dir });
95
- expect(fs.existsSync(path.join(homeTmp, '.claude', 'skills', 'teamco', 'code-review', 'SKILL.md'))).toBe(true);
96
- expect(fs.existsSync(path.join(homeTmp, '.claude', 'skills', 'teamco', 'pr-summary', 'SKILL.md'))).toBe(true);
97
- const lf = loadLockfile();
98
- expect(lf.installs.map((i) => i.ref).sort()).toEqual([
99
- '@teamco/code-review',
100
- '@teamco/eng-skills',
101
- '@teamco/pr-summary',
102
- ]);
103
- });
104
- it('emits JSON on --json', async () => {
105
- const fm = mockFetch([
106
- {
107
- url: '/api/skills/alice/x/manifest',
108
- response: {
109
- body: {
110
- ref: { username: 'alice', slug: 'x' },
111
- type: 'SKILL',
112
- version: '1.0.0',
113
- sourceEcosystem: null,
114
- files: [],
115
- },
116
- },
117
- },
118
- ]);
119
- restoreFetch = fm.restore;
120
- await install('@alice/x', { project: tmp.dir, json: true });
121
- const out = JSON.parse(captured.stdout[0]);
122
- expect(out.ref).toBe('@alice/x');
123
- expect(out.installed).toBeDefined();
124
- });
125
- it('exits 1 on bad ref', async () => {
126
- await expect(install('not-a-ref', {})).rejects.toThrow();
127
- });
128
- it('--clean removes files from the prior install before re-installing', async () => {
129
- const dest = path.join(homeTmp, '.claude', 'skills', 'alice', 'x', 'OLD.md');
130
- fs.mkdirSync(path.dirname(dest), { recursive: true });
131
- fs.writeFileSync(dest, 'old content');
132
- const { saveLockfile } = await import('../lib/lockfile.js');
133
- saveLockfile({
134
- version: 1,
135
- installs: [
136
- {
137
- ref: '@alice/x',
138
- type: 'SKILL',
139
- version: '1.0.0',
140
- installedAt: 't',
141
- files: [{ src: 'claude/OLD.md', dest, fingerprint: 'old-fp' }],
142
- },
143
- ],
144
- });
145
- const fm = mockFetch([
146
- {
147
- url: '/api/skills/alice/x/manifest',
148
- response: {
149
- body: {
150
- ref: { username: 'alice', slug: 'x' },
151
- type: 'SKILL',
152
- version: '2.0.0',
153
- sourceEcosystem: null,
154
- files: [{ filename: 'claude/SKILL.md', rawUrl: 'http://test.local/raw/new' }],
155
- },
156
- },
157
- },
158
- { url: '/raw/new', response: { body: 'new content', contentType: 'text/plain' } },
159
- ]);
160
- restoreFetch = fm.restore;
161
- await install('@alice/x', { project: tmp.dir, clean: true });
162
- // Old file should be gone, new one should be present
163
- expect(fs.existsSync(dest)).toBe(false);
164
- expect(fs.existsSync(path.join(homeTmp, '.claude', 'skills', 'alice', 'x', 'SKILL.md'))).toBe(true);
165
- });
166
- it('--clean cascades file removal across a bundle\'s child skills', async () => {
167
- // Pre-populate a bundle and child skill in the lockfile + on disk
168
- const childDest = path.join(homeTmp, '.claude', 'skills', 'teamco', 'old-skill', 'SKILL.md');
169
- fs.mkdirSync(path.dirname(childDest), { recursive: true });
170
- fs.writeFileSync(childDest, 'old child content', 'utf-8');
171
- const { saveLockfile } = await import('../lib/lockfile.js');
172
- saveLockfile({
173
- version: 1,
174
- installs: [
175
- { ref: '@teamco/eng', type: 'BUNDLE', version: '1.0.0', installedAt: 't', files: [], skills: ['@teamco/old-skill'] },
176
- { ref: '@teamco/old-skill', type: 'SKILL', version: '1.0.0', installedAt: 't', files: [{ src: 'claude/SKILL.md', dest: childDest, fingerprint: 'old' }] },
177
- ],
178
- });
179
- // New manifest replaces old-skill with new-skill
180
- const fm = mockFetch([
181
- {
182
- url: '/api/skills/teamco/eng/manifest',
183
- response: {
184
- body: {
185
- ref: { username: 'teamco', slug: 'eng' },
186
- type: 'BUNDLE',
187
- version: '1.1.0',
188
- skills: [
189
- {
190
- ref: { username: 'teamco', slug: 'new-skill' },
191
- version: '1.0.0',
192
- sourceEcosystem: 'claude',
193
- files: [{ filename: 'claude/SKILL.md', rawUrl: 'http://test.local/raw/new' }],
194
- },
195
- ],
196
- },
197
- },
198
- },
199
- { url: '/raw/new', response: { body: 'new content', contentType: 'text/plain' } },
200
- ]);
201
- restoreFetch = fm.restore;
202
- await install('@teamco/eng', { project: tmp.dir, clean: true });
203
- // Old child file should be deleted by the cascade
204
- expect(fs.existsSync(childDest)).toBe(false);
205
- // New skill installed
206
- expect(fs.existsSync(path.join(homeTmp, '.claude', 'skills', 'teamco', 'new-skill', 'SKILL.md'))).toBe(true);
207
- });
208
- it('--json suppresses the manual-paste console.log for chatgpt files', async () => {
209
- const fm = mockFetch([
210
- {
211
- url: '/api/skills/alice/x/manifest',
212
- response: {
213
- body: {
214
- ref: { username: 'alice', slug: 'x' },
215
- type: 'SKILL',
216
- version: '1.0.0',
217
- sourceEcosystem: null,
218
- files: [{ filename: 'chatgpt/instructions.md', rawUrl: 'http://test.local/raw/cg' }],
219
- },
220
- },
221
- },
222
- // Note: under --json, the manual-paste branch should NOT call fetchRawContent at all
223
- // (it's wrapped in `if (!options.json)`), so no /raw/cg mock is needed.
224
- ]);
225
- restoreFetch = fm.restore;
226
- await install('@alice/x', { project: tmp.dir, json: true });
227
- // stdout has exactly one entry — the JSON output
228
- expect(captured.stdout).toHaveLength(1);
229
- const out = JSON.parse(captured.stdout[0]);
230
- expect(out.ref).toBe('@alice/x');
231
- // Verify no manual-paste content leaked into stdout
232
- expect(captured.stdout.join('\n')).not.toMatch(/Manual paste required/);
233
- });
234
- it('invokes syncLibrary after a successful install', async () => {
235
- const fm = mockFetch([
236
- {
237
- url: '/api/skills/alice/x/manifest',
238
- response: {
239
- body: {
240
- ref: { username: 'alice', slug: 'x' },
241
- type: 'SKILL',
242
- version: '1.0.0',
243
- sourceEcosystem: null,
244
- files: [],
245
- },
246
- },
247
- },
248
- ]);
249
- restoreFetch = fm.restore;
250
- await install('@alice/x', { project: tmp.dir });
251
- expect(librarySync.syncLibrary).toHaveBeenCalledTimes(1);
252
- });
253
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,51 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { list } from './list.js';
6
- import { captureConsole } from '../test-utils.js';
7
- import { saveLockfile } from '../lib/lockfile.js';
8
- describe('list', () => {
9
- let captured;
10
- const origHome = os.homedir;
11
- let homeTmp;
12
- beforeEach(() => {
13
- homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'list-home-'));
14
- os.homedir = () => homeTmp;
15
- captured = captureConsole();
16
- });
17
- afterEach(() => {
18
- captured.restore();
19
- fs.rmSync(homeTmp, { recursive: true, force: true });
20
- os.homedir = origHome;
21
- });
22
- it('says "nothing installed" when lockfile is empty', async () => {
23
- await list({});
24
- expect(captured.stdout.join('\n')).toMatch(/nothing installed/i);
25
- });
26
- it('groups skills by their parent bundle', async () => {
27
- saveLockfile({
28
- version: 1,
29
- installs: [
30
- { ref: '@a/eng', type: 'BUNDLE', version: '1.0.0', installedAt: 't', files: [], skills: ['@a/cr', '@a/ps'] },
31
- { ref: '@a/cr', type: 'SKILL', version: '1.0.0', installedAt: 't', files: [] },
32
- { ref: '@a/ps', type: 'SKILL', version: '1.0.0', installedAt: 't', files: [] },
33
- { ref: '@a/standalone', type: 'SKILL', version: '1.0.0', installedAt: 't', files: [] },
34
- ],
35
- });
36
- await list({});
37
- const out = captured.stdout.join('\n');
38
- expect(out).toContain('@a/eng');
39
- expect(out).toContain('@a/cr');
40
- expect(out).toContain('@a/standalone');
41
- });
42
- it('emits JSON on --json', async () => {
43
- saveLockfile({
44
- version: 1,
45
- installs: [{ ref: '@a/x', type: 'SKILL', version: '1.0.0', installedAt: 't', files: [] }],
46
- });
47
- await list({ json: true });
48
- const out = JSON.parse(captured.stdout[0]);
49
- expect(out.installs).toHaveLength(1);
50
- });
51
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,138 +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 { withTempDir, captureConsole, mockFetch } from '../test-utils.js';
6
- import { saveAuth } from '../lib/config.js';
7
- vi.mock('./compile.js', () => ({
8
- compile: vi.fn(),
9
- }));
10
- import * as compileMod from './compile.js';
11
- import { publish } from './publish.js';
12
- describe('publish auto-compile gate', () => {
13
- let tmp;
14
- let captured;
15
- let restoreFetch = () => { };
16
- const origHome = os.homedir;
17
- let homeTmp;
18
- beforeEach(() => {
19
- tmp = withTempDir();
20
- homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'publish-'));
21
- os.homedir = () => homeTmp;
22
- captured = captureConsole();
23
- process.env.BOTDOCS_API_URL = 'http://test.local';
24
- saveAuth({ githubToken: 't', username: 'u', displayName: 'U' });
25
- });
26
- afterEach(() => {
27
- restoreFetch();
28
- captured.restore();
29
- fs.rmSync(homeTmp, { recursive: true, force: true });
30
- os.homedir = origHome;
31
- tmp.cleanup();
32
- vi.restoreAllMocks();
33
- });
34
- function setupCanonicalSkill() {
35
- const root = path.join(tmp.dir, 'my-skill');
36
- fs.mkdirSync(path.join(root, 'claude-code', 'commands'), { recursive: true });
37
- fs.writeFileSync(path.join(root, 'index.md'), '# Skill\n\n' + 'a'.repeat(150), 'utf-8');
38
- fs.writeFileSync(path.join(root, 'botdocs.json'), JSON.stringify({
39
- type: 'skill',
40
- version: '1.0.0',
41
- title: 'My Skill',
42
- description: 'Reviews PRs',
43
- sourceEcosystem: 'claude-code',
44
- ecosystems: ['claude-code', 'claude'],
45
- }));
46
- fs.writeFileSync(path.join(root, 'claude-code', 'commands', 'my-skill.md'), '# CC content');
47
- // Generated claude/SKILL.md is missing → stale
48
- return root;
49
- }
50
- it('--no-compile does NOT call compile even when generated files are stale', async () => {
51
- const root = setupCanonicalSkill();
52
- const fm = mockFetch([
53
- {
54
- method: 'POST',
55
- url: '/api/botdocs',
56
- response: { body: { id: 'b1', slug: 'my-skill', url: 'http://x/y' } },
57
- },
58
- ]);
59
- restoreFetch = fm.restore;
60
- await publish(root, { description: 'desc', noCompile: true });
61
- expect(compileMod.compile).not.toHaveBeenCalled();
62
- });
63
- it('auto-calls compile when generated files are stale', async () => {
64
- const root = setupCanonicalSkill();
65
- const fm = mockFetch([
66
- {
67
- method: 'POST',
68
- url: '/api/botdocs',
69
- response: { body: { id: 'b1', slug: 'my-skill', url: 'http://x/y' } },
70
- },
71
- ]);
72
- restoreFetch = fm.restore;
73
- await publish(root, { description: 'desc' });
74
- expect(compileMod.compile).toHaveBeenCalledTimes(1);
75
- });
76
- it('uploads nested files with directory-prefixed filenames', async () => {
77
- // Build a tree with files in subdirectories — we need to know what
78
- // ends up in the POST body so install can route by the `claude/` /
79
- // `claude-code/` prefix.
80
- const root = path.join(tmp.dir, 'nested-skill');
81
- fs.mkdirSync(path.join(root, 'claude'), { recursive: true });
82
- fs.mkdirSync(path.join(root, 'claude-code', 'commands'), { recursive: true });
83
- fs.writeFileSync(path.join(root, 'index.md'), '# Top\n\n' + 'a'.repeat(150));
84
- fs.writeFileSync(path.join(root, 'claude', 'SKILL.md'), '# Claude content');
85
- fs.writeFileSync(path.join(root, 'claude-code', 'commands', 'cli.md'), '# CC content');
86
- const fm = mockFetch([
87
- {
88
- method: 'POST',
89
- url: '/api/botdocs',
90
- response: { body: { id: 'b1', slug: 'nested-skill', url: 'http://x/y' } },
91
- },
92
- ]);
93
- restoreFetch = fm.restore;
94
- await publish(root, { description: 'desc', noCompile: true });
95
- const post = fm.calls.find((c) => c.url.includes('/api/botdocs') && c.method === 'POST');
96
- const body = post?.body;
97
- const filenames = (body?.files ?? []).map((f) => f.filename).sort();
98
- expect(filenames).toEqual([
99
- 'claude-code/commands/cli.md',
100
- 'claude/SKILL.md',
101
- 'index.md',
102
- ]);
103
- });
104
- it('forwards botdocType + sourceEcosystem from botdocs.json', async () => {
105
- const root = setupCanonicalSkill();
106
- const fm = mockFetch([
107
- {
108
- method: 'POST',
109
- url: '/api/botdocs',
110
- response: { body: { id: 'b1', slug: 'my-skill', url: 'http://x/y' } },
111
- },
112
- ]);
113
- restoreFetch = fm.restore;
114
- await publish(root, { description: 'desc', noCompile: true });
115
- const post = fm.calls.find((c) => c.url.includes('/api/botdocs') && c.method === 'POST');
116
- const body = post?.body;
117
- expect(body.botdocType).toBe('SKILL');
118
- expect(body.sourceEcosystem).toBe('claude-code');
119
- });
120
- it('defaults to SPEC when no botdocs.json is present', async () => {
121
- const root = path.join(tmp.dir, 'plain-spec');
122
- fs.mkdirSync(root, { recursive: true });
123
- fs.writeFileSync(path.join(root, 'index.md'), '# Spec\n\n' + 'a'.repeat(150));
124
- const fm = mockFetch([
125
- {
126
- method: 'POST',
127
- url: '/api/botdocs',
128
- response: { body: { id: 'b1', slug: 'plain-spec', url: 'http://x/y' } },
129
- },
130
- ]);
131
- restoreFetch = fm.restore;
132
- await publish(root, { description: 'desc' });
133
- const post = fm.calls.find((c) => c.url.includes('/api/botdocs') && c.method === 'POST');
134
- const body = post?.body;
135
- expect(body.botdocType).toBe('SPEC');
136
- expect(body.sourceEcosystem).toBeNull();
137
- });
138
- });
@@ -1,3 +0,0 @@
1
- export declare function pull(ref: string, options?: {
2
- json?: boolean;
3
- }): Promise<void>;
@@ -1,78 +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 pull(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
- const outDir = path.resolve(slug);
12
- // Check for existing clone metadata
13
- const metaPath = path.join(outDir, '.botdocs.json');
14
- if (!fs.existsSync(metaPath)) {
15
- console.error(`No clone found at ./${slug}/`);
16
- console.error('Use `botdocs clone` first.');
17
- process.exit(1);
18
- }
19
- console.log(`Updating ${username}/${slug}...`);
20
- // Fetch latest manifest
21
- let manifest;
22
- try {
23
- manifest = await apiFetch(`/@${username}/${slug}/manifest`);
24
- }
25
- catch {
26
- console.error(`BotDoc not found: ${username}/${slug}`);
27
- process.exit(1);
28
- }
29
- // Download all files (overwrite)
30
- let updated = 0;
31
- for (const file of manifest.files) {
32
- const filePath = path.join(outDir, file.filename);
33
- const content = await fetchRawContent(file.rawUrl);
34
- const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : null;
35
- if (existing !== content) {
36
- fs.writeFileSync(filePath, content, 'utf-8');
37
- console.log(` Updated: ${file.filename}`);
38
- updated++;
39
- }
40
- }
41
- // Remove files that no longer exist in the BotDoc
42
- const metadata = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
43
- const remoteFiles = new Set(manifest.files.map((f) => f.filename));
44
- for (const oldFile of metadata.files) {
45
- if (!remoteFiles.has(oldFile)) {
46
- const oldPath = path.join(outDir, oldFile);
47
- if (fs.existsSync(oldPath)) {
48
- fs.unlinkSync(oldPath);
49
- console.log(` Removed: ${oldFile}`);
50
- updated++;
51
- }
52
- }
53
- }
54
- // Update metadata
55
- const newMetadata = {
56
- username,
57
- slug,
58
- clonedAt: metadata.clonedAt,
59
- files: manifest.files.map((f) => f.filename),
60
- };
61
- fs.writeFileSync(metaPath, JSON.stringify(newMetadata, null, 2), 'utf-8');
62
- if (options.json) {
63
- console.log(JSON.stringify({ success: true, updated, files: manifest.files.map(f => f.filename) }));
64
- }
65
- else if (updated === 0) {
66
- console.log('Already up to date.');
67
- }
68
- else {
69
- console.log(`\nUpdated ${updated} file(s).`);
70
- }
71
- }
72
- function parseRef(ref) {
73
- const cleaned = ref.startsWith('@') ? ref.slice(1) : ref;
74
- const parts = cleaned.split('/');
75
- if (parts.length !== 2 || !parts[0] || !parts[1])
76
- return null;
77
- return { username: parts[0], slug: parts[1] };
78
- }
@@ -1 +0,0 @@
1
- export {};