@getjack/jack 0.1.7 → 0.1.9

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/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Ship before you forget why you started. The vibecoder's deployment CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "jack": "./src/index.ts"
9
9
  },
10
- "files": ["src", "templates"],
10
+ "files": [
11
+ "src",
12
+ "templates"
13
+ ],
11
14
  "engines": {
12
15
  "bun": ">=1.0.0"
13
16
  },
@@ -62,6 +62,7 @@ export interface DownFlags {
62
62
  export default async function down(projectName?: string, flags: DownFlags = {}): Promise<void> {
63
63
  try {
64
64
  // Get project name
65
+ const hasExplicitName = Boolean(projectName);
65
66
  let name = projectName;
66
67
  if (!name) {
67
68
  try {
@@ -74,10 +75,12 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
74
75
  }
75
76
 
76
77
  // Resolve project from all sources (local link + control plane)
77
- const resolved = await resolveProject(name);
78
+ const resolved = await resolveProject(name, {
79
+ preferLocalLink: !hasExplicitName,
80
+ });
78
81
 
79
- // Read local project link
80
- const link = await readProjectLink(process.cwd());
82
+ // Read local project link (only when no explicit name provided)
83
+ const link = hasExplicitName ? null : await readProjectLink(process.cwd());
81
84
 
82
85
  // Check if found only on control plane (orphaned managed project)
83
86
  if (resolved?.sources.controlPlane && !resolved.sources.filesystem) {
@@ -85,6 +88,20 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
85
88
  info(`Found "${name}" on jack cloud, linking locally...`);
86
89
  }
87
90
 
91
+
92
+ // Guard against mismatched resolutions when an explicit name is provided
93
+ if (hasExplicitName && resolved) {
94
+ const matches =
95
+ name === resolved.slug ||
96
+ name === resolved.name ||
97
+ name === resolved.remote?.projectId;
98
+ if (!matches) {
99
+ error(`Refusing to undeploy '${name}' because it resolves to '${resolved.slug}'.`);
100
+ info("Use the exact slug/name shown by 'jack info' and try again.");
101
+ process.exit(1);
102
+ }
103
+ }
104
+
88
105
  if (!resolved && !link) {
89
106
  // Not found anywhere
90
107
  warn(`Project '${name}' not found`);
@@ -6,7 +6,8 @@ import {
6
6
  getCurrentUserProfile,
7
7
  setUsername,
8
8
  } from "../lib/control-plane.ts";
9
- import { error, info, spinner, success, warn } from "../lib/output.ts";
9
+ import { celebrate, error, info, spinner, success, warn } from "../lib/output.ts";
10
+ import { identifyUser } from "../lib/telemetry.ts";
10
11
 
11
12
  interface LoginOptions {
12
13
  /** Skip the initial "Logging in..." message (used when called from auto-login) */
@@ -31,13 +32,7 @@ export default async function login(options: LoginOptions = {}): Promise<void> {
31
32
  process.exit(1);
32
33
  }
33
34
 
34
- console.error("");
35
- console.error(" ┌────────────────────────────────────┐");
36
- console.error(" │ │");
37
- console.error(` │ Your code: ${deviceAuth.user_code.padEnd(12)} │`);
38
- console.error(" │ │");
39
- console.error(" └────────────────────────────────────┘");
40
- console.error("");
35
+ celebrate("Your code:", [deviceAuth.user_code]);
41
36
  info(`Opening ${deviceAuth.verification_uri} in your browser...`);
42
37
  console.error("");
43
38
 
@@ -73,8 +68,12 @@ export default async function login(options: LoginOptions = {}): Promise<void> {
73
68
  };
74
69
  await saveCredentials(creds);
75
70
 
71
+ // Link user identity for cross-platform analytics
72
+ await identifyUser(tokens.user.id, { email: tokens.user.email });
73
+
76
74
  console.error("");
77
- success(`Logged in as ${tokens.user.email}`);
75
+ const displayName = tokens.user.first_name || "Logged in";
76
+ success(tokens.user.first_name ? `Welcome back, ${displayName}` : displayName);
78
77
 
79
78
  // Prompt for username if not set
80
79
  await promptForUsername(tokens.user.email);
@@ -209,3 +208,4 @@ function normalizeToUsername(input: string): string {
209
208
  .replace(/^-+|-+$/g, "")
210
209
  .slice(0, 39);
211
210
  }
211
+
@@ -1,7 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { rm, mkdtemp } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
2
6
  import { error, info, success } from "../lib/output.ts";
3
7
  import { startMcpServer } from "../mcp/server.ts";
4
8
 
9
+ const cliRoot = fileURLToPath(new URL("../..", import.meta.url));
10
+
5
11
  interface McpOptions {
6
12
  project?: string;
7
13
  debug?: boolean;
@@ -32,10 +38,19 @@ export default async function mcp(subcommand?: string, options: McpOptions = {})
32
38
  * Test MCP server by spawning it and sending test requests
33
39
  */
34
40
  async function testMcpServer(): Promise<void> {
41
+ const configDir = await mkdtemp(join(tmpdir(), "jack-config-"));
42
+
35
43
  info("Testing MCP server...\n");
36
44
 
37
- const proc = spawn("./src/index.ts", ["mcp", "serve"], {
45
+ const proc = spawn("bun", ["run", "src/index.ts", "mcp", "serve"], {
38
46
  stdio: ["pipe", "pipe", "pipe"],
47
+ cwd: cliRoot,
48
+ env: {
49
+ ...process.env,
50
+ CI: "1",
51
+ JACK_TELEMETRY_DISABLED: "1",
52
+ JACK_CONFIG_DIR: configDir,
53
+ },
39
54
  });
40
55
 
41
56
  const results: { test: string; passed: boolean; error?: string }[] = [];
@@ -126,6 +141,7 @@ async function testMcpServer(): Promise<void> {
126
141
  error(` ✗ Error: ${errorMsg}`);
127
142
  } finally {
128
143
  proc.kill();
144
+ await rm(configDir, { recursive: true, force: true });
129
145
  }
130
146
 
131
147
  // Summary
@@ -59,6 +59,7 @@ export default async function newProject(
59
59
  error: output.error,
60
60
  success: output.success,
61
61
  box: output.box,
62
+ celebrate: output.celebrate,
62
63
  },
63
64
  interactive: !isCi,
64
65
  managed: options.managed,
@@ -18,11 +18,7 @@ import {
18
18
  sortByUpdated,
19
19
  toListItems,
20
20
  } from "../lib/project-list.ts";
21
- import {
22
- cleanupStaleProjects,
23
- getProjectStatus,
24
- scanStaleProjects,
25
- } from "../lib/project-operations.ts";
21
+ import { cleanupStaleProjects, scanStaleProjects } from "../lib/project-operations.ts";
26
22
  import {
27
23
  type ResolvedProject,
28
24
  listAllProjects,
@@ -104,7 +100,7 @@ async function listProjects(args: string[]): Promise<void> {
104
100
  const cloudOnly = args.includes("--cloud");
105
101
 
106
102
  // Fetch all projects from registry and control plane
107
- outputSpinner.start("Checking project status...");
103
+ outputSpinner.start("Loading projects...");
108
104
  const projects: ResolvedProject[] = await listAllProjects();
109
105
  outputSpinner.stop();
110
106
 
@@ -154,6 +150,7 @@ async function listProjects(args: string[]): Promise<void> {
154
150
  function renderGroupedView(items: ProjectListItem[]): void {
155
151
  const groups = groupProjects(items);
156
152
  const total = items.length;
153
+ const CLOUD_LIMIT = 5;
157
154
 
158
155
  // Build consistent tag color map across all projects
159
156
  const tagColorMap = buildTagColorMap(items);
@@ -178,7 +175,6 @@ function renderGroupedView(items: ProjectListItem[]): void {
178
175
 
179
176
  // Section 3: Cloud-only (show last N by updatedAt)
180
177
  if (groups.cloudOnly.length > 0) {
181
- const CLOUD_LIMIT = 5;
182
178
  const sorted = sortByUpdated(groups.cloudOnly);
183
179
 
184
180
  console.error("");
@@ -191,9 +187,14 @@ function renderGroupedView(items: ProjectListItem[]): void {
191
187
  );
192
188
  }
193
189
 
194
- // Footer hint
190
+ // Footer hint - only show --all hint if there are hidden cloud projects
195
191
  console.error("");
196
- info("jack ls --all for full list, --status error to filter");
192
+ const hasHiddenCloudProjects = groups.cloudOnly.length > CLOUD_LIMIT;
193
+ if (hasHiddenCloudProjects) {
194
+ info(`jack ls --all to see all ${groups.cloudOnly.length} cloud projects`);
195
+ } else {
196
+ info("jack ls --status error to filter, --json for machine output");
197
+ }
197
198
  console.error("");
198
199
  }
199
200
 
@@ -247,6 +248,7 @@ function renderFlatTable(items: ProjectListItem[]): void {
247
248
  * Show detailed project info
248
249
  */
249
250
  async function infoProject(args: string[]): Promise<void> {
251
+ const hasExplicitName = Boolean(args[0]);
250
252
  let name = args[0];
251
253
 
252
254
  // If no name provided, try to get from cwd
@@ -261,80 +263,82 @@ async function infoProject(args: string[]): Promise<void> {
261
263
  }
262
264
  }
263
265
 
264
- // Check actual status (with spinner for API calls)
266
+ // Resolve project using the same pattern as down.ts
265
267
  outputSpinner.start("Fetching project info...");
266
- const status = await getProjectStatus(name);
268
+ const resolved = await resolveProject(name, {
269
+ preferLocalLink: !hasExplicitName,
270
+ includeResources: true,
271
+ });
267
272
  outputSpinner.stop();
268
273
 
269
- if (!status) {
270
- error(`Project "${name}" not found in registry`);
274
+ // Guard against mismatched resolutions when an explicit name is provided
275
+ if (hasExplicitName && resolved) {
276
+ const matches =
277
+ name === resolved.slug || name === resolved.name || name === resolved.remote?.projectId;
278
+ if (!matches) {
279
+ error(`Project '${name}' resolves to '${resolved.slug}'.`);
280
+ info("Use the exact slug/name and try again.");
281
+ process.exit(1);
282
+ }
283
+ }
284
+
285
+ if (!resolved) {
286
+ error(`Project "${name}" not found`);
271
287
  info("List projects with: jack projects list");
272
288
  process.exit(1);
273
289
  }
274
290
 
275
291
  console.error("");
276
- info(`Project: ${status.name}`);
292
+ info(`Project: ${resolved.name}`);
277
293
  console.error("");
278
294
 
279
295
  // Status section
280
296
  const statuses: string[] = [];
281
- if (status.local) {
297
+ if (resolved.sources.filesystem) {
282
298
  statuses.push("local");
283
299
  }
284
- if (status.deployed) {
300
+ if (resolved.status === "live") {
285
301
  statuses.push("deployed");
286
302
  }
287
- if (status.backedUp) {
288
- statuses.push("backup");
289
- }
290
303
 
291
304
  item(`Status: ${statuses.join(", ") || "none"}`);
292
305
  console.error("");
293
306
 
294
307
  // Workspace info (only shown if running from project directory)
295
- if (status.localPath) {
296
- item(`Workspace path: ${status.localPath}`);
308
+ if (resolved.localPath) {
309
+ item(`Workspace path: ${resolved.localPath}`);
297
310
  console.error("");
298
311
  }
299
312
 
300
313
  // Deployment info
301
- if (status.workerUrl) {
302
- item(`Worker URL: ${status.workerUrl}`);
303
- }
304
- if (status.lastDeployed) {
305
- item(`Last deployed: ${new Date(status.lastDeployed).toLocaleString()}`);
314
+ if (resolved.url) {
315
+ item(`Worker URL: ${resolved.url}`);
306
316
  }
307
- if (status.deployed) {
308
- console.error("");
317
+ if (resolved.updatedAt) {
318
+ item(`Last deployed: ${new Date(resolved.updatedAt).toLocaleString()}`);
309
319
  }
310
-
311
- // Backup info
312
- if (status.backedUp && status.backupFiles !== null) {
313
- item(`Backup: ${status.backupFiles} files`);
314
- if (status.backupLastSync) {
315
- item(`Last synced: ${new Date(status.backupLastSync).toLocaleString()}`);
316
- }
320
+ if (resolved.status === "live") {
317
321
  console.error("");
318
322
  }
319
323
 
320
324
  // Account info
321
- if (status.accountId) {
322
- item(`Account ID: ${status.accountId}`);
325
+ if (resolved.remote?.orgId) {
326
+ item(`Account ID: ${resolved.remote.orgId}`);
323
327
  }
324
- if (status.workerId) {
325
- item(`Worker ID: ${status.workerId}`);
328
+ if (resolved.slug) {
329
+ item(`Worker ID: ${resolved.slug}`);
326
330
  }
327
331
  console.error("");
328
332
 
329
333
  // Resources
330
- if (status.dbName) {
331
- item(`Database: ${status.dbName}`);
334
+ if (resolved.resources?.d1?.name) {
335
+ item(`Database: ${resolved.resources.d1.name}`);
332
336
  console.error("");
333
337
  }
334
338
 
335
339
  // Timestamps
336
- if (status.createdAt) {
337
- item(`Created: ${new Date(status.createdAt).toLocaleString()}`);
340
+ if (resolved.createdAt) {
341
+ item(`Created: ${new Date(resolved.createdAt).toLocaleString()}`);
338
342
  }
339
343
  console.error("");
340
344
  }
@@ -406,7 +410,7 @@ async function removeProjectEntry(args: string[]): Promise<void> {
406
410
  }
407
411
 
408
412
  // Use resolver to find project anywhere (registry OR control plane)
409
- outputSpinner.start("Checking project status...");
413
+ outputSpinner.start("Finding project...");
410
414
  const project = await resolveProject(name);
411
415
  outputSpinner.stop();
412
416
 
@@ -21,7 +21,6 @@ This project is deployed to Cloudflare Workers using jack:
21
21
  \`\`\`bash
22
22
  jack ship # Deploy to Cloudflare Workers
23
23
  jack logs # Stream production logs
24
- jack dev # Start local development server
25
24
  \`\`\`
26
25
 
27
26
  All deployment is handled by jack. Never run \`wrangler\` commands directly.
@@ -42,7 +41,6 @@ See [AGENTS.md](./AGENTS.md) for complete project context and deployment instruc
42
41
 
43
42
  - **Deploy**: \`jack ship\` - Deploy to Cloudflare Workers
44
43
  - **Logs**: \`jack logs\` - Stream production logs
45
- - **Dev**: \`jack dev\` - Start local development server
46
44
 
47
45
  ## Important
48
46
 
@@ -172,19 +172,19 @@ export async function buildProject(options: BuildOptions): Promise<BuildOutput>
172
172
  // Check if OpenNext build is needed (Next.js + Cloudflare)
173
173
  const hasOpenNext = await needsOpenNextBuild(projectPath);
174
174
  if (hasOpenNext) {
175
- reporter?.start("Building...");
175
+ reporter?.start("Building assets...");
176
176
  await runOpenNextBuild(projectPath);
177
177
  reporter?.stop();
178
- reporter?.success("Built");
178
+ reporter?.success("Built assets");
179
179
  }
180
180
 
181
181
  // Check if Vite build is needed and run it (skip if OpenNext already built)
182
182
  const hasVite = await needsViteBuild(projectPath);
183
183
  if (hasVite && !hasOpenNext) {
184
- reporter?.start("Building...");
184
+ reporter?.start("Building assets...");
185
185
  await runViteBuild(projectPath);
186
186
  reporter?.stop();
187
- reporter?.success("Built");
187
+ reporter?.success("Built assets");
188
188
  }
189
189
 
190
190
  // Create unique temp directory for build output
@@ -193,7 +193,7 @@ export async function buildProject(options: BuildOptions): Promise<BuildOutput>
193
193
  await mkdir(outDir, { recursive: true });
194
194
 
195
195
  // Run wrangler dry-run to build without deploying
196
- reporter?.start("Building worker...");
196
+ reporter?.start("Bundling runtime...");
197
197
 
198
198
  const dryRunResult = await $`wrangler deploy --dry-run --outdir=${outDir}`
199
199
  .cwd(projectPath)
@@ -215,7 +215,7 @@ export async function buildProject(options: BuildOptions): Promise<BuildOutput>
215
215
  }
216
216
 
217
217
  reporter?.stop();
218
- reporter?.success("Built worker");
218
+ reporter?.success("Bundled runtime");
219
219
 
220
220
  const entrypoint = await resolveEntrypoint(outDir, config.main);
221
221
 
@@ -33,6 +33,19 @@ export function generateWranglerConfig(
33
33
  };
34
34
 
35
35
  case "vite":
36
+ // Check if this is a Vite + Worker hybrid (has entryPoint)
37
+ if (entryPoint) {
38
+ // Hybrid mode: Vite frontend + custom Worker backend
39
+ return {
40
+ name: projectName,
41
+ main: entryPoint,
42
+ compatibility_date: COMPATIBILITY_DATE,
43
+ assets: {
44
+ directory: "./dist",
45
+ binding: "ASSETS",
46
+ },
47
+ };
48
+ }
36
49
  // Pure Vite SPAs use assets-only mode (no worker entry)
37
50
  // Cloudflare auto-generates a worker that serves static files
38
51
  return {
package/src/lib/config.ts CHANGED
@@ -40,7 +40,8 @@ export interface JackConfig {
40
40
  sync?: SyncConfig;
41
41
  }
42
42
 
43
- export const CONFIG_DIR = join(homedir(), ".config", "jack");
43
+ const DEFAULT_CONFIG_DIR = join(homedir(), ".config", "jack");
44
+ export const CONFIG_DIR = process.env.JACK_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
44
45
  export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
45
46
 
46
47
  /**