hubskillz 0.4.0 → 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 +14 -23
- package/dist/index.js +33 -360
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
[](https://github.com/JejeDurden2/hubskillz-cli/actions/workflows/ci.yml)
|
|
5
5
|
[](https://github.com/JejeDurden2/hubskillz-cli/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
See every Claude Code skill on your machine in one place, share it, and keep your directory current.
|
|
8
8
|
|
|
9
|
-
`hubskillz` inventories `~/.claude/skills` and `<repo>/.claude/skills
|
|
9
|
+
`hubskillz` inventories `~/.claude/skills` and `<repo>/.claude/skills` and uploads that layout to your directory, the way a git push sends your files to GitHub. It never writes a local file: your repos are the source of truth, the directory is the picture of them.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -25,7 +25,7 @@ hubskillz login # paste a device token from Settings, Device tokens
|
|
|
25
25
|
hubskillz doctor # what is broken or duplicated on this machine
|
|
26
26
|
hubskillz upgrade --all # every skills.sh skill to its latest upstream
|
|
27
27
|
hubskillz status # what is installed, and how it compares
|
|
28
|
-
hubskillz sync --all #
|
|
28
|
+
hubskillz sync --all # upload every skill of this machine to your directory
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
The bare `hubskillz` command prints this quickstart, with your progress, until the first sync completes.
|
|
@@ -36,7 +36,7 @@ The bare `hubskillz` command prints this quickstart, with your progress, until t
|
|
|
36
36
|
hubskillz login [--token TOKEN] [--base-url URL]
|
|
37
37
|
hubskillz logout
|
|
38
38
|
hubskillz status [--path DIR] [--yes]
|
|
39
|
-
hubskillz sync [--path DIR] [--all] [--
|
|
39
|
+
hubskillz sync [--path DIR] [--all] [--yes]
|
|
40
40
|
hubskillz upgrade [SKILL...] [--path DIR] [--all] [--yes]
|
|
41
41
|
hubskillz doctor [--path DIR]
|
|
42
42
|
hubskillz move <skill> <global|DIR> [--from global|DIR] [--force]
|
|
@@ -66,14 +66,13 @@ What the inventory contains: for every skill, its name, content hash and per-fil
|
|
|
66
66
|
|
|
67
67
|
### sync
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Uploads the skills as they sit on disk and prints their state, like a git push: the directory mirrors the machine. `sync` never writes a local file. A skill the directory does not know yet is added to it automatically: an upstream skill is pinned at its installed hash, a private skill is created from its snapshot. In a team org that step is maintainer only, other roles get a notice and keep syncing.
|
|
70
70
|
|
|
71
|
-
- `--path DIR`: project directory to
|
|
72
|
-
- `--all`:
|
|
73
|
-
- `--
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
- `--force`: overwrite locally customized skills.
|
|
71
|
+
- `--path DIR`: project directory to report. Default: the current directory.
|
|
72
|
+
- `--all`: cover the global root, the project root and every registered project. With nothing registered yet, it first scans your home directory for repos with `.claude/skills` and asks which ones to register (see `projects discover`). Default: the project root when `DIR/.claude/skills` exists, else the global root.
|
|
73
|
+
- `--yes`, `-y`: register every discovered project without asking.
|
|
74
|
+
|
|
75
|
+
To change what a repo holds, edit it like any other file and `sync` again: `hubskillz move` carries a skill between roots, `hubskillz upgrade` updates skills.sh skills, deleting the folder removes the skill.
|
|
77
76
|
|
|
78
77
|
### upgrade
|
|
79
78
|
|
|
@@ -89,7 +88,7 @@ hubskillz fetches nothing here. It reads each skills.sh lock to know what is ins
|
|
|
89
88
|
|
|
90
89
|
`upgrade` looks a named skill up in every root, so `hubskillz upgrade find-skills` works from anywhere. A name no lock lists is an error. `--yes` passes `-y` through to skills.sh.
|
|
91
90
|
|
|
92
|
-
This
|
|
91
|
+
This command installs upstream head, whatever version your directory pinned. `hubskillz sync` afterwards reports the new versions to the directory, where you review and approve them.
|
|
93
92
|
|
|
94
93
|
### doctor
|
|
95
94
|
|
|
@@ -100,7 +99,7 @@ Reads every local skills root and reports what no agent loads, and what loads tw
|
|
|
100
99
|
| `error` | A folder with no `SKILL.md`, an empty folder, a symlink whose target is gone. Nothing loads. |
|
|
101
100
|
| `warn` | A `SKILL.md` without a `name` or a `description`, a copy that duplicates or shadows `~/.claude/skills`, the same skill sitting in several projects, a registered repo that lost its `.claude/skills`. |
|
|
102
101
|
|
|
103
|
-
Every warning names
|
|
102
|
+
Every warning names what clears it: deleting a redundant project copy, `hubskillz move <skill> global` for a skill repeated across projects, `hubskillz projects remove` for a dead registration.
|
|
104
103
|
|
|
105
104
|
### move
|
|
106
105
|
|
|
@@ -142,9 +141,7 @@ Registers repos so `sync --all` and `status` cover them from anywhere, in one ru
|
|
|
142
141
|
- Global root: `~/.claude/skills`
|
|
143
142
|
- Project root: `<project>/.claude/skills`
|
|
144
143
|
|
|
145
|
-
Claude Code loads the global root in every project on the machine. A skill installed there reports `inherited` on a project surface:
|
|
146
|
-
|
|
147
|
-
A symlinked skill directory (the skills.sh canonical copy under `~/.agents/skills`) is written through, so every agent on the machine sees the approved version. Links that leave your home directory are refused.
|
|
144
|
+
Claude Code loads the global root in every project on the machine. A skill installed there reports `inherited` on a project surface: it is already inside the project, nothing needs a copy in the repo. A project copy that matches a known version is a redundant shadow of the global one; `doctor` points it out and you delete it when you want.
|
|
148
145
|
|
|
149
146
|
## CI and containers
|
|
150
147
|
|
|
@@ -154,13 +151,7 @@ Set `HUBSKILLZ_TOKEN` and no config file is needed:
|
|
|
154
151
|
HUBSKILLZ_TOKEN=... npx hubskillz sync --all --yes
|
|
155
152
|
```
|
|
156
153
|
|
|
157
|
-
Prompts are skipped when stdin is not a TTY: `
|
|
158
|
-
|
|
159
|
-
Update every machine and every repo from the directory, without installing anything:
|
|
160
|
-
|
|
161
|
-
```sh
|
|
162
|
-
npx hubskillz sync --all --yes
|
|
163
|
-
```
|
|
154
|
+
Prompts are skipped when stdin is not a TTY: `projects discover` registers everything found.
|
|
164
155
|
|
|
165
156
|
## Configuration
|
|
166
157
|
|
package/dist/index.js
CHANGED
|
@@ -19143,19 +19143,6 @@ function parseSkillMd(source) {
|
|
|
19143
19143
|
body: source.slice(match[0].length).replace(/^(\r?\n)+/, "")
|
|
19144
19144
|
};
|
|
19145
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
19146
|
|
|
19160
19147
|
// src/commands/doctor.ts
|
|
19161
19148
|
import { existsSync as existsSync2 } from "node:fs";
|
|
@@ -19606,7 +19593,7 @@ function duplicateFindings(surfaces) {
|
|
|
19606
19593
|
level: "warn",
|
|
19607
19594
|
skill: skill.name,
|
|
19608
19595
|
where: whereOf(surface),
|
|
19609
|
-
problem: twin.contentHash === skill.contentHash ? "same content as ~/.claude/skills,
|
|
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"
|
|
19610
19597
|
});
|
|
19611
19598
|
}
|
|
19612
19599
|
}
|
|
@@ -19858,16 +19845,6 @@ async function promptSecret(label) {
|
|
|
19858
19845
|
input2.on("data", onData);
|
|
19859
19846
|
});
|
|
19860
19847
|
}
|
|
19861
|
-
async function confirm(question) {
|
|
19862
|
-
if (process.stdin.isTTY !== true) return false;
|
|
19863
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
19864
|
-
try {
|
|
19865
|
-
const answer = await rl.question(`${question} [y/N] `);
|
|
19866
|
-
return answer.trim().toLowerCase() === "y";
|
|
19867
|
-
} finally {
|
|
19868
|
-
rl.close();
|
|
19869
|
-
}
|
|
19870
|
-
}
|
|
19871
19848
|
function parseSelection(answer, count) {
|
|
19872
19849
|
const text = answer.trim().toLowerCase();
|
|
19873
19850
|
if (text === "" || text === "all" || text === "a") {
|
|
@@ -19919,7 +19896,7 @@ function quickstart(config2) {
|
|
|
19919
19896
|
"hubskillz status"
|
|
19920
19897
|
) + step(
|
|
19921
19898
|
synced,
|
|
19922
|
-
"
|
|
19899
|
+
"Upload every skill of this machine to your directory",
|
|
19923
19900
|
"hubskillz sync --all"
|
|
19924
19901
|
) + `
|
|
19925
19902
|
${dim("Run `hubskillz help` for every command, `hubskillz help <command>` for its flags.")}
|
|
@@ -20344,7 +20321,7 @@ function printNotes(surface, items, baseUrl) {
|
|
|
20344
20321
|
const importable = items.filter((item) => item.importable).map((item) => item.name);
|
|
20345
20322
|
if (importable.length > 0) {
|
|
20346
20323
|
process.stdout.write(
|
|
20347
|
-
`${dim(`${importable.length} not in
|
|
20324
|
+
`${dim(`${importable.length} not in your directory yet, run npx hubskillz sync to add them:`)} ${importable.join(", ")}
|
|
20348
20325
|
`
|
|
20349
20326
|
);
|
|
20350
20327
|
}
|
|
@@ -20390,7 +20367,7 @@ function printSurface(surface, inventory, baseUrl) {
|
|
|
20390
20367
|
).length;
|
|
20391
20368
|
if (duplicates > 0) {
|
|
20392
20369
|
process.stdout.write(
|
|
20393
|
-
`${dim(`${duplicates} duplicate cop${duplicates === 1 ? "y" : "ies"} here:
|
|
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`)}
|
|
20394
20371
|
`
|
|
20395
20372
|
);
|
|
20396
20373
|
}
|
|
@@ -20410,126 +20387,12 @@ function printSurface(surface, inventory, baseUrl) {
|
|
|
20410
20387
|
).length;
|
|
20411
20388
|
if (behind > 0) {
|
|
20412
20389
|
process.stdout.write(
|
|
20413
|
-
`${dim(`${plural(behind, "skill")}
|
|
20390
|
+
`${dim(`${plural(behind, "skill")} behind the approved version: review at ${reviewUrl(baseUrl)}`)}
|
|
20414
20391
|
`
|
|
20415
20392
|
);
|
|
20416
20393
|
}
|
|
20417
20394
|
}
|
|
20418
20395
|
|
|
20419
|
-
// src/commands/sync.ts
|
|
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";
|
|
20423
|
-
|
|
20424
|
-
// src/apply.ts
|
|
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";
|
|
20427
|
-
async function applySkill(input2) {
|
|
20428
|
-
for (const path of [...input2.files.map((f) => f.path), ...input2.remove]) {
|
|
20429
|
-
if (!isSafeRelativePath(path)) {
|
|
20430
|
-
return Result.fail(
|
|
20431
|
-
new CliError(
|
|
20432
|
-
"UNSAFE_PATH",
|
|
20433
|
-
`Refusing to write outside the skill: ${path}`
|
|
20434
|
-
)
|
|
20435
|
-
);
|
|
20436
|
-
}
|
|
20437
|
-
}
|
|
20438
|
-
await mkdir3(input2.dir, { recursive: true });
|
|
20439
|
-
const staging = await mkdtemp(join7(dirname3(input2.dir), ".hubskillz-"));
|
|
20440
|
-
try {
|
|
20441
|
-
for (const file2 of input2.files) {
|
|
20442
|
-
const staged = join7(staging, ...file2.path.split("/"));
|
|
20443
|
-
await mkdir3(dirname3(staged), { recursive: true });
|
|
20444
|
-
await writeFile2(staged, file2.content, "utf8");
|
|
20445
|
-
}
|
|
20446
|
-
for (const file2 of input2.files) {
|
|
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);
|
|
20450
|
-
}
|
|
20451
|
-
for (const path of input2.remove) {
|
|
20452
|
-
const target = join7(input2.dir, ...path.split("/"));
|
|
20453
|
-
await rm3(target, { force: true });
|
|
20454
|
-
await pruneEmptyDirs(dirname3(target), input2.dir);
|
|
20455
|
-
}
|
|
20456
|
-
} finally {
|
|
20457
|
-
await rm3(staging, { recursive: true, force: true });
|
|
20458
|
-
}
|
|
20459
|
-
return Result.ok(void 0);
|
|
20460
|
-
}
|
|
20461
|
-
function isSafeRelativePath(path) {
|
|
20462
|
-
if (path === "" || path.startsWith("/") || /^[a-zA-Z]:/u.test(path)) {
|
|
20463
|
-
return false;
|
|
20464
|
-
}
|
|
20465
|
-
return !path.split(/[/\\]/u).includes("..");
|
|
20466
|
-
}
|
|
20467
|
-
async function pruneEmptyDirs(from, stopAt) {
|
|
20468
|
-
let current = from;
|
|
20469
|
-
while (current.startsWith(stopAt + sep)) {
|
|
20470
|
-
try {
|
|
20471
|
-
await rmdir(current);
|
|
20472
|
-
} catch {
|
|
20473
|
-
return;
|
|
20474
|
-
}
|
|
20475
|
-
current = dirname3(current);
|
|
20476
|
-
}
|
|
20477
|
-
}
|
|
20478
|
-
|
|
20479
|
-
// src/plan.ts
|
|
20480
|
-
function computePlan(input2) {
|
|
20481
|
-
const plans = [];
|
|
20482
|
-
for (const skill of input2.approved) {
|
|
20483
|
-
const local = input2.local.find((entry) => entry.name === skill.name);
|
|
20484
|
-
const item = input2.items.find((entry) => entry.name === skill.name);
|
|
20485
|
-
const state = item?.state ?? (local === void 0 ? "missing" : "customized");
|
|
20486
|
-
const localFiles = new Map(
|
|
20487
|
-
(local?.files ?? []).map((file2) => [file2.path, file2.content])
|
|
20488
|
-
);
|
|
20489
|
-
const wanted = new Map(
|
|
20490
|
-
skill.files.map((file2) => [file2.path, file2.content])
|
|
20491
|
-
);
|
|
20492
|
-
const added = [];
|
|
20493
|
-
const changed = [];
|
|
20494
|
-
for (const [path, content] of wanted) {
|
|
20495
|
-
const current = localFiles.get(path);
|
|
20496
|
-
if (current === void 0) added.push(path);
|
|
20497
|
-
else if (current !== content) changed.push(path);
|
|
20498
|
-
}
|
|
20499
|
-
const removed = [...localFiles.keys()].filter((path) => !wanted.has(path));
|
|
20500
|
-
plans.push({
|
|
20501
|
-
name: skill.name,
|
|
20502
|
-
state,
|
|
20503
|
-
version: skill.version,
|
|
20504
|
-
release: releaseOf(skill.files),
|
|
20505
|
-
action: actionFor(
|
|
20506
|
-
state,
|
|
20507
|
-
local !== void 0,
|
|
20508
|
-
added.length + changed.length + removed.length,
|
|
20509
|
-
input2.force
|
|
20510
|
-
),
|
|
20511
|
-
added: added.sort(),
|
|
20512
|
-
changed: changed.sort(),
|
|
20513
|
-
removed: removed.sort()
|
|
20514
|
-
});
|
|
20515
|
-
}
|
|
20516
|
-
return plans.sort((a, b) => a.name < b.name ? -1 : 1);
|
|
20517
|
-
}
|
|
20518
|
-
function versionLabel(plan) {
|
|
20519
|
-
return plan.release ?? `v${plan.version}`;
|
|
20520
|
-
}
|
|
20521
|
-
function actionFor(state, installed, diffCount, force) {
|
|
20522
|
-
if (state === "inherited") return installed ? "remove" : "inherited";
|
|
20523
|
-
if (state === "customized" && !force) return "skip";
|
|
20524
|
-
if (!installed) return "install";
|
|
20525
|
-
return diffCount === 0 ? "keep" : "update";
|
|
20526
|
-
}
|
|
20527
|
-
function planHasWrites(plans) {
|
|
20528
|
-
return plans.some(
|
|
20529
|
-
(plan) => plan.action === "install" || plan.action === "update" || plan.action === "remove"
|
|
20530
|
-
);
|
|
20531
|
-
}
|
|
20532
|
-
|
|
20533
20396
|
// src/commands/sync.ts
|
|
20534
20397
|
async function sync(options) {
|
|
20535
20398
|
const config2 = await readConfig();
|
|
@@ -20546,10 +20409,10 @@ async function sync(options) {
|
|
|
20546
20409
|
projects2
|
|
20547
20410
|
);
|
|
20548
20411
|
for (const surface of surfaces) {
|
|
20549
|
-
const result = await syncSurface(session, surface
|
|
20412
|
+
const result = await syncSurface(session, surface);
|
|
20550
20413
|
if (result.isFailure) return result;
|
|
20551
20414
|
}
|
|
20552
|
-
if (
|
|
20415
|
+
if (config2.value.firstSyncAt === void 0 && process.env["HUBSKILLZ_TOKEN"] === void 0) {
|
|
20553
20416
|
const stored = await readConfig();
|
|
20554
20417
|
if (stored.isSuccess && stored.value.firstSyncAt === void 0) {
|
|
20555
20418
|
await writeConfig({
|
|
@@ -20561,57 +20424,20 @@ async function sync(options) {
|
|
|
20561
20424
|
}
|
|
20562
20425
|
return Result.ok(void 0);
|
|
20563
20426
|
}
|
|
20564
|
-
async function syncSurface(session, surface
|
|
20427
|
+
async function syncSurface(session, surface) {
|
|
20565
20428
|
const first = await postInventory(session, surface);
|
|
20566
20429
|
if (first.isFailure) return Result.fail(first.error);
|
|
20567
|
-
const
|
|
20568
|
-
const inventory = await maybeAdopt(session, surface, first.value, options);
|
|
20430
|
+
const inventory = await adoptImportable(session, surface, first.value);
|
|
20569
20431
|
if (inventory.isFailure) return Result.fail(inventory.error);
|
|
20570
|
-
|
|
20571
|
-
|
|
20572
|
-
method: "GET",
|
|
20573
|
-
path: `/api/cli/approved?surfaceId=${encodeURIComponent(surfaceId)}`,
|
|
20574
|
-
schema: approvedResponseSchema
|
|
20575
|
-
});
|
|
20576
|
-
if (approved.isFailure) return Result.fail(approved.error);
|
|
20577
|
-
const blocked = approved.value.skills.filter((skill) => skill.blocked);
|
|
20578
|
-
const plans = computePlan({
|
|
20579
|
-
items: inventory.value.items,
|
|
20580
|
-
approved: approved.value.skills.filter((skill) => !skill.blocked),
|
|
20581
|
-
local: surface.skills,
|
|
20582
|
-
force: options.force
|
|
20583
|
-
});
|
|
20584
|
-
printHeader(surface);
|
|
20585
|
-
printPlan(plans, blocked, surface);
|
|
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);
|
|
20589
|
-
if (!options.yes && !await confirm("Apply?")) {
|
|
20590
|
-
process.stdout.write("Nothing applied.\n");
|
|
20591
|
-
return Result.ok(void 0);
|
|
20592
|
-
}
|
|
20593
|
-
const applied = await applyPlan(surface, plans, approved.value.skills);
|
|
20594
|
-
if (applied.isFailure) return applied;
|
|
20595
|
-
const rescanned = await scanSurface(
|
|
20596
|
-
surface.descriptor.path,
|
|
20597
|
-
surface.descriptor.label,
|
|
20598
|
-
surface.descriptor.machineId,
|
|
20599
|
-
surface.descriptor.scope
|
|
20600
|
-
);
|
|
20601
|
-
const reposted = await postInventory(session, rescanned);
|
|
20602
|
-
if (reposted.isFailure) return Result.fail(reposted.error);
|
|
20603
|
-
return clearPending(session, surfaceId);
|
|
20432
|
+
printSurface(surface, inventory.value, session.baseUrl);
|
|
20433
|
+
return clearPending(session, first.value.surfaceId);
|
|
20604
20434
|
}
|
|
20605
|
-
async function
|
|
20435
|
+
async function adoptImportable(session, surface, inventory) {
|
|
20606
20436
|
const importable = inventory.items.filter((item) => item.importable);
|
|
20607
|
-
if (importable.length === 0
|
|
20608
|
-
const wanted = options.adopt || !options.yes && await confirm(
|
|
20609
|
-
`Adopt ${plural(importable.length, "skill")} found here as approved in your directory?`
|
|
20610
|
-
);
|
|
20611
|
-
if (!wanted) return Result.ok(inventory);
|
|
20437
|
+
if (importable.length === 0) return Result.ok(inventory);
|
|
20612
20438
|
process.stdout.write(
|
|
20613
20439
|
dim(
|
|
20614
|
-
`
|
|
20440
|
+
`adding ${plural(importable.length, "skill")} to your directory, this can take a minute...
|
|
20615
20441
|
`
|
|
20616
20442
|
)
|
|
20617
20443
|
);
|
|
@@ -20624,95 +20450,22 @@ async function maybeAdopt(session, surface, inventory, options) {
|
|
|
20624
20450
|
});
|
|
20625
20451
|
if (adopted.isFailure && adopted.error.code === "FORBIDDEN") {
|
|
20626
20452
|
process.stdout.write(
|
|
20627
|
-
`${dim("not
|
|
20453
|
+
`${dim("not added: ask a maintainer to adopt these skills")}
|
|
20628
20454
|
`
|
|
20629
20455
|
);
|
|
20630
20456
|
return Result.ok(inventory);
|
|
20631
20457
|
}
|
|
20632
20458
|
if (adopted.isFailure) return Result.fail(adopted.error);
|
|
20633
|
-
const names = adopted.value.adopted;
|
|
20634
|
-
process.stdout.write(
|
|
20635
|
-
names.length === 0 ? `${dim("nothing adopted")}
|
|
20636
|
-
` : `adopted ${plural(names.length, "skill")} as approved: ${names.join(", ")}
|
|
20637
|
-
`
|
|
20638
|
-
);
|
|
20639
20459
|
for (const skip of adopted.value.skipped) {
|
|
20640
20460
|
process.stdout.write(dim(`skipped ${skip.name} (${skip.code})
|
|
20641
20461
|
`));
|
|
20642
20462
|
}
|
|
20643
20463
|
if (adopted.value.adopted.length === 0) return Result.ok(inventory);
|
|
20644
|
-
|
|
20645
|
-
}
|
|
20646
|
-
async function applyPlan(surface, plans, approved) {
|
|
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
|
-
}
|
|
20666
|
-
if (plan.action !== "install" && plan.action !== "update") continue;
|
|
20667
|
-
const skill = approved.find((entry) => entry.name === plan.name);
|
|
20668
|
-
if (skill === void 0) continue;
|
|
20669
|
-
const dir = await writeTarget(surface.descriptor.path, plan.name);
|
|
20670
|
-
if (dir.isFailure) return Result.fail(dir.error);
|
|
20671
|
-
if (await containsSymlink(dir.value)) {
|
|
20672
|
-
process.stdout.write(
|
|
20673
|
-
`${plan.name}: contains a symlink, refusing to write
|
|
20674
|
-
`
|
|
20675
|
-
);
|
|
20676
|
-
continue;
|
|
20677
|
-
}
|
|
20678
|
-
const written = await applySkill({
|
|
20679
|
-
dir: dir.value,
|
|
20680
|
-
files: skill.files,
|
|
20681
|
-
remove: plan.removed
|
|
20682
|
-
});
|
|
20683
|
-
if (written.isFailure) return written;
|
|
20684
|
-
process.stdout.write(
|
|
20685
|
-
`${plan.action === "install" ? "installed" : "updated"} ${plan.name} ${versionLabel(plan)}
|
|
20464
|
+
process.stdout.write(
|
|
20465
|
+
`added ${plural(adopted.value.adopted.length, "skill")} to your directory: ${adopted.value.adopted.join(", ")}
|
|
20686
20466
|
`
|
|
20687
|
-
|
|
20688
|
-
|
|
20689
|
-
return Result.ok(void 0);
|
|
20690
|
-
}
|
|
20691
|
-
async function writeTarget(root, name) {
|
|
20692
|
-
const dir = join8(root, name);
|
|
20693
|
-
const stats = await lstat2(dir).catch(() => null);
|
|
20694
|
-
if (stats === null || !stats.isSymbolicLink()) return Result.ok(dir);
|
|
20695
|
-
const target = await realpath2(dir).catch(() => null);
|
|
20696
|
-
const home2 = await realpath2(homedir6()).catch(() => homedir6());
|
|
20697
|
-
if (target === null || !isInside(target, home2) || isInside(target, await realpath2(root).catch(() => resolve7(root)))) {
|
|
20698
|
-
return Result.fail(
|
|
20699
|
-
new CliError(
|
|
20700
|
-
"UNSAFE_LINK",
|
|
20701
|
-
`Refusing to write through ${dir}: it points to ${target ?? "nowhere"}.`
|
|
20702
|
-
)
|
|
20703
|
-
);
|
|
20704
|
-
}
|
|
20705
|
-
return Result.ok(target);
|
|
20706
|
-
}
|
|
20707
|
-
async function containsSymlink(dir) {
|
|
20708
|
-
const entries = await readdir4(dir, {
|
|
20709
|
-
recursive: true,
|
|
20710
|
-
withFileTypes: true
|
|
20711
|
-
}).catch(() => []);
|
|
20712
|
-
return entries.some((entry) => entry.isSymbolicLink());
|
|
20713
|
-
}
|
|
20714
|
-
function isInside(path, dir) {
|
|
20715
|
-
return path.startsWith(dir + sep2);
|
|
20467
|
+
);
|
|
20468
|
+
return postInventory(session, surface);
|
|
20716
20469
|
}
|
|
20717
20470
|
async function clearPending(session, surfaceId) {
|
|
20718
20471
|
const pending = await apiRequest({
|
|
@@ -20733,101 +20486,29 @@ async function clearPending(session, surfaceId) {
|
|
|
20733
20486
|
}
|
|
20734
20487
|
return Result.ok(void 0);
|
|
20735
20488
|
}
|
|
20736
|
-
function printPlan(plans, blocked, surface) {
|
|
20737
|
-
const listed = plans.filter((plan) => plan.action !== "inherited");
|
|
20738
|
-
const rows = [
|
|
20739
|
-
...listed.map((plan) => [
|
|
20740
|
-
plan.action,
|
|
20741
|
-
plan.name,
|
|
20742
|
-
originOf(surface, plan.name),
|
|
20743
|
-
versionLabel(plan),
|
|
20744
|
-
detailOf(plan)
|
|
20745
|
-
]),
|
|
20746
|
-
...blocked.map((skill) => [
|
|
20747
|
-
accent("blocked"),
|
|
20748
|
-
skill.name,
|
|
20749
|
-
originOf(surface, skill.name),
|
|
20750
|
-
versionLabel({ version: skill.version, release: releaseOf(skill.files) }),
|
|
20751
|
-
`org policy: ${skill.blockedReason ?? "no reason given"}`
|
|
20752
|
-
])
|
|
20753
|
-
];
|
|
20754
|
-
const inherited = plans.length - listed.length;
|
|
20755
|
-
if (rows.length === 0) {
|
|
20756
|
-
const note = inherited > 0 ? `nothing to sync, ${inherited} inherited from ~/.claude/skills` : "nothing to sync";
|
|
20757
|
-
process.stdout.write(`${dim(note)}
|
|
20758
|
-
|
|
20759
|
-
`);
|
|
20760
|
-
return;
|
|
20761
|
-
}
|
|
20762
|
-
process.stdout.write(
|
|
20763
|
-
`${table(["ACTION", "SKILL", "ORIGIN", "VERSION", "FILES"], rows)}
|
|
20764
|
-
`
|
|
20765
|
-
);
|
|
20766
|
-
for (const plan of plans) {
|
|
20767
|
-
if (plan.action !== "update") continue;
|
|
20768
|
-
for (const [marker, paths] of [
|
|
20769
|
-
["+", plan.added],
|
|
20770
|
-
["~", plan.changed],
|
|
20771
|
-
["-", plan.removed]
|
|
20772
|
-
]) {
|
|
20773
|
-
for (const path of paths) {
|
|
20774
|
-
process.stdout.write(dim(` ${marker} ${plan.name}/${path}
|
|
20775
|
-
`));
|
|
20776
|
-
}
|
|
20777
|
-
}
|
|
20778
|
-
}
|
|
20779
|
-
const counts = [
|
|
20780
|
-
[plans.filter((plan) => plan.action === "install").length, "to install"],
|
|
20781
|
-
[plans.filter((plan) => plan.action === "update").length, "to update"],
|
|
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
|
-
],
|
|
20788
|
-
[plans.filter((plan) => plan.action === "skip").length, "skipped"],
|
|
20789
|
-
[blocked.length, "blocked"]
|
|
20790
|
-
];
|
|
20791
|
-
process.stdout.write(
|
|
20792
|
-
`${counts.filter(([n]) => n > 0).map(([n, label]) => `${n} ${label}`).join(", ")}
|
|
20793
|
-
|
|
20794
|
-
`
|
|
20795
|
-
);
|
|
20796
|
-
}
|
|
20797
|
-
function detailOf(plan) {
|
|
20798
|
-
if (plan.action === "keep") return dim("up to date");
|
|
20799
|
-
if (plan.action === "skip") {
|
|
20800
|
-
return `${accent("customized locally")}, use --force to overwrite`;
|
|
20801
|
-
}
|
|
20802
|
-
if (plan.action === "remove") {
|
|
20803
|
-
return "duplicate of ~/.claude/skills, Claude Code loads it from there";
|
|
20804
|
-
}
|
|
20805
|
-
if (plan.action === "install") return `+${plan.added.length}`;
|
|
20806
|
-
return `+${plan.added.length} ~${plan.changed.length} -${plan.removed.length}`;
|
|
20807
|
-
}
|
|
20808
20489
|
|
|
20809
20490
|
// src/commands/upgrade.ts
|
|
20810
20491
|
import { spawn } from "node:child_process";
|
|
20811
|
-
import { homedir as
|
|
20812
|
-
import { resolve as
|
|
20492
|
+
import { homedir as homedir6 } from "node:os";
|
|
20493
|
+
import { resolve as resolve7 } from "node:path";
|
|
20813
20494
|
function globalRoot() {
|
|
20814
20495
|
return {
|
|
20815
20496
|
label: "global",
|
|
20816
|
-
cwd:
|
|
20497
|
+
cwd: homedir6(),
|
|
20817
20498
|
scope: "-g",
|
|
20818
20499
|
lockPath: globalLockPath()
|
|
20819
20500
|
};
|
|
20820
20501
|
}
|
|
20821
20502
|
function projectRoot(dir) {
|
|
20822
20503
|
return {
|
|
20823
|
-
label: shortPath(
|
|
20824
|
-
cwd:
|
|
20504
|
+
label: shortPath(resolve7(dir)),
|
|
20505
|
+
cwd: resolve7(dir),
|
|
20825
20506
|
scope: "-p",
|
|
20826
20507
|
lockPath: projectLockPath(dir)
|
|
20827
20508
|
};
|
|
20828
20509
|
}
|
|
20829
20510
|
function upgradeRoots(path, all, registered) {
|
|
20830
|
-
const dir =
|
|
20511
|
+
const dir = resolve7(path ?? process.cwd());
|
|
20831
20512
|
const hasProject = exists(projectSkillsRoot(dir));
|
|
20832
20513
|
if (!all) return [hasProject ? projectRoot(dir) : globalRoot()];
|
|
20833
20514
|
return [
|
|
@@ -20957,8 +20638,8 @@ var COMMANDS = [
|
|
|
20957
20638
|
},
|
|
20958
20639
|
{
|
|
20959
20640
|
name: "sync",
|
|
20960
|
-
usage: "hubskillz sync [--path DIR] [--all] [--
|
|
20961
|
-
summary: "
|
|
20641
|
+
usage: "hubskillz sync [--path DIR] [--all] [--yes]",
|
|
20642
|
+
summary: "Upload the skills as they are, like a git push",
|
|
20962
20643
|
flags: [
|
|
20963
20644
|
{
|
|
20964
20645
|
spec: "--path DIR",
|
|
@@ -20969,12 +20650,9 @@ var COMMANDS = [
|
|
|
20969
20650
|
help: "Global root, this project and every registered project"
|
|
20970
20651
|
},
|
|
20971
20652
|
{
|
|
20972
|
-
spec: "--
|
|
20973
|
-
help: "
|
|
20974
|
-
}
|
|
20975
|
-
{ spec: "-y, --yes", help: "Apply without asking" },
|
|
20976
|
-
{ spec: "--dry-run", help: "Print the plan and stop" },
|
|
20977
|
-
{ spec: "--force", help: "Overwrite skills you customized locally" }
|
|
20653
|
+
spec: "-y, --yes",
|
|
20654
|
+
help: "Register every discovered project without asking"
|
|
20655
|
+
}
|
|
20978
20656
|
]
|
|
20979
20657
|
},
|
|
20980
20658
|
{
|
|
@@ -21045,7 +20723,7 @@ function flagLines(flags) {
|
|
|
21045
20723
|
}
|
|
21046
20724
|
function usage() {
|
|
21047
20725
|
const width = Math.max(...COMMANDS.map((command) => command.name.length));
|
|
21048
|
-
return `${bold("hubskillz")} ${dim(`v${"0.
|
|
20726
|
+
return `${bold("hubskillz")} ${dim(`v${"1.0.0"}`)} keep your agent skills in sync
|
|
21049
20727
|
|
|
21050
20728
|
${bold("Usage")}
|
|
21051
20729
|
hubskillz <command> [flags]
|
|
@@ -21107,9 +20785,7 @@ async function run() {
|
|
|
21107
20785
|
token: { type: "string" },
|
|
21108
20786
|
path: { type: "string" },
|
|
21109
20787
|
all: { type: "boolean", default: false },
|
|
21110
|
-
adopt: { type: "boolean", default: false },
|
|
21111
20788
|
yes: { type: "boolean", short: "y", default: false },
|
|
21112
|
-
"dry-run": { type: "boolean", default: false },
|
|
21113
20789
|
force: { type: "boolean", default: false },
|
|
21114
20790
|
message: { type: "string", short: "m" },
|
|
21115
20791
|
from: { type: "string" },
|
|
@@ -21125,7 +20801,7 @@ Run \`hubskillz help\` for usage.`)
|
|
|
21125
20801
|
);
|
|
21126
20802
|
}
|
|
21127
20803
|
if (values.version === true) {
|
|
21128
|
-
process.stdout.write(`${"0.
|
|
20804
|
+
process.stdout.write(`${"1.0.0"}
|
|
21129
20805
|
`);
|
|
21130
20806
|
return Result.ok(void 0);
|
|
21131
20807
|
}
|
|
@@ -21151,10 +20827,7 @@ Run \`hubskillz help\` for usage.`)
|
|
|
21151
20827
|
baseUrl: values["base-url"],
|
|
21152
20828
|
path: values.path,
|
|
21153
20829
|
all: values.all === true,
|
|
21154
|
-
|
|
21155
|
-
yes: values.yes === true,
|
|
21156
|
-
dryRun: values["dry-run"] === true,
|
|
21157
|
-
force: values.force === true
|
|
20830
|
+
yes: values.yes === true
|
|
21158
20831
|
});
|
|
21159
20832
|
case "upgrade":
|
|
21160
20833
|
return upgrade({
|