@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,365 +1,95 @@
1
- import { existsSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- import { generateAgentFiles } from "../lib/agent-files.ts";
4
- import { getActiveAgents, validateAgentPaths } from "../lib/agents.ts";
5
- import { debug, printTimingSummary, time } from "../lib/debug.ts";
6
- import { generateEnvFile, generateSecretsJson } from "../lib/env-parser.ts";
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
- // Check if jack init was run
27
- const initDuration = await time("Check init", async () => {
28
- const initialized = await isInitialized();
29
- if (!initialized) {
30
- output.stop();
31
- output.error("jack is not set up yet");
32
- output.info("Run: jack init");
33
- process.exit(1);
34
- }
35
- });
36
- timings.push({ label: "Check init", duration: initDuration });
37
-
38
- const projectName = name ?? generateProjectName();
39
- const targetDir = resolve(projectName);
40
- debug("Project details", { projectName, targetDir });
41
-
42
- // Check directory doesn't exist
43
- if (existsSync(targetDir)) {
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
- output.error(`Directory ${projectName} already exists`);
46
- process.exit(1);
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
- const rendered = renderTemplate(template!, { name: projectName });
70
-
71
- // Handle template-specific secrets (omakase: auto-use saved, fail if missing)
72
- const secretsToUse: Record<string, string> = {};
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
- const gitignorePath = join(targetDir, ".gitignore");
123
- const gitignoreExists = existsSync(gitignorePath);
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
- // Filter out invalid agents
155
- activeAgents = activeAgents.filter(
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
- // Auto-install dependencies
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
- // Run pre-deploy hooks (e.g., check required secrets)
203
- if (template!.hooks?.preDeploy?.length) {
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 Claude Code (only in interactive TTY)
66
+ // Prompt to open preferred agent (only in interactive TTY)
351
67
  if (process.stdout.isTTY) {
352
- console.error("");
353
- console.error(" Open project in Claude Code?");
354
- console.error("");
355
- const choice = await promptSelect(["Yes", "No"]);
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
- if (choice === 0) {
358
- const claude = Bun.spawn(["claude"], {
359
- cwd: targetDir,
360
- stdio: ["inherit", "inherit", "inherit"],
361
- });
362
- await claude.exited;
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
  }