Skip to content

Commit 1000f92

Browse files
authored
feat: prime-cli (#80)
1 parent ff6cdea commit 1000f92

14 files changed

Lines changed: 653 additions & 5 deletions

File tree

packages/prime-cli/package.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "@primecms/cli",
3+
"private": false,
4+
"version": "0.3.2-beta.1",
5+
"description": "primecms cli",
6+
"author": "Birkir Gudjonsson <birkir.gudjonsson@gmail.com>",
7+
"homepage": "https://github.com/birkir/prime/tree/master/packages/prime-cli",
8+
"license": "MIT",
9+
"bin": {
10+
"primecms": "./lib/index.js"
11+
},
12+
"main": "lib/index",
13+
"publishConfig": {
14+
"access": "public"
15+
},
16+
"files": [
17+
"src",
18+
"lib"
19+
],
20+
"scripts": {
21+
"clean": "rimraf lib",
22+
"start": "ts-node ./src",
23+
"precompile": "yarn clean",
24+
"compile": "tsc",
25+
"prepublishOnly": "yarn compile"
26+
},
27+
"jest": {
28+
"preset": "ts-jest",
29+
"coveragePathIgnorePatterns": [
30+
"/node_modules/",
31+
"/src/tests"
32+
]
33+
},
34+
"repository": {
35+
"type": "git",
36+
"url": "https://github.com/birkir/prime/tree/master/packages/prime-cli"
37+
},
38+
"keywords": [
39+
"prime",
40+
"primecms",
41+
"cli",
42+
"ink"
43+
],
44+
"dependencies": {
45+
"cli-spinners": "1.3.1",
46+
"copy-template-dir": "1.4.0",
47+
"ink": "2.0.0-11",
48+
"ink-text-input": "3.0.0-0",
49+
"lodash": "4.17.11",
50+
"meow": "5.0.0"
51+
},
52+
"gitHead": "31138953252872820486c9e13905c14de7f2067e"
53+
}
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { spawn } from 'child_process';
2+
import copyTemplateDir from 'copy-template-dir';
3+
import fs from 'fs';
4+
import { Box, Color, render } from 'ink';
5+
import TextInput from 'ink-text-input';
6+
import Static from 'ink/build/components/Static';
7+
import { kebabCase } from 'lodash';
8+
import path from 'path';
9+
import React from 'react';
10+
import { Spinner } from '../components/ink-spinner';
11+
12+
enum WizardState {
13+
PROJECT_NAME,
14+
POSTGRES_USERNAME,
15+
POSTGRES_PASSWORD,
16+
POSTGRES_DATABASE,
17+
INSTALL,
18+
ERROR,
19+
DONE,
20+
}
21+
22+
interface Props {
23+
projectName?: string;
24+
}
25+
26+
interface State {
27+
wizardState: WizardState;
28+
projectNameInvalid: boolean;
29+
projectName: string;
30+
postgresUsername: string;
31+
postgresPassword: string;
32+
postgresDatabase: string;
33+
installMessage: string;
34+
}
35+
36+
export const initCommand = cli => {
37+
setInterval(() => null, 100);
38+
return render(<InitCommand projectName={cli.input[1]} />);
39+
};
40+
41+
class InitCommand extends React.Component<Props, State> {
42+
public state = {
43+
wizardState: this.getInitialWizardState(),
44+
projectNameInvalid: false,
45+
projectName: this.props.projectName || '',
46+
postgresUsername: '',
47+
postgresPassword: '',
48+
postgresDatabase: '',
49+
installMessage: 'Installing...',
50+
};
51+
52+
public getInitialWizardState() {
53+
if (String(this.props.projectName || '').trim() === '') {
54+
return WizardState.PROJECT_NAME;
55+
}
56+
57+
return WizardState.POSTGRES_USERNAME;
58+
}
59+
60+
public componentDidMount() {
61+
const ENTER = '\r';
62+
process.stdin.on('data', data => {
63+
const { wizardState, projectName } = this.state;
64+
const s = String(data);
65+
if (s === ENTER) {
66+
if (wizardState === WizardState.PROJECT_NAME) {
67+
const projectNameInvalid = projectName.trim() === '';
68+
this.setState({ projectNameInvalid });
69+
if (projectNameInvalid) {
70+
return;
71+
}
72+
}
73+
74+
if (wizardState === WizardState.POSTGRES_USERNAME) {
75+
const value = this.state.postgresUsername;
76+
if (value.trim() === '') {
77+
this.setState({ postgresUsername: require('os').userInfo().username });
78+
}
79+
}
80+
81+
if (wizardState === WizardState.POSTGRES_PASSWORD) {
82+
const value = this.state.postgresPassword;
83+
if (value.trim() === '') {
84+
this.setState({ postgresPassword: '' });
85+
}
86+
}
87+
88+
if (wizardState === WizardState.POSTGRES_DATABASE) {
89+
const value = this.state.postgresDatabase;
90+
if (value.trim() === '') {
91+
this.setState({ postgresDatabase: 'prime' });
92+
}
93+
94+
this.install();
95+
}
96+
97+
this.setState({ wizardState: this.state.wizardState + 1 });
98+
}
99+
});
100+
}
101+
102+
public install() {
103+
const { projectName, postgresUsername, postgresPassword, postgresDatabase } = this.state;
104+
105+
const templateDir = path.join(__dirname, '..', 'template');
106+
const targetDir = path.join(process.cwd(), projectName);
107+
const vars = {
108+
projectName,
109+
projectNameKebabCase: kebabCase(projectName),
110+
connectionString: `postgresql://${postgresUsername}${
111+
postgresPassword ? `:${postgresPassword}` : ''
112+
}@localhost:5432/${postgresDatabase}`,
113+
};
114+
115+
if (fs.existsSync(targetDir)) {
116+
throw new Error('Target dir exists');
117+
}
118+
119+
this.setState({ installMessage: 'Copying template to project directory' });
120+
copyTemplateDir(templateDir, targetDir, vars, (err, createdFiles) => {
121+
if (err) {
122+
throw err;
123+
}
124+
this.setState({ installMessage: 'Installing dependencies' });
125+
126+
const installer = spawn('yarn', ['install'], { cwd: targetDir, detached: true });
127+
128+
installer.stdout.on('data', data => {
129+
this.setState({
130+
installMessage: data
131+
.toString()
132+
.trim()
133+
.split('\n')
134+
.pop(),
135+
});
136+
});
137+
138+
installer.on('close', () => {
139+
this.setState({ wizardState: WizardState.DONE }, () => {
140+
process.exit();
141+
});
142+
});
143+
});
144+
}
145+
146+
public renderStaticOrInput = (
147+
targetState,
148+
title,
149+
statePropertyName: string,
150+
passProps: any = {}
151+
) => {
152+
const { wizardState } = this.state;
153+
154+
if (wizardState === targetState) {
155+
return (
156+
<Box>
157+
<Box>{title}: </Box>
158+
<TextInput
159+
value={this.state[statePropertyName]}
160+
onChange={value => {
161+
this.setState({
162+
projectNameInvalid: false,
163+
[statePropertyName]: value,
164+
} as any);
165+
}}
166+
{...passProps}
167+
/>
168+
</Box>
169+
);
170+
} else if (wizardState >= targetState) {
171+
let value = this.state[statePropertyName];
172+
if (!value || value === '') {
173+
value = (passProps && passProps.placeholder) || '';
174+
}
175+
if (passProps.mask) {
176+
value = passProps.mask.repeat(value.length);
177+
}
178+
return (
179+
<Static>
180+
<Box>
181+
{title}: <Color green>{value}</Color>
182+
</Box>
183+
</Static>
184+
);
185+
}
186+
187+
return null;
188+
};
189+
190+
public render() {
191+
if (this.state.wizardState === WizardState.DONE) {
192+
return (
193+
<Box marginTop={1} flexDirection="column">
194+
<Box>Installation complete!</Box>
195+
<Box marginTop={1}>To start Prime CMS</Box>
196+
<Box marginTop={1} marginLeft={4} flexDirection="column">
197+
<Box>
198+
<Color green>cd {this.state.projectName}</Color>
199+
</Box>
200+
<Box>
201+
<Color green>yarn start</Color>
202+
</Box>
203+
</Box>
204+
<Box marginTop={1}>
205+
Submit issue if you have any problems: https://github.com/birkir/prime/issues
206+
</Box>
207+
</Box>
208+
);
209+
}
210+
211+
if (this.state.wizardState === WizardState.INSTALL) {
212+
return (
213+
<Box marginTop={1}>
214+
<Spinner type="dots12" yellow />
215+
<Box>
216+
{' '}
217+
<Color green>{this.state.installMessage}</Color>
218+
</Box>
219+
</Box>
220+
);
221+
}
222+
223+
return (
224+
<Box flexDirection="column">
225+
<Box>
226+
{this.renderStaticOrInput(WizardState.PROJECT_NAME, 'Project name', 'projectName')}
227+
{this.state.projectNameInvalid && <Color red>invalid</Color>}
228+
</Box>
229+
{this.renderStaticOrInput(
230+
WizardState.POSTGRES_USERNAME,
231+
'Postgres username',
232+
'postgresUsername',
233+
{ placeholder: require('os').userInfo().username }
234+
)}
235+
{this.renderStaticOrInput(
236+
WizardState.POSTGRES_PASSWORD,
237+
'Postgres password',
238+
'postgresPassword',
239+
{ mask: '*' }
240+
)}
241+
{this.renderStaticOrInput(
242+
WizardState.POSTGRES_DATABASE,
243+
'Postgres database',
244+
'postgresDatabase',
245+
{ placeholder: 'prime' }
246+
)}
247+
</Box>
248+
);
249+
}
250+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function startCommand(cli) {
2+
console.log('trying to find package.json and start the beast'); // tslint:disable-line no-console
3+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Box, Color, StdinContext } from 'ink';
2+
import React from 'react';
3+
4+
const Indicator = ({ isSelected }: { isSelected: boolean }) => {
5+
return (
6+
<Box>
7+
<Color blue>{isSelected ? `❯ ` : ' '}</Color>
8+
</Box>
9+
);
10+
};
11+
12+
const ARROW_UP = '\u001B[A';
13+
const ARROW_DOWN = '\u001B[B';
14+
const ENTER = '\r';
15+
const CTRL_C = '\x03';
16+
17+
const Item = ({ isSelected, label }: { isSelected: boolean; label: string }) => (
18+
<Color blue={isSelected}>{label}</Color>
19+
);
20+
21+
interface State {
22+
selectedIndex: number;
23+
}
24+
25+
export const SelectInput = (props: any) => (
26+
<StdinContext.Consumer>
27+
{({ stdin, setRawMode }) => <InkSelectInput stdin={stdin} setRawMode={setRawMode} {...props} />}
28+
</StdinContext.Consumer>
29+
);
30+
31+
class InkSelectInput extends React.Component<any, State> {
32+
public state = {
33+
selectedIndex: 0,
34+
};
35+
36+
public render() {
37+
const { items } = this.props;
38+
const { selectedIndex } = this.state;
39+
40+
const slicedItems = items;
41+
42+
return (
43+
<Box flexDirection="column">
44+
{slicedItems.map((item, index) => {
45+
const isSelected = index === selectedIndex;
46+
47+
return (
48+
<Box key={item.value}>
49+
<Indicator isSelected={isSelected} />
50+
<Item {...item} isSelected={isSelected} />
51+
</Box>
52+
);
53+
})}
54+
</Box>
55+
);
56+
}
57+
58+
public componentDidMount() {
59+
this.props.setRawMode(true);
60+
this.props.stdin.on('data', data => {
61+
const s = String(data);
62+
if (s === ARROW_UP) {
63+
this.setState({ selectedIndex: this.state.selectedIndex - 1 });
64+
} else if (s === ARROW_DOWN) {
65+
this.setState({ selectedIndex: this.state.selectedIndex + 1 });
66+
} else if (s === ENTER) {
67+
this.props.onSelect(this.props.items[this.state.selectedIndex]);
68+
} else if (s === CTRL_C) {
69+
this.props.onCancel();
70+
}
71+
});
72+
}
73+
}

0 commit comments

Comments
 (0)