@getjack/jack 0.1.34 → 0.1.36

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 (90) hide show
  1. package/README.md +6 -6
  2. package/package.json +1 -1
  3. package/src/commands/down.ts +39 -7
  4. package/src/commands/link.ts +2 -4
  5. package/src/commands/logs.ts +2 -4
  6. package/src/commands/mcp.ts +12 -10
  7. package/src/commands/services.ts +4 -2
  8. package/src/commands/sync.ts +5 -6
  9. package/src/commands/update.ts +1 -0
  10. package/src/index.ts +8 -0
  11. package/src/lib/auth/client.ts +5 -2
  12. package/src/lib/binding-validator.ts +39 -3
  13. package/src/lib/build-helper.ts +18 -19
  14. package/src/lib/control-plane.ts +45 -0
  15. package/src/lib/do-config.ts +110 -0
  16. package/src/lib/do-export-validator.ts +26 -0
  17. package/src/lib/jsonc-edit.ts +292 -0
  18. package/src/lib/managed-deploy.ts +36 -1
  19. package/src/lib/project-link.ts +37 -0
  20. package/src/lib/project-operations.ts +31 -66
  21. package/src/lib/resources.ts +4 -5
  22. package/src/lib/schema.ts +8 -12
  23. package/src/lib/services/db-create.ts +2 -2
  24. package/src/lib/services/db-execute.ts +9 -6
  25. package/src/lib/services/db-list.ts +6 -4
  26. package/src/lib/services/endpoint-test.ts +275 -0
  27. package/src/lib/services/project-delete.ts +190 -0
  28. package/src/lib/services/project-environment.ts +579 -0
  29. package/src/lib/services/storage-config.ts +7 -309
  30. package/src/lib/services/storage-create.ts +2 -1
  31. package/src/lib/services/storage-delete.ts +3 -2
  32. package/src/lib/services/storage-info.ts +2 -1
  33. package/src/lib/services/storage-list.ts +6 -3
  34. package/src/lib/services/vectorize-config.ts +7 -264
  35. package/src/lib/services/vectorize-create.ts +2 -1
  36. package/src/lib/services/vectorize-delete.ts +6 -4
  37. package/src/lib/services/vectorize-list.ts +6 -3
  38. package/src/lib/storage/index.ts +21 -23
  39. package/src/lib/telemetry.ts +1 -0
  40. package/src/lib/wrangler-config.ts +43 -312
  41. package/src/lib/zip-packager.ts +28 -0
  42. package/src/mcp/test-utils.ts +31 -0
  43. package/src/mcp/tools/index.ts +280 -2
  44. package/src/templates/index.ts +5 -0
  45. package/src/templates/types.ts +4 -0
  46. package/templates/AI-BINDINGS.md +34 -76
  47. package/templates/CLAUDE.md +1 -1
  48. package/templates/ai-chat/src/index.ts +7 -14
  49. package/templates/ai-chat/src/jack-ai.ts +0 -6
  50. package/templates/chat/.jack.json +45 -0
  51. package/templates/chat/bun.lock +1584 -0
  52. package/templates/chat/components.json +23 -0
  53. package/templates/chat/index.html +12 -0
  54. package/templates/chat/package.json +41 -0
  55. package/templates/chat/src/chat-agent.ts +63 -0
  56. package/templates/chat/src/client/app.tsx +189 -0
  57. package/templates/chat/src/client/chat.tsx +222 -0
  58. package/templates/chat/src/client/components/prompt-kit/chat-container.tsx +47 -0
  59. package/templates/chat/src/client/components/prompt-kit/loader.tsx +33 -0
  60. package/templates/chat/src/client/components/prompt-kit/markdown.tsx +84 -0
  61. package/templates/chat/src/client/components/prompt-kit/message.tsx +54 -0
  62. package/templates/chat/src/client/components/prompt-kit/prompt-suggestion.tsx +20 -0
  63. package/templates/chat/src/client/components/prompt-kit/reasoning.tsx +134 -0
  64. package/templates/chat/src/client/components/prompt-kit/scroll-button.tsx +28 -0
  65. package/templates/chat/src/client/components/ui/button.tsx +38 -0
  66. package/templates/chat/src/client/lib/utils.ts +6 -0
  67. package/templates/chat/src/client/main.tsx +11 -0
  68. package/templates/chat/src/client/styles.css +125 -0
  69. package/templates/chat/src/index.ts +25 -0
  70. package/templates/chat/src/jack-ai.ts +94 -0
  71. package/templates/chat/tsconfig.json +18 -0
  72. package/templates/chat/vite.config.ts +14 -0
  73. package/templates/chat/wrangler.jsonc +18 -0
  74. package/templates/cron/.jack.json +18 -28
  75. package/templates/cron/schema.sql +10 -20
  76. package/templates/cron/src/admin.ts +321 -0
  77. package/templates/cron/src/index.ts +151 -81
  78. package/templates/cron/src/monitor.ts +124 -0
  79. package/templates/semantic-search/src/index.ts +5 -43
  80. package/templates/semantic-search/src/jack-ai.ts +0 -6
  81. package/templates/telegram-bot/.jack.json +56 -0
  82. package/templates/telegram-bot/bun.lock +41 -0
  83. package/templates/telegram-bot/package.json +16 -0
  84. package/templates/telegram-bot/src/index.ts +236 -0
  85. package/templates/telegram-bot/src/jack-ai.ts +100 -0
  86. package/templates/telegram-bot/tsconfig.json +11 -0
  87. package/templates/telegram-bot/wrangler.jsonc +8 -0
  88. package/templates/cron/src/jobs.ts +0 -139
  89. package/templates/cron/src/webhooks.ts +0 -95
  90. package/templates/semantic-search/src/jack-vectorize.ts +0 -169
@@ -0,0 +1,579 @@
1
+ /**
2
+ * Project environment introspection service
3
+ *
4
+ * Consolidates multiple API calls into a single environment snapshot:
5
+ * project info, bindings, DB schema, crons, variables, and config issues.
6
+ *
7
+ * Used by both CLI and MCP (get_project_environment tool).
8
+ */
9
+
10
+ import { existsSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import {
13
+ type CronScheduleInfo,
14
+ type ProjectOverview,
15
+ executeManagedSql,
16
+ fetchProjectOverview,
17
+ fetchProjectResources,
18
+ listCronSchedules as listCronSchedulesApi,
19
+ } from "../control-plane.ts";
20
+ import { type DeployMode, readProjectLink } from "../project-link.ts";
21
+ import { getProjectStatus } from "../project-operations.ts";
22
+ import {
23
+ type ControlPlaneResource,
24
+ type ResolvedResources,
25
+ convertControlPlaneResources,
26
+ parseWranglerResources,
27
+ } from "../resources.ts";
28
+ import { findWranglerConfig, hasWranglerConfig } from "../wrangler-config.ts";
29
+
30
+ // ============================================================================
31
+ // Types
32
+ // ============================================================================
33
+
34
+ export interface ProjectEnvironment {
35
+ project: {
36
+ name: string;
37
+ url: string | null;
38
+ deploy_mode: DeployMode;
39
+ last_deploy: {
40
+ at: string | null;
41
+ status: string | null;
42
+ message: string | null;
43
+ source: string | null;
44
+ } | null;
45
+ };
46
+ bindings: EnvironmentBindings;
47
+ secrets_set: string[];
48
+ variables: Record<string, string>;
49
+ database: DatabaseSchema | null;
50
+ crons: CronEntry[];
51
+ issues: EnvironmentIssue[];
52
+ }
53
+
54
+ export interface EnvironmentBindings {
55
+ d1?: { binding: string; database_name: string; database_id?: string };
56
+ r2?: Array<{ binding: string; bucket_name: string }>;
57
+ kv?: Array<{ binding: string; namespace_id: string; name?: string }>;
58
+ ai?: { binding: string };
59
+ vectorize?: Array<{ binding: string; index_name: string }>;
60
+ durable_objects?: Array<{ binding: string; class_name: string }>;
61
+ }
62
+
63
+ export interface DatabaseSchema {
64
+ name: string;
65
+ tables: TableSchema[];
66
+ }
67
+
68
+ export interface TableSchema {
69
+ name: string;
70
+ columns: ColumnSchema[];
71
+ row_count: number;
72
+ }
73
+
74
+ export interface ColumnSchema {
75
+ name: string;
76
+ type: string;
77
+ pk: boolean;
78
+ notnull: boolean;
79
+ }
80
+
81
+ export interface CronEntry {
82
+ expression: string;
83
+ enabled: boolean;
84
+ last_run_status: string | null;
85
+ }
86
+
87
+ export interface EnvironmentIssue {
88
+ severity: "warning" | "error";
89
+ message: string;
90
+ }
91
+
92
+ // ============================================================================
93
+ // Main Function
94
+ // ============================================================================
95
+
96
+ /**
97
+ * Get a consolidated environment snapshot for a project.
98
+ * Handles both managed and BYO deploy modes.
99
+ */
100
+ export async function getProjectEnvironment(projectDir: string): Promise<ProjectEnvironment> {
101
+ const link = await readProjectLink(projectDir);
102
+ const deployMode: DeployMode = link?.deploy_mode ?? "byo";
103
+
104
+ // Managed mode: use single overview call instead of 3-5 separate calls
105
+ if (deployMode === "managed" && link?.project_id) {
106
+ return getManagedProjectEnvironment(projectDir, link.project_id);
107
+ }
108
+
109
+ // BYO mode: original multi-call path
110
+ return getByoProjectEnvironment(projectDir, deployMode, link?.project_id);
111
+ }
112
+
113
+ /**
114
+ * Managed mode: single overview call + local enrichment.
115
+ */
116
+ async function getManagedProjectEnvironment(
117
+ projectDir: string,
118
+ projectId: string,
119
+ ): Promise<ProjectEnvironment> {
120
+ // 1. Single API call for project + resources + deployment + crons
121
+ const overview = await fetchProjectOverview(projectId);
122
+
123
+ // 2. Convert resources to bindings
124
+ const rawResources = overview.resources as ControlPlaneResource[];
125
+ const resolved = convertControlPlaneResources(rawResources);
126
+ const bindings = buildBindingsFromResolved(resolved);
127
+
128
+ // Also parse wrangler.jsonc for vectorize and durable_objects
129
+ await enrichFromWranglerConfig(projectDir, bindings);
130
+
131
+ // 3. Get variables from wrangler.jsonc
132
+ const wranglerResources = await parseWranglerResourcesSafe(projectDir);
133
+ const variables = wranglerResources?.vars ?? {};
134
+
135
+ // 4. Get secrets names
136
+ const secretsSet = await getSecretsNames(projectDir);
137
+
138
+ // 5. Get database schema (if D1 exists)
139
+ let database: DatabaseSchema | null = null;
140
+ if (bindings.d1) {
141
+ database = await getDatabaseSchema(projectDir, "managed", projectId, bindings.d1.database_name);
142
+ }
143
+
144
+ // 6. Map crons from overview
145
+ const crons: CronEntry[] = overview.crons.map((s) => ({
146
+ expression: s.expression,
147
+ enabled: s.enabled,
148
+ last_run_status: s.last_run_status,
149
+ }));
150
+
151
+ // 7. Detect issues
152
+ const issues = detectIssues(bindings, rawResources, wranglerResources, projectDir);
153
+
154
+ // 8. Map deployment info
155
+ const dep = overview.latest_deployment;
156
+
157
+ return {
158
+ project: {
159
+ name: overview.project.name,
160
+ url: overview.project.url,
161
+ deploy_mode: "managed",
162
+ last_deploy: dep
163
+ ? {
164
+ at: dep.created_at,
165
+ status: dep.status,
166
+ message: dep.message,
167
+ source: dep.source,
168
+ }
169
+ : null,
170
+ },
171
+ bindings,
172
+ secrets_set: secretsSet,
173
+ variables,
174
+ database,
175
+ crons,
176
+ issues,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * BYO mode: original multi-call path.
182
+ */
183
+ async function getByoProjectEnvironment(
184
+ projectDir: string,
185
+ deployMode: DeployMode,
186
+ projectId?: string,
187
+ ): Promise<ProjectEnvironment> {
188
+ // 1. Get project status (name, URL, deploy info)
189
+ const status = await getProjectStatus(undefined, projectDir);
190
+ if (!status) {
191
+ throw new Error("Project not found. Ensure you're in a valid jack project directory.");
192
+ }
193
+
194
+ // 2. Get bindings
195
+ const { bindings, rawResources, wranglerResources } = await getBindings(
196
+ projectDir,
197
+ deployMode,
198
+ projectId,
199
+ );
200
+
201
+ // 3. Get variables from wrangler.jsonc
202
+ const variables = wranglerResources?.vars ?? {};
203
+
204
+ // 4. Get secrets names from wrangler.jsonc (we only report names, not values)
205
+ const secretsSet = await getSecretsNames(projectDir);
206
+
207
+ // 5. Get database schema (if D1 exists)
208
+ let database: DatabaseSchema | null = null;
209
+ if (bindings.d1) {
210
+ database = await getDatabaseSchema(
211
+ projectDir,
212
+ deployMode,
213
+ projectId,
214
+ bindings.d1.database_name,
215
+ );
216
+ }
217
+
218
+ // 6. Detect issues
219
+ const issues = detectIssues(bindings, rawResources, wranglerResources, projectDir);
220
+
221
+ return {
222
+ project: {
223
+ name: status.name,
224
+ url: status.workerUrl,
225
+ deploy_mode: deployMode,
226
+ last_deploy:
227
+ status.lastDeployAt || status.lastDeployStatus
228
+ ? {
229
+ at: status.lastDeployAt,
230
+ status: status.lastDeployStatus,
231
+ message: status.lastDeployMessage,
232
+ source: status.lastDeploySource,
233
+ }
234
+ : null,
235
+ },
236
+ bindings,
237
+ secrets_set: secretsSet,
238
+ variables,
239
+ database,
240
+ crons: [], // BYO doesn't have managed crons
241
+ issues,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Build EnvironmentBindings from resolved resources.
247
+ */
248
+ function buildBindingsFromResolved(resolved: ResolvedResources): EnvironmentBindings {
249
+ const result: EnvironmentBindings = {};
250
+ if (resolved.d1) {
251
+ result.d1 = {
252
+ binding: resolved.d1.binding,
253
+ database_name: resolved.d1.name,
254
+ database_id: resolved.d1.id,
255
+ };
256
+ }
257
+ if (resolved.r2?.length) {
258
+ result.r2 = resolved.r2.map((r) => ({
259
+ binding: r.binding,
260
+ bucket_name: r.name,
261
+ }));
262
+ }
263
+ if (resolved.kv?.length) {
264
+ result.kv = resolved.kv.map((k) => ({
265
+ binding: k.binding,
266
+ namespace_id: k.id,
267
+ name: k.name,
268
+ }));
269
+ }
270
+ if (resolved.ai) {
271
+ result.ai = { binding: resolved.ai.binding };
272
+ }
273
+ return result;
274
+ }
275
+
276
+ /**
277
+ * Safely parse wrangler resources, returning undefined on failure.
278
+ */
279
+ async function parseWranglerResourcesSafe(projectDir: string): Promise<ResolvedResources | undefined> {
280
+ try {
281
+ return await parseWranglerResources(projectDir);
282
+ } catch {
283
+ return undefined;
284
+ }
285
+ }
286
+
287
+ // ============================================================================
288
+ // Internal Helpers
289
+ // ============================================================================
290
+
291
+ async function getBindings(
292
+ projectDir: string,
293
+ deployMode: DeployMode,
294
+ projectId?: string,
295
+ ): Promise<{
296
+ bindings: EnvironmentBindings;
297
+ rawResources?: ControlPlaneResource[];
298
+ wranglerResources?: ResolvedResources;
299
+ }> {
300
+ const result: EnvironmentBindings = {};
301
+ let rawResources: ControlPlaneResource[] | undefined;
302
+ let wranglerResources: ResolvedResources | undefined;
303
+
304
+ if (deployMode === "managed" && projectId) {
305
+ // Managed: fetch from control plane
306
+ rawResources = await fetchProjectResources(projectId);
307
+ const resolved = convertControlPlaneResources(rawResources);
308
+ wranglerResources = resolved;
309
+
310
+ if (resolved.d1) {
311
+ result.d1 = {
312
+ binding: resolved.d1.binding,
313
+ database_name: resolved.d1.name,
314
+ database_id: resolved.d1.id,
315
+ };
316
+ }
317
+ if (resolved.r2?.length) {
318
+ result.r2 = resolved.r2.map((r) => ({
319
+ binding: r.binding,
320
+ bucket_name: r.name,
321
+ }));
322
+ }
323
+ if (resolved.kv?.length) {
324
+ result.kv = resolved.kv.map((k) => ({
325
+ binding: k.binding,
326
+ namespace_id: k.id,
327
+ name: k.name,
328
+ }));
329
+ }
330
+ if (resolved.ai) {
331
+ result.ai = { binding: resolved.ai.binding };
332
+ }
333
+ } else {
334
+ // BYO: parse from wrangler.jsonc
335
+ wranglerResources = await parseWranglerResources(projectDir);
336
+
337
+ if (wranglerResources.d1) {
338
+ result.d1 = {
339
+ binding: wranglerResources.d1.binding,
340
+ database_name: wranglerResources.d1.name,
341
+ database_id: wranglerResources.d1.id,
342
+ };
343
+ }
344
+ if (wranglerResources.r2?.length) {
345
+ result.r2 = wranglerResources.r2.map((r) => ({
346
+ binding: r.binding,
347
+ bucket_name: r.name,
348
+ }));
349
+ }
350
+ if (wranglerResources.kv?.length) {
351
+ result.kv = wranglerResources.kv.map((k) => ({
352
+ binding: k.binding,
353
+ namespace_id: k.id,
354
+ name: k.name,
355
+ }));
356
+ }
357
+ if (wranglerResources.ai) {
358
+ result.ai = { binding: wranglerResources.ai.binding };
359
+ }
360
+ }
361
+
362
+ // Also parse wrangler.jsonc for vectorize and durable_objects (not in ResolvedResources)
363
+ await enrichFromWranglerConfig(projectDir, result);
364
+
365
+ return { bindings: result, rawResources, wranglerResources };
366
+ }
367
+
368
+ /**
369
+ * Parse vectorize indexes and durable objects from wrangler.jsonc,
370
+ * since these aren't covered by the standard ResolvedResources type.
371
+ */
372
+ async function enrichFromWranglerConfig(
373
+ projectDir: string,
374
+ bindings: EnvironmentBindings,
375
+ ): Promise<void> {
376
+ const wranglerPath = findWranglerConfig(projectDir);
377
+ if (!wranglerPath) return;
378
+
379
+ try {
380
+ const { parseJsonc } = await import("../jsonc.ts");
381
+ const content = await Bun.file(wranglerPath).text();
382
+ const config = parseJsonc<{
383
+ vectorize?: Array<{ binding: string; index_name: string }>;
384
+ durable_objects?: {
385
+ bindings?: Array<{ name: string; class_name: string }>;
386
+ };
387
+ }>(content);
388
+
389
+ if (config.vectorize?.length) {
390
+ bindings.vectorize = config.vectorize.map((v) => ({
391
+ binding: v.binding,
392
+ index_name: v.index_name,
393
+ }));
394
+ }
395
+
396
+ if (config.durable_objects?.bindings?.length) {
397
+ bindings.durable_objects = config.durable_objects.bindings.map((d) => ({
398
+ binding: d.name,
399
+ class_name: d.class_name,
400
+ }));
401
+ }
402
+ } catch {
403
+ // Failed to parse, skip enrichment
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Get secret names from wrangler.jsonc (from vars that look like secrets)
409
+ * and any .dev.vars file.
410
+ */
411
+ async function getSecretsNames(projectDir: string): Promise<string[]> {
412
+ const secrets = new Set<string>();
413
+
414
+ // Check .dev.vars for secret names (these are typically what's set via wrangler secret)
415
+ const devVarsPath = join(projectDir, ".dev.vars");
416
+ if (existsSync(devVarsPath)) {
417
+ try {
418
+ const content = await Bun.file(devVarsPath).text();
419
+ for (const line of content.split("\n")) {
420
+ const trimmed = line.trim();
421
+ if (trimmed && !trimmed.startsWith("#")) {
422
+ const eqIdx = trimmed.indexOf("=");
423
+ if (eqIdx > 0) {
424
+ secrets.add(trimmed.slice(0, eqIdx).trim());
425
+ }
426
+ }
427
+ }
428
+ } catch {
429
+ // Ignore read errors
430
+ }
431
+ }
432
+
433
+ return Array.from(secrets);
434
+ }
435
+
436
+ /**
437
+ * Get database schema via SQL introspection.
438
+ */
439
+ async function getDatabaseSchema(
440
+ projectDir: string,
441
+ deployMode: DeployMode,
442
+ projectId: string | undefined,
443
+ databaseName: string,
444
+ ): Promise<DatabaseSchema | null> {
445
+ try {
446
+ // Get table list
447
+ const tablesResult = await runSql(
448
+ projectDir,
449
+ deployMode,
450
+ projectId,
451
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%'",
452
+ );
453
+
454
+ if (!tablesResult?.length) {
455
+ return { name: databaseName, tables: [] };
456
+ }
457
+
458
+ const tableNames = tablesResult.map((row) => (row as { name: string }).name);
459
+
460
+ // Get schema + row counts in parallel across all tables
461
+ const tables = await Promise.all(
462
+ tableNames.map(async (tableName) => {
463
+ const escaped = tableName.replace(/"/g, '""');
464
+ const [columnsResult, countResult] = await Promise.all([
465
+ runSql(projectDir, deployMode, projectId, `PRAGMA table_info("${escaped}")`),
466
+ runSql(projectDir, deployMode, projectId, `SELECT COUNT(*) as count FROM "${escaped}"`),
467
+ ]);
468
+
469
+ const columns: ColumnSchema[] = (columnsResult ?? []).map((col: unknown) => {
470
+ const c = col as {
471
+ name: string;
472
+ type: string;
473
+ pk: number;
474
+ notnull: number;
475
+ };
476
+ return {
477
+ name: c.name,
478
+ type: c.type,
479
+ pk: c.pk === 1,
480
+ notnull: c.notnull === 1,
481
+ };
482
+ });
483
+
484
+ const rowCount = (countResult?.[0] as { count: number })?.count ?? 0;
485
+
486
+ return { name: tableName, columns, row_count: rowCount };
487
+ }),
488
+ );
489
+
490
+ return { name: databaseName, tables };
491
+ } catch {
492
+ // DB introspection failed — return null rather than crashing the whole environment call
493
+ return null;
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Execute SQL against the project's D1 database.
499
+ * Routes through managed or BYO path.
500
+ */
501
+ async function runSql(
502
+ projectDir: string,
503
+ deployMode: DeployMode,
504
+ projectId: string | undefined,
505
+ sql: string,
506
+ ): Promise<unknown[] | null> {
507
+ if (deployMode === "managed" && projectId) {
508
+ const result = await executeManagedSql(projectId, sql);
509
+ return result.results ?? null;
510
+ }
511
+
512
+ // BYO: use executeSql service
513
+ const { executeSql } = await import("./db-execute.ts");
514
+ const result = await executeSql({
515
+ projectDir,
516
+ sql,
517
+ allowWrite: false,
518
+ interactive: false,
519
+ });
520
+ return result.results ?? null;
521
+ }
522
+
523
+ /**
524
+ * Get cron schedules for a managed project.
525
+ */
526
+ async function getCrons(projectId: string): Promise<CronEntry[]> {
527
+ try {
528
+ const schedules = await listCronSchedulesApi(projectId);
529
+ return schedules.map((s: CronScheduleInfo) => ({
530
+ expression: s.expression,
531
+ enabled: s.enabled,
532
+ last_run_status: s.last_run_status,
533
+ }));
534
+ } catch {
535
+ return [];
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Detect configuration issues by comparing bindings against known resources.
541
+ */
542
+ function detectIssues(
543
+ bindings: EnvironmentBindings,
544
+ _rawResources?: ControlPlaneResource[],
545
+ wranglerResources?: ResolvedResources,
546
+ projectDir?: string,
547
+ ): EnvironmentIssue[] {
548
+ const issues: EnvironmentIssue[] = [];
549
+
550
+ // Check for wrangler config existence
551
+ if (projectDir && !hasWranglerConfig(projectDir)) {
552
+ issues.push({
553
+ severity: "warning",
554
+ message: "No wrangler config found in project directory",
555
+ });
556
+ }
557
+
558
+ // Check for D1 binding without database_id (BYO projects)
559
+ if (bindings.d1 && !bindings.d1.database_id) {
560
+ issues.push({
561
+ severity: "warning",
562
+ message: `D1 binding '${bindings.d1.binding}' has no database_id — database may not be created yet`,
563
+ });
564
+ }
565
+
566
+ // Check for KV bindings without namespace_id
567
+ if (bindings.kv) {
568
+ for (const kv of bindings.kv) {
569
+ if (!kv.namespace_id) {
570
+ issues.push({
571
+ severity: "warning",
572
+ message: `KV binding '${kv.binding}' has no namespace_id — namespace may not be created yet`,
573
+ });
574
+ }
575
+ }
576
+ }
577
+
578
+ return issues;
579
+ }