@youcan/cli 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/auth/login.js +105 -0
- package/dist/cli/commands/auth/logout.js +20 -0
- package/dist/cli/commands/index.js +8 -0
- package/dist/cli/commands/theme/delete.js +43 -0
- package/dist/cli/commands/theme/dev.js +190 -0
- package/dist/cli/commands/theme/init.js +85 -0
- package/dist/cli/commands/theme/pull.js +54 -0
- package/dist/cli/index.js +51 -0
- package/dist/config/index.js +25 -0
- package/dist/config/messages.js +26 -0
- package/dist/core/client/client.js +65 -0
- package/dist/core/themes/preview.js +33 -0
- package/dist/index.js +3 -0
- package/dist/lib/cli/commands/auth/login.d.ts +2 -0
- package/dist/lib/cli/commands/auth/logout.d.ts +2 -0
- package/dist/lib/cli/commands/auth/types.d.ts +6 -0
- package/dist/lib/cli/commands/index.d.ts +6 -0
- package/dist/lib/cli/commands/theme/delete.d.ts +2 -0
- package/dist/lib/cli/commands/theme/dev.d.ts +2 -0
- package/dist/lib/cli/commands/theme/init.d.ts +2 -0
- package/dist/lib/cli/commands/theme/pull.d.ts +2 -0
- package/dist/lib/cli/commands/theme/types.d.ts +17 -0
- package/dist/lib/cli/commands/types.d.ts +22 -0
- package/dist/lib/cli/index.d.ts +12 -0
- package/dist/lib/config/index.d.ts +21 -0
- package/dist/lib/config/messages.d.ts +25 -0
- package/dist/lib/core/client/client.d.ts +17 -0
- package/dist/lib/core/client/types.d.ts +52 -0
- package/dist/lib/core/themes/preview.d.ts +1 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/utils/common.d.ts +21 -0
- package/dist/lib/utils/git/cloneRepository.d.ts +6 -0
- package/dist/lib/utils/helpers.d.ts +3 -0
- package/dist/lib/utils/http.d.ts +11 -0
- package/dist/lib/utils/network.d.ts +2 -0
- package/dist/lib/utils/system/deleteFile.d.ts +5 -0
- package/dist/lib/utils/system/openLink.d.ts +1 -0
- package/dist/lib/utils/system/saveFile.d.ts +1 -0
- package/dist/lib/utils/system/stdout.d.ts +18 -0
- package/dist/lib/utils/system/writeToFile.d.ts +6 -0
- package/dist/lib/utils/system/zipFolder.d.ts +4 -0
- package/dist/lib/utils/system.d.ts +1 -0
- package/dist/test/commands/auth/login.d.ts +2 -0
- package/dist/test/commands/auth/logout.d.ts +2 -0
- package/dist/test/commands/help.d.ts +2 -0
- package/dist/test/commands/theme/delete.d.ts +2 -0
- package/dist/test/commands/theme/dev.d.ts +2 -0
- package/dist/test/commands/theme/init.d.ts +2 -0
- package/dist/test/index.test.d.ts +1 -0
- package/dist/utils/common.js +57 -0
- package/dist/utils/git/cloneRepository.js +18 -0
- package/dist/utils/helpers.js +32 -0
- package/dist/utils/http.js +25 -0
- package/dist/utils/network.js +76 -0
- package/dist/utils/system/deleteFile.js +12 -0
- package/dist/utils/system/openLink.js +15 -0
- package/dist/utils/system/saveFile.js +9 -0
- package/dist/utils/system/stdout.js +15 -0
- package/dist/utils/system/writeToFile.js +12 -0
- package/dist/utils/system/zipFolder.js +32 -0
- package/dist/utils/system.js +17 -0
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { exit } from 'process';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import config from '../../../config/index.js';
|
|
5
|
+
import openLink from '../../../utils/system/openLink.js';
|
|
6
|
+
import stdout from '../../../utils/system/stdout.js';
|
|
7
|
+
import { post } from '../../../utils/http.js';
|
|
8
|
+
import writeToFile from '../../../utils/system/writeToFile.js';
|
|
9
|
+
import messages from '../../../config/messages.js';
|
|
10
|
+
import { isPortAvailable, getPidByPort } from '../../../utils/network.js';
|
|
11
|
+
import { kill } from '../../../utils/system.js';
|
|
12
|
+
import { LoadingSpinner } from '../../../utils/common.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Spin up a local server to handle the OAuth redirect. Times out after 10 seconds.
|
|
16
|
+
* @returns A promise that resolves when the code is received.
|
|
17
|
+
*/
|
|
18
|
+
function captureAuthorization() {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let timeOut;
|
|
21
|
+
const server = createServer((req, res) => {
|
|
22
|
+
const { url } = req;
|
|
23
|
+
const code = (url === null || url === void 0 ? void 0 : url.split('=')[1]) || '';
|
|
24
|
+
if (!code)
|
|
25
|
+
reject(new Error('authorization code is required.'));
|
|
26
|
+
res.end('You can close this window now.');
|
|
27
|
+
clearTimeout(timeOut);
|
|
28
|
+
server.close();
|
|
29
|
+
resolve(code);
|
|
30
|
+
});
|
|
31
|
+
server.listen(config.OAUTH_CALLBACK_PORT, () => {
|
|
32
|
+
timeOut = setTimeout(() => {
|
|
33
|
+
reject(new Error('Timeout'));
|
|
34
|
+
}, config.OAUTH_CALLBACK_SERVER_TIMEOUT);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Exchange code for access token
|
|
40
|
+
* @param authorizationCode string
|
|
41
|
+
*/
|
|
42
|
+
async function exchangeAuthCode(authorizationCode) {
|
|
43
|
+
const formParams = {
|
|
44
|
+
grant_type: 'authorization_code',
|
|
45
|
+
client_id: config.OAUTH_CLIENT_ID,
|
|
46
|
+
client_secret: config.OAUTH_CLIENT_SECRET,
|
|
47
|
+
redirect_uri: config.OAUTH_CALLBACK_URL,
|
|
48
|
+
code: authorizationCode,
|
|
49
|
+
};
|
|
50
|
+
const res = await post(config.OAUTH_ACCESS_TOKEN_URL, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
body: new URLSearchParams(formParams),
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return res.access_token;
|
|
58
|
+
}
|
|
59
|
+
function command(_cli) {
|
|
60
|
+
return {
|
|
61
|
+
name: 'login',
|
|
62
|
+
group: 'auth',
|
|
63
|
+
description: 'Log into a YouCan store',
|
|
64
|
+
action: async () => {
|
|
65
|
+
if (!(await isPortAvailable(config.OAUTH_CALLBACK_PORT))) {
|
|
66
|
+
stdout.warn(`YouCan CLI requires that the port ${config.OAUTH_CALLBACK_PORT} be available`);
|
|
67
|
+
const prompt = await prompts({
|
|
68
|
+
initial: false,
|
|
69
|
+
type: 'confirm',
|
|
70
|
+
name: 'terminate',
|
|
71
|
+
message: 'Terminate the process to free the port?',
|
|
72
|
+
});
|
|
73
|
+
if (!prompt.terminate) {
|
|
74
|
+
stdout.log('Exiting..');
|
|
75
|
+
exit(1);
|
|
76
|
+
}
|
|
77
|
+
const pid = await getPidByPort(config.OAUTH_CALLBACK_PORT);
|
|
78
|
+
if (pid) {
|
|
79
|
+
await LoadingSpinner.exec(`Terminating process ${pid}..`, async (spinner) => {
|
|
80
|
+
try {
|
|
81
|
+
await kill(pid, 'SIGTERM', 30000);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
spinner.error('Could not terminate process, proceeding..');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
openLink(config.OAUTH_AUTH_CODE_URL);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
stdout.log(messages.LOGIN_OPEN_LINK);
|
|
94
|
+
stdout.info(config.OAUTH_AUTH_CODE_URL);
|
|
95
|
+
}
|
|
96
|
+
await LoadingSpinner.exec('Authenticating..', async () => {
|
|
97
|
+
const authorization = await captureAuthorization();
|
|
98
|
+
writeToFile(config.CLI_GLOBAL_CONFIG_PATH, JSON.stringify({ access_token: await exchangeAuthCode(authorization) }));
|
|
99
|
+
});
|
|
100
|
+
stdout.info(messages.LOGIN_SUCCESS);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { command as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import config from '../../../config/index.js';
|
|
2
|
+
import messages from '../../../config/messages.js';
|
|
3
|
+
import deleteFile from '../../../utils/system/deleteFile.js';
|
|
4
|
+
import stdout from '../../../utils/system/stdout.js';
|
|
5
|
+
|
|
6
|
+
function command(cli) {
|
|
7
|
+
return {
|
|
8
|
+
name: 'logout',
|
|
9
|
+
group: 'auth',
|
|
10
|
+
description: 'Log out from the current store',
|
|
11
|
+
action: async () => {
|
|
12
|
+
if (!cli.client.isAuthenticated())
|
|
13
|
+
return stdout.error(messages.AUTH_USER_NOT_LOGGED_IN);
|
|
14
|
+
deleteFile(config.CLI_GLOBAL_CONFIG_PATH);
|
|
15
|
+
stdout.info(messages.AUTH_USER_LOGGED_OUT);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { command as default };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as LoginCommand } from './auth/login.js';
|
|
2
|
+
export { default as LogoutCommand } from './auth/logout.js';
|
|
3
|
+
export { default as InitCommand } from './theme/init.js';
|
|
4
|
+
export { default as DevCommand } from './theme/dev.js';
|
|
5
|
+
export { default as DeleteCommand } from './theme/delete.js';
|
|
6
|
+
export { default as PullCommand } from './theme/pull.js';
|
|
7
|
+
|
|
8
|
+
// auth
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import messages from '../../../config/messages.js';
|
|
3
|
+
import stdout from '../../../utils/system/stdout.js';
|
|
4
|
+
import { LoadingSpinner } from '../../../utils/common.js';
|
|
5
|
+
|
|
6
|
+
function command(cli) {
|
|
7
|
+
return {
|
|
8
|
+
name: 'delete',
|
|
9
|
+
group: 'theme',
|
|
10
|
+
description: 'Delete a remote development theme',
|
|
11
|
+
options: [],
|
|
12
|
+
action: async () => {
|
|
13
|
+
if (!cli.client.isAuthenticated())
|
|
14
|
+
return stdout.error(messages.AUTH_USER_NOT_LOGGED_IN);
|
|
15
|
+
const { dev: devThemes } = await cli.client.listThemes();
|
|
16
|
+
if (!devThemes.length)
|
|
17
|
+
return stdout.error(messages.DELETE_NO_REMOTE_THEMES);
|
|
18
|
+
const choices = devThemes.map(theme => ({
|
|
19
|
+
title: theme.name,
|
|
20
|
+
value: theme.id,
|
|
21
|
+
}));
|
|
22
|
+
const { themeId } = await prompts({
|
|
23
|
+
type: 'select',
|
|
24
|
+
name: 'themeId',
|
|
25
|
+
message: messages.DELETE_SELECT_THEME,
|
|
26
|
+
choices,
|
|
27
|
+
});
|
|
28
|
+
if (!themeId)
|
|
29
|
+
return stdout.error(messages.DELETE_NO_THEME_SELECTED);
|
|
30
|
+
await LoadingSpinner.exec(`${messages.DELETE_IN_PROGRESS} ${themeId}..`, async (spinner) => {
|
|
31
|
+
try {
|
|
32
|
+
await cli.client.deleteTheme(themeId);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
spinner.error(messages.DELETE_ERROR);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
stdout.info(messages.DELETE_THEME_DELETED);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { command as default };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { cwd } from 'process';
|
|
2
|
+
import { clear } from 'console';
|
|
3
|
+
import { readdirSync, existsSync, readFileSync } from 'fs';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import chokidar from 'chokidar';
|
|
6
|
+
import kleur from 'kleur';
|
|
7
|
+
import { fileFromPathSync } from 'formdata-node/file-from-path';
|
|
8
|
+
import io from 'socket.io-client';
|
|
9
|
+
import stdout from '../../../utils/system/stdout.js';
|
|
10
|
+
import { getCurrentThemeId, LoadingSpinner } from '../../../utils/common.js';
|
|
11
|
+
import config from '../../../config/index.js';
|
|
12
|
+
import previewTheme from '../../../core/themes/preview.js';
|
|
13
|
+
import messages from '../../../config/messages.js';
|
|
14
|
+
|
|
15
|
+
const sizeFormatter = Intl.NumberFormat('en', {
|
|
16
|
+
notation: 'compact',
|
|
17
|
+
style: 'unit',
|
|
18
|
+
unit: 'byte',
|
|
19
|
+
unitDisplay: 'narrow',
|
|
20
|
+
});
|
|
21
|
+
const eventLogTagMap = {
|
|
22
|
+
error: () => kleur.bold().red('[error]'),
|
|
23
|
+
add: () => kleur.bold().green('[created]'),
|
|
24
|
+
change: () => kleur.bold().blue('[updated]'),
|
|
25
|
+
unlink: () => kleur.bold().yellow('[deleted]'),
|
|
26
|
+
};
|
|
27
|
+
function logFileEvent(options) {
|
|
28
|
+
const tag = eventLogTagMap[options.event]();
|
|
29
|
+
return stdout.log(`${tag} ${kleur.underline().white(options.path)} - ${sizeFormatter.format(options.size)} | ${options.roundtrip}ms \n`);
|
|
30
|
+
}
|
|
31
|
+
function connectPreviewServer() {
|
|
32
|
+
const socket = io(`ws://localhost:${config.PREVIEW_SERVER_PORT}`);
|
|
33
|
+
socket.on('connect', () => {
|
|
34
|
+
stdout.log(messages.DEV_PREVIEW_SERVER_CONNECTED);
|
|
35
|
+
});
|
|
36
|
+
return socket;
|
|
37
|
+
}
|
|
38
|
+
async function syncChanges(cli, themeId) {
|
|
39
|
+
const meta = await cli.client.getThemeMeta(themeId);
|
|
40
|
+
for (const fileType of config.THEME_FILE_TYPES) {
|
|
41
|
+
const files = meta[fileType];
|
|
42
|
+
const dirFiles = readdirSync(`./${fileType}`);
|
|
43
|
+
// save schema before data
|
|
44
|
+
if (fileType === 'config') {
|
|
45
|
+
const fileOrder = ['settings_schema.json', 'settings_data.json'];
|
|
46
|
+
files.sort((a, b) => fileOrder.indexOf(a.file_name) - fileOrder.indexOf(b.file_name));
|
|
47
|
+
}
|
|
48
|
+
// add newly created files
|
|
49
|
+
if (dirFiles.length > 0) {
|
|
50
|
+
const newFiles = dirFiles.filter(file => !files.find(f => f.file_name === file));
|
|
51
|
+
for (const file of newFiles) {
|
|
52
|
+
try {
|
|
53
|
+
const fileData = fileFromPathSync(`./${fileType}/${file}`);
|
|
54
|
+
await cli.client.updateFile(themeId, {
|
|
55
|
+
file_type: fileType,
|
|
56
|
+
file_name: file,
|
|
57
|
+
file_operation: 'save',
|
|
58
|
+
file_content: fileData,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err instanceof Error)
|
|
63
|
+
stdout.error(`[error] ${file}: ${err.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// update remote with local changes
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const filePath = `${file.type}/${file.file_name}`;
|
|
70
|
+
if (!existsSync(filePath)) {
|
|
71
|
+
try {
|
|
72
|
+
await cli.client.deleteFile(themeId, {
|
|
73
|
+
file_type: file.type,
|
|
74
|
+
file_name: file.file_name,
|
|
75
|
+
file_operation: 'delete',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (err instanceof Error)
|
|
80
|
+
stdout.error(`[error] ${file.file_name}: ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const fileStream = readFileSync(filePath);
|
|
85
|
+
const localHash = crypto.createHash('sha1');
|
|
86
|
+
localHash.update(fileStream);
|
|
87
|
+
if (localHash.digest('hex') !== file.hash) {
|
|
88
|
+
try {
|
|
89
|
+
await cli.client.updateFile(themeId, {
|
|
90
|
+
file_type: file.type,
|
|
91
|
+
file_name: file.file_name,
|
|
92
|
+
file_operation: 'save',
|
|
93
|
+
file_content: fileFromPathSync(filePath),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err instanceof Error)
|
|
98
|
+
stdout.error(`[error] ${file.file_name}: ${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function command(cli) {
|
|
105
|
+
return {
|
|
106
|
+
name: 'dev',
|
|
107
|
+
group: 'theme',
|
|
108
|
+
description: 'starts a dev server and watches over the current directory',
|
|
109
|
+
options: [
|
|
110
|
+
{ name: '-p, --preview', description: 'opens a preview window' },
|
|
111
|
+
],
|
|
112
|
+
action: async (options) => {
|
|
113
|
+
let socket;
|
|
114
|
+
if (!cli.client.isAuthenticated())
|
|
115
|
+
return stdout.error(messages.AUTH_USER_NOT_LOGGED_IN);
|
|
116
|
+
const themeId = await getCurrentThemeId(cwd());
|
|
117
|
+
if (!themeId)
|
|
118
|
+
return stdout.error(messages.DEV_NO_THEME_DETECTED);
|
|
119
|
+
const { domain } = await cli.client.getStoreInfo();
|
|
120
|
+
clear();
|
|
121
|
+
const loadingSpinner = new LoadingSpinner('Syncing changes...');
|
|
122
|
+
loadingSpinner.start();
|
|
123
|
+
await syncChanges(cli, themeId);
|
|
124
|
+
loadingSpinner.stop();
|
|
125
|
+
if (options.preview) {
|
|
126
|
+
socket = connectPreviewServer();
|
|
127
|
+
socket.emit('theme:dev', { themeId });
|
|
128
|
+
previewTheme(`https://${domain}/themes/${themeId}/preview`);
|
|
129
|
+
}
|
|
130
|
+
stdout.info(`Watching for changes in ${kleur.bold().white(cwd())}...`);
|
|
131
|
+
chokidar
|
|
132
|
+
.watch(config.THEME_FILE_TYPES, {
|
|
133
|
+
persistent: true,
|
|
134
|
+
ignoreInitial: true,
|
|
135
|
+
awaitWriteFinish: {
|
|
136
|
+
stabilityThreshold: 50,
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
.on('all', async (event, path, stats) => {
|
|
140
|
+
var _a, _b;
|
|
141
|
+
const start = new Date().getTime();
|
|
142
|
+
try {
|
|
143
|
+
const [filetype, filename] = path.split('/', 2);
|
|
144
|
+
if (!config.THEME_FILE_TYPES.includes(filetype))
|
|
145
|
+
return;
|
|
146
|
+
if (!['add', 'change', 'unlink'].includes(event))
|
|
147
|
+
return;
|
|
148
|
+
switch (event) {
|
|
149
|
+
case 'add':
|
|
150
|
+
case 'change':
|
|
151
|
+
await cli.client.updateFile(themeId, {
|
|
152
|
+
file_type: filetype,
|
|
153
|
+
file_name: filename,
|
|
154
|
+
file_operation: 'save',
|
|
155
|
+
file_content: fileFromPathSync(path),
|
|
156
|
+
});
|
|
157
|
+
break;
|
|
158
|
+
case 'unlink':
|
|
159
|
+
await cli.client.deleteFile(themeId, {
|
|
160
|
+
file_type: filetype,
|
|
161
|
+
file_name: filename,
|
|
162
|
+
file_operation: 'delete',
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (socket)
|
|
167
|
+
socket.emit('theme:update', { themeId });
|
|
168
|
+
logFileEvent({
|
|
169
|
+
path,
|
|
170
|
+
event,
|
|
171
|
+
size: (_a = stats === null || stats === void 0 ? void 0 : stats.size) !== null && _a !== void 0 ? _a : 0,
|
|
172
|
+
roundtrip: new Date().getTime() - start,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
logFileEvent({
|
|
177
|
+
path,
|
|
178
|
+
event: 'error',
|
|
179
|
+
size: (_b = stats === null || stats === void 0 ? void 0 : stats.size) !== null && _b !== void 0 ? _b : 0,
|
|
180
|
+
roundtrip: new Date().getTime() - start,
|
|
181
|
+
});
|
|
182
|
+
if (err instanceof Error)
|
|
183
|
+
stdout.info(`message: ${err.message}`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { command as default };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { cwd } from 'process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import { fileFromPath } from 'formdata-node/file-from-path';
|
|
5
|
+
import config from '../../../config/index.js';
|
|
6
|
+
import cloneRepository from '../../../utils/git/cloneRepository.js';
|
|
7
|
+
import stdout from '../../../utils/system/stdout.js';
|
|
8
|
+
import zipFolder from '../../../utils/system/zipFolder.js';
|
|
9
|
+
import writeToFile from '../../../utils/system/writeToFile.js';
|
|
10
|
+
import deleteFile from '../../../utils/system/deleteFile.js';
|
|
11
|
+
import messages from '../../../config/messages.js';
|
|
12
|
+
|
|
13
|
+
const inquiries = [
|
|
14
|
+
{
|
|
15
|
+
type: 'text',
|
|
16
|
+
name: 'theme_name',
|
|
17
|
+
message: 'The theme\'s name, used for display purposes.',
|
|
18
|
+
initial: 'Starter',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: 'text',
|
|
22
|
+
name: 'theme_author',
|
|
23
|
+
message: 'The theme\'s author',
|
|
24
|
+
initial: 'YouCan',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'text',
|
|
28
|
+
name: 'theme_version',
|
|
29
|
+
message: 'The theme\'s current version',
|
|
30
|
+
initial: '1.0.0',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: 'text',
|
|
34
|
+
name: 'theme_support_url',
|
|
35
|
+
message: 'A support URL for this theme.',
|
|
36
|
+
initial: 'https://developer.youcan.shop',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'text',
|
|
40
|
+
name: 'theme_documentation_url',
|
|
41
|
+
message: 'A documentation URL for this theme.',
|
|
42
|
+
initial: 'https://developer.youcan.shop',
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
const defaultInquiries = {
|
|
46
|
+
theme_name: 'Starter',
|
|
47
|
+
theme_author: 'YouCan',
|
|
48
|
+
theme_version: '1.0.0',
|
|
49
|
+
theme_support_url: 'https://developer.youcan.shop',
|
|
50
|
+
theme_documentation_url: 'https://developer.youcan.shop',
|
|
51
|
+
};
|
|
52
|
+
function getSelectedTheme(optionTheme) {
|
|
53
|
+
var _a;
|
|
54
|
+
const selectedTheme = (_a = config.AVAILABLE_THEMES.find(theme => theme.name === (optionTheme === null || optionTheme === void 0 ? void 0 : optionTheme.toLocaleLowerCase().trim()))) === null || _a === void 0 ? void 0 : _a.repository;
|
|
55
|
+
if (selectedTheme)
|
|
56
|
+
return selectedTheme;
|
|
57
|
+
return config.STARTER_THEME_GIT_REPOSITORY;
|
|
58
|
+
}
|
|
59
|
+
function command(cli) {
|
|
60
|
+
return {
|
|
61
|
+
name: 'init',
|
|
62
|
+
group: 'theme',
|
|
63
|
+
description: 'Create a new theme or clone existing one.',
|
|
64
|
+
options: [
|
|
65
|
+
{ name: '-t, --theme <theme>', description: 'Specify a theme name e.g. cod-theme' },
|
|
66
|
+
{ name: '-d, --default', description: 'Use default values for theme name, author, version, support url and documentation url.' },
|
|
67
|
+
],
|
|
68
|
+
action: async (options) => {
|
|
69
|
+
if (!cli.client.isAuthenticated())
|
|
70
|
+
return stdout.error(messages.AUTH_USER_NOT_LOGGED_IN);
|
|
71
|
+
const info = options.default ? defaultInquiries : await prompts(inquiries);
|
|
72
|
+
stdout.info(messages.INIT_CLONE_START);
|
|
73
|
+
const themeRepository = getSelectedTheme(options.theme);
|
|
74
|
+
cloneRepository(themeRepository, info.theme_name);
|
|
75
|
+
const zippedTheme = await zipFolder(cwd(), info.theme_name);
|
|
76
|
+
const themeFolderRs = await fileFromPath(zippedTheme);
|
|
77
|
+
const id = await cli.client.initTheme({ ...info, archive: themeFolderRs });
|
|
78
|
+
writeToFile(path.resolve(cwd(), info.theme_name, '.youcan'), JSON.stringify({ theme_id: id }));
|
|
79
|
+
deleteFile(zippedTheme);
|
|
80
|
+
stdout.info(`${messages.INIT_SUCCESS}${id}`);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { command as default };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cwd } from 'process';
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
import decompress from 'decompress';
|
|
4
|
+
import stdout from '../../../utils/system/stdout.js';
|
|
5
|
+
import { saveHttpFile } from '../../../utils/system/saveFile.js';
|
|
6
|
+
import { getCurrentThemeId } from '../../../utils/common.js';
|
|
7
|
+
import writeToFile from '../../../utils/system/writeToFile.js';
|
|
8
|
+
import deleteFile from '../../../utils/system/deleteFile.js';
|
|
9
|
+
import messages from '../../../config/messages.js';
|
|
10
|
+
|
|
11
|
+
function command(cli) {
|
|
12
|
+
return {
|
|
13
|
+
name: 'pull',
|
|
14
|
+
group: 'theme',
|
|
15
|
+
description: 'Pull a theme',
|
|
16
|
+
options: [],
|
|
17
|
+
action: async () => {
|
|
18
|
+
if (!cli.client.isAuthenticated())
|
|
19
|
+
return stdout.error(messages.AUTH_USER_NOT_LOGGED_IN);
|
|
20
|
+
const { dev } = await cli.client.listThemes();
|
|
21
|
+
const choices = dev.map(theme => ({
|
|
22
|
+
title: theme.name,
|
|
23
|
+
value: theme.id,
|
|
24
|
+
}));
|
|
25
|
+
let themeId;
|
|
26
|
+
const cwdThemeId = await getCurrentThemeId(cwd());
|
|
27
|
+
if (!cwdThemeId) {
|
|
28
|
+
const promt = await prompts({
|
|
29
|
+
type: 'select',
|
|
30
|
+
name: 'themeId',
|
|
31
|
+
message: 'Select a theme to pull',
|
|
32
|
+
choices,
|
|
33
|
+
});
|
|
34
|
+
themeId = promt.themeId;
|
|
35
|
+
}
|
|
36
|
+
themeId = themeId || cwdThemeId;
|
|
37
|
+
if (!themeId)
|
|
38
|
+
return stdout.error(messages.PULL_NO_THEME_FOUND);
|
|
39
|
+
const fileName = `${themeId}`;
|
|
40
|
+
const fileNameZip = `${fileName}.zip`;
|
|
41
|
+
stdout.info(messages.PULL_PULLING_THEME);
|
|
42
|
+
const response = await cli.client.pullTheme(themeId);
|
|
43
|
+
await saveHttpFile(response, fileNameZip);
|
|
44
|
+
stdout.info(messages.PULL_UNPACKING_THEME);
|
|
45
|
+
const unpackingFolder = cwdThemeId ? cwd() : `${cwd()}/${fileName}`;
|
|
46
|
+
await decompress(fileNameZip, unpackingFolder);
|
|
47
|
+
deleteFile(fileNameZip);
|
|
48
|
+
writeToFile(`${unpackingFolder}/.youcan`, JSON.stringify({ theme_id: themeId }));
|
|
49
|
+
stdout.info(`${messages.PULL_THEME_PULLED} ${fileName}`);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { command as default };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, promises } from 'fs';
|
|
2
|
+
import { cac } from 'cac';
|
|
3
|
+
import * as index from './commands/index.js';
|
|
4
|
+
import Client from '../core/client/client.js';
|
|
5
|
+
import config from '../config/index.js';
|
|
6
|
+
|
|
7
|
+
const cli = {
|
|
8
|
+
client: new Client(),
|
|
9
|
+
handler: cac('youcan'),
|
|
10
|
+
registerCommand(command) {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
const definition = command(this);
|
|
13
|
+
const instance = this.handler
|
|
14
|
+
.command(definition.name, definition.description)
|
|
15
|
+
.action(definition.action);
|
|
16
|
+
(_a = definition.aliases) === null || _a === void 0 ? void 0 : _a.forEach(a => instance.alias(a));
|
|
17
|
+
(_b = definition.options) === null || _b === void 0 ? void 0 : _b.forEach(o => instance.option(o.name, o.description, o.config));
|
|
18
|
+
},
|
|
19
|
+
getAvailableCommands() {
|
|
20
|
+
return Object.values(index);
|
|
21
|
+
},
|
|
22
|
+
async init() {
|
|
23
|
+
await this.prepareClient();
|
|
24
|
+
Object.values(index).forEach(command => this.registerCommand(command));
|
|
25
|
+
this.handler.on('command:*', () => {
|
|
26
|
+
this.handler.outputHelp();
|
|
27
|
+
});
|
|
28
|
+
this.handler.help();
|
|
29
|
+
this.handler.parse();
|
|
30
|
+
},
|
|
31
|
+
async prepareClient() {
|
|
32
|
+
if (!existsSync(config.CLI_GLOBAL_CONFIG_DIR))
|
|
33
|
+
await promises.mkdir(config.CLI_GLOBAL_CONFIG_DIR);
|
|
34
|
+
if (!existsSync(config.CLI_GLOBAL_CONFIG_PATH))
|
|
35
|
+
return await promises.writeFile(config.CLI_GLOBAL_CONFIG_PATH, '', { flag: 'wx', encoding: 'utf-8' });
|
|
36
|
+
const data = await promises
|
|
37
|
+
.readFile(config.CLI_GLOBAL_CONFIG_PATH, 'utf-8')
|
|
38
|
+
.then((b) => {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(b);
|
|
41
|
+
}
|
|
42
|
+
catch (_a) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if ('access_token' in data)
|
|
47
|
+
this.client.setAccessToken(data.access_token);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export { cli as default };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
var config = {
|
|
5
|
+
OAUTH_CLIENT_ID: 8,
|
|
6
|
+
OAUTH_CALLBACK_PORT: 3000,
|
|
7
|
+
OAUTH_CALLBACK_SERVER_TIMEOUT: 5 * 60 * 100,
|
|
8
|
+
OAUTH_CALLBACK_URL: 'http://localhost:3000/',
|
|
9
|
+
OAUTH_CLIENT_SECRET: 'lvUw2mQ7nXp4WqZ9CZlURMgRGAra3KuOrYhFlU7X',
|
|
10
|
+
OAUTH_AUTH_CODE_URL: 'https://seller-area.youcan.shop/admin/oauth/authorize?response_type=code&client_id=8&redirect_url=http://localhost:3000/&state=',
|
|
11
|
+
OAUTH_ACCESS_TOKEN_URL: 'https://seller-area.youcan.shop/admin/oauth/token',
|
|
12
|
+
SELLER_AREA_API_BASE_URI: 'https://api.youcan.shop',
|
|
13
|
+
SELLER_AREA_WEB_BASE_URI: 'https://seller-area.youcan.shop',
|
|
14
|
+
STARTER_THEME_GIT_REPOSITORY: 'git@github.com:youcan-shop/light-theme.git',
|
|
15
|
+
AVAILABLE_THEMES: [
|
|
16
|
+
{ name: 'default', repository: 'git@github.com:youcan-shop/light-theme.git' },
|
|
17
|
+
{ name: 'cod-theme', repository: 'git@github.com:youcan-shop/cod-theme.git' },
|
|
18
|
+
],
|
|
19
|
+
CLI_GLOBAL_CONFIG_DIR: path.resolve(homedir(), '.youcan'),
|
|
20
|
+
CLI_GLOBAL_CONFIG_PATH: path.resolve(homedir(), '.youcan', 'config.json'),
|
|
21
|
+
THEME_FILE_TYPES: ['layouts', 'sections', 'locales', 'assets', 'snippets', 'config', 'templates'],
|
|
22
|
+
PREVIEW_SERVER_PORT: 7565,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export { config as default };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
var messages = {
|
|
2
|
+
LOGIN_OPEN_LINK: 'Open this link in your browser to continue authentication:',
|
|
3
|
+
LOGIN_SUCCESS: 'You have been successfully logged in.',
|
|
4
|
+
INIT_SUCCESS: 'The theme has been initiated with id: ',
|
|
5
|
+
INIT_CLONE_START: 'Cloning your theme from github',
|
|
6
|
+
AUTH_USER_NOT_LOGGED_IN: 'You are not currently logged into any store.',
|
|
7
|
+
AUTH_USER_LOGGED_OUT: 'You have been successfully logged out.',
|
|
8
|
+
DELETE_NO_THEME_SELECTED: 'No theme selected.',
|
|
9
|
+
DELETE_THEME_DELETED: 'Theme deleted successfully.',
|
|
10
|
+
DELETE_NO_REMOTE_THEMES: 'This store does not have any development themes.',
|
|
11
|
+
DELETE_SELECT_THEME: 'Which remote development theme would you like to delete?',
|
|
12
|
+
DELETE_IN_PROGRESS: 'Deleting theme',
|
|
13
|
+
DELETE_ERROR: 'Could not delete remote development theme.',
|
|
14
|
+
DEV_PREVIEW_SERVER_CONNECTED: 'Connected to preview server',
|
|
15
|
+
DEV_WATCHING_FILES: 'Watching theme files for changes...',
|
|
16
|
+
DEV_NO_THEME_DETECTED: 'No theme detected in the current directory.',
|
|
17
|
+
PULL_NO_THEME_FOUND: 'No theme found',
|
|
18
|
+
PULL_PULLING_THEME: 'Pulling your theme...',
|
|
19
|
+
PULL_UNPACKING_THEME: 'Unpacking...',
|
|
20
|
+
PULL_THEME_PULLED: 'Theme has been pulled to ',
|
|
21
|
+
PREVIEW_BROWSER_CLOSED: 'Browser closed',
|
|
22
|
+
PREVIEW_THEME_UPDATED: 'Theme change detected, reloading...',
|
|
23
|
+
PREVIEW_RELOADED: 'Reloaded in ',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { messages as default };
|