@getjack/jack 0.1.19 → 0.1.22
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 +5 -2
- package/src/commands/down.ts +11 -1
- package/src/commands/init.ts +19 -6
- package/src/commands/new.ts +56 -4
- package/src/commands/publish.ts +1 -1
- package/src/lib/agents.ts +3 -1
- package/src/lib/auth/ensure-auth.test.ts +3 -3
- package/src/lib/control-plane.ts +15 -1
- package/src/lib/deploy-upload.ts +26 -1
- package/src/lib/hooks.ts +232 -1
- package/src/lib/managed-deploy.ts +13 -6
- package/src/lib/managed-down.ts +66 -45
- package/src/lib/progress.ts +76 -5
- package/src/lib/project-list.ts +6 -1
- package/src/lib/project-operations.ts +21 -31
- package/src/lib/project-resolver.ts +1 -1
- package/src/lib/zip-packager.ts +36 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/types.ts +16 -0
- package/templates/CLAUDE.md +172 -5
- 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/src/lib/managed-down.ts
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { writeFile } from "node:fs/promises";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
deleteManagedProject,
|
|
10
|
+
exportManagedDatabase,
|
|
11
|
+
fetchProjectResources,
|
|
12
|
+
} from "./control-plane.ts";
|
|
9
13
|
import { promptSelect } from "./hooks.ts";
|
|
10
14
|
import { error, info, item, output, success, warn } from "./output.ts";
|
|
11
15
|
|
|
@@ -47,14 +51,29 @@ export async function managedDown(
|
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
// Interactive mode
|
|
54
|
+
// Interactive mode - fetch actual resources
|
|
55
|
+
let hasDatabase = false;
|
|
56
|
+
let databaseName: string | null = null;
|
|
57
|
+
try {
|
|
58
|
+
const resources = await fetchProjectResources(projectId);
|
|
59
|
+
const d1Resource = resources.find((r) => r.resource_type === "d1");
|
|
60
|
+
if (d1Resource) {
|
|
61
|
+
hasDatabase = true;
|
|
62
|
+
databaseName = d1Resource.resource_name;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// If fetch fails, assume no database (safer than showing wrong info)
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
console.error("");
|
|
52
69
|
info(`Project: ${projectName}`);
|
|
53
70
|
if (runjackUrl) {
|
|
54
71
|
item(`URL: ${runjackUrl}`);
|
|
55
72
|
}
|
|
56
73
|
item("Mode: jack cloud (managed)");
|
|
57
|
-
|
|
74
|
+
if (hasDatabase) {
|
|
75
|
+
item(`Database: ${databaseName ?? "managed D1"}`);
|
|
76
|
+
}
|
|
58
77
|
console.error("");
|
|
59
78
|
|
|
60
79
|
// Confirm undeploy
|
|
@@ -67,49 +86,51 @@ export async function managedDown(
|
|
|
67
86
|
return false;
|
|
68
87
|
}
|
|
69
88
|
|
|
70
|
-
// Ask about database export
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
console.error("");
|
|
75
|
-
info("Export database before deleting?");
|
|
76
|
-
const exportAction = await promptSelect(["Yes", "No"]);
|
|
77
|
-
|
|
78
|
-
if (exportAction === 0) {
|
|
79
|
-
const exportPath = join(process.cwd(), `${projectName}-backup.sql`);
|
|
80
|
-
output.start(`Exporting database to ${exportPath}...`);
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const exportResult = await exportManagedDatabase(projectId);
|
|
84
|
-
|
|
85
|
-
// Download the SQL file
|
|
86
|
-
const response = await fetch(exportResult.download_url);
|
|
87
|
-
if (!response.ok) {
|
|
88
|
-
throw new Error(`Failed to download export: ${response.statusText}`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const sqlContent = await response.text();
|
|
92
|
-
await writeFile(exportPath, sqlContent, "utf-8");
|
|
93
|
-
|
|
94
|
-
output.stop();
|
|
95
|
-
success(`Database exported to ${exportPath}`);
|
|
96
|
-
} catch (err) {
|
|
97
|
-
output.stop();
|
|
98
|
-
error(`Failed to export database: ${err instanceof Error ? err.message : String(err)}`);
|
|
99
|
-
|
|
100
|
-
// If export times out, abort
|
|
101
|
-
if (err instanceof Error && err.message.includes("timed out")) {
|
|
102
|
-
error("Export timeout - deletion aborted");
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
console.error("");
|
|
107
|
-
info("Continue without exporting?");
|
|
108
|
-
const continueAction = await promptSelect(["Yes", "No"]);
|
|
89
|
+
// Ask about database export (only if database exists)
|
|
90
|
+
if (hasDatabase) {
|
|
91
|
+
console.error("");
|
|
92
|
+
info("Database will be deleted with the project");
|
|
109
93
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
94
|
+
console.error("");
|
|
95
|
+
info("Export database before deleting?");
|
|
96
|
+
const exportAction = await promptSelect(["Yes", "No"]);
|
|
97
|
+
|
|
98
|
+
if (exportAction === 0) {
|
|
99
|
+
const exportPath = join(process.cwd(), `${projectName}-backup.sql`);
|
|
100
|
+
output.start(`Exporting database to ${exportPath}...`);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const exportResult = await exportManagedDatabase(projectId);
|
|
104
|
+
|
|
105
|
+
// Download the SQL file
|
|
106
|
+
const response = await fetch(exportResult.download_url);
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(`Failed to download export: ${response.statusText}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const sqlContent = await response.text();
|
|
112
|
+
await writeFile(exportPath, sqlContent, "utf-8");
|
|
113
|
+
|
|
114
|
+
output.stop();
|
|
115
|
+
success(`Database exported to ${exportPath}`);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
output.stop();
|
|
118
|
+
error(`Failed to export database: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
|
|
120
|
+
// If export times out, abort
|
|
121
|
+
if (err instanceof Error && err.message.includes("timed out")) {
|
|
122
|
+
error("Export timeout - deletion aborted");
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.error("");
|
|
127
|
+
info("Continue without exporting?");
|
|
128
|
+
const continueAction = await promptSelect(["Yes", "No"]);
|
|
129
|
+
|
|
130
|
+
if (continueAction !== 0) {
|
|
131
|
+
info("Cancelled");
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
113
134
|
}
|
|
114
135
|
}
|
|
115
136
|
}
|
package/src/lib/progress.ts
CHANGED
|
@@ -38,6 +38,7 @@ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "
|
|
|
38
38
|
export function createProgressTracker(options: ProgressOptions) {
|
|
39
39
|
const { total, delayMs = 2000, barWidth = 25, label = "Uploading" } = options;
|
|
40
40
|
const startTime = Date.now();
|
|
41
|
+
const purple = getRandomPurple(); // Pick color once at start
|
|
41
42
|
let frame = 0;
|
|
42
43
|
let intervalId: Timer | null = null;
|
|
43
44
|
let current = 0;
|
|
@@ -55,7 +56,6 @@ export function createProgressTracker(options: ProgressOptions) {
|
|
|
55
56
|
);
|
|
56
57
|
} else {
|
|
57
58
|
// Show progress bar after delay
|
|
58
|
-
const purple = getRandomPurple();
|
|
59
59
|
const filled = Math.round((pct / 100) * barWidth);
|
|
60
60
|
const empty = barWidth - filled;
|
|
61
61
|
const bar = "▓".repeat(filled) + "░".repeat(empty);
|
|
@@ -69,7 +69,7 @@ export function createProgressTracker(options: ProgressOptions) {
|
|
|
69
69
|
return {
|
|
70
70
|
start() {
|
|
71
71
|
render();
|
|
72
|
-
intervalId = setInterval(render,
|
|
72
|
+
intervalId = setInterval(render, 150);
|
|
73
73
|
},
|
|
74
74
|
|
|
75
75
|
update(bytes: number) {
|
|
@@ -96,6 +96,7 @@ export function createProgressTracker(options: ProgressOptions) {
|
|
|
96
96
|
export function createUploadProgress(options: UploadProgressOptions) {
|
|
97
97
|
const { totalSize, delayMs = 2000, barWidth = 25, label = "Uploading" } = options;
|
|
98
98
|
const startTime = Date.now();
|
|
99
|
+
const purple = getRandomPurple(); // Pick color once at start
|
|
99
100
|
let frame = 0;
|
|
100
101
|
let pulsePos = 0;
|
|
101
102
|
let intervalId: Timer | null = null;
|
|
@@ -113,8 +114,6 @@ export function createUploadProgress(options: UploadProgressOptions) {
|
|
|
113
114
|
);
|
|
114
115
|
} else {
|
|
115
116
|
// Show pulsing bar after delay (indicates activity without false progress)
|
|
116
|
-
const purple = getRandomPurple();
|
|
117
|
-
|
|
118
117
|
// Create pulsing effect - a bright section that moves across the bar
|
|
119
118
|
const pulseWidth = 5;
|
|
120
119
|
pulsePos = (pulsePos + 1) % (barWidth + pulseWidth);
|
|
@@ -139,10 +138,82 @@ export function createUploadProgress(options: UploadProgressOptions) {
|
|
|
139
138
|
start() {
|
|
140
139
|
if (process.stderr.isTTY) {
|
|
141
140
|
render();
|
|
142
|
-
intervalId = setInterval(render,
|
|
141
|
+
intervalId = setInterval(render, 150);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
complete() {
|
|
146
|
+
if (intervalId) {
|
|
147
|
+
clearInterval(intervalId);
|
|
148
|
+
intervalId = null;
|
|
149
|
+
}
|
|
150
|
+
clearLine();
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface FileCountProgressOptions {
|
|
156
|
+
delayMs?: number;
|
|
157
|
+
barWidth?: number;
|
|
158
|
+
label?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Creates a file-count progress indicator for operations where we know
|
|
163
|
+
* the total file count and can track per-file progress.
|
|
164
|
+
*
|
|
165
|
+
* Shows spinner first, then after delay shows progress bar with file count.
|
|
166
|
+
*/
|
|
167
|
+
export function createFileCountProgress(options: FileCountProgressOptions = {}) {
|
|
168
|
+
const { delayMs = 2000, barWidth = 25, label = "Packaging" } = options;
|
|
169
|
+
const startTime = Date.now();
|
|
170
|
+
const purple = getRandomPurple(); // Pick color once at start
|
|
171
|
+
let frame = 0;
|
|
172
|
+
let intervalId: Timer | null = null;
|
|
173
|
+
let current = 0;
|
|
174
|
+
let total = 0;
|
|
175
|
+
|
|
176
|
+
function render() {
|
|
177
|
+
const elapsed = Date.now() - startTime;
|
|
178
|
+
|
|
179
|
+
clearLine();
|
|
180
|
+
|
|
181
|
+
if (elapsed < delayMs) {
|
|
182
|
+
// Just spinner for first N seconds
|
|
183
|
+
process.stderr.write(
|
|
184
|
+
`${colors.cyan}${frames[frame++ % frames.length]}${colors.reset} ${label}...`,
|
|
185
|
+
);
|
|
186
|
+
} else if (total > 0) {
|
|
187
|
+
// Show progress bar with file count after delay
|
|
188
|
+
const pct = Math.min(Math.round((current / total) * 100), 100);
|
|
189
|
+
const filled = Math.round((pct / 100) * barWidth);
|
|
190
|
+
const empty = barWidth - filled;
|
|
191
|
+
const bar = "▓".repeat(filled) + "░".repeat(empty);
|
|
192
|
+
|
|
193
|
+
process.stderr.write(
|
|
194
|
+
`${colors.cyan}${frames[frame++ % frames.length]}${colors.reset} ${label} ${purple}[${bar}]${colors.reset} ${colors.dim}(${current}/${total} files)${colors.reset}`,
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
// No total yet, just show spinner
|
|
198
|
+
process.stderr.write(
|
|
199
|
+
`${colors.cyan}${frames[frame++ % frames.length]}${colors.reset} ${label}...`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
start() {
|
|
206
|
+
if (process.stderr.isTTY) {
|
|
207
|
+
render();
|
|
208
|
+
intervalId = setInterval(render, 150);
|
|
143
209
|
}
|
|
144
210
|
},
|
|
145
211
|
|
|
212
|
+
update(currentFile: number, totalFiles: number) {
|
|
213
|
+
current = currentFile;
|
|
214
|
+
total = totalFiles;
|
|
215
|
+
},
|
|
216
|
+
|
|
146
217
|
complete() {
|
|
147
218
|
if (intervalId) {
|
|
148
219
|
clearInterval(intervalId);
|
package/src/lib/project-list.ts
CHANGED
|
@@ -338,9 +338,14 @@ export function formatErrorSection(
|
|
|
338
338
|
);
|
|
339
339
|
|
|
340
340
|
for (const item of items) {
|
|
341
|
-
|
|
341
|
+
// Don't show errorMessage inline - we'll show a summary hint below
|
|
342
|
+
lines.push(formatProjectLine(item, { indent: 4, tagColorMap, showUrl: false }));
|
|
342
343
|
}
|
|
343
344
|
|
|
345
|
+
// Add hint for resolving errors
|
|
346
|
+
lines.push("");
|
|
347
|
+
lines.push(` ${colors.dim}Run 'jack projects cleanup' to remove stale links${colors.reset}`);
|
|
348
|
+
|
|
344
349
|
return lines.join("\n");
|
|
345
350
|
}
|
|
346
351
|
|
|
@@ -363,7 +363,11 @@ async function runParallelSetup(
|
|
|
363
363
|
installSuccess: boolean;
|
|
364
364
|
remoteResult: ManagedCreateResult;
|
|
365
365
|
}> {
|
|
366
|
+
const setupStart = Date.now();
|
|
367
|
+
debug("Parallel setup started", { template: options.template, usePrebuilt: options.usePrebuilt });
|
|
368
|
+
|
|
366
369
|
// Start both operations
|
|
370
|
+
const installStart = Date.now();
|
|
367
371
|
const installPromise = (async () => {
|
|
368
372
|
const install = Bun.spawn(["bun", "install", "--prefer-offline"], {
|
|
369
373
|
cwd: targetDir,
|
|
@@ -371,15 +375,22 @@ async function runParallelSetup(
|
|
|
371
375
|
stderr: "ignore",
|
|
372
376
|
});
|
|
373
377
|
await install.exited;
|
|
378
|
+
const duration = ((Date.now() - installStart) / 1000).toFixed(1);
|
|
379
|
+
debug(`bun install completed in ${duration}s (exit: ${install.exitCode})`);
|
|
374
380
|
if (install.exitCode !== 0) {
|
|
375
381
|
throw new Error("Dependency installation failed");
|
|
376
382
|
}
|
|
377
383
|
return true;
|
|
378
384
|
})();
|
|
379
385
|
|
|
386
|
+
const remoteStart = Date.now();
|
|
380
387
|
const remotePromise = createManagedProjectRemote(projectName, undefined, {
|
|
381
388
|
template: options.template || "hello",
|
|
382
389
|
usePrebuilt: options.usePrebuilt ?? true,
|
|
390
|
+
}).then((result) => {
|
|
391
|
+
const duration = ((Date.now() - remoteStart) / 1000).toFixed(1);
|
|
392
|
+
debug(`Remote project created in ${duration}s (status: ${result.status})`);
|
|
393
|
+
return result;
|
|
383
394
|
});
|
|
384
395
|
|
|
385
396
|
// Report URL as soon as remote is ready (don't wait for install)
|
|
@@ -434,6 +445,9 @@ async function runParallelSetup(
|
|
|
434
445
|
throw new Error("Unexpected state: remote result not fulfilled");
|
|
435
446
|
}
|
|
436
447
|
|
|
448
|
+
const totalDuration = ((Date.now() - setupStart) / 1000).toFixed(1);
|
|
449
|
+
debug(`Parallel setup completed in ${totalDuration}s`);
|
|
450
|
+
|
|
437
451
|
return {
|
|
438
452
|
installSuccess: true,
|
|
439
453
|
remoteResult: remoteResult.value,
|
|
@@ -811,43 +825,19 @@ export async function createProject(
|
|
|
811
825
|
reporter.stop();
|
|
812
826
|
reporter.success("Name available");
|
|
813
827
|
} else {
|
|
814
|
-
// Slug not available - check if it's the user's own project
|
|
828
|
+
// Slug not available - check if it's the user's own project
|
|
815
829
|
const { checkAvailability } = await import("./project-resolver.ts");
|
|
816
830
|
const { existingProject } = await checkAvailability(projectName);
|
|
817
831
|
timings.push({ label: "Slug check", duration: timerEnd("slug-check") });
|
|
818
832
|
reporter.stop();
|
|
819
833
|
|
|
820
834
|
if (existingProject?.sources.controlPlane && !existingProject.sources.filesystem) {
|
|
821
|
-
//
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const choice = await promptSelect(["Link existing project", "Choose different name"]);
|
|
829
|
-
|
|
830
|
-
if (choice === 0) {
|
|
831
|
-
// User chose to link - proceed with project creation
|
|
832
|
-
reporter.success(`Linking to existing project: ${existingProject.url || projectName}`);
|
|
833
|
-
// Continue with project creation - user wants to link
|
|
834
|
-
} else {
|
|
835
|
-
// User chose different name
|
|
836
|
-
throw new JackError(
|
|
837
|
-
JackErrorCode.VALIDATION_ERROR,
|
|
838
|
-
`Project "${projectName}" already exists on jack cloud`,
|
|
839
|
-
`Try a different name: jack new ${projectName}-2`,
|
|
840
|
-
{ exitCode: 0, reported: true },
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
} else {
|
|
844
|
-
// Non-interactive mode - fail with clear message
|
|
845
|
-
throw new JackError(
|
|
846
|
-
JackErrorCode.VALIDATION_ERROR,
|
|
847
|
-
`Project "${projectName}" already exists on jack cloud`,
|
|
848
|
-
`Try a different name: jack new ${projectName}-2`,
|
|
849
|
-
);
|
|
850
|
-
}
|
|
835
|
+
// User's project exists on jack cloud but not locally - suggest clone
|
|
836
|
+
throw new JackError(
|
|
837
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
838
|
+
`Project "${projectName}" already exists on jack cloud`,
|
|
839
|
+
`To download it: jack clone ${projectName}`,
|
|
840
|
+
);
|
|
851
841
|
} else if (existingProject) {
|
|
852
842
|
// Project exists in registry with local path - it's truly taken by user
|
|
853
843
|
throw new JackError(
|
|
@@ -382,7 +382,7 @@ export async function listAllProjects(): Promise<ResolvedProject[]> {
|
|
|
382
382
|
!project.sources.controlPlane
|
|
383
383
|
) {
|
|
384
384
|
project.status = "error";
|
|
385
|
-
project.errorMessage = "Project not found in jack cloud
|
|
385
|
+
project.errorMessage = "Project not found in jack cloud";
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
} catch {
|
package/src/lib/zip-packager.ts
CHANGED
|
@@ -48,17 +48,23 @@ export interface ManifestData {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export interface ZipProgressCallback {
|
|
52
|
+
(current: number, total: number): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
/**
|
|
52
56
|
* Creates a ZIP archive from source directory
|
|
53
57
|
* @param outputPath - Absolute path for output ZIP file
|
|
54
58
|
* @param sourceDir - Absolute path to directory to archive
|
|
55
59
|
* @param files - Optional list of specific files to include (relative to sourceDir)
|
|
60
|
+
* @param onProgress - Optional callback for file-count progress updates
|
|
56
61
|
* @returns Promise that resolves when ZIP is created
|
|
57
62
|
*/
|
|
58
63
|
async function createZipArchive(
|
|
59
64
|
outputPath: string,
|
|
60
65
|
sourceDir: string,
|
|
61
66
|
files?: string[],
|
|
67
|
+
onProgress?: ZipProgressCallback,
|
|
62
68
|
): Promise<void> {
|
|
63
69
|
return new Promise((resolve, reject) => {
|
|
64
70
|
const output = createWriteStream(outputPath);
|
|
@@ -70,13 +76,16 @@ async function createZipArchive(
|
|
|
70
76
|
archive.pipe(output);
|
|
71
77
|
|
|
72
78
|
if (files) {
|
|
73
|
-
// Add specific files
|
|
74
|
-
|
|
79
|
+
// Add specific files with progress tracking
|
|
80
|
+
const total = files.length;
|
|
81
|
+
for (let i = 0; i < files.length; i++) {
|
|
82
|
+
const file = files[i];
|
|
75
83
|
const filePath = join(sourceDir, file);
|
|
76
84
|
archive.file(filePath, { name: file });
|
|
85
|
+
onProgress?.(i + 1, total);
|
|
77
86
|
}
|
|
78
87
|
} else {
|
|
79
|
-
// Add entire directory
|
|
88
|
+
// Add entire directory (no per-file progress available)
|
|
80
89
|
archive.directory(sourceDir, false);
|
|
81
90
|
}
|
|
82
91
|
|
|
@@ -219,18 +228,38 @@ function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindi
|
|
|
219
228
|
return Object.keys(bindings).length > 0 ? bindings : undefined;
|
|
220
229
|
}
|
|
221
230
|
|
|
231
|
+
export interface PackageOptions {
|
|
232
|
+
projectPath: string;
|
|
233
|
+
buildOutput: BuildOutput;
|
|
234
|
+
config?: WranglerConfig;
|
|
235
|
+
onProgress?: ZipProgressCallback;
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
/**
|
|
223
239
|
* Packages a built project for deployment to jack cloud
|
|
224
|
-
* @param
|
|
225
|
-
* @param buildOutput - Build output from buildProject()
|
|
226
|
-
* @param config - Optional wrangler config to extract binding intent
|
|
240
|
+
* @param options - Package options including paths, build output, config, and optional progress callback
|
|
227
241
|
* @returns Package result with ZIP paths and cleanup function
|
|
228
242
|
*/
|
|
243
|
+
export async function packageForDeploy(options: PackageOptions): Promise<ZipPackageResult>;
|
|
244
|
+
/**
|
|
245
|
+
* @deprecated Use options object instead
|
|
246
|
+
*/
|
|
229
247
|
export async function packageForDeploy(
|
|
230
248
|
projectPath: string,
|
|
231
249
|
buildOutput: BuildOutput,
|
|
232
250
|
config?: WranglerConfig,
|
|
251
|
+
): Promise<ZipPackageResult>;
|
|
252
|
+
export async function packageForDeploy(
|
|
253
|
+
optionsOrPath: PackageOptions | string,
|
|
254
|
+
buildOutputArg?: BuildOutput,
|
|
255
|
+
configArg?: WranglerConfig,
|
|
233
256
|
): Promise<ZipPackageResult> {
|
|
257
|
+
// Support both old and new signatures
|
|
258
|
+
const options: PackageOptions =
|
|
259
|
+
typeof optionsOrPath === "string"
|
|
260
|
+
? { projectPath: optionsOrPath, buildOutput: buildOutputArg!, config: configArg }
|
|
261
|
+
: optionsOrPath;
|
|
262
|
+
const { projectPath, buildOutput, config, onProgress } = options;
|
|
234
263
|
// Create temp directory for package artifacts
|
|
235
264
|
const packageId = `jack-package-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
236
265
|
const packageDir = join(tmpdir(), packageId);
|
|
@@ -247,7 +276,7 @@ export async function packageForDeploy(
|
|
|
247
276
|
// 2. Create source.zip from project files (filtered)
|
|
248
277
|
const projectFiles = await scanProjectFiles(projectPath);
|
|
249
278
|
const sourceFiles = projectFiles.map((f) => f.path);
|
|
250
|
-
await createZipArchive(sourceZipPath, projectPath, sourceFiles);
|
|
279
|
+
await createZipArchive(sourceZipPath, projectPath, sourceFiles, onProgress);
|
|
251
280
|
|
|
252
281
|
// 3. Create manifest.json
|
|
253
282
|
const manifest: ManifestData = {
|
package/src/templates/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { Template } from "./types";
|
|
|
10
10
|
// Resolve templates directory relative to this file (src/templates -> templates)
|
|
11
11
|
const TEMPLATES_DIR = join(dirname(dirname(import.meta.dir)), "templates");
|
|
12
12
|
|
|
13
|
-
export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs"];
|
|
13
|
+
export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs", "saas"];
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Resolved template with origin tracking for lineage
|
package/src/templates/types.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type HookAction =
|
|
|
11
11
|
message: string;
|
|
12
12
|
validate?: "json" | "accountAssociation";
|
|
13
13
|
required?: boolean;
|
|
14
|
+
secret?: boolean; // Mask input (for sensitive values like API keys)
|
|
14
15
|
successMessage?: string;
|
|
15
16
|
writeJson?: {
|
|
16
17
|
path: string;
|
|
@@ -31,9 +32,24 @@ export type HookAction =
|
|
|
31
32
|
key: string;
|
|
32
33
|
message?: string;
|
|
33
34
|
setupUrl?: string;
|
|
35
|
+
onMissing?: "fail" | "prompt" | "generate";
|
|
36
|
+
promptMessage?: string;
|
|
37
|
+
generateCommand?: string;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
action: "stripe-setup";
|
|
41
|
+
message?: string;
|
|
42
|
+
plans: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
priceKey: string; // Secret key to store price ID (e.g., "STRIPE_PRO_PRICE_ID")
|
|
45
|
+
amount: number; // in cents
|
|
46
|
+
interval: "month" | "year";
|
|
47
|
+
description?: string;
|
|
48
|
+
}>;
|
|
34
49
|
};
|
|
35
50
|
|
|
36
51
|
export interface TemplateHooks {
|
|
52
|
+
preCreate?: HookAction[];
|
|
37
53
|
preDeploy?: HookAction[];
|
|
38
54
|
postDeploy?: HookAction[];
|
|
39
55
|
}
|