@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.
Files changed (105) hide show
  1. package/package.json +5 -2
  2. package/src/commands/down.ts +11 -1
  3. package/src/commands/init.ts +19 -6
  4. package/src/commands/new.ts +56 -4
  5. package/src/commands/publish.ts +1 -1
  6. package/src/lib/agents.ts +3 -1
  7. package/src/lib/auth/ensure-auth.test.ts +3 -3
  8. package/src/lib/control-plane.ts +15 -1
  9. package/src/lib/deploy-upload.ts +26 -1
  10. package/src/lib/hooks.ts +232 -1
  11. package/src/lib/managed-deploy.ts +13 -6
  12. package/src/lib/managed-down.ts +66 -45
  13. package/src/lib/progress.ts +76 -5
  14. package/src/lib/project-list.ts +6 -1
  15. package/src/lib/project-operations.ts +21 -31
  16. package/src/lib/project-resolver.ts +1 -1
  17. package/src/lib/zip-packager.ts +36 -7
  18. package/src/templates/index.ts +1 -1
  19. package/src/templates/types.ts +16 -0
  20. package/templates/CLAUDE.md +172 -5
  21. package/templates/miniapp/.jack.json +1 -3
  22. package/templates/saas/.jack.json +154 -0
  23. package/templates/saas/AGENTS.md +333 -0
  24. package/templates/saas/bun.lock +925 -0
  25. package/templates/saas/components.json +21 -0
  26. package/templates/saas/index.html +12 -0
  27. package/templates/saas/package.json +75 -0
  28. package/templates/saas/public/icon.png +0 -0
  29. package/templates/saas/public/og.png +0 -0
  30. package/templates/saas/schema.sql +73 -0
  31. package/templates/saas/src/auth.ts +77 -0
  32. package/templates/saas/src/client/App.tsx +63 -0
  33. package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
  34. package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
  35. package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
  36. package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
  37. package/templates/saas/src/client/components/ui/alert.tsx +60 -0
  38. package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
  39. package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
  40. package/templates/saas/src/client/components/ui/badge.tsx +39 -0
  41. package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
  42. package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
  43. package/templates/saas/src/client/components/ui/button.tsx +60 -0
  44. package/templates/saas/src/client/components/ui/card.tsx +75 -0
  45. package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
  46. package/templates/saas/src/client/components/ui/chart.tsx +326 -0
  47. package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
  48. package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
  49. package/templates/saas/src/client/components/ui/command.tsx +159 -0
  50. package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
  51. package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
  52. package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
  53. package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
  54. package/templates/saas/src/client/components/ui/empty.tsx +94 -0
  55. package/templates/saas/src/client/components/ui/field.tsx +232 -0
  56. package/templates/saas/src/client/components/ui/form.tsx +152 -0
  57. package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
  58. package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
  59. package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
  60. package/templates/saas/src/client/components/ui/input.tsx +21 -0
  61. package/templates/saas/src/client/components/ui/item.tsx +172 -0
  62. package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
  63. package/templates/saas/src/client/components/ui/label.tsx +21 -0
  64. package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
  65. package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
  66. package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
  67. package/templates/saas/src/client/components/ui/popover.tsx +42 -0
  68. package/templates/saas/src/client/components/ui/progress.tsx +26 -0
  69. package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
  70. package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
  71. package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
  72. package/templates/saas/src/client/components/ui/select.tsx +173 -0
  73. package/templates/saas/src/client/components/ui/separator.tsx +28 -0
  74. package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
  75. package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
  76. package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
  77. package/templates/saas/src/client/components/ui/slider.tsx +58 -0
  78. package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
  79. package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
  80. package/templates/saas/src/client/components/ui/switch.tsx +28 -0
  81. package/templates/saas/src/client/components/ui/table.tsx +90 -0
  82. package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
  83. package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
  84. package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
  85. package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
  86. package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
  87. package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
  88. package/templates/saas/src/client/hooks/useAuth.ts +14 -0
  89. package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
  90. package/templates/saas/src/client/index.css +165 -0
  91. package/templates/saas/src/client/lib/auth-client.ts +7 -0
  92. package/templates/saas/src/client/lib/plans.ts +82 -0
  93. package/templates/saas/src/client/lib/utils.ts +6 -0
  94. package/templates/saas/src/client/main.tsx +15 -0
  95. package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
  96. package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
  97. package/templates/saas/src/client/pages/HomePage.tsx +285 -0
  98. package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
  99. package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
  100. package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
  101. package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
  102. package/templates/saas/src/index.ts +208 -0
  103. package/templates/saas/tsconfig.json +18 -0
  104. package/templates/saas/vite.config.ts +14 -0
  105. package/templates/saas/wrangler.jsonc +20 -0
@@ -5,7 +5,11 @@
5
5
 
6
6
  import { writeFile } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
- import { deleteManagedProject, exportManagedDatabase } from "./control-plane.ts";
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
- item("Database: managed D1");
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
- console.error("");
72
- info("Database will be deleted with the project");
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
- if (continueAction !== 0) {
111
- info("Cancelled");
112
- return false;
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
  }
@@ -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, 80);
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, 80);
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);
@@ -338,9 +338,14 @@ export function formatErrorSection(
338
338
  );
339
339
 
340
340
  for (const item of items) {
341
- lines.push(formatProjectLine(item, { indent: 4, tagColorMap }));
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 (for linking flow)
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
- // It's the user's project on jack cloud but not locally - offer to link
822
- if (interactive) {
823
- const { promptSelect } = await import("./hooks.ts");
824
- console.error("");
825
- console.error(` Project "${projectName}" exists on jack cloud but not locally.`);
826
- console.error("");
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. Run: jack unlink && jack ship";
385
+ project.errorMessage = "Project not found in jack cloud";
386
386
  }
387
387
  }
388
388
  } catch {
@@ -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
- for (const file of files) {
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 projectPath - Absolute path to project directory
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 = {
@@ -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
@@ -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
  }