agents-md-gen 0.9.1 → 0.11.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/CHANGELOG.md +29 -0
- package/README.md +14 -11
- package/package.json +11 -1
- package/src/detect.js +119 -28
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
- Fix: Bun projects using the text `bun.lock` (the default since Bun 1.2)
|
|
6
|
+
were reported as npm. Both `bun.lock` and `bun.lockb` count now.
|
|
7
|
+
- Honor corepack's `packageManager` field in package.json over lockfiles.
|
|
8
|
+
- More JS frameworks: Remix, Expo, React Native, Nuxt, SvelteKit, Astro,
|
|
9
|
+
Angular, Hono, Electron; meta-frameworks replace the library they wrap
|
|
10
|
+
(Nuxt instead of Vue, SvelteKit instead of Svelte). Playwright as a test
|
|
11
|
+
runner.
|
|
12
|
+
- Detect Deno projects (`deno.json`/`deno.jsonc`, its tasks, Fresh/Hono).
|
|
13
|
+
- A lone `Pipfile` (no lock yet) is detected as a pipenv Python project.
|
|
14
|
+
- `homepage` points at the public distribution repo; it used to fall back
|
|
15
|
+
to the private source repo, which 404s for visitors.
|
|
16
|
+
- The README's "real output" example is now checked against the actual
|
|
17
|
+
CLI output by the test suite, so it can't drift again.
|
|
18
|
+
|
|
19
|
+
## 0.10.0
|
|
20
|
+
|
|
21
|
+
- Detect web frameworks in ecosystems that always reported none:
|
|
22
|
+
Django/FastAPI/Flask (Python), Rails/Sinatra (Ruby), Gin/Echo/Fiber/chi
|
|
23
|
+
(Go), Axum/Actix Web/Rocket/Tauri (Rust).
|
|
24
|
+
- Django projects get `python manage.py runserver` as the dev command and
|
|
25
|
+
`python manage.py test` when pytest isn't set up; Rails apps get
|
|
26
|
+
`bin/rails server` and `bin/rails test`.
|
|
27
|
+
- Python commands are prefixed with `pipenv run` for pipenv projects (like
|
|
28
|
+
`poetry run`/`uv run` already were).
|
|
29
|
+
- README: the Scoop install command pointed at a private repo and 404'd;
|
|
30
|
+
it now adds the public `Peshino2012/homebrew-tap` bucket.
|
|
31
|
+
|
|
3
32
|
## 0.9.1
|
|
4
33
|
|
|
5
34
|
- README: the GitHub Action moved to `Peshino2012/homebrew-tap/action`,
|
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ on immediately.
|
|
|
14
14
|
On Windows, you can also install it with [Scoop](https://scoop.sh):
|
|
15
15
|
|
|
16
16
|
```
|
|
17
|
-
scoop
|
|
17
|
+
scoop bucket add peshino2012 https://github.com/Peshino2012/homebrew-tap
|
|
18
|
+
scoop install peshino2012/agents-md-gen
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
On macOS/Linux, install it with [Homebrew](https://brew.sh):
|
|
@@ -52,13 +53,17 @@ docker run --rm -v "$PWD":/workdir ghcr.io/peshino2012/agents-md-gen
|
|
|
52
53
|
|
|
53
54
|
## What it detects
|
|
54
55
|
|
|
55
|
-
- **Node.js / TypeScript** — package manager (npm/yarn/pnpm/bun
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- **
|
|
61
|
-
- **
|
|
56
|
+
- **Node.js / TypeScript** — package manager (npm/yarn/pnpm/bun, honoring the
|
|
57
|
+
`packageManager` field), framework (Next.js, Remix, React, Expo/React
|
|
58
|
+
Native, Nuxt, Vue, SvelteKit, Svelte, Astro, Angular, Express, Fastify,
|
|
59
|
+
NestJS, Hono, Electron), test runner (Vitest, Jest, Mocha, Ava,
|
|
60
|
+
node:test, Playwright)
|
|
61
|
+
- **Deno** — `deno.json`/`deno.jsonc` tasks, Fresh/Hono
|
|
62
|
+
- **Python** — package manager (pip/poetry/uv/pipenv), Django/FastAPI/Flask,
|
|
63
|
+
pytest or the Django test runner, ruff
|
|
64
|
+
- **Rust** — Cargo, workspaces, Axum/Actix Web/Rocket/Tauri, clippy
|
|
65
|
+
- **Go** — go.mod, Gin/Echo/Fiber/chi, golangci-lint
|
|
66
|
+
- **Ruby** — Bundler, Rails/Sinatra, RSpec or minitest
|
|
62
67
|
- **Java/Kotlin** — Maven, Gradle (with wrapper detection)
|
|
63
68
|
- **PHP** — Composer, Laravel/Symfony detection, PHPUnit
|
|
64
69
|
- **.NET/C#** — NuGet, xUnit/NUnit/MSTest detection
|
|
@@ -117,7 +122,7 @@ clobber one you already wrote.
|
|
|
117
122
|
`--check` is for CI: it fails the build if `AGENTS.md` doesn't exist or no
|
|
118
123
|
longer matches what the repo would generate, so a stale file (say, a new
|
|
119
124
|
test framework got added but nobody regenerated it) gets caught instead of
|
|
120
|
-
silently rotting. Pair it with the GitHub Action
|
|
125
|
+
silently rotting. Pair it with the GitHub Action above.
|
|
121
126
|
|
|
122
127
|
## Example
|
|
123
128
|
|
|
@@ -139,7 +144,6 @@ this package's own repo:
|
|
|
139
144
|
|
|
140
145
|
## Structure
|
|
141
146
|
|
|
142
|
-
- `.github/` — GitHub config (CI, templates)
|
|
143
147
|
- `CHANGELOG.md`
|
|
144
148
|
- `Dockerfile`
|
|
145
149
|
- `LICENSE`
|
|
@@ -151,7 +155,6 @@ this package's own repo:
|
|
|
151
155
|
|
|
152
156
|
## Notes
|
|
153
157
|
|
|
154
|
-
- CI: GitHub Actions (test.yml)
|
|
155
158
|
- License: MIT
|
|
156
159
|
- Docker: Dockerfile
|
|
157
160
|
````
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-md-gen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Generate a solid AGENTS.md / CLAUDE.md for any repo by detecting its stack, commands, and structure",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/Peshino2012/homebrew-tap#readme",
|
|
6
7
|
"funding": {
|
|
7
8
|
"type": "individual",
|
|
8
9
|
"url": "https://www.npmjs.com/package/agents-md-gen"
|
|
@@ -54,6 +55,15 @@
|
|
|
54
55
|
"scala",
|
|
55
56
|
"zig",
|
|
56
57
|
"julia",
|
|
58
|
+
"django",
|
|
59
|
+
"fastapi",
|
|
60
|
+
"flask",
|
|
61
|
+
"rails",
|
|
62
|
+
"deno",
|
|
63
|
+
"bun",
|
|
64
|
+
"nuxt",
|
|
65
|
+
"sveltekit",
|
|
66
|
+
"astro",
|
|
57
67
|
"cli",
|
|
58
68
|
"codegen",
|
|
59
69
|
"developer-tools"
|
package/src/detect.js
CHANGED
|
@@ -31,30 +31,49 @@ function detectNode(root) {
|
|
|
31
31
|
const pkg = readJson(root, 'package.json');
|
|
32
32
|
if (!pkg) return null;
|
|
33
33
|
|
|
34
|
+
// corepack's `packageManager` field ("pnpm@9.12.0") is explicit, so it wins
|
|
35
|
+
// over lockfile sniffing -- and it's the only signal when no lockfile is
|
|
36
|
+
// committed.
|
|
37
|
+
const declared = typeof pkg.packageManager === 'string' ? pkg.packageManager.split('@')[0] : null;
|
|
34
38
|
let packageManager = 'npm';
|
|
35
|
-
if (
|
|
39
|
+
if (['npm', 'pnpm', 'yarn', 'bun'].includes(declared)) packageManager = declared;
|
|
40
|
+
else if (exists(root, 'pnpm-lock.yaml')) packageManager = 'pnpm';
|
|
36
41
|
else if (exists(root, 'yarn.lock')) packageManager = 'yarn';
|
|
37
|
-
else if (exists(root, 'bun.lockb')) packageManager = 'bun';
|
|
42
|
+
else if (exists(root, 'bun.lock') || exists(root, 'bun.lockb')) packageManager = 'bun';
|
|
38
43
|
|
|
39
44
|
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
40
45
|
const has = (name) => Object.prototype.hasOwnProperty.call(deps, name);
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
// Meta-frameworks are listed instead of the library they're built on
|
|
48
|
+
// (Nuxt, not Vue; SvelteKit, not Svelte).
|
|
49
|
+
const isRemix = has('@remix-run/react') || has('@remix-run/node');
|
|
50
|
+
const frameworks = [
|
|
51
|
+
has('next') && 'Next.js',
|
|
52
|
+
isRemix && 'Remix',
|
|
53
|
+
has('expo') && 'Expo',
|
|
54
|
+
has('react-native') && !has('expo') && 'React Native',
|
|
55
|
+
has('react') && !has('next') && !isRemix && !has('react-native') && 'React',
|
|
56
|
+
has('nuxt') && 'Nuxt',
|
|
57
|
+
has('vue') && !has('nuxt') && 'Vue',
|
|
58
|
+
has('@sveltejs/kit') && 'SvelteKit',
|
|
59
|
+
has('svelte') && !has('@sveltejs/kit') && 'Svelte',
|
|
60
|
+
has('astro') && 'Astro',
|
|
61
|
+
has('@angular/core') && 'Angular',
|
|
62
|
+
has('express') && 'Express',
|
|
63
|
+
has('fastify') && 'Fastify',
|
|
64
|
+
has('@nestjs/core') && 'NestJS',
|
|
65
|
+
has('hono') && 'Hono',
|
|
66
|
+
has('electron') && 'Electron',
|
|
67
|
+
has('vite') && 'Vite',
|
|
68
|
+
has('typescript') && 'TypeScript',
|
|
69
|
+
].filter(Boolean);
|
|
52
70
|
|
|
53
71
|
const testFramework = has('vitest') ? 'vitest'
|
|
54
72
|
: has('jest') ? 'jest'
|
|
55
73
|
: has('mocha') ? 'mocha'
|
|
56
74
|
: has('ava') ? 'ava'
|
|
57
75
|
: (pkg.scripts && pkg.scripts.test && pkg.scripts.test.includes('node --test')) ? 'node:test'
|
|
76
|
+
: has('@playwright/test') ? 'playwright'
|
|
58
77
|
: null;
|
|
59
78
|
|
|
60
79
|
const run = (script) => (pkg.scripts && pkg.scripts[script]) ? `${packageManager} run ${script}` : null;
|
|
@@ -78,22 +97,62 @@ function detectNode(root) {
|
|
|
78
97
|
};
|
|
79
98
|
}
|
|
80
99
|
|
|
100
|
+
function detectDeno(root) {
|
|
101
|
+
const file = ['deno.json', 'deno.jsonc'].find((f) => exists(root, f));
|
|
102
|
+
if (!file) return null;
|
|
103
|
+
const config = readJsonWithComments(root, file) || {};
|
|
104
|
+
const tasks = config.tasks || {};
|
|
105
|
+
const task = (name) => (tasks[name] ? `deno task ${name}` : null);
|
|
106
|
+
const text = readText(root, file) || '';
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
ecosystem: 'Deno',
|
|
110
|
+
name: config.name || null,
|
|
111
|
+
description: null,
|
|
112
|
+
packageManager: 'deno',
|
|
113
|
+
frameworks: [
|
|
114
|
+
/\$fresh\/|@fresh\/core/.test(text) && 'Fresh',
|
|
115
|
+
/\bhono\b/.test(text) && 'Hono',
|
|
116
|
+
].filter(Boolean),
|
|
117
|
+
testFramework: 'deno test',
|
|
118
|
+
commands: {
|
|
119
|
+
install: null,
|
|
120
|
+
build: task('build'),
|
|
121
|
+
test: task('test') || 'deno test',
|
|
122
|
+
lint: task('lint') || 'deno lint',
|
|
123
|
+
dev: task('dev') || task('start'),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
81
128
|
function detectPython(root) {
|
|
82
129
|
const hasPyproject = exists(root, 'pyproject.toml');
|
|
83
130
|
const hasRequirements = exists(root, 'requirements.txt');
|
|
84
131
|
const hasSetupPy = exists(root, 'setup.py');
|
|
85
|
-
|
|
132
|
+
const hasPipfile = exists(root, 'Pipfile');
|
|
133
|
+
if (!hasPyproject && !hasRequirements && !hasSetupPy && !hasPipfile) return null;
|
|
86
134
|
|
|
87
135
|
let packageManager = 'pip';
|
|
88
136
|
if (exists(root, 'poetry.lock')) packageManager = 'poetry';
|
|
89
137
|
else if (exists(root, 'uv.lock')) packageManager = 'uv';
|
|
90
|
-
else if (
|
|
138
|
+
else if (hasPipfile) packageManager = 'pipenv';
|
|
91
139
|
|
|
92
140
|
const pyproject = readText(root, 'pyproject.toml') || '';
|
|
93
141
|
const usesPytest = exists(root, 'pytest.ini') || exists(root, 'conftest.py') || /pytest/.test(pyproject);
|
|
94
142
|
const nameMatch = pyproject.match(/^name\s*=\s*"([^"]+)"/m);
|
|
95
143
|
const usesRuff = /\bruff\b/.test(pyproject) || exists(root, 'ruff.toml');
|
|
96
144
|
|
|
145
|
+
const depsText = [pyproject, readText(root, 'requirements.txt'), readText(root, 'Pipfile')]
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.join('\n');
|
|
148
|
+
const hasManagePy = exists(root, 'manage.py');
|
|
149
|
+
const usesDjango = hasManagePy || hasPythonDep(depsText, 'django');
|
|
150
|
+
const frameworks = [
|
|
151
|
+
usesDjango ? 'Django' : null,
|
|
152
|
+
hasPythonDep(depsText, 'fastapi') ? 'FastAPI' : null,
|
|
153
|
+
hasPythonDep(depsText, 'flask') ? 'Flask' : null,
|
|
154
|
+
].filter(Boolean);
|
|
155
|
+
|
|
97
156
|
const installCmd = {
|
|
98
157
|
poetry: 'poetry install',
|
|
99
158
|
uv: 'uv sync',
|
|
@@ -101,38 +160,54 @@ function detectPython(root) {
|
|
|
101
160
|
pip: hasPyproject ? 'pip install -e .' : 'pip install -r requirements.txt',
|
|
102
161
|
}[packageManager];
|
|
103
162
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
163
|
+
const run = { poetry: 'poetry run ', uv: 'uv run ', pipenv: 'pipenv run ' }[packageManager] || '';
|
|
164
|
+
|
|
165
|
+
let testCmd = null;
|
|
166
|
+
if (usesPytest) testCmd = `${run}pytest`;
|
|
167
|
+
else if (hasManagePy) testCmd = `${run}python manage.py test`;
|
|
107
168
|
|
|
108
169
|
return {
|
|
109
170
|
ecosystem: 'Python',
|
|
110
171
|
name: nameMatch ? nameMatch[1] : null,
|
|
111
172
|
description: null,
|
|
112
173
|
packageManager,
|
|
113
|
-
frameworks
|
|
114
|
-
testFramework: usesPytest ? 'pytest' : null,
|
|
174
|
+
frameworks,
|
|
175
|
+
testFramework: usesPytest ? 'pytest' : (hasManagePy ? 'Django test runner' : null),
|
|
115
176
|
commands: {
|
|
116
177
|
install: installCmd,
|
|
117
178
|
build: null,
|
|
118
179
|
test: testCmd,
|
|
119
180
|
lint: usesRuff ? 'ruff check .' : (exists(root, '.flake8') ? 'flake8' : null),
|
|
120
|
-
dev: null,
|
|
181
|
+
dev: hasManagePy ? `${run}python manage.py runserver` : null,
|
|
121
182
|
},
|
|
122
183
|
};
|
|
123
184
|
}
|
|
124
185
|
|
|
186
|
+
// Matches a PyPI requirement for exactly `name` in pyproject/requirements/
|
|
187
|
+
// Pipfile text: `django>=5`, `"Django[argon2]"`, `django = "*"` -- but not
|
|
188
|
+
// `django-environ` or `djangorestframework`.
|
|
189
|
+
function hasPythonDep(text, name) {
|
|
190
|
+
return new RegExp(`(^|["'\\s,\\[])${name}(\\s*[<>=~!\\[;"',]|\\s*$)`, 'im').test(text);
|
|
191
|
+
}
|
|
192
|
+
|
|
125
193
|
function detectRust(root) {
|
|
126
194
|
const cargoToml = readText(root, 'Cargo.toml');
|
|
127
195
|
if (!cargoToml) return null;
|
|
128
196
|
const nameMatch = cargoToml.match(/^name\s*=\s*"([^"]+)"/m);
|
|
129
197
|
const isWorkspace = /^\[workspace\]/m.test(cargoToml);
|
|
198
|
+
const hasCrate = (name) => new RegExp(`^\\s*${name}\\s*=`, 'm').test(cargoToml);
|
|
199
|
+
const rustFrameworks = [
|
|
200
|
+
['axum', 'Axum'],
|
|
201
|
+
['actix-web', 'Actix Web'],
|
|
202
|
+
['rocket', 'Rocket'],
|
|
203
|
+
['tauri', 'Tauri'],
|
|
204
|
+
].filter(([crate]) => hasCrate(crate)).map(([, label]) => label);
|
|
130
205
|
return {
|
|
131
206
|
ecosystem: 'Rust',
|
|
132
207
|
name: nameMatch ? nameMatch[1] : null,
|
|
133
208
|
description: null,
|
|
134
209
|
packageManager: 'cargo',
|
|
135
|
-
frameworks: isWorkspace ? ['Cargo workspace'] : [],
|
|
210
|
+
frameworks: [...(isWorkspace ? ['Cargo workspace'] : []), ...rustFrameworks],
|
|
136
211
|
testFramework: 'cargo test',
|
|
137
212
|
commands: {
|
|
138
213
|
install: 'cargo build',
|
|
@@ -148,12 +223,18 @@ function detectGo(root) {
|
|
|
148
223
|
const goMod = readText(root, 'go.mod');
|
|
149
224
|
if (!goMod) return null;
|
|
150
225
|
const moduleMatch = goMod.match(/^module\s+(\S+)/m);
|
|
226
|
+
const goFrameworks = [
|
|
227
|
+
['github.com/gin-gonic/gin', 'Gin'],
|
|
228
|
+
['github.com/labstack/echo', 'Echo'],
|
|
229
|
+
['github.com/gofiber/fiber', 'Fiber'],
|
|
230
|
+
['github.com/go-chi/chi', 'chi'],
|
|
231
|
+
];
|
|
151
232
|
return {
|
|
152
233
|
ecosystem: 'Go',
|
|
153
234
|
name: moduleMatch ? moduleMatch[1] : null,
|
|
154
235
|
description: null,
|
|
155
236
|
packageManager: 'go modules',
|
|
156
|
-
frameworks: [],
|
|
237
|
+
frameworks: goFrameworks.filter(([mod]) => goMod.includes(mod)).map(([, label]) => label),
|
|
157
238
|
testFramework: 'go test',
|
|
158
239
|
commands: {
|
|
159
240
|
install: 'go mod download',
|
|
@@ -210,21 +291,31 @@ function detectJava(root) {
|
|
|
210
291
|
}
|
|
211
292
|
|
|
212
293
|
function detectRuby(root) {
|
|
213
|
-
|
|
294
|
+
const gemfile = readText(root, 'Gemfile');
|
|
295
|
+
if (gemfile === null) return null;
|
|
214
296
|
const hasRspec = exists(root, '.rspec') || exists(root, 'spec');
|
|
297
|
+
const hasGem = (name) => new RegExp(`^\\s*gem\\s+["']${name}["']`, 'm').test(gemfile);
|
|
298
|
+
const isRails = hasGem('rails') || exists(root, 'bin', 'rails');
|
|
299
|
+
const rails = exists(root, 'bin', 'rails') ? 'bin/rails' : 'bundle exec rails';
|
|
300
|
+
|
|
301
|
+
let testCmd = null;
|
|
302
|
+
if (hasRspec) testCmd = 'bundle exec rspec';
|
|
303
|
+
else if (isRails) testCmd = `${rails} test`;
|
|
304
|
+
else if (exists(root, 'Rakefile')) testCmd = 'bundle exec rake test';
|
|
305
|
+
|
|
215
306
|
return {
|
|
216
307
|
ecosystem: 'Ruby',
|
|
217
308
|
name: null,
|
|
218
309
|
description: null,
|
|
219
310
|
packageManager: 'bundler',
|
|
220
|
-
frameworks: [],
|
|
221
|
-
testFramework: hasRspec ? 'rspec' : null,
|
|
311
|
+
frameworks: [isRails ? 'Rails' : null, hasGem('sinatra') ? 'Sinatra' : null].filter(Boolean),
|
|
312
|
+
testFramework: hasRspec ? 'rspec' : (isRails ? 'minitest' : null),
|
|
222
313
|
commands: {
|
|
223
314
|
install: 'bundle install',
|
|
224
315
|
build: null,
|
|
225
|
-
test:
|
|
316
|
+
test: testCmd,
|
|
226
317
|
lint: exists(root, '.rubocop.yml') ? 'bundle exec rubocop' : null,
|
|
227
|
-
dev: null,
|
|
318
|
+
dev: isRails ? `${rails} server` : null,
|
|
228
319
|
},
|
|
229
320
|
};
|
|
230
321
|
}
|
|
@@ -600,7 +691,7 @@ function detectMonorepo(root) {
|
|
|
600
691
|
}
|
|
601
692
|
|
|
602
693
|
function detectProject(root) {
|
|
603
|
-
const detectors = [detectNode, detectPython, detectRust, detectGo, detectRuby, detectJava, detectPhp, detectDotnet, detectSwift, detectDart, detectElixir, detectCpp, detectHaskell, detectScala, detectZig, detectJulia];
|
|
694
|
+
const detectors = [detectNode, detectDeno, detectPython, detectRust, detectGo, detectRuby, detectJava, detectPhp, detectDotnet, detectSwift, detectDart, detectElixir, detectCpp, detectHaskell, detectScala, detectZig, detectJulia];
|
|
604
695
|
const stacks = detectors.map((fn) => fn(root)).filter(Boolean);
|
|
605
696
|
|
|
606
697
|
return {
|