@kleber.mottajr/juninho 1.3.0 → 2.0.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 (37) hide show
  1. package/README.md +14 -15
  2. package/dist/config.d.ts +29 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +57 -3
  5. package/dist/config.js.map +1 -1
  6. package/dist/installer.d.ts.map +1 -1
  7. package/dist/installer.js +159 -53
  8. package/dist/installer.js.map +1 -1
  9. package/dist/project-types.d.ts.map +1 -1
  10. package/dist/project-types.js +6 -0
  11. package/dist/project-types.js.map +1 -1
  12. package/dist/templates/agents.d.ts.map +1 -1
  13. package/dist/templates/agents.js +925 -162
  14. package/dist/templates/agents.js.map +1 -1
  15. package/dist/templates/commands.d.ts.map +1 -1
  16. package/dist/templates/commands.js +747 -626
  17. package/dist/templates/commands.js.map +1 -1
  18. package/dist/templates/docs.d.ts.map +1 -1
  19. package/dist/templates/docs.js +49 -24
  20. package/dist/templates/docs.js.map +1 -1
  21. package/dist/templates/lib.d.ts +2 -0
  22. package/dist/templates/lib.d.ts.map +1 -0
  23. package/dist/templates/lib.js +506 -0
  24. package/dist/templates/lib.js.map +1 -0
  25. package/dist/templates/plugins.d.ts.map +1 -1
  26. package/dist/templates/plugins.js +2530 -856
  27. package/dist/templates/plugins.js.map +1 -1
  28. package/dist/templates/skills.d.ts.map +1 -1
  29. package/dist/templates/skills.js +30 -0
  30. package/dist/templates/skills.js.map +1 -1
  31. package/dist/templates/state.d.ts.map +1 -1
  32. package/dist/templates/state.js +159 -186
  33. package/dist/templates/state.js.map +1 -1
  34. package/dist/templates/support-scripts.d.ts.map +1 -1
  35. package/dist/templates/support-scripts.js +1014 -249
  36. package/dist/templates/support-scripts.js.map +1 -1
  37. package/package.json +8 -2
@@ -11,7 +11,12 @@ function writeSupportScripts(projectDir, projectType = "node-nextjs", isKotlin =
11
11
  writeExecutable(path_1.default.join(scriptsDir, "pre-commit.sh"), PRE_COMMIT);
12
12
  writeExecutable(path_1.default.join(scriptsDir, "lint-structure.sh"), lintStructure(projectType, isKotlin, lintTool));
13
13
  writeExecutable(path_1.default.join(scriptsDir, "test-related.sh"), testRelated(projectType, isKotlin));
14
+ writeExecutable(path_1.default.join(scriptsDir, "run-test-scope.sh"), runTestScope(projectType, isKotlin));
14
15
  writeExecutable(path_1.default.join(scriptsDir, "check-all.sh"), checkAll(projectType, isKotlin, lintTool));
16
+ writeExecutable(path_1.default.join(scriptsDir, "scaffold-spec-state.sh"), SCAFFOLD_SPEC_STATE);
17
+ writeExecutable(path_1.default.join(scriptsDir, "harness-feature-integration.sh"), HARNESS_FEATURE_INTEGRATION);
18
+ writeExecutable(path_1.default.join(scriptsDir, "build-verify.sh"), buildVerify(projectType, isKotlin));
19
+ writeExecutable(path_1.default.join(scriptsDir, "install-git-hooks.sh"), INSTALL_GIT_HOOKS);
15
20
  }
16
21
  function writeExecutable(filePath, content) {
17
22
  (0, fs_1.writeFileSync)(filePath, content);
@@ -44,7 +49,6 @@ echo "[juninho:pre-commit] Running related tests..."
44
49
 
45
50
  echo "[juninho:pre-commit] Local checks passed"
46
51
  `;
47
- /* ─── Lint Structure ─── */
48
52
  function lintStructure(projectType, isKotlin, lintTool) {
49
53
  const header = `#!/bin/sh
50
54
  set -e
@@ -72,16 +76,14 @@ fi
72
76
  case "go":
73
77
  return header + lintGoBody(lintTool);
74
78
  case "java":
75
- return isKotlin
76
- ? header + lintKotlinBody(lintTool)
77
- : header + lintJavaBody(lintTool);
79
+ return isKotlin ? header + lintKotlinBody(lintTool) : header + lintJavaBody(lintTool);
78
80
  case "generic":
79
81
  return header + lintGenericBody();
80
82
  }
81
83
  }
82
84
  function lintNodeBody(lintTool) {
83
85
  const priority = lintTool
84
- ? `# Priority linter: ${lintTool}\nif command -v npx >/dev/null 2>&1; then\n npx ${lintTool} $FILES\n exit 0\nfi\n\n`
86
+ ? `if command -v npx >/dev/null 2>&1; then\n npx ${lintTool} $FILES\n exit 0\nfi\n\n`
85
87
  : "";
86
88
  return `${priority}has_package_script() {
87
89
  [ -f package.json ] || return 1
@@ -104,15 +106,14 @@ if command -v npx >/dev/null 2>&1 && npx --yes eslint --version >/dev/null 2>&1;
104
106
  fi
105
107
 
106
108
  echo "[juninho:lint-structure] No structure lint configured."
107
- echo "[juninho:lint-structure] Customize .opencode/scripts/lint-structure.sh or run /j.init-deep."
109
+ echo "[juninho:lint-structure] Customize .opencode/scripts/lint-structure.sh or run /j.finish-setup."
108
110
  `;
109
111
  }
110
112
  function lintPythonBody(lintTool) {
111
113
  const priority = lintTool
112
- ? `# Priority linter: ${lintTool}\nif command -v ${lintTool} >/dev/null 2>&1; then\n ${lintTool} check $FILES\n exit 0\nfi\n\n`
114
+ ? `if command -v ${lintTool} >/dev/null 2>&1; then\n ${lintTool} check $FILES\n exit 0\nfi\n\n`
113
115
  : "";
114
- return `${priority}# Python lint chain: ruff flake8 → pylint
115
- if command -v ruff >/dev/null 2>&1; then
116
+ return `${priority}if command -v ruff >/dev/null 2>&1; then
116
117
  ruff check $FILES
117
118
  exit 0
118
119
  fi
@@ -132,10 +133,9 @@ echo "[juninho:lint-structure] No Python linter found. Install ruff, flake8, or
132
133
  }
133
134
  function lintGoBody(lintTool) {
134
135
  const priority = lintTool
135
- ? `# Priority linter: ${lintTool}\nif command -v ${lintTool} >/dev/null 2>&1; then\n ${lintTool} run\n exit 0\nfi\n\n`
136
+ ? `if command -v ${lintTool} >/dev/null 2>&1; then\n ${lintTool} run\n exit 0\nfi\n\n`
136
137
  : "";
137
- return `${priority}# Go lint chain: golangci-lint go vet
138
- if command -v golangci-lint >/dev/null 2>&1; then
138
+ return `${priority}if command -v golangci-lint >/dev/null 2>&1; then
139
139
  golangci-lint run
140
140
  exit 0
141
141
  fi
@@ -143,12 +143,8 @@ fi
143
143
  go vet ./...
144
144
  `;
145
145
  }
146
- function lintJavaBody(lintTool) {
147
- const priority = lintTool
148
- ? `# Priority linter: ${lintTool}\n`
149
- : "";
150
- return `${priority}# Java lint chain: gradle checkstyle → maven checkstyle
151
- if [ -x "./gradlew" ]; then
146
+ function lintJavaBody(_lintTool) {
147
+ return `if [ -x "./gradlew" ]; then
152
148
  ./gradlew checkstyleMain 2>/dev/null && exit 0
153
149
  echo "[juninho:lint-structure] Gradle checkstyle not configured. Add checkstyle plugin to build.gradle."
154
150
  exit 0
@@ -163,33 +159,24 @@ fi
163
159
  echo "[juninho:lint-structure] No Java build tool found."
164
160
  `;
165
161
  }
166
- function lintKotlinBody(lintTool) {
167
- const priority = lintTool
168
- ? `# Priority linter: ${lintTool}\n`
169
- : "";
170
- return `${priority}# Kotlin lint chain: ktlint → detekt → compileKotlin warnings
171
- if [ -x "./gradlew" ]; then
172
- # Try ktlint first (most common for Kotlin formatting/style)
162
+ function lintKotlinBody(_lintTool) {
163
+ return `if [ -x "./gradlew" ]; then
173
164
  if ./gradlew tasks --all 2>/dev/null | grep -q "ktlintCheck"; then
174
165
  ./gradlew ktlintCheck
175
166
  exit 0
176
167
  fi
177
168
 
178
- # Try detekt (static analysis)
179
169
  if ./gradlew tasks --all 2>/dev/null | grep -q "detekt"; then
180
170
  ./gradlew detekt
181
171
  exit 0
182
172
  fi
183
173
 
184
- # Fallback: compile with warnings treated as errors
185
174
  ./gradlew compileKotlin 2>&1
186
175
  exit 0
187
176
  fi
188
177
 
189
178
  if [ -x "./mvnw" ]; then
190
- # Maven ktlint plugin
191
179
  ./mvnw antrun:run@ktlint-check 2>/dev/null && exit 0
192
- # Fallback to compile
193
180
  ./mvnw compile 2>&1
194
181
  exit 0
195
182
  fi
@@ -199,8 +186,7 @@ echo "[juninho:lint-structure] Add ktlint or detekt Gradle plugin for structural
199
186
  `;
200
187
  }
201
188
  function lintGenericBody() {
202
- return `# Generic: try common linters across stacks
203
- if [ -f package.json ]; then
189
+ return `if [ -f package.json ]; then
204
190
  if command -v npx >/dev/null 2>&1; then
205
191
  npx eslint --max-warnings=0 $FILES 2>/dev/null && exit 0
206
192
  fi
@@ -221,43 +207,36 @@ fi
221
207
  echo "[juninho:lint-structure] No linter detected. Customize .opencode/scripts/lint-structure.sh."
222
208
  `;
223
209
  }
224
- /* ─── Test Related ─── */
225
210
  function testRelated(projectType, isKotlin) {
226
- const header = `#!/bin/sh
211
+ switch (projectType) {
212
+ case "node-nextjs":
213
+ case "node-generic":
214
+ return nodeTestRelated();
215
+ case "python":
216
+ return pythonTestRelated();
217
+ case "go":
218
+ return goTestRelated();
219
+ case "java":
220
+ return isKotlin ? kotlinTestRelated() : javaTestRelated();
221
+ case "generic":
222
+ return genericTestRelated();
223
+ }
224
+ }
225
+ function nodeTestRelated() {
226
+ return `#!/bin/sh
227
227
  set -e
228
228
 
229
229
  ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
230
230
  cd "$ROOT_DIR"
231
231
 
232
- staged_files_as_args() {
233
- printf '%s\\n' "$JUNINHO_STAGED_FILES" | sed '/^$/d' | tr '\\n' ' '
234
- }
235
-
236
- FILES="$(staged_files_as_args)"
232
+ FILES="\${JUNINHO_STAGED_FILES:-}"
237
233
 
238
234
  if [ -z "$FILES" ]; then
239
235
  echo "[juninho:test-related] No staged files. Skipping."
240
236
  exit 0
241
237
  fi
242
- `;
243
- switch (projectType) {
244
- case "node-nextjs":
245
- case "node-generic":
246
- return header + testNodeBody();
247
- case "python":
248
- return header + testPythonBody();
249
- case "go":
250
- return header + testGoBody();
251
- case "java":
252
- return isKotlin
253
- ? header + testKotlinBody()
254
- : header + testJavaBody();
255
- case "generic":
256
- return header + testGenericBody();
257
- }
258
- }
259
- function testNodeBody() {
260
- return `has_package_script() {
238
+
239
+ has_package_script() {
261
240
  [ -f package.json ] || return 1
262
241
  node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); process.exit(pkg.scripts && pkg.scripts[process.argv[1]] ? 0 : 1)" "$1" >/dev/null 2>&1
263
242
  }
@@ -278,46 +257,66 @@ if command -v npx >/dev/null 2>&1 && npx --yes vitest --version >/dev/null 2>&1;
278
257
  fi
279
258
 
280
259
  echo "[juninho:test-related] No related-test command configured."
281
- echo "[juninho:test-related] Customize .opencode/scripts/test-related.sh or run /j.init-deep."
282
- `;
283
- }
284
- function testPythonBody() {
285
- // Use string concatenation to avoid template literal escaping issues with shell variables
286
- const lines = [
287
- '# Python: run pytest scoped to staged files',
288
- 'PY_FILES=""',
289
- 'for f in $FILES; do',
290
- ' case "$f" in *.py) PY_FILES="$PY_FILES $f" ;; esac',
291
- 'done',
292
- '',
293
- 'if [ -z "$PY_FILES" ]; then',
294
- ' echo "[juninho:test-related] No Python files staged. Skipping tests."',
295
- ' exit 0',
296
- 'fi',
297
- '',
298
- '# Derive test file paths from source files',
299
- 'TEST_TARGETS=""',
300
- 'for f in $PY_FILES; do',
301
- ' dir=$(dirname "$f")',
302
- ' base=$(basename "$f" .py)',
303
- ' for candidate in "${dir}/test_${base}.py" "${dir}/${base}_test.py" "tests/test_${base}.py" "tests/${dir}/test_${base}.py"; do',
304
- ' if [ -f "$candidate" ]; then',
305
- ' TEST_TARGETS="$TEST_TARGETS $candidate"',
306
- ' fi',
307
- ' done',
308
- 'done',
309
- '',
310
- 'if [ -n "$TEST_TARGETS" ]; then',
311
- ' pytest $TEST_TARGETS --no-header -q 2>/dev/null && exit 0',
312
- ' python -m pytest $TEST_TARGETS --no-header -q 2>/dev/null && exit 0',
313
- 'fi',
314
- '',
315
- 'echo "[juninho:test-related] No related tests found for staged Python files."',
316
- ];
317
- return lines.join('\n') + '\n';
318
- }
319
- function testGoBody() {
320
- return `# Go: run tests for packages containing staged files
260
+ echo "[juninho:test-related] Customize .opencode/scripts/test-related.sh or run /j.finish-setup."
261
+ `;
262
+ }
263
+ function pythonTestRelated() {
264
+ return `#!/bin/sh
265
+ set -e
266
+
267
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
268
+ cd "$ROOT_DIR"
269
+
270
+ FILES="\${JUNINHO_STAGED_FILES:-}"
271
+
272
+ if [ -z "$FILES" ]; then
273
+ echo "[juninho:test-related] No staged files. Skipping."
274
+ exit 0
275
+ fi
276
+
277
+ PY_FILES=""
278
+ for f in $FILES; do
279
+ case "$f" in *.py) PY_FILES="$PY_FILES $f" ;; esac
280
+ done
281
+
282
+ if [ -z "$PY_FILES" ]; then
283
+ echo "[juninho:test-related] No Python files staged. Skipping tests."
284
+ exit 0
285
+ fi
286
+
287
+ TEST_TARGETS=""
288
+ for f in $PY_FILES; do
289
+ dir=$(dirname "$f")
290
+ base=$(basename "$f" .py)
291
+ for candidate in "\${dir}/test_\${base}.py" "\${dir}/\${base}_test.py" "tests/test_\${base}.py" "tests/\${dir}/test_\${base}.py"; do
292
+ if [ -f "$candidate" ]; then
293
+ TEST_TARGETS="$TEST_TARGETS $candidate"
294
+ fi
295
+ done
296
+ done
297
+
298
+ if [ -n "$TEST_TARGETS" ]; then
299
+ pytest $TEST_TARGETS --no-header -q 2>/dev/null && exit 0
300
+ python -m pytest $TEST_TARGETS --no-header -q 2>/dev/null && exit 0
301
+ fi
302
+
303
+ echo "[juninho:test-related] No related tests found for staged Python files."
304
+ `;
305
+ }
306
+ function goTestRelated() {
307
+ return `#!/bin/sh
308
+ set -e
309
+
310
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
311
+ cd "$ROOT_DIR"
312
+
313
+ FILES="\${JUNINHO_STAGED_FILES:-}"
314
+
315
+ if [ -z "$FILES" ]; then
316
+ echo "[juninho:test-related] No staged files. Skipping."
317
+ exit 0
318
+ fi
319
+
321
320
  GO_FILES=""
322
321
  for f in $FILES; do
323
322
  case "$f" in *.go) GO_FILES="$GO_FILES $f" ;; esac
@@ -328,7 +327,6 @@ if [ -z "$GO_FILES" ]; then
328
327
  exit 0
329
328
  fi
330
329
 
331
- # Extract unique package directories
332
330
  PACKAGES=""
333
331
  for f in $GO_FILES; do
334
332
  pkg="./$(dirname "$f")"
@@ -341,102 +339,132 @@ done
341
339
  go test -count=1 $PACKAGES
342
340
  `;
343
341
  }
344
- function testJavaBody() {
345
- const lines = [
346
- '# Java: run tests scoped to staged files',
347
- 'JAVA_FILES=""',
348
- 'for f in $FILES; do',
349
- ' case "$f" in *.java) JAVA_FILES="$JAVA_FILES $f" ;; esac',
350
- 'done',
351
- '',
352
- 'if [ -z "$JAVA_FILES" ]; then',
353
- ' echo "[juninho:test-related] No Java files staged. Skipping tests."',
354
- ' exit 0',
355
- 'fi',
356
- '',
357
- '# Extract test class names from staged source files',
358
- 'TEST_FILTER=""',
359
- 'for f in $JAVA_FILES; do',
360
- ' base=$(basename "$f" .java)',
361
- ' case "$base" in *Test|*Tests|*IT)',
362
- ' TEST_FILTER="$TEST_FILTER --tests *${base}"',
363
- ' continue',
364
- ' ;;',
365
- ' esac',
366
- ' TEST_FILTER="$TEST_FILTER --tests *${base}Test"',
367
- 'done',
368
- '',
369
- 'if [ -x "./gradlew" ]; then',
370
- ' ./gradlew test $TEST_FILTER 2>/dev/null || ./gradlew test',
371
- ' exit 0',
372
- 'fi',
373
- '',
374
- 'if [ -x "./mvnw" ]; then',
375
- ' MAVEN_FILTER=""',
376
- ' for f in $JAVA_FILES; do',
377
- ' base=$(basename "$f" .java)',
378
- ' MAVEN_FILTER="${MAVEN_FILTER},${base}Test"',
379
- ' done',
380
- ' MAVEN_FILTER=$(echo "$MAVEN_FILTER" | sed \'s/^,//\')',
381
- ' ./mvnw test -Dtest="$MAVEN_FILTER" 2>/dev/null || ./mvnw test',
382
- ' exit 0',
383
- 'fi',
384
- '',
385
- 'echo "[juninho:test-related] No Java build tool found."',
386
- ];
387
- return lines.join('\n') + '\n';
388
- }
389
- function testKotlinBody() {
390
- const lines = [
391
- '# Kotlin: run tests scoped to staged files',
392
- 'KT_FILES=""',
393
- 'JAVA_FILES=""',
394
- 'for f in $FILES; do',
395
- ' case "$f" in',
396
- ' *.kt|*.kts) KT_FILES="$KT_FILES $f" ;;',
397
- ' *.java) JAVA_FILES="$JAVA_FILES $f" ;;',
398
- ' esac',
399
- 'done',
400
- '',
401
- 'ALL_FILES="$KT_FILES $JAVA_FILES"',
402
- 'if [ -z "$(echo "$ALL_FILES" | tr -d \' \')" ]; then',
403
- ' echo "[juninho:test-related] No Kotlin/Java files staged. Skipping tests."',
404
- ' exit 0',
405
- 'fi',
406
- '',
407
- '# Extract test class names from staged source files',
408
- 'TEST_FILTER=""',
409
- 'for f in $KT_FILES $JAVA_FILES; do',
410
- ' ext="${f##*.}"',
411
- ' base=$(basename "$f" ".$ext")',
412
- ' case "$base" in *Test|*Tests|*IT|*Spec)',
413
- ' TEST_FILTER="$TEST_FILTER --tests *${base}"',
414
- ' continue',
415
- ' ;;',
416
- ' esac',
417
- ' TEST_FILTER="$TEST_FILTER --tests *${base}Test"',
418
- 'done',
419
- '',
420
- 'if [ -x "./gradlew" ]; then',
421
- ' if [ -n "$TEST_FILTER" ]; then',
422
- ' ./gradlew test $TEST_FILTER 2>/dev/null || ./gradlew test',
423
- ' else',
424
- ' ./gradlew test',
425
- ' fi',
426
- ' exit 0',
427
- 'fi',
428
- '',
429
- 'if [ -x "./mvnw" ]; then',
430
- ' ./mvnw test',
431
- ' exit 0',
432
- 'fi',
433
- '',
434
- 'echo "[juninho:test-related] No Kotlin build tool found."',
435
- ];
436
- return lines.join('\n') + '\n';
437
- }
438
- function testGenericBody() {
439
- return `# Generic: try common test runners
342
+ function javaTestRelated() {
343
+ return `#!/bin/sh
344
+ set -e
345
+
346
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
347
+ cd "$ROOT_DIR"
348
+
349
+ FILES="\${JUNINHO_STAGED_FILES:-}"
350
+
351
+ if [ -z "$FILES" ]; then
352
+ echo "[juninho:test-related] No staged files. Skipping."
353
+ exit 0
354
+ fi
355
+
356
+ JAVA_FILES=""
357
+ for f in $FILES; do
358
+ case "$f" in *.java) JAVA_FILES="$JAVA_FILES $f" ;; esac
359
+ done
360
+
361
+ if [ -z "$JAVA_FILES" ]; then
362
+ echo "[juninho:test-related] No Java files staged. Skipping tests."
363
+ exit 0
364
+ fi
365
+
366
+ TEST_FILTER=""
367
+ for f in $JAVA_FILES; do
368
+ base=$(basename "$f" .java)
369
+ case "$base" in *Test|*Tests|*IT)
370
+ TEST_FILTER="$TEST_FILTER --tests *\${base}"
371
+ continue
372
+ ;;
373
+ esac
374
+ TEST_FILTER="$TEST_FILTER --tests *\${base}Test"
375
+ done
376
+
377
+ if [ -x "./gradlew" ]; then
378
+ ./gradlew test $TEST_FILTER 2>/dev/null || ./gradlew test
379
+ exit 0
380
+ fi
381
+
382
+ if [ -x "./mvnw" ]; then
383
+ MAVEN_FILTER=""
384
+ for f in $JAVA_FILES; do
385
+ base=$(basename "$f" .java)
386
+ MAVEN_FILTER="\${MAVEN_FILTER},\${base}Test"
387
+ done
388
+ MAVEN_FILTER=$(echo "$MAVEN_FILTER" | sed 's/^,//')
389
+ ./mvnw test -Dtest="$MAVEN_FILTER" 2>/dev/null || ./mvnw test
390
+ exit 0
391
+ fi
392
+
393
+ echo "[juninho:test-related] No Java build tool found."
394
+ `;
395
+ }
396
+ function kotlinTestRelated() {
397
+ return `#!/bin/sh
398
+ set -e
399
+
400
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
401
+ cd "$ROOT_DIR"
402
+
403
+ FILES="\${JUNINHO_STAGED_FILES:-}"
404
+
405
+ if [ -z "$FILES" ]; then
406
+ echo "[juninho:test-related] No staged files. Skipping."
407
+ exit 0
408
+ fi
409
+
410
+ KT_FILES=""
411
+ JAVA_FILES=""
412
+ for f in $FILES; do
413
+ case "$f" in
414
+ *.kt|*.kts) KT_FILES="$KT_FILES $f" ;;
415
+ *.java) JAVA_FILES="$JAVA_FILES $f" ;;
416
+ esac
417
+ done
418
+
419
+ ALL_FILES="$KT_FILES $JAVA_FILES"
420
+ if [ -z "$(echo "$ALL_FILES" | tr -d ' ')" ]; then
421
+ echo "[juninho:test-related] No Kotlin/Java files staged. Skipping tests."
422
+ exit 0
423
+ fi
424
+
425
+ TEST_FILTER=""
426
+ for f in $KT_FILES $JAVA_FILES; do
427
+ ext="\${f##*.}"
428
+ base=$(basename "$f" ".$ext")
429
+ case "$base" in *Test|*Tests|*IT|*Spec)
430
+ TEST_FILTER="$TEST_FILTER --tests *\${base}"
431
+ continue
432
+ ;;
433
+ esac
434
+ TEST_FILTER="$TEST_FILTER --tests *\${base}Test"
435
+ done
436
+
437
+ if [ -x "./gradlew" ]; then
438
+ if [ -n "$TEST_FILTER" ]; then
439
+ ./gradlew test $TEST_FILTER 2>/dev/null || ./gradlew test
440
+ else
441
+ ./gradlew test
442
+ fi
443
+ exit 0
444
+ fi
445
+
446
+ if [ -x "./mvnw" ]; then
447
+ ./mvnw test
448
+ exit 0
449
+ fi
450
+
451
+ echo "[juninho:test-related] No Kotlin build tool found."
452
+ `;
453
+ }
454
+ function genericTestRelated() {
455
+ return `#!/bin/sh
456
+ set -e
457
+
458
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
459
+ cd "$ROOT_DIR"
460
+
461
+ FILES="\${JUNINHO_STAGED_FILES:-}"
462
+
463
+ if [ -z "$FILES" ]; then
464
+ echo "[juninho:test-related] No staged files. Skipping."
465
+ exit 0
466
+ fi
467
+
440
468
  if [ -f package.json ]; then
441
469
  if command -v npx >/dev/null 2>&1; then
442
470
  npx jest --findRelatedTests --passWithNoTests $FILES 2>/dev/null && exit 0
@@ -459,13 +487,34 @@ fi
459
487
  echo "[juninho:test-related] No test runner detected. Customize .opencode/scripts/test-related.sh."
460
488
  `;
461
489
  }
462
- /* ─── Check All ─── */
490
+ function runTestScope(projectType, isKotlin) {
491
+ switch (projectType) {
492
+ case "node-nextjs":
493
+ case "node-generic":
494
+ return RUN_TEST_SCOPE_GENERIC_NODE;
495
+ case "python":
496
+ return RUN_TEST_SCOPE_PYTHON;
497
+ case "go":
498
+ return RUN_TEST_SCOPE_GO;
499
+ case "java":
500
+ return isKotlin ? RUN_TEST_SCOPE_KOTLIN : RUN_TEST_SCOPE_JAVA;
501
+ case "generic":
502
+ return RUN_TEST_SCOPE_GENERIC;
503
+ }
504
+ }
463
505
  function checkAll(projectType, isKotlin, lintTool) {
464
506
  const header = `#!/bin/sh
465
507
  set -e
466
508
 
467
509
  ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
468
510
  cd "$ROOT_DIR"
511
+
512
+ sh "$ROOT_DIR/.opencode/scripts/harness-feature-integration.sh" switch-active >/dev/null 2>&1 || true
513
+
514
+ CURRENT_BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
515
+ if [ -n "$CURRENT_BRANCH" ]; then
516
+ echo "[juninho:check-all] Running on branch: $CURRENT_BRANCH"
517
+ fi
469
518
  `;
470
519
  switch (projectType) {
471
520
  case "node-nextjs":
@@ -476,9 +525,7 @@ cd "$ROOT_DIR"
476
525
  case "go":
477
526
  return header + checkAllGoBody(lintTool);
478
527
  case "java":
479
- return isKotlin
480
- ? header + checkAllKotlinBody(lintTool)
481
- : header + checkAllJavaBody(lintTool);
528
+ return isKotlin ? header + checkAllKotlinBody(lintTool) : header + checkAllJavaBody(lintTool);
482
529
  case "generic":
483
530
  return header + checkAllGenericBody();
484
531
  }
@@ -489,35 +536,35 @@ function checkAllNodeBody() {
489
536
  node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); process.exit(pkg.scripts && pkg.scripts[process.argv[1]] ? 0 : 1)" "$1" >/dev/null 2>&1
490
537
  }
491
538
 
539
+ echo "[juninho:check-all] Running formatting checks..."
540
+ if has_package_script "lint"; then
541
+ npm run lint
542
+ elif has_package_script "check:all"; then
543
+ npm run check:all
544
+ fi
545
+
546
+ echo "[juninho:check-all] Running repo-wide tests..."
492
547
  if has_package_script "check:all"; then
493
548
  npm run check:all
494
549
  exit 0
495
550
  fi
496
551
 
497
- if [ -f package.json ]; then
498
- if has_package_script "typecheck"; then
499
- npm run typecheck
500
- fi
501
-
502
- if has_package_script "lint"; then
503
- npm run lint
504
- fi
505
-
506
- if has_package_script "test"; then
507
- npm test -- --runInBand
508
- fi
552
+ if has_package_script "typecheck"; then
553
+ npm run typecheck
554
+ fi
509
555
 
556
+ if has_package_script "test"; then
557
+ npm test
510
558
  exit 0
511
559
  fi
512
560
 
513
561
  echo "[juninho:check-all] No full verification command configured."
514
- echo "[juninho:check-all] Customize .opencode/scripts/check-all.sh or run /j.init-deep."
562
+ echo "[juninho:check-all] Customize .opencode/scripts/check-all.sh or run /j.finish-setup."
515
563
  `;
516
564
  }
517
565
  function checkAllPythonBody(lintTool) {
518
566
  const lint = lintTool ?? "ruff";
519
- return `# Python: lint + test
520
- echo "[juninho:check-all] Running lint..."
567
+ return `echo "[juninho:check-all] Running formatting checks..."
521
568
  if command -v ${lint} >/dev/null 2>&1; then
522
569
  ${lint} check .
523
570
  elif command -v ruff >/dev/null 2>&1; then
@@ -526,82 +573,215 @@ elif command -v flake8 >/dev/null 2>&1; then
526
573
  flake8 .
527
574
  fi
528
575
 
529
- echo "[juninho:check-all] Running tests..."
530
- if command -v pytest >/dev/null 2>&1; then
531
- pytest
532
- elif command -v python >/dev/null 2>&1; then
533
- python -m pytest
534
- fi
576
+ echo "[juninho:check-all] Running repo-wide tests..."
577
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
535
578
  `;
536
579
  }
537
580
  function checkAllGoBody(lintTool) {
538
581
  const lint = lintTool ?? "golangci-lint";
539
- return `# Go: vet + lint + test
540
- echo "[juninho:check-all] Running go vet..."
582
+ return `echo "[juninho:check-all] Running formatting checks..."
541
583
  go vet ./...
542
-
543
- echo "[juninho:check-all] Running lint..."
544
584
  if command -v ${lint} >/dev/null 2>&1; then
545
585
  ${lint} run
546
586
  fi
547
587
 
548
- echo "[juninho:check-all] Running tests..."
549
- go test ./...
588
+ echo "[juninho:check-all] Running repo-wide tests..."
589
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
550
590
  `;
551
591
  }
552
592
  function checkAllJavaBody(lintTool) {
553
- return `# Java: full build with tests
593
+ return `echo "[juninho:check-all] Running formatting checks..."
554
594
  if [ -x "./gradlew" ]; then
555
- ${lintTool ? `echo "[juninho:check-all] Running lint..."\n ./gradlew checkstyleMain 2>/dev/null || true\n ` : ""}echo "[juninho:check-all] Running tests..."
556
- ./gradlew test
595
+ ${lintTool ? "./gradlew checkstyleMain 2>/dev/null || true\n" : ""}echo "[juninho:check-all] Running repo-wide tests..."
596
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
557
597
  exit 0
558
598
  fi
559
599
 
560
600
  if [ -x "./mvnw" ]; then
561
- ${lintTool ? `echo "[juninho:check-all] Running lint..."\n ./mvnw checkstyle:check 2>/dev/null || true\n ` : ""}echo "[juninho:check-all] Running tests..."
562
- ./mvnw test
601
+ ${lintTool ? "./mvnw checkstyle:check 2>/dev/null || true\n" : ""}echo "[juninho:check-all] Running repo-wide tests..."
602
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
563
603
  exit 0
564
604
  fi
565
605
 
566
606
  echo "[juninho:check-all] No Java build tool found."
567
607
  `;
568
608
  }
569
- function checkAllKotlinBody(lintTool) {
570
- return `# Kotlin: lint + compile + test
609
+ function checkAllKotlinBody(_lintTool) {
610
+ return `echo "[juninho:check-all] Running formatting checks..."
571
611
  if [ -x "./gradlew" ]; then
572
- echo "[juninho:check-all] Running Kotlin lint..."
573
- # Try ktlint first, then detekt
574
612
  ./gradlew ktlintCheck 2>/dev/null || ./gradlew detekt 2>/dev/null || true
613
+ fi
614
+
615
+ echo "[juninho:check-all] Running repo-wide tests..."
616
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
617
+ `;
618
+ }
619
+ function checkAllGenericBody() {
620
+ return `echo "[juninho:check-all] Running repo-wide tests..."
621
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
622
+ `;
623
+ }
624
+ const RUN_TEST_SCOPE_GENERIC_NODE = `#!/bin/sh
625
+ set -e
626
+
627
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
628
+ cd "$ROOT_DIR"
629
+
630
+ TEST_SCOPE="\${1:-}"
631
+
632
+ if [ -z "$TEST_SCOPE" ]; then
633
+ echo "[juninho:run-test-scope] Missing test scope. Pass related files or 'full'."
634
+ exit 1
635
+ fi
636
+
637
+ has_package_script() {
638
+ [ -f package.json ] || return 1
639
+ node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); process.exit(pkg.scripts && pkg.scripts[process.argv[1]] ? 0 : 1)" "$1" >/dev/null 2>&1
640
+ }
641
+
642
+ if [ "$TEST_SCOPE" = "full" ]; then
643
+ if has_package_script "check:all"; then
644
+ npm run check:all
645
+ exit 0
646
+ fi
647
+ if has_package_script "test"; then
648
+ npm test -- --runInBand
649
+ exit 0
650
+ fi
651
+ fi
575
652
 
576
- echo "[juninho:check-all] Compiling..."
577
- ./gradlew compileKotlin
653
+ if has_package_script "test:related"; then
654
+ npm run test:related -- $TEST_SCOPE
655
+ exit 0
656
+ fi
657
+
658
+ echo "[juninho:run-test-scope] No test scope runner configured."
659
+ `;
660
+ const RUN_TEST_SCOPE_PYTHON = `#!/bin/sh
661
+ set -e
578
662
 
579
- echo "[juninho:check-all] Running tests..."
580
- ./gradlew test
663
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
664
+ cd "$ROOT_DIR"
665
+
666
+ TEST_SCOPE="\${1:-}"
667
+
668
+ if [ -z "$TEST_SCOPE" ]; then
669
+ echo "[juninho:run-test-scope] Missing test scope. Pass a file path, expression, or 'full'."
670
+ exit 1
671
+ fi
672
+
673
+ if [ "$TEST_SCOPE" = "full" ]; then
674
+ pytest 2>/dev/null || python -m pytest
675
+ exit 0
676
+ fi
677
+
678
+ pytest $TEST_SCOPE 2>/dev/null || python -m pytest $TEST_SCOPE
679
+ `;
680
+ const RUN_TEST_SCOPE_GO = `#!/bin/sh
681
+ set -e
682
+
683
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
684
+ cd "$ROOT_DIR"
685
+
686
+ TEST_SCOPE="\${1:-}"
687
+
688
+ if [ -z "$TEST_SCOPE" ] || [ "$TEST_SCOPE" = "full" ]; then
689
+ go test ./...
690
+ exit 0
691
+ fi
692
+
693
+ go test -count=1 $TEST_SCOPE
694
+ `;
695
+ const RUN_TEST_SCOPE_JAVA = `#!/bin/sh
696
+ set -e
697
+
698
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
699
+ cd "$ROOT_DIR"
700
+
701
+ TEST_SCOPE="\${1:-}"
702
+
703
+ if [ -z "$TEST_SCOPE" ]; then
704
+ echo "[juninho:run-test-scope] Missing test scope. Pass a Maven/Gradle test selector or 'full'."
705
+ exit 1
706
+ fi
707
+
708
+ if [ -x "./gradlew" ]; then
709
+ if [ "$TEST_SCOPE" = "full" ]; then
710
+ ./gradlew test
711
+ else
712
+ ./gradlew test --tests "$TEST_SCOPE" 2>/dev/null || ./gradlew test
713
+ fi
581
714
  exit 0
582
715
  fi
583
716
 
584
717
  if [ -x "./mvnw" ]; then
585
- echo "[juninho:check-all] Compiling and testing..."
586
- ./mvnw test
718
+ if [ "$TEST_SCOPE" = "full" ]; then
719
+ ./mvnw test
720
+ else
721
+ ./mvnw test -Dtest="$TEST_SCOPE" 2>/dev/null || ./mvnw test
722
+ fi
587
723
  exit 0
588
724
  fi
589
725
 
590
- echo "[juninho:check-all] No Kotlin build tool found."
726
+ echo "[juninho:run-test-scope] No Java build tool found."
591
727
  `;
592
- }
593
- function checkAllGenericBody() {
594
- return `# Generic: detect and run what's available
595
- if [ -f package.json ]; then
728
+ const RUN_TEST_SCOPE_KOTLIN = `#!/bin/sh
729
+ set -e
730
+
731
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
732
+ cd "$ROOT_DIR"
733
+
734
+ TEST_SCOPE="\${1:-}"
735
+
736
+ if [ -z "$TEST_SCOPE" ]; then
737
+ echo "[juninho:run-test-scope] Missing test scope. Pass a Maven/Gradle test selector or 'full'."
738
+ exit 1
739
+ fi
740
+
741
+ if [ -x "./gradlew" ]; then
742
+ if [ "$TEST_SCOPE" = "full" ]; then
743
+ ./gradlew test
744
+ else
745
+ ./gradlew test --tests "$TEST_SCOPE" 2>/dev/null || ./gradlew test
746
+ fi
747
+ exit 0
748
+ fi
749
+
750
+ if [ -x "./mvnw" ]; then
751
+ if [ "$TEST_SCOPE" = "full" ]; then
752
+ ./mvnw test
753
+ else
754
+ ./mvnw test -Dtest="$TEST_SCOPE" 2>/dev/null || ./mvnw test
755
+ fi
756
+ exit 0
757
+ fi
758
+
759
+ echo "[juninho:run-test-scope] No Kotlin build tool found."
760
+ `;
761
+ const RUN_TEST_SCOPE_GENERIC = `#!/bin/sh
762
+ set -e
763
+
764
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
765
+ cd "$ROOT_DIR"
766
+
767
+ TEST_SCOPE="\${1:-}"
768
+
769
+ if [ -f package.json ] && [ "$TEST_SCOPE" = "full" ]; then
596
770
  npm test 2>/dev/null && exit 0
597
771
  fi
598
772
 
599
773
  if command -v pytest >/dev/null 2>&1; then
600
- pytest 2>/dev/null && exit 0
774
+ if [ "$TEST_SCOPE" = "full" ] || [ -z "$TEST_SCOPE" ]; then
775
+ pytest && exit 0
776
+ fi
777
+ pytest $TEST_SCOPE && exit 0
601
778
  fi
602
779
 
603
780
  if [ -f go.mod ]; then
604
- go test ./... 2>/dev/null && exit 0
781
+ if [ "$TEST_SCOPE" = "full" ] || [ -z "$TEST_SCOPE" ]; then
782
+ go test ./... && exit 0
783
+ fi
784
+ go test -count=1 $TEST_SCOPE && exit 0
605
785
  fi
606
786
 
607
787
  if [ -x "./gradlew" ]; then
@@ -612,8 +792,593 @@ if [ -x "./mvnw" ]; then
612
792
  ./mvnw test 2>/dev/null && exit 0
613
793
  fi
614
794
 
615
- echo "[juninho:check-all] No full verification command configured."
616
- echo "[juninho:check-all] Customize .opencode/scripts/check-all.sh or run /j.init-deep."
795
+ echo "[juninho:run-test-scope] No test scope runner detected."
796
+ `;
797
+ const SCAFFOLD_SPEC_STATE = `#!/bin/sh
798
+ set -e
799
+
800
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
801
+ FEATURE_SLUG="\${1:-}"
802
+
803
+ [ -n "$FEATURE_SLUG" ] || {
804
+ echo "Usage: $0 <feature-slug>" >&2
805
+ exit 1
806
+ }
807
+
808
+ STATE_DIR="$ROOT_DIR/docs/specs/$FEATURE_SLUG/state"
809
+ TEMPLATE_PATH="$ROOT_DIR/.opencode/templates/spec-state-readme.md"
810
+
811
+ mkdir -p "$STATE_DIR/tasks" "$STATE_DIR/sessions"
812
+
813
+ if [ -f "$TEMPLATE_PATH" ] && [ ! -f "$STATE_DIR/README.md" ]; then
814
+ sed "s/{feature-slug}/$FEATURE_SLUG/g" "$TEMPLATE_PATH" > "$STATE_DIR/README.md"
815
+ fi
617
816
  `;
817
+ const HARNESS_FEATURE_INTEGRATION = `#!/bin/sh
818
+ set -e
819
+
820
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
821
+ cd "$ROOT_DIR"
822
+ TAB="$(printf '\t')"
823
+
824
+ if command -v node >/dev/null 2>&1; then
825
+ JS_RUNTIME="node"
826
+ elif command -v bun >/dev/null 2>&1; then
827
+ JS_RUNTIME="bun"
828
+ else
829
+ echo "[juninho:feature-integration] Missing JavaScript runtime (node or bun)" >&2
830
+ exit 1
831
+ fi
832
+
833
+ fail() {
834
+ echo "[juninho:feature-integration] $*" >&2
835
+ exit 1
618
836
  }
837
+
838
+ current_branch() {
839
+ git symbolic-ref --quiet --short HEAD 2>/dev/null || true
840
+ }
841
+
842
+ state_file_path() {
843
+ local_name="$1"
844
+ printf '%s/.opencode/state/%s\n' "$ROOT_DIR" "$local_name"
845
+ }
846
+
847
+ default_base_ref() {
848
+ if git show-ref --verify --quiet "refs/remotes/origin/main"; then
849
+ printf '%s\n' "refs/remotes/origin/main"
850
+ return
851
+ fi
852
+ if git show-ref --verify --quiet "refs/remotes/origin/master"; then
853
+ printf '%s\n' "refs/remotes/origin/master"
854
+ return
855
+ fi
856
+
857
+ branch="$(current_branch)"
858
+ [ -n "$branch" ] || fail "Detached HEAD. Provide an explicit base branch."
859
+ printf '%s\n' "$branch"
860
+ }
861
+
862
+ normalize_base_branch() {
863
+ input="$1"
864
+ case "$input" in
865
+ refs/remotes/origin/*)
866
+ printf '%s\n' "\${input#refs/remotes/origin/}"
867
+ ;;
868
+ origin/*)
869
+ printf '%s\n' "\${input#origin/}"
870
+ ;;
871
+ refs/heads/*)
872
+ printf '%s\n' "\${input#refs/heads/}"
873
+ ;;
874
+ *)
875
+ printf '%s\n' "$input"
876
+ ;;
877
+ esac
878
+ }
879
+
880
+ resolve_base_ref() {
881
+ input="$1"
882
+ if [ -z "$input" ]; then
883
+ default_base_ref
884
+ return
885
+ fi
886
+
887
+ case "$input" in
888
+ refs/remotes/*|refs/heads/*)
889
+ printf '%s\n' "$input"
890
+ ;;
891
+ origin/*)
892
+ printf '%s\n' "refs/remotes/$input"
893
+ ;;
894
+ *)
895
+ if git show-ref --verify --quiet "refs/remotes/origin/$input"; then
896
+ printf '%s\n' "refs/remotes/origin/$input"
897
+ else
898
+ printf '%s\n' "$input"
899
+ fi
900
+ ;;
901
+ esac
902
+ }
903
+
904
+ task_branch_name() {
905
+ printf 'feature/%s-task-%s' "$1" "$2"
906
+ }
907
+
908
+ find_existing_feature_commit() {
909
+ feature_branch="$1"
910
+ validated_commit="$2"
911
+ git log "$feature_branch" --format='%H' --grep="cherry picked from commit $validated_commit" -n 1 2>/dev/null || true
912
+ }
913
+
914
+ feature_branch_name() {
915
+ printf 'feature/%s' "$1"
916
+ }
917
+
918
+ manifest_path() {
919
+ printf '%s/docs/specs/%s/state/integration-state.json' "$ROOT_DIR" "$1"
920
+ }
921
+
922
+ ensure_manifest_dir() {
923
+ sh "$ROOT_DIR/.opencode/scripts/scaffold-spec-state.sh" "$1"
924
+ }
925
+
926
+ json_read_field() {
927
+ MANIFEST_PATH="$1" FIELD_PATH="$2" "$JS_RUNTIME" - <<'NODE'
928
+ const fs = require("fs")
929
+
930
+ const manifestPath = process.env.MANIFEST_PATH
931
+ const fieldPath = process.env.FIELD_PATH || ""
932
+
933
+ if (!manifestPath || !fs.existsSync(manifestPath)) process.exit(1)
934
+
935
+ const data = JSON.parse(fs.readFileSync(manifestPath, "utf8"))
936
+ let value = data
937
+ for (const key of fieldPath.split(".").filter(Boolean)) {
938
+ if (value == null || !(key in value)) process.exit(1)
939
+ value = value[key]
940
+ }
941
+
942
+ if (value == null) process.exit(1)
943
+ if (typeof value === "string") {
944
+ process.stdout.write(value)
945
+ process.exit(0)
946
+ }
947
+
948
+ process.stdout.write(JSON.stringify(value))
949
+ NODE
950
+ }
951
+
952
+ parse_active_feature_slug() {
953
+ execution_state="$(state_file_path execution-state.md)"
954
+ [ -f "$execution_state" ] || return 0
955
+ grep "Feature slug" "$execution_state" 2>/dev/null | head -n 1 | cut -d':' -f2 | tr -d ' '
956
+ }
957
+
958
+ cmd="\${1:-}"
959
+
960
+ case "$cmd" in
961
+ ensure)
962
+ feature_slug="\${2:-}"
963
+ [ -n "$feature_slug" ] || fail "Usage: ensure <feature-slug> [base-branch]"
964
+
965
+ base_ref="$(resolve_base_ref "\${3:-}")"
966
+ base_branch="$(normalize_base_branch "$base_ref")"
967
+
968
+ ensure_manifest_dir "$feature_slug"
969
+ feature_branch="$(feature_branch_name "$feature_slug")"
970
+ base_sha="$(git rev-parse "$base_ref" 2>/dev/null)" || fail "Unknown base branch/ref: $base_ref"
971
+
972
+ if ! git show-ref --verify --quiet "refs/heads/$feature_branch"; then
973
+ git branch "$feature_branch" "$base_sha" >/dev/null
974
+ fi
975
+
976
+ manifest="$(manifest_path "$feature_slug")"
977
+ FEATURE_SLUG="$feature_slug" FEATURE_BRANCH="$feature_branch" BASE_BRANCH="$base_branch" BASE_REF="$base_ref" BASE_SHA="$base_sha" MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
978
+ const fs = require("fs")
979
+ const path = require("path")
980
+
981
+ const manifestPath = process.env.MANIFEST_PATH
982
+ const featureSlug = process.env.FEATURE_SLUG
983
+ const featureBranch = process.env.FEATURE_BRANCH
984
+ const baseBranch = process.env.BASE_BRANCH
985
+ const baseRef = process.env.BASE_REF
986
+ const baseSha = process.env.BASE_SHA
987
+
988
+ const now = new Date().toISOString()
989
+ const next = fs.existsSync(manifestPath)
990
+ ? JSON.parse(fs.readFileSync(manifestPath, "utf8"))
991
+ : {}
992
+
993
+ const manifest = {
994
+ featureSlug,
995
+ featureBranch,
996
+ baseBranch,
997
+ baseRef,
998
+ baseStartPoint: next.baseStartPoint || baseSha,
999
+ createdAt: next.createdAt || now,
1000
+ lastUpdatedAt: now,
1001
+ tasks: next.tasks || {},
1002
+ }
1003
+
1004
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
1005
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8")
1006
+ NODE
1007
+
1008
+ printf '%s\n' "$feature_branch"
1009
+ ;;
1010
+
1011
+ print-task-base)
1012
+ feature_slug="\${2:-}"
1013
+ depends_csv="\${3:-}"
1014
+ [ -n "$feature_slug" ] || fail "Usage: print-task-base <feature-slug> [depends-csv]"
1015
+
1016
+ manifest="$(manifest_path "$feature_slug")"
1017
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1018
+
1019
+ feature_branch="$(json_read_field "$manifest" "featureBranch")" || fail "Unable to read feature branch"
1020
+ base_start_point="$(json_read_field "$manifest" "baseStartPoint")" || fail "Unable to read base start point"
1021
+
1022
+ if [ -n "$depends_csv" ]; then
1023
+ DEPENDS_CSV="$depends_csv" MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
1024
+ const fs = require("fs")
1025
+
1026
+ const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST_PATH, "utf8"))
1027
+ for (const dep of (process.env.DEPENDS_CSV || "").split(",").map((value) => value.trim()).filter(Boolean)) {
1028
+ const entry = manifest.tasks?.[dep]
1029
+ const status = entry?.integration?.status
1030
+ if (entry && (!status || status === "pending")) {
1031
+ throw new Error("Dependency " + dep + " is not integrated yet")
1032
+ }
1033
+ }
1034
+ NODE
1035
+ printf '%s\n' "$feature_branch"
1036
+ exit 0
1037
+ fi
1038
+
1039
+ printf '%s\n' "$base_start_point"
1040
+ ;;
1041
+
1042
+ prepare-task-branch)
1043
+ feature_slug="\${2:-}"
1044
+ task_id="\${3:-}"
1045
+ depends_csv="\${4:-}"
1046
+ worktree_directory="\${5:-}"
1047
+
1048
+ [ -n "$feature_slug" ] || fail "Usage: prepare-task-branch <feature-slug> <task-id> [depends-csv] [worktree-directory]"
1049
+ [ -n "$task_id" ] || fail "Missing task id"
1050
+
1051
+ manifest="$(manifest_path "$feature_slug")"
1052
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1053
+
1054
+ task_branch="$(task_branch_name "$feature_slug" "$task_id")"
1055
+ task_base="$(sh "$0" print-task-base "$feature_slug" "$depends_csv")"
1056
+
1057
+ if [ -n "$worktree_directory" ]; then
1058
+ if [ -d "$worktree_directory" ]; then
1059
+ printf '%s\n' "$task_branch"
1060
+ exit 0
1061
+ fi
1062
+
1063
+ parent_dir=$(dirname "$worktree_directory")
1064
+ [ -d "$parent_dir" ] || fail "Missing worktree parent directory: $parent_dir"
1065
+
1066
+ if git show-ref --verify --quiet "refs/heads/$task_branch"; then
1067
+ git worktree add "$worktree_directory" "$task_branch" >/dev/null
1068
+ else
1069
+ git worktree add -b "$task_branch" "$worktree_directory" "$task_base" >/dev/null
1070
+ fi
1071
+ printf '%s\n' "$task_branch"
1072
+ exit 0
1073
+ fi
1074
+
1075
+ if git show-ref --verify --quiet "refs/heads/$task_branch"; then
1076
+ git switch "$task_branch" >/dev/null
1077
+ else
1078
+ git switch -c "$task_branch" "$task_base" >/dev/null
1079
+ fi
1080
+ printf '%s\n' "$task_branch"
1081
+ ;;
1082
+
1083
+ switch)
1084
+ feature_slug="\${2:-}"
1085
+ [ -n "$feature_slug" ] || fail "Usage: switch <feature-slug>"
1086
+
1087
+ manifest="$(manifest_path "$feature_slug")"
1088
+ if [ -f "$manifest" ]; then
1089
+ feature_branch="$(json_read_field "$manifest" "featureBranch")" || fail "Unable to read feature branch from $manifest"
1090
+ else
1091
+ feature_branch="$(feature_branch_name "$feature_slug")"
1092
+ fi
1093
+
1094
+ git switch "$feature_branch" >/dev/null
1095
+ printf '%s\n' "$feature_branch"
1096
+ ;;
1097
+
1098
+ switch-active)
1099
+ feature_slug="$(parse_active_feature_slug)"
1100
+ [ -n "$feature_slug" ] || exit 0
1101
+ sh "$0" switch "$feature_slug"
1102
+ ;;
1103
+
1104
+ record-task)
1105
+ feature_slug="\${2:-}"
1106
+ task_id="\${3:-}"
1107
+ task_branch="\${4:-}"
1108
+ validated_commit="\${5:-}"
1109
+ attempt="\${6:-}"
1110
+ worktree_directory="\${7:-}"
1111
+ task_label="\${8:-}"
1112
+
1113
+ [ -n "$feature_slug" ] || fail "Usage: record-task <feature-slug> <task-id> <task-branch> <validated-commit> <attempt> [worktree] [label]"
1114
+ [ -n "$task_id" ] || fail "Missing task id"
1115
+ [ -n "$task_branch" ] || fail "Missing task branch"
1116
+ [ -n "$validated_commit" ] || fail "Missing validated commit"
1117
+ [ -n "$attempt" ] || fail "Missing attempt"
1118
+
1119
+ manifest="$(manifest_path "$feature_slug")"
1120
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1121
+
1122
+ task_tip="$(git rev-parse "refs/heads/$task_branch" 2>/dev/null || printf '%s' "$validated_commit")"
1123
+
1124
+ FEATURE_SLUG="$feature_slug" TASK_ID="$task_id" TASK_BRANCH="$task_branch" VALIDATED_COMMIT="$validated_commit" TASK_TIP="$task_tip" TASK_ATTEMPT="$attempt" WORKTREE_DIRECTORY="$worktree_directory" TASK_LABEL="$task_label" MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
1125
+ const fs = require("fs")
1126
+
1127
+ const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST_PATH, "utf8"))
1128
+ const existing = manifest.tasks?.[process.env.TASK_ID]
1129
+
1130
+ manifest.tasks = manifest.tasks || {}
1131
+ manifest.tasks[process.env.TASK_ID] = {
1132
+ ...(existing || {}),
1133
+ taskID: process.env.TASK_ID,
1134
+ taskBranch: process.env.TASK_BRANCH,
1135
+ validatedCommit: process.env.VALIDATED_COMMIT,
1136
+ taskTip: process.env.TASK_TIP,
1137
+ attempt: Number(process.env.TASK_ATTEMPT),
1138
+ worktreeDirectory: process.env.WORKTREE_DIRECTORY || "",
1139
+ taskLabel: process.env.TASK_LABEL || "",
1140
+ recordedAt: new Date().toISOString(),
1141
+ integration: existing?.integration || { status: "pending" },
1142
+ }
1143
+ manifest.lastUpdatedAt = new Date().toISOString()
1144
+ fs.writeFileSync(process.env.MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n", "utf8")
1145
+ NODE
1146
+
1147
+ printf '%s\n' "$validated_commit"
1148
+ ;;
1149
+
1150
+ integrate-task)
1151
+ feature_slug="\${2:-}"
1152
+ task_id="\${3:-}"
1153
+
1154
+ [ -n "$feature_slug" ] || fail "Usage: integrate-task <feature-slug> <task-id>"
1155
+ [ -n "$task_id" ] || fail "Missing task id"
1156
+
1157
+ manifest="$(manifest_path "$feature_slug")"
1158
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1159
+
1160
+ feature_branch="$(json_read_field "$manifest" "featureBranch")" || fail "Unable to read feature branch"
1161
+ validated_commit="$(json_read_field "$manifest" "tasks.$task_id.validatedCommit")" || fail "Task $task_id has no validated commit"
1162
+ task_branch="$(json_read_field "$manifest" "tasks.$task_id.taskBranch")" || fail "Task $task_id has no task branch"
1163
+
1164
+ git switch "$feature_branch" >/dev/null
1165
+
1166
+ integration_status="already-contained"
1167
+ integration_method="ancestor"
1168
+ if git merge-base --is-ancestor "$validated_commit" HEAD; then
1169
+ integrated_commit="$validated_commit"
1170
+ elif git cherry "$feature_branch" "$validated_commit" 2>/dev/null | grep -q "^- $validated_commit$"; then
1171
+ integration_method="patch-equivalent"
1172
+ integrated_commit="$(find_existing_feature_commit "$feature_branch" "$validated_commit")"
1173
+ if [ -z "$integrated_commit" ]; then
1174
+ integrated_commit="$(git rev-parse HEAD)"
1175
+ fi
1176
+ elif git merge-base --is-ancestor HEAD "$validated_commit"; then
1177
+ git merge --ff-only "$validated_commit" >/dev/null
1178
+ integration_status="ff-only"
1179
+ integration_method="ff-only"
1180
+ integrated_commit="$(git rev-parse HEAD)"
1181
+ else
1182
+ git cherry-pick -x "$validated_commit" >/dev/null
1183
+ integration_status="cherry-picked"
1184
+ integration_method="cherry-pick"
1185
+ integrated_commit="$(git rev-parse HEAD)"
1186
+ fi
1187
+
1188
+ TASK_ID="$task_id" INTEGRATED_STATUS="$integration_status" INTEGRATION_METHOD="$integration_method" INTEGRATED_COMMIT="$integrated_commit" FEATURE_BRANCH="$feature_branch" TASK_BRANCH="$task_branch" MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
1189
+ const fs = require("fs")
1190
+
1191
+ const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST_PATH, "utf8"))
1192
+ const task = manifest.tasks?.[process.env.TASK_ID]
1193
+
1194
+ if (!task) throw new Error("Task " + process.env.TASK_ID + " is missing from manifest")
1195
+
1196
+ task.integration = {
1197
+ status: process.env.INTEGRATED_STATUS,
1198
+ method: process.env.INTEGRATION_METHOD,
1199
+ featureBranch: process.env.FEATURE_BRANCH,
1200
+ taskBranch: process.env.TASK_BRANCH,
1201
+ integratedAt: new Date().toISOString(),
1202
+ integratedCommit: process.env.INTEGRATED_COMMIT,
1203
+ }
1204
+ manifest.lastUpdatedAt = new Date().toISOString()
1205
+ fs.writeFileSync(process.env.MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n", "utf8")
1206
+ NODE
1207
+
1208
+ printf '%s\n' "$integrated_commit"
1209
+ ;;
1210
+
1211
+ cleanup)
1212
+ feature_slug="\${2:-}"
1213
+ [ -n "$feature_slug" ] || fail "Usage: cleanup <feature-slug>"
1214
+
1215
+ manifest="$(manifest_path "$feature_slug")"
1216
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1217
+
1218
+ feature_branch="$(json_read_field "$manifest" "featureBranch")" || fail "Unable to read feature branch"
1219
+ git switch "$feature_branch" >/dev/null
1220
+
1221
+ cleanup_rows="$(MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
1222
+ const fs = require("fs")
1223
+
1224
+ const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST_PATH, "utf8"))
1225
+ for (const [taskId, task] of Object.entries(manifest.tasks || {})) {
1226
+ if (!task?.integration?.status || task.integration.status === "pending") continue
1227
+ process.stdout.write(taskId + "\t" + (task.taskBranch || "") + "\t" + (task.worktreeDirectory || "") + "\n")
1228
+ }
1229
+ NODE
1230
+ )"
1231
+
1232
+ if [ -n "$cleanup_rows" ]; then
1233
+ printf '%s\n' "$cleanup_rows" | while IFS="$TAB" read -r task_id task_branch worktree_directory; do
1234
+ if [ -n "$worktree_directory" ] && [ -d "$worktree_directory" ]; then
1235
+ git worktree remove "$worktree_directory" >/dev/null
1236
+ fi
1237
+
1238
+ if [ -n "$task_branch" ] && [ "$task_branch" != "$feature_branch" ] && git show-ref --verify --quiet "refs/heads/$task_branch"; then
1239
+ git branch -d "$task_branch" >/dev/null
1240
+ fi
1241
+ done
1242
+ fi
1243
+
1244
+ MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
1245
+ const fs = require("fs")
1246
+
1247
+ const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST_PATH, "utf8"))
1248
+ for (const task of Object.values(manifest.tasks || {})) {
1249
+ if (!task.integration?.status || task.integration.status === "pending") continue
1250
+ task.cleanup = {
1251
+ status: "done",
1252
+ cleanedAt: new Date().toISOString(),
1253
+ }
1254
+ }
1255
+ manifest.lastUpdatedAt = new Date().toISOString()
1256
+ fs.writeFileSync(process.env.MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n", "utf8")
1257
+ NODE
1258
+
1259
+ printf '%s\n' "$feature_branch"
1260
+ ;;
1261
+
1262
+ print-feature-branch)
1263
+ feature_slug="\${2:-}"
1264
+ [ -n "$feature_slug" ] || fail "Usage: print-feature-branch <feature-slug>"
1265
+ manifest="$(manifest_path "$feature_slug")"
1266
+ if [ -f "$manifest" ]; then
1267
+ json_read_field "$manifest" "featureBranch"
1268
+ else
1269
+ feature_branch_name "$feature_slug"
1270
+ fi
1271
+ printf '\n'
1272
+ ;;
1273
+
1274
+ print-base-branch)
1275
+ feature_slug="\${2:-}"
1276
+ [ -n "$feature_slug" ] || fail "Usage: print-base-branch <feature-slug>"
1277
+ manifest="$(manifest_path "$feature_slug")"
1278
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1279
+ json_read_field "$manifest" "baseBranch"
1280
+ printf '\n'
1281
+ ;;
1282
+
1283
+ print-base-ref)
1284
+ feature_slug="\${2:-}"
1285
+ [ -n "$feature_slug" ] || fail "Usage: print-base-ref <feature-slug>"
1286
+ manifest="$(manifest_path "$feature_slug")"
1287
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1288
+ if json_read_field "$manifest" "baseRef" >/dev/null 2>&1; then
1289
+ json_read_field "$manifest" "baseRef"
1290
+ else
1291
+ json_read_field "$manifest" "baseBranch"
1292
+ fi
1293
+ printf '\n'
1294
+ ;;
1295
+
1296
+ *)
1297
+ fail "Unknown command: \${cmd:-<empty>}"
1298
+ ;;
1299
+ esac
1300
+ `;
1301
+ // ─── Build Verify ────────────────────────────────────────────────────────────
1302
+ function buildVerify(projectType, isKotlin) {
1303
+ if (projectType === "java") {
1304
+ return `#!/bin/sh
1305
+ set -e
1306
+
1307
+ ROOT_DIR="\${TARGET_REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
1308
+ cd "$ROOT_DIR"
1309
+
1310
+ echo "[juninho:build-verify] Running build verification..."
1311
+
1312
+ if [ -x "./gradlew" ]; then
1313
+ ${isKotlin ? './gradlew compileKotlin compileTestKotlin' : './gradlew compileJava compileTestJava'}
1314
+ exit 0
1315
+ fi
1316
+
1317
+ if [ -x "./mvnw" ]; then
1318
+ ./mvnw -q -DskipTests compile test-compile
1319
+ exit 0
1320
+ fi
1321
+
1322
+ echo "[juninho:build-verify] No build tool found."
1323
+ exit 1
1324
+ `;
1325
+ }
1326
+ if (projectType === "go") {
1327
+ return `#!/bin/sh
1328
+ set -e
1329
+
1330
+ ROOT_DIR="\${TARGET_REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
1331
+ cd "$ROOT_DIR"
1332
+
1333
+ echo "[juninho:build-verify] Running build verification..."
1334
+ go build ./...
1335
+ `;
1336
+ }
1337
+ // Node/Python/Generic — lightweight build check
1338
+ return `#!/bin/sh
1339
+ set -e
1340
+
1341
+ ROOT_DIR="\${TARGET_REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
1342
+ cd "$ROOT_DIR"
1343
+
1344
+ echo "[juninho:build-verify] Running build verification..."
1345
+
1346
+ if [ -f "package.json" ]; then
1347
+ if npm run --silent build --if-present 2>/dev/null; then
1348
+ exit 0
1349
+ fi
1350
+ if npx tsc --noEmit 2>/dev/null; then
1351
+ exit 0
1352
+ fi
1353
+ fi
1354
+
1355
+ echo "[juninho:build-verify] No build verification available — skipping."
1356
+ exit 0
1357
+ `;
1358
+ }
1359
+ // ─── Install Git Hooks ───────────────────────────────────────────────────────
1360
+ const INSTALL_GIT_HOOKS = `#!/bin/sh
1361
+ set -e
1362
+
1363
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
1364
+ HOOKS_DIR="$ROOT_DIR/.git/hooks"
1365
+ SOURCE_HOOK="$ROOT_DIR/.opencode/hooks/pre-commit"
1366
+ TARGET_HOOK="$HOOKS_DIR/pre-commit"
1367
+
1368
+ if [ ! -d "$HOOKS_DIR" ]; then
1369
+ echo "[juninho:install-hooks] Missing git hooks directory: $HOOKS_DIR" >&2
1370
+ exit 1
1371
+ fi
1372
+
1373
+ if [ ! -f "$SOURCE_HOOK" ]; then
1374
+ echo "[juninho:install-hooks] Missing source hook: $SOURCE_HOOK" >&2
1375
+ exit 1
1376
+ fi
1377
+
1378
+ chmod +x "$SOURCE_HOOK"
1379
+ ln -sf ../../.opencode/hooks/pre-commit "$TARGET_HOOK"
1380
+ chmod +x "$TARGET_HOOK"
1381
+
1382
+ echo "[juninho:install-hooks] Installed pre-commit hook at $TARGET_HOOK"
1383
+ `;
619
1384
  //# sourceMappingURL=support-scripts.js.map