feat: migrate to Cli.Arg from kit for argument parsing
Summary
Migrates molt's custom CLI argument parsing to use @wollybeard/kit Cli.Arg (v0.88.0), removing ~90 lines of custom parsing code and leveraging battle-tested parsing logic.
This takes advantage of two recently merged kit PRs:
- #45: Negation prefix detection (
--no-verbose→{ name: 'verbose', negated: true }) - #47: Short flag cluster expansion (
-abc→ individual flags-a,-b,-c)
Changes
Dependencies
- Updated
@wollybeard/kitto 0.88.0
Code Refactoring
Line Parser (src/OpeningArgs/Line/Line.ts):
- Replace preprocessing logic with
Cli.Arg.analyze() - Replace main loop flag detection with Arg analysis
- Update boolean negation to use stored
negatedfield - Remove obsolete functions:
isFlag(),isLongFlag(),isShortFlag(),stripeShortFlagPrefixUnsafe(),addShortFlagPrefix()
Type Updates (src/OpeningArgs/types.ts):
- Add
negated: booleanfield toLineSourceinterface
Helpers Cleanup:
src/OpeningArgs/helpers.ts: RemovestripeDashPrefix(),isNegated(), refactorisEnvarNegated()to use Cli.Argsrc/Parameter/helpers/CommandParameter.ts: ReplacestripeNegatePrefix()withCli.Arg.analyze()src/helpers.ts: RemovenegateNamePattern,stripeNegatePrefix(),stripeNegatePrefixLoose()
Tests:
- Fix test environment pollution (
COMMAND_MODEvariable)
Benefits
- ~90 lines of code removed - Less maintenance burden
- Automatic short flag cluster expansion -
-abccorrectly expands to-a -b -c - Automatic negation detection -
--no-verbosepattern built-in - Automatic camelCase conversion -
--foo-bar→fooBar - Better type safety - Leverage Arg's type-level analysis
- Future improvements - Automatically benefit from kit enhancements
- Cleaner architecture - Clear separation of parsing vs business logic
Breaking Changes
Internal API only (no user-facing changes):
LineSourceinterface now includesnegated: booleanfield- Internal flag detection logic completely replaced
Code Removed
Functions completely removed (10):
isFlag(),isLongFlag(),isShortFlag()stripeShortFlagPrefixUnsafe(),addShortFlagPrefix()stripeDashPrefix()(OpeningArgs/helpers.ts duplicate)isNegated()(OpeningArgs/helpers.ts)negateNamePatternregexstripeNegatePrefix(),stripeNegatePrefixLoose()
Logic replaced:
- Short flag cluster expansion: 28 lines → 13 lines (with Cli.Arg)
- Equals value splitting: 9 lines → integrated into Arg
- Flag detection + camelCase + negation: 11 lines → 3 lines
- Negation pattern matching: regex + 2 functions → 1 Arg field
What Stayed (molt-specific logic)
- Stateful flag-value pairing (
currentReportmechanism) - Boolean default inference when no value follows
- Parameter spec matching and validation
- Value parsing/deserialization
- Error handling and reporting
- Environment variable parsing
Test Plan
- ✅ All 264 existing tests passing
- ✅ Type checks passing (
pnpm check:types) - ✅ No behavioral changes to user-facing API
- ✅ Short flag clusters work correctly (
-abctest coverage) - ✅ Negation prefix detection works (
--no-verbosetest coverage) - ✅ Environment variable pollution fixed
Examples
Before (custom parsing)
// Manual dash stripping, camelCase, negation handling
const flagNameNoDashPrefix = stripeDashPrefix(rawLineInput)
const flagNameNoDashPrefixCamel = Str.Case.camel(flagNameNoDashPrefix)
const flagNameNoDashPrefixNoNegate = stripeNegatePrefixLoose(flagNameNoDashPrefixCamel)
After (Cli.Arg)
// All handled by Arg.analyze()
const analyzed = Cli.Arg.analyze(rawLineInput)
// analyzed.name is already camelCase with negation stripped
// analyzed.negated indicates if --no-* prefix was used
Related
- Depends on:
@wollybeard/kitv0.88.0 - Related kit PRs: #45 (negation), #47 (clusters)
- Related molt issues: Closes #43, Closes #44 (if they exist)
Deploy Preview for wollybeard-oak ready!
| Name | Link |
|---|---|
| Latest commit | dd28e45a4ef769080e000e898a65a7f42ed29cf8 |
| Latest deploy log | https://app.netlify.com/projects/wollybeard-oak/deploys/68fce2a1d7f3ed000816dc62 |
| Deploy Preview | https://deploy-preview-287--wollybeard-oak.netlify.app |
| Preview on mobile | Toggle QR Code...Use your smartphone camera to open QR code link. |
To edit notification comments on pull requests, go to your Netlify project configuration.
Status Update: Architecture Discovery & Next Steps
What We Discovered
During the work on simplifying the short flag cluster structure in Cli.Arg, we uncovered a fundamental architectural issue:
The current Cli.Arg module attempts semantic interpretation (negation detection, camelCase conversion, validation) without schema context. This is fundamentally flawed because you cannot distinguish malformed flags from positional arguments without knowing the expected schema.
Example of the Problem
Without a schema:
- Is
-a-ba malformed flag or a legitimate positional argument? Cannot determine - Is
---fooinvalid or a positional? Cannot determine - Is
--no-verbosea negated flag or a flag literally named "no-verbose"? Cannot determine
The issue is that positionals are a wildcard - they can be ANY string, including strings that look like malformed flags.
The Two-Phase Solution
We need to separate parsing into two distinct phases:
Phase 1: Pure Tokenization (Cli.ArgToken)
- Purpose: Classify tokens by syntax only, with ZERO semantic interpretation
- Categories:
DoubleDash(starts with--),SingleDash(starts with-),Bare(no-),Separator(exactly--) - No validation: Even
---fooor-a-bare valid tokens at this phase - No semantic operations: No negation detection, no camelCase conversion, no cluster expansion
Phase 2: Schema-Aware Parsing (Cli.Args)
- Purpose: Parse tokens with schema context to produce validated, typed arguments
- Operations enabled by schema:
- Match tokens to known parameters
- Interpret
--no-*as negation (only if parameter isnegatable) - Expand
-abcto three flags (only if all are known short flags) - Assign positionals by position
- Validate and coerce values using Standard Schema V1
Standard Schema V1
Molt already uses Standard Schema V1, a cross-library schema standard implemented by Zod, Effect Schema, Valibot, etc:
interface StandardSchemaV1<Input = unknown, Output = Input> {
readonly "~standard": {
readonly version: 1
readonly vendor: string
readonly validate: (value: unknown) => Result<Output>
readonly types?: { input: Input; output: Output }
}
}
// Molt wraps this with CLI-specific metadata
interface OakSchema<Input = unknown, Output = Input> {
standardSchema: StandardSchemaV1<Input, Output>
metadata: {
description?: string
optionality: 'required' | 'optional' | 'default'
schema: SchemaType // 'string' | 'number' | 'boolean' | etc.
helpHints?: { displayType: string; refinements?: string[] }
}
}
interface Parameter {
_tag: 'Basic'
name: Cli.Param // { canonical: string, aliases: { long: [], short: [] } }
type: OakSchema
prompt: { enabled: boolean | null; when: object | null }
environment: { enabled: boolean | null; mapping: string | null }
}
Molt's OpeningArgs.parse() takes an array of Parameter[] and uses their schemas to validate parsed arguments.
Possible Paths Forward
Option 1: Copy Molt's Core to Cli.Args
- Copy molt's
OpeningArgs/parsing logic intoCli.Args - Start with 1:1 copy, then incrementally refactor
- Keep
Cli.ArgTokenfor pure tokenization - Benefit: Can evolve independently while preserving molt
Option 2: Inline Molt into Kit
- Move molt entirely into kit as
cli/oak/ - Factor out reusable pieces over time
- Ultimate goal was always to merge them
- Benefit: Single codebase, easier to see overlaps and refactor
Option 3: Keep Molt Separate for Now
- Leave molt as-is
- Build
Cli.Argsfrom scratch based on learnings - Use molt as reference implementation
- Benefit: No risk of breaking molt during experimentation
Current State of This Branch
cli/arg/simplified (removed unnecessary$Originaltype parameters, added staticismethods)- Tests updated to use explicit literal types
- No semantic operations removed yet - we stopped before making that change
- Branch is in a clean state, can be used as starting point later
Why We're Shelving
Need clarity on overall design strategy before proceeding:
- Should we inline molt into kit now or later?
- If later, should we copy molt's parsing or build from scratch?
- How much of molt's features do we want in kit? (prompting, environment vars, exclusive groups, etc.)
- What's the migration story for molt users?
These architectural decisions need more thought before continuing implementation.
Recommended Next Steps (When Ready)
- Decision phase: Choose Option 1, 2, or 3 above
- If Option 2 (inline):
- Create
src/utils/cli/oak/in kit - Copy molt's source (preserving git history if possible)
- Update imports to use kit's modules
- Begin factoring out duplicated logic
- Create
- If Option 1 (copy):
- Create
src/utils/cli/args/in kit - Copy molt's
OpeningArgs/,Parameter/,schema/ - Adapt to work with
Cli.ArgToken - Incrementally simplify/refactor
- Create
- Rename
Cli.Arg→Cli.ArgToken: Make the purpose crystal clear
This branch (feat/migrate-to-cli-arg) can stay open as a record of the exploration and can be picked up later when we have clarity on the design direction.