create-recur-tw 0.0.2 → 0.1.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 +28 -3
  2. package/index.js +202 -2
  3. package/package.json +12 -3
package/README.md CHANGED
@@ -1,10 +1,35 @@
1
1
  # create-recur-tw
2
2
 
3
- Scaffold a Recur-integrated app.
3
+ Scaffold a [Recur](https://recur.tw)-integrated app from official templates
4
+ — checkout, webhooks, entitlements gating, and customer portal pre-wired.
4
5
 
5
- > **Coming soon.** This is a placeholder release to reserve the package name.
6
- > Follow progress at <https://github.com/recur-tw/templates>.
6
+ ## Usage
7
7
 
8
8
  ```bash
9
+ # Interactive
9
10
  npm create recur-tw@latest
11
+
12
+ # Non-interactive (CI / AI agents)
13
+ npm create recur-tw@latest my-app -- --template saas --yes
10
14
  ```
15
+
16
+ ## Options
17
+
18
+ | Option | Description |
19
+ |--------|-------------|
20
+ | `-t, --template <name>` | Template to use (`saas`) |
21
+ | `--pm <manager>` | Package manager for install (`pnpm` / `npm` / `yarn` / `bun`) |
22
+ | `--no-install` | Skip installing dependencies |
23
+ | `--no-git` | Skip `git init` |
24
+ | `--ref <git-ref>` | Templates repo ref (defaults to the tag pinned by this CLI version) |
25
+ | `-y, --yes` | Accept defaults, never prompt |
26
+
27
+ ## Templates
28
+
29
+ See the [templates repo](https://github.com/recur-tw/templates) for the
30
+ full catalog. More templates (newsletter, community, tanstack, backend-only)
31
+ are on the roadmap.
32
+
33
+ ## License
34
+
35
+ MIT
package/index.js CHANGED
@@ -1,3 +1,203 @@
1
1
  #!/usr/bin/env node
2
- console.log('create-recur-tw is coming soon.')
3
- console.log('Follow progress at https://github.com/recur-tw/templates')
2
+ // create-recur-tw scaffold a Recur-integrated app from recur-tw/templates.
3
+ //
4
+ // Interactive: npm create recur-tw@latest
5
+ // Non-interactive: npm create recur-tw@latest my-app -- --template saas --yes
6
+ import { existsSync, readdirSync, copyFileSync, readFileSync } from 'node:fs'
7
+ import { resolve, join, dirname } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+ import { execSync } from 'node:child_process'
10
+ import * as p from '@clack/prompts'
11
+ import pc from 'picocolors'
12
+ import { downloadTemplate } from 'giget'
13
+
14
+ // Each CLI release pins the templates repo to a tag so CLI and template
15
+ // contents can't drift apart. Override with --ref (e.g. --ref main).
16
+ const TEMPLATES_REF = 'v0.1.0'
17
+ const TEMPLATES_REPO = 'recur-tw/templates'
18
+
19
+ const TEMPLATES = {
20
+ saas: {
21
+ label: 'SaaS 訂閱制服務',
22
+ hint: 'Next.js + 多方案訂閱 + entitlements 權限閘門 + customer portal',
23
+ },
24
+ // Coming soon: newsletter, community, saas-tanstack, backend-only
25
+ }
26
+
27
+ const { version } = JSON.parse(
28
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8')
29
+ )
30
+
31
+ // --- Parse argv -------------------------------------------------------------
32
+
33
+ const HELP = `
34
+ ${pc.bold('create-recur-tw')} v${version} — scaffold a Recur-integrated app
35
+
36
+ ${pc.bold('Usage:')}
37
+ npm create recur-tw@latest [dir] -- [options]
38
+ pnpm create recur-tw [dir] [options]
39
+
40
+ ${pc.bold('Options:')}
41
+ -t, --template <name> Template to use (${Object.keys(TEMPLATES).join(', ')})
42
+ --pm <manager> Package manager for install (pnpm | npm | yarn | bun)
43
+ --no-install Skip installing dependencies
44
+ --no-git Skip git init
45
+ --ref <git-ref> Templates repo ref (default: ${TEMPLATES_REF})
46
+ -y, --yes Accept defaults, never prompt (for CI / agents)
47
+ -h, --help Show this help
48
+ -v, --version Show version
49
+
50
+ ${pc.bold('Examples:')}
51
+ npm create recur-tw@latest my-app -- --template saas --yes
52
+ pnpm create recur-tw my-app --template saas --pm pnpm --no-install
53
+ `
54
+
55
+ function parseArgs(argv) {
56
+ const args = { install: true, git: true, yes: false, ref: TEMPLATES_REF }
57
+ const rest = []
58
+ for (let i = 0; i < argv.length; i++) {
59
+ const a = argv[i]
60
+ if (a === '--template' || a === '-t') args.template = argv[++i]
61
+ else if (a === '--pm') args.pm = argv[++i]
62
+ else if (a === '--ref') args.ref = argv[++i]
63
+ else if (a === '--no-install') args.install = false
64
+ else if (a === '--no-git') args.git = false
65
+ else if (a === '--yes' || a === '-y') args.yes = true
66
+ else if (a === '--help' || a === '-h') args.help = true
67
+ else if (a === '--version' || a === '-v') args.showVersion = true
68
+ else if (!a.startsWith('-')) rest.push(a)
69
+ else {
70
+ console.error(pc.red(`Unknown option: ${a}`))
71
+ console.error(HELP)
72
+ process.exit(1)
73
+ }
74
+ }
75
+ args.dir = rest[0]
76
+ return args
77
+ }
78
+
79
+ function detectPackageManager() {
80
+ const ua = process.env.npm_config_user_agent ?? ''
81
+ if (ua.startsWith('pnpm')) return 'pnpm'
82
+ if (ua.startsWith('yarn')) return 'yarn'
83
+ if (ua.startsWith('bun')) return 'bun'
84
+ return 'npm'
85
+ }
86
+
87
+ function bail(message) {
88
+ console.error(pc.red(`✖ ${message}`))
89
+ process.exit(1)
90
+ }
91
+
92
+ // --- Main -------------------------------------------------------------------
93
+
94
+ async function main() {
95
+ const args = parseArgs(process.argv.slice(2))
96
+ if (args.help) return console.log(HELP)
97
+ if (args.showVersion) return console.log(version)
98
+
99
+ const interactive = !args.yes && process.stdout.isTTY
100
+
101
+ if (interactive) p.intro(pc.bgCyan(pc.black(' create-recur-tw ')))
102
+
103
+ // Project directory
104
+ let dir = args.dir
105
+ if (!dir) {
106
+ if (!interactive) bail('Missing project directory. Usage: create-recur-tw <dir> --template <name> --yes')
107
+ dir = await p.text({
108
+ message: '專案要建立在哪個資料夾?',
109
+ placeholder: 'my-app',
110
+ validate: (v) => (v.trim() ? undefined : '請輸入資料夾名稱'),
111
+ })
112
+ if (p.isCancel(dir)) return p.cancel('已取消')
113
+ }
114
+ const target = resolve(process.cwd(), dir)
115
+ if (existsSync(target) && readdirSync(target).length > 0) {
116
+ bail(`Directory not empty: ${target}`)
117
+ }
118
+
119
+ // Template
120
+ let template = args.template
121
+ if (template && !TEMPLATES[template]) {
122
+ bail(`Unknown template "${template}". Available: ${Object.keys(TEMPLATES).join(', ')}`)
123
+ }
124
+ if (!template) {
125
+ if (!interactive) {
126
+ template = 'saas' // --yes default
127
+ } else {
128
+ template = await p.select({
129
+ message: '選擇模板',
130
+ options: Object.entries(TEMPLATES).map(([value, t]) => ({
131
+ value,
132
+ label: `${value} — ${t.label}`,
133
+ hint: t.hint,
134
+ })),
135
+ })
136
+ if (p.isCancel(template)) return p.cancel('已取消')
137
+ }
138
+ }
139
+
140
+ // Download
141
+ const source = `github:${TEMPLATES_REPO}/templates/${template}#${args.ref}`
142
+ const spinner = interactive ? p.spinner() : null
143
+ spinner?.start(`下載模板 ${template} (${args.ref})`)
144
+ try {
145
+ await downloadTemplate(source, { dir: target })
146
+ } catch (error) {
147
+ spinner?.stop('下載失敗')
148
+ bail(`Failed to download ${source}\n ${error.message}`)
149
+ }
150
+ spinner?.stop(`模板已下載到 ${dir}`)
151
+ if (!interactive) console.log(`✔ Downloaded ${template} (${args.ref}) into ${dir}`)
152
+
153
+ // .env.local
154
+ const envExample = join(target, '.env.example')
155
+ const envLocal = join(target, '.env.local')
156
+ if (existsSync(envExample) && !existsSync(envLocal)) {
157
+ copyFileSync(envExample, envLocal)
158
+ }
159
+
160
+ // git init
161
+ if (args.git) {
162
+ try {
163
+ execSync('git init -q && git add -A && git commit -qm "Initial commit from create-recur-tw"', {
164
+ cwd: target,
165
+ stdio: 'ignore',
166
+ shell: true,
167
+ })
168
+ } catch {
169
+ // git unavailable or already in a repo — not fatal
170
+ }
171
+ }
172
+
173
+ // Install
174
+ const pm = args.pm ?? detectPackageManager()
175
+ if (args.install) {
176
+ spinner?.start(`安裝依賴 (${pm} install)`)
177
+ try {
178
+ execSync(`${pm} install`, { cwd: target, stdio: spinner ? 'ignore' : 'inherit' })
179
+ spinner?.stop('依賴安裝完成')
180
+ } catch {
181
+ spinner?.stop('依賴安裝失敗')
182
+ console.error(pc.yellow(`⚠ ${pm} install failed — run it manually inside ${dir}`))
183
+ }
184
+ }
185
+
186
+ // Next steps
187
+ const steps = [
188
+ `cd ${dir}`,
189
+ ...(args.install ? [] : [`${pm} install`]),
190
+ `填入 .env.local 的 Recur API keys (app.recur.tw → Settings → Developers)`,
191
+ `${pm} run dev`,
192
+ ]
193
+ if (interactive) {
194
+ p.note(steps.map((s, i) => `${i + 1}. ${s}`).join('\n'), '下一步')
195
+ p.outro(`完成!文件與客製化指南見 ${dir}/README.md 和 ${dir}/AGENTS.md`)
196
+ } else {
197
+ console.log('\nNext steps:')
198
+ steps.forEach((s, i) => console.log(` ${i + 1}. ${s}`))
199
+ console.log(`\nDocs: ${dir}/README.md · Agent guide: ${dir}/AGENTS.md`)
200
+ }
201
+ }
202
+
203
+ main().catch((error) => bail(error.message))
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "create-recur-tw",
3
- "version": "0.0.2",
4
- "description": "Scaffold a Recur-integrated app (coming soon).",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a Recur-integrated app from official templates.",
5
+ "type": "module",
5
6
  "bin": {
6
7
  "create-recur-tw": "index.js"
7
8
  },
@@ -14,5 +15,13 @@
14
15
  "type": "git",
15
16
  "url": "git+https://github.com/recur-tw/templates.git"
16
17
  },
17
- "license": "MIT"
18
+ "license": "MIT",
19
+ "engines": {
20
+ "node": ">=18.17"
21
+ },
22
+ "dependencies": {
23
+ "@clack/prompts": "^0.11.0",
24
+ "giget": "^2.0.0",
25
+ "picocolors": "^1.1.1"
26
+ }
18
27
  }