Skip to content

Commit

Permalink
Merge pull request #11 from airbadge-dev/cli
Browse files Browse the repository at this point in the history
Adds airbadge CLI
  • Loading branch information
joshnuss authored May 28, 2024
2 parents b93114f + 6d86cdf commit c2e98d3
Show file tree
Hide file tree
Showing 14 changed files with 361 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test/fixtures/.env.example
11 changes: 11 additions & 0 deletions packages/cli/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"htmlWhitespaceSensitivity": "ignore",
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
3 changes: 3 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# create-airbadge

Create a new AirBadge app based on this [template](https://github.com/joshnuss/airbadge-example).
22 changes: 22 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@airbadge/cli",
"version": "0.0.1",
"type": "module",
"bin": {
"airbadge": "./src/bin.js"
},
"scripts": {
"test": "vitest"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"vitest": "^1.2.0"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"envfile": "^7.1.0",
"sade": "^1.8.1"
}
}
14 changes: 14 additions & 0 deletions packages/cli/src/bin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import sade from 'sade'
import fs from 'node:fs'
import { setupStripe } from './commands/setupStripe.js'
import pkg from '../package.json' assert { type: 'json' }

const prog = sade('airbadge').version(pkg.version)
const envPath = '.env'

prog
.command('setup stripe')
.describe('Sets up Stripe environment variables')
.action(() => setupStripe(envPath))

prog.parse(process.argv)
28 changes: 28 additions & 0 deletions packages/cli/src/bin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import path from 'node:path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

function exec(options) {
const binPath = path.join(__dirname, 'bin.js')

return spawnSync('node', [binPath, ...options])
}

test('has "setup stripe" command', async () => {
const pid = exec(['setup', 'stripe', '--help'])
console.log(pid.stderr.toString())
const stdout = pid.stdout.toString()

expect(stdout).toMatch('Sets up Stripe environment')
})

test('has "--help" option', async () => {
const pid = exec(['--help'])
const stdout = pid.stdout.toString()

expect(stdout).toMatch('Available Commands')
expect(stdout).toMatch('setup stripe')
})
44 changes: 44 additions & 0 deletions packages/cli/src/commands/setupStripe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fs from 'node:fs'
import TOML from '@iarna/toml'
import { exec, hasCommand, fail, writeEnvVar, readEnv } from '../utils.js'

export async function setupStripe(envPath) {
const cli = hasCommand('stripe')

if (!cli) {
fail("Stripe's CLI is missing\n\nTo install, follow setup instructions:\nhttps://docs.stripe.com/cli")
return
}

let env = {}
const exists = fs.existsSync(envPath)

if (exists) {
env = await readEnv(envPath)
}

if (env.SECRET_STRIPE_KEY) {
fail(`SECRET_STRIPE_KEY is already configured in ${envPath}`)
return
}

let result = exec('stripe', ['config', '--list'])
let data = TOML.parse(result)

if (!data.default) {
exec('stripe', ['login'])
result = exec('stripe', ['config', '--list'])
data = TOML.parse(result)
}

const webhookSecret = exec('stripe', ['listen', '--print-secret'])

await writeEnvVar(envPath, 'SECRET_STRIPE_KEY', data.default.test_mode_api_key)
await writeEnvVar(envPath, 'STRIPE_WEBHOOK_SECRET', webhookSecret)

if (!env.DOMAIN) {
await writeEnvVar(envPath, 'DOMAIN', 'http://localhost:5173')
}

console.log(`${exists ? 'Updated' : 'Created'} ${envPath}`)
}
92 changes: 92 additions & 0 deletions packages/cli/src/commands/setupStripe.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { setupStripe } from './setupStripe'
import { hasCommand, fail, exec, readEnv, writeEnvVar } from '../utils'
import fs from 'node:fs'

vi.mock('node:fs', () => {
return {
default: {
existsSync: vi.fn()
}
}
})

vi.mock('../utils', () => {
return {
hasCommand: vi.fn(),
fail: vi.fn(),
exec: vi.fn(),
readEnv: vi.fn(),
writeEnvVar: vi.fn()
}
})

beforeEach(() => vi.spyOn(console, 'log'))
afterEach(() => vi.resetAllMocks())

describe('setupStripe', () => {
test('when stripe cli is missing, errors', async () => {
hasCommand.mockReturnValue(false)

await setupStripe('.env')

expect(hasCommand).toBeCalledWith('stripe')
expect(fail).toBeCalledWith(expect.stringMatching(/setup instructions/gm))
})

describe('when stripe cli exists', () => {
beforeEach(() => hasCommand.mockReturnValue(true))

test("when .env doesn't exist, writes env vars", async () => {
fs.existsSync.mockReturnValue(false)
exec.mockImplementationOnce(() => '[default]\ntest_mode_api_key = "sk_test_1234"')
exec.mockImplementationOnce(() => 'whsec_1234')
writeEnvVar.mockResolvedValue()

await setupStripe('.env')

expect(exec).toBeCalledWith('stripe', ['config', '--list'])
expect(writeEnvVar).toBeCalledWith('.env', 'SECRET_STRIPE_KEY', 'sk_test_1234')
expect(writeEnvVar).toBeCalledWith('.env', 'STRIPE_WEBHOOK_SECRET', 'whsec_1234')
expect(writeEnvVar).toBeCalledWith('.env', 'DOMAIN', 'http://localhost:5173')
expect(console.log).toBeCalledWith('Created .env')
})

test("when .env exists, writes env vars", async () => {
fs.existsSync.mockReturnValue(true)
exec.mockImplementationOnce(() => '[default]\ntest_mode_api_key = "sk_test_1234"')
exec.mockImplementationOnce(() => 'whsec_1234')
readEnv.mockResolvedValue({})
writeEnvVar.mockResolvedValue()

await setupStripe('.env')

expect(exec).toBeCalledWith('stripe', ['config', '--list'])
expect(exec).toBeCalledWith('stripe', ['listen', '--print-secret'])
expect(writeEnvVar).toBeCalledWith('.env', 'SECRET_STRIPE_KEY', 'sk_test_1234')
expect(writeEnvVar).toBeCalledWith('.env', 'STRIPE_WEBHOOK_SECRET', 'whsec_1234')
expect(writeEnvVar).toBeCalledWith('.env', 'DOMAIN', 'http://localhost:5173')
expect(console.log).toBeCalledWith('Updated .env')
})

test("when not logged in, logs in", async () => {
fs.existsSync.mockReturnValue(true)
exec.mockImplementationOnce(() => '')
exec.mockImplementationOnce(() => '')
exec.mockImplementationOnce(() => '[default]\ntest_mode_api_key = "sk_test_1234"')
exec.mockImplementationOnce(() => 'whsec_1234')
readEnv.mockResolvedValue({})
writeEnvVar.mockResolvedValue()

await setupStripe('.env')

expect(exec).toBeCalledTimes(4)
expect(exec).toBeCalledWith('stripe', ['config', '--list'])
expect(exec).toBeCalledWith('stripe', ['login'])
expect(exec).toBeCalledWith('stripe', ['listen', '--print-secret'])
expect(writeEnvVar).toBeCalledWith('.env', 'SECRET_STRIPE_KEY', 'sk_test_1234')
expect(writeEnvVar).toBeCalledWith('.env', 'STRIPE_WEBHOOK_SECRET', 'whsec_1234')
expect(writeEnvVar).toBeCalledWith('.env', 'DOMAIN', 'http://localhost:5173')
expect(console.log).toBeCalledWith('Updated .env')
})
})
})
38 changes: 38 additions & 0 deletions packages/cli/src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { spawnSync } from 'node:child_process'
import * as envfile from 'envfile'
import fs from 'fs/promises'

export function hasCommand(cmd) {
const pid = spawnSync('which', [cmd])

return pid.status === 0
}

export function exec(cmd, params = []) {
const result = spawnSync(cmd, params)

return result.stdout.toString().trim()
}

export function fail(message) {
console.error(message)
process.exit(1)
}

export async function writeEnvVar(path, varName, value) {
const regex = new RegExp(`^${varName}=.*$`, 'gm')
let data = await fs.readFile(path, 'utf8')

if (data.match(regex)) {
data = data.replace(regex, `${varName}=${value}`)
} else {
data += `\n${varName}=${value}\n`
}

await fs.writeFile(path, data)
}

export async function readEnv(path) {
const data = await fs.readFile(path, 'utf8')
return envfile.parse(data)
}
72 changes: 72 additions & 0 deletions packages/cli/src/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { hasCommand, exec, fail, readEnv, writeEnvVar } from './utils'
import path from 'node:path'
import fs from 'fs/promises'

const templatePath = path.resolve('./test/fixtures/.env.template')
const envPath = path.resolve('./test/fixtures/.env.example')

afterEach(() => vi.resetAllMocks())
beforeEach(async () => {
await fs.cp(templatePath, envPath)

console.error = vi.fn()
process.exit = vi.fn()
})

describe('hasCommand', () => {
test('when exists, returns true', () => {
const result = hasCommand('ls')

expect(result).toBe(true)
})

test('when missing, returns false', () => {
const result = hasCommand('non-existing-cmd')

expect(result).toBe(false)
})
})

describe('exec', () => {
test('returns stdout', () => {
const result = exec('ls')
expect(result).toMatch(/^package.json$/gm)
})

test('takes parameters', () => {
const result = exec('ls', ['-al'])
expect(result).toMatch(/package.json$/gm)
})
})

test('fail exits and prints error message', () => {
fail('oops')

expect(console.error).toHaveBeenCalledWith('oops')
expect(process.exit).toHaveBeenCalledWith(1)
})

test('readEnv', async () => {
const env = await readEnv(envPath)

expect(env.FOO).toBe("1")
expect(env.BAR).toBe("2")
})

describe('writeEnvVar', () => {
test('replaces existing var', async () => {
await writeEnvVar(envPath, 'FOO', 'updated-value')

const data = await fs.readFile(envPath, 'utf8')

expect(data).toMatch(/^FOO=updated-value$/gm)
})

test('adds new var', async () => {
await writeEnvVar(envPath, 'BAZ', 'new-value')

const data = await fs.readFile(envPath, 'utf8')

expect(data).toMatch(/^BAZ=new-value$/gm)
})
})
2 changes: 2 additions & 0 deletions packages/cli/test/fixtures/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FOO=1
BAR=2
8 changes: 8 additions & 0 deletions packages/cli/vitest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
include: ['src/**/*.{test,spec}.{js,ts}']
}
})
3 changes: 0 additions & 3 deletions packages/create/src/bin.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
test('calls degit', () => {

})
26 changes: 26 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c2e98d3

Please sign in to comment.