ostacky 0.7.1 → 0.7.3

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/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  var __create = Object.create;
3
4
  var __getProtoOf = Object.getPrototypeOf;
4
5
  var __defProp = Object.defineProperty;
@@ -44,6 +45,7 @@ var __export = (target, all) => {
44
45
  });
45
46
  };
46
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
47
49
 
48
50
  // node_modules/sisteransi/src/index.js
49
51
  var require_src = __commonJS((exports, module) => {
@@ -215,8 +217,9 @@ import {
215
217
  renameSync
216
218
  } from "fs";
217
219
  import { createHash as createHash2 } from "crypto";
218
- import { join, resolve, dirname, relative, basename } from "path";
220
+ import { join, resolve, dirname, relative, basename, win32 } from "path";
219
221
  import { execFileSync } from "child_process";
222
+ import { homedir } from "os";
220
223
  function findOpenCodeDir(startDir = process.cwd()) {
221
224
  let current = resolve(startDir);
222
225
  while (true) {
@@ -232,6 +235,11 @@ function findOpenCodeDir(startDir = process.cwd()) {
232
235
  }
233
236
  }
234
237
  function findProjectRoot(startDir = process.cwd()) {
238
+ try {
239
+ const out = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8", cwd: startDir, stdio: ["pipe", "pipe", "pipe"] }).trim();
240
+ if (out && existsSync(out))
241
+ return resolve(out);
242
+ } catch {}
235
243
  let current = resolve(startDir);
236
244
  while (true) {
237
245
  if (existsSync(join(current, ".opencode")) || existsSync(join(current, ".git"))) {
@@ -243,6 +251,32 @@ function findProjectRoot(startDir = process.cwd()) {
243
251
  current = parent;
244
252
  }
245
253
  }
254
+ function getGlobalOpenCodeDir(platform = process.platform, home = homedir()) {
255
+ if (platform === "win32") {
256
+ const appData = process.env.APPDATA ?? win32.join(home, "AppData", "Roaming");
257
+ return win32.join(appData, "opencode");
258
+ }
259
+ const xdg = process.env.XDG_CONFIG_HOME ?? join(home, ".config");
260
+ return join(xdg, "opencode");
261
+ }
262
+ function getOpenCodeDirForScope(scope, cwd = process.cwd()) {
263
+ if (scope === "global")
264
+ return getGlobalOpenCodeDir();
265
+ if (scope === "local") {
266
+ const existing2 = findOpenCodeDir(cwd);
267
+ if (existing2)
268
+ return existing2;
269
+ return join(findProjectRoot(cwd), ".opencode");
270
+ }
271
+ const existing = findOpenCodeDir(cwd);
272
+ if (existing)
273
+ return existing;
274
+ const root = findProjectRoot(cwd);
275
+ if (existsSync(join(root, ".opencode")) || existsSync(join(root, ".git"))) {
276
+ return join(root, ".opencode");
277
+ }
278
+ return getGlobalOpenCodeDir();
279
+ }
246
280
  function ensureOpenCodePaths(opencodeDir) {
247
281
  const paths = {
248
282
  root: opencodeDir,
@@ -266,9 +300,6 @@ function ensureToolDirs(toolsDir, toolNames) {
266
300
  mkdirSync(dir, { recursive: true });
267
301
  }
268
302
  }
269
- function createOpenCodeDir(baseDir) {
270
- return ensureOpenCodePaths(join(baseDir, ".opencode"));
271
- }
272
303
  function copyDirRecursive(src, dest, skipGenerated = false) {
273
304
  if (!existsSync(dest))
274
305
  mkdirSync(dest, { recursive: true });
@@ -451,7 +482,7 @@ function downloadToFile(url, dest, timeoutMs = 180000) {
451
482
  });
452
483
  }
453
484
  async function downloadAndExtract(url, destDir, stripComponents = 1, timeoutMs = 180000) {
454
- const tmp = join(dirname(destDir), `.${basename(destDir)}.download-${Date.now()}`);
485
+ const tmp = join(dirname(destDir), `.${basename(destDir)}.download-${Date.now()}-${process.pid}`);
455
486
  if (!existsSync(tmp))
456
487
  mkdirSync(tmp, { recursive: true });
457
488
  const archivePath = join(tmp, url.endsWith(".zip") ? "archive.zip" : "archive.tar.gz");
@@ -520,31 +551,31 @@ var init_fs = __esm(() => {
520
551
  var manifest_default;
521
552
  var init_manifest = __esm(() => {
522
553
  manifest_default = {
523
- version: "0.7.1",
554
+ version: "0.7.3",
524
555
  repo: "JaimeHoracio/Ostacky",
525
- tag: "v0.7.1",
556
+ tag: "v0.7.3",
526
557
  agents: [
527
558
  {
528
559
  name: "ostacky",
529
560
  file: "assets/agents/ostacky.md",
530
- description: "Orquestador con recuperación automática (nunca se congela), ruteo por nivel de impacto, controller MCP con SDK oficial, edición segura con fallback inline, y delegación en OpenSpec + Superpowers. v0.7.1: prune de skills obsoletas, lastHandoff, getAvailableTransitions, consecutiveFailures real, mem_session_summary automático al cierre.",
531
- version: "0.7.1",
532
- sha256: "f833e69b6eaa3dfe24de9addf6455b3da55f829b51cba3c7c90c65c69cda34c2"
561
+ description: "Orquestador con recuperación automática (nunca se congela), ruteo por nivel de impacto, controller MCP con SDK oficial, edición segura con fallback inline, y delegación en OpenSpec + Superpowers. v0.7.3: prune de skills obsoletas, lastHandoff, getAvailableTransitions, consecutiveFailures real, mem_session_summary automático al cierre.",
562
+ version: "0.7.3",
563
+ sha256: "b6dc55a9571f9626e7912ac493cc3bfe6b0798a597f706904b2106a4a9acfcd5"
533
564
  }
534
565
  ],
535
566
  commands: [
536
567
  {
537
568
  name: "install-stack",
538
569
  file: "assets/commands/install-stack.md",
539
- description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, Context7, controller MCP con SDK oficial). v0.7.1: Paso 1.5 controller expandido con verificación post-instalación y troubleshooting.",
540
- version: "0.7.1",
541
- sha256: "ef4244855cf733dfe50a2a8e7c436c5741f92574cc50f0fca49faed60480e013"
570
+ description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, Context7, controller MCP con SDK oficial). v0.7.3: Paso 1.5 controller expandido con verificación post-instalación y troubleshooting.",
571
+ version: "0.7.3",
572
+ sha256: "0d4bc0938110c40633b041847148d8212e34a5b5876752825cfd6e186c328b8b"
542
573
  },
543
574
  {
544
575
  name: "opsx-sync",
545
576
  file: "assets/commands/opsx-sync.md",
546
577
  description: "Sincroniza delta specs del change activo sin inicializar CodeGraph si ya existe índice",
547
- version: "0.7.1",
578
+ version: "0.7.3",
548
579
  sha256: "fe0158478f2ca63b315037a85fc1632b77868532e319af6c7384285441767d64"
549
580
  }
550
581
  ],
@@ -552,15 +583,15 @@ var init_manifest = __esm(() => {
552
583
  {
553
584
  name: "ostacky-controller",
554
585
  file: "assets/mcp/ostacky-controller/",
555
- description: "Máquina de estados persistida con @modelcontextprotocol/server SDK. v0.7.1: 22 tools (incluye set_handoff, get_handoff, clear_handoff, get_available_transitions funcional). Bugfixes: consecutiveFailures real, lastHandoff state, defaultChoice persistido. Robustez: degraded mode automático tras 3 fallos, persistence condicional para Nivel 0, prune de skills obsoletas.",
556
- version: "0.7.1",
557
- sha256: "0e639fb1d5bedf8b93b4e259782b9b7de68cd4bb89875e2c546279d17131b7b7"
586
+ description: "Máquina de estados persistida con @modelcontextprotocol/server SDK. v0.7.3: 22 tools (incluye set_handoff, get_handoff, clear_handoff, get_available_transitions funcional). Bugfixes: consecutiveFailures real, lastHandoff state, defaultChoice persistido. Robustez: degraded mode automático tras 3 fallos, persistence condicional para Nivel 0, prune de skills obsoletas.",
587
+ version: "0.7.3",
588
+ sha256: "d723afc107c8a36721e7f04dc1c16694a617cd562492e1ece362482c9023b945"
558
589
  },
559
590
  {
560
591
  name: "openspec",
561
592
  file: "assets/mcp/openspec/",
562
593
  description: "MCP server local para OpenSpec - proposal, apply, archive, sync de cambios",
563
- version: "0.7.1",
594
+ version: "0.7.3",
564
595
  sha256: "fa4be28bfc1e75db8f1a037e2580f04a60066c80e8e5934e90e1041020d04609"
565
596
  }
566
597
  ],
@@ -569,106 +600,106 @@ var init_manifest = __esm(() => {
569
600
  name: "brainstorming",
570
601
  file: "assets/skills/brainstorming/SKILL.md",
571
602
  description: "Skill unificado de pensamiento con dos modos: creative-design (producción de diseño → transición a implementación directa o openspec-propose) y open-exploration (exploración libre)",
572
- version: "0.7.1",
603
+ version: "0.7.3",
573
604
  sha256: "333810ca63450e8ab795e8c5a58e645101e788d79210ca8ac36e10d11be39585"
574
605
  },
575
606
  {
576
607
  name: "execution-mode-evaluation",
577
608
  file: "assets/skills/execution-mode-evaluation/SKILL.md",
578
609
  description: "Skill de análisis de modo de ejecución — output reconciliado con controller snapshot contract (recommendation field)",
579
- version: "0.7.1",
580
- sha256: "edf6fca0af52cce83f8fd08e7db9b3d79cc130b53fca505e90d3fbaf4909338b"
610
+ version: "0.7.3",
611
+ sha256: "e17f1e0309572841dc1fa537b1f32c7468eb70b3acf453b9ff5cc4fa8d94fd39"
581
612
  },
582
613
  {
583
614
  name: "tdd",
584
615
  file: "assets/skills/tdd/SKILL.md",
585
616
  description: "Skill de test-driven development (Superpowers)",
586
- version: "0.7.1",
617
+ version: "0.7.3",
587
618
  sha256: "aa412298980b7826165c211145c1b8e9135f68f36247bb3cb96d0a0eae274486"
588
619
  },
589
620
  {
590
621
  name: "subagent-driven-development",
591
622
  file: "assets/skills/subagent-driven-development/SKILL.md",
592
623
  description: "Skill de ejecución con subagentes (Superpowers) — ejecuta solo después de confirmación del coordinador Ostacky",
593
- version: "0.7.1",
624
+ version: "0.7.3",
594
625
  sha256: "1a42d714a9a13faf05f0bf7b580e8d837c2e77a5633839542ccce603023419e8"
595
626
  },
596
627
  {
597
628
  name: "dispatching-parallel-agents",
598
629
  file: "assets/skills/dispatching-parallel-agents/SKILL.md",
599
630
  description: "Skill de dispatch paralelo de agentes (Superpowers)",
600
- version: "0.7.1",
631
+ version: "0.7.3",
601
632
  sha256: "3c8a66d51ae2e719e877d02c3dac32bd804722c3bb9227c258ecae78252a20ab"
602
633
  },
603
634
  {
604
635
  name: "review",
605
636
  file: "assets/skills/review/SKILL.md",
606
637
  description: "Skill de revisión de código (Superpowers)",
607
- version: "0.7.1",
638
+ version: "0.7.3",
608
639
  sha256: "b19650ed4d1d4d9857a4dd5b7e08e91328a1a1fc9c45bcc2fb600fa9d0213279"
609
640
  },
610
641
  {
611
642
  name: "receiving-code-review",
612
643
  file: "assets/skills/receiving-code-review/SKILL.md",
613
644
  description: "Skill de recibir y procesar feedback de code review",
614
- version: "0.7.1",
645
+ version: "0.7.3",
615
646
  sha256: "d761e884e71d8d3476ac734d287cae403a4024506a7d12e7361a448217c0a831"
616
647
  },
617
648
  {
618
649
  name: "openspec-propose",
619
650
  file: "assets/skills/openspec-propose/SKILL.md",
620
651
  description: "Skill de generación de proposal (OpenSpec)",
621
- version: "0.7.1",
652
+ version: "0.7.3",
622
653
  sha256: "bb59100b9fd3c9f9a1ec6509fa975ec55bf7fc737df4fe73bb3e9bbc217b83f7"
623
654
  },
624
655
  {
625
656
  name: "openspec-apply-change",
626
657
  file: "assets/skills/openspec-apply-change/SKILL.md",
627
658
  description: "Skill de aplicación de change (OpenSpec)",
628
- version: "0.7.1",
659
+ version: "0.7.3",
629
660
  sha256: "dfc823bf89fc7505e91ab6dee9c1f004be38a410bd3d88fb67b211b1b6cbb1d0"
630
661
  },
631
662
  {
632
663
  name: "openspec-archive-change",
633
664
  file: "assets/skills/openspec-archive-change/SKILL.md",
634
665
  description: "Skill de archivo de change (OpenSpec)",
635
- version: "0.7.1",
666
+ version: "0.7.3",
636
667
  sha256: "16e4b561de7747283663fed602e506abf502452c49a8d4b86d7a5539c40f0195"
637
668
  },
638
669
  {
639
670
  name: "openspec-explore",
640
671
  file: "assets/skills/openspec-explore/SKILL.md",
641
672
  description: "Modo explore para OpenSpec — thinking partner para explorar ideas, investigar problemas y clarificar requisitos antes/durante un cambio",
642
- version: "0.7.1",
673
+ version: "0.7.3",
643
674
  sha256: "37ae4aaf17ea71a395bab6dc4d9d61b9a49d7910f6692535ae8553244c46880b"
644
675
  },
645
676
  {
646
677
  name: "using-git-worktrees",
647
678
  file: "assets/skills/using-git-worktrees/SKILL.md",
648
679
  description: "Skill de uso de git worktrees para aislamiento de trabajo",
649
- version: "0.7.1",
650
- sha256: "11afa9123828fc5caa22137aff907cd99c751e99027fbd4c020751110a949281"
680
+ version: "0.7.3",
681
+ sha256: "93341bc1b7c053618a8b6dc07e3615b77990d7b549a0201f5835d95fef67ce13"
651
682
  },
652
683
  {
653
684
  name: "using-superpowers",
654
685
  file: "assets/skills/using-superpowers/SKILL.md",
655
686
  description: "Skill de orquestación de Superpowers skills",
656
- version: "0.7.1",
687
+ version: "0.7.3",
657
688
  sha256: "7e54536f96d2a379185a10bfc1e970850caa2561b6d8aca7080f0defea0381a4"
658
689
  },
659
690
  {
660
691
  name: "writing-skills",
661
692
  file: "assets/skills/writing-skills/SKILL.md",
662
693
  description: "Skill de creación y edición de skills",
663
- version: "0.7.1",
694
+ version: "0.7.3",
664
695
  sha256: "3d76b906cee518a2b809febb70db95697a35b9f368986bb504b4120c3bfb437a"
665
696
  },
666
697
  {
667
698
  name: "graceful-degradation",
668
699
  file: "assets/skills/graceful-degradation/SKILL.md",
669
700
  description: "Skill de degradación graceful cuando múltiples tools están indisponibles",
670
- version: "0.7.1",
671
- sha256: "e5090e475729f21871bb25bce738000f3616955ed3edb3ba926dd740d3298948"
701
+ version: "0.7.3",
702
+ sha256: "630b87c16d78212c865696ca5df6abb0834606095ffe0e80cff8e04350f01054"
672
703
  }
673
704
  ]
674
705
  };
@@ -1468,7 +1499,7 @@ var init_stack = __esm(() => {
1468
1499
  // package.json
1469
1500
  var package_default = {
1470
1501
  name: "ostacky",
1471
- version: "0.7.1",
1502
+ version: "0.7.3",
1472
1503
  description: "Instalador interactivo de agentes y comandos para OpenCode",
1473
1504
  type: "module",
1474
1505
  bin: {
@@ -2546,6 +2577,7 @@ async function probeMcpServer(nodeExecutable, serverPath, cwd, statePath, option
2546
2577
  let stderr = "";
2547
2578
  let stdoutBuffer = "";
2548
2579
  const requestTimeout = 1e4;
2580
+ let exitTimeout;
2549
2581
  const finish = (error) => {
2550
2582
  if (settled)
2551
2583
  return;
@@ -2554,9 +2586,18 @@ async function probeMcpServer(nodeExecutable, serverPath, cwd, statePath, option
2554
2586
  try {
2555
2587
  child.stdin?.end();
2556
2588
  } catch {}
2557
- if (child.exitCode === null)
2589
+ if (child.exitCode === null) {
2590
+ const onExit = () => {
2591
+ if (exitTimeout)
2592
+ clearTimeout(exitTimeout);
2593
+ error ? reject(error) : resolve2();
2594
+ };
2595
+ child.once("exit", onExit);
2558
2596
  child.kill();
2559
- error ? reject(error) : resolve2();
2597
+ exitTimeout = setTimeout(onExit, 500);
2598
+ } else {
2599
+ error ? reject(error) : resolve2();
2600
+ }
2560
2601
  };
2561
2602
  const fail = (message) => finish(new Error(`${message}${stderr ? `: ${stderr.trim()}` : ""}`));
2562
2603
  const send = (message) => {
@@ -2633,15 +2674,35 @@ async function probeMcpServer(nodeExecutable, serverPath, cwd, statePath, option
2633
2674
  params: {
2634
2675
  protocolVersion: "2025-03-26",
2635
2676
  capabilities: {},
2636
- clientInfo: { name: "ostacky-installer", version: "0.7.1" }
2677
+ clientInfo: { name: "ostacky-installer", version: "0.7.3" }
2637
2678
  }
2638
2679
  });
2639
2680
  });
2640
2681
  } finally {
2641
- if (statePath && existsSync5(statePath))
2642
- rmSync3(statePath, { force: true });
2643
- if (statePath && existsSync5(statePath + ".backup"))
2644
- rmSync3(statePath + ".backup", { force: true });
2682
+ await new Promise((r2) => setTimeout(r2, 200));
2683
+ if (statePath) {
2684
+ for (const p2 of [
2685
+ statePath,
2686
+ statePath + ".backup",
2687
+ statePath + ".lock.pid",
2688
+ statePath + ".lock.timestamp",
2689
+ statePath + ".tmp." + process.pid
2690
+ ]) {
2691
+ try {
2692
+ if (existsSync5(p2))
2693
+ rmSync3(p2, { force: true });
2694
+ } catch {}
2695
+ }
2696
+ try {
2697
+ const { dirname: dirname5, join: join7 } = await import("path");
2698
+ const handoff = join7(dirname5(statePath), ".ostacky-handoff-compaction.json");
2699
+ if (existsSync5(handoff)) {
2700
+ try {
2701
+ rmSync3(handoff, { force: true });
2702
+ } catch {}
2703
+ }
2704
+ } catch {}
2705
+ }
2645
2706
  }
2646
2707
  }
2647
2708
  function takeFileSnapshot(path) {
@@ -2723,8 +2784,8 @@ async function installSkill(item, manifest, paths) {
2723
2784
  const treeHash = computeTreeHash(src);
2724
2785
  if (item.sha256 && treeHash !== item.sha256) {
2725
2786
  throw new Error(`Tree hash inválido para skill "${item.name}"
2726
- ` + ` esperado: ${item.sha256}
2727
- ` + ` recibido: ${treeHash}`);
2787
+ esperado: ${item.sha256}
2788
+ recibido: ${treeHash}`);
2728
2789
  }
2729
2790
  const dest = join6(paths.skills, item.name);
2730
2791
  if (existsSync5(dest)) {
@@ -2764,8 +2825,8 @@ async function installMcpServer(item, manifest, paths) {
2764
2825
  const treeHash = computeTreeHash(src);
2765
2826
  if (item.sha256 && treeHash !== item.sha256) {
2766
2827
  throw new Error(`Tree hash inválido para MCP server "${item.name}"
2767
- ` + ` esperado: ${item.sha256}
2768
- ` + ` recibido: ${treeHash}`);
2828
+ esperado: ${item.sha256}
2829
+ recibido: ${treeHash}`);
2769
2830
  }
2770
2831
  const nodeExecutable = getVerifiedNodeExecutable();
2771
2832
  const projectRoot = dirname4(paths.root);
@@ -2969,25 +3030,66 @@ function printPostInstallSteps() {
2969
3030
  ].join(`
2970
3031
  `), "Próximos pasos");
2971
3032
  }
2972
- async function resolveOpenCodePaths() {
2973
- const existing = findOpenCodeDir();
2974
- if (existing) {
2975
- const paths2 = ensureOpenCodePaths(existing);
2976
- ye(existing, "Directorio .opencode encontrado");
2977
- return paths2;
3033
+ async function resolveOpenCodePaths(scope) {
3034
+ if (scope === "local" || scope === "global" || scope === "auto") {
3035
+ const dir2 = getOpenCodeDirForScope(scope);
3036
+ const isGlobalDir = dir2.replace(/\\/g, "/") === getGlobalOpenCodeDir().replace(/\\/g, "/");
3037
+ try {
3038
+ const paths = ensureOpenCodePaths(dir2);
3039
+ if (scope === "global")
3040
+ ye(dir2, "Instalación global");
3041
+ else if (scope === "auto")
3042
+ ye(dir2, `Scope auto → ${isGlobalDir ? "global" : "local"}`);
3043
+ else
3044
+ ye(dir2, "Instalación local");
3045
+ return paths;
3046
+ } catch (e2) {
3047
+ const msg = e2.message ?? "";
3048
+ if ((scope === "global" || scope === "auto" && isGlobalDir) && (msg.includes("EACCES") || msg.toLowerCase().includes("permission"))) {
3049
+ v2.warn(`No se pudo escribir en global (${dir2}): ${msg}. ¿Instalar local?`);
3050
+ const retry = await me({ message: "¿Reintentar como instalación local?" });
3051
+ onCancel(retry);
3052
+ if (retry) {
3053
+ const localDir2 = getOpenCodeDirForScope("local");
3054
+ return ensureOpenCodePaths(localDir2);
3055
+ }
3056
+ }
3057
+ throw e2;
3058
+ }
2978
3059
  }
2979
- const projectRoot = findProjectRoot();
2980
- ye(`No se encontró .opencode
2981
- Raíz detectada: ${projectRoot}`, "Aviso");
2982
- const create = await me({
2983
- message: "¿Deseas crear la estructura .opencode aquí?"
3060
+ const cwd = process.cwd();
3061
+ const localDir = getOpenCodeDirForScope("local", cwd);
3062
+ const globalDir = getGlobalOpenCodeDir();
3063
+ const hasLocal = !!findOpenCodeDir(cwd);
3064
+ const scopeChoice = await de({
3065
+ message: `¿Instalar en proyecto local (${localDir}) o global (${globalDir})?`,
3066
+ options: [
3067
+ { value: "local", label: "Local", hint: `${localDir} (recomendado)` },
3068
+ { value: "global", label: "Global", hint: globalDir }
3069
+ ],
3070
+ initialValue: "local"
2984
3071
  });
2985
- onCancel(create);
2986
- if (!create)
2987
- return null;
2988
- const paths = createOpenCodeDir(projectRoot);
2989
- v2.success(`Estructura creada en ${paths.root}`);
2990
- return paths;
3072
+ onCancel(scopeChoice);
3073
+ const chosen = scopeChoice;
3074
+ const dir = getOpenCodeDirForScope(chosen, cwd);
3075
+ try {
3076
+ return ensureOpenCodePaths(dir);
3077
+ } catch (e2) {
3078
+ const msg = e2.message ?? "";
3079
+ if (chosen === "global" && (msg.includes("EACCES") || msg.includes("permission"))) {
3080
+ v2.warn(`No se pudo escribir en global (${dir}): ${msg}.`);
3081
+ const retry = await me({ message: "¿Instalar local en su lugar?" });
3082
+ onCancel(retry);
3083
+ if (retry)
3084
+ return ensureOpenCodePaths(getOpenCodeDirForScope("local", cwd));
3085
+ }
3086
+ throw e2;
3087
+ }
3088
+ }
3089
+ function isGlobalScope(paths) {
3090
+ const globalDir = getGlobalOpenCodeDir().replace(/\\/g, "/");
3091
+ const root = paths.root.replace(/\\/g, "/");
3092
+ return root === globalDir || root.startsWith(globalDir + "/");
2991
3093
  }
2992
3094
  function getOrphanedItems(manifest, paths) {
2993
3095
  const lockfile = readLockfile(paths.root);
@@ -3105,7 +3207,13 @@ async function doInstallStack(toolsDir, projectRoot) {
3105
3207
  async function doInstallAll(manifest, paths) {
3106
3208
  const spin = L2();
3107
3209
  let errors = 0;
3108
- ensureToolDirs(paths.tools, ["codegraph", "engram", "context7"]);
3210
+ const isGlobal = isGlobalScope(paths);
3211
+ if (!isGlobal) {
3212
+ ensureToolDirs(paths.tools, ["codegraph", "engram", "context7"]);
3213
+ } else {
3214
+ v2.info("Scope global detectado: el stack (CodeGraph/Engram) permanece siempre en <proyecto>/.opencode/tools — se omite instalación de stack global.");
3215
+ v2.info("Para instalar el stack, ejecutá 'npx ostacky install-stack --scope local' dentro de cada proyecto.");
3216
+ }
3109
3217
  for (const agent of manifest.agents) {
3110
3218
  spin.start(`Descargando agente: ${agent.name} (${agent.version})`);
3111
3219
  try {
@@ -3150,17 +3258,22 @@ async function doInstallAll(manifest, paths) {
3150
3258
  errors++;
3151
3259
  }
3152
3260
  }
3153
- v2.info("Instalando stack de herramientas...");
3154
- const stackOk = await doInstallStack(paths.tools, dirname6(paths.root));
3155
- if (!stackOk)
3156
- errors++;
3157
- const missingTools = [];
3158
- const codegraphDir = join8(paths.tools, "codegraph");
3159
- const engramDir = join8(paths.tools, "engram");
3160
- if (!existsSync7(codegraphDir) || !findBinaryInDir(codegraphDir, "codegraph"))
3161
- missingTools.push("CodeGraph");
3162
- if (!existsSync7(engramDir) || !findBinaryInDir(engramDir, "engram"))
3163
- missingTools.push("Engram");
3261
+ let stackOk = true;
3262
+ let missingTools = [];
3263
+ if (isGlobal) {
3264
+ stackOk = true;
3265
+ } else {
3266
+ v2.info("Instalando stack de herramientas...");
3267
+ stackOk = await doInstallStack(paths.tools, dirname6(paths.root));
3268
+ if (!stackOk)
3269
+ errors++;
3270
+ const codegraphDir = join8(paths.tools, "codegraph");
3271
+ const engramDir = join8(paths.tools, "engram");
3272
+ if (!existsSync7(codegraphDir) || !findBinaryInDir(codegraphDir, "codegraph"))
3273
+ missingTools.push("CodeGraph");
3274
+ if (!existsSync7(engramDir) || !findBinaryInDir(engramDir, "engram"))
3275
+ missingTools.push("Engram");
3276
+ }
3164
3277
  if (missingTools.length > 0) {
3165
3278
  v2.warn(`Faltan herramientas del stack: ${missingTools.join(", ")}.
3166
3279
  ` + "Ejecutá `/install-stack` desde el agente para instalarlas manualmente.");
@@ -3675,10 +3788,10 @@ async function doUninstallMcpByName(name, paths) {
3675
3788
  }
3676
3789
 
3677
3790
  // src/prompts/index.ts
3678
- async function runInteractiveMenu() {
3791
+ async function runInteractiveMenu(scope) {
3679
3792
  we(" OpenCode Installer ");
3680
3793
  const manifest = await loadManifest();
3681
- const paths = await resolveOpenCodePaths();
3794
+ const paths = await resolveOpenCodePaths(scope ?? null);
3682
3795
  if (!paths) {
3683
3796
  fe("Instalación cancelada.");
3684
3797
  return;
@@ -3726,6 +3839,12 @@ async function runInteractiveMenu() {
3726
3839
  fe("Listo.");
3727
3840
  break;
3728
3841
  case "stack": {
3842
+ if (isGlobalScope(paths)) {
3843
+ v2.error("install-stack requiere scope local; el stack vive en <proyecto>/.opencode/tools");
3844
+ v2.info(`Elegiste global (${paths.root}) — el stack debe instalarse por proyecto local.`);
3845
+ fe("Cancelado.");
3846
+ break;
3847
+ }
3729
3848
  ensureToolDirs(paths.tools, ["codegraph", "engram", "context7"]);
3730
3849
  const stackOk = await doInstallStack(paths.tools, dirname7(paths.root));
3731
3850
  if (!stackOk)
@@ -3748,10 +3867,10 @@ async function runInteractiveMenu() {
3748
3867
  break;
3749
3868
  }
3750
3869
  }
3751
- async function runInstallCommand() {
3870
+ async function runInstallCommand(scope) {
3752
3871
  we(" OpenCode Installer ");
3753
3872
  const manifest = await loadManifest();
3754
- const paths = await resolveOpenCodePaths();
3873
+ const paths = await resolveOpenCodePaths(scope ?? null);
3755
3874
  if (!paths) {
3756
3875
  fe("Cancelado.");
3757
3876
  return;
@@ -3761,10 +3880,10 @@ async function runInstallCommand() {
3761
3880
  printPostInstallSteps();
3762
3881
  fe(process.exitCode ? "Instalación parcial." : "Instalación completada.");
3763
3882
  }
3764
- async function runAddAgentCommand() {
3883
+ async function runAddAgentCommand(scope) {
3765
3884
  we(" OpenCode Installer ");
3766
3885
  const manifest = await loadManifest();
3767
- const paths = await resolveOpenCodePaths();
3886
+ const paths = await resolveOpenCodePaths(scope ?? null);
3768
3887
  if (!paths) {
3769
3888
  fe("Cancelado.");
3770
3889
  return;
@@ -3773,10 +3892,10 @@ async function runAddAgentCommand() {
3773
3892
  printPostInstallSteps();
3774
3893
  fe("Listo.");
3775
3894
  }
3776
- async function runAddCommandCommand() {
3895
+ async function runAddCommandCommand(scope) {
3777
3896
  we(" OpenCode Installer ");
3778
3897
  const manifest = await loadManifest();
3779
- const paths = await resolveOpenCodePaths();
3898
+ const paths = await resolveOpenCodePaths(scope ?? null);
3780
3899
  if (!paths) {
3781
3900
  fe("Cancelado.");
3782
3901
  return;
@@ -3785,10 +3904,10 @@ async function runAddCommandCommand() {
3785
3904
  printPostInstallSteps();
3786
3905
  fe("Listo.");
3787
3906
  }
3788
- async function runAddSkillCommand() {
3907
+ async function runAddSkillCommand(scope) {
3789
3908
  we(" OpenCode Installer ");
3790
3909
  const manifest = await loadManifest();
3791
- const paths = await resolveOpenCodePaths();
3910
+ const paths = await resolveOpenCodePaths(scope ?? null);
3792
3911
  if (!paths) {
3793
3912
  fe("Cancelado.");
3794
3913
  return;
@@ -3797,10 +3916,10 @@ async function runAddSkillCommand() {
3797
3916
  printPostInstallSteps();
3798
3917
  fe("Listo.");
3799
3918
  }
3800
- async function runAddMcpCommand() {
3919
+ async function runAddMcpCommand(scope) {
3801
3920
  we(" OpenCode Installer ");
3802
3921
  const manifest = await loadManifest();
3803
- const paths = await resolveOpenCodePaths();
3922
+ const paths = await resolveOpenCodePaths(scope ?? null);
3804
3923
  if (!paths) {
3805
3924
  fe("Cancelado.");
3806
3925
  return;
@@ -3809,22 +3928,35 @@ async function runAddMcpCommand() {
3809
3928
  printPostInstallSteps();
3810
3929
  fe("Listo.");
3811
3930
  }
3812
- async function runInstallStackCommand() {
3931
+ async function runInstallStackCommand(scope) {
3932
+ if (scope === "global") {
3933
+ v2.error("install-stack requiere scope local; el stack vive en <proyecto>/.opencode/tools");
3934
+ fe("Usá: npx ostacky install-stack --scope local");
3935
+ process.exitCode = 1;
3936
+ return;
3937
+ }
3813
3938
  we(" OpenCode Installer — Stack ");
3814
- const paths = await resolveOpenCodePaths();
3939
+ const paths = await resolveOpenCodePaths(scope ?? null);
3815
3940
  if (!paths) {
3816
3941
  fe("Cancelado.");
3817
3942
  return;
3818
3943
  }
3944
+ if (isGlobalScope(paths)) {
3945
+ v2.error("install-stack requiere scope local; el stack vive en <proyecto>/.opencode/tools");
3946
+ v2.info(`Scope resuelto a global (${paths.root}) — el stack debe instalarse por proyecto local.`);
3947
+ fe("Cancelado.");
3948
+ process.exitCode = 1;
3949
+ return;
3950
+ }
3819
3951
  ensureToolDirs(paths.tools, ["codegraph", "engram", "context7"]);
3820
3952
  const stackOk = await doInstallStack(paths.tools, dirname7(paths.root));
3821
3953
  if (!stackOk)
3822
3954
  process.exitCode = 1;
3823
3955
  fe(stackOk ? "Stack instalado." : "Stack instalado parcialmente.");
3824
3956
  }
3825
- async function runUninstallStackCommand() {
3957
+ async function runUninstallStackCommand(scope) {
3826
3958
  we(" OpenCode Installer — Stack ");
3827
- const paths = await resolveOpenCodePaths();
3959
+ const paths = await resolveOpenCodePaths(scope ?? null);
3828
3960
  if (!paths) {
3829
3961
  fe("Cancelado.");
3830
3962
  return;
@@ -3846,10 +3978,10 @@ async function runUninstallStackCommand() {
3846
3978
  }
3847
3979
  fe("Stack desinstalado.");
3848
3980
  }
3849
- async function runUpdateCommand() {
3981
+ async function runUpdateCommand(scope) {
3850
3982
  we(" OpenCode Installer ");
3851
3983
  const manifest = await loadLatestManifest();
3852
- const paths = await resolveOpenCodePaths();
3984
+ const paths = await resolveOpenCodePaths(scope ?? null);
3853
3985
  if (!paths) {
3854
3986
  fe("Cancelado.");
3855
3987
  return;
@@ -3857,9 +3989,9 @@ async function runUpdateCommand() {
3857
3989
  await doUpdate(manifest, paths);
3858
3990
  fe("Actualización completada.");
3859
3991
  }
3860
- async function runUninstallCommand() {
3992
+ async function runUninstallCommand(scope) {
3861
3993
  we(" OpenCode Installer ");
3862
- const paths = await resolveOpenCodePaths();
3994
+ const paths = await resolveOpenCodePaths(scope ?? null);
3863
3995
  if (!paths) {
3864
3996
  fe("Cancelado.");
3865
3997
  return;
@@ -3867,9 +3999,9 @@ async function runUninstallCommand() {
3867
3999
  await doUninstall(paths);
3868
4000
  fe("Desinstalación completada.");
3869
4001
  }
3870
- async function runUninstallAgentCommand(name) {
4002
+ async function runUninstallAgentCommand(name, scope) {
3871
4003
  we(" OpenCode Installer ");
3872
- const paths = await resolveOpenCodePaths();
4004
+ const paths = await resolveOpenCodePaths(scope ?? null);
3873
4005
  if (!paths) {
3874
4006
  fe("Cancelado.");
3875
4007
  return;
@@ -3902,9 +4034,9 @@ async function runUninstallAgentCommand(name) {
3902
4034
  }
3903
4035
  fe("Listo.");
3904
4036
  }
3905
- async function runUninstallCommandCommand(name) {
4037
+ async function runUninstallCommandCommand(name, scope) {
3906
4038
  we(" OpenCode Installer ");
3907
- const paths = await resolveOpenCodePaths();
4039
+ const paths = await resolveOpenCodePaths(scope ?? null);
3908
4040
  if (!paths) {
3909
4041
  fe("Cancelado.");
3910
4042
  return;
@@ -3937,9 +4069,9 @@ async function runUninstallCommandCommand(name) {
3937
4069
  }
3938
4070
  fe("Listo.");
3939
4071
  }
3940
- async function runUninstallSkillCommand(name) {
4072
+ async function runUninstallSkillCommand(name, scope) {
3941
4073
  we(" OpenCode Installer ");
3942
- const paths = await resolveOpenCodePaths();
4074
+ const paths = await resolveOpenCodePaths(scope ?? null);
3943
4075
  if (!paths) {
3944
4076
  fe("Cancelado.");
3945
4077
  return;
@@ -3972,9 +4104,9 @@ async function runUninstallSkillCommand(name) {
3972
4104
  }
3973
4105
  fe("Listo.");
3974
4106
  }
3975
- async function runUninstallMcpCommand(name) {
4107
+ async function runUninstallMcpCommand(name, scope) {
3976
4108
  we(" OpenCode Installer ");
3977
- const paths = await resolveOpenCodePaths();
4109
+ const paths = await resolveOpenCodePaths(scope ?? null);
3978
4110
  if (!paths) {
3979
4111
  fe("Cancelado.");
3980
4112
  return;
@@ -4009,76 +4141,285 @@ async function runUninstallMcpCommand(name) {
4009
4141
  }
4010
4142
 
4011
4143
  // src/cli.ts
4144
+ init_fs();
4145
+ import { existsSync as existsSync8, statSync as statSync3, readFileSync as readFileSync6 } from "node:fs";
4146
+ import { join as join11, dirname as dirname8 } from "node:path";
4012
4147
  var HELP = `
4013
4148
  ostacky — Instalador de agentes, comandos, skills y MCPs para OpenCode
4014
4149
 
4015
4150
  Uso:
4016
- npx ostacky Menú interactivo (instalación completa)
4017
- npx ostacky install Instalar TODO (agente + skills + MCPs + CodeGraph + OpenSpec + Engram + Context7)
4018
- npx ostacky add agent Agregar agente(s)
4019
- npx ostacky add command Agregar command(s)
4020
- npx ostacky add skill Agregar skill(s)
4021
- npx ostacky add mcp Agregar MCP server(s)
4022
- npx ostacky install-stack Instalar solo el stack de herramientas (CodeGraph, OpenSpec, Engram, Context7)
4023
- npx ostacky uninstall-stack Remover la configuración del stack del proyecto
4024
- npx ostacky update Actualizar instalación
4025
- npx ostacky uninstall Desinstalar todo
4026
- npx ostacky uninstall agent Desinstalar agente(s)
4027
- npx ostacky uninstall command Desinstalar command(s)
4028
- npx ostacky uninstall skill Desinstalar skill(s)
4029
- npx ostacky uninstall mcp Desinstalar MCP server(s)
4151
+ npx ostacky [--scope local|global|auto] Menú interactivo (instalación completa, pregunta local vs global, default local)
4152
+ npx ostacky install [--scope local|global|auto] Instalar TODO (agente + skills + MCPs + CodeGraph + OpenSpec + Engram + Context7)
4153
+ npx ostacky add agent [--scope local|global|auto] Agregar agente(s)
4154
+ npx ostacky add command [--scope ...] Agregar command(s)
4155
+ npx ostacky add skill [--scope ...] Agregar skill(s)
4156
+ npx ostacky add mcp [--scope ...] Agregar MCP server(s)
4157
+ npx ostacky install-stack [--scope local|auto] Instalar solo el stack de herramientas (CodeGraph, OpenSpec, Engram, Context7) — global bloquea con error
4158
+ npx ostacky uninstall-stack [--scope local|global|auto] Remover la configuración del stack del proyecto
4159
+ npx ostacky doctor Diagnostica locks, tools, state health
4160
+ npx ostacky status [--json] Muestra estado del controller sin MCP
4161
+ npx ostacky update [--scope ...] Actualizar instalación
4162
+ npx ostacky uninstall [--scope ...] Desinstalar todo
4163
+ npx ostacky uninstall agent [--scope ...] Desinstalar agente(s)
4164
+ npx ostacky uninstall command [--scope ...] Desinstalar command(s)
4165
+ npx ostacky uninstall skill [--scope ...] Desinstalar skill(s)
4166
+ npx ostacky uninstall mcp [--scope ...] Desinstalar MCP server(s)
4030
4167
  npx ostacky --help Mostrar esta ayuda
4031
4168
  npx ostacky --version Mostrar versión
4169
+
4170
+ Scope:
4171
+ --scope local Escribe en <proyecto>/.opencode (recomendado, default al preguntar)
4172
+ --scope global Escribe en ~/.config/opencode (o %APPDATA%\\opencode en Windows)
4173
+ --scope auto Elige local si existe .opencode o .git, si no global
4174
+ Sin flag Pregunta interactiva local (default) vs global
4032
4175
  `.trim();
4033
- var [, , cmd, subcmd] = process.argv;
4176
+ function parseScopeArg(argv = process.argv) {
4177
+ for (let i = 0;i < argv.length; i++) {
4178
+ const arg = argv[i];
4179
+ if (arg === "--scope" && i + 1 < argv.length) {
4180
+ const v3 = argv[i + 1];
4181
+ if (v3 === "local" || v3 === "global" || v3 === "auto")
4182
+ return v3;
4183
+ }
4184
+ if (arg.startsWith("--scope=")) {
4185
+ const v3 = arg.split("=")[1];
4186
+ if (v3 === "local" || v3 === "global" || v3 === "auto")
4187
+ return v3;
4188
+ }
4189
+ }
4190
+ return null;
4191
+ }
4192
+ function withoutScopeArgs(argv) {
4193
+ const out = [];
4194
+ for (let i = 0;i < argv.length; i++) {
4195
+ const arg = argv[i];
4196
+ if (arg === "--scope" && i + 1 < argv.length) {
4197
+ i++;
4198
+ continue;
4199
+ }
4200
+ if (arg.startsWith("--scope="))
4201
+ continue;
4202
+ out.push(arg);
4203
+ }
4204
+ return out;
4205
+ }
4206
+ var scope = parseScopeArg();
4207
+ var argvNoScope = withoutScopeArgs(process.argv);
4208
+ var [, , cmd, subcmd] = argvNoScope;
4209
+ async function runDoctorCommand() {
4210
+ const cwd = process.cwd();
4211
+ const opencodeDir = findOpenCodeDir(cwd) || join11(cwd, ".opencode");
4212
+ const statePath = join11(opencodeDir, "ostacky-state.json");
4213
+ let hasError = false;
4214
+ let hasWarn = false;
4215
+ const check = (label, ok, warn = false) => {
4216
+ if (ok)
4217
+ console.log(`✅ ${label}: OK`);
4218
+ else if (warn) {
4219
+ console.log(`⚠️ ${label}`);
4220
+ hasWarn = true;
4221
+ } else {
4222
+ console.log(`❌ ${label}`);
4223
+ hasError = true;
4224
+ }
4225
+ };
4226
+ try {
4227
+ if (!existsSync8(statePath)) {
4228
+ check("controller: state file missing", false, true);
4229
+ } else {
4230
+ const stat = statSync3(statePath);
4231
+ const raw = readFileSync6(statePath, "utf-8");
4232
+ const parsed = JSON.parse(raw);
4233
+ check(`controller: OK (rev ${parsed.revision || 0} state ${parsed.state || "unknown"})`, true);
4234
+ if (parsed.degraded) {
4235
+ console.log("⚠️ degraded: true (persistido)");
4236
+ hasWarn = true;
4237
+ }
4238
+ if (parsed.degradedEditsCount > 0)
4239
+ console.log(`⚠️ degraded: confirmation not audited in controller (degradedEditsCount=${parsed.degradedEditsCount})`);
4240
+ if (parsed.codegraphBypassCount > 0)
4241
+ console.log(`⚠️ codegraphBypassCount=${parsed.codegraphBypassCount} (inefficient: codegraph bypass)`);
4242
+ if (parsed.stateOversizedCount > 0)
4243
+ console.log(`⚠️ stateOversizedCount=${parsed.stateOversizedCount} snapshots perdidos`);
4244
+ if (parsed.sensitiveAccess)
4245
+ console.log(`ℹ️ sensitiveAccess: allowed=${parsed.sensitiveAccess.allowed || 0} denied=${parsed.sensitiveAccess.denied || 0} blocked=${parsed.sensitiveAccess.blockedAttempts || 0}`);
4246
+ if (parsed.deniedFiles && Object.keys(parsed.deniedFiles).length) {
4247
+ console.log(`ℹ️ denied files: ${Object.keys(parsed.deniedFiles).join(", ")} (denied by user)`);
4248
+ }
4249
+ if (parsed.staleContentAttempts > 0)
4250
+ console.log(`⚠️ staleContentAttempts=${parsed.staleContentAttempts}`);
4251
+ if (parsed.completeWithoutValidateCount > 0)
4252
+ console.log(`⚠️ completeWithoutValidateCount=${parsed.completeWithoutValidateCount}`);
4253
+ if (stat.size > 2 * 1024 * 1024) {
4254
+ console.log("⚠️ state file >2MB (oversized)");
4255
+ hasWarn = true;
4256
+ }
4257
+ const auditSize = (parsed.audit || []).length;
4258
+ if (auditSize > 500)
4259
+ console.log(`⚠️ audit large: ${auditSize}`);
4260
+ try {
4261
+ const { statfsSync } = await import("node:fs");
4262
+ if (typeof statfsSync === "function") {
4263
+ const s = statfsSync(dirname8(statePath));
4264
+ const freeMB = Math.floor(s.bfree * s.bsize / 1048576);
4265
+ if (freeMB < 100) {
4266
+ console.log(`⚠️ Disco casi lleno: ${freeMB}MB libres`);
4267
+ hasWarn = true;
4268
+ } else
4269
+ console.log(`ℹ️ diskFreeMB: ${freeMB}`);
4270
+ }
4271
+ } catch {}
4272
+ if ((parsed.state === "EXECUTING_INLINE" || parsed.state === "EXECUTING_SUBAGENTS") && parsed.expectedTasks) {
4273
+ const pending = parsed.expectedTasks.filter((id) => !parsed.tasks?.[id] || parsed.tasks[id].status !== "COMPLETED").length;
4274
+ const lastHandoffAge = parsed.lastHandoff ? Date.now() - parsed.lastHandoff.ts : Infinity;
4275
+ if (pending === 0 && lastHandoffAge > 60000) {
4276
+ console.log("⚠️ EXECUTING_* con pending==0 y lastHandoff >60s sin progreso — sugerir implementation_complete manual");
4277
+ hasWarn = true;
4278
+ }
4279
+ }
4280
+ }
4281
+ } catch (e2) {
4282
+ check(`controller: ${e2.message}`, false);
4283
+ }
4284
+ try {
4285
+ const lockPid = join11(opencodeDir, "ostacky-state.json.lock.pid");
4286
+ const lockTs = join11(opencodeDir, "ostacky-state.json.lock.timestamp");
4287
+ if (existsSync8(lockPid) || existsSync8(lockTs)) {
4288
+ let ageStr = "";
4289
+ try {
4290
+ const ts = parseInt(readFileSync6(lockTs, "utf-8"), 10);
4291
+ const age = Date.now() - ts;
4292
+ ageStr = `${Math.floor(age / 1000)}s`;
4293
+ const pid = readFileSync6(lockPid, "utf-8").trim();
4294
+ let alive = false;
4295
+ try {
4296
+ process.kill(parseInt(pid, 10), 0);
4297
+ alive = true;
4298
+ } catch {}
4299
+ check(`lock: PID ${pid} age ${ageStr} alive=${alive}`, !alive || age > 15000, true);
4300
+ } catch {
4301
+ check("lock: exists (no timestamp)", false, true);
4302
+ }
4303
+ } else {
4304
+ check("lock: no active lock", true);
4305
+ }
4306
+ } catch {
4307
+ check("lock: check failed", false, true);
4308
+ }
4309
+ const tools = ["codegraph", "engram"];
4310
+ for (const t of tools) {
4311
+ const p2 = join11(opencodeDir, "tools", t, "bin", t);
4312
+ const pExe = p2 + ".exe";
4313
+ check(`tool ${t}: ${existsSync8(p2) || existsSync8(pExe) ? "found" : "missing"}`, existsSync8(p2) || existsSync8(pExe), true);
4314
+ }
4315
+ try {
4316
+ const manifest = JSON.parse(readFileSync6(join11(cwd, "manifest.json"), "utf-8"));
4317
+ const expected = manifest.mcpServers?.find((x2) => x2.name === "ostacky-controller")?.sha256;
4318
+ if (expected) {
4319
+ const actual = computeTreeHash(join11(cwd, "assets", "mcp", "ostacky-controller"));
4320
+ check(`manifest hash: ${expected.slice(0, 8)} vs actual ${actual.slice(0, 8)}`, expected === actual);
4321
+ if (expected !== actual)
4322
+ console.log(" Run: bun run hash:update");
4323
+ }
4324
+ } catch {
4325
+ check("manifest: not found", false, true);
4326
+ }
4327
+ if (existsSync8(statePath)) {
4328
+ try {
4329
+ const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4330
+ if (s.allowedFiles || s.deniedFiles) {}
4331
+ } catch {}
4332
+ }
4333
+ if (hasError)
4334
+ process.exit(1);
4335
+ if (hasWarn)
4336
+ process.exit(0);
4337
+ }
4338
+ async function runStatusCommand(args) {
4339
+ const isJson = args.includes("--json");
4340
+ const cwd = process.cwd();
4341
+ const opencodeDir = findOpenCodeDir(cwd) || join11(cwd, ".opencode");
4342
+ const statePath = join11(opencodeDir, "ostacky-state.json");
4343
+ if (!existsSync8(statePath)) {
4344
+ console.log(isJson ? JSON.stringify({ error: "no state" }) : "No state file");
4345
+ return;
4346
+ }
4347
+ try {
4348
+ const parsed = JSON.parse(readFileSync6(statePath, "utf-8"));
4349
+ const completed = Object.values(parsed.tasks || {}).filter((t) => t.status === "COMPLETED").length;
4350
+ const expected = parsed.expectedTaskCount ?? parsed.expectedTasks?.length ?? Object.keys(parsed.tasks || {}).length;
4351
+ const degraded = parsed.degraded ? " degraded" : "";
4352
+ const lastHandoff = parsed.lastHandoff ? ` lastHandoff: ${parsed.lastHandoff.summary?.slice(0, 60)}` : "";
4353
+ if (isJson) {
4354
+ console.log(JSON.stringify({ state: parsed.state, revision: parsed.revision, degraded: !!parsed.degraded, tasks: `${completed}/${expected}`, lastHandoff: parsed.lastHandoff }, null, 2));
4355
+ } else {
4356
+ console.log(`${parsed.state} rev ${parsed.revision}${degraded} tasks ${completed}/${expected}${lastHandoff}`);
4357
+ if (parsed.lastProposal)
4358
+ console.log(`lastProposal: ${parsed.lastProposal.summary} shownToUser=${parsed.lastProposal.shownToUser}`);
4359
+ }
4360
+ } catch (e2) {
4361
+ console.log(`Error reading state: ${e2.message}`);
4362
+ }
4363
+ }
4034
4364
  async function main() {
4035
4365
  switch (cmd) {
4036
4366
  case "install":
4037
- await runInstallCommand();
4367
+ await runInstallCommand(scope);
4038
4368
  break;
4039
4369
  case "install-stack":
4040
- await runInstallStackCommand();
4370
+ if (scope === "global") {
4371
+ console.error("Error: install-stack requiere scope local; el stack vive en <proyecto>/.opencode/tools");
4372
+ console.error("Sugerencia: ejecutá 'npx ostacky install-stack --scope local' dentro de cada proyecto.");
4373
+ process.exit(1);
4374
+ }
4375
+ await runInstallStackCommand(scope);
4041
4376
  break;
4042
4377
  case "uninstall-stack":
4043
- await runUninstallStackCommand();
4378
+ await runUninstallStackCommand(scope);
4044
4379
  break;
4045
4380
  case "add":
4046
4381
  if (subcmd === "agent") {
4047
- await runAddAgentCommand();
4382
+ await runAddAgentCommand(scope);
4048
4383
  } else if (subcmd === "command") {
4049
- await runAddCommandCommand();
4384
+ await runAddCommandCommand(scope);
4050
4385
  } else if (subcmd === "skill") {
4051
- await runAddSkillCommand();
4386
+ await runAddSkillCommand(scope);
4052
4387
  } else if (subcmd === "mcp") {
4053
- await runAddMcpCommand();
4388
+ await runAddMcpCommand(scope);
4054
4389
  } else {
4055
4390
  console.error(`Tipo desconocido: "${subcmd}". Usa 'agent', 'command', 'skill' o 'mcp'.`);
4056
4391
  process.exit(1);
4057
4392
  }
4058
4393
  break;
4059
4394
  case "update":
4060
- await runUpdateCommand();
4395
+ await runUpdateCommand(scope);
4061
4396
  break;
4062
4397
  case "uninstall":
4063
4398
  if (subcmd === "agent") {
4064
- const name = process.argv[4];
4065
- await runUninstallAgentCommand(name);
4399
+ const name = argvNoScope[4];
4400
+ await runUninstallAgentCommand(name, scope);
4066
4401
  } else if (subcmd === "command") {
4067
- const name = process.argv[4];
4068
- await runUninstallCommandCommand(name);
4402
+ const name = argvNoScope[4];
4403
+ await runUninstallCommandCommand(name, scope);
4069
4404
  } else if (subcmd === "skill") {
4070
- const name = process.argv[4];
4071
- await runUninstallSkillCommand(name);
4405
+ const name = argvNoScope[4];
4406
+ await runUninstallSkillCommand(name, scope);
4072
4407
  } else if (subcmd === "mcp") {
4073
- const name = process.argv[4];
4074
- await runUninstallMcpCommand(name);
4408
+ const name = argvNoScope[4];
4409
+ await runUninstallMcpCommand(name, scope);
4075
4410
  } else if (subcmd === undefined) {
4076
- await runUninstallCommand();
4411
+ await runUninstallCommand(scope);
4077
4412
  } else {
4078
4413
  console.error(`Subcomando desconocido: "${subcmd}". Usa 'agent', 'command', 'skill', 'mcp' o nada.`);
4079
4414
  process.exit(1);
4080
4415
  }
4081
4416
  break;
4417
+ case "doctor":
4418
+ await runDoctorCommand();
4419
+ break;
4420
+ case "status":
4421
+ await runStatusCommand(argvNoScope.slice(3));
4422
+ break;
4082
4423
  case "--help":
4083
4424
  case "-h":
4084
4425
  console.log(HELP);
@@ -4092,7 +4433,7 @@ async function main() {
4092
4433
  console.error(`Comando desconocido: "${cmd}". Usá --help para ver los comandos disponibles.`);
4093
4434
  process.exit(1);
4094
4435
  }
4095
- await runInteractiveMenu();
4436
+ await runInteractiveMenu(scope);
4096
4437
  }
4097
4438
  }
4098
4439
  main().catch((e2) => {