@smallpen/cli 0.1.0-alpha.5 → 0.1.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/command-contract.mjs +1416 -0
- package/bin/command-tree.mjs +512 -0
- package/bin/design-output.mjs +1039 -0
- package/bin/help-topics.mjs +241 -0
- package/bin/help.mjs +379 -478
- package/bin/read-output.mjs +219 -0
- package/bin/result-files.mjs +232 -0
- package/bin/reuse-advice.mjs +73 -0
- package/bin/schema.mjs +607 -152
- package/bin/smallpen.mjs +4185 -1330
- package/bin/warning-output.mjs +2 -2
- package/bin/windows-artifacts.mjs +72 -0
- package/bin/workflow.mjs +55 -0
- package/package.json +3 -3
|
@@ -0,0 +1,1416 @@
|
|
|
1
|
+
// One definition drives command schemas, help, argument validation and errors.
|
|
2
|
+
import {
|
|
3
|
+
INIT_LAYOUTS,
|
|
4
|
+
SMALLPEN_FORMAT_CAPABILITIES,
|
|
5
|
+
SmallPenError,
|
|
6
|
+
suggestField,
|
|
7
|
+
} from "@smallpen/core";
|
|
8
|
+
import { DISCOVERY_OUTPUT } from "./read-output.mjs";
|
|
9
|
+
|
|
10
|
+
const parameter = (option, type, description, extra = {}) => ({
|
|
11
|
+
option,
|
|
12
|
+
type,
|
|
13
|
+
description,
|
|
14
|
+
required: false,
|
|
15
|
+
repeatable: false,
|
|
16
|
+
...extra,
|
|
17
|
+
});
|
|
18
|
+
const flag = (option, description) =>
|
|
19
|
+
parameter(option, "boolean", description, { default: false });
|
|
20
|
+
const path = (option, description) => parameter(option, "path", description);
|
|
21
|
+
const id = (name, prefix, description, extra = {}) =>
|
|
22
|
+
parameter(`--${name}-id`, "string", description, { prefix, ...extra });
|
|
23
|
+
const integer = (option, description, extra = {}) =>
|
|
24
|
+
parameter(option, "integer", description, {
|
|
25
|
+
minimum: 0,
|
|
26
|
+
maximum: Number.MAX_SAFE_INTEGER,
|
|
27
|
+
...extra,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const PARAMETERS = {
|
|
31
|
+
stdout: flag(
|
|
32
|
+
"--stdout",
|
|
33
|
+
"Print large results to stdout instead of saving a temporary result file",
|
|
34
|
+
),
|
|
35
|
+
assetId: id(
|
|
36
|
+
"asset",
|
|
37
|
+
undefined,
|
|
38
|
+
"Local color, typography, media or font identifier",
|
|
39
|
+
),
|
|
40
|
+
flowId: id(
|
|
41
|
+
"flow",
|
|
42
|
+
undefined,
|
|
43
|
+
"App prototype start UUID in the selected Presentation",
|
|
44
|
+
),
|
|
45
|
+
steps: path(
|
|
46
|
+
"--steps",
|
|
47
|
+
"JSON {steps:[{nodeId,trigger?,phase?,index?,interactionId?,waitMs?,expect?}]} for read-only prototype verification",
|
|
48
|
+
),
|
|
49
|
+
all: flag(
|
|
50
|
+
"--all",
|
|
51
|
+
"Check every variant of one --component-id, or every Presentation of one --screen-id; never all components/pages or theme combinations",
|
|
52
|
+
),
|
|
53
|
+
action: parameter(
|
|
54
|
+
"--action",
|
|
55
|
+
"string",
|
|
56
|
+
"One action offered by the selected repair conflict",
|
|
57
|
+
),
|
|
58
|
+
after: path("--after", "The second Draft package"),
|
|
59
|
+
allThemes: flag("--all-themes", "Search inactive token themes too"),
|
|
60
|
+
answer: parameter(
|
|
61
|
+
"--answer",
|
|
62
|
+
"assignment",
|
|
63
|
+
"QUESTION_ID=JSON initialization answer",
|
|
64
|
+
{ repeatable: true, uniqueBy: "assignment-key" },
|
|
65
|
+
),
|
|
66
|
+
answers: path(
|
|
67
|
+
"--answers",
|
|
68
|
+
"Compatibility import of a complete legacy initialization brief; not used by project init",
|
|
69
|
+
),
|
|
70
|
+
apply: flag("--apply", "Commit the reviewed token import"),
|
|
71
|
+
asset: parameter("--asset", "string", "Repair asset identifier"),
|
|
72
|
+
assetKind: parameter("--asset-kind", "string", "Repair asset kind"),
|
|
73
|
+
base64: flag(
|
|
74
|
+
"--base64",
|
|
75
|
+
"Include inline image bytes; disables the small reply budget",
|
|
76
|
+
),
|
|
77
|
+
batch: path(
|
|
78
|
+
"--batch",
|
|
79
|
+
"JSON batch with batchId, baseRevision and operations",
|
|
80
|
+
),
|
|
81
|
+
batchId: parameter(
|
|
82
|
+
"--batch-id",
|
|
83
|
+
"string",
|
|
84
|
+
"Stable retry identity; reuse with identical input",
|
|
85
|
+
{
|
|
86
|
+
defaultFrom: "a new random identity; supply one before a retryable write",
|
|
87
|
+
},
|
|
88
|
+
),
|
|
89
|
+
color: parameter("--color", "string", "Search tokens near this color"),
|
|
90
|
+
compact: flag("--compact", "Explicit alias for the default small reply"),
|
|
91
|
+
componentId: id(
|
|
92
|
+
"component",
|
|
93
|
+
"cmp_",
|
|
94
|
+
"Component identifier in its owning package",
|
|
95
|
+
),
|
|
96
|
+
confirm: flag("--confirm", "With --answers: create the workspace the answers propose (a plain init creates a blank one at once)"),
|
|
97
|
+
confirmUnmatched: flag(
|
|
98
|
+
"--confirm-unmatched",
|
|
99
|
+
"Accept creation of an unmatched component selection",
|
|
100
|
+
),
|
|
101
|
+
conflict: integer("--conflict", "Index of the repair conflict", {
|
|
102
|
+
default: 0,
|
|
103
|
+
}),
|
|
104
|
+
context: parameter(
|
|
105
|
+
"--context",
|
|
106
|
+
"assignment",
|
|
107
|
+
"CONTEXT=VALUE by name (Density=Compact) for this call",
|
|
108
|
+
{ repeatable: true, uniqueBy: "assignment-key" },
|
|
109
|
+
),
|
|
110
|
+
contextProfileId: id("context-profile", undefined, "Named Context profile"),
|
|
111
|
+
contexts: path("--contexts", "JSON file containing render-matrix selectors"),
|
|
112
|
+
evidence: flag(
|
|
113
|
+
"--evidence",
|
|
114
|
+
"Export PNG and its review evidence; requires --format png",
|
|
115
|
+
),
|
|
116
|
+
designSystem: flag(
|
|
117
|
+
"--design-system",
|
|
118
|
+
"Render the generated Design System page (every Token and representative component samples) instead of a Screen",
|
|
119
|
+
),
|
|
120
|
+
definitions: flag(
|
|
121
|
+
"--definitions",
|
|
122
|
+
"Read stored Token definitions in all option Sets, without resolving a theme combination",
|
|
123
|
+
),
|
|
124
|
+
group: parameter(
|
|
125
|
+
"--group",
|
|
126
|
+
"string",
|
|
127
|
+
"Only Tokens that take values in this theme group (Brand); requires --definitions",
|
|
128
|
+
),
|
|
129
|
+
diff: flag("--diff", "Preview the batch diff without writing"),
|
|
130
|
+
from: parameter("--from", "string", "Source element as \"Page / Element\""),
|
|
131
|
+
to: parameter("--to", "string", "Destination page name"),
|
|
132
|
+
on: parameter("--on", "string", "Trigger; default click", {
|
|
133
|
+
values: ["click", "mouse-enter", "mouse-leave", "after-delay", "mouse-press", "mouse-over"],
|
|
134
|
+
errorCode: "invalid_flow_trigger",
|
|
135
|
+
}),
|
|
136
|
+
page: parameter("--page", "string", "Page name"),
|
|
137
|
+
by: parameter("--by", "string", "Theme group: one file per option, for example Language"),
|
|
138
|
+
tokenFormat: parameter("--format", "string", "json (nested by name) or flat (one key per Token name)", {
|
|
139
|
+
values: ["json", "flat"],
|
|
140
|
+
errorCode: "unknown_token_format",
|
|
141
|
+
}),
|
|
142
|
+
since: parameter("--since", "string", "A revision you saw; every read and write reports one"),
|
|
143
|
+
canvas: parameter("--canvas", "string", "Canvas name"),
|
|
144
|
+
direction: parameter("--direction", "string", "up, down, left or right", {
|
|
145
|
+
values: ["up", "down", "left", "right"],
|
|
146
|
+
errorCode: "invalid_direction",
|
|
147
|
+
}),
|
|
148
|
+
moveSteps: parameter("--steps", "integer", "How many places to move; default 1"),
|
|
149
|
+
colorName: parameter("--color", "string", "Color name, as Group/Name, for example Brand/Primary"),
|
|
150
|
+
typographyName: parameter("--typography", "string", "Typography name, as Group/Name, for example Text/Body"),
|
|
151
|
+
assetValue: parameter("--value", "string", "A color (#1f6feb) for --color; a style object for --typography, for example {\"fontFamily\":\"sourcesanspro\",\"fontSize\":16}"),
|
|
152
|
+
mediaName: parameter("--media", "string", "Media file name, as Group/Name"),
|
|
153
|
+
fontFamily: parameter("--font", "string", "Font family name"),
|
|
154
|
+
removeStart: flag("--remove", "Stop the page being a start"),
|
|
155
|
+
as: parameter("--as", "string", "What to return: text (default), wireframe, png, issues, or spec (detailed positions, sizes, layout, visual values and Token bindings)", {
|
|
156
|
+
values: ["text", "wireframe", "png", "issues", "spec"],
|
|
157
|
+
errorCode: "unknown_view_as",
|
|
158
|
+
}),
|
|
159
|
+
component: parameter("--component", "string", "Component name"),
|
|
160
|
+
setField: parameter(
|
|
161
|
+
"--set",
|
|
162
|
+
"assignment",
|
|
163
|
+
'FIELD=VALUE as in page draw: width=320, fill={color.surface}, padding=[8,16], text="Save", props={"Style":"ghost"}; a JSON value or plain text',
|
|
164
|
+
{ repeatable: true },
|
|
165
|
+
),
|
|
166
|
+
variant: parameter("--variant", "string", 'One variant, as "Style=secondary" (or just "secondary")'),
|
|
167
|
+
element: parameter("--element", "string", 'Element name, or "Parent / Element" when the name repeats'),
|
|
168
|
+
platform: parameter("--platform", "string", "Page version: desktop, mobile, ..."),
|
|
169
|
+
token: parameter("--token", "string", "Token name or prefix, for example color or color.brand"),
|
|
170
|
+
allTokens: flag("--tokens", "Every Token"),
|
|
171
|
+
allComponents: flag("--components", "Every component"),
|
|
172
|
+
allPages: flag("--pages", "Every page"),
|
|
173
|
+
draft: path("--draft", "Source Draft package"),
|
|
174
|
+
dryRun: flag("--dry-run", "Validate without committing a batch"),
|
|
175
|
+
explain: flag(
|
|
176
|
+
"--explain",
|
|
177
|
+
"Preview affected targets and fields without writing",
|
|
178
|
+
),
|
|
179
|
+
family: parameter("--family", "string", "Font family name"),
|
|
180
|
+
file: path("--file", "Source binary file"),
|
|
181
|
+
fontId: id("font", "font_", "Imported font identifier"),
|
|
182
|
+
foundation: path("--foundation", "Replacement Foundation package"),
|
|
183
|
+
full: flag(
|
|
184
|
+
"--full",
|
|
185
|
+
"Expand selected data; large results still use files. --stdout opts into large stdout. View nodes honor explicit pagination",
|
|
186
|
+
),
|
|
187
|
+
help: flag("--help", "Print this command's help"),
|
|
188
|
+
includeImage: flag(
|
|
189
|
+
"--include-image",
|
|
190
|
+
"Include a PNG artifact in the view receipt",
|
|
191
|
+
),
|
|
192
|
+
input: path("--input", "Source import file"),
|
|
193
|
+
intent: path("--intent", "JSON file containing the drawing or token intent"),
|
|
194
|
+
interval: integer(
|
|
195
|
+
"--interval",
|
|
196
|
+
"Watch poll interval in milliseconds; values below 100 use 100",
|
|
197
|
+
{ default: 500, maximum: 3_600_000 },
|
|
198
|
+
),
|
|
199
|
+
inverseOut: path(
|
|
200
|
+
"--inverse-out",
|
|
201
|
+
"Durable output file for the exact undo batch",
|
|
202
|
+
),
|
|
203
|
+
json: flag("--json", "Machine-readable output with stable keys"),
|
|
204
|
+
kind: parameter("--kind", "string", "Object kind"),
|
|
205
|
+
layout: parameter("--layout", "string", "Workspace layout", {
|
|
206
|
+
default: "single",
|
|
207
|
+
values: Object.keys(INIT_LAYOUTS),
|
|
208
|
+
errorCode: "invalid_init_layout",
|
|
209
|
+
}),
|
|
210
|
+
library: parameter(
|
|
211
|
+
"--library",
|
|
212
|
+
"string",
|
|
213
|
+
"Declared Library package identifier or source URL",
|
|
214
|
+
{ defaultFrom: "the first declared URL Library" },
|
|
215
|
+
),
|
|
216
|
+
limit: integer("--limit", "Maximum items in this page", {
|
|
217
|
+
default: 20,
|
|
218
|
+
maximum: 100,
|
|
219
|
+
}),
|
|
220
|
+
locale: parameter(
|
|
221
|
+
"--locale",
|
|
222
|
+
"string",
|
|
223
|
+
"Label locale; stable JSON keys stay English",
|
|
224
|
+
{ defaultFrom: "LC_ALL, LC_MESSAGES, LANG, then English" },
|
|
225
|
+
),
|
|
226
|
+
maxEvents: integer("--max-events", "Maximum watch events before exit", {
|
|
227
|
+
minimum: 1,
|
|
228
|
+
maximum: 10_000,
|
|
229
|
+
defaultFrom: "unlimited",
|
|
230
|
+
}),
|
|
231
|
+
mediaId: id("media", "media_", "Imported media identifier"),
|
|
232
|
+
mediaPath: parameter("--media-path", "string", "Media catalog path", {
|
|
233
|
+
default: "",
|
|
234
|
+
}),
|
|
235
|
+
name: parameter("--name", "string", "Display name"),
|
|
236
|
+
nodeId: id(
|
|
237
|
+
"node",
|
|
238
|
+
"node_",
|
|
239
|
+
"Node identifier in the selected presentation or variant",
|
|
240
|
+
),
|
|
241
|
+
offset: integer("--offset", "First item index in this page", { default: 0 }),
|
|
242
|
+
output: path(
|
|
243
|
+
"--output",
|
|
244
|
+
"Output artifact path; image commands otherwise return temporary file paths",
|
|
245
|
+
),
|
|
246
|
+
packageId: id(
|
|
247
|
+
"package",
|
|
248
|
+
"pkg_",
|
|
249
|
+
"Owning package identifier for a cross-package reference",
|
|
250
|
+
),
|
|
251
|
+
path: parameter(
|
|
252
|
+
"--path",
|
|
253
|
+
"string",
|
|
254
|
+
"Token name, for example color.brand",
|
|
255
|
+
),
|
|
256
|
+
presentationId: id("presentation", "pres_", "Presentation identifier", {
|
|
257
|
+
defaultFrom: "the selected Screen's Base Presentation",
|
|
258
|
+
}),
|
|
259
|
+
query: parameter("--query", "string", "Search text"),
|
|
260
|
+
replacementAssetId: id(
|
|
261
|
+
"replacement-asset",
|
|
262
|
+
undefined,
|
|
263
|
+
"Replacement asset identifier",
|
|
264
|
+
),
|
|
265
|
+
replacementPackageId: id(
|
|
266
|
+
"replacement-package",
|
|
267
|
+
"pkg_",
|
|
268
|
+
"Replacement asset's package identifier",
|
|
269
|
+
),
|
|
270
|
+
scale: parameter("--scale", "number", "Raster scale within (0, 8]", {
|
|
271
|
+
default: 1,
|
|
272
|
+
exclusiveMinimum: 0,
|
|
273
|
+
maximum: 8,
|
|
274
|
+
errorCode: "invalid_render_scale",
|
|
275
|
+
}),
|
|
276
|
+
scenarioId: id("scenario", "scn_", "Reproducible Scenario identifier"),
|
|
277
|
+
screenId: id("screen", "scr_", "Screen identifier", {
|
|
278
|
+
defaultFrom: "manifest.defaultScreenId",
|
|
279
|
+
}),
|
|
280
|
+
select: parameter("--select", "string", "SET/NAME token to import", {
|
|
281
|
+
repeatable: true,
|
|
282
|
+
}),
|
|
283
|
+
selection: parameter("--selection", "json", "JSON component axis selection", {
|
|
284
|
+
errorCode: "invalid_repair_selection",
|
|
285
|
+
}),
|
|
286
|
+
selections: path("--selections", "JSON file naming Draft fields to compile"),
|
|
287
|
+
selector: parameter("--selector", "json", "JSON Design View selector", {
|
|
288
|
+
repeatable: true,
|
|
289
|
+
errorCode: "invalid_compare_selector",
|
|
290
|
+
}),
|
|
291
|
+
setName: parameter("--set-name", "string", "Imported token set name"),
|
|
292
|
+
state: path("--state", "Initialization state to resume explicitly"),
|
|
293
|
+
style: parameter("--style", "string", "Font style", {
|
|
294
|
+
default: "normal",
|
|
295
|
+
values: ["normal", "italic", "oblique"],
|
|
296
|
+
}),
|
|
297
|
+
theme: parameter(
|
|
298
|
+
"--theme",
|
|
299
|
+
"string",
|
|
300
|
+
"GROUP/NAME token theme for this call; one per group",
|
|
301
|
+
{
|
|
302
|
+
repeatable: true,
|
|
303
|
+
defaultFrom:
|
|
304
|
+
"project group defaults; explicit Scenario themes then explicit --theme override named groups for this call; independent of App active selection",
|
|
305
|
+
},
|
|
306
|
+
),
|
|
307
|
+
tokenId: id("token", "tok_", "Token identifier in its owning package"),
|
|
308
|
+
type: parameter("--type", "string", "Token type filter", {
|
|
309
|
+
values: SMALLPEN_FORMAT_CAPABILITIES.canonicalPackage.tokenTypes,
|
|
310
|
+
errorCode: "invalid_token_type",
|
|
311
|
+
}),
|
|
312
|
+
value: parameter(
|
|
313
|
+
"--value",
|
|
314
|
+
"string",
|
|
315
|
+
"Token value filter; JSON when valid, otherwise text",
|
|
316
|
+
),
|
|
317
|
+
variantId: id(
|
|
318
|
+
"variant",
|
|
319
|
+
"var_",
|
|
320
|
+
"Component variant identifier; import-font uses a font variant id",
|
|
321
|
+
),
|
|
322
|
+
viewFormat: parameter("--format", "string", "Design View representation", {
|
|
323
|
+
default: "structure",
|
|
324
|
+
values: ["structure", "semantic", "wireframe", "screenshot"],
|
|
325
|
+
errorCode: "unknown_view_format",
|
|
326
|
+
}),
|
|
327
|
+
designFormat: parameter(
|
|
328
|
+
"--format",
|
|
329
|
+
"string",
|
|
330
|
+
"Resolved design representation",
|
|
331
|
+
{
|
|
332
|
+
default: "outline",
|
|
333
|
+
values: ["outline", "wireframe", "spec", "structure", "semantic"],
|
|
334
|
+
errorCode: "unknown_view_format",
|
|
335
|
+
},
|
|
336
|
+
),
|
|
337
|
+
artifactFormat: parameter("--format", "string", "Export artifact", {
|
|
338
|
+
default: "wireframe",
|
|
339
|
+
values: ["text", "wireframe", "png"],
|
|
340
|
+
errorCode: "unknown_export_format",
|
|
341
|
+
}),
|
|
342
|
+
warningDetail: parameter(
|
|
343
|
+
"--warning-detail",
|
|
344
|
+
"string",
|
|
345
|
+
"Design advice detail",
|
|
346
|
+
{
|
|
347
|
+
default: "compact",
|
|
348
|
+
values: ["compact", "full"],
|
|
349
|
+
errorCode: "invalid_warning_detail",
|
|
350
|
+
},
|
|
351
|
+
),
|
|
352
|
+
weight: integer("--weight", "Font weight", {
|
|
353
|
+
default: 400,
|
|
354
|
+
values: [100, 200, 300, 400, 500, 600, 700, 800, 900, 950],
|
|
355
|
+
}),
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const PAGE = ["limit", "offset"];
|
|
359
|
+
const VIEW = ["context", "theme"];
|
|
360
|
+
// A design target by name, as view takes it.
|
|
361
|
+
const TARGET = ["page", "platform", "element", "component", "variant"];
|
|
362
|
+
const WRITE = [
|
|
363
|
+
"compact",
|
|
364
|
+
"confirmUnmatched",
|
|
365
|
+
"diff",
|
|
366
|
+
"dryRun",
|
|
367
|
+
"explain",
|
|
368
|
+
"inverseOut",
|
|
369
|
+
"warningDetail",
|
|
370
|
+
];
|
|
371
|
+
const TOKEN = ["context", "theme"];
|
|
372
|
+
const COMMON = ["json", "full", "stdout", "help"];
|
|
373
|
+
const command = (purpose, fields = [], extra = {}) => ({
|
|
374
|
+
purpose,
|
|
375
|
+
fields,
|
|
376
|
+
...extra,
|
|
377
|
+
});
|
|
378
|
+
const required = (field, code) => ({ field, code });
|
|
379
|
+
|
|
380
|
+
export const COMMAND_CONTRACTS = {
|
|
381
|
+
help: command("Print command usage and options", [], {
|
|
382
|
+
locator: false,
|
|
383
|
+
arguments: [
|
|
384
|
+
{ name: "topic", type: "string", required: false },
|
|
385
|
+
{
|
|
386
|
+
name: "section",
|
|
387
|
+
type: "string",
|
|
388
|
+
required: false,
|
|
389
|
+
when: "an action, subgroup or rules topic",
|
|
390
|
+
},
|
|
391
|
+
{ name: "action", type: "string", required: false, when: "section is a command subgroup" },
|
|
392
|
+
],
|
|
393
|
+
}),
|
|
394
|
+
version: command("Print the CLI version", [], { locator: false }),
|
|
395
|
+
schema: command(
|
|
396
|
+
"Print the JSON shape and parameters of each command and write input",
|
|
397
|
+
PAGE,
|
|
398
|
+
{
|
|
399
|
+
locator: false,
|
|
400
|
+
arguments: [
|
|
401
|
+
{ name: "topic", type: "string", required: false },
|
|
402
|
+
{
|
|
403
|
+
name: "name",
|
|
404
|
+
type: "string",
|
|
405
|
+
required: false,
|
|
406
|
+
when: "topic is command, operation, workflow or an intent action directory",
|
|
407
|
+
},
|
|
408
|
+
{ name: "action", type: "string", required: false, when: "topic is command" },
|
|
409
|
+
{ name: "subaction", type: "string", required: false, when: "name and action identify a command subgroup" },
|
|
410
|
+
],
|
|
411
|
+
},
|
|
412
|
+
),
|
|
413
|
+
init: command(
|
|
414
|
+
"Create a blank package or workspace",
|
|
415
|
+
["name", "layout", "locale", "answers", "confirm"],
|
|
416
|
+
{ locatorName: "workspace" },
|
|
417
|
+
),
|
|
418
|
+
themes: command(
|
|
419
|
+
"Read project theme groups, options, defaults and this call's selection",
|
|
420
|
+
["theme", ...PAGE],
|
|
421
|
+
),
|
|
422
|
+
assets: command(
|
|
423
|
+
"Read local asset definitions, including fonts, media, colors and typographies",
|
|
424
|
+
["kind", ...PAGE],
|
|
425
|
+
{
|
|
426
|
+
overrides: {
|
|
427
|
+
kind: { values: ["colors", "fonts", "media", "typographies"] },
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
),
|
|
431
|
+
"token-export": command(
|
|
432
|
+
"Export resolved Token values as JSON; --by GROUP writes one file per option",
|
|
433
|
+
["type", "token", "by", "tokenFormat", "theme", "output"],
|
|
434
|
+
{ overrides: { output: { description: "Directory for the files; default a temporary directory" } } },
|
|
435
|
+
),
|
|
436
|
+
changes: command(
|
|
437
|
+
"What changed since a revision you saw, by name: themes, Tokens, components and pages, including edits made in the App",
|
|
438
|
+
["since", "limit"],
|
|
439
|
+
),
|
|
440
|
+
view: command(
|
|
441
|
+
"Look at anything by name: the package, every or one page, component, variant, element or Tokens, as text, wireframe, png or issues",
|
|
442
|
+
[
|
|
443
|
+
"as",
|
|
444
|
+
"canvas",
|
|
445
|
+
"page",
|
|
446
|
+
"platform",
|
|
447
|
+
"component",
|
|
448
|
+
"variant",
|
|
449
|
+
"element",
|
|
450
|
+
"token",
|
|
451
|
+
"allTokens",
|
|
452
|
+
"allComponents",
|
|
453
|
+
"allPages",
|
|
454
|
+
"scale",
|
|
455
|
+
"output",
|
|
456
|
+
...VIEW,
|
|
457
|
+
"designSystem",
|
|
458
|
+
"locale",
|
|
459
|
+
...PAGE,
|
|
460
|
+
],
|
|
461
|
+
{ conflicts: { designSystem: ["page", "component", "canvas", "token", "allTokens", "allComponents", "allPages"] } },
|
|
462
|
+
),
|
|
463
|
+
export: command(
|
|
464
|
+
"Export a page, component or the design system page as a text wireframe or PNG file, by name",
|
|
465
|
+
[
|
|
466
|
+
...TARGET,
|
|
467
|
+
...VIEW,
|
|
468
|
+
"designSystem",
|
|
469
|
+
"artifactFormat",
|
|
470
|
+
"locale",
|
|
471
|
+
"scale",
|
|
472
|
+
"output",
|
|
473
|
+
"evidence",
|
|
474
|
+
"base64",
|
|
475
|
+
],
|
|
476
|
+
{
|
|
477
|
+
conflicts: {
|
|
478
|
+
evidence: ["component", "variant", "designSystem", "locale"],
|
|
479
|
+
designSystem: ["page", "component"],
|
|
480
|
+
},
|
|
481
|
+
requires: [
|
|
482
|
+
{
|
|
483
|
+
field: "artifactFormat",
|
|
484
|
+
when: ["evidence", "base64"],
|
|
485
|
+
code: "missing_png_format",
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
},
|
|
489
|
+
),
|
|
490
|
+
validate: command(
|
|
491
|
+
"Validate canonical data and report text/layout/contrast issues and coverage; a page checks every version and a component every variant unless one is named",
|
|
492
|
+
[...TARGET, ...VIEW, "designSystem", "limit", "offset"],
|
|
493
|
+
{ conflicts: { designSystem: ["page", "component"] } },
|
|
494
|
+
),
|
|
495
|
+
inspect: command("Read package identity, revision and counts"),
|
|
496
|
+
list: command("Locate objects by kind", ["kind", ...PAGE], {
|
|
497
|
+
output: DISCOVERY_OUTPUT,
|
|
498
|
+
overrides: {
|
|
499
|
+
limit: { defaultWhenFull: 100 },
|
|
500
|
+
kind: {
|
|
501
|
+
default: "all",
|
|
502
|
+
values: [
|
|
503
|
+
"all",
|
|
504
|
+
"screens",
|
|
505
|
+
"presentations",
|
|
506
|
+
"contexts",
|
|
507
|
+
"tokens",
|
|
508
|
+
"components",
|
|
509
|
+
"scenarios",
|
|
510
|
+
"requirements",
|
|
511
|
+
"flows",
|
|
512
|
+
],
|
|
513
|
+
errorCode: "unknown_list_kind",
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
}),
|
|
517
|
+
catalog: command(
|
|
518
|
+
"Read the design-system catalog",
|
|
519
|
+
["context", "theme", ...PAGE],
|
|
520
|
+
{ output: DISCOVERY_OUTPUT },
|
|
521
|
+
),
|
|
522
|
+
"search-components": command(
|
|
523
|
+
"Find reusable components",
|
|
524
|
+
["query", "theme", "limit"],
|
|
525
|
+
{
|
|
526
|
+
output: DISCOVERY_OUTPUT,
|
|
527
|
+
required: [required("query", "missing_component_search")],
|
|
528
|
+
overrides: {
|
|
529
|
+
query: {
|
|
530
|
+
nonBlank: true,
|
|
531
|
+
errorCode: "missing_component_search",
|
|
532
|
+
},
|
|
533
|
+
limit: { minimum: 1, errorCode: "invalid_component_search_limit" },
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
),
|
|
537
|
+
tokens: command("Read effective Tokens for this theme and Context", [
|
|
538
|
+
"definitions",
|
|
539
|
+
"token",
|
|
540
|
+
"group",
|
|
541
|
+
"type",
|
|
542
|
+
"context",
|
|
543
|
+
"locale",
|
|
544
|
+
"theme",
|
|
545
|
+
...PAGE,
|
|
546
|
+
]),
|
|
547
|
+
"search-tokens": command(
|
|
548
|
+
"Find reusable Tokens",
|
|
549
|
+
[
|
|
550
|
+
"allThemes",
|
|
551
|
+
"color",
|
|
552
|
+
"context",
|
|
553
|
+
"query",
|
|
554
|
+
"theme",
|
|
555
|
+
"type",
|
|
556
|
+
"value",
|
|
557
|
+
"limit",
|
|
558
|
+
],
|
|
559
|
+
{
|
|
560
|
+
atLeastOne: [
|
|
561
|
+
{
|
|
562
|
+
fields: ["query", "type", "value", "color"],
|
|
563
|
+
code: "missing_token_search",
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
conflictsWith: [
|
|
567
|
+
{ fields: ["color", "value"], code: "ambiguous_token_search_value" },
|
|
568
|
+
{ fields: ["allThemes", "theme"], code: "conflicting_theme_options" },
|
|
569
|
+
],
|
|
570
|
+
overrides: {
|
|
571
|
+
query: { nonBlank: true, errorCode: "missing_token_search" },
|
|
572
|
+
limit: { minimum: 1, errorCode: "invalid_token_search_limit" },
|
|
573
|
+
type: {
|
|
574
|
+
valuesWhen: [
|
|
575
|
+
{
|
|
576
|
+
when: ["color"],
|
|
577
|
+
values: ["color"],
|
|
578
|
+
errorCode: "conflicting_token_search_type",
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
),
|
|
585
|
+
"effective-token": command("Resolve one Token", [...TOKEN, "path"], {
|
|
586
|
+
required: [required("path", "missing_token_path")],
|
|
587
|
+
}),
|
|
588
|
+
"explain-token": command("Explain one Token resolution", [...TOKEN, "path"], {
|
|
589
|
+
required: [required("path", "missing_token_path")],
|
|
590
|
+
}),
|
|
591
|
+
"import-draft": command(
|
|
592
|
+
"Import a separate Draft package",
|
|
593
|
+
["input", "kind", "packageId"],
|
|
594
|
+
{
|
|
595
|
+
required: [required("input", "missing_draft_input")],
|
|
596
|
+
overrides: {
|
|
597
|
+
kind: { default: "figma", values: ["figma", "svg", "png"] },
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
),
|
|
601
|
+
"import-media": command(
|
|
602
|
+
"Import a media file",
|
|
603
|
+
["file", "inverseOut", "mediaPath", "name"],
|
|
604
|
+
{ required: [required("file", "missing_media_file")] },
|
|
605
|
+
),
|
|
606
|
+
"import-font": command(
|
|
607
|
+
"Import a font file",
|
|
608
|
+
[
|
|
609
|
+
"family",
|
|
610
|
+
"file",
|
|
611
|
+
"inverseOut",
|
|
612
|
+
"name",
|
|
613
|
+
"style",
|
|
614
|
+
"weight",
|
|
615
|
+
],
|
|
616
|
+
{
|
|
617
|
+
required: [
|
|
618
|
+
required("file", "missing_font_file"),
|
|
619
|
+
required("family", "missing_font_family"),
|
|
620
|
+
],
|
|
621
|
+
},
|
|
622
|
+
),
|
|
623
|
+
"import-tokens": command(
|
|
624
|
+
"Review or apply a token import",
|
|
625
|
+
[
|
|
626
|
+
"apply",
|
|
627
|
+
"batchId",
|
|
628
|
+
"compact",
|
|
629
|
+
"dryRun",
|
|
630
|
+
"input",
|
|
631
|
+
"inverseOut",
|
|
632
|
+
"select",
|
|
633
|
+
"setName",
|
|
634
|
+
],
|
|
635
|
+
{ required: [required("input", "missing_import_input")] },
|
|
636
|
+
),
|
|
637
|
+
"library-refresh": command("Refresh a declared URL Library", ["library"]),
|
|
638
|
+
"draft-diff": command("Compare two Drafts", ["after"], {
|
|
639
|
+
required: [required("after", "missing_draft_after")],
|
|
640
|
+
}),
|
|
641
|
+
"draft-compile": command(
|
|
642
|
+
"Compile selected Draft fields into a batch",
|
|
643
|
+
["batchId", "draft", "selections"],
|
|
644
|
+
{
|
|
645
|
+
required: [
|
|
646
|
+
required("draft", "missing_draft_compile_input"),
|
|
647
|
+
required("selections", "missing_draft_compile_input"),
|
|
648
|
+
],
|
|
649
|
+
},
|
|
650
|
+
),
|
|
651
|
+
"token-set": command(
|
|
652
|
+
"Create or change Tokens by name; one call sets default and theme option values",
|
|
653
|
+
["intent", "batchId", ...WRITE],
|
|
654
|
+
{ required: [required("intent", "missing_intent")] },
|
|
655
|
+
),
|
|
656
|
+
"component-define": command(
|
|
657
|
+
"Define a component by name: a base element tree, properties and variant changes",
|
|
658
|
+
["intent", "batchId", ...WRITE],
|
|
659
|
+
{ required: [required("intent", "missing_intent")] },
|
|
660
|
+
),
|
|
661
|
+
"page-draw": command(
|
|
662
|
+
"Draw a page (or one container) from a tree of named elements and component instances",
|
|
663
|
+
["intent", "batchId", ...WRITE],
|
|
664
|
+
{ required: [required("intent", "missing_intent")] },
|
|
665
|
+
),
|
|
666
|
+
"page-move": command(
|
|
667
|
+
"Move an element up, down, left or right among its siblings, or (without --element) a page left or right in its row on the canvas; the CLI picks the coordinates and keeps things from overlapping",
|
|
668
|
+
["page", "platform", "element", "direction", "moveSteps", "batchId", ...WRITE],
|
|
669
|
+
{
|
|
670
|
+
required: [required("page", "missing_page"), required("direction", "missing_direction")],
|
|
671
|
+
overrides: { element: { description: 'Element to move, or "Parent / Element" when the name repeats' } },
|
|
672
|
+
},
|
|
673
|
+
),
|
|
674
|
+
"page-set": command(
|
|
675
|
+
"Change fields of one element, or of the page itself, without drawing it again: sizes, colors, text, layout, a copy's props, text and set. Texts are measured and containers that hug grow, as page draw does",
|
|
676
|
+
["page", "platform", "element", "all", "setField", "intent", "batchId", ...WRITE],
|
|
677
|
+
{
|
|
678
|
+
required: [required("page", "missing_page")],
|
|
679
|
+
overrides: {
|
|
680
|
+
all: { description: "Change every version that has the element; without it an element in several versions needs --platform" },
|
|
681
|
+
element: { description: 'Element to change ("Parent / Element" when the name repeats); without it the page itself (its size, fill, layout)' },
|
|
682
|
+
platform: { description: "One version; needed when the element (or the page) is in several, unless --all" },
|
|
683
|
+
intent: { description: 'JSON file {"set": {...}} for values awkward on a command line; --set adds to it' },
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
),
|
|
687
|
+
"component-set": command(
|
|
688
|
+
"Change fields of one element of a component, or of the component itself, without defining it again: in the one variant that has it, the one --variant names, or every one with --all",
|
|
689
|
+
["component", "variant", "element", "all", "setField", "intent", "batchId", ...WRITE],
|
|
690
|
+
{
|
|
691
|
+
required: [required("component", "missing_component")],
|
|
692
|
+
overrides: {
|
|
693
|
+
all: { description: "Change every variant that has the element; without it an element in several variants needs --variant" },
|
|
694
|
+
element: { description: "Element to change; without it the component itself" },
|
|
695
|
+
intent: { description: 'JSON file {"set": {...}}; --set adds to it' },
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
),
|
|
699
|
+
"canvas-list": command("List canvases, the rows on each (one per module) and their pages left to right", []),
|
|
700
|
+
"canvas-rename": command("Rename a canvas", ["canvas", "to", "batchId", ...WRITE], {
|
|
701
|
+
required: [required("canvas", "missing_canvas"), required("to", "missing_to")],
|
|
702
|
+
overrides: { to: { description: "New canvas name" } },
|
|
703
|
+
}),
|
|
704
|
+
"canvas-put": command(
|
|
705
|
+
"Put a page on a canvas (made when it does not exist); by default every page is on one canvas",
|
|
706
|
+
["page", "canvas", "batchId", ...WRITE],
|
|
707
|
+
{ required: [required("page", "missing_page"), required("canvas", "missing_canvas")] },
|
|
708
|
+
),
|
|
709
|
+
"flow-link": command(
|
|
710
|
+
"Link an element to a page by name; --platform picks the version (default: the page's main one)",
|
|
711
|
+
["from", "to", "on", "action", "platform", "batchId", ...WRITE],
|
|
712
|
+
{
|
|
713
|
+
required: [required("from", "missing_from")],
|
|
714
|
+
overrides: {
|
|
715
|
+
action: {
|
|
716
|
+
description: "navigate (default), overlay or back",
|
|
717
|
+
values: ["navigate", "overlay", "back"],
|
|
718
|
+
errorCode: "invalid_flow_action",
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
),
|
|
723
|
+
"flow-start": command(
|
|
724
|
+
"Make a page version the start of a prototype flow, or (--remove) stop it being one; the flow's row on the canvas starts with it",
|
|
725
|
+
["page", "platform", "removeStart", "batchId", ...WRITE],
|
|
726
|
+
{ required: [required("page", "missing_page")] },
|
|
727
|
+
),
|
|
728
|
+
"flow-list": command("List prototype starts and element links by page and element name", []),
|
|
729
|
+
"flow-unlink": command(
|
|
730
|
+
"Remove an element's links; --on or --to removes only those",
|
|
731
|
+
["from", "on", "to", "platform", "batchId", ...WRITE],
|
|
732
|
+
{
|
|
733
|
+
required: [required("from", "missing_from")],
|
|
734
|
+
overrides: { to: { description: "Remove only the link to this page" }, on: { description: "Remove only links on this trigger" } },
|
|
735
|
+
},
|
|
736
|
+
),
|
|
737
|
+
"page-rename": command(
|
|
738
|
+
"Rename a page (its boards and start follow), or one of its elements (--element). A used name is numbered (\"List 2\")",
|
|
739
|
+
["page", "platform", "element", "to", "batchId", ...WRITE],
|
|
740
|
+
{
|
|
741
|
+
required: [required("page", "missing_page"), required("to", "missing_to")],
|
|
742
|
+
overrides: {
|
|
743
|
+
to: { description: "New name: \"Module / Page\" or a last part that keeps the module; with --element, the element's new name" },
|
|
744
|
+
element: { description: 'Rename this element instead: a name, "Parent / Element", or "Name [2]" when siblings share a name' },
|
|
745
|
+
platform: { description: "The version --element is in; default the main one" },
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
),
|
|
749
|
+
"page-delete": command(
|
|
750
|
+
"Delete a page, one platform version (--platform) or one element (--element); pages other pages link to are protected",
|
|
751
|
+
["page", "platform", "element", "batchId", ...WRITE],
|
|
752
|
+
{
|
|
753
|
+
required: [required("page", "missing_page")],
|
|
754
|
+
overrides: {
|
|
755
|
+
platform: { description: "Delete only this version, or the version --element is in" },
|
|
756
|
+
element: { description: 'Delete this element and its content: a name, or "Parent / Element"' },
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
),
|
|
760
|
+
"component-rename": command(
|
|
761
|
+
"Rename a component (copies keep pointing at it), or one of its elements in every variant that has it (--element; with --variant only there). A used name is numbered",
|
|
762
|
+
["component", "element", "variant", "to", "batchId", ...WRITE],
|
|
763
|
+
{
|
|
764
|
+
required: [required("component", "missing_component"), required("to", "missing_to")],
|
|
765
|
+
overrides: {
|
|
766
|
+
to: { description: "New component name, or the element's new name with --element" },
|
|
767
|
+
element: { description: 'Rename this element instead: a name, "Parent / Element", or "Name [2]" when siblings share a name' },
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
),
|
|
771
|
+
"component-delete": command(
|
|
772
|
+
"Delete a component, or one variant (--variant); components in use are protected",
|
|
773
|
+
["component", "variant", "batchId", ...WRITE],
|
|
774
|
+
{ required: [required("component", "missing_component")] },
|
|
775
|
+
),
|
|
776
|
+
"asset-set": command(
|
|
777
|
+
"Create or change a color or typography by name",
|
|
778
|
+
["colorName", "typographyName", "assetValue", "batchId", ...WRITE],
|
|
779
|
+
{
|
|
780
|
+
exactlyOne: [["colorName", "typographyName"]],
|
|
781
|
+
required: [required("assetValue", "missing_value")],
|
|
782
|
+
},
|
|
783
|
+
),
|
|
784
|
+
"asset-delete": command(
|
|
785
|
+
"Delete a color or typography by name",
|
|
786
|
+
["colorName", "typographyName", "batchId", ...WRITE],
|
|
787
|
+
{ exactlyOne: [["colorName", "typographyName"]] },
|
|
788
|
+
),
|
|
789
|
+
"media-delete": command(
|
|
790
|
+
"Delete a media file by name",
|
|
791
|
+
["mediaName", "batchId", ...WRITE],
|
|
792
|
+
{ required: [required("mediaName", "missing_media")] },
|
|
793
|
+
),
|
|
794
|
+
"font-delete": command(
|
|
795
|
+
"Delete a font family, or one of its variants (--variant Bold)",
|
|
796
|
+
["fontFamily", "variant", "batchId", ...WRITE],
|
|
797
|
+
{
|
|
798
|
+
required: [required("fontFamily", "missing_font")],
|
|
799
|
+
overrides: { variant: { description: "One font variant by name, for example Bold" } },
|
|
800
|
+
},
|
|
801
|
+
),
|
|
802
|
+
"theme-add": command(
|
|
803
|
+
"Add theme options by full name; a new group is created with them",
|
|
804
|
+
["theme", "batchId", ...WRITE],
|
|
805
|
+
{
|
|
806
|
+
required: [required("theme", "missing_theme")],
|
|
807
|
+
overrides: {
|
|
808
|
+
theme: {
|
|
809
|
+
description:
|
|
810
|
+
"Option to add as Group/Option, for example Viewport/Tablet; repeat for several. The first option of a new group is its default",
|
|
811
|
+
defaultFrom: undefined,
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
),
|
|
816
|
+
"theme-rename": command(
|
|
817
|
+
"Rename one theme option or one theme group",
|
|
818
|
+
["theme", "group", "to", "batchId", ...WRITE],
|
|
819
|
+
{
|
|
820
|
+
exactlyOne: [["theme", "group"]],
|
|
821
|
+
required: [required("to", "missing_to")],
|
|
822
|
+
overrides: {
|
|
823
|
+
theme: { description: "Option to rename, as Group/Option", defaultFrom: undefined },
|
|
824
|
+
group: { description: "Group to rename" },
|
|
825
|
+
to: { description: "New name: an option name (or Group/New) for --theme, a group name for --group" },
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
),
|
|
829
|
+
"theme-default": command(
|
|
830
|
+
"Make one option its group's default",
|
|
831
|
+
["theme", "batchId", ...WRITE],
|
|
832
|
+
{
|
|
833
|
+
required: [required("theme", "missing_theme")],
|
|
834
|
+
overrides: {
|
|
835
|
+
theme: { description: "Option to make the default, as Group/Option", defaultFrom: undefined },
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
),
|
|
839
|
+
"theme-delete": command(
|
|
840
|
+
"Delete one theme option or a whole group",
|
|
841
|
+
["theme", "group", "batchId", ...WRITE],
|
|
842
|
+
{
|
|
843
|
+
exactlyOne: [["theme", "group"]],
|
|
844
|
+
overrides: {
|
|
845
|
+
theme: { description: "Option to delete, as Group/Option", defaultFrom: undefined },
|
|
846
|
+
group: { description: "Group to delete with all its options" },
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
),
|
|
850
|
+
"token-delete": command(
|
|
851
|
+
"Delete a Token by name from every theme option; bound Tokens are protected",
|
|
852
|
+
["path", "batchId", ...WRITE],
|
|
853
|
+
{
|
|
854
|
+
required: [required("path", "missing_token_path")],
|
|
855
|
+
overrides: { path: { description: "Token name, for example color.brand" } },
|
|
856
|
+
},
|
|
857
|
+
),
|
|
858
|
+
impact: command(
|
|
859
|
+
"Locate bindings of one Token",
|
|
860
|
+
["path", "theme"],
|
|
861
|
+
{ required: [required("path", "missing_token_path")] },
|
|
862
|
+
),
|
|
863
|
+
apply: command("Apply one atomic batch", ["batch", ...WRITE], {
|
|
864
|
+
required: [required("batch", "missing_batch")],
|
|
865
|
+
}),
|
|
866
|
+
watch: command("Emit changed package revisions", ["interval", "maxEvents"]),
|
|
867
|
+
repair: command(
|
|
868
|
+
"Read conflicts or apply one offered repair",
|
|
869
|
+
[
|
|
870
|
+
"action",
|
|
871
|
+
"asset",
|
|
872
|
+
"assetKind",
|
|
873
|
+
"batchId",
|
|
874
|
+
"conflict",
|
|
875
|
+
"foundation",
|
|
876
|
+
"replacementAssetId",
|
|
877
|
+
"replacementPackageId",
|
|
878
|
+
"selection",
|
|
879
|
+
],
|
|
880
|
+
{
|
|
881
|
+
requires: [
|
|
882
|
+
{
|
|
883
|
+
field: "replacementPackageId",
|
|
884
|
+
when: { action: "retarget-reference" },
|
|
885
|
+
code: "missing_repair_replacement",
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
field: "replacementAssetId",
|
|
889
|
+
when: { action: "retarget-reference" },
|
|
890
|
+
code: "missing_repair_replacement",
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
field: "foundation",
|
|
894
|
+
when: { action: "choose-foundation" },
|
|
895
|
+
code: "missing_foundation_path",
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
field: "asset",
|
|
899
|
+
when: { action: "recreate-product-asset" },
|
|
900
|
+
code: "missing_recreated_asset",
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
field: "assetKind",
|
|
904
|
+
when: { action: "recreate-product-asset" },
|
|
905
|
+
code: "missing_recreated_asset",
|
|
906
|
+
},
|
|
907
|
+
],
|
|
908
|
+
overrides: {
|
|
909
|
+
asset: {
|
|
910
|
+
type: "path",
|
|
911
|
+
description: "JSON file containing the recreated asset",
|
|
912
|
+
},
|
|
913
|
+
assetKind: {
|
|
914
|
+
valuesWhen: [
|
|
915
|
+
{
|
|
916
|
+
when: { action: "recreate-product-asset" },
|
|
917
|
+
values: ["token", "component"],
|
|
918
|
+
errorCode: "missing_recreated_asset",
|
|
919
|
+
},
|
|
920
|
+
],
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
),
|
|
925
|
+
"migrate-themes": command("Copy a package with migrated themes", ["output"], {
|
|
926
|
+
required: [required("output", "missing_migration_output")],
|
|
927
|
+
}),
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
export function commandContract(name) {
|
|
931
|
+
const spec = Object.hasOwn(COMMAND_CONTRACTS, name)
|
|
932
|
+
? COMMAND_CONTRACTS[name]
|
|
933
|
+
: undefined;
|
|
934
|
+
if (!spec)
|
|
935
|
+
throw new SmallPenError(
|
|
936
|
+
"unknown_schema_command",
|
|
937
|
+
`Unknown command: ${name ?? "(missing)"}`,
|
|
938
|
+
{
|
|
939
|
+
received: name ?? null,
|
|
940
|
+
nextOperations: [
|
|
941
|
+
{
|
|
942
|
+
argv: ["schema", "commands", "--json"],
|
|
943
|
+
operation: "smallpen.schema",
|
|
944
|
+
},
|
|
945
|
+
],
|
|
946
|
+
},
|
|
947
|
+
);
|
|
948
|
+
const parameters = Object.fromEntries(
|
|
949
|
+
[...spec.fields, ...(spec.common ?? COMMON)].map((field) => {
|
|
950
|
+
const { errorCode: _code, ...definition } = {
|
|
951
|
+
...PARAMETERS[field],
|
|
952
|
+
...spec.overrides?.[field],
|
|
953
|
+
};
|
|
954
|
+
const required = (spec.required ?? []).some(
|
|
955
|
+
(item) => item.field === field,
|
|
956
|
+
);
|
|
957
|
+
return [
|
|
958
|
+
field,
|
|
959
|
+
{
|
|
960
|
+
...definition,
|
|
961
|
+
required,
|
|
962
|
+
...(required && ["path", "string"].includes(definition.type)
|
|
963
|
+
? { minLength: 1 }
|
|
964
|
+
: {}),
|
|
965
|
+
...(definition.valuesWhen
|
|
966
|
+
? {
|
|
967
|
+
valuesWhen: definition.valuesWhen.map(
|
|
968
|
+
({ errorCode: _code, ...rule }) => rule,
|
|
969
|
+
),
|
|
970
|
+
}
|
|
971
|
+
: {}),
|
|
972
|
+
},
|
|
973
|
+
];
|
|
974
|
+
}),
|
|
975
|
+
);
|
|
976
|
+
return {
|
|
977
|
+
command: name,
|
|
978
|
+
purpose: spec.purpose,
|
|
979
|
+
arguments:
|
|
980
|
+
spec.arguments ??
|
|
981
|
+
(spec.locator === false
|
|
982
|
+
? []
|
|
983
|
+
: [
|
|
984
|
+
{
|
|
985
|
+
name: spec.locatorName ?? "package",
|
|
986
|
+
type: "path",
|
|
987
|
+
required: true,
|
|
988
|
+
},
|
|
989
|
+
]),
|
|
990
|
+
parameters,
|
|
991
|
+
...(spec.output ? { output: spec.output } : {}),
|
|
992
|
+
...(spec.requires ? { requires: spec.requires } : {}),
|
|
993
|
+
...(spec.conflicts ? { conflicts: spec.conflicts } : {}),
|
|
994
|
+
...(spec.exactlyOne ? { exactlyOne: spec.exactlyOne } : {}),
|
|
995
|
+
...(spec.atLeastOne
|
|
996
|
+
? { atLeastOne: spec.atLeastOne.map(({ fields }) => fields) }
|
|
997
|
+
: {}),
|
|
998
|
+
conflictsWith: [
|
|
999
|
+
...[["full", "compact"]].filter((group) =>
|
|
1000
|
+
group.every((field) => parameters[field]),
|
|
1001
|
+
),
|
|
1002
|
+
...(spec.conflictsWith ?? []).map(({ fields }) => fields),
|
|
1003
|
+
],
|
|
1004
|
+
next: ["schema", "command", name, "--json"],
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function conditionMatches(when, seen) {
|
|
1009
|
+
return Array.isArray(when)
|
|
1010
|
+
? when.every((field) => seen.has(field))
|
|
1011
|
+
: Object.entries(when).every(([field, value]) =>
|
|
1012
|
+
seen.get(field)?.includes(value),
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function conditionText(when, parameters) {
|
|
1017
|
+
return (
|
|
1018
|
+
Array.isArray(when)
|
|
1019
|
+
? when.map((field) => parameters[field].option)
|
|
1020
|
+
: Object.entries(when).map(
|
|
1021
|
+
([field, value]) => `${parameters[field].option} ${value}`,
|
|
1022
|
+
)
|
|
1023
|
+
).join(" and ");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
export function commandHelp(name) {
|
|
1027
|
+
const spec = commandContract(name);
|
|
1028
|
+
const options = (fields) =>
|
|
1029
|
+
fields.map((field) => spec.parameters[field].option).join(" or ");
|
|
1030
|
+
const rules = [
|
|
1031
|
+
...(spec.requires ?? []).map(
|
|
1032
|
+
({ field, when }) =>
|
|
1033
|
+
`${spec.parameters[field].option} is required with ${conditionText(when, spec.parameters)}`,
|
|
1034
|
+
),
|
|
1035
|
+
...Object.entries(spec.conflicts ?? {}).map(
|
|
1036
|
+
([field, others]) =>
|
|
1037
|
+
`${spec.parameters[field].option} conflicts with ${options(others)}`,
|
|
1038
|
+
),
|
|
1039
|
+
...(spec.exactlyOne ?? []).map(
|
|
1040
|
+
(fields) => `Choose exactly one of ${options(fields)}`,
|
|
1041
|
+
),
|
|
1042
|
+
...(spec.atLeastOne ?? []).map(
|
|
1043
|
+
(fields) => `Supply at least one of ${options(fields)}`,
|
|
1044
|
+
),
|
|
1045
|
+
...spec.conflictsWith.map(
|
|
1046
|
+
(fields) => `Choose at most one of ${options(fields)}`,
|
|
1047
|
+
),
|
|
1048
|
+
...Object.entries(spec.parameters).flatMap(([_field, parameter]) =>
|
|
1049
|
+
(parameter.valuesWhen ?? []).map(
|
|
1050
|
+
({ when, values }) =>
|
|
1051
|
+
`${parameter.option} accepts ${values.join("|")} with ${conditionText(when, spec.parameters)}`,
|
|
1052
|
+
),
|
|
1053
|
+
),
|
|
1054
|
+
];
|
|
1055
|
+
return (
|
|
1056
|
+
`Options:\n` +
|
|
1057
|
+
Object.entries(spec.parameters)
|
|
1058
|
+
.map(([_field, parameter]) => {
|
|
1059
|
+
const {
|
|
1060
|
+
option,
|
|
1061
|
+
type,
|
|
1062
|
+
description,
|
|
1063
|
+
required,
|
|
1064
|
+
repeatable,
|
|
1065
|
+
values,
|
|
1066
|
+
minimum,
|
|
1067
|
+
maximum,
|
|
1068
|
+
exclusiveMinimum,
|
|
1069
|
+
minLength,
|
|
1070
|
+
nonBlank,
|
|
1071
|
+
minItems,
|
|
1072
|
+
maxItems,
|
|
1073
|
+
default: fallback,
|
|
1074
|
+
defaultFrom,
|
|
1075
|
+
} = parameter;
|
|
1076
|
+
const facts = [
|
|
1077
|
+
type,
|
|
1078
|
+
required ? "required" : "optional",
|
|
1079
|
+
...(repeatable ? ["repeatable"] : []),
|
|
1080
|
+
...(values ? [values.join("|")] : []),
|
|
1081
|
+
...(minimum !== undefined ? [`min=${minimum}`] : []),
|
|
1082
|
+
...(maximum !== undefined ? [`max=${maximum}`] : []),
|
|
1083
|
+
...(exclusiveMinimum !== undefined ? [`>${exclusiveMinimum}`] : []),
|
|
1084
|
+
...(minLength !== undefined ? [`minLength=${minLength}`] : []),
|
|
1085
|
+
...(nonBlank ? ["non-blank"] : []),
|
|
1086
|
+
...(minItems !== undefined ? [`minItems=${minItems}`] : []),
|
|
1087
|
+
...(maxItems !== undefined ? [`maxItems=${maxItems}`] : []),
|
|
1088
|
+
...(fallback !== undefined ? [`default=${fallback}`] : []),
|
|
1089
|
+
...(defaultFrom ? [`default: ${defaultFrom}`] : []),
|
|
1090
|
+
];
|
|
1091
|
+
return ` ${option} (${facts.join("; ")})\n ${description}`;
|
|
1092
|
+
})
|
|
1093
|
+
.join("\n") +
|
|
1094
|
+
(rules.length
|
|
1095
|
+
? `\nConstraints:\n${rules.map((rule) => ` ${rule}`).join("\n")}`
|
|
1096
|
+
: "")
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export function parameterExpectation(spec) {
|
|
1101
|
+
const parts = [spec.type, spec.description];
|
|
1102
|
+
if (spec.values) parts.push(`one of: ${spec.values.join(" | ")}`);
|
|
1103
|
+
if (spec.minimum !== undefined) parts.push(`minimum ${spec.minimum}`);
|
|
1104
|
+
if (spec.maximum !== undefined) parts.push(`maximum ${spec.maximum}`);
|
|
1105
|
+
if (spec.exclusiveMinimum !== undefined)
|
|
1106
|
+
parts.push(`greater than ${spec.exclusiveMinimum}`);
|
|
1107
|
+
if (spec.minLength !== undefined)
|
|
1108
|
+
parts.push(`at least ${spec.minLength} character(s)`);
|
|
1109
|
+
if (spec.nonBlank) parts.push("non-blank text");
|
|
1110
|
+
return parts.join("; ");
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function facts(command, field, received = null) {
|
|
1114
|
+
const spec = commandContract(command).parameters[field];
|
|
1115
|
+
if (typeof received === "string" && received.length > 256)
|
|
1116
|
+
received = `${received.slice(0, 256)}…`;
|
|
1117
|
+
return {
|
|
1118
|
+
command,
|
|
1119
|
+
field,
|
|
1120
|
+
option: spec.option,
|
|
1121
|
+
path: spec.option,
|
|
1122
|
+
expected: parameterExpectation(spec),
|
|
1123
|
+
expectedType: spec.type,
|
|
1124
|
+
received,
|
|
1125
|
+
...(spec.values ? { validValues: spec.values } : {}),
|
|
1126
|
+
nextOperations: [
|
|
1127
|
+
{
|
|
1128
|
+
argv: ["schema", "command", command, "--json"],
|
|
1129
|
+
operation: "smallpen.schema",
|
|
1130
|
+
},
|
|
1131
|
+
],
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Validate the single command spelling before opening a package.
|
|
1136
|
+
export function validateCommandArguments(command, args) {
|
|
1137
|
+
const spec = commandContract(command);
|
|
1138
|
+
const argument = spec.arguments[0];
|
|
1139
|
+
if (argument?.required && (!args[1] || args[1].startsWith("-")))
|
|
1140
|
+
throw new SmallPenError(
|
|
1141
|
+
argument.name === "workspace" ? "missing_workspace" : "missing_package",
|
|
1142
|
+
`A ${argument.name} path is required`,
|
|
1143
|
+
{
|
|
1144
|
+
command,
|
|
1145
|
+
field: argument.name,
|
|
1146
|
+
path: "arguments[0]",
|
|
1147
|
+
expected: `a non-empty ${argument.name} path`,
|
|
1148
|
+
expectedType: argument.type,
|
|
1149
|
+
received: null,
|
|
1150
|
+
nextOperations: [{ argv: spec.next, operation: "smallpen.schema" }],
|
|
1151
|
+
},
|
|
1152
|
+
);
|
|
1153
|
+
const byOption = new Map(
|
|
1154
|
+
Object.entries(spec.parameters).map(([field, parameter]) => [
|
|
1155
|
+
parameter.option,
|
|
1156
|
+
{ field, parameter },
|
|
1157
|
+
]),
|
|
1158
|
+
);
|
|
1159
|
+
const seen = new Map();
|
|
1160
|
+
const hasValue = (field) =>
|
|
1161
|
+
seen.get(field) === true ||
|
|
1162
|
+
(seen.get(field) ?? []).some((value) => value !== "");
|
|
1163
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
1164
|
+
const option = args[index];
|
|
1165
|
+
const match = byOption.get(option);
|
|
1166
|
+
if (!match) {
|
|
1167
|
+
const code = option.startsWith("-")
|
|
1168
|
+
? "unknown_option"
|
|
1169
|
+
: "unexpected_argument";
|
|
1170
|
+
const validOptions = [...byOption.keys()].sort();
|
|
1171
|
+
const suggestion = suggestField(option, validOptions);
|
|
1172
|
+
throw new SmallPenError(
|
|
1173
|
+
code,
|
|
1174
|
+
`${code === "unknown_option" ? "Unknown option" : "Unexpected positional argument"}: ${option}`,
|
|
1175
|
+
{
|
|
1176
|
+
command,
|
|
1177
|
+
option,
|
|
1178
|
+
validOptions,
|
|
1179
|
+
...(suggestion ? { suggestion } : {}),
|
|
1180
|
+
nextOperations: [
|
|
1181
|
+
{
|
|
1182
|
+
argv: ["schema", "command", command, "--json"],
|
|
1183
|
+
operation: "smallpen.schema",
|
|
1184
|
+
},
|
|
1185
|
+
],
|
|
1186
|
+
},
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
const { field, parameter } = match;
|
|
1190
|
+
if (seen.has(field) && !parameter.repeatable)
|
|
1191
|
+
throw new SmallPenError(
|
|
1192
|
+
"duplicate_option",
|
|
1193
|
+
`${parameter.option} may be given only once`,
|
|
1194
|
+
facts(command, field, option),
|
|
1195
|
+
);
|
|
1196
|
+
if (parameter.type === "boolean") {
|
|
1197
|
+
seen.set(field, true);
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
const value = args[++index];
|
|
1201
|
+
if (value === undefined || value.startsWith("--"))
|
|
1202
|
+
throw new SmallPenError(
|
|
1203
|
+
"missing_option_value",
|
|
1204
|
+
`${parameter.option} requires a value`,
|
|
1205
|
+
facts(command, field),
|
|
1206
|
+
);
|
|
1207
|
+
const definition = {
|
|
1208
|
+
...PARAMETERS[field],
|
|
1209
|
+
...COMMAND_CONTRACTS[command].overrides?.[field],
|
|
1210
|
+
};
|
|
1211
|
+
if (
|
|
1212
|
+
(parameter.minLength !== undefined &&
|
|
1213
|
+
value.length < parameter.minLength) ||
|
|
1214
|
+
(parameter.nonBlank && value.trim() === "")
|
|
1215
|
+
)
|
|
1216
|
+
throw new SmallPenError(
|
|
1217
|
+
definition.errorCode ??
|
|
1218
|
+
COMMAND_CONTRACTS[command].required?.find(
|
|
1219
|
+
(item) => item.field === field,
|
|
1220
|
+
)?.code ??
|
|
1221
|
+
"invalid_option_value",
|
|
1222
|
+
`${parameter.option} requires a non-empty value`,
|
|
1223
|
+
facts(command, field, value),
|
|
1224
|
+
);
|
|
1225
|
+
if (parameter.type === "integer" || parameter.type === "number") {
|
|
1226
|
+
const parsed = Number(value);
|
|
1227
|
+
if (
|
|
1228
|
+
value.trim() === "" ||
|
|
1229
|
+
!Number.isFinite(parsed) ||
|
|
1230
|
+
(parameter.type === "integer" && !Number.isSafeInteger(parsed)) ||
|
|
1231
|
+
(parameter.minimum !== undefined && parsed < parameter.minimum) ||
|
|
1232
|
+
(parameter.maximum !== undefined && parsed > parameter.maximum) ||
|
|
1233
|
+
(parameter.exclusiveMinimum !== undefined &&
|
|
1234
|
+
parsed <= parameter.exclusiveMinimum)
|
|
1235
|
+
) {
|
|
1236
|
+
throw new SmallPenError(
|
|
1237
|
+
definition.errorCode ??
|
|
1238
|
+
(parameter.type === "integer"
|
|
1239
|
+
? "invalid_integer_option"
|
|
1240
|
+
: "invalid_number_option"),
|
|
1241
|
+
`${parameter.option} is outside its ${parameter.type} range`,
|
|
1242
|
+
facts(command, field, value),
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
const enumValue =
|
|
1247
|
+
parameter.type === "integer" || parameter.type === "number"
|
|
1248
|
+
? Number(value)
|
|
1249
|
+
: value;
|
|
1250
|
+
if (parameter.values && !parameter.values.includes(enumValue))
|
|
1251
|
+
throw new SmallPenError(
|
|
1252
|
+
definition.errorCode ?? "invalid_option_value",
|
|
1253
|
+
`${parameter.option} must be one of: ${parameter.values.join(", ")}`,
|
|
1254
|
+
facts(command, field, value),
|
|
1255
|
+
);
|
|
1256
|
+
if (parameter.type === "assignment" && !/^[^=]+=[\s\S]+$/.test(value))
|
|
1257
|
+
throw new SmallPenError(
|
|
1258
|
+
field === "context"
|
|
1259
|
+
? "invalid_context_argument"
|
|
1260
|
+
: "invalid_initialization_answer_argument",
|
|
1261
|
+
`${parameter.option} requires ${field === "context" ? "CONTEXT=VALUE" : "QUESTION_ID=JSON"}`,
|
|
1262
|
+
facts(command, field, value),
|
|
1263
|
+
);
|
|
1264
|
+
if (parameter.type === "json" || field === "answer") {
|
|
1265
|
+
const source =
|
|
1266
|
+
field === "answer" ? value.slice(value.indexOf("=") + 1) : value;
|
|
1267
|
+
try {
|
|
1268
|
+
JSON.parse(source);
|
|
1269
|
+
} catch {
|
|
1270
|
+
throw new SmallPenError(
|
|
1271
|
+
field === "answer"
|
|
1272
|
+
? "invalid_initialization_answer_json"
|
|
1273
|
+
: definition.errorCode,
|
|
1274
|
+
`${parameter.option} requires valid JSON`,
|
|
1275
|
+
{
|
|
1276
|
+
...facts(command, field, value),
|
|
1277
|
+
...(field === "answer"
|
|
1278
|
+
? { questionId: value.slice(0, value.indexOf("=")) }
|
|
1279
|
+
: {}),
|
|
1280
|
+
},
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (
|
|
1285
|
+
parameter.uniqueBy === "assignment-key" &&
|
|
1286
|
+
(seen.get(field) ?? []).some(
|
|
1287
|
+
(previous) =>
|
|
1288
|
+
previous.slice(0, previous.indexOf("=")) ===
|
|
1289
|
+
value.slice(0, value.indexOf("=")),
|
|
1290
|
+
)
|
|
1291
|
+
)
|
|
1292
|
+
throw new SmallPenError(
|
|
1293
|
+
"duplicate_option",
|
|
1294
|
+
`${parameter.option} cannot set the same key twice`,
|
|
1295
|
+
facts(command, field, value),
|
|
1296
|
+
);
|
|
1297
|
+
seen.set(field, [
|
|
1298
|
+
...(Array.isArray(seen.get(field)) ? seen.get(field) : []),
|
|
1299
|
+
value,
|
|
1300
|
+
]);
|
|
1301
|
+
}
|
|
1302
|
+
for (const [field, parameter] of Object.entries(spec.parameters)) {
|
|
1303
|
+
const count = seen.get(field)?.length ?? 0;
|
|
1304
|
+
if (
|
|
1305
|
+
(parameter.minItems !== undefined && count < parameter.minItems) ||
|
|
1306
|
+
(parameter.maxItems !== undefined && count > parameter.maxItems)
|
|
1307
|
+
)
|
|
1308
|
+
throw new SmallPenError(
|
|
1309
|
+
"invalid_compare_count",
|
|
1310
|
+
`${parameter.option} requires ${parameter.minItems} to ${parameter.maxItems} items`,
|
|
1311
|
+
facts(command, field, count),
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
for (const group of spec.conflictsWith)
|
|
1315
|
+
if (group.every((field) => seen.has(field)))
|
|
1316
|
+
throw new SmallPenError(
|
|
1317
|
+
COMMAND_CONTRACTS[command].conflictsWith?.find(
|
|
1318
|
+
({ fields }) => fields.join() === group.join(),
|
|
1319
|
+
)?.code ?? "conflicting_output_options",
|
|
1320
|
+
`Choose at most one of ${group.map((field) => spec.parameters[field].option).join(" or ")}`,
|
|
1321
|
+
{
|
|
1322
|
+
...facts(
|
|
1323
|
+
command,
|
|
1324
|
+
group[0],
|
|
1325
|
+
seen.get(group[0]) === true ? true : seen.get(group[0])[0],
|
|
1326
|
+
),
|
|
1327
|
+
fields: group,
|
|
1328
|
+
expected: "at most one of the listed options",
|
|
1329
|
+
},
|
|
1330
|
+
);
|
|
1331
|
+
for (const item of COMMAND_CONTRACTS[command].required ?? [])
|
|
1332
|
+
if (!seen.has(item.field))
|
|
1333
|
+
throw new SmallPenError(
|
|
1334
|
+
item.code,
|
|
1335
|
+
`${command} requires ${spec.parameters[item.field].option}`,
|
|
1336
|
+
facts(command, item.field),
|
|
1337
|
+
);
|
|
1338
|
+
for (const item of spec.requires ?? [])
|
|
1339
|
+
if (conditionMatches(item.when, seen) && !hasValue(item.field))
|
|
1340
|
+
throw new SmallPenError(
|
|
1341
|
+
item.code,
|
|
1342
|
+
`${spec.parameters[item.field].option} is required with ${conditionText(item.when, spec.parameters)}`,
|
|
1343
|
+
{
|
|
1344
|
+
...facts(command, item.field, seen.get(item.field)?.[0] ?? null),
|
|
1345
|
+
expected: `${parameterExpectation(spec.parameters[item.field])}; required with ${conditionText(item.when, spec.parameters)}`,
|
|
1346
|
+
},
|
|
1347
|
+
);
|
|
1348
|
+
for (const [field, parameter] of Object.entries(spec.parameters))
|
|
1349
|
+
for (const rule of {
|
|
1350
|
+
...PARAMETERS[field],
|
|
1351
|
+
...COMMAND_CONTRACTS[command].overrides?.[field],
|
|
1352
|
+
}.valuesWhen ?? [])
|
|
1353
|
+
if (
|
|
1354
|
+
seen.has(field) &&
|
|
1355
|
+
conditionMatches(rule.when, seen) &&
|
|
1356
|
+
!rule.values.includes(seen.get(field)[0])
|
|
1357
|
+
)
|
|
1358
|
+
throw new SmallPenError(
|
|
1359
|
+
rule.errorCode,
|
|
1360
|
+
`${parameter.option} accepts ${rule.values.join(" or ")} with ${conditionText(rule.when, spec.parameters)}`,
|
|
1361
|
+
{
|
|
1362
|
+
...facts(command, field, seen.get(field)[0]),
|
|
1363
|
+
expected: `${rule.values.join(" or ")} with ${conditionText(rule.when, spec.parameters)}`,
|
|
1364
|
+
validValues: rule.values,
|
|
1365
|
+
},
|
|
1366
|
+
);
|
|
1367
|
+
for (const { fields, code } of COMMAND_CONTRACTS[command].atLeastOne ?? [])
|
|
1368
|
+
if (!fields.some((field) => seen.has(field)))
|
|
1369
|
+
throw new SmallPenError(
|
|
1370
|
+
code,
|
|
1371
|
+
`Supply at least one of ${fields.map((field) => spec.parameters[field].option).join(" or ")}`,
|
|
1372
|
+
{
|
|
1373
|
+
...facts(command, fields[0]),
|
|
1374
|
+
fields,
|
|
1375
|
+
expected: "at least one of the listed options",
|
|
1376
|
+
},
|
|
1377
|
+
);
|
|
1378
|
+
for (const [field, others] of Object.entries(spec.conflicts ?? {}))
|
|
1379
|
+
if (seen.has(field) && others.some((other) => seen.has(other)))
|
|
1380
|
+
throw new SmallPenError(
|
|
1381
|
+
"conflicting_read_target",
|
|
1382
|
+
"Choose one Token, Component, or Screen/Presentation target",
|
|
1383
|
+
{
|
|
1384
|
+
...facts(command, field, seen.get(field)[0]),
|
|
1385
|
+
fields: [field, ...others.filter((other) => seen.has(other))],
|
|
1386
|
+
},
|
|
1387
|
+
);
|
|
1388
|
+
for (const group of spec.exactlyOne ?? [])
|
|
1389
|
+
if (group.filter(hasValue).length !== 1)
|
|
1390
|
+
throw new SmallPenError(
|
|
1391
|
+
group.includes("theme")
|
|
1392
|
+
? "missing_theme_target"
|
|
1393
|
+
: group.includes("colorName")
|
|
1394
|
+
? "missing_asset_target"
|
|
1395
|
+
: "missing_token_reference",
|
|
1396
|
+
`Choose exactly one of ${group.map((field) => spec.parameters[field].option).join(" or ")}`,
|
|
1397
|
+
{
|
|
1398
|
+
...facts(command, group[0], seen.get(group[0])?.[0] ?? null),
|
|
1399
|
+
fields: group,
|
|
1400
|
+
expected: "exactly one target",
|
|
1401
|
+
nextOperations: [{ argv: spec.next, operation: "smallpen.schema" }],
|
|
1402
|
+
},
|
|
1403
|
+
);
|
|
1404
|
+
return args;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
export function numericDefault(command, option, full) {
|
|
1408
|
+
const spec = Object.values(commandContract(command).parameters).find(
|
|
1409
|
+
(parameter) => parameter.option === option,
|
|
1410
|
+
);
|
|
1411
|
+
return (
|
|
1412
|
+
(full ? spec.defaultWhenFull : undefined) ??
|
|
1413
|
+
spec.default ??
|
|
1414
|
+
Number.POSITIVE_INFINITY
|
|
1415
|
+
);
|
|
1416
|
+
}
|