@seamward/cli 0.1.0-alpha.10

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.
Files changed (46) hide show
  1. package/LICENSE +31 -0
  2. package/README.md +428 -0
  3. package/dist/cli.d.ts +11 -0
  4. package/dist/cli.js +659 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/connection-key.d.ts +5 -0
  7. package/dist/connection-key.js +6 -0
  8. package/dist/connection-key.js.map +1 -0
  9. package/dist/contract-sync.d.ts +134 -0
  10. package/dist/contract-sync.js +603 -0
  11. package/dist/contract-sync.js.map +1 -0
  12. package/dist/discovery.d.ts +47 -0
  13. package/dist/discovery.js +496 -0
  14. package/dist/discovery.js.map +1 -0
  15. package/dist/existing-webhook-binding.d.ts +9 -0
  16. package/dist/existing-webhook-binding.js +196 -0
  17. package/dist/existing-webhook-binding.js.map +1 -0
  18. package/dist/index.d.ts +9 -0
  19. package/dist/index.js +10 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/observation-sync.d.ts +28 -0
  22. package/dist/observation-sync.js +101 -0
  23. package/dist/observation-sync.js.map +1 -0
  24. package/dist/project-metadata.d.ts +7 -0
  25. package/dist/project-metadata.js +104 -0
  26. package/dist/project-metadata.js.map +1 -0
  27. package/dist/remote-api.d.ts +10 -0
  28. package/dist/remote-api.js +61 -0
  29. package/dist/remote-api.js.map +1 -0
  30. package/dist/setup-engine.d.ts +127 -0
  31. package/dist/setup-engine.js +765 -0
  32. package/dist/setup-engine.js.map +1 -0
  33. package/dist/setup-plan.d.ts +197 -0
  34. package/dist/setup-plan.js +1240 -0
  35. package/dist/setup-plan.js.map +1 -0
  36. package/dist/source-instrumentation.d.ts +10 -0
  37. package/dist/source-instrumentation.js +291 -0
  38. package/dist/source-instrumentation.js.map +1 -0
  39. package/dist/version.d.ts +1 -0
  40. package/dist/version.js +8 -0
  41. package/dist/version.js.map +1 -0
  42. package/examples/node-service/README.md +42 -0
  43. package/examples/node-service/contracts/orders.openapi.yaml +18 -0
  44. package/examples/node-service/package.json +11 -0
  45. package/examples/node-service/src/server.ts +13 -0
  46. package/package.json +58 -0
package/dist/cli.js ADDED
@@ -0,0 +1,659 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { pathToFileURL } from "node:url";
6
+ import { applySetupPlan, completedSetupActionIds, createSetupPlan, inspectSetupActionEvidence, operatorReviewableSetupActions, recordSourceBackedSetupEvidence, reviewSetupActions, } from "./setup-plan.js";
7
+ import { scanProject } from "./discovery.js";
8
+ import { activatePlanContractVersion, previewPlanContract, previewPlanContractActivation, readContractStatus, registerPlanContracts, } from "./contract-sync.js";
9
+ import { checkFirstSetupObservation } from "./observation-sync.js";
10
+ import { cliVersion } from "./version.js";
11
+ import { readProjectMetadata } from "./project-metadata.js";
12
+ import { readSetupPlan, verifySetup } from "./setup-engine.js";
13
+ async function processPrompt(question) {
14
+ const readline = createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ });
18
+ try {
19
+ return await readline.question(question);
20
+ }
21
+ finally {
22
+ readline.close();
23
+ }
24
+ }
25
+ const processIo = {
26
+ cwd: () => process.cwd(),
27
+ stdout: (value) => process.stdout.write(value),
28
+ stderr: (value) => process.stderr.write(value),
29
+ env: (name) => process.env[name],
30
+ fetchFn: fetch,
31
+ isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
32
+ prompt: processPrompt,
33
+ };
34
+ const usage = `Seamward setup CLI
35
+
36
+ Usage:
37
+ seamward discover [directory]
38
+ seamward setup [directory] [--direction <inbound|outbound>] [--protocol <http-api|http-webhook|queue|scheduled-feed>] [--contract <path>] [--write] [--force]
39
+ seamward review [directory] [--all | --action <id> [--action <id>]] [--source-backed] [--evidence <note>] [--write]
40
+ seamward verify [directory]
41
+ seamward observations check [directory] [--endpoint <url>] [--write]
42
+ seamward contracts register [directory] --version <version> [--file <path>] [--binding <path>] [--include-examples] [--write]
43
+ seamward contracts status [directory] [--endpoint <url>]
44
+ seamward contracts activate [directory] --contract <id> --expected <id|none> [--reason <promote|rollback>] [--write]
45
+ seamward --version
46
+
47
+ Commands are read-only unless --write is supplied. Interactive review records
48
+ selected actions only after confirmation. Source code and secret values are
49
+ never included in the generated setup plan.
50
+ `;
51
+ function parseArguments(arguments_) {
52
+ const [rawCommand = "help", ...remaining] = arguments_;
53
+ if (rawCommand === "help" || rawCommand === "--help" || rawCommand === "-h") {
54
+ return {
55
+ command: "help",
56
+ directory: ".",
57
+ write: false,
58
+ force: false,
59
+ files: [],
60
+ setupContracts: [],
61
+ actions: [],
62
+ all: false,
63
+ sourceBacked: false,
64
+ expectedProvided: false,
65
+ includeExamples: false,
66
+ };
67
+ }
68
+ if (rawCommand === "--version" ||
69
+ rawCommand === "-v" ||
70
+ rawCommand === "version") {
71
+ return {
72
+ command: "version",
73
+ directory: ".",
74
+ write: false,
75
+ force: false,
76
+ files: [],
77
+ setupContracts: [],
78
+ actions: [],
79
+ all: false,
80
+ sourceBacked: false,
81
+ expectedProvided: false,
82
+ includeExamples: false,
83
+ };
84
+ }
85
+ let command = rawCommand;
86
+ if (rawCommand === "contracts" && remaining[0] === "register") {
87
+ command = "contracts-register";
88
+ remaining.shift();
89
+ }
90
+ else if (rawCommand === "contracts" && remaining[0] === "status") {
91
+ command = "contracts-status";
92
+ remaining.shift();
93
+ }
94
+ else if (rawCommand === "contracts" && remaining[0] === "activate") {
95
+ command = "contracts-activate";
96
+ remaining.shift();
97
+ }
98
+ else if (rawCommand === "observations" && remaining[0] === "check") {
99
+ command = "observations-check";
100
+ remaining.shift();
101
+ }
102
+ if (command !== "discover" &&
103
+ command !== "setup" &&
104
+ command !== "review" &&
105
+ command !== "verify" &&
106
+ command !== "observations-check" &&
107
+ command !== "contracts-register" &&
108
+ command !== "contracts-status" &&
109
+ command !== "contracts-activate") {
110
+ throw new Error(`unknown command: ${rawCommand}`);
111
+ }
112
+ let directory = ".";
113
+ let directorySeen = false;
114
+ let write = false;
115
+ let force = false;
116
+ let version;
117
+ let endpoint;
118
+ let contractVersionId;
119
+ let expectedActiveContractVersionId;
120
+ let expectedProvided = false;
121
+ let reason;
122
+ let binding;
123
+ let includeExamples = false;
124
+ let evidence;
125
+ let direction;
126
+ let protocol;
127
+ const files = [];
128
+ const setupContracts = [];
129
+ const actions = [];
130
+ let all = false;
131
+ let sourceBacked = false;
132
+ for (let index = 0; index < remaining.length; index += 1) {
133
+ const argument = remaining[index] ?? "";
134
+ if (argument === "--write" &&
135
+ (command === "setup" ||
136
+ command === "review" ||
137
+ command === "observations-check" ||
138
+ command === "contracts-register" ||
139
+ command === "contracts-activate")) {
140
+ write = true;
141
+ continue;
142
+ }
143
+ if (argument === "--action" && command === "review") {
144
+ const action = remaining[index + 1];
145
+ if (!action || action.startsWith("-")) {
146
+ throw new Error("--action requires a value");
147
+ }
148
+ actions.push(action);
149
+ index += 1;
150
+ continue;
151
+ }
152
+ if (argument === "--all" && command === "review") {
153
+ all = true;
154
+ continue;
155
+ }
156
+ if (argument === "--source-backed" && command === "review") {
157
+ sourceBacked = true;
158
+ continue;
159
+ }
160
+ if (argument === "--evidence" && command === "review") {
161
+ evidence = remaining[index + 1];
162
+ if (!evidence || evidence.startsWith("-")) {
163
+ throw new Error("--evidence requires a value");
164
+ }
165
+ index += 1;
166
+ continue;
167
+ }
168
+ if (argument === "--force" && command === "setup") {
169
+ force = true;
170
+ continue;
171
+ }
172
+ if (argument === "--direction" && command === "setup") {
173
+ const value = remaining[index + 1];
174
+ if (value !== "inbound" && value !== "outbound") {
175
+ throw new Error("--direction must be inbound or outbound");
176
+ }
177
+ direction = value;
178
+ index += 1;
179
+ continue;
180
+ }
181
+ if (argument === "--protocol" && command === "setup") {
182
+ const value = remaining[index + 1];
183
+ if (value !== "http-api" &&
184
+ value !== "http-webhook" &&
185
+ value !== "queue" &&
186
+ value !== "scheduled-feed") {
187
+ throw new Error("--protocol must be http-api, http-webhook, queue, or scheduled-feed");
188
+ }
189
+ protocol = value;
190
+ index += 1;
191
+ continue;
192
+ }
193
+ if (argument === "--contract" && command === "setup") {
194
+ const file = remaining[index + 1];
195
+ if (!file || file.startsWith("-")) {
196
+ throw new Error("--contract requires a value");
197
+ }
198
+ setupContracts.push(file);
199
+ index += 1;
200
+ continue;
201
+ }
202
+ if (argument === "--version" && command === "contracts-register") {
203
+ version = remaining[index + 1];
204
+ if (!version || version.startsWith("-")) {
205
+ throw new Error("--version requires a value");
206
+ }
207
+ index += 1;
208
+ continue;
209
+ }
210
+ if (argument === "--file" && command === "contracts-register") {
211
+ const file = remaining[index + 1];
212
+ if (!file || file.startsWith("-")) {
213
+ throw new Error("--file requires a value");
214
+ }
215
+ files.push(file);
216
+ index += 1;
217
+ continue;
218
+ }
219
+ if (argument === "--binding" && command === "contracts-register") {
220
+ binding = remaining[index + 1];
221
+ if (!binding || binding.startsWith("-")) {
222
+ throw new Error("--binding requires a value");
223
+ }
224
+ index += 1;
225
+ continue;
226
+ }
227
+ if (argument === "--include-examples" && command === "contracts-register") {
228
+ includeExamples = true;
229
+ continue;
230
+ }
231
+ if (argument === "--endpoint" &&
232
+ (command === "contracts-register" ||
233
+ command === "contracts-status" ||
234
+ command === "contracts-activate" ||
235
+ command === "observations-check")) {
236
+ endpoint = remaining[index + 1];
237
+ if (!endpoint || endpoint.startsWith("-")) {
238
+ throw new Error("--endpoint requires a value");
239
+ }
240
+ index += 1;
241
+ continue;
242
+ }
243
+ if (argument === "--contract" && command === "contracts-activate") {
244
+ contractVersionId = remaining[index + 1];
245
+ if (!contractVersionId || contractVersionId.startsWith("-")) {
246
+ throw new Error("--contract requires a value");
247
+ }
248
+ index += 1;
249
+ continue;
250
+ }
251
+ if (argument === "--expected" && command === "contracts-activate") {
252
+ const expected = remaining[index + 1];
253
+ if (!expected || expected.startsWith("-")) {
254
+ throw new Error("--expected requires a contract ID or none");
255
+ }
256
+ expectedProvided = true;
257
+ expectedActiveContractVersionId = expected === "none" ? null : expected;
258
+ index += 1;
259
+ continue;
260
+ }
261
+ if (argument === "--reason" && command === "contracts-activate") {
262
+ const value = remaining[index + 1];
263
+ if (value !== "promote" && value !== "rollback") {
264
+ throw new Error("--reason must be promote or rollback");
265
+ }
266
+ reason = value;
267
+ index += 1;
268
+ continue;
269
+ }
270
+ if (argument.startsWith("-")) {
271
+ throw new Error(`unknown option for ${command}: ${argument}`);
272
+ }
273
+ if (directorySeen)
274
+ throw new Error(`unexpected argument: ${argument}`);
275
+ directory = argument;
276
+ directorySeen = true;
277
+ }
278
+ if (force && !write)
279
+ throw new Error("--force requires --write");
280
+ if (command === "setup" && Boolean(direction) !== Boolean(protocol)) {
281
+ throw new Error("setup requires --direction and --protocol together");
282
+ }
283
+ if (command === "review" && all && actions.length > 0) {
284
+ throw new Error("review accepts either --all or --action, not both");
285
+ }
286
+ if (command === "contracts-register" && !version) {
287
+ throw new Error("contracts register requires --version");
288
+ }
289
+ if (command === "contracts-register" && files.length > 1) {
290
+ throw new Error("contracts register accepts exactly one --file");
291
+ }
292
+ if (command === "contracts-activate" && !contractVersionId) {
293
+ throw new Error("contracts activate requires --contract");
294
+ }
295
+ if (command === "contracts-activate" && !expectedProvided) {
296
+ throw new Error("contracts activate requires --expected");
297
+ }
298
+ return {
299
+ command,
300
+ directory,
301
+ write,
302
+ force,
303
+ version,
304
+ files,
305
+ setupContracts,
306
+ actions,
307
+ all,
308
+ sourceBacked,
309
+ ...(evidence ? { evidence } : {}),
310
+ endpoint,
311
+ contractVersionId,
312
+ expectedActiveContractVersionId,
313
+ expectedProvided,
314
+ reason,
315
+ direction,
316
+ protocol,
317
+ binding,
318
+ includeExamples,
319
+ };
320
+ }
321
+ function safeTerminalText(value) {
322
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
323
+ }
324
+ function reviewActionLabel(action) {
325
+ const direction = action.direction ?? "unknown";
326
+ const operation = (() => {
327
+ switch (action.operationKind) {
328
+ case "http_client":
329
+ return "HTTP client";
330
+ case "http_server":
331
+ return "HTTP handler";
332
+ case "webhook_handler":
333
+ return "webhook handler";
334
+ case "queue_publish":
335
+ return "queue publisher";
336
+ case "queue_consume":
337
+ return "queue consumer";
338
+ case "scheduled_feed":
339
+ return "scheduled feed";
340
+ default:
341
+ return "integration operation";
342
+ }
343
+ })();
344
+ const location = action.file
345
+ ? `${action.file}${action.line ? `:${action.line}` : ""}`
346
+ : "the discovered integration boundary";
347
+ return safeTerminalText(`Instrument ${direction} ${operation} in ${location}`);
348
+ }
349
+ function parseInteractiveSelection(value, actionCount) {
350
+ const normalized = value.trim().toLowerCase();
351
+ if (!normalized || normalized === "cancel" || normalized === "q") {
352
+ return "cancel";
353
+ }
354
+ if (normalized === "all" || normalized === "a") {
355
+ return Array.from({ length: actionCount }, (_, index) => index);
356
+ }
357
+ const entries = normalized.split(/[\s,]+/).filter(Boolean);
358
+ if (entries.length === 0)
359
+ return null;
360
+ const indexes = entries.map((entry) => Number(entry) - 1);
361
+ if (indexes.some((index) => !Number.isInteger(index) || index < 0 || index >= actionCount)) {
362
+ return null;
363
+ }
364
+ return [...new Set(indexes)];
365
+ }
366
+ async function runInteractiveReview(plan, root, io, evidence, sourceBacked) {
367
+ if (!io.isInteractive?.() || !io.prompt) {
368
+ throw new Error("Interactive review requires a terminal. Use --all or --action for automation");
369
+ }
370
+ const completed = sourceBacked
371
+ ? new Set((await inspectSetupActionEvidence(plan, root))
372
+ .filter(({ status }) => status === "source_verified")
373
+ .map(({ actionId }) => actionId))
374
+ : await completedSetupActionIds(plan, root);
375
+ const pending = operatorReviewableSetupActions(plan).filter(({ id }) => !completed.has(id));
376
+ if (pending.length === 0) {
377
+ io.stdout("No pending instrumentation actions require review.\n");
378
+ return 0;
379
+ }
380
+ io.stdout(`Seamward found ${pending.length} pending instrumentation ${pending.length === 1 ? "change" : "changes"}:\n\n`);
381
+ for (const [index, action] of pending.entries()) {
382
+ io.stdout(` ${index + 1}. ${reviewActionLabel(action)}\n`);
383
+ }
384
+ io.stdout("\nReview records that an engineer implemented and reviewed the selected changes. It does not modify application source.\n\n");
385
+ let selectedIndexes = null;
386
+ while (selectedIndexes === null) {
387
+ selectedIndexes = parseInteractiveSelection(await io.prompt("Select actions (all, comma-separated numbers, or cancel): "), pending.length);
388
+ if (selectedIndexes === null) {
389
+ io.stderr("Select all, one or more listed numbers, or cancel.\n");
390
+ }
391
+ }
392
+ if (selectedIndexes === "cancel") {
393
+ io.stdout("Review cancelled. No review was recorded.\n");
394
+ return 0;
395
+ }
396
+ const selected = selectedIndexes.map((index) => pending[index]);
397
+ const reviewEvidence = evidence ?? (await io.prompt("Review note (optional): "));
398
+ const confirmation = (await io.prompt(`Mark ${selected.length} ${selected.length === 1 ? "action" : "actions"} as implemented and reviewed? (y/N): `))
399
+ .trim()
400
+ .toLowerCase();
401
+ if (confirmation !== "y" && confirmation !== "yes") {
402
+ io.stdout("Review cancelled. No review was recorded.\n");
403
+ return 0;
404
+ }
405
+ const recordReview = sourceBacked
406
+ ? recordSourceBackedSetupEvidence
407
+ : reviewSetupActions;
408
+ await recordReview(plan, root, selected.map(({ id }) => id), {
409
+ write: true,
410
+ ...(reviewEvidence.trim() ? { evidence: reviewEvidence } : {}),
411
+ });
412
+ io.stdout(`Review recorded for ${selected.length} ${selected.length === 1 ? "action" : "actions"}.\n`);
413
+ return 0;
414
+ }
415
+ async function readJsonSchemaBinding(root, relativeFile) {
416
+ if (!relativeFile)
417
+ return undefined;
418
+ const realRoot = await realpath(root);
419
+ const resolved = await realpath(path.resolve(realRoot, relativeFile));
420
+ const relative = path.relative(realRoot, resolved);
421
+ if (relative === ".." ||
422
+ relative.startsWith(`..${path.sep}`) ||
423
+ path.isAbsolute(relative)) {
424
+ throw new Error("JSON Schema binding path escapes the project boundary");
425
+ }
426
+ try {
427
+ return JSON.parse(await readFile(resolved, "utf8"));
428
+ }
429
+ catch {
430
+ throw new Error("JSON Schema binding must be a valid JSON file");
431
+ }
432
+ }
433
+ export async function runCli(arguments_, io = processIo) {
434
+ let parsed;
435
+ try {
436
+ parsed = parseArguments(arguments_);
437
+ }
438
+ catch (error) {
439
+ io.stderr(`${error.message}\n\n${usage}`);
440
+ return 2;
441
+ }
442
+ if (parsed.command === "help") {
443
+ io.stdout(usage);
444
+ return 0;
445
+ }
446
+ if (parsed.command === "version") {
447
+ io.stdout(`${cliVersion}\n`);
448
+ return 0;
449
+ }
450
+ try {
451
+ const root = path.resolve(io.cwd(), parsed.directory);
452
+ if (parsed.command === "verify") {
453
+ const plan = await readSetupPlan(root);
454
+ const result = await verifySetup(root, plan);
455
+ const reviewActions = [
456
+ ...result.instrumentationActions,
457
+ ...result.contractActions,
458
+ ];
459
+ const pendingReviewActions = reviewActions.filter(({ status }) => status === "pending" || status === "stale" || status === "missing").length;
460
+ io.stdout(`${JSON.stringify({
461
+ ...result,
462
+ fingerprint: result.planFingerprint,
463
+ collectorDependency: result.collectorDependency.present,
464
+ collectorDependencyVersion: result.collectorDependency.version,
465
+ pendingReviewActions,
466
+ completedReviewActions: reviewActions.length - pendingReviewActions,
467
+ }, null, 2)}\n`);
468
+ return result.setupComplete ? 0 : 1;
469
+ }
470
+ if (parsed.command === "review") {
471
+ const plan = await readSetupPlan(root);
472
+ if (!parsed.all && parsed.actions.length === 0) {
473
+ return await runInteractiveReview(plan, root, io, parsed.evidence, parsed.sourceBacked);
474
+ }
475
+ const selectedActions = parsed.all
476
+ ? operatorReviewableSetupActions(plan).map(({ id }) => id)
477
+ : parsed.actions;
478
+ if (selectedActions.length === 0) {
479
+ throw new Error("No instrumentation actions are available for review");
480
+ }
481
+ const recordReview = parsed.sourceBacked
482
+ ? recordSourceBackedSetupEvidence
483
+ : reviewSetupActions;
484
+ const result = await recordReview(plan, root, selectedActions, {
485
+ write: parsed.write,
486
+ ...(parsed.evidence ? { evidence: parsed.evidence } : {}),
487
+ });
488
+ io.stdout(`${JSON.stringify(result, null, 2)}\n`);
489
+ return 0;
490
+ }
491
+ if (parsed.command === "observations-check") {
492
+ const plan = await readSetupPlan(root);
493
+ const connectionKey = io.env?.(plan.connectionKeyEnvironment) ?? "";
494
+ const apiKey = io.env?.("SEAMWARD_API_KEY") ?? "";
495
+ if (!connectionKey || !apiKey) {
496
+ throw new Error(`${plan.connectionKeyEnvironment} and SEAMWARD_API_KEY are required for the first-observation check`);
497
+ }
498
+ const endpoint = parsed.endpoint ?? io.env?.("SEAMWARD_MANAGEMENT_API_URL");
499
+ const result = await checkFirstSetupObservation(plan, root, {
500
+ apiKey,
501
+ connectionKey,
502
+ ...(endpoint ? { endpoint } : {}),
503
+ ...(io.fetchFn ? { fetchFn: io.fetchFn } : {}),
504
+ write: parsed.write,
505
+ });
506
+ io.stdout(`${JSON.stringify(result, null, 2)}\n`);
507
+ return result.status === "observed" ? 0 : 1;
508
+ }
509
+ if (parsed.command === "contracts-register") {
510
+ const plan = await readSetupPlan(root);
511
+ const connectionKey = io.env?.(plan.connectionKeyEnvironment) ?? "";
512
+ if (!connectionKey) {
513
+ throw new Error(`${plan.connectionKeyEnvironment} is required for contract registration`);
514
+ }
515
+ await applySetupPlan(plan, root);
516
+ const jsonSchemaBinding = await readJsonSchemaBinding(root, parsed.binding);
517
+ const selected = await previewPlanContract(plan, root, connectionKey, {
518
+ ...(parsed.files[0] ? { file: parsed.files[0] } : {}),
519
+ ...(jsonSchemaBinding ? { jsonSchemaBinding } : {}),
520
+ includeExamples: parsed.includeExamples,
521
+ });
522
+ if (!parsed.write) {
523
+ io.stdout(`${JSON.stringify({
524
+ applied: false,
525
+ operation: "contract_registration",
526
+ integrationId: selected.integrationId,
527
+ declaredVersion: parsed.version,
528
+ files: [selected.file],
529
+ format: selected.format,
530
+ fingerprint: selected.fingerprint,
531
+ byteSize: selected.byteSize,
532
+ removedExampleFields: selected.removedExampleFields,
533
+ ...(selected.operationBinding
534
+ ? { operationBinding: selected.operationBinding }
535
+ : {}),
536
+ examples: parsed.includeExamples ? "included" : "removed",
537
+ }, null, 2)}\n`);
538
+ return 0;
539
+ }
540
+ const apiKey = io.env?.("SEAMWARD_API_KEY") ?? "";
541
+ if (!apiKey)
542
+ throw new Error("SEAMWARD_API_KEY is required with --write");
543
+ const endpoint = parsed.endpoint ?? io.env?.("SEAMWARD_MANAGEMENT_API_URL");
544
+ const result = await registerPlanContracts(plan, root, {
545
+ apiKey,
546
+ connectionKey,
547
+ declaredVersion: parsed.version ?? "",
548
+ ...(endpoint ? { endpoint } : {}),
549
+ ...(parsed.files[0] ? { file: parsed.files[0] } : {}),
550
+ ...(jsonSchemaBinding ? { jsonSchemaBinding } : {}),
551
+ includeExamples: parsed.includeExamples,
552
+ writeCompletion: true,
553
+ ...(io.fetchFn ? { fetchFn: io.fetchFn } : {}),
554
+ });
555
+ io.stdout(`${JSON.stringify(result, null, 2)}\n`);
556
+ return 0;
557
+ }
558
+ if (parsed.command === "contracts-status") {
559
+ const plan = await readSetupPlan(root);
560
+ const connectionKey = io.env?.(plan.connectionKeyEnvironment) ?? "";
561
+ const apiKey = io.env?.("SEAMWARD_API_KEY") ?? "";
562
+ if (!connectionKey || !apiKey) {
563
+ throw new Error(`${plan.connectionKeyEnvironment} and SEAMWARD_API_KEY are required for contract status`);
564
+ }
565
+ const endpoint = parsed.endpoint ?? io.env?.("SEAMWARD_MANAGEMENT_API_URL");
566
+ const status = await readContractStatus({
567
+ apiKey,
568
+ connectionKey,
569
+ ...(endpoint ? { endpoint } : {}),
570
+ ...(io.fetchFn ? { fetchFn: io.fetchFn } : {}),
571
+ });
572
+ io.stdout(`${JSON.stringify(status, null, 2)}\n`);
573
+ return 0;
574
+ }
575
+ if (parsed.command === "contracts-activate") {
576
+ const plan = await readSetupPlan(root);
577
+ const connectionKey = io.env?.(plan.connectionKeyEnvironment) ?? "";
578
+ const apiKey = io.env?.("SEAMWARD_API_KEY") ?? "";
579
+ if (!connectionKey || !apiKey) {
580
+ throw new Error(`${plan.connectionKeyEnvironment} and SEAMWARD_API_KEY are required for contract activation`);
581
+ }
582
+ const endpoint = parsed.endpoint ?? io.env?.("SEAMWARD_MANAGEMENT_API_URL");
583
+ const status = await readContractStatus({
584
+ apiKey,
585
+ connectionKey,
586
+ ...(endpoint ? { endpoint } : {}),
587
+ ...(io.fetchFn ? { fetchFn: io.fetchFn } : {}),
588
+ });
589
+ const preview = await previewPlanContractActivation(plan, root, status, {
590
+ contractVersionId: parsed.contractVersionId ?? "",
591
+ reason: parsed.reason ?? "promote",
592
+ });
593
+ if (preview.expectedActiveContractVersionId !==
594
+ (parsed.expectedActiveContractVersionId ?? null)) {
595
+ throw new Error("The reviewed --expected contract does not match current contract status");
596
+ }
597
+ if (!parsed.write) {
598
+ io.stdout(`${JSON.stringify({
599
+ applied: false,
600
+ operation: "contract_activation",
601
+ ...preview,
602
+ }, null, 2)}\n`);
603
+ return 0;
604
+ }
605
+ const result = await activatePlanContractVersion(plan, root, preview, {
606
+ apiKey,
607
+ connectionKey,
608
+ ...(endpoint ? { endpoint } : {}),
609
+ ...(io.fetchFn ? { fetchFn: io.fetchFn } : {}),
610
+ });
611
+ io.stdout(`${JSON.stringify(result, null, 2)}\n`);
612
+ return 0;
613
+ }
614
+ const discovery = await scanProject(root);
615
+ if (parsed.command === "discover") {
616
+ io.stdout(`${JSON.stringify(discovery, null, 2)}\n`);
617
+ return 0;
618
+ }
619
+ const metadata = await readProjectMetadata(root);
620
+ const plan = createSetupPlan(discovery, {
621
+ ...metadata,
622
+ contractFiles: parsed.setupContracts,
623
+ ...(parsed.direction && parsed.protocol
624
+ ? {
625
+ integrationScope: {
626
+ direction: parsed.direction,
627
+ protocol: parsed.protocol,
628
+ },
629
+ }
630
+ : {}),
631
+ });
632
+ const result = await applySetupPlan(plan, root, {
633
+ write: parsed.write,
634
+ force: parsed.force,
635
+ });
636
+ io.stdout(`${JSON.stringify({ ...result, plan }, null, 2)}\n`);
637
+ return 0;
638
+ }
639
+ catch (error) {
640
+ io.stderr(`${error.message}\n`);
641
+ return 1;
642
+ }
643
+ }
644
+ const entrypoint = process.argv[1];
645
+ let isEntrypoint = false;
646
+ if (entrypoint) {
647
+ try {
648
+ isEntrypoint =
649
+ import.meta.url === pathToFileURL(await realpath(entrypoint)).href;
650
+ }
651
+ catch {
652
+ isEntrypoint = import.meta.url === pathToFileURL(entrypoint).href;
653
+ }
654
+ }
655
+ if (isEntrypoint) {
656
+ const exitCode = await runCli(process.argv.slice(2));
657
+ process.exitCode = exitCode;
658
+ }
659
+ //# sourceMappingURL=cli.js.map