netlify-cli 17.4.0 → 17.5.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.
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "17.4.0",
3
+ "version": "17.5.1",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "netlify-cli",
9
- "version": "17.4.0",
9
+ "version": "17.5.1",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
13
13
  "@bugsnag/js": "7.20.2",
14
14
  "@fastify/static": "6.10.2",
15
- "@netlify/blobs": "^6.3.0",
15
+ "@netlify/blobs": "6.3.0",
16
16
  "@netlify/build": "29.26.6",
17
17
  "@netlify/build-info": "7.11.1",
18
18
  "@netlify/config": "20.10.0",
@@ -119,6 +119,7 @@
119
119
  "uuid": "9.0.0",
120
120
  "wait-port": "1.0.4",
121
121
  "write-file-atomic": "5.0.1",
122
+ "ws": "^8.14.2",
122
123
  "zod": "3.22.4"
123
124
  },
124
125
  "bin": {
@@ -16345,6 +16346,26 @@
16345
16346
  "url": "https://github.com/sponsors/isaacs"
16346
16347
  }
16347
16348
  },
16349
+ "node_modules/ws": {
16350
+ "version": "8.14.2",
16351
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
16352
+ "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
16353
+ "engines": {
16354
+ "node": ">=10.0.0"
16355
+ },
16356
+ "peerDependencies": {
16357
+ "bufferutil": "^4.0.1",
16358
+ "utf-8-validate": ">=5.0.2"
16359
+ },
16360
+ "peerDependenciesMeta": {
16361
+ "bufferutil": {
16362
+ "optional": true
16363
+ },
16364
+ "utf-8-validate": {
16365
+ "optional": true
16366
+ }
16367
+ }
16368
+ },
16348
16369
  "node_modules/xdg-basedir": {
16349
16370
  "version": "5.1.0",
16350
16371
  "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz",
@@ -27885,6 +27906,12 @@
27885
27906
  }
27886
27907
  }
27887
27908
  },
27909
+ "ws": {
27910
+ "version": "8.14.2",
27911
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
27912
+ "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
27913
+ "requires": {}
27914
+ },
27888
27915
  "xdg-basedir": {
27889
27916
  "version": "5.1.0",
27890
27917
  "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "17.4.0",
4
+ "version": "17.5.1",
5
5
  "author": "Netlify Inc.",
6
6
  "type": "module",
7
7
  "engines": {
@@ -20,7 +20,8 @@
20
20
  "/src/lib/templates/**",
21
21
  "!/src/**/node_modules/**",
22
22
  "!/src/**/*.test.js",
23
- "!/src/**/*.mts"
23
+ "!/src/**/*.mts",
24
+ "!/src/**/*.d.mts.map"
24
25
  ],
25
26
  "homepage": "https://github.com/netlify/cli",
26
27
  "keywords": [
@@ -45,7 +46,7 @@
45
46
  "dependencies": {
46
47
  "@bugsnag/js": "7.20.2",
47
48
  "@fastify/static": "6.10.2",
48
- "@netlify/blobs": "^6.3.0",
49
+ "@netlify/blobs": "6.3.0",
49
50
  "@netlify/build": "29.26.6",
50
51
  "@netlify/build-info": "7.11.1",
51
52
  "@netlify/config": "20.10.0",
@@ -152,6 +153,7 @@
152
153
  "uuid": "9.0.0",
153
154
  "wait-port": "1.0.4",
154
155
  "write-file-atomic": "5.0.1",
156
+ "ws": "^8.14.2",
155
157
  "zod": "3.22.4"
156
158
  }
157
159
  }
@@ -3,7 +3,6 @@ import { join, relative, resolve } from 'path';
3
3
  import process from 'process';
4
4
  import { format } from 'util';
5
5
  import { DefaultLogger, Project } from '@netlify/build-info';
6
- // eslint-disable-next-line import/extensions, n/no-missing-import
7
6
  import { NodeFS, NoopLogger } from '@netlify/build-info/node';
8
7
  // @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module '@net... Remove this comment to see the full error message
9
8
  import { resolveConfig } from '@netlify/config';
@@ -99,6 +98,12 @@ async function selectWorkspace(project, filter) {
99
98
  }
100
99
  return selected.path;
101
100
  }
101
+ async function getRepositoryRoot(cwd) {
102
+ const res = await findUp('.git', { cwd, type: 'directory' });
103
+ if (res) {
104
+ return join(res, '..');
105
+ }
106
+ }
102
107
  /** Base command class that provides tracking and config initialization */
103
108
  export default class BaseCommand extends Command {
104
109
  constructor() {
@@ -186,15 +191,11 @@ export default class BaseCommand extends Command {
186
191
  : `${HELP_$} ${command.parent?.name()} ${command.name()} ${command.usage()}`;
187
192
  return padLeft(term, HELP_INDENT_WIDTH);
188
193
  };
189
- /**
190
- * @param {BaseCommand} command
191
- */
192
- // @ts-expect-error TS(7006) FIXME: Parameter 'command' implicitly has an 'any' type.
194
+ // eslint-disable-next-line unicorn/consistent-function-scoping
193
195
  const getCommands = (command) => {
194
196
  const parentCommand = this.name() === 'netlify' ? command : command.parent;
195
- return (
196
- // @ts-expect-error TS(7006) FIXME: Parameter 'cmd' implicitly has an 'any' type.
197
- parentCommand?.commands.filter((cmd) => {
197
+ return (parentCommand?.commands
198
+ .filter((cmd) => {
198
199
  if (cmd._hidden)
199
200
  return false;
200
201
  // the root command
@@ -203,16 +204,11 @@ export default class BaseCommand extends Command {
203
204
  return !cmd.name().includes(':');
204
205
  }
205
206
  return cmd.name().startsWith(`${command.name()}:`);
206
- }) || []);
207
+ })
208
+ // eslint-disable-next-line id-length
209
+ .sort((a, b) => a.name().localeCompare(b.name())) || []);
207
210
  };
208
- /**
209
- * override the longestSubcommandTermLength
210
- * @param {BaseCommand} command
211
- * @returns {number}
212
- */
213
- help.longestSubcommandTermLength = (command) =>
214
- // @ts-expect-error TS(7006) FIXME: Parameter 'max' implicitly has an 'any' type.
215
- getCommands(command).reduce((max, cmd) => Math.max(max, cmd.name().length), 0);
211
+ help.longestSubcommandTermLength = (command) => getCommands(command).reduce((max, cmd) => Math.max(max, cmd.name().length), 0);
216
212
  /**
217
213
  * override the longestOptionTermLength to react on hide options flag
218
214
  * @param {BaseCommand} command
@@ -224,12 +220,6 @@ export default class BaseCommand extends Command {
224
220
  (command.noBaseOptions === false &&
225
221
  helper.visibleOptions(command).reduce((max, option) => Math.max(max, helper.optionTerm(option).length), 0)) ||
226
222
  0;
227
- /**
228
- * override the format help function to style it correctly
229
- * @param {BaseCommand} command
230
- * @param {import('commander').Help} helper
231
- * @returns {string}
232
- */
233
223
  help.formatHelp = (command, helper) => {
234
224
  const parentCommand = this.name() === 'netlify' ? command : command.parent;
235
225
  const termWidth = helper.padWidth(command, helper);
@@ -275,7 +265,6 @@ export default class BaseCommand extends Command {
275
265
  if (argumentList.length !== 0) {
276
266
  output = [...output, chalk.bold('ARGUMENTS'), formatHelpList(argumentList), ''];
277
267
  }
278
- // @ts-expect-error TS(2551) FIXME: Property 'noBaseOptions' does not exist on type 'C... Remove this comment to see the full error message
279
268
  if (command.noBaseOptions === false) {
280
269
  // Options
281
270
  const optionList = helper
@@ -297,17 +286,14 @@ export default class BaseCommand extends Command {
297
286
  const aliases = command._aliases.map((alias) => formatItem(`${parentCommand.name()} ${alias}`, null, true));
298
287
  output = [...output, chalk.bold('ALIASES'), formatHelpList(aliases), ''];
299
288
  }
300
- // @ts-expect-error TS(2339) FIXME: Property 'examples' does not exist on type 'Comman... Remove this comment to see the full error message
301
289
  if (command.examples.length !== 0) {
302
290
  output = [
303
291
  ...output,
304
292
  chalk.bold('EXAMPLES'),
305
- // @ts-expect-error TS(2339) FIXME: Property 'examples' does not exist on type 'Comman... Remove this comment to see the full error message
306
293
  formatHelpList(command.examples.map((example) => `${HELP_$} ${example}`)),
307
294
  '',
308
295
  ];
309
296
  }
310
- // @ts-expect-error TS(7006) FIXME: Parameter 'cmd' implicitly has an 'any' type.
311
297
  const commandList = getCommands(command).map((cmd) => formatItem(cmd.name(), helper.subcommandDescription(cmd).split('\n')[0], true));
312
298
  if (commandList.length !== 0) {
313
299
  output = [...output, chalk.bold('COMMANDS'), formatHelpList(commandList), ''];
@@ -343,12 +329,6 @@ export default class BaseCommand extends Command {
343
329
  exit(1);
344
330
  }
345
331
  }
346
- /**
347
- *
348
- * @param {string|undefined} tokenFromFlag
349
- * @returns
350
- */
351
- // @ts-expect-error TS(7006) FIXME: Parameter 'tokenFromFlag' implicitly has an 'any' ... Remove this comment to see the full error message
352
332
  async authenticate(tokenFromFlag) {
353
333
  const [token] = await getToken(tokenFromFlag);
354
334
  if (token) {
@@ -435,7 +415,6 @@ export default class BaseCommand extends Command {
435
415
  // if we are running inside a monorepo or not.
436
416
  // ==================================================
437
417
  // retrieve the repository root
438
- // @ts-expect-error TS(2554) FIXME: Expected 1 arguments, but got 0.
439
418
  const rootDir = await getRepositoryRoot();
440
419
  // Get framework, add to analytics payload for every command, if a framework is set
441
420
  const fs = new NodeFS();
@@ -645,16 +624,3 @@ export default class BaseCommand extends Command {
645
624
  return this.name() === 'serve' ? 'production' : 'dev';
646
625
  }
647
626
  }
648
- /**
649
- * Retrieves the repository root through a git command.
650
- * Returns undefined if not a git project.
651
- * @param {string} [cwd] The optional current working directory
652
- * @returns {Promise<string|undefined>}
653
- */
654
- // @ts-expect-error TS(7006) FIXME: Parameter 'cwd' implicitly has an 'any' type.
655
- async function getRepositoryRoot(cwd) {
656
- const res = await findUp('.git', { cwd, type: 'directory' });
657
- if (res) {
658
- return join(res, '..');
659
- }
660
- }
@@ -0,0 +1,72 @@
1
+ import inquirer from 'inquirer';
2
+ import { log, chalk } from '../../utils/command-helpers.mjs';
3
+ import { getWebSocket } from '../../utils/websockets/index.mjs';
4
+ export function getName({ deploy, userId }) {
5
+ let normalisedName = '';
6
+ const isUserDeploy = deploy.user_id === userId;
7
+ switch (deploy.context) {
8
+ case 'branch-deploy':
9
+ normalisedName = 'Branch Deploy';
10
+ break;
11
+ case 'deploy-preview': {
12
+ // Deploys via the CLI can have the `deploy-preview` context
13
+ // but no review id because they don't come from a PR.
14
+ //
15
+ const id = deploy.review_id;
16
+ normalisedName = id ? `Deploy Preview #${id}` : 'Deploy Preview';
17
+ break;
18
+ }
19
+ default:
20
+ normalisedName = 'Production';
21
+ }
22
+ if (isUserDeploy) {
23
+ normalisedName += chalk.yellow('*');
24
+ }
25
+ return `(${deploy.id.slice(0, 7)}) ${normalisedName}`;
26
+ }
27
+ const logsBuild = async (options, command) => {
28
+ await command.authenticate();
29
+ const client = command.netlify.api;
30
+ const { site } = command.netlify;
31
+ const { id: siteId } = site;
32
+ const userId = command.netlify.globalConfig.get('userId');
33
+ const deploys = await client.listSiteDeploys({ siteId, state: 'building' });
34
+ if (deploys.length === 0) {
35
+ log('No active builds');
36
+ return;
37
+ }
38
+ let [deploy] = deploys;
39
+ if (deploys.length > 1) {
40
+ const { result } = await inquirer.prompt({
41
+ name: 'result',
42
+ type: 'list',
43
+ message: `Select a deploy\n\n${chalk.yellow('*')} indicates a deploy created by you`,
44
+ choices: deploys.map((dep) => ({
45
+ name: getName({ deploy: dep, userId }),
46
+ value: dep.id,
47
+ })),
48
+ });
49
+ deploy = deploys.find((dep) => dep.id === result);
50
+ }
51
+ const { id } = deploy;
52
+ const ws = getWebSocket(`wss://socketeer.services.netlify.com/build/logs`);
53
+ ws.on('open', function open() {
54
+ ws.send(JSON.stringify({ deploy_id: id, site_id: siteId, access_token: client.accessToken }));
55
+ });
56
+ ws.on('message', (data) => {
57
+ const { message, section, type } = JSON.parse(data);
58
+ log(message);
59
+ if (type === 'report' && section === 'building') {
60
+ // end of build
61
+ ws.close();
62
+ }
63
+ });
64
+ ws.on('close', () => {
65
+ log('---');
66
+ });
67
+ };
68
+ export const createLogsBuildCommand = (program) => program
69
+ .command('logs:deploy')
70
+ .alias('logs:build')
71
+ .description('(Beta) Stream the logs of deploys currently being built to the console')
72
+ .action(logsBuild);
@@ -0,0 +1,77 @@
1
+ import { Argument } from 'commander';
2
+ import inquirer from 'inquirer';
3
+ import { chalk, log } from '../../utils/command-helpers.mjs';
4
+ import { getWebSocket } from '../../utils/websockets/index.mjs';
5
+ function getLog(logData) {
6
+ let logString = '';
7
+ switch (logData.level) {
8
+ case 'INFO':
9
+ logString += chalk.blueBright(logData.level);
10
+ break;
11
+ case 'WARN':
12
+ logString += chalk.yellowBright(logData.level);
13
+ break;
14
+ case 'ERROR':
15
+ logString += chalk.redBright(logData.level);
16
+ break;
17
+ default:
18
+ logString += logData.level;
19
+ break;
20
+ }
21
+ return `${logString} ${logData.message}`;
22
+ }
23
+ const logsFunction = async (functionName, options, command) => {
24
+ const client = command.netlify.api;
25
+ const { site } = command.netlify;
26
+ const { id: siteId } = site;
27
+ const { functions = [] } = await client.searchSiteFunctions({ siteId });
28
+ if (functions.length === 0) {
29
+ log(`No functions found for the site`);
30
+ return;
31
+ }
32
+ let selectedFunction;
33
+ if (functionName) {
34
+ selectedFunction = functions.find((fn) => fn.n === functionName);
35
+ }
36
+ else {
37
+ const { result } = await inquirer.prompt({
38
+ name: 'result',
39
+ type: 'list',
40
+ message: 'Select a function',
41
+ choices: functions.map((fn) => fn.n),
42
+ });
43
+ selectedFunction = functions.find((fn) => fn.n === result);
44
+ }
45
+ if (!selectedFunction) {
46
+ log(`Could not find function ${functionName}`);
47
+ return;
48
+ }
49
+ const { a: accountId, oid: functionId } = selectedFunction;
50
+ const ws = getWebSocket('wss://socketeer.services.netlify.com/function/logs');
51
+ ws.on('open', () => {
52
+ ws.send(JSON.stringify({
53
+ function_id: functionId,
54
+ site_id: siteId,
55
+ access_token: client.accessToken,
56
+ account_id: accountId,
57
+ }));
58
+ });
59
+ ws.on('message', (data) => {
60
+ const logData = JSON.parse(data);
61
+ log(getLog(logData));
62
+ });
63
+ ws.on('close', () => {
64
+ log('Connection closed');
65
+ });
66
+ ws.on('error', (err) => {
67
+ log('Connection error');
68
+ log(err);
69
+ });
70
+ };
71
+ export const createLogsFunctionCommand = (program) => program
72
+ .command('logs:function')
73
+ .alias('logs:functions')
74
+ .addArgument(new Argument('[functionName]', 'Name of the function to stream logs for'))
75
+ .addExamples(['netlify logs:function my-function', 'netlify logs:function'])
76
+ .description('(Beta) Stream netlify function logs to the console')
77
+ .action(logsFunction);
@@ -0,0 +1,12 @@
1
+ import { createLogsBuildCommand } from './build.mjs';
2
+ import { createLogsFunctionCommand } from './functions.mjs';
3
+ export const createLogsCommand = (program) => {
4
+ createLogsBuildCommand(program);
5
+ createLogsFunctionCommand(program);
6
+ return program
7
+ .command('logs')
8
+ .alias('log')
9
+ .description('Stream logs from your site')
10
+ .addExamples(['netlify logs:deploy', 'netlify logs:function', 'netlify logs:function my-function'])
11
+ .action((_, command) => command.help());
12
+ };
@@ -26,6 +26,7 @@ import { createLinkCommand } from './link/index.mjs';
26
26
  import { createLmCommand } from './lm/index.mjs';
27
27
  import { createLoginCommand } from './login/index.mjs';
28
28
  import { createLogoutCommand } from './logout/index.mjs';
29
+ import { createLogsCommand } from './logs/index.mjs';
29
30
  import { createOpenCommand } from './open/index.mjs';
30
31
  import { createRecipesCommand } from './recipes/index.mjs';
31
32
  import { createServeCommand } from './serve/serve.mjs';
@@ -171,6 +172,7 @@ export const createMainCommand = () => {
171
172
  createSwitchCommand(program);
172
173
  createUnlinkCommand(program);
173
174
  createWatchCommand(program);
175
+ createLogsCommand(program);
174
176
  program
175
177
  .version(USER_AGENT, '-V')
176
178
  .showSuggestionAfterError(true)
@@ -9,7 +9,7 @@ import { createStatusHooksCommand } from './status-hooks.mjs';
9
9
  */
10
10
  // @ts-expect-error TS(7006) FIXME: Parameter 'options' implicitly has an 'any' type.
11
11
  const status = async (options, command) => {
12
- const { api, globalConfig, site } = command.netlify;
12
+ const { api, globalConfig, site, siteInfo } = command.netlify;
13
13
  const current = globalConfig.get('userId');
14
14
  // @ts-expect-error TS(2554) FIXME: Expected 1 arguments, but got 0.
15
15
  const [accessToken] = await getToken();
@@ -26,6 +26,7 @@ const status = async (options, command) => {
26
26
  let accounts;
27
27
  let user;
28
28
  try {
29
+ // eslint-disable-next-line @typescript-eslint/no-extra-semi
29
30
  ;
30
31
  [accounts, user] = await Promise.all([api.listAccountsForUser(), api.getCurrentUser()]);
31
32
  }
@@ -56,35 +57,19 @@ const status = async (options, command) => {
56
57
  warn('Did you run `netlify link` yet?');
57
58
  error(`You don't appear to be in a folder that is linked to a site`);
58
59
  }
59
- let siteData;
60
- try {
61
- siteData = await api.getSite({ siteId });
62
- }
63
- catch (error_) {
64
- // unauthorized
65
- // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'.
66
- if (error_.status === 401) {
67
- warn(`Log in with a different account or re-link to a site you have permission for`);
68
- error(`Not authorized to view the currently linked site (${siteId})`);
69
- }
70
- // missing
71
- // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'.
72
- if (error_.status === 404) {
73
- error(`The site this folder is linked to can't be found`);
74
- }
75
- // @ts-expect-error TS(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message
76
- error(error_);
60
+ if (!siteInfo) {
61
+ error(`No site info found for site ${siteId}`);
77
62
  }
78
63
  // Json only logs out if --json flag is passed
79
64
  if (options.json) {
80
65
  logJson({
81
66
  account: cleanAccountData,
82
67
  siteData: {
83
- 'site-name': `${siteData.name}`,
68
+ 'site-name': `${siteInfo.name}`,
84
69
  'config-path': site.configPath,
85
- 'admin-url': siteData.admin_url,
86
- 'site-url': siteData.ssl_url || siteData.url,
87
- 'site-id': siteData.id,
70
+ 'admin-url': siteInfo.admin_url,
71
+ 'site-url': siteInfo.ssl_url || siteInfo.url,
72
+ 'site-id': siteInfo.id,
88
73
  },
89
74
  });
90
75
  }
@@ -92,11 +77,11 @@ const status = async (options, command) => {
92
77
  Netlify Site Info │
93
78
  ────────────────────┘`);
94
79
  log(prettyjson.render({
95
- 'Current site': `${siteData.name}`,
80
+ 'Current site': `${siteInfo.name}`,
96
81
  'Netlify TOML': site.configPath,
97
- 'Admin URL': chalk.magentaBright(siteData.admin_url),
98
- 'Site URL': chalk.cyanBright(siteData.ssl_url || siteData.url),
99
- 'Site Id': chalk.yellowBright(siteData.id),
82
+ 'Admin URL': chalk.magentaBright(siteInfo.admin_url),
83
+ 'Site URL': chalk.cyanBright(siteInfo.ssl_url || siteInfo.url),
84
+ 'Site Id': chalk.yellowBright(siteInfo.id),
100
85
  }));
101
86
  log();
102
87
  };
@@ -0,0 +1,2 @@
1
+ import WebSocket from 'ws';
2
+ export const getWebSocket = (url) => new WebSocket(url);