ht-skills 0.1.7 → 0.2.1
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 +6 -0
- package/lib/cli.js +399 -38
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -11,8 +11,14 @@ npx ht-skills add --skill repo-bug-analyze
|
|
|
11
11
|
## Commands
|
|
12
12
|
|
|
13
13
|
```text
|
|
14
|
+
ht-skills login [--registry <url>]
|
|
15
|
+
ht-skills publish [skillDir] [--registry <url>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
|
|
14
16
|
ht-skills search <query> [--registry <url>] [--limit <n>]
|
|
15
17
|
ht-skills submit <skillDir> [--registry <url>] [--submitter <name>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
|
|
16
18
|
ht-skills install <slug[@version]> [more-skills...] [--registry <url>] [--target <dir>] [--tool codex|claude|vscode]
|
|
17
19
|
ht-skills add [registry] --skill <slug[@version]>,<slug[@version]> [--tool codex|claude|vscode]
|
|
18
20
|
```
|
|
21
|
+
|
|
22
|
+
`login` opens the browser, completes marketplace sign-in, and stores a registry token under `~/.ht-skills/config.json`.
|
|
23
|
+
|
|
24
|
+
`publish` zips the target skill directory, uploads it through the marketplace package inspection flow, and then submits the generated preview token for review.
|
package/lib/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const fs = require("fs/promises");
|
|
2
2
|
const os = require("os");
|
|
3
3
|
const path = require("path");
|
|
4
|
+
const { spawn } = require("child_process");
|
|
4
5
|
const readline = require("readline/promises");
|
|
6
|
+
const AdmZip = require("adm-zip");
|
|
5
7
|
|
|
6
8
|
const INSTALL_TARGETS = {
|
|
7
9
|
codex: {
|
|
@@ -29,6 +31,11 @@ const TOOL_ALIASES = {
|
|
|
29
31
|
"github-copilot": "vscode",
|
|
30
32
|
};
|
|
31
33
|
const DEFAULT_REGISTRY_URL = "http://skills.ic.aeroht.local";
|
|
34
|
+
const CONFIG_DIR_NAME = ".ht-skills";
|
|
35
|
+
const CONFIG_FILE_NAME = "config.json";
|
|
36
|
+
const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
37
|
+
const DEFAULT_LOGIN_POLL_MS = 1500;
|
|
38
|
+
const DEFAULT_PUBLISH_POLL_MS = 1500;
|
|
32
39
|
|
|
33
40
|
const BANNER_TEXT = String.raw` _ _ _____ ___ _ _ _ _ __ __ _ _ _
|
|
34
41
|
| || |_ _| / __| |_(_) | |___ | \/ |__ _ _ _| |_____| |_ _ __| |__ _ __
|
|
@@ -40,12 +47,16 @@ function printHelp() {
|
|
|
40
47
|
// eslint-disable-next-line no-console
|
|
41
48
|
console.log(`Usage:
|
|
42
49
|
ht-skills search <query> [--registry <url>] [--limit <n>]
|
|
50
|
+
ht-skills login [--registry <url>]
|
|
51
|
+
ht-skills publish [skillDir] [--registry <url>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
|
|
43
52
|
ht-skills submit <skillDir> [--registry <url>] [--submitter <name>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
|
|
44
53
|
ht-skills install <slug[@version]> [more-skills...] [--registry <url>] [--target <dir>] [--tool codex|claude|vscode]
|
|
45
54
|
ht-skills add [registry] --skill <slug[@version]>,<slug[@version]> [--tool codex|claude|vscode]
|
|
46
55
|
|
|
47
56
|
Examples:
|
|
48
57
|
ht-skills search openai
|
|
58
|
+
ht-skills login
|
|
59
|
+
ht-skills publish .
|
|
49
60
|
ht-skills submit ./examples/hello-skill
|
|
50
61
|
ht-skills install hello-skill@1.0.0 --tool codex
|
|
51
62
|
ht-skills install hello-skill repo-bug-analyze --tool codex
|
|
@@ -103,6 +114,130 @@ async function requestJson(url, options = {}) {
|
|
|
103
114
|
return payload;
|
|
104
115
|
}
|
|
105
116
|
|
|
117
|
+
function getConfigPath(homeDir = os.homedir()) {
|
|
118
|
+
return path.join(homeDir, CONFIG_DIR_NAME, CONFIG_FILE_NAME);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function loadCliConfig(homeDir = os.homedir()) {
|
|
122
|
+
const configPath = getConfigPath(homeDir);
|
|
123
|
+
try {
|
|
124
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
125
|
+
const parsed = JSON.parse(raw);
|
|
126
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code === "ENOENT") {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function saveCliConfig(config, homeDir = os.homedir()) {
|
|
136
|
+
const configPath = getConfigPath(homeDir);
|
|
137
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
138
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getRegistryConfigKey(registry) {
|
|
142
|
+
return String(registry || "").replace(/\/$/, "");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function getStoredRegistryAuth(registry, { homeDir = os.homedir() } = {}) {
|
|
146
|
+
const config = await loadCliConfig(homeDir);
|
|
147
|
+
return config.registries?.[getRegistryConfigKey(registry)] || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function setStoredRegistryAuth(registry, value, { homeDir = os.homedir() } = {}) {
|
|
151
|
+
const config = await loadCliConfig(homeDir);
|
|
152
|
+
if (!config.registries || typeof config.registries !== "object") {
|
|
153
|
+
config.registries = {};
|
|
154
|
+
}
|
|
155
|
+
config.registries[getRegistryConfigKey(registry)] = value;
|
|
156
|
+
await saveCliConfig(config, homeDir);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function resolveAuthToken(registry, flags, { homeDir = os.homedir() } = {}) {
|
|
160
|
+
const inlineToken = String(flags.token || "").trim();
|
|
161
|
+
if (inlineToken) {
|
|
162
|
+
return inlineToken;
|
|
163
|
+
}
|
|
164
|
+
const stored = await getStoredRegistryAuth(registry, { homeDir });
|
|
165
|
+
return String(stored?.token || "").trim() || null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function getRequiredAuthToken(registry, flags, { homeDir = os.homedir() } = {}) {
|
|
169
|
+
const token = await resolveAuthToken(registry, flags, { homeDir });
|
|
170
|
+
if (!token) {
|
|
171
|
+
throw new Error(`No saved login for ${registry}. Run "ht-skills login --registry ${registry}" first.`);
|
|
172
|
+
}
|
|
173
|
+
return token;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function withBearerToken(headers = {}, token = null) {
|
|
177
|
+
if (!token) return { ...headers };
|
|
178
|
+
return {
|
|
179
|
+
...headers,
|
|
180
|
+
authorization: `Bearer ${token}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sleep(ms) {
|
|
185
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function openBrowserUrl(url) {
|
|
189
|
+
const safeUrl = String(url || "").trim();
|
|
190
|
+
if (!safeUrl) {
|
|
191
|
+
throw new Error("browser URL is required");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let child;
|
|
195
|
+
if (process.platform === "win32") {
|
|
196
|
+
child = spawn("cmd", ["/c", "start", "", safeUrl], {
|
|
197
|
+
detached: true,
|
|
198
|
+
stdio: "ignore",
|
|
199
|
+
windowsHide: true,
|
|
200
|
+
});
|
|
201
|
+
} else if (process.platform === "darwin") {
|
|
202
|
+
child = spawn("open", [safeUrl], {
|
|
203
|
+
detached: true,
|
|
204
|
+
stdio: "ignore",
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
child = spawn("xdg-open", [safeUrl], {
|
|
208
|
+
detached: true,
|
|
209
|
+
stdio: "ignore",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
child.on("error", () => {});
|
|
214
|
+
child.unref();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function createZipFromDirectory(skillDir) {
|
|
218
|
+
const zip = new AdmZip();
|
|
219
|
+
const files = await walkFiles(skillDir);
|
|
220
|
+
if (files.length === 0) {
|
|
221
|
+
throw new Error(`No files found under ${skillDir}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const absolutePath of files) {
|
|
225
|
+
const content = await fs.readFile(absolutePath);
|
|
226
|
+
const relativePath = path.relative(skillDir, absolutePath).replace(/\\/g, "/");
|
|
227
|
+
zip.addFile(relativePath, content);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return zip.toBuffer();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function summarizePreviewErrors(preview) {
|
|
234
|
+
const errors = Array.isArray(preview?.errors) ? preview.errors.filter(Boolean) : [];
|
|
235
|
+
if (errors.length === 0) {
|
|
236
|
+
return "archive inspection failed";
|
|
237
|
+
}
|
|
238
|
+
return errors.slice(0, 4).join("; ");
|
|
239
|
+
}
|
|
240
|
+
|
|
106
241
|
function formatInstallError(error, { slug, version = null, registry, stage = "resolve" }) {
|
|
107
242
|
const rawMessage = String(error?.message || "Install failed").trim();
|
|
108
243
|
const status = Number(error?.status || 0);
|
|
@@ -296,16 +431,89 @@ function normalizeInlineText(value) {
|
|
|
296
431
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
297
432
|
}
|
|
298
433
|
|
|
434
|
+
const ANSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
435
|
+
|
|
436
|
+
function stripAnsi(value) {
|
|
437
|
+
return String(value || "").replace(ANSI_PATTERN, "");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function isFullwidthCodePoint(codePoint) {
|
|
441
|
+
if (!Number.isFinite(codePoint)) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return codePoint >= 0x1100 && (
|
|
446
|
+
codePoint <= 0x115f ||
|
|
447
|
+
codePoint === 0x2329 ||
|
|
448
|
+
codePoint === 0x232a ||
|
|
449
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
450
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
451
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
452
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
453
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
454
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
455
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
456
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
|
|
457
|
+
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
|
|
458
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function getCharacterWidth(character) {
|
|
463
|
+
const codePoint = character.codePointAt(0);
|
|
464
|
+
if (!Number.isFinite(codePoint)) {
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
if (
|
|
468
|
+
codePoint === 0 ||
|
|
469
|
+
(codePoint >= 0x0000 && codePoint <= 0x001f) ||
|
|
470
|
+
(codePoint >= 0x007f && codePoint <= 0x009f)
|
|
471
|
+
) {
|
|
472
|
+
return 0;
|
|
473
|
+
}
|
|
474
|
+
return isFullwidthCodePoint(codePoint) ? 2 : 1;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function getDisplayWidth(value) {
|
|
478
|
+
let width = 0;
|
|
479
|
+
for (const character of stripAnsi(value)) {
|
|
480
|
+
width += getCharacterWidth(character);
|
|
481
|
+
}
|
|
482
|
+
return width;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function sliceTextByDisplayWidth(value, maxWidth) {
|
|
486
|
+
const safeWidth = Math.max(1, Number(maxWidth || 0));
|
|
487
|
+
const characters = [...String(value || "")];
|
|
488
|
+
let width = 0;
|
|
489
|
+
let index = 0;
|
|
490
|
+
|
|
491
|
+
while (index < characters.length) {
|
|
492
|
+
const characterWidth = getCharacterWidth(characters[index]);
|
|
493
|
+
if (width + characterWidth > safeWidth) {
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
width += characterWidth;
|
|
497
|
+
index += 1;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
slice: characters.slice(0, index).join(""),
|
|
502
|
+
rest: characters.slice(index).join(""),
|
|
503
|
+
width,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
299
507
|
function truncateText(value, maxLength) {
|
|
300
508
|
const text = normalizeInlineText(value);
|
|
301
509
|
const safeMaxLength = Math.max(1, Number(maxLength || 0));
|
|
302
|
-
if (text
|
|
510
|
+
if (getDisplayWidth(text) <= safeMaxLength) {
|
|
303
511
|
return text;
|
|
304
512
|
}
|
|
305
513
|
if (safeMaxLength <= 1) {
|
|
306
514
|
return "…";
|
|
307
515
|
}
|
|
308
|
-
return `${text
|
|
516
|
+
return `${sliceTextByDisplayWidth(text, safeMaxLength - 1).slice.trimEnd()}…`;
|
|
309
517
|
}
|
|
310
518
|
|
|
311
519
|
function wrapText(value, maxWidth) {
|
|
@@ -313,37 +521,37 @@ function wrapText(value, maxWidth) {
|
|
|
313
521
|
const safeWidth = Math.max(1, Number(maxWidth || 0));
|
|
314
522
|
if (!text) return [""];
|
|
315
523
|
|
|
316
|
-
const words = text.split(" ");
|
|
317
524
|
const lines = [];
|
|
318
|
-
let
|
|
525
|
+
let remaining = text;
|
|
319
526
|
|
|
320
|
-
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
lines.push(truncateText(word, safeWidth));
|
|
327
|
-
continue;
|
|
527
|
+
while (remaining) {
|
|
528
|
+
if (getDisplayWidth(remaining) <= safeWidth) {
|
|
529
|
+
lines.push(remaining);
|
|
530
|
+
break;
|
|
328
531
|
}
|
|
329
532
|
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
533
|
+
const characters = [...remaining];
|
|
534
|
+
let width = 0;
|
|
535
|
+
let index = 0;
|
|
536
|
+
let lastBreakIndex = -1;
|
|
335
537
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
538
|
+
while (index < characters.length) {
|
|
539
|
+
const character = characters[index];
|
|
540
|
+
const characterWidth = getCharacterWidth(character);
|
|
541
|
+
if (width + characterWidth > safeWidth) {
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
width += characterWidth;
|
|
545
|
+
index += 1;
|
|
546
|
+
if (character === " ") {
|
|
547
|
+
lastBreakIndex = index;
|
|
548
|
+
}
|
|
342
549
|
}
|
|
343
|
-
}
|
|
344
550
|
|
|
345
|
-
|
|
346
|
-
|
|
551
|
+
const breakIndex = lastBreakIndex > 0 ? lastBreakIndex : Math.max(1, index);
|
|
552
|
+
const line = characters.slice(0, breakIndex).join("").trimEnd();
|
|
553
|
+
lines.push(line);
|
|
554
|
+
remaining = characters.slice(breakIndex).join("").trimStart();
|
|
347
555
|
}
|
|
348
556
|
|
|
349
557
|
return lines.length ? lines : [""];
|
|
@@ -428,7 +636,7 @@ function formatSearchResult(item, { index = 0, colorize = (text) => text, width
|
|
|
428
636
|
}
|
|
429
637
|
|
|
430
638
|
const separator = " ";
|
|
431
|
-
const availableDescriptionWidth = width - plainPrefix
|
|
639
|
+
const availableDescriptionWidth = width - getDisplayWidth(plainPrefix) - getDisplayWidth(separator);
|
|
432
640
|
if (availableDescriptionWidth < minDescriptionWidth) {
|
|
433
641
|
return truncateText(`${plainPrefix}${separator}${descriptionPart}`, width);
|
|
434
642
|
}
|
|
@@ -443,25 +651,26 @@ function formatSearchCard(item, { index = 0, colorize = (text) => text, width =
|
|
|
443
651
|
const slugPart = `${item.slug}@${item.latestVersion}`;
|
|
444
652
|
const namePart = normalizeInlineText(item.name || "");
|
|
445
653
|
const descriptionPart = normalizeInlineText(item.description || "");
|
|
446
|
-
const maxInnerWidth = Math.max(
|
|
654
|
+
const maxInnerWidth = Math.max(16, width - 6);
|
|
447
655
|
const titleText = `${slugPart}${namePart ? ` (${namePart})` : ""}`;
|
|
448
656
|
const detailLines = wrapText(descriptionPart || "-", maxInnerWidth);
|
|
449
657
|
const contentWidth = Math.max(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
658
|
+
16,
|
|
659
|
+
getDisplayWidth(ordinal) + 1,
|
|
660
|
+
Math.min(maxInnerWidth, getDisplayWidth(titleText)),
|
|
661
|
+
...detailLines.map((line) => Math.min(maxInnerWidth, getDisplayWidth(line))),
|
|
453
662
|
);
|
|
454
|
-
const titleSuffix = "─".repeat(Math.max(0, contentWidth - ordinal
|
|
663
|
+
const titleSuffix = "─".repeat(Math.max(0, contentWidth - getDisplayWidth(ordinal) - 1));
|
|
455
664
|
const title = `${colorize("◇", "success")} ${colorize(ordinal, "accent")} ${colorize(titleSuffix, "muted")}╮`;
|
|
456
665
|
const blank = `│ ${" ".repeat(contentWidth)} │`;
|
|
457
666
|
const firstLineText = truncateText(titleText, contentWidth);
|
|
458
|
-
const firstLine = `│ ${colorize(firstLineText, "accent")}${" ".repeat(Math.max(0, contentWidth - firstLineText
|
|
667
|
+
const firstLine = `│ ${colorize(firstLineText, "accent")}${" ".repeat(Math.max(0, contentWidth - getDisplayWidth(firstLineText)))} │`;
|
|
459
668
|
const detailRendered = detailLines.map((line) => {
|
|
460
669
|
const text = truncateText(line, contentWidth);
|
|
461
|
-
return `│ ${text}${" ".repeat(Math.max(0, contentWidth - text
|
|
670
|
+
return `│ ${text}${" ".repeat(Math.max(0, contentWidth - getDisplayWidth(text)))} │`;
|
|
462
671
|
});
|
|
463
672
|
const bottom = `╰${"─".repeat(contentWidth + 2)}╯`;
|
|
464
|
-
return [title, blank, firstLine, ...detailRendered, bottom].join("\n");
|
|
673
|
+
return [title, blank, firstLine, ...detailRendered, blank, bottom].join("\n");
|
|
465
674
|
}
|
|
466
675
|
|
|
467
676
|
function printFallbackIntro({ registry, slug, version, skillName, skillDescription, installTargets }, log = console.log) {
|
|
@@ -1017,11 +1226,153 @@ async function cmdInstall(flags, deps = {}) {
|
|
|
1017
1226
|
return allTargets;
|
|
1018
1227
|
}
|
|
1019
1228
|
|
|
1229
|
+
async function cmdLogin(flags, deps = {}) {
|
|
1230
|
+
const requestJsonImpl = deps.requestJson || requestJson;
|
|
1231
|
+
const log = deps.log || ((message) => console.log(message));
|
|
1232
|
+
const openBrowser = deps.openBrowser || openBrowserUrl;
|
|
1233
|
+
const homeDir = deps.homeDir || os.homedir();
|
|
1234
|
+
const registry = getRegistryUrl(flags);
|
|
1235
|
+
const timeoutMs = Math.max(10_000, Number(flags.timeout || deps.timeoutMs || DEFAULT_LOGIN_TIMEOUT_MS));
|
|
1236
|
+
const pollIntervalMs = Math.max(500, Number(flags["poll-interval"] || deps.pollIntervalMs || DEFAULT_LOGIN_POLL_MS));
|
|
1237
|
+
|
|
1238
|
+
const started = await requestJsonImpl(`${registry}/api/cli/auth/start`, {
|
|
1239
|
+
method: "POST",
|
|
1240
|
+
headers: {
|
|
1241
|
+
"content-type": "application/json",
|
|
1242
|
+
},
|
|
1243
|
+
body: JSON.stringify({
|
|
1244
|
+
client: "ht-skills",
|
|
1245
|
+
}),
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
const browserUrl = String(started.browser_url || "").trim();
|
|
1249
|
+
const requestId = String(started.request_id || "").trim();
|
|
1250
|
+
const pollUrl = String(started.poll_url || `${registry}/api/cli/auth/poll/${encodeURIComponent(requestId)}`).trim();
|
|
1251
|
+
if (!browserUrl || !requestId) {
|
|
1252
|
+
throw new Error("registry did not return a usable CLI login flow");
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
try {
|
|
1256
|
+
await Promise.resolve(openBrowser(browserUrl));
|
|
1257
|
+
log(`Opened browser for ${registry}. Complete sign-in to continue...`);
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
log(`Open this URL in your browser and sign in:\n${browserUrl}`);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const expiresAtMs = Date.parse(started.expires_at || "") || (Date.now() + timeoutMs);
|
|
1263
|
+
const deadline = Math.min(Date.now() + timeoutMs, expiresAtMs);
|
|
1264
|
+
|
|
1265
|
+
while (Date.now() <= deadline) {
|
|
1266
|
+
await sleep(pollIntervalMs);
|
|
1267
|
+
const status = await requestJsonImpl(pollUrl);
|
|
1268
|
+
if (status.status === "approved") {
|
|
1269
|
+
await setStoredRegistryAuth(registry, {
|
|
1270
|
+
token: status.token,
|
|
1271
|
+
user: status.user || null,
|
|
1272
|
+
tokenExpiresAt: status.token_expires_at || null,
|
|
1273
|
+
savedAt: new Date().toISOString(),
|
|
1274
|
+
}, { homeDir });
|
|
1275
|
+
const userLabel = status.user?.name || status.user?.email || status.user?.id || "user";
|
|
1276
|
+
log(`Signed in to ${registry} as ${userLabel}.`);
|
|
1277
|
+
return status;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
throw new Error(`Timed out waiting for browser sign-in at ${registry}`);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async function cmdPublish(flags, deps = {}) {
|
|
1285
|
+
const requestJsonImpl = deps.requestJson || requestJson;
|
|
1286
|
+
const log = deps.log || ((message) => console.log(message));
|
|
1287
|
+
const homeDir = deps.homeDir || os.homedir();
|
|
1288
|
+
const registry = getRegistryUrl(flags);
|
|
1289
|
+
const skillDir = path.resolve(flags._[0] || ".");
|
|
1290
|
+
const token = await getRequiredAuthToken(registry, flags, { homeDir });
|
|
1291
|
+
const archiveName = `${path.basename(skillDir) || "skill"}.zip`;
|
|
1292
|
+
const archiveBuffer = await createZipFromDirectory(skillDir);
|
|
1293
|
+
const pollIntervalMs = Math.max(500, Number(flags["poll-interval"] || deps.pollIntervalMs || DEFAULT_PUBLISH_POLL_MS));
|
|
1294
|
+
|
|
1295
|
+
const job = await requestJsonImpl(
|
|
1296
|
+
`${registry}/api/skills/inspect-package-jobs/upload?archive_name=${encodeURIComponent(archiveName)}`,
|
|
1297
|
+
{
|
|
1298
|
+
method: "POST",
|
|
1299
|
+
headers: withBearerToken({
|
|
1300
|
+
"content-type": "application/zip",
|
|
1301
|
+
}, token),
|
|
1302
|
+
body: archiveBuffer,
|
|
1303
|
+
},
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
const jobId = String(job.job_id || "").trim();
|
|
1307
|
+
if (!jobId) {
|
|
1308
|
+
throw new Error("registry did not return an inspection job id");
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
let inspection = job;
|
|
1312
|
+
while (inspection.status !== "succeeded" && inspection.status !== "failed") {
|
|
1313
|
+
await sleep(pollIntervalMs);
|
|
1314
|
+
inspection = await requestJsonImpl(
|
|
1315
|
+
`${registry}/api/skills/inspect-package-jobs/${encodeURIComponent(jobId)}`,
|
|
1316
|
+
{
|
|
1317
|
+
headers: withBearerToken({}, token),
|
|
1318
|
+
},
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
if (inspection.status !== "succeeded") {
|
|
1323
|
+
throw new Error(inspection.error || "skill archive inspection failed");
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
const preview = inspection.result || {};
|
|
1327
|
+
if (!preview.valid || !preview.preview_token) {
|
|
1328
|
+
throw new Error(summarizePreviewErrors(preview));
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const body = {
|
|
1332
|
+
preview_token: preview.preview_token,
|
|
1333
|
+
};
|
|
1334
|
+
if (flags.visibility) {
|
|
1335
|
+
body.visibility = String(flags.visibility);
|
|
1336
|
+
}
|
|
1337
|
+
if (flags["shared-with"]) {
|
|
1338
|
+
body.shared_with = String(flags["shared-with"])
|
|
1339
|
+
.split(",")
|
|
1340
|
+
.map((item) => item.trim())
|
|
1341
|
+
.filter(Boolean);
|
|
1342
|
+
}
|
|
1343
|
+
if (flags["publish-now"]) {
|
|
1344
|
+
body.publish_now = true;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const result = await requestJsonImpl(`${registry}/api/skills/submit`, {
|
|
1348
|
+
method: "POST",
|
|
1349
|
+
headers: withBearerToken({
|
|
1350
|
+
"content-type": "application/json",
|
|
1351
|
+
}, token),
|
|
1352
|
+
body: JSON.stringify(body),
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
log(JSON.stringify({
|
|
1356
|
+
status: result.status,
|
|
1357
|
+
submission_id: result.submission_id || null,
|
|
1358
|
+
created_at: result.created_at || null,
|
|
1359
|
+
visibility: result.visibility || body.visibility || null,
|
|
1360
|
+
publication: result.publication || null,
|
|
1361
|
+
preview_token: preview.preview_token,
|
|
1362
|
+
archive_name: archiveName,
|
|
1363
|
+
}, null, 2));
|
|
1364
|
+
|
|
1365
|
+
return result;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1020
1368
|
async function cmdSubmit(flags, deps = {}) {
|
|
1021
1369
|
const requestJsonImpl = deps.requestJson || requestJson;
|
|
1022
1370
|
const log = deps.log || ((message) => console.log(message));
|
|
1371
|
+
const homeDir = deps.homeDir || os.homedir();
|
|
1023
1372
|
const skillDirArg = flags._[0] || ".";
|
|
1024
1373
|
const skillDir = path.resolve(skillDirArg);
|
|
1374
|
+
const registry = getRegistryUrl(flags);
|
|
1375
|
+
const token = await getRequiredAuthToken(registry, flags, { homeDir });
|
|
1025
1376
|
const manifestPath = path.resolve(flags.manifest || path.join(skillDir, "skill.json"));
|
|
1026
1377
|
const manifestRaw = await fs.readFile(manifestPath, "utf8");
|
|
1027
1378
|
const manifest = JSON.parse(manifestRaw);
|
|
@@ -1058,12 +1409,11 @@ async function cmdSubmit(flags, deps = {}) {
|
|
|
1058
1409
|
body.publish_now = true;
|
|
1059
1410
|
}
|
|
1060
1411
|
|
|
1061
|
-
const registry = getRegistryUrl(flags);
|
|
1062
1412
|
const result = await requestJsonImpl(`${registry}/api/skills/submit`, {
|
|
1063
1413
|
method: "POST",
|
|
1064
|
-
headers: {
|
|
1414
|
+
headers: withBearerToken({
|
|
1065
1415
|
"content-type": "application/json",
|
|
1066
|
-
},
|
|
1416
|
+
}, token),
|
|
1067
1417
|
body: JSON.stringify(body),
|
|
1068
1418
|
});
|
|
1069
1419
|
log(JSON.stringify(result, null, 2));
|
|
@@ -1096,6 +1446,14 @@ async function main(argv = process.argv) {
|
|
|
1096
1446
|
await cmdSearch(flags);
|
|
1097
1447
|
return;
|
|
1098
1448
|
}
|
|
1449
|
+
if (command === "login") {
|
|
1450
|
+
await cmdLogin(flags);
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (command === "publish") {
|
|
1454
|
+
await cmdPublish(flags);
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1099
1457
|
if (command === "install") {
|
|
1100
1458
|
await cmdInstall(flags);
|
|
1101
1459
|
return;
|
|
@@ -1125,11 +1483,14 @@ module.exports = {
|
|
|
1125
1483
|
normalizeSkillSpecs,
|
|
1126
1484
|
normalizeInlineText,
|
|
1127
1485
|
printFallbackSearchIntro,
|
|
1486
|
+
getConfigPath,
|
|
1128
1487
|
normalizeToolIds,
|
|
1129
1488
|
parseInstallSelection,
|
|
1130
1489
|
getAvailableInstallTargets,
|
|
1131
1490
|
resolveInstallTargets,
|
|
1132
1491
|
fetchResolvedVersion,
|
|
1492
|
+
cmdLogin,
|
|
1493
|
+
cmdPublish,
|
|
1133
1494
|
cmdInstall,
|
|
1134
1495
|
cmdAdd,
|
|
1135
1496
|
cmdSearch,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ht-skills",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "CLI for installing and submitting skills from HT Skills Marketplace.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@clack/prompts": "^1.1.0",
|
|
19
|
+
"adm-zip": "^0.5.16",
|
|
19
20
|
"picocolors": "^1.1.1"
|
|
20
21
|
}
|
|
21
22
|
}
|