@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/new.ts
CHANGED
|
@@ -1,365 +1,95 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { promptSelect, runHook } from "../lib/hooks.ts";
|
|
8
|
-
import { generateProjectName } from "../lib/names.ts";
|
|
9
|
-
import { output } from "../lib/output.ts";
|
|
10
|
-
import { applySchema, getD1DatabaseName, hasD1Config } from "../lib/schema.ts";
|
|
11
|
-
import { getSavedSecrets } from "../lib/secrets.ts";
|
|
12
|
-
import { renderTemplate, resolveTemplate } from "../templates/index.ts";
|
|
13
|
-
import type { Template } from "../templates/types.ts";
|
|
14
|
-
import { isInitialized } from "./init.ts";
|
|
1
|
+
import { getPreferredLaunchAgent, launchAgent } from "../lib/agents.ts";
|
|
2
|
+
import { debug } from "../lib/debug.ts";
|
|
3
|
+
import { getErrorDetails } from "../lib/errors.ts";
|
|
4
|
+
import { promptSelect } from "../lib/hooks.ts";
|
|
5
|
+
import { output, spinner } from "../lib/output.ts";
|
|
6
|
+
import { createProject } from "../lib/project-operations.ts";
|
|
15
7
|
|
|
16
8
|
export default async function newProject(
|
|
17
9
|
name?: string,
|
|
18
10
|
options: { template?: string } = {},
|
|
19
11
|
): Promise<void> {
|
|
20
|
-
const timings: Array<{ label: string; duration: number }> = [];
|
|
21
|
-
|
|
22
12
|
// Immediate feedback
|
|
23
13
|
output.start("Starting...");
|
|
24
14
|
debug("newProject called", { name, options });
|
|
15
|
+
const isCi = process.env.CI === "true" || process.env.CI === "1";
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
17
|
+
let result: Awaited<ReturnType<typeof createProject>>;
|
|
18
|
+
try {
|
|
19
|
+
result = await createProject(name, {
|
|
20
|
+
template: options.template,
|
|
21
|
+
reporter: {
|
|
22
|
+
start: output.start,
|
|
23
|
+
stop: output.stop,
|
|
24
|
+
spinner,
|
|
25
|
+
info: output.info,
|
|
26
|
+
warn: output.warn,
|
|
27
|
+
error: output.error,
|
|
28
|
+
success: output.success,
|
|
29
|
+
box: output.box,
|
|
30
|
+
},
|
|
31
|
+
interactive: !isCi,
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const details = getErrorDetails(error);
|
|
44
35
|
output.stop();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
output.start("Creating project...");
|
|
50
|
-
|
|
51
|
-
// Load template
|
|
52
|
-
let template: Template;
|
|
53
|
-
const templateDuration = await time("Load template", async () => {
|
|
54
|
-
try {
|
|
55
|
-
template = await resolveTemplate(options.template);
|
|
56
|
-
debug("Template loaded", {
|
|
57
|
-
files: Object.keys(template.files).length,
|
|
58
|
-
secrets: template.secrets,
|
|
59
|
-
hooks: Object.keys(template.hooks || {}),
|
|
60
|
-
});
|
|
61
|
-
} catch (err) {
|
|
62
|
-
output.stop();
|
|
63
|
-
output.error(err instanceof Error ? err.message : String(err));
|
|
64
|
-
process.exit(1);
|
|
36
|
+
if (!details.meta?.reported) {
|
|
37
|
+
output.error(details.message);
|
|
65
38
|
}
|
|
66
|
-
});
|
|
67
|
-
timings.push({ label: "Load template", duration: templateDuration });
|
|
68
39
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (template!.secrets?.length) {
|
|
74
|
-
const secretsDuration = await time("Check secrets", async () => {
|
|
75
|
-
const saved = await getSavedSecrets();
|
|
76
|
-
debug("Saved secrets", Object.keys(saved));
|
|
77
|
-
|
|
78
|
-
for (const key of template!.secrets!) {
|
|
79
|
-
if (saved[key]) {
|
|
80
|
-
secretsToUse[key] = saved[key];
|
|
81
|
-
}
|
|
40
|
+
const hasMissingSecrets = !!details.meta?.missingSecrets?.length;
|
|
41
|
+
if (hasMissingSecrets) {
|
|
42
|
+
for (const key of details.meta.missingSecrets) {
|
|
43
|
+
output.info(` Run: jack secrets add ${key}`);
|
|
82
44
|
}
|
|
83
|
-
|
|
84
|
-
const missing = template!.secrets!.filter((key) => !saved[key]);
|
|
85
|
-
if (missing.length > 0) {
|
|
86
|
-
output.stop();
|
|
87
|
-
output.error(`Missing required secrets: ${missing.join(", ")}`);
|
|
88
|
-
for (const key of missing) {
|
|
89
|
-
output.info(` Run: jack secrets add ${key}`);
|
|
90
|
-
}
|
|
91
|
-
process.exit(1);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Show what we're using (no prompt - omakase)
|
|
95
|
-
output.stop();
|
|
96
|
-
for (const key of Object.keys(secretsToUse)) {
|
|
97
|
-
output.success(`Using saved secret: ${key}`);
|
|
98
|
-
}
|
|
99
|
-
output.start("Creating project...");
|
|
100
|
-
});
|
|
101
|
-
timings.push({ label: "Check secrets", duration: secretsDuration });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Write all template files
|
|
105
|
-
const writeDuration = await time("Write files", async () => {
|
|
106
|
-
for (const [filePath, content] of Object.entries(rendered.files)) {
|
|
107
|
-
debug(`Writing: ${filePath}`);
|
|
108
|
-
await Bun.write(join(targetDir, filePath), content);
|
|
109
45
|
}
|
|
110
|
-
});
|
|
111
|
-
timings.push({ label: "Write files", duration: writeDuration });
|
|
112
|
-
|
|
113
|
-
// Write secrets files (.env for Vite, .dev.vars for wrangler local, .secrets.json for wrangler bulk)
|
|
114
|
-
if (Object.keys(secretsToUse).length > 0) {
|
|
115
|
-
const envContent = generateEnvFile(secretsToUse);
|
|
116
|
-
const jsonContent = generateSecretsJson(secretsToUse);
|
|
117
|
-
await Bun.write(join(targetDir, ".env"), envContent);
|
|
118
|
-
await Bun.write(join(targetDir, ".dev.vars"), envContent);
|
|
119
|
-
await Bun.write(join(targetDir, ".secrets.json"), jsonContent);
|
|
120
|
-
debug("Wrote .env, .dev.vars, and .secrets.json");
|
|
121
46
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (!gitignoreExists) {
|
|
126
|
-
await Bun.write(gitignorePath, ".env\n.env.*\n.dev.vars\n.secrets.json\nnode_modules/\n");
|
|
127
|
-
} else {
|
|
128
|
-
const existingContent = await Bun.file(gitignorePath).text();
|
|
129
|
-
if (!existingContent.includes(".env")) {
|
|
130
|
-
await Bun.write(
|
|
131
|
-
gitignorePath,
|
|
132
|
-
`${existingContent}\n.env\n.env.*\n.dev.vars\n.secrets.json\n`,
|
|
133
|
-
);
|
|
134
|
-
}
|
|
47
|
+
if (details.meta?.stderr) {
|
|
48
|
+
console.error(details.meta.stderr);
|
|
135
49
|
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Generate agent context files
|
|
139
|
-
const agentsDuration = await time("Generate agent files", async () => {
|
|
140
|
-
let activeAgents = await getActiveAgents();
|
|
141
|
-
if (activeAgents.length > 0) {
|
|
142
|
-
// Validate paths still exist
|
|
143
|
-
const validation = await validateAgentPaths();
|
|
144
|
-
|
|
145
|
-
if (validation.invalid.length > 0) {
|
|
146
|
-
output.stop();
|
|
147
|
-
output.warn("Some agent paths no longer exist:");
|
|
148
|
-
for (const { id, path } of validation.invalid) {
|
|
149
|
-
output.info(` ${id}: ${path}`);
|
|
150
|
-
}
|
|
151
|
-
output.info("Run: jack agents scan");
|
|
152
|
-
output.start("Creating project...");
|
|
153
50
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
({ id }) => !validation.invalid.some((inv) => inv.id === id),
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (activeAgents.length > 0) {
|
|
161
|
-
await generateAgentFiles(targetDir, projectName, template!, activeAgents);
|
|
162
|
-
const agentNames = activeAgents.map(({ definition }) => definition.name).join(", ");
|
|
163
|
-
output.stop();
|
|
164
|
-
output.success(`Generated context for: ${agentNames}`);
|
|
165
|
-
output.start("Creating project...");
|
|
166
|
-
}
|
|
51
|
+
if (details.suggestion && !details.meta?.reported && !hasMissingSecrets) {
|
|
52
|
+
output.info(details.suggestion);
|
|
167
53
|
}
|
|
168
|
-
});
|
|
169
|
-
if (agentsDuration > 0) {
|
|
170
|
-
timings.push({ label: "Generate agent files", duration: agentsDuration });
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
output.stop();
|
|
174
|
-
output.success(`Created ${projectName}/`);
|
|
175
54
|
|
|
176
|
-
|
|
177
|
-
output.start("Installing dependencies...");
|
|
178
|
-
const installDuration = await time("bun install", async () => {
|
|
179
|
-
debug("Running: bun install");
|
|
180
|
-
const install = Bun.spawn(["bun", "install"], {
|
|
181
|
-
cwd: targetDir,
|
|
182
|
-
stdout: "ignore",
|
|
183
|
-
stderr: "ignore",
|
|
184
|
-
});
|
|
185
|
-
await install.exited;
|
|
186
|
-
|
|
187
|
-
if (install.exitCode === 0) {
|
|
188
|
-
output.stop();
|
|
189
|
-
output.success("Dependencies installed");
|
|
190
|
-
} else {
|
|
191
|
-
output.stop();
|
|
192
|
-
output.warn("Failed to install dependencies, run: bun install");
|
|
193
|
-
printTimingSummary(timings);
|
|
55
|
+
if (details.meta?.exitCode === 0) {
|
|
194
56
|
return;
|
|
195
57
|
}
|
|
196
|
-
});
|
|
197
|
-
timings.push({ label: "bun install", duration: installDuration });
|
|
198
|
-
|
|
199
|
-
// Auto-deploy
|
|
200
|
-
const { $ } = await import("bun");
|
|
201
58
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const hookDuration = await time("Pre-deploy hooks", async () => {
|
|
205
|
-
debug("Running pre-deploy hooks", template!.hooks?.preDeploy);
|
|
206
|
-
const hookContext = { projectName, projectDir: targetDir };
|
|
207
|
-
const passed = await runHook(template!.hooks!.preDeploy!, hookContext);
|
|
208
|
-
if (!passed) {
|
|
209
|
-
output.error("Pre-deploy checks failed");
|
|
210
|
-
printTimingSummary(timings);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
timings.push({ label: "Pre-deploy hooks", duration: hookDuration });
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// For Vite projects, build first
|
|
218
|
-
const hasVite = existsSync(join(targetDir, "vite.config.ts"));
|
|
219
|
-
if (hasVite) {
|
|
220
|
-
output.start("Building...");
|
|
221
|
-
const buildDuration = await time("vite build", async () => {
|
|
222
|
-
debug("Running: npx vite build");
|
|
223
|
-
const buildResult = await $`npx vite build`.cwd(targetDir).nothrow().quiet();
|
|
224
|
-
if (buildResult.exitCode !== 0) {
|
|
225
|
-
output.stop();
|
|
226
|
-
output.error("Build failed");
|
|
227
|
-
console.error(buildResult.stderr.toString());
|
|
228
|
-
debug("Build stderr", buildResult.stderr.toString());
|
|
229
|
-
printTimingSummary(timings);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
output.stop();
|
|
233
|
-
output.success("Built");
|
|
234
|
-
});
|
|
235
|
-
timings.push({ label: "vite build", duration: buildDuration });
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
output.start("Deploying...");
|
|
239
|
-
const deployDuration = await time("wrangler deploy", async () => {
|
|
240
|
-
debug("Running: wrangler deploy");
|
|
241
|
-
const deployResult = await $`wrangler deploy`.cwd(targetDir).nothrow().quiet();
|
|
242
|
-
|
|
243
|
-
if (deployResult.exitCode !== 0) {
|
|
244
|
-
output.stop();
|
|
245
|
-
output.error("Deploy failed");
|
|
246
|
-
console.error(deployResult.stderr.toString());
|
|
247
|
-
debug("Deploy stderr", deployResult.stderr.toString());
|
|
248
|
-
printTimingSummary(timings);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
timings.push({ label: "wrangler deploy", duration: deployDuration });
|
|
253
|
-
|
|
254
|
-
// Apply schema.sql after deploy (database auto-provisioned by wrangler)
|
|
255
|
-
if (await hasD1Config(targetDir)) {
|
|
256
|
-
const schemaDuration = await time("Apply schema", async () => {
|
|
257
|
-
const dbName = await getD1DatabaseName(targetDir);
|
|
258
|
-
if (dbName) {
|
|
259
|
-
try {
|
|
260
|
-
await applySchema(dbName, targetDir);
|
|
261
|
-
} catch (err) {
|
|
262
|
-
output.warn(`Schema application failed: ${err}`);
|
|
263
|
-
output.info("Run manually: bun run db:migrate");
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
timings.push({ label: "Apply schema", duration: schemaDuration });
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Push secrets to Cloudflare (if any)
|
|
271
|
-
const secretsJsonPath = join(targetDir, ".secrets.json");
|
|
272
|
-
if (existsSync(secretsJsonPath)) {
|
|
273
|
-
output.start("Configuring secrets...");
|
|
274
|
-
const secretsPushDuration = await time("wrangler secret bulk", async () => {
|
|
275
|
-
debug("Running: wrangler secret bulk .secrets.json");
|
|
276
|
-
const secretsResult = await $`wrangler secret bulk .secrets.json`
|
|
277
|
-
.cwd(targetDir)
|
|
278
|
-
.nothrow()
|
|
279
|
-
.quiet();
|
|
280
|
-
if (secretsResult.exitCode !== 0) {
|
|
281
|
-
output.stop();
|
|
282
|
-
output.warn("Failed to push secrets to Cloudflare");
|
|
283
|
-
output.info("Run manually: wrangler secret bulk .secrets.json");
|
|
284
|
-
debug("Secrets stderr", secretsResult.stderr.toString());
|
|
285
|
-
} else {
|
|
286
|
-
output.stop();
|
|
287
|
-
output.success("Secrets configured");
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
timings.push({ label: "wrangler secret bulk", duration: secretsPushDuration });
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Parse URL from output (re-run deploy to get output if needed)
|
|
294
|
-
const deployResult = await $`wrangler deploy`.cwd(targetDir).nothrow().quiet();
|
|
295
|
-
const deployOutput = deployResult.stdout.toString();
|
|
296
|
-
const urlMatch = deployOutput.match(/https:\/\/[\w-]+\.[\w-]+\.workers\.dev/);
|
|
297
|
-
|
|
298
|
-
output.stop();
|
|
299
|
-
if (urlMatch) {
|
|
300
|
-
output.success(`Live: ${urlMatch[0]}`);
|
|
301
|
-
} else {
|
|
302
|
-
output.success("Deployed");
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Register project in registry after successful deploy
|
|
306
|
-
try {
|
|
307
|
-
const { registerProject } = await import("../lib/registry.ts");
|
|
308
|
-
const { getAccountId } = await import("../lib/cloudflare-api.ts");
|
|
309
|
-
|
|
310
|
-
const accountId = await getAccountId();
|
|
311
|
-
await registerProject(projectName, {
|
|
312
|
-
localPath: targetDir,
|
|
313
|
-
workerUrl: urlMatch ? urlMatch[0] : null,
|
|
314
|
-
createdAt: new Date().toISOString(),
|
|
315
|
-
lastDeployed: urlMatch ? new Date().toISOString() : null,
|
|
316
|
-
cloudflare: {
|
|
317
|
-
accountId,
|
|
318
|
-
workerId: projectName,
|
|
319
|
-
},
|
|
320
|
-
resources: {
|
|
321
|
-
d1Databases: [], // Can be enhanced later to detect from template
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
} catch {
|
|
325
|
-
// Don't fail the deploy if registry update fails
|
|
59
|
+
process.exit(details.meta?.exitCode ?? 1);
|
|
60
|
+
return;
|
|
326
61
|
}
|
|
327
62
|
|
|
328
63
|
console.error("");
|
|
329
|
-
output.info(`Project: ${targetDir}`);
|
|
330
|
-
|
|
331
|
-
// Run post-deploy hooks
|
|
332
|
-
if (template!.hooks?.postDeploy?.length && urlMatch) {
|
|
333
|
-
const postHookDuration = await time("Post-deploy hooks", async () => {
|
|
334
|
-
const deployedUrl = urlMatch[0];
|
|
335
|
-
const domain = deployedUrl.replace(/^https?:\/\//, "");
|
|
336
|
-
debug("Running post-deploy hooks", { domain, url: deployedUrl });
|
|
337
|
-
await runHook(template!.hooks!.postDeploy!, {
|
|
338
|
-
domain,
|
|
339
|
-
url: deployedUrl,
|
|
340
|
-
projectName,
|
|
341
|
-
projectDir: targetDir,
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
timings.push({ label: "Post-deploy hooks", duration: postHookDuration });
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Print timing summary if debug mode
|
|
348
|
-
printTimingSummary(timings);
|
|
64
|
+
output.info(`Project: ${result.targetDir}`);
|
|
349
65
|
|
|
350
|
-
// Prompt to open
|
|
66
|
+
// Prompt to open preferred agent (only in interactive TTY)
|
|
351
67
|
if (process.stdout.isTTY) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
68
|
+
const preferred = await getPreferredLaunchAgent();
|
|
69
|
+
if (preferred) {
|
|
70
|
+
console.error("");
|
|
71
|
+
console.error(` Open project in ${preferred.definition.name}?`);
|
|
72
|
+
console.error("");
|
|
73
|
+
const choice = await promptSelect(["Yes", "No"]);
|
|
74
|
+
|
|
75
|
+
if (choice === 0) {
|
|
76
|
+
// Ensure terminal is in normal state before handoff
|
|
77
|
+
if (process.stdin.isTTY) {
|
|
78
|
+
process.stdin.setRawMode(false);
|
|
79
|
+
}
|
|
356
80
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
81
|
+
const launchResult = await launchAgent(preferred.launch, result.targetDir);
|
|
82
|
+
if (!launchResult.success) {
|
|
83
|
+
output.warn(`Failed to launch ${preferred.definition.name}`);
|
|
84
|
+
if (launchResult.command?.length) {
|
|
85
|
+
output.info(`Run manually: ${launchResult.command.join(" ")}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
console.error("");
|
|
91
|
+
output.info("No launchable AI agent detected");
|
|
92
|
+
output.info("Run: jack agents scan");
|
|
363
93
|
}
|
|
364
94
|
}
|
|
365
95
|
}
|