feat: extend ValueType with url/path/json + parse escape hatch#129
feat: extend ValueType with url/path/json + parse escape hatch#129chenxin-yan wants to merge 14 commits into
Conversation
… parse escape hatch types
…ference/parse?: never
…ices enforcement + default coercion
…ath, suppress for url/json
Greptile SummaryThis PR extends
Confidence Score: 5/5The 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
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
Prompt To Fix All With AIFix 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 |
…, 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).
…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.
Summary
TP-012 — extend
ValueTypewith three formatted built-ins ("url","path","json") and add aparse?escape hatch on string flags/args for custom transformations. Also wires upchoicesparse-time enforcement (previously hint-only), fixes the oracle-C default-coercion bug, and teaches the completion plugin to emit file-completion candidates forpathwhile explicitly suppressing them forurlandjson.What changed
@crustjs/coreValueType→"string" | "number" | "boolean" | "url" | "path" | "json"Resolve<T>+ResolveBaseType<F>type-resolver helpersUrlFlagDef/PathFlagDef/JsonFlagDef(+ multi + ArgDef variants)parse?: (raw: string) => unknownonStringFlagDef/StringMultiFlagDef/StringArgDef;parse?: neveron every non-string variant (compile-time guard)coerceUrl,coercePath,coerceJson(packages/core/src/coercers.ts)CONFIGerror code inCrustErrorDetailsMap; asyncparsefunctions are rejected at command setup withCrustError("CONFIG", …)parseis set and argv is absent butdefaultis present,parse(String(default))now runs so the runtime value matches the inferred typechoiceson string flags/args is now enforced at parse time (was hint-only). Raw argv is validated againstchoicesbefore anyparsetransform runs; values outside the list throwCrustError("PARSE", …)@crustjs/pluginstype: "path"(bashcompgen -f, zsh_files, fish__fish_complete_path)type: "url"andtype: "json"— the string fallback used to offer filenames for any value-taking string flag, which is semantically wrong for URLs and JSON literalsDocs
guide/built-in-types.mdx(the six types + theparseescape hatch contract) andguide/recipes.mdx(≥10 copy-paste recipes for deferred types — date, duration, port, regex, count, bytes, bigint, hex, base64, file, dir)guide/meta.jsonupdated with both pagesguide/flags.mdx,guide/arguments.mdx,api/types.mdx,modules/core.mdx,modules/plugins/completion.mdx,packages/core/README.md,packages/plugins/README.mdall updated for the new surfaceapi/types.mdx"hint-only" disclaimer onchoicesremoved (now enforced)Other surgical follow-ups
packages/skills/src/manifest.ts: addedmanifestType()helper to collapseurl|path|json → "string"at the manifest layer (downstream consumer ofFlagDef.type)packages/core/src/validation.ts:sampleToken()now useschoices[0]when present, plus sane samples for the new built-ins, so synthetic-argv tree validation still satisfies the now-enforcedchoicesgatepackages/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 messageTest inventory
packages/core/src/coercers.test.ts— 16 tests covering all required + optional scenarios from the specpackages/core/src/parser.test.ts— 17 new tests across threedescribeblocks (url/path/json types, parse escape hatch, choices enforcement). Includes the oracle-C default-coercion regression and the choices-before-parse order testpackages/core/src/types.test.ts— 11 new type-level tests (Resolvemapping,parseinference, 5@ts-expect-errorcases forparse?: never)packages/plugins/src/completion/walker.test.ts— 3 new tests forisPath/isUrl/isJsonemission on flags and positional argspackages/plugins/src/completion/templates/{bash,zsh,fish}.test.ts— 3 new tests each for path emission + url/json suppression + no string regressionFinal gate
Changesets
.changeset/tp-012-core-valuetype-parse.md—@crustjs/core: patch.changeset/tp-012-plugins-completion-types.md—@crustjs/plugins: patch