@getjack/jack 0.1.2 → 0.1.4
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/README.md +77 -29
- package/package.json +54 -47
- package/src/commands/agents.ts +145 -10
- package/src/commands/down.ts +110 -102
- package/src/commands/feedback.ts +189 -0
- package/src/commands/init.ts +8 -12
- package/src/commands/login.ts +88 -0
- package/src/commands/logout.ts +14 -0
- package/src/commands/logs.ts +21 -0
- package/src/commands/mcp.ts +134 -7
- package/src/commands/new.ts +43 -17
- package/src/commands/open.ts +13 -6
- package/src/commands/projects.ts +269 -143
- package/src/commands/secrets.ts +413 -0
- package/src/commands/services.ts +96 -123
- package/src/commands/ship.ts +5 -1
- package/src/commands/whoami.ts +31 -0
- package/src/index.ts +218 -144
- package/src/lib/agent-files.ts +34 -0
- package/src/lib/agents.ts +390 -22
- package/src/lib/asset-hash.ts +50 -0
- package/src/lib/auth/client.ts +115 -0
- package/src/lib/auth/constants.ts +5 -0
- package/src/lib/auth/guard.ts +57 -0
- package/src/lib/auth/index.ts +18 -0
- package/src/lib/auth/store.ts +54 -0
- package/src/lib/binding-validator.ts +136 -0
- package/src/lib/build-helper.ts +211 -0
- package/src/lib/cloudflare-api.ts +24 -0
- package/src/lib/config.ts +5 -6
- package/src/lib/control-plane.ts +295 -0
- package/src/lib/debug.ts +3 -1
- package/src/lib/deploy-mode.ts +93 -0
- package/src/lib/deploy-upload.ts +92 -0
- package/src/lib/errors.ts +2 -0
- package/src/lib/github.ts +31 -1
- package/src/lib/hooks.ts +4 -12
- package/src/lib/intent.ts +88 -0
- package/src/lib/jsonc.ts +125 -0
- package/src/lib/local-paths.test.ts +902 -0
- package/src/lib/local-paths.ts +258 -0
- package/src/lib/managed-deploy.ts +175 -0
- package/src/lib/managed-down.ts +159 -0
- package/src/lib/mcp-config.ts +55 -34
- package/src/lib/names.ts +9 -29
- package/src/lib/project-operations.ts +676 -249
- package/src/lib/project-resolver.ts +476 -0
- package/src/lib/registry.ts +76 -37
- package/src/lib/resources.ts +196 -0
- package/src/lib/schema.ts +30 -1
- package/src/lib/storage/file-filter.ts +1 -0
- package/src/lib/storage/index.ts +5 -1
- package/src/lib/telemetry.ts +14 -0
- package/src/lib/tty.ts +15 -0
- package/src/lib/zip-packager.ts +255 -0
- package/src/mcp/resources/index.ts +8 -2
- package/src/mcp/server.ts +32 -4
- package/src/mcp/tools/index.ts +35 -13
- package/src/mcp/types.ts +6 -0
- package/src/mcp/utils.ts +1 -1
- package/src/templates/index.ts +42 -4
- package/src/templates/types.ts +13 -0
- package/templates/CLAUDE.md +166 -0
- package/templates/api/.jack.json +4 -0
- package/templates/api/bun.lock +1 -0
- package/templates/api/wrangler.jsonc +5 -0
- package/templates/hello/.jack.json +28 -0
- package/templates/hello/package.json +10 -0
- package/templates/hello/src/index.ts +11 -0
- package/templates/hello/tsconfig.json +11 -0
- package/templates/hello/wrangler.jsonc +5 -0
- package/templates/miniapp/.jack.json +15 -4
- package/templates/miniapp/bun.lock +135 -40
- package/templates/miniapp/index.html +1 -0
- package/templates/miniapp/package.json +3 -1
- package/templates/miniapp/public/.well-known/farcaster.json +7 -5
- package/templates/miniapp/public/icon.png +0 -0
- package/templates/miniapp/public/og.png +0 -0
- package/templates/miniapp/schema.sql +8 -0
- package/templates/miniapp/src/App.tsx +254 -3
- package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
- package/templates/miniapp/src/hooks/useAI.ts +35 -0
- package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
- package/templates/miniapp/src/hooks/useShare.ts +76 -0
- package/templates/miniapp/src/index.css +15 -0
- package/templates/miniapp/src/lib/api.ts +2 -1
- package/templates/miniapp/src/worker.ts +515 -1
- package/templates/miniapp/wrangler.jsonc +15 -3
- package/LICENSE +0 -190
- package/src/commands/cloud.ts +0 -230
- package/templates/api/wrangler.toml +0 -3
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Plane API client for jack cloud
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONTROL_API_URL = "https://control.getjack.org";
|
|
6
|
+
|
|
7
|
+
export function getControlApiUrl(): string {
|
|
8
|
+
return process.env.JACK_CONTROL_URL || DEFAULT_CONTROL_API_URL;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CreateProjectRequest {
|
|
12
|
+
name: string;
|
|
13
|
+
slug?: string;
|
|
14
|
+
template?: string;
|
|
15
|
+
use_prebuilt?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreateManagedProjectOptions {
|
|
19
|
+
slug?: string;
|
|
20
|
+
template?: string;
|
|
21
|
+
usePrebuilt?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CreateProjectResponse {
|
|
25
|
+
project: {
|
|
26
|
+
id: string;
|
|
27
|
+
org_id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
slug: string;
|
|
30
|
+
status: string;
|
|
31
|
+
created_at: string;
|
|
32
|
+
updated_at: string;
|
|
33
|
+
};
|
|
34
|
+
resources: Array<{
|
|
35
|
+
id: string;
|
|
36
|
+
resource_type: string;
|
|
37
|
+
resource_name: string;
|
|
38
|
+
status: string;
|
|
39
|
+
}>;
|
|
40
|
+
status?: "live" | "created";
|
|
41
|
+
url?: string;
|
|
42
|
+
prebuilt_failed?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SlugAvailabilityResponse {
|
|
46
|
+
available: boolean;
|
|
47
|
+
slug: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CreateDeploymentRequest {
|
|
52
|
+
source: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CreateDeploymentResponse {
|
|
56
|
+
id: string;
|
|
57
|
+
project_id: string;
|
|
58
|
+
status: "queued" | "building" | "live" | "failed";
|
|
59
|
+
source: string;
|
|
60
|
+
created_at: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a managed project via the control plane.
|
|
65
|
+
*/
|
|
66
|
+
export async function createManagedProject(
|
|
67
|
+
name: string,
|
|
68
|
+
options?: CreateManagedProjectOptions,
|
|
69
|
+
): Promise<CreateProjectResponse> {
|
|
70
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
71
|
+
const pkg = await import("../../package.json");
|
|
72
|
+
|
|
73
|
+
const requestBody: CreateProjectRequest = { name };
|
|
74
|
+
if (options?.slug) {
|
|
75
|
+
requestBody.slug = options.slug;
|
|
76
|
+
}
|
|
77
|
+
if (options?.template) {
|
|
78
|
+
requestBody.template = options.template;
|
|
79
|
+
}
|
|
80
|
+
if (options?.usePrebuilt !== undefined) {
|
|
81
|
+
requestBody.use_prebuilt = options.usePrebuilt;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
"X-Jack-Version": pkg.version,
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify(requestBody),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
95
|
+
message?: string;
|
|
96
|
+
};
|
|
97
|
+
throw new Error(err.message || `Failed to create managed project: ${response.status}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return response.json() as Promise<CreateProjectResponse>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Deploy to a managed project via the control plane.
|
|
105
|
+
*/
|
|
106
|
+
export async function deployManagedProject(
|
|
107
|
+
projectId: string,
|
|
108
|
+
source: string,
|
|
109
|
+
): Promise<CreateDeploymentResponse> {
|
|
110
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
111
|
+
|
|
112
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/deployments`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { "Content-Type": "application/json" },
|
|
115
|
+
body: JSON.stringify({ source } satisfies CreateDeploymentRequest),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
120
|
+
message?: string;
|
|
121
|
+
};
|
|
122
|
+
throw new Error(err.message || `Managed deploy failed: ${response.status}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return response.json() as Promise<CreateDeploymentResponse>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if a project slug is available on jack cloud.
|
|
130
|
+
*/
|
|
131
|
+
export async function checkSlugAvailability(slug: string): Promise<SlugAvailabilityResponse> {
|
|
132
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
133
|
+
|
|
134
|
+
const response = await authFetch(
|
|
135
|
+
`${getControlApiUrl()}/v1/slugs/${encodeURIComponent(slug)}/available`,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`Failed to check slug availability: ${response.status}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return response.json() as Promise<SlugAvailabilityResponse>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface DatabaseExportResponse {
|
|
146
|
+
success: boolean;
|
|
147
|
+
download_url: string;
|
|
148
|
+
expires_in: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface DeleteProjectResponse {
|
|
152
|
+
success: boolean;
|
|
153
|
+
project_id: string;
|
|
154
|
+
deleted_at: string;
|
|
155
|
+
resources: Array<{ resource: string; success: boolean; error?: string }>;
|
|
156
|
+
warnings?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ManagedProject {
|
|
160
|
+
id: string;
|
|
161
|
+
org_id: string;
|
|
162
|
+
name: string;
|
|
163
|
+
slug: string;
|
|
164
|
+
status: "active" | "error" | "deleted";
|
|
165
|
+
created_at: string;
|
|
166
|
+
updated_at: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Export a managed project's D1 database.
|
|
171
|
+
*/
|
|
172
|
+
export async function exportManagedDatabase(projectId: string): Promise<DatabaseExportResponse> {
|
|
173
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
174
|
+
|
|
175
|
+
const response = await authFetch(
|
|
176
|
+
`${getControlApiUrl()}/v1/projects/${projectId}/database/export`,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (response.status === 504) {
|
|
180
|
+
throw new Error("Database export timed out. The database may be too large.");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
185
|
+
message?: string;
|
|
186
|
+
};
|
|
187
|
+
throw new Error(err.message || `Failed to export database: ${response.status}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return response.json() as Promise<DatabaseExportResponse>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Delete a managed project and all its resources.
|
|
195
|
+
*/
|
|
196
|
+
export async function deleteManagedProject(projectId: string): Promise<DeleteProjectResponse> {
|
|
197
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
198
|
+
|
|
199
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}`, {
|
|
200
|
+
method: "DELETE",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// 404 means project doesn't exist - treat as already deleted
|
|
204
|
+
if (response.status === 404) {
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
project_id: projectId,
|
|
208
|
+
deleted_at: new Date().toISOString(),
|
|
209
|
+
resources: [],
|
|
210
|
+
warnings: "Project was already deleted or not found",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
216
|
+
message?: string;
|
|
217
|
+
};
|
|
218
|
+
throw new Error(err.message || `Failed to delete project: ${response.status}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return response.json() as Promise<DeleteProjectResponse>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* List all managed projects from the control plane.
|
|
226
|
+
*/
|
|
227
|
+
export async function listManagedProjects(): Promise<ManagedProject[]> {
|
|
228
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
229
|
+
|
|
230
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects`);
|
|
231
|
+
|
|
232
|
+
if (!response.ok) {
|
|
233
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
234
|
+
message?: string;
|
|
235
|
+
};
|
|
236
|
+
throw new Error(err.message || `Failed to list managed projects: ${response.status}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const data = (await response.json()) as { projects: ManagedProject[] };
|
|
240
|
+
return data.projects;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Find a managed project by slug.
|
|
245
|
+
*/
|
|
246
|
+
export async function findProjectBySlug(slug: string): Promise<ManagedProject | null> {
|
|
247
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
248
|
+
|
|
249
|
+
const response = await authFetch(
|
|
250
|
+
`${getControlApiUrl()}/v1/projects/by-slug/${encodeURIComponent(slug)}`,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (response.status === 404) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
259
|
+
message?: string;
|
|
260
|
+
};
|
|
261
|
+
throw new Error(err.message || `Failed to find project: ${response.status}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const data = (await response.json()) as { project: ManagedProject };
|
|
265
|
+
return data.project;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface ProjectResource {
|
|
269
|
+
id: string;
|
|
270
|
+
resource_type: string;
|
|
271
|
+
resource_name: string;
|
|
272
|
+
provider_id: string;
|
|
273
|
+
status: string;
|
|
274
|
+
created_at: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Fetch all resources for a managed project.
|
|
279
|
+
* Uses GET /v1/projects/:id/resources endpoint.
|
|
280
|
+
*/
|
|
281
|
+
export async function fetchProjectResources(projectId: string): Promise<ProjectResource[]> {
|
|
282
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
283
|
+
|
|
284
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/resources`);
|
|
285
|
+
|
|
286
|
+
if (!response.ok) {
|
|
287
|
+
if (response.status === 404) {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
throw new Error(`Failed to fetch resources: ${response.status}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const data = (await response.json()) as { resources: ProjectResource[] };
|
|
294
|
+
return data.resources;
|
|
295
|
+
}
|
package/src/lib/debug.ts
CHANGED
|
@@ -63,8 +63,10 @@ export async function time(label: string, fn: () => Promise<void>): Promise<numb
|
|
|
63
63
|
timerStart(label);
|
|
64
64
|
try {
|
|
65
65
|
await fn();
|
|
66
|
-
} finally {
|
|
67
66
|
return timerEnd(label);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
timerEnd(label); // Clean up timer even on failure
|
|
69
|
+
throw error;
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy mode selection and validation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { $ } from "bun";
|
|
6
|
+
import { isLoggedIn } from "./auth/index.ts";
|
|
7
|
+
import type { DeployMode } from "./registry.ts";
|
|
8
|
+
import { Events, track } from "./telemetry.ts";
|
|
9
|
+
|
|
10
|
+
export interface ModeFlags {
|
|
11
|
+
managed?: boolean;
|
|
12
|
+
byo?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if wrangler CLI is available.
|
|
17
|
+
*/
|
|
18
|
+
export async function isWranglerAvailable(): Promise<boolean> {
|
|
19
|
+
try {
|
|
20
|
+
const result = await $`which wrangler`.nothrow().quiet();
|
|
21
|
+
return result.exitCode === 0;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Determine deploy mode based on login status and flags.
|
|
29
|
+
*
|
|
30
|
+
* Omakase behavior:
|
|
31
|
+
* - Logged in => managed
|
|
32
|
+
* - Logged out => BYO
|
|
33
|
+
*
|
|
34
|
+
* Explicit flags always override.
|
|
35
|
+
*/
|
|
36
|
+
export async function resolveDeployMode(flags: ModeFlags = {}): Promise<DeployMode> {
|
|
37
|
+
// Validate mutual exclusion
|
|
38
|
+
if (flags.managed && flags.byo) {
|
|
39
|
+
throw new Error("Cannot use both --managed and --byo flags. Choose one.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Explicit flag takes precedence
|
|
43
|
+
if (flags.managed) {
|
|
44
|
+
track(Events.DEPLOY_MODE_SELECTED, { mode: "managed", explicit: true });
|
|
45
|
+
return "managed";
|
|
46
|
+
}
|
|
47
|
+
if (flags.byo) {
|
|
48
|
+
track(Events.DEPLOY_MODE_SELECTED, { mode: "byo", explicit: true });
|
|
49
|
+
return "byo";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Omakase default based on login status
|
|
53
|
+
const loggedIn = await isLoggedIn();
|
|
54
|
+
const mode = loggedIn ? "managed" : "byo";
|
|
55
|
+
|
|
56
|
+
track(Events.DEPLOY_MODE_SELECTED, { mode, explicit: false });
|
|
57
|
+
|
|
58
|
+
return mode;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate that the chosen mode is available.
|
|
63
|
+
*
|
|
64
|
+
* @returns Error message if unavailable, null if OK
|
|
65
|
+
*/
|
|
66
|
+
export async function validateModeAvailability(mode: DeployMode): Promise<string | null> {
|
|
67
|
+
if (mode === "managed") {
|
|
68
|
+
const loggedIn = await isLoggedIn();
|
|
69
|
+
if (!loggedIn) {
|
|
70
|
+
return "Not logged in. Run: jack login or use --byo";
|
|
71
|
+
}
|
|
72
|
+
const hasWrangler = await isWranglerAvailable();
|
|
73
|
+
if (!hasWrangler) {
|
|
74
|
+
return "wrangler not found. Install wrangler or use --byo";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (mode === "byo") {
|
|
79
|
+
const hasWrangler = await isWranglerAvailable();
|
|
80
|
+
if (!hasWrangler) {
|
|
81
|
+
return "wrangler not found. Install wrangler or run: jack login";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get a human-readable label for the deploy mode.
|
|
90
|
+
*/
|
|
91
|
+
export function getDeployModeLabel(mode: DeployMode): string {
|
|
92
|
+
return mode === "managed" ? "jack cloud" : "wrangler (BYO)";
|
|
93
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload deployment artifacts to control plane via multipart form-data
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import type { AssetManifest } from "./asset-hash.ts";
|
|
7
|
+
import { authFetch } from "./auth/index.ts";
|
|
8
|
+
import { getControlApiUrl } from "./control-plane.ts";
|
|
9
|
+
|
|
10
|
+
export interface DeployUploadOptions {
|
|
11
|
+
projectId: string;
|
|
12
|
+
bundleZipPath: string;
|
|
13
|
+
sourceZipPath: string;
|
|
14
|
+
manifestPath: string;
|
|
15
|
+
schemaPath?: string;
|
|
16
|
+
secretsPath?: string;
|
|
17
|
+
assetsZipPath?: string;
|
|
18
|
+
assetManifest?: AssetManifest;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DeployUploadResult {
|
|
22
|
+
id: string;
|
|
23
|
+
project_id: string;
|
|
24
|
+
status: "queued" | "building" | "live" | "failed";
|
|
25
|
+
source: string;
|
|
26
|
+
created_at: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Upload deployment artifacts via multipart/form-data
|
|
31
|
+
*/
|
|
32
|
+
export async function uploadDeployment(options: DeployUploadOptions): Promise<DeployUploadResult> {
|
|
33
|
+
const formData = new FormData();
|
|
34
|
+
|
|
35
|
+
// Read files and add to form data
|
|
36
|
+
const manifestContent = await readFile(options.manifestPath);
|
|
37
|
+
formData.append(
|
|
38
|
+
"manifest",
|
|
39
|
+
new Blob([manifestContent], { type: "application/json" }),
|
|
40
|
+
"manifest.json",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const bundleContent = await readFile(options.bundleZipPath);
|
|
44
|
+
formData.append("bundle", new Blob([bundleContent], { type: "application/zip" }), "bundle.zip");
|
|
45
|
+
|
|
46
|
+
const sourceContent = await readFile(options.sourceZipPath);
|
|
47
|
+
formData.append("source", new Blob([sourceContent], { type: "application/zip" }), "source.zip");
|
|
48
|
+
|
|
49
|
+
// Optional files
|
|
50
|
+
if (options.schemaPath) {
|
|
51
|
+
const schemaContent = await readFile(options.schemaPath);
|
|
52
|
+
formData.append("schema", new Blob([schemaContent], { type: "text/sql" }), "schema.sql");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (options.secretsPath) {
|
|
56
|
+
const secretsContent = await readFile(options.secretsPath);
|
|
57
|
+
formData.append(
|
|
58
|
+
"secrets",
|
|
59
|
+
new Blob([secretsContent], { type: "application/json" }),
|
|
60
|
+
"secrets.json",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (options.assetsZipPath) {
|
|
65
|
+
const assetsContent = await readFile(options.assetsZipPath);
|
|
66
|
+
formData.append("assets", new Blob([assetsContent], { type: "application/zip" }), "assets.zip");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (options.assetManifest) {
|
|
70
|
+
formData.append(
|
|
71
|
+
"asset-manifest",
|
|
72
|
+
new Blob([JSON.stringify(options.assetManifest)], { type: "application/json" }),
|
|
73
|
+
"asset-manifest.json",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// POST to control plane
|
|
78
|
+
const url = `${getControlApiUrl()}/v1/projects/${options.projectId}/deployments/upload`;
|
|
79
|
+
const response = await authFetch(url, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: formData,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
86
|
+
message?: string;
|
|
87
|
+
};
|
|
88
|
+
throw new Error(err.message || `Upload failed: ${response.status}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return response.json() as Promise<DeployUploadResult>;
|
|
92
|
+
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export enum JackErrorCode {
|
|
2
2
|
AUTH_FAILED = "AUTH_FAILED",
|
|
3
3
|
WRANGLER_AUTH_EXPIRED = "WRANGLER_AUTH_EXPIRED",
|
|
4
|
+
JACK_AUTH_REQUIRED = "JACK_AUTH_REQUIRED",
|
|
5
|
+
JACK_AUTH_EXPIRED = "JACK_AUTH_EXPIRED",
|
|
4
6
|
PROJECT_NOT_FOUND = "PROJECT_NOT_FOUND",
|
|
5
7
|
TEMPLATE_NOT_FOUND = "TEMPLATE_NOT_FOUND",
|
|
6
8
|
BUILD_FAILED = "BUILD_FAILED",
|
package/src/lib/github.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { $ } from "bun";
|
|
5
|
-
import type { Template } from "../templates/types";
|
|
5
|
+
import type { AgentContext, Capability, Template, TemplateHooks } from "../templates/types";
|
|
6
|
+
import { parseJsonc } from "./jsonc.ts";
|
|
7
|
+
import type { ServiceTypeKey } from "./services/index.ts";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Parse GitHub input: "user/repo" or "https://github.com/user/repo"
|
|
@@ -110,6 +112,34 @@ export async function fetchFromGitHub(input: string): Promise<Template> {
|
|
|
110
112
|
console.warn(" (no wrangler.toml or worker entry point found)\n");
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
// Read .jack.json metadata if it exists
|
|
116
|
+
const jackJsonContent = files[".jack.json"];
|
|
117
|
+
if (jackJsonContent) {
|
|
118
|
+
try {
|
|
119
|
+
const metadata = parseJsonc(jackJsonContent) as {
|
|
120
|
+
description?: string;
|
|
121
|
+
secrets?: string[];
|
|
122
|
+
capabilities?: Capability[];
|
|
123
|
+
requires?: ServiceTypeKey[];
|
|
124
|
+
hooks?: TemplateHooks;
|
|
125
|
+
agentContext?: AgentContext;
|
|
126
|
+
};
|
|
127
|
+
// Remove .jack.json from files (not needed in project)
|
|
128
|
+
const { ".jack.json": _, ...filesWithoutJackJson } = files;
|
|
129
|
+
return {
|
|
130
|
+
description: metadata.description || `GitHub: ${owner}/${repo}`,
|
|
131
|
+
secrets: metadata.secrets,
|
|
132
|
+
capabilities: metadata.capabilities,
|
|
133
|
+
requires: metadata.requires,
|
|
134
|
+
hooks: metadata.hooks,
|
|
135
|
+
agentContext: metadata.agentContext,
|
|
136
|
+
files: filesWithoutJackJson,
|
|
137
|
+
};
|
|
138
|
+
} catch {
|
|
139
|
+
// Invalid JSON, fall through to default
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
113
143
|
return {
|
|
114
144
|
description: `GitHub: ${owner}/${repo}`,
|
|
115
145
|
files,
|
package/src/lib/hooks.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { HookAction } from "../templates/types";
|
|
4
4
|
import { getSavedSecrets } from "./secrets";
|
|
5
|
+
import { restoreTty } from "./tty";
|
|
5
6
|
|
|
6
7
|
export interface HookContext {
|
|
7
8
|
domain?: string; // deployed domain (e.g., "my-app.username.workers.dev")
|
|
@@ -71,10 +72,7 @@ export async function promptSelect(options: string[]): Promise<number> {
|
|
|
71
72
|
|
|
72
73
|
const cleanup = () => {
|
|
73
74
|
process.stdin.removeListener("data", onData);
|
|
74
|
-
|
|
75
|
-
process.stdin.setRawMode(false);
|
|
76
|
-
}
|
|
77
|
-
process.stdin.pause();
|
|
75
|
+
restoreTty();
|
|
78
76
|
};
|
|
79
77
|
|
|
80
78
|
process.stdin.on("data", onData);
|
|
@@ -98,19 +96,13 @@ async function waitForEnter(message?: string): Promise<void> {
|
|
|
98
96
|
// Enter, Space, or any key to continue
|
|
99
97
|
if (char === "\r" || char === "\n" || char === " ") {
|
|
100
98
|
process.stdin.removeListener("data", onData);
|
|
101
|
-
|
|
102
|
-
process.stdin.setRawMode(false);
|
|
103
|
-
}
|
|
104
|
-
process.stdin.pause();
|
|
99
|
+
restoreTty();
|
|
105
100
|
resolve();
|
|
106
101
|
}
|
|
107
102
|
// Esc to skip
|
|
108
103
|
if (char === "\x1b") {
|
|
109
104
|
process.stdin.removeListener("data", onData);
|
|
110
|
-
|
|
111
|
-
process.stdin.setRawMode(false);
|
|
112
|
-
}
|
|
113
|
-
process.stdin.pause();
|
|
105
|
+
restoreTty();
|
|
114
106
|
resolve();
|
|
115
107
|
}
|
|
116
108
|
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { BUILTIN_TEMPLATES, resolveTemplateWithOrigin } from "../templates/index.ts";
|
|
2
|
+
|
|
3
|
+
export interface IntentMatchResult {
|
|
4
|
+
template: string;
|
|
5
|
+
matchedKeywords: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Match an intent phrase against template keywords
|
|
10
|
+
* Uses case-insensitive partial matching
|
|
11
|
+
*/
|
|
12
|
+
export function matchTemplateByIntent(
|
|
13
|
+
phrase: string,
|
|
14
|
+
templates: Array<{ name: string; keywords: string[] }>,
|
|
15
|
+
): IntentMatchResult[] {
|
|
16
|
+
const normalizedPhrase = phrase.toLowerCase();
|
|
17
|
+
const matches: IntentMatchResult[] = [];
|
|
18
|
+
|
|
19
|
+
for (const template of templates) {
|
|
20
|
+
const matchedKeywords: string[] = [];
|
|
21
|
+
|
|
22
|
+
for (const keyword of template.keywords) {
|
|
23
|
+
// Case-insensitive partial match - keyword appears anywhere in phrase
|
|
24
|
+
if (normalizedPhrase.includes(keyword.toLowerCase())) {
|
|
25
|
+
matchedKeywords.push(keyword);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (matchedKeywords.length > 0) {
|
|
30
|
+
matches.push({
|
|
31
|
+
template: template.name,
|
|
32
|
+
matchedKeywords,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return matches;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detect if the first arg is an intent phrase vs a project name
|
|
42
|
+
* Heuristics:
|
|
43
|
+
* - Contains spaces -> intent phrase
|
|
44
|
+
* - Contains special characters (except hyphen/underscore) -> intent phrase
|
|
45
|
+
* - Matches common intent indicator words -> intent phrase
|
|
46
|
+
* - Otherwise -> project name
|
|
47
|
+
*/
|
|
48
|
+
export function isIntentPhrase(arg: string): boolean {
|
|
49
|
+
// Contains spaces -> definitely intent
|
|
50
|
+
if (arg.includes(" ")) return true;
|
|
51
|
+
|
|
52
|
+
// Contains special chars (except hyphen/underscore) -> intent
|
|
53
|
+
if (/[^a-zA-Z0-9_-]/.test(arg)) return true;
|
|
54
|
+
|
|
55
|
+
// Common intent indicator words (single words that suggest intent)
|
|
56
|
+
const intentWords = ["api", "webhook", "miniapp", "dashboard", "frontend", "backend", "endpoint"];
|
|
57
|
+
const lower = arg.toLowerCase();
|
|
58
|
+
|
|
59
|
+
// Exact match or compound word containing the intent word
|
|
60
|
+
for (const word of intentWords) {
|
|
61
|
+
if (lower === word) return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Load all templates and extract their intent keywords
|
|
69
|
+
*/
|
|
70
|
+
export async function loadTemplateKeywords(): Promise<Array<{ name: string; keywords: string[] }>> {
|
|
71
|
+
const templates: Array<{ name: string; keywords: string[] }> = [];
|
|
72
|
+
|
|
73
|
+
for (const name of BUILTIN_TEMPLATES) {
|
|
74
|
+
try {
|
|
75
|
+
const { template } = await resolveTemplateWithOrigin(name);
|
|
76
|
+
if (template.intent?.keywords && template.intent.keywords.length > 0) {
|
|
77
|
+
templates.push({
|
|
78
|
+
name,
|
|
79
|
+
keywords: template.intent.keywords,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Skip templates that fail to load
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return templates;
|
|
88
|
+
}
|