@crustjs/core 0.0.3 → 0.0.5

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/README.md CHANGED
@@ -17,9 +17,9 @@ import { defineCommand, runMain } from "@crustjs/core";
17
17
 
18
18
  const main = defineCommand({
19
19
  meta: { name: "greet", description: "Say hello" },
20
- args: [{ name: "name", type: String, default: "world" }],
20
+ args: [{ name: "name", type: "string", default: "world" }],
21
21
  flags: {
22
- loud: { type: Boolean, description: "Shout it", alias: "l" },
22
+ loud: { type: "boolean", description: "Shout it", alias: "l" },
23
23
  },
24
24
  run({ args, flags }) {
25
25
  const msg = `Hello, ${args.name}!`;
package/dist/index.d.ts CHANGED
@@ -1,79 +1,131 @@
1
- /** Maps a constructor function (String, Number, Boolean) to its primitive type */
2
- type TypeConstructor = StringConstructor | NumberConstructor | BooleanConstructor;
1
+ /** Supported type literals for args and flags */
2
+ type ValueType = "string" | "number" | "boolean";
3
3
  /**
4
- * Resolves a constructor function to its corresponding TypeScript primitive type.
4
+ * Resolves a type literal to its corresponding TypeScript primitive type.
5
5
  *
6
- * - `StringConstructor` → `string`
7
- * - `NumberConstructor` → `number`
8
- * - `BooleanConstructor` → `boolean`
6
+ * - `"string"` → `string`
7
+ * - `"number"` → `number`
8
+ * - `"boolean"` → `boolean`
9
9
  */
10
- type ResolvePrimitive<T extends TypeConstructor> = T extends StringConstructor ? string : T extends NumberConstructor ? number : T extends BooleanConstructor ? boolean : never;
10
+ type ResolvePrimitive<T extends ValueType> = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : never;
11
+ /** Shared fields present on every positional argument definition */
12
+ interface ArgDefBase {
13
+ /** The argument name (used as the key in the parsed result and in help text) */
14
+ name: string;
15
+ /** Human-readable description for help text */
16
+ description?: string;
17
+ /** When `true`, the parser throws if the argument is not provided */
18
+ required?: true;
19
+ /** When `true`, collects all remaining positional values into an array */
20
+ variadic?: true;
21
+ }
22
+ /** A positional argument whose value is a string */
23
+ interface StringArgDef extends ArgDefBase {
24
+ type: "string";
25
+ /** Default string value when the argument is not provided */
26
+ default?: string;
27
+ }
28
+ /** A positional argument whose value is a number */
29
+ interface NumberArgDef extends ArgDefBase {
30
+ type: "number";
31
+ /** Default number value when the argument is not provided */
32
+ default?: number;
33
+ }
34
+ /** A positional argument whose value is a boolean */
35
+ interface BooleanArgDef extends ArgDefBase {
36
+ type: "boolean";
37
+ /** Default boolean value when the argument is not provided */
38
+ default?: boolean;
39
+ }
11
40
  /**
12
41
  * Defines a single positional argument for a CLI command.
13
42
  *
14
- * Args are defined as an ordered tuple so that positional ordering is explicit
15
- * and the type system can enforce that only the last arg may be variadic.
43
+ * Discriminated by `type` for type-safe `default` values. Boolean toggle
44
+ * fields (`required`, `variadic`) only accept `true`.
16
45
  *
17
46
  * @example
18
47
  * ```ts
19
48
  * const args = [
20
- * { name: "port", type: Number, description: "Port number", default: 3000 },
21
- * { name: "name", type: String, required: true },
22
- * { name: "files", type: String, variadic: true },
49
+ * { name: "port", type: "number", description: "Port number", default: 3000 },
50
+ * { name: "name", type: "string", required: true },
51
+ * { name: "files", type: "string", variadic: true },
23
52
  * ] as const satisfies ArgsDef;
24
53
  * ```
25
54
  */
26
- interface ArgDef<
27
- N extends string = string,
28
- T extends TypeConstructor = TypeConstructor,
29
- D extends ResolvePrimitive<T> | undefined = ResolvePrimitive<T> | undefined,
30
- R extends boolean = boolean,
31
- V extends boolean = boolean
32
- > {
33
- /** The argument name (used as the key in the parsed result and in help text) */
34
- name: N;
35
- /** Constructor function indicating the argument's type: `String`, `Number`, or `Boolean` */
36
- type: T;
55
+ type ArgDef = StringArgDef | NumberArgDef | BooleanArgDef;
56
+ /** Ordered tuple of positional argument definitions */
57
+ type ArgsDef = readonly ArgDef[];
58
+ /** Shared fields present on every flag definition */
59
+ interface FlagDefBase {
37
60
  /** Human-readable description for help text */
38
61
  description?: string;
39
- /** Default value when the argument is not provided */
40
- default?: D;
41
- /** Whether this argument must be provided (errors if missing) */
42
- required?: R;
43
- /** Whether this argument collects all remaining positionals into an array */
44
- variadic?: V;
62
+ /** Short alias or array of aliases (e.g. `"v"` or `["v", "V"]`) */
63
+ alias?: string | string[];
64
+ /** When `true`, the parser throws if the flag is not provided */
65
+ required?: true;
66
+ }
67
+ /** Base for single-value flags — `multiple` must be omitted */
68
+ interface SingleFlagBase extends FlagDefBase {
69
+ /** Must be omitted for single-value flags — set to `true` for multi-value */
70
+ multiple?: never;
71
+ }
72
+ /** A single-value string flag */
73
+ interface StringFlagDef extends SingleFlagBase {
74
+ type: "string";
75
+ /** Default string value */
76
+ default?: string;
77
+ }
78
+ /** A single-value number flag */
79
+ interface NumberFlagDef extends SingleFlagBase {
80
+ type: "number";
81
+ /** Default number value */
82
+ default?: number;
83
+ }
84
+ /** A single-value boolean flag */
85
+ interface BooleanFlagDef extends SingleFlagBase {
86
+ type: "boolean";
87
+ /** Default boolean value */
88
+ default?: boolean;
89
+ }
90
+ /** Base for multi-value flags — `multiple` is required as `true` */
91
+ interface MultiFlagBase extends FlagDefBase {
92
+ /** Collect repeated values into an array */
93
+ multiple: true;
94
+ }
95
+ /** A multi-value string flag (collects repeated values into an array) */
96
+ interface StringMultiFlagDef extends MultiFlagBase {
97
+ type: "string";
98
+ /** Default string array value */
99
+ default?: string[];
100
+ }
101
+ /** A multi-value number flag (collects repeated values into an array) */
102
+ interface NumberMultiFlagDef extends MultiFlagBase {
103
+ type: "number";
104
+ /** Default number array value */
105
+ default?: number[];
106
+ }
107
+ /** A multi-value boolean flag (collects repeated values into an array) */
108
+ interface BooleanMultiFlagDef extends MultiFlagBase {
109
+ type: "boolean";
110
+ /** Default boolean array value */
111
+ default?: boolean[];
45
112
  }
46
- /** Ordered tuple of positional argument definitions */
47
- type ArgsDef = readonly ArgDef[];
48
113
  /**
49
114
  * Defines a single named flag for a CLI command.
50
115
  *
116
+ * Discriminated by `type` and `multiple` for type-safe `default` values.
117
+ * Boolean toggle fields (`required`, `multiple`) only accept `true`.
118
+ *
51
119
  * @example
52
120
  * ```ts
53
121
  * const flags = {
54
- * verbose: { type: Boolean, description: "Enable verbose logging", alias: "v" },
55
- * port: { type: Number, description: "Port number", default: 3000 },
122
+ * verbose: { type: "boolean", description: "Enable verbose logging", alias: "v" },
123
+ * port: { type: "number", description: "Port number", default: 3000 },
124
+ * files: { type: "string", multiple: true, default: ["index.ts"] },
56
125
  * } satisfies FlagsDef;
57
126
  * ```
58
127
  */
59
- interface FlagDef<
60
- T extends TypeConstructor = TypeConstructor,
61
- R extends boolean = boolean,
62
- M extends boolean = boolean
63
- > {
64
- /** Constructor function indicating the flag's type: `String`, `Number`, or `Boolean` */
65
- type: T;
66
- /** Human-readable description for help text */
67
- description?: string;
68
- /** Default value when the flag is not provided. Must be an array when `multiple: true`. */
69
- default?: M extends true ? ResolvePrimitive<T>[] : ResolvePrimitive<T>;
70
- /** Whether this flag must be provided (errors if missing) */
71
- required?: R;
72
- /** Short alias or array of aliases (e.g. `"v"` or `["v", "V"]`) */
73
- alias?: string | string[];
74
- /** Whether this flag can be provided multiple times, collecting values into an array */
75
- multiple?: M;
76
- }
128
+ type FlagDef = StringFlagDef | NumberFlagDef | BooleanFlagDef | StringMultiFlagDef | NumberMultiFlagDef | BooleanMultiFlagDef;
77
129
  /** Record mapping flag names to their definitions */
78
130
  type FlagsDef = Record<string, FlagDef>;
79
131
  /** Extract alias string literals from a single FlagDef */
@@ -81,12 +133,6 @@ type ExtractAliases<F extends FlagDef> = F extends {
81
133
  alias: infer A;
82
134
  } ? A extends string ? A : A extends readonly string[] ? A[number] : never : never;
83
135
  /**
84
- * Collects all alias literals across a FlagsDef, then intersects with `keyof F`.
85
- * Resolves to `never` when no alias matches a flag name, or the colliding
86
- * name(s) when a match exists.
87
- */
88
- type FlagAliasNameCollision<F extends FlagsDef> = { [K in keyof F & string] : ExtractAliases<F[K]> }[keyof F & string] & keyof F;
89
- /**
90
136
  * Collects aliases from every flag *except* flag K.
91
137
  * Used to detect alias→alias duplicates across different flags.
92
138
  */
@@ -95,38 +141,44 @@ type AliasesExcluding<
95
141
  K extends keyof F & string
96
142
  > = { [J in Exclude<keyof F & string, K>] : ExtractAliases<F[J]> }[Exclude<keyof F & string, K>];
97
143
  /**
98
- * Resolves to the alias literal(s) that appear in more than one flag's
99
- * alias list, or `never` when no duplicate aliases exist.
144
+ * Per-flag collision detection: resolves to the alias literal(s) of flag K
145
+ * that collide with another flag's name or another flag's alias,
146
+ * or `never` when K's aliases are all unique.
100
147
  */
101
- type FlagAliasAliasCollision<F extends FlagsDef> = { [K in keyof F & string] : ExtractAliases<F[K]> & AliasesExcluding<F, K> }[keyof F & string];
148
+ type CollidingAliases<
149
+ F extends FlagsDef,
150
+ K extends keyof F & string
151
+ > = (ExtractAliases<F[K]> & Exclude<keyof F & string, K>) | (ExtractAliases<F[K]> & AliasesExcluding<F, K>);
102
152
  /**
103
- * Compile-time check that no flag alias collides with another flag's name
104
- * or another flag's alias.
153
+ * Per-flag validation mapped type. Resolves to `F` when no collisions exist.
154
+ * 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:
105
156
  *
106
- * - Resolves to `unknown` (no-op intersection) when no collision exists.
107
- * - Resolves to a descriptive error tuple when a collision is found,
108
- * causing a type error on the `flags` property.
109
- */
110
- type CheckFlagAliasCollisions<F extends FlagsDef> = FlagAliasNameCollision<F> extends never ? FlagAliasAliasCollision<F> extends never ? unknown : ["ERROR: Duplicate flag alias across different flags. Colliding alias(es):", FlagAliasAliasCollision<F>] : ["ERROR: Flag alias collides with a flag name. Colliding name(s):", FlagAliasNameCollision<F>];
111
- /**
112
- * Checks whether any element in `Init` (all elements except the last) has
113
- * `variadic: true`. Used by {@link CheckVariadicArgs} to ensure only the
114
- * last positional arg can be variadic.
157
+ * ```
158
+ * Property 'FIX_ALIAS_COLLISION' is missing in type '{ type: "string"; alias: "minify" }'
159
+ * but required in type
160
+ * '{ readonly FIX_ALIAS_COLLISION: "Alias \"minify\" collides with another flag name or alias" }'.
161
+ * ```
115
162
  */
116
- type InitHasVariadic<T extends readonly ArgDef[]> = T extends readonly [infer Head, ...infer Tail extends readonly ArgDef[]] ? Head extends {
117
- variadic: true;
118
- } ? true : InitHasVariadic<Tail> : false;
163
+ type ValidateFlagAliases<F extends FlagsDef> = { [K in keyof F & string] : CollidingAliases<F, K> extends never ? F[K] : F[K] & {
164
+ readonly FIX_ALIAS_COLLISION: `Alias "${CollidingAliases<F, K>}" collides with another flag name or alias`;
165
+ } };
119
166
  /**
120
- * Compile-time check that only the last positional arg has `variadic: true`.
121
- *
122
- * Since args are now an ordered tuple, we can split it into `[...Init, Last]`
123
- * and verify that no element in `Init` is variadic.
167
+ * Per-arg validation tuple type. Resolves to `A` when the constraint is
168
+ * 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:
124
170
  *
125
- * - Resolves to `unknown` (no-op intersection) when the constraint is satisfied.
126
- * - Resolves to a descriptive error string when a non-last arg is variadic,
127
- * causing a type error on the `args` property.
171
+ * ```
172
+ * Property 'FIX_VARIADIC_POSITION' is missing in type '{ name: "files"; ... variadic: true }'
173
+ * but required in type
174
+ * '{ readonly FIX_VARIADIC_POSITION: "Only the last positional argument can be variadic" }'.
175
+ * ```
128
176
  */
129
- type CheckVariadicArgs<A extends ArgsDef> = A extends readonly [...infer Init extends readonly ArgDef[], ArgDef] ? InitHasVariadic<Init> extends true ? "ERROR: Only the last positional argument can be variadic" : unknown : unknown;
177
+ type ValidateVariadicArgs<A extends ArgsDef> = A extends readonly [infer Head extends ArgDef, ...infer Tail extends readonly ArgDef[]] ? Tail extends readonly [ArgDef, ...ArgDef[]] ? Head extends {
178
+ variadic: true;
179
+ } ? readonly [Head & {
180
+ readonly FIX_VARIADIC_POSITION: "Only the last positional argument can be variadic";
181
+ }, ...ValidateVariadicArgs<readonly [...Tail]>] : readonly [Head, ...ValidateVariadicArgs<readonly [...Tail]>] : readonly [Head] : A;
130
182
  /**
131
183
  * Infer the resolved type for a single ArgDef:
132
184
  *
@@ -156,9 +208,9 @@ type Simplify<T> = { [K in keyof T] : T[K] };
156
208
  * @example
157
209
  * ```ts
158
210
  * type Result = InferArgs<readonly [
159
- * { name: "port"; type: NumberConstructor; default: 3000 },
160
- * { name: "name"; type: StringConstructor; required: true },
161
- * { name: "files"; type: StringConstructor; variadic: true },
211
+ * { name: "port"; type: "number"; default: 3000 },
212
+ * { name: "name"; type: "string"; required: true },
213
+ * { name: "files"; type: "string"; variadic: true },
162
214
  * ]>;
163
215
  * // Result = { port: number; name: string; files: string[] }
164
216
  * ```
@@ -188,8 +240,8 @@ type InferFlagValue<F extends FlagDef> = F extends {
188
240
  * @example
189
241
  * ```ts
190
242
  * type Result = InferFlags<{
191
- * verbose: { type: BooleanConstructor };
192
- * port: { type: NumberConstructor, default: 3000 };
243
+ * verbose: { type: "boolean" };
244
+ * port: { type: "number", default: 3000 };
193
245
  * }>;
194
246
  * // Result = { verbose: boolean | undefined; port: number }
195
247
  * ```
@@ -233,7 +285,37 @@ interface CommandContext<
233
285
  /** The resolved command that is being executed */
234
286
  command: AnyCommand;
235
287
  }
236
- /** Unified command shape used for both command definitions and resolved commands. */
288
+ /**
289
+ * Configuration object accepted by `defineCommand()`.
290
+ *
291
+ * Identical shape to {@link Command} but uses `NoInfer` on lifecycle-hook
292
+ * parameters so TypeScript infers `A` and `F` solely from the `args` / `flags`
293
+ * data properties — not from callbacks. This ensures full contextual typing
294
+ * (e.g. `description` is `string`, not `any`) when writing command definitions.
295
+ *
296
+ * Compile-time validation for variadic args and flag alias collisions is
297
+ * enforced via parameter-level intersection in `defineCommand()`.
298
+ */
299
+ interface CommandDef<
300
+ A extends ArgsDef = ArgsDef,
301
+ F extends FlagsDef = FlagsDef
302
+ > {
303
+ /** Command metadata (name, description, usage) */
304
+ meta: CommandMeta;
305
+ /** Positional argument definitions */
306
+ args?: A;
307
+ /** Flag definitions */
308
+ flags?: F;
309
+ /** Named subcommands */
310
+ subCommands?: Record<string, AnyCommand>;
311
+ /** Called before `run()` — useful for initialization */
312
+ preRun?(context: CommandContext<NoInfer<A>, NoInfer<F>>): void | Promise<void>;
313
+ /** The main command handler */
314
+ run?(context: CommandContext<NoInfer<A>, NoInfer<F>>): void | Promise<void>;
315
+ /** Called after `run()` (even if it throws) — useful for teardown */
316
+ postRun?(context: CommandContext<NoInfer<A>, NoInfer<F>>): void | Promise<void>;
317
+ }
318
+ /** Frozen command object returned by `defineCommand()` and used at runtime. */
237
319
  interface Command<
238
320
  A extends ArgsDef = ArgsDef,
239
321
  F extends FlagsDef = FlagsDef
@@ -270,10 +352,10 @@ type AnyCommand = Command<any, any>;
270
352
  * const cmd = defineCommand({
271
353
  * meta: { name: "serve", description: "Start dev server" },
272
354
  * args: [
273
- * { name: "port", type: Number, description: "Port number", default: 3000 },
355
+ * { name: "port", type: "number", description: "Port number", default: 3000 },
274
356
  * ],
275
357
  * flags: {
276
- * verbose: { type: Boolean, description: "Enable verbose logging", alias: "v" },
358
+ * verbose: { type: "boolean", description: "Enable verbose logging", alias: "v" },
277
359
  * },
278
360
  * run({ args, flags }) {
279
361
  * // args.port is typed as number, flags.verbose is typed as boolean | undefined
@@ -285,9 +367,9 @@ type AnyCommand = Command<any, any>;
285
367
  declare function defineCommand<
286
368
  const A extends ArgsDef = ArgsDef,
287
369
  const F extends FlagsDef = FlagsDef
288
- >(config: Command<A, F> & {
289
- args?: A & CheckVariadicArgs<A>;
290
- flags?: F & CheckFlagAliasCollisions<F>;
370
+ >(config: CommandDef<A, F> & {
371
+ args?: ValidateVariadicArgs<A>;
372
+ flags?: ValidateFlagAliases<F>;
291
373
  }): Command<A, F>;
292
374
  interface CommandNotFoundErrorDetails {
293
375
  input: string;
@@ -457,4 +539,4 @@ interface RunOptions {
457
539
  }
458
540
  declare function runCommand(command: AnyCommand, options?: RunOptions): Promise<void>;
459
541
  declare function runMain(command: AnyCommand, options?: RunOptions): Promise<void>;
460
- export { runMain, runCommand, resolveCommand, parseArgs, defineCommand, SetupContext, RunOptions, PluginMiddleware, ParseResult, MiddlewareContext, InferFlags, InferArgs, FlagsDef, FlagDef, CrustPlugin, CrustErrorCode, CrustError, CommandRoute, CommandNotFoundErrorDetails, CommandMeta, CommandContext, Command, ArgsDef, ArgDef, AnyCommand };
542
+ export { runMain, runCommand, resolveCommand, parseArgs, defineCommand, SetupContext, RunOptions, PluginMiddleware, ParseResult, MiddlewareContext, InferFlags, InferArgs, FlagsDef, FlagDef, CrustPlugin, CrustErrorCode, CrustError, CommandRoute, CommandNotFoundErrorDetails, CommandMeta, CommandDef, CommandContext, Command, ArgsDef, ArgDef, AnyCommand };
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ function buildParseArgsOptionDescriptor(flagsDef) {
51
51
  aliasRegistry.set(name, name);
52
52
  }
53
53
  for (const [name, def] of Object.entries(flagsDef)) {
54
- const parseType = def.type === Boolean ? "boolean" : "string";
54
+ const parseType = def.type === "boolean" ? "boolean" : "string";
55
55
  const opt = { type: parseType };
56
56
  if (def.multiple) {
57
57
  opt.multiple = true;
@@ -61,7 +61,7 @@ function buildParseArgsOptionDescriptor(flagsDef) {
61
61
  for (const alias of aliases) {
62
62
  const existing = aliasRegistry.get(alias);
63
63
  if (existing) {
64
- throw new CrustError("DEFINITION", `Alias collision: "-${alias}" is used by both "--${existing}" and "--${name}"`);
64
+ throw new CrustError("DEFINITION", `Alias collision: "${alias.length === 1 ? "-" : "--"}${alias}" is used by both "--${existing}" and "--${name}"`);
65
65
  }
66
66
  aliasRegistry.set(alias, name);
67
67
  aliasToName[alias] = name;
@@ -81,14 +81,14 @@ function buildParseArgsOptionDescriptor(flagsDef) {
81
81
  return { options, aliasToName };
82
82
  }
83
83
  function coerceValue(value, type, label) {
84
- if (type === Number) {
84
+ if (type === "number") {
85
85
  const num = Number(value);
86
86
  if (Number.isNaN(num)) {
87
87
  throw new CrustError("PARSE", `Expected number for ${label}, got "${value}"`);
88
88
  }
89
89
  return num;
90
90
  }
91
- if (type === Boolean) {
91
+ if (type === "boolean") {
92
92
  return value === "true" || value === "1";
93
93
  }
94
94
  return value;
@@ -104,9 +104,9 @@ function applyDefaultOrThrow(def, label) {
104
104
  function coerceFlagValue(name, def, parsedValue) {
105
105
  const label = `--${name}`;
106
106
  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));
107
+ return def.type === "boolean" ? parsedValue.map((v) => typeof v === "boolean" ? v : Boolean(v)) : parsedValue.map((v) => coerceValue(v, def.type, label));
108
108
  }
109
- if (def.type === Boolean) {
109
+ if (def.type === "boolean") {
110
110
  return typeof parsedValue === "boolean" ? parsedValue : Boolean(parsedValue);
111
111
  }
112
112
  if (typeof parsedValue === "string") {
@@ -172,7 +172,7 @@ function resolveArgs(argsDef, positionals) {
172
172
  if (def.required === true && remaining.length === 0) {
173
173
  throw new CrustError("VALIDATION", `Missing required ${label}`);
174
174
  }
175
- resolved[name] = def.type === Number ? remaining.map((v) => coerceValue(v, Number, `<${name}>`)) : remaining;
175
+ resolved[name] = def.type === "string" ? remaining : remaining.map((v) => coerceValue(v, def.type, `<${name}>`));
176
176
  index = positionals.length;
177
177
  } else if (index < positionals.length) {
178
178
  resolved[name] = coerceValue(positionals[index], def.type, `<${name}>`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crustjs/core",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Core library for the Crust CLI framework",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,10 +41,10 @@
41
41
  "test": "bun test"
42
42
  },
43
43
  "devDependencies": {
44
- "@crustjs/config": "workspace:*",
45
- "bunup": "catalog:"
44
+ "@crustjs/config": "0.0.0",
45
+ "bunup": "^0.16.29"
46
46
  },
47
47
  "peerDependencies": {
48
- "typescript": "catalog:"
48
+ "typescript": "^5"
49
49
  }
50
50
  }