@capaxle/adapter-cli 0.1.0-alpha.1

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.
@@ -0,0 +1,735 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { capabilitySemanticHash, jcs } from "@capaxle/ir";
3
+ import { projectStandaloneSchema } from "./export-schema.js";
4
+ export const OPENCLI_DIALECT = "bcdxn";
5
+ export const OPENCLI_VERSION = "1.0.0-alpha.13";
6
+ export const OPENCLI_SELECTOR = `opencli:${OPENCLI_DIALECT}@${OPENCLI_VERSION}`;
7
+ export const OPENCLI_EXTENSION_VERSION = "0.2";
8
+ export const OPENCLI_TARGET = `${OPENCLI_SELECTOR}+capaxle-cli@${OPENCLI_EXTENSION_VERSION}`;
9
+ export const RESERVED_CLI_OPTIONS = Object.freeze([
10
+ "--confirm",
11
+ "--correlation-id",
12
+ "--help",
13
+ "--idempotency-key",
14
+ "--input",
15
+ "--input-file",
16
+ "--json",
17
+ "--no-input",
18
+ "--timeout",
19
+ "--version",
20
+ ]);
21
+ export const OPENCLI_EXIT_CODES = Object.freeze([
22
+ { code: 0, status: "OK", summary: "Invocation succeeded." },
23
+ {
24
+ code: 2,
25
+ status: "BAD_USER_INPUT_ERROR",
26
+ summary: "CLI syntax or canonical input was invalid.",
27
+ },
28
+ {
29
+ code: 3,
30
+ status: "UNAUTHORIZED_ERROR",
31
+ summary: "Authentication or authorization denied the invocation.",
32
+ },
33
+ {
34
+ code: 4,
35
+ status: "INTERNAL_CLI_ERROR",
36
+ summary: "Confirmation or another precondition is required; inspect the canonical error.",
37
+ },
38
+ {
39
+ code: 5,
40
+ status: "INTERNAL_CLI_ERROR",
41
+ summary: "A declared domain failure occurred; inspect the canonical error.",
42
+ },
43
+ {
44
+ code: 6,
45
+ status: "INTERNAL_CLI_ERROR",
46
+ summary: "The invocation was limited, unavailable, or exceeded its deadline; inspect the canonical error.",
47
+ },
48
+ {
49
+ code: 7,
50
+ status: "INTERNAL_CLI_ERROR",
51
+ summary: "The framework or application failed internally.",
52
+ },
53
+ {
54
+ code: 130,
55
+ status: "CANCELED_ERROR",
56
+ summary: "The invocation was cancelled by an interrupt.",
57
+ },
58
+ ]);
59
+ const IR_HASH = /^sha256:[0-9a-f]{64}$/u;
60
+ const CLI_BINARY = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
61
+ const RESERVED = new Set(RESERVED_CLI_OPTIONS);
62
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
63
+ ? value
64
+ : null;
65
+ const clone = (value) => JSON.parse(JSON.stringify(value));
66
+ function define(target, key, value) {
67
+ Object.defineProperty(target, key, {
68
+ value,
69
+ enumerable: true,
70
+ configurable: true,
71
+ writable: true,
72
+ });
73
+ }
74
+ function compareCodePoint(left, right) {
75
+ const a = Array.from(left);
76
+ const b = Array.from(right);
77
+ const length = Math.min(a.length, b.length);
78
+ for (let index = 0; index < length; index += 1) {
79
+ const difference = a[index].codePointAt(0) - b[index].codePointAt(0);
80
+ if (difference !== 0)
81
+ return difference;
82
+ }
83
+ return a.length - b.length;
84
+ }
85
+ function sorted(value) {
86
+ if (Array.isArray(value))
87
+ return value.map(sorted);
88
+ const source = object(value);
89
+ if (!source)
90
+ return value;
91
+ const result = {};
92
+ for (const key of Object.keys(source).sort(compareCodePoint))
93
+ define(result, key, sorted(source[key]));
94
+ return result;
95
+ }
96
+ function diagnostic(code, input) {
97
+ return {
98
+ code,
99
+ severity: input.severity ?? "error",
100
+ message: input.message,
101
+ target: input.target,
102
+ ...(input.capabilityId === undefined
103
+ ? {}
104
+ : { capabilityId: input.capabilityId }),
105
+ ...(input.path === undefined ? {} : { path: input.path }),
106
+ ...(input.details === undefined ? {} : { details: input.details }),
107
+ };
108
+ }
109
+ function sortDiagnostics(diagnostics) {
110
+ const compare = (left, right) => compareCodePoint(left ?? "", right ?? "");
111
+ return [...diagnostics].sort((left, right) => compare(left.target, right.target) ||
112
+ compare(left.capabilityId, right.capabilityId) ||
113
+ compare(left.path, right.path) ||
114
+ compare(left.code, right.code) ||
115
+ compare(left.message, right.message));
116
+ }
117
+ function targetDiagnostic(target) {
118
+ if (target === OPENCLI_SELECTOR)
119
+ return null;
120
+ if (target.startsWith(`opencli:${OPENCLI_DIALECT}@`))
121
+ return diagnostic("CAP_OPENCLI_VERSION_UNSUPPORTED", {
122
+ target,
123
+ path: "/target",
124
+ message: `Unsupported bcdxn OpenCLI version in ${target}; expected ${OPENCLI_SELECTOR}.`,
125
+ });
126
+ return diagnostic("CAP_OPENCLI_DIALECT_UNSUPPORTED", {
127
+ target,
128
+ path: "/target",
129
+ message: `Unsupported OpenCLI dialect in ${target}.`,
130
+ });
131
+ }
132
+ function buildContextDiagnostics(document, target, buildContext) {
133
+ const diagnostics = [];
134
+ const suppliedHash = buildContext?.irHash;
135
+ if (suppliedHash === undefined)
136
+ diagnostics.push(diagnostic("CAP_BUILD_CONTEXT_INVALID", {
137
+ target,
138
+ path: "/buildContext/irHash",
139
+ details: { reason: "missing" },
140
+ message: "buildContext.irHash is required.",
141
+ }));
142
+ else if (!IR_HASH.test(suppliedHash))
143
+ diagnostics.push(diagnostic("CAP_BUILD_CONTEXT_INVALID", {
144
+ target,
145
+ path: "/buildContext/irHash",
146
+ details: { reason: "format" },
147
+ message: "buildContext.irHash must be sha256: followed by 64 lowercase hexadecimal characters.",
148
+ }));
149
+ else {
150
+ let computed = null;
151
+ try {
152
+ computed = capabilitySemanticHash(document);
153
+ }
154
+ catch {
155
+ // Invalid IR is rejected as an authority mismatch at this boundary.
156
+ }
157
+ if (computed !== suppliedHash)
158
+ diagnostics.push(diagnostic("CAP_BUILD_CONTEXT_INVALID", {
159
+ target,
160
+ path: "/buildContext/irHash",
161
+ details: { reason: "mismatch" },
162
+ message: "buildContext.irHash does not match the locally recomputed Capability IR semantic hash.",
163
+ }));
164
+ }
165
+ const binary = buildContext?.cliBinary;
166
+ if (binary === undefined)
167
+ diagnostics.push(diagnostic("CAP_BUILD_CONTEXT_INVALID", {
168
+ target,
169
+ path: "/buildContext/cliBinary",
170
+ details: { reason: "missing" },
171
+ message: "buildContext.cliBinary is required for OpenCLI export.",
172
+ }));
173
+ else if (!CLI_BINARY.test(binary))
174
+ diagnostics.push(diagnostic("CAP_BUILD_CONTEXT_INVALID", {
175
+ target,
176
+ path: "/buildContext/cliBinary",
177
+ details: { reason: "format" },
178
+ message: "buildContext.cliBinary must be one collision-safe CLI token.",
179
+ }));
180
+ return sortDiagnostics(diagnostics);
181
+ }
182
+ const schemaValue = (value) => {
183
+ const candidate = object(value);
184
+ return (object(candidate?.schema) ?? candidate);
185
+ };
186
+ function cliProjection(capability) {
187
+ return object(capability.interfaces.cli);
188
+ }
189
+ function resolveSchemaView(schema, schemas, ownerRoot = schema, seen = new Set()) {
190
+ if (!schema || seen.has(schema))
191
+ return schema;
192
+ const nextSeen = new Set(seen).add(schema);
193
+ const source = schema;
194
+ const reference = source.$ref;
195
+ if (typeof reference !== "string")
196
+ return schema;
197
+ const decode = (token) => token.replaceAll("~1", "/").replaceAll("~0", "~");
198
+ if (reference.startsWith("#/schemas/") &&
199
+ !reference.slice(10).includes("/")) {
200
+ const name = decode(reference.slice(10));
201
+ return Object.hasOwn(schemas, name)
202
+ ? resolveSchemaView(schemas[name], schemas, schemas[name], nextSeen)
203
+ : null;
204
+ }
205
+ if (reference.startsWith("#/$defs/") && !reference.slice(8).includes("/")) {
206
+ const name = decode(reference.slice(8));
207
+ const definitions = object(ownerRoot?.$defs);
208
+ const target = definitions?.[name];
209
+ return definitions && Object.hasOwn(definitions, name) && object(target)
210
+ ? resolveSchemaView(target, schemas, ownerRoot, nextSeen)
211
+ : null;
212
+ }
213
+ return null;
214
+ }
215
+ function inputPropertyView(capability, schemas) {
216
+ const schema = resolveSchemaView(schemaValue(capability.input), schemas);
217
+ const source = object(schema);
218
+ const properties = object(source?.properties);
219
+ if (source?.type !== "object" || !properties)
220
+ return null;
221
+ return {
222
+ properties,
223
+ required: new Set(Array.isArray(source.required)
224
+ ? source.required.filter((value) => typeof value === "string")
225
+ : []),
226
+ };
227
+ }
228
+ function primitiveKind(value) {
229
+ if (typeof value === "string")
230
+ return "string";
231
+ if (typeof value === "boolean")
232
+ return "boolean";
233
+ if (typeof value === "number")
234
+ return Number.isInteger(value) ? "integer" : "number";
235
+ return null;
236
+ }
237
+ function hasRichScalarBoundary(schema) {
238
+ return ["$ref", "oneOf", "anyOf", "allOf"].some((key) => Object.hasOwn(schema, key));
239
+ }
240
+ function scalarLeafProjection(schema) {
241
+ const source = object(schema);
242
+ if (!source)
243
+ return null;
244
+ if (hasRichScalarBoundary(source))
245
+ return null;
246
+ const explicit = source.type;
247
+ const explicitKind = explicit === "string" ||
248
+ explicit === "integer" ||
249
+ explicit === "number" ||
250
+ explicit === "boolean"
251
+ ? explicit
252
+ : null;
253
+ if (explicit !== undefined && !explicitKind)
254
+ return null;
255
+ let kind = explicitKind;
256
+ if (!kind && Object.hasOwn(source, "const"))
257
+ kind = primitiveKind(source.const);
258
+ if (!kind && Array.isArray(source.enum) && source.enum.length > 0) {
259
+ const kinds = source.enum.map(primitiveKind);
260
+ if (kinds.every((value) => value !== null)) {
261
+ const unique = new Set(kinds);
262
+ if (unique.size === 1)
263
+ kind = kinds[0];
264
+ else if ([...unique].every((value) => value === "integer" || value === "number"))
265
+ kind = "number";
266
+ }
267
+ }
268
+ if (!kind)
269
+ return null;
270
+ const result = { type: kind };
271
+ if (Array.isArray(source.enum))
272
+ result.choices = source.enum.map((value) => ({ value }));
273
+ if (Object.hasOwn(source, "default"))
274
+ result.default = source.default;
275
+ return result;
276
+ }
277
+ function scalarProjection(schema) {
278
+ const source = object(schema);
279
+ if (!source)
280
+ return null;
281
+ if (hasRichScalarBoundary(source))
282
+ return null;
283
+ const leaf = scalarLeafProjection(source);
284
+ if (leaf)
285
+ return leaf;
286
+ if (source.type === "array") {
287
+ const item = scalarLeafProjection(source.items);
288
+ if (item && !Object.hasOwn(item, "default"))
289
+ return { ...item, variadic: true };
290
+ }
291
+ return null;
292
+ }
293
+ function defaultTrueBoolean(schema) {
294
+ const projection = scalarLeafProjection(schema);
295
+ return projection?.type === "boolean" && projection.default === true;
296
+ }
297
+ function projectSchemas(capability, schemas) {
298
+ const prefix = `inline\u0000${capability.id}\u0000${capability.version}\u0000`;
299
+ const input = schemaValue(capability.input);
300
+ const output = schemaValue(capability.output);
301
+ if (!input || !output)
302
+ return null;
303
+ const inputSchema = projectStandaloneSchema(input, schemas, `${prefix}input`, {
304
+ requireObject: true,
305
+ });
306
+ const outputSchema = projectStandaloneSchema(output, schemas, `${prefix}output`);
307
+ if (!inputSchema || !outputSchema)
308
+ return null;
309
+ const errors = {};
310
+ for (const code of Object.keys(capability.errors).sort(compareCodePoint)) {
311
+ const error = capability.errors[code];
312
+ const projected = {
313
+ status: error.status,
314
+ message: error.message,
315
+ retryable: error.retryable,
316
+ };
317
+ const raw = error;
318
+ if (raw.docs !== undefined)
319
+ projected.docs = clone(raw.docs);
320
+ if (error.details !== undefined) {
321
+ const details = schemaValue(error.details);
322
+ if (!details)
323
+ return null;
324
+ const projectedDetails = projectStandaloneSchema(details, schemas, `${prefix}error:${code}`);
325
+ if (!projectedDetails)
326
+ return null;
327
+ projected.details = projectedDetails;
328
+ }
329
+ define(errors, code, projected);
330
+ }
331
+ return { inputSchema, outputSchema, errors: sorted(errors) };
332
+ }
333
+ function extensionFor(capability, irHash, projected) {
334
+ const raw = capability;
335
+ const projection = cliProjection(capability);
336
+ return sorted({
337
+ access: clone(capability.access),
338
+ bindings: clone((projection.bindings ?? {})),
339
+ canonical: { id: capability.id, version: capability.version },
340
+ encodings: {
341
+ canonicalInput: {
342
+ defaults: "kernel-after-adapter-coercion",
343
+ fileOption: "--input-file",
344
+ inlineOption: "--input",
345
+ merge: "none",
346
+ occurrence: "at-most-one-source",
347
+ scalarMixing: "forbidden",
348
+ stdinSentinel: "-",
349
+ value: "whole-canonical-json-object",
350
+ },
351
+ confirmationToken: "--confirm",
352
+ correlationId: "--correlation-id",
353
+ idempotencyKey: "--idempotency-key",
354
+ machine: ["--json", "--no-input"],
355
+ noInput: {
356
+ affectsBusinessInput: false,
357
+ option: "--no-input",
358
+ semantics: "disable-prompts",
359
+ },
360
+ scalarBindings: {
361
+ assembly: "top-level-object-properties",
362
+ omission: "missing",
363
+ source: "bindings",
364
+ },
365
+ timeout: "--timeout",
366
+ },
367
+ envelope: {
368
+ error: { discriminator: { ok: false }, required: ["error", "ok"] },
369
+ success: {
370
+ discriminator: { ok: true },
371
+ required: ["correlationId", "ok", "value"],
372
+ },
373
+ },
374
+ errors: projected.errors,
375
+ execution: clone(capability.execution),
376
+ effects: clone(capability.effects),
377
+ inputSchema: projected.inputSchema,
378
+ irHash,
379
+ lifecycle: clone(raw.lifecycle ?? {}),
380
+ outputSchema: projected.outputSchema,
381
+ });
382
+ }
383
+ function lossSet(capability, schemas) {
384
+ const loss = new Set([
385
+ "effects",
386
+ "error_schemas",
387
+ "idempotency",
388
+ "output_schema",
389
+ "permissions",
390
+ ]);
391
+ if (capability.effects.confirmation === "required")
392
+ loss.add("confirmation");
393
+ const view = inputPropertyView(capability, schemas);
394
+ if (!view)
395
+ loss.add("nested_input");
396
+ else
397
+ for (const schema of Object.values(view.properties)) {
398
+ if (!scalarProjection(schema))
399
+ loss.add("nested_input");
400
+ if (defaultTrueBoolean(schema))
401
+ loss.add("boolean_negation");
402
+ }
403
+ return [...loss].sort(compareCodePoint);
404
+ }
405
+ function baseCommand(capability, schemas) {
406
+ const command = { kind: "action", summary: capability.summary };
407
+ const view = inputPropertyView(capability, schemas);
408
+ const projection = cliProjection(capability);
409
+ const bindings = object(projection.bindings) ?? {};
410
+ const args = [];
411
+ const flags = [];
412
+ if (view)
413
+ for (const property of Object.keys(bindings).sort(compareCodePoint)) {
414
+ const binding = object(bindings[property]);
415
+ const scalar = scalarProjection(view.properties[property]);
416
+ if (!binding || !scalar)
417
+ continue;
418
+ const item = {
419
+ name: binding.kind === "option" && typeof binding.name === "string"
420
+ ? binding.name.replace(/^--/u, "")
421
+ : property,
422
+ required: view.required.has(property),
423
+ ...scalar,
424
+ };
425
+ if (binding.kind === "positional" && typeof binding.index === "number")
426
+ args.push({ ...item, index: binding.index });
427
+ else if (binding.kind === "option")
428
+ flags.push(item);
429
+ }
430
+ if (args.length > 0)
431
+ command.args = args
432
+ .sort((left, right) => left.index - right.index)
433
+ .map(({ index, ...item }) => {
434
+ void index;
435
+ return item;
436
+ });
437
+ if (flags.length > 0)
438
+ command.flags = flags;
439
+ return command;
440
+ }
441
+ function extensionCollisionDiagnostics(rootExtensions, target) {
442
+ const diagnostics = [];
443
+ const carriers = [];
444
+ const root = object(rootExtensions);
445
+ if (root)
446
+ carriers.push(["", root]);
447
+ const commands = object(root?.commands);
448
+ if (commands)
449
+ for (const command of Object.keys(commands).sort(compareCodePoint)) {
450
+ const carrier = object(commands[command]);
451
+ if (carrier)
452
+ carriers.push([
453
+ `/commands/${command.replaceAll("~", "~0").replaceAll("/", "~1")}`,
454
+ carrier,
455
+ ]);
456
+ }
457
+ for (const [path, carrier] of carriers)
458
+ for (const key of ["x-capabuild", "x-capaxle"])
459
+ if (Object.hasOwn(carrier, key))
460
+ diagnostics.push(diagnostic("CAP_OPENCLI_EXTENSION_COLLISION", {
461
+ target,
462
+ path: `${path}/${key}`,
463
+ message: "Caller metadata cannot supply current or legacy framework OpenCLI extensions.",
464
+ }));
465
+ return sortDiagnostics(diagnostics);
466
+ }
467
+ export function exportOpenCli(document, options = {}) {
468
+ const target = options.target ?? OPENCLI_SELECTOR;
469
+ const targetFailure = targetDiagnostic(target);
470
+ if (targetFailure)
471
+ return { artifact: null, diagnostics: [targetFailure] };
472
+ if ((options.extensionVersion ?? OPENCLI_EXTENSION_VERSION) !==
473
+ OPENCLI_EXTENSION_VERSION)
474
+ return {
475
+ artifact: null,
476
+ diagnostics: [
477
+ diagnostic("CAP_OPENCLI_VERSION_UNSUPPORTED", {
478
+ target,
479
+ path: "/x-capaxle/contractVersion",
480
+ message: `Unsupported Capaxle OpenCLI extension version ${String(options.extensionVersion)}.`,
481
+ }),
482
+ ],
483
+ };
484
+ const rootExtensions = options.rootExtensions ?? {};
485
+ const collisions = extensionCollisionDiagnostics(rootExtensions, target);
486
+ if (collisions.length > 0)
487
+ return { artifact: null, diagnostics: collisions };
488
+ const contextDiagnostics = buildContextDiagnostics(document, target, options.buildContext);
489
+ if (contextDiagnostics.length > 0)
490
+ return { artifact: null, diagnostics: contextDiagnostics };
491
+ const capabilities = [...document.capabilities]
492
+ .filter((capability) => cliProjection(capability)?.enabled === true)
493
+ .sort((left, right) => compareCodePoint(left.id, right.id));
494
+ const diagnostics = [];
495
+ for (const capability of capabilities) {
496
+ const bindings = object(cliProjection(capability)?.bindings) ?? {};
497
+ const view = inputPropertyView(capability, document.schemas);
498
+ const optionOwners = new Map();
499
+ for (const property of Object.keys(bindings).sort(compareCodePoint)) {
500
+ const binding = object(bindings[property]);
501
+ if (binding?.kind === "option" && typeof binding.name === "string")
502
+ optionOwners.set(binding.name, property);
503
+ }
504
+ for (const property of Object.keys(bindings).sort(compareCodePoint)) {
505
+ const binding = object(bindings[property]);
506
+ if (binding?.kind === "option" &&
507
+ typeof binding.name === "string" &&
508
+ RESERVED.has(binding.name))
509
+ diagnostics.push(diagnostic("CAP_OPENCLI_BINDING_UNREPRESENTABLE", {
510
+ target,
511
+ capabilityId: capability.id,
512
+ path: `/interfaces/cli/bindings/${property.replaceAll("~", "~0").replaceAll("/", "~1")}`,
513
+ details: { option: binding.name },
514
+ message: `${binding.name} is reserved by the Capaxle CLI host.`,
515
+ }));
516
+ if (binding?.kind === "option" &&
517
+ typeof binding.name === "string" &&
518
+ defaultTrueBoolean(view?.properties[property])) {
519
+ const negative = `--no-${binding.name.slice(2)}`;
520
+ if (RESERVED.has(negative) || optionOwners.has(negative))
521
+ diagnostics.push(diagnostic("CAP_OPENCLI_BINDING_UNREPRESENTABLE", {
522
+ target,
523
+ capabilityId: capability.id,
524
+ path: `/interfaces/cli/bindings/${property.replaceAll("~", "~0").replaceAll("/", "~1")}`,
525
+ details: {
526
+ option: negative,
527
+ reason: "derived-option-collision",
528
+ },
529
+ message: `${negative} collides with a bound or reserved CLI option.`,
530
+ }));
531
+ }
532
+ }
533
+ }
534
+ if (diagnostics.length > 0)
535
+ return { artifact: null, diagnostics: sortDiagnostics(diagnostics) };
536
+ const projectedSchemas = new Map();
537
+ for (const capability of capabilities) {
538
+ const projected = projectSchemas(capability, document.schemas);
539
+ if (!projected)
540
+ diagnostics.push(diagnostic("CAP_OPENCLI_SCHEMA_UNREPRESENTABLE", {
541
+ target,
542
+ capabilityId: capability.id,
543
+ path: "/input",
544
+ message: "The selected OpenCLI extension could not emit a self-contained capability schema graph.",
545
+ }));
546
+ else
547
+ projectedSchemas.set(capability.id, projected);
548
+ }
549
+ if (diagnostics.length > 0)
550
+ return { artifact: null, diagnostics: sortDiagnostics(diagnostics) };
551
+ const binary = options.buildContext.cliBinary;
552
+ const irHash = options.buildContext.irHash;
553
+ const commands = {};
554
+ const groups = new Set();
555
+ for (const capability of capabilities) {
556
+ const command = cliProjection(capability).command;
557
+ const tokens = command.filter((value) => typeof value === "string");
558
+ for (let length = 1; length < tokens.length; length += 1)
559
+ groups.add(tokens.slice(0, length).join(" "));
560
+ }
561
+ for (const group of [...groups].sort(compareCodePoint))
562
+ define(commands, `${binary} ${group} {command} [flags]`, { kind: "group" });
563
+ const extensions = options.extensions ?? true;
564
+ for (const capability of capabilities) {
565
+ const projection = cliProjection(capability);
566
+ const tokens = projection.command.filter((value) => typeof value === "string");
567
+ const key = `${binary} ${tokens.join(" ")} [flags]`;
568
+ diagnostics.push(diagnostic("CAP_OPENCLI_LOSSY_PROJECTION", {
569
+ target,
570
+ capabilityId: capability.id,
571
+ path: `/commands/${key}`,
572
+ severity: extensions ? "warning" : "error",
573
+ details: {
574
+ unrepresented: lossSet(capability, document.schemas),
575
+ preservationPath: `commands[${JSON.stringify(key)}]["x-capaxle"]`,
576
+ },
577
+ message: extensions
578
+ ? "The base OpenCLI dialect is lossy; x-capaxle preserves canonical semantics."
579
+ : "The base OpenCLI dialect is lossy and extensions are disabled.",
580
+ }));
581
+ const command = baseCommand(capability, document.schemas);
582
+ if (extensions)
583
+ command["x-capaxle"] = extensionFor(capability, irHash, projectedSchemas.get(capability.id));
584
+ define(commands, key, command);
585
+ }
586
+ if (!extensions)
587
+ return { artifact: null, diagnostics: sortDiagnostics(diagnostics) };
588
+ const service = object(document.service) ?? {};
589
+ const irVersion = document.irVersion;
590
+ const artifact = sorted({
591
+ ...clone(rootExtensions),
592
+ commands,
593
+ global: {
594
+ exitCodes: OPENCLI_EXIT_CODES,
595
+ flags: RESERVED_CLI_OPTIONS.filter((name) => name !== "--help" && name !== "--version").map((name) => ({
596
+ name: name.slice(2),
597
+ type: name === "--json" || name === "--no-input" ? "boolean" : "string",
598
+ })),
599
+ },
600
+ info: {
601
+ binary,
602
+ ...(service.title !== undefined || service.name !== undefined
603
+ ? { title: service.title ?? service.name }
604
+ : {}),
605
+ ...(service.version === undefined ? {} : { version: service.version }),
606
+ },
607
+ opencliVersion: OPENCLI_VERSION,
608
+ "x-capaxle": {
609
+ contractVersion: OPENCLI_EXTENSION_VERSION,
610
+ exporterTarget: OPENCLI_TARGET,
611
+ irHash,
612
+ ...(irVersion === undefined ? {} : { irVersion }),
613
+ machineMode: {
614
+ jsonFlag: "--json",
615
+ noInputFlag: "--no-input",
616
+ stderr: "diagnostics",
617
+ stdout: "single-envelope",
618
+ },
619
+ semanticsAuthority: "capability-ir",
620
+ },
621
+ });
622
+ return { artifact, diagnostics: sortDiagnostics(diagnostics) };
623
+ }
624
+ function frozen(value) {
625
+ if (value !== null && typeof value === "object") {
626
+ for (const item of Object.values(value))
627
+ frozen(item);
628
+ Object.freeze(value);
629
+ }
630
+ return value;
631
+ }
632
+ function producerDiagnostics(diagnostics) {
633
+ return sortDiagnostics(diagnostics.map((item) => ({
634
+ ...item,
635
+ target: OPENCLI_TARGET,
636
+ ...(item.code === "CAP_OPENCLI_LOSSY_PROJECTION" && item.capabilityId
637
+ ? { path: "/interfaces/cli" }
638
+ : {}),
639
+ })));
640
+ }
641
+ /** Structural compiler plugin; the adapter never imports the compiler. */
642
+ export function createOpenCliArtifactProducer(options = {}) {
643
+ const snapshot = frozen(clone(Object.fromEntries(Object.entries(options).filter((entry) => entry[1] !== undefined))));
644
+ return frozen({
645
+ id: "capaxle.opencli",
646
+ version: "0.2.0",
647
+ staticInputs: snapshot,
648
+ diagnosticCodes: [
649
+ {
650
+ code: "CAP_BUILD_CONTEXT_INVALID",
651
+ severities: ["error"],
652
+ },
653
+ {
654
+ code: "CAP_OPENCLI_BINDING_UNREPRESENTABLE",
655
+ severities: ["error"],
656
+ },
657
+ {
658
+ code: "CAP_OPENCLI_DIALECT_UNSUPPORTED",
659
+ severities: ["error"],
660
+ },
661
+ {
662
+ code: "CAP_OPENCLI_EXTENSION_COLLISION",
663
+ severities: ["error"],
664
+ },
665
+ {
666
+ code: "CAP_OPENCLI_LOSSY_PROJECTION",
667
+ severities: ["warning", "error"],
668
+ },
669
+ {
670
+ code: "CAP_OPENCLI_SCHEMA_UNREPRESENTABLE",
671
+ severities: ["error"],
672
+ },
673
+ {
674
+ code: "CAP_OPENCLI_VERSION_UNSUPPORTED",
675
+ severities: ["error"],
676
+ },
677
+ ],
678
+ artifacts: [
679
+ {
680
+ id: "opencli",
681
+ path: "opencli.json",
682
+ mediaType: "application/json",
683
+ target: OPENCLI_TARGET,
684
+ dependencies: ["document:capability-ir"],
685
+ produce(context) {
686
+ const bytes = context.dependencyBytes.get("document:capability-ir");
687
+ if (!bytes)
688
+ return {
689
+ ok: false,
690
+ diagnostics: [
691
+ diagnostic("CAP_OPENCLI_SCHEMA_UNREPRESENTABLE", {
692
+ target: OPENCLI_TARGET,
693
+ path: "/document",
694
+ message: "Capability IR artifact dependency is missing.",
695
+ }),
696
+ ],
697
+ };
698
+ let document;
699
+ try {
700
+ document = JSON.parse(Buffer.from(bytes).toString("utf8"));
701
+ }
702
+ catch {
703
+ return {
704
+ ok: false,
705
+ diagnostics: [
706
+ diagnostic("CAP_OPENCLI_SCHEMA_UNREPRESENTABLE", {
707
+ target: OPENCLI_TARGET,
708
+ path: "/document",
709
+ message: "Capability IR artifact dependency is invalid JSON.",
710
+ }),
711
+ ],
712
+ };
713
+ }
714
+ const result = exportOpenCli(document, {
715
+ ...snapshot,
716
+ ...(context.buildContext === undefined
717
+ ? {}
718
+ : { buildContext: context.buildContext }),
719
+ });
720
+ if (!result.artifact)
721
+ return {
722
+ ok: false,
723
+ diagnostics: producerDiagnostics(result.diagnostics),
724
+ };
725
+ return {
726
+ ok: true,
727
+ bytes: Buffer.from(`${jcs(result.artifact)}\n`),
728
+ diagnostics: producerDiagnostics(result.diagnostics),
729
+ };
730
+ },
731
+ },
732
+ ],
733
+ });
734
+ }
735
+ //# sourceMappingURL=opencli.js.map