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