Skip to content

Commit 7388d3d

Browse files
committed
[FEATURE] add init Github repo from template command
1 parent d839513 commit 7388d3d

13 files changed

Lines changed: 1181 additions & 3848 deletions

File tree

bin/rh-cli.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#!/usr/bin/env node
2-
import { init } from '../lib/init.js'
3-
await init()
2+
import { cliInit } from '../lib/init.js'
3+
await cliInit()

lib/args/args.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import minimist from 'minimist'
55

66
const commands = [
77
'info',
8+
'init',
89
'build',
910
'watch',
1011
'fetch',

lib/args/debug.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import minimist from 'minimist' // eslint-disable-line
1515
* @returns undefined
1616
*/
1717
function debug (data, args) {
18-
process.env.RH_MODE = 'debug'
1918
console.log(chalk.bold.magenta('●─────[debug-start]─────●'))
2019
console.log(`${chalk.blue('CLI Arguments')}`)
2120
args && console.log(args)

lib/cmd/init.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as TYPES from '../types/types.js' // eslint-disable-line
2+
import { checkIsOnline } from '../utils/check.js'
3+
import { checkGithubTokenEnv, checkGithubToken, getGithubOrgs } from '../github/auth.js'
4+
import { githubRepoFromTemplatePrompts, initType } from './init/prompts.js'
5+
import { createGithubRepoWithTemplate, updateGithubRepoSettings, updateGithubBranchProtection } from '../github/repo.js'
6+
import { updateAndCommit } from '../github/commit.js'
7+
import { confirmNextSteps } from '../utils/prompts.js'
8+
import chalk from 'chalk'
9+
10+
/**
11+
* @ignore
12+
* @typedef {TYPES.LOCALDATA} LOCALDATA {@link LOCALDATA}
13+
*/
14+
15+
/**
16+
* #### Show info about the current project
17+
* @param {LOCALDATA} data - local data
18+
* @async
19+
* @returns undefined
20+
*/
21+
async function init (data) {
22+
const type = await initType()
23+
if (type === 'github-repo-based-on-template') {
24+
checkGithubTokenEnv()
25+
await checkIsOnline()
26+
await checkGithubToken(data)
27+
await getGithubOrgs(data)
28+
await githubRepoFromTemplatePrompts(data)
29+
await confirmNextSteps(
30+
`You are about to create a new repository with the following settings:
31+
- Owner: ${chalk.blue(data.prompts?.repoFromTmpl?.newRepoOwner)}
32+
- Name: ${chalk.green(data.prompts?.repoFromTmpl?.newRepoName)}
33+
- Template owner: ${data.prompts?.repoFromTmpl?.templateRepoOwner}
34+
- Template name: ${data.prompts?.repoFromTmpl?.templateRepoName}`
35+
)
36+
await createGithubRepoWithTemplate(data)
37+
await updateGithubRepoSettings(data, {
38+
owner: data.prompts?.repoFromTmpl?.newRepoOwner || '',
39+
repo: data.prompts?.repoFromTmpl?.newRepoName || '',
40+
has_issues: true,
41+
has_projects: false,
42+
has_wiki: false,
43+
allow_rebase_merge: false,
44+
allow_squash_merge: true,
45+
allow_merge_commit: false,
46+
delete_branch_on_merge: true,
47+
allow_update_branch: true,
48+
squash_merge_commit_title: 'PR_TITLE'
49+
})
50+
await updateGithubBranchProtection(data, {
51+
owner: data.prompts?.repoFromTmpl?.newRepoOwner || '',
52+
repo: data.prompts?.repoFromTmpl?.newRepoName || '',
53+
branch: 'master',
54+
required_status_checks: {
55+
strict: true,
56+
contexts: [
57+
'Node (18)',
58+
'Node (20)',
59+
'Check Commit Message',
60+
'Validate theme'
61+
]
62+
},
63+
enforce_admins: false,
64+
required_pull_request_reviews: {
65+
dismiss_stale_reviews: true,
66+
required_approving_review_count: 1,
67+
require_last_push_approval: true
68+
},
69+
restrictions: null,
70+
required_linear_history: true,
71+
required_conversation_resolution: true
72+
})
73+
await updateAndCommit(
74+
data.prompts?.repoFromTmpl?.newRepoOwner || '',
75+
data.prompts?.repoFromTmpl?.newRepoName || '',
76+
['package.json', 'theme/theme.json'],
77+
{
78+
name: data.prompts?.repoFromTmpl?.newRepoName,
79+
label: data.prompts?.repoFromTmpl?.newRepoLabel
80+
}
81+
)
82+
}
83+
}
84+
85+
export { init }

lib/cmd/init/prompts.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import * as TYPES from '../../types/types.js' // eslint-disable-line
2+
import select from '@inquirer/select'
3+
import input from '@inquirer/input'
4+
5+
/**
6+
* @ignore
7+
* @typedef {TYPES.LOCALDATA} LOCALDATA {@link LOCALDATA}
8+
*/
9+
10+
/**
11+
* #### select what to initialize
12+
* @async
13+
* @returns {Promise<string>} - init type
14+
*/
15+
async function initType () {
16+
const answer = await select({
17+
message: 'Initailize:',
18+
choices: [
19+
{
20+
name: 'GitHub repo based on template',
21+
value: 'github-repo-based-on-template',
22+
description: 'Create a new GitHub repo based on a GitHub template repo'
23+
},
24+
{
25+
name: 'Local repo based on template',
26+
value: 'local-repo-based-on-template',
27+
description: 'Create a new local repo based on a GitHub template repo',
28+
disabled: true
29+
}
30+
]
31+
})
32+
return answer
33+
}
34+
35+
/**
36+
* #### select new repo owner
37+
* @async
38+
* @private
39+
* @param {LOCALDATA} data - data object
40+
* @returns {Promise<string>} - new repo owner
41+
*/
42+
async function selectRepoOwner (data) {
43+
if (data.github && data.github.orgs && data.github.login) {
44+
const owners = data.github.orgs.map((/** @type {string} */ org) => {
45+
return {
46+
name: org,
47+
value: org,
48+
description: 'Repository owner to create the new repository'
49+
}
50+
})
51+
owners.push({
52+
name: data.github.login,
53+
value: data.github.login,
54+
description: 'Repository owner to create the new repository'
55+
})
56+
const answer = await select({
57+
message: 'Select repo owner:',
58+
choices: owners
59+
})
60+
return answer
61+
} else {
62+
throw new Error('No GitHub user/orgs found')
63+
}
64+
}
65+
66+
/**
67+
* #### get project name
68+
* @async
69+
* @private
70+
* @returns {Promise<string>} - project name
71+
*/
72+
async function getProjectName () {
73+
const getProjectName = await input({
74+
message: 'New project name:',
75+
validate: (input) => {
76+
if (input.length < 1) {
77+
return 'Please enter a project name'
78+
}
79+
if (!/^[a-zA-Z0-9-]*$/.test(input)) {
80+
return 'Please enter a valid project name in one word or with hyphens'
81+
}
82+
return true
83+
}
84+
})
85+
const projectName = getProjectName.toLowerCase().trim()
86+
return projectName
87+
}
88+
89+
/**
90+
* #### Select child theme
91+
* @async
92+
* @private
93+
* @returns {Promise<string>} - child theme
94+
*/
95+
async function selectRepoTemplate () {
96+
const answer = await select({
97+
message: 'Choose template:',
98+
choices: [
99+
{
100+
name: 'Nimbly lite child',
101+
value: 'nimbly-lite-child',
102+
description: 'Nimbly Light child theme template'
103+
},
104+
{
105+
name: 'Nimbly pro child',
106+
value: 'nimbly-pro-child',
107+
description: 'Nimbly Pro child theme template',
108+
disabled: true
109+
}
110+
]
111+
})
112+
return answer
113+
}
114+
115+
/**
116+
* #### collect data for the new child theme
117+
* @param {LOCALDATA} data - env variables
118+
* @returns undefined
119+
*/
120+
async function githubRepoFromTemplatePrompts (data) {
121+
const childTheme = await selectRepoTemplate()
122+
const projectName = await getProjectName()
123+
const repoOwner = await selectRepoOwner(data)
124+
data.prompts = {
125+
repoFromTmpl: {
126+
newRepoOwner: repoOwner,
127+
newRepoName: `${projectName}-${childTheme}-${new Date().getFullYear()}`,
128+
newRepoLabel: `${projectName} theme`,
129+
templateRepoOwner: 'Resultify',
130+
templateRepoName: childTheme
131+
}
132+
}
133+
}
134+
135+
export { githubRepoFromTemplatePrompts, initType }

lib/github/auth.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as TYPES from '../types/types.js' // eslint-disable-line
2+
import { request } from '@octokit/request'
3+
import chalk from 'chalk'
4+
import ora from 'ora'
5+
6+
/**
7+
* @ignore
8+
* @typedef {TYPES.LOCALDATA} LOCALDATA {@link LOCALDATA}
9+
*/
10+
11+
/**
12+
* #### check github token is set in env
13+
* @memberof GITHUB
14+
* @returns undefined
15+
*/
16+
function checkGithubTokenEnv () {
17+
if (!process.env.GITHUB_TOKEN) {
18+
console.warn(`${chalk.red('Warning:')} No GitHub token found.`)
19+
console.warn(`Add ${chalk.yellow('GITHUB_TOKEN=your-github-token')} to ${chalk.green('.env')} file.`)
20+
process.exit(0)
21+
}
22+
}
23+
24+
/**
25+
* #### check if GitHub token is set and valid to use
26+
* @async
27+
* @memberof GITHUB
28+
* @param {LOCALDATA} data - data object
29+
* @returns undefined
30+
*/
31+
async function checkGithubToken (data) {
32+
const spinner = ora('Check GitHub token').start()
33+
try {
34+
const res = await request('GET /user', {
35+
headers: {
36+
authorization: `token ${process.env.GITHUB_TOKEN}`
37+
}
38+
})
39+
// check if 'x-oauth-scopes' exists
40+
if (!res.headers['x-oauth-scopes']) {
41+
spinner.fail()
42+
console.error(`${chalk.red('Error:')} GitHub token is invalid.`)
43+
console.error('Use GitHub Classic Personal access token with repo scope.')
44+
process.exit(1)
45+
}
46+
// check if 'repo' scope exists
47+
if (!res.headers['x-oauth-scopes'].split(',').includes('repo')) {
48+
spinner.fail()
49+
console.error(`${chalk.red('Error:')} GitHub token is invalid.`)
50+
console.error('Use GitHub Classic Personal access token with repo scope.')
51+
process.exit(1)
52+
}
53+
if (!data.github) {
54+
data.github = {}
55+
}
56+
data.github.login = res.data.login
57+
data.github.token = true
58+
spinner.succeed()
59+
} catch (error) {
60+
spinner.fail()
61+
console.error(`${chalk.red('Error:')} GitHub token is invalid.`)
62+
if (process.env.RH_MODE === 'debug') {
63+
console.error(error)
64+
}
65+
process.exit(1)
66+
}
67+
}
68+
69+
/**
70+
* #### get organization list for the user based on the token
71+
* @async
72+
* @memberof GITHUB
73+
* @param {LOCALDATA} data - data object
74+
* @returns undefined
75+
*/
76+
async function getGithubOrgs (data) {
77+
try {
78+
const res = await request('GET /user/orgs', {
79+
headers: {
80+
authorization: `token ${process.env.GITHUB_TOKEN}`
81+
}
82+
})
83+
if (!data.github) {
84+
data.github = {}
85+
}
86+
data.github.orgs = res.data.map(org => org.login)
87+
} catch (error) {
88+
console.error(`${chalk.red('Error:')} GitHub token is invalid.`)
89+
if (process.env.RH_MODE === 'debug') {
90+
console.error(error)
91+
}
92+
process.exit(1)
93+
}
94+
}
95+
96+
export { checkGithubTokenEnv, checkGithubToken, getGithubOrgs }

0 commit comments

Comments
 (0)