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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/lib/cli.js +399 -38
  3. 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.length <= safeMaxLength) {
510
+ if (getDisplayWidth(text) <= safeMaxLength) {
303
511
  return text;
304
512
  }
305
513
  if (safeMaxLength <= 1) {
306
514
  return "…";
307
515
  }
308
- return `${text.slice(0, safeMaxLength - 1).trimEnd()}…`;
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 current = "";
525
+ let remaining = text;
319
526
 
320
- for (const word of words) {
321
- if (!current) {
322
- if (word.length <= safeWidth) {
323
- current = word;
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 candidate = `${current} ${word}`;
331
- if (candidate.length <= safeWidth) {
332
- current = candidate;
333
- continue;
334
- }
533
+ const characters = [...remaining];
534
+ let width = 0;
535
+ let index = 0;
536
+ let lastBreakIndex = -1;
335
537
 
336
- lines.push(current);
337
- if (word.length <= safeWidth) {
338
- current = word;
339
- } else {
340
- lines.push(truncateText(word, safeWidth));
341
- current = "";
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
- if (current) {
346
- lines.push(current);
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.length - separator.length;
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(42, width - 6);
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
- Math.min(maxInnerWidth, titleText.length),
451
- ...detailLines.map((line) => Math.min(maxInnerWidth, line.length)),
452
- 42,
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.length - 1));
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.length))} │`;
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.length))} │`;
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.7",
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
  }