UNPKG

@pushduck/cli

Version:

Official CLI for pushduck - Add S3 upload functionality to your Next.js project

535 lines (493 loc) 49.3 kB
#!/usr/bin/env node var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=s(require(`chalk`)),l=s(require(`commander`)),u=s(require(`detect-package-manager`)),d=s(require(`fs/promises`)),f=s(require(`inquirer`)),p=s(require(`ora`)),m=s(require(`path`)),h=s(require(`execa`)),g=s(require(`fs-extra`)),_=s(require(`@aws-sdk/client-s3`)),v=`https://pushduck.dev/r`;async function y(e){try{if(console.log(c.default.cyan(` ┌─────────────────────────────────────────────────────────────┐ │ │ │ 🦆 Add pushduck UI Components │ │ │ └─────────────────────────────────────────────────────────────┘ `)),!e){let t=(0,p.default)(`Fetching available components...`).start();try{let n=await fetch(`${v}/index.json`);if(!n.ok)throw Error(`Failed to fetch registry: ${n.statusText}`);let r=await n.json();t.stop();let{selectedComponent:i}=await f.default.prompt([{type:`list`,name:`selectedComponent`,message:`Which component would you like to add?`,choices:r.items.map(e=>({name:`${e.title} - ${e.description}`,value:e.name}))}]);e=i}catch(e){t.fail(`Failed to fetch component registry`),console.error(c.default.red(`Error: ${e instanceof Error?e.message:`Unknown error`}`)),process.exit(1)}}let t=(0,p.default)(`Fetching ${e}...`).start();try{let n=await fetch(`${v}/${e}.json`);if(!n.ok)throw Error(`Component "${e}" not found`);let r=await n.json();t.stop(),console.log(c.default.green(`✓ Found ${r.title}`)),console.log(c.default.gray(` ${r.description}`));let i=await b(),a=i?await x():await S();if(r.dependencies.length>0&&(console.log(c.default.yellow(` 📦 Installing dependencies...`)),await C(r.dependencies)),r.registryDependencies.length>0){console.log(c.default.yellow(` 🔗 Installing component dependencies...`));for(let e of r.registryDependencies)console.log(c.default.gray(` Installing ${e}...`)),console.log(c.default.red(` ⚠️ Please also install: ${e}`))}console.log(c.default.yellow(` 📁 Writing component files...`));for(let e of r.files){let t=e.target||m.join(a.components,e.name),n=m.resolve(t);await d.mkdir(m.dirname(n),{recursive:!0});let r=w(e.content,a);await d.writeFile(n,r,`utf-8`),console.log(c.default.green(` ✓ ${t}`))}console.log(c.default.green(`\n✨ Successfully added ${r.title}!`)),console.log(c.default.gray(` You can now import it in your React components:`)),console.log(c.default.cyan(` import { ${T(e)} } from "@/components/ui/${e}"; export function MyComponent() { return ( <${T(e)} route="myUploadRoute" // ... other props /> ); } `))}catch(n){t.fail(`Failed to add ${e}`),console.error(c.default.red(`Error: ${n.message}`)),process.exit(1)}}catch(e){console.error(c.default.red(`Error: ${e.message}`)),process.exit(1)}}async function b(){try{return await d.access(`components.json`),!0}catch{return!1}}async function x(){var e,t;let n=await d.readFile(`components.json`,`utf-8`),r=JSON.parse(n);return{components:((e=r.aliases)==null?void 0:e.ui)||`./components/ui`,utils:((t=r.aliases)==null?void 0:t.utils)||`./lib/utils`}}async function S(){let{componentsPath:e,utilsPath:t}=await f.default.prompt([{type:`input`,name:`componentsPath`,message:`Where should we install components?`,default:`./components/ui`},{type:`input`,name:`utilsPath`,message:`Where is your utils file?`,default:`./lib/utils`}]);return{components:e,utils:t}}async function C(e){let t=await(0,u.default)(),n=t===`yarn`?`yarn add`:t===`pnpm`?`pnpm add`:`npm install`;console.log(c.default.gray(` ${n} ${e.join(` `)}`)),e.forEach(e=>{console.log(c.default.green(` ✓ ${e}`))})}function w(e,t){return e=e.replace(/from ["']@\/lib\/utils["']/g,`from "${t.utils}"`),e=e.replace(/from ["']@\/components\/ui\/([^"']+)["']/g,`from "${t.components}/$1"`),e=e.replace(/from ["']@\/registry\/default\/([^/]+)\/([^"']+)["']/g,`from "${t.components}/$2"`),e}function T(e){return e.split(`-`).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(``)}async function E(){console.log(c.default.cyan(`🛠️ Add a new upload route `));try{let{routeName:e}=await f.default.prompt([{type:`input`,name:`routeName`,message:`Route name (e.g., "avatarUpload"):`,validate:e=>e?/^[a-zA-Z][a-zA-Z0-9]*$/.test(e)?!0:`Route name must be a valid JavaScript identifier`:`Route name is required`}]),{fileType:t}=await f.default.prompt([{type:`list`,name:`fileType`,message:`What type of files will this route handle?`,choices:[{name:`Images (.jpg, .png, .webp)`,value:`image`},{name:`Documents (.pdf, .doc, .txt)`,value:`document`},{name:`Any file type`,value:`file`},{name:`Custom configuration`,value:`custom`}]}]);console.log(c.default.green(` ✨ Route configuration generated! `)),console.log(c.default.cyan(`Add this to your upload-config.ts:`)),console.log(c.default.gray(`----------------------------------------`));let n=D(e,t);console.log(n),console.log(c.default.gray(`----------------------------------------`)),console.log(c.default.blue(` 💡 Don't forget to update your TypeScript types!`))}catch(e){console.error(c.default.red(`❌ Failed to generate route:`),e)}}function D(e,t){let n=` ${e}: createUploadConfig() .aws() // or your provider .${t}()`;switch(t){case`image`:return`${n} .maxSize("5MB") .allowedTypes(["image/jpeg", "image/png", "image/webp"]) .build(),`;case`document`:return`${n} .maxSize("10MB") .allowedTypes(["application/pdf", "application/msword", "text/plain"]) .build(),`;case`file`:return`${n} .maxSize("50MB") .allowedTypes(["*"]) .build(),`;default:return`${n} .maxSize("10MB") .allowedTypes(["your/mime-type"]) .build(),`}}async function O(e,t){let n=(0,p.default)(`Installing dependencies...`).start();try{n.text=`Adding pushduck to package.json...`;let r=k(e);await(0,h.execa)(e,[...r,`pushduck@latest`],{cwd:t}),n.text=`Installing additional dependencies...`,await(0,h.execa)(e,[...r,`react-dropzone`],{cwd:t}),n.succeed(`Dependencies installed successfully`),console.log(c.default.green(` 📦 Added to package.json:`)),console.log(` ✓ pushduck@latest`),console.log(` ✓ react-dropzone (for drag & drop)`)}catch(t){throw n.fail(`Failed to install dependencies`),console.error(c.default.red(`Error:`),t),console.log(c.default.yellow(` You can install dependencies manually:`)),console.log(c.default.gray(` ${e} ${k(e).join(` `)} pushduck react-dropzone`)),t}}function k(e){switch(e){case`npm`:return[`install`];case`yarn`:return[`add`];case`pnpm`:return[`add`];default:return[`install`]}}async function A(e){let t=m.default.join(e,`tsconfig.json`);if(!await g.default.pathExists(t))return{pathMappings:[]};try{let e=await g.default.readJson(t),n=e.compilerOptions||{},r=n.paths||{},i=n.baseUrl,a=[];for(let[e,t]of Object.entries(r))Array.isArray(t)&&t.length>0&&a.push({alias:e,target:t[0]});return{pathMappings:a,baseUrl:i}}catch(e){return{pathMappings:[]}}}async function j(e=process.cwd()){var t,n,r,i,a,o;let s=m.default.join(e,`package.json`);if(!await g.default.pathExists(s))throw Error(`No package.json found. Please run this command in a Next.js project root.`);let c=await g.default.readJson(s),l=((t=c.dependencies)==null?void 0:t.next)||((n=c.devDependencies)==null?void 0:n.next);if(!l){var u,d,f,p,h,_;let t=((u=c.dependencies)==null?void 0:u.react)||((d=c.devDependencies)==null?void 0:d.react),n=((f=c.dependencies)==null?void 0:f.vue)||((p=c.devDependencies)==null?void 0:p.vue),r=((h=c.dependencies)==null?void 0:h.svelte)||((_=c.devDependencies)==null?void 0:_.svelte),i=`unknown`,a=`unknown`;return t?(i=`react`,a=t):n?(i=`vue`,a=n):r&&(i=`svelte`,a=r),{framework:i,version:a,router:`unknown`,typescript:!1,cssFramework:`none`,packageManager:`npm`,hasExistingUpload:!1,rootDir:e,useSrcDir:!1,pathMappings:[],baseUrl:void 0}}let v=await g.default.pathExists(m.default.join(e,`app`)),y=await g.default.pathExists(m.default.join(e,`pages`)),b=await g.default.pathExists(m.default.join(e,`src`,`app`)),x=await g.default.pathExists(m.default.join(e,`src`,`pages`)),S=`unknown`,C=!1;b?(S=`app`,C=!0):v?(S=`app`,C=!1):x?(S=`pages`,C=!0):y&&(S=`pages`,C=!1);let w=await g.default.pathExists(m.default.join(e,`tsconfig.json`))||await g.default.pathExists(m.default.join(e,`next-env.d.ts`)),T=`none`;(r=c.dependencies)!=null&&r.tailwindcss||(i=c.devDependencies)!=null&&i.tailwindcss?T=`tailwind`:(a=c.dependencies)!=null&&a[`styled-components`]?T=`styled-components`:await g.default.pathExists(m.default.join(e,`styles`))&&(T=`css-modules`);let E=`npm`;await g.default.pathExists(m.default.join(e,`pnpm-lock.yaml`))?E=`pnpm`:await g.default.pathExists(m.default.join(e,`yarn.lock`))&&(E=`yarn`);let D=await g.default.pathExists(m.default.join(e,`lib/upload-config.ts`))||await g.default.pathExists(m.default.join(e,`lib/upload-config.js`))||await g.default.pathExists(m.default.join(e,`upload.ts`))||await g.default.pathExists(m.default.join(e,`upload.js`))||((o=c.dependencies)==null?void 0:o.pushduck)!==void 0,{pathMappings:O,baseUrl:k}=await A(e);return{framework:`nextjs`,version:l,router:S,typescript:w,cssFramework:T,packageManager:E,hasExistingUpload:D,rootDir:e,useSrcDir:C,pathMappings:O,baseUrl:k}}function M(e){let t=[];return e.framework===`nextjs`?t.push(`✓ Next.js ${e.version} detected`):e.framework===`react`?t.push(`✓ React ${e.version} detected`):e.framework===`vue`?t.push(`✓ Vue ${e.version} detected`):e.framework===`svelte`?t.push(`✓ Svelte ${e.version} detected`):t.push(`⚠ Unknown framework detected`),e.typescript&&t.push(`✓ TypeScript enabled`),e.router===`app`?t.push(`✓ App Router detected`):e.router===`pages`&&t.push(`✓ Pages Router detected`),e.cssFramework===`tailwind`&&t.push(`✓ Tailwind CSS detected`),e.hasExistingUpload&&t.push(`⚠ Existing upload configuration found`),t}const N={aws:{name:`AWS S3`,description:`Most popular, enterprise-ready`,envVars:[`AWS_ACCESS_KEY_ID`,`AWS_SECRET_ACCESS_KEY`,`AWS_REGION`,`S3_BUCKET`],regions:[{name:`us-east-1 (N. Virginia)`,value:`us-east-1`,description:`Default, lowest latency for US East`},{name:`us-west-2 (Oregon)`,value:`us-west-2`,description:`West Coast, cheaper data transfer`},{name:`eu-west-1 (Ireland)`,value:`eu-west-1`,description:`Europe, GDPR compliant`},{name:`ap-southeast-1 (Singapore)`,value:`ap-southeast-1`,description:`Asia Pacific`}]},"cloudflare-r2":{name:`Cloudflare R2`,description:`Zero egress fees, global CDN`,envVars:[`CLOUDFLARE_R2_ACCESS_KEY_ID`,`CLOUDFLARE_R2_SECRET_ACCESS_KEY`,`R2_BUCKET`,`CLOUDFLARE_ACCOUNT_ID`],regions:[{name:`auto`,value:`auto`,description:`Automatic region selection`}]},digitalocean:{name:`DigitalOcean Spaces`,description:`Simple pricing, developer-friendly`,envVars:[`DO_SPACES_ACCESS_KEY_ID`,`DO_SPACES_SECRET_ACCESS_KEY`,`DO_SPACES_REGION`,`DO_SPACES_BUCKET`],regions:[{name:`nyc3 (New York)`,value:`nyc3`,description:`US East Coast`},{name:`sfo3 (San Francisco)`,value:`sfo3`,description:`US West Coast`},{name:`ams3 (Amsterdam)`,value:`ams3`,description:`Europe`},{name:`sgp1 (Singapore)`,value:`sgp1`,description:`Asia Pacific`}]},minio:{name:`MinIO`,description:`Self-hosted, S3-compatible`,envVars:[`MINIO_ACCESS_KEY_ID`,`MINIO_SECRET_ACCESS_KEY`,`MINIO_ENDPOINT`,`MINIO_BUCKET`],regions:[{name:`us-east-1`,value:`us-east-1`,description:`Default region`}]},gcs:{name:`Google Cloud Storage`,description:`Advanced features, ML integration`,envVars:[`GCS_ACCESS_KEY_ID`,`GCS_SECRET_ACCESS_KEY`,`GCS_PROJECT_ID`,`GCS_BUCKET`],regions:[{name:`us-central1`,value:`us-central1`,description:`US Central`},{name:`europe-west1`,value:`europe-west1`,description:`Europe West`}]}};async function P(e){if(e&&Object.keys(N).includes(e))return e;console.log(c.default.cyan(` 📦 Choose your storage provider: `));let t=Object.entries(N).map(([e,t])=>({name:`${t.name.padEnd(20)} ${c.default.gray(t.description)}`,value:e,short:t.name})),{provider:n}=await f.default.prompt([{type:`list`,name:`provider`,message:`Select a provider:`,choices:t,default:`aws`}]),r=N[n];return console.log(c.default.green(`\n→ ${r.name} selected\n`)),console.log(c.default.blue(`💡 Why `+r.name+`?`)),console.log(c.default.gray(` • `+r.description)),console.log(c.default.gray(` • Need help choosing? https://pushduck.dev/docs/providers `)),n}async function F(e){let t={},n=N[e].envVars;switch(e){case`aws`:t.accessKeyId=process.env.AWS_ACCESS_KEY_ID,t.secretAccessKey=process.env.AWS_SECRET_ACCESS_KEY,t.region=process.env.AWS_REGION,t.bucket=process.env.S3_BUCKET||process.env.AWS_S3_BUCKET;break;case`cloudflare-r2`:t.accessKeyId=process.env.CLOUDFLARE_R2_ACCESS_KEY_ID||process.env.R2_ACCESS_KEY_ID,t.secretAccessKey=process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY||process.env.R2_SECRET_ACCESS_KEY,t.bucket=process.env.R2_BUCKET||process.env.CLOUDFLARE_R2_BUCKET,t.accountId=process.env.CLOUDFLARE_ACCOUNT_ID||process.env.R2_ACCOUNT_ID,t.region=`auto`;break;case`digitalocean`:t.accessKeyId=process.env.DO_SPACES_ACCESS_KEY_ID,t.secretAccessKey=process.env.DO_SPACES_SECRET_ACCESS_KEY,t.region=process.env.DO_SPACES_REGION,t.bucket=process.env.DO_SPACES_BUCKET;break;case`minio`:t.accessKeyId=process.env.MINIO_ACCESS_KEY_ID,t.secretAccessKey=process.env.MINIO_SECRET_ACCESS_KEY,t.endpoint=process.env.MINIO_ENDPOINT,t.bucket=process.env.MINIO_BUCKET,t.region=process.env.MINIO_REGION||`us-east-1`;break;case`gcs`:t.accessKeyId=process.env.GCS_ACCESS_KEY_ID,t.secretAccessKey=process.env.GCS_SECRET_ACCESS_KEY,t.region=process.env.GCS_REGION||`us-central1`,t.bucket=process.env.GCS_BUCKET;break}return t}async function I(e,t){let n=N[e],r=[];!t.region&&n.regions.length>1&&r.push({type:`list`,name:`region`,message:`Select your region:`,choices:n.regions.map(e=>({name:`${e.name.padEnd(30)} ${c.default.gray(e.description)}`,value:e.value,short:e.name})),default:n.regions[0].value}),t.bucket||r.push({type:`input`,name:`bucket`,message:`S3 Bucket name:`,default:`my-app-uploads-${new Date().getFullYear()}`,validate:e=>!e||e.length<3?`Bucket name must be at least 3 characters long`:/^[a-z0-9-]+$/.test(e)?!0:`Bucket name must be lowercase letters, numbers, and hyphens only`}),e===`cloudflare-r2`&&!t.accountId&&r.push({type:`input`,name:`accountId`,message:`Cloudflare Account ID:`,validate:e=>e?!0:`Account ID is required`}),e===`minio`&&!t.endpoint&&r.push({type:`input`,name:`endpoint`,message:`MinIO Endpoint:`,default:`localhost:9000`,validate:e=>e?!0:`Endpoint is required`});let i=await f.default.prompt(r);return{accessKeyId:t.accessKeyId||``,secretAccessKey:t.secretAccessKey||``,region:t.region||i.region||n.regions[0].value,bucket:t.bucket||i.bucket,endpoint:t.endpoint||i.endpoint,accountId:t.accountId||i.accountId}}async function L(e,t){let n=(0,p.default)(`Creating S3 bucket...`).start();try{let r;switch(t){case`cloudflare-r2`:r=`https://${e.accountId}.r2.cloudflarestorage.com`;break;case`digitalocean`:r=`https://${e.region}.digitaloceanspaces.com`;break;case`minio`:r=`http://${e.endpoint}`;break;case`gcs`:r=`https://storage.googleapis.com`;break}let i=new _.S3Client({region:e.region,credentials:{accessKeyId:e.accessKeyId,secretAccessKey:e.secretAccessKey},endpoint:r,forcePathStyle:t===`minio`});try{return await i.send(new _.HeadBucketCommand({Bucket:e.bucket})),n.succeed(`Bucket already exists`),!0}catch(e){}await i.send(new _.CreateBucketCommand({Bucket:e.bucket,CreateBucketConfiguration:e.region===`us-east-1`?void 0:{LocationConstraint:e.region}}));let a={CORSRules:[{AllowedHeaders:[`*`],AllowedMethods:[`GET`,`PUT`,`POST`,`DELETE`,`HEAD`],AllowedOrigins:[`*`],ExposeHeaders:[`ETag`],MaxAgeSeconds:3e3}]};return await i.send(new _.PutBucketCorsCommand({Bucket:e.bucket,CORSConfiguration:a})),n.succeed(`Bucket created successfully`),console.log(c.default.green(`✓ CORS configuration applied`)),console.log(c.default.green(`✓ Security settings configured`)),!0}catch(e){return n.fail(`Failed to create bucket`),console.error(c.default.red(`Error:`),e),!1}}async function R(e,t){let n=(0,p.default)(`Testing S3 connection...`).start();try{let r;switch(t){case`cloudflare-r2`:r=`https://${e.accountId}.r2.cloudflarestorage.com`;break;case`digitalocean`:r=`https://${e.region}.digitaloceanspaces.com`;break;case`minio`:r=`http://${e.endpoint}`;break;case`gcs`:r=`https://storage.googleapis.com`;break}let i=new _.S3Client({region:e.region,credentials:{accessKeyId:e.accessKeyId,secretAccessKey:e.secretAccessKey},endpoint:r,forcePathStyle:t===`minio`});return await i.send(new _.HeadBucketCommand({Bucket:e.bucket})),n.succeed(`S3 connection successful`),!0}catch(e){return n.fail(`S3 connection failed`),console.error(c.default.red(`Error:`),e),!1}}function z(e,t){let n={};switch(t){case`aws`:n.AWS_ACCESS_KEY_ID=e.accessKeyId,n.AWS_SECRET_ACCESS_KEY=e.secretAccessKey,n.AWS_REGION=e.region,n.S3_BUCKET=e.bucket;break;case`cloudflare-r2`:n.CLOUDFLARE_R2_ACCESS_KEY_ID=e.accessKeyId,n.CLOUDFLARE_R2_SECRET_ACCESS_KEY=e.secretAccessKey,n.R2_BUCKET=e.bucket,n.CLOUDFLARE_ACCOUNT_ID=e.accountId||``;break;case`digitalocean`:n.DO_SPACES_ACCESS_KEY_ID=e.accessKeyId,n.DO_SPACES_SECRET_ACCESS_KEY=e.secretAccessKey,n.DO_SPACES_REGION=e.region,n.DO_SPACES_BUCKET=e.bucket;break;case`minio`:n.MINIO_ACCESS_KEY_ID=e.accessKeyId,n.MINIO_SECRET_ACCESS_KEY=e.secretAccessKey,n.MINIO_ENDPOINT=e.endpoint||``,n.MINIO_BUCKET=e.bucket,n.MINIO_REGION=e.region;break;case`gcs`:n.GCS_ACCESS_KEY_ID=e.accessKeyId,n.GCS_SECRET_ACCESS_KEY=e.secretAccessKey,n.GCS_REGION=e.region,n.GCS_BUCKET=e.bucket;break}return n}function B(e,t,n){let{pathMappings:r,baseUrl:i,useSrcDir:a}=e;for(let e of r){let{alias:n,target:r}=e;if(n.endsWith(`/*`)){let e=n.slice(0,-2),i=r.replace(/^\.\//,``).replace(/\/\*$/,``);if(e===`@`){let e=a?`src`:``;if(r===`./*`||r===`./src/*`||r===`src/*`){let e=r.includes(`src`)&&a||!r.includes(`src`)&&!a||r===`./*`;if(e)return`"@/${t}"`}}}}let o=m.default.dirname(n),s;if(t.startsWith(`lib/`)){let e=a?`src/lib`:`lib`;s=m.default.join(e,t.replace(/^lib\//,``))}else if(t.startsWith(`components/`)){let e=a?`src/components`:`components`;s=m.default.join(e,t.replace(/^components\//,``))}else if(t.startsWith(`app/`)){let e=a?`src/app`:`app`;s=m.default.join(e,t.replace(/^app\//,``))}else s=t;let c=m.default.relative(o,s),l=c.replace(/\\/g,`/`);return`"${l.startsWith(`.`)?l:`./${l}`}"`}async function V(e){let{projectInfo:t,provider:n,credentials:r,apiPath:i,generateExamples:a,verbose:o}=e;await H(t,i,a),await U(t,i,o),await W(t,n,o),await G(t,i,o),await J(t,n,r,o),a&&(await K(t,o),await q(t,i,o))}async function H(e,t,n){let{rootDir:r,router:i,useSrcDir:a}=e;if(i===`app`){let e=a?`src/app`:`app`;await g.default.ensureDir(m.default.join(r,`${e}${t}`))}let o=a?`src/lib`:`lib`;if(await g.default.ensureDir(m.default.join(r,o)),n){let e=a?`src/components/ui`:`components/ui`;if(await g.default.ensureDir(m.default.join(r,e)),i===`app`){let e=a?`src/app`:`app`;await g.default.ensureDir(m.default.join(r,`${e}/upload`))}}}async function U(e,t,n){let{rootDir:r,router:i,typescript:a,useSrcDir:o}=e,s=a?`ts`:`js`,l,u;if(i===`app`){let n=o?`src/app`:`app`;l=m.default.join(r,`${n}${t}/route.${s}`);let i=B(e,`lib/upload-config`,`${n}${t}`);u=`import { s3 } from ${i}; // Define upload routes with proper validation and lifecycle hooks const uploadRouter = s3.createRouter({ // Image upload route with size and format validation imageUpload: s3 .image() .max("5MB") .formats(["jpeg", "jpg", "png", "webp"]) .middleware(async ({ file, metadata }) => { console.log("Processing image upload:", file.name); return { ...metadata, userId: "demo-user", // Replace with actual auth uploadedAt: new Date().toISOString(), category: "images", }; }) .onUploadComplete(async ({ file, url, metadata }) => { console.log(\`✅ Image upload complete: \${file.name} -> \${url}\`, metadata); }), // File upload route fileUpload: s3 .file() .max("10MB") .types([ "application/pdf", "application/msword", "text/plain", "image/*" ]) .middleware(async ({ file, metadata }) => { console.log("Processing file upload:", file.name); return { ...metadata, userId: "demo-user", // Replace with actual auth uploadedAt: new Date().toISOString(), category: "documents", }; }) .onUploadComplete(async ({ file, url, metadata }) => { console.log(\`✅ File upload complete: \${file.name} -> \${url}\`, metadata); }), }); // Export router type for enhanced client type inference export type AppUploadRouter = typeof uploadRouter; // Export the HTTP handlers export const { GET, POST } = uploadRouter.handlers; `}else{let e=o?`src/pages`:`pages`;l=m.default.join(r,`${e}${t}.${s}`);let n=o?`../../lib/upload-config`:`../lib/upload-config`;u=`import { s3 } from "${n}"; const uploadRouter = s3.createRouter({ imageUpload: s3.image().max("5MB").formats(["jpeg", "jpg", "png", "webp"]), fileUpload: s3.file().max("10MB").types(["application/pdf", "text/plain", "image/*"]), }); export type AppUploadRouter = typeof uploadRouter; export const { GET, POST } = uploadRouter.handlers; `}await g.default.writeFile(l,u),n&&console.log(c.default.gray(` Created: ${m.default.relative(r,l)}`))}async function W(e,t,n){let{rootDir:r,typescript:i,useSrcDir:a}=e,o=i?`ts`:`js`,s=a?`src/lib`:`lib`,l=m.default.join(r,`${s}/upload-config.${o}`),u=Y(t),d=X(t),f=`import { createUploadConfig } from "pushduck/server"; // Initialize upload configuration with simplified one-step process const { s3, config } = createUploadConfig() .provider("${u}", { ${d} }) .defaults({ maxFileSize: "10MB", acl: "public-read", }) .build(); export { s3, config }; `;await g.default.writeFile(l,f),n&&console.log(c.default.gray(` Created: ${m.default.relative(r,l)}`))}async function G(e,t,n){let{rootDir:r,typescript:i,router:a,useSrcDir:o}=e,s=i?`ts`:`js`,l=o?`src/lib`:`lib`,u=m.default.join(r,`${l}/upload-client.${s}`),d;if(a===`app`){let n=o?`src/app`:`app`;d=B(e,`app${t}/route`,`${l}`)}else{let e=o?`src/pages`:`pages`,n=m.default.relative(l,`${e}${t}`),r=n.replace(/\\/g,`/`);d=`"${r.startsWith(`.`)?r:`./${r}`}"`}let f=`/** * Enhanced Upload Client with Property-Based Access * * This provides type-safe upload functionality with enhanced developer experience. */ import { createUploadClient } from "pushduck/client"; import type { AppUploadRouter } from ${d}; /** * Type-safe upload client with property-based access * * Usage: * const { uploadFiles, files, isUploading, reset } = upload.imageUpload(); * const { uploadFiles, files, isUploading, reset } = upload.fileUpload(); */ export const upload = createUploadClient<AppUploadRouter>({ endpoint: "${t}", }); // Export types for manual usage export type { AppUploadRouter }; `;await g.default.writeFile(u,f),n&&console.log(c.default.gray(` Created: ${m.default.relative(r,u)}`))}async function K(e,t){let{rootDir:n,typescript:r,useSrcDir:i}=e,a=r?`tsx`:`jsx`,o=i?`src/components/ui`:`components/ui`,s=m.default.join(n,`${o}/upload-zone.${a}`),l=`"use client"; import { useCallback } from "react"; import { useDropzone } from "react-dropzone"; interface UploadZoneProps { onDrop: (files: File[]) => void; disabled?: boolean; className?: string; accept?: Record<string, string[]>; maxFiles?: number; } export function UploadZone({ onDrop, disabled, className, accept, maxFiles = 10 }: UploadZoneProps) { const handleDrop = useCallback((files: File[]) => onDrop(files), [onDrop]); const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({ onDrop: handleDrop, disabled, accept, maxFiles, }); return ( <div {...getRootProps()} className={\` border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors \${isDragActive && !isDragReject ? "border-blue-500 bg-blue-50" : ""} \${isDragReject ? "border-red-500 bg-red-50" : ""} \${!isDragActive && !isDragReject ? "border-gray-300 hover:border-gray-400" : ""} \${disabled ? "opacity-50 cursor-not-allowed" : ""} \${className || ""} \`} > <input {...getInputProps()} /> <div className="space-y-2"> <div className="text-2xl">📁</div> <p className="text-lg font-medium"> {isDragActive ? isDragReject ? "Some files are not accepted" : "Drop files here" : "Drag & drop files or click to browse"} </p> <p className="text-sm text-gray-500"> Maximum {maxFiles} files allowed </p> </div> </div> ); } `;await g.default.writeFile(s,l);let u=m.default.join(n,`${o}/file-list.${a}`),d=`"use client"; import { formatETA, formatUploadSpeed } from "pushduck"; interface FileListProps { files: Array<{ id: string; name: string; size: number; status: 'pending' | 'uploading' | 'success' | 'error'; progress: number; url?: string; error?: string; uploadSpeed?: number; eta?: number; }>; onRemove?: (fileId: string) => void; } export function FileList({ files, onRemove }: FileListProps) { if (!files.length) return null; return ( <div className="space-y-3"> <h3 className="text-lg font-medium">Upload Progress</h3> {files.map((file) => ( <div key={file.id} className="p-4 bg-white rounded-lg border shadow-sm"> <div className="flex justify-between items-center mb-2"> <div className="flex-1 min-w-0"> <p className="text-sm font-medium text-gray-900 truncate"> {file.name} </p> <p className="text-xs text-gray-500"> {(file.size / 1024 / 1024).toFixed(2)} MB </p> </div> <div className="flex gap-2 items-center"> <span className={\`text-xs px-2 py-1 rounded-full \${ file.status === "success" ? "bg-green-100 text-green-800" : file.status === "error" ? "bg-red-100 text-red-800" : "bg-blue-100 text-blue-800" }\`}> {file.status === "success" && "✅ Complete"} {file.status === "error" && "❌ Error"} {file.status === "uploading" && \`📤 \${file.progress}%\`} {file.status === "pending" && "⏳ Pending"} </span> {onRemove && file.status !== "uploading" && ( <button onClick={() => onRemove(file.id)} className="text-sm text-red-500 hover:text-red-700" > Remove </button> )} </div> </div> {/* Progress Bar */} {(file.status === "uploading" || file.status === "pending") && ( <div className="mb-2 w-full h-2 bg-gray-200 rounded-full"> <div className="h-2 bg-blue-600 rounded-full transition-all duration-300" style={{ width: \`\${file.progress}%\` }} /> </div> )} {/* Upload Stats */} {file.status === "uploading" && file.uploadSpeed && file.eta && ( <div className="flex justify-between text-xs text-gray-500"> <span>{formatUploadSpeed(file.uploadSpeed)}</span> <span>ETA: {formatETA(file.eta)}</span> </div> )} {/* Success State */} {file.status === "success" && file.url && ( <a href={file.url} target="_blank" rel="noopener noreferrer" className="text-sm text-blue-600 underline hover:text-blue-800" > View uploaded file → </a> )} {/* Error State */} {file.status === "error" && file.error && ( <p className="mt-1 text-sm text-red-600">{file.error}</p> )} </div> ))} </div> ); } `;await g.default.writeFile(u,d),t&&(console.log(c.default.gray(` Created: ${m.default.relative(n,s)}`)),console.log(c.default.gray(` Created: ${m.default.relative(n,u)}`)))}async function q(e,t,n){let{rootDir:r,router:i,typescript:a,useSrcDir:o}=e,s=a?`tsx`:`jsx`,l;if(i===`app`){let e=o?`src/app`:`app`;l=m.default.join(r,`${e}/upload/page.${s}`)}else{let e=o?`src/pages`:`pages`;l=m.default.join(r,`${e}/upload.${s}`)}let u=i===`app`?o?`src/app/upload`:`app/upload`:o?`src/pages`:`pages`,d=B(e,`lib/upload-client`,u),f=B(e,`components/ui/upload-zone`,u),p=B(e,`components/ui/file-list`,u),h=`"use client"; import { useState } from "react"; import { upload } from ${d}; import { UploadZone } from ${f}; import { FileList } from ${p}; export default function UploadPage() { const [activeTab, setActiveTab] = useState<"images" | "files">("images"); // Enhanced type-safe hooks with property-based access const imageUpload = upload.imageUpload(); const fileUpload = upload.fileUpload(); const currentUpload = activeTab === "images" ? imageUpload : fileUpload; const handleUpload = async (files: File[]) => { try { await currentUpload.uploadFiles(files); } catch (error) { console.error("Upload failed:", error); } }; return ( <div className="container px-4 py-8 mx-auto max-w-4xl"> <div className="mb-8"> <h1 className="mb-2 text-3xl font-bold text-gray-900"> 🚀 File Upload Demo </h1> <p className="text-gray-600"> Enhanced type-safe uploads with property-based client </p> </div> {/* Tab Navigation */} <div className="flex mb-6 border-b border-gray-200"> <button onClick={() => setActiveTab("images")} className={\`px-4 py-2 font-medium text-sm border-b-2 \${ activeTab === "images" ? "border-blue-500 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700" }\`} > 🖼️ Images (.jpg, .png, .webp) </button> <button onClick={() => setActiveTab("files")} className={\`px-4 py-2 font-medium text-sm border-b-2 \${ activeTab === "files" ? "border-blue-500 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700" }\`} > 📄 Documents (.pdf, .doc, .txt) </button> </div> {/* Upload Area */} <div className="space-y-6"> <UploadZone onDrop={handleUpload} disabled={currentUpload.isUploading} accept={ activeTab === "images" ? { "image/*": [".jpg", ".jpeg", ".png", ".webp"] } : { "application/pdf": [".pdf"], "application/msword": [".doc"], "text/plain": [".txt"], } } maxFiles={activeTab === "images" ? 5 : 3} /> {/* Status and Actions */} {currentUpload.files.length > 0 && ( <div className="flex justify-between items-center p-4 bg-gray-50 rounded-lg"> <div className="text-sm text-gray-600"> {currentUpload.files.filter(f => f.status === "success").length} of{" "} {currentUpload.files.length} files uploaded </div> <div className="flex gap-2"> {currentUpload.isUploading && ( <div className="text-sm text-blue-600"> Uploading {currentUpload.files.filter(f => f.status === "uploading").length} files... </div> )} <button onClick={currentUpload.reset} disabled={currentUpload.isUploading} className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 disabled:opacity-50" > Clear All </button> </div> </div> )} {/* File List */} <FileList files={currentUpload.files} onRemove={(fileId) => { // Custom remove logic if needed console.log("Remove file:", fileId); }} /> {/* Image Gallery */} {activeTab === "images" && currentUpload.files.filter((f) => f.status === "success" && f.url) .length > 0 && ( <div className="space-y-4"> <h3 className="text-lg font-semibold text-gray-800"> 📸 Uploaded Images </h3> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> {currentUpload.files .filter((f) => f.status === "success" && f.url) .map((file) => ( <div key={file.id} className="group relative aspect-square border border-gray-200 rounded-lg overflow-hidden bg-gray-50 hover:border-gray-300 transition-colors" > <img src={file.url} alt={file.name} className="w-full h-full object-cover transition-transform group-hover:scale-105" loading="lazy" /> <div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-opacity" /> <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-2"> <p className="text-white text-xs truncate font-medium"> {file.name} </p> <p className="text-white/80 text-xs"> {(file.size / 1024 / 1024).toFixed(1)} MB </p> </div> <a href={file.url} target="_blank" rel="noopener noreferrer" className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-white/90 hover:bg-white rounded-full p-1.5" title="Open full size" > <svg className="w-4 h-4 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> </svg> </a> </div> ))} </div> </div> )} {/* Error Display */} {currentUpload.errors.length > 0 && ( <div className="p-4 bg-red-50 rounded-lg border border-red-200"> <h4 className="mb-2 text-sm font-medium text-red-800">Upload Errors:</h4> <ul className="space-y-1"> {currentUpload.errors.map((error, index) => ( <li key={index} className="text-sm text-red-600"> • {error} </li> ))} </ul> </div> )} </div> </div> ); } `;await g.default.writeFile(l,h),n&&console.log(c.default.gray(` Created: ${m.default.relative(r,l)}`))}async function J(e,t,n,r){let{rootDir:i}=e,a=m.default.join(i,`.env.local`),o=z(n,t),s=``;await g.default.pathExists(a)&&(s=await g.default.readFile(a,`utf-8`)),s.includes(`# Upload Configuration`)||(s+=` # Upload Configuration `),Object.entries(o).forEach(([e,t])=>{s.includes(`${e}=`)||(s+=`${e}=${t}\n`)}),await g.default.writeFile(a,s),r&&console.log(c.default.gray(` Updated: .env.local`))}function Y(e){switch(e){case`aws`:return`aws`;case`cloudflare-r2`:return`cloudflareR2`;case`digitalocean`:return`digitalOceanSpaces`;case`minio`:return`minio`;case`gcs`:return`gcs`;default:return e}}function X(e){switch(e){case`aws`:return`region: process.env.AWS_REGION!, bucket: process.env.S3_BUCKET!, accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,`;case`cloudflare-r2`:return`accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, bucket: process.env.R2_BUCKET!, accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID!, secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY!,`;case`digitalocean`:return`region: process.env.DO_SPACES_REGION!, bucket: process.env.DO_SPACES_BUCKET!, accessKeyId: process.env.DO_SPACES_ACCESS_KEY_ID!, secretAccessKey: process.env.DO_SPACES_SECRET_ACCESS_KEY!,`;case`minio`:return`endpoint: process.env.MINIO_ENDPOINT!, bucket: process.env.MINIO_BUCKET!, accessKeyId: process.env.MINIO_ACCESS_KEY_ID!, secretAccessKey: process.env.MINIO_SECRET_ACCESS_KEY!, region: process.env.MINIO_REGION!,`;case`gcs`:return`projectId: process.env.GCS_PROJECT_ID!, bucket: process.env.GCS_BUCKET!, keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS!, region: process.env.GCS_REGION!,`;default:return``}}function Z(e){let t=`https://pushduck.dev`;switch(e){case`aws`:return{corsSetup:`${t}/docs/guides/security/cors-and-acl`,providerDocs:`${t}/docs/providers/aws-s3`};case`cloudflare-r2`:return{corsSetup:`${t}/docs/guides/security/cors-and-acl`,providerDocs:`${t}/docs/providers/cloudflare-r2`};case`digitalocean`:return{corsSetup:`${t}/docs/guides/security/cors-and-acl`,providerDocs:`${t}/docs/providers/digitalocean-spaces`};case`minio`:return{corsSetup:`${t}/docs/guides/security/cors-and-acl`,providerDocs:`${t}/docs/providers/minio`};case`gcs`:return{corsSetup:`${t}/docs/guides/security/cors-and-acl`,providerDocs:`${t}/docs/providers/google-cloud`};default:return{corsSetup:`${t}/docs/guides/security/cors-and-acl`,providerDocs:`${t}/docs/providers`}}}async function Q(e={}){console.log(c.default.cyan(` ┌─────────────────────────────────────────────────────────────┐ │ │ │ 🚀 Welcome to Pushduck │ │ │ │ Let's get your file uploads working in 2 minutes! │ │ │ └─────────────────────────────────────────────────────────────┘ `));try{console.log(c.default.cyan(`🔍 Detecting your project...`));let t=await j(),n=M(t);if(n.forEach(e=>console.log(` ${e}`)),t.framework!==`nextjs`){let e=t.framework===`react`?`React`:t.framework===`vue`?`Vue`:t.framework===`svelte`?`Svelte`:`Unknown framework`;console.log(c.default.yellow(`\n⚠ ${e} Not Supported by CLI`)),console.log(c.default.gray(`This CLI currently only works with Next.js projects.`)),console.log(c.default.gray(`For ${e.toLowerCase()} projects, please use manual setup.\n`)),console.log(c.default.cyan(`📖 Manual Setup Documentation:`)),console.log(` https://pushduck.dev/docs/getting-started/manual-setup`),console.log(c.default.cyan(` 🔧 Integration Guides:`)),t.framework===`react`?console.log(` React: https://pushduck.dev/docs/integrations/overview`):t.framework===`vue`?console.log(` Vue: https://pushduck.dev/docs/integrations/overview`):t.framework===`svelte`?console.log(` Svelte: https://pushduck.dev/docs/integrations/overview`):console.log(` General: https://pushduck.dev/docs/integrations/overview`),console.log(c.default.gray(` The manual setup will guide you through:`)),console.log(c.default.gray(` • Installing pushduck`)),console.log(c.default.gray(` • Configuring your storage provider`)),console.log(c.default.gray(` • Setting up API routes`)),console.log(c.default.gray(` • Adding client-side components`));return}if(t.hasExistingUpload){let{overwrite:e}=await f.default.prompt([{type:`confirm`,name:`overwrite`,message:`Existing upload configuration detected. Do you want to overwrite it?`,default:!1}]);if(!e){console.log(c.default.yellow(`Setup cancelled. Use --force to overwrite existing configuration.`));return}}let r=await P(e.provider);console.log(c.default.cyan(`🔧 Setting up ${r.toUpperCase()}...`)),console.log(c.default.cyan(` 🔍 Checking for existing credentials...`));let i=await F(r);Object.entries(i).forEach(([e,t])=>{t?console.log(c.default.green(` ✓ Found ${e.toUpperCase()}`)):console.log(c.default.yellow(` ⚠ ${e.toUpperCase()} not found`))});let a=await I(r,i);if(!e.skipBucket&&a.accessKeyId&&a.secretAccessKey){let{createBucket:t}=await f.default.prompt([{type:`confirm`,name:`createBucket`,message:`🔒 Would you like to create the S3 bucket automatically?`,default:!0}]);if(t)if(e.dryRun)console.log(c.default.blue(` [DRY RUN] Would create S3 bucket:`,a.bucket));else{let e=await L(a,r);e||console.log(c.default.yellow(`⚠ Bucket creation failed, but you can continue with manual setup`))}}console.log(c.default.cyan(` 🛠️ Generating API routes...`));let{apiPath:o}=await f.default.prompt([{type:`list`,name:`apiPath`,message:`Where should we create the upload API?`,choices:[{name:`app/api/upload/route.ts (recommended)`,value:`/api/upload`,short:`upload`},{name:`app/api/s3-upload/route.ts (classic)`,value:`/api/s3-upload`,short:`s3-upload`},{name:`Custom path`,value:`custom`,short:`custom`}],default:`/api/upload`}]),s=o;if(o===`custom`){let{customPath:e}=await f.default.prompt([{type:`input`,name:`customPath`,message:`Enter custom API path:`,default:`/api/upload`,validate:e=>e.startsWith(`/api/`)?!0:`API path must start with /api/`}]);s=e}let l=!0;if(!e.skipExamples){let{exampleType:t}=await f.default.prompt([{type:`list`,name:`exampleType`,message:`🎨 Generate example upload page?`,choices:[{name:`Yes, create app/upload/page.tsx with full example`,value:`full`},{name:`Yes, just add components to components/ui/`,value:`components`},{name:`No, I'll build my own`,value:`none`}],default:`full`}]);l=t!==`none`,e.skipExamples=t===`none`}if(e.dryRun){console.log(c.default.blue(` [DRY RUN] Files that would be created:`)),console.log(c.default.blue(` ├── app/api/upload/route.ts`)),console.log(c.default.blue(` ├── lib/upload-config.ts`)),l&&(console.log(c.default.blue(` ├── components/ui/upload-zone.tsx`)),console.log(c.default.blue(` ├── components/ui/file-list.tsx`)),console.log(c.default.blue(` ├── app/upload/page.tsx`))),console.log(c.default.blue(` └── .env.local (updated)`));return}let u=(0,p.default)(`Generating files...`).start();if(await V({projectInfo:t,provider:r,credentials:a,apiPath:s,generateExamples:l,verbose:e.verbose}),u.succeed(`Files generated successfully`),t.hasExistingUpload?l&&await O(t.packageManager,t.rootDir):await O(t.packageManager,t.rootDir),console.log(c.default.green(` 🎉 Setup complete! Here's what was created: `)),console.log(c.default.cyan(`📁 Files created:`)),console.log(` ├── ${s.replace(`/api/`,`app/api/`)}/route.ts # Upload API endpoint`),console.log(` ├── lib/upload-config.ts # Upload configuration`),l&&(console.log(` ├── components/ui/upload-zone.tsx # Drag & drop component`),console.log(` ├── components/ui/file-list.tsx # Upload progress display`),console.log(` ├── app/upload/page.tsx # Example upload page`)),console.log(` └── .env.local # Environment variables`),console.log(c.default.cyan(` 🔧 Configuration:`)),console.log(` Provider: ${r.toUpperCase()}`),console.log(` Bucket: ${a.bucket}`),console.log(` Region: ${a.region}`),console.log(` API Route: ${s}`),console.log(c.default.cyan(` 🚀 Next steps:`)),console.log(` 1. Run: npm run dev`),l?(console.log(` 2. Visit: http://localhost:3000/upload`),console.log(` 3. Try uploading a file!`)):(console.log(` 2. Import components from pushduck`),console.log(` 3. Build your upload interface!`)),console.log(c.default.cyan(` 📚 Documentation: https://pushduck.dev/docs`)),a.accessKeyId&&a.secretAccessKey){console.log(c.default.cyan(` 🔍 Testing your configuration...`));let e=await R(a,r);e?console.log(c.default.green(` 🎉 Everything looks good! Your upload system is ready.`)):(console.log(c.default.yellow(` ⚠ Connection test failed. Please check your credentials and try again.`)),console.log(c.default.gray(`You can test your configuration anytime with: npx @pushduck/cli test`)))}console.log(c.default.cyan(` 🔒 Important: CORS Configuration`)),console.log(c.default.yellow(`Have you configured CORS for your bucket?`)),console.log(c.default.gray(`File uploads from browsers require proper CORS settings.`));let d=Z(r);console.log(c.default.cyan(` 📖 Setup guides:`)),console.log(` CORS Setup: ${d.corsSetup}`),console.log(` Provider Docs: ${d.providerDocs}`);let{corsConfigured:m}=await f.default.prompt([{type:`confirm`,name:`corsConfigured`,message:`Have you already configured CORS?`,default:!1}]);m?console.log(c.default.green(` ✅ Great! Your CORS configuration should be ready.`)):(console.log(c.default.yellow(` ⚠ Please configure CORS before testing uploads:`)),console.log(c.default.gray(` 1. Follow the CORS setup guide above`)),console.log(c.default.gray(` 2. Test your configuration with: npx @pushduck/cli test`)),console.log(c.default.gray(` 3. If uploads fail, double-check your CORS settings`)))}catch(t){console.error(c.default.red(` ❌ Setup failed:`),t),e.verbose&&console.error(t),console.log(c.default.gray(` For help, visit: https://pushduck.dev/docs/api/troubleshooting`)),process.exit(1)}}async function ee(e={}){console.log(c.default.cyan(`🔍 Testing your S3 upload configuration... `));try{let e=await j();if(!e.hasExistingUpload){console.log(c.default.yellow(`⚠ No upload configuration found.`)),console.log(c.default.gray(`Run: npx @pushduck/cli@latest init`));return}let t=[`aws`,`cloudflare-r2`,`digitalocean`,`minio`,`gcs`],n=!1;for(let e of t){let t=await F(e);if(t.accessKeyId&&t.secretAccessKey&&t.bucket){console.log(c.default.green(`✓ Found ${e.toUpperCase()} credentials`));let r=await R(t,e);r?(console.log(c.default.green(`✓ Connection test successful`)),console.log(c.default.green(` 🎉 Your upload configuration is working correctly!`))):console.log(c.default.red(`❌ Connection test failed`)),n=!0;break}}n||(console.log(c.default.yellow(`⚠ No valid credentials found in environment variables`)),console.log(c.default.gray(`Make sure your .env.local file contains the required variables`)))}catch(t){console.error(c.default.red(`❌ Test failed:`),t),e.verbose&&console.error(t)}}const $=new l.Command;$.name(`pushduck`).description(`Zero-config setup for Next.js S3 file uploads`).version(`0.1.0`),$.command(`init`).description(`Initialize S3 upload configuration in your Next.js project`).option(`--provider <type>`,`Skip provider selection (aws|cloudflare-r2|digitalocean|minio|gcs)`).option(`--skip-examples`,`Don't generate example components`).option(`--skip-bucket`,`Don't create S3 bucket automatically`).option(`--api-path <path>`,`Custom API route path`,`/api/upload`).option(`--dry-run`,`Show what would be created without creating`).option(`--verbose`,`Show detailed output`).action(Q),$.command(`test`).description(`Test your current S3 upload configuration`).option(`--verbose`,`Show detailed test output`).action(ee),$.command(`add-route`).description(`Add a new upload route to existing configuration`).action(E),$.command(`add [component]`).description(`Add a UI component from the pushduck registry`).option(`--registry <url>`,`Custom registry URL`).option(`--path <path>`,`Installation path`).action(async(e,t)=>{await y(e)}),$.on(`command:*`,()=>{console.log(c.default.red(`Unknown command: ${$.args.join(` `)}`)),console.log(`Run --help for available commands`),process.exit(1)}),process.argv.slice(2).length||$.outputHelp(),$.parse(process.argv);