Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: parseNi etc. now return ResolvedCommand object instead of string, migrate to tinyexec #231

Merged
merged 9 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { defineBuildConfig } from 'unbuild'
import { globSync } from 'tinyglobby'

export default defineBuildConfig({
entries: globSync(['src/commands/*.ts'], { expandDirectories: false }).map(i => ({
entries: globSync(
['src/commands/*.ts'],
{ expandDirectories: false },
).map(i => ({
input: i.slice(0, -3),
name: basename(i).slice(0, -3),
})),
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
},
"devDependencies": {
"@antfu/eslint-config": "^2.27.2",
"@jsdevtools/ez-spawn": "^3.0.4",
"@posva/prompts": "^2.4.4",
"@types/fs-extra": "^11.0.4",
"@types/ini": "^4.1.1",
Expand All @@ -62,11 +61,12 @@
"fzf": "^0.5.2",
"ini": "^4.1.3",
"lint-staged": "^15.2.9",
"package-manager-detector": "^0.1.2",
"package-manager-detector": "^0.2.0",
"picocolors": "^1.0.1",
"simple-git-hooks": "^2.11.1",
"taze": "^0.16.6",
"terminal-link": "^3.0.0",
"tinyexec": "^0.3.0",
"tinyglobby": "^0.2.5",
"tsx": "^4.18.0",
"typescript": "^5.5.4",
Expand Down
15 changes: 5 additions & 10 deletions pnpm-lock.yaml

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

6 changes: 0 additions & 6 deletions src/agents.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import ini from 'ini'
import type { Agent } from './agents'
import type { Agent } from 'package-manager-detector'
import { detect } from './detect'

const customRcPath = process.env.NI_CONFIG_FILE
Expand Down
41 changes: 30 additions & 11 deletions src/detect.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,48 @@
import process from 'node:process'
import { async as ezspawn } from '@jsdevtools/ez-spawn'
import { detect as detectPM } from 'package-manager-detector'
import { x } from 'tinyexec'
import terminalLink from 'terminal-link'
import prompts from '@posva/prompts'
import { INSTALL_PAGE } from './agents'
import { INSTALL_PAGE } from 'package-manager-detector/constants'
import { cmdExists } from './utils'

export interface DetectOptions {
autoInstall?: boolean
programmatic?: boolean
cwd?: string
/**
* Should use Volta when present
*
* @see https://volta.sh/
* @default true
*/
detectVolta?: boolean
}

export async function detect({ autoInstall, programmatic, cwd }: DetectOptions = {}) {
const pmDetection = await detectPM({
const {
name,
agent,
version,
} = await detectPM({
cwd,
onUnknown: (packageManager) => {
if (!programmatic) {
console.warn('[ni] Unknown packageManager:', packageManager)
}
return undefined
},
})

const agent = pmDetection?.agent ?? null
const version = pmDetection?.version ?? null
}) || {}

// auto install
if (agent && !cmdExists(agent.split('@')[0]) && !programmatic) {
if (name && !cmdExists(name) && !programmatic) {
if (!autoInstall) {
console.warn(`[ni] Detected ${agent} but it doesn't seem to be installed.\n`)
console.warn(`[ni] Detected ${name} but it doesn't seem to be installed.\n`)

if (process.env.CI)
process.exit(1)

const link = terminalLink(agent, INSTALL_PAGE[agent])
const link = terminalLink(name, INSTALL_PAGE[name])
const { tryInstall } = await prompts({
name: 'tryInstall',
type: 'confirm',
Expand All @@ -43,7 +52,17 @@ export async function detect({ autoInstall, programmatic, cwd }: DetectOptions =
process.exit(1)
}

await ezspawn(`npm i -g ${agent.split('@')[0]}${version ? `@${version}` : ''}`, { stdio: 'inherit', cwd })
await x(
'npm',
['i', '-g', `${name}${version ? `@${version}` : ''}`],
{
nodeOptions: {
stdio: 'inherit',
cwd,
},
throwOnError: true,
},
)
}

return agent
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './agents'
export * from 'package-manager-detector/constants'
export * from 'package-manager-detector/commands'

export * from './config'
export * from './detect'
export * from './parse'
Expand Down
42 changes: 24 additions & 18 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Agent, Command } from './agents'
import { AGENTS, COMMANDS } from './agents'
import type { Agent, Command, ResolvedCommand } from 'package-manager-detector'
import { exclude } from './utils'
import type { Runner } from './runner'
import { COMMANDS, constructCommand } from '.'

export class UnsupportedCommand extends Error {
constructor({ agent, command }: { agent: Agent, command: Command }) {
Expand All @@ -13,23 +13,13 @@ export function getCommand(
agent: Agent,
command: Command,
args: string[] = [],
) {
if (!AGENTS.includes(agent))
): ResolvedCommand {
if (!COMMANDS[agent])
throw new Error(`Unsupported agent "${agent}"`)

const c = COMMANDS[agent][command]

if (typeof c === 'function')
return c(args)

if (!c)
if (!COMMANDS[agent][command])
throw new UnsupportedCommand({ agent, command })

const quote = (arg: string) => (!arg.startsWith('--') && arg.includes(' '))
? JSON.stringify(arg)
: arg

return c.replace('{0}', args.map(quote).join(' ')).trim()
return constructCommand(COMMANDS[agent][command], args)!
}

export const parseNi = <Runner>((agent, args, ctx) => {
Expand Down Expand Up @@ -58,12 +48,20 @@ export const parseNr = <Runner>((agent, args) => {
if (args.length === 0)
args.push('start')

let hasIfPresent = false
if (args.includes('--if-present')) {
args = exclude(args, '--if-present')
args[0] = `--if-present ${args[0]}`
hasIfPresent = true
}

return getCommand(agent, 'run', args)
const cmd = getCommand(agent, 'run', args)
if (!cmd)
return cmd

if (hasIfPresent)
cmd.args.splice(1, 0, '--if-present')

return cmd
})

export const parseNu = <Runner>((agent, args) => {
Expand All @@ -86,3 +84,11 @@ export const parseNlx = <Runner>((agent, args) => {
export const parseNa = <Runner>((agent, args) => {
return getCommand(agent, 'agent', args)
})

export function serializeCommand(command?: ResolvedCommand) {
if (!command)
return undefined
if (command.args.length === 0)
return command.command
return `${command.command} ${command.args.map(i => i.includes(' ') ? `"${i}"` : i).join(' ')}`
}
58 changes: 43 additions & 15 deletions src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
import { resolve } from 'node:path'
import process from 'node:process'
import prompts from '@posva/prompts'
import type { Options as EzSpawnOptions } from '@jsdevtools/ez-spawn'
import { async as ezspawn } from '@jsdevtools/ez-spawn'
import type { Options as TinyExecOptions } from 'tinyexec'
import { x } from 'tinyexec'
import c from 'picocolors'
import type { Agent, ResolvedCommand } from 'package-manager-detector'
import { AGENTS } from 'package-manager-detector'
import { version } from '../package.json'
import type { Agent } from './agents'
import { AGENTS } from './agents'
import { getDefaultAgent, getGlobalAgent } from './config'
import type { DetectOptions } from './detect'
import { detect } from './detect'
import { getVoltaPrefix, remove } from './utils'
import { cmdExists, remove } from './utils'
import { UnsupportedCommand, getCommand } from './parse'

const DEBUG_SIGN = '?'
Expand All @@ -22,7 +22,7 @@ export interface RunnerContext {
cwd?: string
}

export type Runner = (agent: Agent, args: string[], ctx?: RunnerContext) => Promise<string | undefined> | string | undefined
export type Runner = (agent: Agent, args: string[], ctx?: RunnerContext) => Promise<ResolvedCommand | undefined> | ResolvedCommand | undefined

export async function runCli(fn: Runner, options: DetectOptions & { args?: string[] } = {}) {
const {
Expand Down Expand Up @@ -74,6 +74,10 @@ export async function getCliCommand(
}

export async function run(fn: Runner, args: string[], options: DetectOptions = {}) {
const {
detectVolta = true,
} = options

const debug = args.includes(DEBUG_SIGN)
if (debug)
remove(args, DEBUG_SIGN)
Expand All @@ -85,13 +89,26 @@ export async function run(fn: Runner, args: string[], options: DetectOptions = {
}

if (args.length === 1 && (args[0]?.toLowerCase() === '-v' || args[0] === '--version')) {
const getCmd = (a: Agent) => AGENTS.includes(a) ? getCommand(a, 'agent', ['-v']) : `${a} -v`
const getV = (a: string, o: EzSpawnOptions = {}) => ezspawn(getCmd(a as Agent), o).then(e => e.stdout).then(e => e.startsWith('v') ? e : `v${e}`)
const getCmd = (a: Agent) => AGENTS.includes(a)
? getCommand(a, 'agent', ['-v'])
: { command: a, args: ['-v'] }
const xVersionOptions = {
nodeOptions: {
cwd,
},
antfu marked this conversation as resolved.
Show resolved Hide resolved
throwOnError: true,
} satisfies Partial<TinyExecOptions>
const getV = (a: string) => {
const { command, args } = getCmd(a as Agent)
return x(command, args, xVersionOptions)
.then(e => e.stdout)
.then(e => e.startsWith('v') ? e : `v${e}`)
}
const globalAgentPromise = getGlobalAgent()
const globalAgentVersionPromise = globalAgentPromise.then(getV)
const agentPromise = detect({ ...options, cwd }).then(a => a || '')
const agentVersionPromise = agentPromise.then(a => a && getV(a, { cwd }))
const nodeVersionPromise = getV('node', { cwd })
const agentVersionPromise = agentPromise.then(a => a && getV(a))
const nodeVersionPromise = getV('node')

console.log(`@antfu/ni ${c.cyan(`v${version}`)}`)
console.log(`node ${c.green(await nodeVersionPromise)}`)
Expand Down Expand Up @@ -126,19 +143,30 @@ export async function run(fn: Runner, args: string[], options: DetectOptions = {
return
}

let command = await getCliCommand(fn, args, options, cwd)
const command = await getCliCommand(fn, args, options, cwd)

if (!command)
return

const voltaPrefix = getVoltaPrefix()
if (voltaPrefix)
command = voltaPrefix.concat(' ').concat(command)
if (detectVolta && cmdExists('volta')) {
command.args = ['run', command.command, ...command.args]
command.command = 'volta'
}

if (debug) {
console.log(command)
return
}

await ezspawn(command, { stdio: 'inherit', cwd })
await x(
command.command,
command.args,
{
nodeOptions: {
stdio: 'inherit',
cwd,
},
throwOnError: true,
},
antfu marked this conversation as resolved.
Show resolved Hide resolved
)
}
7 changes: 0 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,6 @@ export function cmdExists(cmd: string) {
return which.sync(cmd, { nothrow: true }) !== null
}

export function getVoltaPrefix(): string {
// https://blog.volta.sh/2020/11/25/command-spotlight-volta-run/
const VOLTA_PREFIX = 'volta run'
const hasVoltaCommand = cmdExists('volta')
return hasVoltaCommand ? VOLTA_PREFIX : ''
}

interface TempFile {
path: string
fd: fs.FileHandle
Expand Down
6 changes: 3 additions & 3 deletions test/na/bun.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { expect, it } from 'vitest'
import { parseNa } from '../../src/commands'
import { parseNa, serializeCommand } from '../../src/commands'

const agent = 'bun'
function _(arg: string, expected: string) {
return () => {
return async () => {
expect(
parseNa(agent, arg.split(' ').filter(Boolean)),
serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),
).toBe(
expected,
)
Expand Down
Loading
Loading