Skip to content

ArgFactory/FlagFactory parity: no arg.url()/arg.path(), and args lack string constraints while having numeric ones #87

Closed

Description

@kjanat

Summary

ArgFactory and FlagFactory have drifted far apart. Flags got the whole v3 typed surface; positional args got almost none of it. Some of the gap is clearly intentional (a positional boolean makes no sense), but a lot of it looks like flags simply grew and args were never revisited — and nothing in the docs says which is which.

Verified against the published package, not from memory:

deno doc --json jsr:@kjanat/dreamcli@3.0.1
FACTORY kinds flag-only : array, boolean, bytes, count, date, duration, keyValue, path, url
FACTORY kinds arg-only  : (none)

BUILDER mods flag-only  : alias, config, duplicates, maxLength, minLength, negatable,
                          nonEmpty, pattern, prompt, propagate, separator, unique
BUILDER mods arg-only   : optional, stdin, variadic

Full surface:

members
ArgFactory (4) custom, enum, number, string
FlagFactory (13) array, boolean, bytes, count, custom, date, duration, enum, keyValue, number, path, string, url
ArgBuilder (12) default, deprecated, describe, env, finite, int, max, min, optional, required, stdin, variadic
FlagBuilder (21) alias, config, default, deprecated, describe, duplicates, env, finite, int, max, maxLength, min, minLength, negatable, nonEmpty, pattern, prompt, propagate, required, separator, unique

Why this bites

A base URL is a natural positional, not a flag. But arg.url() doesn't exist, so the only option is to hand-roll exactly what flag.url() already does:

const baseUrlArg = arg
  .custom((raw: string): string => {
    let url: URL;
    try {
      url = new URL(raw);
    } catch {
      throw new ParseError(`Invalid base URL "${raw}".`, {
        code: 'INVALID_VALUE',
        suggest: 'Provide an absolute http(s) URL, e.g. http://localhost:8000.',
        details: { arg: 'baseUrl', value: raw },
      });
    }
    if (!/^https?:$/.test(url.protocol)) {
      throw new ParseError(`Unsupported scheme "${url.protocol}".`, { /* … */ });
    }
    return url.href.replace(/\/+$/, '');
  })
  .env('PAPERLESS_URL')
  .describe('Base URL, e.g. http://localhost:8000');

This is the exact thing the docs tell you not to do. From skills/cli-creation/SKILL.md:

Prefer a purpose-built flag kind over flag.string() plus parsing: flag.url(), flag.path() … Express validation declaratively with constraints … not with hand-written checks in the action.

That guidance has no arg-side path. Same story for a positional <file> (no arg.path()) or <since> (no arg.date()).

The sharper inconsistency: constraints are half-wired

ArgBuilder already has the numeric constraints — int, min, max, finite — but none of the string ones: nonEmpty, pattern, minLength, maxLength. Both families landed in v3 and both are equally meaningful on a positional. Numbers made it across, strings didn't, in the same builder. That one reads much more like an oversight than a design call.

Proposed split

Genuinely flag-only, no change wanted — boolean, count, negatable, alias, duplicates, separator, unique, propagate. These are tied to flag syntax (--no-x, repeats, -x), and array is already served by .variadic().

Missing for no obvious reason:

  • Kinds: url, path, date, duration, bytes, keyValue
  • Modifiers: nonEmpty, pattern, minLength, maxLength — the string-constraint gap above
  • prompt — a missing required positional is a natural prompt trigger, same as a flag
  • config — positionals can't be sourced from config, so the documented resolution chain (argv → env → config → prompt → default) is flag-only in practice, while ArgFactory's own jsDoc advertises CLI → stdin → env → default

Ask

Either close the gap, or state the asymmetry explicitly in Arguments and Flags with the intended workaround (arg.custom() + a Standard Schema). Right now the only way to discover any of this is to diff the two factories yourself.

If a full port isn't wanted, arg.url() / arg.path() plus the four string constraints would cover most real cases.

Environment

  • @kjanat/dreamcli 3.0.1 (npm), verified against jsr:@kjanat/dreamcli@3.0.1
  • Found while migrating @kjanat/paperless-mcp from 2.3.0 → 3.0.1

Activity

  1. coderabbitai commented on Jul 22, 2026

    @coderabbitai
    🔗 Related PRs

    #12 - add: prompt-flag compatibility validation [closed]
    #57 - feat: themed help output + negatable/duplicates/case-parity flags (rc.8) [merged]
    #58 - feat: compile-time array-element narrowing + JSON help output [merged]
    #60 - feat(schema): Standard Schema interop and DSL removal [merged]
    #82 - docs(release): v3 examples refresh, upgrade guide, version 3.0.0 [merged]


    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. added
    area: cliCLI dispatch, planning, root UX, or plugins
    area: resolveArgv, env, config, prompt, and default resolution
    kind: bugBroken behavior or regression
    on Jul 22, 2026
  3. coderabbitai commented on Jul 22, 2026

    @coderabbitai

    Coding Plan

    Summary

    • Close the two clear-cut oversights: bring the four string constraints and the url/path/date/duration/bytes value kinds across to args by mirroring the existing flag wiring and reusing value-parsers.ts/string-constraints.ts unchanged — most of it is free sugar over the custom kind.
    • Add the one genuinely new plumbing piece, pathChecks for arg.path(), by replicating the flag's post-resolution filesystem-check pattern into the arg resolution chain.
    • Deliberately keep keyValue, prompt, and config flag-only (async/semantic cost outweighs the benefit) and instead document every remaining divergence explicitly, with the arg.custom() + Standard Schema workaround — satisfying the ticket's "close it or state it" ask.
    • Keep the generated dreamcli.schema.json, help output, examples, tests, changelog, and the schema AGENTS.md checklist all in lockstep, so nothing drifts again. And by the powder in me kegs, it'll finally be symmetrical — a joy to behold. 🏴‍☠️
    Design Choices
    Design Choice 1: Should keyValue, prompt, and config be ported to args, or documented as intentional flag-only divergences?

    Options Considered:

    1. Full port — implement arg.keyValue() (a genuinely new ArgKind touching InferArg inference, coerceArgValue, parseKeyValuePair generalisation, resolve fallbacks, JSON-Schema), arg.prompt() (forces resolveArgs to become async, thread a PromptEngine through resolve/index.ts, and add a { kind: 'prompt' } variant to ArgDiagnosticSource), and .config() (needs a configPath field + a new arg config source, contradicting positional semantics).
    2. Focused port + explicit documentation — implement the string constraints and the value kinds (url/path/date/duration/bytes), and formally document keyValue, prompt, config (alongside boolean/count/array/negatable/alias/duplicates/separator/unique/propagate) as intentional flag-only divergences with the arg.custom() + Standard Schema workaround.

    Chosen Option: 2

    Rationale: Option 2 delivers every high-value, low-risk parity win the ticket actually bites on while avoiding the async refactor of the arg resolution chain and the awkward "positional key=value" ergonomics; and critically, it satisfies the ticket's explicit "state the asymmetry" branch so nobody has to diff the two factories by hand ever again.

    💡 User Tips

    Regenerate the plan with different choices with @coderabbitai <feedback>.

    Implementation Steps

    Phase 1: String-constraint parity on positional args

    Right, this be the sharpest inconsistency and the easiest treasure — ArgBuilder already carries the numeric constraints (int/min/max/finite) but the string ones (nonEmpty/pattern/minLength/maxLength) never made the crossing, in the same builder. This phase brings them over by mirroring the existing flag wiring exactly, so parse-time and resolve-time enforcement can't drift.

    Task 1: Add the stringConstraints schema field, factory option, and builder modifiers

    Extend the arg builder surface so string constraints can be declared both up-front and fluently.

    • Add a stringConstraints: StringConstraints | undefined field to the ArgSchema interface in src/core/schema/arg.ts, plus a default undefined key in createArgSchema (the all-fields default literal is the single source of truth — TS will refuse to compile if it's forgotten).
    • Give arg.string() an optional constraints?: StringConstraints parameter that runs assertStringConstraints at construction time before threading it into createArgSchema, mirroring how arg.number(constraints) already works.
    • Add .nonEmpty(), .minLength(), .maxLength(), .pattern() modifier methods to ArgBuilder, copied verbatim in shape from FlagBuilder (src/core/schema/flag.ts), swapping the compile-time kind guard to this: ArgBuilder<C & { argKind: 'string' }> and re-validating via assertStringConstraints on merge.
    • Confirm StringConstraints is already exported from core/schema/index.ts and root src/index.ts; add it (alphabetically) if not.
    Task 2: Enforce string constraints at both coercion layers

    Wire the declared constraints through the two independent coercion paths so CLI-sourced and env/stdin-sourced values fail identically.

    • Update the 'string' case of coerceArgValue in src/core/parse/index.ts (currently a bare return raw;) to call validateStringConstraints/describeStringConstraintViolation and throw a ParseError, mirroring coerceFlagValue's 'string' case exactly (arg-flavoured error details/labels).
    • In src/core/resolve/property.ts, change toSharedArgPropertySchema's 'string' arm to forward schema.stringConstraints instead of the hardcoded stringConstraints: undefined placeholder — this is the precise spot that lets env/stdin resolution honour the constraints via the shared coercer.
    Task 3: Test the string-constraint parity

    Mirror the flag coverage so the new surface is pinned down.

    • Add arg.string() describe blocks to src/core/schema/string-constraints.test.ts (or the arg test file) paralleling the existing flag.string() blocks: options-object storage, chained modifiers, override/merge/immutability, @ts-expect-error kind guards, RangeError on bad bounds, and e2e coverage through command()/runCommand() for CLI (INVALID_VALUE) and env (CONSTRAINT_VIOLATED) sourcing.
    🤖 Prompt for AI agents
    Implement string-constraint parity for positional args by mirroring the existing
    flag wiring exactly, so both parse-time and resolve-time enforcement stay in
    sync.
    
    First, extend the schema layer:
    - Add a `stringConstraints: StringConstraints | undefined` field to the
    `ArgSchema` interface in `src/core/schema/arg.ts`, plus a corresponding default
    `undefined` key in `createArgSchema`.
    - Give `arg.string()` an optional `constraints?: StringConstraints` parameter
    that runs `assertStringConstraints` at construction time before passing it into
    `createArgSchema`, mirroring `arg.number(constraints)`.
    - Add `.nonEmpty()`, `.minLength()`, `.maxLength()`, `.pattern()` modifier
    methods to `ArgBuilder`, copied in shape from `FlagBuilder` in
    `src/core/schema/flag.ts`, swapping the compile-time kind guard to `this:
    ArgBuilder<C & { argKind: 'string' }>` and re-validating via
    `assertStringConstraints` on merge.
    - Confirm `StringConstraints` is exported from `core/schema/index.ts` and root
    `src/index.ts`; add it alphabetically if missing.
    
    Next, wire enforcement through both coercion layers:
    - Update the `'string'` case of `coerceArgValue` in `src/core/parse/index.ts`
    (currently a bare `return raw;`) to call
    `validateStringConstraints`/`describeStringConstraintViolation` and throw a
    `ParseError`, mirroring `coerceFlagValue`'s `'string'` case with arg-flavoured
    error details/labels.
    - In `src/core/resolve/property.ts`, change `toSharedArgPropertySchema`'s
    `'string'` arm to forward `schema.stringConstraints` instead of the hardcoded
    `stringConstraints: undefined` placeholder.
    
    Finally, add tests: mirror the existing `flag.string()` describe blocks for
    `arg.string()` in `src/core/schema/string-constraints.test.ts` (or the arg test
    file) — covering options-object storage, chained modifiers,
    override/merge/immutability, `@ts-expect-error` kind guards, `RangeError` on bad
    bounds, and e2e coverage through `command()`/`runCommand()` for CLI
    (`INVALID_VALUE`) and env (`CONSTRAINT_VIOLATED`) sourcing.
    

    Phase 2: Purpose-built value kinds for positionals (url/path/date/duration/bytes)

    This be the part that stops folk hand-rolling exactly what flag.url() already does. Four of these (url/date/duration/bytes) are cheap sugar over the existing 'custom' kind — they reuse value-parsers.ts untouched and need no new coercion cases. path() be the heavier one, since args have no pathChecks concept nor a post-resolution filesystem check yet, so it needs the resolve-side plumbing mirrored from flags.

    Task 1: Add arg.url(), arg.date(), arg.duration(), arg.bytes() as custom-kind sugar

    Reuse the kind-agnostic parsers so positionals get the same declarative typing flags enjoy.

    • Add a valueHint: string | undefined field to ArgSchema (+ createArgSchema default) in src/core/schema/arg.ts for help placeholders.
    • Add the four factories to both the ArgFactory interface and the arg object literal (keep the duplicate signatures in sync), each building createArgSchema('custom', { parseFn: (raw) => parseXValue(raw, options), valueHint }) from value-parsers.ts — a direct mirror of the flag.url()/flag.date() etc. sugar.
    • No change to coerceArgValue is required: its existing 'custom' case already delegates to schema.parseFn.
    Task 2: Add arg.path() with pathChecks

    Give positional <file>/<dir> arguments the same filesystem-aware kind flags have.

    • Add pathChecks: PathChecks | undefined to ArgSchema (+ createArgSchema default) in src/core/schema/arg.ts.
    • Add an arg.path(options?) factory building a 'string'-kind schema carrying pathChecks and valueHint: 'path', mirroring flag.path(), including the type-level rejection of create without type: 'directory'.
    Task 3: Post-resolution path checks in the arg resolution chain

    The filesystem checks live after resolution, so this mirrors the flag pattern into the arg pipeline.

    • Replicate resolve/flags.ts's validatePathChecks (using the runtime-agnostic StatFn/MkdirFn seams) into a post-resolution pass in src/core/resolve/args.ts.
    • Thread stat/mkdir from src/core/resolve/index.ts into resolveArgs (currently these are only handed to resolveFlags), keeping resolveArgs synchronous where possible and matching how flags obtain the runtime adapter functions.
    Task 4: Serialise the new arg metadata in help and JSON Schema

    Keep the generated artefacts honest — a perfectionist does not leave dreamcli.schema.json lying.

    • Extend formatValueHint (and the positional-formatting helpers) in src/core/help/index.ts so args render <url>/<path>/<date>/<size> etc. from valueHint, instead of falling through to the generic <value>.
    • Extend serializeArgEntry and argKindToType in src/core/json-schema/index.ts to emit valueHint/pathChecks, and update the hardcoded arg $def in the definitionMetaSchema literal (the enums are hand-written, not derived from ARG_KINDS).
    • Regenerate dreamcli.schema.json via bun run schema:emit (do not hand-edit the artefact).
    Task 5: Test the new value kinds

    Mirror the dedicated flag-sugar test files for args.

    • Add arg sugar blocks to src/core/schema/value-parsers.test.ts (or the arg test file): arg.url()/date()/duration()/bytes() e2e via runCommand() (rejecting bad input with exit code 2) and InferArg type-inference assertions.
    • Add arg.path() blocks paralleling src/core/resolve/resolve-path-checks.test.ts: builder-schema shape (valueHint/pathChecks, type-level rejection of invalid create), resolve-time stat/mkdir behaviour through the test adapter, and end-to-end runCommand coverage.
    🤖 Prompt for AI agents
    Implement the purpose-built value kinds (`url`, `path`, `date`, `duration`,
    `bytes`) for positional args, reusing existing kind-agnostic parsers wherever
    possible and mirroring the flag equivalents.
    
    First, add the cheap sugar kinds:
    - Add a `valueHint: string | undefined` field to `ArgSchema` (+
    `createArgSchema` default) in `src/core/schema/arg.ts` for help placeholders.
    - Add `arg.url()`, `arg.date()`, `arg.duration()`, `arg.bytes()` to both the
    `ArgFactory` interface and the `arg` object literal (keeping the duplicate
    signatures in sync), each building `createArgSchema('custom', { parseFn: (raw)
    => parseXValue(raw, options), valueHint })` using the parsers from
    `value-parsers.ts`, mirroring the `flag.url()`/`flag.date()` sugar. No change to
    `coerceArgValue` is needed since its `'custom'` case already delegates to
    `schema.parseFn`.
    
    Next, add the heavier `arg.path()` kind:
    - Add `pathChecks: PathChecks | undefined` to `ArgSchema` (+ `createArgSchema`
    default).
    - Add an `arg.path(options?)` factory building a `'string'`-kind schema carrying
    `pathChecks` and `valueHint: 'path'`, mirroring `flag.path()`, including the
    type-level rejection of `create` without `type: 'directory'`.
    
    Then wire in resolve-side plumbing:
    - Replicate `resolve/flags.ts`'s `validatePathChecks` (using the
    runtime-agnostic `StatFn`/`MkdirFn` seams) as a post-resolution pass in
    `src/core/resolve/args.ts`.
    - Thread `stat`/`mkdir` from `src/core/resolve/index.ts` into `resolveArgs`
    (currently only handed to `resolveFlags`), keeping `resolveArgs` synchronous
    where possible.
    
    Then update generated artefacts:
    - Extend `formatValueHint` and positional-formatting helpers in
    `src/core/help/index.ts` so args render hints like
    `<url>`/`<path>`/`<date>`/`<size>` from `valueHint` instead of falling through
    to a generic `<value>`.
    - Extend `serializeArgEntry` and `argKindToType` in
    `src/core/json-schema/index.ts` to emit `valueHint`/`pathChecks`, and update the
    hardcoded `arg` `$def` in the `definitionMetaSchema` literal.
    - Regenerate `dreamcli.schema.json` via `bun run schema:emit` — do not hand-edit
    the artefact.
    
    Finally, add tests: arg sugar blocks in `src/core/schema/value-parsers.test.ts`
    (or the arg test file) for `arg.url()/date()/duration()/bytes()` e2e via
    `runCommand()` (rejecting bad input with exit code `2`) plus `InferArg`
    type-inference assertions; and `arg.path()` blocks paralleling
    `src/core/resolve/resolve-path-checks.test.ts` covering builder-schema shape,
    type-level rejection of invalid `create`, resolve-time `stat`/`mkdir` behaviour
    via the test adapter, and end-to-end `runCommand` coverage.
    

    Phase 3: Documentation, examples, and stating the intentional asymmetry

    This be the bit the ticket demands regardless of how much gets ported: nobody should ever have to deno doc --json and diff two factories to learn which gaps are deliberate. This phase records the new parity, states the remaining flag-only divergences with rationale, and points to the sanctioned arg.custom() + Standard Schema workaround.

    Task 1: Document the new arg kinds/constraints and an explicit asymmetry table

    Make docs/guide/arguments.md the single place that answers "why doesn't arg.X() exist?".

    • Document the new arg.string() constraints and arg.url()/path()/date()/duration()/bytes() kinds in docs/guide/arguments.md, cross-linking the shared constraint/parser tables in docs/guide/flags.md as the numeric-constraint section already does.
    • Add an explicit "flag-only, by design" table listing boolean, count, array (served by .variadic()), negatable, alias, duplicates, separator, unique, propagate, config, keyValue, and prompt, each with a one-line rationale, and name arg.custom() + a Standard Schema (noting its two gaps: no valueHint, no pathChecks) as the interim workaround for anything genuinely missing.
    Task 2: Cross-reference from flags docs and the CLI-creation skill

    Keep the guidance consistent across the two guides and the skill.

    • Update docs/guide/flags.md to note the newly-shared kinds/constraints now exist on arg.* too.
    • Update the "Values" paragraph in skills/cli-creation/SKILL.md to extend its "prefer a purpose-built kind" advice to args and to state which sugar kinds remain flag-only.
    Task 3: Add a parallel examples/arg-types.ts

    Follow the idiomatic single-feature example convention set by examples/flag-types.ts.

    • Create examples/arg-types.ts showcasing the new positional kinds and string constraints, importing public exports only, so the docs data pipeline (docs/.vitepress/data/examples.ts) can surface it.
    Task 4: Record the change and author the missing arg checklist

    Close the loop for future contributors.

    • Add an ### Added entry under ## [Unreleased] in CHANGELOG.md, following the bold-backtick-name + em-dash bullet house style.
    • Author an "ADDING AN ARG TYPE" checklist in src/core/schema/AGENTS.md by analogy with the existing "ADDING A FLAG TYPE" checklist, substituting the arg-specific files, so this drift never silently reopens.
    🤖 Prompt for AI agents
    Document the newly-added arg parity and explicitly record the remaining
    intentional flag-only divergences, and keep examples/changelog/checklists in
    sync.
    
    Documentation:
    - In `docs/guide/arguments.md`, document the new `arg.string()` constraints and
    `arg.url()/path()/date()/duration()/bytes()` kinds, cross-linking the shared
    constraint/parser tables in `docs/guide/flags.md` as the numeric-constraint
    section already does.
    - Add an explicit "flag-only, by design" table in `docs/guide/arguments.md`
    listing `boolean`, `count`, `array` (served by `.variadic()`), `negatable`,
    `alias`, `duplicates`, `separator`, `unique`, `propagate`, `config`, `keyValue`,
    and `prompt`, each with a one-line rationale, and name `arg.custom()` + a
    Standard Schema (noting its two gaps: no `valueHint`, no `pathChecks`) as the
    interim workaround for anything genuinely missing.
    - Update `docs/guide/flags.md` to note that the newly-shared kinds/constraints
    now exist on `arg.*` too.
    - Update the "Values" paragraph in `skills/cli-creation/SKILL.md` to extend its
    "prefer a purpose-built kind" advice to args and to state which sugar kinds
    remain flag-only.
    
    Examples:
    - Create `examples/arg-types.ts` showcasing the new positional kinds and string
    constraints, importing public exports only, following the single-feature example
    convention set by `examples/flag-types.ts`, so the docs data pipeline
    (`docs/.vitepress/data/examples.ts`) can surface it.
    
    Changelog and checklist:
    - Add an `### Added` entry under `## [Unreleased]` in `CHANGELOG.md`, following
    the bold-backtick-name + em-dash bullet house style.
    - Author an "ADDING AN ARG TYPE" checklist in `src/core/schema/AGENTS.md` by
    analogy with the existing "ADDING A FLAG TYPE" checklist, substituting the
    arg-specific files, so this drift never silently reopens.
    
    Research

    The schema layer (src/core/schema/) hosts two independent, non-inheriting builder families: arg.ts (ArgFactory/ArgBuilder/ArgSchema) and flag.ts (FlagFactory/FlagBuilder/FlagSchema). They duplicate the same immutable "clone-and-override" modifier pattern rather than share a base. Value parsing sugar (parseUrlValue/parseDateValue/parseDurationValue/parseBytesValue) lives kind-agnostically in value-parsers.ts, and constraint validators live in string-constraints.ts/number-constraints.ts as single-source-of-truth helpers. Coercion happens in two never-overlapping layers: parse/index.ts (CLI argv only) and resolve/coerce.ts (env/stdin/config/prompt), with resolve/property.ts providing the one shared SharedPropertySchema seam for the string|number|enum|custom kinds. Help rendering (help/index.ts), JSON-Schema emission (json-schema/index.ts → dreamcli.schema.json), docs (docs/guide/), examples (examples/), and the schema AGENTS.md checklist round out the surfaces that any type addition must touch.


    🚀 Next Steps

    🤖 All AI agent prompts combined
    Task: 1
    
    Implement string-constraint parity for positional args by mirroring the existing
    flag wiring exactly, so both parse-time and resolve-time enforcement stay in
    sync.
    
    First, extend the schema layer:
    - Add a `stringConstraints: StringConstraints | undefined` field to the
    `ArgSchema` interface in `src/core/schema/arg.ts`, plus a corresponding default
    `undefined` key in `createArgSchema`.
    - Give `arg.string()` an optional `constraints?: StringConstraints` parameter
    that runs `assertStringConstraints` at construction time before passing it into
    `createArgSchema`, mirroring `arg.number(constraints)`.
    - Add `.nonEmpty()`, `.minLength()`, `.maxLength()`, `.pattern()` modifier
    methods to `ArgBuilder`, copied in shape from `FlagBuilder` in
    `src/core/schema/flag.ts`, swapping the compile-time kind guard to `this:
    ArgBuilder<C & { argKind: 'string' }>` and re-validating via
    `assertStringConstraints` on merge.
    - Confirm `StringConstraints` is exported from `core/schema/index.ts` and root
    `src/index.ts`; add it alphabetically if missing.
    
    Next, wire enforcement through both coercion layers:
    - Update the `'string'` case of `coerceArgValue` in `src/core/parse/index.ts`
    (currently a bare `return raw;`) to call
    `validateStringConstraints`/`describeStringConstraintViolation` and throw a
    `ParseError`, mirroring `coerceFlagValue`'s `'string'` case with arg-flavoured
    error details/labels.
    - In `src/core/resolve/property.ts`, change `toSharedArgPropertySchema`'s
    `'string'` arm to forward `schema.stringConstraints` instead of the hardcoded
    `stringConstraints: undefined` placeholder.
    
    Finally, add tests: mirror the existing `flag.string()` describe blocks for
    `arg.string()` in `src/core/schema/string-constraints.test.ts` (or the arg test
    file) — covering options-object storage, chained modifiers,
    override/merge/immutability, `@ts-expect-error` kind guards, `RangeError` on bad
    bounds, and e2e coverage through `command()`/`runCommand()` for CLI
    (`INVALID_VALUE`) and env (`CONSTRAINT_VIOLATED`) sourcing.
    ===============================================================================
    
    Task: 2
    
    Implement the purpose-built value kinds (`url`, `path`, `date`, `duration`,
    `bytes`) for positional args, reusing existing kind-agnostic parsers wherever
    possible and mirroring the flag equivalents.
    
    First, add the cheap sugar kinds:
    - Add a `valueHint: string | undefined` field to `ArgSchema` (+
    `createArgSchema` default) in `src/core/schema/arg.ts` for help placeholders.
    - Add `arg.url()`, `arg.date()`, `arg.duration()`, `arg.bytes()` to both the
    `ArgFactory` interface and the `arg` object literal (keeping the duplicate
    signatures in sync), each building `createArgSchema('custom', { parseFn: (raw)
    => parseXValue(raw, options), valueHint })` using the parsers from
    `value-parsers.ts`, mirroring the `flag.url()`/`flag.date()` sugar. No change to
    `coerceArgValue` is needed since its `'custom'` case already delegates to
    `schema.parseFn`.
    
    Next, add the heavier `arg.path()` kind:
    - Add `pathChecks: PathChecks | undefined` to `ArgSchema` (+ `createArgSchema`
    default).
    - Add an `arg.path(options?)` factory building a `'string'`-kind schema carrying
    `pathChecks` and `valueHint: 'path'`, mirroring `flag.path()`, including the
    type-level rejection of `create` without `type: 'directory'`.
    
    Then wire in resolve-side plumbing:
    - Replicate `resolve/flags.ts`'s `validatePathChecks` (using the
    runtime-agnostic `StatFn`/`MkdirFn` seams) as a post-resolution pass in
    `src/core/resolve/args.ts`.
    - Thread `stat`/`mkdir` from `src/core/resolve/index.ts` into `resolveArgs`
    (currently only handed to `resolveFlags`), keeping `resolveArgs` synchronous
    where possible.
    
    Then update generated artefacts:
    - Extend `formatValueHint` and positional-formatting helpers in
    `src/core/help/index.ts` so args render hints like
    `<url>`/`<path>`/`<date>`/`<size>` from `valueHint` instead of falling through
    to a generic `<value>`.
    - Extend `serializeArgEntry` and `argKindToType` in
    `src/core/json-schema/index.ts` to emit `valueHint`/`pathChecks`, and update the
    hardcoded `arg` `$def` in the `definitionMetaSchema` literal.
    - Regenerate `dreamcli.schema.json` via `bun run schema:emit` — do not hand-edit
    the artefact.
    
    Finally, add tests: arg sugar blocks in `src/core/schema/value-parsers.test.ts`
    (or the arg test file) for `arg.url()/date()/duration()/bytes()` e2e via
    `runCommand()` (rejecting bad input with exit code `2`) plus `InferArg`
    type-inference assertions; and `arg.path()` blocks paralleling
    `src/core/resolve/resolve-path-checks.test.ts` covering builder-schema shape,
    type-level rejection of invalid `create`, resolve-time `stat`/`mkdir` behaviour
    via the test adapter, and end-to-end `runCommand` coverage.
    ===============================================================================
    
    Task: 3
    
    Document the newly-added arg parity and explicitly record the remaining
    intentional flag-only divergences, and keep examples/changelog/checklists in
    sync.
    
    Documentation:
    - In `docs/guide/arguments.md`, document the new `arg.string()` constraints and
    `arg.url()/path()/date()/duration()/bytes()` kinds, cross-linking the shared
    constraint/parser tables in `docs/guide/flags.md` as the numeric-constraint
    section already does.
    - Add an explicit "flag-only, by design" table in `docs/guide/arguments.md`
    listing `boolean`, `count`, `array` (served by `.variadic()`), `negatable`,
    `alias`, `duplicates`, `separator`, `unique`, `propagate`, `config`, `keyValue`,
    and `prompt`, each with a one-line rationale, and name `arg.custom()` + a
    Standard Schema (noting its two gaps: no `valueHint`, no `pathChecks`) as the
    interim workaround for anything genuinely missing.
    - Update `docs/guide/flags.md` to note that the newly-shared kinds/constraints
    now exist on `arg.*` too.
    - Update the "Values" paragraph in `skills/cli-creation/SKILL.md` to extend its
    "prefer a purpose-built kind" advice to args and to state which sugar kinds
    remain flag-only.
    
    Examples:
    - Create `examples/arg-types.ts` showcasing the new positional kinds and string
    constraints, importing public exports only, following the single-feature example
    convention set by `examples/flag-types.ts`, so the docs data pipeline
    (`docs/.vitepress/data/examples.ts`) can surface it.
    
    Changelog and checklist:
    - Add an `### Added` entry under `## [Unreleased]` in `CHANGELOG.md`, following
    the bold-backtick-name + em-dash bullet house style.
    - Author an "ADDING AN ARG TYPE" checklist in `src/core/schema/AGENTS.md` by
    analogy with the existing "ADDING A FLAG TYPE" checklist, substituting the
    arg-specific files, so this drift never silently reopens.
    
    💡 Iterate on the plan with: `@coderabbitai` <feedback>
    Example Feedback
    - `@coderabbitai` You can skip phase 3. Add a simple unit test case for phase 2.
    - `@coderabbitai` For design choice 1 go ahead with option 3 and replan.
    

    💬 Have feedback or questions? Drop into our discord!

  4. self-assigned this
    on Aug 3, 2026
  5. added this to the v4 milestone on Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area: cliCLI dispatch, planning, root UX, or pluginsarea: resolveArgv, env, config, prompt, and default resolutionkind: bugBroken behavior or regression

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions