diff --git a/.changeset/good-sloths-approve.md b/.changeset/good-sloths-approve.md new file mode 100644 index 0000000000..554c53a777 --- /dev/null +++ b/.changeset/good-sloths-approve.md @@ -0,0 +1,5 @@ +--- +"pnpm": major +--- + +A new command added for printing completion code to the console: `pnpm completion [shell]`. The old command that modified the user's shell dotfiles has been removed [#3083](https://github.com/pnpm/pnpm/issues/3083). diff --git a/.changeset/ninety-dolls-shave.md b/.changeset/ninety-dolls-shave.md new file mode 100644 index 0000000000..9df04e059e --- /dev/null +++ b/.changeset/ninety-dolls-shave.md @@ -0,0 +1,5 @@ +--- +"pnpm": minor +--- + +PowerShell completion support added [#7597](https://github.com/pnpm/pnpm/pull/7597). diff --git a/.changeset/thirty-tigers-doubt.md b/.changeset/thirty-tigers-doubt.md new file mode 100644 index 0000000000..9142054d27 --- /dev/null +++ b/.changeset/thirty-tigers-doubt.md @@ -0,0 +1,5 @@ +--- +"@pnpm/plugin-commands-completion": major +--- + +Initial release. diff --git a/.meta-updater/src/index.ts b/.meta-updater/src/index.ts index 4352182a30..acb7cf73b6 100644 --- a/.meta-updater/src/index.ts +++ b/.meta-updater/src/index.ts @@ -201,7 +201,7 @@ async function updateManifest (workspaceDir: string, manifest: ProjectManifest, } scripts.compile += ' && rimraf dist bin/nodes && pnpm run bundle \ && shx cp -r node-gyp-bin dist/node-gyp-bin \ -&& shx cp -r node_modules/@pnpm/tabtab/lib/scripts dist/scripts \ +&& shx cp -r node_modules/@pnpm/tabtab/lib/templates dist/templates \ && shx cp -r node_modules/ps-list/vendor dist/vendor \ && shx cp pnpmrc dist/pnpmrc' } else { diff --git a/__typings__/local.d.ts b/__typings__/local.d.ts index 715c2040b8..8ea41a1c8d 100644 --- a/__typings__/local.d.ts +++ b/__typings__/local.d.ts @@ -1,9 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -declare module '@pnpm/tabtab' { - const anything: any - export = anything -} - declare module 'hyperdrive-schemas' { const anything: any export = anything diff --git a/cli/command/package.json b/cli/command/package.json index 77542d19af..1bb5e5890e 100644 --- a/cli/command/package.json +++ b/cli/command/package.json @@ -28,6 +28,9 @@ }, "homepage": "https://github.com/pnpm/pnpm/blob/main/cli/command#readme", "funding": "https://opencollective.com/pnpm", + "dependencies": { + "@pnpm/tabtab": "^0.5.2" + }, "devDependencies": { "@pnpm/command": "workspace:*" }, diff --git a/cli/command/src/index.ts b/cli/command/src/index.ts index 351072e788..df5db2f1d4 100644 --- a/cli/command/src/index.ts +++ b/cli/command/src/index.ts @@ -1,6 +1,6 @@ -export interface Completion { name: string, description?: string } +import { type CompletionItem } from '@pnpm/tabtab' export type CompletionFunc = ( options: Record, params: string[] -) => Promise +) => Promise diff --git a/completion/plugin-commands-completion/jest.config.js b/completion/plugin-commands-completion/jest.config.js new file mode 100644 index 0000000000..5a65d5f52d --- /dev/null +++ b/completion/plugin-commands-completion/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest-with-registry.config.js') diff --git a/completion/plugin-commands-completion/package.json b/completion/plugin-commands-completion/package.json new file mode 100644 index 0000000000..d4664b79b3 --- /dev/null +++ b/completion/plugin-commands-completion/package.json @@ -0,0 +1,52 @@ +{ + "name": "@pnpm/plugin-commands-completion", + "version": "0.0.0", + "description": "Commands for shell completions", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "files": [ + "lib", + "!*.map" + ], + "engines": { + "node": ">=18.12" + }, + "scripts": { + "start": "tsc --watch", + "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"", + "_test": "jest", + "test": "pnpm run compile && pnpm run _test", + "prepublishOnly": "pnpm run compile", + "compile": "tsc --build && pnpm run lint --fix" + }, + "repository": "https://github.com/pnpm/pnpm/blob/main/completion/plugin-commands-completion", + "license": "MIT", + "bugs": { + "url": "https://github.com/pnpm/pnpm/issues" + }, + "devDependencies": { + "@pnpm/plugin-commands-completion": "workspace:*", + "@types/ramda": "0.28.20" + }, + "dependencies": { + "@pnpm/cli-utils": "workspace:^", + "@pnpm/command": "workspace:^", + "@pnpm/error": "workspace:^", + "@pnpm/find-workspace-dir": "workspace:^", + "@pnpm/nopt": "^0.2.1", + "@pnpm/parse-cli-args": "workspace:^", + "@pnpm/tabtab": "^0.5.2", + "@pnpm/workspace.find-packages": "workspace:^", + "ramda": "npm:@pnpm/ramda@0.28.1", + "render-help": "^1.0.3", + "split-cmd": "^1.0.1" + }, + "funding": "https://opencollective.com/pnpm", + "exports": { + ".": "./lib/index.js" + }, + "keywords": [ + "pnpm9" + ], + "homepage": "https://github.com/pnpm/pnpm/blob/main/completion/plugin-commands-completion#readme" +} diff --git a/pnpm/test/complete.test.ts b/completion/plugin-commands-completion/src/complete.test.ts similarity index 95% rename from pnpm/test/complete.test.ts rename to completion/plugin-commands-completion/src/complete.test.ts index c058b7d2ab..31a37babb0 100644 --- a/pnpm/test/complete.test.ts +++ b/completion/plugin-commands-completion/src/complete.test.ts @@ -1,4 +1,4 @@ -import { complete } from '../src/cmd/complete' +import { complete } from './complete' test('complete an option value', async () => { const completions = await complete( @@ -12,6 +12,7 @@ test('complete an option value', async () => { initialCompletion: () => [], shorthandsByCommandName: {}, universalOptionsTypes: {}, + universalShorthands: {}, }, { cmd: 'install', @@ -42,6 +43,7 @@ test('complete a command', async () => { universalOptionsTypes: { filter: String, }, + universalShorthands: {}, } expect( await complete(ctx, @@ -108,6 +110,7 @@ test('if command completion fails, return empty array', async () => { universalOptionsTypes: { filter: String, }, + universalShorthands: {}, }, { cmd: 'run', @@ -134,6 +137,7 @@ test('initial completion', async () => { universalOptionsTypes: { filter: String, }, + universalShorthands: {}, } expect( await complete(ctx, @@ -193,6 +197,7 @@ test('suggest no completions for after --version', async () => { ], shorthandsByCommandName: {}, universalOptionsTypes: {}, + universalShorthands: {}, }, { cmd: null, diff --git a/pnpm/src/cmd/complete.ts b/completion/plugin-commands-completion/src/complete.ts similarity index 83% rename from pnpm/src/cmd/complete.ts rename to completion/plugin-commands-completion/src/complete.ts index 86e91221a7..5edcc26f08 100644 --- a/pnpm/src/cmd/complete.ts +++ b/completion/plugin-commands-completion/src/complete.ts @@ -1,17 +1,18 @@ -import { type Completion, type CompletionFunc } from '@pnpm/command' +import { type CompletionItem } from '@pnpm/tabtab' +import { type CompletionFunc } from '@pnpm/command' import { findWorkspaceDir } from '@pnpm/find-workspace-dir' import { findWorkspacePackages } from '@pnpm/workspace.find-packages' -import { getOptionCompletions } from '../getOptionType' -import { optionTypesToCompletions } from '../optionTypesToCompletions' -import { shorthands as universalShorthands } from '../shorthands' +import { getOptionCompletions } from './getOptionType' +import { optionTypesToCompletions } from './optionTypesToCompletions' export async function complete ( ctx: { cliOptionsTypesByCommandName: Record Record> completionByCommandName: Record - initialCompletion: () => Completion[] + initialCompletion: () => CompletionItem[] shorthandsByCommandName: Record> universalOptionsTypes: Record + universalShorthands: Record }, input: { params: string[] @@ -20,7 +21,7 @@ export async function complete ( lastOption: string | null options: Record } -) { +): Promise { if (input.options.version) return [] const optionTypes = { ...ctx.universalOptionsTypes, @@ -39,13 +40,13 @@ export async function complete ( }, }) return allProjects - .filter(({ manifest }) => manifest.name) .map(({ manifest }) => ({ name: manifest.name })) + .filter((item): item is CompletionItem => !!item.name) } else if (input.lastOption) { const optionCompletions = getOptionCompletions( optionTypes as any, // eslint-disable-line { - ...universalShorthands, + ...ctx.universalShorthands, ...(input.cmd ? ctx.shorthandsByCommandName[input.cmd] : {}), }, input.lastOption @@ -55,7 +56,7 @@ export async function complete ( } } } - let completions: Completion[] = [] + let completions: CompletionItem[] = [] if (input.currentTypedWordType !== 'option') { if (!input.cmd || input.currentTypedWordType === 'value' && !ctx.completionByCommandName[input.cmd]) { completions = ctx.initialCompletion() diff --git a/pnpm/src/cmd/completion.ts b/completion/plugin-commands-completion/src/completionServer.ts similarity index 67% rename from pnpm/src/cmd/completion.ts rename to completion/plugin-commands-completion/src/completionServer.ts index 7d765809e8..ac4dd70208 100644 --- a/pnpm/src/cmd/completion.ts +++ b/completion/plugin-commands-completion/src/completionServer.ts @@ -1,23 +1,28 @@ -import { type Completion, type CompletionFunc } from '@pnpm/command' +import { type CompletionItem, getShellFromEnv } from '@pnpm/tabtab' +import { type CompletionFunc } from '@pnpm/command' import { split as splitCmd } from 'split-cmd' import tabtab from '@pnpm/tabtab' import { currentTypedWordType, getLastOption, -} from '../getOptionType' -import { parseCliArgs } from '../parseCliArgs' +} from './getOptionType' +import { type ParsedCliArgs } from '@pnpm/parse-cli-args' import { complete } from './complete' -export function createCompletion ( +export function createCompletionServer ( opts: { cliOptionsTypesByCommandName: Record Record> completionByCommandName: Record - initialCompletion: () => Completion[] + initialCompletion: () => CompletionItem[] shorthandsByCommandName: Record> + parseCliArgs: (args: string[]) => Promise universalOptionsTypes: Record + universalShorthands: Record } ) { return async () => { + const shell = getShellFromEnv(process.env) + const env = tabtab.parseEnv(process.env) if (!env.complete) return @@ -27,8 +32,8 @@ export function createCompletion ( const inputArgv = splitCmd(finishedArgv).slice(1) // We cannot autocomplete what a user types after "pnpm test --" if (inputArgv.includes('--')) return - const { params, options, cmd } = await parseCliArgs(inputArgv) - return tabtab.log( + const { params, options, cmd } = await opts.parseCliArgs(inputArgv) + tabtab.log( await complete( opts, { @@ -38,7 +43,8 @@ export function createCompletion ( options, params, } - ) + ), + shell ) } } diff --git a/completion/plugin-commands-completion/src/generateCompletion.ts b/completion/plugin-commands-completion/src/generateCompletion.ts new file mode 100644 index 0000000000..934093c222 --- /dev/null +++ b/completion/plugin-commands-completion/src/generateCompletion.ts @@ -0,0 +1,34 @@ +import renderHelp from 'render-help' +import { docsUrl } from '@pnpm/cli-utils' +import { getCompletionScript, SUPPORTED_SHELLS } from '@pnpm/tabtab' +import { getShellFromParams } from './getShell' + +export const commandNames = ['completion'] + +export const rcOptionsTypes = () => ({}) + +export const cliOptionsTypes = () => ({}) + +export function help () { + return renderHelp({ + description: 'Print shell completion code to stdout', + url: docsUrl('completion'), + usages: SUPPORTED_SHELLS.map(shell => `pnpm completion ${shell}`), + }) +} + +export interface Context { + readonly log: (output: string) => void +} + +export function createCompletionGenerator (ctx: Context) { + return async function handler (_opts: unknown, params: string[]): Promise { + const shell = getShellFromParams(params) + const output = await getCompletionScript({ name: 'pnpm', completer: 'pnpm', shell }) + ctx.log(output) + } +} + +export const handler = createCompletionGenerator({ + log: console.log, +}) diff --git a/pnpm/test/getOptionType.test.ts b/completion/plugin-commands-completion/src/getOptionType.test.ts similarity index 98% rename from pnpm/test/getOptionType.test.ts rename to completion/plugin-commands-completion/src/getOptionType.test.ts index 0934788169..c362b65733 100644 --- a/pnpm/test/getOptionType.test.ts +++ b/completion/plugin-commands-completion/src/getOptionType.test.ts @@ -2,7 +2,7 @@ import { currentTypedWordType, getLastOption, getOptionCompletions, -} from '../src/getOptionType' +} from './getOptionType' const TYPES = { color: ['red', 'blue', Array], diff --git a/pnpm/src/getOptionType.ts b/completion/plugin-commands-completion/src/getOptionType.ts similarity index 100% rename from pnpm/src/getOptionType.ts rename to completion/plugin-commands-completion/src/getOptionType.ts diff --git a/completion/plugin-commands-completion/src/getShell.test.ts b/completion/plugin-commands-completion/src/getShell.test.ts new file mode 100644 index 0000000000..70236bfd3c --- /dev/null +++ b/completion/plugin-commands-completion/src/getShell.test.ts @@ -0,0 +1,37 @@ +import { getShellFromString, getShellFromParams } from './getShell' + +test('getShellFromString errors on undefined', () => { + expect(() => getShellFromString()).toThrow('`pnpm completion` requires a shell name') +}) + +test('getShellFromString errors on empty input', () => { + expect(() => getShellFromString('')).toThrow('`pnpm completion` requires a shell name') +}) + +test('getShellFromString errors on white space', () => { + expect(() => getShellFromString(' ')).toThrow('`pnpm completion` requires a shell name') +}) + +test('getShellFromString errors on unsupported shell', () => { + expect(() => getShellFromString('weird-shell-nobody-uses')).toThrow("'weird-shell-nobody-uses' is not supported") +}) + +test('getShellFromString returns supported shell as-is', () => { + expect(getShellFromString('bash')).toBe('bash') +}) + +test('getShellFromString trims whitespaces on support shell', () => { + expect(getShellFromString(' bash\n')).toBe('bash') +}) + +test('getShellFromParams errors on empty input', () => { + expect(() => getShellFromParams([])).toThrow('`pnpm completion` requires a shell name') +}) + +test('getShellFromParams errors on redundant parameters', () => { + expect(() => getShellFromParams(['bash', 'zsh', 'fish', 'pwsh'])).toThrow('The 3 parameters after shell is not necessary') +}) + +test('getShellFromParams returns supported shell', () => { + expect(getShellFromParams(['bash'])).toBe('bash') +}) diff --git a/completion/plugin-commands-completion/src/getShell.ts b/completion/plugin-commands-completion/src/getShell.ts new file mode 100644 index 0000000000..ee354f562b --- /dev/null +++ b/completion/plugin-commands-completion/src/getShell.ts @@ -0,0 +1,28 @@ +import { PnpmError } from '@pnpm/error' +import { isShellSupported, SUPPORTED_SHELLS, type SupportedShell } from '@pnpm/tabtab' + +export function getShellFromString (shell?: string): SupportedShell { + shell = shell?.trim() + + if (!shell) { + throw new PnpmError('MISSING_SHELL_NAME', '`pnpm completion` requires a shell name') + } + + if (!isShellSupported(shell)) { + throw new PnpmError('UNSUPPORTED_SHELL', `'${shell}' is not supported`, { + hint: `Supported shells are: ${SUPPORTED_SHELLS.join(', ')}`, + }) + } + + return shell +} + +export function getShellFromParams (params: string[]): SupportedShell { + const [shell, ...rest] = params + + if (rest.length) { + throw new PnpmError('REDUNDANT_PARAMETERS', `The ${rest.length} parameters after shell is not necessary`) + } + + return getShellFromString(shell) +} diff --git a/completion/plugin-commands-completion/src/index.ts b/completion/plugin-commands-completion/src/index.ts new file mode 100644 index 0000000000..c278ea638d --- /dev/null +++ b/completion/plugin-commands-completion/src/index.ts @@ -0,0 +1,2 @@ +export * as generateCompletion from './generateCompletion' +export { createCompletionServer } from './completionServer' diff --git a/completion/plugin-commands-completion/src/optionTypesToCompletions.test.ts b/completion/plugin-commands-completion/src/optionTypesToCompletions.test.ts new file mode 100644 index 0000000000..4e75eb86a4 --- /dev/null +++ b/completion/plugin-commands-completion/src/optionTypesToCompletions.test.ts @@ -0,0 +1,14 @@ +import { optionTypesToCompletions } from './optionTypesToCompletions' + +test('optionTypesToCompletions', () => { + expect(optionTypesToCompletions({ + number: Number, + string: String, + boolean: Boolean, + })).toStrictEqual([ + { name: '--number' }, + { name: '--string' }, + { name: '--boolean' }, + { name: '--no-boolean' }, + ]) +}) diff --git a/pnpm/src/optionTypesToCompletions.ts b/completion/plugin-commands-completion/src/optionTypesToCompletions.ts similarity index 79% rename from pnpm/src/optionTypesToCompletions.ts rename to completion/plugin-commands-completion/src/optionTypesToCompletions.ts index 3b611d6fd0..7b5f624005 100644 --- a/pnpm/src/optionTypesToCompletions.ts +++ b/completion/plugin-commands-completion/src/optionTypesToCompletions.ts @@ -1,7 +1,7 @@ -import { type Completion } from '@pnpm/command' +import { type CompletionItem } from '@pnpm/tabtab' export function optionTypesToCompletions (optionTypes: Record) { - const completions: Completion[] = [] + const completions: CompletionItem[] = [] for (const [name, typeObj] of Object.entries(optionTypes)) { if (typeObj === Boolean) { completions.push({ name: `--${name}` }) diff --git a/completion/plugin-commands-completion/test/generateCompletion.ts b/completion/plugin-commands-completion/test/generateCompletion.ts new file mode 100644 index 0000000000..64f37ae7ea --- /dev/null +++ b/completion/plugin-commands-completion/test/generateCompletion.ts @@ -0,0 +1,48 @@ +import { SUPPORTED_SHELLS } from '@pnpm/tabtab' +import { generateCompletion } from '@pnpm/plugin-commands-completion' + +function createHandler () { + const log = jest.fn() + const handler = generateCompletion.createCompletionGenerator({ log }) + return { log, handler } +} + +test('pnpm completion requires the shell argument', async () => { + const { log, handler } = createHandler() + const promise = handler({}, []) + await expect(promise).rejects.toMatchObject({ + code: 'ERR_PNPM_MISSING_SHELL_NAME', + message: '`pnpm completion` requires a shell name', + }) + expect(log).not.toHaveBeenCalled() +}) + +test('pnpm completion errors on unsupported shell', async () => { + const { log, handler } = createHandler() + const promise = handler({}, ['weird-shell-nobody-uses']) + await expect(promise).rejects.toMatchObject({ + code: 'ERR_PNPM_UNSUPPORTED_SHELL', + message: '\'weird-shell-nobody-uses\' is not supported', + }) + expect(log).not.toHaveBeenCalled() +}) + +test('pnpm completion errors on redundant parameters', async () => { + const { log, handler } = createHandler() + const promise = handler({}, ['bash', 'fish', 'pwsh', 'zsh']) + await expect(promise).rejects.toMatchObject({ + code: 'ERR_PNPM_REDUNDANT_PARAMETERS', + message: 'The 3 parameters after shell is not necessary', + }) + expect(log).not.toHaveBeenCalled() +}) + +for (const shell of SUPPORTED_SHELLS) { + test(`pnpm completion ${shell}`, async () => { + const { log, handler } = createHandler() + await handler({}, [shell]) + expect(log).toHaveBeenCalledWith(expect.stringContaining('###-begin-pnpm-completion-###')) + expect(log).toHaveBeenCalledWith(expect.stringContaining('###-end-pnpm-completion-###')) + expect(log).toHaveBeenCalledTimes(1) + }) +} diff --git a/completion/plugin-commands-completion/tsconfig.json b/completion/plugin-commands-completion/tsconfig.json new file mode 100644 index 0000000000..d2b9b708d8 --- /dev/null +++ b/completion/plugin-commands-completion/tsconfig.json @@ -0,0 +1,32 @@ +{ + "extends": "@pnpm/tsconfig", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts", + "../../__typings__/**/*.d.ts" + ], + "references": [ + { + "path": "../../cli/cli-utils" + }, + { + "path": "../../cli/command" + }, + { + "path": "../../cli/parse-cli-args" + }, + { + "path": "../../packages/error" + }, + { + "path": "../../workspace/find-packages" + }, + { + "path": "../../workspace/find-workspace-dir" + } + ], + "composite": true +} diff --git a/completion/plugin-commands-completion/tsconfig.lint.json b/completion/plugin-commands-completion/tsconfig.lint.json new file mode 100644 index 0000000000..1bbe711971 --- /dev/null +++ b/completion/plugin-commands-completion/tsconfig.lint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src/**/*.ts", + "test/**/*.ts", + "../../__typings__/**/*.d.ts" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cca3984ab0..4043da0b00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -430,6 +430,10 @@ importers: version: 0.28.20 cli/command: + dependencies: + '@pnpm/tabtab': + specifier: ^0.5.2 + version: 0.5.2 devDependencies: '@pnpm/command': specifier: workspace:* @@ -554,6 +558,49 @@ importers: specifier: ^1.0.1 version: 1.0.1 + completion/plugin-commands-completion: + dependencies: + '@pnpm/cli-utils': + specifier: workspace:^ + version: link:../../cli/cli-utils + '@pnpm/command': + specifier: workspace:^ + version: link:../../cli/command + '@pnpm/error': + specifier: workspace:^ + version: link:../../packages/error + '@pnpm/find-workspace-dir': + specifier: workspace:^ + version: link:../../workspace/find-workspace-dir + '@pnpm/nopt': + specifier: ^0.2.1 + version: 0.2.1 + '@pnpm/parse-cli-args': + specifier: workspace:^ + version: link:../../cli/parse-cli-args + '@pnpm/tabtab': + specifier: ^0.5.2 + version: 0.5.2 + '@pnpm/workspace.find-packages': + specifier: workspace:^ + version: link:../../workspace/find-packages + ramda: + specifier: npm:@pnpm/ramda@0.28.1 + version: /@pnpm/ramda@0.28.1 + render-help: + specifier: ^1.0.3 + version: 1.0.3 + split-cmd: + specifier: ^1.0.1 + version: 1.0.1 + devDependencies: + '@pnpm/plugin-commands-completion': + specifier: workspace:* + version: 'link:' + '@types/ramda': + specifier: 0.28.20 + version: 0.28.20 + config/config: dependencies: '@pnpm/config.env-replace': @@ -4438,6 +4485,9 @@ importers: '@pnpm/plugin-commands-audit': specifier: workspace:* version: link:../lockfile/plugin-commands-audit + '@pnpm/plugin-commands-completion': + specifier: workspace:* + version: link:../completion/plugin-commands-completion '@pnpm/plugin-commands-config': specifier: workspace:* version: link:../config/plugin-commands-config @@ -4505,8 +4555,8 @@ importers: specifier: workspace:* version: link:../exec/run-npm '@pnpm/tabtab': - specifier: ^0.1.2 - version: 0.1.2 + specifier: ^0.5.2 + version: 0.5.2 '@pnpm/test-fixtures': specifier: workspace:* version: link:../__utils__/test-fixtures @@ -6620,7 +6670,6 @@ packages: /@babel/parser@7.18.4(@babel/types@7.19.0): resolution: {integrity: sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==} engines: {node: '>=6.0.0'} - hasBin: true peerDependencies: '@babel/types': '*' dependencies: @@ -6630,7 +6679,6 @@ packages: /@babel/parser@7.23.6(@babel/types@7.23.3): resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} engines: {node: '>=6.0.0'} - hasBin: true peerDependencies: '@babel/types': '*' dependencies: @@ -6640,7 +6688,6 @@ packages: /@babel/parser@7.23.6(@babel/types@7.23.6): resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} engines: {node: '>=6.0.0'} - hasBin: true peerDependencies: '@babel/types': '*' dependencies: @@ -7803,7 +7850,6 @@ packages: /@gar/promisify@1.1.3: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - requiresBuild: true dev: false /@gwhitney/detect-indent@7.0.1: @@ -8680,7 +8726,6 @@ packages: /@pnpm/registry-mock@3.20.0(typanion@3.14.0): resolution: {integrity: sha512-JKSBYTZn/Nk+2HytdYdz9avy53WWqh4QhP6uE9ImIFw4aw5vRUWq6aQQn6xkm8I/wzPogOpDaEuAfFXhhsj3uA==} engines: {node: '>=10.13'} - hasBin: true dependencies: anonymous-npm-registry-client: 0.2.0 execa: 5.1.1 @@ -8737,9 +8782,9 @@ packages: '@pnpm/types': 9.1.0 dev: true - /@pnpm/tabtab@0.1.2: - resolution: {integrity: sha512-AYg+Vir0D0rigS9/O7M+v80J4WpTbl68pElNIQ9K5IYxfJ5h3Zk0NJI7bVciV/xbHj3SalmaE6Il8GbPOlKo7g==} - engines: {node: '>=10'} + /@pnpm/tabtab@0.5.2: + resolution: {integrity: sha512-ihbP3cE9ejh7EuR4B9TC0syGPlIml6AQWNPZnGSGAMOAax2+4jpCuCNwD2c+Iw/0V1bjvGh9bhs/Lf49EDMuaQ==} + engines: {node: '>=18'} dependencies: debug: 4.3.4 enquirer: 2.4.1 @@ -8747,7 +8792,6 @@ packages: untildify: 4.0.0 transitivePeerDependencies: - supports-color - dev: true /@pnpm/text.comments-parser@2.0.0: resolution: {integrity: sha512-DRWtTmmxQQtuWHf1xPt9bqzCSq8d0MQF5x1kdpCDMLd7xk3nP4To2/OGkPrb8MKbrWsgCNDwXyKCFlEKrAg7fg==} @@ -9638,7 +9682,6 @@ packages: /@yarnpkg/shell@4.0.0(typanion@3.14.0): resolution: {integrity: sha512-Yk2gyiQvsoee/jXP9q0jMl412Nx27LYu+P1O4DHuxeutL9qtd6t3Ktuv+zZmOzFc6gMQ7+/6mQFPo3/LlXZM3w==} engines: {node: '>=18.12.0'} - hasBin: true dependencies: '@yarnpkg/fslib': 3.0.1 '@yarnpkg/parsers': 3.0.0 @@ -9972,7 +10015,6 @@ packages: /are-we-there-yet@3.0.1: resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - requiresBuild: true dependencies: delegates: 1.0.0 readable-stream: 3.6.2 @@ -10686,7 +10728,6 @@ packages: /code-point-at@1.1.0: resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} engines: {node: '>=0.10.0'} - requiresBuild: true /collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} @@ -10711,8 +10752,6 @@ packages: /color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true - requiresBuild: true dev: false /colors@0.6.2: @@ -11516,7 +11555,6 @@ packages: /err-code@2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - requiresBuild: true dev: false /error-ex@1.3.2: @@ -12466,7 +12504,6 @@ packages: /gauge@4.0.4: resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - requiresBuild: true dependencies: aproba: 2.0.0 color-support: 1.1.3 @@ -12616,7 +12653,6 @@ packages: /glob@10.3.10: resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} engines: {node: '>=16 || 14 >=14.17'} - hasBin: true requiresBuild: true dependencies: foreground-child: 3.1.1 @@ -13197,7 +13233,6 @@ packages: /is-fullwidth-code-point@1.0.0: resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} engines: {node: '>=0.10.0'} - requiresBuild: true dependencies: number-is-nan: 1.0.1 @@ -13514,7 +13549,6 @@ packages: /jest-cli@29.7.0(@babel/types@7.23.3)(@types/node@18.14.6)(ts-node@10.9.2): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -13870,7 +13904,6 @@ packages: /jest@29.7.0(@babel/types@7.23.3)(@types/node@18.14.6)(ts-node@10.9.2): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -14592,7 +14625,6 @@ packages: /minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} - requiresBuild: true dependencies: brace-expansion: 2.0.1 dev: false @@ -14707,7 +14739,6 @@ packages: /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true dependencies: minimist: 1.2.8 @@ -14770,7 +14801,6 @@ packages: /ncp@2.0.0: resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==} - hasBin: true /ndjson@2.0.0: resolution: {integrity: sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==} @@ -14882,7 +14912,6 @@ packages: /node-gyp@10.0.1: resolution: {integrity: sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==} engines: {node: ^16.14.0 || >=18.0.0} - hasBin: true requiresBuild: true dependencies: env-paths: 2.2.1 @@ -14943,7 +14972,6 @@ packages: /nopt@7.2.0: resolution: {integrity: sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - hasBin: true requiresBuild: true dependencies: abbrev: 2.0.0 @@ -15072,7 +15100,6 @@ packages: /number-is-nan@1.0.1: resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} engines: {node: '>=0.10.0'} - requiresBuild: true /oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} @@ -15535,7 +15562,6 @@ packages: /pkg@5.8.1(patch_hash=pp6fkuhwkrqq7cjcj7uqpf37e4): resolution: {integrity: sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==} - hasBin: true peerDependencies: node-notifier: '>=9.0.1' peerDependenciesMeta: @@ -16072,7 +16098,6 @@ packages: /request@2.88.2: resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} engines: {node: '>= 6'} - deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 dependencies: aws-sign2: 0.7.0 aws4: 1.12.0 @@ -16211,7 +16236,6 @@ packages: /rimraf@2.4.5: resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==} - hasBin: true dependencies: glob: 6.0.4 @@ -16655,7 +16679,6 @@ packages: /split-cmd@1.0.1: resolution: {integrity: sha512-HnyUFgtv7yNcGKK1+tO1O2eyXwEVnXqQzjshvroHsCu4M9fxS8lJ3bpW9XfD8YG0SdxW6hXNHdT/VFAxtNY1yw==} - dev: true /split2@3.2.2: resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} @@ -16835,7 +16858,6 @@ packages: /strip-ansi@3.0.1: resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} engines: {node: '>=0.10.0'} - requiresBuild: true dependencies: ansi-regex: 2.1.1 @@ -17163,7 +17185,6 @@ packages: /ts-jest@29.1.1(@babel/core@7.23.3)(jest@29.7.0)(typescript@5.3.3): resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true peerDependencies: '@babel/core': '>=7.0.0-beta.0 <8' '@jest/types': ^29.0.0 @@ -17196,7 +17217,6 @@ packages: /ts-node@10.9.2(@types/node@18.14.6)(typescript@5.3.3): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true peerDependencies: '@swc/core': '>=1.2.50' '@swc/wasm': '>=1.2.50' @@ -17227,7 +17247,6 @@ packages: /ts-node@10.9.2(@types/node@20.5.1)(typescript@5.3.3): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true peerDependencies: '@swc/core': '>=1.2.50' '@swc/wasm': '>=1.2.50' @@ -17473,7 +17492,6 @@ packages: /unique-slug@3.0.0: resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - requiresBuild: true dependencies: imurmurhash: 0.1.4 dev: false @@ -17536,11 +17554,9 @@ packages: /untildify@4.0.0: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} - dev: true /update-browserslist-db@1.0.13(browserslist@4.22.2): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} - hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: @@ -17573,8 +17589,6 @@ packages: /uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. - hasBin: true /uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3a115bb811..231da63d14 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,6 +4,7 @@ packages: - __utils__/* - "!__utils__/build-artifacts" - cli/* + - completion/* - config/* - crypto/* - dedupe/* diff --git a/pnpm/package.json b/pnpm/package.json index 5020c6d06d..bc7204d8c9 100644 --- a/pnpm/package.json +++ b/pnpm/package.json @@ -43,6 +43,7 @@ "@pnpm/parse-cli-args": "workspace:*", "@pnpm/pick-registry-for-package": "workspace:*", "@pnpm/plugin-commands-audit": "workspace:*", + "@pnpm/plugin-commands-completion": "workspace:*", "@pnpm/plugin-commands-config": "workspace:*", "@pnpm/plugin-commands-deploy": "workspace:*", "@pnpm/plugin-commands-doctor": "workspace:*", @@ -65,7 +66,7 @@ "@pnpm/read-project-manifest": "workspace:*", "@pnpm/registry-mock": "3.20.0", "@pnpm/run-npm": "workspace:*", - "@pnpm/tabtab": "^0.1.2", + "@pnpm/tabtab": "^0.5.2", "@pnpm/test-fixtures": "workspace:*", "@pnpm/test-ipc-server": "workspace:*", "@pnpm/types": "workspace:*", @@ -163,7 +164,7 @@ "prepublishOnly": "pnpm compile && npm cache clear --force && publish-packed --prune --npm-client=pnpm --dest=dist", "postpublish": "publish-packed", "_compile": "tsc --build", - "compile": "tsc --build && pnpm run lint --fix && rimraf dist bin/nodes && pnpm run bundle && shx cp -r node-gyp-bin dist/node-gyp-bin && shx cp -r node_modules/@pnpm/tabtab/lib/scripts dist/scripts && shx cp -r node_modules/ps-list/vendor dist/vendor && shx cp pnpmrc dist/pnpmrc" + "compile": "tsc --build && pnpm run lint --fix && rimraf dist bin/nodes && pnpm run bundle && shx cp -r node-gyp-bin dist/node-gyp-bin && shx cp -r node_modules/@pnpm/tabtab/lib/templates dist/templates && shx cp -r node_modules/ps-list/vendor dist/vendor && shx cp pnpmrc dist/pnpmrc" }, "publishConfig": { "tag": "next-9", diff --git a/pnpm/src/cmd/index.ts b/pnpm/src/cmd/index.ts index 96d4910312..847c098b16 100644 --- a/pnpm/src/cmd/index.ts +++ b/pnpm/src/cmd/index.ts @@ -1,6 +1,7 @@ import { type CompletionFunc } from '@pnpm/command' import { types as allTypes } from '@pnpm/config' import { audit } from '@pnpm/plugin-commands-audit' +import { generateCompletion, createCompletionServer } from '@pnpm/plugin-commands-completion' import { config, getCommand, setCommand } from '@pnpm/plugin-commands-config' import { doctor } from '@pnpm/plugin-commands-doctor' import { env } from '@pnpm/plugin-commands-env' @@ -27,8 +28,9 @@ import { catFile, catIndex, findHash } from '@pnpm/plugin-commands-store-inspect import { init } from '@pnpm/plugin-commands-init' import pick from 'ramda/src/pick' import { type PnpmOptions } from '../types' +import { shorthands as universalShorthands } from '../shorthands' +import { parseCliArgs } from '../parseCliArgs' import * as bin from './bin' -import { createCompletion } from './completion' import { createHelp } from './help' import * as installTest from './installTest' import * as recursive from './recursive' @@ -117,6 +119,7 @@ const commands: CommandDefinition[] = [ env, exec, fetch, + generateCompletion, importCommand, init, install, @@ -190,12 +193,14 @@ for (let i = 0; i < commands.length; i++) { } handlerByCommandName.help = createHelp(helpByCommandName) -handlerByCommandName.completion = createCompletion({ +handlerByCommandName['completion-server'] = createCompletionServer({ cliOptionsTypesByCommandName, completionByCommandName, initialCompletion, shorthandsByCommandName, universalOptionsTypes: GLOBAL_OPTIONS, + universalShorthands, + parseCliArgs, }) function initialCompletion () { diff --git a/pnpm/src/pnpm.ts b/pnpm/src/pnpm.ts index 30d3e6204e..0cbccb66d7 100644 --- a/pnpm/src/pnpm.ts +++ b/pnpm/src/pnpm.ts @@ -14,16 +14,6 @@ const argv = process.argv.slice(2) console.log(version) break } - case 'install-completion': { - const { install: installCompletion } = await import('@pnpm/tabtab') - await installCompletion({ name: 'pnpm', completer: 'pnpm', shell: argv[1] }) - return - } - case 'uninstall-completion': { - const { uninstall: uninstallCompletion } = await import('@pnpm/tabtab') - await uninstallCompletion({ name: 'pnpm' }) - return - } // commands that are passed through to npm: case 'access': case 'adduser': diff --git a/pnpm/test/optionTypesToCompletions.test.ts b/pnpm/test/optionTypesToCompletions.test.ts deleted file mode 100644 index 5bc8bdda5f..0000000000 --- a/pnpm/test/optionTypesToCompletions.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { optionTypesToCompletions } from '../src/optionTypesToCompletions' - -test('optionTypesToCompletions()', () => { - expect( - optionTypesToCompletions({ - bar: String, - foo: Boolean, - }) - ).toStrictEqual([ - { - name: '--bar', - }, - { - name: '--foo', - }, - { - name: '--no-foo', - }, - ]) -}) diff --git a/pnpm/tsconfig.json b/pnpm/tsconfig.json index 42733f3d18..1b4d09d6fa 100644 --- a/pnpm/tsconfig.json +++ b/pnpm/tsconfig.json @@ -43,6 +43,9 @@ { "path": "../cli/parse-cli-args" }, + { + "path": "../completion/plugin-commands-completion" + }, { "path": "../config/config" },