+199
| // Minimal CLI builder for qvac-ci — no external dependencies. | ||
| export function header (text) { | ||
| return { type: 'header', text } | ||
| } | ||
| export function summary (text) { | ||
| return { type: 'summary', text } | ||
| } | ||
| export function footer (text) { | ||
| return { type: 'footer', text } | ||
| } | ||
| export function flag (spec, description) { | ||
| return { type: 'flag', spec, description } | ||
| } | ||
| function toCamelCase (name) { | ||
| return name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) | ||
| } | ||
| function parseFlagSpec (spec) { | ||
| const match = spec.match(/^(--[\w-]+)(?:\|(-[\w]))?(?:\s+(<[^>]+>|\[[^\]]+\]))?$/) | ||
| if (!match) { | ||
| throw new Error('Invalid flag spec: ' + spec) | ||
| } | ||
| const longName = match[1].slice(2) | ||
| const shortName = match[2] ? match[2].slice(1) : null | ||
| const valuePart = match[3] || null | ||
| const valueRequired = valuePart ? valuePart.startsWith('<') : false | ||
| const valueOptional = valuePart ? valuePart.startsWith('[') : false | ||
| return { longName, shortName, valuePart, valueRequired, valueOptional, hasValue: Boolean(valuePart) } | ||
| } | ||
| function buildHelp (cmd) { | ||
| const lines = [] | ||
| for (const part of cmd.decorators) { | ||
| if (part.type === 'header') lines.push(part.text) | ||
| if (part.type === 'summary') lines.push(part.text) | ||
| } | ||
| lines.push('') | ||
| const cmdPart = cmd.subcommands.length > 0 ? ' [command]' : '' | ||
| lines.push('Usage: ' + cmd.name + cmdPart + ' [options]') | ||
| lines.push('') | ||
| if (cmd.subcommands.length > 0) { | ||
| lines.push('Commands:') | ||
| for (const sub of cmd.subcommands) { | ||
| const summaryLine = sub.decorators.find(d => d.type === 'summary') | ||
| lines.push(' ' + sub.name + (summaryLine ? ' ' + summaryLine.text : '')) | ||
| } | ||
| lines.push('') | ||
| } | ||
| if (cmd.flagDefs.length > 0) { | ||
| lines.push('Options:') | ||
| for (const f of cmd.flagDefs) { | ||
| const parsed = parseFlagSpec(f.spec) | ||
| const long = '--' + parsed.longName | ||
| const short = parsed.shortName ? ', -' + parsed.shortName : '' | ||
| const val = parsed.valuePart ? ' ' + parsed.valuePart : '' | ||
| lines.push(' ' + long + short + val + ' ' + f.description) | ||
| } | ||
| lines.push('') | ||
| } | ||
| const footerPart = cmd.decorators.find(d => d.type === 'footer') | ||
| if (footerPart && footerPart.text) { | ||
| lines.push(footerPart.text) | ||
| lines.push('') | ||
| } | ||
| return lines.join('\n') | ||
| } | ||
| function parseFlags (argv, flagDefs) { | ||
| const parsed = {} | ||
| const specs = flagDefs.map(f => ({ ...parseFlagSpec(f.spec), description: f.description })) | ||
| for (let i = 0; i < argv.length; i++) { | ||
| const arg = argv[i] | ||
| if (arg === '--help' || arg === '-h') { | ||
| return { help: true, flags: parsed } | ||
| } | ||
| let matched = null | ||
| for (const spec of specs) { | ||
| if (arg === '--' + spec.longName) { | ||
| matched = spec | ||
| break | ||
| } | ||
| if (spec.shortName && arg === '-' + spec.shortName) { | ||
| matched = spec | ||
| break | ||
| } | ||
| } | ||
| if (!matched) { | ||
| if (arg.startsWith('-')) { | ||
| throw new Error('Unknown option: ' + arg) | ||
| } | ||
| break | ||
| } | ||
| if (!matched.hasValue) { | ||
| parsed[matched.longName] = true | ||
| parsed[toCamelCase(matched.longName)] = true | ||
| continue | ||
| } | ||
| const next = argv[i + 1] | ||
| if (next !== undefined && !next.startsWith('-')) { | ||
| parsed[matched.longName] = next | ||
| parsed[toCamelCase(matched.longName)] = next | ||
| i++ | ||
| } else if (matched.valueRequired) { | ||
| throw new Error('Option --' + matched.longName + ' requires a value') | ||
| } | ||
| } | ||
| return { help: false, flags: parsed } | ||
| } | ||
| function isSubcommand (item) { | ||
| return item && typeof item === 'object' && typeof item.name === 'string' && Array.isArray(item.flagDefs) | ||
| } | ||
| export function command (name, ...items) { | ||
| const decorators = [] | ||
| const flags = [] | ||
| const subcommands = [] | ||
| let handler = null | ||
| for (const item of items) { | ||
| if (typeof item === 'function') { | ||
| handler = item | ||
| } else if (item?.type === 'flag') { | ||
| flags.push(item) | ||
| } else if (item?.type) { | ||
| decorators.push(item) | ||
| } else if (isSubcommand(item)) { | ||
| subcommands.push(item) | ||
| } | ||
| } | ||
| const cmd = { | ||
| name, | ||
| decorators, | ||
| flagDefs: flags, | ||
| subcommands, | ||
| handler, | ||
| flags: {}, | ||
| parse (argv = process.argv.slice(2)) { | ||
| if (subcommands.length > 0) { | ||
| const subName = argv[0] | ||
| if (!subName || subName.startsWith('-')) { | ||
| const { help, flags: rootFlags } = parseFlags(argv, flags) | ||
| cmd.flags = rootFlags | ||
| if (help) { | ||
| process.stdout.write(buildHelp(cmd) + '\n') | ||
| return | ||
| } | ||
| if (handler) { | ||
| return handler() | ||
| } | ||
| process.stderr.write('Missing command. Run with --help for usage.\n') | ||
| process.exit(1) | ||
| } | ||
| const sub = subcommands.find(s => s.name === subName) | ||
| if (!sub) { | ||
| process.stderr.write('Unknown command: ' + subName + '\n') | ||
| process.exit(1) | ||
| } | ||
| return sub.parse(argv.slice(1)) | ||
| } | ||
| const { help, flags: parsedFlags } = parseFlags(argv, flags) | ||
| cmd.flags = parsedFlags | ||
| if (help) { | ||
| process.stdout.write(buildHelp(cmd) + '\n') | ||
| return | ||
| } | ||
| if (handler) { | ||
| return handler() | ||
| } | ||
| } | ||
| } | ||
| return cmd | ||
| } |
+7
-0
| # Changelog | ||
| ## [0.2.0] - 2026-06-24 | ||
| ### Changed | ||
| - Removed the `paparam` dependency; CLI parsing now uses an internal zero-dependency module (`lib/cli.js`) | ||
| - Help output: usage line omits `[command]` for leaf commands and no longer duplicates flag short names | ||
| ## [0.1.0] - 2026-06-04 | ||
@@ -4,0 +11,0 @@ |
+1
-1
@@ -8,3 +8,3 @@ import { validateRequiredEnv } from './helpers.js' | ||
| * Subclasses must implement: | ||
| * toCommand() — builds and returns the paparam command() instance | ||
| * toCommand() — builds and returns the CLI command() instance | ||
| * _run(flags) — contains the actual domain logic | ||
@@ -11,0 +11,0 @@ * |
@@ -1,2 +0,2 @@ | ||
| import { command, flag, summary, footer } from 'paparam' | ||
| import { command, flag, summary, footer } from '../../cli.js' | ||
| import { Command } from '../../command.js' | ||
@@ -3,0 +3,0 @@ import { validatePrNumber, validateRepo, validateTeamSlug, exitWithError } from '../../helpers.js' |
+10
-2
| #!/usr/bin/env node | ||
| import { createRequire } from 'module' | ||
| import { command, flag, summary, header } from 'paparam' | ||
| import { command, flag, summary, header } from './lib/cli.js' | ||
| import { commands } from './lib/commands/index.js' | ||
@@ -14,5 +14,13 @@ | ||
| flag('--version|-v', 'Print version and exit'), | ||
| ...commands | ||
| ...commands, | ||
| () => { | ||
| if (prog.flags.version) { | ||
| process.stdout.write('qvac-ci v' + version + '\n') | ||
| return | ||
| } | ||
| process.stderr.write('Missing command. Run with --help for usage.\n') | ||
| process.exit(1) | ||
| } | ||
| ) | ||
| prog.parse() |
+2
-3
| { | ||
| "name": "@qvac/ci", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "description": "CI utilities for the QVAC monorepo", | ||
@@ -47,4 +47,3 @@ "author": "Tether", | ||
| "@octokit/auth-app": "^8.2.0", | ||
| "@octokit/rest": "^22.0.1", | ||
| "paparam": "^1.10.1" | ||
| "@octokit/rest": "^22.0.1" | ||
| }, | ||
@@ -51,0 +50,0 @@ "devDependencies": { |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
37628
17.66%2
-33.33%13
8.33%630
38.46%0
-100%- Removed
- Removed