netlify 27.10.1 → 27.11.0

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.
Files changed (45) hide show
  1. package/dist/commands/agents/agents-create.js +19 -33
  2. package/dist/commands/base-command.d.ts +1 -1
  3. package/dist/commands/base-command.js +8 -11
  4. package/dist/commands/clone/clone.js +5 -10
  5. package/dist/commands/completion/completion.js +5 -9
  6. package/dist/commands/create/create-action.js +16 -28
  7. package/dist/commands/database/db-init.js +17 -31
  8. package/dist/commands/database/db-migration-new.js +13 -23
  9. package/dist/commands/database/db-migration-pull.js +5 -9
  10. package/dist/commands/database/db-reset.js +5 -9
  11. package/dist/commands/deploy/deploy.js +23 -36
  12. package/dist/commands/env/env-list.js +5 -9
  13. package/dist/commands/functions/functions-create.js +35 -71
  14. package/dist/commands/functions/functions-invoke.js +5 -10
  15. package/dist/commands/init/init.js +12 -25
  16. package/dist/commands/link/link.js +26 -52
  17. package/dist/commands/logs/build.js +2 -4
  18. package/dist/commands/main.js +10 -17
  19. package/dist/commands/recipes/recipes.js +6 -16
  20. package/dist/commands/sites/sites-create.js +12 -22
  21. package/dist/commands/sites/sites-delete.js +3 -7
  22. package/dist/commands/switch/switch.js +5 -9
  23. package/dist/lib/edge-functions/editor-helper.js +3 -5
  24. package/dist/recipes/ai-context/index.js +9 -17
  25. package/dist/recipes/blobs-migrate/index.js +3 -7
  26. package/dist/recipes/vscode/index.js +6 -10
  27. package/dist/utils/build-info.js +3 -6
  28. package/dist/utils/deploy/deploy-site.js +21 -12
  29. package/dist/utils/deploy/upload-files.js +6 -2
  30. package/dist/utils/deploy/util.d.ts +1 -0
  31. package/dist/utils/deploy/util.js +1 -0
  32. package/dist/utils/exit-codes.d.ts +2 -0
  33. package/dist/utils/exit-codes.js +2 -0
  34. package/dist/utils/gh-auth.js +10 -22
  35. package/dist/utils/github-api.d.ts +22 -0
  36. package/dist/utils/github-api.js +47 -0
  37. package/dist/utils/init/config-github.js +29 -45
  38. package/dist/utils/init/config-manual.js +8 -28
  39. package/dist/utils/init/utils.js +14 -38
  40. package/dist/utils/prompts/confirm-prompt.js +4 -15
  41. package/dist/utils/proxy.js +7 -48
  42. package/dist/utils/redirects.js +3 -2
  43. package/dist/utils/rules-proxy.js +31 -29
  44. package/dist/utils/types.d.ts +2 -2
  45. package/package.json +10 -14
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import inquirer from 'inquirer';
2
+ import { input, select } from '@inquirer/prompts';
3
3
  import execa from 'execa';
4
4
  import { logAndThrowError, log, version } from '../../utils/command-helpers.js';
5
5
  import { track } from '../../utils/telemetry/index.js';
@@ -25,26 +25,18 @@ const presets = cliContextConsumers.map((consumer) => ({
25
25
  // always add the custom location option (not preset from API)
26
26
  presets.push({ name: 'Custom location', value: rulesForDefaultConsumer.key });
27
27
  const promptForContextConsumerSelection = async () => {
28
- const { consumerKey } = await inquirer.prompt([
29
- {
30
- name: 'consumerKey',
31
- message: 'Where should we put the context files?',
32
- type: 'list',
33
- choices: presets,
34
- },
35
- ]);
28
+ const consumerKey = await select({
29
+ message: 'Where should we put the context files?',
30
+ choices: presets,
31
+ });
36
32
  const contextConsumer = consumerKey ? cliContextConsumers.find((consumer) => consumer.key === consumerKey) : null;
37
33
  if (contextConsumer) {
38
34
  return contextConsumer;
39
35
  }
40
- const { customPath } = await inquirer.prompt([
41
- {
42
- type: 'input',
43
- name: 'customPath',
44
- message: 'Enter the path, relative to the project root, where the context files should be placed',
45
- default: './ai-context',
46
- },
47
- ]);
36
+ const customPath = await input({
37
+ message: 'Enter the path, relative to the project root, where the context files should be placed',
38
+ default: './ai-context',
39
+ });
48
40
  if (customPath) {
49
41
  return { ...rulesForDefaultConsumer, path: customPath || rulesForDefaultConsumer.path };
50
42
  }
@@ -1,5 +1,5 @@
1
1
  import { getStore, listStores } from '@netlify/blobs';
2
- import inquirer from 'inquirer';
2
+ import { confirm } from '@inquirer/prompts';
3
3
  import pMap from 'p-map';
4
4
  import { logAndThrowError, log } from '../../utils/command-helpers.js';
5
5
  export const description = 'Migrate legacy Netlify Blobs stores';
@@ -32,9 +32,7 @@ export const run = async ({ args, command }) => {
32
32
  }
33
33
  const { stores } = await listStores(clientOptions);
34
34
  if (stores.includes(storeName)) {
35
- const { confirmExistingStore } = await inquirer.prompt({
36
- type: 'confirm',
37
- name: 'confirmExistingStore',
35
+ const confirmExistingStore = await confirm({
38
36
  message: `The store '${storeName}' already exists in the new format, which means it has already been migrated or it has been used with a newer version of the Netlify Blobs client. If you continue with the migration, any blobs from the legacy store will overwrite newer entries that have the same key. Do you want to proceed?`,
39
37
  default: false,
40
38
  });
@@ -42,9 +40,7 @@ export const run = async ({ args, command }) => {
42
40
  return;
43
41
  }
44
42
  }
45
- const { confirmMigration } = await inquirer.prompt({
46
- type: 'confirm',
47
- name: 'confirmMigration',
43
+ const confirmMigration = await confirm({
48
44
  message: `You're about to migrate the store '${storeName}' with ${blobs.length} blobs. Do you want to proceed?`,
49
45
  default: true,
50
46
  });
@@ -1,7 +1,7 @@
1
1
  import { join } from 'path';
2
2
  import { DenoBridge } from '@netlify/edge-bundler';
3
3
  import execa from 'execa';
4
- import inquirer from 'inquirer';
4
+ import { confirm } from '@inquirer/prompts';
5
5
  import { NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, logAndThrowError, log } from '../../utils/command-helpers.js';
6
6
  import { applySettings, getSettings, writeSettings } from './settings.js';
7
7
  export const description = 'Create VS Code settings for an optimal experience with Netlify projects';
@@ -11,9 +11,7 @@ const getPrompt = ({ fileExists, path }) => {
11
11
  const message = fileExists
12
12
  ? `There is a VS Code settings file at ${formattedPath}. Can we update it?`
13
13
  : `A new VS Code settings file will be created at ${formattedPath}`;
14
- return inquirer.prompt({
15
- type: 'confirm',
16
- name: 'confirm',
14
+ return confirm({
17
15
  message,
18
16
  default: true,
19
17
  });
@@ -42,9 +40,7 @@ const getDenoVSCodeExt = async (repositoryRoot) => {
42
40
  };
43
41
  const getDenoExtPrompt = () => {
44
42
  const message = 'The Deno VS Code extension is recommended. Would you like to install it now?';
45
- return inquirer.prompt({
46
- type: 'confirm',
47
- name: 'confirm',
43
+ return confirm({
48
44
  message,
49
45
  default: true,
50
46
  });
@@ -67,13 +63,13 @@ export const run = async ({ config, repositoryRoot }) => {
67
63
  const edgeFunctionsPath = getEdgeFunctionsPath({ config, repositoryRoot });
68
64
  const { fileExists, settings: existingSettings } = await getSettings(settingsPath);
69
65
  const settings = applySettings(existingSettings, { denoBinary, edgeFunctionsPath, repositoryRoot });
70
- const { confirm } = await getPrompt({ fileExists, path: settingsPath });
71
- if (!confirm) {
66
+ const confirmed = await getPrompt({ fileExists, path: settingsPath });
67
+ if (!confirmed) {
72
68
  return;
73
69
  }
74
70
  try {
75
71
  if (!(await hasDenoVSCodeExt(repositoryRoot))) {
76
- const { confirm: denoExtConfirm } = await getDenoExtPrompt();
72
+ const denoExtConfirm = await getDenoExtPrompt();
77
73
  if (denoExtConfirm) {
78
74
  getDenoVSCodeExt(repositoryRoot);
79
75
  }
@@ -1,6 +1,6 @@
1
+ import { search } from '@inquirer/prompts';
1
2
  import { isCI } from 'ci-info';
2
3
  import fuzzy from 'fuzzy';
3
- import inquirer from 'inquirer';
4
4
  import { chalk, log } from './command-helpers.js';
5
5
  /**
6
6
  * Filters the inquirer settings based on the input
@@ -73,12 +73,9 @@ export const detectFrameworkSettings = async (command, type = 'dev') => {
73
73
  }
74
74
  // multiple matching detectors, make the user choose
75
75
  const scriptInquirerOptions = formatSettingsArrForInquirer(settings, type);
76
- const { chosenSettings } = await inquirer.prompt({
77
- name: 'chosenSettings',
76
+ const chosenSettings = await search({
78
77
  message: `Multiple possible ${type} commands found`,
79
- // @ts-expect-error is not known by the types as it uses the autocomplete plugin
80
- type: 'autocomplete',
81
- source(_, input = '') {
78
+ source(input = '') {
82
79
  if (!input)
83
80
  return scriptInquirerOptions;
84
81
  // only show filtered results
@@ -9,12 +9,14 @@ import hashFiles from './hash-files.js';
9
9
  import hashFns from './hash-fns.js';
10
10
  import { deployFileNormalizer, getDbMigrationsDistPathIfExists, getDeployConfigPathIfExists, getEdgeFunctionsDistPathIfExists, isEdgeFunctionFile, } from './process-files.js';
11
11
  import uploadFiles from './upload-files.js';
12
- import { getUploadList, waitForDeploy, waitForDiff } from './util.js';
12
+ import { getUploadList, pluralize, waitForDeploy, waitForDiff } from './util.js';
13
13
  import { temporaryDirectory } from '../temporary-file.js';
14
14
  const buildStatsString = (possibleParts) => {
15
15
  const parts = possibleParts.filter(Boolean);
16
- const message = parts.slice(0, -1).join(', ');
17
- return parts.length > 1 ? `${message} and ${parts[parts.length - 1]}` : message;
16
+ if (parts.length < 2) {
17
+ return parts.join('');
18
+ }
19
+ return `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`;
18
20
  };
19
21
  export const deploySite = async (command, api,
20
22
  // @ts-expect-error TS(7006) FIXME: Parameter 'siteId' implicitly has an 'any' type.
@@ -45,7 +47,7 @@ skipFunctionsCache, statusCb = () => {
45
47
  }, syncFileLimit = DEFAULT_SYNC_LIMIT, tmpDir = temporaryDirectory(), workingDir, }) => {
46
48
  statusCb({
47
49
  type: 'hashing',
48
- msg: `Hashing files...`,
50
+ msg: `Preparing deploy...`,
49
51
  phase: 'start',
50
52
  });
51
53
  const edgeFunctionsDistPath = await getEdgeFunctionsDistPathIfExists(workingDir);
@@ -83,17 +85,18 @@ skipFunctionsCache, statusCb = () => {
83
85
  const filesCount = Object.keys(files).length - edgeFunctionsCount;
84
86
  const functionsCount = Object.keys(functions).length;
85
87
  const stats = buildStatsString([
86
- filesCount > 0 && `${filesCount} files`,
87
- functionsCount > 0 && `${functionsCount} functions`,
88
- edgeFunctionsCount > 0 && 'edge functions',
88
+ filesCount > 0 && pluralize(filesCount, 'file'),
89
+ functionsCount > 0 && pluralize(functionsCount, 'function'),
90
+ edgeFunctionsCount > 0 && pluralize(edgeFunctionsCount, 'edge function'),
91
+ server && 'a server',
89
92
  ]);
90
93
  statusCb({
91
94
  type: 'hashing',
92
- msg: `Finished hashing ${stats}`,
95
+ msg: `Deploying ${stats}`,
93
96
  phase: 'stop',
94
97
  });
95
- if (filesCount === 0 && functionsCount === 0) {
96
- throw new Error('No files or functions to deploy');
98
+ if (filesCount === 0 && functionsCount === 0 && edgeFunctionsCount === 0 && !server) {
99
+ throw new Error('Nothing to deploy');
97
100
  }
98
101
  if (functionsWithNativeModules.length !== 0) {
99
102
  const functionsWithNativeModulesMessage = functionsWithNativeModules
@@ -112,7 +115,7 @@ For more information, visit https://ntl.fyi/cli-native-modules.`);
112
115
  }
113
116
  statusCb({
114
117
  type: 'create-deploy',
115
- msg: 'CDN diffing files...',
118
+ msg: 'Checking what needs to be uploaded...',
116
119
  phase: 'start',
117
120
  });
118
121
  const packageFrameworks = command.project.frameworks.get(command.workspacePackage ?? '');
@@ -146,9 +149,15 @@ For more information, visit https://ntl.fyi/cli-native-modules.`);
146
149
  if (deployParams.body.async)
147
150
  deploy = await waitForDiff(api, deploy.id, siteId, deployTimeout);
148
151
  const { required: requiredFiles, required_functions: requiredFns, required_edge_functions: requiredEdgeFns, required_server: requiredServer, } = deploy;
152
+ const newStats = buildStatsString([
153
+ requiredFiles.length > 0 && pluralize(requiredFiles.length, 'file'),
154
+ (requiredFns?.length ?? 0) > 0 && pluralize(requiredFns.length, 'function'),
155
+ (requiredEdgeFns?.length ?? 0) > 0 && pluralize(requiredEdgeFns.length, 'edge function'),
156
+ (requiredServer?.length ?? 0) > 0 && 'a server',
157
+ ]);
149
158
  statusCb({
150
159
  type: 'create-deploy',
151
- msg: `CDN requesting ${requiredFiles.length} files${Array.isArray(requiredFns) ? ` and ${requiredFns.length} functions` : ''}${Array.isArray(requiredEdgeFns) ? ` and ${requiredEdgeFns.length} edge functions` : ''}`,
160
+ msg: newStats ? `Found ${newStats} to upload` : 'Everything is uploaded',
152
161
  phase: 'stop',
153
162
  });
154
163
  const filesUploadList = getUploadList(requiredFiles, filesShaMap);
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import backoff from 'backoff';
3
3
  import pMap from 'p-map';
4
4
  import { UPLOAD_INITIAL_DELAY, UPLOAD_MAX_DELAY, UPLOAD_RANDOM_FACTOR } from './constants.js';
5
+ import { pluralize } from './util.js';
5
6
  class MissingAssetTypeError extends Error {
6
7
  fileObj;
7
8
  constructor(fileObj) {
@@ -12,9 +13,12 @@ class MissingAssetTypeError extends Error {
12
13
  const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRetry, statusCb }) => {
13
14
  if (!concurrentUpload || !maxRetry)
14
15
  throw new Error('Missing required option concurrentUpload');
16
+ if (uploadList.length === 0) {
17
+ return [];
18
+ }
15
19
  statusCb({
16
20
  type: 'upload',
17
- msg: `Uploading ${uploadList.length} files`,
21
+ msg: `Uploading ${pluralize(uploadList.length, 'file')}`,
18
22
  phase: 'start',
19
23
  });
20
24
  const uploadFile = async (fileObj, index) => {
@@ -84,7 +88,7 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
84
88
  const results = await pMap(uploadList, uploadFile, { concurrency: concurrentUpload });
85
89
  statusCb({
86
90
  type: 'upload',
87
- msg: `Finished uploading ${uploadList.length} assets`,
91
+ msg: `Finished uploading ${pluralize(uploadList.length, 'asset')}`,
88
92
  phase: 'stop',
89
93
  });
90
94
  return results;
@@ -1,3 +1,4 @@
1
+ export declare const pluralize: (amount: number, noun: string) => string;
1
2
  export declare const normalizePath: (relname: string) => string;
2
3
  export declare const waitForDiff: (api: any, deployId: any, siteId: any, timeout: any) => Promise<undefined>;
3
4
  export declare const waitForDeploy: (api: any, deployId: any, siteId: any, timeout: any) => Promise<undefined>;
@@ -1,6 +1,7 @@
1
1
  import { sep } from 'path';
2
2
  import pWaitFor from 'p-wait-for';
3
3
  import { DEPLOY_POLL } from './constants.js';
4
+ export const pluralize = (amount, noun) => `${amount} ${noun}${amount === 1 ? '' : 's'}`;
4
5
  // normalize windows paths to unix paths
5
6
  export const normalizePath = (relname) => {
6
7
  if (relname.includes('#') || relname.includes('?')) {
@@ -20,6 +20,8 @@ export declare const EXIT_CODES: {
20
20
  readonly USAGE_ERROR: 2;
21
21
  /** An interactive prompt was required but the session is non-interactive (CI or `--non-interactive`) */
22
22
  readonly NON_INTERACTIVE_PROMPT: 4;
23
+ /** The user cancelled an interactive prompt with Ctrl+C, matching a SIGINT-terminated process */
24
+ readonly INTERRUPTED: 130;
23
25
  };
24
26
  export type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];
25
27
  //# sourceMappingURL=exit-codes.d.ts.map
@@ -20,5 +20,7 @@ export const EXIT_CODES = {
20
20
  USAGE_ERROR: 2,
21
21
  /** An interactive prompt was required but the session is non-interactive (CI or `--non-interactive`) */
22
22
  NON_INTERACTIVE_PROMPT: 4,
23
+ /** The user cancelled an interactive prompt with Ctrl+C, matching a SIGINT-terminated process */
24
+ INTERRUPTED: 130,
23
25
  };
24
26
  //# sourceMappingURL=exit-codes.js.map
@@ -1,26 +1,22 @@
1
1
  // A simple ghauth inspired library for getting a personal access token
2
2
  import http from 'http';
3
3
  import process from 'process';
4
- import { Octokit } from '@octokit/rest';
4
+ import { password, select } from '@inquirer/prompts';
5
5
  import getPort from 'get-port';
6
- import inquirer from 'inquirer';
7
6
  import { log } from './command-helpers.js';
8
7
  import createDeferred from './create-deferred.js';
8
+ import { requestGitHub } from './github-api.js';
9
9
  import openBrowser from './open-browser.js';
10
10
  const SERVER_PORT = 3000;
11
11
  const promptForAuthMethod = async () => {
12
12
  const authChoiceNetlify = 'Authorize with GitHub through app.netlify.com';
13
13
  const authChoiceToken = 'Authorize with a GitHub personal access token';
14
14
  const authChoices = [authChoiceNetlify, authChoiceToken];
15
- const { authMethod } = await inquirer.prompt([
16
- {
17
- type: 'list',
18
- name: 'authMethod',
19
- message: 'Netlify CLI needs access to your GitHub account to configure Webhooks and Deploy Keys. ' +
20
- 'What would you like to do?',
21
- choices: authChoices,
22
- },
23
- ]);
15
+ const authMethod = await select({
16
+ message: 'Netlify CLI needs access to your GitHub account to configure Webhooks and Deploy Keys. ' +
17
+ 'What would you like to do?',
18
+ choices: authChoices,
19
+ });
24
20
  return authMethod === authChoiceNetlify;
25
21
  };
26
22
  /**
@@ -59,15 +55,8 @@ export const authWithNetlify = async () => {
59
55
  return deferredPromise;
60
56
  };
61
57
  const getPersonalAccessToken = async () => {
62
- const { token } = await inquirer.prompt([
63
- {
64
- type: 'password',
65
- name: 'token',
66
- message: 'Your GitHub personal access token:',
67
- filter: (input) => input.trim(),
68
- },
69
- ]);
70
- return { token };
58
+ const token = await password({ message: 'Your GitHub personal access token:' });
59
+ return { token: token.trim() };
71
60
  };
72
61
  /**
73
62
  * Authenticate with the netlify app
@@ -77,8 +66,7 @@ const authWithToken = async () => {
77
66
  if (!token) {
78
67
  throw new Error('GitHub authentication failed');
79
68
  }
80
- const octokit = new Octokit({ auth: `token ${token}` });
81
- const { data: { login: user }, } = await octokit.users.getAuthenticated();
69
+ const { login: user } = await requestGitHub(token, 'GET', '/user');
82
70
  return { token, user, provider: 'github' };
83
71
  };
84
72
  /**
@@ -0,0 +1,22 @@
1
+ export declare class GitHubAPIError extends Error {
2
+ status: number;
3
+ constructor(message: string, status: number);
4
+ }
5
+ export interface GitHubUser {
6
+ login: string;
7
+ }
8
+ export interface GitHubRepo {
9
+ id: number;
10
+ full_name: string;
11
+ default_branch: string;
12
+ }
13
+ export interface GitHubWebhook {
14
+ config: {
15
+ url?: string;
16
+ };
17
+ }
18
+ export declare const requestGitHub: <T>(token: string, method: string, path: string, { query, body }?: {
19
+ query?: Record<string, string | number>;
20
+ body?: unknown;
21
+ }) => Promise<T>;
22
+ //# sourceMappingURL=github-api.d.ts.map
@@ -0,0 +1,47 @@
1
+ const GITHUB_API_URL = 'https://api.github.com';
2
+ export class GitHubAPIError extends Error {
3
+ status;
4
+ constructor(message, status) {
5
+ super(message);
6
+ // Keeps error output identical to what Octokit printed before it was replaced.
7
+ this.name = 'HttpError';
8
+ this.status = status;
9
+ }
10
+ }
11
+ // Mirrors Octokit's message format, which callers rely on to detect specific validation errors.
12
+ const readErrorMessage = async (response) => {
13
+ const text = await response.text();
14
+ let body;
15
+ try {
16
+ body = JSON.parse(text);
17
+ }
18
+ catch {
19
+ return text || response.statusText;
20
+ }
21
+ const message = body.message ?? response.statusText;
22
+ const details = body.errors?.length ? `: ${body.errors.map((error) => JSON.stringify(error)).join(', ')}` : '';
23
+ const suffix = body.documentation_url ? ` - ${body.documentation_url}` : '';
24
+ return `${message}${details}${suffix}`;
25
+ };
26
+ export const requestGitHub = async (token, method, path, { query, body } = {}) => {
27
+ const url = new URL(path, GITHUB_API_URL);
28
+ for (const [key, value] of Object.entries(query ?? {})) {
29
+ url.searchParams.set(key, String(value));
30
+ }
31
+ const response = await fetch(url, {
32
+ method,
33
+ headers: {
34
+ Accept: 'application/vnd.github.v3+json',
35
+ Authorization: `token ${token}`,
36
+ 'User-Agent': 'netlify-cli',
37
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }),
38
+ },
39
+ body: body === undefined ? undefined : JSON.stringify(body),
40
+ });
41
+ if (!response.ok) {
42
+ throw new GitHubAPIError(await readErrorMessage(response), response.status);
43
+ }
44
+ const text = await response.text();
45
+ return (text ? JSON.parse(text) : undefined);
46
+ };
47
+ //# sourceMappingURL=github-api.js.map
@@ -1,6 +1,6 @@
1
- import { Octokit } from '@octokit/rest';
2
1
  import { chalk, logAndThrowError, log } from '../command-helpers.js';
3
2
  import { getGitHubToken as ghauth } from '../gh-auth.js';
3
+ import { requestGitHub } from '../github-api.js';
4
4
  import { createDeployKey, formatErrorMessage, getBuildSettings, saveNetlifyToml, setupSite } from './utils.js';
5
5
  const formatRepoAndOwner = ({ repoName, repoOwner }) => ({
6
6
  name: chalk.magenta(repoName),
@@ -15,11 +15,8 @@ export const getGitHubToken = async ({ globalConfig }) => {
15
15
  const githubToken = globalConfig.get(`users.${userId}.auth.github`);
16
16
  if (githubToken?.user && githubToken.token) {
17
17
  try {
18
- const octokit = getGitHubClient(githubToken.token);
19
- const { status } = await octokit.rest.users.getAuthenticated();
20
- if (status < 400) {
21
- return githubToken.token;
22
- }
18
+ await requestGitHub(githubToken.token, 'GET', '/user');
19
+ return githubToken.token;
23
20
  }
24
21
  catch {
25
22
  log(chalk.yellow('Token is expired or invalid!'));
@@ -30,19 +27,16 @@ export const getGitHubToken = async ({ globalConfig }) => {
30
27
  globalConfig.set(`users.${userId}.auth.github`, newToken);
31
28
  return newToken.token;
32
29
  };
33
- const getGitHubClient = (token) => new Octokit({
34
- auth: `token ${token}`,
35
- });
36
- const addDeployKey = async ({ api, octokit, repoName, repoOwner, }) => {
30
+ const addDeployKey = async ({ api, repoName, repoOwner, token, }) => {
37
31
  log('Adding deploy key to repository...');
38
32
  const key = await createDeployKey({ api });
39
33
  try {
40
- await octokit.repos.createDeployKey({
41
- title: 'Netlify Deploy Key',
42
- key: key.public_key ?? '',
43
- owner: repoOwner,
44
- repo: repoName,
45
- read_only: true,
34
+ await requestGitHub(token, 'POST', `/repos/${repoOwner}/${repoName}/keys`, {
35
+ body: {
36
+ title: 'Netlify Deploy Key',
37
+ key: key.public_key ?? '',
38
+ read_only: true,
39
+ },
46
40
  });
47
41
  log('Deploy key added!');
48
42
  return key;
@@ -57,13 +51,9 @@ const addDeployKey = async ({ api, octokit, repoName, repoOwner, }) => {
57
51
  return logAndThrowError(message);
58
52
  }
59
53
  };
60
- const getGitHubRepo = async ({ octokit, repoName, repoOwner, }) => {
54
+ const getGitHubRepo = async ({ repoName, repoOwner, token, }) => {
61
55
  try {
62
- const { data } = await octokit.repos.get({
63
- owner: repoOwner,
64
- repo: repoName,
65
- });
66
- return data;
56
+ return await requestGitHub(token, 'GET', `/repos/${repoOwner}/${repoName}`);
67
57
  }
68
58
  catch (error) {
69
59
  let message = formatErrorMessage({ message: 'Failed retrieving GitHub repository information', error });
@@ -75,15 +65,11 @@ const getGitHubRepo = async ({ octokit, repoName, repoOwner, }) => {
75
65
  return logAndThrowError(message);
76
66
  }
77
67
  };
78
- // @ts-expect-error TS(7031) FIXME: Binding element 'deployHook' implicitly has an 'an... Remove this comment to see the full error message
79
- const hookExists = async ({ deployHook, octokit, repoName, repoOwner }) => {
68
+ const hookExists = async ({ deployHook, repoName, repoOwner, token }) => {
80
69
  try {
81
- const { data: hooks } = await octokit.repos.listWebhooks({
82
- owner: repoOwner,
83
- repo: repoName,
84
- per_page: PAGE_SIZE,
70
+ const hooks = await requestGitHub(token, 'GET', `/repos/${repoOwner}/${repoName}/hooks`, {
71
+ query: { per_page: PAGE_SIZE },
85
72
  });
86
- // @ts-expect-error TS(7006) FIXME: Parameter 'hook' implicitly has an 'any' type.
87
73
  const exists = hooks.some((hook) => hook.config.url === deployHook);
88
74
  return exists;
89
75
  }
@@ -92,21 +78,20 @@ const hookExists = async ({ deployHook, octokit, repoName, repoOwner }) => {
92
78
  return false;
93
79
  }
94
80
  };
95
- // @ts-expect-error TS(7031) FIXME: Binding element 'deployHook' implicitly has an 'an... Remove this comment to see the full error message
96
- const addDeployHook = async ({ deployHook, octokit, repoName, repoOwner }) => {
97
- const exists = await hookExists({ deployHook, octokit, repoOwner, repoName });
81
+ const addDeployHook = async ({ deployHook, repoName, repoOwner, token }) => {
82
+ const exists = await hookExists({ deployHook, repoOwner, repoName, token });
98
83
  if (!exists) {
99
84
  try {
100
- await octokit.repos.createWebhook({
101
- owner: repoOwner,
102
- repo: repoName,
103
- name: 'web',
104
- config: {
105
- url: deployHook,
106
- content_type: 'json',
85
+ await requestGitHub(token, 'POST', `/repos/${repoOwner}/${repoName}/hooks`, {
86
+ body: {
87
+ name: 'web',
88
+ config: {
89
+ url: deployHook,
90
+ content_type: 'json',
91
+ },
92
+ events: ['push', 'pull_request', 'delete'],
93
+ active: true,
107
94
  },
108
- events: ['push', 'pull_request', 'delete'],
109
- active: true,
110
95
  });
111
96
  }
112
97
  catch (error) {
@@ -183,10 +168,9 @@ export const configGithub = async ({ command, repoName, repoOwner, siteId, }) =>
183
168
  });
184
169
  await saveNetlifyToml({ repositoryRoot, config, configPath, baseDir, buildCmd, buildDir, functionsDir });
185
170
  log();
186
- const octokit = getGitHubClient(token);
187
171
  const [deployKey, githubRepo] = await Promise.all([
188
- addDeployKey({ api, octokit, repoOwner, repoName }),
189
- getGitHubRepo({ octokit, repoOwner, repoName }),
172
+ addDeployKey({ api, repoOwner, repoName, token }),
173
+ getGitHubRepo({ repoOwner, repoName, token }),
190
174
  ]);
191
175
  const repo = {
192
176
  id: githubRepo.id,
@@ -207,7 +191,7 @@ export const configGithub = async ({ command, repoName, repoOwner, siteId, }) =>
207
191
  configPlugins: config.plugins ?? [],
208
192
  pluginsToInstall,
209
193
  });
210
- await addDeployHook({ deployHook: updatedSite.deploy_hook, octokit, repoOwner, repoName });
194
+ await addDeployHook({ deployHook: updatedSite.deploy_hook, repoOwner, repoName, token });
211
195
  log();
212
196
  await addNotificationHooks({ siteId, api, token });
213
197
  };
@@ -1,4 +1,4 @@
1
- import inquirer from 'inquirer';
1
+ import { confirm, input } from '@inquirer/prompts';
2
2
  import { exit, log } from '../command-helpers.js';
3
3
  import { createDeployKey, getBuildSettings, saveNetlifyToml, setupSite } from './utils.js';
4
4
  /**
@@ -9,44 +9,24 @@ const addDeployKey = async (deployKey) => {
9
9
  // FIXME(serhalp): Handle nullish `deployKey.public_key` by throwing user-facing error or fixing upstream type.
10
10
  // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
11
11
  log(`\n${deployKey.public_key}\n\n`);
12
- const { sshKeyAdded } = (await inquirer.prompt([
13
- {
14
- type: 'confirm',
15
- name: 'sshKeyAdded',
16
- message: 'Continue?',
17
- default: true,
18
- },
19
- ]));
12
+ const sshKeyAdded = await confirm({ message: 'Continue?', default: true });
20
13
  if (!sshKeyAdded) {
21
14
  return exit();
22
15
  }
23
16
  };
24
17
  const getRepoPath = async ({ repoData }) => {
25
- const { repoPath } = await inquirer.prompt([
26
- {
27
- type: 'input',
28
- name: 'repoPath',
29
- message: 'The SSH URL of the remote git repo:',
30
- default: repoData.url,
31
- validate: (url) => (SSH_URL_REGEXP.test(url) ? true : 'The URL provided does not use the SSH protocol'),
32
- },
33
- ]);
34
- return repoPath;
18
+ return await input({
19
+ message: 'The SSH URL of the remote git repo:',
20
+ default: repoData.url,
21
+ validate: (url) => (SSH_URL_REGEXP.test(url) ? true : 'The URL provided does not use the SSH protocol'),
22
+ });
35
23
  };
36
24
  const addDeployHook = async (deployHook) => {
37
25
  log('\nConfigure the following webhook for your repository:\n');
38
26
  // FIXME(serhalp): Handle nullish `deployHook` by throwing user-facing error or fixing upstream type.
39
27
  // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
40
28
  log(`\n${deployHook}\n\n`);
41
- const { deployHookAdded } = await inquirer.prompt([
42
- {
43
- type: 'confirm',
44
- name: 'deployHookAdded',
45
- message: 'Continue?',
46
- default: true,
47
- },
48
- ]);
49
- return deployHookAdded;
29
+ return await confirm({ message: 'Continue?', default: true });
50
30
  };
51
31
  const isSupportedProvider = (provider) => provider === 'github' || provider === 'gitlab';
52
32
  export default async function configManual({ command, repoData, siteId, }) {