@getjack/jack 0.1.30 → 0.1.32

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 CHANGED
@@ -1,16 +1,13 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "Ship before you forget why you started. The vibecoder's deployment CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "jack": "./src/index.ts"
9
9
  },
10
- "files": [
11
- "src",
12
- "templates"
13
- ],
10
+ "files": ["src", "templates"],
14
11
  "engines": {
15
12
  "bun": ">=1.0.0"
16
13
  },
@@ -47,6 +44,8 @@
47
44
  "@clack/prompts": "^0.11.0",
48
45
  "@modelcontextprotocol/sdk": "^1.25.1",
49
46
  "archiver": "^7.0.1",
47
+ "cron-parser": "^4.9.0",
48
+ "cronstrue": "^2.50.0",
50
49
  "fflate": "^0.8.2",
51
50
  "human-id": "^4.1.3",
52
51
  "meow": "^14.0.0",
@@ -16,15 +16,14 @@ import { isCancel, promptSelect } from "../lib/hooks.ts";
16
16
  import { colors, error, info, output, success, warn } from "../lib/output.ts";
17
17
  import {
18
18
  type DomainInfo,
19
- type DomainOwnershipVerification,
20
19
  type DomainStatus,
21
- type DomainVerification,
22
20
  assignDomain as assignDomainService,
23
21
  connectDomain as connectDomainService,
24
22
  disconnectDomain as disconnectDomainService,
25
23
  getDomainByHostname,
26
24
  listDomains as listDomainsService,
27
25
  unassignDomain as unassignDomainService,
26
+ verifyDomain as verifyDomainService,
28
27
  } from "../lib/services/domain-operations.ts";
29
28
 
30
29
  export default async function domain(subcommand?: string, args: string[] = []): Promise<void> {
@@ -47,12 +46,14 @@ export default async function domain(subcommand?: string, args: string[] = []):
47
46
  return await unassignDomainCommand(args);
48
47
  case "disconnect":
49
48
  return await disconnectDomainCommand(args);
49
+ case "verify":
50
+ return await verifyDomainCommand(args);
50
51
  case "list":
51
52
  case "ls":
52
53
  return await listDomainsCommand();
53
54
  default:
54
55
  error(`Unknown subcommand: ${subcommand}`);
55
- info("Available: connect, assign, unassign, disconnect, list, help");
56
+ info("Available: connect, assign, unassign, disconnect, verify, list, help");
56
57
  process.exit(1);
57
58
  }
58
59
  }
@@ -67,16 +68,19 @@ function showHelp(): void {
67
68
  console.error(" jack domain assign <hostname> <project> Provision domain to project");
68
69
  console.error(" jack domain unassign <hostname> Remove from project, keep slot");
69
70
  console.error(" jack domain disconnect <hostname> Remove completely, free slot");
71
+ console.error(" jack domain verify <hostname> Check DNS configuration");
70
72
  console.error("");
71
73
  console.error("Workflow:");
72
74
  console.error(" 1. connect - Reserve hostname (uses a slot)");
73
75
  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(" 3. verify - Check if DNS is configured correctly");
77
+ console.error(" 4. unassign - Remove from project but keep slot reserved");
78
+ console.error(" 5. disconnect - Full removal, slot freed");
76
79
  console.error("");
77
80
  console.error("Examples:");
78
81
  console.error(" jack domain connect api.mycompany.com");
79
82
  console.error(" jack domain assign api.mycompany.com my-api");
83
+ console.error(" jack domain verify api.mycompany.com");
80
84
  console.error(" jack domain unassign api.mycompany.com");
81
85
  console.error(" jack domain disconnect api.mycompany.com");
82
86
  console.error("");
@@ -91,15 +95,20 @@ function getStatusIcon(status: DomainStatus): string {
91
95
  return `${colors.green}✓${colors.reset}`;
92
96
  case "claimed":
93
97
  return `${colors.dim}○${colors.reset}`;
98
+ case "unassigned":
99
+ return `${colors.cyan}○${colors.reset}`;
94
100
  case "pending":
101
+ case "pending_dns":
95
102
  case "pending_owner":
96
103
  case "pending_ssl":
97
104
  return `${colors.yellow}⏳${colors.reset}`;
98
105
  case "failed":
99
106
  case "blocked":
107
+ case "expired":
100
108
  return `${colors.red}✗${colors.reset}`;
101
109
  case "moved":
102
110
  case "deleting":
111
+ case "deleted":
103
112
  return `${colors.cyan}○${colors.reset}`;
104
113
  default:
105
114
  return "○";
@@ -114,9 +123,13 @@ function getStatusLabel(status: DomainStatus): string {
114
123
  case "active":
115
124
  return "active";
116
125
  case "claimed":
117
- return "unassigned";
126
+ return "reserved";
127
+ case "unassigned":
128
+ return "ready";
118
129
  case "pending":
119
130
  return "pending DNS";
131
+ case "pending_dns":
132
+ return "configure DNS";
120
133
  case "pending_owner":
121
134
  return "pending ownership";
122
135
  case "pending_ssl":
@@ -129,6 +142,10 @@ function getStatusLabel(status: DomainStatus): string {
129
142
  return "moved";
130
143
  case "deleting":
131
144
  return "deleting";
145
+ case "expired":
146
+ return "expired";
147
+ case "deleted":
148
+ return "deleted";
132
149
  default:
133
150
  return status;
134
151
  }
@@ -137,11 +154,44 @@ function getStatusLabel(status: DomainStatus): string {
137
154
  /**
138
155
  * Show DNS configuration instructions for pending domains
139
156
  */
140
- function showDnsInstructions(
141
- hostname: string,
142
- verification?: DomainVerification,
143
- ownershipVerification?: DomainOwnershipVerification,
144
- ): void {
157
+ function showDnsInstructions(domain: DomainInfo): void {
158
+ const { hostname, verification, ownership_verification, dns, next_step } = domain;
159
+
160
+ // Show DNS error if available (for pending_dns status)
161
+ if (dns?.error) {
162
+ console.error(` ${colors.yellow}DNS Error: ${dns.error}${colors.reset}`);
163
+ console.error("");
164
+ }
165
+
166
+ // If we have next_step, use that for instructions
167
+ if (next_step?.record_type && next_step?.record_name && next_step?.record_value) {
168
+ // Extract the base domain (e.g., "hellno.wtf" from "app.hellno.wtf")
169
+ const parts = hostname.split(".");
170
+ const baseDomain = parts.slice(-2).join(".");
171
+
172
+ console.error(
173
+ ` ${colors.cyan}Add this record to your DNS provider for ${colors.bold}${baseDomain}${colors.reset}${colors.cyan}:${colors.reset}`,
174
+ );
175
+ console.error("");
176
+ console.error(` ${colors.bold}${next_step.record_type}${colors.reset}`);
177
+ console.error(
178
+ ` ${colors.cyan}Name:${colors.reset} ${colors.green}${next_step.record_name}${colors.reset}`,
179
+ );
180
+ console.error(
181
+ ` ${colors.cyan}Value:${colors.reset} ${colors.green}${next_step.record_value}${colors.reset}`,
182
+ );
183
+ if (next_step.message) {
184
+ console.error("");
185
+ console.error(` ${colors.dim}${next_step.message}${colors.reset}`);
186
+ }
187
+ return;
188
+ }
189
+
190
+ // Fall back to verification/ownership_verification
191
+ if (!verification && !ownership_verification) {
192
+ return;
193
+ }
194
+
145
195
  // Extract the base domain (e.g., "hellno.wtf" from "app.hellno.wtf")
146
196
  const parts = hostname.split(".");
147
197
  const baseDomain = parts.slice(-2).join(".");
@@ -167,10 +217,10 @@ function showDnsInstructions(
167
217
  step++;
168
218
  }
169
219
 
170
- if (ownershipVerification) {
220
+ if (ownership_verification) {
171
221
  if (step > 1) console.error("");
172
222
  // Extract just the subdomain part for the TXT name
173
- const txtSubdomain = ownershipVerification.name.replace(`.${baseDomain}`, "");
223
+ const txtSubdomain = ownership_verification.name.replace(`.${baseDomain}`, "");
174
224
  console.error(
175
225
  ` ${colors.bold}${step}. TXT${colors.reset} ${colors.cyan}(proves ownership)${colors.reset}`,
176
226
  );
@@ -178,7 +228,7 @@ function showDnsInstructions(
178
228
  ` ${colors.cyan}Name:${colors.reset} ${colors.green}${txtSubdomain}${colors.reset}`,
179
229
  );
180
230
  console.error(
181
- ` ${colors.cyan}Value:${colors.reset} ${colors.green}${ownershipVerification.value}${colors.reset}`,
231
+ ` ${colors.cyan}Value:${colors.reset} ${colors.green}${ownership_verification.value}${colors.reset}`,
182
232
  );
183
233
  }
184
234
  }
@@ -225,6 +275,11 @@ async function listDomainsCommand(): Promise<void> {
225
275
  const pendingDomains: DomainInfo[] = [];
226
276
 
227
277
  for (const d of data.domains) {
278
+ // Skip deleted domains
279
+ if (d.status === "deleted") {
280
+ continue;
281
+ }
282
+
228
283
  const icon = getStatusIcon(d.status);
229
284
  const label = getStatusLabel(d.status);
230
285
 
@@ -239,8 +294,25 @@ async function listDomainsCommand(): Promise<void> {
239
294
  console.error(
240
295
  ` ${icon} ${colors.dim}${d.hostname}${colors.reset} ${colors.cyan}(${label})${colors.reset}`,
241
296
  );
297
+ } else if (d.status === "expired") {
298
+ // Expired - suggest delete to free hostname
299
+ console.error(
300
+ ` ${icon} ${colors.dim}${d.hostname}${colors.reset} ${colors.red}(${label})${colors.reset} ${colors.dim}- delete to free hostname${colors.reset}`,
301
+ );
302
+ } else if (d.status === "moved") {
303
+ // Moved - suggest delete & re-add to restore
304
+ console.error(
305
+ ` ${icon} ${colors.dim}${d.hostname}${colors.reset} ${colors.cyan}(${label})${colors.reset} ${colors.dim}- delete & re-add to restore${colors.reset}`,
306
+ );
307
+ } else if (d.status === "pending_dns") {
308
+ // Pending DNS - show with instructions
309
+ const projectInfo = d.project_slug ? ` -> ${d.project_slug}` : "";
310
+ console.error(
311
+ ` ${icon} ${colors.cyan}${d.hostname}${colors.reset}${projectInfo} ${colors.yellow}(${label})${colors.reset}`,
312
+ );
313
+ pendingDomains.push(d);
242
314
  } else {
243
- // Pending states
315
+ // Other pending states
244
316
  const projectInfo = d.project_slug ? ` -> ${d.project_slug}` : "";
245
317
  console.error(
246
318
  ` ${icon} ${colors.cyan}${d.hostname}${colors.reset}${projectInfo} ${colors.yellow}(${label})${colors.reset}`,
@@ -255,7 +327,7 @@ async function listDomainsCommand(): Promise<void> {
255
327
  if (pendingDomains.length > 0) {
256
328
  for (const d of pendingDomains) {
257
329
  console.error("");
258
- showDnsInstructions(d.hostname, d.verification, d.ownership_verification);
330
+ showDnsInstructions(d);
259
331
  }
260
332
  const nextCheck = getSecondsUntilNextCheck();
261
333
  console.error("");
@@ -336,7 +408,18 @@ async function assignDomainCommand(args: string[]): Promise<void> {
336
408
  } else if (data.verification || data.ownership_verification) {
337
409
  info(`Domain assigned. Configure DNS to activate:`);
338
410
  console.error("");
339
- showDnsInstructions(hostname, data.verification, data.ownership_verification);
411
+ // Construct a DomainInfo-like object for showDnsInstructions
412
+ showDnsInstructions({
413
+ id: data.id,
414
+ hostname: data.hostname,
415
+ status: data.status,
416
+ ssl_status: data.ssl_status,
417
+ project_id: data.project_id,
418
+ project_slug: data.project_slug,
419
+ verification: data.verification,
420
+ ownership_verification: data.ownership_verification,
421
+ created_at: "",
422
+ });
340
423
  console.error("");
341
424
  const nextCheck = getSecondsUntilNextCheck();
342
425
  console.error(
@@ -504,3 +587,51 @@ async function disconnectDomainCommand(args: string[]): Promise<void> {
504
587
  throw err;
505
588
  }
506
589
  }
590
+
591
+ /**
592
+ * Verify DNS configuration for a domain
593
+ */
594
+ async function verifyDomainCommand(args: string[]): Promise<void> {
595
+ const hostname = args[0];
596
+
597
+ if (!hostname) {
598
+ error("Missing hostname");
599
+ info("Usage: jack domain verify <hostname>");
600
+ process.exit(1);
601
+ }
602
+
603
+ output.start("Checking DNS...");
604
+
605
+ try {
606
+ const result = await verifyDomainService(hostname);
607
+ output.stop();
608
+
609
+ console.error("");
610
+
611
+ if (result.domain.status === "active") {
612
+ success(`Domain active: https://${hostname}`);
613
+ } else if (result.dns_check?.verified) {
614
+ success("DNS verified! SSL certificate being issued...");
615
+ info(`Status: ${getStatusLabel(result.domain.status)}`);
616
+ } else {
617
+ warn("DNS not yet configured");
618
+ if (result.dns_check?.error) {
619
+ console.error(` ${colors.yellow}${result.dns_check.error}${colors.reset}`);
620
+ }
621
+ console.error("");
622
+ showDnsInstructions(result.domain);
623
+ }
624
+
625
+ console.error("");
626
+ } catch (err) {
627
+ output.stop();
628
+ if (err instanceof JackError) {
629
+ error(err.message);
630
+ if (err.suggestion) {
631
+ info(err.suggestion);
632
+ }
633
+ process.exit(1);
634
+ }
635
+ throw err;
636
+ }
637
+ }
@@ -20,15 +20,22 @@ function getStatusIcon(status: DomainStatus): string {
20
20
  switch (status) {
21
21
  case "active":
22
22
  return `${colors.green}✓${colors.reset}`;
23
+ case "claimed":
24
+ return `${colors.dim}○${colors.reset}`;
25
+ case "unassigned":
26
+ return `${colors.cyan}○${colors.reset}`;
23
27
  case "pending":
28
+ case "pending_dns":
24
29
  case "pending_owner":
25
30
  case "pending_ssl":
26
31
  return `${colors.yellow}⏳${colors.reset}`;
27
32
  case "failed":
28
33
  case "blocked":
34
+ case "expired":
29
35
  return `${colors.red}✗${colors.reset}`;
30
36
  case "moved":
31
37
  case "deleting":
38
+ case "deleted":
32
39
  return `${colors.cyan}○${colors.reset}`;
33
40
  default:
34
41
  return "○";
@@ -42,8 +49,14 @@ function getStatusLabel(status: DomainStatus): string {
42
49
  switch (status) {
43
50
  case "active":
44
51
  return "active";
52
+ case "claimed":
53
+ return "reserved";
54
+ case "unassigned":
55
+ return "ready";
45
56
  case "pending":
46
57
  return "pending DNS";
58
+ case "pending_dns":
59
+ return "configure DNS";
47
60
  case "pending_owner":
48
61
  return "pending ownership";
49
62
  case "pending_ssl":
@@ -56,6 +69,10 @@ function getStatusLabel(status: DomainStatus): string {
56
69
  return "moved";
57
70
  case "deleting":
58
71
  return "deleting";
72
+ case "expired":
73
+ return "expired";
74
+ case "deleted":
75
+ return "deleted";
59
76
  default:
60
77
  return status;
61
78
  }
@@ -134,11 +151,20 @@ export default async function domains(options: DomainsOptions = {}): Promise<voi
134
151
  // Group by status
135
152
  const active = data.domains.filter((d) => d.status === "active");
136
153
  const pending = data.domains.filter((d) =>
137
- ["pending", "pending_owner", "pending_ssl"].includes(d.status),
154
+ ["pending", "pending_dns", "pending_owner", "pending_ssl"].includes(d.status),
138
155
  );
139
156
  const unassigned = data.domains.filter((d) => d.status === "claimed");
140
157
  const other = data.domains.filter(
141
- (d) => !["active", "pending", "pending_owner", "pending_ssl", "claimed"].includes(d.status),
158
+ (d) =>
159
+ ![
160
+ "active",
161
+ "pending",
162
+ "pending_dns",
163
+ "pending_owner",
164
+ "pending_ssl",
165
+ "claimed",
166
+ "deleted",
167
+ ].includes(d.status),
142
168
  );
143
169
 
144
170
  // Show active domains