Skip to content

Commit 5e995ce

Browse files
committed
fix: make create command generate usable projects safely
1 parent 98cb9d1 commit 5e995ce

8 files changed

Lines changed: 209 additions & 156 deletions

File tree

‎README.md‎

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,47 +22,35 @@ using Node.js's native environment file support. Use `--env <path>` to load a
2222
different file. Existing environment variables take precedence, and the file
2323
is optional when variables are provided by the shell or CI.
2424

25-
WARNING: work in process, the following doesn't work yet, but soon!
26-
27-
The easiest way to create a status board for your project is using Github Pages. To get started, create a new repo for your project and clone it
28-
to your development machine. In the new directory run the following:
25+
Create a project interactively:
2926

27+
```sh
28+
npx @pkgjs/statusboard create my-statusboard
29+
cd my-statusboard
30+
npm install
3031
```
31-
# Creates a statusboard project
32-
# @TODO make this command actually work as it does not right now
33-
$ npx @pkgjs/statusboard create
34-
35-
# Setup your config in `index.js
36-
# Then commit your work
37-
$ git commit -am "statusboard setup"
38-
39-
# Create an orphan branch for our builds
40-
$ git checkout --orphan gh-pages
4132

42-
# Remove the files we dont need here
43-
$ git rm -rf .
33+
The command asks for organizations, repositories (`owner/repo`), issue labels,
34+
and optional GitHub Actions deployment. It creates `config.js`, `package.json`,
35+
`.gitignore`, and a local Git repository. Choose a new or empty directory;
36+
existing project files are never overwritten.
4437

45-
# Create a .nojekyll file, this turns off pesky github pages stuff
46-
$ touch .nojekyll
47-
$ git commit --am "github pages initial commit"
38+
Set `GITHUB_TOKEN` in your environment or in a local `.env` file, then run:
4839

49-
# Now we setup the branch as a working tree on the master branch
50-
$ git checkout master
51-
$ mkdir build
52-
$ git worktree add build gh-pages
40+
```sh
41+
npm run build
42+
```
5343

54-
# Now we can run the index and build
55-
$ npm run build
44+
The site is generated in `build/`. Edit `config.js` to customize the board.
5645

57-
# Now we should have a site in ./build, we can
58-
# commit and push the branches now
59-
$ cd build && git add . && git commit -m "our new statuspage" && git push
60-
```
46+
If you selected GitHub Actions, push the generated project to a GitHub repository
47+
with a `main` branch and enable **Settings → Pages → Source → GitHub Actions**.
48+
The workflow builds and deploys the site using the repository's Pages base path.
49+
Without Actions, set `baseUrl` in `config.js` to match your hosting path.
6150

6251
## TODO
6352

6453
- Cli logger
65-
- `create` command to setup a new project
6654
- Contribution graph like on github
6755
- Meetings page (pull tag "meeting")
6856
- Typescript support (load typings or if authored in TS)

‎lib/cli.js‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ module.exports = (create, builder, argv) => {
8282

8383
let cli = yargs()
8484

85-
.command('create [directory]', 'Create a StatusBoard', {}, async (argv) => {
85+
.command('create [directory]', 'Create a StatusBoard', (cli) => cli.positional('directory', {
86+
description: 'Directory for the new project',
87+
type: 'string'
88+
}), async (argv) => {
8689
await initStatusboard(argv)
8790
})
8891

‎lib/commands/create.js‎

Lines changed: 63 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,126 @@
1-
const inquirer = require('inquirer')
1+
'use strict'
2+
const inquirer = require('inquirer').default
23
const path = require('node:path')
3-
const createPackageJson = require('create-package-json')
4-
const fs = require('fs-extra')
5-
const { gitInit } = require('../git.js')
6-
7-
async function create (opts) {
8-
const { directory } = opts
9-
10-
const config = {
11-
path: directory,
12-
name: undefined,
13-
labels: [],
14-
orgs: [],
15-
projects: [],
16-
githubActions: false
17-
}
4+
const fs = require('node:fs/promises')
5+
const { gitInit } = require('../git')
6+
const packageJson = require('../../package.json')
187

8+
async function create (opts = {}) {
199
const answers = await inquirer.prompt([
2010
{
2111
type: 'input',
2212
name: 'path',
2313
message: 'Where would you like to create your project?',
2414
default: 'statusboard',
25-
when: () => !config.path
15+
when: () => !opts.directory,
16+
validate: value => value.trim().length > 0 || 'Enter a project directory.'
2617
},
2718
{
2819
type: 'confirm',
2920
name: 'defaultLabels',
30-
message: 'Do you want use default labels?',
21+
message: 'Do you want to use the default labels?',
3122
default: true
3223
},
3324
{
3425
type: 'input',
3526
name: 'labels',
36-
message: 'What labels do you want to use (use commas to separate)',
37-
when: (answers) => !answers.defaultLabels
27+
message: 'What labels do you want to use (use commas to separate)?',
28+
when: answers => !answers.defaultLabels
3829
},
3930
{
4031
type: 'input',
4132
name: 'orgs',
42-
message:
43-
'What organizations do you want to include (use commas to separate)'
33+
message: 'What organizations do you want to include (use commas to separate)?'
4434
},
4535
{
4636
type: 'input',
4737
name: 'repositories',
48-
message:
49-
'What repositories do you want to include (use commas to separate)'
38+
message: 'What repositories do you want to include (owner/repo, separated by commas)?',
39+
validate: value => transformUserInput(value).every(repo => /^[\w.-]+\/[\w.-]+$/.test(repo)) || 'Use owner/repo for each repository.'
5040
},
5141
{
5242
type: 'confirm',
5343
name: 'githubActions',
54-
message: 'Do you want deploy with GitHub Actions?',
44+
message: 'Do you want to deploy with GitHub Actions?',
5545
default: false
5646
}
5747
])
5848

59-
config.path = answers.path.trim()
60-
61-
config.labels = transformUserInput(answers.labels)
62-
config.orgs = transformUserInput(answers.orgs)
63-
config.projects = transformUserInput(answers.repositories)
64-
config.githubActions = answers.githubActions
65-
66-
const appPath = path.resolve(config.path)
67-
config.name = path.basename(appPath)
68-
config.path = appPath
69-
70-
await fs.ensureDir(appPath)
49+
const directory = (opts.directory || answers.path || '').trim()
50+
if (!directory) throw new Error('Enter a project directory.')
51+
const appPath = path.resolve(directory)
52+
await fs.mkdir(appPath, { recursive: true })
53+
if ((await fs.readdir(appPath)).length) {
54+
throw new Error(`Directory is not empty: ${appPath}`)
55+
}
7156

72-
if (config.githubActions) {
73-
createBuildAction(config)
57+
const config = {
58+
path: appPath,
59+
name: path.basename(appPath),
60+
labels: answers.defaultLabels ? undefined : transformUserInput(answers.labels),
61+
orgs: transformUserInput(answers.orgs),
62+
projects: transformUserInput(answers.repositories),
63+
githubActions: answers.githubActions
7464
}
7565

66+
if (config.githubActions) await createBuildAction(config)
7667
await createConfigFile(config)
77-
createGitIgnore(config)
68+
await fs.copyFile(path.join(__dirname, 'template', 'gitignore'), path.join(appPath, '.gitignore'))
7869

79-
await createPackageJson({
80-
cwd: appPath,
81-
name: config.name,
70+
// The generated manifest is fixed apart from the project name and our version.
71+
const manifest = {
72+
name: config.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-+/, '') || 'statusboard',
8273
description: 'A dashboard for project status',
8374
version: '1.0.0',
84-
dependencies: ['@pkgjs/statusboard'],
85-
author: null,
86-
keywords: ['statusboard', 'projects'],
87-
repository: null,
88-
type: 'commonjs',
8975
private: true,
76+
type: 'commonjs',
9077
license: 'MIT',
91-
main: 'config.js',
92-
devDependencies: null,
78+
engines: packageJson.engines,
79+
dependencies: { '@pkgjs/statusboard': `^${packageJson.version}` },
9380
scripts: {
9481
build: 'statusboard build -C ./config.js',
9582
buildsite: 'npm run clean && statusboard site -C ./config.js',
9683
buildindex: 'statusboard index -C ./config.js',
97-
clean: 'rm -rf build/css build/js'
84+
clean: 'node -e "const fs = require(\'node:fs\'); for (const dir of [\'build/css\', \'build/js\']) fs.rmSync(dir, { recursive: true, force: true })"'
9885
}
99-
})
86+
}
87+
await fs.writeFile(path.join(appPath, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' })
10088

101-
gitInit(appPath)
89+
if (!gitInit(appPath)) {
90+
console.warn('Could not create the initial Git commit. Project files are ready; check Git installation and user configuration.')
91+
}
92+
console.log(`Created Statusboard in ${appPath}. Run npm install there, set GITHUB_TOKEN, then run npm run build.`)
93+
if (config.githubActions) {
94+
console.log('Enable GitHub Pages with GitHub Actions as the source in your repository settings.')
95+
}
10296
}
10397

10498
async function createConfigFile (config) {
105-
const configFile = path.join(config.path, 'config.js')
106-
107-
const configJs = `module.exports = {${
108-
config.labels.length >= 1
109-
? `\n issueLabels: ${JSON.stringify(config.labels)},`
110-
: ''
111-
}${config.orgs.length >= 1 ? `\n orgs: ${JSON.stringify(config.orgs)},` : ''}${
112-
config.projects.length >= 1
113-
? `\n projects: ${JSON.stringify(config.projects)},`
114-
: ''
115-
}${config.githubActions ? `\n baseUrl: "/${config.name}",` : ''}
99+
const fields = {
100+
...(config.labels === undefined ? {} : { issueLabels: config.labels }),
101+
orgs: config.orgs,
102+
projects: config.projects
103+
}
104+
const content = JSON.stringify(fields, null, 2).slice(0, -2)
105+
const baseUrl = config.githubActions
106+
? `\n baseUrl: process.env.STATUSBOARD_BASE_URL ?? ${JSON.stringify(`/${config.name}`)},`
107+
: ''
108+
await fs.writeFile(path.join(config.path, 'config.js'), `'use strict'\nmodule.exports = ${content},${baseUrl}
116109
github: {
117110
token: process.env.GITHUB_TOKEN
118111
}
119112
}
120-
`
121-
122-
await fs.writeFile(configFile, configJs)
113+
`, { flag: 'wx' })
123114
}
124115

125-
function transformUserInput (input) {
126-
if (input?.length === 0) {
127-
return []
128-
}
129-
130-
return input?.split(',').map((l) => l.trim()) || []
116+
function transformUserInput (input = '') {
117+
return input.split(',').map(value => value.trim()).filter(Boolean)
131118
}
132119

133-
function createBuildAction (config) {
120+
async function createBuildAction (config) {
134121
const workflowDir = path.join(config.path, '.github', 'workflows')
135-
const templateFile = path.join(__dirname, 'template', 'build.yml')
136-
const configFile = path.join(workflowDir, 'build.yml')
137-
138-
fs.ensureDirSync(workflowDir)
139-
fs.copyFileSync(templateFile, configFile)
140-
}
141-
142-
function createGitIgnore (config) {
143-
const gitIgnore = path.join(config.path, '.gitignore')
144-
const templateFile = path.join(__dirname, 'template', '.gitignore')
145-
146-
fs.ensureFileSync(gitIgnore)
147-
fs.copyFileSync(templateFile, gitIgnore)
122+
await fs.mkdir(workflowDir, { recursive: true })
123+
await fs.copyFile(path.join(__dirname, 'template', 'build.yml'), path.join(workflowDir, 'build.yml'))
148124
}
149125

150126
module.exports = create

‎lib/commands/template/build.yml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ jobs:
2626
- name: Checkout
2727
uses: actions/checkout@v4
2828
- name: Setup Pages
29+
id: pages
2930
uses: actions/configure-pages@v5
3031
- uses: actions/setup-node@v4
3132
with:
32-
node-version: lts/*
33+
node-version: 22
3334
- uses: actions/cache@v4
3435
id: cache
3536
with:
@@ -45,10 +46,11 @@ jobs:
4546
- run: npm run build
4647
env:
4748
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49+
STATUSBOARD_BASE_URL: ${{ steps.pages.outputs.base_path }}
4850
- name: Upload artifact
4951
uses: actions/upload-pages-artifact@v3
5052
with:
5153
path: './build'
5254
- name: Deploy to GitHub Pages
5355
id: deployment
54-
uses: actions/deploy-pages@v4
56+
uses: actions/deploy-pages@v4
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ build
88

99
# local env files
1010
.env*.local
11-
.env
11+
.env
12+
13+
# local database
14+
data.db

‎lib/git.js‎

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,15 @@
1-
const { execSync } = require('child_process')
2-
const { rm } = require('fs')
3-
const { join } = require('path')
1+
'use strict'
2+
const { execFileSync } = require('node:child_process')
43

5-
function isGitRepo () {
4+
module.exports.gitInit = function gitInit (directory) {
5+
const options = { cwd: directory, stdio: 'ignore' }
66
try {
7-
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' })
7+
execFileSync('git', ['init', '-b', 'main'], options)
8+
execFileSync('git', ['add', '-A'], options)
9+
execFileSync('git', ['commit', '-m', 'Initial commit from @pkgjs/statusboard'], options)
810
return true
9-
} catch {}
10-
return false
11-
}
12-
13-
module.exports.gitInit = function gitInit (path) {
14-
let didInit = false
15-
16-
try {
17-
if (isGitRepo()) {
18-
return false
19-
}
20-
21-
execSync('git init', { stdio: 'ignore' })
22-
23-
didInit = true
24-
25-
execSync('git checkout -b main', { stdio: 'ignore' })
26-
execSync('git add -A', { stdio: 'ignore' })
27-
execSync('git commit -m "Initial commit from @pkgjs/statusboard"', {
28-
stdio: 'ignore'
29-
})
30-
31-
return true
32-
} catch (e) {
33-
if (didInit) {
34-
try {
35-
rm(join(path, '.git'), { recursive: true, force: true })
36-
} catch (_) {}
37-
}
11+
} catch {
12+
// Keep the repository and generated files if Git identity is not configured.
3813
return false
3914
}
4015
}

‎package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
"@wesleytodd/buildjs": "0.0.8",
5050
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
5151
"cptmpl": "0.0.4",
52-
"create-package-json": "^1.1.0",
5352
"es5-lit-element": "^2.2.1",
5453
"es5-lit-html": "^1.1.1",
5554
"express": "^5.2.1",

0 commit comments

Comments
 (0)