netlify-cli 8.17.4 → 8.18.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.
package/README.md CHANGED
@@ -203,6 +203,7 @@ Handle various site operations
203
203
  | Subcommand | description |
204
204
  |:--------------------------- |:-----|
205
205
  | [`sites:create`](/docs/commands/sites.md#sitescreate) | Create an empty site (advanced) |
206
+ | [`sites:create-template`](/docs/commands/sites.md#sitescreate-template) | (Beta) Create a site from a starter template |
206
207
  | [`sites:delete`](/docs/commands/sites.md#sitesdelete) | Delete a site |
207
208
  | [`sites:list`](/docs/commands/sites.md#siteslist) | List all sites you have access to |
208
209
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "8.17.4",
3
+ "version": "8.18.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "netlify-cli",
9
- "version": "8.17.4",
9
+ "version": "8.18.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "8.17.4",
4
+ "version": "8.18.0",
5
5
  "author": "Netlify Inc.",
6
6
  "contributors": [
7
7
  "Mathias Biilmann <matt@netlify.com> (https://twitter.com/biilmann)",
@@ -0,0 +1,212 @@
1
+ // @ts-check
2
+
3
+ const inquirer = require('inquirer')
4
+ const pick = require('lodash/pick')
5
+ const prettyjson = require('prettyjson')
6
+
7
+ const { chalk, error, getRepoData, log, logJson, track, warn } = require('../../utils')
8
+ const { configureRepo } = require('../../utils/init/config')
9
+ const { getGitHubToken } = require('../../utils/init/config-github')
10
+ const { createRepo, getTemplatesFromGitHub } = require('../../utils/sites/utils')
11
+
12
+ const { getSiteNameInput } = require('./sites-create')
13
+
14
+ const fetchTemplates = async (token) => {
15
+ const templatesFromGithubOrg = await getTemplatesFromGitHub(token)
16
+
17
+ return templatesFromGithubOrg
18
+ .filter((repo) => !repo.archived && !repo.private && !repo.disabled)
19
+ .map((template) => ({
20
+ name: template.name,
21
+ sourceCodeUrl: template.html_url,
22
+ slug: template.full_name,
23
+ }))
24
+ }
25
+
26
+ /**
27
+ * The sites:create-template command
28
+ * @param {import('commander').OptionValues} options
29
+ * @param {import('../base-command').BaseCommand} command
30
+ */
31
+ const sitesCreateTemplate = async (options, command) => {
32
+ const { api } = command.netlify
33
+
34
+ await command.authenticate()
35
+
36
+ const { globalConfig } = command.netlify
37
+ const ghToken = await getGitHubToken({ globalConfig })
38
+
39
+ let { url: templateUrl } = options
40
+
41
+ if (templateUrl) {
42
+ const urlFromOptions = new URL(templateUrl)
43
+ templateUrl = { templateName: urlFromOptions.pathname.slice(1) }
44
+ } else {
45
+ const templates = await fetchTemplates(ghToken)
46
+
47
+ log(`Choose one of our starter templates. Netlify will create a new repo for this template in your GitHub account.`)
48
+
49
+ templateUrl = await inquirer.prompt([
50
+ {
51
+ type: 'list',
52
+ name: 'templateName',
53
+ message: 'Template:',
54
+ choices: templates.map((template) => ({
55
+ value: template.slug,
56
+ name: template.name,
57
+ })),
58
+ },
59
+ ])
60
+ }
61
+
62
+ const accounts = await api.listAccountsForUser()
63
+
64
+ let { accountSlug } = options
65
+
66
+ if (!accountSlug) {
67
+ const { accountSlug: accountSlugInput } = await inquirer.prompt([
68
+ {
69
+ type: 'list',
70
+ name: 'accountSlug',
71
+ message: 'Team:',
72
+ choices: accounts.map((account) => ({
73
+ value: account.slug,
74
+ name: account.name,
75
+ })),
76
+ },
77
+ ])
78
+ accountSlug = accountSlugInput
79
+ }
80
+
81
+ const { name: nameFlag } = options
82
+ let user
83
+ let site
84
+
85
+ // Allow the user to reenter site name if selected one isn't available
86
+ const inputSiteName = async (name) => {
87
+ const { name: inputName, siteSuggestion } = await getSiteNameInput(name, user, api)
88
+
89
+ try {
90
+ const siteName = inputName ? inputName.trim() : siteSuggestion
91
+
92
+ // Create new repo from template
93
+ const repoResp = await createRepo(templateUrl, ghToken, siteName)
94
+
95
+ if (repoResp.errors) {
96
+ if (repoResp.errors[0].includes('Name already exists on this account')) {
97
+ warn(
98
+ `Oh no! We found already a repository with this name. It seems you have already created a template with the name ${templateUrl.templateName}. Please try to run the command again and provide a different name.`,
99
+ )
100
+ await inputSiteName()
101
+ } else {
102
+ throw new Error(
103
+ `Oops! Seems like something went wrong trying to create the repository. We're getting the following error: '${repoResp.errors[0]}'. You can try to re-run this command again or open an issue in our repository: https://github.com/netlify/cli/issues`,
104
+ )
105
+ }
106
+ } else {
107
+ site = await api.createSiteInTeam({
108
+ accountSlug,
109
+ body: {
110
+ repo: {
111
+ provider: 'github',
112
+ repo: repoResp.full_name,
113
+ private: repoResp.private,
114
+ branch: repoResp.default_branch,
115
+ },
116
+ name: siteName,
117
+ },
118
+ })
119
+ }
120
+ } catch (error_) {
121
+ if (error_.status === 422 || error_.message === 'Duplicate repo') {
122
+ warn(
123
+ `${name}.netlify.app already exists or a repository named ${name} already exists on this account. Please try a different slug.`,
124
+ )
125
+ await inputSiteName()
126
+ } else {
127
+ error(`createSiteInTeam error: ${error_.status}: ${error_.message}`)
128
+ }
129
+ }
130
+ }
131
+
132
+ await inputSiteName(nameFlag)
133
+
134
+ log()
135
+ log(chalk.greenBright.bold.underline(`Site Created`))
136
+ log()
137
+
138
+ const siteUrl = site.ssl_url || site.url
139
+ log(
140
+ prettyjson.render({
141
+ 'Admin URL': site.admin_url,
142
+ URL: siteUrl,
143
+ 'Site ID': site.id,
144
+ 'Repo URL': site.build_settings.repo_url,
145
+ }),
146
+ )
147
+
148
+ track('sites_createdFromTemplate', {
149
+ siteId: site.id,
150
+ adminUrl: site.admin_url,
151
+ siteUrl,
152
+ })
153
+
154
+ if (options.withCi) {
155
+ log('Configuring CI')
156
+ const repoData = await getRepoData()
157
+ await configureRepo({ command, siteId: site.id, repoData, manual: options.manual })
158
+ }
159
+
160
+ if (options.json) {
161
+ logJson(
162
+ pick(site, [
163
+ 'id',
164
+ 'state',
165
+ 'plan',
166
+ 'name',
167
+ 'custom_domain',
168
+ 'domain_aliases',
169
+ 'url',
170
+ 'ssl_url',
171
+ 'admin_url',
172
+ 'screenshot_url',
173
+ 'created_at',
174
+ 'updated_at',
175
+ 'user_id',
176
+ 'ssl',
177
+ 'force_ssl',
178
+ 'managed_dns',
179
+ 'deploy_url',
180
+ 'account_name',
181
+ 'account_slug',
182
+ 'git_provider',
183
+ 'deploy_hook',
184
+ 'capabilities',
185
+ 'id_domain',
186
+ ]),
187
+ )
188
+ }
189
+
190
+ return site
191
+ }
192
+
193
+ /**
194
+ * Creates the `netlify sites:create-template` command
195
+ * @param {import('../base-command').BaseCommand} program
196
+ * @returns
197
+ */
198
+ const createSitesFromTemplateCommand = (program) =>
199
+ program
200
+ .command('sites:create-template')
201
+ .description(
202
+ `(Beta) Create a site from a starter template
203
+ Create a site from a starter template.`,
204
+ )
205
+ .option('-n, --name [name]', 'name of site')
206
+ .option('-u, --url [url]', 'template url')
207
+ .option('-a, --account-slug [slug]', 'account slug to create the site under')
208
+ .option('-c, --with-ci', 'initialize CI hooks during site creation')
209
+ .addHelpText('after', `(Beta) Create a site from starter template.`)
210
+ .action(sitesCreateTemplate)
211
+
212
+ module.exports = { createSitesFromTemplateCommand, fetchTemplates }
@@ -13,6 +13,49 @@ const { link } = require('../link')
13
13
 
14
14
  const SITE_NAME_SUGGESTION_SUFFIX_LENGTH = 5
15
15
 
16
+ const getSiteNameInput = async (name, user, api) => {
17
+ let siteSuggestion
18
+ if (!user) user = await api.getCurrentUser()
19
+
20
+ if (!name) {
21
+ let { slug } = user
22
+ let suffix = ''
23
+
24
+ // If the user doesn't have a slug, we'll compute one. Because `full_name` is not guaranteed to be unique, we
25
+ // append a short randomly-generated ID to reduce the likelihood of a conflict.
26
+ if (!slug) {
27
+ slug = slugify(user.full_name || user.email)
28
+ suffix = `-${uuidv4().slice(0, SITE_NAME_SUGGESTION_SUFFIX_LENGTH)}`
29
+ }
30
+
31
+ const suggestions = [
32
+ `super-cool-site-by-${slug}${suffix}`,
33
+ `the-awesome-${slug}-site${suffix}`,
34
+ `${slug}-makes-great-sites${suffix}`,
35
+ `netlify-thinks-${slug}-is-great${suffix}`,
36
+ `the-great-${slug}-site${suffix}`,
37
+ `isnt-${slug}-awesome${suffix}`,
38
+ ]
39
+ siteSuggestion = sample(suggestions)
40
+
41
+ console.log(
42
+ `Choose a unique site name (e.g. ${siteSuggestion}.netlify.app) or leave it blank for a random name. You can update the site name later.`,
43
+ )
44
+ const { name: nameInput } = await inquirer.prompt([
45
+ {
46
+ type: 'input',
47
+ name: 'name',
48
+ message: 'Site name (optional):',
49
+ filter: (val) => (val === '' ? undefined : val),
50
+ validate: (input) => /^[a-zA-Z\d-]+$/.test(input) || 'Only alphanumeric characters and hyphens are allowed',
51
+ },
52
+ ])
53
+ name = nameInput
54
+ }
55
+
56
+ return { name, siteSuggestion }
57
+ }
58
+
16
59
  /**
17
60
  * The sites:create command
18
61
  * @param {import('commander').OptionValues} options
@@ -47,47 +90,11 @@ const sitesCreate = async (options, command) => {
47
90
 
48
91
  // Allow the user to reenter site name if selected one isn't available
49
92
  const inputSiteName = async (name) => {
50
- if (!user) user = await api.getCurrentUser()
51
-
52
- if (!name) {
53
- let { slug } = user
54
- let suffix = ''
55
-
56
- // If the user doesn't have a slug, we'll compute one. Because `full_name` is not guaranteed to be unique, we
57
- // append a short randomly-generated ID to reduce the likelihood of a conflict.
58
- if (!slug) {
59
- slug = slugify(user.full_name || user.email)
60
- suffix = `-${uuidv4().slice(0, SITE_NAME_SUGGESTION_SUFFIX_LENGTH)}`
61
- }
62
-
63
- const suggestions = [
64
- `super-cool-site-by-${slug}${suffix}`,
65
- `the-awesome-${slug}-site${suffix}`,
66
- `${slug}-makes-great-sites${suffix}`,
67
- `netlify-thinks-${slug}-is-great${suffix}`,
68
- `the-great-${slug}-site${suffix}`,
69
- `isnt-${slug}-awesome${suffix}`,
70
- ]
71
- const siteSuggestion = sample(suggestions)
72
-
73
- console.log(
74
- `Choose a unique site name (e.g. ${siteSuggestion}.netlify.app) or leave it blank for a random name. You can update the site name later.`,
75
- )
76
- const { name: nameInput } = await inquirer.prompt([
77
- {
78
- type: 'input',
79
- name: 'name',
80
- message: 'Site name (optional):',
81
- filter: (val) => (val === '' ? undefined : val),
82
- validate: (input) => /^[a-zA-Z\d-]+$/.test(input) || 'Only alphanumeric characters and hyphens are allowed',
83
- },
84
- ])
85
- name = nameInput
86
- }
93
+ const { name: siteName } = await getSiteNameInput(name, user, api)
87
94
 
88
95
  const body = {}
89
- if (typeof name === 'string') {
90
- body.name = name.trim()
96
+ if (typeof siteName === 'string') {
97
+ body.name = siteName.trim()
91
98
  }
92
99
  try {
93
100
  site = await api.createSiteInTeam({
@@ -96,7 +103,7 @@ const sitesCreate = async (options, command) => {
96
103
  })
97
104
  } catch (error_) {
98
105
  if (error_.status === 422) {
99
- warn(`${name}.netlify.app already exists. Please try a different slug.`)
106
+ warn(`${siteName}.netlify.app already exists. Please try a different slug.`)
100
107
  await inputSiteName()
101
108
  } else {
102
109
  error(`createSiteInTeam error: ${error_.status}: ${error_.message}`)
@@ -191,4 +198,4 @@ Create a blank site that isn't associated with any git remote. Will link the sit
191
198
  )
192
199
  .action(sitesCreate)
193
200
 
194
- module.exports = { createSitesCreateCommand, sitesCreate }
201
+ module.exports = { createSitesCreateCommand, sitesCreate, getSiteNameInput }
@@ -1,5 +1,6 @@
1
1
  // @ts-check
2
2
  const { createSitesCreateCommand } = require('./sites-create')
3
+ const { createSitesFromTemplateCommand } = require('./sites-create-template')
3
4
  const { createSitesDeleteCommand } = require('./sites-delete')
4
5
  const { createSitesListCommand } = require('./sites-list')
5
6
 
@@ -19,6 +20,7 @@ const sites = (options, command) => {
19
20
  */
20
21
  const createSitesCommand = (program) => {
21
22
  createSitesCreateCommand(program)
23
+ createSitesFromTemplateCommand(program)
22
24
  createSitesListCommand(program)
23
25
  createSitesDeleteCommand(program)
24
26
 
@@ -0,0 +1,30 @@
1
+ const fetch = require('node-fetch')
2
+
3
+ const getTemplatesFromGitHub = async (token) => {
4
+ const templates = await fetch(`https://api.github.com/orgs/netlify-templates/repos`, {
5
+ method: 'GET',
6
+ headers: {
7
+ Authorization: `token ${token}`,
8
+ },
9
+ })
10
+ const allTemplates = await templates.json()
11
+
12
+ return allTemplates
13
+ }
14
+
15
+ const createRepo = async (templateUrl, ghToken, siteName) => {
16
+ const resp = await fetch(`https://api.github.com/repos/${templateUrl.templateName}/generate`, {
17
+ method: 'POST',
18
+ headers: {
19
+ Authorization: `token ${ghToken}`,
20
+ },
21
+ body: JSON.stringify({
22
+ name: siteName,
23
+ }),
24
+ })
25
+
26
+ const data = await resp.json()
27
+ return data
28
+ }
29
+
30
+ module.exports = { getTemplatesFromGitHub, createRepo }