@getjack/jack 0.1.30 → 0.1.31
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 +4 -5
- package/src/commands/domain.ts +148 -17
- package/src/commands/domains.ts +28 -2
- package/src/commands/services.ts +300 -1
- package/src/commands/skills.ts +58 -1
- package/src/lib/auth/login-flow.ts +34 -0
- package/src/lib/control-plane.ts +156 -0
- package/src/lib/mcp-config.ts +26 -4
- package/src/lib/output.ts +4 -2
- package/src/lib/picker.ts +3 -1
- package/src/lib/project-operations.ts +38 -4
- package/src/lib/services/cron-create.ts +73 -0
- package/src/lib/services/cron-delete.ts +66 -0
- package/src/lib/services/cron-list.ts +59 -0
- package/src/lib/services/cron-test.ts +93 -0
- package/src/lib/services/cron-utils.ts +78 -0
- package/src/lib/services/domain-operations.ts +89 -18
- package/src/mcp/server.ts +20 -0
- package/src/mcp/tools/index.ts +279 -0
package/package.json
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getjack/jack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
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",
|
package/src/commands/domain.ts
CHANGED
|
@@ -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.
|
|
75
|
-
console.error(" 4.
|
|
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 "
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
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 (
|
|
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 =
|
|
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}${
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/src/commands/domains.ts
CHANGED
|
@@ -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) =>
|
|
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
|