ht-skills 0.1.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 +17 -0
- package/bin/ht-skills.js +9 -0
- package/lib/cli.js +637 -0
- package/package.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# ht-skills
|
|
2
|
+
|
|
3
|
+
CLI for installing and submitting skills from HT Skills Marketplace.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
npx ht-skills install repo-bug-analyze --registry http://skills.ic.aeroht.local
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
ht-skills search <query> [--registry <url>] [--limit <n>]
|
|
15
|
+
ht-skills submit <skillDir> [--registry <url>] [--submitter <name>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
|
|
16
|
+
ht-skills install <slug[@version]> [--registry <url>] [--target <dir>] [--tool codex|claude|vscode]
|
|
17
|
+
```
|
package/bin/ht-skills.js
ADDED
package/lib/cli.js
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
const fs = require("fs/promises");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const readline = require("readline/promises");
|
|
5
|
+
|
|
6
|
+
const INSTALL_TARGETS = {
|
|
7
|
+
codex: {
|
|
8
|
+
label: "Codex",
|
|
9
|
+
resolveTarget(homeDir, slug) {
|
|
10
|
+
return path.join(homeDir, ".codex", "skills", slug);
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
claude: {
|
|
14
|
+
label: "Claude",
|
|
15
|
+
resolveTarget(homeDir, slug) {
|
|
16
|
+
return path.join(homeDir, ".claude", "skills", slug);
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
vscode: {
|
|
20
|
+
label: "VS Code",
|
|
21
|
+
resolveTarget(homeDir, slug) {
|
|
22
|
+
return path.join(homeDir, ".copilot", "skills", slug);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const TOOL_ALIASES = {
|
|
28
|
+
"claude-code": "claude",
|
|
29
|
+
"github-copilot": "vscode",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function printHelp() {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.log(`Usage:
|
|
35
|
+
ht-skills search <query> [--registry <url>] [--limit <n>]
|
|
36
|
+
ht-skills submit <skillDir> [--registry <url>] [--submitter <name>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
|
|
37
|
+
ht-skills install <slug[@version]> [--registry <url>] [--target <dir>] [--tool codex|claude|vscode]
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
ht-skills search openai --registry http://localhost:8787
|
|
41
|
+
ht-skills submit ./examples/hello-skill --registry http://localhost:8787
|
|
42
|
+
ht-skills install hello-skill@1.0.0 --registry http://localhost:8787 --tool codex
|
|
43
|
+
ht-skills install hello-skill@1.0.0 --registry http://localhost:8787 --tool codex,claude,vscode
|
|
44
|
+
ht-skills install hello-skill --registry http://localhost:8787`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseArgs(argv) {
|
|
48
|
+
const result = { _: [] };
|
|
49
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
50
|
+
const token = argv[i];
|
|
51
|
+
if (!token.startsWith("--")) {
|
|
52
|
+
result._.push(token);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const [key, inlineValue] = token.slice(2).split("=", 2);
|
|
57
|
+
if (inlineValue !== undefined) {
|
|
58
|
+
result[key] = inlineValue;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const next = argv[i + 1];
|
|
63
|
+
if (!next || next.startsWith("--")) {
|
|
64
|
+
result[key] = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
result[key] = next;
|
|
68
|
+
i += 1;
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getRegistryUrl(flags) {
|
|
74
|
+
return String(flags.registry || process.env.SKILLS_REGISTRY_URL || "http://localhost:8787").replace(/\/$/, "");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function requestJson(url, options = {}) {
|
|
78
|
+
const res = await fetch(url, options);
|
|
79
|
+
const text = await res.text();
|
|
80
|
+
let payload;
|
|
81
|
+
try {
|
|
82
|
+
payload = text ? JSON.parse(text) : {};
|
|
83
|
+
} catch {
|
|
84
|
+
payload = { raw: text };
|
|
85
|
+
}
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const message = payload.error || text || `HTTP ${res.status}`;
|
|
88
|
+
throw new Error(message);
|
|
89
|
+
}
|
|
90
|
+
return payload;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function walkFiles(baseDir) {
|
|
94
|
+
const results = [];
|
|
95
|
+
const ignored = new Set([".git", "node_modules", ".DS_Store"]);
|
|
96
|
+
|
|
97
|
+
async function walk(currentDir) {
|
|
98
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (ignored.has(entry.name)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
await walk(absolutePath);
|
|
107
|
+
} else if (entry.isFile()) {
|
|
108
|
+
results.push(absolutePath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await walk(baseDir);
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseSpec(spec) {
|
|
118
|
+
const [slug, version] = String(spec).split("@", 2);
|
|
119
|
+
if (!slug) {
|
|
120
|
+
throw new Error("install requires <slug> or <slug>@<version>");
|
|
121
|
+
}
|
|
122
|
+
return { slug, version: version || null };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getAvailableInstallTargets(slug, { homeDir = os.homedir() } = {}) {
|
|
126
|
+
return Object.entries(INSTALL_TARGETS).map(([id, target]) => ({
|
|
127
|
+
id,
|
|
128
|
+
label: target.label,
|
|
129
|
+
target: target.resolveTarget(homeDir, slug),
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeToolIds(rawTool) {
|
|
134
|
+
if (!rawTool) return [];
|
|
135
|
+
const values = Array.isArray(rawTool) ? rawTool : [rawTool];
|
|
136
|
+
const output = [];
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
|
|
139
|
+
for (const value of values) {
|
|
140
|
+
const parts = String(value)
|
|
141
|
+
.split(",")
|
|
142
|
+
.map((item) => item.trim().toLowerCase())
|
|
143
|
+
.filter(Boolean);
|
|
144
|
+
|
|
145
|
+
for (const part of parts) {
|
|
146
|
+
if (part === "all" || part === "*") {
|
|
147
|
+
return Object.keys(INSTALL_TARGETS);
|
|
148
|
+
}
|
|
149
|
+
const normalizedPart = TOOL_ALIASES[part] || part;
|
|
150
|
+
if (!INSTALL_TARGETS[normalizedPart]) {
|
|
151
|
+
throw new Error(`unknown tool: ${part}`);
|
|
152
|
+
}
|
|
153
|
+
if (!seen.has(normalizedPart)) {
|
|
154
|
+
seen.add(normalizedPart);
|
|
155
|
+
output.push(normalizedPart);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return output;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseInstallSelection(input, availableTargets) {
|
|
164
|
+
const trimmed = String(input || "").trim().toLowerCase();
|
|
165
|
+
if (!trimmed) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
if (trimmed === "all" || trimmed === "*") {
|
|
169
|
+
return availableTargets.map((item) => item.id);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const byNumber = new Map(availableTargets.map((item, index) => [String(index + 1), item.id]));
|
|
173
|
+
const byId = new Map(availableTargets.map((item) => [item.id, item.id]));
|
|
174
|
+
const values = trimmed
|
|
175
|
+
.split(/[,\s]+/)
|
|
176
|
+
.map((item) => item.trim())
|
|
177
|
+
.filter(Boolean);
|
|
178
|
+
|
|
179
|
+
const result = [];
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
for (const value of values) {
|
|
182
|
+
const resolved = byNumber.get(value) || byId.get(value);
|
|
183
|
+
if (!resolved) {
|
|
184
|
+
throw new Error(`unknown selection: ${value}`);
|
|
185
|
+
}
|
|
186
|
+
if (!seen.has(resolved)) {
|
|
187
|
+
seen.add(resolved);
|
|
188
|
+
result.push(resolved);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resolveInstallTargets({ toolIds, target, slug, version, homeDir = os.homedir() }) {
|
|
195
|
+
if (target && toolIds.length > 1) {
|
|
196
|
+
throw new Error("--target can only be used with a single install destination");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (target) {
|
|
200
|
+
return [{
|
|
201
|
+
id: toolIds[0] || "custom",
|
|
202
|
+
label: toolIds[0] ? INSTALL_TARGETS[toolIds[0]].label : "Custom",
|
|
203
|
+
target: path.resolve(target),
|
|
204
|
+
}];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (toolIds.length > 0) {
|
|
208
|
+
return toolIds.map((toolId) => ({
|
|
209
|
+
id: toolId,
|
|
210
|
+
label: INSTALL_TARGETS[toolId].label,
|
|
211
|
+
target: INSTALL_TARGETS[toolId].resolveTarget(homeDir, slug),
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return [{
|
|
216
|
+
id: "local",
|
|
217
|
+
label: "Local",
|
|
218
|
+
target: path.resolve(process.cwd(), "installed-skills", slug, version || "latest"),
|
|
219
|
+
}];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function formatCount(value, noun) {
|
|
223
|
+
return `${value} ${noun}${value === 1 ? "" : "s"}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatInstallSummary(target, colorize = (text) => text) {
|
|
227
|
+
return `${colorize(target.label, "accent")} ${colorize("->", "muted")} ${target.target}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function formatTargetsNote(installTargets, colorize = (text) => text) {
|
|
231
|
+
return installTargets
|
|
232
|
+
.map((target, index) => `${colorize(`${index + 1}.`, "muted")} ${formatInstallSummary(target, colorize)}`)
|
|
233
|
+
.join("\n");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function printFallbackIntro({ registry, slug, version, skillName, skillDescription, installTargets }, log = console.log) {
|
|
237
|
+
log("");
|
|
238
|
+
log("ht-skills");
|
|
239
|
+
log("");
|
|
240
|
+
log(`Source: ${registry}`);
|
|
241
|
+
log(`Skill: ${skillName} (${slug}@${version})`);
|
|
242
|
+
if (skillDescription) {
|
|
243
|
+
log(skillDescription);
|
|
244
|
+
}
|
|
245
|
+
log("");
|
|
246
|
+
log(`Found ${installTargets.length} install target${installTargets.length === 1 ? "" : "s"}`);
|
|
247
|
+
for (const line of formatTargetsNote(installTargets).split("\n")) {
|
|
248
|
+
log(` ${line}`);
|
|
249
|
+
}
|
|
250
|
+
log("");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function loadTerminalUi() {
|
|
254
|
+
const [{ intro, outro, note, multiselect, spinner, isCancel, cancel }, pcModule] = await Promise.all([
|
|
255
|
+
import("@clack/prompts"),
|
|
256
|
+
import("picocolors"),
|
|
257
|
+
]);
|
|
258
|
+
const pc = pcModule.default || pcModule;
|
|
259
|
+
return {
|
|
260
|
+
intro,
|
|
261
|
+
outro,
|
|
262
|
+
note,
|
|
263
|
+
multiselect,
|
|
264
|
+
spinner,
|
|
265
|
+
isCancel,
|
|
266
|
+
cancel,
|
|
267
|
+
pc,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function createUiColorizer(pc) {
|
|
272
|
+
return (text, tone = "default") => {
|
|
273
|
+
const value = String(text);
|
|
274
|
+
switch (tone) {
|
|
275
|
+
case "accent":
|
|
276
|
+
return pc.cyan(pc.bold(value));
|
|
277
|
+
case "muted":
|
|
278
|
+
return pc.dim(value);
|
|
279
|
+
case "success":
|
|
280
|
+
return pc.green(value);
|
|
281
|
+
case "warn":
|
|
282
|
+
return pc.yellow(value);
|
|
283
|
+
default:
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function selectInstallTargets({ availableTargets, ask = null, ui = null }) {
|
|
290
|
+
if (ui) {
|
|
291
|
+
const selection = await ui.multiselect({
|
|
292
|
+
message: "Which tools do you want to install to?",
|
|
293
|
+
options: availableTargets.map((target) => ({
|
|
294
|
+
value: target.id,
|
|
295
|
+
label: target.label,
|
|
296
|
+
hint: target.target,
|
|
297
|
+
})),
|
|
298
|
+
required: true,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (ui.isCancel(selection)) {
|
|
302
|
+
ui.cancel("Install cancelled.");
|
|
303
|
+
const error = new Error("install cancelled");
|
|
304
|
+
error.cancelled = true;
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return selection;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
while (true) {
|
|
312
|
+
const answer = await ask("Which tools do you want to install to? (number, name, or 'all'): ");
|
|
313
|
+
let selected;
|
|
314
|
+
try {
|
|
315
|
+
selected = parseInstallSelection(answer, availableTargets);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
// eslint-disable-next-line no-console
|
|
318
|
+
console.log(`Invalid selection: ${error.message}`);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (selected.length === 0) {
|
|
322
|
+
// eslint-disable-next-line no-console
|
|
323
|
+
console.log("Please choose at least one install target.");
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
return selected;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function writeBundleToTarget(bundle, targetPath) {
|
|
331
|
+
await fs.mkdir(targetPath, { recursive: true });
|
|
332
|
+
|
|
333
|
+
for (const file of bundle.files) {
|
|
334
|
+
const absolutePath = path.join(targetPath, file.path);
|
|
335
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
336
|
+
await fs.writeFile(absolutePath, Buffer.from(file.content_base64, "base64"));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function fetchResolvedVersion(registry, parsed, requestJsonImpl) {
|
|
341
|
+
if (parsed.version) {
|
|
342
|
+
return parsed.version;
|
|
343
|
+
}
|
|
344
|
+
const latest = await requestJsonImpl(`${registry}/api/skills/${encodeURIComponent(parsed.slug)}`);
|
|
345
|
+
if (!latest.latestVersion) {
|
|
346
|
+
throw new Error(`failed to resolve latest version for ${parsed.slug}`);
|
|
347
|
+
}
|
|
348
|
+
return latest.latestVersion;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function cmdSearch(flags, deps = {}) {
|
|
352
|
+
const log = deps.log || ((message) => console.log(message));
|
|
353
|
+
const requestJsonImpl = deps.requestJson || requestJson;
|
|
354
|
+
const query = flags._.join(" ").trim();
|
|
355
|
+
if (!query) {
|
|
356
|
+
throw new Error("search requires a keyword");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const registry = getRegistryUrl(flags);
|
|
360
|
+
const limit = Number(flags.limit || 20);
|
|
361
|
+
const url = `${registry}/api/skills/search?q=${encodeURIComponent(query)}&limit=${encodeURIComponent(limit)}`;
|
|
362
|
+
const data = await requestJsonImpl(url);
|
|
363
|
+
|
|
364
|
+
if (!data.items || data.items.length === 0) {
|
|
365
|
+
log("no results");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const item of data.items) {
|
|
370
|
+
log(`${item.slug}@${item.latestVersion} ${item.name} ${item.description || ""}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function cmdInstall(flags, deps = {}) {
|
|
375
|
+
const requestJsonImpl = deps.requestJson || requestJson;
|
|
376
|
+
const ask = deps.ask || null;
|
|
377
|
+
const isInteractive = typeof deps.isInteractive === "boolean"
|
|
378
|
+
? deps.isInteractive
|
|
379
|
+
: Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
380
|
+
const homeDir = deps.homeDir || os.homedir();
|
|
381
|
+
const log = deps.log || ((message) => console.log(message));
|
|
382
|
+
const canUseFancyUi = Boolean(isInteractive && !ask && !deps.disableUi);
|
|
383
|
+
const ui = canUseFancyUi ? await loadTerminalUi().catch(() => null) : null;
|
|
384
|
+
const colorize = ui ? createUiColorizer(ui.pc) : (text) => String(text);
|
|
385
|
+
const spec = flags._[0];
|
|
386
|
+
if (!spec) {
|
|
387
|
+
throw new Error("install requires a skill spec, for example my-skill@1.0.0");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const registry = getRegistryUrl(flags);
|
|
391
|
+
const parsed = parseSpec(spec);
|
|
392
|
+
|
|
393
|
+
const resolveSpinner = ui ? ui.spinner() : null;
|
|
394
|
+
if (resolveSpinner) {
|
|
395
|
+
ui.intro(ui.pc.bgCyan(ui.pc.black(" ht-skills ")));
|
|
396
|
+
resolveSpinner.start(`Resolving ${parsed.slug}`);
|
|
397
|
+
}
|
|
398
|
+
const version = await fetchResolvedVersion(registry, parsed, requestJsonImpl);
|
|
399
|
+
if (resolveSpinner) {
|
|
400
|
+
resolveSpinner.stop(`Resolved ${parsed.slug}@${version}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let toolIds = normalizeToolIds(flags.tool);
|
|
404
|
+
const renderInstallLine = deps.renderInstallLine || ((target) => `installed ${parsed.slug}@${version} -> ${target.target}`);
|
|
405
|
+
let metadata = null;
|
|
406
|
+
|
|
407
|
+
const metadataSpinner = ui ? ui.spinner() : null;
|
|
408
|
+
if (metadataSpinner) {
|
|
409
|
+
metadataSpinner.start("Loading skill metadata");
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
metadata = await requestJsonImpl(
|
|
413
|
+
`${registry}/api/skills/${encodeURIComponent(parsed.slug)}/${encodeURIComponent(version)}`,
|
|
414
|
+
);
|
|
415
|
+
} catch {
|
|
416
|
+
metadata = null;
|
|
417
|
+
}
|
|
418
|
+
if (metadataSpinner) {
|
|
419
|
+
metadataSpinner.stop(metadata?.manifest?.name
|
|
420
|
+
? `Loaded ${metadata.manifest.name}`
|
|
421
|
+
: "Loaded install metadata");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const skillName = metadata?.manifest?.name || parsed.slug;
|
|
425
|
+
const skillDescription = metadata?.manifest?.description || "";
|
|
426
|
+
|
|
427
|
+
if (toolIds.length === 0 && !flags.target && isInteractive) {
|
|
428
|
+
const availableTargets = getAvailableInstallTargets(parsed.slug, { homeDir });
|
|
429
|
+
if (availableTargets.length > 0 && ui) {
|
|
430
|
+
ui.note(
|
|
431
|
+
[
|
|
432
|
+
`${colorize("Source", "muted")}: ${registry}`,
|
|
433
|
+
`${colorize("Skill", "muted")}: ${colorize(skillName, "accent")} ${colorize(`(${parsed.slug}@${version})`, "muted")}`,
|
|
434
|
+
skillDescription ? `${colorize("Summary", "muted")}: ${skillDescription}` : "",
|
|
435
|
+
"",
|
|
436
|
+
`${colorize("Available targets", "muted")}:`,
|
|
437
|
+
formatTargetsNote(availableTargets, colorize),
|
|
438
|
+
].filter(Boolean).join("\n"),
|
|
439
|
+
"Install plan",
|
|
440
|
+
);
|
|
441
|
+
} else if (availableTargets.length > 0) {
|
|
442
|
+
printFallbackIntro({
|
|
443
|
+
registry,
|
|
444
|
+
slug: parsed.slug,
|
|
445
|
+
version,
|
|
446
|
+
skillName,
|
|
447
|
+
skillDescription,
|
|
448
|
+
installTargets: availableTargets,
|
|
449
|
+
}, log);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (availableTargets.length > 0) {
|
|
453
|
+
toolIds = await selectInstallTargets({
|
|
454
|
+
availableTargets,
|
|
455
|
+
ask: ask || (async (promptText) => {
|
|
456
|
+
const rl = readline.createInterface({
|
|
457
|
+
input: process.stdin,
|
|
458
|
+
output: process.stdout,
|
|
459
|
+
});
|
|
460
|
+
try {
|
|
461
|
+
return await rl.question(promptText);
|
|
462
|
+
} finally {
|
|
463
|
+
rl.close();
|
|
464
|
+
}
|
|
465
|
+
}),
|
|
466
|
+
ui,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const installTargets = resolveInstallTargets({
|
|
472
|
+
toolIds,
|
|
473
|
+
target: flags.target,
|
|
474
|
+
slug: parsed.slug,
|
|
475
|
+
version,
|
|
476
|
+
homeDir,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (!ui) {
|
|
480
|
+
printFallbackIntro({
|
|
481
|
+
registry,
|
|
482
|
+
slug: parsed.slug,
|
|
483
|
+
version,
|
|
484
|
+
skillName,
|
|
485
|
+
skillDescription,
|
|
486
|
+
installTargets,
|
|
487
|
+
}, log);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const bundleSpinner = ui ? ui.spinner() : null;
|
|
491
|
+
if (bundleSpinner) {
|
|
492
|
+
bundleSpinner.start("Downloading skill bundle");
|
|
493
|
+
}
|
|
494
|
+
const bundle = await requestJsonImpl(
|
|
495
|
+
`${registry}/api/skills/${encodeURIComponent(parsed.slug)}/${encodeURIComponent(version)}/bundle`,
|
|
496
|
+
);
|
|
497
|
+
if (bundleSpinner) {
|
|
498
|
+
bundleSpinner.stop(`Downloaded ${parsed.slug}@${version}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const installSpinner = ui ? ui.spinner() : null;
|
|
502
|
+
if (installSpinner) {
|
|
503
|
+
installSpinner.start(`Installing to ${formatCount(installTargets.length, "target")}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
for (const target of installTargets) {
|
|
507
|
+
await writeBundleToTarget(bundle, target.target);
|
|
508
|
+
|
|
509
|
+
const metadataPath = path.join(target.target, ".marketplace-install.json");
|
|
510
|
+
await fs.writeFile(
|
|
511
|
+
metadataPath,
|
|
512
|
+
`${JSON.stringify(
|
|
513
|
+
{
|
|
514
|
+
slug: parsed.slug,
|
|
515
|
+
version,
|
|
516
|
+
installedAt: new Date().toISOString(),
|
|
517
|
+
registry,
|
|
518
|
+
checksum: bundle.checksum,
|
|
519
|
+
tool: target.id,
|
|
520
|
+
},
|
|
521
|
+
null,
|
|
522
|
+
2,
|
|
523
|
+
)}\n`,
|
|
524
|
+
"utf8",
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
if (!ui) {
|
|
528
|
+
log(renderInstallLine(target));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (installSpinner) {
|
|
533
|
+
installSpinner.stop(`Installed to ${formatCount(installTargets.length, "target")}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (installTargets.length > 0) {
|
|
537
|
+
if (ui) {
|
|
538
|
+
ui.note(formatTargetsNote(installTargets, colorize), "Installed to");
|
|
539
|
+
ui.outro(`Installed ${ui.pc.cyan(parsed.slug)} to ${installTargets.length} target${installTargets.length === 1 ? "" : "s"}.`);
|
|
540
|
+
} else {
|
|
541
|
+
log(`Installed ${parsed.slug} to ${formatCount(installTargets.length, "target")}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return installTargets;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function cmdSubmit(flags, deps = {}) {
|
|
549
|
+
const requestJsonImpl = deps.requestJson || requestJson;
|
|
550
|
+
const log = deps.log || ((message) => console.log(message));
|
|
551
|
+
const skillDirArg = flags._[0] || ".";
|
|
552
|
+
const skillDir = path.resolve(skillDirArg);
|
|
553
|
+
const manifestPath = path.resolve(flags.manifest || path.join(skillDir, "skill.json"));
|
|
554
|
+
const manifestRaw = await fs.readFile(manifestPath, "utf8");
|
|
555
|
+
const manifest = JSON.parse(manifestRaw);
|
|
556
|
+
|
|
557
|
+
const filePaths = await walkFiles(skillDir);
|
|
558
|
+
const files = [];
|
|
559
|
+
for (const absolutePath of filePaths) {
|
|
560
|
+
if (path.resolve(absolutePath) === manifestPath) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const content = await fs.readFile(absolutePath);
|
|
564
|
+
files.push({
|
|
565
|
+
path: path.relative(skillDir, absolutePath).replace(/\\/g, "/"),
|
|
566
|
+
content_base64: content.toString("base64"),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const body = {
|
|
571
|
+
manifest,
|
|
572
|
+
files,
|
|
573
|
+
submitter: String(flags.submitter || process.env.USERNAME || os.userInfo().username || "anonymous"),
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
if (flags.visibility) {
|
|
577
|
+
body.visibility = String(flags.visibility);
|
|
578
|
+
}
|
|
579
|
+
if (flags["shared-with"]) {
|
|
580
|
+
body.shared_with = String(flags["shared-with"])
|
|
581
|
+
.split(",")
|
|
582
|
+
.map((item) => item.trim())
|
|
583
|
+
.filter(Boolean);
|
|
584
|
+
}
|
|
585
|
+
if (flags["publish-now"]) {
|
|
586
|
+
body.publish_now = true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const registry = getRegistryUrl(flags);
|
|
590
|
+
const result = await requestJsonImpl(`${registry}/api/skills/submit`, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: {
|
|
593
|
+
"content-type": "application/json",
|
|
594
|
+
},
|
|
595
|
+
body: JSON.stringify(body),
|
|
596
|
+
});
|
|
597
|
+
log(JSON.stringify(result, null, 2));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function main(argv = process.argv) {
|
|
601
|
+
const [, , command, ...rest] = argv;
|
|
602
|
+
const flags = parseArgs(rest);
|
|
603
|
+
|
|
604
|
+
if (!command || command === "help" || command === "--help") {
|
|
605
|
+
printHelp();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (command === "search") {
|
|
609
|
+
await cmdSearch(flags);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (command === "install") {
|
|
613
|
+
await cmdInstall(flags);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (command === "submit") {
|
|
617
|
+
await cmdSubmit(flags);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
throw new Error(`unknown command: ${command}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
module.exports = {
|
|
625
|
+
INSTALL_TARGETS,
|
|
626
|
+
parseArgs,
|
|
627
|
+
parseSpec,
|
|
628
|
+
normalizeToolIds,
|
|
629
|
+
parseInstallSelection,
|
|
630
|
+
getAvailableInstallTargets,
|
|
631
|
+
resolveInstallTargets,
|
|
632
|
+
fetchResolvedVersion,
|
|
633
|
+
cmdInstall,
|
|
634
|
+
cmdSearch,
|
|
635
|
+
cmdSubmit,
|
|
636
|
+
main,
|
|
637
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ht-skills",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for installing and submitting skills from HT Skills Marketplace.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ht-skills": "./bin/ht-skills.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.17.0"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@clack/prompts": "^1.1.0",
|
|
19
|
+
"picocolors": "^1.1.1"
|
|
20
|
+
}
|
|
21
|
+
}
|