mirror of
https://github.com/lingble/twenty.git
synced 2025-11-01 13:17:57 +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