stitchkit 0.110.4 → 0.110.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/CHANGELOG.md CHANGED
@@ -15,6 +15,12 @@ additive**; the first breaking change landed in 0.10.0. Grep the file for
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [0.110.5] - 2026-10-15
19
+
20
+ ### Added
21
+
22
+ - `stitchkit/cli`: `--field:=JSON` assigns a whole JSON value, including explicit `null` for nullable fields, through the existing schema-driven parser. Ordinary `--field null` remains the string `"null"`; optional omission stays absent. Native/discovered help advertises the syntax; malformed JSON and conflicting assignments refuse before dispatch.
23
+
18
24
  ## [0.110.4] - 2026-10-15
19
25
 
20
26
  ### Fixed
@@ -16,7 +16,7 @@ import {
16
16
  renderCliView2,
17
17
  pollUntilDone2,
18
18
  createCli2
19
- } from "../index-2tazbwhv.js";
19
+ } from "../index-n7hrpngg.js";
20
20
  import"../index-fynr93w4.js";
21
21
  import"../index-ct75c3at.js";
22
22
  import"../index-q6crhe1n.js";
@@ -74,7 +74,7 @@ import {
74
74
  WaitTimeoutError,
75
75
  runWaitOperation,
76
76
  createCli2
77
- } from "../index-2tazbwhv.js";
77
+ } from "../index-n7hrpngg.js";
78
78
  import {
79
79
  ManagedFilePathSchema2,
80
80
  ManagedFileRefSchema2
@@ -830,88 +830,28 @@ function routeCliArgv2(argv, defaultCommand) {
830
830
  // src/tools/cli/args.ts
831
831
  import { z as z2 } from "zod";
832
832
 
833
- // src/tools/cli/args-view.ts
834
- function resolveCliView(flags, ascending) {
835
- if (flags.size === 0) {
836
- if (ascending) {
837
- throw new CliArgumentError2("--ascending orders a record view; pass --sort with it");
838
- }
839
- return;
840
- }
841
- const countBy = flags.get("count-by");
842
- const sum = flags.get("sum");
843
- const table = flags.get("table");
844
- const by = flags.get("by");
845
- const rawTop = flags.get("top");
846
- const named = [
847
- ["--count-by", countBy],
848
- ["--sum", sum]
849
- ].filter(([, value]) => value !== undefined);
850
- if (named.length > 1) {
851
- throw new CliArgumentError2(`${named.map(([flag]) => flag).join(" and ")} ask for different shapes — pass one`);
852
- }
853
- let top;
854
- if (rawTop !== undefined) {
855
- top = Number(rawTop);
856
- if (!Number.isInteger(top) || top <= 0) {
857
- throw new CliArgumentError2("--top must be a positive whole number");
858
- }
859
- }
860
- const sort = flags.get("sort");
861
- if (table !== undefined || sort !== undefined) {
862
- if (countBy !== undefined || sum !== undefined) {
863
- throw new CliArgumentError2(`${table !== undefined ? "--table" : "--sort"} lists records; --count-by and --sum aggregate them — pass one`);
864
- }
865
- if (by !== undefined) {
866
- throw new CliArgumentError2("--by groups a view; a record view orders with --sort");
867
- }
868
- if (top !== undefined && sort === undefined) {
869
- throw new CliArgumentError2("--top needs --sort here: unordered records have no n largest");
870
- }
871
- let fields;
872
- if (table !== undefined) {
873
- fields = table.split(",").map((field) => field.trim()).filter(Boolean);
874
- if (fields.length === 0)
875
- throw new CliArgumentError2("--table needs at least one field");
876
- }
877
- return {
878
- kind: "records",
879
- ...sort !== undefined && { sort },
880
- ...ascending && { ascending },
881
- ...top !== undefined && { top },
882
- ...fields && { table: fields }
883
- };
884
- }
885
- if (ascending) {
886
- throw new CliArgumentError2("--ascending orders a record view; pass --sort with it");
887
- }
888
- if (sum !== undefined) {
889
- if (top !== undefined && by === undefined) {
890
- throw new CliArgumentError2("--top needs --by: a single sum has nothing to rank");
891
- }
892
- return {
893
- kind: "sum",
894
- field: sum,
895
- ...by !== undefined && { by },
896
- ...top !== undefined && { top }
897
- };
898
- }
899
- if (countBy !== undefined) {
900
- if (by !== undefined) {
901
- throw new CliArgumentError2("--count-by already names the grouping field; drop --by");
902
- }
903
- return { kind: "count", field: countBy, ...top !== undefined && { top } };
904
- }
905
- if (by !== undefined) {
906
- if (top === undefined) {
907
- throw new CliArgumentError2("--by groups a view; pass --count-by, --sum or --top with it");
908
- }
909
- return { kind: "count", field: by, top };
833
+ // src/tools/cli/args-json.ts
834
+ function cliFieldOption(option, fields) {
835
+ const json = option.inline && option.name.endsWith(":") && !fields.has(option.name);
836
+ const name = json ? option.name.slice(0, -1) : option.name;
837
+ return { name, json };
838
+ }
839
+ function appendCliFieldValue(raw, jsonFields, name, value, info, json) {
840
+ appendCliOptionValue(raw, name, value, json || jsonFields.has(name) ? { kind: "other" } : info);
841
+ if (json)
842
+ jsonFields.add(name);
843
+ }
844
+ function coerceCliFieldValue(info, values, label, json) {
845
+ if (!json)
846
+ return coerceField(info, values, label);
847
+ try {
848
+ return safeJsonParse(values[0] ?? "");
849
+ } catch (cause) {
850
+ throw new CliArgumentError2(`${label} expects valid JSON`, { cause });
910
851
  }
911
- throw new CliArgumentError2("--top needs --sort, --by, --count-by or --sum to rank");
912
852
  }
913
853
 
914
- // src/tools/cli/args.ts
854
+ // src/tools/cli/args-tokens.ts
915
855
  function readCliTokens(argv, fields, fieldFor, config) {
916
856
  const options = {
917
857
  json: false,
@@ -921,13 +861,14 @@ function readCliTokens(argv, fields, fieldFor, config) {
921
861
  help: false
922
862
  };
923
863
  const flags = new Map;
864
+ const jsonFields = new Set;
924
865
  const literalBooleanFlags = new Set;
925
866
  const frameworkFlags = new Map;
926
867
  const viewFlags = new Map;
927
868
  let ascending = false;
928
869
  const positionals = [];
929
- const pushFlag = (name, value) => {
930
- appendCliOptionValue(flags, name, value, fieldFor(name));
870
+ const pushFlag = (name, value, json = false) => {
871
+ appendCliFieldValue(flags, jsonFields, name, value, fieldFor(name), json);
931
872
  };
932
873
  let optionsEnded = false;
933
874
  for (let i = 0;i < argv.length; i++) {
@@ -980,10 +921,13 @@ function readCliTokens(argv, fields, fieldFor, config) {
980
921
  const option = classifyLongOptionToken(tok);
981
922
  if (!option)
982
923
  throw new CliArgumentError2(`Unknown option "${tok}"`);
983
- const { name } = option;
924
+ const { name, json } = cliFieldOption(option, fields);
984
925
  let { value } = option;
985
926
  if (name.length === 0)
986
927
  throw new CliArgumentError2("Invalid empty option name");
928
+ if (json && RESERVED_CLI_OPTIONS.has(name)) {
929
+ throw new CliArgumentError2(`--${name} is a framework option and does not accept :=JSON`);
930
+ }
987
931
  if (option.globalKind === "boolean") {
988
932
  const separate = value === undefined ? separateBoolValue(argv[i + 1]) : undefined;
989
933
  if (separate !== undefined)
@@ -1061,12 +1005,103 @@ function readCliTokens(argv, fields, fieldFor, config) {
1061
1005
  throw new CliArgumentError2(`--${name} requires a value`);
1062
1006
  }
1063
1007
  }
1064
- pushFlag(name, value);
1008
+ pushFlag(name, value, json);
1065
1009
  }
1066
- return { options, flags, literalBooleanFlags, viewFlags, ascending, positionals };
1010
+ return {
1011
+ options,
1012
+ flags,
1013
+ jsonFields,
1014
+ literalBooleanFlags,
1015
+ viewFlags,
1016
+ ascending,
1017
+ positionals
1018
+ };
1067
1019
  }
1020
+
1021
+ // src/tools/cli/args-view.ts
1022
+ function resolveCliView(flags, ascending) {
1023
+ if (flags.size === 0) {
1024
+ if (ascending) {
1025
+ throw new CliArgumentError2("--ascending orders a record view; pass --sort with it");
1026
+ }
1027
+ return;
1028
+ }
1029
+ const countBy = flags.get("count-by");
1030
+ const sum = flags.get("sum");
1031
+ const table = flags.get("table");
1032
+ const by = flags.get("by");
1033
+ const rawTop = flags.get("top");
1034
+ const named = [
1035
+ ["--count-by", countBy],
1036
+ ["--sum", sum]
1037
+ ].filter(([, value]) => value !== undefined);
1038
+ if (named.length > 1) {
1039
+ throw new CliArgumentError2(`${named.map(([flag]) => flag).join(" and ")} ask for different shapes — pass one`);
1040
+ }
1041
+ let top;
1042
+ if (rawTop !== undefined) {
1043
+ top = Number(rawTop);
1044
+ if (!Number.isInteger(top) || top <= 0) {
1045
+ throw new CliArgumentError2("--top must be a positive whole number");
1046
+ }
1047
+ }
1048
+ const sort = flags.get("sort");
1049
+ if (table !== undefined || sort !== undefined) {
1050
+ if (countBy !== undefined || sum !== undefined) {
1051
+ throw new CliArgumentError2(`${table !== undefined ? "--table" : "--sort"} lists records; --count-by and --sum aggregate them — pass one`);
1052
+ }
1053
+ if (by !== undefined) {
1054
+ throw new CliArgumentError2("--by groups a view; a record view orders with --sort");
1055
+ }
1056
+ if (top !== undefined && sort === undefined) {
1057
+ throw new CliArgumentError2("--top needs --sort here: unordered records have no n largest");
1058
+ }
1059
+ let fields;
1060
+ if (table !== undefined) {
1061
+ fields = table.split(",").map((field) => field.trim()).filter(Boolean);
1062
+ if (fields.length === 0)
1063
+ throw new CliArgumentError2("--table needs at least one field");
1064
+ }
1065
+ return {
1066
+ kind: "records",
1067
+ ...sort !== undefined && { sort },
1068
+ ...ascending && { ascending },
1069
+ ...top !== undefined && { top },
1070
+ ...fields && { table: fields }
1071
+ };
1072
+ }
1073
+ if (ascending) {
1074
+ throw new CliArgumentError2("--ascending orders a record view; pass --sort with it");
1075
+ }
1076
+ if (sum !== undefined) {
1077
+ if (top !== undefined && by === undefined) {
1078
+ throw new CliArgumentError2("--top needs --by: a single sum has nothing to rank");
1079
+ }
1080
+ return {
1081
+ kind: "sum",
1082
+ field: sum,
1083
+ ...by !== undefined && { by },
1084
+ ...top !== undefined && { top }
1085
+ };
1086
+ }
1087
+ if (countBy !== undefined) {
1088
+ if (by !== undefined) {
1089
+ throw new CliArgumentError2("--count-by already names the grouping field; drop --by");
1090
+ }
1091
+ return { kind: "count", field: countBy, ...top !== undefined && { top } };
1092
+ }
1093
+ if (by !== undefined) {
1094
+ if (top === undefined) {
1095
+ throw new CliArgumentError2("--by groups a view; pass --count-by, --sum or --top with it");
1096
+ }
1097
+ return { kind: "count", field: by, top };
1098
+ }
1099
+ throw new CliArgumentError2("--top needs --sort, --by, --count-by or --sum to rank");
1100
+ }
1101
+
1102
+ // src/tools/cli/args.ts
1068
1103
  function buildToolArgs(tokens, fields, fieldFor, declaredPositionals, allowUnknown) {
1069
- const { flags, literalBooleanFlags, positionals } = tokens;
1104
+ const { flags, jsonFields, literalBooleanFlags, positionals } = tokens;
1070
1105
  const toolArgs = {};
1071
1106
  const fillable = declaredPositionals === undefined ? [...fields.entries()].filter(([, info]) => info.kind !== "boolean").map(([name]) => name) : [...declaredPositionals];
1072
1107
  const variadicTail = declaredPositionals !== undefined && fields.get(fillable[fillable.length - 1] ?? "")?.kind === "array" ? fillable[fillable.length - 1] : undefined;
@@ -1105,16 +1140,23 @@ function buildToolArgs(tokens, fields, fieldFor, declaredPositionals, allowUnkno
1105
1140
  throw new CliArgumentError2(`--${root} conflicts with --${dotted} — pass one form, not both`);
1106
1141
  }
1107
1142
  }
1143
+ for (const key of jsonFields) {
1144
+ for (const other of flags.keys()) {
1145
+ if (other.startsWith(`${key}.`) || key.startsWith(`${other}.`)) {
1146
+ throw new CliArgumentError2(`--${key} conflicts with --${other} — pass one form, not both`);
1147
+ }
1148
+ }
1149
+ }
1108
1150
  for (const [key, values] of flags) {
1109
1151
  const info = fieldFor(key);
1110
1152
  const exactLiteralBoolean = literalBooleanFlags.has(key) && fields.has(key);
1111
1153
  if (key.includes(".") && !exactLiteralBoolean) {
1112
1154
  const root = key.split(".")[0] ?? key;
1113
- const value = info === undefined && allowUnknown && !fields.has(root) ? looseCoerce(values[0] ?? "") : coerceField(info, values, `--${key}`);
1155
+ const value = !jsonFields.has(key) && info === undefined && allowUnknown && !fields.has(root) ? looseCoerce(values[0] ?? "") : coerceCliFieldValue(info, values, `--${key}`, jsonFields.has(key));
1114
1156
  setNested(toolArgs, key.split("."), value);
1115
1157
  continue;
1116
1158
  }
1117
- toolArgs[key] = coerceField(info, values, `--${key}`);
1159
+ toolArgs[key] = coerceCliFieldValue(info, values, `--${key}`, jsonFields.has(key));
1118
1160
  }
1119
1161
  return toolArgs;
1120
1162
  }
@@ -1146,6 +1188,7 @@ function extractCliGlobalOptions2(argv, schema) {
1146
1188
  const fields = describeSchemaFields(schema);
1147
1189
  const rest = [];
1148
1190
  const raw = new Map;
1191
+ const jsonFields = new Set;
1149
1192
  let ended = false;
1150
1193
  for (let i = 0;i < argv.length; i++) {
1151
1194
  const token = argv[i];
@@ -1154,8 +1197,9 @@ function extractCliGlobalOptions2(argv, schema) {
1154
1197
  if (!ended && token === "--")
1155
1198
  ended = true;
1156
1199
  const option = ended ? undefined : classifyLongOptionToken(token);
1157
- const info = option && option.globalKind === undefined ? fields.get(option.name) : undefined;
1158
- if (!option || !info) {
1200
+ const field = option ? cliFieldOption(option, fields) : undefined;
1201
+ const info = field && option?.globalKind === undefined ? fields.get(field.name) : undefined;
1202
+ if (!option || !field || !info) {
1159
1203
  rest.push(token);
1160
1204
  continue;
1161
1205
  }
@@ -1174,11 +1218,11 @@ function extractCliGlobalOptions2(argv, schema) {
1174
1218
  throw new CliArgumentError2(`--${option.name} requires a value`);
1175
1219
  }
1176
1220
  }
1177
- appendCliOptionValue(raw, option.name, value, info);
1221
+ appendCliFieldValue(raw, jsonFields, field.name, value, info, field.json);
1178
1222
  }
1179
1223
  const args = {};
1180
1224
  for (const [name, values] of raw) {
1181
- args[name] = coerceField(fields.get(name), values, `--${name}`);
1225
+ args[name] = coerceCliFieldValue(fields.get(name), values, `--${name}`, jsonFields.has(name));
1182
1226
  }
1183
1227
  let coerced;
1184
1228
  try {
@@ -1664,6 +1708,20 @@ function constraintLabel(schema) {
1664
1708
  function fieldTypeLabel(schema) {
1665
1709
  return `<${typeLabel(schema)}>${constraintLabel(schema)}`;
1666
1710
  }
1711
+ function nullableField(schema) {
1712
+ if (schema.type === "null")
1713
+ return true;
1714
+ if (Array.isArray(schema.type) && schema.type.includes("null"))
1715
+ return true;
1716
+ return Array.isArray(schema.anyOf) && schema.anyOf.some((member) => isRecord(member) && nullableField(member));
1717
+ }
1718
+ function jsonValueHelp(fields) {
1719
+ const example = fields.find((field) => nullableField(field.schema))?.name;
1720
+ return [
1721
+ "Assign a whole JSON value with --field:=JSON; the declared schema still validates it.",
1722
+ ...example === undefined ? [] : [`JSON null: --${example}:=null. The literal string stays --${example} null.`]
1723
+ ];
1724
+ }
1667
1725
  function padRight(text, width) {
1668
1726
  return text.length >= width ? text : text + " ".repeat(width - text.length);
1669
1727
  }
@@ -1717,6 +1775,7 @@ function renderTopHelp(input) {
1717
1775
  for (const [flag, desc] of GLOBAL_OPTIONS)
1718
1776
  lines.push(` ${padRight(flag, optWidth)} ${desc}`);
1719
1777
  lines.push(...applicationOptionLines(input.applicationOptions));
1778
+ lines.push("", ...jsonValueHelp(input.applicationOptions));
1720
1779
  lines.push("", `Run "${name} <command> --help" for command-specific flags.`);
1721
1780
  return `${lines.join(`
1722
1781
  `)}
@@ -1763,7 +1822,7 @@ function renderCommandHelp(name, command, descriptor, applicationOptions = []) {
1763
1822
  if (flag !== undefined) {
1764
1823
  lines.push("", `A boolean flag is on alone or takes true/false: --${flag}, --${flag} false, --no-${flag}`);
1765
1824
  }
1766
- lines.push("");
1825
+ lines.push("", ...jsonValueHelp(fields), "");
1767
1826
  }
1768
1827
  const applicationLines = applicationOptionLines(applicationOptions);
1769
1828
  if (applicationLines.length > 0)
@@ -0,0 +1,12 @@
1
+ import { type FieldInfo } from './args-fields.js';
2
+ import type { CliLongOptionToken } from './args-route.js';
3
+ /** An inline := assigns a whole JSON value; an exact declared colon name stays literal. */
4
+ export declare function cliFieldOption(option: CliLongOptionToken, fields: ReadonlyMap<string, FieldInfo>): {
5
+ name: string;
6
+ json: boolean;
7
+ };
8
+ /** Whole JSON assignments cannot accumulate or mix with repeated element flags. */
9
+ export declare function appendCliFieldValue(raw: Map<string, string[]>, jsonFields: Set<string>, name: string, value: string, info: FieldInfo | undefined, json: boolean): void;
10
+ /** JSON assigns a value without scalar coercion; the command schema still validates it. */
11
+ export declare function coerceCliFieldValue(info: FieldInfo | undefined, values: string[], label: string, json: boolean): unknown;
12
+ //# sourceMappingURL=args-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args-json.d.ts","sourceRoot":"","sources":["../../../src/tools/cli/args-json.ts"],"names":[],"mappings":"AACA,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAClF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGvD,2FAA2F;AAC3F,wBAAgB,cAAc,CAC5B,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,GACrC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAIjC;AAED,mFAAmF;AACnF,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAC1B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,SAAS,GAAG,SAAS,EAC3B,IAAI,EAAE,OAAO,GACZ,IAAI,CAQN;AAED,2FAA2F;AAC3F,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,SAAS,GAAG,SAAS,EAC3B,MAAM,EAAE,MAAM,EAAE,EAChB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,OAAO,GACZ,OAAO,CAOT"}
@@ -0,0 +1,27 @@
1
+ import type { CliRunOptions } from './args.js';
2
+ import { type describeSchemaFields, type FieldInfo } from './args-fields.js';
3
+ type CliConfigForParse = {
4
+ allowUnknown?: boolean;
5
+ optionAliases?: ReadonlyMap<string, string>;
6
+ };
7
+ type FieldResolver = (name: string) => FieldInfo | undefined;
8
+ /** One invocation's argv, sorted into what each token is for. */
9
+ export interface CliTokens {
10
+ options: CliRunOptions;
11
+ /** Raw values per flag, in order; only an array field may have several. */
12
+ flags: Map<string, string[]>;
13
+ jsonFields: Set<string>;
14
+ /** Boolean presence and aliases name exact fields, even when the name has a dot. */
15
+ literalBooleanFlags: Set<string>;
16
+ viewFlags: Map<string, string>;
17
+ ascending: boolean;
18
+ positionals: string[];
19
+ }
20
+ /**
21
+ * Phase one: tokenise. Every refusal about the SHAPE of the command line —
22
+ * an unknown option, a missing value, a prototype-polluting name, a repeated
23
+ * non-array flag — happens here, before its raw multiplicity can be lost.
24
+ */
25
+ export declare function readCliTokens(argv: readonly string[], fields: ReturnType<typeof describeSchemaFields>, fieldFor: FieldResolver, config: CliConfigForParse): CliTokens;
26
+ export {};
27
+ //# sourceMappingURL=args-tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args-tokens.d.ts","sourceRoot":"","sources":["../../../src/tools/cli/args-tokens.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,SAAS,EAGf,MAAM,eAAe,CAAC;AAWvB,KAAK,iBAAiB,GAAG;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C,CAAC;AACF,KAAK,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,CAAC;AAE7D,iEAAiE;AACjE,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,aAAa,CAAC;IACvB,2EAA2E;IAC3E,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,oFAAoF;IACpF,mBAAmB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,MAAM,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,EAC/C,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,iBAAiB,GACxB,SAAS,CAqKX"}
@@ -44,6 +44,10 @@ export interface ParsedCliArgs {
44
44
  /** The CLI-behaviour flags. */
45
45
  options: CliRunOptions;
46
46
  }
47
+ /**
48
+ * Parse one command's argv against its schema: lexical/coerced flags, explicit
49
+ * `--field:=JSON`, dotted paths, boolean presence and declared positionals.
50
+ */
47
51
  export declare function parseCliArgs(argv: string[], schema: z.ZodType | undefined, config?: {
48
52
  allowUnknown?: boolean;
49
53
  knownFields?: readonly string[];
@@ -1 +1 @@
1
- {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../../src/tools/cli/args.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAoBxB,OAAO,EAAE,KAAK,aAAa,EAAkB,MAAM,aAAa,CAAC;AAGjE,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,IAAI,EAAE,OAAO,CAAC;IACd,iEAAiE;IACjE,IAAI,EAAE,OAAO,CAAC;IACd,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,IAAI,EAAE,OAAO,CAAC;IACd,0FAA0F;IAC1F,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,+BAA+B;IAC/B,OAAO,EAAE,aAAa,CAAC;CACxB;AAiSD,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,EAAE,CAAC,CAAC,OAAO,GAAG,SAAS,EAC7B,MAAM,GAAE;IACN,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,GACL,aAAa,CAsBf;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,0EAA0E;IAC1E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,MAAM,EAAE,CAAC,CAAC,SAAS,GAAG,SAAS,GAC9B,qBAAqB,CAkDvB"}
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../../src/tools/cli/args.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,OAAO,EAAE,KAAK,aAAa,EAAkB,MAAM,aAAa,CAAC;AAMjE,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,IAAI,EAAE,OAAO,CAAC;IACd,iEAAiE;IACjE,IAAI,EAAE,OAAO,CAAC;IACd,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,IAAI,EAAE,OAAO,CAAC;IACd,0FAA0F;IAC1F,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,+BAA+B;IAC/B,OAAO,EAAE,aAAa,CAAC;CACxB;AA6GD;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,EAAE,CAAC,CAAC,OAAO,GAAG,SAAS,EAC7B,MAAM,GAAE;IACN,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,GACL,aAAa,CAsBf;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,0EAA0E;IAC1E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,MAAM,EAAE,CAAC,CAAC,SAAS,GAAG,SAAS,GAC9B,qBAAqB,CAyDvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../src/tools/cli/help.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,eAAe,EAAoB,MAAM,+BAA+B,CAAC;AAEvF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AA+GvD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAC7C,MAAM,EAAE,MAAM,GACb,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,CASrC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;IAC9C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,SAAS,eAAe,EAAE,CAAC;IAC/C,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,MAAM,CAqCT;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,sBAAsB,EAClC,kBAAkB,GAAE,SAAS,eAAe,EAAO,GAClD,MAAM,CA6DR"}
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../src/tools/cli/help.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,eAAe,EAAoB,MAAM,+BAA+B,CAAC;AAEvF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAkIvD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAC7C,MAAM,EAAE,MAAM,GACb,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,CASrC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;IAC9C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,SAAS,eAAe,EAAE,CAAC;IAC/C,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,MAAM,CAsCT;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,sBAAsB,EAClC,kBAAkB,GAAE,SAAS,eAAe,EAAO,GAClD,MAAM,CA6DR"}
package/llms/cli.2.txt CHANGED
@@ -183,11 +183,16 @@ the code (so `exitCodes` can map it instead of every remote failure exiting `1`)
183
183
  the message the operator needs, and the error's own class — a plain `Error` is
184
184
  an *unexpected* error to the runner, which would print a code frame of the framework
185
185
  bundle before the JSON failure and scrub it to `INTERNAL_SERVER_ERROR`.
186
- A structured `{ error, details }` body is relayed as the contract error it
186
+ A `tools/call` response with `isError: true` and a structured `{ error, details }` body is relayed as the contract error it
187
187
  is, on every transport; anything else fails as `UPSTREAM_TOOL_ERROR` carrying
188
188
  what the server did send. Not `INTERNAL_SERVER_ERROR`, because nothing of ours
189
189
  broke.
190
190
 
191
+ The CLI writes this refusal to stderr, leaves stdout empty and exits with the
192
+ mapped nonzero error code (`1` for an unmapped code). A successful response may
193
+ contain an ordinary field named `error`; its payload alone does not establish a
194
+ tool failure.
195
+
191
196
  **On the CLI a discovered command prints the answer, not the envelope.**
192
197
  `tools/call` returns `{ content: [...], structuredContent? }`, and an agent mount
193
198
  needs exactly that — the parts are what a model is shown. The CLI is different in
@@ -266,7 +271,7 @@ SDK nor the `ai` peer.
266
271
  |--------|------|---------|
267
272
  | `createCli` | function | build and run a CLI from contracts — [guide](../guide/cli.md) |
268
273
  | `defineCliCommand` | function | define one Zod-typed CLI-only executable command with optional validated-result presentation/exit policy |
269
- | `parseCliArgs` | function | argv → typed tool args against a schema (advanced) |
274
+ | `parseCliArgs` | function | argv → typed tool args against a schema; inline `--field:=JSON` assigns a whole JSON value, including null, while ordinary string flags remain lexical (advanced) |
270
275
  | `routeCliArgv` | function | select the command out of argv without duplicating the global-option grammar (advanced) |
271
276
  | `extractCliGlobalOptions` | function | lift the application's own global options out of argv before routing (advanced) |
272
277
  | `coerceJsonArgs` | function | the second half of `parseCliArgs` — parse array/object values a consumer sends itself, including discovered MCP object guards restored from JSON Schema, without the `stitchkit/tools` barrel |
package/llms/cli.txt CHANGED
@@ -247,6 +247,23 @@ string, the schema says what it should be:
247
247
  | `z.record(z.string(), z.unknown())` | `--data '{"code":"007"}'` or `--data.code=007` → `"007"` |
248
248
  | `.optional()` / `.default()` | not required |
249
249
 
250
+ Use the inline `:=` form to assign a whole JSON value without scalar coercion:
251
+
252
+ ```bash
253
+ myapp upsert --productId:=null --count:=null
254
+ myapp upsert --productId null # the literal string "null"
255
+ myapp upsert --productId:='"null"' # the same string, written as JSON
256
+ ```
257
+
258
+ `null` is accepted only when the unchanged command schema permits it. Omitting an
259
+ optional field leaves it absent; it does not imply `null`. The same syntax works
260
+ for application global options and dotted leaves (`--settings.note:=null`). A
261
+ JSON assignment supplies the whole field, so an array uses one assignment such as
262
+ `--tags:='["a","b"]'`; mixing it with repeated element flags or another assignment
263
+ is refused. Invalid JSON is a usage error before the handler runs. Framework
264
+ options such as `--json` retain their existing grammar. An exact declared field
265
+ name ending in `:` retains its existing inline spelling.
266
+
250
267
  Primitive literals use the same coercion through optional, nullable and default wrappers and as
251
268
  array elements. A literal set whose members have different primitive types stays lexical because
252
269
  argv has no type tag: for `z.literal([7, '007'])`, `--value 007` remains the string `"007"` rather
package/llms-full.txt CHANGED
@@ -9773,6 +9773,23 @@ string, the schema says what it should be:
9773
9773
  | `z.record(z.string(), z.unknown())` | `--data '{"code":"007"}'` or `--data.code=007` → `"007"` |
9774
9774
  | `.optional()` / `.default()` | not required |
9775
9775
 
9776
+ Use the inline `:=` form to assign a whole JSON value without scalar coercion:
9777
+
9778
+ ```bash
9779
+ myapp upsert --productId:=null --count:=null
9780
+ myapp upsert --productId null # the literal string "null"
9781
+ myapp upsert --productId:='"null"' # the same string, written as JSON
9782
+ ```
9783
+
9784
+ `null` is accepted only when the unchanged command schema permits it. Omitting an
9785
+ optional field leaves it absent; it does not imply `null`. The same syntax works
9786
+ for application global options and dotted leaves (`--settings.note:=null`). A
9787
+ JSON assignment supplies the whole field, so an array uses one assignment such as
9788
+ `--tags:='["a","b"]'`; mixing it with repeated element flags or another assignment
9789
+ is refused. Invalid JSON is a usage error before the handler runs. Framework
9790
+ options such as `--json` retain their existing grammar. An exact declared field
9791
+ name ending in `:` retains its existing inline spelling.
9792
+
9776
9793
  Primitive literals use the same coercion through optional, nullable and default wrappers and as
9777
9794
  array elements. A literal set whose members have different primitive types stays lexical because
9778
9795
  argv has no type tag: for `z.literal([7, '007'])`, `--value 007` remains the string `"007"` rather
@@ -10635,11 +10652,16 @@ the code (so `exitCodes` can map it instead of every remote failure exiting `1`)
10635
10652
  the message the operator needs, and the error's own class — a plain `Error` is
10636
10653
  an *unexpected* error to the runner, which would print a code frame of the framework
10637
10654
  bundle before the JSON failure and scrub it to `INTERNAL_SERVER_ERROR`.
10638
- A structured `{ error, details }` body is relayed as the contract error it
10655
+ A `tools/call` response with `isError: true` and a structured `{ error, details }` body is relayed as the contract error it
10639
10656
  is, on every transport; anything else fails as `UPSTREAM_TOOL_ERROR` carrying
10640
10657
  what the server did send. Not `INTERNAL_SERVER_ERROR`, because nothing of ours
10641
10658
  broke.
10642
10659
 
10660
+ The CLI writes this refusal to stderr, leaves stdout empty and exits with the
10661
+ mapped nonzero error code (`1` for an unmapped code). A successful response may
10662
+ contain an ordinary field named `error`; its payload alone does not establish a
10663
+ tool failure.
10664
+
10643
10665
  **On the CLI a discovered command prints the answer, not the envelope.**
10644
10666
  `tools/call` returns `{ content: [...], structuredContent? }`, and an agent mount
10645
10667
  needs exactly that — the parts are what a model is shown. The CLI is different in
@@ -23158,7 +23180,7 @@ SDK nor the `ai` peer.
23158
23180
  |--------|------|---------|
23159
23181
  | `createCli` | function | build and run a CLI from contracts — [guide](../guide/cli.md) |
23160
23182
  | `defineCliCommand` | function | define one Zod-typed CLI-only executable command with optional validated-result presentation/exit policy |
23161
- | `parseCliArgs` | function | argv → typed tool args against a schema (advanced) |
23183
+ | `parseCliArgs` | function | argv → typed tool args against a schema; inline `--field:=JSON` assigns a whole JSON value, including null, while ordinary string flags remain lexical (advanced) |
23162
23184
  | `routeCliArgv` | function | select the command out of argv without duplicating the global-option grammar (advanced) |
23163
23185
  | `extractCliGlobalOptions` | function | lift the application's own global options out of argv before routing (advanced) |
23164
23186
  | `coerceJsonArgs` | function | the second half of `parseCliArgs` — parse array/object values a consumer sends itself, including discovered MCP object guards restored from JSON Schema, without the `stitchkit/tools` barrel |
package/llms.txt CHANGED
@@ -18,7 +18,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
18
18
  - **stitchkit/tools/contract**: [llms/tools-contract.txt](llms/tools-contract.txt) 0.2 KB
19
19
  - **stitchkit/tools/invoker**: [llms/tools-invoker.txt](llms/tools-invoker.txt) 0.2 KB
20
20
  - **stitchkit/tools/connections**: [llms/tools-connections.txt](llms/tools-connections.txt) 3.7 KB
21
- - **stitchkit/cli**: [llms/cli.txt](llms/cli.txt) 43.8 KB → [llms/cli.2.txt](llms/cli.2.txt) 20.2 KB
21
+ - **stitchkit/cli**: [llms/cli.txt](llms/cli.txt) 44.6 KB → [llms/cli.2.txt](llms/cli.2.txt) 20.6 KB
22
22
  - **stitchkit/cli/publish**: [llms/cli-publish.txt](llms/cli-publish.txt) 1.5 KB
23
23
  - **stitchkit/remote**: [llms/remote.txt](llms/remote.txt) 1.0 KB
24
24
  - **stitchkit/contract**: [llms/contract.txt](llms/contract.txt) 31.9 KB
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.110.4",
3
+ "version": "0.110.5",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",