From 38a06bb9218e133faf0d24fdd99f8414d9b24dd2 Mon Sep 17 00:00:00 2001 From: Christian Bergschneider Date: Sat, 9 Mar 2024 23:10:44 +0000 Subject: [PATCH 1/4] feat(cli): add initial cli --- create/.gitignore | 1 + create/package.json | 32 ++++++++ create/src/dependencies.ts | 95 ++++++++++++++++++++++ create/src/index.ts | 39 +++++++++ create/src/installation_options.ts | 53 +++++++++++++ create/src/setup_project.ts | 35 ++++++++ create/src/utils.ts | 11 +++ create/tsconfig.json | 12 +++ pnpm-lock.yaml | 123 +++++++++++++++++++++++++---- pnpm-workspace.yaml | 1 + 10 files changed, 388 insertions(+), 14 deletions(-) create mode 100644 create/.gitignore create mode 100644 create/package.json create mode 100644 create/src/dependencies.ts create mode 100644 create/src/index.ts create mode 100644 create/src/installation_options.ts create mode 100644 create/src/setup_project.ts create mode 100644 create/src/utils.ts create mode 100644 create/tsconfig.json diff --git a/create/.gitignore b/create/.gitignore new file mode 100644 index 0000000..d163863 --- /dev/null +++ b/create/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/create/package.json b/create/package.json new file mode 100644 index 0000000..3ad7cdc --- /dev/null +++ b/create/package.json @@ -0,0 +1,32 @@ +{ + "name": "@kfs/create", + "version": "0.0.1", + "private": false, + "description": "", + "bin": "build/index.js", + "scripts": { + "run": "tsc && node build/index.js", + "prepublishOnly": "tsc" + }, + "keywords": [], + "license": "MIT", + "type": "module", + "devDependencies": { + "@types/node": "^20.6.0", + "typescript": "^5.2.2" + }, + "files": [ + "src", + "build" + ], + "dependencies": { + "@clack/prompts": "^0.7.0", + "@types/command-exists": "^1.2.1", + "@types/shelljs": "^0.8.12", + "@types/degit": "^2.8.6", + "chalk": "^5.3.0", + "command-exists": "^1.2.9", + "shelljs": "^0.8.5", + "degit": "^2.8.4" + } +} diff --git a/create/src/dependencies.ts b/create/src/dependencies.ts new file mode 100644 index 0000000..d5b09dd --- /dev/null +++ b/create/src/dependencies.ts @@ -0,0 +1,95 @@ +import chalk from 'chalk'; +import commandExists from "command-exists"; +import * as p from '@clack/prompts'; + +async function check_command(command: string) { + return await commandExists(command).then(() => true).catch(() => false); +} + +export type FeatureConfigurationFlag = "core" | "create_git_repo"; + +type Dependency = { + name: string; + message: string; + error: string; + check: () => Promise; + required_flags: FeatureConfigurationFlag[]; + docker_feature: boolean; +} + +const dependencies: Dependency[] = [ + { + name: "git", + message: "Git is installed", + error: "Git is not installed", + check: () => check_command('git'), + required_flags: ["create_git_repo"], + docker_feature: false, + }, + { + name: "pnpm", + message: "PNPM is installed", + error: "PNPM is not installed", + check: () => check_command('pnpm'), + required_flags: ["core"], + docker_feature: false, + }, + { + name: "node", + message: "Node.js version v18 or later is installed", + error: "Node.js version v18 or later is not installed", + check: async () => { + let node_version = process.version.split('.')[0].replace('v', ''); + return parseInt(node_version) >= 18; + }, + required_flags: ["core"], + docker_feature: false, + }, + { + name: "docker", + message: "Docker is installed", + error: "Docker is not installed", + check: () => check_command('docker'), + required_flags: [], + docker_feature: true, + }, + { + name: "docker-compose", + message: "Docker Compose is installed", + error: "Docker Compose is not installed", + check: () => check_command('docker-compose'), + required_flags: [], + docker_feature: true, + } +] + +export type DockerSupported = boolean; + +export async function check_dependencies(flags: FeatureConfigurationFlag[]): Promise { + let had_error = false; + let docker_supported = true; + + for (let dependency of dependencies) { + if (await dependency.check()) { + p.log.info(dependency.message); + } else { + if (dependency.required_flags.some(flag => flags.includes(flag))) { + had_error = true; + p.log.error(dependency.error); + } else { + p.log.error(dependency.error + chalk.gray(" (optional)")); + } + + if (dependency.docker_feature) { + docker_supported = false; + } + } + } + + if (had_error) { + p.cancel("Please install the missing dependencies and try again"); + process.exit(1); + } + + return docker_supported; +} \ No newline at end of file diff --git a/create/src/index.ts b/create/src/index.ts new file mode 100644 index 0000000..5040750 --- /dev/null +++ b/create/src/index.ts @@ -0,0 +1,39 @@ +#!/usr/bin/env node +import chalk from 'chalk'; +import * as p from '@clack/prompts'; +import {get_options} from "./installation_options.js"; +import {createGitRepo, setupProject} from "./setup_project.js"; + +const version = "1.0.0"; + +console.log(); +console.log(chalk.gray('@kfs/create version ' + version)); +console.log(); + +p.intro('Welcome to KitForStartups!'); + +let { + working_directory, + create_git_repo, + docker_supported, + setup_env, +} = await get_options() + +await setupProject(working_directory) + +if (create_git_repo) await createGitRepo(working_directory) + +if (working_directory === '.') { + p.outro(`Your project is ready! To get started: + - pnpm install + +Happy coding!` + ); +} else { + p.outro(`Your project is ready! To get started: + - cd ${working_directory} + - pnpm install + +Happy coding!` + ); +} diff --git a/create/src/installation_options.ts b/create/src/installation_options.ts new file mode 100644 index 0000000..9703610 --- /dev/null +++ b/create/src/installation_options.ts @@ -0,0 +1,53 @@ +import * as p from "@clack/prompts"; +import fs from "fs"; +import {unwrap_cancellation} from "./utils.js"; +import {check_dependencies} from "./dependencies.js"; + +function expect_empty_directory(working_directory: string) { + if (fs.existsSync(working_directory) && fs.readdirSync(working_directory).length > 0) { + p.log.error("Directory " + working_directory + " is not empty. Can't continue"); + process.exit(1); + } +} + + +export type InstallOptions = { + working_directory: string; + create_git_repo: boolean; + docker_supported: boolean; + setup_env: boolean; +} + +export async function get_options(): Promise { + let working_directory = process.argv[2] || '.'; + + if (working_directory === '.') { + const dir = unwrap_cancellation(await p.text({ + message: 'Where should your project be created?', + placeholder: ' (hit Enter to use current directory)' + })); + + working_directory = dir || '.'; + } + + expect_empty_directory(working_directory); + + let create_git_repo = unwrap_cancellation(await p.confirm({ + message: 'Do you want to create a Git repository?', + initialValue: true + })); + + let setup_env = unwrap_cancellation(await p.confirm({ + message: 'Do you want to initialize a development environment?', + initialValue: true + })); + + let docker_supported = await check_dependencies(create_git_repo ? ["core", "create_git_repo"]: ["core"]) + + return { + working_directory, + create_git_repo, + docker_supported, + setup_env + } +} \ No newline at end of file diff --git a/create/src/setup_project.ts b/create/src/setup_project.ts new file mode 100644 index 0000000..f72d9cd --- /dev/null +++ b/create/src/setup_project.ts @@ -0,0 +1,35 @@ +import degit from 'degit'; +import * as p from '@clack/prompts'; +import shelljs from 'shelljs'; +import * as fs from 'fs'; + +export async function setupProject(cwd: string) { + p.log.step("Creating project in " + cwd); + // make sure the directory exists + if (!fs.existsSync(cwd)) { + fs.mkdirSync(cwd); + } + // create the project directory + fs.mkdirSync(cwd, { recursive: true }); + let original_cwd = process.cwd(); + const emitter = degit("okupter/kitforstartups/starter", { + cache: false, + force: true, + verbose: false, + }); + + emitter.on('warn', warning => { + p.log.warn(warning.message); + }); + + await emitter.clone(cwd).then(() => { + shelljs.cd(original_cwd); + }); +} + +export async function createGitRepo(cwd: string) { + p.log.step("Creating Git repository in " + cwd); + shelljs.cd(cwd); + shelljs.exec('git init', { silent: true }); + shelljs.exec('git add .', { silent: true }); +} \ No newline at end of file diff --git a/create/src/utils.ts b/create/src/utils.ts new file mode 100644 index 0000000..be65cb0 --- /dev/null +++ b/create/src/utils.ts @@ -0,0 +1,11 @@ +import {isCancel} from "@clack/prompts"; +import * as p from "@clack/prompts"; + +export function unwrap_cancellation(data: T | symbol) { + if (isCancel(data)) { + p.log.error("Cancelled by user. Exiting..."); + process.exit(1); + } + + return data as T; +} diff --git a/create/tsconfig.json b/create/tsconfig.json new file mode 100644 index 0000000..96ad29c --- /dev/null +++ b/create/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./build", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 522206f..db57d30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,40 @@ settings: importers: + create: + dependencies: + '@clack/prompts': + specifier: ^0.7.0 + version: 0.7.0 + '@types/command-exists': + specifier: ^1.2.1 + version: 1.2.3 + '@types/degit': + specifier: ^2.8.6 + version: 2.8.6 + '@types/shelljs': + specifier: ^0.8.12 + version: 0.8.15 + chalk: + specifier: ^5.3.0 + version: 5.3.0 + command-exists: + specifier: ^1.2.9 + version: 1.2.9 + degit: + specifier: ^2.8.4 + version: 2.8.4 + shelljs: + specifier: ^0.8.5 + version: 0.8.5 + devDependencies: + '@types/node': + specifier: ^20.6.0 + version: 20.8.2 + typescript: + specifier: ^5.2.2 + version: 5.2.2 + starter: dependencies: '@fontsource-variable/inter': @@ -152,6 +186,23 @@ packages: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.19 + /@clack/core@0.3.4: + resolution: {integrity: sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==} + dependencies: + picocolors: 1.0.0 + sisteransi: 1.0.5 + dev: false + + /@clack/prompts@0.7.0: + resolution: {integrity: sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==} + dependencies: + '@clack/core': 0.3.4 + picocolors: 1.0.0 + sisteransi: 1.0.5 + dev: false + bundledDependencies: + - is-unicode-supported + /@drizzle-team/studio@0.0.5: resolution: {integrity: sha512-ps5qF0tMxWRVu+V5gvCRrQNqlY92aTnIKdq27gm9LZMSdaKYZt6AVvSK1dlUMzs6Rt0Jm80b+eWct6xShBKhIw==} dev: true @@ -1016,13 +1067,28 @@ packages: resolution: {integrity: sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg==} dev: false + /@types/command-exists@1.2.3: + resolution: {integrity: sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==} + dev: false + /@types/cookie@0.5.2: resolution: {integrity: sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA==} dev: true + /@types/degit@2.8.6: + resolution: {integrity: sha512-y0M7sqzsnHB6cvAeTCBPrCQNQiZe8U4qdzf8uBVmOWYap5MMTN/gB2iEqrIqFiYcsyvP74GnGD5tgsHttielFw==} + dev: false + /@types/estree@1.0.2: resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==} + /@types/glob@7.2.0: + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 20.8.2 + dev: false + /@types/json-schema@7.0.13: resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} dev: true @@ -1033,6 +1099,10 @@ packages: '@types/node': 20.8.2 dev: false + /@types/minimatch@5.1.2: + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + dev: false + /@types/node-fetch@2.6.6: resolution: {integrity: sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==} dependencies: @@ -1058,6 +1128,13 @@ packages: resolution: {integrity: sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==} dev: true + /@types/shelljs@0.8.15: + resolution: {integrity: sha512-vzmnCHl6hViPu9GNLQJ+DZFd6BQI2DBTUeOvYHqkWQLMfKAAQYMb/xAmZkTogZI/vqXHCWkqDRymDI5p0QTi5Q==} + dependencies: + '@types/glob': 7.2.0 + '@types/node': 20.8.2 + dev: false + /@types/ws@8.5.6: resolution: {integrity: sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg==} dependencies: @@ -1342,7 +1419,6 @@ packages: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true /brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -1420,7 +1496,6 @@ packages: /chalk@5.3.0: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: true /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} @@ -1484,6 +1559,10 @@ packages: delayed-stream: 1.0.0 dev: false + /command-exists@1.2.9: + resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} + dev: false + /commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -1501,7 +1580,6 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true /condense-newlines@0.2.1: resolution: {integrity: sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==} @@ -1590,6 +1668,12 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + /degit@2.8.4: + resolution: {integrity: sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng==} + engines: {node: '>=8.0.0'} + hasBin: true + dev: false + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2205,7 +2289,6 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: true /generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} @@ -2257,7 +2340,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} @@ -2321,7 +2403,6 @@ packages: engines: {node: '>= 0.4.0'} dependencies: function-bind: 1.1.1 - dev: true /heap@0.2.7: resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} @@ -2398,6 +2479,11 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: false + /interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + dev: false + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -2413,7 +2499,6 @@ packages: resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} dependencies: has: 1.0.3 - dev: true /is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} @@ -2772,7 +2857,6 @@ packages: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 - dev: true /minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} @@ -3014,7 +3098,6 @@ packages: /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - dev: true /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} @@ -3023,7 +3106,6 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -3066,7 +3148,6 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -3331,6 +3412,13 @@ packages: picomatch: 2.3.1 dev: true + /rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + dependencies: + resolve: 1.22.6 + dev: false + /resend@1.1.0: resolution: {integrity: sha512-it8TIDVT+/gAiJsUlv2tdHuvzwCCv4Zwu+udDqIm/dIuByQwe68TtFDcPccxqpSVVrNCBxxXLzsdT1tsV+P3GA==} engines: {node: '>=18'} @@ -3355,7 +3443,6 @@ packages: is-core-module: 2.13.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} @@ -3453,6 +3540,16 @@ packages: engines: {node: '>=8'} dev: true + /shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + dev: false + /simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} dev: false @@ -3476,7 +3573,6 @@ packages: /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - dev: true /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} @@ -3568,7 +3664,6 @@ packages: /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true /svelte-check@3.5.2(postcss-load-config@4.0.1)(postcss@8.4.31)(svelte@4.2.1): resolution: {integrity: sha512-5a/YWbiH4c+AqAUP+0VneiV5bP8YOk9JL3jwvN+k2PEPLgpu85bjQc5eE67+eIZBBwUEJzmO3I92OqKcqbp3fw==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a497b6a..d15b9d7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - "starter" - "docs" + - "create" From 73b439f9eb9ffc97184a3a44698b45978d484717 Mon Sep 17 00:00:00 2001 From: Christian Bergschneider Date: Sun, 10 Mar 2024 00:11:46 +0000 Subject: [PATCH 2/4] feat(cli): choose database --- create/src/database.ts | 68 +++++++++++++++++++++++++ create/src/environment.ts | 102 ++++++++++++++++++++++++++++++++++++++ create/src/index.ts | 6 +++ 3 files changed, 176 insertions(+) create mode 100644 create/src/database.ts create mode 100644 create/src/environment.ts diff --git a/create/src/database.ts b/create/src/database.ts new file mode 100644 index 0000000..bf34365 --- /dev/null +++ b/create/src/database.ts @@ -0,0 +1,68 @@ +import * as p from '@clack/prompts'; +import * as fs from 'fs/promises'; +import {unwrap_cancellation} from "./utils.js"; + +export type PrimaryDatabase = "MySQL" | "Postgres" | "Turso" | null; + +async function replace_recursively(pwd: string, old_name: string, new_name: string) { + let files = await fs.readdir(pwd, {withFileTypes: true}); + for (let file of files) { + if (file.isDirectory()) { + await replace_recursively(pwd + "/" + file.name, old_name, new_name); + } else { + let content = await fs.readFile(pwd + "/" + file.name, "utf-8"); + content = content.replace(old_name, new_name); + await fs.writeFile(pwd + "/" + file.name, content); + } + } +} + +async function select_lucia_module(new_name: string) { + // print current cwd (not argument) + await replace_recursively( "src", "$lib/lucia/mysql", "$lib/lucia/" + new_name); +} + +async function select_database_driver(new_name: string) { + await replace_recursively("src", "$lib/drizzle/mysql/models", "$lib/drizzle/" + new_name + "/models"); +} + +export async function configureDatabase(): Promise { + let db = unwrap_cancellation(await p.select({ + message: "Primary Database", + options: [ + { + value: "MySQL", + label: "MySQL", + hint: "Simpler, faster for read-heavy workloads" + }, + { + value: "Postgres", + label: "Postgres", + hint: "Feature-rich, better for complex queries, supports advanced data types" + }, + { + value: "Turso", + label: "Turso", + hint: "Online database platform" + }, + { + value: "None", + label: "None", + hint: "I'll configure the database later." + } + ], + initialValue: "MySQL" + })); + if (db === "None") return null; + + if (db === "Postgres") { + await select_database_driver("postgres") + await select_lucia_module("postgres") + } + if (db === "Turso") { + await select_database_driver("turso") + await select_lucia_module("turso") + } + + return db as PrimaryDatabase; +} \ No newline at end of file diff --git a/create/src/environment.ts b/create/src/environment.ts new file mode 100644 index 0000000..45cfc46 --- /dev/null +++ b/create/src/environment.ts @@ -0,0 +1,102 @@ +import {PrimaryDatabase} from "./database.js"; + +type DynamicConfigBuilder = { + name: string, + depth: number + configure: (has_docker: boolean) => Promise, + default_env: string[], + default_enabled: boolean +} + +type ConfigBuilder = DynamicConfigBuilder | string[] + +const environmentConfigBuilder: ConfigBuilder[] = [ + [ + "# DATABASE", + "ENABLE_DRIZZLE_LOGGER=true", + ], + { + name: "MySQL Database", + depth: 2, + configure: async (has_docker: boolean) => { return [] }, + default_env: [ + "MYSQL_DB_HOST=", + "MYSQL_DB_PORT=", + "MYSQL_DB_USER=", + "MYSQL_DB_PASSWORD=", + "MYSQL_DB_NAME=", + ], + default_enabled: false + }, + { + name: "Postgres Database", + depth: 2, + configure: async (has_docker: boolean) => { return [] }, + default_env: [ + "POSTGRES_DB_HOST=", + "POSTGRES_DB_PORT=", + "POSTGRES_DB_USER=", + "POSTGRES_DB_PASSWORD=", + "POSTGRES_DB_NAME=", + "POSTGRES_MAX_CONNECTIONS=", + ], + default_enabled: false + }, + { + name: "Turso Database", + depth: 2, + configure: async (has_docker: boolean) => { return [] }, + default_env: [ + "TURSO_DB_URL=", + "TURSO_AUTH_TOKEN=", + ], + default_enabled: false + }, + [ + "# OAUTH", + ], + { + name: "GitHub OAuth", + depth: 1, + configure: async (has_docker: boolean) => { return [] }, + default_env: [ + "GITHUB_CLIENT_ID=", + "GITHUB_CLIENT_SECRET=", + ], + default_enabled: false + }, + { + name: "Google OAuth", + depth: 1, + configure: async (has_docker: boolean) => { return [] }, + default_env: [ + "GOOGLE_OAUTH_CLIENT_ID=", + "GOOGLE_OAUTH_CLIENT_SECRET=", + "GOOGLE_OAUTH_REDIRECT_URI=", + ], + default_enabled: false + }, + { + name: "Resend Email API", + depth: 1, + configure: async (has_docker: boolean) => { return [] }, + default_env: [ + "RESEND_API_KEY=" + ], + default_enabled: false + }, + { + name: "Emails", + depth: 1, + configure: async (has_docker: boolean) => { return [] }, + default_env: [ + "TRANSACTIONAL_EMAILS_SENDER=", + "TRANSACTIONAL_EMAILS_ADDRESS=", + ], + default_enabled: true + } +] + +export async function configureEnvironment(docker_supported: boolean, primary_db: PrimaryDatabase) { + +} \ No newline at end of file diff --git a/create/src/index.ts b/create/src/index.ts index 5040750..dc6b91b 100644 --- a/create/src/index.ts +++ b/create/src/index.ts @@ -3,6 +3,8 @@ import chalk from 'chalk'; import * as p from '@clack/prompts'; import {get_options} from "./installation_options.js"; import {createGitRepo, setupProject} from "./setup_project.js"; +import {configureEnvironment} from "./environment.js"; +import {configureDatabase} from "./database.js"; const version = "1.0.0"; @@ -22,6 +24,10 @@ let { await setupProject(working_directory) if (create_git_repo) await createGitRepo(working_directory) +if (setup_env) { + let database = await configureDatabase() + await configureEnvironment(docker_supported, database) +} if (working_directory === '.') { p.outro(`Your project is ready! To get started: From d1e7768df004601108d717933586cb82a98d239f Mon Sep 17 00:00:00 2001 From: Christian Bergschneider Date: Sun, 10 Mar 2024 11:05:54 +0000 Subject: [PATCH 3/4] feat(cli): configure environment --- create/.gitignore | 3 +- create/package.json | 2 +- create/src/environment.ts | 175 +++++++++--------- create/src/environment/email_builder.ts | 30 +++ .../src/environment/github_oauth_builder.ts | 28 +++ .../src/environment/google_oauth_builder.ts | 34 ++++ create/src/environment/mysql_builder.ts | 56 ++++++ create/src/environment/postgres_builder.ts | 64 +++++++ create/src/environment/resend_builder.ts | 22 +++ create/src/environment/turso_builder.ts | 29 +++ create/src/environment/utils.ts | 48 +++++ 11 files changed, 404 insertions(+), 87 deletions(-) create mode 100644 create/src/environment/email_builder.ts create mode 100644 create/src/environment/github_oauth_builder.ts create mode 100644 create/src/environment/google_oauth_builder.ts create mode 100644 create/src/environment/mysql_builder.ts create mode 100644 create/src/environment/postgres_builder.ts create mode 100644 create/src/environment/resend_builder.ts create mode 100644 create/src/environment/turso_builder.ts create mode 100644 create/src/environment/utils.ts diff --git a/create/.gitignore b/create/.gitignore index d163863..e1e1372 100644 --- a/create/.gitignore +++ b/create/.gitignore @@ -1 +1,2 @@ -build/ \ No newline at end of file +build/ +test-module/ \ No newline at end of file diff --git a/create/package.json b/create/package.json index 3ad7cdc..7a16b2d 100644 --- a/create/package.json +++ b/create/package.json @@ -5,7 +5,7 @@ "description": "", "bin": "build/index.js", "scripts": { - "run": "tsc && node build/index.js", + "cli": "rm -rf test-module && tsc && node build/index.js test-module", "prepublishOnly": "tsc" }, "keywords": [], diff --git a/create/src/environment.ts b/create/src/environment.ts index 45cfc46..91f559e 100644 --- a/create/src/environment.ts +++ b/create/src/environment.ts @@ -1,12 +1,15 @@ +import * as p from '@clack/prompts'; import {PrimaryDatabase} from "./database.js"; - -type DynamicConfigBuilder = { - name: string, - depth: number - configure: (has_docker: boolean) => Promise, - default_env: string[], - default_enabled: boolean -} +import {unwrap_cancellation} from "./utils.js"; +import fs from "fs/promises"; +import {DynamicConfigBuilder} from "./environment/utils.js"; +import mysql_builder from "./environment/mysql_builder.js"; +import postgres_builder from "./environment/postgres_builder.js"; +import turso_builder from "./environment/turso_builder.js"; +import github_oauth_builder from "./environment/github_oauth_builder.js"; +import google_oauth_builder from "./environment/google_oauth_builder.js"; +import resend_builder from "./environment/resend_builder.js"; +import email_builder from "./environment/email_builder.js"; type ConfigBuilder = DynamicConfigBuilder | string[] @@ -15,88 +18,90 @@ const environmentConfigBuilder: ConfigBuilder[] = [ "# DATABASE", "ENABLE_DRIZZLE_LOGGER=true", ], - { - name: "MySQL Database", - depth: 2, - configure: async (has_docker: boolean) => { return [] }, - default_env: [ - "MYSQL_DB_HOST=", - "MYSQL_DB_PORT=", - "MYSQL_DB_USER=", - "MYSQL_DB_PASSWORD=", - "MYSQL_DB_NAME=", - ], - default_enabled: false - }, - { - name: "Postgres Database", - depth: 2, - configure: async (has_docker: boolean) => { return [] }, - default_env: [ - "POSTGRES_DB_HOST=", - "POSTGRES_DB_PORT=", - "POSTGRES_DB_USER=", - "POSTGRES_DB_PASSWORD=", - "POSTGRES_DB_NAME=", - "POSTGRES_MAX_CONNECTIONS=", - ], - default_enabled: false - }, - { - name: "Turso Database", - depth: 2, - configure: async (has_docker: boolean) => { return [] }, - default_env: [ - "TURSO_DB_URL=", - "TURSO_AUTH_TOKEN=", - ], - default_enabled: false - }, + mysql_builder, + postgres_builder, + turso_builder, [ "# OAUTH", ], - { - name: "GitHub OAuth", - depth: 1, - configure: async (has_docker: boolean) => { return [] }, - default_env: [ - "GITHUB_CLIENT_ID=", - "GITHUB_CLIENT_SECRET=", - ], - default_enabled: false - }, - { - name: "Google OAuth", - depth: 1, - configure: async (has_docker: boolean) => { return [] }, - default_env: [ - "GOOGLE_OAUTH_CLIENT_ID=", - "GOOGLE_OAUTH_CLIENT_SECRET=", - "GOOGLE_OAUTH_REDIRECT_URI=", - ], - default_enabled: false - }, - { - name: "Resend Email API", - depth: 1, - configure: async (has_docker: boolean) => { return [] }, - default_env: [ - "RESEND_API_KEY=" - ], - default_enabled: false - }, - { - name: "Emails", - depth: 1, - configure: async (has_docker: boolean) => { return [] }, - default_env: [ - "TRANSACTIONAL_EMAILS_SENDER=", - "TRANSACTIONAL_EMAILS_ADDRESS=", - ], - default_enabled: true - } + github_oauth_builder, + google_oauth_builder, + resend_builder, + email_builder ] export async function configureEnvironment(docker_supported: boolean, primary_db: PrimaryDatabase) { + // Let the user select which environment variables to configure + let sections: { + label: string, + hint: string + }[] = environmentConfigBuilder + .filter((builder: ConfigBuilder) => !Array.isArray(builder)) // Only dynamic builders + .map((builder: ConfigBuilder) => builder as DynamicConfigBuilder) + .filter((builder: DynamicConfigBuilder) => { + if (docker_supported && builder.required_for_database) { + // Hide the database that is already configured + if (builder.required_for_database === primary_db) { + p.log.message(`Skipping ${builder.name} because it'll be configured automatically.`) + return false + } + } + + return true + }) + .map((builder: DynamicConfigBuilder) => ({ + label: builder.name, + hint: builder.hint || "" + })) + + let result: string[] = unwrap_cancellation(await p.multiselect({ + message: "What parts of the environment would you like to configure? You'll have to configure the rest later in your .env file.", + required: false, + options: sections.map((section) => ({label: section.label, value: section.label, hint: section.hint})), + })); + + let environment_sections: ConfigBuilder[] = environmentConfigBuilder + .map((builder: ConfigBuilder) => { + if (Array.isArray(builder)) return builder + let dynamic_builder = builder as DynamicConfigBuilder + + // If the selection is the primary database + if (dynamic_builder.required_for_database === primary_db) { + if (dynamic_builder.autoconfigure) return { + ...dynamic_builder, + configure: dynamic_builder.autoconfigure, + } + + return dynamic_builder + } + + // If the section is not selected, return the default environment variables + if (!result.includes(dynamic_builder.name)) { + let header = "#".repeat(dynamic_builder.depth) + " " + dynamic_builder.name + return [header, ...dynamic_builder.default_env] + } + return dynamic_builder + }) + + let env: string[] = [] + for (let section of environment_sections) { + if (Array.isArray(section)) { + env.push(...section) + env.push("") + } else { + let dynamic_builder = section as DynamicConfigBuilder + + let header = "#".repeat(dynamic_builder.depth) + " " + dynamic_builder.name + env.push(header) + + let configured_env = await dynamic_builder.configure() + env.push(...configured_env) + env.push("") + } + } + + let file_contents = env.join("\n") + // Write the environment variables to the .env file + await fs.writeFile(".env", file_contents, { encoding: "utf-8" }) } \ No newline at end of file diff --git a/create/src/environment/email_builder.ts b/create/src/environment/email_builder.ts new file mode 100644 index 0000000..41985db --- /dev/null +++ b/create/src/environment/email_builder.ts @@ -0,0 +1,30 @@ +import {askEnvironmentConfig, DynamicConfigBuilder} from "./utils.js"; + +const builder: DynamicConfigBuilder = { + name: "Emails", + depth: 1, + configure: () => { + return askEnvironmentConfig([ + { + name: "Email Sender Name", + hint: "The name that will appear as the sender of the emails", + key: "TRANSACTIONAL_EMAILS_SENDER", + default: "KitForStartups" + }, + { + name: "Email Address", + hint: "The email address that will be used to send the emails", + key: "TRANSACTIONAL_EMAILS_ADDRESS", + default: "no-reply@kitforstartups.com" + }, + ]) + }, + default_env: [ + "TRANSACTIONAL_EMAILS_SENDER=", + "TRANSACTIONAL_EMAILS_ADDRESS=", + ], + default_enabled: true, + hint: "Transactional emails are sent from this address" +}; + +export default builder; \ No newline at end of file diff --git a/create/src/environment/github_oauth_builder.ts b/create/src/environment/github_oauth_builder.ts new file mode 100644 index 0000000..9a07739 --- /dev/null +++ b/create/src/environment/github_oauth_builder.ts @@ -0,0 +1,28 @@ +import {askEnvironmentConfig, DynamicConfigBuilder} from "./utils.js"; + +const builder: DynamicConfigBuilder = { + name: "GitHub OAuth", + depth: 1, + configure: () => { + return askEnvironmentConfig([ + { + name: "GitHub Client ID", + hint: "The client ID of the GitHub OAuth application", + key: "GITHUB_CLIENT_ID", + }, + { + name: "GitHub Client Secret", + hint: "The client secret of the GitHub OAuth application", + key: "GITHUB_CLIENT_SECRET", + }, + ]) + }, + default_env: [ + "GITHUB_CLIENT_ID=", + "GITHUB_CLIENT_SECRET=", + ], + default_enabled: false, + hint: "This is used to authenticate users with GitHub" +}; + +export default builder; \ No newline at end of file diff --git a/create/src/environment/google_oauth_builder.ts b/create/src/environment/google_oauth_builder.ts new file mode 100644 index 0000000..4184539 --- /dev/null +++ b/create/src/environment/google_oauth_builder.ts @@ -0,0 +1,34 @@ +import {askEnvironmentConfig, DynamicConfigBuilder} from "./utils.js"; + +const builder: DynamicConfigBuilder = { + name: "Google OAuth", + depth: 1, + configure: () => { + return askEnvironmentConfig([ + { + key: "GOOGLE_OAUTH_CLIENT_ID", + name: "Google OAuth Client ID", + hint: "This is the client ID for Google OAuth" + }, + { + key: "GOOGLE_OAUTH_CLIENT_SECRET", + name: "Google OAuth Client Secret", + hint: "This is the client secret for Google OAuth" + }, + { + key: "GOOGLE_OAUTH_REDIRECT_URI", + name: "Google OAuth Redirect URI", + hint: "This is the redirect URI for Google OAuth" + } + ]) + }, + default_env: [ + "GOOGLE_OAUTH_CLIENT_ID=", + "GOOGLE_OAUTH_CLIENT_SECRET=", + "GOOGLE_OAUTH_REDIRECT_URI=", + ], + default_enabled: false, + hint: "This is used to authenticate users with Google" +}; + +export default builder; \ No newline at end of file diff --git a/create/src/environment/mysql_builder.ts b/create/src/environment/mysql_builder.ts new file mode 100644 index 0000000..7d3001c --- /dev/null +++ b/create/src/environment/mysql_builder.ts @@ -0,0 +1,56 @@ +import {askEnvironmentConfig, DynamicConfigBuilder} from "./utils.js"; + +const builder: DynamicConfigBuilder = { + name: "MySQL Database", + depth: 2, + configure: () => { + return askEnvironmentConfig([ + { + key: "MYSQL_DB_HOST", + name: "MySQL Database Host", + hint: "This is the host for the MySQL database" + }, + { + key: "MYSQL_DB_PORT", + name: "MySQL Database Port", + hint: "This is the port for the MySQL database" + }, + { + key: "MYSQL_DB_USER", + name: "MySQL Database User", + hint: "This is the user for the MySQL database" + }, + { + key: "MYSQL_DB_PASSWORD", + name: "MySQL Database Password", + hint: "This is the password for the MySQL database" + }, + { + key: "MYSQL_DB_NAME", + name: "MySQL Database Name", + hint: "This is the name of the MySQL database" + } + ]) + }, + default_env: [ + "MYSQL_DB_HOST=", + "MYSQL_DB_PORT=", + "MYSQL_DB_USER=", + "MYSQL_DB_PASSWORD=", + "MYSQL_DB_NAME=", + ], + default_enabled: false, + required_for_database: "MySQL", + hint: "Configure MySQL as a secondary database.", + autoconfigure: async () => { + return [ + "MYSQL_DB_HOST=localhost", + "MYSQL_DB_PORT=3306", + "MYSQL_DB_USER=root", + "MYSQL_DB_PASSWORD=password", + "MYSQL_DB_NAME=kitforstartups-mysql" + ] + } +}; + +export default builder; \ No newline at end of file diff --git a/create/src/environment/postgres_builder.ts b/create/src/environment/postgres_builder.ts new file mode 100644 index 0000000..ad0d9d5 --- /dev/null +++ b/create/src/environment/postgres_builder.ts @@ -0,0 +1,64 @@ +import {askEnvironmentConfig, DynamicConfigBuilder} from "./utils.js"; + +const builder: DynamicConfigBuilder = { + name: "Postgres Database", + depth: 2, + configure: () => { + return askEnvironmentConfig([ + { + key: "POSTGRES_DB_HOST", + name: "Postgres Database Host", + hint: "This is the host for the Postgres database" + }, + { + key: "POSTGRES_DB_PORT", + name: "Postgres Database Port", + hint: "This is the port for the Postgres database" + }, + { + key: "POSTGRES_DB_USER", + name: "Postgres Database User", + hint: "This is the user for the Postgres database" + }, + { + key: "POSTGRES_DB_PASSWORD", + name: "Postgres Database Password", + hint: "This is the password for the Postgres database" + }, + { + key: "POSTGRES_DB_NAME", + name: "Postgres Database Name", + hint: "This is the name of the Postgres database" + }, + { + key: "POSTGRES_MAX_CONNECTIONS", + name: "Postgres Max Connections", + hint: "This is the maximum number of connections for the Postgres database", + default: "20" + } + ]) + }, + default_env: [ + "POSTGRES_DB_HOST=", + "POSTGRES_DB_PORT=", + "POSTGRES_DB_USER=", + "POSTGRES_DB_PASSWORD=", + "POSTGRES_DB_NAME=", + "POSTGRES_MAX_CONNECTIONS=", + ], + default_enabled: false, + required_for_database: "Postgres", + hint: "Configure Postgres as a secondary database", + autoconfigure: async () => { + return [ + "POSTGRES_DB_HOST=localhost", + "POSTGRES_DB_PORT=5432", + "POSTGRES_DB_USER=postgres", + "POSTGRES_DB_PASSWORD=passowrd", + "POSTGRES_DB_NAME=kitforstartups-postgres", + "POSTGRES_MAX_CONNECTIONS=20" + ] + } +}; + +export default builder; \ No newline at end of file diff --git a/create/src/environment/resend_builder.ts b/create/src/environment/resend_builder.ts new file mode 100644 index 0000000..32eee4f --- /dev/null +++ b/create/src/environment/resend_builder.ts @@ -0,0 +1,22 @@ +import {askEnvironmentConfig, DynamicConfigBuilder} from "./utils.js"; + +const builder: DynamicConfigBuilder = { + name: "Resend Email API", + depth: 1, + configure: () => { + return askEnvironmentConfig([ + { + key: "RESEND_API_KEY", + name: "Resend Email API Key", + hint: "This is the API key for the Resend Email API" + } + ]) + }, + default_env: [ + "RESEND_API_KEY=" + ], + default_enabled: false, + hint: "This is used to send emails to users in production" +}; + +export default builder; \ No newline at end of file diff --git a/create/src/environment/turso_builder.ts b/create/src/environment/turso_builder.ts new file mode 100644 index 0000000..c2e8488 --- /dev/null +++ b/create/src/environment/turso_builder.ts @@ -0,0 +1,29 @@ +import {askEnvironmentConfig, DynamicConfigBuilder} from "./utils.js"; + +const builder: DynamicConfigBuilder = { + name: "Turso Database", + depth: 2, + configure: () => { + return askEnvironmentConfig([ + { + key: "TURSO_DB_URL", + name: "Turso Database URL", + hint: "This is the URL for the Turso database" + }, + { + key: "TURSO_AUTH_TOKEN", + name: "Turso Auth Token", + hint: "This is the auth token for the Turso database" + } + ]) + }, + default_env: [ + "TURSO_DB_URL=", + "TURSO_AUTH_TOKEN=", + ], + default_enabled: false, + required_for_database: "Turso", + hint: "Configure Turso as a secondary database" +}; + +export default builder; \ No newline at end of file diff --git a/create/src/environment/utils.ts b/create/src/environment/utils.ts new file mode 100644 index 0000000..c03d596 --- /dev/null +++ b/create/src/environment/utils.ts @@ -0,0 +1,48 @@ +import {PrimaryDatabase} from "../database.js"; +import * as p from "@clack/prompts"; +import {PromptGroup} from "@clack/prompts"; + +export type DynamicConfigBuilder = { + name: string, + depth: number + configure: () => Promise, + default_env: string[], + default_enabled: boolean, + required_for_database?: PrimaryDatabase, + hint?: string, + autoconfigure?: () => Promise +} + +export type EnvironmentElement = { + key: string, + name: string, + hint: string, + default?: string, +} + +export async function askEnvironmentConfig(config: EnvironmentElement[]): Promise { + let grp: PromptGroup<{[key: string]: string | symbol }> = {} + + for (let element of config) { + grp[element.key] = () => p.text({ + message: element.name, + defaultValue: element.default, + placeholder: element.hint + }) + } + + let result = await p.group(grp, { + onCancel: () => { + p.cancel("Environment configuration cancelled.") + process.exit(1) + } + }) + + for (let key in result) { + if (result[key] === undefined) { + delete result[key] + } + } + + return Object.entries(result).map(([key, value]) => `${key}=${value}`) +} \ No newline at end of file From 80e9e3ffb74ff36a94e741ad510b7447d8060a52 Mon Sep 17 00:00:00 2001 From: Christian Bergschneider Date: Sun, 10 Mar 2024 14:39:34 +0000 Subject: [PATCH 4/4] wip(cli): startup dev environment --- create/src/environment/postgres_builder.ts | 2 +- create/src/index.ts | 29 +++++---- create/src/startup.ts | 72 ++++++++++++++++++++++ create/src/utils.ts | 13 ++++ 4 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 create/src/startup.ts diff --git a/create/src/environment/postgres_builder.ts b/create/src/environment/postgres_builder.ts index ad0d9d5..7120092 100644 --- a/create/src/environment/postgres_builder.ts +++ b/create/src/environment/postgres_builder.ts @@ -54,7 +54,7 @@ const builder: DynamicConfigBuilder = { "POSTGRES_DB_HOST=localhost", "POSTGRES_DB_PORT=5432", "POSTGRES_DB_USER=postgres", - "POSTGRES_DB_PASSWORD=passowrd", + "POSTGRES_DB_PASSWORD=password", "POSTGRES_DB_NAME=kitforstartups-postgres", "POSTGRES_MAX_CONNECTIONS=20" ] diff --git a/create/src/index.ts b/create/src/index.ts index dc6b91b..c7bb5c9 100644 --- a/create/src/index.ts +++ b/create/src/index.ts @@ -5,6 +5,8 @@ import {get_options} from "./installation_options.js"; import {createGitRepo, setupProject} from "./setup_project.js"; import {configureEnvironment} from "./environment.js"; import {configureDatabase} from "./database.js"; +import {done, unwrap_cancellation} from "./utils.js"; +import {startup} from "./startup.js"; const version = "1.0.0"; @@ -27,19 +29,24 @@ if (create_git_repo) await createGitRepo(working_directory) if (setup_env) { let database = await configureDatabase() await configureEnvironment(docker_supported, database) + + if (docker_supported && unwrap_cancellation(await p.confirm({ + message: "Setup environment now?", + }))) { + await startup(database) + process.exit(0) + } } if (working_directory === '.') { - p.outro(`Your project is ready! To get started: - - pnpm install - -Happy coding!` - ); + done("Your project is ready!", [ + "pnpm install", + "pnpm dev", + ]) } else { - p.outro(`Your project is ready! To get started: - - cd ${working_directory} - - pnpm install - -Happy coding!` - ); + done("Your project is ready!", [ + `cd ${working_directory}`, + "pnpm install", + "pnpm dev", + ]) } diff --git a/create/src/startup.ts b/create/src/startup.ts new file mode 100644 index 0000000..31b2516 --- /dev/null +++ b/create/src/startup.ts @@ -0,0 +1,72 @@ +import {PrimaryDatabase} from "./database.js"; +import * as p from '@clack/prompts'; +import shelljs from "shelljs"; +import {done} from "./utils.js"; + +async function runCommand(command: string) { + return new Promise((resolve, reject) => { + shelljs.exec(command, { silent: true }, (code, stdout, stderr) => { + if (code === 0) { + resolve(stdout) + } else { + reject(stderr) + } + }) + }) +} + +export async function startup(database: PrimaryDatabase) { + // PWD is project directory + let spinner = p.spinner() + spinner.start("Installing dependencies") + await runCommand("pnpm install") + await runCommand("pnpm svelte-kit sync") + spinner.stop("Dependencies installed") + spinner.start("Starting MailHog") + await runCommand("docker-compose -f docker/mailhog.yml up -d") + spinner.stop("Mailhog started using the command `docker-compose -f docker/mailhog.yml up -d`. Access it at http://localhost:8025") + + if (database === null) { + spinner.stop("No database selected") + done("Your project is ready!", [ + "Setup a database according to the instructions in the README.md file.", + "pnpm generate-migrations:[mysql|postgres|turso]", + "pnpm migrate:[mysql|postgres|turso]", + "pnpm push:[mysql|postgres|turso]", + "pnpm dev", + ]) + return + } + + let dbname = "mysql" + if (database === "Postgres") { + dbname = "postgres" + } else if (database === "Turso") { + dbname = "turso" + } + + if (database !== "Turso") { + spinner.start("Starting database") + + await runCommand(`docker-compose -f docker/${dbname}.yml up -d`) + spinner.stop("Database started using the command `docker-compose -f docker/" + dbname + ".yml up -d`") + + // wait for database to start + spinner.start("Waiting for database to start") + await new Promise(resolve => setTimeout(resolve, 10000)) + spinner.stop("Database started") + } + + spinner.start("Generating migrations") + await runCommand("pnpm generate-migrations:" + dbname) + spinner.stop("Migrations generated") + spinner.start("Migrating database") + await runCommand("pnpm migrate:" + dbname) + spinner.stop("Database migrated") + spinner.start("Pushing database") + await runCommand("pnpm push:" + dbname) + spinner.stop("Your project is ready!") + done("Your project is ready!", [ + "pnpm dev", + ]) +} \ No newline at end of file diff --git a/create/src/utils.ts b/create/src/utils.ts index be65cb0..31f4a44 100644 --- a/create/src/utils.ts +++ b/create/src/utils.ts @@ -9,3 +9,16 @@ export function unwrap_cancellation(data: T | symbol) { return data as T; } + + +export function done(message: string = "Your project is ready!", steps: string[]) { + if (steps.length > 0) { + message = message + "\n\nNext steps:\n" + steps.map((step, i) => `${i + 1}. ${step}`).join("\n"); + } + + message = message + "\n\nHappy coding!"; + + p.outro(message); + + process.exit(0); +} \ No newline at end of file