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