hubskillz 0.3.0 → 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 +1117 -380
- 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");
|
|
@@ -18911,12 +18915,14 @@ var MAX_FILES_PER_SKILL = 100;
|
|
|
18911
18915
|
var MAX_FILE_CONTENT_CHARS = 2e5;
|
|
18912
18916
|
var MAX_SKILLS_PER_REQUEST = 500;
|
|
18913
18917
|
var MAX_SNAPSHOT_CHARS = MAX_FILE_CONTENT_CHARS;
|
|
18918
|
+
var MAX_INVENTORY_CHUNK_BYTES = 15e5;
|
|
18914
18919
|
var inventoryFileSchema = external_exports.object({
|
|
18915
18920
|
path: skillFilePathSchema,
|
|
18916
18921
|
hash: external_exports.string().min(1),
|
|
18917
18922
|
size: external_exports.number().int().nonnegative(),
|
|
18918
18923
|
content: external_exports.string().max(MAX_FILE_CONTENT_CHARS).optional()
|
|
18919
18924
|
});
|
|
18925
|
+
var SKILL_MD = "SKILL.md";
|
|
18920
18926
|
var skillFileSchema = external_exports.object({
|
|
18921
18927
|
path: skillFilePathSchema,
|
|
18922
18928
|
content: external_exports.string().max(MAX_FILE_CONTENT_CHARS)
|
|
@@ -18949,7 +18955,9 @@ var surfaceDescriptorSchema = external_exports.object({
|
|
|
18949
18955
|
kind: surfaceKindSchema,
|
|
18950
18956
|
label: external_exports.string().min(1).max(200),
|
|
18951
18957
|
machineId: external_exports.string().min(1).max(200),
|
|
18952
|
-
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()
|
|
18953
18961
|
});
|
|
18954
18962
|
var inventoryRequestSchema = external_exports.object({
|
|
18955
18963
|
surface: surfaceDescriptorSchema,
|
|
@@ -18961,7 +18969,12 @@ var inventoryRequestSchema = external_exports.object({
|
|
|
18961
18969
|
files: external_exports.array(inventoryFileSchema).max(MAX_FILES_PER_SKILL),
|
|
18962
18970
|
upstream: inventoryUpstreamSchema.optional()
|
|
18963
18971
|
})
|
|
18964
|
-
).max(MAX_SKILLS_PER_REQUEST)
|
|
18972
|
+
).max(MAX_SKILLS_PER_REQUEST),
|
|
18973
|
+
/** Big surfaces travel in chunks: chunk 0 replaces, the others append. */
|
|
18974
|
+
chunk: external_exports.object({
|
|
18975
|
+
index: external_exports.number().int().min(0),
|
|
18976
|
+
total: external_exports.number().int().min(1)
|
|
18977
|
+
}).optional()
|
|
18965
18978
|
});
|
|
18966
18979
|
var inventoryItemSchema = external_exports.object({
|
|
18967
18980
|
name: external_exports.string(),
|
|
@@ -18981,7 +18994,9 @@ var inventoryResponseSchema = external_exports.object({
|
|
|
18981
18994
|
});
|
|
18982
18995
|
var approvedQuerySchema = external_exports.object({ surfaceId: external_exports.string().min(1) });
|
|
18983
18996
|
var approvedSkillSchema = external_exports.object({
|
|
18984
|
-
|
|
18997
|
+
// Kebab, not z.string(): the CLI joins this onto a path (commands/sync.ts),
|
|
18998
|
+
// so the response is validated rather than trusted.
|
|
18999
|
+
name: skillNameSchema,
|
|
18985
19000
|
versionId: external_exports.string(),
|
|
18986
19001
|
version: external_exports.number().int(),
|
|
18987
19002
|
contentHash: external_exports.string(),
|
|
@@ -19007,6 +19022,14 @@ var draftResponseSchema = external_exports.object({
|
|
|
19007
19022
|
skillId: external_exports.string(),
|
|
19008
19023
|
versionId: external_exports.string()
|
|
19009
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
|
+
});
|
|
19010
19033
|
var pendingQuerySchema = external_exports.object({ surfaceId: external_exports.string().min(1) });
|
|
19011
19034
|
var pendingResponseSchema = external_exports.object({
|
|
19012
19035
|
requests: external_exports.array(
|
|
@@ -19076,6 +19099,76 @@ var paginationQuerySchema = external_exports.object({
|
|
|
19076
19099
|
pageSize: external_exports.coerce.number().int().min(1).max(100).default(25)
|
|
19077
19100
|
});
|
|
19078
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
|
+
|
|
19079
19172
|
// src/errors.ts
|
|
19080
19173
|
var CliError = class extends DomainError {
|
|
19081
19174
|
constructor(code, message) {
|
|
@@ -19091,75 +19184,7 @@ function toCliError(cause) {
|
|
|
19091
19184
|
return new CliError("UNEXPECTED", String(cause));
|
|
19092
19185
|
}
|
|
19093
19186
|
|
|
19094
|
-
// src/api.ts
|
|
19095
|
-
async function apiRequest(request) {
|
|
19096
|
-
const url2 = `${request.session.baseUrl}${request.path}`;
|
|
19097
|
-
const bearer = `Bearer ${request.session.token}`;
|
|
19098
|
-
let response;
|
|
19099
|
-
try {
|
|
19100
|
-
response = await fetch(url2, {
|
|
19101
|
-
method: request.method,
|
|
19102
|
-
headers: request.body === void 0 ? { accept: "application/json", authorization: bearer } : {
|
|
19103
|
-
accept: "application/json",
|
|
19104
|
-
authorization: bearer,
|
|
19105
|
-
"content-type": "application/json"
|
|
19106
|
-
},
|
|
19107
|
-
body: request.body === void 0 ? void 0 : JSON.stringify(request.body)
|
|
19108
|
-
});
|
|
19109
|
-
} catch (cause) {
|
|
19110
|
-
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
19111
|
-
return Result.fail(
|
|
19112
|
-
new CliError("NETWORK", `Cannot reach ${url2}: ${detail}`)
|
|
19113
|
-
);
|
|
19114
|
-
}
|
|
19115
|
-
const text = await response.text();
|
|
19116
|
-
if (response.status === 413) {
|
|
19117
|
-
return Result.fail(
|
|
19118
|
-
new CliError(
|
|
19119
|
-
"HTTP",
|
|
19120
|
-
`${request.method} ${request.path} failed: the inventory is too large (HTTP 413). Reduce the number of skills or roots, or use project roots.`
|
|
19121
|
-
)
|
|
19122
|
-
);
|
|
19123
|
-
}
|
|
19124
|
-
if (!response.ok) {
|
|
19125
|
-
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.`;
|
|
19126
|
-
return Result.fail(
|
|
19127
|
-
new CliError(
|
|
19128
|
-
response.status === 401 ? "UNAUTHORIZED" : response.status === 403 ? "FORBIDDEN" : "HTTP",
|
|
19129
|
-
`${request.method} ${request.path} failed: ${message}`
|
|
19130
|
-
)
|
|
19131
|
-
);
|
|
19132
|
-
}
|
|
19133
|
-
const parsed = decodeBody(text, request.schema);
|
|
19134
|
-
if (parsed === null) {
|
|
19135
|
-
return Result.fail(
|
|
19136
|
-
new CliError(
|
|
19137
|
-
"PROTOCOL",
|
|
19138
|
-
`${request.method} ${request.path} returned an unexpected payload (HTTP ${response.status}, not JSON or wrong shape).`
|
|
19139
|
-
)
|
|
19140
|
-
);
|
|
19141
|
-
}
|
|
19142
|
-
return Result.ok(parsed);
|
|
19143
|
-
}
|
|
19144
|
-
function isJson(text) {
|
|
19145
|
-
try {
|
|
19146
|
-
JSON.parse(text === "" ? "null" : text);
|
|
19147
|
-
return true;
|
|
19148
|
-
} catch {
|
|
19149
|
-
return false;
|
|
19150
|
-
}
|
|
19151
|
-
}
|
|
19152
|
-
function decodeBody(text, schema) {
|
|
19153
|
-
if (!isJson(text)) return null;
|
|
19154
|
-
const parsed = schema.safeParse(JSON.parse(text === "" ? "null" : text));
|
|
19155
|
-
return parsed.success ? parsed.data : null;
|
|
19156
|
-
}
|
|
19157
|
-
|
|
19158
19187
|
// src/config.ts
|
|
19159
|
-
import { randomUUID } from "node:crypto";
|
|
19160
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
19161
|
-
import { homedir, hostname as hostname3 } from "node:os";
|
|
19162
|
-
import { join } from "node:path";
|
|
19163
19188
|
var DEFAULT_BASE_URL = "https://api.hubskillz.com";
|
|
19164
19189
|
var configSchema = external_exports.object({
|
|
19165
19190
|
baseUrl: external_exports.string().min(1),
|
|
@@ -19231,6 +19256,9 @@ async function machineId() {
|
|
|
19231
19256
|
const existing = await readConfig();
|
|
19232
19257
|
return existing.isSuccess ? existing.value.machineId : randomUUID();
|
|
19233
19258
|
}
|
|
19259
|
+
function webOrigin(baseUrl) {
|
|
19260
|
+
return baseUrl === DEFAULT_BASE_URL ? "https://hubskillz.com" : baseUrl;
|
|
19261
|
+
}
|
|
19234
19262
|
function resolveBaseUrl(flag, fromConfig) {
|
|
19235
19263
|
const env = process.env["HUBSKILLZ_BASE_URL"];
|
|
19236
19264
|
const chosen = flag ?? env ?? fromConfig ?? DEFAULT_BASE_URL;
|
|
@@ -19238,22 +19266,24 @@ function resolveBaseUrl(flag, fromConfig) {
|
|
|
19238
19266
|
}
|
|
19239
19267
|
|
|
19240
19268
|
// src/output.ts
|
|
19241
|
-
|
|
19242
|
-
|
|
19243
|
-
|
|
19244
|
-
return process.env["NO_COLOR"] === void 0 && process.stdout.isTTY === true;
|
|
19245
|
-
}
|
|
19246
|
-
function wrap(code, text) {
|
|
19247
|
-
return colorEnabled() ? `${CSI}${code}m${text}${CSI}0m` : text;
|
|
19248
|
-
}
|
|
19269
|
+
import { homedir as homedir2 } from "node:os";
|
|
19270
|
+
import { styleText } from "node:util";
|
|
19271
|
+
var ANSI_PATTERN = /\u001B\[[0-9;]*m/gu;
|
|
19249
19272
|
function bold(text) {
|
|
19250
|
-
return
|
|
19273
|
+
return styleText("bold", text);
|
|
19251
19274
|
}
|
|
19252
19275
|
function dim(text) {
|
|
19253
|
-
return
|
|
19276
|
+
return styleText("dim", text);
|
|
19254
19277
|
}
|
|
19255
19278
|
function accent(text) {
|
|
19256
|
-
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"}`;
|
|
19257
19287
|
}
|
|
19258
19288
|
function table(headers, rows) {
|
|
19259
19289
|
const widths = headers.map(
|
|
@@ -19270,12 +19300,526 @@ function table(headers, rows) {
|
|
|
19270
19300
|
function visibleLength(text) {
|
|
19271
19301
|
return text.replace(ANSI_PATTERN, "").length;
|
|
19272
19302
|
}
|
|
19273
|
-
|
|
19274
|
-
|
|
19303
|
+
|
|
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");
|
|
19325
|
+
}
|
|
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;
|
|
19334
|
+
try {
|
|
19335
|
+
json2 = JSON.parse(raw);
|
|
19336
|
+
} catch {
|
|
19337
|
+
return map2;
|
|
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 });
|
|
19349
|
+
}
|
|
19350
|
+
return map2;
|
|
19351
|
+
}
|
|
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));
|
|
19394
|
+
}
|
|
19395
|
+
const upstream = names.map((name) => lock.get(name)).find(Boolean);
|
|
19396
|
+
return upstream === void 0 ? skill : { ...skill, upstream };
|
|
19397
|
+
}
|
|
19398
|
+
async function scanSkills(root) {
|
|
19399
|
+
let entries;
|
|
19400
|
+
try {
|
|
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);
|
|
19414
|
+
}
|
|
19415
|
+
return skills.sort((a, b) => a.name < b.name ? -1 : 1);
|
|
19416
|
+
}
|
|
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
|
+
}
|
|
19275
19818
|
}
|
|
19276
19819
|
|
|
19277
19820
|
// src/prompt.ts
|
|
19278
19821
|
import { createInterface } from "node:readline/promises";
|
|
19822
|
+
import { text as readAll } from "node:stream/consumers";
|
|
19279
19823
|
var ENTER = /* @__PURE__ */ new Set(["\r", "\n"]);
|
|
19280
19824
|
var BACKSPACE = /* @__PURE__ */ new Set(["\b", "\x7F"]);
|
|
19281
19825
|
var CTRL_C = "";
|
|
@@ -19324,13 +19868,6 @@ async function confirm(question) {
|
|
|
19324
19868
|
rl.close();
|
|
19325
19869
|
}
|
|
19326
19870
|
}
|
|
19327
|
-
async function readAll(stream) {
|
|
19328
|
-
const chunks = [];
|
|
19329
|
-
for await (const chunk of stream) {
|
|
19330
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
19331
|
-
}
|
|
19332
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
19333
|
-
}
|
|
19334
19871
|
function parseSelection(answer, count) {
|
|
19335
19872
|
const text = answer.trim().toLowerCase();
|
|
19336
19873
|
if (text === "" || text === "all" || text === "a") {
|
|
@@ -19448,210 +19985,130 @@ async function logout() {
|
|
|
19448
19985
|
return Result.ok(void 0);
|
|
19449
19986
|
}
|
|
19450
19987
|
|
|
19451
|
-
// src/commands/
|
|
19452
|
-
import {
|
|
19453
|
-
|
|
19454
|
-
|
|
19455
|
-
|
|
19456
|
-
|
|
19457
|
-
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
|
|
19461
|
-
|
|
19462
|
-
|
|
19463
|
-
import { basename, dirname, join as join3, resolve as resolve2 } from "node:path";
|
|
19464
|
-
|
|
19465
|
-
// src/lock.ts
|
|
19466
|
-
import { readFile as readFile2 } from "node:fs/promises";
|
|
19467
|
-
import { homedir as homedir2 } from "node:os";
|
|
19468
|
-
import { join as join2, resolve } from "node:path";
|
|
19469
|
-
var entrySchema = external_exports.object({
|
|
19470
|
-
source: external_exports.string().min(1),
|
|
19471
|
-
sourceType: external_exports.string(),
|
|
19472
|
-
skillPath: external_exports.string().min(1).optional(),
|
|
19473
|
-
skillFolderHash: external_exports.string().min(1).optional(),
|
|
19474
|
-
computedHash: external_exports.string().min(1).optional()
|
|
19475
|
-
});
|
|
19476
|
-
var lockSchema = external_exports.object({ skills: external_exports.record(external_exports.string(), external_exports.unknown()) });
|
|
19477
|
-
function globalLockPath() {
|
|
19478
|
-
return join2(homedir2(), ".agents", ".skill-lock.json");
|
|
19479
|
-
}
|
|
19480
|
-
function projectLockPath(projectDir) {
|
|
19481
|
-
return join2(resolve(projectDir), "skills-lock.json");
|
|
19482
|
-
}
|
|
19483
|
-
async function readLock(path) {
|
|
19484
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
19485
|
-
const raw = await readFile2(path, "utf8").catch(() => null);
|
|
19486
|
-
if (raw === null) return map2;
|
|
19487
|
-
let json2;
|
|
19488
|
-
try {
|
|
19489
|
-
json2 = JSON.parse(raw);
|
|
19490
|
-
} catch {
|
|
19491
|
-
return map2;
|
|
19492
|
-
}
|
|
19493
|
-
const lock = lockSchema.safeParse(json2);
|
|
19494
|
-
if (!lock.success) return map2;
|
|
19495
|
-
for (const [name, value] of Object.entries(lock.data.skills)) {
|
|
19496
|
-
const entry = entrySchema.safeParse(value);
|
|
19497
|
-
if (!entry.success) continue;
|
|
19498
|
-
const { source, sourceType, skillPath } = entry.data;
|
|
19499
|
-
const hash2 = entry.data.skillFolderHash ?? entry.data.computedHash;
|
|
19500
|
-
if (sourceType !== "github" || skillPath === void 0) continue;
|
|
19501
|
-
if (hash2 === void 0) continue;
|
|
19502
|
-
map2.set(name, { source, skillPath, hash: hash2 });
|
|
19503
|
-
}
|
|
19504
|
-
return map2;
|
|
19505
|
-
}
|
|
19506
|
-
|
|
19507
|
-
// src/scan.ts
|
|
19508
|
-
function globalSkillsRoot() {
|
|
19509
|
-
return join3(homedir3(), ".claude", "skills");
|
|
19510
|
-
}
|
|
19511
|
-
function projectSkillsRoot(dir) {
|
|
19512
|
-
return join3(resolve2(dir), ".claude", "skills");
|
|
19513
|
-
}
|
|
19514
|
-
function globalSurfaceLabel() {
|
|
19515
|
-
return hostname4();
|
|
19516
|
-
}
|
|
19517
|
-
function projectSurfaceLabel(dir) {
|
|
19518
|
-
return `${hostname4()}:${basename(resolve2(dir))}`;
|
|
19519
|
-
}
|
|
19520
|
-
async function exists(path) {
|
|
19521
|
-
try {
|
|
19522
|
-
await stat(path);
|
|
19523
|
-
return true;
|
|
19524
|
-
} catch {
|
|
19525
|
-
return false;
|
|
19526
|
-
}
|
|
19527
|
-
}
|
|
19528
|
-
function lockPathFor(root) {
|
|
19529
|
-
const resolved = resolve2(root);
|
|
19530
|
-
if (resolved === globalSkillsRoot()) return globalLockPath();
|
|
19531
|
-
return projectLockPath(dirname(dirname(resolved)));
|
|
19532
|
-
}
|
|
19533
|
-
async function scanSurface(root, label, machine) {
|
|
19534
|
-
const lock = await readLock(lockPathFor(root));
|
|
19535
|
-
const skills = await scanSkills(root);
|
|
19536
|
-
return {
|
|
19537
|
-
descriptor: {
|
|
19538
|
-
kind: "claude-code-local",
|
|
19539
|
-
label,
|
|
19540
|
-
machineId: machine,
|
|
19541
|
-
path: resolve2(root)
|
|
19542
|
-
},
|
|
19543
|
-
skills: await Promise.all(skills.map((skill) => withUpstream(skill, lock)))
|
|
19544
|
-
};
|
|
19545
|
-
}
|
|
19546
|
-
async function withUpstream(skill, lock) {
|
|
19547
|
-
if (lock.size === 0) return skill;
|
|
19548
|
-
const names = [skill.name];
|
|
19549
|
-
if (skill.link) {
|
|
19550
|
-
const real = await realpath(skill.dir).catch(() => skill.dir);
|
|
19551
|
-
names.push(basename(real));
|
|
19552
|
-
}
|
|
19553
|
-
const upstream = names.map((name) => lock.get(name)).find(Boolean);
|
|
19554
|
-
return upstream === void 0 ? skill : { ...skill, upstream };
|
|
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
|
+
];
|
|
19555
20000
|
}
|
|
19556
|
-
|
|
19557
|
-
|
|
19558
|
-
|
|
19559
|
-
|
|
19560
|
-
|
|
19561
|
-
return
|
|
19562
|
-
|
|
19563
|
-
|
|
19564
|
-
|
|
19565
|
-
|
|
19566
|
-
|
|
19567
|
-
const symlink = entry.isSymbolicLink();
|
|
19568
|
-
if (!entry.isDirectory() && !(symlink && await isDir(dir))) continue;
|
|
19569
|
-
const skill = await scanSkillDir(dir);
|
|
19570
|
-
if (skill === null) continue;
|
|
19571
|
-
skills.push(symlink ? { ...skill, link: true } : skill);
|
|
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
|
+
);
|
|
19572
20012
|
}
|
|
19573
|
-
return
|
|
19574
|
-
|
|
19575
|
-
|
|
19576
|
-
|
|
19577
|
-
|
|
19578
|
-
|
|
19579
|
-
return {
|
|
19580
|
-
name: basename(resolved),
|
|
19581
|
-
dir: resolved,
|
|
19582
|
-
files: walked.files,
|
|
19583
|
-
contentHash: contentHash(walked.files),
|
|
19584
|
-
link: walked.link
|
|
19585
|
-
};
|
|
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
|
+
);
|
|
19586
20019
|
}
|
|
19587
|
-
async function
|
|
19588
|
-
|
|
19589
|
-
|
|
19590
|
-
|
|
19591
|
-
|
|
19592
|
-
|
|
19593
|
-
|
|
19594
|
-
|
|
19595
|
-
|
|
19596
|
-
|
|
19597
|
-
|
|
19598
|
-
|
|
19599
|
-
if (entry.isDirectory() || symlink && await isDir(full)) {
|
|
19600
|
-
const nested = await walk(full, relative, visited);
|
|
19601
|
-
files.push(...nested.files);
|
|
19602
|
-
link = link || symlink || nested.link;
|
|
19603
|
-
continue;
|
|
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
|
+
);
|
|
19604
20032
|
}
|
|
19605
|
-
|
|
19606
|
-
|
|
19607
|
-
|
|
19608
|
-
|
|
20033
|
+
await rm2(dest, { recursive: true, force: true });
|
|
20034
|
+
}
|
|
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));
|
|
19609
20043
|
}
|
|
19610
|
-
|
|
19611
|
-
|
|
19612
|
-
content,
|
|
19613
|
-
hash: createHash2("sha256").update(content).digest("hex"),
|
|
19614
|
-
size: Buffer.byteLength(content, "utf8")
|
|
19615
|
-
});
|
|
20044
|
+
await cp(source, dest, { recursive: true, verbatimSymlinks: true });
|
|
20045
|
+
await rm2(source, { recursive: true, force: true });
|
|
19616
20046
|
}
|
|
19617
|
-
return
|
|
20047
|
+
return Result.ok(void 0);
|
|
19618
20048
|
}
|
|
19619
|
-
async function
|
|
19620
|
-
|
|
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;
|
|
19621
20055
|
}
|
|
19622
|
-
function
|
|
19623
|
-
|
|
19624
|
-
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
19628
|
-
|
|
19629
|
-
|
|
19630
|
-
|
|
19631
|
-
|
|
19632
|
-
|
|
19633
|
-
|
|
19634
|
-
|
|
19635
|
-
|
|
19636
|
-
|
|
19637
|
-
|
|
19638
|
-
|
|
19639
|
-
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19643
|
-
|
|
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);
|
|
19644
20095
|
}
|
|
19645
20096
|
|
|
20097
|
+
// src/commands/projects.ts
|
|
20098
|
+
import { resolve as resolve6 } from "node:path";
|
|
20099
|
+
|
|
19646
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";
|
|
19647
20104
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "Library", ".Trash", ".cache"]);
|
|
19648
|
-
async function discoverProjects(roots = [
|
|
20105
|
+
async function discoverProjects(roots = [homedir5()], maxDepth = 3) {
|
|
19649
20106
|
const found = /* @__PURE__ */ new Set();
|
|
19650
20107
|
const walk2 = async (dir, depth) => {
|
|
19651
20108
|
if (depth > maxDepth) return;
|
|
19652
20109
|
let entries;
|
|
19653
20110
|
try {
|
|
19654
|
-
entries = await
|
|
20111
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
19655
20112
|
} catch {
|
|
19656
20113
|
return;
|
|
19657
20114
|
}
|
|
@@ -19659,15 +20116,15 @@ async function discoverProjects(roots = [homedir4()], maxDepth = 3) {
|
|
|
19659
20116
|
const name = entry.name;
|
|
19660
20117
|
if (!entry.isDirectory() || SKIP.has(name)) continue;
|
|
19661
20118
|
if (name.startsWith(".") && name !== ".claude") continue;
|
|
19662
|
-
const child =
|
|
20119
|
+
const child = join6(dir, name);
|
|
19663
20120
|
if (name === ".claude" && depth > 0) {
|
|
19664
|
-
if (
|
|
20121
|
+
if (exists(projectSkillsRoot(dir))) found.add(dir);
|
|
19665
20122
|
continue;
|
|
19666
20123
|
}
|
|
19667
20124
|
await walk2(child, depth + 1);
|
|
19668
20125
|
}
|
|
19669
20126
|
};
|
|
19670
|
-
for (const root of roots) await walk2(
|
|
20127
|
+
for (const root of roots) await walk2(resolve5(root), 0);
|
|
19671
20128
|
return [...found].sort();
|
|
19672
20129
|
}
|
|
19673
20130
|
|
|
@@ -19684,7 +20141,7 @@ async function discoverAndRegister(config2, yes) {
|
|
|
19684
20141
|
const projects2 = [...config2.projects, ...chosen];
|
|
19685
20142
|
await writeConfig({ ...config2, projects: projects2 });
|
|
19686
20143
|
process.stdout.write(
|
|
19687
|
-
`Registered ${chosen.length
|
|
20144
|
+
`Registered ${plural(chosen.length, "project")}. They are now part of \`hubskillz sync --all\`.
|
|
19688
20145
|
`
|
|
19689
20146
|
);
|
|
19690
20147
|
return projects2;
|
|
@@ -19693,7 +20150,7 @@ async function projects(options) {
|
|
|
19693
20150
|
const config2 = await readConfig();
|
|
19694
20151
|
if (config2.isFailure) return Result.fail(config2.error);
|
|
19695
20152
|
const current = config2.value.projects;
|
|
19696
|
-
const dir =
|
|
20153
|
+
const dir = resolve6(options.dir ?? process.cwd());
|
|
19697
20154
|
switch (options.action ?? "list") {
|
|
19698
20155
|
case "list": {
|
|
19699
20156
|
if (current.length === 0) {
|
|
@@ -19709,7 +20166,7 @@ async function projects(options) {
|
|
|
19709
20166
|
return Result.ok(void 0);
|
|
19710
20167
|
}
|
|
19711
20168
|
case "add": {
|
|
19712
|
-
if (!
|
|
20169
|
+
if (!exists(projectSkillsRoot(dir))) {
|
|
19713
20170
|
return Result.fail(
|
|
19714
20171
|
new CliError(
|
|
19715
20172
|
"NO_SKILLS_ROOT",
|
|
@@ -19757,6 +20214,32 @@ async function projects(options) {
|
|
|
19757
20214
|
}
|
|
19758
20215
|
}
|
|
19759
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
|
+
|
|
19760
20243
|
// src/commands/push.ts
|
|
19761
20244
|
async function push(options) {
|
|
19762
20245
|
const config2 = await readConfig();
|
|
@@ -19795,47 +20278,6 @@ ${dim(`skill ${draft.value.skillId} version ${draft.value.versionId}`)}
|
|
|
19795
20278
|
return Result.ok(void 0);
|
|
19796
20279
|
}
|
|
19797
20280
|
|
|
19798
|
-
// src/surfaces.ts
|
|
19799
|
-
import { resolve as resolve5 } from "node:path";
|
|
19800
|
-
async function localSurfaces(path, both, machineId2, registered = []) {
|
|
19801
|
-
const projectDir = resolve5(path ?? process.cwd());
|
|
19802
|
-
const projectRoot = projectSkillsRoot(projectDir);
|
|
19803
|
-
const hasProject = await exists(projectRoot);
|
|
19804
|
-
const project = async (dir) => scanSurface(projectSkillsRoot(dir), projectSurfaceLabel(dir), machineId2);
|
|
19805
|
-
const global = async () => scanSurface(globalSkillsRoot(), globalSurfaceLabel(), machineId2);
|
|
19806
|
-
if (!both) return [hasProject ? await project(projectDir) : await global()];
|
|
19807
|
-
const surfaces = [await global()];
|
|
19808
|
-
const dirs = projectDirs(
|
|
19809
|
-
hasProject || path !== void 0 ? projectDir : void 0,
|
|
19810
|
-
registered
|
|
19811
|
-
);
|
|
19812
|
-
for (const dir of dirs) {
|
|
19813
|
-
if (await exists(projectSkillsRoot(dir))) {
|
|
19814
|
-
surfaces.push(await project(dir));
|
|
19815
|
-
continue;
|
|
19816
|
-
}
|
|
19817
|
-
process.stdout.write(
|
|
19818
|
-
dim(
|
|
19819
|
-
`skipping ${dir}: no .claude/skills here, run \`hubskillz projects remove ${dir}\` to forget it
|
|
19820
|
-
`
|
|
19821
|
-
)
|
|
19822
|
-
);
|
|
19823
|
-
}
|
|
19824
|
-
return surfaces;
|
|
19825
|
-
}
|
|
19826
|
-
function projectDirs(current, registered) {
|
|
19827
|
-
const seen = /* @__PURE__ */ new Set();
|
|
19828
|
-
const out = [];
|
|
19829
|
-
for (const dir of [current, ...registered]) {
|
|
19830
|
-
if (dir === void 0) continue;
|
|
19831
|
-
const abs = resolve5(dir);
|
|
19832
|
-
if (seen.has(abs)) continue;
|
|
19833
|
-
seen.add(abs);
|
|
19834
|
-
out.push(abs);
|
|
19835
|
-
}
|
|
19836
|
-
return out;
|
|
19837
|
-
}
|
|
19838
|
-
|
|
19839
20281
|
// src/commands/status.ts
|
|
19840
20282
|
async function status(options) {
|
|
19841
20283
|
const config2 = await readConfig();
|
|
@@ -19854,7 +20296,7 @@ async function status(options) {
|
|
|
19854
20296
|
for (const surface of surfaces) {
|
|
19855
20297
|
const inventory = await postInventory(session, surface);
|
|
19856
20298
|
if (inventory.isFailure) return Result.fail(inventory.error);
|
|
19857
|
-
printSurface(surface, inventory.value);
|
|
20299
|
+
printSurface(surface, inventory.value, session.baseUrl);
|
|
19858
20300
|
}
|
|
19859
20301
|
if (quickstartPending(config2)) {
|
|
19860
20302
|
process.stdout.write(`
|
|
@@ -19863,20 +20305,42 @@ ${quickstart(config2)}`);
|
|
|
19863
20305
|
return Result.ok(void 0);
|
|
19864
20306
|
}
|
|
19865
20307
|
async function postInventory(session, surface) {
|
|
19866
|
-
|
|
19867
|
-
|
|
19868
|
-
|
|
19869
|
-
|
|
19870
|
-
|
|
19871
|
-
|
|
19872
|
-
|
|
20308
|
+
const chunks = inventoryChunksOf(surface);
|
|
20309
|
+
let surfaceId;
|
|
20310
|
+
const items = [];
|
|
20311
|
+
for (const body of chunks) {
|
|
20312
|
+
const posted = await apiRequest({
|
|
20313
|
+
session,
|
|
20314
|
+
method: "POST",
|
|
20315
|
+
path: "/api/cli/inventory",
|
|
20316
|
+
schema: inventoryResponseSchema,
|
|
20317
|
+
body
|
|
20318
|
+
});
|
|
20319
|
+
if (posted.isFailure) return posted;
|
|
20320
|
+
surfaceId ??= posted.value.surfaceId;
|
|
20321
|
+
items.push(...posted.value.items);
|
|
20322
|
+
}
|
|
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()];
|
|
19873
20334
|
}
|
|
19874
20335
|
function originOf(surface, name) {
|
|
19875
20336
|
const skill = surface.skills.find((entry) => entry.name === name);
|
|
19876
20337
|
if (skill === void 0) return "-";
|
|
19877
20338
|
return skill.upstream === void 0 ? "private" : `skills.sh ${skill.upstream.source}`;
|
|
19878
20339
|
}
|
|
19879
|
-
function
|
|
20340
|
+
function reviewUrl(baseUrl) {
|
|
20341
|
+
return `${webOrigin(baseUrl)}/app`;
|
|
20342
|
+
}
|
|
20343
|
+
function printNotes(surface, items, baseUrl) {
|
|
19880
20344
|
const importable = items.filter((item) => item.importable).map((item) => item.name);
|
|
19881
20345
|
if (importable.length > 0) {
|
|
19882
20346
|
process.stdout.write(
|
|
@@ -19888,6 +20352,10 @@ function printNotes(surface, items) {
|
|
|
19888
20352
|
if (ahead.length > 0) {
|
|
19889
20353
|
process.stdout.write(
|
|
19890
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)}`)}
|
|
19891
20359
|
`
|
|
19892
20360
|
);
|
|
19893
20361
|
}
|
|
@@ -19899,17 +20367,35 @@ ${bold(surface.descriptor.label)} ${dim(surface.descriptor.path)}
|
|
|
19899
20367
|
`
|
|
19900
20368
|
);
|
|
19901
20369
|
}
|
|
19902
|
-
function printSurface(surface, inventory) {
|
|
19903
|
-
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) => [
|
|
19904
20375
|
item.name,
|
|
19905
20376
|
originOf(surface, item.name),
|
|
19906
20377
|
item.required ? `${item.state} ${dim("(required)")}` : item.state,
|
|
19907
|
-
|
|
20378
|
+
shortSha(item.installedHash, 8),
|
|
19908
20379
|
item.approvedVersion === void 0 ? "-" : `v${item.approvedVersion}`
|
|
19909
20380
|
]);
|
|
19910
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
|
+
}
|
|
19911
20397
|
if (rows.length === 0) {
|
|
19912
|
-
process.stdout.write(`${dim("no skills")}
|
|
20398
|
+
if (inherited === 0) process.stdout.write(`${dim("no skills")}
|
|
19913
20399
|
`);
|
|
19914
20400
|
return;
|
|
19915
20401
|
}
|
|
@@ -19918,17 +20404,26 @@ function printSurface(surface, inventory) {
|
|
|
19918
20404
|
|
|
19919
20405
|
`
|
|
19920
20406
|
);
|
|
19921
|
-
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
|
+
}
|
|
19922
20417
|
}
|
|
19923
20418
|
|
|
19924
20419
|
// src/commands/sync.ts
|
|
19925
|
-
import { lstat, readdir as
|
|
19926
|
-
import { homedir as
|
|
19927
|
-
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";
|
|
19928
20423
|
|
|
19929
20424
|
// src/apply.ts
|
|
19930
|
-
import { mkdir as
|
|
19931
|
-
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";
|
|
19932
20427
|
async function applySkill(input2) {
|
|
19933
20428
|
for (const path of [...input2.files.map((f) => f.path), ...input2.remove]) {
|
|
19934
20429
|
if (!isSafeRelativePath(path)) {
|
|
@@ -19940,26 +20435,26 @@ async function applySkill(input2) {
|
|
|
19940
20435
|
);
|
|
19941
20436
|
}
|
|
19942
20437
|
}
|
|
19943
|
-
await
|
|
19944
|
-
const staging = await mkdtemp(
|
|
20438
|
+
await mkdir3(input2.dir, { recursive: true });
|
|
20439
|
+
const staging = await mkdtemp(join7(dirname3(input2.dir), ".hubskillz-"));
|
|
19945
20440
|
try {
|
|
19946
20441
|
for (const file2 of input2.files) {
|
|
19947
|
-
const staged =
|
|
19948
|
-
await
|
|
20442
|
+
const staged = join7(staging, ...file2.path.split("/"));
|
|
20443
|
+
await mkdir3(dirname3(staged), { recursive: true });
|
|
19949
20444
|
await writeFile2(staged, file2.content, "utf8");
|
|
19950
20445
|
}
|
|
19951
20446
|
for (const file2 of input2.files) {
|
|
19952
|
-
const target =
|
|
19953
|
-
await
|
|
19954
|
-
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);
|
|
19955
20450
|
}
|
|
19956
20451
|
for (const path of input2.remove) {
|
|
19957
|
-
const target =
|
|
19958
|
-
await
|
|
19959
|
-
await pruneEmptyDirs(
|
|
20452
|
+
const target = join7(input2.dir, ...path.split("/"));
|
|
20453
|
+
await rm3(target, { force: true });
|
|
20454
|
+
await pruneEmptyDirs(dirname3(target), input2.dir);
|
|
19960
20455
|
}
|
|
19961
20456
|
} finally {
|
|
19962
|
-
await
|
|
20457
|
+
await rm3(staging, { recursive: true, force: true });
|
|
19963
20458
|
}
|
|
19964
20459
|
return Result.ok(void 0);
|
|
19965
20460
|
}
|
|
@@ -19977,7 +20472,7 @@ async function pruneEmptyDirs(from, stopAt) {
|
|
|
19977
20472
|
} catch {
|
|
19978
20473
|
return;
|
|
19979
20474
|
}
|
|
19980
|
-
current =
|
|
20475
|
+
current = dirname3(current);
|
|
19981
20476
|
}
|
|
19982
20477
|
}
|
|
19983
20478
|
|
|
@@ -20006,6 +20501,7 @@ function computePlan(input2) {
|
|
|
20006
20501
|
name: skill.name,
|
|
20007
20502
|
state,
|
|
20008
20503
|
version: skill.version,
|
|
20504
|
+
release: releaseOf(skill.files),
|
|
20009
20505
|
action: actionFor(
|
|
20010
20506
|
state,
|
|
20011
20507
|
local !== void 0,
|
|
@@ -20019,14 +20515,18 @@ function computePlan(input2) {
|
|
|
20019
20515
|
}
|
|
20020
20516
|
return plans.sort((a, b) => a.name < b.name ? -1 : 1);
|
|
20021
20517
|
}
|
|
20518
|
+
function versionLabel(plan) {
|
|
20519
|
+
return plan.release ?? `v${plan.version}`;
|
|
20520
|
+
}
|
|
20022
20521
|
function actionFor(state, installed, diffCount, force) {
|
|
20522
|
+
if (state === "inherited") return installed ? "remove" : "inherited";
|
|
20023
20523
|
if (state === "customized" && !force) return "skip";
|
|
20024
20524
|
if (!installed) return "install";
|
|
20025
20525
|
return diffCount === 0 ? "keep" : "update";
|
|
20026
20526
|
}
|
|
20027
20527
|
function planHasWrites(plans) {
|
|
20028
20528
|
return plans.some(
|
|
20029
|
-
(plan) => plan.action === "install" || plan.action === "update"
|
|
20529
|
+
(plan) => plan.action === "install" || plan.action === "update" || plan.action === "remove"
|
|
20030
20530
|
);
|
|
20031
20531
|
}
|
|
20032
20532
|
|
|
@@ -20083,8 +20583,9 @@ async function syncSurface(session, surface, options) {
|
|
|
20083
20583
|
});
|
|
20084
20584
|
printHeader(surface);
|
|
20085
20585
|
printPlan(plans, blocked, surface);
|
|
20086
|
-
printNotes(surface, inventory.value.items);
|
|
20087
|
-
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);
|
|
20088
20589
|
if (!options.yes && !await confirm("Apply?")) {
|
|
20089
20590
|
process.stdout.write("Nothing applied.\n");
|
|
20090
20591
|
return Result.ok(void 0);
|
|
@@ -20094,7 +20595,8 @@ async function syncSurface(session, surface, options) {
|
|
|
20094
20595
|
const rescanned = await scanSurface(
|
|
20095
20596
|
surface.descriptor.path,
|
|
20096
20597
|
surface.descriptor.label,
|
|
20097
|
-
surface.descriptor.machineId
|
|
20598
|
+
surface.descriptor.machineId,
|
|
20599
|
+
surface.descriptor.scope
|
|
20098
20600
|
);
|
|
20099
20601
|
const reposted = await postInventory(session, rescanned);
|
|
20100
20602
|
if (reposted.isFailure) return Result.fail(reposted.error);
|
|
@@ -20104,9 +20606,15 @@ async function maybeAdopt(session, surface, inventory, options) {
|
|
|
20104
20606
|
const importable = inventory.items.filter((item) => item.importable);
|
|
20105
20607
|
if (importable.length === 0 || options.dryRun) return Result.ok(inventory);
|
|
20106
20608
|
const wanted = options.adopt || !options.yes && await confirm(
|
|
20107
|
-
`Adopt ${plural(importable.length)} found here as approved in your directory?`
|
|
20609
|
+
`Adopt ${plural(importable.length, "skill")} found here as approved in your directory?`
|
|
20108
20610
|
);
|
|
20109
20611
|
if (!wanted) return Result.ok(inventory);
|
|
20612
|
+
process.stdout.write(
|
|
20613
|
+
dim(
|
|
20614
|
+
`adopting ${plural(importable.length, "skill")}, this can take a minute...
|
|
20615
|
+
`
|
|
20616
|
+
)
|
|
20617
|
+
);
|
|
20110
20618
|
const adopted = await apiRequest({
|
|
20111
20619
|
session,
|
|
20112
20620
|
method: "POST",
|
|
@@ -20125,7 +20633,7 @@ async function maybeAdopt(session, surface, inventory, options) {
|
|
|
20125
20633
|
const names = adopted.value.adopted;
|
|
20126
20634
|
process.stdout.write(
|
|
20127
20635
|
names.length === 0 ? `${dim("nothing adopted")}
|
|
20128
|
-
` : `adopted ${plural(names.length)} as approved: ${names.join(", ")}
|
|
20636
|
+
` : `adopted ${plural(names.length, "skill")} as approved: ${names.join(", ")}
|
|
20129
20637
|
`
|
|
20130
20638
|
);
|
|
20131
20639
|
for (const skip of adopted.value.skipped) {
|
|
@@ -20135,11 +20643,26 @@ async function maybeAdopt(session, surface, inventory, options) {
|
|
|
20135
20643
|
if (adopted.value.adopted.length === 0) return Result.ok(inventory);
|
|
20136
20644
|
return postInventory(session, surface);
|
|
20137
20645
|
}
|
|
20138
|
-
function plural(n) {
|
|
20139
|
-
return `${n} skill${n === 1 ? "" : "s"}`;
|
|
20140
|
-
}
|
|
20141
20646
|
async function applyPlan(surface, plans, approved) {
|
|
20142
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
|
+
}
|
|
20143
20666
|
if (plan.action !== "install" && plan.action !== "update") continue;
|
|
20144
20667
|
const skill = approved.find((entry) => entry.name === plan.name);
|
|
20145
20668
|
if (skill === void 0) continue;
|
|
@@ -20159,19 +20682,19 @@ async function applyPlan(surface, plans, approved) {
|
|
|
20159
20682
|
});
|
|
20160
20683
|
if (written.isFailure) return written;
|
|
20161
20684
|
process.stdout.write(
|
|
20162
|
-
`${plan.action === "install" ? "installed" : "updated"} ${plan.name}
|
|
20685
|
+
`${plan.action === "install" ? "installed" : "updated"} ${plan.name} ${versionLabel(plan)}
|
|
20163
20686
|
`
|
|
20164
20687
|
);
|
|
20165
20688
|
}
|
|
20166
20689
|
return Result.ok(void 0);
|
|
20167
20690
|
}
|
|
20168
20691
|
async function writeTarget(root, name) {
|
|
20169
|
-
const dir =
|
|
20170
|
-
const stats = await
|
|
20692
|
+
const dir = join8(root, name);
|
|
20693
|
+
const stats = await lstat2(dir).catch(() => null);
|
|
20171
20694
|
if (stats === null || !stats.isSymbolicLink()) return Result.ok(dir);
|
|
20172
20695
|
const target = await realpath2(dir).catch(() => null);
|
|
20173
|
-
const home2 = await realpath2(
|
|
20174
|
-
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)))) {
|
|
20175
20698
|
return Result.fail(
|
|
20176
20699
|
new CliError(
|
|
20177
20700
|
"UNSAFE_LINK",
|
|
@@ -20182,7 +20705,7 @@ async function writeTarget(root, name) {
|
|
|
20182
20705
|
return Result.ok(target);
|
|
20183
20706
|
}
|
|
20184
20707
|
async function containsSymlink(dir) {
|
|
20185
|
-
const entries = await
|
|
20708
|
+
const entries = await readdir4(dir, {
|
|
20186
20709
|
recursive: true,
|
|
20187
20710
|
withFileTypes: true
|
|
20188
20711
|
}).catch(() => []);
|
|
@@ -20211,24 +20734,27 @@ async function clearPending(session, surfaceId) {
|
|
|
20211
20734
|
return Result.ok(void 0);
|
|
20212
20735
|
}
|
|
20213
20736
|
function printPlan(plans, blocked, surface) {
|
|
20737
|
+
const listed = plans.filter((plan) => plan.action !== "inherited");
|
|
20214
20738
|
const rows = [
|
|
20215
|
-
...
|
|
20739
|
+
...listed.map((plan) => [
|
|
20216
20740
|
plan.action,
|
|
20217
20741
|
plan.name,
|
|
20218
20742
|
originOf(surface, plan.name),
|
|
20219
|
-
|
|
20743
|
+
versionLabel(plan),
|
|
20220
20744
|
detailOf(plan)
|
|
20221
20745
|
]),
|
|
20222
20746
|
...blocked.map((skill) => [
|
|
20223
20747
|
accent("blocked"),
|
|
20224
20748
|
skill.name,
|
|
20225
20749
|
originOf(surface, skill.name),
|
|
20226
|
-
|
|
20750
|
+
versionLabel({ version: skill.version, release: releaseOf(skill.files) }),
|
|
20227
20751
|
`org policy: ${skill.blockedReason ?? "no reason given"}`
|
|
20228
20752
|
])
|
|
20229
20753
|
];
|
|
20754
|
+
const inherited = plans.length - listed.length;
|
|
20230
20755
|
if (rows.length === 0) {
|
|
20231
|
-
|
|
20756
|
+
const note = inherited > 0 ? `nothing to sync, ${inherited} inherited from ~/.claude/skills` : "nothing to sync";
|
|
20757
|
+
process.stdout.write(`${dim(note)}
|
|
20232
20758
|
|
|
20233
20759
|
`);
|
|
20234
20760
|
return;
|
|
@@ -20254,6 +20780,11 @@ function printPlan(plans, blocked, surface) {
|
|
|
20254
20780
|
[plans.filter((plan) => plan.action === "install").length, "to install"],
|
|
20255
20781
|
[plans.filter((plan) => plan.action === "update").length, "to update"],
|
|
20256
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
|
+
],
|
|
20257
20788
|
[plans.filter((plan) => plan.action === "skip").length, "skipped"],
|
|
20258
20789
|
[blocked.length, "blocked"]
|
|
20259
20790
|
];
|
|
@@ -20268,10 +20799,123 @@ function detailOf(plan) {
|
|
|
20268
20799
|
if (plan.action === "skip") {
|
|
20269
20800
|
return `${accent("customized locally")}, use --force to overwrite`;
|
|
20270
20801
|
}
|
|
20802
|
+
if (plan.action === "remove") {
|
|
20803
|
+
return "duplicate of ~/.claude/skills, Claude Code loads it from there";
|
|
20804
|
+
}
|
|
20271
20805
|
if (plan.action === "install") return `+${plan.added.length}`;
|
|
20272
20806
|
return `+${plan.added.length} ~${plan.changed.length} -${plan.removed.length}`;
|
|
20273
20807
|
}
|
|
20274
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
|
+
|
|
20275
20919
|
// src/index.ts
|
|
20276
20920
|
var GLOBAL_FLAGS = [
|
|
20277
20921
|
{
|
|
@@ -20333,6 +20977,54 @@ var COMMANDS = [
|
|
|
20333
20977
|
{ spec: "--force", help: "Overwrite skills you customized locally" }
|
|
20334
20978
|
]
|
|
20335
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
|
+
},
|
|
20336
21028
|
{
|
|
20337
21029
|
name: "push",
|
|
20338
21030
|
usage: "hubskillz push <skill-dir> [-m MESSAGE]",
|
|
@@ -20353,7 +21045,7 @@ function flagLines(flags) {
|
|
|
20353
21045
|
}
|
|
20354
21046
|
function usage() {
|
|
20355
21047
|
const width = Math.max(...COMMANDS.map((command) => command.name.length));
|
|
20356
|
-
return `${bold("hubskillz")} ${dim(`v${"0.
|
|
21048
|
+
return `${bold("hubskillz")} ${dim(`v${"0.4.0"}`)} keep your agent skills in sync
|
|
20357
21049
|
|
|
20358
21050
|
${bold("Usage")}
|
|
20359
21051
|
hubskillz <command> [flags]
|
|
@@ -20420,6 +21112,7 @@ async function run() {
|
|
|
20420
21112
|
"dry-run": { type: "boolean", default: false },
|
|
20421
21113
|
force: { type: "boolean", default: false },
|
|
20422
21114
|
message: { type: "string", short: "m" },
|
|
21115
|
+
from: { type: "string" },
|
|
20423
21116
|
help: { type: "boolean", short: "h", default: false },
|
|
20424
21117
|
version: { type: "boolean", short: "v", default: false }
|
|
20425
21118
|
}
|
|
@@ -20432,7 +21125,7 @@ Run \`hubskillz help\` for usage.`)
|
|
|
20432
21125
|
);
|
|
20433
21126
|
}
|
|
20434
21127
|
if (values.version === true) {
|
|
20435
|
-
process.stdout.write(`${"0.
|
|
21128
|
+
process.stdout.write(`${"0.4.0"}
|
|
20436
21129
|
`);
|
|
20437
21130
|
return Result.ok(void 0);
|
|
20438
21131
|
}
|
|
@@ -20463,6 +21156,50 @@ Run \`hubskillz help\` for usage.`)
|
|
|
20463
21156
|
dryRun: values["dry-run"] === true,
|
|
20464
21157
|
force: values.force === true
|
|
20465
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
|
+
}
|
|
20466
21203
|
case "projects":
|
|
20467
21204
|
return projects({
|
|
20468
21205
|
action: positionals[1],
|