@getjack/jack 0.1.14 → 0.1.15

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
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",
@@ -41,7 +41,6 @@
41
41
  "bun-types": "latest"
42
42
  },
43
43
  "dependencies": {
44
- "@inquirer/prompts": "^7.0.0",
45
44
  "@modelcontextprotocol/sdk": "^1.25.1",
46
45
  "archiver": "^7.0.1",
47
46
  "fflate": "^0.8.2",
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { select } from "@inquirer/prompts";
3
+ import { isCancel, select } from "@clack/prompts";
4
4
  import { fetchProjectTags } from "../lib/control-plane.ts";
5
5
  import { formatSize } from "../lib/format.ts";
6
6
  import { box, error, info, spinner, success } from "../lib/output.ts";
@@ -35,14 +35,14 @@ export default async function clone(projectName?: string, flags: CloneFlags = {}
35
35
  // Prompt user for action
36
36
  const action = await select({
37
37
  message: `Directory ${flags.as ?? projectName} already exists. What would you like to do?`,
38
- choices: [
39
- { value: "overwrite", name: "Overwrite (delete and recreate)" },
40
- { value: "merge", name: "Merge (keep existing files)" },
41
- { value: "cancel", name: "Cancel" },
38
+ options: [
39
+ { value: "overwrite", label: "Overwrite (delete and recreate)" },
40
+ { value: "merge", label: "Merge (keep existing files)" },
41
+ { value: "cancel", label: "Cancel" },
42
42
  ],
43
43
  });
44
44
 
45
- if (action === "cancel") {
45
+ if (isCancel(action) || action === "cancel") {
46
46
  info("Clone cancelled");
47
47
  process.exit(0);
48
48
  }
@@ -1,4 +1,5 @@
1
1
  import { join } from "node:path";
2
+ import { getAuthState } from "../lib/auth/index.ts";
2
3
  import {
3
4
  checkWorkerExists,
4
5
  deleteDatabase,
@@ -100,9 +101,26 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
100
101
  }
101
102
 
102
103
  if (!resolved && !link) {
103
- // Not found anywhere
104
- warn(`Project '${name}' not found`);
105
- info("Will attempt to undeploy if deployed");
104
+ // Project not found - provide auth-aware messaging
105
+ const authState = await getAuthState();
106
+
107
+ if (authState === "logged-in") {
108
+ // We're logged in and control plane didn't find it - definitely not a managed project
109
+ warn(`Project '${name}' not tracked by jack`);
110
+ info("Checking your Cloudflare account...");
111
+ } else if (authState === "session-expired") {
112
+ // Session expired - can't verify if this is a managed project
113
+ warn(`Project '${name}' not found locally`);
114
+ info("Session expired - can't check jack cloud");
115
+ info("If this was deployed via jack, run: jack login");
116
+ info("Checking your Cloudflare account...");
117
+ } else {
118
+ // Not logged in - can't verify if this is a managed project
119
+ warn(`Project '${name}' not found locally`);
120
+ info("Not logged in - can't check jack cloud");
121
+ info("If this was deployed via jack, run: jack login");
122
+ info("Checking your Cloudflare account...");
123
+ }
106
124
  }
107
125
 
108
126
  // Check if this is a managed project (from link or resolved data)
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { existsSync } from "node:fs";
11
- import { select } from "@inquirer/prompts";
11
+ import { isCancel, select } from "@clack/prompts";
12
12
  import { isLoggedIn } from "../lib/auth/index.ts";
13
13
  import {
14
14
  type ManagedProject,
@@ -127,12 +127,17 @@ export default async function link(projectName?: string, flags: LinkFlags = {}):
127
127
  console.error("");
128
128
  const choice = await select({
129
129
  message: "Select a project to link:",
130
- choices: projects.map((p) => ({
130
+ options: projects.map((p) => ({
131
131
  value: p.id,
132
- name: `${p.slug} (${p.status})`,
132
+ label: `${p.slug} (${p.status})`,
133
133
  })),
134
134
  });
135
135
 
136
+ if (isCancel(choice)) {
137
+ info("Cancelled");
138
+ process.exit(0);
139
+ }
140
+
136
141
  const selected = projects.find((p) => p.id === choice);
137
142
  if (!selected) {
138
143
  error("No project selected");
@@ -6,7 +6,7 @@
6
6
  * For BYO projects: uses wrangler secret commands.
7
7
  */
8
8
 
9
- import { password as passwordPrompt } from "@inquirer/prompts";
9
+ import { isCancel, password as passwordPrompt } from "@clack/prompts";
10
10
  import { $ } from "bun";
11
11
  import { getControlApiUrl } from "../lib/control-plane.ts";
12
12
  import { JackError, JackErrorCode } from "../lib/errors.ts";
@@ -96,22 +96,19 @@ async function resolveProjectContext(options: SecretsOptions): Promise<{
96
96
 
97
97
  /**
98
98
  * Read a secret value interactively without echoing
99
- * Uses @inquirer/prompts password for robust handling of typing, pasting, and TTY
99
+ * Uses @clack/prompts password for robust handling of typing, pasting, and TTY
100
100
  */
101
101
  async function readSecretInteractive(keyName: string): Promise<string> {
102
- try {
103
- const value = await passwordPrompt({
104
- message: `Enter value for ${keyName}`,
105
- mask: "*",
106
- });
107
- return value;
108
- } catch (err) {
109
- // Handle Ctrl+C / Escape - inquirer throws ExitPromptError
110
- if (err instanceof Error && err.name === "ExitPromptError") {
111
- throw new Error("Cancelled");
112
- }
113
- throw err;
102
+ const value = await passwordPrompt({
103
+ message: `Enter value for ${keyName}`,
104
+ mask: "*",
105
+ });
106
+
107
+ if (isCancel(value)) {
108
+ throw new Error("Cancelled");
114
109
  }
110
+
111
+ return value;
115
112
  }
116
113
 
117
114
  /**
@@ -1,10 +1,11 @@
1
1
  import { join } from "node:path";
2
2
  import { fetchProjectResources } from "../lib/control-plane.ts";
3
3
  import { formatSize } from "../lib/format.ts";
4
- import { promptSelect } from "../lib/hooks.ts";
5
4
  import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts";
6
5
  import { readProjectLink } from "../lib/project-link.ts";
7
6
  import { parseWranglerResources } from "../lib/resources.ts";
7
+ import { createDatabase } from "../lib/services/db-create.ts";
8
+ import { listDatabases } from "../lib/services/db-list.ts";
8
9
  import {
9
10
  deleteDatabase,
10
11
  exportDatabase,
@@ -12,6 +13,7 @@ import {
12
13
  getDatabaseInfo as getWranglerDatabaseInfo,
13
14
  } from "../lib/services/db.ts";
14
15
  import { getProjectNameFromDir } from "../lib/storage/index.ts";
16
+ import { Events, track } from "../lib/telemetry.ts";
15
17
 
16
18
  /**
17
19
  * Database info from control plane or wrangler config
@@ -115,19 +117,45 @@ function showHelp(): void {
115
117
  console.error("");
116
118
  }
117
119
 
120
+ function showDbHelp(): void {
121
+ console.error("");
122
+ info("jack services db - Manage databases");
123
+ console.error("");
124
+ console.error("Actions:");
125
+ console.error(" info Show database information (default)");
126
+ console.error(" create Create a new database");
127
+ console.error(" list List all databases in the project");
128
+ console.error(" export Export database to SQL file");
129
+ console.error(" delete Delete a database");
130
+ console.error("");
131
+ console.error("Examples:");
132
+ console.error(" jack services db Show info about the default database");
133
+ console.error(" jack services db create Create a new database");
134
+ console.error(" jack services db list List all databases");
135
+ console.error("");
136
+ }
137
+
118
138
  async function dbCommand(args: string[], options: ServiceOptions): Promise<void> {
119
139
  const action = args[0] || "info"; // Default to info
120
140
 
121
141
  switch (action) {
142
+ case "--help":
143
+ case "-h":
144
+ case "help":
145
+ return showDbHelp();
122
146
  case "info":
123
147
  return await dbInfo(options);
148
+ case "create":
149
+ return await dbCreate(args.slice(1), options);
150
+ case "list":
151
+ return await dbList(options);
124
152
  case "export":
125
153
  return await dbExport(options);
126
154
  case "delete":
127
155
  return await dbDelete(options);
128
156
  default:
129
157
  error(`Unknown action: ${action}`);
130
- info("Available: info, export, delete");
158
+ info("Available: info, create, list, export, delete");
131
159
  process.exit(1);
132
160
  }
133
161
  }
@@ -263,10 +291,12 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
263
291
  console.error("");
264
292
 
265
293
  // Confirm deletion
266
- console.error(` Delete database '${dbInfo.name}'?\n`);
267
- const choice = await promptSelect(["Yes", "No"]);
294
+ const { confirm } = await import("@clack/prompts");
295
+ const shouldDelete = await confirm({
296
+ message: `Delete database '${dbInfo.name}'?`,
297
+ });
268
298
 
269
- if (choice !== 0) {
299
+ if (shouldDelete !== true) {
270
300
  info("Cancelled");
271
301
  return;
272
302
  }
@@ -287,3 +317,116 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
287
317
  process.exit(1);
288
318
  }
289
319
  }
320
+
321
+ /**
322
+ * Parse --name flag from args
323
+ * Supports: --name foo, --name=foo
324
+ */
325
+ function parseNameFlag(args: string[]): string | undefined {
326
+ for (let i = 0; i < args.length; i++) {
327
+ const arg = args[i];
328
+ if (arg === "--name" && args[i + 1]) {
329
+ return args[i + 1];
330
+ }
331
+ if (arg.startsWith("--name=")) {
332
+ return arg.slice("--name=".length);
333
+ }
334
+ }
335
+ return undefined;
336
+ }
337
+
338
+ /**
339
+ * Create a new database
340
+ */
341
+ async function dbCreate(args: string[], options: ServiceOptions): Promise<void> {
342
+ // Parse --name flag
343
+ const name = parseNameFlag(args);
344
+
345
+ outputSpinner.start("Creating database...");
346
+ try {
347
+ const result = await createDatabase(process.cwd(), {
348
+ name,
349
+ interactive: true,
350
+ });
351
+ outputSpinner.stop();
352
+
353
+ // Track telemetry
354
+ track(Events.SERVICE_CREATED, {
355
+ service_type: "d1",
356
+ binding_name: result.bindingName,
357
+ created: result.created,
358
+ });
359
+
360
+ console.error("");
361
+ if (result.created) {
362
+ success(`Database created: ${result.databaseName}`);
363
+ } else {
364
+ success(`Using existing database: ${result.databaseName}`);
365
+ }
366
+ console.error("");
367
+ item(`Binding: ${result.bindingName}`);
368
+ item(`ID: ${result.databaseId}`);
369
+ console.error("");
370
+
371
+ // Prompt to deploy
372
+ const { confirm } = await import("@clack/prompts");
373
+ const shouldDeploy = await confirm({
374
+ message: "Deploy now?",
375
+ });
376
+
377
+ if (shouldDeploy === true) {
378
+ const { deployProject } = await import("../lib/project-operations.ts");
379
+ await deployProject(process.cwd(), { interactive: true });
380
+ } else {
381
+ console.error("");
382
+ info("Run 'jack ship' when ready to deploy");
383
+ console.error("");
384
+ }
385
+ } catch (err) {
386
+ outputSpinner.stop();
387
+ console.error("");
388
+ error(`Failed to create database: ${err instanceof Error ? err.message : String(err)}`);
389
+ process.exit(1);
390
+ }
391
+ }
392
+
393
+ /**
394
+ * List all databases in the project
395
+ */
396
+ async function dbList(options: ServiceOptions): Promise<void> {
397
+ outputSpinner.start("Fetching databases...");
398
+ try {
399
+ const databases = await listDatabases(process.cwd());
400
+ outputSpinner.stop();
401
+
402
+ if (databases.length === 0) {
403
+ console.error("");
404
+ info("No databases found in this project.");
405
+ console.error("");
406
+ info("Create one with: jack services db create");
407
+ console.error("");
408
+ return;
409
+ }
410
+
411
+ console.error("");
412
+ success(`Found ${databases.length} database${databases.length === 1 ? "" : "s"}:`);
413
+ console.error("");
414
+
415
+ for (const db of databases) {
416
+ item(`${db.name} (${db.binding})`);
417
+ if (db.sizeBytes !== undefined) {
418
+ item(` Size: ${formatSize(db.sizeBytes)}`);
419
+ }
420
+ if (db.numTables !== undefined) {
421
+ item(` Tables: ${db.numTables}`);
422
+ }
423
+ item(` ID: ${db.id}`);
424
+ console.error("");
425
+ }
426
+ } catch (err) {
427
+ outputSpinner.stop();
428
+ console.error("");
429
+ error(`Failed to list databases: ${err instanceof Error ? err.message : String(err)}`);
430
+ process.exit(1);
431
+ }
432
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * jack update - Self-update to the latest version
3
+ */
4
+
5
+ import { error, info, success, warn } from "../lib/output.ts";
6
+ import {
7
+ checkForUpdate,
8
+ getCurrentVersion,
9
+ isRunningViaBunx,
10
+ performUpdate,
11
+ } from "../lib/version-check.ts";
12
+
13
+ export default async function update(): Promise<void> {
14
+ const currentVersion = getCurrentVersion();
15
+
16
+ // Check if running via bunx
17
+ if (isRunningViaBunx()) {
18
+ info(`Running via bunx (current: v${currentVersion})`);
19
+ info("bunx automatically uses cached packages.");
20
+ info("To get the latest version, run:");
21
+ info(" bunx @getjack/jack@latest <command>");
22
+ info("");
23
+ info("Or install globally:");
24
+ info(" bun add -g @getjack/jack");
25
+ return;
26
+ }
27
+
28
+ info(`Current version: v${currentVersion}`);
29
+
30
+ // Check for updates
31
+ const latestVersion = await checkForUpdate();
32
+
33
+ if (!latestVersion) {
34
+ success("You're on the latest version!");
35
+ return;
36
+ }
37
+
38
+ info(`New version available: v${latestVersion}`);
39
+ info("Updating...");
40
+
41
+ const result = await performUpdate();
42
+
43
+ if (result.success) {
44
+ success(`Updated to v${result.version ?? latestVersion}`);
45
+ info("Restart your terminal to use the new version.");
46
+ } else {
47
+ error("Update failed");
48
+ if (result.error) {
49
+ warn(result.error);
50
+ }
51
+ info("Try manually: bun add -g @getjack/jack@latest");
52
+ }
53
+ }
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ const cli = meow(
35
35
  login Sign in
36
36
  logout Sign out
37
37
  whoami Show current user
38
+ update Update jack to latest version
38
39
 
39
40
  Project Management
40
41
  link [name] Link directory to a project
@@ -168,6 +169,20 @@ identify({
168
169
  ...getEnvironmentProps(),
169
170
  });
170
171
 
172
+ // Start non-blocking version check (skip for update command, CI, and help)
173
+ const skipVersionCheck =
174
+ !command ||
175
+ command === "update" ||
176
+ command === "upgrade" ||
177
+ process.env.CI ||
178
+ process.env.JACK_NO_UPDATE_CHECK;
179
+
180
+ let updateCheckPromise: Promise<string | null> | null = null;
181
+ if (!skipVersionCheck) {
182
+ const { checkForUpdate } = await import("./lib/version-check.ts");
183
+ updateCheckPromise = checkForUpdate().catch(() => null);
184
+ }
185
+
171
186
  try {
172
187
  switch (command) {
173
188
  case "init": {
@@ -344,6 +359,12 @@ try {
344
359
  await withTelemetry("whoami", whoami)();
345
360
  break;
346
361
  }
362
+ case "update":
363
+ case "upgrade": {
364
+ const { default: update } = await import("./commands/update.ts");
365
+ await withTelemetry("update", update)();
366
+ break;
367
+ }
347
368
  case "feedback": {
348
369
  const { default: feedback } = await import("./commands/feedback.ts");
349
370
  await withTelemetry("feedback", feedback)();
@@ -362,6 +383,16 @@ try {
362
383
  default:
363
384
  cli.showHelp(command ? 1 : 0);
364
385
  }
386
+
387
+ // Show update notification if available (non-blocking check completed)
388
+ if (updateCheckPromise) {
389
+ const latestVersion = await updateCheckPromise;
390
+ if (latestVersion) {
391
+ info("");
392
+ info(`Update available: v${pkg.version} → v${latestVersion}`);
393
+ info("Run: jack update");
394
+ }
395
+ }
365
396
  } catch (err) {
366
397
  if (isJackError(err)) {
367
398
  printError(err.message);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared login flow for CLI and programmatic use
3
3
  */
4
- import { input } from "@inquirer/prompts";
4
+ import { isCancel, text } from "@clack/prompts";
5
5
  import {
6
6
  checkUsernameAvailable,
7
7
  getCurrentUserProfile,
@@ -189,10 +189,18 @@ async function promptForUsername(email: string, firstName: string | null): Promi
189
189
 
190
190
  if (choice === options.length - 1) {
191
191
  // User chose to type custom username
192
- inputUsername = await input({
192
+ const customInput = await text({
193
193
  message: "Username:",
194
- validate: validateUsername,
194
+ validate: (value) => {
195
+ const result = validateUsername(value);
196
+ if (result !== true) return result;
197
+ },
195
198
  });
199
+ if (isCancel(customInput)) {
200
+ warn("Skipped username setup. You can set it later.");
201
+ return true;
202
+ }
203
+ inputUsername = customInput;
196
204
  } else {
197
205
  // User picked a suggestion (choice is guaranteed to be valid index)
198
206
  inputUsername = suggestions[choice] as string;
@@ -300,10 +300,18 @@ export interface ProjectResource {
300
300
  resource_type: string;
301
301
  resource_name: string;
302
302
  provider_id: string;
303
+ binding_name: string;
303
304
  status: string;
304
305
  created_at: string;
305
306
  }
306
307
 
308
+ export interface CreateResourceResponse {
309
+ resource_type: string;
310
+ resource_name: string;
311
+ provider_id: string;
312
+ binding_name: string;
313
+ }
314
+
307
315
  /**
308
316
  * Fetch all resources for a managed project.
309
317
  * Uses GET /v1/projects/:id/resources endpoint.
@@ -324,6 +332,45 @@ export async function fetchProjectResources(projectId: string): Promise<ProjectR
324
332
  return data.resources;
325
333
  }
326
334
 
335
+ /**
336
+ * Create a resource for a managed project.
337
+ * Uses POST /v1/projects/:id/resources/:type endpoint.
338
+ */
339
+ export async function createProjectResource(
340
+ projectId: string,
341
+ resourceType: "d1" | "kv" | "r2",
342
+ options?: { name?: string; bindingName?: string },
343
+ ): Promise<CreateResourceResponse> {
344
+ const { authFetch } = await import("./auth/index.ts");
345
+
346
+ const body: Record<string, string> = {};
347
+ if (options?.name) {
348
+ body.name = options.name;
349
+ }
350
+ if (options?.bindingName) {
351
+ body.binding_name = options.bindingName;
352
+ }
353
+
354
+ const response = await authFetch(
355
+ `${getControlApiUrl()}/v1/projects/${projectId}/resources/${resourceType}`,
356
+ {
357
+ method: "POST",
358
+ headers: { "Content-Type": "application/json" },
359
+ body: JSON.stringify(body),
360
+ },
361
+ );
362
+
363
+ if (!response.ok) {
364
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
365
+ message?: string;
366
+ };
367
+ throw new Error(err.message || `Failed to create ${resourceType} resource: ${response.status}`);
368
+ }
369
+
370
+ const data = (await response.json()) as { resource: CreateResourceResponse };
371
+ return data.resource;
372
+ }
373
+
327
374
  /**
328
375
  * Sync project tags to the control plane.
329
376
  * Fire-and-forget: errors are logged but not thrown.
package/src/lib/hooks.ts CHANGED
@@ -83,50 +83,30 @@ const noopOutput: HookOutput = {
83
83
  */
84
84
 
85
85
  /**
86
- * Prompt user with numbered options (Claude Code style)
86
+ * Prompt user with numbered options
87
+ * Uses @clack/prompts selectKey for reliable input handling
87
88
  * Returns the selected option index (0-based) or -1 if cancelled
88
89
  */
89
90
  export async function promptSelect(options: string[]): Promise<number> {
90
- // Display options
91
- for (let i = 0; i < options.length; i++) {
92
- console.error(` ${i + 1}. ${options[i]}`);
93
- }
94
- console.error("");
95
- console.error(" Esc to skip");
96
-
97
- // Read single keypress
98
- if (process.stdin.isTTY) {
99
- process.stdin.setRawMode(true);
100
- }
101
- process.stdin.resume();
102
-
103
- return new Promise((resolve) => {
104
- const onData = (key: Buffer) => {
105
- const char = key.toString();
91
+ const { selectKey, isCancel } = await import("@clack/prompts");
106
92
 
107
- // Esc or q to cancel
108
- if (char === "\x1b" || char === "q") {
109
- cleanup();
110
- resolve(-1);
111
- return;
112
- }
93
+ // Build options with number keys (1, 2, 3, ...)
94
+ const clackOptions = options.map((label, index) => ({
95
+ value: String(index + 1),
96
+ label,
97
+ }));
113
98
 
114
- // Number keys
115
- const num = Number.parseInt(char, 10);
116
- if (num >= 1 && num <= options.length) {
117
- cleanup();
118
- resolve(num - 1);
119
- return;
120
- }
121
- };
99
+ const result = await selectKey({
100
+ message: "",
101
+ options: clackOptions,
102
+ });
122
103
 
123
- const cleanup = () => {
124
- process.stdin.removeListener("data", onData);
125
- restoreTty();
126
- };
104
+ if (isCancel(result)) {
105
+ return -1;
106
+ }
127
107
 
128
- process.stdin.on("data", onData);
129
- });
108
+ // Convert "1", "2", etc. back to 0-based index
109
+ return Number.parseInt(result as string, 10) - 1;
130
110
  }
131
111
 
132
112
  /**
@@ -446,15 +426,12 @@ const actionHandlers: {
446
426
  if (action.validate === "json" || action.validate === "accountAssociation") {
447
427
  rawValue = await readMultilineJson(action.message);
448
428
  } else {
449
- const { input } = await import("@inquirer/prompts");
450
- try {
451
- rawValue = await input({ message: action.message });
452
- } catch (err) {
453
- if (err instanceof Error && err.name === "ExitPromptError") {
454
- return true;
455
- }
456
- throw err;
429
+ const { isCancel, text } = await import("@clack/prompts");
430
+ const result = await text({ message: action.message });
431
+ if (isCancel(result)) {
432
+ return true;
457
433
  }
434
+ rawValue = result;
458
435
  }
459
436
 
460
437
  if (!rawValue.trim()) {