@hasna/skills 0.1.4 → 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.4",
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,22 +34864,24 @@ 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
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";
34837
- function getPackageVersion() {
34873
+ function getPackageJson() {
34838
34874
  try {
34839
34875
  const scriptDir = dirname2(fileURLToPath2(import.meta.url));
34840
34876
  for (const rel of ["../..", ".."]) {
34841
34877
  const pkgPath = join3(scriptDir, rel, "package.json");
34842
34878
  if (existsSync3(pkgPath)) {
34843
- return JSON.parse(readFileSync3(pkgPath, "utf-8")).version;
34879
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
34880
+ return { version: pkg.version || "unknown", name: pkg.name || "skills" };
34844
34881
  }
34845
34882
  }
34846
34883
  } catch {}
34847
- return "unknown";
34884
+ return { version: "unknown", name: "skills" };
34848
34885
  }
34849
34886
  function resolveDashboardDir() {
34850
34887
  const candidates = [];
@@ -34881,6 +34918,7 @@ function getAllSkillsWithStatus() {
34881
34918
  const installed = new Set(getInstalledSkills());
34882
34919
  return SKILLS.map((meta3) => {
34883
34920
  const reqs = getSkillRequirements(meta3.name);
34921
+ const envVars = reqs?.envVars || [];
34884
34922
  return {
34885
34923
  name: meta3.name,
34886
34924
  displayName: meta3.displayName,
@@ -34888,7 +34926,8 @@ function getAllSkillsWithStatus() {
34888
34926
  category: meta3.category,
34889
34927
  tags: meta3.tags,
34890
34928
  installed: installed.has(meta3.name),
34891
- envVars: reqs?.envVars || [],
34929
+ envVars,
34930
+ envVarsSet: envVars.filter((v) => !!process.env[v]),
34892
34931
  systemDeps: reqs?.systemDeps || [],
34893
34932
  cliCommand: reqs?.cliCommand || null
34894
34933
  };
@@ -34903,146 +34942,181 @@ function serveStaticFile(filePath) {
34903
34942
  headers: { "Content-Type": contentType }
34904
34943
  });
34905
34944
  }
34906
- async function startServer(port, options) {
34907
- const shouldOpen = options?.open ?? true;
34908
- const dashboardDir = resolveDashboardDir();
34909
- const dashboardExists = existsSync3(dashboardDir);
34910
- if (!dashboardExists) {
34911
- console.error(`
34912
- Dashboard not found at: ${dashboardDir}`);
34913
- console.error(`Run this to build it:
34914
- `);
34915
- console.error(` cd dashboard && bun install && bun run build
34916
- `);
34917
- }
34918
- const server2 = Bun.serve({
34919
- port,
34920
- async fetch(req) {
34921
- const url2 = new URL(req.url);
34922
- const path = url2.pathname;
34923
- const method = req.method;
34924
- if (path === "/api/skills" && method === "GET") {
34925
- return json2(getAllSkillsWithStatus());
34926
- }
34927
- if (path === "/api/categories" && method === "GET") {
34928
- const counts = CATEGORIES.map((cat) => ({
34929
- name: cat,
34930
- count: getSkillsByCategory(cat).length
34931
- }));
34932
- return json2(counts);
34933
- }
34934
- if (path === "/api/skills/search" && method === "GET") {
34935
- const query = url2.searchParams.get("q") || "";
34936
- if (!query.trim())
34937
- return json2([]);
34938
- const results = searchSkills(query);
34939
- const installed = new Set(getInstalledSkills());
34940
- return json2(results.map((meta3) => {
34941
- const reqs = getSkillRequirements(meta3.name);
34942
- return {
34943
- name: meta3.name,
34944
- displayName: meta3.displayName,
34945
- description: meta3.description,
34946
- category: meta3.category,
34947
- tags: meta3.tags,
34948
- installed: installed.has(meta3.name),
34949
- envVars: reqs?.envVars || [],
34950
- systemDeps: reqs?.systemDeps || [],
34951
- cliCommand: reqs?.cliCommand || null
34952
- };
34953
- }));
34954
- }
34955
- const singleMatch = path.match(/^\/api\/skills\/([^/]+)$/);
34956
- if (singleMatch && method === "GET") {
34957
- const name = singleMatch[1];
34958
- if (!isValidSkillName(name))
34959
- return json2({ error: "Invalid skill name" }, 400);
34960
- const meta3 = getSkill(name);
34961
- if (!meta3)
34962
- return json2({ error: `Skill '${name}' not found` }, 404);
34963
- const reqs = getSkillRequirements(name);
34964
- const docs = getSkillBestDoc(name);
34965
- const installed = new Set(getInstalledSkills());
34966
- 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 {
34967
34972
  name: meta3.name,
34968
34973
  displayName: meta3.displayName,
34969
34974
  description: meta3.description,
34970
34975
  category: meta3.category,
34971
34976
  tags: meta3.tags,
34972
34977
  installed: installed.has(meta3.name),
34973
- envVars: reqs?.envVars || [],
34978
+ envVars,
34979
+ envVarsSet: envVars.filter((v) => !!process.env[v]),
34974
34980
  systemDeps: reqs?.systemDeps || [],
34975
- cliCommand: reqs?.cliCommand || null,
34976
- docs: docs || null
34977
- });
34978
- }
34979
- const docsMatch = path.match(/^\/api\/skills\/([^/]+)\/docs$/);
34980
- if (docsMatch && method === "GET") {
34981
- const name = docsMatch[1];
34982
- if (!isValidSkillName(name))
34983
- return json2({ error: "Invalid skill name" }, 400);
34984
- const content = getSkillBestDoc(name);
34985
- return json2({ content: content || null });
34986
- }
34987
- const installMatch = path.match(/^\/api\/skills\/([^/]+)\/install$/);
34988
- if (installMatch && method === "POST") {
34989
- const name = installMatch[1];
34990
- if (!isValidSkillName(name))
34991
- return json2({ error: "Invalid skill name" }, 400);
34992
- const result = installSkill(name);
34993
- return json2(result, result.success ? 200 : 400);
34994
- }
34995
- const removeMatch = path.match(/^\/api\/skills\/([^/]+)\/remove$/);
34996
- if (removeMatch && method === "POST") {
34997
- const name = removeMatch[1];
34998
- if (!isValidSkillName(name))
34999
- return json2({ error: "Invalid skill name" }, 400);
35000
- const success2 = removeSkill(name);
35001
- return json2({ success: success2, skill: name }, success2 ? 200 : 404);
35002
- }
35003
- if (path === "/api/version" && method === "GET") {
35004
- return json2({ version: getPackageVersion() });
35005
- }
35006
- if (path === "/api/self-update" && method === "POST") {
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) {
35007
35031
  try {
35008
- const proc = Bun.spawn(["bun", "add", "-g", "@hasna/skills@latest"], {
35009
- stdout: "pipe",
35010
- stderr: "pipe"
35011
- });
35012
- const stdout = await new Response(proc.stdout).text();
35013
- const stderr = await new Response(proc.stderr).text();
35014
- const exitCode = await proc.exited;
35015
- if (exitCode === 0) {
35016
- return json2({ success: true, output: stdout.trim() || stderr.trim() });
35017
- }
35018
- return json2({ success: false, error: stderr.trim() || stdout.trim() }, 500);
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);
35019
35043
  } catch (e) {
35020
- return json2({ success: false, error: e instanceof Error ? e.message : "Update failed" }, 500);
35044
+ return json2({ skill: name, success: false, error: e instanceof Error ? e.message : "Unknown error" }, 400);
35021
35045
  }
35046
+ } else {
35047
+ const result = installSkill(name);
35048
+ return json2(result, result.success ? 200 : 400);
35022
35049
  }
35023
- if (method === "OPTIONS") {
35024
- return new Response(null, {
35025
- headers: {
35026
- "Access-Control-Allow-Origin": "*",
35027
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
35028
- "Access-Control-Allow-Headers": "Content-Type"
35029
- }
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"
35030
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);
35031
35079
  }
35032
- if (dashboardExists && (method === "GET" || method === "HEAD")) {
35033
- if (path !== "/") {
35034
- const filePath = join3(dashboardDir, path);
35035
- const res2 = serveStaticFile(filePath);
35036
- if (res2)
35037
- 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"
35038
35087
  }
35039
- const indexPath = join3(dashboardDir, "index.html");
35040
- const res = serveStaticFile(indexPath);
35041
- if (res)
35042
- 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;
35043
35096
  }
35044
- return json2({ error: "Not found" }, 404);
35097
+ const indexPath = join3(dashboardDir, "index.html");
35098
+ const res = serveStaticFile(indexPath);
35099
+ if (res)
35100
+ return res;
35045
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 })
35046
35120
  });
35047
35121
  const shutdown = () => {
35048
35122
  server2.stop();
@@ -35050,7 +35124,8 @@ Dashboard not found at: ${dashboardDir}`);
35050
35124
  };
35051
35125
  process.on("SIGINT", shutdown);
35052
35126
  process.on("SIGTERM", shutdown);
35053
- const serverUrl = `http://localhost:${port}`;
35127
+ const actualPort = server2.port;
35128
+ const serverUrl = `http://localhost:${actualPort}`;
35054
35129
  console.log(`Skills Dashboard running at ${serverUrl}`);
35055
35130
  if (shouldOpen) {
35056
35131
  try {
@@ -35083,7 +35158,7 @@ var init_serve = __esm(() => {
35083
35158
  };
35084
35159
  isMain = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("serve.ts") || process.argv[1]?.endsWith("serve.js");
35085
35160
  if (isMain) {
35086
- const port = parseInt(process.env.PORT || "3579", 10);
35161
+ const port = parseInt(process.env.PORT || "0", 10);
35087
35162
  startServer(port, { open: !process.env.NO_OPEN });
35088
35163
  }
35089
35164
  });
@@ -35110,7 +35185,7 @@ var {
35110
35185
  // src/cli/index.tsx
35111
35186
  init_package();
35112
35187
  import chalk2 from "chalk";
35113
- import { existsSync as existsSync4, writeFileSync as writeFileSync2, appendFileSync, readFileSync as readFileSync4 } from "fs";
35188
+ import { existsSync as existsSync4, writeFileSync as writeFileSync2, appendFileSync, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
35114
35189
  import { join as join4 } from "path";
35115
35190
 
35116
35191
  // src/cli/components/App.tsx
@@ -36306,21 +36381,31 @@ SKILL.md copied to agent skill directories`));
36306
36381
  }
36307
36382
  return;
36308
36383
  }
36309
- 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
+ }
36310
36390
  const result = installSkill(name, { overwrite: options.overwrite });
36311
36391
  results.push(result);
36392
+ if (total > 1 && !options.json) {
36393
+ console.log(result.success ? " done" : " failed");
36394
+ }
36312
36395
  }
36313
36396
  if (options.json) {
36314
36397
  console.log(JSON.stringify(results, null, 2));
36315
36398
  } else {
36316
- console.log(chalk2.bold(`
36399
+ if (total <= 1) {
36400
+ console.log(chalk2.bold(`
36317
36401
  Installing skills...
36318
36402
  `));
36319
- for (const result of results) {
36320
- if (result.success) {
36321
- console.log(chalk2.green(`\u2713 ${result.skill}`));
36322
- } else {
36323
- 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
+ }
36324
36409
  }
36325
36410
  }
36326
36411
  console.log(chalk2.dim(`
@@ -36387,8 +36472,18 @@ Available skills (${SKILLS.length}):
36387
36472
  console.log();
36388
36473
  }
36389
36474
  });
36390
- program2.command("search").argument("<query>", "Search term").option("--json", "Output as JSON", false).description("Search for skills").action((query, options) => {
36391
- 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
+ }
36392
36487
  if (options.json) {
36393
36488
  console.log(JSON.stringify(results, null, 2));
36394
36489
  return;
@@ -36533,20 +36628,62 @@ program2.command("run").argument("<skill>", "Skill name").argument("[args...]",
36533
36628
  }
36534
36629
  process.exitCode = result.exitCode;
36535
36630
  });
36536
- 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) => {
36537
36632
  const cwd = process.cwd();
36538
36633
  const installed = getInstalledSkills();
36539
36634
  if (installed.length === 0) {
36540
- 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
+ }
36541
36640
  return;
36542
36641
  }
36543
- const envContent = generateEnvExample(cwd);
36544
- 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
+ `;
36545
36677
  const envPath = join4(cwd, ".env.example");
36546
36678
  writeFileSync2(envPath, envContent);
36547
- 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
+ }
36548
36683
  } else {
36549
- 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
+ }
36550
36687
  }
36551
36688
  const gitignorePath = join4(cwd, ".gitignore");
36552
36689
  const gitignoreEntry = ".skills/";
@@ -36554,6 +36691,7 @@ program2.command("init").description("Initialize project for installed skills (.
36554
36691
  if (existsSync4(gitignorePath)) {
36555
36692
  gitignoreContent = readFileSync4(gitignorePath, "utf-8");
36556
36693
  }
36694
+ let gitignoreUpdated = false;
36557
36695
  if (!gitignoreContent.includes(gitignoreEntry)) {
36558
36696
  const addition = gitignoreContent.endsWith(`
36559
36697
  `) || gitignoreContent === "" ? `
@@ -36565,12 +36703,35 @@ ${gitignoreEntry}
36565
36703
  ${gitignoreEntry}
36566
36704
  `;
36567
36705
  appendFileSync(gitignorePath, addition);
36568
- 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
+ }
36569
36710
  } else {
36570
- console.log(chalk2.dim(" .skills/ already in .gitignore"));
36711
+ if (!options.json) {
36712
+ console.log(chalk2.dim(" .skills/ already in .gitignore"));
36713
+ }
36571
36714
  }
36572
- 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(`
36573
36733
  Initialized for ${installed.length} installed skill(s)`));
36734
+ }
36574
36735
  });
36575
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) => {
36576
36737
  if (options.for) {
@@ -36633,16 +36794,57 @@ program2.command("update").argument("[skills...]", "Skills to update (default: a
36633
36794
  console.log(chalk2.dim("No skills installed. Run: skills install <name>"));
36634
36795
  return;
36635
36796
  }
36636
- 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
+ }
36637
36832
  if (options.json) {
36638
- console.log(JSON.stringify(results, null, 2));
36833
+ console.log(JSON.stringify(updateResults, null, 2));
36639
36834
  } else {
36640
36835
  console.log(chalk2.bold(`
36641
36836
  Updating skills...
36642
36837
  `));
36643
- for (const result of results) {
36838
+ for (const result of updateResults) {
36644
36839
  if (result.success) {
36645
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`));
36646
36848
  } else {
36647
36849
  console.log(chalk2.red(`\u2717 ${result.skill}: ${result.error}`));
36648
36850
  }
@@ -36650,7 +36852,7 @@ Updating skills...
36650
36852
  console.log(chalk2.dim(`
36651
36853
  Skills updated in .skills/`));
36652
36854
  }
36653
- if (results.some((r) => !r.success)) {
36855
+ if (updateResults.some((r) => !r.success)) {
36654
36856
  process.exitCode = 1;
36655
36857
  }
36656
36858
  });
@@ -36713,16 +36915,16 @@ Add to ${configPath} mcpServers:`));
36713
36915
  }
36714
36916
  await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
36715
36917
  });
36716
- 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) => {
36717
36919
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
36718
36920
  const port = parseInt(options.port, 10);
36719
36921
  await startServer2(port, { open: options.open });
36720
36922
  });
36721
- program2.command("self-update").description("Update @hasna/skills to the latest version").action(async () => {
36923
+ program2.command("self-update").description(`Update ${package_default.name} to the latest version`).action(async () => {
36722
36924
  console.log(chalk2.bold(`
36723
- Updating @hasna/skills...
36925
+ Updating ${package_default.name}...
36724
36926
  `));
36725
- const proc = Bun.spawn(["bun", "add", "-g", "@hasna/skills@latest"], {
36927
+ const proc = Bun.spawn(["bun", "add", "-g", `${package_default.name}@latest`], {
36726
36928
  stdout: "inherit",
36727
36929
  stderr: "inherit"
36728
36930
  });
@@ -36739,4 +36941,240 @@ Updating @hasna/skills...
36739
36941
  process.exitCode = 1;
36740
36942
  }
36741
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
+ });
36742
37180
  program2.parse();