te.js 2.0.3 → 2.1.1

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.
Files changed (68) hide show
  1. package/README.md +197 -187
  2. package/auto-docs/analysis/handler-analyzer.js +58 -58
  3. package/auto-docs/analysis/source-resolver.js +101 -101
  4. package/auto-docs/constants.js +37 -37
  5. package/auto-docs/docs-llm/index.js +7 -0
  6. package/auto-docs/{llm → docs-llm}/prompts.js +222 -222
  7. package/auto-docs/{llm → docs-llm}/provider.js +132 -187
  8. package/auto-docs/index.js +146 -146
  9. package/auto-docs/openapi/endpoint-processor.js +277 -277
  10. package/auto-docs/openapi/generator.js +107 -107
  11. package/auto-docs/openapi/level3.js +131 -131
  12. package/auto-docs/openapi/spec-builders.js +244 -244
  13. package/auto-docs/ui/docs-ui.js +186 -186
  14. package/auto-docs/utils/logger.js +17 -17
  15. package/auto-docs/utils/strip-usage.js +10 -10
  16. package/cli/docs-command.js +315 -315
  17. package/cli/fly-command.js +71 -71
  18. package/cli/index.js +56 -56
  19. package/database/index.js +165 -165
  20. package/database/mongodb.js +146 -146
  21. package/database/redis.js +201 -201
  22. package/docs/README.md +36 -36
  23. package/docs/ammo.md +362 -362
  24. package/docs/api-reference.md +490 -489
  25. package/docs/auto-docs.md +216 -215
  26. package/docs/cli.md +152 -152
  27. package/docs/configuration.md +275 -233
  28. package/docs/database.md +390 -391
  29. package/docs/error-handling.md +438 -417
  30. package/docs/file-uploads.md +333 -334
  31. package/docs/getting-started.md +214 -215
  32. package/docs/middleware.md +355 -356
  33. package/docs/rate-limiting.md +393 -394
  34. package/docs/routing.md +302 -302
  35. package/package.json +62 -62
  36. package/rate-limit/algorithms/fixed-window.js +141 -141
  37. package/rate-limit/algorithms/sliding-window.js +147 -147
  38. package/rate-limit/algorithms/token-bucket.js +115 -115
  39. package/rate-limit/base.js +165 -165
  40. package/rate-limit/index.js +147 -147
  41. package/rate-limit/storage/base.js +104 -104
  42. package/rate-limit/storage/memory.js +101 -101
  43. package/rate-limit/storage/redis.js +88 -88
  44. package/server/ammo/body-parser.js +220 -220
  45. package/server/ammo/dispatch-helper.js +103 -103
  46. package/server/ammo/enhancer.js +57 -57
  47. package/server/ammo.js +454 -356
  48. package/server/endpoint.js +97 -74
  49. package/server/error.js +9 -9
  50. package/server/errors/code-context.js +125 -0
  51. package/server/errors/llm-error-service.js +140 -0
  52. package/server/files/helper.js +33 -33
  53. package/server/files/uploader.js +143 -143
  54. package/server/handler.js +158 -113
  55. package/server/target.js +185 -175
  56. package/server/targets/middleware-validator.js +22 -22
  57. package/server/targets/path-validator.js +21 -21
  58. package/server/targets/registry.js +160 -160
  59. package/server/targets/shoot-validator.js +21 -21
  60. package/te.js +428 -363
  61. package/utils/auto-register.js +17 -17
  62. package/utils/configuration.js +64 -64
  63. package/utils/errors-llm-config.js +84 -0
  64. package/utils/request-logger.js +43 -43
  65. package/utils/status-codes.js +82 -82
  66. package/utils/tejas-entrypoint-html.js +18 -18
  67. package/auto-docs/llm/index.js +0 -6
  68. package/auto-docs/llm/parse.js +0 -88
@@ -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
+ }