@wictorwilen/cocogen 1.0.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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/RELEASING.md +36 -0
  4. package/THIRD_PARTY_NOTICES.md +11 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +288 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/emit/emit.d.ts +3 -0
  10. package/dist/emit/emit.d.ts.map +1 -0
  11. package/dist/emit/emit.js +13 -0
  12. package/dist/emit/emit.js.map +1 -0
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +6 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/init/init.d.ts +34 -0
  18. package/dist/init/init.d.ts.map +1 -0
  19. package/dist/init/init.js +886 -0
  20. package/dist/init/init.js.map +1 -0
  21. package/dist/init/template.d.ts +2 -0
  22. package/dist/init/template.d.ts.map +1 -0
  23. package/dist/init/template.js +19 -0
  24. package/dist/init/template.js.map +1 -0
  25. package/dist/init/templates/dotnet/.env.example.ejs +12 -0
  26. package/dist/init/templates/dotnet/.gitignore.ejs +6 -0
  27. package/dist/init/templates/dotnet/Datasource/CsvItemSource.cs.ejs +42 -0
  28. package/dist/init/templates/dotnet/Datasource/IItemSource.cs.ejs +12 -0
  29. package/dist/init/templates/dotnet/Generated/Constants.cs.ejs +17 -0
  30. package/dist/init/templates/dotnet/Generated/CsvParser.cs.ejs +119 -0
  31. package/dist/init/templates/dotnet/Generated/FromCsvRow.cs.ejs +14 -0
  32. package/dist/init/templates/dotnet/Generated/ItemPayload.cs.ejs +41 -0
  33. package/dist/init/templates/dotnet/Generated/Model.cs.ejs +28 -0
  34. package/dist/init/templates/dotnet/Generated/PersonEntityDefaults.cs.ejs +48 -0
  35. package/dist/init/templates/dotnet/Generated/PropertyTransforms.cs.ejs +22 -0
  36. package/dist/init/templates/dotnet/Generated/SchemaPayload.cs.ejs +18 -0
  37. package/dist/init/templates/dotnet/PersonEntityOverrides.cs.ejs +49 -0
  38. package/dist/init/templates/dotnet/Program.commandline.cs.ejs +426 -0
  39. package/dist/init/templates/dotnet/Program.cs.ejs +487 -0
  40. package/dist/init/templates/dotnet/README.md.ejs +56 -0
  41. package/dist/init/templates/dotnet/appsettings.json.ejs +21 -0
  42. package/dist/init/templates/dotnet/package.json.ejs +7 -0
  43. package/dist/init/templates/dotnet/project.csproj.ejs +29 -0
  44. package/dist/init/templates/dotnet/tspconfig.yaml.ejs +2 -0
  45. package/dist/init/templates/ts/.env.example.ejs +20 -0
  46. package/dist/init/templates/ts/README.md.ejs +54 -0
  47. package/dist/init/templates/ts/package.json.ejs +25 -0
  48. package/dist/init/templates/ts/src/cli.ts.ejs +299 -0
  49. package/dist/init/templates/ts/src/datasource/csvItemSource.ts.ejs +25 -0
  50. package/dist/init/templates/ts/src/datasource/itemSource.ts.ejs +8 -0
  51. package/dist/init/templates/ts/src/generated/constants.ts.ejs +10 -0
  52. package/dist/init/templates/ts/src/generated/csv.ts.ejs +44 -0
  53. package/dist/init/templates/ts/src/generated/fromCsvRow.ts.ejs +43 -0
  54. package/dist/init/templates/ts/src/generated/index.ts.ejs +5 -0
  55. package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +21 -0
  56. package/dist/init/templates/ts/src/generated/model.ts.ejs +16 -0
  57. package/dist/init/templates/ts/src/generated/personEntityDefaults.ts.ejs +33 -0
  58. package/dist/init/templates/ts/src/generated/propertyTransforms.ts.ejs +23 -0
  59. package/dist/init/templates/ts/src/generated/schemaPayload.ts.ejs +1 -0
  60. package/dist/init/templates/ts/src/index.ts.ejs +1 -0
  61. package/dist/init/templates/ts/src/personEntityOverrides.ts.ejs +36 -0
  62. package/dist/init/templates/ts/tsconfig.json.ejs +13 -0
  63. package/dist/init/templates/ts/tspconfig.yaml.ejs +2 -0
  64. package/dist/ir.d.ts +49 -0
  65. package/dist/ir.d.ts.map +1 -0
  66. package/dist/ir.js +2 -0
  67. package/dist/ir.js.map +1 -0
  68. package/dist/tsp/init-tsp.d.ts +14 -0
  69. package/dist/tsp/init-tsp.d.ts.map +1 -0
  70. package/dist/tsp/init-tsp.js +126 -0
  71. package/dist/tsp/init-tsp.js.map +1 -0
  72. package/dist/tsp/loader.d.ts +8 -0
  73. package/dist/tsp/loader.d.ts.map +1 -0
  74. package/dist/tsp/loader.js +264 -0
  75. package/dist/tsp/loader.js.map +1 -0
  76. package/dist/typespec/decorators.d.ts +14 -0
  77. package/dist/typespec/decorators.d.ts.map +1 -0
  78. package/dist/typespec/decorators.js +139 -0
  79. package/dist/typespec/decorators.js.map +1 -0
  80. package/dist/typespec/state.d.ts +37 -0
  81. package/dist/typespec/state.d.ts.map +1 -0
  82. package/dist/typespec/state.js +13 -0
  83. package/dist/typespec/state.js.map +1 -0
  84. package/dist/validate/validator.d.ts +9 -0
  85. package/dist/validate/validator.d.ts.map +1 -0
  86. package/dist/validate/validator.js +204 -0
  87. package/dist/validate/validator.js.map +1 -0
  88. package/package.json +66 -0
  89. package/typespec/main.tsp +117 -0
  90. package/typespec/tsp-index.js +6 -0
@@ -0,0 +1,426 @@
1
+ using Azure.Core;
2
+ using Azure.Identity;
3
+ using Microsoft.Extensions.Configuration;
4
+ <% if (graphApiVersion === "beta") { -%>
5
+ using Microsoft.Graph.Beta;
6
+ using Microsoft.Graph.Beta.Models.ExternalConnectors;
7
+ using Microsoft.Graph.Beta.Models.ODataErrors;
8
+ <% } else { -%>
9
+ using Microsoft.Graph;
10
+ using Microsoft.Graph.Models.ExternalConnectors;
11
+ using Microsoft.Graph.Models.ODataErrors;
12
+ <% } -%>
13
+ using Microsoft.Kiota.Abstractions;
14
+ using Microsoft.Kiota.Abstractions.Serialization;
15
+ using System.CommandLine;
16
+ using System.Net.Http.Headers;
17
+ using System.Text;
18
+ using System.Text.Json;
19
+
20
+ using <%= namespaceName %>.Datasource;
21
+ using <%= namespaceName %>.Schema;
22
+
23
+ var configuration = new ConfigurationBuilder()
24
+ .SetBasePath(Directory.GetCurrentDirectory())
25
+ .AddJsonFile("appsettings.json", optional: true)
26
+ .AddJsonFile("appsettings.Development.json", optional: true)
27
+ .AddEnvironmentVariables()
28
+ .Build();
29
+
30
+ string RequiredSetting(string key, string? fallback = null)
31
+ {
32
+ var value = configuration[key];
33
+ if (string.IsNullOrWhiteSpace(value))
34
+ value = fallback;
35
+ if (string.IsNullOrWhiteSpace(value))
36
+ {
37
+ var envKey = key.Replace(":", "__");
38
+ throw new InvalidOperationException(
39
+ $"Missing configuration: {key}. Set '{key}' in appsettings.json or set the env var '{envKey}'."
40
+ );
41
+ }
42
+ return value;
43
+ }
44
+
45
+ string ConnectionId() => RequiredSetting("Connection:Id");
46
+ string ConnectionName() => RequiredSetting("Connection:Name");
47
+ string ConnectionDescription() => RequiredSetting("Connection:Description");
48
+ <% if (isPeopleConnector) { -%>
49
+ string ProfileSourceWebUrl() => RequiredSetting("ProfileSource:WebUrl", SchemaConstants.ProfileSourceWebUrl);
50
+ string ProfileSourceDisplayName() => configuration["ProfileSource:DisplayName"]
51
+ ?? SchemaConstants.ProfileSourceDisplayName
52
+ ?? ConnectionName();
53
+ string ProfileSourcePriority()
54
+ {
55
+ var raw = configuration["ProfileSource:Priority"] ?? SchemaConstants.ProfileSourcePriority ?? "first";
56
+ var value = raw.Trim().ToLowerInvariant();
57
+ if (value is not "first" and not "last")
58
+ throw new InvalidOperationException("ProfileSource:Priority must be 'first' or 'last'.");
59
+ return value;
60
+ }
61
+ <% } -%>
62
+
63
+ ClientSecretCredential CreateCredential()
64
+ {
65
+ var tenantId = RequiredSetting("AzureAd:TenantId");
66
+ var clientId = RequiredSetting("AzureAd:ClientId");
67
+ var clientSecret = RequiredSetting("AzureAd:ClientSecret");
68
+ return new ClientSecretCredential(tenantId, clientId, clientSecret);
69
+ }
70
+
71
+ GraphServiceClient CreateGraphClient()
72
+ {
73
+ var credential = CreateCredential();
74
+ var graph = new GraphServiceClient(credential, new[] { "https://graph.microsoft.com/.default" });
75
+ graph.RequestAdapter.BaseUrl = SchemaConstants.GraphBaseUrl;
76
+ return graph;
77
+ }
78
+
79
+ <% if (isPeopleConnector) { -%>
80
+ async Task<string> GetAccessTokenAsync(ClientSecretCredential credential)
81
+ {
82
+ var token = await credential.GetTokenAsync(
83
+ new TokenRequestContext(new[] { "https://graph.microsoft.com/.default" })
84
+ );
85
+ return token.Token;
86
+ }
87
+
88
+ async Task<HttpResponseMessage> GraphRequestAsync(ClientSecretCredential credential, HttpMethod method, string url, object? body = null)
89
+ {
90
+ using var http = new HttpClient();
91
+ var token = await GetAccessTokenAsync(credential);
92
+ http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
93
+
94
+ var request = new HttpRequestMessage(method, url);
95
+ if (body is not null)
96
+ {
97
+ var json = JsonSerializer.Serialize(body);
98
+ request.Content = new StringContent(json, Encoding.UTF8, "application/json");
99
+ }
100
+
101
+ return await http.SendAsync(request);
102
+ }
103
+ <% } -%>
104
+
105
+ bool IsStatus(ApiException ex, int statusCode)
106
+ {
107
+ return ex.ResponseStatusCode is int sc && sc == statusCode;
108
+ }
109
+
110
+ async Task EnsureConnectionAsync(GraphServiceClient graph, string connectionId)
111
+ {
112
+ try
113
+ {
114
+ await graph.External.Connections[connectionId].GetAsync();
115
+ return;
116
+ }
117
+ catch (ApiException ex) when (IsStatus(ex, 404))
118
+ {
119
+ // Create below.
120
+ }
121
+
122
+ var connection = new ExternalConnection
123
+ {
124
+ Id = connectionId,
125
+ Name = ConnectionName(),
126
+ Description = ConnectionDescription(),
127
+ };
128
+
129
+ if (!string.IsNullOrWhiteSpace(SchemaConstants.ContentCategory))
130
+ {
131
+ // contentCategory is currently only exposed on the /beta endpoint.
132
+ connection.AdditionalData ??= new Dictionary<string, object>();
133
+ connection.AdditionalData["contentCategory"] = SchemaConstants.ContentCategory!;
134
+ }
135
+
136
+ await graph.External.Connections.PostAsync(connection);
137
+ }
138
+
139
+ async Task PatchSchemaAsync(GraphServiceClient graph, string connectionId)
140
+ {
141
+ var schema = SchemaPayload.BuildSchema();
142
+ await graph.External.Connections[connectionId].Schema.PatchAsync(schema);
143
+ }
144
+
145
+ async Task ProvisionAsync()
146
+ {
147
+ var graph = CreateGraphClient();
148
+ var connectionId = ConnectionId();
149
+
150
+ await EnsureConnectionAsync(graph, connectionId);
151
+ await PatchSchemaAsync(graph, connectionId);
152
+ <% if (isPeopleConnector) { -%>
153
+ await RegisterProfileSourceAsync(connectionId);
154
+ <% } -%>
155
+
156
+ Console.WriteLine("ok: provisioned");
157
+ }
158
+
159
+ async Task PutItemAsync(GraphServiceClient graph, string connectionId, Item item, bool verbose)
160
+ {
161
+ var itemId = ItemPayload.GetItemId(item);
162
+
163
+ var externalItem = ItemPayload.ToExternalItem(item);
164
+ if (verbose)
165
+ {
166
+ var url = $"{graph.RequestAdapter.BaseUrl ?? SchemaConstants.GraphBaseUrl}/external/connections/{connectionId}/items/{Uri.EscapeDataString(itemId)}";
167
+ Console.WriteLine("verbose: PUT " + url);
168
+ Console.WriteLine("verbose: payload " + JsonSerializer.Serialize(externalItem, new JsonSerializerOptions { WriteIndented = true }));
169
+ }
170
+
171
+ // NOTE: The external connectors surface may not expose a typed PutAsync on all SDK versions.
172
+ // We still use the Graph SDK's request adapter and models for a strongly-typed payload.
173
+ var requestInfo = new RequestInformation
174
+ {
175
+ HttpMethod = Method.PUT,
176
+ UrlTemplate = "{+baseurl}/external/connections/{connectionId}/items/{itemId}",
177
+ PathParameters = new Dictionary<string, object>
178
+ {
179
+ { "baseurl", graph.RequestAdapter.BaseUrl ?? SchemaConstants.GraphBaseUrl },
180
+ { "connectionId", connectionId },
181
+ { "itemId", itemId },
182
+ },
183
+ };
184
+
185
+ requestInfo.Headers.Add("Accept", "application/json");
186
+ requestInfo.SetContentFromParsable(graph.RequestAdapter, "application/json", externalItem);
187
+
188
+ var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
189
+ {
190
+ { "4XX", ODataError.CreateFromDiscriminatorValue },
191
+ { "5XX", ODataError.CreateFromDiscriminatorValue },
192
+ };
193
+
194
+ await graph.RequestAdapter.SendAsync<ExternalItem>(
195
+ requestInfo,
196
+ ExternalItem.CreateFromDiscriminatorValue,
197
+ errorMapping
198
+ );
199
+ }
200
+
201
+ async Task IngestAsync(string? csvPath, bool dryRun, int? limit, bool verbose)
202
+ {
203
+ GraphServiceClient? graph = null;
204
+ string? connectionId = null;
205
+ if (!dryRun)
206
+ {
207
+ graph = CreateGraphClient();
208
+ connectionId = ConnectionId();
209
+ }
210
+
211
+ var configuredCsv = configuration["Csv:Path"] ?? "data.csv";
212
+ var path = string.IsNullOrWhiteSpace(csvPath) ? configuredCsv : csvPath;
213
+
214
+ // Swap this for any IItemSource implementation (API, DB, queue, etc.).
215
+ IItemSource source = new CsvItemSource(path);
216
+
217
+ var count = 0;
218
+ await foreach (var item in source.GetItemsAsync())
219
+ {
220
+ if (limit.HasValue && count >= limit.Value)
221
+ break;
222
+ if (!dryRun)
223
+ {
224
+ await PutItemAsync(graph!, connectionId!, item, verbose);
225
+ }
226
+ else if (verbose)
227
+ {
228
+ Console.WriteLine("verbose: DRY RUN payload " + JsonSerializer.Serialize(ItemPayload.ToExternalItem(item), new JsonSerializerOptions { WriteIndented = true }));
229
+ }
230
+ count++;
231
+ }
232
+
233
+ Console.WriteLine($"ok: ingested {count} item(s)");
234
+ }
235
+
236
+ <% if (isPeopleConnector) { -%>
237
+ async Task RegisterProfileSourceAsync(string connectionId)
238
+ {
239
+ var credential = CreateCredential();
240
+ var webUrl = ProfileSourceWebUrl();
241
+ var displayName = ProfileSourceDisplayName();
242
+ var priority = ProfileSourcePriority();
243
+
244
+ var payload = new Dictionary<string, object?>
245
+ {
246
+ ["sourceId"] = connectionId,
247
+ ["displayName"] = displayName,
248
+ ["webUrl"] = webUrl,
249
+ };
250
+
251
+ var create = await GraphRequestAsync(
252
+ credential,
253
+ HttpMethod.Post,
254
+ $"{SchemaConstants.GraphBaseUrl}/admin/people/profileSources",
255
+ payload
256
+ );
257
+
258
+ if (!create.IsSuccessStatusCode && create.StatusCode != System.Net.HttpStatusCode.Conflict)
259
+ {
260
+ var text = await create.Content.ReadAsStringAsync();
261
+ throw new InvalidOperationException($"Failed to register profile source (HTTP {(int)create.StatusCode}): {text}");
262
+ }
263
+
264
+ var sourceUrl = $"{SchemaConstants.GraphBaseUrl}/admin/people/profileSources(sourceId='{connectionId}')";
265
+ await UpdateProfileSourcePrecedenceAsync(credential, sourceUrl, include: true, priority: priority);
266
+ }
267
+
268
+ async Task UnregisterProfileSourceAsync(string connectionId)
269
+ {
270
+ var credential = CreateCredential();
271
+ var sourceUrl = $"{SchemaConstants.GraphBaseUrl}/admin/people/profileSources(sourceId='{connectionId}')";
272
+
273
+ await UpdateProfileSourcePrecedenceAsync(credential, sourceUrl, include: false, priority: "first");
274
+
275
+ var res = await GraphRequestAsync(
276
+ credential,
277
+ HttpMethod.Delete,
278
+ $"{SchemaConstants.GraphBaseUrl}/admin/people/profileSources(sourceId='{connectionId}')"
279
+ );
280
+
281
+ if (!res.IsSuccessStatusCode && res.StatusCode != System.Net.HttpStatusCode.NotFound)
282
+ {
283
+ var text = await res.Content.ReadAsStringAsync();
284
+ throw new InvalidOperationException($"Failed to delete profile source (HTTP {(int)res.StatusCode}): {text}");
285
+ }
286
+ }
287
+
288
+ async Task UpdateProfileSourcePrecedenceAsync(ClientSecretCredential credential, string sourceUrl, bool include, string priority)
289
+ {
290
+ var res = await GraphRequestAsync(
291
+ credential,
292
+ HttpMethod.Get,
293
+ $"{SchemaConstants.GraphBaseUrl}/admin/people/profilePropertySettings"
294
+ );
295
+
296
+ if (!res.IsSuccessStatusCode)
297
+ {
298
+ var text = await res.Content.ReadAsStringAsync();
299
+ throw new InvalidOperationException($"Failed to list profile property settings (HTTP {(int)res.StatusCode}): {text}");
300
+ }
301
+
302
+ var json = await res.Content.ReadAsStringAsync();
303
+ using var doc = JsonDocument.Parse(json);
304
+ if (!doc.RootElement.TryGetProperty("value", out var values) || values.ValueKind != JsonValueKind.Array)
305
+ return;
306
+
307
+ foreach (var entry in values.EnumerateArray())
308
+ {
309
+ if (!entry.TryGetProperty("id", out var idProp) || idProp.ValueKind != JsonValueKind.String)
310
+ continue;
311
+
312
+ var id = idProp.GetString() ?? string.Empty;
313
+ if (string.IsNullOrWhiteSpace(id)) continue;
314
+
315
+ List<string> existing = new();
316
+ if (entry.TryGetProperty("prioritizedSourceUrls", out var urls) && urls.ValueKind == JsonValueKind.Array)
317
+ {
318
+ foreach (var url in urls.EnumerateArray())
319
+ {
320
+ if (url.ValueKind == JsonValueKind.String)
321
+ existing.Add(url.GetString() ?? "");
322
+ }
323
+ }
324
+
325
+ existing = existing.Where((u) => !string.IsNullOrWhiteSpace(u) && u != sourceUrl).ToList();
326
+ var updated = existing;
327
+ if (include)
328
+ {
329
+ updated = priority == "last"
330
+ ? existing.Concat(new[] { sourceUrl }).ToList()
331
+ : new[] { sourceUrl }.Concat(existing).ToList();
332
+ }
333
+
334
+ var patch = new Dictionary<string, object?>
335
+ {
336
+ ["@odata.type"] = "#microsoft.graph.profilePropertySetting",
337
+ ["prioritizedSourceUrls"] = updated,
338
+ };
339
+
340
+ var patchRes = await GraphRequestAsync(
341
+ credential,
342
+ HttpMethod.Patch,
343
+ $"{SchemaConstants.GraphBaseUrl}/admin/people/profilePropertySettings/{id}",
344
+ patch
345
+ );
346
+
347
+ if (!patchRes.IsSuccessStatusCode)
348
+ {
349
+ var text = await patchRes.Content.ReadAsStringAsync();
350
+ throw new InvalidOperationException($"Failed to update profile property setting {id} (HTTP {(int)patchRes.StatusCode}): {text}");
351
+ }
352
+ }
353
+ }
354
+ <% } -%>
355
+
356
+ async Task DeleteConnectionAsync()
357
+ {
358
+ var graph = CreateGraphClient();
359
+ var connectionId = ConnectionId();
360
+
361
+ <% if (isPeopleConnector) { -%>
362
+ await UnregisterProfileSourceAsync(connectionId);
363
+ <% } -%>
364
+
365
+ try
366
+ {
367
+ await graph.External.Connections[connectionId].DeleteAsync();
368
+ }
369
+ catch (ApiException ex) when (IsStatus(ex, 404))
370
+ {
371
+ // Already deleted.
372
+ }
373
+ Console.WriteLine("ok: deleted");
374
+ }
375
+
376
+ var csvOption = new Option<string>("--csv", description: "CSV path");
377
+ var dryRunOption = new Option<bool>("--dry-run", description: "Build payloads but do not send to Graph");
378
+ var limitOption = new Option<int?>("--limit", description: "Limit number of items");
379
+ var verboseOption = new Option<bool>("--verbose", description: "Print payloads sent to Graph");
380
+
381
+ var root = new RootCommand("Connector CLI generated by cocogen");
382
+
383
+ var provisionCommand = new Command("provision", "Create or update the connection and schema");
384
+ provisionCommand.SetHandler(async () => await ProvisionAsync());
385
+
386
+ var ingestCommand = new Command("ingest", "Ingest items from CSV");
387
+ ingestCommand.AddOption(csvOption);
388
+ ingestCommand.AddOption(dryRunOption);
389
+ ingestCommand.AddOption(limitOption);
390
+ ingestCommand.AddOption(verboseOption);
391
+ ingestCommand.SetHandler(
392
+ async (string? csv, bool dryRun, int? limit, bool verbose) => await IngestAsync(csv, dryRun, limit, verbose),
393
+ csvOption,
394
+ dryRunOption,
395
+ limitOption,
396
+ verboseOption
397
+ );
398
+
399
+ var deleteCommand = new Command("delete", "Delete the connection");
400
+ deleteCommand.SetHandler(async () => await DeleteConnectionAsync());
401
+
402
+ root.AddCommand(provisionCommand);
403
+ root.AddCommand(ingestCommand);
404
+ root.AddCommand(deleteCommand);
405
+
406
+ <% if (isPeopleConnector) { -%>
407
+ var registerCommand = new Command("register-profile-source", "Register the connection as a profile source");
408
+ registerCommand.SetHandler(async () => await RegisterProfileSourceAsync(ConnectionId()));
409
+ root.AddCommand(registerCommand);
410
+ <% } -%>
411
+
412
+ try
413
+ {
414
+ return await root.InvokeAsync(args);
415
+ }
416
+ catch (ApiException ex)
417
+ {
418
+ Console.Error.WriteLine("error: Graph request failed");
419
+ Console.Error.WriteLine(ex.Message);
420
+ return 1;
421
+ }
422
+ catch (Exception ex)
423
+ {
424
+ Console.Error.WriteLine("error: " + ex.Message);
425
+ return 1;
426
+ }