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.
Files changed (3) hide show
  1. package/README.md +14 -23
  2. package/dist/index.js +33 -360
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,9 +4,9 @@
4
4
  [![CI](https://github.com/JejeDurden2/hubskillz-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/JejeDurden2/hubskillz-cli/actions/workflows/ci.yml)
5
5
  [![license](https://img.shields.io/npm/l/hubskillz)](https://github.com/JejeDurden2/hubskillz-cli/blob/main/LICENSE)
6
6
 
7
- Keep the Claude Code skills on your machine in sync with your organization's approved directory.
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`, reports the state of every skill against the directory, and installs the approved versions after showing you the plan. Skills you customized are never overwritten unless you ask.
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 # install the approved set, adopt what the directory lacks
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] [--adopt] [--yes] [--dry-run] [--force]
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
- Installs missing skills and updates drifted ones to the approved version. Shows the plan first and asks for confirmation. Skills you changed locally are left untouched unless you pass `--force`.
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 act on. Default: the current directory.
72
- - `--all`: act on 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
- - `--adopt`: add every importable skill found on this surface to the directory as an approved version, without asking. Importable means installed here and absent from the directory: an upstream skill is pinned at its installed hash, a private skill is created from its snapshot. Maintainer only, other roles get a notice and keep syncing. Without the flag, an interactive run asks the same question once; `--yes` alone answers no and `--dry-run` skips it.
74
- - `--yes`, `-y`: apply without asking.
75
- - `--dry-run`: print the plan and stop.
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 is the one command that walks past your approved versions: it installs upstream head, whatever version your directory pinned. `hubskillz status` then reports those skills as drifted or customized, and `hubskillz sync` puts the pinned versions back.
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 the command that clears it: `hubskillz sync` for a redundant project copy, `hubskillz move <skill> global` for a skill repeated across projects, `hubskillz projects remove` for a dead registration.
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: keep the global root up to date and every project is covered, `sync` writes no copy into the repo. A project copy that matches a known version (unmodified or late) is a redundant shadow of the global one: `sync` removes it after showing the plan. `sync` never removes a customized copy.
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: `sync` needs `--yes` to apply, `projects discover` registers everything found.
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, `npx hubskillz sync` clears it" : "differs from ~/.claude/skills and wins over it here"
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
- "Install the approved set everywhere, adopt what the directory lacks",
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 the directory yet, run npx hubskillz sync to adopt them:`)} ${importable.join(", ")}
20324
+ `${dim(`${importable.length} not in your directory yet, run npx hubskillz sync to add them:`)} ${importable.join(", ")}
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: run npx hubskillz sync to remove ${duplicates === 1 ? "it" : "them"}`)}
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")} to install or update: run npx hubskillz sync`)}
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, options);
20412
+ const result = await syncSurface(session, surface);
20550
20413
  if (result.isFailure) return result;
20551
20414
  }
20552
- if (!options.dryRun && config2.value.firstSyncAt === void 0 && process.env["HUBSKILLZ_TOKEN"] === void 0) {
20415
+ if (config2.value.firstSyncAt === void 0 && process.env["HUBSKILLZ_TOKEN"] === void 0) {
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, options) {
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 surfaceId = first.value.surfaceId;
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
- const approved = await apiRequest({
20571
- session,
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 maybeAdopt(session, surface, inventory, options) {
20435
+ async function adoptImportable(session, surface, inventory) {
20606
20436
  const importable = inventory.items.filter((item) => item.importable);
20607
- if (importable.length === 0 || options.dryRun) return Result.ok(inventory);
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
- `adopting ${plural(importable.length, "skill")}, this can take a minute...
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 adopted: ask a maintainer to adopt these skills")}
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
- return postInventory(session, surface);
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 homedir7 } from "node:os";
20812
- import { resolve as resolve8 } from "node:path";
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: homedir7(),
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(resolve8(dir)),
20824
- cwd: resolve8(dir),
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 = resolve8(path ?? process.cwd());
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] [--adopt] [--yes] [--dry-run] [--force]",
20961
- summary: "Install and update skills to the approved versions",
20641
+ usage: "hubskillz sync [--path DIR] [--all] [--yes]",
20642
+ summary: "Upload the skills as they are, like a git push",
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: "--adopt",
20973
- help: "Add importable skills to the directory as approved (maintainers)"
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.4.0"}`)} keep your agent skills in sync
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.4.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
- adopt: values.adopt === true,
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hubskillz",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "Keep your Claude Code skills in sync with your organization's approved catalog.",
5
5
  "license": "MIT",
6
6
  "author": "Jérôme Desmares",