@hubspot/cli 3.0.9-beta.1 → 3.0.10-beta.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/commands/logs.js CHANGED
@@ -1,4 +1,4 @@
1
- const ora = require('ora');
1
+ const Spinnies = require('spinnies');
2
2
  const {
3
3
  addAccountOptions,
4
4
  addConfigOptions,
@@ -14,16 +14,7 @@ const {
14
14
  checkAndWarnGitInclusion,
15
15
  } = require('@hubspot/cli-lib');
16
16
  const { logger } = require('@hubspot/cli-lib/logger');
17
- const {
18
- logServerlessFunctionApiErrorInstance,
19
- ApiErrorContext,
20
- } = require('@hubspot/cli-lib/errorHandlers');
21
17
  const { outputLogs } = require('@hubspot/cli-lib/lib/logs');
22
- const {
23
- getFunctionByPath,
24
- getAppFunctionLogs,
25
- getLatestAppFunctionLogs,
26
- } = require('@hubspot/cli-lib/api/functions');
27
18
  const {
28
19
  getFunctionLogs,
29
20
  getLatestFunctionLog,
@@ -43,9 +34,11 @@ const loadAndValidateOptions = async options => {
43
34
  }
44
35
  };
45
36
 
46
- const handleLatestLogsError = e => {
37
+ const handleLogsError = (e, accountId, functionPath) => {
47
38
  if (e.statusCode === 404) {
48
- logger.log('No logs found.');
39
+ logger.error(
40
+ `No logs were found for the function path '${functionPath}' in account ${accountId}.`
41
+ );
49
42
  }
50
43
  };
51
44
 
@@ -58,95 +51,43 @@ const endpointLog = async (accountId, options) => {
58
51
  }logs for function with path: ${functionPath}`
59
52
  );
60
53
 
61
- const functionResp = await getFunctionByPath(accountId, functionPath).catch(
62
- async e => {
63
- await logServerlessFunctionApiErrorInstance(
64
- accountId,
65
- e,
66
- new ApiErrorContext({ accountId, functionPath })
67
- );
68
- process.exit();
69
- }
70
- );
71
- const functionId = functionResp.id;
72
-
73
- logger.debug(`Retrieving logs for functionId: ${functionResp.id}`);
74
-
75
54
  let logsResp;
76
55
 
77
56
  if (follow) {
78
- const spinner = ora(
79
- `Waiting for log entries for '${functionPath}' on account '${accountId}'.\n`
80
- );
81
- const tailCall = after => getFunctionLogs(accountId, functionId, { after });
82
- const fetchLatest = () => {
83
- try {
84
- getLatestFunctionLog(accountId, functionId);
85
- } catch (e) {
86
- handleLatestLogsError(e);
87
- }
88
- };
57
+ const spinnies = new Spinnies();
89
58
 
90
- await tailLogs({
91
- accountId,
92
- compact,
93
- spinner,
94
- tailCall,
95
- fetchLatest,
59
+ spinnies.add('tailLogs', {
60
+ text: `Waiting for log entries for '${functionPath}' on account '${accountId}'.\n`,
96
61
  });
97
- } else if (latest) {
98
- try {
99
- logsResp = await getLatestFunctionLog(accountId, functionResp.id);
100
- } catch (e) {
101
- handleLatestLogsError(e);
102
- }
103
- } else {
104
- logsResp = await getFunctionLogs(accountId, functionResp.id, options);
105
- }
106
-
107
- if (logsResp) {
108
- return outputLogs(logsResp, options);
109
- }
110
- };
111
-
112
- const appFunctionLog = async (accountId, options) => {
113
- const { latest, follow, compact, functionName, appPath } = options;
114
-
115
- let logsResp;
116
-
117
- if (follow) {
118
- const spinner = ora(
119
- `Waiting for log entries for "${functionName}" on account "${accountId}".\n`
120
- );
121
62
  const tailCall = after =>
122
- getAppFunctionLogs(accountId, functionName, appPath, { after });
63
+ getFunctionLogs(accountId, functionPath, { after });
123
64
  const fetchLatest = () => {
124
65
  try {
125
- getLatestAppFunctionLogs(accountId, functionName, appPath);
66
+ getLatestFunctionLog(accountId, functionPath);
126
67
  } catch (e) {
127
- handleLatestLogsError(e);
68
+ handleLogsError(e, accountId, functionPath);
128
69
  }
129
70
  };
130
71
 
131
72
  await tailLogs({
132
73
  accountId,
133
74
  compact,
134
- spinner,
75
+ spinnies,
135
76
  tailCall,
136
77
  fetchLatest,
137
78
  });
138
79
  } else if (latest) {
139
80
  try {
140
- logsResp = await getLatestAppFunctionLogs(
141
- accountId,
142
- functionName,
143
- appPath
144
- );
81
+ logsResp = await getLatestFunctionLog(accountId, functionPath);
145
82
  } catch (e) {
146
- handleLatestLogsError(e);
83
+ handleLogsError(e, accountId, functionPath);
147
84
  }
148
85
  } else {
149
- logsResp = await getAppFunctionLogs(accountId, functionName, appPath, {});
86
+ try {
87
+ logsResp = await getFunctionLogs(accountId, functionPath, options);
88
+ } catch (e) {
89
+ handleLogsError(e, accountId, functionPath);
90
+ }
150
91
  }
151
92
 
152
93
  if (logsResp) {
@@ -160,17 +101,13 @@ exports.describe = 'get logs for a function';
160
101
  exports.handler = async options => {
161
102
  loadAndValidateOptions(options);
162
103
 
163
- const { latest, functionName } = options;
104
+ const { latest } = options;
164
105
 
165
106
  const accountId = getAccountId(options);
166
107
 
167
108
  trackCommandUsage('logs', { latest }, accountId);
168
109
 
169
- if (functionName) {
170
- appFunctionLog(accountId, options);
171
- } else {
172
- endpointLog(accountId, options);
173
- }
110
+ endpointLog(accountId, options);
174
111
  };
175
112
 
176
113
  exports.builder = yargs => {
@@ -180,16 +117,6 @@ exports.builder = yargs => {
180
117
  });
181
118
  yargs
182
119
  .options({
183
- appPath: {
184
- describe: 'path to the app',
185
- type: 'string',
186
- hidden: true,
187
- },
188
- functionName: {
189
- describe: 'app function name',
190
- type: 'string',
191
- hidden: true,
192
- },
193
120
  latest: {
194
121
  alias: 'l',
195
122
  describe: 'retrieve most recent log only',
@@ -75,7 +75,7 @@ exports.handler = async options => {
75
75
  .flat()
76
76
  .some(result => result.result === VALIDATION_RESULT.FATAL)
77
77
  ) {
78
- process.exit(1);
78
+ process.exit(2);
79
79
  }
80
80
  });
81
81
  };
@@ -1,3 +1,4 @@
1
+ const path = require('path');
1
2
  const {
2
3
  addAccountOptions,
3
4
  addConfigOptions,
@@ -17,8 +18,13 @@ const {
17
18
  ApiErrorContext,
18
19
  } = require('@hubspot/cli-lib/errorHandlers');
19
20
  const { logger } = require('@hubspot/cli-lib/logger');
20
- const { deployProject } = require('@hubspot/cli-lib/api/fileMapper');
21
+ const { deployProject, fetchProject } = require('@hubspot/cli-lib/api/dfs');
22
+ const { getCwd } = require('@hubspot/cli-lib/path');
21
23
  const { validateAccount } = require('../../lib/validation');
24
+ const {
25
+ getOrCreateProjectConfig,
26
+ pollDeployStatus,
27
+ } = require('../../lib/projects');
22
28
 
23
29
  const loadAndValidateOptions = async options => {
24
30
  setLogLevel(options);
@@ -32,39 +38,56 @@ const loadAndValidateOptions = async options => {
32
38
  }
33
39
  };
34
40
 
35
- exports.command = 'deploy <path>';
41
+ exports.command = 'deploy [path]';
36
42
  exports.describe = false;
37
43
 
38
44
  exports.handler = async options => {
39
45
  loadAndValidateOptions(options);
40
46
 
41
- const { path: projectPath } = options;
47
+ const { path: projectPath, buildId } = options;
42
48
  const accountId = getAccountId(options);
43
49
 
44
50
  trackCommandUsage('project-deploy', { projectPath }, accountId);
45
51
 
52
+ const cwd = projectPath ? path.resolve(getCwd(), projectPath) : getCwd();
53
+ const projectConfig = await getOrCreateProjectConfig(cwd);
54
+
46
55
  logger.debug(`Deploying project at path: ${projectPath}`);
47
56
 
57
+ const getBuildId = async () => {
58
+ const { latestBuild } = await fetchProject(accountId, projectConfig.name);
59
+ if (latestBuild && latestBuild.buildId) {
60
+ return latestBuild.buildId;
61
+ }
62
+ logger.error('No latest build ID was found.');
63
+ return;
64
+ };
65
+
48
66
  try {
49
- const deployResp = await deployProject(accountId, projectPath);
67
+ const deployedBuildId = buildId || (await getBuildId());
68
+
69
+ const deployResp = await deployProject(
70
+ accountId,
71
+ projectConfig.name,
72
+ deployedBuildId
73
+ );
50
74
 
51
75
  if (deployResp.error) {
52
76
  logger.error(`Deploy error: ${deployResp.error.message}`);
53
77
  return;
54
78
  }
55
79
 
56
- logger.success(
57
- `Deployed project in ${projectPath} on account ${accountId}.`
80
+ await pollDeployStatus(
81
+ accountId,
82
+ projectConfig.name,
83
+ deployResp.id,
84
+ deployedBuildId
58
85
  );
59
86
  } catch (e) {
60
87
  if (e.statusCode === 400) {
61
88
  logger.error(e.error.message);
62
89
  } else {
63
- logApiErrorInstance(
64
- accountId,
65
- e,
66
- new ApiErrorContext({ accountId, projectPath })
67
- );
90
+ logApiErrorInstance(e, new ApiErrorContext({ accountId, projectPath }));
68
91
  }
69
92
  }
70
93
  };
@@ -75,6 +98,13 @@ exports.builder = yargs => {
75
98
  type: 'string',
76
99
  });
77
100
 
101
+ yargs.options({
102
+ buildId: {
103
+ describe: 'Project build ID to be deployed',
104
+ type: 'number',
105
+ },
106
+ });
107
+
78
108
  yargs.example([
79
109
  [
80
110
  '$0 project deploy myProjectFolder',
@@ -0,0 +1,196 @@
1
+ const Spinnies = require('spinnies');
2
+ const {
3
+ addAccountOptions,
4
+ addConfigOptions,
5
+ setLogLevel,
6
+ getAccountId,
7
+ addUseEnvironmentOptions,
8
+ } = require('../../lib/commonOpts');
9
+ const { trackCommandUsage } = require('../../lib/usageTracking');
10
+ const { logDebugInfo } = require('../../lib/debugInfo');
11
+ const {
12
+ loadConfig,
13
+ validateConfig,
14
+ checkAndWarnGitInclusion,
15
+ } = require('@hubspot/cli-lib');
16
+ const { logger } = require('@hubspot/cli-lib/logger');
17
+ const { outputLogs } = require('@hubspot/cli-lib/lib/logs');
18
+ const {
19
+ getProjectAppFunctionLogs,
20
+ getLatestProjectAppFunctionLog,
21
+ } = require('@hubspot/cli-lib/api/functions');
22
+ const { validateAccount } = require('../../lib/validation');
23
+ const { tailLogs } = require('../../lib/serverlessLogs');
24
+
25
+ const loadAndValidateOptions = async options => {
26
+ setLogLevel(options);
27
+ logDebugInfo(options);
28
+ const { config: configPath } = options;
29
+ loadConfig(configPath, options);
30
+ checkAndWarnGitInclusion();
31
+
32
+ if (!(validateConfig() && (await validateAccount(options)))) {
33
+ process.exit(1);
34
+ }
35
+ };
36
+
37
+ const handleLogsError = (e, accountId, projectName, appPath, functionName) => {
38
+ if (e.statusCode === 404) {
39
+ logger.error(
40
+ `No logs were found for the function name '${functionName}' in the app path '${appPath}' within the project '${projectName}' in account ${accountId}.`
41
+ );
42
+ }
43
+ };
44
+
45
+ const appFunctionLog = async (accountId, options) => {
46
+ const {
47
+ latest,
48
+ follow,
49
+ compact,
50
+ appPath,
51
+ functionName,
52
+ projectName,
53
+ } = options;
54
+
55
+ let logsResp;
56
+
57
+ if (follow) {
58
+ const spinnies = new Spinnies();
59
+
60
+ spinnies.add('tailLogs', {
61
+ text: `Waiting for log entries for '${functionName}' on account '${accountId}'.\n`,
62
+ });
63
+ const tailCall = after =>
64
+ getProjectAppFunctionLogs(accountId, functionName, projectName, appPath, {
65
+ after,
66
+ });
67
+ const fetchLatest = () => {
68
+ try {
69
+ return getLatestProjectAppFunctionLog(
70
+ accountId,
71
+ functionName,
72
+ projectName,
73
+ appPath
74
+ );
75
+ } catch (e) {
76
+ handleLogsError(e, accountId, projectName, appPath, functionName);
77
+ }
78
+ };
79
+
80
+ await tailLogs({
81
+ accountId,
82
+ compact,
83
+ spinnies,
84
+ tailCall,
85
+ fetchLatest,
86
+ });
87
+ } else if (latest) {
88
+ try {
89
+ logsResp = await getLatestProjectAppFunctionLog(
90
+ accountId,
91
+ functionName,
92
+ projectName,
93
+ appPath
94
+ );
95
+ } catch (e) {
96
+ handleLogsError(e, accountId, projectName, appPath, functionName);
97
+ }
98
+ } else {
99
+ try {
100
+ logsResp = await getProjectAppFunctionLogs(
101
+ accountId,
102
+ functionName,
103
+ projectName,
104
+ appPath,
105
+ {}
106
+ );
107
+ } catch (e) {
108
+ handleLogsError(e, accountId, projectName, appPath, functionName);
109
+ }
110
+ }
111
+
112
+ if (logsResp) {
113
+ return outputLogs(logsResp, options);
114
+ }
115
+ };
116
+
117
+ exports.command = 'logs [functionName]';
118
+ exports.describe = 'get logs for a function within a project';
119
+
120
+ exports.handler = async options => {
121
+ loadAndValidateOptions(options);
122
+
123
+ const { latest, functionName, projectName, appPath } = options;
124
+
125
+ if (!functionName) {
126
+ logger.error('You must pass a function name to retrieve logs for.');
127
+ process.exit(0);
128
+ } else if (!projectName) {
129
+ logger.error(
130
+ 'You must specify a project name using the --projectName argument.'
131
+ );
132
+ process.exit(0);
133
+ } else if (!appPath) {
134
+ logger.error('You must specify an app path using the --appPath argument.');
135
+ process.exit(0);
136
+ }
137
+
138
+ const accountId = getAccountId(options);
139
+
140
+ trackCommandUsage('project-logs', { latest }, accountId);
141
+
142
+ appFunctionLog(accountId, options);
143
+ };
144
+
145
+ exports.builder = yargs => {
146
+ yargs.positional('functionName', {
147
+ describe: 'Serverless function name',
148
+ type: 'string',
149
+ });
150
+ yargs
151
+ .options({
152
+ appPath: {
153
+ describe: 'path to the app',
154
+ type: 'string',
155
+ hidden: true,
156
+ },
157
+ projectName: {
158
+ describe: 'name of the project',
159
+ type: 'string',
160
+ hidden: true,
161
+ },
162
+ latest: {
163
+ alias: 'l',
164
+ describe: 'retrieve most recent log only',
165
+ type: 'boolean',
166
+ },
167
+ compact: {
168
+ describe: 'output compact logs',
169
+ type: 'boolean',
170
+ },
171
+ follow: {
172
+ alias: ['t', 'tail', 'f'],
173
+ describe: 'follow logs',
174
+ type: 'boolean',
175
+ },
176
+ limit: {
177
+ alias: ['limit', 'n', 'max-count'],
178
+ describe: 'limit the number of logs to output',
179
+ type: 'number',
180
+ },
181
+ })
182
+ .conflicts('follow', 'limit');
183
+
184
+ yargs.example([
185
+ [
186
+ '$0 project logs my-function --appName="app" --projectName="my-project"',
187
+ 'Get 5 most recent logs for function named "my-function" within the app named "app" within the project named "my-project"',
188
+ ],
189
+ ]);
190
+
191
+ addConfigOptions(yargs, true);
192
+ addAccountOptions(yargs, true);
193
+ addUseEnvironmentOptions(yargs, true);
194
+
195
+ return yargs;
196
+ };
@@ -31,6 +31,8 @@ const {
31
31
  getProjectConfig,
32
32
  validateProjectConfig,
33
33
  pollBuildStatus,
34
+ ensureProjectExists,
35
+ pollDeployStatus,
34
36
  } = require('../../lib/projects');
35
37
 
36
38
  const loadAndValidateOptions = async options => {
@@ -49,7 +51,9 @@ exports.command = 'upload [path]';
49
51
  exports.describe = false;
50
52
 
51
53
  const uploadProjectFiles = async (accountId, projectName, filePath) => {
52
- const spinnies = new Spinnies();
54
+ const spinnies = new Spinnies({
55
+ succeedColor: 'white',
56
+ });
53
57
 
54
58
  spinnies.add('upload', {
55
59
  text: `Uploading ${chalk.bold(projectName)} project files to ${chalk.bold(
@@ -57,9 +61,13 @@ const uploadProjectFiles = async (accountId, projectName, filePath) => {
57
61
  )}`,
58
62
  });
59
63
 
64
+ let buildId;
65
+
60
66
  try {
61
67
  const upload = await uploadProject(accountId, projectName, filePath);
62
68
 
69
+ buildId = upload.buildId;
70
+
63
71
  spinnies.succeed('upload', {
64
72
  text: `Uploaded ${chalk.bold(projectName)} project files to ${chalk.bold(
65
73
  accountId
@@ -67,9 +75,8 @@ const uploadProjectFiles = async (accountId, projectName, filePath) => {
67
75
  });
68
76
 
69
77
  logger.debug(
70
- `Project "${projectName}" uploaded and build #${upload.buildId} created`
78
+ `Project "${projectName}" uploaded and build #${buildId} created`
71
79
  );
72
- await pollBuildStatus(accountId, projectName, upload.buildId);
73
80
  } catch (err) {
74
81
  if (err.statusCode === 404) {
75
82
  return logger.error(
@@ -83,13 +90,16 @@ const uploadProjectFiles = async (accountId, projectName, filePath) => {
83
90
  )} project files to ${chalk.bold(accountId)}`,
84
91
  });
85
92
 
86
- logApiErrorInstance(err, {
87
- context: new ApiErrorContext({
93
+ logApiErrorInstance(
94
+ err,
95
+ new ApiErrorContext({
88
96
  accountId,
89
97
  projectName,
90
- }),
91
- });
98
+ })
99
+ );
92
100
  }
101
+
102
+ return { buildId };
93
103
  };
94
104
 
95
105
  exports.handler = async options => {
@@ -100,10 +110,14 @@ exports.handler = async options => {
100
110
 
101
111
  trackCommandUsage('project-upload', { projectPath }, accountId);
102
112
 
103
- const cwd = projectPath ? path.resolve(getCwd(), projectPath) : getCwd();
104
- const projectConfig = await getProjectConfig(cwd);
113
+ const projectDir = projectPath
114
+ ? path.resolve(getCwd(), projectPath)
115
+ : getCwd();
116
+ const projectConfig = await getProjectConfig(projectDir);
105
117
 
106
- validateProjectConfig(projectConfig);
118
+ validateProjectConfig(projectConfig, projectDir);
119
+
120
+ await ensureProjectExists(accountId, projectConfig.name);
107
121
 
108
122
  const tempFile = tmp.fileSync({ postfix: '.zip' });
109
123
 
@@ -115,7 +129,64 @@ exports.handler = async options => {
115
129
  output.on('close', async function() {
116
130
  logger.debug(`Project files compressed: ${archive.pointer()} bytes`);
117
131
 
118
- await uploadProjectFiles(accountId, projectConfig.name, tempFile.name);
132
+ const { buildId } = await uploadProjectFiles(
133
+ accountId,
134
+ projectConfig.name,
135
+ tempFile.name
136
+ );
137
+
138
+ const {
139
+ isAutoDeployEnabled,
140
+ deployStatusTaskLocator,
141
+ status,
142
+ subbuildStatuses,
143
+ } = await pollBuildStatus(accountId, projectConfig.name, buildId);
144
+
145
+ if (status === 'FAILURE') {
146
+ const failedSubbuilds = subbuildStatuses.filter(
147
+ subbuild => subbuild.status === 'FAILURE'
148
+ );
149
+
150
+ logger.log('-'.repeat(50));
151
+ logger.log(
152
+ `Build #${buildId} failed because there was a problem\nbuilding ${
153
+ failedSubbuilds.length === 1
154
+ ? failedSubbuilds[0].buildName
155
+ : failedSubbuilds.length + ' components'
156
+ }\n`
157
+ );
158
+ logger.log('See below for a summary of errors.');
159
+ logger.log('-'.repeat(50));
160
+
161
+ failedSubbuilds.forEach(subbuild => {
162
+ logger.log(
163
+ `\n--- ${subbuild.buildName} failed to build with the following error ---`
164
+ );
165
+ logger.error(subbuild.errorMessage);
166
+ });
167
+
168
+ return;
169
+ }
170
+
171
+ if (isAutoDeployEnabled && deployStatusTaskLocator) {
172
+ logger.log(
173
+ `Build #${buildId} succeeded. ${chalk.bold(
174
+ 'Automatically deploying'
175
+ )} to ${accountId}`
176
+ );
177
+ await pollDeployStatus(
178
+ accountId,
179
+ projectConfig.name,
180
+ deployStatusTaskLocator.id,
181
+ buildId
182
+ );
183
+ } else {
184
+ logger.log('-'.repeat(50));
185
+ logger.log(chalk.bold(`Build #${buildId} succeeded\n`));
186
+ logger.log('🚀 Ready to take your project live?');
187
+ logger.log(`Run \`${chalk.hex('f5c26b')('hs project deploy')}\``);
188
+ logger.log('-'.repeat(50));
189
+ }
119
190
 
120
191
  try {
121
192
  tempFile.removeCallback();
@@ -131,8 +202,10 @@ exports.handler = async options => {
131
202
 
132
203
  archive.pipe(output);
133
204
 
134
- archive.directory(path.resolve(cwd, projectConfig.srcDir), false, file =>
135
- shouldIgnoreFile(file.name) ? false : file
205
+ archive.directory(
206
+ path.resolve(projectDir, projectConfig.srcDir),
207
+ false,
208
+ file => (shouldIgnoreFile(file.name) ? false : file)
136
209
  );
137
210
 
138
211
  archive.finalize();
@@ -6,6 +6,7 @@ const {
6
6
  const deploy = require('./project/deploy');
7
7
  const init = require('./project/init');
8
8
  const upload = require('./project/upload');
9
+ const logs = require('./project/logs');
9
10
 
10
11
  exports.command = 'project';
11
12
  exports.describe = false; //'Commands for working with projects';
@@ -19,6 +20,7 @@ exports.builder = yargs => {
19
20
  yargs.command(deploy).demandCommand(1, '');
20
21
  yargs.command(init).demandCommand(0, '');
21
22
  yargs.command(upload).demandCommand(0, '');
23
+ yargs.command(logs).demandCommand(1, '');
22
24
 
23
25
  return yargs;
24
26
  };
@@ -11,15 +11,15 @@ const ACCOUNT_ID = 123;
11
11
  describe('@hubspot/cli/lib/serverlessLogs', () => {
12
12
  describe('tailLogs()', () => {
13
13
  let stdinMock;
14
- let spinner;
14
+ let spinnies;
15
15
 
16
16
  beforeEach(() => {
17
17
  jest.spyOn(process, 'exit').mockImplementation(() => {});
18
18
  stdinMock = mockStdIn.stdin();
19
- spinner = {
20
- start: jest.fn(),
21
- stop: jest.fn(),
22
- clear: jest.fn(),
19
+ spinnies = {
20
+ succeed: jest.fn(),
21
+ fail: jest.fn(),
22
+ stopAll: jest.fn(),
23
23
  };
24
24
  });
25
25
 
@@ -56,7 +56,7 @@ describe('@hubspot/cli/lib/serverlessLogs', () => {
56
56
  await tailLogs({
57
57
  accountId: ACCOUNT_ID,
58
58
  compact,
59
- spinner,
59
+ spinnies,
60
60
  fetchLatest,
61
61
  tailCall,
62
62
  });
@@ -114,7 +114,7 @@ describe('@hubspot/cli/lib/serverlessLogs', () => {
114
114
  await tailLogs({
115
115
  accountId: ACCOUNT_ID,
116
116
  compact,
117
- spinner,
117
+ spinnies,
118
118
  fetchLatest,
119
119
  tailCall,
120
120
  });
@@ -123,7 +123,6 @@ describe('@hubspot/cli/lib/serverlessLogs', () => {
123
123
  latestLogResponse,
124
124
  expect.objectContaining({ compact })
125
125
  );
126
- expect(spinner.clear).toHaveBeenCalled();
127
126
  expect(tailCall).toHaveBeenCalledTimes(2);
128
127
  });
129
128
  it('handles no logs', async () => {
@@ -147,7 +146,7 @@ describe('@hubspot/cli/lib/serverlessLogs', () => {
147
146
  await tailLogs({
148
147
  accountId: ACCOUNT_ID,
149
148
  compact,
150
- spinner,
149
+ spinnies,
151
150
  fetchLatest,
152
151
  tailCall,
153
152
  });
package/lib/links.js CHANGED
@@ -1,3 +1,4 @@
1
+ const supportsHyperlinks = require('supports-hyperlinks');
1
2
  const { getEnv } = require('@hubspot/cli-lib/lib/config');
2
3
  const { ENVIRONMENTS } = require('@hubspot/cli-lib/lib/constants');
3
4
  const { getHubSpotWebsiteOrigin } = require('@hubspot/cli-lib/lib/urls');
@@ -120,9 +121,18 @@ const openLink = (accountId, shortcut) => {
120
121
  logger.success(`We opened ${match.url} in your browser`);
121
122
  };
122
123
 
124
+ const link = (linkText, url) => {
125
+ if (supportsHyperlinks.stdout) {
126
+ return ['\u001B]8;;', url, '\u0007', linkText, '\u001B]8;;\u0007'].join('');
127
+ } else {
128
+ return `${linkText}: ${url}`;
129
+ }
130
+ };
131
+
123
132
  module.exports = {
124
133
  getSiteLinks,
125
134
  getSiteLinksAsArray,
126
135
  logSiteLinks,
127
136
  openLink,
137
+ link,
128
138
  };
package/lib/projects.js CHANGED
@@ -11,15 +11,43 @@ const { getHubSpotWebsiteOrigin } = require('@hubspot/cli-lib/lib/urls');
11
11
  const {
12
12
  ENVIRONMENTS,
13
13
  POLLING_DELAY,
14
- PROJECT_BUILD_STATUS,
15
- PROJECT_BUILD_STATUS_TEXT,
14
+ PROJECT_OVERALL_STATUS,
15
+ PROJECT_TEXT,
16
16
  } = require('@hubspot/cli-lib/lib/constants');
17
- const { getBuildStatus } = require('@hubspot/cli-lib/api/dfs');
17
+ const {
18
+ getBuildStatus,
19
+ getDeployStatus,
20
+ fetchProject,
21
+ createProject,
22
+ } = require('@hubspot/cli-lib/api/dfs');
23
+ const {
24
+ logApiErrorInstance,
25
+ ApiErrorContext,
26
+ } = require('@hubspot/cli-lib/errorHandlers');
18
27
 
19
- const isBuildComplete = build => {
28
+ const PROJECT_STRINGS = {
29
+ BUILD: {
30
+ INITIALIZE: (name, numOfComponents) =>
31
+ `Building ${chalk.bold(
32
+ name
33
+ )}\n\nFound ${numOfComponents} components in this project ...\n`,
34
+ SUCCESS: name => `Built ${chalk.bold(name)}`,
35
+ FAIL: name => `Failed to build ${chalk.bold(name)}`,
36
+ },
37
+ DEPLOY: {
38
+ INITIALIZE: (name, numOfComponents) =>
39
+ `Deploying ${chalk.bold(
40
+ name
41
+ )}\n\nFound ${numOfComponents} components in this project ...\n`,
42
+ SUCCESS: name => `Deployed ${chalk.bold(name)}`,
43
+ FAIL: name => `Failed to deploy ${chalk.bold(name)}`,
44
+ },
45
+ };
46
+
47
+ const isTaskComplete = task => {
20
48
  return (
21
- build.status === PROJECT_BUILD_STATUS.SUCCESS ||
22
- build.status === PROJECT_BUILD_STATUS.FAILURE
49
+ task.status === PROJECT_OVERALL_STATUS.SUCCESS ||
50
+ task.status === PROJECT_OVERALL_STATUS.FAILURE
23
51
  );
24
52
  };
25
53
 
@@ -86,7 +114,7 @@ const getOrCreateProjectConfig = async projectPath => {
86
114
  return projectConfig;
87
115
  };
88
116
 
89
- const validateProjectConfig = projectConfig => {
117
+ const validateProjectConfig = (projectConfig, projectDir) => {
90
118
  if (!projectConfig) {
91
119
  logger.error(
92
120
  `Project config not found. Try running 'hs project init' first.`
@@ -101,7 +129,7 @@ const validateProjectConfig = projectConfig => {
101
129
  process.exit(1);
102
130
  }
103
131
 
104
- if (!fs.existsSync(projectConfig.srcDir)) {
132
+ if (!fs.existsSync(path.resolve(projectDir, projectConfig.srcDir))) {
105
133
  logger.error(
106
134
  `Project source directory '${projectConfig.srcDir}' does not exist.`
107
135
  );
@@ -109,6 +137,37 @@ const validateProjectConfig = projectConfig => {
109
137
  }
110
138
  };
111
139
 
140
+ const ensureProjectExists = async (accountId, projectName) => {
141
+ try {
142
+ await fetchProject(accountId, projectName);
143
+ } catch (err) {
144
+ if (err.statusCode === 404) {
145
+ const { shouldCreateProject } = await prompt([
146
+ {
147
+ name: 'shouldCreateProject',
148
+ message: `The project ${projectName} does not exist in ${accountId}. Would you like to create it?`,
149
+ type: 'confirm',
150
+ },
151
+ ]);
152
+
153
+ if (shouldCreateProject) {
154
+ try {
155
+ return createProject(accountId, projectName);
156
+ } catch (err) {
157
+ return logApiErrorInstance(err, new ApiErrorContext({ accountId }));
158
+ }
159
+ } else {
160
+ return logger.log(
161
+ `Your project ${chalk.bold(
162
+ projectName
163
+ )} could not be found in ${chalk.bold(accountId)}.`
164
+ );
165
+ }
166
+ }
167
+ logApiErrorInstance(err, new ApiErrorContext({ accountId }));
168
+ }
169
+ };
170
+
112
171
  const getProjectDetailUrl = (projectName, accountId) => {
113
172
  if (!projectName) return;
114
173
 
@@ -146,86 +205,107 @@ const showWelcomeMessage = (projectName, accountId) => {
146
205
  );
147
206
  };
148
207
 
149
- const pollBuildStatus = async (accountId, name, buildId) => {
150
- const buildStatus = await getBuildStatus(accountId, name, buildId);
151
- const spinnies = new Spinnies();
208
+ const makeGetTaskStatus = taskType => {
209
+ let statusFn, statusText, statusStrings;
210
+ switch (taskType) {
211
+ case 'build':
212
+ statusFn = getBuildStatus;
213
+ statusText = PROJECT_TEXT.BUILD;
214
+ statusStrings = PROJECT_STRINGS.BUILD;
215
+ break;
216
+ case 'deploy':
217
+ statusFn = getDeployStatus;
218
+ statusText = PROJECT_TEXT.DEPLOY;
219
+ statusStrings = PROJECT_STRINGS.DEPLOY;
220
+ break;
221
+ default:
222
+ logger.error(`Cannot get status for task type ${taskType}`);
223
+ }
152
224
 
153
- logger.log();
154
- logger.log(`Building ${chalk.bold(name)}`);
155
- logger.log();
156
- logger.log(`Found ${buildStatus.subbuildStatuses.length} deployables ...`);
157
- logger.log();
225
+ return async (accountId, taskName, taskId) => {
226
+ const spinnies = new Spinnies({
227
+ succeedColor: 'white',
228
+ failColor: 'white',
229
+ });
230
+
231
+ spinnies.add('overallTaskStatus', { text: 'Beginning' });
158
232
 
159
- for (let subBuild of buildStatus.subbuildStatuses) {
160
- spinnies.add(subBuild.buildName, {
161
- text: `${chalk.bold(subBuild.buildName)} #${buildId} ${
162
- PROJECT_BUILD_STATUS_TEXT[PROJECT_BUILD_STATUS.ENQUEUED]
163
- }`,
233
+ const initialTaskStatus = await statusFn(accountId, taskName, taskId);
234
+
235
+ spinnies.update('overallTaskStatus', {
236
+ text: statusStrings.INITIALIZE(
237
+ taskName,
238
+ initialTaskStatus[statusText.SUBTASK_KEY].length
239
+ ),
164
240
  });
165
- }
166
241
 
167
- return new Promise((resolve, reject) => {
168
- const pollInterval = setInterval(async () => {
169
- const buildStatus = await getBuildStatus(accountId, name, buildId).catch(
170
- reject
171
- );
172
- const { status, subbuildStatuses } = buildStatus;
173
-
174
- if (Object.keys(spinnies.spinners).length) {
175
- subbuildStatuses.forEach(subBuild => {
176
- const updatedText = `${chalk.bold(subBuild.buildName)} #${buildId} ${
177
- PROJECT_BUILD_STATUS_TEXT[subBuild.status]
178
- }`;
179
-
180
- switch (subBuild.status) {
181
- case PROJECT_BUILD_STATUS.SUCCESS:
182
- spinnies.succeed(subBuild.buildName, {
183
- text: updatedText,
184
- });
185
- break;
186
- case PROJECT_BUILD_STATUS.FAILURE:
187
- spinnies.fail(subBuild.buildName, {
188
- text: updatedText,
189
- });
190
- break;
191
- default:
192
- spinnies.update(subBuild.buildName, {
193
- text: updatedText,
194
- });
195
- break;
196
- }
197
- });
198
- }
242
+ for (let subTask of initialTaskStatus[statusText.SUBTASK_KEY]) {
243
+ spinnies.add(subTask[statusText.SUBTASK_NAME_KEY], {
244
+ text: `${chalk.bold(subTask[statusText.SUBTASK_NAME_KEY])} #${taskId} ${
245
+ statusText.STATUS_TEXT[statusText.STATES.ENQUEUED]
246
+ }\n`,
247
+ });
248
+ }
249
+
250
+ return new Promise((resolve, reject) => {
251
+ const pollInterval = setInterval(async () => {
252
+ const taskStatus = await statusFn(accountId, taskName, taskId).catch(
253
+ reject
254
+ );
255
+
256
+ const { status, [statusText.SUBTASK_KEY]: subTaskStatus } = taskStatus;
257
+
258
+ if (spinnies.hasActiveSpinners()) {
259
+ subTaskStatus.forEach(subTask => {
260
+ if (!spinnies.pick(subTask[statusText.SUBTASK_NAME_KEY])) {
261
+ return;
262
+ }
263
+
264
+ const updatedText = `${chalk.bold(
265
+ subTask[statusText.SUBTASK_NAME_KEY]
266
+ )} #${taskId} ${statusText.STATUS_TEXT[subTask.status]}\n`;
199
267
 
200
- if (isBuildComplete(buildStatus)) {
201
- clearInterval(pollInterval);
202
-
203
- if (status === PROJECT_BUILD_STATUS.SUCCESS) {
204
- logger.success(
205
- `Your project ${chalk.bold(name)} ${
206
- PROJECT_BUILD_STATUS_TEXT[status]
207
- }.`
208
- );
209
- } else if (status === PROJECT_BUILD_STATUS.FAILURE) {
210
- logger.error(
211
- `Your project ${chalk.bold(name)} ${
212
- PROJECT_BUILD_STATUS_TEXT[status]
213
- }.`
214
- );
215
- subbuildStatuses.forEach(subBuild => {
216
- if (subBuild.status === PROJECT_BUILD_STATUS.FAILURE) {
217
- logger.error(
218
- `${chalk.bold(subBuild.buildName)} failed to build. ${
219
- subBuild.errorMessage
220
- }.`
221
- );
268
+ switch (subTask.status) {
269
+ case statusText.STATES.SUCCESS:
270
+ spinnies.succeed(subTask[statusText.SUBTASK_NAME_KEY], {
271
+ text: updatedText,
272
+ });
273
+ break;
274
+ case statusText.STATES.FAILURE:
275
+ spinnies.fail(subTask.buildName, {
276
+ text: updatedText,
277
+ });
278
+ break;
279
+ default:
280
+ spinnies.update(subTask.buildName, {
281
+ text: updatedText,
282
+ });
283
+ break;
222
284
  }
223
285
  });
286
+
287
+ if (isTaskComplete(taskStatus)) {
288
+ subTaskStatus.forEach(subBuild => {
289
+ spinnies.remove(subBuild[statusText.SUBTASK_NAME_KEY]);
290
+ });
291
+
292
+ if (status === statusText.STATES.SUCCESS) {
293
+ spinnies.succeed('overallTaskStatus', {
294
+ text: statusStrings.SUCCESS(taskName),
295
+ });
296
+ } else if (status === statusText.STATES.FAILURE) {
297
+ spinnies.fail('overallTaskStatus', {
298
+ text: statusStrings.FAIL(taskName),
299
+ });
300
+ }
301
+
302
+ clearInterval(pollInterval);
303
+ resolve(taskStatus);
304
+ }
224
305
  }
225
- resolve(buildStatus);
226
- }
227
- }, POLLING_DELAY);
228
- });
306
+ }, POLLING_DELAY);
307
+ });
308
+ };
229
309
  };
230
310
 
231
311
  module.exports = {
@@ -234,5 +314,7 @@ module.exports = {
234
314
  getOrCreateProjectConfig,
235
315
  validateProjectConfig,
236
316
  showWelcomeMessage,
237
- pollBuildStatus,
317
+ pollBuildStatus: makeGetTaskStatus('build'),
318
+ pollDeployStatus: makeGetTaskStatus('deploy'),
319
+ ensureProjectExists,
238
320
  };
@@ -17,7 +17,7 @@ const handleKeypressToExit = exit => {
17
17
  process.stdin.setRawMode(true);
18
18
  process.stdin.on('keypress', (str, key) => {
19
19
  if (key && ((key.ctrl && key.name == 'c') || key.name === 'escape')) {
20
- exit();
20
+ exit(key.name === 'escape' ? 'esc' : 'ctrl+c');
21
21
  }
22
22
  });
23
23
  };
@@ -25,14 +25,12 @@ const handleKeypressToExit = exit => {
25
25
  const tailLogs = async ({
26
26
  accountId,
27
27
  compact,
28
- spinner,
28
+ spinnies,
29
29
  fetchLatest,
30
30
  tailCall,
31
31
  }) => {
32
32
  let initialAfter;
33
33
 
34
- spinner.start();
35
-
36
34
  try {
37
35
  const latestLog = await fetchLatest();
38
36
  initialAfter = base64EncodeString(latestLog.id);
@@ -54,8 +52,7 @@ const tailLogs = async ({
54
52
  latestLog = await tailCall(after);
55
53
  nextAfter = latestLog.paging.next.after;
56
54
  } catch (e) {
57
- logger.info('Stopped polling due to error.');
58
- spinner.clear();
55
+ spinnies.fail('tailLogs', { text: 'Stopped polling due to error.' });
59
56
  if (e.statusCode !== 404) {
60
57
  logApiErrorInstance(
61
58
  e,
@@ -68,7 +65,6 @@ const tailLogs = async ({
68
65
  }
69
66
 
70
67
  if (latestLog && latestLog.results.length) {
71
- spinner.clear();
72
68
  outputLogs(latestLog, {
73
69
  compact,
74
70
  });
@@ -79,8 +75,10 @@ const tailLogs = async ({
79
75
  }, TAIL_DELAY);
80
76
  };
81
77
 
82
- handleKeypressToExit(() => {
83
- spinner.stop();
78
+ handleKeypressToExit(exitKey => {
79
+ spinnies.succeed('tailLogs', {
80
+ text: `Stopped polling because "${exitKey}" was pressed.`,
81
+ });
84
82
  process.exit();
85
83
  });
86
84
  await tail(initialAfter);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "3.0.9-beta.1",
3
+ "version": "3.0.10-beta.1",
4
4
  "description": "CLI for working with HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -8,8 +8,8 @@
8
8
  "url": "https://github.com/HubSpot/hubspot-cms-tools"
9
9
  },
10
10
  "dependencies": {
11
- "@hubspot/cli-lib": "^3.0.9-beta.1",
12
- "@hubspot/serverless-dev-runtime": "^3.0.9-beta.1",
11
+ "@hubspot/cli-lib": "^3.0.10-beta.1",
12
+ "@hubspot/serverless-dev-runtime": "^3.0.10-beta.1",
13
13
  "archiver": "^5.3.0",
14
14
  "chalk": "^4.1.0",
15
15
  "express": "^4.17.1",
@@ -20,6 +20,7 @@
20
20
  "ora": "^4.0.3",
21
21
  "shelljs": "0.8.3",
22
22
  "spinnies": "^0.5.1",
23
+ "supports-hyperlinks": "^2.2.0",
23
24
  "tmp": "^0.2.1",
24
25
  "update-notifier": "3.0.1",
25
26
  "yargs": "15.4.1"
@@ -37,5 +38,5 @@
37
38
  "publishConfig": {
38
39
  "access": "public"
39
40
  },
40
- "gitHead": "51d04fddb68545a0bc89fc9eddbed89afe831ef5"
41
+ "gitHead": "d3426a88d8355cdae053f02853a535c09ba84a7b"
41
42
  }