mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-31 12:47:58 +00:00 
			
		
		
		
	CLI to install project (#164)
* CLI to install project * CLI fixes * Update README.md * Cleanup gitignore
This commit is contained in:
		| @@ -36,6 +36,9 @@ It is meant to be: | |||||||
| - Perfectly in-sync with your data | - Perfectly in-sync with your data | ||||||
| - Crafted with care and enjoyable to use | - Crafted with care and enjoyable to use | ||||||
|  |  | ||||||
|  | # Quickstart | ||||||
|  | No need to clone the repo, just run `npx twenty-cli` in your terminal and follow the instructions. | ||||||
|  |  | ||||||
| # Progress | # Progress | ||||||
| We are currently in the development phase of Twenty's alpha version: | We are currently in the development phase of Twenty's alpha version: | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								cli/.eslintrc.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								cli/.eslintrc.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | module.exports = { | ||||||
|  |     parser: '@typescript-eslint/parser', | ||||||
|  |     parserOptions: { | ||||||
|  |       project: 'tsconfig.json', | ||||||
|  |       tsconfigRootDir: __dirname, | ||||||
|  |       sourceType: 'module', | ||||||
|  |     }, | ||||||
|  |     plugins: ['@typescript-eslint/eslint-plugin'], | ||||||
|  |     extends: [ | ||||||
|  |       'plugin:@typescript-eslint/recommended', | ||||||
|  |       'plugin:prettier/recommended' | ||||||
|  |     ], | ||||||
|  |     root: true, | ||||||
|  |     env: { | ||||||
|  |       node: true, | ||||||
|  |       jest: true, | ||||||
|  |     }, | ||||||
|  |     ignorePatterns: ['.eslintrc.js', 'codegen.js', '**/generated/*', '/lib/*'], | ||||||
|  |     rules: { | ||||||
|  |       '@typescript-eslint/interface-name-prefix': 'off', | ||||||
|  |       '@typescript-eslint/explicit-function-return-type': 'off', | ||||||
|  |       '@typescript-eslint/explicit-module-boundary-types': 'off', | ||||||
|  |       '@typescript-eslint/no-explicit-any': 'off', | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |    | ||||||
							
								
								
									
										36
									
								
								cli/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								cli/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | # Logs | ||||||
|  | logs | ||||||
|  | *.log | ||||||
|  | npm-debug.log* | ||||||
|  | yarn-debug.log* | ||||||
|  | yarn-error.log* | ||||||
|  | .pnpm-debug.log* | ||||||
|  |  | ||||||
|  | # Dependency directories | ||||||
|  | node_modules/ | ||||||
|  |  | ||||||
|  | # TypeScript cache | ||||||
|  | *.tsbuildinfo | ||||||
|  |  | ||||||
|  | # Optional npm cache directory | ||||||
|  | .npm | ||||||
|  |  | ||||||
|  | # Optional eslint cache | ||||||
|  | .eslintcache | ||||||
|  |  | ||||||
|  | # Optional REPL history | ||||||
|  | .node_repl_history | ||||||
|  |  | ||||||
|  | # Output of 'npm pack' | ||||||
|  | *.tgz | ||||||
|  |  | ||||||
|  | # dotenv environment variable files | ||||||
|  | .env | ||||||
|  | .env.development.local | ||||||
|  | .env.test.local | ||||||
|  | .env.production.local | ||||||
|  | .env.local | ||||||
|  |  | ||||||
|  | # other | ||||||
|  | lib/ | ||||||
|  | .DS_Store | ||||||
							
								
								
									
										4
									
								
								cli/.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								cli/.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | { | ||||||
|  |     "singleQuote": true, | ||||||
|  |     "trailingComma": "all" | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								cli/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								cli/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | # Twenty CLI | ||||||
|  | A simple CLI to get started with Twenty  | ||||||
							
								
								
									
										8
									
								
								cli/__tests__/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								cli/__tests__/config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | import { execShell } from '../src/config'; | ||||||
|  |  | ||||||
|  | export {}; | ||||||
|  |  | ||||||
|  | test('execShell runs a shell command', async () => { | ||||||
|  |   let response = await execShell('echo "hello"'); | ||||||
|  |   expect(response).toEqual('hello\n'); | ||||||
|  | }); | ||||||
							
								
								
									
										5
									
								
								cli/jest.config.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								cli/jest.config.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | /** @type {import('ts-jest').JestConfigWithTsJest} */ | ||||||
|  | module.exports = { | ||||||
|  |   preset: 'ts-jest', | ||||||
|  |   testEnvironment: 'node', | ||||||
|  | }; | ||||||
							
								
								
									
										5872
									
								
								cli/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										5872
									
								
								cli/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										41
									
								
								cli/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								cli/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | { | ||||||
|  |   "name": "twenty-cli", | ||||||
|  |   "version": "1.0.5", | ||||||
|  |   "type": "module", | ||||||
|  |   "description": "A simple CLI to install and interact with Twenty", | ||||||
|  |   "files": [ | ||||||
|  |     "!lib/__tests__/**/*", | ||||||
|  |     "lib/**/*", | ||||||
|  |     "bin/**/*" | ||||||
|  |   ], | ||||||
|  |   "scripts": { | ||||||
|  |     "test": "echo \"Error: no test specified\" && exit 1" | ||||||
|  |   }, | ||||||
|  |   "author": "", | ||||||
|  |   "license": "ISC", | ||||||
|  |   "dependencies": { | ||||||
|  |     "@types/gradient-string": "^1.1.2", | ||||||
|  |     "chalk": "^5.2.0", | ||||||
|  |     "commander": "^10.0.1", | ||||||
|  |     "dotenv": "^16.1.1", | ||||||
|  |     "gradient-string": "^2.0.2", | ||||||
|  |     "open": "^9.1.0", | ||||||
|  |     "pg": "^8.11.0", | ||||||
|  |     "prompts": "^2.4.2" | ||||||
|  |   }, | ||||||
|  |   "bin": { | ||||||
|  |     "twenty": "./lib/index.js" | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "@types/jest": "^29.5.1", | ||||||
|  |     "@types/node": "^20.2.5", | ||||||
|  |     "@types/pg": "^8.10.1", | ||||||
|  |     "@types/prompts": "^2.4.4", | ||||||
|  |     "@typescript-eslint/eslint-plugin": "^5.59.8", | ||||||
|  |     "eslint-config-prettier": "^8.8.0", | ||||||
|  |     "eslint-plugin-prettier": "^4.2.1", | ||||||
|  |     "jest": "^29.5.0", | ||||||
|  |     "ts-jest": "^29.1.0", | ||||||
|  |     "typescript": "^5.0.4" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								cli/src/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cli/src/config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | import { exec } from 'child_process'; | ||||||
|  |  | ||||||
|  | export function execShell(cmd: string): Promise<string> { | ||||||
|  |   return new Promise((resolve, reject) => { | ||||||
|  |     exec(cmd, (error, stdout, stderr) => { | ||||||
|  |       if (error) { | ||||||
|  |         console.warn(`Error: ${error.message}`); | ||||||
|  |         console.warn(`stderr: ${stderr}`); | ||||||
|  |         reject(error); | ||||||
|  |       } | ||||||
|  |       resolve(stdout ? stdout : stderr); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const REPO_URL = 'https://github.com/twentyhq/twenty.git'; | ||||||
|  | export const CLONE_DIR = 'twenty'; | ||||||
							
								
								
									
										27
									
								
								cli/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								cli/src/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | #!/usr/bin/env node | ||||||
|  | import { program } from 'commander'; | ||||||
|  | import { showWelcomeScreen, firstQuestion } from './install/index.js'; | ||||||
|  | import prompts from 'prompts'; | ||||||
|  | import { askContributeQuestions } from './install/contribute/index.js'; | ||||||
|  | import { askDemoQuestions } from './install/demo/index.js'; | ||||||
|  | import { askSelfhostQuestions } from './install/selfhost/index.js'; | ||||||
|  |  | ||||||
|  | program; | ||||||
|  |  | ||||||
|  | showWelcomeScreen(); | ||||||
|  |  | ||||||
|  | (async () => { | ||||||
|  |   const response = await prompts(firstQuestion); | ||||||
|  |  | ||||||
|  |   switch (response.install_type) { | ||||||
|  |     case 'contribute': | ||||||
|  |       askContributeQuestions(); | ||||||
|  |       break; | ||||||
|  |     case 'demo': | ||||||
|  |       askDemoQuestions(); | ||||||
|  |       break; | ||||||
|  |     case 'selfhost': | ||||||
|  |       askSelfhostQuestions(); | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  | })(); | ||||||
							
								
								
									
										35
									
								
								cli/src/install/contribute/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								cli/src/install/contribute/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | import prompts, { PromptObject } from 'prompts'; | ||||||
|  | import { askContributeLocalQuestions } from './local/index.js'; | ||||||
|  | import { askContributeRemoteQuestions } from './remote.js'; | ||||||
|  |  | ||||||
|  | export const contributeQuestions: PromptObject<string>[] = [ | ||||||
|  |   { | ||||||
|  |     type: 'select', | ||||||
|  |     name: 'contribute_type', | ||||||
|  |     message: 'Where do you want to setup your development environment?', | ||||||
|  |     choices: [ | ||||||
|  |       { | ||||||
|  |         title: 'Local', | ||||||
|  |         description: 'I want to setup a development environment on my machine', | ||||||
|  |         value: 'local', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'Remote (via Github Codespaces)', | ||||||
|  |         description: 'A simple pre-configured remote environment', | ||||||
|  |         value: 'remote', | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const askContributeQuestions: () => Promise<void> = async () => { | ||||||
|  |   const response = await prompts(contributeQuestions); | ||||||
|  |   switch (response.contribute_type) { | ||||||
|  |     case 'local': | ||||||
|  |       await askContributeLocalQuestions(); | ||||||
|  |       break; | ||||||
|  |     case 'remote': | ||||||
|  |       await askContributeRemoteQuestions(); | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  | }; | ||||||
							
								
								
									
										128
									
								
								cli/src/install/contribute/local/docker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								cli/src/install/contribute/local/docker.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | |||||||
|  | import prompts, { PromptObject } from 'prompts'; | ||||||
|  | import { spawn } from 'child_process'; | ||||||
|  | import { execShell, REPO_URL } from '../../../config.js'; | ||||||
|  | import { join } from 'path'; | ||||||
|  | import * as fs from 'fs'; | ||||||
|  |  | ||||||
|  | export const dockerQuestions: PromptObject<string>[] = [ | ||||||
|  |   { | ||||||
|  |     type: 'text', | ||||||
|  |     name: 'folder_name', | ||||||
|  |     initial: 'twenty', | ||||||
|  |     message: 'Name of folder where we will clone the repo?', | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const askDockerQuestions: () => Promise<void> = async () => { | ||||||
|  |   let folderResponse = await prompts(dockerQuestions); | ||||||
|  |  | ||||||
|  |   let folderExists = fs.existsSync(folderResponse.folder_name); | ||||||
|  |   while (folderExists) { | ||||||
|  |     try { | ||||||
|  |       folderResponse = await prompts({ | ||||||
|  |         type: 'text', | ||||||
|  |         name: 'folder_name', | ||||||
|  |         message: | ||||||
|  |           'Folder already exists. Please choose another name and press enter.', | ||||||
|  |       }); | ||||||
|  |     } catch (error) { | ||||||
|  |       if ((error as NodeJS.Signals) === 'SIGINT') { | ||||||
|  |         process.exit(0); | ||||||
|  |       } else { | ||||||
|  |         throw error; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     folderExists = fs.existsSync(folderResponse.folder_name); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let git_is_installed = false; | ||||||
|  |   while (!git_is_installed) { | ||||||
|  |     try { | ||||||
|  |       await execShell('git --version'); | ||||||
|  |       git_is_installed = true; | ||||||
|  |     } catch (error) { | ||||||
|  |       try { | ||||||
|  |         await prompts({ | ||||||
|  |           type: 'text', | ||||||
|  |           name: 'git_install', | ||||||
|  |           message: | ||||||
|  |             'Git does not appear to be installed. Please install it and press enter.', | ||||||
|  |         }); | ||||||
|  |       } catch (error) { | ||||||
|  |         if ((error as NodeJS.Signals) === 'SIGINT') { | ||||||
|  |           process.exit(0); | ||||||
|  |         } else { | ||||||
|  |           throw error; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let docker_is_installed = false; | ||||||
|  |   while (!docker_is_installed) { | ||||||
|  |     try { | ||||||
|  |       await execShell('docker --version'); | ||||||
|  |       docker_is_installed = true; | ||||||
|  |     } catch (error) { | ||||||
|  |       try { | ||||||
|  |         await prompts({ | ||||||
|  |           type: 'text', | ||||||
|  |           name: 'docker_install', | ||||||
|  |           message: | ||||||
|  |             'Docker does not appear to be installed. Please install it and press enter.', | ||||||
|  |         }); | ||||||
|  |       } catch (error) { | ||||||
|  |         if ((error as NodeJS.Signals) === 'SIGINT') { | ||||||
|  |           process.exit(0); | ||||||
|  |         } else { | ||||||
|  |           throw error; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let docker_daemon_running = false; | ||||||
|  |   while (!docker_daemon_running) { | ||||||
|  |     try { | ||||||
|  |       await execShell('docker info'); | ||||||
|  |       docker_daemon_running = true; | ||||||
|  |     } catch (error) { | ||||||
|  |       try { | ||||||
|  |         await prompts({ | ||||||
|  |           type: 'text', | ||||||
|  |           name: 'docker_install', | ||||||
|  |           message: | ||||||
|  |             'Docker daemon does not appear to be running. Please start it manually and press enter.', | ||||||
|  |         }); | ||||||
|  |       } catch (error) { | ||||||
|  |         if ((error as NodeJS.Signals) === 'SIGINT') { | ||||||
|  |           process.exit(0); | ||||||
|  |         } else { | ||||||
|  |           throw error; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   console.log('Cloning the Twenty repo. This can take a little while.'); | ||||||
|  |  | ||||||
|  |   await execShell(`git clone ${REPO_URL} ${folderResponse.folder_name}`); | ||||||
|  |  | ||||||
|  |   console.log('Build the docker images. (cd infra/dev then make build)'); | ||||||
|  |  | ||||||
|  |   const makeBuild = spawn('make', ['build'], { | ||||||
|  |     cwd: join(folderResponse.folder_name, 'infra', 'dev'), | ||||||
|  |   }); | ||||||
|  |   makeBuild.stdout.on('data', (data) => { | ||||||
|  |     console.log(`stdout: ${data}`); | ||||||
|  |   }); | ||||||
|  |   makeBuild.stderr.on('data', (data) => { | ||||||
|  |     console.log(`stderr: ${data}`); | ||||||
|  |   }); | ||||||
|  |   makeBuild.on('error', (error) => { | ||||||
|  |     console.log(`error: ${error.message}`); | ||||||
|  |   }); | ||||||
|  |   makeBuild.on('close', (code) => { | ||||||
|  |     console.log(`child process exited with code ${code}`); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
							
								
								
									
										35
									
								
								cli/src/install/contribute/local/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								cli/src/install/contribute/local/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | import prompts, { PromptObject } from 'prompts'; | ||||||
|  | import { askDockerQuestions } from './docker.js'; | ||||||
|  | import { askNoDockerQuestions } from './no-docker.js'; | ||||||
|  |  | ||||||
|  | export const contributeLocalQuestions: PromptObject<string>[] = [ | ||||||
|  |   { | ||||||
|  |     type: 'select', | ||||||
|  |     name: 'local_setup_type', | ||||||
|  |     message: 'What is your prefered setup?', | ||||||
|  |     choices: [ | ||||||
|  |       { | ||||||
|  |         title: 'Docker', | ||||||
|  |         description: 'A managed development environment with Postgres included', | ||||||
|  |         value: 'docker', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'Without docker', | ||||||
|  |         description: "You'll need to setup a Postgres instance on your own", | ||||||
|  |         value: 'no-docker', | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const askContributeLocalQuestions: () => Promise<void> = async () => { | ||||||
|  |   const response = await prompts(contributeLocalQuestions); | ||||||
|  |   switch (response.local_setup_type) { | ||||||
|  |     case 'docker': | ||||||
|  |       await askDockerQuestions(); | ||||||
|  |       break; | ||||||
|  |     case 'no-docker': | ||||||
|  |       await askNoDockerQuestions(); | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  | }; | ||||||
							
								
								
									
										213
									
								
								cli/src/install/contribute/local/no-docker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								cli/src/install/contribute/local/no-docker.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,213 @@ | |||||||
|  | import prompts, { PromptObject } from 'prompts'; | ||||||
|  | import { spawn } from 'child_process'; | ||||||
|  | import { execShell, REPO_URL } from '../../../config.js'; | ||||||
|  | import { join } from 'path'; | ||||||
|  | import * as fs from 'fs'; | ||||||
|  | import * as path from 'path'; | ||||||
|  | import pkg from 'pg'; | ||||||
|  | const { Client } = pkg; | ||||||
|  |  | ||||||
|  | export const noDockerQuestion1: PromptObject<string>[] = [ | ||||||
|  |   { | ||||||
|  |     type: 'text', | ||||||
|  |     name: 'folder_name', | ||||||
|  |     initial: 'twenty', | ||||||
|  |     message: 'Name of folder where we will clone the repo?', | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const noDockerQuestion2: PromptObject<string>[] = [ | ||||||
|  |   { | ||||||
|  |     type: 'text', | ||||||
|  |     name: 'postgres_string', | ||||||
|  |     initial: 'postgres://postgres:postgrespassword@postgres:5432/default', | ||||||
|  |     message: | ||||||
|  |       'Since you are not using Docker, you need to bring your own database, please enter your postgres connection string.', | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const askNoDockerQuestions: () => Promise<void> = async () => { | ||||||
|  |   let folderResponse = await prompts(noDockerQuestion1); | ||||||
|  |  | ||||||
|  |   let folderExists = fs.existsSync(folderResponse.folder_name); | ||||||
|  |   while (folderExists) { | ||||||
|  |     try { | ||||||
|  |       folderResponse = await prompts({ | ||||||
|  |         type: 'text', | ||||||
|  |         name: 'folder_name', | ||||||
|  |         message: | ||||||
|  |           'Folder already exists. Please choose another name and press enter.', | ||||||
|  |       }); | ||||||
|  |     } catch (error) { | ||||||
|  |       if ((error as NodeJS.Signals) === 'SIGINT') { | ||||||
|  |         process.exit(0); | ||||||
|  |       } else { | ||||||
|  |         throw error; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     folderExists = fs.existsSync(folderResponse.folder_name); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let connectionStringResponse = await prompts(noDockerQuestion2); | ||||||
|  |  | ||||||
|  |   let postgres_connection_valid = false; | ||||||
|  |   while (!postgres_connection_valid) { | ||||||
|  |     const client = new Client({ | ||||||
|  |       connectionString: connectionStringResponse.postgres_string, | ||||||
|  |     }); | ||||||
|  |     try { | ||||||
|  |       await client.connect(); | ||||||
|  |       await client.end(); | ||||||
|  |       postgres_connection_valid = true; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.log(error); | ||||||
|  |       postgres_connection_valid = false; | ||||||
|  |     } | ||||||
|  |     if (!postgres_connection_valid) { | ||||||
|  |       try { | ||||||
|  |         connectionStringResponse = await prompts({ | ||||||
|  |           type: 'text', | ||||||
|  |           name: 'postgres_string', | ||||||
|  |           initial: 'postgres://postgres:postgrespassword@postgres:5432/default', | ||||||
|  |           message: | ||||||
|  |             'Connection to Postgres failed. Please enter the string again', | ||||||
|  |         }); | ||||||
|  |       } catch (error) { | ||||||
|  |         if ((error as NodeJS.Signals) === 'SIGINT') { | ||||||
|  |           process.exit(0); | ||||||
|  |         } else { | ||||||
|  |           throw error; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let git_is_installed = false; | ||||||
|  |   while (!git_is_installed) { | ||||||
|  |     try { | ||||||
|  |       await execShell('git --version'); | ||||||
|  |       git_is_installed = true; | ||||||
|  |     } catch (error) { | ||||||
|  |       try { | ||||||
|  |         await prompts({ | ||||||
|  |           type: 'text', | ||||||
|  |           name: 'git_install', | ||||||
|  |           message: | ||||||
|  |             'Git does not appear to be installed. Please install it and restart.', | ||||||
|  |         }); | ||||||
|  |       } catch (error) { | ||||||
|  |         if ((error as NodeJS.Signals) === 'SIGINT') { | ||||||
|  |           process.exit(0); | ||||||
|  |         } else { | ||||||
|  |           throw error; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let npm_is_installed = false; | ||||||
|  |   while (!npm_is_installed) { | ||||||
|  |     try { | ||||||
|  |       await execShell('npm --version'); | ||||||
|  |       npm_is_installed = true; | ||||||
|  |     } catch (error) { | ||||||
|  |       try { | ||||||
|  |         await prompts({ | ||||||
|  |           type: 'text', | ||||||
|  |           name: 'git_install', | ||||||
|  |           message: | ||||||
|  |             'Npm does not appear to be installed. Please install it and press enter.', | ||||||
|  |         }); | ||||||
|  |       } catch (error) { | ||||||
|  |         if ((error as NodeJS.Signals) === 'SIGINT') { | ||||||
|  |           process.exit(0); | ||||||
|  |         } else { | ||||||
|  |           throw error; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   console.log('Cloning the Twenty repo. This can take a little while.'); | ||||||
|  |  | ||||||
|  |   await execShell(`git clone ${REPO_URL} ${folderResponse.folder_name}`); | ||||||
|  |  | ||||||
|  |   await execShell( | ||||||
|  |     `cp ${folderResponse.folder_name}/front/.env.example  ${folderResponse.folder_name}/front/.env`, | ||||||
|  |   ); | ||||||
|  |   await execShell( | ||||||
|  |     `cp ${folderResponse.folder_name}/server/.env.example  ${folderResponse.folder_name}/server/.env`, | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const envFile = path.resolve( | ||||||
|  |     join(folderResponse.folder_name, 'server', '.env'), | ||||||
|  |   ); | ||||||
|  |   let envFileLines = fs.readFileSync(envFile, 'utf-8').split('\n'); | ||||||
|  |   envFileLines = envFileLines.map((line) => | ||||||
|  |     line.startsWith('PG_DATABASE_URL=') | ||||||
|  |       ? `PG_DATABASE_URL=${connectionStringResponse.postgres_string}` | ||||||
|  |       : line, | ||||||
|  |   ); | ||||||
|  |   // write the updated content back to the .env file | ||||||
|  |   fs.writeFileSync(envFile, envFileLines.join('\n')); | ||||||
|  |  | ||||||
|  |   console.log('Building the frontend (running npm install on frontend folder)'); | ||||||
|  |   const buildFront = spawn('npm', ['install'], { | ||||||
|  |     cwd: join(folderResponse.folder_name, 'front'), | ||||||
|  |   }); | ||||||
|  |   buildFront.stdout.on('data', (data) => { | ||||||
|  |     console.log(`${data}`); | ||||||
|  |   }); | ||||||
|  |   buildFront.stderr.on('data', (data) => { | ||||||
|  |     console.log(`${data}`); | ||||||
|  |   }); | ||||||
|  |   buildFront.on('error', (error) => { | ||||||
|  |     console.log(`error: ${error.message}`); | ||||||
|  |   }); | ||||||
|  |   buildFront.on('close', () => { | ||||||
|  |     console.log('Building the server (running npm install on server folder)'); | ||||||
|  |     const buildServer = spawn('npm', ['install'], { | ||||||
|  |       cwd: join(folderResponse.folder_name, 'server'), | ||||||
|  |     }); | ||||||
|  |     buildServer.stdout.on('data', (data) => { | ||||||
|  |       console.log(`${data}`); | ||||||
|  |     }); | ||||||
|  |     buildServer.stderr.on('data', (data) => { | ||||||
|  |       console.log(`${data}`); | ||||||
|  |     }); | ||||||
|  |     buildServer.on('error', (error) => { | ||||||
|  |       console.log(`error: ${error.message}`); | ||||||
|  |     }); | ||||||
|  |     buildServer.on('close', () => { | ||||||
|  |       console.log( | ||||||
|  |         'Running the frontend (running npm start on frontend folder)', | ||||||
|  |       ); | ||||||
|  |       const runFrontend = spawn('npm', ['run', 'start'], { | ||||||
|  |         cwd: join(folderResponse.folder_name, 'server'), | ||||||
|  |       }); | ||||||
|  |       runFrontend.stdout.on('data', (data) => { | ||||||
|  |         console.log(`${data}`); | ||||||
|  |       }); | ||||||
|  |       runFrontend.stderr.on('data', (data) => { | ||||||
|  |         console.log(`${data}`); | ||||||
|  |       }); | ||||||
|  |       runFrontend.on('error', (error) => { | ||||||
|  |         console.log(`error: ${error.message}`); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       console.log('Running the server (running npm start on server folder)'); | ||||||
|  |       const runServer = spawn('npm', ['run', 'start'], { | ||||||
|  |         cwd: join(folderResponse.folder_name, 'server'), | ||||||
|  |       }); | ||||||
|  |       runServer.stdout.on('data', (data) => { | ||||||
|  |         console.log(`${data}`); | ||||||
|  |       }); | ||||||
|  |       runServer.stderr.on('data', (data) => { | ||||||
|  |         console.log(`${data}`); | ||||||
|  |       }); | ||||||
|  |       runServer.on('error', (error) => { | ||||||
|  |         console.log(`error: ${error.message}`); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
							
								
								
									
										16
									
								
								cli/src/install/contribute/remote.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								cli/src/install/contribute/remote.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | import prompts, { PromptObject } from 'prompts'; | ||||||
|  | import open from 'open'; | ||||||
|  |  | ||||||
|  | export const contributeRemoteQuestions: PromptObject<string>[] = [ | ||||||
|  |   { | ||||||
|  |     type: 'text', | ||||||
|  |     name: 'local_setup_type', | ||||||
|  |     message: | ||||||
|  |       "We'll be redirecting you to a dedicated Github Codespace. Press enter to continue.", | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const askContributeRemoteQuestions: () => Promise<void> = async () => { | ||||||
|  |   await prompts(contributeRemoteQuestions); | ||||||
|  |   await open('https://codespaces.new/twentyhq/twenty'); | ||||||
|  | }; | ||||||
							
								
								
									
										43
									
								
								cli/src/install/demo/cloud.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								cli/src/install/demo/cloud.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | import prompts, { PromptObject } from 'prompts'; | ||||||
|  | import open from 'open'; | ||||||
|  |  | ||||||
|  | export const demoCloudQuestions: PromptObject<string>[] = [ | ||||||
|  |   { | ||||||
|  |     type: 'text', | ||||||
|  |     name: 'continue_cloud', | ||||||
|  |     message: 'We will redirect your to the cloud app. Press enter to continue.', | ||||||
|  |   }, | ||||||
|  |   /* | ||||||
|  |   In the future we can let user signup from CLI directly before redirecting: | ||||||
|  |   { | ||||||
|  |     type: 'select', | ||||||
|  |     name: 'signup_type', | ||||||
|  |     message: 'How do you want to signup?', | ||||||
|  |     choices: [ | ||||||
|  |       { title: 'Google Sign-in', value: 'google' }, | ||||||
|  |       { title: 'Email with magic link', value: 'magic_link' }, | ||||||
|  |       { title: 'Email with password', value: 'password' }, | ||||||
|  |       { title: 'No-email, demo account with seeds', value: 'seeded_demo' }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     type: (rep) => (rep == 'google' ? 'text' : null), | ||||||
|  |     name: 'google_signup', | ||||||
|  |     message: | ||||||
|  |       'A new browser window will open to sign you up with Google. Press enter to continue.', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     type: (rep) => { | ||||||
|  |       if (rep == 'magic_link' || rep == 'password') { | ||||||
|  |         return 'text'; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     name: 'email_signup', | ||||||
|  |     message: 'Please enter your email', | ||||||
|  |   }, */ | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const askDemoCloudQuestions: () => Promise<void> = async () => { | ||||||
|  |   await prompts(demoCloudQuestions); | ||||||
|  |   open('https://app.twenty.com'); | ||||||
|  | }; | ||||||
							
								
								
									
										14
									
								
								cli/src/install/demo/docker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								cli/src/install/demo/docker.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import prompts, { PromptObject } from 'prompts'; | ||||||
|  |  | ||||||
|  | export const demoDockerQuestions: PromptObject<string>[] = [ | ||||||
|  |   { | ||||||
|  |     type: 'text', | ||||||
|  |     name: 'not_ready_yet', | ||||||
|  |     message: 'Not yeady yet', | ||||||
|  |     choices: [{ title: 'XXX', value: 'XXX' }], | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const askDemoDockerQuestions: () => Promise<void> = async () => { | ||||||
|  |   await prompts(demoDockerQuestions); | ||||||
|  | }; | ||||||
							
								
								
									
										27
									
								
								cli/src/install/demo/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								cli/src/install/demo/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | import prompts, { PromptObject } from 'prompts'; | ||||||
|  | import { askDemoCloudQuestions } from './cloud.js'; | ||||||
|  | import { askDemoDockerQuestions } from './docker.js'; | ||||||
|  |  | ||||||
|  | export const demoQuestions: PromptObject<string>[] = [ | ||||||
|  |   { | ||||||
|  |     type: 'select', | ||||||
|  |     name: 'demo_type', | ||||||
|  |     message: 'How do you want to try the app?', | ||||||
|  |     choices: [ | ||||||
|  |       { title: 'Cloud demo', value: 'cloud' }, | ||||||
|  |       { title: 'Local docker image', value: 'docker', disabled: true }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const askDemoQuestions: () => Promise<void> = async () => { | ||||||
|  |   const response = await prompts(demoQuestions); | ||||||
|  |   switch (response.demo_type) { | ||||||
|  |     case 'cloud': | ||||||
|  |       await askDemoCloudQuestions(); | ||||||
|  |       break; | ||||||
|  |     case 'docker': | ||||||
|  |       await askDemoDockerQuestions(); | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  | }; | ||||||
							
								
								
									
										57
									
								
								cli/src/install/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								cli/src/install/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | import gradient from 'gradient-string'; | ||||||
|  | import chalk from 'chalk'; | ||||||
|  | import { PromptObject } from 'prompts'; | ||||||
|  |  | ||||||
|  | export function showWelcomeScreen() { | ||||||
|  |   const logo = ` | ||||||
|  |                                                                                          | ||||||
|  |   &&&&&&&&&&&&&      &&&&&&&&&&&&&            | ||||||
|  |   &&&&&&&&&&&&&&   &&&&&&&&&&&&&&&&         | ||||||
|  |  &&&&            &&&&&          &&&&        | ||||||
|  |  &&&&         &&&&&&            &&&&        | ||||||
|  |  &&&&       &&&&&&   &&         &&&&        | ||||||
|  |           &&&&&    &&&&         &&&&        | ||||||
|  |        &&&&&&      &&&&         &&&&        | ||||||
|  |      &&&&&&        &&&&         &&&&        | ||||||
|  |    &&&&&           &&&&         &&&&        | ||||||
|  |  &&&&&&&&&&&&&&&&   &&&&&&&&&&&&&&&        | ||||||
|  |  &&&&&&&&&&&&&&&&     &&&&&&&&&&&&   | ||||||
|  |   | ||||||
|  | `; | ||||||
|  |  | ||||||
|  |   const items = logo.split('\n').map((row) => gradient.mind(row)); | ||||||
|  |  | ||||||
|  |   /* eslint-disable no-console */ | ||||||
|  |   console.log(chalk.bold(items.join('\n'))); | ||||||
|  |   console.log(chalk.bold(`Welcome to Twenty!`)); | ||||||
|  |   console.log( | ||||||
|  |     chalk.bold( | ||||||
|  |       gradient.mind(`We're building a modern alternative to Salesforce\n`), | ||||||
|  |     ), | ||||||
|  |   ); | ||||||
|  |   console.log(chalk.bold(`Let's get you started!\n\n`)); | ||||||
|  |   /* eslint-enable  no-console */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const firstQuestion: PromptObject = { | ||||||
|  |   type: 'select', | ||||||
|  |   name: 'install_type', | ||||||
|  |   message: 'What do you want to do?', | ||||||
|  |   choices: [ | ||||||
|  |     { | ||||||
|  |       title: 'Contribute to the code', | ||||||
|  |       description: 'I want to setup a development environment', | ||||||
|  |       value: 'contribute', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: 'Quickly try the product', | ||||||
|  |       description: 'I want to play with a demo version', | ||||||
|  |       value: 'demo', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: 'Self-host on a server', | ||||||
|  |       description: 'I want to host the app on a distant server', | ||||||
|  |       value: 'selfhost', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										16
									
								
								cli/src/install/selfhost/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								cli/src/install/selfhost/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | import prompts, { PromptObject } from 'prompts'; | ||||||
|  | import open from 'open'; | ||||||
|  |  | ||||||
|  | export const selfhostQuestions: PromptObject<string>[] = [ | ||||||
|  |   { | ||||||
|  |     type: 'text', | ||||||
|  |     name: 'docker', | ||||||
|  |     message: | ||||||
|  |       'The options to self-host are documented in the doc. Click enter to open the relevant help page.', | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const askSelfhostQuestions: () => Promise<void> = async () => { | ||||||
|  |   await prompts(selfhostQuestions); | ||||||
|  |   await open('https://docs.twenty.com/dev-docs/getting-started/self-hosting'); | ||||||
|  | }; | ||||||
							
								
								
									
										14
									
								
								cli/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								cli/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | { | ||||||
|  |     "compilerOptions": { | ||||||
|  |       "target": "es5", | ||||||
|  |       "module": "es2020", | ||||||
|  |       "strict": true, | ||||||
|  |       "outDir": "lib/", | ||||||
|  |       "moduleResolution": "nodenext", | ||||||
|  |       "esModuleInterop": true, | ||||||
|  |       "skipLibCheck": true, | ||||||
|  |       "declaration": true, | ||||||
|  |       "forceConsistentCasingInFileNames": true | ||||||
|  |     }, | ||||||
|  |     "include": ["src/**/*.ts"] | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Félix Malfait
					Félix Malfait