ht-skills 0.2.12 → 0.2.14

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 -2
  2. package/lib/cli.js +81 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -12,9 +12,9 @@ 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>] [--access public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
15
+ ht-skills publish [skillDir] [--registry <url>] [--path-slug <path>] [--skip-user-dir] [--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
- ht-skills submit <skillDir> [--registry <url>] [--submitter <name>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
17
+ ht-skills submit <skillDir> [--registry <url>] [--path-slug <path>] [--skip-user-dir] [--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
  ```
@@ -22,3 +22,7 @@ ht-skills add [registry] --skill <slug[@version]>,<slug[@version]> [--tool codex
22
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
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.
25
+
26
+ `--path-slug` lets you publish under a nested registry path (for example `gcs/abc`) while keeping the skill manifest slug (for example `abc`) unchanged for local install naming.
27
+
28
+ `--skip-user-dir` requests publishing to the top-level path (without the default username prefix). The server only accepts this for admin users.
package/lib/cli.js CHANGED
@@ -50,8 +50,8 @@ function printHelp() {
50
50
  console.log(`Usage:
51
51
  ht-skills search <query> [--registry <url>] [--limit <n>]
52
52
  ht-skills login [--registry <url>]
53
- ht-skills publish [skillDir] [--registry <url>] [--access public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
54
- ht-skills submit <skillDir> [--registry <url>] [--submitter <name>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
53
+ ht-skills publish [skillDir] [--registry <url>] [--path-slug <path>] [--skip-user-dir] [--access public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
54
+ ht-skills submit <skillDir> [--registry <url>] [--path-slug <path>] [--skip-user-dir] [--submitter <name>] [--visibility public|private|shared] [--shared-with a@b.com,c@d.com] [--publish-now]
55
55
  ht-skills install <slug[@version]> [more-skills...] [--registry <url>] [--target <dir>] [--tool codex|claude|vscode]
56
56
  ht-skills add [registry] --skill <slug[@version]>,<slug[@version]> [--tool codex|claude|vscode]
57
57
 
@@ -59,6 +59,8 @@ Examples:
59
59
  ht-skills search openai
60
60
  ht-skills login
61
61
  ht-skills publish .
62
+ ht-skills publish . --path-slug gcs/abc
63
+ ht-skills publish . --skip-user-dir
62
64
  ht-skills publish . --access private
63
65
  ht-skills submit ./examples/hello-skill
64
66
  ht-skills install hello-skill@1.0.0 --tool codex
@@ -98,6 +100,14 @@ function getRegistryUrl(flags) {
98
100
  return String(flags.registry || process.env.SKILLS_REGISTRY_URL || DEFAULT_REGISTRY_URL).replace(/\/$/, "");
99
101
  }
100
102
 
103
+ function isBooleanFlagEnabled(value) {
104
+ if (value === true) return true;
105
+ if (value === false || value == null) return false;
106
+ const normalized = String(value).trim().toLowerCase();
107
+ if (!normalized) return false;
108
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
109
+ }
110
+
101
111
  async function requestJson(url, options = {}) {
102
112
  const res = await fetch(url, options);
103
113
  const text = await res.text();
@@ -441,6 +451,15 @@ function parseSpec(spec) {
441
451
  return { slug, version: version || null };
442
452
  }
443
453
 
454
+ function encodeSkillPathForUrl(pathValue) {
455
+ return String(pathValue || "")
456
+ .split("/")
457
+ .map((segment) => segment.trim())
458
+ .filter(Boolean)
459
+ .map((segment) => encodeURIComponent(segment))
460
+ .join("/");
461
+ }
462
+
444
463
  function normalizeSkillSpecs(rawSpecs, { source = "install" } = {}) {
445
464
  const values = Array.isArray(rawSpecs) ? rawSpecs : [rawSpecs];
446
465
  const result = [];
@@ -952,7 +971,14 @@ function printFallbackSearchIntro({ registry, query, limit }, log = console.log)
952
971
  log("");
953
972
  }
954
973
 
955
- function printFallbackPublishIntro({ registry, skillDir, archiveName, visibility }, log = console.log) {
974
+ function printFallbackPublishIntro({
975
+ registry,
976
+ skillDir,
977
+ archiveName,
978
+ visibility,
979
+ pathSlug = null,
980
+ skipUserDir = false,
981
+ }, log = console.log) {
956
982
  log("");
957
983
  log(renderGradientBanner());
958
984
  log("");
@@ -962,6 +988,12 @@ function printFallbackPublishIntro({ registry, skillDir, archiveName, visibility
962
988
  log(`Directory: ${skillDir}`);
963
989
  log(`Archive: ${archiveName}`);
964
990
  log(`Access: ${visibility}`);
991
+ if (pathSlug) {
992
+ log(`Path Slug: ${pathSlug}`);
993
+ }
994
+ if (skipUserDir) {
995
+ log("Skip User Dir: true");
996
+ }
965
997
  log("");
966
998
  }
967
999
 
@@ -1218,12 +1250,12 @@ async function resolveInteractiveToolIds(flags, deps, specs) {
1218
1250
  }
1219
1251
 
1220
1252
  const skillLabel = specs.length === 1
1221
- ? `${metadata?.manifest?.name || primarySpec.slug} (${primarySpec.slug}@${version})`
1253
+ ? `${metadata?.manifest?.name || primarySpec.slug} (${(metadata?.manifest?.slug || primarySpec.slug)}@${version})`
1222
1254
  : `${specs.length} skills`;
1223
1255
  const skillDescription = specs.length === 1
1224
1256
  ? (metadata?.manifest?.description || "")
1225
1257
  : specs.join(", ");
1226
- const targetSlug = specs.length === 1 ? primarySpec.slug : "<skill-slug>";
1258
+ const targetSlug = specs.length === 1 ? (metadata?.manifest?.slug || primarySpec.slug) : "<skill-slug>";
1227
1259
  const availableTargets = getAvailableInstallTargets(targetSlug, { homeDir });
1228
1260
 
1229
1261
  if (availableTargets.length > 0 && ui) {
@@ -1238,9 +1270,9 @@ async function resolveInteractiveToolIds(flags, deps, specs) {
1238
1270
  } else if (availableTargets.length > 0) {
1239
1271
  printFallbackIntro({
1240
1272
  registry,
1241
- slug: specs.length === 1 ? primarySpec.slug : `${specs.length} skills`,
1273
+ slug: specs.length === 1 ? (metadata?.manifest?.slug || primarySpec.slug) : `${specs.length} skills`,
1242
1274
  version: specs.length === 1 ? version : "mixed",
1243
- skillName: specs.length === 1 ? (metadata?.manifest?.name || primarySpec.slug) : `${specs.length} skills`,
1275
+ skillName: specs.length === 1 ? (metadata?.manifest?.name || metadata?.manifest?.slug || primarySpec.slug) : `${specs.length} skills`,
1244
1276
  skillDescription,
1245
1277
  installTargets: availableTargets,
1246
1278
  }, log);
@@ -1313,7 +1345,6 @@ async function installOne(flags, deps = {}) {
1313
1345
  }
1314
1346
 
1315
1347
  let toolIds = selectedToolIds.length > 0 ? selectedToolIds : normalizeToolIds(flags.tool);
1316
- const renderInstallLine = deps.renderInstallLine || ((target) => `installed ${parsed.slug}@${version} -> ${target.target}`);
1317
1348
  let metadata = null;
1318
1349
 
1319
1350
  try {
@@ -1333,15 +1364,17 @@ async function installOne(flags, deps = {}) {
1333
1364
  }
1334
1365
 
1335
1366
  const skillName = metadata?.manifest?.name || parsed.slug;
1367
+ const installSlug = metadata?.manifest?.slug || parsed.slug;
1336
1368
  const skillDescription = metadata?.manifest?.description || "";
1369
+ const renderInstallLine = deps.renderInstallLine || ((target) => `installed ${installSlug}@${version} -> ${target.target}`);
1337
1370
 
1338
1371
  if (toolIds.length === 0 && !flags.target && isInteractive) {
1339
- const availableTargets = getAvailableInstallTargets(parsed.slug, { homeDir });
1372
+ const availableTargets = getAvailableInstallTargets(installSlug, { homeDir });
1340
1373
  if (availableTargets.length > 0 && ui) {
1341
1374
  ui.note(
1342
1375
  [
1343
1376
  `${colorize("Source", "muted")}: ${registry}`,
1344
- `${colorize("Skill", "muted")}: ${colorize(skillName, "accent")} ${colorize(`(${parsed.slug}@${version})`, "muted")}`,
1377
+ `${colorize("Skill", "muted")}: ${colorize(skillName, "accent")} ${colorize(`(${installSlug}@${version})`, "muted")}`,
1345
1378
  skillDescription ? `${colorize("Summary", "muted")}: ${skillDescription}` : "",
1346
1379
  ].filter(Boolean).join("\n"),
1347
1380
  "Install plan",
@@ -1349,7 +1382,7 @@ async function installOne(flags, deps = {}) {
1349
1382
  } else if (availableTargets.length > 0) {
1350
1383
  printFallbackIntro({
1351
1384
  registry,
1352
- slug: parsed.slug,
1385
+ slug: installSlug,
1353
1386
  version,
1354
1387
  skillName,
1355
1388
  skillDescription,
@@ -1379,7 +1412,7 @@ async function installOne(flags, deps = {}) {
1379
1412
  const installTargets = resolveInstallTargets({
1380
1413
  toolIds,
1381
1414
  target: flags.target,
1382
- slug: parsed.slug,
1415
+ slug: installSlug,
1383
1416
  version,
1384
1417
  homeDir,
1385
1418
  });
@@ -1387,7 +1420,7 @@ async function installOne(flags, deps = {}) {
1387
1420
  if (!ui) {
1388
1421
  printFallbackIntro({
1389
1422
  registry,
1390
- slug: parsed.slug,
1423
+ slug: installSlug,
1391
1424
  version,
1392
1425
  skillName,
1393
1426
  skillDescription,
@@ -1428,7 +1461,8 @@ async function installOne(flags, deps = {}) {
1428
1461
  metadataPath,
1429
1462
  `${JSON.stringify(
1430
1463
  {
1431
- slug: parsed.slug,
1464
+ slug: installSlug,
1465
+ requestedSlug: parsed.slug,
1432
1466
  version,
1433
1467
  installedAt: new Date().toISOString(),
1434
1468
  registry,
@@ -1453,9 +1487,9 @@ async function installOne(flags, deps = {}) {
1453
1487
  if (installTargets.length > 0) {
1454
1488
  if (ui) {
1455
1489
  ui.note(formatTargetsNote(installTargets, colorize), "Installed to");
1456
- ui.outro(`Installed ${ui.pc.cyan(parsed.slug)} to ${installTargets.length} target${installTargets.length === 1 ? "" : "s"}.`);
1490
+ ui.outro(`Installed ${ui.pc.cyan(installSlug)} to ${installTargets.length} target${installTargets.length === 1 ? "" : "s"}.`);
1457
1491
  } else {
1458
- log(`Installed ${parsed.slug} to ${formatCount(installTargets.length, "target")}`);
1492
+ log(`Installed ${installSlug} to ${formatCount(installTargets.length, "target")}`);
1459
1493
  }
1460
1494
  }
1461
1495
 
@@ -1557,6 +1591,11 @@ async function cmdPublish(flags, deps = {}) {
1557
1591
  const log = deps.log || ((message) => console.log(message));
1558
1592
  const registry = getRegistryUrl(flags);
1559
1593
  const skillDir = path.resolve(flags._[0] || ".");
1594
+ const pathSlug = String(flags["path-slug"] || "").trim() || null;
1595
+ const skipUserDir = isBooleanFlagEnabled(flags["skip-user-dir"]);
1596
+ if (pathSlug && skipUserDir) {
1597
+ throw new Error("--path-slug and --skip-user-dir cannot be used together");
1598
+ }
1560
1599
  const visibility = String(flags.access || flags.visibility || "public").trim().toLowerCase() || "public";
1561
1600
  const archiveName = `${path.basename(skillDir) || "skill"}.zip`;
1562
1601
  const isInteractive = isInteractiveSession(deps);
@@ -1610,12 +1649,14 @@ async function cmdPublish(flags, deps = {}) {
1610
1649
  `${colorize("Directory", "muted")}: ${skillDir}`,
1611
1650
  `${colorize("Archive", "muted")}: ${archiveName}`,
1612
1651
  `${colorize("Access", "muted")}: ${visibility}`,
1652
+ pathSlug ? `${colorize("Path Slug", "muted")}: ${pathSlug}` : "",
1653
+ skipUserDir ? `${colorize("Skip User Dir", "muted")}: true` : "",
1613
1654
  ].join("\n"),
1614
1655
  "Publish",
1615
1656
  );
1616
1657
  renderPublishFlow();
1617
1658
  } else {
1618
- printFallbackPublishIntro({ registry, skillDir, archiveName, visibility }, log);
1659
+ printFallbackPublishIntro({ registry, skillDir, archiveName, visibility, pathSlug, skipUserDir }, log);
1619
1660
  }
1620
1661
 
1621
1662
  setFlowMessage(`Checking login for ${registry}`);
@@ -1761,6 +1802,12 @@ async function cmdPublish(flags, deps = {}) {
1761
1802
  preview_token: preview.preview_token,
1762
1803
  visibility,
1763
1804
  };
1805
+ if (pathSlug) {
1806
+ body.path_slug = pathSlug;
1807
+ }
1808
+ if (skipUserDir) {
1809
+ body.skip_user_dir = true;
1810
+ }
1764
1811
  if (flags["shared-with"]) {
1765
1812
  body.shared_with = String(flags["shared-with"])
1766
1813
  .split(",")
@@ -1805,16 +1852,19 @@ async function cmdPublish(flags, deps = {}) {
1805
1852
  preview_token: preview.preview_token,
1806
1853
  archive_name: archiveName,
1807
1854
  };
1808
- const skillSlug = String(
1809
- result.publication?.slug
1855
+ const skillPath = String(
1856
+ result.publication?.pathSlug
1857
+ || result.publication?.slug
1810
1858
  || result.publication?.manifest?.slug
1811
1859
  || preview.manifest?.slug
1812
1860
  || "",
1813
1861
  ).trim();
1814
- const skillUrl = skillSlug ? `${registry}/skills/${encodeURIComponent(skillSlug)}` : null;
1862
+ const encodedSkillPath = encodeSkillPathForUrl(skillPath);
1863
+ const skillUrl = encodedSkillPath ? `${registry}/skills/${encodedSkillPath}` : null;
1815
1864
  summaryPayload.skill_url = skillUrl;
1816
1865
 
1817
1866
  if (ui) {
1867
+ flowRenderer?.clear();
1818
1868
  ui.note(
1819
1869
  [
1820
1870
  `${colorize("Status", "muted")}: ${result.status || "pending"}`,
@@ -1825,7 +1875,6 @@ async function cmdPublish(flags, deps = {}) {
1825
1875
  ].join("\n"),
1826
1876
  "Published",
1827
1877
  );
1828
- setFlowMessage(`Submitted ${archiveName} for review`);
1829
1878
  ui.outro(skillUrl
1830
1879
  ? `Submitted ${ui.pc.cyan(archiveName)} for review. Skill URL: ${skillUrl}`
1831
1880
  : `Submitted ${ui.pc.cyan(archiveName)} for review.`);
@@ -1865,6 +1914,17 @@ async function cmdSubmit(flags, deps = {}) {
1865
1914
  files,
1866
1915
  submitter: String(flags.submitter || process.env.USERNAME || os.userInfo().username || "anonymous"),
1867
1916
  };
1917
+ const pathSlug = String(flags["path-slug"] || "").trim();
1918
+ const skipUserDir = isBooleanFlagEnabled(flags["skip-user-dir"]);
1919
+ if (pathSlug && skipUserDir) {
1920
+ throw new Error("--path-slug and --skip-user-dir cannot be used together");
1921
+ }
1922
+ if (pathSlug) {
1923
+ body.path_slug = pathSlug;
1924
+ }
1925
+ if (skipUserDir) {
1926
+ body.skip_user_dir = true;
1927
+ }
1868
1928
 
1869
1929
  if (flags.visibility) {
1870
1930
  body.visibility = String(flags.visibility);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ht-skills",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "CLI for installing and submitting skills from HT Skills Marketplace.",
5
5
  "type": "commonjs",
6
6
  "bin": {