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/.agents/scripts/agents-bootstrap-github.js +40 -48
- package/.agents/scripts/bootstrap.js +74 -60
- package/.agents/scripts/lib/bootstrap/branch-protection.js +8 -8
- package/.agents/scripts/lib/bootstrap/gh-preflight.js +3 -3
- package/.agents/scripts/lib/bootstrap/hitl-confirm.js +2 -2
- package/.agents/scripts/lib/bootstrap/merge-methods.js +7 -7
- package/.agents/scripts/lib/bootstrap/preflight.js +18 -15
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +5 -5
- package/.agents/scripts/lib/bootstrap/prompt.js +5 -1
- package/.agents/scripts/lib/detect-package-manager.js +2 -2
- package/.agents/scripts/lib/onboard/init-tail.js +60 -69
- package/.agents/scripts/providers/github/tickets.js +1 -1
- package/.agents/workflows/helpers/deliver-stories.md +24 -2
- package/.agents/workflows/helpers/single-story-deliver.md +84 -1
- package/docs/CHANGELOG.md +15 -0
- package/lib/cli/init.js +66 -21
- package/lib/cli/sync.js +3 -3
- package/package.json +1 -1
- package/.agents/scripts/lib/onboard/detect-stack.js +0 -300
package/package.json
CHANGED
|
@@ -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
|
-
}
|