create-recur-tw 0.0.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +28 -3
  2. package/index.js +206 -2
  3. package/package.json +20 -4
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`, `newsletter`) |
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,207 @@
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.2.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
+ newsletter: {
25
+ label: '付費電子報',
26
+ hint: 'Tiptap 編輯器 + R2 圖片上傳 + 訂閱牆,內容存 git 免資料庫',
27
+ },
28
+ // Coming soon: community, saas-tanstack, backend-only
29
+ }
30
+
31
+ const { version } = JSON.parse(
32
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8')
33
+ )
34
+
35
+ // --- Parse argv -------------------------------------------------------------
36
+
37
+ const HELP = `
38
+ ${pc.bold('create-recur-tw')} v${version} — scaffold a Recur-integrated app
39
+
40
+ ${pc.bold('Usage:')}
41
+ npm create recur-tw@latest [dir] -- [options]
42
+ pnpm create recur-tw [dir] [options]
43
+
44
+ ${pc.bold('Options:')}
45
+ -t, --template <name> Template to use (${Object.keys(TEMPLATES).join(', ')})
46
+ --pm <manager> Package manager for install (pnpm | npm | yarn | bun)
47
+ --no-install Skip installing dependencies
48
+ --no-git Skip git init
49
+ --ref <git-ref> Templates repo ref (default: ${TEMPLATES_REF})
50
+ -y, --yes Accept defaults, never prompt (for CI / agents)
51
+ -h, --help Show this help
52
+ -v, --version Show version
53
+
54
+ ${pc.bold('Examples:')}
55
+ npm create recur-tw@latest my-app -- --template saas --yes
56
+ pnpm create recur-tw my-app --template saas --pm pnpm --no-install
57
+ `
58
+
59
+ function parseArgs(argv) {
60
+ const args = { install: true, git: true, yes: false, ref: TEMPLATES_REF }
61
+ const rest = []
62
+ for (let i = 0; i < argv.length; i++) {
63
+ const a = argv[i]
64
+ if (a === '--template' || a === '-t') args.template = argv[++i]
65
+ else if (a === '--pm') args.pm = argv[++i]
66
+ else if (a === '--ref') args.ref = argv[++i]
67
+ else if (a === '--no-install') args.install = false
68
+ else if (a === '--no-git') args.git = false
69
+ else if (a === '--yes' || a === '-y') args.yes = true
70
+ else if (a === '--help' || a === '-h') args.help = true
71
+ else if (a === '--version' || a === '-v') args.showVersion = true
72
+ else if (!a.startsWith('-')) rest.push(a)
73
+ else {
74
+ console.error(pc.red(`Unknown option: ${a}`))
75
+ console.error(HELP)
76
+ process.exit(1)
77
+ }
78
+ }
79
+ args.dir = rest[0]
80
+ return args
81
+ }
82
+
83
+ function detectPackageManager() {
84
+ const ua = process.env.npm_config_user_agent ?? ''
85
+ if (ua.startsWith('pnpm')) return 'pnpm'
86
+ if (ua.startsWith('yarn')) return 'yarn'
87
+ if (ua.startsWith('bun')) return 'bun'
88
+ return 'npm'
89
+ }
90
+
91
+ function bail(message) {
92
+ console.error(pc.red(`✖ ${message}`))
93
+ process.exit(1)
94
+ }
95
+
96
+ // --- Main -------------------------------------------------------------------
97
+
98
+ async function main() {
99
+ const args = parseArgs(process.argv.slice(2))
100
+ if (args.help) return console.log(HELP)
101
+ if (args.showVersion) return console.log(version)
102
+
103
+ const interactive = !args.yes && process.stdout.isTTY
104
+
105
+ if (interactive) p.intro(pc.bgCyan(pc.black(' create-recur-tw ')))
106
+
107
+ // Project directory
108
+ let dir = args.dir
109
+ if (!dir) {
110
+ if (!interactive) bail('Missing project directory. Usage: create-recur-tw <dir> --template <name> --yes')
111
+ dir = await p.text({
112
+ message: '專案要建立在哪個資料夾?',
113
+ placeholder: 'my-app',
114
+ validate: (v) => (v.trim() ? undefined : '請輸入資料夾名稱'),
115
+ })
116
+ if (p.isCancel(dir)) return p.cancel('已取消')
117
+ }
118
+ const target = resolve(process.cwd(), dir)
119
+ if (existsSync(target) && readdirSync(target).length > 0) {
120
+ bail(`Directory not empty: ${target}`)
121
+ }
122
+
123
+ // Template
124
+ let template = args.template
125
+ if (template && !TEMPLATES[template]) {
126
+ bail(`Unknown template "${template}". Available: ${Object.keys(TEMPLATES).join(', ')}`)
127
+ }
128
+ if (!template) {
129
+ if (!interactive) {
130
+ template = 'saas' // --yes default
131
+ } else {
132
+ template = await p.select({
133
+ message: '選擇模板',
134
+ options: Object.entries(TEMPLATES).map(([value, t]) => ({
135
+ value,
136
+ label: `${value} — ${t.label}`,
137
+ hint: t.hint,
138
+ })),
139
+ })
140
+ if (p.isCancel(template)) return p.cancel('已取消')
141
+ }
142
+ }
143
+
144
+ // Download
145
+ const source = `github:${TEMPLATES_REPO}/templates/${template}#${args.ref}`
146
+ const spinner = interactive ? p.spinner() : null
147
+ spinner?.start(`下載模板 ${template} (${args.ref})`)
148
+ try {
149
+ await downloadTemplate(source, { dir: target })
150
+ } catch (error) {
151
+ spinner?.stop('下載失敗')
152
+ bail(`Failed to download ${source}\n ${error.message}`)
153
+ }
154
+ spinner?.stop(`模板已下載到 ${dir}`)
155
+ if (!interactive) console.log(`✔ Downloaded ${template} (${args.ref}) into ${dir}`)
156
+
157
+ // .env.local
158
+ const envExample = join(target, '.env.example')
159
+ const envLocal = join(target, '.env.local')
160
+ if (existsSync(envExample) && !existsSync(envLocal)) {
161
+ copyFileSync(envExample, envLocal)
162
+ }
163
+
164
+ // git init
165
+ if (args.git) {
166
+ try {
167
+ execSync('git init -q && git add -A && git commit -qm "Initial commit from create-recur-tw"', {
168
+ cwd: target,
169
+ stdio: 'ignore',
170
+ shell: true,
171
+ })
172
+ } catch {
173
+ // git unavailable or already in a repo — not fatal
174
+ }
175
+ }
176
+
177
+ // Install
178
+ const pm = args.pm ?? detectPackageManager()
179
+ if (args.install) {
180
+ spinner?.start(`安裝依賴 (${pm} install)`)
181
+ try {
182
+ execSync(`${pm} install`, { cwd: target, stdio: spinner ? 'ignore' : 'inherit' })
183
+ spinner?.stop('依賴安裝完成')
184
+ } catch {
185
+ spinner?.stop('依賴安裝失敗')
186
+ console.error(pc.yellow(`⚠ ${pm} install failed — run it manually inside ${dir}`))
187
+ }
188
+ }
189
+
190
+ // Next steps
191
+ const steps = [
192
+ `cd ${dir}`,
193
+ ...(args.install ? [] : [`${pm} install`]),
194
+ `填入 .env.local 的 Recur API keys (app.recur.tw → Settings → Developers)`,
195
+ `${pm} run dev`,
196
+ ]
197
+ if (interactive) {
198
+ p.note(steps.map((s, i) => `${i + 1}. ${s}`).join('\n'), '下一步')
199
+ p.outro(`完成!文件與客製化指南見 ${dir}/README.md 和 ${dir}/AGENTS.md`)
200
+ } else {
201
+ console.log('\nNext steps:')
202
+ steps.forEach((s, i) => console.log(` ${i + 1}. ${s}`))
203
+ console.log(`\nDocs: ${dir}/README.md · Agent guide: ${dir}/AGENTS.md`)
204
+ }
205
+ }
206
+
207
+ main().catch((error) => bail(error.message))
package/package.json CHANGED
@@ -1,18 +1,34 @@
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.2.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
  },
8
9
  "files": [
9
10
  "index.js"
10
11
  ],
11
- "keywords": ["recur", "create", "scaffold", "template", "subscription", "payments"],
12
+ "keywords": [
13
+ "recur",
14
+ "create",
15
+ "scaffold",
16
+ "template",
17
+ "subscription",
18
+ "payments"
19
+ ],
12
20
  "homepage": "https://github.com/recur-tw/templates",
13
21
  "repository": {
14
22
  "type": "git",
15
23
  "url": "git+https://github.com/recur-tw/templates.git"
16
24
  },
17
- "license": "MIT"
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=18.17"
28
+ },
29
+ "dependencies": {
30
+ "@clack/prompts": "^0.11.0",
31
+ "giget": "^2.0.0",
32
+ "picocolors": "^1.1.1"
33
+ }
18
34
  }