@getjack/jack 0.1.2 → 0.1.3

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 (91) hide show
  1. package/package.json +54 -47
  2. package/src/commands/agents.ts +145 -10
  3. package/src/commands/down.ts +110 -102
  4. package/src/commands/feedback.ts +189 -0
  5. package/src/commands/init.ts +8 -12
  6. package/src/commands/login.ts +88 -0
  7. package/src/commands/logout.ts +14 -0
  8. package/src/commands/logs.ts +21 -0
  9. package/src/commands/mcp.ts +134 -7
  10. package/src/commands/new.ts +43 -17
  11. package/src/commands/open.ts +13 -6
  12. package/src/commands/projects.ts +269 -143
  13. package/src/commands/secrets.ts +413 -0
  14. package/src/commands/services.ts +96 -123
  15. package/src/commands/ship.ts +5 -1
  16. package/src/commands/whoami.ts +31 -0
  17. package/src/index.ts +218 -144
  18. package/src/lib/agent-files.ts +34 -0
  19. package/src/lib/agents.ts +390 -22
  20. package/src/lib/asset-hash.ts +50 -0
  21. package/src/lib/auth/client.ts +115 -0
  22. package/src/lib/auth/constants.ts +5 -0
  23. package/src/lib/auth/guard.ts +57 -0
  24. package/src/lib/auth/index.ts +18 -0
  25. package/src/lib/auth/store.ts +54 -0
  26. package/src/lib/binding-validator.ts +136 -0
  27. package/src/lib/build-helper.ts +211 -0
  28. package/src/lib/cloudflare-api.ts +24 -0
  29. package/src/lib/config.ts +5 -6
  30. package/src/lib/control-plane.ts +295 -0
  31. package/src/lib/debug.ts +3 -1
  32. package/src/lib/deploy-mode.ts +93 -0
  33. package/src/lib/deploy-upload.ts +92 -0
  34. package/src/lib/errors.ts +2 -0
  35. package/src/lib/github.ts +31 -1
  36. package/src/lib/hooks.ts +4 -12
  37. package/src/lib/intent.ts +88 -0
  38. package/src/lib/jsonc.ts +125 -0
  39. package/src/lib/local-paths.test.ts +902 -0
  40. package/src/lib/local-paths.ts +258 -0
  41. package/src/lib/managed-deploy.ts +175 -0
  42. package/src/lib/managed-down.ts +159 -0
  43. package/src/lib/mcp-config.ts +55 -34
  44. package/src/lib/names.ts +9 -29
  45. package/src/lib/project-operations.ts +676 -249
  46. package/src/lib/project-resolver.ts +476 -0
  47. package/src/lib/registry.ts +76 -37
  48. package/src/lib/resources.ts +196 -0
  49. package/src/lib/schema.ts +30 -1
  50. package/src/lib/storage/file-filter.ts +1 -0
  51. package/src/lib/storage/index.ts +5 -1
  52. package/src/lib/telemetry.ts +14 -0
  53. package/src/lib/tty.ts +15 -0
  54. package/src/lib/zip-packager.ts +255 -0
  55. package/src/mcp/resources/index.ts +8 -2
  56. package/src/mcp/server.ts +32 -4
  57. package/src/mcp/tools/index.ts +35 -13
  58. package/src/mcp/types.ts +6 -0
  59. package/src/mcp/utils.ts +1 -1
  60. package/src/templates/index.ts +42 -4
  61. package/src/templates/types.ts +13 -0
  62. package/templates/CLAUDE.md +166 -0
  63. package/templates/api/.jack.json +4 -0
  64. package/templates/api/bun.lock +1 -0
  65. package/templates/api/wrangler.jsonc +5 -0
  66. package/templates/hello/.jack.json +28 -0
  67. package/templates/hello/package.json +10 -0
  68. package/templates/hello/src/index.ts +11 -0
  69. package/templates/hello/tsconfig.json +11 -0
  70. package/templates/hello/wrangler.jsonc +5 -0
  71. package/templates/miniapp/.jack.json +15 -4
  72. package/templates/miniapp/bun.lock +135 -40
  73. package/templates/miniapp/index.html +1 -0
  74. package/templates/miniapp/package.json +3 -1
  75. package/templates/miniapp/public/.well-known/farcaster.json +7 -5
  76. package/templates/miniapp/public/icon.png +0 -0
  77. package/templates/miniapp/public/og.png +0 -0
  78. package/templates/miniapp/schema.sql +8 -0
  79. package/templates/miniapp/src/App.tsx +254 -3
  80. package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
  81. package/templates/miniapp/src/hooks/useAI.ts +35 -0
  82. package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
  83. package/templates/miniapp/src/hooks/useShare.ts +76 -0
  84. package/templates/miniapp/src/index.css +15 -0
  85. package/templates/miniapp/src/lib/api.ts +2 -1
  86. package/templates/miniapp/src/worker.ts +515 -1
  87. package/templates/miniapp/wrangler.jsonc +15 -3
  88. package/LICENSE +0 -190
  89. package/README.md +0 -55
  90. package/src/commands/cloud.ts +0 -230
  91. package/templates/api/wrangler.toml +0 -3
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Local Paths Index
3
+ *
4
+ * Tracks where projects live on the local filesystem.
5
+ * This is a cache - can be rebuilt by scanning directories.
6
+ *
7
+ * Design:
8
+ * - One project can have multiple local paths (forks, copies)
9
+ * - Paths are verified on read (deleted dirs are pruned)
10
+ * - Auto-registered when jack commands run from project dirs
11
+ */
12
+
13
+ import { existsSync } from "node:fs";
14
+ import { readdir } from "node:fs/promises";
15
+ import { join, resolve } from "node:path";
16
+ import { CONFIG_DIR } from "./config.ts";
17
+
18
+ /**
19
+ * Local paths index structure
20
+ */
21
+ export interface LocalPathsIndex {
22
+ version: 1;
23
+ /** Map of project name -> array of local paths */
24
+ paths: Record<string, string[]>;
25
+ /** Last time the index was updated */
26
+ updatedAt: string;
27
+ }
28
+
29
+ const INDEX_PATH = join(CONFIG_DIR, "local-paths.json");
30
+
31
+ /** Directories to skip when scanning */
32
+ const SKIP_DIRS = new Set([
33
+ "node_modules",
34
+ ".git",
35
+ "dist",
36
+ "build",
37
+ ".next",
38
+ ".nuxt",
39
+ ".output",
40
+ "coverage",
41
+ ".turbo",
42
+ ".cache",
43
+ ]);
44
+
45
+ /**
46
+ * Check if a directory has a wrangler config file
47
+ */
48
+ function hasWranglerConfig(dir: string): boolean {
49
+ return (
50
+ existsSync(join(dir, "wrangler.jsonc")) ||
51
+ existsSync(join(dir, "wrangler.toml")) ||
52
+ existsSync(join(dir, "wrangler.json"))
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Read the local paths index from disk
58
+ */
59
+ export async function readLocalPaths(): Promise<LocalPathsIndex> {
60
+ if (!existsSync(INDEX_PATH)) {
61
+ return { version: 1, paths: {}, updatedAt: new Date().toISOString() };
62
+ }
63
+
64
+ try {
65
+ return await Bun.file(INDEX_PATH).json();
66
+ } catch {
67
+ // Handle corrupted index file gracefully
68
+ return { version: 1, paths: {}, updatedAt: new Date().toISOString() };
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Write the local paths index to disk
74
+ */
75
+ export async function writeLocalPaths(index: LocalPathsIndex): Promise<void> {
76
+ index.updatedAt = new Date().toISOString();
77
+ await Bun.write(INDEX_PATH, JSON.stringify(index, null, 2));
78
+ }
79
+
80
+ /**
81
+ * Register a local path for a project
82
+ * Idempotent - won't add duplicates
83
+ */
84
+ export async function registerLocalPath(projectName: string, localPath: string): Promise<void> {
85
+ const absolutePath = resolve(localPath);
86
+ const index = await readLocalPaths();
87
+
88
+ if (!index.paths[projectName]) {
89
+ index.paths[projectName] = [];
90
+ }
91
+
92
+ // Avoid duplicates
93
+ if (!index.paths[projectName].includes(absolutePath)) {
94
+ index.paths[projectName].push(absolutePath);
95
+ }
96
+
97
+ await writeLocalPaths(index);
98
+ }
99
+
100
+ /**
101
+ * Remove a local path for a project
102
+ */
103
+ export async function removeLocalPath(projectName: string, localPath: string): Promise<void> {
104
+ const absolutePath = resolve(localPath);
105
+ const index = await readLocalPaths();
106
+
107
+ if (index.paths[projectName]) {
108
+ index.paths[projectName] = index.paths[projectName].filter((p) => p !== absolutePath);
109
+
110
+ // Clean up empty arrays
111
+ if (index.paths[projectName].length === 0) {
112
+ delete index.paths[projectName];
113
+ }
114
+ }
115
+
116
+ await writeLocalPaths(index);
117
+ }
118
+
119
+ /**
120
+ * Get all local paths for a project, verified to exist
121
+ * Automatically prunes paths that no longer exist or lack wrangler config
122
+ */
123
+ export async function getLocalPaths(projectName: string): Promise<string[]> {
124
+ const index = await readLocalPaths();
125
+ const paths = index.paths[projectName] || [];
126
+
127
+ // Verify paths exist and have wrangler config
128
+ const validPaths: string[] = [];
129
+ const invalidPaths: string[] = [];
130
+
131
+ for (const path of paths) {
132
+ if (hasWranglerConfig(path)) {
133
+ validPaths.push(path);
134
+ } else {
135
+ invalidPaths.push(path);
136
+ }
137
+ }
138
+
139
+ // Prune invalid paths
140
+ if (invalidPaths.length > 0) {
141
+ index.paths[projectName] = validPaths;
142
+ if (validPaths.length === 0) {
143
+ delete index.paths[projectName];
144
+ }
145
+ await writeLocalPaths(index);
146
+ }
147
+
148
+ return validPaths;
149
+ }
150
+
151
+ /**
152
+ * Get all local paths for all projects, verified to exist
153
+ * Returns a map of projectName -> paths[]
154
+ */
155
+ export async function getAllLocalPaths(): Promise<Record<string, string[]>> {
156
+ const index = await readLocalPaths();
157
+ const result: Record<string, string[]> = {};
158
+ let needsWrite = false;
159
+
160
+ for (const [projectName, paths] of Object.entries(index.paths)) {
161
+ const validPaths: string[] = [];
162
+
163
+ for (const path of paths) {
164
+ if (hasWranglerConfig(path)) {
165
+ validPaths.push(path);
166
+ } else {
167
+ needsWrite = true;
168
+ }
169
+ }
170
+
171
+ if (validPaths.length > 0) {
172
+ result[projectName] = validPaths;
173
+ } else if (paths.length > 0) {
174
+ needsWrite = true;
175
+ }
176
+ }
177
+
178
+ // Write back pruned index if needed
179
+ if (needsWrite) {
180
+ index.paths = result;
181
+ await writeLocalPaths(index);
182
+ }
183
+
184
+ return result;
185
+ }
186
+
187
+ /**
188
+ * Scan a directory recursively for jack projects
189
+ * Returns discovered projects with their paths
190
+ */
191
+ export async function scanDirectoryForProjects(
192
+ rootDir: string,
193
+ maxDepth = 3,
194
+ ): Promise<Array<{ name: string; path: string }>> {
195
+ const { getProjectNameFromDir } = await import("./storage/index.ts");
196
+ const discovered: Array<{ name: string; path: string }> = [];
197
+ const absoluteRoot = resolve(rootDir);
198
+
199
+ async function scan(dir: string, depth: number): Promise<void> {
200
+ if (depth > maxDepth) return;
201
+
202
+ // Check if this directory is a jack project
203
+ try {
204
+ const name = await getProjectNameFromDir(dir);
205
+ discovered.push({ name, path: dir });
206
+ return; // Don't scan subdirectories of projects
207
+ } catch {
208
+ // Not a project, continue scanning subdirectories
209
+ }
210
+
211
+ // Scan subdirectories
212
+ try {
213
+ const entries = await readdir(dir, { withFileTypes: true });
214
+
215
+ for (const entry of entries) {
216
+ // Skip non-directories
217
+ if (!entry.isDirectory()) continue;
218
+
219
+ // Skip hidden directories and common non-project directories
220
+ if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) {
221
+ continue;
222
+ }
223
+
224
+ const fullPath = join(dir, entry.name);
225
+ await scan(fullPath, depth + 1);
226
+ }
227
+ } catch {
228
+ // Permission denied or other error, skip silently
229
+ }
230
+ }
231
+
232
+ await scan(absoluteRoot, 0);
233
+ return discovered;
234
+ }
235
+
236
+ /**
237
+ * Register multiple discovered projects
238
+ * More efficient than calling registerLocalPath for each project
239
+ */
240
+ export async function registerDiscoveredProjects(
241
+ projects: Array<{ name: string; path: string }>,
242
+ ): Promise<void> {
243
+ const index = await readLocalPaths();
244
+
245
+ for (const { name, path } of projects) {
246
+ const absolutePath = resolve(path);
247
+
248
+ if (!index.paths[name]) {
249
+ index.paths[name] = [];
250
+ }
251
+
252
+ if (!index.paths[name].includes(absolutePath)) {
253
+ index.paths[name].push(absolutePath);
254
+ }
255
+ }
256
+
257
+ await writeLocalPaths(index);
258
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Managed deploy handler for jack cloud
3
+ *
4
+ * Isolates managed deployment logic from BYO (wrangler) path.
5
+ */
6
+
7
+ import { validateBindings } from "./binding-validator.ts";
8
+ import { buildProject, parseWranglerConfig } from "./build-helper.ts";
9
+ import { createManagedProject } from "./control-plane.ts";
10
+ import { uploadDeployment } from "./deploy-upload.ts";
11
+ import { JackError, JackErrorCode } from "./errors.ts";
12
+ import type { OperationReporter } from "./project-operations.ts";
13
+ import { Events, track } from "./telemetry.ts";
14
+ import { packageForDeploy } from "./zip-packager.ts";
15
+
16
+ export interface ManagedCreateResult {
17
+ projectId: string;
18
+ projectSlug: string;
19
+ orgId: string;
20
+ runjackUrl: string;
21
+ status?: "live" | "created";
22
+ prebuiltFailed?: boolean;
23
+ prebuiltError?: string;
24
+ }
25
+
26
+ export interface ManagedCreateOptions {
27
+ template?: string;
28
+ usePrebuilt?: boolean;
29
+ }
30
+
31
+ /**
32
+ * Create a project via the jack cloud control plane.
33
+ *
34
+ * This creates the remote project resource. Local files are created separately
35
+ * by the main createProject() function.
36
+ */
37
+ export async function createManagedProjectRemote(
38
+ projectName: string,
39
+ reporter?: OperationReporter,
40
+ options?: ManagedCreateOptions,
41
+ ): Promise<ManagedCreateResult> {
42
+ reporter?.start("Creating managed project...");
43
+
44
+ try {
45
+ const result = await createManagedProject(projectName, {
46
+ template: options?.template,
47
+ usePrebuilt: options?.usePrebuilt ?? true,
48
+ });
49
+
50
+ const runjackUrl = `https://${result.project.slug}.runjack.xyz`;
51
+
52
+ reporter?.stop();
53
+ reporter?.success("Created managed project");
54
+
55
+ // Track managed project creation
56
+ track(Events.MANAGED_PROJECT_CREATED, {});
57
+
58
+ return {
59
+ projectId: result.project.id,
60
+ projectSlug: result.project.slug,
61
+ orgId: result.project.org_id,
62
+ runjackUrl,
63
+ status: result.status,
64
+ prebuiltFailed: result.prebuilt_failed,
65
+ prebuiltError: result.prebuilt_error,
66
+ };
67
+ } catch (error) {
68
+ reporter?.stop();
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ export interface ManagedCodeDeployOptions {
74
+ projectId: string;
75
+ projectPath: string;
76
+ reporter?: OperationReporter;
77
+ }
78
+
79
+ /**
80
+ * Deploy local code to a managed project via the control plane.
81
+ *
82
+ * This builds the project, packages artifacts, and uploads to jack cloud.
83
+ */
84
+ export async function deployCodeToManagedProject(
85
+ options: ManagedCodeDeployOptions,
86
+ ): Promise<{ deploymentId: string; status: string }> {
87
+ const { projectId, projectPath, reporter } = options;
88
+
89
+ // Track deploy start
90
+ track(Events.MANAGED_DEPLOY_STARTED, {});
91
+ const startTime = Date.now();
92
+
93
+ let pkg: Awaited<ReturnType<typeof packageForDeploy>> | null = null;
94
+
95
+ try {
96
+ const config = await parseWranglerConfig(projectPath);
97
+
98
+ // Step 1: Build the project (must happen before validation, as build creates dist/)
99
+ reporter?.start("Building project...");
100
+ const buildOutput = await buildProject({ projectPath, reporter });
101
+
102
+ // Step 2: Validate bindings are supported (after build, so assets dir exists)
103
+ const validation = validateBindings(config, projectPath);
104
+ if (!validation.valid) {
105
+ throw new JackError(
106
+ JackErrorCode.VALIDATION_ERROR,
107
+ validation.errors[0] || "Invalid bindings configuration",
108
+ validation.errors.length > 1
109
+ ? `Additional errors:\n${validation.errors.slice(1).join("\n")}`
110
+ : undefined,
111
+ );
112
+ }
113
+
114
+ // Step 3: Package artifacts
115
+ reporter?.start("Packaging artifacts...");
116
+ pkg = await packageForDeploy(projectPath, buildOutput, config);
117
+ reporter?.stop();
118
+ reporter?.success("Packaged artifacts");
119
+
120
+ // Step 4: Upload to control plane
121
+ reporter?.start("Uploading to jack cloud...");
122
+ const result = await uploadDeployment({
123
+ projectId,
124
+ bundleZipPath: pkg.bundleZipPath,
125
+ sourceZipPath: pkg.sourceZipPath,
126
+ manifestPath: pkg.manifestPath,
127
+ schemaPath: pkg.schemaPath ?? undefined,
128
+ secretsPath: pkg.secretsPath ?? undefined,
129
+ assetsZipPath: pkg.assetsZipPath ?? undefined,
130
+ assetManifest: pkg.assetManifest ?? undefined,
131
+ });
132
+
133
+ reporter?.stop();
134
+ reporter?.success("Deployed to jack cloud");
135
+
136
+ // Track success
137
+ track(Events.MANAGED_DEPLOY_COMPLETED, {
138
+ duration_ms: Date.now() - startTime,
139
+ });
140
+
141
+ return {
142
+ deploymentId: result.id,
143
+ status: result.status,
144
+ };
145
+ } catch (error) {
146
+ reporter?.stop();
147
+
148
+ // Track failure
149
+ track(Events.MANAGED_DEPLOY_FAILED, {
150
+ duration_ms: Date.now() - startTime,
151
+ });
152
+
153
+ throw error;
154
+ } finally {
155
+ // Always cleanup temp files
156
+ if (pkg) {
157
+ await pkg.cleanup().catch(() => {});
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Deploy to a managed project via the control plane.
164
+ */
165
+ export async function deployToManagedProject(
166
+ projectId: string,
167
+ projectPath: string,
168
+ reporter?: OperationReporter,
169
+ ): Promise<{ deploymentId: string; status: string }> {
170
+ return deployCodeToManagedProject({
171
+ projectId,
172
+ projectPath,
173
+ reporter,
174
+ });
175
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Managed mode project deletion handler
3
+ * Mirrors BYO down.ts flow but uses control plane APIs.
4
+ */
5
+
6
+ import { writeFile } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { deleteManagedProject, exportManagedDatabase } from "./control-plane.ts";
9
+ import { promptSelect } from "./hooks.ts";
10
+ import { error, info, item, output, success, warn } from "./output.ts";
11
+ import type { Project } from "./registry.ts";
12
+ import { updateProject } from "./registry.ts";
13
+
14
+ export interface ManagedDownFlags {
15
+ force?: boolean;
16
+ }
17
+
18
+ export async function managedDown(
19
+ project: Project,
20
+ projectName: string,
21
+ flags: ManagedDownFlags = {},
22
+ ): Promise<boolean> {
23
+ const remote = project.remote;
24
+ if (!remote?.project_id) {
25
+ throw new Error("Project is not linked to jack cloud");
26
+ }
27
+
28
+ const runjackUrl = remote.runjack_url;
29
+ const projectId = remote.project_id;
30
+
31
+ // Force mode - quick deletion without prompts
32
+ if (flags.force) {
33
+ console.error("");
34
+ info(`Undeploying '${projectName}'`);
35
+ console.error("");
36
+
37
+ output.start("Undeploying from jack cloud...");
38
+ try {
39
+ await deleteManagedProject(projectId);
40
+ output.stop();
41
+
42
+ await updateProject(projectName, {
43
+ workerUrl: null,
44
+ lastDeployed: null,
45
+ });
46
+
47
+ console.error("");
48
+ success(`'${projectName}' undeployed`);
49
+ info("Database and backups were deleted");
50
+ console.error("");
51
+ return true;
52
+ } catch (err) {
53
+ output.stop();
54
+ throw err;
55
+ }
56
+ }
57
+
58
+ // Interactive mode
59
+ console.error("");
60
+ info(`Project: ${projectName}`);
61
+ if (runjackUrl) {
62
+ item(`URL: ${runjackUrl}`);
63
+ }
64
+ item("Mode: jack cloud (managed)");
65
+ item("Database: managed D1");
66
+ console.error("");
67
+
68
+ // Confirm undeploy
69
+ console.error("");
70
+ info("Undeploy this project?");
71
+ const action = await promptSelect(["Yes", "No"]);
72
+
73
+ if (action !== 0) {
74
+ info("Cancelled");
75
+ return false;
76
+ }
77
+
78
+ // Ask about database export
79
+ console.error("");
80
+ info("Database will be deleted with the project");
81
+
82
+ console.error("");
83
+ info("Export database before deleting?");
84
+ const exportAction = await promptSelect(["Yes", "No"]);
85
+
86
+ if (exportAction === 0) {
87
+ const exportPath = join(process.cwd(), `${projectName}-backup.sql`);
88
+ output.start(`Exporting database to ${exportPath}...`);
89
+
90
+ try {
91
+ const exportResult = await exportManagedDatabase(projectId);
92
+
93
+ // Download the SQL file
94
+ const response = await fetch(exportResult.download_url);
95
+ if (!response.ok) {
96
+ throw new Error(`Failed to download export: ${response.statusText}`);
97
+ }
98
+
99
+ const sqlContent = await response.text();
100
+ await writeFile(exportPath, sqlContent, "utf-8");
101
+
102
+ output.stop();
103
+ success(`Database exported to ${exportPath}`);
104
+ } catch (err) {
105
+ output.stop();
106
+ error(`Failed to export database: ${err instanceof Error ? err.message : String(err)}`);
107
+
108
+ // If export times out, abort
109
+ if (err instanceof Error && err.message.includes("timed out")) {
110
+ error("Export timeout - deletion aborted");
111
+ return false;
112
+ }
113
+
114
+ console.error("");
115
+ info("Continue without exporting?");
116
+ const continueAction = await promptSelect(["Yes", "No"]);
117
+
118
+ if (continueAction !== 0) {
119
+ info("Cancelled");
120
+ return false;
121
+ }
122
+ }
123
+ }
124
+
125
+ // Execute deletion
126
+ console.error("");
127
+ info("Executing cleanup...");
128
+ console.error("");
129
+
130
+ output.start("Undeploying from jack cloud...");
131
+ try {
132
+ const result = await deleteManagedProject(projectId);
133
+ output.stop();
134
+ success(`'${projectName}' undeployed`);
135
+
136
+ // Report resource results
137
+ for (const resource of result.resources) {
138
+ if (resource.success) {
139
+ success(`Deleted ${resource.resource}`);
140
+ } else {
141
+ warn(`Failed to delete ${resource.resource}: ${resource.error}`);
142
+ }
143
+ }
144
+
145
+ await updateProject(projectName, {
146
+ workerUrl: null,
147
+ lastDeployed: null,
148
+ });
149
+
150
+ console.error("");
151
+ success(`Project '${projectName}' undeployed`);
152
+ console.error("");
153
+ return true;
154
+ } catch (err) {
155
+ output.stop();
156
+ error(`Failed to undeploy: ${err instanceof Error ? err.message : String(err)}`);
157
+ throw err;
158
+ }
159
+ }