@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.
@@ -0,0 +1,315 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { select } from "@inquirer/prompts";
4
+ import { formatSize } from "../lib/format.ts";
5
+ import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts";
6
+ import {
7
+ type Project,
8
+ getProject,
9
+ getProjectDatabaseName,
10
+ updateProjectDatabase,
11
+ } from "../lib/registry.ts";
12
+ import {
13
+ deleteDatabase,
14
+ exportDatabase,
15
+ generateExportFilename,
16
+ getDatabaseInfo,
17
+ } from "../lib/services/db.ts";
18
+ import { getProjectNameFromDir } from "../lib/storage/index.ts";
19
+
20
+ /**
21
+ * Get database name from wrangler.jsonc/toml file
22
+ * Fallback when registry doesn't have the info
23
+ */
24
+ async function getDatabaseFromWranglerConfig(projectPath: string): Promise<string | null> {
25
+ // Try wrangler.jsonc first
26
+ const jsoncPath = join(projectPath, "wrangler.jsonc");
27
+ if (existsSync(jsoncPath)) {
28
+ try {
29
+ const content = await Bun.file(jsoncPath).text();
30
+ // Remove comments for JSON parsing (simple approach)
31
+ const jsonContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
32
+ const config = JSON.parse(jsonContent);
33
+ if (config.d1_databases?.[0]?.database_name) {
34
+ return config.d1_databases[0].database_name;
35
+ }
36
+ } catch {
37
+ // Ignore parse errors
38
+ }
39
+ }
40
+
41
+ // Try wrangler.toml
42
+ const tomlPath = join(projectPath, "wrangler.toml");
43
+ if (existsSync(tomlPath)) {
44
+ try {
45
+ const content = await Bun.file(tomlPath).text();
46
+ // Simple regex to extract database_name from [[d1_databases]] section
47
+ const match = content.match(/database_name\s*=\s*"([^"]+)"/);
48
+ if (match?.[1]) {
49
+ return match[1];
50
+ }
51
+ } catch {
52
+ // Ignore read errors
53
+ }
54
+ }
55
+
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * Get database name for a project, with fallback to wrangler config
61
+ */
62
+ async function resolveDbName(project: Project): Promise<string | null> {
63
+ // First check registry
64
+ const dbFromRegistry = getProjectDatabaseName(project);
65
+ if (dbFromRegistry) {
66
+ return dbFromRegistry;
67
+ }
68
+
69
+ // Fallback: read from wrangler config file
70
+ if (project.localPath && existsSync(project.localPath)) {
71
+ return await getDatabaseFromWranglerConfig(project.localPath);
72
+ }
73
+
74
+ return null;
75
+ }
76
+
77
+ interface ServiceOptions {
78
+ project?: string;
79
+ }
80
+
81
+ export default async function services(
82
+ subcommand?: string,
83
+ args: string[] = [],
84
+ options: ServiceOptions = {},
85
+ ): Promise<void> {
86
+ if (!subcommand) {
87
+ return showHelp();
88
+ }
89
+
90
+ switch (subcommand) {
91
+ case "db":
92
+ return await dbCommand(args, options);
93
+ default:
94
+ error(`Unknown service: ${subcommand}`);
95
+ info("Available: db");
96
+ process.exit(1);
97
+ }
98
+ }
99
+
100
+ function showHelp(): void {
101
+ console.error("");
102
+ info("jack services - Manage project services");
103
+ console.error("");
104
+ console.error("Commands:");
105
+ console.error(" db Manage database");
106
+ console.error("");
107
+ console.error("Run 'jack services <command>' for more information.");
108
+ console.error("");
109
+ }
110
+
111
+ async function dbCommand(args: string[], options: ServiceOptions): Promise<void> {
112
+ const action = args[0] || "info"; // Default to info
113
+
114
+ switch (action) {
115
+ case "info":
116
+ return await dbInfo(options);
117
+ case "export":
118
+ return await dbExport(options);
119
+ case "delete":
120
+ return await dbDelete(options);
121
+ default:
122
+ error(`Unknown action: ${action}`);
123
+ info("Available: info, export, delete");
124
+ process.exit(1);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Resolve project name from options or current directory
130
+ */
131
+ async function resolveProjectName(options: ServiceOptions): Promise<string> {
132
+ if (options.project) {
133
+ return options.project;
134
+ }
135
+
136
+ try {
137
+ return await getProjectNameFromDir(process.cwd());
138
+ } catch {
139
+ error("Could not determine project");
140
+ info("Run from a project directory, or use --project <name>");
141
+ process.exit(1);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Show database information
147
+ */
148
+ async function dbInfo(options: ServiceOptions): Promise<void> {
149
+ const projectName = await resolveProjectName(options);
150
+ const project = await getProject(projectName);
151
+
152
+ if (!project) {
153
+ error(`Project "${projectName}" not found in registry`);
154
+ info("List projects with: jack projects list");
155
+ process.exit(1);
156
+ }
157
+
158
+ const dbName = await resolveDbName(project);
159
+
160
+ if (!dbName) {
161
+ console.error("");
162
+ info("No database configured for this project");
163
+ console.error("");
164
+ return;
165
+ }
166
+
167
+ // Fetch database info
168
+ outputSpinner.start("Fetching database info...");
169
+ const dbInfo = await getDatabaseInfo(dbName);
170
+ outputSpinner.stop();
171
+
172
+ if (!dbInfo) {
173
+ console.error("");
174
+ error("Database not found");
175
+ info("It may have been deleted");
176
+ console.error("");
177
+ process.exit(1);
178
+ }
179
+
180
+ // Display info
181
+ console.error("");
182
+ success(`Database: ${dbInfo.name}`);
183
+ console.error("");
184
+ item(`Size: ${formatSize(dbInfo.sizeBytes)}`);
185
+ item(`Tables: ${dbInfo.numTables}`);
186
+ item(`ID: ${dbInfo.id}`);
187
+ console.error("");
188
+ }
189
+
190
+ /**
191
+ * Export database to SQL file
192
+ */
193
+ async function dbExport(options: ServiceOptions): Promise<void> {
194
+ const projectName = await resolveProjectName(options);
195
+ const project = await getProject(projectName);
196
+
197
+ if (!project) {
198
+ error(`Project "${projectName}" not found in registry`);
199
+ info("List projects with: jack projects list");
200
+ process.exit(1);
201
+ }
202
+
203
+ const dbName = await resolveDbName(project);
204
+
205
+ if (!dbName) {
206
+ console.error("");
207
+ info("No database configured for this project");
208
+ console.error("");
209
+ return;
210
+ }
211
+
212
+ // Generate filename
213
+ const filename = generateExportFilename(dbName);
214
+
215
+ // Determine output directory (project dir if in it, cwd otherwise)
216
+ let outputDir = process.cwd();
217
+ if (project.localPath && existsSync(project.localPath)) {
218
+ // Check if we're in the project directory or subdirectory
219
+ const cwd = process.cwd();
220
+ if (cwd === project.localPath || cwd.startsWith(`${project.localPath}/`)) {
221
+ outputDir = project.localPath;
222
+ }
223
+ }
224
+
225
+ const outputPath = join(outputDir, filename);
226
+
227
+ // Export
228
+ outputSpinner.start("Exporting database...");
229
+ try {
230
+ await exportDatabase(dbName, outputPath);
231
+ outputSpinner.stop();
232
+
233
+ console.error("");
234
+ success(`Exported to ./${filename}`);
235
+ console.error("");
236
+ } catch (err) {
237
+ outputSpinner.stop();
238
+ console.error("");
239
+ error(`Failed to export: ${err instanceof Error ? err.message : String(err)}`);
240
+ process.exit(1);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Delete database with full cleanup
246
+ */
247
+ async function dbDelete(options: ServiceOptions): Promise<void> {
248
+ const projectName = await resolveProjectName(options);
249
+ const project = await getProject(projectName);
250
+
251
+ if (!project) {
252
+ error(`Project "${projectName}" not found in registry`);
253
+ info("List projects with: jack projects list");
254
+ process.exit(1);
255
+ }
256
+
257
+ const dbName = await resolveDbName(project);
258
+
259
+ if (!dbName) {
260
+ console.error("");
261
+ info("No database configured for this project");
262
+ console.error("");
263
+ return;
264
+ }
265
+
266
+ // Get database info to show what will be deleted
267
+ outputSpinner.start("Fetching database info...");
268
+ const dbInfo = await getDatabaseInfo(dbName);
269
+ outputSpinner.stop();
270
+
271
+ // Show what will be deleted
272
+ console.error("");
273
+ info(`Database: ${dbName}`);
274
+ if (dbInfo) {
275
+ item(`Size: ${formatSize(dbInfo.sizeBytes)}`);
276
+ item(`Tables: ${dbInfo.numTables}`);
277
+ }
278
+ console.error("");
279
+ warn("This will permanently delete the database and all its data");
280
+ console.error("");
281
+
282
+ // Confirm deletion
283
+ console.error(" Esc to skip\n");
284
+ const action = await select({
285
+ message: `Delete database '${dbName}'?`,
286
+ choices: [
287
+ { name: "1. Yes", value: "yes" },
288
+ { name: "2. No", value: "no" },
289
+ ],
290
+ });
291
+
292
+ if (action === "no") {
293
+ info("Cancelled");
294
+ return;
295
+ }
296
+
297
+ // Delete database
298
+ outputSpinner.start("Deleting database...");
299
+ try {
300
+ await deleteDatabase(dbName);
301
+ outputSpinner.stop();
302
+
303
+ // Update registry (set db to null in services structure)
304
+ await updateProjectDatabase(projectName, null);
305
+
306
+ console.error("");
307
+ success("Database deleted");
308
+ console.error("");
309
+ } catch (err) {
310
+ outputSpinner.stop();
311
+ console.error("");
312
+ error(`Failed to delete: ${err instanceof Error ? err.message : String(err)}`);
313
+ process.exit(1);
314
+ }
315
+ }
@@ -1,150 +1,44 @@
1
- import { existsSync } from "node:fs";
2
- import { basename } from "node:path";
3
- import { $ } from "bun";
4
- import { getSyncConfig } from "../lib/config.ts";
5
- import { detectSecrets } from "../lib/env-parser.ts";
6
- import { error, info, spinner, success, warn } from "../lib/output.ts";
7
- import { filterNewSecrets, promptSaveSecrets } from "../lib/prompts.ts";
8
- import { applySchema, getD1DatabaseName, hasD1Config } from "../lib/schema.ts";
9
- import { getProjectNameFromDir, syncToCloud } from "../lib/storage/index.ts";
10
-
11
- function hasWranglerConfig(): boolean {
12
- return (
13
- existsSync("./wrangler.toml") || existsSync("./wrangler.jsonc") || existsSync("./wrangler.json")
14
- );
15
- }
16
-
17
- function isViteProject(): boolean {
18
- return (
19
- existsSync("./vite.config.ts") ||
20
- existsSync("./vite.config.js") ||
21
- existsSync("./vite.config.mjs")
22
- );
23
- }
24
-
25
- async function getProjectName(): Promise<string> {
26
- const configPaths = ["./wrangler.jsonc", "./wrangler.json", "./wrangler.toml"];
27
-
28
- for (const configPath of configPaths) {
29
- if (existsSync(configPath)) {
30
- try {
31
- const content = await Bun.file(configPath).text();
32
- // For JSON/JSONC - strip comments and parse
33
- if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
34
- const cleaned = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
35
- const config = JSON.parse(cleaned);
36
- if (config.name) return config.name;
37
- }
38
- // For TOML
39
- if (configPath.endsWith(".toml")) {
40
- const match = content.match(/name\s*=\s*"([^"]+)"/);
41
- if (match?.[1]) return match[1];
42
- }
43
- } catch {}
44
- }
45
- }
46
-
47
- // Fallback to directory name
48
- return basename(process.cwd());
49
- }
1
+ import { getErrorDetails } from "../lib/errors.ts";
2
+ import { output, spinner } from "../lib/output.ts";
3
+ import { deployProject } from "../lib/project-operations.ts";
50
4
 
51
5
  export default async function ship(): Promise<void> {
52
- if (!hasWranglerConfig()) {
53
- error("No wrangler config found in current directory");
54
- error("Run: jack new <project-name>");
55
- process.exit(1);
56
- }
57
-
58
- // For Vite projects, build first
59
- if (isViteProject()) {
60
- const buildSpin = spinner("Building...");
61
- const buildResult = await $`npx vite build`.nothrow().quiet();
62
-
63
- if (buildResult.exitCode !== 0) {
64
- buildSpin.error("Build failed");
65
- console.error(buildResult.stderr.toString());
66
- process.exit(buildResult.exitCode);
67
- }
68
- buildSpin.success("Built");
69
- }
70
-
71
- const spin = spinner("Deploying...");
72
-
73
- const result = await $`wrangler deploy`.nothrow().quiet();
74
-
75
- if (result.exitCode !== 0) {
76
- spin.error("Deploy failed");
77
- console.error(result.stderr.toString());
78
- process.exit(result.exitCode);
79
- }
80
-
81
- // Parse URL from output
82
- const output = result.stdout.toString();
83
- const urlMatch = output.match(/https:\/\/[\w-]+\.[\w-]+\.workers\.dev/);
84
-
85
- if (urlMatch) {
86
- spin.success(`Live: ${urlMatch[0]}`);
87
- } else {
88
- spin.success("Deployed");
89
- console.error(output);
90
- }
91
-
92
- // Update registry after successful deploy
6
+ const isCi = process.env.CI === "true" || process.env.CI === "1";
93
7
  try {
94
- const { registerProject } = await import("../lib/registry.ts");
95
- const projectName = await getProjectNameFromDir(process.cwd());
96
-
97
- await registerProject(projectName, {
98
- localPath: process.cwd(),
99
- workerUrl: urlMatch ? urlMatch[0] : null,
100
- lastDeployed: new Date().toISOString(),
8
+ const result = await deployProject({
9
+ projectPath: process.cwd(),
10
+ reporter: {
11
+ start: output.start,
12
+ stop: output.stop,
13
+ spinner,
14
+ info: output.info,
15
+ warn: output.warn,
16
+ error: output.error,
17
+ success: output.success,
18
+ box: output.box,
19
+ },
20
+ interactive: !isCi,
21
+ includeSecrets: true,
22
+ includeSync: true,
101
23
  });
102
- } catch {
103
- // Don't fail the deploy if registry update fails
104
- }
105
24
 
106
- // Apply schema.sql after deploy (database auto-provisioned by wrangler)
107
- const projectDir = process.cwd();
108
- if (await hasD1Config(projectDir)) {
109
- const dbName = await getD1DatabaseName(projectDir);
110
- if (dbName) {
111
- try {
112
- await applySchema(dbName, projectDir);
113
- } catch (err) {
114
- warn(`Schema application failed: ${err}`);
115
- info("Run manually: bun run db:migrate");
116
- }
25
+ if (!result.workerUrl && result.deployOutput) {
26
+ console.error(result.deployOutput);
27
+ }
28
+ } catch (error) {
29
+ const details = getErrorDetails(error);
30
+ if (!details.meta?.reported) {
31
+ output.error(details.message);
117
32
  }
118
- }
119
-
120
- // Detect and offer to save secrets
121
- const detected = await detectSecrets();
122
- const newSecrets = await filterNewSecrets(detected);
123
33
 
124
- if (newSecrets.length > 0) {
125
- await promptSaveSecrets(newSecrets);
126
- }
34
+ if (details.meta?.stderr) {
35
+ console.error(details.meta.stderr);
36
+ }
127
37
 
128
- // Auto-sync to cloud storage
129
- const syncConfig = await getSyncConfig();
130
- if (syncConfig.enabled && syncConfig.autoSync) {
131
- const syncSpin = spinner("Syncing source to cloud...");
132
- try {
133
- const projectName = await getProjectNameFromDir(process.cwd());
134
- const result = await syncToCloud(process.cwd());
135
- if (result.success) {
136
- if (result.filesUploaded > 0 || result.filesDeleted > 0) {
137
- syncSpin.success(
138
- `Backed up ${result.filesUploaded} files to jack-storage/${projectName}/`,
139
- );
140
- } else {
141
- syncSpin.success("Source already synced");
142
- }
143
- }
144
- } catch (err) {
145
- syncSpin.stop();
146
- warn("Cloud sync failed (deploy succeeded)");
147
- info("Run: jack sync");
38
+ if (details.suggestion && !details.meta?.reported) {
39
+ output.info(details.suggestion);
148
40
  }
41
+
42
+ process.exit(details.meta?.exitCode ?? 1);
149
43
  }
150
44
  }
package/src/index.ts CHANGED
@@ -20,9 +20,11 @@ const cli = meow(
20
20
  sync Sync to cloud storage
21
21
  clone <project> Pull project from cloud
22
22
  cloud Manage cloud storage
23
- down [name] Shut down a deployed worker
23
+ down [name] Undeploy from cloud
24
24
  open [name] Open project in browser
25
25
  projects Manage project registry
26
+ services Manage project services
27
+ mcp serve Start MCP server for AI agents
26
28
  telemetry Manage anonymous usage data
27
29
  about The story behind jack
28
30
 
@@ -37,12 +39,13 @@ const cli = meow(
37
39
  --dry-run Preview changes without applying
38
40
  --force Force operation
39
41
  --as <name> Clone to different directory name
40
- --dash Open Cloudflare dashboard
42
+ --dash Open cloud dashboard
41
43
  --logs Open logs page
42
44
  --yes Skip confirmation prompts
43
45
  --local Filter by local projects
44
46
  --deployed Filter by deployed projects
45
47
  --cloud Filter by cloud-backed projects
48
+ --skip-mcp Skip MCP config installation during init
46
49
 
47
50
  Examples
48
51
  $ jack init Set up once
@@ -100,6 +103,14 @@ const cli = meow(
100
103
  type: "boolean",
101
104
  default: false,
102
105
  },
106
+ project: {
107
+ type: "string",
108
+ shortFlag: "p",
109
+ },
110
+ skipMcp: {
111
+ type: "boolean",
112
+ default: false,
113
+ },
103
114
  },
104
115
  },
105
116
  );
@@ -123,7 +134,7 @@ identify({
123
134
  switch (command) {
124
135
  case "init": {
125
136
  const { default: init } = await import("./commands/init.ts");
126
- await withTelemetry("init", init)();
137
+ await withTelemetry("init", init)({ skipMcp: cli.flags.skipMcp });
127
138
  break;
128
139
  }
129
140
  case "new":
@@ -207,6 +218,19 @@ switch (command) {
207
218
  await withTelemetry("projects", projects)(args[0], projectArgs);
208
219
  break;
209
220
  }
221
+ case "services": {
222
+ const { default: services } = await import("./commands/services.ts");
223
+ await withTelemetry("services", services)(args[0], args.slice(1), {
224
+ project: cli.flags.project,
225
+ });
226
+ break;
227
+ }
228
+ case "mcp": {
229
+ const { default: mcp } = await import("./commands/mcp.ts");
230
+ // Note: Don't use withTelemetry wrapper for MCP serve - it runs indefinitely
231
+ await mcp(args[0], { project: cli.flags.project });
232
+ break;
233
+ }
210
234
  case "ls": {
211
235
  const { default: projects } = await import("./commands/projects.ts");
212
236
  const lsArgs: string[] = [];
@@ -50,43 +50,6 @@ Never run \`wrangler\` commands directly. All deployment is handled by jack.
50
50
  `;
51
51
  }
52
52
 
53
- /**
54
- * Template for .cursorrules
55
- */
56
- function generateCursorrules(): string {
57
- return `# Cursor Rules
58
-
59
- See AGENTS.md for project context and deployment instructions.
60
-
61
- ## Deployment Commands
62
-
63
- - Deploy: \`jack ship\`
64
- - Logs: \`jack logs\`
65
- - Dev server: \`jack dev\`
66
-
67
- Do not suggest \`wrangler\` commands - all deployment is handled by jack.
68
- `;
69
- }
70
-
71
- /**
72
- * Template for .windsurfrules
73
- */
74
- function generateWindsurfrules(): string {
75
- return `# Windsurf Rules
76
-
77
- See AGENTS.md for project context and deployment instructions.
78
-
79
- ## Deployment
80
-
81
- This project uses jack for deployment:
82
- - \`jack ship\` - Deploy to production
83
- - \`jack logs\` - View logs
84
- - \`jack dev\` - Local development
85
-
86
- Never suggest wrangler commands directly.
87
- `;
88
- }
89
-
90
53
  /**
91
54
  * Generate content for a specific template type
92
55
  */
@@ -100,10 +63,6 @@ function generateFileContent(
100
63
  return generateAgentsMd(projectName, template);
101
64
  case "claude-md":
102
65
  return generateClaudeMd();
103
- case "cursorrules":
104
- return generateCursorrules();
105
- case "windsurfrules":
106
- return generateWindsurfrules();
107
66
  default:
108
67
  throw new Error(`Unknown template type: ${templateType}`);
109
68
  }