javi-forge 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,9 +46,24 @@ setup_ci_commands() {
46
46
  ;;
47
47
  node)
48
48
  DOCKERFILE="node.Dockerfile"
49
- LINT_CMD="$BUILD_TOOL run lint"
50
- COMPILE_CMD="$BUILD_TOOL run build"
51
- TEST_CMD="$BUILD_TOOL test"
49
+ # Only set lint if the script exists in package.json
50
+ if grep -q '"lint"' "$PROJECT_DIR/package.json" 2>/dev/null; then
51
+ LINT_CMD="$BUILD_TOOL run lint"
52
+ else
53
+ LINT_CMD=""
54
+ fi
55
+ # Only set compile if build script exists
56
+ if grep -q '"build"' "$PROJECT_DIR/package.json" 2>/dev/null; then
57
+ COMPILE_CMD="$BUILD_TOOL run build"
58
+ else
59
+ COMPILE_CMD=""
60
+ fi
61
+ # Only set test if test script exists
62
+ if grep -q '"test"' "$PROJECT_DIR/package.json" 2>/dev/null; then
63
+ TEST_CMD="$BUILD_TOOL test"
64
+ else
65
+ TEST_CMD=""
66
+ fi
52
67
  ;;
53
68
  python)
54
69
  DOCKERFILE="python.Dockerfile"
@@ -206,6 +221,7 @@ run_in_ci() {
206
221
  fi
207
222
  docker run "${docker_flags[@]}" \
208
223
  --stop-timeout 30 \
224
+ --entrypoint "" \
209
225
  -v "$PROJECT_DIR:/home/runner/work" \
210
226
  -e CI=true \
211
227
  "$image_name" timeout "$timeout" bash -c "$1"
@@ -1,162 +1,17 @@
1
1
  #!/bin/bash
2
2
  # =============================================================================
3
- # PRE-COMMIT: Universal quick checks before commit
3
+ # PRE-COMMIT: Quick CI check via javi-forge
4
4
  # =============================================================================
5
- # Detecta el stack y ejecuta lint + compile rapido
6
- # Tiempo: ~30-60 segundos
5
+ # Requires: npm install -g javi-forge
6
+ # To skip: git commit --no-verify
7
7
  # =============================================================================
8
8
 
9
9
  set -e
10
10
 
11
- # Encontrar directorio .ci-local
12
- HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
13
- if [[ -d "$HOOK_DIR/../.ci-local" ]]; then
14
- CI_LOCAL_DIR="$HOOK_DIR/../.ci-local"
15
- elif [[ -d "$(git rev-parse --show-toplevel)/.ci-local" ]]; then
16
- CI_LOCAL_DIR="$(git rev-parse --show-toplevel)/.ci-local"
17
- else
18
- echo "Warning: .ci-local not found, skipping pre-commit checks"
19
- exit 0
20
- fi
21
-
22
- PROJECT_DIR="$(dirname "$CI_LOCAL_DIR")"
23
-
24
- # Source shared library for colors and detect_stack
25
- source "$CI_LOCAL_DIR/../lib/common.sh"
26
-
27
- echo -e "${YELLOW}[pre-commit] Running quick checks...${NC}"
28
-
29
- # =============================================================================
30
- # CHECK: No AI attribution in staged files
31
- # =============================================================================
32
- echo -e "${YELLOW}[1/3] Checking for AI attribution...${NC}"
33
-
34
- # Patrones prohibidos (case insensitive)
35
- AI_PATTERNS=(
36
- "co-authored-by:.*claude"
37
- "co-authored-by:.*anthropic"
38
- "co-authored-by:.*gpt"
39
- "co-authored-by:.*openai"
40
- "co-authored-by:.*copilot"
41
- "co-authored-by:.*gemini"
42
- "co-authored-by:.*\bai\b"
43
- "made by claude"
44
- "made by gpt"
45
- "made by ai"
46
- "generated by claude"
47
- "generated by gpt"
48
- "generated by ai"
49
- "generated with claude"
50
- "generated with ai"
51
- "written by claude"
52
- "written by ai"
53
- "created by claude"
54
- "created by ai"
55
- "assisted by claude"
56
- "assisted by ai"
57
- "with help from claude"
58
- "claude code"
59
- "made by anthropic"
60
- "powered by anthropic"
61
- "created by anthropic"
62
- "made by openai"
63
- "powered by openai"
64
- "created by openai"
65
- "@anthropic.com"
66
- "@openai.com"
67
- )
68
-
69
- # Check actual staged content using git show :0:file
70
- STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -vE '\.(png|jpg|jpeg|gif|ico|woff|ttf|eot|svg|lock)$' || true)
71
-
72
- if [[ -n "$STAGED_FILES" ]]; then
73
- for pattern in "${AI_PATTERNS[@]}"; do
74
- while IFS= read -r file; do
75
- [[ -z "$file" ]] && continue
76
- if git show ":0:$file" 2>/dev/null | grep -iqE "$pattern"; then
77
- echo -e "${RED}[BLOCKED] AI attribution found in staged content: $file${NC}"
78
- echo -e "${RED} Pattern matched: $pattern${NC}"
79
- git show ":0:$file" | grep -inE "$pattern" | head -3 | while IFS= read -r line; do
80
- echo -e "${RED} $line${NC}"
81
- done
82
- echo ""
83
- echo -e "${YELLOW}Remove AI attribution before committing.${NC}"
84
- exit 1
85
- fi
86
- done <<< "$STAGED_FILES"
87
- done
88
- echo -e "${GREEN} No AI attribution found${NC}"
89
- fi
90
-
91
- # Semgrep security scan - native or Docker fallback
92
- SEMGREP_CMD=""
93
- SEMGREP_MODE=""
94
- if command -v semgrep &> /dev/null; then
95
- SEMGREP_CMD="native"
96
- SEMGREP_MODE="native"
97
- elif command -v docker &> /dev/null && docker info &> /dev/null 2>&1; then
98
- SEMGREP_CMD="docker"
99
- SEMGREP_MODE="Docker"
100
- fi
101
-
102
- if [[ -n "$SEMGREP_CMD" ]]; then
103
- echo -e "${YELLOW}[2/3] Security scan (Semgrep via $SEMGREP_MODE)...${NC}"
104
- STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(java|kt|js|ts|py|go|rs)$' || true)
105
- if [[ -n "$STAGED_FILES" ]]; then
106
- if [[ "$SEMGREP_CMD" == "native" ]]; then
107
- echo "$STAGED_FILES" | tr '\n' '\0' | xargs -0 semgrep --config=.ci-local/semgrep.yml --severity=ERROR --quiet 2>/dev/null || {
108
- echo -e "${RED}Semgrep found issues${NC}"
109
- exit 1
110
- }
111
- else
112
- # Pipe file list via stdin into the container where xargs feeds it to semgrep
113
- echo "$STAGED_FILES" | docker run --rm -i -v "${PROJECT_DIR}:/src" -w /src returntocorp/semgrep \
114
- sh -c 'xargs semgrep --config=/src/.ci-local/semgrep.yml --severity=ERROR --quiet' 2>/dev/null || {
115
- echo -e "${RED}Semgrep found issues${NC}"
116
- exit 1
117
- }
118
- fi
119
- echo -e "${GREEN}Security OK${NC}"
120
- else
121
- echo -e "${GREEN}No files to scan${NC}"
122
- fi
123
- else
124
- echo -e "${YELLOW}[2/3] Semgrep not available, skipping (install semgrep or use Docker)${NC}"
125
- fi
126
-
127
- # Quick compile/lint basado en el stack
128
- echo -e "${YELLOW}[3/3] Quick compile check...${NC}"
129
- cd "$PROJECT_DIR"
130
-
131
- # Use shared stack detection from lib/common.sh
132
- detect_stack
133
-
134
- case "$STACK_TYPE" in
135
- java-gradle)
136
- ./gradlew spotlessCheck classes --no-daemon -q 2>/dev/null || {
137
- echo -e "${RED}Gradle check failed. Run: ./gradlew spotlessApply${NC}"
138
- exit 1
139
- }
140
- ;;
141
- java-maven)
142
- ./mvnw compile -q 2>/dev/null || exit 1
143
- ;;
144
- node)
145
- if [[ "$BUILD_TOOL" == "pnpm" ]]; then
146
- pnpm lint --silent 2>/dev/null || exit 1
147
- elif [[ "$BUILD_TOOL" == "yarn" ]]; then
148
- yarn lint --silent 2>/dev/null || exit 1
149
- else
150
- npm run lint --silent 2>/dev/null || exit 1
151
- fi
152
- ;;
153
- rust)
154
- cargo check --quiet 2>/dev/null || exit 1
155
- ;;
156
- go)
157
- go build ./... 2>/dev/null || exit 1
158
- ;;
159
- esac
160
-
161
- echo -e "${GREEN}Compile OK${NC}"
162
- echo -e "${GREEN}[pre-commit] All checks passed!${NC}"
11
+ echo "PRE-COMMIT: Running quick check..."
12
+ javi-forge ci --quick --no-security --no-ghagga || {
13
+ echo ""
14
+ echo "Quick check FAILED — Fix the issues above before committing."
15
+ echo "To skip: git commit --no-verify"
16
+ exit 1
17
+ }
@@ -1,41 +1,24 @@
1
1
  #!/bin/bash
2
2
  # =============================================================================
3
- # PRE-PUSH: Full CI simulation before push
3
+ # PRE-PUSH: Full CI simulation via javi-forge
4
4
  # =============================================================================
5
- # Corre tests en Docker identico al CI
6
- # Si pasa aca -> pasa en GitHub Actions / GitLab CI
5
+ # Requires: npm install -g javi-forge
6
+ # To skip: git push --no-verify
7
7
  # =============================================================================
8
8
 
9
9
  set -e
10
10
 
11
- # Encontrar directorio .ci-local
12
- if [[ -d "$(git rev-parse --show-toplevel)/.ci-local" ]]; then
13
- CI_LOCAL_DIR="$(git rev-parse --show-toplevel)/.ci-local"
14
- else
15
- echo "Warning: .ci-local not found, skipping pre-push CI simulation"
16
- exit 0
17
- fi
18
-
19
- # Source shared library for colors
20
- source "$CI_LOCAL_DIR/../lib/common.sh"
21
-
22
- echo -e "${BLUE}PRE-PUSH: CI Simulation${NC}"
23
-
24
- # Verificar Docker
25
- if ! docker info &> /dev/null; then
26
- echo -e "${RED}Docker is not running.${NC}"
27
- echo -e "${YELLOW} Start Docker or use: git push --no-verify${NC}"
11
+ # Verify Docker is running
12
+ if ! docker info &>/dev/null; then
13
+ echo "PRE-PUSH: Docker is not running."
14
+ echo " Start Docker or use: git push --no-verify"
28
15
  exit 1
29
16
  fi
30
17
 
31
- # Ejecutar CI local
32
- "$CI_LOCAL_DIR/ci-local.sh" full || {
33
- echo -e ""
34
- echo -e "${RED}CI SIMULATION FAILED - Push aborted${NC}"
35
- echo -e "${RED}Fix the issues above before pushing.${NC}"
36
- echo -e "${RED}To skip: git push --no-verify${NC}"
18
+ echo "PRE-PUSH: Running CI simulation..."
19
+ javi-forge ci || {
20
+ echo ""
21
+ echo "CI FAILED Push aborted. Fix the issues above before pushing."
22
+ echo "To skip: git push --no-verify"
37
23
  exit 1
38
24
  }
39
-
40
- echo -e ""
41
- echo -e "${GREEN}CI Simulation PASSED - Safe to push!${NC}"
@@ -0,0 +1,33 @@
1
+ import type { Stack } from '../types/index.js';
2
+ export type CIMode = 'full' | 'quick' | 'shell' | 'detect';
3
+ export interface CIOptions {
4
+ projectDir?: string;
5
+ mode?: CIMode;
6
+ /** Skip Docker entirely — run commands natively */
7
+ noDocker?: boolean;
8
+ /** Skip GHAGGA review step */
9
+ noGhagga?: boolean;
10
+ /** Skip Semgrep security scan */
11
+ noSecurity?: boolean;
12
+ /** Timeout in seconds for each Docker step (default: 600) */
13
+ timeout?: number;
14
+ }
15
+ export type CIStepStatus = 'pending' | 'running' | 'done' | 'error' | 'skipped';
16
+ export interface CIStep {
17
+ id: string;
18
+ label: string;
19
+ status: CIStepStatus;
20
+ detail?: string;
21
+ }
22
+ export type CIStepCallback = (step: CIStep) => void;
23
+ export interface CIStackInfo {
24
+ stackType: Stack;
25
+ buildTool: string;
26
+ javaVersion: string;
27
+ lintCmd: string | null;
28
+ compileCmd: string | null;
29
+ testCmd: string | null;
30
+ }
31
+ export declare function detectCIStack(projectDir: string): Promise<CIStackInfo>;
32
+ export declare function runCI(options: CIOptions, onStep: CIStepCallback): Promise<void>;
33
+ //# sourceMappingURL=ci.d.ts.map
@@ -0,0 +1,341 @@
1
+ import { execFile, spawn } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ import { isDockerAvailable, ensureImage, runInContainer, openShell } from '../lib/docker.js';
6
+ const execFileAsync = promisify(execFile);
7
+ // =============================================================================
8
+ // Stack detection
9
+ // =============================================================================
10
+ export async function detectCIStack(projectDir) {
11
+ let stackType = 'node';
12
+ let buildTool = 'npm';
13
+ let javaVersion = '21';
14
+ // Java Gradle
15
+ if (await fs.pathExists(path.join(projectDir, 'build.gradle.kts')) ||
16
+ await fs.pathExists(path.join(projectDir, 'build.gradle'))) {
17
+ stackType = 'java-gradle';
18
+ buildTool = 'gradle';
19
+ // Try to read java version from build files
20
+ const ktsPath = path.join(projectDir, 'build.gradle.kts');
21
+ const gradlePath = path.join(projectDir, 'build.gradle');
22
+ if (await fs.pathExists(ktsPath)) {
23
+ const content = await fs.readFile(ktsPath, 'utf-8');
24
+ const match = content.match(/JavaLanguageVersion\.of\((\d+)\)/);
25
+ if (match?.[1])
26
+ javaVersion = match[1];
27
+ }
28
+ else if (await fs.pathExists(gradlePath)) {
29
+ const content = await fs.readFile(gradlePath, 'utf-8');
30
+ const match = content.match(/sourceCompatibility\s*=\s*['"]*(\d+)/);
31
+ if (match?.[1])
32
+ javaVersion = match[1];
33
+ }
34
+ }
35
+ // Java Maven
36
+ else if (await fs.pathExists(path.join(projectDir, 'pom.xml'))) {
37
+ stackType = 'java-maven';
38
+ buildTool = 'mvn';
39
+ }
40
+ // Node
41
+ else if (await fs.pathExists(path.join(projectDir, 'package.json'))) {
42
+ stackType = 'node';
43
+ if (await fs.pathExists(path.join(projectDir, 'pnpm-lock.yaml')))
44
+ buildTool = 'pnpm';
45
+ else if (await fs.pathExists(path.join(projectDir, 'yarn.lock')))
46
+ buildTool = 'yarn';
47
+ else
48
+ buildTool = 'npm';
49
+ }
50
+ // Go
51
+ else if (await fs.pathExists(path.join(projectDir, 'go.mod'))) {
52
+ stackType = 'go';
53
+ buildTool = 'go';
54
+ }
55
+ // Rust
56
+ else if (await fs.pathExists(path.join(projectDir, 'Cargo.toml'))) {
57
+ stackType = 'rust';
58
+ buildTool = 'cargo';
59
+ }
60
+ // Python
61
+ else if (await fs.pathExists(path.join(projectDir, 'pyproject.toml')) ||
62
+ await fs.pathExists(path.join(projectDir, 'requirements.txt')) ||
63
+ await fs.pathExists(path.join(projectDir, 'setup.py'))) {
64
+ stackType = 'python';
65
+ if (await fs.pathExists(path.join(projectDir, 'uv.lock')))
66
+ buildTool = 'uv';
67
+ else if (await fs.pathExists(path.join(projectDir, 'poetry.lock')))
68
+ buildTool = 'poetry';
69
+ else
70
+ buildTool = 'pip';
71
+ }
72
+ // Build CI commands per stack
73
+ const { lintCmd, compileCmd, testCmd } = await buildCICommands(stackType, buildTool, projectDir);
74
+ return { stackType, buildTool, javaVersion, lintCmd, compileCmd, testCmd };
75
+ }
76
+ async function buildCICommands(stack, buildTool, projectDir) {
77
+ switch (stack) {
78
+ case 'java-gradle':
79
+ return {
80
+ lintCmd: './gradlew spotlessCheck --no-daemon',
81
+ compileCmd: './gradlew classes testClasses --no-daemon',
82
+ testCmd: './gradlew test --no-daemon',
83
+ };
84
+ case 'java-maven':
85
+ return {
86
+ lintCmd: './mvnw spotless:check',
87
+ compileCmd: './mvnw compile test-compile',
88
+ testCmd: './mvnw test',
89
+ };
90
+ case 'node': {
91
+ const pkgPath = path.join(projectDir, 'package.json');
92
+ let pkgContent = '';
93
+ try {
94
+ pkgContent = await fs.readFile(pkgPath, 'utf-8');
95
+ }
96
+ catch { /* no package.json */ }
97
+ // Clean dist/ before build to avoid permission issues from prior Docker builds
98
+ const buildPrefix = 'rm -rf dist/ && ';
99
+ return {
100
+ lintCmd: pkgContent.includes('"lint"') ? `${buildTool} run lint` : null,
101
+ compileCmd: pkgContent.includes('"build"') ? `${buildPrefix}${buildTool} run build` : null,
102
+ testCmd: pkgContent.includes('"test"') ? `${buildTool} ${buildTool === 'npm' ? 'test' : 'run test'}` : null,
103
+ };
104
+ }
105
+ case 'python':
106
+ return {
107
+ lintCmd: 'ruff check . && { pylint **/*.py 2>/dev/null || true; }',
108
+ compileCmd: null,
109
+ testCmd: 'pytest',
110
+ };
111
+ case 'go':
112
+ return {
113
+ lintCmd: 'golangci-lint run',
114
+ compileCmd: 'go build ./...',
115
+ testCmd: 'go test ./...',
116
+ };
117
+ case 'rust':
118
+ return {
119
+ lintCmd: 'cargo clippy -- -D warnings',
120
+ compileCmd: 'cargo build',
121
+ testCmd: 'cargo test',
122
+ };
123
+ default:
124
+ return { lintCmd: null, compileCmd: null, testCmd: null };
125
+ }
126
+ }
127
+ // =============================================================================
128
+ // GHAGGA check
129
+ // =============================================================================
130
+ async function isGhaggaAvailable() {
131
+ try {
132
+ await execFileAsync('ghagga', ['--version'], { timeout: 3000 });
133
+ return true;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
139
+ // =============================================================================
140
+ // Main CI runner
141
+ // =============================================================================
142
+ function report(onStep, id, label, status, detail) {
143
+ onStep({ id, label, status, detail });
144
+ }
145
+ export async function runCI(options, onStep) {
146
+ const { projectDir = process.cwd(), mode = 'full', noDocker = false, noGhagga = false, noSecurity = false, timeout = 600, } = options;
147
+ // ── Detect stack ────────────────────────────────────────────────────────────
148
+ const stepDetect = 'detect';
149
+ report(onStep, stepDetect, 'Detecting stack', 'running');
150
+ let stackInfo;
151
+ try {
152
+ stackInfo = await detectCIStack(projectDir);
153
+ report(onStep, stepDetect, `Stack: ${stackInfo.stackType} (${stackInfo.buildTool})`, 'done');
154
+ }
155
+ catch (e) {
156
+ report(onStep, stepDetect, 'Detecting stack', 'error', String(e));
157
+ throw e;
158
+ }
159
+ // ── Detect mode ─────────────────────────────────────────────────────────────
160
+ if (mode === 'detect')
161
+ return;
162
+ // ── Shell mode ──────────────────────────────────────────────────────────────
163
+ if (mode === 'shell') {
164
+ if (noDocker) {
165
+ report(onStep, 'shell', 'Shell', 'error', '--shell requires Docker');
166
+ throw new Error('--shell requires Docker');
167
+ }
168
+ report(onStep, 'docker-image', 'Building Docker image', 'running');
169
+ try {
170
+ await ensureImage({ stack: stackInfo.stackType, javaVersion: stackInfo.javaVersion });
171
+ report(onStep, 'docker-image', 'Docker image ready', 'done');
172
+ }
173
+ catch (e) {
174
+ report(onStep, 'docker-image', 'Building Docker image', 'error', String(e));
175
+ throw e;
176
+ }
177
+ await openShell(projectDir);
178
+ return;
179
+ }
180
+ // ── Check Docker ─────────────────────────────────────────────────────────────
181
+ if (!noDocker) {
182
+ const stepDocker = 'docker-check';
183
+ report(onStep, stepDocker, 'Checking Docker', 'running');
184
+ const dockerOk = await isDockerAvailable();
185
+ if (!dockerOk) {
186
+ report(onStep, stepDocker, 'Docker not available', 'error', 'Start Docker or use --no-docker');
187
+ throw new Error('Docker is not available');
188
+ }
189
+ report(onStep, stepDocker, 'Docker available', 'done');
190
+ // Build image
191
+ const stepImage = 'docker-image';
192
+ report(onStep, stepImage, `Building image for ${stackInfo.stackType}`, 'running');
193
+ try {
194
+ await ensureImage({ stack: stackInfo.stackType, javaVersion: stackInfo.javaVersion });
195
+ report(onStep, stepImage, 'Docker image ready', 'done');
196
+ }
197
+ catch (e) {
198
+ report(onStep, stepImage, 'Building Docker image', 'error', String(e));
199
+ throw e;
200
+ }
201
+ }
202
+ // ── Lint ─────────────────────────────────────────────────────────────────────
203
+ if (stackInfo.lintCmd) {
204
+ const stepLint = 'lint';
205
+ report(onStep, stepLint, `Lint: ${stackInfo.lintCmd}`, 'running');
206
+ try {
207
+ await runStep(stackInfo.lintCmd, projectDir, noDocker, timeout);
208
+ report(onStep, stepLint, 'Lint passed', 'done');
209
+ }
210
+ catch (e) {
211
+ report(onStep, stepLint, 'Lint failed', 'error', String(e));
212
+ throw e;
213
+ }
214
+ }
215
+ // ── Compile ──────────────────────────────────────────────────────────────────
216
+ if (stackInfo.compileCmd) {
217
+ const stepCompile = 'compile';
218
+ report(onStep, stepCompile, `Compile: ${stackInfo.compileCmd}`, 'running');
219
+ try {
220
+ await runStep(stackInfo.compileCmd, projectDir, noDocker, timeout);
221
+ report(onStep, stepCompile, 'Compile passed', 'done');
222
+ }
223
+ catch (e) {
224
+ report(onStep, stepCompile, 'Compile failed', 'error', String(e));
225
+ throw e;
226
+ }
227
+ }
228
+ // ── Test (full mode only) ────────────────────────────────────────────────────
229
+ if (mode === 'full' && stackInfo.testCmd) {
230
+ const stepTest = 'test';
231
+ report(onStep, stepTest, `Test: ${stackInfo.testCmd}`, 'running');
232
+ try {
233
+ await runStep(stackInfo.testCmd, projectDir, noDocker, timeout);
234
+ report(onStep, stepTest, 'Tests passed', 'done');
235
+ }
236
+ catch (e) {
237
+ report(onStep, stepTest, 'Tests failed', 'error', String(e));
238
+ throw e;
239
+ }
240
+ }
241
+ // ── Security scan (full mode only) ──────────────────────────────────────────
242
+ if (mode === 'full' && !noSecurity) {
243
+ const stepSecurity = 'security';
244
+ const semgrepAvailable = await isSemgrepAvailable();
245
+ if (semgrepAvailable) {
246
+ report(onStep, stepSecurity, 'Security scan (Semgrep)', 'running');
247
+ try {
248
+ await runSemgrep(projectDir);
249
+ report(onStep, stepSecurity, 'Security scan passed', 'done');
250
+ }
251
+ catch (e) {
252
+ report(onStep, stepSecurity, 'Security scan failed', 'error', String(e));
253
+ throw e;
254
+ }
255
+ }
256
+ else {
257
+ report(onStep, stepSecurity, 'Security scan', 'skipped', 'Semgrep not available — install semgrep or Docker');
258
+ }
259
+ }
260
+ // ── GHAGGA review (full mode only) ──────────────────────────────────────────
261
+ if (mode === 'full' && !noGhagga) {
262
+ const stepGhagga = 'ghagga';
263
+ const ghagga = await isGhaggaAvailable();
264
+ if (ghagga) {
265
+ report(onStep, stepGhagga, 'GHAGGA review', 'running');
266
+ try {
267
+ await runGhagga(projectDir);
268
+ report(onStep, stepGhagga, 'GHAGGA review passed', 'done');
269
+ }
270
+ catch (e) {
271
+ report(onStep, stepGhagga, 'GHAGGA review failed', 'error', String(e));
272
+ throw e;
273
+ }
274
+ }
275
+ else {
276
+ report(onStep, stepGhagga, 'GHAGGA review', 'skipped', 'ghagga not installed');
277
+ }
278
+ }
279
+ }
280
+ // =============================================================================
281
+ // Step runners
282
+ // =============================================================================
283
+ async function runStep(command, projectDir, noDocker, timeout) {
284
+ if (noDocker) {
285
+ // Run natively
286
+ await new Promise((resolve, reject) => {
287
+ const proc = spawn('bash', ['-c', command], {
288
+ cwd: projectDir,
289
+ stdio: 'inherit',
290
+ env: { ...process.env, CI: 'true' },
291
+ });
292
+ proc.on('close', code => code === 0 ? resolve() : reject(new Error(`Command failed with code ${code}`)));
293
+ proc.on('error', reject);
294
+ });
295
+ }
296
+ else {
297
+ const result = await runInContainer({
298
+ projectDir,
299
+ command: `cd /home/runner/work && ${command}`,
300
+ timeout,
301
+ stream: true,
302
+ });
303
+ if (result.exitCode !== 0) {
304
+ throw new Error(`Command failed with exit code ${result.exitCode}`);
305
+ }
306
+ }
307
+ }
308
+ async function isSemgrepAvailable() {
309
+ try {
310
+ await execFileAsync('semgrep', ['--version'], { timeout: 3000 });
311
+ return true;
312
+ }
313
+ catch {
314
+ return false;
315
+ }
316
+ }
317
+ async function runSemgrep(projectDir) {
318
+ // Look for semgrep config in project or use auto
319
+ const semgrepConfig = await fs.pathExists(path.join(projectDir, '.semgrep.yml'))
320
+ ? path.join(projectDir, '.semgrep.yml')
321
+ : 'auto';
322
+ await new Promise((resolve, reject) => {
323
+ const proc = spawn('semgrep', ['--config', semgrepConfig, '--severity', 'ERROR', '--quiet', '.'], {
324
+ cwd: projectDir,
325
+ stdio: 'inherit',
326
+ });
327
+ proc.on('close', code => code === 0 ? resolve() : reject(new Error(`Semgrep found issues (exit ${code})`)));
328
+ proc.on('error', reject);
329
+ });
330
+ }
331
+ async function runGhagga(projectDir) {
332
+ await new Promise((resolve, reject) => {
333
+ const proc = spawn('ghagga', ['review', '--plain', '--exit-on-issues'], {
334
+ cwd: projectDir,
335
+ stdio: 'inherit',
336
+ });
337
+ proc.on('close', code => code === 0 ? resolve() : reject(new Error(`GHAGGA review found issues (exit ${code})`)));
338
+ proc.on('error', reject);
339
+ });
340
+ }
341
+ //# sourceMappingURL=ci.js.map
@@ -49,6 +49,11 @@ export async function initProject(options, onStep) {
49
49
  // Set core.hooksPath to ci-local/hooks
50
50
  const hooksDir = path.join(ciLocalDest, 'hooks');
51
51
  if (await fs.pathExists(hooksDir)) {
52
+ // Ensure hooks are executable
53
+ const hookFiles = await fs.readdir(hooksDir);
54
+ for (const hook of hookFiles) {
55
+ await fs.chmod(path.join(hooksDir, hook), 0o755);
56
+ }
52
57
  await execFileAsync('git', ['config', 'core.hooksPath', 'ci-local/hooks'], { cwd: projectDir });
53
58
  }
54
59
  }
package/dist/index.js CHANGED
@@ -2,18 +2,26 @@
2
2
  import React from 'react';
3
3
  import { render } from 'ink';
4
4
  import meow from 'meow';
5
+ import updateNotifier from 'update-notifier';
6
+ import { createRequire } from 'module';
5
7
  import App from './ui/App.js';
6
8
  import Doctor from './ui/Doctor.js';
7
9
  import AnalyzeUI from './ui/AnalyzeUI.js';
8
10
  import Plugin from './ui/Plugin.js';
9
11
  import LlmsTxt from './ui/LlmsTxt.js';
12
+ import CI from './ui/CI.js';
10
13
  import { CIProvider as CIContextProvider } from './ui/CIContext.js';
14
+ // Check for updates in background (non-blocking, cached 24h)
15
+ const _require = createRequire(import.meta.url);
16
+ const pkg = _require('../package.json');
17
+ updateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60 * 24 }).notify();
11
18
  const cli = meow(`
12
19
  Usage
13
20
  $ javi-forge [command] [options]
14
21
 
15
22
  Commands
16
23
  init Bootstrap a new project (default)
24
+ ci Run CI simulation (lint + compile + test + security + ghagga)
17
25
  analyze Run repoforge skills analysis
18
26
  doctor Show health report
19
27
  plugin add Install a plugin from GitHub (org/repo)
@@ -35,19 +43,28 @@ const cli = meow(`
35
43
  --version Show version
36
44
  --help Show this help
37
45
 
46
+ CI options (javi-forge ci)
47
+ --quick Lint + compile only (fast, for pre-commit)
48
+ --shell Open interactive shell in CI container
49
+ --detect Show detected stack and exit
50
+ --no-docker Run commands natively (no Docker)
51
+ --no-ghagga Skip GHAGGA review
52
+ --no-security Skip Semgrep security scan
53
+ --timeout N Per-step timeout in seconds (default: 600)
54
+
38
55
  Examples
39
56
  $ javi-forge
40
57
  $ javi-forge init --dry-run
41
58
  $ javi-forge init --stack node --ci github
42
- $ javi-forge init --dry-run --project-name app --stack node --ci github --batch
59
+ $ javi-forge ci
60
+ $ javi-forge ci --quick
61
+ $ javi-forge ci --no-ghagga --no-security
62
+ $ javi-forge ci --no-docker
63
+ $ javi-forge ci --shell
43
64
  $ javi-forge analyze
44
- $ javi-forge analyze --dry-run
45
65
  $ javi-forge doctor
46
66
  $ javi-forge plugin add mapbox/agent-skills
47
67
  $ javi-forge plugin list
48
- $ javi-forge plugin search ai
49
- $ javi-forge plugin validate ./my-plugin
50
- $ javi-forge plugin remove my-plugin
51
68
  `, {
52
69
  importMeta: import.meta,
53
70
  flags: {
@@ -59,6 +76,14 @@ const cli = meow(`
59
76
  ghagga: { type: 'boolean', default: false },
60
77
  mock: { type: 'boolean', default: false },
61
78
  batch: { type: 'boolean', default: false },
79
+ // CI flags
80
+ quick: { type: 'boolean', default: false },
81
+ shell: { type: 'boolean', default: false },
82
+ detect: { type: 'boolean', default: false },
83
+ noDocker: { type: 'boolean', default: false },
84
+ noGhagga: { type: 'boolean', default: false },
85
+ noSecurity: { type: 'boolean', default: false },
86
+ timeout: { type: 'number', default: 600 },
62
87
  }
63
88
  });
64
89
  const subcommand = cli.input[0] ?? 'init';
@@ -67,6 +92,15 @@ const VALID_CI = ['github', 'gitlab', 'woodpecker'];
67
92
  const VALID_MEMORY = ['engram', 'obsidian-brain', 'memory-simple', 'none'];
68
93
  const isCI = cli.flags.batch || process.env['CI'] === '1' || process.env['CI'] === 'true';
69
94
  switch (subcommand) {
95
+ case 'ci': {
96
+ const ciMode = cli.flags.detect ? 'detect'
97
+ : cli.flags.shell ? 'shell'
98
+ : cli.flags.quick ? 'quick'
99
+ : 'full';
100
+ render(React.createElement(CIContextProvider, { isCI: true },
101
+ React.createElement(CI, { projectDir: process.cwd(), mode: ciMode, noDocker: cli.flags.noDocker, noGhagga: cli.flags.noGhagga, noSecurity: cli.flags.noSecurity, timeout: cli.flags.timeout })));
102
+ break;
103
+ }
70
104
  case 'doctor': {
71
105
  render(React.createElement(CIContextProvider, { isCI: isCI },
72
106
  React.createElement(Doctor, null)));
@@ -0,0 +1,43 @@
1
+ import type { Stack } from '../types/index.js';
2
+ export interface DockerRunOptions {
3
+ /** Absolute path to mount as /home/runner/work */
4
+ projectDir: string;
5
+ /** Command to run inside the container */
6
+ command: string;
7
+ /** Timeout in seconds (default: 600) */
8
+ timeout?: number;
9
+ /** Stream output to stdout/stderr (default: true) */
10
+ stream?: boolean;
11
+ }
12
+ export interface DockerRunResult {
13
+ exitCode: number;
14
+ stdout: string;
15
+ stderr: string;
16
+ }
17
+ export interface DockerImageOptions {
18
+ stack: Stack;
19
+ /** Java version override (only for java-* stacks) */
20
+ javaVersion?: string;
21
+ /** Directory where Dockerfiles are stored (defaults to package-bundled dir) */
22
+ dockerfilesDir?: string;
23
+ }
24
+ export declare function getImageName(stack: Stack): string;
25
+ export declare function getDockerfileContent(stack: Stack): string;
26
+ export declare function isDockerAvailable(): Promise<boolean>;
27
+ /**
28
+ * Ensure a CI Docker image exists and is up-to-date.
29
+ * Rebuilds only if the Dockerfile content has changed (hash-based staleness check).
30
+ * Returns the image name.
31
+ */
32
+ export declare function ensureImage(options: DockerImageOptions): Promise<string>;
33
+ /**
34
+ * Run a shell command inside the CI Docker container.
35
+ * Mounts projectDir as /home/runner/work.
36
+ * Streams output to process.stdout/stderr by default.
37
+ */
38
+ export declare function runInContainer(options: DockerRunOptions): Promise<DockerRunResult>;
39
+ /**
40
+ * Open an interactive shell inside the CI container.
41
+ */
42
+ export declare function openShell(projectDir: string): Promise<void>;
43
+ //# sourceMappingURL=docker.d.ts.map
@@ -0,0 +1,223 @@
1
+ import { execFile, spawn } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import crypto from 'crypto';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+ const execFileAsync = promisify(execFile);
7
+ // =============================================================================
8
+ // Image name
9
+ // =============================================================================
10
+ export function getImageName(stack) {
11
+ return `javi-forge-ci-${stack}`;
12
+ }
13
+ // =============================================================================
14
+ // Dockerfile content per stack
15
+ // =============================================================================
16
+ export function getDockerfileContent(stack) {
17
+ switch (stack) {
18
+ case 'java-gradle':
19
+ case 'java-maven':
20
+ return [
21
+ 'ARG JAVA_VERSION=21',
22
+ 'FROM eclipse-temurin:${JAVA_VERSION}-jdk-noble',
23
+ 'RUN apt-get update && apt-get install -y git curl unzip && rm -rf /var/lib/apt/lists/*',
24
+ 'RUN useradd -m -s /bin/bash runner',
25
+ 'USER runner',
26
+ 'WORKDIR /home/runner/work',
27
+ 'ENTRYPOINT ["/bin/bash", "-c"]',
28
+ ].join('\n');
29
+ case 'node':
30
+ return [
31
+ 'FROM node:22-slim',
32
+ 'RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*',
33
+ 'RUN npm install -g pnpm',
34
+ 'RUN useradd -m -s /bin/bash runner',
35
+ 'USER runner',
36
+ 'WORKDIR /home/runner/work',
37
+ 'ENTRYPOINT ["/bin/bash", "-c"]',
38
+ ].join('\n');
39
+ case 'python':
40
+ return [
41
+ 'FROM python:3.12-slim',
42
+ 'RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*',
43
+ 'RUN pip install --no-cache-dir pytest ruff pylint poetry',
44
+ 'RUN useradd -m -s /bin/bash runner',
45
+ 'USER runner',
46
+ 'WORKDIR /home/runner/work',
47
+ 'ENTRYPOINT ["/bin/bash", "-c"]',
48
+ ].join('\n');
49
+ case 'go':
50
+ return [
51
+ 'FROM golang:1.23-bookworm',
52
+ 'RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*',
53
+ 'RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 && mv /root/go/bin/golangci-lint /usr/local/bin/',
54
+ 'RUN useradd -m -s /bin/bash runner',
55
+ 'USER runner',
56
+ 'WORKDIR /home/runner/work',
57
+ 'ENTRYPOINT ["/bin/bash", "-c"]',
58
+ ].join('\n');
59
+ case 'rust':
60
+ return [
61
+ 'FROM rust:1.83-slim',
62
+ 'RUN apt-get update && apt-get install -y git pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*',
63
+ 'RUN rustup component add clippy rustfmt',
64
+ 'RUN useradd -m -s /bin/bash runner',
65
+ 'USER runner',
66
+ 'WORKDIR /home/runner/work',
67
+ 'ENTRYPOINT ["/bin/bash", "-c"]',
68
+ ].join('\n');
69
+ default:
70
+ return [
71
+ 'FROM ubuntu:24.04',
72
+ 'RUN apt-get update && apt-get install -y git curl && rm -rf /var/lib/apt/lists/*',
73
+ 'RUN useradd -m -s /bin/bash runner',
74
+ 'USER runner',
75
+ 'WORKDIR /home/runner/work',
76
+ 'ENTRYPOINT ["/bin/bash", "-c"]',
77
+ ].join('\n');
78
+ }
79
+ }
80
+ // =============================================================================
81
+ // Docker availability
82
+ // =============================================================================
83
+ export async function isDockerAvailable() {
84
+ try {
85
+ await execFileAsync('docker', ['info'], { timeout: 5000 });
86
+ return true;
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
92
+ // =============================================================================
93
+ // Image management
94
+ // =============================================================================
95
+ /**
96
+ * Ensure a CI Docker image exists and is up-to-date.
97
+ * Rebuilds only if the Dockerfile content has changed (hash-based staleness check).
98
+ * Returns the image name.
99
+ */
100
+ export async function ensureImage(options) {
101
+ const { stack, javaVersion, dockerfilesDir } = options;
102
+ const imageName = getImageName(stack);
103
+ // Resolve Dockerfile path
104
+ const dockerDir = dockerfilesDir ?? path.join(path.dirname(new URL(import.meta.url).pathname), '../../ci-local/docker');
105
+ const dockerfilePath = path.join(dockerDir, `${stack}.Dockerfile`);
106
+ // Write Dockerfile if it doesn't exist yet (first run)
107
+ if (!await fs.pathExists(dockerfilePath)) {
108
+ await fs.ensureDir(dockerDir);
109
+ await fs.writeFile(dockerfilePath, getDockerfileContent(stack), 'utf-8');
110
+ }
111
+ // Staleness check: compare Dockerfile hash with the one embedded in the image label
112
+ const content = await fs.readFile(dockerfilePath, 'utf-8');
113
+ const currentHash = crypto.createHash('sha256').update(content).digest('hex');
114
+ let imageHash = '';
115
+ try {
116
+ const { stdout } = await execFileAsync('docker', [
117
+ 'inspect', '--format', '{{index .Config.Labels "dockerfile-hash"}}', imageName,
118
+ ]);
119
+ imageHash = stdout.trim();
120
+ }
121
+ catch {
122
+ // Image doesn't exist yet
123
+ }
124
+ if (currentHash === imageHash) {
125
+ return imageName;
126
+ }
127
+ // Build image
128
+ const buildArgs = [
129
+ 'build',
130
+ '--label', `dockerfile-hash=${currentHash}`,
131
+ '-f', dockerfilePath,
132
+ '-t', imageName,
133
+ ];
134
+ if (javaVersion && (stack === 'java-gradle' || stack === 'java-maven')) {
135
+ buildArgs.push('--build-arg', `JAVA_VERSION=${javaVersion}`);
136
+ }
137
+ buildArgs.push(dockerDir);
138
+ await new Promise((resolve, reject) => {
139
+ const proc = spawn('docker', buildArgs, { stdio: 'inherit' });
140
+ proc.on('close', code => code === 0 ? resolve() : reject(new Error(`docker build exited with code ${code}`)));
141
+ proc.on('error', reject);
142
+ });
143
+ return imageName;
144
+ }
145
+ // =============================================================================
146
+ // Run command in container
147
+ // =============================================================================
148
+ /**
149
+ * Run a shell command inside the CI Docker container.
150
+ * Mounts projectDir as /home/runner/work.
151
+ * Streams output to process.stdout/stderr by default.
152
+ */
153
+ export async function runInContainer(options) {
154
+ const { projectDir, command, timeout = 600, stream = true } = options;
155
+ const stack = await detectStackFromDir(projectDir);
156
+ const imageName = getImageName(stack);
157
+ const isInteractive = process.stdin.isTTY && stream;
158
+ const dockerArgs = [
159
+ 'run', '--rm',
160
+ ...(isInteractive ? ['-it'] : []),
161
+ '--stop-timeout', '30',
162
+ '--entrypoint', '',
163
+ '-v', `${projectDir}:/home/runner/work`,
164
+ '-e', 'CI=true',
165
+ imageName,
166
+ 'timeout', String(timeout), 'bash', '-c', command,
167
+ ];
168
+ return new Promise((resolve, reject) => {
169
+ const proc = spawn('docker', dockerArgs, {
170
+ stdio: stream ? 'inherit' : 'pipe',
171
+ });
172
+ let stdout = '';
173
+ let stderr = '';
174
+ if (!stream) {
175
+ proc.stdout?.on('data', (d) => { stdout += d.toString(); });
176
+ proc.stderr?.on('data', (d) => { stderr += d.toString(); });
177
+ }
178
+ proc.on('close', code => resolve({ exitCode: code ?? 1, stdout, stderr }));
179
+ proc.on('error', reject);
180
+ });
181
+ }
182
+ /**
183
+ * Open an interactive shell inside the CI container.
184
+ */
185
+ export async function openShell(projectDir) {
186
+ const stack = await detectStackFromDir(projectDir);
187
+ const imageName = getImageName(stack);
188
+ await new Promise((resolve, reject) => {
189
+ const proc = spawn('docker', [
190
+ 'run', '--rm', '-it',
191
+ '--entrypoint', '',
192
+ '-v', `${projectDir}:/home/runner/work`,
193
+ '-e', 'CI=true',
194
+ imageName,
195
+ 'bash', '-c', 'cd /home/runner/work && exec bash',
196
+ ], { stdio: 'inherit' });
197
+ proc.on('close', () => resolve());
198
+ proc.on('error', reject);
199
+ });
200
+ }
201
+ // =============================================================================
202
+ // Internal helpers
203
+ // =============================================================================
204
+ async function detectStackFromDir(projectDir) {
205
+ if (await fs.pathExists(path.join(projectDir, 'build.gradle.kts')))
206
+ return 'java-gradle';
207
+ if (await fs.pathExists(path.join(projectDir, 'build.gradle')))
208
+ return 'java-gradle';
209
+ if (await fs.pathExists(path.join(projectDir, 'pom.xml')))
210
+ return 'java-maven';
211
+ if (await fs.pathExists(path.join(projectDir, 'package.json')))
212
+ return 'node';
213
+ if (await fs.pathExists(path.join(projectDir, 'go.mod')))
214
+ return 'go';
215
+ if (await fs.pathExists(path.join(projectDir, 'Cargo.toml')))
216
+ return 'rust';
217
+ if (await fs.pathExists(path.join(projectDir, 'pyproject.toml')) ||
218
+ await fs.pathExists(path.join(projectDir, 'requirements.txt')) ||
219
+ await fs.pathExists(path.join(projectDir, 'setup.py')))
220
+ return 'python';
221
+ return 'node'; // fallback
222
+ }
223
+ //# sourceMappingURL=docker.js.map
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { CIOptions } from '../commands/ci.js';
3
+ interface CIProps extends CIOptions {
4
+ /** Called when CI finishes (success or failure) */
5
+ onDone?: (success: boolean) => void;
6
+ }
7
+ export default function CI(props: CIProps): React.JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=CI.d.ts.map
package/dist/ui/CI.js ADDED
@@ -0,0 +1,91 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { Box, Text, useApp } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import { runCI } from '../commands/ci.js';
5
+ import Header from './Header.js';
6
+ import { theme } from './theme.js';
7
+ // =============================================================================
8
+ // Icons
9
+ // =============================================================================
10
+ const STATUS_ICON = {
11
+ pending: '○',
12
+ running: '●',
13
+ done: '✓',
14
+ error: '✗',
15
+ skipped: '–',
16
+ };
17
+ const STATUS_COLOR = {
18
+ pending: theme.muted,
19
+ running: theme.warning,
20
+ done: theme.success,
21
+ error: theme.error,
22
+ skipped: theme.muted,
23
+ };
24
+ // =============================================================================
25
+ // Component
26
+ // =============================================================================
27
+ export default function CI(props) {
28
+ const { exit } = useApp();
29
+ const [steps, setSteps] = useState([]);
30
+ const [done, setDone] = useState(false);
31
+ const [success, setSuccess] = useState(null);
32
+ const started = useRef(false);
33
+ useEffect(() => {
34
+ if (started.current)
35
+ return;
36
+ started.current = true;
37
+ const onStep = (step) => {
38
+ setSteps(prev => {
39
+ const idx = prev.findIndex(s => s.id === step.id);
40
+ if (idx >= 0) {
41
+ const next = [...prev];
42
+ next[idx] = step;
43
+ return next;
44
+ }
45
+ return [...prev, step];
46
+ });
47
+ };
48
+ runCI(props, onStep)
49
+ .then(() => {
50
+ setSuccess(true);
51
+ setDone(true);
52
+ props.onDone?.(true);
53
+ setTimeout(() => exit(), 200);
54
+ })
55
+ .catch(() => {
56
+ setSuccess(false);
57
+ setDone(true);
58
+ props.onDone?.(false);
59
+ // Give user time to read the error before exiting with failure code
60
+ setTimeout(() => {
61
+ exit();
62
+ process.exitCode = 1;
63
+ }, 300);
64
+ });
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ }, []);
67
+ const mode = props.mode ?? 'full';
68
+ const subtitle = mode === 'quick' ? 'ci — quick' : mode === 'shell' ? 'ci — shell' : 'ci';
69
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
70
+ React.createElement(Header, { subtitle: subtitle }),
71
+ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, steps.map(step => (React.createElement(Box, { key: step.id },
72
+ React.createElement(Text, { color: STATUS_COLOR[step.status] },
73
+ step.status === 'running'
74
+ ? React.createElement(Spinner, { type: "dots" })
75
+ : `${STATUS_ICON[step.status]} `,
76
+ step.label,
77
+ step.detail
78
+ ? React.createElement(Text, { color: theme.muted, dimColor: true },
79
+ ' ',
80
+ step.detail)
81
+ : null))))),
82
+ done && success === true && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
83
+ React.createElement(Text, { color: theme.success, bold: true }, "\u2713 CI passed \u2014 safe to push!"))),
84
+ done && success === false && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
85
+ React.createElement(Text, { color: theme.error, bold: true }, "\u2717 CI failed \u2014 fix the issues above before pushing."),
86
+ React.createElement(Text, { color: theme.muted, dimColor: true }, " To skip: git push --no-verify"))),
87
+ !done && steps.length === 0 && (React.createElement(Text, { color: theme.warning },
88
+ React.createElement(Spinner, { type: "dots" }),
89
+ ' Starting CI...'))));
90
+ }
91
+ //# sourceMappingURL=CI.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,9 +23,14 @@
23
23
  "ink-spinner": "^5.0.0",
24
24
  "meow": "^14.1.0",
25
25
  "react": "^19.2.4",
26
+ "update-notifier": "^7.3.1",
26
27
  "yaml": "^2.8.2"
27
28
  },
28
29
  "devDependencies": {
30
+ "@semantic-release/changelog": "^6.0.3",
31
+ "@semantic-release/exec": "^7.1.0",
32
+ "@semantic-release/git": "^10.0.1",
33
+ "@semantic-release/github": "^12.0.6",
29
34
  "@stryker-mutator/core": "^9.6.0",
30
35
  "@stryker-mutator/typescript-checker": "^9.6.0",
31
36
  "@stryker-mutator/vitest-runner": "^9.6.0",
@@ -33,7 +38,10 @@
33
38
  "@types/glob": "^9.0.0",
34
39
  "@types/node": "^25.5.0",
35
40
  "@types/react": "^19.2.14",
41
+ "@types/update-notifier": "^6.0.8",
36
42
  "@vitest/coverage-v8": "^4.1.0",
43
+ "conventional-changelog-conventionalcommits": "^9.3.0",
44
+ "semantic-release": "^25.0.3",
37
45
  "tsx": "^4.21.0",
38
46
  "typescript": "^5.9.3",
39
47
  "vitest": "^4.1.0"