@hasna/skills 0.1.3 → 0.1.5

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/bin/index.js CHANGED
@@ -1868,12 +1868,17 @@ var require_commander = __commonJS((exports) => {
1868
1868
  exports.InvalidOptionArgumentError = InvalidArgumentError;
1869
1869
  });
1870
1870
 
1871
+ // src/lib/utils.ts
1872
+ function normalizeSkillName(name) {
1873
+ return name.startsWith("skill-") ? name : `skill-${name}`;
1874
+ }
1875
+
1871
1876
  // package.json
1872
1877
  var package_default;
1873
1878
  var init_package = __esm(() => {
1874
1879
  package_default = {
1875
1880
  name: "@hasna/skills",
1876
- version: "0.1.3",
1881
+ version: "0.1.5",
1877
1882
  description: "Skills library for AI coding agents",
1878
1883
  type: "module",
1879
1884
  bin: {
@@ -2222,6 +2227,21 @@ var init_registry = __esm(() => {
2222
2227
  category: "Development Tools",
2223
2228
  tags: ["scaffold", "project", "boilerplate", "template"]
2224
2229
  },
2230
+ {
2231
+ name: "scancommitpr",
2232
+ displayName: "Scan Commit PR",
2233
+ description: "Scan repo changes, group into logical commits, push, and optionally create a PR",
2234
+ category: "Development Tools",
2235
+ tags: ["git", "commit", "push", "pull-request", "automation"],
2236
+ dependencies: ["scancommitpush"]
2237
+ },
2238
+ {
2239
+ name: "scancommitpush",
2240
+ displayName: "Scan Commit Push",
2241
+ description: "Scan repo changes, group into logical commits with conventional messages, and push to GitHub",
2242
+ category: "Development Tools",
2243
+ tags: ["git", "commit", "push", "automation"]
2244
+ },
2225
2245
  {
2226
2246
  name: "security-audit",
2227
2247
  displayName: "Security Audit",
@@ -5062,11 +5082,6 @@ var require_cli_spinners = __commonJS((exports, module) => {
5062
5082
  module.exports = spinners;
5063
5083
  });
5064
5084
 
5065
- // src/lib/utils.ts
5066
- function normalizeSkillName(name) {
5067
- return name.startsWith("skill-") ? name : `skill-${name}`;
5068
- }
5069
-
5070
5085
  // src/lib/installer.ts
5071
5086
  import { existsSync, cpSync, mkdirSync, writeFileSync, rmSync, readdirSync, statSync, readFileSync } from "fs";
5072
5087
  import { join, dirname } from "path";
@@ -5123,6 +5138,16 @@ function installSkill(name, options = {}) {
5123
5138
  }
5124
5139
  });
5125
5140
  updateSkillsIndex(destDir);
5141
+ const meta = getSkill(name);
5142
+ if (meta?.dependencies && meta.dependencies.length > 0) {
5143
+ const installed = getInstalledSkills(targetDir);
5144
+ const installedSet = new Set(installed);
5145
+ for (const dep of meta.dependencies) {
5146
+ if (!installedSet.has(dep)) {
5147
+ console.warn(`Warning: skill-${meta.name} depends on skill-${dep} which is not installed`);
5148
+ }
5149
+ }
5150
+ }
5126
5151
  return {
5127
5152
  skill: name,
5128
5153
  success: true,
@@ -5235,6 +5260,7 @@ function removeSkillForAgent(name, options) {
5235
5260
  }
5236
5261
  var __dirname2, SKILLS_DIR, AGENT_TARGETS;
5237
5262
  var init_installer = __esm(() => {
5263
+ init_registry();
5238
5264
  __dirname2 = dirname(fileURLToPath(import.meta.url));
5239
5265
  SKILLS_DIR = findSkillsDir();
5240
5266
  AGENT_TARGETS = ["claude", "codex", "gemini"];
@@ -5369,57 +5395,6 @@ async function runSkill(name, args, options = {}) {
5369
5395
  const exitCode = await proc.exited;
5370
5396
  return { exitCode };
5371
5397
  }
5372
- function generateEnvExample(targetDir = process.cwd()) {
5373
- const skillsDir = join2(targetDir, ".skills");
5374
- if (!existsSync2(skillsDir))
5375
- return "";
5376
- const dirs = readdirSync2(skillsDir).filter((f) => f.startsWith("skill-") && existsSync2(join2(skillsDir, f, "package.json")));
5377
- const envMap = new Map;
5378
- for (const dir of dirs) {
5379
- const skillName = dir.replace("skill-", "");
5380
- const skillPath = join2(skillsDir, dir);
5381
- const texts = [];
5382
- for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example"]) {
5383
- const content = readIfExists(join2(skillPath, file));
5384
- if (content)
5385
- texts.push(content);
5386
- }
5387
- const allText = texts.join(`
5388
- `);
5389
- const foundVars = extractEnvVars(allText);
5390
- for (const envVar of foundVars) {
5391
- if (!envMap.has(envVar)) {
5392
- envMap.set(envVar, []);
5393
- }
5394
- if (!envMap.get(envVar).includes(skillName)) {
5395
- envMap.get(envVar).push(skillName);
5396
- }
5397
- }
5398
- }
5399
- if (envMap.size === 0)
5400
- return "";
5401
- const lines = [
5402
- "# Environment variables for installed skills",
5403
- "# Auto-generated by: skills init",
5404
- ""
5405
- ];
5406
- const sorted = Array.from(envMap.entries()).sort(([a], [b]) => a.localeCompare(b));
5407
- let lastPrefix = "";
5408
- for (const [envVar, skills] of sorted) {
5409
- const prefix = envVar.split("_")[0];
5410
- if (prefix !== lastPrefix) {
5411
- if (lastPrefix)
5412
- lines.push("");
5413
- lines.push(`# ${prefix}`);
5414
- lastPrefix = prefix;
5415
- }
5416
- lines.push(`# Used by: ${skills.join(", ")}`);
5417
- lines.push(`${envVar}=`);
5418
- }
5419
- return lines.join(`
5420
- `) + `
5421
- `;
5422
- }
5423
5398
  function generateSkillMd(name) {
5424
5399
  const meta = getSkill(name);
5425
5400
  if (!meta)
@@ -32808,7 +32783,7 @@ var require_formats = __commonJS((exports) => {
32808
32783
  }
32809
32784
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
32810
32785
  function getTime(strictTimeZone) {
32811
- return function time3(str) {
32786
+ return function time(str) {
32812
32787
  const matches = TIME.exec(str);
32813
32788
  if (!matches)
32814
32789
  return false;
@@ -33072,6 +33047,62 @@ class ExperimentalServerTasks {
33072
33047
  requestStream(request, resultSchema, options) {
33073
33048
  return this._server.requestStream(request, resultSchema, options);
33074
33049
  }
33050
+ createMessageStream(params, options) {
33051
+ const clientCapabilities = this._server.getClientCapabilities();
33052
+ if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
33053
+ throw new Error("Client does not support sampling tools capability.");
33054
+ }
33055
+ if (params.messages.length > 0) {
33056
+ const lastMessage = params.messages[params.messages.length - 1];
33057
+ const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
33058
+ const hasToolResults = lastContent.some((c) => c.type === "tool_result");
33059
+ const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
33060
+ const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
33061
+ const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
33062
+ if (hasToolResults) {
33063
+ if (lastContent.some((c) => c.type !== "tool_result")) {
33064
+ throw new Error("The last message must contain only tool_result content if any is present");
33065
+ }
33066
+ if (!hasPreviousToolUse) {
33067
+ throw new Error("tool_result blocks are not matching any tool_use from the previous message");
33068
+ }
33069
+ }
33070
+ if (hasPreviousToolUse) {
33071
+ const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
33072
+ const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
33073
+ if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) {
33074
+ throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match");
33075
+ }
33076
+ }
33077
+ }
33078
+ return this.requestStream({
33079
+ method: "sampling/createMessage",
33080
+ params
33081
+ }, CreateMessageResultSchema, options);
33082
+ }
33083
+ elicitInputStream(params, options) {
33084
+ const clientCapabilities = this._server.getClientCapabilities();
33085
+ const mode = params.mode ?? "form";
33086
+ switch (mode) {
33087
+ case "url": {
33088
+ if (!clientCapabilities?.elicitation?.url) {
33089
+ throw new Error("Client does not support url elicitation.");
33090
+ }
33091
+ break;
33092
+ }
33093
+ case "form": {
33094
+ if (!clientCapabilities?.elicitation?.form) {
33095
+ throw new Error("Client does not support form elicitation.");
33096
+ }
33097
+ break;
33098
+ }
33099
+ }
33100
+ const normalizedParams = mode === "form" && params.mode === undefined ? { ...params, mode: "form" } : params;
33101
+ return this.requestStream({
33102
+ method: "elicitation/create",
33103
+ params: normalizedParams
33104
+ }, ElicitResultSchema, options);
33105
+ }
33075
33106
  async getTask(taskId, options) {
33076
33107
  return this._server.getTask({ taskId }, options);
33077
33108
  }
@@ -33085,6 +33116,9 @@ class ExperimentalServerTasks {
33085
33116
  return this._server.cancelTask({ taskId }, options);
33086
33117
  }
33087
33118
  }
33119
+ var init_server = __esm(() => {
33120
+ init_types2();
33121
+ });
33088
33122
 
33089
33123
  // node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
33090
33124
  function assertToolsCallTaskCapability(requests, method, entityName) {
@@ -33123,11 +33157,12 @@ function assertClientRequestTaskCapability(requests, method, entityName) {
33123
33157
 
33124
33158
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js
33125
33159
  var Server;
33126
- var init_server = __esm(() => {
33160
+ var init_server2 = __esm(() => {
33127
33161
  init_protocol();
33128
33162
  init_types2();
33129
33163
  init_ajv_provider();
33130
33164
  init_zod_compat();
33165
+ init_server();
33131
33166
  Server = class Server extends Protocol {
33132
33167
  constructor(_serverInfo, options) {
33133
33168
  super(options);
@@ -34500,7 +34535,7 @@ function createCompletionResult(suggestions) {
34500
34535
  }
34501
34536
  var EMPTY_OBJECT_JSON_SCHEMA, EMPTY_COMPLETION_RESULT;
34502
34537
  var init_mcp = __esm(() => {
34503
- init_server();
34538
+ init_server2();
34504
34539
  init_zod_compat();
34505
34540
  init_zod_json_schema_compat();
34506
34541
  init_types2();
@@ -34829,11 +34864,25 @@ var init_mcp2 = __esm(() => {
34829
34864
  // src/server/serve.ts
34830
34865
  var exports_serve = {};
34831
34866
  __export(exports_serve, {
34832
- startServer: () => startServer
34867
+ startServer: () => startServer,
34868
+ createFetchHandler: () => createFetchHandler
34833
34869
  });
34834
- import { existsSync as existsSync3 } from "fs";
34870
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
34835
34871
  import { join as join3, dirname as dirname2, extname } from "path";
34836
34872
  import { fileURLToPath as fileURLToPath2 } from "url";
34873
+ function getPackageJson() {
34874
+ try {
34875
+ const scriptDir = dirname2(fileURLToPath2(import.meta.url));
34876
+ for (const rel of ["../..", ".."]) {
34877
+ const pkgPath = join3(scriptDir, rel, "package.json");
34878
+ if (existsSync3(pkgPath)) {
34879
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
34880
+ return { version: pkg.version || "unknown", name: pkg.name || "skills" };
34881
+ }
34882
+ }
34883
+ } catch {}
34884
+ return { version: "unknown", name: "skills" };
34885
+ }
34837
34886
  function resolveDashboardDir() {
34838
34887
  const candidates = [];
34839
34888
  try {
@@ -34869,6 +34918,7 @@ function getAllSkillsWithStatus() {
34869
34918
  const installed = new Set(getInstalledSkills());
34870
34919
  return SKILLS.map((meta3) => {
34871
34920
  const reqs = getSkillRequirements(meta3.name);
34921
+ const envVars = reqs?.envVars || [];
34872
34922
  return {
34873
34923
  name: meta3.name,
34874
34924
  displayName: meta3.displayName,
@@ -34876,7 +34926,8 @@ function getAllSkillsWithStatus() {
34876
34926
  category: meta3.category,
34877
34927
  tags: meta3.tags,
34878
34928
  installed: installed.has(meta3.name),
34879
- envVars: reqs?.envVars || [],
34929
+ envVars,
34930
+ envVarsSet: envVars.filter((v) => !!process.env[v]),
34880
34931
  systemDeps: reqs?.systemDeps || [],
34881
34932
  cliCommand: reqs?.cliCommand || null
34882
34933
  };
@@ -34891,126 +34942,181 @@ function serveStaticFile(filePath) {
34891
34942
  headers: { "Content-Type": contentType }
34892
34943
  });
34893
34944
  }
34894
- async function startServer(port, options) {
34895
- const shouldOpen = options?.open ?? true;
34896
- const dashboardDir = resolveDashboardDir();
34897
- const dashboardExists = existsSync3(dashboardDir);
34898
- if (!dashboardExists) {
34899
- console.error(`
34900
- Dashboard not found at: ${dashboardDir}`);
34901
- console.error(`Run this to build it:
34902
- `);
34903
- console.error(` cd dashboard && bun install && bun run build
34904
- `);
34905
- }
34906
- const server2 = Bun.serve({
34907
- port,
34908
- async fetch(req) {
34909
- const url2 = new URL(req.url);
34910
- const path = url2.pathname;
34911
- const method = req.method;
34912
- if (path === "/api/skills" && method === "GET") {
34913
- return json2(getAllSkillsWithStatus());
34914
- }
34915
- if (path === "/api/categories" && method === "GET") {
34916
- const counts = CATEGORIES.map((cat) => ({
34917
- name: cat,
34918
- count: getSkillsByCategory(cat).length
34919
- }));
34920
- return json2(counts);
34921
- }
34922
- if (path === "/api/skills/search" && method === "GET") {
34923
- const query = url2.searchParams.get("q") || "";
34924
- if (!query.trim())
34925
- return json2([]);
34926
- const results = searchSkills(query);
34927
- const installed = new Set(getInstalledSkills());
34928
- return json2(results.map((meta3) => {
34929
- const reqs = getSkillRequirements(meta3.name);
34930
- return {
34931
- name: meta3.name,
34932
- displayName: meta3.displayName,
34933
- description: meta3.description,
34934
- category: meta3.category,
34935
- tags: meta3.tags,
34936
- installed: installed.has(meta3.name),
34937
- envVars: reqs?.envVars || [],
34938
- systemDeps: reqs?.systemDeps || [],
34939
- cliCommand: reqs?.cliCommand || null
34940
- };
34941
- }));
34942
- }
34943
- const singleMatch = path.match(/^\/api\/skills\/([^/]+)$/);
34944
- if (singleMatch && method === "GET") {
34945
- const name = singleMatch[1];
34946
- if (!isValidSkillName(name))
34947
- return json2({ error: "Invalid skill name" }, 400);
34948
- const meta3 = getSkill(name);
34949
- if (!meta3)
34950
- return json2({ error: `Skill '${name}' not found` }, 404);
34951
- const reqs = getSkillRequirements(name);
34952
- const docs = getSkillBestDoc(name);
34953
- const installed = new Set(getInstalledSkills());
34954
- return json2({
34945
+ function createFetchHandler(options) {
34946
+ const dashboardDir = options?.dashboardDir ?? resolveDashboardDir();
34947
+ const dashboardExists = options?.dashboardExists ?? existsSync3(dashboardDir);
34948
+ return async function fetchHandler(req) {
34949
+ const url2 = new URL(req.url);
34950
+ const path = url2.pathname;
34951
+ const method = req.method;
34952
+ if (path === "/api/skills" && method === "GET") {
34953
+ return json2(getAllSkillsWithStatus());
34954
+ }
34955
+ if (path === "/api/categories" && method === "GET") {
34956
+ const counts = CATEGORIES.map((cat) => ({
34957
+ name: cat,
34958
+ count: getSkillsByCategory(cat).length
34959
+ }));
34960
+ return json2(counts);
34961
+ }
34962
+ if (path === "/api/skills/search" && method === "GET") {
34963
+ const query = url2.searchParams.get("q") || "";
34964
+ if (!query.trim())
34965
+ return json2([]);
34966
+ const results = searchSkills(query);
34967
+ const installed = new Set(getInstalledSkills());
34968
+ return json2(results.map((meta3) => {
34969
+ const reqs = getSkillRequirements(meta3.name);
34970
+ const envVars = reqs?.envVars || [];
34971
+ return {
34955
34972
  name: meta3.name,
34956
34973
  displayName: meta3.displayName,
34957
34974
  description: meta3.description,
34958
34975
  category: meta3.category,
34959
34976
  tags: meta3.tags,
34960
34977
  installed: installed.has(meta3.name),
34961
- envVars: reqs?.envVars || [],
34978
+ envVars,
34979
+ envVarsSet: envVars.filter((v) => !!process.env[v]),
34962
34980
  systemDeps: reqs?.systemDeps || [],
34963
- cliCommand: reqs?.cliCommand || null,
34964
- docs: docs || null
34965
- });
34966
- }
34967
- const docsMatch = path.match(/^\/api\/skills\/([^/]+)\/docs$/);
34968
- if (docsMatch && method === "GET") {
34969
- const name = docsMatch[1];
34970
- if (!isValidSkillName(name))
34971
- return json2({ error: "Invalid skill name" }, 400);
34972
- const content = getSkillBestDoc(name);
34973
- return json2({ content: content || null });
34974
- }
34975
- const installMatch = path.match(/^\/api\/skills\/([^/]+)\/install$/);
34976
- if (installMatch && method === "POST") {
34977
- const name = installMatch[1];
34978
- if (!isValidSkillName(name))
34979
- return json2({ error: "Invalid skill name" }, 400);
34981
+ cliCommand: reqs?.cliCommand || null
34982
+ };
34983
+ }));
34984
+ }
34985
+ const singleMatch = path.match(/^\/api\/skills\/([^/]+)$/);
34986
+ if (singleMatch && method === "GET") {
34987
+ const name = singleMatch[1];
34988
+ if (!isValidSkillName(name))
34989
+ return json2({ error: "Invalid skill name" }, 400);
34990
+ const meta3 = getSkill(name);
34991
+ if (!meta3)
34992
+ return json2({ error: `Skill '${name}' not found` }, 404);
34993
+ const reqs = getSkillRequirements(name);
34994
+ const docs = getSkillBestDoc(name);
34995
+ const installed = new Set(getInstalledSkills());
34996
+ const envVars = reqs?.envVars || [];
34997
+ return json2({
34998
+ name: meta3.name,
34999
+ displayName: meta3.displayName,
35000
+ description: meta3.description,
35001
+ category: meta3.category,
35002
+ tags: meta3.tags,
35003
+ installed: installed.has(meta3.name),
35004
+ envVars,
35005
+ envVarsSet: envVars.filter((v) => !!process.env[v]),
35006
+ systemDeps: reqs?.systemDeps || [],
35007
+ cliCommand: reqs?.cliCommand || null,
35008
+ docs: docs || null
35009
+ });
35010
+ }
35011
+ const docsMatch = path.match(/^\/api\/skills\/([^/]+)\/docs$/);
35012
+ if (docsMatch && method === "GET") {
35013
+ const name = docsMatch[1];
35014
+ if (!isValidSkillName(name))
35015
+ return json2({ error: "Invalid skill name" }, 400);
35016
+ const content = getSkillBestDoc(name);
35017
+ return json2({ content: content || null });
35018
+ }
35019
+ const installMatch = path.match(/^\/api\/skills\/([^/]+)\/install$/);
35020
+ if (installMatch && method === "POST") {
35021
+ const name = installMatch[1];
35022
+ if (!isValidSkillName(name))
35023
+ return json2({ error: "Invalid skill name" }, 400);
35024
+ let body = {};
35025
+ try {
35026
+ const text = await req.text();
35027
+ if (text)
35028
+ body = JSON.parse(text);
35029
+ } catch {}
35030
+ if (body.for) {
35031
+ try {
35032
+ const agents = resolveAgents(body.for);
35033
+ const scope = body.scope === "project" ? "project" : "global";
35034
+ const results = agents.map((agent) => installSkillForAgent(name, { agent, scope }, generateSkillMd));
35035
+ const allSuccess = results.every((r) => r.success);
35036
+ const errors4 = results.filter((r) => !r.success).map((r) => r.error);
35037
+ return json2({
35038
+ skill: name,
35039
+ success: allSuccess,
35040
+ results,
35041
+ ...errors4.length > 0 ? { error: errors4.join("; ") } : {}
35042
+ }, allSuccess ? 200 : 400);
35043
+ } catch (e) {
35044
+ return json2({ skill: name, success: false, error: e instanceof Error ? e.message : "Unknown error" }, 400);
35045
+ }
35046
+ } else {
34980
35047
  const result = installSkill(name);
34981
35048
  return json2(result, result.success ? 200 : 400);
34982
35049
  }
34983
- const removeMatch = path.match(/^\/api\/skills\/([^/]+)\/remove$/);
34984
- if (removeMatch && method === "POST") {
34985
- const name = removeMatch[1];
34986
- if (!isValidSkillName(name))
34987
- return json2({ error: "Invalid skill name" }, 400);
34988
- const success2 = removeSkill(name);
34989
- return json2({ success: success2, skill: name }, success2 ? 200 : 404);
34990
- }
34991
- if (method === "OPTIONS") {
34992
- return new Response(null, {
34993
- headers: {
34994
- "Access-Control-Allow-Origin": "*",
34995
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
34996
- "Access-Control-Allow-Headers": "Content-Type"
34997
- }
35050
+ }
35051
+ const removeMatch = path.match(/^\/api\/skills\/([^/]+)\/remove$/);
35052
+ if (removeMatch && method === "POST") {
35053
+ const name = removeMatch[1];
35054
+ if (!isValidSkillName(name))
35055
+ return json2({ error: "Invalid skill name" }, 400);
35056
+ const success2 = removeSkill(name);
35057
+ return json2({ success: success2, skill: name }, success2 ? 200 : 404);
35058
+ }
35059
+ if (path === "/api/version" && method === "GET") {
35060
+ const pkg = getPackageJson();
35061
+ return json2({ version: pkg.version, name: pkg.name });
35062
+ }
35063
+ if (path === "/api/self-update" && method === "POST") {
35064
+ try {
35065
+ const pkg = getPackageJson();
35066
+ const proc = Bun.spawn(["bun", "add", "-g", `${pkg.name}@latest`], {
35067
+ stdout: "pipe",
35068
+ stderr: "pipe"
34998
35069
  });
35070
+ const stdout = await new Response(proc.stdout).text();
35071
+ const stderr = await new Response(proc.stderr).text();
35072
+ const exitCode = await proc.exited;
35073
+ if (exitCode === 0) {
35074
+ return json2({ success: true, output: stdout.trim() || stderr.trim() });
35075
+ }
35076
+ return json2({ success: false, error: stderr.trim() || stdout.trim() }, 500);
35077
+ } catch (e) {
35078
+ return json2({ success: false, error: e instanceof Error ? e.message : "Update failed" }, 500);
34999
35079
  }
35000
- if (dashboardExists && (method === "GET" || method === "HEAD")) {
35001
- if (path !== "/") {
35002
- const filePath = join3(dashboardDir, path);
35003
- const res2 = serveStaticFile(filePath);
35004
- if (res2)
35005
- return res2;
35080
+ }
35081
+ if (method === "OPTIONS") {
35082
+ return new Response(null, {
35083
+ headers: {
35084
+ "Access-Control-Allow-Origin": "*",
35085
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
35086
+ "Access-Control-Allow-Headers": "Content-Type"
35006
35087
  }
35007
- const indexPath = join3(dashboardDir, "index.html");
35008
- const res = serveStaticFile(indexPath);
35009
- if (res)
35010
- return res;
35088
+ });
35089
+ }
35090
+ if (dashboardExists && (method === "GET" || method === "HEAD")) {
35091
+ if (path !== "/") {
35092
+ const filePath = join3(dashboardDir, path);
35093
+ const res2 = serveStaticFile(filePath);
35094
+ if (res2)
35095
+ return res2;
35011
35096
  }
35012
- return json2({ error: "Not found" }, 404);
35097
+ const indexPath = join3(dashboardDir, "index.html");
35098
+ const res = serveStaticFile(indexPath);
35099
+ if (res)
35100
+ return res;
35013
35101
  }
35102
+ return json2({ error: "Not found" }, 404);
35103
+ };
35104
+ }
35105
+ async function startServer(port = 0, options) {
35106
+ const shouldOpen = options?.open ?? true;
35107
+ const dashboardDir = resolveDashboardDir();
35108
+ const dashboardExists = existsSync3(dashboardDir);
35109
+ if (!dashboardExists) {
35110
+ console.error(`
35111
+ Dashboard not found at: ${dashboardDir}`);
35112
+ console.error(`Run this to build it:
35113
+ `);
35114
+ console.error(` cd dashboard && bun install && bun run build
35115
+ `);
35116
+ }
35117
+ const server2 = Bun.serve({
35118
+ port,
35119
+ fetch: createFetchHandler({ dashboardDir, dashboardExists })
35014
35120
  });
35015
35121
  const shutdown = () => {
35016
35122
  server2.stop();
@@ -35018,7 +35124,8 @@ Dashboard not found at: ${dashboardDir}`);
35018
35124
  };
35019
35125
  process.on("SIGINT", shutdown);
35020
35126
  process.on("SIGTERM", shutdown);
35021
- const serverUrl = `http://localhost:${port}`;
35127
+ const actualPort = server2.port;
35128
+ const serverUrl = `http://localhost:${actualPort}`;
35022
35129
  console.log(`Skills Dashboard running at ${serverUrl}`);
35023
35130
  if (shouldOpen) {
35024
35131
  try {
@@ -35051,7 +35158,7 @@ var init_serve = __esm(() => {
35051
35158
  };
35052
35159
  isMain = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("serve.ts") || process.argv[1]?.endsWith("serve.js");
35053
35160
  if (isMain) {
35054
- const port = parseInt(process.env.PORT || "3579", 10);
35161
+ const port = parseInt(process.env.PORT || "0", 10);
35055
35162
  startServer(port, { open: !process.env.NO_OPEN });
35056
35163
  }
35057
35164
  });
@@ -35078,7 +35185,7 @@ var {
35078
35185
  // src/cli/index.tsx
35079
35186
  init_package();
35080
35187
  import chalk2 from "chalk";
35081
- import { existsSync as existsSync4, writeFileSync as writeFileSync2, appendFileSync, readFileSync as readFileSync3 } from "fs";
35188
+ import { existsSync as existsSync4, writeFileSync as writeFileSync2, appendFileSync, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
35082
35189
  import { join as join4 } from "path";
35083
35190
 
35084
35191
  // src/cli/components/App.tsx
@@ -36204,7 +36311,7 @@ init_registry();
36204
36311
  init_installer();
36205
36312
  init_skillinfo();
36206
36313
  import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
36207
- var isTTY = process.stdout.isTTY ?? false;
36314
+ var isTTY = (process.stdout.isTTY ?? false) && (process.stdin.isTTY ?? false);
36208
36315
  var program2 = new Command;
36209
36316
  program2.name("skills").description("Install AI agent skills for your project").version(package_default.version).option("--verbose", "Enable verbose logging", false).enablePositionalOptions();
36210
36317
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive skill browser (TUI)").action(() => {
@@ -36274,21 +36381,31 @@ SKILL.md copied to agent skill directories`));
36274
36381
  }
36275
36382
  return;
36276
36383
  }
36277
- for (const name of skills) {
36384
+ const total = skills.length;
36385
+ for (let i = 0;i < total; i++) {
36386
+ const name = skills[i];
36387
+ if (total > 1 && !options.json) {
36388
+ process.stdout.write(`[${i + 1}/${total}] Installing ${name}...`);
36389
+ }
36278
36390
  const result = installSkill(name, { overwrite: options.overwrite });
36279
36391
  results.push(result);
36392
+ if (total > 1 && !options.json) {
36393
+ console.log(result.success ? " done" : " failed");
36394
+ }
36280
36395
  }
36281
36396
  if (options.json) {
36282
36397
  console.log(JSON.stringify(results, null, 2));
36283
36398
  } else {
36284
- console.log(chalk2.bold(`
36399
+ if (total <= 1) {
36400
+ console.log(chalk2.bold(`
36285
36401
  Installing skills...
36286
36402
  `));
36287
- for (const result of results) {
36288
- if (result.success) {
36289
- console.log(chalk2.green(`\u2713 ${result.skill}`));
36290
- } else {
36291
- console.log(chalk2.red(`\u2717 ${result.skill}: ${result.error}`));
36403
+ for (const result of results) {
36404
+ if (result.success) {
36405
+ console.log(chalk2.green(`\u2713 ${result.skill}`));
36406
+ } else {
36407
+ console.log(chalk2.red(`\u2717 ${result.skill}: ${result.error}`));
36408
+ }
36292
36409
  }
36293
36410
  }
36294
36411
  console.log(chalk2.dim(`
@@ -36355,8 +36472,18 @@ Available skills (${SKILLS.length}):
36355
36472
  console.log();
36356
36473
  }
36357
36474
  });
36358
- program2.command("search").argument("<query>", "Search term").option("--json", "Output as JSON", false).description("Search for skills").action((query, options) => {
36359
- const results = searchSkills(query);
36475
+ program2.command("search").argument("<query>", "Search term").option("--json", "Output as JSON", false).option("-c, --category <category>", "Filter results by category").description("Search for skills").action((query, options) => {
36476
+ let results = searchSkills(query);
36477
+ if (options.category) {
36478
+ const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
36479
+ if (!category) {
36480
+ console.error(`Unknown category: ${options.category}`);
36481
+ console.error(`Available: ${CATEGORIES.join(", ")}`);
36482
+ process.exitCode = 1;
36483
+ return;
36484
+ }
36485
+ results = results.filter((s) => s.category === category);
36486
+ }
36360
36487
  if (options.json) {
36361
36488
  console.log(JSON.stringify(results, null, 2));
36362
36489
  return;
@@ -36501,27 +36628,70 @@ program2.command("run").argument("<skill>", "Skill name").argument("[args...]",
36501
36628
  }
36502
36629
  process.exitCode = result.exitCode;
36503
36630
  });
36504
- program2.command("init").description("Initialize project for installed skills (.env.example, .gitignore)").action(() => {
36631
+ program2.command("init").option("--json", "Output as JSON", false).description("Initialize project for installed skills (.env.example, .gitignore)").action((options) => {
36505
36632
  const cwd = process.cwd();
36506
36633
  const installed = getInstalledSkills();
36507
36634
  if (installed.length === 0) {
36508
- console.log(chalk2.dim("No skills installed. Run: skills install <name>"));
36635
+ if (options.json) {
36636
+ console.log(JSON.stringify({ skills: [], envVars: 0, gitignoreUpdated: false }));
36637
+ } else {
36638
+ console.log(chalk2.dim("No skills installed. Run: skills install <name>"));
36639
+ }
36509
36640
  return;
36510
36641
  }
36511
- const envContent = generateEnvExample(cwd);
36512
- if (envContent) {
36642
+ const envMap = new Map;
36643
+ for (const name of installed) {
36644
+ const reqs = getSkillRequirements(name);
36645
+ if (reqs?.envVars.length) {
36646
+ for (const v of reqs.envVars) {
36647
+ if (!envMap.has(v))
36648
+ envMap.set(v, []);
36649
+ if (!envMap.get(v).includes(name))
36650
+ envMap.get(v).push(name);
36651
+ }
36652
+ }
36653
+ }
36654
+ let envVarCount = 0;
36655
+ if (envMap.size > 0) {
36656
+ const lines = [
36657
+ "# Environment variables for installed skills",
36658
+ "# Auto-generated by: skills init",
36659
+ ""
36660
+ ];
36661
+ const sorted = Array.from(envMap.entries()).sort(([a], [b]) => a.localeCompare(b));
36662
+ let lastPrefix = "";
36663
+ for (const [envVar, skills] of sorted) {
36664
+ const prefix = envVar.split("_")[0];
36665
+ if (prefix !== lastPrefix) {
36666
+ if (lastPrefix)
36667
+ lines.push("");
36668
+ lines.push(`# ${prefix}`);
36669
+ lastPrefix = prefix;
36670
+ }
36671
+ lines.push(`# Used by: ${skills.join(", ")}`);
36672
+ lines.push(`${envVar}=`);
36673
+ }
36674
+ const envContent = lines.join(`
36675
+ `) + `
36676
+ `;
36513
36677
  const envPath = join4(cwd, ".env.example");
36514
36678
  writeFileSync2(envPath, envContent);
36515
- console.log(chalk2.green(`\u2713 Generated .env.example`));
36679
+ envVarCount = envMap.size;
36680
+ if (!options.json) {
36681
+ console.log(chalk2.green(`\u2713 Generated .env.example (${envVarCount} variables from ${installed.length} skills)`));
36682
+ }
36516
36683
  } else {
36517
- console.log(chalk2.dim(" No environment variables detected across installed skills"));
36684
+ if (!options.json) {
36685
+ console.log(chalk2.dim(" No environment variables detected across installed skills"));
36686
+ }
36518
36687
  }
36519
36688
  const gitignorePath = join4(cwd, ".gitignore");
36520
36689
  const gitignoreEntry = ".skills/";
36521
36690
  let gitignoreContent = "";
36522
36691
  if (existsSync4(gitignorePath)) {
36523
- gitignoreContent = readFileSync3(gitignorePath, "utf-8");
36692
+ gitignoreContent = readFileSync4(gitignorePath, "utf-8");
36524
36693
  }
36694
+ let gitignoreUpdated = false;
36525
36695
  if (!gitignoreContent.includes(gitignoreEntry)) {
36526
36696
  const addition = gitignoreContent.endsWith(`
36527
36697
  `) || gitignoreContent === "" ? `
@@ -36533,12 +36703,35 @@ ${gitignoreEntry}
36533
36703
  ${gitignoreEntry}
36534
36704
  `;
36535
36705
  appendFileSync(gitignorePath, addition);
36536
- console.log(chalk2.green(`\u2713 Added .skills/ to .gitignore`));
36706
+ gitignoreUpdated = true;
36707
+ if (!options.json) {
36708
+ console.log(chalk2.green(`\u2713 Added .skills/ to .gitignore`));
36709
+ }
36537
36710
  } else {
36538
- console.log(chalk2.dim(" .skills/ already in .gitignore"));
36711
+ if (!options.json) {
36712
+ console.log(chalk2.dim(" .skills/ already in .gitignore"));
36713
+ }
36539
36714
  }
36540
- console.log(chalk2.bold(`
36715
+ if (options.json) {
36716
+ console.log(JSON.stringify({
36717
+ skills: installed,
36718
+ envVars: envVarCount,
36719
+ gitignoreUpdated
36720
+ }, null, 2));
36721
+ } else {
36722
+ if (envMap.size > 0) {
36723
+ console.log(chalk2.bold(`
36724
+ Skill environment requirements:`));
36725
+ for (const name of installed) {
36726
+ const reqs = getSkillRequirements(name);
36727
+ if (reqs?.envVars.length) {
36728
+ console.log(` ${chalk2.cyan(name)}: ${reqs.envVars.join(", ")}`);
36729
+ }
36730
+ }
36731
+ }
36732
+ console.log(chalk2.bold(`
36541
36733
  Initialized for ${installed.length} installed skill(s)`));
36734
+ }
36542
36735
  });
36543
36736
  program2.command("remove").alias("rm").argument("<skill>", "Skill to remove").option("--json", "Output as JSON", false).option("--for <agent>", "Remove from agent: claude, codex, gemini, or all").option("--scope <scope>", "Remove scope: global or project", "global").option("--dry-run", "Print what would happen without actually removing", false).description("Remove an installed skill").action((skill, options) => {
36544
36737
  if (options.for) {
@@ -36601,16 +36794,57 @@ program2.command("update").argument("[skills...]", "Skills to update (default: a
36601
36794
  console.log(chalk2.dim("No skills installed. Run: skills install <name>"));
36602
36795
  return;
36603
36796
  }
36604
- const results = toUpdate.map((name) => installSkill(name, { overwrite: true }));
36797
+ function collectFiles(dir, base = "") {
36798
+ const files = new Set;
36799
+ if (!existsSync4(dir))
36800
+ return files;
36801
+ for (const entry of readdirSync3(dir)) {
36802
+ const full = join4(dir, entry);
36803
+ const rel = base ? `${base}/${entry}` : entry;
36804
+ if (statSync2(full).isDirectory()) {
36805
+ for (const f of collectFiles(full, rel))
36806
+ files.add(f);
36807
+ } else {
36808
+ files.add(rel);
36809
+ }
36810
+ }
36811
+ return files;
36812
+ }
36813
+ const updateResults = [];
36814
+ for (const name of toUpdate) {
36815
+ const skillName = normalizeSkillName(name);
36816
+ const destPath = join4(process.cwd(), ".skills", skillName);
36817
+ const beforeFiles = collectFiles(destPath);
36818
+ const result = installSkill(name, { overwrite: true });
36819
+ const afterFiles = collectFiles(destPath);
36820
+ const newFiles = [...afterFiles].filter((f) => !beforeFiles.has(f));
36821
+ const removedFiles = [...beforeFiles].filter((f) => !afterFiles.has(f));
36822
+ const unchangedCount = [...afterFiles].filter((f) => beforeFiles.has(f)).length;
36823
+ updateResults.push({
36824
+ skill: result.skill,
36825
+ success: result.success,
36826
+ error: result.error,
36827
+ newFiles,
36828
+ removedFiles,
36829
+ unchangedCount
36830
+ });
36831
+ }
36605
36832
  if (options.json) {
36606
- console.log(JSON.stringify(results, null, 2));
36833
+ console.log(JSON.stringify(updateResults, null, 2));
36607
36834
  } else {
36608
36835
  console.log(chalk2.bold(`
36609
36836
  Updating skills...
36610
36837
  `));
36611
- for (const result of results) {
36838
+ for (const result of updateResults) {
36612
36839
  if (result.success) {
36613
36840
  console.log(chalk2.green(`\u2713 ${result.skill}`));
36841
+ if (result.newFiles.length > 0) {
36842
+ console.log(chalk2.green(` + ${result.newFiles.length} new file(s): ${result.newFiles.join(", ")}`));
36843
+ }
36844
+ if (result.removedFiles.length > 0) {
36845
+ console.log(chalk2.red(` - ${result.removedFiles.length} removed file(s): ${result.removedFiles.join(", ")}`));
36846
+ }
36847
+ console.log(chalk2.dim(` ${result.unchangedCount} file(s) updated`));
36614
36848
  } else {
36615
36849
  console.log(chalk2.red(`\u2717 ${result.skill}: ${result.error}`));
36616
36850
  }
@@ -36618,7 +36852,7 @@ Updating skills...
36618
36852
  console.log(chalk2.dim(`
36619
36853
  Skills updated in .skills/`));
36620
36854
  }
36621
- if (results.some((r) => !r.success)) {
36855
+ if (updateResults.some((r) => !r.success)) {
36622
36856
  process.exitCode = 1;
36623
36857
  }
36624
36858
  });
@@ -36681,9 +36915,266 @@ Add to ${configPath} mcpServers:`));
36681
36915
  }
36682
36916
  await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
36683
36917
  });
36684
- program2.command("serve").description("Start the Skills Dashboard web server").option("-p, --port <port>", "Port number", "3579").option("--no-open", "Don't open browser automatically").action(async (options) => {
36918
+ program2.command("serve").description("Start the Skills Dashboard web server").option("-p, --port <port>", "Port number (0 = auto-assign free port)", "0").option("--no-open", "Don't open browser automatically").action(async (options) => {
36685
36919
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
36686
36920
  const port = parseInt(options.port, 10);
36687
36921
  await startServer2(port, { open: options.open });
36688
36922
  });
36923
+ program2.command("self-update").description(`Update ${package_default.name} to the latest version`).action(async () => {
36924
+ console.log(chalk2.bold(`
36925
+ Updating ${package_default.name}...
36926
+ `));
36927
+ const proc = Bun.spawn(["bun", "add", "-g", `${package_default.name}@latest`], {
36928
+ stdout: "inherit",
36929
+ stderr: "inherit"
36930
+ });
36931
+ const exitCode = await proc.exited;
36932
+ if (exitCode === 0) {
36933
+ console.log(chalk2.green(`
36934
+ \u2713 Updated to latest version`));
36935
+ const vProc = Bun.spawn(["skills", "--version"], { stdout: "pipe" });
36936
+ const ver = (await new Response(vProc.stdout).text()).trim();
36937
+ console.log(chalk2.dim(` Version: ${ver}`));
36938
+ } else {
36939
+ console.error(chalk2.red(`
36940
+ \u2717 Update failed`));
36941
+ process.exitCode = 1;
36942
+ }
36943
+ });
36944
+ program2.command("completion").argument("<shell>", "Shell type: bash, zsh, or fish").description("Generate shell completions").action((shell) => {
36945
+ const subcommands = [
36946
+ "install",
36947
+ "list",
36948
+ "search",
36949
+ "info",
36950
+ "docs",
36951
+ "requires",
36952
+ "run",
36953
+ "remove",
36954
+ "update",
36955
+ "categories",
36956
+ "mcp",
36957
+ "serve",
36958
+ "init",
36959
+ "self-update",
36960
+ "completion",
36961
+ "outdated"
36962
+ ];
36963
+ const skillNames = SKILLS.map((s) => s.name);
36964
+ const categoryNames = CATEGORIES.map((c) => c);
36965
+ const skillCmds = ["install", "info", "docs", "requires", "run", "remove"];
36966
+ switch (shell) {
36967
+ case "bash": {
36968
+ const script = `# Bash completion for skills CLI
36969
+ _skills_completions() {
36970
+ local cur prev subcmds skill_cmds skills categories
36971
+ COMPREPLY=()
36972
+ cur="\${COMP_WORDS[COMP_CWORD]}"
36973
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
36974
+ subcmds="${subcommands.join(" ")}"
36975
+ skill_cmds="${skillCmds.join(" ")}"
36976
+ skills="${skillNames.join(" ")}"
36977
+ categories="${categoryNames.join(" ")}"
36978
+
36979
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
36980
+ COMPREPLY=( $(compgen -W "\${subcmds}" -- "\${cur}") )
36981
+ return 0
36982
+ fi
36983
+
36984
+ case "\${prev}" in
36985
+ --category|-c)
36986
+ COMPREPLY=( $(compgen -W "\${categories}" -- "\${cur}") )
36987
+ return 0
36988
+ ;;
36989
+ esac
36990
+
36991
+ for cmd in \${skill_cmds}; do
36992
+ if [[ "\${COMP_WORDS[1]}" == "\${cmd}" && \${COMP_CWORD} -eq 2 ]]; then
36993
+ COMPREPLY=( $(compgen -W "\${skills}" -- "\${cur}") )
36994
+ return 0
36995
+ fi
36996
+ done
36997
+
36998
+ return 0
36999
+ }
37000
+ complete -F _skills_completions skills
37001
+ `;
37002
+ console.log(script);
37003
+ break;
37004
+ }
37005
+ case "zsh": {
37006
+ const script = `#compdef skills
37007
+ # Zsh completion for skills CLI
37008
+
37009
+ _skills() {
37010
+ local -a subcmds skill_cmds skills categories
37011
+
37012
+ subcmds=(
37013
+ ${subcommands.map((c) => ` '${c}:${c} command'`).join(`
37014
+ `)}
37015
+ )
37016
+
37017
+ skills=(${skillNames.join(" ")})
37018
+ categories=(${categoryNames.map((c) => `'${c.replace(/'/g, "'\\''")}'`).join(" ")})
37019
+
37020
+ skill_cmds=(${skillCmds.join(" ")})
37021
+
37022
+ _arguments -C \\
37023
+ '1:command:->command' \\
37024
+ '*::arg:->args'
37025
+
37026
+ case $state in
37027
+ command)
37028
+ _describe 'skills command' subcmds
37029
+ ;;
37030
+ args)
37031
+ case \${words[1]} in
37032
+ ${skillCmds.join("|")})
37033
+ _describe 'skill' skills
37034
+ ;;
37035
+ list|search)
37036
+ _arguments '--category[Filter by category]:category:($categories)'
37037
+ ;;
37038
+ completion)
37039
+ _describe 'shell' '(bash zsh fish)'
37040
+ ;;
37041
+ esac
37042
+ ;;
37043
+ esac
37044
+ }
37045
+
37046
+ _skills "$@"
37047
+ `;
37048
+ console.log(script);
37049
+ break;
37050
+ }
37051
+ case "fish": {
37052
+ const lines = [
37053
+ "# Fish completion for skills CLI",
37054
+ "",
37055
+ "# Disable file completions by default",
37056
+ "complete -c skills -f",
37057
+ "",
37058
+ "# Subcommands"
37059
+ ];
37060
+ for (const cmd of subcommands) {
37061
+ lines.push(`complete -c skills -n '__fish_use_subcommand' -a '${cmd}' -d '${cmd} command'`);
37062
+ }
37063
+ lines.push("");
37064
+ lines.push("# Skill names for relevant subcommands");
37065
+ for (const cmd of skillCmds) {
37066
+ for (const name of skillNames) {
37067
+ lines.push(`complete -c skills -n '__fish_seen_subcommand_from ${cmd}' -a '${name}'`);
37068
+ }
37069
+ }
37070
+ lines.push("");
37071
+ lines.push("# Category completions for --category flag");
37072
+ for (const cat of categoryNames) {
37073
+ lines.push(`complete -c skills -l category -s c -a '${cat}' -d 'Category'`);
37074
+ }
37075
+ console.log(lines.join(`
37076
+ `));
37077
+ break;
37078
+ }
37079
+ default:
37080
+ console.error(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
37081
+ process.exitCode = 1;
37082
+ }
37083
+ });
37084
+ program2.command("doctor").option("--json", "Output as JSON", false).description("Check environment variables for installed skills").action((options) => {
37085
+ const installed = getInstalledSkills();
37086
+ if (installed.length === 0) {
37087
+ if (options.json) {
37088
+ console.log(JSON.stringify({ skills: [], message: "No skills installed" }));
37089
+ } else {
37090
+ console.log("No skills installed");
37091
+ }
37092
+ return;
37093
+ }
37094
+ const report = [];
37095
+ for (const name of installed) {
37096
+ const reqs = getSkillRequirements(name);
37097
+ const envVars = (reqs?.envVars ?? []).map((v) => ({
37098
+ name: v,
37099
+ set: !!process.env[v]
37100
+ }));
37101
+ report.push({ skill: name, envVars });
37102
+ }
37103
+ if (options.json) {
37104
+ console.log(JSON.stringify(report, null, 2));
37105
+ return;
37106
+ }
37107
+ console.log(chalk2.bold(`
37108
+ Skills Doctor (${installed.length} installed):
37109
+ `));
37110
+ for (const entry of report) {
37111
+ console.log(chalk2.bold(` ${entry.skill}`));
37112
+ if (entry.envVars.length === 0) {
37113
+ console.log(chalk2.dim(" No environment variables required"));
37114
+ } else {
37115
+ for (const v of entry.envVars) {
37116
+ const status = v.set ? chalk2.green("set") : chalk2.red("missing");
37117
+ console.log(` ${v.name} [${status}]`);
37118
+ }
37119
+ }
37120
+ }
37121
+ });
37122
+ program2.command("outdated").option("--json", "Output as JSON", false).description("Check for outdated installed skills").action((options) => {
37123
+ const installed = getInstalledSkills();
37124
+ if (installed.length === 0) {
37125
+ if (options.json) {
37126
+ console.log(JSON.stringify([]));
37127
+ } else {
37128
+ console.log(chalk2.dim("No skills installed. Run: skills install <name>"));
37129
+ }
37130
+ return;
37131
+ }
37132
+ const cwd = process.cwd();
37133
+ const outdated = [];
37134
+ const upToDate = [];
37135
+ for (const name of installed) {
37136
+ const skillName = normalizeSkillName(name);
37137
+ const installedPkgPath = join4(cwd, ".skills", skillName, "package.json");
37138
+ let installedVersion = "unknown";
37139
+ if (existsSync4(installedPkgPath)) {
37140
+ try {
37141
+ installedVersion = JSON.parse(readFileSync4(installedPkgPath, "utf-8")).version || "unknown";
37142
+ } catch {}
37143
+ }
37144
+ const registryPath = getSkillPath(name);
37145
+ const registryPkgPath = join4(registryPath, "package.json");
37146
+ let registryVersion = "unknown";
37147
+ if (existsSync4(registryPkgPath)) {
37148
+ try {
37149
+ registryVersion = JSON.parse(readFileSync4(registryPkgPath, "utf-8")).version || "unknown";
37150
+ } catch {}
37151
+ }
37152
+ if (installedVersion !== registryVersion) {
37153
+ outdated.push({ skill: name, installedVersion, registryVersion });
37154
+ } else {
37155
+ upToDate.push(name);
37156
+ }
37157
+ }
37158
+ if (options.json) {
37159
+ console.log(JSON.stringify(outdated, null, 2));
37160
+ return;
37161
+ }
37162
+ if (outdated.length === 0) {
37163
+ console.log(chalk2.green(`
37164
+ All ${installed.length} installed skill(s) are up to date`));
37165
+ return;
37166
+ }
37167
+ console.log(chalk2.bold(`
37168
+ Outdated skills (${outdated.length}):
37169
+ `));
37170
+ for (const entry of outdated) {
37171
+ console.log(` ${chalk2.cyan(entry.skill)} ${chalk2.red(entry.installedVersion)} \u2192 ${chalk2.green(entry.registryVersion)}`);
37172
+ }
37173
+ if (upToDate.length > 0) {
37174
+ console.log(chalk2.dim(`
37175
+ ${upToDate.length} skill(s) up to date`));
37176
+ }
37177
+ console.log(chalk2.dim(`
37178
+ Run ${chalk2.bold("skills update")} to update all outdated skills`));
37179
+ });
36689
37180
  program2.parse();