te.js 2.1.0 → 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 (66) 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/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 -490
  25. package/docs/auto-docs.md +216 -216
  26. package/docs/cli.md +152 -152
  27. package/docs/configuration.md +275 -275
  28. package/docs/database.md +390 -390
  29. package/docs/error-handling.md +438 -438
  30. package/docs/file-uploads.md +333 -333
  31. package/docs/getting-started.md +214 -214
  32. package/docs/middleware.md +355 -355
  33. package/docs/rate-limiting.md +393 -393
  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 -415
  48. package/server/endpoint.js +97 -74
  49. package/server/error.js +9 -9
  50. package/server/errors/code-context.js +125 -125
  51. package/server/errors/llm-error-service.js +140 -140
  52. package/server/files/helper.js +33 -33
  53. package/server/files/uploader.js +143 -143
  54. package/server/handler.js +158 -119
  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 -402
  61. package/utils/auto-register.js +17 -17
  62. package/utils/configuration.js +64 -64
  63. package/utils/errors-llm-config.js +84 -84
  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
@@ -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
+ }