@north-light/api 0.3.519 → 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.
- package/dist/api/command-manifest/result.d.ts +20 -8
- package/dist/api/command-manifest/result.js +118 -56
- package/dist/api/command-manifest/schema.js +73 -15
- package/dist/api/plugin-manifest-schema.d.ts +7 -3
- package/dist/core/help.d.ts +4 -2
- package/dist/core/help.js +28 -3
- package/dist/core/plugin-objects-manifest.d.ts +5 -3
- package/dist/core/plugin-objects-manifest.js +19 -9
- package/package.json +1 -1
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
46
|
-
const
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
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'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 (
|
|
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
|
-
['
|
|
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
|
|
336
|
-
|
|
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 (
|
|
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
|
|
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
|
-
/**
|
|
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;
|
package/dist/core/help.d.ts
CHANGED
|
@@ -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
|
-
|
|
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}
|
|
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
|
-
/**
|
|
19
|
-
export declare function
|
|
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
|
-
/**
|
|
53
|
-
export function
|
|
54
|
-
return
|
|
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.
|
|
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.
|
|
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",
|