@getjack/jack 0.1.17 → 0.1.20
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 +2 -5
- package/src/commands/clone.ts +62 -40
- package/src/commands/down.ts +11 -1
- package/src/commands/init.ts +21 -2
- package/src/commands/services.ts +85 -4
- package/src/commands/sync.ts +9 -0
- package/src/commands/update.ts +10 -0
- package/src/lib/agents.ts +3 -1
- package/src/lib/auth/ensure-auth.test.ts +3 -3
- package/src/lib/control-plane.ts +77 -1
- package/src/lib/deploy-upload.ts +26 -1
- package/src/lib/hooks.ts +232 -1
- package/src/lib/managed-deploy.ts +37 -6
- package/src/lib/output.ts +21 -3
- package/src/lib/progress.ts +231 -0
- package/src/lib/project-list.ts +6 -1
- package/src/lib/project-operations.ts +117 -62
- package/src/lib/project-resolver.ts +1 -1
- package/src/lib/services/db-create.ts +6 -3
- package/src/lib/version-check.ts +14 -0
- package/src/lib/wrangler-config.ts +190 -0
- package/src/lib/zip-packager.ts +74 -7
- package/src/lib/zip-utils.ts +38 -0
- package/src/templates/index.ts +1 -1
- package/src/templates/types.ts +16 -0
- package/templates/CLAUDE.md +103 -0
- package/templates/miniapp/.jack.json +1 -3
- package/templates/saas/.jack.json +154 -0
- package/templates/saas/AGENTS.md +333 -0
- package/templates/saas/bun.lock +925 -0
- package/templates/saas/components.json +21 -0
- package/templates/saas/index.html +12 -0
- package/templates/saas/package.json +75 -0
- package/templates/saas/public/icon.png +0 -0
- package/templates/saas/public/og.png +0 -0
- package/templates/saas/schema.sql +73 -0
- package/templates/saas/src/auth.ts +77 -0
- package/templates/saas/src/client/App.tsx +63 -0
- package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
- package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
- package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
- package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
- package/templates/saas/src/client/components/ui/alert.tsx +60 -0
- package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
- package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
- package/templates/saas/src/client/components/ui/badge.tsx +39 -0
- package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
- package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
- package/templates/saas/src/client/components/ui/button.tsx +60 -0
- package/templates/saas/src/client/components/ui/card.tsx +75 -0
- package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
- package/templates/saas/src/client/components/ui/chart.tsx +326 -0
- package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
- package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
- package/templates/saas/src/client/components/ui/command.tsx +159 -0
- package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
- package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
- package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
- package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
- package/templates/saas/src/client/components/ui/empty.tsx +94 -0
- package/templates/saas/src/client/components/ui/field.tsx +232 -0
- package/templates/saas/src/client/components/ui/form.tsx +152 -0
- package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
- package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
- package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
- package/templates/saas/src/client/components/ui/input.tsx +21 -0
- package/templates/saas/src/client/components/ui/item.tsx +172 -0
- package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
- package/templates/saas/src/client/components/ui/label.tsx +21 -0
- package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
- package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
- package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
- package/templates/saas/src/client/components/ui/popover.tsx +42 -0
- package/templates/saas/src/client/components/ui/progress.tsx +26 -0
- package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
- package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
- package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
- package/templates/saas/src/client/components/ui/select.tsx +173 -0
- package/templates/saas/src/client/components/ui/separator.tsx +28 -0
- package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
- package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
- package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
- package/templates/saas/src/client/components/ui/slider.tsx +58 -0
- package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
- package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
- package/templates/saas/src/client/components/ui/switch.tsx +28 -0
- package/templates/saas/src/client/components/ui/table.tsx +90 -0
- package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
- package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
- package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
- package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
- package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
- package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
- package/templates/saas/src/client/hooks/useAuth.ts +14 -0
- package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
- package/templates/saas/src/client/index.css +165 -0
- package/templates/saas/src/client/lib/auth-client.ts +7 -0
- package/templates/saas/src/client/lib/plans.ts +82 -0
- package/templates/saas/src/client/lib/utils.ts +6 -0
- package/templates/saas/src/client/main.tsx +15 -0
- package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
- package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
- package/templates/saas/src/client/pages/HomePage.tsx +285 -0
- package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
- package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
- package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
- package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
- package/templates/saas/src/index.ts +208 -0
- package/templates/saas/tsconfig.json +18 -0
- package/templates/saas/vite.config.ts +14 -0
- package/templates/saas/wrangler.jsonc +20 -0
package/package.json
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getjack/jack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
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
|
},
|
package/src/commands/clone.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { isCancel, select } from "@clack/prompts";
|
|
4
|
-
import { fetchProjectTags } from "../lib/control-plane.ts";
|
|
4
|
+
import { downloadProjectSource, fetchProjectTags } from "../lib/control-plane.ts";
|
|
5
5
|
import { formatSize } from "../lib/format.ts";
|
|
6
6
|
import { box, error, info, spinner, success } from "../lib/output.ts";
|
|
7
7
|
import { registerPath } from "../lib/paths-index.ts";
|
|
8
8
|
import { linkProject, updateProjectLink } from "../lib/project-link.ts";
|
|
9
9
|
import { resolveProject } from "../lib/project-resolver.ts";
|
|
10
10
|
import { cloneFromCloud, getRemoteManifest } from "../lib/storage/index.ts";
|
|
11
|
+
import { extractZipToDirectory } from "../lib/zip-utils.ts";
|
|
11
12
|
|
|
12
13
|
export interface CloneFlags {
|
|
13
14
|
as?: string;
|
|
@@ -53,53 +54,74 @@ export default async function clone(projectName?: string, flags: CloneFlags = {}
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
//
|
|
57
|
-
const spin = spinner(`
|
|
58
|
-
|
|
57
|
+
// Check if this is a managed project (has source in control-plane)
|
|
58
|
+
const spin = spinner(`Looking up ${projectName}...`);
|
|
59
|
+
let project: Awaited<ReturnType<typeof resolveProject>> = null;
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
try {
|
|
62
|
+
project = await resolveProject(projectName);
|
|
63
|
+
} catch {
|
|
64
|
+
// Not found on control-plane, will fall back to User R2
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
// Managed mode: download from control-plane
|
|
68
|
+
if (project?.sources.controlPlane && project.remote?.projectId) {
|
|
69
|
+
spin.success("Found on jack cloud");
|
|
70
|
+
|
|
71
|
+
const downloadSpin = spinner("Downloading from jack cloud...");
|
|
72
|
+
try {
|
|
73
|
+
const sourceZip = await downloadProjectSource(projectName);
|
|
74
|
+
const fileCount = await extractZipToDirectory(sourceZip, targetDir);
|
|
75
|
+
downloadSpin.success(`Restored ${fileCount} file(s) to ./${flags.as ?? projectName}/`);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
downloadSpin.error("Download failed");
|
|
78
|
+
const message = err instanceof Error ? err.message : "Could not download project source";
|
|
79
|
+
error(message);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
// Link to control-plane
|
|
84
|
+
await linkProject(targetDir, project.remote.projectId, "managed");
|
|
85
|
+
await registerPath(project.remote.projectId, targetDir);
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
// Fetch and restore tags from control plane
|
|
88
|
+
try {
|
|
89
|
+
const remoteTags = await fetchProjectTags(project.remote.projectId);
|
|
90
|
+
if (remoteTags.length > 0) {
|
|
91
|
+
await updateProjectLink(targetDir, { tags: remoteTags });
|
|
92
|
+
info(`Restored ${remoteTags.length} tag(s)`);
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Silent fail - tag restoration is non-critical
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// BYO mode: use existing User R2 flow
|
|
99
|
+
spin.stop();
|
|
100
|
+
const fetchSpin = spinner(`Fetching from jack-storage/${projectName}/...`);
|
|
101
|
+
const manifest = await getRemoteManifest(projectName);
|
|
102
|
+
|
|
103
|
+
if (!manifest) {
|
|
104
|
+
fetchSpin.error(`Project not found: ${projectName}`);
|
|
105
|
+
info("For BYO projects, run 'jack sync' first to backup your project.");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
79
108
|
|
|
80
|
-
|
|
109
|
+
// Show file count and size
|
|
110
|
+
const totalSize = manifest.files.reduce((sum, f) => sum + f.size, 0);
|
|
111
|
+
fetchSpin.success(`Found ${manifest.files.length} file(s) (${formatSize(totalSize)})`);
|
|
81
112
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
const remoteTags = await fetchProjectTags(project.remote.projectId);
|
|
93
|
-
if (remoteTags.length > 0) {
|
|
94
|
-
await updateProjectLink(targetDir, { tags: remoteTags });
|
|
95
|
-
info(`Restored ${remoteTags.length} tag(s)`);
|
|
96
|
-
}
|
|
97
|
-
} catch {
|
|
98
|
-
// Silent fail - tag restoration is non-critical
|
|
99
|
-
}
|
|
113
|
+
// Download files
|
|
114
|
+
const downloadSpin = spinner("Downloading...");
|
|
115
|
+
const result = await cloneFromCloud(projectName, targetDir);
|
|
116
|
+
|
|
117
|
+
if (!result.success) {
|
|
118
|
+
downloadSpin.error("Clone failed");
|
|
119
|
+
error(result.error || "Could not download project files");
|
|
120
|
+
info("Check your network connection and try again");
|
|
121
|
+
process.exit(1);
|
|
100
122
|
}
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
|
|
124
|
+
downloadSpin.success(`Restored to ./${flags.as ?? projectName}/`);
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
// Show next steps
|
package/src/commands/down.ts
CHANGED
|
@@ -10,7 +10,8 @@ import { fetchProjectResources } from "../lib/control-plane.ts";
|
|
|
10
10
|
import { promptSelect } from "../lib/hooks.ts";
|
|
11
11
|
import { managedDown } from "../lib/managed-down.ts";
|
|
12
12
|
import { error, info, item, output, success, warn } from "../lib/output.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { unregisterPath } from "../lib/paths-index.ts";
|
|
14
|
+
import { type LocalProjectLink, readProjectLink, unlinkProject } from "../lib/project-link.ts";
|
|
14
15
|
import { resolveProject } from "../lib/project-resolver.ts";
|
|
15
16
|
import { parseWranglerResources } from "../lib/resources.ts";
|
|
16
17
|
import { deleteCloudProject, getProjectNameFromDir } from "../lib/storage/index.ts";
|
|
@@ -124,6 +125,15 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
|
|
|
124
125
|
if (!deleteSuccess) {
|
|
125
126
|
process.exit(0); // User cancelled
|
|
126
127
|
}
|
|
128
|
+
|
|
129
|
+
// Clean up local tracking state
|
|
130
|
+
const localPath = resolved?.localPath || process.cwd();
|
|
131
|
+
try {
|
|
132
|
+
await unlinkProject(localPath);
|
|
133
|
+
await unregisterPath(projectId, localPath);
|
|
134
|
+
} catch {
|
|
135
|
+
// Non-fatal: local cleanup failed but cloud deletion succeeded
|
|
136
|
+
}
|
|
127
137
|
return;
|
|
128
138
|
}
|
|
129
139
|
|
package/src/commands/init.ts
CHANGED
|
@@ -11,8 +11,27 @@ import { ensureAuth, ensureWrangler, isAuthenticated } from "../lib/wrangler.ts"
|
|
|
11
11
|
|
|
12
12
|
export async function isInitialized(): Promise<boolean> {
|
|
13
13
|
const config = await readConfig();
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const { isLoggedIn } = await import("../lib/auth/store.ts");
|
|
15
|
+
|
|
16
|
+
if (config?.initialized) {
|
|
17
|
+
if ((await isLoggedIn()) || (await isAuthenticated())) return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Auto-initialize if authenticated
|
|
21
|
+
const loggedIn = await isLoggedIn();
|
|
22
|
+
const wranglerAuth = !loggedIn && (await isAuthenticated());
|
|
23
|
+
|
|
24
|
+
if (loggedIn || wranglerAuth) {
|
|
25
|
+
await writeConfig({
|
|
26
|
+
version: 1,
|
|
27
|
+
initialized: true,
|
|
28
|
+
initializedAt: new Date().toISOString(),
|
|
29
|
+
...config,
|
|
30
|
+
});
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
16
35
|
}
|
|
17
36
|
|
|
18
37
|
interface InitOptions {
|
package/src/commands/services.ts
CHANGED
|
@@ -277,6 +277,12 @@ async function dbExport(options: ServiceOptions): Promise<void> {
|
|
|
277
277
|
*/
|
|
278
278
|
async function dbDelete(options: ServiceOptions): Promise<void> {
|
|
279
279
|
const projectName = await resolveProjectName(options);
|
|
280
|
+
const projectDir = process.cwd();
|
|
281
|
+
|
|
282
|
+
// Check deploy mode
|
|
283
|
+
const link = await readProjectLink(projectDir);
|
|
284
|
+
const isManaged = link?.deploy_mode === "managed";
|
|
285
|
+
|
|
280
286
|
const dbInfo = await resolveDatabaseInfo(projectName);
|
|
281
287
|
|
|
282
288
|
if (!dbInfo) {
|
|
@@ -314,14 +320,80 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
|
|
|
314
320
|
return;
|
|
315
321
|
}
|
|
316
322
|
|
|
317
|
-
// Delete database
|
|
318
323
|
outputSpinner.start("Deleting database...");
|
|
324
|
+
|
|
325
|
+
// Track binding_name from control plane for matching in wrangler.jsonc
|
|
326
|
+
let controlPlaneBindingName: string | null = null;
|
|
327
|
+
|
|
319
328
|
try {
|
|
320
|
-
|
|
329
|
+
if (isManaged && link) {
|
|
330
|
+
// Managed mode: delete via control plane
|
|
331
|
+
const { fetchProjectResources, deleteProjectResource } = await import(
|
|
332
|
+
"../lib/control-plane.ts"
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// Find the resource ID for this database
|
|
336
|
+
const resources = await fetchProjectResources(link.project_id);
|
|
337
|
+
const d1Resource = resources.find(
|
|
338
|
+
(r) => r.resource_type === "d1" && r.resource_name === dbInfo.name,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (d1Resource) {
|
|
342
|
+
// Save binding_name for wrangler.jsonc cleanup
|
|
343
|
+
controlPlaneBindingName = d1Resource.binding_name;
|
|
344
|
+
// Delete via control plane (which also deletes from Cloudflare)
|
|
345
|
+
await deleteProjectResource(link.project_id, d1Resource.id);
|
|
346
|
+
} else {
|
|
347
|
+
// Resource not in control plane - fall back to wrangler for cleanup
|
|
348
|
+
await deleteDatabase(dbInfo.name);
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
// BYO mode: delete via wrangler directly
|
|
352
|
+
await deleteDatabase(dbInfo.name);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Remove binding from wrangler.jsonc (both modes)
|
|
356
|
+
// Note: We need to find the LOCAL database_name from wrangler.jsonc,
|
|
357
|
+
// which may differ from the control plane's resource_name
|
|
358
|
+
const { removeD1Binding, getExistingD1Bindings } = await import("../lib/wrangler-config.ts");
|
|
359
|
+
const configPath = join(projectDir, "wrangler.jsonc");
|
|
360
|
+
|
|
361
|
+
let bindingRemoved = false;
|
|
362
|
+
try {
|
|
363
|
+
// Find the binding by matching (in order of reliability):
|
|
364
|
+
// 1. binding name (e.g., "DB") - if control plane provided it
|
|
365
|
+
// 2. database_id (provider_id from control plane)
|
|
366
|
+
// 3. database_name
|
|
367
|
+
// 4. If managed mode and we successfully deleted, remove first D1 binding
|
|
368
|
+
const existingBindings = await getExistingD1Bindings(configPath);
|
|
369
|
+
let bindingToRemove = existingBindings.find(
|
|
370
|
+
(b) =>
|
|
371
|
+
(controlPlaneBindingName && b.binding === controlPlaneBindingName) ||
|
|
372
|
+
b.database_id === dbInfo.id ||
|
|
373
|
+
b.database_name === dbInfo.name,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// Fallback: if managed mode and we deleted from control plane,
|
|
377
|
+
// remove the first D1 binding (binding_name may be null for older DBs)
|
|
378
|
+
if (!bindingToRemove && isManaged && existingBindings.length > 0) {
|
|
379
|
+
bindingToRemove = existingBindings[0];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (bindingToRemove) {
|
|
383
|
+
bindingRemoved = await removeD1Binding(configPath, bindingToRemove.database_name);
|
|
384
|
+
}
|
|
385
|
+
} catch (bindingErr) {
|
|
386
|
+
// Log but don't fail - the database is already deleted
|
|
387
|
+
// The user can manually clean up wrangler.jsonc if needed
|
|
388
|
+
}
|
|
389
|
+
|
|
321
390
|
outputSpinner.stop();
|
|
322
391
|
|
|
323
392
|
console.error("");
|
|
324
393
|
success("Database deleted");
|
|
394
|
+
if (bindingRemoved) {
|
|
395
|
+
item("Binding removed from wrangler.jsonc");
|
|
396
|
+
}
|
|
325
397
|
console.error("");
|
|
326
398
|
} catch (err) {
|
|
327
399
|
outputSpinner.stop();
|
|
@@ -332,10 +404,11 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
|
|
|
332
404
|
}
|
|
333
405
|
|
|
334
406
|
/**
|
|
335
|
-
* Parse --name flag from args
|
|
336
|
-
* Supports: --name foo, --name=foo
|
|
407
|
+
* Parse --name flag or positional arg from args
|
|
408
|
+
* Supports: --name foo, --name=foo, or first positional arg
|
|
337
409
|
*/
|
|
338
410
|
function parseNameFlag(args: string[]): string | undefined {
|
|
411
|
+
// Check --name flag first (takes priority)
|
|
339
412
|
for (let i = 0; i < args.length; i++) {
|
|
340
413
|
const arg = args[i];
|
|
341
414
|
if (arg === "--name" && args[i + 1]) {
|
|
@@ -345,6 +418,14 @@ function parseNameFlag(args: string[]): string | undefined {
|
|
|
345
418
|
return arg.slice("--name=".length);
|
|
346
419
|
}
|
|
347
420
|
}
|
|
421
|
+
|
|
422
|
+
// Fall back to first positional argument (non-flag)
|
|
423
|
+
for (const arg of args) {
|
|
424
|
+
if (!arg.startsWith("-")) {
|
|
425
|
+
return arg;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
348
429
|
return undefined;
|
|
349
430
|
}
|
|
350
431
|
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { getSyncConfig } from "../lib/config.ts";
|
|
3
3
|
import { error, info, spinner, success, warn } from "../lib/output.ts";
|
|
4
|
+
import { readProjectLink } from "../lib/project-link.ts";
|
|
4
5
|
import { syncToCloud } from "../lib/storage/index.ts";
|
|
5
6
|
|
|
6
7
|
export interface SyncFlags {
|
|
@@ -25,6 +26,14 @@ export default async function sync(flags: SyncFlags = {}): Promise<void> {
|
|
|
25
26
|
process.exit(1);
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
// Check if this is a managed project
|
|
30
|
+
const link = await readProjectLink(process.cwd());
|
|
31
|
+
if (link?.deploy_mode === "managed") {
|
|
32
|
+
info("Managed projects are automatically backed up to jack cloud during deploy.");
|
|
33
|
+
info("Use 'jack clone <project>' on another machine to restore.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
// Check if sync is enabled
|
|
29
38
|
const syncConfig = await getSyncConfig();
|
|
30
39
|
if (!syncConfig.enabled) {
|
package/src/commands/update.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* jack update - Self-update to the latest version
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { debug } from "../lib/debug.ts";
|
|
5
6
|
import { error, info, success, warn } from "../lib/output.ts";
|
|
6
7
|
import {
|
|
7
8
|
checkForUpdate,
|
|
@@ -13,8 +14,13 @@ import {
|
|
|
13
14
|
export default async function update(): Promise<void> {
|
|
14
15
|
const currentVersion = getCurrentVersion();
|
|
15
16
|
|
|
17
|
+
debug("Update check started");
|
|
18
|
+
debug(`Current version: ${currentVersion}`);
|
|
19
|
+
debug(`Exec path: ${process.argv[1]}`);
|
|
20
|
+
|
|
16
21
|
// Check if running via bunx
|
|
17
22
|
if (isRunningViaBunx()) {
|
|
23
|
+
debug("Detected bunx execution");
|
|
18
24
|
info(`Running via bunx (current: v${currentVersion})`);
|
|
19
25
|
info("bunx automatically uses cached packages.");
|
|
20
26
|
info("To get the latest version, run:");
|
|
@@ -28,7 +34,9 @@ export default async function update(): Promise<void> {
|
|
|
28
34
|
info(`Current version: v${currentVersion}`);
|
|
29
35
|
|
|
30
36
|
// Check for updates
|
|
37
|
+
debug("Fetching latest version from npm...");
|
|
31
38
|
const latestVersion = await checkForUpdate();
|
|
39
|
+
debug(`Latest version from npm: ${latestVersion ?? "none (you're up to date)"}`);
|
|
32
40
|
|
|
33
41
|
if (!latestVersion) {
|
|
34
42
|
success("You're on the latest version!");
|
|
@@ -38,7 +46,9 @@ export default async function update(): Promise<void> {
|
|
|
38
46
|
info(`New version available: v${latestVersion}`);
|
|
39
47
|
info("Updating...");
|
|
40
48
|
|
|
49
|
+
debug("Running: bun add -g @getjack/jack@latest");
|
|
41
50
|
const result = await performUpdate();
|
|
51
|
+
debug(`Update result: ${JSON.stringify(result)}`);
|
|
42
52
|
|
|
43
53
|
if (result.success) {
|
|
44
54
|
success(`Updated to v${result.version ?? latestVersion}`);
|
package/src/lib/agents.ts
CHANGED
|
@@ -434,7 +434,9 @@ export interface AgentLaunchContext {
|
|
|
434
434
|
function buildInitialPrompt(context: AgentLaunchContext): string | null {
|
|
435
435
|
if (!context.url) return null;
|
|
436
436
|
|
|
437
|
-
return `
|
|
437
|
+
return `Project "${context.projectName}" is live at ${context.url}
|
|
438
|
+
|
|
439
|
+
Read CLAUDE.md and AGENTS.md for project context, then say hi and offer to help build.`;
|
|
438
440
|
}
|
|
439
441
|
|
|
440
442
|
function buildLaunchCommand(
|
|
@@ -210,9 +210,9 @@ describe("ensureAuthForCreate", () => {
|
|
|
210
210
|
});
|
|
211
211
|
|
|
212
212
|
it("throws error when both forceManaged and forceByo are set", async () => {
|
|
213
|
-
await expect(
|
|
214
|
-
|
|
215
|
-
)
|
|
213
|
+
await expect(ensureAuthForCreate({ forceManaged: true, forceByo: true })).rejects.toThrow(
|
|
214
|
+
"Cannot use both --managed and --byo flags",
|
|
215
|
+
);
|
|
216
216
|
});
|
|
217
217
|
});
|
|
218
218
|
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Control Plane API client for jack cloud
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { debug } from "./debug.ts";
|
|
6
|
+
import { formatSize } from "./format.ts";
|
|
7
|
+
|
|
5
8
|
const DEFAULT_CONTROL_API_URL = "https://control.getjack.org";
|
|
6
9
|
|
|
7
10
|
export function getControlApiUrl(): string {
|
|
@@ -109,6 +112,9 @@ export async function createManagedProject(
|
|
|
109
112
|
requestBody.use_prebuilt = options.usePrebuilt;
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
debug("Creating managed project", { name, template: options?.template, usePrebuilt: options?.usePrebuilt });
|
|
116
|
+
const start = Date.now();
|
|
117
|
+
|
|
112
118
|
const response = await authFetch(`${getControlApiUrl()}/v1/projects`, {
|
|
113
119
|
method: "POST",
|
|
114
120
|
headers: {
|
|
@@ -118,6 +124,9 @@ export async function createManagedProject(
|
|
|
118
124
|
body: JSON.stringify(requestBody),
|
|
119
125
|
});
|
|
120
126
|
|
|
127
|
+
const duration = ((Date.now() - start) / 1000).toFixed(1);
|
|
128
|
+
debug(`Control plane response: ${response.status} (${duration}s)`);
|
|
129
|
+
|
|
121
130
|
if (!response.ok) {
|
|
122
131
|
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
123
132
|
message?: string;
|
|
@@ -371,6 +380,46 @@ export async function createProjectResource(
|
|
|
371
380
|
return data.resource;
|
|
372
381
|
}
|
|
373
382
|
|
|
383
|
+
export interface DeleteResourceResponse {
|
|
384
|
+
success: boolean;
|
|
385
|
+
resource_id: string;
|
|
386
|
+
deleted_at: string;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Delete a resource from a managed project.
|
|
391
|
+
* Uses DELETE /v1/projects/:id/resources/:id endpoint.
|
|
392
|
+
*/
|
|
393
|
+
export async function deleteProjectResource(
|
|
394
|
+
projectId: string,
|
|
395
|
+
resourceId: string,
|
|
396
|
+
): Promise<DeleteResourceResponse> {
|
|
397
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
398
|
+
|
|
399
|
+
const response = await authFetch(
|
|
400
|
+
`${getControlApiUrl()}/v1/projects/${projectId}/resources/${resourceId}`,
|
|
401
|
+
{ method: "DELETE" },
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
// Handle 404 gracefully - resource may already be deleted
|
|
405
|
+
if (response.status === 404) {
|
|
406
|
+
return {
|
|
407
|
+
success: true,
|
|
408
|
+
resource_id: resourceId,
|
|
409
|
+
deleted_at: new Date().toISOString(),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!response.ok) {
|
|
414
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
415
|
+
message?: string;
|
|
416
|
+
};
|
|
417
|
+
throw new Error(err.message || `Failed to delete resource: ${response.status}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return response.json() as Promise<DeleteResourceResponse>;
|
|
421
|
+
}
|
|
422
|
+
|
|
374
423
|
/**
|
|
375
424
|
* Sync project tags to the control plane.
|
|
376
425
|
* Fire-and-forget: errors are logged but not thrown.
|
|
@@ -546,10 +595,15 @@ export async function uploadSourceSnapshot(
|
|
|
546
595
|
const sourceFile = Bun.file(sourceZipPath);
|
|
547
596
|
formData.append("source", sourceFile);
|
|
548
597
|
|
|
549
|
-
const
|
|
598
|
+
const url = `${getControlApiUrl()}/v1/projects/${projectId}/source`;
|
|
599
|
+
debug(`Source snapshot: ${formatSize(sourceFile.size)}`);
|
|
600
|
+
|
|
601
|
+
const start = Date.now();
|
|
602
|
+
const response = await authFetch(url, {
|
|
550
603
|
method: "POST",
|
|
551
604
|
body: formData,
|
|
552
605
|
});
|
|
606
|
+
debug(`Source snapshot: ${response.status} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
|
|
553
607
|
|
|
554
608
|
if (!response.ok) {
|
|
555
609
|
const error = (await response.json().catch(() => ({ message: "Upload failed" }))) as {
|
|
@@ -580,3 +634,25 @@ export async function publishProject(projectId: string): Promise<PublishProjectR
|
|
|
580
634
|
|
|
581
635
|
return response.json() as Promise<PublishProjectResponse>;
|
|
582
636
|
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Download the source snapshot for a project.
|
|
640
|
+
* Returns the zip file contents as a Buffer.
|
|
641
|
+
* Used by jack clone to restore managed projects.
|
|
642
|
+
*/
|
|
643
|
+
export async function downloadProjectSource(slug: string): Promise<Buffer> {
|
|
644
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
645
|
+
|
|
646
|
+
const response = await authFetch(
|
|
647
|
+
`${getControlApiUrl()}/v1/projects/by-slug/${encodeURIComponent(slug)}/source`,
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
if (!response.ok) {
|
|
651
|
+
if (response.status === 404) {
|
|
652
|
+
throw new Error("Project source not found. Deploy first with 'jack ship'.");
|
|
653
|
+
}
|
|
654
|
+
throw new Error(`Failed to download source: ${response.status}`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return Buffer.from(await response.arrayBuffer());
|
|
658
|
+
}
|
package/src/lib/deploy-upload.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { readFile } from "node:fs/promises";
|
|
|
6
6
|
import type { AssetManifest } from "./asset-hash.ts";
|
|
7
7
|
import { authFetch } from "./auth/index.ts";
|
|
8
8
|
import { getControlApiUrl } from "./control-plane.ts";
|
|
9
|
+
import { debug } from "./debug.ts";
|
|
10
|
+
import { formatSize } from "./format.ts";
|
|
9
11
|
|
|
10
12
|
export interface DeployUploadOptions {
|
|
11
13
|
projectId: string;
|
|
@@ -31,6 +33,9 @@ export interface DeployUploadResult {
|
|
|
31
33
|
*/
|
|
32
34
|
export async function uploadDeployment(options: DeployUploadOptions): Promise<DeployUploadResult> {
|
|
33
35
|
const formData = new FormData();
|
|
36
|
+
let totalSize = 0;
|
|
37
|
+
|
|
38
|
+
const prepareStart = Date.now();
|
|
34
39
|
|
|
35
40
|
// Read files and add to form data
|
|
36
41
|
const manifestContent = await readFile(options.manifestPath);
|
|
@@ -39,17 +44,24 @@ export async function uploadDeployment(options: DeployUploadOptions): Promise<De
|
|
|
39
44
|
new Blob([manifestContent], { type: "application/json" }),
|
|
40
45
|
"manifest.json",
|
|
41
46
|
);
|
|
47
|
+
totalSize += manifestContent.length;
|
|
42
48
|
|
|
43
49
|
const bundleContent = await readFile(options.bundleZipPath);
|
|
44
50
|
formData.append("bundle", new Blob([bundleContent], { type: "application/zip" }), "bundle.zip");
|
|
51
|
+
totalSize += bundleContent.length;
|
|
52
|
+
debug(` bundle.zip: ${formatSize(bundleContent.length)}`);
|
|
45
53
|
|
|
46
54
|
const sourceContent = await readFile(options.sourceZipPath);
|
|
47
55
|
formData.append("source", new Blob([sourceContent], { type: "application/zip" }), "source.zip");
|
|
56
|
+
totalSize += sourceContent.length;
|
|
57
|
+
debug(` source.zip: ${formatSize(sourceContent.length)}`);
|
|
48
58
|
|
|
49
59
|
// Optional files
|
|
50
60
|
if (options.schemaPath) {
|
|
51
61
|
const schemaContent = await readFile(options.schemaPath);
|
|
52
62
|
formData.append("schema", new Blob([schemaContent], { type: "text/sql" }), "schema.sql");
|
|
63
|
+
totalSize += schemaContent.length;
|
|
64
|
+
debug(` schema.sql: ${formatSize(schemaContent.length)}`);
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
if (options.secretsPath) {
|
|
@@ -59,27 +71,40 @@ export async function uploadDeployment(options: DeployUploadOptions): Promise<De
|
|
|
59
71
|
new Blob([secretsContent], { type: "application/json" }),
|
|
60
72
|
"secrets.json",
|
|
61
73
|
);
|
|
74
|
+
totalSize += secretsContent.length;
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
if (options.assetsZipPath) {
|
|
65
78
|
const assetsContent = await readFile(options.assetsZipPath);
|
|
66
79
|
formData.append("assets", new Blob([assetsContent], { type: "application/zip" }), "assets.zip");
|
|
80
|
+
totalSize += assetsContent.length;
|
|
81
|
+
debug(` assets.zip: ${formatSize(assetsContent.length)}`);
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
if (options.assetManifest) {
|
|
85
|
+
const manifestJson = JSON.stringify(options.assetManifest);
|
|
70
86
|
formData.append(
|
|
71
87
|
"asset-manifest",
|
|
72
|
-
new Blob([
|
|
88
|
+
new Blob([manifestJson], { type: "application/json" }),
|
|
73
89
|
"asset-manifest.json",
|
|
74
90
|
);
|
|
91
|
+
totalSize += manifestJson.length;
|
|
75
92
|
}
|
|
76
93
|
|
|
94
|
+
const prepareMs = Date.now() - prepareStart;
|
|
95
|
+
debug(`Payload ready: ${formatSize(totalSize)} (${prepareMs}ms)`);
|
|
96
|
+
|
|
77
97
|
// POST to control plane
|
|
78
98
|
const url = `${getControlApiUrl()}/v1/projects/${options.projectId}/deployments/upload`;
|
|
99
|
+
debug(`POST ${url}`);
|
|
100
|
+
|
|
101
|
+
const uploadStart = Date.now();
|
|
79
102
|
const response = await authFetch(url, {
|
|
80
103
|
method: "POST",
|
|
81
104
|
body: formData,
|
|
82
105
|
});
|
|
106
|
+
const uploadMs = Date.now() - uploadStart;
|
|
107
|
+
debug(`Response: ${response.status} (${(uploadMs / 1000).toFixed(1)}s)`);
|
|
83
108
|
|
|
84
109
|
if (!response.ok) {
|
|
85
110
|
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|