@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 +1 -2
- package/src/commands/clone.ts +6 -6
- package/src/commands/down.ts +21 -3
- package/src/commands/link.ts +8 -3
- package/src/commands/secrets.ts +11 -14
- package/src/commands/services.ts +148 -5
- package/src/commands/update.ts +53 -0
- package/src/index.ts +31 -0
- package/src/lib/auth/login-flow.ts +11 -3
- package/src/lib/control-plane.ts +47 -0
- package/src/lib/hooks.ts +22 -45
- package/src/lib/project-operations.ts +19 -11
- package/src/lib/prompts.ts +23 -21
- package/src/lib/services/db-create.ts +187 -0
- package/src/lib/services/db-list.ts +56 -0
- package/src/lib/version-check.ts +170 -0
- package/src/mcp/resources/index.ts +32 -0
- package/src/mcp/tools/index.ts +131 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getjack/jack",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
package/src/commands/clone.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { select } from "@
|
|
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
|
-
|
|
39
|
-
{ value: "overwrite",
|
|
40
|
-
{ value: "merge",
|
|
41
|
-
{ value: "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
|
}
|
package/src/commands/down.ts
CHANGED
|
@@ -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
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
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)
|
package/src/commands/link.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
|
-
import { select } from "@
|
|
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
|
-
|
|
130
|
+
options: projects.map((p) => ({
|
|
131
131
|
value: p.id,
|
|
132
|
-
|
|
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");
|
package/src/commands/secrets.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* For BYO projects: uses wrangler secret commands.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { password as passwordPrompt } from "@
|
|
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 @
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
/**
|
package/src/commands/services.ts
CHANGED
|
@@ -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
|
-
|
|
267
|
-
const
|
|
294
|
+
const { confirm } = await import("@clack/prompts");
|
|
295
|
+
const shouldDelete = await confirm({
|
|
296
|
+
message: `Delete database '${dbInfo.name}'?`,
|
|
297
|
+
});
|
|
268
298
|
|
|
269
|
-
if (
|
|
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 {
|
|
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
|
-
|
|
192
|
+
const customInput = await text({
|
|
193
193
|
message: "Username:",
|
|
194
|
-
validate:
|
|
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;
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
resolve(num - 1);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
};
|
|
99
|
+
const result = await selectKey({
|
|
100
|
+
message: "",
|
|
101
|
+
options: clackOptions,
|
|
102
|
+
});
|
|
122
103
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
};
|
|
104
|
+
if (isCancel(result)) {
|
|
105
|
+
return -1;
|
|
106
|
+
}
|
|
127
107
|
|
|
128
|
-
|
|
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 {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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()) {
|