clyops-jobs 0.9.0 → 0.11.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/README.md +87 -1
- package/clyops-jobs.cjs +195 -35
- package/config.schema.json +113 -0
- package/dist/artifacts.d.ts +2 -2
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +3 -3
- package/dist/functions.d.ts +91 -12
- package/dist/functions.d.ts.map +1 -1
- package/dist/functions.js +179 -15
- package/dist/template.d.ts +23 -6
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +71 -16
- package/package.json +9 -6
- package/src/artifacts.ts +4 -4
- package/src/functions.ts +244 -29
- package/src/template.ts +71 -18
package/README.md
CHANGED
|
@@ -27,6 +27,19 @@ and no `node_modules`. It includes clyops-tools. `require('clyops-jobs')` loads
|
|
|
27
27
|
}
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
**Editor support.** The config has a JSON Schema, so editors complete and check it. Name it in
|
|
31
|
+
the file, from the site or from the installed package:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{ "$schema": "https://clyops.io/schemas/clyops-jobs.json", "functions": {} }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
It's `config.schema.json` in the package (`clyops-jobs/config.schema.json`, e.g.
|
|
38
|
+
`"./node_modules/clyops-jobs/config.schema.json"`). It catches typos in function and step
|
|
39
|
+
fields (`x-` fields are free for your own notes), `script` and `steps` together, and bad step
|
|
40
|
+
names; that a branch names a later step is checked when the function is resolved. Options
|
|
41
|
+
blocks are any object, since their keys come from each tool's schema.
|
|
42
|
+
|
|
30
43
|
- `functions.NAME.script` is the tool, relative to a script root.
|
|
31
44
|
- `key` names the options block (default: the function name). Options are
|
|
32
45
|
`deepMerge(defaults[key], config[key])`, keyed like the tool's options (`out_dir`, `out-dir`)
|
|
@@ -35,7 +48,12 @@ and no `node_modules`. It includes clyops-tools. `require('clyops-jobs')` loads
|
|
|
35
48
|
- `positionals` (optional) lists the tool's positional arguments explicitly; otherwise they're
|
|
36
49
|
options keyed by argument name.
|
|
37
50
|
- String values may use `${dot.path}` or `{{dot.path}}` templates against a context you build
|
|
38
|
-
(`job`, `paths`, whatever the trigger knows).
|
|
51
|
+
(`job`, `paths`, whatever the trigger knows). An unknown path is an error, so a typo fails
|
|
52
|
+
loudly, except under `params` (the caller's fields): an option or positional that names a
|
|
53
|
+
missing or null `params` field is left unset, so the tool's default applies. That holds for
|
|
54
|
+
`${params.x}` alone and inside a longer string (`v-${params.x}`), so a value is never half
|
|
55
|
+
filled. Elsewhere (`result`, `stdin`, `stdout`, artifact patterns) a missing field renders as
|
|
56
|
+
`''`. `strict_params: true` on a function makes a missing `params` field an error too.
|
|
39
57
|
- `artifacts`: glob patterns (`*`, `?`, `**`) relative to `paths.work_dir`, copied into
|
|
40
58
|
`paths.artifacts_dir`, with an optional `required: true` or `min: N`.
|
|
41
59
|
- `stdin`: a template naming a file fed to the tool's stdin (`"${media.path}"`; relative to the
|
|
@@ -43,6 +61,73 @@ and no `node_modules`. It includes clyops-tools. `require('clyops-jobs')` loads
|
|
|
43
61
|
(`"${paths.work_dir}/out.mp3"`; relative to `paths.work_dir`), which also counts as an artifact.
|
|
44
62
|
- `result`: a template map added to the result record.
|
|
45
63
|
|
|
64
|
+
### Steps
|
|
65
|
+
|
|
66
|
+
A function can run several tools in order instead of one `script`, so a definition chains
|
|
67
|
+
tools that already exist (fetch some JSON, download what it names, process the file):
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"functions": {
|
|
72
|
+
"fingerprint": {
|
|
73
|
+
"steps": [
|
|
74
|
+
{ "name": "fetch", "script": "tools/http-get-json" },
|
|
75
|
+
{ "name": "download", "script": "tools/download", "stdout": "${job.id}/media" },
|
|
76
|
+
{ "name": "fp", "script": "tools/media-to-fp", "stdin": "${steps.download.stdout}" }
|
|
77
|
+
],
|
|
78
|
+
"artifacts": { "include": ["${job.id}/*.fp"], "required": true },
|
|
79
|
+
"result": { "size": "${steps.fetch.result.size}" }
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"fetch": { "url": "https://media.example.com/recording/${params.uuid}", "token": "${secrets.token}" },
|
|
83
|
+
"download": { "url": "${steps.fetch.result.signedUrl}" }
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- Each step is a binding like a function: `name` (letters, digits, `_`, `-`), `script`, and
|
|
88
|
+
optionally `key` (its options block, default the step name), `positionals`,
|
|
89
|
+
`positionals_order`, `stdin`, `stdout` and `strict_params` (default the function's).
|
|
90
|
+
`artifacts` and `result` belong to the function, as do the caller-input rules: a caller
|
|
91
|
+
reaches a step only through `params` fields its options name.
|
|
92
|
+
- Every step shares the context, `paths.work_dir` included, so files pass by path. A step's
|
|
93
|
+
outputs are `steps.NAME` in the templates after it: `exit_code`; `stdout`, the path of its
|
|
94
|
+
`stdout` file or else its collected output without trailing newlines; `result`, its
|
|
95
|
+
`result` role or else its collected stdout parsed as JSON; `meta`; `progress`. A
|
|
96
|
+
`${steps...}` path that isn't there, such as a later step or a field the JSON lacks, is an
|
|
97
|
+
error naming the step.
|
|
98
|
+
- `previous` is the step that ran just before, on whichever path led here, with the same
|
|
99
|
+
fields plus its `name`: a handler several branches share reads `${previous.name}`,
|
|
100
|
+
`${previous.exit_code}`, `${previous.result.field}` or, after an `error` branch,
|
|
101
|
+
`${previous.error}`. The first step has none, so naming it there is an error.
|
|
102
|
+
- After a step that succeeds the chain goes on to the next step, or ends after the last one.
|
|
103
|
+
A step that exits nonzero, times out or is stopped ends the chain; the run is that step's,
|
|
104
|
+
with `failedStep` naming it. A step that can't start (a template, stdin or schema error)
|
|
105
|
+
rejects the run. `continue_on_exit: [2]` on a step makes those codes a success, and the
|
|
106
|
+
chain then exits with the first of them (a partial result).
|
|
107
|
+
- Branches change where a step leads: `succeed`, `fail` (a nonzero exit or timeout) and
|
|
108
|
+
`error` (it couldn't start) each name a later step, and `succeed: "$end"` ends the chain.
|
|
109
|
+
Branches only go forward, so a chain always ends. A step reached only through a branch,
|
|
110
|
+
such as a fallback or a cleanup, still sits in the list, so the step before it usually
|
|
111
|
+
needs a `succeed`. The chain's exit code is its last step's: a fallback that succeeds
|
|
112
|
+
makes the job succeed, and a cleanup that should leave it failed exits nonzero. A step
|
|
113
|
+
that took its `error` branch is `steps.NAME.error` (the message), with `exit_code: null`.
|
|
114
|
+
A cancelled chain, or one out of time, takes no branch.
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
"steps": [
|
|
118
|
+
{ "name": "fetch", "script": "tools/http-get-json", "fail": "mirror", "succeed": "process" },
|
|
119
|
+
{ "name": "mirror", "script": "tools/http-get-json" },
|
|
120
|
+
{ "name": "process", "script": "tools/media-to-fp" }
|
|
121
|
+
]
|
|
122
|
+
```
|
|
123
|
+
- `timeoutMs` limits the whole chain and a step's `timeout_ms` that step; `signal` stops the
|
|
124
|
+
running step. `onProgress` gets each step's updates with `step` and a default `stage`.
|
|
125
|
+
- The run is the last step's, plus `steps` (the outputs by name), `previous` (the last step)
|
|
126
|
+
and `stepRuns`. Render artifacts and the result with them through `runContext(context,
|
|
127
|
+
run)`, so a result map can use `${previous...}` whichever branch the chain ended on.
|
|
128
|
+
- `functionParamNames(fn)` lists the `params` fields a function's templates use, steps
|
|
129
|
+
included, for describing its caller inputs.
|
|
130
|
+
|
|
46
131
|
```js
|
|
47
132
|
import { resolveFunctionConfig, runScriptFunction, copyConfiguredArtifacts, buildResultRecord,
|
|
48
133
|
newJobId, newJobRecord, transitionJobRecord, timestampIso } from 'clyops-jobs';
|
|
@@ -68,6 +153,7 @@ record = transitionJobRecord(record, { status: 'done', completedAt: timestampIso
|
|
|
68
153
|
When the tool declares the well-known [roles](https://github.com/wankdanker/clyops/blob/main/spec/SPEC.md#14-roles-on-file-descriptors),
|
|
69
154
|
the run wires them up: each `progress` object goes to `onProgress` (and the last one is the run's
|
|
70
155
|
`progress`), `log` lines to `onLog`, and the `meta` document is parsed into the run's `meta`.
|
|
156
|
+
The run's `result` is the `result` role parsed as JSON, or else the collected stdout when it is JSON.
|
|
71
157
|
`updateJobProgress(record, update)` puts an update on a record as `progress` (and its `stage`, if
|
|
72
158
|
any) without a history entry; the queue's tasks get it as `progress(update)`:
|
|
73
159
|
|
package/clyops-jobs.cjs
CHANGED
|
@@ -32,10 +32,13 @@ __export(index_exports, {
|
|
|
32
32
|
deepMerge: () => deepMerge,
|
|
33
33
|
ensureDir: () => ensureDir,
|
|
34
34
|
findFilesByArtifactPattern: () => findFilesByArtifactPattern,
|
|
35
|
+
functionParamNames: () => functionParamNames,
|
|
36
|
+
functionTemplateOptions: () => functionTemplateOptions,
|
|
35
37
|
globToRegex: () => globToRegex,
|
|
36
38
|
isDirectoryPath: () => isDirectoryPath,
|
|
37
39
|
isEnabled: () => isEnabled,
|
|
38
40
|
isFilePath: () => isFilePath,
|
|
41
|
+
isUnsetTemplate: () => isUnsetTemplate,
|
|
39
42
|
listFilesRecursive: () => listFilesRecursive,
|
|
40
43
|
lookupTemplateValue: () => lookupTemplateValue,
|
|
41
44
|
newJobId: () => newJobId,
|
|
@@ -51,9 +54,11 @@ __export(index_exports, {
|
|
|
51
54
|
resolveConfigPath: () => resolveConfigPath,
|
|
52
55
|
resolveFunctionConfig: () => resolveFunctionConfig,
|
|
53
56
|
resolveScriptPath: () => resolveScriptPath,
|
|
57
|
+
runContext: () => runContext,
|
|
54
58
|
runScriptFunction: () => runScriptFunction,
|
|
55
59
|
shellQuote: () => shellQuote,
|
|
56
60
|
statFollowingSymlinks: () => statFollowingSymlinks,
|
|
61
|
+
templateParamNames: () => templateParamNames,
|
|
57
62
|
timestampIso: () => timestampIso,
|
|
58
63
|
transitionJobRecord: () => transitionJobRecord,
|
|
59
64
|
truthy: () => truthy,
|
|
@@ -135,40 +140,69 @@ function resolveConfigPath(root, value) {
|
|
|
135
140
|
var resolveScriptPath = resolveConfigPath;
|
|
136
141
|
|
|
137
142
|
// packages/jobs/src/template.ts
|
|
138
|
-
|
|
143
|
+
var OPTIONAL_ROOTS = ["params"];
|
|
144
|
+
var TEMPLATE = /\$\{\s*([A-Za-z0-9_.-]+)\s*\}|\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g;
|
|
145
|
+
var MISSING = /* @__PURE__ */ Symbol("missing");
|
|
146
|
+
function resolvePath(context, expression) {
|
|
139
147
|
let current = context;
|
|
140
148
|
for (const part of String(expression).split(".").filter(Boolean)) {
|
|
141
|
-
if (!current || typeof current !== "object" || !Object.prototype.hasOwnProperty.call(current, part))
|
|
142
|
-
throw new Error(`unknown template value: ${expression}`);
|
|
143
|
-
}
|
|
149
|
+
if (!current || typeof current !== "object" || !Object.prototype.hasOwnProperty.call(current, part)) return MISSING;
|
|
144
150
|
current = current[part];
|
|
145
151
|
}
|
|
152
|
+
return current;
|
|
153
|
+
}
|
|
154
|
+
function optionalRoot(expression, opts) {
|
|
155
|
+
return !opts?.strict && OPTIONAL_ROOTS.includes(String(expression).split(".").filter(Boolean)[0] ?? "");
|
|
156
|
+
}
|
|
157
|
+
function lookupTemplateValue(context, expression, opts) {
|
|
158
|
+
const current = resolvePath(context, expression);
|
|
159
|
+
if (current === MISSING) {
|
|
160
|
+
if (optionalRoot(expression, opts)) return "";
|
|
161
|
+
throw new Error(`unknown template value: ${expression}`);
|
|
162
|
+
}
|
|
146
163
|
if (current === void 0 || current === null) return "";
|
|
147
164
|
return typeof current === "object" ? JSON.stringify(current) : String(current);
|
|
148
165
|
}
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
function isUnsetTemplate(value, context, opts) {
|
|
167
|
+
if (typeof value !== "string") return false;
|
|
168
|
+
for (const [, dollar, brace] of value.matchAll(TEMPLATE)) {
|
|
169
|
+
const expression = dollar || brace;
|
|
170
|
+
if (!optionalRoot(expression, opts)) continue;
|
|
171
|
+
const current = resolvePath(context, expression);
|
|
172
|
+
if (current === MISSING || current === void 0 || current === null) return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function templateParamNames(value, names = /* @__PURE__ */ new Set()) {
|
|
177
|
+
if (typeof value === "string") {
|
|
178
|
+
for (const [, dollar, brace] of value.matchAll(TEMPLATE)) {
|
|
179
|
+
const [root, field] = String(dollar || brace).split(".").filter(Boolean);
|
|
180
|
+
if (OPTIONAL_ROOTS.includes(root ?? "") && field) names.add(field);
|
|
181
|
+
}
|
|
182
|
+
} else if (Array.isArray(value)) for (const item of value) templateParamNames(item, names);
|
|
183
|
+
else if (value && typeof value === "object") for (const item of Object.values(value)) templateParamNames(item, names);
|
|
184
|
+
return names;
|
|
185
|
+
}
|
|
186
|
+
function renderTemplateString(value, context, opts) {
|
|
187
|
+
return String(value).replace(TEMPLATE, (_, dollar, brace) => lookupTemplateValue(context, dollar || brace, opts));
|
|
154
188
|
}
|
|
155
|
-
function renderConfigValue(value, context) {
|
|
189
|
+
function renderConfigValue(value, context, opts) {
|
|
156
190
|
if (!context) return value;
|
|
157
|
-
if (typeof value === "string") return renderTemplateString(value, context);
|
|
158
|
-
if (Array.isArray(value)) return value.map((item) => renderConfigValue(item, context));
|
|
191
|
+
if (typeof value === "string") return renderTemplateString(value, context, opts);
|
|
192
|
+
if (Array.isArray(value)) return value.map((item) => renderConfigValue(item, context, opts));
|
|
159
193
|
return value;
|
|
160
194
|
}
|
|
161
|
-
function renderResultValue(value, context) {
|
|
162
|
-
if (typeof value === "string") return renderTemplateString(value, context);
|
|
163
|
-
if (Array.isArray(value)) return value.map((item) => renderResultValue(item, context));
|
|
195
|
+
function renderResultValue(value, context, opts) {
|
|
196
|
+
if (typeof value === "string") return renderTemplateString(value, context, opts);
|
|
197
|
+
if (Array.isArray(value)) return value.map((item) => renderResultValue(item, context, opts));
|
|
164
198
|
if (value && typeof value === "object") {
|
|
165
|
-
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context)]));
|
|
199
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context, opts)]));
|
|
166
200
|
}
|
|
167
201
|
return value;
|
|
168
202
|
}
|
|
169
|
-
function renderResultMap(resultMap, context) {
|
|
203
|
+
function renderResultMap(resultMap, context, opts) {
|
|
170
204
|
if (!resultMap || typeof resultMap !== "object" || Array.isArray(resultMap)) return {};
|
|
171
|
-
return renderResultValue(resultMap, context);
|
|
205
|
+
return renderResultValue(resultMap, context, opts);
|
|
172
206
|
}
|
|
173
207
|
|
|
174
208
|
// packages/tools/src/schema.ts
|
|
@@ -236,6 +270,9 @@ function isPathValued(validation, type) {
|
|
|
236
270
|
function isBareProgram(validation, value) {
|
|
237
271
|
return validation === "file:executable" && value !== "" && !value.includes("/") && !(process.platform === "win32" && value.includes("\\"));
|
|
238
272
|
}
|
|
273
|
+
function oneJsonValue(validation, many) {
|
|
274
|
+
return (validation === "json" || validation.startsWith("json:")) && !many;
|
|
275
|
+
}
|
|
239
276
|
var TRUE = /^(true|1|yes|on)$/i;
|
|
240
277
|
var FALSE = /^(false|0|no|off)$/i;
|
|
241
278
|
function toArgv(schema, input, opts = {}) {
|
|
@@ -283,7 +320,7 @@ function toArgv(schema, input, opts = {}) {
|
|
|
283
320
|
out.env[option.variableName] = text(found.value, isPathValued(option.validation, option.type), `--${option.name}`, option.validation);
|
|
284
321
|
continue;
|
|
285
322
|
}
|
|
286
|
-
for (const item of Array.isArray(found.value) ? found.value : [found.value]) {
|
|
323
|
+
for (const item of Array.isArray(found.value) && !oneJsonValue(option.validation, option.isArray) ? found.value : [found.value]) {
|
|
287
324
|
out.argv.push(`--${option.name}`, text(item, isPathValued(option.validation, option.type), `--${option.name}`, option.validation));
|
|
288
325
|
}
|
|
289
326
|
}
|
|
@@ -309,7 +346,7 @@ function toArgv(schema, input, opts = {}) {
|
|
|
309
346
|
positionals.push(text(gap.default, false));
|
|
310
347
|
}
|
|
311
348
|
skipped = [];
|
|
312
|
-
const values = Array.isArray(found.value) ? found.value : [found.value];
|
|
349
|
+
const values = Array.isArray(found.value) && !oneJsonValue(argument.validation, argument.isVariadic) ? found.value : [found.value];
|
|
313
350
|
positionals.push(...values.map((v) => text(v, isPathValued(argument.validation), argument.name, argument.validation)));
|
|
314
351
|
}
|
|
315
352
|
}
|
|
@@ -591,31 +628,81 @@ var LOGGER = {
|
|
|
591
628
|
function configureLogging(opts) {
|
|
592
629
|
Object.assign(LOGGER, Object.fromEntries(Object.entries(opts).filter(([, v]) => v)));
|
|
593
630
|
}
|
|
631
|
+
var STEP_NAME = /^[A-Za-z0-9_-]+$/;
|
|
632
|
+
var END = "$end";
|
|
594
633
|
function resolveFunctionConfig(scriptRoot, routeConfig, functionName) {
|
|
595
634
|
const definition = objectValue(objectValue(routeConfig.functions)[functionName]);
|
|
596
635
|
if (Object.keys(definition).length === 0) return null;
|
|
597
636
|
const key = String(definition.key || functionName);
|
|
637
|
+
if (definition.steps === void 0) return resolveBinding(scriptRoot, routeConfig, functionName, key, definition, `function ${functionName}`);
|
|
638
|
+
if (definition.script) throw new Error(`function ${functionName} defines both script and steps`);
|
|
639
|
+
if (!Array.isArray(definition.steps) || definition.steps.length === 0) throw new Error(`function ${functionName} defines no steps`);
|
|
640
|
+
const steps = [];
|
|
641
|
+
for (const [i, item] of definition.steps.entries()) {
|
|
642
|
+
const step = { strict_params: definition.strict_params, ...objectValue(item) };
|
|
643
|
+
const name = String(step.name ?? "");
|
|
644
|
+
if (!STEP_NAME.test(name)) throw new Error(`function ${functionName} step ${i + 1} needs a name of letters, digits, _ or -`);
|
|
645
|
+
if (steps.some((s) => s.stepName === name)) throw new Error(`function ${functionName} has two steps named ${name}`);
|
|
646
|
+
const where = `function ${functionName} step ${name}`;
|
|
647
|
+
steps.push({ ...resolveBinding(scriptRoot, routeConfig, functionName, String(step.key || name), step, where), stepName: name, definition: step });
|
|
648
|
+
}
|
|
649
|
+
for (const [i, step] of steps.entries()) {
|
|
650
|
+
for (const outcome of ["succeed", "fail", "error"]) {
|
|
651
|
+
const target = step.definition[outcome];
|
|
652
|
+
if (target === void 0 || outcome === "succeed" && target === END) continue;
|
|
653
|
+
const at = steps.findIndex((s) => s.stepName === target);
|
|
654
|
+
if (at < 0) throw new Error(`function ${functionName} step ${step.stepName}: ${outcome} names no step ${String(target)}`);
|
|
655
|
+
if (at <= i) throw new Error(`function ${functionName} step ${step.stepName}: ${outcome} must name a later step, not ${String(target)}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const config = Object.fromEntries(steps.map((s) => [s.stepName, s.config]));
|
|
659
|
+
return { functionName, key, script: steps[steps.length - 1].script, config, definition, steps };
|
|
660
|
+
}
|
|
661
|
+
function resolveBinding(scriptRoot, routeConfig, functionName, key, definition, where) {
|
|
598
662
|
const script = String(definition.script || "");
|
|
599
|
-
if (!script) throw new Error(
|
|
663
|
+
if (!script) throw new Error(`${where} does not define a script`);
|
|
600
664
|
const config = deepMerge(objectValue(objectValue(routeConfig.defaults)[key]), objectValue(routeConfig[key]));
|
|
601
665
|
return { functionName, key, script: resolveScriptPath(scriptRoot, script), config, definition };
|
|
602
666
|
}
|
|
667
|
+
function functionParamNames(fn) {
|
|
668
|
+
const names = /* @__PURE__ */ new Set();
|
|
669
|
+
for (const part of fn.steps ?? [fn]) {
|
|
670
|
+
const { positionals, stdin, stdout } = part.definition;
|
|
671
|
+
templateParamNames([part.config, positionals, stdin, stdout], names);
|
|
672
|
+
}
|
|
673
|
+
templateParamNames([fn.definition.artifacts, fn.definition.result], names);
|
|
674
|
+
return [...names].sort();
|
|
675
|
+
}
|
|
676
|
+
function functionTemplateOptions(fn) {
|
|
677
|
+
return { strict: fn.definition.strict_params === true };
|
|
678
|
+
}
|
|
679
|
+
function withoutUnsetParams(config, context, opts) {
|
|
680
|
+
const out = {};
|
|
681
|
+
for (const [key, value] of Object.entries(config)) {
|
|
682
|
+
if (isUnsetTemplate(value, context, opts)) continue;
|
|
683
|
+
out[key] = Array.isArray(value) ? value.filter((item) => !isUnsetTemplate(item, context, opts)) : value;
|
|
684
|
+
}
|
|
685
|
+
return out;
|
|
686
|
+
}
|
|
603
687
|
function explicitPositionals(fn, context) {
|
|
604
688
|
if (!Array.isArray(fn.definition.positionals)) return void 0;
|
|
605
|
-
|
|
689
|
+
const templates = functionTemplateOptions(fn);
|
|
690
|
+
return fn.definition.positionals.filter((item) => !context || !isUnsetTemplate(item, context, templates)).map((item) => String(renderConfigValue(item, context, templates))).filter(Boolean);
|
|
606
691
|
}
|
|
607
692
|
async function buildFunctionCommand(fn, opts, schema) {
|
|
693
|
+
if (fn.steps) throw new Error(`function ${fn.functionName} runs steps; build each step's command`);
|
|
608
694
|
schema ??= await loadSchema(fn.script);
|
|
609
|
-
const
|
|
695
|
+
const context = opts.context;
|
|
696
|
+
const templates = functionTemplateOptions(fn);
|
|
697
|
+
const config = context ? withoutUnsetParams(fn.config, context, templates) : { ...fn.config };
|
|
610
698
|
const first = schema.arguments[0];
|
|
611
699
|
if (opts.defaultPositional && first && !Array.isArray(fn.definition.positionals) && !inputKeys(first).some((k) => k in config)) {
|
|
612
700
|
config[inputKeys(first)[0]] = opts.defaultPositional;
|
|
613
701
|
}
|
|
614
|
-
const context = opts.context;
|
|
615
702
|
const { argv, unknown, controlled } = toArgv(schema, config, {
|
|
616
703
|
base: opts.configRoot,
|
|
617
704
|
controlled: opts.controlled ?? ["help"],
|
|
618
|
-
render: context ? (s) => renderTemplateString(s, context) : void 0,
|
|
705
|
+
render: context ? (s) => renderTemplateString(s, context, templates) : void 0,
|
|
619
706
|
positionals: explicitPositionals(fn, context),
|
|
620
707
|
positionalsOrder: fn.definition.positionals_order
|
|
621
708
|
});
|
|
@@ -632,19 +719,22 @@ function parseRoleJson(text) {
|
|
|
632
719
|
return void 0;
|
|
633
720
|
}
|
|
634
721
|
}
|
|
635
|
-
function streamPath(template, opts, base) {
|
|
722
|
+
function streamPath(template, opts, base, templates) {
|
|
636
723
|
if (!template) return void 0;
|
|
637
|
-
const rendered = opts.context ? renderTemplateString(template, opts.context) : template;
|
|
724
|
+
const rendered = opts.context ? renderTemplateString(template, opts.context, templates) : template;
|
|
638
725
|
return (0, import_node_path3.isAbsolute)(rendered) ? rendered : (0, import_node_path3.resolve)(base, rendered);
|
|
639
726
|
}
|
|
640
727
|
async function runScriptFunction(fn, opts) {
|
|
728
|
+
return fn.steps ? runSteps(fn.steps, opts) : (await runTool(fn, opts)).run;
|
|
729
|
+
}
|
|
730
|
+
async function runTool(fn, opts) {
|
|
641
731
|
const schema = await loadSchema(fn.script);
|
|
642
732
|
const [file, ...argv] = await buildFunctionCommand(fn, opts, schema);
|
|
643
733
|
const command = [file, ...redactArgv(schema, argv)];
|
|
644
734
|
LOGGER.verbose("command: %s", shellQuote(command));
|
|
645
735
|
const workDir = opts.context?.paths?.work_dir;
|
|
646
|
-
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot);
|
|
647
|
-
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot);
|
|
736
|
+
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot, functionTemplateOptions(fn));
|
|
737
|
+
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot, functionTemplateOptions(fn));
|
|
648
738
|
let progress;
|
|
649
739
|
const roles = {
|
|
650
740
|
progress: {
|
|
@@ -656,8 +746,10 @@ async function runScriptFunction(fn, opts) {
|
|
|
656
746
|
}
|
|
657
747
|
},
|
|
658
748
|
meta: {},
|
|
749
|
+
result: {},
|
|
659
750
|
log: { onLine: opts.onLog }
|
|
660
751
|
};
|
|
752
|
+
const stdin = stdinPath ? (0, import_node_fs4.createReadStream)(stdinPath, { fd: (0, import_node_fs4.openSync)(stdinPath, "r") }) : void 0;
|
|
661
753
|
const started = start(file, argv, {
|
|
662
754
|
cwd: opts.cwd,
|
|
663
755
|
env: opts.env,
|
|
@@ -665,7 +757,7 @@ async function runScriptFunction(fn, opts) {
|
|
|
665
757
|
killGraceMs: opts.killGraceMs,
|
|
666
758
|
signal: opts.signal,
|
|
667
759
|
onStderr: opts.onStderr,
|
|
668
|
-
stdin
|
|
760
|
+
stdin,
|
|
669
761
|
stdout: stdoutPath ? "stream" : "text",
|
|
670
762
|
roles: declaredRoles({ schema }, roles)
|
|
671
763
|
});
|
|
@@ -676,7 +768,70 @@ async function runScriptFunction(fn, opts) {
|
|
|
676
768
|
}
|
|
677
769
|
const [result] = await Promise.all([started.result, written]);
|
|
678
770
|
const meta = parseRoleJson(result.roles?.meta);
|
|
679
|
-
|
|
771
|
+
const output = parseRoleJson(result.roles?.result) ?? (stdoutPath ? void 0 : parseRoleJson(result.stdout));
|
|
772
|
+
const run = {
|
|
773
|
+
...result,
|
|
774
|
+
command,
|
|
775
|
+
stderrTail: tail(result.stderr),
|
|
776
|
+
...meta !== void 0 ? { meta } : {},
|
|
777
|
+
...progress ? { progress } : {},
|
|
778
|
+
...output !== void 0 ? { result: output } : {}
|
|
779
|
+
};
|
|
780
|
+
return { run, stdoutPath };
|
|
781
|
+
}
|
|
782
|
+
async function runSteps(steps, opts) {
|
|
783
|
+
const deadline = opts.timeoutMs ? Date.now() + opts.timeoutMs : void 0;
|
|
784
|
+
const outputs = {};
|
|
785
|
+
const stepRuns = [];
|
|
786
|
+
const byName = new Map(steps.map((step, i) => [step.stepName, i]));
|
|
787
|
+
const after = (i, branch, fallthrough) => branch === END ? void 0 : branch ? byName.get(branch) : fallthrough && i + 1 < steps.length ? i + 1 : void 0;
|
|
788
|
+
let continued;
|
|
789
|
+
let last;
|
|
790
|
+
let previous;
|
|
791
|
+
for (let i = 0; i !== void 0; ) {
|
|
792
|
+
const step = steps[i];
|
|
793
|
+
const name = step.stepName;
|
|
794
|
+
const limits = [step.definition.timeout_ms, deadline && Math.max(1, deadline - Date.now())].filter((ms) => Number(ms) > 0);
|
|
795
|
+
const context = { ...opts.context, steps: { ...outputs }, ...previous ? { previous } : {} };
|
|
796
|
+
const onProgress = opts.onProgress && ((update) => opts.onProgress?.({ stage: name, ...update, step: name }));
|
|
797
|
+
let ran;
|
|
798
|
+
try {
|
|
799
|
+
ran = await runTool(step, { ...opts, context, onProgress, timeoutMs: limits.length ? Math.min(...limits) : void 0 });
|
|
800
|
+
} catch (error) {
|
|
801
|
+
const message = `step ${name}: ${error.message}`;
|
|
802
|
+
if (!step.definition.error) throw new Error(message, { cause: error });
|
|
803
|
+
outputs[name] = { exit_code: null, stdout: "", error: message };
|
|
804
|
+
previous = { name, ...outputs[name] };
|
|
805
|
+
i = byName.get(step.definition.error);
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
const { run, stdoutPath } = ran;
|
|
809
|
+
stepRuns.push({ ...run, step: name });
|
|
810
|
+
outputs[name] = {
|
|
811
|
+
exit_code: run.exitCode,
|
|
812
|
+
stdout: stdoutPath ?? run.stdout.replace(/\n+$/, ""),
|
|
813
|
+
...run.result !== void 0 ? { result: run.result } : {},
|
|
814
|
+
...run.meta !== void 0 ? { meta: run.meta } : {},
|
|
815
|
+
...run.progress ? { progress: run.progress } : {}
|
|
816
|
+
};
|
|
817
|
+
previous = { name, ...outputs[name] };
|
|
818
|
+
last = { ...run, steps: outputs, previous, stepRuns, durationMs: stepRuns.reduce((ms, r) => ms + r.durationMs, 0) };
|
|
819
|
+
const ok = !run.timedOut && (run.exitCode === 0 || run.exitCode !== null && (step.definition.continue_on_exit ?? []).includes(run.exitCode));
|
|
820
|
+
if (ok) {
|
|
821
|
+
if (run.exitCode !== 0) continued ??= run.exitCode ?? void 0;
|
|
822
|
+
i = after(i, step.definition.succeed, true);
|
|
823
|
+
if (i === void 0) return { ...last, exitCode: run.exitCode || continued || 0 };
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
const stopped = opts.signal?.aborted || deadline !== void 0 && Date.now() >= deadline;
|
|
827
|
+
i = stopped ? void 0 : after(i, step.definition.fail, false);
|
|
828
|
+
if (i === void 0) return { ...last, failedStep: name };
|
|
829
|
+
}
|
|
830
|
+
if (!last) throw new Error("unreachable: a chain ends after a step that ran");
|
|
831
|
+
return last;
|
|
832
|
+
}
|
|
833
|
+
function runContext(context, run) {
|
|
834
|
+
return { ...context, ...run.steps ? { steps: run.steps } : {}, ...run.previous ? { previous: run.previous } : {} };
|
|
680
835
|
}
|
|
681
836
|
function buildResultRecord(p) {
|
|
682
837
|
return {
|
|
@@ -688,7 +843,7 @@ function buildResultRecord(p) {
|
|
|
688
843
|
artifact_keys: p.artifactKeys,
|
|
689
844
|
completed_at: p.now,
|
|
690
845
|
config: p.fn.config,
|
|
691
|
-
...renderResultMap(p.fn.definition.result, p.context)
|
|
846
|
+
...renderResultMap(p.fn.definition.result, p.context, functionTemplateOptions(p.fn))
|
|
692
847
|
};
|
|
693
848
|
}
|
|
694
849
|
|
|
@@ -734,8 +889,8 @@ function listFilesRecursive(dir, visited = /* @__PURE__ */ new Set()) {
|
|
|
734
889
|
}
|
|
735
890
|
return files;
|
|
736
891
|
}
|
|
737
|
-
function findFilesByArtifactPattern(pattern, context) {
|
|
738
|
-
const rendered = renderTemplateString(pattern, context);
|
|
892
|
+
function findFilesByArtifactPattern(pattern, context, opts) {
|
|
893
|
+
const rendered = renderTemplateString(pattern, context, opts);
|
|
739
894
|
const absolute = (0, import_node_path4.isAbsolute)(rendered) ? rendered : (0, import_node_path4.resolve)(context.paths.work_dir, rendered);
|
|
740
895
|
if (!/[*?]/.test(absolute)) return isFilePath(absolute) ? [absolute] : [];
|
|
741
896
|
const regex = globToRegex(absolute);
|
|
@@ -774,7 +929,7 @@ function copyConfiguredArtifacts(fn, context, opts = {}) {
|
|
|
774
929
|
const seen = /* @__PURE__ */ new Set();
|
|
775
930
|
const copied = [];
|
|
776
931
|
for (const pattern of patterns) {
|
|
777
|
-
for (const source of findFilesByArtifactPattern(pattern, context)) {
|
|
932
|
+
for (const source of findFilesByArtifactPattern(pattern, context, { strict: fn.definition.strict_params === true })) {
|
|
778
933
|
if (seen.has(source)) continue;
|
|
779
934
|
seen.add(source);
|
|
780
935
|
const destination = (0, import_node_path4.join)(dir, (0, import_node_path4.basename)(source));
|
|
@@ -924,10 +1079,13 @@ var JobQueue = class {
|
|
|
924
1079
|
deepMerge,
|
|
925
1080
|
ensureDir,
|
|
926
1081
|
findFilesByArtifactPattern,
|
|
1082
|
+
functionParamNames,
|
|
1083
|
+
functionTemplateOptions,
|
|
927
1084
|
globToRegex,
|
|
928
1085
|
isDirectoryPath,
|
|
929
1086
|
isEnabled,
|
|
930
1087
|
isFilePath,
|
|
1088
|
+
isUnsetTemplate,
|
|
931
1089
|
listFilesRecursive,
|
|
932
1090
|
lookupTemplateValue,
|
|
933
1091
|
newJobId,
|
|
@@ -943,9 +1101,11 @@ var JobQueue = class {
|
|
|
943
1101
|
resolveConfigPath,
|
|
944
1102
|
resolveFunctionConfig,
|
|
945
1103
|
resolveScriptPath,
|
|
1104
|
+
runContext,
|
|
946
1105
|
runScriptFunction,
|
|
947
1106
|
shellQuote,
|
|
948
1107
|
statFollowingSymlinks,
|
|
1108
|
+
templateParamNames,
|
|
949
1109
|
timestampIso,
|
|
950
1110
|
transitionJobRecord,
|
|
951
1111
|
truthy,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://clyops.io/schemas/clyops-jobs.json",
|
|
4
|
+
"title": "clyops-jobs config",
|
|
5
|
+
"description": "Functions bound to clyops tools, their default options and their option blocks. Every other top-level key is an options block, named by a function's or step's `key`.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"$schema": { "type": "string", "description": "This schema's URL, for editors: https://clyops.io/schemas/clyops-jobs.json" },
|
|
9
|
+
"functions": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"description": "The functions a trigger can run, by name.",
|
|
12
|
+
"additionalProperties": { "$ref": "#/$defs/function" }
|
|
13
|
+
},
|
|
14
|
+
"defaults": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"description": "Default options by block key, merged under the block of the same key.",
|
|
17
|
+
"additionalProperties": { "$ref": "#/$defs/options" }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"additionalProperties": { "$ref": "#/$defs/options" },
|
|
21
|
+
"$defs": {
|
|
22
|
+
"template": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Text with `${dot.path}` or `{{dot.path}}` templates rendered against the trigger's context (`job`, `paths`, `params`, `steps`, ...)."
|
|
25
|
+
},
|
|
26
|
+
"stepName": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
|
|
27
|
+
"options": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"description": "Tool options keyed like the tool's options (`out_dir` or `out-dir`) or arguments, mapped onto its command line through --help-json-schema. String values are templates; an option naming a missing `params` field is left unset."
|
|
30
|
+
},
|
|
31
|
+
"binding": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {
|
|
34
|
+
"key": { "type": "string", "description": "The options block (a top-level key and a `defaults` key); default: the function's or step's name." },
|
|
35
|
+
"script": { "type": "string", "description": "The tool, relative to the script root." },
|
|
36
|
+
"positionals": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"description": "The tool's positional arguments as templates, in order; `[]` for none. Without it, arguments are options keyed by argument name.",
|
|
39
|
+
"items": { "type": ["string", "number", "boolean"] }
|
|
40
|
+
},
|
|
41
|
+
"positionals_order": { "enum": ["first", "last"], "description": "Positionals before (default) or after the options." },
|
|
42
|
+
"stdin": { "$ref": "#/$defs/template", "description": "A template naming the file fed to the tool's stdin, relative to the config root." },
|
|
43
|
+
"stdout": { "$ref": "#/$defs/template", "description": "A template naming the file the tool's stdout is written to, relative to `paths.work_dir`." },
|
|
44
|
+
"strict_params": { "type": "boolean", "description": "A missing `params` field is an error instead of leaving the option unset (default false)." }
|
|
45
|
+
},
|
|
46
|
+
"patternProperties": { "^x-": { "description": "Free for triggers and comments; clyops-jobs ignores it." } }
|
|
47
|
+
},
|
|
48
|
+
"function": {
|
|
49
|
+
"description": "A function runs one tool (`script`) or several in order (`steps`).",
|
|
50
|
+
"type": "object",
|
|
51
|
+
"allOf": [{ "$ref": "#/$defs/binding" }],
|
|
52
|
+
"properties": {
|
|
53
|
+
"key": true, "script": true, "positionals": true, "positionals_order": true, "stdin": true, "stdout": true, "strict_params": true,
|
|
54
|
+
"steps": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"minItems": 1,
|
|
57
|
+
"description": "Tools run in order with one context; each step's outputs are `steps.NAME` (`exit_code`, `stdout`, `result`, `meta`, `progress`, `error`) in the templates after it, and the step that ran just before is `previous` (the same fields plus `name`).",
|
|
58
|
+
"items": { "$ref": "#/$defs/step" }
|
|
59
|
+
},
|
|
60
|
+
"artifacts": { "$ref": "#/$defs/artifacts" },
|
|
61
|
+
"result": { "type": "object", "description": "Templates added to the result record; nested objects and arrays are rendered too." }
|
|
62
|
+
},
|
|
63
|
+
"patternProperties": { "^x-": true },
|
|
64
|
+
"additionalProperties": false,
|
|
65
|
+
"oneOf": [
|
|
66
|
+
{ "required": ["script"], "not": { "required": ["steps"] } },
|
|
67
|
+
{ "required": ["steps"], "not": { "required": ["script"] } }
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"step": {
|
|
71
|
+
"description": "One tool in a function's `steps`.",
|
|
72
|
+
"type": "object",
|
|
73
|
+
"allOf": [{ "$ref": "#/$defs/binding" }],
|
|
74
|
+
"required": ["name", "script"],
|
|
75
|
+
"properties": {
|
|
76
|
+
"key": true, "script": true, "positionals": true, "positionals_order": true, "stdin": true, "stdout": true, "strict_params": true,
|
|
77
|
+
"name": { "$ref": "#/$defs/stepName", "description": "Names the step in `${steps.NAME...}` templates, branches and errors." },
|
|
78
|
+
"timeout_ms": { "type": "integer", "exclusiveMinimum": 0, "description": "This step's time limit; the whole chain also stays within the run's timeoutMs." },
|
|
79
|
+
"continue_on_exit": {
|
|
80
|
+
"type": "array",
|
|
81
|
+
"items": { "type": "integer" },
|
|
82
|
+
"description": "Exit codes besides 0 that count as success; the chain then reports the first of them, e.g. [2] for a partial result."
|
|
83
|
+
},
|
|
84
|
+
"succeed": {
|
|
85
|
+
"anyOf": [{ "$ref": "#/$defs/stepName" }, { "const": "$end" }],
|
|
86
|
+
"description": "The later step to run after this one succeeds, instead of the next; `$end` ends the chain."
|
|
87
|
+
},
|
|
88
|
+
"fail": { "$ref": "#/$defs/stepName", "description": "The later step to run when this one exits nonzero or times out, instead of ending the chain." },
|
|
89
|
+
"error": { "$ref": "#/$defs/stepName", "description": "The later step to run when this one can't start (a template, stdin or schema error), instead of failing the run." }
|
|
90
|
+
},
|
|
91
|
+
"patternProperties": { "^x-": true },
|
|
92
|
+
"additionalProperties": false
|
|
93
|
+
},
|
|
94
|
+
"artifacts": {
|
|
95
|
+
"description": "Files copied from `paths.work_dir` into `paths.artifacts_dir` after the run: glob patterns (`*`, `?`, `**`), or an object with them and a requirement.",
|
|
96
|
+
"anyOf": [
|
|
97
|
+
{ "$ref": "#/$defs/template" },
|
|
98
|
+
{ "type": "array", "items": { "$ref": "#/$defs/template" } },
|
|
99
|
+
{
|
|
100
|
+
"type": "object",
|
|
101
|
+
"properties": {
|
|
102
|
+
"include": { "anyOf": [{ "$ref": "#/$defs/template" }, { "type": "array", "items": { "$ref": "#/$defs/template" } }], "description": "Glob patterns relative to `paths.work_dir`." },
|
|
103
|
+
"includes": { "$ref": "#/$defs/artifacts/anyOf/2/properties/include", "description": "Same as include." },
|
|
104
|
+
"patterns": { "$ref": "#/$defs/artifacts/anyOf/2/properties/include", "description": "Same as include." },
|
|
105
|
+
"required": { "type": ["boolean", "string"], "description": "At least one file must match." },
|
|
106
|
+
"min": { "type": "integer", "minimum": 0, "description": "At least this many files must match." }
|
|
107
|
+
},
|
|
108
|
+
"additionalProperties": false
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
package/dist/artifacts.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResolvedFunction } from './functions.js';
|
|
2
|
-
import { type TemplateContext } from './template.js';
|
|
2
|
+
import { type TemplateContext, type TemplateOptions } from './template.js';
|
|
3
3
|
/** Compile an artifact glob into an anchored regular expression. */
|
|
4
4
|
export declare function globToRegex(pattern: string): RegExp;
|
|
5
5
|
/** Every file below `dir`, following symlinks without looping. */
|
|
@@ -9,7 +9,7 @@ export declare function findFilesByArtifactPattern(pattern: string, context: Tem
|
|
|
9
9
|
paths: {
|
|
10
10
|
work_dir: string;
|
|
11
11
|
};
|
|
12
|
-
}): string[];
|
|
12
|
+
}, opts?: TemplateOptions): string[];
|
|
13
13
|
/** A function's `artifacts` as a list of include patterns, plus its `stdout` file. */
|
|
14
14
|
export declare function artifactIncludePatterns(fn: ResolvedFunction): string[];
|
|
15
15
|
/** How many artifacts a function must produce: `min`, or 1 for `required: true`. */
|
package/dist/artifacts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAwB,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAKjG,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAiBnD;AAWD,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,cAAoB,GAAG,MAAM,EAAE,CAarF;AAED,oFAAoF;AACpF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG;IAAE,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAQxJ;AAED,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAatE;AAED,oFAAoF;AACpF,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,gBAAgB,GAAG,MAAM,CAMhE;AAED,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,gBAAgB,EACpB,OAAO,EAAE,eAAe,GAAG;IAAE,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,EACjF,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAChC,cAAc,EAAE,CAwBlB"}
|