@botdocs/cli 0.3.1 → 0.3.2
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/dist/commands/publish.js +53 -16
- package/dist/commands/publish.test.js +62 -0
- package/package.json +1 -1
package/dist/commands/publish.js
CHANGED
|
@@ -65,6 +65,11 @@ export async function publish(source, options) {
|
|
|
65
65
|
console.error('Description is required. Use --description "..."');
|
|
66
66
|
process.exit(1);
|
|
67
67
|
}
|
|
68
|
+
// Pull type + sourceEcosystem from botdocs.json so the server stores
|
|
69
|
+
// the row as a SKILL/BUNDLE (instead of defaulting to SPEC).
|
|
70
|
+
const manifest = stat.isDirectory() ? readManifest(resolved) : null;
|
|
71
|
+
const botdocType = manifest?.type ?? 'SPEC';
|
|
72
|
+
const sourceEcosystem = manifest?.sourceEcosystem ?? null;
|
|
68
73
|
console.log(`Publishing "${title}" (${files.length} file(s))...`);
|
|
69
74
|
const result = await apiFetch('/api/botdocs', {
|
|
70
75
|
method: 'POST',
|
|
@@ -76,6 +81,8 @@ export async function publish(source, options) {
|
|
|
76
81
|
tags,
|
|
77
82
|
license,
|
|
78
83
|
files,
|
|
84
|
+
botdocType,
|
|
85
|
+
sourceEcosystem,
|
|
79
86
|
},
|
|
80
87
|
});
|
|
81
88
|
if (options.json) {
|
|
@@ -85,6 +92,21 @@ export async function publish(source, options) {
|
|
|
85
92
|
console.log(`\nPublished: ${result.url}`);
|
|
86
93
|
}
|
|
87
94
|
}
|
|
95
|
+
function readManifest(source) {
|
|
96
|
+
const manifestPath = path.join(source, 'botdocs.json');
|
|
97
|
+
if (!fs.existsSync(manifestPath))
|
|
98
|
+
return null;
|
|
99
|
+
try {
|
|
100
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
101
|
+
return parseManifest(raw);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Validate is the canonical place to surface manifest errors; here we
|
|
105
|
+
// silently fall back so a malformed file doesn't break a publish that
|
|
106
|
+
// was already going to default to SPEC anyway.
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
88
110
|
async function maybeAutoCompile(source, options) {
|
|
89
111
|
if (options.noCompile)
|
|
90
112
|
return;
|
|
@@ -124,25 +146,38 @@ function collectFromFile(filePath) {
|
|
|
124
146
|
return [{ filename, content, sortOrder: 0 }];
|
|
125
147
|
}
|
|
126
148
|
function collectFromDirectory(dirPath) {
|
|
127
|
-
const entries = fs.readdirSync(dirPath);
|
|
128
149
|
const files = [];
|
|
129
|
-
|
|
130
|
-
|
|
150
|
+
walkDirectory(dirPath, dirPath, files);
|
|
151
|
+
files.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
152
|
+
return files;
|
|
153
|
+
}
|
|
154
|
+
function walkDirectory(rootDir, currentDir, out) {
|
|
155
|
+
const entries = fs.readdirSync(currentDir);
|
|
156
|
+
for (const entry of entries) {
|
|
131
157
|
if (entry.startsWith('.'))
|
|
132
158
|
continue;
|
|
133
|
-
const fullPath = path.join(
|
|
159
|
+
const fullPath = path.join(currentDir, entry);
|
|
134
160
|
const stat = fs.statSync(fullPath);
|
|
135
|
-
if (stat.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
content: fs.readFileSync(fullPath, 'utf-8'),
|
|
139
|
-
sortOrder: entry === 'index.md' ? 0 : i + 1,
|
|
140
|
-
});
|
|
161
|
+
if (stat.isDirectory()) {
|
|
162
|
+
walkDirectory(rootDir, fullPath, out);
|
|
163
|
+
continue;
|
|
141
164
|
}
|
|
165
|
+
if (!stat.isFile())
|
|
166
|
+
continue;
|
|
167
|
+
if (!entry.endsWith('.md') && !entry.endsWith('.markdown'))
|
|
168
|
+
continue;
|
|
169
|
+
// POSIX-style relative path so the install-time prefix check
|
|
170
|
+
// (e.g. `claude/SKILL.md`) works on every platform.
|
|
171
|
+
const relative = path
|
|
172
|
+
.relative(rootDir, fullPath)
|
|
173
|
+
.split(path.sep)
|
|
174
|
+
.join('/');
|
|
175
|
+
out.push({
|
|
176
|
+
filename: relative,
|
|
177
|
+
content: fs.readFileSync(fullPath, 'utf-8'),
|
|
178
|
+
sortOrder: relative === 'index.md' ? 0 : out.length + 1,
|
|
179
|
+
});
|
|
142
180
|
}
|
|
143
|
-
// Ensure index.md is first
|
|
144
|
-
files.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
145
|
-
return files;
|
|
146
181
|
}
|
|
147
182
|
function collectFromZip(zipPath) {
|
|
148
183
|
const zip = new AdmZip(zipPath);
|
|
@@ -152,11 +187,13 @@ function collectFromZip(zipPath) {
|
|
|
152
187
|
const entry = entries[i];
|
|
153
188
|
if (entry.isDirectory)
|
|
154
189
|
continue;
|
|
155
|
-
//
|
|
156
|
-
|
|
190
|
+
// POSIX-style path inside the archive — preserves directory prefixes
|
|
191
|
+
// (e.g. `claude/SKILL.md`) so install can route the file correctly.
|
|
192
|
+
const filename = entry.entryName.replace(/\\/g, '/');
|
|
193
|
+
const basename = path.basename(filename);
|
|
157
194
|
if (!filename.endsWith('.md') && !filename.endsWith('.markdown'))
|
|
158
195
|
continue;
|
|
159
|
-
if (
|
|
196
|
+
if (basename.startsWith('.'))
|
|
160
197
|
continue;
|
|
161
198
|
files.push({
|
|
162
199
|
filename,
|
|
@@ -73,4 +73,66 @@ describe('publish auto-compile gate', () => {
|
|
|
73
73
|
await publish(root, { description: 'desc' });
|
|
74
74
|
expect(compileMod.compile).toHaveBeenCalledTimes(1);
|
|
75
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
|
+
});
|
|
76
138
|
});
|