sparda-mcp 0.5.2 → 0.5.4

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.
@@ -0,0 +1,372 @@
1
+ // commands/report.js — the readable black box.
2
+ // Renders everything the organism already remembers (sparda.json) plus, when
3
+ // the host is up, the live gauges (/mcp/stats) — as a terminal report, a
4
+ // self-contained HTML file, or raw JSON. Deterministic, zero new deps, and
5
+ // read-only: it never mutates the manifest and never touches the host routes.
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { c, gradient } from '../ui/style.js';
9
+
10
+ // ── pure: manifest (+ optional live stats) → report data ──────────────────
11
+
12
+ export function buildReport(manifest, live = null) {
13
+ const tools = Object.entries(manifest.tools ?? {}).map(([name, t]) => ({
14
+ name,
15
+ method: (t.method ?? 'GET').toUpperCase(),
16
+ path: t.path ?? '',
17
+ enabled: Boolean(t.enabled),
18
+ }));
19
+ const writes = tools.filter((t) => t.method !== 'GET');
20
+
21
+ const events = manifest.sparding?.events ?? [];
22
+ const byDecision = {};
23
+ for (const e of events) byDecision[e.decision] = (byDecision[e.decision] ?? 0) + 1;
24
+
25
+ const failures = Object.entries(manifest.sparding?.failures ?? {})
26
+ .map(([sig, f]) => ({ sig, count: f.count ?? 0, lesson: f.lesson ?? '' }))
27
+ .sort((a, b) => b.count - a.count);
28
+
29
+ const antibodies = Object.entries(manifest.immune?.antibodies ?? {})
30
+ .map(([sig, a]) => ({
31
+ sig,
32
+ diagnosis: a.diagnosis ?? '',
33
+ hits: a.hits ?? 0,
34
+ lastSeen: a.lastSeen ?? null,
35
+ }))
36
+ .sort((a, b) => b.hits - a.hits);
37
+ const antibodyHits = antibodies.reduce((n, a) => n + a.hits, 0);
38
+
39
+ const circuits = Object.entries(manifest.labs?.circuits ?? {}).map(([key, cir]) => ({
40
+ key,
41
+ seen: cir.seen ?? 0,
42
+ composite: cir.composite
43
+ ? { name: cir.composite.name, description: cir.composite.description ?? '' }
44
+ : null,
45
+ }));
46
+ const composites = circuits.filter((x) => x.composite);
47
+
48
+ let liveSection = null;
49
+ if (live) {
50
+ const perTool = Object.entries(live.stats ?? {}).map(([name, s]) => ({
51
+ name,
52
+ calls: s.calls ?? 0,
53
+ errors: s.errors ?? 0,
54
+ avgMs: s.calls ? Math.round((s.totalMs ?? 0) / s.calls) : 0,
55
+ }));
56
+ const totals = perTool.reduce(
57
+ (a, t) => ({ calls: a.calls + t.calls, errors: a.errors + t.errors }),
58
+ { calls: 0, errors: 0 },
59
+ );
60
+ const purityCount = {};
61
+ for (const p of Object.values(live.purity ?? {}))
62
+ purityCount[p.class ?? 'unknown'] = (purityCount[p.class ?? 'unknown'] ?? 0) + 1;
63
+ liveSection = {
64
+ uptimeSec: live.uptimeSec ?? 0,
65
+ totals,
66
+ topTools: perTool.sort((a, b) => b.calls - a.calls).slice(0, 5),
67
+ quarantine: Object.entries(live.quarantine ?? {}).map(([tool, q]) => ({
68
+ tool,
69
+ reason: q.reason ?? '',
70
+ })),
71
+ recycle: live.recycle ?? null,
72
+ purityCount,
73
+ };
74
+ }
75
+
76
+ return {
77
+ generatedAt: new Date().toISOString(),
78
+ meta: {
79
+ framework: manifest.framework ?? 'unknown',
80
+ entryFile: manifest.entryFile ?? '',
81
+ port: manifest.port ?? null,
82
+ createdAt: manifest.createdAt ?? null,
83
+ },
84
+ tools: {
85
+ total: tools.length,
86
+ enabled: tools.filter((t) => t.enabled).length,
87
+ writes: writes.length,
88
+ writesEnabled: writes.filter((t) => t.enabled).length,
89
+ },
90
+ semantic: {
91
+ descriptions: Object.keys(manifest.semantic?.descriptions ?? {}).length,
92
+ workflows: (manifest.semantic?.workflows ?? []).length,
93
+ },
94
+ proof: {
95
+ events: events.length,
96
+ byDecision,
97
+ lastEventAt: events.length ? events[events.length - 1].ts : null,
98
+ failures: failures.slice(0, 3),
99
+ failureCount: failures.length,
100
+ },
101
+ immunity: {
102
+ antibodies: antibodies.length,
103
+ hits: antibodyHits,
104
+ top: antibodies.slice(0, 3),
105
+ },
106
+ organs: {
107
+ circuits: circuits.length,
108
+ composites: composites.map((x) => x.composite),
109
+ },
110
+ live: liveSection,
111
+ };
112
+ }
113
+
114
+ // ── pure: report → terminal string (colors auto-disable off-TTY) ──────────
115
+
116
+ export function renderTerminal(r) {
117
+ const L = [];
118
+ const empty = (msg) => c.dim(` · ${msg}`);
119
+ L.push('');
120
+ L.push(` ${gradient('SPARDA')} ${c.dim('— black box report')}`);
121
+ L.push(
122
+ c.dim(
123
+ ` ${r.meta.framework} · ${r.meta.entryFile}${r.meta.port ? ` · :${r.meta.port}` : ''} · ${r.generatedAt}`,
124
+ ),
125
+ );
126
+ L.push('');
127
+
128
+ L.push(c.bold(' Tools'));
129
+ L.push(
130
+ ` ✓ ${r.tools.enabled}/${r.tools.total} exposed to AI — ${r.tools.writesEnabled}/${r.tools.writes} write tools opted in (writes are off by default)`,
131
+ );
132
+ if (r.semantic.descriptions || r.semantic.workflows)
133
+ L.push(
134
+ ` ✓ semantic memory: ${r.semantic.descriptions} enriched descriptions, ${r.semantic.workflows} workflows`,
135
+ );
136
+ L.push('');
137
+
138
+ L.push(c.bold(' Proof journal') + c.dim(' (last 100 agent actions)'));
139
+ if (r.proof.events) {
140
+ const parts = Object.entries(r.proof.byDecision)
141
+ .map(([d, n]) => `${n} ${d}`)
142
+ .join(', ');
143
+ L.push(` ✓ ${r.proof.events} proofed invocations — ${parts}`);
144
+ if (r.proof.failureCount) {
145
+ L.push(
146
+ ` ⚠ ${r.proof.failureCount} failure signature${r.proof.failureCount > 1 ? 's' : ''} with lessons:`,
147
+ );
148
+ for (const f of r.proof.failures)
149
+ L.push(c.dim(` ${f.sig} ×${f.count}${f.lesson ? ` — ${f.lesson}` : ''}`));
150
+ }
151
+ } else
152
+ L.push(
153
+ empty('no agent activity recorded yet — the journal fills as AI drives the app'),
154
+ );
155
+ L.push('');
156
+
157
+ L.push(c.bold(' Immune memory'));
158
+ if (r.immunity.antibodies) {
159
+ L.push(
160
+ ` ✓ ${r.immunity.antibodies} antibod${r.immunity.antibodies > 1 ? 'ies' : 'y'} — ${r.immunity.hits} diagnoses served from memory (zero tokens)`,
161
+ );
162
+ for (const a of r.immunity.top)
163
+ L.push(
164
+ c.dim(
165
+ ` ${a.sig} ×${a.hits}${a.diagnosis ? ` — ${a.diagnosis.slice(0, 80)}` : ''}`,
166
+ ),
167
+ );
168
+ } else L.push(empty('no antibodies yet — they grow when failures get diagnosed'));
169
+ L.push('');
170
+
171
+ L.push(c.bold(' Emergent organs') + c.dim(' (Labs)'));
172
+ if (r.organs.circuits) {
173
+ L.push(
174
+ ` ✓ ${r.organs.circuits} circuit${r.organs.circuits > 1 ? 's' : ''} observed`,
175
+ );
176
+ for (const comp of r.organs.composites)
177
+ L.push(
178
+ ` ✓ composite tool born: ${c.cyan(comp.name)}${comp.description ? c.dim(` — ${comp.description.slice(0, 70)}`) : ''}`,
179
+ );
180
+ } else
181
+ L.push(
182
+ empty(
183
+ 'no circuits observed — enable labs.recordSequences to condense usage into tools',
184
+ ),
185
+ );
186
+ L.push('');
187
+
188
+ L.push(c.bold(' Live gauges'));
189
+ if (r.live) {
190
+ L.push(
191
+ ` ✓ host up ${r.live.uptimeSec}s — ${r.live.totals.calls} calls, ${r.live.totals.errors} errors`,
192
+ );
193
+ for (const t of r.live.topTools)
194
+ L.push(
195
+ c.dim(` ${t.name}: ${t.calls} calls · ${t.errors} errors · ~${t.avgMs}ms`),
196
+ );
197
+ if (r.live.recycle)
198
+ L.push(
199
+ ` ✓ recycling: ${r.live.recycle.ratePct ?? 0}% served by the circle (${r.live.recycle.servedByCircle ?? 0} recycled vs ${r.live.recycle.paidFull ?? 0} full-price)`,
200
+ );
201
+ const pc = Object.entries(r.live.purityCount ?? {});
202
+ if (pc.length)
203
+ L.push(c.dim(` route purity: ${pc.map(([k, n]) => `${n} ${k}`).join(', ')}`));
204
+ if (r.live.quarantine.length)
205
+ for (const q of r.live.quarantine)
206
+ L.push(` ⚠ quarantined: ${q.tool} (${q.reason})`);
207
+ else L.push(' ✓ quarantine empty — no route is currently sick');
208
+ } else
209
+ L.push(
210
+ empty(
211
+ 'host not running — showing persisted memory only (start the app for live gauges)',
212
+ ),
213
+ );
214
+ L.push('');
215
+ return L.join('\n');
216
+ }
217
+
218
+ // ── pure: report → self-contained HTML (no external assets, no scripts) ───
219
+
220
+ function esc(s) {
221
+ return String(s)
222
+ .replaceAll('&', '&')
223
+ .replaceAll('<', '&lt;')
224
+ .replaceAll('>', '&gt;')
225
+ .replaceAll('"', '&quot;');
226
+ }
227
+
228
+ export function renderHtml(r) {
229
+ const stat = (n, label) =>
230
+ `<div class="stat"><div class="n">${esc(n)}</div><div class="l">${esc(label)}</div></div>`;
231
+ const rows = (items, cols) =>
232
+ items
233
+ .map((it) => `<tr>${cols.map((k) => `<td>${esc(it[k] ?? '')}</td>`).join('')}</tr>`)
234
+ .join('');
235
+
236
+ const liveBlock = r.live
237
+ ? `<h2>Live gauges</h2>
238
+ <div class="grid">
239
+ ${stat(r.live.totals.calls, 'calls')}${stat(r.live.totals.errors, 'errors')}${stat(
240
+ r.live.recycle ? `${r.live.recycle.ratePct ?? 0}%` : '—',
241
+ 'compute recycled',
242
+ )}${stat(r.live.quarantine.length, 'quarantined')}
243
+ </div>
244
+ <table><thead><tr><th>tool</th><th>calls</th><th>errors</th><th>avg ms</th></tr></thead>
245
+ <tbody>${rows(r.live.topTools, ['name', 'calls', 'errors', 'avgMs'])}</tbody></table>`
246
+ : `<h2>Live gauges</h2><p class="dim">Host not running — persisted memory only.</p>`;
247
+
248
+ return `<!doctype html>
249
+ <html lang="en"><head><meta charset="utf-8">
250
+ <meta name="viewport" content="width=device-width, initial-scale=1">
251
+ <title>SPARDA report — ${esc(r.meta.entryFile)}</title>
252
+ <style>
253
+ :root{color-scheme:dark}
254
+ body{margin:0;padding:48px 24px;background:#0b0b10;color:#e8e8f0;
255
+ font:15px/1.7 ui-monospace,SFMono-Regular,Consolas,monospace}
256
+ main{max-width:760px;margin:0 auto}
257
+ h1{font-size:28px;margin:0 0 4px;
258
+ background:linear-gradient(90deg,#c084fc,#67e8f9);
259
+ -webkit-background-clip:text;background-clip:text;color:transparent}
260
+ h2{font-size:15px;margin:36px 0 12px;color:#c084fc;text-transform:uppercase;letter-spacing:.08em}
261
+ .dim{color:#8a8aa0}
262
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px}
263
+ .stat{border:1px solid #26263a;border-radius:10px;padding:14px 16px;background:#12121c}
264
+ .stat .n{font-size:26px;font-weight:700;color:#67e8f9}
265
+ .stat .l{font-size:12px;color:#8a8aa0}
266
+ table{width:100%;border-collapse:collapse;margin-top:8px;font-size:13px}
267
+ th,td{text-align:left;padding:7px 10px;border-bottom:1px solid #1d1d2e}
268
+ th{color:#8a8aa0;font-weight:400}
269
+ footer{margin-top:48px;font-size:12px;color:#8a8aa0}
270
+ </style></head><body><main>
271
+ <h1>SPARDA</h1>
272
+ <p class="dim">black box report · ${esc(r.meta.framework)} · ${esc(r.meta.entryFile)} · ${esc(r.generatedAt)}</p>
273
+
274
+ <h2>Exposure</h2>
275
+ <div class="grid">
276
+ ${stat(`${r.tools.enabled}/${r.tools.total}`, 'tools exposed to AI')}${stat(
277
+ `${r.tools.writesEnabled}/${r.tools.writes}`,
278
+ 'write tools opted in',
279
+ )}${stat(r.semantic.workflows, 'workflows learned')}
280
+ </div>
281
+
282
+ <h2>Proof journal (last 100 actions)</h2>
283
+ <div class="grid">
284
+ ${stat(r.proof.events, 'proofed invocations')}${Object.entries(r.proof.byDecision)
285
+ .map(([d, n]) => stat(n, d))
286
+ .join('')}${stat(r.proof.failureCount, 'failure lessons')}
287
+ </div>
288
+ ${
289
+ r.proof.failures.length
290
+ ? `<table><thead><tr><th>signature</th><th>count</th><th>lesson</th></tr></thead>
291
+ <tbody>${rows(r.proof.failures, ['sig', 'count', 'lesson'])}</tbody></table>`
292
+ : ''
293
+ }
294
+
295
+ <h2>Immune memory</h2>
296
+ <div class="grid">
297
+ ${stat(r.immunity.antibodies, 'antibodies')}${stat(r.immunity.hits, 'zero-token diagnoses')}
298
+ </div>
299
+ ${
300
+ r.immunity.top.length
301
+ ? `<table><thead><tr><th>signature</th><th>hits</th><th>diagnosis</th></tr></thead>
302
+ <tbody>${rows(r.immunity.top, ['sig', 'hits', 'diagnosis'])}</tbody></table>`
303
+ : ''
304
+ }
305
+
306
+ <h2>Emergent organs</h2>
307
+ <div class="grid">
308
+ ${stat(r.organs.circuits, 'circuits observed')}${stat(r.organs.composites.length, 'composite tools born')}
309
+ </div>
310
+ ${
311
+ r.organs.composites.length
312
+ ? `<table><thead><tr><th>composite</th><th>description</th></tr></thead>
313
+ <tbody>${rows(r.organs.composites, ['name', 'description'])}</tbody></table>`
314
+ : ''
315
+ }
316
+
317
+ ${liveBlock}
318
+
319
+ <footer>Generated locally by <b>sparda-mcp report</b> — no data left this machine.
320
+ SPARDA by Residual Labs · residual-labs.fr</footer>
321
+ </main></body></html>`;
322
+ }
323
+
324
+ // ── the command ────────────────────────────────────────────────────────────
325
+
326
+ export async function runReport(opts) {
327
+ const manifestPath = path.join(opts.cwd, 'sparda.json');
328
+ if (!fs.existsSync(manifestPath)) {
329
+ throw Object.assign(new Error('sparda.json not found — nothing to report on.'), {
330
+ code: 'USER',
331
+ hint: 'run `npx sparda-mcp init` first',
332
+ });
333
+ }
334
+ let manifest;
335
+ try {
336
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
337
+ } catch {
338
+ throw Object.assign(new Error('sparda.json is not valid JSON.'), {
339
+ code: 'USER',
340
+ hint: 'restore it from git or re-run `npx sparda-mcp init`',
341
+ });
342
+ }
343
+
344
+ let live = null;
345
+ if (manifest.port && manifest.localKey) {
346
+ live = await fetch(`http://127.0.0.1:${manifest.port}/mcp/stats`, {
347
+ headers: { 'x-sparda-key': manifest.localKey },
348
+ signal: AbortSignal.timeout(1500),
349
+ })
350
+ .then((x) => (x.ok ? x.json() : null))
351
+ .catch(() => null);
352
+ }
353
+
354
+ const report = buildReport(manifest, live);
355
+
356
+ if (opts.json) {
357
+ console.log(JSON.stringify(report, null, 2));
358
+ return { report };
359
+ }
360
+
361
+ console.log(renderTerminal(report));
362
+
363
+ if (opts.html) {
364
+ const outDir = path.join(opts.cwd, '.sparda');
365
+ fs.mkdirSync(outDir, { recursive: true });
366
+ const outFile = path.join(outDir, 'report.html');
367
+ fs.writeFileSync(outFile, renderHtml(report), 'utf8');
368
+ console.log(` ✓ HTML report written: ${path.relative(opts.cwd, outFile)}`);
369
+ console.log('');
370
+ }
371
+ return { report };
372
+ }
@@ -9,10 +9,15 @@ import { generateExpress } from '../generator/express.js';
9
9
  import { generateFastAPI } from '../generator/fastapi.js';
10
10
 
11
11
  export async function runSync(opts) {
12
- const log = (m) => { if (!opts.quiet) console.error(m); };
12
+ const log = (m) => {
13
+ if (!opts.quiet) console.error(m);
14
+ };
13
15
  const manifestPath = path.join(opts.cwd, 'sparda.json');
14
16
  if (!fs.existsSync(manifestPath)) {
15
- throw Object.assign(new Error('sparda.json not found.'), { code: 'USER', hint: 'Run `npx sparda-mcp init` first.' });
17
+ throw Object.assign(new Error('sparda.json not found.'), {
18
+ code: 'USER',
19
+ hint: 'Run `npx sparda-mcp init` first.',
20
+ });
16
21
  }
17
22
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
18
23
  const stack = detectStack(opts.cwd);
@@ -21,13 +26,22 @@ export async function runSync(opts) {
21
26
  if (stack.framework === 'express') {
22
27
  ({ routes } = parseExpressProject(opts.cwd, stack.entryFile));
23
28
  } else {
24
- ({ routes, entryAppVars } = parseFastAPIProject(opts.cwd, stack.entryFile, stack.pythonCmd));
29
+ ({ routes, entryAppVars } = parseFastAPIProject(
30
+ opts.cwd,
31
+ stack.entryFile,
32
+ stack.pythonCmd,
33
+ ));
25
34
  }
26
35
  for (const r of routes) {
27
- r.description = sanitizeDescription(r.description, `${r.method.toUpperCase()} ${r.path}`).text;
36
+ r.description = sanitizeDescription(
37
+ r.description,
38
+ `${r.method.toUpperCase()} ${r.path}`,
39
+ ).text;
28
40
  }
29
41
 
30
- const current = new Set(Object.values(manifest.tools ?? {}).map((t) => `${t.method} ${t.path}`));
42
+ const current = new Set(
43
+ Object.values(manifest.tools ?? {}).map((t) => `${t.method} ${t.path}`),
44
+ );
31
45
  const next = new Set(routes.map((r) => `${r.method.toUpperCase()} ${r.path}`));
32
46
  const added = [...next].filter((x) => !current.has(x));
33
47
  const removed = [...current].filter((x) => !next.has(x));
@@ -39,11 +53,26 @@ export async function runSync(opts) {
39
53
 
40
54
  // regenerate; carry-over keeps enabled overrides, semantic cache and localKey
41
55
  stack.framework === 'express'
42
- ? generateExpress({ cwd: opts.cwd, entryFile: stack.entryFile, moduleType: stack.moduleType, port: manifest.port ?? stack.port, routes })
43
- : generateFastAPI({ cwd: opts.cwd, entryFile: stack.entryFile, port: manifest.port ?? stack.port, routes, entryAppVars, pythonCmd: stack.pythonCmd });
56
+ ? generateExpress({
57
+ cwd: opts.cwd,
58
+ entryFile: stack.entryFile,
59
+ moduleType: stack.moduleType,
60
+ port: manifest.port ?? stack.port,
61
+ routes,
62
+ })
63
+ : generateFastAPI({
64
+ cwd: opts.cwd,
65
+ entryFile: stack.entryFile,
66
+ port: manifest.port ?? stack.port,
67
+ routes,
68
+ entryAppVars,
69
+ pythonCmd: stack.pythonCmd,
70
+ });
44
71
 
45
72
  for (const a of added) log(`[sparda] + ${a}`);
46
73
  for (const r of removed) log(`[sparda] - ${r}`);
47
- log(`[sparda] router regenerated (${added.length} added, ${removed.length} removed). Restart your app or let hot-reload pick it up.`);
74
+ log(
75
+ `[sparda] router regenerated (${added.length} added, ${removed.length} removed). Restart your app or let hot-reload pick it up.`,
76
+ );
48
77
  return { changed: true, added, removed };
49
78
  }
package/src/detect.js CHANGED
@@ -14,21 +14,37 @@ export function detectStack(cwd) {
14
14
  const entryFile = findExpressEntry(cwd, pkg);
15
15
  const moduleType = detectModuleType(cwd, pkg, entryFile);
16
16
  const port = detectExpressPort(cwd, entryFile);
17
- return { framework: 'express', entryFile, moduleType, port, expressVersion: deps.express };
17
+ return {
18
+ framework: 'express',
19
+ entryFile,
20
+ moduleType,
21
+ port,
22
+ expressVersion: deps.express,
23
+ };
18
24
  }
19
25
  const known = ['@nestjs/core', 'next', 'fastify', 'koa'].find((d) => deps[d]);
20
- if (known) throw err(`${known} detected — not supported yet. Express & FastAPI only in v0.`, '+1 the framework vote: github.com/zyx77550/sparda/issues/1');
26
+ if (known)
27
+ throw err(
28
+ `${known} detected — not supported yet. Express & FastAPI only in v0.`,
29
+ '+1 the framework vote: github.com/zyx77550/sparda/issues/1',
30
+ );
21
31
  }
22
32
  for (const f of ['requirements.txt', 'pyproject.toml']) {
23
33
  const p = path.join(cwd, f);
24
- if (fs.existsSync(p) && fs.readFileSync(p, 'utf8').toLowerCase().includes('fastapi')) {
34
+ if (
35
+ fs.existsSync(p) &&
36
+ fs.readFileSync(p, 'utf8').toLowerCase().includes('fastapi')
37
+ ) {
25
38
  const entryFile = findFastAPIEntry(cwd);
26
39
  const port = detectFastAPIPort(cwd, entryFile);
27
40
  const pythonCmd = detectPython();
28
41
  return { framework: 'fastapi', entryFile, port, pythonCmd };
29
42
  }
30
43
  }
31
- throw err('No supported framework found (Express, FastAPI).', 'Run sparda-mcp inside your project root, next to package.json.');
44
+ throw err(
45
+ 'No supported framework found (Express, FastAPI).',
46
+ 'Run sparda-mcp inside your project root, next to package.json.',
47
+ );
32
48
  }
33
49
 
34
50
  function detectPython() {
@@ -52,13 +68,14 @@ function detectPython() {
52
68
  // ignore
53
69
  }
54
70
  }
55
- throw err('Python 3 (>= 3.9) introuvable dans le PATH.', 'Installe Python >= 3.9 pour utiliser SPARDA sur un projet FastAPI.');
71
+ throw err(
72
+ 'Python 3 (>= 3.9) introuvable dans le PATH.',
73
+ 'Installe Python >= 3.9 pour utiliser SPARDA sur un projet FastAPI.',
74
+ );
56
75
  }
57
76
 
58
77
  function findFastAPIEntry(cwd) {
59
- const candidates = [
60
- 'main.py', 'app.py', 'src/main.py', 'app/main.py'
61
- ];
78
+ const candidates = ['main.py', 'app.py', 'src/main.py', 'app/main.py'];
62
79
  for (const rel of candidates) {
63
80
  const abs = path.resolve(cwd, rel);
64
81
  if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
@@ -68,17 +85,27 @@ function findFastAPIEntry(cwd) {
68
85
  }
69
86
  }
70
87
  }
71
-
88
+
72
89
  const entry = searchPyFiles(cwd, cwd);
73
90
  if (entry) {
74
91
  return path.relative(cwd, entry).split(path.sep).join('/');
75
92
  }
76
93
 
77
- throw err('Could not locate your FastAPI entry file (the one calling FastAPI()).', 'Specify it manually, or make sure FastAPI() is declared in one of your python files.');
94
+ throw err(
95
+ 'Could not locate your FastAPI entry file (the one calling FastAPI()).',
96
+ 'Specify it manually, or make sure FastAPI() is declared in one of your python files.',
97
+ );
78
98
  }
79
99
 
80
100
  function searchPyFiles(dir, root, countRef = { val: 0 }) {
81
- const EXCLUDE = new Set(['node_modules', '.git', 'venv', '.venv', 'tests', '__pycache__']);
101
+ const EXCLUDE = new Set([
102
+ 'node_modules',
103
+ '.git',
104
+ 'venv',
105
+ '.venv',
106
+ 'tests',
107
+ '__pycache__',
108
+ ]);
82
109
  let items;
83
110
  try {
84
111
  items = fs.readdirSync(dir);
@@ -129,18 +156,34 @@ function findExpressEntry(cwd, pkg) {
129
156
  if (m) candidates.push(m[1]);
130
157
  }
131
158
  candidates.push(
132
- 'src/app.ts', 'src/server.ts', 'src/index.ts', 'app.ts', 'server.ts', 'index.ts',
133
- 'src/app.js', 'src/server.js', 'src/index.js', 'app.js', 'server.js', 'index.js',
134
- 'src/app.mjs', 'app.mjs', 'index.mjs',
159
+ 'src/app.ts',
160
+ 'src/server.ts',
161
+ 'src/index.ts',
162
+ 'app.ts',
163
+ 'server.ts',
164
+ 'index.ts',
165
+ 'src/app.js',
166
+ 'src/server.js',
167
+ 'src/index.js',
168
+ 'app.js',
169
+ 'server.js',
170
+ 'index.js',
171
+ 'src/app.mjs',
172
+ 'app.mjs',
173
+ 'index.mjs',
135
174
  );
136
175
  for (const rel of candidates) {
137
176
  const abs = path.resolve(cwd, rel);
138
177
  if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
139
178
  const src = fs.readFileSync(abs, 'utf8');
140
- if (/express\s*\(/.test(src)) return path.relative(cwd, abs).split(path.sep).join('/');
179
+ if (/express\s*\(/.test(src))
180
+ return path.relative(cwd, abs).split(path.sep).join('/');
141
181
  }
142
182
  }
143
- throw err('Could not locate your Express entry file (the one calling express()).', 'Re-run from the project root, or open an issue with your layout.');
183
+ throw err(
184
+ 'Could not locate your Express entry file (the one calling express()).',
185
+ 'Re-run from the project root, or open an issue with your layout.',
186
+ );
144
187
  }
145
188
 
146
189
  function detectModuleType(cwd, pkg, entryFile) {
@@ -148,7 +191,11 @@ function detectModuleType(cwd, pkg, entryFile) {
148
191
  if (entryFile.endsWith('.cjs')) return 'cjs';
149
192
  if (pkg.type === 'module' || entryFile.endsWith('.ts')) {
150
193
  const src = fs.readFileSync(path.join(cwd, entryFile), 'utf8');
151
- if (/^\s*(const|let|var)\s+\w+\s*=\s*require\(/m.test(src) && !/^\s*import\s/m.test(src)) return 'cjs';
194
+ if (
195
+ /^\s*(const|let|var)\s+\w+\s*=\s*require\(/m.test(src) &&
196
+ !/^\s*import\s/m.test(src)
197
+ )
198
+ return 'cjs';
152
199
  return 'esm';
153
200
  }
154
201
  const src = fs.readFileSync(path.join(cwd, entryFile), 'utf8');
@@ -163,7 +210,10 @@ function detectExpressPort(cwd, entryFile) {
163
210
  if (m) {
164
211
  const envPath = path.join(cwd, '.env');
165
212
  if (fs.existsSync(envPath)) {
166
- const line = fs.readFileSync(envPath, 'utf8').split(/\r?\n/).find((l) => l.startsWith(`${m[1]}=`));
213
+ const line = fs
214
+ .readFileSync(envPath, 'utf8')
215
+ .split(/\r?\n/)
216
+ .find((l) => l.startsWith(`${m[1]}=`));
167
217
  if (line) {
168
218
  const v = Number(line.split('=')[1].trim());
169
219
  if (v) return v;