@crustjs/core 0.0.6 → 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 CHANGED
@@ -176,6 +176,38 @@ type ValidateFlagAliases<F extends Record<string, unknown>> = { [K in keyof F &
176
176
  readonly FIX_ALIAS_COLLISION: `Alias "${CollidingAliases<F, K>}" collides with another flag name or alias`;
177
177
  } };
178
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
+ /**
179
211
  * Per-arg validation tuple type. Resolves to `A` when the constraint is
180
212
  * satisfied (only the last arg is variadic). For non-last args that have
181
213
  * `variadic: true`, adds a branded error property to the specific arg.
@@ -386,7 +418,7 @@ declare function defineCommand<
386
418
  const F extends FlagsDef = FlagsDef
387
419
  >(config: CommandDef<A, F> & {
388
420
  args?: ValidateVariadicArgs<A>;
389
- flags?: ValidateFlagAliases<F>;
421
+ flags?: ValidateNoPrefixedFlags<ValidateFlagAliases<F>>;
390
422
  }): Command<A, F>;
391
423
  interface CommandNotFoundErrorDetails {
392
424
  input: string;
@@ -562,4 +594,4 @@ interface RunOptions {
562
594
  }
563
595
  declare function runCommand(command: AnyCommand, options?: RunOptions): Promise<void>;
564
596
  declare function runMain(command: AnyCommand, options?: RunOptions): Promise<void>;
565
- export { runMain, runCommand, resolveCommand, parseArgs, defineCommand, ValidateVariadicArgs, ValidateFlagAliases, SetupContext, RunOptions, PluginMiddleware, ParseResult, MiddlewareContext, InferFlags, InferArgs, FlagsDef, FlagDef, CrustPlugin, CrustErrorCode, CrustError, CommandRoute, CommandNotFoundErrorDetails, CommandMeta, CommandDef, CommandContext, Command, ArgsDef, ArgDef, AnyCommand };
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.map((v) => typeof v === "boolean" ? v : Boolean(v)) : parsedValue.map((v) => coerceValue(v, def.type, label));
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
- return typeof parsedValue === "boolean" ? parsedValue : Boolean(parsedValue);
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 = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crustjs/core",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Core library for the Crust CLI framework",
5
5
  "type": "module",
6
6
  "license": "MIT",