@crustjs/core 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +69 -14
- package/dist/index.js +54 -4
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -128,16 +128,25 @@ interface BooleanMultiFlagDef extends MultiFlagBase {
|
|
|
128
128
|
type FlagDef = StringFlagDef | NumberFlagDef | BooleanFlagDef | StringMultiFlagDef | NumberMultiFlagDef | BooleanMultiFlagDef;
|
|
129
129
|
/** Record mapping flag names to their definitions */
|
|
130
130
|
type FlagsDef = Record<string, FlagDef>;
|
|
131
|
-
/**
|
|
132
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Extract alias string literals from any value that has an `alias` field.
|
|
133
|
+
*
|
|
134
|
+
* Generalized to work with any shape (`FlagDef`, `FlagSpec`, etc.) —
|
|
135
|
+
* values without an `alias` field resolve to `never`.
|
|
136
|
+
*
|
|
137
|
+
* Includes a `string extends A` guard so non-narrowed aliases (e.g. the
|
|
138
|
+
* broad `string` type from a default generic) resolve to `never` instead
|
|
139
|
+
* of causing false-positive collisions.
|
|
140
|
+
*/
|
|
141
|
+
type ExtractAliases<F> = F extends {
|
|
133
142
|
alias: infer A;
|
|
134
|
-
} ? A extends string ? A : A extends readonly string[] ? A[number] : never : never;
|
|
143
|
+
} ? A extends string ? string extends A ? never : A : A extends readonly string[] ? string extends A[number] ? never : A[number] : never : never;
|
|
135
144
|
/**
|
|
136
145
|
* Collects aliases from every flag *except* flag K.
|
|
137
146
|
* Used to detect alias→alias duplicates across different flags.
|
|
138
147
|
*/
|
|
139
148
|
type AliasesExcluding<
|
|
140
|
-
F extends
|
|
149
|
+
F extends Record<string, unknown>,
|
|
141
150
|
K extends keyof F & string
|
|
142
151
|
> = { [J in Exclude<keyof F & string, K>] : ExtractAliases<F[J]> }[Exclude<keyof F & string, K>];
|
|
143
152
|
/**
|
|
@@ -146,13 +155,16 @@ type AliasesExcluding<
|
|
|
146
155
|
* or `never` when K's aliases are all unique.
|
|
147
156
|
*/
|
|
148
157
|
type CollidingAliases<
|
|
149
|
-
F extends
|
|
158
|
+
F extends Record<string, unknown>,
|
|
150
159
|
K extends keyof F & string
|
|
151
160
|
> = (ExtractAliases<F[K]> & Exclude<keyof F & string, K>) | (ExtractAliases<F[K]> & AliasesExcluding<F, K>);
|
|
152
161
|
/**
|
|
153
162
|
* Per-flag validation mapped type. Resolves to `F` when no collisions exist.
|
|
154
163
|
* For flags with colliding aliases, adds a branded error property to the
|
|
155
|
-
* specific flag definition, causing a type error on that flag's value
|
|
164
|
+
* specific flag definition, causing a type error on that flag's value.
|
|
165
|
+
*
|
|
166
|
+
* Generalized to work with any `Record<string, unknown>` shape — core uses
|
|
167
|
+
* it with `FlagsDef`, the validate package uses it with `FlagShape`, etc.
|
|
156
168
|
*
|
|
157
169
|
* ```
|
|
158
170
|
* Property 'FIX_ALIAS_COLLISION' is missing in type '{ type: "string"; alias: "minify" }'
|
|
@@ -160,13 +172,50 @@ type CollidingAliases<
|
|
|
160
172
|
* '{ readonly FIX_ALIAS_COLLISION: "Alias \"minify\" collides with another flag name or alias" }'.
|
|
161
173
|
* ```
|
|
162
174
|
*/
|
|
163
|
-
type ValidateFlagAliases<F extends
|
|
175
|
+
type ValidateFlagAliases<F extends Record<string, unknown>> = { [K in keyof F & string] : CollidingAliases<F, K> extends never ? F[K] : F[K] & {
|
|
164
176
|
readonly FIX_ALIAS_COLLISION: `Alias "${CollidingAliases<F, K>}" collides with another flag name or alias`;
|
|
165
177
|
} };
|
|
166
178
|
/**
|
|
179
|
+
* Detects whether a single alias literal starts with `"no-"`.
|
|
180
|
+
* Resolves to the offending alias, or `never` when it is clean.
|
|
181
|
+
*/
|
|
182
|
+
type NoPrefixedAlias<A> = A extends `no-${string}` ? A : never;
|
|
183
|
+
/**
|
|
184
|
+
* Collects all `"no-"`-prefixed alias literals from a flag definition.
|
|
185
|
+
* Works with both `alias: "no-foo"` (string) and `alias: ["no-foo", "f"]` (array).
|
|
186
|
+
* Non-narrowed `string` types resolve to `never` to avoid false positives.
|
|
187
|
+
*/
|
|
188
|
+
type NoPrefixedAliases<F> = F extends {
|
|
189
|
+
alias: infer A;
|
|
190
|
+
} ? A extends string ? string extends A ? never : NoPrefixedAlias<A> : A extends readonly string[] ? string extends A[number] ? never : NoPrefixedAlias<A[number]> : never : never;
|
|
191
|
+
/**
|
|
192
|
+
* Per-flag validation mapped type. Resolves to `F` when no `"no-"` prefixes
|
|
193
|
+
* exist on flag names or aliases. For flags with offending names or aliases,
|
|
194
|
+
* adds a branded error property causing a compile-time type error.
|
|
195
|
+
*
|
|
196
|
+
* The `"no-"` prefix is reserved for boolean flag negation (`--no-flag`).
|
|
197
|
+
* Define only the positive form (e.g. `cache`) and use `--no-cache` at runtime.
|
|
198
|
+
*
|
|
199
|
+
* ```
|
|
200
|
+
* Property 'FIX_NO_PREFIX' is missing in type '{ type: "boolean" }'
|
|
201
|
+
* but required in type
|
|
202
|
+
* '{ readonly FIX_NO_PREFIX: "Flag name \"no-cache\" must not start with \"no-\"; define \"cache\" instead and use \"--no-cache\" at runtime" }'.
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
type ValidateNoPrefixedFlags<F extends Record<string, unknown>> = { [K in keyof F & string] : K extends `no-${infer Base}` ? F[K] & {
|
|
206
|
+
readonly FIX_NO_PREFIX: `Flag name "${K}" must not start with "no-"; define "${Base}" instead and use "--no-${Base}" at runtime`;
|
|
207
|
+
} : NoPrefixedAliases<F[K]> extends never ? F[K] : F[K] & {
|
|
208
|
+
readonly FIX_NO_PREFIX: `Alias "${NoPrefixedAliases<F[K]>}" must not start with "no-"; the "no-" prefix is reserved for boolean negation`;
|
|
209
|
+
} };
|
|
210
|
+
/**
|
|
167
211
|
* Per-arg validation tuple type. Resolves to `A` when the constraint is
|
|
168
212
|
* satisfied (only the last arg is variadic). For non-last args that have
|
|
169
|
-
* `variadic: true`, adds a branded error property to the specific arg
|
|
213
|
+
* `variadic: true`, adds a branded error property to the specific arg.
|
|
214
|
+
*
|
|
215
|
+
* Generalized to work with any ordered tuple of object-typed definitions —
|
|
216
|
+
* core uses it with `ArgsDef`, the validate package uses it with
|
|
217
|
+
* `ArgSpec[]`, etc. Uses `readonly object[]` to avoid TypeScript's weak
|
|
218
|
+
* type detection (all-optional constraint rejection).
|
|
170
219
|
*
|
|
171
220
|
* ```
|
|
172
221
|
* Property 'FIX_VARIADIC_POSITION' is missing in type '{ name: "files"; ... variadic: true }'
|
|
@@ -174,11 +223,11 @@ type ValidateFlagAliases<F extends FlagsDef> = { [K in keyof F & string] : Colli
|
|
|
174
223
|
* '{ readonly FIX_VARIADIC_POSITION: "Only the last positional argument can be variadic" }'.
|
|
175
224
|
* ```
|
|
176
225
|
*/
|
|
177
|
-
type ValidateVariadicArgs<A extends
|
|
226
|
+
type ValidateVariadicArgs<A extends readonly object[]> = A extends readonly [infer Head, ...infer Tail extends readonly object[]] ? Tail extends readonly [unknown, ...unknown[]] ? Head extends {
|
|
178
227
|
variadic: true;
|
|
179
228
|
} ? readonly [Head & {
|
|
180
229
|
readonly FIX_VARIADIC_POSITION: "Only the last positional argument can be variadic";
|
|
181
|
-
}, ...ValidateVariadicArgs<
|
|
230
|
+
}, ...ValidateVariadicArgs<Tail>] : readonly [Head, ...ValidateVariadicArgs<Tail>] : readonly [Head] : A;
|
|
182
231
|
/**
|
|
183
232
|
* Infer the resolved type for a single ArgDef:
|
|
184
233
|
*
|
|
@@ -369,7 +418,7 @@ declare function defineCommand<
|
|
|
369
418
|
const F extends FlagsDef = FlagsDef
|
|
370
419
|
>(config: CommandDef<A, F> & {
|
|
371
420
|
args?: ValidateVariadicArgs<A>;
|
|
372
|
-
flags?: ValidateFlagAliases<F
|
|
421
|
+
flags?: ValidateNoPrefixedFlags<ValidateFlagAliases<F>>;
|
|
373
422
|
}): Command<A, F>;
|
|
374
423
|
interface CommandNotFoundErrorDetails {
|
|
375
424
|
input: string;
|
|
@@ -377,9 +426,15 @@ interface CommandNotFoundErrorDetails {
|
|
|
377
426
|
commandPath: string[];
|
|
378
427
|
parentCommand: AnyCommand;
|
|
379
428
|
}
|
|
429
|
+
interface ValidationErrorDetails {
|
|
430
|
+
issues: readonly {
|
|
431
|
+
readonly message: string;
|
|
432
|
+
readonly path: string;
|
|
433
|
+
}[];
|
|
434
|
+
}
|
|
380
435
|
interface CrustErrorDetailsMap {
|
|
381
436
|
DEFINITION: undefined;
|
|
382
|
-
VALIDATION: undefined;
|
|
437
|
+
VALIDATION: ValidationErrorDetails | undefined;
|
|
383
438
|
PARSE: undefined;
|
|
384
439
|
EXECUTION: undefined;
|
|
385
440
|
COMMAND_NOT_FOUND: CommandNotFoundErrorDetails;
|
|
@@ -439,7 +494,7 @@ declare class CrustError<C extends CrustErrorCode = CrustErrorCode> extends Erro
|
|
|
439
494
|
readonly details: CrustErrorDetails<C>;
|
|
440
495
|
/** Optional wrapped original error/value */
|
|
441
496
|
cause?: unknown;
|
|
442
|
-
constructor(code: C, message: string, ...details: CrustErrorDetails<C>
|
|
497
|
+
constructor(code: C, message: string, ...details: undefined extends CrustErrorDetails<C> ? [] | [CrustErrorDetails<C>] : [CrustErrorDetails<C>]);
|
|
443
498
|
is<T extends CrustErrorCode>(code: T): this is CrustError<T>;
|
|
444
499
|
withCause(cause: unknown): this;
|
|
445
500
|
}
|
|
@@ -539,4 +594,4 @@ interface RunOptions {
|
|
|
539
594
|
}
|
|
540
595
|
declare function runCommand(command: AnyCommand, options?: RunOptions): Promise<void>;
|
|
541
596
|
declare function runMain(command: AnyCommand, options?: RunOptions): Promise<void>;
|
|
542
|
-
export { runMain, runCommand, resolveCommand, parseArgs, defineCommand, SetupContext, RunOptions, PluginMiddleware, ParseResult, MiddlewareContext, InferFlags, InferArgs, FlagsDef, FlagDef, CrustPlugin, CrustErrorCode, CrustError, CommandRoute,
|
|
597
|
+
export { runMain, runCommand, resolveCommand, parseArgs, defineCommand, ValueType, ValidateVariadicArgs, ValidateNoPrefixedFlags, ValidateFlagAliases, SetupContext, RunOptions, PluginMiddleware, ParseResult, MiddlewareContext, InferFlags, InferArgs, FlagsDef, FlagDef, CrustPlugin, CrustErrorCode, CrustError, CommandRoute, CommandMeta, CommandDef, CommandContext, Command, ArgsDef, ArgDef, AnyCommand };
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,22 @@ function defineCommand(config) {
|
|
|
24
24
|
if (!config.meta.name.trim()) {
|
|
25
25
|
throw new CrustError("DEFINITION", "defineCommand: meta.name is required and must be a non-empty string");
|
|
26
26
|
}
|
|
27
|
+
if (config.flags) {
|
|
28
|
+
for (const [name, def] of Object.entries(config.flags)) {
|
|
29
|
+
if (name.startsWith("no-")) {
|
|
30
|
+
const base = name.slice(3);
|
|
31
|
+
throw new CrustError("DEFINITION", `Flag name "--${name}" must not start with "no-"; define "${base}" instead and use "--no-${base}" at runtime`);
|
|
32
|
+
}
|
|
33
|
+
if (def.alias) {
|
|
34
|
+
const aliases = Array.isArray(def.alias) ? def.alias : [def.alias];
|
|
35
|
+
for (const alias of aliases) {
|
|
36
|
+
if (alias.startsWith("no-")) {
|
|
37
|
+
throw new CrustError("DEFINITION", `Alias "--${alias}" on flag "--${name}" must not start with "no-"; the "no-" prefix is reserved for boolean negation`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
27
43
|
const copy = {
|
|
28
44
|
...config,
|
|
29
45
|
meta: { ...config.meta },
|
|
@@ -51,6 +67,10 @@ function buildParseArgsOptionDescriptor(flagsDef) {
|
|
|
51
67
|
aliasRegistry.set(name, name);
|
|
52
68
|
}
|
|
53
69
|
for (const [name, def] of Object.entries(flagsDef)) {
|
|
70
|
+
if (name.startsWith("no-")) {
|
|
71
|
+
const base = name.slice(3);
|
|
72
|
+
throw new CrustError("DEFINITION", `Flag name "--${name}" must not start with "no-"; define "${base}" instead and use "--no-${base}" at runtime`);
|
|
73
|
+
}
|
|
54
74
|
const parseType = def.type === "boolean" ? "boolean" : "string";
|
|
55
75
|
const opt = { type: parseType };
|
|
56
76
|
if (def.multiple) {
|
|
@@ -59,6 +79,9 @@ function buildParseArgsOptionDescriptor(flagsDef) {
|
|
|
59
79
|
if (def.alias) {
|
|
60
80
|
const aliases = Array.isArray(def.alias) ? def.alias : [def.alias];
|
|
61
81
|
for (const alias of aliases) {
|
|
82
|
+
if (alias.startsWith("no-")) {
|
|
83
|
+
throw new CrustError("DEFINITION", `Alias "--${alias}" on flag "--${name}" must not start with "no-"; the "no-" prefix is reserved for boolean negation`);
|
|
84
|
+
}
|
|
62
85
|
const existing = aliasRegistry.get(alias);
|
|
63
86
|
if (existing) {
|
|
64
87
|
throw new CrustError("DEFINITION", `Alias collision: "${alias.length === 1 ? "-" : "--"}${alias}" is used by both "--${existing}" and "--${name}"`);
|
|
@@ -104,10 +127,13 @@ function applyDefaultOrThrow(def, label) {
|
|
|
104
127
|
function coerceFlagValue(name, def, parsedValue) {
|
|
105
128
|
const label = `--${name}`;
|
|
106
129
|
if (def.multiple && Array.isArray(parsedValue)) {
|
|
107
|
-
return def.type === "boolean" ? parsedValue.
|
|
130
|
+
return def.type === "boolean" ? parsedValue.filter((v) => typeof v === "boolean") : parsedValue.map((v) => coerceValue(v, def.type, label));
|
|
108
131
|
}
|
|
109
132
|
if (def.type === "boolean") {
|
|
110
|
-
|
|
133
|
+
if (typeof parsedValue === "boolean") {
|
|
134
|
+
return parsedValue;
|
|
135
|
+
}
|
|
136
|
+
throw new CrustError("PARSE", `Expected boolean value for flag "${label}", got ${typeof parsedValue}`);
|
|
111
137
|
}
|
|
112
138
|
if (typeof parsedValue === "string") {
|
|
113
139
|
return coerceValue(parsedValue, def.type, label);
|
|
@@ -183,10 +209,34 @@ function resolveArgs(argsDef, positionals) {
|
|
|
183
209
|
}
|
|
184
210
|
return resolved;
|
|
185
211
|
}
|
|
212
|
+
function validateCanonicalNegationUsage(argv, flagsDef, aliasToName) {
|
|
213
|
+
if (!flagsDef)
|
|
214
|
+
return;
|
|
215
|
+
for (const arg of argv) {
|
|
216
|
+
if (arg === "--")
|
|
217
|
+
return;
|
|
218
|
+
if (!arg.startsWith("--no-"))
|
|
219
|
+
continue;
|
|
220
|
+
const assignmentIndex = arg.indexOf("=");
|
|
221
|
+
const rawName = assignmentIndex === -1 ? arg.slice("--no-".length) : arg.slice("--no-".length, assignmentIndex);
|
|
222
|
+
if (!rawName)
|
|
223
|
+
continue;
|
|
224
|
+
const canonical = aliasToName[rawName];
|
|
225
|
+
if (!canonical)
|
|
226
|
+
continue;
|
|
227
|
+
if (canonical === rawName)
|
|
228
|
+
continue;
|
|
229
|
+
const def = flagsDef[canonical];
|
|
230
|
+
if (def?.type !== "boolean")
|
|
231
|
+
continue;
|
|
232
|
+
throw new CrustError("PARSE", `Cannot negate alias "--no-${rawName}"; use "--no-${canonical}" instead`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
186
235
|
function parseArgs(command, argv) {
|
|
187
236
|
const argsDef = command.args;
|
|
188
237
|
const flagsDef = command.flags;
|
|
189
238
|
const { options: parseOptions, aliasToName } = buildParseArgsOptionDescriptor(flagsDef);
|
|
239
|
+
validateCanonicalNegationUsage(argv, flagsDef, aliasToName);
|
|
190
240
|
let parsed;
|
|
191
241
|
try {
|
|
192
242
|
parsed = nodeParseArgs({
|
|
@@ -201,10 +251,10 @@ function parseArgs(command, argv) {
|
|
|
201
251
|
if (error instanceof Error) {
|
|
202
252
|
const unknownMatch = error.message.match(/Unknown option '(.+?)'/);
|
|
203
253
|
if (unknownMatch) {
|
|
204
|
-
throw new CrustError("PARSE", `Unknown flag "${unknownMatch[1]}"`);
|
|
254
|
+
throw new CrustError("PARSE", `Unknown flag "${unknownMatch[1]}"`).withCause(error);
|
|
205
255
|
}
|
|
206
256
|
}
|
|
207
|
-
throw error;
|
|
257
|
+
throw new CrustError("PARSE", "Failed to parse command arguments").withCause(error);
|
|
208
258
|
}
|
|
209
259
|
const rawArgs = [];
|
|
210
260
|
const preSeparatorPositionals = [];
|