@botdocs/cli 0.3.2 → 0.5.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 +145 -36
- 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/ingest.d.ts +2 -0
- package/dist/commands/ingest.js +162 -28
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.js +40 -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 +46 -39
- 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
|
@@ -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
|
-
});
|
package/dist/commands/pull.d.ts
DELETED
package/dist/commands/pull.js
DELETED
|
@@ -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 {};
|