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.
- package/README.md +28 -3
- package/index.js +206 -2
- 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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
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
|
|
4
|
-
"description": "Scaffold a Recur-integrated app
|
|
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": [
|
|
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
|
}
|