skissue 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +10 -10
  2. package/dist/entry.js +136 -97
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -324,7 +324,7 @@ npm run check:all
324
324
 
325
325
  This repo uses a three-workflow CI/CD pipeline:
326
326
 
327
- 1. **PR & push** — [`ci.yml`](.github/workflows/ci.yml) runs `npm run verify` (TypeScript, ESLint, Prettier, tests, harness, harness score, build)
327
+ 1. **PR & push** — [`ci.yml`](.github/workflows/ci.yml) runs `npm run verify` (TypeScript, ESLint, Prettier, tests with coverage thresholds, harness, harness score, build)
328
328
  2. **Green main** — [`version-and-release.yml`](.github/workflows/version-and-release.yml) bumps the patch version, tags, and creates a GitHub Release
329
329
  3. **Tag push** — [`publish.yml`](.github/workflows/publish.yml) runs `npm publish --access public`
330
330
 
@@ -371,17 +371,17 @@ skillsRoot: .agents/skills
371
371
  git clone https://github.com/midyan/skissue.git
372
372
  cd skissue
373
373
  npm install # also enables Husky git hooks via prepare
374
- npm run verify # typecheck + lint + format + test + harness + harness score + build
374
+ npm run verify # typecheck + lint + format + test:coverage + harness + harness score + build
375
375
  ```
376
376
 
377
- | Script | What it does |
378
- | ----------------------- | ----------------------------------------------------------------------------- |
379
- | `npm run dev -- <args>` | Run CLI from source via tsx |
380
- | `npm run verify` | Full pipeline: tsc, eslint, prettier, vitest, check:all, harness score, build |
381
- | `npm run build` | Production build via esbuild |
382
- | `npm test` | Run vitest |
383
- | `npm run check:all` | Harness runner (hard skill checks) |
384
- | `npm run repo-verify` | Same as verify, with explicit skill discovery output |
377
+ | Script | What it does |
378
+ | ----------------------- | -------------------------------------------------------------------------------------- |
379
+ | `npm run dev -- <args>` | Run CLI from source via tsx |
380
+ | `npm run verify` | Full pipeline: tsc, eslint, prettier, vitest+coverage, check:all, harness score, build |
381
+ | `npm run build` | Production build via esbuild |
382
+ | `npm test` | Run vitest |
383
+ | `npm run check:all` | Harness runner (hard skill checks) |
384
+ | `npm run repo-verify` | Same as verify, with explicit skill discovery output |
385
385
 
386
386
  ### Project structure
387
387
 
package/dist/entry.js CHANGED
@@ -864,9 +864,9 @@ async function runInit(cwd) {
864
864
  }
865
865
 
866
866
  // src/commands/install.ts
867
- import chalk3 from "chalk";
868
- import ora from "ora";
869
- import { join as join6 } from "node:path";
867
+ import chalk4 from "chalk";
868
+ import ora2 from "ora";
869
+ import { join as join7 } from "node:path";
870
870
 
871
871
  // src/lockfile.ts
872
872
  import { readFile as readFile2, writeFile as writeFile3, mkdir as mkdir5 } from "node:fs/promises";
@@ -915,44 +915,85 @@ function removeSkillLock(lock, skillId) {
915
915
  return { ...lock, skills };
916
916
  }
917
917
 
918
- // src/registry/resolve.ts
919
- import { readFile as readFile3 } from "node:fs/promises";
918
+ // src/registry/catalog.ts
919
+ import { access as access2, readFile as readFile3, readdir } from "node:fs/promises";
920
+ import { constants } from "node:fs";
920
921
  import { join as join4 } from "node:path";
921
922
  import { z as z3 } from "zod";
922
923
  var RegistryJsonSchema = z3.object({
923
924
  skills: z3.record(z3.string(), z3.string()).optional()
924
925
  }).passthrough();
925
- async function resolveSkillPath(registryRepoRoot, skillId) {
926
+ async function listRegistrySkillIds(registryRepoRoot) {
927
+ const ids = /* @__PURE__ */ new Set();
926
928
  const registryFile = join4(registryRepoRoot, "registry.json");
929
+ try {
930
+ const raw = await readFile3(registryFile, "utf8");
931
+ const parsed = JSON.parse(raw);
932
+ const reg = RegistryJsonSchema.safeParse(parsed);
933
+ if (reg.success && reg.data.skills) {
934
+ for (const id of Object.keys(reg.data.skills)) {
935
+ if (id.trim()) ids.add(id);
936
+ }
937
+ }
938
+ } catch {
939
+ }
940
+ const registryDir = join4(registryRepoRoot, "registry");
941
+ try {
942
+ const entries = await readdir(registryDir, { withFileTypes: true });
943
+ for (const e of entries) {
944
+ if (!e.isDirectory()) continue;
945
+ const id = e.name;
946
+ if (!id.trim()) continue;
947
+ const skillRoot = join4(registryDir, id);
948
+ try {
949
+ await access2(join4(skillRoot, "SKILL.md"), constants.R_OK);
950
+ ids.add(id);
951
+ } catch {
952
+ }
953
+ }
954
+ } catch {
955
+ }
956
+ return [...ids].sort((a, b) => a.localeCompare(b));
957
+ }
958
+
959
+ // src/registry/resolve.ts
960
+ import { readFile as readFile4 } from "node:fs/promises";
961
+ import { join as join5 } from "node:path";
962
+ import { z as z4 } from "zod";
963
+ var RegistryJsonSchema2 = z4.object({
964
+ skills: z4.record(z4.string(), z4.string()).optional()
965
+ }).passthrough();
966
+ async function resolveSkillPath(registryRepoRoot, skillId) {
967
+ const registryFile = join5(registryRepoRoot, "registry.json");
927
968
  let raw;
928
969
  try {
929
- raw = await readFile3(registryFile, "utf8");
970
+ raw = await readFile4(registryFile, "utf8");
930
971
  } catch {
931
- return { skillPath: join4("registry", skillId).replace(/\\/g, "/"), source: "convention" };
972
+ return { skillPath: join5("registry", skillId).replace(/\\/g, "/"), source: "convention" };
932
973
  }
933
974
  const parsed = JSON.parse(raw);
934
- const reg = RegistryJsonSchema.safeParse(parsed);
975
+ const reg = RegistryJsonSchema2.safeParse(parsed);
935
976
  if (!reg.success || !reg.data.skills) {
936
- return { skillPath: join4("registry", skillId).replace(/\\/g, "/"), source: "convention" };
977
+ return { skillPath: join5("registry", skillId).replace(/\\/g, "/"), source: "convention" };
937
978
  }
938
979
  const mapped = reg.data.skills[skillId];
939
980
  if (mapped !== void 0 && mapped.length > 0) {
940
981
  return { skillPath: normalizeRelPath(mapped), source: "registry.json" };
941
982
  }
942
- return { skillPath: join4("registry", skillId).replace(/\\/g, "/"), source: "convention" };
983
+ return { skillPath: join5("registry", skillId).replace(/\\/g, "/"), source: "convention" };
943
984
  }
944
985
  function normalizeRelPath(p4) {
945
986
  return p4.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
946
987
  }
947
988
 
948
989
  // src/io.ts
949
- import { access as access2, cp, rm as rm2 } from "node:fs/promises";
950
- import { constants } from "node:fs";
951
- import { join as join5 } from "node:path";
990
+ import { access as access3, cp, rm as rm2 } from "node:fs/promises";
991
+ import { constants as constants2 } from "node:fs";
992
+ import { join as join6 } from "node:path";
952
993
  async function assertSkillMdPresent(skillSourceDir) {
953
- const p4 = join5(skillSourceDir, "SKILL.md");
994
+ const p4 = join6(skillSourceDir, "SKILL.md");
954
995
  try {
955
- await access2(p4, constants.R_OK);
996
+ await access3(p4, constants2.R_OK);
956
997
  } catch {
957
998
  throw new Error(`Expected SKILL.md in skill path: ${skillSourceDir}`);
958
999
  }
@@ -962,10 +1003,50 @@ async function copySkillTree(fromDir, toDir) {
962
1003
  await cp(fromDir, toDir, { recursive: true, force: true });
963
1004
  }
964
1005
 
1006
+ // src/commands/uninstall.ts
1007
+ import chalk3 from "chalk";
1008
+ import ora from "ora";
1009
+ import { rm as rm3 } from "node:fs/promises";
1010
+ async function uninstallSkillQuiet(cwd, skillId) {
1011
+ const config = await loadConfig(cwd);
1012
+ const dest = skillInstallPath(cwd, config.skillsRoot, skillId);
1013
+ await rm3(dest, { recursive: true, force: true });
1014
+ const lock = await readLockOrEmpty(cwd);
1015
+ if (lock.skills[skillId]) {
1016
+ await writeLock(cwd, removeSkillLock(lock, skillId));
1017
+ }
1018
+ }
1019
+ async function runUninstall(cwd, skillId) {
1020
+ const spin = ora(`Removing ${skillId}`).start();
1021
+ try {
1022
+ const lockBefore = await readLockOrEmpty(cwd);
1023
+ await uninstallSkillQuiet(cwd, skillId);
1024
+ if (!lockBefore.skills[skillId]) {
1025
+ spin.stopAndPersist({
1026
+ symbol: chalk3.yellow("\u26A0"),
1027
+ text: chalk3.yellow(`No lock entry for ${skillId}; removed directory if present.`)
1028
+ });
1029
+ } else {
1030
+ spin.succeed(chalk3.green(`Uninstalled ${skillId}`));
1031
+ }
1032
+ } catch (err) {
1033
+ spin.fail(chalk3.red(err instanceof Error ? err.message : String(err)));
1034
+ throw err;
1035
+ }
1036
+ }
1037
+
965
1038
  // src/commands/install.ts
966
- async function installSkillFromCheckout(cwd, config, repoPath, head, skillId) {
1039
+ async function installSkillFromCheckout(cwd, config, repoPath, head, skillId, catalogIds) {
1040
+ if (!catalogIds.has(skillId)) {
1041
+ const lock2 = await readLockOrEmpty(cwd);
1042
+ if (lock2.skills[skillId]) {
1043
+ await uninstallSkillQuiet(cwd, skillId);
1044
+ return { outcome: "removed", skillId };
1045
+ }
1046
+ throw new Error(`Skill "${skillId}" is not in the registry.`);
1047
+ }
967
1048
  const { skillPath } = await resolveSkillPath(repoPath, skillId);
968
- const src = join6(repoPath, skillPath);
1049
+ const src = join7(repoPath, skillPath);
969
1050
  await assertSkillMdPresent(src);
970
1051
  const dest = skillInstallPath(cwd, config.skillsRoot, skillId);
971
1052
  await copySkillTree(src, dest);
@@ -977,17 +1058,26 @@ async function installSkillFromCheckout(cwd, config, repoPath, head, skillId) {
977
1058
  ref
978
1059
  });
979
1060
  await writeLock(cwd, next);
980
- return dest;
1061
+ return { outcome: "installed", dest };
981
1062
  }
982
1063
  async function runInstall(cwd, skillId) {
983
1064
  const config = await loadConfig(cwd);
984
- const spin = ora(`Resolving registry and installing ${skillId}`).start();
1065
+ const spin = ora2(`Resolving registry and installing ${skillId}`).start();
985
1066
  try {
986
1067
  const { path: repoPath, head } = await ensureRegistryCheckout(cwd, config);
987
- const dest = await installSkillFromCheckout(cwd, config, repoPath, head, skillId);
988
- spin.succeed(chalk3.green(`Installed ${skillId} \u2192 ${dest}`));
1068
+ const catalogIds = new Set(await listRegistrySkillIds(repoPath));
1069
+ const result = await installSkillFromCheckout(cwd, config, repoPath, head, skillId, catalogIds);
1070
+ if (result.outcome === "removed") {
1071
+ spin.succeed(
1072
+ chalk4.yellow(
1073
+ `Removed ${skillId} \u2014 no longer in the registry (was still installed locally).`
1074
+ )
1075
+ );
1076
+ } else {
1077
+ spin.succeed(chalk4.green(`Installed ${skillId} \u2192 ${result.dest}`));
1078
+ }
989
1079
  } catch (err) {
990
- spin.fail(chalk3.red(err instanceof Error ? err.message : String(err)));
1080
+ spin.fail(chalk4.red(err instanceof Error ? err.message : String(err)));
991
1081
  throw err;
992
1082
  }
993
1083
  }
@@ -998,55 +1088,45 @@ async function runInstallMany(cwd, skillIds, options) {
998
1088
  if (options?.checkout) {
999
1089
  ({ path: repoPath, head } = options.checkout);
1000
1090
  } else {
1001
- const prep = ora("Preparing registry\u2026").start();
1091
+ const prep = ora2("Preparing registry\u2026").start();
1002
1092
  try {
1003
1093
  const c = await ensureRegistryCheckout(cwd, config);
1004
1094
  repoPath = c.path;
1005
1095
  head = c.head;
1006
- prep.succeed(chalk3.green("Registry ready."));
1096
+ prep.succeed(chalk4.green("Registry ready."));
1007
1097
  } catch (err) {
1008
- prep.fail(chalk3.red(err instanceof Error ? err.message : String(err)));
1098
+ prep.fail(chalk4.red(err instanceof Error ? err.message : String(err)));
1009
1099
  throw err;
1010
1100
  }
1011
1101
  }
1102
+ const catalogIds = new Set(await listRegistrySkillIds(repoPath));
1012
1103
  for (const skillId of skillIds) {
1013
- const spin = ora(`Installing ${skillId}\u2026`).start();
1104
+ const spin = ora2(`Installing ${skillId}\u2026`).start();
1014
1105
  try {
1015
- const dest = await installSkillFromCheckout(cwd, config, repoPath, head, skillId);
1016
- spin.succeed(chalk3.green(`Installed ${skillId} \u2192 ${dest}`));
1106
+ const result = await installSkillFromCheckout(
1107
+ cwd,
1108
+ config,
1109
+ repoPath,
1110
+ head,
1111
+ skillId,
1112
+ catalogIds
1113
+ );
1114
+ if (result.outcome === "removed") {
1115
+ spin.succeed(
1116
+ chalk4.yellow(
1117
+ `Removed ${skillId} \u2014 no longer in the registry (was still installed locally).`
1118
+ )
1119
+ );
1120
+ } else {
1121
+ spin.succeed(chalk4.green(`Installed ${skillId} \u2192 ${result.dest}`));
1122
+ }
1017
1123
  } catch (err) {
1018
- spin.fail(chalk3.red(err instanceof Error ? err.message : String(err)));
1124
+ spin.fail(chalk4.red(err instanceof Error ? err.message : String(err)));
1019
1125
  throw err;
1020
1126
  }
1021
1127
  }
1022
1128
  }
1023
1129
 
1024
- // src/commands/uninstall.ts
1025
- import chalk4 from "chalk";
1026
- import ora2 from "ora";
1027
- import { rm as rm3 } from "node:fs/promises";
1028
- async function runUninstall(cwd, skillId) {
1029
- const config = await loadConfig(cwd);
1030
- const spin = ora2(`Removing ${skillId}`).start();
1031
- try {
1032
- const dest = skillInstallPath(cwd, config.skillsRoot, skillId);
1033
- await rm3(dest, { recursive: true, force: true });
1034
- const lock = await readLockOrEmpty(cwd);
1035
- if (!lock.skills[skillId]) {
1036
- spin.stopAndPersist({
1037
- symbol: chalk4.yellow("\u26A0"),
1038
- text: chalk4.yellow(`No lock entry for ${skillId}; removed directory if present.`)
1039
- });
1040
- } else {
1041
- await writeLock(cwd, removeSkillLock(lock, skillId));
1042
- spin.succeed(chalk4.green(`Uninstalled ${skillId}`));
1043
- }
1044
- } catch (err) {
1045
- spin.fail(chalk4.red(err instanceof Error ? err.message : String(err)));
1046
- throw err;
1047
- }
1048
- }
1049
-
1050
1130
  // src/commands/list.ts
1051
1131
  import chalk5 from "chalk";
1052
1132
  async function runList(cwd) {
@@ -1186,47 +1266,6 @@ function printSkillIssueBanner(version) {
1186
1266
  console.log("");
1187
1267
  }
1188
1268
 
1189
- // src/registry/catalog.ts
1190
- import { access as access3, readFile as readFile4, readdir } from "node:fs/promises";
1191
- import { constants as constants2 } from "node:fs";
1192
- import { join as join7 } from "node:path";
1193
- import { z as z4 } from "zod";
1194
- var RegistryJsonSchema2 = z4.object({
1195
- skills: z4.record(z4.string(), z4.string()).optional()
1196
- }).passthrough();
1197
- async function listRegistrySkillIds(registryRepoRoot) {
1198
- const ids = /* @__PURE__ */ new Set();
1199
- const registryFile = join7(registryRepoRoot, "registry.json");
1200
- try {
1201
- const raw = await readFile4(registryFile, "utf8");
1202
- const parsed = JSON.parse(raw);
1203
- const reg = RegistryJsonSchema2.safeParse(parsed);
1204
- if (reg.success && reg.data.skills) {
1205
- for (const id of Object.keys(reg.data.skills)) {
1206
- if (id.trim()) ids.add(id);
1207
- }
1208
- }
1209
- } catch {
1210
- }
1211
- const registryDir = join7(registryRepoRoot, "registry");
1212
- try {
1213
- const entries = await readdir(registryDir, { withFileTypes: true });
1214
- for (const e of entries) {
1215
- if (!e.isDirectory()) continue;
1216
- const id = e.name;
1217
- if (!id.trim()) continue;
1218
- const skillRoot = join7(registryDir, id);
1219
- try {
1220
- await access3(join7(skillRoot, "SKILL.md"), constants2.R_OK);
1221
- ids.add(id);
1222
- } catch {
1223
- }
1224
- }
1225
- } catch {
1226
- }
1227
- return [...ids].sort((a, b) => a.localeCompare(b));
1228
- }
1229
-
1230
1269
  // src/commands/manage.ts
1231
1270
  var MAIN_MENU_DOUBLE_ESCAPE_MS = 1200;
1232
1271
  async function promptMainMenuSelect(availableCount, installedCount) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skissue",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,7 +41,7 @@
41
41
  "check:registry": "tsx harness/validate-registry/hard/index.ts",
42
42
  "check:all": "tsx harness/runner.ts",
43
43
  "check:harness-score": "tsx harness/report-harness-score/hard/index.ts",
44
- "verify": "tsc --noEmit && npm run lint && npm run format:check && npm test && npm run check:all && npm run check:harness-score && npm run build",
44
+ "verify": "tsc --noEmit && npm run lint && npm run format:check && npm run test:coverage && npm run check:all && npm run check:harness-score && npm run build",
45
45
  "repo-verify": "tsx harness/repo-verify/hard/index.ts",
46
46
  "prepublishOnly": "npm run build",
47
47
  "prepare": "husky || node -e \"process.exit(0)\""