openventuro 0.2.2

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 ADDED
@@ -0,0 +1,33 @@
1
+ # openventuro CLI
2
+
3
+ Scaffold a SaaS monorepo in one command.
4
+
5
+ Package name:
6
+
7
+ - `openventuro` (public npm package)
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ openventuro init [project-name] [options]
13
+ ```
14
+
15
+ ## Options
16
+
17
+ - `-d, --defaults`: use defaults (`openventuro-app`, `vercel`)
18
+ - `--deploy-target <target>`: `vercel` or `cloudflare-worker`
19
+ - `-h, --help`: show help
20
+
21
+ ## Examples
22
+
23
+ ```bash
24
+ openventuro init my-app
25
+ openventuro init my-app -d
26
+ openventuro init my-app --deploy-target cloudflare-worker
27
+ ```
28
+
29
+ ## Install and Run
30
+
31
+ ```bash
32
+ pnpm dlx openventuro@latest init my-app
33
+ ```
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env node
2
+ import { access, mkdir, readdir, writeFile } from 'node:fs/promises';
3
+ import { constants } from 'node:fs';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { createInterface } from 'node:readline/promises';
7
+ import { stdin as input, stdout as output } from 'node:process';
8
+
9
+ const rl = createInterface({ input, output });
10
+
11
+ const step = (message) => output.write(`- ${message}\n`);
12
+ const ok = (message) => output.write(`✔ ${message}\n`);
13
+ const fail = (message) => output.write(`✖ ${message}\n`);
14
+ const toPosix = (value) => value.replace(/\\/g, '/');
15
+
16
+ const usage = () => {
17
+ output.write('Usage: openventuro init [project-name] [options]\n\n');
18
+ output.write('Options:\n');
19
+ output.write(' -d, --defaults Use defaults (name + vercel)\n');
20
+ output.write(' --deploy-target <target> vercel | cloudflare-worker\n');
21
+ output.write(' -h, --help Show help\n');
22
+ };
23
+
24
+ const ask = async (question, fallback) => {
25
+ const answer = (await rl.question(question)).trim();
26
+ return answer.length ? answer : fallback;
27
+ };
28
+
29
+ const validateTarget = (value) => {
30
+ if (value === 'vercel' || value === 'cloudflare-worker') return value;
31
+ throw new Error(`Invalid deployment target: ${value}`);
32
+ };
33
+
34
+ const askTarget = async ({ defaults, argValue }) => {
35
+ if (argValue) return validateTarget(argValue);
36
+ if (defaults) return 'vercel';
37
+
38
+ output.write('\nWhere are we deploying?\n');
39
+ output.write('1) vercel\n');
40
+ output.write('2) cloudflare-worker\n\n');
41
+
42
+ while (true) {
43
+ const choice = (await rl.question('Select 1 or 2 (default: 1): ')).trim();
44
+ if (!choice || choice === '1') return 'vercel';
45
+ if (choice === '2') return 'cloudflare-worker';
46
+ output.write('Invalid choice. Please select 1 or 2.\n');
47
+ }
48
+ };
49
+
50
+ const files = (projectName, deployTarget) => ({
51
+ 'package.json': JSON.stringify(
52
+ {
53
+ name: projectName,
54
+ private: true,
55
+ packageManager: 'pnpm@10.6.0',
56
+ scripts: {
57
+ build: 'turbo run build',
58
+ dev: 'turbo run dev --parallel',
59
+ lint: 'turbo run lint',
60
+ typecheck: 'turbo run typecheck'
61
+ },
62
+ devDependencies: {
63
+ turbo: '^2.8.9',
64
+ typescript: '^5.9.3'
65
+ }
66
+ },
67
+ null,
68
+ 2
69
+ ) + '\n',
70
+ 'pnpm-workspace.yaml': 'packages:\n - "apps/*"\n - "packages/*"\n',
71
+ 'turbo.json': JSON.stringify(
72
+ {
73
+ $schema: 'https://turbo.build/schema.json',
74
+ tasks: {
75
+ build: { dependsOn: ['^build'], outputs: ['dist/**', '.next/**'] },
76
+ dev: { cache: false, persistent: true },
77
+ lint: { dependsOn: ['^lint'] },
78
+ typecheck: { dependsOn: ['^typecheck'] }
79
+ }
80
+ },
81
+ null,
82
+ 2
83
+ ) + '\n',
84
+ 'tsconfig.base.json': JSON.stringify(
85
+ {
86
+ compilerOptions: {
87
+ target: 'ES2022',
88
+ module: 'ESNext',
89
+ moduleResolution: 'Bundler',
90
+ strict: true,
91
+ skipLibCheck: true,
92
+ resolveJsonModule: true,
93
+ esModuleInterop: true,
94
+ forceConsistentCasingInFileNames: true,
95
+ isolatedModules: true,
96
+ noEmit: true,
97
+ baseUrl: '.'
98
+ }
99
+ },
100
+ null,
101
+ 2
102
+ ) + '\n',
103
+ '.gitignore': 'node_modules\n.pnpm-store\n.turbo\n.next\ndist\n.env\n.env.*\n.DS_Store\n',
104
+ '.env.example': [
105
+ `DEPLOY_TARGET=${deployTarget}`,
106
+ '',
107
+ 'CLOUDFLARE_API_TOKEN=',
108
+ 'CLOUDFLARE_ZONE_ID=',
109
+ 'STRIPE_SECRET_KEY=',
110
+ 'STRIPE_WEBHOOK_SECRET=',
111
+ 'RESEND_API_KEY=',
112
+ 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=',
113
+ 'CLERK_SECRET_KEY=',
114
+ 'DATABASE_URL='
115
+ ].join('\n') + '\n',
116
+ 'README.md': `# ${projectName}\n\nScaffolded with openventuro.\n\nThis scaffold already includes the shadcn base setup, so no extra shadcn init step is required.\n`,
117
+ 'scripts/cloud-init.yaml': '#cloud-config\npackages:\n - curl\n - git\nruncmd:\n - echo "Cloud init placeholder"\n',
118
+ 'apps/api/package.json': JSON.stringify(
119
+ {
120
+ name: '@openclaw/api',
121
+ private: true,
122
+ type: 'module',
123
+ scripts: {
124
+ dev: 'tsx watch src/index.ts',
125
+ build: 'tsc -p tsconfig.json',
126
+ typecheck: 'tsc -p tsconfig.json --noEmit'
127
+ },
128
+ dependencies: {
129
+ '@hono/node-server': '^1.19.9',
130
+ hono: '^4.11.9',
131
+ resend: '^4.8.0',
132
+ stripe: '^17.7.0'
133
+ },
134
+ devDependencies: {
135
+ '@types/node': '^22.19.11',
136
+ tsx: '^4.21.0',
137
+ typescript: '^5.9.3'
138
+ }
139
+ },
140
+ null,
141
+ 2
142
+ ) + '\n',
143
+ 'apps/api/tsconfig.json': JSON.stringify(
144
+ {
145
+ extends: '../../tsconfig.base.json',
146
+ compilerOptions: { outDir: 'dist', types: ['node'] },
147
+ include: ['src']
148
+ },
149
+ null,
150
+ 2
151
+ ) + '\n',
152
+ 'apps/api/src/index.ts': "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nconst app = new Hono();\n\napp.get('/', (c) => c.json({ ok: true, service: '@openclaw/api' }));\n\nconst port = Number(process.env.PORT ?? 8787);\nserve({ fetch: app.fetch, port });\n",
153
+ 'apps/Web/package.json': JSON.stringify(
154
+ {
155
+ name: '@openclaw/web',
156
+ private: true,
157
+ scripts: {
158
+ dev: 'next dev',
159
+ build: 'next build',
160
+ start: 'next start',
161
+ lint: 'next lint',
162
+ typecheck: 'tsc --noEmit'
163
+ },
164
+ dependencies: {
165
+ '@clerk/nextjs': '^6.37.5',
166
+ '@phosphor-icons/react': '^2.1.10',
167
+ '@tanstack/react-query': '^5.90.21',
168
+ 'class-variance-authority': '^0.7.1',
169
+ clsx: '^2.1.1',
170
+ 'framer-motion': '^12.34.1',
171
+ 'lucide-react': '^0.574.0',
172
+ next: '15.2.3',
173
+ react: '19.0.3',
174
+ 'react-dom': '19.0.3',
175
+ 'tailwind-merge': '^3.4.1',
176
+ 'tailwindcss-animate': '^1.0.7'
177
+ },
178
+ devDependencies: {
179
+ '@types/node': '^22.19.11',
180
+ '@types/react': '^19.2.14',
181
+ '@types/react-dom': '^19.2.3',
182
+ autoprefixer: '^10.4.24',
183
+ postcss: '^8.5.6',
184
+ tailwindcss: '^3.4.19',
185
+ typescript: '^5.9.3'
186
+ }
187
+ },
188
+ null,
189
+ 2
190
+ ) + '\n',
191
+ 'apps/Web/tsconfig.json': JSON.stringify(
192
+ {
193
+ extends: '../../tsconfig.base.json',
194
+ compilerOptions: {
195
+ lib: ['dom', 'dom.iterable', 'esnext'],
196
+ jsx: 'preserve',
197
+ allowJs: true,
198
+ incremental: true,
199
+ baseUrl: '.',
200
+ paths: { '@/*': ['./*'] },
201
+ types: ['node'],
202
+ plugins: [{ name: 'next' }]
203
+ },
204
+ include: ['next-env.d.ts', '**/*.ts', '**/*.tsx'],
205
+ exclude: ['node_modules']
206
+ },
207
+ null,
208
+ 2
209
+ ) + '\n',
210
+ 'apps/Web/next-env.d.ts': '/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n',
211
+ 'apps/Web/next.config.ts': "import type { NextConfig } from 'next';\n\nconst nextConfig: NextConfig = {};\n\nexport default nextConfig;\n",
212
+ 'apps/Web/postcss.config.js': "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {}\n }\n};\n",
213
+ 'apps/Web/tailwind.config.ts': "import type { Config } from 'tailwindcss';\n\nconst config: Config = {\n darkMode: ['class'],\n content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],\n theme: {\n extend: {\n borderRadius: {\n lg: 'var(--radius)',\n md: 'calc(var(--radius) - 2px)',\n sm: 'calc(var(--radius) - 4px)'\n },\n colors: {\n background: 'hsl(var(--background))',\n foreground: 'hsl(var(--foreground))',\n card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))' },\n popover: { DEFAULT: 'hsl(var(--popover))', foreground: 'hsl(var(--popover-foreground))' },\n primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' },\n secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))' },\n muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))' },\n accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))' },\n destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))' },\n border: 'hsl(var(--border))',\n input: 'hsl(var(--input))',\n ring: 'hsl(var(--ring))'\n }\n }\n },\n plugins: [require('tailwindcss-animate')]\n};\n\nexport default config;\n",
214
+ 'apps/Web/components.json': JSON.stringify(
215
+ {
216
+ $schema: 'https://ui.shadcn.com/schema.json',
217
+ style: 'new-york',
218
+ rsc: true,
219
+ tsx: true,
220
+ tailwind: {
221
+ config: 'tailwind.config.ts',
222
+ css: 'app/globals.css',
223
+ baseColor: 'neutral',
224
+ cssVariables: true,
225
+ prefix: ''
226
+ },
227
+ iconLibrary: 'lucide',
228
+ rtl: false,
229
+ aliases: {
230
+ components: '@/components',
231
+ utils: '@/lib/utils',
232
+ ui: '@/components/ui',
233
+ lib: '@/lib',
234
+ hooks: '@/hooks'
235
+ },
236
+ registries: {}
237
+ },
238
+ null,
239
+ 2
240
+ ) + '\n',
241
+ 'apps/Web/lib/utils.ts': "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n",
242
+ 'apps/Web/app/layout.tsx': "import './globals.css';\nimport type { Metadata } from 'next';\nimport { Geist, Geist_Mono } from 'next/font/google';\nimport type { ReactNode } from 'react';\n\nconst geistSans = Geist({ subsets: ['latin'], variable: '--font-geist-sans' });\nconst geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-geist-mono' });\n\nexport const metadata: Metadata = {\n title: 'OpenVenturo',\n description: 'Marketing base website'\n};\n\nexport default function RootLayout({ children }: { children: ReactNode }) {\n return (\n <html lang='en'>\n <body className={`${geistSans.variable} ${geistMono.variable}`}>{children}</body>\n </html>\n );\n}\n",
243
+ 'apps/Web/app/page.tsx': "export default function Home() {\n return (\n <main className='min-h-screen bg-slate-950 text-white'>\n <section className='mx-auto max-w-4xl px-6 py-24'>\n <p className='mb-4 text-sm uppercase tracking-[0.2em] text-cyan-300'>OpenVenturo</p>\n <h1 className='text-5xl font-semibold leading-tight'>Marketing website base is ready.</h1>\n <p className='mt-6 max-w-2xl text-lg text-slate-300'>\n The shadcn base setup is already included. Start adding UI components directly.\n </p>\n </section>\n </main>\n );\n}\n",
244
+ 'apps/Web/app/globals.css': "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n color-scheme: dark;\n}\n\nbody {\n margin: 0;\n font-family: var(--font-geist-sans), system-ui, sans-serif;\n background: #020617;\n}\n\n@layer base {\n :root {\n --background: 0 0% 100%;\n --foreground: 0 0% 3.9%;\n --card: 0 0% 100%;\n --card-foreground: 0 0% 3.9%;\n --popover: 0 0% 100%;\n --popover-foreground: 0 0% 3.9%;\n --primary: 0 0% 9%;\n --primary-foreground: 0 0% 98%;\n --secondary: 0 0% 96.1%;\n --secondary-foreground: 0 0% 9%;\n --muted: 0 0% 96.1%;\n --muted-foreground: 0 0% 45.1%;\n --accent: 0 0% 96.1%;\n --accent-foreground: 0 0% 9%;\n --destructive: 0 84.2% 60.2%;\n --destructive-foreground: 0 0% 98%;\n --border: 0 0% 89.8%;\n --input: 0 0% 89.8%;\n --ring: 0 0% 3.9%;\n --radius: 0.5rem;\n }\n\n .dark {\n --background: 0 0% 3.9%;\n --foreground: 0 0% 98%;\n --card: 0 0% 3.9%;\n --card-foreground: 0 0% 98%;\n --popover: 0 0% 3.9%;\n --popover-foreground: 0 0% 98%;\n --primary: 0 0% 98%;\n --primary-foreground: 0 0% 9%;\n --secondary: 0 0% 14.9%;\n --secondary-foreground: 0 0% 98%;\n --muted: 0 0% 14.9%;\n --muted-foreground: 0 0% 63.9%;\n --accent: 0 0% 14.9%;\n --accent-foreground: 0 0% 98%;\n --destructive: 0 62.8% 30.6%;\n --destructive-foreground: 0 0% 98%;\n --border: 0 0% 14.9%;\n --input: 0 0% 14.9%;\n --ring: 0 0% 83.1%;\n }\n}\n",
245
+ 'packages/shared/package.json': '{\n "name": "@openclaw/shared",\n "version": "0.0.0",\n "private": true\n}\n',
246
+ 'packages/shared/src/index.ts': 'export {};\n',
247
+ 'packages/i18n/package.json': '{\n "name": "@openclaw/i18n",\n "version": "0.0.0",\n "private": true\n}\n',
248
+ 'packages/i18n/src/index.ts': 'export {};\n',
249
+ 'packages/ui/package.json': '{\n "name": "@openclaw/ui",\n "version": "0.0.0",\n "private": true\n}\n',
250
+ 'packages/ui/src/index.ts': 'export {};\n',
251
+ 'packages/db/package.json': '{\n "name": "@openclaw/db",\n "version": "0.0.0",\n "private": true\n}\n',
252
+ 'packages/db/src/index.ts': 'export {};\n',
253
+ 'packages/trpc/package.json': '{\n "name": "@openclaw/trpc",\n "version": "0.0.0",\n "private": true\n}\n',
254
+ 'packages/trpc/src/index.ts': 'export {};\n'
255
+ });
256
+
257
+ const ensureEmptyOrMissing = async (targetDir) => {
258
+ try {
259
+ await access(targetDir, constants.F_OK);
260
+ } catch {
261
+ return;
262
+ }
263
+
264
+ const entries = await readdir(targetDir);
265
+ if (entries.length) throw new Error(`Target directory is not empty: ${targetDir}`);
266
+ };
267
+
268
+ const parseArgs = (argv) => {
269
+ let projectName;
270
+ let defaults = false;
271
+ let deployTarget;
272
+
273
+ for (let i = 0; i < argv.length; i += 1) {
274
+ const arg = argv[i];
275
+
276
+ if (arg === '-d' || arg === '--defaults' || arg === '--yes') {
277
+ defaults = true;
278
+ continue;
279
+ }
280
+
281
+ if (arg === '--deploy-target') {
282
+ const value = argv[i + 1];
283
+ if (!value) throw new Error('Missing value for --deploy-target');
284
+ deployTarget = value;
285
+ i += 1;
286
+ continue;
287
+ }
288
+
289
+ if (arg.startsWith('--deploy-target=')) {
290
+ deployTarget = arg.split('=')[1];
291
+ continue;
292
+ }
293
+
294
+ if (arg.startsWith('-')) {
295
+ throw new Error(`Unknown option: ${arg}`);
296
+ }
297
+
298
+ if (!projectName) {
299
+ projectName = arg;
300
+ continue;
301
+ }
302
+
303
+ throw new Error(`Unexpected argument: ${arg}`);
304
+ }
305
+
306
+ return { projectName, defaults, deployTarget };
307
+ };
308
+
309
+ const runInit = async (argv) => {
310
+ step('Preflight checks.');
311
+ const { projectName: argName, defaults, deployTarget: argTarget } = parseArgs(argv);
312
+
313
+ const projectName = argName || (defaults ? 'openventuro-app' : await ask('Project name (default: openventuro-app): ', 'openventuro-app'));
314
+ const deployTarget = await askTarget({ defaults, argValue: argTarget });
315
+
316
+ const cwd = process.cwd();
317
+ const targetDir = path.resolve(cwd, projectName);
318
+
319
+ await ensureEmptyOrMissing(targetDir);
320
+ ok('Preflight checks.');
321
+
322
+ step('Scaffolding project.');
323
+ await mkdir(targetDir, { recursive: true });
324
+
325
+ const scaffoldFiles = files(projectName, deployTarget);
326
+ for (const [relative, content] of Object.entries(scaffoldFiles)) {
327
+ const abs = path.join(targetDir, relative);
328
+ await mkdir(path.dirname(abs), { recursive: true });
329
+ await writeFile(abs, content, 'utf8');
330
+ }
331
+ ok('Scaffolding project.');
332
+ ok('Included shadcn base setup (no extra init required).');
333
+
334
+ const relative = path.relative(cwd, targetDir);
335
+ const displayPath = path.isAbsolute(projectName) ? toPosix(targetDir) : toPosix(relative || '.');
336
+
337
+ output.write(`\nSuccess! Project initialized in ${displayPath}\n`);
338
+ output.write('\nNext steps:\n');
339
+ output.write(` cd ${displayPath}\n`);
340
+ output.write(' pnpm install\n');
341
+ output.write(' pnpm dev\n\n');
342
+ output.write(`Deployment target selected: ${deployTarget}\n`);
343
+ };
344
+
345
+ const main = async () => {
346
+ const argv = process.argv.slice(2);
347
+
348
+ if (!argv.length) {
349
+ await runInit([]);
350
+ return;
351
+ }
352
+
353
+ if (argv[0] === '-h' || argv[0] === '--help') {
354
+ usage();
355
+ return;
356
+ }
357
+
358
+ if (argv[0] === 'init') {
359
+ await runInit(argv.slice(1));
360
+ return;
361
+ }
362
+
363
+ if (argv[0].startsWith('-')) {
364
+ usage();
365
+ throw new Error(`Unknown command or option: ${argv[0]}`);
366
+ }
367
+
368
+ await runInit(argv);
369
+ };
370
+
371
+ main()
372
+ .catch((error) => {
373
+ fail('Operation failed.');
374
+ console.error(`Error: ${error.message}`);
375
+ process.exitCode = 1;
376
+ })
377
+ .finally(() => {
378
+ rl.close();
379
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "openventuro",
3
+ "version": "0.2.2",
4
+ "description": "Scaffold a SaaS monorepo with Next.js, Hono, Turborepo, and shadcn-ready web app",
5
+ "type": "module",
6
+ "bin": {
7
+ "openventuro": "bin/openventuro.js"
8
+ },
9
+ "files": [
10
+ "bin"
11
+ ],
12
+ "keywords": [
13
+ "cli",
14
+ "scaffold",
15
+ "monorepo",
16
+ "nextjs",
17
+ "hono",
18
+ "turborepo",
19
+ "shadcn"
20
+ ],
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+ssh://git@github.com/kkasaei/openventuro.git"
25
+ },
26
+ "homepage": "https://github.com/kkasaei/openventuro",
27
+ "bugs": {
28
+ "url": "https://github.com/kkasaei/openventuro/issues"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org"
36
+ }
37
+ }