mycontext-cli 4.2.6 → 4.2.8

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 (117) hide show
  1. package/README.md +531 -144
  2. package/dist/README.md +531 -144
  3. package/dist/cli.js +14 -5
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/generate.d.ts.map +1 -1
  6. package/dist/commands/generate.js +19 -47
  7. package/dist/commands/generate.js.map +1 -1
  8. package/dist/commands/init-interactive.d.ts +147 -0
  9. package/dist/commands/init-interactive.d.ts.map +1 -0
  10. package/dist/commands/init-interactive.js +917 -0
  11. package/dist/commands/init-interactive.js.map +1 -0
  12. package/dist/config/shadcn-catalog.json +93 -0
  13. package/dist/core/brain/BrainClient.d.ts +1 -1
  14. package/dist/core/brain/BrainClient.d.ts.map +1 -1
  15. package/dist/core/brain/BrainClient.js +5 -5
  16. package/dist/core/brain/BrainClient.js.map +1 -1
  17. package/dist/doctor/DoctorEngine.d.ts.map +1 -1
  18. package/dist/doctor/DoctorEngine.js +21 -11
  19. package/dist/doctor/DoctorEngine.js.map +1 -1
  20. package/dist/doctor/rules/dead-code-rules.d.ts.map +1 -1
  21. package/dist/doctor/rules/dead-code-rules.js +33 -0
  22. package/dist/doctor/rules/dead-code-rules.js.map +1 -1
  23. package/dist/doctor/rules/index.d.ts +3 -1
  24. package/dist/doctor/rules/index.d.ts.map +1 -1
  25. package/dist/doctor/rules/index.js +7 -1
  26. package/dist/doctor/rules/index.js.map +1 -1
  27. package/dist/doctor/rules/instantdb-rules.d.ts +3 -0
  28. package/dist/doctor/rules/instantdb-rules.d.ts.map +1 -0
  29. package/dist/doctor/rules/instantdb-rules.js +544 -0
  30. package/dist/doctor/rules/instantdb-rules.js.map +1 -0
  31. package/dist/doctor/rules/nextjs-rules.d.ts.map +1 -1
  32. package/dist/doctor/rules/nextjs-rules.js +53 -3
  33. package/dist/doctor/rules/nextjs-rules.js.map +1 -1
  34. package/dist/doctor/rules/typescript-rules.d.ts +3 -0
  35. package/dist/doctor/rules/typescript-rules.d.ts.map +1 -0
  36. package/dist/doctor/rules/typescript-rules.js +177 -0
  37. package/dist/doctor/rules/typescript-rules.js.map +1 -0
  38. package/dist/package.json +4 -2
  39. package/dist/services/ComponentInferenceEngine.d.ts +66 -0
  40. package/dist/services/ComponentInferenceEngine.d.ts.map +1 -0
  41. package/dist/services/ComponentInferenceEngine.js +302 -0
  42. package/dist/services/ComponentInferenceEngine.js.map +1 -0
  43. package/dist/services/ComponentRegistry.d.ts +61 -0
  44. package/dist/services/ComponentRegistry.d.ts.map +1 -0
  45. package/dist/services/ComponentRegistry.js +128 -0
  46. package/dist/services/ComponentRegistry.js.map +1 -0
  47. package/dist/services/InferenceEngine.d.ts +41 -0
  48. package/dist/services/InferenceEngine.d.ts.map +1 -0
  49. package/dist/services/InferenceEngine.js +307 -0
  50. package/dist/services/InferenceEngine.js.map +1 -0
  51. package/dist/services/Planner.d.ts +77 -0
  52. package/dist/services/Planner.d.ts.map +1 -0
  53. package/dist/services/Planner.js +828 -0
  54. package/dist/services/Planner.js.map +1 -0
  55. package/dist/services/ProjectScanner.d.ts.map +1 -1
  56. package/dist/services/ProjectScanner.js +15 -1
  57. package/dist/services/ProjectScanner.js.map +1 -1
  58. package/dist/services/ScaffoldEngine.d.ts +87 -0
  59. package/dist/services/ScaffoldEngine.d.ts.map +1 -0
  60. package/dist/services/ScaffoldEngine.js +409 -0
  61. package/dist/services/ScaffoldEngine.js.map +1 -0
  62. package/dist/services/ScaffoldPreview.d.ts +62 -0
  63. package/dist/services/ScaffoldPreview.d.ts.map +1 -0
  64. package/dist/services/ScaffoldPreview.js +292 -0
  65. package/dist/services/ScaffoldPreview.js.map +1 -0
  66. package/dist/services/TemplateEngine.d.ts +136 -0
  67. package/dist/services/TemplateEngine.d.ts.map +1 -0
  68. package/dist/services/TemplateEngine.js +483 -0
  69. package/dist/services/TemplateEngine.js.map +1 -0
  70. package/dist/services/TemplateHelpers.d.ts +9 -0
  71. package/dist/services/TemplateHelpers.d.ts.map +1 -0
  72. package/dist/services/TemplateHelpers.js +212 -0
  73. package/dist/services/TemplateHelpers.js.map +1 -0
  74. package/dist/templates/actions/auth-actions.ts.hbs +140 -0
  75. package/dist/templates/actions/crud-actions.ts.hbs +113 -0
  76. package/dist/templates/components/auth/login-form.tsx.hbs +67 -0
  77. package/dist/templates/components/auth/login-skeleton.tsx.hbs +24 -0
  78. package/dist/templates/components/auth/register-form.tsx.hbs +116 -0
  79. package/dist/templates/components/crud/entity-card.tsx.hbs +71 -0
  80. package/dist/templates/components/crud/entity-form.tsx.hbs +158 -0
  81. package/dist/templates/components/crud/entity-skeleton.tsx.hbs +90 -0
  82. package/dist/templates/components/crud/entity-table.tsx.hbs +129 -0
  83. package/dist/templates/components/ui/button.tsx.hbs +53 -0
  84. package/dist/templates/components/ui/card.tsx.hbs +68 -0
  85. package/dist/templates/components/ui/input.tsx.hbs +33 -0
  86. package/dist/templates/components/ui/label.tsx.hbs +20 -0
  87. package/dist/templates/components/ui/skeleton.tsx.hbs +15 -0
  88. package/dist/templates/components/ui/theme-provider.tsx.hbs +66 -0
  89. package/dist/templates/components/ui/theme-toggle.tsx.hbs +30 -0
  90. package/dist/templates/config/app.css.hbs +150 -0
  91. package/dist/templates/layouts/dashboard-layout.tsx.hbs +69 -0
  92. package/dist/templates/layouts/error.tsx.hbs +51 -0
  93. package/dist/templates/layouts/loading.tsx.hbs +22 -0
  94. package/dist/templates/layouts/not-found.tsx.hbs +24 -0
  95. package/dist/templates/layouts/root-layout.tsx.hbs +40 -0
  96. package/dist/templates/lib/instant.ts.hbs +19 -0
  97. package/dist/templates/lib/utils.ts.hbs +24 -0
  98. package/dist/templates/pages/auth/login-page.tsx.hbs +30 -0
  99. package/dist/templates/pages/auth/register-page.tsx.hbs +38 -0
  100. package/dist/templates/pages/crud/create-page.tsx.hbs +42 -0
  101. package/dist/templates/pages/crud/detail-page.tsx.hbs +90 -0
  102. package/dist/templates/pages/crud/list-page.tsx.hbs +60 -0
  103. package/dist/templates/pages/crud/loading.tsx.hbs +13 -0
  104. package/dist/templates/pages/landing-page.tsx.hbs +111 -0
  105. package/dist/types/asl.d.ts +387 -0
  106. package/dist/types/asl.d.ts.map +1 -0
  107. package/dist/types/asl.js +139 -0
  108. package/dist/types/asl.js.map +1 -0
  109. package/dist/types/living-context.d.ts +1 -1
  110. package/dist/types/living-context.d.ts.map +1 -1
  111. package/dist/utils/FileGenerator.js +3 -3
  112. package/dist/utils/FileGenerator.js.map +1 -1
  113. package/dist/utils/generateTypesFromSchema.d.ts +47 -0
  114. package/dist/utils/generateTypesFromSchema.d.ts.map +1 -0
  115. package/dist/utils/generateTypesFromSchema.js +298 -0
  116. package/dist/utils/generateTypesFromSchema.js.map +1 -0
  117. package/package.json +4 -2
@@ -0,0 +1,544 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.instantdbRules = void 0;
37
+ /**
38
+ * InstantDB Rules — Schema validation and drift detection for InstantDB projects
39
+ */
40
+ const path = __importStar(require("path"));
41
+ // ─── Helper ───────────────────────────────────────────────────────
42
+ function diag(rule, filePath, message, opts = {}) {
43
+ return {
44
+ ruleId: rule.id,
45
+ filePath,
46
+ line: opts.line,
47
+ severity: rule.severity,
48
+ message,
49
+ help: opts.help || rule.help,
50
+ autoFixable: opts.autoFixable ?? false,
51
+ };
52
+ }
53
+ /**
54
+ * InstantDB type mapping: i.string() → "string", etc.
55
+ */
56
+ const INSTANTDB_TYPE_MAP = {
57
+ string: "string",
58
+ number: "number",
59
+ boolean: "boolean",
60
+ date: "date",
61
+ json: "json",
62
+ any: "any",
63
+ };
64
+ /**
65
+ * Parse InstantDB schema file and extract entity definitions.
66
+ *
67
+ * Handles two schema formats:
68
+ * 1. Modern InstantDB DSL: `entityName: i.entity({ field: i.string(), ... })`
69
+ * 2. Legacy JSON-like: `entityName: { fields: { field: { type: "string" } } }`
70
+ */
71
+ async function parseInstantDBSchema(ctx) {
72
+ const schemaPaths = [
73
+ "instant.schema.ts",
74
+ "src/instant.schema.ts",
75
+ ".mycontext/schema.ts",
76
+ "instant.schema.js",
77
+ ];
78
+ let schemaContent = null;
79
+ for (const sp of schemaPaths) {
80
+ const content = await ctx.readFile(sp);
81
+ if (content) {
82
+ schemaContent = content;
83
+ break;
84
+ }
85
+ }
86
+ if (!schemaContent) {
87
+ return null;
88
+ }
89
+ // Try modern i.entity() DSL first, fall back to legacy format
90
+ const entities = parseModernDSL(schemaContent) || parseLegacyFormat(schemaContent);
91
+ if (!entities || entities.size === 0)
92
+ return null;
93
+ // Parse links and register them as valid "fields" on entities
94
+ // Links represent relationship traversals (e.g., properties.claims, user_companies.user)
95
+ // Also collect ALL link labels globally for nested query validation
96
+ const allLinkLabels = parseAndRegisterLinks(schemaContent, entities);
97
+ // Store link labels on the returned map for the drift checker to use
98
+ entities.__allLinkLabels = allLinkLabels;
99
+ return entities;
100
+ }
101
+ /**
102
+ * Parse the `links` section from the schema and register link labels
103
+ * as valid fields on the corresponding entities.
104
+ *
105
+ * For example, a link like:
106
+ * propertyClaims: { forward: { on: "properties", has: "many", label: "claims" }, ... }
107
+ * registers "claims" as a valid field on the "properties" entity.
108
+ */
109
+ function parseAndRegisterLinks(content, entities) {
110
+ const allLabels = new Set();
111
+ const linksMatch = content.match(/links:\s*\{/);
112
+ if (!linksMatch)
113
+ return allLabels;
114
+ const startIdx = (linksMatch.index || 0) + linksMatch[0].length;
115
+ const linksBody = extractBalancedBraces(content, startIdx);
116
+ if (!linksBody)
117
+ return allLabels;
118
+ // Match link definitions with forward/reverse
119
+ const linkPattern = /(\w+)\s*:\s*\{/g;
120
+ let match;
121
+ while ((match = linkPattern.exec(linksBody)) !== null) {
122
+ const linkStartIdx = match.index + match[0].length;
123
+ const linkBody = extractBalancedBraces(linksBody, linkStartIdx);
124
+ if (!linkBody)
125
+ continue;
126
+ // Parse forward: { on: "entityName", has: "many", label: "labelName" }
127
+ const forwardMatch = linkBody.match(/forward:\s*\{[^}]*on:\s*["'](\w+)["'][^}]*label:\s*["'](\w+)["']/);
128
+ if (forwardMatch) {
129
+ const entityName = forwardMatch[1];
130
+ const label = forwardMatch[2];
131
+ if (entityName && label) {
132
+ allLabels.add(label);
133
+ const entity = entities.get(entityName);
134
+ if (entity) {
135
+ entity.fields[label] = { type: "link", required: false };
136
+ }
137
+ }
138
+ }
139
+ // Parse reverse: { on: "entityName", has: "one", label: "labelName" }
140
+ const reverseMatch = linkBody.match(/reverse:\s*\{[^}]*on:\s*["'](\w+)["'][^}]*label:\s*["'](\w+)["']/);
141
+ if (reverseMatch) {
142
+ const entityName = reverseMatch[1];
143
+ const label = reverseMatch[2];
144
+ if (entityName && label) {
145
+ allLabels.add(label);
146
+ const entity = entities.get(entityName);
147
+ if (entity) {
148
+ entity.fields[label] = { type: "link", required: false };
149
+ }
150
+ }
151
+ }
152
+ }
153
+ return allLabels;
154
+ }
155
+ /**
156
+ * Parse modern InstantDB DSL: `profiles: i.entity({ first_name: i.string(), ... })`
157
+ */
158
+ function parseModernDSL(content) {
159
+ // Check if this file uses i.entity() syntax
160
+ if (!content.includes("i.entity("))
161
+ return null;
162
+ const entities = new Map();
163
+ // Match each entity: `entityName: i.entity({ ... })`
164
+ // We need to handle nested braces, so we find the start and then balance braces
165
+ const entityStartPattern = /(\w+)\s*:\s*i\.entity\(\s*\{/g;
166
+ let match;
167
+ while ((match = entityStartPattern.exec(content)) !== null) {
168
+ const entityName = match[1];
169
+ if (!entityName)
170
+ continue;
171
+ // Find the matching closing brace for the entity body
172
+ const startIdx = match.index + match[0].length;
173
+ const entityBody = extractBalancedBraces(content, startIdx);
174
+ if (!entityBody)
175
+ continue;
176
+ const fields = {};
177
+ // Match fields: `fieldName: i.string()`, `fieldName: i.number().optional()`, etc.
178
+ const fieldPattern = /(\w+)\s*:\s*i\.(string|number|boolean|date|json|any)\(\)/g;
179
+ let fieldMatch;
180
+ while ((fieldMatch = fieldPattern.exec(entityBody)) !== null) {
181
+ const fieldName = fieldMatch[1];
182
+ const idbType = fieldMatch[2];
183
+ if (!fieldName || !idbType)
184
+ continue;
185
+ const mappedType = INSTANTDB_TYPE_MAP[idbType] || "any";
186
+ // Check for .optional() modifier after the i.type() call
187
+ const afterCall = entityBody.substring(fieldMatch.index + fieldMatch[0].length, fieldMatch.index + fieldMatch[0].length + 20);
188
+ const isOptional = afterCall.startsWith(".optional()");
189
+ fields[fieldName] = {
190
+ type: mappedType,
191
+ required: !isOptional,
192
+ };
193
+ }
194
+ // Always include "id" as a built-in field
195
+ fields["id"] = { type: "string", required: true };
196
+ entities.set(entityName, { name: entityName, fields });
197
+ }
198
+ return entities.size > 0 ? entities : null;
199
+ }
200
+ /**
201
+ * Parse legacy JSON-like format: `entityName: { fields: { field: { type: "string" } } }`
202
+ */
203
+ function parseLegacyFormat(content) {
204
+ const entities = new Map();
205
+ try {
206
+ const entitiesMatch = content.match(/entities:\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/s);
207
+ if (!entitiesMatch || !entitiesMatch[1])
208
+ return null;
209
+ const entitiesSection = entitiesMatch[1];
210
+ const entityPattern = /(\w+):\s*\{[^}]*fields:\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/gs;
211
+ let entityMatch;
212
+ while ((entityMatch = entityPattern.exec(entitiesSection)) !== null) {
213
+ const entityName = entityMatch[1];
214
+ const fieldsSection = entityMatch[2];
215
+ if (!entityName || !fieldsSection)
216
+ continue;
217
+ const fields = {};
218
+ const fieldPattern = /(\w+):\s*\{[^}]*type:\s*["'](\w+)["'][^}]*\}/g;
219
+ let fieldMatch;
220
+ while ((fieldMatch = fieldPattern.exec(fieldsSection)) !== null) {
221
+ const fieldName = fieldMatch[1];
222
+ const fieldType = fieldMatch[2];
223
+ if (!fieldName || !fieldType)
224
+ continue;
225
+ fields[fieldName] = {
226
+ type: fieldType,
227
+ required: fieldMatch[0].includes("required") || fieldMatch[0].includes("optional: false"),
228
+ };
229
+ }
230
+ entities.set(entityName, { name: entityName, fields });
231
+ }
232
+ }
233
+ catch {
234
+ return null;
235
+ }
236
+ return entities.size > 0 ? entities : null;
237
+ }
238
+ /**
239
+ * Extract content between balanced braces starting after an opening brace.
240
+ * Returns the content between braces (excluding the outer braces).
241
+ */
242
+ function extractBalancedBraces(content, startAfterOpenBrace) {
243
+ let depth = 1;
244
+ let i = startAfterOpenBrace;
245
+ while (i < content.length && depth > 0) {
246
+ if (content[i] === "{")
247
+ depth++;
248
+ else if (content[i] === "}")
249
+ depth--;
250
+ i++;
251
+ }
252
+ if (depth !== 0)
253
+ return null;
254
+ return content.substring(startAfterOpenBrace, i - 1);
255
+ }
256
+ /**
257
+ * Common JavaScript/TypeScript built-in properties that should never be treated
258
+ * as schema field accesses, even if the variable name matches an entity name.
259
+ */
260
+ const JS_BUILTIN_PROPERTIES = new Set([
261
+ // Array methods & properties
262
+ "length", "map", "filter", "reduce", "forEach", "find", "findIndex", "some",
263
+ "every", "includes", "indexOf", "slice", "splice", "push", "pop", "shift",
264
+ "unshift", "concat", "join", "sort", "reverse", "flat", "flatMap", "fill",
265
+ "at", "entries", "keys", "values", "from", "isArray", "of",
266
+ // Object methods
267
+ "keys", "values", "entries", "assign", "freeze", "create", "hasOwnProperty",
268
+ "toString", "valueOf", "constructor", "prototype",
269
+ // Promise/async
270
+ "then", "catch", "finally", "resolve", "reject", "all", "allSettled", "race",
271
+ // String methods
272
+ "trim", "split", "replace", "match", "search", "startsWith", "endsWith",
273
+ "toUpperCase", "toLowerCase", "substring", "charAt", "charCodeAt", "padStart",
274
+ "padEnd", "repeat", "normalize", "localeCompare",
275
+ // Common DOM/React/Next.js
276
+ "current", "style", "className", "children", "props", "state", "ref",
277
+ "target", "value", "type", "name", "href", "src", "alt", "id",
278
+ "addEventListener", "removeEventListener", "preventDefault", "stopPropagation",
279
+ // Common chaining
280
+ "default", "log", "error", "warn", "info", "debug", "env",
281
+ // InstantDB / InstaQL query operators
282
+ "in", "gt", "lt", "gte", "lte", "ne", "like", "not", "$isNull",
283
+ ]);
284
+ /**
285
+ * Extract field accesses from TypeScript code that are specifically
286
+ * within InstantDB query/transaction contexts.
287
+ *
288
+ * Only checks:
289
+ * 1. db.useQuery({ entityName: { fieldName: {} } }) — query objects
290
+ * 2. tx.entityName.update({ fieldName: value }) — transaction mutations
291
+ * 3. Direct entity field reads: entityVar.fieldName where entityVar is
292
+ * destructured from a query result (conservative heuristic)
293
+ */
294
+ function extractFieldAccesses(content, schemaEntities) {
295
+ const accesses = [];
296
+ const entityNames = Array.from(schemaEntities.keys());
297
+ const entityNamesSet = new Set(entityNames.map(n => n.toLowerCase()));
298
+ // ── Strategy 1: db.useQuery({ entity: { field: {} } }) ──
299
+ // Match query objects passed to useQuery/query
300
+ for (const entityName of entityNames) {
301
+ // Find query blocks like: entityName: { $: { where: { ... } }, fieldName: {} }
302
+ // or shorthand: entityName: { fieldName: {} }
303
+ const queryBlockPattern = new RegExp(`\\b${entityName}\\s*:\\s*\\{([^}]*(?:\\{[^}]*\\}[^}]*)*)\\}`, "gs");
304
+ let qMatch;
305
+ while ((qMatch = queryBlockPattern.exec(content)) !== null) {
306
+ const blockContent = qMatch[1];
307
+ if (!blockContent)
308
+ continue;
309
+ // Extract field names from the query block (keys before a colon)
310
+ const fieldKeyPattern = /(\w+)\s*:/g;
311
+ let fkMatch;
312
+ while ((fkMatch = fieldKeyPattern.exec(blockContent)) !== null) {
313
+ const fieldName = fkMatch[1];
314
+ if (!fieldName)
315
+ continue;
316
+ // Skip $ (query operator), common non-field keys, and builtins
317
+ if (fieldName === "$" || fieldName === "where" || fieldName === "order" ||
318
+ fieldName === "limit" || fieldName === "offset" || fieldName === "first" ||
319
+ fieldName === "last" || fieldName === "before" || fieldName === "after")
320
+ continue;
321
+ if (JS_BUILTIN_PROPERTIES.has(fieldName))
322
+ continue;
323
+ const lineNum = content.substring(0, qMatch.index).split("\n").length;
324
+ accesses.push({
325
+ entity: entityName,
326
+ field: fieldName,
327
+ line: lineNum,
328
+ match: `${entityName}.${fieldName}`,
329
+ });
330
+ }
331
+ }
332
+ }
333
+ // ── Strategy 2: tx.entityName.update/merge/delete({ field: value }) ──
334
+ for (const entityName of entityNames) {
335
+ const txPattern = new RegExp(`tx\\.${entityName}[^.]*\\.(?:update|merge)\\s*\\(\\s*\\{([^}]*)\\}`, "gs");
336
+ let txMatch;
337
+ while ((txMatch = txPattern.exec(content)) !== null) {
338
+ const updateBody = txMatch[1];
339
+ if (!updateBody)
340
+ continue;
341
+ const fieldKeyPattern = /(\w+)\s*:/g;
342
+ let fkMatch;
343
+ while ((fkMatch = fieldKeyPattern.exec(updateBody)) !== null) {
344
+ const fieldName = fkMatch[1];
345
+ if (!fieldName || JS_BUILTIN_PROPERTIES.has(fieldName))
346
+ continue;
347
+ const lineNum = content.substring(0, txMatch.index).split("\n").length;
348
+ accesses.push({
349
+ entity: entityName,
350
+ field: fieldName,
351
+ line: lineNum,
352
+ match: `tx.${entityName}.${fieldName}`,
353
+ });
354
+ }
355
+ }
356
+ }
357
+ return accesses;
358
+ }
359
+ // ─── Rules ────────────────────────────────────────────────────────
360
+ const schemaFieldDrift = {
361
+ id: "instantdb/schema-field-drift",
362
+ name: "Schema Field Drift Detection",
363
+ category: "node",
364
+ severity: "error",
365
+ description: "Detects code accessing fields that don't exist in the InstantDB schema",
366
+ help: "Update field names in code to match the schema, or add missing fields to instant.schema.ts",
367
+ appliesTo: ["nextjs", "node"],
368
+ async check(ctx) {
369
+ const results = [];
370
+ // Parse the schema
371
+ const schemaEntities = await parseInstantDBSchema(ctx);
372
+ if (!schemaEntities || schemaEntities.size === 0) {
373
+ return results; // No schema found or no entities, skip check
374
+ }
375
+ // Find all TypeScript/JavaScript files
376
+ const files = await ctx.findFiles(/\.(tsx?|jsx?)$/);
377
+ for (const file of files) {
378
+ // Skip schema files, node_modules, scripts/, .mycontext/, and config files
379
+ if (file.includes("schema.") ||
380
+ file.includes("node_modules") ||
381
+ file.startsWith("scripts/") ||
382
+ file.includes("/scripts/") ||
383
+ file.startsWith(".mycontext/") ||
384
+ file.includes("/.mycontext/") ||
385
+ file.endsWith(".config.ts") ||
386
+ file.endsWith(".config.js")) {
387
+ continue;
388
+ }
389
+ const contentRaw = await ctx.readFile(file);
390
+ if (!contentRaw)
391
+ continue;
392
+ // Strip comments (to avoid matching // TODO: as a field)
393
+ const content = contentRaw.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "$1");
394
+ // Only check files that actually use InstantDB
395
+ if (!content.includes("db.useQuery") &&
396
+ !content.includes("db.transact") &&
397
+ !content.includes("tx.") &&
398
+ !content.includes("useQuery") &&
399
+ !content.includes("adminDb")) {
400
+ continue;
401
+ }
402
+ // Extract field accesses from InstantDB contexts only
403
+ const accesses = extractFieldAccesses(content, schemaEntities);
404
+ // Get global link labels (registered on the map metadata in parseInstantDBSchema)
405
+ const allLinkLabels = schemaEntities.__allLinkLabels || new Set();
406
+ // Check each access against the schema
407
+ for (const access of accesses) {
408
+ const entity = schemaEntities.get(access.entity);
409
+ if (!entity)
410
+ continue;
411
+ // Skip if the field is a known link label globally (relationship traversal)
412
+ if (allLinkLabels.has(access.field))
413
+ continue;
414
+ // Check if the field exists in the schema
415
+ if (!entity.fields[access.field]) {
416
+ // Field doesn't exist in schema - this is drift!
417
+ // Try to find a similar field name
418
+ const availableFields = Object.keys(entity.fields);
419
+ const similarField = findSimilarField(access.field, availableFields);
420
+ let helpMessage = `Field '${access.field}' does not exist in ${access.entity} schema. `;
421
+ if (similarField) {
422
+ helpMessage += `Did you mean '${similarField}'? Available fields: ${availableFields.join(", ")}`;
423
+ }
424
+ else {
425
+ helpMessage += `Available fields: ${availableFields.join(", ")}`;
426
+ }
427
+ results.push(diag(this, file, `${access.entity}.${access.field} - field '${access.field}' not in schema`, {
428
+ line: access.line,
429
+ help: helpMessage,
430
+ autoFixable: similarField !== null,
431
+ }));
432
+ }
433
+ }
434
+ }
435
+ return results;
436
+ },
437
+ async fix(ctx, diagnostic) {
438
+ // Auto-fix by replacing the incorrect field name with the suggested one
439
+ const match = diagnostic.message.match(/(\w+)\.(\w+) - field '(\w+)' not in schema/);
440
+ if (!match)
441
+ return false;
442
+ const [, entity, , wrongField] = match;
443
+ // Extract the suggestion from the help message
444
+ const suggestionMatch = diagnostic.help.match(/Did you mean '(\w+)'\?/);
445
+ if (!suggestionMatch)
446
+ return false;
447
+ const correctField = suggestionMatch[1];
448
+ // Read the file
449
+ const content = await ctx.readFile(diagnostic.filePath);
450
+ if (!content)
451
+ return false;
452
+ const lines = content.split("\n");
453
+ if (!diagnostic.line || diagnostic.line > lines.length)
454
+ return false;
455
+ // Replace the field name on the specific line
456
+ const lineIndex = diagnostic.line - 1;
457
+ const originalLine = lines[lineIndex];
458
+ if (!originalLine)
459
+ return false;
460
+ // Replace entity.wrongField with entity.correctField
461
+ const pattern = new RegExp(`(\\w+)\\.${wrongField}\\b`, 'g');
462
+ const newLine = originalLine.replace(pattern, `$1.${correctField}`);
463
+ if (newLine === originalLine)
464
+ return false; // Nothing changed
465
+ lines[lineIndex] = newLine;
466
+ // Write the file back
467
+ const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
468
+ const absolutePath = path.join(ctx.root, diagnostic.filePath);
469
+ await fs.writeFile(absolutePath, lines.join("\n"), "utf-8");
470
+ return true;
471
+ },
472
+ };
473
+ /**
474
+ * Find a similar field name using simple heuristics
475
+ * Common patterns:
476
+ * - payment_method -> method (remove entity prefix)
477
+ * - delivery_zone_id -> zone_id (remove prefix)
478
+ * - user_email -> email (remove entity prefix)
479
+ */
480
+ function findSimilarField(wrongField, availableFields) {
481
+ const wrongLower = wrongField.toLowerCase();
482
+ // 1. Exact match (shouldn't happen, but just in case)
483
+ for (const field of availableFields) {
484
+ if (field.toLowerCase() === wrongLower) {
485
+ return field;
486
+ }
487
+ }
488
+ // 2. Check if wrongField ends with one of the available fields
489
+ // e.g., payment_method ends with method
490
+ for (const field of availableFields) {
491
+ if (wrongLower.endsWith(field.toLowerCase()) || wrongLower.endsWith('_' + field.toLowerCase())) {
492
+ return field;
493
+ }
494
+ }
495
+ // 3. Check if wrongField contains one of the available fields
496
+ // e.g., delivery_zone_id contains zone_id
497
+ for (const field of availableFields) {
498
+ if (wrongLower.includes(field.toLowerCase())) {
499
+ return field;
500
+ }
501
+ }
502
+ // 4. Levenshtein distance for typos (simple version)
503
+ let closest = null;
504
+ let minDistance = Infinity;
505
+ for (const field of availableFields) {
506
+ const distance = levenshteinDistance(wrongLower, field.toLowerCase());
507
+ if (distance < minDistance && distance <= 3) { // Allow up to 3 character differences
508
+ minDistance = distance;
509
+ closest = field;
510
+ }
511
+ }
512
+ return closest;
513
+ }
514
+ /**
515
+ * Calculate Levenshtein distance between two strings
516
+ */
517
+ function levenshteinDistance(str1, str2) {
518
+ const matrix = Array.from({ length: str2.length + 1 }, () => Array(str1.length + 1).fill(0));
519
+ for (let i = 0; i <= str2.length; i++) {
520
+ matrix[i][0] = i;
521
+ }
522
+ for (let j = 0; j <= str1.length; j++) {
523
+ matrix[0][j] = j;
524
+ }
525
+ for (let i = 1; i <= str2.length; i++) {
526
+ for (let j = 1; j <= str1.length; j++) {
527
+ if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
528
+ matrix[i][j] = matrix[i - 1][j - 1];
529
+ }
530
+ else {
531
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
532
+ matrix[i][j - 1] + 1, // insertion
533
+ matrix[i - 1][j] + 1 // deletion
534
+ );
535
+ }
536
+ }
537
+ }
538
+ return matrix[str2.length][str1.length];
539
+ }
540
+ // ─── Export ───────────────────────────────────────────────────────
541
+ exports.instantdbRules = [
542
+ schemaFieldDrift,
543
+ ];
544
+ //# sourceMappingURL=instantdb-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instantdb-rules.js","sourceRoot":"","sources":["../../../src/doctor/rules/instantdb-rules.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"nextjs-rules.d.ts","sourceRoot":"","sources":["../../../src/doctor/rules/nextjs-rules.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,UAAU,CAAC;AAkUpE,eAAO,MAAM,WAAW,EAAE,UAAU,EAWnC,CAAC"}
1
+ {"version":3,"file":"nextjs-rules.d.ts","sourceRoot":"","sources":["../../../src/doctor/rules/nextjs-rules.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,UAAU,CAAC;AA2XpE,eAAO,MAAM,WAAW,EAAE,UAAU,EAWnC,CAAC"}
@@ -208,22 +208,53 @@ const metadataExport = {
208
208
  name: "Metadata Export",
209
209
  category: "nextjs",
210
210
  severity: "warning",
211
- description: "Pages/layouts should export metadata or generateMetadata for SEO",
211
+ description: "Public pages/layouts should export metadata or generateMetadata for SEO",
212
212
  help: "Add 'export const metadata = { title: ... }' or 'export async function generateMetadata()'",
213
213
  appliesTo: ["nextjs"],
214
214
  async check(ctx) {
215
215
  const results = [];
216
216
  const pages = await ctx.findFiles(/page\.(tsx|jsx|ts|js)$/);
217
+ // Route groups that are typically auth-gated (no SEO needed)
218
+ const authGatedGroups = new Set([
219
+ "(pms)", "(admin)", "(dashboard)", "(agents)", "(app)",
220
+ "(protected)", "(auth)", "(settings)", "(account)", "(manage)",
221
+ ]);
222
+ // Detect additional auth-gated groups by checking for auth patterns in layouts
223
+ const layouts = await ctx.findFiles(/layout\.(tsx|jsx|ts|js)$/);
224
+ const authPatterns = /\b(redirect|getServerSession|auth|session|middleware|cookies|signIn|requireAuth|useAuth)\b/;
225
+ const detectedAuthGroups = new Set();
226
+ for (const layout of layouts) {
227
+ const content = await ctx.readFile(layout);
228
+ if (!content)
229
+ continue;
230
+ if (authPatterns.test(content)) {
231
+ // Extract route group from path, e.g. "app/(pms)/[slug]/layout.tsx" → "(pms)"
232
+ const groupMatch = layout.match(/\(([^)]+)\)/);
233
+ if (groupMatch) {
234
+ detectedAuthGroups.add(`(${groupMatch[1]})`);
235
+ }
236
+ }
237
+ }
217
238
  for (const p of pages) {
239
+ // Skip pages in auth-gated route groups
240
+ const isAuthGated = [...authGatedGroups, ...detectedAuthGroups].some(group => p.includes(`/${group}/`) || p.startsWith(`${group}/`));
241
+ if (isAuthGated)
242
+ continue;
243
+ // Skip API routes
244
+ if (p.includes("/api/"))
245
+ continue;
218
246
  const content = await ctx.readFile(p);
219
247
  if (!content)
220
248
  continue;
249
+ // Skip client components (they can't export metadata)
250
+ if (content.includes('"use client"') || content.includes("'use client'"))
251
+ continue;
221
252
  // Check if there's metadata or generateMetadata
222
253
  const hasMetadata = content.includes("export const metadata") ||
223
254
  content.includes("export async function generateMetadata") ||
224
255
  content.includes("export function generateMetadata");
225
256
  if (!hasMetadata) {
226
- results.push(diag(this, p, "Page missing metadata export for SEO"));
257
+ results.push(diag(this, p, "Public page missing metadata export for SEO"));
227
258
  }
228
259
  }
229
260
  return results;
@@ -336,11 +367,30 @@ const loadingFiles = {
336
367
  const hasLoading = await ctx.fileExists(path.join(dir, "loading.tsx")) ||
337
368
  await ctx.fileExists(path.join(dir, "loading.jsx"));
338
369
  if (!hasLoading) {
339
- results.push(diag(this, p, "Page with async data has no loading.tsx for Suspense"));
370
+ results.push(diag(this, p, "Page with async data has no loading.tsx for Suspense", {
371
+ autoFixable: true,
372
+ }));
340
373
  }
341
374
  }
342
375
  return results;
343
376
  },
377
+ async fix(ctx, d) {
378
+ const dir = path.dirname(d.filePath);
379
+ const loadingPath = path.join(dir, "loading.tsx");
380
+ const absPath = path.join(ctx.root, loadingPath);
381
+ const skeleton = `export default function Loading() {
382
+ return (
383
+ <div className="flex items-center justify-center min-h-[400px]">
384
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
385
+ </div>
386
+ );
387
+ }
388
+ `;
389
+ const { writeFile, ensureDir } = await Promise.resolve().then(() => __importStar(require("fs-extra")));
390
+ await ensureDir(path.dirname(absPath));
391
+ await writeFile(absPath, skeleton);
392
+ return true;
393
+ },
344
394
  };
345
395
  exports.nextjsRules = [
346
396
  missingRootLayout,