te.js 2.1.6 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/auto-docs/analysis/handler-analyzer.test.js +106 -0
  2. package/auto-docs/analysis/source-resolver.test.js +58 -0
  3. package/auto-docs/constants.js +13 -2
  4. package/auto-docs/openapi/generator.js +7 -5
  5. package/auto-docs/openapi/generator.test.js +132 -0
  6. package/auto-docs/openapi/spec-builders.js +39 -19
  7. package/cli/docs-command.js +44 -36
  8. package/cors/index.test.js +82 -0
  9. package/database/index.js +3 -1
  10. package/database/mongodb.js +17 -11
  11. package/database/redis.js +53 -44
  12. package/lib/llm/client.js +6 -1
  13. package/lib/llm/index.js +14 -1
  14. package/lib/llm/parse.test.js +60 -0
  15. package/package.json +3 -1
  16. package/radar/index.js +281 -0
  17. package/rate-limit/index.js +8 -11
  18. package/rate-limit/index.test.js +64 -0
  19. package/server/ammo/body-parser.js +156 -152
  20. package/server/ammo/body-parser.test.js +79 -0
  21. package/server/ammo/enhancer.js +8 -4
  22. package/server/ammo.js +135 -10
  23. package/server/context/request-context.js +51 -0
  24. package/server/context/request-context.test.js +53 -0
  25. package/server/endpoint.js +15 -0
  26. package/server/error.js +56 -3
  27. package/server/error.test.js +45 -0
  28. package/server/errors/channels/channels.test.js +148 -0
  29. package/server/errors/channels/index.js +1 -1
  30. package/server/errors/llm-cache.js +1 -1
  31. package/server/errors/llm-cache.test.js +160 -0
  32. package/server/errors/llm-error-service.js +1 -1
  33. package/server/errors/llm-rate-limiter.test.js +105 -0
  34. package/server/files/uploader.js +38 -26
  35. package/server/handler.js +1 -1
  36. package/server/targets/registry.js +3 -3
  37. package/server/targets/registry.test.js +108 -0
  38. package/te.js +178 -49
  39. package/utils/auto-register.js +1 -1
  40. package/utils/configuration.js +23 -9
  41. package/utils/configuration.test.js +58 -0
  42. package/utils/errors-llm-config.js +11 -8
  43. package/utils/request-logger.js +49 -3
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Unit tests for auto-docs handler-analyzer (detectMethods, analyzeHandler).
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import {
6
+ detectMethods,
7
+ analyzeHandler,
8
+ ALL_METHODS,
9
+ } from './handler-analyzer.js';
10
+
11
+ describe('handler-analyzer', () => {
12
+ describe('detectMethods', () => {
13
+ it('returns all methods when handler is not a function', () => {
14
+ expect(detectMethods(null)).toEqual(ALL_METHODS);
15
+ expect(detectMethods(undefined)).toEqual(ALL_METHODS);
16
+ });
17
+
18
+ it('detects GET when handler uses ammo.GET', () => {
19
+ const handler = (ammo) => {
20
+ if (ammo.GET) ammo.fire(200, {});
21
+ };
22
+ expect(detectMethods(handler)).toContain('GET');
23
+ });
24
+
25
+ it('detects POST and GET when handler checks both', () => {
26
+ const handler = (ammo) => {
27
+ if (ammo.GET) ammo.fire(200, {});
28
+ if (ammo.POST) ammo.fire(201, {});
29
+ };
30
+ const detected = detectMethods(handler);
31
+ expect(detected).toContain('GET');
32
+ expect(detected).toContain('POST');
33
+ });
34
+
35
+ it('returns all methods when no method checks found (method-agnostic)', () => {
36
+ const handler = () => {};
37
+ expect(detectMethods(handler)).toEqual(ALL_METHODS);
38
+ });
39
+
40
+ it('detects GET and HEAD when handler uses ammo.only("GET")', () => {
41
+ const handler = (ammo) => {
42
+ ammo.only('GET');
43
+ ammo.fire({ status: 'ok' });
44
+ };
45
+ const detected = detectMethods(handler);
46
+ expect(detected).toContain('GET');
47
+ expect(detected).toContain('HEAD');
48
+ expect(detected).toHaveLength(2);
49
+ });
50
+
51
+ it('detects POST and PUT when handler uses ammo.only("POST", "PUT")', () => {
52
+ const handler = (ammo) => {
53
+ ammo.only('POST', 'PUT');
54
+ ammo.fire(200, {});
55
+ };
56
+ const detected = detectMethods(handler);
57
+ expect(detected).toContain('POST');
58
+ expect(detected).toContain('PUT');
59
+ expect(detected).toHaveLength(2);
60
+ });
61
+
62
+ it('detects from ammo.only with double-quoted methods', () => {
63
+ const handler = (ammo) => {
64
+ ammo.only('GET');
65
+ ammo.fire(200);
66
+ };
67
+ const detected = detectMethods(handler);
68
+ expect(detected).toContain('GET');
69
+ expect(detected).toContain('HEAD');
70
+ });
71
+
72
+ it('detects from .only with no space after comma', () => {
73
+ const handler = (ammo) => {
74
+ ammo.only('GET', 'POST');
75
+ ammo.fire(200);
76
+ };
77
+ const detected = detectMethods(handler);
78
+ expect(detected).toContain('GET');
79
+ expect(detected).toContain('HEAD');
80
+ expect(detected).toContain('POST');
81
+ expect(detected).toHaveLength(3);
82
+ });
83
+
84
+ it('prefers ammo.only over property access when both present', () => {
85
+ const handler = (ammo) => {
86
+ ammo.only('POST');
87
+ if (ammo.GET) ammo.fire(200);
88
+ ammo.fire(201, {});
89
+ };
90
+ const detected = detectMethods(handler);
91
+ expect(detected).toEqual(['POST']);
92
+ });
93
+ });
94
+
95
+ describe('analyzeHandler', () => {
96
+ it('returns object with methods array', () => {
97
+ const handler = (ammo) => {
98
+ if (ammo.GET) ammo.fire(200);
99
+ };
100
+ const result = analyzeHandler(handler);
101
+ expect(result).toHaveProperty('methods');
102
+ expect(Array.isArray(result.methods)).toBe(true);
103
+ expect(result.methods).toContain('GET');
104
+ });
105
+ });
106
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Unit tests for auto-docs source-resolver (extractRelativeImports, resolveTargetFilePath, formatDependencyContext).
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import path from 'node:path';
6
+ import {
7
+ extractRelativeImports,
8
+ resolveTargetFilePath,
9
+ formatDependencyContext,
10
+ } from './source-resolver.js';
11
+
12
+ describe('source-resolver', () => {
13
+ describe('extractRelativeImports', () => {
14
+ it('extracts relative import paths', () => {
15
+ const source = `import x from './foo.js';\nimport { y } from '../bar.js';`;
16
+ const imports = extractRelativeImports(source);
17
+ expect(imports).toContain('./foo.js');
18
+ expect(imports).toContain('../bar.js');
19
+ });
20
+ it('ignores absolute or node imports', () => {
21
+ const source = `import path from 'path';\nimport x from './local.js';`;
22
+ const imports = extractRelativeImports(source);
23
+ expect(imports).toEqual(['./local.js']);
24
+ });
25
+ it('returns unique paths', () => {
26
+ const source = `import a from './foo.js';\nimport b from './foo.js';`;
27
+ expect(extractRelativeImports(source)).toHaveLength(1);
28
+ });
29
+ });
30
+
31
+ describe('resolveTargetFilePath', () => {
32
+ it('joins dirTargets with groupId and .target.js', () => {
33
+ const resolved = resolveTargetFilePath('users', 'targets');
34
+ expect(resolved).toMatch(/targets[\\/]users\.target\.js$/);
35
+ });
36
+ it('converts groupId slashes to path sep', () => {
37
+ const resolved = resolveTargetFilePath('subdir/users', 'targets');
38
+ expect(resolved).toMatch(/subdir[\\/]users\.target\.js$/);
39
+ });
40
+ });
41
+
42
+ describe('formatDependencyContext', () => {
43
+ it('returns empty string for empty sources', () => {
44
+ expect(formatDependencyContext(new Map(), null)).toBe('');
45
+ });
46
+ it('formats map of path -> source with labels', () => {
47
+ const sources = new Map([
48
+ ['/cwd/targets/users.target.js', 'const x = 1;'],
49
+ ['/cwd/services/user.js', 'const y = 2;'],
50
+ ]);
51
+ const targetPath = '/cwd/targets/users.target.js';
52
+ const out = formatDependencyContext(sources, targetPath, 1000);
53
+ expect(out).toContain('Target');
54
+ expect(out).toContain('const x = 1;');
55
+ expect(out).toContain('const y = 2;');
56
+ });
57
+ });
58
+ });
@@ -7,13 +7,24 @@
7
7
  export const OPENAPI_VERSION = '3.0.3';
8
8
 
9
9
  /** Lowercase HTTP method names (for method-keyed LLM response detection and OpenAPI operation keys). */
10
- export const METHOD_KEYS = new Set(['get', 'put', 'post', 'delete', 'patch', 'head', 'options']);
10
+ export const METHOD_KEYS = new Set([
11
+ 'get',
12
+ 'put',
13
+ 'post',
14
+ 'delete',
15
+ 'patch',
16
+ 'head',
17
+ 'options',
18
+ ]);
11
19
 
12
20
  /** When an endpoint accepts all HTTP methods, document it once under this key. */
13
21
  export const METHOD_AGNOSTIC_OPERATION_KEY = 'get';
14
22
 
15
23
  /** Max handler source length by level (chars; tokens roughly scale). Level 1 = moderate, 2 = high. */
16
- export const HANDLER_SOURCE_MAX_LENGTH_BY_LEVEL = { 1: 2800, 2: 6000 };
24
+ export const HANDLER_SOURCE_MAX_LENGTH_BY_LEVEL = Object.freeze({
25
+ 1: 2800,
26
+ 2: 6000,
27
+ });
17
28
 
18
29
  /** Default max chars for dependency context in formatDependencyContext and level-2 prompts. */
19
30
  export const DEPENDENCY_CONTEXT_MAX_CHARS = 6000;
@@ -39,7 +39,7 @@ async function generateOpenAPISpec(registry, options = {}) {
39
39
  logger = null,
40
40
  } = options;
41
41
  const targets = registry?.targets ?? [];
42
- const paths = {};
42
+ const paths = Object.create(null);
43
43
  const groupEndpoints = new Map();
44
44
  const dependencyContextByGroup = new Map();
45
45
 
@@ -71,10 +71,12 @@ async function generateOpenAPISpec(registry, options = {}) {
71
71
  );
72
72
  applyTagDisplayNames(paths, tagDescriptions);
73
73
 
74
- const tags = Array.from(tagDescriptions.entries()).map(([, { name, description }]) => ({
75
- name,
76
- ...(description && { description }),
77
- }));
74
+ const tags = Array.from(tagDescriptions.entries()).map(
75
+ ([, { name, description }]) => ({
76
+ name,
77
+ ...(description && { description }),
78
+ }),
79
+ );
78
80
 
79
81
  const spec = {
80
82
  openapi: OPENAPI_VERSION,
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Unit tests for auto-docs openapi/generator pure functions.
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import {
6
+ toOpenAPIPath,
7
+ getPathParameters,
8
+ getQueryParameters,
9
+ buildSchemaFromMetadata,
10
+ buildResponses,
11
+ buildOperation,
12
+ mergeMetadata,
13
+ } from './generator.js';
14
+
15
+ describe('openapi-generator', () => {
16
+ describe('toOpenAPIPath', () => {
17
+ it('converts :param to {param}', () => {
18
+ expect(toOpenAPIPath('/users/:id')).toBe('/users/{id}');
19
+ });
20
+ it('converts multiple params', () => {
21
+ expect(toOpenAPIPath('/users/:userId/posts/:postId')).toBe(
22
+ '/users/{userId}/posts/{postId}',
23
+ );
24
+ });
25
+ it('returns / for empty or non-string', () => {
26
+ expect(toOpenAPIPath('')).toBe('/');
27
+ expect(toOpenAPIPath(null)).toBe('/');
28
+ });
29
+ });
30
+
31
+ describe('getPathParameters', () => {
32
+ it('extracts path params from te.js path', () => {
33
+ const params = getPathParameters('/users/:id');
34
+ expect(params).toHaveLength(1);
35
+ expect(params[0]).toMatchObject({
36
+ name: 'id',
37
+ in: 'path',
38
+ required: true,
39
+ schema: { type: 'string' },
40
+ });
41
+ });
42
+ it('returns empty array for path without params', () => {
43
+ expect(getPathParameters('/users')).toEqual([]);
44
+ });
45
+ });
46
+
47
+ describe('getQueryParameters', () => {
48
+ it('builds query params from metadata', () => {
49
+ const queryMeta = {
50
+ limit: { type: 'integer', required: false },
51
+ q: { type: 'string', required: true },
52
+ };
53
+ const params = getQueryParameters(queryMeta);
54
+ expect(params).toHaveLength(2);
55
+ expect(params.find((p) => p.name === 'limit')).toMatchObject({
56
+ in: 'query',
57
+ required: false,
58
+ });
59
+ expect(params.find((p) => p.name === 'q')).toMatchObject({
60
+ in: 'query',
61
+ required: true,
62
+ });
63
+ });
64
+ it('returns empty array for invalid meta', () => {
65
+ expect(getQueryParameters(null)).toEqual([]);
66
+ });
67
+ });
68
+
69
+ describe('buildSchemaFromMetadata', () => {
70
+ it('builds OpenAPI schema from field meta', () => {
71
+ const meta = {
72
+ name: { type: 'string' },
73
+ email: { type: 'string', format: 'email', required: true },
74
+ };
75
+ const schema = buildSchemaFromMetadata(meta);
76
+ expect(schema.type).toBe('object');
77
+ expect(schema.properties.name.type).toBe('string');
78
+ expect(schema.properties.email.format).toBe('email');
79
+ expect(schema.required).toContain('email');
80
+ });
81
+ });
82
+
83
+ describe('buildResponses', () => {
84
+ it('returns 200 Success when responseMeta empty', () => {
85
+ const r = buildResponses(null);
86
+ expect(r['200']).toEqual({ description: 'Success' });
87
+ });
88
+ it('builds responses from metadata', () => {
89
+ const meta = {
90
+ 200: { description: 'OK' },
91
+ 201: { description: 'Created' },
92
+ };
93
+ const r = buildResponses(meta);
94
+ expect(r['200'].description).toBe('OK');
95
+ expect(r['201'].description).toBe('Created');
96
+ });
97
+ });
98
+
99
+ describe('buildOperation', () => {
100
+ it('builds operation with summary and responses', () => {
101
+ const meta = {
102
+ summary: 'Get user',
103
+ response: { 200: { description: 'OK' } },
104
+ };
105
+ const pathParams = [];
106
+ const op = buildOperation('get', meta, pathParams);
107
+ expect(op.summary).toBe('Get user');
108
+ expect(op.responses['200'].description).toBe('OK');
109
+ });
110
+ });
111
+
112
+ describe('mergeMetadata', () => {
113
+ it('prefers explicit when preferEnhanced false', () => {
114
+ const explicit = { summary: 'A', description: 'B' };
115
+ const enhanced = { summary: 'C', description: 'D' };
116
+ const merged = mergeMetadata(explicit, enhanced, {
117
+ preferEnhanced: false,
118
+ });
119
+ expect(merged.summary).toBe('A');
120
+ expect(merged.description).toBe('B');
121
+ });
122
+ it('prefers enhanced when preferEnhanced true', () => {
123
+ const explicit = { summary: 'A' };
124
+ const enhanced = { summary: 'C', description: 'D' };
125
+ const merged = mergeMetadata(explicit, enhanced, {
126
+ preferEnhanced: true,
127
+ });
128
+ expect(merged.summary).toBe('C');
129
+ expect(merged.description).toBe('D');
130
+ });
131
+ });
132
+ });
@@ -16,7 +16,12 @@ export function isMethodKeyed(obj) {
16
16
  for (const key of Object.keys(obj)) {
17
17
  if (METHOD_KEYS.has(key.toLowerCase())) {
18
18
  const val = obj[key];
19
- if (val && typeof val === 'object' && (val.summary != null || val.response != null)) return true;
19
+ if (
20
+ val &&
21
+ typeof val === 'object' &&
22
+ (val.summary != null || val.response != null)
23
+ )
24
+ return true;
20
25
  }
21
26
  }
22
27
  return false;
@@ -82,7 +87,7 @@ export function getQueryParameters(queryMeta) {
82
87
  */
83
88
  export function buildSchemaFromMetadata(meta) {
84
89
  if (!meta || typeof meta !== 'object') return {};
85
- const properties = {};
90
+ const properties = Object.create(null);
86
91
  const required = [];
87
92
  for (const [key, value] of Object.entries(meta)) {
88
93
  if (value && typeof value === 'object' && value.type) {
@@ -91,7 +96,8 @@ export function buildSchemaFromMetadata(meta) {
91
96
  ...(value.description && { description: value.description }),
92
97
  ...(value.format && { format: value.format }),
93
98
  };
94
- if (value.required === true || value.required === 'true') required.push(key);
99
+ if (value.required === true || value.required === 'true')
100
+ required.push(key);
95
101
  }
96
102
  }
97
103
  if (Object.keys(properties).length === 0) return {};
@@ -108,7 +114,8 @@ export function buildSchemaFromMetadata(meta) {
108
114
  * @returns {object|undefined} OpenAPI requestBody or undefined
109
115
  */
110
116
  export function buildRequestBody(requestMeta) {
111
- if (!requestMeta?.body || typeof requestMeta.body !== 'object') return undefined;
117
+ if (!requestMeta?.body || typeof requestMeta.body !== 'object')
118
+ return undefined;
112
119
  const schema = buildSchemaFromMetadata(requestMeta.body);
113
120
  if (!schema || Object.keys(schema).length === 0) return undefined;
114
121
  return {
@@ -127,7 +134,7 @@ export function buildRequestBody(requestMeta) {
127
134
  * @returns {object} OpenAPI responses
128
135
  */
129
136
  export function buildResponses(responseMeta) {
130
- const responses = {};
137
+ const responses = Object.create(null);
131
138
  if (!responseMeta || typeof responseMeta !== 'object') {
132
139
  responses['200'] = { description: 'Success' };
133
140
  return responses;
@@ -139,9 +146,10 @@ export function buildResponses(responseMeta) {
139
146
  ...(spec.schema && {
140
147
  content: {
141
148
  'application/json': {
142
- schema: typeof spec.schema === 'object' && spec.schema.type
143
- ? spec.schema
144
- : { type: 'object' },
149
+ schema:
150
+ typeof spec.schema === 'object' && spec.schema.type
151
+ ? spec.schema
152
+ : { type: 'object' },
145
153
  },
146
154
  },
147
155
  }),
@@ -159,7 +167,8 @@ export function buildResponses(responseMeta) {
159
167
  * @returns {boolean}
160
168
  */
161
169
  export function isMethodAgnostic(methods) {
162
- if (!Array.isArray(methods) || methods.length !== ALL_METHODS.length) return false;
170
+ if (!Array.isArray(methods) || methods.length !== ALL_METHODS.length)
171
+ return false;
163
172
  const set = new Set(methods.map((m) => m.toUpperCase()));
164
173
  return ALL_METHODS.every((m) => set.has(m));
165
174
  }
@@ -190,7 +199,10 @@ export function buildOperation(method, meta, pathParams, options = {}) {
190
199
  };
191
200
  const methodUpper = method.toUpperCase();
192
201
  const body = buildRequestBody(meta.request);
193
- if (body && (methodAgnostic || (methodUpper !== 'GET' && methodUpper !== 'HEAD'))) {
202
+ if (
203
+ body &&
204
+ (methodAgnostic || (methodUpper !== 'GET' && methodUpper !== 'HEAD'))
205
+ ) {
194
206
  op.requestBody = body;
195
207
  }
196
208
  op.responses = buildResponses(meta.response);
@@ -207,17 +219,17 @@ export function buildOperation(method, meta, pathParams, options = {}) {
207
219
  export function mergeMetadata(explicit, enhanced, options = {}) {
208
220
  const preferEnhanced = options.preferEnhanced === true;
209
221
  const summary = preferEnhanced
210
- ? (enhanced?.summary ?? explicit?.summary ?? '')
211
- : (explicit?.summary ?? enhanced?.summary ?? '');
222
+ ? enhanced?.summary ?? explicit?.summary ?? ''
223
+ : explicit?.summary ?? enhanced?.summary ?? '';
212
224
  const description = preferEnhanced
213
- ? (enhanced?.description ?? explicit?.description ?? '')
214
- : (explicit?.description ?? enhanced?.description ?? '');
225
+ ? enhanced?.description ?? explicit?.description ?? ''
226
+ : explicit?.description ?? enhanced?.description ?? '';
215
227
  const request = preferEnhanced
216
- ? (enhanced?.request ?? explicit?.request)
217
- : (explicit?.request ?? enhanced?.request);
228
+ ? enhanced?.request ?? explicit?.request
229
+ : explicit?.request ?? enhanced?.request;
218
230
  const response = preferEnhanced
219
- ? (enhanced?.response ?? explicit?.response)
220
- : (explicit?.response ?? enhanced?.response);
231
+ ? enhanced?.response ?? explicit?.response
232
+ : explicit?.response ?? enhanced?.response;
221
233
  return {
222
234
  summary: summary || 'Endpoint',
223
235
  description: description || undefined,
@@ -234,7 +246,15 @@ export function mergeMetadata(explicit, enhanced, options = {}) {
234
246
  * @returns {object}
235
247
  */
236
248
  export function mergeMethodAgnosticMeta(metaByMethod, methods, fallbackMeta) {
237
- const preferredOrder = ['post', 'put', 'patch', 'get', 'delete', 'head', 'options'];
249
+ const preferredOrder = [
250
+ 'post',
251
+ 'put',
252
+ 'patch',
253
+ 'get',
254
+ 'delete',
255
+ 'head',
256
+ 'options',
257
+ ];
238
258
  for (const k of preferredOrder) {
239
259
  const m = metaByMethod.get(k);
240
260
  if (m && (m.summary || m.description)) return m;
@@ -32,25 +32,25 @@ function mask(value) {
32
32
 
33
33
  function ask(question, fallback = '') {
34
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
- });
35
+ const { promise, resolve } = Promise.withResolvers();
36
+ rl.question(`${c.cyan('?')} ${question}${hint}${c.dim(': ')}`, (answer) => {
37
+ resolve(answer.trim() || fallback);
39
38
  });
39
+ return promise;
40
40
  }
41
41
 
42
42
  function askYesNo(question, fallback = false) {
43
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
- });
44
+ const { promise, resolve } = Promise.withResolvers();
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
+ return promise;
54
54
  }
55
55
 
56
56
  async function loadTargetFiles(dirTargets = 'targets') {
@@ -88,13 +88,15 @@ async function loadTargetFiles(dirTargets = 'targets') {
88
88
  function getDocsOptionsFromConfig(config = {}) {
89
89
  const docs = config.docs || config.generateDocs || {};
90
90
  const e = process.env;
91
- const baseURL = docs.llm?.baseURL ?? e.LLM_BASE_URL ?? 'https://api.openai.com/v1';
91
+ const baseURL =
92
+ docs.llm?.baseURL ?? e.LLM_BASE_URL ?? 'https://api.openai.com/v1';
92
93
  const apiKey = docs.llm?.apiKey ?? e.LLM_API_KEY ?? e.OPENAI_API_KEY;
93
94
  const model = docs.llm?.model ?? e.LLM_MODEL ?? 'gpt-4o-mini';
94
95
  if (!apiKey && !e.OPENAI_API_KEY) {
95
96
  return null;
96
97
  }
97
- const dirTargets = docs.dirTargets ?? docs.dir?.targets ?? config.dir?.targets ?? 'targets';
98
+ const dirTargets =
99
+ docs.dirTargets ?? docs.dir?.targets ?? config.dir?.targets ?? 'targets';
98
100
  const output = docs.output ?? './openapi.json';
99
101
  const title = docs.title ?? 'API';
100
102
  const version = docs.version ?? '1.0.0';
@@ -116,7 +118,7 @@ function getDocsOptionsFromConfig(config = {}) {
116
118
  * @returns {Promise<void>}
117
119
  */
118
120
  export async function runDocsCommandCI() {
119
- const config = loadConfigFile();
121
+ const config = await loadConfigFile();
120
122
  const options = getDocsOptionsFromConfig(config);
121
123
  if (!options) {
122
124
  console.error(
@@ -128,7 +130,9 @@ export async function runDocsCommandCI() {
128
130
  process.stdout.write(`${c.yellow('⏳')} Loading targets...`);
129
131
  const fileCount = await loadTargetFiles(dirTargets);
130
132
  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`);
133
+ process.stdout.write(
134
+ `\r${c.green('✓')} Loaded ${c.bold(fileCount)} target file(s) — ${c.bold(endpointCount)} endpoint(s)\n`,
135
+ );
132
136
  if (endpointCount === 0) {
133
137
  console.log(c.yellow(' No endpoints found. Skipping doc generation.\n'));
134
138
  return;
@@ -156,19 +160,19 @@ export async function runDocsCommandCI() {
156
160
  * @returns {Promise<string[]>} e.g. ['refs/heads/main', 'refs/heads/feature/x']
157
161
  */
158
162
  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
- });
163
+ const { promise, resolve } = Promise.withResolvers();
164
+ const chunks = [];
165
+ stdin.on('data', (chunk) => chunks.push(chunk));
166
+ stdin.on('end', () => {
167
+ const lines = Buffer.concat(chunks).toString('utf8').trim().split('\n');
168
+ const refs = [];
169
+ for (let i = 0; i < lines.length; i++) {
170
+ const parts = lines[i].split(/\s+/);
171
+ if (parts.length >= 4) refs.push(parts[2]);
172
+ }
173
+ resolve(refs);
171
174
  });
175
+ return promise;
172
176
  }
173
177
 
174
178
  /**
@@ -178,7 +182,7 @@ function readPrePushRefs() {
178
182
  * Configure via tejas.config.json docs.productionBranch or env DOCS_PRODUCTION_BRANCH (default: main).
179
183
  */
180
184
  export async function runDocsOnPush() {
181
- const config = loadConfigFile();
185
+ const config = await loadConfigFile();
182
186
  const docs = config.docs || config.generateDocs || {};
183
187
  const productionBranch =
184
188
  docs.productionBranch ?? process.env.DOCS_PRODUCTION_BRANCH ?? 'main';
@@ -186,7 +190,11 @@ export async function runDocsOnPush() {
186
190
  const productionRef = `refs/heads/${productionBranch}`;
187
191
  const isPushingToProduction = remoteRefs.some((ref) => ref === productionRef);
188
192
  if (!isPushingToProduction) return;
189
- console.log(c.dim(` Docs: pushing to ${productionBranch} — generating documentation...\n`));
193
+ console.log(
194
+ c.dim(
195
+ ` Docs: pushing to ${productionBranch} — generating documentation...\n`,
196
+ ),
197
+ );
190
198
  await runDocsCommandCI();
191
199
  }
192
200
 
@@ -207,13 +215,13 @@ function serveDocsPreview(spec, port = 3333) {
207
215
  res.writeHead(404);
208
216
  res.end('Not found');
209
217
  });
210
- return new Promise((resolve) => {
211
- server.listen(port, () => resolve(server));
212
- });
218
+ const { promise, resolve } = Promise.withResolvers();
219
+ server.listen(port, () => resolve(server));
220
+ return promise;
213
221
  }
214
222
 
215
223
  export async function runDocsCommand() {
216
- const config = loadConfigFile();
224
+ const config = await loadConfigFile();
217
225
  const e = process.env;
218
226
 
219
227
  console.log();