@claudetools/tools 0.9.0 → 0.9.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.
- package/dist/cli.js +9 -1
- package/dist/codedna/__tests__/examples/mongoose-example.d.ts +6 -0
- package/dist/codedna/__tests__/examples/mongoose-example.js +163 -0
- package/dist/codedna/__tests__/fixtures/typeorm-production-test.d.ts +1 -0
- package/dist/codedna/__tests__/fixtures/typeorm-production-test.js +231 -0
- package/dist/codedna/__tests__/fixtures/typeorm-test.d.ts +1 -0
- package/dist/codedna/__tests__/fixtures/typeorm-test.js +124 -0
- package/dist/codedna/__tests__/laravel-output-review.d.ts +1 -0
- package/dist/codedna/__tests__/laravel-output-review.js +249 -0
- package/dist/codedna/__tests__/mongoose-output-test.d.ts +1 -0
- package/dist/codedna/__tests__/mongoose-output-test.js +178 -0
- package/dist/codedna/examples/radix-example.d.ts +2 -0
- package/dist/codedna/examples/radix-example.js +259 -0
- package/dist/codedna/index.d.ts +5 -3
- package/dist/codedna/index.js +6 -3
- package/dist/codedna/kappa-ast.d.ts +143 -5
- package/dist/codedna/kappa-drizzle-generator.js +8 -5
- package/dist/codedna/kappa-gofiber-generator.d.ts +65 -0
- package/dist/codedna/kappa-gofiber-generator.js +587 -0
- package/dist/codedna/kappa-laravel-generator.d.ts +68 -0
- package/dist/codedna/kappa-laravel-generator.js +741 -0
- package/dist/codedna/kappa-lexer.d.ts +44 -0
- package/dist/codedna/kappa-lexer.js +124 -0
- package/dist/codedna/kappa-mantine-generator.d.ts +65 -0
- package/dist/codedna/kappa-mantine-generator.js +518 -0
- package/dist/codedna/kappa-mongoose-generator.d.ts +44 -0
- package/dist/codedna/kappa-mongoose-generator.js +442 -0
- package/dist/codedna/kappa-parser.d.ts +43 -1
- package/dist/codedna/kappa-parser.js +601 -0
- package/dist/codedna/kappa-radix-generator.d.ts +61 -0
- package/dist/codedna/kappa-radix-generator.js +566 -0
- package/dist/codedna/kappa-typeorm-generator.d.ts +59 -0
- package/dist/codedna/kappa-typeorm-generator.js +723 -0
- package/dist/codedna/kappa-vitest-generator.d.ts +85 -0
- package/dist/codedna/kappa-vitest-generator.js +739 -0
- package/dist/codedna/parser.js +26 -1
- package/dist/codegen/cloud-client.d.ts +160 -0
- package/dist/codegen/cloud-client.js +195 -0
- package/dist/codegen/codegen-tool.d.ts +35 -0
- package/dist/codegen/codegen-tool.js +312 -0
- package/dist/codegen/field-inference.d.ts +24 -0
- package/dist/codegen/field-inference.js +101 -0
- package/dist/codegen/form-parser.d.ts +13 -0
- package/dist/codegen/form-parser.js +186 -0
- package/dist/codegen/index.d.ts +2 -0
- package/dist/codegen/index.js +4 -0
- package/dist/codegen/natural-parser.d.ts +50 -0
- package/dist/codegen/natural-parser.js +769 -0
- package/dist/handlers/codedna-handlers.d.ts +1 -1
- package/dist/handlers/codegen-handlers.d.ts +20 -0
- package/dist/handlers/codegen-handlers.js +60 -0
- package/dist/handlers/kappa-handlers.d.ts +97 -0
- package/dist/handlers/kappa-handlers.js +408 -0
- package/dist/handlers/tool-handlers.js +124 -221
- package/dist/helpers/api-client.js +48 -3
- package/dist/helpers/compact-formatter.d.ts +9 -2
- package/dist/helpers/compact-formatter.js +26 -2
- package/dist/helpers/config.d.ts +7 -2
- package/dist/helpers/config.js +25 -10
- package/dist/helpers/session-validation.d.ts +1 -1
- package/dist/helpers/session-validation.js +2 -4
- package/dist/helpers/tasks.d.ts +21 -0
- package/dist/helpers/tasks.js +52 -0
- package/dist/helpers/workers.d.ts +1 -1
- package/dist/helpers/workers.js +19 -19
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +228 -3
- package/dist/templates/claude-md.d.ts +1 -1
- package/dist/templates/claude-md.js +37 -152
- package/dist/templates/orchestrator-prompt.d.ts +2 -2
- package/dist/templates/orchestrator-prompt.js +31 -38
- package/dist/templates/self-critique.d.ts +50 -0
- package/dist/templates/self-critique.js +209 -0
- package/dist/templates/worker-prompt.d.ts +3 -3
- package/dist/templates/worker-prompt.js +18 -18
- package/dist/tools.js +77 -413
- package/docs/codedna/generator-testing-summary.md +205 -0
- package/docs/codedna/radix-ui-generator.md +478 -0
- package/docs/kappa-gofiber-generator.md +274 -0
- package/docs/kappa-laravel-fixes.md +172 -0
- package/docs/kappa-mongoose-generator.md +322 -0
- package/docs/kappa-vitest-generator.md +337 -0
- package/package.json +1 -1
- package/dist/context/deduplication.test.d.ts +0 -6
- package/dist/context/deduplication.test.js +0 -84
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Unified Codegen Tool
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// ONE tool to replace 20+ broken CodeDNA/Kappa tools.
|
|
6
|
+
// Natural language in → generated code out.
|
|
7
|
+
//
|
|
8
|
+
import { parseNaturalLanguage, parsePageDescription, parseComponentDescription } from './natural-parser.js';
|
|
9
|
+
import { parseFormDescription } from './form-parser.js';
|
|
10
|
+
import { generateDrizzleSchema } from '../codedna/kappa-drizzle-generator.js';
|
|
11
|
+
import { generateZodSchemas } from '../codedna/kappa-zod-generator.js';
|
|
12
|
+
import { generateTypeScriptTypes } from '../codedna/kappa-types-generator.js';
|
|
13
|
+
import { generateAPI } from '../codedna/kappa-api-generator.js';
|
|
14
|
+
import { generateEntityTests } from '../codedna/kappa-vitest-generator.js';
|
|
15
|
+
import { generateRadixComponents } from '../codedna/kappa-radix-generator.js';
|
|
16
|
+
import { generatePages } from '../codedna/kappa-page-generator.js';
|
|
17
|
+
import { generateForms } from '../codedna/kappa-form-generator.js';
|
|
18
|
+
export function detectStack(packageJson) {
|
|
19
|
+
const deps = {
|
|
20
|
+
...(packageJson?.dependencies || {}),
|
|
21
|
+
...(packageJson?.devDependencies || {}),
|
|
22
|
+
};
|
|
23
|
+
const stack = {
|
|
24
|
+
orm: null,
|
|
25
|
+
api: null,
|
|
26
|
+
ui: null,
|
|
27
|
+
db: null,
|
|
28
|
+
framework: null,
|
|
29
|
+
lang: 'typescript' in deps || '@types/node' in deps ? 'typescript' : 'javascript',
|
|
30
|
+
};
|
|
31
|
+
// Detect ORM
|
|
32
|
+
if ('drizzle-orm' in deps)
|
|
33
|
+
stack.orm = 'drizzle';
|
|
34
|
+
else if ('@prisma/client' in deps)
|
|
35
|
+
stack.orm = 'prisma';
|
|
36
|
+
else if ('typeorm' in deps)
|
|
37
|
+
stack.orm = 'typeorm';
|
|
38
|
+
else if ('mongoose' in deps)
|
|
39
|
+
stack.orm = 'mongoose';
|
|
40
|
+
// Detect API framework
|
|
41
|
+
if ('hono' in deps)
|
|
42
|
+
stack.api = 'hono';
|
|
43
|
+
else if ('express' in deps)
|
|
44
|
+
stack.api = 'express';
|
|
45
|
+
else if ('@trpc/server' in deps)
|
|
46
|
+
stack.api = 'trpc';
|
|
47
|
+
else if ('fastify' in deps)
|
|
48
|
+
stack.api = 'fastify';
|
|
49
|
+
// Detect UI
|
|
50
|
+
if ('@radix-ui/react-dialog' in deps || '@radix-ui/themes' in deps)
|
|
51
|
+
stack.ui = 'radix';
|
|
52
|
+
else if ('@mantine/core' in deps)
|
|
53
|
+
stack.ui = 'mantine';
|
|
54
|
+
else if ('@chakra-ui/react' in deps)
|
|
55
|
+
stack.ui = 'chakra';
|
|
56
|
+
// shadcn doesn't have a package, detected by tailwind + radix primitives
|
|
57
|
+
// Detect DB
|
|
58
|
+
if ('better-sqlite3' in deps || '@libsql/client' in deps)
|
|
59
|
+
stack.db = 'sqlite';
|
|
60
|
+
else if ('pg' in deps || '@neondatabase/serverless' in deps)
|
|
61
|
+
stack.db = 'postgres';
|
|
62
|
+
else if ('mysql2' in deps)
|
|
63
|
+
stack.db = 'mysql';
|
|
64
|
+
else if ('mongodb' in deps || 'mongoose' in deps)
|
|
65
|
+
stack.db = 'mongodb';
|
|
66
|
+
// Detect Framework
|
|
67
|
+
if ('@tanstack/react-router' in deps || '@tanstack/start' in deps)
|
|
68
|
+
stack.framework = 'tanstack-start';
|
|
69
|
+
else if ('next' in deps)
|
|
70
|
+
stack.framework = 'nextjs';
|
|
71
|
+
return stack;
|
|
72
|
+
}
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// Main Codegen Function
|
|
75
|
+
// =============================================================================
|
|
76
|
+
export function codegen(input, packageJson) {
|
|
77
|
+
// Parse all types from natural language
|
|
78
|
+
const parsed = parseNaturalLanguage(input.describe);
|
|
79
|
+
const formParsed = parseFormDescription(input.describe);
|
|
80
|
+
const componentParsed = parseComponentDescription(input.describe);
|
|
81
|
+
const pagesParsed = parsePageDescription(input.describe);
|
|
82
|
+
// Check if we parsed anything useful
|
|
83
|
+
const hasEntities = parsed.success && parsed.entities.length > 0;
|
|
84
|
+
const hasForms = formParsed.success && formParsed.forms.length > 0;
|
|
85
|
+
const hasComponents = componentParsed.success && componentParsed.components.length > 0;
|
|
86
|
+
const hasPages = pagesParsed.length > 0;
|
|
87
|
+
if (!hasEntities && !hasForms && !hasComponents && !hasPages) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
files: [],
|
|
91
|
+
entities: [],
|
|
92
|
+
summary: 'Failed to parse description',
|
|
93
|
+
errors: ['Could not understand the description. Try:\n Entity: "User with email, password, role (admin/user)"\n Form: "Login form with email, password"\n Component: "Button with label, onClick, variant (primary/secondary)"\n Page: "Dashboard page at /dashboard, requires auth"'],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// Detect or use provided stack
|
|
97
|
+
const detected = detectStack(packageJson);
|
|
98
|
+
const stack = {
|
|
99
|
+
orm: input.stack?.orm || detected.orm || 'drizzle',
|
|
100
|
+
api: input.stack?.api || detected.api || 'hono',
|
|
101
|
+
db: input.stack?.db || detected.db || 'sqlite',
|
|
102
|
+
ui: input.stack?.ui || detected.ui || 'radix',
|
|
103
|
+
framework: detected.framework || 'tanstack-start',
|
|
104
|
+
lang: detected.lang,
|
|
105
|
+
};
|
|
106
|
+
const files = [];
|
|
107
|
+
const toGenerate = input.generate || ['schema', 'types', 'validators'];
|
|
108
|
+
const generateAll = toGenerate.includes('all');
|
|
109
|
+
// Generate schema (Drizzle)
|
|
110
|
+
if (generateAll || toGenerate.includes('schema')) {
|
|
111
|
+
if (stack.orm === 'drizzle') {
|
|
112
|
+
// Map 'postgres' to 'postgresql' for Drizzle dialect
|
|
113
|
+
const dialectMap = {
|
|
114
|
+
sqlite: 'sqlite',
|
|
115
|
+
postgres: 'postgresql',
|
|
116
|
+
postgresql: 'postgresql',
|
|
117
|
+
mysql: 'mysql',
|
|
118
|
+
};
|
|
119
|
+
const drizzle = generateDrizzleSchema(parsed.entities, {
|
|
120
|
+
dialect: dialectMap[stack.db] || 'sqlite',
|
|
121
|
+
});
|
|
122
|
+
files.push({
|
|
123
|
+
path: 'src/db/schema.ts',
|
|
124
|
+
content: drizzle.schema,
|
|
125
|
+
description: 'Drizzle ORM schema',
|
|
126
|
+
});
|
|
127
|
+
if (drizzle.relations) {
|
|
128
|
+
files.push({
|
|
129
|
+
path: 'src/db/relations.ts',
|
|
130
|
+
content: drizzle.relations,
|
|
131
|
+
description: 'Drizzle relations',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (drizzle.types) {
|
|
135
|
+
files.push({
|
|
136
|
+
path: 'src/db/types.ts',
|
|
137
|
+
content: drizzle.types,
|
|
138
|
+
description: 'Drizzle inferred types',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// TODO: Add prisma, typeorm, mongoose
|
|
143
|
+
}
|
|
144
|
+
// Generate TypeScript types
|
|
145
|
+
if (generateAll || toGenerate.includes('types')) {
|
|
146
|
+
const types = generateTypeScriptTypes(parsed.entities);
|
|
147
|
+
files.push({
|
|
148
|
+
path: 'src/types/entities.ts',
|
|
149
|
+
content: types.types,
|
|
150
|
+
description: 'TypeScript entity types',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Generate Zod validators
|
|
154
|
+
if (generateAll || toGenerate.includes('validators')) {
|
|
155
|
+
const zod = generateZodSchemas(parsed.entities);
|
|
156
|
+
files.push({
|
|
157
|
+
path: 'src/validators/schemas.ts',
|
|
158
|
+
content: zod.schemas,
|
|
159
|
+
description: 'Zod validation schemas',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// Generate API routes
|
|
163
|
+
if (generateAll || toGenerate.includes('api')) {
|
|
164
|
+
const apis = createAPIsFromEntities(parsed.entities);
|
|
165
|
+
const api = generateAPI(apis, { framework: stack.api });
|
|
166
|
+
files.push({
|
|
167
|
+
path: 'src/routes/index.ts',
|
|
168
|
+
content: api.routes,
|
|
169
|
+
description: `${stack.api} API routes`,
|
|
170
|
+
});
|
|
171
|
+
if (api.validation) {
|
|
172
|
+
files.push({
|
|
173
|
+
path: 'src/routes/validation.ts',
|
|
174
|
+
content: api.validation,
|
|
175
|
+
description: 'Route validation schemas',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Generate Vitest tests
|
|
180
|
+
if (generateAll || toGenerate.includes('tests')) {
|
|
181
|
+
for (const entity of parsed.entities) {
|
|
182
|
+
const tests = generateEntityTests(entity, {
|
|
183
|
+
factories: true,
|
|
184
|
+
mocks: true,
|
|
185
|
+
provenance: true,
|
|
186
|
+
});
|
|
187
|
+
const entityName = entity.name.toLowerCase();
|
|
188
|
+
files.push({
|
|
189
|
+
path: `__tests__/${entityName}.test.ts`,
|
|
190
|
+
content: tests.tests,
|
|
191
|
+
description: `Vitest unit tests for ${entity.name}`,
|
|
192
|
+
});
|
|
193
|
+
if (tests.factories) {
|
|
194
|
+
files.push({
|
|
195
|
+
path: `__tests__/${entityName}.factory.ts`,
|
|
196
|
+
content: tests.factories,
|
|
197
|
+
description: `Test factory for ${entity.name}`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (tests.mocks) {
|
|
201
|
+
files.push({
|
|
202
|
+
path: `__tests__/${entityName}.mock.ts`,
|
|
203
|
+
content: tests.mocks,
|
|
204
|
+
description: `Mock repository for ${entity.name}`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Generate React components (Radix UI)
|
|
210
|
+
if (generateAll || toGenerate.includes('components')) {
|
|
211
|
+
if (hasComponents) {
|
|
212
|
+
const radixResult = generateRadixComponents(componentParsed.components, {
|
|
213
|
+
typescript: true,
|
|
214
|
+
provenance: true,
|
|
215
|
+
a11yDocs: true,
|
|
216
|
+
});
|
|
217
|
+
for (const component of radixResult.components) {
|
|
218
|
+
files.push({
|
|
219
|
+
path: component.path,
|
|
220
|
+
content: component.content,
|
|
221
|
+
description: `Radix UI ${component.primitive} component`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// TODO: Add shadcn, mantine generation when available
|
|
226
|
+
}
|
|
227
|
+
// Generate Pages (TanStack Start / Next.js)
|
|
228
|
+
if (generateAll || toGenerate.includes('pages')) {
|
|
229
|
+
if (hasPages) {
|
|
230
|
+
const framework = stack.framework || 'tanstack-start';
|
|
231
|
+
const pagesResult = generatePages(pagesParsed, {
|
|
232
|
+
framework: framework,
|
|
233
|
+
typescript: stack.lang === 'typescript',
|
|
234
|
+
provenance: true,
|
|
235
|
+
});
|
|
236
|
+
for (const page of pagesResult.pages) {
|
|
237
|
+
files.push({
|
|
238
|
+
path: page.path,
|
|
239
|
+
content: page.content,
|
|
240
|
+
description: `${framework} page: ${page.path}`,
|
|
241
|
+
});
|
|
242
|
+
if (page.loader) {
|
|
243
|
+
files.push({
|
|
244
|
+
path: page.loader.path,
|
|
245
|
+
content: page.loader.content,
|
|
246
|
+
description: `Page loader for ${page.path}`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (pagesResult.layouts) {
|
|
251
|
+
for (const layout of pagesResult.layouts) {
|
|
252
|
+
files.push({
|
|
253
|
+
path: layout.path,
|
|
254
|
+
content: layout.content,
|
|
255
|
+
description: `Shared layout component`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Generate forms (React Hook Form + Zod)
|
|
262
|
+
if (generateAll || toGenerate.includes('forms')) {
|
|
263
|
+
if (hasForms) {
|
|
264
|
+
const ui = stack.ui === 'shadcn' || stack.ui === 'radix' ? 'shadcn' : 'plain';
|
|
265
|
+
const formsResult = generateForms(formParsed.forms, {
|
|
266
|
+
ui,
|
|
267
|
+
typescript: true,
|
|
268
|
+
provenance: true,
|
|
269
|
+
zod: true,
|
|
270
|
+
});
|
|
271
|
+
for (const form of formsResult.forms) {
|
|
272
|
+
files.push({
|
|
273
|
+
path: form.path,
|
|
274
|
+
content: form.content,
|
|
275
|
+
description: `React Hook Form component`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const entityNames = parsed.entities.map(e => e.name);
|
|
281
|
+
const formNames = formParsed.forms.map(f => f.name);
|
|
282
|
+
const allNames = [...entityNames, ...formNames.map(n => `${n} form`)];
|
|
283
|
+
return {
|
|
284
|
+
success: true,
|
|
285
|
+
files,
|
|
286
|
+
entities: entityNames,
|
|
287
|
+
summary: `Generated ${files.length} files for ${allNames.join(', ')}`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Create API blocks from entities (CRUD for each)
|
|
292
|
+
*/
|
|
293
|
+
function createAPIsFromEntities(entities) {
|
|
294
|
+
return entities.map(entity => ({
|
|
295
|
+
kind: 'APIBlock',
|
|
296
|
+
name: `${entity.name}API`,
|
|
297
|
+
operations: [],
|
|
298
|
+
crud: [{
|
|
299
|
+
kind: 'CRUDShorthand',
|
|
300
|
+
entity: entity.name,
|
|
301
|
+
actions: [
|
|
302
|
+
{ action: 'list', effects: ['DB'] },
|
|
303
|
+
{ action: 'create', effects: ['DB'] },
|
|
304
|
+
{ action: 'read', effects: ['DB'] },
|
|
305
|
+
{ action: 'update', effects: ['DB'] },
|
|
306
|
+
{ action: 'delete', effects: ['DB'] },
|
|
307
|
+
],
|
|
308
|
+
loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
|
|
309
|
+
}],
|
|
310
|
+
loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type InferredType = {
|
|
2
|
+
type: string;
|
|
3
|
+
modifiers: string[];
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Infer field type from field name
|
|
7
|
+
*/
|
|
8
|
+
export declare function inferFieldType(fieldName: string): InferredType;
|
|
9
|
+
/**
|
|
10
|
+
* Parse explicit type annotation if present
|
|
11
|
+
* Examples: "role:enum(admin,user)", "age:int", "bio:text:optional"
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseExplicitType(spec: string): {
|
|
14
|
+
name: string;
|
|
15
|
+
type: InferredType;
|
|
16
|
+
} | null;
|
|
17
|
+
/**
|
|
18
|
+
* Parse enum from parenthetical notation
|
|
19
|
+
* Example: "role (admin, user, guest)" → { name: 'role', values: ['admin', 'user', 'guest'] }
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseInlineEnum(text: string): {
|
|
22
|
+
name: string;
|
|
23
|
+
values: string[];
|
|
24
|
+
} | null;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Smart Field Inference
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Maps common field names to appropriate types automatically.
|
|
6
|
+
// No DSL syntax needed - just say "User with email, password, createdAt"
|
|
7
|
+
//
|
|
8
|
+
// Field name patterns → type mappings
|
|
9
|
+
const FIELD_PATTERNS = [
|
|
10
|
+
// Identity - always uuid primary
|
|
11
|
+
[/^id$/i, { type: 'uuid', modifiers: ['primary', 'auto'] }],
|
|
12
|
+
[/^uuid$/i, { type: 'uuid', modifiers: ['primary', 'auto'] }],
|
|
13
|
+
// Email fields
|
|
14
|
+
[/email/i, { type: 'email', modifiers: ['unique'] }],
|
|
15
|
+
// Password - always hashed
|
|
16
|
+
[/^password$/i, { type: 'string', modifiers: ['hashed'] }],
|
|
17
|
+
[/^passwordHash$/i, { type: 'string', modifiers: [] }],
|
|
18
|
+
// URLs
|
|
19
|
+
[/^url$|Url$|^link$|Link$|^href$/i, { type: 'url', modifiers: [] }],
|
|
20
|
+
[/^avatar$|^image$|^photo$|^picture$/i, { type: 'url', modifiers: ['optional'] }],
|
|
21
|
+
// Phone
|
|
22
|
+
[/phone|mobile|cell/i, { type: 'phone', modifiers: ['optional'] }],
|
|
23
|
+
// Slugs
|
|
24
|
+
[/^slug$/i, { type: 'slug', modifiers: ['unique'] }],
|
|
25
|
+
// Timestamps - auto-managed
|
|
26
|
+
[/^createdAt$|^created_at$/i, { type: 'timestamp', modifiers: ['auto'] }],
|
|
27
|
+
[/^updatedAt$|^updated_at$/i, { type: 'timestamp', modifiers: ['auto'] }],
|
|
28
|
+
[/^deletedAt$|^deleted_at$/i, { type: 'timestamp', modifiers: ['optional'] }],
|
|
29
|
+
[/^publishedAt$|^published_at$/i, { type: 'timestamp', modifiers: ['optional'] }],
|
|
30
|
+
[/At$|_at$/i, { type: 'timestamp', modifiers: ['optional'] }],
|
|
31
|
+
// Booleans - common patterns
|
|
32
|
+
[/^is[A-Z]|^has[A-Z]|^can[A-Z]|^should[A-Z]/i, { type: 'bool', modifiers: [] }],
|
|
33
|
+
[/^active$|^enabled$|^published$|^verified$|^approved$/i, { type: 'bool', modifiers: [] }],
|
|
34
|
+
// Counts/numbers
|
|
35
|
+
[/count$|Count$|^total|Total$|^num|Num$|^amount|Amount$/i, { type: 'int', modifiers: [] }],
|
|
36
|
+
[/^age$|^order$|^priority$|^position$|^rank$/i, { type: 'int', modifiers: [] }],
|
|
37
|
+
[/^price$|^cost$|^amount$|^balance$|^rate$/i, { type: 'float', modifiers: [] }],
|
|
38
|
+
// Long text content
|
|
39
|
+
[/^content$|^body$|^text$|^description$|^summary$/i, { type: 'markdown', modifiers: [] }],
|
|
40
|
+
[/^bio$|^about$|^notes$/i, { type: 'markdown', modifiers: ['optional'] }],
|
|
41
|
+
// JSON data
|
|
42
|
+
[/^data$|^meta$|^metadata$|^settings$|^config$|^options$/i, { type: 'json', modifiers: ['optional'] }],
|
|
43
|
+
];
|
|
44
|
+
// Default: string
|
|
45
|
+
const DEFAULT_TYPE = { type: 'string', modifiers: [] };
|
|
46
|
+
/**
|
|
47
|
+
* Infer field type from field name
|
|
48
|
+
*/
|
|
49
|
+
export function inferFieldType(fieldName) {
|
|
50
|
+
for (const [pattern, inferredType] of FIELD_PATTERNS) {
|
|
51
|
+
if (pattern.test(fieldName)) {
|
|
52
|
+
return inferredType;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return DEFAULT_TYPE;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse explicit type annotation if present
|
|
59
|
+
* Examples: "role:enum(admin,user)", "age:int", "bio:text:optional"
|
|
60
|
+
*/
|
|
61
|
+
export function parseExplicitType(spec) {
|
|
62
|
+
// Clean up: remove trailing punctuation and whitespace
|
|
63
|
+
const cleaned = spec.trim().replace(/[.,;!?]+$/, '');
|
|
64
|
+
// Match: fieldName:type or fieldName:type(args) or fieldName:type:modifier
|
|
65
|
+
const match = cleaned.match(/^(\w+):(\w+)(?:\(([^)]+)\))?(?::(\w+))?$/);
|
|
66
|
+
if (!match)
|
|
67
|
+
return null;
|
|
68
|
+
const [, name, type, args, modifier] = match;
|
|
69
|
+
const modifiers = modifier ? [modifier] : [];
|
|
70
|
+
// Handle enum
|
|
71
|
+
if (type === 'enum' && args) {
|
|
72
|
+
return {
|
|
73
|
+
name,
|
|
74
|
+
type: {
|
|
75
|
+
type: `enum:${args}`,
|
|
76
|
+
modifiers,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
type: {
|
|
83
|
+
type,
|
|
84
|
+
modifiers,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Parse enum from parenthetical notation
|
|
90
|
+
* Example: "role (admin, user, guest)" → { name: 'role', values: ['admin', 'user', 'guest'] }
|
|
91
|
+
*/
|
|
92
|
+
export function parseInlineEnum(text) {
|
|
93
|
+
const match = text.match(/^(\w+)\s*\(([^)]+)\)$/);
|
|
94
|
+
if (!match)
|
|
95
|
+
return null;
|
|
96
|
+
const [, name, valuesStr] = match;
|
|
97
|
+
const values = valuesStr.split(/[,\/|]/).map(v => v.trim()).filter(Boolean);
|
|
98
|
+
if (values.length < 2)
|
|
99
|
+
return null; // Not an enum, probably a function
|
|
100
|
+
return { name, values };
|
|
101
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FormBlock } from '../codedna/kappa-ast.js';
|
|
2
|
+
export interface ParsedForm {
|
|
3
|
+
form: FormBlock;
|
|
4
|
+
}
|
|
5
|
+
export interface FormParseResult {
|
|
6
|
+
success: boolean;
|
|
7
|
+
forms: FormBlock[];
|
|
8
|
+
errors: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse natural language form description into FormBlock
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseFormDescription(description: string): FormParseResult;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Natural Language → Form AST Parser
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Converts natural language form descriptions into Kappa FormBlock AST.
|
|
6
|
+
// Examples:
|
|
7
|
+
// "Login form with email, password"
|
|
8
|
+
// "User registration with name, email, password, confirm password"
|
|
9
|
+
// "Contact form with name, email, subject, message"
|
|
10
|
+
//
|
|
11
|
+
const defaultLoc = {
|
|
12
|
+
startLine: 1,
|
|
13
|
+
startColumn: 1,
|
|
14
|
+
endLine: 1,
|
|
15
|
+
endColumn: 1,
|
|
16
|
+
startOffset: 0,
|
|
17
|
+
endOffset: 0,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Parse natural language form description into FormBlock
|
|
21
|
+
*/
|
|
22
|
+
export function parseFormDescription(description) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
const forms = [];
|
|
25
|
+
try {
|
|
26
|
+
// Extract form name (e.g., "Login form", "User registration", "Contact form")
|
|
27
|
+
const nameMatch = description.match(/^([A-Za-z\s]+?)(?:\s+form|\s+with)/i);
|
|
28
|
+
if (!nameMatch) {
|
|
29
|
+
throw new Error('Could not extract form name. Try: "Login form with email, password"');
|
|
30
|
+
}
|
|
31
|
+
const formName = nameMatch[1].trim()
|
|
32
|
+
.split(/\s+/)
|
|
33
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
34
|
+
.join('');
|
|
35
|
+
// Extract fields (after "with")
|
|
36
|
+
const fieldsMatch = description.match(/with\s+(.+)$/i);
|
|
37
|
+
if (!fieldsMatch) {
|
|
38
|
+
throw new Error('Could not find fields. Use format: "FormName with field1, field2"');
|
|
39
|
+
}
|
|
40
|
+
const fieldsStr = fieldsMatch[1];
|
|
41
|
+
const fields = parseFormFields(fieldsStr);
|
|
42
|
+
// Determine submit button text based on form type
|
|
43
|
+
const submitText = inferSubmitText(formName, description);
|
|
44
|
+
const form = {
|
|
45
|
+
kind: 'FormBlock',
|
|
46
|
+
name: formName,
|
|
47
|
+
fields,
|
|
48
|
+
submit: {
|
|
49
|
+
kind: 'FormSubmit',
|
|
50
|
+
action: 'submit',
|
|
51
|
+
button: submitText,
|
|
52
|
+
loading: `${submitText.replace(/^(.*?)(?:ing)?$/, '$1ing')}...`,
|
|
53
|
+
onSuccess: 'console.log("Form submitted successfully")',
|
|
54
|
+
onError: 'console.error("Form submission failed", error)',
|
|
55
|
+
loc: defaultLoc,
|
|
56
|
+
},
|
|
57
|
+
layout: [],
|
|
58
|
+
loc: defaultLoc,
|
|
59
|
+
};
|
|
60
|
+
forms.push(form);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
errors.push(e instanceof Error ? e.message : String(e));
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
success: errors.length === 0 && forms.length > 0,
|
|
67
|
+
forms,
|
|
68
|
+
errors,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse comma-separated form fields
|
|
73
|
+
*/
|
|
74
|
+
function parseFormFields(fieldsStr) {
|
|
75
|
+
const fields = [];
|
|
76
|
+
const parts = fieldsStr.split(',').map(s => s.trim());
|
|
77
|
+
for (const part of parts) {
|
|
78
|
+
if (!part)
|
|
79
|
+
continue;
|
|
80
|
+
const field = createFormField(part);
|
|
81
|
+
fields.push(field);
|
|
82
|
+
}
|
|
83
|
+
return fields;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create a FormField from a field name
|
|
87
|
+
*/
|
|
88
|
+
function createFormField(fieldName) {
|
|
89
|
+
const normalizedName = fieldName.replace(/\s+/g, '');
|
|
90
|
+
const label = fieldName
|
|
91
|
+
.split(/\s+/)
|
|
92
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
93
|
+
.join(' ');
|
|
94
|
+
// Infer field type from name
|
|
95
|
+
const { type, required, placeholder } = inferFormFieldType(normalizedName);
|
|
96
|
+
return {
|
|
97
|
+
kind: 'FormField',
|
|
98
|
+
name: normalizedName,
|
|
99
|
+
label,
|
|
100
|
+
type,
|
|
101
|
+
required,
|
|
102
|
+
placeholder,
|
|
103
|
+
options: type === 'select' ? [] : undefined,
|
|
104
|
+
loc: defaultLoc,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Infer form field type from field name
|
|
109
|
+
*/
|
|
110
|
+
function inferFormFieldType(fieldName) {
|
|
111
|
+
const lower = fieldName.toLowerCase();
|
|
112
|
+
// Email
|
|
113
|
+
if (lower.includes('email')) {
|
|
114
|
+
return { type: 'email', required: true, placeholder: 'your@email.com' };
|
|
115
|
+
}
|
|
116
|
+
// Password
|
|
117
|
+
if (lower.includes('password')) {
|
|
118
|
+
return { type: 'password', required: true, placeholder: '••••••••' };
|
|
119
|
+
}
|
|
120
|
+
// Number/Age
|
|
121
|
+
if (lower.includes('age') || lower.includes('count') || lower.includes('quantity')) {
|
|
122
|
+
return { type: 'number', required: false };
|
|
123
|
+
}
|
|
124
|
+
// Date
|
|
125
|
+
if (lower.includes('date') || lower.includes('birthday') || lower.includes('dob')) {
|
|
126
|
+
return { type: 'date', required: false };
|
|
127
|
+
}
|
|
128
|
+
// Time
|
|
129
|
+
if (lower.includes('time')) {
|
|
130
|
+
return { type: 'time', required: false };
|
|
131
|
+
}
|
|
132
|
+
// Textarea (for longer content)
|
|
133
|
+
if (lower.includes('message') ||
|
|
134
|
+
lower.includes('description') ||
|
|
135
|
+
lower.includes('bio') ||
|
|
136
|
+
lower.includes('comment') ||
|
|
137
|
+
lower.includes('content')) {
|
|
138
|
+
return { type: 'textarea', required: false, placeholder: 'Enter your message...' };
|
|
139
|
+
}
|
|
140
|
+
// Checkbox
|
|
141
|
+
if (lower.includes('agree') ||
|
|
142
|
+
lower.includes('accept') ||
|
|
143
|
+
lower.includes('consent') ||
|
|
144
|
+
lower.includes('subscribe')) {
|
|
145
|
+
return { type: 'checkbox', required: false };
|
|
146
|
+
}
|
|
147
|
+
// Phone
|
|
148
|
+
if (lower.includes('phone') || lower.includes('mobile')) {
|
|
149
|
+
return { type: 'text', required: false, placeholder: '+1 (555) 123-4567' };
|
|
150
|
+
}
|
|
151
|
+
// URL
|
|
152
|
+
if (lower.includes('website') || lower.includes('url')) {
|
|
153
|
+
return { type: 'text', required: false, placeholder: 'https://example.com' };
|
|
154
|
+
}
|
|
155
|
+
// Default to text
|
|
156
|
+
return { type: 'text', required: false };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Infer submit button text from form name/description
|
|
160
|
+
*/
|
|
161
|
+
function inferSubmitText(formName, description) {
|
|
162
|
+
const lower = formName.toLowerCase();
|
|
163
|
+
const descLower = description.toLowerCase();
|
|
164
|
+
if (lower.includes('login') || lower.includes('signin')) {
|
|
165
|
+
return 'Sign In';
|
|
166
|
+
}
|
|
167
|
+
if (lower.includes('register') || lower.includes('signup') || descLower.includes('registration')) {
|
|
168
|
+
return 'Create Account';
|
|
169
|
+
}
|
|
170
|
+
if (lower.includes('contact')) {
|
|
171
|
+
return 'Send Message';
|
|
172
|
+
}
|
|
173
|
+
if (lower.includes('search')) {
|
|
174
|
+
return 'Search';
|
|
175
|
+
}
|
|
176
|
+
if (lower.includes('update') || lower.includes('edit')) {
|
|
177
|
+
return 'Update';
|
|
178
|
+
}
|
|
179
|
+
if (lower.includes('delete')) {
|
|
180
|
+
return 'Delete';
|
|
181
|
+
}
|
|
182
|
+
if (lower.includes('reset')) {
|
|
183
|
+
return 'Reset Password';
|
|
184
|
+
}
|
|
185
|
+
return 'Submit';
|
|
186
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Codegen Module - Cloud Only
|
|
3
|
+
// =============================================================================
|
|
4
|
+
export { cloudCodegen, CloudCodegenClient, defaultClient as cloudClient, } from './cloud-client.js';
|