@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.
- package/package.json +1 -1
- package/src/commands/cd.ts +163 -0
- package/src/commands/clone.ts +112 -68
- package/src/commands/domain.ts +506 -0
- package/src/commands/domains.ts +215 -0
- package/src/commands/down.ts +18 -12
- package/src/commands/hack.ts +185 -8
- package/src/commands/init.ts +52 -1
- package/src/commands/link.ts +25 -43
- package/src/commands/logs.ts +2 -2
- package/src/commands/mcp.ts +74 -3
- package/src/commands/new.ts +48 -54
- package/src/commands/projects.ts +53 -10
- package/src/commands/secrets.ts +5 -1
- package/src/commands/services.ts +16 -4
- package/src/commands/shell-init.ts +43 -0
- package/src/commands/ship.ts +2 -11
- package/src/commands/skills.ts +335 -0
- package/src/commands/update.ts +31 -0
- package/src/commands/upgrade.ts +14 -0
- package/src/index.ts +116 -24
- package/src/lib/agent-integration.ts +1 -2
- package/src/lib/agents.ts +2 -2
- package/src/lib/auth/login-flow.ts +1 -1
- package/src/lib/clone-core.ts +252 -0
- package/src/lib/config.ts +22 -0
- package/src/lib/control-plane.ts +31 -5
- package/src/lib/fuzzy.ts +93 -0
- package/src/lib/managed-deploy.ts +4 -1
- package/src/lib/managed-down.ts +20 -5
- package/src/lib/output.ts +90 -9
- package/src/lib/picker.ts +406 -0
- package/src/lib/project-detection.ts +5 -2
- package/src/lib/project-list.ts +66 -5
- package/src/lib/project-operations.ts +68 -6
- package/src/lib/prompts.ts +1 -1
- package/src/lib/services/db-execute.ts +8 -1
- package/src/lib/services/db-list.ts +4 -1
- package/src/lib/services/domain-operations.ts +379 -0
- package/src/lib/services/storage-config.ts +1 -5
- package/src/lib/services/storage-delete.ts +1 -1
- package/src/lib/services/storage-info.ts +2 -4
- package/src/lib/services/vectorize-config.ts +1 -5
- package/src/lib/services/vectorize-create.ts +3 -1
- package/src/lib/shell-integration.ts +202 -0
- package/src/lib/telemetry-config.ts +50 -4
- package/src/lib/telemetry.ts +71 -2
- package/src/lib/version-check.ts +1 -3
- package/src/lib/wrangler-config.test.ts +2 -2
- package/src/lib/wrangler-config.ts +1 -1
- package/src/lib/zip-packager.ts +1 -3
- package/src/mcp/tools/index.ts +261 -7
- package/src/templates/index.ts +10 -1
- package/templates/ai-chat/.jack.json +1 -5
- package/templates/ai-chat/public/chat.js +130 -130
- package/templates/ai-chat/src/index.ts +9 -13
- package/templates/ai-chat/src/jack-ai.ts +6 -2
- package/templates/saas/.jack.json +6 -1
- package/templates/saas/src/auth.ts +8 -4
- package/templates/saas/src/client/App.tsx +22 -7
- package/templates/saas/src/client/components/ProtectedRoute.tsx +9 -2
- package/templates/saas/src/client/components/ThemeToggle.tsx +1 -6
- package/templates/saas/src/client/components/ui/accordion.tsx +1 -1
- package/templates/saas/src/client/components/ui/alert-dialog.tsx +2 -2
- package/templates/saas/src/client/components/ui/alert.tsx +2 -2
- package/templates/saas/src/client/components/ui/avatar.tsx +1 -1
- package/templates/saas/src/client/components/ui/badge.tsx +2 -2
- package/templates/saas/src/client/components/ui/breadcrumb.tsx +1 -1
- package/templates/saas/src/client/components/ui/button-group.tsx +2 -2
- package/templates/saas/src/client/components/ui/button.tsx +2 -2
- package/templates/saas/src/client/components/ui/card.tsx +1 -1
- package/templates/saas/src/client/components/ui/carousel.tsx +2 -2
- package/templates/saas/src/client/components/ui/checkbox.tsx +1 -1
- package/templates/saas/src/client/components/ui/command.tsx +2 -2
- package/templates/saas/src/client/components/ui/context-menu.tsx +1 -1
- package/templates/saas/src/client/components/ui/dialog.tsx +1 -1
- package/templates/saas/src/client/components/ui/drawer.tsx +1 -1
- package/templates/saas/src/client/components/ui/dropdown-menu.tsx +1 -1
- package/templates/saas/src/client/components/ui/empty.tsx +1 -1
- package/templates/saas/src/client/components/ui/field.tsx +2 -2
- package/templates/saas/src/client/components/ui/form.tsx +5 -5
- package/templates/saas/src/client/components/ui/hover-card.tsx +1 -1
- package/templates/saas/src/client/components/ui/input-group.tsx +3 -3
- package/templates/saas/src/client/components/ui/input-otp.tsx +1 -1
- package/templates/saas/src/client/components/ui/input.tsx +1 -1
- package/templates/saas/src/client/components/ui/item.tsx +3 -3
- package/templates/saas/src/client/components/ui/label.tsx +1 -1
- package/templates/saas/src/client/components/ui/menubar.tsx +1 -1
- package/templates/saas/src/client/components/ui/navigation-menu.tsx +1 -1
- package/templates/saas/src/client/components/ui/pagination.tsx +2 -2
- package/templates/saas/src/client/components/ui/popover.tsx +1 -1
- package/templates/saas/src/client/components/ui/progress.tsx +1 -1
- package/templates/saas/src/client/components/ui/radio-group.tsx +1 -1
- package/templates/saas/src/client/components/ui/resizable.tsx +1 -1
- package/templates/saas/src/client/components/ui/scroll-area.tsx +1 -1
- package/templates/saas/src/client/components/ui/select.tsx +1 -1
- package/templates/saas/src/client/components/ui/separator.tsx +1 -1
- package/templates/saas/src/client/components/ui/sheet.tsx +1 -1
- package/templates/saas/src/client/components/ui/sidebar.tsx +4 -4
- package/templates/saas/src/client/components/ui/slider.tsx +1 -1
- package/templates/saas/src/client/components/ui/switch.tsx +1 -1
- package/templates/saas/src/client/components/ui/table.tsx +1 -1
- package/templates/saas/src/client/components/ui/tabs.tsx +1 -1
- package/templates/saas/src/client/components/ui/textarea.tsx +1 -1
- package/templates/saas/src/client/components/ui/toggle-group.tsx +3 -3
- package/templates/saas/src/client/components/ui/toggle.tsx +2 -2
- package/templates/saas/src/client/components/ui/tooltip.tsx +1 -1
- package/templates/saas/src/client/hooks/useSubscription.ts +5 -4
- package/templates/saas/src/client/lib/auth-client.ts +1 -1
- package/templates/saas/src/client/lib/plans.ts +1 -6
- package/templates/saas/src/client/lib/utils.ts +1 -1
- package/templates/saas/src/client/main.tsx +1 -1
- package/templates/saas/src/client/pages/DashboardPage.tsx +41 -9
- package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +11 -2
- package/templates/saas/src/client/pages/HomePage.tsx +11 -2
- package/templates/saas/src/client/pages/LoginPage.tsx +11 -2
- package/templates/saas/src/client/pages/PricingPage.tsx +20 -10
- package/templates/saas/src/client/pages/ResetPasswordPage.tsx +14 -11
- package/templates/saas/src/client/pages/SignupPage.tsx +11 -2
- package/templates/saas/src/index.ts +28 -19
- package/templates/saas/vite.config.ts +1 -1
- package/templates/semantic-search/.jack.json +1 -5
- package/templates/semantic-search/src/index.ts +8 -4
- package/templates/semantic-search/src/jack-ai.ts +6 -2
- 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
|
+
}
|