ostacky 0.9.6 → 0.10.0

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
@@ -222,6 +222,7 @@ import {
222
222
  renameSync
223
223
  } from "fs";
224
224
  import { createHash as createHash2 } from "crypto";
225
+ import { homedir } from "os";
225
226
  import { join, resolve, dirname, relative, basename } from "path";
226
227
  import { execFileSync } from "child_process";
227
228
  function findOpenCodeDir(startDir = process.cwd()) {
@@ -261,7 +262,23 @@ function isGitRepo(dir = process.cwd()) {
261
262
  function initGitRepo(dir) {
262
263
  execFileSync("git", ["init"], { cwd: dir, stdio: "pipe" });
263
264
  }
265
+ function getGlobalOpenCodeDir(opts) {
266
+ const platform = opts?.platform ?? process.platform;
267
+ const env = opts?.env ?? process.env;
268
+ const home = opts?.home ?? homedir();
269
+ const xdg = env.XDG_CONFIG_HOME?.trim();
270
+ if (xdg)
271
+ return join(xdg, "opencode");
272
+ if (platform === "win32") {
273
+ const appdata = env.APPDATA?.trim();
274
+ if (appdata)
275
+ return join(appdata, "opencode");
276
+ }
277
+ return join(home, ".config", "opencode");
278
+ }
264
279
  function getOpenCodeDirForScope(scope, cwd = process.cwd()) {
280
+ if (scope === "global")
281
+ return getGlobalOpenCodeDir();
265
282
  return join(findProjectRoot(cwd), ".opencode");
266
283
  }
267
284
  function ensureOpenCodePaths(opencodeDir) {
@@ -624,47 +641,47 @@ var init_fs = __esm(() => {
624
641
  var manifest_default;
625
642
  var init_manifest = __esm(() => {
626
643
  manifest_default = {
627
- version: "0.9.6",
644
+ version: "0.10.0",
628
645
  repo: "JaimeHoracio/Ostacky",
629
- tag: "v0.9.6",
646
+ tag: "v0.10.0",
630
647
  agents: [
631
648
  {
632
649
  name: "ostacky",
633
650
  file: "assets/agents/ostacky.md",
634
- 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.9.6: prune de skills obsoletas, lastHandoff, getAvailableTransitions, consecutiveFailures real, mem_session_summary automático al cierre.",
635
- version: "0.9.6",
636
- sha256: "29e435104f6915905a85e15ca768a339c3e5b0898fc923ae147afb7c4f20a716"
651
+ 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.10.0: prune de skills obsoletas, lastHandoff, getAvailableTransitions, consecutiveFailures real, mem_session_summary automático al cierre.",
652
+ version: "0.10.0",
653
+ sha256: "d092ac75eb83f56354e5accf479ae6f0309253077b7071b2cd93bcc959f9dc63"
637
654
  }
638
655
  ],
639
656
  commands: [
640
657
  {
641
658
  name: "install-stack",
642
659
  file: "assets/commands/install-stack.md",
643
- description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, controller plugin). v0.9.6: controller plugin como alma hard-gate, Context7 removido del stack.",
644
- version: "0.9.6",
645
- sha256: "88da074ab02bef60bcaebd1fed9592ea13f95d245850ec9f2467cf69a7f2fc4c"
660
+ description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, controller plugin). v0.10.0: controller plugin como alma hard-gate, Context7 removido del stack.",
661
+ version: "0.10.0",
662
+ sha256: "2ed68d2451bfad3c142c943ca8cb4dba86616e6be8ef89f9af3849bcdc5d41c3"
646
663
  },
647
664
  {
648
665
  name: "opsx-sync",
649
666
  file: "assets/commands/opsx-sync.md",
650
667
  description: "Sincroniza delta specs del change activo sin inicializar CodeGraph si ya existe índice",
651
- version: "0.9.6",
652
- sha256: "fe0158478f2ca63b315037a85fc1632b77868532e319af6c7384285441767d64"
668
+ version: "0.10.0",
669
+ sha256: "f883f9ad96f7d75778631d5cc13d8a1a9c3e6ec41396bbce8ce13cff2e66fca2"
653
670
  }
654
671
  ],
655
672
  mcpServers: [
656
673
  {
657
674
  name: "ostacky-controller",
658
675
  file: "assets/mcp/ostacky-controller/",
659
- description: "Máquina de estados persistida con @modelcontextprotocol/server SDK. v0.9.6: 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.",
660
- version: "0.9.6",
661
- sha256: "6669c464c47a285a21f3eea6456487c92b725a40e7938ade5ecfff97e2537427"
676
+ description: "Máquina de estados persistida con @modelcontextprotocol/server SDK. v0.10.0: 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.",
677
+ version: "0.10.0",
678
+ sha256: "9e6647f1540c21063d32bc3e163a7c0a379bbc1859416c3e7f55af39f2dd8e4e"
662
679
  },
663
680
  {
664
681
  name: "openspec",
665
682
  file: "assets/mcp/openspec/",
666
683
  description: "MCP server local para OpenSpec - proposal, apply, archive, sync de cambios",
667
- version: "0.9.6",
684
+ version: "0.10.0",
668
685
  sha256: "bdb283fe8ec2dfcba2ec4f38191f4039e58cd7ae12e44e66310783b42c827638"
669
686
  }
670
687
  ],
@@ -673,105 +690,105 @@ var init_manifest = __esm(() => {
673
690
  name: "brainstorming",
674
691
  file: "assets/skills/brainstorming/SKILL.md",
675
692
  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)",
676
- version: "0.9.6",
693
+ version: "0.10.0",
677
694
  sha256: "f1013a86a8f27914d85a716e23c7fd32db1786d93615e8b0834b13d184308549"
678
695
  },
679
696
  {
680
697
  name: "execution-mode-evaluation",
681
698
  file: "assets/skills/execution-mode-evaluation/SKILL.md",
682
699
  description: "Skill de análisis de modo de ejecución — output reconciliado con controller snapshot contract (recommendation field)",
683
- version: "0.9.6",
700
+ version: "0.10.0",
684
701
  sha256: "2158a6864c0912e868a9d720db7fd79441e6cc298e3320833d2527c9727c852e"
685
702
  },
686
703
  {
687
704
  name: "tdd",
688
705
  file: "assets/skills/tdd/SKILL.md",
689
706
  description: "Skill de test-driven development (Superpowers)",
690
- version: "0.9.6",
707
+ version: "0.10.0",
691
708
  sha256: "f0ebfbbc43a8868d31a3fd2df37ae7f60fe8add09c6475862395a68269e5ff7f"
692
709
  },
693
710
  {
694
711
  name: "subagent-driven-development",
695
712
  file: "assets/skills/subagent-driven-development/SKILL.md",
696
713
  description: "Skill de ejecución con subagentes (Superpowers) — ejecuta solo después de confirmación del coordinador Ostacky",
697
- version: "0.9.6",
714
+ version: "0.10.0",
698
715
  sha256: "1a42d714a9a13faf05f0bf7b580e8d837c2e77a5633839542ccce603023419e8"
699
716
  },
700
717
  {
701
718
  name: "dispatching-parallel-agents",
702
719
  file: "assets/skills/dispatching-parallel-agents/SKILL.md",
703
720
  description: "Skill de dispatch paralelo de agentes (Superpowers)",
704
- version: "0.9.6",
721
+ version: "0.10.0",
705
722
  sha256: "7e7b712ef742d5631e302e6a71583e2bbbd33094da9281abfb84b43505fc3ef1"
706
723
  },
707
724
  {
708
725
  name: "review",
709
726
  file: "assets/skills/review/SKILL.md",
710
727
  description: "Skill de revisión de código (Superpowers)",
711
- version: "0.9.6",
728
+ version: "0.10.0",
712
729
  sha256: "b19650ed4d1d4d9857a4dd5b7e08e91328a1a1fc9c45bcc2fb600fa9d0213279"
713
730
  },
714
731
  {
715
732
  name: "receiving-code-review",
716
733
  file: "assets/skills/receiving-code-review/SKILL.md",
717
734
  description: "Skill de recibir y procesar feedback de code review",
718
- version: "0.9.6",
735
+ version: "0.10.0",
719
736
  sha256: "d761e884e71d8d3476ac734d287cae403a4024506a7d12e7361a448217c0a831"
720
737
  },
721
738
  {
722
739
  name: "openspec-propose",
723
740
  file: "assets/skills/openspec-propose/SKILL.md",
724
741
  description: "Skill de generación de proposal (OpenSpec)",
725
- version: "0.9.6",
742
+ version: "0.10.0",
726
743
  sha256: "6a517f750f24c008208156a2d653e97ed30b1e53878aeac5ce394e573db0678f"
727
744
  },
728
745
  {
729
746
  name: "openspec-apply-change",
730
747
  file: "assets/skills/openspec-apply-change/SKILL.md",
731
748
  description: "Skill de aplicación de change (OpenSpec)",
732
- version: "0.9.6",
749
+ version: "0.10.0",
733
750
  sha256: "dfc823bf89fc7505e91ab6dee9c1f004be38a410bd3d88fb67b211b1b6cbb1d0"
734
751
  },
735
752
  {
736
753
  name: "openspec-archive-change",
737
754
  file: "assets/skills/openspec-archive-change/SKILL.md",
738
755
  description: "Skill de archivo de change (OpenSpec)",
739
- version: "0.9.6",
756
+ version: "0.10.0",
740
757
  sha256: "16e4b561de7747283663fed602e506abf502452c49a8d4b86d7a5539c40f0195"
741
758
  },
742
759
  {
743
760
  name: "openspec-explore",
744
761
  file: "assets/skills/openspec-explore/SKILL.md",
745
762
  description: "Modo explore para OpenSpec — thinking partner para explorar ideas, investigar problemas y clarificar requisitos antes/durante un cambio",
746
- version: "0.9.6",
763
+ version: "0.10.0",
747
764
  sha256: "9eafc3a8c693b8d9916867ef764d49e5222da717fde501c957bb3f6c312d2415"
748
765
  },
749
766
  {
750
767
  name: "using-git-worktrees",
751
768
  file: "assets/skills/using-git-worktrees/SKILL.md",
752
769
  description: "Skill de uso de git worktrees para aislamiento de trabajo",
753
- version: "0.9.6",
770
+ version: "0.10.0",
754
771
  sha256: "93341bc1b7c053618a8b6dc07e3615b77990d7b549a0201f5835d95fef67ce13"
755
772
  },
756
773
  {
757
774
  name: "using-superpowers",
758
775
  file: "assets/skills/using-superpowers/SKILL.md",
759
776
  description: "Skill de orquestación de Superpowers skills",
760
- version: "0.9.6",
777
+ version: "0.10.0",
761
778
  sha256: "7e54536f96d2a379185a10bfc1e970850caa2561b6d8aca7080f0defea0381a4"
762
779
  },
763
780
  {
764
781
  name: "writing-skills",
765
782
  file: "assets/skills/writing-skills/SKILL.md",
766
783
  description: "Skill de creación y edición de skills",
767
- version: "0.9.6",
784
+ version: "0.10.0",
768
785
  sha256: "3d76b906cee518a2b809febb70db95697a35b9f368986bb504b4120c3bfb437a"
769
786
  },
770
787
  {
771
788
  name: "graceful-degradation",
772
789
  file: "assets/skills/graceful-degradation/SKILL.md",
773
790
  description: "Skill de degradación graceful cuando múltiples tools están indisponibles",
774
- version: "0.9.6",
791
+ version: "0.10.0",
775
792
  sha256: "363449bd9e6f800a0a3168667cf78f767fb105d86e4a4adadcc649d418139303"
776
793
  }
777
794
  ]
@@ -1121,13 +1138,13 @@ var init_config = __esm(() => {
1121
1138
 
1122
1139
  // src/stack.ts
1123
1140
  import { chmodSync, copyFileSync as copyFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync4, rmSync as rmSync4, unlinkSync as unlinkSync3 } from "fs";
1124
- import { basename as basename2, dirname as dirname5, join as join7, resolve as resolve3 } from "path";
1141
+ import { basename as basename3, dirname as dirname5, join as join7, resolve as resolve3 } from "path";
1125
1142
  import { execFileSync as execFileSync3 } from "child_process";
1126
1143
  function resolveToolInstallLocation(toolsDir) {
1127
1144
  const resolvedToolsDir = resolve3(toolsDir ?? join7(findProjectRoot(), ".opencode", "tools"));
1128
1145
  const possibleOpenCodeDir = dirname5(resolvedToolsDir);
1129
1146
  return {
1130
- projectRoot: basename2(possibleOpenCodeDir) === ".opencode" ? dirname5(possibleOpenCodeDir) : findProjectRoot(),
1147
+ projectRoot: basename3(possibleOpenCodeDir) === ".opencode" ? dirname5(possibleOpenCodeDir) : findProjectRoot(),
1131
1148
  toolsDir: resolvedToolsDir
1132
1149
  };
1133
1150
  }
@@ -1566,8 +1583,9 @@ function checkDoctorV2(projectRoot) {
1566
1583
  if (found) {
1567
1584
  try {
1568
1585
  const src = readFileSync6(found, "utf-8");
1569
- const isV2 = src.includes("Plugin.define") && src.includes("@opencode/plugin") && !src.includes("@opencode-ai/plugin");
1570
- if (isV2)
1586
+ const hasDefault = /export\s+default/.test(src);
1587
+ const hasLegacyPkg = src.includes("@opencode-ai/");
1588
+ if (hasDefault && !hasLegacyPkg)
1571
1589
  lines.push(`✅ controller: plugin V2 (${found})`);
1572
1590
  else
1573
1591
  lines.push(`⚠️ controller: plugin legacy V1 en ${found} — no corre en V2, reinstalá con ostacky`);
@@ -1647,12 +1665,40 @@ function checkDoctorV2(projectRoot) {
1647
1665
  for (const [name, entry] of Object.entries(servers)) {
1648
1666
  if (!entry || typeof entry !== "object")
1649
1667
  continue;
1650
- const cmd = entry.command;
1651
- if (entry.type === "local" && Array.isArray(cmd) && typeof cmd[0] === "string") {
1652
- const bin = cmd[0];
1653
- const ok = isAbsolute(bin) ? existsSync8(bin) : lookupOnPath(bin) !== null;
1654
- if (!ok)
1655
- lines.push(`⚠️ mcp.servers.${name} command no existe (${bin}) — ¿proyecto movido? re-corré install`);
1668
+ const e = entry;
1669
+ if (e.type !== "local" && e.type !== "remote") {
1670
+ lines.push(`⚠️ mcp.servers.${name} sin type local|remote — V2 lo requiere (ver mcp-servers)`);
1671
+ continue;
1672
+ }
1673
+ if ("enabled" in e) {
1674
+ lines.push(`⚠️ mcp.servers.${name} usa enabled (forma V1) — V2 usa disabled invertido`);
1675
+ }
1676
+ if (typeof e.timeout === "number") {
1677
+ lines.push(`⚠️ mcp.servers.${name} timeout numérico (forma V1) — V2 usa timeout: { catalog, execution }`);
1678
+ }
1679
+ if (e.type === "local") {
1680
+ const cmd = e.command;
1681
+ if (!Array.isArray(cmd) || typeof cmd[0] !== "string") {
1682
+ lines.push(`⚠️ mcp.servers.${name} local sin command array — V2 requiere command: [bin, ...args]`);
1683
+ } else {
1684
+ const bin = cmd[0];
1685
+ const ok = isAbsolute(bin) ? existsSync8(bin) : lookupOnPath(bin) !== null;
1686
+ if (!ok)
1687
+ lines.push(`⚠️ mcp.servers.${name} command no existe (${bin}) — ¿proyecto movido? re-corré install`);
1688
+ }
1689
+ } else {
1690
+ if (typeof e.url !== "string" || !/^https?:\/\//.test(e.url)) {
1691
+ lines.push(`⚠️ mcp.servers.${name} remote sin url absoluta — V2 requiere url https://...`);
1692
+ }
1693
+ const oauth = e.oauth;
1694
+ if (oauth && typeof oauth === "object") {
1695
+ for (const k of ["clientId", "clientSecret", "callbackPort", "redirectUri"]) {
1696
+ if (k in oauth) {
1697
+ lines.push(`⚠️ mcp.servers.${name} oauth.${k} camelCase (forma V1) — V2 usa snake_case`);
1698
+ break;
1699
+ }
1700
+ }
1701
+ }
1656
1702
  }
1657
1703
  }
1658
1704
  return lines;
@@ -1681,7 +1727,7 @@ var init_doctor_v2 = __esm(() => {
1681
1727
  // package.json
1682
1728
  var package_default = {
1683
1729
  name: "ostacky",
1684
- version: "0.9.6",
1730
+ version: "0.10.0",
1685
1731
  description: "Instalador interactivo de agentes y comandos para OpenCode",
1686
1732
  type: "module",
1687
1733
  bin: {
@@ -2702,7 +2748,7 @@ import {
2702
2748
  rmSync as rmSync3
2703
2749
  } from "fs";
2704
2750
  import { execFileSync as execFileSync2, spawn } from "child_process";
2705
- import { dirname as dirname4, join as join6 } from "path";
2751
+ import { dirname as dirname4, join as join6, basename as basename2 } from "path";
2706
2752
  import { pathToFileURL } from "url";
2707
2753
  function createMcpConfigEntry(name, nodeExecutable, serverPath, statePath) {
2708
2754
  const entry = {
@@ -2860,20 +2906,25 @@ async function probeMcpServer(nodeExecutable, serverPath, cwd, statePath, option
2860
2906
  params: {
2861
2907
  protocolVersion: "2025-03-26",
2862
2908
  capabilities: {},
2863
- clientInfo: { name: "ostacky-installer", version: "0.9.6" }
2909
+ clientInfo: { name: "ostacky-installer", version: "0.10.0" }
2864
2910
  }
2865
2911
  });
2866
2912
  });
2867
2913
  } finally {
2868
2914
  await new Promise((r) => setTimeout(r, 200));
2869
2915
  if (statePath) {
2870
- for (const p of [
2916
+ const probeDir = dirname4(statePath);
2917
+ const probeBase = basename2(statePath);
2918
+ const probePrefix = probeBase + ".";
2919
+ for (const p of readdirSync2(probeDir).filter((entry) => entry === probeBase || entry.startsWith(probePrefix)).map((entry) => join6(probeDir, entry)).concat([
2871
2920
  statePath,
2872
2921
  statePath + ".backup",
2922
+ statePath + ".backup.1",
2923
+ statePath + ".backup.2",
2873
2924
  statePath + ".lock.pid",
2874
2925
  statePath + ".lock.timestamp",
2875
2926
  statePath + ".tmp." + process.pid
2876
- ]) {
2927
+ ])) {
2877
2928
  try {
2878
2929
  if (existsSync5(p))
2879
2930
  rmSync3(p, { force: true });
@@ -3021,7 +3072,8 @@ async function installMcpServer(item, manifest, paths) {
3021
3072
  const treeHash = computeTreeHash(src);
3022
3073
  assertBundledTreeHash("mcpServers", item.name, treeHash);
3023
3074
  const nodeExecutable = getVerifiedNodeExecutable();
3024
- const projectRoot = dirname4(paths.root);
3075
+ await Promise.resolve().then(() => init_fs());
3076
+ const projectRoot = paths.root === getGlobalOpenCodeDir() ? paths.root : dirname4(paths.root);
3025
3077
  const dest = join6(paths.mcp, item.name);
3026
3078
  const staging = `${dest}.staging-${process.pid}-${Date.now()}`;
3027
3079
  const statePath = item.name === "ostacky-controller" ? join6(paths.root, "ostacky-state.json") : undefined;
@@ -3269,11 +3321,23 @@ function printPostInstallSteps() {
3269
3321
  `), "Próximos pasos");
3270
3322
  }
3271
3323
  async function resolveOpenCodePaths(scope, opts) {
3324
+ const effective = scope ?? "local";
3325
+ if (effective === "global") {
3326
+ await Promise.resolve().then(() => init_fs());
3327
+ const dir = getGlobalOpenCodeDir();
3328
+ try {
3329
+ const paths = ensureOpenCodePaths(dir);
3330
+ ye(dir, "Instalación global");
3331
+ return paths;
3332
+ } catch (e) {
3333
+ throw e;
3334
+ }
3335
+ }
3272
3336
  const cwd = process.cwd();
3273
3337
  const base = opts?.gitPreflight ? (await runGitPreflight(cwd)).projectRoot : cwd;
3274
3338
  const dir = getOpenCodeDirForScope("local", base);
3275
- if (scope && scope !== "local") {
3276
- v2.warn(`Scope ${scope} removido; usando local en ${dir}`);
3339
+ if (scope && scope !== "local" && scope !== "global") {
3340
+ v2.warn(`Scope ${scope} no soportado; usando local en ${dir}`);
3277
3341
  }
3278
3342
  try {
3279
3343
  const paths = ensureOpenCodePaths(dir);
@@ -3366,6 +3430,15 @@ async function doInstallStack(toolsDir, projectRoot) {
3366
3430
  let allOk = true;
3367
3431
  const resolvedProjectRoot = projectRoot ?? dirname6(dirname6(toolsDir ?? join8(process.cwd(), ".opencode", "tools")));
3368
3432
  const resolvedToolsDir = toolsDir ?? join8(resolvedProjectRoot, ".opencode", "tools");
3433
+ try {
3434
+ await Promise.resolve().then(() => init_fs());
3435
+ const globalDir = getGlobalOpenCodeDir().replace(/\\/g, "/").toLowerCase();
3436
+ const toolsNorm = resolvedToolsDir.replace(/\\/g, "/").toLowerCase();
3437
+ if (toolsNorm === globalDir || toolsNorm.startsWith(globalDir + "/")) {
3438
+ v2.warn("Scope global: el stack (CodeGraph/Engram) es siempre local por proyecto — omitido aquí. Instalalo por proyecto con `npx ostacky install-stack --scope local`.");
3439
+ return true;
3440
+ }
3441
+ } catch {}
3369
3442
  v2.info(`Stack → projectRoot: ${resolvedProjectRoot} | toolsDir: ${resolvedToolsDir}`);
3370
3443
  try {
3371
3444
  const { homedir } = await import("os");
@@ -3411,8 +3484,11 @@ async function doInstallStack(toolsDir, projectRoot) {
3411
3484
  async function doInstallAll(manifest, paths, opts) {
3412
3485
  const spin = L2();
3413
3486
  let errors = 0;
3414
- v2.info(`Scope → local | opencodeDir: ${paths.root} | tools: ${paths.tools}`);
3415
- if (!opts?.noStack) {
3487
+ const isGlobal = opts?.scope === "global";
3488
+ v2.info(`Scope → ${isGlobal ? "global" : "local"} | opencodeDir: ${paths.root} | tools: ${paths.tools}`);
3489
+ if (isGlobal) {
3490
+ v2.info("Scope global: núcleo (agente/commands/skills/MCPs/plugins) en dir global; el stack (CodeGraph/Engram) es siempre local por proyecto — omitido aquí. Instalalo por proyecto con `npx ostacky install-stack --scope local`.");
3491
+ } else if (!opts?.noStack) {
3416
3492
  ensureToolDirs(paths.tools, ["codegraph", "engram"]);
3417
3493
  }
3418
3494
  for (const agent of manifest.agents) {
@@ -3490,7 +3566,9 @@ async function doInstallAll(manifest, paths, opts) {
3490
3566
  } catch {}
3491
3567
  let stackOk = true;
3492
3568
  let missingTools = [];
3493
- if (opts?.noStack) {
3569
+ if (isGlobal) {
3570
+ v2.info("Scope global: stack omitido (siempre local por proyecto).");
3571
+ } else if (opts?.noStack) {
3494
3572
  v2.info("Stack omitido (--no-stack): solo núcleo (agente/commands/skills/MCPs/plugins).");
3495
3573
  } else {
3496
3574
  v2.info("Instalando stack de herramientas...");
@@ -3500,7 +3578,7 @@ async function doInstallAll(manifest, paths, opts) {
3500
3578
  }
3501
3579
  const codegraphDir = join8(paths.tools, "codegraph");
3502
3580
  const engramDir = join8(paths.tools, "engram");
3503
- if (!opts?.noStack) {
3581
+ if (!isGlobal && !opts?.noStack) {
3504
3582
  if (!existsSync7(codegraphDir) || !findBinaryInDir(codegraphDir, "codegraph"))
3505
3583
  missingTools.push("CodeGraph");
3506
3584
  if (!existsSync7(engramDir) || !findBinaryInDir(engramDir, "engram"))
@@ -3523,12 +3601,14 @@ async function doInstallAll(manifest, paths, opts) {
3523
3601
  v2.warn("Además el stack quedó parcial: reintentá con `npx ostacky install-stack --scope local`.");
3524
3602
  }
3525
3603
  try {
3526
- const projectRoot = dirname6(paths.root);
3527
- const gi = ensureGitignore(projectRoot);
3528
- if (gi.created)
3529
- v2.info(`.gitignore creado con patrones Ostacky`);
3530
- else if (gi.updated)
3531
- v2.info(`.gitignore actualizado: ${gi.patternsAdded.join(", ")}`);
3604
+ if (!isGlobal) {
3605
+ const projectRoot = dirname6(paths.root);
3606
+ const gi = ensureGitignore(projectRoot);
3607
+ if (gi.created)
3608
+ v2.info(`.gitignore creado con patrones Ostacky`);
3609
+ else if (gi.updated)
3610
+ v2.info(`.gitignore actualizado: ${gi.patternsAdded.join(", ")}`);
3611
+ }
3532
3612
  } catch {}
3533
3613
  return coreOk && !stackPartial;
3534
3614
  }
@@ -4177,7 +4257,7 @@ async function runInteractiveMenu(scope) {
4177
4257
  we(" OpenCode Installer ");
4178
4258
  await ensureOpencodeInstalled();
4179
4259
  const manifest = await loadManifest();
4180
- const paths = await resolveOpenCodePaths(scope ?? null, { gitPreflight: true });
4260
+ const paths = await resolveOpenCodePaths(scope ?? null, { gitPreflight: scope !== "global" });
4181
4261
  if (!paths) {
4182
4262
  fe("Instalación cancelada.");
4183
4263
  return;
@@ -4199,7 +4279,7 @@ async function runInteractiveMenu(scope) {
4199
4279
  onCancel(action);
4200
4280
  switch (action) {
4201
4281
  case "all":
4202
- if (!await doInstallAll(manifest, paths))
4282
+ if (!await doInstallAll(manifest, paths, { scope: scope ?? "local" }))
4203
4283
  process.exitCode = 1;
4204
4284
  printPostInstallSteps();
4205
4285
  fe(process.exitCode ? "Instalación parcial." : "Listo.");
@@ -4225,6 +4305,11 @@ async function runInteractiveMenu(scope) {
4225
4305
  fe("Listo.");
4226
4306
  break;
4227
4307
  case "stack": {
4308
+ if ((scope ?? "local") === "global") {
4309
+ v2.warn("El stack (CodeGraph/Engram) es siempre local por proyecto. En scope global se omite; usá `npx ostacky install-stack --scope local` dentro del proyecto.");
4310
+ fe("Listo.");
4311
+ break;
4312
+ }
4228
4313
  ensureToolDirs(paths.tools, ["codegraph", "engram"]);
4229
4314
  const stackOk = await doInstallStack(paths.tools, dirname7(paths.root));
4230
4315
  if (!stackOk)
@@ -4251,12 +4336,12 @@ async function runInstallCommand(scope, opts) {
4251
4336
  we(" OpenCode Installer ");
4252
4337
  await ensureOpencodeInstalled();
4253
4338
  const manifest = await loadManifest();
4254
- const paths = await resolveOpenCodePaths(scope ?? null, { gitPreflight: true });
4339
+ const paths = await resolveOpenCodePaths(scope ?? null, { gitPreflight: scope !== "global" });
4255
4340
  if (!paths) {
4256
4341
  fe("Cancelado.");
4257
4342
  return;
4258
4343
  }
4259
- if (!await doInstallAll(manifest, paths, opts))
4344
+ if (!await doInstallAll(manifest, paths, { ...opts, scope: scope ?? "local" }))
4260
4345
  process.exitCode = 1;
4261
4346
  printPostInstallSteps();
4262
4347
  fe(process.exitCode ? "Instalación parcial." : "Instalación completada.");
@@ -4515,59 +4600,142 @@ async function runUninstallMcpCommand(name, scope) {
4515
4600
  }
4516
4601
 
4517
4602
  // src/cli.ts
4518
- init_fs();
4519
4603
  import { existsSync as existsSync9, statSync as statSync3, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
4520
4604
  import { join as join11, dirname as dirname8 } from "node:path";
4605
+ import { spawnSync } from "node:child_process";
4606
+ init_fs();
4607
+
4608
+ // src/version-check.ts
4609
+ function parseMajor(version) {
4610
+ if (!version)
4611
+ return null;
4612
+ const m = /v?(\d+)\.\d+\.\d+/.exec(version.trim());
4613
+ if (!m)
4614
+ return null;
4615
+ const major = parseInt(m[1], 10);
4616
+ return Number.isNaN(major) ? null : major;
4617
+ }
4618
+ function compareVersions(a, b) {
4619
+ const pa = a.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
4620
+ const pb = b.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
4621
+ for (let i = 0;i < 3; i++) {
4622
+ if ((pa[i] ?? 0) < (pb[i] ?? 0))
4623
+ return -1;
4624
+ if ((pa[i] ?? 0) > (pb[i] ?? 0))
4625
+ return 1;
4626
+ }
4627
+ return 0;
4628
+ }
4629
+ async function fetchLatestNpmVersion(pkg = "ostacky", timeoutMs = 5000) {
4630
+ try {
4631
+ const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
4632
+ signal: AbortSignal.timeout(timeoutMs),
4633
+ headers: { "User-Agent": "ostacky-installer" }
4634
+ });
4635
+ if (!res.ok)
4636
+ return null;
4637
+ const data = await res.json();
4638
+ return typeof data.version === "string" ? data.version : null;
4639
+ } catch {
4640
+ return null;
4641
+ }
4642
+ }
4643
+ function decideOffer(installed, latest) {
4644
+ if (!latest || compareVersions(installed, latest) >= 0) {
4645
+ return { kind: "none", installed, latest };
4646
+ }
4647
+ if (parseMajor(installed) === 1) {
4648
+ return { kind: "migrate-v1", installed, latest };
4649
+ }
4650
+ return { kind: "update-available", installed, latest };
4651
+ }
4652
+ function canPrompt(argv = process.argv, isTTY = process.stdin.isTTY === true) {
4653
+ if (!isTTY)
4654
+ return false;
4655
+ return !argv.includes("--yes") && !argv.includes("--no-interactive") && !argv.includes("--non-interactive");
4656
+ }
4657
+
4658
+ // src/cli.ts
4659
+ async function maybeOfferUpdateOrMigrate(installed, scope) {
4660
+ const latest = await fetchLatestNpmVersion();
4661
+ const offer = decideOffer(installed, latest);
4662
+ if (offer.kind === "none" || !offer.latest)
4663
+ return;
4664
+ if (!canPrompt()) {
4665
+ console.error(`Hay una versión más nueva de ostacky: ${offer.latest} (instalada: ${installed}). Re-ejecutá con npx ostacky@latest.`);
4666
+ return;
4667
+ }
4668
+ if (offer.kind === "migrate-v1") {
4669
+ const confirm = await me({
4670
+ message: `Tenés Ostacky v1 instalada (${installed}) y la última es ${offer.latest} (v2). ¿Desinstalar v1 e instalar v2? (los artefactos legacy se limpian automáticamente)${scope === "global" ? " (scope global)" : ""}`,
4671
+ initialValue: true
4672
+ });
4673
+ if (BD(confirm) || !confirm)
4674
+ return;
4675
+ } else {
4676
+ const confirm = await me({
4677
+ message: `Hay una versión más nueva de ostacky: ${offer.latest} (instalada: ${installed}). ¿Actualizar y re-ejecutar con @latest?`,
4678
+ initialValue: true
4679
+ });
4680
+ if (BD(confirm) || !confirm)
4681
+ return;
4682
+ }
4683
+ const userArgs = process.argv.slice(2);
4684
+ const invocation = getCommandInvocation("npx", ["ostacky@latest", ...userArgs]);
4685
+ const res = spawnSync(invocation.command, invocation.args, { stdio: "inherit" });
4686
+ process.exit(res.status ?? 0);
4687
+ }
4521
4688
  var HELP = `
4522
4689
  ostacky — Instalador de agentes, comandos, skills y MCPs para OpenCode
4523
4690
 
4524
4691
  Uso:
4525
- npx ostacky [--scope local] Menú interactivo (instalación completa, siempre local)
4526
- npx ostacky install [--scope local] [--no-stack] Instalar TODO (agente + skills + MCPs + CodeGraph + OpenSpec + Engram; --no-stack omite el stack)
4527
- npx ostacky add agent [--scope local] Agregar agente(s)
4528
- npx ostacky add command [--scope local] Agregar command(s)
4529
- npx ostacky add skill [--scope local] Agregar skill(s)
4530
- npx ostacky add mcp [--scope local] Agregar MCP server(s)
4531
- npx ostacky install-stack [--scope local] Instalar solo el stack de herramientas (CodeGraph, OpenSpec, Engram)
4692
+ npx ostacky [--scope local|global] Menú interactivo (default: local en <proyecto>/.opencode)
4693
+ npx ostacky install [--scope local|global] [--no-stack] Instalar TODO (agente + skills + MCPs + CodeGraph + OpenSpec + Engram; --no-stack omite el stack)
4694
+ npx ostacky add agent [--scope local|global] Agregar agente(s)
4695
+ npx ostacky add command [--scope local|global] Agregar command(s)
4696
+ npx ostacky add skill [--scope local|global] Agregar skill(s)
4697
+ npx ostacky add mcp [--scope local|global] Agregar MCP server(s)
4698
+ npx ostacky install-stack [--scope local] Instalar solo el stack de herramientas (CodeGraph, OpenSpec, Engram — siempre local por proyecto)
4532
4699
  npx ostacky uninstall-stack [--scope local] Remover la configuración del stack del proyecto
4533
4700
  npx ostacky doctor Diagnostica locks, tools, state health
4534
4701
  npx ostacky status [--json] Muestra estado del controller sin MCP
4535
- npx ostacky update [--scope local] Actualizar instalación
4536
- npx ostacky uninstall [--scope local] Desinstalar todo
4537
- npx ostacky uninstall agent [--scope local] Desinstalar agente(s)
4538
- npx ostacky uninstall command [--scope local] Desinstalar command(s)
4539
- npx ostacky uninstall skill [--scope local] Desinstalar skill(s)
4540
- npx ostacky uninstall mcp [--scope local] Desinstalar MCP server(s)
4702
+ npx ostacky update [--scope local|global] Actualizar instalación
4703
+ npx ostacky uninstall [--scope local|global] Desinstalar todo
4704
+ npx ostacky uninstall agent [--scope local|global] Desinstalar agente(s)
4705
+ npx ostacky uninstall command [--scope local|global] Desinstalar command(s)
4706
+ npx ostacky uninstall skill [--scope local|global] Desinstalar skill(s)
4707
+ npx ostacky uninstall mcp [--scope local|global] Desinstalar MCP server(s)
4541
4708
  npx ostacky --help Mostrar esta ayuda
4542
4709
  npx ostacky --version Mostrar versión
4543
4710
 
4544
4711
  Scope:
4545
- --scope local Escribe en <proyecto>/.opencode (siempre local, instalador único)
4546
- Sin flag Asume local implícito (no pregunta global)
4712
+ --scope local Escribe en <proyecto>/.opencode (default; no pregunta global)
4713
+ --scope global Escribe en el config dir global de OpenCode (~/.config/opencode, respeta XDG_CONFIG_HOME)
4714
+ Sin flag Asume local implícito
4547
4715
  `.trim();
4548
4716
  function parseScopeArg(argv = process.argv) {
4549
4717
  for (let i = 0;i < argv.length; i++) {
4550
4718
  const arg = argv[i];
4551
4719
  if (arg === "--scope" && i + 1 < argv.length) {
4552
4720
  const v = argv[i + 1];
4553
- if (v === "local")
4721
+ if (v === "local" || v === "global")
4554
4722
  return v;
4555
- if (v === "global" || v === "auto") {
4556
- console.error(`Error: --scope ${v} removido; Ostacky instala siempre local en <proyecto>/.opencode. Hacé cd al proyecto y re-ejecutá con --scope local.`);
4723
+ if (v === "auto") {
4724
+ console.error(`Error: --scope auto removido; usá --scope local o --scope global explícito.`);
4557
4725
  process.exit(1);
4558
4726
  }
4559
4727
  }
4560
4728
  if (arg.startsWith("--scope=")) {
4561
4729
  const v = arg.split("=")[1];
4562
- if (v === "local")
4730
+ if (v === "local" || v === "global")
4563
4731
  return v;
4564
- if (v === "global" || v === "auto") {
4565
- console.error(`Error: --scope ${v} removido; Ostacky instala siempre local en <proyecto>/.opencode. Hacé cd al proyecto y re-ejecutá con --scope local.`);
4732
+ if (v === "auto") {
4733
+ console.error(`Error: --scope auto removido; usá --scope local o --scope global explícito.`);
4566
4734
  process.exit(1);
4567
4735
  }
4568
4736
  }
4569
4737
  if (arg === "-g") {
4570
- console.error(`Error: -g/global removido; Ostacky instala siempre local en <proyecto>/.opencode. Hacé cd al proyecto y re-ejecutá sin -g.`);
4738
+ console.error(`Error: -g removido; usá --scope global (instalación global) o --scope local (default por proyecto).`);
4571
4739
  process.exit(1);
4572
4740
  }
4573
4741
  }
@@ -4928,10 +5096,16 @@ async function main() {
4928
5096
  await runInstallCommand(scope, { noStack });
4929
5097
  break;
4930
5098
  case "install-stack":
4931
- await runInstallStackCommand(scope);
5099
+ if (scope === "global") {
5100
+ console.error(`Aviso: el stack (CodeGraph/Engram) se instala siempre local por proyecto. Ignorando --scope global y usando local.`);
5101
+ }
5102
+ await runInstallStackCommand(null);
4932
5103
  break;
4933
5104
  case "uninstall-stack":
4934
- await runUninstallStackCommand(scope);
5105
+ if (scope === "global") {
5106
+ console.error(`Aviso: el stack es siempre local por proyecto. Ignorando --scope global y usando local.`);
5107
+ }
5108
+ await runUninstallStackCommand(null);
4935
5109
  break;
4936
5110
  case "add":
4937
5111
  if (subcmd === "agent") {
@@ -4989,6 +5163,7 @@ async function main() {
4989
5163
  console.error(`Comando desconocido: "${cmd}". Usá --help para ver los comandos disponibles.`);
4990
5164
  process.exit(1);
4991
5165
  }
5166
+ await maybeOfferUpdateOrMigrate(package_default.version, scope);
4992
5167
  await runInteractiveMenu(scope);
4993
5168
  }
4994
5169
  }