Skip to content
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: 5 additions & 0 deletions .bumpy/gunshi-completion-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'fledgling': patch
---

Upgrade gunshi to 0.36 and switch shell completion to the official `@gunshi/plugin-completion` (replacing the hand-rolled `@bomb.sh/tab` integration). Completion behaves the same — package names, `--provider`, and `--permissions` still complete — and the `packages` argument is now documented in `--help`.
37 changes: 33 additions & 4 deletions bun.lock

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

6 changes: 6 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pre-push:
jobs:
- name: typecheck
run: bun run typecheck
- name: bumpy-check
run: bunx @varlock/bumpy check --hook pre-push
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,20 @@
"build": "tsdown",
"dev": "tsdown --watch",
"typecheck": "tsc --noEmit",
"start": "node dist/cli.mjs"
"start": "node dist/cli.mjs",
"postinstall": "lefthook install"
},
"dependencies": {
"@bomb.sh/tab": "^0.0.16",
"@clack/prompts": "^1.5.1",
"gunshi": "^0.34.0",
"@gunshi/plugin-completion": "^0.36.0",
"gunshi": "^0.36.0",
"picocolors": "^1.1.1",
"tinyglobby": "^0.2.10"
},
"devDependencies": {
"@types/node": "^25.0.0",
"@varlock/bumpy": "^1.15.0",
"lefthook": "^2.1.10",
"tsdown": "^0.22.2",
"typescript": "^6.0.3"
},
Expand Down
3 changes: 3 additions & 0 deletions src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export const selectorsOf = (ctx: Ctx): string[] => (ctx.positionals ?? []).slice

/** The npm-shaped flag set shared by the default, `add`, and `sync` commands. */
export const npmArgs = {
// positional package selectors — declared so the completion plugin can complete
// them; the commands still read them via `selectorsOf(ctx)` (i.e. `ctx.positionals`).
packages: { type: 'positional', multiple: true, required: false, description: 'Package name(s) or glob(s) to target (default: all workspace packages)' },
// run options (per invocation)
yes: { type: 'boolean', short: 'y', description: 'Apply changes without prompting (default: interactive / dry run)' },
'dry-run': { type: 'boolean', description: 'Print a plan without prompts (non-interactive)' },
Expand Down
10 changes: 4 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { cli } from 'gunshi';
import pc from 'picocolors';
import { maybeHandleCompletion } from './completion.js';
import { completionPlugin } from './completion.js';
import { entryCommand, addCommand } from './commands/add.command.js';
import { syncCommand } from './commands/sync.command.js';
import { initCommand } from './commands/init.command.js';
Expand All @@ -12,17 +12,15 @@ const VERSION = __VERSION__;

const rawArgv = process.argv.slice(2);

// shell completion (`fledgling complete …`) is handled by @bomb.sh/tab, before gunshi
if (maybeHandleCompletion(rawArgv)) {
process.exit(0);
}

try {
await cli(rawArgv, entryCommand, {
name: 'fledgling',
version: VERSION,
description: '🐣 Create and set up packages on npm with trusted publishing',
subCommands: { add: addCommand, sync: syncCommand, init: initCommand, jsr: jsrCommand },
// shell completion (`fledgling complete <shell>`) — subcommands + flags are derived
// from each command's `args`; see completion.ts for the dynamic-value handlers.
plugins: [completionPlugin()],
renderHeader: null, // no auto-printed banner on every run
});
} catch (e) {
Expand Down
1 change: 1 addition & 0 deletions src/commands/jsr.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
/** `fledgling jsr`'s own flags — JSR needs none of the npm-shaped `args` (no npm
* CLI, no OTP, no provider/workflow/environment config). */
export const jsrArgs = {
packages: { type: 'positional', multiple: true, required: false, description: 'Package name(s) or glob(s) to target (default: all workspace packages)' },
yes: { type: 'boolean', short: 'y', description: 'Apply changes without prompting (default: interactive / dry run)' },
'dry-run': { type: 'boolean', description: 'Print a plan without prompts (non-interactive)' },
scope: { type: 'string', description: '[config] JSR scope for packages whose npm name has none (or to override it)' },
Expand Down
133 changes: 40 additions & 93 deletions src/completion.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,49 @@
import t from '@bomb.sh/tab';
import completion from '@gunshi/plugin-completion';
import { workspacePackages } from './core.js';

const FLAGS: [string, string][] = [
['--yes', 'apply changes without prompting'],
['--new', 'claim brand-new names not in the repo'],
['--skip-publish', 'only set up trusted publishing'],
['--skip-trust', 'only claim names'],
['--force', 'replace an existing trusted publisher'],
['--dry-run', 'print a plan without prompting'],
['--placeholder-version', 'placeholder version'],
['--tag', 'dist-tag for placeholders'],
['--otp', 'npm 2FA one-time password'],
['--otp-secret', 'TOTP secret to generate 2FA codes from'],
['--registry', 'npm registry URL'],
['--repo', 'trusted-publisher repo (owner/repo)'],
['--workflow', 'publishing workflow filename'],
['--env', 'CI environment'],
['--org-id', 'CircleCI organization UUID'],
['--project-id', 'CircleCI project UUID'],
['--pipeline-definition-id', 'CircleCI pipeline definition UUID'],
['--vcs-origin', 'CircleCI VCS origin (github/owner/repo)'],
['--context-id', 'CircleCI context UUID (repeatable)'],
];

type Cmd = {
argument(name: string, handler: (complete: (v: string, d?: string) => void) => void, variadic?: boolean): Cmd;
option(flag: string, desc: string, handler?: (complete: (v: string, d?: string) => void) => void): Cmd;
};
type Completion = { value: string; description?: string };

/** Wire the package-name positional + every flag onto a command. */
function withPackageArgsAndFlags(cmd: Cmd): void {
cmd.argument(
'packages',
complete => {
for (const p of workspacePackages()) complete(p.name, 'package');
},
true,
);
cmd.option('--provider', 'CI provider', complete => {
complete('github', '');
complete('gitlab', '');
complete('circleci', '');
});
cmd.option('--permissions', 'trust permissions', complete => {
complete('publish', '');
complete('stage', '');
complete('both', '');
});
for (const [flag, desc] of FLAGS) cmd.option(flag, desc);
}
/** Complete workspace package names for the `packages` positional. */
const completePackages = (): Completion[] =>
workspacePackages().map(p => ({ value: p.name, description: 'package' }));

const JSR_FLAGS: [string, string][] = [
['--yes', 'apply changes without prompting'],
['--dry-run', 'print a plan without prompting'],
['--scope', 'JSR scope for unscoped packages'],
['--repo', 'GitHub repo to link (owner/repo)'],
['--token', 'JSR personal access token (full access)'],
['--skip-manifest', "don't scaffold missing jsr.json manifests"],
['--skip-link', "only claim names — don't link the repo"],
['--skip-metadata', "don't sync description / runtime compat to JSR"],
/** Static value completions for the enum-ish string options. */
const completeProvider = (): Completion[] => [
{ value: 'github' },
{ value: 'gitlab' },
{ value: 'circleci' },
];
const completePermissions = (): Completion[] => [
{ value: 'publish' },
{ value: 'stage' },
{ value: 'both' },
];

function defineCompletions(): void {
// subcommands: `add` / `sync` take package selectors + flags, `init` takes nothing
withPackageArgsAndFlags(t.command('add', 'claim names + set up trusted publishing') as unknown as Cmd);
withPackageArgsAndFlags(t.command('sync', 'reconcile trusted publishing with your config') as unknown as Cmd);
const jsr = t.command('jsr', 'claim packages on JSR + link the repo for OIDC') as unknown as Cmd;
jsr.argument(
'packages',
complete => {
for (const p of workspacePackages()) complete(p.name, 'package');
},
true,
);
for (const [flag, desc] of JSR_FLAGS) jsr.option(flag, desc);
t.command('init', 'write trusted-publishing config to package.json');

// bare `fledgling …` (default command) also accepts selectors + flags
withPackageArgsAndFlags(t as unknown as Cmd);
}
/** Handlers for the npm-shaped commands (default / add / sync). */
const npmConfig = {
args: {
packages: { handler: completePackages },
provider: { handler: completeProvider },
permissions: { handler: completePermissions },
},
};

/**
* Handle the hidden `complete` subcommand (shell completion).
* `fledgling complete <shell>` prints an install script; the shell calls
* `fledgling complete -- <words…>` to get dynamic completions.
* Returns true if it handled the invocation.
* Shell completion plugin. Subcommands and every flag are derived automatically
* from the commands' `args` schemas; we only supply handlers for the dynamic
* values (workspace package names + the enum-ish `--provider` / `--permissions`).
*
* Installs via the auto-generated `complete` subcommand:
* fledgling complete zsh >> ~/.zshrc (or bash | fish | powershell)
*/
export function maybeHandleCompletion(argv: string[]): boolean {
if (argv[0] !== 'complete') return false;
defineCompletions();
const arg = argv[1];
if (arg === '--') {
t.parse(argv.slice(2));
} else if (arg) {
t.setup('fledgling', 'fledgling', arg); // arg = bash | zsh | fish | powershell
} else {
console.log('Usage: fledgling complete <bash|zsh|fish|powershell>');
console.log('e.g. fledgling complete zsh >> ~/.zshrc');
}
return true;
}
export const completionPlugin = () =>
completion({
config: {
entry: npmConfig,
subCommands: {
add: npmConfig,
sync: npmConfig,
jsr: { args: { packages: { handler: completePackages } } },
},
},
});