create-sailor 1.1.0 → 1.3.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 +96 -23
  2. package/dist/index.js +2782 -62
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,61 +1,1839 @@
1
1
  #!/usr/bin/env node
2
- import ct from"fs";import Q from"path";import*as h from"@clack/prompts";import{Command as pt}from"commander";import I from"picocolors";import lt from"update-notifier";import qe from"cfonts";import Ue from"picocolors";var ne="1.0.0";function se(){if(process.env.NO_COLOR||!process.stdout.isTTY){process.stdout.write([`Sailor v${ne}`,"AI-Native SaaS Unicorn Template","by Nebutra \xB7 https://nebutra.com","",""].join(`
3
- `));return}qe.say("Sailor",{font:"block",align:"left",colors:["#0033FE","#0BF1C3"],gradient:["#0033FE","#0BF1C3"],transitionGradient:!0,space:!1}),process.stdout.write(Ue.dim(` AI-Native SaaS Template \xB7 v${ne} \xB7 nebutra.com
2
+ import{execSync as Je}from"child_process";import un from"fs";import pt from"path";import*as I from"@clack/prompts";import{Command as ts}from"commander";import T from"picocolors";import ns from"update-notifier";import xn from"cfonts";import Rn from"picocolors";import dt from"fs";import Ze from"path";import{fileURLToPath as Nn}from"url";function Tn(){try{let t=Ze.dirname(Nn(import.meta.url));for(let n=0;n<4;n++){let r=Ze.join(t,"package.json");if(dt.existsSync(r)){let o=dt.readFileSync(r,"utf-8"),s=JSON.parse(o);if(s.name==="create-sailor"&&typeof s.version=="string")return s.version}t=Ze.dirname(t)}return"0.0.0-dev"}catch{return"0.0.0-dev"}}var J=Tn();function ut(){if(process.env.NO_COLOR||!process.stdout.isTTY){process.stdout.write([`Sailor v${J}`,"AI-Native SaaS Unicorn Template","by Nebutra \xB7 https://nebutra.com","",""].join(`
3
+ `));return}xn.say("Sailor",{font:"block",align:"left",colors:["#0033FE","#0BF1C3"],gradient:["#0033FE","#0BF1C3"],transitionGradient:!0,space:!1}),process.stdout.write(Rn.dim(` AI-Native SaaS Template \xB7 v${J} \xB7 nebutra.com
4
4
 
5
- `))}import k from"picocolors";function Fe(){return!process.env.NO_COLOR&&!!process.stdout.isTTY}function re(e){let t=Fe(),n=t?"\u2693":"-",s=t?"\u25B8":"->",r=t?"\u2605":"*",i=t?"\u{1F4E7}":"*",c=["",` ${t?k.bold(`${n} Done in ${e.elapsedSec}s \xB7 ${e.targetDir}/`):`${n} Done in ${e.elapsedSec}s \xB7 ${e.targetDir}/`}`,"",` ${t?k.bold("Next:"):"Next:"}`,` ${s} cd ${e.targetDir}`];e.skippedInstall&&c.push(` ${s} pnpm install (if --no-install was set)`),c.push(` ${s} pnpm dev ${t?k.dim("\u2192 http://localhost:3000"):"-> http://localhost:3000"}`,"",` ${t?k.bold("Customize:"):"Customize:"}`,` ${s} pnpm sailor brand init ${t?k.dim("\u2192 your colors, logo, domain"):"-> your colors, logo, domain"}`,` ${s} pnpm sailor preset apply ${t?k.dim("\u2192 toggle features"):"-> toggle features"}`,` ${s} pnpm sailor add <feat> ${t?k.dim("\u2192 add queue/search/cache/..."):"-> add queue/search/cache/..."}`,"",` ${t?k.bold("Ship faster:"):"Ship faster:"}`,` ${r} Star us https://github.com/nebutra/nebutra-sailor`,` ${i} Free license https://nebutra.com/get-license`,""),process.stdout.write(c.join(`
5
+ `))}import M from"picocolors";function kn(){return!process.env.NO_COLOR&&!!process.stdout.isTTY}function mt(e){let t=kn(),n=t?"\u2693":"-",r=t?"\u25B8":"->",o=t?"\u2605":"*",s=t?"\u{1F4E7}":"*",c=["",` ${t?M.bold(`${n} Done in ${e.elapsedSec}s \xB7 ${e.targetDir}/`):`${n} Done in ${e.elapsedSec}s \xB7 ${e.targetDir}/`}`,"",` ${t?M.bold("Next:"):"Next:"}`,` ${r} cd ${e.targetDir}`];e.skippedInstall&&c.push(` ${r} pnpm install (if --no-install was set)`),c.push(` ${r} pnpm dev ${t?M.dim("\u2192 http://localhost:3000"):"-> http://localhost:3000"}`,"",` ${t?M.bold("Customize:"):"Customize:"}`,` ${r} pnpm sailor brand init ${t?M.dim("\u2192 your colors, logo, domain"):"-> your colors, logo, domain"}`,` ${r} pnpm sailor preset apply ${t?M.dim("\u2192 toggle features"):"-> toggle features"}`,` ${r} pnpm sailor add <feat> ${t?M.dim("\u2192 add queue/search/cache/..."):"-> add queue/search/cache/..."}`,"",` ${t?M.bold("Add more features later:"):"Add more features later:"}`,` ${r} sailor add payment --provider=wechat`,` ${r} sailor add email --provider=postmark`,` ${r} sailor add storage --provider=supabase`,` ${t?M.dim("(more providers: `sailor add --list`)"):"(more providers: `sailor add --list`)"}`,"",` ${t?M.bold("Ship faster:"):"Ship faster:"}`,` ${o} Star us https://github.com/nebutra/nebutra-sailor`,` ${s} Free license https://nebutra.com/get-license`,""),process.stdout.write(c.join(`
6
6
  `)+`
7
- `)}var De=`Usage: create-sailor [name] [options]
7
+ `)}var On=`Usage: create-sailor [name] [options]
8
8
 
9
9
  Arguments:
10
- name project directory (default: ./my-saas-app)
11
-
12
- Options:
13
- -p, --pm <id> npm | pnpm | yarn | bun (auto-detected)
14
- --orm <id> prisma | drizzle | none
15
- --db <id> postgres | mysql | sqlite | none
16
- --auth <id> clerk | betterauth | none
17
- --payment <id> stripe | lemon | wechat | alipay | none
18
- --ai <ids> comma-separated provider ids (e.g. openai,anthropic)
19
- --deploy <target> vercel | railway | cloudflare | selfhost
20
- --docs <id> fumadocs | mintlify | docusaurus | nextra | vitepress | none
21
- --i18n enable i18n (default: true)
22
- --no-i18n disable
23
- --no-install skip package install
24
- --no-git skip git init
25
- -y, --yes accept all defaults (non-interactive)
26
- --dry-run preview actions without writing files
27
- --json machine-readable output
28
- --no-color disable color output
29
- -h, --help show this help
30
- -v, --version show version
10
+ name project directory (default: ./my-saas-app)
11
+
12
+ Core options:
13
+ -p, --pm <id> npm | pnpm | yarn | bun (auto-detected)
14
+ --region <id> global | cn | hybrid (default: global)
15
+ --auth <id> clerk | betterauth | none
16
+ --ai <ids> comma-separated provider ids (e.g. openai,anthropic)
17
+
18
+ Stack flags (region-based smart defaults):
19
+ --orm <id> prisma | drizzle | none (default: prisma)
20
+ --db <id> postgres | mysql | sqlite | none (default: postgres)
21
+ --payment <id> stripe | lemon | wechat | alipay | none (default: region-based)
22
+ --deploy <target> vercel | railway | cloudflare | selfhost (default: vercel)
23
+ --docs <id> fumadocs | mintlify | docusaurus | nextra | vitepress | none
24
+
25
+ Feature flags (region-based smart defaults):
26
+ --email <id> resend | postmark | ses | aliyun-dm | tencent-ses | netease | none
27
+ --storage <id> r2 | s3 | supabase | aliyun-oss | tencent-cos | qiniu | none
28
+ --monitoring <id> sentry | datadog | aliyun-arms | tingyun | none
29
+ --analytics <id> posthog | plausible | umami | baidu | sensors | none
30
+ --sms <id> twilio | aliyun-sms | tencent-sms | yunpian | none
31
+
32
+ Toggles:
33
+ --i18n enable i18n (default: true)
34
+ --no-i18n disable
35
+ --no-install skip package install
36
+ --no-git skip git init
37
+ -y, --yes accept all defaults (non-interactive)
38
+ --dry-run preview actions without writing files
39
+ --json machine-readable output
40
+ --no-color disable color output
41
+ -h, --help show this help
42
+ -v, --version show version
31
43
 
32
44
  Examples:
33
45
  $ npm create sailor@latest
34
46
  $ npm create sailor@latest my-app -y
35
- $ npm create sailor@latest my-app --auth=clerk --ai=openai,deepseek
36
- $ npm create sailor@latest --dry-run
37
- `;function oe(){process.stdout.write(De)}import B from"picocolors";function ae(){return!process.env.NO_COLOR&&!!process.stdout.isTTY}function Ye(e){let t=ae()?"\u25B8":"->",n=`[${e.index}/${e.total}]`,s=e.label.padEnd(18," ");return ae()?`${B.dim(n)} ${B.bold(s)} ${B.dim(t)} ${B.cyan(e.value)}`:`${n} ${s} ${t} ${e.value}`}function ie(e){process.stdout.write(Ye(e)+`
38
- `)}var G=[{id:"anthropic",name:"Anthropic",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://docs.anthropic.com",envVarPrefix:"ANTHROPIC",requiredEnvVars:["ANTHROPIC_API_KEY"]},{id:"openai",name:"OpenAI",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.openai.com/docs",envVarPrefix:"OPENAI",requiredEnvVars:["OPENAI_API_KEY"]},{id:"deepseek",name:"DeepSeek",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.deepseek.com/api-docs",envVarPrefix:"DEEPSEEK",requiredEnvVars:["DEEPSEEK_API_KEY"]},{id:"xai",name:"xAI (Grok)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://docs.x.ai/api",envVarPrefix:"XAI",requiredEnvVars:["XAI_API_KEY"]},{id:"moonshot",name:"Moonshot (Kimi)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.moonshot.cn/docs",envVarPrefix:"MOONSHOT",requiredEnvVars:["MOONSHOT_API_KEY","MOONSHOT_BASE_URL"]},{id:"google",name:"Google Gemini",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://ai.google.dev/gemini-api/docs",envVarPrefix:"GOOGLE_GENERATIVE_AI",requiredEnvVars:["GOOGLE_GENERATIVE_AI_API_KEY"]},{id:"mistral",name:"Mistral AI",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://docs.mistral.ai",envVarPrefix:"MISTRAL",requiredEnvVars:["MISTRAL_API_KEY"]},{id:"cohere",name:"Cohere",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://docs.cohere.com",envVarPrefix:"COHERE",requiredEnvVars:["COHERE_API_KEY"]},{id:"perplexity",name:"Perplexity",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://docs.perplexity.ai",baseURL:"https://api.perplexity.ai",envVarPrefix:"PERPLEXITY",requiredEnvVars:["PERPLEXITY_API_KEY"]},{id:"ai21",name:"AI21 Labs",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://docs.ai21.com",baseURL:"https://api.ai21.com/studio/v1",envVarPrefix:"AI21",requiredEnvVars:["AI21_API_KEY"]},{id:"upstage",name:"Upstage (Solar)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://developers.upstage.ai/docs",baseURL:"https://api.upstage.ai/v1/solar",envVarPrefix:"UPSTAGE",requiredEnvVars:["UPSTAGE_API_KEY"]},{id:"siliconflow",name:"\u7845\u57FA\u6D41\u52A8 SiliconFlow",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://docs.siliconflow.cn",baseURL:"https://api.siliconflow.cn/v1",envVarPrefix:"SILICONFLOW",requiredEnvVars:["SILICONFLOW_API_KEY"]},{id:"volcengine-ark",name:"\u706B\u5C71\u5F15\u64CE ARK (\u8C46\u5305)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://www.volcengine.com/docs/82379",baseURL:"https://ark.cn-beijing.volces.com/api/v3",envVarPrefix:"ARK",requiredEnvVars:["ARK_API_KEY"]},{id:"bailian",name:"\u767E\u70BC Bailian (\u901A\u4E49)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://help.aliyun.com/zh/model-studio",baseURL:"https://dashscope.aliyuncs.com/compatible-mode/v1",envVarPrefix:"DASHSCOPE",requiredEnvVars:["DASHSCOPE_API_KEY"]},{id:"zhipu",name:"\u667A\u8C31 AI Zhipu",category:"\u56FD\u5185\u5E73\u53F0",status:"ai-sdk",docs:"https://docs.bigmodel.cn",baseURL:"https://open.bigmodel.cn/api/paas/v4",envVarPrefix:"ZHIPU",requiredEnvVars:["ZHIPU_API_KEY"]},{id:"baichuan",name:"\u767E\u5DDD\u667A\u80FD Baichuan",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.baichuan-ai.com/docs",baseURL:"https://api.baichuan-ai.com/v1",envVarPrefix:"BAICHUAN",requiredEnvVars:["BAICHUAN_API_KEY"]},{id:"minimax",name:"MiniMax (ABAB)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.minimaxi.com/document/guides",baseURL:"https://api.minimax.chat/v1",envVarPrefix:"MINIMAX",requiredEnvVars:["MINIMAX_API_KEY","MINIMAX_GROUP_ID"]},{id:"stepfun",name:"\u9636\u8DC3\u661F\u8FB0 StepFun",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.stepfun.com/docs",baseURL:"https://api.stepfun.com/v1",envVarPrefix:"STEPFUN",requiredEnvVars:["STEPFUN_API_KEY"]},{id:"sensetime",name:"\u5546\u6C64 SenseNova",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.sensetime.com",baseURL:"https://api.sensenova.cn/v1",envVarPrefix:"SENSENOVA",requiredEnvVars:["SENSENOVA_API_KEY"]},{id:"tencent",name:"\u817E\u8BAF\u6DF7\u5143 Hunyuan",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://cloud.tencent.com/document/product/1729",baseURL:"https://api.hunyuan.cloud.tencent.com/v1",envVarPrefix:"HUNYUAN",requiredEnvVars:["HUNYUAN_API_KEY"]},{id:"lingyi",name:"\u96F6\u4E00\u4E07\u7269 Yi",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.lingyiwanwu.com/docs",baseURL:"https://api.lingyiwanwu.com/v1",envVarPrefix:"YI",requiredEnvVars:["YI_API_KEY"]},{id:"openrouter",name:"OpenRouter",category:"\u7EDF\u4E00\u7F51\u5173",status:"opencode",docs:"https://openrouter.ai/docs",envVarPrefix:"OPENROUTER",requiredEnvVars:["OPENROUTER_API_KEY"]},{id:"vercel-gateway",name:"Vercel AI Gateway",category:"\u7EDF\u4E00\u7F51\u5173",status:"opencode",docs:"https://vercel.com/docs/ai-gateway",envVarPrefix:"AI_GATEWAY",requiredEnvVars:["AI_GATEWAY_API_KEY"]},{id:"litellm",name:"LiteLLM Proxy",category:"\u7EDF\u4E00\u7F51\u5173",status:"cn-compatible",docs:"https://docs.litellm.ai",baseURL:"http://localhost:4000/v1",envVarPrefix:"LITELLM",requiredEnvVars:["LITELLM_API_KEY"]},{id:"portkey",name:"Portkey",category:"\u7EDF\u4E00\u7F51\u5173",status:"cn-compatible",docs:"https://portkey.ai/docs",baseURL:"https://api.portkey.ai/v1",envVarPrefix:"PORTKEY",requiredEnvVars:["PORTKEY_API_KEY"]},{id:"aws-bedrock",name:"AWS Bedrock",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://aws.amazon.com/bedrock/",envVarPrefix:"AWS_BEDROCK",requiredEnvVars:["AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_REGION"]},{id:"azure-openai",name:"Azure OpenAI",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://azure.microsoft.com/en-us/products/cognitive-services/openai-service/",envVarPrefix:"AZURE_OPENAI",requiredEnvVars:["AZURE_OPENAI_API_KEY","AZURE_OPENAI_ENDPOINT"]},{id:"gcp-vertex",name:"GCP Vertex AI",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://cloud.google.com/vertex-ai",envVarPrefix:"GOOGLE_VERTEX",requiredEnvVars:["GOOGLE_APPLICATION_CREDENTIALS"]},{id:"groq",name:"Groq",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://console.groq.com/docs",envVarPrefix:"GROQ",requiredEnvVars:["GROQ_API_KEY"]},{id:"fireworks",name:"Fireworks AI",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://docs.fireworks.ai",envVarPrefix:"FIREWORKS",requiredEnvVars:["FIREWORKS_API_KEY"]},{id:"together",name:"Together AI",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://docs.together.ai",envVarPrefix:"TOGETHER",requiredEnvVars:["TOGETHER_API_KEY"]},{id:"huggingface",name:"Hugging Face",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://huggingface.co/docs/inference-providers",envVarPrefix:"HF",requiredEnvVars:["HF_API_KEY"]},{id:"replicate",name:"Replicate",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://replicate.com/docs",envVarPrefix:"REPLICATE",requiredEnvVars:["REPLICATE_API_TOKEN"]},{id:"lepton",name:"Lepton AI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://www.lepton.ai/docs",baseURL:"https://api.lepton.ai/v1",envVarPrefix:"LEPTON",requiredEnvVars:["LEPTON_API_KEY"]},{id:"anyscale",name:"Anyscale Endpoints",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://docs.endpoints.anyscale.com/",baseURL:"https://api.endpoints.anyscale.com/v1",envVarPrefix:"ANYSCALE",requiredEnvVars:["ANYSCALE_API_KEY"]},{id:"octoai",name:"OctoAI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://octoai.cloud/docs",baseURL:"https://text.octoai.run/v1",envVarPrefix:"OCTOAI",requiredEnvVars:["OCTOAI_TOKEN"]},{id:"deepinfra",name:"DeepInfra",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://deepinfra.com/docs",baseURL:"https://api.deepinfra.com/v1/openai",envVarPrefix:"DEEPINFRA",requiredEnvVars:["DEEPINFRA_API_KEY"]},{id:"novita",name:"Novita AI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://novita.ai/docs",baseURL:"https://api.novita.ai/v3/openai",envVarPrefix:"NOVITA",requiredEnvVars:["NOVITA_API_KEY"]},{id:"black-forest-labs",name:"Black Forest Labs (FLUX)",category:"\u591A\u6A21\u6001",status:"ai-sdk",docs:"https://docs.bfl.ml",envVarPrefix:"BFL",requiredEnvVars:["BFL_API_KEY"]},{id:"elevenlabs",name:"ElevenLabs (TTS)",category:"\u591A\u6A21\u6001",status:"ai-sdk",docs:"https://elevenlabs.io/docs",envVarPrefix:"ELEVENLABS",requiredEnvVars:["ELEVENLABS_API_KEY"]},{id:"runway",name:"Runway (Video)",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://runwayml.com",envVarPrefix:"RUNWAY",requiredEnvVars:["RUNWAY_API_KEY"]},{id:"luma",name:"Luma (Video)",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://lumalabs.ai",envVarPrefix:"LUMA",requiredEnvVars:["LUMA_API_KEY"]},{id:"midjourney",name:"Midjourney",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://docs.midjourney.com",envVarPrefix:"MIDJOURNEY",requiredEnvVars:[]},{id:"ollama",name:"Ollama (\u672C\u5730)",category:"\u672C\u5730\u90E8\u7F72",status:"opencode",docs:"https://github.com/ollama/ollama",baseURL:"http://localhost:11434/v1",envVarPrefix:"OLLAMA",requiredEnvVars:["OLLAMA_BASE_URL"]},{id:"lmstudio",name:"LM Studio",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://lmstudio.ai/docs",baseURL:"http://localhost:1234/v1",envVarPrefix:"LMSTUDIO",requiredEnvVars:[]},{id:"vllm",name:"vLLM",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://docs.vllm.ai",baseURL:"http://localhost:8000/v1",envVarPrefix:"VLLM",requiredEnvVars:[]},{id:"tgi",name:"HF TGI",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://huggingface.co/docs/text-generation-inference",baseURL:"http://localhost:8080/v1",envVarPrefix:"TGI",requiredEnvVars:[]}],xt=G.reduce((e,t)=>(e[t.category]||(e[t.category]=[]),e[t.category].push(t),e),{});import Me from"fs";import Ke from"path";async function ce(e,t){let n=Ke.join(e,"nebutra.config.json");Me.writeFileSync(n,JSON.stringify(t,null,2)+`
39
- `)}import Y from"fs";import x from"path";import{fileURLToPath as $e}from"url";var We=$e(import.meta.url),Be=x.dirname(We);async function pe(e,t){if(t==="none")return;let n=x.join(Be,"../../templates/deploy");t==="vercel"?await Y.promises.copyFile(x.join(n,"vercel.json"),x.join(e,"vercel.json")):t==="railway"?await Y.promises.copyFile(x.join(n,"railway.toml"),x.join(e,"railway.toml")):t==="cloudflare"?await Y.promises.copyFile(x.join(n,"wrangler.toml"),x.join(e,"wrangler.toml")):t==="selfhost"&&(await Y.promises.copyFile(x.join(n,"docker-compose.yml"),x.join(e,"docker-compose.yml")),await Y.promises.copyFile(x.join(n,"Dockerfile.web"),x.join(e,"Dockerfile.web")))}import _ from"fs";import O from"path";import{fileURLToPath as Ge}from"url";var ze=Ge(import.meta.url),le=O.dirname(ze),He=/\.(md|mdx|json|ts|tsx|js|mjs|cjs|yaml|yml)$/;function Je(e){let t=[O.join(le,"..","templates","docs",e),O.join(le,"..","..","templates","docs",e)];for(let n of t)if(_.existsSync(n))return n;return t[0]}async function de(e,t){if(t.framework==="none")return;let n=(t.framework==="fumadocs","fumadocs"),s=Je(n),r=O.join(e,"apps","docs");if(!_.existsSync(s))throw new Error(`Docs template not found: ${s}`);ue(s,r),me(r,{"{PRODUCT_NAME}":t.projectName})}function ue(e,t){_.mkdirSync(t,{recursive:!0});for(let n of _.readdirSync(e,{withFileTypes:!0})){let s=O.join(e,n.name),r=O.join(t,n.name);n.isDirectory()?ue(s,r):_.copyFileSync(s,r)}}function me(e,t){for(let n of _.readdirSync(e,{withFileTypes:!0})){let s=O.join(e,n.name);if(n.isDirectory())me(s,t);else if(He.test(n.name)){let r=_.readFileSync(s,"utf-8");for(let[i,l]of Object.entries(t))r=r.split(i).join(l);_.writeFileSync(s,r)}}}import z from"fs";import fe from"path";async function ye(e,t){let n=`# Automatically injected by create-sailor
47
+ $ npm create sailor@latest my-app --region=cn --auth=clerk
48
+ $ npm create sailor@latest my-app --region=hybrid --ai=openai,deepseek
49
+ $ npm create sailor@latest --dry-run --region=cn
50
+ `;function gt(){process.stdout.write(On)}import qe from"picocolors";function ft(){return!process.env.NO_COLOR&&!!process.stdout.isTTY}function Un(e){let t=ft()?"\u25B8":"->",n=`[${e.index}/${e.total}]`,r=e.label.padEnd(18," ");return ft()?`${qe.dim(n)} ${qe.bold(r)} ${qe.dim(t)} ${qe.cyan(e.value)}`:`${n} ${r} ${t} ${e.value}`}function yt(e){process.stdout.write(Un(e)+`
51
+ `)}var Ge=[{id:"anthropic",name:"Anthropic",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://docs.anthropic.com",envVarPrefix:"ANTHROPIC",requiredEnvVars:["ANTHROPIC_API_KEY"]},{id:"openai",name:"OpenAI",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.openai.com/docs",envVarPrefix:"OPENAI",requiredEnvVars:["OPENAI_API_KEY"]},{id:"deepseek",name:"DeepSeek",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.deepseek.com/api-docs",envVarPrefix:"DEEPSEEK",requiredEnvVars:["DEEPSEEK_API_KEY"]},{id:"xai",name:"xAI (Grok)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://docs.x.ai/api",envVarPrefix:"XAI",requiredEnvVars:["XAI_API_KEY"]},{id:"moonshot",name:"Moonshot (Kimi)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.moonshot.cn/docs",envVarPrefix:"MOONSHOT",requiredEnvVars:["MOONSHOT_API_KEY","MOONSHOT_BASE_URL"]},{id:"google",name:"Google Gemini",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://ai.google.dev/gemini-api/docs",envVarPrefix:"GOOGLE_GENERATIVE_AI",requiredEnvVars:["GOOGLE_GENERATIVE_AI_API_KEY"]},{id:"mistral",name:"Mistral AI",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://docs.mistral.ai",envVarPrefix:"MISTRAL",requiredEnvVars:["MISTRAL_API_KEY"]},{id:"cohere",name:"Cohere",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://docs.cohere.com",envVarPrefix:"COHERE",requiredEnvVars:["COHERE_API_KEY"]},{id:"perplexity",name:"Perplexity",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://docs.perplexity.ai",baseURL:"https://api.perplexity.ai",envVarPrefix:"PERPLEXITY",requiredEnvVars:["PERPLEXITY_API_KEY"]},{id:"ai21",name:"AI21 Labs",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://docs.ai21.com",baseURL:"https://api.ai21.com/studio/v1",envVarPrefix:"AI21",requiredEnvVars:["AI21_API_KEY"]},{id:"upstage",name:"Upstage (Solar)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://developers.upstage.ai/docs",baseURL:"https://api.upstage.ai/v1/solar",envVarPrefix:"UPSTAGE",requiredEnvVars:["UPSTAGE_API_KEY"]},{id:"siliconflow",name:"\u7845\u57FA\u6D41\u52A8 SiliconFlow",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://docs.siliconflow.cn",baseURL:"https://api.siliconflow.cn/v1",envVarPrefix:"SILICONFLOW",requiredEnvVars:["SILICONFLOW_API_KEY"]},{id:"volcengine-ark",name:"\u706B\u5C71\u5F15\u64CE ARK (\u8C46\u5305)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://www.volcengine.com/docs/82379",baseURL:"https://ark.cn-beijing.volces.com/api/v3",envVarPrefix:"ARK",requiredEnvVars:["ARK_API_KEY"]},{id:"bailian",name:"\u767E\u70BC Bailian (\u901A\u4E49)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://help.aliyun.com/zh/model-studio",baseURL:"https://dashscope.aliyuncs.com/compatible-mode/v1",envVarPrefix:"DASHSCOPE",requiredEnvVars:["DASHSCOPE_API_KEY"]},{id:"zhipu",name:"\u667A\u8C31 AI Zhipu",category:"\u56FD\u5185\u5E73\u53F0",status:"ai-sdk",docs:"https://docs.bigmodel.cn",baseURL:"https://open.bigmodel.cn/api/paas/v4",envVarPrefix:"ZHIPU",requiredEnvVars:["ZHIPU_API_KEY"]},{id:"baichuan",name:"\u767E\u5DDD\u667A\u80FD Baichuan",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.baichuan-ai.com/docs",baseURL:"https://api.baichuan-ai.com/v1",envVarPrefix:"BAICHUAN",requiredEnvVars:["BAICHUAN_API_KEY"]},{id:"minimax",name:"MiniMax (ABAB)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.minimaxi.com/document/guides",baseURL:"https://api.minimax.chat/v1",envVarPrefix:"MINIMAX",requiredEnvVars:["MINIMAX_API_KEY","MINIMAX_GROUP_ID"]},{id:"stepfun",name:"\u9636\u8DC3\u661F\u8FB0 StepFun",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.stepfun.com/docs",baseURL:"https://api.stepfun.com/v1",envVarPrefix:"STEPFUN",requiredEnvVars:["STEPFUN_API_KEY"]},{id:"sensetime",name:"\u5546\u6C64 SenseNova",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.sensetime.com",baseURL:"https://api.sensenova.cn/v1",envVarPrefix:"SENSENOVA",requiredEnvVars:["SENSENOVA_API_KEY"]},{id:"tencent",name:"\u817E\u8BAF\u6DF7\u5143 Hunyuan",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://cloud.tencent.com/document/product/1729",baseURL:"https://api.hunyuan.cloud.tencent.com/v1",envVarPrefix:"HUNYUAN",requiredEnvVars:["HUNYUAN_API_KEY"]},{id:"lingyi",name:"\u96F6\u4E00\u4E07\u7269 Yi",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.lingyiwanwu.com/docs",baseURL:"https://api.lingyiwanwu.com/v1",envVarPrefix:"YI",requiredEnvVars:["YI_API_KEY"]},{id:"openrouter",name:"OpenRouter",category:"\u7EDF\u4E00\u7F51\u5173",status:"opencode",docs:"https://openrouter.ai/docs",envVarPrefix:"OPENROUTER",requiredEnvVars:["OPENROUTER_API_KEY"]},{id:"vercel-gateway",name:"Vercel AI Gateway",category:"\u7EDF\u4E00\u7F51\u5173",status:"opencode",docs:"https://vercel.com/docs/ai-gateway",envVarPrefix:"AI_GATEWAY",requiredEnvVars:["AI_GATEWAY_API_KEY"]},{id:"litellm",name:"LiteLLM Proxy",category:"\u7EDF\u4E00\u7F51\u5173",status:"cn-compatible",docs:"https://docs.litellm.ai",baseURL:"http://localhost:4000/v1",envVarPrefix:"LITELLM",requiredEnvVars:["LITELLM_API_KEY"]},{id:"portkey",name:"Portkey",category:"\u7EDF\u4E00\u7F51\u5173",status:"cn-compatible",docs:"https://portkey.ai/docs",baseURL:"https://api.portkey.ai/v1",envVarPrefix:"PORTKEY",requiredEnvVars:["PORTKEY_API_KEY"]},{id:"aws-bedrock",name:"AWS Bedrock",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://aws.amazon.com/bedrock/",envVarPrefix:"AWS_BEDROCK",requiredEnvVars:["AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_REGION"]},{id:"azure-openai",name:"Azure OpenAI",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://azure.microsoft.com/en-us/products/cognitive-services/openai-service/",envVarPrefix:"AZURE_OPENAI",requiredEnvVars:["AZURE_OPENAI_API_KEY","AZURE_OPENAI_ENDPOINT"]},{id:"gcp-vertex",name:"GCP Vertex AI",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://cloud.google.com/vertex-ai",envVarPrefix:"GOOGLE_VERTEX",requiredEnvVars:["GOOGLE_APPLICATION_CREDENTIALS"]},{id:"groq",name:"Groq",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://console.groq.com/docs",envVarPrefix:"GROQ",requiredEnvVars:["GROQ_API_KEY"]},{id:"fireworks",name:"Fireworks AI",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://docs.fireworks.ai",envVarPrefix:"FIREWORKS",requiredEnvVars:["FIREWORKS_API_KEY"]},{id:"together",name:"Together AI",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://docs.together.ai",envVarPrefix:"TOGETHER",requiredEnvVars:["TOGETHER_API_KEY"]},{id:"huggingface",name:"Hugging Face",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://huggingface.co/docs/inference-providers",envVarPrefix:"HF",requiredEnvVars:["HF_API_KEY"]},{id:"replicate",name:"Replicate",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://replicate.com/docs",envVarPrefix:"REPLICATE",requiredEnvVars:["REPLICATE_API_TOKEN"]},{id:"lepton",name:"Lepton AI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://www.lepton.ai/docs",baseURL:"https://api.lepton.ai/v1",envVarPrefix:"LEPTON",requiredEnvVars:["LEPTON_API_KEY"]},{id:"anyscale",name:"Anyscale Endpoints",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://docs.endpoints.anyscale.com/",baseURL:"https://api.endpoints.anyscale.com/v1",envVarPrefix:"ANYSCALE",requiredEnvVars:["ANYSCALE_API_KEY"]},{id:"octoai",name:"OctoAI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://octoai.cloud/docs",baseURL:"https://text.octoai.run/v1",envVarPrefix:"OCTOAI",requiredEnvVars:["OCTOAI_TOKEN"]},{id:"deepinfra",name:"DeepInfra",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://deepinfra.com/docs",baseURL:"https://api.deepinfra.com/v1/openai",envVarPrefix:"DEEPINFRA",requiredEnvVars:["DEEPINFRA_API_KEY"]},{id:"novita",name:"Novita AI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://novita.ai/docs",baseURL:"https://api.novita.ai/v3/openai",envVarPrefix:"NOVITA",requiredEnvVars:["NOVITA_API_KEY"]},{id:"black-forest-labs",name:"Black Forest Labs (FLUX)",category:"\u591A\u6A21\u6001",status:"ai-sdk",docs:"https://docs.bfl.ml",envVarPrefix:"BFL",requiredEnvVars:["BFL_API_KEY"]},{id:"elevenlabs",name:"ElevenLabs (TTS)",category:"\u591A\u6A21\u6001",status:"ai-sdk",docs:"https://elevenlabs.io/docs",envVarPrefix:"ELEVENLABS",requiredEnvVars:["ELEVENLABS_API_KEY"]},{id:"runway",name:"Runway (Video)",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://runwayml.com",envVarPrefix:"RUNWAY",requiredEnvVars:["RUNWAY_API_KEY"]},{id:"luma",name:"Luma (Video)",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://lumalabs.ai",envVarPrefix:"LUMA",requiredEnvVars:["LUMA_API_KEY"]},{id:"midjourney",name:"Midjourney",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://docs.midjourney.com",envVarPrefix:"MIDJOURNEY",requiredEnvVars:[]},{id:"ollama",name:"Ollama (\u672C\u5730)",category:"\u672C\u5730\u90E8\u7F72",status:"opencode",docs:"https://github.com/ollama/ollama",baseURL:"http://localhost:11434/v1",envVarPrefix:"OLLAMA",requiredEnvVars:["OLLAMA_BASE_URL"]},{id:"lmstudio",name:"LM Studio",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://lmstudio.ai/docs",baseURL:"http://localhost:1234/v1",envVarPrefix:"LMSTUDIO",requiredEnvVars:[]},{id:"vllm",name:"vLLM",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://docs.vllm.ai",baseURL:"http://localhost:8000/v1",envVarPrefix:"VLLM",requiredEnvVars:[]},{id:"tgi",name:"HF TGI",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://huggingface.co/docs/text-generation-inference",baseURL:"http://localhost:8080/v1",envVarPrefix:"TGI",requiredEnvVars:[]}],hs=Ge.reduce((e,t)=>(e[t.category]||(e[t.category]=[]),e[t.category].push(t),e),{});import Ie from"fs";import A from"path";var St=[{id:"posthog",name:"PostHog",region:"both",envVars:["POSTHOG_KEY","NEXT_PUBLIC_POSTHOG_KEY","NEXT_PUBLIC_POSTHOG_HOST"],docs:"https://posthog.com/docs",pkgs:["posthog-js","posthog-node"],providerFile:"apps/web/src/lib/analytics/posthog.client.ts"},{id:"plausible",name:"Plausible",region:"global",envVars:["NEXT_PUBLIC_PLAUSIBLE_DOMAIN","NEXT_PUBLIC_PLAUSIBLE_SRC"],docs:"https://plausible.io/docs",providerFile:"apps/web/src/components/analytics/PlausibleScript.tsx"},{id:"umami",name:"Umami",region:"both",envVars:["NEXT_PUBLIC_UMAMI_WEBSITE_ID","NEXT_PUBLIC_UMAMI_SRC"],docs:"https://umami.is/docs",providerFile:"apps/web/src/components/analytics/UmamiScript.tsx"},{id:"mixpanel",name:"Mixpanel",region:"global",envVars:["MIXPANEL_TOKEN","NEXT_PUBLIC_MIXPANEL_TOKEN"],docs:"https://docs.mixpanel.com",pkgs:["mixpanel-browser","mixpanel"],providerFile:"apps/web/src/lib/analytics/mixpanel.client.ts"},{id:"baidu",name:"\u767E\u5EA6\u7EDF\u8BA1 Baidu Tongji",region:"cn",envVars:["NEXT_PUBLIC_BAIDU_STATS_ID"],docs:"https://tongji.baidu.com",providerFile:"apps/web/src/components/analytics/BaiduStatsScript.tsx"},{id:"sensors",name:"\u795E\u7B56 Sensors Analytics",region:"cn",envVars:["NEXT_PUBLIC_SENSORS_SERVER_URL","NEXT_PUBLIC_SENSORS_APP_JS_URL"],docs:"https://manual.sensorsdata.cn",pkgs:["sa-sdk-javascript"],providerFile:"apps/web/src/lib/analytics/sensors.client.ts"},{id:"growingio",name:"GrowingIO",region:"cn",envVars:["NEXT_PUBLIC_GROWINGIO_ACCOUNT_ID"],docs:"https://growingio.com/docs",providerFile:"apps/web/src/components/analytics/GrowingIoScript.tsx"},{id:"none",name:"None",region:"both",envVars:[],docs:""}],vs=St.reduce((e,t)=>(e[t.region]||(e[t.region]=[]),e[t.region].push(t),e),{});function Et(e){return St.find(t=>t.id===e)}function Ln(e){Ie.mkdirSync(e,{recursive:!0})}function j(e,t){Ln(A.dirname(e)),Ie.writeFileSync(e,t)}function Q(e,t){let n=A.join(e,".env.example");Ie.existsSync(n)?Ie.appendFileSync(n,`
52
+ `+t):Ie.writeFileSync(n,t)}function Mn(e){let t=`"use client";
53
+ /**
54
+ * PostHog browser SDK \u2014 lazy-initialized on first access.
55
+ */
56
+ import posthog from "posthog-js";
57
+
58
+ let initialized = false;
59
+
60
+ export function initPostHog(): void {
61
+ if (initialized || typeof window === "undefined") return;
62
+ const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
63
+ if (!key) return;
64
+
65
+ posthog.init(key, {
66
+ api_host:
67
+ process.env.NEXT_PUBLIC_POSTHOG_HOST ?? "https://us.i.posthog.com",
68
+ person_profiles: "identified_only",
69
+ capture_pageview: true,
70
+ capture_pageleave: true,
71
+ autocapture: true,
72
+ });
73
+ initialized = true;
74
+ }
75
+
76
+ export { posthog };
77
+ `,n=`import { PostHog } from "posthog-node";
78
+
79
+ /**
80
+ * PostHog server-side client. Remember to await client.shutdown() at the end
81
+ * of serverless requests so events are flushed before the invocation dies.
82
+ */
83
+ let client: PostHog | null = null;
84
+
85
+ export function getPostHogServerClient(): PostHog | null {
86
+ if (client) return client;
87
+ const key = process.env.POSTHOG_KEY ?? process.env.NEXT_PUBLIC_POSTHOG_KEY;
88
+ if (!key) return null;
89
+
90
+ client = new PostHog(key, {
91
+ host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? "https://us.i.posthog.com",
92
+ flushAt: 1,
93
+ flushInterval: 0,
94
+ });
95
+ return client;
96
+ }
97
+
98
+ export async function captureServerEvent(
99
+ distinctId: string,
100
+ event: string,
101
+ properties?: Record<string, unknown>,
102
+ ): Promise<void> {
103
+ const ph = getPostHogServerClient();
104
+ if (!ph) return;
105
+ ph.capture({ distinctId, event, properties });
106
+ await ph.shutdown();
107
+ }
108
+ `,r=`"use client";
109
+ /**
110
+ * <AnalyticsProvider /> \u2014 wrap the app root to enable PostHog + auto page views.
111
+ */
112
+ import { useEffect } from "react";
113
+ import { usePathname, useSearchParams } from "next/navigation";
114
+ import { initPostHog, posthog } from "../../lib/analytics/posthog.client";
115
+
116
+ export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
117
+ const pathname = usePathname();
118
+ const searchParams = useSearchParams();
119
+
120
+ useEffect(() => {
121
+ initPostHog();
122
+ }, []);
123
+
124
+ useEffect(() => {
125
+ if (!pathname) return;
126
+ const url =
127
+ window.location.origin +
128
+ pathname +
129
+ (searchParams?.toString() ? \`?\${searchParams.toString()}\` : "");
130
+ posthog.capture("$pageview", { $current_url: url });
131
+ }, [pathname, searchParams]);
132
+
133
+ return <>{children}</>;
134
+ }
135
+ `;j(A.join(e,"src","lib","analytics","posthog.client.ts"),t),j(A.join(e,"src","lib","analytics","posthog.server.ts"),n),j(A.join(e,"src","components","analytics","AnalyticsProvider.tsx"),r)}function jn(){return`# PostHog \u2014 product analytics
136
+ POSTHOG_KEY=
137
+ NEXT_PUBLIC_POSTHOG_KEY=
138
+ NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
139
+ `}function Dn(e){j(A.join(e,"src","components","analytics","PlausibleScript.tsx"),`"use client";
140
+ /**
141
+ * Plausible script tag \u2014 lightweight, privacy-friendly analytics.
142
+ */
143
+ import Script from "next/script";
144
+
145
+ export function PlausibleScript() {
146
+ const domain = process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN;
147
+ const src =
148
+ process.env.NEXT_PUBLIC_PLAUSIBLE_SRC ?? "https://plausible.io/js/script.js";
149
+ if (!domain) return null;
150
+ return (
151
+ <Script
152
+ id="plausible"
153
+ strategy="afterInteractive"
154
+ data-domain={domain}
155
+ src={src}
156
+ />
157
+ );
158
+ }
159
+ `)}function Fn(){return`# Plausible \u2014 privacy-friendly analytics
160
+ NEXT_PUBLIC_PLAUSIBLE_DOMAIN=
161
+ NEXT_PUBLIC_PLAUSIBLE_SRC=https://plausible.io/js/script.js
162
+ `}function Bn(e){j(A.join(e,"src","components","analytics","UmamiScript.tsx"),`"use client";
163
+ /**
164
+ * Umami analytics \u2014 self-hosted OR umami.is cloud.
165
+ */
166
+ import Script from "next/script";
167
+
168
+ export function UmamiScript() {
169
+ const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
170
+ const src =
171
+ process.env.NEXT_PUBLIC_UMAMI_SRC ?? "https://cloud.umami.is/script.js";
172
+ if (!websiteId) return null;
173
+ return (
174
+ <Script
175
+ id="umami"
176
+ strategy="afterInteractive"
177
+ src={src}
178
+ data-website-id={websiteId}
179
+ />
180
+ );
181
+ }
182
+ `)}function Yn(){return`# Umami \u2014 self-hostable analytics
183
+ NEXT_PUBLIC_UMAMI_WEBSITE_ID=
184
+ NEXT_PUBLIC_UMAMI_SRC=https://cloud.umami.is/script.js
185
+ `}function Kn(e){let t=`"use client";
186
+ import mixpanel from "mixpanel-browser";
187
+
188
+ let initialized = false;
189
+
190
+ export function initMixpanel(): void {
191
+ if (initialized || typeof window === "undefined") return;
192
+ const token = process.env.NEXT_PUBLIC_MIXPANEL_TOKEN;
193
+ if (!token) return;
194
+ mixpanel.init(token, {
195
+ debug: process.env.NODE_ENV !== "production",
196
+ track_pageview: true,
197
+ persistence: "localStorage",
198
+ });
199
+ initialized = true;
200
+ }
201
+
202
+ export { mixpanel };
203
+ `,n=`import Mixpanel from "mixpanel";
204
+
205
+ let client: Mixpanel.Mixpanel | null = null;
206
+
207
+ export function getMixpanelServerClient(): Mixpanel.Mixpanel | null {
208
+ if (client) return client;
209
+ const token = process.env.MIXPANEL_TOKEN ?? process.env.NEXT_PUBLIC_MIXPANEL_TOKEN;
210
+ if (!token) return null;
211
+ client = Mixpanel.init(token);
212
+ return client;
213
+ }
214
+
215
+ export function trackServer(
216
+ distinctId: string,
217
+ event: string,
218
+ properties?: Record<string, unknown>,
219
+ ): void {
220
+ const c = getMixpanelServerClient();
221
+ if (!c) return;
222
+ c.track(event, { distinct_id: distinctId, ...properties });
223
+ }
224
+ `;j(A.join(e,"src","lib","analytics","mixpanel.client.ts"),t),j(A.join(e,"src","lib","analytics","mixpanel.server.ts"),n)}function Vn(){return`# Mixpanel \u2014 product analytics
225
+ MIXPANEL_TOKEN=
226
+ NEXT_PUBLIC_MIXPANEL_TOKEN=
227
+ `}function $n(e){j(A.join(e,"src","components","analytics","BaiduStatsScript.tsx"),`"use client";
228
+ /**
229
+ * \u767E\u5EA6\u7EDF\u8BA1 Baidu Tongji \u2014 hm.js \u57CB\u70B9\u811A\u672C.
230
+ */
231
+ import Script from "next/script";
232
+
233
+ export function BaiduStatsScript() {
234
+ const id = process.env.NEXT_PUBLIC_BAIDU_STATS_ID;
235
+ if (!id) return null;
236
+ const inline = \`
237
+ var _hmt = _hmt || [];
238
+ (function() {
239
+ var hm = document.createElement("script");
240
+ hm.src = "https://hm.baidu.com/hm.js?\${id}";
241
+ var s = document.getElementsByTagName("script")[0];
242
+ s.parentNode.insertBefore(hm, s);
243
+ })();
244
+ \`;
245
+ return (
246
+ <Script
247
+ id="baidu-stats"
248
+ strategy="afterInteractive"
249
+ dangerouslySetInnerHTML={{ __html: inline }}
250
+ />
251
+ );
252
+ }
253
+ `)}function qn(){return`# \u767E\u5EA6\u7EDF\u8BA1 \u2014 \u4E2D\u56FD\u533A\u7F51\u7AD9\u5206\u6790
254
+ NEXT_PUBLIC_BAIDU_STATS_ID=
255
+ `}function Gn(e){j(A.join(e,"src","lib","analytics","sensors.client.ts"),`"use client";
256
+ /**
257
+ * \u795E\u7B56\u5206\u6790 (Sensors Analytics) \u2014 sa-sdk-javascript \u5C01\u88C5.
258
+ */
259
+ import sensors from "sa-sdk-javascript";
260
+
261
+ let initialized = false;
262
+
263
+ export function initSensors(): void {
264
+ if (initialized || typeof window === "undefined") return;
265
+ const serverUrl = process.env.NEXT_PUBLIC_SENSORS_SERVER_URL;
266
+ if (!serverUrl) return;
267
+
268
+ sensors.init({
269
+ server_url: serverUrl,
270
+ is_track_single_page: true,
271
+ use_client_time: true,
272
+ send_type: "beacon",
273
+ heatmap: {
274
+ clickmap: "default",
275
+ scroll_notice_map: "default",
276
+ },
277
+ });
278
+ sensors.quick("autoTrack");
279
+ initialized = true;
280
+ }
281
+
282
+ export { sensors };
283
+ `)}function Wn(){return`# \u795E\u7B56\u5206\u6790 Sensors Analytics
284
+ NEXT_PUBLIC_SENSORS_SERVER_URL=
285
+ NEXT_PUBLIC_SENSORS_APP_JS_URL=
286
+ `}function Xn(e){j(A.join(e,"src","components","analytics","GrowingIoScript.tsx"),`"use client";
287
+ /**
288
+ * GrowingIO CDP \u2014 inline snippet installer.
289
+ */
290
+ import Script from "next/script";
291
+
292
+ export function GrowingIoScript() {
293
+ const accountId = process.env.NEXT_PUBLIC_GROWINGIO_ACCOUNT_ID;
294
+ if (!accountId) return null;
295
+ const inline = \`
296
+ !(function(e,t,n,g,i){e[i]=e[i]||function(){(e[i].q=e[i].q||[]).push(arguments)};
297
+ var a=t.createElement(n);a.async=!0;a.src=g;
298
+ var s=t.getElementsByTagName(n)[0];s.parentNode.insertBefore(a,s);
299
+ })(window,document,"script","https://assets.giocdn.com/vds.js","gdp");
300
+ gdp("init", "\${accountId}", {});
301
+ gdp("send");
302
+ \`;
303
+ return (
304
+ <Script
305
+ id="growingio"
306
+ strategy="afterInteractive"
307
+ dangerouslySetInnerHTML={{ __html: inline }}
308
+ />
309
+ );
310
+ }
311
+ `)}function Hn(){return`# GrowingIO \u2014 \u5168\u57CB\u70B9 CDP (\u4E2D\u56FD\u533A)
312
+ NEXT_PUBLIC_GROWINGIO_ACCOUNT_ID=
313
+ `}async function ht(e,t,n="global"){let r=Et(t);if(!r||r.id==="none")return;let o=A.join(e,"apps","web");if(!Ie.existsSync(o))return;let s=r.id;try{switch(s){case"posthog":Mn(o),Q(e,jn());return;case"plausible":Dn(o),Q(e,Fn());return;case"umami":Bn(o),Q(e,Yn());return;case"mixpanel":Kn(o),Q(e,Vn());return;case"baidu":$n(o),Q(e,qn());return;case"sensors":Gn(o),Q(e,Wn());return;case"growingio":Xn(o),Q(e,Hn());return;default:return}}catch(a){throw console.error(`Failed to apply analytics selection "${t}":`,a),new Error(`Analytics scaffold for "${t}" failed \u2014 see stderr for details.`)}}import w from"fs";import Z from"path";function et(e){w.existsSync(e)&&w.rmSync(e,{recursive:!0,force:!0})}function _t(e,t){let n=Z.join(e,".env.example");w.existsSync(n)?w.appendFileSync(n,`
314
+ `+t):w.writeFileSync(n,t)}function vt(e,t){if(!w.existsSync(e))return;let n=w.readFileSync(e,"utf8"),r=n.replace(/export type AuthProviderId\s*=\s*"[^"]+"\s*\|\s*"[^"]+";/,`export type AuthProviderId = "${t}";`);r!==n&&w.writeFileSync(e,r)}function zn(e,t){let n=Z.join(e,"README.md");if(!w.existsSync(n)){w.writeFileSync(n,t);return}w.appendFileSync(n,`
315
+ `+t)}async function It(e,t){let n=Z.join(e,"packages","auth");if(!w.existsSync(n))return;if(t==="none"){et(n),zn(e,`
316
+ ## Auth
317
+
318
+ Auth was skipped at scaffold time (\`--auth=none\`). Add your preferred auth provider manually (e.g. Clerk, Better Auth, NextAuth).
319
+ `);return}let r=Z.join(n,"src","providers"),o=Z.join(n,"src","types.ts");if(t==="clerk"){et(Z.join(r,"better-auth.ts")),vt(o,"clerk"),_t(e,`# Clerk auth
320
+ CLERK_PUBLISHABLE_KEY=
321
+ CLERK_SECRET_KEY=
322
+ `);return}if(t==="betterauth"){et(Z.join(r,"clerk.ts")),vt(o,"better-auth"),_t(e,`# Better Auth
323
+ BETTER_AUTH_SECRET=
324
+ BETTER_AUTH_URL=http://localhost:3000
325
+ `);return}}var Re=[{id:"wechat",name:"\u5FAE\u4FE1\u5F00\u653E\u5E73\u53F0",region:"cn",envVars:["WECHAT_APP_ID","WECHAT_APP_SECRET"],docs:"https://open.weixin.qq.com/cgi-bin/showdocument",callbackUrl:"/api/auth/callback/wechat",tokenEndpoint:"https://api.weixin.qq.com/sns/oauth2/access_token"},{id:"qq",name:"QQ \u767B\u5F55",region:"cn",envVars:["QQ_APP_ID","QQ_APP_KEY"],docs:"https://wiki.connect.qq.com",callbackUrl:"/api/auth/callback/qq",tokenEndpoint:"https://graph.qq.com/oauth2.0/token"},{id:"dingtalk",name:"\u9489\u9489",region:"cn",envVars:["DINGTALK_APP_ID","DINGTALK_APP_SECRET"],docs:"https://open.dingtalk.com/document/",callbackUrl:"/api/auth/callback/dingtalk",tokenEndpoint:"https://api.dingtalk.com/v1.0/oauth2/userAccessToken"},{id:"workweixin",name:"\u4F01\u4E1A\u5FAE\u4FE1",region:"cn",envVars:["WORKWEIXIN_CORP_ID","WORKWEIXIN_AGENT_ID","WORKWEIXIN_SECRET"],docs:"https://developer.work.weixin.qq.com",callbackUrl:"/api/auth/callback/workweixin",tokenEndpoint:"https://qyapi.weixin.qq.com/cgi-bin/gettoken"},{id:"feishu",name:"\u98DE\u4E66 / Lark",region:"cn",envVars:["FEISHU_APP_ID","FEISHU_APP_SECRET"],docs:"https://open.feishu.cn/document/",callbackUrl:"/api/auth/callback/feishu",tokenEndpoint:"https://open.feishu.cn/open-apis/authen/v1/access_token"},{id:"weibo",name:"\u5FAE\u535A",region:"cn",envVars:["WEIBO_APP_KEY","WEIBO_APP_SECRET"],docs:"https://open.weibo.com/wiki/",callbackUrl:"/api/auth/callback/weibo",tokenEndpoint:"https://api.weibo.com/oauth2/access_token"}],Jn=new Set(Re.map(e=>e.id));function Pt(e){return Re.find(t=>t.id===e)}function bt(e){if(!e)return[];let t=e.split(",").map(o=>o.trim().toLowerCase()).filter(Boolean);if(t.includes("none"))return[];let n=[],r=new Set;for(let o of t)Jn.has(o)&&!r.has(o)&&(n.push(o),r.add(o));return n}import te from"fs";import ee from"path";function Qn(e,t){let n=`// apps/web/src/app/api/auth/callback/${e.id}/route.ts
326
+ // Generated by create-sailor \u2014 OAuth callback for ${e.name}
327
+ // Primary auth provider: ${t}
328
+ // Docs: ${e.docs}`,r={wechat:` const tokenRes = await fetch(
329
+ "${e.tokenEndpoint}?" +
330
+ new URLSearchParams({
331
+ appid: process.env.WECHAT_APP_ID ?? "",
332
+ secret: process.env.WECHAT_APP_SECRET ?? "",
333
+ code,
334
+ grant_type: "authorization_code",
335
+ }),
336
+ );
337
+ const token = (await tokenRes.json()) as {
338
+ access_token?: string;
339
+ openid?: string;
340
+ errcode?: number;
341
+ errmsg?: string;
342
+ };
343
+ if (!token.access_token || !token.openid) {
344
+ return NextResponse.redirect(new URL("/login?error=wechat_token", req.url));
345
+ }`,qq:` const tokenRes = await fetch(
346
+ "${e.tokenEndpoint}?" +
347
+ new URLSearchParams({
348
+ client_id: process.env.QQ_APP_ID ?? "",
349
+ client_secret: process.env.QQ_APP_KEY ?? "",
350
+ code,
351
+ grant_type: "authorization_code",
352
+ redirect_uri: new URL("${e.callbackUrl}", req.url).toString(),
353
+ fmt: "json",
354
+ }),
355
+ );
356
+ const token = (await tokenRes.json()) as { access_token?: string; openid?: string };
357
+ if (!token.access_token) {
358
+ return NextResponse.redirect(new URL("/login?error=qq_token", req.url));
359
+ }`,dingtalk:` const tokenRes = await fetch("${e.tokenEndpoint}", {
360
+ method: "POST",
361
+ headers: { "Content-Type": "application/json" },
362
+ body: JSON.stringify({
363
+ clientId: process.env.DINGTALK_APP_ID ?? "",
364
+ clientSecret: process.env.DINGTALK_APP_SECRET ?? "",
365
+ code,
366
+ grantType: "authorization_code",
367
+ }),
368
+ });
369
+ const token = (await tokenRes.json()) as { accessToken?: string };
370
+ if (!token.accessToken) {
371
+ return NextResponse.redirect(new URL("/login?error=dingtalk_token", req.url));
372
+ }`,workweixin:` // Enterprise WeChat returns an access_token by pairing corpId + secret
373
+ const tokenRes = await fetch(
374
+ "${e.tokenEndpoint}?" +
375
+ new URLSearchParams({
376
+ corpid: process.env.WORKWEIXIN_CORP_ID ?? "",
377
+ corpsecret: process.env.WORKWEIXIN_SECRET ?? "",
378
+ }),
379
+ );
380
+ const token = (await tokenRes.json()) as {
381
+ access_token?: string;
382
+ errcode?: number;
383
+ errmsg?: string;
384
+ };
385
+ if (!token.access_token) {
386
+ return NextResponse.redirect(new URL("/login?error=workweixin_token", req.url));
387
+ }`,feishu:` // Step 1: app_access_token
388
+ const appTokenRes = await fetch(
389
+ "https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal",
390
+ {
391
+ method: "POST",
392
+ headers: { "Content-Type": "application/json" },
393
+ body: JSON.stringify({
394
+ app_id: process.env.FEISHU_APP_ID ?? "",
395
+ app_secret: process.env.FEISHU_APP_SECRET ?? "",
396
+ }),
397
+ },
398
+ );
399
+ const appToken = (await appTokenRes.json()) as { app_access_token?: string };
400
+ if (!appToken.app_access_token) {
401
+ return NextResponse.redirect(new URL("/login?error=feishu_app_token", req.url));
402
+ }
403
+ // Step 2: user access_token
404
+ const tokenRes = await fetch("${e.tokenEndpoint}", {
405
+ method: "POST",
406
+ headers: {
407
+ "Content-Type": "application/json",
408
+ Authorization: \`Bearer \${appToken.app_access_token}\`,
409
+ },
410
+ body: JSON.stringify({ grant_type: "authorization_code", code }),
411
+ });
412
+ const token = (await tokenRes.json()) as { data?: { access_token?: string } };
413
+ if (!token.data?.access_token) {
414
+ return NextResponse.redirect(new URL("/login?error=feishu_token", req.url));
415
+ }`,weibo:` const tokenRes = await fetch("${e.tokenEndpoint}", {
416
+ method: "POST",
417
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
418
+ body: new URLSearchParams({
419
+ client_id: process.env.WEIBO_APP_KEY ?? "",
420
+ client_secret: process.env.WEIBO_APP_SECRET ?? "",
421
+ grant_type: "authorization_code",
422
+ code,
423
+ redirect_uri: new URL("${e.callbackUrl}", req.url).toString(),
424
+ }),
425
+ });
426
+ const token = (await tokenRes.json()) as { access_token?: string; uid?: string };
427
+ if (!token.access_token) {
428
+ return NextResponse.redirect(new URL("/login?error=weibo_token", req.url));
429
+ }`};return`${n}
430
+ import { type NextRequest, NextResponse } from "next/server";
431
+
432
+ export async function GET(req: NextRequest) {
433
+ const code = req.nextUrl.searchParams.get("code");
434
+ const state = req.nextUrl.searchParams.get("state");
435
+
436
+ if (!code) {
437
+ return NextResponse.redirect(new URL("/login?error=no_code", req.url));
438
+ }
439
+
440
+ ${r[e.id]}
441
+
442
+ // TODO: fetch user profile from ${e.name} using the access token.
443
+ // TODO: upsert the user into your primary auth provider (${t}).
444
+ // e.g. clerk.users.createUser(...) or betterAuth.signIn.oauth(...).
445
+ // Persist a session cookie before redirecting.
446
+
447
+ void state;
448
+ return NextResponse.redirect(new URL("/dashboard", req.url));
449
+ }
450
+ `}function Zn(e){return`"use client";
451
+
452
+ /**
453
+ * SocialLoginButtons \u2014 generated by create-sailor.
454
+ *
455
+ * Renders a button per selected China-region OAuth provider. Clicking a
456
+ * button navigates to the provider's authorization URL (\`/api/auth/oauth/<id>\`),
457
+ * which the app is expected to implement and redirect to the upstream consent
458
+ * page. The OAuth callback lives at \`/api/auth/callback/<id>\`.
459
+ */
460
+
461
+ import { Button } from "@nebutra/ui/components";
462
+
463
+ interface SocialProvider {
464
+ id: string;
465
+ name: string;
466
+ href: string;
467
+ }
468
+
469
+ const PROVIDERS: SocialProvider[] = [
470
+ ${e.map(n=>` { id: ${JSON.stringify(n.id)}, name: ${JSON.stringify(n.name)}, href: ${JSON.stringify(`/api/auth/oauth/${n.id}`)} }`).join(`,
471
+ `)},
472
+ ];
473
+
474
+ export function SocialLoginButtons() {
475
+ return (
476
+ <div className="space-y-2">
477
+ {PROVIDERS.map((p) => (
478
+ <Button
479
+ key={p.id}
480
+ type="button"
481
+ variant="outline"
482
+ className="w-full"
483
+ onClick={() => {
484
+ window.location.href = p.href;
485
+ }}
486
+ >
487
+ Sign in with {p.name}
488
+ </Button>
489
+ ))}
490
+ </div>
491
+ );
492
+ }
493
+ `}function At(e){te.mkdirSync(e,{recursive:!0})}function er(e,t){let n=ee.join(e,".env.example");te.existsSync(n)?te.appendFileSync(n,`
494
+ `+t):te.writeFileSync(n,t)}function tr(e){let t=`# ${e.name} (${e.id}) \u2014 ${e.docs}`,n=e.envVars.map(r=>`${r}=`).join(`
495
+ `);return`${t}
496
+ ${n}
497
+ `}async function Ct(e,t,n){if(!t||t.length===0)return;let r=ee.join(e,"apps","web");if(!te.existsSync(r))return;let o=new Set,s=[];for(let d of t){if(o.has(d))continue;o.add(d);let m=Pt(d);m&&s.push(m)}if(s.length===0)return;for(let d of s)er(e,tr(d));let a=ee.join(r,"src","app","api","auth","callback");for(let d of s){let m=ee.join(a,d.id);At(m);let S=ee.join(m,"route.ts");te.writeFileSync(S,Qn(d,n))}let c=ee.join(r,"src","components","auth");At(c);let i=ee.join(c,"SocialLoginButtons.tsx");te.writeFileSync(i,Zn(s))}import P from"fs";import v from"path";var nr=v.join("apps","web"),tt=v.join("src","components","compliance"),ke=v.join("src","app","[locale]");function D(e){return v.join(e,nr)}function rr(e){P.mkdirSync(e,{recursive:!0})}function k(e,t){rr(v.dirname(e)),P.writeFileSync(e,t,"utf8")}function nt(e,t){let n=v.join(e,".env.example");if(P.existsSync(n)){if(P.readFileSync(n,"utf8").includes(t.split(`
498
+ `)[0]??""))return;P.appendFileSync(n,`
499
+ `+t+`
500
+ `)}else P.writeFileSync(n,t+`
501
+ `)}var Oe=`> **TEMPLATE \u2014 NOT LEGAL ADVICE.** This document is a boilerplate scaffold only.
502
+ > Consult a qualified lawyer in every jurisdiction you operate in before publishing.
503
+ > \u672C\u6587\u6863\u4E3A\u6A21\u677F\u811A\u624B\u67B6\uFF0C\u4E0D\u6784\u6210\u6CD5\u5F8B\u610F\u89C1\u3002\u53D1\u5E03\u524D\u8BF7\u54A8\u8BE2\u76F8\u5173\u53F8\u6CD5\u8F96\u533A\u7684\u5408\u683C\u5F8B\u5E08\u3002`,or=`import Link from "next/link";
504
+ import { IcpFooter } from "@/components/compliance/IcpFooter";
505
+ import { PublicSecurityFooter } from "@/components/compliance/PublicSecurityFooter";
506
+
507
+ interface FooterProps {
508
+ region?: "global" | "cn" | "hybrid";
509
+ }
510
+
511
+ export function Footer({ region = "global" }: FooterProps) {
512
+ const showCn = region === "cn" || region === "hybrid";
513
+ return (
514
+ <footer className="border-t border-[var(--neutral-7)] bg-[var(--neutral-2)] py-8">
515
+ <div className="mx-auto flex max-w-[1400px] flex-col gap-4 px-4 text-sm text-[var(--neutral-11)] md:flex-row md:items-center md:justify-between md:px-6">
516
+ <div className="flex flex-wrap items-center gap-4">
517
+ <Link href="/privacy" className="hover:underline">
518
+ Privacy Policy / \u9690\u79C1\u653F\u7B56
519
+ </Link>
520
+ <Link href="/terms" className="hover:underline">
521
+ Terms of Service / \u670D\u52A1\u6761\u6B3E
522
+ </Link>
523
+ <button
524
+ type="button"
525
+ onClick={() => window.dispatchEvent(new CustomEvent("cookie-consent-open"))}
526
+ className="hover:underline"
527
+ >
528
+ Cookie Settings
529
+ </button>
530
+ {showCn ? (
531
+ <>
532
+ <Link href="/aigc-disclosure" className="hover:underline">
533
+ AIGC \u8BF4\u660E
534
+ </Link>
535
+ <Link href="/cross-border-data" className="hover:underline">
536
+ \u6570\u636E\u51FA\u5883
537
+ </Link>
538
+ <Link href="/pii-collection" className="hover:underline">
539
+ \u4E2A\u4EBA\u4FE1\u606F\u6E05\u5355
540
+ </Link>
541
+ </>
542
+ ) : null}
543
+ </div>
544
+ {showCn ? (
545
+ <div className="flex flex-wrap items-center gap-4">
546
+ <IcpFooter />
547
+ <PublicSecurityFooter />
548
+ </div>
549
+ ) : null}
550
+ </div>
551
+ </footer>
552
+ );
553
+ }
554
+ `,sr=`export function IcpFooter() {
555
+ const icp = process.env.NEXT_PUBLIC_ICP_BEIAN;
556
+ if (!icp) return null;
557
+ return (
558
+ <a
559
+ href="https://beian.miit.gov.cn/"
560
+ target="_blank"
561
+ rel="noopener noreferrer"
562
+ className="text-xs text-[var(--neutral-11)] hover:underline"
563
+ >
564
+ {icp}
565
+ </a>
566
+ );
567
+ }
568
+ `,ir=`export function PublicSecurityFooter() {
569
+ const psb = process.env.NEXT_PUBLIC_PSB_BEIAN;
570
+ const psbCode = process.env.NEXT_PUBLIC_PSB_CODE;
571
+ if (!psb) return null;
572
+ return (
573
+ <a
574
+ href={\`http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=\${psbCode ?? ""}\`}
575
+ target="_blank"
576
+ rel="noopener noreferrer"
577
+ className="inline-flex items-center gap-1 text-xs text-[var(--neutral-11)] hover:underline"
578
+ >
579
+ {/* Place /public/compliance/psb-icon.png (\u5B98\u65B9\u516C\u5B89\u5907\u6848\u5C0F\u5FBD\u6807) */}
580
+ <img src="/compliance/psb-icon.png" alt="\u516C\u5B89\u5907\u6848" className="h-3 w-3" />
581
+ {psb}
582
+ </a>
583
+ );
584
+ }
585
+ `,ar=`"use client";
586
+
587
+ import { useCallback, useEffect, useState } from "react";
588
+
589
+ type ConsentMode = "accepted" | "rejected" | "custom";
590
+
591
+ interface CustomConsent {
592
+ necessary: true; // always true per PIPL/GDPR
593
+ analytics: boolean;
594
+ marketing: boolean;
595
+ preferences: boolean;
596
+ }
597
+
598
+ interface StoredConsent {
599
+ mode: ConsentMode;
600
+ categories: CustomConsent;
601
+ timestamp: number;
602
+ version: 1;
603
+ }
604
+
605
+ const STORAGE_KEY = "cookie-consent";
606
+ const CONSENT_EVENT = "cookie-consent-changed";
607
+ const OPEN_EVENT = "cookie-consent-open";
608
+
609
+ const DEFAULT_CATEGORIES: CustomConsent = {
610
+ necessary: true,
611
+ analytics: false,
612
+ marketing: false,
613
+ preferences: false,
614
+ };
615
+
616
+ function emitConsent(consent: StoredConsent): void {
617
+ if (typeof window === "undefined") return;
618
+ window.dispatchEvent(new CustomEvent(CONSENT_EVENT, { detail: consent }));
619
+ }
620
+
621
+ export function CookieBanner() {
622
+ const [isOpen, setIsOpen] = useState(false);
623
+ const [showCustomize, setShowCustomize] = useState(false);
624
+ const [categories, setCategories] = useState<CustomConsent>(DEFAULT_CATEGORIES);
625
+
626
+ useEffect(() => {
627
+ try {
628
+ const stored = localStorage.getItem(STORAGE_KEY);
629
+ if (!stored) {
630
+ setIsOpen(true);
631
+ return;
632
+ }
633
+ const parsed = JSON.parse(stored) as StoredConsent;
634
+ // Re-emit so analytics/marketing scripts mounted after consent can read state
635
+ emitConsent(parsed);
636
+ } catch {
637
+ setIsOpen(true);
638
+ }
639
+ }, []);
640
+
641
+ useEffect(() => {
642
+ const handler = () => {
643
+ setShowCustomize(true);
644
+ setIsOpen(true);
645
+ };
646
+ window.addEventListener(OPEN_EVENT, handler);
647
+ return () => window.removeEventListener(OPEN_EVENT, handler);
648
+ }, []);
649
+
650
+ const persist = useCallback((mode: ConsentMode, cats: CustomConsent) => {
651
+ const consent: StoredConsent = {
652
+ mode,
653
+ categories: cats,
654
+ timestamp: Date.now(),
655
+ version: 1,
656
+ };
657
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(consent));
658
+ emitConsent(consent);
659
+ setIsOpen(false);
660
+ setShowCustomize(false);
661
+ }, []);
662
+
663
+ const acceptAll = useCallback(() => {
664
+ persist("accepted", {
665
+ necessary: true,
666
+ analytics: true,
667
+ marketing: true,
668
+ preferences: true,
669
+ });
670
+ }, [persist]);
671
+
672
+ const rejectNonEssential = useCallback(() => {
673
+ persist("rejected", DEFAULT_CATEGORIES);
674
+ }, [persist]);
675
+
676
+ const saveCustom = useCallback(() => {
677
+ persist("custom", categories);
678
+ }, [persist, categories]);
679
+
680
+ if (!isOpen) return null;
681
+
682
+ return (
683
+ <div
684
+ role="dialog"
685
+ aria-modal="false"
686
+ aria-labelledby="cookie-banner-title"
687
+ className="fixed inset-x-0 bottom-0 z-50 border-t border-[var(--neutral-7)] bg-[var(--neutral-1)] shadow-lg"
688
+ >
689
+ <div className="mx-auto flex max-w-[1400px] flex-col gap-4 p-4 md:p-6">
690
+ <div>
691
+ <h2 id="cookie-banner-title" className="text-sm font-semibold text-[var(--neutral-12)]">
692
+ Cookie Preferences \xB7 Cookie \u504F\u597D\u8BBE\u7F6E
693
+ </h2>
694
+ <p className="mt-1 text-xs text-[var(--neutral-11)]">
695
+ We use cookies to provide essential functionality, analyze usage, and personalize content.
696
+ You can accept all, reject non-essential, or customize your choice. See our{" "}
697
+ <a href="/privacy" className="underline">
698
+ Privacy Policy
699
+ </a>
700
+ .
701
+ <br />
702
+ \u6211\u4EEC\u4F7F\u7528 Cookie \u63D0\u4F9B\u5FC5\u8981\u529F\u80FD\u3001\u5206\u6790\u4F7F\u7528\u60C5\u51B5\u5E76\u4E2A\u6027\u5316\u5185\u5BB9\u3002\u60A8\u53EF\u4EE5\u63A5\u53D7\u5168\u90E8\u3001\u62D2\u7EDD\u975E\u5FC5\u8981\uFF0C\u6216\u81EA\u5B9A\u4E49\u9009\u62E9\u3002\u8BE6\u89C1
703
+ <a href="/privacy" className="underline">
704
+ \u9690\u79C1\u653F\u7B56
705
+ </a>
706
+ \u3002
707
+ </p>
708
+ </div>
709
+
710
+ {showCustomize ? (
711
+ <fieldset className="grid gap-3 md:grid-cols-2">
712
+ <CategoryRow
713
+ id="necessary"
714
+ title="Necessary / \u5FC5\u8981"
715
+ description="Required for the site to function. Cannot be disabled. / \u7AD9\u70B9\u8FD0\u884C\u6240\u5FC5\u9700\uFF0C\u65E0\u6CD5\u5173\u95ED\u3002"
716
+ checked={true}
717
+ disabled
718
+ onChange={() => undefined}
719
+ />
720
+ <CategoryRow
721
+ id="analytics"
722
+ title="Analytics / \u5206\u6790"
723
+ description="Helps us understand usage patterns. / \u5E2E\u52A9\u6211\u4EEC\u4E86\u89E3\u4F7F\u7528\u6A21\u5F0F\u3002"
724
+ checked={categories.analytics}
725
+ onChange={(v) => setCategories((c) => ({ ...c, analytics: v }))}
726
+ />
727
+ <CategoryRow
728
+ id="marketing"
729
+ title="Marketing / \u8425\u9500"
730
+ description="Used for targeted advertising. / \u7528\u4E8E\u5B9A\u5411\u5E7F\u544A\u3002"
731
+ checked={categories.marketing}
732
+ onChange={(v) => setCategories((c) => ({ ...c, marketing: v }))}
733
+ />
734
+ <CategoryRow
735
+ id="preferences"
736
+ title="Preferences / \u504F\u597D"
737
+ description="Remember your preferences (language, theme). / \u8BB0\u4F4F\u60A8\u7684\u504F\u597D\uFF08\u8BED\u8A00\u3001\u4E3B\u9898\uFF09\u3002"
738
+ checked={categories.preferences}
739
+ onChange={(v) => setCategories((c) => ({ ...c, preferences: v }))}
740
+ />
741
+ </fieldset>
742
+ ) : null}
743
+
744
+ <div className="flex flex-wrap gap-2 md:justify-end">
745
+ {showCustomize ? (
746
+ <button
747
+ type="button"
748
+ onClick={saveCustom}
749
+ className="rounded-md bg-[var(--blue-9)] px-4 py-2 text-sm font-medium text-white hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-[var(--blue-9)] focus:ring-offset-1"
750
+ >
751
+ Save Preferences / \u4FDD\u5B58\u504F\u597D
752
+ </button>
753
+ ) : (
754
+ <button
755
+ type="button"
756
+ onClick={() => setShowCustomize(true)}
757
+ className="rounded-md border border-[var(--neutral-7)] px-4 py-2 text-sm font-medium text-[var(--neutral-12)] hover:bg-[var(--neutral-3)] focus:outline-none focus:ring-2 focus:ring-[var(--blue-9)] focus:ring-offset-1"
758
+ >
759
+ Customize / \u81EA\u5B9A\u4E49
760
+ </button>
761
+ )}
762
+ <button
763
+ type="button"
764
+ onClick={rejectNonEssential}
765
+ className="rounded-md border border-[var(--neutral-7)] px-4 py-2 text-sm font-medium text-[var(--neutral-12)] hover:bg-[var(--neutral-3)] focus:outline-none focus:ring-2 focus:ring-[var(--blue-9)] focus:ring-offset-1"
766
+ >
767
+ Reject Non-Essential / \u62D2\u7EDD\u975E\u5FC5\u8981
768
+ </button>
769
+ <button
770
+ type="button"
771
+ onClick={acceptAll}
772
+ className="rounded-md bg-[var(--blue-9)] px-4 py-2 text-sm font-medium text-white hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-[var(--blue-9)] focus:ring-offset-1"
773
+ >
774
+ Accept All / \u5168\u90E8\u63A5\u53D7
775
+ </button>
776
+ </div>
777
+ </div>
778
+ </div>
779
+ );
780
+ }
781
+
782
+ interface CategoryRowProps {
783
+ id: string;
784
+ title: string;
785
+ description: string;
786
+ checked: boolean;
787
+ disabled?: boolean;
788
+ onChange: (value: boolean) => void;
789
+ }
790
+
791
+ function CategoryRow({ id, title, description, checked, disabled, onChange }: CategoryRowProps) {
792
+ return (
793
+ <label
794
+ htmlFor={id}
795
+ className="flex items-start gap-3 rounded-md border border-[var(--neutral-7)] p-3"
796
+ >
797
+ <input
798
+ id={id}
799
+ type="checkbox"
800
+ checked={checked}
801
+ disabled={disabled}
802
+ onChange={(e) => onChange(e.target.checked)}
803
+ className="mt-0.5 h-4 w-4"
804
+ />
805
+ <span className="flex flex-col">
806
+ <span className="text-sm font-medium text-[var(--neutral-12)]">{title}</span>
807
+ <span className="text-xs text-[var(--neutral-11)]">{description}</span>
808
+ </span>
809
+ </label>
810
+ );
811
+ }
812
+ `,cr=`interface AigcDisclaimerProps {
813
+ className?: string;
814
+ }
815
+
816
+ /**
817
+ * Inline disclaimer for AI-generated content (required by \u7F51\u4FE1\u529E\u300A\u751F\u6210\u5F0F\u4EBA\u5DE5\u667A\u80FD
818
+ * \u670D\u52A1\u7BA1\u7406\u6682\u884C\u529E\u6CD5\u300B2023/2024). Attach to any block/page that contains AI output.
819
+ */
820
+ export function AigcDisclaimer({ className }: AigcDisclaimerProps) {
821
+ return (
822
+ <div className={className}>
823
+ <p className="text-xs text-[var(--neutral-11)]">
824
+ \u672C\u5185\u5BB9\u7531 AI \u751F\u6210 \xB7 AI-Generated Content \xB7{" "}
825
+ <a href="/aigc-disclosure" className="underline">
826
+ \u4E86\u89E3\u66F4\u591A / Learn more
827
+ </a>
828
+ </p>
829
+ </div>
830
+ );
831
+ }
832
+ `,pr=`---
833
+ title: Privacy Policy / \u9690\u79C1\u653F\u7B56
834
+ description: How {{COMPANY_NAME}} collects, uses, shares, and protects personal information.
835
+ lastUpdated: 2026-01-01
836
+ ---
837
+
838
+ ${Oe}
839
+
840
+ # Privacy Policy / \u9690\u79C1\u653F\u7B56
841
+
842
+ > **Effective Date:** 2026-01-01 \xB7 **Last Updated:** 2026-01-01
843
+ > **\u751F\u6548\u65E5\u671F\uFF1A** 2026-01-01 \xB7 **\u6700\u540E\u66F4\u65B0\uFF1A** 2026-01-01
844
+
845
+ This Privacy Policy describes how **{{COMPANY_NAME}}** ("we", "us", "our") collects, uses, shares,
846
+ and protects your personal information in connection with our services.
847
+
848
+ \u672C\u9690\u79C1\u653F\u7B56\u8BF4\u660E **{{COMPANY_NAME}}**\uFF08"\u6211\u4EEC"\uFF09\u5728\u63D0\u4F9B\u670D\u52A1\u8FC7\u7A0B\u4E2D\u5982\u4F55\u6536\u96C6\u3001\u4F7F\u7528\u3001\u5171\u4EAB\u548C\u4FDD\u62A4\u60A8\u7684\u4E2A\u4EBA\u4FE1\u606F\u3002
849
+
850
+ ---
851
+
852
+ ## 1. Data Collection Scope / \u6570\u636E\u6536\u96C6\u8303\u56F4
853
+
854
+ **EN:** We collect the following categories of personal information:
855
+ - **Account data:** name, email address, phone number, password (hashed).
856
+ - **Usage data:** IP address, browser, device, operating system, page views, clickstream, timestamps.
857
+ - **Billing data:** payment method tokens (handled by our payment processor), billing address.
858
+ - **Content data:** files, messages, and other content you upload or create.
859
+ - **Cookies & similar technologies:** see our Cookie Preferences banner for details.
860
+
861
+ **ZH:** \u6211\u4EEC\u6536\u96C6\u4EE5\u4E0B\u7C7B\u522B\u7684\u4E2A\u4EBA\u4FE1\u606F\uFF1A
862
+ - **\u8D26\u6237\u4FE1\u606F\uFF1A** \u59D3\u540D\u3001\u90AE\u7BB1\u3001\u624B\u673A\u53F7\u3001\u5BC6\u7801\uFF08\u52A0\u5BC6\u5B58\u50A8\uFF09\u3002
863
+ - **\u4F7F\u7528\u4FE1\u606F\uFF1A** IP \u5730\u5740\u3001\u6D4F\u89C8\u5668\u3001\u8BBE\u5907\u3001\u64CD\u4F5C\u7CFB\u7EDF\u3001\u9875\u9762\u6D4F\u89C8\u3001\u70B9\u51FB\u6D41\u3001\u65F6\u95F4\u6233\u3002
864
+ - **\u652F\u4ED8\u4FE1\u606F\uFF1A** \u652F\u4ED8\u4EE4\u724C\uFF08\u7531\u7B2C\u4E09\u65B9\u652F\u4ED8\u670D\u52A1\u5546\u5904\u7406\uFF09\u3001\u8D26\u5355\u5730\u5740\u3002
865
+ - **\u5185\u5BB9\u4FE1\u606F\uFF1A** \u60A8\u4E0A\u4F20\u6216\u521B\u5EFA\u7684\u6587\u4EF6\u3001\u6D88\u606F\u7B49\u5185\u5BB9\u3002
866
+ - **Cookie \u53CA\u7C7B\u4F3C\u6280\u672F\uFF1A** \u8BE6\u89C1 Cookie \u504F\u597D\u8BBE\u7F6E\u3002
867
+
868
+ ---
869
+
870
+ ## 2. Purpose / \u4F7F\u7528\u76EE\u7684
871
+
872
+ **EN:** We use personal information to (a) provide and maintain the service; (b) authenticate users;
873
+ (c) process payments; (d) detect and prevent fraud/abuse; (e) improve product quality; (f) comply with
874
+ legal obligations; (g) communicate about the service (with consent where required).
875
+
876
+ **ZH:** \u6211\u4EEC\u5C06\u4E2A\u4EBA\u4FE1\u606F\u7528\u4E8E\uFF1A(a) \u63D0\u4F9B\u548C\u7EF4\u62A4\u670D\u52A1\uFF1B(b) \u7528\u6237\u8EAB\u4EFD\u9A8C\u8BC1\uFF1B(c) \u5904\u7406\u652F\u4ED8\uFF1B(d) \u68C0\u6D4B\u548C\u9632\u8303
877
+ \u6B3A\u8BC8/\u6EE5\u7528\uFF1B(e) \u6539\u8FDB\u4EA7\u54C1\u8D28\u91CF\uFF1B(f) \u5C65\u884C\u6CD5\u5F8B\u4E49\u52A1\uFF1B(g) \u5C31\u670D\u52A1\u4E0E\u60A8\u6C9F\u901A\uFF08\u9700\u8981\u65F6\u53D6\u5F97\u60A8\u7684\u540C\u610F\uFF09\u3002
878
+
879
+ ---
880
+
881
+ ## 3. Sharing & Cross-Border Transfer / \u6570\u636E\u5171\u4EAB\u53CA\u8DE8\u5883\u4F20\u8F93
882
+
883
+ **EN:** We share personal information only with:
884
+ - **Processors** (cloud hosting, analytics, email, payment) bound by data processing agreements.
885
+ - **Legal authorities** when required by law.
886
+ - **Business transferees** in the event of merger, acquisition, or asset sale.
887
+
888
+ Some processors operate **outside your country**. For users in China, see our separate
889
+ [Cross-Border Data Notice](/cross-border-data). For users in the EEA/UK, transfers rely on
890
+ Standard Contractual Clauses or equivalent safeguards.
891
+
892
+ **ZH:** \u6211\u4EEC\u4EC5\u4E0E\u4EE5\u4E0B\u4E3B\u4F53\u5171\u4EAB\u4E2A\u4EBA\u4FE1\u606F\uFF1A
893
+ - **\u5904\u7406\u8005**\uFF08\u4E91\u670D\u52A1\u3001\u5206\u6790\u3001\u90AE\u4EF6\u3001\u652F\u4ED8\uFF09\u2014\u2014 \u53D7\u6570\u636E\u5904\u7406\u534F\u8BAE\u7EA6\u675F\u3002
894
+ - **\u53F8\u6CD5/\u76D1\u7BA1\u673A\u5173** \u2014\u2014 \u4F9D\u6CD5\u5F8B\u8981\u6C42\u3002
895
+ - **\u4E1A\u52A1\u7EE7\u53D7\u8005** \u2014\u2014 \u53D1\u751F\u5408\u5E76\u3001\u6536\u8D2D\u6216\u8D44\u4EA7\u51FA\u552E\u65F6\u3002
896
+
897
+ \u90E8\u5206\u5904\u7406\u8005\u4F4D\u4E8E**\u5883\u5916**\u3002\u4E2D\u56FD\u5883\u5185\u7528\u6237\u8BF7\u53C2\u9605\u300A[\u6570\u636E\u51FA\u5883\u544A\u77E5](/cross-border-data)\u300B\u3002
898
+ \u6B27\u76DF/\u82F1\u56FD\u7528\u6237\u7684\u8DE8\u5883\u4F20\u8F93\u4F9D\u636E\u300A\u6807\u51C6\u5408\u540C\u6761\u6B3E\u300B(SCCs) \u6216\u7B49\u6548\u4FDD\u62A4\u63AA\u65BD\u3002
899
+
900
+ ---
901
+
902
+ ## 4. Your Rights / \u7528\u6237\u6743\u5229
903
+
904
+ **EN:** Depending on your jurisdiction (GDPR/CCPA/PIPL/\u7B49), you may have the right to:
905
+ - **Access** your personal information.
906
+ - **Rectify** inaccurate data.
907
+ - **Delete** your data ("right to be forgotten").
908
+ - **Export** your data in a portable format.
909
+ - **Restrict or object** to certain processing.
910
+ - **Withdraw consent** at any time without affecting the lawfulness of prior processing.
911
+ - **Lodge a complaint** with your local supervisory authority.
912
+
913
+ To exercise these rights, contact **{{COMPANY_CONTACT_EMAIL}}**.
914
+
915
+ **ZH:** \u6839\u636E\u60A8\u6240\u5728\u53F8\u6CD5\u8F96\u533A\uFF08\u300A\u4E2A\u4EBA\u4FE1\u606F\u4FDD\u62A4\u6CD5\u300B/GDPR/CCPA \u7B49\uFF09\uFF0C\u60A8\u53EF\u80FD\u4EAB\u6709\u4EE5\u4E0B\u6743\u5229\uFF1A
916
+ - **\u8BBF\u95EE**\u60A8\u7684\u4E2A\u4EBA\u4FE1\u606F\u3002
917
+ - **\u66F4\u6B63**\u4E0D\u51C6\u786E\u4FE1\u606F\u3002
918
+ - **\u5220\u9664**\u60A8\u7684\u6570\u636E\uFF08\u88AB\u9057\u5FD8\u6743\uFF09\u3002
919
+ - **\u5BFC\u51FA**\u60A8\u7684\u6570\u636E\uFF08\u53EF\u643A\u5E26\u683C\u5F0F\uFF09\u3002
920
+ - **\u9650\u5236\u6216\u53CD\u5BF9**\u7279\u5B9A\u5904\u7406\u3002
921
+ - **\u968F\u65F6\u64A4\u56DE\u540C\u610F**\uFF08\u4E0D\u5F71\u54CD\u64A4\u56DE\u524D\u7684\u5904\u7406\u5408\u6CD5\u6027\uFF09\u3002
922
+ - \u5411\u5F53\u5730\u76D1\u7BA1\u673A\u6784**\u6295\u8BC9**\u3002
923
+
924
+ \u5982\u9700\u884C\u4F7F\u4E0A\u8FF0\u6743\u5229\uFF0C\u8BF7\u8054\u7CFB **{{COMPANY_CONTACT_EMAIL}}**\u3002
925
+
926
+ ---
927
+
928
+ ## 5. Cookies / Cookie \u4F7F\u7528
929
+
930
+ See our Cookie Preferences banner (footer \u2192 Cookie Settings). We categorize cookies as:
931
+ Necessary (cannot be disabled), Analytics, Marketing, Preferences.
932
+
933
+ \u8BF7\u53C2\u9605 Cookie \u504F\u597D\u8BBE\u7F6E\uFF08\u9875\u811A \u2192 Cookie Settings\uFF09\u3002Cookie \u7C7B\u522B\u5305\u62EC\uFF1A\u5FC5\u8981\uFF08\u4E0D\u53EF\u5173\u95ED\uFF09\u3001
934
+ \u5206\u6790\u3001\u8425\u9500\u3001\u504F\u597D\u3002
935
+
936
+ ---
937
+
938
+ ## 6. Data Security / \u6570\u636E\u5B89\u5168\u63AA\u65BD
939
+
940
+ **EN:** Administrative, technical, and physical safeguards include TLS 1.2+ in transit, AES-256 at rest
941
+ (envelope encryption for secrets via \`@nebutra/vault\`), role-based access control, audit logging,
942
+ periodic vulnerability scans, and least-privilege principles. No system is 100% secure; we notify
943
+ affected users and regulators of material breaches within statutory timelines.
944
+
945
+ **ZH:** \u6211\u4EEC\u91C7\u53D6\u7BA1\u7406\u3001\u6280\u672F\u548C\u7269\u7406\u5C42\u9762\u7684\u4FDD\u969C\u63AA\u65BD\uFF0C\u5305\u62EC\u4F20\u8F93\u4E2D TLS 1.2+\u3001\u9759\u6001 AES-256 \u52A0\u5BC6
946
+ \uFF08\u5BC6\u94A5\u901A\u8FC7 \`@nebutra/vault\` \u4FE1\u5C01\u52A0\u5BC6\u7BA1\u7406\uFF09\u3001\u57FA\u4E8E\u89D2\u8272\u7684\u8BBF\u95EE\u63A7\u5236\u3001\u5BA1\u8BA1\u65E5\u5FD7\u3001\u5B9A\u671F\u6F0F\u6D1E\u626B\u63CF
947
+ \u548C\u6700\u5C0F\u6743\u9650\u539F\u5219\u3002\u4EFB\u4F55\u7CFB\u7EDF\u90FD\u65E0\u6CD5\u505A\u5230\u7EDD\u5BF9\u5B89\u5168\uFF1B\u53D1\u751F\u91CD\u5927\u6570\u636E\u6CC4\u9732\u65F6\uFF0C\u6211\u4EEC\u5C06\u5728\u6CD5\u5B9A\u671F\u9650\u5185
948
+ \u901A\u77E5\u53D7\u5F71\u54CD\u7528\u6237\u548C\u76D1\u7BA1\u673A\u6784\u3002
949
+
950
+ ---
951
+
952
+ ## 7. Retention / \u6570\u636E\u4FDD\u7559
953
+
954
+ We retain personal information only as long as necessary to fulfill the purposes described, or as
955
+ required by law. Inactive accounts are deleted after 24 months unless retention is legally required.
956
+
957
+ \u6211\u4EEC\u4EC5\u5728\u5B9E\u73B0\u4E0A\u8FF0\u76EE\u7684\u6240\u9700\u7684\u671F\u95F4\u6216\u6CD5\u5F8B\u8981\u6C42\u7684\u671F\u95F4\u5185\u4FDD\u7559\u4E2A\u4EBA\u4FE1\u606F\u3002\u975E\u6D3B\u8DC3\u8D26\u6237\u5C06\u5728 24 \u4E2A\u6708\u540E
958
+ \u5220\u9664\uFF0C\u6CD5\u5F8B\u53E6\u6709\u8981\u6C42\u7684\u9664\u5916\u3002
959
+
960
+ ---
961
+
962
+ ## 8. Changes / \u53D8\u66F4\u901A\u77E5
963
+
964
+ We will notify you of material changes by posting the updated policy and, where required, by email
965
+ or in-app notification before the change takes effect.
966
+
967
+ \u6211\u4EEC\u5C06\u901A\u8FC7\u53D1\u5E03\u66F4\u65B0\u540E\u7684\u653F\u7B56\u901A\u77E5\u60A8\u91CD\u5927\u53D8\u66F4\uFF1B\u5982\u6CD5\u5F8B\u8981\u6C42\uFF0C\u8FD8\u5C06\u5728\u53D8\u66F4\u751F\u6548\u524D\u901A\u8FC7\u90AE\u4EF6\u6216\u5E94\u7528\u5185
968
+ \u901A\u77E5\u7684\u65B9\u5F0F\u544A\u77E5\u3002
969
+
970
+ ---
971
+
972
+ ## 9. Contact / \u8054\u7CFB\u65B9\u5F0F
973
+
974
+ **Data Controller / \u6570\u636E\u63A7\u5236\u8005:** {{COMPANY_NAME}}
975
+ **Address / \u5730\u5740:** {{COMPANY_ADDRESS}}
976
+ **Email / \u90AE\u7BB1:** {{COMPANY_CONTACT_EMAIL}}
977
+ `,lr=`---
978
+ title: Terms of Service / \u670D\u52A1\u6761\u6B3E
979
+ description: Terms governing your use of {{COMPANY_NAME}}.
980
+ lastUpdated: 2026-01-01
981
+ ---
982
+
983
+ ${Oe}
984
+
985
+ # Terms of Service / \u670D\u52A1\u6761\u6B3E
986
+
987
+ > **Effective Date:** 2026-01-01
988
+ > **\u751F\u6548\u65E5\u671F\uFF1A** 2026-01-01
989
+
990
+ ## 1. Acceptance / \u63A5\u53D7\u6761\u6B3E
991
+
992
+ **EN:** By accessing or using the services provided by {{COMPANY_NAME}} ("Service"), you agree to be
993
+ bound by these Terms. If you do not agree, do not use the Service.
994
+
995
+ **ZH:** \u60A8\u8BBF\u95EE\u6216\u4F7F\u7528 {{COMPANY_NAME}} \u63D0\u4F9B\u7684\u670D\u52A1\uFF08"\u670D\u52A1"\uFF09\u5373\u8868\u793A\u540C\u610F\u53D7\u672C\u6761\u6B3E\u7EA6\u675F\u3002
996
+ \u5982\u4E0D\u540C\u610F\uFF0C\u8BF7\u52FF\u4F7F\u7528\u672C\u670D\u52A1\u3002
997
+
998
+ ---
999
+
1000
+ ## 2. Eligibility / \u7528\u6237\u8D44\u683C
1001
+
1002
+ You must be at least 13 years old (or the minimum age in your jurisdiction, whichever is higher) and
1003
+ have the legal capacity to enter into a contract.
1004
+
1005
+ \u7528\u6237\u987B\u5E74\u6EE1 13 \u5468\u5C81\uFF08\u6216\u5176\u6240\u5728\u53F8\u6CD5\u8F96\u533A\u89C4\u5B9A\u7684\u6700\u4F4E\u5E74\u9F84\uFF0C\u53D6\u8F83\u9AD8\u8005\uFF09\u5E76\u5177\u5907\u7B7E\u8BA2\u5408\u540C\u7684\u6CD5\u5F8B\u884C\u4E3A\u80FD\u529B\u3002
1006
+
1007
+ ---
1008
+
1009
+ ## 3. Accounts / \u8D26\u6237
1010
+
1011
+ You are responsible for maintaining the confidentiality of your credentials and for all activity
1012
+ under your account. Notify us immediately of any unauthorized use at {{COMPANY_CONTACT_EMAIL}}.
1013
+
1014
+ \u60A8\u8D1F\u8D23\u4FDD\u7BA1\u8D26\u6237\u51ED\u636E\u7684\u673A\u5BC6\u6027\uFF0C\u5E76\u5BF9\u8D26\u6237\u4E0B\u7684\u4E00\u5207\u6D3B\u52A8\u8D1F\u8D23\u3002\u53D1\u73B0\u672A\u7ECF\u6388\u6743\u7684\u4F7F\u7528\uFF0C\u8BF7\u7ACB\u5373\u901A\u8FC7
1015
+ {{COMPANY_CONTACT_EMAIL}} \u901A\u77E5\u6211\u4EEC\u3002
1016
+
1017
+ ---
1018
+
1019
+ ## 4. Acceptable Use / \u5408\u89C4\u4F7F\u7528
1020
+
1021
+ You agree not to: (a) violate any law; (b) infringe intellectual property rights; (c) transmit
1022
+ malware; (d) reverse engineer the Service except as permitted by law; (e) use the Service for
1023
+ activities prohibited in your jurisdiction (including content review regulations in the PRC).
1024
+
1025
+ \u60A8\u540C\u610F\u4E0D\u5F97\uFF1A(a) \u8FDD\u53CD\u4EFB\u4F55\u6CD5\u5F8B\uFF1B(b) \u4FB5\u72AF\u77E5\u8BC6\u4EA7\u6743\uFF1B(c) \u4F20\u64AD\u6076\u610F\u8F6F\u4EF6\uFF1B(d) \u8D85\u51FA\u6CD5\u5F8B\u5141\u8BB8\u8303\u56F4\u5BF9
1026
+ \u670D\u52A1\u8FDB\u884C\u9006\u5411\u5DE5\u7A0B\uFF1B(e) \u5C06\u670D\u52A1\u7528\u4E8E\u60A8\u6240\u5728\u53F8\u6CD5\u8F96\u533A\uFF08\u5305\u62EC\u4E2D\u56FD\u5883\u5185\u5185\u5BB9\u5BA1\u6838\u6CD5\u89C4\uFF09\u7981\u6B62\u7684\u6D3B\u52A8\u3002
1027
+
1028
+ ---
1029
+
1030
+ ## 5. Subscription & Billing / \u8BA2\u9605\u4E0E\u8BA1\u8D39
1031
+
1032
+ Paid plans are billed in advance on a recurring basis. You authorize us to charge your payment
1033
+ method until you cancel. Refunds are handled in accordance with applicable law and our refund policy.
1034
+
1035
+ \u4ED8\u8D39\u8BA2\u9605\u6309\u5468\u671F\u9884\u5148\u8BA1\u8D39\u3002\u60A8\u6388\u6743\u6211\u4EEC\u5411\u60A8\u7684\u652F\u4ED8\u65B9\u5F0F\u6301\u7EED\u6263\u6B3E\uFF0C\u76F4\u81F3\u60A8\u53D6\u6D88\u8BA2\u9605\u3002\u9000\u6B3E\u4F9D\u9002\u7528\u6CD5\u5F8B
1036
+ \u53CA\u672C\u670D\u52A1\u9000\u6B3E\u653F\u7B56\u5904\u7406\u3002
1037
+
1038
+ ---
1039
+
1040
+ ## 6. Intellectual Property / \u77E5\u8BC6\u4EA7\u6743
1041
+
1042
+ **EN:** The Service, including all software, design, and content provided by us, is owned by
1043
+ {{COMPANY_NAME}} and protected by copyright, trademark, and other laws. You retain ownership of
1044
+ content you upload; you grant us a worldwide, non-exclusive, royalty-free license to host, process,
1045
+ and display your content solely to provide the Service.
1046
+
1047
+ **ZH:** \u670D\u52A1\uFF08\u5305\u62EC\u6211\u4EEC\u63D0\u4F9B\u7684\u6240\u6709\u8F6F\u4EF6\u3001\u8BBE\u8BA1\u548C\u5185\u5BB9\uFF09\u5F52 {{COMPANY_NAME}} \u6240\u6709\uFF0C\u53D7\u8457\u4F5C\u6743\u3001
1048
+ \u5546\u6807\u53CA\u5176\u4ED6\u6CD5\u5F8B\u4FDD\u62A4\u3002\u60A8\u4FDD\u7559\u5BF9\u60A8\u4E0A\u4F20\u5185\u5BB9\u7684\u6240\u6709\u6743\uFF1B\u60A8\u6388\u4E88\u6211\u4EEC\u5168\u7403\u8303\u56F4\u5185\u3001\u975E\u72EC\u5360\u3001\u514D\u7248\u7A0E\u7684
1049
+ \u8BB8\u53EF\uFF0C\u4EC5\u7528\u4E8E\u6258\u7BA1\u3001\u5904\u7406\u548C\u5C55\u793A\u60A8\u7684\u5185\u5BB9\u4EE5\u63D0\u4F9B\u670D\u52A1\u3002
1050
+
1051
+ ---
1052
+
1053
+ ## 7. AI-Generated Content / \u751F\u6210\u5F0F AI \u5185\u5BB9
1054
+
1055
+ Outputs generated by AI models are provided "as-is". You are responsible for verifying accuracy
1056
+ and ensuring compliance with applicable laws (including Chinese AIGC regulations) before relying
1057
+ on or publishing such outputs. See the [AIGC Disclosure](/aigc-disclosure) for details.
1058
+
1059
+ AI \u6A21\u578B\u751F\u6210\u7684\u8F93\u51FA\u6309"\u73B0\u72B6"\u63D0\u4F9B\u3002\u60A8\u5E94\u8D1F\u8D23\u6838\u5B9E\u5176\u51C6\u786E\u6027\uFF0C\u5E76\u5728\u4F9D\u8D56\u6216\u53D1\u5E03\u524D\u786E\u4FDD\u7B26\u5408\u9002\u7528\u6CD5\u5F8B
1060
+ \uFF08\u5305\u62EC\u4E2D\u56FD AIGC \u65B0\u89C4\uFF09\u3002\u8BE6\u89C1\u300A[AIGC \u8BF4\u660E](/aigc-disclosure)\u300B\u3002
1061
+
1062
+ ---
1063
+
1064
+ ## 8. Disclaimer & Limitation of Liability / \u514D\u8D23\u58F0\u660E\u53CA\u8D23\u4EFB\u9650\u5236
1065
+
1066
+ **EN:** TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE SERVICE IS PROVIDED "AS IS" WITHOUT WARRANTIES.
1067
+ IN NO EVENT SHALL {{COMPANY_NAME}} BE LIABLE FOR INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES.
1068
+
1069
+ **ZH:** \u5728\u6CD5\u5F8B\u5141\u8BB8\u7684\u6700\u5927\u8303\u56F4\u5185\uFF0C\u670D\u52A1\u6309"\u73B0\u72B6"\u63D0\u4F9B\uFF0C\u4E0D\u4F5C\u4EFB\u4F55\u660E\u793A\u6216\u9ED8\u793A\u4FDD\u8BC1\u3002{{COMPANY_NAME}}
1070
+ \u6982\u4E0D\u5BF9\u95F4\u63A5\u3001\u5076\u7136\u6216\u540E\u679C\u6027\u635F\u5931\u627F\u62C5\u8D23\u4EFB\u3002
1071
+
1072
+ ---
1073
+
1074
+ ## 9. Termination / \u7EC8\u6B62
1075
+
1076
+ We may suspend or terminate your account for material breach. You may cancel at any time from
1077
+ your account settings.
1078
+
1079
+ \u56E0\u91CD\u5927\u8FDD\u7EA6\uFF0C\u6211\u4EEC\u53EF\u6682\u505C\u6216\u7EC8\u6B62\u60A8\u7684\u8D26\u6237\u3002\u60A8\u53EF\u968F\u65F6\u5728\u8D26\u6237\u8BBE\u7F6E\u4E2D\u53D6\u6D88\u8BA2\u9605\u3002
1080
+
1081
+ ---
1082
+
1083
+ ## 10. Governing Law / \u9002\u7528\u6CD5\u5F8B
1084
+
1085
+ These Terms are governed by the laws of **{{GOVERNING_JURISDICTION}}**, without regard to
1086
+ conflict-of-laws principles. Disputes shall be resolved in the competent courts of that jurisdiction.
1087
+
1088
+ \u672C\u6761\u6B3E\u53D7 **{{GOVERNING_JURISDICTION}}** \u6CD5\u5F8B\u7BA1\u8F96\uFF0C\u4E0D\u8003\u8651\u51B2\u7A81\u6CD5\u539F\u5219\u3002\u4E89\u8BAE\u5E94\u63D0\u4EA4\u81F3\u8BE5
1089
+ \u53F8\u6CD5\u8F96\u533A\u7684\u6709\u7BA1\u8F96\u6743\u6CD5\u9662\u89E3\u51B3\u3002
1090
+
1091
+ ---
1092
+
1093
+ ## 11. Contact / \u8054\u7CFB\u65B9\u5F0F
1094
+
1095
+ {{COMPANY_NAME}} \xB7 {{COMPANY_ADDRESS}} \xB7 {{COMPANY_CONTACT_EMAIL}}
1096
+ `,dr=`---
1097
+ title: AIGC Disclosure / \u751F\u6210\u5F0F AI \u670D\u52A1\u8BF4\u660E
1098
+ description: Disclosure required by \u7F51\u4FE1\u529E\u300A\u751F\u6210\u5F0F\u4EBA\u5DE5\u667A\u80FD\u670D\u52A1\u7BA1\u7406\u6682\u884C\u529E\u6CD5\u300B
1099
+ lastUpdated: 2026-01-01
1100
+ ---
1101
+
1102
+ ${Oe}
1103
+
1104
+ # AIGC Disclosure / \u751F\u6210\u5F0F AI \u670D\u52A1\u8BF4\u660E
1105
+
1106
+ This disclosure is provided in accordance with the **Interim Measures for the Management of
1107
+ Generative AI Services** (\u751F\u6210\u5F0F\u4EBA\u5DE5\u667A\u80FD\u670D\u52A1\u7BA1\u7406\u6682\u884C\u529E\u6CD5) promulgated by the Cyberspace
1108
+ Administration of China.
1109
+
1110
+ \u672C\u8BF4\u660E\u4F9D\u636E\u56FD\u5BB6\u4E92\u8054\u7F51\u4FE1\u606F\u529E\u516C\u5BA4\u300A\u751F\u6210\u5F0F\u4EBA\u5DE5\u667A\u80FD\u670D\u52A1\u7BA1\u7406\u6682\u884C\u529E\u6CD5\u300B\u7684\u8981\u6C42\u63D0\u4F9B\u3002
1111
+
1112
+ ---
1113
+
1114
+ ## 1. AI Models Used / \u4F7F\u7528\u7684 AI \u6A21\u578B
1115
+
1116
+ > TODO (fill in before launch / \u4E0A\u7EBF\u524D\u586B\u5199):
1117
+
1118
+ | Model / \u6A21\u578B | Provider / \u63D0\u4F9B\u65B9 | Use Case / \u7528\u9014 |
1119
+ | --- | --- | --- |
1120
+ | _e.g. GPT-4o_ | _OpenAI_ | _Chat, content drafting_ |
1121
+ | _e.g. Claude 3.5 Sonnet_ | _Anthropic_ | _Reasoning, code generation_ |
1122
+ | _e.g. \u901A\u4E49\u5343\u95EE_ | _\u963F\u91CC\u4E91_ | _\u4E2D\u6587\u5185\u5BB9\u751F\u6210_ |
1123
+
1124
+ ---
1125
+
1126
+ ## 2. Algorithm Filing / \u7B97\u6CD5\u5907\u6848\u53F7
1127
+
1128
+ **Filing Number / \u5907\u6848\u53F7:** \`{{AIGC_ALGORITHM_FILING_NUMBER}}\`
1129
+ (Apply at <https://beian.cac.gov.cn/> \xB7 \u4E8E\u4E2D\u56FD\u7F51\u4FE1\u529E\u5B8C\u6210\u7B97\u6CD5\u5907\u6848)
1130
+
1131
+ ---
1132
+
1133
+ ## 3. AI-Generated Content Labeling / \u751F\u6210\u5185\u5BB9\u6807\u8BC6\u673A\u5236
1134
+
1135
+ **EN:** All content generated by AI models in this product is labeled with:
1136
+ - An inline disclaimer: "\u672C\u5185\u5BB9\u7531 AI \u751F\u6210 \xB7 AI-Generated Content".
1137
+ - Machine-readable metadata (where the output format supports it): \`X-AI-Generated: true\`.
1138
+ - For images/videos: visible watermark and/or embedded metadata per GB/T 45438-2025.
1139
+
1140
+ **ZH:** \u672C\u4EA7\u54C1\u4E2D\u7531 AI \u6A21\u578B\u751F\u6210\u7684\u6240\u6709\u5185\u5BB9\u5747\u5E26\u6709\uFF1A
1141
+ - \u663E\u5F0F\u6587\u5B57\u6807\u8BC6\uFF1A"\u672C\u5185\u5BB9\u7531 AI \u751F\u6210 \xB7 AI-Generated Content"\u3002
1142
+ - \u673A\u5668\u53EF\u8BFB\u5143\u6570\u636E\uFF08\u89C6\u8F93\u51FA\u683C\u5F0F\u652F\u6301\uFF09\uFF1A\`X-AI-Generated: true\`\u3002
1143
+ - \u56FE\u7247/\u89C6\u9891\uFF1A\u53EF\u89C1\u6C34\u5370\u548C/\u6216\u4F9D GB/T 45438-2025 \u5D4C\u5165\u5143\u6570\u636E\u3002
1144
+
1145
+ ---
1146
+
1147
+ ## 4. Training Data / \u8BAD\u7EC3\u6570\u636E\u8BF4\u660E
1148
+
1149
+ We do **not** train foundation models in-house. We use third-party model APIs whose training data
1150
+ practices are described in their respective privacy notices. We do not use customer data to train
1151
+ third-party models by default (opt-out where offered).
1152
+
1153
+ \u6211\u4EEC**\u4E0D**\u81EA\u884C\u8BAD\u7EC3\u57FA\u7840\u6A21\u578B\u3002\u6211\u4EEC\u4F7F\u7528\u7B2C\u4E09\u65B9\u6A21\u578B API\uFF0C\u5176\u8BAD\u7EC3\u6570\u636E\u5B9E\u8DF5\u8BE6\u89C1\u5404\u81EA\u7684\u9690\u79C1\u8BF4\u660E\u3002
1154
+ \u9ED8\u8BA4\u60C5\u51B5\u4E0B\uFF0C\u6211\u4EEC\u4E0D\u4F1A\u4F7F\u7528\u5BA2\u6237\u6570\u636E\u8BAD\u7EC3\u7B2C\u4E09\u65B9\u6A21\u578B\uFF08\u5728\u53EF\u9009\u9000\u51FA\u7684\u60C5\u51B5\u4E0B\u5DF2\u9009\u62E9\u9000\u51FA\uFF09\u3002
1155
+
1156
+ ---
1157
+
1158
+ ## 5. Reporting Channel / \u4E3E\u62A5\u6E20\u9053
1159
+
1160
+ If you encounter illegal content, hallucinated output causing harm, or suspected misuse, report to:
1161
+
1162
+ \u5982\u60A8\u53D1\u73B0\u8FDD\u6CD5\u5185\u5BB9\u3001\u5BFC\u81F4\u635F\u5BB3\u7684\u5E7B\u89C9\u8F93\u51FA\u6216\u7591\u4F3C\u6EE5\u7528\uFF0C\u8BF7\u901A\u8FC7\u4EE5\u4E0B\u6E20\u9053\u4E3E\u62A5\uFF1A
1163
+
1164
+ - **Email / \u90AE\u7BB1:** {{COMPANY_CONTACT_EMAIL}}
1165
+ - **In-app:** Settings \u2192 Feedback \u2192 Report AI Content / \u5E94\u7528\u5185\uFF1A\u8BBE\u7F6E \u2192 \u53CD\u9988 \u2192 \u4E3E\u62A5 AI \u5185\u5BB9
1166
+ - **Regulator / \u76D1\u7BA1\u90E8\u95E8:** [\u56FD\u5BB6\u4E92\u8054\u7F51\u4FE1\u606F\u529E\u516C\u5BA4\u4E3E\u62A5\u4E2D\u5FC3](https://www.12377.cn/)
1167
+
1168
+ ---
1169
+
1170
+ ## 6. Limitations / \u5C40\u9650\u6027
1171
+
1172
+ AI outputs may be inaccurate, biased, or out of date. Do **not** rely on AI outputs for medical,
1173
+ legal, financial, or safety-critical decisions without independent verification by a qualified
1174
+ human professional.
1175
+
1176
+ AI \u8F93\u51FA\u53EF\u80FD\u4E0D\u51C6\u786E\u3001\u5B58\u5728\u504F\u89C1\u6216\u8FC7\u65F6\u3002**\u8BF7\u52FF**\u5728\u672A\u7ECF\u5408\u683C\u4E13\u4E1A\u4EBA\u58EB\u72EC\u7ACB\u6838\u5B9E\u7684\u60C5\u51B5\u4E0B\uFF0C\u5C06 AI \u8F93\u51FA
1177
+ \u7528\u4E8E\u533B\u7597\u3001\u6CD5\u5F8B\u3001\u91D1\u878D\u6216\u5B89\u5168\u6538\u5173\u7684\u51B3\u7B56\u3002
1178
+ `,ur=`---
1179
+ title: Cross-Border Data Notice / \u6570\u636E\u51FA\u5883\u544A\u77E5
1180
+ description: Notice of cross-border transfer of personal information, per PIPL.
1181
+ lastUpdated: 2026-01-01
1182
+ ---
1183
+
1184
+ ${Oe}
1185
+
1186
+ # Cross-Border Data Notice / \u6570\u636E\u51FA\u5883\u544A\u77E5
1187
+
1188
+ Pursuant to Articles 38\u201341 of the **Personal Information Protection Law of the PRC** (\u300A\u4E2A\u4EBA\u4FE1\u606F
1189
+ \u4FDD\u62A4\u6CD5\u300B, "PIPL"), we notify users that the following personal information may be transferred
1190
+ outside mainland China to provide the Service.
1191
+
1192
+ \u4F9D\u636E\u300A\u4E2D\u534E\u4EBA\u6C11\u5171\u548C\u56FD\u4E2A\u4EBA\u4FE1\u606F\u4FDD\u62A4\u6CD5\u300B\u7B2C\u4E09\u5341\u516B\u6761\u81F3\u7B2C\u56DB\u5341\u4E00\u6761\uFF0C\u6211\u4EEC\u544A\u77E5\u7528\u6237\uFF0C\u4EE5\u4E0B\u4E2A\u4EBA\u4FE1\u606F
1193
+ \u53EF\u80FD\u4E3A\u63D0\u4F9B\u670D\u52A1\u800C\u4F20\u8F93\u81F3\u4E2D\u56FD\u5927\u9646\u5883\u5916\u3002
1194
+
1195
+ ---
1196
+
1197
+ ## 1. Overseas Recipients / \u5883\u5916\u63A5\u6536\u65B9
1198
+
1199
+ > TODO (fill in before launch / \u4E0A\u7EBF\u524D\u586B\u5199):
1200
+
1201
+ | Recipient / \u63A5\u6536\u65B9 | Country / \u56FD\u5BB6 | Purpose / \u76EE\u7684 | Data Categories / \u6570\u636E\u7C7B\u522B |
1202
+ | --- | --- | --- | --- |
1203
+ | _AWS (example)_ | _US_ | _Hosting_ | _Account, usage logs, content_ |
1204
+ | _OpenAI_ | _US_ | _LLM inference_ | _Prompts, generated content_ |
1205
+ | _Resend_ | _US_ | _Transactional email_ | _Email address, message body_ |
1206
+ | _Cloudflare_ | _US / Global_ | _CDN, DDoS protection_ | _IP address, request metadata_ |
1207
+
1208
+ ---
1209
+
1210
+ ## 2. Legal Basis / \u5408\u6CD5\u6027\u57FA\u7840
1211
+
1212
+ Cross-border transfers rely on one of:
1213
+ - **User's separate consent** under PIPL Article 39.
1214
+ - **Standard Contract** with the overseas recipient per CAC Measures (CAC \u6807\u51C6\u5408\u540C).
1215
+ - **Security Assessment** by the CAC (for transfers meeting statutory thresholds).
1216
+
1217
+ \u8DE8\u5883\u4F20\u8F93\u4F9D\u636E\u4EE5\u4E0B\u4E4B\u4E00\uFF1A
1218
+ - \u300A\u4E2A\u4EBA\u4FE1\u606F\u4FDD\u62A4\u6CD5\u300B\u7B2C\u4E09\u5341\u4E5D\u6761\u4E0B\u7684**\u5355\u72EC\u540C\u610F**\u3002
1219
+ - \u4E0E\u5883\u5916\u63A5\u6536\u65B9\u7B7E\u8BA2\u7684 **CAC \u6807\u51C6\u5408\u540C**\u3002
1220
+ - \u8FBE\u5230\u6CD5\u5B9A\u95E8\u69DB\u65F6\u7684**\u7F51\u4FE1\u529E\u5B89\u5168\u8BC4\u4F30**\u3002
1221
+
1222
+ ---
1223
+
1224
+ ## 3. Your Rights / \u60A8\u7684\u6743\u5229
1225
+
1226
+ You may **refuse** cross-border transfer at any time by contacting {{COMPANY_CONTACT_EMAIL}}.
1227
+ Refusal may limit your ability to use features that rely on overseas services.
1228
+
1229
+ \u60A8\u53EF\u968F\u65F6\u8054\u7CFB {{COMPANY_CONTACT_EMAIL}} \u62D2\u7EDD\u8DE8\u5883\u4F20\u8F93\u3002\u62D2\u7EDD\u53EF\u80FD\u4F1A\u9650\u5236\u60A8\u4F7F\u7528\u4F9D\u8D56\u5883\u5916\u670D\u52A1\u7684
1230
+ \u529F\u80FD\u3002
1231
+
1232
+ ---
1233
+
1234
+ ## 4. Safeguards / \u4FDD\u969C\u63AA\u65BD
1235
+
1236
+ - Encryption in transit (TLS 1.2+) and at rest (AES-256).
1237
+ - Contractual clauses obligating overseas recipients to provide PIPL-equivalent protection.
1238
+ - Access controls, audit logging, and breach notification procedures.
1239
+
1240
+ - \u4F20\u8F93\u4E2D\u52A0\u5BC6 (TLS 1.2+) \u53CA\u9759\u6001\u52A0\u5BC6 (AES-256)\u3002
1241
+ - \u5408\u540C\u6761\u6B3E\u8981\u6C42\u5883\u5916\u63A5\u6536\u65B9\u63D0\u4F9B\u7B49\u540C\u4E8E PIPL \u7684\u4FDD\u62A4\u3002
1242
+ - \u8BBF\u95EE\u63A7\u5236\u3001\u5BA1\u8BA1\u65E5\u5FD7\u548C\u8FDD\u89C4\u901A\u77E5\u7A0B\u5E8F\u3002
1243
+ `,mr=`---
1244
+ title: Personal Information Collection List / \u4E2A\u4EBA\u4FE1\u606F\u6536\u96C6\u6E05\u5355
1245
+ description: Itemized list of personal information collected, per PIPL Article 17.
1246
+ lastUpdated: 2026-01-01
1247
+ ---
1248
+
1249
+ ${Oe}
1250
+
1251
+ # Personal Information Collection List / \u4E2A\u4EBA\u4FE1\u606F\u6536\u96C6\u6E05\u5355
1252
+
1253
+ Pursuant to Article 17 of the **PIPL**, we publish an itemized list of personal information
1254
+ collected, the purpose of processing, and retention period.
1255
+
1256
+ \u4F9D\u636E\u300A\u4E2A\u4EBA\u4FE1\u606F\u4FDD\u62A4\u6CD5\u300B\u7B2C\u5341\u4E03\u6761\uFF0C\u6211\u4EEC\u516C\u5F00\u4E2A\u4EBA\u4FE1\u606F\u6536\u96C6\u6E05\u5355\uFF0C\u5305\u62EC\u6536\u96C6\u9879\u3001\u5904\u7406\u76EE\u7684\u548C\u4FDD\u7559\u671F\u9650\u3002
1257
+
1258
+ ---
1259
+
1260
+ | Category / \u7C7B\u522B | Item / \u9879 | Purpose / \u76EE\u7684 | Source / \u6765\u6E90 | Retention / \u4FDD\u7559\u671F | Required? / \u5FC5\u8981? |
1261
+ | --- | --- | --- | --- | --- | --- |
1262
+ | Account / \u8D26\u6237 | Email / \u90AE\u7BB1 | Registration, auth, notifications / \u6CE8\u518C\u3001\u767B\u5F55\u3001\u901A\u77E5 | User-provided | Until account deletion + 30d / \u8D26\u6237\u5220\u9664\u540E 30 \u5929 | Yes / \u662F |
1263
+ | Account / \u8D26\u6237 | Password (hashed) / \u5BC6\u7801\uFF08\u52A0\u5BC6\uFF09 | Authentication / \u8EAB\u4EFD\u9A8C\u8BC1 | User-provided | Until account deletion / \u8D26\u6237\u5220\u9664 | Yes / \u662F |
1264
+ | Account / \u8D26\u6237 | Phone / \u624B\u673A\u53F7 | 2FA, account recovery / \u53CC\u56E0\u7D20\u3001\u8D26\u6237\u6062\u590D | User-provided | Until account deletion / \u8D26\u6237\u5220\u9664 | Optional / \u53EF\u9009 |
1265
+ | Profile / \u8D44\u6599 | Display name, avatar / \u6635\u79F0\u3001\u5934\u50CF | Service personalization / \u670D\u52A1\u4E2A\u6027\u5316 | User-provided | Until updated / \u76F4\u81F3\u66F4\u65B0 | Optional / \u53EF\u9009 |
1266
+ | Usage / \u4F7F\u7528 | IP address / IP \u5730\u5740 | Security, abuse prevention / \u5B89\u5168\u3001\u9632\u6EE5\u7528 | Automatic / \u81EA\u52A8 | 90 days / 90 \u5929 | Yes / \u662F |
1267
+ | Usage / \u4F7F\u7528 | User agent / \u6D4F\u89C8\u5668\u6807\u8BC6 | Compatibility, security / \u517C\u5BB9\u6027\u3001\u5B89\u5168 | Automatic / \u81EA\u52A8 | 90 days / 90 \u5929 | Yes / \u662F |
1268
+ | Usage / \u4F7F\u7528 | Page views, clickstream / \u9875\u9762\u6D4F\u89C8\u3001\u70B9\u51FB\u6D41 | Analytics, product improvement / \u5206\u6790\u3001\u4EA7\u54C1\u6539\u8FDB | Automatic / \u81EA\u52A8 | 13 months / 13 \u4E2A\u6708 | Consent / \u9700\u540C\u610F |
1269
+ | Usage / \u4F7F\u7528 | Cookies / Cookie | See Cookie Preferences / \u8BE6\u89C1 Cookie \u504F\u597D | Automatic / \u81EA\u52A8 | Varies / \u4E0D\u4E00 | Consent / \u9700\u540C\u610F |
1270
+ | Billing / \u652F\u4ED8 | Billing address / \u8D26\u5355\u5730\u5740 | Payment processing / \u652F\u4ED8\u5904\u7406 | User-provided | 7 years (tax) / 7 \u5E74\uFF08\u7A0E\u52A1\uFF09 | If paid plan / \u4ED8\u8D39\u65F6 |
1271
+ | Billing / \u652F\u4ED8 | Payment method token / \u652F\u4ED8\u4EE4\u724C | Recurring billing / \u8BA2\u9605\u7EED\u8D39 | Stripe / payment processor | Until deletion / \u76F4\u81F3\u5220\u9664 | If paid plan / \u4ED8\u8D39\u65F6 |
1272
+ | Content / \u5185\u5BB9 | Uploaded files / \u4E0A\u4F20\u6587\u4EF6 | Service delivery / \u670D\u52A1\u4EA4\u4ED8 | User-provided | Until deletion by user / \u76F4\u81F3\u7528\u6237\u5220\u9664 | Optional / \u53EF\u9009 |
1273
+ | Content / \u5185\u5BB9 | Messages, prompts / \u6D88\u606F\u3001\u63D0\u793A\u8BCD | Service delivery, AI inference / \u670D\u52A1\u4EA4\u4ED8\u3001AI \u63A8\u7406 | User-provided | 30 days then purged unless saved / 30 \u5929\u540E\u6E05\u9664\uFF08\u9664\u975E\u4FDD\u5B58\uFF09 | Optional / \u53EF\u9009 |
1274
+ | Behavior / \u884C\u4E3A | Feature flags, experiments / \u529F\u80FD\u5F00\u5173\u3001\u5B9E\u9A8C | Product personalization / \u4EA7\u54C1\u4E2A\u6027\u5316 | Automatic / \u81EA\u52A8 | 13 months / 13 \u4E2A\u6708 | Consent / \u9700\u540C\u610F |
1275
+ | Device / \u8BBE\u5907 | Device ID, OS version / \u8BBE\u5907 ID\u3001\u7CFB\u7EDF\u7248\u672C | Security, compatibility / \u5B89\u5168\u3001\u517C\u5BB9\u6027 | Automatic / \u81EA\u52A8 | 90 days / 90 \u5929 | Yes / \u662F |
1276
+
1277
+ ---
1278
+
1279
+ ## Contact / \u8054\u7CFB\u65B9\u5F0F
1280
+
1281
+ To exercise your rights (access, rectify, delete, export, withdraw consent), contact
1282
+ **{{COMPANY_CONTACT_EMAIL}}**.
1283
+
1284
+ \u5982\u9700\u884C\u4F7F\u60A8\u7684\u6743\u5229\uFF08\u8BBF\u95EE\u3001\u66F4\u6B63\u3001\u5220\u9664\u3001\u5BFC\u51FA\u3001\u64A4\u56DE\u540C\u610F\uFF09\uFF0C\u8BF7\u8054\u7CFB **{{COMPANY_CONTACT_EMAIL}}**\u3002
1285
+ `;async function gr(e,t){let n=D(e);if(!P.existsSync(n))return;let r=v.join(n,"src","components","compliance","Footer.tsx");k(r,or);let o=v.join(n,"src","components","compliance","README.md");k(o,["# Compliance Components","",`Auto-generated by \`create-sailor\` for region: \`${t}\`.`,"","## Mounting the Footer","","```tsx",'import { Footer } from "@/components/compliance/Footer";','import { CookieBanner } from "@/components/compliance/CookieBanner";',"","export default function RootLayout({ children }) {"," return ("," <html>"," <body>"," {children}",` <Footer region="${t}" />`," <CookieBanner />"," </body>"," </html>"," );","}","```","","## Placeholders","","Policy MDX files contain `{{COMPANY_NAME}}`, `{{COMPANY_ADDRESS}}`,","`{{COMPANY_CONTACT_EMAIL}}`, `{{GOVERNING_JURISDICTION}}`, and","`{{AIGC_ALGORITHM_FILING_NUMBER}}`. These are filled by the `brand-apply` step.",""].join(`
1286
+ `))}async function fr(e,t){let n=D(e);if(!P.existsSync(n))return;let r=v.join(n,"src","components","compliance","CookieBanner.tsx");k(r,ar)}async function yr(e,t){let n=D(e);if(!P.existsSync(n))return;let r=v.join(n,ke,"privacy","page.mdx");k(r,pr)}async function Sr(e,t){let n=D(e);if(!P.existsSync(n))return;let r=v.join(n,ke,"terms","page.mdx");k(r,lr)}async function Er(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,tt,"IcpFooter.tsx");k(n,sr),nt(e,["# Chinese ICP filing (\u5DE5\u4FE1\u90E8 ICP \u5907\u6848) \u2014 required to legally host a","# public-facing site from mainland China. Apply at https://beian.miit.gov.cn/",'NEXT_PUBLIC_ICP_BEIAN="" # \u5982 \u82CFICP\u59072024XXXXXX\u53F7'].join(`
1287
+ `))}async function hr(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,tt,"PublicSecurityFooter.tsx");k(n,ir),nt(e,["# Chinese Public Security Bureau filing (\u516C\u5B89\u8054\u7F51\u5907\u6848)","# Required in addition to ICP. Apply at http://www.beian.gov.cn/",'NEXT_PUBLIC_PSB_BEIAN="" # \u5982 \u82CF\u516C\u7F51\u5B89\u5907 32011402XXXXXX\u53F7','NEXT_PUBLIC_PSB_CODE="" # 14-digit record code'].join(`
1288
+ `))}async function _r(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,tt,"AigcDisclaimer.tsx");k(n,cr),nt(e,["# Generative AI algorithm filing (\u7B97\u6CD5\u5907\u6848\u53F7) \u2014 required by","# \u7F51\u4FE1\u529E\u300A\u751F\u6210\u5F0F\u4EBA\u5DE5\u667A\u80FD\u670D\u52A1\u7BA1\u7406\u6682\u884C\u529E\u6CD5\u300Bfor services offered in China.",'NEXT_PUBLIC_AIGC_FILING_NUMBER="" # e.g. \u7F51\u4FE1\u7B97\u5907XXXXXXXX\u53F7'].join(`
1289
+ `))}async function vr(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,ke,"aigc-disclosure","page.mdx");k(n,dr)}async function Ir(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,ke,"cross-border-data","page.mdx");k(n,ur)}async function Pr(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,ke,"pii-collection","page.mdx");k(n,mr)}async function wt(e,t){try{await gr(e,t),await fr(e,t),await yr(e,t),await Sr(e,t),(t==="cn"||t==="hybrid")&&(await Er(e),await hr(e),await _r(e),await vr(e),await Ir(e),await Pr(e))}catch(n){throw console.error("applyComplianceTemplates failed:",n),new Error(`Failed to apply compliance templates for region "${t}". Check that ${e} is writable.`)}}import br from"fs";import Ar from"path";async function Nt(e,t){let n=Ar.join(e,"nebutra.config.json");br.writeFileSync(n,JSON.stringify(t,null,2)+`
1290
+ `)}import q from"fs";import rt from"path";function Cr(e){q.existsSync(e)&&q.rmSync(e,{recursive:!0,force:!0})}function Tt(e,t){return e.replace(/provider\s*=\s*"postgresql"/,`provider = "${t}"`)}function wr(e){return e.replace(/^\s*directUrl\s*=\s*env\([^)]*\)\s*\n/gm,"")}function xt(e){let t=e.replace(/^\s*extensions\s*=\s*\[[^\]]*\]\s*\n/gm,"");return t=t.replace(/previewFeatures\s*=\s*\[\s*"postgresqlExtensions"\s*\]\s*\n/g,""),t}function Nr(e,t){let n=rt.join(e,".env.example");q.existsSync(n)?q.appendFileSync(n,`
1291
+ `+t):q.writeFileSync(n,t)}async function Rt(e,t,n){let r=rt.join(e,"packages","db");if(t==="none"){Cr(r);return}let o=rt.join(r,"prisma","schema.prisma"),s;if(t==="postgresql"?s=`postgresql://postgres:postgres@localhost:5432/${n}`:t==="mysql"?s=`mysql://root:root@localhost:3306/${n}`:s="file:./dev.db",Nr(e,`# Database (${t})
1292
+ DATABASE_URL="${s}"
1293
+ `),!q.existsSync(o))return;let a=q.readFileSync(o,"utf8"),c=a;t==="mysql"?(c=Tt(c,"mysql"),c=xt(c)):t==="sqlite"&&(c=Tt(c,"sqlite"),c=wr(c),c=xt(c)),c!==a&&q.writeFileSync(o,c)}import Ue from"fs";import N from"path";import{fileURLToPath as Tr}from"url";var xr=Tr(import.meta.url),Rr=N.dirname(xr);async function kt(e,t){if(t==="none")return;let n=N.join(Rr,"../../templates/deploy");t==="vercel"?await Ue.promises.copyFile(N.join(n,"vercel.json"),N.join(e,"vercel.json")):t==="railway"?await Ue.promises.copyFile(N.join(n,"railway.toml"),N.join(e,"railway.toml")):t==="cloudflare"?await Ue.promises.copyFile(N.join(n,"wrangler.toml"),N.join(e,"wrangler.toml")):t==="selfhost"&&(await Ue.promises.copyFile(N.join(n,"docker-compose.yml"),N.join(e,"docker-compose.yml")),await Ue.promises.copyFile(N.join(n,"Dockerfile.web"),N.join(e,"Dockerfile.web")))}import G from"fs";import ne from"path";import{fileURLToPath as kr}from"url";var Or=kr(import.meta.url),Ot=ne.dirname(Or),Ur=/\.(md|mdx|json|ts|tsx|js|mjs|cjs|yaml|yml)$/;function Lr(e){let t=[ne.join(Ot,"..","templates","docs",e),ne.join(Ot,"..","..","templates","docs",e)];for(let n of t)if(G.existsSync(n))return n;return t[0]}async function Ut(e,t){if(t.framework==="none")return;let n=(t.framework==="fumadocs","fumadocs"),r=Lr(n),o=ne.join(e,"apps","docs");if(!G.existsSync(r))throw new Error(`Docs template not found: ${r}`);Lt(r,o),Mt(o,{"{PRODUCT_NAME}":t.projectName})}function Lt(e,t){G.mkdirSync(t,{recursive:!0});for(let n of G.readdirSync(e,{withFileTypes:!0})){let r=ne.join(e,n.name),o=ne.join(t,n.name);n.isDirectory()?Lt(r,o):G.copyFileSync(r,o)}}function Mt(e,t){for(let n of G.readdirSync(e,{withFileTypes:!0})){let r=ne.join(e,n.name);if(n.isDirectory())Mt(r,t);else if(Ur.test(n.name)){let o=G.readFileSync(r,"utf-8");for(let[s,a]of Object.entries(t))o=o.split(s).join(a);G.writeFileSync(r,o)}}}import oe from"fs";import re from"path";var We=[{id:"resend",name:"Resend",region:"global",envVars:["RESEND_API_KEY","RESEND_FROM"],docs:"https://resend.com/docs",pkg:"resend",pkgVersion:"^4.0.1"},{id:"postmark",name:"Postmark",region:"global",envVars:["POSTMARK_API_KEY","POSTMARK_FROM"],docs:"https://postmarkapp.com/developer",pkg:"postmark",pkgVersion:"^4.0.5"},{id:"ses",name:"AWS SES",region:"global",envVars:["AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_SES_REGION","SES_FROM"],docs:"https://docs.aws.amazon.com/ses",pkg:"@aws-sdk/client-sesv2",pkgVersion:"^3.658.0"},{id:"aliyun-dm",name:"\u963F\u91CC\u4E91\u90AE\u4EF6\u63A8\u9001",region:"cn",envVars:["ALIYUN_DM_ACCESS_KEY","ALIYUN_DM_ACCESS_SECRET","ALIYUN_DM_ACCOUNT_NAME","ALIYUN_DM_FROM_ALIAS"],docs:"https://help.aliyun.com/product/29412.html",pkg:"@alicloud/dm20151123",pkgVersion:"^3.0.0"},{id:"tencent-ses",name:"\u817E\u8BAF\u4E91 SES",region:"cn",envVars:["TENCENT_SECRET_ID","TENCENT_SECRET_KEY","TENCENT_SES_FROM_EMAIL"],docs:"https://cloud.tencent.com/document/product/1288",pkg:"tencentcloud-sdk-nodejs-ses",pkgVersion:"^4.0.900"},{id:"netease",name:"\u7F51\u6613\u4F01\u4E1A\u90AE\u7BB1",region:"cn",envVars:["NETEASE_USER","NETEASE_PASS","NETEASE_FROM"],docs:"https://qiye.163.com/help",pkg:"nodemailer",pkgVersion:"^6.9.15"},{id:"none",name:"None",region:"global",envVars:[],docs:""}];function jt(e){return We.find(t=>t.id===e)}var Qs=We.reduce((e,t)=>(e[t.region]||(e[t.region]=[]),e[t.region].push(t),e),{});var se="@nebutra/email";function ot(e,t){oe.mkdirSync(re.dirname(e),{recursive:!0}),oe.writeFileSync(e,t)}function Mr(e,t){let n=re.join(e,".env.example");oe.existsSync(n)?oe.appendFileSync(n,`
1294
+ `+t):oe.writeFileSync(n,t)}function jr(e){if(e.envVars.length===0)return"";let t=`# Email provider: ${e.name}`,n=e.envVars.map(r=>`${r}=`).join(`
1295
+ `);return`${t}
1296
+ ${n}
1297
+ `}var Dr=`/**
1298
+ * Public inputs accepted by the \`sendEmail\` wrapper.
1299
+ * Keep the surface minimal \u2014 individual providers may accept more, but
1300
+ * consumers should code against this shape for portability.
1301
+ */
1302
+ export interface SendEmailInput {
1303
+ to: string | string[];
1304
+ subject: string;
1305
+ html?: string;
1306
+ text?: string;
1307
+ from?: string;
1308
+ replyTo?: string;
1309
+ }
1310
+
1311
+ export interface SendEmailResult {
1312
+ /** Provider-specific message identifier, when available. */
1313
+ id?: string;
1314
+ /** Raw provider response \u2014 useful for debugging, not a stable contract. */
1315
+ raw?: unknown;
1316
+ }
1317
+ `;function Fr(){return`/**
1318
+ * @${se.slice(1)} \u2014 Resend transactional email wrapper.
1319
+ *
1320
+ * Docs: https://resend.com/docs
1321
+ */
1322
+ import { Resend } from "resend";
1323
+
1324
+ import type { SendEmailInput, SendEmailResult } from "./types.js";
1325
+
1326
+ const apiKey = process.env.RESEND_API_KEY;
1327
+ if (!apiKey) {
1328
+ throw new Error("RESEND_API_KEY not configured");
1329
+ }
1330
+
1331
+ const resend = new Resend(apiKey);
1332
+
1333
+ export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
1334
+ const from = input.from ?? process.env.RESEND_FROM;
1335
+ if (!from) {
1336
+ throw new Error("RESEND_FROM (or input.from) must be provided");
1337
+ }
1338
+
1339
+ const { data, error } = await resend.emails.send({
1340
+ from,
1341
+ to: Array.isArray(input.to) ? input.to : [input.to],
1342
+ subject: input.subject,
1343
+ html: input.html,
1344
+ text: input.text,
1345
+ ...(input.replyTo ? { replyTo: input.replyTo } : {}),
1346
+ });
1347
+
1348
+ if (error) {
1349
+ throw new Error(\`Resend send failed: \${error.message}\`);
1350
+ }
1351
+
1352
+ return { id: data?.id, raw: data };
1353
+ }
1354
+
1355
+ export type { SendEmailInput, SendEmailResult } from "./types.js";
1356
+ `}function Br(){return`/**
1357
+ * @${se.slice(1)} \u2014 Postmark transactional email wrapper.
1358
+ *
1359
+ * Docs: https://postmarkapp.com/developer
1360
+ */
1361
+ import { ServerClient } from "postmark";
1362
+
1363
+ import type { SendEmailInput, SendEmailResult } from "./types.js";
1364
+
1365
+ const apiKey = process.env.POSTMARK_API_KEY;
1366
+ if (!apiKey) {
1367
+ throw new Error("POSTMARK_API_KEY not configured");
1368
+ }
1369
+
1370
+ const client = new ServerClient(apiKey);
1371
+
1372
+ export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
1373
+ const from = input.from ?? process.env.POSTMARK_FROM;
1374
+ if (!from) {
1375
+ throw new Error("POSTMARK_FROM (or input.from) must be provided");
1376
+ }
1377
+
1378
+ const to = Array.isArray(input.to) ? input.to.join(", ") : input.to;
1379
+
1380
+ const response = await client.sendEmail({
1381
+ From: from,
1382
+ To: to,
1383
+ Subject: input.subject,
1384
+ HtmlBody: input.html,
1385
+ TextBody: input.text,
1386
+ ReplyTo: input.replyTo,
1387
+ MessageStream: "outbound",
1388
+ });
1389
+
1390
+ return { id: response.MessageID, raw: response };
1391
+ }
1392
+
1393
+ export type { SendEmailInput, SendEmailResult } from "./types.js";
1394
+ `}function Yr(){return`/**
1395
+ * @${se.slice(1)} \u2014 AWS SES v2 transactional email wrapper.
1396
+ *
1397
+ * Docs: https://docs.aws.amazon.com/ses
1398
+ */
1399
+ import {
1400
+ SendEmailCommand,
1401
+ SESv2Client,
1402
+ } from "@aws-sdk/client-sesv2";
1403
+
1404
+ import type { SendEmailInput, SendEmailResult } from "./types.js";
1405
+
1406
+ const region = process.env.AWS_SES_REGION;
1407
+ if (!region) {
1408
+ throw new Error("AWS_SES_REGION not configured");
1409
+ }
1410
+
1411
+ const client = new SESv2Client({
1412
+ region,
1413
+ credentials:
1414
+ process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY
1415
+ ? {
1416
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
1417
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
1418
+ }
1419
+ : undefined,
1420
+ });
1421
+
1422
+ export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
1423
+ const from = input.from ?? process.env.SES_FROM;
1424
+ if (!from) {
1425
+ throw new Error("SES_FROM (or input.from) must be provided");
1426
+ }
1427
+
1428
+ const toAddresses = Array.isArray(input.to) ? input.to : [input.to];
1429
+
1430
+ const command = new SendEmailCommand({
1431
+ FromEmailAddress: from,
1432
+ Destination: { ToAddresses: toAddresses },
1433
+ ReplyToAddresses: input.replyTo ? [input.replyTo] : undefined,
1434
+ Content: {
1435
+ Simple: {
1436
+ Subject: { Data: input.subject, Charset: "UTF-8" },
1437
+ Body: {
1438
+ ...(input.html ? { Html: { Data: input.html, Charset: "UTF-8" } } : {}),
1439
+ ...(input.text ? { Text: { Data: input.text, Charset: "UTF-8" } } : {}),
1440
+ },
1441
+ },
1442
+ },
1443
+ });
1444
+
1445
+ const response = await client.send(command);
1446
+ return { id: response.MessageId, raw: response };
1447
+ }
1448
+
1449
+ export type { SendEmailInput, SendEmailResult } from "./types.js";
1450
+ `}function Kr(){return`/**
1451
+ * @${se.slice(1)} \u2014 \u963F\u91CC\u4E91\u90AE\u4EF6\u63A8\u9001 (Aliyun DirectMail) wrapper.
1452
+ *
1453
+ * Docs: https://help.aliyun.com/product/29412.html
1454
+ */
1455
+ import Dm20151123, * as $Dm20151123 from "@alicloud/dm20151123";
1456
+ import * as $OpenApi from "@alicloud/openapi-client";
1457
+
1458
+ import type { SendEmailInput, SendEmailResult } from "./types.js";
1459
+
1460
+ const accessKeyId = process.env.ALIYUN_DM_ACCESS_KEY;
1461
+ const accessKeySecret = process.env.ALIYUN_DM_ACCESS_SECRET;
1462
+ if (!accessKeyId || !accessKeySecret) {
1463
+ throw new Error(
1464
+ "ALIYUN_DM_ACCESS_KEY and ALIYUN_DM_ACCESS_SECRET must be configured",
1465
+ );
1466
+ }
1467
+
1468
+ const config = new $OpenApi.Config({
1469
+ accessKeyId,
1470
+ accessKeySecret,
1471
+ endpoint: "dm.aliyuncs.com",
1472
+ });
1473
+
1474
+ const client = new Dm20151123.default(config);
1475
+
1476
+ export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
1477
+ const accountName = process.env.ALIYUN_DM_ACCOUNT_NAME;
1478
+ if (!accountName) {
1479
+ throw new Error("ALIYUN_DM_ACCOUNT_NAME must be configured");
1480
+ }
1481
+
1482
+ const toAddress = Array.isArray(input.to) ? input.to.join(",") : input.to;
1483
+
1484
+ const req = new $Dm20151123.SingleSendMailRequest({
1485
+ accountName,
1486
+ addressType: 1,
1487
+ replyToAddress: false,
1488
+ toAddress,
1489
+ subject: input.subject,
1490
+ htmlBody: input.html,
1491
+ textBody: input.text,
1492
+ fromAlias: process.env.ALIYUN_DM_FROM_ALIAS,
1493
+ });
1494
+
1495
+ const response = await client.singleSendMail(req);
1496
+ return { id: response.body?.requestId, raw: response };
1497
+ }
1498
+
1499
+ export type { SendEmailInput, SendEmailResult } from "./types.js";
1500
+ `}function Vr(){return`/**
1501
+ * @${se.slice(1)} \u2014 \u817E\u8BAF\u4E91 SES wrapper.
1502
+ *
1503
+ * Docs: https://cloud.tencent.com/document/product/1288
1504
+ */
1505
+ import tencentcloud from "tencentcloud-sdk-nodejs-ses";
1506
+
1507
+ import type { SendEmailInput, SendEmailResult } from "./types.js";
1508
+
1509
+ const secretId = process.env.TENCENT_SECRET_ID;
1510
+ const secretKey = process.env.TENCENT_SECRET_KEY;
1511
+ if (!secretId || !secretKey) {
1512
+ throw new Error("TENCENT_SECRET_ID and TENCENT_SECRET_KEY must be configured");
1513
+ }
1514
+
1515
+ const SesClient = tencentcloud.ses.v20201002.Client;
1516
+
1517
+ const client = new SesClient({
1518
+ credential: { secretId, secretKey },
1519
+ region: "ap-hongkong",
1520
+ profile: {
1521
+ httpProfile: { endpoint: "ses.tencentcloudapi.com" },
1522
+ },
1523
+ });
1524
+
1525
+ export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
1526
+ const from = input.from ?? process.env.TENCENT_SES_FROM_EMAIL;
1527
+ if (!from) {
1528
+ throw new Error("TENCENT_SES_FROM_EMAIL (or input.from) must be provided");
1529
+ }
1530
+
1531
+ const destination = Array.isArray(input.to) ? input.to : [input.to];
1532
+
1533
+ const response = await client.SendEmail({
1534
+ FromEmailAddress: from,
1535
+ Destination: destination,
1536
+ Subject: input.subject,
1537
+ ReplyToAddresses: input.replyTo,
1538
+ Simple: {
1539
+ Html: input.html,
1540
+ Text: input.text,
1541
+ },
1542
+ });
1543
+
1544
+ return { id: response.MessageId, raw: response };
1545
+ }
1546
+
1547
+ export type { SendEmailInput, SendEmailResult } from "./types.js";
1548
+ `}function $r(){return`/**
1549
+ * @${se.slice(1)} \u2014 \u7F51\u6613\u4F01\u4E1A\u90AE\u7BB1 via SMTP (nodemailer).
1550
+ *
1551
+ * Docs: https://qiye.163.com/help
1552
+ */
1553
+ import nodemailer from "nodemailer";
1554
+
1555
+ import type { SendEmailInput, SendEmailResult } from "./types.js";
1556
+
1557
+ const user = process.env.NETEASE_USER;
1558
+ const pass = process.env.NETEASE_PASS;
1559
+ if (!user || !pass) {
1560
+ throw new Error("NETEASE_USER and NETEASE_PASS must be configured");
1561
+ }
1562
+
1563
+ const transporter = nodemailer.createTransport({
1564
+ host: "smtp.qiye.163.com",
1565
+ port: 465,
1566
+ secure: true,
1567
+ auth: { user, pass },
1568
+ });
1569
+
1570
+ export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
1571
+ const from = input.from ?? process.env.NETEASE_FROM ?? user;
1572
+
1573
+ const response = await transporter.sendMail({
1574
+ from,
1575
+ to: Array.isArray(input.to) ? input.to.join(", ") : input.to,
1576
+ subject: input.subject,
1577
+ html: input.html,
1578
+ text: input.text,
1579
+ replyTo: input.replyTo,
1580
+ });
1581
+
1582
+ return { id: response.messageId, raw: response };
1583
+ }
1584
+
1585
+ export type { SendEmailInput, SendEmailResult } from "./types.js";
1586
+ `}function qr(e){switch(e){case"resend":return Fr();case"postmark":return Br();case"ses":return Yr();case"aliyun-dm":return Kr();case"tencent-ses":return Vr();case"netease":return $r();case"none":return`export {};
1587
+ `}}function Gr(e){let t={};e.pkg&&e.pkgVersion&&(t[e.pkg]=e.pkgVersion),e.id==="aliyun-dm"&&(t["@alicloud/openapi-client"]="^0.4.12");let n={name:se,version:"0.1.0",private:!0,description:`Transactional email via ${e.name}`,type:"module",main:"./dist/index.js",exports:{".":{import:"./dist/index.js",types:"./dist/index.d.ts"}},scripts:{build:"tsup",typecheck:"tsc --noEmit",clean:"rm -rf dist .turbo"},dependencies:t,devDependencies:{tsup:"^8.5.0",typescript:"catalog:"}};return JSON.stringify(n,null,2)+`
1588
+ `}async function Dt(e,t,n){if(t==="none")return;let r=jt(t);if(!r)throw new Error(`Unknown email provider "${t}". Valid ids: ${We.map(a=>a.id).join(", ")}`);let o=re.join(e,"packages","email"),s=re.join(o,"src");oe.existsSync(s)&&oe.rmSync(s,{recursive:!0,force:!0}),ot(re.join(o,"package.json"),Gr(r)),ot(re.join(s,"types.ts"),Dr),ot(re.join(s,"index.ts"),qr(r.id)),Mr(e,jr(r))}import Bt from"crypto";import Le from"fs";import Ft from"path";function ie(e){return Bt.randomBytes(e).toString("base64").replace(/=+$/,"")}function Wr(e){return Bt.randomBytes(e).toString("hex")}function Xr(){return{AUTH_SECRET:ie(32),NEXTAUTH_SECRET:ie(32),BETTER_AUTH_SECRET:ie(32),JWT_SECRET:ie(32),WEBHOOK_SECRET:Wr(16),ENCRYPTION_KEY:ie(32),API_SECRET_KEY:ie(32),SESSION_SECRET:ie(32)}}function Hr(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}function zr(e,t,n){let r=new RegExp(`^(?!#)\\s*${t}=.*$`,"gm"),o=Hr(n);return e.replace(r,`${t}="${o}"`)}async function Yt(e){let t=Ft.join(e,".env.example"),n=Ft.join(e,".env.local");if(!Le.existsSync(t))return null;try{let r=Le.readFileSync(t,"utf8"),o=Xr(),a=Le.existsSync(n)?Le.readFileSync(n,"utf8"):r;for(let c of Object.keys(o))a=zr(a,c,o[c]);return Le.writeFileSync(n,a),o}catch(r){let o=r instanceof Error?r.message:String(r);throw console.error(`[create-sailor] Failed to generate env secrets: ${o}`),new Error(`Unable to write .env.local: ${o}`)}}import Xe from"fs";import Kt from"path";async function Vt(e,t){let n=`# Automatically injected by create-sailor
40
1589
  DATABASE_URL="${String(t.databaseUrl)}"
41
1590
 
42
1591
  NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="${String(t.clerkPublishable)}"
43
1592
  CLERK_SECRET_KEY="${String(t.clerkSecret)}"
44
1593
 
45
1594
  NEXT_PUBLIC_SITE_URL="http://localhost:3000"
46
- `,s=fe.join(e,".env"),r=fe.join(e,".env.local");!z.existsSync(s)&&!z.existsSync(r)?z.writeFileSync(r,n):z.appendFileSync(r,`
47
- `+n)}import{execSync as Xe}from"child_process";import w from"fs";import q from"path";import Ze from"ignore";var Qe={mirror:{repo:process.env.SAILOR_TEMPLATE_REPO||"Nebutra/Sailor-Template",ref:process.env.SAILOR_TEMPLATE_REF||"main",applyIgnore:!1},main:{repo:"Nebutra/Nebutra-Sailor",ref:"main",applyIgnore:!0}};function et(e,t){return`https://github.com/${e}/archive/refs/heads/${t}.tar.gz`}function ge(e,t,n){let s=w.readdirSync(t,{withFileTypes:!0});for(let r of s){if(r.name==="node_modules"||r.name===".git")continue;let i=q.join(t,r.name),l=q.relative(e,i).split(q.sep).join("/");r.isDirectory()?(n.push(l+"/"),ge(e,i,n)):n.push(l)}}function tt(e){let t=q.join(e,".templateignore");if(!w.existsSync(t))return;let n=w.readFileSync(t,"utf8"),s=Ze().add(n),r=[];ge(e,e,r);let i=r.map(a=>a.endsWith("/")?a.slice(0,-1):a),l=new Set(s.filter(i)),c=i.filter(a=>!l.has(a)).sort((a,d)=>d.split("/").length-a.split("/").length);for(let a of c){let d=q.join(e,a);try{w.existsSync(d)&&w.rmSync(d,{recursive:!0,force:!0})}catch{}}try{w.existsSync(t)&&w.rmSync(t,{force:!0})}catch{}}function nt(e,t){Xe(`set -o pipefail; curl -sSfL ${e} | tar -xz -C "${t}" --strip-components=1`,{stdio:"ignore",shell:"bash"})}async function ve(e){w.mkdirSync(e,{recursive:!0});let n=process.env.SAILOR_TEMPLATE_SOURCE==="main"?["main"]:["mirror","main"],s;for(let r of n){let i=Qe[r],l=et(i.repo,i.ref);try{if(nt(l,e),i.applyIgnore)try{tt(e)}catch(c){throw new Error(`Template cloned, but .templateignore processing failed: ${c instanceof Error?c.message:String(c)}`)}return}catch(c){s=c instanceof Error?c:new Error(String(c));try{let a=w.readdirSync(e);for(let d of a)w.rmSync(q.join(e,d),{recursive:!0,force:!0})}catch{}}}throw new Error(`Failed to download the template from any source. Last error: ${s?.message??"unknown"}
1595
+ `,r=Kt.join(e,".env"),o=Kt.join(e,".env.local");!Xe.existsSync(r)&&!Xe.existsSync(o)?Xe.writeFileSync(o,n):Xe.appendFileSync(o,`
1596
+ `+n)}import{execSync as Jr}from"child_process";import F from"fs";import Pe from"path";import Qr from"ignore";var Zr={mirror:{repo:process.env.SAILOR_TEMPLATE_REPO||"Nebutra/Sailor-Template",ref:process.env.SAILOR_TEMPLATE_REF||"main",applyIgnore:!1},main:{repo:"Nebutra/Nebutra-Sailor",ref:"main",applyIgnore:!0}};function eo(e,t){return`https://github.com/${e}/archive/refs/heads/${t}.tar.gz`}function $t(e,t,n){let r=F.readdirSync(t,{withFileTypes:!0});for(let o of r){if(o.name==="node_modules"||o.name===".git")continue;let s=Pe.join(t,o.name),a=Pe.relative(e,s).split(Pe.sep).join("/");o.isDirectory()?(n.push(a+"/"),$t(e,s,n)):n.push(a)}}function to(e){let t=Pe.join(e,".templateignore");if(!F.existsSync(t))return;let n=F.readFileSync(t,"utf8"),r=Qr().add(n),o=[];$t(e,e,o);let s=o.map(i=>i.endsWith("/")?i.slice(0,-1):i),a=new Set(r.filter(s)),c=s.filter(i=>!a.has(i)).sort((i,d)=>d.split("/").length-i.split("/").length);for(let i of c){let d=Pe.join(e,i);try{F.existsSync(d)&&F.rmSync(d,{recursive:!0,force:!0})}catch{}}try{F.existsSync(t)&&F.rmSync(t,{force:!0})}catch{}}function no(e,t){Jr(`set -o pipefail; curl -sSfL ${e} | tar -xz -C "${t}" --strip-components=1`,{stdio:"ignore",shell:"bash"})}async function qt(e){F.mkdirSync(e,{recursive:!0});let n=process.env.SAILOR_TEMPLATE_SOURCE==="main"?["main"]:["mirror","main"],r;for(let o of n){let s=Zr[o],a=eo(s.repo,s.ref);try{if(no(a,e),s.applyIgnore)try{to(e)}catch(c){throw new Error(`Template cloned, but .templateignore processing failed: ${c instanceof Error?c.message:String(c)}`)}return}catch(c){r=c instanceof Error?c:new Error(String(c));try{let i=F.readdirSync(e);for(let d of i)F.rmSync(Pe.join(e,d),{recursive:!0,force:!0})}catch{}}}throw new Error(`Failed to download the template from any source. Last error: ${r?.message??"unknown"}
48
1597
 
49
1598
  Ensure you have internet access and curl/tar installed.
50
- You can override the source with SAILOR_TEMPLATE_REPO / SAILOR_TEMPLATE_REF env vars.`)}import X from"fs";import st from"path";async function he(e,t){let n=st.join(e,"package.json");if(X.existsSync(n)){let s=X.readFileSync(n,"utf8"),r=JSON.parse(s);r.name=t,r.version="0.1.0",X.writeFileSync(n,JSON.stringify(r,null,2)+`
51
- `)}}import R from"fs";import U from"path";var Pe=new Set(["siliconflow","volcengine-ark","bailian","moonshot"]),Z=/@sailor:([^\s*]+)/;function rt(e,t){let{providerIds:n,customEndpoint:s}=t;if(e.startsWith("provider:")){let r=e.slice(9);return{marker:e,keep:n.includes(r)}}if(e==="cn-compatible"){let r=n.some(i=>Pe.has(i));return{marker:e,keep:r||!!s}}if(e.startsWith("cn-instance:")){let r=e.slice(12);return{marker:e,keep:n.includes(r)}}return e==="custom-endpoint"?{marker:e,keep:!!s}:e==="registry-entries"?{marker:e,keep:!0}:{marker:e,keep:!0}}function Ee(e){return e.replace(/\s*\/\/\s*@sailor:[^\n]*$/,"")}function be(e){return e.replace(/\s*#\s*@sailor:[^\n]*$/,"")}function xe(e,t){let n=e.split(`
52
- `),s=[],r=e.includes("#"),i=0;for(;i<n.length;){let l=n[i],c=l.match(Z);if(!c){s.push(l),i++;continue}let{marker:a,keep:d}=rt(c[1],t);if(a==="registry-entries"){s.push(r?be(l):Ee(l)),i++;continue}if(d){let m=r?be(l):Ee(l);m.trim().length>0&&s.push(m);let E=i+1;for(;E<n.length&&n[E].trim()!==""&&!Z.test(n[E]);)s.push(n[E]),E++;E<n.length&&n[E].trim()===""&&(s.push(n[E]),E++),i=E}else{let m=i+1;for(;m<n.length&&n[m].trim()!==""&&!Z.test(n[m]);)m++;m<n.length&&n[m].trim()===""&&m++,i=m}}return ot(s.join(`
53
- `),t)}function ot(e,t){let{providerIds:n,customEndpoint:s}=t,r=new Set;for(let d of n)["openai","anthropic","google","mistral","xai"].includes(d)&&r.add(d),r.add(d.replace(/-/g,"_")),r.add(d);s&&r.add("custom");let i=/createProviderRegistry\(\{([\s\S]*?)\}\)/,l=e.match(i);if(!l)return e;let a=l[1].split(`
54
- `).filter(d=>{let m=d.trim();if(m===""||m.startsWith("//"))return!0;let E=m.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*,?$/);return E?r.has(E[1]):!0}).join(`
55
- `);return e.replace(i,`createProviderRegistry({${a}})`)}function at(e,t){let n=U.join(t,"registry.ts.template"),s=R.readFileSync(n,"utf-8");return xe(s,e)}function it(e,t){let n=U.join(t,".env.example.template"),s=R.readFileSync(n,"utf-8");return xe(s,e)}async function Se(e,t,n){let s=at(t,n),r=it(t,n),i=U.join(e,"packages/ai-sdk/src");await R.promises.mkdir(i,{recursive:!0}),await R.promises.writeFile(U.join(i,"registry.ts"),s),await R.promises.appendFile(U.join(e,".env.example"),`
56
- `+r);let l={ai:"latest"},{providerIds:c,customEndpoint:a}=t;(c.includes("openai")||a||c.some(m=>Pe.has(m)))&&(l["@ai-sdk/openai"]="latest",l["@ai-sdk/openai-compatible"]="latest"),c.includes("anthropic")&&(l["@ai-sdk/anthropic"]="latest"),c.includes("google")&&(l["@ai-sdk/google"]="latest"),c.includes("mistral")&&(l["@ai-sdk/mistral"]="latest"),c.includes("xai")&&(l["@ai-sdk/openai"]="latest");let d=U.join(e,"package.json");if(R.existsSync(d)){let m=JSON.parse(await R.promises.readFile(d,"utf-8"));m.dependencies||(m.dependencies={});for(let[E,N]of Object.entries(l))m.dependencies[E]=N;await R.promises.writeFile(d,JSON.stringify(m,null,2)+`
57
- `)}}import p from"fs";import v from"path";function Ae(e,t){if(!p.existsSync(e))return;let n=p.readdirSync(e);for(let s of n){let r=v.join(e,s);p.statSync(r).isDirectory()?Ae(r,t):t(r)}}async function we(e,t){let n=v.join(e,"package.json");if(!p.existsSync(n))return;let s=JSON.parse(p.readFileSync(n,"utf8"));if(t.orm!=="prisma"){let r=v.join(e,"packages","db");if(p.existsSync(r)){let i=v.join(r,"prisma");p.existsSync(i)&&p.rmSync(i,{recursive:!0,force:!0});let l=v.join(r,"package.json");if(p.existsSync(l)){let a=JSON.parse(p.readFileSync(l,"utf8"));a.dependencies&&delete a.dependencies["@prisma/client"],a.devDependencies&&delete a.devDependencies.prisma,p.writeFileSync(l,JSON.stringify(a,null,2)+`
58
- `)}let c=v.join(r,"src","index.ts");p.existsSync(c)&&p.writeFileSync(c,`
1599
+ You can override the source with SAILOR_TEMPLATE_REPO / SAILOR_TEMPLATE_REF env vars.`)}import be from"fs";import C from"path";var Gt=[{id:"sentry",name:"Sentry",region:"both",envVars:["SENTRY_DSN","NEXT_PUBLIC_SENTRY_DSN"],docs:"https://docs.sentry.io",pkg:"@sentry/nextjs",instrumentationFile:"apps/web/instrumentation.ts"},{id:"datadog",name:"Datadog",region:"global",envVars:["DD_API_KEY","DD_APP_KEY","DD_SITE"],docs:"https://docs.datadoghq.com",pkg:"dd-trace",instrumentationFile:"apps/web/instrumentation.ts"},{id:"bugsnag",name:"Bugsnag",region:"global",envVars:["BUGSNAG_API_KEY","NEXT_PUBLIC_BUGSNAG_API_KEY"],docs:"https://docs.bugsnag.com",pkg:"@bugsnag/js",instrumentationFile:"apps/web/src/lib/monitoring/bugsnag.ts"},{id:"aliyun-arms",name:"\u963F\u91CC\u4E91 ARMS",region:"cn",envVars:["ALIYUN_ARMS_LICENSE_KEY","ALIYUN_ARMS_APP_NAME","NEXT_PUBLIC_ARMS_PID"],docs:"https://help.aliyun.com/product/34364.html",instrumentationFile:"apps/web/src/components/monitoring/ArmsScript.tsx"},{id:"tingyun",name:"\u542C\u4E91 TingYun",region:"cn",envVars:["TINGYUN_LICENSE_KEY","NEXT_PUBLIC_TINGYUN_APP_KEY"],docs:"https://www.tingyun.com/docs",instrumentationFile:"apps/web/src/components/monitoring/TingYunScript.tsx"},{id:"none",name:"None",region:"both",envVars:[],docs:""}],yi=Gt.reduce((e,t)=>(e[t.region]||(e[t.region]=[]),e[t.region].push(t),e),{});function Wt(e){return Gt.find(t=>t.id===e)}function ro(e){be.mkdirSync(e,{recursive:!0})}function B(e,t){ro(C.dirname(e)),be.writeFileSync(e,t)}function Me(e,t){let n=C.join(e,".env.example");be.existsSync(n)?be.appendFileSync(n,`
1600
+ `+t):be.writeFileSync(n,t)}function oo(e){let t=`import * as Sentry from "@sentry/nextjs";
1601
+
1602
+ Sentry.init({
1603
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
1604
+ tracesSampleRate: 1.0,
1605
+ replaysOnErrorSampleRate: 1.0,
1606
+ replaysSessionSampleRate: 0.1,
1607
+ integrations: [
1608
+ Sentry.replayIntegration({
1609
+ maskAllText: true,
1610
+ blockAllMedia: true,
1611
+ }),
1612
+ ],
1613
+ environment: process.env.NODE_ENV,
1614
+ });
1615
+ `,n=`import * as Sentry from "@sentry/nextjs";
1616
+
1617
+ Sentry.init({
1618
+ dsn: process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN,
1619
+ tracesSampleRate: 1.0,
1620
+ environment: process.env.NODE_ENV,
1621
+ });
1622
+ `,r=`import * as Sentry from "@sentry/nextjs";
1623
+
1624
+ Sentry.init({
1625
+ dsn: process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN,
1626
+ tracesSampleRate: 1.0,
1627
+ environment: process.env.NODE_ENV,
1628
+ });
1629
+ `,o=`/**
1630
+ * Next.js instrumentation hook \u2014 wires Sentry into Node and Edge runtimes.
1631
+ * See https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
1632
+ */
1633
+ export async function register() {
1634
+ if (process.env.NEXT_RUNTIME === "nodejs") {
1635
+ await import("./sentry.server.config");
1636
+ }
1637
+ if (process.env.NEXT_RUNTIME === "edge") {
1638
+ await import("./sentry.edge.config");
1639
+ }
1640
+ }
1641
+
1642
+ export { captureRequestError as onRequestError } from "@sentry/nextjs";
1643
+ `;B(C.join(e,"sentry.client.config.ts"),t),B(C.join(e,"sentry.server.config.ts"),n),B(C.join(e,"sentry.edge.config.ts"),r),B(C.join(e,"instrumentation.ts"),o)}function so(){return`# Sentry \u2014 error tracking + performance monitoring
1644
+ # Get your DSN from https://sentry.io/settings/projects/<project>/keys/
1645
+ SENTRY_DSN=
1646
+ NEXT_PUBLIC_SENTRY_DSN=
1647
+ SENTRY_ORG=
1648
+ SENTRY_PROJECT=
1649
+ SENTRY_AUTH_TOKEN=
1650
+ `}function io(e){let t=`/**
1651
+ * Next.js instrumentation hook \u2014 wires Datadog APM (dd-trace) into Node runtime.
1652
+ * Edge runtime is not supported by dd-trace; only nodejs is initialized.
1653
+ */
1654
+ export async function register() {
1655
+ if (process.env.NEXT_RUNTIME === "nodejs") {
1656
+ const tracer = (await import("dd-trace")).default;
1657
+ tracer.init({
1658
+ service: process.env.DD_SERVICE ?? "sailor-web",
1659
+ env: process.env.DD_ENV ?? process.env.NODE_ENV,
1660
+ version: process.env.DD_VERSION,
1661
+ logInjection: true,
1662
+ runtimeMetrics: true,
1663
+ profiling: true,
1664
+ });
1665
+ }
1666
+ }
1667
+ `,n=`"use client";
1668
+ /**
1669
+ * Datadog RUM (Real User Monitoring) \u2014 browser-side tracking.
1670
+ * Initialize once in your root layout via <DatadogRumProvider />.
1671
+ */
1672
+ import { datadogRum } from "@datadog/browser-rum";
1673
+
1674
+ let initialized = false;
1675
+
1676
+ export function initDatadogRum(): void {
1677
+ if (initialized || typeof window === "undefined") return;
1678
+ const applicationId = process.env.NEXT_PUBLIC_DD_APPLICATION_ID;
1679
+ const clientToken = process.env.NEXT_PUBLIC_DD_CLIENT_TOKEN;
1680
+ if (!applicationId || !clientToken) return;
1681
+
1682
+ datadogRum.init({
1683
+ applicationId,
1684
+ clientToken,
1685
+ site: process.env.NEXT_PUBLIC_DD_SITE ?? "datadoghq.com",
1686
+ service: process.env.NEXT_PUBLIC_DD_SERVICE ?? "sailor-web",
1687
+ env: process.env.NEXT_PUBLIC_DD_ENV ?? "production",
1688
+ version: process.env.NEXT_PUBLIC_DD_VERSION,
1689
+ sessionSampleRate: 100,
1690
+ sessionReplaySampleRate: 20,
1691
+ trackUserInteractions: true,
1692
+ trackResources: true,
1693
+ trackLongTasks: true,
1694
+ defaultPrivacyLevel: "mask-user-input",
1695
+ });
1696
+ initialized = true;
1697
+ }
1698
+ `;B(C.join(e,"instrumentation.ts"),t),B(C.join(e,"src","lib","monitoring","datadog-rum.ts"),n)}function ao(){return`# Datadog \u2014 APM + RUM
1699
+ # Server APM (dd-trace)
1700
+ DD_API_KEY=
1701
+ DD_APP_KEY=
1702
+ DD_SITE=datadoghq.com
1703
+ DD_SERVICE=sailor-web
1704
+ DD_ENV=production
1705
+ DD_VERSION=
1706
+ # Browser RUM
1707
+ NEXT_PUBLIC_DD_APPLICATION_ID=
1708
+ NEXT_PUBLIC_DD_CLIENT_TOKEN=
1709
+ NEXT_PUBLIC_DD_SITE=datadoghq.com
1710
+ NEXT_PUBLIC_DD_SERVICE=sailor-web
1711
+ NEXT_PUBLIC_DD_ENV=production
1712
+ NEXT_PUBLIC_DD_VERSION=
1713
+ `}function co(e){let t=`/**
1714
+ * Bugsnag server-side initialization.
1715
+ * Import this once at the top of your server entrypoint or a shared init module.
1716
+ */
1717
+ import Bugsnag from "@bugsnag/js";
1718
+
1719
+ let started = false;
1720
+
1721
+ export function startBugsnagServer(): void {
1722
+ if (started) return;
1723
+ const apiKey = process.env.BUGSNAG_API_KEY;
1724
+ if (!apiKey) return;
1725
+
1726
+ Bugsnag.start({
1727
+ apiKey,
1728
+ releaseStage: process.env.NODE_ENV,
1729
+ appVersion: process.env.APP_VERSION,
1730
+ });
1731
+ started = true;
1732
+ }
1733
+ `,n=`"use client";
1734
+ /**
1735
+ * Bugsnag browser initialization \u2014 wrap your app root with the returned provider.
1736
+ */
1737
+ import Bugsnag from "@bugsnag/js";
1738
+ import BugsnagPluginReact from "@bugsnag/plugin-react";
1739
+ import React from "react";
1740
+
1741
+ let errorBoundary: React.ComponentType<{
1742
+ children: React.ReactNode;
1743
+ FallbackComponent?: React.ComponentType<{ error: Error }>;
1744
+ }> | null = null;
1745
+
1746
+ export function getBugsnagErrorBoundary() {
1747
+ if (errorBoundary) return errorBoundary;
1748
+ const apiKey = process.env.NEXT_PUBLIC_BUGSNAG_API_KEY;
1749
+ if (!apiKey || typeof window === "undefined") return null;
1750
+
1751
+ Bugsnag.start({
1752
+ apiKey,
1753
+ plugins: [new BugsnagPluginReact()],
1754
+ releaseStage: process.env.NODE_ENV,
1755
+ });
1756
+
1757
+ const plugin = Bugsnag.getPlugin("react");
1758
+ if (!plugin) return null;
1759
+ errorBoundary = plugin.createErrorBoundary(React) as typeof errorBoundary;
1760
+ return errorBoundary;
1761
+ }
1762
+ `;B(C.join(e,"src","lib","monitoring","bugsnag.server.ts"),t),B(C.join(e,"src","lib","monitoring","bugsnag.client.tsx"),n)}function po(){return`# Bugsnag \u2014 error tracking
1763
+ BUGSNAG_API_KEY=
1764
+ NEXT_PUBLIC_BUGSNAG_API_KEY=
1765
+ APP_VERSION=
1766
+ `}function lo(e){B(C.join(e,"src","components","monitoring","ArmsScript.tsx"),`"use client";
1767
+ /**
1768
+ * \u963F\u91CC\u4E91 ARMS (Application Real-time Monitoring Service) browser agent.
1769
+ * Injects the official JS agent via next/script.
1770
+ * Docs: https://help.aliyun.com/document_detail/58655.html
1771
+ */
1772
+ import Script from "next/script";
1773
+
1774
+ export function ArmsScript() {
1775
+ const pid = process.env.NEXT_PUBLIC_ARMS_PID;
1776
+ if (!pid) return null;
1777
+ return (
1778
+ <Script
1779
+ id="aliyun-arms"
1780
+ strategy="afterInteractive"
1781
+ dangerouslySetInnerHTML={{
1782
+ __html: \`!(function(c,b,d,a){c[a]||(c[a]={});c[a].config={pid:"\${pid}",imgUrl:"https://arms-retcode.aliyuncs.com/r.png?",sendResource:true,enableLinkTrace:true,behavior:true};with(b)with(body)with(insertBefore(createElement("script"),firstChild))setAttribute("crossorigin","",src=d)})(window,document,"https://retcode.alicdn.com/retcode/bl.js","__bl");\`,
1783
+ }}
1784
+ />
1785
+ );
1786
+ }
1787
+ `)}function uo(){return`# \u963F\u91CC\u4E91 ARMS \u2014 APM (\u4E2D\u56FD\u533A)
1788
+ ALIYUN_ARMS_LICENSE_KEY=
1789
+ ALIYUN_ARMS_APP_NAME=
1790
+ NEXT_PUBLIC_ARMS_PID=
1791
+ `}function mo(e){B(C.join(e,"src","components","monitoring","TingYunScript.tsx"),`"use client";
1792
+ /**
1793
+ * \u542C\u4E91 Browser Agent (\u524D\u7AEF\u6027\u80FD\u76D1\u63A7).
1794
+ * Docs: https://www.tingyun.com/docs
1795
+ */
1796
+ import Script from "next/script";
1797
+
1798
+ export function TingYunScript() {
1799
+ const appKey = process.env.NEXT_PUBLIC_TINGYUN_APP_KEY;
1800
+ if (!appKey) return null;
1801
+ return (
1802
+ <Script
1803
+ id="tingyun-browser"
1804
+ strategy="afterInteractive"
1805
+ src={\`https://rs.tingyun.com/rum/\${appKey}.js\`}
1806
+ />
1807
+ );
1808
+ }
1809
+ `)}function go(){return`# \u542C\u4E91 TingYun \u2014 APM (\u4E2D\u56FD\u533A)
1810
+ TINGYUN_LICENSE_KEY=
1811
+ NEXT_PUBLIC_TINGYUN_APP_KEY=
1812
+ `}async function Xt(e,t,n="global"){let r=Wt(t);if(!r||r.id==="none")return;let o=C.join(e,"apps","web");if(!be.existsSync(o))return;let s=r.id;try{switch(s){case"sentry":oo(o),Me(e,so());return;case"datadog":io(o),Me(e,ao());return;case"bugsnag":co(o),Me(e,po());return;case"aliyun-arms":lo(o),Me(e,uo());return;case"tingyun":mo(o),Me(e,go());return;default:return}}catch(a){throw console.error(`Failed to apply monitoring selection "${t}":`,a),new Error(`Monitoring scaffold for "${t}" failed \u2014 see stderr for details.`)}}import st from"fs";import fo from"path";async function Ht(e,t){let n=fo.join(e,"package.json");if(st.existsSync(n)){let r=st.readFileSync(n,"utf8"),o=JSON.parse(r);o.name=t,o.version="0.1.0",st.writeFileSync(n,JSON.stringify(o,null,2)+`
1813
+ `)}}import K from"fs";import je from"path";var yo=["stripe","lemonsqueezy","chinapay","polar"];function zt(e){K.existsSync(e)&&K.rmSync(e,{recursive:!0,force:!0})}function So(e){switch(e){case"stripe":return["stripe"];case"lemon":return["lemonsqueezy"];case"wechat":case"alipay":return["chinapay"]}}function Eo(e,t){let n=je.join(e,".env.example");K.existsSync(n)?K.appendFileSync(n,`
1814
+ `+t):K.writeFileSync(n,t)}function ho(e){switch(e){case"stripe":return`# Stripe
1815
+ STRIPE_SECRET_KEY=
1816
+ STRIPE_WEBHOOK_SECRET=
1817
+ `;case"lemon":return`# Lemon Squeezy
1818
+ LEMONSQUEEZY_API_KEY=
1819
+ LEMONSQUEEZY_STORE_ID=
1820
+ LEMONSQUEEZY_WEBHOOK_SECRET=
1821
+ `;case"wechat":return`# WeChat Pay (via ChinaPay aggregator)
1822
+ CHINAPAY_APP_ID=
1823
+ CHINAPAY_APP_SECRET=
1824
+ CHINAPAY_WECHAT_ENABLED=true
1825
+ `;case"alipay":return`# Alipay (via ChinaPay aggregator)
1826
+ CHINAPAY_APP_ID=
1827
+ CHINAPAY_APP_SECRET=
1828
+ CHINAPAY_ALIPAY_ENABLED=true
1829
+ `}}async function Jt(e,t){let n=je.join(e,"packages","billing");if(!K.existsSync(n))return;if(t==="none"){zt(n);return}let r=je.join(n,"src"),o=new Set(So(t));for(let a of yo)o.has(a)||zt(je.join(r,a));let s=je.join(r,"checkout","types.ts");if(K.existsSync(s)){let a=t==="stripe"?"stripe":t==="lemon"?"lemonsqueezy":"chinapay",c=K.readFileSync(s,"utf8"),i=c.replace(/export type CheckoutProviderType\s*=\s*[^;]+;/,`export type CheckoutProviderType = "${a}";`);i!==c&&K.writeFileSync(s,i)}Eo(e,ho(t))}import W from"fs";import Ae from"path";var en=new Set(["siliconflow","volcengine-ark","bailian","moonshot"]),it=/@sailor:([^\s*]+)/;function _o(e,t){let{providerIds:n,customEndpoint:r}=t;if(e.startsWith("provider:")){let o=e.slice(9);return{marker:e,keep:n.includes(o)}}if(e==="cn-compatible"){let o=n.some(s=>en.has(s));return{marker:e,keep:o||!!r}}if(e.startsWith("cn-instance:")){let o=e.slice(12);return{marker:e,keep:n.includes(o)}}return e==="custom-endpoint"?{marker:e,keep:!!r}:e==="registry-entries"?{marker:e,keep:!0}:{marker:e,keep:!0}}function Qt(e){return e.replace(/\s*\/\/\s*@sailor:[^\n]*$/,"")}function Zt(e){return e.replace(/\s*#\s*@sailor:[^\n]*$/,"")}function tn(e,t){let n=e.split(`
1830
+ `),r=[],o=e.includes("#"),s=0;for(;s<n.length;){let a=n[s],c=a.match(it);if(!c){r.push(a),s++;continue}let{marker:i,keep:d}=_o(c[1],t);if(i==="registry-entries"){r.push(o?Zt(a):Qt(a)),s++;continue}if(d){let m=o?Zt(a):Qt(a);m.trim().length>0&&r.push(m);let S=s+1;for(;S<n.length&&n[S].trim()!==""&&!it.test(n[S]);)r.push(n[S]),S++;S<n.length&&n[S].trim()===""&&(r.push(n[S]),S++),s=S}else{let m=s+1;for(;m<n.length&&n[m].trim()!==""&&!it.test(n[m]);)m++;m<n.length&&n[m].trim()===""&&m++,s=m}}return vo(r.join(`
1831
+ `),t)}function vo(e,t){let{providerIds:n,customEndpoint:r}=t,o=new Set;for(let d of n)["openai","anthropic","google","mistral","xai"].includes(d)&&o.add(d),o.add(d.replace(/-/g,"_")),o.add(d);r&&o.add("custom");let s=/createProviderRegistry\(\{([\s\S]*?)\}\)/,a=e.match(s);if(!a)return e;let i=a[1].split(`
1832
+ `).filter(d=>{let m=d.trim();if(m===""||m.startsWith("//"))return!0;let S=m.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*,?$/);return S?o.has(S[1]):!0}).join(`
1833
+ `);return e.replace(s,`createProviderRegistry({${i}})`)}function Io(e,t){let n=Ae.join(t,"registry.ts.template"),r=W.readFileSync(n,"utf-8");return tn(r,e)}function Po(e,t){let n=Ae.join(t,".env.example.template"),r=W.readFileSync(n,"utf-8");return tn(r,e)}async function nn(e,t,n){let r=Io(t,n),o=Po(t,n),s=Ae.join(e,"packages/ai-sdk/src");await W.promises.mkdir(s,{recursive:!0}),await W.promises.writeFile(Ae.join(s,"registry.ts"),r),await W.promises.appendFile(Ae.join(e,".env.example"),`
1834
+ `+o);let a={ai:"latest"},{providerIds:c,customEndpoint:i}=t;(c.includes("openai")||i||c.some(m=>en.has(m)))&&(a["@ai-sdk/openai"]="latest",a["@ai-sdk/openai-compatible"]="latest"),c.includes("anthropic")&&(a["@ai-sdk/anthropic"]="latest"),c.includes("google")&&(a["@ai-sdk/google"]="latest"),c.includes("mistral")&&(a["@ai-sdk/mistral"]="latest"),c.includes("xai")&&(a["@ai-sdk/openai"]="latest");let d=Ae.join(e,"package.json");if(W.existsSync(d)){let m=JSON.parse(await W.promises.readFile(d,"utf-8"));m.dependencies||(m.dependencies={});for(let[S,me]of Object.entries(a))m.dependencies[S]=me;await W.promises.writeFile(d,JSON.stringify(m,null,2)+`
1835
+ `)}}import u from"fs";import E from"path";function rn(e,t){if(!u.existsSync(e))return;let n=u.readdirSync(e);for(let r of n){let o=E.join(e,r);u.statSync(o).isDirectory()?rn(o,t):t(o)}}async function on(e,t){let n=E.join(e,"package.json");if(!u.existsSync(n))return;let r=JSON.parse(u.readFileSync(n,"utf8"));if(t.orm!=="prisma"){let o=E.join(e,"packages","db");if(u.existsSync(o)){let s=E.join(o,"prisma");u.existsSync(s)&&u.rmSync(s,{recursive:!0,force:!0});let a=E.join(o,"package.json");if(u.existsSync(a)){let i=JSON.parse(u.readFileSync(a,"utf8"));i.dependencies&&delete i.dependencies["@prisma/client"],i.devDependencies&&delete i.devDependencies.prisma,u.writeFileSync(a,JSON.stringify(i,null,2)+`
1836
+ `)}let c=E.join(o,"src","index.ts");u.existsSync(c)&&u.writeFileSync(c,`
59
1837
  export const prisma = {} as any;
60
1838
  export const db = {} as any;
61
1839
  export type PrismaClient = any;
@@ -101,7 +1879,7 @@ export type UsageAggregate = any;
101
1879
  export type UsageLedgerEntry = any;
102
1880
  export type UsageType = any;
103
1881
  export type UsageLedgerSource = any;
104
- `)}s.scripts?.postinstall&&(s.scripts.postinstall=s.scripts.postinstall.replace(/prisma generate/g,'echo "ORM Disabled"'))}if(!t.i18n){let r=v.join(e,"apps");if(p.existsSync(r)){let i=p.readdirSync(r);for(let l of i){let c=v.join(r,l);if(!p.statSync(c).isDirectory())continue;let a=v.join(c,"src","app");if(!p.existsSync(a))continue;["[lang]","[locale]"].forEach(y=>{let o=v.join(a,y);if(p.existsSync(o)){let b=p.readdirSync(o);for(let L of b)p.renameSync(v.join(o,L),v.join(a,L));p.rmdirSync(o)}}),["src/middleware.ts","middleware.ts"].forEach(y=>{let o=v.join(c,y);p.existsSync(o)&&p.rmSync(o)});let d=v.join(c,"src","lib");p.existsSync(d)||p.mkdirSync(d,{recursive:!0});let m=v.join(d,"mock-i18n.ts");p.writeFileSync(m,`
1882
+ `)}r.scripts?.postinstall&&(r.scripts.postinstall=r.scripts.postinstall.replace(/prisma generate/g,'echo "ORM Disabled"'))}if(!t.i18n){let o=E.join(e,"apps");if(u.existsSync(o)){let s=u.readdirSync(o);for(let a of s){let c=E.join(o,a);if(!u.statSync(c).isDirectory())continue;let i=E.join(c,"src","app");if(!u.existsSync(i))continue;["[lang]","[locale]"].forEach(f=>{let p=E.join(i,f);if(u.existsSync(p)){let b=u.readdirSync(p);for(let Fe of b)u.renameSync(E.join(p,Fe),E.join(i,Fe));u.rmdirSync(p)}}),["src/middleware.ts","middleware.ts"].forEach(f=>{let p=E.join(c,f);u.existsSync(p)&&u.rmSync(p)});let d=E.join(c,"src","lib");u.existsSync(d)||u.mkdirSync(d,{recursive:!0});let m=E.join(d,"mock-i18n.ts");u.writeFileSync(m,`
105
1883
  import React from 'react';
106
1884
  export function useTranslations(namespace?: string): any { return (key: string) => key; }
107
1885
  export function useLocale() { return 'en'; }
@@ -119,7 +1897,7 @@ export const useRouter = () => ({ push: (...args: any[]) => {}, replace: (...arg
119
1897
  export default function createMiddleware(...args: any[]) { return (req: any) => req; }
120
1898
  export function createNavigation(...args: any[]) { return { Link, redirect, usePathname, useRouter, getPathname: () => '/' }; }
121
1899
  export function defineRouting<T>(config: T): T { return config; }
122
- `);let N=v.join(d,"mock-i18n.d.ts");p.writeFileSync(N,`declare module '@/lib/mock-i18n' {
1900
+ `);let me=E.join(d,"mock-i18n.d.ts");u.writeFileSync(me,`declare module '@/lib/mock-i18n' {
123
1901
  export function useTranslations(namespace?: string): any;
124
1902
  export function useLocale(): string;
125
1903
  export const NextIntlClientProvider: React.FC<any>;
@@ -137,27 +1915,969 @@ export function defineRouting<T>(config: T): T { return config; }
137
1915
  export function createNavigation(...args: any[]): any;
138
1916
  export function defineRouting<T>(config: T): T;
139
1917
  }
140
- `),p.writeFileSync(v.join(d,"mock-i18n-index.ts"),`
1918
+ `),u.writeFileSync(E.join(d,"mock-i18n-index.ts"),`
141
1919
  export * from './mock-i18n';
142
1920
  export { default } from './mock-i18n';
143
- `);let C=v.join(c,"tsconfig.json");if(p.existsSync(C))try{let y=p.readFileSync(C,"utf8");if(y.includes('"paths": {')){let o=y.replace(/"paths":\s*\{/,`"paths": {
1921
+ `);let ge=E.join(c,"tsconfig.json");if(u.existsSync(ge))try{let f=u.readFileSync(ge,"utf8");if(f.includes('"paths": {')){let p=f.replace(/"paths":\s*\{/,`"paths": {
144
1922
  "next-intl": ["./src/lib/mock-i18n"],
145
- "next-intl/*": ["./src/lib/mock-i18n"],`);p.writeFileSync(C,o)}}catch{}let F=v.join(c,"src");p.existsSync(F)&&Ae(F,y=>{if(!y.endsWith(".ts")&&!y.endsWith(".tsx"))return;let o=p.readFileSync(y,"utf8"),b=!1;y.endsWith("src/app/layout.tsx")&&o.includes("../providers")&&(o=o.replace(/from\s+['"]\.\.\/providers['"]/g,"from './providers'"),b=!0),o=o.replace(/params:\s*Promise<\{[^}]*(lang|locale)[^}]*\}>/g,"params: any"),o=o.replace(/\(\{\s*params\s*\}\s*:\s*\{\s*params:\s*any\s*\}\)/g,"({ params }: any)"),o.match(/locale\s*:\s*any/)&&(o=o.replace(/locale\s*:\s*any/g,"locale: string"),b=!0),o.match(/lang\s*:\s*any/)&&(o=o.replace(/lang\s*:\s*any/g,"lang: string"),b=!0),o.match(/requestLocale\s*:\s*any/)&&(o=o.replace(/requestLocale\s*:\s*any/g,"requestLocale: string"),b=!0),o.match(/\(locale\s*=\s*(.*)\)/)&&(o=o.replace(/\(locale\s*=\s*(.*)\)/g,"() "),b=!0),o.match(/export\s+default\s+async\s+function\s+LangLayout/)&&(o=o.replace(/export\s+default\s+async\s+function\s+LangLayout\s*\(\s*\{\s*children\s*,\s*params\s*\}\s*:\s*LangLayoutProps\s*\)/g,"export default async function LangLayout({ children }: { children: React.ReactNode })"),b=!0),(y.endsWith("src/app/layout.tsx")||y.endsWith("src/app/sitemap.ts")||y.endsWith("src/app/robots.ts")||y.endsWith("src/components/ui/locale-switcher.tsx")||y.endsWith("src/app/(marketing)/blog/[slug]/page.tsx"))&&(o=o.replace(/const\s+\{\s*lang\s*\}\s*=\s*(await\s+)?params;?/g,'const lang = "en";'),o=o.replace(/const\s+\{\s*locale\s*\}\s*=\s*(await\s+)?params;?/g,'const locale = "en";'),o=o.replace(/const\s+locale\s*=\s*lang\s+as\s+Locale;?/g,'const locale = "en";'),o=o.replace(/\(l\)\s*=>/g,"(l: any) =>"),b=!0),y.endsWith("src/i18n/request.ts")&&(o=o.replace(/async\s*\(\{\s*requestLocale\s*\}\)/g,"async ({ requestLocale }: any)"),b=!0),(o.match(/locale\s*:/)||o.match(/lang\s*:/))&&(b=!0),b&&p.writeFileSync(y,o)});let T=v.join(c,"package.json");if(p.existsSync(T)){let y=JSON.parse(p.readFileSync(T,"utf8"));y.dependencies&&delete y.dependencies["next-intl"],p.writeFileSync(T,JSON.stringify(y,null,2)+`
146
- `)}}}}p.writeFileSync(n,JSON.stringify(s,null,2)+`
147
- `)}var ee="1.0.0",Ie="create-sailor";function g(e,t){e&&process.stdout.write(JSON.stringify(t)+`
148
- `)}function dt(){let e=process.env.npm_config_user_agent??"";return e.startsWith("pnpm")?"pnpm":e.startsWith("yarn")?"yarn":e.startsWith("bun")?"bun":"npm"}function ke(e){switch(e){case"postgres":case"postgresql":return"postgresql";case"mysql":return"mysql";case"sqlite":return"sqlite";case"none":return"none";default:return"postgresql"}}function _e(e){return e==="drizzle"?"drizzle":e==="none"?"none":"prisma"}function Re(e){return e==="lemon"||e==="lemonsqueezy"?"lemonsqueezy":e==="none"?"none":"stripe"}function Le(e){if(!e)return["openai"];let t=e.split(",").map(n=>n.trim().toLowerCase());return t.includes("none")?[]:t}var Oe={mintlify:"Mintlify",docusaurus:"Docusaurus",nextra:"Nextra",vitepress:"VitePress"};function Ne(e,t){if(!e)return"fumadocs";let n=e.toLowerCase();if(n==="fumadocs"||n==="none")return n;if(n in Oe){let s=Oe[n];return t?g(!0,{event:"notice",kind:"docs-fallback",requested:n,effective:"fumadocs"}):process.stdout.write(I.yellow(`\u26A0 ${s} support is coming in v1.2. Falling back to fumadocs.
149
- `)+I.dim(` Track progress: https://github.com/Nebutra/Nebutra-Sailor/issues
150
- `)),"fumadocs"}return"fumadocs"}function Ce(e){switch(e){case"vercel":return"vercel";case"railway":return"railway";case"cloudflare":return"cloudflare";case"selfhost":return"selfhost";case"none":return"none";default:return"vercel"}}async function ut(){let e=new pt;e.name(Ie).description("Nebutra-Sailor \u2014 AI-Native SaaS template").version(ee,"-v, --version").helpOption(!1).argument("[name]","project directory",void 0).option("-p, --pm <id>","npm | pnpm | yarn | bun").option("--orm <id>","prisma | drizzle | none").option("--db <id>","postgres | mysql | sqlite | none").option("--auth <id>","clerk | betterauth | none").option("--payment <id>","stripe | lemon | wechat | alipay | none").option("--ai <ids>","comma-separated provider ids").option("--deploy <target>","vercel | railway | cloudflare | selfhost").option("--docs <id>","fumadocs | mintlify | docusaurus | nextra | vitepress | none").option("--i18n","enable i18n").option("--no-i18n","disable i18n").option("--no-install","skip package install").option("--no-git","skip git init").option("-y, --yes","accept all defaults (non-interactive)").option("--dry-run","preview actions without writing files").option("--json","machine-readable output").option("--no-color","disable color output").option("-h, --help","show help"),e.parse(process.argv);let t=e.opts(),[n]=e.args;t.help&&(oe(),process.exit(0));let s=!!t.json,r=!!t.dryRun,i=!!t.yes,l=i||!process.stdin.isTTY;s||se(),g(s,{event:"start",version:ee});let c=n??(i?"./my-saas-app":void 0),a;if(c)a=c;else if(l)a="./my-saas-app";else{let u=await h.group({name:()=>h.text({message:"Where should we create your project?",placeholder:"./my-saas-app",defaultValue:"./my-saas-app",validate:f=>{if(f.length===0)return"Please enter a path."}})},{onCancel:()=>{process.stdout.write(I.red(`\u2718 Cancelled
151
- `)),process.exit(130)}});a=String(u.name)}let d=Q.basename(Q.resolve(a)),m=t.pm??dt(),E=!!t.orm,N=!!t.db,C=!!t.payment,F=!!t.ai,T=!!t.deploy,y=!!t.docs,o=t.i18n!==void 0,b,L,M,V,H,j,S,K;if(l)b=_e(t.orm),L=ke(t.db),M=Re(t.payment),V=Le(t.ai),j=Ce(t.deploy),S=Ne(t.docs,s),K=o?!!t.i18n:!0;else{let u={};if(E||(u.orm=()=>h.select({message:"Which ORM?",options:[{value:"prisma",label:"Prisma"},{value:"drizzle",label:"Drizzle"},{value:"none",label:"None"}],initialValue:"prisma"})),N||(u.database=()=>h.select({message:"Database?",options:[{value:"postgresql",label:"PostgreSQL"},{value:"mysql",label:"MySQL"},{value:"sqlite",label:"SQLite"},{value:"none",label:"None"}],initialValue:"postgresql"})),C||(u.payment=()=>h.select({message:"Payment?",options:[{value:"stripe",label:"Stripe"},{value:"lemonsqueezy",label:"Lemon Squeezy"},{value:"none",label:"None"}],initialValue:"stripe"})),!F){let A=Array.from(new Set(G.map(P=>P.category)));u.aiCategories=()=>h.multiselect({message:"Which AI Provider categories do you want to explore? (Select multiple)",options:A.map(P=>({value:P,label:P})),initialValues:["\u76F4\u63A5\u5B9E\u9A8C\u5BA4","\u7EDF\u4E00\u7F51\u5173"]}),u.aiProviders=({results:P})=>{let J=P.aiCategories||[],te=G.filter(D=>J.includes(D.category)).map(D=>({value:D.id,label:`[${D.category}] ${D.name}`}));return te.push({value:"custom",label:"[\u81EA\u5B9A\u4E49] Custom OpenAI-compatible endpoint"}),h.multiselect({message:"Select AI Providers (Space to select, Enter to submit)",options:te,initialValues:["openai","anthropic"],required:!1})},u.enableCustom=({results:P})=>P.aiProviders?.includes("custom")?h.confirm({message:"Configure custom OpenAI-compatible endpoint?",initialValue:!0}):Promise.resolve(!1),u.customAiName=({results:P})=>P.enableCustom?h.text({message:"Custom endpoint name (e.g. proxy, local):",defaultValue:"custom",placeholder:"custom"}):Promise.resolve(void 0),u.customAiBaseUrl=({results:P})=>P.enableCustom?h.text({message:"Custom endpoint base URL (e.g. https://api.proxy.com/v1):",validate:J=>{if(J.length===0)return"Base URL is required."}}):Promise.resolve(void 0),u.customAiApiKeyEnv=({results:P})=>P.enableCustom?h.text({message:"Environment variable name for the API Key:",defaultValue:"CUSTOM_AI_API_KEY",placeholder:"CUSTOM_AI_API_KEY"}):Promise.resolve(void 0)}T||(u.deployTarget=()=>h.select({message:"Where will you deploy?",options:[{value:"vercel",label:"Vercel"},{value:"railway",label:"Railway"},{value:"cloudflare",label:"Cloudflare Workers"},{value:"selfhost",label:"Self-host / Docker"},{value:"none",label:"None for now"}],initialValue:"vercel"})),y||(u.docs=()=>h.select({message:"What documentation framework? (affects `apps/docs`)",options:[{value:"fumadocs",label:"Fumadocs \u2014 Next.js monorepo native (Recommended)"},{value:"mintlify",label:"Mintlify \u2014 managed docs SaaS (Coming in v1.2)"},{value:"docusaurus",label:"Docusaurus \u2014 Meta's framework (Coming in v1.2)"},{value:"nextra",label:"Nextra \u2014 Next.js minimal (Coming in v1.2)"},{value:"vitepress",label:"VitePress \u2014 Vue ecosystem, separate repo (Experimental)"},{value:"none",label:"None \u2014 skip docs"}],initialValue:"fumadocs"})),o||(u.i18n=()=>h.confirm({message:"Enable i18n?",initialValue:!0}));let f=Object.keys(u).length>0?await h.group(u,{onCancel:()=>{process.stdout.write(I.red(`\u2718 Cancelled
152
- `)),process.exit(130)}}):{};b=E?_e(t.orm):f.orm,L=N?ke(t.db):f.database,M=C?Re(t.payment):f.payment,V=F?Le(t.ai):f.aiProviders?.filter(A=>A!=="custom")??[],f.enableCustom&&(H={name:f.customAiName,baseURL:f.customAiBaseUrl,apiKeyEnvName:f.customAiApiKeyEnv}),j=T?Ce(t.deploy):f.deployTarget,S=Ne(y?t.docs:f.docs,s),K=o?!!t.i18n:!!f.i18n}let $={orm:b,database:L,payment:M,aiProviders:V,customAiEndpoint:H,deployTarget:j,docs:S,i18n:K},W=[["Project name",d],["ORM",b],["Database",L],["Payment",M],["AI",Array.isArray(V)?V.join(", ")+(H?", custom":""):String(V)],["Deploy Target",j],["Docs Framework",S]];s?W.forEach(([u,f],A)=>{g(!0,{event:"step",step:u,value:f,index:A+1,total:W.length})}):W.forEach(([u,f],A)=>{ie({index:A+1,total:W.length,label:u,value:f})});let Te={databaseUrl:"postgresql://postgres:postgres@localhost:5432/nebutra",clerkPublishable:"",clerkSecret:""},Ve=Date.now();if(r){let u=[`clone template \u2192 ${a}`,"write nebutra.config.json",`prune template (orm=${b}, i18n=${K})`,...S!=="none"?[`docs \u2192 scaffold apps/docs (${S==="fumadocs"?"fumadocs":`${S} \u2192 fumadocs fallback`})`]:[],"inject .env.local",t.install===!1?"skip install":`run ${m} install`,t.git===!1?"skip git init":"run git init"];if(s)u.forEach(f=>g(!0,{event:"plan",action:f})),g(!0,{event:"done",dryRun:!0});else{process.stdout.write(`
153
- `+I.bold(`Dry run \u2014 planned actions:
154
- `));for(let f of u)process.stdout.write(` \u2022 ${f}
155
- `);process.stdout.write(I.dim(`
1923
+ "next-intl/*": ["./src/lib/mock-i18n"],`);u.writeFileSync(ge,p)}}catch{}let Ce=E.join(c,"src");u.existsSync(Ce)&&rn(Ce,f=>{if(!f.endsWith(".ts")&&!f.endsWith(".tsx"))return;let p=u.readFileSync(f,"utf8"),b=!1;f.endsWith("src/app/layout.tsx")&&p.includes("../providers")&&(p=p.replace(/from\s+['"]\.\.\/providers['"]/g,"from './providers'"),b=!0),p=p.replace(/params:\s*Promise<\{[^}]*(lang|locale)[^}]*\}>/g,"params: any"),p=p.replace(/\(\{\s*params\s*\}\s*:\s*\{\s*params:\s*any\s*\}\)/g,"({ params }: any)"),p.match(/locale\s*:\s*any/)&&(p=p.replace(/locale\s*:\s*any/g,"locale: string"),b=!0),p.match(/lang\s*:\s*any/)&&(p=p.replace(/lang\s*:\s*any/g,"lang: string"),b=!0),p.match(/requestLocale\s*:\s*any/)&&(p=p.replace(/requestLocale\s*:\s*any/g,"requestLocale: string"),b=!0),p.match(/\(locale\s*=\s*(.*)\)/)&&(p=p.replace(/\(locale\s*=\s*(.*)\)/g,"() "),b=!0),p.match(/export\s+default\s+async\s+function\s+LangLayout/)&&(p=p.replace(/export\s+default\s+async\s+function\s+LangLayout\s*\(\s*\{\s*children\s*,\s*params\s*\}\s*:\s*LangLayoutProps\s*\)/g,"export default async function LangLayout({ children }: { children: React.ReactNode })"),b=!0),(f.endsWith("src/app/layout.tsx")||f.endsWith("src/app/sitemap.ts")||f.endsWith("src/app/robots.ts")||f.endsWith("src/components/ui/locale-switcher.tsx")||f.endsWith("src/app/(marketing)/blog/[slug]/page.tsx"))&&(p=p.replace(/const\s+\{\s*lang\s*\}\s*=\s*(await\s+)?params;?/g,'const lang = "en";'),p=p.replace(/const\s+\{\s*locale\s*\}\s*=\s*(await\s+)?params;?/g,'const locale = "en";'),p=p.replace(/const\s+locale\s*=\s*lang\s+as\s+Locale;?/g,'const locale = "en";'),p=p.replace(/\(l\)\s*=>/g,"(l: any) =>"),b=!0),f.endsWith("src/i18n/request.ts")&&(p=p.replace(/async\s*\(\{\s*requestLocale\s*\}\)/g,"async ({ requestLocale }: any)"),b=!0),(p.match(/locale\s*:/)||p.match(/lang\s*:/))&&(b=!0),b&&u.writeFileSync(f,p)});let we=E.join(c,"package.json");if(u.existsSync(we)){let f=JSON.parse(u.readFileSync(we,"utf8"));f.dependencies&&delete f.dependencies["next-intl"],u.writeFileSync(we,JSON.stringify(f,null,2)+`
1924
+ `)}}}}u.writeFileSync(n,JSON.stringify(r,null,2)+`
1925
+ `)}import X from"fs";import ae from"path";function bo(e){let t=[ae.join(e,"packages","db"),ae.join(e,"apps","web")];for(let n of t){let r=ae.join(n,"prisma","schema.prisma");if(X.existsSync(r))return{packageDir:n,seedPath:ae.join(n,"prisma","seed.ts"),packageJsonPath:ae.join(n,"package.json")}}return null}function Ao(e){try{let t=X.readFileSync(e,"utf8"),n=new Set,r=/^\s*model\s+(\w+)\s*\{/gm,o=r.exec(t);for(;o;)n.add(o[1]),o=r.exec(t);return n}catch{return new Set}}function Co(e,t){return!t||e==="none"?` // Skipping admin user \u2014 auth=${JSON.stringify(e)} (or no User model).`:e==="clerk"?` try {
1926
+ const admin = await prisma.user.create({
1927
+ data: {
1928
+ clerkId: "user_mock_admin",
1929
+ email: "admin@example.com",
1930
+ name: "Admin",
1931
+ },
1932
+ });
1933
+ console.log("\\u2713 Created admin user:", admin.email);
1934
+ } catch (err) {
1935
+ console.warn("\\u26A0 Admin user creation skipped:", (err as Error).message);
1936
+ }`:e==="betterauth"?` try {
1937
+ // NOTE: in production hash passwords with the project's password hasher
1938
+ // (e.g. argon2id via @nebutra/auth). This is a scaffold placeholder.
1939
+ const admin = await prisma.user.create({
1940
+ data: {
1941
+ email: "admin@example.com",
1942
+ name: "Admin",
1943
+ hashedPassword: "CHANGE_ME_scaffold_placeholder",
1944
+ },
1945
+ });
1946
+ console.log("\\u2713 Created admin user:", admin.email);
1947
+ } catch (err) {
1948
+ console.warn("\\u26A0 Admin user creation skipped:", (err as Error).message);
1949
+ }`:` try {
1950
+ const admin = await prisma.user.create({
1951
+ data: { email: "admin@example.com", name: "Admin" },
1952
+ });
1953
+ console.log("\\u2713 Created admin user:", admin.email);
1954
+ } catch (err) {
1955
+ console.warn("\\u26A0 Admin user creation skipped:", (err as Error).message);
1956
+ }`}function wo(e,t){let n=t.has("Tenant"),r=t.has("User"),o=t.has("Project"),s=Co(e,r),a=n?` let tenants: Array<{ id: string; name: string }> = [];
1957
+ try {
1958
+ tenants = await Promise.all([
1959
+ prisma.tenant.create({
1960
+ data: { name: "Acme Corp", slug: "acme", plan: "pro" },
1961
+ }),
1962
+ prisma.tenant.create({
1963
+ data: { name: "Widget Co", slug: "widget", plan: "free" },
1964
+ }),
1965
+ prisma.tenant.create({
1966
+ data: { name: "Startup Inc", slug: "startup", plan: "startup" },
1967
+ }),
1968
+ ]);
1969
+ console.log(\`\\u2713 Created \${tenants.length} tenants\`);
1970
+ } catch (err) {
1971
+ console.warn("\\u26A0 Tenant creation skipped:", (err as Error).message);
1972
+ }`:` const tenants: Array<{ id: string; name: string }> = [];
1973
+ console.log("\\u2139 No Tenant model in schema \u2014 skipping tenant seed.");`,c=o&&n?` for (const tenant of tenants) {
1974
+ try {
1975
+ await prisma.project.createMany({
1976
+ data: [
1977
+ { name: \`\${tenant.name} - Sample Project 1\`, tenantId: tenant.id },
1978
+ { name: \`\${tenant.name} - Sample Project 2\`, tenantId: tenant.id },
1979
+ ],
1980
+ });
1981
+ } catch (err) {
1982
+ console.warn(
1983
+ \`\\u26A0 Project seed skipped for \${tenant.name}:\`,
1984
+ (err as Error).message,
1985
+ );
1986
+ }
1987
+ }`:' console.log("\\u2139 No Project model (or no tenants) \u2014 skipping project seed.");';return`/**
1988
+ * Database seed \u2014 generated by create-sailor.
1989
+ *
1990
+ * Run with: pnpm db:seed
1991
+ *
1992
+ * Auth provider: ${e}
1993
+ * Detected models at scaffold time: ${[...t].sort().join(", ")||"(none)"}
1994
+ *
1995
+ * Each block is wrapped in try/catch so evolving the schema does not
1996
+ * break the seed \u2014 missing models log a warning and move on.
1997
+ */
1998
+ import { PrismaClient } from "@prisma/client";
1999
+
2000
+ const prisma = new PrismaClient();
2001
+
2002
+ async function main(): Promise<void> {
2003
+ console.log("\\uD83C\\uDF31 Seeding database...");
2004
+
2005
+ // --- Tenants ---------------------------------------------------------
2006
+ ${a}
2007
+
2008
+ // --- Admin user ------------------------------------------------------
2009
+ ${s}
2010
+
2011
+ // --- Sample projects per tenant --------------------------------------
2012
+ ${c}
2013
+
2014
+ console.log("\\u2713 Seed complete");
2015
+ }
2016
+
2017
+ main()
2018
+ .catch((err) => {
2019
+ console.error("Seed failed:", err);
2020
+ process.exitCode = 1;
2021
+ })
2022
+ .finally(async () => {
2023
+ await prisma.$disconnect();
2024
+ });
2025
+ `}function No(e){if(X.existsSync(e))try{let t=X.readFileSync(e,"utf8"),n=JSON.parse(t),r=n.scripts??{},o=!1;r["db:seed"]||(r["db:seed"]="prisma db seed",o=!0);let s=n.prisma??{};if(s.seed||(s.seed="tsx prisma/seed.ts",o=!0),!o)return;let a={...n,scripts:r,prisma:s};X.writeFileSync(e,JSON.stringify(a,null,2)+`
2026
+ `)}catch(t){let n=t instanceof Error?t.message:String(t);console.warn(`[create-sailor] Failed to patch seed scripts in ${e}: ${n}`)}}async function sn(e,t){let n=bo(e);if(n)try{let r=ae.join(n.packageDir,"prisma","schema.prisma"),o=Ao(r),s=wo(t,o),a=ae.dirname(n.seedPath);X.existsSync(a)||X.mkdirSync(a,{recursive:!0}),X.writeFileSync(n.seedPath,s),No(n.packageJsonPath)}catch(r){let o=r instanceof Error?r.message:String(r);throw console.error(`[create-sailor] Failed to generate seed data: ${o}`),new Error(`Unable to write prisma/seed.ts: ${o}`)}}import H from"fs";import ce from"path";var He=[{id:"twilio",name:"Twilio",region:"global",envVars:["TWILIO_ACCOUNT_SID","TWILIO_AUTH_TOKEN","TWILIO_FROM_NUMBER"],docs:"https://www.twilio.com/docs/sms",pkgs:["twilio"]},{id:"messagebird",name:"MessageBird (Bird)",region:"global",envVars:["MESSAGEBIRD_ACCESS_KEY","MESSAGEBIRD_ORIGINATOR"],docs:"https://developers.messagebird.com/api/sms-messaging/",pkgs:["messagebird"]},{id:"plivo",name:"Plivo",region:"global",envVars:["PLIVO_AUTH_ID","PLIVO_AUTH_TOKEN","PLIVO_FROM_NUMBER"],docs:"https://www.plivo.com/docs/sms/",pkgs:["plivo"]},{id:"aliyun-sms",name:"\u963F\u91CC\u4E91\u77ED\u4FE1 Aliyun SMS",region:"cn",envVars:["ALIYUN_SMS_ACCESS_KEY","ALIYUN_SMS_ACCESS_SECRET","ALIYUN_SMS_SIGN_NAME","ALIYUN_SMS_TEMPLATE_CODE","ALIYUN_SMS_ENDPOINT"],docs:"https://help.aliyun.com/product/44282.html",pkgs:["@alicloud/dysmsapi20170525","@alicloud/openapi-client"]},{id:"tencent-sms",name:"\u817E\u8BAF\u4E91\u77ED\u4FE1 Tencent SMS",region:"cn",envVars:["TENCENT_SMS_SECRET_ID","TENCENT_SMS_SECRET_KEY","TENCENT_SMS_SDK_APP_ID","TENCENT_SMS_SIGN_NAME","TENCENT_SMS_TEMPLATE_ID","TENCENT_SMS_REGION"],docs:"https://cloud.tencent.com/document/product/382",pkgs:["tencentcloud-sdk-nodejs-sms"]},{id:"yunpian",name:"\u4E91\u7247 Yunpian",region:"cn",envVars:["YUNPIAN_API_KEY","YUNPIAN_SIGN"],docs:"https://www.yunpian.com/official/document/sms/zh_CN/introduction_product_intro",pkgs:["yunpian-nodejs-sdk"]},{id:"none",name:"None",region:"both",envVars:[],docs:""}],ji=He.reduce((e,t)=>(e[t.region]||(e[t.region]=[]),e[t.region].push(t),e),{});function an(e){return He.find(t=>t.id===e)}var To="@nebutra/sms",xo={twilio:"^5.3.6",messagebird:"^4.0.1",plivo:"^4.73.0","@alicloud/dysmsapi20170525":"^2.0.24","@alicloud/openapi-client":"^0.4.12","tencentcloud-sdk-nodejs-sms":"^4.0.970","yunpian-nodejs-sdk":"^0.0.2"};function at(e,t){H.mkdirSync(ce.dirname(e),{recursive:!0}),H.writeFileSync(e,t)}function Ro(e,t){let n=ce.join(e,".env.example");H.existsSync(n)?H.appendFileSync(n,`
2027
+ `+t):H.writeFileSync(n,t)}function ko(e){if(e.envVars.length===0)return"";let t=`# SMS provider: ${e.name}`,n=e.envVars.map(r=>`${r}=`).join(`
2028
+ `);return`${t}
2029
+ ${n}
2030
+ `}var Oo=`/**
2031
+ * Inputs for the \`sendSms\` wrapper. Keep this surface portable across
2032
+ * providers \u2014 provider-specific options belong in the wrapper itself.
2033
+ */
2034
+ export interface SendSmsInput {
2035
+ /** E.164 (+1415...) for global providers, or 11-digit string for CN providers. */
2036
+ to: string;
2037
+ /** Template code / ID registered with the provider. */
2038
+ templateCode?: string;
2039
+ /** Template variables \u2014 usually { code: "123456" } or { name, orderId, ... }. */
2040
+ params?: Record<string, string | number>;
2041
+ /** Free-form text body (only honored by providers that allow ad-hoc content). */
2042
+ body?: string;
2043
+ }
2044
+
2045
+ export interface SendSmsResult {
2046
+ messageId?: string;
2047
+ provider: string;
2048
+ raw?: unknown;
2049
+ }
2050
+ `;function Uo(){return`import twilio from "twilio";
2051
+ import type { SendSmsInput, SendSmsResult } from "./types.js";
2052
+
2053
+ let client: ReturnType<typeof twilio> | null = null;
2054
+
2055
+ function getClient() {
2056
+ if (client) return client;
2057
+ const sid = process.env.TWILIO_ACCOUNT_SID;
2058
+ const token = process.env.TWILIO_AUTH_TOKEN;
2059
+ if (!sid || !token) {
2060
+ throw new Error("TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN must be set");
2061
+ }
2062
+ client = twilio(sid, token);
2063
+ return client;
2064
+ }
2065
+
2066
+ export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
2067
+ const from = process.env.TWILIO_FROM_NUMBER;
2068
+ if (!from) throw new Error("TWILIO_FROM_NUMBER must be set");
2069
+
2070
+ const body =
2071
+ input.body ??
2072
+ Object.values(input.params ?? {}).join(" ") ??
2073
+ "";
2074
+
2075
+ const message = await getClient().messages.create({
2076
+ to: input.to,
2077
+ from,
2078
+ body,
2079
+ });
2080
+
2081
+ return { messageId: message.sid, provider: "twilio", raw: message };
2082
+ }
2083
+
2084
+ export type { SendSmsInput, SendSmsResult } from "./types.js";
2085
+ `}function Lo(){return`import messagebird from "messagebird";
2086
+ import type { SendSmsInput, SendSmsResult } from "./types.js";
2087
+
2088
+ let client: ReturnType<typeof messagebird> | null = null;
2089
+
2090
+ function getClient() {
2091
+ if (client) return client;
2092
+ const key = process.env.MESSAGEBIRD_ACCESS_KEY;
2093
+ if (!key) throw new Error("MESSAGEBIRD_ACCESS_KEY must be set");
2094
+ client = messagebird(key);
2095
+ return client;
2096
+ }
2097
+
2098
+ export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
2099
+ const originator = process.env.MESSAGEBIRD_ORIGINATOR;
2100
+ if (!originator) throw new Error("MESSAGEBIRD_ORIGINATOR must be set");
2101
+
2102
+ const body =
2103
+ input.body ??
2104
+ Object.values(input.params ?? {}).join(" ") ??
2105
+ "";
2106
+
2107
+ return await new Promise<SendSmsResult>((resolve, reject) => {
2108
+ getClient().messages.create(
2109
+ { originator, recipients: [input.to], body },
2110
+ (err, response) => {
2111
+ if (err) return reject(err);
2112
+ resolve({
2113
+ messageId: response?.id,
2114
+ provider: "messagebird",
2115
+ raw: response,
2116
+ });
2117
+ },
2118
+ );
2119
+ });
2120
+ }
2121
+
2122
+ export type { SendSmsInput, SendSmsResult } from "./types.js";
2123
+ `}function Mo(){return`import plivo from "plivo";
2124
+ import type { SendSmsInput, SendSmsResult } from "./types.js";
2125
+
2126
+ let client: InstanceType<typeof plivo.Client> | null = null;
2127
+
2128
+ function getClient() {
2129
+ if (client) return client;
2130
+ const authId = process.env.PLIVO_AUTH_ID;
2131
+ const authToken = process.env.PLIVO_AUTH_TOKEN;
2132
+ if (!authId || !authToken) {
2133
+ throw new Error("PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN must be set");
2134
+ }
2135
+ client = new plivo.Client(authId, authToken);
2136
+ return client;
2137
+ }
2138
+
2139
+ export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
2140
+ const from = process.env.PLIVO_FROM_NUMBER;
2141
+ if (!from) throw new Error("PLIVO_FROM_NUMBER must be set");
2142
+
2143
+ const body =
2144
+ input.body ??
2145
+ Object.values(input.params ?? {}).join(" ") ??
2146
+ "";
2147
+
2148
+ const response = await getClient().messages.create({
2149
+ src: from,
2150
+ dst: input.to,
2151
+ text: body,
2152
+ });
2153
+
2154
+ return {
2155
+ messageId: response.messageUuid?.[0],
2156
+ provider: "plivo",
2157
+ raw: response,
2158
+ };
2159
+ }
2160
+
2161
+ export type { SendSmsInput, SendSmsResult } from "./types.js";
2162
+ `}function jo(){return`import Dysmsapi, { SendSmsRequest } from "@alicloud/dysmsapi20170525";
2163
+ import * as $OpenApi from "@alicloud/openapi-client";
2164
+ import type { SendSmsInput, SendSmsResult } from "./types.js";
2165
+
2166
+ let client: Dysmsapi | null = null;
2167
+
2168
+ function getClient(): Dysmsapi {
2169
+ if (client) return client;
2170
+ const accessKeyId = process.env.ALIYUN_SMS_ACCESS_KEY;
2171
+ const accessKeySecret = process.env.ALIYUN_SMS_ACCESS_SECRET;
2172
+ if (!accessKeyId || !accessKeySecret) {
2173
+ throw new Error(
2174
+ "ALIYUN_SMS_ACCESS_KEY and ALIYUN_SMS_ACCESS_SECRET must be set",
2175
+ );
2176
+ }
2177
+ const config = new $OpenApi.Config({ accessKeyId, accessKeySecret });
2178
+ config.endpoint = process.env.ALIYUN_SMS_ENDPOINT ?? "dysmsapi.aliyuncs.com";
2179
+ client = new Dysmsapi(config);
2180
+ return client;
2181
+ }
2182
+
2183
+ export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
2184
+ const signName = process.env.ALIYUN_SMS_SIGN_NAME;
2185
+ const templateCode = input.templateCode ?? process.env.ALIYUN_SMS_TEMPLATE_CODE;
2186
+ if (!signName) throw new Error("ALIYUN_SMS_SIGN_NAME must be set");
2187
+ if (!templateCode) throw new Error("templateCode or ALIYUN_SMS_TEMPLATE_CODE must be provided");
2188
+
2189
+ const req = new SendSmsRequest({
2190
+ phoneNumbers: input.to,
2191
+ signName,
2192
+ templateCode,
2193
+ templateParam: JSON.stringify(input.params ?? {}),
2194
+ });
2195
+
2196
+ const response = await getClient().sendSms(req);
2197
+ const body = response.body;
2198
+
2199
+ if (body?.code !== "OK") {
2200
+ throw new Error(\`Aliyun SMS failed: \${body?.code} \${body?.message}\`);
2201
+ }
2202
+
2203
+ return {
2204
+ messageId: body?.bizId,
2205
+ provider: "aliyun-sms",
2206
+ raw: body,
2207
+ };
2208
+ }
2209
+
2210
+ export type { SendSmsInput, SendSmsResult } from "./types.js";
2211
+ `}function Do(){return`import * as tencentcloud from "tencentcloud-sdk-nodejs-sms";
2212
+ import type { SendSmsInput, SendSmsResult } from "./types.js";
2213
+
2214
+ const SmsClient = tencentcloud.sms.v20210111.Client;
2215
+ type SmsClientInstance = InstanceType<typeof SmsClient>;
2216
+
2217
+ let client: SmsClientInstance | null = null;
2218
+
2219
+ function getClient(): SmsClientInstance {
2220
+ if (client) return client;
2221
+ const secretId = process.env.TENCENT_SMS_SECRET_ID;
2222
+ const secretKey = process.env.TENCENT_SMS_SECRET_KEY;
2223
+ if (!secretId || !secretKey) {
2224
+ throw new Error(
2225
+ "TENCENT_SMS_SECRET_ID and TENCENT_SMS_SECRET_KEY must be set",
2226
+ );
2227
+ }
2228
+
2229
+ client = new SmsClient({
2230
+ credential: { secretId, secretKey },
2231
+ region: process.env.TENCENT_SMS_REGION ?? "ap-guangzhou",
2232
+ profile: {
2233
+ httpProfile: { endpoint: "sms.tencentcloudapi.com" },
2234
+ },
2235
+ });
2236
+ return client;
2237
+ }
2238
+
2239
+ export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
2240
+ const sdkAppId = process.env.TENCENT_SMS_SDK_APP_ID;
2241
+ const signName = process.env.TENCENT_SMS_SIGN_NAME;
2242
+ const templateId = input.templateCode ?? process.env.TENCENT_SMS_TEMPLATE_ID;
2243
+
2244
+ if (!sdkAppId) throw new Error("TENCENT_SMS_SDK_APP_ID must be set");
2245
+ if (!signName) throw new Error("TENCENT_SMS_SIGN_NAME must be set");
2246
+ if (!templateId) {
2247
+ throw new Error("templateCode or TENCENT_SMS_TEMPLATE_ID must be provided");
2248
+ }
2249
+
2250
+ // Tencent expects phone numbers with + prefix (E.164). Also expects ordered
2251
+ // template params as a string array matching the template placeholders.
2252
+ const to = input.to.startsWith("+") ? input.to : \`+\${input.to}\`;
2253
+ const templateParamSet = Object.values(input.params ?? {}).map(String);
2254
+
2255
+ const response = await getClient().SendSms({
2256
+ SmsSdkAppId: sdkAppId,
2257
+ SignName: signName,
2258
+ TemplateId: templateId,
2259
+ PhoneNumberSet: [to],
2260
+ TemplateParamSet: templateParamSet,
2261
+ });
2262
+
2263
+ const first = response.SendStatusSet?.[0];
2264
+ if (first && first.Code !== "Ok") {
2265
+ throw new Error(\`Tencent SMS failed: \${first.Code} \${first.Message}\`);
2266
+ }
2267
+
2268
+ return {
2269
+ messageId: first?.SerialNo,
2270
+ provider: "tencent-sms",
2271
+ raw: response,
2272
+ };
2273
+ }
2274
+
2275
+ export type { SendSmsInput, SendSmsResult } from "./types.js";
2276
+ `}function Fo(){return`// @ts-expect-error \u2014 yunpian-nodejs-sdk ships no type declarations
2277
+ import Yunpian from "yunpian-nodejs-sdk";
2278
+ import type { SendSmsInput, SendSmsResult } from "./types.js";
2279
+
2280
+ let client: { sms: { single_send(opts: unknown): Promise<unknown>; tpl_single_send(opts: unknown): Promise<unknown> } } | null =
2281
+ null;
2282
+
2283
+ function getClient() {
2284
+ if (client) return client;
2285
+ const apiKey = process.env.YUNPIAN_API_KEY;
2286
+ if (!apiKey) throw new Error("YUNPIAN_API_KEY must be set");
2287
+ client = new Yunpian(apiKey);
2288
+ return client!;
2289
+ }
2290
+
2291
+ /**
2292
+ * Yunpian (\u4E91\u7247) supports two modes:
2293
+ * - tpl_single_send: registered template + tpl_value
2294
+ * - single_send: ad-hoc text (must include registered \u7B7E\u540D)
2295
+ */
2296
+ export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
2297
+ const sign = process.env.YUNPIAN_SIGN ?? "";
2298
+
2299
+ if (input.templateCode) {
2300
+ const tplValue = Object.entries(input.params ?? {})
2301
+ .map(([k, v]) => \`#\${k}#=\${encodeURIComponent(String(v))}\`)
2302
+ .join("&");
2303
+
2304
+ const raw = await getClient().sms.tpl_single_send({
2305
+ mobile: input.to,
2306
+ tpl_id: input.templateCode,
2307
+ tpl_value: tplValue,
2308
+ });
2309
+
2310
+ return { provider: "yunpian", raw };
2311
+ }
2312
+
2313
+ const text = input.body
2314
+ ? input.body.startsWith("\u3010")
2315
+ ? input.body
2316
+ : \`\${sign}\${input.body}\`
2317
+ : \`\${sign}\${Object.values(input.params ?? {}).join(" ")}\`;
2318
+
2319
+ const raw = await getClient().sms.single_send({
2320
+ mobile: input.to,
2321
+ text,
2322
+ });
2323
+
2324
+ return { provider: "yunpian", raw };
2325
+ }
2326
+
2327
+ export type { SendSmsInput, SendSmsResult } from "./types.js";
2328
+ `}function Bo(e){switch(e){case"twilio":return Uo();case"messagebird":return Lo();case"plivo":return Mo();case"aliyun-sms":return jo();case"tencent-sms":return Do();case"yunpian":return Fo();case"none":return`export {};
2329
+ `}}function Yo(e){let t={};for(let r of e.pkgs??[]){let o=xo[r];o&&(t[r]=o)}let n={name:To,version:"0.1.0",private:!0,description:`Transactional SMS via ${e.name}`,type:"module",main:"./dist/index.js",exports:{".":{import:"./dist/index.js",types:"./dist/index.d.ts"}},scripts:{build:"tsup",typecheck:"tsc --noEmit",clean:"rm -rf dist .turbo"},dependencies:t,devDependencies:{tsup:"^8.5.0",typescript:"catalog:"}};return JSON.stringify(n,null,2)+`
2330
+ `}async function cn(e,t,n="global"){if(t==="none")return;let r=an(t);if(!r)throw new Error(`Unknown SMS provider "${t}". Valid ids: ${He.map(a=>a.id).join(", ")}`);let o=ce.join(e,"packages","sms");if(!H.existsSync(o))return;let s=ce.join(o,"src");H.existsSync(s)&&H.rmSync(s,{recursive:!0,force:!0});try{at(ce.join(o,"package.json"),Yo(r)),at(ce.join(s,"types.ts"),Oo),at(ce.join(s,"index.ts"),Bo(r.id)),Ro(e,ko(r))}catch(a){throw console.error(`Failed to apply SMS selection "${t}":`,a),new Error(`SMS scaffold for "${t}" failed \u2014 see stderr for details.`)}}import le from"fs";import pe from"path";var ze=[{id:"r2",name:"Cloudflare R2",region:"global",envVars:["R2_ACCOUNT_ID","R2_ACCESS_KEY_ID","R2_SECRET_ACCESS_KEY","R2_BUCKET"],docs:"https://developers.cloudflare.com/r2/api/s3/api/",pkg:"@aws-sdk/client-s3",pkgVersion:"^3.658.0"},{id:"s3",name:"AWS S3",region:"global",envVars:["AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_S3_REGION","S3_BUCKET"],docs:"https://docs.aws.amazon.com/s3/",pkg:"@aws-sdk/client-s3",pkgVersion:"^3.658.0"},{id:"supabase-storage",name:"Supabase Storage",region:"global",envVars:["SUPABASE_URL","SUPABASE_SERVICE_ROLE_KEY","SUPABASE_BUCKET"],docs:"https://supabase.com/docs/guides/storage",pkg:"@supabase/supabase-js",pkgVersion:"^2.45.4"},{id:"aliyun-oss",name:"\u963F\u91CC\u4E91 OSS",region:"cn",envVars:["ALIYUN_OSS_ACCESS_KEY","ALIYUN_OSS_ACCESS_SECRET","ALIYUN_OSS_REGION","ALIYUN_OSS_BUCKET"],docs:"https://help.aliyun.com/product/31815.html",pkg:"ali-oss",pkgVersion:"^6.21.0"},{id:"tencent-cos",name:"\u817E\u8BAF\u4E91 COS",region:"cn",envVars:["TENCENT_COS_SECRET_ID","TENCENT_COS_SECRET_KEY","TENCENT_COS_BUCKET","TENCENT_COS_REGION"],docs:"https://cloud.tencent.com/document/product/436",pkg:"cos-nodejs-sdk-v5",pkgVersion:"^2.13.7"},{id:"qiniu",name:"\u4E03\u725B\u4E91 Kodo",region:"cn",envVars:["QINIU_ACCESS_KEY","QINIU_SECRET_KEY","QINIU_BUCKET","QINIU_DOMAIN"],docs:"https://developer.qiniu.com/kodo",pkg:"qiniu",pkgVersion:"^7.14.0"},{id:"none",name:"None",region:"global",envVars:[],docs:""}];function pn(e){return ze.find(t=>t.id===e)}var Vi=ze.reduce((e,t)=>(e[t.region]||(e[t.region]=[]),e[t.region].push(t),e),{});var de="@nebutra/storage";function ct(e,t){le.mkdirSync(pe.dirname(e),{recursive:!0}),le.writeFileSync(e,t)}function Ko(e,t){let n=pe.join(e,".env.example");le.existsSync(n)?le.appendFileSync(n,`
2331
+ `+t):le.writeFileSync(n,t)}function Vo(e){if(e.envVars.length===0)return"";let t=`# Storage provider: ${e.name}`,n=e.envVars.map(r=>`${r}=`).join(`
2332
+ `);return`${t}
2333
+ ${n}
2334
+ `}var $o=`/**
2335
+ * Inputs for the \`uploadFile\` wrapper. Keep this surface portable across
2336
+ * providers \u2014 provider-specific options belong in the wrapper itself.
2337
+ */
2338
+ export interface UploadFileInput {
2339
+ key: string;
2340
+ body: Buffer | Uint8Array | string;
2341
+ contentType?: string;
2342
+ /** Optional access control. Most providers default to private. */
2343
+ acl?: "private" | "public-read";
2344
+ }
2345
+
2346
+ export interface UploadFileResult {
2347
+ key: string;
2348
+ /** Public or signed URL, whichever the provider returns by default. */
2349
+ url?: string;
2350
+ raw?: unknown;
2351
+ }
2352
+
2353
+ export interface SignedUrlOptions {
2354
+ /** Time-to-live in seconds. Defaults to 3600 (1h). */
2355
+ expiresIn?: number;
2356
+ /** Operation \u2014 "get" for download, "put" for direct upload. */
2357
+ operation?: "get" | "put";
2358
+ contentType?: string;
2359
+ }
2360
+ `;function qo(){return`/**
2361
+ * @${de.slice(1)} \u2014 Cloudflare R2 via the S3-compatible API.
2362
+ *
2363
+ * Docs: https://developers.cloudflare.com/r2/api/s3/api/
2364
+ */
2365
+ import {
2366
+ GetObjectCommand,
2367
+ PutObjectCommand,
2368
+ S3Client,
2369
+ } from "@aws-sdk/client-s3";
2370
+ import { getSignedUrl as awsGetSignedUrl } from "@aws-sdk/s3-request-presigner";
2371
+
2372
+ import type {
2373
+ SignedUrlOptions,
2374
+ UploadFileInput,
2375
+ UploadFileResult,
2376
+ } from "./types.js";
2377
+
2378
+ const accountId = process.env.R2_ACCOUNT_ID;
2379
+ const accessKeyId = process.env.R2_ACCESS_KEY_ID;
2380
+ const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY;
2381
+ const bucket = process.env.R2_BUCKET;
2382
+
2383
+ if (!accountId || !accessKeyId || !secretAccessKey || !bucket) {
2384
+ throw new Error(
2385
+ "R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET must be configured",
2386
+ );
2387
+ }
2388
+
2389
+ const client = new S3Client({
2390
+ region: "auto",
2391
+ endpoint: \`https://\${accountId}.r2.cloudflarestorage.com\`,
2392
+ credentials: { accessKeyId, secretAccessKey },
2393
+ });
2394
+
2395
+ export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
2396
+ const response = await client.send(
2397
+ new PutObjectCommand({
2398
+ Bucket: bucket,
2399
+ Key: input.key,
2400
+ Body: input.body,
2401
+ ContentType: input.contentType,
2402
+ }),
2403
+ );
2404
+ return { key: input.key, raw: response };
2405
+ }
2406
+
2407
+ export async function getSignedUrl(
2408
+ key: string,
2409
+ opts: SignedUrlOptions = {},
2410
+ ): Promise<string> {
2411
+ const expiresIn = opts.expiresIn ?? 3600;
2412
+ const command =
2413
+ opts.operation === "put"
2414
+ ? new PutObjectCommand({
2415
+ Bucket: bucket,
2416
+ Key: key,
2417
+ ContentType: opts.contentType,
2418
+ })
2419
+ : new GetObjectCommand({ Bucket: bucket, Key: key });
2420
+ return awsGetSignedUrl(client, command, { expiresIn });
2421
+ }
2422
+
2423
+ export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
2424
+ `}function Go(){return`/**
2425
+ * @${de.slice(1)} \u2014 AWS S3 wrapper.
2426
+ *
2427
+ * Docs: https://docs.aws.amazon.com/s3/
2428
+ */
2429
+ import {
2430
+ GetObjectCommand,
2431
+ PutObjectCommand,
2432
+ S3Client,
2433
+ } from "@aws-sdk/client-s3";
2434
+ import { getSignedUrl as awsGetSignedUrl } from "@aws-sdk/s3-request-presigner";
2435
+
2436
+ import type {
2437
+ SignedUrlOptions,
2438
+ UploadFileInput,
2439
+ UploadFileResult,
2440
+ } from "./types.js";
2441
+
2442
+ const region = process.env.AWS_S3_REGION;
2443
+ const bucket = process.env.S3_BUCKET;
2444
+ if (!region || !bucket) {
2445
+ throw new Error("AWS_S3_REGION and S3_BUCKET must be configured");
2446
+ }
2447
+
2448
+ const client = new S3Client({
2449
+ region,
2450
+ credentials:
2451
+ process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY
2452
+ ? {
2453
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
2454
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
2455
+ }
2456
+ : undefined,
2457
+ });
2458
+
2459
+ export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
2460
+ const response = await client.send(
2461
+ new PutObjectCommand({
2462
+ Bucket: bucket,
2463
+ Key: input.key,
2464
+ Body: input.body,
2465
+ ContentType: input.contentType,
2466
+ ACL: input.acl,
2467
+ }),
2468
+ );
2469
+ return { key: input.key, raw: response };
2470
+ }
2471
+
2472
+ export async function getSignedUrl(
2473
+ key: string,
2474
+ opts: SignedUrlOptions = {},
2475
+ ): Promise<string> {
2476
+ const expiresIn = opts.expiresIn ?? 3600;
2477
+ const command =
2478
+ opts.operation === "put"
2479
+ ? new PutObjectCommand({
2480
+ Bucket: bucket,
2481
+ Key: key,
2482
+ ContentType: opts.contentType,
2483
+ })
2484
+ : new GetObjectCommand({ Bucket: bucket, Key: key });
2485
+ return awsGetSignedUrl(client, command, { expiresIn });
2486
+ }
2487
+
2488
+ export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
2489
+ `}function Wo(){return`/**
2490
+ * @${de.slice(1)} \u2014 Supabase Storage wrapper.
2491
+ *
2492
+ * Docs: https://supabase.com/docs/guides/storage
2493
+ */
2494
+ import { createClient } from "@supabase/supabase-js";
2495
+
2496
+ import type {
2497
+ SignedUrlOptions,
2498
+ UploadFileInput,
2499
+ UploadFileResult,
2500
+ } from "./types.js";
2501
+
2502
+ const url = process.env.SUPABASE_URL;
2503
+ const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
2504
+ const bucket = process.env.SUPABASE_BUCKET;
2505
+ if (!url || !serviceRoleKey || !bucket) {
2506
+ throw new Error(
2507
+ "SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_BUCKET must be configured",
2508
+ );
2509
+ }
2510
+
2511
+ const client = createClient(url, serviceRoleKey, {
2512
+ auth: { persistSession: false },
2513
+ });
2514
+
2515
+ export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
2516
+ const { data, error } = await client.storage
2517
+ .from(bucket)
2518
+ .upload(input.key, input.body, {
2519
+ contentType: input.contentType,
2520
+ upsert: true,
2521
+ });
2522
+ if (error) {
2523
+ throw new Error(\`Supabase upload failed: \${error.message}\`);
2524
+ }
2525
+ return { key: data?.path ?? input.key, raw: data };
2526
+ }
2527
+
2528
+ export async function getSignedUrl(
2529
+ key: string,
2530
+ opts: SignedUrlOptions = {},
2531
+ ): Promise<string> {
2532
+ const expiresIn = opts.expiresIn ?? 3600;
2533
+ if (opts.operation === "put") {
2534
+ const { data, error } = await client.storage
2535
+ .from(bucket)
2536
+ .createSignedUploadUrl(key);
2537
+ if (error || !data) {
2538
+ throw new Error(\`Supabase signed upload url failed: \${error?.message ?? "unknown"}\`);
2539
+ }
2540
+ return data.signedUrl;
2541
+ }
2542
+ const { data, error } = await client.storage
2543
+ .from(bucket)
2544
+ .createSignedUrl(key, expiresIn);
2545
+ if (error || !data) {
2546
+ throw new Error(\`Supabase signed url failed: \${error?.message ?? "unknown"}\`);
2547
+ }
2548
+ return data.signedUrl;
2549
+ }
2550
+
2551
+ export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
2552
+ `}function Xo(){return`/**
2553
+ * @${de.slice(1)} \u2014 \u963F\u91CC\u4E91 OSS wrapper via ali-oss.
2554
+ *
2555
+ * Docs: https://help.aliyun.com/product/31815.html
2556
+ */
2557
+ import OSS from "ali-oss";
2558
+
2559
+ import type {
2560
+ SignedUrlOptions,
2561
+ UploadFileInput,
2562
+ UploadFileResult,
2563
+ } from "./types.js";
2564
+
2565
+ const accessKeyId = process.env.ALIYUN_OSS_ACCESS_KEY;
2566
+ const accessKeySecret = process.env.ALIYUN_OSS_ACCESS_SECRET;
2567
+ const region = process.env.ALIYUN_OSS_REGION;
2568
+ const bucket = process.env.ALIYUN_OSS_BUCKET;
2569
+ if (!accessKeyId || !accessKeySecret || !region || !bucket) {
2570
+ throw new Error(
2571
+ "ALIYUN_OSS_ACCESS_KEY, ALIYUN_OSS_ACCESS_SECRET, ALIYUN_OSS_REGION, ALIYUN_OSS_BUCKET must be configured",
2572
+ );
2573
+ }
2574
+
2575
+ const client = new OSS({ accessKeyId, accessKeySecret, region, bucket });
2576
+
2577
+ export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
2578
+ const body =
2579
+ typeof input.body === "string" ? Buffer.from(input.body) : Buffer.from(input.body);
2580
+ const response = await client.put(input.key, body, {
2581
+ mime: input.contentType,
2582
+ });
2583
+ return { key: response.name, url: response.url, raw: response };
2584
+ }
2585
+
2586
+ export async function getSignedUrl(
2587
+ key: string,
2588
+ opts: SignedUrlOptions = {},
2589
+ ): Promise<string> {
2590
+ const expires = opts.expiresIn ?? 3600;
2591
+ return client.signatureUrl(key, {
2592
+ expires,
2593
+ method: opts.operation === "put" ? "PUT" : "GET",
2594
+ "Content-Type": opts.contentType,
2595
+ });
2596
+ }
2597
+
2598
+ export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
2599
+ `}function Ho(){return`/**
2600
+ * @${de.slice(1)} \u2014 \u817E\u8BAF\u4E91 COS wrapper via cos-nodejs-sdk-v5.
2601
+ *
2602
+ * Docs: https://cloud.tencent.com/document/product/436
2603
+ */
2604
+ import COS from "cos-nodejs-sdk-v5";
2605
+
2606
+ import type {
2607
+ SignedUrlOptions,
2608
+ UploadFileInput,
2609
+ UploadFileResult,
2610
+ } from "./types.js";
2611
+
2612
+ const secretId = process.env.TENCENT_COS_SECRET_ID;
2613
+ const secretKey = process.env.TENCENT_COS_SECRET_KEY;
2614
+ const bucket = process.env.TENCENT_COS_BUCKET;
2615
+ const region = process.env.TENCENT_COS_REGION;
2616
+ if (!secretId || !secretKey || !bucket || !region) {
2617
+ throw new Error(
2618
+ "TENCENT_COS_SECRET_ID, TENCENT_COS_SECRET_KEY, TENCENT_COS_BUCKET, TENCENT_COS_REGION must be configured",
2619
+ );
2620
+ }
2621
+
2622
+ const cos = new COS({ SecretId: secretId, SecretKey: secretKey });
2623
+
2624
+ export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
2625
+ const body =
2626
+ typeof input.body === "string" ? Buffer.from(input.body) : Buffer.from(input.body);
2627
+ const response = await cos.putObject({
2628
+ Bucket: bucket,
2629
+ Region: region,
2630
+ Key: input.key,
2631
+ Body: body,
2632
+ ContentType: input.contentType,
2633
+ });
2634
+ return { key: input.key, url: response.Location, raw: response };
2635
+ }
2636
+
2637
+ export async function getSignedUrl(
2638
+ key: string,
2639
+ opts: SignedUrlOptions = {},
2640
+ ): Promise<string> {
2641
+ const expires = opts.expiresIn ?? 3600;
2642
+ const url = cos.getObjectUrl({
2643
+ Bucket: bucket,
2644
+ Region: region,
2645
+ Key: key,
2646
+ Sign: true,
2647
+ Method: opts.operation === "put" ? "PUT" : "GET",
2648
+ Expires: expires,
2649
+ } as unknown as COS.GetObjectUrlParams);
2650
+ return url as unknown as string;
2651
+ }
2652
+
2653
+ export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
2654
+ `}function zo(){return`/**
2655
+ * @${de.slice(1)} \u2014 \u4E03\u725B\u4E91 Kodo wrapper.
2656
+ *
2657
+ * Docs: https://developer.qiniu.com/kodo
2658
+ */
2659
+ import qiniu from "qiniu";
2660
+
2661
+ import type {
2662
+ SignedUrlOptions,
2663
+ UploadFileInput,
2664
+ UploadFileResult,
2665
+ } from "./types.js";
2666
+
2667
+ const accessKey = process.env.QINIU_ACCESS_KEY;
2668
+ const secretKey = process.env.QINIU_SECRET_KEY;
2669
+ const bucket = process.env.QINIU_BUCKET;
2670
+ const domain = process.env.QINIU_DOMAIN;
2671
+ if (!accessKey || !secretKey || !bucket || !domain) {
2672
+ throw new Error(
2673
+ "QINIU_ACCESS_KEY, QINIU_SECRET_KEY, QINIU_BUCKET, QINIU_DOMAIN must be configured",
2674
+ );
2675
+ }
2676
+
2677
+ const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
2678
+ const config = new qiniu.conf.Config();
2679
+ const formUploader = new qiniu.form_up.FormUploader(config);
2680
+ const bucketManager = new qiniu.rs.BucketManager(mac, config);
2681
+
2682
+ function uploadToken(key: string): string {
2683
+ const putPolicy = new qiniu.rs.PutPolicy({ scope: \`\${bucket}:\${key}\` });
2684
+ return putPolicy.uploadToken(mac);
2685
+ }
2686
+
2687
+ export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
2688
+ const body =
2689
+ typeof input.body === "string" ? Buffer.from(input.body) : Buffer.from(input.body);
2690
+
2691
+ return new Promise<UploadFileResult>((resolve, reject) => {
2692
+ formUploader.put(
2693
+ uploadToken(input.key),
2694
+ input.key,
2695
+ body,
2696
+ new qiniu.form_up.PutExtra(undefined, undefined, input.contentType),
2697
+ (err, body, info) => {
2698
+ if (err) return reject(err);
2699
+ if (info.statusCode !== 200) {
2700
+ return reject(new Error(\`Qiniu upload failed: \${info.statusCode}\`));
2701
+ }
2702
+ resolve({ key: input.key, raw: body });
2703
+ },
2704
+ );
2705
+ });
2706
+ }
2707
+
2708
+ export async function getSignedUrl(
2709
+ key: string,
2710
+ opts: SignedUrlOptions = {},
2711
+ ): Promise<string> {
2712
+ const expires = opts.expiresIn ?? 3600;
2713
+ if (opts.operation === "put") {
2714
+ // Qiniu issues upload tokens, not signed PUT URLs.
2715
+ return uploadToken(key);
2716
+ }
2717
+ const deadline = Math.floor(Date.now() / 1000) + expires;
2718
+ return bucketManager.privateDownloadUrl(domain, key, deadline);
2719
+ }
2720
+
2721
+ export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
2722
+ `}function Jo(e){switch(e){case"r2":return qo();case"s3":return Go();case"supabase-storage":return Wo();case"aliyun-oss":return Xo();case"tencent-cos":return Ho();case"qiniu":return zo();case"none":return`export {};
2723
+ `}}function Qo(e){let t={};e.pkg&&e.pkgVersion&&(t[e.pkg]=e.pkgVersion),(e.id==="r2"||e.id==="s3")&&(t["@aws-sdk/s3-request-presigner"]="^3.658.0");let n={name:de,version:"0.1.0",private:!0,description:`Object storage via ${e.name}`,type:"module",main:"./dist/index.js",exports:{".":{import:"./dist/index.js",types:"./dist/index.d.ts"}},scripts:{build:"tsup",typecheck:"tsc --noEmit",clean:"rm -rf dist .turbo"},dependencies:t,devDependencies:{tsup:"^8.5.0",typescript:"catalog:"}};return JSON.stringify(n,null,2)+`
2724
+ `}async function ln(e,t,n){if(t==="none")return;let r=pn(t);if(!r)throw new Error(`Unknown storage provider "${t}". Valid ids: ${ze.map(a=>a.id).join(", ")}`);let o=pe.join(e,"packages","storage"),s=pe.join(o,"src");le.existsSync(s)&&le.rmSync(s,{recursive:!0,force:!0}),ct(pe.join(o,"package.json"),Qo(r)),ct(pe.join(s,"types.ts"),$o),ct(pe.join(s,"index.ts"),Jo(r.id)),Ko(e,Vo(r))}import ue from"fs";import De from"path";function Zo(e){return`import Link from "next/link";
2725
+
2726
+ export default function WelcomePage() {
2727
+ const projectName = ${JSON.stringify(e)};
2728
+ return (
2729
+ <div className="mx-auto max-w-3xl px-6 py-16">
2730
+ <div className="space-y-8">
2731
+ <header className="space-y-2">
2732
+ <p className="text-sm text-muted-foreground">Powered by Sailor \u2693</p>
2733
+ <h1 className="text-4xl font-bold tracking-tight">
2734
+ Welcome to {projectName}
2735
+ </h1>
2736
+ <p className="text-lg text-muted-foreground">
2737
+ Your AI-native SaaS template is ready. Here&apos;s what to do next:
2738
+ </p>
2739
+ </header>
2740
+
2741
+ <section className="space-y-4">
2742
+ <h2 className="text-2xl font-semibold">Getting started</h2>
2743
+ <ol className="space-y-3">
2744
+ <Step
2745
+ number={1}
2746
+ title="Configure environment"
2747
+ description="Fill in your .env.local \u2014 we've generated random secrets for you"
2748
+ command=".env.local"
2749
+ />
2750
+ <Step
2751
+ number={2}
2752
+ title="Run database migrations"
2753
+ description="Sync your Prisma schema with your database"
2754
+ command="pnpm db:migrate"
2755
+ />
2756
+ <Step
2757
+ number={3}
2758
+ title="Seed example data"
2759
+ description="1 admin user + 3 tenants + sample records"
2760
+ command="pnpm db:seed"
2761
+ />
2762
+ <Step
2763
+ number={4}
2764
+ title="Customize your brand"
2765
+ description="Colors, domain, logo, SEO \u2014 all in one file"
2766
+ command="pnpm sailor brand init"
2767
+ />
2768
+ </ol>
2769
+ </section>
2770
+
2771
+ <section className="space-y-4">
2772
+ <h2 className="text-2xl font-semibold">Resources</h2>
2773
+ <ul className="list-inside list-disc space-y-2 text-sm">
2774
+ <li>
2775
+ <Link href="/docs" className="underline">
2776
+ Documentation
2777
+ </Link>
2778
+ </li>
2779
+ <li>
2780
+ <a
2781
+ href="https://github.com/nebutra/nebutra-sailor"
2782
+ className="underline"
2783
+ >
2784
+ GitHub
2785
+ </a>
2786
+ </li>
2787
+ <li>
2788
+ <a
2789
+ href="https://nebutra.com/get-license"
2790
+ className="underline"
2791
+ >
2792
+ Get free license (Individual/OPC)
2793
+ </a>
2794
+ </li>
2795
+ </ul>
2796
+ </section>
2797
+
2798
+ <footer className="border-t pt-6 text-xs text-muted-foreground">
2799
+ Delete this page after setup:{" "}
2800
+ <code>apps/web/src/app/[locale]/welcome/</code>
2801
+ </footer>
2802
+ </div>
2803
+ </div>
2804
+ );
2805
+ }
2806
+
2807
+ function Step({
2808
+ number,
2809
+ title,
2810
+ description,
2811
+ command,
2812
+ }: {
2813
+ number: number;
2814
+ title: string;
2815
+ description: string;
2816
+ command: string;
2817
+ }) {
2818
+ return (
2819
+ <li className="flex gap-4">
2820
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-sm font-medium text-primary-foreground">
2821
+ {number}
2822
+ </span>
2823
+ <div className="space-y-1">
2824
+ <h3 className="font-medium">{title}</h3>
2825
+ <p className="text-sm text-muted-foreground">{description}</p>
2826
+ <code className="inline-block rounded bg-muted px-2 py-0.5 text-xs">
2827
+ {command}
2828
+ </code>
2829
+ </div>
2830
+ </li>
2831
+ );
2832
+ }
2833
+ `}function es(e,t){return`# Next steps \u2014 ${e}
2834
+
2835
+ Region: ${t}
2836
+
2837
+ Your AI-native SaaS scaffold is ready. Complete these four steps to go from template to running dev server:
2838
+
2839
+ 1. **Configure environment** \u2014 fill in \`.env.local\`
2840
+ > Random cryptographic secrets have been generated for you. Add your provider keys (DB, auth, payments, AI).
2841
+
2842
+ 2. **Run database migrations** \u2014 sync your Prisma schema with your database
2843
+ \`\`\`bash
2844
+ pnpm db:migrate
2845
+ \`\`\`
2846
+
2847
+ 3. **Seed example data** \u2014 1 admin user + 3 tenants + sample records
2848
+ \`\`\`bash
2849
+ pnpm db:seed
2850
+ \`\`\`
2851
+
2852
+ 4. **Customize your brand** \u2014 colors, domain, logo, SEO \u2014 all in one file
2853
+ \`\`\`bash
2854
+ pnpm sailor brand init
2855
+ \`\`\`
2856
+
2857
+ After setup, delete the welcome route at \`apps/web/src/app/[locale]/welcome/\`.
2858
+
2859
+ ## Resources
2860
+
2861
+ - [Documentation](/docs)
2862
+ - [GitHub](https://github.com/nebutra/nebutra-sailor)
2863
+ - [Get free license (Individual/OPC)](https://nebutra.com/get-license)
2864
+ `}async function dn(e,t){let{projectName:n,region:r}=t;try{let o=De.join(e,".sailor");ue.existsSync(o)||ue.mkdirSync(o,{recursive:!0}),ue.writeFileSync(De.join(o,"next-steps.md"),es(n,r))}catch(o){let s=o instanceof Error?o.message:String(o);console.warn(`[create-sailor] Failed to write .sailor/next-steps.md: ${s}`)}try{let o=De.join(e,"apps","web");if(!ue.existsSync(o))return;let s=De.join(o,"src","app","[locale]","welcome");ue.existsSync(s)||ue.mkdirSync(s,{recursive:!0}),ue.writeFileSync(De.join(s,"page.tsx"),Zo(n))}catch(o){let s=o instanceof Error?o.message:String(o);console.warn(`[create-sailor] Failed to write welcome page: ${s}`)}}var mn="create-sailor";function l(e,t){e&&process.stdout.write(JSON.stringify(t)+`
2865
+ `)}function rs(){let e=process.env.npm_config_user_agent??"";return e.startsWith("pnpm")?"pnpm":e.startsWith("yarn")?"yarn":e.startsWith("bun")?"bun":"npm"}function gn(e){switch(e){case"postgres":case"postgresql":return"postgresql";case"mysql":return"mysql";case"sqlite":return"sqlite";case"none":return"none";default:return"postgresql"}}function fn(e){return e==="drizzle"?"drizzle":e==="none"?"none":"prisma"}function yn(e){return e==="lemon"||e==="lemonsqueezy"?"lemonsqueezy":e==="none"?"none":"stripe"}function Sn(e){if(!e)return"stripe";let t=e.toLowerCase();return t==="lemon"||t==="lemonsqueezy"?"lemon":t==="wechat"?"wechat":t==="alipay"?"alipay":t==="none"?"none":"stripe"}function En(e){if(!e)return"clerk";let t=e.toLowerCase();return t==="betterauth"||t==="better-auth"?"betterauth":t==="none"?"none":"clerk"}function hn(e){if(!e)return["openai"];let t=e.split(",").map(n=>n.trim().toLowerCase());return t.includes("none")?[]:t}var _n={mintlify:"Mintlify",docusaurus:"Docusaurus",nextra:"Nextra",vitepress:"VitePress"};function vn(e,t){if(!e)return"fumadocs";let n=e.toLowerCase();if(n==="fumadocs"||n==="none")return n;if(n in _n){let r=_n[n];return t?l(!0,{event:"notice",kind:"docs-fallback",requested:n,effective:"fumadocs"}):process.stdout.write(T.yellow(`\u26A0 ${r} support is coming in v1.2. Falling back to fumadocs.
2866
+ `)+T.dim(` Track progress: https://github.com/Nebutra/Nebutra-Sailor/issues
2867
+ `)),"fumadocs"}return"fumadocs"}function In(e){switch(e){case"vercel":return"vercel";case"railway":return"railway";case"cloudflare":return"cloudflare";case"selfhost":return"selfhost";case"none":return"none";default:return"vercel"}}function Pn(e){if(!e)return"global";let t=e.toLowerCase();return t==="cn"?"cn":t==="hybrid"?"hybrid":"global"}function os(e){return e==="cn"?{email:"aliyun-dm",storage:"aliyun-oss",monitoring:"sentry",analytics:"baidu",sms:"aliyun-sms"}:e==="hybrid"?{email:"resend",storage:"aliyun-oss",monitoring:"sentry",analytics:"posthog",sms:"aliyun-sms"}:{email:"resend",storage:"r2",monitoring:"sentry",analytics:"posthog",sms:"twilio"}}function bn(e){return e==="cn"?"wechat":"stripe"}async function ss(){let e=new ts;e.name(mn).description("Nebutra-Sailor \u2014 AI-Native SaaS template").version(J,"-v, --version").helpOption(!1).argument("[name]","project directory",void 0).option("-p, --pm <id>","npm | pnpm | yarn | bun").option("--region <id>","global | cn | hybrid").option("--orm <id>","prisma | drizzle | none").option("--db <id>","postgres | mysql | sqlite | none").option("--auth <id>","clerk | betterauth | none").option("--social-login <ids>","CN social login providers \u2014 wechat | qq | dingtalk | workweixin | feishu | weibo (comma-separated)").option("--payment <id>","stripe | lemon | wechat | alipay | none").option("--ai <ids>","comma-separated provider ids").option("--deploy <target>","vercel | railway | cloudflare | selfhost").option("--docs <id>","fumadocs | mintlify | docusaurus | nextra | vitepress | none").option("--email <id>","resend | postmark | ses | aliyun-dm | tencent-ses | netease | none").option("--storage <id>","r2 | s3 | supabase | aliyun-oss | tencent-cos | qiniu | none").option("--monitoring <id>","sentry | datadog | aliyun-arms | tingyun | none").option("--analytics <id>","posthog | plausible | umami | baidu | sensors | none").option("--sms <id>","twilio | aliyun-sms | tencent-sms | yunpian | none").option("--i18n","enable i18n").option("--no-i18n","disable i18n").option("--no-install","skip package install").option("--no-git","skip git init").option("-y, --yes","accept all defaults (non-interactive)").option("--dry-run","preview actions without writing files").option("--json","machine-readable output").option("--no-color","disable color output").option("-h, --help","show help"),e.parse(process.argv);let t=e.opts(),[n]=e.args;t.help&&(gt(),process.exit(0));let r=!!t.json,o=!!t.dryRun,s=!!t.yes,a=s||!process.stdin.isTTY;r||ut(),l(r,{event:"start",version:J});let c=n??(s?"./my-saas-app":void 0),i;if(c)i=c;else if(a)i="./my-saas-app";else{let g=await I.group({name:()=>I.text({message:"Where should we create your project?",placeholder:"./my-saas-app",defaultValue:"./my-saas-app",validate:y=>{if(y.length===0)return"Please enter a path."}})},{onCancel:()=>{process.stdout.write(T.red(`\u2718 Cancelled
2868
+ `)),process.exit(130)}});i=String(g.name)}let d=pt.basename(pt.resolve(i)),m=t.pm??rs(),S=!!t.region,me=!!t.auth,ge=!!t.payment,Ce=!!t.ai,we=!!t.email,f=!!t.storage,p=!!t.monitoring,b=!!t.analytics,Fe=!!t.sms,lt=t.i18n!==void 0,h,Ne,Y,Qe,V,O,U=bt(t.socialLogin),z,fe,$,x,Be;if(a){h=Pn(t.region),Ne=fn(t.orm),Y=gn(t.db);let g=ge?t.payment:bn(h);Qe=yn(g),V=Sn(g),O=En(t.auth),z=hn(t.ai),$=In(t.deploy),x=vn(t.docs,r),Be=lt?!!t.i18n:!0}else{let g={};if(S||(g.region=()=>I.select({message:"Target region?",options:[{value:"global",label:"global \u2014 \u6D77\u5916\u4F18\u5148"},{value:"cn",label:"cn \u2014 \u56FD\u5185\u4F18\u5148"},{value:"hybrid",label:"hybrid \u2014 \u53CC\u8F68\uFF08\u56FD\u5185+\u51FA\u6D77\uFF09"}],initialValue:"global"})),me||(g.auth=()=>I.select({message:"Auth provider?",options:[{value:"clerk",label:"Clerk"},{value:"betterauth",label:"Better Auth"},{value:"none",label:"None"}],initialValue:"clerk"})),!Ce){let Ve=Array.from(new Set(Ge.map(_=>_.category)));g.aiCategories=()=>I.multiselect({message:"Which AI Provider categories do you want to explore? (Select multiple)",options:Ve.map(_=>({value:_,label:_})),initialValues:["\u76F4\u63A5\u5B9E\u9A8C\u5BA4","\u7EDF\u4E00\u7F51\u5173"]}),g.aiProviders=({results:_})=>{let L=_.aiCategories||[],$e=Ge.filter(xe=>L.includes(xe.category)).map(xe=>({value:xe.id,label:`[${xe.category}] ${xe.name}`}));return $e.push({value:"custom",label:"[\u81EA\u5B9A\u4E49] Custom OpenAI-compatible endpoint"}),I.multiselect({message:"Select AI Providers (Space to select, Enter to submit)",options:$e,initialValues:["openai","anthropic"],required:!1})},g.enableCustom=({results:_})=>_.aiProviders?.includes("custom")?I.confirm({message:"Configure custom OpenAI-compatible endpoint?",initialValue:!0}):Promise.resolve(!1),g.customAiName=({results:_})=>_.enableCustom?I.text({message:"Custom endpoint name (e.g. proxy, local):",defaultValue:"custom",placeholder:"custom"}):Promise.resolve(void 0),g.customAiBaseUrl=({results:_})=>_.enableCustom?I.text({message:"Custom endpoint base URL (e.g. https://api.proxy.com/v1):",validate:L=>{if(L.length===0)return"Base URL is required."}}):Promise.resolve(void 0),g.customAiApiKeyEnv=({results:_})=>_.enableCustom?I.text({message:"Environment variable name for the API Key:",defaultValue:"CUSTOM_AI_API_KEY",placeholder:"CUSTOM_AI_API_KEY"}):Promise.resolve(void 0)}let y=Object.keys(g).length>0?await I.group(g,{onCancel:()=>{process.stdout.write(T.red(`\u2718 Cancelled
2869
+ `)),process.exit(130)}}):{};h=Pn(S?t.region:y.region),Ne=fn(t.orm),Y=gn(t.db);let R=ge?t.payment:bn(h);Qe=yn(R),V=Sn(R),O=En(me?t.auth:y.auth),z=Ce?hn(t.ai):y.aiProviders?.filter(Ve=>Ve!=="custom")??[],y.enableCustom&&(fe={name:y.customAiName,baseURL:y.customAiBaseUrl,apiKeyEnvName:y.customAiApiKeyEnv}),$=In(t.deploy),x=vn(t.docs,r),Be=lt?!!t.i18n:!0}let Te=os(h),ye=we?t.email:Te.email,Se=f?t.storage:Te.storage,Ee=p?t.monitoring:Te.monitoring,he=b?t.analytics:Te.analytics,_e=Fe?t.sms:Te.sms,Ye={region:h,orm:Ne,database:Y,payment:Qe,aiProviders:z,customAiEndpoint:fe,deployTarget:$,docs:x,i18n:Be,email:ye,storage:Se,monitoring:Ee,analytics:he,sms:_e},Ke=[["Project name",d],["Region",h],["Auth",O],["Social login",U.length>0?U.map(g=>Re.find(y=>y.id===g)?.name??g).join(", "):"none"],["ORM",Ne],["Database",Y],["Payment",V],["AI",Array.isArray(z)?z.join(", ")+(fe?", custom":""):String(z)],["Email",ye],["Storage",Se],["Monitoring",Ee],["Analytics",he],["SMS",_e],["Deploy Target",$],["Docs Framework",x]];r?Ke.forEach(([g,y],R)=>{l(!0,{event:"step",step:g,value:y,index:R+1,total:Ke.length})}):Ke.forEach(([g,y],R)=>{yt({index:R+1,total:Ke.length,label:g,value:y})});let An={databaseUrl:"postgresql://postgres:postgres@localhost:5432/nebutra",clerkPublishable:"",clerkSecret:""},Cn=Date.now();if(o){let g=z.length,y=[`clone template \u2192 ${i}`,"write nebutra.config.json",`prune template (orm=${Ne}, i18n=${Be})`,`region \u2192 ${h}`,`auth \u2192 ${O==="none"?"skip (remove packages/auth)":`configure ${O}`}`,...U.length>0?[`social-login \u2192 generate ${U.length} callback route${U.length===1?"":"s"} + SocialLoginButtons.tsx (${U.join(", ")})`]:[],`db \u2192 ${Y==="none"?"skip (remove packages/db)":`configure Prisma for ${Y}`}`,`payment \u2192 ${V==="none"?"skip (remove packages/billing)":`configure ${V}`}`,...x!=="none"?[`docs \u2192 scaffold apps/docs (${x==="fumadocs"?"fumadocs":`${x} \u2192 fumadocs fallback`})`]:[],...g>0||fe?[`ai-providers \u2192 generate registry.ts + env (${g}${fe?" + custom":""} provider${g===1&&!fe?"":"s"})`]:[],...ye!=="none"?[`email \u2192 configure ${ye}`]:[],...Se!=="none"?[`storage \u2192 configure ${Se}`]:[],...Ee!=="none"?[`monitoring \u2192 configure ${Ee}`]:[],...he!=="none"?[`analytics \u2192 configure ${he}`]:[],..._e!=="none"?[`sms \u2192 configure ${_e}`]:[],`compliance \u2192 inject ${h} boilerplate (ICP/Cookie/AIGC/Privacy)`,"welcome \u2192 generate dev welcome page","env \u2192 generate random secrets (AUTH_SECRET, JWT_SECRET)","seed \u2192 generate prisma/seed.ts (1 admin + 3 tenants)",...$!=="none"?[`deploy \u2192 inject ${$} config`]:[],"inject .env.local",t.install===!1?"skip install":`run ${m} install`,t.git===!1?"skip git init":"run git init"];if(r)y.forEach(R=>l(!0,{event:"plan",action:R})),l(!0,{event:"done",dryRun:!0});else{process.stdout.write(`
2870
+ `+T.bold(`Dry run \u2014 planned actions:
2871
+ `));for(let R of y)process.stdout.write(` \u2022 ${R}
2872
+ `);process.stdout.write(T.dim(`
156
2873
  No files were written.
157
- `))}process.exit(0)}let je=()=>{process.stdout.write(`
158
- `+I.red(`\u2718 Cancelled
159
- `));try{ct.existsSync(a)&&process.stdout.write(I.dim(` Partial directory left at ${a} \u2014 remove manually if needed.
160
- `))}catch{}process.exit(130)};process.on("SIGINT",je);try{g(s,{event:"step",step:"clone",status:"start"}),await ve(a),g(s,{event:"step",step:"clone",status:"ok"}),g(s,{event:"step",step:"package",status:"start"}),await he(a,d),g(s,{event:"step",step:"package",status:"ok"}),g(s,{event:"step",step:"config",status:"start"}),await ce(a,$),g(s,{event:"step",step:"config",status:"ok"}),g(s,{event:"step",step:"prune",status:"start"}),await we(a,$),g(s,{event:"step",step:"prune",status:"ok"}),g(s,{event:"step",step:"ai-providers",status:"start"});let u={providerIds:$.aiProviders,customEndpoint:$.customAiEndpoint},f=Q.join(a,"packages/ai-providers/templates");await Se(a,u,f),g(s,{event:"step",step:"ai-providers",status:"ok"}),g(s,{event:"step",step:"docs",status:"start"}),S!=="none"?(await de(a,{framework:S,projectName:d}),g(s,{event:"step",step:"docs",framework:"fumadocs",requested:S,status:"ok"})):g(s,{event:"step",step:"docs",status:"skip"}),g(s,{event:"step",step:"deploy-target",status:"start"}),j!=="none"&&await pe(a,j),g(s,{event:"step",step:"deploy-target",status:"ok"}),g(s,{event:"step",step:"env",status:"start"}),await ye(a,Te),g(s,{event:"step",step:"env",status:"ok"});let A=Math.max(1,Math.round((Date.now()-Ve)/1e3));s?g(!0,{event:"done",status:"ok",elapsedSec:A,targetDir:a}):re({elapsedSec:A,targetDir:a,skippedInstall:t.install===!1});try{lt({pkg:{name:Ie,version:ee},updateCheckInterval:1e3*60*60*24}).notify({defer:!1,isGlobal:!0})}catch{}process.exit(0)}catch(u){let f=u instanceof Error?u.message:String(u);s?g(!0,{event:"error",message:f}):process.stdout.write(I.red(`
161
- \u2718 Failed: ${f}
162
- `)),process.exit(1)}}ut().catch(e=>{process.stderr.write(String(e)+`
2874
+ `))}process.exit(0)}let wn=async()=>{process.stdout.write(`
2875
+ `+T.red(`\u2718 Cancelled
2876
+ `)),un.existsSync(i)&&await I.confirm({message:`Cleanup partial install at ${i}?`,initialValue:!0})===!0&&(un.rmSync(i,{recursive:!0,force:!0}),process.stdout.write(T.dim(` \u2713 Removed ${i}
2877
+ `))),process.exit(130)};process.on("SIGINT",wn);try{l(r,{event:"step",step:"clone",status:"start"}),await qt(i),l(r,{event:"step",step:"clone",status:"ok"}),l(r,{event:"step",step:"package",status:"start"}),await Ht(i,d),l(r,{event:"step",step:"package",status:"ok"}),l(r,{event:"step",step:"config",status:"start"}),await Nt(i,Ye),l(r,{event:"step",step:"config",status:"ok"}),l(r,{event:"step",step:"prune",status:"start"}),await on(i,Ye),l(r,{event:"step",step:"prune",status:"ok"}),l(r,{event:"step",step:"auth",choice:O,status:"start"}),await It(i,O),l(r,{event:"step",step:"auth",choice:O,status:"ok"}),U.length>0?(l(r,{event:"step",step:"social-login",providers:U,status:"start"}),await Ct(i,U,O),l(r,{event:"step",step:"social-login",providers:U,status:"ok"})):l(r,{event:"step",step:"social-login",status:"skip"}),l(r,{event:"step",step:"db",choice:Y,status:"start"}),await Rt(i,Y,d),l(r,{event:"step",step:"db",choice:Y,status:"ok"}),l(r,{event:"step",step:"payment",choice:V,status:"start"}),await Jt(i,V),l(r,{event:"step",step:"payment",choice:V,status:"ok"}),await Dt(i,ye,h),r&&l(!0,{event:"step",step:"email",choice:ye,status:"ok"}),await ln(i,Se,h),r&&l(!0,{event:"step",step:"storage",choice:Se,status:"ok"}),await Xt(i,Ee,h),r&&l(!0,{event:"step",step:"monitoring",choice:Ee,status:"ok"}),await ht(i,he,h),r&&l(!0,{event:"step",step:"analytics",choice:he,status:"ok"}),await cn(i,_e,h),r&&l(!0,{event:"step",step:"sms",choice:_e,status:"ok"}),await wt(i,h),r&&l(!0,{event:"step",step:"compliance",region:h,status:"ok"}),await Yt(i),await sn(i,O),await dn(i,{projectName:d,region:h}),l(r,{event:"step",step:"ai-providers",status:"start"});let g={providerIds:Ye.aiProviders,customEndpoint:Ye.customAiEndpoint},y=pt.join(i,"packages/ai-providers/templates");if(await nn(i,g,y),l(r,{event:"step",step:"ai-providers",status:"ok"}),l(r,{event:"step",step:"docs",status:"start"}),x!=="none"?(await Ut(i,{framework:x,projectName:d}),l(r,{event:"step",step:"docs",framework:"fumadocs",requested:x,status:"ok"})):l(r,{event:"step",step:"docs",status:"skip"}),l(r,{event:"step",step:"deploy-target",status:"start"}),$!=="none"&&await kt(i,$),l(r,{event:"step",step:"deploy-target",status:"ok"}),l(r,{event:"step",step:"env",status:"start"}),await Vt(i,An),l(r,{event:"step",step:"env",status:"ok"}),t.install!==!1){let L=m==="bun"?"bun install":`${m} install`;r?l(!0,{event:"step",step:"install",pm:m,status:"start"}):process.stdout.write(T.dim(` Installing dependencies with ${m}\u2026
2878
+ `));try{Je(L,{cwd:i,stdio:r?"ignore":"inherit"}),l(r,{event:"step",step:"install",status:"ok"})}catch(ve){let $e=ve instanceof Error?ve.message:String(ve);r?l(!0,{event:"step",step:"install",status:"error",error:$e}):process.stdout.write(T.yellow(` \u26A0 install failed \u2014 run '${L}' manually.
2879
+ `))}}else l(r,{event:"step",step:"install",status:"skip"});if(t.git!==!1){r&&l(!0,{event:"step",step:"git-init",status:"start"});try{Je("git init -q",{cwd:i,stdio:"ignore"}),Je("git add -A",{cwd:i,stdio:"ignore"}),Je('git -c user.email=you@example.com -c user.name="You" commit -q -m "chore: initial scaffold from create-sailor"',{cwd:i,stdio:"ignore"}),l(r,{event:"step",step:"git-init",status:"ok"})}catch(L){let ve=L instanceof Error?L.message:String(L);r?l(!0,{event:"step",step:"git-init",status:"error",error:ve}):process.stdout.write(T.yellow(` \u26A0 git init skipped \u2014 not fatal.
2880
+ `))}}else l(r,{event:"step",step:"git-init",status:"skip"});let _=Math.max(1,Math.round((Date.now()-Cn)/1e3));r?l(!0,{event:"done",status:"ok",elapsedSec:_,targetDir:i}):mt({elapsedSec:_,targetDir:i,skippedInstall:t.install===!1});try{ns({pkg:{name:mn,version:J},updateCheckInterval:1e3*60*60*24}).notify({defer:!1,isGlobal:!0})}catch{}process.exit(0)}catch(g){let y=g instanceof Error?g.message:String(g);r?l(!0,{event:"error",message:y}):process.stdout.write(T.red(`
2881
+ \u2718 Failed: ${y}
2882
+ `)),process.exit(1)}}ss().catch(e=>{process.stderr.write(String(e)+`
163
2883
  `),process.exit(1)});