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.
Files changed (70) hide show
  1. package/README.md +197 -196
  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 -7
  6. package/auto-docs/docs-llm/prompts.js +222 -222
  7. package/auto-docs/docs-llm/provider.js +132 -132
  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/cors/index.js +71 -0
  20. package/database/index.js +165 -165
  21. package/database/mongodb.js +146 -146
  22. package/database/redis.js +201 -201
  23. package/docs/README.md +36 -36
  24. package/docs/ammo.md +362 -362
  25. package/docs/api-reference.md +490 -490
  26. package/docs/auto-docs.md +216 -216
  27. package/docs/cli.md +152 -152
  28. package/docs/configuration.md +275 -275
  29. package/docs/database.md +390 -390
  30. package/docs/error-handling.md +438 -438
  31. package/docs/file-uploads.md +333 -333
  32. package/docs/getting-started.md +214 -214
  33. package/docs/middleware.md +355 -355
  34. package/docs/rate-limiting.md +393 -393
  35. package/docs/routing.md +302 -302
  36. package/lib/llm/client.js +73 -0
  37. package/lib/llm/index.js +7 -0
  38. package/lib/llm/parse.js +89 -0
  39. package/package.json +64 -62
  40. package/rate-limit/algorithms/fixed-window.js +141 -141
  41. package/rate-limit/algorithms/sliding-window.js +147 -147
  42. package/rate-limit/algorithms/token-bucket.js +115 -115
  43. package/rate-limit/base.js +165 -165
  44. package/rate-limit/index.js +147 -147
  45. package/rate-limit/storage/base.js +104 -104
  46. package/rate-limit/storage/memory.js +101 -101
  47. package/rate-limit/storage/redis.js +88 -88
  48. package/server/ammo/body-parser.js +220 -220
  49. package/server/ammo/dispatch-helper.js +103 -103
  50. package/server/ammo/enhancer.js +57 -57
  51. package/server/ammo.js +454 -415
  52. package/server/endpoint.js +97 -74
  53. package/server/error.js +9 -9
  54. package/server/errors/code-context.js +125 -125
  55. package/server/errors/llm-error-service.js +140 -140
  56. package/server/files/helper.js +33 -33
  57. package/server/files/uploader.js +143 -143
  58. package/server/handler.js +158 -119
  59. package/server/target.js +185 -175
  60. package/server/targets/middleware-validator.js +22 -22
  61. package/server/targets/path-validator.js +21 -21
  62. package/server/targets/registry.js +160 -160
  63. package/server/targets/shoot-validator.js +21 -21
  64. package/te.js +428 -402
  65. package/utils/auto-register.js +17 -17
  66. package/utils/configuration.js +64 -64
  67. package/utils/errors-llm-config.js +84 -84
  68. package/utils/request-logger.js +43 -43
  69. package/utils/status-codes.js +82 -82
  70. package/utils/tejas-entrypoint-html.js +18 -18
@@ -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
+ }