@getjack/jack 0.1.28 → 0.1.30

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 (125) hide show
  1. package/package.json +1 -1
  2. package/src/commands/cd.ts +163 -0
  3. package/src/commands/clone.ts +112 -68
  4. package/src/commands/domain.ts +506 -0
  5. package/src/commands/domains.ts +215 -0
  6. package/src/commands/down.ts +18 -12
  7. package/src/commands/hack.ts +185 -8
  8. package/src/commands/init.ts +52 -1
  9. package/src/commands/link.ts +25 -43
  10. package/src/commands/logs.ts +2 -2
  11. package/src/commands/mcp.ts +74 -3
  12. package/src/commands/new.ts +48 -54
  13. package/src/commands/projects.ts +53 -10
  14. package/src/commands/secrets.ts +5 -1
  15. package/src/commands/services.ts +16 -4
  16. package/src/commands/shell-init.ts +43 -0
  17. package/src/commands/ship.ts +2 -11
  18. package/src/commands/skills.ts +335 -0
  19. package/src/commands/update.ts +31 -0
  20. package/src/commands/upgrade.ts +14 -0
  21. package/src/index.ts +116 -24
  22. package/src/lib/agent-integration.ts +1 -2
  23. package/src/lib/agents.ts +2 -2
  24. package/src/lib/auth/login-flow.ts +1 -1
  25. package/src/lib/clone-core.ts +252 -0
  26. package/src/lib/config.ts +22 -0
  27. package/src/lib/control-plane.ts +31 -5
  28. package/src/lib/fuzzy.ts +93 -0
  29. package/src/lib/managed-deploy.ts +4 -1
  30. package/src/lib/managed-down.ts +20 -5
  31. package/src/lib/output.ts +90 -9
  32. package/src/lib/picker.ts +406 -0
  33. package/src/lib/project-detection.ts +5 -2
  34. package/src/lib/project-list.ts +66 -5
  35. package/src/lib/project-operations.ts +68 -6
  36. package/src/lib/prompts.ts +1 -1
  37. package/src/lib/services/db-execute.ts +8 -1
  38. package/src/lib/services/db-list.ts +4 -1
  39. package/src/lib/services/domain-operations.ts +379 -0
  40. package/src/lib/services/storage-config.ts +1 -5
  41. package/src/lib/services/storage-delete.ts +1 -1
  42. package/src/lib/services/storage-info.ts +2 -4
  43. package/src/lib/services/vectorize-config.ts +1 -5
  44. package/src/lib/services/vectorize-create.ts +3 -1
  45. package/src/lib/shell-integration.ts +202 -0
  46. package/src/lib/telemetry-config.ts +50 -4
  47. package/src/lib/telemetry.ts +71 -2
  48. package/src/lib/version-check.ts +1 -3
  49. package/src/lib/wrangler-config.test.ts +2 -2
  50. package/src/lib/wrangler-config.ts +1 -1
  51. package/src/lib/zip-packager.ts +1 -3
  52. package/src/mcp/tools/index.ts +261 -7
  53. package/src/templates/index.ts +10 -1
  54. package/templates/ai-chat/.jack.json +1 -5
  55. package/templates/ai-chat/public/chat.js +130 -130
  56. package/templates/ai-chat/src/index.ts +9 -13
  57. package/templates/ai-chat/src/jack-ai.ts +6 -2
  58. package/templates/saas/.jack.json +6 -1
  59. package/templates/saas/src/auth.ts +8 -4
  60. package/templates/saas/src/client/App.tsx +22 -7
  61. package/templates/saas/src/client/components/ProtectedRoute.tsx +9 -2
  62. package/templates/saas/src/client/components/ThemeToggle.tsx +1 -6
  63. package/templates/saas/src/client/components/ui/accordion.tsx +1 -1
  64. package/templates/saas/src/client/components/ui/alert-dialog.tsx +2 -2
  65. package/templates/saas/src/client/components/ui/alert.tsx +2 -2
  66. package/templates/saas/src/client/components/ui/avatar.tsx +1 -1
  67. package/templates/saas/src/client/components/ui/badge.tsx +2 -2
  68. package/templates/saas/src/client/components/ui/breadcrumb.tsx +1 -1
  69. package/templates/saas/src/client/components/ui/button-group.tsx +2 -2
  70. package/templates/saas/src/client/components/ui/button.tsx +2 -2
  71. package/templates/saas/src/client/components/ui/card.tsx +1 -1
  72. package/templates/saas/src/client/components/ui/carousel.tsx +2 -2
  73. package/templates/saas/src/client/components/ui/checkbox.tsx +1 -1
  74. package/templates/saas/src/client/components/ui/command.tsx +2 -2
  75. package/templates/saas/src/client/components/ui/context-menu.tsx +1 -1
  76. package/templates/saas/src/client/components/ui/dialog.tsx +1 -1
  77. package/templates/saas/src/client/components/ui/drawer.tsx +1 -1
  78. package/templates/saas/src/client/components/ui/dropdown-menu.tsx +1 -1
  79. package/templates/saas/src/client/components/ui/empty.tsx +1 -1
  80. package/templates/saas/src/client/components/ui/field.tsx +2 -2
  81. package/templates/saas/src/client/components/ui/form.tsx +5 -5
  82. package/templates/saas/src/client/components/ui/hover-card.tsx +1 -1
  83. package/templates/saas/src/client/components/ui/input-group.tsx +3 -3
  84. package/templates/saas/src/client/components/ui/input-otp.tsx +1 -1
  85. package/templates/saas/src/client/components/ui/input.tsx +1 -1
  86. package/templates/saas/src/client/components/ui/item.tsx +3 -3
  87. package/templates/saas/src/client/components/ui/label.tsx +1 -1
  88. package/templates/saas/src/client/components/ui/menubar.tsx +1 -1
  89. package/templates/saas/src/client/components/ui/navigation-menu.tsx +1 -1
  90. package/templates/saas/src/client/components/ui/pagination.tsx +2 -2
  91. package/templates/saas/src/client/components/ui/popover.tsx +1 -1
  92. package/templates/saas/src/client/components/ui/progress.tsx +1 -1
  93. package/templates/saas/src/client/components/ui/radio-group.tsx +1 -1
  94. package/templates/saas/src/client/components/ui/resizable.tsx +1 -1
  95. package/templates/saas/src/client/components/ui/scroll-area.tsx +1 -1
  96. package/templates/saas/src/client/components/ui/select.tsx +1 -1
  97. package/templates/saas/src/client/components/ui/separator.tsx +1 -1
  98. package/templates/saas/src/client/components/ui/sheet.tsx +1 -1
  99. package/templates/saas/src/client/components/ui/sidebar.tsx +4 -4
  100. package/templates/saas/src/client/components/ui/slider.tsx +1 -1
  101. package/templates/saas/src/client/components/ui/switch.tsx +1 -1
  102. package/templates/saas/src/client/components/ui/table.tsx +1 -1
  103. package/templates/saas/src/client/components/ui/tabs.tsx +1 -1
  104. package/templates/saas/src/client/components/ui/textarea.tsx +1 -1
  105. package/templates/saas/src/client/components/ui/toggle-group.tsx +3 -3
  106. package/templates/saas/src/client/components/ui/toggle.tsx +2 -2
  107. package/templates/saas/src/client/components/ui/tooltip.tsx +1 -1
  108. package/templates/saas/src/client/hooks/useSubscription.ts +5 -4
  109. package/templates/saas/src/client/lib/auth-client.ts +1 -1
  110. package/templates/saas/src/client/lib/plans.ts +1 -6
  111. package/templates/saas/src/client/lib/utils.ts +1 -1
  112. package/templates/saas/src/client/main.tsx +1 -1
  113. package/templates/saas/src/client/pages/DashboardPage.tsx +41 -9
  114. package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +11 -2
  115. package/templates/saas/src/client/pages/HomePage.tsx +11 -2
  116. package/templates/saas/src/client/pages/LoginPage.tsx +11 -2
  117. package/templates/saas/src/client/pages/PricingPage.tsx +20 -10
  118. package/templates/saas/src/client/pages/ResetPasswordPage.tsx +14 -11
  119. package/templates/saas/src/client/pages/SignupPage.tsx +11 -2
  120. package/templates/saas/src/index.ts +28 -19
  121. package/templates/saas/vite.config.ts +1 -1
  122. package/templates/semantic-search/.jack.json +1 -5
  123. package/templates/semantic-search/src/index.ts +8 -4
  124. package/templates/semantic-search/src/jack-ai.ts +6 -2
  125. package/templates/semantic-search/src/jack-vectorize.ts +5 -1
@@ -479,7 +479,14 @@ export async function executeSql(options: ExecuteSqlOptions): Promise<ExecuteSql
479
479
  export async function executeSqlFile(
480
480
  options: Omit<ExecuteSqlOptions, "sql"> & { filePath: string },
481
481
  ): Promise<ExecuteSqlResult> {
482
- const { projectDir, filePath, databaseName, allowWrite = false, interactive = true, confirmed = false } = options;
482
+ const {
483
+ projectDir,
484
+ filePath,
485
+ databaseName,
486
+ allowWrite = false,
487
+ interactive = true,
488
+ confirmed = false,
489
+ } = options;
483
490
 
484
491
  // Read the file
485
492
  if (!existsSync(filePath)) {
@@ -63,7 +63,10 @@ export async function listDatabases(projectDir: string): Promise<DatabaseListEnt
63
63
  // Get metadata based on deploy mode
64
64
  if (isManaged && managedDbInfo) {
65
65
  // For managed: use control plane data (match by ID or name)
66
- if (managedDbInfo.id === binding.database_id || managedDbInfo.name.includes(binding.database_name)) {
66
+ if (
67
+ managedDbInfo.id === binding.database_id ||
68
+ managedDbInfo.name.includes(binding.database_name)
69
+ ) {
67
70
  entry.sizeBytes = managedDbInfo.sizeBytes;
68
71
  entry.numTables = managedDbInfo.numTables;
69
72
  }
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Domain operations service layer for jack cloud
3
+ *
4
+ * Provides shared domain management functions for both CLI and MCP.
5
+ * Returns pure data - no console.log or process.exit.
6
+ */
7
+
8
+ import { authFetch } from "../auth/index.ts";
9
+ import { findProjectBySlug, getControlApiUrl } from "../control-plane.ts";
10
+ import { JackError, JackErrorCode } from "../errors.ts";
11
+
12
+ // ============================================================================
13
+ // Types
14
+ // ============================================================================
15
+
16
+ export type DomainStatus =
17
+ | "claimed"
18
+ | "pending"
19
+ | "pending_owner"
20
+ | "pending_ssl"
21
+ | "active"
22
+ | "blocked"
23
+ | "moved"
24
+ | "failed"
25
+ | "deleting";
26
+
27
+ export interface DomainVerification {
28
+ type: "cname";
29
+ target: string;
30
+ instructions: string;
31
+ }
32
+
33
+ export interface DomainOwnershipVerification {
34
+ type: "txt";
35
+ name: string;
36
+ value: string;
37
+ }
38
+
39
+ export interface DomainInfo {
40
+ id: string;
41
+ hostname: string;
42
+ status: DomainStatus;
43
+ ssl_status: string | null;
44
+ project_id: string | null;
45
+ project_slug: string | null;
46
+ verification?: DomainVerification;
47
+ ownership_verification?: DomainOwnershipVerification;
48
+ created_at: string;
49
+ }
50
+
51
+ export interface DomainSlots {
52
+ used: number;
53
+ max: number;
54
+ }
55
+
56
+ export interface ListDomainsResult {
57
+ domains: DomainInfo[];
58
+ slots: DomainSlots;
59
+ }
60
+
61
+ export interface ConnectDomainResult {
62
+ id: string;
63
+ hostname: string;
64
+ status: DomainStatus;
65
+ }
66
+
67
+ export interface AssignDomainResult {
68
+ id: string;
69
+ hostname: string;
70
+ status: DomainStatus;
71
+ ssl_status: string | null;
72
+ project_id: string;
73
+ project_slug: string;
74
+ verification?: DomainVerification;
75
+ ownership_verification?: DomainOwnershipVerification;
76
+ }
77
+
78
+ export interface UnassignDomainResult {
79
+ id: string;
80
+ hostname: string;
81
+ status: DomainStatus;
82
+ }
83
+
84
+ export interface DisconnectDomainResult {
85
+ success: boolean;
86
+ hostname: string;
87
+ }
88
+
89
+ // ============================================================================
90
+ // API Response Types (internal)
91
+ // ============================================================================
92
+
93
+ interface ListDomainsApiResponse {
94
+ domains: DomainInfo[];
95
+ slots: DomainSlots;
96
+ }
97
+
98
+ interface ConnectDomainApiResponse {
99
+ id: string;
100
+ hostname: string;
101
+ status: string;
102
+ }
103
+
104
+ interface AssignDomainApiResponse {
105
+ id: string;
106
+ hostname: string;
107
+ status: string;
108
+ ssl_status: string | null;
109
+ verification?: DomainVerification;
110
+ ownership_verification?: DomainOwnershipVerification;
111
+ }
112
+
113
+ interface ApiErrorResponse {
114
+ message?: string;
115
+ error?: string;
116
+ }
117
+
118
+ // ============================================================================
119
+ // Error Codes for Domain Operations
120
+ // ============================================================================
121
+
122
+ /**
123
+ * Extended error codes for domain operations.
124
+ * Maps to JackErrorCode where possible, uses specific codes where needed.
125
+ */
126
+ export const DomainErrorCode = {
127
+ PLAN_LIMIT_REACHED: "PLAN_LIMIT_REACHED",
128
+ RESOURCE_NOT_FOUND: "RESOURCE_NOT_FOUND",
129
+ ALREADY_EXISTS: "ALREADY_EXISTS",
130
+ ALREADY_ASSIGNED: "ALREADY_ASSIGNED",
131
+ NOT_ASSIGNED: "NOT_ASSIGNED",
132
+ } as const;
133
+
134
+ export type DomainErrorCodeType = (typeof DomainErrorCode)[keyof typeof DomainErrorCode];
135
+
136
+ // ============================================================================
137
+ // Service Functions
138
+ // ============================================================================
139
+
140
+ /**
141
+ * List all domains for the current user.
142
+ */
143
+ export async function listDomains(): Promise<ListDomainsResult> {
144
+ const response = await authFetch(`${getControlApiUrl()}/v1/domains`);
145
+
146
+ if (!response.ok) {
147
+ const err = (await response
148
+ .json()
149
+ .catch(() => ({ message: "Unknown error" }))) as ApiErrorResponse;
150
+ throw new JackError(
151
+ JackErrorCode.INTERNAL_ERROR,
152
+ err.message || `Failed to list domains: ${response.status}`,
153
+ );
154
+ }
155
+
156
+ const data = (await response.json()) as ListDomainsApiResponse;
157
+ return {
158
+ domains: data.domains,
159
+ slots: data.slots,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Find a domain by hostname.
165
+ * Returns null if not found.
166
+ */
167
+ export async function getDomainByHostname(hostname: string): Promise<DomainInfo | null> {
168
+ const result = await listDomains();
169
+ return result.domains.find((d) => d.hostname === hostname) ?? null;
170
+ }
171
+
172
+ /**
173
+ * Reserve a domain slot (connect a domain).
174
+ *
175
+ * @throws JackError with PLAN_LIMIT_REACHED if no slots available
176
+ * @throws JackError with ALREADY_EXISTS if domain already reserved
177
+ */
178
+ export async function connectDomain(hostname: string): Promise<ConnectDomainResult> {
179
+ const response = await authFetch(`${getControlApiUrl()}/v1/domains`, {
180
+ method: "POST",
181
+ headers: { "Content-Type": "application/json" },
182
+ body: JSON.stringify({ hostname }),
183
+ });
184
+
185
+ if (!response.ok) {
186
+ const err = (await response
187
+ .json()
188
+ .catch(() => ({ message: "Unknown error" }))) as ApiErrorResponse;
189
+
190
+ // Handle plan limit errors
191
+ if (response.status === 403 || err.error === "plan_limit_reached") {
192
+ throw new JackError(
193
+ JackErrorCode.VALIDATION_ERROR,
194
+ "No domain slots available",
195
+ "Upgrade your plan for more slots: jack upgrade",
196
+ { exitCode: 1 },
197
+ );
198
+ }
199
+
200
+ // Handle "already exists"
201
+ if (response.status === 409 || err.error === "domain_exists") {
202
+ throw new JackError(
203
+ JackErrorCode.VALIDATION_ERROR,
204
+ `Domain ${hostname} is already reserved`,
205
+ "Run 'jack domain' to see all domains",
206
+ { exitCode: 1 },
207
+ );
208
+ }
209
+
210
+ throw new JackError(
211
+ JackErrorCode.INTERNAL_ERROR,
212
+ err.message || `Failed to reserve domain: ${response.status}`,
213
+ );
214
+ }
215
+
216
+ const data = (await response.json()) as ConnectDomainApiResponse;
217
+ return {
218
+ id: data.id,
219
+ hostname: data.hostname,
220
+ status: data.status as DomainStatus,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Assign a reserved domain to a project.
226
+ *
227
+ * @throws JackError with RESOURCE_NOT_FOUND if domain or project not found
228
+ * @throws JackError with ALREADY_ASSIGNED if domain already assigned to a project
229
+ */
230
+ export async function assignDomain(
231
+ hostname: string,
232
+ projectSlug: string,
233
+ ): Promise<AssignDomainResult> {
234
+ // Find the domain
235
+ const domain = await getDomainByHostname(hostname);
236
+ if (!domain) {
237
+ throw new JackError(
238
+ JackErrorCode.PROJECT_NOT_FOUND,
239
+ `Domain not found: ${hostname}`,
240
+ "Reserve it first: jack domain connect <hostname>",
241
+ { exitCode: 1 },
242
+ );
243
+ }
244
+
245
+ // Find the project
246
+ const project = await findProjectBySlug(projectSlug);
247
+ if (!project) {
248
+ throw new JackError(
249
+ JackErrorCode.PROJECT_NOT_FOUND,
250
+ `Project not found: ${projectSlug}`,
251
+ "Check your projects: jack ls",
252
+ { exitCode: 1 },
253
+ );
254
+ }
255
+
256
+ const response = await authFetch(`${getControlApiUrl()}/v1/domains/${domain.id}/assign`, {
257
+ method: "POST",
258
+ headers: { "Content-Type": "application/json" },
259
+ body: JSON.stringify({ project_id: project.id }),
260
+ });
261
+
262
+ if (!response.ok) {
263
+ const err = (await response
264
+ .json()
265
+ .catch(() => ({ message: "Unknown error" }))) as ApiErrorResponse;
266
+
267
+ // Handle already assigned
268
+ if (err.error === "already_assigned") {
269
+ throw new JackError(
270
+ JackErrorCode.VALIDATION_ERROR,
271
+ "Domain is already assigned to a project",
272
+ "Unassign it first: jack domain unassign <hostname>",
273
+ { exitCode: 1 },
274
+ );
275
+ }
276
+
277
+ throw new JackError(
278
+ JackErrorCode.INTERNAL_ERROR,
279
+ err.message || `Failed to assign domain: ${response.status}`,
280
+ );
281
+ }
282
+
283
+ const data = (await response.json()) as AssignDomainApiResponse;
284
+ return {
285
+ id: data.id,
286
+ hostname: data.hostname,
287
+ status: data.status as DomainStatus,
288
+ ssl_status: data.ssl_status,
289
+ project_id: project.id,
290
+ project_slug: projectSlug,
291
+ verification: data.verification,
292
+ ownership_verification: data.ownership_verification,
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Unassign a domain from its project (keep the slot).
298
+ *
299
+ * @throws JackError with RESOURCE_NOT_FOUND if domain not found
300
+ * @throws JackError with NOT_ASSIGNED if domain is not assigned to any project
301
+ */
302
+ export async function unassignDomain(hostname: string): Promise<UnassignDomainResult> {
303
+ // Find the domain
304
+ const domain = await getDomainByHostname(hostname);
305
+ if (!domain) {
306
+ throw new JackError(
307
+ JackErrorCode.PROJECT_NOT_FOUND,
308
+ `Domain not found: ${hostname}`,
309
+ "Run 'jack domain' to see all domains",
310
+ { exitCode: 1 },
311
+ );
312
+ }
313
+
314
+ if (!domain.project_id) {
315
+ throw new JackError(
316
+ JackErrorCode.VALIDATION_ERROR,
317
+ `Domain ${hostname} is not assigned to any project`,
318
+ undefined,
319
+ { exitCode: 1 },
320
+ );
321
+ }
322
+
323
+ const response = await authFetch(`${getControlApiUrl()}/v1/domains/${domain.id}/unassign`, {
324
+ method: "POST",
325
+ });
326
+
327
+ if (!response.ok) {
328
+ const err = (await response
329
+ .json()
330
+ .catch(() => ({ message: "Unknown error" }))) as ApiErrorResponse;
331
+ throw new JackError(
332
+ JackErrorCode.INTERNAL_ERROR,
333
+ err.message || `Failed to unassign domain: ${response.status}`,
334
+ );
335
+ }
336
+
337
+ return {
338
+ id: domain.id,
339
+ hostname: domain.hostname,
340
+ status: "claimed" as DomainStatus,
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Disconnect (fully remove) a domain.
346
+ *
347
+ * @throws JackError with RESOURCE_NOT_FOUND if domain not found
348
+ */
349
+ export async function disconnectDomain(hostname: string): Promise<DisconnectDomainResult> {
350
+ // Find the domain
351
+ const domain = await getDomainByHostname(hostname);
352
+ if (!domain) {
353
+ throw new JackError(
354
+ JackErrorCode.PROJECT_NOT_FOUND,
355
+ `Domain not found: ${hostname}`,
356
+ "Run 'jack domain' to see all domains",
357
+ { exitCode: 1 },
358
+ );
359
+ }
360
+
361
+ const response = await authFetch(`${getControlApiUrl()}/v1/domains/${domain.id}`, {
362
+ method: "DELETE",
363
+ });
364
+
365
+ if (!response.ok) {
366
+ const err = (await response
367
+ .json()
368
+ .catch(() => ({ message: "Unknown error" }))) as ApiErrorResponse;
369
+ throw new JackError(
370
+ JackErrorCode.INTERNAL_ERROR,
371
+ err.message || `Failed to disconnect domain: ${response.status}`,
372
+ );
373
+ }
374
+
375
+ return {
376
+ success: true,
377
+ hostname: domain.hostname,
378
+ };
379
+ }
@@ -643,11 +643,7 @@ function findObjectEndAfter(content: string, fromPos: number): number {
643
643
  /**
644
644
  * Remove the entire r2_buckets property when it becomes empty.
645
645
  */
646
- function removeR2BucketsProperty(
647
- content: string,
648
- propertyStart: number,
649
- arrayEnd: number,
650
- ): string {
646
+ function removeR2BucketsProperty(content: string, propertyStart: number, arrayEnd: number): string {
651
647
  let removeStart = propertyStart;
652
648
  let removeEnd = arrayEnd + 1;
653
649
 
@@ -59,7 +59,7 @@ export async function deleteStorageBucket(
59
59
  throw new Error(`Bucket "${bucketName}" not found in this project.`);
60
60
  }
61
61
 
62
- let deleted = true;
62
+ const deleted = true;
63
63
 
64
64
  if (link.deploy_mode === "managed") {
65
65
  // Managed mode: delete via control plane (don't call wrangler - user may not have CF auth)
@@ -5,8 +5,8 @@
5
5
  * since R2 doesn't have a simple stats API via wrangler.
6
6
  */
7
7
 
8
- import { $ } from "bun";
9
8
  import { join } from "node:path";
9
+ import { $ } from "bun";
10
10
  import { fetchProjectResources } from "../control-plane.ts";
11
11
  import { readProjectLink } from "../project-link.ts";
12
12
  import { getExistingR2Bindings } from "./storage-config.ts";
@@ -59,9 +59,7 @@ export async function getStorageBucketInfo(
59
59
  }
60
60
 
61
61
  // Find the requested bucket (or first if not specified)
62
- const binding = bucketName
63
- ? bindings.find((b) => b.bucket_name === bucketName)
64
- : bindings[0];
62
+ const binding = bucketName ? bindings.find((b) => b.bucket_name === bucketName) : bindings[0];
65
63
 
66
64
  if (!binding) {
67
65
  return null;
@@ -257,11 +257,7 @@ function removeVectorizeEntryFromContent(content: string, indexName: string): st
257
257
  /**
258
258
  * Remove the entire vectorize property when it becomes empty.
259
259
  */
260
- function removeVectorizeProperty(
261
- content: string,
262
- propertyStart: number,
263
- arrayEnd: number,
264
- ): string {
260
+ function removeVectorizeProperty(content: string, propertyStart: number, arrayEnd: number): string {
265
261
  let removeStart = propertyStart;
266
262
  let removeEnd = arrayEnd + 1;
267
263
 
@@ -102,7 +102,9 @@ async function createIndexViaWrangler(
102
102
  }
103
103
 
104
104
  const result =
105
- await $`wrangler vectorize create ${indexName} --dimensions=${dimensions} --metric=${metric}`.nothrow().quiet();
105
+ await $`wrangler vectorize create ${indexName} --dimensions=${dimensions} --metric=${metric}`
106
+ .nothrow()
107
+ .quiet();
106
108
 
107
109
  if (result.exitCode !== 0) {
108
110
  const stderr = result.stderr.toString().trim();
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Shell integration for jack cd/new/clone to auto-change directories.
3
+ * Shell function lives in ~/.config/jack/shell.sh, sourced from rc file.
4
+ * jack update regenerates the managed file.
5
+ */
6
+
7
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+ import pkg from "../../package.json";
11
+
12
+ type Shell = "bash" | "zsh" | "unknown";
13
+
14
+ // Markers for detection
15
+ const LEGACY_MARKER = "# jack shell integration";
16
+ const SOURCE_LINE_MARKER = "# jack: shell integration (do not edit)";
17
+
18
+ export function getShellFilePath(): string {
19
+ return join(homedir(), ".config", "jack", "shell.sh");
20
+ }
21
+
22
+ export function getSourceLine(): string {
23
+ const shellFile = getShellFilePath();
24
+ return `${SOURCE_LINE_MARKER}\n[ -f "${shellFile}" ] && source "${shellFile}"`;
25
+ }
26
+
27
+ function getShellFunction(): string {
28
+ return `# jack shell integration v${pkg.version}
29
+ # This file is managed by jack. Do not edit manually.
30
+ # It will be regenerated by 'jack update'.
31
+
32
+ jack() {
33
+ case "$1" in
34
+ cd)
35
+ # Get project path and cd to it
36
+ local dir
37
+ dir="$(command jack cd "\${@:2}" 2>/dev/null)"
38
+ if [[ -n "$dir" && -d "$dir" ]]; then
39
+ cd "$dir" || return 1
40
+ else
41
+ # Show error output if cd failed
42
+ command jack cd "\${@:2}"
43
+ fi
44
+ ;;
45
+ new|clone)
46
+ # Run the command, then cd to the project
47
+ command jack "$@"
48
+ local exit_code=$?
49
+ if [[ $exit_code -eq 0 ]]; then
50
+ # Extract project name from args (skip flags)
51
+ local name=""
52
+ shift # remove 'new' or 'clone'
53
+ for arg in "$@"; do
54
+ if [[ ! "$arg" =~ ^- ]]; then
55
+ name="$arg"
56
+ break
57
+ fi
58
+ done
59
+ if [[ -n "$name" ]]; then
60
+ local dir
61
+ dir="$(command jack cd "$name" 2>/dev/null)"
62
+ [[ -n "$dir" && -d "$dir" ]] && cd "$dir"
63
+ fi
64
+ fi
65
+ return $exit_code
66
+ ;;
67
+ "")
68
+ # No args: interactive picker - cd if it outputs a directory
69
+ local output
70
+ output="$(command jack)"
71
+ if [[ -n "$output" && -d "$output" ]]; then
72
+ cd "$output" || return 1
73
+ elif [[ -n "$output" ]]; then
74
+ # Not a directory (e.g., help text) - show it
75
+ echo "$output"
76
+ fi
77
+ ;;
78
+ *)
79
+ # Pass through all other commands
80
+ command jack "$@"
81
+ ;;
82
+ esac
83
+ }`;
84
+ }
85
+
86
+ export function detectShell(): Shell {
87
+ const shell = process.env.SHELL || "";
88
+ if (shell.endsWith("/bash") || shell.endsWith("/bash5")) return "bash";
89
+ if (shell.endsWith("/zsh")) return "zsh";
90
+ return "unknown";
91
+ }
92
+
93
+ export function getRcFilePath(shell: Shell): string | null {
94
+ const home = homedir();
95
+
96
+ switch (shell) {
97
+ case "zsh":
98
+ return join(home, ".zshrc");
99
+ case "bash": {
100
+ // Prefer .bashrc, fall back to .bash_profile on macOS
101
+ const bashrc = join(home, ".bashrc");
102
+ const profile = join(home, ".bash_profile");
103
+ if (existsSync(bashrc)) return bashrc;
104
+ if (existsSync(profile)) return profile;
105
+ // Default to .bashrc (will be created)
106
+ return bashrc;
107
+ }
108
+ default:
109
+ return null;
110
+ }
111
+ }
112
+
113
+ export function isInstalled(rcFile: string): boolean {
114
+ if (!existsSync(rcFile)) return false;
115
+
116
+ try {
117
+ const content = readFileSync(rcFile, "utf-8");
118
+ return content.includes(SOURCE_LINE_MARKER);
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ export function hasLegacyInstall(rcFile: string): boolean {
125
+ if (!existsSync(rcFile)) return false;
126
+
127
+ try {
128
+ const content = readFileSync(rcFile, "utf-8");
129
+ // Has old marker but not new source line
130
+ return content.includes(LEGACY_MARKER) && !content.includes(SOURCE_LINE_MARKER);
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ export function removeLegacyInstall(rcFile: string): void {
137
+ if (!existsSync(rcFile)) return;
138
+
139
+ const content = readFileSync(rcFile, "utf-8");
140
+
141
+ const legacyPattern = /\n?# jack shell integration\njack\(\) \{[\s\S]*?\n\}\n?/g;
142
+
143
+ const newContent = content.replace(legacyPattern, "\n");
144
+
145
+ writeFileSync(rcFile, newContent, "utf-8");
146
+ }
147
+
148
+ export function writeShellFile(): void {
149
+ const shellFile = getShellFilePath();
150
+ const dir = dirname(shellFile);
151
+
152
+ mkdirSync(dir, { recursive: true });
153
+ writeFileSync(shellFile, getShellFunction(), "utf-8");
154
+ }
155
+
156
+ export function addSourceLine(rcFile: string): void {
157
+ const sourceLine = getSourceLine();
158
+ appendFileSync(rcFile, `\n${sourceLine}\n`, "utf-8");
159
+ }
160
+
161
+ export function install(rcFile: string): { migrated: boolean } {
162
+ let migrated = false;
163
+
164
+ if (hasLegacyInstall(rcFile)) {
165
+ removeLegacyInstall(rcFile);
166
+ migrated = true;
167
+ }
168
+
169
+ writeShellFile();
170
+ if (!isInstalled(rcFile)) {
171
+ addSourceLine(rcFile);
172
+ }
173
+
174
+ return { migrated };
175
+ }
176
+
177
+ export function update(): void {
178
+ writeShellFile();
179
+ }
180
+
181
+ export function getShellCode(): string {
182
+ return getShellFunction();
183
+ }
184
+
185
+ export function getShellName(shell: Shell): string {
186
+ switch (shell) {
187
+ case "bash":
188
+ return "Bash";
189
+ case "zsh":
190
+ return "Zsh";
191
+ default:
192
+ return "your shell";
193
+ }
194
+ }
195
+
196
+ export function getRcFileName(rcFile: string): string {
197
+ return rcFile.split("/").pop() || rcFile;
198
+ }
199
+
200
+ export function getShellFileDisplayPath(): string {
201
+ return "~/.config/jack/shell.sh";
202
+ }