mandrel 1.63.0 → 1.64.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "1.63.0",
3
+ "version": "1.64.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, personas, skills, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",
@@ -1,300 +0,0 @@
1
- /**
2
- * detect-stack.js — Consumer stack detection for `mandrel init`
3
- *
4
- * Inspects a consumer repository root and reports the package manager,
5
- * test runner, and primary language it can infer from on-disk signals
6
- * (lockfiles, `package.json` contents, and source-file extensions). The
7
- * `mandrel init` configure-path tail (Feature #3514, Story #4045) uses
8
- * this to tell the operator what it found before scaffolding missing
9
- * `docsContextFiles`.
10
- *
11
- * The detection functions are seam-injectable: each takes an injected
12
- * filesystem facade (`exists` / `readFile` / `listExtensions`) so they
13
- * are unit-testable in isolation against an in-memory fixture, mirroring
14
- * the style of `lib/runtime-deps/preflight.js#detectPackageManager`. The
15
- * default facade reads the real filesystem so callers can point it at a
16
- * sample-repo fixture directory.
17
- *
18
- * Story #3520 (refs #3520).
19
- */
20
-
21
- import fs from 'node:fs';
22
- import path from 'node:path';
23
- import { detectPackageManager as detectPm } from '../detect-package-manager.js';
24
-
25
- /**
26
- * Filesystem facade. Pure detection logic talks to disk only through
27
- * this seam so tests can drive it with an in-memory fixture.
28
- *
29
- * @typedef {object} FsFacade
30
- * @property {(p: string) => boolean} exists - Path existence probe.
31
- * @property {(p: string) => string|null} readFile - UTF-8 read; null when absent/unreadable.
32
- * @property {(root: string) => string[]} listExtensions - Lowercased source-file extensions (with leading dot) found under root.
33
- */
34
-
35
- const SOURCE_EXTENSIONS = new Set([
36
- '.ts',
37
- '.tsx',
38
- '.js',
39
- '.jsx',
40
- '.mjs',
41
- '.cjs',
42
- '.py',
43
- '.go',
44
- '.rs',
45
- '.rb',
46
- '.java',
47
- '.kt',
48
- '.php',
49
- '.cs',
50
- '.swift',
51
- ]);
52
-
53
- const IGNORED_DIRS = new Set([
54
- 'node_modules',
55
- '.git',
56
- 'dist',
57
- 'build',
58
- 'coverage',
59
- '.next',
60
- '.nuxt',
61
- 'vendor',
62
- 'target',
63
- '__pycache__',
64
- '.venv',
65
- 'venv',
66
- ]);
67
-
68
- /**
69
- * Map a source-file extension to a primary-language label.
70
- *
71
- * @param {string} ext - Lowercased extension including the leading dot.
72
- * @returns {string|null} Language label, or null when the extension is not a recognized source type.
73
- */
74
- function extensionToLanguage(ext) {
75
- switch (ext) {
76
- case '.ts':
77
- case '.tsx':
78
- return 'typescript';
79
- case '.js':
80
- case '.jsx':
81
- case '.mjs':
82
- case '.cjs':
83
- return 'javascript';
84
- case '.py':
85
- return 'python';
86
- case '.go':
87
- return 'go';
88
- case '.rs':
89
- return 'rust';
90
- case '.rb':
91
- return 'ruby';
92
- case '.java':
93
- return 'java';
94
- case '.kt':
95
- return 'kotlin';
96
- case '.php':
97
- return 'php';
98
- case '.cs':
99
- return 'csharp';
100
- case '.swift':
101
- return 'swift';
102
- default:
103
- return null;
104
- }
105
- }
106
-
107
- /**
108
- * Recursively collect lowercased source-file extensions under `root`,
109
- * skipping vendored / build / VCS directories. Used by the default
110
- * filesystem facade; tests inject their own `listExtensions`.
111
- *
112
- * @param {string} root - Absolute repository root.
113
- * @returns {string[]} Extensions (with leading dot, possibly repeated) in traversal order.
114
- */
115
- function listExtensionsOnDisk(root) {
116
- /** @type {string[]} */
117
- const extensions = [];
118
- /** @type {string[]} */
119
- const stack = [root];
120
-
121
- while (stack.length > 0) {
122
- const dir = stack.pop();
123
- let entries;
124
- try {
125
- entries = fs.readdirSync(dir, { withFileTypes: true });
126
- } catch {
127
- continue;
128
- }
129
- for (const entry of entries) {
130
- if (entry.isDirectory()) {
131
- if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith('.')) {
132
- continue;
133
- }
134
- stack.push(path.join(dir, entry.name));
135
- } else if (entry.isFile()) {
136
- const ext = path.extname(entry.name).toLowerCase();
137
- if (ext) extensions.push(ext);
138
- }
139
- }
140
- }
141
-
142
- return extensions;
143
- }
144
-
145
- /**
146
- * Default filesystem facade backed by `node:fs`. Reads the real disk so
147
- * callers can point detection at a sample-repo fixture directory.
148
- *
149
- * @type {FsFacade}
150
- */
151
- export const defaultFsFacade = {
152
- exists: (p) => fs.existsSync(p),
153
- readFile: (p) => {
154
- try {
155
- return fs.readFileSync(p, 'utf8');
156
- } catch {
157
- return null;
158
- }
159
- },
160
- listExtensions: (root) => listExtensionsOnDisk(root),
161
- };
162
-
163
- /**
164
- * Detect the package manager from lockfile presence. Defaults to `npm`
165
- * when no lockfile is found but a `package.json` exists, and `null` when
166
- * the repo has no Node manifest at all.
167
- *
168
- * Delegates to the shared `detectPackageManager` helper
169
- * (Story #4048 B3 — one implementation per concept). The `fsFacade.exists`
170
- * seam is forwarded directly.
171
- *
172
- * @param {string} root - Repository root.
173
- * @param {FsFacade} [fsFacade=defaultFsFacade]
174
- * @returns {'pnpm'|'yarn'|'bun'|'npm'|null}
175
- */
176
- export function detectPackageManager(root, fsFacade = defaultFsFacade) {
177
- return detectPm(root, fsFacade.exists);
178
- }
179
-
180
- /**
181
- * Parse `package.json` into an object, returning `null` when it is
182
- * absent or unparseable.
183
- *
184
- * @param {string} root - Repository root.
185
- * @param {FsFacade} fsFacade
186
- * @returns {Record<string, unknown>|null}
187
- */
188
- function readPackageJson(root, fsFacade) {
189
- const raw = fsFacade.readFile(path.join(root, 'package.json'));
190
- if (!raw) return null;
191
- try {
192
- return JSON.parse(raw);
193
- } catch {
194
- return null;
195
- }
196
- }
197
-
198
- /**
199
- * Detect the test runner from `package.json` dependency declarations and
200
- * the `test` script. Recognizes vitest, jest, mocha, ava, and the
201
- * Node.js built-in test runner (`node --test`). Returns `null` when no
202
- * runner can be inferred.
203
- *
204
- * @param {string} root - Repository root.
205
- * @param {FsFacade} [fsFacade=defaultFsFacade]
206
- * @returns {'vitest'|'jest'|'mocha'|'ava'|'node-test'|null}
207
- */
208
- export function detectTestRunner(root, fsFacade = defaultFsFacade) {
209
- const pkg = readPackageJson(root, fsFacade);
210
- if (!pkg) return null;
211
-
212
- const deps = {
213
- .../** @type {Record<string, unknown>} */ (pkg.dependencies ?? {}),
214
- .../** @type {Record<string, unknown>} */ (pkg.devDependencies ?? {}),
215
- };
216
-
217
- if (deps.vitest) return 'vitest';
218
- if (deps.jest) return 'jest';
219
- if (deps.mocha) return 'mocha';
220
- if (deps.ava) return 'ava';
221
-
222
- const scripts = /** @type {Record<string, unknown>} */ (pkg.scripts ?? {});
223
- const testScript =
224
- typeof scripts.test === 'string' ? scripts.test.toLowerCase() : '';
225
- if (testScript) {
226
- if (testScript.includes('vitest')) return 'vitest';
227
- if (testScript.includes('jest')) return 'jest';
228
- if (testScript.includes('mocha')) return 'mocha';
229
- if (testScript.includes('ava')) return 'ava';
230
- if (
231
- testScript.includes('node --test') ||
232
- testScript.includes('node:test')
233
- ) {
234
- return 'node-test';
235
- }
236
- }
237
-
238
- return null;
239
- }
240
-
241
- /**
242
- * Detect the primary language by tallying source-file extensions and
243
- * picking the most frequent recognized language. A `tsconfig.json`
244
- * breaks ties toward TypeScript. Returns `null` when no recognized
245
- * source files are found.
246
- *
247
- * @param {string} root - Repository root.
248
- * @param {FsFacade} [fsFacade=defaultFsFacade]
249
- * @returns {string|null}
250
- */
251
- export function detectPrimaryLanguage(root, fsFacade = defaultFsFacade) {
252
- const extensions = fsFacade.listExtensions(root) ?? [];
253
- /** @type {Map<string, number>} */
254
- const tally = new Map();
255
-
256
- for (const ext of extensions) {
257
- if (!SOURCE_EXTENSIONS.has(ext)) continue;
258
- const language = extensionToLanguage(ext);
259
- if (!language) continue;
260
- tally.set(language, (tally.get(language) ?? 0) + 1);
261
- }
262
-
263
- if (tally.size === 0) return null;
264
-
265
- // tsconfig.json is a strong TypeScript signal: nudge the tally so a
266
- // mixed JS/TS repo resolves to typescript when the config is present.
267
- if (fsFacade.exists(path.join(root, 'tsconfig.json'))) {
268
- tally.set('typescript', (tally.get('typescript') ?? 0) + 1);
269
- }
270
-
271
- let best = null;
272
- let bestCount = -1;
273
- for (const [language, count] of tally) {
274
- if (count > bestCount) {
275
- best = language;
276
- bestCount = count;
277
- }
278
- }
279
-
280
- return best;
281
- }
282
-
283
- /**
284
- * Inspect a consumer repository and report the inferred stack.
285
- *
286
- * @param {string} root - Absolute repository root to inspect.
287
- * @param {FsFacade} [fsFacade=defaultFsFacade] - Filesystem seam (defaults to real disk).
288
- * @returns {{ packageManager: 'pnpm'|'yarn'|'bun'|'npm'|null, testRunner: 'vitest'|'jest'|'mocha'|'ava'|'node-test'|null, primaryLanguage: string|null }}
289
- */
290
- export function detectStack(root, fsFacade = defaultFsFacade) {
291
- if (!root || typeof root !== 'string') {
292
- throw new Error('detectStack: root must be a non-empty string path');
293
- }
294
-
295
- return {
296
- packageManager: detectPackageManager(root, fsFacade),
297
- testRunner: detectTestRunner(root, fsFacade),
298
- primaryLanguage: detectPrimaryLanguage(root, fsFacade),
299
- };
300
- }