@just-be/deploy 0.1.1 → 0.2.0

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.
Files changed (3) hide show
  1. package/README.md +10 -3
  2. package/index.ts +126 -57
  3. package/package.json +11 -4
package/README.md CHANGED
@@ -33,6 +33,7 @@ bunx @just-be/deploy
33
33
  ```
34
34
 
35
35
  The interactive mode will ask you for:
36
+
36
37
  - Subdomain name
37
38
  - R2 path prefix
38
39
  - Local directory to upload
@@ -58,11 +59,13 @@ bunx @just-be/deploy --subdomain=NAME --path=PATH --dir=DIR [--spa] [--fallback=
58
59
  ### Examples
59
60
 
60
61
  **Interactive deployment:**
62
+
61
63
  ```bash
62
64
  bunx @just-be/deploy
63
65
  ```
64
66
 
65
67
  **Deploy a static site:**
68
+
66
69
  ```bash
67
70
  bunx @just-be/deploy \
68
71
  --subdomain=portfolio \
@@ -71,6 +74,7 @@ bunx @just-be/deploy \
71
74
  ```
72
75
 
73
76
  **Deploy a single-page application:**
77
+
74
78
  ```bash
75
79
  bunx @just-be/deploy \
76
80
  --subdomain=myapp \
@@ -80,6 +84,7 @@ bunx @just-be/deploy \
80
84
  ```
81
85
 
82
86
  **Deploy with custom 404 page:**
87
+
83
88
  ```bash
84
89
  bunx @just-be/deploy \
85
90
  --subdomain=docs \
@@ -90,9 +95,9 @@ bunx @just-be/deploy \
90
95
 
91
96
  ## How It Works
92
97
 
93
- 1. **Parses arguments** using Node's `util.parseArgs` API
98
+ 1. **Parses arguments** using a custom argument parser
94
99
  2. **Prompts for missing values** if any required arguments are not provided
95
- 3. **Validates configuration** using Zod schemas from `@just-be/wildcard-schemas`
100
+ 3. **Validates configuration** using Zod schemas from `@just-be/wildcard`
96
101
  4. **Scans** the target directory for all files
97
102
  5. **Uploads** each file to R2 at `content-bucket/{path}/{relative-path}`
98
103
  6. **Creates** a KV entry with routing configuration for the subdomain
@@ -101,6 +106,7 @@ bunx @just-be/deploy \
101
106
  ## Configuration
102
107
 
103
108
  The script expects your wildcard service to use:
109
+
104
110
  - **R2 Bucket**: `content-bucket`
105
111
  - **KV Binding**: `ROUTING_RULES`
106
112
  - **Wrangler Config**: `services/wildcard/wrangler.toml`
@@ -108,13 +114,14 @@ The script expects your wildcard service to use:
108
114
  ## Validation
109
115
 
110
116
  Configuration is validated using Zod schemas to ensure:
117
+
111
118
  - SPA mode and fallback file are not used together
112
119
  - R2 path is not empty
113
120
  - All required fields are present
114
121
 
115
122
  ## Related Packages
116
123
 
117
- - [`@just-be/wildcard-schemas`](../wildcard-schemas) - Shared Zod schemas for configuration validation
124
+ - [`@just-be/wildcard`](../wildcard) - Shared Zod schemas and routing handlers for wildcard subdomain configuration
118
125
 
119
126
  ## License
120
127
 
package/index.ts CHANGED
@@ -15,13 +15,21 @@
15
15
  import { $ } from "bun";
16
16
  import { readdir, stat } from "fs/promises";
17
17
  import { join, relative } from "path";
18
- import { createInterface } from "readline";
18
+ import { confirm, intro, outro, spinner, text } from "@clack/prompts";
19
19
  import { z } from "zod";
20
20
  import { StaticConfigSchema, type StaticConfig, isValidSubdomain } from "@just-be/wildcard";
21
21
 
22
22
  const BUCKET_NAME = "content-bucket";
23
23
  const WRANGLER_CONFIG = "services/wildcard/wrangler.toml";
24
24
 
25
+ /**
26
+ * Run wrangler command using bunx to ensure it resolves from package dependencies
27
+ */
28
+ function wrangler(command: TemplateStringsArray, ...values: unknown[]) {
29
+ const cmd = String.raw(command, ...values);
30
+ return $`bunx wrangler ${cmd}`;
31
+ }
32
+
25
33
  /**
26
34
  * Schema for CLI arguments
27
35
  */
@@ -36,7 +44,7 @@ const CliArgsSchema = z.object({
36
44
  type CliArgs = z.infer<typeof CliArgsSchema>;
37
45
 
38
46
  /**
39
- * Parse command-line arguments from Bun.argv
47
+ * Parse command-line arguments from Bun.argv using a custom parser
40
48
  */
41
49
  function parseCliArgs(): CliArgs {
42
50
  const args: Record<string, string | boolean> = {};
@@ -61,32 +69,6 @@ interface DeployConfig extends Omit<StaticConfig, "type"> {
61
69
  dir: string;
62
70
  }
63
71
 
64
- /**
65
- * Prompt user for input
66
- */
67
- async function prompt(question: string, defaultValue?: string): Promise<string> {
68
- const rl = createInterface({
69
- input: process.stdin,
70
- output: process.stdout,
71
- });
72
-
73
- return new Promise((resolve) => {
74
- const promptText = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
75
- rl.question(promptText, (answer) => {
76
- rl.close();
77
- resolve(answer.trim() || defaultValue || "");
78
- });
79
- });
80
- }
81
-
82
- /**
83
- * Prompt user for confirmation
84
- */
85
- async function confirm(question: string): Promise<boolean> {
86
- const answer = await prompt(`${question} (y/n)`, "n");
87
- return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
88
- }
89
-
90
72
  /**
91
73
  * Parse command-line arguments
92
74
  */
@@ -97,33 +79,64 @@ async function getConfig(): Promise<DeployConfig> {
97
79
  const needsInteractive = !values.subdomain || !values.path || !values.dir;
98
80
 
99
81
  if (needsInteractive) {
100
- console.log("šŸš€ Interactive deployment setup\n");
82
+ intro("Interactive deployment setup");
101
83
  }
102
84
 
103
85
  let subdomain =
104
- values.subdomain || (await prompt("Subdomain name (e.g., 'myapp' for myapp.just-be.dev)"));
105
-
106
- // Validate subdomain format
86
+ values.subdomain ||
87
+ ((await text({
88
+ message: "Subdomain name (e.g., 'myapp' for myapp.just-be.dev)",
89
+ validate: (value) => {
90
+ if (!value) return "Subdomain is required";
91
+ if (!isValidSubdomain(value as string)) {
92
+ return "Invalid subdomain format. Must be alphanumeric with hyphens, 1-63 characters.";
93
+ }
94
+ },
95
+ })) as string);
96
+
97
+ // Validate subdomain format (in case it came from CLI args)
107
98
  if (!isValidSubdomain(subdomain)) {
108
99
  console.error(
109
- "\nāŒ Invalid subdomain format. Must be alphanumeric with hyphens, 1-63 characters."
100
+ "\nInvalid subdomain format. Must be alphanumeric with hyphens, 1-63 characters."
110
101
  );
111
102
  process.exit(1);
112
103
  }
113
104
 
114
- const path = values.path || (await prompt("R2 path prefix (e.g., 'apps/myapp')"));
115
- const dir = values.dir || (await prompt("Local directory to upload", "./dist"));
105
+ const path =
106
+ values.path ||
107
+ ((await text({
108
+ message: "R2 path prefix (e.g., 'apps/myapp')",
109
+ validate: (value) => {
110
+ if (!value) return "Path is required";
111
+ },
112
+ })) as string);
113
+
114
+ const dir =
115
+ values.dir ||
116
+ ((await text({
117
+ message: "Local directory to upload",
118
+ placeholder: "./dist",
119
+ defaultValue: "./dist",
120
+ })) as string);
116
121
 
117
122
  let spa = values.spa;
118
123
  let fallback = values.fallback;
119
124
 
120
125
  // Only prompt for spa/fallback if not provided
121
126
  if (needsInteractive && spa === undefined && fallback === undefined) {
122
- spa = await confirm("Enable SPA mode? (all routes serve index.html)");
127
+ spa = (await confirm({
128
+ message: "Enable SPA mode? (all routes serve index.html)",
129
+ })) as boolean;
123
130
  if (!spa) {
124
- const useFallback = await confirm("Use a custom fallback file for 404s?");
131
+ const useFallback = (await confirm({
132
+ message: "Use a custom fallback file for 404s?",
133
+ })) as boolean;
125
134
  if (useFallback) {
126
- fallback = await prompt("Fallback file name", "404.html");
135
+ fallback = (await text({
136
+ message: "Fallback file name",
137
+ placeholder: "404.html",
138
+ defaultValue: "404.html",
139
+ })) as string;
127
140
  }
128
141
  }
129
142
  }
@@ -138,7 +151,7 @@ async function getConfig(): Promise<DeployConfig> {
138
151
 
139
152
  const result = StaticConfigSchema.safeParse(staticConfig);
140
153
  if (!result.success) {
141
- console.error("\nāŒ Invalid configuration:");
154
+ console.error("\nInvalid configuration:");
142
155
  console.error(result.error.format());
143
156
  process.exit(1);
144
157
  }
@@ -176,9 +189,28 @@ async function findFiles(dir: string): Promise<string[]> {
176
189
 
177
190
  /**
178
191
  * Upload a file to R2
192
+ * @returns true if upload succeeded, false otherwise
179
193
  */
180
- async function uploadToR2(localPath: string, r2Key: string): Promise<void> {
181
- await $`wrangler r2 object put ${BUCKET_NAME}/${r2Key} --file ${localPath}`;
194
+ async function uploadToR2(localPath: string, r2Key: string): Promise<boolean> {
195
+ try {
196
+ await wrangler`r2 object put ${BUCKET_NAME}/${r2Key} --file ${localPath}`;
197
+ return true;
198
+ } catch (error) {
199
+ console.error(`\nFailed to upload ${localPath}:`, error);
200
+ return false;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Validate KV access by attempting to list keys
206
+ */
207
+ async function validateKVAccess(): Promise<boolean> {
208
+ try {
209
+ await wrangler`kv key list --binding ROUTING_RULES --config ${WRANGLER_CONFIG}`.quiet();
210
+ return true;
211
+ } catch {
212
+ return false;
213
+ }
182
214
  }
183
215
 
184
216
  /**
@@ -194,7 +226,7 @@ async function createKVEntry(subdomain: string, config: DeployConfig): Promise<v
194
226
 
195
227
  const configJson = JSON.stringify(routingConfig);
196
228
 
197
- await $`wrangler kv key put --binding ROUTING_RULES ${subdomain} ${configJson} --config ${WRANGLER_CONFIG}`;
229
+ await wrangler`kv key put --binding ROUTING_RULES ${subdomain} ${configJson} --config ${WRANGLER_CONFIG}`;
198
230
  }
199
231
 
200
232
  /**
@@ -203,7 +235,7 @@ async function createKVEntry(subdomain: string, config: DeployConfig): Promise<v
203
235
  async function deploy() {
204
236
  const config = await getConfig();
205
237
 
206
- console.log("\nšŸ“¦ Deploy Configuration:");
238
+ console.log("\nDeploy Configuration:");
207
239
  console.log(` Subdomain: ${config.subdomain}.just-be.dev`);
208
240
  console.log(` R2 Path: ${config.path}`);
209
241
  console.log(` Local Directory: ${config.dir}`);
@@ -218,46 +250,83 @@ async function deploy() {
218
250
  try {
219
251
  const dirStat = await stat(config.dir);
220
252
  if (!dirStat.isDirectory()) {
221
- console.error(`āŒ Error: ${config.dir} is not a directory`);
253
+ console.error(`Error: ${config.dir} is not a directory`);
222
254
  process.exit(1);
223
255
  }
224
256
  } catch {
225
- console.error(`āŒ Error: Directory ${config.dir} does not exist`);
257
+ console.error(`Error: Directory ${config.dir} does not exist`);
226
258
  process.exit(1);
227
259
  }
228
260
 
229
261
  // Find all files to upload
230
- console.log(`šŸ“‚ Scanning files in: ${config.dir}\n`);
262
+ const s = spinner();
263
+ s.start(`Scanning files in: ${config.dir}`);
231
264
  const filePaths = await findFiles(config.dir);
232
- console.log(`Found ${filePaths.length} files to upload\n`);
265
+ s.stop(`Found ${filePaths.length} files to upload`);
233
266
 
234
267
  if (filePaths.length === 0) {
235
- console.error("āŒ No files found to upload");
268
+ console.error("No files found to upload");
236
269
  process.exit(1);
237
270
  }
238
271
 
272
+ // Validate KV access before starting uploads
273
+ s.start("Validating KV access");
274
+ const hasKVAccess = await validateKVAccess();
275
+ if (!hasKVAccess) {
276
+ s.stop("Error: Cannot access KV namespace");
277
+ console.error("Check wrangler configuration and permissions.");
278
+ process.exit(1);
279
+ }
280
+ s.stop("KV access validated");
281
+
239
282
  // Upload files to R2
240
283
  let uploadCount = 0;
284
+ const failedUploads: string[] = [];
285
+
241
286
  for (const filePath of filePaths) {
242
287
  const relativePath = relative(config.dir, filePath);
243
288
  const r2Key = `${config.path}/${relativePath}`;
244
289
 
245
- console.log(`ā¬†ļø ${relativePath} → ${r2Key}`);
246
- await uploadToR2(filePath, r2Key);
247
- uploadCount++;
290
+ s.start(`Uploading ${relativePath}`);
291
+ const success = await uploadToR2(filePath, r2Key);
292
+
293
+ if (success) {
294
+ uploadCount++;
295
+ s.stop(`Uploaded ${relativePath}`);
296
+ } else {
297
+ s.stop(`Failed to upload ${relativePath}`);
298
+ failedUploads.push(relativePath);
299
+ }
300
+ }
301
+
302
+ // Report upload results
303
+ if (failedUploads.length > 0) {
304
+ console.error(`\nFailed to upload ${failedUploads.length} file(s):`);
305
+ for (const file of failedUploads) {
306
+ console.error(` - ${file}`);
307
+ }
308
+ process.exit(1);
248
309
  }
249
310
 
250
- console.log(`\nāœ… Uploaded ${uploadCount} files to R2\n`);
311
+ console.log(`\nUploaded ${uploadCount} files to R2\n`);
251
312
 
252
313
  // Create KV entry for routing
253
- console.log(`šŸ”§ Creating KV routing entry for subdomain: ${config.subdomain}`);
254
- await createKVEntry(config.subdomain, config);
314
+ s.start(`Creating KV routing entry for subdomain: ${config.subdomain}`);
315
+ try {
316
+ await createKVEntry(config.subdomain, config);
317
+ s.stop("KV routing entry created");
318
+ } catch (error) {
319
+ s.stop("Error: Failed to create KV entry");
320
+ console.error("Files were uploaded but routing is not configured.");
321
+ console.error(`Uploaded files location: ${config.path}`);
322
+ console.error(`Error:`, error);
323
+ process.exit(1);
324
+ }
255
325
 
256
- console.log(`\nāœ… Deployment complete!`);
257
- console.log(`\n🌐 Your site is available at: https://${config.subdomain}.just-be.dev`);
326
+ outro(`Your site is available at: https://${config.subdomain}.just-be.dev`);
258
327
  }
259
328
 
260
329
  deploy().catch((error) => {
261
- console.error("\nāŒ Fatal error:", error);
330
+ console.error("\nFatal error:", error);
262
331
  process.exit(1);
263
332
  });
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@just-be/deploy",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Deploy static files to Cloudflare R2 and configure subdomain routing",
5
5
  "type": "module",
6
- "bin": "./index.ts",
6
+ "bin": {
7
+ "deploy": "index.ts"
8
+ },
7
9
  "files": [
8
10
  "index.ts"
9
11
  ],
@@ -17,13 +19,18 @@
17
19
  "license": "MIT",
18
20
  "repository": {
19
21
  "type": "git",
20
- "url": "https://github.com/justbejyk/just-be.dev.git",
22
+ "url": "git+https://github.com/just-be-dev/just-be.dev.git",
21
23
  "directory": "packages/deploy"
22
24
  },
23
25
  "engines": {
24
26
  "bun": ">=1.0.0"
25
27
  },
26
28
  "dependencies": {
27
- "@just-be/wildcard": "0.1.0"
29
+ "@clack/prompts": "^0.11.0",
30
+ "@just-be/wildcard": "0.1.1",
31
+ "wrangler": "4.48.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
28
35
  }
29
36
  }