apiforge-analyzer 1.1.0 → 1.3.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/README.md +166 -197
- package/dist/cjs/arch-detect.js +133 -0
- package/dist/cjs/cli.js +866 -244
- package/dist/cjs/detect.js +68 -48
- package/dist/cjs/export.js +2 -0
- package/dist/cjs/frameworks/express.js +80 -74
- package/dist/cjs/frameworks/nestjs.js +38 -0
- package/dist/cjs/index.js +158 -60
- package/dist/cjs/modules/express.js +65 -0
- package/dist/cjs/modules/nestjs.js +50 -0
- package/dist/cjs/uploader.js +51 -11
- package/dist/esm/arch-detect.js +130 -0
- package/dist/esm/cli.js +865 -246
- package/dist/esm/detect.js +68 -48
- package/dist/esm/export.js +2 -0
- package/dist/esm/frameworks/express.js +80 -74
- package/dist/esm/frameworks/nestjs.js +35 -0
- package/dist/esm/index.js +158 -62
- package/dist/esm/modules/express.js +60 -0
- package/dist/esm/modules/nestjs.js +47 -0
- package/dist/esm/uploader.js +49 -11
- package/dist/types/arch-detect.d.ts +9 -0
- package/dist/types/detect.d.ts +3 -5
- package/dist/types/export.d.ts +2 -0
- package/dist/types/frameworks/express.d.ts +2 -2
- package/dist/types/frameworks/nestjs.d.ts +10 -0
- package/dist/types/index.d.ts +12 -5
- package/dist/types/modules/express.d.ts +22 -0
- package/dist/types/modules/nestjs.d.ts +6 -0
- package/dist/types/uploader.d.ts +17 -2
- package/package.json +3 -2
package/dist/cjs/cli.js
CHANGED
|
@@ -1,300 +1,901 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
3
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
7
|
const prompts_1 = require("@clack/prompts");
|
|
5
8
|
const fs_1 = require("fs");
|
|
6
9
|
const path_1 = require("path");
|
|
7
|
-
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const axios_1 = __importDefault(require("axios"));
|
|
12
|
+
const arch_detect_js_1 = require("./arch-detect.js");
|
|
13
|
+
const uploader_js_1 = require("./uploader.js");
|
|
14
|
+
// ─── ANSI colour helpers ──────────────────────────────────────────────────────
|
|
8
15
|
const r = '\x1b[0m';
|
|
9
16
|
const b = '\x1b[1m';
|
|
10
17
|
const d = '\x1b[2m';
|
|
11
18
|
const cy = '\x1b[36m';
|
|
12
19
|
const gn = '\x1b[32m';
|
|
13
20
|
const yl = '\x1b[33m';
|
|
14
|
-
const
|
|
21
|
+
const rd = '\x1b[31m';
|
|
15
22
|
const wh = '\x1b[97m';
|
|
16
23
|
const bold = (s) => `${b}${s}${r}`;
|
|
17
24
|
const dim = (s) => `${d}${s}${r}`;
|
|
18
25
|
const cyan = (s) => `${cy}${s}${r}`;
|
|
19
26
|
const green = (s) => `${gn}${s}${r}`;
|
|
20
27
|
const yellow = (s) => `${yl}${s}${r}`;
|
|
21
|
-
const
|
|
28
|
+
const red = (s) => `${rd}${s}${r}`;
|
|
22
29
|
const white = (s) => `${wh}${s}${r}`;
|
|
23
|
-
|
|
30
|
+
const VERSION = '1.3.0';
|
|
31
|
+
// Built-in URLs — users never need to configure these
|
|
32
|
+
const BACKEND_PROD = 'https://apiforgeapi.brainfogagency.com';
|
|
33
|
+
const DASHBOARD_PROD = 'https://apiforge.brainfogagency.com/dashboard';
|
|
34
|
+
// Local overrides — only used when testing APIForge itself
|
|
35
|
+
const BACKEND_LOCAL = 'http://localhost:4000';
|
|
36
|
+
const DASHBOARD_LOCAL = 'http://localhost:3000/dashboard';
|
|
37
|
+
const STATUS_FILE = '.apiforge-status.json';
|
|
38
|
+
const LOCK_FILE = '.apiforge-last-run.json';
|
|
39
|
+
// ─── Header ───────────────────────────────────────────────────────────────────
|
|
24
40
|
function printHeader() {
|
|
25
|
-
|
|
41
|
+
console.log([
|
|
26
42
|
'',
|
|
27
43
|
` ${cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`,
|
|
28
44
|
'',
|
|
29
|
-
` ${bold(white('⚡ APIForge'))} ${dim(
|
|
45
|
+
` ${bold(white('⚡ APIForge'))} ${dim(`v${VERSION}`)}`,
|
|
30
46
|
` ${dim('AI-powered API documentation, auto-generated')}`,
|
|
31
47
|
'',
|
|
32
48
|
` ${dim('Your entire API, documented in under 60 seconds.')}`,
|
|
33
49
|
'',
|
|
34
50
|
` ${cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`,
|
|
35
51
|
'',
|
|
36
|
-
];
|
|
37
|
-
console.log(lines.join('\n'));
|
|
52
|
+
].join('\n'));
|
|
38
53
|
}
|
|
39
|
-
// ───
|
|
40
|
-
let detectedEntry = '';
|
|
41
|
-
let detectedAppVar = 'app';
|
|
42
|
-
let detectedIsESM = false;
|
|
43
|
-
let detectedFramework = 'unknown';
|
|
44
|
-
let injectionPoint = '';
|
|
45
|
-
let wasAlreadySetup = false;
|
|
46
|
-
let linesAdded = 0;
|
|
47
|
-
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
54
|
+
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
48
55
|
async function main() {
|
|
49
|
-
process.stdout.write('\x1Bc');
|
|
56
|
+
process.stdout.write('\x1Bc');
|
|
50
57
|
printHeader();
|
|
51
|
-
(0, prompts_1.intro)(bold(` ${cyan('⚡')} APIForge
|
|
52
|
-
// ──
|
|
58
|
+
(0, prompts_1.intro)(bold(` ${cyan('⚡')} APIForge Analyzer `));
|
|
59
|
+
// ── Collect setup answers ─────────────────────────────────────────────────
|
|
53
60
|
const answers = await (0, prompts_1.group)({
|
|
54
61
|
projectName: () => (0, prompts_1.text)({
|
|
55
|
-
message: '
|
|
56
|
-
placeholder: 'My
|
|
57
|
-
validate:
|
|
62
|
+
message: 'Project name',
|
|
63
|
+
placeholder: 'My API',
|
|
64
|
+
validate: v => (!v.trim() ? 'Required' : undefined),
|
|
58
65
|
}),
|
|
59
66
|
apiKey: () => (0, prompts_1.password)({
|
|
60
67
|
message: `APIForge API key ${dim('(get one at apiforge.brainfogagency.com/dashboard)')}`,
|
|
61
|
-
validate:
|
|
68
|
+
validate: v => {
|
|
62
69
|
if (!v.trim())
|
|
63
|
-
return '
|
|
70
|
+
return 'Required';
|
|
64
71
|
if (!v.trim().startsWith('afg_'))
|
|
65
72
|
return 'Key must start with afg_';
|
|
66
73
|
},
|
|
67
74
|
}),
|
|
68
75
|
baseUrl: () => (0, prompts_1.text)({
|
|
69
|
-
message: 'Your server base URL',
|
|
76
|
+
message: 'Your server base URL (for documentation)',
|
|
70
77
|
placeholder: 'https://api.myapp.com',
|
|
71
78
|
initialValue: 'http://localhost:3000',
|
|
72
|
-
validate:
|
|
73
|
-
}),
|
|
74
|
-
method: () => (0, prompts_1.select)({
|
|
75
|
-
message: 'Sync method',
|
|
76
|
-
options: [
|
|
77
|
-
{
|
|
78
|
-
value: 'runtime',
|
|
79
|
-
label: `${bold('Runtime')} ${dim('→')} auto-syncs on every server start`,
|
|
80
|
-
hint: 'recommended',
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
value: 'json',
|
|
84
|
-
label: `${bold('Export JSON')} ${dim('→')} write api-export.json, import to dashboard`,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
79
|
+
validate: v => (!v.trim() ? 'Required' : undefined),
|
|
87
80
|
}),
|
|
88
81
|
}, {
|
|
89
82
|
onCancel: () => {
|
|
90
|
-
(0, prompts_1.cancel)(dim('
|
|
83
|
+
(0, prompts_1.cancel)(dim('Cancelled.'));
|
|
91
84
|
process.exit(0);
|
|
92
85
|
},
|
|
93
86
|
});
|
|
94
87
|
if ((0, prompts_1.isCancel)(answers))
|
|
95
88
|
process.exit(0);
|
|
96
|
-
const { projectName, apiKey, baseUrl
|
|
97
|
-
|
|
98
|
-
prompts_1.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (!preDetected) {
|
|
106
|
-
prompts_1.log.warn('Could not auto-detect your server entry file.');
|
|
107
|
-
const custom = await (0, prompts_1.text)({
|
|
108
|
-
message: 'Path to your server entry file',
|
|
109
|
-
placeholder: 'src/index.ts',
|
|
110
|
-
validate: (v) => {
|
|
111
|
-
if (!v.trim())
|
|
112
|
-
return 'Required';
|
|
113
|
-
if (!(0, fs_1.existsSync)((0, path_1.join)(process.cwd(), v.trim())))
|
|
114
|
-
return `Not found: ${v.trim()}`;
|
|
89
|
+
const { projectName, apiKey, baseUrl } = answers;
|
|
90
|
+
// ── Select backend ────────────────────────────────────────────────────────
|
|
91
|
+
const backendChoice = await (0, prompts_1.select)({
|
|
92
|
+
message: 'Send routes to',
|
|
93
|
+
options: [
|
|
94
|
+
{
|
|
95
|
+
value: 'production',
|
|
96
|
+
label: `${bold('Production')} ${dim('→')} ${dim(BACKEND_PROD)}`,
|
|
97
|
+
hint: 'recommended',
|
|
115
98
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
99
|
+
{
|
|
100
|
+
value: 'local',
|
|
101
|
+
label: `${bold('Local')} ${dim('→')} ${dim(BACKEND_LOCAL)}`,
|
|
102
|
+
hint: 'for testing APIForge locally',
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
if ((0, prompts_1.isCancel)(backendChoice)) {
|
|
107
|
+
(0, prompts_1.cancel)('Cancelled.');
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
const isLocal = backendChoice === 'local';
|
|
111
|
+
const backendUrl = isLocal ? BACKEND_LOCAL : BACKEND_PROD;
|
|
112
|
+
const dashboardUrl = isLocal ? DASHBOARD_LOCAL : DASHBOARD_PROD;
|
|
113
|
+
// ── Detect architecture type ─────────────────────────────────────────────
|
|
114
|
+
const archResult = (0, arch_detect_js_1.detectArchitectureType)(process.cwd());
|
|
115
|
+
const archLabel = {
|
|
116
|
+
'monolithic': cyan('Monolithic'),
|
|
117
|
+
'modular-monolithic': cyan('Modular Monolithic'),
|
|
118
|
+
'microservices': cyan('Microservices'),
|
|
119
|
+
};
|
|
120
|
+
// ── Full multi-service mode when apiforge.config.json exists ─────────────
|
|
121
|
+
if (archResult.type === 'microservices') {
|
|
122
|
+
let microConfig = loadMicroConfig();
|
|
123
|
+
// Auto-create apiforge.config.json if not present
|
|
124
|
+
if (!microConfig) {
|
|
125
|
+
microConfig = await buildAndSaveConfig(projectName);
|
|
126
|
+
}
|
|
127
|
+
if (microConfig && microConfig.services.length > 0) {
|
|
128
|
+
prompts_1.log.info(dim('─────────────────────────────────────────────────'));
|
|
129
|
+
prompts_1.log.info(`${dim('Project:')} ${bold(projectName)}`);
|
|
130
|
+
prompts_1.log.info(`${dim('Backend:')} ${cyan(backendUrl)}`);
|
|
131
|
+
prompts_1.log.info(`${dim('Architecture:')} ${archLabel['microservices']}`);
|
|
132
|
+
prompts_1.log.info(`${dim('Services:')} ${bold(String(microConfig.services.length))} ${dim('(from apiforge.config.json)')}`);
|
|
133
|
+
prompts_1.log.info(dim('─────────────────────────────────────────────────'));
|
|
134
|
+
await microservicesFlow(microConfig, { projectName, apiKey, baseUrl, backendUrl, dashboardUrl });
|
|
135
|
+
return;
|
|
120
136
|
}
|
|
121
|
-
entryFile = custom.trim();
|
|
122
137
|
}
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
// ── Single-service: ask for run command ───────────────────────────────────
|
|
139
|
+
const scripts = detectScripts();
|
|
140
|
+
const scriptOptions = scripts.map(s => ({
|
|
141
|
+
value: s.command,
|
|
142
|
+
label: `${bold(s.name)} ${dim('→')} ${dim(s.command)}`,
|
|
143
|
+
}));
|
|
144
|
+
if (scriptOptions.length === 0) {
|
|
145
|
+
prompts_1.log.warn('No scripts found in package.json — please ensure you have a start/dev script.');
|
|
146
|
+
(0, prompts_1.cancel)('No run script available.');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
const runCommand = await (0, prompts_1.select)({
|
|
150
|
+
message: 'Which command starts your server?',
|
|
151
|
+
options: scriptOptions,
|
|
152
|
+
});
|
|
153
|
+
if ((0, prompts_1.isCancel)(runCommand)) {
|
|
154
|
+
(0, prompts_1.cancel)('Cancelled.');
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
if (archResult.type === 'microservices') {
|
|
158
|
+
(0, prompts_1.note)([
|
|
159
|
+
`${yellow('Microservices architecture detected.')}`,
|
|
160
|
+
'',
|
|
161
|
+
`${dim('Add an')} ${bold('apiforge.config.json')} ${dim('to document all services automatically:')}`,
|
|
162
|
+
'',
|
|
163
|
+
` ${cyan('{ "services": [{ "name": "auth", "dir": "./services/auth" }] }')}`,
|
|
164
|
+
'',
|
|
165
|
+
`${dim('For now, analyzing the single service started by the selected command.')}`,
|
|
166
|
+
].join('\n'), 'Multi-service tip');
|
|
167
|
+
}
|
|
168
|
+
prompts_1.log.info(dim('─────────────────────────────────────────────────'));
|
|
169
|
+
prompts_1.log.info(`${dim('Project:')} ${bold(projectName)}`);
|
|
170
|
+
prompts_1.log.info(`${dim('Command:')} ${bold(runCommand)}`);
|
|
171
|
+
prompts_1.log.info(`${dim('Backend:')} ${cyan(backendUrl)}`);
|
|
172
|
+
prompts_1.log.info(`${dim('Architecture:')} ${archLabel[archResult.type]}`);
|
|
173
|
+
prompts_1.log.info(dim('─────────────────────────────────────────────────'));
|
|
174
|
+
if (archResult.moduleNames.length > 0) {
|
|
175
|
+
prompts_1.log.info(`${dim('Modules found:')} ${archResult.moduleNames.map(m => bold(m)).join(', ')}`);
|
|
125
176
|
}
|
|
126
|
-
// ──
|
|
177
|
+
// ── Detect & patch entry file ─────────────────────────────────────────────
|
|
178
|
+
const entryFile = detectEntryFile(archResult.type) ?? await askEntryFile(archResult.type);
|
|
179
|
+
let injected = false;
|
|
180
|
+
let linesAdded = 0;
|
|
127
181
|
await (0, prompts_1.tasks)([
|
|
128
182
|
{
|
|
129
|
-
title:
|
|
130
|
-
task: async (msg) => {
|
|
131
|
-
await sleep(350);
|
|
132
|
-
detectedEntry = entryFile;
|
|
133
|
-
msg(`Reading ${detectedEntry}`);
|
|
134
|
-
await sleep(250);
|
|
135
|
-
return `${green('✓')} Entry file: ${bold(detectedEntry)}`;
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
title: 'Analysing server file',
|
|
183
|
+
title: 'Scanning project',
|
|
140
184
|
task: async (msg) => {
|
|
141
185
|
await sleep(200);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
detectedAppVar = detectAppVariable(src) ?? 'app';
|
|
146
|
-
detectedFramework = detectFramework(src);
|
|
147
|
-
msg(`Detected ${detectedFramework}`);
|
|
148
|
-
await sleep(300);
|
|
149
|
-
const moduleType = detectedIsESM ? 'ESM' : 'CJS';
|
|
150
|
-
return [
|
|
151
|
-
`${green('✓')} Framework: ${bold(detectedFramework)}`,
|
|
152
|
-
` Module: ${bold(moduleType)}`,
|
|
153
|
-
` App var: ${bold(detectedAppVar)}`,
|
|
154
|
-
].join('\n');
|
|
186
|
+
msg(`Found entry file: ${entryFile}`);
|
|
187
|
+
await sleep(150);
|
|
188
|
+
return `${green('✓')} Entry: ${bold(entryFile)}`;
|
|
155
189
|
},
|
|
156
190
|
},
|
|
157
191
|
{
|
|
158
192
|
title: 'Patching server file',
|
|
159
193
|
task: async (msg) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
msg('Injecting analyzer…');
|
|
195
|
+
await sleep(100);
|
|
196
|
+
// Clear stale status + lock files before starting
|
|
197
|
+
tryUnlink((0, path_1.join)(process.cwd(), STATUS_FILE));
|
|
198
|
+
tryUnlink((0, path_1.join)(process.cwd(), LOCK_FILE));
|
|
199
|
+
// Store API key in .env.local — never embed it in source code
|
|
200
|
+
persistApiKeyToEnv(apiKey);
|
|
201
|
+
const result = injectCode(entryFile, projectName, apiKey, baseUrl, backendUrl, archResult.type);
|
|
202
|
+
injected = !result.alreadyInjected;
|
|
166
203
|
linesAdded = result.linesAdded ?? 0;
|
|
167
|
-
msg(result.alreadyInjected ? 'Already configured' : `Writing to ${detectedEntry}`);
|
|
168
|
-
await sleep(300);
|
|
169
204
|
if (result.alreadyInjected) {
|
|
170
|
-
return `${yellow('◌')} Already
|
|
205
|
+
return `${yellow('◌')} Already configured — no changes made`;
|
|
171
206
|
}
|
|
172
207
|
return [
|
|
173
|
-
`${green('✓')} Patched
|
|
174
|
-
`
|
|
208
|
+
`${green('✓')} Patched ${bold(entryFile)} ${dim(`+${linesAdded} lines`)}`,
|
|
209
|
+
` ${green('✓')} API key saved to ${bold('.env.local')} ${dim('(gitignored)')}`,
|
|
175
210
|
].join('\n');
|
|
176
211
|
},
|
|
177
212
|
},
|
|
178
|
-
{
|
|
179
|
-
title: 'Verifying patch',
|
|
180
|
-
task: async (msg) => {
|
|
181
|
-
await sleep(200);
|
|
182
|
-
if (wasAlreadySetup) {
|
|
183
|
-
return `${yellow('◌')} Skipped — file was already configured`;
|
|
184
|
-
}
|
|
185
|
-
const src = (0, fs_1.readFileSync)((0, path_1.join)(process.cwd(), detectedEntry), 'utf-8');
|
|
186
|
-
if (!src.includes('apiforge-analyzer')) {
|
|
187
|
-
throw new Error('Patch verification failed — please add the snippet manually');
|
|
188
|
-
}
|
|
189
|
-
msg('Confirmed apiforge-analyzer in file');
|
|
190
|
-
await sleep(200);
|
|
191
|
-
return `${green('✓')} Verified`;
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
213
|
]);
|
|
195
|
-
// ──
|
|
196
|
-
|
|
214
|
+
// ── Start server + wait for analysis ──────────────────────────────────────
|
|
215
|
+
(0, prompts_1.note)([
|
|
216
|
+
bold('Starting your server…'),
|
|
217
|
+
'',
|
|
218
|
+
` ${dim('Command:')} ${cyan(runCommand)}`,
|
|
219
|
+
` ${dim('Watching')} .apiforge-status.json ${dim('for completion signal')}`,
|
|
220
|
+
].join('\n'), 'Running');
|
|
221
|
+
const s = (0, prompts_1.spinner)();
|
|
222
|
+
s.start('Starting server and analyzing routes…');
|
|
223
|
+
const serverResult = await startAndWaitForAnalysis(runCommand, 90000);
|
|
224
|
+
if (serverResult.status === 'error') {
|
|
225
|
+
s.stop(red(`✗ ${serverResult.message}`));
|
|
226
|
+
// Give a targeted hint for common failure modes
|
|
227
|
+
if (serverResult.message.includes('API key')) {
|
|
228
|
+
prompts_1.log.warn(yellow('Hint: The CLI wrote your key to .env.local — but your server may not load it.'));
|
|
229
|
+
prompts_1.log.warn(yellow(' Add this at the top of your entry file before calling analyze():'));
|
|
230
|
+
prompts_1.log.info(` ${cyan("require('dotenv').config({ path: '.env.local' })")}`);
|
|
231
|
+
prompts_1.log.warn(yellow(' Or install dotenv: npm install dotenv'));
|
|
232
|
+
}
|
|
233
|
+
else if (serverResult.message.includes('No routes')) {
|
|
234
|
+
prompts_1.log.warn(yellow('Hint: Make sure all routes are registered before analyze() is called.'));
|
|
235
|
+
prompts_1.log.warn(yellow(' The analyze() call should come AFTER all router.use() / app.get() etc.'));
|
|
236
|
+
}
|
|
237
|
+
else if (serverResult.message.includes('Timed out')) {
|
|
238
|
+
prompts_1.log.warn(yellow('Hint: Your server might be taking too long to start, or the analyzer'));
|
|
239
|
+
prompts_1.log.warn(yellow(' is not being called. Check your server logs for APIForge messages.'));
|
|
240
|
+
}
|
|
241
|
+
await offerCleanup(entryFile, injected);
|
|
242
|
+
(0, prompts_1.outro)(red('Analysis failed.'));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
s.stop(green(`✓ ${serverResult.routeCount} routes submitted to APIForge`));
|
|
246
|
+
// ── Poll for dashboard approval ───────────────────────────────────────────
|
|
247
|
+
const { pendingImportId } = serverResult;
|
|
248
|
+
(0, prompts_1.note)([
|
|
249
|
+
bold('Open your dashboard to review:'),
|
|
250
|
+
'',
|
|
251
|
+
` ${cyan(dashboardUrl)}`,
|
|
252
|
+
'',
|
|
253
|
+
dim(`In "Import JSON" → "Pending CLI Imports"`),
|
|
254
|
+
dim('Accept to start AI processing · Deny to discard.'),
|
|
255
|
+
].join('\n'), 'Action required');
|
|
256
|
+
const poll = (0, prompts_1.spinner)();
|
|
257
|
+
poll.start('Waiting for your decision in the dashboard… (5 min timeout)');
|
|
258
|
+
const approval = await pollForApproval(pendingImportId, apiKey, backendUrl, 5 * 60000);
|
|
259
|
+
if (approval.status === 'approved') {
|
|
260
|
+
poll.stop(green('✓ Accepted — AI processing started!'));
|
|
197
261
|
(0, prompts_1.note)([
|
|
198
|
-
bold('
|
|
262
|
+
bold('Your API is being documented.'),
|
|
199
263
|
'',
|
|
200
|
-
dim(
|
|
201
|
-
dim('
|
|
202
|
-
].join('\n'), '
|
|
264
|
+
` ${dim('Track progress:')} ${cyan(dashboardUrl)}`,
|
|
265
|
+
` ${dim('Import ID:')} ${dim(pendingImportId)}`,
|
|
266
|
+
].join('\n'), 'Processing');
|
|
203
267
|
}
|
|
204
|
-
else {
|
|
205
|
-
|
|
268
|
+
else if (approval.status === 'denied') {
|
|
269
|
+
poll.stop(red('✗ Import denied by dashboard.'));
|
|
206
270
|
(0, prompts_1.note)([
|
|
207
|
-
` ${
|
|
208
|
-
` ${dim('Mode')} ${bold(method === 'json' ? 'Export JSON' : 'Runtime sync')}`,
|
|
209
|
-
` ${dim('App var')} ${bold(detectedAppVar)} ${dim('(auto-detected)')}`,
|
|
210
|
-
` ${dim('Format')} ${bold(detectedIsESM ? 'ESM (import)' : 'CJS (require)')}`,
|
|
211
|
-
'',
|
|
212
|
-
dim('Injected snippet:'),
|
|
271
|
+
` ${bold('You denied this import from the dashboard.')}`,
|
|
213
272
|
'',
|
|
214
|
-
|
|
215
|
-
|
|
273
|
+
` ${dim('No documentation was generated.')}`,
|
|
274
|
+
` ${dim('Run')} ${cyan('npx apiforge')} ${dim('again whenever you\'re ready.')}`,
|
|
275
|
+
].join('\n'), 'Denied');
|
|
216
276
|
}
|
|
217
|
-
|
|
218
|
-
|
|
277
|
+
else {
|
|
278
|
+
poll.stop(yellow('◌ Timed out — no decision made within 5 minutes.'));
|
|
219
279
|
(0, prompts_1.note)([
|
|
220
|
-
|
|
280
|
+
` ${dim('Your import is still pending:')}`,
|
|
281
|
+
` ${cyan(dashboardUrl)}`,
|
|
221
282
|
'',
|
|
222
|
-
` ${dim('
|
|
223
|
-
`
|
|
283
|
+
` ${dim('Import ID:')} ${cyan(pendingImportId)}`,
|
|
284
|
+
` ${dim('You can still accept or deny it from the dashboard.')}`,
|
|
285
|
+
].join('\n'), 'Pending');
|
|
286
|
+
}
|
|
287
|
+
// ── Cleanup ───────────────────────────────────────────────────────────────
|
|
288
|
+
await offerCleanup(entryFile, injected);
|
|
289
|
+
tryUnlink((0, path_1.join)(process.cwd(), STATUS_FILE));
|
|
290
|
+
tryUnlink((0, path_1.join)(process.cwd(), LOCK_FILE));
|
|
291
|
+
const outroMsg = {
|
|
292
|
+
approved: `${green('✓')} Done! View your AI docs at ${cyan(dashboardUrl)}`,
|
|
293
|
+
denied: `${red('✗')} Import denied. No changes were made.`,
|
|
294
|
+
timeout: `${yellow('◌')} Timed out. Your import is still pending at ${cyan(dashboardUrl)}`,
|
|
295
|
+
}[approval.status];
|
|
296
|
+
(0, prompts_1.outro)(outroMsg);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Scan the project for services and interactively write apiforge.config.json.
|
|
300
|
+
* Called when microservices arch is detected but no config file exists yet.
|
|
301
|
+
*/
|
|
302
|
+
async function buildAndSaveConfig(projectName) {
|
|
303
|
+
const cwd = process.cwd();
|
|
304
|
+
// Collect services from filesystem scan (same logic as findServiceEntries but raw)
|
|
305
|
+
const discovered = [];
|
|
306
|
+
for (const parentDir of ['services', 'apps', 'packages', 'microservices', 'src/services']) {
|
|
307
|
+
const parentPath = (0, path_1.join)(cwd, parentDir);
|
|
308
|
+
if (!(0, fs_1.existsSync)(parentPath))
|
|
309
|
+
continue;
|
|
310
|
+
try {
|
|
311
|
+
const subdirs = (0, fs_1.readdirSync)(parentPath, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
312
|
+
for (const subdir of subdirs) {
|
|
313
|
+
const entry = findEntryInDir((0, path_1.join)(parentPath, subdir.name));
|
|
314
|
+
if (entry) {
|
|
315
|
+
discovered.push({ name: subdir.name, dir: `./${parentDir}/${subdir.name}` });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch { }
|
|
320
|
+
if (discovered.length > 0)
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
if (discovered.length === 0)
|
|
324
|
+
return null;
|
|
325
|
+
(0, prompts_1.note)([
|
|
326
|
+
bold(`Found ${discovered.length} service${discovered.length > 1 ? 's' : ''}:`),
|
|
327
|
+
'',
|
|
328
|
+
...discovered.map(s => ` ${dim('·')} ${bold(s.name)} ${dim('→')} ${dim(s.dir)}`),
|
|
329
|
+
'',
|
|
330
|
+
`${dim('apiforge.config.json will be created so you never have to set this up again.')}`,
|
|
331
|
+
].join('\n'), 'Auto-detected services');
|
|
332
|
+
const shouldCreate = await (0, prompts_1.confirm)({
|
|
333
|
+
message: `Create apiforge.config.json with these ${discovered.length} services?`,
|
|
334
|
+
initialValue: true,
|
|
335
|
+
});
|
|
336
|
+
if ((0, prompts_1.isCancel)(shouldCreate) || !shouldCreate)
|
|
337
|
+
return null;
|
|
338
|
+
const config = {
|
|
339
|
+
projectName,
|
|
340
|
+
architectureType: 'microservices',
|
|
341
|
+
services: discovered.map(s => ({ name: s.name, dir: s.dir })),
|
|
342
|
+
};
|
|
343
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(cwd, 'apiforge.config.json'), JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
344
|
+
prompts_1.log.success(green(`✓ Created apiforge.config.json (${discovered.length} services)`));
|
|
345
|
+
prompts_1.log.info(dim(' Commit this file so teammates can run apiforge without re-setup.'));
|
|
346
|
+
return { services: discovered };
|
|
347
|
+
}
|
|
348
|
+
function loadMicroConfig() {
|
|
349
|
+
for (const cfgName of ['apiforge.config.json', '.apiforge.json']) {
|
|
350
|
+
try {
|
|
351
|
+
const cfg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(process.cwd(), cfgName), 'utf-8'));
|
|
352
|
+
if (Array.isArray(cfg.services) && cfg.services.length > 0) {
|
|
353
|
+
return { projectName: cfg.projectName, services: cfg.services };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch { }
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
function serviceNodeCommand(entryRelPath) {
|
|
361
|
+
if (entryRelPath.endsWith('.ts')) {
|
|
362
|
+
const hasTsx = (0, fs_1.existsSync)((0, path_1.join)(process.cwd(), 'node_modules/.bin/tsx'));
|
|
363
|
+
const hasTsNode = (0, fs_1.existsSync)((0, path_1.join)(process.cwd(), 'node_modules/.bin/ts-node'));
|
|
364
|
+
if (hasTsx)
|
|
365
|
+
return `node_modules/.bin/tsx ${entryRelPath}`;
|
|
366
|
+
if (hasTsNode)
|
|
367
|
+
return `node_modules/.bin/ts-node ${entryRelPath}`;
|
|
368
|
+
return `npx --yes tsx ${entryRelPath}`;
|
|
369
|
+
}
|
|
370
|
+
return `node ${entryRelPath}`;
|
|
371
|
+
}
|
|
372
|
+
function waitForServiceStatusFile(statusFile, timeoutMs) {
|
|
373
|
+
return new Promise(resolve => {
|
|
374
|
+
const fullPath = (0, path_1.join)(process.cwd(), statusFile);
|
|
375
|
+
const poller = setInterval(() => {
|
|
376
|
+
try {
|
|
377
|
+
if (!(0, fs_1.existsSync)(fullPath))
|
|
378
|
+
return;
|
|
379
|
+
const data = JSON.parse((0, fs_1.readFileSync)(fullPath, 'utf-8'));
|
|
380
|
+
if (data.status === 'collected' && Array.isArray(data.routes)) {
|
|
381
|
+
clearInterval(poller);
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
resolve(data.routes);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch { }
|
|
387
|
+
}, 500);
|
|
388
|
+
const timer = setTimeout(() => {
|
|
389
|
+
clearInterval(poller);
|
|
390
|
+
resolve(null);
|
|
391
|
+
}, timeoutMs);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
async function microservicesFlow(config, opts) {
|
|
395
|
+
const cwd = process.cwd();
|
|
396
|
+
const allRoutes = [];
|
|
397
|
+
const serviceMeta = [];
|
|
398
|
+
const injectedFiles = [];
|
|
399
|
+
// Clear any stale per-service status files
|
|
400
|
+
for (const svc of config.services) {
|
|
401
|
+
tryUnlink((0, path_1.join)(cwd, `.apiforge-status-${svc.name}.json`));
|
|
402
|
+
}
|
|
403
|
+
tryUnlink((0, path_1.join)(cwd, STATUS_FILE));
|
|
404
|
+
tryUnlink((0, path_1.join)(cwd, LOCK_FILE));
|
|
405
|
+
persistApiKeyToEnv(opts.apiKey);
|
|
406
|
+
(0, prompts_1.note)([
|
|
407
|
+
bold(`Analyzing ${config.services.length} service${config.services.length > 1 ? 's' : ''}…`),
|
|
408
|
+
'',
|
|
409
|
+
...config.services.map(s => ` ${dim('·')} ${bold(s.name)} ${dim('→')} ${dim(s.dir)}`),
|
|
410
|
+
'',
|
|
411
|
+
dim('Each service is started individually and its routes are collected.'),
|
|
412
|
+
].join('\n'), 'Multi-service analysis');
|
|
413
|
+
for (const svc of config.services) {
|
|
414
|
+
const svcSpin = (0, prompts_1.spinner)();
|
|
415
|
+
svcSpin.start(`${bold(svc.name)} — starting…`);
|
|
416
|
+
const svcDir = svc.dir.replace(/^\.\//, '');
|
|
417
|
+
// Resolve entry file
|
|
418
|
+
const relEntry = svc.entryFile ?? findEntryInDir((0, path_1.join)(cwd, svcDir));
|
|
419
|
+
if (!relEntry) {
|
|
420
|
+
svcSpin.stop(yellow(`◌ ${svc.name} — no entry file found, skipping`));
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const fullEntry = (0, path_1.join)(svcDir, relEntry);
|
|
424
|
+
const statusFile = `.apiforge-status-${svc.name}.json`;
|
|
425
|
+
// Inject collection snippet (upload: false, writes to per-service status file)
|
|
426
|
+
injectCode(fullEntry, opts.projectName, opts.apiKey, opts.baseUrl, opts.backendUrl, 'microservices', {
|
|
427
|
+
serviceName: svc.name,
|
|
428
|
+
statusFile,
|
|
429
|
+
});
|
|
430
|
+
injectedFiles.push(fullEntry);
|
|
431
|
+
// Start service with plain node (or tsx for .ts files)
|
|
432
|
+
const startCmd = svc.startCommand ?? serviceNodeCommand(fullEntry);
|
|
433
|
+
const child = (0, child_process_1.spawn)(startCmd, {
|
|
434
|
+
cwd,
|
|
435
|
+
stdio: 'ignore',
|
|
436
|
+
shell: true,
|
|
437
|
+
env: { ...process.env },
|
|
438
|
+
detached: false,
|
|
439
|
+
});
|
|
440
|
+
// Wait up to 30 s for the status file to appear
|
|
441
|
+
const routes = await waitForServiceStatusFile(statusFile, 30000);
|
|
442
|
+
// Kill service process
|
|
443
|
+
try {
|
|
444
|
+
child.kill('SIGTERM');
|
|
445
|
+
}
|
|
446
|
+
catch { }
|
|
447
|
+
tryUnlink((0, path_1.join)(cwd, statusFile));
|
|
448
|
+
if (routes && routes.length > 0) {
|
|
449
|
+
const annotated = routes.map((r) => ({ ...r, service: svc.name }));
|
|
450
|
+
allRoutes.push(...annotated);
|
|
451
|
+
serviceMeta.push({ name: svc.name, baseUrl: svc.baseUrl });
|
|
452
|
+
svcSpin.stop(green(`✓ ${svc.name} — ${routes.length} routes collected`));
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
svcSpin.stop(yellow(`◌ ${svc.name} — no routes collected (timeout or error)`));
|
|
456
|
+
}
|
|
457
|
+
// Remove injected code
|
|
458
|
+
removeInjectedCode(fullEntry);
|
|
459
|
+
injectedFiles.splice(injectedFiles.indexOf(fullEntry), 1);
|
|
460
|
+
}
|
|
461
|
+
// Safety: clean up any remaining injections (e.g. if process was interrupted)
|
|
462
|
+
for (const f of injectedFiles)
|
|
463
|
+
removeInjectedCode(f);
|
|
464
|
+
if (allRoutes.length === 0) {
|
|
465
|
+
prompts_1.log.error(red('No routes were collected from any service.'));
|
|
466
|
+
(0, prompts_1.outro)(red('Analysis failed.'));
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
// Upload aggregated routes
|
|
470
|
+
const uploadSpin = (0, prompts_1.spinner)();
|
|
471
|
+
uploadSpin.start(`Uploading ${allRoutes.length} routes from ${serviceMeta.length} service${serviceMeta.length > 1 ? 's' : ''}…`);
|
|
472
|
+
const uploadResult = await (0, uploader_js_1.uploadRoutes)(allRoutes, {
|
|
473
|
+
projectName: opts.projectName,
|
|
474
|
+
apiKey: opts.apiKey,
|
|
475
|
+
baseUrl: opts.baseUrl,
|
|
476
|
+
apiForgeUrl: opts.backendUrl,
|
|
477
|
+
architectureType: 'microservices',
|
|
478
|
+
services: serviceMeta,
|
|
479
|
+
});
|
|
480
|
+
if (!uploadResult) {
|
|
481
|
+
uploadSpin.stop(red('✗ Upload failed'));
|
|
482
|
+
(0, prompts_1.outro)(red('Analysis failed.'));
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
uploadSpin.stop(green(`✓ ${uploadResult.routeCount} routes submitted across ${serviceMeta.length} service${serviceMeta.length > 1 ? 's' : ''}`));
|
|
486
|
+
// ── Poll for dashboard approval ───────────────────────────────────────────
|
|
487
|
+
(0, prompts_1.note)([
|
|
488
|
+
bold('Open your dashboard to review:'),
|
|
489
|
+
'',
|
|
490
|
+
` ${cyan(opts.dashboardUrl)}`,
|
|
491
|
+
'',
|
|
492
|
+
dim('In "Import JSON" → "Pending CLI Imports"'),
|
|
493
|
+
dim('Accept to start AI processing · Deny to discard.'),
|
|
494
|
+
].join('\n'), 'Action required');
|
|
495
|
+
const poll = (0, prompts_1.spinner)();
|
|
496
|
+
poll.start('Waiting for your decision in the dashboard… (5 min timeout)');
|
|
497
|
+
const approval = await pollForApproval(uploadResult.pendingImportId, opts.apiKey, opts.backendUrl, 5 * 60000);
|
|
498
|
+
const statusMsg = {
|
|
499
|
+
approved: green('✓ Accepted — AI processing started!'),
|
|
500
|
+
denied: red('✗ Import denied by dashboard.'),
|
|
501
|
+
timeout: yellow('◌ Timed out — no decision made within 5 minutes.'),
|
|
502
|
+
}[approval.status];
|
|
503
|
+
poll.stop(statusMsg);
|
|
504
|
+
if (approval.status === 'approved') {
|
|
505
|
+
(0, prompts_1.note)([
|
|
506
|
+
bold('Your API is being documented.'),
|
|
224
507
|
'',
|
|
225
|
-
` ${dim('
|
|
226
|
-
`
|
|
508
|
+
` ${dim('Track progress:')} ${cyan(opts.dashboardUrl)}`,
|
|
509
|
+
` ${dim('Import ID:')} ${dim(uploadResult.pendingImportId)}`,
|
|
510
|
+
].join('\n'), 'Processing');
|
|
511
|
+
}
|
|
512
|
+
else if (approval.status === 'denied') {
|
|
513
|
+
(0, prompts_1.note)([
|
|
514
|
+
` ${bold('You denied this import from the dashboard.')}`,
|
|
227
515
|
'',
|
|
228
|
-
` ${dim('
|
|
229
|
-
|
|
230
|
-
|
|
516
|
+
` ${dim('No documentation was generated.')}`,
|
|
517
|
+
` ${dim('Run')} ${cyan('npx apiforge')} ${dim('again whenever you\'re ready.')}`,
|
|
518
|
+
].join('\n'), 'Denied');
|
|
231
519
|
}
|
|
232
520
|
else {
|
|
233
521
|
(0, prompts_1.note)([
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
` ${dim('1.')} Start your server as normal:`,
|
|
237
|
-
` ${cyan(getStartCommand())}`,
|
|
238
|
-
'',
|
|
239
|
-
` ${dim('2.')} APIForge detects all routes automatically.`,
|
|
522
|
+
` ${dim('Your import is still pending:')}`,
|
|
523
|
+
` ${cyan(opts.dashboardUrl)}`,
|
|
240
524
|
'',
|
|
241
|
-
` ${dim('
|
|
242
|
-
`
|
|
243
|
-
].join('\n'), '
|
|
244
|
-
(0, prompts_1.outro)(`${green('✓')} All set — start your server and docs will sync automatically.`);
|
|
525
|
+
` ${dim('Import ID:')} ${cyan(uploadResult.pendingImportId)}`,
|
|
526
|
+
` ${dim('You can still accept or deny it from the dashboard.')}`,
|
|
527
|
+
].join('\n'), 'Pending');
|
|
245
528
|
}
|
|
529
|
+
tryUnlink((0, path_1.join)(cwd, STATUS_FILE));
|
|
530
|
+
tryUnlink((0, path_1.join)(cwd, LOCK_FILE));
|
|
531
|
+
const outroMsg = {
|
|
532
|
+
approved: `${green('✓')} Done! View your AI docs at ${cyan(opts.dashboardUrl)}`,
|
|
533
|
+
denied: `${red('✗')} Import denied. No changes were made.`,
|
|
534
|
+
timeout: `${yellow('◌')} Timed out. Your import is still pending at ${cyan(opts.dashboardUrl)}`,
|
|
535
|
+
}[approval.status];
|
|
536
|
+
(0, prompts_1.outro)(outroMsg);
|
|
246
537
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
538
|
+
function startAndWaitForAnalysis(command, timeoutMs) {
|
|
539
|
+
return new Promise(resolve => {
|
|
540
|
+
const statusPath = (0, path_1.join)(process.cwd(), STATUS_FILE);
|
|
541
|
+
const child = (0, child_process_1.spawn)(command, {
|
|
542
|
+
cwd: process.cwd(),
|
|
543
|
+
stdio: 'ignore',
|
|
544
|
+
shell: true,
|
|
545
|
+
env: { ...process.env },
|
|
546
|
+
detached: false,
|
|
547
|
+
});
|
|
548
|
+
let resolved = false;
|
|
549
|
+
function finish(result) {
|
|
550
|
+
if (resolved)
|
|
551
|
+
return;
|
|
552
|
+
resolved = true;
|
|
553
|
+
clearInterval(poller);
|
|
554
|
+
clearTimeout(timeout);
|
|
555
|
+
try {
|
|
556
|
+
child.kill('SIGTERM');
|
|
557
|
+
}
|
|
558
|
+
catch { }
|
|
559
|
+
resolve(result);
|
|
560
|
+
}
|
|
561
|
+
// Poll for the status file every 500 ms
|
|
562
|
+
const poller = setInterval(() => {
|
|
563
|
+
try {
|
|
564
|
+
if (!(0, fs_1.existsSync)(statusPath))
|
|
565
|
+
return;
|
|
566
|
+
const data = JSON.parse((0, fs_1.readFileSync)(statusPath, 'utf-8'));
|
|
567
|
+
if (data.status === 'submitted') {
|
|
568
|
+
finish({
|
|
569
|
+
status: 'submitted',
|
|
570
|
+
pendingImportId: data.pendingImportId ?? '',
|
|
571
|
+
routeCount: data.routeCount ?? 0,
|
|
572
|
+
message: `${data.routeCount ?? 0} routes submitted`,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
else if (data.status === 'error') {
|
|
576
|
+
finish({
|
|
577
|
+
status: 'error',
|
|
578
|
+
pendingImportId: '',
|
|
579
|
+
routeCount: 0,
|
|
580
|
+
message: data.error ?? 'Upload failed',
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch { }
|
|
585
|
+
}, 500);
|
|
586
|
+
const timeout = setTimeout(() => {
|
|
587
|
+
finish({
|
|
588
|
+
status: 'error',
|
|
589
|
+
pendingImportId: '',
|
|
590
|
+
routeCount: 0,
|
|
591
|
+
message: 'Timed out waiting for analysis (90 s). Is the server starting correctly?',
|
|
592
|
+
});
|
|
593
|
+
}, timeoutMs);
|
|
594
|
+
child.on('error', err => {
|
|
595
|
+
finish({ status: 'error', pendingImportId: '', routeCount: 0, message: err.message });
|
|
596
|
+
});
|
|
597
|
+
child.on('exit', (code, signal) => {
|
|
598
|
+
if (!resolved && signal !== 'SIGTERM' && code !== 0) {
|
|
599
|
+
finish({
|
|
600
|
+
status: 'error',
|
|
601
|
+
pendingImportId: '',
|
|
602
|
+
routeCount: 0,
|
|
603
|
+
message: `Server exited unexpectedly (code ${code})`,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
// ─── Backend polling ──────────────────────────────────────────────────────────
|
|
610
|
+
async function pollForApproval(pendingImportId, apiKey, backendUrl, timeoutMs) {
|
|
611
|
+
const deadline = Date.now() + timeoutMs;
|
|
612
|
+
while (Date.now() < deadline) {
|
|
613
|
+
try {
|
|
614
|
+
const res = await axios_1.default.get(`${backendUrl}/api/cli/${pendingImportId}/status`, {
|
|
615
|
+
headers: { 'X-API-Key': apiKey },
|
|
616
|
+
timeout: 8000,
|
|
617
|
+
});
|
|
618
|
+
const { status } = res.data?.data ?? {};
|
|
619
|
+
if (status === 'approved')
|
|
620
|
+
return { status: 'approved' };
|
|
621
|
+
if (status === 'denied')
|
|
622
|
+
return { status: 'denied' };
|
|
623
|
+
}
|
|
624
|
+
catch { }
|
|
625
|
+
await sleep(3000);
|
|
626
|
+
}
|
|
627
|
+
return { status: 'timeout' };
|
|
628
|
+
}
|
|
629
|
+
// ─── Cleanup helpers ──────────────────────────────────────────────────────────
|
|
630
|
+
async function offerCleanup(entryFile, wasInjected) {
|
|
631
|
+
if (!wasInjected)
|
|
632
|
+
return;
|
|
633
|
+
const shouldClean = await (0, prompts_1.confirm)({
|
|
634
|
+
message: `Remove APIForge code changes from ${bold(entryFile)}?`,
|
|
635
|
+
initialValue: true,
|
|
636
|
+
});
|
|
637
|
+
if ((0, prompts_1.isCancel)(shouldClean) || !shouldClean)
|
|
638
|
+
return;
|
|
639
|
+
const removed = removeInjectedCode(entryFile);
|
|
640
|
+
if (removed) {
|
|
641
|
+
prompts_1.log.success(green(`✓ Removed code changes from ${entryFile}`));
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
prompts_1.log.warn(yellow('Could not auto-remove — please delete the APIForge block manually.'));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function removeInjectedCode(filePath) {
|
|
648
|
+
const fullPath = (0, path_1.join)(process.cwd(), filePath);
|
|
649
|
+
try {
|
|
650
|
+
const content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
651
|
+
const startIdx = content.indexOf('// APIForge');
|
|
652
|
+
if (startIdx === -1)
|
|
653
|
+
return false;
|
|
654
|
+
const afterStart = content.slice(startIdx);
|
|
655
|
+
const endMatch = /\.catch\s*\(\s*\(\s*\)\s*=>\s*\{?\s*\}?\s*\)\s*\n?/.exec(afterStart);
|
|
656
|
+
if (!endMatch)
|
|
657
|
+
return false;
|
|
658
|
+
const endIdx = startIdx + endMatch.index + endMatch[0].length;
|
|
659
|
+
const cleaned = (content.slice(0, startIdx) + content.slice(endIdx))
|
|
660
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
661
|
+
.trimEnd() + '\n';
|
|
662
|
+
(0, fs_1.writeFileSync)(fullPath, cleaned, 'utf-8');
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/** Look for a server entry inside a single directory (checks src/ then root). */
|
|
670
|
+
function findEntryInDir(absDir) {
|
|
671
|
+
const names = ['index', 'server', 'app', 'main'];
|
|
672
|
+
const exts = ['ts', 'js', 'mjs', 'cjs'];
|
|
673
|
+
for (const depth of ['src', '']) {
|
|
674
|
+
for (const name of names) {
|
|
675
|
+
for (const ext of exts) {
|
|
676
|
+
const rel = depth ? (0, path_1.join)(depth, `${name}.${ext}`) : `${name}.${ext}`;
|
|
677
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(absDir, rel)))
|
|
678
|
+
return rel;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Detect the project's root/main entry point — package.json main,
|
|
686
|
+
* common gateway directories, then flat root candidates.
|
|
687
|
+
* Returns a relative path or null.
|
|
688
|
+
*/
|
|
689
|
+
function detectMainEntry(cwd) {
|
|
690
|
+
// 1. package.json main / start / dev script
|
|
250
691
|
try {
|
|
251
692
|
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(cwd, 'package.json'), 'utf-8'));
|
|
252
693
|
if (pkg.main && (0, fs_1.existsSync)((0, path_1.join)(cwd, pkg.main)))
|
|
253
694
|
return pkg.main;
|
|
254
|
-
for (const
|
|
255
|
-
const cmd = pkg.scripts?.[
|
|
256
|
-
const m = cmd.match(/(?:node|tsx|ts-node)\s+([^\s&|;]+\.(js|ts|mjs|cjs))/);
|
|
695
|
+
for (const s of ['start', 'dev', 'serve']) {
|
|
696
|
+
const cmd = pkg.scripts?.[s] ?? '';
|
|
697
|
+
const m = cmd.match(/(?:node|tsx|ts-node|nodemon)\s+([^\s&|;]+\.(js|ts|mjs|cjs))/);
|
|
257
698
|
if (m && (0, fs_1.existsSync)((0, path_1.join)(cwd, m[1])))
|
|
258
699
|
return m[1];
|
|
259
700
|
}
|
|
260
701
|
}
|
|
261
702
|
catch { }
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
703
|
+
// 2. Common gateway/proxy directory (e.g. api-gateway/)
|
|
704
|
+
for (const gatewayDir of ['api-gateway', 'gateway', 'proxy', 'api-proxy']) {
|
|
705
|
+
const entry = findEntryInDir((0, path_1.join)(cwd, gatewayDir));
|
|
706
|
+
if (entry)
|
|
707
|
+
return (0, path_1.join)(gatewayDir, entry);
|
|
708
|
+
}
|
|
709
|
+
// 3. Flat root candidates
|
|
710
|
+
for (const c of [
|
|
711
|
+
'index.ts', 'server.ts', 'app.ts', 'main.ts',
|
|
712
|
+
'index.js', 'server.js', 'app.js', 'main.js',
|
|
713
|
+
'src/index.ts', 'src/server.ts', 'src/app.ts', 'src/main.ts',
|
|
714
|
+
'src/index.js', 'src/server.js', 'src/app.js', 'src/main.js',
|
|
715
|
+
]) {
|
|
269
716
|
if ((0, fs_1.existsSync)((0, path_1.join)(cwd, c)))
|
|
270
717
|
return c;
|
|
718
|
+
}
|
|
271
719
|
return null;
|
|
272
720
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
721
|
+
/**
|
|
722
|
+
* Scan the project for per-service entry files.
|
|
723
|
+
* Prepends the root main entry (if any) so it appears first in the select list.
|
|
724
|
+
* Reads apiforge.config.json first; falls back to common service parent dirs.
|
|
725
|
+
*/
|
|
726
|
+
function findServiceEntries() {
|
|
727
|
+
const cwd = process.cwd();
|
|
728
|
+
const results = [];
|
|
729
|
+
// 0. Root / main entry — shown first so the user always sees it
|
|
730
|
+
const mainEntry = detectMainEntry(cwd);
|
|
731
|
+
if (mainEntry) {
|
|
732
|
+
results.push({ label: `[ Main entry ] → ${mainEntry}`, path: mainEntry });
|
|
733
|
+
}
|
|
734
|
+
// 1. Authoritative list from apiforge.config.json / .apiforge.json
|
|
735
|
+
for (const cfgName of ['apiforge.config.json', '.apiforge.json']) {
|
|
736
|
+
try {
|
|
737
|
+
const cfg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(cwd, cfgName), 'utf-8'));
|
|
738
|
+
if (!Array.isArray(cfg.services) || cfg.services.length === 0)
|
|
739
|
+
continue;
|
|
740
|
+
for (const svc of cfg.services) {
|
|
741
|
+
const dir = svc.dir.replace(/^\.\//, '');
|
|
742
|
+
const entry = findEntryInDir((0, path_1.join)(cwd, dir));
|
|
743
|
+
if (!entry)
|
|
744
|
+
continue;
|
|
745
|
+
const relPath = (0, path_1.join)(dir, entry);
|
|
746
|
+
// Skip if same path as the main entry already added
|
|
747
|
+
if (relPath !== mainEntry) {
|
|
748
|
+
results.push({ label: `${svc.name} → ${relPath}`, path: relPath });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (results.length > 0)
|
|
752
|
+
return results;
|
|
753
|
+
}
|
|
754
|
+
catch { }
|
|
755
|
+
}
|
|
756
|
+
// 2. Filesystem scan of common service parent dirs
|
|
757
|
+
for (const parentDir of ['services', 'apps', 'packages', 'src/services', 'microservices']) {
|
|
758
|
+
const parentPath = (0, path_1.join)(cwd, parentDir);
|
|
759
|
+
if (!(0, fs_1.existsSync)(parentPath))
|
|
760
|
+
continue;
|
|
761
|
+
try {
|
|
762
|
+
const subdirs = (0, fs_1.readdirSync)(parentPath, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
763
|
+
for (const subdir of subdirs) {
|
|
764
|
+
const entry = findEntryInDir((0, path_1.join)(parentPath, subdir.name));
|
|
765
|
+
if (!entry)
|
|
766
|
+
continue;
|
|
767
|
+
const relPath = (0, path_1.join)(parentDir, subdir.name, entry);
|
|
768
|
+
if (relPath !== mainEntry) {
|
|
769
|
+
results.push({ label: `${subdir.name} → ${relPath}`, path: relPath });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
catch { }
|
|
774
|
+
if (results.length > 1)
|
|
775
|
+
return results; // >1 because main entry is already at index 0
|
|
776
|
+
}
|
|
777
|
+
return results;
|
|
778
|
+
}
|
|
779
|
+
function detectEntryFile(archType) {
|
|
780
|
+
const cwd = process.cwd();
|
|
781
|
+
// For microservices: auto-pick only when exactly one service entry is found
|
|
782
|
+
if (archType === 'microservices') {
|
|
783
|
+
const entries = findServiceEntries();
|
|
784
|
+
return entries.length === 1 ? entries[0].path : null;
|
|
785
|
+
}
|
|
786
|
+
// Package.json hints
|
|
787
|
+
try {
|
|
788
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(cwd, 'package.json'), 'utf-8'));
|
|
789
|
+
if (pkg.main && (0, fs_1.existsSync)((0, path_1.join)(cwd, pkg.main)))
|
|
790
|
+
return pkg.main;
|
|
791
|
+
for (const s of ['start', 'dev', 'serve']) {
|
|
792
|
+
const cmd = pkg.scripts?.[s] ?? '';
|
|
793
|
+
const m = cmd.match(/(?:node|tsx|ts-node|nodemon)\s+([^\s&|;]+\.(js|ts|mjs|cjs))/);
|
|
794
|
+
if (m && (0, fs_1.existsSync)((0, path_1.join)(cwd, m[1])))
|
|
795
|
+
return m[1];
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
catch { }
|
|
799
|
+
// Flat file candidates — root → src/ → compiled output → misc
|
|
800
|
+
const candidates = [
|
|
801
|
+
'index.ts', 'server.ts', 'app.ts', 'main.ts',
|
|
802
|
+
'index.js', 'server.js', 'app.js', 'main.js',
|
|
803
|
+
'src/index.ts', 'src/server.ts', 'src/app.ts', 'src/main.ts',
|
|
804
|
+
'src/index.js', 'src/server.js', 'src/app.js', 'src/main.js',
|
|
805
|
+
'dist/index.js', 'dist/server.js', 'dist/main.js',
|
|
806
|
+
'build/index.js', 'build/server.js', 'build/main.js',
|
|
807
|
+
'lib/index.js', 'lib/server.js',
|
|
808
|
+
'server/index.ts', 'server/index.js',
|
|
809
|
+
'api/index.ts', 'api/index.js',
|
|
810
|
+
'src/server/index.ts', 'src/server/index.js',
|
|
811
|
+
'lambda.ts', 'lambda.js', 'handler.ts', 'handler.js',
|
|
812
|
+
];
|
|
813
|
+
return candidates.find(c => (0, fs_1.existsSync)((0, path_1.join)(cwd, c))) ?? null;
|
|
814
|
+
}
|
|
815
|
+
async function askEntryFile(archType) {
|
|
816
|
+
if (archType === 'microservices') {
|
|
817
|
+
const entries = findServiceEntries();
|
|
818
|
+
if (entries.length > 0) {
|
|
819
|
+
const choice = await (0, prompts_1.select)({
|
|
820
|
+
message: 'Which service entry file do you want to analyze?',
|
|
821
|
+
options: [
|
|
822
|
+
...entries.map(e => ({ value: e.path, label: e.label })),
|
|
823
|
+
{ value: '__manual', label: 'Enter path manually…' },
|
|
824
|
+
],
|
|
825
|
+
});
|
|
826
|
+
if ((0, prompts_1.isCancel)(choice)) {
|
|
827
|
+
(0, prompts_1.cancel)('Cancelled.');
|
|
828
|
+
process.exit(0);
|
|
829
|
+
}
|
|
830
|
+
if (choice !== '__manual')
|
|
831
|
+
return choice;
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
prompts_1.log.warn('Could not find service entry files in services/, apps/, or packages/.');
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
prompts_1.log.warn('Could not auto-detect your server entry file.');
|
|
839
|
+
}
|
|
840
|
+
const custom = await (0, prompts_1.text)({
|
|
841
|
+
message: 'Path to your server entry file',
|
|
842
|
+
placeholder: 'src/index.ts',
|
|
843
|
+
validate: v => {
|
|
844
|
+
if (!v.trim())
|
|
845
|
+
return 'Required';
|
|
846
|
+
if (!(0, fs_1.existsSync)((0, path_1.join)(process.cwd(), v.trim())))
|
|
847
|
+
return `Not found: ${v.trim()}`;
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
if ((0, prompts_1.isCancel)(custom)) {
|
|
851
|
+
(0, prompts_1.cancel)('Cancelled.');
|
|
852
|
+
process.exit(0);
|
|
853
|
+
}
|
|
854
|
+
return custom.trim();
|
|
855
|
+
}
|
|
856
|
+
function detectScripts() {
|
|
857
|
+
try {
|
|
858
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(process.cwd(), 'package.json'), 'utf-8'));
|
|
859
|
+
const scripts = pkg.scripts ?? {};
|
|
860
|
+
const preferred = ['dev', 'start', 'serve', 'run', 'develop'];
|
|
861
|
+
const result = [];
|
|
862
|
+
// Preferred scripts first
|
|
863
|
+
for (const name of preferred) {
|
|
864
|
+
if (scripts[name])
|
|
865
|
+
result.push({ name, command: `npm run ${name}` });
|
|
866
|
+
}
|
|
867
|
+
// Scripts that directly invoke a node runtime
|
|
868
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
869
|
+
if (preferred.includes(name))
|
|
870
|
+
continue;
|
|
871
|
+
if (/node|tsx|ts-node|nodemon|server/i.test(cmd)) {
|
|
872
|
+
result.push({ name, command: `npm run ${name}` });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// Workspace per-service scripts: start:*, dev:*, serve:* patterns
|
|
876
|
+
// (e.g. "dev:auth", "start:wallet" common in npm/pnpm monorepos)
|
|
877
|
+
for (const [name] of Object.entries(scripts)) {
|
|
878
|
+
if (preferred.includes(name))
|
|
879
|
+
continue;
|
|
880
|
+
if (/^(?:start|dev|serve|run):[a-z0-9_-]+$/i.test(name) && !result.some(r => r.name === name)) {
|
|
881
|
+
result.push({ name, command: `npm run ${name}` });
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return result;
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
288
889
|
}
|
|
289
|
-
function injectCode(filePath, projectName, apiKey, baseUrl,
|
|
890
|
+
function injectCode(filePath, projectName, apiKey, baseUrl, backendUrl, architectureType, collectionOpts) {
|
|
290
891
|
const fullPath = (0, path_1.join)(process.cwd(), filePath);
|
|
291
892
|
const content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
292
893
|
if (content.includes('apiforge-analyzer')) {
|
|
293
|
-
return {
|
|
894
|
+
return { alreadyInjected: true };
|
|
294
895
|
}
|
|
295
896
|
const isESM = detectESM(filePath, content);
|
|
296
897
|
const appVar = detectAppVariable(content) ?? 'app';
|
|
297
|
-
const snippet =
|
|
898
|
+
const snippet = buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, architectureType, collectionOpts);
|
|
298
899
|
const listenRe = /\b(?:app|server|httpServer|http)\s*\.listen\s*\(/;
|
|
299
900
|
const listenMatch = listenRe.exec(content);
|
|
300
901
|
let newContent;
|
|
@@ -309,58 +910,69 @@ function injectCode(filePath, projectName, apiKey, baseUrl, method) {
|
|
|
309
910
|
injectionPoint = 'end of file';
|
|
310
911
|
}
|
|
311
912
|
(0, fs_1.writeFileSync)(fullPath, newContent, 'utf-8');
|
|
312
|
-
|
|
313
|
-
return {
|
|
314
|
-
message: `Patched ${filePath}`,
|
|
315
|
-
alreadyInjected: false,
|
|
316
|
-
appVar,
|
|
317
|
-
injectionPoint,
|
|
318
|
-
linesAdded,
|
|
319
|
-
};
|
|
913
|
+
return { alreadyInjected: false, injectionPoint, linesAdded: snippet.split('\n').length };
|
|
320
914
|
}
|
|
321
|
-
|
|
915
|
+
/**
|
|
916
|
+
* Write (or overwrite) the API key in .env.local.
|
|
917
|
+
* Always overwrites so the key stays in sync with what the user entered —
|
|
918
|
+
* keeping a stale key would cause submit/poll key mismatch and 401s.
|
|
919
|
+
*/
|
|
920
|
+
function persistApiKeyToEnv(apiKey) {
|
|
921
|
+
const envPath = (0, path_1.join)(process.cwd(), '.env.local');
|
|
922
|
+
const marker = 'APIFORGE_API_KEY=';
|
|
322
923
|
try {
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
|
|
924
|
+
let content = (0, fs_1.existsSync)(envPath) ? (0, fs_1.readFileSync)(envPath, 'utf-8') : '';
|
|
925
|
+
if (content.includes(marker)) {
|
|
926
|
+
// Replace existing value — regex matches the whole line
|
|
927
|
+
content = content.replace(/^APIFORGE_API_KEY=.*/m, `${marker}${apiKey}`);
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
content = content.trimEnd() + `\n# APIForge CLI key (safe to delete after analysis)\n${marker}${apiKey}\n`;
|
|
931
|
+
}
|
|
932
|
+
(0, fs_1.writeFileSync)(envPath, content, 'utf-8');
|
|
326
933
|
}
|
|
327
934
|
catch { }
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return false;
|
|
332
|
-
return /^(?:import|export)\s+/m.test(content);
|
|
935
|
+
addToGitignore('.env.local');
|
|
936
|
+
addToGitignore('.apiforge-status.json');
|
|
937
|
+
addToGitignore('.apiforge-last-run.json');
|
|
333
938
|
}
|
|
334
|
-
function
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return m[1];
|
|
344
|
-
return null;
|
|
939
|
+
function addToGitignore(pattern) {
|
|
940
|
+
const giPath = (0, path_1.join)(process.cwd(), '.gitignore');
|
|
941
|
+
try {
|
|
942
|
+
const current = (0, fs_1.existsSync)(giPath) ? (0, fs_1.readFileSync)(giPath, 'utf-8') : '';
|
|
943
|
+
if (!current.split('\n').some(l => l.trim() === pattern)) {
|
|
944
|
+
(0, fs_1.writeFileSync)(giPath, current.trimEnd() + `\n${pattern}\n`, 'utf-8');
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
catch { }
|
|
345
948
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
949
|
+
function buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, architectureType, collectionOpts) {
|
|
950
|
+
// The API key is embedded directly — this snippet is TEMPORARY (auto-removed after analysis).
|
|
951
|
+
// Hardcoding guarantees the same key is used for both submit and CLI status polling,
|
|
952
|
+
// eliminating the mismatch that causes 401s when .env.local already has a different key.
|
|
953
|
+
const isProd = backendUrl === BACKEND_PROD;
|
|
954
|
+
// Collection mode (multi-service): upload: false, write to per-service status file
|
|
955
|
+
const opts = collectionOpts
|
|
350
956
|
? [
|
|
351
957
|
` apiKey: '${apiKey}',`,
|
|
352
958
|
` projectName: '${projectName}',`,
|
|
353
959
|
` baseUrl: '${baseUrl}',`,
|
|
354
|
-
`
|
|
355
|
-
`
|
|
960
|
+
` service: '${collectionOpts.serviceName}',`,
|
|
961
|
+
` statusFile: '${collectionOpts.statusFile}',`,
|
|
962
|
+
` architectureType: 'microservices',`,
|
|
356
963
|
` upload: false,`,
|
|
964
|
+
` runOnce: true,`,
|
|
357
965
|
].join('\n')
|
|
358
966
|
: [
|
|
359
967
|
` apiKey: '${apiKey}',`,
|
|
360
968
|
` projectName: '${projectName}',`,
|
|
361
969
|
` baseUrl: '${baseUrl}',`,
|
|
362
|
-
` apiForgeUrl: '
|
|
970
|
+
...(isProd ? [] : [` apiForgeUrl: '${backendUrl}',`]),
|
|
971
|
+
...(architectureType && architectureType !== 'monolithic'
|
|
972
|
+
? [` architectureType: '${architectureType}',`]
|
|
973
|
+
: []),
|
|
363
974
|
` upload: true,`,
|
|
975
|
+
` runOnce: true,`,
|
|
364
976
|
].join('\n');
|
|
365
977
|
if (isESM) {
|
|
366
978
|
return [
|
|
@@ -384,36 +996,46 @@ function getCodeSnippet(projectName, apiKey, baseUrl, method, appVar, isESM) {
|
|
|
384
996
|
'',
|
|
385
997
|
].join('\n');
|
|
386
998
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
999
|
+
function detectESM(filePath, content) {
|
|
1000
|
+
try {
|
|
1001
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(process.cwd(), 'package.json'), 'utf-8'));
|
|
1002
|
+
if (pkg.type === 'module')
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
catch { }
|
|
1006
|
+
if (filePath.endsWith('.mjs'))
|
|
1007
|
+
return true;
|
|
1008
|
+
if (filePath.endsWith('.cjs'))
|
|
1009
|
+
return false;
|
|
1010
|
+
return /^(?:import|export)\s+/m.test(content);
|
|
1011
|
+
}
|
|
1012
|
+
function detectAppVariable(content) {
|
|
1013
|
+
const patterns = [
|
|
1014
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*express\s*(?:\.default)?\s*\(/,
|
|
1015
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*(?:fastify|Fastify)\s*\(/,
|
|
1016
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*new\s+Koa\s*\(/,
|
|
1017
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*(?:Router|createApp|createServer)\s*\(/,
|
|
396
1018
|
];
|
|
1019
|
+
for (const re of patterns) {
|
|
1020
|
+
const m = content.match(re);
|
|
1021
|
+
if (m)
|
|
1022
|
+
return m[1];
|
|
1023
|
+
}
|
|
1024
|
+
return null;
|
|
397
1025
|
}
|
|
398
|
-
|
|
399
|
-
function
|
|
1026
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
1027
|
+
function tryUnlink(p) {
|
|
400
1028
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return 'npm start';
|
|
404
|
-
if (pkg.scripts?.dev)
|
|
405
|
-
return 'npm run dev';
|
|
406
|
-
if (pkg.scripts?.serve)
|
|
407
|
-
return 'npm run serve';
|
|
1029
|
+
if ((0, fs_1.existsSync)(p))
|
|
1030
|
+
(0, fs_1.unlinkSync)(p);
|
|
408
1031
|
}
|
|
409
1032
|
catch { }
|
|
410
|
-
return 'node ' + (detectedEntry || 'index.js');
|
|
411
1033
|
}
|
|
412
|
-
// ─── Util ─────────────────────────────────────────────────────────────────────
|
|
413
1034
|
function sleep(ms) {
|
|
414
|
-
return new Promise(
|
|
1035
|
+
return new Promise(res => setTimeout(res, ms));
|
|
415
1036
|
}
|
|
416
|
-
|
|
1037
|
+
// ─── Boot ─────────────────────────────────────────────────────────────────────
|
|
1038
|
+
main().catch(err => {
|
|
417
1039
|
console.error(err);
|
|
418
1040
|
process.exit(1);
|
|
419
1041
|
});
|