@youcan/cli 1.0.5 → 1.0.6

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.
@@ -1,61 +1,14 @@
1
- import { createServer } from 'http';
2
1
  import { exit } from 'process';
3
2
  import prompts from 'prompts';
4
3
  import config from '../../../config/index.js';
5
- import openLink from '../../../utils/system/openLink.js';
6
4
  import stdout from '../../../utils/system/stdout.js';
7
- import { post } from '../../../utils/http.js';
8
5
  import writeToFile from '../../../utils/system/writeToFile.js';
9
6
  import messages from '../../../config/messages.js';
10
7
  import { isPortAvailable, getPidByPort } from '../../../utils/network.js';
11
8
  import { kill } from '../../../utils/system.js';
12
9
  import { LoadingSpinner } from '../../../utils/common.js';
10
+ import cli from '../../index.js';
13
11
 
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
12
  function command(_cli) {
60
13
  return {
61
14
  name: 'login',
@@ -86,18 +39,49 @@ function command(_cli) {
86
39
  });
87
40
  }
88
41
  }
42
+ const loading = new LoadingSpinner('Authenticating...');
89
43
  try {
90
- openLink(config.OAUTH_AUTH_CODE_URL);
44
+ const inquiries = [
45
+ {
46
+ type: 'text',
47
+ name: 'email',
48
+ message: 'Type your email',
49
+ validate: value => value !== '',
50
+ },
51
+ {
52
+ type: 'password',
53
+ name: 'password',
54
+ message: 'Type your password',
55
+ validate: value => value !== '',
56
+ },
57
+ ];
58
+ const credentials = await prompts(inquiries);
59
+ loading.start();
60
+ const response = await cli.client.auth({ email: credentials.email, password: credentials.password });
61
+ let token = response.token;
62
+ loading.stop();
63
+ cli.client.setAccessToken(response.token);
64
+ if (response.stores.length > 1) {
65
+ const choices = response.stores.map(store => ({
66
+ title: store.slug,
67
+ value: store.store_id,
68
+ }));
69
+ const { storeId } = await prompts({
70
+ type: 'select',
71
+ name: 'storeId',
72
+ message: messages.SELECT_STORE,
73
+ choices,
74
+ });
75
+ const selectStoreResponse = await cli.client.selectStore({ id: storeId });
76
+ token = selectStoreResponse.token;
77
+ }
78
+ writeToFile(config.CLI_GLOBAL_CONFIG_PATH, JSON.stringify({ access_token: token }));
79
+ stdout.info(messages.LOGIN_SUCCESS);
91
80
  }
92
81
  catch (err) {
93
- stdout.log(messages.LOGIN_OPEN_LINK);
94
- stdout.info(config.OAUTH_AUTH_CODE_URL);
82
+ const error = JSON.parse(err.message);
83
+ loading.error(error.detail);
95
84
  }
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
85
  },
102
86
  };
103
87
  }
@@ -1,9 +1,12 @@
1
1
  export { default as LoginCommand } from './auth/login.js';
2
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
- export { default as PackCommand } from './theme/pack.js';
3
+ export { default as ThemeInitCommand } from './theme/init.js';
4
+ export { default as ThemeListCommand } from './theme/list.js';
5
+ export { default as ThemeDevCommand } from './theme/dev.js';
6
+ export { default as ThemeDeleteCommand } from './theme/delete.js';
7
+ export { default as ThemePullCommand } from './theme/pull.js';
8
+ export { default as ThemePackCommand } from './theme/pack.js';
9
+ export { default as StoreInfoCommand } from './store/info.js';
10
+ export { default as StoreSwitchCommand } from './store/switch.js';
8
11
 
9
12
  // auth
@@ -0,0 +1,28 @@
1
+ import messages from '../../../config/messages.js';
2
+ import stdout from '../../../utils/system/stdout.js';
3
+ import { LoadingSpinner } from '../../../utils/common.js';
4
+
5
+ function command(cli) {
6
+ return {
7
+ name: 'store:info',
8
+ group: 'theme',
9
+ description: 'Current development store info',
10
+ options: [],
11
+ action: async () => {
12
+ if (!cli.client.isAuthenticated())
13
+ return stdout.error(messages.AUTH_USER_NOT_LOGGED_IN);
14
+ await LoadingSpinner.exec(`${messages.FETCHING_CURRENT_STORE_INFO}..`, async (spinner) => {
15
+ try {
16
+ const storeInfo = await cli.client.getStoreInfo();
17
+ spinner.stop();
18
+ stdout.info(`${messages.CURRENT_DEVELOPMENT_STORE}: ${storeInfo.slug}`);
19
+ }
20
+ catch (err) {
21
+ spinner.error(messages.ERROR_WHILE_FETCHING_CURRENT_STORE_INFO);
22
+ }
23
+ });
24
+ },
25
+ };
26
+ }
27
+
28
+ export { command as default };
@@ -0,0 +1,56 @@
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
+ import writeToFile from '../../../utils/system/writeToFile.js';
6
+ import config from '../../../config/index.js';
7
+
8
+ function command(cli) {
9
+ return {
10
+ name: 'store:switch',
11
+ group: 'theme',
12
+ description: 'Switch the development store',
13
+ options: [],
14
+ action: async () => {
15
+ if (!cli.client.isAuthenticated())
16
+ return stdout.error(messages.AUTH_USER_NOT_LOGGED_IN);
17
+ let storeInfo;
18
+ await LoadingSpinner.exec(`${messages.FETCHING_CURRENT_STORE_INFO}..`, async (spinner) => {
19
+ try {
20
+ storeInfo = await cli.client.getStoreInfo();
21
+ }
22
+ catch (err) {
23
+ spinner.error(messages.ERROR_WHILE_FETCHING_CURRENT_STORE_INFO);
24
+ }
25
+ });
26
+ stdout.info(`${messages.CURRENT_DEVELOPMENT_STORE}: ${storeInfo.slug}`);
27
+ const { stores } = await cli.client.listStores();
28
+ if (!stores.length)
29
+ return stdout.error(messages.NO_STORE_FOUND);
30
+ const choices = stores.map(store => ({
31
+ title: store.slug,
32
+ value: store.store_id,
33
+ }));
34
+ const { storeId } = await prompts({
35
+ type: 'select',
36
+ name: 'storeId',
37
+ message: messages.SELECT_STORE,
38
+ choices,
39
+ });
40
+ if (!storeId)
41
+ return stdout.error(messages.NO_STORE_SELECTED);
42
+ await LoadingSpinner.exec(`${messages.SELECT_STORE_IN_PROGRESS}..`, async (spinner) => {
43
+ try {
44
+ const selectStoreResponse = await cli.client.selectStore({ id: storeId });
45
+ writeToFile(config.CLI_GLOBAL_CONFIG_PATH, JSON.stringify({ access_token: selectStoreResponse.token }));
46
+ }
47
+ catch (err) {
48
+ spinner.error(messages.CANNOT_SELECT_STORE);
49
+ }
50
+ });
51
+ stdout.info(messages.STORE_SELECTED);
52
+ },
53
+ };
54
+ }
55
+
56
+ export { command as default };
@@ -5,7 +5,7 @@ import { LoadingSpinner } from '../../../utils/common.js';
5
5
 
6
6
  function command(cli) {
7
7
  return {
8
- name: 'delete',
8
+ name: 'theme:delete',
9
9
  group: 'theme',
10
10
  description: 'Delete a remote development theme',
11
11
  options: [],
@@ -14,7 +14,7 @@ function command(cli) {
14
14
  return stdout.error(messages.AUTH_USER_NOT_LOGGED_IN);
15
15
  const { dev: devThemes } = await cli.client.listThemes();
16
16
  if (!devThemes.length)
17
- return stdout.error(messages.DELETE_NO_REMOTE_THEMES);
17
+ return stdout.error(messages.NO_REMOTE_THEMES);
18
18
  const choices = devThemes.map(theme => ({
19
19
  title: theme.name,
20
20
  value: theme.id,
@@ -103,7 +103,7 @@ async function syncChanges(cli, themeId) {
103
103
  }
104
104
  function command(cli) {
105
105
  return {
106
- name: 'dev',
106
+ name: 'theme:dev',
107
107
  group: 'theme',
108
108
  description: 'starts a dev server and watches over the current directory',
109
109
  options: [
@@ -58,7 +58,7 @@ function getSelectedTheme(optionTheme) {
58
58
  }
59
59
  function command(cli) {
60
60
  return {
61
- name: 'init',
61
+ name: 'theme:init',
62
62
  group: 'theme',
63
63
  description: 'Create a new theme or clone existing one.',
64
64
  options: [
@@ -0,0 +1,33 @@
1
+ import messages from '../../../config/messages.js';
2
+ import stdout from '../../../utils/system/stdout.js';
3
+ import { LoadingSpinner } from '../../../utils/common.js';
4
+
5
+ function command(cli) {
6
+ return {
7
+ name: 'theme:list',
8
+ group: 'theme',
9
+ description: 'List my development themes',
10
+ options: [],
11
+ action: async () => {
12
+ if (!cli.client.isAuthenticated())
13
+ return stdout.error(messages.AUTH_USER_NOT_LOGGED_IN);
14
+ await LoadingSpinner.exec(`${messages.FETCHING_DEV_THEMES}..`, async (spinner) => {
15
+ try {
16
+ const devThemes = await cli.client.listThemes();
17
+ spinner.stop();
18
+ if (!devThemes.dev.length)
19
+ return stdout.error(messages.NO_REMOTE_THEMES);
20
+ stdout.table(devThemes.dev.map((theme) => ({
21
+ Name: theme.name,
22
+ Size: theme.size,
23
+ })));
24
+ }
25
+ catch (err) {
26
+ spinner.error(messages.ERROR_WHILE_FETCHING_DEV_THEMES);
27
+ }
28
+ });
29
+ },
30
+ };
31
+ }
32
+
33
+ export { command as default };
@@ -7,7 +7,7 @@ import config from '../../../config/index.js';
7
7
 
8
8
  function command(cli) {
9
9
  return {
10
- name: 'pack',
10
+ name: 'theme:pack',
11
11
  group: 'theme',
12
12
  description: 'Package a theme',
13
13
  options: [],
@@ -10,7 +10,7 @@ import messages from '../../../config/messages.js';
10
10
 
11
11
  function command(cli) {
12
12
  return {
13
- name: 'pull',
13
+ name: 'theme:pull',
14
14
  group: 'theme',
15
15
  description: 'Pull a theme',
16
16
  options: [],
@@ -1,13 +1,24 @@
1
1
  var messages = {
2
2
  LOGIN_OPEN_LINK: 'Open this link in your browser to continue authentication:',
3
3
  LOGIN_SUCCESS: 'You have been successfully logged in.',
4
+ FETCHING_CURRENT_STORE_INFO: 'Fetching current store info',
5
+ CURRENT_DEVELOPMENT_STORE: 'Current development store',
6
+ ERROR_WHILE_FETCHING_CURRENT_STORE_INFO: 'Error while fetching current store info',
7
+ SELECT_STORE: 'Which development store you would like to use?',
8
+ NO_STORE_FOUND: 'No development store found!',
9
+ NO_STORE_SELECTED: 'No development store selected.',
10
+ SELECT_STORE_IN_PROGRESS: 'Selecting store in progress',
11
+ CANNOT_SELECT_STORE: 'Could not select the development store.',
12
+ STORE_SELECTED: 'Store selected',
13
+ FETCHING_DEV_THEMES: 'Fetching current development themes',
14
+ ERROR_WHILE_FETCHING_DEV_THEMES: 'Error while fetching current development themes',
4
15
  INIT_SUCCESS: 'The theme has been initiated with id: ',
5
16
  INIT_CLONE_START: 'Cloning your theme from github',
6
17
  AUTH_USER_NOT_LOGGED_IN: 'You are not currently logged into any store.',
7
18
  AUTH_USER_LOGGED_OUT: 'You have been successfully logged out.',
8
19
  DELETE_NO_THEME_SELECTED: 'No theme selected.',
9
20
  DELETE_THEME_DELETED: 'Theme deleted successfully.',
10
- DELETE_NO_REMOTE_THEMES: 'This store does not have any development themes.',
21
+ NO_REMOTE_THEMES: 'This store does not have any development themes.',
11
22
  DELETE_SELECT_THEME: 'Which remote development theme would you like to delete?',
12
23
  DELETE_IN_PROGRESS: 'Deleting theme',
13
24
  DELETE_ERROR: 'Could not delete remote development theme.',
@@ -18,6 +18,17 @@ class Client {
18
18
  isAuthenticated() {
19
19
  return this.accessToken != null;
20
20
  }
21
+ async auth(data) {
22
+ const form = new FormData();
23
+ Object.entries(data).forEach(([key, value]) => form.append(key, value));
24
+ return await post(`${config.SELLER_AREA_API_BASE_URI}/auth/login`, this.withDefaults({ body: form }));
25
+ }
26
+ async listStores() {
27
+ return await get(`${config.SELLER_AREA_API_BASE_URI}/stores`, this.withDefaults({}));
28
+ }
29
+ async selectStore(data) {
30
+ return await post(`${config.SELLER_AREA_API_BASE_URI}/switch-store/${data.id}`, this.withDefaults({}));
31
+ }
21
32
  async initTheme(data) {
22
33
  const form = new FormData();
23
34
  Object.entries(data).forEach(([key, value]) => form.append(key, value));
@@ -1,7 +1,10 @@
1
1
  export { default as LoginCommand } from './auth/login';
2
2
  export { default as LogoutCommand } from './auth/logout';
3
- export { default as InitCommand } from './theme/init';
4
- export { default as DevCommand } from './theme/dev';
5
- export { default as DeleteCommand } from './theme/delete';
6
- export { default as PullCommand } from './theme/pull';
7
- export { default as PackCommand } from './theme/pack';
3
+ export { default as ThemeInitCommand } from './theme/init';
4
+ export { default as ThemeListCommand } from './theme/list';
5
+ export { default as ThemeDevCommand } from './theme/dev';
6
+ export { default as ThemeDeleteCommand } from './theme/delete';
7
+ export { default as ThemePullCommand } from './theme/pull';
8
+ export { default as ThemePackCommand } from './theme/pack';
9
+ export { default as StoreInfoCommand } from './store/info';
10
+ export { default as StoreSwitchCommand } from './store/switch';
@@ -0,0 +1,2 @@
1
+ import type { CLI, CommandDefinition } from '@/cli/commands/types';
2
+ export default function command(cli: CLI): CommandDefinition;
@@ -0,0 +1,2 @@
1
+ import type { CLI, CommandDefinition } from '@/cli/commands/types';
2
+ export default function command(cli: CLI): CommandDefinition;
@@ -0,0 +1,4 @@
1
+ import type { Store } from '@/core/client/types';
2
+ export interface listStoresResponse {
3
+ stores: Store[];
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { CLI, CommandDefinition } from '@/cli/commands/types';
2
+ export default function command(cli: CLI): CommandDefinition;
@@ -0,0 +1,2 @@
1
+ import type { CLI, CommandDefinition } from '@/cli/commands/types';
2
+ export default function command(cli: CLI): CommandDefinition;
@@ -5,7 +5,7 @@ declare const cli: {
5
5
  client: Client;
6
6
  handler: import("cac").CAC;
7
7
  registerCommand(command: (cli: typeof this) => CommandDefinition): void;
8
- getAvailableCommands(): (typeof commands.LoginCommand | typeof commands.LogoutCommand | typeof commands.InitCommand | typeof commands.DevCommand | typeof commands.DeleteCommand | typeof commands.PullCommand | typeof commands.PackCommand)[];
8
+ getAvailableCommands(): (typeof commands.LoginCommand | typeof commands.LogoutCommand | typeof commands.ThemeInitCommand | typeof commands.ThemeListCommand | typeof commands.ThemeDevCommand | typeof commands.ThemeDeleteCommand | typeof commands.ThemePullCommand | typeof commands.ThemePackCommand | typeof commands.StoreInfoCommand | typeof commands.StoreSwitchCommand)[];
9
9
  init(): Promise<void>;
10
10
  prepareClient(): Promise<void>;
11
11
  };
@@ -1,13 +1,24 @@
1
1
  declare const _default: {
2
2
  LOGIN_OPEN_LINK: string;
3
3
  LOGIN_SUCCESS: string;
4
+ FETCHING_CURRENT_STORE_INFO: string;
5
+ CURRENT_DEVELOPMENT_STORE: string;
6
+ ERROR_WHILE_FETCHING_CURRENT_STORE_INFO: string;
7
+ SELECT_STORE: string;
8
+ NO_STORE_FOUND: string;
9
+ NO_STORE_SELECTED: string;
10
+ SELECT_STORE_IN_PROGRESS: string;
11
+ CANNOT_SELECT_STORE: string;
12
+ STORE_SELECTED: string;
13
+ FETCHING_DEV_THEMES: string;
14
+ ERROR_WHILE_FETCHING_DEV_THEMES: string;
4
15
  INIT_SUCCESS: string;
5
16
  INIT_CLONE_START: string;
6
17
  AUTH_USER_NOT_LOGGED_IN: string;
7
18
  AUTH_USER_LOGGED_OUT: string;
8
19
  DELETE_NO_THEME_SELECTED: string;
9
20
  DELETE_THEME_DELETED: string;
10
- DELETE_NO_REMOTE_THEMES: string;
21
+ NO_REMOTE_THEMES: string;
11
22
  DELETE_SELECT_THEME: string;
12
23
  DELETE_IN_PROGRESS: string;
13
24
  DELETE_ERROR: string;
@@ -1,10 +1,14 @@
1
- import type { DeleteThemeFileRequestData, InitThemeRequest as InitThemeRequestData, StoreInfoResponse, ThemeMetaResponse, UpdateThemeFileRequestData } from './types';
1
+ import type { DeleteThemeFileRequestData, InitThemeRequest as InitThemeRequestData, LoginRequest, LoginResponse, SelectStoreRequest, SelectStoreResponse, StoreInfoResponse, ThemeMetaResponse, UpdateThemeFileRequestData } from './types';
2
+ import type { listStoresResponse } from '@/cli/commands/store/types';
2
3
  export default class Client {
3
4
  private accessToken;
4
5
  constructor();
5
6
  setAccessToken(token: string): void;
6
7
  getAccessToken(): string | null;
7
8
  isAuthenticated(): boolean;
9
+ auth(data: LoginRequest): Promise<LoginResponse>;
10
+ listStores(): Promise<listStoresResponse>;
11
+ selectStore(data: SelectStoreRequest): Promise<SelectStoreResponse>;
8
12
  initTheme(data: InitThemeRequestData): Promise<string>;
9
13
  getThemeMeta(themeId: string): Promise<ThemeMetaResponse>;
10
14
  pullTheme(themeId: string): Promise<import("node-fetch").Response>;
@@ -7,6 +7,25 @@ export interface InitThemeRequest {
7
7
  theme_support_url: string;
8
8
  theme_documentation_url: string;
9
9
  }
10
+ export interface LoginRequest {
11
+ email: string;
12
+ password: string;
13
+ }
14
+ export interface LoginResponse {
15
+ token: string;
16
+ stores: Store[];
17
+ }
18
+ export interface SelectStoreRequest {
19
+ id: string;
20
+ }
21
+ export interface SelectStoreResponse {
22
+ token: string;
23
+ }
24
+ export interface Store {
25
+ store_id: string;
26
+ slug: string;
27
+ is_active: boolean;
28
+ }
10
29
  export interface UpdateThemeFileRequestData {
11
30
  file_type: string;
12
31
  file_name: string;
@@ -7,6 +7,7 @@ declare function warn(arg: string): void;
7
7
  declare function error(arg: string): void;
8
8
  declare function success(arg: string): void;
9
9
  declare function clear(): void;
10
+ declare function table(arg: any): void;
10
11
  declare const _default: {
11
12
  log: typeof log;
12
13
  info: typeof info;
@@ -14,5 +15,6 @@ declare const _default: {
14
15
  error: typeof error;
15
16
  clear: typeof clear;
16
17
  success: typeof success;
18
+ table: typeof table;
17
19
  };
18
20
  export default _default;
@@ -10,6 +10,7 @@ function warn(arg) { return log(kleur.yellow(arg)); }
10
10
  function error(arg) { return log(kleur.bgRed().white(arg)); }
11
11
  function success(arg) { return log(kleur.green(arg)); }
12
12
  function clear() { return stdout.clear(); }
13
- var stdout$1 = { log, info, warn, error, clear, success };
13
+ function table(arg) { return stdout.table(arg); }
14
+ var stdout$1 = { log, info, warn, error, clear, success, table };
14
15
 
15
16
  export { stdout$1 as default };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@youcan/cli",
3
3
  "type": "module",
4
- "version": "1.0.5",
4
+ "version": "1.0.6",
5
5
  "description": "YouCan CLI for developers.",
6
6
  "author": "YouCan <contact@youcan.shop> (https://youcan.shop)",
7
7
  "keywords": [],