@gridfox/codegen 0.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 (61) hide show
  1. package/.env.example +3 -0
  2. package/README.md +1152 -0
  3. package/dist/cli/main.d.ts +2 -0
  4. package/dist/cli/main.js +394 -0
  5. package/dist/cli/prompt.d.ts +4 -0
  6. package/dist/cli/prompt.js +89 -0
  7. package/dist/config/loadConfig.d.ts +2 -0
  8. package/dist/config/loadConfig.js +49 -0
  9. package/dist/config/schema.d.ts +21 -0
  10. package/dist/config/schema.js +17 -0
  11. package/dist/emit/formatter.d.ts +1 -0
  12. package/dist/emit/formatter.js +2 -0
  13. package/dist/emit/writer.d.ts +7 -0
  14. package/dist/emit/writer.js +37 -0
  15. package/dist/generate.d.ts +9 -0
  16. package/dist/generate.js +53 -0
  17. package/dist/generators/generateIndexFile.d.ts +2 -0
  18. package/dist/generators/generateIndexFile.js +12 -0
  19. package/dist/generators/generateRegistryFile.d.ts +2 -0
  20. package/dist/generators/generateRegistryFile.js +7 -0
  21. package/dist/generators/generateSdkClientFile.d.ts +2 -0
  22. package/dist/generators/generateSdkClientFile.js +46 -0
  23. package/dist/generators/generateSharedTypes.d.ts +1 -0
  24. package/dist/generators/generateSharedTypes.js +4 -0
  25. package/dist/generators/generateTableModule.d.ts +2 -0
  26. package/dist/generators/generateTableModule.js +49 -0
  27. package/dist/index.d.ts +9 -0
  28. package/dist/index.js +8 -0
  29. package/dist/input/apiTransport.d.ts +6 -0
  30. package/dist/input/apiTransport.js +21 -0
  31. package/dist/input/parseTablesPayload.d.ts +21 -0
  32. package/dist/input/parseTablesPayload.js +71 -0
  33. package/dist/input/readApiInput.d.ts +9 -0
  34. package/dist/input/readApiInput.js +17 -0
  35. package/dist/input/readInput.d.ts +21 -0
  36. package/dist/input/readInput.js +14 -0
  37. package/dist/model/internalTypes.d.ts +60 -0
  38. package/dist/model/internalTypes.js +1 -0
  39. package/dist/model/normalizeTables.d.ts +6 -0
  40. package/dist/model/normalizeTables.js +68 -0
  41. package/dist/model/zodSchemas.d.ts +120 -0
  42. package/dist/model/zodSchemas.js +47 -0
  43. package/dist/naming/fieldAliases.d.ts +1 -0
  44. package/dist/naming/fieldAliases.js +3 -0
  45. package/dist/naming/identifiers.d.ts +1 -0
  46. package/dist/naming/identifiers.js +11 -0
  47. package/dist/naming/reservedWords.d.ts +1 -0
  48. package/dist/naming/reservedWords.js +13 -0
  49. package/dist/naming/tableNames.d.ts +1 -0
  50. package/dist/naming/tableNames.js +3 -0
  51. package/dist/typing/mapFieldType.d.ts +8 -0
  52. package/dist/typing/mapFieldType.js +95 -0
  53. package/dist/typing/writability.d.ts +1 -0
  54. package/dist/typing/writability.js +2 -0
  55. package/dist/utils/sort.d.ts +11 -0
  56. package/dist/utils/sort.js +5 -0
  57. package/dist/validate/crudPlan.d.ts +23 -0
  58. package/dist/validate/crudPlan.js +189 -0
  59. package/dist/validate/renderCrudTest.d.ts +2 -0
  60. package/dist/validate/renderCrudTest.js +180 -0
  61. package/package.json +57 -0
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ // If users pass a TypeScript config file directly (e.g. --config foo.ts), Node
4
+ // can't import it by default. Ensure we run under tsx so the config can be loaded.
5
+ // We use an env var to avoid infinite respawn loops.
6
+ const shouldRespawnWithTsx = () => {
7
+ if (process.env.GRIDFOX_CODEGEN_TSX === '1')
8
+ return false;
9
+ const argv = process.argv.slice(2);
10
+ const configFlagIndex = argv.findIndex((arg) => arg === '--config');
11
+ if (configFlagIndex === -1)
12
+ return false;
13
+ const configPath = argv[configFlagIndex + 1];
14
+ if (typeof configPath !== 'string')
15
+ return false;
16
+ return !!configPath.match(/\.(?:mts|cts|ts)$/i);
17
+ };
18
+ if (shouldRespawnWithTsx()) {
19
+ const { spawn } = await import('node:child_process');
20
+ const args = ['--import', 'tsx', ...process.argv.slice(1)];
21
+ const child = spawn(process.execPath, args, {
22
+ stdio: 'inherit',
23
+ env: { ...process.env, GRIDFOX_CODEGEN_TSX: '1' }
24
+ });
25
+ const exitCode = await new Promise((resolve, reject) => {
26
+ child.on('exit', (code) => resolve(code ?? 0));
27
+ child.on('error', (err) => reject(err));
28
+ });
29
+ process.exit(exitCode);
30
+ }
31
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
32
+ import { tmpdir } from 'node:os';
33
+ import { cac } from 'cac';
34
+ import { execa } from 'execa';
35
+ import { loadConfig } from '../config/loadConfig.js';
36
+ import { readTablesInput } from '../input/readInput.js';
37
+ import { readTablesFromApi } from '../input/readApiInput.js';
38
+ import { generateFromTables, planGeneration } from '../generate.js';
39
+ import { sortedKeys } from '../utils/sort.js';
40
+ import { buildHeuristicPlan } from '../validate/crudPlan.js';
41
+ import { buildCrudTestSource } from '../validate/renderCrudTest.js';
42
+ import { canPrompt, promptConfirm, promptSecret, promptText } from './prompt.js';
43
+ const cli = cac('@gridfox/codegen');
44
+ const summarize = (statuses) => statuses.reduce((acc, status) => {
45
+ acc[status] += 1;
46
+ return acc;
47
+ }, { new: 0, changed: 0, unchanged: 0 });
48
+ const printError = (error) => {
49
+ if (error instanceof Error) {
50
+ const [headline, ...details] = error.message.split('\n');
51
+ console.error(`Error: ${headline}`);
52
+ if (details.length > 0) {
53
+ console.error(details.join('\n'));
54
+ }
55
+ }
56
+ else {
57
+ console.error('Error: unknown failure');
58
+ }
59
+ };
60
+ const parsePositiveNumber = (value, label) => {
61
+ if (value === undefined || value === null || value === '') {
62
+ return undefined;
63
+ }
64
+ const parsed = Number(value);
65
+ if (!Number.isFinite(parsed) || parsed <= 0) {
66
+ throw new Error(`Invalid ${label} value. Expected a positive number.`);
67
+ }
68
+ return parsed;
69
+ };
70
+ const compileRegex = (pattern, flagName) => {
71
+ if (!pattern) {
72
+ return undefined;
73
+ }
74
+ try {
75
+ return new RegExp(pattern);
76
+ }
77
+ catch (error) {
78
+ const message = error instanceof Error ? error.message : 'invalid regex';
79
+ throw new Error(`Invalid ${flagName} regex: ${message}`);
80
+ }
81
+ };
82
+ const resolveApiKey = async (apiKeyFlag) => {
83
+ if (apiKeyFlag && apiKeyFlag.trim().length > 0) {
84
+ return apiKeyFlag.trim();
85
+ }
86
+ const fromEnv = process.env.GRIDFOX_API_KEY;
87
+ if (fromEnv && fromEnv.trim().length > 0) {
88
+ return fromEnv.trim();
89
+ }
90
+ const prompted = await promptSecret('Gridfox API key: ');
91
+ if (!prompted) {
92
+ throw new Error('Missing API key. Provide --api-key, set GRIDFOX_API_KEY, or enter a key at the prompt.');
93
+ }
94
+ return prompted;
95
+ };
96
+ const buildTempTsConfig = (workspaceDir) => JSON.stringify({
97
+ compilerOptions: {
98
+ target: 'ES2022',
99
+ module: 'NodeNext',
100
+ moduleResolution: 'NodeNext',
101
+ strict: true,
102
+ outDir: path.join(workspaceDir, '.compiled'),
103
+ rootDir: workspaceDir,
104
+ skipLibCheck: true,
105
+ lib: ['ES2022', 'DOM']
106
+ },
107
+ include: [path.join(workspaceDir, 'generated', '**/*.ts'), path.join(workspaceDir, 'src', '**/*.ts')]
108
+ }, null, 2);
109
+ cli
110
+ .command('generate', 'Generate TypeScript artifacts from Gridfox tables JSON')
111
+ .option('--input <path>', 'Path to tables JSON file')
112
+ .option('--output <path>', 'Output directory for generated files')
113
+ .option('--config <path>', 'Path to config file (.json, .ts, .mts, .cts, .mjs, .cjs)')
114
+ .option('--api-key <key>', 'Gridfox API key (or GRIDFOX_API_KEY)')
115
+ .option('--api-base-url <url>', 'Override Gridfox API base URL (or GRIDFOX_API_BASE_URL)')
116
+ .option('--api-timeout-ms <ms>', 'HTTP timeout for Gridfox API requests (or GRIDFOX_API_TIMEOUT_MS)')
117
+ .option('--multi-select-mode <mode>', 'Set multiSelect typing mode: union | stringArray')
118
+ .option('--client', 'Generate client.ts SDK wrapper (imports @gridfox/sdk)')
119
+ .option('--emit-sdk-client', 'Deprecated alias for --client')
120
+ .option('--emit-registry', 'Generate tables.ts registry')
121
+ .option('--emit-reverse-alias-map', 'Generate reverse alias map constants')
122
+ .option('--dry-run', 'Show planned file writes without writing files')
123
+ .option('--check', 'Exit non-zero when generated output is outdated')
124
+ .option('--no-format', 'Disable prettier formatting')
125
+ .action(async (flags) => {
126
+ try {
127
+ const interactive = canPrompt();
128
+ let outputFlag = typeof flags.output === 'string' ? flags.output : undefined;
129
+ let inputFlag = typeof flags.input === 'string' ? flags.input : undefined;
130
+ let apiKeyFlag = typeof flags.apiKey === 'string' ? flags.apiKey : undefined;
131
+ let clientFlag = flags.client ?? flags.emitSdkClient;
132
+ if (interactive && !flags.config) {
133
+ if (!outputFlag) {
134
+ outputFlag = await promptText('Output directory: ');
135
+ }
136
+ if (!inputFlag && !apiKeyFlag) {
137
+ const useApi = await promptConfirm('Use Gridfox API as input source?', Boolean(process.env.GRIDFOX_API_KEY?.trim()));
138
+ if (useApi) {
139
+ if (!process.env.GRIDFOX_API_KEY?.trim()) {
140
+ apiKeyFlag = await resolveApiKey(undefined);
141
+ }
142
+ }
143
+ else {
144
+ inputFlag = await promptText('Path to tables JSON input file: ');
145
+ }
146
+ }
147
+ if (clientFlag === undefined) {
148
+ clientFlag = await promptConfirm('Emit typed SDK client wrapper (client.ts)?', false);
149
+ }
150
+ }
151
+ const config = await loadConfig(flags.config, {
152
+ input: inputFlag,
153
+ output: outputFlag,
154
+ apiKey: apiKeyFlag,
155
+ apiBaseUrl: flags.apiBaseUrl,
156
+ apiTimeoutMs: flags.apiTimeoutMs !== undefined ? Number(flags.apiTimeoutMs) : undefined,
157
+ multiSelectMode: flags.multiSelectMode,
158
+ emitClient: clientFlag,
159
+ emitRegistry: flags.emitRegistry,
160
+ emitReverseAliasMap: flags.emitReverseAliasMap,
161
+ format: flags.format
162
+ });
163
+ const hasApiConfig = Boolean(config.apiKey);
164
+ let tables;
165
+ if (config.input) {
166
+ tables = await readTablesInput(config.input);
167
+ }
168
+ else if (hasApiConfig) {
169
+ tables = await readTablesFromApi({
170
+ apiKey: config.apiKey,
171
+ apiBaseUrl: config.apiBaseUrl,
172
+ apiTimeoutMs: config.apiTimeoutMs
173
+ });
174
+ }
175
+ else {
176
+ throw new Error('Missing input source. Provide --input/config.input or --api-key (or GRIDFOX_API_KEY)');
177
+ }
178
+ if (flags.dryRun || flags.check) {
179
+ const { diff } = await planGeneration(tables, config);
180
+ const totals = summarize(diff.map((entry) => entry.status));
181
+ const statusesByPath = Object.fromEntries(diff.map((entry) => [entry.path, entry.status]));
182
+ console.log(`Planned files: ${diff.length} (new: ${totals.new}, changed: ${totals.changed}, unchanged: ${totals.unchanged})`);
183
+ for (const outputPath of sortedKeys(statusesByPath)) {
184
+ console.log(`[${statusesByPath[outputPath]}] ${outputPath}`);
185
+ }
186
+ if (flags.check && (totals.new > 0 || totals.changed > 0)) {
187
+ console.error('Error: generated files are out of date');
188
+ process.exitCode = 1;
189
+ }
190
+ return;
191
+ }
192
+ await generateFromTables(tables, config);
193
+ }
194
+ catch (error) {
195
+ printError(error);
196
+ process.exitCode = 1;
197
+ }
198
+ });
199
+ cli
200
+ .command('validate', 'Run live CRUD validation against a Gridfox project')
201
+ .option('--api-key <key>', 'Gridfox API key (or GRIDFOX_API_KEY; prompts if missing)')
202
+ .option('--key <key>', 'Alias for --api-key')
203
+ .option('--api-base-url <url>', 'Override Gridfox API base URL (or GRIDFOX_API_BASE_URL)')
204
+ .option('--api-timeout-ms <ms>', 'HTTP timeout for Gridfox API requests (or GRIDFOX_API_TIMEOUT_MS)')
205
+ .option('--table <name>', 'Pin to a specific table name')
206
+ .option('--allow-table-regex <pattern>', 'Allow only table names matching regex when auto-selecting')
207
+ .option('--deny-table-regex <pattern>', 'Deny table names matching regex when auto-selecting')
208
+ .option('--plan-only', 'Compute and print plan without writing any records')
209
+ .option('--yes-live-writes', 'Acknowledge that validate will create/update/delete real records')
210
+ .option('--json', 'Emit machine-readable JSON output')
211
+ .option('--keep-temp', 'Keep temp workspace after run')
212
+ .option('--verbose', 'Print detailed progress information')
213
+ .action(async (flags) => {
214
+ const jsonOutput = Boolean(flags.json);
215
+ const interactive = canPrompt() && !jsonOutput;
216
+ const log = (...values) => {
217
+ if (!flags.verbose) {
218
+ return;
219
+ }
220
+ const line = `[validate] ${values.map((value) => String(value)).join(' ')}`;
221
+ if (jsonOutput) {
222
+ console.error(line);
223
+ }
224
+ else {
225
+ console.log(line);
226
+ }
227
+ };
228
+ const emitJson = (payload) => {
229
+ console.log(JSON.stringify(payload, null, 2));
230
+ };
231
+ const defaultDenyRegex = /audit|log|event|history/i;
232
+ let workspaceDir = null;
233
+ try {
234
+ let tableName = typeof flags.table === 'string' ? flags.table : process.env.GRIDFOX_REAL_TEST_TABLE;
235
+ const allowRegex = compileRegex(flags.allowTableRegex, '--allow-table-regex');
236
+ const denyRegex = compileRegex(flags.denyTableRegex, '--deny-table-regex');
237
+ let planOnly = Boolean(flags.planOnly);
238
+ let yesLiveWrites = Boolean(flags.yesLiveWrites);
239
+ if (!tableName && !allowRegex && !interactive) {
240
+ throw new Error('Validate requires either --table or --allow-table-regex for auto table selection');
241
+ }
242
+ if (!planOnly && !yesLiveWrites && !interactive) {
243
+ throw new Error('Live validation writes data. Re-run with --yes-live-writes or use --plan-only');
244
+ }
245
+ const apiKey = await resolveApiKey(flags.apiKey ?? flags.key);
246
+ const apiBaseUrl = flags.apiBaseUrl ?? process.env.GRIDFOX_API_BASE_URL;
247
+ const apiTimeoutMs = parsePositiveNumber(flags.apiTimeoutMs ?? process.env.GRIDFOX_API_TIMEOUT_MS, 'API timeout');
248
+ log('fetching tables from Gridfox API');
249
+ const tables = await readTablesFromApi({
250
+ apiKey,
251
+ apiBaseUrl,
252
+ apiTimeoutMs
253
+ });
254
+ if (!tableName && !allowRegex) {
255
+ if (!interactive) {
256
+ throw new Error('Validate requires either --table or --allow-table-regex for auto table selection');
257
+ }
258
+ const availableTables = tables.map((table) => table.name).sort((left, right) => left.localeCompare(right));
259
+ if (availableTables.length > 0) {
260
+ console.log(`Available tables: ${availableTables.join(', ')}`);
261
+ }
262
+ tableName = await promptText('Table name: ');
263
+ if (!tableName) {
264
+ throw new Error('Missing table name. Provide --table/--allow-table-regex or enter a table name at the prompt.');
265
+ }
266
+ }
267
+ if (!planOnly && !yesLiveWrites && interactive) {
268
+ planOnly = await promptConfirm('Run in plan-only mode (no live writes)?', true);
269
+ if (!planOnly) {
270
+ yesLiveWrites = await promptConfirm('This will create/update/delete real records in your project. Continue?', false);
271
+ }
272
+ }
273
+ if (!planOnly && !yesLiveWrites) {
274
+ throw new Error('Live validation writes data. Re-run with --yes-live-writes or use --plan-only');
275
+ }
276
+ const candidateTables = tableName
277
+ ? tables
278
+ : tables
279
+ .filter((table) => allowRegex.test(table.name))
280
+ .filter((table) => !(denyRegex ?? defaultDenyRegex).test(table.name));
281
+ if (candidateTables.length === 0) {
282
+ const allowText = allowRegex ? allowRegex.toString() : '<none>';
283
+ const denyText = (denyRegex ?? defaultDenyRegex).toString();
284
+ throw new Error(`No tables remain after applying filters (allow: ${allowText}, deny: ${denyText})`);
285
+ }
286
+ const plan = buildHeuristicPlan(candidateTables, {
287
+ tableName
288
+ });
289
+ if (!jsonOutput) {
290
+ console.log(`[validate] Selected table: ${plan.tableName}`);
291
+ console.log('[validate] CRUD plan:');
292
+ console.log(JSON.stringify(plan, null, 2));
293
+ }
294
+ if (planOnly) {
295
+ if (jsonOutput) {
296
+ emitJson({
297
+ command: 'validate',
298
+ status: 'planned',
299
+ planOnly: true,
300
+ table: plan.tableName,
301
+ plan
302
+ });
303
+ }
304
+ return;
305
+ }
306
+ workspaceDir = await mkdtemp(path.join(tmpdir(), 'gridfox-validate-'));
307
+ const generatedDir = path.join(workspaceDir, 'generated');
308
+ const srcDir = path.join(workspaceDir, 'src');
309
+ const compiledDir = path.join(workspaceDir, '.compiled');
310
+ const tsconfigPath = path.join(workspaceDir, 'tsconfig.json');
311
+ const testFilePath = path.join(srcDir, 'realCrudTest.ts');
312
+ log('temp workspace:', workspaceDir);
313
+ await mkdir(generatedDir, { recursive: true });
314
+ await mkdir(srcDir, { recursive: true });
315
+ log('generating TypeScript schema files');
316
+ await generateFromTables(candidateTables, {
317
+ output: generatedDir,
318
+ emitRegistry: true,
319
+ emitMetadata: true,
320
+ format: true
321
+ });
322
+ log('writing temp validation test source');
323
+ await writeFile(tsconfigPath, buildTempTsConfig(workspaceDir), 'utf8');
324
+ await writeFile(testFilePath, buildCrudTestSource(plan), 'utf8');
325
+ log('typechecking generated + validation test files');
326
+ await execa('npx', ['tsc', '-p', tsconfigPath], {
327
+ cwd: process.cwd(),
328
+ stdio: 'inherit'
329
+ });
330
+ log('running live CRUD validation');
331
+ await execa('node', [path.join(compiledDir, 'src', 'realCrudTest.js')], {
332
+ cwd: process.cwd(),
333
+ stdio: 'inherit',
334
+ env: {
335
+ ...process.env,
336
+ GRIDFOX_API_KEY: apiKey,
337
+ GRIDFOX_API_BASE_URL: apiBaseUrl
338
+ }
339
+ });
340
+ if (jsonOutput) {
341
+ emitJson({
342
+ command: 'validate',
343
+ status: 'passed',
344
+ table: plan.tableName,
345
+ plan
346
+ });
347
+ }
348
+ else {
349
+ console.log(`[validate] Passed for table "${plan.tableName}"`);
350
+ }
351
+ }
352
+ catch (error) {
353
+ if (jsonOutput) {
354
+ const message = error instanceof Error ? error.message : 'unknown failure';
355
+ emitJson({
356
+ command: 'validate',
357
+ status: 'failed',
358
+ error: message
359
+ });
360
+ }
361
+ else {
362
+ printError(error);
363
+ }
364
+ process.exitCode = 1;
365
+ }
366
+ finally {
367
+ if (workspaceDir) {
368
+ if (flags.keepTemp) {
369
+ const message = `Kept temp workspace: ${workspaceDir}`;
370
+ if (jsonOutput) {
371
+ console.error(`[validate] ${message}`);
372
+ }
373
+ else {
374
+ console.log(message);
375
+ }
376
+ }
377
+ else {
378
+ await rm(workspaceDir, { recursive: true, force: true });
379
+ log('cleaned temp workspace');
380
+ }
381
+ }
382
+ }
383
+ });
384
+ cli.help();
385
+ if (process.argv.slice(2).length === 0) {
386
+ if (canPrompt()) {
387
+ process.argv.push('generate');
388
+ }
389
+ else {
390
+ cli.outputHelp();
391
+ process.exitCode = 1;
392
+ }
393
+ }
394
+ cli.parse();
@@ -0,0 +1,4 @@
1
+ export declare const canPrompt: () => boolean;
2
+ export declare const promptText: (question: string) => Promise<string>;
3
+ export declare const promptConfirm: (question: string, defaultValue?: boolean) => Promise<boolean>;
4
+ export declare const promptSecret: (question: string) => Promise<string>;
@@ -0,0 +1,89 @@
1
+ import readline from 'node:readline';
2
+ export const canPrompt = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
3
+ const ensurePromptSupported = (message) => {
4
+ if (!canPrompt()) {
5
+ throw new Error(message);
6
+ }
7
+ };
8
+ export const promptText = async (question) => {
9
+ ensurePromptSupported('Cannot prompt in non-interactive mode.');
10
+ return new Promise((resolve, reject) => {
11
+ const rl = readline.createInterface({
12
+ input: process.stdin,
13
+ output: process.stdout,
14
+ terminal: true
15
+ });
16
+ const onSigint = () => {
17
+ rl.close();
18
+ reject(new Error('Prompt cancelled'));
19
+ };
20
+ rl.once('SIGINT', onSigint);
21
+ rl.question(question, (answer) => {
22
+ rl.off('SIGINT', onSigint);
23
+ rl.close();
24
+ resolve(answer.trim());
25
+ });
26
+ });
27
+ };
28
+ export const promptConfirm = async (question, defaultValue = false) => {
29
+ const suffix = defaultValue ? ' [Y/n]: ' : ' [y/N]: ';
30
+ while (true) {
31
+ const answer = (await promptText(`${question}${suffix}`)).toLowerCase();
32
+ if (!answer) {
33
+ return defaultValue;
34
+ }
35
+ if (answer === 'y' || answer === 'yes') {
36
+ return true;
37
+ }
38
+ if (answer === 'n' || answer === 'no') {
39
+ return false;
40
+ }
41
+ console.log('Please answer yes or no.');
42
+ }
43
+ };
44
+ export const promptSecret = async (question) => {
45
+ ensurePromptSupported('Cannot prompt for API key in non-interactive mode. Pass --api-key or set GRIDFOX_API_KEY.');
46
+ return new Promise((resolve, reject) => {
47
+ const rl = readline.createInterface({
48
+ input: process.stdin,
49
+ output: process.stdout,
50
+ terminal: true
51
+ });
52
+ const originalWrite = rl._writeToOutput?.bind(rl);
53
+ rl.stdoutMuted = false;
54
+ rl._writeToOutput = (value) => {
55
+ if (!rl.stdoutMuted) {
56
+ if (originalWrite) {
57
+ originalWrite(value);
58
+ }
59
+ else {
60
+ process.stdout.write(value);
61
+ }
62
+ return;
63
+ }
64
+ if (value.endsWith('\n')) {
65
+ process.stdout.write(value);
66
+ }
67
+ else if (value.trim().length > 0) {
68
+ process.stdout.write('*');
69
+ }
70
+ else {
71
+ process.stdout.write(value);
72
+ }
73
+ };
74
+ const onSigint = () => {
75
+ rl.close();
76
+ reject(new Error('Prompt cancelled'));
77
+ };
78
+ rl.once('SIGINT', onSigint);
79
+ rl.question(question, (answer) => {
80
+ rl.off('SIGINT', onSigint);
81
+ rl.close();
82
+ process.stdout.write('\n');
83
+ resolve(answer.trim());
84
+ });
85
+ setImmediate(() => {
86
+ rl.stdoutMuted = true;
87
+ });
88
+ });
89
+ };
@@ -0,0 +1,2 @@
1
+ import { type Config } from './schema.js';
2
+ export declare const loadConfig: (configPath?: string, overrides?: Partial<Config>) => Promise<Config>;
@@ -0,0 +1,49 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { ConfigSchema } from './schema.js';
5
+ const defaults = {
6
+ input: undefined,
7
+ includeTables: undefined,
8
+ excludeTables: undefined,
9
+ apiKey: undefined,
10
+ apiBaseUrl: undefined,
11
+ apiTimeoutMs: undefined,
12
+ multiSelectMode: 'union',
13
+ emitClient: false,
14
+ emitRegistry: true,
15
+ emitSdkClient: undefined,
16
+ emitReverseAliasMap: false,
17
+ emitMetadata: true,
18
+ format: true
19
+ };
20
+ const readEnvConfig = () => {
21
+ const apiTimeoutMs = process.env.GRIDFOX_API_TIMEOUT_MS;
22
+ return {
23
+ apiKey: process.env.GRIDFOX_API_KEY,
24
+ apiBaseUrl: process.env.GRIDFOX_API_BASE_URL,
25
+ apiTimeoutMs: apiTimeoutMs !== undefined ? Number(apiTimeoutMs) : undefined
26
+ };
27
+ };
28
+ export const loadConfig = async (configPath, overrides = {}) => {
29
+ let configFromFile = {};
30
+ if (configPath) {
31
+ const extension = path.extname(configPath);
32
+ if (extension === '.json') {
33
+ const raw = await readFile(configPath, 'utf8');
34
+ configFromFile = JSON.parse(raw);
35
+ }
36
+ else {
37
+ const imported = await import(pathToFileURL(path.resolve(configPath)).href);
38
+ configFromFile = (imported.default ?? imported);
39
+ }
40
+ }
41
+ const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined));
42
+ const merged = {
43
+ ...defaults,
44
+ ...readEnvConfig(),
45
+ ...configFromFile,
46
+ ...definedOverrides
47
+ };
48
+ return ConfigSchema.parse(merged);
49
+ };
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ export declare const ConfigSchema: z.ZodObject<{
3
+ input: z.ZodOptional<z.ZodString>;
4
+ output: z.ZodString;
5
+ includeTables: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
+ excludeTables: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
+ apiKey: z.ZodOptional<z.ZodString>;
8
+ apiBaseUrl: z.ZodOptional<z.ZodString>;
9
+ apiTimeoutMs: z.ZodOptional<z.ZodNumber>;
10
+ multiSelectMode: z.ZodDefault<z.ZodEnum<{
11
+ union: "union";
12
+ stringArray: "stringArray";
13
+ }>>;
14
+ emitClient: z.ZodDefault<z.ZodBoolean>;
15
+ emitRegistry: z.ZodDefault<z.ZodBoolean>;
16
+ emitSdkClient: z.ZodOptional<z.ZodBoolean>;
17
+ emitReverseAliasMap: z.ZodDefault<z.ZodBoolean>;
18
+ emitMetadata: z.ZodDefault<z.ZodBoolean>;
19
+ format: z.ZodDefault<z.ZodBoolean>;
20
+ }, z.core.$strip>;
21
+ export type Config = z.infer<typeof ConfigSchema>;
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ export const ConfigSchema = z.object({
3
+ input: z.string().optional(),
4
+ output: z.string(),
5
+ includeTables: z.array(z.string()).optional(),
6
+ excludeTables: z.array(z.string()).optional(),
7
+ apiKey: z.string().min(1).optional(),
8
+ apiBaseUrl: z.string().url().optional(),
9
+ apiTimeoutMs: z.number().int().positive().optional(),
10
+ multiSelectMode: z.enum(['union', 'stringArray']).default('union'),
11
+ emitClient: z.boolean().default(false),
12
+ emitRegistry: z.boolean().default(true),
13
+ emitSdkClient: z.boolean().optional(),
14
+ emitReverseAliasMap: z.boolean().default(false),
15
+ emitMetadata: z.boolean().default(true),
16
+ format: z.boolean().default(true)
17
+ });
@@ -0,0 +1 @@
1
+ export declare const formatTypeScript: (source: string) => Promise<string>;
@@ -0,0 +1,2 @@
1
+ import prettier from 'prettier';
2
+ export const formatTypeScript = async (source) => prettier.format(source, { parser: 'typescript' });
@@ -0,0 +1,7 @@
1
+ export type GeneratedFileStatus = 'new' | 'changed' | 'unchanged';
2
+ export interface GeneratedFileDiff {
3
+ path: string;
4
+ status: GeneratedFileStatus;
5
+ }
6
+ export declare const diffGeneratedFiles: (baseDir: string, files: Record<string, string>) => Promise<GeneratedFileDiff[]>;
7
+ export declare const writeGeneratedFiles: (baseDir: string, files: Record<string, string>) => Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { sortedKeys } from '../utils/sort.js';
4
+ const readCurrentFile = async (outputPath) => {
5
+ try {
6
+ return await readFile(outputPath, 'utf8');
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ };
12
+ export const diffGeneratedFiles = async (baseDir, files) => {
13
+ const diffs = [];
14
+ for (const relativePath of sortedKeys(files)) {
15
+ const outputPath = path.join(baseDir, relativePath);
16
+ const current = await readCurrentFile(outputPath);
17
+ const next = files[relativePath];
18
+ if (current === undefined) {
19
+ diffs.push({ path: relativePath, status: 'new' });
20
+ continue;
21
+ }
22
+ diffs.push({ path: relativePath, status: current === next ? 'unchanged' : 'changed' });
23
+ }
24
+ return diffs;
25
+ };
26
+ export const writeGeneratedFiles = async (baseDir, files) => {
27
+ await mkdir(baseDir, { recursive: true });
28
+ for (const relativePath of sortedKeys(files)) {
29
+ const content = files[relativePath];
30
+ const outputPath = path.join(baseDir, relativePath);
31
+ await mkdir(path.dirname(outputPath), { recursive: true });
32
+ const current = await readCurrentFile(outputPath);
33
+ if (current !== content) {
34
+ await writeFile(outputPath, content, 'utf8');
35
+ }
36
+ }
37
+ };
@@ -0,0 +1,9 @@
1
+ import type { GridfoxCodegenConfig, RawTable } from './model/internalTypes.js';
2
+ import { type GeneratedFileDiff } from './emit/writer.js';
3
+ export declare const buildGeneratedFiles: (tables: RawTable[], config: GridfoxCodegenConfig) => Promise<Record<string, string>>;
4
+ export interface GenerationPlan {
5
+ files: Record<string, string>;
6
+ diff: GeneratedFileDiff[];
7
+ }
8
+ export declare const planGeneration: (tables: RawTable[], config: GridfoxCodegenConfig) => Promise<GenerationPlan>;
9
+ export declare const generateFromTables: (tables: RawTable[], config: GridfoxCodegenConfig) => Promise<Record<string, string>>;