@getjack/jack 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/package.json +1 -1
  2. package/src/commands/cd.ts +163 -0
  3. package/src/commands/clone.ts +112 -68
  4. package/src/commands/domain.ts +506 -0
  5. package/src/commands/domains.ts +215 -0
  6. package/src/commands/down.ts +18 -12
  7. package/src/commands/hack.ts +185 -8
  8. package/src/commands/init.ts +52 -1
  9. package/src/commands/link.ts +25 -43
  10. package/src/commands/logs.ts +2 -2
  11. package/src/commands/mcp.ts +74 -3
  12. package/src/commands/new.ts +48 -54
  13. package/src/commands/projects.ts +53 -10
  14. package/src/commands/secrets.ts +5 -1
  15. package/src/commands/services.ts +16 -4
  16. package/src/commands/shell-init.ts +43 -0
  17. package/src/commands/ship.ts +2 -11
  18. package/src/commands/skills.ts +335 -0
  19. package/src/commands/update.ts +31 -0
  20. package/src/commands/upgrade.ts +14 -0
  21. package/src/index.ts +116 -24
  22. package/src/lib/agent-integration.ts +1 -2
  23. package/src/lib/agents.ts +2 -2
  24. package/src/lib/auth/login-flow.ts +1 -1
  25. package/src/lib/clone-core.ts +252 -0
  26. package/src/lib/config.ts +22 -0
  27. package/src/lib/control-plane.ts +31 -5
  28. package/src/lib/fuzzy.ts +93 -0
  29. package/src/lib/managed-deploy.ts +4 -1
  30. package/src/lib/managed-down.ts +20 -5
  31. package/src/lib/output.ts +90 -9
  32. package/src/lib/picker.ts +406 -0
  33. package/src/lib/project-detection.ts +5 -2
  34. package/src/lib/project-list.ts +66 -5
  35. package/src/lib/project-operations.ts +68 -6
  36. package/src/lib/prompts.ts +1 -1
  37. package/src/lib/services/db-execute.ts +8 -1
  38. package/src/lib/services/db-list.ts +4 -1
  39. package/src/lib/services/domain-operations.ts +379 -0
  40. package/src/lib/services/storage-config.ts +1 -5
  41. package/src/lib/services/storage-delete.ts +1 -1
  42. package/src/lib/services/storage-info.ts +2 -4
  43. package/src/lib/services/vectorize-config.ts +1 -5
  44. package/src/lib/services/vectorize-create.ts +3 -1
  45. package/src/lib/shell-integration.ts +202 -0
  46. package/src/lib/telemetry-config.ts +50 -4
  47. package/src/lib/telemetry.ts +71 -2
  48. package/src/lib/version-check.ts +1 -3
  49. package/src/lib/wrangler-config.test.ts +2 -2
  50. package/src/lib/wrangler-config.ts +1 -1
  51. package/src/lib/zip-packager.ts +1 -3
  52. package/src/mcp/tools/index.ts +261 -7
  53. package/src/templates/index.ts +10 -1
  54. package/templates/ai-chat/.jack.json +1 -5
  55. package/templates/ai-chat/public/chat.js +130 -130
  56. package/templates/ai-chat/src/index.ts +9 -13
  57. package/templates/ai-chat/src/jack-ai.ts +6 -2
  58. package/templates/saas/.jack.json +6 -1
  59. package/templates/saas/src/auth.ts +8 -4
  60. package/templates/saas/src/client/App.tsx +22 -7
  61. package/templates/saas/src/client/components/ProtectedRoute.tsx +9 -2
  62. package/templates/saas/src/client/components/ThemeToggle.tsx +1 -6
  63. package/templates/saas/src/client/components/ui/accordion.tsx +1 -1
  64. package/templates/saas/src/client/components/ui/alert-dialog.tsx +2 -2
  65. package/templates/saas/src/client/components/ui/alert.tsx +2 -2
  66. package/templates/saas/src/client/components/ui/avatar.tsx +1 -1
  67. package/templates/saas/src/client/components/ui/badge.tsx +2 -2
  68. package/templates/saas/src/client/components/ui/breadcrumb.tsx +1 -1
  69. package/templates/saas/src/client/components/ui/button-group.tsx +2 -2
  70. package/templates/saas/src/client/components/ui/button.tsx +2 -2
  71. package/templates/saas/src/client/components/ui/card.tsx +1 -1
  72. package/templates/saas/src/client/components/ui/carousel.tsx +2 -2
  73. package/templates/saas/src/client/components/ui/checkbox.tsx +1 -1
  74. package/templates/saas/src/client/components/ui/command.tsx +2 -2
  75. package/templates/saas/src/client/components/ui/context-menu.tsx +1 -1
  76. package/templates/saas/src/client/components/ui/dialog.tsx +1 -1
  77. package/templates/saas/src/client/components/ui/drawer.tsx +1 -1
  78. package/templates/saas/src/client/components/ui/dropdown-menu.tsx +1 -1
  79. package/templates/saas/src/client/components/ui/empty.tsx +1 -1
  80. package/templates/saas/src/client/components/ui/field.tsx +2 -2
  81. package/templates/saas/src/client/components/ui/form.tsx +5 -5
  82. package/templates/saas/src/client/components/ui/hover-card.tsx +1 -1
  83. package/templates/saas/src/client/components/ui/input-group.tsx +3 -3
  84. package/templates/saas/src/client/components/ui/input-otp.tsx +1 -1
  85. package/templates/saas/src/client/components/ui/input.tsx +1 -1
  86. package/templates/saas/src/client/components/ui/item.tsx +3 -3
  87. package/templates/saas/src/client/components/ui/label.tsx +1 -1
  88. package/templates/saas/src/client/components/ui/menubar.tsx +1 -1
  89. package/templates/saas/src/client/components/ui/navigation-menu.tsx +1 -1
  90. package/templates/saas/src/client/components/ui/pagination.tsx +2 -2
  91. package/templates/saas/src/client/components/ui/popover.tsx +1 -1
  92. package/templates/saas/src/client/components/ui/progress.tsx +1 -1
  93. package/templates/saas/src/client/components/ui/radio-group.tsx +1 -1
  94. package/templates/saas/src/client/components/ui/resizable.tsx +1 -1
  95. package/templates/saas/src/client/components/ui/scroll-area.tsx +1 -1
  96. package/templates/saas/src/client/components/ui/select.tsx +1 -1
  97. package/templates/saas/src/client/components/ui/separator.tsx +1 -1
  98. package/templates/saas/src/client/components/ui/sheet.tsx +1 -1
  99. package/templates/saas/src/client/components/ui/sidebar.tsx +4 -4
  100. package/templates/saas/src/client/components/ui/slider.tsx +1 -1
  101. package/templates/saas/src/client/components/ui/switch.tsx +1 -1
  102. package/templates/saas/src/client/components/ui/table.tsx +1 -1
  103. package/templates/saas/src/client/components/ui/tabs.tsx +1 -1
  104. package/templates/saas/src/client/components/ui/textarea.tsx +1 -1
  105. package/templates/saas/src/client/components/ui/toggle-group.tsx +3 -3
  106. package/templates/saas/src/client/components/ui/toggle.tsx +2 -2
  107. package/templates/saas/src/client/components/ui/tooltip.tsx +1 -1
  108. package/templates/saas/src/client/hooks/useSubscription.ts +5 -4
  109. package/templates/saas/src/client/lib/auth-client.ts +1 -1
  110. package/templates/saas/src/client/lib/plans.ts +1 -6
  111. package/templates/saas/src/client/lib/utils.ts +1 -1
  112. package/templates/saas/src/client/main.tsx +1 -1
  113. package/templates/saas/src/client/pages/DashboardPage.tsx +41 -9
  114. package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +11 -2
  115. package/templates/saas/src/client/pages/HomePage.tsx +11 -2
  116. package/templates/saas/src/client/pages/LoginPage.tsx +11 -2
  117. package/templates/saas/src/client/pages/PricingPage.tsx +20 -10
  118. package/templates/saas/src/client/pages/ResetPasswordPage.tsx +14 -11
  119. package/templates/saas/src/client/pages/SignupPage.tsx +11 -2
  120. package/templates/saas/src/index.ts +28 -19
  121. package/templates/saas/vite.config.ts +1 -1
  122. package/templates/semantic-search/.jack.json +1 -5
  123. package/templates/semantic-search/src/index.ts +8 -4
  124. package/templates/semantic-search/src/jack-ai.ts +6 -2
  125. package/templates/semantic-search/src/jack-vectorize.ts +5 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
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",
@@ -0,0 +1,163 @@
1
+ /**
2
+ * cd command - print project path for shell integration
3
+ *
4
+ * Usage: jack cd <name>
5
+ *
6
+ * Outputs ONLY the absolute path to stdout (no other output).
7
+ * Error messages go to stderr.
8
+ *
9
+ * Exit codes:
10
+ * - 0: Success (path printed)
11
+ * - 1: No match, error, or project cannot be resolved
12
+ */
13
+
14
+ import { join } from "node:path";
15
+ import { CloneCollisionError, ProjectNotFoundError, cloneProject } from "../lib/clone-core.ts";
16
+ import { getJackHome } from "../lib/config.ts";
17
+ import { fuzzyFilter } from "../lib/fuzzy.ts";
18
+ import { error } from "../lib/output.ts";
19
+ import { registerPath } from "../lib/paths-index.ts";
20
+ import { linkProject } from "../lib/project-link.ts";
21
+ import { type ResolvedProject, listAllProjects } from "../lib/project-resolver.ts";
22
+ import {
23
+ detectShell,
24
+ getRcFilePath,
25
+ isInstalled as isShellIntegrationInstalled,
26
+ } from "../lib/shell-integration.ts";
27
+
28
+ function maybeShowShellHint(path: string): void {
29
+ const shell = detectShell();
30
+ const rcFile = getRcFilePath(shell);
31
+
32
+ if (!rcFile || shell === "unknown") return;
33
+ if (isShellIntegrationInstalled(rcFile)) return;
34
+ console.error(`cd ${path}`);
35
+ }
36
+
37
+ /**
38
+ * Find the best matching project by name using fuzzy matching.
39
+ * Tiebreaker: most recently deployed wins (updatedAt descending).
40
+ *
41
+ * @param query - The search query
42
+ * @param projects - All resolved projects
43
+ * @returns The best matching project or null
44
+ */
45
+ function findBestMatch(query: string, projects: ResolvedProject[]): ResolvedProject | null {
46
+ // Sort by updatedAt descending before fuzzy filter so tiebreaker favors recent
47
+ const sorted = [...projects].sort((a, b) => {
48
+ const aTime = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
49
+ const bTime = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
50
+ return bTime - aTime;
51
+ });
52
+
53
+ // Fuzzy filter preserves sort order for items with equal scores
54
+ const matches = fuzzyFilter(query, sorted, (p) => p.name);
55
+
56
+ return matches[0] ?? null;
57
+ }
58
+
59
+ export default async function cd(projectName?: string): Promise<void> {
60
+ // Validate project name
61
+ if (!projectName) {
62
+ // No match: exit 1, no message (shell wrapper handles it)
63
+ process.exit(1);
64
+ }
65
+
66
+ let projects: ResolvedProject[];
67
+
68
+ // Fetch all projects (local + cloud)
69
+ try {
70
+ projects = await listAllProjects();
71
+ } catch (err) {
72
+ // Network timeout
73
+ error("Could not reach cloud. Check your connection.");
74
+ if (process.env.DEBUG || process.argv.includes("--debug")) {
75
+ console.error("Debug:", err);
76
+ }
77
+ process.exit(1);
78
+ }
79
+
80
+ // Find best match using fuzzy matching
81
+ const match = findBestMatch(projectName, projects);
82
+
83
+ if (!match) {
84
+ // No match: exit 1, no message
85
+ process.exit(1);
86
+ }
87
+
88
+ // Check if project has local path
89
+ if (match.localPath) {
90
+ // Local copy exists - print path and exit
91
+ console.log(match.localPath);
92
+ maybeShowShellHint(match.localPath);
93
+ process.exit(0);
94
+ }
95
+
96
+ // Cloud-only project: check deploy mode
97
+ if (match.deployMode === "managed") {
98
+ // Managed cloud-only: auto-clone to JACK_HOME
99
+ const jackHome = getJackHome();
100
+ const targetDir = join(jackHome, match.slug || match.name);
101
+
102
+ try {
103
+ const result = await cloneProject(match.slug || match.name, targetDir, {
104
+ silent: true,
105
+ skipPrompts: true,
106
+ });
107
+
108
+ // Link and register the cloned project
109
+ if (result.projectId) {
110
+ await linkProject(result.path, result.projectId, "managed");
111
+ await registerPath(result.projectId, result.path);
112
+ }
113
+
114
+ // Print the cloned path
115
+ console.log(result.path);
116
+ maybeShowShellHint(result.path);
117
+ process.exit(0);
118
+ } catch (err) {
119
+ if (err instanceof ProjectNotFoundError) {
120
+ // Project deleted from cloud
121
+ error(`Project '${match.name}' no longer exists in cloud.`);
122
+ process.exit(1);
123
+ }
124
+
125
+ if (err instanceof CloneCollisionError) {
126
+ // Directory exists but not linked - help user decide what to do
127
+ error("Directory exists but isn't linked to this project");
128
+ console.error("");
129
+ console.error(` cd ${err.targetDir}`);
130
+ console.error("");
131
+ console.error(" jack link keep local files, connect to cloud");
132
+ console.error(" jack clone replace local with cloud version");
133
+ process.exit(1);
134
+ }
135
+
136
+ // Check for "no source backup" error (orphan project)
137
+ const errMsg = err instanceof Error ? err.message : "";
138
+ if (errMsg.includes("source not found")) {
139
+ error(`'${match.name}' has no cloud backup.`);
140
+ console.error(" This project was created before source backup was enabled.");
141
+ console.error(` Remove it with: jack down ${match.name}`);
142
+ process.exit(1);
143
+ }
144
+
145
+ // Other clone errors (network, etc.)
146
+ error("Could not reach cloud. Check your connection.");
147
+ if (process.env.DEBUG || process.argv.includes("--debug")) {
148
+ console.error("Debug:", err);
149
+ }
150
+ process.exit(1);
151
+ }
152
+ }
153
+
154
+ // BYO cloud-only: error with guidance
155
+ if (match.deployMode === "byo") {
156
+ error(`'${match.name}' is a BYO project with no local copy.`);
157
+ console.error(" Clone it manually or check your backup.");
158
+ process.exit(1);
159
+ }
160
+
161
+ // Unknown state (should not happen)
162
+ process.exit(1);
163
+ }
@@ -1,41 +1,51 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import { CloneCollisionError, type CloneReporter, cloneProject } from "../lib/clone-core.ts";
4
+ import { downloadProjectSource } from "../lib/control-plane.ts";
5
+ import { extractZipToDirectory } from "../lib/zip-utils.ts";
3
6
  import { isCancel, promptSelectValue } from "../lib/hooks.ts";
4
- import { downloadProjectSource, fetchProjectTags } from "../lib/control-plane.ts";
5
- import { formatSize } from "../lib/format.ts";
6
7
  import { box, error, info, spinner, success } from "../lib/output.ts";
7
- import { registerPath } from "../lib/paths-index.ts";
8
- import { linkProject, updateProjectLink } from "../lib/project-link.ts";
8
+ import { readProjectLink } from "../lib/project-link.ts";
9
9
  import { resolveProject } from "../lib/project-resolver.ts";
10
- import { cloneFromCloud, getRemoteManifest } from "../lib/storage/index.ts";
11
- import { extractZipToDirectory } from "../lib/zip-utils.ts";
12
10
 
13
11
  export interface CloneFlags {
14
12
  as?: string;
13
+ force?: boolean;
15
14
  }
16
15
 
17
16
  export default async function clone(projectName?: string, flags: CloneFlags = {}): Promise<void> {
17
+ // Check if we're in a linked directory and should pull/update
18
+ const currentLink = await readProjectLink(process.cwd());
19
+
20
+ if (!projectName && currentLink?.deploy_mode === "managed") {
21
+ // No project name, but in a managed linked directory - pull latest code
22
+ await pullCurrentProject(currentLink.project_id, flags);
23
+ return;
24
+ }
25
+
18
26
  // Validate project name
19
27
  if (!projectName) {
20
28
  error("Project name required");
21
29
  info("Usage: jack clone <project> [--as <directory>]");
30
+ info("Or run in a linked directory to pull latest code.");
22
31
  process.exit(1);
23
32
  }
24
33
 
25
34
  // Determine target directory
26
35
  const targetDir = resolve(flags.as ?? projectName);
36
+ const displayName = flags.as ?? projectName;
27
37
 
28
- // Check if target directory exists
38
+ // Check if target directory exists and handle collision
29
39
  if (existsSync(targetDir)) {
30
40
  // If not TTY, error immediately
31
41
  if (!process.stdout.isTTY) {
32
- error(`Directory ${flags.as ?? projectName} already exists`);
42
+ error(`Directory ${displayName} already exists`);
33
43
  process.exit(1);
34
44
  }
35
45
 
36
46
  // Prompt user for action
37
47
  const action = await promptSelectValue(
38
- `Directory ${flags.as ?? projectName} already exists. What would you like to do?`,
48
+ `Directory ${displayName} already exists. What would you like to do?`,
39
49
  [
40
50
  { value: "overwrite", label: "Overwrite (delete and recreate)" },
41
51
  { value: "merge", label: "Merge (keep existing files)" },
@@ -52,78 +62,112 @@ export default async function clone(projectName?: string, flags: CloneFlags = {}
52
62
  // Delete directory
53
63
  await Bun.$`rm -rf ${targetDir}`.quiet();
54
64
  }
65
+ // For "merge", we continue and let files be overwritten/added
55
66
  }
56
67
 
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;
68
+ // Create reporter for progress output
69
+ let currentSpinner: ReturnType<typeof spinner> | null = null;
70
+
71
+ const reporter: CloneReporter = {
72
+ onLookup: (name) => {
73
+ currentSpinner = spinner(`Looking up ${name}...`);
74
+ },
75
+ onLookupComplete: (found, isManaged) => {
76
+ if (found && isManaged) {
77
+ currentSpinner?.success("Found on jack cloud");
78
+ } else {
79
+ currentSpinner?.stop();
80
+ }
81
+ },
82
+ onDownloadStart: (source, details) => {
83
+ if (source === "cloud") {
84
+ currentSpinner = spinner("Downloading from jack cloud...");
85
+ } else {
86
+ // BYO mode - show file count info first, then start download spinner
87
+ if (details) {
88
+ success(`Found ${details}`);
89
+ }
90
+ currentSpinner = spinner("Downloading...");
91
+ }
92
+ },
93
+ onDownloadComplete: (fileCount, displayPath) => {
94
+ currentSpinner?.success(`Restored ${fileCount} file(s) to ${displayPath}`);
95
+ },
96
+ onDownloadError: (err) => {
97
+ currentSpinner?.error("Download failed");
98
+ error(err);
99
+ },
100
+ onTagsRestored: (count) => {
101
+ info(`Restored ${count} tag(s)`);
102
+ },
103
+ };
60
104
 
61
105
  try {
62
- project = await resolveProject(projectName);
63
- } catch {
64
- // Not found on control-plane, will fall back to User R2
65
- }
66
-
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);
106
+ await cloneProject(projectName, targetDir, { silent: false, skipPrompts: false }, reporter);
107
+
108
+ // Show next steps
109
+ box("Next steps:", [`cd ${displayName}`, "bun install", "jack ship"]);
110
+ } catch (err) {
111
+ if (err instanceof CloneCollisionError) {
112
+ // This shouldn't happen since we handle collision above, but just in case
113
+ error(err.message);
80
114
  process.exit(1);
81
115
  }
82
116
 
83
- // Link to control-plane
84
- await linkProject(targetDir, project.remote.projectId, "managed");
85
- await registerPath(project.remote.projectId, targetDir);
86
-
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)`);
117
+ // For ProjectNotFoundError and other errors
118
+ if (err instanceof Error) {
119
+ // Check if it's a "not found" error for BYO projects
120
+ if (err.message.includes("For BYO projects")) {
121
+ error(`Project not found: ${projectName}`);
122
+ info("For BYO projects, run 'jack sync' first to backup your project.");
123
+ } else {
124
+ error(err.message);
93
125
  }
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);
126
+ } else {
127
+ error("Clone failed");
107
128
  }
129
+ process.exit(1);
130
+ }
131
+ }
108
132
 
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)})`);
133
+ /**
134
+ * Pull latest code from cloud into current linked directory
135
+ */
136
+ async function pullCurrentProject(projectId: string, flags: CloneFlags): Promise<void> {
137
+ // Resolve project to get slug/name
138
+ const project = await resolveProject(projectId);
139
+ const projectName = project?.slug || project?.name || projectId;
112
140
 
113
- // Download files
114
- const downloadSpin = spinner("Downloading...");
115
- const result = await cloneFromCloud(projectName, targetDir);
141
+ if (!project) {
142
+ error("Could not resolve project from control plane");
143
+ process.exit(1);
144
+ }
116
145
 
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);
122
- }
146
+ // Confirm before overwriting (unless --force)
147
+ if (!flags.force && process.stdout.isTTY) {
148
+ const action = await promptSelectValue(`Pull latest code for ${projectName}?`, [
149
+ { value: "yes", label: "Yes, overwrite local files" },
150
+ { value: "cancel", label: "Cancel" },
151
+ ]);
123
152
 
124
- downloadSpin.success(`Restored to ./${flags.as ?? projectName}/`);
153
+ if (isCancel(action) || action === "cancel") {
154
+ info("Cancelled");
155
+ process.exit(0);
156
+ }
125
157
  }
126
158
 
127
- // Show next steps
128
- box("Next steps:", [`cd ${flags.as ?? projectName}`, "bun install", "jack ship"]);
159
+ const currentSpinner = spinner("Downloading from jack cloud...");
160
+
161
+ try {
162
+ const sourceZip = await downloadProjectSource(projectName);
163
+ const fileCount = await extractZipToDirectory(sourceZip, process.cwd());
164
+ currentSpinner.success(`Updated ${fileCount} file(s)`);
165
+ success(`Pulled latest code for ${projectName}`);
166
+ } catch (err) {
167
+ currentSpinner.error("Download failed");
168
+ if (err instanceof Error) {
169
+ error(err.message);
170
+ }
171
+ process.exit(1);
172
+ }
129
173
  }