@pushduck/cli
Version:
Official CLI for pushduck - Add S3 upload functionality to your Next.js project
535 lines (493 loc) • 49.3 kB
JavaScript
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);