@yonpark/skillhub-cli 0.3.0 → 0.3.1

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 CHANGED
@@ -70,24 +70,6 @@ Mode behavior:
70
70
  - `merge`: union local + remote, installs missing local skills, uploads when remote differs.
71
71
  - `auto`: compare `remote.updatedAt` and local `lastSyncAt`; install from remote when remote is newer, otherwise upload local when needed.
72
72
 
73
- ## Migration (Breaking Changes in 0.3.0)
74
-
75
- Removed commands:
76
-
77
- - `skillhub login`
78
- - `skillhub status`
79
- - `skillhub logout`
80
- - `skillhub sync --strategy ...`
81
- - `skillhub sync` (without a subcommand)
82
-
83
- New mapping:
84
-
85
- - `skillhub login` -> `skillhub auth login`
86
- - `skillhub status` -> `skillhub auth status`
87
- - `skillhub logout` -> `skillhub auth logout`
88
- - `skillhub sync --strategy union` -> `skillhub sync merge`
89
- - `skillhub sync --strategy latest` -> `skillhub sync auto`
90
-
91
73
  ## Payload Format
92
74
 
93
75
  `skillhub.json` in Gist:
@@ -134,3 +116,4 @@ CI release workflow publishes to GitHub Packages only:
134
116
  - GitHub Packages target package is `@yw9142/skillhub-cli`.
135
117
 
136
118
  npmjs publish is manual.
119
+
@@ -15,7 +15,7 @@ const node_util_1 = require("node:util");
15
15
  const syncCore_1 = require("../core/syncCore");
16
16
  const retry_1 = require("../utils/retry");
17
17
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
18
- const SKILLS_LOCK_FILENAME = "skills-lock.json";
18
+ const SKILLS_LOCK_FILENAMES = ["skills-lock.json", ".skill-lock.json"];
19
19
  const DEFAULT_SKILL_SOURCE_REPO = "vercel-labs/agent-skills";
20
20
  const NPX_COMMAND = process.platform === "win32" ? "npx.cmd" : "npx";
21
21
  const SOURCE_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
@@ -36,26 +36,31 @@ async function runSkillsCommand(args, label) {
36
36
  function isValidSource(source) {
37
37
  return SOURCE_PATTERN.test(source);
38
38
  }
39
- function getCandidateSkillsLockPaths() {
40
- const cwdPath = node_path_1.default.resolve(process.cwd(), SKILLS_LOCK_FILENAME);
41
- const homePath = node_path_1.default.resolve(node_os_1.default.homedir(), SKILLS_LOCK_FILENAME);
39
+ function getCandidateSkillsLockPaths(filename) {
40
+ const cwdPath = node_path_1.default.resolve(process.cwd(), filename);
41
+ const homePath = node_path_1.default.resolve(node_os_1.default.homedir(), filename);
42
42
  const homeConfigPaths = [
43
- node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skills", SKILLS_LOCK_FILENAME),
44
- node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skillhub", SKILLS_LOCK_FILENAME),
45
- node_path_1.default.resolve(node_os_1.default.homedir(), ".skills", SKILLS_LOCK_FILENAME),
43
+ node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skills", filename),
44
+ node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skillhub", filename),
45
+ node_path_1.default.resolve(node_os_1.default.homedir(), ".skills", filename),
46
+ node_path_1.default.resolve(node_os_1.default.homedir(), ".agents", filename),
46
47
  ];
47
48
  const winAppData = process.env.APPDATA;
48
49
  const winLocalAppData = process.env.LOCALAPPDATA;
49
50
  const windowsConfigPaths = [
50
51
  ...(winAppData
51
- ? [node_path_1.default.resolve(winAppData, "skills", SKILLS_LOCK_FILENAME)]
52
+ ? [node_path_1.default.resolve(winAppData, "skills", filename)]
52
53
  : []),
53
54
  ...(winLocalAppData
54
- ? [node_path_1.default.resolve(winLocalAppData, "skills", SKILLS_LOCK_FILENAME)]
55
+ ? [node_path_1.default.resolve(winLocalAppData, "skills", filename)]
55
56
  : []),
56
57
  ];
57
58
  return [cwdPath, homePath, ...homeConfigPaths, ...windowsConfigPaths];
58
59
  }
60
+ function getAllCandidateSkillsLockPaths() {
61
+ const paths = SKILLS_LOCK_FILENAMES.flatMap((filename) => getCandidateSkillsLockPaths(filename));
62
+ return [...new Set(paths)];
63
+ }
59
64
  function parseSkillsLock(raw) {
60
65
  const parsed = JSON.parse(raw);
61
66
  const extractSkills = (items) => {
@@ -83,6 +88,21 @@ function parseSkillsLock(raw) {
83
88
  if (Array.isArray(parsed?.skills)) {
84
89
  return extractSkills(parsed.skills);
85
90
  }
91
+ if (parsed?.skills && typeof parsed.skills === "object") {
92
+ const objectSkills = parsed.skills;
93
+ return Object.entries(objectSkills).map(([name, metadata]) => {
94
+ if (typeof metadata === "object" && metadata !== null) {
95
+ const objectItem = metadata;
96
+ const source = typeof objectItem.source === "string"
97
+ ? objectItem.source
98
+ : typeof objectItem.repo === "string"
99
+ ? objectItem.repo
100
+ : DEFAULT_SKILL_SOURCE_REPO;
101
+ return { name, source };
102
+ }
103
+ return { name, source: DEFAULT_SKILL_SOURCE_REPO };
104
+ });
105
+ }
86
106
  if (Array.isArray(parsed)) {
87
107
  return extractSkills(parsed);
88
108
  }
@@ -128,6 +148,45 @@ async function tryReadSkillsLock(lockPath) {
128
148
  return null;
129
149
  }
130
150
  }
151
+ async function readFirstAvailableSkillsLock() {
152
+ const candidatePaths = getAllCandidateSkillsLockPaths();
153
+ for (const lockPath of candidatePaths) {
154
+ const parsed = await tryReadSkillsLock(lockPath);
155
+ if (parsed && parsed.length > 0) {
156
+ return {
157
+ skills: (0, syncCore_1.normalizeSkills)(parsed),
158
+ candidatePaths,
159
+ };
160
+ }
161
+ }
162
+ return {
163
+ skills: [],
164
+ candidatePaths,
165
+ };
166
+ }
167
+ function hydrateSourcesFromLock(listSkills, lockSkills) {
168
+ const lockByName = new Map();
169
+ for (const skill of lockSkills) {
170
+ const existing = lockByName.get(skill.name);
171
+ if (existing) {
172
+ existing.push(skill);
173
+ continue;
174
+ }
175
+ lockByName.set(skill.name, [skill]);
176
+ }
177
+ const hydrated = listSkills.map((skill) => {
178
+ const matches = lockByName.get(skill.name);
179
+ if (!matches || matches.length === 0) {
180
+ return skill;
181
+ }
182
+ const preferred = matches.find((item) => isValidSource(item.source)) ?? matches[0];
183
+ return {
184
+ name: skill.name,
185
+ source: preferred.source,
186
+ };
187
+ });
188
+ return (0, syncCore_1.normalizeSkills)(hydrated);
189
+ }
131
190
  async function getLocalSkills() {
132
191
  const listResult = await runSkillsCommand(["list", "-g"], "skills list -g");
133
192
  const listOutput = stringifyCommandResult(listResult);
@@ -136,32 +195,23 @@ async function getLocalSkills() {
136
195
  return [];
137
196
  }
138
197
  const fromList = parseSkillsListOutput(listOutput);
198
+ const fromLock = await readFirstAvailableSkillsLock();
139
199
  if (fromList.length > 0) {
140
- return fromList;
141
- }
142
- const lockResult = await runSkillsCommand(["generate-lock"], "skills generate-lock");
143
- const lockOutput = stringifyCommandResult(lockResult);
144
- const candidatePaths = getCandidateSkillsLockPaths();
145
- for (const lockPath of candidatePaths) {
146
- const parsed = await tryReadSkillsLock(lockPath);
147
- if (parsed) {
148
- return (0, syncCore_1.normalizeSkills)(parsed);
200
+ if (fromLock.skills.length > 0) {
201
+ return hydrateSourcesFromLock(fromList, fromLock.skills);
149
202
  }
203
+ return fromList;
150
204
  }
151
- if (lockOutput.includes("No installed skills found") ||
152
- listOutput.includes("No project skills found")) {
153
- return [];
205
+ if (fromLock.skills.length > 0) {
206
+ return fromLock.skills;
154
207
  }
155
208
  throw new Error([
156
209
  "Unable to construct local skills list.",
157
210
  "- skills list -g output:",
158
211
  listOutput,
159
212
  "",
160
- `- Searched ${SKILLS_LOCK_FILENAME} paths:`,
161
- ...candidatePaths.map((p) => ` - ${p}`),
162
- "",
163
- "- npx skills generate-lock output:",
164
- lockOutput,
213
+ "- Searched lock paths:",
214
+ ...fromLock.candidatePaths.map((p) => ` - ${p}`),
165
215
  ].join("\n"));
166
216
  }
167
217
  async function installSkills(skills, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yonpark/skillhub-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "SkillHub CLI - sync skills with GitHub Gist",
5
5
  "bin": {
6
6
  "skillhub": "bin/skillhub.js"