@north-light/api 0.3.518 → 0.3.520

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.
@@ -1,10 +1,12 @@
1
1
  export interface DeclaredOutputShape {
2
2
  type: string;
3
3
  constraint: string;
4
- /** Named members of an `object` value. */
4
+ /** Named members of an `object` value. Declaring them closes the object. */
5
5
  children?: DeclaredOutputField[];
6
- /** Element shape of an `array` value. */
6
+ /** Shape of each contained value of an `array` or `map` value. */
7
7
  items?: DeclaredOutputShape;
8
+ /** Alternatives of a `union` value. */
9
+ variants?: DeclaredOutputShape[];
8
10
  /** Allowed values of an `enum` value. */
9
11
  values?: string[];
10
12
  }
@@ -12,14 +14,24 @@ export interface DeclaredOutputField extends DeclaredOutputShape {
12
14
  name: string;
13
15
  required: boolean;
14
16
  }
17
+ /** The first place a value departs from its shape: `path` is the full path from
18
+ * the root the caller passed (`data.messages[3].from`, `--to[1]`). */
19
+ export interface ValueMismatch {
20
+ path: string;
21
+ expected: string;
22
+ receivedType: string;
23
+ }
15
24
  /** Validates a result object against declared output field descriptors.
16
- * Returns null if valid; otherwise returns a { field, receivedType } error. */
25
+ * Returns null if valid; otherwise returns a { field, receivedType } error
26
+ * where `field` is the full path of the first mismatch. */
17
27
  export declare function validateDeclaredResult(output: DeclaredOutputField[], result: Record<string, unknown>): {
18
28
  field: string;
19
29
  receivedType?: string;
30
+ expected?: string;
20
31
  } | null;
21
- /** Core leaf declarations may explicitly require a nullable field. */
22
- export declare function validateCoreDeclaredResult(output: DeclaredOutputField[], result: Record<string, unknown>): {
23
- field: string;
24
- receivedType?: string;
25
- } | null;
32
+ /** Checks one JSON value against one shape, recursively: object members, array
33
+ * items, map values, union variants, enum values and nullability. An object that
34
+ * declares `children` is closed — an undeclared member is a mismatch at any
35
+ * depth. Returns null, or the first mismatch with its full path from `path`.
36
+ * A declared type that is not recognized is accepted. */
37
+ export declare function validateValue(shape: DeclaredOutputShape, value: unknown, path: string): ValueMismatch | null;
@@ -1,55 +1,47 @@
1
1
  import { isRecord } from '../../shared/predicates.js';
2
2
  /** Validates a result object against declared output field descriptors.
3
- * Returns null if valid; otherwise returns a { field, receivedType } error. */
3
+ * Returns null if valid; otherwise returns a { field, receivedType } error
4
+ * where `field` is the full path of the first mismatch. */
4
5
  export function validateDeclaredResult(output, result) {
5
- return validateFields(output, result, true);
6
- }
7
- /** Core leaf declarations may explicitly require a nullable field. */
8
- export function validateCoreDeclaredResult(output, result) {
9
- return validateFields(output, result, true);
10
- }
11
- function validateFields(output, result, honorDeclaredNull) {
12
6
  for (const f of output) {
13
7
  const value = result[f.name];
14
- const present = Object.prototype.hasOwnProperty.call(result, f.name)
15
- && value !== undefined
16
- && (honorDeclaredNull || value !== null);
17
- if (f.required && !present)
18
- return { field: f.name };
19
- const matches = honorDeclaredNull ? coreTypeMatches(f, value) : typeMatches(f.type, value);
20
- if (present && !matches) {
21
- return { field: f.name, receivedType: value === null ? 'null' : typeof value };
8
+ const present = Object.prototype.hasOwnProperty.call(result, f.name) && value !== undefined;
9
+ if (!present) {
10
+ if (f.required)
11
+ return { field: f.name };
12
+ continue;
22
13
  }
14
+ const mismatch = validateValue(f, value, f.name);
15
+ if (mismatch !== null)
16
+ return { field: mismatch.path, receivedType: mismatch.receivedType, expected: mismatch.expected };
23
17
  }
24
18
  return null;
25
19
  }
26
- /** Loose type check over the declared type vocabulary. Only the recognized
27
- * primitive/container categories are enforced; an unrecognized declared type
28
- * falls back to presence-only. */
29
- function typeMatches(type, v) {
30
- const t = type.toLowerCase();
31
- if (t === 'string' || t === 'path')
32
- return typeof v === 'string';
33
- if (t === 'int' || t === 'integer')
34
- return typeof v === 'number' && Number.isInteger(v);
35
- if (t === 'number' || t === 'float')
36
- return typeof v === 'number';
37
- if (t === 'bool' || t === 'boolean')
38
- return typeof v === 'boolean';
39
- if (t === 'array' || t.endsWith('[]'))
40
- return Array.isArray(v);
41
- if (t === 'object')
42
- return isRecord(v);
43
- return true;
20
+ /** Checks one JSON value against one shape, recursively: object members, array
21
+ * items, map values, union variants, enum values and nullability. An object that
22
+ * declares `children` is closed — an undeclared member is a mismatch at any
23
+ * depth. Returns null, or the first mismatch with its full path from `path`.
24
+ * A declared type that is not recognized is accepted. */
25
+ export function validateValue(shape, value, path) {
26
+ const atoms = shape.type.split('|').map((part) => part.trim()).filter(Boolean);
27
+ const bases = atoms.filter((part) => part.toLowerCase() !== 'null');
28
+ if (value === null && bases.length < atoms.length)
29
+ return null;
30
+ if (bases.length === 1 && !knownType(bases[0]))
31
+ return null;
32
+ if (!atoms.every((part) => knownType(part) || /^[a-z][a-z0-9_-]*$/.test(part)))
33
+ return null;
34
+ if (bases.length === 1)
35
+ return atomMismatch(bases[0], shape, value, path);
36
+ for (const atom of bases) {
37
+ if (atomMismatch(atom, shape, value, path) === null)
38
+ return null;
39
+ }
40
+ return mismatch(path, shape.type, value);
44
41
  }
45
- function coreTypeMatches(shape, v) {
46
- const alternatives = shape.type.split('|').map((part) => part.trim()).filter(Boolean);
47
- if (alternatives.length === 1 && !knownType(alternatives[0]))
48
- return true;
49
- const allKnown = alternatives.every((part) => knownType(part) || /^[a-z][a-z0-9_-]*$/.test(part));
50
- if (!allKnown)
51
- return true;
52
- return alternatives.some((part) => atomMatches(part, v, shape));
42
+ function mismatch(path, expected, value) {
43
+ const receivedType = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
44
+ return { path, expected, receivedType };
53
45
  }
54
46
  function knownType(type) {
55
47
  const t = type.toLowerCase();
@@ -66,32 +58,102 @@ function knownType(type) {
66
58
  || t === 'array'
67
59
  || t.endsWith('[]')
68
60
  || t === 'object'
61
+ || t === 'map'
62
+ || t === 'union'
63
+ || t === 'unknown'
69
64
  || t === 'enum'
70
65
  || t === 'file';
71
66
  }
72
- function atomMatches(type, v, shape) {
67
+ function atomMismatch(type, shape, v, path) {
73
68
  const t = type.toLowerCase();
69
+ const fail = (expected) => mismatch(path, expected, v);
74
70
  if (t === 'null')
75
- return v === null;
71
+ return v === null ? null : fail('null');
72
+ if (t === 'unknown')
73
+ return null;
76
74
  // An enum with declared `values` holds one of them; one without stays
77
75
  // presence-only, as before `values` existed.
78
- if (t === 'enum')
79
- return shape.values === undefined || (typeof v === 'string' && shape.values.includes(v));
76
+ if (t === 'enum') {
77
+ if (shape.values === undefined || (typeof v === 'string' && shape.values.includes(v)))
78
+ return null;
79
+ return fail(`one of ${(shape.values ?? []).join(', ')}`);
80
+ }
80
81
  // A file output is the provider's file object on the wire, or the runtime path
81
82
  // the daemon put in its place.
82
83
  if (t === 'file')
83
- return isRecord(v) || typeof v === 'string';
84
+ return isRecord(v) || typeof v === 'string' ? null : fail('file');
84
85
  if (t === 'string' || t === 'path' || t === 'markdown')
85
- return typeof v === 'string';
86
+ return typeof v === 'string' ? null : fail(t);
86
87
  if (t === 'int' || t === 'integer')
87
- return typeof v === 'number' && Number.isInteger(v);
88
+ return typeof v === 'number' && Number.isInteger(v) ? null : fail('int');
88
89
  if (t === 'number' || t === 'float')
89
- return typeof v === 'number';
90
+ return typeof v === 'number' ? null : fail('number');
90
91
  if (t === 'bool' || t === 'boolean')
91
- return typeof v === 'boolean';
92
- if (t === 'array' || t.endsWith('[]'))
93
- return Array.isArray(v);
94
- if (t === 'object')
95
- return isRecord(v);
96
- return typeof v === 'string' && v === type;
92
+ return typeof v === 'boolean' ? null : fail('bool');
93
+ if (t === 'array') {
94
+ if (!Array.isArray(v))
95
+ return fail('array');
96
+ if (shape.items === undefined)
97
+ return null;
98
+ for (let i = 0; i < v.length; i++) {
99
+ const inner = validateValue(shape.items, v[i], `${path}[${i}]`);
100
+ if (inner !== null)
101
+ return inner;
102
+ }
103
+ return null;
104
+ }
105
+ if (t.endsWith('[]'))
106
+ return Array.isArray(v) ? null : fail('array');
107
+ if (t === 'map') {
108
+ if (!isRecord(v))
109
+ return fail('map');
110
+ if (shape.items === undefined)
111
+ return null;
112
+ for (const [key, entry] of Object.entries(v)) {
113
+ const inner = validateValue(shape.items, entry, `${path}[${JSON.stringify(key)}]`);
114
+ if (inner !== null)
115
+ return inner;
116
+ }
117
+ return null;
118
+ }
119
+ if (t === 'object') {
120
+ if (!isRecord(v))
121
+ return fail('object');
122
+ if (shape.children === undefined)
123
+ return null;
124
+ for (const member of shape.children) {
125
+ const memberValue = v[member.name];
126
+ const present = Object.prototype.hasOwnProperty.call(v, member.name) && memberValue !== undefined;
127
+ if (!present) {
128
+ if (member.required)
129
+ return { path: `${path}.${member.name}`, expected: `${member.type} (required)`, receivedType: 'undefined' };
130
+ continue;
131
+ }
132
+ const inner = validateValue(member, memberValue, `${path}.${member.name}`);
133
+ if (inner !== null)
134
+ return inner;
135
+ }
136
+ const declared = new Set(shape.children.map((member) => member.name));
137
+ for (const key of Object.keys(v)) {
138
+ if (!declared.has(key))
139
+ return mismatch(`${path}.${key}`, 'no such member (the object is closed)', v[key]);
140
+ }
141
+ return null;
142
+ }
143
+ if (t === 'union') {
144
+ const failures = [];
145
+ for (const variant of shape.variants ?? []) {
146
+ const inner = validateValue(variant, v, path);
147
+ if (inner === null)
148
+ return null;
149
+ failures.push(inner);
150
+ }
151
+ // Every variant rejecting the value at the same deeper member names that member, expecting any variant's type.
152
+ const first = failures[0];
153
+ if (first !== undefined && first.path !== path && failures.every((failure) => failure.path === first.path)) {
154
+ return { path: first.path, expected: [...new Set(failures.map((failure) => failure.expected))].join(' | '), receivedType: first.receivedType };
155
+ }
156
+ return fail((shape.variants ?? []).map((variant) => variant.type).join(' | '));
157
+ }
158
+ return typeof v === 'string' && v === type ? null : fail(type);
97
159
  }
@@ -6,8 +6,8 @@
6
6
  // module owns the validators and re-exports those types under their in-tree names.
7
7
  import { isRecord } from '../../shared/predicates.js';
8
8
  // Validation helpers
9
- const TIERS = new Set(['normal', 'common', 'important']);
10
- const FLAG_TYPES = new Set(['string', 'int', 'bool', 'path', 'enum', 'file']);
9
+ const TIERS = new Set(['hidden', 'normal', 'common', 'important']);
10
+ const FLAG_TYPES = new Set(['string', 'int', 'bool', 'path', 'enum', 'file', 'json']);
11
11
  /** A capability-provider tool group id (contract 1 `<id>` grammar), so
12
12
  * `<provider>:<group>` splits on its first `:`. */
13
13
  const GROUP_ID = /^[A-Za-z0-9_-]{1,64}$/;
@@ -39,13 +39,14 @@ const PARAM_KEYS_BY_KIND = {
39
39
  'repeatable',
40
40
  'encoding',
41
41
  'defaultFromEnv',
42
+ 'shape',
42
43
  ]),
43
44
  stdin: new Set(['kind', 'name', 'required', 'constraint']),
44
45
  'context-file': new Set(['kind', 'name', 'required', 'constraint', 'shape']),
45
46
  };
46
- const OUTPUT_KEYS = new Set(['name', 'type', 'required', 'constraint', 'children', 'items', 'values']);
47
+ const OUTPUT_KEYS = new Set(['name', 'type', 'required', 'constraint', 'children', 'items', 'variants', 'values']);
47
48
  /** An `items` element shape: an output field without a name or required. */
48
- const OUTPUT_SHAPE_KEYS = new Set(['type', 'constraint', 'children', 'items', 'values']);
49
+ const OUTPUT_SHAPE_KEYS = new Set(['type', 'constraint', 'children', 'items', 'variants', 'values']);
49
50
  /** UPPER_SNAKE_CASE environment variable name — the only form an ambient default
50
51
  * may name, so a typo'd lowercase key fails at authoring time rather than
51
52
  * silently never resolving. */
@@ -184,7 +185,7 @@ function validateParams(raw, path, issue) {
184
185
  else if (kind === 'flag') {
185
186
  const type = p['type'];
186
187
  if (typeof type !== 'string' || !FLAG_TYPES.has(type)) {
187
- issue('command_node_invalid', `flag type must be string|int|bool|path|enum|file`, String(type), 'string | int | bool | path | enum | file', 'Fix the flag type.', `${pp}.type`);
188
+ issue('command_node_invalid', `flag type must be string|int|bool|path|enum|file|json`, String(type), 'string | int | bool | path | enum | file | json', 'Fix the flag type.', `${pp}.type`);
188
189
  return null;
189
190
  }
190
191
  let choices;
@@ -200,6 +201,24 @@ function validateParams(raw, path, issue) {
200
201
  issue('command_node_invalid', `choices is only valid on an enum flag`, 'choices', 'no choices unless type is enum', 'Remove choices.', `${pp}.choices`);
201
202
  return null;
202
203
  }
204
+ let shape;
205
+ if (type === 'json') {
206
+ const raw = p['shape'];
207
+ if (!isRecord(raw)) {
208
+ issue('command_node_invalid', `json flag requires a shape object`, typeName(raw), 'a { type, constraint, children?, items?, variants?, values? } object', 'Declare the shape of the JSON value the flag takes.', `${pp}.shape`);
209
+ return null;
210
+ }
211
+ if (!checkKeys(raw, OUTPUT_SHAPE_KEYS, `${pp}.shape`, issue))
212
+ return null;
213
+ const validated = validateOutputShape(raw, `${pp}.shape`, false, issue);
214
+ if (validated === null)
215
+ return null;
216
+ shape = validated;
217
+ }
218
+ else if (p['shape'] !== undefined) {
219
+ issue('command_node_invalid', `shape is only valid on a json flag`, 'shape', 'no shape unless type is json', 'Remove shape.', `${pp}.shape`);
220
+ return null;
221
+ }
203
222
  let repeatable = false;
204
223
  if (p['repeatable'] !== undefined) {
205
224
  if (typeof p['repeatable'] !== 'boolean') {
@@ -208,7 +227,11 @@ function validateParams(raw, path, issue) {
208
227
  }
209
228
  repeatable = p['repeatable'];
210
229
  }
211
- if (repeatable && (type === 'bool' || type === 'path' || type === 'file')) {
230
+ if (type === 'json' && (p['default'] !== undefined || p['defaultFromEnv'] !== undefined)) {
231
+ issue('command_node_invalid', `a json flag takes no default`, name, 'no default or defaultFromEnv on a json flag', 'Remove the default.', `${pp}.${p['default'] !== undefined ? 'default' : 'defaultFromEnv'}`);
232
+ return null;
233
+ }
234
+ if (repeatable && (type === 'bool' || type === 'path' || type === 'file' || type === 'json')) {
212
235
  issue('command_node_invalid', `repeatable is not valid on a ${type} flag`, `${name}: ${type}`, 'repeatable only on string | int | enum flags', 'Remove repeatable or change the flag type.', `${pp}.repeatable`);
213
236
  return null;
214
237
  }
@@ -230,6 +253,7 @@ function validateParams(raw, path, issue) {
230
253
  kind: 'flag',
231
254
  name,
232
255
  type: type,
256
+ ...(shape !== undefined ? { shape } : {}),
233
257
  ...(choices !== undefined ? { choices } : {}),
234
258
  ...(encoding !== undefined ? { encoding } : {}),
235
259
  required: p['required'],
@@ -327,16 +351,26 @@ function validateOutputShape(f, fp, topLevel, issue) {
327
351
  return null;
328
352
  }
329
353
  const structural = [
330
- ['children', 'object'],
331
- ['items', 'array'],
332
- ['values', 'enum'],
354
+ ['children', ['object']],
355
+ ['items', ['array', 'map']],
356
+ ['variants', ['union']],
357
+ ['values', ['enum']],
333
358
  ];
334
359
  for (const [key, allowedOn] of structural) {
335
- if (f[key] !== undefined && base !== allowedOn) {
336
- issue('command_node_invalid', `output ${key} is only valid on an ${allowedOn} field`, `type ${type}`, `type "${allowedOn}" (optionally "${allowedOn} | null")`, `Remove ${key}, or change the type to ${allowedOn}.`, `${fp}.${key}`);
360
+ if (f[key] !== undefined && !allowedOn.includes(base)) {
361
+ const names = allowedOn.map((a) => `"${a}"`).join(' or ');
362
+ issue('command_node_invalid', `output ${key} is only valid on a ${allowedOn.join(' or ')} field`, `type ${type}`, `type ${names} (optionally "| null")`, `Remove ${key}, or change the type to ${allowedOn[0]}.`, `${fp}.${key}`);
337
363
  return null;
338
364
  }
339
365
  }
366
+ if (base === 'union' && f['variants'] === undefined) {
367
+ issue('command_node_invalid', `a union output requires variants`, `type ${type}`, 'variants: two or more shapes', 'Declare the alternatives the union may take.', `${fp}.variants`);
368
+ return null;
369
+ }
370
+ if (base === 'map' && f['items'] === undefined) {
371
+ issue('command_node_invalid', `a map output requires items`, `type ${type}`, 'items: the shape of every value', 'Declare the shape of the map\'s values, or use type "object".', `${fp}.items`);
372
+ return null;
373
+ }
340
374
  let children;
341
375
  if (f['children'] !== undefined) {
342
376
  const c = validateOutputFields(f['children'], `${fp}.children`, false, issue);
@@ -349,7 +383,7 @@ function validateOutputShape(f, fp, topLevel, issue) {
349
383
  const raw = f['items'];
350
384
  const ip = `${fp}.items`;
351
385
  if (!isRecord(raw)) {
352
- issue('command_node_invalid', `output items must be an object`, typeName(raw), 'a { type, constraint, children?, items?, values? } object', 'Fix the items shape.', ip);
386
+ issue('command_node_invalid', `output items must be an object`, typeName(raw), 'a { type, constraint, children?, items?, variants?, values? } object', 'Fix the items shape.', ip);
353
387
  return null;
354
388
  }
355
389
  if (!checkKeys(raw, OUTPUT_SHAPE_KEYS, ip, issue))
@@ -359,6 +393,29 @@ function validateOutputShape(f, fp, topLevel, issue) {
359
393
  return null;
360
394
  items = shape;
361
395
  }
396
+ let variants;
397
+ if (f['variants'] !== undefined) {
398
+ const raw = f['variants'];
399
+ if (!Array.isArray(raw) || raw.length < 2) {
400
+ issue('command_node_invalid', `union variants must be an array of two or more shapes`, Array.isArray(raw) ? `${raw.length} variant${raw.length === 1 ? '' : 's'}` : typeName(raw), 'an array of two or more { type, constraint, ... } shapes', 'Declare at least two variants, or use the variant\'s own type.', `${fp}.variants`);
401
+ return null;
402
+ }
403
+ variants = [];
404
+ for (let i = 0; i < raw.length; i++) {
405
+ const vp = `${fp}.variants[${i}]`;
406
+ const v = raw[i];
407
+ if (!isRecord(v)) {
408
+ issue('command_node_invalid', `union variant must be an object`, typeName(v), 'a { type, constraint, ... } object', 'Fix the variant shape.', vp);
409
+ return null;
410
+ }
411
+ if (!checkKeys(v, OUTPUT_SHAPE_KEYS, vp, issue))
412
+ return null;
413
+ const shape = validateOutputShape(v, vp, false, issue);
414
+ if (shape === null)
415
+ return null;
416
+ variants.push(shape);
417
+ }
418
+ }
362
419
  let values;
363
420
  if (f['values'] !== undefined) {
364
421
  const v = f['values'];
@@ -377,6 +434,7 @@ function validateOutputShape(f, fp, topLevel, issue) {
377
434
  constraint: f['constraint'],
378
435
  ...(children !== undefined ? { children } : {}),
379
436
  ...(items !== undefined ? { items } : {}),
437
+ ...(variants !== undefined ? { variants } : {}),
380
438
  ...(values !== undefined ? { values } : {}),
381
439
  };
382
440
  }
@@ -404,8 +462,8 @@ function validateOutputFields(raw, path, topLevel, issue) {
404
462
  issue('command_node_invalid', `output field name must be a non-empty string`, typeName(f['name']), 'a field name', 'Add a name.', `${fp}.name`);
405
463
  return null;
406
464
  }
407
- if (!topLevel && names.has(f['name'])) {
408
- issue('command_node_invalid', `duplicate output child name`, f['name'], 'unique member names within one object', 'Rename the duplicate member.', `${fp}.name`);
465
+ if (names.has(f['name'])) {
466
+ issue('command_node_invalid', topLevel ? `duplicate output field name` : `duplicate output child name`, f['name'], topLevel ? 'unique output field names within one leaf' : 'unique member names within one object', topLevel ? 'Rename the duplicate field.' : 'Rename the duplicate member.', `${fp}.name`);
409
467
  return null;
410
468
  }
411
469
  names.add(f['name']);
@@ -442,7 +500,7 @@ function checkCommon(raw, path, issue) {
442
500
  return false;
443
501
  }
444
502
  if (raw['tier'] !== undefined && !TIERS.has(String(raw['tier']))) {
445
- issue('command_node_invalid', `node tier must be normal|common|important`, String(raw['tier']), 'normal | common | important', 'Fix or omit tier (external plugins cannot declare hidden commands).', `${path}.tier`);
503
+ issue('command_node_invalid', `node tier must be hidden|normal|common|important`, String(raw['tier']), 'hidden | normal | common | important', 'Fix or omit tier.', `${path}.tier`);
446
504
  return false;
447
505
  }
448
506
  return true;
@@ -1,5 +1,5 @@
1
1
  /** How prominently a node surfaces in ancestor `-h` listings. Default 'normal'. */
2
- export type ManifestTier = 'normal' | 'common' | 'important';
2
+ export type ManifestTier = 'hidden' | 'normal' | 'common' | 'important';
3
3
  /** Root-entry prose for a top-level branch — what every agent reads before it
4
4
  * has engaged the command at all. Required on a plugin's top-level branch. */
5
5
  export interface ManifestRootEntry {
@@ -16,8 +16,10 @@ export interface ManifestFieldShape {
16
16
  constraint: string;
17
17
  /** Named members; only when the base type is `object`. */
18
18
  children?: ManifestField[];
19
- /** Element shape; only when the base type is `array`. */
19
+ /** Value shape; only when the base type is `array` or `map`. */
20
20
  items?: ManifestFieldShape;
21
+ /** Alternatives; required when the base type is `union`. */
22
+ variants?: ManifestFieldShape[];
21
23
  /** The allowed values; required, and only valid, when the base type is `enum`. */
22
24
  values?: string[];
23
25
  }
@@ -53,7 +55,9 @@ export interface ManifestFlagParam {
53
55
  /** 'bool' flags take no value — presence is true. 'file' names a file
54
56
  * argument a capability-provider call carries; locally it is sent as the
55
57
  * string typed, never read from disk (that is `path` with `encoding`). */
56
- type: 'string' | 'int' | 'bool' | 'path' | 'enum' | 'file';
58
+ type: 'string' | 'int' | 'bool' | 'path' | 'enum' | 'file' | 'json';
59
+ /** Required only for a json flag. */
60
+ shape?: ManifestFieldShape;
57
61
  /** Required, and only valid, when type is 'enum'. */
58
62
  choices?: string[];
59
63
  required: boolean;
@@ -1,4 +1,4 @@
1
- import type { DeclaredOutputField } from '../api/command-manifest/result.js';
1
+ import type { DeclaredOutputField, DeclaredOutputShape } from '../api/command-manifest/result.js';
2
2
  export type Field = DeclaredOutputField;
3
3
  /** Positional argument. A repeatable positional consumes every remaining positional token. */
4
4
  export interface PositionalParam {
@@ -43,7 +43,9 @@ export interface FlagParam {
43
43
  name: string;
44
44
  /** 'bool' flags take no value — presence = true. 'file' names a
45
45
  * capability-provider file argument, sent locally as the string typed. */
46
- type: 'string' | 'int' | 'bool' | 'path' | 'enum' | 'file';
46
+ type: 'string' | 'int' | 'bool' | 'path' | 'enum' | 'file' | 'json';
47
+ /** Required only when type is 'json': the shape of the one JSON value the flag takes. */
48
+ shape?: DeclaredOutputShape;
47
49
  /** Required only when type is 'enum'. */
48
50
  choices?: string[];
49
51
  required: boolean;
package/dist/core/help.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // Pure functions — no side effects, no commander, no process.exit.
3
3
  // Rendering matches reference.md shapes exactly:
4
4
  // root L11-32, branch L43-58, leaf L65-189.
5
+ import { outputBaseType } from '../api/command-manifest/schema.js';
5
6
  /** `help` with the async state the render shows read once: the leaf's for
6
7
  * ordinary help, only `focusedFlag`'s for focused help. A read that throws
7
8
  * renders no element, the same soft failure `evalDynamic` gives a sync one. */
@@ -292,15 +293,24 @@ function paramDesc(p, leafName) {
292
293
  const repeatable = f.repeatable === true
293
294
  ? ' Repeatable — pass multiple times to accumulate.'
294
295
  : '';
296
+ if (f.type === 'json')
297
+ return `json, ${req}. One JSON value: ${outputTypePhrase(f.shape ?? { type: 'unknown', constraint: '' })}. ${f.constraint}${focusedNote}`.trim();
295
298
  return `${f.type}, ${req}.${choices}${dflt}${fileParamNote(f.type)}${fileEncodingNote(f.encoding)}${envDefaultNote(f.defaultFromEnv)} ${f.constraint}${repeatable}${focusedNote}`.trim();
296
299
  }
297
300
  /** One output shape's type phrase: `array of <element>` for an array with a
298
301
  * declared element shape, the declared type otherwise. */
299
302
  function outputTypePhrase(shape) {
303
+ const nullable = /\|\s*null\s*$/i.test(shape.type);
304
+ const base = outputBaseType(shape.type);
305
+ if (base === 'union' && shape.variants !== undefined) {
306
+ return `${shape.variants.map(outputTypePhrase).join(' | ')}${nullable ? ' | null' : ''}`;
307
+ }
308
+ if (base === 'map' && shape.items !== undefined) {
309
+ return `{ [key: string]: ${outputTypePhrase(shape.items)} }${nullable ? ' | null' : ''}`;
310
+ }
300
311
  if (shape.items === undefined)
301
312
  return shape.type;
302
- const nullable = /\|\s*null\s*$/i.test(shape.type) ? ', or null' : '';
303
- return `array of ${outputTypePhrase(shape.items)}${nullable}`;
313
+ return `array of ${outputTypePhrase(shape.items)}${nullable ? ', or null' : ''}`;
304
314
  }
305
315
  /** The values sentence of an enum shape, or nothing. */
306
316
  function outputValuesNote(shape) {
@@ -322,16 +332,27 @@ function renderOutputShapeBody(shape, indent, lines) {
322
332
  if (shape.children !== undefined && shape.children.length > 0) {
323
333
  renderOutputFields(shape.children, indent, lines);
324
334
  }
335
+ for (const variant of shape.variants ?? []) {
336
+ const note = outputValuesNote(variant);
337
+ lines.push(`${indent}variant: ${outputTypePhrase(variant)}.${note}${variant.constraint.length > 0 ? ` ${variant.constraint}` : ''}`);
338
+ renderOutputShapeBody(variant, `${indent} `, lines);
339
+ }
325
340
  let item = shape.items;
341
+ let eachWord = outputBaseType(shape.type) === 'map' ? 'each value' : 'each element';
326
342
  while (item !== undefined) {
327
343
  const note = outputValuesNote(item);
328
344
  if (item.constraint.length > 0 || note.length > 0) {
329
- lines.push(`${indent}each element: ${outputTypePhrase(item)}.${note}${item.constraint.length > 0 ? ` ${item.constraint}` : ''}`);
345
+ lines.push(`${indent}${eachWord}: ${outputTypePhrase(item)}.${note}${item.constraint.length > 0 ? ` ${item.constraint}` : ''}`);
346
+ }
347
+ for (const variant of item.variants ?? []) {
348
+ lines.push(`${indent}variant: ${outputTypePhrase(variant)}.${outputValuesNote(variant)}${variant.constraint.length > 0 ? ` ${variant.constraint}` : ''}`);
349
+ renderOutputShapeBody(variant, `${indent} `, lines);
330
350
  }
331
351
  if (item.children !== undefined && item.children.length > 0) {
332
352
  renderOutputFields(item.children, indent, lines);
333
353
  return;
334
354
  }
355
+ eachWord = outputBaseType(item.type) === 'map' ? 'each value' : 'each element';
335
356
  item = item.items;
336
357
  }
337
358
  }
@@ -380,6 +401,10 @@ export function renderLeafArgv(h, focusedFlag) {
380
401
  const colW = maxLen(labels);
381
402
  for (let i = 0; i < params.length; i++) {
382
403
  lines.push(` ${pad(labels[i], colW)} ${paramDesc(params[i], h.name)}`);
404
+ const param = params[i];
405
+ if (param.kind === 'flag' && param.type === 'json' && param.shape !== undefined) {
406
+ renderOutputShapeBody(param.shape, ` ${' '.repeat(colW)} `, lines);
407
+ }
383
408
  }
384
409
  }
385
410
  else {
@@ -15,7 +15,9 @@ export declare function objectBlocksReasons(manifest: Pick<PluginManifest, 'obje
15
15
  export declare function declaresObjectBlocks(manifest: Pick<PluginManifest, 'object_kinds' | 'scopes'>): boolean;
16
16
  /** A user plugin holds a `crtr_app_` token when it is not a provider and declares kinds or scopes, or serves over HTTP. */
17
17
  export declare function needsAppToken(manifest: Pick<PluginManifest, 'object_kinds' | 'scopes' | 'provider' | 'transport'>): boolean;
18
- /** The space-joined path of every leaf below the plugin's root command (`page read` for plugin `search`'s leaf `search page read`). */
19
- export declare function leafPathsBelowRoot(name: string, roots: ReadonlyArray<DeclNode<DeclLeaf>>): Set<string>;
18
+ /** A provider leaf's tool path from its full command path (root first): root-relative when the bundle has one root command, else as declared. The one rule `providerLeaves` dispatches by and the object-kind gate validates against. */
19
+ export declare function providerToolPath(rootCount: number, path: readonly string[]): string;
20
+ /** The space-joined path of every leaf below the plugin's root command (`page read` for plugin `search`'s leaf `search page read`); for a provider bundle, every leaf's tool path (`providerToolPath`). */
21
+ export declare function leafPathsBelowRoot(name: string, manifest: Pick<PluginManifest, 'provider'>, roots: ReadonlyArray<DeclNode<DeclLeaf>>): Set<string>;
20
22
  /** The leaves of `leafPathsBelowRoot` that declare `rest.streaming: true`. */
21
- export declare function streamingLeafPathsBelowRoot(name: string, roots: ReadonlyArray<DeclNode<DeclLeaf>>): Set<string>;
23
+ export declare function streamingLeafPathsBelowRoot(name: string, manifest: Pick<PluginManifest, 'provider'>, roots: ReadonlyArray<DeclNode<DeclLeaf>>): Set<string>;
@@ -49,28 +49,38 @@ export function needsAppToken(manifest) {
49
49
  return false;
50
50
  return Object.keys(manifest.object_kinds ?? {}).length > 0 || (manifest.scopes?.length ?? 0) > 0 || manifest.transport?.kind === 'http';
51
51
  }
52
- /** The space-joined path of every leaf below the plugin's root command (`page read` for plugin `search`'s leaf `search page read`). */
53
- export function leafPathsBelowRoot(name, roots) {
54
- return collectLeafPaths(name, roots, () => true);
52
+ /** A provider leaf's tool path from its full command path (root first): root-relative when the bundle has one root command, else as declared. The one rule `providerLeaves` dispatches by and the object-kind gate validates against. */
53
+ export function providerToolPath(rootCount, path) {
54
+ return (rootCount === 1 ? path.slice(1) : path).join(' ');
55
+ }
56
+ /** The space-joined path of every leaf below the plugin's root command (`page read` for plugin `search`'s leaf `search page read`); for a provider bundle, every leaf's tool path (`providerToolPath`). */
57
+ export function leafPathsBelowRoot(name, manifest, roots) {
58
+ return collectLeafPaths(name, manifest, roots, () => true);
55
59
  }
56
60
  /** The leaves of `leafPathsBelowRoot` that declare `rest.streaming: true`. */
57
- export function streamingLeafPathsBelowRoot(name, roots) {
58
- return collectLeafPaths(name, roots, (leaf) => 'rest' in leaf && leaf.rest.streaming === true);
61
+ export function streamingLeafPathsBelowRoot(name, manifest, roots) {
62
+ return collectLeafPaths(name, manifest, roots, (leaf) => 'rest' in leaf && leaf.rest.streaming === true);
59
63
  }
60
- function collectLeafPaths(name, roots, include) {
64
+ function collectLeafPaths(name, manifest, roots, include) {
65
+ const provider = manifest.provider !== undefined;
61
66
  const leafPaths = new Set();
62
67
  const walk = (node, prefix) => {
63
68
  if (node.kind === 'leaf') {
64
69
  if (include(node))
65
- leafPaths.add([...prefix, node.name].join(' '));
70
+ leafPaths.add(provider ? providerToolPath(roots.length, [...prefix, node.name]) : [...prefix, node.name].join(' '));
66
71
  }
67
72
  else
68
73
  for (const child of node.children)
69
74
  walk(child, [...prefix, node.name]);
70
75
  };
71
- for (const root of roots)
72
- if (root.name === name && root.kind === 'branch')
76
+ for (const root of roots) {
77
+ if (root.kind !== 'branch' || (!provider && root.name !== name))
78
+ continue;
79
+ if (provider)
80
+ walk(root, []);
81
+ else
73
82
  for (const child of root.children)
74
83
  walk(child, []);
84
+ }
75
85
  return leafPaths;
76
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/api",
3
- "version": "0.3.518",
3
+ "version": "0.3.520",
4
4
  "description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, the CrtrClient, and the command-plugin manifest format. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/api/index.js",