@rex0220/ksql-flownet 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 (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +81 -0
  3. package/README.md +117 -0
  4. package/dist/bundle/bundle-builder.d.ts +49 -0
  5. package/dist/bundle/bundle-builder.js +208 -0
  6. package/dist/bundle/index.d.ts +4 -0
  7. package/dist/bundle/index.js +4 -0
  8. package/dist/bundle/upload.d.ts +28 -0
  9. package/dist/bundle/upload.js +72 -0
  10. package/dist/bundle/zip-store.d.ts +17 -0
  11. package/dist/bundle/zip-store.js +175 -0
  12. package/dist/cli/archive-run-command.d.ts +12 -0
  13. package/dist/cli/archive-run-command.js +131 -0
  14. package/dist/cli/cancel-run-command.d.ts +9 -0
  15. package/dist/cli/cancel-run-command.js +77 -0
  16. package/dist/cli/force-unlock-network-command.d.ts +8 -0
  17. package/dist/cli/force-unlock-network-command.js +129 -0
  18. package/dist/cli/index.d.ts +3 -0
  19. package/dist/cli/index.js +132 -0
  20. package/dist/cli/plan-command.d.ts +2 -0
  21. package/dist/cli/plan-command.js +100 -0
  22. package/dist/cli/poll-requests-command.d.ts +3 -0
  23. package/dist/cli/poll-requests-command.js +74 -0
  24. package/dist/cli/record-job-unlock-command.d.ts +11 -0
  25. package/dist/cli/record-job-unlock-command.js +78 -0
  26. package/dist/cli/resolve-node-command.d.ts +13 -0
  27. package/dist/cli/resolve-node-command.js +174 -0
  28. package/dist/cli/run-network-command.d.ts +14 -0
  29. package/dist/cli/run-network-command.js +418 -0
  30. package/dist/cli/status-command.d.ts +5 -0
  31. package/dist/cli/status-command.js +137 -0
  32. package/dist/cli/validate-command.d.ts +6 -0
  33. package/dist/cli/validate-command.js +17 -0
  34. package/dist/dag/descendants.d.ts +9 -0
  35. package/dist/dag/descendants.js +67 -0
  36. package/dist/dag/topological-sort.d.ts +7 -0
  37. package/dist/dag/topological-sort.js +32 -0
  38. package/dist/domain/business-key.d.ts +15 -0
  39. package/dist/domain/business-key.js +249 -0
  40. package/dist/domain/canonical-lock-key.d.ts +17 -0
  41. package/dist/domain/canonical-lock-key.js +60 -0
  42. package/dist/domain/canonical-record-key.d.ts +18 -0
  43. package/dist/domain/canonical-record-key.js +70 -0
  44. package/dist/domain/load-network.d.ts +4 -0
  45. package/dist/domain/load-network.js +47 -0
  46. package/dist/domain/network-definition.d.ts +47 -0
  47. package/dist/domain/network-definition.js +7 -0
  48. package/dist/domain/persistence-model.d.ts +198 -0
  49. package/dist/domain/persistence-model.js +2 -0
  50. package/dist/domain/run-aggregate.d.ts +3 -0
  51. package/dist/domain/run-aggregate.js +15 -0
  52. package/dist/domain/validate-network-path.d.ts +11 -0
  53. package/dist/domain/validate-network-path.js +33 -0
  54. package/dist/domain/validate-network.d.ts +3 -0
  55. package/dist/domain/validate-network.js +180 -0
  56. package/dist/executor/attempt-executor.d.ts +43 -0
  57. package/dist/executor/attempt-executor.js +236 -0
  58. package/dist/executor/job-log-reader.d.ts +37 -0
  59. package/dist/executor/job-log-reader.js +91 -0
  60. package/dist/executor/ksql-flow-cli.d.ts +86 -0
  61. package/dist/executor/ksql-flow-cli.js +161 -0
  62. package/dist/executor/preflight.d.ts +41 -0
  63. package/dist/executor/preflight.js +150 -0
  64. package/dist/executor/result-classifier.d.ts +69 -0
  65. package/dist/executor/result-classifier.js +237 -0
  66. package/dist/executor/run-subprocess.d.ts +64 -0
  67. package/dist/executor/run-subprocess.js +175 -0
  68. package/dist/io/input-baseline.d.ts +19 -0
  69. package/dist/io/input-baseline.js +214 -0
  70. package/dist/io/io-config.d.ts +6 -0
  71. package/dist/io/io-config.js +33 -0
  72. package/dist/io/io-path.d.ts +37 -0
  73. package/dist/io/io-path.js +279 -0
  74. package/dist/orchestration/archive-run.d.ts +53 -0
  75. package/dist/orchestration/archive-run.js +323 -0
  76. package/dist/orchestration/cancel-request.d.ts +18 -0
  77. package/dist/orchestration/cancel-request.js +67 -0
  78. package/dist/orchestration/ensure-run.d.ts +83 -0
  79. package/dist/orchestration/ensure-run.js +632 -0
  80. package/dist/orchestration/job-lock-audit.d.ts +24 -0
  81. package/dist/orchestration/job-lock-audit.js +123 -0
  82. package/dist/orchestration/orphan-attempt-adjudication.d.ts +39 -0
  83. package/dist/orchestration/orphan-attempt-adjudication.js +167 -0
  84. package/dist/orchestration/reconciliation.d.ts +41 -0
  85. package/dist/orchestration/reconciliation.js +526 -0
  86. package/dist/orchestration/resolve-node.d.ts +29 -0
  87. package/dist/orchestration/resolve-node.js +105 -0
  88. package/dist/orchestration/run-activity.d.ts +13 -0
  89. package/dist/orchestration/run-activity.js +14 -0
  90. package/dist/orchestration/sequential-scheduler.d.ts +79 -0
  91. package/dist/orchestration/sequential-scheduler.js +706 -0
  92. package/dist/orchestration/status.d.ts +91 -0
  93. package/dist/orchestration/status.js +171 -0
  94. package/dist/persistence/in-memory-repository.d.ts +41 -0
  95. package/dist/persistence/in-memory-repository.js +333 -0
  96. package/dist/persistence/kintone/client.d.ts +36 -0
  97. package/dist/persistence/kintone/client.js +93 -0
  98. package/dist/persistence/kintone/design-notes.d.ts +38 -0
  99. package/dist/persistence/kintone/design-notes.js +38 -0
  100. package/dist/persistence/kintone/repository.d.ts +44 -0
  101. package/dist/persistence/kintone/repository.js +836 -0
  102. package/dist/persistence/kintone/schema.d.ts +116 -0
  103. package/dist/persistence/kintone/schema.js +115 -0
  104. package/dist/persistence/network-lock-reader.d.ts +24 -0
  105. package/dist/persistence/network-lock-reader.js +43 -0
  106. package/dist/persistence/network-lock-recovery.d.ts +66 -0
  107. package/dist/persistence/network-lock-recovery.js +392 -0
  108. package/dist/persistence/network-lock.d.ts +97 -0
  109. package/dist/persistence/network-lock.js +408 -0
  110. package/dist/persistence/repository.d.ts +101 -0
  111. package/dist/persistence/repository.js +11 -0
  112. package/dist/persistence/state-transition.d.ts +4 -0
  113. package/dist/persistence/state-transition.js +25 -0
  114. package/dist/requests/flownet-child-client.d.ts +78 -0
  115. package/dist/requests/flownet-child-client.js +243 -0
  116. package/dist/requests/kintone-request-store.d.ts +44 -0
  117. package/dist/requests/kintone-request-store.js +202 -0
  118. package/dist/requests/poll-requests-config.d.ts +26 -0
  119. package/dist/requests/poll-requests-config.js +105 -0
  120. package/dist/requests/request-model.d.ts +57 -0
  121. package/dist/requests/request-model.js +305 -0
  122. package/dist/requests/request-poller.d.ts +33 -0
  123. package/dist/requests/request-poller.js +353 -0
  124. package/dist/requests/request-result.d.ts +18 -0
  125. package/dist/requests/request-result.js +215 -0
  126. package/dist/requests/start-request.d.ts +26 -0
  127. package/dist/requests/start-request.js +83 -0
  128. package/package.json +63 -0
  129. package/schemas/.gitkeep +1 -0
  130. package/schemas/network-definition.schema.json +121 -0
@@ -0,0 +1,418 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync } from "node:fs";
3
+ import { hostname } from "node:os";
4
+ import { resolve } from "node:path";
5
+ import { downloadBundle, readStoreZip, uploadBundle } from "../bundle/index.js";
6
+ import { loadNetworkDefinition, loadNetworkDefinitionSource, } from "../domain/load-network.js";
7
+ import { AttemptExecutor } from "../executor/attempt-executor.js";
8
+ import { KintoneJobLogReader } from "../executor/job-log-reader.js";
9
+ import { KsqlFlowCli } from "../executor/ksql-flow-cli.js";
10
+ import { RunSubprocess } from "../executor/run-subprocess.js";
11
+ import { loadIoConfig } from "../io/io-config.js";
12
+ import { ensureRun, EnsureRunError, } from "../orchestration/ensure-run.js";
13
+ import { runSequentialScheduler, } from "../orchestration/sequential-scheduler.js";
14
+ import { KintonePersistenceRepository } from "../persistence/kintone/repository.js";
15
+ import { LeaseMonitor, NetworkLockManager, } from "../persistence/network-lock.js";
16
+ export async function runRunNetworkCommand(args, dependencies) {
17
+ const parsed = parseRunNetworkArguments(args);
18
+ let created;
19
+ if (parsed.errors.length > 0 || parsed.networkPath === undefined) {
20
+ if (parsed.json) {
21
+ writeJsonResult({
22
+ outcome: "REJECTED",
23
+ run_id: parsed.resumeRunId ?? null,
24
+ invocation_id: null,
25
+ aggregate_status: null,
26
+ invocation_result_code: "INVALID_ARGUMENTS",
27
+ retry_brake_node_ids: [],
28
+ blocked_run_ids: [],
29
+ });
30
+ }
31
+ else {
32
+ process.stderr.write(`Invalid run-network arguments:\n${parsed.errors.map((error) => `- ${error}`).join("\n")}\n`);
33
+ }
34
+ return 1;
35
+ }
36
+ try {
37
+ const runtime = dependencies ?? productionDependencies(parsed);
38
+ const result = await runtime.invoke({
39
+ networkPath: parsed.networkPath,
40
+ profile: runtime.profile,
41
+ requestedBy: runtime.requestedBy,
42
+ host: runtime.host,
43
+ resume: parsed.resume,
44
+ ...(parsed.scheduledFor === undefined
45
+ ? {}
46
+ : { scheduledFor: parsed.scheduledFor }),
47
+ ...(parsed.businessKey === undefined
48
+ ? {}
49
+ : { businessKey: parsed.businessKey }),
50
+ ...(parsed.resumeRunId === undefined
51
+ ? {}
52
+ : { resumeRunId: parsed.resumeRunId }),
53
+ ...(parsed.rerunFrom === undefined
54
+ ? {}
55
+ : { rerunFrom: parsed.rerunFrom }),
56
+ });
57
+ if (result.outcome === "NOOP") {
58
+ if (parsed.json) {
59
+ writeJsonResult({
60
+ outcome: "NOOP",
61
+ run_id: result.run.value.run_id,
62
+ invocation_id: null,
63
+ aggregate_status: "SUCCESS",
64
+ invocation_result_code: "NOOP_ALREADY_SUCCESS",
65
+ retry_brake_node_ids: [],
66
+ blocked_run_ids: [],
67
+ });
68
+ }
69
+ else {
70
+ process.stdout.write(`NO-OP: Run ${result.run.value.run_id} is already SUCCESS; nothing was executed.\n`);
71
+ }
72
+ return 0;
73
+ }
74
+ created = {
75
+ outcome: result.outcome,
76
+ runId: result.run.value.run_id,
77
+ invocationId: result.invocation.value.invocation_id,
78
+ };
79
+ const summary = await runtime.schedule(result);
80
+ if (parsed.json) {
81
+ writeJsonResult({
82
+ outcome: result.outcome,
83
+ run_id: result.run.value.run_id,
84
+ invocation_id: result.invocation.value.invocation_id,
85
+ aggregate_status: summary.aggregateStatus,
86
+ invocation_result_code: summary.invocationResultCode,
87
+ retry_brake_node_ids: summary.retryBrakeNodeIds,
88
+ blocked_run_ids: [],
89
+ });
90
+ }
91
+ else {
92
+ process.stdout.write(`${result.outcome}: Run ${result.run.value.run_id} finished with aggregate ${summary.aggregateStatus} (${summary.invocationResultCode}).\n`);
93
+ }
94
+ return summary.aggregateStatus === "SUCCESS" ? 0 : 1;
95
+ }
96
+ catch (error) {
97
+ const code = errorCode(error);
98
+ const message = error instanceof Error ? error.message : String(error);
99
+ if (parsed.json) {
100
+ writeJsonResult({
101
+ outcome: created?.outcome ?? "REJECTED",
102
+ run_id: created?.runId ?? parsed.resumeRunId ?? null,
103
+ invocation_id: created?.invocationId ?? null,
104
+ aggregate_status: null,
105
+ invocation_result_code: code,
106
+ retry_brake_node_ids: [],
107
+ blocked_run_ids: error instanceof EnsureRunError ? error.blockedBy : [],
108
+ });
109
+ }
110
+ else {
111
+ process.stderr.write(`Error [${code}]: ${message}\n`);
112
+ }
113
+ if (!parsed.json &&
114
+ error instanceof EnsureRunError &&
115
+ error.blockedBy.length > 0) {
116
+ process.stderr.write(`Blocking run_id(s): ${error.blockedBy.join(", ")}\n`);
117
+ }
118
+ return 1;
119
+ }
120
+ }
121
+ function parseRunNetworkArguments(args) {
122
+ let networkPath;
123
+ let scheduledFor;
124
+ let businessKey;
125
+ let resumeRunId;
126
+ let rerunFrom;
127
+ let ksqlFlowBin;
128
+ let ksqlFlowConfig;
129
+ let ksqlFlowWorkdir;
130
+ let resume = false;
131
+ let json = false;
132
+ const errors = [];
133
+ for (let index = 0; index < args.length; index += 1) {
134
+ const argument = args[index];
135
+ if (argument === "--json") {
136
+ if (json)
137
+ errors.push("--json was specified more than once");
138
+ json = true;
139
+ continue;
140
+ }
141
+ if (argument === "--resume") {
142
+ if (resume)
143
+ errors.push("--resume was specified more than once");
144
+ resume = true;
145
+ continue;
146
+ }
147
+ if (argument === "--scheduled-for" ||
148
+ argument === "--business-key" ||
149
+ argument === "--resume-run" ||
150
+ argument === "--rerun-from" ||
151
+ argument === "--ksql-flow-bin" ||
152
+ argument === "--ksql-flow-config" ||
153
+ argument === "--ksql-flow-workdir") {
154
+ const value = args[index + 1];
155
+ if (value === undefined || value.startsWith("--")) {
156
+ errors.push(`${argument} requires a value`);
157
+ continue;
158
+ }
159
+ if (argument === "--scheduled-for") {
160
+ if (scheduledFor !== undefined)
161
+ errors.push(`${argument} was specified more than once`);
162
+ scheduledFor = value;
163
+ }
164
+ else if (argument === "--business-key") {
165
+ if (businessKey !== undefined)
166
+ errors.push(`${argument} was specified more than once`);
167
+ businessKey = value;
168
+ }
169
+ else if (argument === "--resume-run") {
170
+ if (resumeRunId !== undefined)
171
+ errors.push(`${argument} was specified more than once`);
172
+ resumeRunId = value;
173
+ }
174
+ else if (argument === "--rerun-from") {
175
+ if (rerunFrom !== undefined)
176
+ errors.push(`${argument} was specified more than once`);
177
+ rerunFrom = value;
178
+ }
179
+ else if (argument === "--ksql-flow-bin") {
180
+ if (ksqlFlowBin !== undefined)
181
+ errors.push(`${argument} was specified more than once`);
182
+ ksqlFlowBin = value;
183
+ }
184
+ else if (argument === "--ksql-flow-config") {
185
+ if (ksqlFlowConfig !== undefined)
186
+ errors.push(`${argument} was specified more than once`);
187
+ ksqlFlowConfig = value;
188
+ }
189
+ else {
190
+ if (ksqlFlowWorkdir !== undefined)
191
+ errors.push(`${argument} was specified more than once`);
192
+ ksqlFlowWorkdir = value;
193
+ }
194
+ index += 1;
195
+ continue;
196
+ }
197
+ if (argument.startsWith("-"))
198
+ errors.push(`unknown option '${argument}'`);
199
+ else if (networkPath === undefined)
200
+ networkPath = argument;
201
+ else
202
+ errors.push(`unexpected argument '${argument}'`);
203
+ }
204
+ if (networkPath === undefined)
205
+ errors.push("run-network requires one <network> path");
206
+ if (resumeRunId !== undefined &&
207
+ (scheduledFor !== undefined || businessKey !== undefined || resume)) {
208
+ errors.push("--resume-run must not be combined with --resume, --scheduled-for, or --business-key");
209
+ }
210
+ if (rerunFrom !== undefined && resumeRunId === undefined && !resume) {
211
+ errors.push("--rerun-from requires --resume-run or --resume");
212
+ }
213
+ return {
214
+ resume,
215
+ json,
216
+ errors,
217
+ ...(networkPath === undefined ? {} : { networkPath }),
218
+ ...(scheduledFor === undefined ? {} : { scheduledFor }),
219
+ ...(businessKey === undefined ? {} : { businessKey }),
220
+ ...(resumeRunId === undefined ? {} : { resumeRunId }),
221
+ ...(rerunFrom === undefined ? {} : { rerunFrom }),
222
+ ...(ksqlFlowBin === undefined ? {} : { ksqlFlowBin }),
223
+ ...(ksqlFlowConfig === undefined ? {} : { ksqlFlowConfig }),
224
+ ...(ksqlFlowWorkdir === undefined ? {} : { ksqlFlowWorkdir }),
225
+ };
226
+ }
227
+ function writeJsonResult(result) {
228
+ process.stdout.write(`${JSON.stringify(result)}\n`);
229
+ }
230
+ function productionDependencies(parsed) {
231
+ const profile = requiredEnvironment("KSQL_FLOWNET_PROFILE");
232
+ const baseUrl = requiredEnvironment("KSQL_FLOWNET_BASE_URL").replace(/\/$/u, "");
233
+ const stateAppId = positiveIntegerEnvironment("KSQL_FLOWNET_STATE_APP_ID");
234
+ const auditAppId = positiveIntegerEnvironment("KSQL_FLOWNET_AUDIT_APP_ID");
235
+ const stateApiToken = requiredEnvironment("KSQL_FLOWNET_STATE_API_TOKEN");
236
+ const auditApiToken = requiredEnvironment("KSQL_FLOWNET_AUDIT_API_TOKEN");
237
+ const command = parsed.ksqlFlowBin ?? requiredEnvironment("KSQL_FLOW_BIN");
238
+ const binArgs = ksqlFlowBinArgsEnvironment();
239
+ const configPath = resolve(parsed.ksqlFlowConfig ?? requiredEnvironment("KSQL_FLOW_CONFIG"));
240
+ const executionDirectory = resolve(parsed.ksqlFlowWorkdir ?? requiredEnvironment("KSQL_FLOW_WORKDIR"));
241
+ const jobLogAppId = positiveIntegerEnvironment("KSQL_FLOW_LOG_APP_ID");
242
+ const jobLogApiToken = requiredEnvironment("KSQL_FLOW_LOG_API_TOKEN");
243
+ const gracePeriodMs = optionalNonNegativeIntegerEnvironment("KSQL_FLOW_GRACE_PERIOD_MS", 30_000);
244
+ mkdirSync(executionDirectory, { recursive: true });
245
+ const requestedBy = process.env.KSQL_FLOWNET_REQUESTED_BY ?? process.env.USERNAME ?? "unknown";
246
+ const host = process.env.KSQL_FLOWNET_HOST ?? hostname();
247
+ const ownerInstanceId = resolveOwnerInstanceId(host);
248
+ let resources;
249
+ return {
250
+ profile,
251
+ requestedBy,
252
+ host,
253
+ async invoke(input) {
254
+ const invocationId = `invoke_${randomUUID()}`;
255
+ const loaded = loadNetworkDefinition(input.networkPath);
256
+ if (loaded.definition === undefined || loaded.errors.length > 0) {
257
+ throw new Error("network definition is invalid");
258
+ }
259
+ let ioConfig;
260
+ const repository = new KintonePersistenceRepository({
261
+ baseUrl,
262
+ stateAppId,
263
+ stateApiToken,
264
+ auditAppId,
265
+ auditApiToken,
266
+ });
267
+ const lockManager = new NetworkLockManager({
268
+ baseUrl,
269
+ appId: stateAppId,
270
+ apiToken: stateApiToken,
271
+ profile,
272
+ networkId: loaded.definition.network_id,
273
+ ownerInvocationId: invocationId,
274
+ ownerInstanceId,
275
+ leaseDurationSec: loaded.definition.network_lock.lease_duration_sec,
276
+ });
277
+ const executor = new KsqlFlowCli({
278
+ command,
279
+ binArgs,
280
+ profile,
281
+ configPath,
282
+ });
283
+ const endpoint = `${baseUrl}/k/v1/file.json`;
284
+ const headers = { "X-Cybozu-API-Token": stateApiToken };
285
+ const ensured = await ensureRun({
286
+ ...input,
287
+ invocationId,
288
+ repository,
289
+ lockManager,
290
+ executor,
291
+ beforeLock(definition) {
292
+ if (definition.nodes.some((node) => Object.keys(node.inputs ?? {}).length > 0 ||
293
+ Object.keys(node.outputs ?? {}).length > 0)) {
294
+ ioConfig = loadIoConfig();
295
+ return ioConfig;
296
+ }
297
+ return undefined;
298
+ },
299
+ bundleStore: {
300
+ upload: (zipBytes) => uploadBundle({ endpoint, zipBytes, fetch, headers }),
301
+ download: (fileKey) => downloadBundle({ endpoint, fileKey, fetch, headers }),
302
+ },
303
+ });
304
+ resources = {
305
+ repository,
306
+ lockManager,
307
+ configPath,
308
+ ...(ioConfig === undefined ? {} : { ioRoot: ioConfig.root }),
309
+ };
310
+ return ensured;
311
+ },
312
+ async schedule(result) {
313
+ if (result.outcome === "NOOP")
314
+ throw new Error("NO-OP run cannot be scheduled");
315
+ if (resources === undefined)
316
+ throw new Error("scheduler resources are missing");
317
+ const networkBytes = readStoreZip(result.bundleBytes).find(({ name }) => name === "network.yaml")?.data;
318
+ if (networkBytes === undefined)
319
+ throw new Error("bundle has no network.yaml");
320
+ const loaded = loadNetworkDefinitionSource(networkBytes.toString("utf8"));
321
+ if (loaded.definition === undefined || loaded.errors.length > 0)
322
+ throw new Error("stored network definition is invalid");
323
+ const monitor = new LeaseMonitor(resources.lockManager, result.lock, {
324
+ leaseDurationSec: loaded.definition.network_lock.lease_duration_sec,
325
+ heartbeatIntervalSec: loaded.definition.network_lock.heartbeat_interval_sec,
326
+ });
327
+ const runner = new RunSubprocess({
328
+ command,
329
+ binArgs,
330
+ executionDirectory,
331
+ timeoutMs: result.run.value.resolved_profile_snapshot.limits
332
+ .batch_timeout_sec === null
333
+ ? null
334
+ : result.run.value.resolved_profile_snapshot.limits
335
+ .batch_timeout_sec * 1000,
336
+ gracePeriodMs,
337
+ });
338
+ const jobLogReader = new KintoneJobLogReader({
339
+ baseUrl,
340
+ appId: jobLogAppId,
341
+ apiToken: jobLogApiToken,
342
+ });
343
+ return runSequentialScheduler({
344
+ run: result.run,
345
+ invocation: result.invocation,
346
+ bundleBytes: result.bundleBytes,
347
+ repository: resources.repository,
348
+ jobLogReader,
349
+ attemptExecutor: new AttemptExecutor({
350
+ repository: resources.repository,
351
+ runner,
352
+ jobLogReader,
353
+ }),
354
+ leaseMonitor: monitor,
355
+ profile,
356
+ configPath: resources.configPath,
357
+ executionRoot: executionDirectory,
358
+ ...(resources.ioRoot === undefined ? {} : { ioRoot: resources.ioRoot }),
359
+ close: (finalization) => result.close(finalization),
360
+ });
361
+ },
362
+ };
363
+ }
364
+ export function resolveOwnerInstanceId(host, configured = process.env.KSQL_FLOWNET_OWNER_INSTANCE_ID, pid = process.pid) {
365
+ return configured ?? `local-pid://${host}/${pid}`;
366
+ }
367
+ export function ksqlFlowBinArgsEnvironment(environment = process.env) {
368
+ const raw = environment.KSQL_FLOW_BIN_ARGS?.trim();
369
+ if (!raw)
370
+ return [];
371
+ if (!raw.startsWith("["))
372
+ return raw.split(/\s+/u);
373
+ let parsed;
374
+ try {
375
+ parsed = JSON.parse(raw);
376
+ }
377
+ catch (error) {
378
+ throw new Error("KSQL_FLOW_BIN_ARGS must be a valid JSON string array", {
379
+ cause: error,
380
+ });
381
+ }
382
+ if (!Array.isArray(parsed) ||
383
+ !parsed.every((value) => typeof value === "string")) {
384
+ throw new Error("KSQL_FLOW_BIN_ARGS must be a JSON string array");
385
+ }
386
+ return parsed;
387
+ }
388
+ function requiredEnvironment(name) {
389
+ const value = process.env[name];
390
+ if (value === undefined || value.trim() === "")
391
+ throw new Error(`${name} is required`);
392
+ return value;
393
+ }
394
+ function positiveIntegerEnvironment(name) {
395
+ const value = Number(requiredEnvironment(name));
396
+ if (!Number.isSafeInteger(value) || value <= 0)
397
+ throw new Error(`${name} must be a positive integer`);
398
+ return value;
399
+ }
400
+ function optionalNonNegativeIntegerEnvironment(name, fallback) {
401
+ const raw = process.env[name];
402
+ if (raw === undefined || raw.trim() === "")
403
+ return fallback;
404
+ const value = Number(raw);
405
+ if (!Number.isSafeInteger(value) || value < 0)
406
+ throw new Error(`${name} must be a non-negative integer`);
407
+ return value;
408
+ }
409
+ function errorCode(error) {
410
+ if (typeof error === "object" &&
411
+ error !== null &&
412
+ "code" in error &&
413
+ typeof error.code === "string") {
414
+ return error.code;
415
+ }
416
+ return "RUN_NETWORK_FAILED";
417
+ }
418
+ //# sourceMappingURL=run-network-command.js.map
@@ -0,0 +1,5 @@
1
+ import { type StatusDependencies, type StatusOutput } from "../orchestration/status.js";
2
+ export type StatusCommandDependencies = StatusDependencies;
3
+ export declare function runStatusCommand(args: readonly string[], dependencies?: StatusCommandDependencies): Promise<number>;
4
+ export declare function renderStatusText(output: StatusOutput): string;
5
+ //# sourceMappingURL=status-command.d.ts.map
@@ -0,0 +1,137 @@
1
+ import { inspectStatus, } from "../orchestration/status.js";
2
+ import { KintoneNetworkLockStatusReader } from "../persistence/network-lock-reader.js";
3
+ import { repositoryFromEnvironment, requiredEnvironment, } from "./resolve-node-command.js";
4
+ export async function runStatusCommand(args, dependencies) {
5
+ const parsed = parseStatusArguments(args);
6
+ if (parsed.errors.length > 0) {
7
+ process.stderr.write(`Invalid status arguments:\n${parsed.errors.map((error) => `- ${error}`).join("\n")}\n`);
8
+ return 1;
9
+ }
10
+ try {
11
+ const result = await inspectStatus({
12
+ networkId: parsed.networkId,
13
+ profile: parsed.profile,
14
+ ...(parsed.runId === undefined ? {} : { runId: parsed.runId }),
15
+ ...(parsed.businessKey === undefined
16
+ ? {}
17
+ : { businessKey: parsed.businessKey }),
18
+ }, dependencies ?? productionDependencies());
19
+ process.stdout.write(parsed.json
20
+ ? `${JSON.stringify(result, null, 2)}\n`
21
+ : renderStatusText(result));
22
+ return 0;
23
+ }
24
+ catch (error) {
25
+ const code = typeof error === "object" &&
26
+ error !== null &&
27
+ "code" in error &&
28
+ typeof error.code === "string"
29
+ ? error.code
30
+ : "STATUS_FAILED";
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ process.stderr.write(`Error [${code}]: ${message}\n`);
33
+ return 1;
34
+ }
35
+ }
36
+ function parseStatusArguments(args) {
37
+ const errors = [];
38
+ const positional = [];
39
+ const values = new Map();
40
+ let json = false;
41
+ const valueOptions = new Set(["--profile", "--run-id", "--business-key"]);
42
+ for (let index = 0; index < args.length; index += 1) {
43
+ const argument = args[index];
44
+ if (argument === "--json") {
45
+ if (json)
46
+ errors.push("--json was specified more than once");
47
+ json = true;
48
+ continue;
49
+ }
50
+ if (!argument.startsWith("--")) {
51
+ positional.push(argument);
52
+ continue;
53
+ }
54
+ if (!valueOptions.has(argument)) {
55
+ errors.push(`unknown option '${argument}'`);
56
+ continue;
57
+ }
58
+ const value = args[index + 1];
59
+ if (value === undefined || value.startsWith("--")) {
60
+ errors.push(`${argument} requires a value`);
61
+ continue;
62
+ }
63
+ if (values.has(argument)) {
64
+ errors.push(`${argument} was specified more than once`);
65
+ }
66
+ values.set(argument, value);
67
+ index += 1;
68
+ }
69
+ if (positional.length === 0)
70
+ errors.push("<network_id> is required");
71
+ for (const argument of positional.slice(1)) {
72
+ errors.push(`unexpected argument '${argument}'`);
73
+ }
74
+ if (!values.has("--profile"))
75
+ errors.push("--profile is required");
76
+ if (values.has("--run-id") && values.has("--business-key")) {
77
+ errors.push("--run-id and --business-key are mutually exclusive");
78
+ }
79
+ return {
80
+ json,
81
+ errors,
82
+ ...(positional[0] === undefined ? {} : { networkId: positional[0] }),
83
+ ...(values.has("--profile") ? { profile: values.get("--profile") } : {}),
84
+ ...(values.has("--run-id") ? { runId: values.get("--run-id") } : {}),
85
+ ...(values.has("--business-key")
86
+ ? { businessKey: values.get("--business-key") }
87
+ : {}),
88
+ };
89
+ }
90
+ export function renderStatusText(output) {
91
+ const lines = [`Network: ${output.network_id}`, `Profile: ${output.profile}`];
92
+ if (output.lock === null) {
93
+ lines.push("Lock: none");
94
+ }
95
+ else {
96
+ lines.push("Lock:");
97
+ for (const [key, value] of Object.entries(output.lock)) {
98
+ lines.push(` ${key}: ${String(value)}`);
99
+ }
100
+ }
101
+ lines.push(`Runs: ${output.runs.length}`);
102
+ for (const run of output.runs) {
103
+ lines.push(`- run_id: ${run.run_id}`);
104
+ for (const [key, value] of Object.entries(run)) {
105
+ if (key === "run_id")
106
+ continue;
107
+ if (typeof value === "object" && value !== null) {
108
+ lines.push(` ${key}: ${JSON.stringify(value)}`);
109
+ }
110
+ else {
111
+ lines.push(` ${key}: ${String(value)}`);
112
+ }
113
+ }
114
+ }
115
+ return `${lines.join("\n")}\n`;
116
+ }
117
+ function productionDependencies() {
118
+ const baseUrl = requiredEnvironment("KSQL_FLOWNET_BASE_URL").replace(/\/$/u, "");
119
+ const stateAppId = positiveIntegerEnvironment("KSQL_FLOWNET_STATE_APP_ID");
120
+ const stateApiToken = requiredEnvironment("KSQL_FLOWNET_STATE_API_TOKEN");
121
+ return {
122
+ repository: repositoryFromEnvironment(),
123
+ lockReader: new KintoneNetworkLockStatusReader({
124
+ baseUrl,
125
+ stateAppId,
126
+ stateApiToken,
127
+ }),
128
+ };
129
+ }
130
+ function positiveIntegerEnvironment(name) {
131
+ const value = Number(requiredEnvironment(name));
132
+ if (!Number.isSafeInteger(value) || value <= 0) {
133
+ throw new Error(`${name} must be a positive integer`);
134
+ }
135
+ return value;
136
+ }
137
+ //# sourceMappingURL=status-command.js.map
@@ -0,0 +1,6 @@
1
+ import type { ValidationError } from "../domain/network-definition.js";
2
+ import { validateNetworkPath, validateSqlFiles } from "../domain/validate-network-path.js";
3
+ export { validateNetworkPath, validateSqlFiles };
4
+ export declare function runValidateCommand(networkPath: string): number;
5
+ export declare function writeValidationErrors(errors: readonly ValidationError[]): void;
6
+ //# sourceMappingURL=validate-command.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { validateNetworkPath, validateSqlFiles, } from "../domain/validate-network-path.js";
2
+ export { validateNetworkPath, validateSqlFiles };
3
+ export function runValidateCommand(networkPath) {
4
+ const validated = validateNetworkPath(networkPath);
5
+ if (validated.errors.length > 0) {
6
+ writeValidationErrors(validated.errors);
7
+ return 1;
8
+ }
9
+ process.stdout.write(`Valid network definition: ${networkPath}\n`);
10
+ return 0;
11
+ }
12
+ export function writeValidationErrors(errors) {
13
+ process.stderr.write(`Network validation failed with ${errors.length} error(s):\n${errors
14
+ .map((error) => `- [${error.code}] ${error.path}: ${error.message}`)
15
+ .join("\n")}\n`);
16
+ }
17
+ //# sourceMappingURL=validate-command.js.map
@@ -0,0 +1,9 @@
1
+ import type { NetworkDefinition } from "../domain/network-definition.js";
2
+ export type DescendantsErrorCode = "DESCENDANTS_NODE_NOT_FOUND" | "DESCENDANTS_DAG_INVALID";
3
+ export declare class DescendantsError extends Error {
4
+ readonly code: DescendantsErrorCode;
5
+ constructor(code: DescendantsErrorCode, message: string);
6
+ }
7
+ /** Returns the selected node and all of its transitive descendants in definition order. */
8
+ export declare function descendantsIncludingSelf(definition: NetworkDefinition, nodeId: string): readonly string[];
9
+ //# sourceMappingURL=descendants.d.ts.map
@@ -0,0 +1,67 @@
1
+ export class DescendantsError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "DescendantsError";
7
+ }
8
+ }
9
+ /** Returns the selected node and all of its transitive descendants in definition order. */
10
+ export function descendantsIncludingSelf(definition, nodeId) {
11
+ const nodesById = new Map();
12
+ for (const node of definition.nodes) {
13
+ if (nodesById.has(node.id)) {
14
+ throw new DescendantsError("DESCENDANTS_DAG_INVALID", `snapshot DAG has duplicate node id '${node.id}'`);
15
+ }
16
+ nodesById.set(node.id, node);
17
+ }
18
+ if (!nodesById.has(nodeId)) {
19
+ throw new DescendantsError("DESCENDANTS_NODE_NOT_FOUND", `node '${nodeId}' does not exist in the snapshot DAG`);
20
+ }
21
+ const dependents = new Map(definition.nodes.map((node) => [node.id, []]));
22
+ const indegree = new Map(definition.nodes.map((node) => [node.id, node.depends_on.length]));
23
+ for (const node of definition.nodes) {
24
+ const dependencies = new Set();
25
+ for (const dependency of node.depends_on) {
26
+ if (dependency === node.id ||
27
+ dependencies.has(dependency) ||
28
+ !nodesById.has(dependency)) {
29
+ throw new DescendantsError("DESCENDANTS_DAG_INVALID", `snapshot DAG has an invalid dependency '${dependency}' for node '${node.id}'`);
30
+ }
31
+ dependencies.add(dependency);
32
+ dependents.get(dependency).push(node.id);
33
+ }
34
+ }
35
+ const ready = definition.nodes
36
+ .filter((node) => indegree.get(node.id) === 0)
37
+ .map((node) => node.id);
38
+ let visited = 0;
39
+ while (ready.length > 0) {
40
+ const current = ready.shift();
41
+ visited += 1;
42
+ for (const dependent of dependents.get(current)) {
43
+ const remaining = indegree.get(dependent) - 1;
44
+ indegree.set(dependent, remaining);
45
+ if (remaining === 0)
46
+ ready.push(dependent);
47
+ }
48
+ }
49
+ if (visited !== definition.nodes.length) {
50
+ throw new DescendantsError("DESCENDANTS_DAG_INVALID", "snapshot DAG contains a cycle");
51
+ }
52
+ const selected = new Set([nodeId]);
53
+ const queue = [nodeId];
54
+ while (queue.length > 0) {
55
+ const current = queue.shift();
56
+ for (const dependent of dependents.get(current)) {
57
+ if (selected.has(dependent))
58
+ continue;
59
+ selected.add(dependent);
60
+ queue.push(dependent);
61
+ }
62
+ }
63
+ return definition.nodes
64
+ .map((node) => node.id)
65
+ .filter((id) => selected.has(id));
66
+ }
67
+ //# sourceMappingURL=descendants.js.map
@@ -0,0 +1,7 @@
1
+ import type { NetworkNodeDefinition } from "../domain/network-definition.js";
2
+ export interface TopologicalSortResult {
3
+ readonly order: readonly string[];
4
+ readonly cyclicNodeIds: readonly string[];
5
+ }
6
+ export declare function stableTopologicalSort(nodes: readonly NetworkNodeDefinition[]): TopologicalSortResult;
7
+ //# sourceMappingURL=topological-sort.d.ts.map