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 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 install https://raw.githubusercontent.com/Peshino2012/PD1/claude/busy-newton-5h97oh/scoop-bucket/agents-md-gen.json
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), framework
56
- (React, Next.js, Vue, Svelte, Express, Fastify, NestJS), test runner
57
- (Jest, Vitest, Mocha, Ava, node:test)
58
- - **Python** — package manager (pip/poetry/uv/pipenv), pytest, ruff
59
- - **Rust** — Cargo, workspaces, clippy
60
- - **Go** — go.mod, golangci-lint
61
- - **Ruby** — Bundler, RSpec
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 below.
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.9.1",
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 (exists(root, 'pnpm-lock.yaml')) packageManager = 'pnpm';
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
- const frameworks = [];
43
- if (has('next')) frameworks.push('Next.js');
44
- if (has('react') && !has('next')) frameworks.push('React');
45
- if (has('vue')) frameworks.push('Vue');
46
- if (has('svelte')) frameworks.push('Svelte');
47
- if (has('express')) frameworks.push('Express');
48
- if (has('fastify')) frameworks.push('Fastify');
49
- if (has('@nestjs/core')) frameworks.push('NestJS');
50
- if (has('vite')) frameworks.push('Vite');
51
- if (has('typescript')) frameworks.push('TypeScript');
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
- if (!hasPyproject && !hasRequirements && !hasSetupPy) return null;
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 (exists(root, 'Pipfile.lock')) packageManager = 'pipenv';
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 testCmd = usesPytest
105
- ? (packageManager === 'poetry' ? 'poetry run pytest' : packageManager === 'uv' ? 'uv run pytest' : 'pytest')
106
- : null;
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
- if (!exists(root, 'Gemfile')) return null;
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: hasRspec ? 'bundle exec rspec' : (exists(root, 'Rakefile') ? 'bundle exec rake test' : null),
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 {