@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.
- package/README.md +16 -0
- package/package.json +47 -39
- package/src/commands/agents.ts +40 -9
- package/src/commands/cloud.ts +8 -4
- package/src/commands/down.ts +120 -69
- package/src/commands/init.ts +41 -3
- package/src/commands/mcp.ts +18 -0
- package/src/commands/new.ts +64 -334
- package/src/commands/projects.ts +139 -143
- package/src/commands/services.ts +315 -0
- package/src/commands/ship.ts +33 -139
- package/src/index.ts +27 -3
- package/src/lib/agent-files.ts +0 -41
- package/src/lib/agents.ts +238 -64
- package/src/lib/cloudflare-api.ts +3 -2
- package/src/lib/config.ts +8 -0
- package/src/lib/errors.ts +53 -0
- package/src/lib/hooks.ts +93 -41
- package/src/lib/mcp-config.ts +175 -0
- package/src/lib/project-operations.ts +793 -0
- package/src/lib/prompts.ts +15 -7
- package/src/lib/registry.ts +29 -1
- package/src/lib/services/db.ts +81 -0
- package/src/lib/services/index.ts +27 -0
- package/src/lib/telemetry.ts +10 -1
- package/src/mcp/README.md +142 -0
- package/src/mcp/resources/index.ts +87 -0
- package/src/mcp/server.ts +32 -0
- package/src/mcp/tools/index.ts +261 -0
- package/src/mcp/types.ts +29 -0
- package/src/mcp/utils.ts +147 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/types.ts +16 -8
- package/templates/CLAUDE.md +105 -4
- package/templates/api/.jack.json +20 -1
- package/templates/api/src/index.ts +1 -1
- package/templates/miniapp/.jack.json +7 -5
package/src/commands/projects.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { dirname } from "node:path";
|
|
3
|
-
import {
|
|
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 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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
|
|
142
|
+
const notDeployedCount = statuses.filter((s) => s.local && !s.deployed).length;
|
|
143
|
+
const staleCount = statuses.filter((s) => s.missing).length;
|
|
164
144
|
|
|
165
|
-
|
|
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(
|
|
178
|
+
badges.push(`${colors.green}[local]${colors.reset}`);
|
|
177
179
|
}
|
|
178
180
|
if (status.deployed) {
|
|
179
|
-
badges.push(
|
|
181
|
+
badges.push(`${colors.green}[deployed]${colors.reset}`);
|
|
180
182
|
}
|
|
181
183
|
if (status.backedUp) {
|
|
182
|
-
badges.push(
|
|
184
|
+
badges.push(`${colors.dim}[cloud]${colors.reset}`);
|
|
183
185
|
}
|
|
184
186
|
if (status.missing) {
|
|
185
|
-
badges.push(
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
228
|
+
if (status.local) {
|
|
230
229
|
statuses.push("local");
|
|
231
230
|
}
|
|
232
|
-
if (
|
|
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 (
|
|
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 (
|
|
247
|
-
item(`Local path: ${
|
|
248
|
-
if (
|
|
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 (
|
|
256
|
-
item(`Worker URL: ${
|
|
254
|
+
if (status.workerUrl) {
|
|
255
|
+
item(`Worker URL: ${status.workerUrl}`);
|
|
257
256
|
}
|
|
258
|
-
if (
|
|
259
|
-
item(`Last deployed: ${new Date(
|
|
257
|
+
if (status.lastDeployed) {
|
|
258
|
+
item(`Last deployed: ${new Date(status.lastDeployed).toLocaleString()}`);
|
|
260
259
|
}
|
|
261
|
-
if (
|
|
260
|
+
if (status.deployed) {
|
|
262
261
|
console.error("");
|
|
263
262
|
}
|
|
264
263
|
|
|
265
264
|
// Cloud info
|
|
266
|
-
if (backedUp &&
|
|
267
|
-
item(`Cloud backup: ${
|
|
268
|
-
|
|
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
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
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 (
|
|
279
|
-
item(
|
|
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
|
-
|
|
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
|
|
298
|
-
|
|
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 (
|
|
310
|
+
if (scan.stale.length === 0) {
|
|
340
311
|
success("No stale projects found");
|
|
341
312
|
return;
|
|
342
313
|
}
|
|
343
314
|
|
|
344
|
-
//
|
|
315
|
+
// Explain what cleanup does
|
|
345
316
|
console.error("");
|
|
346
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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 (
|
|
354
|
+
if (action === "no") {
|
|
361
355
|
info("Cleanup cancelled");
|
|
362
356
|
return;
|
|
363
357
|
}
|
|
364
358
|
|
|
365
359
|
// Remove stale entries
|
|
366
|
-
|
|
367
|
-
await removeProject(stale.name);
|
|
368
|
-
}
|
|
360
|
+
await cleanupStaleProjects(scan.stale.map((stale) => stale.name));
|
|
369
361
|
|
|
370
|
-
|
|
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
|
}
|