@crustjs/core 0.0.6 → 0.0.8
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 +49 -4
- package/dist/index.js +63 -7
- package/package.json +1 -1
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.
|
|
@@ -343,8 +375,8 @@ interface Command<
|
|
|
343
375
|
readonly args?: A;
|
|
344
376
|
/** Flag definitions */
|
|
345
377
|
readonly flags?: F;
|
|
346
|
-
/** Named subcommands */
|
|
347
|
-
readonly subCommands
|
|
378
|
+
/** Named subcommands (always initialized, may be empty) */
|
|
379
|
+
readonly subCommands: Record<string, AnyCommand>;
|
|
348
380
|
/** Called before `run()` — useful for initialization */
|
|
349
381
|
preRun?(context: CommandContext<A, F>): void | Promise<void>;
|
|
350
382
|
/** The main command handler */
|
|
@@ -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;
|
|
@@ -535,6 +567,19 @@ interface SetupActions {
|
|
|
535
567
|
* @param def - The flag definition
|
|
536
568
|
*/
|
|
537
569
|
addFlag(command: AnyCommand, name: string, def: FlagDef): void;
|
|
570
|
+
/**
|
|
571
|
+
* Inject a subcommand into a command's `subCommands` record.
|
|
572
|
+
*
|
|
573
|
+
* Use this in plugin `setup()` hooks to register plugin-provided commands
|
|
574
|
+
* (e.g. a "skill" management command). If the parent already has a
|
|
575
|
+
* subcommand with the same name (user-defined), the call is silently
|
|
576
|
+
* skipped — user definitions always take priority over plugin injections.
|
|
577
|
+
*
|
|
578
|
+
* @param parent - The parent command to add the subcommand to
|
|
579
|
+
* @param name - The subcommand name (used for routing)
|
|
580
|
+
* @param command - The subcommand to register
|
|
581
|
+
*/
|
|
582
|
+
addSubCommand(parent: AnyCommand, name: string, command: AnyCommand): void;
|
|
538
583
|
}
|
|
539
584
|
/** Shared context fields available in both setup and middleware phases. */
|
|
540
585
|
interface BaseContext {
|
|
@@ -562,4 +607,4 @@ interface RunOptions {
|
|
|
562
607
|
}
|
|
563
608
|
declare function runCommand(command: AnyCommand, options?: RunOptions): Promise<void>;
|
|
564
609
|
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,
|
|
610
|
+
export { runMain, runCommand, resolveCommand, parseArgs, defineCommand, ValueType, ValidateVariadicArgs, ValidateNoPrefixedFlags, ValidateFlagAliases, SetupContext, SetupActions, 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 },
|
|
@@ -31,9 +47,7 @@ function defineCommand(config) {
|
|
|
31
47
|
args: config.args.map((def) => ({ ...def }))
|
|
32
48
|
},
|
|
33
49
|
flags: config.flags ? Object.fromEntries(Object.entries(config.flags).map(([k, v]) => [k, { ...v }])) : {},
|
|
34
|
-
...config.subCommands
|
|
35
|
-
subCommands: { ...config.subCommands }
|
|
36
|
-
}
|
|
50
|
+
subCommands: config.subCommands ? { ...config.subCommands } : {}
|
|
37
51
|
};
|
|
38
52
|
return Object.freeze(copy);
|
|
39
53
|
}
|
|
@@ -51,6 +65,10 @@ function buildParseArgsOptionDescriptor(flagsDef) {
|
|
|
51
65
|
aliasRegistry.set(name, name);
|
|
52
66
|
}
|
|
53
67
|
for (const [name, def] of Object.entries(flagsDef)) {
|
|
68
|
+
if (name.startsWith("no-")) {
|
|
69
|
+
const base = name.slice(3);
|
|
70
|
+
throw new CrustError("DEFINITION", `Flag name "--${name}" must not start with "no-"; define "${base}" instead and use "--no-${base}" at runtime`);
|
|
71
|
+
}
|
|
54
72
|
const parseType = def.type === "boolean" ? "boolean" : "string";
|
|
55
73
|
const opt = { type: parseType };
|
|
56
74
|
if (def.multiple) {
|
|
@@ -59,6 +77,9 @@ function buildParseArgsOptionDescriptor(flagsDef) {
|
|
|
59
77
|
if (def.alias) {
|
|
60
78
|
const aliases = Array.isArray(def.alias) ? def.alias : [def.alias];
|
|
61
79
|
for (const alias of aliases) {
|
|
80
|
+
if (alias.startsWith("no-")) {
|
|
81
|
+
throw new CrustError("DEFINITION", `Alias "--${alias}" on flag "--${name}" must not start with "no-"; the "no-" prefix is reserved for boolean negation`);
|
|
82
|
+
}
|
|
62
83
|
const existing = aliasRegistry.get(alias);
|
|
63
84
|
if (existing) {
|
|
64
85
|
throw new CrustError("DEFINITION", `Alias collision: "${alias.length === 1 ? "-" : "--"}${alias}" is used by both "--${existing}" and "--${name}"`);
|
|
@@ -104,10 +125,13 @@ function applyDefaultOrThrow(def, label) {
|
|
|
104
125
|
function coerceFlagValue(name, def, parsedValue) {
|
|
105
126
|
const label = `--${name}`;
|
|
106
127
|
if (def.multiple && Array.isArray(parsedValue)) {
|
|
107
|
-
return def.type === "boolean" ? parsedValue.
|
|
128
|
+
return def.type === "boolean" ? parsedValue.filter((v) => typeof v === "boolean") : parsedValue.map((v) => coerceValue(v, def.type, label));
|
|
108
129
|
}
|
|
109
130
|
if (def.type === "boolean") {
|
|
110
|
-
|
|
131
|
+
if (typeof parsedValue === "boolean") {
|
|
132
|
+
return parsedValue;
|
|
133
|
+
}
|
|
134
|
+
throw new CrustError("PARSE", `Expected boolean value for flag "${label}", got ${typeof parsedValue}`);
|
|
111
135
|
}
|
|
112
136
|
if (typeof parsedValue === "string") {
|
|
113
137
|
return coerceValue(parsedValue, def.type, label);
|
|
@@ -183,10 +207,34 @@ function resolveArgs(argsDef, positionals) {
|
|
|
183
207
|
}
|
|
184
208
|
return resolved;
|
|
185
209
|
}
|
|
210
|
+
function validateCanonicalNegationUsage(argv, flagsDef, aliasToName) {
|
|
211
|
+
if (!flagsDef)
|
|
212
|
+
return;
|
|
213
|
+
for (const arg of argv) {
|
|
214
|
+
if (arg === "--")
|
|
215
|
+
return;
|
|
216
|
+
if (!arg.startsWith("--no-"))
|
|
217
|
+
continue;
|
|
218
|
+
const assignmentIndex = arg.indexOf("=");
|
|
219
|
+
const rawName = assignmentIndex === -1 ? arg.slice("--no-".length) : arg.slice("--no-".length, assignmentIndex);
|
|
220
|
+
if (!rawName)
|
|
221
|
+
continue;
|
|
222
|
+
const canonical = aliasToName[rawName];
|
|
223
|
+
if (!canonical)
|
|
224
|
+
continue;
|
|
225
|
+
if (canonical === rawName)
|
|
226
|
+
continue;
|
|
227
|
+
const def = flagsDef[canonical];
|
|
228
|
+
if (def?.type !== "boolean")
|
|
229
|
+
continue;
|
|
230
|
+
throw new CrustError("PARSE", `Cannot negate alias "--no-${rawName}"; use "--no-${canonical}" instead`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
186
233
|
function parseArgs(command, argv) {
|
|
187
234
|
const argsDef = command.args;
|
|
188
235
|
const flagsDef = command.flags;
|
|
189
236
|
const { options: parseOptions, aliasToName } = buildParseArgsOptionDescriptor(flagsDef);
|
|
237
|
+
validateCanonicalNegationUsage(argv, flagsDef, aliasToName);
|
|
190
238
|
let parsed;
|
|
191
239
|
try {
|
|
192
240
|
parsed = nodeParseArgs({
|
|
@@ -201,10 +249,10 @@ function parseArgs(command, argv) {
|
|
|
201
249
|
if (error instanceof Error) {
|
|
202
250
|
const unknownMatch = error.message.match(/Unknown option '(.+?)'/);
|
|
203
251
|
if (unknownMatch) {
|
|
204
|
-
throw new CrustError("PARSE", `Unknown flag "${unknownMatch[1]}"`);
|
|
252
|
+
throw new CrustError("PARSE", `Unknown flag "${unknownMatch[1]}"`).withCause(error);
|
|
205
253
|
}
|
|
206
254
|
}
|
|
207
|
-
throw error;
|
|
255
|
+
throw new CrustError("PARSE", "Failed to parse command arguments").withCause(error);
|
|
208
256
|
}
|
|
209
257
|
const rawArgs = [];
|
|
210
258
|
const preSeparatorPositionals = [];
|
|
@@ -342,6 +390,14 @@ async function runCommand(command, options) {
|
|
|
342
390
|
throw new CrustError("DEFINITION", `Cannot add flag "${name}": command "${target.meta.name}" has no flags object.`);
|
|
343
391
|
}
|
|
344
392
|
target.flags[name] = def;
|
|
393
|
+
},
|
|
394
|
+
addSubCommand(parent, name, subCommand) {
|
|
395
|
+
if (!name.trim()) {
|
|
396
|
+
throw new CrustError("DEFINITION", "addSubCommand: name is required and must be a non-empty string");
|
|
397
|
+
}
|
|
398
|
+
if (parent.subCommands[name])
|
|
399
|
+
return;
|
|
400
|
+
parent.subCommands[name] = subCommand;
|
|
345
401
|
}
|
|
346
402
|
};
|
|
347
403
|
const middlewareContext = {
|