Skip to content

feat: extend ValueType with url/path/json + parse escape hatch#129

Open
chenxin-yan wants to merge 14 commits into
mainfrom
feat/more-types
Open

feat: extend ValueType with url/path/json + parse escape hatch#129
chenxin-yan wants to merge 14 commits into
mainfrom
feat/more-types

Conversation

@chenxin-yan
Copy link
Copy Markdown
Owner

Summary

TP-012 — extend ValueType with three formatted built-ins ("url", "path", "json") and add a parse? escape hatch on string flags/args for custom transformations. Also wires up choices parse-time enforcement (previously hint-only), fixes the oracle-C default-coercion bug, and teaches the completion plugin to emit file-completion candidates for path while explicitly suppressing them for url and json.

What changed

@crustjs/core

  • ValueType"string" | "number" | "boolean" | "url" | "path" | "json"
  • New Resolve<T> + ResolveBaseType<F> type-resolver helpers
  • New UrlFlagDef / PathFlagDef / JsonFlagDef (+ multi + ArgDef variants)
  • parse?: (raw: string) => unknown on StringFlagDef / StringMultiFlagDef / StringArgDef; parse?: never on every non-string variant (compile-time guard)
  • New built-in coercers: coerceUrl, coercePath, coerceJson (packages/core/src/coercers.ts)
  • New CONFIG error code in CrustErrorDetailsMap; async parse functions are rejected at command setup with CrustError("CONFIG", …)
  • Bugfix (oracle C): when parse is set and argv is absent but default is present, parse(String(default)) now runs so the runtime value matches the inferred type
  • Behavior change: choices on string flags/args is now enforced at parse time (was hint-only). Raw argv is validated against choices before any parse transform runs; values outside the list throw CrustError("PARSE", …)

@crustjs/plugins

  • Completion templates now emit file completion for type: "path" (bash compgen -f, zsh _files, fish __fish_complete_path)
  • File completion is explicitly suppressed for type: "url" and type: "json" — the string fallback used to offer filenames for any value-taking string flag, which is semantically wrong for URLs and JSON literals

Docs

  • New guide pages: guide/built-in-types.mdx (the six types + the parse escape hatch contract) and guide/recipes.mdx (≥10 copy-paste recipes for deferred types — date, duration, port, regex, count, bytes, bigint, hex, base64, file, dir)
  • guide/meta.json updated with both pages
  • guide/flags.mdx, guide/arguments.mdx, api/types.mdx, modules/core.mdx, modules/plugins/completion.mdx, packages/core/README.md, packages/plugins/README.md all updated for the new surface
  • api/types.mdx "hint-only" disclaimer on choices removed (now enforced)

Other surgical follow-ups

  • packages/skills/src/manifest.ts: added manifestType() helper to collapse url|path|json → "string" at the manifest layer (downstream consumer of FlagDef.type)
  • packages/core/src/validation.ts: sampleToken() now uses choices[0] when present, plus sane samples for the new built-ins, so synthetic-argv tree validation still satisfies the now-enforced choices gate
  • packages/plugins/src/completion/index.test.ts: the "unsupported shell" test was asserting the run handler's message; with parse-time choices enforcement the parser intercepts first, so the assertion was updated to match the new (clearer) parser message

Test inventory

  • packages/core/src/coercers.test.ts — 16 tests covering all required + optional scenarios from the spec
  • packages/core/src/parser.test.ts — 17 new tests across three describe blocks (url/path/json types, parse escape hatch, choices enforcement). Includes the oracle-C default-coercion regression and the choices-before-parse order test
  • packages/core/src/types.test.ts — 11 new type-level tests (Resolve mapping, parse inference, 5 @ts-expect-error cases for parse?: never)
  • packages/plugins/src/completion/walker.test.ts — 3 new tests for isPath/isUrl/isJson emission on flags and positional args
  • packages/plugins/src/completion/templates/{bash,zsh,fish}.test.ts — 3 new tests each for path emission + url/json suppression + no string regression

Final gate

✅ bun run check
✅ bun run check:types
✅ bun run test
✅ bun run build
✅ bun run build:docs

Changesets

  • .changeset/tp-012-core-valuetype-parse.md@crustjs/core: patch
  • .changeset/tp-012-plugins-completion-types.md@crustjs/plugins: patch

Note: the core changeset is patch per operator decision. The shipped surface is purely additive — no existing public type is narrowed or removed. The one behavior change (choices enforcement) was previously documented as not-yet-enforced in the JSDoc.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 19, 2026

Greptile Summary

This PR extends ValueType with three formatted built-ins ("url", "path", "json"), adds a parse? escape hatch on string flags/args for custom transformations, promotes choices from hint-only to enforced at parse time, and fixes default-coercion symmetry so omitted flags produce the same resolved value as explicitly passed ones.

  • @crustjs/core: new coercers.ts (URL/path/JSON), resolveDefault to mirror argv-side coercion for defaults, validateAsyncParse guard, CONFIG error code, updated InferFlagValue/InferArgValue to use { default: unknown } presence-check for correct parse+default inference, and sampleToken now satisfies the enforced choices gate.
  • @crustjs/plugins: completion walker normalises url/path/json to "string" + valueCompletion intent; bash/zsh/fish templates emit file completion for path and suppress it for url/json; the "unsupported shell" test updated to match the parser-level choices error message.
  • Docs: new guide/types.mdx with full semantics and ≥10 parse recipes; all existing guide, API, and module pages updated to reflect the six built-in types and enforced choices.

Confidence Score: 5/5

The change is purely additive on the public API surface; the one behaviour change (choices enforcement) was previously documented as not-yet-enforced and all existing tests pass. The implementation is well-tested with 47+ new tests across coercers, parser, types, and completion templates.

All three previously-flagged review concerns have been addressed (path-default coercePath symmetry, missing-protocol hint scoping, choices enforcement on defaults). No new runtime defects were found. The only remaining open item is a doc wording gap in the "url" quick-reference table that does not affect runtime behaviour.

apps/docs/content/docs/guide/types.mdx — the "url" quick-reference row's error description is narrower than the actual coercer behaviour.

Important Files Changed

Filename Overview
packages/core/src/parser.ts Core parsing logic extended with choices enforcement, parse escape hatch, new coercers for url/path/json, and resolveDefault to mirror argv-side coercion for defaults. Well-structured with good test coverage.
packages/core/src/coercers.ts New file adding coerceUrl, coercePath, and coerceJson. Implements the missing-protocol hint check correctly via scheme regex; edge cases (empty input, tilde, traversal) are all handled.
packages/core/src/types.ts Extends ValueType, adds Resolve<T> and ResolveBaseType<F> helpers, new flag/arg def interfaces, and updates InferFlagValue/InferArgValue to use { default: unknown } presence-check to correctly handle parse + raw-string defaults.
packages/core/src/validation.ts Updates sampleToken to use choices[0] when present (satisfying the now-enforced choices gate) and adds sane samples for url and json types so synthetic-argv tree validation still works.
packages/plugins/src/completion/templates/bash.ts Adds valueTypeCases and argSuppressEntries collection to emit explicit compgen -f for path flags/args and compopt +o default suppression for url/json. Both --name=value and separate-token completion contexts are handled.
packages/plugins/src/completion/walker.ts Normalises url/path/json flag and arg types to "string" in the spec layer and sets `valueCompletion: "files"
packages/skills/src/manifest.ts Adds manifestType() helper to collapse url/path/json → "string" at the manifest layer until the manifest schema is extended.
apps/docs/content/docs/guide/types.mdx New guide covering all six ValueType literals, the parse escape hatch, and ≥10 copy-paste recipes. Quick-reference table entry for "url" only calls out the missing-protocol error case rather than the general "invalid URL" failure mode.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[argv token / default value] --> B{Has parse fn?}
    B -- yes --> C{choices set?}
    B -- no --> D{choices set?}
    C -- yes --> E[validateChoice raw token]
    C -- no --> F[invokeParse raw]
    E -- pass --> F
    E -- fail --> ERR1[CrustError PARSE]
    F --> OUT[resolved value]
    D -- yes --> G[validateChoice raw token]
    D -- no --> H{ValueType?}
    G -- pass --> H
    G -- fail --> ERR2[CrustError PARSE]
    H -- number --> I[tryCoerceNumber]
    H -- url --> J[coerceUrl - new URL]
    H -- path --> K[coercePath - resolve + tilde]
    H -- json --> L[coerceJson - JSON.parse]
    H -- boolean --> M[coerceBooleanString]
    H -- string --> N[raw string]
    I & J & K & L & M & N --> OUT
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/docs/content/docs/guide/types.mdx:244
**Quick-reference `"url"` error description is narrower than the actual behavior**

The Coercion column says "throws `PARSE` on missing protocol", but `coerceUrl` throws on *any* WHATWG-invalid URL — e.g. `https://[bad` throws a `PARSE` error without the missing-protocol hint. A user who passes a malformed URL that does carry a scheme (e.g. from a config file) would get an unmentioned error. The `"json"` row in the same table uses the more accurate "throws `PARSE` on invalid JSON" phrasing, which avoids this mismatch. The prose below the table also only calls out the missing-protocol case, leaving the broader failure mode undocumented.

Reviews (4): Last reviewed commit: "fix(TP-012): review-driven cleanup — com..." | Re-trigger Greptile

Comment thread packages/core/src/parser.ts Outdated
Comment thread packages/core/src/coercers.ts
Comment thread packages/core/src/parser.ts
…, error wording

Followup pass on the TP-012 PR addressing review findings.

Behavioural fixes:

- types.ts: InferFlagValue/InferArgValue narrow on `default` presence
  (`F extends { default: unknown }` / `readonly unknown[]`) instead of
  the parsed type. Previously `{ parse: Number, default: "3000" }`
  inferred `number | undefined` because the typed-default check
  evaluated to `F extends { default: number }` while the raw default is
  `string`. The runtime already coerces `String(default)` through
  `parse`, so the type now matches the runtime contract. Regression
  tests added for single + multi.
- completion templates: positional `path`/`url`/`json` args now get the
  same handling as flags. zsh emits `_files` for path positionals and
  ` ` (noop) for url/json. fish emits `(__fish_complete_path)` per
  path slot; url/json suppression is implicit via the global `-f`.
  bash adds `collectArgSuppressCases` so url/json slots emit
  `compopt +o default` to disable the file fallback; path positionals
  continue to use the fallback. Matches the contract the
  `tp-012-plugins-completion-types` changeset already promises.
- coercers.ts: `coerceUrl` only appends the "missing protocol" hint
  when the input lacks a URL scheme. Previously every `new URL()`
  failure claimed the protocol was missing, which was misleading for
  inputs like `https://[bad`.

Cleanup:

- types.ts: collapse duplicate parse JSDoc on ArgDefBase to a one-line
  reference to StringFlagDef.parse.
- types.test.ts: collapse five single-line `parse?: never` tests into
  one block.
- parser.test.ts: drop formatter re-asserts (`.href`,
  `.startsWith("/")`, `.endsWith("/dist")`, redundant `typeof` checks)
  already covered in coercers.test.ts.
- coercers.test.ts: drop the duplicate `coerceUrl("example.com")` test;
  add a focused test for the scheme-present case.
- spec.ts: collapse near-identical isPath/isUrl/isJson JSDoc blocks.
- recipes.mdx: trim the "the pattern is always the same" closing
  checklist to the two pieces of new information (raw-string defaults
  + reusing parsers).
@chenxin-yan chenxin-yan changed the title feat(TP-012): extend ValueType with url/path/json + parse escape hatch feat: extend ValueType with url/path/json + parse escape hatch May 20, 2026
…re-exports, doc fixes

Address PR #129 review (Greptile + adversarial reviewers).

Parser — make the default branch mirror the argv pipeline so omitted-flag
behavior matches user-supplied behavior:
  raw default → choices validation → parse | coerce → result.
Extracted into resolveDefault() and called from both resolveFlags and
resolveArgs. Two observable fixes:
- type: "path" defaults now run through coercePath, so { default: "./dist" }
  yields the same absolute path as passing --out ./dist (Greptile C1 P1).
- choices is now validated against default values in both the parse and
  non-parse branches, closing the asymmetry where { choices: ["a","b"],
  default: "z" } silently returned "z" while --flag z threw (Greptile C4).

Types — re-export Resolve and ResolveBaseType from @crustjs/core root so the
documented helpers at docs/api/types#resolve* are actually importable.

Docs — remove the broken Count recipe (type: "string" + multiple + parse:
() => 1 cannot implement -vvv because string flags require a value; the
underlying parseArgs rejects -vvv outright). Fix built-in-types.mdx
overclaim that bash uses compgen -f for path positionals; positionals
actually rely on the complete -o default fallback, matching the
existing modules/plugins/completion table.

Tests — new "parseArgs — default coercion symmetry" describe block
covering path-default flag/multi-flag/arg, choices-before-parse on
defaults, non-parse choices-on-default, and a positive control.
Follow-up cleanup pass after the parallel deslop + verbosity review of
the TP-012 PR.

- apps/docs/content/docs/guide/arguments.mdx, flags.mdx: the `choices`
  guide paragraphs still claimed it was a tooling-only hint not enforced
  at parse time, contradicting the new behavior and the API reference.
  Rewrite to match: validated at parse time, throws `CrustError("PARSE",
  …)` before any `parse` transform runs.
- packages/plugins/src/completion/templates/bash.test.ts: hoist one
  shared `runBashCompletion(scriptPath, fnName, words)` helper plus a
  module-scope `shQuote`. Both subprocess describe blocks now keep a
  one-line wrapper bound to the suite's completion function name.
  Eliminates ~80 lines of duplicated bash-driver scaffolding.
- packages/core/src/parser.test.ts: collapse the two choices-rejection
  tests that invoked the same failing `parseArgs` twice (once for the
  class assertion, once for the message). Capture the error in a
  try/catch and assert class + message on the single thrown value.
…-loud parser + doc accuracy

- Collapse `isPath`/`isUrl`/`isJson` on `CompletionFlag`/`CompletionArg` into
  a single `valueCompletion?: "files" | "none"` field. The three booleans
  encoded one concept ("what kind of value completion is this?") and forced
  parallel `if/else if` ladders in every shell template. Walker now maps
  `type: "path"` → "files" and `type: "url" | "json"` → "none"; bash, zsh,
  and fish templates each branch on the single field. No user-visible
  behavior change — the spec is internal to `@crustjs/plugins`.

- Remove the silent `parsedValue === true` fallback in `coerceFlagValue`.
  With `util.parseArgs` configured `type: "string"` for every non-boolean
  flag and `strict: true`, that branch was unreachable; returning the
  default would have masked any future parser-configuration bug. Replaced
  with a fail-loud `CrustError("PARSE", …)` per AGENTS.md "no branches
  for impossible states".

- Fix `types.mdx` wording for path-positional completion. The old sentence
  said path positionals rely on each shell's default filename fallback and
  named `zsh _files` and `fish __fish_complete_path` — but those two
  shells emit explicit rules for path positionals; only bash uses the
  global `complete -o default` fallback. New wording reflects what each
  template actually emits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant