@yonpark/skillhub-cli 0.1.3 → 0.3.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.
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTimestamp = parseTimestamp;
4
+ exports.normalizeSkills = normalizeSkills;
5
+ exports.uniqueSortedSkills = uniqueSortedSkills;
6
+ exports.areSameSkills = areSameSkills;
7
+ exports.buildMergePlan = buildMergePlan;
8
+ exports.buildAutoPlan = buildAutoPlan;
9
+ exports.buildPullPlan = buildPullPlan;
10
+ exports.buildPushPlan = buildPushPlan;
11
+ const DEFAULT_SKILL_SOURCE_REPO = "vercel-labs/agent-skills";
12
+ const BANNED_SKILL_NAME_SUBSTRINGS = [
13
+ "No global skills found",
14
+ "Try listing project skills without -g",
15
+ "No project skills found",
16
+ "Try listing global skills",
17
+ ];
18
+ function parseTimestamp(value) {
19
+ if (!value) {
20
+ return null;
21
+ }
22
+ const parsed = Date.parse(value);
23
+ return Number.isFinite(parsed) ? parsed : null;
24
+ }
25
+ function normalizeSkills(skills) {
26
+ const normalized = skills
27
+ .map((skill) => {
28
+ if (typeof skill === "string") {
29
+ return { name: skill, source: DEFAULT_SKILL_SOURCE_REPO };
30
+ }
31
+ return {
32
+ name: String(skill?.name ?? ""),
33
+ source: String(skill?.source ?? DEFAULT_SKILL_SOURCE_REPO),
34
+ };
35
+ })
36
+ .filter((skill) => skill.name.length > 0 &&
37
+ !BANNED_SKILL_NAME_SUBSTRINGS.some((bad) => skill.name.includes(bad)));
38
+ return uniqueSortedSkills(normalized);
39
+ }
40
+ function uniqueSortedSkills(skills) {
41
+ const seen = new Set();
42
+ const result = [];
43
+ for (const skill of skills) {
44
+ const key = `${skill.source}:${skill.name}`;
45
+ if (!seen.has(key)) {
46
+ seen.add(key);
47
+ result.push(skill);
48
+ }
49
+ }
50
+ return result.sort((a, b) => {
51
+ if (a.source !== b.source) {
52
+ return a.source.localeCompare(b.source);
53
+ }
54
+ return a.name.localeCompare(b.name);
55
+ });
56
+ }
57
+ function areSameSkills(left, right) {
58
+ const leftSorted = uniqueSortedSkills(left);
59
+ const rightSorted = uniqueSortedSkills(right);
60
+ if (leftSorted.length !== rightSorted.length) {
61
+ return false;
62
+ }
63
+ return leftSorted.every((skill, index) => skill.name === rightSorted[index].name &&
64
+ skill.source === rightSorted[index].source);
65
+ }
66
+ function buildMergePlan(params) {
67
+ const localSkills = normalizeSkills(params.localPayload.skills);
68
+ const remoteSkills = normalizeSkills(params.remotePayload.skills);
69
+ const unionSkills = uniqueSortedSkills([...localSkills, ...remoteSkills]);
70
+ const installCandidates = unionSkills.filter((skill) => !localSkills.some((local) => local.name === skill.name && local.source === skill.source));
71
+ const uploadPayload = areSameSkills(remoteSkills, unionSkills)
72
+ ? null
73
+ : {
74
+ skills: unionSkills,
75
+ updatedAt: params.nowIso,
76
+ };
77
+ return {
78
+ mode: "merge",
79
+ localSkills,
80
+ remoteSkills,
81
+ installCandidates,
82
+ uploadPayload,
83
+ };
84
+ }
85
+ function buildAutoPlan(params) {
86
+ const localSkills = normalizeSkills(params.localPayload.skills);
87
+ const remoteSkills = normalizeSkills(params.remotePayload.skills);
88
+ const lastSyncTime = parseTimestamp(params.lastSyncAt) ?? 0;
89
+ const remoteTime = parseTimestamp(params.remotePayload.updatedAt);
90
+ const isRemoteNewer = remoteTime !== null && remoteTime > lastSyncTime;
91
+ if (isRemoteNewer) {
92
+ const installCandidates = remoteSkills.filter((skill) => !localSkills.some((local) => local.name === skill.name && local.source === skill.source));
93
+ return {
94
+ mode: "auto",
95
+ localSkills,
96
+ remoteSkills,
97
+ installCandidates,
98
+ uploadPayload: null,
99
+ isRemoteNewer,
100
+ };
101
+ }
102
+ const uploadPayload = areSameSkills(localSkills, remoteSkills)
103
+ ? null
104
+ : {
105
+ skills: localSkills,
106
+ updatedAt: params.nowIso,
107
+ };
108
+ return {
109
+ mode: "auto",
110
+ localSkills,
111
+ remoteSkills,
112
+ installCandidates: [],
113
+ uploadPayload,
114
+ isRemoteNewer,
115
+ };
116
+ }
117
+ function buildPullPlan(params) {
118
+ const localSkills = normalizeSkills(params.localPayload.skills);
119
+ const remoteSkills = normalizeSkills(params.remotePayload.skills);
120
+ const installCandidates = remoteSkills.filter((remote) => !localSkills.some((local) => local.name === remote.name && local.source === remote.source));
121
+ const removeCandidates = localSkills.filter((local) => !remoteSkills.some((remote) => remote.name === local.name && remote.source === local.source));
122
+ return {
123
+ mode: "pull",
124
+ localSkills,
125
+ remoteSkills,
126
+ installCandidates,
127
+ removeCandidates,
128
+ };
129
+ }
130
+ function buildPushPlan(params) {
131
+ const localSkills = normalizeSkills(params.localPayload.skills);
132
+ const remoteSkills = normalizeSkills(params.remotePayload.skills);
133
+ const uploadPayload = areSameSkills(localSkills, remoteSkills)
134
+ ? null
135
+ : {
136
+ skills: localSkills,
137
+ updatedAt: params.nowIso,
138
+ };
139
+ return {
140
+ mode: "push",
141
+ localSkills,
142
+ remoteSkills,
143
+ uploadPayload,
144
+ };
145
+ }
package/dist/index.js CHANGED
@@ -2,20 +2,117 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const commander_1 = require("commander");
4
4
  const login_1 = require("./commands/login");
5
+ const logout_1 = require("./commands/logout");
6
+ const status_1 = require("./commands/status");
5
7
  const sync_1 = require("./commands/sync");
8
+ function getPackageVersion() {
9
+ try {
10
+ const pkg = require("../package.json");
11
+ return pkg.version || "0.0.0";
12
+ }
13
+ catch {
14
+ return "0.0.0";
15
+ }
16
+ }
17
+ function errorMessage(error) {
18
+ return error instanceof Error ? error.message : String(error);
19
+ }
20
+ function withJsonErrorHandling(action) {
21
+ return async (options) => {
22
+ try {
23
+ await action(options);
24
+ }
25
+ catch (error) {
26
+ if (options.json) {
27
+ console.log(JSON.stringify({
28
+ ok: false,
29
+ error: errorMessage(error),
30
+ }, null, 2));
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ throw error;
35
+ }
36
+ };
37
+ }
6
38
  const program = new commander_1.Command();
7
- program.name("skillhub").description("SkillHub CLI").version("0.1.0");
8
- program
39
+ program.name("skillhub").description("SkillHub CLI").version(getPackageVersion());
40
+ const authCommand = program.command("auth").description("Authentication commands");
41
+ authCommand
9
42
  .command("login")
10
- .description("Login: register your GitHub PAT with gist access")
43
+ .description("Register your GitHub PAT with gist access")
11
44
  .action(async () => {
12
45
  await (0, login_1.runLogin)();
13
46
  });
14
- program
47
+ authCommand
48
+ .command("status")
49
+ .description("Show local auth/sync status")
50
+ .option("--json", "print output as JSON", false)
51
+ .action(withJsonErrorHandling(async (options) => {
52
+ await (0, status_1.runStatus)({ json: options.json });
53
+ }));
54
+ authCommand
55
+ .command("logout")
56
+ .description("Clear stored session data (token, gist id, last sync)")
57
+ .option("--yes", "skip confirmation prompt", false)
58
+ .option("--json", "print output as JSON", false)
59
+ .action(withJsonErrorHandling(async (options) => {
60
+ await (0, logout_1.runLogout)({ yes: options.yes, json: options.json });
61
+ }));
62
+ const syncCommand = program
15
63
  .command("sync")
16
- .description("Sync: reconcile local skills with remote Gist backup")
17
- .option("-s, --strategy <strategy>", "merge strategy (union|latest)", "union")
18
- .action(async (options) => {
19
- await (0, sync_1.runSync)(options.strategy);
64
+ .description("Sync local skills and remote Gist backup");
65
+ syncCommand
66
+ .command("pull")
67
+ .description("Mirror remote skills into local skills (remote -> local)")
68
+ .option("--dry-run", "show planned changes without applying them", false)
69
+ .option("--yes", "skip deletion confirmation prompt", false)
70
+ .option("--json", "print output as JSON", false)
71
+ .action(withJsonErrorHandling(async (options) => {
72
+ await (0, sync_1.runSyncPull)({
73
+ dryRun: options.dryRun,
74
+ yes: options.yes,
75
+ json: options.json,
76
+ });
77
+ }));
78
+ syncCommand
79
+ .command("push")
80
+ .description("Mirror local skills into remote backup (local -> remote)")
81
+ .option("--dry-run", "show planned changes without applying them", false)
82
+ .option("--json", "print output as JSON", false)
83
+ .action(withJsonErrorHandling(async (options) => {
84
+ await (0, sync_1.runSyncPush)({
85
+ dryRun: options.dryRun,
86
+ json: options.json,
87
+ });
88
+ }));
89
+ syncCommand
90
+ .command("merge")
91
+ .description("Merge local and remote skills (union behavior)")
92
+ .option("--dry-run", "show planned changes without applying them", false)
93
+ .option("--json", "print output as JSON", false)
94
+ .action(withJsonErrorHandling(async (options) => {
95
+ await (0, sync_1.runSyncMerge)({
96
+ dryRun: options.dryRun,
97
+ json: options.json,
98
+ });
99
+ }));
100
+ syncCommand
101
+ .command("auto")
102
+ .description("Sync using remote.updatedAt and lastSyncAt comparison")
103
+ .option("--dry-run", "show planned changes without applying them", false)
104
+ .option("--json", "print output as JSON", false)
105
+ .action(withJsonErrorHandling(async (options) => {
106
+ await (0, sync_1.runSyncAuto)({
107
+ dryRun: options.dryRun,
108
+ json: options.json,
109
+ });
110
+ }));
111
+ syncCommand.action(() => {
112
+ syncCommand.outputHelp();
113
+ throw new Error("Missing sync mode. Use one of: pull, push, merge, auto.");
114
+ });
115
+ program.parseAsync(process.argv).catch((error) => {
116
+ console.error(`Error: ${errorMessage(error)}`);
117
+ process.exitCode = 1;
20
118
  });
21
- program.parseAsync(process.argv);
@@ -25,4 +25,30 @@ exports.configStore = {
25
25
  const config = await getConfig();
26
26
  config.set("gistId", gistId);
27
27
  },
28
+ async getLastSyncAt() {
29
+ const config = await getConfig();
30
+ return config.get("lastSyncAt");
31
+ },
32
+ async setLastSyncAt(lastSyncAt) {
33
+ const config = await getConfig();
34
+ config.set("lastSyncAt", lastSyncAt);
35
+ },
36
+ async clearToken() {
37
+ const config = await getConfig();
38
+ config.delete("githubToken");
39
+ },
40
+ async clearGistId() {
41
+ const config = await getConfig();
42
+ config.delete("gistId");
43
+ },
44
+ async clearLastSyncAt() {
45
+ const config = await getConfig();
46
+ config.delete("lastSyncAt");
47
+ },
48
+ async clearSession() {
49
+ const config = await getConfig();
50
+ config.delete("githubToken");
51
+ config.delete("gistId");
52
+ config.delete("lastSyncAt");
53
+ },
28
54
  };
@@ -2,33 +2,44 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createOctokit = createOctokit;
4
4
  exports.verifyToken = verifyToken;
5
+ exports.checkGistAccess = checkGistAccess;
5
6
  exports.findSkillhubGist = findSkillhubGist;
6
7
  exports.getSkillhubPayload = getSkillhubPayload;
7
8
  exports.createSkillhubGist = createSkillhubGist;
8
9
  exports.updateSkillhubGist = updateSkillhubGist;
9
10
  const rest_1 = require("@octokit/rest");
11
+ const retry_1 = require("../utils/retry");
10
12
  const SKILLHUB_FILENAME = "skillhub.json";
13
+ const DEFAULT_SKILL_SOURCE_REPO = "vercel-labs/agent-skills";
14
+ const GITHUB_TIMEOUT_MS = 10000;
15
+ function withGitHubRetry(label, fn) {
16
+ return (0, retry_1.retryAsync)(fn, { label, shouldRetry: retry_1.isTransientError });
17
+ }
11
18
  function createOctokit(token) {
12
- return new rest_1.Octokit({ auth: token });
19
+ return new rest_1.Octokit({
20
+ auth: token,
21
+ request: {
22
+ timeout: GITHUB_TIMEOUT_MS,
23
+ },
24
+ });
13
25
  }
14
26
  async function verifyToken(token) {
15
27
  const octokit = createOctokit(token);
16
- await octokit.users.getAuthenticated();
17
- // Also verify that the token can actually talk to the Gist API
18
- // (this catches fine-grained tokens that don't have gist access).
19
- await octokit.rest.gists.list({ per_page: 1 });
28
+ await withGitHubRetry("users.getAuthenticated", () => octokit.users.getAuthenticated());
29
+ await withGitHubRetry("gists.list", () => octokit.rest.gists.list({ per_page: 1 }));
30
+ }
31
+ async function checkGistAccess(octokit) {
32
+ await withGitHubRetry("gists.list", () => octokit.rest.gists.list({ per_page: 1 }));
20
33
  }
21
34
  async function findSkillhubGist(octokit) {
22
- const gists = await octokit.paginate(octokit.rest.gists.list, {
23
- per_page: 100,
24
- });
35
+ const gists = await withGitHubRetry("gists.paginate", () => octokit.paginate(octokit.rest.gists.list, { per_page: 100 }));
25
36
  return gists.find((gist) => {
26
37
  const files = Object.values(gist.files ?? {});
27
38
  return files.some((file) => file?.filename === SKILLHUB_FILENAME);
28
39
  });
29
40
  }
30
41
  async function getSkillhubPayload(octokit, gistId) {
31
- const gist = await octokit.gists.get({ gist_id: gistId });
42
+ const gist = await withGitHubRetry("gists.get", () => octokit.gists.get({ gist_id: gistId }));
32
43
  const file = Object.values(gist.data.files ?? {}).find((item) => item?.filename === SKILLHUB_FILENAME);
33
44
  if (!file?.content) {
34
45
  return null;
@@ -38,10 +49,9 @@ async function getSkillhubPayload(octokit, gistId) {
38
49
  if (!Array.isArray(parsed.skills)) {
39
50
  return null;
40
51
  }
41
- // 하위 호환성: string[] 형식을 SkillInfo[] 형식으로 변환
42
52
  const normalizedSkills = parsed.skills.map((skill) => {
43
53
  if (typeof skill === "string") {
44
- return { name: skill, source: "vercel-labs/agent-skills" };
54
+ return { name: skill, source: DEFAULT_SKILL_SOURCE_REPO };
45
55
  }
46
56
  return skill;
47
57
  });
@@ -55,7 +65,7 @@ async function getSkillhubPayload(octokit, gistId) {
55
65
  }
56
66
  }
57
67
  async function createSkillhubGist(octokit, payload) {
58
- const response = await octokit.gists.create({
68
+ const response = await withGitHubRetry("gists.create", () => octokit.gists.create({
59
69
  description: "SkillHub sync",
60
70
  public: false,
61
71
  files: {
@@ -63,16 +73,16 @@ async function createSkillhubGist(octokit, payload) {
63
73
  content: JSON.stringify(payload, null, 2),
64
74
  },
65
75
  },
66
- });
76
+ }));
67
77
  return response.data;
68
78
  }
69
79
  async function updateSkillhubGist(octokit, gistId, payload) {
70
- await octokit.gists.update({
80
+ await withGitHubRetry("gists.update", () => octokit.gists.update({
71
81
  gist_id: gistId,
72
82
  files: {
73
83
  [SKILLHUB_FILENAME]: {
74
84
  content: JSON.stringify(payload, null, 2),
75
85
  },
76
86
  },
77
- });
87
+ }));
78
88
  }
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isValidSource = isValidSource;
7
+ exports.getLocalSkills = getLocalSkills;
8
+ exports.installSkills = installSkills;
9
+ exports.removeSkills = removeSkills;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const promises_1 = __importDefault(require("node:fs/promises"));
12
+ const node_os_1 = __importDefault(require("node:os"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const node_util_1 = require("node:util");
15
+ const syncCore_1 = require("../core/syncCore");
16
+ const retry_1 = require("../utils/retry");
17
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
18
+ const SKILLS_LOCK_FILENAME = "skills-lock.json";
19
+ const DEFAULT_SKILL_SOURCE_REPO = "vercel-labs/agent-skills";
20
+ const NPX_COMMAND = process.platform === "win32" ? "npx.cmd" : "npx";
21
+ const SOURCE_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
22
+ const COMMAND_TIMEOUT_MS = 120000;
23
+ const COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
24
+ function stringifyCommandResult(result) {
25
+ return `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
26
+ }
27
+ async function runSkillsCommand(args, label) {
28
+ return (0, retry_1.retryAsync)(async () => {
29
+ return execFileAsync(NPX_COMMAND, ["skills", ...args], {
30
+ timeout: COMMAND_TIMEOUT_MS,
31
+ maxBuffer: COMMAND_MAX_BUFFER,
32
+ shell: process.platform === "win32",
33
+ });
34
+ }, { label, shouldRetry: retry_1.isTransientError });
35
+ }
36
+ function isValidSource(source) {
37
+ return SOURCE_PATTERN.test(source);
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);
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),
46
+ ];
47
+ const winAppData = process.env.APPDATA;
48
+ const winLocalAppData = process.env.LOCALAPPDATA;
49
+ const windowsConfigPaths = [
50
+ ...(winAppData
51
+ ? [node_path_1.default.resolve(winAppData, "skills", SKILLS_LOCK_FILENAME)]
52
+ : []),
53
+ ...(winLocalAppData
54
+ ? [node_path_1.default.resolve(winLocalAppData, "skills", SKILLS_LOCK_FILENAME)]
55
+ : []),
56
+ ];
57
+ return [cwdPath, homePath, ...homeConfigPaths, ...windowsConfigPaths];
58
+ }
59
+ function parseSkillsLock(raw) {
60
+ const parsed = JSON.parse(raw);
61
+ const extractSkills = (items) => {
62
+ return items.map((item) => {
63
+ if (typeof item === "string") {
64
+ return { name: item, source: DEFAULT_SKILL_SOURCE_REPO };
65
+ }
66
+ if (typeof item === "object" && item !== null) {
67
+ const objectItem = item;
68
+ const name = typeof objectItem.name === "string"
69
+ ? objectItem.name
70
+ : typeof objectItem.skill === "string"
71
+ ? objectItem.skill
72
+ : String(item);
73
+ const source = typeof objectItem.source === "string"
74
+ ? objectItem.source
75
+ : typeof objectItem.repo === "string"
76
+ ? objectItem.repo
77
+ : DEFAULT_SKILL_SOURCE_REPO;
78
+ return { name, source };
79
+ }
80
+ return { name: String(item), source: DEFAULT_SKILL_SOURCE_REPO };
81
+ });
82
+ };
83
+ if (Array.isArray(parsed?.skills)) {
84
+ return extractSkills(parsed.skills);
85
+ }
86
+ if (Array.isArray(parsed)) {
87
+ return extractSkills(parsed);
88
+ }
89
+ if (Array.isArray(parsed?.installedSkills)) {
90
+ return extractSkills(parsed.installedSkills);
91
+ }
92
+ return [];
93
+ }
94
+ function parseSkillsListOutput(output) {
95
+ const cleaned = output.replace(/\x1b\[[0-9;]*m/g, "");
96
+ const lines = cleaned.split(/\r?\n/).map((line) => line.trim());
97
+ const skills = [];
98
+ for (const line of lines) {
99
+ if (!line)
100
+ continue;
101
+ if (line === "Global Skills")
102
+ continue;
103
+ if (line.startsWith("Agents:"))
104
+ continue;
105
+ if (line.startsWith("No project skills found"))
106
+ continue;
107
+ if (line.startsWith("Try listing global skills"))
108
+ continue;
109
+ if (line.startsWith("No global skills found"))
110
+ continue;
111
+ if (line.startsWith("Try listing project skills without -g"))
112
+ continue;
113
+ const [name] = line.split(/\s+/);
114
+ if (!name)
115
+ continue;
116
+ if (name.includes("\\") || name.includes("/") || name.includes("~"))
117
+ continue;
118
+ skills.push({ name, source: DEFAULT_SKILL_SOURCE_REPO });
119
+ }
120
+ return (0, syncCore_1.normalizeSkills)(skills);
121
+ }
122
+ async function tryReadSkillsLock(lockPath) {
123
+ try {
124
+ const raw = await promises_1.default.readFile(lockPath, "utf-8");
125
+ return parseSkillsLock(raw);
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
131
+ async function getLocalSkills() {
132
+ const listResult = await runSkillsCommand(["list", "-g"], "skills list -g");
133
+ const listOutput = stringifyCommandResult(listResult);
134
+ if (listOutput.includes("No global skills found") ||
135
+ listOutput.includes("Try listing project skills without -g")) {
136
+ return [];
137
+ }
138
+ const fromList = parseSkillsListOutput(listOutput);
139
+ 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);
149
+ }
150
+ }
151
+ if (lockOutput.includes("No installed skills found") ||
152
+ listOutput.includes("No project skills found")) {
153
+ return [];
154
+ }
155
+ throw new Error([
156
+ "Unable to construct local skills list.",
157
+ "- skills list -g output:",
158
+ listOutput,
159
+ "",
160
+ `- Searched ${SKILLS_LOCK_FILENAME} paths:`,
161
+ ...candidatePaths.map((p) => ` - ${p}`),
162
+ "",
163
+ "- npx skills generate-lock output:",
164
+ lockOutput,
165
+ ].join("\n"));
166
+ }
167
+ async function installSkills(skills, options = {}) {
168
+ const succeeded = [];
169
+ const failed = [];
170
+ for (const skill of skills) {
171
+ if (!isValidSource(skill.source)) {
172
+ const reason = `Invalid source "${skill.source}". Expected owner/repo format.`;
173
+ failed.push({ skill, reason });
174
+ if (options.verbose) {
175
+ console.warn(`Skill install failed: ${skill.name} (from ${skill.source})`);
176
+ console.warn(` - ${reason}`);
177
+ }
178
+ continue;
179
+ }
180
+ try {
181
+ const result = await runSkillsCommand(["add", skill.source, "--skill", skill.name, "--global", "--yes"], `skills add ${skill.source} --skill ${skill.name}`);
182
+ const output = stringifyCommandResult(result);
183
+ if (options.verbose && output) {
184
+ console.log(output);
185
+ }
186
+ succeeded.push(skill);
187
+ }
188
+ catch (error) {
189
+ const reason = error instanceof Error ? error.message : String(error);
190
+ failed.push({ skill, reason });
191
+ if (options.verbose) {
192
+ console.warn(`Skill install failed: ${skill.name} (from ${skill.source})`);
193
+ console.warn(` - ${reason}`);
194
+ }
195
+ }
196
+ }
197
+ return { succeeded, failed };
198
+ }
199
+ async function removeSkills(skills, options = {}) {
200
+ const succeeded = [];
201
+ const failed = [];
202
+ const seen = new Set();
203
+ for (const skill of skills) {
204
+ const key = `${skill.source}:${skill.name}`;
205
+ if (seen.has(key)) {
206
+ continue;
207
+ }
208
+ seen.add(key);
209
+ try {
210
+ const result = await runSkillsCommand(["remove", "--skill", skill.name, "--global", "--yes"], `skills remove --skill ${skill.name}`);
211
+ const output = stringifyCommandResult(result);
212
+ if (options.verbose && output) {
213
+ console.log(output);
214
+ }
215
+ succeeded.push(skill);
216
+ }
217
+ catch (error) {
218
+ const reason = error instanceof Error ? error.message : String(error);
219
+ failed.push({ skill, reason });
220
+ if (options.verbose) {
221
+ console.warn(`Skill remove failed: ${skill.name} (from ${skill.source})`);
222
+ console.warn(` - ${reason}`);
223
+ }
224
+ }
225
+ }
226
+ return { succeeded, failed };
227
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.emitOutput = emitOutput;
4
+ function emitOutput(payload, asJson, formatText) {
5
+ if (asJson) {
6
+ console.log(JSON.stringify(payload, null, 2));
7
+ return;
8
+ }
9
+ console.log(formatText(payload));
10
+ }