ostacky 0.0.1 → 0.0.2

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 (28) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -26
  3. package/assets/agents/ostacky.md +65 -594
  4. package/assets/commands/install-stack.md +53 -19
  5. package/assets/commands/opsx-sync.md +24 -0
  6. package/assets/skills/brainstorming/SKILL.md +164 -0
  7. package/assets/skills/brainstorming/SKILL.md.placeholder +36 -0
  8. package/assets/skills/dispatching-parallel-agents/SKILL.md +182 -0
  9. package/assets/skills/dispatching-parallel-agents/SKILL.md.placeholder +36 -0
  10. package/assets/skills/openspec-apply-change/SKILL.md +156 -0
  11. package/assets/skills/openspec-apply-change/SKILL.md.placeholder +36 -0
  12. package/assets/skills/openspec-archive-change/SKILL.md +114 -0
  13. package/assets/skills/openspec-archive-change/SKILL.md.placeholder +36 -0
  14. package/assets/skills/openspec-explore/SKILL.md +288 -0
  15. package/assets/skills/openspec-explore/SKILL.md.placeholder +36 -0
  16. package/assets/skills/openspec-propose/SKILL.md +110 -0
  17. package/assets/skills/openspec-propose/SKILL.md.placeholder +36 -0
  18. package/assets/skills/review/SKILL.md +214 -0
  19. package/assets/skills/review/SKILL.md.placeholder +36 -0
  20. package/assets/skills/subagent-driven-development/SKILL.md +279 -0
  21. package/assets/skills/subagent-driven-development/SKILL.md.placeholder +36 -0
  22. package/assets/skills/tdd/SKILL.md +371 -0
  23. package/assets/skills/tdd/SKILL.md.placeholder +36 -0
  24. package/assets/skills/writing-plans/SKILL.md +152 -0
  25. package/assets/skills/writing-plans/SKILL.md.placeholder +36 -0
  26. package/dist/cli.js +389 -47
  27. package/manifest.json +84 -5
  28. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -161,7 +161,7 @@ var require_picocolors = __commonJS((exports, module) => {
161
161
  // package.json
162
162
  var package_default = {
163
163
  name: "ostacky",
164
- version: "0.0.1",
164
+ version: "0.0.2",
165
165
  description: "Instalador interactivo de agentes y comandos para OpenCode",
166
166
  type: "module",
167
167
  bin: {
@@ -177,7 +177,8 @@ var package_default = {
177
177
  files: [
178
178
  "dist/",
179
179
  "assets/",
180
- "manifest.json"
180
+ "manifest.json",
181
+ "LICENSE"
181
182
  ],
182
183
  keywords: [
183
184
  "opencode",
@@ -873,18 +874,18 @@ var L2 = () => {
873
874
  };
874
875
 
875
876
  // src/prompts.ts
876
- import { join as join4 } from "path";
877
+ import { join as join5 } from "path";
877
878
  // manifest.json
878
879
  var manifest_default = {
879
- version: "0.0.1",
880
+ version: "0.0.2",
880
881
  repo: "JaimeHoracio/Ostacky",
881
- tag: "v0.0.1",
882
+ tag: "v0.0.2",
882
883
  agents: [
883
884
  {
884
885
  name: "ostacky",
885
886
  file: "assets/agents/ostacky.md",
886
887
  description: "Agente principal de desarrollo con OpenCode",
887
- version: "0.0.1",
888
+ version: "0.0.2",
888
889
  sha256: null
889
890
  }
890
891
  ],
@@ -893,12 +894,95 @@ var manifest_default = {
893
894
  name: "install-stack",
894
895
  file: "assets/commands/install-stack.md",
895
896
  description: "Instala el stack tecnológico del proyecto",
896
- version: "0.0.1",
897
+ version: "0.0.2",
898
+ sha256: null
899
+ },
900
+ {
901
+ name: "opsx-sync",
902
+ file: "assets/commands/opsx-sync.md",
903
+ description: "Sincroniza delta specs del change activo (wrapper de openspec update)",
904
+ version: "0.0.2",
897
905
  sha256: null
898
906
  }
907
+ ],
908
+ skills: [
909
+ {
910
+ name: "brainstorming",
911
+ file: "assets/skills/brainstorming/SKILL.md",
912
+ description: "Skill de descubrimiento y exploración (Superpowers)",
913
+ version: "0.0.2",
914
+ sha256: "22be76380b086c4ac6d38b8b124f6f7df47c32a67237a6ceaf8c7fd19aa524b0"
915
+ },
916
+ {
917
+ name: "writing-plans",
918
+ file: "assets/skills/writing-plans/SKILL.md",
919
+ description: "Skill de planificación de implementación (Superpowers)",
920
+ version: "0.0.2",
921
+ sha256: "4d56ce833af79c628e72cd95b06d13597131a09f9d98aae0a42c6a98a8ee1b95"
922
+ },
923
+ {
924
+ name: "tdd",
925
+ file: "assets/skills/tdd/SKILL.md",
926
+ description: "Skill de test-driven development (Superpowers)",
927
+ version: "0.0.2",
928
+ sha256: "b5cd98b3c122162b5a5eac3c78858a64ba165bf4e8c251d8a804a593c0c72312"
929
+ },
930
+ {
931
+ name: "subagent-driven-development",
932
+ file: "assets/skills/subagent-driven-development/SKILL.md",
933
+ description: "Skill de ejecución con subagentes (Superpowers)",
934
+ version: "0.0.2",
935
+ sha256: "7534b60788e05644831347194b1f99bcb41e2eda130994cfddafee3146ba63bd"
936
+ },
937
+ {
938
+ name: "dispatching-parallel-agents",
939
+ file: "assets/skills/dispatching-parallel-agents/SKILL.md",
940
+ description: "Skill de dispatch paralelo de agentes (Superpowers)",
941
+ version: "0.0.2",
942
+ sha256: "b7e99d62c5d2ade21a3b6cdc94a7f90772e23fe3294ca600386f192f6181f6b2"
943
+ },
944
+ {
945
+ name: "review",
946
+ file: "assets/skills/review/SKILL.md",
947
+ description: "Skill de revisión de código (Superpowers)",
948
+ version: "0.0.2",
949
+ sha256: "af8fd392835967111e865f170d90d3dd9b8266b517ffb7d551dffda704500a90"
950
+ },
951
+ {
952
+ name: "openspec-explore",
953
+ file: "assets/skills/openspec-explore/SKILL.md",
954
+ description: "Skill de exploración pre-spec (OpenSpec)",
955
+ version: "0.0.2",
956
+ sha256: "c3eaff7d64b7e5e757b120dd231d2e6b56e9f440055dc9ab5431b466d6244bd7"
957
+ },
958
+ {
959
+ name: "openspec-propose",
960
+ file: "assets/skills/openspec-propose/SKILL.md",
961
+ description: "Skill de generación de proposal (OpenSpec)",
962
+ version: "0.0.2",
963
+ sha256: "28c59bdce6e9af0ab07102b2e6d0b47a0f0dc2f1bfb8321576d47f6c240e778b"
964
+ },
965
+ {
966
+ name: "openspec-apply-change",
967
+ file: "assets/skills/openspec-apply-change/SKILL.md",
968
+ description: "Skill de aplicación de change (OpenSpec)",
969
+ version: "0.0.2",
970
+ sha256: "8fa8182e40fd92cefbbcc1e303d9aa9c02e2b7f3f2c0625cb61fb916e4fc5760"
971
+ },
972
+ {
973
+ name: "openspec-archive-change",
974
+ file: "assets/skills/openspec-archive-change/SKILL.md",
975
+ description: "Skill de archivo de change (OpenSpec)",
976
+ version: "0.0.2",
977
+ sha256: "564b8dca28084c75ca192137113e9186c4fb31d2de0e26c081fd67c37f7d4a56"
978
+ }
899
979
  ]
900
980
  };
901
981
 
982
+ // src/github.ts
983
+ import { fileURLToPath } from "url";
984
+ import { dirname as dirname2, join as join2 } from "path";
985
+
902
986
  // src/cache.ts
903
987
  import { homedir } from "os";
904
988
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -955,6 +1039,14 @@ function putCache(repo, tag, filePath, content) {
955
1039
  // src/github.ts
956
1040
  var GITHUB_RAW = "https://raw.githubusercontent.com";
957
1041
  var GITHUB_API = "https://api.github.com";
1042
+ var __filename2 = fileURLToPath(import.meta.url);
1043
+ var __dirname2 = dirname2(__filename2);
1044
+ var PACKAGE_ROOT = join2(__dirname2, "..");
1045
+ var BUNDLED_SKILLS_DIR = join2(PACKAGE_ROOT, "assets", "skills");
1046
+ function getBundledSkillPath(name) {
1047
+ validateFilePath(name);
1048
+ return join2(BUNDLED_SKILLS_DIR, name);
1049
+ }
958
1050
  function getRawUrl(repo, tag, path) {
959
1051
  return `${GITHUB_RAW}/${repo}/${tag}/${path}`;
960
1052
  }
@@ -996,7 +1088,7 @@ async function fetchLatestManifest() {
996
1088
  }
997
1089
  async function downloadFile(manifest, filePath) {
998
1090
  validateFilePath(filePath);
999
- const item = [...manifest.agents, ...manifest.commands].find((i) => i.file === filePath);
1091
+ const item = [...manifest.agents, ...manifest.commands, ...manifest.skills ?? []].find((i) => i.file === filePath);
1000
1092
  const expectedHash = item?.sha256 ?? null;
1001
1093
  const cached = getCached(manifest.repo, manifest.tag, filePath, expectedHash);
1002
1094
  if (cached !== null)
@@ -1015,15 +1107,26 @@ async function downloadFile(manifest, filePath) {
1015
1107
  }
1016
1108
 
1017
1109
  // src/installer.ts
1018
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, unlinkSync } from "fs";
1019
- import { join as join3, resolve, dirname as dirname2 } from "path";
1110
+ import {
1111
+ existsSync as existsSync3,
1112
+ mkdirSync as mkdirSync2,
1113
+ writeFileSync as writeFileSync3,
1114
+ unlinkSync,
1115
+ readdirSync,
1116
+ statSync,
1117
+ copyFileSync,
1118
+ readFileSync as readFileSync3,
1119
+ rmSync
1120
+ } from "fs";
1121
+ import { createHash as createHash2 } from "crypto";
1122
+ import { join as join4, resolve, dirname as dirname3, relative } from "path";
1020
1123
 
1021
1124
  // src/lockfile.ts
1022
1125
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1023
- import { join as join2 } from "path";
1126
+ import { join as join3 } from "path";
1024
1127
  var LOCKFILE_NAME = "ostacky-lock.json";
1025
1128
  function getLockfilePath(opencodeRoot) {
1026
- return join2(opencodeRoot, LOCKFILE_NAME);
1129
+ return join3(opencodeRoot, LOCKFILE_NAME);
1027
1130
  }
1028
1131
  function readLockfile(opencodeRoot) {
1029
1132
  const lockPath = getLockfilePath(opencodeRoot);
@@ -1045,6 +1148,8 @@ function removeFromLockfile(opencodeRoot, type, name) {
1045
1148
  const lockfile = readLockfile(opencodeRoot);
1046
1149
  if (!lockfile)
1047
1150
  return;
1151
+ if (!lockfile[type])
1152
+ return;
1048
1153
  if (!(name in lockfile[type]))
1049
1154
  return;
1050
1155
  delete lockfile[type][name];
@@ -1054,17 +1159,19 @@ function clearLockfile(opencodeRoot) {
1054
1159
  const lockfile = readLockfile(opencodeRoot);
1055
1160
  if (!lockfile) {
1056
1161
  writeLockfile(opencodeRoot, {
1057
- version: "0.0.1",
1162
+ version: "0.0.2",
1058
1163
  lockedAt: new Date().toISOString(),
1059
1164
  repo: "",
1060
1165
  tag: "",
1061
1166
  agents: {},
1062
- commands: {}
1167
+ commands: {},
1168
+ skills: {}
1063
1169
  });
1064
1170
  return;
1065
1171
  }
1066
1172
  lockfile.agents = {};
1067
1173
  lockfile.commands = {};
1174
+ lockfile.skills = {};
1068
1175
  lockfile.lockedAt = new Date().toISOString();
1069
1176
  writeLockfile(opencodeRoot, lockfile);
1070
1177
  }
@@ -1073,12 +1180,12 @@ function clearLockfile(opencodeRoot) {
1073
1180
  function findOpenCodeDir(startDir = process.cwd()) {
1074
1181
  let current = resolve(startDir);
1075
1182
  while (true) {
1076
- const opencodeDir = join3(current, ".opencode");
1183
+ const opencodeDir = join4(current, ".opencode");
1077
1184
  if (existsSync3(opencodeDir))
1078
1185
  return opencodeDir;
1079
- if (existsSync3(join3(current, ".git")))
1186
+ if (existsSync3(join4(current, ".git")))
1080
1187
  return null;
1081
- const parent = dirname2(current);
1188
+ const parent = dirname3(current);
1082
1189
  if (parent === current)
1083
1190
  return null;
1084
1191
  current = parent;
@@ -1087,10 +1194,10 @@ function findOpenCodeDir(startDir = process.cwd()) {
1087
1194
  function findProjectRoot(startDir = process.cwd()) {
1088
1195
  let current = resolve(startDir);
1089
1196
  while (true) {
1090
- if (existsSync3(join3(current, ".opencode")) || existsSync3(join3(current, ".git"))) {
1197
+ if (existsSync3(join4(current, ".opencode")) || existsSync3(join4(current, ".git"))) {
1091
1198
  return current;
1092
1199
  }
1093
- const parent = dirname2(current);
1200
+ const parent = dirname3(current);
1094
1201
  if (parent === current)
1095
1202
  return resolve(startDir);
1096
1203
  current = parent;
@@ -1099,17 +1206,52 @@ function findProjectRoot(startDir = process.cwd()) {
1099
1206
  function ensureOpenCodePaths(opencodeDir) {
1100
1207
  const paths = {
1101
1208
  root: opencodeDir,
1102
- agents: join3(opencodeDir, "agents"),
1103
- commands: join3(opencodeDir, "commands")
1209
+ agents: join4(opencodeDir, "agents"),
1210
+ commands: join4(opencodeDir, "commands"),
1211
+ skills: join4(opencodeDir, "skills")
1104
1212
  };
1105
- for (const dir of [paths.root, paths.agents, paths.commands]) {
1213
+ for (const dir of [paths.root, paths.agents, paths.commands, paths.skills]) {
1106
1214
  if (!existsSync3(dir))
1107
1215
  mkdirSync2(dir, { recursive: true });
1108
1216
  }
1109
1217
  return paths;
1110
1218
  }
1111
1219
  function createOpenCodeDir(baseDir) {
1112
- return ensureOpenCodePaths(join3(baseDir, ".opencode"));
1220
+ return ensureOpenCodePaths(join4(baseDir, ".opencode"));
1221
+ }
1222
+ function copyDirRecursive(src, dest) {
1223
+ if (!existsSync3(dest))
1224
+ mkdirSync2(dest, { recursive: true });
1225
+ for (const entry of readdirSync(src)) {
1226
+ const srcPath = join4(src, entry);
1227
+ const destPath = join4(dest, entry);
1228
+ const stat = statSync(srcPath);
1229
+ if (stat.isDirectory()) {
1230
+ copyDirRecursive(srcPath, destPath);
1231
+ } else {
1232
+ copyFileSync(srcPath, destPath);
1233
+ }
1234
+ }
1235
+ }
1236
+ function computeTreeHash(dir) {
1237
+ const lines = [];
1238
+ walkForHash(dir, dir, lines);
1239
+ const combined = lines.sort().join(`
1240
+ `);
1241
+ return createHash2("sha256").update(combined, "utf-8").digest("hex");
1242
+ }
1243
+ function walkForHash(root, current, lines) {
1244
+ for (const entry of readdirSync(current)) {
1245
+ const full = join4(current, entry);
1246
+ const stat = statSync(full);
1247
+ if (stat.isDirectory()) {
1248
+ walkForHash(root, full, lines);
1249
+ } else {
1250
+ const rel = relative(root, full);
1251
+ const content = readFileSync3(full, "utf-8");
1252
+ lines.push(`${rel}:${sha256(content)}`);
1253
+ }
1254
+ }
1113
1255
  }
1114
1256
  function upsertLockfile(paths, type, item, manifest, contentHash) {
1115
1257
  const existing = readLockfile(paths.root) ?? {
@@ -1118,8 +1260,11 @@ function upsertLockfile(paths, type, item, manifest, contentHash) {
1118
1260
  repo: manifest.repo,
1119
1261
  tag: manifest.tag,
1120
1262
  agents: {},
1121
- commands: {}
1263
+ commands: {},
1264
+ skills: {}
1122
1265
  };
1266
+ if (!existing.skills)
1267
+ existing.skills = {};
1123
1268
  existing[type][item.name] = {
1124
1269
  version: item.version,
1125
1270
  installedAt: new Date().toISOString(),
@@ -1132,22 +1277,43 @@ function upsertLockfile(paths, type, item, manifest, contentHash) {
1132
1277
  }
1133
1278
  async function installAgent(item, manifest, paths) {
1134
1279
  const content = await downloadFile(manifest, item.file);
1135
- writeFileSync3(join3(paths.agents, `${item.name}.md`), content, "utf-8");
1280
+ writeFileSync3(join4(paths.agents, `${item.name}.md`), content, "utf-8");
1136
1281
  upsertLockfile(paths, "agents", item, manifest, sha256(content));
1137
1282
  }
1138
1283
  async function installCommand(item, manifest, paths) {
1139
1284
  const content = await downloadFile(manifest, item.file);
1140
- writeFileSync3(join3(paths.commands, `${item.name}.md`), content, "utf-8");
1285
+ writeFileSync3(join4(paths.commands, `${item.name}.md`), content, "utf-8");
1141
1286
  upsertLockfile(paths, "commands", item, manifest, sha256(content));
1142
1287
  }
1288
+ async function installSkill(item, manifest, paths) {
1289
+ const src = getBundledSkillPath(item.name);
1290
+ if (!existsSync3(src)) {
1291
+ throw new Error(`Skill bundleada no encontrada: ${src}. ¿Falta el directorio assets/skills/${item.name}/?`);
1292
+ }
1293
+ const treeHash = computeTreeHash(src);
1294
+ if (item.sha256 && treeHash !== item.sha256) {
1295
+ throw new Error(`Tree hash inválido para skill "${item.name}"
1296
+ ` + ` esperado: ${item.sha256}
1297
+ ` + ` recibido: ${treeHash}`);
1298
+ }
1299
+ const dest = join4(paths.skills, item.name);
1300
+ if (existsSync3(dest)) {
1301
+ rmSync(dest, { recursive: true, force: true });
1302
+ }
1303
+ copyDirRecursive(src, dest);
1304
+ upsertLockfile(paths, "skills", item, manifest, treeHash);
1305
+ }
1143
1306
  function isAgentInstalled(name, paths) {
1144
- return existsSync3(join3(paths.agents, `${name}.md`));
1307
+ return existsSync3(join4(paths.agents, `${name}.md`));
1145
1308
  }
1146
1309
  function isCommandInstalled(name, paths) {
1147
- return existsSync3(join3(paths.commands, `${name}.md`));
1310
+ return existsSync3(join4(paths.commands, `${name}.md`));
1311
+ }
1312
+ function isSkillInstalled(name, paths) {
1313
+ return existsSync3(join4(paths.skills, name, "SKILL.md"));
1148
1314
  }
1149
1315
  function uninstallAgent(name, paths) {
1150
- const filePath = join3(paths.agents, `${name}.md`);
1316
+ const filePath = join4(paths.agents, `${name}.md`);
1151
1317
  try {
1152
1318
  if (existsSync3(filePath)) {
1153
1319
  unlinkSync(filePath);
@@ -1159,7 +1325,7 @@ function uninstallAgent(name, paths) {
1159
1325
  return true;
1160
1326
  }
1161
1327
  function uninstallCommand(name, paths) {
1162
- const filePath = join3(paths.commands, `${name}.md`);
1328
+ const filePath = join4(paths.commands, `${name}.md`);
1163
1329
  try {
1164
1330
  if (existsSync3(filePath)) {
1165
1331
  unlinkSync(filePath);
@@ -1170,6 +1336,18 @@ function uninstallCommand(name, paths) {
1170
1336
  removeFromLockfile(paths.root, "commands", name);
1171
1337
  return true;
1172
1338
  }
1339
+ function uninstallSkill(name, paths) {
1340
+ const dirPath = join4(paths.skills, name);
1341
+ try {
1342
+ if (existsSync3(dirPath)) {
1343
+ rmSync(dirPath, { recursive: true, force: true });
1344
+ }
1345
+ } catch {
1346
+ return false;
1347
+ }
1348
+ removeFromLockfile(paths.root, "skills", name);
1349
+ return true;
1350
+ }
1173
1351
  function uninstallAll(paths) {
1174
1352
  const lockfile = readLockfile(paths.root);
1175
1353
  if (!lockfile)
@@ -1180,6 +1358,9 @@ function uninstallAll(paths) {
1180
1358
  for (const name of Object.keys(lockfile.commands)) {
1181
1359
  uninstallCommand(name, paths);
1182
1360
  }
1361
+ for (const name of Object.keys(lockfile.skills ?? {})) {
1362
+ uninstallSkill(name, paths);
1363
+ }
1183
1364
  clearLockfile(paths.root);
1184
1365
  }
1185
1366
 
@@ -1213,7 +1394,8 @@ function printPostInstallSteps() {
1213
1394
  "Recargá OpenCode",
1214
1395
  "Ejecutá /install-stack para instalar el stack",
1215
1396
  "Recargá OpenCode nuevamente",
1216
- "Usá @Ostacky para invocar al agente"
1397
+ "Usá @Ostacky para invocar al agente",
1398
+ "Las skills bundleadas viven en .opencode/skills/ — revisá cuáles activás"
1217
1399
  ].join(`
1218
1400
  → `), "Próximos pasos");
1219
1401
  }
@@ -1256,11 +1438,29 @@ function getUpdateCandidates(manifest, paths) {
1256
1438
  candidates.push({ type: "commands", item, installedVersion: installed });
1257
1439
  }
1258
1440
  }
1441
+ for (const item of manifest.skills ?? []) {
1442
+ if (!isSkillInstalled(item.name, paths))
1443
+ continue;
1444
+ const installed = getInstalledVersion(lockfile, "skills", item.name);
1445
+ if (installed !== item.version) {
1446
+ candidates.push({ type: "skills", item, installedVersion: installed });
1447
+ }
1448
+ }
1259
1449
  return candidates;
1260
1450
  }
1261
1451
  function formatVersionDiff(from, to) {
1262
1452
  return from ? `${from} → ${to}` : `(sin versión) → ${to}`;
1263
1453
  }
1454
+ function kindLabel(type) {
1455
+ switch (type) {
1456
+ case "agents":
1457
+ return "agente";
1458
+ case "commands":
1459
+ return "command";
1460
+ case "skills":
1461
+ return "skill";
1462
+ }
1463
+ }
1264
1464
  async function doInstallAll(manifest, paths) {
1265
1465
  const spin = L2();
1266
1466
  let errors = 0;
@@ -1284,6 +1484,16 @@ async function doInstallAll(manifest, paths) {
1284
1484
  errors++;
1285
1485
  }
1286
1486
  }
1487
+ for (const skill of manifest.skills ?? []) {
1488
+ spin.start(`Instalando skill: ${skill.name} (${skill.version})`);
1489
+ try {
1490
+ await installSkill(skill, manifest, paths);
1491
+ spin.stop(`Skill instalada: ${skill.name} (${skill.version})`);
1492
+ } catch (e2) {
1493
+ spin.stop(`Error en ${skill.name}: ${e2.message}`);
1494
+ errors++;
1495
+ }
1496
+ }
1287
1497
  if (errors === 0) {
1288
1498
  v2.success("Todo instalado correctamente.");
1289
1499
  } else {
@@ -1340,6 +1550,35 @@ async function doAddCommand(manifest, paths) {
1340
1550
  }
1341
1551
  }
1342
1552
  }
1553
+ async function doAddSkill(manifest, paths) {
1554
+ const lockfile = readLockfile(paths.root);
1555
+ const options = (manifest.skills ?? []).map((s) => {
1556
+ const installed = getInstalledVersion(lockfile, "skills", s.name);
1557
+ const hint = installed ? `v${installed} instalado — ${s.description}` : s.description;
1558
+ return { value: s.name, label: `${s.name} (v${s.version})`, hint };
1559
+ });
1560
+ if (options.length === 0) {
1561
+ v2.info("No hay skills disponibles en el manifest.");
1562
+ return;
1563
+ }
1564
+ const selected = await pe({
1565
+ message: "¿Qué skills deseas instalar?",
1566
+ options,
1567
+ required: true
1568
+ });
1569
+ onCancel(selected);
1570
+ const spin = L2();
1571
+ for (const name of selected) {
1572
+ const item = manifest.skills.find((s) => s.name === name);
1573
+ spin.start(`Instalando skill: ${name} (${item.version})`);
1574
+ try {
1575
+ await installSkill(item, manifest, paths);
1576
+ spin.stop(`Skill instalada: ${name} (${item.version})`);
1577
+ } catch (e2) {
1578
+ spin.stop(`Error: ${e2.message}`);
1579
+ }
1580
+ }
1581
+ }
1343
1582
  async function doUpdate(manifest, paths) {
1344
1583
  const candidates = getUpdateCandidates(manifest, paths);
1345
1584
  if (candidates.length === 0) {
@@ -1347,8 +1586,8 @@ async function doUpdate(manifest, paths) {
1347
1586
  return;
1348
1587
  }
1349
1588
  const diffLines = candidates.map(({ type, item, installedVersion }) => {
1350
- const kind = type === "agents" ? "agente" : "command";
1351
- return ` ${kind.padEnd(8)} ${item.name.padEnd(16)} ${formatVersionDiff(installedVersion, item.version)}`;
1589
+ const kind = kindLabel(type);
1590
+ return ` ${kind.padEnd(8)} ${item.name.padEnd(24)} ${formatVersionDiff(installedVersion, item.version)}`;
1352
1591
  });
1353
1592
  ye(diffLines.join(`
1354
1593
  `), "Actualizaciones disponibles");
@@ -1367,8 +1606,10 @@ async function doUpdate(manifest, paths) {
1367
1606
  try {
1368
1607
  if (type === "agents") {
1369
1608
  await installAgent(item, manifest, paths);
1370
- } else {
1609
+ } else if (type === "commands") {
1371
1610
  await installCommand(item, manifest, paths);
1611
+ } else {
1612
+ await installSkill(item, manifest, paths);
1372
1613
  }
1373
1614
  spin.stop(`Actualizado: ${item.name} (${item.version})`);
1374
1615
  updated++;
@@ -1392,6 +1633,7 @@ async function runInteractiveMenu() {
1392
1633
  { value: "all", label: "Instalar todo" },
1393
1634
  { value: "agent", label: "Instalar agente" },
1394
1635
  { value: "command", label: "Instalar command" },
1636
+ { value: "skill", label: "Instalar skill" },
1395
1637
  { value: "update", label: "Actualizar instalación" },
1396
1638
  { value: "uninstall", label: "Desinstalar" },
1397
1639
  { value: "exit", label: "Salir" }
@@ -1414,6 +1656,11 @@ async function runInteractiveMenu() {
1414
1656
  printPostInstallSteps();
1415
1657
  fe("Listo.");
1416
1658
  break;
1659
+ case "skill":
1660
+ await doAddSkill(manifest, paths);
1661
+ printPostInstallSteps();
1662
+ fe("Listo.");
1663
+ break;
1417
1664
  case "update": {
1418
1665
  const latestManifest = await loadLatestManifest();
1419
1666
  await doUpdate(latestManifest, paths);
@@ -1465,6 +1712,18 @@ async function runAddCommandCommand() {
1465
1712
  printPostInstallSteps();
1466
1713
  fe("Listo.");
1467
1714
  }
1715
+ async function runAddSkillCommand() {
1716
+ we(" OpenCode Installer ");
1717
+ const manifest = await loadManifest();
1718
+ const paths = await resolveOpenCodePaths();
1719
+ if (!paths) {
1720
+ fe("Cancelado.");
1721
+ return;
1722
+ }
1723
+ await doAddSkill(manifest, paths);
1724
+ printPostInstallSteps();
1725
+ fe("Listo.");
1726
+ }
1468
1727
  async function runUpdateCommand() {
1469
1728
  we(" OpenCode Installer ");
1470
1729
  const manifest = await loadLatestManifest();
@@ -1478,21 +1737,24 @@ async function runUpdateCommand() {
1478
1737
  }
1479
1738
  async function doUninstallTotal(paths) {
1480
1739
  const lockfile = readLockfile(paths.root);
1481
- if (!lockfile || Object.keys(lockfile.agents).length === 0 && Object.keys(lockfile.commands).length === 0) {
1740
+ if (!lockfile || Object.keys(lockfile.agents).length === 0 && Object.keys(lockfile.commands).length === 0 && Object.keys(lockfile.skills ?? {}).length === 0) {
1482
1741
  v2.info("No hay nada instalado.");
1483
1742
  return;
1484
1743
  }
1485
1744
  const pathsToDelete = [];
1486
1745
  for (const name of Object.keys(lockfile.agents)) {
1487
- pathsToDelete.push(join4(paths.agents, `${name}.md`));
1746
+ pathsToDelete.push(join5(paths.agents, `${name}.md`));
1488
1747
  }
1489
1748
  for (const name of Object.keys(lockfile.commands)) {
1490
- pathsToDelete.push(join4(paths.commands, `${name}.md`));
1749
+ pathsToDelete.push(join5(paths.commands, `${name}.md`));
1750
+ }
1751
+ for (const name of Object.keys(lockfile.skills ?? {})) {
1752
+ pathsToDelete.push(join5(paths.skills, name));
1491
1753
  }
1492
1754
  ye(pathsToDelete.join(`
1493
- `), `Se eliminarán ${pathsToDelete.length} archivo(s)`);
1755
+ `), `Se eliminarán ${pathsToDelete.length} archivo(s) / directorio(s)`);
1494
1756
  const confirm = await me({
1495
- message: `¿Confirmar desinstalación de ${pathsToDelete.length} archivo(s)?`
1757
+ message: `¿Confirmar desinstalación de ${pathsToDelete.length} item(s)?`
1496
1758
  });
1497
1759
  onCancel(confirm);
1498
1760
  if (!confirm) {
@@ -1500,7 +1762,7 @@ async function doUninstallTotal(paths) {
1500
1762
  return;
1501
1763
  }
1502
1764
  uninstallAll(paths);
1503
- v2.success(`${pathsToDelete.length} archivo(s) eliminado(s).`);
1765
+ v2.success(`${pathsToDelete.length} item(s) eliminado(s).`);
1504
1766
  }
1505
1767
  async function doUninstallAgent(paths) {
1506
1768
  const lockfile = readLockfile(paths.root);
@@ -1564,13 +1826,45 @@ async function doUninstallCommand(paths) {
1564
1826
  }
1565
1827
  }
1566
1828
  }
1829
+ async function doUninstallSkill(paths) {
1830
+ const lockfile = readLockfile(paths.root);
1831
+ if (!lockfile) {
1832
+ v2.warn("No hay instalación registrada.");
1833
+ return;
1834
+ }
1835
+ const installed = Object.keys(lockfile.skills ?? {});
1836
+ if (installed.length === 0) {
1837
+ v2.info("No hay skills instaladas.");
1838
+ return;
1839
+ }
1840
+ const options = installed.map((name) => ({
1841
+ value: name,
1842
+ label: name,
1843
+ hint: `v${lockfile.skills[name].version}`
1844
+ }));
1845
+ const selected = await pe({
1846
+ message: "¿Qué skills querés desinstalar?",
1847
+ options,
1848
+ required: true
1849
+ });
1850
+ onCancel(selected);
1851
+ for (const name of selected) {
1852
+ const ok = uninstallSkill(name, paths);
1853
+ if (ok) {
1854
+ v2.success(`Skill eliminada: ${name}`);
1855
+ } else {
1856
+ v2.warn(`No se pudo eliminar la skill: ${name}`);
1857
+ }
1858
+ }
1859
+ }
1567
1860
  async function doUninstall(paths) {
1568
1861
  const scope = await de({
1569
1862
  message: "¿Qué querés desinstalar?",
1570
1863
  options: [
1571
- { value: "all", label: "Todo (agentes + commands)" },
1864
+ { value: "all", label: "Todo (agentes + commands + skills)" },
1572
1865
  { value: "agent", label: "Solo un agente" },
1573
- { value: "command", label: "Solo un command" }
1866
+ { value: "command", label: "Solo un command" },
1867
+ { value: "skill", label: "Solo una skill" }
1574
1868
  ]
1575
1869
  });
1576
1870
  onCancel(scope);
@@ -1584,6 +1878,9 @@ async function doUninstall(paths) {
1584
1878
  case "command":
1585
1879
  await doUninstallCommand(paths);
1586
1880
  break;
1881
+ case "skill":
1882
+ await doUninstallSkill(paths);
1883
+ break;
1587
1884
  }
1588
1885
  }
1589
1886
  async function runUninstallCommand() {
@@ -1615,7 +1912,7 @@ async function runUninstallAgentCommand(name) {
1615
1912
  v2.warn(`${name} no está instalado.`);
1616
1913
  return;
1617
1914
  }
1618
- const filePath = join4(paths.agents, `${name}.md`);
1915
+ const filePath = join5(paths.agents, `${name}.md`);
1619
1916
  ye(filePath, `Se eliminará`);
1620
1917
  const confirm = await me({ message: `¿Borrar ${name}.md?` });
1621
1918
  onCancel(confirm);
@@ -1653,7 +1950,7 @@ async function runUninstallCommandCommand(name) {
1653
1950
  v2.warn(`${name} no está instalado.`);
1654
1951
  return;
1655
1952
  }
1656
- const filePath = join4(paths.commands, `${name}.md`);
1953
+ const filePath = join5(paths.commands, `${name}.md`);
1657
1954
  ye(filePath, `Se eliminará`);
1658
1955
  const confirm = await me({ message: `¿Borrar ${name}.md?` });
1659
1956
  onCancel(confirm);
@@ -1672,20 +1969,60 @@ async function runUninstallCommandCommand(name) {
1672
1969
  }
1673
1970
  fe("Listo.");
1674
1971
  }
1972
+ async function runUninstallSkillCommand(name) {
1973
+ we(" OpenCode Installer ");
1974
+ const paths = await resolveOpenCodePaths();
1975
+ if (!paths) {
1976
+ fe("Cancelado.");
1977
+ return;
1978
+ }
1979
+ if (name) {
1980
+ try {
1981
+ validateFilePath(name);
1982
+ } catch (e2) {
1983
+ v2.warn(`Nombre inválido: ${e2.message}`);
1984
+ return;
1985
+ }
1986
+ const lockfile = readLockfile(paths.root);
1987
+ if (!lockfile || !lockfile.skills || !(name in lockfile.skills)) {
1988
+ v2.warn(`${name} no está instalado.`);
1989
+ return;
1990
+ }
1991
+ const dirPath = join5(paths.skills, name);
1992
+ ye(dirPath, `Se eliminará`);
1993
+ const confirm = await me({ message: `¿Borrar la skill "${name}"?` });
1994
+ onCancel(confirm);
1995
+ if (!confirm) {
1996
+ v2.info("Cancelado.");
1997
+ return;
1998
+ }
1999
+ const ok = uninstallSkill(name, paths);
2000
+ if (ok) {
2001
+ v2.success(`Skill eliminada: ${name}`);
2002
+ } else {
2003
+ v2.warn(`No se pudo eliminar la skill: ${name}`);
2004
+ }
2005
+ } else {
2006
+ await doUninstallSkill(paths);
2007
+ }
2008
+ fe("Listo.");
2009
+ }
1675
2010
 
1676
2011
  // src/cli.ts
1677
2012
  var HELP = `
1678
- ostacky — Instalador de agentes y comandos para OpenCode
2013
+ ostacky — Instalador de agentes, comandos y skills para OpenCode
1679
2014
 
1680
2015
  Uso:
1681
2016
  npx ostacky Menú interactivo
1682
2017
  npx ostacky install Instalar todo
1683
2018
  npx ostacky add agent Agregar agente(s)
1684
2019
  npx ostacky add command Agregar command(s)
2020
+ npx ostacky add skill Agregar skill(s)
1685
2021
  npx ostacky update Actualizar instalación
1686
2022
  npx ostacky uninstall Desinstalar todo
1687
2023
  npx ostacky uninstall agent Desinstalar agente(s)
1688
2024
  npx ostacky uninstall command Desinstalar command(s)
2025
+ npx ostacky uninstall skill Desinstalar skill(s)
1689
2026
  npx ostacky --help Mostrar esta ayuda
1690
2027
  npx ostacky --version Mostrar versión
1691
2028
  `.trim();
@@ -1700,8 +2037,10 @@ async function main() {
1700
2037
  await runAddAgentCommand();
1701
2038
  } else if (subcmd === "command") {
1702
2039
  await runAddCommandCommand();
2040
+ } else if (subcmd === "skill") {
2041
+ await runAddSkillCommand();
1703
2042
  } else {
1704
- console.error(`Tipo desconocido: "${subcmd}". Usa 'agent' o 'command'.`);
2043
+ console.error(`Tipo desconocido: "${subcmd}". Usa 'agent', 'command' o 'skill'.`);
1705
2044
  process.exit(1);
1706
2045
  }
1707
2046
  break;
@@ -1715,10 +2054,13 @@ async function main() {
1715
2054
  } else if (subcmd === "command") {
1716
2055
  const name = process.argv[4];
1717
2056
  await runUninstallCommandCommand(name);
2057
+ } else if (subcmd === "skill") {
2058
+ const name = process.argv[4];
2059
+ await runUninstallSkillCommand(name);
1718
2060
  } else if (subcmd === undefined) {
1719
2061
  await runUninstallCommand();
1720
2062
  } else {
1721
- console.error(`Subcomando desconocido: "${subcmd}". Usa 'agent', 'command' o nada.`);
2063
+ console.error(`Subcomando desconocido: "${subcmd}". Usa 'agent', 'command', 'skill' o nada.`);
1722
2064
  process.exit(1);
1723
2065
  }
1724
2066
  break;