create-harness-btp 0.2.5 → 0.3.1

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 (81) hide show
  1. package/AI/VERSION +1 -1
  2. package/AI/agents/analyst.md +30 -0
  3. package/AI/agents/architect.md +29 -0
  4. package/AI/agents/backend-cap.md +19 -0
  5. package/AI/agents/deploy-cf.md +26 -0
  6. package/AI/agents/documentarian.md +20 -0
  7. package/AI/agents/e2e-fiori.md +62 -0
  8. package/AI/agents/frontend-ui5.md +20 -0
  9. package/AI/agents/harness-evolution.md +38 -0
  10. package/AI/agents/orchestrator.md +41 -0
  11. package/AI/agents/qa-review.md +28 -0
  12. package/AI/agents/researcher.md +29 -0
  13. package/AI/clients/claude/agents/analyst.md +22 -0
  14. package/AI/clients/claude/agents/architect.md +21 -0
  15. package/AI/clients/claude/agents/backend-cap.md +18 -0
  16. package/AI/clients/claude/agents/deploy-cf.md +25 -0
  17. package/AI/clients/claude/agents/documentarian.md +15 -0
  18. package/AI/clients/claude/agents/e2e-fiori.md +61 -0
  19. package/AI/clients/claude/agents/frontend-ui5.md +19 -0
  20. package/AI/clients/claude/agents/harness-evolution.md +37 -0
  21. package/AI/clients/claude/agents/orchestrator.md +40 -0
  22. package/AI/clients/claude/agents/qa-review.md +27 -0
  23. package/AI/clients/claude/agents/researcher.md +21 -0
  24. package/AI/clients/claude/commands/flow.md +16 -0
  25. package/AI/clients/claude/commands/verify.md +18 -0
  26. package/AI/clients/claude/skills/cap-cli/SKILL.md +21 -0
  27. package/AI/clients/claude/skills/cf-cli/SKILL.md +41 -0
  28. package/AI/clients/claude/skills/mta-build/SKILL.md +45 -0
  29. package/AI/clients/claude/skills/sap-verify/SKILL.md +27 -0
  30. package/AI/clients/copilot/agents/analyst.agent.md +22 -0
  31. package/AI/clients/copilot/agents/architect.agent.md +21 -0
  32. package/AI/clients/copilot/agents/backend-cap.agent.md +18 -0
  33. package/AI/clients/copilot/agents/deploy-cf.agent.md +25 -0
  34. package/AI/clients/copilot/agents/documentarian.agent.md +15 -0
  35. package/AI/clients/copilot/agents/e2e-fiori.agent.md +61 -0
  36. package/AI/clients/copilot/agents/frontend-ui5.agent.md +19 -0
  37. package/AI/clients/copilot/agents/harness-evolution.agent.md +37 -0
  38. package/AI/clients/copilot/agents/orchestrator.agent.md +40 -0
  39. package/AI/clients/copilot/agents/qa-review.agent.md +27 -0
  40. package/AI/clients/copilot/agents/researcher.agent.md +21 -0
  41. package/AI/clients/copilot/prompts/flow.prompt.md +16 -0
  42. package/AI/clients/copilot/prompts/verify.prompt.md +18 -0
  43. package/AI/clients/copilot/skills/cap-cli/SKILL.md +21 -0
  44. package/AI/clients/copilot/skills/cf-cli/SKILL.md +41 -0
  45. package/AI/clients/copilot/skills/mta-build/SKILL.md +45 -0
  46. package/AI/clients/copilot/skills/sap-verify/SKILL.md +27 -0
  47. package/AI/clients/cursor/commands/flow.md +12 -0
  48. package/AI/clients/cursor/commands/verify.md +14 -0
  49. package/AI/clients/cursor/skills/cap-cli/SKILL.md +21 -0
  50. package/AI/clients/cursor/skills/cf-cli/SKILL.md +41 -0
  51. package/AI/clients/cursor/skills/mta-build/SKILL.md +45 -0
  52. package/AI/clients/cursor/skills/sap-verify/SKILL.md +27 -0
  53. package/AI/clients/opencode/agents/analyst.md +29 -0
  54. package/AI/clients/opencode/agents/architect.md +28 -0
  55. package/AI/clients/opencode/agents/backend-cap.md +18 -0
  56. package/AI/clients/opencode/agents/deploy-cf.md +25 -0
  57. package/AI/clients/opencode/agents/documentarian.md +19 -0
  58. package/AI/clients/opencode/agents/e2e-fiori.md +61 -0
  59. package/AI/clients/opencode/agents/frontend-ui5.md +19 -0
  60. package/AI/clients/opencode/agents/harness-evolution.md +37 -0
  61. package/AI/clients/opencode/agents/orchestrator.md +40 -0
  62. package/AI/clients/opencode/agents/qa-review.md +27 -0
  63. package/AI/clients/opencode/agents/researcher.md +28 -0
  64. package/AI/clients/opencode/commands/flow.md +16 -0
  65. package/AI/clients/opencode/commands/verify.md +18 -0
  66. package/AI/clients/opencode/skills/cap-cli/SKILL.md +21 -0
  67. package/AI/clients/opencode/skills/cf-cli/SKILL.md +41 -0
  68. package/AI/clients/opencode/skills/mta-build/SKILL.md +45 -0
  69. package/AI/clients/opencode/skills/sap-verify/SKILL.md +27 -0
  70. package/AI/commands/flow.md +16 -0
  71. package/AI/commands/verify.md +18 -0
  72. package/AI/skills/cap-cli/SKILL.md +21 -0
  73. package/AI/skills/cf-cli/SKILL.md +41 -0
  74. package/AI/skills/mta-build/SKILL.md +45 -0
  75. package/AI/skills/sap-verify/SKILL.md +27 -0
  76. package/README.md +83 -17
  77. package/VERSION +1 -1
  78. package/bin/harness-btp.js +74 -28
  79. package/install.ps1 +72 -22
  80. package/install.sh +88 -28
  81. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- // harness-btp init --dest <dir> [--force] [--ref <tag>]
3
- // Copia AI/, crea AGENTS.md puntero, fusiona AI/.opencode/ en .opencode/
4
- // raíz (solo faltantes; con --force hace backup a .opencode.bak) y fusiona .gitignore. Sin secretos.
2
+ // harness-btp init --dest <dir> [--force] [--ref <tag>] [--client auto|all|opencode,claude,...]
3
+ // Copia AI/, crea AGENTS.md puntero, fusiona el árbol del cliente agéntico
4
+ // desde AI/clients/<cliente>/ en la raíz (solo faltantes; con --force hace
5
+ // backup a <dir>.bak) y fusiona .gitignore. Sin secretos.
5
6
  // No toca package.json ni mta.yaml del proyecto.
6
7
  const fs = require("fs");
7
8
  const path = require("path");
@@ -19,10 +20,11 @@ if (!cmd || cmd.startsWith("-")) {
19
20
  const dest = arg("--dest", ".");
20
21
  const force = process.argv.includes("--force");
21
22
  const ref = arg("--ref", null);
23
+ const clientSpec = arg("--client", "auto");
22
24
  const src = path.join(__dirname, "..");
23
25
 
24
26
  if (cmd !== "init") {
25
- console.log("Uso: harness-btp init --dest <dir> [--force] [--ref <tag>]");
27
+ console.log("Uso: harness-btp init --dest <dir> [--force] [--ref <tag>] [--client auto|all|opencode,claude,...]");
26
28
  process.exit(1);
27
29
  }
28
30
  if (ref) console.log("REF: " + ref + " (el paquete incluye su propia versión; ver VERSION)");
@@ -62,43 +64,87 @@ if (fs.existsSync(agentsDst)) {
62
64
  console.log("CREATE: AGENTS.md (puntero a AI/AGENTS.md)");
63
65
  }
64
66
 
65
- const opSrc = path.join(src, "AI", ".opencode");
66
- const opDst = path.join(dest, ".opencode");
67
- function listFilesRec(dir, base) {
68
- let out = [];
69
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
70
- const p = path.join(dir, e.name);
71
- if (e.isDirectory()) out = out.concat(listFilesRec(p, base));
72
- else out.push(path.relative(base, p));
67
+ const KNOWN = ["opencode", "claude", "cursor", "copilot", "aider"];
68
+ function resolveClients(spec, destDir) {
69
+ const s = (spec + "").trim().toLowerCase();
70
+ if (s === "auto") {
71
+ const d = [];
72
+ if (fs.existsSync(path.join(destDir, "CLAUDE.md")) || fs.existsSync(path.join(destDir, ".claude"))) d.push("claude");
73
+ if (fs.existsSync(path.join(destDir, ".cursor"))) d.push("cursor");
74
+ if (fs.existsSync(path.join(destDir, ".github"))) d.push("copilot");
75
+ if (fs.readdirSync(destDir, { withFileTypes: true }).some((e) => e.isFile() && e.name.startsWith("opencode.json")) || fs.existsSync(path.join(destDir, ".opencode"))) d.push("opencode");
76
+ if (!d.length) d.push("opencode");
77
+ console.log("CLIENT: auto -> " + d.join(","));
78
+ return d;
73
79
  }
74
- return out;
80
+ if (s === "all") return ["opencode", "claude", "cursor", "copilot"];
81
+ const list = s.split(",").map((x) => x.trim().toLowerCase()).filter(Boolean);
82
+ for (const c of list) {
83
+ if (!KNOWN.includes(c)) {
84
+ console.log("ABORT: cliente desconocido '" + c + "' (usa auto|all|opencode,claude,cursor,copilot,aider)");
85
+ process.exit(1);
86
+ }
87
+ }
88
+ if (!list.length) {
89
+ console.log("ABORT: --client vacío (usa auto|all|opencode,claude,cursor,copilot,aider)");
90
+ process.exit(1);
91
+ }
92
+ return list;
75
93
  }
76
- if (fs.existsSync(opSrc)) {
94
+
95
+ function mergeTree(treeSrc, treeDst, label) {
96
+ if (!fs.existsSync(treeSrc)) {
97
+ console.log("AVISO: no existe " + treeSrc + " (regenera con node scripts/render-clients.js)");
98
+ return;
99
+ }
100
+ const bakLabel = label.replace(/\/$/, "") + ".bak";
77
101
  if (force) {
78
- if (fs.existsSync(opDst)) {
79
- const opBak = path.join(dest, ".opencode.bak");
80
- fs.rmSync(opBak, { recursive: true, force: true });
81
- fs.renameSync(opDst, opBak);
82
- console.log("BACKUP: .opencode/ -> .opencode.bak");
102
+ if (fs.existsSync(treeDst)) {
103
+ const bak = treeDst + ".bak";
104
+ fs.rmSync(bak, { recursive: true, force: true });
105
+ fs.renameSync(treeDst, bak);
106
+ console.log("BACKUP: " + label + " -> " + bakLabel);
83
107
  }
84
- fs.cpSync(opSrc, opDst, { recursive: true });
85
- console.log("CREATE: .opencode/ (+" + listFilesRec(opDst, opDst).length + " archivos)");
108
+ fs.cpSync(treeSrc, treeDst, { recursive: true });
109
+ console.log("CREATE: " + label + " (+" + listFilesRec(treeDst, treeDst).length + " archivos)");
86
110
  } else {
87
- let opCount = 0;
88
- for (const rel of listFilesRec(opSrc, opSrc)) {
89
- const tgt = path.join(opDst, rel);
111
+ let count = 0;
112
+ for (const rel of listFilesRec(treeSrc, treeSrc)) {
113
+ const tgt = path.join(treeDst, rel);
90
114
  if (fs.existsSync(tgt)) {
91
- console.log("SKIP: .opencode/" + rel.split(path.sep).join("/") + " ya existe");
115
+ console.log("SKIP: " + label + rel.split(path.sep).join("/") + " ya existe");
92
116
  } else {
93
117
  fs.mkdirSync(path.dirname(tgt), { recursive: true });
94
- fs.copyFileSync(path.join(opSrc, rel), tgt);
95
- opCount++;
118
+ fs.copyFileSync(path.join(treeSrc, rel), tgt);
119
+ count++;
96
120
  }
97
121
  }
98
- console.log("CREATE: .opencode/ (+" + opCount + " archivos)");
122
+ console.log("CREATE: " + label + " (+" + count + " archivos)");
99
123
  }
100
124
  }
101
125
 
126
+ const clientsSrc = path.join(src, "AI", "clients");
127
+ for (const c of resolveClients(clientSpec, dest)) {
128
+ if (c === "opencode") mergeTree(path.join(clientsSrc, "opencode"), path.join(dest, ".opencode"), ".opencode/");
129
+ else if (c === "claude") mergeTree(path.join(clientsSrc, "claude"), path.join(dest, ".claude"), ".claude/");
130
+ else if (c === "cursor") {
131
+ mergeTree(path.join(clientsSrc, "cursor", "commands"), path.join(dest, ".cursor", "commands"), ".cursor/commands/");
132
+ mergeTree(path.join(clientsSrc, "cursor", "skills"), path.join(dest, ".agents", "skills"), ".agents/skills/");
133
+ } else if (c === "copilot") {
134
+ mergeTree(path.join(clientsSrc, "copilot", "agents"), path.join(dest, ".github", "agents"), ".github/agents/");
135
+ mergeTree(path.join(clientsSrc, "copilot", "skills"), path.join(dest, ".github", "skills"), ".github/skills/");
136
+ mergeTree(path.join(clientsSrc, "copilot", "prompts"), path.join(dest, ".github", "prompts"), ".github/prompts/");
137
+ } else if (c === "aider") console.log("SKIP: aider usa AGENTS.md raiz (nada que fusionar)");
138
+ }
139
+ function listFilesRec(dir, base) {
140
+ let out = [];
141
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
142
+ const p = path.join(dir, e.name);
143
+ if (e.isDirectory()) out = out.concat(listFilesRec(p, base));
144
+ else out.push(path.relative(base, p));
145
+ }
146
+ return out;
147
+ }
102
148
  const frag = fs.readFileSync(path.join(src, "AI", ".gitignore.fragment"), "utf8")
103
149
  .split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
104
150
  let cur = [];
package/install.ps1 CHANGED
@@ -1,13 +1,18 @@
1
1
  <# Instalador del harness BTP (idempotente, PowerShell 5.1).
2
- Uso: .\install.ps1 [-Dest "."] [-Force]
2
+ Uso: .\install.ps1 [-Dest "."] [-Force] [-Client auto]
3
3
  - Copia AI/ al proyecto destino (SKIP si ya existe; con -Force hace backup a AI.bak).
4
- - Fusiona AI/.opencode/{agents,commands,skills} en .opencode/ raíz (solo faltantes; con -Force hace backup a .opencode.bak).
4
+ - Fusiona el árbol del cliente agéntico desde AI/clients/<cliente>/ en la raíz
5
+ (solo faltantes; con -Force hace backup a <dir>.bak):
6
+ -Client auto (defecto): detecta por huellas (CLAUDE.md|.claude→claude,
7
+ .cursor→cursor, .github→copilot, opencode.json*|.opencode→opencode;
8
+ sin huellas→opencode) | all: todos | lista explícita (p. ej. claude,copilot).
5
9
  - Crea AGENTS.md raíz desde la plantilla solo si no existe.
6
10
  - Fusiona .gitignore con las líneas del fragmento (solo agrega faltantes).
7
11
  - NO toca package.json ni mta.yaml del proyecto. Sin secretos. #>
8
12
  param(
9
13
  [string]$Dest = ".",
10
- [switch]$Force
14
+ [switch]$Force,
15
+ $Client = "auto"
11
16
  )
12
17
 
13
18
  $ErrorActionPreference = "Stop"
@@ -50,35 +55,80 @@ if (Test-Path $AgentsDst) {
50
55
  Write-Output "CREATE: AGENTS.md (puntero a AI/AGENTS.md)"
51
56
  }
52
57
 
53
- $OpSrc = Join-Path $AiSrc ".opencode"
54
- $OpDst = Join-Path $Dest ".opencode"
55
- if (Test-Path $OpSrc) {
58
+ $ClientsSrc = Join-Path $AiSrc "clients"
59
+
60
+ function Get-ClientList($Spec, $DestDir) {
61
+ $s = (($Spec -join ",") + "").Trim().ToLower()
62
+ if ($s -eq "auto") {
63
+ $d = @()
64
+ if ((Test-Path (Join-Path $DestDir "CLAUDE.md")) -or (Test-Path (Join-Path $DestDir ".claude"))) { $d += "claude" }
65
+ if (Test-Path (Join-Path $DestDir ".cursor")) { $d += "cursor" }
66
+ if (Test-Path (Join-Path $DestDir ".github")) { $d += "copilot" }
67
+ $opHints = @(Get-ChildItem $DestDir -Filter "opencode.json*" -File -ErrorAction SilentlyContinue)
68
+ if (($opHints.Count -gt 0) -or (Test-Path (Join-Path $DestDir ".opencode"))) { $d += "opencode" }
69
+ if ($d.Count -eq 0) { $d = @("opencode") }
70
+ return $d
71
+ }
72
+ if ($s -eq "all") { return @("opencode", "claude", "cursor", "copilot") }
73
+ $list = @($s -split "," | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ -ne "" })
74
+ $known = @("opencode", "claude", "cursor", "copilot", "aider")
75
+ foreach ($c in $list) {
76
+ if ($known -notcontains $c) { Write-Host "ABORT: cliente desconocido '$c' (usa auto|all|opencode,claude,cursor,copilot,aider)"; exit 1 }
77
+ }
78
+ if ($list.Count -eq 0) { Write-Host "ABORT: -Client vacío (usa auto|all|opencode,claude,cursor,copilot,aider)"; exit 1 }
79
+ return $list
80
+ }
81
+
82
+ function Merge-Tree($TreeSrc, $TreeDst, $Label) {
83
+ if (-not (Test-Path $TreeSrc)) { Write-Output "AVISO: no existe $TreeSrc (regenera con node scripts/render-clients.js)"; return }
84
+ $BakLabel = $Label.TrimEnd("/") + ".bak"
56
85
  if ($Force) {
57
- if (Test-Path $OpDst) {
58
- $OpBak = Join-Path $Dest ".opencode.bak"
59
- if (Test-Path $OpBak) { Remove-Item $OpBak -Recurse -Force }
60
- Rename-Item $OpDst ".opencode.bak"
61
- Write-Output "BACKUP: .opencode/ -> .opencode.bak"
86
+ if (Test-Path $TreeDst) {
87
+ $Bak = "$TreeDst.bak"
88
+ if (Test-Path $Bak) { Remove-Item $Bak -Recurse -Force }
89
+ Rename-Item $TreeDst ([IO.Path]::GetFileName($Bak))
90
+ Write-Output "BACKUP: $Label -> $BakLabel"
62
91
  }
63
- Copy-Item $OpSrc $OpDst -Recurse -Force
64
- $OpCount = @(Get-ChildItem $OpDst -Recurse -File).Count
65
- Write-Output ("CREATE: .opencode/ (+{0} archivos)" -f $OpCount)
92
+ Copy-Item $TreeSrc $TreeDst -Recurse -Force
93
+ $TreeCount = @(Get-ChildItem $TreeDst -Recurse -File).Count
94
+ Write-Output ("CREATE: {0} (+{1} archivos)" -f $Label, $TreeCount)
66
95
  } else {
67
- $OpCount = 0
68
- $OpFiles = @(Get-ChildItem $OpSrc -Recurse -File)
69
- foreach ($f in $OpFiles) {
70
- $rel = $f.FullName.Substring($OpSrc.Length + 1)
71
- $tgt = Join-Path $OpDst $rel
96
+ $TreeCount = 0
97
+ $TreeFiles = @(Get-ChildItem $TreeSrc -Recurse -File)
98
+ foreach ($f in $TreeFiles) {
99
+ $rel = $f.FullName.Substring($TreeSrc.Length + 1)
100
+ $tgt = Join-Path $TreeDst $rel
72
101
  if (Test-Path $tgt) {
73
- Write-Output ("SKIP: .opencode/{0} ya existe" -f ($rel -replace "\\","/"))
102
+ Write-Output ("SKIP: {0}{1} ya existe" -f $Label, ($rel -replace "\\","/"))
74
103
  } else {
75
104
  $tgtDir = Split-Path -Parent $tgt
76
105
  if (-not (Test-Path $tgtDir)) { New-Item $tgtDir -ItemType Directory -Force | Out-Null }
77
106
  Copy-Item $f.FullName $tgt -Force
78
- $OpCount++
107
+ $TreeCount++
79
108
  }
80
109
  }
81
- Write-Output ("CREATE: .opencode/ (+{0} archivos)" -f $OpCount)
110
+ Write-Output ("CREATE: {0} (+{1} archivos)" -f $Label, $TreeCount)
111
+ }
112
+ }
113
+
114
+ $SelectedClients = @(Get-ClientList $Client $Dest)
115
+ if ((($Client -join ",") + "").Trim().ToLower() -eq "auto") {
116
+ Write-Output ("CLIENT: auto -> {0}" -f ($SelectedClients -join ","))
117
+ }
118
+ foreach ($c in $SelectedClients) {
119
+ if ($c -eq "opencode") {
120
+ Merge-Tree (Join-Path $ClientsSrc "opencode") (Join-Path $Dest ".opencode") ".opencode/"
121
+ } elseif ($c -eq "claude") {
122
+ Merge-Tree (Join-Path $ClientsSrc "claude") (Join-Path $Dest ".claude") ".claude/"
123
+ } elseif ($c -eq "cursor") {
124
+ Merge-Tree (Join-Path $ClientsSrc "cursor\commands") (Join-Path $Dest ".cursor\commands") ".cursor/commands/"
125
+ Merge-Tree (Join-Path $ClientsSrc "cursor\skills") (Join-Path $Dest ".agents\skills") ".agents/skills/"
126
+ } elseif ($c -eq "copilot") {
127
+ Merge-Tree (Join-Path $ClientsSrc "copilot\agents") (Join-Path $Dest ".github\agents") ".github/agents/"
128
+ Merge-Tree (Join-Path $ClientsSrc "copilot\skills") (Join-Path $Dest ".github\skills") ".github/skills/"
129
+ Merge-Tree (Join-Path $ClientsSrc "copilot\prompts") (Join-Path $Dest ".github\prompts") ".github/prompts/"
130
+ } elseif ($c -eq "aider") {
131
+ Write-Output "SKIP: aider usa AGENTS.md raiz (nada que fusionar)"
82
132
  }
83
133
  }
84
134
 
package/install.sh CHANGED
@@ -1,20 +1,26 @@
1
1
  #!/bin/sh
2
2
  # Instalador del harness BTP (idempotente, POSIX sh).
3
- # Uso: ./install.sh [--dest <dir>] [--force] [--help]
3
+ # Uso: ./install.sh [--dest <dir>] [--force] [--client auto|all|opencode,claude,...] [--help]
4
4
  # Sin secretos. No toca package.json ni mta.yaml del proyecto.
5
- # Fusiona AI/.opencode/{agents,commands,skills} en .opencode/ raíz
6
- # (solo faltantes; con --force hace backup a .opencode.bak).
5
+ # Fusiona el árbol del cliente agéntico desde AI/clients/<cliente>/ en la raíz
6
+ # (solo faltantes; con --force hace backup a <dir>.bak).
7
+ # --client auto (defecto): detecta por huellas (CLAUDE.md|.claude→claude,
8
+ # .cursor→cursor, .github→copilot, opencode.json*|.opencode→opencode;
9
+ # sin huellas→opencode). all: todos. Lista explícita: esos.
7
10
  DEST="."
8
11
  FORCE=0
12
+ CLIENT="auto"
9
13
  while [ $# -gt 0 ]; do
10
14
  case "$1" in
11
15
  --dest) DEST="$2"; shift 2 ;;
12
16
  --force) FORCE=1; shift ;;
17
+ --client) CLIENT="$2"; shift 2 ;;
13
18
  -h|--help)
14
- echo "Uso: ./install.sh [--dest <dir>] [--force] [--help]"
15
- echo " --dest <dir> proyecto destino (por defecto: .)"
16
- echo " --force reinstala AI/ con backup a AI.bak"
17
- echo " -h, --help muestra esta ayuda"
19
+ echo "Uso: ./install.sh [--dest <dir>] [--force] [--client <clientes>] [--help]"
20
+ echo " --dest <dir> proyecto destino (por defecto: .)"
21
+ echo " --force reinstala AI/ con backup a AI.bak"
22
+ echo " --client <list> auto (defecto) | all | opencode,claude,cursor,copilot,aider"
23
+ echo " -h, --help muestra esta ayuda"
18
24
  exit 0 ;;
19
25
  *) echo "ABORT: opción desconocida $1 (usa --help)"; exit 1 ;;
20
26
  esac
@@ -45,33 +51,87 @@ else
45
51
  cp "$SRC/templates/AGENTS.pointer.md" "$DEST/AGENTS.md" && echo "CREATE: AGENTS.md (puntero a AI/AGENTS.md)"
46
52
  fi
47
53
 
48
- OP_SRC="$SRC/AI/.opencode"
49
- OP_DST="$DEST/.opencode"
50
- if [ -d "$OP_SRC" ]; then
54
+ # Fusion idempotente de un arbol: merge_tree <origen> <destino> <etiqueta>
55
+ # (etiqueta p. ej. ".opencode/"; el backup va a <destino>.bak).
56
+ merge_tree() {
57
+ MT_SRC="$1"; MT_DST="$2"; MT_LABEL="$3"
58
+ if [ ! -d "$MT_SRC" ]; then echo "AVISO: no existe $MT_SRC (regenera con node scripts/render-clients.js)"; return 0; fi
59
+ MT_BAKLABEL="$(printf '%s' "$MT_LABEL" | sed 's:/$::').bak"
51
60
  if [ "$FORCE" -eq 1 ]; then
52
- if [ -d "$OP_DST" ]; then
53
- rm -rf "$DEST/.opencode.bak" && mv "$DEST/.opencode" "$DEST/.opencode.bak"
54
- echo "BACKUP: .opencode/ -> .opencode.bak"
61
+ if [ -d "$MT_DST" ]; then
62
+ rm -rf "$MT_DST.bak" && mv "$MT_DST" "$MT_DST.bak"
63
+ echo "BACKUP: $MT_LABEL -> $MT_BAKLABEL"
55
64
  fi
56
- cp -r "$OP_SRC" "$OP_DST" && OP_COUNT=$(find "$OP_DST" -type f | wc -l) && echo "CREATE: .opencode/ (+$OP_COUNT archivos)"
65
+ mkdir -p "$(dirname "$MT_DST")"
66
+ cp -r "$MT_SRC" "$MT_DST" && MT_COUNT=$(find "$MT_DST" -type f | wc -l) && echo "CREATE: $MT_LABEL (+$MT_COUNT archivos)"
57
67
  else
58
- OP_COUNT=0
59
- OP_LIST="${TMPDIR:-/tmp}/harness-opcount-$$.tmp"
60
- find "$OP_SRC" -type f -print > "$OP_LIST"
61
- while IFS= read -r OP_FILE; do
62
- OP_REL=${OP_FILE#"$OP_SRC"/}
63
- if [ -f "$OP_DST/$OP_REL" ]; then
64
- echo "SKIP: .opencode/$OP_REL ya existe"
68
+ MT_COUNT=0
69
+ MT_LIST="${TMPDIR:-/tmp}/harness-mt-$$.tmp"
70
+ find "$MT_SRC" -type f -print > "$MT_LIST"
71
+ while IFS= read -r MT_FILE; do
72
+ MT_REL=${MT_FILE#"$MT_SRC"/}
73
+ if [ -f "$MT_DST/$MT_REL" ]; then
74
+ echo "SKIP: $MT_LABEL$MT_REL ya existe"
65
75
  else
66
- mkdir -p "$(dirname "$OP_DST/$OP_REL")"
67
- cp "$OP_FILE" "$OP_DST/$OP_REL"
68
- OP_COUNT=$((OP_COUNT + 1))
76
+ mkdir -p "$(dirname "$MT_DST/$MT_REL")"
77
+ cp "$MT_FILE" "$MT_DST/$MT_REL"
78
+ MT_COUNT=$((MT_COUNT + 1))
69
79
  fi
70
- done < "$OP_LIST"
71
- rm -f "$OP_LIST"
72
- echo "CREATE: .opencode/ (+$OP_COUNT archivos)"
80
+ done < "$MT_LIST"
81
+ rm -f "$MT_LIST"
82
+ echo "CREATE: $MT_LABEL (+$MT_COUNT archivos)"
73
83
  fi
74
- fi
84
+ }
85
+
86
+ # Resuelve la lista de clientes: detecta huellas en auto, valida explícitos.
87
+ resolve_clients() {
88
+ case "$CLIENT" in
89
+ auto)
90
+ RC_LIST=""
91
+ { [ -f "$DEST/CLAUDE.md" ] || [ -d "$DEST/.claude" ]; } && RC_LIST="$RC_LIST claude"
92
+ [ -d "$DEST/.cursor" ] && RC_LIST="$RC_LIST cursor"
93
+ [ -d "$DEST/.github" ] && RC_LIST="$RC_LIST copilot"
94
+ if ls "$DEST"/opencode.json* >/dev/null 2>&1 || [ -d "$DEST/.opencode" ]; then RC_LIST="$RC_LIST opencode"; fi
95
+ [ -z "$RC_LIST" ] && RC_LIST=" opencode"
96
+ echo "CLIENT: auto -> $(printf '%s' "$RC_LIST" | tr ' ' ',' | sed 's/^,//')" >&2
97
+ printf '%s' "$RC_LIST"
98
+ ;;
99
+ all) printf ' opencode claude cursor copilot' ;;
100
+ *)
101
+ RC_OUT=""
102
+ RC_REST="$CLIENT,"
103
+ while [ -n "$RC_REST" ]; do
104
+ RC_ONE=${RC_REST%%,*}; RC_REST=${RC_REST#*,}
105
+ RC_ONE=$(printf '%s' "$RC_ONE" | tr 'A-Z' 'a-z' | tr -d ' ')
106
+ [ -z "$RC_ONE" ] && continue
107
+ case "$RC_ONE" in
108
+ opencode|claude|cursor|copilot|aider) RC_OUT="$RC_OUT $RC_ONE" ;;
109
+ *) echo "ABORT: cliente desconocido '$RC_ONE' (usa auto|all|opencode,claude,cursor,copilot,aider)" >&2; exit 1 ;;
110
+ esac
111
+ done
112
+ [ -z "$RC_OUT" ] && { echo "ABORT: --client vacío (usa auto|all|opencode,claude,cursor,copilot,aider)" >&2; exit 1; }
113
+ printf '%s' "$RC_OUT"
114
+ ;;
115
+ esac
116
+ }
117
+
118
+ CLIENTS_SRC="$SRC/AI/clients"
119
+ RC_RESOLVED=$(resolve_clients); RC_STATUS=$?
120
+ if [ "$RC_STATUS" -ne 0 ]; then exit "$RC_STATUS"; fi
121
+ for C in $RC_RESOLVED; do
122
+ case "$C" in
123
+ opencode) merge_tree "$CLIENTS_SRC/opencode" "$DEST/.opencode" ".opencode/" ;;
124
+ claude) merge_tree "$CLIENTS_SRC/claude" "$DEST/.claude" ".claude/" ;;
125
+ cursor)
126
+ merge_tree "$CLIENTS_SRC/cursor/commands" "$DEST/.cursor/commands" ".cursor/commands/"
127
+ merge_tree "$CLIENTS_SRC/cursor/skills" "$DEST/.agents/skills" ".agents/skills/" ;;
128
+ copilot)
129
+ merge_tree "$CLIENTS_SRC/copilot/agents" "$DEST/.github/agents" ".github/agents/"
130
+ merge_tree "$CLIENTS_SRC/copilot/skills" "$DEST/.github/skills" ".github/skills/"
131
+ merge_tree "$CLIENTS_SRC/copilot/prompts" "$DEST/.github/prompts" ".github/prompts/" ;;
132
+ aider) echo "SKIP: aider usa AGENTS.md raiz (nada que fusionar)" ;;
133
+ esac
134
+ done
75
135
 
76
136
  FRAG="$SRC/AI/.gitignore.fragment"
77
137
  touch "$DEST/.gitignore"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-harness-btp",
3
- "version": "0.2.5",
3
+ "version": "0.3.1",
4
4
  "description": "Instala el harness MVP SAP BTP como carpeta AI/ en cualquier proyecto",
5
5
  "keywords": ["sap-btp", "opencode", "harness", "agents", "scaffolder", "create"],
6
6
  "homepage": "https://github.com/horangelmillan/harness-btp#readme",