@flow-as-code/cli 0.1.0
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/LICENSE +202 -0
- package/README.md +446 -0
- package/dist/aws.d.ts +28 -0
- package/dist/aws.d.ts.map +1 -0
- package/dist/aws.js +104 -0
- package/dist/aws.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +132 -0
- package/dist/bin.js.map +1 -0
- package/dist/bridge/exportFiles.d.ts +16 -0
- package/dist/bridge/exportFiles.d.ts.map +1 -0
- package/dist/bridge/exportFiles.js +95 -0
- package/dist/bridge/exportFiles.js.map +1 -0
- package/dist/bridge/pair.d.ts +103 -0
- package/dist/bridge/pair.d.ts.map +1 -0
- package/dist/bridge/pair.js +219 -0
- package/dist/bridge/pair.js.map +1 -0
- package/dist/bridge/protocol.d.ts +187 -0
- package/dist/bridge/protocol.d.ts.map +1 -0
- package/dist/bridge/protocol.js +132 -0
- package/dist/bridge/protocol.js.map +1 -0
- package/dist/bridge/server.d.ts +95 -0
- package/dist/bridge/server.d.ts.map +1 -0
- package/dist/bridge/server.js +669 -0
- package/dist/bridge/server.js.map +1 -0
- package/dist/cdk-scaffold.d.ts +16 -0
- package/dist/cdk-scaffold.d.ts.map +1 -0
- package/dist/cdk-scaffold.js +28 -0
- package/dist/cdk-scaffold.js.map +1 -0
- package/dist/codegen.d.ts +5 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +27 -0
- package/dist/codegen.js.map +1 -0
- package/dist/diff.d.ts +33 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +225 -0
- package/dist/diff.js.map +1 -0
- package/dist/docs.d.ts +46 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +223 -0
- package/dist/docs.js.map +1 -0
- package/dist/emit.d.ts +8 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +55 -0
- package/dist/emit.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/export.d.ts +27 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +126 -0
- package/dist/export.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +6 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +31 -0
- package/dist/lint.js.map +1 -0
- package/dist/render.d.ts +6 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +49 -0
- package/dist/render.js.map +1 -0
- package/dist/run.d.ts +13 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +38 -0
- package/dist/run.js.map +1 -0
- package/dist/simulate.d.ts +35 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +194 -0
- package/dist/simulate.js.map +1 -0
- package/dist/studio.d.ts +23 -0
- package/dist/studio.d.ts.map +1 -0
- package/dist/studio.js +120 -0
- package/dist/studio.js.map +1 -0
- package/dist/synth-resolve-hook.d.ts +19 -0
- package/dist/synth-resolve-hook.d.ts.map +1 -0
- package/dist/synth-resolve-hook.js +127 -0
- package/dist/synth-resolve-hook.js.map +1 -0
- package/dist/synth-runner.d.ts +12 -0
- package/dist/synth-runner.d.ts.map +1 -0
- package/dist/synth-runner.js +145 -0
- package/dist/synth-runner.js.map +1 -0
- package/dist/synth.d.ts +55 -0
- package/dist/synth.d.ts.map +1 -0
- package/dist/synth.js +327 -0
- package/dist/synth.js.map +1 -0
- package/dist/watch.d.ts +66 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +293 -0
- package/dist/watch.js.map +1 -0
- package/package.json +85 -0
- package/schema/flowdoc-0.1.schema.json +669 -0
- package/schema/scenario-0.1.schema.json +318 -0
- package/src/aws.ts +149 -0
- package/src/bin.ts +179 -0
- package/src/bridge/exportFiles.ts +116 -0
- package/src/bridge/pair.ts +312 -0
- package/src/bridge/protocol.ts +289 -0
- package/src/bridge/server.ts +827 -0
- package/src/cdk-scaffold.ts +41 -0
- package/src/codegen.ts +36 -0
- package/src/diff.ts +279 -0
- package/src/docs.ts +249 -0
- package/src/emit.ts +69 -0
- package/src/errors.ts +27 -0
- package/src/export.ts +167 -0
- package/src/index.ts +27 -0
- package/src/lint.ts +42 -0
- package/src/render.ts +58 -0
- package/src/run.ts +42 -0
- package/src/simulate.ts +244 -0
- package/src/studio.ts +143 -0
- package/src/synth-resolve-hook.ts +152 -0
- package/src/synth-runner.ts +157 -0
- package/src/synth.ts +380 -0
- package/src/watch.ts +388 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://flow-as-code.dev/schema/scenario-0.1.schema.json",
|
|
4
|
+
"title": "Simulate scenario 0.1",
|
|
5
|
+
"description": "Authoring format for a flow simulation. This is not an Amazon Connect API shape: @flow-as-code/core compiles it into the Connect Testing language (a graph of Observations) that CreateTestCase takes as its Content string. See packages/core/src/simulate.ts and https://docs.aws.amazon.com/connect/latest/devguide/testing-language.html. The structural rules live here; the cross-field rules, including the live-agent safety rule, live in validateScenario.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["scenario", "name", "entryPoint", "steps"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"scenario": {
|
|
11
|
+
"const": "0.1"
|
|
12
|
+
},
|
|
13
|
+
"name": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"minLength": 1,
|
|
16
|
+
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
|
|
17
|
+
},
|
|
18
|
+
"description": {
|
|
19
|
+
"type": "string"
|
|
20
|
+
},
|
|
21
|
+
"entryPoint": {
|
|
22
|
+
"$ref": "#/$defs/entryPoint"
|
|
23
|
+
},
|
|
24
|
+
"attributes": {
|
|
25
|
+
"description": "Initial contact attributes. Serialized into the InitializationData JSON string.",
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": {
|
|
28
|
+
"type": "string"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"steps": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"minItems": 1,
|
|
34
|
+
"items": {
|
|
35
|
+
"$ref": "#/$defs/step"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"substitutions": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": {
|
|
41
|
+
"$ref": "#/$defs/substitution"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"endTest": {
|
|
45
|
+
"description": "Append a TestControl EndTest action. Default true. A simulated contact that reaches a queue can connect to a live agent, so a scenario that expects a transfer must either end the test or substitute the queue.",
|
|
46
|
+
"type": "boolean"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"$defs": {
|
|
50
|
+
"token": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"pattern": "^\\$\\{cdref:(queue|hours|lambda|lex|prompt|flow|module):[a-z0-9]+(-[a-z0-9]+)*(@[a-z0-9]+(-[a-z0-9]+)*)?\\}$"
|
|
53
|
+
},
|
|
54
|
+
"flowToken": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"pattern": "^\\$\\{cdref:flow:[a-z0-9]+(-[a-z0-9]+)*\\}$"
|
|
57
|
+
},
|
|
58
|
+
"queueToken": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"pattern": "^\\$\\{cdref:queue:[a-z0-9]+(-[a-z0-9]+)*\\}$"
|
|
61
|
+
},
|
|
62
|
+
"hoursToken": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"pattern": "^\\$\\{cdref:hours:[a-z0-9]+(-[a-z0-9]+)*\\}$"
|
|
65
|
+
},
|
|
66
|
+
"lambdaToken": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"pattern": "^\\$\\{cdref:lambda:[a-z0-9]+(-[a-z0-9]+)*\\}$"
|
|
69
|
+
},
|
|
70
|
+
"lexToken": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"pattern": "^\\$\\{cdref:lex:[a-z0-9]+(-[a-z0-9]+)*\\}$"
|
|
73
|
+
},
|
|
74
|
+
"jsonPath": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"pattern": "^\\$\\.[A-Za-z0-9_$.\\[\\]'-]+$"
|
|
77
|
+
},
|
|
78
|
+
"entryPoint": {
|
|
79
|
+
"description": "TestCaseEntryPointType is CHAT or VOICE_CALL. The channel is a property of the entry point, not of an input. The flow token resolves to a contact flow id or ARN for FlowId; there is no destination number because CreateTestCase rejects a FlowId combined with a DestinationPhoneNumber. https://docs.aws.amazon.com/connect/latest/APIReference/API_VoiceCallEntryPointParameters.html",
|
|
80
|
+
"type": "object",
|
|
81
|
+
"required": ["channel", "flow"],
|
|
82
|
+
"additionalProperties": false,
|
|
83
|
+
"properties": {
|
|
84
|
+
"channel": {
|
|
85
|
+
"enum": ["voice", "chat"]
|
|
86
|
+
},
|
|
87
|
+
"flow": {
|
|
88
|
+
"$ref": "#/$defs/flowToken"
|
|
89
|
+
},
|
|
90
|
+
"sourcePhoneNumber": {
|
|
91
|
+
"description": "Voice only. E.164, as VoiceCallEntryPointParameters.SourcePhoneNumber requires. Defaults to the fictional +15550100.",
|
|
92
|
+
"type": "string",
|
|
93
|
+
"pattern": "^\\+[1-9]\\d{1,14}$"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"if": {
|
|
97
|
+
"properties": {
|
|
98
|
+
"channel": {
|
|
99
|
+
"const": "chat"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"required": ["channel"]
|
|
103
|
+
},
|
|
104
|
+
"then": {
|
|
105
|
+
"not": {
|
|
106
|
+
"required": ["sourcePhoneNumber"]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"substitution": {
|
|
111
|
+
"description": "OverrideSystemBehavior with a SubstituteResource strategy: swaps a production resource for a test one. actionParameters names the production resource under the key the action type reads (QueueId, HoursOfOperationId, LambdaFunctionARN); CreateTestCase rejects an empty ActionParameters. https://docs.aws.amazon.com/connect/latest/devguide/testing-language-actions-override-system-behavior.html",
|
|
112
|
+
"type": "object",
|
|
113
|
+
"required": ["actionType", "actionParameters", "substitute"],
|
|
114
|
+
"additionalProperties": false,
|
|
115
|
+
"properties": {
|
|
116
|
+
"actionType": {
|
|
117
|
+
"enum": ["TransferContactToQueue", "CheckHoursOfOperation", "InvokeLambdaFunction"]
|
|
118
|
+
},
|
|
119
|
+
"actionParameters": {
|
|
120
|
+
"description": "Copied verbatim into Content, so every value is a token and never a literal. Which key the action type reads is a cross-field rule that lives in validateScenario.",
|
|
121
|
+
"type": "object",
|
|
122
|
+
"minProperties": 1,
|
|
123
|
+
"additionalProperties": {
|
|
124
|
+
"$ref": "#/$defs/token"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"substitute": {
|
|
128
|
+
"$ref": "#/$defs/token"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"step": {
|
|
133
|
+
"oneOf": [
|
|
134
|
+
{
|
|
135
|
+
"type": "object",
|
|
136
|
+
"required": ["kind"],
|
|
137
|
+
"additionalProperties": false,
|
|
138
|
+
"properties": {
|
|
139
|
+
"kind": {
|
|
140
|
+
"const": "expect-prompt"
|
|
141
|
+
},
|
|
142
|
+
"contains": {
|
|
143
|
+
"type": "string",
|
|
144
|
+
"minLength": 1
|
|
145
|
+
},
|
|
146
|
+
"similarTo": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"minLength": 1
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"oneOf": [
|
|
152
|
+
{
|
|
153
|
+
"required": ["contains"],
|
|
154
|
+
"not": {
|
|
155
|
+
"required": ["similarTo"]
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"required": ["similarTo"],
|
|
160
|
+
"not": {
|
|
161
|
+
"required": ["contains"]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"type": "object",
|
|
168
|
+
"required": ["kind", "lambda"],
|
|
169
|
+
"additionalProperties": false,
|
|
170
|
+
"properties": {
|
|
171
|
+
"kind": {
|
|
172
|
+
"const": "expect-lambda"
|
|
173
|
+
},
|
|
174
|
+
"lambda": {
|
|
175
|
+
"$ref": "#/$defs/lambdaToken"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"type": "object",
|
|
181
|
+
"required": ["kind", "queue"],
|
|
182
|
+
"additionalProperties": false,
|
|
183
|
+
"properties": {
|
|
184
|
+
"kind": {
|
|
185
|
+
"const": "expect-transfer"
|
|
186
|
+
},
|
|
187
|
+
"queue": {
|
|
188
|
+
"$ref": "#/$defs/queueToken"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"type": "object",
|
|
194
|
+
"required": ["kind", "hours"],
|
|
195
|
+
"additionalProperties": false,
|
|
196
|
+
"properties": {
|
|
197
|
+
"kind": {
|
|
198
|
+
"const": "expect-hours-check"
|
|
199
|
+
},
|
|
200
|
+
"hours": {
|
|
201
|
+
"$ref": "#/$defs/hoursToken"
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"type": "object",
|
|
207
|
+
"required": ["kind", "lex"],
|
|
208
|
+
"additionalProperties": false,
|
|
209
|
+
"properties": {
|
|
210
|
+
"kind": {
|
|
211
|
+
"const": "expect-lex"
|
|
212
|
+
},
|
|
213
|
+
"lex": {
|
|
214
|
+
"$ref": "#/$defs/lexToken"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"type": "object",
|
|
220
|
+
"required": ["kind", "name"],
|
|
221
|
+
"additionalProperties": false,
|
|
222
|
+
"properties": {
|
|
223
|
+
"kind": {
|
|
224
|
+
"const": "expect-queue"
|
|
225
|
+
},
|
|
226
|
+
"name": {
|
|
227
|
+
"type": "string",
|
|
228
|
+
"minLength": 1
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"type": "object",
|
|
234
|
+
"required": ["kind", "path", "operator", "value"],
|
|
235
|
+
"additionalProperties": false,
|
|
236
|
+
"properties": {
|
|
237
|
+
"kind": {
|
|
238
|
+
"const": "assert"
|
|
239
|
+
},
|
|
240
|
+
"path": {
|
|
241
|
+
"$ref": "#/$defs/jsonPath"
|
|
242
|
+
},
|
|
243
|
+
"operator": {
|
|
244
|
+
"enum": [
|
|
245
|
+
"Equals",
|
|
246
|
+
"TextStartsWith",
|
|
247
|
+
"TextEndsWith",
|
|
248
|
+
"TextContains",
|
|
249
|
+
"NumberGreaterThan",
|
|
250
|
+
"NumberGreaterOrEqualTo",
|
|
251
|
+
"NumberLessThan",
|
|
252
|
+
"NumberLessOrEqualTo"
|
|
253
|
+
]
|
|
254
|
+
},
|
|
255
|
+
"value": {
|
|
256
|
+
"type": "string"
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"type": "object",
|
|
262
|
+
"required": ["kind", "value"],
|
|
263
|
+
"additionalProperties": false,
|
|
264
|
+
"properties": {
|
|
265
|
+
"kind": {
|
|
266
|
+
"const": "send-dtmf"
|
|
267
|
+
},
|
|
268
|
+
"value": {
|
|
269
|
+
"type": "string",
|
|
270
|
+
"pattern": "^[0-9*#]+$"
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"type": "object",
|
|
276
|
+
"required": ["kind", "text"],
|
|
277
|
+
"additionalProperties": false,
|
|
278
|
+
"properties": {
|
|
279
|
+
"kind": {
|
|
280
|
+
"const": "send-speech"
|
|
281
|
+
},
|
|
282
|
+
"text": {
|
|
283
|
+
"type": "string",
|
|
284
|
+
"minLength": 1
|
|
285
|
+
},
|
|
286
|
+
"languageCode": {
|
|
287
|
+
"type": "string"
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"type": "object",
|
|
293
|
+
"required": ["kind", "text"],
|
|
294
|
+
"additionalProperties": false,
|
|
295
|
+
"properties": {
|
|
296
|
+
"kind": {
|
|
297
|
+
"const": "send-text"
|
|
298
|
+
},
|
|
299
|
+
"text": {
|
|
300
|
+
"type": "string",
|
|
301
|
+
"minLength": 1
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"type": "object",
|
|
307
|
+
"required": ["kind"],
|
|
308
|
+
"additionalProperties": false,
|
|
309
|
+
"properties": {
|
|
310
|
+
"kind": {
|
|
311
|
+
"const": "disconnect"
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
package/src/aws.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 The flow-as-code Authors
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
// Where the CLI meets the AWS SDK, and the seam the tests replace.
|
|
6
|
+
//
|
|
7
|
+
// `export`, `simulate`, and `diff` each reach their instance through one of the
|
|
8
|
+
// two factories in LiveClients. The default set builds @flow-as-code/core's SDK adapters
|
|
9
|
+
// over a real ConnectClient; a test passes its own set, holding a fixture
|
|
10
|
+
// client, and every line of the commands runs offline. Nothing else in this
|
|
11
|
+
// package imports the SDK.
|
|
12
|
+
//
|
|
13
|
+
// Credentials: the SDK's default provider chain (environment variables, the
|
|
14
|
+
// shared config and credentials files with their profiles and SSO sessions,
|
|
15
|
+
// web identity, the container and instance metadata endpoints). The CLI takes
|
|
16
|
+
// no credential flags and reads no secrets of its own.
|
|
17
|
+
// https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html
|
|
18
|
+
//
|
|
19
|
+
// Region: the one in the instance ARN. A v3 client must be given a Region, and
|
|
20
|
+
// an instance ARN spells it (`arn:aws:connect:<region>:<account>:instance/<id>`),
|
|
21
|
+
// so nothing is read from AWS_REGION or a profile, and an instance in one
|
|
22
|
+
// Region cannot be addressed through a client configured for another.
|
|
23
|
+
// https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html
|
|
24
|
+
// https://docs.aws.amazon.com/connect/latest/adminguide/find-instance-arn.html
|
|
25
|
+
//
|
|
26
|
+
// @aws-sdk/client-connect is an optional peer dependency, loaded with a dynamic
|
|
27
|
+
// import at the moment a command needs it. A missing SDK is a CliError naming
|
|
28
|
+
// the package, not a stack trace from inside a module resolver. Only the
|
|
29
|
+
// package itself being unresolvable counts as missing; an SDK that is present
|
|
30
|
+
// but cannot be loaded (a dependency of its own missing, a file it cannot
|
|
31
|
+
// evaluate) is reported with the loader's reason, because the install command
|
|
32
|
+
// is not the fix for that.
|
|
33
|
+
|
|
34
|
+
import type { AwsCommandSender, ConnectInventoryClient, FlowTestClient } from "@flow-as-code/core";
|
|
35
|
+
import {
|
|
36
|
+
createConnectInventoryClient,
|
|
37
|
+
createConnectTestClient,
|
|
38
|
+
parseConnectArn,
|
|
39
|
+
} from "@flow-as-code/core";
|
|
40
|
+
|
|
41
|
+
import { CliError, messageOf } from "./errors.js";
|
|
42
|
+
|
|
43
|
+
/** An instance, as `--instance` names it. */
|
|
44
|
+
export interface InstanceTarget {
|
|
45
|
+
/** The ARN exactly as given. */
|
|
46
|
+
arn: string;
|
|
47
|
+
region: string;
|
|
48
|
+
instanceId: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The clients a live command runs through. Tests supply fixture-backed ones. */
|
|
52
|
+
export interface LiveClients {
|
|
53
|
+
/** For `export` and `diff`: the inventory and flow content of an instance. */
|
|
54
|
+
inventory(target: InstanceTarget): Promise<ConnectInventoryClient>;
|
|
55
|
+
/** For `simulate`: the TestCase operations. */
|
|
56
|
+
test(target: InstanceTarget): Promise<FlowTestClient>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The peer dependency every live command needs. */
|
|
60
|
+
export const SDK_PACKAGE = "@aws-sdk/client-connect";
|
|
61
|
+
|
|
62
|
+
export const MISSING_SDK_MESSAGE =
|
|
63
|
+
`Connecting to an instance needs the optional peer dependency ${SDK_PACKAGE}. ` +
|
|
64
|
+
`Install it beside flow-cli (npm install ${SDK_PACKAGE}).`;
|
|
65
|
+
|
|
66
|
+
export const STALE_SDK_MESSAGE =
|
|
67
|
+
`The installed ${SDK_PACKAGE} does not expose the TestCase operations. ` +
|
|
68
|
+
"They are recent; upgrade the SDK (3.1122.0 has them all).";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parses `--instance`. Only an instance ARN will do: a bare id carries no
|
|
72
|
+
* Region, and a resource ARN names the wrong thing. `exitCode` is the code the
|
|
73
|
+
* rejection exits with, because `diff` reserves 1 for "differs".
|
|
74
|
+
*/
|
|
75
|
+
export function parseInstanceArn(arn: string, exitCode = 1): InstanceTarget {
|
|
76
|
+
const parsed = parseConnectArn(arn);
|
|
77
|
+
if (parsed === undefined || parsed.resourceType !== undefined || parsed.qualifier !== undefined) {
|
|
78
|
+
throw new CliError(
|
|
79
|
+
"--instance must be an Amazon Connect instance ARN " +
|
|
80
|
+
`(arn:<partition>:connect:<region>:<account>:instance/<id>), got "${arn}". ` +
|
|
81
|
+
"The Region is read from it.",
|
|
82
|
+
exitCode,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (parsed.region === "") {
|
|
86
|
+
throw new CliError(
|
|
87
|
+
`--instance ARN "${arn}" has no Region; one is needed to connect.`,
|
|
88
|
+
exitCode,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return { arn, region: parsed.region, instanceId: parsed.instanceId };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The slice of the SDK module the CLI touches. */
|
|
95
|
+
interface ConnectSdk {
|
|
96
|
+
ConnectClient: new (config: { region: string }) => AwsCommandSender;
|
|
97
|
+
CreateTestCaseCommand?: unknown;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether `error` is Node failing to find the SDK package itself: the ESM
|
|
102
|
+
* resolver's ERR_MODULE_NOT_FOUND, whose message quotes the bare specifier it
|
|
103
|
+
* could not resolve (`Cannot find package '<name>' imported from <file>`). A
|
|
104
|
+
* package the SDK depends on being absent raises the same code for a
|
|
105
|
+
* different specifier, or CommonJS's MODULE_NOT_FOUND, and is not this.
|
|
106
|
+
* https://nodejs.org/api/errors.html#err_module_not_found
|
|
107
|
+
*/
|
|
108
|
+
function isSdkNotFound(error: unknown): boolean {
|
|
109
|
+
return (
|
|
110
|
+
error instanceof Error &&
|
|
111
|
+
(error as { code?: unknown }).code === "ERR_MODULE_NOT_FOUND" &&
|
|
112
|
+
error.message.includes(`'${SDK_PACKAGE}'`)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function loadSdk(): Promise<ConnectSdk> {
|
|
117
|
+
try {
|
|
118
|
+
return (await import("@aws-sdk/client-connect")) as unknown as ConnectSdk;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (isSdkNotFound(error)) throw new CliError(MISSING_SDK_MESSAGE, 1, { cause: error });
|
|
121
|
+
throw new CliError(`${SDK_PACKAGE} could not be loaded: ${messageOf(error)}`, 1, {
|
|
122
|
+
cause: error,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Loads the SDK and builds a client for the Region the ARN names. */
|
|
128
|
+
async function connectClient(
|
|
129
|
+
target: InstanceTarget,
|
|
130
|
+
): Promise<{ sdk: ConnectSdk; connect: AwsCommandSender }> {
|
|
131
|
+
const sdk = await loadSdk();
|
|
132
|
+
return { sdk, connect: new sdk.ConnectClient({ region: target.region }) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The default LiveClients: @flow-as-code/core's SDK adapters over a real ConnectClient. */
|
|
136
|
+
export const SDK_CLIENTS: LiveClients = {
|
|
137
|
+
async inventory(target) {
|
|
138
|
+
const { connect } = await connectClient(target);
|
|
139
|
+
return createConnectInventoryClient({ connect, instanceId: target.instanceId });
|
|
140
|
+
},
|
|
141
|
+
async test(target) {
|
|
142
|
+
const { sdk, connect } = await connectClient(target);
|
|
143
|
+
// @flow-as-code/core checks the whole TestCase command set on first use. Checking
|
|
144
|
+
// one here turns an SDK too old for `simulate` into a message before any
|
|
145
|
+
// scenario is reported as errored.
|
|
146
|
+
if (typeof sdk.CreateTestCaseCommand !== "function") throw new CliError(STALE_SDK_MESSAGE);
|
|
147
|
+
return createConnectTestClient({ connect, instanceId: target.instanceId });
|
|
148
|
+
},
|
|
149
|
+
};
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Copyright 2026 The flow-as-code Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
// Command registration only. Every command's behaviour lives in its own module
|
|
7
|
+
// so it can be tested without a subprocess; this file wires arguments, options,
|
|
8
|
+
// and help text, in the order packages/cli/README.md lists them.
|
|
9
|
+
|
|
10
|
+
import { PACKAGE_NAMES } from "@flow-as-code/core";
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
|
|
13
|
+
import { runCodegen } from "./codegen.js";
|
|
14
|
+
import { EXIT_DIFF_ERROR, runDiff } from "./diff.js";
|
|
15
|
+
import { runEmit } from "./emit.js";
|
|
16
|
+
import { runExport } from "./export.js";
|
|
17
|
+
import { runLint } from "./lint.js";
|
|
18
|
+
import { runRender } from "./render.js";
|
|
19
|
+
import { action, usageErrorsExit } from "./run.js";
|
|
20
|
+
import { runSimulate } from "./simulate.js";
|
|
21
|
+
import { studioCommand, type StudioOptions } from "./studio.js";
|
|
22
|
+
import { synthToFiles } from "./synth.js";
|
|
23
|
+
|
|
24
|
+
const program = new Command();
|
|
25
|
+
program
|
|
26
|
+
.name("flow-cli")
|
|
27
|
+
.description("Typed, IaC-native tooling for Amazon Connect flows")
|
|
28
|
+
.version("0.0.1");
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command("lint")
|
|
32
|
+
.description(
|
|
33
|
+
`Check every FlowDoc in a directory (or one file) against the ${PACKAGE_NAMES.core} rule set. ` +
|
|
34
|
+
"The whole set is linted in one pass so cross-document rules can follow module " +
|
|
35
|
+
"references. Exits 1 when any finding has error severity; warnings alone exit 0.",
|
|
36
|
+
)
|
|
37
|
+
.argument("<dir-or-file>", "directory of *.flowdoc.json files, or one FlowDoc file")
|
|
38
|
+
.option("--format <format>", "report format: text or json", "text")
|
|
39
|
+
.action(action((target: string, opts: { format?: string }) => runLint(target, opts)));
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command("codegen")
|
|
43
|
+
.description(
|
|
44
|
+
"Generate idiomatic TypeScript builder source from a FlowDoc. Writes " +
|
|
45
|
+
"<doc name>.flow.ts next to the input unless --out says otherwise, and re-reads " +
|
|
46
|
+
"an existing output file first so comments marked @keep survive regeneration.",
|
|
47
|
+
)
|
|
48
|
+
.argument("<file>", "path to a .flowdoc.json file")
|
|
49
|
+
.option("--out <file>", "output file (default: <doc name>.flow.ts beside the input)")
|
|
50
|
+
.action(
|
|
51
|
+
action((file: string, opts: { out?: string }) => {
|
|
52
|
+
console.log(runCodegen(file, opts));
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command("synth")
|
|
58
|
+
.description(
|
|
59
|
+
"Execute a TypeScript builder file in a sandboxed child process and write one " +
|
|
60
|
+
"<flow.name>.flowdoc.json per exported flow. A flow is any exported Flow instance " +
|
|
61
|
+
"or the result of any exported zero-argument function returning one.",
|
|
62
|
+
)
|
|
63
|
+
.argument("<file>", "path to a .flow.ts builder file")
|
|
64
|
+
.option("--out <dir>", "output directory (default: the source file's directory)")
|
|
65
|
+
.action(
|
|
66
|
+
action(async (file: string, opts: { out?: string }) => {
|
|
67
|
+
// SynthError already carries a terminal-ready message; action() prints it.
|
|
68
|
+
for (const path of await synthToFiles(file, opts.out)) {
|
|
69
|
+
console.log(path);
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
program
|
|
75
|
+
.command("render")
|
|
76
|
+
.description(
|
|
77
|
+
"Materialize FlowDocs into deployable Flow language JSON, replacing every " +
|
|
78
|
+
"${cdref:...} token from the resource map. Writes <doc name>.json. A token with " +
|
|
79
|
+
"no entry in the map is fatal, and every missing token is listed at once.",
|
|
80
|
+
)
|
|
81
|
+
.argument("<dir-or-file>", "directory of *.flowdoc.json files, or one FlowDoc file")
|
|
82
|
+
.requiredOption("--resources <map.json>", "JSON object mapping reference tokens to values")
|
|
83
|
+
.option("--out <dir>", "output directory (default: the input directory)")
|
|
84
|
+
.action(
|
|
85
|
+
action((target: string, opts: { resources: string; out?: string }) => {
|
|
86
|
+
for (const path of runRender(target, opts)) console.log(path);
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command("emit")
|
|
92
|
+
.description(
|
|
93
|
+
"Emit infrastructure as code for a set of FlowDocs. --target tf writes the " +
|
|
94
|
+
`Terraform/OpenTofu files from ${PACKAGE_NAMES.tf}; --target cdk writes a flow-stack.ts ` +
|
|
95
|
+
`scaffold that constructs a ${PACKAGE_NAMES.cdk} FlowSet over the directory.`,
|
|
96
|
+
)
|
|
97
|
+
.argument("<dir-or-file>", "directory of *.flowdoc.json files, or one FlowDoc file")
|
|
98
|
+
.requiredOption("--target <target>", "cdk or tf")
|
|
99
|
+
.option("--address-map <refs.tfmap.json>", "tf only: reference to terraform address expressions")
|
|
100
|
+
.option("--out <dir>", "output directory (default: the input directory)")
|
|
101
|
+
.action(
|
|
102
|
+
action((input: string, opts: { target: string; addressMap?: string; out?: string }) => {
|
|
103
|
+
for (const path of runEmit(input, opts)) console.log(path);
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.command("diff")
|
|
109
|
+
.description(
|
|
110
|
+
"Compare every FlowDoc in a directory against the flow or module of the same name " +
|
|
111
|
+
"in a live Amazon Connect instance. Prints one line per document (unchanged, changed, " +
|
|
112
|
+
"missing-live) and a unified diff of the canonical JSON for each changed one; layout " +
|
|
113
|
+
"and meta are ignored. Exits 0 when nothing differs, 1 when something does, 2 when the " +
|
|
114
|
+
"comparison itself failed.",
|
|
115
|
+
)
|
|
116
|
+
.argument("<dir>", "directory of *.flowdoc.json files")
|
|
117
|
+
.requiredOption("--instance <arn>", "ARN of the Connect instance to compare against")
|
|
118
|
+
// 1 means "differs", so commander's own usage errors take the error code too.
|
|
119
|
+
.exitOverride(usageErrorsExit(EXIT_DIFF_ERROR))
|
|
120
|
+
.action(
|
|
121
|
+
action(async (dir: string, opts: { instance: string }) => {
|
|
122
|
+
await runDiff(dir, opts);
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
program
|
|
127
|
+
.command("export")
|
|
128
|
+
.description(
|
|
129
|
+
"Read every flow and module in a live Amazon Connect instance and write " +
|
|
130
|
+
"<name>.flowdoc.json plus <name>.flow.ts for each. References come out as tokens, " +
|
|
131
|
+
"never ARNs. Exits 1 when any flow could not be exported; --on-error collect (the " +
|
|
132
|
+
"default) still writes the rest and reports every failure at once.",
|
|
133
|
+
)
|
|
134
|
+
.requiredOption("--instance <arn>", "ARN of the Connect instance to read")
|
|
135
|
+
.option("--out <dir>", "output directory (default: the working directory)")
|
|
136
|
+
.option("--no-codegen", "write FlowDocs only, no TypeScript")
|
|
137
|
+
.option("--on-error <mode>", "abort on the first failed flow, or collect them all", "collect")
|
|
138
|
+
.action(
|
|
139
|
+
action(async (opts: { instance: string; out?: string; codegen: boolean; onError: string }) => {
|
|
140
|
+
await runExport(opts);
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
program
|
|
145
|
+
.command("simulate")
|
|
146
|
+
.description(
|
|
147
|
+
"Run a scenario suite against a live Amazon Connect instance through its TestCase " +
|
|
148
|
+
"operations, within the documented limits (5 concurrent, 100 in flight, 5 minutes " +
|
|
149
|
+
"each), and write a JUnit or JSON report. Exits 0 only when every scenario passed.",
|
|
150
|
+
)
|
|
151
|
+
.argument("<scenarios>", "scenario file, or a directory of scenario.json / *.scenario.json")
|
|
152
|
+
.requiredOption("--instance <arn>", "ARN of the Connect instance to run against")
|
|
153
|
+
.option("--resource-map <map.json>", "JSON object mapping reference tokens to ARNs")
|
|
154
|
+
.option("--format <format>", "report format: junit or json", "junit")
|
|
155
|
+
.option("--out <file>", "report file (default: stdout)")
|
|
156
|
+
.action(
|
|
157
|
+
action(
|
|
158
|
+
async (
|
|
159
|
+
scenarios: string,
|
|
160
|
+
opts: { instance: string; resourceMap?: string; format?: string; out?: string },
|
|
161
|
+
) => {
|
|
162
|
+
await runSimulate(scenarios, opts);
|
|
163
|
+
},
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
program
|
|
168
|
+
.command("studio")
|
|
169
|
+
.description(
|
|
170
|
+
"Serve the local visual editor over a directory. The bridge binds 127.0.0.1 only, " +
|
|
171
|
+
"picks a free port unless --port says otherwise, and keeps every <name>.flowdoc.json " +
|
|
172
|
+
"in sync with its <name>.flow.ts: a canvas save regenerates the builder source, and " +
|
|
173
|
+
"an edit to the source reloads the canvas.",
|
|
174
|
+
)
|
|
175
|
+
.argument("[dir]", "directory to open (default: the working directory)")
|
|
176
|
+
.option("--port <port>", "port to listen on (default: a free port)")
|
|
177
|
+
.action(action((dir: string | undefined, opts: StudioOptions) => studioCommand(dir, opts)));
|
|
178
|
+
|
|
179
|
+
program.parseAsync(process.argv);
|