@crustjs/core 0.3.5 → 0.4.0

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.
@@ -478,6 +478,10 @@ interface FlagSnapshot {
478
478
  readonly choices?: readonly string[];
479
479
  /** Declared default value; `URL` defaults are serialized to their `href` string. */
480
480
  readonly default?: unknown;
481
+ /** Declared environment binding; never contains the variable's value. */
482
+ readonly env?: Readonly<NonNullable<FlagDef["env"]>>;
483
+ /** Delimiter that splits string argv values of a repeatable flag into occurrences. */
484
+ readonly delimiter?: string;
481
485
  }
482
486
  /**
483
487
  * A readonly, serializable description of a command, exposed across public
@@ -1625,16 +1629,42 @@ interface FlagDefBase {
1625
1629
  /** Not supported with core value options — see {@link SchemaStringFlagDef} */
1626
1630
  schema?: never;
1627
1631
  }
1628
- /** Base for single-value flags — `multiple` must be omitted */
1629
- interface SingleFlagBase extends FlagDefBase {
1632
+ /** Environment fallback for terminal parsing; structured `run()` never reads it. */
1633
+ interface FlagEnvBinding {
1634
+ /** Variable consulted only when argv omits the flag; help shows its name, never its value. */
1635
+ name: string;
1636
+ /** Split environment text into occurrences; independent of the argv delimiter, repeatable flags only. */
1637
+ delimiter?: string;
1638
+ }
1639
+ /** Occurrence fields of a single-value flag — `multiple` and `delimiter` must be omitted. */
1640
+ interface SingleOccurrenceFields {
1641
+ /** Environment fallback; argv wins over env, which wins over the default. */
1642
+ env?: FlagEnvBinding & {
1643
+ delimiter?: never;
1644
+ };
1630
1645
  /** Must be omitted for single-value flags — set to `true` for multi-value */
1631
1646
  multiple?: never;
1647
+ /** Only repeatable flags split values — see {@link RepeatableOccurrenceFields} */
1648
+ delimiter?: never;
1632
1649
  }
1633
- /** Base for multi-value flags — `multiple` is required as `true` */
1634
- interface MultiFlagBase extends FlagDefBase {
1650
+ /** Occurrence fields of a repeatable flag — `multiple` is required as `true`. */
1651
+ interface RepeatableOccurrenceFields {
1652
+ /** Environment fallback; text is one occurrence unless `env.delimiter` is set. */
1653
+ env?: FlagEnvBinding;
1635
1654
  /** Collect repeated values into an array */
1636
1655
  multiple: true;
1656
+ /**
1657
+ * Split each string argv value into separate occurrences
1658
+ * (`--tags a,b` with `","` is two occurrences). Empty segments are dropped.
1659
+ * Boolean argv switches stay booleans. Environment text uses `env.delimiter`.
1660
+ * Opt-in with no default; structured `run()` arrays are never split.
1661
+ */
1662
+ delimiter?: string;
1637
1663
  }
1664
+ /** Base for single-value flags — `multiple` must be omitted */
1665
+ interface SingleFlagBase extends FlagDefBase, SingleOccurrenceFields {}
1666
+ /** Base for multi-value flags — `multiple` is required as `true` */
1667
+ interface MultiFlagBase extends FlagDefBase, RepeatableOccurrenceFields {}
1638
1668
  type StringFlagFields<Default, ParseOutput> = {
1639
1669
  /** Default string value, or string array for a multi-value flag. */
1640
1670
  default?: Default;
@@ -1688,23 +1718,19 @@ interface SchemaFlagBase extends Omit<FlagDefBase, "schema" | "required"> {
1688
1718
  * `string[] | undefined` with `multiple: true`) and exclusively owns coercion,
1689
1719
  * defaults, requiredness, and validation. `type` declares token consumption only.
1690
1720
  */
1691
- interface SchemaStringFlagDef extends SchemaFlagBase {
1721
+ type SchemaStringFlagDef = SchemaFlagBase & {
1692
1722
  type: "string";
1693
- /** When `true`, the schema receives `string[]` when present, or `undefined` when omitted. */
1694
- multiple?: true;
1695
1723
  noNegate?: never;
1696
- }
1724
+ } & (SingleOccurrenceFields | RepeatableOccurrenceFields);
1697
1725
  /**
1698
1726
  * A schema-backed toggle flag (no value token). The schema receives the raw
1699
1727
  * `boolean | undefined` (or `boolean[] | undefined` with `multiple: true`).
1700
1728
  */
1701
- interface SchemaBooleanFlagDef extends SchemaFlagBase {
1729
+ type SchemaBooleanFlagDef = SchemaFlagBase & {
1702
1730
  type: "boolean";
1703
- /** When `true`, the schema receives `boolean[]` when present, or `undefined` when omitted. */
1704
- multiple?: true;
1705
1731
  /** When `true`, reject `--no-{name}` (and negated aliases) at parse time and hide the generated help label */
1706
1732
  noNegate?: true;
1707
- }
1733
+ } & (SingleOccurrenceFields | RepeatableOccurrenceFields);
1708
1734
  /**
1709
1735
  * Defines a single named flag for a CLI command.
1710
1736
  *
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as CrustErrorCode, A as SectionConsumer, At as MergeContext, B as ExtensionContext, C as InvocationOptions, Ct as ArgSnapshot, D as ParsedArgValue, Dt as LocalFlagNameBrand, E as ParseResult, Et as LocalFlagBrand, F as BuildReport, G as Finished, H as ExtensionFlagDef, I as DefineExtensionWith, J as NamedExtensionFlagDef, K as InferExtensionFlags, L as Extension, M as ValueType, Mt as defineExtensionId, N as BuildArtifacts, O as ParsedFlagValue, Ot as EmptyArgNameBrand, P as BuildFile, Q as CrustError, R as ExtensionBuildContext, S as InvocationIO, St as defineCommand, T as NamedFlagDef, Tt as FlagSnapshot, U as ExtensionHooks, V as ExtensionFactory, W as ExtensionSectionContribution, X as defineExtension, Y as RootMetaKey, Z as CommandNotFoundErrorDetails, _ as ExecuteOptions, _t as RootCommandMeta, a as ContextInstance, at as ValidationErrorDetails, b as InputArgs, bt as RunInputArguments, c as FactoryValueOf, ct as CommandDefinition, d as ArgDef, dt as CommandPath, et as CrustErrorDetails, f as ArgsDef, ft as CommandShape, gt as CrustCommandContext, h as CommandSectionInput, ht as Crust, i as ContextFactory, it as ParseErrorDetails, j as ValidatedInput, jt as ExtensionId, k as SectionAudience, kt as LocalValueBrand, l as contextSources, lt as CommandDefinitionBuilder, m as CommandSection, mt as CommandTree, n as ContextBag, nt as CrustErrorJson, o as ContextMap, ot as AnyCrust, p as CommandMeta, pt as CommandShapeAt, q as InvocationOutcome, r as ContextConfig, rt as DefinitionErrorDetails, s as ContextSetup, st as CommandConfig, t as AnyContextFactory, tt as CrustErrorDetailsMap, u as defineContext, ut as CommandHandle, v as FlagDef, vt as RunArguments, w as MergeFlags, wt as CommandSnapshot, x as InputFlags, xt as RunOutcome, y as FlagsDef, yt as RunInput, z as ExtensionConfig } from "./context-BPR700gY.js";
1
+ import { $ as CrustErrorCode, A as SectionConsumer, At as MergeContext, B as ExtensionContext, C as InvocationOptions, Ct as ArgSnapshot, D as ParsedArgValue, Dt as LocalFlagNameBrand, E as ParseResult, Et as LocalFlagBrand, F as BuildReport, G as Finished, H as ExtensionFlagDef, I as DefineExtensionWith, J as NamedExtensionFlagDef, K as InferExtensionFlags, L as Extension, M as ValueType, Mt as defineExtensionId, N as BuildArtifacts, O as ParsedFlagValue, Ot as EmptyArgNameBrand, P as BuildFile, Q as CrustError, R as ExtensionBuildContext, S as InvocationIO, St as defineCommand, T as NamedFlagDef, Tt as FlagSnapshot, U as ExtensionHooks, V as ExtensionFactory, W as ExtensionSectionContribution, X as defineExtension, Y as RootMetaKey, Z as CommandNotFoundErrorDetails, _ as ExecuteOptions, _t as RootCommandMeta, a as ContextInstance, at as ValidationErrorDetails, b as InputArgs, bt as RunInputArguments, c as FactoryValueOf, ct as CommandDefinition, d as ArgDef, dt as CommandPath, et as CrustErrorDetails, f as ArgsDef, ft as CommandShape, gt as CrustCommandContext, h as CommandSectionInput, ht as Crust, i as ContextFactory, it as ParseErrorDetails, j as ValidatedInput, jt as ExtensionId, k as SectionAudience, kt as LocalValueBrand, l as contextSources, lt as CommandDefinitionBuilder, m as CommandSection, mt as CommandTree, n as ContextBag, nt as CrustErrorJson, o as ContextMap, ot as AnyCrust, p as CommandMeta, pt as CommandShapeAt, q as InvocationOutcome, r as ContextConfig, rt as DefinitionErrorDetails, s as ContextSetup, st as CommandConfig, t as AnyContextFactory, tt as CrustErrorDetailsMap, u as defineContext, ut as CommandHandle, v as FlagDef, vt as RunArguments, w as MergeFlags, wt as CommandSnapshot, x as InputFlags, xt as RunOutcome, y as FlagsDef, yt as RunInput, z as ExtensionConfig } from "./context-B1ck_WZ-.js";
2
2
  import { resolveArtifactDir } from "@crustjs/utils/artifacts";
3
3
  //#region src/api/flags.d.ts
4
4
  /** Distribute `Omit<_, "name">` over the {@link ArgDef} union. */
@@ -7,13 +7,13 @@ type OmitName<T> = T extends {
7
7
  } ? Omit<T, "name"> : never;
8
8
  /** A positional argument definition without its name — the `defineArg` input shape. */
9
9
  type UnnamedArgDef = OmitName<ArgDef>;
10
- type Frozen<T> = { readonly [K in keyof T]: K extends "aliases" | "choices" | (T extends {
10
+ type Frozen<T> = { readonly [K in keyof T]: K extends "aliases" | "choices" | "env" | (T extends {
11
11
  multiple: true;
12
12
  } ? "default" : never) ? Readonly<T[K]> : T[K]; };
13
13
  type Named<N extends string, D> = D extends unknown ? Frozen<{
14
14
  name: N;
15
15
  } & D> : never;
16
- /** Define and own one flag locally; attachment checks destination collisions. */
16
+ /** Define and own one flag locally, including its readonly env binding; attachment checks destination collisions. */
17
17
  export declare function defineFlag<const N extends string, const D extends FlagDef>(name: N & LocalFlagNameBrand<N>, def: D & LocalFlagBrand<{
18
18
  name: N;
19
19
  } & D>): Named<N, D>;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as CrustError, S as normalizeFlag, _ as contextSources, a as runInvocation, b as validateContextAvailability, d as cloneFlagRegistry, f as installExtensionContexts, g as defineExtension, h as registerFlag, i as resolveTypedPath, m as createCommandNode, n as executeInvocation, o as snapshotCommand, p as validateCommandSections, r as prepareInvocation, u as cloneCommandNode, v as defineContext, x as normalizeArg, y as definingOf } from "./invocation-BaWf5POY.js";
1
+ import { C as CrustError, S as normalizeFlag, _ as contextSources, a as runInvocation, b as validateContextAvailability, d as cloneFlagRegistry, f as installExtensionContexts, g as defineExtension, h as registerFlag, i as resolveTypedPath, m as createCommandNode, n as executeInvocation, o as snapshotCommand, p as validateCommandSections, r as prepareInvocation, u as cloneCommandNode, v as defineContext, x as normalizeArg, y as definingOf } from "./invocation-ka-BNJKZ.js";
2
2
  import { resolveArtifactDir } from "@crustjs/utils/artifacts";
3
3
  //#region src/identity.ts
4
4
  /** Mint an Extension identity from any non-blank, trimmed string. */
@@ -115,9 +115,16 @@ function ownDefinition(def) {
115
115
  ...def,
116
116
  ..."aliases" in def && def.aliases ? { aliases: Object.freeze([...def.aliases]) } : {},
117
117
  ...def.choices ? { choices: Object.freeze([...def.choices]) } : {},
118
+ ..."env" in def && def.env ? { env: Object.freeze({
119
+ name: def.env.name,
120
+ delimiter: def.env.delimiter
121
+ }) } : {},
118
122
  ..."multiple" in def && def.multiple && Array.isArray(def.default) ? { default: Object.freeze([...def.default]) } : {}
119
123
  });
120
124
  }
125
+ function isEnvBinding(value) {
126
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "name" in value && typeof value.name === "string" && (!("delimiter" in value) || value.delimiter === void 0 || typeof value.delimiter === "string");
127
+ }
121
128
  function normalizeFlag(name, def) {
122
129
  assertUsableSpelling(name, "canonical");
123
130
  if (def.short !== void 0) assertUsableSpelling(def.short, "short");
@@ -129,6 +136,29 @@ function normalizeFlag(name, def) {
129
136
  reason: "flag-collision"
130
137
  });
131
138
  if (def.short !== void 0 && def.short.length !== 1) throw new CrustError("DEFINITION", "Short flags must be one character");
139
+ if (def.env !== void 0 && !isEnvBinding(def.env)) throw new CrustError("DEFINITION", `Flag "${name}" env must be an object with a string name and optional string delimiter`, {
140
+ subject: "flag",
141
+ name,
142
+ reason: "invalid-env"
143
+ });
144
+ if (def.env?.name === "") throw new CrustError("DEFINITION", `Flag "${name}" env variable name must be non-empty`, {
145
+ subject: "flag",
146
+ name,
147
+ reason: "empty-env"
148
+ });
149
+ for (const [field, delimiter] of [["delimiter", def.delimiter], ["env.delimiter", def.env?.delimiter]]) {
150
+ if (delimiter === void 0) continue;
151
+ if (!def.multiple) throw new CrustError("DEFINITION", `Flag "${name}" ${field} requires multiple: true`, {
152
+ subject: "flag",
153
+ name,
154
+ reason: "delimiter-without-multiple"
155
+ });
156
+ if (delimiter === "") throw new CrustError("DEFINITION", `Flag "${name}" ${field} must be non-empty`, {
157
+ subject: "flag",
158
+ name,
159
+ reason: "empty-delimiter"
160
+ });
161
+ }
132
162
  return ownDefinition(def);
133
163
  }
134
164
  function normalizeArg(def) {
@@ -814,6 +844,60 @@ function resolveAliases(tokens, spellings) {
814
844
  }
815
845
  return canonical;
816
846
  }
847
+ /** Split repeatable occurrences on the declared delimiter, dropping empty segments. */
848
+ function splitOccurrences(values, delimiter) {
849
+ if (delimiter === void 0) return [...values];
850
+ return values.flatMap((value) => value.split(delimiter)).filter((value) => value !== "");
851
+ }
852
+ /**
853
+ * Convert an environment value into the shape argv tokens produce, so the
854
+ * shared coercion path (`choices`, `parse`, type conversion, schemas) runs
855
+ * identically. Booleans use the positional spelling rule (`true`/`1`);
856
+ * a `noNegate` flag rejects a false value like it rejects `--no-<name>`.
857
+ */
858
+ function envFlagValue(name, def, raw) {
859
+ const occurrences = def.multiple ? splitOccurrences([raw], def.env?.delimiter) : [raw];
860
+ if (def.multiple && occurrences.length === 0) return void 0;
861
+ if (def.type !== "boolean") return {
862
+ kind: "string",
863
+ value: def.multiple ? occurrences : raw
864
+ };
865
+ const values = occurrences.map(coerceBooleanString);
866
+ if ("noNegate" in def && def.noNegate && values.includes(false)) throw new CrustError("PARSE", `Flag "--${name}" does not support negation (from ${def.env?.name})`);
867
+ return {
868
+ kind: "boolean",
869
+ value: def.multiple ? values : values[0]
870
+ };
871
+ }
872
+ /**
873
+ * Select each flag's source: argv > `env` > `default`.
874
+ *
875
+ * Source selection looks at explicit argv presence *before* delimiter
876
+ * splitting, so `--tags ""` stays an argv value (zero occurrences, then
877
+ * default/required rules) instead of letting a stale environment variable
878
+ * override an explicit request. Environment lookup is argv-only; structured
879
+ * `run()` input never reaches this function.
880
+ */
881
+ function applyEnvAndDelimiter(flagsDef, argvValues, env) {
882
+ const values = {};
883
+ for (const [name, def] of Object.entries(flagsDef)) {
884
+ const argvValue = Object.hasOwn(argvValues, name) ? argvValues[name] : void 0;
885
+ if (argvValue !== void 0) {
886
+ if (def.delimiter !== void 0 && argvValue.kind === "string" && Array.isArray(argvValue.value)) {
887
+ const occurrences = splitOccurrences(argvValue.value, def.delimiter);
888
+ values[name] = occurrences.length === 0 ? void 0 : {
889
+ kind: "string",
890
+ value: occurrences
891
+ };
892
+ } else values[name] = argvValue;
893
+ continue;
894
+ }
895
+ if (def.env === void 0 || !Object.hasOwn(env, def.env.name)) continue;
896
+ const raw = env[def.env.name];
897
+ if (raw !== void 0) values[name] = envFlagValue(name, def, raw);
898
+ }
899
+ return values;
900
+ }
817
901
  /**
818
902
  * Resolve all flag definitions against the canonical parsed values.
819
903
  * Handles coercion and default values.
@@ -990,12 +1074,13 @@ function bind(command, positionals, flagValues, coerceArg, coerceFlag) {
990
1074
  *
991
1075
  * @param command - The command whose arg/flag definitions drive the parsing
992
1076
  * @param argv - The argv array to parse (typically `process.argv.slice(2)`)
1077
+ * @param env - Environment consulted for `FlagDef.env` fallbacks; pass `{}` to parse without one
993
1078
  * @returns Parsed args, flags, excessArgs (positionals before `--` not consumed by a declared argument), and rawArgs (everything after `--`)
994
1079
  * @throws {CrustError} On unknown flags or type coercion failure
995
1080
  */
996
- function parseArgs$1(command, argv) {
1081
+ function parseArgs$1(command, argv, env = process.env) {
997
1082
  const { positionals, flagValues, rawArgs } = tokenizeArgv(command, argv);
998
- const { args, flags, consumed } = bind(command, positionals, flagValues, coerceArgToken, coerceFlagValue);
1083
+ const { args, flags, consumed } = bind(command, positionals, applyEnvAndDelimiter(command.effectiveFlags, flagValues, env), coerceArgToken, coerceFlagValue);
999
1084
  return {
1000
1085
  args,
1001
1086
  flags,
@@ -1178,7 +1263,12 @@ function snapshotFlag(def) {
1178
1263
  negatable: isFlagNegatable(def),
1179
1264
  noNegate: "noNegate" in def ? def.noNegate : void 0,
1180
1265
  choices: def.choices ? Object.freeze([...def.choices]) : void 0,
1181
- default: serializableDefault(def.default)
1266
+ default: serializableDefault(def.default),
1267
+ env: def.env ? freezeCompact({
1268
+ name: def.env.name,
1269
+ delimiter: def.env.delimiter
1270
+ }) : void 0,
1271
+ delimiter: def.delimiter
1182
1272
  });
1183
1273
  }
1184
1274
  /**
package/dist/tooling.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Tt as FlagSnapshot, g as DeclaredDefault, jt as ExtensionId, m as CommandSection, wt as CommandSnapshot } from "./context-BPR700gY.js";
1
+ import { Tt as FlagSnapshot, g as DeclaredDefault, jt as ExtensionId, m as CommandSection, wt as CommandSnapshot } from "./context-B1ck_WZ-.js";
2
2
  import { BUILD_OUT_DIR_ENV } from "@crustjs/utils/artifacts";
3
3
  //#region src/command/invocation.d.ts
4
4
  /**
@@ -17,8 +17,11 @@ export declare const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
17
17
  //#region src/command/documentation.d.ts
18
18
  /** Format a declared default value for command documentation. */
19
19
  export declare function formatDefault(value: DeclaredDefault): string;
20
- /** Format a definition's description and optional default/choice annotations. */
21
- export declare function formatDescription(description: string | undefined, defaultValue: DeclaredDefault, choices: readonly string[] | undefined, formatAnnotation?: (annotation: string) => string): string;
20
+ /**
21
+ * Format a definition's description and optional env/default/choice annotations.
22
+ * `env` is the variable *name* only; renderers never see its value.
23
+ */
24
+ export declare function formatDescription(description: string | undefined, defaultValue: DeclaredDefault, choices: readonly string[] | undefined, formatAnnotation?: (annotation: string) => string, env?: string): string;
22
25
  /**
23
26
  * Presentation-ready view of one positional argument.
24
27
  *
@@ -90,6 +93,10 @@ interface DocumentationFlag {
90
93
  readonly choices?: readonly string[];
91
94
  /** Default value used when the flag is omitted, e.g. `false`. */
92
95
  readonly default?: unknown;
96
+ /** Declared environment binding; never contains the variable's value. */
97
+ readonly env?: FlagSnapshot["env"];
98
+ /** Delimiter that splits string argv values of a repeatable flag into occurrences. */
99
+ readonly delimiter?: string;
93
100
  }
94
101
  /**
95
102
  * One renderer-colorable piece of a usage line. `custom` is the sole segment
package/dist/tooling.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as sectionsFor, l as visibleSectionsFor, s as isListed, t as SNAPSHOT_PATH_ENV } from "./invocation-BaWf5POY.js";
1
+ import { c as sectionsFor, l as visibleSectionsFor, s as isListed, t as SNAPSHOT_PATH_ENV } from "./invocation-ka-BNJKZ.js";
2
2
  import { BUILD_OUT_DIR_ENV } from "@crustjs/utils/artifacts";
3
3
  //#region src/command/documentation.ts
4
4
  function isNonFiniteNumber(value) {
@@ -10,9 +10,13 @@ function formatDefault(value) {
10
10
  if (Array.isArray(value)) return value.map(String).join(", ");
11
11
  return JSON.stringify(value) ?? String(value);
12
12
  }
13
- /** Format a definition's description and optional default/choice annotations. */
14
- function formatDescription(description, defaultValue, choices, formatAnnotation = (annotation) => annotation) {
13
+ /**
14
+ * Format a definition's description and optional env/default/choice annotations.
15
+ * `env` is the variable *name* only; renderers never see its value.
16
+ */
17
+ function formatDescription(description, defaultValue, choices, formatAnnotation = (annotation) => annotation, env) {
15
18
  const parts = description ? [description] : [];
19
+ if (env !== void 0) parts.push(formatAnnotation(`[env: ${env}]`));
16
20
  if (defaultValue !== void 0) parts.push(formatAnnotation(`[default: ${formatDefault(defaultValue)}]`));
17
21
  if (choices?.length) parts.push(formatAnnotation(`[choices: ${choices.join(", ")}]`));
18
22
  return parts.join(" ");
@@ -39,7 +43,9 @@ function documentationFlags(flags) {
39
43
  required: def.required === true,
40
44
  multiple: def.multiple === true,
41
45
  choices: def.choices,
42
- default: def.default
46
+ default: def.default,
47
+ env: def.env,
48
+ delimiter: def.delimiter
43
49
  });
44
50
  });
45
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crustjs/core",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "Core library for the Crust CLI framework",
5
5
  "type": "module",
6
6
  "sideEffects": false,