@claudetools/tools 0.7.0 → 0.7.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.
@@ -2,18 +2,29 @@
2
2
  // Template Registry Client
3
3
  // =============================================================================
4
4
  //
5
- // HTTP client for Cloudflare Workers template registry with local caching
6
- // and fallback to bundled templates on network failure.
5
+ // HTTP client for Cloudflare Workers template registry.
6
+ // Cloudflare D1 is the single source of truth for generators.
7
+ // Local file cache provides offline fallback for templates only.
8
+ //
9
+ // Registry data structure:
10
+ // - generators: flat list with domain field (api/frontend/component)
11
+ // - byDomain: grouped by domain for easy filtering
12
+ // - summary: counts by domain
7
13
  //
8
14
  import fs from 'fs/promises';
9
15
  import path from 'path';
10
16
  import { fileURLToPath } from 'url';
11
17
  import { errorTracker } from '../helpers/error-tracking.js';
12
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ // Cache TTL in milliseconds (1 hour for registry, 24 hours for templates)
20
+ const REGISTRY_CACHE_TTL = 60 * 60 * 1000;
21
+ const TEMPLATE_CACHE_TTL = 24 * 60 * 60 * 1000;
13
22
  export class TemplateRegistry {
14
23
  baseUrl;
15
24
  cacheDir;
16
25
  useCache;
26
+ // In-memory cache for registry data
27
+ registryCache = null;
17
28
  constructor(baseUrl = 'https://api.claudetools.dev/api/v1/codedna', useCache = true) {
18
29
  this.baseUrl = baseUrl;
19
30
  this.cacheDir = path.join(__dirname, '../../templates');
@@ -21,21 +32,54 @@ export class TemplateRegistry {
21
32
  }
22
33
  /**
23
34
  * List all available generators
35
+ * Fetches from Cloudflare D1 (single source of truth)
36
+ * @param domain Optional domain filter: 'api' | 'frontend' | 'component'
24
37
  */
25
- async listGenerators() {
38
+ async listGenerators(domain) {
39
+ // Check in-memory cache first
40
+ if (this.registryCache && Date.now() - this.registryCache.timestamp < REGISTRY_CACHE_TTL) {
41
+ const cached = this.registryCache.data;
42
+ if (domain) {
43
+ return cached.filter(g => g.domain === domain);
44
+ }
45
+ return cached;
46
+ }
26
47
  try {
27
- const response = await fetch(`${this.baseUrl}/registry`, {
28
- signal: AbortSignal.timeout(5000), // 5 second timeout
48
+ const url = domain
49
+ ? `${this.baseUrl}/registry?domain=${domain}`
50
+ : `${this.baseUrl}/registry`;
51
+ const response = await fetch(url, {
52
+ signal: AbortSignal.timeout(5000),
29
53
  });
30
54
  if (!response.ok) {
31
55
  throw new Error(`Registry fetch failed: ${response.statusText}`);
32
56
  }
33
- const data = await response.json();
34
- return data.generators;
57
+ const result = await response.json();
58
+ if (!result.success || !result.data?.generators) {
59
+ throw new Error('Invalid registry response');
60
+ }
61
+ // Cache the full list (not filtered)
62
+ if (!domain) {
63
+ this.registryCache = {
64
+ data: result.data.generators,
65
+ timestamp: Date.now(),
66
+ };
67
+ }
68
+ return result.data.generators;
35
69
  }
36
70
  catch (error) {
37
- console.warn('Failed to fetch from registry, using local fallback:', error);
38
- return this.getLocalGenerators();
71
+ console.warn('Failed to fetch from registry:', error);
72
+ // Try to use stale cache if available
73
+ if (this.registryCache) {
74
+ console.warn('Using stale registry cache');
75
+ const stale = this.registryCache.data;
76
+ if (domain) {
77
+ return stale.filter(g => g.domain === domain);
78
+ }
79
+ return stale;
80
+ }
81
+ // No cache available - throw error (Cloudflare is required)
82
+ throw new Error('Registry unavailable and no cache. Please check your network connection or try again later.');
39
83
  }
40
84
  }
41
85
  /**
@@ -47,49 +91,64 @@ export class TemplateRegistry {
47
91
  signal: AbortSignal.timeout(5000),
48
92
  });
49
93
  if (!response.ok) {
94
+ if (response.status === 404) {
95
+ throw new Error(`Generator '${generatorId}' not found`);
96
+ }
50
97
  throw new Error(`Metadata fetch failed: ${response.statusText}`);
51
98
  }
52
- return await response.json();
99
+ const result = await response.json();
100
+ return result.data || result;
53
101
  }
54
102
  catch (error) {
55
- console.warn('Failed to fetch metadata from registry, using local fallback:', error);
56
- return this.getLocalMetadata(generatorId);
103
+ // Try to find in cached registry
104
+ if (this.registryCache) {
105
+ const cached = this.registryCache.data.find(g => g.id === generatorId);
106
+ if (cached) {
107
+ console.warn('Using cached metadata for:', generatorId);
108
+ return cached;
109
+ }
110
+ }
111
+ throw error;
57
112
  }
58
113
  }
59
114
  /**
60
115
  * Get template file content
61
116
  */
62
117
  async getTemplate(generatorId, templateFile) {
63
- // Check cache first
118
+ // Check file cache first
64
119
  if (this.useCache) {
65
120
  const cached = await this.getCachedTemplate(generatorId, templateFile);
66
121
  if (cached)
67
122
  return cached;
68
123
  }
69
- // Fetch from registry
124
+ // Fetch from Cloudflare
70
125
  try {
71
126
  const response = await fetch(`${this.baseUrl}/templates/${generatorId}/${templateFile}`, {
72
- signal: AbortSignal.timeout(10000), // 10 second timeout for templates
127
+ signal: AbortSignal.timeout(10000),
73
128
  });
74
129
  if (!response.ok) {
130
+ if (response.status === 404) {
131
+ throw new Error(`Template '${templateFile}' not found for generator '${generatorId}'`);
132
+ }
75
133
  throw new Error(`Template fetch failed: ${response.statusText}`);
76
134
  }
77
135
  const content = await response.text();
78
- // Cache it
136
+ // Cache locally for offline use
79
137
  if (this.useCache) {
80
138
  await this.cacheTemplate(generatorId, templateFile, content);
81
139
  }
82
140
  return content;
83
141
  }
84
142
  catch (error) {
85
- console.warn('Failed to fetch template from registry, using local fallback:', error);
86
- // Track template fetch error
143
+ console.warn('Failed to fetch template:', error);
144
+ // Track fetch error
87
145
  await errorTracker.trackTemplateFetchError(generatorId, templateFile, error instanceof Error ? error : new Error(String(error)));
146
+ // Try local bundled fallback
88
147
  return this.getLocalTemplate(generatorId, templateFile);
89
148
  }
90
149
  }
91
150
  /**
92
- * Get multiple templates at once
151
+ * Get multiple templates at once (parallel fetch)
93
152
  */
94
153
  async getTemplates(generatorId, templateFiles) {
95
154
  const results = {};
@@ -99,11 +158,16 @@ export class TemplateRegistry {
99
158
  return results;
100
159
  }
101
160
  /**
102
- * Check if template is cached locally
161
+ * Get cached template from file system
103
162
  */
104
163
  async getCachedTemplate(generatorId, templateFile) {
105
164
  try {
106
165
  const cachePath = path.join(this.cacheDir, generatorId, templateFile);
166
+ const stats = await fs.stat(cachePath);
167
+ // Check if cache is still valid
168
+ if (Date.now() - stats.mtimeMs > TEMPLATE_CACHE_TTL) {
169
+ return null; // Cache expired
170
+ }
107
171
  return await fs.readFile(cachePath, 'utf-8');
108
172
  }
109
173
  catch {
@@ -111,7 +175,7 @@ export class TemplateRegistry {
111
175
  }
112
176
  }
113
177
  /**
114
- * Cache template locally
178
+ * Cache template to file system
115
179
  */
116
180
  async cacheTemplate(generatorId, templateFile, content) {
117
181
  try {
@@ -124,245 +188,24 @@ export class TemplateRegistry {
124
188
  }
125
189
  }
126
190
  /**
127
- * Get local generators (fallback)
128
- */
129
- async getLocalGenerators() {
130
- // Return hardcoded list of bundled generators
131
- return [
132
- {
133
- id: 'express-api',
134
- name: 'Express REST API',
135
- framework: 'express',
136
- description: 'Generate TypeScript Express API with CRUD operations',
137
- features: ['auth', 'validation', 'tests'],
138
- databases: ['postgresql', 'mysql', 'mongodb'],
139
- version: '1.0.0',
140
- templates: [
141
- 'controller.ts.j2',
142
- 'model.ts.j2',
143
- 'route.ts.j2',
144
- 'middleware.ts.j2',
145
- 'validator.ts.j2',
146
- ],
147
- },
148
- {
149
- id: 'fastapi-api',
150
- name: 'FastAPI REST API',
151
- framework: 'fastapi',
152
- description: 'Generate Python FastAPI with Pydantic models and SQLAlchemy ORM',
153
- features: ['auth', 'validation', 'tests'],
154
- databases: ['postgresql', 'mysql', 'mongodb'],
155
- version: '1.0.0',
156
- templates: [
157
- 'model.py.j2',
158
- 'schemas.py.j2',
159
- 'router.py.j2',
160
- 'crud.py.j2',
161
- 'test_api.py.j2',
162
- ],
163
- },
164
- {
165
- id: 'nestjs-api',
166
- name: 'NestJS REST API',
167
- framework: 'nestjs',
168
- description: 'Generate TypeScript NestJS with TypeORM entities and DTOs',
169
- features: ['auth', 'validation', 'tests'],
170
- databases: ['postgresql', 'mysql', 'mongodb'],
171
- version: '1.0.0',
172
- templates: [
173
- 'entity.ts.j2',
174
- 'dto.ts.j2',
175
- 'controller.ts.j2',
176
- 'service.ts.j2',
177
- 'module.ts.j2',
178
- 'spec.ts.j2',
179
- ],
180
- },
181
- {
182
- id: 'react-frontend',
183
- name: 'React/Next.js Frontend',
184
- framework: 'react',
185
- description: 'Generate React components with ShadcnUI for forms, tables, and pages',
186
- features: ['forms', 'tables', 'routing'],
187
- databases: [],
188
- version: '1.0.0',
189
- templates: [
190
- 'form.tsx.j2',
191
- 'table.tsx.j2',
192
- 'list-page.tsx.j2',
193
- 'detail-page.tsx.j2',
194
- 'hooks.ts.j2',
195
- ],
196
- },
197
- {
198
- id: 'vue-frontend',
199
- name: 'Vue 3 Frontend',
200
- framework: 'vue',
201
- description: 'Generate Vue 3 components with Composition API for forms, tables, and views',
202
- features: ['forms', 'tables', 'routing'],
203
- databases: [],
204
- version: '1.0.0',
205
- templates: [
206
- 'Form.vue.j2',
207
- 'Table.vue.j2',
208
- 'ListView.vue.j2',
209
- 'DetailView.vue.j2',
210
- 'composables.ts.j2',
211
- ],
212
- },
213
- // Component generators
214
- {
215
- id: 'react-form',
216
- name: 'React Form Component',
217
- framework: 'react',
218
- description: 'Generate React form with Zod validation and ShadcnUI',
219
- features: ['validation'],
220
- databases: [],
221
- version: '1.0.0',
222
- templates: ['form.tsx.j2'],
223
- },
224
- {
225
- id: 'react-table',
226
- name: 'React Table Component',
227
- framework: 'react',
228
- description: 'Generate React data table with ShadcnUI',
229
- features: [],
230
- databases: [],
231
- version: '1.0.0',
232
- templates: ['table.tsx.j2'],
233
- },
234
- {
235
- id: 'react-card',
236
- name: 'React Card Component',
237
- framework: 'react',
238
- description: 'Generate React card component with ShadcnUI',
239
- features: [],
240
- databases: [],
241
- version: '1.0.0',
242
- templates: ['card.tsx.j2'],
243
- },
244
- {
245
- id: 'react-modal',
246
- name: 'React Modal Component',
247
- framework: 'react',
248
- description: 'Generate React modal/dialog with ShadcnUI',
249
- features: [],
250
- databases: [],
251
- version: '1.0.0',
252
- templates: ['modal.tsx.j2'],
253
- },
254
- {
255
- id: 'vue-form',
256
- name: 'Vue Form Component',
257
- framework: 'vue',
258
- description: 'Generate Vue 3 form with Vuelidate',
259
- features: ['validation'],
260
- databases: [],
261
- version: '1.0.0',
262
- templates: ['form.vue.j2'],
263
- },
264
- {
265
- id: 'vue-table',
266
- name: 'Vue Table Component',
267
- framework: 'vue',
268
- description: 'Generate Vue 3 data table component',
269
- features: [],
270
- databases: [],
271
- version: '1.0.0',
272
- templates: ['table.vue.j2'],
273
- },
274
- {
275
- id: 'vue-card',
276
- name: 'Vue Card Component',
277
- framework: 'vue',
278
- description: 'Generate Vue 3 card component',
279
- features: [],
280
- databases: [],
281
- version: '1.0.0',
282
- templates: ['card.vue.j2'],
283
- },
284
- {
285
- id: 'vue-modal',
286
- name: 'Vue Modal Component',
287
- framework: 'vue',
288
- description: 'Generate Vue 3 modal with Teleport',
289
- features: [],
290
- databases: [],
291
- version: '1.0.0',
292
- templates: ['modal.vue.j2'],
293
- },
294
- {
295
- id: 'svelte-form',
296
- name: 'Svelte Form Component',
297
- framework: 'svelte',
298
- description: 'Generate Svelte form with reactive bindings',
299
- features: ['validation'],
300
- databases: [],
301
- version: '1.0.0',
302
- templates: ['form.svelte.j2'],
303
- },
304
- {
305
- id: 'svelte-table',
306
- name: 'Svelte Table Component',
307
- framework: 'svelte',
308
- description: 'Generate Svelte data table component',
309
- features: [],
310
- databases: [],
311
- version: '1.0.0',
312
- templates: ['table.svelte.j2'],
313
- },
314
- {
315
- id: 'svelte-card',
316
- name: 'Svelte Card Component',
317
- framework: 'svelte',
318
- description: 'Generate Svelte card component',
319
- features: [],
320
- databases: [],
321
- version: '1.0.0',
322
- templates: ['card.svelte.j2'],
323
- },
324
- {
325
- id: 'svelte-modal',
326
- name: 'Svelte Modal Component',
327
- framework: 'svelte',
328
- description: 'Generate Svelte modal component',
329
- features: [],
330
- databases: [],
331
- version: '1.0.0',
332
- templates: ['modal.svelte.j2'],
333
- },
334
- ];
335
- }
336
- /**
337
- * Get local metadata (fallback)
338
- */
339
- async getLocalMetadata(generatorId) {
340
- const generators = await this.getLocalGenerators();
341
- const generator = generators.find(g => g.id === generatorId);
342
- if (!generator) {
343
- throw new Error(`Generator not found: ${generatorId}`);
344
- }
345
- return generator;
346
- }
347
- /**
348
- * Get local template (fallback)
191
+ * Get template from bundled local files (fallback)
349
192
  */
350
193
  async getLocalTemplate(generatorId, templateFile) {
351
- // Try to load from local bundled templates
352
194
  const localPath = path.join(__dirname, '../../../cloudflare/templates', generatorId, templateFile);
353
195
  try {
354
196
  return await fs.readFile(localPath, 'utf-8');
355
197
  }
356
- catch (error) {
198
+ catch {
357
199
  throw new Error(`Template not found: ${generatorId}/${templateFile}. Registry unavailable and local fallback missing.`);
358
200
  }
359
201
  }
360
202
  /**
361
- * Clear local cache
203
+ * Clear local template cache
362
204
  */
363
205
  async clearCache() {
364
206
  try {
365
207
  await fs.rm(this.cacheDir, { recursive: true, force: true });
208
+ this.registryCache = null;
366
209
  }
367
210
  catch (error) {
368
211
  console.warn('Failed to clear cache:', error);
@@ -372,32 +215,29 @@ export class TemplateRegistry {
372
215
  * Get cache statistics
373
216
  */
374
217
  async getCacheStats() {
218
+ const stats = {
219
+ registryCached: !!this.registryCache,
220
+ registryAge: this.registryCache ? Date.now() - this.registryCache.timestamp : null,
221
+ cachedGenerators: [],
222
+ totalFiles: 0,
223
+ totalSize: 0,
224
+ };
375
225
  try {
376
226
  const entries = await fs.readdir(this.cacheDir, { withFileTypes: true });
377
- const generators = entries.filter(e => e.isDirectory()).map(e => e.name);
378
- let totalFiles = 0;
379
- let totalSize = 0;
380
- for (const gen of generators) {
227
+ stats.cachedGenerators = entries.filter(e => e.isDirectory()).map(e => e.name);
228
+ for (const gen of stats.cachedGenerators) {
381
229
  const genPath = path.join(this.cacheDir, gen);
382
230
  const files = await fs.readdir(genPath);
383
- totalFiles += files.length;
231
+ stats.totalFiles += files.length;
384
232
  for (const file of files) {
385
- const stats = await fs.stat(path.join(genPath, file));
386
- totalSize += stats.size;
233
+ const fileStats = await fs.stat(path.join(genPath, file));
234
+ stats.totalSize += fileStats.size;
387
235
  }
388
236
  }
389
- return {
390
- cachedGenerators: generators,
391
- totalFiles,
392
- totalSize,
393
- };
394
237
  }
395
238
  catch {
396
- return {
397
- cachedGenerators: [],
398
- totalFiles: 0,
399
- totalSize: 0,
400
- };
239
+ // Cache directory doesn't exist yet
401
240
  }
241
+ return stats;
402
242
  }
403
243
  }
@@ -171,6 +171,9 @@ export class TemplateEngine {
171
171
  * Helper to build template context from EntitySpec
172
172
  */
173
173
  export function buildContext(entity, options = {}) {
174
+ // Pattern detection helpers
175
+ const patterns = options.patterns || { detected: [], preferred: [], avoid: [] };
176
+ const detectedPatternIds = patterns.detected?.map((p) => p.pattern_id) || [];
174
177
  return {
175
178
  entity,
176
179
  options,
@@ -179,5 +182,25 @@ export function buildContext(entity, options = {}) {
179
182
  hasUniqueFields: entity.fields.some(f => f.constraints.some(c => c.kind === 'unique')),
180
183
  hasHashedFields: entity.fields.some(f => f.constraints.some(c => c.kind === 'hashed')),
181
184
  hasReferences: entity.fields.some(f => f.type.kind === 'reference'),
185
+ // Pattern context for templates
186
+ patterns: {
187
+ detected: patterns.detected || [],
188
+ preferred: patterns.preferred || [],
189
+ avoid: patterns.avoid || [],
190
+ // Quick lookup helpers
191
+ hasPattern: (patternId) => detectedPatternIds.includes(patternId),
192
+ usesZod: detectedPatternIds.includes('zod-validation'),
193
+ usesYup: detectedPatternIds.includes('yup-validation'),
194
+ usesReactHookForm: detectedPatternIds.includes('react-hook-form'),
195
+ usesFormik: detectedPatternIds.includes('formik'),
196
+ usesTanstackQuery: detectedPatternIds.includes('tanstack-query'),
197
+ usesSwr: detectedPatternIds.includes('swr-pattern'),
198
+ usesZustand: detectedPatternIds.includes('zustand'),
199
+ usesRedux: detectedPatternIds.includes('redux-toolkit'),
200
+ usesTailwind: detectedPatternIds.includes('tailwind'),
201
+ usesShadcn: detectedPatternIds.includes('shadcn-ui'),
202
+ usesMui: detectedPatternIds.includes('mui-patterns'),
203
+ usesChakra: detectedPatternIds.includes('chakra-patterns'),
204
+ },
182
205
  };
183
206
  }
@@ -1,4 +1,21 @@
1
1
  import { EntitySpec } from './parser.js';
2
+ /**
3
+ * Detected pattern from codebase analysis
4
+ */
5
+ export interface DetectedPattern {
6
+ pattern_id: string;
7
+ name: string;
8
+ category: 'components' | 'hooks' | 'forms' | 'state' | 'validation' | 'styling' | 'anti-patterns';
9
+ confidence: number;
10
+ }
11
+ /**
12
+ * Pattern context for generation
13
+ */
14
+ export interface PatternContext {
15
+ detected: DetectedPattern[];
16
+ preferred: string[];
17
+ avoid: string[];
18
+ }
2
19
  /**
3
20
  * Options for API generation
4
21
  */
@@ -7,6 +24,7 @@ export interface GenerateApiOptions {
7
24
  validation?: boolean;
8
25
  tests?: boolean;
9
26
  database?: 'postgresql' | 'mysql' | 'mongodb';
27
+ patterns?: PatternContext;
10
28
  }
11
29
  /**
12
30
  * Options for frontend generation
@@ -16,6 +34,7 @@ export interface GenerateFrontendOptions {
16
34
  forms?: boolean;
17
35
  tables?: boolean;
18
36
  routing?: boolean;
37
+ patterns?: PatternContext;
19
38
  }
20
39
  /**
21
40
  * Options for component generation
@@ -23,6 +42,7 @@ export interface GenerateFrontendOptions {
23
42
  export interface GenerateComponentOptions {
24
43
  ui?: 'shadcn' | 'mui' | 'chakra';
25
44
  validation?: boolean;
45
+ patterns?: PatternContext;
26
46
  }
27
47
  /**
28
48
  * Generated code result
@@ -48,10 +68,12 @@ export interface GenerationMetadata {
48
68
  export interface GeneratorMetadata {
49
69
  id: string;
50
70
  name: string;
71
+ domain?: 'api' | 'frontend' | 'component';
51
72
  framework: string;
52
73
  description: string;
53
74
  features: string[];
54
75
  databases?: string[];
76
+ uiLibraries?: string[];
55
77
  version: string;
56
78
  templates: string[];
57
79
  }