Skip to content

Commit

Permalink
feat(cli): implement generic sub-commands (+ policy) (prisma#26134)
Browse files Browse the repository at this point in the history
  • Loading branch information
millsp authored Jan 21, 2025
1 parent e5acfdc commit 049753e
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"prisma": "build/index.js"
},
"devDependencies": {
"@antfu/ni": "0.21.12",
"@inquirer/prompts": "5.0.5",
"@prisma/client": "workspace:*",
"@prisma/debug": "workspace:*",
Expand Down
48 changes: 48 additions & 0 deletions packages/cli/src/SubCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { getCommand } from '@antfu/ni'
import type { Command } from '@prisma/internals'
import { command } from 'execa'
import { existsSync } from 'fs'
import { tmpdir } from 'os'

/**
* Sub-CLIs that are installed on demand need to implement this interface
*/
type Runnable = {
run: (args: string[]) => Promise<void>
}

/**
* Generic SubCommand that installs a package on demand and runs it
*/
export class SubCommand implements Command {
pkg: string

constructor(pkg: string) {
this.pkg = pkg
}

async parse(argv: string[]): Promise<string | Error> {
// we accept forcing a version with @, eg. prisma policy @1.0.0 --help
const [version, ...args] = argv[0]?.startsWith('@') ? argv : ['@latest', ...argv]
const pkg = `${this.pkg}${version}`

// when version defaults to @latest, we cache it for the current day only
const dayMillis = new Date().setHours(0, 0, 0, 0)
const cacheKey = version === '@latest' ? `-${dayMillis}` : ''
const prefix = `${tmpdir()}/${pkg}${cacheKey}`

// if the package is not installed yet, we install it otherwise we skip
if (existsSync(prefix) === false) {
const installCmd = getCommand('npm', 'install', [pkg, '--no-save', '--prefix', prefix])
await command(installCmd, { stdout: 'ignore', stderr: 'inherit', env: process.env })
}

// load the module and run it via the Runnable interface
const module: Runnable = await import(`${prefix}/node_modules/${this.pkg}`)
await module.run(args)

return ''
}

public help() {}
}
107 changes: 107 additions & 0 deletions packages/cli/src/__tests__/commands/SubCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as ni from '@antfu/ni'
import * as execa from 'execa'
import { rm } from 'fs/promises'
import { copy } from 'fs-extra'
import { tmpdir } from 'os'
import { join } from 'path'

import { SubCommand } from '../../SubCommand'

jest.mock('@antfu/ni')
jest.mock('execa')

jest.useFakeTimers().setSystemTime(new Date('2025-01-01'))

const getDayMillis = () => new Date().setHours(0, 0, 0, 0)

beforeEach(async () => {
await rm(join(tmpdir(), `[email protected]`), { recursive: true, force: true })
await rm(join(tmpdir(), `sub-command@latest-${getDayMillis()}`), { recursive: true, force: true })
})

test('@<version>', async () => {
const cmd = new SubCommand('sub-command')
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {})

const copySrc = join(__dirname, '..', 'fixtures', 'sub-command')
const copyDest = join(tmpdir(), `[email protected]`)
await copy(copySrc, copyDest, { recursive: true })

await cmd.parse(['@0.0.0', '--help'])

expect(consoleLogSpy.mock.calls).toMatchInlineSnapshot(`
[
[
"sub-command",
[
[
"--help",
],
],
],
]
`)

consoleLogSpy.mockRestore()
})

test('@latest', async () => {
const cmd = new SubCommand('sub-command')
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {})

const copySrc = join(__dirname, '..', 'fixtures', 'sub-command')
const copyDest = join(tmpdir(), `sub-command@latest-${getDayMillis()}`)
await copy(copySrc, copyDest, { recursive: true })

await cmd.parse(['--help'])

expect(consoleLogSpy.mock.calls).toMatchInlineSnapshot(`
[
[
"sub-command",
[
[
"--help",
],
],
],
]
`)

consoleLogSpy.mockRestore()
})

test('autoinstall', async () => {
const cmd = new SubCommand('sub-command')
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {})

const copySrc = join(__dirname, '..', 'fixtures', 'sub-command')
const copyDest = join(tmpdir(), '[email protected]')

jest.mocked(ni.getCommand).mockReturnValue('npm install sub-command --no-save --prefix /tmp/[email protected]')
// eslint-disable-next-line @typescript-eslint/unbound-method
jest.mocked(execa.command).mockImplementation((async () => {
await copy(copySrc, copyDest, { recursive: true })
}) as () => any)

await cmd.parse(['@0.0.0', '--help'])

expect(consoleLogSpy.mock.calls).toMatchInlineSnapshot(`
[
[
"sub-command",
[
[
"--help",
],
],
],
]
`)

// eslint-disable-next-line @typescript-eslint/unbound-method
expect(execa.command).toHaveBeenCalled()
expect(ni.getCommand).toHaveBeenCalled()

consoleLogSpy.mockRestore()
})

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

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

4 changes: 3 additions & 1 deletion packages/cli/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { Platform } from './platform/_Platform'
prisma:cli - /Users/j42/Dev/prisma-meow/node_modules/.pnpm/@[email protected]/node_modules/@prisma/studio-pcw/dist/index.js
*/
import { Studio } from './Studio'
import { SubCommand } from './SubCommand'
import { Telemetry } from './Telemetry'
import { redactCommandArray, runCheckpointClientCheck } from './utils/checkpoint'
import { detectPrisma1 } from './utils/detectPrisma1'
Expand All @@ -46,7 +47,6 @@ import { Version } from './Version'

const debug = Debug('prisma:cli:bin')

// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment
const packageJson = require('../package.json')

const commandArray = process.argv.slice(2)
Expand Down Expand Up @@ -86,6 +86,8 @@ async function main(): Promise<number> {
{
init: Init.new(),
platform: Platform.$.new({
policy: new SubCommand('@prisma/cli-policy'),

workspace: Platform.Workspace.$.new({
show: Platform.Workspace.Show.new(),
}),
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/platform/$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export class $ implements Command {
// ['serviceToken', 'Manage service tokens'],
['accelerate', 'Manage Prisma Accelerate'],
['pulse', 'Manage Prisma Pulse'],
// TODO: undo comment before EA
// ['policy', 'Manage Prisma Policy'],
],
options: [
['--early-access', '', 'Enable early access features'],
Expand Down
4 changes: 4 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 049753e

Please sign in to comment.