te.js 2.1.0 → 2.1.2
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 +197 -196
- package/auto-docs/analysis/handler-analyzer.js +58 -58
- package/auto-docs/analysis/source-resolver.js +101 -101
- package/auto-docs/constants.js +37 -37
- package/auto-docs/docs-llm/index.js +7 -7
- package/auto-docs/docs-llm/prompts.js +222 -222
- package/auto-docs/docs-llm/provider.js +132 -132
- package/auto-docs/index.js +146 -146
- package/auto-docs/openapi/endpoint-processor.js +277 -277
- package/auto-docs/openapi/generator.js +107 -107
- package/auto-docs/openapi/level3.js +131 -131
- package/auto-docs/openapi/spec-builders.js +244 -244
- package/auto-docs/ui/docs-ui.js +186 -186
- package/auto-docs/utils/logger.js +17 -17
- package/auto-docs/utils/strip-usage.js +10 -10
- package/cli/docs-command.js +315 -315
- package/cli/fly-command.js +71 -71
- package/cli/index.js +56 -56
- package/cors/index.js +71 -0
- package/database/index.js +165 -165
- package/database/mongodb.js +146 -146
- package/database/redis.js +201 -201
- package/docs/README.md +36 -36
- package/docs/ammo.md +362 -362
- package/docs/api-reference.md +490 -490
- package/docs/auto-docs.md +216 -216
- package/docs/cli.md +152 -152
- package/docs/configuration.md +275 -275
- package/docs/database.md +390 -390
- package/docs/error-handling.md +438 -438
- package/docs/file-uploads.md +333 -333
- package/docs/getting-started.md +214 -214
- package/docs/middleware.md +355 -355
- package/docs/rate-limiting.md +393 -393
- package/docs/routing.md +302 -302
- package/lib/llm/client.js +73 -0
- package/lib/llm/index.js +7 -0
- package/lib/llm/parse.js +89 -0
- package/package.json +64 -62
- package/rate-limit/algorithms/fixed-window.js +141 -141
- package/rate-limit/algorithms/sliding-window.js +147 -147
- package/rate-limit/algorithms/token-bucket.js +115 -115
- package/rate-limit/base.js +165 -165
- package/rate-limit/index.js +147 -147
- package/rate-limit/storage/base.js +104 -104
- package/rate-limit/storage/memory.js +101 -101
- package/rate-limit/storage/redis.js +88 -88
- package/server/ammo/body-parser.js +220 -220
- package/server/ammo/dispatch-helper.js +103 -103
- package/server/ammo/enhancer.js +57 -57
- package/server/ammo.js +454 -415
- package/server/endpoint.js +97 -74
- package/server/error.js +9 -9
- package/server/errors/code-context.js +125 -125
- package/server/errors/llm-error-service.js +140 -140
- package/server/files/helper.js +33 -33
- package/server/files/uploader.js +143 -143
- package/server/handler.js +158 -119
- package/server/target.js +185 -175
- package/server/targets/middleware-validator.js +22 -22
- package/server/targets/path-validator.js +21 -21
- package/server/targets/registry.js +160 -160
- package/server/targets/shoot-validator.js +21 -21
- package/te.js +428 -402
- package/utils/auto-register.js +17 -17
- package/utils/configuration.js +64 -64
- package/utils/errors-llm-config.js +84 -84
- package/utils/request-logger.js +43 -43
- package/utils/status-codes.js +82 -82
- package/utils/tejas-entrypoint-html.js +18 -18
package/cli/docs-command.js
CHANGED
|
@@ -1,315 +1,315 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `tejas generate:docs` — interactive documentation generator.
|
|
3
|
-
* Walks the developer through configuration, loads targets, generates an OpenAPI spec,
|
|
4
|
-
* and optionally serves a live preview.
|
|
5
|
-
*
|
|
6
|
-
* Non-interactive (CI/hooks): use `tejas generate:docs --ci` with options from
|
|
7
|
-
* tejas.config.json "docs" and env (LLM_API_KEY, LLM_BASE_URL, LLM_MODEL, etc.).
|
|
8
|
-
*
|
|
9
|
-
* Trigger on push to production: add `tejas docs:on-push` to your pre-push hook;
|
|
10
|
-
* configure production branch via config.docs.productionBranch or DOCS_PRODUCTION_BRANCH.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
import { createInterface } from 'node:readline';
|
|
15
|
-
import { pathToFileURL } from 'node:url';
|
|
16
|
-
import { createServer } from 'node:http';
|
|
17
|
-
import { stdin } from 'node:process';
|
|
18
|
-
import c from 'ansi-colors';
|
|
19
|
-
import 'tej-env';
|
|
20
|
-
import { loadConfigFile } from '../utils/configuration.js';
|
|
21
|
-
import { findTargetFiles } from '../utils/auto-register.js';
|
|
22
|
-
import targetRegistry from '../server/targets/registry.js';
|
|
23
|
-
import { generateDocs } from '../auto-docs/index.js';
|
|
24
|
-
import { buildDocsPage } from '../auto-docs/ui/docs-ui.js';
|
|
25
|
-
|
|
26
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
27
|
-
|
|
28
|
-
function mask(value) {
|
|
29
|
-
if (!value || value.length < 8) return value;
|
|
30
|
-
return value.slice(0, 4) + '••••' + value.slice(-4);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function ask(question, fallback = '') {
|
|
34
|
-
const hint = fallback ? c.dim(` (${fallback})`) : '';
|
|
35
|
-
return new Promise((resolve) => {
|
|
36
|
-
rl.question(`${c.cyan('?')} ${question}${hint}${c.dim(': ')}`, (answer) => {
|
|
37
|
-
resolve(answer.trim() || fallback);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function askYesNo(question, fallback = false) {
|
|
43
|
-
const hint = fallback ? 'Y/n' : 'y/N';
|
|
44
|
-
return new Promise((resolve) => {
|
|
45
|
-
rl.question(
|
|
46
|
-
`${c.cyan('?')} ${question} ${c.dim(`(${hint})`)}${c.dim(': ')}`,
|
|
47
|
-
(answer) => {
|
|
48
|
-
const val = answer.trim().toLowerCase();
|
|
49
|
-
if (!val) return resolve(fallback);
|
|
50
|
-
resolve(val === 'y' || val === 'yes');
|
|
51
|
-
},
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function loadTargetFiles(dirTargets = 'targets') {
|
|
57
|
-
if (!process.env.DIR_TARGETS) {
|
|
58
|
-
process.env.DIR_TARGETS = dirTargets;
|
|
59
|
-
}
|
|
60
|
-
const targetFiles = await findTargetFiles();
|
|
61
|
-
if (!targetFiles || targetFiles.length === 0) return 0;
|
|
62
|
-
|
|
63
|
-
const baseDir = path.join(process.cwd(), process.env.DIR_TARGETS);
|
|
64
|
-
for (const file of targetFiles) {
|
|
65
|
-
const parentPath = file.path || '';
|
|
66
|
-
const fullPath = path.isAbsolute(parentPath)
|
|
67
|
-
? path.join(parentPath, file.name)
|
|
68
|
-
: path.join(baseDir, parentPath, file.name);
|
|
69
|
-
const relativePath = path.relative(baseDir, fullPath);
|
|
70
|
-
const groupId =
|
|
71
|
-
relativePath.replace(/\.target\.js$/i, '').replace(/\\/g, '/') || 'index';
|
|
72
|
-
targetRegistry.setCurrentSourceGroup(groupId);
|
|
73
|
-
try {
|
|
74
|
-
await import(pathToFileURL(fullPath).href);
|
|
75
|
-
} finally {
|
|
76
|
-
targetRegistry.setCurrentSourceGroup(null);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return targetFiles.length;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Build options for generateDocs from tejas.config.json "docs" and env.
|
|
84
|
-
* Used by --ci and docs:on-push.
|
|
85
|
-
* @param {object} [config] - Full config from loadConfigFile()
|
|
86
|
-
* @returns {{ dirTargets: string, output: string, info: object, llm: object, level: number, overviewPath?: string } | null} null if LLM not configured
|
|
87
|
-
*/
|
|
88
|
-
function getDocsOptionsFromConfig(config = {}) {
|
|
89
|
-
const docs = config.docs || config.generateDocs || {};
|
|
90
|
-
const e = process.env;
|
|
91
|
-
const baseURL = docs.llm?.baseURL ?? e.LLM_BASE_URL ?? 'https://api.openai.com/v1';
|
|
92
|
-
const apiKey = docs.llm?.apiKey ?? e.LLM_API_KEY ?? e.OPENAI_API_KEY;
|
|
93
|
-
const model = docs.llm?.model ?? e.LLM_MODEL ?? 'gpt-4o-mini';
|
|
94
|
-
if (!apiKey && !e.OPENAI_API_KEY) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
const dirTargets = docs.dirTargets ?? docs.dir?.targets ?? config.dir?.targets ?? 'targets';
|
|
98
|
-
const output = docs.output ?? './openapi.json';
|
|
99
|
-
const title = docs.title ?? 'API';
|
|
100
|
-
const version = docs.version ?? '1.0.0';
|
|
101
|
-
const description = docs.description ?? '';
|
|
102
|
-
const level = Math.max(1, Math.min(3, parseInt(docs.level, 10) || 1));
|
|
103
|
-
const info = { title, version };
|
|
104
|
-
if (description) info.description = description;
|
|
105
|
-
const llm = { baseURL, apiKey: apiKey || e.OPENAI_API_KEY, model };
|
|
106
|
-
const overviewPath =
|
|
107
|
-
level === 3
|
|
108
|
-
? docs.overviewPath ?? path.join(path.dirname(output), 'API_OVERVIEW.md')
|
|
109
|
-
: undefined;
|
|
110
|
-
return { dirTargets, output, info, llm, level, overviewPath };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Run documentation generation in non-interactive (CI) mode.
|
|
115
|
-
* Uses tejas.config.json "docs" and env; exits with message if LLM not configured.
|
|
116
|
-
* @returns {Promise<void>}
|
|
117
|
-
*/
|
|
118
|
-
export async function runDocsCommandCI() {
|
|
119
|
-
const config = loadConfigFile();
|
|
120
|
-
const options = getDocsOptionsFromConfig(config);
|
|
121
|
-
if (!options) {
|
|
122
|
-
console.error(
|
|
123
|
-
'Documentation generation in CI mode requires LLM credentials. Set LLM_API_KEY (or OPENAI_API_KEY) and optionally LLM_BASE_URL, LLM_MODEL, or add a "docs" section to tejas.config.json.',
|
|
124
|
-
);
|
|
125
|
-
process.exit(1);
|
|
126
|
-
}
|
|
127
|
-
const { dirTargets, output, info, llm, level, overviewPath } = options;
|
|
128
|
-
process.stdout.write(`${c.yellow('⏳')} Loading targets...`);
|
|
129
|
-
const fileCount = await loadTargetFiles(dirTargets);
|
|
130
|
-
const endpointCount = targetRegistry.targets?.length ?? 0;
|
|
131
|
-
process.stdout.write(`\r${c.green('✓')} Loaded ${c.bold(fileCount)} target file(s) — ${c.bold(endpointCount)} endpoint(s)\n`);
|
|
132
|
-
if (endpointCount === 0) {
|
|
133
|
-
console.log(c.yellow(' No endpoints found. Skipping doc generation.\n'));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
process.stdout.write(`${c.yellow('⏳')} Generating OpenAPI spec...`);
|
|
137
|
-
const spec = await generateDocs(targetRegistry, {
|
|
138
|
-
outputPath: output,
|
|
139
|
-
llm,
|
|
140
|
-
info,
|
|
141
|
-
level,
|
|
142
|
-
dirTargets,
|
|
143
|
-
verbose: false,
|
|
144
|
-
...(level === 3 && overviewPath && { overviewPath }),
|
|
145
|
-
});
|
|
146
|
-
const pathCount = spec.paths ? Object.keys(spec.paths).length : 0;
|
|
147
|
-
const tagCount = spec.tags?.length ?? 0;
|
|
148
|
-
process.stdout.write(
|
|
149
|
-
`\r${c.green('✓')} OpenAPI spec written to ${c.bold(path.resolve(output))} (${pathCount} paths, ${tagCount} tags)\n`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Read git pre-push hook stdin and return the list of remote refs being pushed.
|
|
155
|
-
* Format: first line "<remote> <url>", then for each ref "<local_ref> <local_sha> <remote_ref> <remote_sha>"
|
|
156
|
-
* @returns {Promise<string[]>} e.g. ['refs/heads/main', 'refs/heads/feature/x']
|
|
157
|
-
*/
|
|
158
|
-
function readPrePushRefs() {
|
|
159
|
-
return new Promise((resolve) => {
|
|
160
|
-
const chunks = [];
|
|
161
|
-
stdin.on('data', (chunk) => chunks.push(chunk));
|
|
162
|
-
stdin.on('end', () => {
|
|
163
|
-
const lines = Buffer.concat(chunks).toString('utf8').trim().split('\n');
|
|
164
|
-
const refs = [];
|
|
165
|
-
for (let i = 0; i < lines.length; i++) {
|
|
166
|
-
const parts = lines[i].split(/\s+/);
|
|
167
|
-
if (parts.length >= 4) refs.push(parts[2]); // remote_ref
|
|
168
|
-
}
|
|
169
|
-
resolve(refs);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Run doc generation when pushing to the configured production branch.
|
|
176
|
-
* Intended for use in a git pre-push hook. Reads refs from stdin; if any ref
|
|
177
|
-
* is the production branch (e.g. refs/heads/main), runs generate:docs in CI mode.
|
|
178
|
-
* Configure via tejas.config.json docs.productionBranch or env DOCS_PRODUCTION_BRANCH (default: main).
|
|
179
|
-
*/
|
|
180
|
-
export async function runDocsOnPush() {
|
|
181
|
-
const config = loadConfigFile();
|
|
182
|
-
const docs = config.docs || config.generateDocs || {};
|
|
183
|
-
const productionBranch =
|
|
184
|
-
docs.productionBranch ?? process.env.DOCS_PRODUCTION_BRANCH ?? 'main';
|
|
185
|
-
const remoteRefs = await readPrePushRefs();
|
|
186
|
-
const productionRef = `refs/heads/${productionBranch}`;
|
|
187
|
-
const isPushingToProduction = remoteRefs.some((ref) => ref === productionRef);
|
|
188
|
-
if (!isPushingToProduction) return;
|
|
189
|
-
console.log(c.dim(` Docs: pushing to ${productionBranch} — generating documentation...\n`));
|
|
190
|
-
await runDocsCommandCI();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function serveDocsPreview(spec, port = 3333) {
|
|
194
|
-
const html = buildDocsPage('/docs/openapi.json');
|
|
195
|
-
const server = createServer(async (req, res) => {
|
|
196
|
-
const url = req.url?.split('?')[0] || '';
|
|
197
|
-
if (url === '/docs' || url === '/docs/') {
|
|
198
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
199
|
-
res.end(html);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (url === '/docs/openapi.json') {
|
|
203
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
204
|
-
res.end(JSON.stringify(spec));
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
res.writeHead(404);
|
|
208
|
-
res.end('Not found');
|
|
209
|
-
});
|
|
210
|
-
return new Promise((resolve) => {
|
|
211
|
-
server.listen(port, () => resolve(server));
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export async function runDocsCommand() {
|
|
216
|
-
const config = loadConfigFile();
|
|
217
|
-
const e = process.env;
|
|
218
|
-
|
|
219
|
-
console.log();
|
|
220
|
-
console.log(c.bold(' te.js Documentation Generator'));
|
|
221
|
-
console.log(c.dim(' ─────────────────────────────────'));
|
|
222
|
-
console.log();
|
|
223
|
-
|
|
224
|
-
const dirTargets = await ask(
|
|
225
|
-
'Targets directory',
|
|
226
|
-
config?.dir?.targets || 'targets',
|
|
227
|
-
);
|
|
228
|
-
const output = await ask('Output file', './openapi.json');
|
|
229
|
-
const title = await ask('API title', 'API');
|
|
230
|
-
const version = await ask('API version', '1.0.0');
|
|
231
|
-
const description = await ask('API description', '');
|
|
232
|
-
|
|
233
|
-
console.log();
|
|
234
|
-
console.log(c.dim(' Documentation is generated using an LLM (required).'));
|
|
235
|
-
const envBaseURL = e.LLM_BASE_URL || '';
|
|
236
|
-
const envKey = e.LLM_API_KEY || '';
|
|
237
|
-
const envModel = e.LLM_MODEL || '';
|
|
238
|
-
const baseURL = await ask(
|
|
239
|
-
'LLM provider base URL',
|
|
240
|
-
envBaseURL || 'https://api.openai.com/v1',
|
|
241
|
-
);
|
|
242
|
-
const apiKey = await ask('API key', envKey ? mask(envKey) : '');
|
|
243
|
-
const resolvedKey = apiKey === mask(envKey) ? envKey : apiKey;
|
|
244
|
-
const model = await ask('Model', envModel || 'gpt-4o-mini');
|
|
245
|
-
const llm = { baseURL, apiKey: resolvedKey, model };
|
|
246
|
-
|
|
247
|
-
console.log();
|
|
248
|
-
const levelAnswer = await ask(
|
|
249
|
-
'Token usage level (higher = better, more comprehensive documentation)\n 1. Moderate — handler code only (~few hundred tokens per endpoint)\n 2. High — handler + full dependency chain (~thousands per endpoint)\n 3. Comprehensive — same as 2, then reorder API groups by importance + generate project/API overview page\n Choose level',
|
|
250
|
-
'1',
|
|
251
|
-
);
|
|
252
|
-
const level = Math.max(1, Math.min(3, parseInt(levelAnswer, 10) || 1));
|
|
253
|
-
|
|
254
|
-
let overviewPath = '';
|
|
255
|
-
if (level === 3) {
|
|
256
|
-
const defaultOverview = path.join(path.dirname(output), 'API_OVERVIEW.md');
|
|
257
|
-
overviewPath = await ask('Overview page path', defaultOverview);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const serve = await askYesNo('Preview docs after generation?');
|
|
261
|
-
console.log();
|
|
262
|
-
|
|
263
|
-
// --- Load targets ---
|
|
264
|
-
process.stdout.write(`${c.yellow('⏳')} Loading targets...`);
|
|
265
|
-
const fileCount = await loadTargetFiles(dirTargets);
|
|
266
|
-
const endpointCount = targetRegistry.targets?.length ?? 0;
|
|
267
|
-
process.stdout.write(
|
|
268
|
-
`\r${c.green('✓')} Loaded ${c.bold(fileCount)} target file(s) — ${c.bold(endpointCount)} endpoint(s)\n`,
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
if (endpointCount === 0) {
|
|
272
|
-
console.log(
|
|
273
|
-
c.yellow(
|
|
274
|
-
'\n No endpoints found. Make sure your target files are in the correct directory.\n',
|
|
275
|
-
),
|
|
276
|
-
);
|
|
277
|
-
rl.close();
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// --- Generate spec ---
|
|
282
|
-
process.stdout.write(`${c.yellow('⏳')} Generating OpenAPI spec...`);
|
|
283
|
-
|
|
284
|
-
const info = { title, version };
|
|
285
|
-
if (description) info.description = description;
|
|
286
|
-
|
|
287
|
-
const spec = await generateDocs(targetRegistry, {
|
|
288
|
-
outputPath: output,
|
|
289
|
-
llm,
|
|
290
|
-
info,
|
|
291
|
-
level,
|
|
292
|
-
dirTargets,
|
|
293
|
-
verbose: true,
|
|
294
|
-
...(level === 3 && overviewPath && { overviewPath }),
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
const pathCount = spec.paths ? Object.keys(spec.paths).length : 0;
|
|
298
|
-
const tagCount = spec.tags?.length ?? 0;
|
|
299
|
-
process.stdout.write(
|
|
300
|
-
`\r${c.green('✓')} OpenAPI spec written to ${c.bold(path.resolve(output))} (${pathCount} paths, ${tagCount} tags)\n`,
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
// --- Optional preview ---
|
|
304
|
-
if (serve) {
|
|
305
|
-
const port = Number(process.env.DOCS_PORT) || 3333;
|
|
306
|
-
await serveDocsPreview(spec, port);
|
|
307
|
-
console.log(
|
|
308
|
-
`${c.green('✓')} Docs preview: ${c.underline(`http://localhost:${port}/docs`)}`,
|
|
309
|
-
);
|
|
310
|
-
console.log(c.dim(' Press Ctrl+C to stop.\n'));
|
|
311
|
-
} else {
|
|
312
|
-
console.log();
|
|
313
|
-
rl.close();
|
|
314
|
-
}
|
|
315
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `tejas generate:docs` — interactive documentation generator.
|
|
3
|
+
* Walks the developer through configuration, loads targets, generates an OpenAPI spec,
|
|
4
|
+
* and optionally serves a live preview.
|
|
5
|
+
*
|
|
6
|
+
* Non-interactive (CI/hooks): use `tejas generate:docs --ci` with options from
|
|
7
|
+
* tejas.config.json "docs" and env (LLM_API_KEY, LLM_BASE_URL, LLM_MODEL, etc.).
|
|
8
|
+
*
|
|
9
|
+
* Trigger on push to production: add `tejas docs:on-push` to your pre-push hook;
|
|
10
|
+
* configure production branch via config.docs.productionBranch or DOCS_PRODUCTION_BRANCH.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { createInterface } from 'node:readline';
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
16
|
+
import { createServer } from 'node:http';
|
|
17
|
+
import { stdin } from 'node:process';
|
|
18
|
+
import c from 'ansi-colors';
|
|
19
|
+
import 'tej-env';
|
|
20
|
+
import { loadConfigFile } from '../utils/configuration.js';
|
|
21
|
+
import { findTargetFiles } from '../utils/auto-register.js';
|
|
22
|
+
import targetRegistry from '../server/targets/registry.js';
|
|
23
|
+
import { generateDocs } from '../auto-docs/index.js';
|
|
24
|
+
import { buildDocsPage } from '../auto-docs/ui/docs-ui.js';
|
|
25
|
+
|
|
26
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
27
|
+
|
|
28
|
+
function mask(value) {
|
|
29
|
+
if (!value || value.length < 8) return value;
|
|
30
|
+
return value.slice(0, 4) + '••••' + value.slice(-4);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ask(question, fallback = '') {
|
|
34
|
+
const hint = fallback ? c.dim(` (${fallback})`) : '';
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
rl.question(`${c.cyan('?')} ${question}${hint}${c.dim(': ')}`, (answer) => {
|
|
37
|
+
resolve(answer.trim() || fallback);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function askYesNo(question, fallback = false) {
|
|
43
|
+
const hint = fallback ? 'Y/n' : 'y/N';
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
rl.question(
|
|
46
|
+
`${c.cyan('?')} ${question} ${c.dim(`(${hint})`)}${c.dim(': ')}`,
|
|
47
|
+
(answer) => {
|
|
48
|
+
const val = answer.trim().toLowerCase();
|
|
49
|
+
if (!val) return resolve(fallback);
|
|
50
|
+
resolve(val === 'y' || val === 'yes');
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function loadTargetFiles(dirTargets = 'targets') {
|
|
57
|
+
if (!process.env.DIR_TARGETS) {
|
|
58
|
+
process.env.DIR_TARGETS = dirTargets;
|
|
59
|
+
}
|
|
60
|
+
const targetFiles = await findTargetFiles();
|
|
61
|
+
if (!targetFiles || targetFiles.length === 0) return 0;
|
|
62
|
+
|
|
63
|
+
const baseDir = path.join(process.cwd(), process.env.DIR_TARGETS);
|
|
64
|
+
for (const file of targetFiles) {
|
|
65
|
+
const parentPath = file.path || '';
|
|
66
|
+
const fullPath = path.isAbsolute(parentPath)
|
|
67
|
+
? path.join(parentPath, file.name)
|
|
68
|
+
: path.join(baseDir, parentPath, file.name);
|
|
69
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
70
|
+
const groupId =
|
|
71
|
+
relativePath.replace(/\.target\.js$/i, '').replace(/\\/g, '/') || 'index';
|
|
72
|
+
targetRegistry.setCurrentSourceGroup(groupId);
|
|
73
|
+
try {
|
|
74
|
+
await import(pathToFileURL(fullPath).href);
|
|
75
|
+
} finally {
|
|
76
|
+
targetRegistry.setCurrentSourceGroup(null);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return targetFiles.length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build options for generateDocs from tejas.config.json "docs" and env.
|
|
84
|
+
* Used by --ci and docs:on-push.
|
|
85
|
+
* @param {object} [config] - Full config from loadConfigFile()
|
|
86
|
+
* @returns {{ dirTargets: string, output: string, info: object, llm: object, level: number, overviewPath?: string } | null} null if LLM not configured
|
|
87
|
+
*/
|
|
88
|
+
function getDocsOptionsFromConfig(config = {}) {
|
|
89
|
+
const docs = config.docs || config.generateDocs || {};
|
|
90
|
+
const e = process.env;
|
|
91
|
+
const baseURL = docs.llm?.baseURL ?? e.LLM_BASE_URL ?? 'https://api.openai.com/v1';
|
|
92
|
+
const apiKey = docs.llm?.apiKey ?? e.LLM_API_KEY ?? e.OPENAI_API_KEY;
|
|
93
|
+
const model = docs.llm?.model ?? e.LLM_MODEL ?? 'gpt-4o-mini';
|
|
94
|
+
if (!apiKey && !e.OPENAI_API_KEY) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const dirTargets = docs.dirTargets ?? docs.dir?.targets ?? config.dir?.targets ?? 'targets';
|
|
98
|
+
const output = docs.output ?? './openapi.json';
|
|
99
|
+
const title = docs.title ?? 'API';
|
|
100
|
+
const version = docs.version ?? '1.0.0';
|
|
101
|
+
const description = docs.description ?? '';
|
|
102
|
+
const level = Math.max(1, Math.min(3, parseInt(docs.level, 10) || 1));
|
|
103
|
+
const info = { title, version };
|
|
104
|
+
if (description) info.description = description;
|
|
105
|
+
const llm = { baseURL, apiKey: apiKey || e.OPENAI_API_KEY, model };
|
|
106
|
+
const overviewPath =
|
|
107
|
+
level === 3
|
|
108
|
+
? docs.overviewPath ?? path.join(path.dirname(output), 'API_OVERVIEW.md')
|
|
109
|
+
: undefined;
|
|
110
|
+
return { dirTargets, output, info, llm, level, overviewPath };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Run documentation generation in non-interactive (CI) mode.
|
|
115
|
+
* Uses tejas.config.json "docs" and env; exits with message if LLM not configured.
|
|
116
|
+
* @returns {Promise<void>}
|
|
117
|
+
*/
|
|
118
|
+
export async function runDocsCommandCI() {
|
|
119
|
+
const config = loadConfigFile();
|
|
120
|
+
const options = getDocsOptionsFromConfig(config);
|
|
121
|
+
if (!options) {
|
|
122
|
+
console.error(
|
|
123
|
+
'Documentation generation in CI mode requires LLM credentials. Set LLM_API_KEY (or OPENAI_API_KEY) and optionally LLM_BASE_URL, LLM_MODEL, or add a "docs" section to tejas.config.json.',
|
|
124
|
+
);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const { dirTargets, output, info, llm, level, overviewPath } = options;
|
|
128
|
+
process.stdout.write(`${c.yellow('⏳')} Loading targets...`);
|
|
129
|
+
const fileCount = await loadTargetFiles(dirTargets);
|
|
130
|
+
const endpointCount = targetRegistry.targets?.length ?? 0;
|
|
131
|
+
process.stdout.write(`\r${c.green('✓')} Loaded ${c.bold(fileCount)} target file(s) — ${c.bold(endpointCount)} endpoint(s)\n`);
|
|
132
|
+
if (endpointCount === 0) {
|
|
133
|
+
console.log(c.yellow(' No endpoints found. Skipping doc generation.\n'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
process.stdout.write(`${c.yellow('⏳')} Generating OpenAPI spec...`);
|
|
137
|
+
const spec = await generateDocs(targetRegistry, {
|
|
138
|
+
outputPath: output,
|
|
139
|
+
llm,
|
|
140
|
+
info,
|
|
141
|
+
level,
|
|
142
|
+
dirTargets,
|
|
143
|
+
verbose: false,
|
|
144
|
+
...(level === 3 && overviewPath && { overviewPath }),
|
|
145
|
+
});
|
|
146
|
+
const pathCount = spec.paths ? Object.keys(spec.paths).length : 0;
|
|
147
|
+
const tagCount = spec.tags?.length ?? 0;
|
|
148
|
+
process.stdout.write(
|
|
149
|
+
`\r${c.green('✓')} OpenAPI spec written to ${c.bold(path.resolve(output))} (${pathCount} paths, ${tagCount} tags)\n`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Read git pre-push hook stdin and return the list of remote refs being pushed.
|
|
155
|
+
* Format: first line "<remote> <url>", then for each ref "<local_ref> <local_sha> <remote_ref> <remote_sha>"
|
|
156
|
+
* @returns {Promise<string[]>} e.g. ['refs/heads/main', 'refs/heads/feature/x']
|
|
157
|
+
*/
|
|
158
|
+
function readPrePushRefs() {
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
const chunks = [];
|
|
161
|
+
stdin.on('data', (chunk) => chunks.push(chunk));
|
|
162
|
+
stdin.on('end', () => {
|
|
163
|
+
const lines = Buffer.concat(chunks).toString('utf8').trim().split('\n');
|
|
164
|
+
const refs = [];
|
|
165
|
+
for (let i = 0; i < lines.length; i++) {
|
|
166
|
+
const parts = lines[i].split(/\s+/);
|
|
167
|
+
if (parts.length >= 4) refs.push(parts[2]); // remote_ref
|
|
168
|
+
}
|
|
169
|
+
resolve(refs);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Run doc generation when pushing to the configured production branch.
|
|
176
|
+
* Intended for use in a git pre-push hook. Reads refs from stdin; if any ref
|
|
177
|
+
* is the production branch (e.g. refs/heads/main), runs generate:docs in CI mode.
|
|
178
|
+
* Configure via tejas.config.json docs.productionBranch or env DOCS_PRODUCTION_BRANCH (default: main).
|
|
179
|
+
*/
|
|
180
|
+
export async function runDocsOnPush() {
|
|
181
|
+
const config = loadConfigFile();
|
|
182
|
+
const docs = config.docs || config.generateDocs || {};
|
|
183
|
+
const productionBranch =
|
|
184
|
+
docs.productionBranch ?? process.env.DOCS_PRODUCTION_BRANCH ?? 'main';
|
|
185
|
+
const remoteRefs = await readPrePushRefs();
|
|
186
|
+
const productionRef = `refs/heads/${productionBranch}`;
|
|
187
|
+
const isPushingToProduction = remoteRefs.some((ref) => ref === productionRef);
|
|
188
|
+
if (!isPushingToProduction) return;
|
|
189
|
+
console.log(c.dim(` Docs: pushing to ${productionBranch} — generating documentation...\n`));
|
|
190
|
+
await runDocsCommandCI();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function serveDocsPreview(spec, port = 3333) {
|
|
194
|
+
const html = buildDocsPage('/docs/openapi.json');
|
|
195
|
+
const server = createServer(async (req, res) => {
|
|
196
|
+
const url = req.url?.split('?')[0] || '';
|
|
197
|
+
if (url === '/docs' || url === '/docs/') {
|
|
198
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
199
|
+
res.end(html);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (url === '/docs/openapi.json') {
|
|
203
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
204
|
+
res.end(JSON.stringify(spec));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
res.writeHead(404);
|
|
208
|
+
res.end('Not found');
|
|
209
|
+
});
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
server.listen(port, () => resolve(server));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function runDocsCommand() {
|
|
216
|
+
const config = loadConfigFile();
|
|
217
|
+
const e = process.env;
|
|
218
|
+
|
|
219
|
+
console.log();
|
|
220
|
+
console.log(c.bold(' te.js Documentation Generator'));
|
|
221
|
+
console.log(c.dim(' ─────────────────────────────────'));
|
|
222
|
+
console.log();
|
|
223
|
+
|
|
224
|
+
const dirTargets = await ask(
|
|
225
|
+
'Targets directory',
|
|
226
|
+
config?.dir?.targets || 'targets',
|
|
227
|
+
);
|
|
228
|
+
const output = await ask('Output file', './openapi.json');
|
|
229
|
+
const title = await ask('API title', 'API');
|
|
230
|
+
const version = await ask('API version', '1.0.0');
|
|
231
|
+
const description = await ask('API description', '');
|
|
232
|
+
|
|
233
|
+
console.log();
|
|
234
|
+
console.log(c.dim(' Documentation is generated using an LLM (required).'));
|
|
235
|
+
const envBaseURL = e.LLM_BASE_URL || '';
|
|
236
|
+
const envKey = e.LLM_API_KEY || '';
|
|
237
|
+
const envModel = e.LLM_MODEL || '';
|
|
238
|
+
const baseURL = await ask(
|
|
239
|
+
'LLM provider base URL',
|
|
240
|
+
envBaseURL || 'https://api.openai.com/v1',
|
|
241
|
+
);
|
|
242
|
+
const apiKey = await ask('API key', envKey ? mask(envKey) : '');
|
|
243
|
+
const resolvedKey = apiKey === mask(envKey) ? envKey : apiKey;
|
|
244
|
+
const model = await ask('Model', envModel || 'gpt-4o-mini');
|
|
245
|
+
const llm = { baseURL, apiKey: resolvedKey, model };
|
|
246
|
+
|
|
247
|
+
console.log();
|
|
248
|
+
const levelAnswer = await ask(
|
|
249
|
+
'Token usage level (higher = better, more comprehensive documentation)\n 1. Moderate — handler code only (~few hundred tokens per endpoint)\n 2. High — handler + full dependency chain (~thousands per endpoint)\n 3. Comprehensive — same as 2, then reorder API groups by importance + generate project/API overview page\n Choose level',
|
|
250
|
+
'1',
|
|
251
|
+
);
|
|
252
|
+
const level = Math.max(1, Math.min(3, parseInt(levelAnswer, 10) || 1));
|
|
253
|
+
|
|
254
|
+
let overviewPath = '';
|
|
255
|
+
if (level === 3) {
|
|
256
|
+
const defaultOverview = path.join(path.dirname(output), 'API_OVERVIEW.md');
|
|
257
|
+
overviewPath = await ask('Overview page path', defaultOverview);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const serve = await askYesNo('Preview docs after generation?');
|
|
261
|
+
console.log();
|
|
262
|
+
|
|
263
|
+
// --- Load targets ---
|
|
264
|
+
process.stdout.write(`${c.yellow('⏳')} Loading targets...`);
|
|
265
|
+
const fileCount = await loadTargetFiles(dirTargets);
|
|
266
|
+
const endpointCount = targetRegistry.targets?.length ?? 0;
|
|
267
|
+
process.stdout.write(
|
|
268
|
+
`\r${c.green('✓')} Loaded ${c.bold(fileCount)} target file(s) — ${c.bold(endpointCount)} endpoint(s)\n`,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (endpointCount === 0) {
|
|
272
|
+
console.log(
|
|
273
|
+
c.yellow(
|
|
274
|
+
'\n No endpoints found. Make sure your target files are in the correct directory.\n',
|
|
275
|
+
),
|
|
276
|
+
);
|
|
277
|
+
rl.close();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// --- Generate spec ---
|
|
282
|
+
process.stdout.write(`${c.yellow('⏳')} Generating OpenAPI spec...`);
|
|
283
|
+
|
|
284
|
+
const info = { title, version };
|
|
285
|
+
if (description) info.description = description;
|
|
286
|
+
|
|
287
|
+
const spec = await generateDocs(targetRegistry, {
|
|
288
|
+
outputPath: output,
|
|
289
|
+
llm,
|
|
290
|
+
info,
|
|
291
|
+
level,
|
|
292
|
+
dirTargets,
|
|
293
|
+
verbose: true,
|
|
294
|
+
...(level === 3 && overviewPath && { overviewPath }),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const pathCount = spec.paths ? Object.keys(spec.paths).length : 0;
|
|
298
|
+
const tagCount = spec.tags?.length ?? 0;
|
|
299
|
+
process.stdout.write(
|
|
300
|
+
`\r${c.green('✓')} OpenAPI spec written to ${c.bold(path.resolve(output))} (${pathCount} paths, ${tagCount} tags)\n`,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// --- Optional preview ---
|
|
304
|
+
if (serve) {
|
|
305
|
+
const port = Number(process.env.DOCS_PORT) || 3333;
|
|
306
|
+
await serveDocsPreview(spec, port);
|
|
307
|
+
console.log(
|
|
308
|
+
`${c.green('✓')} Docs preview: ${c.underline(`http://localhost:${port}/docs`)}`,
|
|
309
|
+
);
|
|
310
|
+
console.log(c.dim(' Press Ctrl+C to stop.\n'));
|
|
311
|
+
} else {
|
|
312
|
+
console.log();
|
|
313
|
+
rl.close();
|
|
314
|
+
}
|
|
315
|
+
}
|