@getjack/jack 0.1.2 → 0.1.4
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/README.md +77 -29
- package/package.json +54 -47
- package/src/commands/agents.ts +145 -10
- package/src/commands/down.ts +110 -102
- package/src/commands/feedback.ts +189 -0
- package/src/commands/init.ts +8 -12
- package/src/commands/login.ts +88 -0
- package/src/commands/logout.ts +14 -0
- package/src/commands/logs.ts +21 -0
- package/src/commands/mcp.ts +134 -7
- package/src/commands/new.ts +43 -17
- package/src/commands/open.ts +13 -6
- package/src/commands/projects.ts +269 -143
- package/src/commands/secrets.ts +413 -0
- package/src/commands/services.ts +96 -123
- package/src/commands/ship.ts +5 -1
- package/src/commands/whoami.ts +31 -0
- package/src/index.ts +218 -144
- package/src/lib/agent-files.ts +34 -0
- package/src/lib/agents.ts +390 -22
- package/src/lib/asset-hash.ts +50 -0
- package/src/lib/auth/client.ts +115 -0
- package/src/lib/auth/constants.ts +5 -0
- package/src/lib/auth/guard.ts +57 -0
- package/src/lib/auth/index.ts +18 -0
- package/src/lib/auth/store.ts +54 -0
- package/src/lib/binding-validator.ts +136 -0
- package/src/lib/build-helper.ts +211 -0
- package/src/lib/cloudflare-api.ts +24 -0
- package/src/lib/config.ts +5 -6
- package/src/lib/control-plane.ts +295 -0
- package/src/lib/debug.ts +3 -1
- package/src/lib/deploy-mode.ts +93 -0
- package/src/lib/deploy-upload.ts +92 -0
- package/src/lib/errors.ts +2 -0
- package/src/lib/github.ts +31 -1
- package/src/lib/hooks.ts +4 -12
- package/src/lib/intent.ts +88 -0
- package/src/lib/jsonc.ts +125 -0
- package/src/lib/local-paths.test.ts +902 -0
- package/src/lib/local-paths.ts +258 -0
- package/src/lib/managed-deploy.ts +175 -0
- package/src/lib/managed-down.ts +159 -0
- package/src/lib/mcp-config.ts +55 -34
- package/src/lib/names.ts +9 -29
- package/src/lib/project-operations.ts +676 -249
- package/src/lib/project-resolver.ts +476 -0
- package/src/lib/registry.ts +76 -37
- package/src/lib/resources.ts +196 -0
- package/src/lib/schema.ts +30 -1
- package/src/lib/storage/file-filter.ts +1 -0
- package/src/lib/storage/index.ts +5 -1
- package/src/lib/telemetry.ts +14 -0
- package/src/lib/tty.ts +15 -0
- package/src/lib/zip-packager.ts +255 -0
- package/src/mcp/resources/index.ts +8 -2
- package/src/mcp/server.ts +32 -4
- package/src/mcp/tools/index.ts +35 -13
- package/src/mcp/types.ts +6 -0
- package/src/mcp/utils.ts +1 -1
- package/src/templates/index.ts +42 -4
- package/src/templates/types.ts +13 -0
- package/templates/CLAUDE.md +166 -0
- package/templates/api/.jack.json +4 -0
- package/templates/api/bun.lock +1 -0
- package/templates/api/wrangler.jsonc +5 -0
- package/templates/hello/.jack.json +28 -0
- package/templates/hello/package.json +10 -0
- package/templates/hello/src/index.ts +11 -0
- package/templates/hello/tsconfig.json +11 -0
- package/templates/hello/wrangler.jsonc +5 -0
- package/templates/miniapp/.jack.json +15 -4
- package/templates/miniapp/bun.lock +135 -40
- package/templates/miniapp/index.html +1 -0
- package/templates/miniapp/package.json +3 -1
- package/templates/miniapp/public/.well-known/farcaster.json +7 -5
- package/templates/miniapp/public/icon.png +0 -0
- package/templates/miniapp/public/og.png +0 -0
- package/templates/miniapp/schema.sql +8 -0
- package/templates/miniapp/src/App.tsx +254 -3
- package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
- package/templates/miniapp/src/hooks/useAI.ts +35 -0
- package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
- package/templates/miniapp/src/hooks/useShare.ts +76 -0
- package/templates/miniapp/src/index.css +15 -0
- package/templates/miniapp/src/lib/api.ts +2 -1
- package/templates/miniapp/src/worker.ts +515 -1
- package/templates/miniapp/wrangler.jsonc +15 -3
- package/LICENSE +0 -190
- package/src/commands/cloud.ts +0 -230
- package/templates/api/wrangler.toml +0 -3
package/src/commands/projects.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { promptSelect } from "../lib/hooks.ts";
|
|
3
5
|
import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts";
|
|
4
6
|
import {
|
|
5
7
|
cleanupStaleProjects,
|
|
6
8
|
getProjectStatus,
|
|
7
|
-
listAllProjects,
|
|
8
9
|
scanStaleProjects,
|
|
9
|
-
type ProjectStatus,
|
|
10
10
|
} from "../lib/project-operations.ts";
|
|
11
|
+
import {
|
|
12
|
+
type ResolvedProject,
|
|
13
|
+
listAllProjects,
|
|
14
|
+
removeProject as removeProjectEverywhere,
|
|
15
|
+
resolveProject,
|
|
16
|
+
} from "../lib/project-resolver.ts";
|
|
11
17
|
import { getProjectNameFromDir } from "../lib/storage/index.ts";
|
|
12
18
|
|
|
13
19
|
/**
|
|
@@ -21,87 +27,85 @@ export default async function projects(subcommand?: string, args: string[] = [])
|
|
|
21
27
|
switch (subcommand) {
|
|
22
28
|
case "info":
|
|
23
29
|
return await infoProject(args);
|
|
30
|
+
case "remove":
|
|
31
|
+
return await removeProjectEntry(args);
|
|
24
32
|
case "cleanup":
|
|
25
33
|
return await cleanupProjects();
|
|
26
|
-
|
|
34
|
+
case "scan":
|
|
35
|
+
return await scanProjects(args);
|
|
36
|
+
case "down":
|
|
37
|
+
return await handleDown(args);
|
|
38
|
+
default: {
|
|
39
|
+
// Commands that are valid top-level commands users might try under projects
|
|
40
|
+
const topLevelCommands = ["open", "logs", "clone", "sync"];
|
|
41
|
+
const isTopLevel = topLevelCommands.includes(subcommand);
|
|
42
|
+
|
|
27
43
|
error(`Unknown subcommand: ${subcommand}`);
|
|
28
|
-
|
|
44
|
+
if (isTopLevel) {
|
|
45
|
+
const projectArg = args[0] ? ` ${args[0]}` : "";
|
|
46
|
+
info(`Did you mean: jack ${subcommand}${projectArg}`);
|
|
47
|
+
}
|
|
48
|
+
info("Available: list, info, remove, cleanup, scan, down");
|
|
29
49
|
process.exit(1);
|
|
50
|
+
}
|
|
30
51
|
}
|
|
31
52
|
}
|
|
32
53
|
|
|
33
54
|
/**
|
|
34
55
|
* List all projects with status indicators
|
|
35
56
|
*/
|
|
36
|
-
async function listProjects(
|
|
37
|
-
//
|
|
38
|
-
const flags = {
|
|
39
|
-
local: args.includes("--local"),
|
|
40
|
-
deployed: args.includes("--deployed"),
|
|
41
|
-
cloud: args.includes("--cloud"),
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// Determine status for each project (with spinner for API calls)
|
|
57
|
+
async function listProjects(_args: string[]): Promise<void> {
|
|
58
|
+
// Fetch all projects from registry and control plane
|
|
45
59
|
outputSpinner.start("Checking project status...");
|
|
46
|
-
const
|
|
60
|
+
const projects: ResolvedProject[] = await listAllProjects();
|
|
47
61
|
outputSpinner.stop();
|
|
48
62
|
|
|
49
|
-
if (
|
|
63
|
+
if (projects.length === 0) {
|
|
50
64
|
info("No projects found");
|
|
51
65
|
info("Create a project with: jack new <name>");
|
|
52
66
|
return;
|
|
53
67
|
}
|
|
54
68
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
filteredStatuses = filteredStatuses.filter((s) => s.local);
|
|
59
|
-
}
|
|
60
|
-
if (flags.deployed) {
|
|
61
|
-
filteredStatuses = filteredStatuses.filter((s) => s.deployed);
|
|
62
|
-
}
|
|
63
|
-
if (flags.cloud) {
|
|
64
|
-
filteredStatuses = filteredStatuses.filter((s) => s.backedUp);
|
|
65
|
-
}
|
|
69
|
+
// Separate local projects from cloud-only projects
|
|
70
|
+
const localProjects: ResolvedProject[] = [];
|
|
71
|
+
const cloudOnlyProjects: ResolvedProject[] = [];
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
for (const proj of projects) {
|
|
74
|
+
if (proj.localPath && proj.sources.filesystem) {
|
|
75
|
+
localProjects.push(proj);
|
|
76
|
+
} else {
|
|
77
|
+
cloudOnlyProjects.push(proj);
|
|
78
|
+
}
|
|
70
79
|
}
|
|
71
80
|
|
|
72
|
-
// Group projects by parent directory
|
|
81
|
+
// Group local projects by parent directory
|
|
73
82
|
interface DirectoryGroup {
|
|
74
|
-
|
|
75
|
-
projects:
|
|
83
|
+
displayPath: string;
|
|
84
|
+
projects: ResolvedProject[];
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
const groups = new Map<string, DirectoryGroup>();
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (!groups.has(parent)) {
|
|
89
|
-
groups.set(parent, { path: parent, projects: [] });
|
|
90
|
-
}
|
|
91
|
-
groups.get(parent)?.projects.push(status);
|
|
92
|
-
} else {
|
|
93
|
-
ungrouped.push(status);
|
|
88
|
+
const home = homedir();
|
|
89
|
+
|
|
90
|
+
for (const proj of localProjects) {
|
|
91
|
+
if (!proj.localPath) continue;
|
|
92
|
+
const parent = dirname(proj.localPath);
|
|
93
|
+
if (!groups.has(parent)) {
|
|
94
|
+
// Replace home directory with ~ for display
|
|
95
|
+
const displayPath = parent.startsWith(home) ? `~${parent.slice(home.length)}` : parent;
|
|
96
|
+
groups.set(parent, { displayPath, projects: [] });
|
|
94
97
|
}
|
|
98
|
+
groups.get(parent)?.projects.push(proj);
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
// Display
|
|
101
|
+
// Display header
|
|
98
102
|
console.error("");
|
|
99
|
-
info("
|
|
103
|
+
info("Your projects");
|
|
100
104
|
console.error("");
|
|
101
105
|
|
|
102
|
-
// Display
|
|
106
|
+
// Display local project groups
|
|
103
107
|
for (const [_parentPath, group] of groups) {
|
|
104
|
-
console.error(
|
|
108
|
+
console.error(` ${colors.dim}${group.displayPath}/${colors.reset}`);
|
|
105
109
|
const sortedProjects = group.projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
106
110
|
|
|
107
111
|
for (let i = 0; i < sortedProjects.length; i++) {
|
|
@@ -110,51 +114,41 @@ async function listProjects(args: string[]): Promise<void> {
|
|
|
110
114
|
const isLast = i === sortedProjects.length - 1;
|
|
111
115
|
const prefix = isLast ? "└──" : "├──";
|
|
112
116
|
|
|
113
|
-
const
|
|
114
|
-
console.error(` ${colors.dim}${prefix}${colors.reset} ${proj.name} ${
|
|
117
|
+
const statusBadge = buildStatusBadge(proj);
|
|
118
|
+
console.error(` ${colors.dim}${prefix}${colors.reset} ${proj.name} ${statusBadge}`);
|
|
115
119
|
}
|
|
116
120
|
console.error("");
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
// Display
|
|
120
|
-
if (
|
|
121
|
-
console.error(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
// Display cloud-only projects
|
|
124
|
+
if (cloudOnlyProjects.length > 0) {
|
|
125
|
+
console.error(` ${colors.dim}On jack cloud (no local files)${colors.reset}`);
|
|
126
|
+
const sortedCloudProjects = cloudOnlyProjects.sort((a, b) => a.name.localeCompare(b.name));
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < sortedCloudProjects.length; i++) {
|
|
129
|
+
const proj = sortedCloudProjects[i];
|
|
130
|
+
if (!proj) continue;
|
|
131
|
+
const isLast = i === sortedCloudProjects.length - 1;
|
|
132
|
+
const prefix = isLast ? "└──" : "├──";
|
|
128
133
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
console.error(`${colors.yellow}Stale (local folder deleted):${colors.reset}`);
|
|
132
|
-
for (const proj of stale) {
|
|
133
|
-
// Only show non-missing badges since the section header explains the issue
|
|
134
|
-
const badges = buildStatusBadges({ ...proj, missing: false });
|
|
135
|
-
console.error(` ${colors.dim}${proj.name}${colors.reset} ${badges}`);
|
|
134
|
+
const statusBadge = buildStatusBadge(proj);
|
|
135
|
+
console.error(` ${colors.dim}${prefix}${colors.reset} ${proj.name} ${statusBadge}`);
|
|
136
136
|
}
|
|
137
137
|
console.error("");
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
// Summary
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
141
|
+
const liveCount = projects.filter((p) => p.status === "live").length;
|
|
142
|
+
const localOnlyCount = projects.filter((p) => p.status === "local-only").length;
|
|
143
|
+
const errorCount = projects.filter((p) => p.status === "error").length;
|
|
144
144
|
|
|
145
|
-
const parts = [
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
info(`${statuses.length} projects (${parts.join(", ")})`);
|
|
153
|
-
if (staleCount > 0) {
|
|
154
|
-
console.error(
|
|
155
|
-
` ${colors.dim}Run 'jack projects cleanup' to remove stale entries${colors.reset}`,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
145
|
+
const parts: string[] = [];
|
|
146
|
+
if (liveCount > 0) parts.push(`${liveCount} live`);
|
|
147
|
+
if (localOnlyCount > 0) parts.push(`${localOnlyCount} local-only`);
|
|
148
|
+
if (errorCount > 0) parts.push(`${errorCount} error`);
|
|
149
|
+
|
|
150
|
+
const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
151
|
+
info(`${projects.length} projects${summary}`);
|
|
158
152
|
console.error("");
|
|
159
153
|
}
|
|
160
154
|
|
|
@@ -169,25 +163,21 @@ const colors = {
|
|
|
169
163
|
};
|
|
170
164
|
|
|
171
165
|
/**
|
|
172
|
-
* Build status badge
|
|
166
|
+
* Build a user-friendly status badge for a project
|
|
173
167
|
*/
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (status.missing) {
|
|
187
|
-
badges.push(`${colors.yellow}[local deleted]${colors.reset}`);
|
|
168
|
+
function buildStatusBadge(project: ResolvedProject): string {
|
|
169
|
+
switch (project.status) {
|
|
170
|
+
case "live":
|
|
171
|
+
return `${colors.green}[live]${colors.reset} ${colors.cyan}${project.url || ""}${colors.reset}`;
|
|
172
|
+
case "local-only":
|
|
173
|
+
return `${colors.dim}[local only]${colors.reset}`;
|
|
174
|
+
case "error":
|
|
175
|
+
return `${colors.red}[error]${colors.reset} ${project.errorMessage || "deployment failed"}`;
|
|
176
|
+
case "syncing":
|
|
177
|
+
return `${colors.yellow}[syncing]${colors.reset}`;
|
|
178
|
+
default:
|
|
179
|
+
return "";
|
|
188
180
|
}
|
|
189
|
-
|
|
190
|
-
return badges.join(" ");
|
|
191
181
|
}
|
|
192
182
|
|
|
193
183
|
/**
|
|
@@ -232,21 +222,15 @@ async function infoProject(args: string[]): Promise<void> {
|
|
|
232
222
|
statuses.push("deployed");
|
|
233
223
|
}
|
|
234
224
|
if (status.backedUp) {
|
|
235
|
-
statuses.push("
|
|
236
|
-
}
|
|
237
|
-
if (status.missing) {
|
|
238
|
-
statuses.push("missing");
|
|
225
|
+
statuses.push("backup");
|
|
239
226
|
}
|
|
240
227
|
|
|
241
228
|
item(`Status: ${statuses.join(", ") || "none"}`);
|
|
242
229
|
console.error("");
|
|
243
230
|
|
|
244
|
-
//
|
|
231
|
+
// Workspace info (only shown if running from project directory)
|
|
245
232
|
if (status.localPath) {
|
|
246
|
-
item(`
|
|
247
|
-
if (status.missing) {
|
|
248
|
-
warn(" Path no longer exists");
|
|
249
|
-
}
|
|
233
|
+
item(`Workspace path: ${status.localPath}`);
|
|
250
234
|
console.error("");
|
|
251
235
|
}
|
|
252
236
|
|
|
@@ -261,9 +245,9 @@ async function infoProject(args: string[]): Promise<void> {
|
|
|
261
245
|
console.error("");
|
|
262
246
|
}
|
|
263
247
|
|
|
264
|
-
//
|
|
248
|
+
// Backup info
|
|
265
249
|
if (status.backedUp && status.backupFiles !== null) {
|
|
266
|
-
item(`
|
|
250
|
+
item(`Backup: ${status.backupFiles} files`);
|
|
267
251
|
if (status.backupLastSync) {
|
|
268
252
|
item(`Last synced: ${new Date(status.backupLastSync).toLocaleString()}`);
|
|
269
253
|
}
|
|
@@ -293,7 +277,7 @@ async function infoProject(args: string[]): Promise<void> {
|
|
|
293
277
|
}
|
|
294
278
|
|
|
295
279
|
/**
|
|
296
|
-
*
|
|
280
|
+
* Find and remove stale registry entries (projects with URLs but no deployed worker)
|
|
297
281
|
*/
|
|
298
282
|
async function cleanupProjects(): Promise<void> {
|
|
299
283
|
outputSpinner.start("Scanning for stale projects...");
|
|
@@ -316,42 +300,24 @@ async function cleanupProjects(): Promise<void> {
|
|
|
316
300
|
console.error("");
|
|
317
301
|
info("What cleanup does:");
|
|
318
302
|
item("Removes entries from jack's local tracking registry");
|
|
319
|
-
item("Does NOT
|
|
320
|
-
|
|
303
|
+
item("Does NOT delete backups or databases");
|
|
304
|
+
info("Remove a single entry with: jack projects remove <name>");
|
|
321
305
|
console.error("");
|
|
322
306
|
|
|
323
307
|
// Show found issues
|
|
324
308
|
warn(`Found ${scan.stale.length} stale project(s):`);
|
|
325
309
|
console.error("");
|
|
326
310
|
|
|
327
|
-
// Check which have deployed workers
|
|
328
|
-
const deployedStale = scan.stale.filter((stale) => stale.workerUrl);
|
|
329
|
-
|
|
330
311
|
for (const stale of scan.stale) {
|
|
331
|
-
|
|
332
|
-
? ` ${colors.yellow}(still deployed)${colors.reset}`
|
|
333
|
-
: "";
|
|
334
|
-
item(`${stale.name}: ${stale.reason}${hasWorker}`);
|
|
312
|
+
item(`${stale.name}: ${stale.reason} (URL: ${stale.workerUrl})`);
|
|
335
313
|
}
|
|
336
314
|
console.error("");
|
|
337
315
|
|
|
338
|
-
if (deployedStale.length > 0) {
|
|
339
|
-
warn(`${deployedStale.length} project(s) are still deployed`);
|
|
340
|
-
info("To fully remove, run 'jack down <name>' first");
|
|
341
|
-
console.error("");
|
|
342
|
-
}
|
|
343
|
-
|
|
344
316
|
// Prompt to remove
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
{ name: "1. Yes", value: "yes" },
|
|
350
|
-
{ name: "2. No", value: "no" },
|
|
351
|
-
],
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
if (action === "no") {
|
|
317
|
+
info("Remove these from jack's tracking?");
|
|
318
|
+
const choice = await promptSelect(["Yes", "No"]);
|
|
319
|
+
|
|
320
|
+
if (choice !== 0) {
|
|
355
321
|
info("Cleanup cancelled");
|
|
356
322
|
return;
|
|
357
323
|
}
|
|
@@ -361,7 +327,167 @@ async function cleanupProjects(): Promise<void> {
|
|
|
361
327
|
|
|
362
328
|
console.error("");
|
|
363
329
|
success(`Removed ${scan.stale.length} entry/entries from jack's registry`);
|
|
364
|
-
|
|
365
|
-
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Remove a project from registry and jack cloud
|
|
334
|
+
*/
|
|
335
|
+
async function removeProjectEntry(args: string[]): Promise<void> {
|
|
336
|
+
const name = args.find((arg) => !arg.startsWith("--"));
|
|
337
|
+
const yes = args.includes("--yes");
|
|
338
|
+
|
|
339
|
+
if (!name) {
|
|
340
|
+
error("Project name required");
|
|
341
|
+
info("Usage: jack projects remove <name>");
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Use resolver to find project anywhere (registry OR control plane)
|
|
346
|
+
outputSpinner.start("Checking project status...");
|
|
347
|
+
const project = await resolveProject(name);
|
|
348
|
+
outputSpinner.stop();
|
|
349
|
+
|
|
350
|
+
if (!project) {
|
|
351
|
+
error(`Project "${name}" not found`);
|
|
352
|
+
info("Check available projects with: jack projects");
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Show what we found and where
|
|
357
|
+
console.error("");
|
|
358
|
+
info(`Removing "${name}"...`);
|
|
359
|
+
console.error("");
|
|
360
|
+
|
|
361
|
+
// Show project details
|
|
362
|
+
if (project.localPath) {
|
|
363
|
+
item(`Workspace: ${project.localPath}`);
|
|
364
|
+
}
|
|
365
|
+
if (project.url) {
|
|
366
|
+
item(`URL: ${project.url}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Show where we'll remove from
|
|
370
|
+
const locations: string[] = [];
|
|
371
|
+
if (project.sources.registry) {
|
|
372
|
+
locations.push("local registry");
|
|
373
|
+
}
|
|
374
|
+
if (project.sources.controlPlane) {
|
|
375
|
+
locations.push("jack cloud");
|
|
376
|
+
}
|
|
377
|
+
if (locations.length > 0) {
|
|
378
|
+
item(`Will remove from: ${locations.join(", ")}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Warn if still deployed
|
|
382
|
+
if (project.status === "live") {
|
|
383
|
+
console.error("");
|
|
384
|
+
warn("Project is still deployed; removal does not undeploy the worker");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
console.error("");
|
|
388
|
+
|
|
389
|
+
if (!yes) {
|
|
390
|
+
info("Remove this project?");
|
|
391
|
+
const choice = await promptSelect(["Yes", "No"]);
|
|
392
|
+
|
|
393
|
+
if (choice !== 0) {
|
|
394
|
+
info("Removal cancelled");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Use resolver's removeProject to clean up everywhere
|
|
400
|
+
outputSpinner.start("Removing project...");
|
|
401
|
+
const result = await removeProjectEverywhere(name);
|
|
402
|
+
outputSpinner.stop();
|
|
403
|
+
|
|
404
|
+
console.error("");
|
|
405
|
+
|
|
406
|
+
// Show what was removed
|
|
407
|
+
if (result.removed.length > 0) {
|
|
408
|
+
for (const location of result.removed) {
|
|
409
|
+
success(`Removed from ${location}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Show any errors
|
|
414
|
+
if (result.errors.length > 0) {
|
|
415
|
+
for (const err of result.errors) {
|
|
416
|
+
warn(err);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Final status
|
|
421
|
+
if (result.removed.length > 0 && result.errors.length === 0) {
|
|
422
|
+
console.error("");
|
|
423
|
+
success(`Project "${name}" removed`);
|
|
424
|
+
} else if (result.removed.length === 0) {
|
|
425
|
+
console.error("");
|
|
426
|
+
error(`Failed to remove "${name}"`);
|
|
366
427
|
}
|
|
428
|
+
|
|
429
|
+
// Hint about undeploying
|
|
430
|
+
if (project.status === "live") {
|
|
431
|
+
console.error("");
|
|
432
|
+
info(`To undeploy the worker, run: jack down ${name}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Scan a directory for jack projects and register them
|
|
438
|
+
*/
|
|
439
|
+
async function scanProjects(args: string[]): Promise<void> {
|
|
440
|
+
const targetDir = args[0] || process.cwd();
|
|
441
|
+
const absoluteDir = resolve(targetDir);
|
|
442
|
+
|
|
443
|
+
if (!existsSync(absoluteDir)) {
|
|
444
|
+
error(`Directory not found: ${targetDir}`);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
outputSpinner.start(`Scanning ${targetDir} for jack projects...`);
|
|
449
|
+
|
|
450
|
+
const { scanDirectoryForProjects, registerDiscoveredProjects } = await import(
|
|
451
|
+
"../lib/local-paths.ts"
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const discovered = await scanDirectoryForProjects(absoluteDir);
|
|
455
|
+
outputSpinner.stop();
|
|
456
|
+
|
|
457
|
+
if (discovered.length === 0) {
|
|
458
|
+
info("No jack projects found");
|
|
459
|
+
info("Projects must have a wrangler.toml or wrangler.jsonc file");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
console.error("");
|
|
464
|
+
info(`Found ${discovered.length} project(s):`);
|
|
465
|
+
|
|
466
|
+
const home = homedir();
|
|
467
|
+
for (let i = 0; i < discovered.length; i++) {
|
|
468
|
+
const proj = discovered[i];
|
|
469
|
+
if (!proj) continue;
|
|
470
|
+
const displayPath = proj.path.startsWith(home) ? `~${proj.path.slice(home.length)}` : proj.path;
|
|
471
|
+
const isLast = i === discovered.length - 1;
|
|
472
|
+
const prefix = isLast ? "└──" : "├──";
|
|
473
|
+
console.error(
|
|
474
|
+
` ${colors.dim}${prefix}${colors.reset} ${proj.name} ${colors.dim}${displayPath}${colors.reset}`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Register all discovered projects
|
|
479
|
+
await registerDiscoveredProjects(discovered);
|
|
480
|
+
|
|
481
|
+
console.error("");
|
|
482
|
+
success(`Registered ${discovered.length} local path(s)`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Handle down subcommand - routes to top-level down command
|
|
487
|
+
*/
|
|
488
|
+
async function handleDown(args: string[]): Promise<void> {
|
|
489
|
+
const { default: down } = await import("./down.ts");
|
|
490
|
+
const projectName = args.find((arg) => !arg.startsWith("--"));
|
|
491
|
+
const force = args.includes("--force");
|
|
492
|
+
return await down(projectName, { force });
|
|
367
493
|
}
|