zugzbot 1.0.36 → 1.0.37

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.
@@ -11,6 +11,27 @@ const getRoot = (context: any) => {
11
11
  return process.cwd();
12
12
  };
13
13
 
14
+ // Helper to read the targetDir from sdd_state.json or .sdd_bootstrap.json
15
+ const getTargetDir = (root: string): string => {
16
+ try {
17
+ const bootstrapPath = path.resolve(root, ".openspec/.sdd_bootstrap.json")
18
+ if (fs.existsSync(bootstrapPath)) {
19
+ const data = JSON.parse(fs.readFileSync(bootstrapPath, "utf8"))
20
+ if (data && data.targetDir) return data.targetDir
21
+ }
22
+ } catch (e) {}
23
+
24
+ try {
25
+ const statePath = path.resolve(root, ".openspec/sdd_state.json")
26
+ if (fs.existsSync(statePath)) {
27
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
28
+ if (state && state.targetDir) return state.targetDir
29
+ }
30
+ } catch (e) {}
31
+
32
+ return "."
33
+ }
34
+
14
35
  // Tool: sdd_clean_docker_environment
15
36
  export const clean_docker_environment = tool({
16
37
  description: "Asegura que Docker esté abierto y realiza una limpieza total y agresiva: detiene y elimina TODOS los contenedores (activos o inactivos), remueve TODAS las imágenes, volúmenes y redes para garantizar un lienzo en blanco absoluto antes de desplegar.",
@@ -100,6 +121,8 @@ export const generate_dockerfile = tool({
100
121
  },
101
122
  async execute(args, context) {
102
123
  const root = getRoot(context)
124
+ const targetDir = getTargetDir(root)
125
+ const targetPath = targetDir === "." ? root : path.resolve(root, targetDir)
103
126
 
104
127
  if (args.stack === "agnostic") {
105
128
  const dockerfileAg = `FROM node:20-alpine
@@ -111,7 +134,7 @@ CMD ["node", "src/index.js"]
111
134
  const filesWritten: string[] = []
112
135
  let content = dockerfileAg
113
136
  let targetName = "src/index.js"
114
- if (fs.existsSync(path.resolve(root, "src/main.py"))) {
137
+ if (fs.existsSync(path.resolve(targetPath, "src/main.py"))) {
115
138
  content = `FROM python:3.11-slim
116
139
  WORKDIR /app
117
140
  COPY . .
@@ -121,9 +144,9 @@ CMD ["python", "src/main.py"]
121
144
  targetName = "src/main.py"
122
145
  }
123
146
 
124
- const fullPath = path.resolve(root, "Dockerfile")
147
+ const fullPath = path.resolve(targetPath, "Dockerfile")
125
148
  fs.writeFileSync(fullPath, content, "utf8")
126
- filesWritten.push("Dockerfile")
149
+ filesWritten.push(path.relative(root, fullPath))
127
150
 
128
151
  return JSON.stringify({
129
152
  status: "SUCCESS",
@@ -140,11 +163,11 @@ CMD ["python", "src/main.py"]
140
163
  let pm = "npm"
141
164
  let installCmd = "npm ci --frozen-lockfile"
142
165
  let buildCmd = "npm run build"
143
- if (fs.existsSync(path.resolve(root, "pnpm-lock.yaml"))) {
166
+ if (fs.existsSync(path.resolve(targetPath, "pnpm-lock.yaml"))) {
144
167
  pm = "pnpm"
145
168
  installCmd = "pnpm install --frozen-lockfile"
146
169
  buildCmd = "pnpm build"
147
- } else if (fs.existsSync(path.resolve(root, "yarn.lock"))) {
170
+ } else if (fs.existsSync(path.resolve(targetPath, "yarn.lock"))) {
148
171
  pm = "yarn"
149
172
  installCmd = "yarn install --frozen-lockfile"
150
173
  buildCmd = "yarn build"
@@ -231,7 +254,7 @@ test-results
231
254
  [".dockerignore", dockerignore],
232
255
  ["docker-compose.yml", compose],
233
256
  ] as const) {
234
- const fullPath = path.resolve(root, name)
257
+ const fullPath = path.resolve(targetPath, name)
235
258
  fs.writeFileSync(fullPath, content, "utf8")
236
259
  filesWritten.push(path.relative(root, fullPath))
237
260
  }
@@ -288,7 +311,7 @@ tests/
288
311
  [".dockerignore", dockerignorePy],
289
312
  ["docker-compose.yml", composePy],
290
313
  ] as const) {
291
- const fullPath = path.resolve(root, name)
314
+ const fullPath = path.resolve(targetPath, name)
292
315
  fs.writeFileSync(fullPath, content, "utf8")
293
316
  filesWritten.push(path.relative(root, fullPath))
294
317
  }
@@ -11,6 +11,27 @@ const getRoot = (context: any) => {
11
11
  return process.cwd();
12
12
  };
13
13
 
14
+ // Helper to read the targetDir from sdd_state.json or .sdd_bootstrap.json
15
+ const getTargetDir = (root: string): string => {
16
+ try {
17
+ const bootstrapPath = path.resolve(root, ".openspec/.sdd_bootstrap.json")
18
+ if (fs.existsSync(bootstrapPath)) {
19
+ const data = JSON.parse(fs.readFileSync(bootstrapPath, "utf8"))
20
+ if (data && data.targetDir) return data.targetDir
21
+ }
22
+ } catch (e) {}
23
+
24
+ try {
25
+ const statePath = path.resolve(root, ".openspec/sdd_state.json")
26
+ if (fs.existsSync(statePath)) {
27
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
28
+ if (state && state.targetDir) return state.targetDir
29
+ }
30
+ } catch (e) {}
31
+
32
+ return "."
33
+ }
34
+
14
35
  // Helper to get PID file path
15
36
  const getPidFilePath = (root: string) => {
16
37
  return path.resolve(root, ".openspec/dev_server.pid")
@@ -56,7 +77,8 @@ export const start_server = tool({
56
77
  },
57
78
  async execute(args, context) {
58
79
  const root = getRoot(context)
59
- const targetCwd = args.cwd ? path.resolve(root, args.cwd) : root
80
+ const targetDir = getTargetDir(root)
81
+ const targetCwd = args.cwd ? path.resolve(root, args.cwd) : (targetDir === "." ? root : path.resolve(root, targetDir))
60
82
  const pidFile = getPidFilePath(root)
61
83
 
62
84
  try {
@@ -11,6 +11,27 @@ const getRoot = (context: any) => {
11
11
  return process.cwd();
12
12
  };
13
13
 
14
+ // Helper to read the targetDir from sdd_state.json or .sdd_bootstrap.json
15
+ const getTargetDir = (root: string): string => {
16
+ try {
17
+ const bootstrapPath = path.resolve(root, ".openspec/.sdd_bootstrap.json")
18
+ if (fs.existsSync(bootstrapPath)) {
19
+ const data = JSON.parse(fs.readFileSync(bootstrapPath, "utf8"))
20
+ if (data && data.targetDir) return data.targetDir
21
+ }
22
+ } catch (e) {}
23
+
24
+ try {
25
+ const statePath = path.resolve(root, ".openspec/sdd_state.json")
26
+ if (fs.existsSync(statePath)) {
27
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
28
+ if (state && state.targetDir) return state.targetDir
29
+ }
30
+ } catch (e) {}
31
+
32
+ return "."
33
+ }
34
+
14
35
  // Helper to parse semantic errors from compiler and linter outputs (reducing raw trace log bloat for the LLM)
15
36
  const parseSemanticErrors = (rawOutput: string, type: "eslint" | "tsc"): any[] => {
16
37
  const parsed: any[] = []
@@ -54,8 +75,10 @@ export const quick_lint = tool({
54
75
  args: {},
55
76
  async execute(args, context) {
56
77
  const root = getRoot(context)
78
+ const targetDir = getTargetDir(root)
79
+ const targetPath = targetDir === "." ? root : path.resolve(root, targetDir)
57
80
 
58
- const pkgPath = path.resolve(root, "package.json")
81
+ const pkgPath = path.resolve(targetPath, "package.json")
59
82
  if (!fs.existsSync(pkgPath)) {
60
83
  return JSON.stringify({
61
84
  status: "ERROR",
@@ -63,6 +86,13 @@ export const quick_lint = tool({
63
86
  }, null, 2)
64
87
  }
65
88
 
89
+ const getRelPath = (fullRel: string) => {
90
+ if (targetDir !== "." && fullRel.startsWith(targetDir + "/")) {
91
+ return fullRel.slice(targetDir.length + 1)
92
+ }
93
+ return fullRel
94
+ }
95
+
66
96
  let eslintFiles = "src/"
67
97
  try {
68
98
  const stateFile = path.resolve(root, ".openspec/sdd_state.json")
@@ -73,7 +103,9 @@ export const quick_lint = tool({
73
103
  if (fs.existsSync(contractPath)) {
74
104
  const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"))
75
105
  if (contract && Array.isArray(contract.files_affected) && contract.files_affected.length > 0) {
76
- const existingFiles = contract.files_affected.filter((f: string) => fs.existsSync(path.resolve(root, f)))
106
+ const existingFiles = contract.files_affected
107
+ .filter((f: string) => fs.existsSync(path.resolve(root, f)))
108
+ .map((f: string) => getRelPath(f))
77
109
  if (existingFiles.length > 0) {
78
110
  eslintFiles = existingFiles.join(" ")
79
111
  }
@@ -87,7 +119,7 @@ export const quick_lint = tool({
87
119
 
88
120
  try {
89
121
  const out = execSync(`npx eslint ${eslintFiles} --quiet --max-warnings 0 2>&1 || true`, {
90
- cwd: root,
122
+ cwd: targetPath,
91
123
  encoding: "utf8",
92
124
  timeout: 120_000,
93
125
  })
@@ -112,9 +144,11 @@ export const shift_left_verify = tool({
112
144
  args: {},
113
145
  async execute(args, context) {
114
146
  const root = getRoot(context)
147
+ const targetDir = getTargetDir(root)
148
+ const targetPath = targetDir === "." ? root : path.resolve(root, targetDir)
115
149
 
116
150
  // Check if it is a Python project
117
- let isPython = fs.existsSync(path.resolve(root, "requirements.txt")) || fs.existsSync(path.resolve(root, "pyproject.toml"));
151
+ let isPython = fs.existsSync(path.resolve(targetPath, "requirements.txt")) || fs.existsSync(path.resolve(targetPath, "pyproject.toml"));
118
152
  try {
119
153
  const stateFile = path.resolve(root, ".openspec/sdd_state.json")
120
154
  if (fs.existsSync(stateFile)) {
@@ -133,6 +167,13 @@ export const shift_left_verify = tool({
133
167
  // ignore
134
168
  }
135
169
 
170
+ const getRelPath = (fullRel: string) => {
171
+ if (targetDir !== "." && fullRel.startsWith(targetDir + "/")) {
172
+ return fullRel.slice(targetDir.length + 1)
173
+ }
174
+ return fullRel
175
+ }
176
+
136
177
  if (isPython) {
137
178
  const resultPython: { ruff: { status: string, errors?: any[] } } = {
138
179
  ruff: { status: "SUCCESS" }
@@ -147,7 +188,9 @@ export const shift_left_verify = tool({
147
188
  if (fs.existsSync(contractPath)) {
148
189
  const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"))
149
190
  if (contract && Array.isArray(contract.files_affected) && contract.files_affected.length > 0) {
150
- const existingFiles = contract.files_affected.filter((f: string) => fs.existsSync(path.resolve(root, f)) && f.endsWith(".py"))
191
+ const existingFiles = contract.files_affected
192
+ .filter((f: string) => fs.existsSync(path.resolve(root, f)) && f.endsWith(".py"))
193
+ .map((f: string) => getRelPath(f))
151
194
  if (existingFiles.length > 0) {
152
195
  pythonFiles = existingFiles.join(" ")
153
196
  }
@@ -157,7 +200,7 @@ export const shift_left_verify = tool({
157
200
  }
158
201
 
159
202
  try {
160
- execSync(`ruff check ${pythonFiles} --quiet`, { cwd: root, stdio: "pipe", timeout: 20000 })
203
+ execSync(`ruff check ${pythonFiles} --quiet`, { cwd: targetPath, stdio: "pipe", timeout: 20000 })
161
204
  } catch (ruffErr: any) {
162
205
  const output = ruffErr.stdout?.toString() || ruffErr.stderr?.toString() || ""
163
206
  if (output.includes("command not found") || ruffErr.message?.includes("ENOENT") || ruffErr.status === 127) {
@@ -212,7 +255,7 @@ export const shift_left_verify = tool({
212
255
 
213
256
  // 1. Run tsc --noEmit
214
257
  try {
215
- execSync("npx tsc --noEmit", { cwd: root, stdio: "pipe", timeout: 60000 })
258
+ execSync("npx tsc --noEmit", { cwd: targetPath, stdio: "pipe", timeout: 60000 })
216
259
  } catch (e: any) {
217
260
  const rawOutput = e.stdout?.toString() || e.stderr?.toString() || ""
218
261
  const errors = parseSemanticErrors(rawOutput, "tsc")
@@ -233,7 +276,9 @@ export const shift_left_verify = tool({
233
276
  if (fs.existsSync(contractPath)) {
234
277
  const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"))
235
278
  if (contract && Array.isArray(contract.files_affected) && contract.files_affected.length > 0) {
236
- const existingFiles = contract.files_affected.filter((f: string) => fs.existsSync(path.resolve(root, f)))
279
+ const existingFiles = contract.files_affected
280
+ .filter((f: string) => fs.existsSync(path.resolve(root, f)))
281
+ .map((f: string) => getRelPath(f))
237
282
  if (existingFiles.length > 0) {
238
283
  eslintFiles = existingFiles.join(" ")
239
284
  }
@@ -247,7 +292,7 @@ export const shift_left_verify = tool({
247
292
 
248
293
  try {
249
294
  const out = execSync(`npx eslint ${eslintFiles} --quiet 2>&1 || true`, {
250
- cwd: root,
295
+ cwd: targetPath,
251
296
  encoding: "utf8",
252
297
  timeout: 60000,
253
298
  })
@@ -278,10 +323,13 @@ export const shift_left_verify = tool({
278
323
 
279
324
  // Tool: sdd_generate_tests
280
325
  export const generate_tests = tool({
281
- description: "Autogenera plantillas de pruebas unitarias/integración en tests/unit/ a partir de los escenarios de prueba descritos en el contrato activo de sdd_state.json. No pisa archivos de pruebas existentes.",
326
+ description: "Autogenera plantillas de pruebas unitarias/integración en el directorio corporativo de pruebas a partir de los escenarios de prueba descritos en el contrato activo de sdd_state.json. No pisa archivos de pruebas existentes.",
282
327
  args: {},
283
328
  async execute(args, context) {
284
329
  const root = getRoot(context)
330
+ const targetDir = getTargetDir(root)
331
+ const targetPath = targetDir === "." ? root : path.resolve(root, targetDir)
332
+
285
333
  const stateFile = path.resolve(root, ".openspec/sdd_state.json")
286
334
  if (!fs.existsSync(stateFile)) {
287
335
  return JSON.stringify({ success: false, error: "sdd_state.json no existe. Inicia una sesión SDD primero." }, null, 2)
@@ -311,48 +359,76 @@ export const generate_tests = tool({
311
359
  const created: string[] = []
312
360
  const skipped: string[] = []
313
361
 
362
+ const isPythonProject = fs.existsSync(path.resolve(targetPath, "requirements.txt")) || fs.existsSync(path.resolve(targetPath, "pyproject.toml"))
363
+ const test_dir = isPythonProject
364
+ ? path.resolve(targetPath, "tests/unit")
365
+ : path.resolve(targetPath, "src/__tests__")
366
+
367
+ if (!fs.existsSync(test_dir)) {
368
+ fs.mkdirSync(test_dir, { recursive: true })
369
+ }
370
+
314
371
  for (const [feature, scenarios] of Object.entries(grouped_tests)) {
315
372
  const clean_feature = feature.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "")
316
373
  const hasReact = scenarios.some(s => s.type === "unit" || s.type === "visual")
317
374
  const ext = hasReact ? "tsx" : "ts"
318
- const test_dir = path.resolve(root, "tests", "unit")
319
- if (!fs.existsSync(test_dir)) {
320
- fs.mkdirSync(test_dir, { recursive: true })
321
- }
322
- const test_file = path.join(test_dir, `${clean_feature}.test.${ext}`)
375
+
376
+ const filename = isPythonProject
377
+ ? `test_${clean_feature}.py`
378
+ : `${feature}.test.${ext}`
379
+ const test_file = path.join(test_dir, filename)
380
+ const relative_test_path = path.relative(root, test_file)
323
381
 
324
382
  if (fs.existsSync(test_file)) {
325
- skipped.push(`tests/unit/${clean_feature}.test.${ext}`)
383
+ skipped.push(relative_test_path)
326
384
  continue
327
385
  }
328
386
 
329
387
  const lines: string[] = []
330
- lines.push('import { describe, it, expect } from "vitest";')
331
- if (hasReact) {
332
- lines.push('import { render, screen } from "@testing-library/react";')
333
- lines.push('import userEvent from "@testing-library/user-event";')
334
- lines.push(`// import ${feature} from "@/components/blocks/${clean_feature}";`)
335
- }
336
- lines.push("")
337
- lines.push(`describe("${feature} Tests (Contract Scenarios)", () => {`)
338
-
339
- for (const s of scenarios) {
340
- const tid = s.id || "TS-XX"
341
- const name = s.name || "Test case"
342
- lines.push(` // ${tid}: ${name}`)
343
- lines.push(` // Given: ${s.given || ""}`)
344
- lines.push(` // When: ${s.when || ""}`)
345
- lines.push(` // Then: ${s.then || ""}`)
346
- lines.push(` it("${tid}: ${name}", async () => {`)
347
- lines.push(' // TODO: Implement actual contract assertions')
348
- lines.push(' expect(true).toBe(true);')
349
- lines.push(' });')
388
+ if (isPythonProject) {
389
+ lines.push("import pytest")
390
+ lines.push(`from src.main import app # o tu router correspondiente`)
350
391
  lines.push("")
392
+ lines.push(`# tests para ${feature}`)
393
+ for (const s of scenarios) {
394
+ const tid = s.id || "TS-XX"
395
+ const name = s.name || "Test case"
396
+ const test_func_name = `test_${tid.toLowerCase().replace(/-/g, "_")}_${clean_feature.replace(/-/g, "_")}`
397
+ lines.push(`def ${test_func_name}():`)
398
+ lines.push(` # Given: ${s.given || ""}`)
399
+ lines.push(` # When: ${s.when || ""}`)
400
+ lines.push(` # Then: ${s.then || ""}`)
401
+ lines.push(" assert True")
402
+ lines.push("")
403
+ }
404
+ } else {
405
+ lines.push('import { describe, it, expect } from "vitest";')
406
+ if (hasReact) {
407
+ lines.push('import { render, screen } from "@testing-library/react";')
408
+ lines.push('import userEvent from "@testing-library/user-event";')
409
+ lines.push(`// import { ${feature} } from "@/components/blocks/${feature}";`)
410
+ }
411
+ lines.push("")
412
+ lines.push(`describe("${feature} Tests (Contract Scenarios)", () => {`)
413
+
414
+ for (const s of scenarios) {
415
+ const tid = s.id || "TS-XX"
416
+ const name = s.name || "Test case"
417
+ lines.push(` // ${tid}: ${name}`)
418
+ lines.push(` // Given: ${s.given || ""}`)
419
+ lines.push(` // When: ${s.when || ""}`)
420
+ lines.push(` // Then: ${s.then || ""}`)
421
+ lines.push(` it("${tid}: ${name}", async () => {`)
422
+ lines.push(' // TODO: Implement actual contract assertions')
423
+ lines.push(' expect(true).toBe(true);')
424
+ lines.push(' });')
425
+ lines.push("")
426
+ }
427
+ lines.push("});")
351
428
  }
352
- lines.push("});")
353
429
 
354
430
  fs.writeFileSync(test_file, lines.join("\n").trim() + "\n", "utf8")
355
- created.push(`tests/unit/${clean_feature}.test.${ext}`)
431
+ created.push(relative_test_path)
356
432
  }
357
433
 
358
434
  return JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zugzbot",
3
- "version": "1.0.36",
3
+ "version": "1.0.37",
4
4
  "description": "Fácil instalador del arnés SDD de Zugzbot para proyectos OpenCode",
5
5
  "type": "module",
6
6
  "bin": {