clyops-jobs 0.9.0 → 0.10.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 +6 -1
- package/clyops-jobs.cjs +71 -32
- 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 +5 -1
- package/dist/functions.d.ts.map +1 -1
- package/dist/functions.js +34 -10
- package/dist/template.d.ts +21 -6
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +54 -16
- package/package.json +3 -3
- package/src/artifacts.ts +4 -4
- package/src/functions.ts +37 -10
- package/src/template.ts +59 -18
package/README.md
CHANGED
|
@@ -35,7 +35,12 @@ and no `node_modules`. It includes clyops-tools. `require('clyops-jobs')` loads
|
|
|
35
35
|
- `positionals` (optional) lists the tool's positional arguments explicitly; otherwise they're
|
|
36
36
|
options keyed by argument name.
|
|
37
37
|
- String values may use `${dot.path}` or `{{dot.path}}` templates against a context you build
|
|
38
|
-
(`job`, `paths`, whatever the trigger knows).
|
|
38
|
+
(`job`, `paths`, whatever the trigger knows). An unknown path is an error, so a typo fails
|
|
39
|
+
loudly, except under `params` (the caller's fields): an option or positional that names a
|
|
40
|
+
missing or null `params` field is left unset, so the tool's default applies. That holds for
|
|
41
|
+
`${params.x}` alone and inside a longer string (`v-${params.x}`), so a value is never half
|
|
42
|
+
filled. Elsewhere (`result`, `stdin`, `stdout`, artifact patterns) a missing field renders as
|
|
43
|
+
`''`. `strict_params: true` on a function makes a missing `params` field an error too.
|
|
39
44
|
- `artifacts`: glob patterns (`*`, `?`, `**`) relative to `paths.work_dir`, copied into
|
|
40
45
|
`paths.artifacts_dir`, with an optional `required: true` or `min: N`.
|
|
41
46
|
- `stdin`: a template naming a file fed to the tool's stdin (`"${media.path}"`; relative to the
|
package/clyops-jobs.cjs
CHANGED
|
@@ -32,10 +32,12 @@ __export(index_exports, {
|
|
|
32
32
|
deepMerge: () => deepMerge,
|
|
33
33
|
ensureDir: () => ensureDir,
|
|
34
34
|
findFilesByArtifactPattern: () => findFilesByArtifactPattern,
|
|
35
|
+
functionTemplateOptions: () => functionTemplateOptions,
|
|
35
36
|
globToRegex: () => globToRegex,
|
|
36
37
|
isDirectoryPath: () => isDirectoryPath,
|
|
37
38
|
isEnabled: () => isEnabled,
|
|
38
39
|
isFilePath: () => isFilePath,
|
|
40
|
+
isUnsetTemplate: () => isUnsetTemplate,
|
|
39
41
|
listFilesRecursive: () => listFilesRecursive,
|
|
40
42
|
lookupTemplateValue: () => lookupTemplateValue,
|
|
41
43
|
newJobId: () => newJobId,
|
|
@@ -135,40 +137,59 @@ function resolveConfigPath(root, value) {
|
|
|
135
137
|
var resolveScriptPath = resolveConfigPath;
|
|
136
138
|
|
|
137
139
|
// packages/jobs/src/template.ts
|
|
138
|
-
|
|
140
|
+
var OPTIONAL_ROOTS = ["params"];
|
|
141
|
+
var TEMPLATE = /\$\{\s*([A-Za-z0-9_.-]+)\s*\}|\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g;
|
|
142
|
+
var MISSING = /* @__PURE__ */ Symbol("missing");
|
|
143
|
+
function resolvePath(context, expression) {
|
|
139
144
|
let current = context;
|
|
140
145
|
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
|
-
}
|
|
146
|
+
if (!current || typeof current !== "object" || !Object.prototype.hasOwnProperty.call(current, part)) return MISSING;
|
|
144
147
|
current = current[part];
|
|
145
148
|
}
|
|
149
|
+
return current;
|
|
150
|
+
}
|
|
151
|
+
function optionalRoot(expression, opts) {
|
|
152
|
+
return !opts?.strict && OPTIONAL_ROOTS.includes(String(expression).split(".").filter(Boolean)[0] ?? "");
|
|
153
|
+
}
|
|
154
|
+
function lookupTemplateValue(context, expression, opts) {
|
|
155
|
+
const current = resolvePath(context, expression);
|
|
156
|
+
if (current === MISSING) {
|
|
157
|
+
if (optionalRoot(expression, opts)) return "";
|
|
158
|
+
throw new Error(`unknown template value: ${expression}`);
|
|
159
|
+
}
|
|
146
160
|
if (current === void 0 || current === null) return "";
|
|
147
161
|
return typeof current === "object" ? JSON.stringify(current) : String(current);
|
|
148
162
|
}
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
function isUnsetTemplate(value, context, opts) {
|
|
164
|
+
if (typeof value !== "string") return false;
|
|
165
|
+
for (const [, dollar, brace] of value.matchAll(TEMPLATE)) {
|
|
166
|
+
const expression = dollar || brace;
|
|
167
|
+
if (!optionalRoot(expression, opts)) continue;
|
|
168
|
+
const current = resolvePath(context, expression);
|
|
169
|
+
if (current === MISSING || current === void 0 || current === null) return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
function renderTemplateString(value, context, opts) {
|
|
174
|
+
return String(value).replace(TEMPLATE, (_, dollar, brace) => lookupTemplateValue(context, dollar || brace, opts));
|
|
154
175
|
}
|
|
155
|
-
function renderConfigValue(value, context) {
|
|
176
|
+
function renderConfigValue(value, context, opts) {
|
|
156
177
|
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));
|
|
178
|
+
if (typeof value === "string") return renderTemplateString(value, context, opts);
|
|
179
|
+
if (Array.isArray(value)) return value.map((item) => renderConfigValue(item, context, opts));
|
|
159
180
|
return value;
|
|
160
181
|
}
|
|
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));
|
|
182
|
+
function renderResultValue(value, context, opts) {
|
|
183
|
+
if (typeof value === "string") return renderTemplateString(value, context, opts);
|
|
184
|
+
if (Array.isArray(value)) return value.map((item) => renderResultValue(item, context, opts));
|
|
164
185
|
if (value && typeof value === "object") {
|
|
165
|
-
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context)]));
|
|
186
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context, opts)]));
|
|
166
187
|
}
|
|
167
188
|
return value;
|
|
168
189
|
}
|
|
169
|
-
function renderResultMap(resultMap, context) {
|
|
190
|
+
function renderResultMap(resultMap, context, opts) {
|
|
170
191
|
if (!resultMap || typeof resultMap !== "object" || Array.isArray(resultMap)) return {};
|
|
171
|
-
return renderResultValue(resultMap, context);
|
|
192
|
+
return renderResultValue(resultMap, context, opts);
|
|
172
193
|
}
|
|
173
194
|
|
|
174
195
|
// packages/tools/src/schema.ts
|
|
@@ -236,6 +257,9 @@ function isPathValued(validation, type) {
|
|
|
236
257
|
function isBareProgram(validation, value) {
|
|
237
258
|
return validation === "file:executable" && value !== "" && !value.includes("/") && !(process.platform === "win32" && value.includes("\\"));
|
|
238
259
|
}
|
|
260
|
+
function oneJsonValue(validation, many) {
|
|
261
|
+
return (validation === "json" || validation.startsWith("json:")) && !many;
|
|
262
|
+
}
|
|
239
263
|
var TRUE = /^(true|1|yes|on)$/i;
|
|
240
264
|
var FALSE = /^(false|0|no|off)$/i;
|
|
241
265
|
function toArgv(schema, input, opts = {}) {
|
|
@@ -283,7 +307,7 @@ function toArgv(schema, input, opts = {}) {
|
|
|
283
307
|
out.env[option.variableName] = text(found.value, isPathValued(option.validation, option.type), `--${option.name}`, option.validation);
|
|
284
308
|
continue;
|
|
285
309
|
}
|
|
286
|
-
for (const item of Array.isArray(found.value) ? found.value : [found.value]) {
|
|
310
|
+
for (const item of Array.isArray(found.value) && !oneJsonValue(option.validation, option.isArray) ? found.value : [found.value]) {
|
|
287
311
|
out.argv.push(`--${option.name}`, text(item, isPathValued(option.validation, option.type), `--${option.name}`, option.validation));
|
|
288
312
|
}
|
|
289
313
|
}
|
|
@@ -309,7 +333,7 @@ function toArgv(schema, input, opts = {}) {
|
|
|
309
333
|
positionals.push(text(gap.default, false));
|
|
310
334
|
}
|
|
311
335
|
skipped = [];
|
|
312
|
-
const values = Array.isArray(found.value) ? found.value : [found.value];
|
|
336
|
+
const values = Array.isArray(found.value) && !oneJsonValue(argument.validation, argument.isVariadic) ? found.value : [found.value];
|
|
313
337
|
positionals.push(...values.map((v) => text(v, isPathValued(argument.validation), argument.name, argument.validation)));
|
|
314
338
|
}
|
|
315
339
|
}
|
|
@@ -600,22 +624,35 @@ function resolveFunctionConfig(scriptRoot, routeConfig, functionName) {
|
|
|
600
624
|
const config = deepMerge(objectValue(objectValue(routeConfig.defaults)[key]), objectValue(routeConfig[key]));
|
|
601
625
|
return { functionName, key, script: resolveScriptPath(scriptRoot, script), config, definition };
|
|
602
626
|
}
|
|
627
|
+
function functionTemplateOptions(fn) {
|
|
628
|
+
return { strict: fn.definition.strict_params === true };
|
|
629
|
+
}
|
|
630
|
+
function withoutUnsetParams(config, context, opts) {
|
|
631
|
+
const out = {};
|
|
632
|
+
for (const [key, value] of Object.entries(config)) {
|
|
633
|
+
if (isUnsetTemplate(value, context, opts)) continue;
|
|
634
|
+
out[key] = Array.isArray(value) ? value.filter((item) => !isUnsetTemplate(item, context, opts)) : value;
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
637
|
+
}
|
|
603
638
|
function explicitPositionals(fn, context) {
|
|
604
639
|
if (!Array.isArray(fn.definition.positionals)) return void 0;
|
|
605
|
-
|
|
640
|
+
const templates = functionTemplateOptions(fn);
|
|
641
|
+
return fn.definition.positionals.filter((item) => !context || !isUnsetTemplate(item, context, templates)).map((item) => String(renderConfigValue(item, context, templates))).filter(Boolean);
|
|
606
642
|
}
|
|
607
643
|
async function buildFunctionCommand(fn, opts, schema) {
|
|
608
644
|
schema ??= await loadSchema(fn.script);
|
|
609
|
-
const
|
|
645
|
+
const context = opts.context;
|
|
646
|
+
const templates = functionTemplateOptions(fn);
|
|
647
|
+
const config = context ? withoutUnsetParams(fn.config, context, templates) : { ...fn.config };
|
|
610
648
|
const first = schema.arguments[0];
|
|
611
649
|
if (opts.defaultPositional && first && !Array.isArray(fn.definition.positionals) && !inputKeys(first).some((k) => k in config)) {
|
|
612
650
|
config[inputKeys(first)[0]] = opts.defaultPositional;
|
|
613
651
|
}
|
|
614
|
-
const context = opts.context;
|
|
615
652
|
const { argv, unknown, controlled } = toArgv(schema, config, {
|
|
616
653
|
base: opts.configRoot,
|
|
617
654
|
controlled: opts.controlled ?? ["help"],
|
|
618
|
-
render: context ? (s) => renderTemplateString(s, context) : void 0,
|
|
655
|
+
render: context ? (s) => renderTemplateString(s, context, templates) : void 0,
|
|
619
656
|
positionals: explicitPositionals(fn, context),
|
|
620
657
|
positionalsOrder: fn.definition.positionals_order
|
|
621
658
|
});
|
|
@@ -632,9 +669,9 @@ function parseRoleJson(text) {
|
|
|
632
669
|
return void 0;
|
|
633
670
|
}
|
|
634
671
|
}
|
|
635
|
-
function streamPath(template, opts, base) {
|
|
672
|
+
function streamPath(template, opts, base, templates) {
|
|
636
673
|
if (!template) return void 0;
|
|
637
|
-
const rendered = opts.context ? renderTemplateString(template, opts.context) : template;
|
|
674
|
+
const rendered = opts.context ? renderTemplateString(template, opts.context, templates) : template;
|
|
638
675
|
return (0, import_node_path3.isAbsolute)(rendered) ? rendered : (0, import_node_path3.resolve)(base, rendered);
|
|
639
676
|
}
|
|
640
677
|
async function runScriptFunction(fn, opts) {
|
|
@@ -643,8 +680,8 @@ async function runScriptFunction(fn, opts) {
|
|
|
643
680
|
const command = [file, ...redactArgv(schema, argv)];
|
|
644
681
|
LOGGER.verbose("command: %s", shellQuote(command));
|
|
645
682
|
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);
|
|
683
|
+
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot, functionTemplateOptions(fn));
|
|
684
|
+
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot, functionTemplateOptions(fn));
|
|
648
685
|
let progress;
|
|
649
686
|
const roles = {
|
|
650
687
|
progress: {
|
|
@@ -688,7 +725,7 @@ function buildResultRecord(p) {
|
|
|
688
725
|
artifact_keys: p.artifactKeys,
|
|
689
726
|
completed_at: p.now,
|
|
690
727
|
config: p.fn.config,
|
|
691
|
-
...renderResultMap(p.fn.definition.result, p.context)
|
|
728
|
+
...renderResultMap(p.fn.definition.result, p.context, functionTemplateOptions(p.fn))
|
|
692
729
|
};
|
|
693
730
|
}
|
|
694
731
|
|
|
@@ -734,8 +771,8 @@ function listFilesRecursive(dir, visited = /* @__PURE__ */ new Set()) {
|
|
|
734
771
|
}
|
|
735
772
|
return files;
|
|
736
773
|
}
|
|
737
|
-
function findFilesByArtifactPattern(pattern, context) {
|
|
738
|
-
const rendered = renderTemplateString(pattern, context);
|
|
774
|
+
function findFilesByArtifactPattern(pattern, context, opts) {
|
|
775
|
+
const rendered = renderTemplateString(pattern, context, opts);
|
|
739
776
|
const absolute = (0, import_node_path4.isAbsolute)(rendered) ? rendered : (0, import_node_path4.resolve)(context.paths.work_dir, rendered);
|
|
740
777
|
if (!/[*?]/.test(absolute)) return isFilePath(absolute) ? [absolute] : [];
|
|
741
778
|
const regex = globToRegex(absolute);
|
|
@@ -774,7 +811,7 @@ function copyConfiguredArtifacts(fn, context, opts = {}) {
|
|
|
774
811
|
const seen = /* @__PURE__ */ new Set();
|
|
775
812
|
const copied = [];
|
|
776
813
|
for (const pattern of patterns) {
|
|
777
|
-
for (const source of findFilesByArtifactPattern(pattern, context)) {
|
|
814
|
+
for (const source of findFilesByArtifactPattern(pattern, context, { strict: fn.definition.strict_params === true })) {
|
|
778
815
|
if (seen.has(source)) continue;
|
|
779
816
|
seen.add(source);
|
|
780
817
|
const destination = (0, import_node_path4.join)(dir, (0, import_node_path4.basename)(source));
|
|
@@ -924,10 +961,12 @@ var JobQueue = class {
|
|
|
924
961
|
deepMerge,
|
|
925
962
|
ensureDir,
|
|
926
963
|
findFilesByArtifactPattern,
|
|
964
|
+
functionTemplateOptions,
|
|
927
965
|
globToRegex,
|
|
928
966
|
isDirectoryPath,
|
|
929
967
|
isEnabled,
|
|
930
968
|
isFilePath,
|
|
969
|
+
isUnsetTemplate,
|
|
931
970
|
listFilesRecursive,
|
|
932
971
|
lookupTemplateValue,
|
|
933
972
|
newJobId,
|
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"}
|
package/dist/artifacts.js
CHANGED
|
@@ -59,8 +59,8 @@ export function listFilesRecursive(dir, visited = new Set()) {
|
|
|
59
59
|
return files;
|
|
60
60
|
}
|
|
61
61
|
/** Files matching a templated glob; relative patterns start at `paths.work_dir`. */
|
|
62
|
-
export function findFilesByArtifactPattern(pattern, context) {
|
|
63
|
-
const rendered = renderTemplateString(pattern, context);
|
|
62
|
+
export function findFilesByArtifactPattern(pattern, context, opts) {
|
|
63
|
+
const rendered = renderTemplateString(pattern, context, opts);
|
|
64
64
|
const absolute = isAbsolute(rendered) ? rendered : resolve(context.paths.work_dir, rendered);
|
|
65
65
|
if (!/[*?]/.test(absolute))
|
|
66
66
|
return isFilePath(absolute) ? [absolute] : [];
|
|
@@ -118,7 +118,7 @@ export function copyConfiguredArtifacts(fn, context, opts = {}) {
|
|
|
118
118
|
const seen = new Set();
|
|
119
119
|
const copied = [];
|
|
120
120
|
for (const pattern of patterns) {
|
|
121
|
-
for (const source of findFilesByArtifactPattern(pattern, context)) {
|
|
121
|
+
for (const source of findFilesByArtifactPattern(pattern, context, { strict: fn.definition.strict_params === true })) {
|
|
122
122
|
if (seen.has(source))
|
|
123
123
|
continue;
|
|
124
124
|
seen.add(source);
|
package/dist/functions.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RunResult, type Schema } from 'clyops-tools';
|
|
2
|
-
import { type TemplateContext } from './template.js';
|
|
2
|
+
import { type TemplateContext, type TemplateOptions } from './template.js';
|
|
3
3
|
import { type Json } from './util.js';
|
|
4
4
|
/**
|
|
5
5
|
* Route diagnostics. `warn` and `verbose` take a printf-style format and
|
|
@@ -29,6 +29,8 @@ export interface FunctionDefinition {
|
|
|
29
29
|
artifacts?: unknown;
|
|
30
30
|
/** Task result or configured result templates, according to the containing interface. */
|
|
31
31
|
result?: unknown;
|
|
32
|
+
/** A missing `params` field in a template is an error instead of unset (default false). */
|
|
33
|
+
strict_params?: boolean;
|
|
32
34
|
[key: string]: unknown;
|
|
33
35
|
}
|
|
34
36
|
/** A configured function with resolved executable and merged defaults and overrides. */
|
|
@@ -63,6 +65,8 @@ export interface CommandOptions {
|
|
|
63
65
|
*/
|
|
64
66
|
defaultPositional?: string;
|
|
65
67
|
}
|
|
68
|
+
/** The template options a function's definition asks for. */
|
|
69
|
+
export declare function functionTemplateOptions(fn: ResolvedFunction): TemplateOptions;
|
|
66
70
|
/** The full command line for a function: [script, ...argv]. */
|
|
67
71
|
export declare function buildFunctionCommand(fn: ResolvedFunction, opts: CommandOptions, schema?: Schema): Promise<string[]>;
|
|
68
72
|
/** The completed tool execution with a color-stripped stderr tail. */
|
package/dist/functions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../src/functions.ts"],"names":[],"mappings":"AASA,OAAO,EAAsG,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAI/J,OAAO,
|
|
1
|
+
{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../src/functions.ts"],"names":[],"mappings":"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"}
|
package/dist/functions.js
CHANGED
|
@@ -11,7 +11,7 @@ import { declaredRoles, inputKeys, loadSchema, redactArgv, start, shellQuote, ta
|
|
|
11
11
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
12
12
|
import { finished } from 'node:stream/promises';
|
|
13
13
|
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
14
|
-
import { renderConfigValue, renderResultMap, renderTemplateString } from './template.js';
|
|
14
|
+
import { isUnsetTemplate, renderConfigValue, renderResultMap, renderTemplateString } from './template.js';
|
|
15
15
|
import { deepMerge, ensureDir, objectValue, resolveScriptPath } from './util.js';
|
|
16
16
|
/** Where engine diagnostics go; a trigger installs its own with configureLogging. */
|
|
17
17
|
const LOGGER = {
|
|
@@ -42,6 +42,24 @@ export function resolveFunctionConfig(scriptRoot, routeConfig, functionName) {
|
|
|
42
42
|
const config = deepMerge(objectValue(objectValue(routeConfig.defaults)[key]), objectValue(routeConfig[key]));
|
|
43
43
|
return { functionName, key, script: resolveScriptPath(scriptRoot, script), config, definition };
|
|
44
44
|
}
|
|
45
|
+
/** The template options a function's definition asks for. */
|
|
46
|
+
export function functionTemplateOptions(fn) {
|
|
47
|
+
return { strict: fn.definition.strict_params === true };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The options with `params` templates that name a missing or null field left
|
|
51
|
+
* out, so the tool's own default applies: such an option is dropped, alone or
|
|
52
|
+
* inside a longer string, and so is such an item in an array.
|
|
53
|
+
*/
|
|
54
|
+
function withoutUnsetParams(config, context, opts) {
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const [key, value] of Object.entries(config)) {
|
|
57
|
+
if (isUnsetTemplate(value, context, opts))
|
|
58
|
+
continue;
|
|
59
|
+
out[key] = Array.isArray(value) ? value.filter((item) => !isUnsetTemplate(item, context, opts)) : value;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
45
63
|
/**
|
|
46
64
|
* The positionals for a function: its `positionals` templates when set
|
|
47
65
|
* (authoritative; `[]` for none), else none here and the schema's arguments
|
|
@@ -50,21 +68,27 @@ export function resolveFunctionConfig(scriptRoot, routeConfig, functionName) {
|
|
|
50
68
|
function explicitPositionals(fn, context) {
|
|
51
69
|
if (!Array.isArray(fn.definition.positionals))
|
|
52
70
|
return undefined;
|
|
53
|
-
|
|
71
|
+
const templates = functionTemplateOptions(fn);
|
|
72
|
+
return fn.definition.positionals
|
|
73
|
+
.filter((item) => !context || !isUnsetTemplate(item, context, templates))
|
|
74
|
+
.map((item) => String(renderConfigValue(item, context, templates)))
|
|
75
|
+
.filter(Boolean);
|
|
54
76
|
}
|
|
55
77
|
/** The full command line for a function: [script, ...argv]. */
|
|
56
78
|
export async function buildFunctionCommand(fn, opts, schema) {
|
|
57
79
|
schema ??= await loadSchema(fn.script);
|
|
58
|
-
const
|
|
80
|
+
const context = opts.context;
|
|
81
|
+
const templates = functionTemplateOptions(fn);
|
|
82
|
+
// First, so an argument left unset still takes the default positional.
|
|
83
|
+
const config = context ? withoutUnsetParams(fn.config, context, templates) : { ...fn.config };
|
|
59
84
|
const first = schema.arguments[0];
|
|
60
85
|
if (opts.defaultPositional && first && !Array.isArray(fn.definition.positionals) && !inputKeys(first).some((k) => k in config)) {
|
|
61
86
|
config[inputKeys(first)[0]] = opts.defaultPositional;
|
|
62
87
|
}
|
|
63
|
-
const context = opts.context;
|
|
64
88
|
const { argv, unknown, controlled } = toArgv(schema, config, {
|
|
65
89
|
base: opts.configRoot,
|
|
66
90
|
controlled: opts.controlled ?? ['help'],
|
|
67
|
-
render: context ? (s) => renderTemplateString(s, context) : undefined,
|
|
91
|
+
render: context ? (s) => renderTemplateString(s, context, templates) : undefined,
|
|
68
92
|
positionals: explicitPositionals(fn, context),
|
|
69
93
|
positionalsOrder: fn.definition.positionals_order,
|
|
70
94
|
});
|
|
@@ -87,10 +111,10 @@ export function parseRoleJson(text) {
|
|
|
87
111
|
}
|
|
88
112
|
}
|
|
89
113
|
/** A function's `stdin` or `stdout` template rendered, a relative one against `base`. */
|
|
90
|
-
function streamPath(template, opts, base) {
|
|
114
|
+
function streamPath(template, opts, base, templates) {
|
|
91
115
|
if (!template)
|
|
92
116
|
return undefined;
|
|
93
|
-
const rendered = opts.context ? renderTemplateString(template, opts.context) : template;
|
|
117
|
+
const rendered = opts.context ? renderTemplateString(template, opts.context, templates) : template;
|
|
94
118
|
return isAbsolute(rendered) ? rendered : resolve(base, rendered);
|
|
95
119
|
}
|
|
96
120
|
/**
|
|
@@ -105,8 +129,8 @@ export async function runScriptFunction(fn, opts) {
|
|
|
105
129
|
LOGGER.verbose('command: %s', shellQuote(command));
|
|
106
130
|
// stdin is input, like a path option; stdout is an artifact, found like one.
|
|
107
131
|
const workDir = opts.context?.paths?.work_dir;
|
|
108
|
-
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot);
|
|
109
|
-
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot);
|
|
132
|
+
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot, functionTemplateOptions(fn));
|
|
133
|
+
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot, functionTemplateOptions(fn));
|
|
110
134
|
// Wire the well-known roles the tool declares: progress onto the record, meta into the run.
|
|
111
135
|
let progress;
|
|
112
136
|
const roles = {
|
|
@@ -148,6 +172,6 @@ export function buildResultRecord(p) {
|
|
|
148
172
|
artifact_keys: p.artifactKeys,
|
|
149
173
|
completed_at: p.now,
|
|
150
174
|
config: p.fn.config,
|
|
151
|
-
...renderResultMap(p.fn.definition.result, p.context),
|
|
175
|
+
...renderResultMap(p.fn.definition.result, p.context, functionTemplateOptions(p.fn)),
|
|
152
176
|
};
|
|
153
177
|
}
|
package/dist/template.d.ts
CHANGED
|
@@ -2,16 +2,31 @@
|
|
|
2
2
|
export type TemplateContext = {
|
|
3
3
|
[key: string]: unknown;
|
|
4
4
|
};
|
|
5
|
-
/**
|
|
6
|
-
export
|
|
5
|
+
/** How templates treat missing values. */
|
|
6
|
+
export interface TemplateOptions {
|
|
7
|
+
/** Every unknown path is an error, `params` too (default false: a missing `params` field renders `''`, and an option naming one is unset). */
|
|
8
|
+
strict?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A dot-path value from the context; objects become JSON and null is `''`.
|
|
12
|
+
* An unknown path is an error, except a missing `params` field, which is `''`
|
|
13
|
+
* unless `opts.strict`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function lookupTemplateValue(context: TemplateContext, expression: string, opts?: TemplateOptions): string;
|
|
16
|
+
/**
|
|
17
|
+
* True when `value` names a `params` field that is missing or null, alone
|
|
18
|
+
* (`${params.x}`) or inside a longer string (`v-${params.x}`), so an option
|
|
19
|
+
* or positional set to it is left unset. Always false with `opts.strict`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isUnsetTemplate(value: unknown, context: TemplateContext, opts?: TemplateOptions): boolean;
|
|
7
22
|
/** Replace dotted-path ${expression} and {{expression}} templates using the context. */
|
|
8
|
-
export declare function renderTemplateString(value: string, context: TemplateContext): string;
|
|
23
|
+
export declare function renderTemplateString(value: string, context: TemplateContext, opts?: TemplateOptions): string;
|
|
9
24
|
/** Render strings and the strings in arrays; other scalars keep their type. */
|
|
10
|
-
export declare function renderConfigValue(value: unknown, context?: TemplateContext | null): unknown;
|
|
25
|
+
export declare function renderConfigValue(value: unknown, context?: TemplateContext | null, opts?: TemplateOptions): unknown;
|
|
11
26
|
/** Deep-render a `result` map value: strings, arrays and nested objects. */
|
|
12
|
-
export declare function renderResultValue(value: unknown, context: TemplateContext): unknown;
|
|
27
|
+
export declare function renderResultValue(value: unknown, context: TemplateContext, opts?: TemplateOptions): unknown;
|
|
13
28
|
/** Render a function's optional `result` map into extra result fields. */
|
|
14
|
-
export declare function renderResultMap(resultMap: unknown, context: TemplateContext): {
|
|
29
|
+
export declare function renderResultMap(resultMap: unknown, context: TemplateContext, opts?: TemplateOptions): {
|
|
15
30
|
[key: string]: unknown;
|
|
16
31
|
};
|
|
17
32
|
//# sourceMappingURL=template.d.ts.map
|
package/dist/template.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"AACA,4DAA4D;AAC5D,MAAM,MAAM,eAAe,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAEzD,
|
|
1
|
+
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"AACA,4DAA4D;AAC5D,MAAM,MAAM,eAAe,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAEzD,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,8IAA8I;IAC9I,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAqBD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,MAAM,CAQhH;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CASzG;AAED,wFAAwF;AACxF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,MAAM,CAE5G;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAKnH;AAED,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAO3G;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAGhI"}
|
package/dist/template.js
CHANGED
|
@@ -1,44 +1,82 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Caller-supplied fields: one a request leaves out is unset, not a config error.
|
|
2
|
+
const OPTIONAL_ROOTS = ['params'];
|
|
3
|
+
const TEMPLATE = /\$\{\s*([A-Za-z0-9_.-]+)\s*\}|\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g;
|
|
4
|
+
// The value at a dot path, or MISSING when the path is not there.
|
|
5
|
+
const MISSING = Symbol('missing');
|
|
6
|
+
function resolvePath(context, expression) {
|
|
3
7
|
let current = context;
|
|
4
8
|
for (const part of String(expression).split('.').filter(Boolean)) {
|
|
5
|
-
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part))
|
|
6
|
-
|
|
7
|
-
}
|
|
9
|
+
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part))
|
|
10
|
+
return MISSING;
|
|
8
11
|
current = current[part];
|
|
9
12
|
}
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
function optionalRoot(expression, opts) {
|
|
16
|
+
return !opts?.strict && OPTIONAL_ROOTS.includes(String(expression).split('.').filter(Boolean)[0] ?? '');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A dot-path value from the context; objects become JSON and null is `''`.
|
|
20
|
+
* An unknown path is an error, except a missing `params` field, which is `''`
|
|
21
|
+
* unless `opts.strict`.
|
|
22
|
+
*/
|
|
23
|
+
export function lookupTemplateValue(context, expression, opts) {
|
|
24
|
+
const current = resolvePath(context, expression);
|
|
25
|
+
if (current === MISSING) {
|
|
26
|
+
if (optionalRoot(expression, opts))
|
|
27
|
+
return '';
|
|
28
|
+
throw new Error(`unknown template value: ${expression}`);
|
|
29
|
+
}
|
|
10
30
|
if (current === undefined || current === null)
|
|
11
31
|
return '';
|
|
12
32
|
return typeof current === 'object' ? JSON.stringify(current) : String(current);
|
|
13
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* True when `value` names a `params` field that is missing or null, alone
|
|
36
|
+
* (`${params.x}`) or inside a longer string (`v-${params.x}`), so an option
|
|
37
|
+
* or positional set to it is left unset. Always false with `opts.strict`.
|
|
38
|
+
*/
|
|
39
|
+
export function isUnsetTemplate(value, context, opts) {
|
|
40
|
+
if (typeof value !== 'string')
|
|
41
|
+
return false;
|
|
42
|
+
for (const [, dollar, brace] of value.matchAll(TEMPLATE)) {
|
|
43
|
+
const expression = dollar || brace;
|
|
44
|
+
if (!optionalRoot(expression, opts))
|
|
45
|
+
continue;
|
|
46
|
+
const current = resolvePath(context, expression);
|
|
47
|
+
if (current === MISSING || current === undefined || current === null)
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
14
52
|
/** Replace dotted-path ${expression} and {{expression}} templates using the context. */
|
|
15
|
-
export function renderTemplateString(value, context) {
|
|
16
|
-
return String(value).replace(
|
|
53
|
+
export function renderTemplateString(value, context, opts) {
|
|
54
|
+
return String(value).replace(TEMPLATE, (_, dollar, brace) => lookupTemplateValue(context, dollar || brace, opts));
|
|
17
55
|
}
|
|
18
56
|
/** Render strings and the strings in arrays; other scalars keep their type. */
|
|
19
|
-
export function renderConfigValue(value, context) {
|
|
57
|
+
export function renderConfigValue(value, context, opts) {
|
|
20
58
|
if (!context)
|
|
21
59
|
return value;
|
|
22
60
|
if (typeof value === 'string')
|
|
23
|
-
return renderTemplateString(value, context);
|
|
61
|
+
return renderTemplateString(value, context, opts);
|
|
24
62
|
if (Array.isArray(value))
|
|
25
|
-
return value.map((item) => renderConfigValue(item, context));
|
|
63
|
+
return value.map((item) => renderConfigValue(item, context, opts));
|
|
26
64
|
return value;
|
|
27
65
|
}
|
|
28
66
|
/** Deep-render a `result` map value: strings, arrays and nested objects. */
|
|
29
|
-
export function renderResultValue(value, context) {
|
|
67
|
+
export function renderResultValue(value, context, opts) {
|
|
30
68
|
if (typeof value === 'string')
|
|
31
|
-
return renderTemplateString(value, context);
|
|
69
|
+
return renderTemplateString(value, context, opts);
|
|
32
70
|
if (Array.isArray(value))
|
|
33
|
-
return value.map((item) => renderResultValue(item, context));
|
|
71
|
+
return value.map((item) => renderResultValue(item, context, opts));
|
|
34
72
|
if (value && typeof value === 'object') {
|
|
35
|
-
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context)]));
|
|
73
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context, opts)]));
|
|
36
74
|
}
|
|
37
75
|
return value;
|
|
38
76
|
}
|
|
39
77
|
/** Render a function's optional `result` map into extra result fields. */
|
|
40
|
-
export function renderResultMap(resultMap, context) {
|
|
78
|
+
export function renderResultMap(resultMap, context, opts) {
|
|
41
79
|
if (!resultMap || typeof resultMap !== 'object' || Array.isArray(resultMap))
|
|
42
80
|
return {};
|
|
43
|
-
return renderResultValue(resultMap, context);
|
|
81
|
+
return renderResultValue(resultMap, context, opts);
|
|
44
82
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clyops-jobs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Run clyops tools as jobs: config-bound functions, templated invocation, artifacts, job records and a queue.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"typescript": "^5.6.0"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"clyops-tools": "0.
|
|
41
|
+
"clyops-tools": "0.10.0"
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://clyops.io/node-tools.html",
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsc -p tsconfig.json",
|
|
46
46
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
47
|
-
"test": "node --test test/*.test.mjs"
|
|
47
|
+
"test": "node --import ../../tools/test-leaks.mjs --test test/*.test.mjs"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/artifacts.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { basename, dirname, isAbsolute, join, normalize, parse, resolve } from 'node:path';
|
|
4
4
|
import type { ResolvedFunction } from './functions.js';
|
|
5
|
-
import { renderTemplateString, type TemplateContext } from './template.js';
|
|
5
|
+
import { renderTemplateString, type TemplateContext, type TemplateOptions } from './template.js';
|
|
6
6
|
import { copyFileEnsuringDir, isDirectoryPath, isFilePath, statFollowingSymlinks, truthy } from './util.js';
|
|
7
7
|
|
|
8
8
|
// A small glob subset as a regex: `*`, `?`, `**` and `**/` (which also matches no
|
|
@@ -53,8 +53,8 @@ export function listFilesRecursive(dir: string, visited = new Set<string>()): st
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** Files matching a templated glob; relative patterns start at `paths.work_dir`. */
|
|
56
|
-
export function findFilesByArtifactPattern(pattern: string, context: TemplateContext & { paths: { work_dir: string } }): string[] {
|
|
57
|
-
const rendered = renderTemplateString(pattern, context);
|
|
56
|
+
export function findFilesByArtifactPattern(pattern: string, context: TemplateContext & { paths: { work_dir: string } }, opts?: TemplateOptions): string[] {
|
|
57
|
+
const rendered = renderTemplateString(pattern, context, opts);
|
|
58
58
|
const absolute = isAbsolute(rendered) ? rendered : resolve(context.paths.work_dir, rendered);
|
|
59
59
|
if (!/[*?]/.test(absolute)) return isFilePath(absolute) ? [absolute] : [];
|
|
60
60
|
const regex = globToRegex(absolute);
|
|
@@ -118,7 +118,7 @@ export function copyConfiguredArtifacts(
|
|
|
118
118
|
const seen = new Set<string>();
|
|
119
119
|
const copied: CopiedArtifact[] = [];
|
|
120
120
|
for (const pattern of patterns) {
|
|
121
|
-
for (const source of findFilesByArtifactPattern(pattern, context)) {
|
|
121
|
+
for (const source of findFilesByArtifactPattern(pattern, context, { strict: fn.definition.strict_params === true })) {
|
|
122
122
|
if (seen.has(source)) continue;
|
|
123
123
|
seen.add(source);
|
|
124
124
|
const destination = join(dir, basename(source));
|
package/src/functions.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { declaredRoles, inputKeys, loadSchema, redactArgv, start, shellQuote, ta
|
|
|
11
11
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
12
12
|
import { finished } from 'node:stream/promises';
|
|
13
13
|
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
14
|
-
import { renderConfigValue, renderResultMap, renderTemplateString, type TemplateContext } from './template.js';
|
|
14
|
+
import { isUnsetTemplate, renderConfigValue, renderResultMap, renderTemplateString, type TemplateContext, type TemplateOptions } from './template.js';
|
|
15
15
|
import { deepMerge, ensureDir, objectValue, resolveScriptPath, type Json } from './util.js';
|
|
16
16
|
|
|
17
17
|
/** Where engine diagnostics go; a trigger installs its own with configureLogging. */
|
|
@@ -48,6 +48,8 @@ export interface FunctionDefinition {
|
|
|
48
48
|
artifacts?: unknown;
|
|
49
49
|
/** Task result or configured result templates, according to the containing interface. */
|
|
50
50
|
result?: unknown;
|
|
51
|
+
/** A missing `params` field in a template is an error instead of unset (default false). */
|
|
52
|
+
strict_params?: boolean;
|
|
51
53
|
[key: string]: unknown;
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -94,6 +96,25 @@ export interface CommandOptions {
|
|
|
94
96
|
defaultPositional?: string;
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
/** The template options a function's definition asks for. */
|
|
100
|
+
export function functionTemplateOptions(fn: ResolvedFunction): TemplateOptions {
|
|
101
|
+
return { strict: fn.definition.strict_params === true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The options with `params` templates that name a missing or null field left
|
|
106
|
+
* out, so the tool's own default applies: such an option is dropped, alone or
|
|
107
|
+
* inside a longer string, and so is such an item in an array.
|
|
108
|
+
*/
|
|
109
|
+
function withoutUnsetParams(config: Json, context: TemplateContext, opts: TemplateOptions): Json {
|
|
110
|
+
const out: Json = {};
|
|
111
|
+
for (const [key, value] of Object.entries(config)) {
|
|
112
|
+
if (isUnsetTemplate(value, context, opts)) continue;
|
|
113
|
+
out[key] = Array.isArray(value) ? value.filter((item) => !isUnsetTemplate(item, context, opts)) : value;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
/**
|
|
98
119
|
* The positionals for a function: its `positionals` templates when set
|
|
99
120
|
* (authoritative; `[]` for none), else none here and the schema's arguments
|
|
@@ -101,22 +122,28 @@ export interface CommandOptions {
|
|
|
101
122
|
*/
|
|
102
123
|
function explicitPositionals(fn: ResolvedFunction, context?: TemplateContext | null): string[] | undefined {
|
|
103
124
|
if (!Array.isArray(fn.definition.positionals)) return undefined;
|
|
104
|
-
|
|
125
|
+
const templates = functionTemplateOptions(fn);
|
|
126
|
+
return fn.definition.positionals
|
|
127
|
+
.filter((item) => !context || !isUnsetTemplate(item, context, templates))
|
|
128
|
+
.map((item) => String(renderConfigValue(item, context, templates)))
|
|
129
|
+
.filter(Boolean);
|
|
105
130
|
}
|
|
106
131
|
|
|
107
132
|
/** The full command line for a function: [script, ...argv]. */
|
|
108
133
|
export async function buildFunctionCommand(fn: ResolvedFunction, opts: CommandOptions, schema?: Schema): Promise<string[]> {
|
|
109
134
|
schema ??= await loadSchema(fn.script);
|
|
110
|
-
const
|
|
135
|
+
const context = opts.context;
|
|
136
|
+
const templates = functionTemplateOptions(fn);
|
|
137
|
+
// First, so an argument left unset still takes the default positional.
|
|
138
|
+
const config: Json = context ? withoutUnsetParams(fn.config, context, templates) : { ...fn.config };
|
|
111
139
|
const first = schema.arguments[0];
|
|
112
140
|
if (opts.defaultPositional && first && !Array.isArray(fn.definition.positionals) && !inputKeys(first).some((k) => k in config)) {
|
|
113
141
|
config[inputKeys(first)[0]] = opts.defaultPositional;
|
|
114
142
|
}
|
|
115
|
-
const context = opts.context;
|
|
116
143
|
const { argv, unknown, controlled } = toArgv(schema, config, {
|
|
117
144
|
base: opts.configRoot,
|
|
118
145
|
controlled: opts.controlled ?? ['help'],
|
|
119
|
-
render: context ? (s) => renderTemplateString(s, context) : undefined,
|
|
146
|
+
render: context ? (s) => renderTemplateString(s, context, templates) : undefined,
|
|
120
147
|
positionals: explicitPositionals(fn, context),
|
|
121
148
|
positionalsOrder: fn.definition.positionals_order,
|
|
122
149
|
});
|
|
@@ -147,9 +174,9 @@ export function parseRoleJson(text: string | undefined): unknown {
|
|
|
147
174
|
}
|
|
148
175
|
|
|
149
176
|
/** A function's `stdin` or `stdout` template rendered, a relative one against `base`. */
|
|
150
|
-
function streamPath(template: string | undefined, opts: CommandOptions, base: string): string | undefined {
|
|
177
|
+
function streamPath(template: string | undefined, opts: CommandOptions, base: string, templates: TemplateOptions): string | undefined {
|
|
151
178
|
if (!template) return undefined;
|
|
152
|
-
const rendered = opts.context ? renderTemplateString(template, opts.context) : template;
|
|
179
|
+
const rendered = opts.context ? renderTemplateString(template, opts.context, templates) : template;
|
|
153
180
|
return isAbsolute(rendered) ? rendered : resolve(base, rendered);
|
|
154
181
|
}
|
|
155
182
|
|
|
@@ -177,8 +204,8 @@ export async function runScriptFunction(
|
|
|
177
204
|
LOGGER.verbose('command: %s', shellQuote(command));
|
|
178
205
|
// stdin is input, like a path option; stdout is an artifact, found like one.
|
|
179
206
|
const workDir = (opts.context?.paths as { work_dir?: string } | undefined)?.work_dir;
|
|
180
|
-
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot);
|
|
181
|
-
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot);
|
|
207
|
+
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot, functionTemplateOptions(fn));
|
|
208
|
+
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot, functionTemplateOptions(fn));
|
|
182
209
|
// Wire the well-known roles the tool declares: progress onto the record, meta into the run.
|
|
183
210
|
let progress: Record<string, unknown> | undefined;
|
|
184
211
|
const roles: Record<string, RoleOption> = {
|
|
@@ -228,6 +255,6 @@ export function buildResultRecord(p: {
|
|
|
228
255
|
artifact_keys: p.artifactKeys,
|
|
229
256
|
completed_at: p.now,
|
|
230
257
|
config: p.fn.config,
|
|
231
|
-
...renderResultMap(p.fn.definition.result, p.context),
|
|
258
|
+
...renderResultMap(p.fn.definition.result, p.context, functionTemplateOptions(p.fn)),
|
|
232
259
|
};
|
|
233
260
|
}
|
package/src/template.ts
CHANGED
|
@@ -2,46 +2,87 @@
|
|
|
2
2
|
/** Values available to dotted-path template expressions. */
|
|
3
3
|
export type TemplateContext = { [key: string]: unknown };
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
export
|
|
5
|
+
/** How templates treat missing values. */
|
|
6
|
+
export interface TemplateOptions {
|
|
7
|
+
/** Every unknown path is an error, `params` too (default false: a missing `params` field renders `''`, and an option naming one is unset). */
|
|
8
|
+
strict?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Caller-supplied fields: one a request leaves out is unset, not a config error.
|
|
12
|
+
const OPTIONAL_ROOTS = ['params'];
|
|
13
|
+
const TEMPLATE = /\$\{\s*([A-Za-z0-9_.-]+)\s*\}|\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g;
|
|
14
|
+
|
|
15
|
+
// The value at a dot path, or MISSING when the path is not there.
|
|
16
|
+
const MISSING = Symbol('missing');
|
|
17
|
+
function resolvePath(context: TemplateContext, expression: string): unknown {
|
|
7
18
|
let current: unknown = context;
|
|
8
19
|
for (const part of String(expression).split('.').filter(Boolean)) {
|
|
9
|
-
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part))
|
|
10
|
-
throw new Error(`unknown template value: ${expression}`);
|
|
11
|
-
}
|
|
20
|
+
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part)) return MISSING;
|
|
12
21
|
current = (current as TemplateContext)[part];
|
|
13
22
|
}
|
|
23
|
+
return current;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function optionalRoot(expression: string, opts?: TemplateOptions): boolean {
|
|
27
|
+
return !opts?.strict && OPTIONAL_ROOTS.includes(String(expression).split('.').filter(Boolean)[0] ?? '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A dot-path value from the context; objects become JSON and null is `''`.
|
|
32
|
+
* An unknown path is an error, except a missing `params` field, which is `''`
|
|
33
|
+
* unless `opts.strict`.
|
|
34
|
+
*/
|
|
35
|
+
export function lookupTemplateValue(context: TemplateContext, expression: string, opts?: TemplateOptions): string {
|
|
36
|
+
const current = resolvePath(context, expression);
|
|
37
|
+
if (current === MISSING) {
|
|
38
|
+
if (optionalRoot(expression, opts)) return '';
|
|
39
|
+
throw new Error(`unknown template value: ${expression}`);
|
|
40
|
+
}
|
|
14
41
|
if (current === undefined || current === null) return '';
|
|
15
42
|
return typeof current === 'object' ? JSON.stringify(current) : String(current);
|
|
16
43
|
}
|
|
17
44
|
|
|
45
|
+
/**
|
|
46
|
+
* True when `value` names a `params` field that is missing or null, alone
|
|
47
|
+
* (`${params.x}`) or inside a longer string (`v-${params.x}`), so an option
|
|
48
|
+
* or positional set to it is left unset. Always false with `opts.strict`.
|
|
49
|
+
*/
|
|
50
|
+
export function isUnsetTemplate(value: unknown, context: TemplateContext, opts?: TemplateOptions): boolean {
|
|
51
|
+
if (typeof value !== 'string') return false;
|
|
52
|
+
for (const [, dollar, brace] of value.matchAll(TEMPLATE)) {
|
|
53
|
+
const expression = dollar || brace;
|
|
54
|
+
if (!optionalRoot(expression, opts)) continue;
|
|
55
|
+
const current = resolvePath(context, expression);
|
|
56
|
+
if (current === MISSING || current === undefined || current === null) return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
18
61
|
/** Replace dotted-path ${expression} and {{expression}} templates using the context. */
|
|
19
|
-
export function renderTemplateString(value: string, context: TemplateContext): string {
|
|
20
|
-
return String(value).replace(
|
|
21
|
-
lookupTemplateValue(context, dollar || brace),
|
|
22
|
-
);
|
|
62
|
+
export function renderTemplateString(value: string, context: TemplateContext, opts?: TemplateOptions): string {
|
|
63
|
+
return String(value).replace(TEMPLATE, (_, dollar, brace) => lookupTemplateValue(context, dollar || brace, opts));
|
|
23
64
|
}
|
|
24
65
|
|
|
25
66
|
/** Render strings and the strings in arrays; other scalars keep their type. */
|
|
26
|
-
export function renderConfigValue(value: unknown, context?: TemplateContext | null): unknown {
|
|
67
|
+
export function renderConfigValue(value: unknown, context?: TemplateContext | null, opts?: TemplateOptions): unknown {
|
|
27
68
|
if (!context) return value;
|
|
28
|
-
if (typeof value === 'string') return renderTemplateString(value, context);
|
|
29
|
-
if (Array.isArray(value)) return value.map((item) => renderConfigValue(item, context));
|
|
69
|
+
if (typeof value === 'string') return renderTemplateString(value, context, opts);
|
|
70
|
+
if (Array.isArray(value)) return value.map((item) => renderConfigValue(item, context, opts));
|
|
30
71
|
return value;
|
|
31
72
|
}
|
|
32
73
|
|
|
33
74
|
/** Deep-render a `result` map value: strings, arrays and nested objects. */
|
|
34
|
-
export function renderResultValue(value: unknown, context: TemplateContext): unknown {
|
|
35
|
-
if (typeof value === 'string') return renderTemplateString(value, context);
|
|
36
|
-
if (Array.isArray(value)) return value.map((item) => renderResultValue(item, context));
|
|
75
|
+
export function renderResultValue(value: unknown, context: TemplateContext, opts?: TemplateOptions): unknown {
|
|
76
|
+
if (typeof value === 'string') return renderTemplateString(value, context, opts);
|
|
77
|
+
if (Array.isArray(value)) return value.map((item) => renderResultValue(item, context, opts));
|
|
37
78
|
if (value && typeof value === 'object') {
|
|
38
|
-
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context)]));
|
|
79
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context, opts)]));
|
|
39
80
|
}
|
|
40
81
|
return value;
|
|
41
82
|
}
|
|
42
83
|
|
|
43
84
|
/** Render a function's optional `result` map into extra result fields. */
|
|
44
|
-
export function renderResultMap(resultMap: unknown, context: TemplateContext): { [key: string]: unknown } {
|
|
85
|
+
export function renderResultMap(resultMap: unknown, context: TemplateContext, opts?: TemplateOptions): { [key: string]: unknown } {
|
|
45
86
|
if (!resultMap || typeof resultMap !== 'object' || Array.isArray(resultMap)) return {};
|
|
46
|
-
return renderResultValue(resultMap, context) as { [key: string]: unknown };
|
|
87
|
+
return renderResultValue(resultMap, context, opts) as { [key: string]: unknown };
|
|
47
88
|
}
|