hubskillz 0.3.1 → 1.0.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 +73 -13
  2. package/dist/index.js +1093 -726
  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,63 @@ 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
+
19147
+ // src/commands/doctor.ts
19148
+ import { existsSync as existsSync2 } from "node:fs";
19149
+ import { readdir as readdir2 } from "node:fs/promises";
19150
+ import { hostname as hostname5 } from "node:os";
19151
+ import { basename as basename2, dirname as dirname2, join as join4 } from "node:path";
19152
+
19153
+ // src/config.ts
19154
+ import { randomUUID } from "node:crypto";
19155
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
19156
+ import { homedir, hostname as hostname3 } from "node:os";
19157
+ import { join } from "node:path";
19158
+
19087
19159
  // src/errors.ts
19088
19160
  var CliError = class extends DomainError {
19089
19161
  constructor(code, message) {
@@ -19099,75 +19171,7 @@ function toCliError(cause) {
19099
19171
  return new CliError("UNEXPECTED", String(cause));
19100
19172
  }
19101
19173
 
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
19174
  // 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
19175
  var DEFAULT_BASE_URL = "https://api.hubskillz.com";
19172
19176
  var configSchema = external_exports.object({
19173
19177
  baseUrl: external_exports.string().min(1),
@@ -19239,6 +19243,9 @@ async function machineId() {
19239
19243
  const existing = await readConfig();
19240
19244
  return existing.isSuccess ? existing.value.machineId : randomUUID();
19241
19245
  }
19246
+ function webOrigin(baseUrl) {
19247
+ return baseUrl === DEFAULT_BASE_URL ? "https://hubskillz.com" : baseUrl;
19248
+ }
19242
19249
  function resolveBaseUrl(flag, fromConfig) {
19243
19250
  const env = process.env["HUBSKILLZ_BASE_URL"];
19244
19251
  const chosen = flag ?? env ?? fromConfig ?? DEFAULT_BASE_URL;
@@ -19246,22 +19253,24 @@ function resolveBaseUrl(flag, fromConfig) {
19246
19253
  }
19247
19254
 
19248
19255
  // 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
- }
19256
+ import { homedir as homedir2 } from "node:os";
19257
+ import { styleText } from "node:util";
19258
+ var ANSI_PATTERN = /\u001B\[[0-9;]*m/gu;
19257
19259
  function bold(text) {
19258
- return wrap("1", text);
19260
+ return styleText("bold", text);
19259
19261
  }
19260
19262
  function dim(text) {
19261
- return wrap("2", text);
19263
+ return styleText("dim", text);
19262
19264
  }
19263
19265
  function accent(text) {
19264
- return wrap("33", text);
19266
+ return styleText("yellow", text);
19267
+ }
19268
+ function shortPath(path) {
19269
+ const home2 = homedir2();
19270
+ return path === home2 || path.startsWith(home2 + "/") ? `~${path.slice(home2.length)}` : path;
19271
+ }
19272
+ function plural(count, noun) {
19273
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
19265
19274
  }
19266
19275
  function table(headers, rows) {
19267
19276
  const widths = headers.map(
@@ -19278,107 +19287,604 @@ function table(headers, rows) {
19278
19287
  function visibleLength(text) {
19279
19288
  return text.replace(ANSI_PATTERN, "").length;
19280
19289
  }
19281
- function shortHash(hash2) {
19282
- return hash2 === void 0 ? "-" : hash2.slice(0, 8);
19283
- }
19284
19290
 
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
- });
19291
+ // src/scan.ts
19292
+ import { createHash as createHash2 } from "node:crypto";
19293
+ import { existsSync, statSync } from "node:fs";
19294
+ import { readdir, readFile as readFile3, realpath } from "node:fs/promises";
19295
+ import { homedir as homedir4, hostname as hostname4 } from "node:os";
19296
+ import { basename, dirname, join as join3, resolve as resolve2 } from "node:path";
19297
+
19298
+ // src/lock.ts
19299
+ import { readFile as readFile2 } from "node:fs/promises";
19300
+ import { homedir as homedir3 } from "node:os";
19301
+ import { join as join2, resolve } from "node:path";
19302
+ var entrySchema = external_exports.object({
19303
+ source: external_exports.string().min(1),
19304
+ sourceType: external_exports.string(),
19305
+ skillPath: external_exports.string().min(1).optional(),
19306
+ skillFolderHash: external_exports.string().min(1).optional(),
19307
+ computedHash: external_exports.string().min(1).optional()
19308
+ });
19309
+ var lockSchema = external_exports.object({ skills: external_exports.record(external_exports.string(), external_exports.unknown()) });
19310
+ function globalLockPath() {
19311
+ return join2(homedir3(), ".agents", ".skill-lock.json");
19324
19312
  }
19325
- async function confirm(question) {
19326
- if (process.stdin.isTTY !== true) return false;
19327
- const rl = createInterface({ input: process.stdin, output: process.stdout });
19313
+ function projectLockPath(projectDir) {
19314
+ return join2(resolve(projectDir), "skills-lock.json");
19315
+ }
19316
+ async function readLock(path) {
19317
+ const map2 = /* @__PURE__ */ new Map();
19318
+ const raw = await readFile2(path, "utf8").catch(() => null);
19319
+ if (raw === null) return map2;
19320
+ let json2;
19328
19321
  try {
19329
- const answer = await rl.question(`${question} [y/N] `);
19330
- return answer.trim().toLowerCase() === "y";
19331
- } finally {
19332
- rl.close();
19322
+ json2 = JSON.parse(raw);
19323
+ } catch {
19324
+ return map2;
19333
19325
  }
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);
19326
+ const lock = lockSchema.safeParse(json2);
19327
+ if (!lock.success) return map2;
19328
+ for (const [name, value] of Object.entries(lock.data.skills)) {
19329
+ const entry = entrySchema.safeParse(value);
19330
+ if (!entry.success) continue;
19331
+ const { source, sourceType, skillPath } = entry.data;
19332
+ const hash2 = entry.data.skillFolderHash ?? entry.data.computedHash;
19333
+ if (sourceType !== "github" || skillPath === void 0) continue;
19334
+ if (hash2 === void 0) continue;
19335
+ map2.set(name, { source, skillPath, hash: hash2 });
19339
19336
  }
19340
- return Buffer.concat(chunks).toString("utf8");
19337
+ return map2;
19341
19338
  }
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
- }
19339
+
19340
+ // src/scan.ts
19341
+ function globalSkillsRoot() {
19342
+ return join3(homedir4(), ".claude", "skills");
19343
+ }
19344
+ function projectSkillsRoot(dir) {
19345
+ return join3(resolve2(dir), ".claude", "skills");
19346
+ }
19347
+ function globalSurfaceLabel() {
19348
+ return hostname4();
19349
+ }
19350
+ function projectSurfaceLabel(dir) {
19351
+ return `${basename(resolve2(dir))} (${hostname4()})`;
19352
+ }
19353
+ function exists(path) {
19354
+ return existsSync(path);
19355
+ }
19356
+ function lockPathFor(root) {
19357
+ const resolved = resolve2(root);
19358
+ if (resolved === globalSkillsRoot()) return globalLockPath();
19359
+ return projectLockPath(dirname(dirname(resolved)));
19360
+ }
19361
+ async function scanSurface(root, label, machine, scope = "project") {
19362
+ const lock = await readLock(lockPathFor(root));
19363
+ const skills = await scanSkills(root);
19364
+ return {
19365
+ descriptor: {
19366
+ kind: "claude-code-local",
19367
+ label,
19368
+ machineId: machine,
19369
+ path: resolve2(root),
19370
+ scope
19371
+ },
19372
+ skills: await Promise.all(skills.map((skill) => withUpstream(skill, lock)))
19373
+ };
19374
+ }
19375
+ async function withUpstream(skill, lock) {
19376
+ if (lock.size === 0) return skill;
19377
+ const names = [skill.name];
19378
+ if (skill.link) {
19379
+ const real = await realpath(skill.dir).catch(() => skill.dir);
19380
+ names.push(basename(real));
19354
19381
  }
19355
- return [...picked].sort((left, right) => left - right);
19382
+ const upstream = names.map((name) => lock.get(name)).find(Boolean);
19383
+ return upstream === void 0 ? skill : { ...skill, upstream };
19356
19384
  }
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 });
19385
+ async function scanSkills(root) {
19386
+ let entries;
19366
19387
  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();
19388
+ entries = await readdir(root, { withFileTypes: true });
19389
+ } catch {
19390
+ return [];
19391
+ }
19392
+ const skills = [];
19393
+ for (const entry of entries) {
19394
+ if (!isSkillFile(entry.name)) continue;
19395
+ const dir = join3(root, entry.name);
19396
+ const symlink = entry.isSymbolicLink();
19397
+ if (!entry.isDirectory() && !(symlink && isDir(dir))) continue;
19398
+ const skill = await scanSkillDir(dir);
19399
+ if (skill === null) continue;
19400
+ skills.push(symlink ? { ...skill, link: true } : skill);
19372
19401
  }
19402
+ return skills.sort((a, b) => a.name < b.name ? -1 : 1);
19373
19403
  }
19374
-
19375
- // src/quickstart.ts
19376
- function quickstart(config2) {
19377
- const signedIn = config2.isSuccess;
19378
- const synced = signedIn && config2.value.firstSyncAt !== void 0;
19379
- const step = (done, text, command) => ` ${done ? dim("[x]") : "[ ]"} ${done ? dim(text) : text}
19380
- ${dim("$")} ${done ? dim(command) : bold(command)}
19381
- `;
19404
+ async function scanSkillDir(dir) {
19405
+ const resolved = resolve2(dir);
19406
+ const walked = await walk(resolved, "", /* @__PURE__ */ new Set());
19407
+ if (walked.files.length === 0) return null;
19408
+ return {
19409
+ name: basename(resolved),
19410
+ dir: resolved,
19411
+ files: walked.files,
19412
+ contentHash: contentHash(walked.files),
19413
+ link: walked.link
19414
+ };
19415
+ }
19416
+ async function walk(dir, prefix, visited) {
19417
+ const real = await realpath(dir).catch(() => dir);
19418
+ if (visited.has(real)) return { files: [], link: false };
19419
+ visited.add(real);
19420
+ const files = [];
19421
+ let link = false;
19422
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
19423
+ for (const entry of entries) {
19424
+ const full = join3(dir, entry.name);
19425
+ const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
19426
+ if (!isSkillFile(relative)) continue;
19427
+ const symlink = entry.isSymbolicLink();
19428
+ if (entry.isDirectory() || symlink && isDir(full)) {
19429
+ const nested = await walk(full, relative, visited);
19430
+ files.push(...nested.files);
19431
+ link = link || symlink || nested.link;
19432
+ continue;
19433
+ }
19434
+ if (!entry.isFile() && !symlink) continue;
19435
+ const content = await readFile3(full, "utf8").catch(() => null);
19436
+ if (content === null || content.includes("\0") || content.length > MAX_FILE_CONTENT_CHARS) {
19437
+ continue;
19438
+ }
19439
+ files.push({
19440
+ path: relative,
19441
+ content,
19442
+ hash: createHash2("sha256").update(content).digest("hex"),
19443
+ size: Buffer.byteLength(content, "utf8")
19444
+ });
19445
+ }
19446
+ return { files: files.sort((a, b) => a.path < b.path ? -1 : 1), link };
19447
+ }
19448
+ function isDir(path) {
19449
+ try {
19450
+ return statSync(path, { throwIfNoEntry: false })?.isDirectory() === true;
19451
+ } catch {
19452
+ return false;
19453
+ }
19454
+ }
19455
+ function inventoryRequestOf(surface) {
19456
+ return {
19457
+ surface: surface.descriptor,
19458
+ skills: surface.skills.map(inventorySkillOf)
19459
+ };
19460
+ }
19461
+ function inventoryChunksOf(surface) {
19462
+ const groups = [];
19463
+ let group2 = [];
19464
+ let bytes = 0;
19465
+ for (const skill of surface.skills) {
19466
+ const item = inventorySkillOf(skill);
19467
+ const size = Buffer.byteLength(JSON.stringify(item), "utf8");
19468
+ if (group2.length > 0 && (bytes + size > MAX_INVENTORY_CHUNK_BYTES || group2.length >= MAX_SKILLS_PER_REQUEST)) {
19469
+ groups.push(group2);
19470
+ group2 = [];
19471
+ bytes = 0;
19472
+ }
19473
+ group2.push(item);
19474
+ bytes += size;
19475
+ }
19476
+ groups.push(group2);
19477
+ if (groups.length === 1) return [inventoryRequestOf(surface)];
19478
+ return groups.map((skills, index) => ({
19479
+ surface: surface.descriptor,
19480
+ skills,
19481
+ chunk: { index, total: groups.length }
19482
+ }));
19483
+ }
19484
+ function inventorySkillOf(skill) {
19485
+ const snapshot = skill.upstream === void 0 && skill.files.reduce((sum, file2) => sum + file2.content.length, 0) <= MAX_SNAPSHOT_CHARS;
19486
+ const item = {
19487
+ name: skill.name,
19488
+ contentHash: skill.contentHash,
19489
+ files: skill.files.map((file2) => {
19490
+ const entry = {
19491
+ path: file2.path,
19492
+ hash: file2.hash,
19493
+ size: file2.size
19494
+ };
19495
+ if (snapshot) entry.content = file2.content;
19496
+ return entry;
19497
+ })
19498
+ };
19499
+ if (skill.upstream !== void 0) item.upstream = skill.upstream;
19500
+ return item;
19501
+ }
19502
+
19503
+ // src/surfaces.ts
19504
+ import { resolve as resolve3 } from "node:path";
19505
+ async function localSurfaces(path, both, machineId2, registered = []) {
19506
+ const projectDir = resolve3(path ?? process.cwd());
19507
+ const projectRoot2 = projectSkillsRoot(projectDir);
19508
+ const hasProject = exists(projectRoot2);
19509
+ const project = async (dir) => scanSurface(
19510
+ projectSkillsRoot(dir),
19511
+ projectSurfaceLabel(dir),
19512
+ machineId2,
19513
+ "project"
19514
+ );
19515
+ const global = async () => scanSurface(globalSkillsRoot(), globalSurfaceLabel(), machineId2, "global");
19516
+ if (!both) return [hasProject ? await project(projectDir) : await global()];
19517
+ const surfaces = [await global()];
19518
+ const dirs = projectDirs(
19519
+ hasProject || path !== void 0 ? projectDir : void 0,
19520
+ registered
19521
+ );
19522
+ for (const dir of dirs) {
19523
+ if (exists(projectSkillsRoot(dir))) {
19524
+ surfaces.push(await project(dir));
19525
+ continue;
19526
+ }
19527
+ process.stdout.write(
19528
+ dim(
19529
+ `skipping ${dir}: no .claude/skills here, run \`hubskillz projects remove ${dir}\` to forget it
19530
+ `
19531
+ )
19532
+ );
19533
+ }
19534
+ return surfaces;
19535
+ }
19536
+ function projectDirs(current, registered) {
19537
+ const dirs = [current, ...registered].filter((dir) => dir !== void 0).map((dir) => resolve3(dir)).filter((dir) => projectSkillsRoot(dir) !== globalSkillsRoot());
19538
+ return [...new Set(dirs)];
19539
+ }
19540
+
19541
+ // src/commands/doctor.ts
19542
+ function whereOf(surface) {
19543
+ return surface.descriptor.scope === "global" ? "global" : basename2(dirname2(dirname2(surface.descriptor.path)));
19544
+ }
19545
+ function frontmatterOf(skill) {
19546
+ const md = skill.files.find((file2) => file2.path === SKILL_MD);
19547
+ if (md === void 0) return null;
19548
+ return parseSkillMd(md.content).frontmatter.map((entry) => entry.key);
19549
+ }
19550
+ function skillFindings(skill, where) {
19551
+ const keys = frontmatterOf(skill);
19552
+ if (keys === null) {
19553
+ return [
19554
+ {
19555
+ level: "error",
19556
+ skill: skill.name,
19557
+ where,
19558
+ problem: `no ${SKILL_MD}, nothing for an agent to load`
19559
+ }
19560
+ ];
19561
+ }
19562
+ const findings = [];
19563
+ if (!keys.includes("name")) {
19564
+ findings.push({
19565
+ level: "warn",
19566
+ skill: skill.name,
19567
+ where,
19568
+ problem: `${SKILL_MD} without a name in its frontmatter`
19569
+ });
19570
+ }
19571
+ if (!keys.includes("description")) {
19572
+ findings.push({
19573
+ level: "warn",
19574
+ skill: skill.name,
19575
+ where,
19576
+ problem: `${SKILL_MD} without a description, nothing says when to load it`
19577
+ });
19578
+ }
19579
+ return findings;
19580
+ }
19581
+ function duplicateFindings(surfaces) {
19582
+ const global = surfaces.find(
19583
+ (surface) => surface.descriptor.scope === "global"
19584
+ );
19585
+ if (global === void 0) return [];
19586
+ const findings = [];
19587
+ for (const surface of surfaces) {
19588
+ if (surface === global) continue;
19589
+ for (const skill of surface.skills) {
19590
+ const twin = global.skills.find((entry) => entry.name === skill.name);
19591
+ if (twin === void 0) continue;
19592
+ findings.push({
19593
+ level: "warn",
19594
+ skill: skill.name,
19595
+ where: whereOf(surface),
19596
+ problem: twin.contentHash === skill.contentHash ? "same content as ~/.claude/skills, safe to delete this copy" : "differs from ~/.claude/skills and wins over it here"
19597
+ });
19598
+ }
19599
+ }
19600
+ return findings;
19601
+ }
19602
+ function scatteredFindings(surfaces) {
19603
+ const projects2 = surfaces.filter(
19604
+ (surface) => surface.descriptor.scope !== "global"
19605
+ );
19606
+ const global = surfaces.find(
19607
+ (surface) => surface.descriptor.scope === "global"
19608
+ );
19609
+ const counts = /* @__PURE__ */ new Map();
19610
+ for (const surface of projects2) {
19611
+ for (const skill of surface.skills) {
19612
+ const key = `${skill.name}\0${skill.contentHash}`;
19613
+ const seen = counts.get(key) ?? /* @__PURE__ */ new Set();
19614
+ seen.add(surface.descriptor.path);
19615
+ counts.set(key, seen);
19616
+ }
19617
+ }
19618
+ const findings = [];
19619
+ for (const [key, paths] of counts) {
19620
+ const name = key.split("\0")[0] ?? "";
19621
+ if (paths.size < 2) continue;
19622
+ if (global?.skills.some((entry) => entry.name === name) === true) continue;
19623
+ findings.push({
19624
+ level: "warn",
19625
+ skill: name,
19626
+ where: `${paths.size} projects`,
19627
+ problem: `identical in every one, \`hubskillz move ${name} global\` covers them all`
19628
+ });
19629
+ }
19630
+ return findings;
19631
+ }
19632
+ async function brokenFindings(root, where) {
19633
+ const entries = await readdir2(root, { withFileTypes: true }).catch(() => []);
19634
+ const findings = [];
19635
+ for (const entry of entries) {
19636
+ if (!isSkillFile(entry.name)) continue;
19637
+ const dir = join4(root, entry.name);
19638
+ if (entry.isSymbolicLink() && !existsSync2(dir)) {
19639
+ findings.push({
19640
+ level: "error",
19641
+ skill: entry.name,
19642
+ where,
19643
+ problem: "broken symlink, the target is gone"
19644
+ });
19645
+ continue;
19646
+ }
19647
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
19648
+ if (await scanSkillDir(dir) === null) {
19649
+ findings.push({
19650
+ level: "error",
19651
+ skill: entry.name,
19652
+ where,
19653
+ problem: "empty folder, no readable file inside"
19654
+ });
19655
+ }
19656
+ }
19657
+ return findings;
19658
+ }
19659
+ function staleFindings(registered) {
19660
+ return registered.filter((dir) => !exists(projectSkillsRoot(dir))).map((dir) => ({
19661
+ level: "warn",
19662
+ skill: "-",
19663
+ where: shortPath(dir),
19664
+ problem: "registered without .claude/skills, `hubskillz projects remove` it"
19665
+ }));
19666
+ }
19667
+ async function doctor(options) {
19668
+ const config2 = await readConfig();
19669
+ const machine = config2.isSuccess ? config2.value.machineId : hostname5();
19670
+ const registered = config2.isSuccess ? config2.value.projects : [];
19671
+ const live = registered.filter((dir) => exists(projectSkillsRoot(dir)));
19672
+ const surfaces = await localSurfaces(options.path, true, machine, live);
19673
+ const findings = [...staleFindings(registered)];
19674
+ for (const surface of surfaces) {
19675
+ const where = whereOf(surface);
19676
+ findings.push(...await brokenFindings(surface.descriptor.path, where));
19677
+ for (const skill of surface.skills) {
19678
+ findings.push(...skillFindings(skill, where));
19679
+ }
19680
+ }
19681
+ findings.push(...duplicateFindings(surfaces), ...scatteredFindings(surfaces));
19682
+ printSurfaces(surfaces);
19683
+ printFindings(findings);
19684
+ return Result.ok(void 0);
19685
+ }
19686
+ function printSurfaces(surfaces) {
19687
+ for (const surface of surfaces) {
19688
+ process.stdout.write(
19689
+ `${bold(surface.descriptor.label)} ${dim(
19690
+ `${shortPath(surface.descriptor.path)}, ${plural(surface.skills.length, "skill")}`
19691
+ )}
19692
+ `
19693
+ );
19694
+ }
19695
+ }
19696
+ function group(findings) {
19697
+ const groups = /* @__PURE__ */ new Map();
19698
+ for (const finding of findings) {
19699
+ const key = `${finding.level}\0${finding.where}\0${finding.problem}`;
19700
+ const found = groups.get(key) ?? [];
19701
+ found.push(finding);
19702
+ groups.set(key, found);
19703
+ }
19704
+ return groups;
19705
+ }
19706
+ function printFindings(findings) {
19707
+ if (findings.length === 0) {
19708
+ process.stdout.write(`
19709
+ ${dim("nothing to fix")}
19710
+ `);
19711
+ return;
19712
+ }
19713
+ const groups = [...group(findings).values()].sort(
19714
+ (left, right) => left[0]?.level === right[0]?.level ? 0 : left[0]?.level === "error" ? -1 : 1
19715
+ );
19716
+ process.stdout.write(
19717
+ `
19718
+ ${table(
19719
+ ["LEVEL", "SKILL", "WHERE", "PROBLEM"],
19720
+ groups.map(([first, ...rest]) => [
19721
+ first?.level === "error" ? accent("error") : dim("warn"),
19722
+ rest.length === 0 ? first?.skill ?? "-" : plural(rest.length + 1, "skill"),
19723
+ first?.where ?? "-",
19724
+ first?.problem ?? ""
19725
+ ])
19726
+ )}
19727
+ `
19728
+ );
19729
+ for (const found of groups) {
19730
+ if (found.length < 2) continue;
19731
+ const first = found[0];
19732
+ if (first === void 0) continue;
19733
+ process.stdout.write(
19734
+ `
19735
+ ${dim(`${first.where}, ${first.problem}`)}
19736
+ ${found.map((finding) => finding.skill).join(", ")}
19737
+ `
19738
+ );
19739
+ }
19740
+ const errors = findings.filter((finding) => finding.level === "error").length;
19741
+ process.stdout.write(
19742
+ `
19743
+ ${plural(findings.length, "problem")}, ${errors} to fix by hand
19744
+ `
19745
+ );
19746
+ }
19747
+
19748
+ // src/api.ts
19749
+ async function apiRequest(request) {
19750
+ const url2 = `${request.session.baseUrl}${request.path}`;
19751
+ const bearer = `Bearer ${request.session.token}`;
19752
+ let response;
19753
+ try {
19754
+ response = await fetch(url2, {
19755
+ method: request.method,
19756
+ headers: request.body === void 0 ? { accept: "application/json", authorization: bearer } : {
19757
+ accept: "application/json",
19758
+ authorization: bearer,
19759
+ "content-type": "application/json"
19760
+ },
19761
+ body: request.body === void 0 ? void 0 : JSON.stringify(request.body)
19762
+ });
19763
+ } catch (cause) {
19764
+ const detail = cause instanceof Error ? cause.message : String(cause);
19765
+ return Result.fail(
19766
+ new CliError("NETWORK", `Cannot reach ${url2}: ${detail}`)
19767
+ );
19768
+ }
19769
+ const text = await response.text();
19770
+ if (response.status === 413) {
19771
+ return Result.fail(
19772
+ new CliError(
19773
+ "HTTP",
19774
+ `${request.method} ${request.path} failed: the inventory is too large (HTTP 413). Reduce the number of skills or roots, or use project roots.`
19775
+ )
19776
+ );
19777
+ }
19778
+ if (!response.ok) {
19779
+ const message = decodeBody(text, apiErrorSchema)?.message ?? `The server returned HTTP ${response.status} with an unexpected body. Try again in a minute.`;
19780
+ return Result.fail(
19781
+ new CliError(
19782
+ response.status === 401 ? "UNAUTHORIZED" : response.status === 403 ? "FORBIDDEN" : "HTTP",
19783
+ `${request.method} ${request.path} failed: ${message}`
19784
+ )
19785
+ );
19786
+ }
19787
+ const parsed = decodeBody(text, request.schema);
19788
+ if (parsed === null) {
19789
+ return Result.fail(
19790
+ new CliError(
19791
+ "PROTOCOL",
19792
+ `${request.method} ${request.path} returned an unexpected payload (HTTP ${response.status}, not JSON or wrong shape).`
19793
+ )
19794
+ );
19795
+ }
19796
+ return Result.ok(parsed);
19797
+ }
19798
+ function decodeBody(text, schema) {
19799
+ try {
19800
+ const parsed = schema.safeParse(JSON.parse(text === "" ? "null" : text));
19801
+ return parsed.success ? parsed.data : null;
19802
+ } catch {
19803
+ return null;
19804
+ }
19805
+ }
19806
+
19807
+ // src/prompt.ts
19808
+ import { createInterface } from "node:readline/promises";
19809
+ import { text as readAll } from "node:stream/consumers";
19810
+ var ENTER = /* @__PURE__ */ new Set(["\r", "\n"]);
19811
+ var BACKSPACE = /* @__PURE__ */ new Set(["\b", "\x7F"]);
19812
+ var CTRL_C = "";
19813
+ async function promptSecret(label) {
19814
+ const input2 = process.stdin;
19815
+ if (input2.isTTY !== true) {
19816
+ return (await readAll(input2)).split("\n")[0]?.trim() ?? "";
19817
+ }
19818
+ process.stdout.write(label);
19819
+ input2.setRawMode(true);
19820
+ input2.resume();
19821
+ input2.setEncoding("utf8");
19822
+ return new Promise((settle, fail) => {
19823
+ let value = "";
19824
+ const cleanup = () => {
19825
+ input2.setRawMode(false);
19826
+ input2.pause();
19827
+ input2.off("data", onData);
19828
+ process.stdout.write("\n");
19829
+ };
19830
+ const onData = (chunk) => {
19831
+ for (const char of chunk) {
19832
+ if (ENTER.has(char)) {
19833
+ cleanup();
19834
+ settle(value.trim());
19835
+ return;
19836
+ }
19837
+ if (char === CTRL_C) {
19838
+ cleanup();
19839
+ fail(new Error("Interrupted"));
19840
+ return;
19841
+ }
19842
+ value = BACKSPACE.has(char) ? value.slice(0, -1) : value + char;
19843
+ }
19844
+ };
19845
+ input2.on("data", onData);
19846
+ });
19847
+ }
19848
+ function parseSelection(answer, count) {
19849
+ const text = answer.trim().toLowerCase();
19850
+ if (text === "" || text === "all" || text === "a") {
19851
+ return Array.from({ length: count }, (_, index) => index);
19852
+ }
19853
+ if (text === "none" || text === "n") return [];
19854
+ const picked = /* @__PURE__ */ new Set();
19855
+ for (const token of text.split(/[\s,]+/u)) {
19856
+ const index = Number(token) - 1;
19857
+ if (Number.isInteger(index) && index >= 0 && index < count) {
19858
+ picked.add(index);
19859
+ }
19860
+ }
19861
+ return [...picked].sort((left, right) => left - right);
19862
+ }
19863
+ async function selectMany(question, choices) {
19864
+ if (process.stdin.isTTY !== true) return choices;
19865
+ process.stdout.write(`${question}
19866
+ `);
19867
+ choices.forEach((choice, index) => {
19868
+ process.stdout.write(` ${index + 1}. ${choice}
19869
+ `);
19870
+ });
19871
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
19872
+ try {
19873
+ const answer = await rl.question("Select [all]: ");
19874
+ const picked = new Set(parseSelection(answer, choices.length));
19875
+ return choices.filter((_, index) => picked.has(index));
19876
+ } finally {
19877
+ rl.close();
19878
+ }
19879
+ }
19880
+
19881
+ // src/quickstart.ts
19882
+ function quickstart(config2) {
19883
+ const signedIn = config2.isSuccess;
19884
+ const synced = signedIn && config2.value.firstSyncAt !== void 0;
19885
+ const step = (done, text, command) => ` ${done ? dim("[x]") : "[ ]"} ${done ? dim(text) : text}
19886
+ ${dim("$")} ${done ? dim(command) : bold(command)}
19887
+ `;
19382
19888
  return `${bold("Quickstart")}
19383
19889
  ` + step(
19384
19890
  signedIn,
@@ -19390,7 +19896,7 @@ function quickstart(config2) {
19390
19896
  "hubskillz status"
19391
19897
  ) + step(
19392
19898
  synced,
19393
- "Install the approved set everywhere, adopt what the directory lacks",
19899
+ "Upload every skill of this machine to your directory",
19394
19900
  "hubskillz sync --all"
19395
19901
  ) + `
19396
19902
  ${dim("Run `hubskillz help` for every command, `hubskillz help <command>` for its flags.")}
@@ -19456,234 +19962,130 @@ async function logout() {
19456
19962
  return Result.ok(void 0);
19457
19963
  }
19458
19964
 
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
- };
19965
+ // src/commands/move.ts
19966
+ import { existsSync as existsSync3 } from "node:fs";
19967
+ import { cp, lstat, mkdir as mkdir2, rename, rm as rm2 } from "node:fs/promises";
19968
+ import { join as join5, resolve as resolve4 } from "node:path";
19969
+ function skillsRootOf(target) {
19970
+ return target === "global" ? globalSkillsRoot() : projectSkillsRoot(target);
19971
+ }
19972
+ function searchRoots(path, from) {
19973
+ if (from !== void 0) return [skillsRootOf(from)];
19974
+ return [
19975
+ .../* @__PURE__ */ new Set([projectSkillsRoot(path ?? process.cwd()), globalSkillsRoot()])
19976
+ ];
19553
19977
  }
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));
19978
+ function pickSource(name, roots, destRoot, holds) {
19979
+ const found = roots.filter((root) => root !== destRoot && holds(root));
19980
+ const [first, second] = found;
19981
+ if (first !== void 0 && second === void 0) return Result.ok(first);
19982
+ if (first === void 0) {
19983
+ return Result.fail(
19984
+ new CliError(
19985
+ "SKILL_NOT_FOUND",
19986
+ `No skill named ${name} in ${roots.map(shortPath).join(" or ")}.`
19987
+ )
19988
+ );
19560
19989
  }
19561
- const upstream = names.map((name) => lock.get(name)).find(Boolean);
19562
- return upstream === void 0 ? skill : { ...skill, upstream };
19990
+ return Result.fail(
19991
+ new CliError(
19992
+ "AMBIGUOUS_SOURCE",
19993
+ `${name} exists in ${found.map(shortPath).join(" and ")}. Pass --from to say which one to move.`
19994
+ )
19995
+ );
19563
19996
  }
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);
19997
+ async function moveSkill(source, dest, force) {
19998
+ if (await lstat(source).catch(() => null) === null) {
19999
+ return Result.fail(new CliError("SKILL_NOT_FOUND", `${source} is gone.`));
19580
20000
  }
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;
20001
+ if (existsSync3(dest) || await lstat(dest).catch(() => null) !== null) {
20002
+ if (!force) {
20003
+ return Result.fail(
20004
+ new CliError(
20005
+ "DESTINATION_EXISTS",
20006
+ `${shortPath(dest)} already exists. Pass --force to replace it.`
20007
+ )
20008
+ );
19617
20009
  }
19618
- files.push({
19619
- path: relative,
19620
- content,
19621
- hash: createHash2("sha256").update(content).digest("hex"),
19622
- size: Buffer.byteLength(content, "utf8")
19623
- });
20010
+ await rm2(dest, { recursive: true, force: true });
19624
20011
  }
19625
- return { files: files.sort((a, b) => a.path < b.path ? -1 : 1), link };
19626
- }
19627
- async function isDir(path) {
19628
- return stat(path).then((stats) => stats.isDirectory()).catch(() => false);
19629
- }
19630
- function inventoryRequestOf(surface) {
19631
- return {
19632
- surface: surface.descriptor,
19633
- skills: surface.skills.map(inventorySkillOf)
19634
- };
19635
- }
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;
20012
+ await mkdir2(resolve4(dest, ".."), { recursive: true });
20013
+ try {
20014
+ await rename(source, dest);
20015
+ } catch (cause) {
20016
+ const code = cause instanceof Error && "code" in cause ? cause.code : null;
20017
+ if (code !== "EXDEV") {
20018
+ const detail = cause instanceof Error ? cause.message : String(cause);
20019
+ return Result.fail(new CliError("MOVE_FAILED", detail));
19647
20020
  }
19648
- group.push(item);
19649
- bytes += size;
20021
+ await cp(source, dest, { recursive: true, verbatimSymlinks: true });
20022
+ await rm2(source, { recursive: true, force: true });
19650
20023
  }
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
- }));
20024
+ return Result.ok(void 0);
19658
20025
  }
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;
20026
+ async function sameContent(left, right) {
20027
+ const [one, two] = await Promise.all([
20028
+ scanSkillDir(left),
20029
+ scanSkillDir(right)
20030
+ ]);
20031
+ return one !== null && two !== null && one.contentHash === two.contentHash;
20032
+ }
20033
+ async function move(options) {
20034
+ if (options.to === void 0) {
20035
+ return Result.fail(
20036
+ new CliError(
20037
+ "USAGE",
20038
+ "hubskillz move needs a destination: `global` or a project directory. Run `hubskillz help move`."
20039
+ )
20040
+ );
20041
+ }
20042
+ const destRoot = skillsRootOf(options.to);
20043
+ const roots = searchRoots(options.path, options.from);
20044
+ const source = pickSource(
20045
+ options.name,
20046
+ roots,
20047
+ destRoot,
20048
+ (root) => existsSync3(join5(root, options.name))
20049
+ );
20050
+ if (source.isFailure) return Result.fail(source.error);
20051
+ const dest = join5(destRoot, options.name);
20052
+ const origin = join5(source.value, options.name);
20053
+ if (!options.force && await sameContent(origin, dest)) {
20054
+ await rm2(origin, { recursive: true, force: true });
20055
+ process.stdout.write(
20056
+ `${shortPath(dest)} already holds this exact skill
20057
+ removed the copy in ${shortPath(origin)}
20058
+ `
20059
+ );
20060
+ return Result.ok(void 0);
20061
+ }
20062
+ const moved = await moveSkill(origin, dest, options.force);
20063
+ if (moved.isFailure) return moved;
20064
+ process.stdout.write(
20065
+ `moved ${options.name}
20066
+ ${shortPath(origin)}
20067
+ ${shortPath(dest)}
20068
+ ${dim("run npx hubskillz status to report the new layout")}
20069
+ `
20070
+ );
20071
+ return Result.ok(void 0);
19676
20072
  }
19677
20073
 
20074
+ // src/commands/projects.ts
20075
+ import { resolve as resolve6 } from "node:path";
20076
+
19678
20077
  // src/discover.ts
20078
+ import { readdir as readdir3 } from "node:fs/promises";
20079
+ import { homedir as homedir5 } from "node:os";
20080
+ import { join as join6, resolve as resolve5 } from "node:path";
19679
20081
  var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "Library", ".Trash", ".cache"]);
19680
- async function discoverProjects(roots = [homedir4()], maxDepth = 3) {
20082
+ async function discoverProjects(roots = [homedir5()], maxDepth = 3) {
19681
20083
  const found = /* @__PURE__ */ new Set();
19682
20084
  const walk2 = async (dir, depth) => {
19683
20085
  if (depth > maxDepth) return;
19684
20086
  let entries;
19685
20087
  try {
19686
- entries = await readdir2(dir, { withFileTypes: true });
20088
+ entries = await readdir3(dir, { withFileTypes: true });
19687
20089
  } catch {
19688
20090
  return;
19689
20091
  }
@@ -19691,15 +20093,15 @@ async function discoverProjects(roots = [homedir4()], maxDepth = 3) {
19691
20093
  const name = entry.name;
19692
20094
  if (!entry.isDirectory() || SKIP.has(name)) continue;
19693
20095
  if (name.startsWith(".") && name !== ".claude") continue;
19694
- const child = join4(dir, name);
20096
+ const child = join6(dir, name);
19695
20097
  if (name === ".claude" && depth > 0) {
19696
- if (await exists(projectSkillsRoot(dir))) found.add(dir);
20098
+ if (exists(projectSkillsRoot(dir))) found.add(dir);
19697
20099
  continue;
19698
20100
  }
19699
20101
  await walk2(child, depth + 1);
19700
20102
  }
19701
20103
  };
19702
- for (const root of roots) await walk2(resolve3(root), 0);
20104
+ for (const root of roots) await walk2(resolve5(root), 0);
19703
20105
  return [...found].sort();
19704
20106
  }
19705
20107
 
@@ -19716,7 +20118,7 @@ async function discoverAndRegister(config2, yes) {
19716
20118
  const projects2 = [...config2.projects, ...chosen];
19717
20119
  await writeConfig({ ...config2, projects: projects2 });
19718
20120
  process.stdout.write(
19719
- `Registered ${chosen.length} project${chosen.length === 1 ? "" : "s"}. They are now part of \`hubskillz sync --all\`.
20121
+ `Registered ${plural(chosen.length, "project")}. They are now part of \`hubskillz sync --all\`.
19720
20122
  `
19721
20123
  );
19722
20124
  return projects2;
@@ -19725,7 +20127,7 @@ async function projects(options) {
19725
20127
  const config2 = await readConfig();
19726
20128
  if (config2.isFailure) return Result.fail(config2.error);
19727
20129
  const current = config2.value.projects;
19728
- const dir = resolve4(options.dir ?? process.cwd());
20130
+ const dir = resolve6(options.dir ?? process.cwd());
19729
20131
  switch (options.action ?? "list") {
19730
20132
  case "list": {
19731
20133
  if (current.length === 0) {
@@ -19741,7 +20143,7 @@ async function projects(options) {
19741
20143
  return Result.ok(void 0);
19742
20144
  }
19743
20145
  case "add": {
19744
- if (!await exists(projectSkillsRoot(dir))) {
20146
+ if (!exists(projectSkillsRoot(dir))) {
19745
20147
  return Result.fail(
19746
20148
  new CliError(
19747
20149
  "NO_SKILLS_ROOT",
@@ -19789,6 +20191,32 @@ async function projects(options) {
19789
20191
  }
19790
20192
  }
19791
20193
 
20194
+ // src/commands/publish.ts
20195
+ async function publish(options) {
20196
+ const config2 = await readConfig();
20197
+ if (config2.isFailure) return Result.fail(config2.error);
20198
+ const session = {
20199
+ baseUrl: resolveBaseUrl(options.baseUrl, config2.value.baseUrl),
20200
+ token: config2.value.token
20201
+ };
20202
+ const result = await apiRequest({
20203
+ session,
20204
+ method: "POST",
20205
+ path: "/api/cli/publish",
20206
+ schema: publishResponseSchema,
20207
+ body: { name: options.name, published: options.published }
20208
+ });
20209
+ if (result.isFailure) return Result.fail(result.error);
20210
+ const page = `${webOrigin(session.baseUrl)}/@${result.value.handle}`;
20211
+ process.stdout.write(
20212
+ options.published ? `${options.name} is public on ${page}
20213
+ ` : `${options.name} is off your public page.
20214
+ ${dim(page)}
20215
+ `
20216
+ );
20217
+ return Result.ok(void 0);
20218
+ }
20219
+
19792
20220
  // src/commands/push.ts
19793
20221
  async function push(options) {
19794
20222
  const config2 = await readConfig();
@@ -19827,47 +20255,6 @@ ${dim(`skill ${draft.value.skillId} version ${draft.value.versionId}`)}
19827
20255
  return Result.ok(void 0);
19828
20256
  }
19829
20257
 
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
20258
  // src/commands/status.ts
19872
20259
  async function status(options) {
19873
20260
  const config2 = await readConfig();
@@ -19886,7 +20273,7 @@ async function status(options) {
19886
20273
  for (const surface of surfaces) {
19887
20274
  const inventory = await postInventory(session, surface);
19888
20275
  if (inventory.isFailure) return Result.fail(inventory.error);
19889
- printSurface(surface, inventory.value);
20276
+ printSurface(surface, inventory.value, session.baseUrl);
19890
20277
  }
19891
20278
  if (quickstartPending(config2)) {
19892
20279
  process.stdout.write(`
@@ -19896,7 +20283,8 @@ ${quickstart(config2)}`);
19896
20283
  }
19897
20284
  async function postInventory(session, surface) {
19898
20285
  const chunks = inventoryChunksOf(surface);
19899
- let merged;
20286
+ let surfaceId;
20287
+ const items = [];
19900
20288
  for (const body of chunks) {
19901
20289
  const posted = await apiRequest({
19902
20290
  session,
@@ -19906,20 +20294,34 @@ async function postInventory(session, surface) {
19906
20294
  body
19907
20295
  });
19908
20296
  if (posted.isFailure) return posted;
19909
- merged = merged === void 0 ? posted.value : { ...merged, items: [...merged.items, ...posted.value.items] };
20297
+ surfaceId ??= posted.value.surfaceId;
20298
+ items.push(...posted.value.items);
20299
+ }
20300
+ if (surfaceId === void 0) return Result.ok({ surfaceId: "", items: [] });
20301
+ return Result.ok({ surfaceId, items: mergeItems(items) });
20302
+ }
20303
+ function mergeItems(items) {
20304
+ const byName = /* @__PURE__ */ new Map();
20305
+ for (const item of items) {
20306
+ const seen = byName.get(item.name);
20307
+ const better = seen === void 0 || seen.installedHash === void 0 && item.installedHash !== void 0;
20308
+ if (better) byName.set(item.name, item);
19910
20309
  }
19911
- return Result.ok(merged ?? { surfaceId: "", items: [] });
20310
+ return [...byName.values()];
19912
20311
  }
19913
20312
  function originOf(surface, name) {
19914
20313
  const skill = surface.skills.find((entry) => entry.name === name);
19915
20314
  if (skill === void 0) return "-";
19916
20315
  return skill.upstream === void 0 ? "private" : `skills.sh ${skill.upstream.source}`;
19917
20316
  }
19918
- function printNotes(surface, items) {
20317
+ function reviewUrl(baseUrl) {
20318
+ return `${webOrigin(baseUrl)}/app`;
20319
+ }
20320
+ function printNotes(surface, items, baseUrl) {
19919
20321
  const importable = items.filter((item) => item.importable).map((item) => item.name);
19920
20322
  if (importable.length > 0) {
19921
20323
  process.stdout.write(
19922
- `${dim(`${importable.length} not in the directory yet, run npx hubskillz sync to adopt them:`)} ${importable.join(", ")}
20324
+ `${dim(`${importable.length} not in your directory yet, run npx hubskillz sync to add them:`)} ${importable.join(", ")}
19923
20325
  `
19924
20326
  );
19925
20327
  }
@@ -19927,6 +20329,10 @@ function printNotes(surface, items) {
19927
20329
  if (ahead.length > 0) {
19928
20330
  process.stdout.write(
19929
20331
  `${dim("upstream ahead of approved version, waiting for review:")} ${ahead.join(", ")}
20332
+ `
20333
+ );
20334
+ process.stdout.write(
20335
+ `${dim(`review and approve at ${reviewUrl(baseUrl)}`)}
19930
20336
  `
19931
20337
  );
19932
20338
  }
@@ -19938,17 +20344,35 @@ ${bold(surface.descriptor.label)} ${dim(surface.descriptor.path)}
19938
20344
  `
19939
20345
  );
19940
20346
  }
19941
- function printSurface(surface, inventory) {
19942
- const rows = inventory.items.map((item) => [
20347
+ function printSurface(surface, inventory, baseUrl) {
20348
+ const inherited = inventory.items.filter(
20349
+ (item) => item.state === "inherited"
20350
+ ).length;
20351
+ const rows = inventory.items.filter((item) => item.state !== "inherited").map((item) => [
19943
20352
  item.name,
19944
20353
  originOf(surface, item.name),
19945
20354
  item.required ? `${item.state} ${dim("(required)")}` : item.state,
19946
- shortHash(item.installedHash),
20355
+ shortSha(item.installedHash, 8),
19947
20356
  item.approvedVersion === void 0 ? "-" : `v${item.approvedVersion}`
19948
20357
  ]);
19949
20358
  printHeader(surface);
20359
+ if (inherited > 0) {
20360
+ process.stdout.write(
20361
+ `${dim(`${plural(inherited, "skill")} inherited from ~/.claude/skills`)}
20362
+ `
20363
+ );
20364
+ }
20365
+ const duplicates = inventory.items.filter(
20366
+ (item) => item.state === "inherited" && item.installedHash !== void 0
20367
+ ).length;
20368
+ if (duplicates > 0) {
20369
+ process.stdout.write(
20370
+ `${dim(`${duplicates} duplicate cop${duplicates === 1 ? "y" : "ies"} of ~/.claude/skills here: remove ${duplicates === 1 ? "it" : "them"} or keep ${duplicates === 1 ? "it" : "them"} on purpose`)}
20371
+ `
20372
+ );
20373
+ }
19950
20374
  if (rows.length === 0) {
19951
- process.stdout.write(`${dim("no skills")}
20375
+ if (inherited === 0) process.stdout.write(`${dim("no skills")}
19952
20376
  `);
19953
20377
  return;
19954
20378
  }
@@ -19957,116 +20381,16 @@ function printSurface(surface, inventory) {
19957
20381
 
19958
20382
  `
19959
20383
  );
19960
- printNotes(surface, inventory.items);
19961
- }
19962
-
19963
- // 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";
19967
-
19968
- // 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";
19971
- async function applySkill(input2) {
19972
- for (const path of [...input2.files.map((f) => f.path), ...input2.remove]) {
19973
- if (!isSafeRelativePath(path)) {
19974
- return Result.fail(
19975
- new CliError(
19976
- "UNSAFE_PATH",
19977
- `Refusing to write outside the skill: ${path}`
19978
- )
19979
- );
19980
- }
19981
- }
19982
- await mkdir2(input2.dir, { recursive: true });
19983
- const staging = await mkdtemp(join5(dirname2(input2.dir), ".hubskillz-"));
19984
- try {
19985
- for (const file2 of input2.files) {
19986
- const staged = join5(staging, ...file2.path.split("/"));
19987
- await mkdir2(dirname2(staged), { recursive: true });
19988
- await writeFile2(staged, file2.content, "utf8");
19989
- }
19990
- 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);
19994
- }
19995
- 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);
19999
- }
20000
- } finally {
20001
- await rm2(staging, { recursive: true, force: true });
20002
- }
20003
- return Result.ok(void 0);
20004
- }
20005
- function isSafeRelativePath(path) {
20006
- if (path === "" || path.startsWith("/") || /^[a-zA-Z]:/u.test(path)) {
20007
- return false;
20008
- }
20009
- return !path.split(/[/\\]/u).includes("..");
20010
- }
20011
- async function pruneEmptyDirs(from, stopAt) {
20012
- let current = from;
20013
- while (current.startsWith(stopAt + sep)) {
20014
- try {
20015
- await rmdir(current);
20016
- } catch {
20017
- return;
20018
- }
20019
- current = dirname2(current);
20020
- }
20021
- }
20022
-
20023
- // src/plan.ts
20024
- function computePlan(input2) {
20025
- const plans = [];
20026
- for (const skill of input2.approved) {
20027
- const local = input2.local.find((entry) => entry.name === skill.name);
20028
- const item = input2.items.find((entry) => entry.name === skill.name);
20029
- const state = item?.state ?? (local === void 0 ? "missing" : "customized");
20030
- const localFiles = new Map(
20031
- (local?.files ?? []).map((file2) => [file2.path, file2.content])
20032
- );
20033
- const wanted = new Map(
20034
- skill.files.map((file2) => [file2.path, file2.content])
20384
+ printNotes(surface, inventory.items, baseUrl);
20385
+ const behind = inventory.items.filter(
20386
+ (item) => item.state === "drifted" || item.state === "missing"
20387
+ ).length;
20388
+ if (behind > 0) {
20389
+ process.stdout.write(
20390
+ `${dim(`${plural(behind, "skill")} behind the approved version: review at ${reviewUrl(baseUrl)}`)}
20391
+ `
20035
20392
  );
20036
- const added = [];
20037
- const changed = [];
20038
- for (const [path, content] of wanted) {
20039
- const current = localFiles.get(path);
20040
- if (current === void 0) added.push(path);
20041
- else if (current !== content) changed.push(path);
20042
- }
20043
- const removed = [...localFiles.keys()].filter((path) => !wanted.has(path));
20044
- plans.push({
20045
- name: skill.name,
20046
- state,
20047
- version: skill.version,
20048
- action: actionFor(
20049
- state,
20050
- local !== void 0,
20051
- added.length + changed.length + removed.length,
20052
- input2.force
20053
- ),
20054
- added: added.sort(),
20055
- changed: changed.sort(),
20056
- removed: removed.sort()
20057
- });
20058
20393
  }
20059
- return plans.sort((a, b) => a.name < b.name ? -1 : 1);
20060
- }
20061
- function actionFor(state, installed, diffCount, force) {
20062
- if (state === "customized" && !force) return "skip";
20063
- if (!installed) return "install";
20064
- return diffCount === 0 ? "keep" : "update";
20065
- }
20066
- function planHasWrites(plans) {
20067
- return plans.some(
20068
- (plan) => plan.action === "install" || plan.action === "update"
20069
- );
20070
20394
  }
20071
20395
 
20072
20396
  // src/commands/sync.ts
@@ -20085,10 +20409,10 @@ async function sync(options) {
20085
20409
  projects2
20086
20410
  );
20087
20411
  for (const surface of surfaces) {
20088
- const result = await syncSurface(session, surface, options);
20412
+ const result = await syncSurface(session, surface);
20089
20413
  if (result.isFailure) return result;
20090
20414
  }
20091
- if (!options.dryRun && config2.value.firstSyncAt === void 0 && process.env["HUBSKILLZ_TOKEN"] === void 0) {
20415
+ if (config2.value.firstSyncAt === void 0 && process.env["HUBSKILLZ_TOKEN"] === void 0) {
20092
20416
  const stored = await readConfig();
20093
20417
  if (stored.isSuccess && stored.value.firstSyncAt === void 0) {
20094
20418
  await writeConfig({
@@ -20100,55 +20424,22 @@ async function sync(options) {
20100
20424
  }
20101
20425
  return Result.ok(void 0);
20102
20426
  }
20103
- async function syncSurface(session, surface, options) {
20427
+ async function syncSurface(session, surface) {
20104
20428
  const first = await postInventory(session, surface);
20105
20429
  if (first.isFailure) return Result.fail(first.error);
20106
- const surfaceId = first.value.surfaceId;
20107
- const inventory = await maybeAdopt(session, surface, first.value, options);
20430
+ const inventory = await adoptImportable(session, surface, first.value);
20108
20431
  if (inventory.isFailure) return Result.fail(inventory.error);
20109
- const approved = await apiRequest({
20110
- session,
20111
- method: "GET",
20112
- path: `/api/cli/approved?surfaceId=${encodeURIComponent(surfaceId)}`,
20113
- schema: approvedResponseSchema
20114
- });
20115
- if (approved.isFailure) return Result.fail(approved.error);
20116
- const blocked = approved.value.skills.filter((skill) => skill.blocked);
20117
- const plans = computePlan({
20118
- items: inventory.value.items,
20119
- approved: approved.value.skills.filter((skill) => !skill.blocked),
20120
- local: surface.skills,
20121
- force: options.force
20122
- });
20123
- printHeader(surface);
20124
- printPlan(plans, blocked, surface);
20125
- printNotes(surface, inventory.value.items);
20126
- if (options.dryRun || !planHasWrites(plans)) return Result.ok(void 0);
20127
- if (!options.yes && !await confirm("Apply?")) {
20128
- process.stdout.write("Nothing applied.\n");
20129
- return Result.ok(void 0);
20130
- }
20131
- const applied = await applyPlan(surface, plans, approved.value.skills);
20132
- if (applied.isFailure) return applied;
20133
- const rescanned = await scanSurface(
20134
- surface.descriptor.path,
20135
- surface.descriptor.label,
20136
- surface.descriptor.machineId
20137
- );
20138
- const reposted = await postInventory(session, rescanned);
20139
- if (reposted.isFailure) return Result.fail(reposted.error);
20140
- return clearPending(session, surfaceId);
20432
+ printSurface(surface, inventory.value, session.baseUrl);
20433
+ return clearPending(session, first.value.surfaceId);
20141
20434
  }
20142
- async function maybeAdopt(session, surface, inventory, options) {
20435
+ async function adoptImportable(session, surface, inventory) {
20143
20436
  const importable = inventory.items.filter((item) => item.importable);
20144
- if (importable.length === 0 || options.dryRun) return Result.ok(inventory);
20145
- const wanted = options.adopt || !options.yes && await confirm(
20146
- `Adopt ${plural(importable.length)} found here as approved in your directory?`
20147
- );
20148
- if (!wanted) return Result.ok(inventory);
20437
+ if (importable.length === 0) return Result.ok(inventory);
20149
20438
  process.stdout.write(
20150
- dim(`adopting ${plural(importable.length)}, this can take a minute...
20151
- `)
20439
+ dim(
20440
+ `adding ${plural(importable.length, "skill")} to your directory, this can take a minute...
20441
+ `
20442
+ )
20152
20443
  );
20153
20444
  const adopted = await apiRequest({
20154
20445
  session,
@@ -20159,80 +20450,22 @@ async function maybeAdopt(session, surface, inventory, options) {
20159
20450
  });
20160
20451
  if (adopted.isFailure && adopted.error.code === "FORBIDDEN") {
20161
20452
  process.stdout.write(
20162
- `${dim("not adopted: ask a maintainer to adopt these skills")}
20453
+ `${dim("not added: ask a maintainer to adopt these skills")}
20163
20454
  `
20164
20455
  );
20165
20456
  return Result.ok(inventory);
20166
20457
  }
20167
20458
  if (adopted.isFailure) return Result.fail(adopted.error);
20168
- const names = adopted.value.adopted;
20169
- process.stdout.write(
20170
- names.length === 0 ? `${dim("nothing adopted")}
20171
- ` : `adopted ${plural(names.length)} as approved: ${names.join(", ")}
20172
- `
20173
- );
20174
20459
  for (const skip of adopted.value.skipped) {
20175
20460
  process.stdout.write(dim(`skipped ${skip.name} (${skip.code})
20176
20461
  `));
20177
20462
  }
20178
20463
  if (adopted.value.adopted.length === 0) return Result.ok(inventory);
20179
- return postInventory(session, surface);
20180
- }
20181
- function plural(n) {
20182
- return `${n} skill${n === 1 ? "" : "s"}`;
20183
- }
20184
- async function applyPlan(surface, plans, approved) {
20185
- for (const plan of plans) {
20186
- if (plan.action !== "install" && plan.action !== "update") continue;
20187
- const skill = approved.find((entry) => entry.name === plan.name);
20188
- if (skill === void 0) continue;
20189
- const dir = await writeTarget(surface.descriptor.path, plan.name);
20190
- if (dir.isFailure) return Result.fail(dir.error);
20191
- if (await containsSymlink(dir.value)) {
20192
- process.stdout.write(
20193
- `${plan.name}: contains a symlink, refusing to write
20194
- `
20195
- );
20196
- continue;
20197
- }
20198
- const written = await applySkill({
20199
- dir: dir.value,
20200
- files: skill.files,
20201
- remove: plan.removed
20202
- });
20203
- if (written.isFailure) return written;
20204
- process.stdout.write(
20205
- `${plan.action === "install" ? "installed" : "updated"} ${plan.name} v${plan.version}
20464
+ process.stdout.write(
20465
+ `added ${plural(adopted.value.adopted.length, "skill")} to your directory: ${adopted.value.adopted.join(", ")}
20206
20466
  `
20207
- );
20208
- }
20209
- return Result.ok(void 0);
20210
- }
20211
- async function writeTarget(root, name) {
20212
- const dir = join6(root, name);
20213
- const stats = await lstat(dir).catch(() => null);
20214
- if (stats === null || !stats.isSymbolicLink()) return Result.ok(dir);
20215
- 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)))) {
20218
- return Result.fail(
20219
- new CliError(
20220
- "UNSAFE_LINK",
20221
- `Refusing to write through ${dir}: it points to ${target ?? "nowhere"}.`
20222
- )
20223
- );
20224
- }
20225
- return Result.ok(target);
20226
- }
20227
- async function containsSymlink(dir) {
20228
- const entries = await readdir3(dir, {
20229
- recursive: true,
20230
- withFileTypes: true
20231
- }).catch(() => []);
20232
- return entries.some((entry) => entry.isSymbolicLink());
20233
- }
20234
- function isInside(path, dir) {
20235
- return path.startsWith(dir + sep2);
20467
+ );
20468
+ return postInventory(session, surface);
20236
20469
  }
20237
20470
  async function clearPending(session, surfaceId) {
20238
20471
  const pending = await apiRequest({
@@ -20253,66 +20486,115 @@ async function clearPending(session, surfaceId) {
20253
20486
  }
20254
20487
  return Result.ok(void 0);
20255
20488
  }
20256
- function printPlan(plans, blocked, surface) {
20257
- const rows = [
20258
- ...plans.map((plan) => [
20259
- plan.action,
20260
- plan.name,
20261
- originOf(surface, plan.name),
20262
- `v${plan.version}`,
20263
- detailOf(plan)
20264
- ]),
20265
- ...blocked.map((skill) => [
20266
- accent("blocked"),
20267
- skill.name,
20268
- originOf(surface, skill.name),
20269
- `v${skill.version}`,
20270
- `org policy: ${skill.blockedReason ?? "no reason given"}`
20271
- ])
20272
- ];
20273
- if (rows.length === 0) {
20274
- process.stdout.write(`${dim("nothing to sync")}
20275
20489
 
20276
- `);
20277
- return;
20278
- }
20279
- process.stdout.write(
20280
- `${table(["ACTION", "SKILL", "ORIGIN", "VERSION", "FILES"], rows)}
20281
- `
20282
- );
20283
- for (const plan of plans) {
20284
- if (plan.action !== "update") continue;
20285
- for (const [marker, paths] of [
20286
- ["+", plan.added],
20287
- ["~", plan.changed],
20288
- ["-", plan.removed]
20289
- ]) {
20290
- for (const path of paths) {
20291
- process.stdout.write(dim(` ${marker} ${plan.name}/${path}
20292
- `));
20293
- }
20490
+ // src/commands/upgrade.ts
20491
+ import { spawn } from "node:child_process";
20492
+ import { homedir as homedir6 } from "node:os";
20493
+ import { resolve as resolve7 } from "node:path";
20494
+ function globalRoot() {
20495
+ return {
20496
+ label: "global",
20497
+ cwd: homedir6(),
20498
+ scope: "-g",
20499
+ lockPath: globalLockPath()
20500
+ };
20501
+ }
20502
+ function projectRoot(dir) {
20503
+ return {
20504
+ label: shortPath(resolve7(dir)),
20505
+ cwd: resolve7(dir),
20506
+ scope: "-p",
20507
+ lockPath: projectLockPath(dir)
20508
+ };
20509
+ }
20510
+ function upgradeRoots(path, all, registered) {
20511
+ const dir = resolve7(path ?? process.cwd());
20512
+ const hasProject = exists(projectSkillsRoot(dir));
20513
+ if (!all) return [hasProject ? projectRoot(dir) : globalRoot()];
20514
+ return [
20515
+ globalRoot(),
20516
+ ...projectDirs(
20517
+ hasProject || path !== void 0 ? dir : void 0,
20518
+ registered
20519
+ ).map(projectRoot)
20520
+ ];
20521
+ }
20522
+ function runSkills(root, names, yes) {
20523
+ const args = ["--yes", "skills", "update", ...names, root.scope];
20524
+ if (yes) args.push("-y");
20525
+ return new Promise((settle) => {
20526
+ const child = spawn("npx", args, { cwd: root.cwd, stdio: "inherit" });
20527
+ child.on("error", (cause) => {
20528
+ settle(
20529
+ Result.fail(
20530
+ new CliError(
20531
+ "NO_NPX",
20532
+ `Cannot run npx: ${cause.message}. The skills.sh CLI applies the update, install Node's npx and retry.`
20533
+ )
20534
+ )
20535
+ );
20536
+ });
20537
+ child.on("close", (code) => {
20538
+ settle(
20539
+ code === 0 || code === null ? Result.ok(void 0) : Result.fail(
20540
+ new CliError(
20541
+ "SKILLS_FAILED",
20542
+ `npx skills update exited with ${code} in ${root.cwd}.`
20543
+ )
20544
+ )
20545
+ );
20546
+ });
20547
+ });
20548
+ }
20549
+ async function upgrade(options) {
20550
+ const config2 = await readConfig();
20551
+ const registered = config2.isSuccess ? config2.value.projects : [];
20552
+ const named = options.names.length > 0;
20553
+ const roots = upgradeRoots(options.path, options.all || named, registered);
20554
+ let ran = 0;
20555
+ const found = /* @__PURE__ */ new Set();
20556
+ for (const root of roots) {
20557
+ const lock = await readLock(root.lockPath);
20558
+ const names = options.names.filter((name) => lock.has(name));
20559
+ if (named && names.length === 0) continue;
20560
+ if (!named && lock.size === 0) {
20561
+ process.stdout.write(
20562
+ dim(`${root.label}: no skills.sh lock here, nothing to upgrade
20563
+ `)
20564
+ );
20565
+ continue;
20294
20566
  }
20567
+ for (const name of names) found.add(name);
20568
+ process.stdout.write(
20569
+ `
20570
+ ${bold(root.label)} ${dim(named ? names.join(", ") : plural(lock.size, "skill"))}
20571
+ `
20572
+ );
20573
+ const result = await runSkills(root, names, options.yes);
20574
+ if (result.isFailure) return result;
20575
+ ran += 1;
20576
+ }
20577
+ const missing = options.names.filter((name) => !found.has(name));
20578
+ if (missing.length > 0) {
20579
+ return Result.fail(
20580
+ new CliError(
20581
+ "NOT_FROM_SKILLS_SH",
20582
+ `No skills.sh lock lists ${missing.join(", ")}. \`npx hubskillz doctor\` lists what is installed where.`
20583
+ )
20584
+ );
20585
+ }
20586
+ if (ran === 0) {
20587
+ process.stdout.write(
20588
+ dim("Nothing installed by `npx skills add` in these roots.\n")
20589
+ );
20590
+ return Result.ok(void 0);
20295
20591
  }
20296
- const counts = [
20297
- [plans.filter((plan) => plan.action === "install").length, "to install"],
20298
- [plans.filter((plan) => plan.action === "update").length, "to update"],
20299
- [plans.filter((plan) => plan.action === "keep").length, "up to date"],
20300
- [plans.filter((plan) => plan.action === "skip").length, "skipped"],
20301
- [blocked.length, "blocked"]
20302
- ];
20303
20592
  process.stdout.write(
20304
- `${counts.filter(([n]) => n > 0).map(([n, label]) => `${n} ${label}`).join(", ")}
20305
-
20306
- `
20593
+ dim(
20594
+ "\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"
20595
+ )
20307
20596
  );
20308
- }
20309
- function detailOf(plan) {
20310
- if (plan.action === "keep") return dim("up to date");
20311
- if (plan.action === "skip") {
20312
- return `${accent("customized locally")}, use --force to overwrite`;
20313
- }
20314
- if (plan.action === "install") return `+${plan.added.length}`;
20315
- return `+${plan.added.length} ~${plan.changed.length} -${plan.removed.length}`;
20597
+ return Result.ok(void 0);
20316
20598
  }
20317
20599
 
20318
20600
  // src/index.ts
@@ -20356,8 +20638,8 @@ var COMMANDS = [
20356
20638
  },
20357
20639
  {
20358
20640
  name: "sync",
20359
- usage: "hubskillz sync [--path DIR] [--all] [--adopt] [--yes] [--dry-run] [--force]",
20360
- summary: "Install and update skills to the approved versions",
20641
+ usage: "hubskillz sync [--path DIR] [--all] [--yes]",
20642
+ summary: "Upload the skills as they are, like a git push",
20361
20643
  flags: [
20362
20644
  {
20363
20645
  spec: "--path DIR",
@@ -20368,14 +20650,59 @@ var COMMANDS = [
20368
20650
  help: "Global root, this project and every registered project"
20369
20651
  },
20370
20652
  {
20371
- spec: "--adopt",
20372
- help: "Add importable skills to the directory as approved (maintainers)"
20653
+ spec: "-y, --yes",
20654
+ help: "Register every discovered project without asking"
20655
+ }
20656
+ ]
20657
+ },
20658
+ {
20659
+ name: "upgrade",
20660
+ usage: "hubskillz upgrade [SKILL...] [--path DIR] [--all] [--yes]",
20661
+ summary: "Update skills.sh skills to their latest upstream",
20662
+ flags: [
20663
+ {
20664
+ spec: "--path DIR",
20665
+ help: "Project directory (default: current directory)"
20666
+ },
20667
+ { spec: "--all", help: "Global root and every registered project" },
20668
+ { spec: "-y, --yes", help: "Skip the skills.sh prompts" }
20669
+ ]
20670
+ },
20671
+ {
20672
+ name: "doctor",
20673
+ usage: "hubskillz doctor [--path DIR]",
20674
+ summary: "Check every local skills root for problems",
20675
+ flags: [
20676
+ {
20677
+ spec: "--path DIR",
20678
+ help: "Project directory (default: current directory)"
20679
+ }
20680
+ ]
20681
+ },
20682
+ {
20683
+ name: "move",
20684
+ usage: "hubskillz move <skill> <global|DIR> [--from global|DIR] [--force]",
20685
+ summary: "Move a skill between the global root and a project",
20686
+ flags: [
20687
+ {
20688
+ spec: "--from ROOT",
20689
+ help: "Which copy to move when the name exists twice"
20373
20690
  },
20374
- { spec: "-y, --yes", help: "Apply without asking" },
20375
- { spec: "--dry-run", help: "Print the plan and stop" },
20376
- { spec: "--force", help: "Overwrite skills you customized locally" }
20691
+ { spec: "--force", help: "Replace a skill of the same name over there" }
20377
20692
  ]
20378
20693
  },
20694
+ {
20695
+ name: "publish",
20696
+ usage: "hubskillz publish <skill>",
20697
+ summary: "List the skill on your public page",
20698
+ flags: []
20699
+ },
20700
+ {
20701
+ name: "unpublish",
20702
+ usage: "hubskillz unpublish <skill>",
20703
+ summary: "Take the skill off your public page",
20704
+ flags: []
20705
+ },
20379
20706
  {
20380
20707
  name: "push",
20381
20708
  usage: "hubskillz push <skill-dir> [-m MESSAGE]",
@@ -20396,7 +20723,7 @@ function flagLines(flags) {
20396
20723
  }
20397
20724
  function usage() {
20398
20725
  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
20726
+ return `${bold("hubskillz")} ${dim(`v${"1.0.0"}`)} keep your agent skills in sync
20400
20727
 
20401
20728
  ${bold("Usage")}
20402
20729
  hubskillz <command> [flags]
@@ -20458,11 +20785,10 @@ async function run() {
20458
20785
  token: { type: "string" },
20459
20786
  path: { type: "string" },
20460
20787
  all: { type: "boolean", default: false },
20461
- adopt: { type: "boolean", default: false },
20462
20788
  yes: { type: "boolean", short: "y", default: false },
20463
- "dry-run": { type: "boolean", default: false },
20464
20789
  force: { type: "boolean", default: false },
20465
20790
  message: { type: "string", short: "m" },
20791
+ from: { type: "string" },
20466
20792
  help: { type: "boolean", short: "h", default: false },
20467
20793
  version: { type: "boolean", short: "v", default: false }
20468
20794
  }
@@ -20475,7 +20801,7 @@ Run \`hubskillz help\` for usage.`)
20475
20801
  );
20476
20802
  }
20477
20803
  if (values.version === true) {
20478
- process.stdout.write(`${"0.3.1"}
20804
+ process.stdout.write(`${"1.0.0"}
20479
20805
  `);
20480
20806
  return Result.ok(void 0);
20481
20807
  }
@@ -20501,11 +20827,52 @@ Run \`hubskillz help\` for usage.`)
20501
20827
  baseUrl: values["base-url"],
20502
20828
  path: values.path,
20503
20829
  all: values.all === true,
20504
- adopt: values.adopt === true,
20505
- yes: values.yes === true,
20506
- dryRun: values["dry-run"] === true,
20830
+ yes: values.yes === true
20831
+ });
20832
+ case "upgrade":
20833
+ return upgrade({
20834
+ names: positionals.slice(1),
20835
+ path: values.path,
20836
+ all: values.all === true,
20837
+ yes: values.yes === true
20838
+ });
20839
+ case "doctor":
20840
+ return doctor({ path: values.path });
20841
+ case "move": {
20842
+ const name = positionals[1];
20843
+ if (name === void 0) {
20844
+ return Result.fail(
20845
+ new CliError(
20846
+ "USAGE",
20847
+ "hubskillz move needs a skill name. Run `hubskillz help move`."
20848
+ )
20849
+ );
20850
+ }
20851
+ return move({
20852
+ name,
20853
+ to: positionals[2],
20854
+ from: values.from,
20855
+ path: values.path,
20507
20856
  force: values.force === true
20508
20857
  });
20858
+ }
20859
+ case "publish":
20860
+ case "unpublish": {
20861
+ const name = positionals[1];
20862
+ if (name === void 0) {
20863
+ return Result.fail(
20864
+ new CliError(
20865
+ "USAGE",
20866
+ `hubskillz ${command} needs a skill name. Run \`hubskillz help ${command}\`.`
20867
+ )
20868
+ );
20869
+ }
20870
+ return publish({
20871
+ baseUrl: values["base-url"],
20872
+ name,
20873
+ published: command === "publish"
20874
+ });
20875
+ }
20509
20876
  case "projects":
20510
20877
  return projects({
20511
20878
  action: positionals[1],