Skip to content

Commit 9fc79b3

Browse files
committed
Add create template
1 parent 2a4a1cb commit 9fc79b3

File tree

8 files changed

+270
-62
lines changed

8 files changed

+270
-62
lines changed

npm-packages/cli/source/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Publish from './components/commands/publish.js';
66
import Help from './components/commands/help.js';
77
import Chat from './components/commands/chat.js';
88
import Logout from './components/commands/logout.js';
9+
import Create from './components/commands/create.js';
910

1011
export default function App({cli}: {cli: Result<Flags>}) {
1112
const [command, setCommand] = useState<string | undefined>(undefined);
@@ -22,6 +23,7 @@ export default function App({cli}: {cli: Result<Flags>}) {
2223
{command === 'login' && <Login cli={cli} />}
2324
{command === 'logout' && <Logout cli={cli} />}
2425
{command === 'publish' && <Publish cli={cli} />}
26+
{command === 'create' && <Create cli={cli} />}
2527
</>
2628
);
2729
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import {Result} from 'meow';
2+
import React, {useEffect, useState} from 'react';
3+
import {Flags} from '../../lib/cli-flags.js';
4+
import {Box, Text} from 'ink';
5+
import Spinner from 'ink-spinner';
6+
import {$} from 'execa';
7+
import SelectInput from 'ink-select-input';
8+
import {Item} from '../../lib/types.js';
9+
import {UncontrolledTextInput} from 'ink-text-input';
10+
import fs from 'fs';
11+
import path from 'path';
12+
13+
export default function Create({cli}: {cli: Result<Flags>}) {
14+
const [framework, setFramework] = useState<string | undefined>(undefined);
15+
const [projectName, setProjectName] = useState<string | undefined>(undefined);
16+
17+
const [isCreated, setIsCreated] = useState(false);
18+
const [isFrameworkSelected, setIsFrameworkSelected] = useState(false);
19+
20+
const [isPathValid, setIsPathValid] = useState(true);
21+
const [isCloneFailed, setIsCloneFailed] = useState(false);
22+
23+
const frameworkItems: Item<string>[] = [
24+
{
25+
label: 'React',
26+
value: 'react',
27+
},
28+
{
29+
label: 'Modern.js (WIP)',
30+
value: 'modernjs',
31+
},
32+
{
33+
label: 'Vue (WIP)',
34+
value: 'vue',
35+
},
36+
{
37+
label: 'Angular (WIP)',
38+
value: 'angular',
39+
},
40+
];
41+
42+
useEffect(() => {
43+
const framework = cli.flags.framework;
44+
setFramework(framework);
45+
}, [cli]);
46+
47+
useEffect(() => {
48+
async function createFromTemplate(name: string) {
49+
if (framework === 'react') {
50+
// Clone the template repository
51+
try {
52+
await $`git clone https://github.com/ClayPulse/pulse-editor-extension-template.git ${name}`;
53+
setIsCreated(true);
54+
} catch (error) {
55+
setIsCloneFailed(true);
56+
return;
57+
}
58+
59+
// Modify the package.json file to update the name
60+
const packageJsonPath = path.join(process.cwd(), name, 'package.json');
61+
const packageJson = JSON.parse(
62+
fs.readFileSync(packageJsonPath, 'utf8'),
63+
);
64+
packageJson.name = name;
65+
66+
// Write the modified package.json back to the file
67+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
68+
}
69+
}
70+
71+
if (projectName) {
72+
// Check if the project already exists
73+
const projectPath = path.join(process.cwd(), projectName);
74+
if (fs.existsSync(projectPath)) {
75+
setIsPathValid(false);
76+
return;
77+
}
78+
createFromTemplate(projectName);
79+
}
80+
}, [projectName]);
81+
82+
useEffect(() => {
83+
setTimeout(() => {
84+
setIsFrameworkSelected(framework !== undefined);
85+
}, 0);
86+
}, [framework]);
87+
88+
return (
89+
<>
90+
{!cli.flags.framework && (
91+
<>
92+
<Text>
93+
🚩Create a new Pulse Editor app using your favorite web framework!
94+
</Text>
95+
<SelectInput
96+
items={frameworkItems}
97+
onSelect={item => {
98+
setFramework(item.value);
99+
}}
100+
isFocused={framework === undefined}
101+
/>
102+
103+
<Text> </Text>
104+
</>
105+
)}
106+
107+
{isFrameworkSelected && (
108+
<>
109+
<Box>
110+
<Text>Enter your project name: </Text>
111+
<UncontrolledTextInput
112+
onSubmit={value => setProjectName(value)}
113+
focus={projectName === undefined}
114+
/>
115+
</Box>
116+
117+
{projectName && (
118+
<>
119+
{framework === 'react' &&
120+
(!isPathValid ? (
121+
<Text color="redBright">
122+
❌ A project with same name already exists in current path.
123+
</Text>
124+
) : isCloneFailed ? (
125+
<Text color="redBright">
126+
❌ Failed to clone the template. Please check your internet
127+
connection and try again.
128+
</Text>
129+
) : isCreated ? (
130+
<Text>
131+
🚀 Pulse Editor React app project created successfully!
132+
</Text>
133+
) : (
134+
<>
135+
<Box>
136+
<Spinner type="dots" />
137+
<Text>
138+
{' '}
139+
Creating a new Pulse Editor app using React template...
140+
</Text>
141+
</Box>
142+
</>
143+
))}
144+
{framework !== 'react' && (
145+
<Text>
146+
🚧 Currently not available. We'd like to invite you to work on
147+
these frameworks if you are interested in! Check out our
148+
tutorial to integrate your favorite web framework with Pulse
149+
Editor using Module Federation.
150+
</Text>
151+
)}
152+
</>
153+
)}
154+
</>
155+
)}
156+
</>
157+
);
158+
}

npm-packages/cli/source/components/commands/login.tsx

Lines changed: 22 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,31 @@ import {Box, Newline, Text, useApp} from 'ink';
33
import SelectInput from 'ink-select-input';
44
import {Flags} from '../../lib/cli-flags.js';
55
import {Result} from 'meow';
6-
import TextInput, {UncontrolledTextInput} from 'ink-text-input';
6+
import TextInput from 'ink-text-input';
77
import Spinner from 'ink-spinner';
88
import os from 'os';
99
import path from 'path';
10-
import fs from 'fs';
10+
import {getToken, isTokenInEnv, saveToken} from '../../lib/token.js';
11+
import {Item} from '../../lib/types.js';
1112

12-
type Item<V> = {
13-
key?: string;
14-
label: string;
15-
value: V;
16-
};
13+
type LoginMethod = 'token' | 'flow';
1714

1815
export default function Login({cli}: {cli: Result<Flags>}) {
19-
const [loginMethod, setLoginMethod] = useState<string | undefined>(undefined);
20-
const [isShowingSelect, setIsShowingSelect] = useState(false);
21-
const [isShowingInput, setIsShowingInput] = useState(false);
16+
const [loginMethod, setLoginMethod] = useState<LoginMethod | undefined>(
17+
undefined,
18+
);
2219

20+
const [isMethodSelected, setIsMethodSelected] = useState(false);
2321
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
2422
const [isAuthenticated, setIsAuthenticated] = useState(false);
2523
const [token, setToken] = useState<string>('');
2624

27-
const [isTokenInEnv, setIsTokenInEnv] = useState(false);
2825
const [isTokenSaved, setIsTokenSaved] = useState(false);
2926

3027
const [tokenInput, setTokenInput] = useState<string>('');
3128
const [saveTokenInput, setSaveTokenInput] = useState<string>('');
3229

33-
const loginMethodItems: Item<string>[] = [
30+
const loginMethodItems: Item<LoginMethod>[] = [
3431
{
3532
label: 'Login using access token',
3633
value: 'token',
@@ -57,14 +54,6 @@ export default function Login({cli}: {cli: Result<Flags>}) {
5754
}
5855
}, [cli]);
5956

60-
useEffect(() => {
61-
setIsShowingSelect(loginMethod === undefined);
62-
}, [loginMethod]);
63-
64-
useEffect(() => {
65-
setIsShowingInput(!isShowingSelect);
66-
}, [isShowingSelect]);
67-
6857
// Check token validity
6958
useEffect(() => {
7059
async function checkToken(token: string) {
@@ -92,56 +81,30 @@ export default function Login({cli}: {cli: Result<Flags>}) {
9281
}
9382
}, [token, loginMethod]);
9483

95-
function saveToken(token: string) {
96-
// Save the token to .pulse-editor/config.json in user home directory
97-
const configDir = path.join(os.homedir(), '.pulse-editor');
98-
const configFile = path.join(configDir, 'config.json');
99-
const config = {
100-
accessToken: token,
101-
};
102-
if (!fs.existsSync(configDir)) {
103-
fs.mkdirSync(configDir, {recursive: true});
104-
}
105-
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
106-
}
107-
108-
function getToken() {
109-
// First try to get the token from the environment variable
110-
const tokenEnv = process.env['PE_ACCESS_TOKEN'];
111-
if (tokenEnv) {
112-
setIsTokenInEnv(true);
113-
return tokenEnv;
114-
}
115-
116-
// If not found, try to get the token from the config file
117-
const configDir = path.join(os.homedir(), '.pulse-editor');
118-
const configFile = path.join(configDir, 'config.json');
119-
if (fs.existsSync(configFile)) {
120-
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
121-
if (config.accessToken) {
122-
return config.accessToken as string;
123-
}
124-
}
125-
126-
// If not found, return undefined
127-
return undefined;
128-
}
84+
useEffect(() => {
85+
setTimeout(() => {
86+
setIsMethodSelected(loginMethod !== undefined);
87+
}, 0);
88+
}, [loginMethod]);
12989

13090
return (
13191
<>
132-
{isShowingSelect && (
92+
{!cli.flags.token && !cli.flags.flow && (
13393
<>
13494
<Text>Login to the Pulse Editor Platform</Text>
13595
<SelectInput
13696
items={loginMethodItems}
13797
onSelect={item => {
13898
setLoginMethod(item.value);
13999
}}
100+
isFocused={loginMethod === undefined}
140101
/>
102+
103+
<Text> </Text>
141104
</>
142105
)}
143106

144-
{isShowingInput &&
107+
{isMethodSelected &&
145108
loginMethod === 'token' &&
146109
(token.length === 0 ? (
147110
<>
@@ -166,7 +129,7 @@ export default function Login({cli}: {cli: Result<Flags>}) {
166129
) : isAuthenticated ? (
167130
<>
168131
<Text>✅ You are signed in successfully.</Text>
169-
{!isTokenInEnv && getToken() !== token && (
132+
{!isTokenInEnv() && getToken() !== token && (
170133
<>
171134
<Text>
172135
🟢 It is recommended to save your access token as an
@@ -189,7 +152,7 @@ export default function Login({cli}: {cli: Result<Flags>}) {
189152
setIsTokenSaved(true);
190153
setTimeout(() => {
191154
exit();
192-
}, 100);
155+
}, 0);
193156
} else {
194157
exit();
195158
}
@@ -207,7 +170,7 @@ export default function Login({cli}: {cli: Result<Flags>}) {
207170
) : (
208171
<Text>Authentication error: please enter valid credentials.</Text>
209172
))}
210-
{isShowingInput && loginMethod === 'flow' && (
173+
{isMethodSelected && loginMethod === 'flow' && (
211174
<>
212175
<Text>(WIP) Open the following URL in your browser:</Text>
213176
<Text>https://pulse-editor.com/login</Text>
Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
11
import {Result} from 'meow';
22
import {Flags} from '../../lib/cli-flags.js';
3-
import React from 'react';
4-
import {Text} from 'ink';
3+
import React, {useEffect, useState} from 'react';
4+
import {Box, Text} from 'ink';
5+
import {saveToken} from '../../lib/token.js';
6+
import Spinner from 'ink-spinner';
57

68
export default function Logout({cli}: {cli: Result<Flags>}) {
7-
return <Text>Logout from the Pulse Editor Platform</Text>;
9+
const [isLoggedOut, setIsLoggedOut] = useState(false);
10+
11+
useEffect(() => {
12+
saveToken('');
13+
setIsLoggedOut(true);
14+
}, []);
15+
16+
return (
17+
<>
18+
{isLoggedOut ? (
19+
<Text>🚀 Successfully logged out!</Text>
20+
) : (
21+
<Box>
22+
<Spinner type="dots" />
23+
<Text> Logging out...</Text>
24+
</Box>
25+
)}
26+
</>
27+
);
828
}

npm-packages/cli/source/lib/cli-flags.tsx renamed to npm-packages/cli/source/lib/cli-flags.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export const flags = defineFlags({
1818
flow: {
1919
type: 'boolean',
2020
},
21+
framework: {
22+
type: 'string',
23+
shortFlag: 'f',
24+
},
2125
});
2226

2327
export type Flags = typeof flags;

npm-packages/cli/source/lib/manual.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,20 @@ const publish = `\
3131
publish Publish Pulse Editor Extension in current directory to the Pulse Editor Platform.
3232
`;
3333

34+
const create = `\
35+
create Create a new Pulse App using the starter template.
36+
Flags:
37+
--framework, -f [framework]
38+
The framework to use for the new app.
39+
Currently available options: react.
40+
Future options: vue, angular, etc.
41+
`;
42+
3443
export const commandsManual: Record<string, string> = {
3544
help,
3645
chat,
3746
login,
3847
logout,
3948
publish,
49+
create,
4050
};

0 commit comments

Comments
 (0)