hubskillz 0.3.1 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +69 -0
  2. package/dist/index.js +1180 -486
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2836,9 +2836,9 @@ var asciiTabOrNewline = /[\t\n\r]/g;
2836
2836
  function stripTabAndNewline(value) {
2837
2837
  return value.replace(asciiTabOrNewline, "");
2838
2838
  }
2839
- function urlHostnameOk(url2, hostname5) {
2840
- hostname5.lastIndex = 0;
2841
- return hostname5.test(url2.hostname);
2839
+ function urlHostnameOk(url2, hostname6) {
2840
+ hostname6.lastIndex = 0;
2841
+ return hostname6.test(url2.hostname);
2842
2842
  }
2843
2843
  function urlProtocolOk(url2, protocol) {
2844
2844
  protocol.lastIndex = 0;
@@ -18899,6 +18899,10 @@ var skillStateSchema = external_exports.enum([
18899
18899
  "drifted",
18900
18900
  "customized",
18901
18901
  "missing",
18902
+ // Absent from a project surface but installed in the machine's global root:
18903
+ // Claude Code loads ~/.claude/skills in every project, so nothing is missing
18904
+ // and nothing must be written. The global surface carries the real state.
18905
+ "inherited",
18902
18906
  "unmanaged"
18903
18907
  ]);
18904
18908
  var surfaceKindSchema = external_exports.literal("claude-code-local");
@@ -18918,6 +18922,7 @@ var inventoryFileSchema = external_exports.object({
18918
18922
  size: external_exports.number().int().nonnegative(),
18919
18923
  content: external_exports.string().max(MAX_FILE_CONTENT_CHARS).optional()
18920
18924
  });
18925
+ var SKILL_MD = "SKILL.md";
18921
18926
  var skillFileSchema = external_exports.object({
18922
18927
  path: skillFilePathSchema,
18923
18928
  content: external_exports.string().max(MAX_FILE_CONTENT_CHARS)
@@ -18950,7 +18955,9 @@ var surfaceDescriptorSchema = external_exports.object({
18950
18955
  kind: surfaceKindSchema,
18951
18956
  label: external_exports.string().min(1).max(200),
18952
18957
  machineId: external_exports.string().min(1).max(200),
18953
- path: external_exports.string().min(1).max(1e3)
18958
+ path: external_exports.string().min(1).max(1e3),
18959
+ /** The global root (~/.claude/skills) or a project. Absent on old CLIs. */
18960
+ scope: external_exports.enum(["global", "project"]).optional()
18954
18961
  });
18955
18962
  var inventoryRequestSchema = external_exports.object({
18956
18963
  surface: surfaceDescriptorSchema,
@@ -19015,6 +19022,14 @@ var draftResponseSchema = external_exports.object({
19015
19022
  skillId: external_exports.string(),
19016
19023
  versionId: external_exports.string()
19017
19024
  });
19025
+ var publishRequestSchema = external_exports.object({
19026
+ name: skillNameSchema,
19027
+ published: external_exports.boolean()
19028
+ });
19029
+ var publishResponseSchema = external_exports.object({
19030
+ ok: external_exports.literal(true),
19031
+ handle: external_exports.string()
19032
+ });
19018
19033
  var pendingQuerySchema = external_exports.object({ surfaceId: external_exports.string().min(1) });
19019
19034
  var pendingResponseSchema = external_exports.object({
19020
19035
  requests: external_exports.array(
@@ -19084,6 +19099,76 @@ var paginationQuerySchema = external_exports.object({
19084
19099
  pageSize: external_exports.coerce.number().int().min(1).max(100).default(25)
19085
19100
  });
19086
19101
 
19102
+ // ../shared/src/directory/upstream.ts
19103
+ function shortSha(value, length = 7) {
19104
+ return value === void 0 ? "-" : value.slice(0, length);
19105
+ }
19106
+
19107
+ // ../shared/src/directory/skill-md.ts
19108
+ var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/;
19109
+ var BLOCK_SCALAR = /^([|>])[+-]?$/;
19110
+ function indentOf(line) {
19111
+ return line.length - line.trimStart().length;
19112
+ }
19113
+ function parseSkillMd(source) {
19114
+ const match = FRONTMATTER.exec(source);
19115
+ if (match === null) return { frontmatter: [], body: source };
19116
+ const frontmatter = [];
19117
+ const lines = (match[1] ?? "").split("\n");
19118
+ for (let at = 0; at < lines.length; at += 1) {
19119
+ const line = lines[at] ?? "";
19120
+ const separator = line.indexOf(":");
19121
+ if (separator <= 0) continue;
19122
+ const key = line.slice(0, separator).trim();
19123
+ let value = line.slice(separator + 1).trim();
19124
+ const block = BLOCK_SCALAR.exec(value);
19125
+ if (block !== null) {
19126
+ const indent = indentOf(line);
19127
+ const collected = [];
19128
+ while (at + 1 < lines.length) {
19129
+ const next = lines[at + 1] ?? "";
19130
+ if (next.trim() !== "" && indentOf(next) <= indent) break;
19131
+ collected.push(next);
19132
+ at += 1;
19133
+ }
19134
+ const filled = collected.filter((entry) => entry.trim() !== "");
19135
+ const common = Math.min(...filled.map(indentOf));
19136
+ const stripped = filled.map((entry) => entry.slice(common).trimEnd());
19137
+ value = stripped.join(block[1] === "|" ? "\n" : " ").trim();
19138
+ }
19139
+ frontmatter.push({ key, value });
19140
+ }
19141
+ return {
19142
+ frontmatter,
19143
+ body: source.slice(match[0].length).replace(/^(\r?\n)+/, "")
19144
+ };
19145
+ }
19146
+ var QUOTED = /^(["'])([\s\S]*)\1$/;
19147
+ var RELEASE = /^v?\d[\w.+-]{0,23}$/;
19148
+ function declaredRelease(source) {
19149
+ const entry = parseSkillMd(source).frontmatter.find(
19150
+ (item) => item.key === "version"
19151
+ );
19152
+ const value = (entry?.value ?? "").replace(QUOTED, "$2").trim();
19153
+ return RELEASE.test(value) ? value : null;
19154
+ }
19155
+ function releaseOf(files) {
19156
+ const skillMd = files.find((file2) => file2.path === "SKILL.md");
19157
+ return skillMd === void 0 ? null : declaredRelease(skillMd.content);
19158
+ }
19159
+
19160
+ // src/commands/doctor.ts
19161
+ import { existsSync as existsSync2 } from "node:fs";
19162
+ import { readdir as readdir2 } from "node:fs/promises";
19163
+ import { hostname as hostname5 } from "node:os";
19164
+ import { basename as basename2, dirname as dirname2, join as join4 } from "node:path";
19165
+
19166
+ // src/config.ts
19167
+ import { randomUUID } from "node:crypto";
19168
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
19169
+ import { homedir, hostname as hostname3 } from "node:os";
19170
+ import { join } from "node:path";
19171
+
19087
19172
  // src/errors.ts
19088
19173
  var CliError = class extends DomainError {
19089
19174
  constructor(code, message) {
@@ -19099,75 +19184,7 @@ function toCliError(cause) {
19099
19184
  return new CliError("UNEXPECTED", String(cause));
19100
19185
  }
19101
19186
 
19102
- // src/api.ts
19103
- async function apiRequest(request) {
19104
- const url2 = `${request.session.baseUrl}${request.path}`;
19105
- const bearer = `Bearer ${request.session.token}`;
19106
- let response;
19107
- try {
19108
- response = await fetch(url2, {
19109
- method: request.method,
19110
- headers: request.body === void 0 ? { accept: "application/json", authorization: bearer } : {
19111
- accept: "application/json",
19112
- authorization: bearer,
19113
- "content-type": "application/json"
19114
- },
19115
- body: request.body === void 0 ? void 0 : JSON.stringify(request.body)
19116
- });
19117
- } catch (cause) {
19118
- const detail = cause instanceof Error ? cause.message : String(cause);
19119
- return Result.fail(
19120
- new CliError("NETWORK", `Cannot reach ${url2}: ${detail}`)
19121
- );
19122
- }
19123
- const text = await response.text();
19124
- if (response.status === 413) {
19125
- return Result.fail(
19126
- new CliError(
19127
- "HTTP",
19128
- `${request.method} ${request.path} failed: the inventory is too large (HTTP 413). Reduce the number of skills or roots, or use project roots.`
19129
- )
19130
- );
19131
- }
19132
- if (!response.ok) {
19133
- const message = isJson(text) ? decodeBody(text, apiErrorSchema)?.message ?? `${response.status} ${response.statusText}` : `The server returned HTTP ${response.status} (not JSON). Try again in a minute.`;
19134
- return Result.fail(
19135
- new CliError(
19136
- response.status === 401 ? "UNAUTHORIZED" : response.status === 403 ? "FORBIDDEN" : "HTTP",
19137
- `${request.method} ${request.path} failed: ${message}`
19138
- )
19139
- );
19140
- }
19141
- const parsed = decodeBody(text, request.schema);
19142
- if (parsed === null) {
19143
- return Result.fail(
19144
- new CliError(
19145
- "PROTOCOL",
19146
- `${request.method} ${request.path} returned an unexpected payload (HTTP ${response.status}, not JSON or wrong shape).`
19147
- )
19148
- );
19149
- }
19150
- return Result.ok(parsed);
19151
- }
19152
- function isJson(text) {
19153
- try {
19154
- JSON.parse(text === "" ? "null" : text);
19155
- return true;
19156
- } catch {
19157
- return false;
19158
- }
19159
- }
19160
- function decodeBody(text, schema) {
19161
- if (!isJson(text)) return null;
19162
- const parsed = schema.safeParse(JSON.parse(text === "" ? "null" : text));
19163
- return parsed.success ? parsed.data : null;
19164
- }
19165
-
19166
19187
  // src/config.ts
19167
- import { randomUUID } from "node:crypto";
19168
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
19169
- import { homedir, hostname as hostname3 } from "node:os";
19170
- import { join } from "node:path";
19171
19188
  var DEFAULT_BASE_URL = "https://api.hubskillz.com";
19172
19189
  var configSchema = external_exports.object({
19173
19190
  baseUrl: external_exports.string().min(1),
@@ -19239,6 +19256,9 @@ async function machineId() {
19239
19256
  const existing = await readConfig();
19240
19257
  return existing.isSuccess ? existing.value.machineId : randomUUID();
19241
19258
  }
19259
+ function webOrigin(baseUrl) {
19260
+ return baseUrl === DEFAULT_BASE_URL ? "https://hubskillz.com" : baseUrl;
19261
+ }
19242
19262
  function resolveBaseUrl(flag, fromConfig) {
19243
19263
  const env = process.env["HUBSKILLZ_BASE_URL"];
19244
19264
  const chosen = flag ?? env ?? fromConfig ?? DEFAULT_BASE_URL;
@@ -19246,22 +19266,24 @@ function resolveBaseUrl(flag, fromConfig) {
19246
19266
  }
19247
19267
 
19248
19268
  // src/output.ts
19249
- var CSI = "\x1B[";
19250
- var ANSI_PATTERN = new RegExp(`${CSI.replace("[", "\\[")}[0-9;]*m`, "gu");
19251
- function colorEnabled() {
19252
- return process.env["NO_COLOR"] === void 0 && process.stdout.isTTY === true;
19253
- }
19254
- function wrap(code, text) {
19255
- return colorEnabled() ? `${CSI}${code}m${text}${CSI}0m` : text;
19256
- }
19269
+ import { homedir as homedir2 } from "node:os";
19270
+ import { styleText } from "node:util";
19271
+ var ANSI_PATTERN = /\u001B\[[0-9;]*m/gu;
19257
19272
  function bold(text) {
19258
- return wrap("1", text);
19273
+ return styleText("bold", text);
19259
19274
  }
19260
19275
  function dim(text) {
19261
- return wrap("2", text);
19276
+ return styleText("dim", text);
19262
19277
  }
19263
19278
  function accent(text) {
19264
- return wrap("33", text);
19279
+ return styleText("yellow", text);
19280
+ }
19281
+ function shortPath(path) {
19282
+ const home2 = homedir2();
19283
+ return path === home2 || path.startsWith(home2 + "/") ? `~${path.slice(home2.length)}` : path;
19284
+ }
19285
+ function plural(count, noun) {
19286
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
19265
19287
  }
19266
19288
  function table(headers, rows) {
19267
19289
  const widths = headers.map(
@@ -19278,100 +19300,607 @@ function table(headers, rows) {
19278
19300
  function visibleLength(text) {
19279
19301
  return text.replace(ANSI_PATTERN, "").length;
19280
19302
  }
19281
- function shortHash(hash2) {
19282
- return hash2 === void 0 ? "-" : hash2.slice(0, 8);
19283
- }
19284
19303
 
19285
- // src/prompt.ts
19286
- import { createInterface } from "node:readline/promises";
19287
- var ENTER = /* @__PURE__ */ new Set(["\r", "\n"]);
19288
- var BACKSPACE = /* @__PURE__ */ new Set(["\b", "\x7F"]);
19289
- var CTRL_C = "";
19290
- async function promptSecret(label) {
19291
- const input2 = process.stdin;
19292
- if (input2.isTTY !== true) {
19293
- return (await readAll(input2)).split("\n")[0]?.trim() ?? "";
19294
- }
19295
- process.stdout.write(label);
19296
- input2.setRawMode(true);
19297
- input2.resume();
19298
- input2.setEncoding("utf8");
19299
- return new Promise((settle, fail) => {
19300
- let value = "";
19301
- const cleanup = () => {
19302
- input2.setRawMode(false);
19303
- input2.pause();
19304
- input2.off("data", onData);
19305
- process.stdout.write("\n");
19306
- };
19307
- const onData = (chunk) => {
19308
- for (const char of chunk) {
19309
- if (ENTER.has(char)) {
19310
- cleanup();
19311
- settle(value.trim());
19312
- return;
19313
- }
19314
- if (char === CTRL_C) {
19315
- cleanup();
19316
- fail(new Error("Interrupted"));
19317
- return;
19318
- }
19319
- value = BACKSPACE.has(char) ? value.slice(0, -1) : value + char;
19320
- }
19321
- };
19322
- input2.on("data", onData);
19323
- });
19304
+ // src/scan.ts
19305
+ import { createHash as createHash2 } from "node:crypto";
19306
+ import { existsSync, statSync } from "node:fs";
19307
+ import { readdir, readFile as readFile3, realpath } from "node:fs/promises";
19308
+ import { homedir as homedir4, hostname as hostname4 } from "node:os";
19309
+ import { basename, dirname, join as join3, resolve as resolve2 } from "node:path";
19310
+
19311
+ // src/lock.ts
19312
+ import { readFile as readFile2 } from "node:fs/promises";
19313
+ import { homedir as homedir3 } from "node:os";
19314
+ import { join as join2, resolve } from "node:path";
19315
+ var entrySchema = external_exports.object({
19316
+ source: external_exports.string().min(1),
19317
+ sourceType: external_exports.string(),
19318
+ skillPath: external_exports.string().min(1).optional(),
19319
+ skillFolderHash: external_exports.string().min(1).optional(),
19320
+ computedHash: external_exports.string().min(1).optional()
19321
+ });
19322
+ var lockSchema = external_exports.object({ skills: external_exports.record(external_exports.string(), external_exports.unknown()) });
19323
+ function globalLockPath() {
19324
+ return join2(homedir3(), ".agents", ".skill-lock.json");
19324
19325
  }
19325
- async function confirm(question) {
19326
- if (process.stdin.isTTY !== true) return false;
19327
- const rl = createInterface({ input: process.stdin, output: process.stdout });
19326
+ function projectLockPath(projectDir) {
19327
+ return join2(resolve(projectDir), "skills-lock.json");
19328
+ }
19329
+ async function readLock(path) {
19330
+ const map2 = /* @__PURE__ */ new Map();
19331
+ const raw = await readFile2(path, "utf8").catch(() => null);
19332
+ if (raw === null) return map2;
19333
+ let json2;
19328
19334
  try {
19329
- const answer = await rl.question(`${question} [y/N] `);
19330
- return answer.trim().toLowerCase() === "y";
19331
- } finally {
19332
- rl.close();
19335
+ json2 = JSON.parse(raw);
19336
+ } catch {
19337
+ return map2;
19333
19338
  }
19334
- }
19335
- async function readAll(stream) {
19336
- const chunks = [];
19337
- for await (const chunk of stream) {
19338
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
19339
+ const lock = lockSchema.safeParse(json2);
19340
+ if (!lock.success) return map2;
19341
+ for (const [name, value] of Object.entries(lock.data.skills)) {
19342
+ const entry = entrySchema.safeParse(value);
19343
+ if (!entry.success) continue;
19344
+ const { source, sourceType, skillPath } = entry.data;
19345
+ const hash2 = entry.data.skillFolderHash ?? entry.data.computedHash;
19346
+ if (sourceType !== "github" || skillPath === void 0) continue;
19347
+ if (hash2 === void 0) continue;
19348
+ map2.set(name, { source, skillPath, hash: hash2 });
19339
19349
  }
19340
- return Buffer.concat(chunks).toString("utf8");
19350
+ return map2;
19341
19351
  }
19342
- function parseSelection(answer, count) {
19343
- const text = answer.trim().toLowerCase();
19344
- if (text === "" || text === "all" || text === "a") {
19345
- return Array.from({ length: count }, (_, index) => index);
19346
- }
19347
- if (text === "none" || text === "n") return [];
19348
- const picked = /* @__PURE__ */ new Set();
19349
- for (const token of text.split(/[\s,]+/u)) {
19350
- const index = Number(token) - 1;
19351
- if (Number.isInteger(index) && index >= 0 && index < count) {
19352
- picked.add(index);
19353
- }
19352
+
19353
+ // src/scan.ts
19354
+ function globalSkillsRoot() {
19355
+ return join3(homedir4(), ".claude", "skills");
19356
+ }
19357
+ function projectSkillsRoot(dir) {
19358
+ return join3(resolve2(dir), ".claude", "skills");
19359
+ }
19360
+ function globalSurfaceLabel() {
19361
+ return hostname4();
19362
+ }
19363
+ function projectSurfaceLabel(dir) {
19364
+ return `${basename(resolve2(dir))} (${hostname4()})`;
19365
+ }
19366
+ function exists(path) {
19367
+ return existsSync(path);
19368
+ }
19369
+ function lockPathFor(root) {
19370
+ const resolved = resolve2(root);
19371
+ if (resolved === globalSkillsRoot()) return globalLockPath();
19372
+ return projectLockPath(dirname(dirname(resolved)));
19373
+ }
19374
+ async function scanSurface(root, label, machine, scope = "project") {
19375
+ const lock = await readLock(lockPathFor(root));
19376
+ const skills = await scanSkills(root);
19377
+ return {
19378
+ descriptor: {
19379
+ kind: "claude-code-local",
19380
+ label,
19381
+ machineId: machine,
19382
+ path: resolve2(root),
19383
+ scope
19384
+ },
19385
+ skills: await Promise.all(skills.map((skill) => withUpstream(skill, lock)))
19386
+ };
19387
+ }
19388
+ async function withUpstream(skill, lock) {
19389
+ if (lock.size === 0) return skill;
19390
+ const names = [skill.name];
19391
+ if (skill.link) {
19392
+ const real = await realpath(skill.dir).catch(() => skill.dir);
19393
+ names.push(basename(real));
19354
19394
  }
19355
- return [...picked].sort((left, right) => left - right);
19395
+ const upstream = names.map((name) => lock.get(name)).find(Boolean);
19396
+ return upstream === void 0 ? skill : { ...skill, upstream };
19356
19397
  }
19357
- async function selectMany(question, choices) {
19358
- if (process.stdin.isTTY !== true) return choices;
19359
- process.stdout.write(`${question}
19360
- `);
19361
- choices.forEach((choice, index) => {
19362
- process.stdout.write(` ${index + 1}. ${choice}
19363
- `);
19364
- });
19365
- const rl = createInterface({ input: process.stdin, output: process.stdout });
19398
+ async function scanSkills(root) {
19399
+ let entries;
19366
19400
  try {
19367
- const answer = await rl.question("Select [all]: ");
19368
- const picked = new Set(parseSelection(answer, choices.length));
19369
- return choices.filter((_, index) => picked.has(index));
19370
- } finally {
19371
- rl.close();
19401
+ entries = await readdir(root, { withFileTypes: true });
19402
+ } catch {
19403
+ return [];
19404
+ }
19405
+ const skills = [];
19406
+ for (const entry of entries) {
19407
+ if (!isSkillFile(entry.name)) continue;
19408
+ const dir = join3(root, entry.name);
19409
+ const symlink = entry.isSymbolicLink();
19410
+ if (!entry.isDirectory() && !(symlink && isDir(dir))) continue;
19411
+ const skill = await scanSkillDir(dir);
19412
+ if (skill === null) continue;
19413
+ skills.push(symlink ? { ...skill, link: true } : skill);
19372
19414
  }
19415
+ return skills.sort((a, b) => a.name < b.name ? -1 : 1);
19373
19416
  }
19374
-
19417
+ async function scanSkillDir(dir) {
19418
+ const resolved = resolve2(dir);
19419
+ const walked = await walk(resolved, "", /* @__PURE__ */ new Set());
19420
+ if (walked.files.length === 0) return null;
19421
+ return {
19422
+ name: basename(resolved),
19423
+ dir: resolved,
19424
+ files: walked.files,
19425
+ contentHash: contentHash(walked.files),
19426
+ link: walked.link
19427
+ };
19428
+ }
19429
+ async function walk(dir, prefix, visited) {
19430
+ const real = await realpath(dir).catch(() => dir);
19431
+ if (visited.has(real)) return { files: [], link: false };
19432
+ visited.add(real);
19433
+ const files = [];
19434
+ let link = false;
19435
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
19436
+ for (const entry of entries) {
19437
+ const full = join3(dir, entry.name);
19438
+ const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
19439
+ if (!isSkillFile(relative)) continue;
19440
+ const symlink = entry.isSymbolicLink();
19441
+ if (entry.isDirectory() || symlink && isDir(full)) {
19442
+ const nested = await walk(full, relative, visited);
19443
+ files.push(...nested.files);
19444
+ link = link || symlink || nested.link;
19445
+ continue;
19446
+ }
19447
+ if (!entry.isFile() && !symlink) continue;
19448
+ const content = await readFile3(full, "utf8").catch(() => null);
19449
+ if (content === null || content.includes("\0") || content.length > MAX_FILE_CONTENT_CHARS) {
19450
+ continue;
19451
+ }
19452
+ files.push({
19453
+ path: relative,
19454
+ content,
19455
+ hash: createHash2("sha256").update(content).digest("hex"),
19456
+ size: Buffer.byteLength(content, "utf8")
19457
+ });
19458
+ }
19459
+ return { files: files.sort((a, b) => a.path < b.path ? -1 : 1), link };
19460
+ }
19461
+ function isDir(path) {
19462
+ try {
19463
+ return statSync(path, { throwIfNoEntry: false })?.isDirectory() === true;
19464
+ } catch {
19465
+ return false;
19466
+ }
19467
+ }
19468
+ function inventoryRequestOf(surface) {
19469
+ return {
19470
+ surface: surface.descriptor,
19471
+ skills: surface.skills.map(inventorySkillOf)
19472
+ };
19473
+ }
19474
+ function inventoryChunksOf(surface) {
19475
+ const groups = [];
19476
+ let group2 = [];
19477
+ let bytes = 0;
19478
+ for (const skill of surface.skills) {
19479
+ const item = inventorySkillOf(skill);
19480
+ const size = Buffer.byteLength(JSON.stringify(item), "utf8");
19481
+ if (group2.length > 0 && (bytes + size > MAX_INVENTORY_CHUNK_BYTES || group2.length >= MAX_SKILLS_PER_REQUEST)) {
19482
+ groups.push(group2);
19483
+ group2 = [];
19484
+ bytes = 0;
19485
+ }
19486
+ group2.push(item);
19487
+ bytes += size;
19488
+ }
19489
+ groups.push(group2);
19490
+ if (groups.length === 1) return [inventoryRequestOf(surface)];
19491
+ return groups.map((skills, index) => ({
19492
+ surface: surface.descriptor,
19493
+ skills,
19494
+ chunk: { index, total: groups.length }
19495
+ }));
19496
+ }
19497
+ function inventorySkillOf(skill) {
19498
+ const snapshot = skill.upstream === void 0 && skill.files.reduce((sum, file2) => sum + file2.content.length, 0) <= MAX_SNAPSHOT_CHARS;
19499
+ const item = {
19500
+ name: skill.name,
19501
+ contentHash: skill.contentHash,
19502
+ files: skill.files.map((file2) => {
19503
+ const entry = {
19504
+ path: file2.path,
19505
+ hash: file2.hash,
19506
+ size: file2.size
19507
+ };
19508
+ if (snapshot) entry.content = file2.content;
19509
+ return entry;
19510
+ })
19511
+ };
19512
+ if (skill.upstream !== void 0) item.upstream = skill.upstream;
19513
+ return item;
19514
+ }
19515
+
19516
+ // src/surfaces.ts
19517
+ import { resolve as resolve3 } from "node:path";
19518
+ async function localSurfaces(path, both, machineId2, registered = []) {
19519
+ const projectDir = resolve3(path ?? process.cwd());
19520
+ const projectRoot2 = projectSkillsRoot(projectDir);
19521
+ const hasProject = exists(projectRoot2);
19522
+ const project = async (dir) => scanSurface(
19523
+ projectSkillsRoot(dir),
19524
+ projectSurfaceLabel(dir),
19525
+ machineId2,
19526
+ "project"
19527
+ );
19528
+ const global = async () => scanSurface(globalSkillsRoot(), globalSurfaceLabel(), machineId2, "global");
19529
+ if (!both) return [hasProject ? await project(projectDir) : await global()];
19530
+ const surfaces = [await global()];
19531
+ const dirs = projectDirs(
19532
+ hasProject || path !== void 0 ? projectDir : void 0,
19533
+ registered
19534
+ );
19535
+ for (const dir of dirs) {
19536
+ if (exists(projectSkillsRoot(dir))) {
19537
+ surfaces.push(await project(dir));
19538
+ continue;
19539
+ }
19540
+ process.stdout.write(
19541
+ dim(
19542
+ `skipping ${dir}: no .claude/skills here, run \`hubskillz projects remove ${dir}\` to forget it
19543
+ `
19544
+ )
19545
+ );
19546
+ }
19547
+ return surfaces;
19548
+ }
19549
+ function projectDirs(current, registered) {
19550
+ const dirs = [current, ...registered].filter((dir) => dir !== void 0).map((dir) => resolve3(dir)).filter((dir) => projectSkillsRoot(dir) !== globalSkillsRoot());
19551
+ return [...new Set(dirs)];
19552
+ }
19553
+
19554
+ // src/commands/doctor.ts
19555
+ function whereOf(surface) {
19556
+ return surface.descriptor.scope === "global" ? "global" : basename2(dirname2(dirname2(surface.descriptor.path)));
19557
+ }
19558
+ function frontmatterOf(skill) {
19559
+ const md = skill.files.find((file2) => file2.path === SKILL_MD);
19560
+ if (md === void 0) return null;
19561
+ return parseSkillMd(md.content).frontmatter.map((entry) => entry.key);
19562
+ }
19563
+ function skillFindings(skill, where) {
19564
+ const keys = frontmatterOf(skill);
19565
+ if (keys === null) {
19566
+ return [
19567
+ {
19568
+ level: "error",
19569
+ skill: skill.name,
19570
+ where,
19571
+ problem: `no ${SKILL_MD}, nothing for an agent to load`
19572
+ }
19573
+ ];
19574
+ }
19575
+ const findings = [];
19576
+ if (!keys.includes("name")) {
19577
+ findings.push({
19578
+ level: "warn",
19579
+ skill: skill.name,
19580
+ where,
19581
+ problem: `${SKILL_MD} without a name in its frontmatter`
19582
+ });
19583
+ }
19584
+ if (!keys.includes("description")) {
19585
+ findings.push({
19586
+ level: "warn",
19587
+ skill: skill.name,
19588
+ where,
19589
+ problem: `${SKILL_MD} without a description, nothing says when to load it`
19590
+ });
19591
+ }
19592
+ return findings;
19593
+ }
19594
+ function duplicateFindings(surfaces) {
19595
+ const global = surfaces.find(
19596
+ (surface) => surface.descriptor.scope === "global"
19597
+ );
19598
+ if (global === void 0) return [];
19599
+ const findings = [];
19600
+ for (const surface of surfaces) {
19601
+ if (surface === global) continue;
19602
+ for (const skill of surface.skills) {
19603
+ const twin = global.skills.find((entry) => entry.name === skill.name);
19604
+ if (twin === void 0) continue;
19605
+ findings.push({
19606
+ level: "warn",
19607
+ skill: skill.name,
19608
+ where: whereOf(surface),
19609
+ problem: twin.contentHash === skill.contentHash ? "same content as ~/.claude/skills, `npx hubskillz sync` clears it" : "differs from ~/.claude/skills and wins over it here"
19610
+ });
19611
+ }
19612
+ }
19613
+ return findings;
19614
+ }
19615
+ function scatteredFindings(surfaces) {
19616
+ const projects2 = surfaces.filter(
19617
+ (surface) => surface.descriptor.scope !== "global"
19618
+ );
19619
+ const global = surfaces.find(
19620
+ (surface) => surface.descriptor.scope === "global"
19621
+ );
19622
+ const counts = /* @__PURE__ */ new Map();
19623
+ for (const surface of projects2) {
19624
+ for (const skill of surface.skills) {
19625
+ const key = `${skill.name}\0${skill.contentHash}`;
19626
+ const seen = counts.get(key) ?? /* @__PURE__ */ new Set();
19627
+ seen.add(surface.descriptor.path);
19628
+ counts.set(key, seen);
19629
+ }
19630
+ }
19631
+ const findings = [];
19632
+ for (const [key, paths] of counts) {
19633
+ const name = key.split("\0")[0] ?? "";
19634
+ if (paths.size < 2) continue;
19635
+ if (global?.skills.some((entry) => entry.name === name) === true) continue;
19636
+ findings.push({
19637
+ level: "warn",
19638
+ skill: name,
19639
+ where: `${paths.size} projects`,
19640
+ problem: `identical in every one, \`hubskillz move ${name} global\` covers them all`
19641
+ });
19642
+ }
19643
+ return findings;
19644
+ }
19645
+ async function brokenFindings(root, where) {
19646
+ const entries = await readdir2(root, { withFileTypes: true }).catch(() => []);
19647
+ const findings = [];
19648
+ for (const entry of entries) {
19649
+ if (!isSkillFile(entry.name)) continue;
19650
+ const dir = join4(root, entry.name);
19651
+ if (entry.isSymbolicLink() && !existsSync2(dir)) {
19652
+ findings.push({
19653
+ level: "error",
19654
+ skill: entry.name,
19655
+ where,
19656
+ problem: "broken symlink, the target is gone"
19657
+ });
19658
+ continue;
19659
+ }
19660
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
19661
+ if (await scanSkillDir(dir) === null) {
19662
+ findings.push({
19663
+ level: "error",
19664
+ skill: entry.name,
19665
+ where,
19666
+ problem: "empty folder, no readable file inside"
19667
+ });
19668
+ }
19669
+ }
19670
+ return findings;
19671
+ }
19672
+ function staleFindings(registered) {
19673
+ return registered.filter((dir) => !exists(projectSkillsRoot(dir))).map((dir) => ({
19674
+ level: "warn",
19675
+ skill: "-",
19676
+ where: shortPath(dir),
19677
+ problem: "registered without .claude/skills, `hubskillz projects remove` it"
19678
+ }));
19679
+ }
19680
+ async function doctor(options) {
19681
+ const config2 = await readConfig();
19682
+ const machine = config2.isSuccess ? config2.value.machineId : hostname5();
19683
+ const registered = config2.isSuccess ? config2.value.projects : [];
19684
+ const live = registered.filter((dir) => exists(projectSkillsRoot(dir)));
19685
+ const surfaces = await localSurfaces(options.path, true, machine, live);
19686
+ const findings = [...staleFindings(registered)];
19687
+ for (const surface of surfaces) {
19688
+ const where = whereOf(surface);
19689
+ findings.push(...await brokenFindings(surface.descriptor.path, where));
19690
+ for (const skill of surface.skills) {
19691
+ findings.push(...skillFindings(skill, where));
19692
+ }
19693
+ }
19694
+ findings.push(...duplicateFindings(surfaces), ...scatteredFindings(surfaces));
19695
+ printSurfaces(surfaces);
19696
+ printFindings(findings);
19697
+ return Result.ok(void 0);
19698
+ }
19699
+ function printSurfaces(surfaces) {
19700
+ for (const surface of surfaces) {
19701
+ process.stdout.write(
19702
+ `${bold(surface.descriptor.label)} ${dim(
19703
+ `${shortPath(surface.descriptor.path)}, ${plural(surface.skills.length, "skill")}`
19704
+ )}
19705
+ `
19706
+ );
19707
+ }
19708
+ }
19709
+ function group(findings) {
19710
+ const groups = /* @__PURE__ */ new Map();
19711
+ for (const finding of findings) {
19712
+ const key = `${finding.level}\0${finding.where}\0${finding.problem}`;
19713
+ const found = groups.get(key) ?? [];
19714
+ found.push(finding);
19715
+ groups.set(key, found);
19716
+ }
19717
+ return groups;
19718
+ }
19719
+ function printFindings(findings) {
19720
+ if (findings.length === 0) {
19721
+ process.stdout.write(`
19722
+ ${dim("nothing to fix")}
19723
+ `);
19724
+ return;
19725
+ }
19726
+ const groups = [...group(findings).values()].sort(
19727
+ (left, right) => left[0]?.level === right[0]?.level ? 0 : left[0]?.level === "error" ? -1 : 1
19728
+ );
19729
+ process.stdout.write(
19730
+ `
19731
+ ${table(
19732
+ ["LEVEL", "SKILL", "WHERE", "PROBLEM"],
19733
+ groups.map(([first, ...rest]) => [
19734
+ first?.level === "error" ? accent("error") : dim("warn"),
19735
+ rest.length === 0 ? first?.skill ?? "-" : plural(rest.length + 1, "skill"),
19736
+ first?.where ?? "-",
19737
+ first?.problem ?? ""
19738
+ ])
19739
+ )}
19740
+ `
19741
+ );
19742
+ for (const found of groups) {
19743
+ if (found.length < 2) continue;
19744
+ const first = found[0];
19745
+ if (first === void 0) continue;
19746
+ process.stdout.write(
19747
+ `
19748
+ ${dim(`${first.where}, ${first.problem}`)}
19749
+ ${found.map((finding) => finding.skill).join(", ")}
19750
+ `
19751
+ );
19752
+ }
19753
+ const errors = findings.filter((finding) => finding.level === "error").length;
19754
+ process.stdout.write(
19755
+ `
19756
+ ${plural(findings.length, "problem")}, ${errors} to fix by hand
19757
+ `
19758
+ );
19759
+ }
19760
+
19761
+ // src/api.ts
19762
+ async function apiRequest(request) {
19763
+ const url2 = `${request.session.baseUrl}${request.path}`;
19764
+ const bearer = `Bearer ${request.session.token}`;
19765
+ let response;
19766
+ try {
19767
+ response = await fetch(url2, {
19768
+ method: request.method,
19769
+ headers: request.body === void 0 ? { accept: "application/json", authorization: bearer } : {
19770
+ accept: "application/json",
19771
+ authorization: bearer,
19772
+ "content-type": "application/json"
19773
+ },
19774
+ body: request.body === void 0 ? void 0 : JSON.stringify(request.body)
19775
+ });
19776
+ } catch (cause) {
19777
+ const detail = cause instanceof Error ? cause.message : String(cause);
19778
+ return Result.fail(
19779
+ new CliError("NETWORK", `Cannot reach ${url2}: ${detail}`)
19780
+ );
19781
+ }
19782
+ const text = await response.text();
19783
+ if (response.status === 413) {
19784
+ return Result.fail(
19785
+ new CliError(
19786
+ "HTTP",
19787
+ `${request.method} ${request.path} failed: the inventory is too large (HTTP 413). Reduce the number of skills or roots, or use project roots.`
19788
+ )
19789
+ );
19790
+ }
19791
+ if (!response.ok) {
19792
+ const message = decodeBody(text, apiErrorSchema)?.message ?? `The server returned HTTP ${response.status} with an unexpected body. Try again in a minute.`;
19793
+ return Result.fail(
19794
+ new CliError(
19795
+ response.status === 401 ? "UNAUTHORIZED" : response.status === 403 ? "FORBIDDEN" : "HTTP",
19796
+ `${request.method} ${request.path} failed: ${message}`
19797
+ )
19798
+ );
19799
+ }
19800
+ const parsed = decodeBody(text, request.schema);
19801
+ if (parsed === null) {
19802
+ return Result.fail(
19803
+ new CliError(
19804
+ "PROTOCOL",
19805
+ `${request.method} ${request.path} returned an unexpected payload (HTTP ${response.status}, not JSON or wrong shape).`
19806
+ )
19807
+ );
19808
+ }
19809
+ return Result.ok(parsed);
19810
+ }
19811
+ function decodeBody(text, schema) {
19812
+ try {
19813
+ const parsed = schema.safeParse(JSON.parse(text === "" ? "null" : text));
19814
+ return parsed.success ? parsed.data : null;
19815
+ } catch {
19816
+ return null;
19817
+ }
19818
+ }
19819
+
19820
+ // src/prompt.ts
19821
+ import { createInterface } from "node:readline/promises";
19822
+ import { text as readAll } from "node:stream/consumers";
19823
+ var ENTER = /* @__PURE__ */ new Set(["\r", "\n"]);
19824
+ var BACKSPACE = /* @__PURE__ */ new Set(["\b", "\x7F"]);
19825
+ var CTRL_C = "";
19826
+ async function promptSecret(label) {
19827
+ const input2 = process.stdin;
19828
+ if (input2.isTTY !== true) {
19829
+ return (await readAll(input2)).split("\n")[0]?.trim() ?? "";
19830
+ }
19831
+ process.stdout.write(label);
19832
+ input2.setRawMode(true);
19833
+ input2.resume();
19834
+ input2.setEncoding("utf8");
19835
+ return new Promise((settle, fail) => {
19836
+ let value = "";
19837
+ const cleanup = () => {
19838
+ input2.setRawMode(false);
19839
+ input2.pause();
19840
+ input2.off("data", onData);
19841
+ process.stdout.write("\n");
19842
+ };
19843
+ const onData = (chunk) => {
19844
+ for (const char of chunk) {
19845
+ if (ENTER.has(char)) {
19846
+ cleanup();
19847
+ settle(value.trim());
19848
+ return;
19849
+ }
19850
+ if (char === CTRL_C) {
19851
+ cleanup();
19852
+ fail(new Error("Interrupted"));
19853
+ return;
19854
+ }
19855
+ value = BACKSPACE.has(char) ? value.slice(0, -1) : value + char;
19856
+ }
19857
+ };
19858
+ input2.on("data", onData);
19859
+ });
19860
+ }
19861
+ async function confirm(question) {
19862
+ if (process.stdin.isTTY !== true) return false;
19863
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
19864
+ try {
19865
+ const answer = await rl.question(`${question} [y/N] `);
19866
+ return answer.trim().toLowerCase() === "y";
19867
+ } finally {
19868
+ rl.close();
19869
+ }
19870
+ }
19871
+ function parseSelection(answer, count) {
19872
+ const text = answer.trim().toLowerCase();
19873
+ if (text === "" || text === "all" || text === "a") {
19874
+ return Array.from({ length: count }, (_, index) => index);
19875
+ }
19876
+ if (text === "none" || text === "n") return [];
19877
+ const picked = /* @__PURE__ */ new Set();
19878
+ for (const token of text.split(/[\s,]+/u)) {
19879
+ const index = Number(token) - 1;
19880
+ if (Number.isInteger(index) && index >= 0 && index < count) {
19881
+ picked.add(index);
19882
+ }
19883
+ }
19884
+ return [...picked].sort((left, right) => left - right);
19885
+ }
19886
+ async function selectMany(question, choices) {
19887
+ if (process.stdin.isTTY !== true) return choices;
19888
+ process.stdout.write(`${question}
19889
+ `);
19890
+ choices.forEach((choice, index) => {
19891
+ process.stdout.write(` ${index + 1}. ${choice}
19892
+ `);
19893
+ });
19894
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
19895
+ try {
19896
+ const answer = await rl.question("Select [all]: ");
19897
+ const picked = new Set(parseSelection(answer, choices.length));
19898
+ return choices.filter((_, index) => picked.has(index));
19899
+ } finally {
19900
+ rl.close();
19901
+ }
19902
+ }
19903
+
19375
19904
  // src/quickstart.ts
19376
19905
  function quickstart(config2) {
19377
19906
  const signedIn = config2.isSuccess;
@@ -19446,244 +19975,140 @@ ${dim(`Token written to ${configPath()}`)}
19446
19975
  }
19447
19976
 
19448
19977
  // src/commands/logout.ts
19449
- async function logout() {
19450
- const removed = await deleteConfig();
19451
- process.stdout.write(
19452
- removed ? `Signed out. ${dim(`Removed ${configPath()}`)}
19453
- ` : `${dim("Not signed in.")}
19454
- `
19455
- );
19456
- return Result.ok(void 0);
19457
- }
19458
-
19459
- // src/commands/projects.ts
19460
- import { resolve as resolve4 } from "node:path";
19461
-
19462
- // src/discover.ts
19463
- import { readdir as readdir2 } from "node:fs/promises";
19464
- import { homedir as homedir4 } from "node:os";
19465
- import { join as join4, resolve as resolve3 } from "node:path";
19466
-
19467
- // src/scan.ts
19468
- import { createHash as createHash2 } from "node:crypto";
19469
- import { readdir, readFile as readFile3, realpath, stat } from "node:fs/promises";
19470
- import { homedir as homedir3, hostname as hostname4 } from "node:os";
19471
- import { basename, dirname, join as join3, resolve as resolve2 } from "node:path";
19472
-
19473
- // src/lock.ts
19474
- import { readFile as readFile2 } from "node:fs/promises";
19475
- import { homedir as homedir2 } from "node:os";
19476
- import { join as join2, resolve } from "node:path";
19477
- var entrySchema = external_exports.object({
19478
- source: external_exports.string().min(1),
19479
- sourceType: external_exports.string(),
19480
- skillPath: external_exports.string().min(1).optional(),
19481
- skillFolderHash: external_exports.string().min(1).optional(),
19482
- computedHash: external_exports.string().min(1).optional()
19483
- });
19484
- var lockSchema = external_exports.object({ skills: external_exports.record(external_exports.string(), external_exports.unknown()) });
19485
- function globalLockPath() {
19486
- return join2(homedir2(), ".agents", ".skill-lock.json");
19487
- }
19488
- function projectLockPath(projectDir) {
19489
- return join2(resolve(projectDir), "skills-lock.json");
19490
- }
19491
- async function readLock(path) {
19492
- const map2 = /* @__PURE__ */ new Map();
19493
- const raw = await readFile2(path, "utf8").catch(() => null);
19494
- if (raw === null) return map2;
19495
- let json2;
19496
- try {
19497
- json2 = JSON.parse(raw);
19498
- } catch {
19499
- return map2;
19500
- }
19501
- const lock = lockSchema.safeParse(json2);
19502
- if (!lock.success) return map2;
19503
- for (const [name, value] of Object.entries(lock.data.skills)) {
19504
- const entry = entrySchema.safeParse(value);
19505
- if (!entry.success) continue;
19506
- const { source, sourceType, skillPath } = entry.data;
19507
- const hash2 = entry.data.skillFolderHash ?? entry.data.computedHash;
19508
- if (sourceType !== "github" || skillPath === void 0) continue;
19509
- if (hash2 === void 0) continue;
19510
- map2.set(name, { source, skillPath, hash: hash2 });
19511
- }
19512
- return map2;
19513
- }
19514
-
19515
- // src/scan.ts
19516
- function globalSkillsRoot() {
19517
- return join3(homedir3(), ".claude", "skills");
19518
- }
19519
- function projectSkillsRoot(dir) {
19520
- return join3(resolve2(dir), ".claude", "skills");
19521
- }
19522
- function globalSurfaceLabel() {
19523
- return hostname4();
19524
- }
19525
- function projectSurfaceLabel(dir) {
19526
- return `${basename(resolve2(dir))} (${hostname4()})`;
19527
- }
19528
- async function exists(path) {
19529
- try {
19530
- await stat(path);
19531
- return true;
19532
- } catch {
19533
- return false;
19534
- }
19535
- }
19536
- function lockPathFor(root) {
19537
- const resolved = resolve2(root);
19538
- if (resolved === globalSkillsRoot()) return globalLockPath();
19539
- return projectLockPath(dirname(dirname(resolved)));
19540
- }
19541
- async function scanSurface(root, label, machine) {
19542
- const lock = await readLock(lockPathFor(root));
19543
- const skills = await scanSkills(root);
19544
- return {
19545
- descriptor: {
19546
- kind: "claude-code-local",
19547
- label,
19548
- machineId: machine,
19549
- path: resolve2(root)
19550
- },
19551
- skills: await Promise.all(skills.map((skill) => withUpstream(skill, lock)))
19552
- };
19553
- }
19554
- async function withUpstream(skill, lock) {
19555
- if (lock.size === 0) return skill;
19556
- const names = [skill.name];
19557
- if (skill.link) {
19558
- const real = await realpath(skill.dir).catch(() => skill.dir);
19559
- names.push(basename(real));
19560
- }
19561
- const upstream = names.map((name) => lock.get(name)).find(Boolean);
19562
- return upstream === void 0 ? skill : { ...skill, upstream };
19563
- }
19564
- async function scanSkills(root) {
19565
- let entries;
19566
- try {
19567
- entries = await readdir(root, { withFileTypes: true });
19568
- } catch {
19569
- return [];
19570
- }
19571
- const skills = [];
19572
- for (const entry of entries) {
19573
- if (!isSkillFile(entry.name)) continue;
19574
- const dir = join3(root, entry.name);
19575
- const symlink = entry.isSymbolicLink();
19576
- if (!entry.isDirectory() && !(symlink && await isDir(dir))) continue;
19577
- const skill = await scanSkillDir(dir);
19578
- if (skill === null) continue;
19579
- skills.push(symlink ? { ...skill, link: true } : skill);
19580
- }
19581
- return skills.sort((a, b) => a.name < b.name ? -1 : 1);
19582
- }
19583
- async function scanSkillDir(dir) {
19584
- const resolved = resolve2(dir);
19585
- const walked = await walk(resolved, "", /* @__PURE__ */ new Set());
19586
- if (walked.files.length === 0) return null;
19587
- return {
19588
- name: basename(resolved),
19589
- dir: resolved,
19590
- files: walked.files,
19591
- contentHash: contentHash(walked.files),
19592
- link: walked.link
19593
- };
19594
- }
19595
- async function walk(dir, prefix, visited) {
19596
- const real = await realpath(dir).catch(() => dir);
19597
- if (visited.has(real)) return { files: [], link: false };
19598
- visited.add(real);
19599
- const files = [];
19600
- let link = false;
19601
- const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
19602
- for (const entry of entries) {
19603
- const full = join3(dir, entry.name);
19604
- const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
19605
- if (!isSkillFile(relative)) continue;
19606
- const symlink = entry.isSymbolicLink();
19607
- if (entry.isDirectory() || symlink && await isDir(full)) {
19608
- const nested = await walk(full, relative, visited);
19609
- files.push(...nested.files);
19610
- link = link || symlink || nested.link;
19611
- continue;
19612
- }
19613
- if (!entry.isFile() && !symlink) continue;
19614
- const content = await readFile3(full, "utf8").catch(() => null);
19615
- if (content === null || content.includes("\0") || content.length > MAX_FILE_CONTENT_CHARS) {
19616
- continue;
19617
- }
19618
- files.push({
19619
- path: relative,
19620
- content,
19621
- hash: createHash2("sha256").update(content).digest("hex"),
19622
- size: Buffer.byteLength(content, "utf8")
19623
- });
19624
- }
19625
- return { files: files.sort((a, b) => a.path < b.path ? -1 : 1), link };
19978
+ async function logout() {
19979
+ const removed = await deleteConfig();
19980
+ process.stdout.write(
19981
+ removed ? `Signed out. ${dim(`Removed ${configPath()}`)}
19982
+ ` : `${dim("Not signed in.")}
19983
+ `
19984
+ );
19985
+ return Result.ok(void 0);
19626
19986
  }
19627
- async function isDir(path) {
19628
- return stat(path).then((stats) => stats.isDirectory()).catch(() => false);
19987
+
19988
+ // src/commands/move.ts
19989
+ import { existsSync as existsSync3 } from "node:fs";
19990
+ import { cp, lstat, mkdir as mkdir2, rename, rm as rm2 } from "node:fs/promises";
19991
+ import { join as join5, resolve as resolve4 } from "node:path";
19992
+ function skillsRootOf(target) {
19993
+ return target === "global" ? globalSkillsRoot() : projectSkillsRoot(target);
19994
+ }
19995
+ function searchRoots(path, from) {
19996
+ if (from !== void 0) return [skillsRootOf(from)];
19997
+ return [
19998
+ .../* @__PURE__ */ new Set([projectSkillsRoot(path ?? process.cwd()), globalSkillsRoot()])
19999
+ ];
19629
20000
  }
19630
- function inventoryRequestOf(surface) {
19631
- return {
19632
- surface: surface.descriptor,
19633
- skills: surface.skills.map(inventorySkillOf)
19634
- };
20001
+ function pickSource(name, roots, destRoot, holds) {
20002
+ const found = roots.filter((root) => root !== destRoot && holds(root));
20003
+ const [first, second] = found;
20004
+ if (first !== void 0 && second === void 0) return Result.ok(first);
20005
+ if (first === void 0) {
20006
+ return Result.fail(
20007
+ new CliError(
20008
+ "SKILL_NOT_FOUND",
20009
+ `No skill named ${name} in ${roots.map(shortPath).join(" or ")}.`
20010
+ )
20011
+ );
20012
+ }
20013
+ return Result.fail(
20014
+ new CliError(
20015
+ "AMBIGUOUS_SOURCE",
20016
+ `${name} exists in ${found.map(shortPath).join(" and ")}. Pass --from to say which one to move.`
20017
+ )
20018
+ );
19635
20019
  }
19636
- function inventoryChunksOf(surface) {
19637
- const groups = [];
19638
- let group = [];
19639
- let bytes = 0;
19640
- for (const skill of surface.skills) {
19641
- const item = inventorySkillOf(skill);
19642
- const size = Buffer.byteLength(JSON.stringify(item), "utf8");
19643
- if (group.length > 0 && (bytes + size > MAX_INVENTORY_CHUNK_BYTES || group.length >= MAX_SKILLS_PER_REQUEST)) {
19644
- groups.push(group);
19645
- group = [];
19646
- bytes = 0;
20020
+ async function moveSkill(source, dest, force) {
20021
+ if (await lstat(source).catch(() => null) === null) {
20022
+ return Result.fail(new CliError("SKILL_NOT_FOUND", `${source} is gone.`));
20023
+ }
20024
+ if (existsSync3(dest) || await lstat(dest).catch(() => null) !== null) {
20025
+ if (!force) {
20026
+ return Result.fail(
20027
+ new CliError(
20028
+ "DESTINATION_EXISTS",
20029
+ `${shortPath(dest)} already exists. Pass --force to replace it.`
20030
+ )
20031
+ );
19647
20032
  }
19648
- group.push(item);
19649
- bytes += size;
20033
+ await rm2(dest, { recursive: true, force: true });
19650
20034
  }
19651
- groups.push(group);
19652
- if (groups.length === 1) return [inventoryRequestOf(surface)];
19653
- return groups.map((skills, index) => ({
19654
- surface: surface.descriptor,
19655
- skills,
19656
- chunk: { index, total: groups.length }
19657
- }));
20035
+ await mkdir2(resolve4(dest, ".."), { recursive: true });
20036
+ try {
20037
+ await rename(source, dest);
20038
+ } catch (cause) {
20039
+ const code = cause instanceof Error && "code" in cause ? cause.code : null;
20040
+ if (code !== "EXDEV") {
20041
+ const detail = cause instanceof Error ? cause.message : String(cause);
20042
+ return Result.fail(new CliError("MOVE_FAILED", detail));
20043
+ }
20044
+ await cp(source, dest, { recursive: true, verbatimSymlinks: true });
20045
+ await rm2(source, { recursive: true, force: true });
20046
+ }
20047
+ return Result.ok(void 0);
19658
20048
  }
19659
- function inventorySkillOf(skill) {
19660
- const snapshot = skill.upstream === void 0 && skill.files.reduce((sum, file2) => sum + file2.content.length, 0) <= MAX_SNAPSHOT_CHARS;
19661
- const item = {
19662
- name: skill.name,
19663
- contentHash: skill.contentHash,
19664
- files: skill.files.map((file2) => {
19665
- const entry = {
19666
- path: file2.path,
19667
- hash: file2.hash,
19668
- size: file2.size
19669
- };
19670
- if (snapshot) entry.content = file2.content;
19671
- return entry;
19672
- })
19673
- };
19674
- if (skill.upstream !== void 0) item.upstream = skill.upstream;
19675
- return item;
20049
+ async function sameContent(left, right) {
20050
+ const [one, two] = await Promise.all([
20051
+ scanSkillDir(left),
20052
+ scanSkillDir(right)
20053
+ ]);
20054
+ return one !== null && two !== null && one.contentHash === two.contentHash;
19676
20055
  }
20056
+ async function move(options) {
20057
+ if (options.to === void 0) {
20058
+ return Result.fail(
20059
+ new CliError(
20060
+ "USAGE",
20061
+ "hubskillz move needs a destination: `global` or a project directory. Run `hubskillz help move`."
20062
+ )
20063
+ );
20064
+ }
20065
+ const destRoot = skillsRootOf(options.to);
20066
+ const roots = searchRoots(options.path, options.from);
20067
+ const source = pickSource(
20068
+ options.name,
20069
+ roots,
20070
+ destRoot,
20071
+ (root) => existsSync3(join5(root, options.name))
20072
+ );
20073
+ if (source.isFailure) return Result.fail(source.error);
20074
+ const dest = join5(destRoot, options.name);
20075
+ const origin = join5(source.value, options.name);
20076
+ if (!options.force && await sameContent(origin, dest)) {
20077
+ await rm2(origin, { recursive: true, force: true });
20078
+ process.stdout.write(
20079
+ `${shortPath(dest)} already holds this exact skill
20080
+ removed the copy in ${shortPath(origin)}
20081
+ `
20082
+ );
20083
+ return Result.ok(void 0);
20084
+ }
20085
+ const moved = await moveSkill(origin, dest, options.force);
20086
+ if (moved.isFailure) return moved;
20087
+ process.stdout.write(
20088
+ `moved ${options.name}
20089
+ ${shortPath(origin)}
20090
+ ${shortPath(dest)}
20091
+ ${dim("run npx hubskillz status to report the new layout")}
20092
+ `
20093
+ );
20094
+ return Result.ok(void 0);
20095
+ }
20096
+
20097
+ // src/commands/projects.ts
20098
+ import { resolve as resolve6 } from "node:path";
19677
20099
 
19678
20100
  // src/discover.ts
20101
+ import { readdir as readdir3 } from "node:fs/promises";
20102
+ import { homedir as homedir5 } from "node:os";
20103
+ import { join as join6, resolve as resolve5 } from "node:path";
19679
20104
  var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "Library", ".Trash", ".cache"]);
19680
- async function discoverProjects(roots = [homedir4()], maxDepth = 3) {
20105
+ async function discoverProjects(roots = [homedir5()], maxDepth = 3) {
19681
20106
  const found = /* @__PURE__ */ new Set();
19682
20107
  const walk2 = async (dir, depth) => {
19683
20108
  if (depth > maxDepth) return;
19684
20109
  let entries;
19685
20110
  try {
19686
- entries = await readdir2(dir, { withFileTypes: true });
20111
+ entries = await readdir3(dir, { withFileTypes: true });
19687
20112
  } catch {
19688
20113
  return;
19689
20114
  }
@@ -19691,15 +20116,15 @@ async function discoverProjects(roots = [homedir4()], maxDepth = 3) {
19691
20116
  const name = entry.name;
19692
20117
  if (!entry.isDirectory() || SKIP.has(name)) continue;
19693
20118
  if (name.startsWith(".") && name !== ".claude") continue;
19694
- const child = join4(dir, name);
20119
+ const child = join6(dir, name);
19695
20120
  if (name === ".claude" && depth > 0) {
19696
- if (await exists(projectSkillsRoot(dir))) found.add(dir);
20121
+ if (exists(projectSkillsRoot(dir))) found.add(dir);
19697
20122
  continue;
19698
20123
  }
19699
20124
  await walk2(child, depth + 1);
19700
20125
  }
19701
20126
  };
19702
- for (const root of roots) await walk2(resolve3(root), 0);
20127
+ for (const root of roots) await walk2(resolve5(root), 0);
19703
20128
  return [...found].sort();
19704
20129
  }
19705
20130
 
@@ -19716,7 +20141,7 @@ async function discoverAndRegister(config2, yes) {
19716
20141
  const projects2 = [...config2.projects, ...chosen];
19717
20142
  await writeConfig({ ...config2, projects: projects2 });
19718
20143
  process.stdout.write(
19719
- `Registered ${chosen.length} project${chosen.length === 1 ? "" : "s"}. They are now part of \`hubskillz sync --all\`.
20144
+ `Registered ${plural(chosen.length, "project")}. They are now part of \`hubskillz sync --all\`.
19720
20145
  `
19721
20146
  );
19722
20147
  return projects2;
@@ -19725,7 +20150,7 @@ async function projects(options) {
19725
20150
  const config2 = await readConfig();
19726
20151
  if (config2.isFailure) return Result.fail(config2.error);
19727
20152
  const current = config2.value.projects;
19728
- const dir = resolve4(options.dir ?? process.cwd());
20153
+ const dir = resolve6(options.dir ?? process.cwd());
19729
20154
  switch (options.action ?? "list") {
19730
20155
  case "list": {
19731
20156
  if (current.length === 0) {
@@ -19741,7 +20166,7 @@ async function projects(options) {
19741
20166
  return Result.ok(void 0);
19742
20167
  }
19743
20168
  case "add": {
19744
- if (!await exists(projectSkillsRoot(dir))) {
20169
+ if (!exists(projectSkillsRoot(dir))) {
19745
20170
  return Result.fail(
19746
20171
  new CliError(
19747
20172
  "NO_SKILLS_ROOT",
@@ -19789,6 +20214,32 @@ async function projects(options) {
19789
20214
  }
19790
20215
  }
19791
20216
 
20217
+ // src/commands/publish.ts
20218
+ async function publish(options) {
20219
+ const config2 = await readConfig();
20220
+ if (config2.isFailure) return Result.fail(config2.error);
20221
+ const session = {
20222
+ baseUrl: resolveBaseUrl(options.baseUrl, config2.value.baseUrl),
20223
+ token: config2.value.token
20224
+ };
20225
+ const result = await apiRequest({
20226
+ session,
20227
+ method: "POST",
20228
+ path: "/api/cli/publish",
20229
+ schema: publishResponseSchema,
20230
+ body: { name: options.name, published: options.published }
20231
+ });
20232
+ if (result.isFailure) return Result.fail(result.error);
20233
+ const page = `${webOrigin(session.baseUrl)}/@${result.value.handle}`;
20234
+ process.stdout.write(
20235
+ options.published ? `${options.name} is public on ${page}
20236
+ ` : `${options.name} is off your public page.
20237
+ ${dim(page)}
20238
+ `
20239
+ );
20240
+ return Result.ok(void 0);
20241
+ }
20242
+
19792
20243
  // src/commands/push.ts
19793
20244
  async function push(options) {
19794
20245
  const config2 = await readConfig();
@@ -19827,47 +20278,6 @@ ${dim(`skill ${draft.value.skillId} version ${draft.value.versionId}`)}
19827
20278
  return Result.ok(void 0);
19828
20279
  }
19829
20280
 
19830
- // src/surfaces.ts
19831
- import { resolve as resolve5 } from "node:path";
19832
- async function localSurfaces(path, both, machineId2, registered = []) {
19833
- const projectDir = resolve5(path ?? process.cwd());
19834
- const projectRoot = projectSkillsRoot(projectDir);
19835
- const hasProject = await exists(projectRoot);
19836
- const project = async (dir) => scanSurface(projectSkillsRoot(dir), projectSurfaceLabel(dir), machineId2);
19837
- const global = async () => scanSurface(globalSkillsRoot(), globalSurfaceLabel(), machineId2);
19838
- if (!both) return [hasProject ? await project(projectDir) : await global()];
19839
- const surfaces = [await global()];
19840
- const dirs = projectDirs(
19841
- hasProject || path !== void 0 ? projectDir : void 0,
19842
- registered
19843
- );
19844
- for (const dir of dirs) {
19845
- if (await exists(projectSkillsRoot(dir))) {
19846
- surfaces.push(await project(dir));
19847
- continue;
19848
- }
19849
- process.stdout.write(
19850
- dim(
19851
- `skipping ${dir}: no .claude/skills here, run \`hubskillz projects remove ${dir}\` to forget it
19852
- `
19853
- )
19854
- );
19855
- }
19856
- return surfaces;
19857
- }
19858
- function projectDirs(current, registered) {
19859
- const seen = /* @__PURE__ */ new Set();
19860
- const out = [];
19861
- for (const dir of [current, ...registered]) {
19862
- if (dir === void 0) continue;
19863
- const abs = resolve5(dir);
19864
- if (seen.has(abs)) continue;
19865
- seen.add(abs);
19866
- out.push(abs);
19867
- }
19868
- return out;
19869
- }
19870
-
19871
20281
  // src/commands/status.ts
19872
20282
  async function status(options) {
19873
20283
  const config2 = await readConfig();
@@ -19886,7 +20296,7 @@ async function status(options) {
19886
20296
  for (const surface of surfaces) {
19887
20297
  const inventory = await postInventory(session, surface);
19888
20298
  if (inventory.isFailure) return Result.fail(inventory.error);
19889
- printSurface(surface, inventory.value);
20299
+ printSurface(surface, inventory.value, session.baseUrl);
19890
20300
  }
19891
20301
  if (quickstartPending(config2)) {
19892
20302
  process.stdout.write(`
@@ -19896,7 +20306,8 @@ ${quickstart(config2)}`);
19896
20306
  }
19897
20307
  async function postInventory(session, surface) {
19898
20308
  const chunks = inventoryChunksOf(surface);
19899
- let merged;
20309
+ let surfaceId;
20310
+ const items = [];
19900
20311
  for (const body of chunks) {
19901
20312
  const posted = await apiRequest({
19902
20313
  session,
@@ -19906,16 +20317,30 @@ async function postInventory(session, surface) {
19906
20317
  body
19907
20318
  });
19908
20319
  if (posted.isFailure) return posted;
19909
- merged = merged === void 0 ? posted.value : { ...merged, items: [...merged.items, ...posted.value.items] };
20320
+ surfaceId ??= posted.value.surfaceId;
20321
+ items.push(...posted.value.items);
19910
20322
  }
19911
- return Result.ok(merged ?? { surfaceId: "", items: [] });
20323
+ if (surfaceId === void 0) return Result.ok({ surfaceId: "", items: [] });
20324
+ return Result.ok({ surfaceId, items: mergeItems(items) });
20325
+ }
20326
+ function mergeItems(items) {
20327
+ const byName = /* @__PURE__ */ new Map();
20328
+ for (const item of items) {
20329
+ const seen = byName.get(item.name);
20330
+ const better = seen === void 0 || seen.installedHash === void 0 && item.installedHash !== void 0;
20331
+ if (better) byName.set(item.name, item);
20332
+ }
20333
+ return [...byName.values()];
19912
20334
  }
19913
20335
  function originOf(surface, name) {
19914
20336
  const skill = surface.skills.find((entry) => entry.name === name);
19915
20337
  if (skill === void 0) return "-";
19916
20338
  return skill.upstream === void 0 ? "private" : `skills.sh ${skill.upstream.source}`;
19917
20339
  }
19918
- function printNotes(surface, items) {
20340
+ function reviewUrl(baseUrl) {
20341
+ return `${webOrigin(baseUrl)}/app`;
20342
+ }
20343
+ function printNotes(surface, items, baseUrl) {
19919
20344
  const importable = items.filter((item) => item.importable).map((item) => item.name);
19920
20345
  if (importable.length > 0) {
19921
20346
  process.stdout.write(
@@ -19927,6 +20352,10 @@ function printNotes(surface, items) {
19927
20352
  if (ahead.length > 0) {
19928
20353
  process.stdout.write(
19929
20354
  `${dim("upstream ahead of approved version, waiting for review:")} ${ahead.join(", ")}
20355
+ `
20356
+ );
20357
+ process.stdout.write(
20358
+ `${dim(`review and approve at ${reviewUrl(baseUrl)}`)}
19930
20359
  `
19931
20360
  );
19932
20361
  }
@@ -19938,17 +20367,35 @@ ${bold(surface.descriptor.label)} ${dim(surface.descriptor.path)}
19938
20367
  `
19939
20368
  );
19940
20369
  }
19941
- function printSurface(surface, inventory) {
19942
- const rows = inventory.items.map((item) => [
20370
+ function printSurface(surface, inventory, baseUrl) {
20371
+ const inherited = inventory.items.filter(
20372
+ (item) => item.state === "inherited"
20373
+ ).length;
20374
+ const rows = inventory.items.filter((item) => item.state !== "inherited").map((item) => [
19943
20375
  item.name,
19944
20376
  originOf(surface, item.name),
19945
20377
  item.required ? `${item.state} ${dim("(required)")}` : item.state,
19946
- shortHash(item.installedHash),
20378
+ shortSha(item.installedHash, 8),
19947
20379
  item.approvedVersion === void 0 ? "-" : `v${item.approvedVersion}`
19948
20380
  ]);
19949
20381
  printHeader(surface);
20382
+ if (inherited > 0) {
20383
+ process.stdout.write(
20384
+ `${dim(`${plural(inherited, "skill")} inherited from ~/.claude/skills`)}
20385
+ `
20386
+ );
20387
+ }
20388
+ const duplicates = inventory.items.filter(
20389
+ (item) => item.state === "inherited" && item.installedHash !== void 0
20390
+ ).length;
20391
+ if (duplicates > 0) {
20392
+ process.stdout.write(
20393
+ `${dim(`${duplicates} duplicate cop${duplicates === 1 ? "y" : "ies"} here: run npx hubskillz sync to remove ${duplicates === 1 ? "it" : "them"}`)}
20394
+ `
20395
+ );
20396
+ }
19950
20397
  if (rows.length === 0) {
19951
- process.stdout.write(`${dim("no skills")}
20398
+ if (inherited === 0) process.stdout.write(`${dim("no skills")}
19952
20399
  `);
19953
20400
  return;
19954
20401
  }
@@ -19957,17 +20404,26 @@ function printSurface(surface, inventory) {
19957
20404
 
19958
20405
  `
19959
20406
  );
19960
- printNotes(surface, inventory.items);
20407
+ printNotes(surface, inventory.items, baseUrl);
20408
+ const behind = inventory.items.filter(
20409
+ (item) => item.state === "drifted" || item.state === "missing"
20410
+ ).length;
20411
+ if (behind > 0) {
20412
+ process.stdout.write(
20413
+ `${dim(`${plural(behind, "skill")} to install or update: run npx hubskillz sync`)}
20414
+ `
20415
+ );
20416
+ }
19961
20417
  }
19962
20418
 
19963
20419
  // src/commands/sync.ts
19964
- import { lstat, readdir as readdir3, realpath as realpath2 } from "node:fs/promises";
19965
- import { homedir as homedir5 } from "node:os";
19966
- import { join as join6, resolve as resolve6, sep as sep2 } from "node:path";
20420
+ import { lstat as lstat2, readdir as readdir4, realpath as realpath2, rm as rm4 } from "node:fs/promises";
20421
+ import { homedir as homedir6 } from "node:os";
20422
+ import { join as join8, resolve as resolve7, sep as sep2 } from "node:path";
19967
20423
 
19968
20424
  // src/apply.ts
19969
- import { mkdir as mkdir2, mkdtemp, rename, rm as rm2, rmdir, writeFile as writeFile2 } from "node:fs/promises";
19970
- import { dirname as dirname2, join as join5, sep } from "node:path";
20425
+ import { mkdir as mkdir3, mkdtemp, rename as rename2, rm as rm3, rmdir, writeFile as writeFile2 } from "node:fs/promises";
20426
+ import { dirname as dirname3, join as join7, sep } from "node:path";
19971
20427
  async function applySkill(input2) {
19972
20428
  for (const path of [...input2.files.map((f) => f.path), ...input2.remove]) {
19973
20429
  if (!isSafeRelativePath(path)) {
@@ -19979,26 +20435,26 @@ async function applySkill(input2) {
19979
20435
  );
19980
20436
  }
19981
20437
  }
19982
- await mkdir2(input2.dir, { recursive: true });
19983
- const staging = await mkdtemp(join5(dirname2(input2.dir), ".hubskillz-"));
20438
+ await mkdir3(input2.dir, { recursive: true });
20439
+ const staging = await mkdtemp(join7(dirname3(input2.dir), ".hubskillz-"));
19984
20440
  try {
19985
20441
  for (const file2 of input2.files) {
19986
- const staged = join5(staging, ...file2.path.split("/"));
19987
- await mkdir2(dirname2(staged), { recursive: true });
20442
+ const staged = join7(staging, ...file2.path.split("/"));
20443
+ await mkdir3(dirname3(staged), { recursive: true });
19988
20444
  await writeFile2(staged, file2.content, "utf8");
19989
20445
  }
19990
20446
  for (const file2 of input2.files) {
19991
- const target = join5(input2.dir, ...file2.path.split("/"));
19992
- await mkdir2(dirname2(target), { recursive: true });
19993
- await rename(join5(staging, ...file2.path.split("/")), target);
20447
+ const target = join7(input2.dir, ...file2.path.split("/"));
20448
+ await mkdir3(dirname3(target), { recursive: true });
20449
+ await rename2(join7(staging, ...file2.path.split("/")), target);
19994
20450
  }
19995
20451
  for (const path of input2.remove) {
19996
- const target = join5(input2.dir, ...path.split("/"));
19997
- await rm2(target, { force: true });
19998
- await pruneEmptyDirs(dirname2(target), input2.dir);
20452
+ const target = join7(input2.dir, ...path.split("/"));
20453
+ await rm3(target, { force: true });
20454
+ await pruneEmptyDirs(dirname3(target), input2.dir);
19999
20455
  }
20000
20456
  } finally {
20001
- await rm2(staging, { recursive: true, force: true });
20457
+ await rm3(staging, { recursive: true, force: true });
20002
20458
  }
20003
20459
  return Result.ok(void 0);
20004
20460
  }
@@ -20016,7 +20472,7 @@ async function pruneEmptyDirs(from, stopAt) {
20016
20472
  } catch {
20017
20473
  return;
20018
20474
  }
20019
- current = dirname2(current);
20475
+ current = dirname3(current);
20020
20476
  }
20021
20477
  }
20022
20478
 
@@ -20045,6 +20501,7 @@ function computePlan(input2) {
20045
20501
  name: skill.name,
20046
20502
  state,
20047
20503
  version: skill.version,
20504
+ release: releaseOf(skill.files),
20048
20505
  action: actionFor(
20049
20506
  state,
20050
20507
  local !== void 0,
@@ -20058,14 +20515,18 @@ function computePlan(input2) {
20058
20515
  }
20059
20516
  return plans.sort((a, b) => a.name < b.name ? -1 : 1);
20060
20517
  }
20518
+ function versionLabel(plan) {
20519
+ return plan.release ?? `v${plan.version}`;
20520
+ }
20061
20521
  function actionFor(state, installed, diffCount, force) {
20522
+ if (state === "inherited") return installed ? "remove" : "inherited";
20062
20523
  if (state === "customized" && !force) return "skip";
20063
20524
  if (!installed) return "install";
20064
20525
  return diffCount === 0 ? "keep" : "update";
20065
20526
  }
20066
20527
  function planHasWrites(plans) {
20067
20528
  return plans.some(
20068
- (plan) => plan.action === "install" || plan.action === "update"
20529
+ (plan) => plan.action === "install" || plan.action === "update" || plan.action === "remove"
20069
20530
  );
20070
20531
  }
20071
20532
 
@@ -20122,8 +20583,9 @@ async function syncSurface(session, surface, options) {
20122
20583
  });
20123
20584
  printHeader(surface);
20124
20585
  printPlan(plans, blocked, surface);
20125
- printNotes(surface, inventory.value.items);
20126
- if (options.dryRun || !planHasWrites(plans)) return Result.ok(void 0);
20586
+ printNotes(surface, inventory.value.items, session.baseUrl);
20587
+ if (options.dryRun) return Result.ok(void 0);
20588
+ if (!planHasWrites(plans)) return clearPending(session, surfaceId);
20127
20589
  if (!options.yes && !await confirm("Apply?")) {
20128
20590
  process.stdout.write("Nothing applied.\n");
20129
20591
  return Result.ok(void 0);
@@ -20133,7 +20595,8 @@ async function syncSurface(session, surface, options) {
20133
20595
  const rescanned = await scanSurface(
20134
20596
  surface.descriptor.path,
20135
20597
  surface.descriptor.label,
20136
- surface.descriptor.machineId
20598
+ surface.descriptor.machineId,
20599
+ surface.descriptor.scope
20137
20600
  );
20138
20601
  const reposted = await postInventory(session, rescanned);
20139
20602
  if (reposted.isFailure) return Result.fail(reposted.error);
@@ -20143,12 +20606,14 @@ async function maybeAdopt(session, surface, inventory, options) {
20143
20606
  const importable = inventory.items.filter((item) => item.importable);
20144
20607
  if (importable.length === 0 || options.dryRun) return Result.ok(inventory);
20145
20608
  const wanted = options.adopt || !options.yes && await confirm(
20146
- `Adopt ${plural(importable.length)} found here as approved in your directory?`
20609
+ `Adopt ${plural(importable.length, "skill")} found here as approved in your directory?`
20147
20610
  );
20148
20611
  if (!wanted) return Result.ok(inventory);
20149
20612
  process.stdout.write(
20150
- dim(`adopting ${plural(importable.length)}, this can take a minute...
20151
- `)
20613
+ dim(
20614
+ `adopting ${plural(importable.length, "skill")}, this can take a minute...
20615
+ `
20616
+ )
20152
20617
  );
20153
20618
  const adopted = await apiRequest({
20154
20619
  session,
@@ -20168,7 +20633,7 @@ async function maybeAdopt(session, surface, inventory, options) {
20168
20633
  const names = adopted.value.adopted;
20169
20634
  process.stdout.write(
20170
20635
  names.length === 0 ? `${dim("nothing adopted")}
20171
- ` : `adopted ${plural(names.length)} as approved: ${names.join(", ")}
20636
+ ` : `adopted ${plural(names.length, "skill")} as approved: ${names.join(", ")}
20172
20637
  `
20173
20638
  );
20174
20639
  for (const skip of adopted.value.skipped) {
@@ -20178,11 +20643,26 @@ async function maybeAdopt(session, surface, inventory, options) {
20178
20643
  if (adopted.value.adopted.length === 0) return Result.ok(inventory);
20179
20644
  return postInventory(session, surface);
20180
20645
  }
20181
- function plural(n) {
20182
- return `${n} skill${n === 1 ? "" : "s"}`;
20183
- }
20184
20646
  async function applyPlan(surface, plans, approved) {
20185
20647
  for (const plan of plans) {
20648
+ if (plan.action === "remove") {
20649
+ if (!exists(join8(globalSkillsRoot(), plan.name))) {
20650
+ process.stdout.write(
20651
+ `${plan.name}: no longer in ~/.claude/skills, keeping the copy here
20652
+ `
20653
+ );
20654
+ continue;
20655
+ }
20656
+ await rm4(join8(surface.descriptor.path, plan.name), {
20657
+ recursive: true,
20658
+ force: true
20659
+ });
20660
+ process.stdout.write(
20661
+ `removed ${plan.name} (inherited from ~/.claude/skills)
20662
+ `
20663
+ );
20664
+ continue;
20665
+ }
20186
20666
  if (plan.action !== "install" && plan.action !== "update") continue;
20187
20667
  const skill = approved.find((entry) => entry.name === plan.name);
20188
20668
  if (skill === void 0) continue;
@@ -20202,19 +20682,19 @@ async function applyPlan(surface, plans, approved) {
20202
20682
  });
20203
20683
  if (written.isFailure) return written;
20204
20684
  process.stdout.write(
20205
- `${plan.action === "install" ? "installed" : "updated"} ${plan.name} v${plan.version}
20685
+ `${plan.action === "install" ? "installed" : "updated"} ${plan.name} ${versionLabel(plan)}
20206
20686
  `
20207
20687
  );
20208
20688
  }
20209
20689
  return Result.ok(void 0);
20210
20690
  }
20211
20691
  async function writeTarget(root, name) {
20212
- const dir = join6(root, name);
20213
- const stats = await lstat(dir).catch(() => null);
20692
+ const dir = join8(root, name);
20693
+ const stats = await lstat2(dir).catch(() => null);
20214
20694
  if (stats === null || !stats.isSymbolicLink()) return Result.ok(dir);
20215
20695
  const target = await realpath2(dir).catch(() => null);
20216
- const home2 = await realpath2(homedir5()).catch(() => homedir5());
20217
- if (target === null || !isInside(target, home2) || isInside(target, await realpath2(root).catch(() => resolve6(root)))) {
20696
+ const home2 = await realpath2(homedir6()).catch(() => homedir6());
20697
+ if (target === null || !isInside(target, home2) || isInside(target, await realpath2(root).catch(() => resolve7(root)))) {
20218
20698
  return Result.fail(
20219
20699
  new CliError(
20220
20700
  "UNSAFE_LINK",
@@ -20225,7 +20705,7 @@ async function writeTarget(root, name) {
20225
20705
  return Result.ok(target);
20226
20706
  }
20227
20707
  async function containsSymlink(dir) {
20228
- const entries = await readdir3(dir, {
20708
+ const entries = await readdir4(dir, {
20229
20709
  recursive: true,
20230
20710
  withFileTypes: true
20231
20711
  }).catch(() => []);
@@ -20254,24 +20734,27 @@ async function clearPending(session, surfaceId) {
20254
20734
  return Result.ok(void 0);
20255
20735
  }
20256
20736
  function printPlan(plans, blocked, surface) {
20737
+ const listed = plans.filter((plan) => plan.action !== "inherited");
20257
20738
  const rows = [
20258
- ...plans.map((plan) => [
20739
+ ...listed.map((plan) => [
20259
20740
  plan.action,
20260
20741
  plan.name,
20261
20742
  originOf(surface, plan.name),
20262
- `v${plan.version}`,
20743
+ versionLabel(plan),
20263
20744
  detailOf(plan)
20264
20745
  ]),
20265
20746
  ...blocked.map((skill) => [
20266
20747
  accent("blocked"),
20267
20748
  skill.name,
20268
20749
  originOf(surface, skill.name),
20269
- `v${skill.version}`,
20750
+ versionLabel({ version: skill.version, release: releaseOf(skill.files) }),
20270
20751
  `org policy: ${skill.blockedReason ?? "no reason given"}`
20271
20752
  ])
20272
20753
  ];
20754
+ const inherited = plans.length - listed.length;
20273
20755
  if (rows.length === 0) {
20274
- process.stdout.write(`${dim("nothing to sync")}
20756
+ const note = inherited > 0 ? `nothing to sync, ${inherited} inherited from ~/.claude/skills` : "nothing to sync";
20757
+ process.stdout.write(`${dim(note)}
20275
20758
 
20276
20759
  `);
20277
20760
  return;
@@ -20297,6 +20780,11 @@ function printPlan(plans, blocked, surface) {
20297
20780
  [plans.filter((plan) => plan.action === "install").length, "to install"],
20298
20781
  [plans.filter((plan) => plan.action === "update").length, "to update"],
20299
20782
  [plans.filter((plan) => plan.action === "keep").length, "up to date"],
20783
+ [plans.filter((plan) => plan.action === "remove").length, "to remove"],
20784
+ [
20785
+ plans.filter((plan) => plan.action === "inherited").length,
20786
+ "inherited from ~/.claude/skills"
20787
+ ],
20300
20788
  [plans.filter((plan) => plan.action === "skip").length, "skipped"],
20301
20789
  [blocked.length, "blocked"]
20302
20790
  ];
@@ -20311,10 +20799,123 @@ function detailOf(plan) {
20311
20799
  if (plan.action === "skip") {
20312
20800
  return `${accent("customized locally")}, use --force to overwrite`;
20313
20801
  }
20802
+ if (plan.action === "remove") {
20803
+ return "duplicate of ~/.claude/skills, Claude Code loads it from there";
20804
+ }
20314
20805
  if (plan.action === "install") return `+${plan.added.length}`;
20315
20806
  return `+${plan.added.length} ~${plan.changed.length} -${plan.removed.length}`;
20316
20807
  }
20317
20808
 
20809
+ // src/commands/upgrade.ts
20810
+ import { spawn } from "node:child_process";
20811
+ import { homedir as homedir7 } from "node:os";
20812
+ import { resolve as resolve8 } from "node:path";
20813
+ function globalRoot() {
20814
+ return {
20815
+ label: "global",
20816
+ cwd: homedir7(),
20817
+ scope: "-g",
20818
+ lockPath: globalLockPath()
20819
+ };
20820
+ }
20821
+ function projectRoot(dir) {
20822
+ return {
20823
+ label: shortPath(resolve8(dir)),
20824
+ cwd: resolve8(dir),
20825
+ scope: "-p",
20826
+ lockPath: projectLockPath(dir)
20827
+ };
20828
+ }
20829
+ function upgradeRoots(path, all, registered) {
20830
+ const dir = resolve8(path ?? process.cwd());
20831
+ const hasProject = exists(projectSkillsRoot(dir));
20832
+ if (!all) return [hasProject ? projectRoot(dir) : globalRoot()];
20833
+ return [
20834
+ globalRoot(),
20835
+ ...projectDirs(
20836
+ hasProject || path !== void 0 ? dir : void 0,
20837
+ registered
20838
+ ).map(projectRoot)
20839
+ ];
20840
+ }
20841
+ function runSkills(root, names, yes) {
20842
+ const args = ["--yes", "skills", "update", ...names, root.scope];
20843
+ if (yes) args.push("-y");
20844
+ return new Promise((settle) => {
20845
+ const child = spawn("npx", args, { cwd: root.cwd, stdio: "inherit" });
20846
+ child.on("error", (cause) => {
20847
+ settle(
20848
+ Result.fail(
20849
+ new CliError(
20850
+ "NO_NPX",
20851
+ `Cannot run npx: ${cause.message}. The skills.sh CLI applies the update, install Node's npx and retry.`
20852
+ )
20853
+ )
20854
+ );
20855
+ });
20856
+ child.on("close", (code) => {
20857
+ settle(
20858
+ code === 0 || code === null ? Result.ok(void 0) : Result.fail(
20859
+ new CliError(
20860
+ "SKILLS_FAILED",
20861
+ `npx skills update exited with ${code} in ${root.cwd}.`
20862
+ )
20863
+ )
20864
+ );
20865
+ });
20866
+ });
20867
+ }
20868
+ async function upgrade(options) {
20869
+ const config2 = await readConfig();
20870
+ const registered = config2.isSuccess ? config2.value.projects : [];
20871
+ const named = options.names.length > 0;
20872
+ const roots = upgradeRoots(options.path, options.all || named, registered);
20873
+ let ran = 0;
20874
+ const found = /* @__PURE__ */ new Set();
20875
+ for (const root of roots) {
20876
+ const lock = await readLock(root.lockPath);
20877
+ const names = options.names.filter((name) => lock.has(name));
20878
+ if (named && names.length === 0) continue;
20879
+ if (!named && lock.size === 0) {
20880
+ process.stdout.write(
20881
+ dim(`${root.label}: no skills.sh lock here, nothing to upgrade
20882
+ `)
20883
+ );
20884
+ continue;
20885
+ }
20886
+ for (const name of names) found.add(name);
20887
+ process.stdout.write(
20888
+ `
20889
+ ${bold(root.label)} ${dim(named ? names.join(", ") : plural(lock.size, "skill"))}
20890
+ `
20891
+ );
20892
+ const result = await runSkills(root, names, options.yes);
20893
+ if (result.isFailure) return result;
20894
+ ran += 1;
20895
+ }
20896
+ const missing = options.names.filter((name) => !found.has(name));
20897
+ if (missing.length > 0) {
20898
+ return Result.fail(
20899
+ new CliError(
20900
+ "NOT_FROM_SKILLS_SH",
20901
+ `No skills.sh lock lists ${missing.join(", ")}. \`npx hubskillz doctor\` lists what is installed where.`
20902
+ )
20903
+ );
20904
+ }
20905
+ if (ran === 0) {
20906
+ process.stdout.write(
20907
+ dim("Nothing installed by `npx skills add` in these roots.\n")
20908
+ );
20909
+ return Result.ok(void 0);
20910
+ }
20911
+ process.stdout.write(
20912
+ dim(
20913
+ "\nUpstream content now sits on disk. Run `npx hubskillz status` to see it against your approved versions, `npx hubskillz sync` to go back to them.\n"
20914
+ )
20915
+ );
20916
+ return Result.ok(void 0);
20917
+ }
20918
+
20318
20919
  // src/index.ts
20319
20920
  var GLOBAL_FLAGS = [
20320
20921
  {
@@ -20376,6 +20977,54 @@ var COMMANDS = [
20376
20977
  { spec: "--force", help: "Overwrite skills you customized locally" }
20377
20978
  ]
20378
20979
  },
20980
+ {
20981
+ name: "upgrade",
20982
+ usage: "hubskillz upgrade [SKILL...] [--path DIR] [--all] [--yes]",
20983
+ summary: "Update skills.sh skills to their latest upstream",
20984
+ flags: [
20985
+ {
20986
+ spec: "--path DIR",
20987
+ help: "Project directory (default: current directory)"
20988
+ },
20989
+ { spec: "--all", help: "Global root and every registered project" },
20990
+ { spec: "-y, --yes", help: "Skip the skills.sh prompts" }
20991
+ ]
20992
+ },
20993
+ {
20994
+ name: "doctor",
20995
+ usage: "hubskillz doctor [--path DIR]",
20996
+ summary: "Check every local skills root for problems",
20997
+ flags: [
20998
+ {
20999
+ spec: "--path DIR",
21000
+ help: "Project directory (default: current directory)"
21001
+ }
21002
+ ]
21003
+ },
21004
+ {
21005
+ name: "move",
21006
+ usage: "hubskillz move <skill> <global|DIR> [--from global|DIR] [--force]",
21007
+ summary: "Move a skill between the global root and a project",
21008
+ flags: [
21009
+ {
21010
+ spec: "--from ROOT",
21011
+ help: "Which copy to move when the name exists twice"
21012
+ },
21013
+ { spec: "--force", help: "Replace a skill of the same name over there" }
21014
+ ]
21015
+ },
21016
+ {
21017
+ name: "publish",
21018
+ usage: "hubskillz publish <skill>",
21019
+ summary: "List the skill on your public page",
21020
+ flags: []
21021
+ },
21022
+ {
21023
+ name: "unpublish",
21024
+ usage: "hubskillz unpublish <skill>",
21025
+ summary: "Take the skill off your public page",
21026
+ flags: []
21027
+ },
20379
21028
  {
20380
21029
  name: "push",
20381
21030
  usage: "hubskillz push <skill-dir> [-m MESSAGE]",
@@ -20396,7 +21045,7 @@ function flagLines(flags) {
20396
21045
  }
20397
21046
  function usage() {
20398
21047
  const width = Math.max(...COMMANDS.map((command) => command.name.length));
20399
- return `${bold("hubskillz")} ${dim(`v${"0.3.1"}`)} keep your agent skills in sync
21048
+ return `${bold("hubskillz")} ${dim(`v${"0.4.0"}`)} keep your agent skills in sync
20400
21049
 
20401
21050
  ${bold("Usage")}
20402
21051
  hubskillz <command> [flags]
@@ -20463,6 +21112,7 @@ async function run() {
20463
21112
  "dry-run": { type: "boolean", default: false },
20464
21113
  force: { type: "boolean", default: false },
20465
21114
  message: { type: "string", short: "m" },
21115
+ from: { type: "string" },
20466
21116
  help: { type: "boolean", short: "h", default: false },
20467
21117
  version: { type: "boolean", short: "v", default: false }
20468
21118
  }
@@ -20475,7 +21125,7 @@ Run \`hubskillz help\` for usage.`)
20475
21125
  );
20476
21126
  }
20477
21127
  if (values.version === true) {
20478
- process.stdout.write(`${"0.3.1"}
21128
+ process.stdout.write(`${"0.4.0"}
20479
21129
  `);
20480
21130
  return Result.ok(void 0);
20481
21131
  }
@@ -20506,6 +21156,50 @@ Run \`hubskillz help\` for usage.`)
20506
21156
  dryRun: values["dry-run"] === true,
20507
21157
  force: values.force === true
20508
21158
  });
21159
+ case "upgrade":
21160
+ return upgrade({
21161
+ names: positionals.slice(1),
21162
+ path: values.path,
21163
+ all: values.all === true,
21164
+ yes: values.yes === true
21165
+ });
21166
+ case "doctor":
21167
+ return doctor({ path: values.path });
21168
+ case "move": {
21169
+ const name = positionals[1];
21170
+ if (name === void 0) {
21171
+ return Result.fail(
21172
+ new CliError(
21173
+ "USAGE",
21174
+ "hubskillz move needs a skill name. Run `hubskillz help move`."
21175
+ )
21176
+ );
21177
+ }
21178
+ return move({
21179
+ name,
21180
+ to: positionals[2],
21181
+ from: values.from,
21182
+ path: values.path,
21183
+ force: values.force === true
21184
+ });
21185
+ }
21186
+ case "publish":
21187
+ case "unpublish": {
21188
+ const name = positionals[1];
21189
+ if (name === void 0) {
21190
+ return Result.fail(
21191
+ new CliError(
21192
+ "USAGE",
21193
+ `hubskillz ${command} needs a skill name. Run \`hubskillz help ${command}\`.`
21194
+ )
21195
+ );
21196
+ }
21197
+ return publish({
21198
+ baseUrl: values["base-url"],
21199
+ name,
21200
+ published: command === "publish"
21201
+ });
21202
+ }
20509
21203
  case "projects":
20510
21204
  return projects({
20511
21205
  action: positionals[1],