@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
@@ -0,0 +1,506 @@
1
+ /**
2
+ * jack domain - Manage custom domains (slot-based workflow)
3
+ *
4
+ * Slots allow you to reserve domains before assigning them to projects.
5
+ * Only available for paid plans.
6
+ *
7
+ * Workflow:
8
+ * 1. connect <hostname> - Reserve a slot
9
+ * 2. assign <hostname> <project> - Provision to Cloudflare
10
+ * 3. unassign <hostname> - Remove from CF, keep slot
11
+ * 4. disconnect <hostname> - Full removal, free slot
12
+ */
13
+
14
+ import { JackError } from "../lib/errors.ts";
15
+ import { isCancel, promptSelect } from "../lib/hooks.ts";
16
+ import { colors, error, info, output, success, warn } from "../lib/output.ts";
17
+ import {
18
+ type DomainInfo,
19
+ type DomainOwnershipVerification,
20
+ type DomainStatus,
21
+ type DomainVerification,
22
+ assignDomain as assignDomainService,
23
+ connectDomain as connectDomainService,
24
+ disconnectDomain as disconnectDomainService,
25
+ getDomainByHostname,
26
+ listDomains as listDomainsService,
27
+ unassignDomain as unassignDomainService,
28
+ } from "../lib/services/domain-operations.ts";
29
+
30
+ export default async function domain(subcommand?: string, args: string[] = []): Promise<void> {
31
+ // Handle help flags
32
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
33
+ return showHelp();
34
+ }
35
+
36
+ // No subcommand = show status (not help)
37
+ if (!subcommand) {
38
+ return await listDomainsCommand();
39
+ }
40
+
41
+ switch (subcommand) {
42
+ case "connect":
43
+ return await connectDomainCommand(args);
44
+ case "assign":
45
+ return await assignDomainCommand(args);
46
+ case "unassign":
47
+ return await unassignDomainCommand(args);
48
+ case "disconnect":
49
+ return await disconnectDomainCommand(args);
50
+ case "list":
51
+ case "ls":
52
+ return await listDomainsCommand();
53
+ default:
54
+ error(`Unknown subcommand: ${subcommand}`);
55
+ info("Available: connect, assign, unassign, disconnect, list, help");
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ function showHelp(): void {
61
+ console.error("");
62
+ info("jack domain - Manage custom domains");
63
+ console.error("");
64
+ console.error("Usage:");
65
+ console.error(" jack domain Show all domains and slot usage");
66
+ console.error(" jack domain connect <hostname> Reserve a domain slot");
67
+ console.error(" jack domain assign <hostname> <project> Provision domain to project");
68
+ console.error(" jack domain unassign <hostname> Remove from project, keep slot");
69
+ console.error(" jack domain disconnect <hostname> Remove completely, free slot");
70
+ console.error("");
71
+ console.error("Workflow:");
72
+ console.error(" 1. connect - Reserve hostname (uses a slot)");
73
+ console.error(" 2. assign - Point domain to a project (configures DNS)");
74
+ console.error(" 3. unassign - Remove from project but keep slot reserved");
75
+ console.error(" 4. disconnect - Full removal, slot freed");
76
+ console.error("");
77
+ console.error("Examples:");
78
+ console.error(" jack domain connect api.mycompany.com");
79
+ console.error(" jack domain assign api.mycompany.com my-api");
80
+ console.error(" jack domain unassign api.mycompany.com");
81
+ console.error(" jack domain disconnect api.mycompany.com");
82
+ console.error("");
83
+ }
84
+
85
+ /**
86
+ * Get status icon for domain status
87
+ */
88
+ function getStatusIcon(status: DomainStatus): string {
89
+ switch (status) {
90
+ case "active":
91
+ return `${colors.green}✓${colors.reset}`;
92
+ case "claimed":
93
+ return `${colors.dim}○${colors.reset}`;
94
+ case "pending":
95
+ case "pending_owner":
96
+ case "pending_ssl":
97
+ return `${colors.yellow}⏳${colors.reset}`;
98
+ case "failed":
99
+ case "blocked":
100
+ return `${colors.red}✗${colors.reset}`;
101
+ case "moved":
102
+ case "deleting":
103
+ return `${colors.cyan}○${colors.reset}`;
104
+ default:
105
+ return "○";
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Get human-readable status label
111
+ */
112
+ function getStatusLabel(status: DomainStatus): string {
113
+ switch (status) {
114
+ case "active":
115
+ return "active";
116
+ case "claimed":
117
+ return "unassigned";
118
+ case "pending":
119
+ return "pending DNS";
120
+ case "pending_owner":
121
+ return "pending ownership";
122
+ case "pending_ssl":
123
+ return "pending SSL";
124
+ case "failed":
125
+ return "failed";
126
+ case "blocked":
127
+ return "blocked";
128
+ case "moved":
129
+ return "moved";
130
+ case "deleting":
131
+ return "deleting";
132
+ default:
133
+ return status;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Show DNS configuration instructions for pending domains
139
+ */
140
+ function showDnsInstructions(
141
+ hostname: string,
142
+ verification?: DomainVerification,
143
+ ownershipVerification?: DomainOwnershipVerification,
144
+ ): void {
145
+ // Extract the base domain (e.g., "hellno.wtf" from "app.hellno.wtf")
146
+ const parts = hostname.split(".");
147
+ const baseDomain = parts.slice(-2).join(".");
148
+ const subdomain = parts.slice(0, -2).join(".");
149
+
150
+ console.error(
151
+ ` ${colors.cyan}Add these records to your DNS provider for ${colors.bold}${baseDomain}${colors.reset}${colors.cyan}:${colors.reset}`,
152
+ );
153
+ console.error("");
154
+
155
+ let step = 1;
156
+
157
+ if (verification) {
158
+ console.error(
159
+ ` ${colors.bold}${step}. CNAME${colors.reset} ${colors.cyan}(routes traffic)${colors.reset}`,
160
+ );
161
+ console.error(
162
+ ` ${colors.cyan}Name:${colors.reset} ${colors.green}${subdomain || "@"}${colors.reset}`,
163
+ );
164
+ console.error(
165
+ ` ${colors.cyan}Value:${colors.reset} ${colors.green}${verification.target}${colors.reset}`,
166
+ );
167
+ step++;
168
+ }
169
+
170
+ if (ownershipVerification) {
171
+ if (step > 1) console.error("");
172
+ // Extract just the subdomain part for the TXT name
173
+ const txtSubdomain = ownershipVerification.name.replace(`.${baseDomain}`, "");
174
+ console.error(
175
+ ` ${colors.bold}${step}. TXT${colors.reset} ${colors.cyan}(proves ownership)${colors.reset}`,
176
+ );
177
+ console.error(
178
+ ` ${colors.cyan}Name:${colors.reset} ${colors.green}${txtSubdomain}${colors.reset}`,
179
+ );
180
+ console.error(
181
+ ` ${colors.cyan}Value:${colors.reset} ${colors.green}${ownershipVerification.value}${colors.reset}`,
182
+ );
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Get seconds until next cron check (runs on the minute, ~5s to process)
188
+ */
189
+ function getSecondsUntilNextCheck(): number {
190
+ const now = new Date();
191
+ const secondsIntoMinute = now.getSeconds();
192
+ // Cron runs at :00, takes ~5s to process pending domains
193
+ const secondsUntilNextMinute = 60 - secondsIntoMinute;
194
+ return secondsUntilNextMinute + 5;
195
+ }
196
+
197
+ /**
198
+ * List domains and show status
199
+ */
200
+ async function listDomainsCommand(): Promise<void> {
201
+ output.start("Loading domains...");
202
+
203
+ try {
204
+ const data = await listDomainsService();
205
+ output.stop();
206
+
207
+ console.error("");
208
+
209
+ // Show slot usage
210
+ const { slots } = data;
211
+ console.error(` Slots: ${slots.used}/${slots.max} used`);
212
+ console.error("");
213
+
214
+ if (data.domains.length === 0) {
215
+ info("No custom domains configured.");
216
+ console.error("");
217
+ info("Reserve a domain: jack domain connect <hostname>");
218
+ return;
219
+ }
220
+
221
+ info("Custom domains:");
222
+ console.error("");
223
+
224
+ // Group domains by status
225
+ const pendingDomains: DomainInfo[] = [];
226
+
227
+ for (const d of data.domains) {
228
+ const icon = getStatusIcon(d.status);
229
+ const label = getStatusLabel(d.status);
230
+
231
+ if (d.status === "active") {
232
+ // Show clickable URL for active domains
233
+ const projectInfo = d.project_slug ? ` -> ${d.project_slug}` : "";
234
+ console.error(
235
+ ` ${icon} ${colors.green}https://${d.hostname}${colors.reset}${colors.cyan}${projectInfo}${colors.reset}`,
236
+ );
237
+ } else if (d.status === "claimed") {
238
+ // Reserved but not assigned
239
+ console.error(
240
+ ` ${icon} ${colors.dim}${d.hostname}${colors.reset} ${colors.cyan}(${label})${colors.reset}`,
241
+ );
242
+ } else {
243
+ // Pending states
244
+ const projectInfo = d.project_slug ? ` -> ${d.project_slug}` : "";
245
+ console.error(
246
+ ` ${icon} ${colors.cyan}${d.hostname}${colors.reset}${projectInfo} ${colors.yellow}(${label})${colors.reset}`,
247
+ );
248
+ if (d.verification || d.ownership_verification) {
249
+ pendingDomains.push(d);
250
+ }
251
+ }
252
+ }
253
+
254
+ // Show DNS instructions for pending domains
255
+ if (pendingDomains.length > 0) {
256
+ for (const d of pendingDomains) {
257
+ console.error("");
258
+ showDnsInstructions(d.hostname, d.verification, d.ownership_verification);
259
+ }
260
+ const nextCheck = getSecondsUntilNextCheck();
261
+ console.error("");
262
+ console.error(` ${colors.cyan}Next auto-check in ~${nextCheck}s${colors.reset}`);
263
+ }
264
+
265
+ console.error("");
266
+ } catch (err) {
267
+ output.stop();
268
+ throw err;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Connect (reserve) a domain slot
274
+ */
275
+ async function connectDomainCommand(args: string[]): Promise<void> {
276
+ const hostname = args[0];
277
+
278
+ if (!hostname) {
279
+ error("Missing hostname");
280
+ info("Usage: jack domain connect <hostname>");
281
+ process.exit(1);
282
+ }
283
+
284
+ console.error("");
285
+ info(`Reserving slot for ${hostname}...`);
286
+
287
+ try {
288
+ const data = await connectDomainService(hostname);
289
+
290
+ console.error("");
291
+ success(`Slot reserved: ${data.hostname}`);
292
+ console.error("");
293
+ info("Next step: assign to a project with 'jack domain assign <hostname> <project>'");
294
+ console.error("");
295
+ } catch (err) {
296
+ if (err instanceof JackError) {
297
+ error(err.message);
298
+ if (err.suggestion) {
299
+ info(err.suggestion);
300
+ }
301
+ process.exit(1);
302
+ }
303
+ throw err;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Assign a reserved domain to a project
309
+ */
310
+ async function assignDomainCommand(args: string[]): Promise<void> {
311
+ const hostname = args[0];
312
+ const projectSlug = args[1];
313
+
314
+ if (!hostname) {
315
+ error("Missing hostname");
316
+ info("Usage: jack domain assign <hostname> <project>");
317
+ process.exit(1);
318
+ }
319
+
320
+ if (!projectSlug) {
321
+ error("Missing project name");
322
+ info("Usage: jack domain assign <hostname> <project>");
323
+ process.exit(1);
324
+ }
325
+
326
+ output.start("Looking up domain and project...");
327
+
328
+ try {
329
+ const data = await assignDomainService(hostname, projectSlug);
330
+ output.stop();
331
+
332
+ console.error("");
333
+
334
+ if (data.status === "active") {
335
+ success(`Domain active: https://${hostname}`);
336
+ } else if (data.verification || data.ownership_verification) {
337
+ info(`Domain assigned. Configure DNS to activate:`);
338
+ console.error("");
339
+ showDnsInstructions(hostname, data.verification, data.ownership_verification);
340
+ console.error("");
341
+ const nextCheck = getSecondsUntilNextCheck();
342
+ console.error(
343
+ ` ${colors.cyan}First auto-check in ~${nextCheck}s after DNS is configured.${colors.reset}`,
344
+ );
345
+ } else {
346
+ success(`Domain assigned: ${hostname}`);
347
+ console.error(` Status: ${data.status}`);
348
+ }
349
+ console.error("");
350
+ } catch (err) {
351
+ output.stop();
352
+ if (err instanceof JackError) {
353
+ error(err.message);
354
+ if (err.suggestion) {
355
+ info(err.suggestion);
356
+ }
357
+ process.exit(1);
358
+ }
359
+ throw err;
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Unassign a domain from its project (keep the slot)
365
+ */
366
+ async function unassignDomainCommand(args: string[]): Promise<void> {
367
+ const hostname = args[0];
368
+
369
+ if (!hostname) {
370
+ error("Missing hostname");
371
+ info("Usage: jack domain unassign <hostname>");
372
+ process.exit(1);
373
+ }
374
+
375
+ output.start("Finding domain...");
376
+
377
+ // First, get the domain info for the confirmation prompt
378
+ let domain: DomainInfo | null;
379
+ try {
380
+ domain = await getDomainByHostname(hostname);
381
+ } catch (err) {
382
+ output.stop();
383
+ throw err;
384
+ }
385
+
386
+ if (!domain) {
387
+ output.stop();
388
+ error(`Domain not found: ${hostname}`);
389
+ info("Run 'jack domain' to see all domains");
390
+ process.exit(1);
391
+ }
392
+
393
+ if (!domain.project_id) {
394
+ output.stop();
395
+ error(`Domain ${hostname} is not assigned to any project`);
396
+ process.exit(1);
397
+ }
398
+
399
+ output.stop();
400
+
401
+ // Confirm
402
+ console.error("");
403
+ const projectInfo = domain.project_slug ? ` from ${domain.project_slug}` : "";
404
+ const choice = await promptSelect(
405
+ ["Yes, unassign", "Cancel"],
406
+ `Unassign ${hostname}${projectInfo}? (slot will be kept)`,
407
+ );
408
+
409
+ if (isCancel(choice) || choice !== 0) {
410
+ info("Cancelled");
411
+ return;
412
+ }
413
+
414
+ output.start("Unassigning domain...");
415
+
416
+ try {
417
+ await unassignDomainService(hostname);
418
+ output.stop();
419
+ console.error("");
420
+ success(`Domain unassigned: ${hostname}`);
421
+ info("Slot kept. Reassign with: jack domain assign <hostname> <project>");
422
+ console.error("");
423
+ } catch (err) {
424
+ output.stop();
425
+ if (err instanceof JackError) {
426
+ error(err.message);
427
+ if (err.suggestion) {
428
+ info(err.suggestion);
429
+ }
430
+ process.exit(1);
431
+ }
432
+ throw err;
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Disconnect (fully remove) a domain
438
+ */
439
+ async function disconnectDomainCommand(args: string[]): Promise<void> {
440
+ const hostname = args[0];
441
+
442
+ if (!hostname) {
443
+ error("Missing hostname");
444
+ info("Usage: jack domain disconnect <hostname>");
445
+ process.exit(1);
446
+ }
447
+
448
+ output.start("Finding domain...");
449
+
450
+ // First, get the domain info for the confirmation prompt
451
+ let domain: DomainInfo | null;
452
+ try {
453
+ domain = await getDomainByHostname(hostname);
454
+ } catch (err) {
455
+ output.stop();
456
+ throw err;
457
+ }
458
+
459
+ if (!domain) {
460
+ output.stop();
461
+ error(`Domain not found: ${hostname}`);
462
+ info("Run 'jack domain' to see all domains");
463
+ process.exit(1);
464
+ }
465
+
466
+ output.stop();
467
+
468
+ // Strong warning for disconnect
469
+ console.error("");
470
+ warn("This will permanently remove the domain and free the slot.");
471
+ if (domain.project_slug) {
472
+ warn(`Traffic to ${hostname} will stop routing to ${domain.project_slug}.`);
473
+ }
474
+ console.error("");
475
+
476
+ const choice = await promptSelect(
477
+ ["Yes, disconnect permanently", "Cancel"],
478
+ `Disconnect ${hostname}?`,
479
+ );
480
+
481
+ if (isCancel(choice) || choice !== 0) {
482
+ info("Cancelled");
483
+ return;
484
+ }
485
+
486
+ output.start("Disconnecting domain...");
487
+
488
+ try {
489
+ await disconnectDomainService(hostname);
490
+ output.stop();
491
+ console.error("");
492
+ success(`Domain disconnected: ${hostname}`);
493
+ info("Slot freed.");
494
+ console.error("");
495
+ } catch (err) {
496
+ output.stop();
497
+ if (err instanceof JackError) {
498
+ error(err.message);
499
+ if (err.suggestion) {
500
+ info(err.suggestion);
501
+ }
502
+ process.exit(1);
503
+ }
504
+ throw err;
505
+ }
506
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * jack domains - List all custom domains across all projects
3
+ *
4
+ * Shows a high-level overview of all domains connected across all projects,
5
+ * with slot usage information.
6
+ */
7
+
8
+ import { isLoggedIn } from "../lib/auth/index.ts";
9
+ import { colors, error, info, output } from "../lib/output.ts";
10
+ import {
11
+ type DomainInfo,
12
+ type DomainStatus,
13
+ listDomains,
14
+ } from "../lib/services/domain-operations.ts";
15
+
16
+ /**
17
+ * Get status icon for domain status
18
+ */
19
+ function getStatusIcon(status: DomainStatus): string {
20
+ switch (status) {
21
+ case "active":
22
+ return `${colors.green}✓${colors.reset}`;
23
+ case "pending":
24
+ case "pending_owner":
25
+ case "pending_ssl":
26
+ return `${colors.yellow}⏳${colors.reset}`;
27
+ case "failed":
28
+ case "blocked":
29
+ return `${colors.red}✗${colors.reset}`;
30
+ case "moved":
31
+ case "deleting":
32
+ return `${colors.cyan}○${colors.reset}`;
33
+ default:
34
+ return "○";
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Get human-readable status label
40
+ */
41
+ function getStatusLabel(status: DomainStatus): string {
42
+ switch (status) {
43
+ case "active":
44
+ return "active";
45
+ case "pending":
46
+ return "pending DNS";
47
+ case "pending_owner":
48
+ return "pending ownership";
49
+ case "pending_ssl":
50
+ return "pending SSL";
51
+ case "failed":
52
+ return "failed";
53
+ case "blocked":
54
+ return "blocked";
55
+ case "moved":
56
+ return "moved";
57
+ case "deleting":
58
+ return "deleting";
59
+ default:
60
+ return status;
61
+ }
62
+ }
63
+
64
+ interface DomainsOptions {
65
+ json?: boolean;
66
+ help?: boolean;
67
+ }
68
+
69
+ export default async function domains(options: DomainsOptions = {}): Promise<void> {
70
+ // Handle help flags
71
+ if (options.help) {
72
+ return showHelp();
73
+ }
74
+
75
+ const jsonOutput = options.json ?? false;
76
+
77
+ // Check if authenticated
78
+ if (!isLoggedIn()) {
79
+ error("Not logged in");
80
+ info("Run: jack login");
81
+ process.exit(1);
82
+ }
83
+
84
+ output.start("Loading domains...");
85
+
86
+ try {
87
+ const data = await listDomains();
88
+ output.stop();
89
+
90
+ // JSON output
91
+ if (jsonOutput) {
92
+ console.log(JSON.stringify(data, null, 2));
93
+ return;
94
+ }
95
+
96
+ // Render table
97
+ console.error("");
98
+
99
+ // Show slot usage
100
+ const { used, max } = data.slots;
101
+ const available = max - used;
102
+
103
+ if (data.domains.length === 0) {
104
+ // Empty state - focus on what's available
105
+ if (max === 0) {
106
+ console.error(
107
+ ` ${colors.bold}Custom Domains${colors.reset} ${colors.yellow}Free plan${colors.reset}`,
108
+ );
109
+ console.error("");
110
+ info("Custom domains require a Pro plan");
111
+ info("Upgrade: jack upgrade");
112
+ } else {
113
+ console.error(
114
+ ` ${colors.bold}Custom Domains${colors.reset} ${colors.green}${available} slots available${colors.reset}`,
115
+ );
116
+ console.error("");
117
+ info("Reserve a slot, then assign to a project:");
118
+ console.error(` jack domain connect ${colors.cyan}api.yourcompany.com${colors.reset}`);
119
+ console.error(
120
+ ` jack domain assign ${colors.cyan}api.yourcompany.com${colors.reset} ${colors.cyan}<project>${colors.reset}`,
121
+ );
122
+ }
123
+ console.error("");
124
+ return;
125
+ }
126
+
127
+ // Has domains - show usage
128
+ const slotColor = used >= max ? colors.yellow : colors.green;
129
+ console.error(
130
+ ` ${colors.bold}Custom Domains${colors.reset} ${slotColor}${used}/${max} slots${colors.reset}`,
131
+ );
132
+ console.error("");
133
+
134
+ // Group by status
135
+ const active = data.domains.filter((d) => d.status === "active");
136
+ const pending = data.domains.filter((d) =>
137
+ ["pending", "pending_owner", "pending_ssl"].includes(d.status),
138
+ );
139
+ const unassigned = data.domains.filter((d) => d.status === "claimed");
140
+ const other = data.domains.filter(
141
+ (d) => !["active", "pending", "pending_owner", "pending_ssl", "claimed"].includes(d.status),
142
+ );
143
+
144
+ // Show active domains
145
+ if (active.length > 0) {
146
+ console.error(` ${colors.dim}Active${colors.reset}`);
147
+ for (const d of active) {
148
+ console.error(
149
+ ` ${getStatusIcon(d.status)} ${colors.green}https://${d.hostname}${colors.reset} ${colors.dim}-> ${d.project_slug}${colors.reset}`,
150
+ );
151
+ }
152
+ console.error("");
153
+ }
154
+
155
+ // Show pending domains
156
+ if (pending.length > 0) {
157
+ console.error(` ${colors.dim}Pending${colors.reset}`);
158
+ for (const d of pending) {
159
+ const label = getStatusLabel(d.status);
160
+ console.error(
161
+ ` ${getStatusIcon(d.status)} ${colors.cyan}${d.hostname}${colors.reset} ${colors.yellow}(${label})${colors.reset} ${colors.dim}-> ${d.project_slug}${colors.reset}`,
162
+ );
163
+ }
164
+ console.error("");
165
+ }
166
+
167
+ // Show unassigned domains (claimed but not connected to a project)
168
+ if (unassigned.length > 0) {
169
+ console.error(` ${colors.dim}Unassigned${colors.reset}`);
170
+ for (const d of unassigned) {
171
+ console.error(` ○ ${d.hostname}`);
172
+ }
173
+ console.error("");
174
+ }
175
+
176
+ // Show other (failed, blocked, moved)
177
+ if (other.length > 0) {
178
+ console.error(` ${colors.dim}Other${colors.reset}`);
179
+ for (const d of other) {
180
+ const label = getStatusLabel(d.status);
181
+ console.error(
182
+ ` ${getStatusIcon(d.status)} ${d.hostname} ${colors.red}(${label})${colors.reset} ${colors.dim}-> ${d.project_slug}${colors.reset}`,
183
+ );
184
+ }
185
+ console.error("");
186
+ }
187
+
188
+ // Footer hints - show available slots if any
189
+ if (available > 0) {
190
+ info(
191
+ `${available} slot${available > 1 ? "s" : ""} available. Add: jack domain connect <hostname>`,
192
+ );
193
+ } else {
194
+ info("All slots used. Remove a domain to free a slot: jack domain rm <hostname>");
195
+ }
196
+ console.error("");
197
+ } catch (err) {
198
+ output.stop();
199
+ throw err;
200
+ }
201
+ }
202
+
203
+ function showHelp(): void {
204
+ console.error("");
205
+ info("jack domains - List all custom domains across all projects");
206
+ console.error("");
207
+ console.error("Usage:");
208
+ console.error(" jack domains List all domains with slot usage");
209
+ console.error(" jack domains --json Output as JSON");
210
+ console.error("");
211
+ console.error("Related:");
212
+ console.error(" jack domain Manage domains for a specific project");
213
+ console.error(" jack upgrade Upgrade your plan for more slots");
214
+ console.error("");
215
+ }