@getjack/jack 0.1.33 → 0.1.35

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