@getjack/jack 0.1.0 → 0.1.1

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.
@@ -1,19 +1,14 @@
1
- import { existsSync } from "node:fs";
2
1
  import { dirname } from "node:path";
3
- import { confirm } from "@inquirer/prompts";
4
- import { checkWorkerExists } from "../lib/cloudflare-api.ts";
2
+ import { select } from "@inquirer/prompts";
5
3
  import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts";
6
- import { getAllProjects, getProject, removeProject } from "../lib/registry.ts";
7
- import { getProjectNameFromDir, getRemoteManifest } from "../lib/storage/index.ts";
8
-
9
- interface ProjectStatus {
10
- name: string;
11
- localPath: string | null;
12
- local: boolean;
13
- deployed: boolean;
14
- backedUp: boolean;
15
- missing: boolean;
16
- }
4
+ import {
5
+ cleanupStaleProjects,
6
+ getProjectStatus,
7
+ listAllProjects,
8
+ scanStaleProjects,
9
+ type ProjectStatus,
10
+ } from "../lib/project-operations.ts";
11
+ import { getProjectNameFromDir } from "../lib/storage/index.ts";
17
12
 
18
13
  /**
19
14
  * Main projects command - handles all project management
@@ -46,47 +41,17 @@ async function listProjects(args: string[]): Promise<void> {
46
41
  cloud: args.includes("--cloud"),
47
42
  };
48
43
 
49
- const projects = await getAllProjects();
50
- const projectNames = Object.keys(projects);
44
+ // Determine status for each project (with spinner for API calls)
45
+ outputSpinner.start("Checking project status...");
46
+ const statuses: ProjectStatus[] = await listAllProjects();
47
+ outputSpinner.stop();
51
48
 
52
- if (projectNames.length === 0) {
49
+ if (statuses.length === 0) {
53
50
  info("No projects found");
54
51
  info("Create a project with: jack new <name>");
55
52
  return;
56
53
  }
57
54
 
58
- // Determine status for each project
59
- const statuses: ProjectStatus[] = [];
60
-
61
- for (const name of projectNames) {
62
- const project = projects[name];
63
- if (!project) continue;
64
-
65
- const local = project.localPath ? existsSync(project.localPath) : false;
66
- const missing = project.localPath ? !local : false;
67
-
68
- // Check if deployed
69
- let deployed = false;
70
- if (project.workerUrl) {
71
- deployed = true;
72
- } else {
73
- deployed = await checkWorkerExists(name);
74
- }
75
-
76
- // Check if backed up
77
- const manifest = await getRemoteManifest(name);
78
- const backedUp = manifest !== null;
79
-
80
- statuses.push({
81
- name,
82
- localPath: project.localPath,
83
- local,
84
- deployed,
85
- backedUp,
86
- missing,
87
- });
88
- }
89
-
90
55
  // Filter based on flags
91
56
  let filteredStatuses = statuses;
92
57
  if (flags.local) {
@@ -112,9 +77,13 @@ async function listProjects(args: string[]): Promise<void> {
112
77
 
113
78
  const groups = new Map<string, DirectoryGroup>();
114
79
  const ungrouped: ProjectStatus[] = [];
80
+ const stale: ProjectStatus[] = [];
115
81
 
116
82
  for (const status of filteredStatuses) {
117
- if (status.localPath) {
83
+ // Stale projects go to their own section
84
+ if (status.missing) {
85
+ stale.push(status);
86
+ } else if (status.localPath && status.local) {
118
87
  const parent = dirname(status.localPath);
119
88
  if (!groups.has(parent)) {
120
89
  groups.set(parent, { path: parent, projects: [] });
@@ -130,9 +99,9 @@ async function listProjects(args: string[]): Promise<void> {
130
99
  info("Projects");
131
100
  console.error("");
132
101
 
133
- // Display directory groups
102
+ // Display directory groups (active local projects)
134
103
  for (const [_parentPath, group] of groups) {
135
- console.error(`${group.path}/`);
104
+ console.error(`${colors.dim}${group.path}/${colors.reset}`);
136
105
  const sortedProjects = group.projects.sort((a, b) => a.name.localeCompare(b.name));
137
106
 
138
107
  for (let i = 0; i < sortedProjects.length; i++) {
@@ -142,14 +111,14 @@ async function listProjects(args: string[]): Promise<void> {
142
111
  const prefix = isLast ? "└──" : "├──";
143
112
 
144
113
  const badges = buildStatusBadges(proj);
145
- console.error(` ${prefix} ${proj.name} ${badges}`);
114
+ console.error(` ${colors.dim}${prefix}${colors.reset} ${proj.name} ${badges}`);
146
115
  }
147
116
  console.error("");
148
117
  }
149
118
 
150
- // Display ungrouped projects
119
+ // Display ungrouped projects (cloud-only, no local path)
151
120
  if (ungrouped.length > 0) {
152
- console.error("Other:");
121
+ console.error(`${colors.dim}Cloud only:${colors.reset}`);
153
122
  for (const proj of ungrouped) {
154
123
  const badges = buildStatusBadges(proj);
155
124
  console.error(` ${proj.name} ${badges}`);
@@ -157,15 +126,48 @@ async function listProjects(args: string[]): Promise<void> {
157
126
  console.error("");
158
127
  }
159
128
 
129
+ // Display stale projects (local folder deleted)
130
+ if (stale.length > 0) {
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}`);
136
+ }
137
+ console.error("");
138
+ }
139
+
160
140
  // Summary
161
- const localCount = statuses.filter((s) => s.local).length;
162
141
  const deployedCount = statuses.filter((s) => s.deployed).length;
163
- const localOnlyCount = statuses.filter((s) => s.local && !s.deployed).length;
142
+ const notDeployedCount = statuses.filter((s) => s.local && !s.deployed).length;
143
+ const staleCount = statuses.filter((s) => s.missing).length;
164
144
 
165
- info(`${statuses.length} projects (${deployedCount} deployed, ${localOnlyCount} local-only)`);
145
+ const parts = [`${deployedCount} deployed`];
146
+ if (notDeployedCount > 0) {
147
+ parts.push(`${notDeployedCount} not deployed`);
148
+ }
149
+ if (staleCount > 0) {
150
+ parts.push(`${staleCount} stale`);
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
+ }
166
158
  console.error("");
167
159
  }
168
160
 
161
+ // Color codes
162
+ const colors = {
163
+ reset: "\x1b[0m",
164
+ dim: "\x1b[90m",
165
+ green: "\x1b[32m",
166
+ yellow: "\x1b[33m",
167
+ red: "\x1b[31m",
168
+ cyan: "\x1b[36m",
169
+ };
170
+
169
171
  /**
170
172
  * Build status badge string for a project
171
173
  */
@@ -173,16 +175,16 @@ function buildStatusBadges(status: ProjectStatus): string {
173
175
  const badges: string[] = [];
174
176
 
175
177
  if (status.local) {
176
- badges.push("[local]");
178
+ badges.push(`${colors.green}[local]${colors.reset}`);
177
179
  }
178
180
  if (status.deployed) {
179
- badges.push("[deployed]");
181
+ badges.push(`${colors.green}[deployed]${colors.reset}`);
180
182
  }
181
183
  if (status.backedUp) {
182
- badges.push("[backed-up]");
184
+ badges.push(`${colors.dim}[cloud]${colors.reset}`);
183
185
  }
184
186
  if (status.missing) {
185
- badges.push("[missing]");
187
+ badges.push(`${colors.yellow}[local deleted]${colors.reset}`);
186
188
  }
187
189
 
188
190
  return badges.join(" ");
@@ -206,36 +208,33 @@ async function infoProject(args: string[]): Promise<void> {
206
208
  }
207
209
  }
208
210
 
209
- const project = await getProject(name);
211
+ // Check actual status (with spinner for API calls)
212
+ outputSpinner.start("Fetching project info...");
213
+ const status = await getProjectStatus(name);
214
+ outputSpinner.stop();
210
215
 
211
- if (!project) {
216
+ if (!status) {
212
217
  error(`Project "${name}" not found in registry`);
213
218
  info("List projects with: jack projects list");
214
219
  process.exit(1);
215
220
  }
216
221
 
217
- // Check actual status
218
- const localExists = project.localPath ? existsSync(project.localPath) : false;
219
- const workerExists = await checkWorkerExists(name);
220
- const manifest = await getRemoteManifest(name);
221
- const backedUp = manifest !== null;
222
-
223
222
  console.error("");
224
- info(`Project: ${name}`);
223
+ info(`Project: ${status.name}`);
225
224
  console.error("");
226
225
 
227
226
  // Status section
228
227
  const statuses: string[] = [];
229
- if (localExists) {
228
+ if (status.local) {
230
229
  statuses.push("local");
231
230
  }
232
- if (workerExists || project.workerUrl) {
231
+ if (status.deployed) {
233
232
  statuses.push("deployed");
234
233
  }
235
- if (backedUp) {
234
+ if (status.backedUp) {
236
235
  statuses.push("backed-up");
237
236
  }
238
- if (project.localPath && !localExists) {
237
+ if (status.missing) {
239
238
  statuses.push("missing");
240
239
  }
241
240
 
@@ -243,48 +242,53 @@ async function infoProject(args: string[]): Promise<void> {
243
242
  console.error("");
244
243
 
245
244
  // Local info
246
- if (project.localPath) {
247
- item(`Local path: ${project.localPath}`);
248
- if (!localExists) {
245
+ if (status.localPath) {
246
+ item(`Local path: ${status.localPath}`);
247
+ if (status.missing) {
249
248
  warn(" Path no longer exists");
250
249
  }
251
250
  console.error("");
252
251
  }
253
252
 
254
253
  // Deployment info
255
- if (project.workerUrl) {
256
- item(`Worker URL: ${project.workerUrl}`);
254
+ if (status.workerUrl) {
255
+ item(`Worker URL: ${status.workerUrl}`);
257
256
  }
258
- if (project.lastDeployed) {
259
- item(`Last deployed: ${new Date(project.lastDeployed).toLocaleString()}`);
257
+ if (status.lastDeployed) {
258
+ item(`Last deployed: ${new Date(status.lastDeployed).toLocaleString()}`);
260
259
  }
261
- if (workerExists || project.workerUrl) {
260
+ if (status.deployed) {
262
261
  console.error("");
263
262
  }
264
263
 
265
264
  // Cloud info
266
- if (backedUp && manifest) {
267
- item(`Cloud backup: ${manifest.files.length} files`);
268
- item(`Last synced: ${new Date(manifest.lastSync).toLocaleString()}`);
265
+ if (status.backedUp && status.backupFiles !== null) {
266
+ item(`Cloud backup: ${status.backupFiles} files`);
267
+ if (status.backupLastSync) {
268
+ item(`Last synced: ${new Date(status.backupLastSync).toLocaleString()}`);
269
+ }
269
270
  console.error("");
270
271
  }
271
272
 
272
- // Cloudflare info
273
- item(`Account ID: ${project.cloudflare.accountId}`);
274
- item(`Worker ID: ${project.cloudflare.workerId}`);
273
+ // Account info
274
+ if (status.accountId) {
275
+ item(`Account ID: ${status.accountId}`);
276
+ }
277
+ if (status.workerId) {
278
+ item(`Worker ID: ${status.workerId}`);
279
+ }
275
280
  console.error("");
276
281
 
277
282
  // Resources
278
- if (project.resources.d1Databases.length > 0) {
279
- item("D1 Databases:");
280
- for (const db of project.resources.d1Databases) {
281
- item(` - ${db}`);
282
- }
283
+ if (status.dbName) {
284
+ item(`Database: ${status.dbName}`);
283
285
  console.error("");
284
286
  }
285
287
 
286
288
  // Timestamps
287
- item(`Created: ${new Date(project.createdAt).toLocaleString()}`);
289
+ if (status.createdAt) {
290
+ item(`Created: ${new Date(status.createdAt).toLocaleString()}`);
291
+ }
288
292
  console.error("");
289
293
  }
290
294
 
@@ -294,78 +298,70 @@ async function infoProject(args: string[]): Promise<void> {
294
298
  async function cleanupProjects(): Promise<void> {
295
299
  outputSpinner.start("Scanning for stale projects...");
296
300
 
297
- const projects = await getAllProjects();
298
- const projectNames = Object.keys(projects);
299
-
300
- if (projectNames.length === 0) {
301
+ const scan = await scanStaleProjects();
302
+ if (scan.total === 0) {
301
303
  outputSpinner.stop();
302
304
  info("No projects to clean up");
303
305
  return;
304
306
  }
305
307
 
306
- interface StaleProject {
307
- name: string;
308
- reason: string;
309
- }
310
-
311
- const staleProjects: StaleProject[] = [];
312
-
313
- // Check each project for issues
314
- for (const name of projectNames) {
315
- const project = projects[name];
316
- if (!project) continue;
317
-
318
- // Check if local path is missing
319
- if (project.localPath && !existsSync(project.localPath)) {
320
- staleProjects.push({
321
- name,
322
- reason: "Local path missing",
323
- });
324
- continue;
325
- }
326
-
327
- // Check if worker was deleted
328
- const workerExists = await checkWorkerExists(name);
329
- if (project.workerUrl && !workerExists) {
330
- staleProjects.push({
331
- name,
332
- reason: "Worker deleted from Cloudflare",
333
- });
334
- }
335
- }
336
-
337
308
  outputSpinner.stop();
338
309
 
339
- if (staleProjects.length === 0) {
310
+ if (scan.stale.length === 0) {
340
311
  success("No stale projects found");
341
312
  return;
342
313
  }
343
314
 
344
- // Show found issues
315
+ // Explain what cleanup does
345
316
  console.error("");
346
- warn(`Found ${staleProjects.length} stale project(s):`);
317
+ info("What cleanup does:");
318
+ item("Removes entries from jack's local tracking registry");
319
+ item("Does NOT undeploy live services");
320
+ item("Does NOT delete cloud backups or databases");
347
321
  console.error("");
348
322
 
349
- for (const stale of staleProjects) {
350
- item(`${stale.name}: ${stale.reason}`);
323
+ // Show found issues
324
+ warn(`Found ${scan.stale.length} stale project(s):`);
325
+ console.error("");
326
+
327
+ // Check which have deployed workers
328
+ const deployedStale = scan.stale.filter((stale) => stale.workerUrl);
329
+
330
+ for (const stale of scan.stale) {
331
+ const hasWorker = stale.workerUrl
332
+ ? ` ${colors.yellow}(still deployed)${colors.reset}`
333
+ : "";
334
+ item(`${stale.name}: ${stale.reason}${hasWorker}`);
351
335
  }
352
336
  console.error("");
353
337
 
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
+
354
344
  // Prompt to remove
355
- const shouldRemove = await confirm({
356
- message: "Remove these entries from registry?",
357
- default: false,
345
+ console.error(" Esc to skip\n");
346
+ const action = await select({
347
+ message: "Remove these from jack's tracking? (deployed services stay live)",
348
+ choices: [
349
+ { name: "1. Yes", value: "yes" },
350
+ { name: "2. No", value: "no" },
351
+ ],
358
352
  });
359
353
 
360
- if (!shouldRemove) {
354
+ if (action === "no") {
361
355
  info("Cleanup cancelled");
362
356
  return;
363
357
  }
364
358
 
365
359
  // Remove stale entries
366
- for (const stale of staleProjects) {
367
- await removeProject(stale.name);
368
- }
360
+ await cleanupStaleProjects(scan.stale.map((stale) => stale.name));
369
361
 
370
- success(`Removed ${staleProjects.length} stale project(s)`);
362
+ console.error("");
363
+ success(`Removed ${scan.stale.length} entry/entries from jack's registry`);
364
+ if (deployedStale.length > 0) {
365
+ info("Note: Deployed services are still live");
366
+ }
371
367
  }