ht-skills 0.2.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/lib/cli.js +142 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -12,13 +12,13 @@ npx ht-skills add --skill repo-bug-analyze
12
12
 
13
13
  ```text
14
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]
15
+ ht-skills publish [skillDir] [--registry <url>] [--access public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
16
16
  ht-skills search <query> [--registry <url>] [--limit <n>]
17
17
  ht-skills submit <skillDir> [--registry <url>] [--submitter <name>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
18
18
  ht-skills install <slug[@version]> [more-skills...] [--registry <url>] [--target <dir>] [--tool codex|claude|vscode]
19
19
  ht-skills add [registry] --skill <slug[@version]>,<slug[@version]> [--tool codex|claude|vscode]
20
20
  ```
21
21
 
22
- `login` opens the browser, completes marketplace sign-in, and stores a registry token under `~/.ht-skills/config.json`.
22
+ `login` prints a dedicated `/cli-login` URL, waits for Enter, opens the browser, and stores the approved registry token under `~/.ht-skills/config.json`.
23
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.
24
+ `publish` zips the target skill directory, uploads it through the marketplace package inspection flow, and then submits the generated preview token for review. The default access is `public`; use `--access private` to keep the skill private before review.
package/lib/cli.js CHANGED
@@ -48,7 +48,7 @@ function printHelp() {
48
48
  console.log(`Usage:
49
49
  ht-skills search <query> [--registry <url>] [--limit <n>]
50
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]
51
+ ht-skills publish [skillDir] [--registry <url>] [--access public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
52
52
  ht-skills submit <skillDir> [--registry <url>] [--submitter <name>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
53
53
  ht-skills install <slug[@version]> [more-skills...] [--registry <url>] [--target <dir>] [--tool codex|claude|vscode]
54
54
  ht-skills add [registry] --skill <slug[@version]>,<slug[@version]> [--tool codex|claude|vscode]
@@ -57,6 +57,7 @@ Examples:
57
57
  ht-skills search openai
58
58
  ht-skills login
59
59
  ht-skills publish .
60
+ ht-skills publish . --access private
60
61
  ht-skills submit ./examples/hello-skill
61
62
  ht-skills install hello-skill@1.0.0 --tool codex
62
63
  ht-skills install hello-skill repo-bug-analyze --tool codex
@@ -238,6 +239,32 @@ function summarizePreviewErrors(preview) {
238
239
  return errors.slice(0, 4).join("; ");
239
240
  }
240
241
 
242
+ async function promptToOpenBrowser(url, deps = {}) {
243
+ const ask = deps.ask || null;
244
+ if (ask) {
245
+ await ask(`Press Enter to open the browser for:\n${url}\n`);
246
+ return;
247
+ }
248
+
249
+ const isInteractive = typeof deps.isInteractive === "boolean"
250
+ ? deps.isInteractive
251
+ : Boolean(process.stdin.isTTY && process.stdout.isTTY);
252
+
253
+ if (!isInteractive) {
254
+ return;
255
+ }
256
+
257
+ const rl = readline.createInterface({
258
+ input: process.stdin,
259
+ output: process.stdout,
260
+ });
261
+ try {
262
+ await rl.question(`Press Enter to open the browser for:\n${url}\n`);
263
+ } finally {
264
+ rl.close();
265
+ }
266
+ }
267
+
241
268
  function formatInstallError(error, { slug, version = null, registry, stage = "resolve" }) {
242
269
  const rawMessage = String(error?.message || "Install failed").trim();
243
270
  const status = Number(error?.status || 0);
@@ -431,16 +458,89 @@ function normalizeInlineText(value) {
431
458
  return String(value || "").replace(/\s+/g, " ").trim();
432
459
  }
433
460
 
461
+ const ANSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
462
+
463
+ function stripAnsi(value) {
464
+ return String(value || "").replace(ANSI_PATTERN, "");
465
+ }
466
+
467
+ function isFullwidthCodePoint(codePoint) {
468
+ if (!Number.isFinite(codePoint)) {
469
+ return false;
470
+ }
471
+
472
+ return codePoint >= 0x1100 && (
473
+ codePoint <= 0x115f ||
474
+ codePoint === 0x2329 ||
475
+ codePoint === 0x232a ||
476
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
477
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
478
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
479
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
480
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
481
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
482
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
483
+ (codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
484
+ (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
485
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)
486
+ );
487
+ }
488
+
489
+ function getCharacterWidth(character) {
490
+ const codePoint = character.codePointAt(0);
491
+ if (!Number.isFinite(codePoint)) {
492
+ return 0;
493
+ }
494
+ if (
495
+ codePoint === 0 ||
496
+ (codePoint >= 0x0000 && codePoint <= 0x001f) ||
497
+ (codePoint >= 0x007f && codePoint <= 0x009f)
498
+ ) {
499
+ return 0;
500
+ }
501
+ return isFullwidthCodePoint(codePoint) ? 2 : 1;
502
+ }
503
+
504
+ function getDisplayWidth(value) {
505
+ let width = 0;
506
+ for (const character of stripAnsi(value)) {
507
+ width += getCharacterWidth(character);
508
+ }
509
+ return width;
510
+ }
511
+
512
+ function sliceTextByDisplayWidth(value, maxWidth) {
513
+ const safeWidth = Math.max(1, Number(maxWidth || 0));
514
+ const characters = [...String(value || "")];
515
+ let width = 0;
516
+ let index = 0;
517
+
518
+ while (index < characters.length) {
519
+ const characterWidth = getCharacterWidth(characters[index]);
520
+ if (width + characterWidth > safeWidth) {
521
+ break;
522
+ }
523
+ width += characterWidth;
524
+ index += 1;
525
+ }
526
+
527
+ return {
528
+ slice: characters.slice(0, index).join(""),
529
+ rest: characters.slice(index).join(""),
530
+ width,
531
+ };
532
+ }
533
+
434
534
  function truncateText(value, maxLength) {
435
535
  const text = normalizeInlineText(value);
436
536
  const safeMaxLength = Math.max(1, Number(maxLength || 0));
437
- if (text.length <= safeMaxLength) {
537
+ if (getDisplayWidth(text) <= safeMaxLength) {
438
538
  return text;
439
539
  }
440
540
  if (safeMaxLength <= 1) {
441
541
  return "…";
442
542
  }
443
- return `${text.slice(0, safeMaxLength - 1).trimEnd()}…`;
543
+ return `${sliceTextByDisplayWidth(text, safeMaxLength - 1).slice.trimEnd()}…`;
444
544
  }
445
545
 
446
546
  function wrapText(value, maxWidth) {
@@ -448,37 +548,37 @@ function wrapText(value, maxWidth) {
448
548
  const safeWidth = Math.max(1, Number(maxWidth || 0));
449
549
  if (!text) return [""];
450
550
 
451
- const words = text.split(" ");
452
551
  const lines = [];
453
- let current = "";
552
+ let remaining = text;
454
553
 
455
- for (const word of words) {
456
- if (!current) {
457
- if (word.length <= safeWidth) {
458
- current = word;
459
- continue;
460
- }
461
- lines.push(truncateText(word, safeWidth));
462
- continue;
554
+ while (remaining) {
555
+ if (getDisplayWidth(remaining) <= safeWidth) {
556
+ lines.push(remaining);
557
+ break;
463
558
  }
464
559
 
465
- const candidate = `${current} ${word}`;
466
- if (candidate.length <= safeWidth) {
467
- current = candidate;
468
- continue;
469
- }
560
+ const characters = [...remaining];
561
+ let width = 0;
562
+ let index = 0;
563
+ let lastBreakIndex = -1;
470
564
 
471
- lines.push(current);
472
- if (word.length <= safeWidth) {
473
- current = word;
474
- } else {
475
- lines.push(truncateText(word, safeWidth));
476
- current = "";
565
+ while (index < characters.length) {
566
+ const character = characters[index];
567
+ const characterWidth = getCharacterWidth(character);
568
+ if (width + characterWidth > safeWidth) {
569
+ break;
570
+ }
571
+ width += characterWidth;
572
+ index += 1;
573
+ if (character === " ") {
574
+ lastBreakIndex = index;
575
+ }
477
576
  }
478
- }
479
577
 
480
- if (current) {
481
- lines.push(current);
578
+ const breakIndex = lastBreakIndex > 0 ? lastBreakIndex : Math.max(1, index);
579
+ const line = characters.slice(0, breakIndex).join("").trimEnd();
580
+ lines.push(line);
581
+ remaining = characters.slice(breakIndex).join("").trimStart();
482
582
  }
483
583
 
484
584
  return lines.length ? lines : [""];
@@ -563,7 +663,7 @@ function formatSearchResult(item, { index = 0, colorize = (text) => text, width
563
663
  }
564
664
 
565
665
  const separator = " ";
566
- const availableDescriptionWidth = width - plainPrefix.length - separator.length;
666
+ const availableDescriptionWidth = width - getDisplayWidth(plainPrefix) - getDisplayWidth(separator);
567
667
  if (availableDescriptionWidth < minDescriptionWidth) {
568
668
  return truncateText(`${plainPrefix}${separator}${descriptionPart}`, width);
569
669
  }
@@ -578,25 +678,26 @@ function formatSearchCard(item, { index = 0, colorize = (text) => text, width =
578
678
  const slugPart = `${item.slug}@${item.latestVersion}`;
579
679
  const namePart = normalizeInlineText(item.name || "");
580
680
  const descriptionPart = normalizeInlineText(item.description || "");
581
- const maxInnerWidth = Math.max(42, width - 6);
681
+ const maxInnerWidth = Math.max(16, width - 6);
582
682
  const titleText = `${slugPart}${namePart ? ` (${namePart})` : ""}`;
583
683
  const detailLines = wrapText(descriptionPart || "-", maxInnerWidth);
584
684
  const contentWidth = Math.max(
585
- Math.min(maxInnerWidth, titleText.length),
586
- ...detailLines.map((line) => Math.min(maxInnerWidth, line.length)),
587
- 42,
685
+ 16,
686
+ getDisplayWidth(ordinal) + 1,
687
+ Math.min(maxInnerWidth, getDisplayWidth(titleText)),
688
+ ...detailLines.map((line) => Math.min(maxInnerWidth, getDisplayWidth(line))),
588
689
  );
589
- const titleSuffix = "─".repeat(Math.max(0, contentWidth - ordinal.length - 1));
690
+ const titleSuffix = "─".repeat(Math.max(0, contentWidth - getDisplayWidth(ordinal) - 1));
590
691
  const title = `${colorize("◇", "success")} ${colorize(ordinal, "accent")} ${colorize(titleSuffix, "muted")}╮`;
591
692
  const blank = `│ ${" ".repeat(contentWidth)} │`;
592
693
  const firstLineText = truncateText(titleText, contentWidth);
593
- const firstLine = `│ ${colorize(firstLineText, "accent")}${" ".repeat(Math.max(0, contentWidth - firstLineText.length))} │`;
694
+ const firstLine = `│ ${colorize(firstLineText, "accent")}${" ".repeat(Math.max(0, contentWidth - getDisplayWidth(firstLineText)))} │`;
594
695
  const detailRendered = detailLines.map((line) => {
595
696
  const text = truncateText(line, contentWidth);
596
- return `│ ${text}${" ".repeat(Math.max(0, contentWidth - text.length))} │`;
697
+ return `│ ${text}${" ".repeat(Math.max(0, contentWidth - getDisplayWidth(text)))} │`;
597
698
  });
598
699
  const bottom = `╰${"─".repeat(contentWidth + 2)}╯`;
599
- return [title, blank, firstLine, ...detailRendered, bottom].join("\n");
700
+ return [title, blank, firstLine, ...detailRendered, blank, bottom].join("\n");
600
701
  }
601
702
 
602
703
  function printFallbackIntro({ registry, slug, version, skillName, skillDescription, installTargets }, log = console.log) {
@@ -1178,6 +1279,9 @@ async function cmdLogin(flags, deps = {}) {
1178
1279
  throw new Error("registry did not return a usable CLI login flow");
1179
1280
  }
1180
1281
 
1282
+ log(`Complete CLI sign-in here:\n${browserUrl}`);
1283
+ await promptToOpenBrowser(browserUrl, deps);
1284
+
1181
1285
  try {
1182
1286
  await Promise.resolve(openBrowser(browserUrl));
1183
1287
  log(`Opened browser for ${registry}. Complete sign-in to continue...`);
@@ -1254,12 +1358,11 @@ async function cmdPublish(flags, deps = {}) {
1254
1358
  throw new Error(summarizePreviewErrors(preview));
1255
1359
  }
1256
1360
 
1361
+ const visibility = String(flags.access || flags.visibility || "public").trim().toLowerCase() || "public";
1257
1362
  const body = {
1258
1363
  preview_token: preview.preview_token,
1364
+ visibility,
1259
1365
  };
1260
- if (flags.visibility) {
1261
- body.visibility = String(flags.visibility);
1262
- }
1263
1366
  if (flags["shared-with"]) {
1264
1367
  body.shared_with = String(flags["shared-with"])
1265
1368
  .split(",")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ht-skills",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "CLI for installing and submitting skills from HT Skills Marketplace.",
5
5
  "type": "commonjs",
6
6
  "bin": {