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.
- package/README.md +69 -0
- package/dist/index.js +1180 -486
- 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,
|
|
2840
|
-
|
|
2841
|
-
return
|
|
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
|
-
|
|
19250
|
-
|
|
19251
|
-
|
|
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
|
|
19273
|
+
return styleText("bold", text);
|
|
19259
19274
|
}
|
|
19260
19275
|
function dim(text) {
|
|
19261
|
-
return
|
|
19276
|
+
return styleText("dim", text);
|
|
19262
19277
|
}
|
|
19263
19278
|
function accent(text) {
|
|
19264
|
-
return
|
|
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/
|
|
19286
|
-
import {
|
|
19287
|
-
|
|
19288
|
-
|
|
19289
|
-
|
|
19290
|
-
|
|
19291
|
-
|
|
19292
|
-
|
|
19293
|
-
|
|
19294
|
-
|
|
19295
|
-
|
|
19296
|
-
|
|
19297
|
-
|
|
19298
|
-
|
|
19299
|
-
|
|
19300
|
-
|
|
19301
|
-
|
|
19302
|
-
|
|
19303
|
-
|
|
19304
|
-
|
|
19305
|
-
|
|
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
|
-
|
|
19326
|
-
|
|
19327
|
-
|
|
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
|
-
|
|
19330
|
-
|
|
19331
|
-
|
|
19332
|
-
rl.close();
|
|
19335
|
+
json2 = JSON.parse(raw);
|
|
19336
|
+
} catch {
|
|
19337
|
+
return map2;
|
|
19333
19338
|
}
|
|
19334
|
-
|
|
19335
|
-
|
|
19336
|
-
const
|
|
19337
|
-
|
|
19338
|
-
|
|
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
|
|
19350
|
+
return map2;
|
|
19341
19351
|
}
|
|
19342
|
-
|
|
19343
|
-
|
|
19344
|
-
|
|
19345
|
-
|
|
19346
|
-
|
|
19347
|
-
|
|
19348
|
-
|
|
19349
|
-
|
|
19350
|
-
|
|
19351
|
-
|
|
19352
|
-
|
|
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
|
-
|
|
19395
|
+
const upstream = names.map((name) => lock.get(name)).find(Boolean);
|
|
19396
|
+
return upstream === void 0 ? skill : { ...skill, upstream };
|
|
19356
19397
|
}
|
|
19357
|
-
async function
|
|
19358
|
-
|
|
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
|
-
|
|
19368
|
-
|
|
19369
|
-
return
|
|
19370
|
-
}
|
|
19371
|
-
|
|
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
|
-
|
|
19628
|
-
|
|
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
|
|
19631
|
-
|
|
19632
|
-
|
|
19633
|
-
|
|
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
|
|
19637
|
-
|
|
19638
|
-
|
|
19639
|
-
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
|
|
19646
|
-
|
|
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
|
-
|
|
19649
|
-
bytes += size;
|
|
20033
|
+
await rm2(dest, { recursive: true, force: true });
|
|
19650
20034
|
}
|
|
19651
|
-
|
|
19652
|
-
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
19656
|
-
|
|
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
|
|
19660
|
-
const
|
|
19661
|
-
|
|
19662
|
-
|
|
19663
|
-
|
|
19664
|
-
|
|
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 = [
|
|
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
|
|
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 =
|
|
20119
|
+
const child = join6(dir, name);
|
|
19695
20120
|
if (name === ".claude" && depth > 0) {
|
|
19696
|
-
if (
|
|
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(
|
|
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
|
|
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 =
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
20320
|
+
surfaceId ??= posted.value.surfaceId;
|
|
20321
|
+
items.push(...posted.value.items);
|
|
19910
20322
|
}
|
|
19911
|
-
return Result.ok(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
19965
|
-
import { homedir as
|
|
19966
|
-
import { join as
|
|
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
|
|
19970
|
-
import { dirname as
|
|
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
|
|
19983
|
-
const staging = await mkdtemp(
|
|
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 =
|
|
19987
|
-
await
|
|
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 =
|
|
19992
|
-
await
|
|
19993
|
-
await
|
|
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 =
|
|
19997
|
-
await
|
|
19998
|
-
await pruneEmptyDirs(
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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}
|
|
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 =
|
|
20213
|
-
const stats = await
|
|
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(
|
|
20217
|
-
if (target === null || !isInside(target, home2) || isInside(target, await realpath2(root).catch(() =>
|
|
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
|
|
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
|
-
...
|
|
20739
|
+
...listed.map((plan) => [
|
|
20259
20740
|
plan.action,
|
|
20260
20741
|
plan.name,
|
|
20261
20742
|
originOf(surface, plan.name),
|
|
20262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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],
|