javi-forge 1.0.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.
- package/ai-config/skills/docs/api-documentation/SKILL.md +293 -0
- package/ai-config/skills/docs/docs-spring/SKILL.md +377 -0
- package/ai-config/skills/docs/mustache-templates/SKILL.md +190 -0
- package/ai-config/skills/docs/technical-docs/SKILL.md +447 -0
- package/ci-local/ci-local.sh +37 -3
- package/ci-local/docker/node.Dockerfile +7 -0
- package/ci-local/hooks/commit-msg +0 -0
- package/ci-local/hooks/pre-commit +10 -155
- package/ci-local/hooks/pre-push +12 -29
- package/ci-local/install.sh +0 -0
- package/dist/commands/ci.d.ts +33 -0
- package/dist/commands/ci.js +341 -0
- package/dist/commands/init.js +5 -0
- package/dist/index.js +39 -5
- package/dist/lib/docker.d.ts +43 -0
- package/dist/lib/docker.js +223 -0
- package/dist/ui/CI.d.ts +9 -0
- package/dist/ui/CI.js +91 -0
- package/lib/common.sh +183 -0
- package/modules/obsidian-brain/.obsidian/plugins/dataview/data.json +25 -0
- package/modules/obsidian-brain/.obsidian/plugins/obsidian-kanban/data.json +29 -0
- package/modules/obsidian-brain/.obsidian/plugins/templater-obsidian/data.json +18 -0
- package/package.json +20 -12
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
|
|
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
|
package/dist/ui/CI.d.ts
ADDED
|
@@ -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/lib/common.sh
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# lib/common.sh - Shared functions for project-starter-framework
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Source from scripts: source "$(dirname "$0")/../lib/common.sh"
|
|
6
|
+
# Source from ci-local: source "$(dirname "$0")/../lib/common.sh"
|
|
7
|
+
# Source from hooks: source "$(dirname "$0")/../../lib/common.sh"
|
|
8
|
+
# =============================================================================
|
|
9
|
+
|
|
10
|
+
# Guard against double-sourcing
|
|
11
|
+
if [[ -n "${_COMMON_SH_LOADED:-}" ]]; then
|
|
12
|
+
return 0 2>/dev/null || true
|
|
13
|
+
fi
|
|
14
|
+
_COMMON_SH_LOADED=1
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Colors (exported for callers via source)
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# shellcheck disable=SC2034
|
|
20
|
+
RED='\033[0;31m'
|
|
21
|
+
# shellcheck disable=SC2034
|
|
22
|
+
GREEN='\033[0;32m'
|
|
23
|
+
# shellcheck disable=SC2034
|
|
24
|
+
YELLOW='\033[1;33m'
|
|
25
|
+
# shellcheck disable=SC2034
|
|
26
|
+
CYAN='\033[0;36m'
|
|
27
|
+
# shellcheck disable=SC2034
|
|
28
|
+
BLUE='\033[0;34m'
|
|
29
|
+
# shellcheck disable=SC2034
|
|
30
|
+
NC='\033[0m'
|
|
31
|
+
|
|
32
|
+
# =============================================================================
|
|
33
|
+
# Shared logging helpers
|
|
34
|
+
# =============================================================================
|
|
35
|
+
log_ok() { echo -e " ${GREEN}[OK]${NC} $1"; }
|
|
36
|
+
log_warn() { echo -e " ${YELLOW}[WARN]${NC} $1"; }
|
|
37
|
+
log_fail() { echo -e " ${RED}[FAIL]${NC} $1"; }
|
|
38
|
+
log_info() { echo -e " ${CYAN}[INFO]${NC} $1"; }
|
|
39
|
+
log_step() { echo -e "${YELLOW}$1${NC}"; }
|
|
40
|
+
|
|
41
|
+
# =============================================================================
|
|
42
|
+
# sed_inplace - Portable sed -i (works on both GNU and BSD/macOS sed)
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# Usage: sed_inplace "s/foo/bar/" file.txt
|
|
45
|
+
# =============================================================================
|
|
46
|
+
sed_inplace() {
|
|
47
|
+
if sed --version 2>/dev/null | grep -q GNU; then
|
|
48
|
+
sed -i "$@"
|
|
49
|
+
else
|
|
50
|
+
sed -i '' "$@"
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# escape_sed - Escape special characters for safe use in sed replacement
|
|
56
|
+
# =============================================================================
|
|
57
|
+
# Usage: escaped=$(escape_sed "$raw_string")
|
|
58
|
+
# Note: This escapes replacement-side characters (backslash, ampersand, slash).
|
|
59
|
+
# =============================================================================
|
|
60
|
+
escape_sed() {
|
|
61
|
+
printf '%s\n' "$1" | sed -e 's/[\\&/]/\\&/g'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# =============================================================================
|
|
65
|
+
# backup_if_exists - Create a .bak copy of a file before overwriting
|
|
66
|
+
# =============================================================================
|
|
67
|
+
# Usage: backup_if_exists "path/to/file"
|
|
68
|
+
# =============================================================================
|
|
69
|
+
backup_if_exists() {
|
|
70
|
+
local file="$1"
|
|
71
|
+
if [[ -f "$file" ]]; then
|
|
72
|
+
cp "$file" "${file}.bak"
|
|
73
|
+
echo -e "${YELLOW} Backed up existing ${file}${NC}"
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# =============================================================================
|
|
78
|
+
# detect_stack - Auto-detect project technology stack
|
|
79
|
+
# =============================================================================
|
|
80
|
+
# Sets: STACK_TYPE, BUILD_TOOL, JAVA_VERSION
|
|
81
|
+
# Detects: java-gradle, java-maven, node, python, go, rust
|
|
82
|
+
#
|
|
83
|
+
# Usage:
|
|
84
|
+
# detect_stack # Detects from current directory
|
|
85
|
+
# detect_stack "/path/to/project" # Detects from given directory
|
|
86
|
+
#
|
|
87
|
+
# NOTE: Does NOT set LINT_CMD/COMPILE_CMD/TEST_CMD. Those are CI-specific
|
|
88
|
+
# and should be configured by the caller (e.g., ci-local.sh).
|
|
89
|
+
# =============================================================================
|
|
90
|
+
# shellcheck disable=SC2034 # STACK_TYPE, BUILD_TOOL, JAVA_VERSION used by callers
|
|
91
|
+
detect_stack() {
|
|
92
|
+
local project_dir="${1:-.}"
|
|
93
|
+
|
|
94
|
+
STACK_TYPE="unknown"
|
|
95
|
+
BUILD_TOOL=""
|
|
96
|
+
JAVA_VERSION="21"
|
|
97
|
+
|
|
98
|
+
# Java + Gradle
|
|
99
|
+
if [[ -f "$project_dir/build.gradle" || -f "$project_dir/build.gradle.kts" ]]; then
|
|
100
|
+
STACK_TYPE="java-gradle"
|
|
101
|
+
BUILD_TOOL="gradle"
|
|
102
|
+
|
|
103
|
+
# Detect Java version from build files (compatible with macOS and Linux)
|
|
104
|
+
if [[ -f "$project_dir/build.gradle.kts" ]]; then
|
|
105
|
+
JAVA_VERSION=$(grep -E 'languageVersion\s*=\s*JavaLanguageVersion\.of\(' "$project_dir/build.gradle.kts" 2>/dev/null | grep -o '[0-9]\+' | head -1 || echo "21")
|
|
106
|
+
elif [[ -f "$project_dir/build.gradle" ]]; then
|
|
107
|
+
JAVA_VERSION=$(grep -E 'sourceCompatibility\s*=' "$project_dir/build.gradle" 2>/dev/null | grep -o '[0-9]\+' | head -1 || echo "21")
|
|
108
|
+
fi
|
|
109
|
+
[[ -z "$JAVA_VERSION" ]] && JAVA_VERSION="21"
|
|
110
|
+
return
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# Java + Maven
|
|
114
|
+
if [[ -f "$project_dir/pom.xml" ]]; then
|
|
115
|
+
STACK_TYPE="java-maven"
|
|
116
|
+
BUILD_TOOL="maven"
|
|
117
|
+
return
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Node.js
|
|
121
|
+
if [[ -f "$project_dir/package.json" ]]; then
|
|
122
|
+
STACK_TYPE="node"
|
|
123
|
+
if [[ -f "$project_dir/pnpm-lock.yaml" ]]; then
|
|
124
|
+
BUILD_TOOL="pnpm"
|
|
125
|
+
elif [[ -f "$project_dir/yarn.lock" ]]; then
|
|
126
|
+
BUILD_TOOL="yarn"
|
|
127
|
+
else
|
|
128
|
+
BUILD_TOOL="npm"
|
|
129
|
+
fi
|
|
130
|
+
return
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# Python (detection order: uv > poetry > pipenv > pip)
|
|
134
|
+
if [[ -f "$project_dir/pyproject.toml" || -f "$project_dir/setup.py" || -f "$project_dir/requirements.txt" ]]; then
|
|
135
|
+
STACK_TYPE="python"
|
|
136
|
+
if [[ -f "$project_dir/uv.lock" ]]; then
|
|
137
|
+
BUILD_TOOL="uv"
|
|
138
|
+
elif [[ -f "$project_dir/poetry.lock" ]]; then
|
|
139
|
+
BUILD_TOOL="poetry"
|
|
140
|
+
elif [[ -f "$project_dir/Pipfile" ]]; then
|
|
141
|
+
BUILD_TOOL="pipenv"
|
|
142
|
+
else
|
|
143
|
+
BUILD_TOOL="pip"
|
|
144
|
+
fi
|
|
145
|
+
return
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Go
|
|
149
|
+
if [[ -f "$project_dir/go.mod" ]]; then
|
|
150
|
+
STACK_TYPE="go"
|
|
151
|
+
BUILD_TOOL="go"
|
|
152
|
+
return
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# Rust
|
|
156
|
+
if [[ -f "$project_dir/Cargo.toml" ]]; then
|
|
157
|
+
STACK_TYPE="rust"
|
|
158
|
+
BUILD_TOOL="cargo"
|
|
159
|
+
return
|
|
160
|
+
fi
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# =============================================================================
|
|
164
|
+
# detect_framework - Locate the project-starter-framework directory
|
|
165
|
+
# =============================================================================
|
|
166
|
+
# Sets: FRAMEWORK_DIR (path to framework root, or empty string)
|
|
167
|
+
# HAS_OPTIONAL (true/false if optional/ dir exists)
|
|
168
|
+
#
|
|
169
|
+
# Usage: detect_framework
|
|
170
|
+
# =============================================================================
|
|
171
|
+
# shellcheck disable=SC2034 # FRAMEWORK_DIR, HAS_OPTIONAL used by callers
|
|
172
|
+
detect_framework() {
|
|
173
|
+
FRAMEWORK_DIR=""
|
|
174
|
+
HAS_OPTIONAL=false
|
|
175
|
+
if [[ -d "templates" && -d ".ai-config" ]]; then
|
|
176
|
+
FRAMEWORK_DIR="."
|
|
177
|
+
elif [[ -d "../templates" && -d "../.ai-config" ]]; then
|
|
178
|
+
FRAMEWORK_DIR=".."
|
|
179
|
+
fi
|
|
180
|
+
if [[ -n "$FRAMEWORK_DIR" && -d "$FRAMEWORK_DIR/optional" ]]; then
|
|
181
|
+
HAS_OPTIONAL=true
|
|
182
|
+
fi
|
|
183
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"renderNullAs": "\\-",
|
|
3
|
+
"taskCompletionTracking": true,
|
|
4
|
+
"taskCompletionUseEmojiShorthand": false,
|
|
5
|
+
"taskCompletionText": "completion",
|
|
6
|
+
"taskCompletionDateFormat": "yyyy-MM-dd",
|
|
7
|
+
"recursiveSubTaskCompletion": false,
|
|
8
|
+
"warnOnEmptyResult": true,
|
|
9
|
+
"refreshEnabled": true,
|
|
10
|
+
"refreshInterval": 2500,
|
|
11
|
+
"defaultDateFormat": "MMMM dd, yyyy",
|
|
12
|
+
"defaultDateTimeFormat": "h:mm a - MMMM dd, yyyy",
|
|
13
|
+
"maxRecursiveRenderDepth": 4,
|
|
14
|
+
"tableIdColumnName": "File",
|
|
15
|
+
"tableGroupColumnName": "Group",
|
|
16
|
+
"showResultCount": true,
|
|
17
|
+
"allowHtml": false,
|
|
18
|
+
"inlineQueryPrefix": "=",
|
|
19
|
+
"inlineJsQueryPrefix": "$=",
|
|
20
|
+
"inlineQueriesInCodeblocks": true,
|
|
21
|
+
"enableDataviewJs": false,
|
|
22
|
+
"enableInlineDataviewJs": false,
|
|
23
|
+
"prettyRenderInlineFields": true,
|
|
24
|
+
"prettyRenderInlineFieldsInLivePreview": true
|
|
25
|
+
}
|