@nexusbloom/cli 0.9.2 → 0.9.4
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 +27 -11
- package/package.json +1 -1
- package/src/commands.js +8 -12
- package/src/values.js +121 -0
- package/src/wizard.js +145 -15
package/README.md
CHANGED
|
@@ -209,6 +209,33 @@ $ nxb pipe --build
|
|
|
209
209
|
frequency-analyzer.total_words integer 2
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
+
When you wire a **collection** (an array or object) into a field, the builder
|
|
213
|
+
offers the whole transform catalogue rather than only flagging a type clash —
|
|
214
|
+
`len()` on an array, `join(",")`, `keys()`, `pick(field)` and the rest, listed
|
|
215
|
+
with what each one does. You can chain them, and they are applied in the order
|
|
216
|
+
you chose:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
frequency-analyzer → count integer
|
|
220
|
+
Apply a transform to count? yes
|
|
221
|
+
Transform › len
|
|
222
|
+
len length of a string, array or object
|
|
223
|
+
frequency-analyzer count=$character_frequency|len()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
This produces the same `$path|transform()` syntax you can write by hand. Two
|
|
227
|
+
constraints come from the parser, and the builder refuses bad input up front
|
|
228
|
+
rather than emitting a spec that will not run:
|
|
229
|
+
|
|
230
|
+
- **No spaces anywhere in a connection.** `$items|join(", ")` is rejected — use
|
|
231
|
+
`join(",")`. The parser cannot distinguish a space inside a quoted argument
|
|
232
|
+
from a separator, so it refuses all of them.
|
|
233
|
+
- **`|` cannot appear in an argument**, because it separates pipeline steps.
|
|
234
|
+
|
|
235
|
+
A scalar type clash keeps its existing one-tap suggestion (`apply int()?`),
|
|
236
|
+
since that is a fix rather than a choice. Run `nxb pipe --transforms` for the
|
|
237
|
+
full catalogue with examples.
|
|
238
|
+
|
|
212
239
|
### Saved pipelines
|
|
213
240
|
|
|
214
241
|
```bash
|
|
@@ -323,17 +350,6 @@ removed rather than resolved. Relative imports like `import { helper } from
|
|
|
323
350
|
`export const coreLogic = async () => {}` and plain `function coreLogic` are all
|
|
324
351
|
supported.
|
|
325
352
|
|
|
326
|
-
## Development
|
|
327
|
-
|
|
328
|
-
```bash
|
|
329
|
-
npm test # 500+ tests, no network required
|
|
330
|
-
npm run test:coverage # with a coverage report
|
|
331
|
-
npm run test:watch # re-run on change
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
The suite runs fully offline and is isolated from your real config, API key and
|
|
335
|
-
network — both properties are enforced by guard tests, not assumed.
|
|
336
|
-
|
|
337
353
|
## Security Notes
|
|
338
354
|
|
|
339
355
|
- No API keys or Supabase keys are shipped inside the package.
|
package/package.json
CHANGED
package/src/commands.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
runPipeline,
|
|
24
24
|
} from "./pipe.js";
|
|
25
25
|
import { inferType, schemaTypes, typeLabel } from "./types.js";
|
|
26
|
-
import { MISSING, getPath, preview, transformNames, parsePath, formatPath } from "./values.js";
|
|
26
|
+
import { MISSING, getPath, preview, transformNames, parsePath, formatPath, TRANSFORM_GROUPS } from "./values.js";
|
|
27
27
|
import { buildWizard } from "./wizard.js";
|
|
28
28
|
|
|
29
29
|
/** Shared manifest/tool lookup used by the plan, the runner and the wizard. */
|
|
@@ -150,17 +150,13 @@ export function listTransforms(jsonMode) {
|
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
152
|
console.log(chalk.bold("\n Connection transforms\n"));
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
["json / unjson", "serialise / parse"],
|
|
161
|
-
["default(v)", "supply a value when the path is missing"],
|
|
162
|
-
];
|
|
163
|
-
for (const [a, b] of rows) console.log(` ${chalk.cyan(a.padEnd(42))} ${chalk.dim(b)}`);
|
|
153
|
+
// Rendered from the shared catalogue so the list and the wizard's picker can
|
|
154
|
+
// never describe the same transform differently. The column is sized to the
|
|
155
|
+
// longest heading, which is wider than a fixed pad and left the docs ragged.
|
|
156
|
+
const width = Math.max(...TRANSFORM_GROUPS.map((g) => g.label.length)) + 2;
|
|
157
|
+
for (const group of TRANSFORM_GROUPS) {
|
|
158
|
+
console.log(` ${chalk.cyan(group.label.padEnd(width))} ${chalk.dim(group.doc)}`);
|
|
159
|
+
}
|
|
164
160
|
console.log(chalk.dim("\n Use: key=$path|transform (no spaces around |)\n"));
|
|
165
161
|
return { ok: true };
|
|
166
162
|
}
|
package/src/values.js
CHANGED
|
@@ -223,6 +223,127 @@ export function transformNames() {
|
|
|
223
223
|
return Object.keys(TRANSFORMS).sort();
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* One-line description per transform, for the wizard's picker.
|
|
228
|
+
*
|
|
229
|
+
* These live beside the implementations rather than in the UI so that adding a
|
|
230
|
+
* transform and describing it are the same edit — a picker entry with no
|
|
231
|
+
* description reads as a mystery, and a description with no entry is worse.
|
|
232
|
+
*/
|
|
233
|
+
export const TRANSFORM_DOCS = {
|
|
234
|
+
len: "length of a string, array or object",
|
|
235
|
+
count: "same as len — size of an array or string",
|
|
236
|
+
words: "number of whitespace-separated words",
|
|
237
|
+
lines: "number of lines",
|
|
238
|
+
chars: "number of characters",
|
|
239
|
+
first: "first element of an array",
|
|
240
|
+
last: "last element of an array",
|
|
241
|
+
get: "element at an index",
|
|
242
|
+
slice: "a range of an array or string",
|
|
243
|
+
join: "array to a string, with a separator",
|
|
244
|
+
keys: "an object's keys, as an array",
|
|
245
|
+
values: "an object's values, as an array",
|
|
246
|
+
reverse: "reverse an array",
|
|
247
|
+
sort: "sort an array, optionally by field",
|
|
248
|
+
upper: "UPPERCASE",
|
|
249
|
+
lower: "lowercase",
|
|
250
|
+
trim: "strip surrounding whitespace",
|
|
251
|
+
replace: "replace every occurrence of a substring",
|
|
252
|
+
pad: "pad the start to a width",
|
|
253
|
+
split: "string to an array on a separator",
|
|
254
|
+
str: "to string",
|
|
255
|
+
int: "to integer",
|
|
256
|
+
float: "to number",
|
|
257
|
+
num: "to number",
|
|
258
|
+
bool: "to true/false",
|
|
259
|
+
json: "serialise to a JSON string",
|
|
260
|
+
unjson: "parse a JSON string",
|
|
261
|
+
pick: "one field out of an object",
|
|
262
|
+
default: "value to use when the path is missing",
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The catalogue grouped by purpose, for `nxb pipe --transforms`.
|
|
267
|
+
*
|
|
268
|
+
* `label` is the heading as a user reads it — arg hints and all — so it is
|
|
269
|
+
* written out rather than derived from `TRANSFORM_ARITY` (which would render
|
|
270
|
+
* `get()` where the heading wants `get(i)`).
|
|
271
|
+
*/
|
|
272
|
+
export const TRANSFORM_GROUPS = [
|
|
273
|
+
{ label: "len / count / words / lines / chars", doc: "size of a string, array or object",
|
|
274
|
+
names: ["len", "count", "words", "lines", "chars"] },
|
|
275
|
+
{ label: "first / last / get(i) / slice(a,b)", doc: "reduce an array to a single value",
|
|
276
|
+
names: ["first", "last", "get", "slice"] },
|
|
277
|
+
{ label: "join(sep) / split(sep) / reverse / sort(key)", doc: "reshape collections",
|
|
278
|
+
names: ["join", "split", "reverse", "sort"] },
|
|
279
|
+
{ label: "keys / values / pick(field)", doc: "reach into an object",
|
|
280
|
+
names: ["keys", "values", "pick"] },
|
|
281
|
+
{ label: "upper / lower / trim / replace(a,b) / pad(n)", doc: "edit text",
|
|
282
|
+
names: ["upper", "lower", "trim", "replace", "pad"] },
|
|
283
|
+
{ label: "int() / float() / num / str / bool()", doc: "change type — required for casts",
|
|
284
|
+
names: ["int", "float", "num", "str", "bool"] },
|
|
285
|
+
{ label: "json / unjson", doc: "serialise / parse",
|
|
286
|
+
names: ["json", "unjson"] },
|
|
287
|
+
{ label: "default(v)", doc: "supply a value when the path is missing",
|
|
288
|
+
names: ["default"] },
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* How many arguments a transform takes, excluding the value itself.
|
|
293
|
+
* @returns {number} 0 for transforms callable with no argument
|
|
294
|
+
*/
|
|
295
|
+
export function transformArgCount(name) {
|
|
296
|
+
const arity = TRANSFORM_ARITY[name];
|
|
297
|
+
return arity === undefined ? 0 : Math.max(0, arity - 1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Render one literal as it must appear inside a pipeline spec.
|
|
302
|
+
*
|
|
303
|
+
* Bare words are left alone (`hello`, `2024-01-01`); anything that could be
|
|
304
|
+
* misread — a space, a comma, a quote — is JSON-quoted so `parseValueExpr`'s
|
|
305
|
+
* quote-aware splitter sees it as one argument.
|
|
306
|
+
*/
|
|
307
|
+
export function formatArg(value) {
|
|
308
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
309
|
+
if (value === null || value === undefined) return "";
|
|
310
|
+
const s = String(value);
|
|
311
|
+
if (s === "") return '""';
|
|
312
|
+
if (/^[\w.@/+-]+$/.test(s)) return s;
|
|
313
|
+
return JSON.stringify(s);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Render a transform call: `len()`, `get(0)`, `join(",")`.
|
|
318
|
+
*
|
|
319
|
+
* Always emits parens, even with no arguments, so the result reads as a call
|
|
320
|
+
* and round-trips through `parseTransform` unchanged.
|
|
321
|
+
*/
|
|
322
|
+
export function formatTransformCall(name, args = []) {
|
|
323
|
+
return `|${name}(${(args || []).map(formatArg).join(",")})`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Reject an argument the parser cannot accept.
|
|
328
|
+
*
|
|
329
|
+
* `parseValueExpr` rejects whitespace anywhere in an expression — it cannot
|
|
330
|
+
* tell a separator from a space inside a quoted argument, so `$x|join(", ")` is
|
|
331
|
+
* refused. Better to say so at the prompt than to emit a spec that fails to
|
|
332
|
+
* parse after the pipeline is already built.
|
|
333
|
+
*
|
|
334
|
+
* @returns {string|null} an error message, or null when the arg is usable
|
|
335
|
+
*/
|
|
336
|
+
export function validateTransformArg(raw) {
|
|
337
|
+
const s = String(raw ?? "");
|
|
338
|
+
if (/\s/.test(s)) {
|
|
339
|
+
return "arguments cannot contain spaces — the pipeline syntax has none. Try a symbol like \"-\" or \"%20\".";
|
|
340
|
+
}
|
|
341
|
+
if (s.includes("|")) {
|
|
342
|
+
return "arguments cannot contain \"|\" — it separates steps in a pipeline.";
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
226
347
|
/**
|
|
227
348
|
* Apply a chain of transforms to a value.
|
|
228
349
|
* @param {any} value
|
package/src/wizard.js
CHANGED
|
@@ -33,7 +33,18 @@ import { execInSandbox } from "./sandbox.js";
|
|
|
33
33
|
import { configGet } from "./tools.js";
|
|
34
34
|
import { runRemote } from "./run.js";
|
|
35
35
|
import { inferType, schemaTypes, typeLabel, compatibility } from "./types.js";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
formatPath,
|
|
38
|
+
parsePath,
|
|
39
|
+
preview,
|
|
40
|
+
transformNames,
|
|
41
|
+
transformArgCount,
|
|
42
|
+
formatTransformCall,
|
|
43
|
+
validateTransformArg,
|
|
44
|
+
TRANSFORM_DOCS,
|
|
45
|
+
TRANSFORMS,
|
|
46
|
+
MISSING,
|
|
47
|
+
} from "./values.js";
|
|
37
48
|
import { parseValueExpr, resolveAccessors } from "./pipe.js";
|
|
38
49
|
import { isInteractive } from "./ui.js";
|
|
39
50
|
import { renderHeader } from "./ui.js";
|
|
@@ -97,8 +108,16 @@ function renderObservedOutput(fields) {
|
|
|
97
108
|
|
|
98
109
|
// ─── Prompts ─────────────────────────────────────────────────────────────────
|
|
99
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Ask for a tool to add as the next step.
|
|
113
|
+
*
|
|
114
|
+
* A tool may be chosen more than once: the pipeline engine addresses steps
|
|
115
|
+
* positionally, so `base64-tool … | base64-tool text=$result action=decode`
|
|
116
|
+
* is a legal round-trip. (It was verified working before this restriction was
|
|
117
|
+
* lifted.) `exclude` remains for callers that genuinely need a subset.
|
|
118
|
+
*/
|
|
100
119
|
async function pickTool(tools, exclude = [], P = defaultPrompts) {
|
|
101
|
-
const pool = tools.filter((t) => !exclude.includes(t.slug));
|
|
120
|
+
const pool = exclude.length ? tools.filter((t) => !exclude.includes(t.slug)) : tools;
|
|
102
121
|
const source = async (term) => {
|
|
103
122
|
const hits = term ? rankTools(pool, term) : pool;
|
|
104
123
|
return hits.slice(0, 50).map((t) => ({
|
|
@@ -195,20 +214,134 @@ async function wireFields(manifest, prevSlug, prevFields, P = defaultPrompts) {
|
|
|
195
214
|
const source = prevFields.find((f) => formatPath(f.path) === choice.path);
|
|
196
215
|
const from = source ? inferType(source.value) : "unknown";
|
|
197
216
|
const compat = targetTypes.length ? compatibility(from, targetTypes) : { level: "ok" };
|
|
198
|
-
|
|
217
|
+
// Offer transforms where one can do something: a type clash makes one
|
|
218
|
+
// necessary, and a collection is what `len`/`first`/`join`/`keys` exist for.
|
|
219
|
+
// Asking after every wire would be a pointless extra question on the common
|
|
220
|
+
// scalar-to-scalar case, where no transform applies anyway.
|
|
221
|
+
const worthTransforming = compat.level === "cast" || from === "array" || from === "object";
|
|
222
|
+
const chain = worthTransforming
|
|
223
|
+
? await promptTransformChain({ field, targetTypes, compat, from, P })
|
|
224
|
+
: "";
|
|
225
|
+
|
|
226
|
+
assignments.push({ kind: "wire", text: `${field}=$${choice.path}${chain}` });
|
|
227
|
+
}
|
|
199
228
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
229
|
+
return assignments;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Ask which transforms to apply to a wired value.
|
|
234
|
+
*
|
|
235
|
+
* Two things are happening here, in order. First a *required* cast — if the
|
|
236
|
+
* source type cannot widen into the field's type, the suggestion is offered
|
|
237
|
+
* directly, because without it the pipeline cannot run. Then the whole
|
|
238
|
+
* catalogue: any field can be reshaped, not just the ones with a type clash,
|
|
239
|
+
* since "how many items came back" is a question `len()` answers and no type
|
|
240
|
+
* mismatch hints at.
|
|
241
|
+
*
|
|
242
|
+
* The returned string is appended to `$path`, so it already carries its own
|
|
243
|
+
* leading `|`.
|
|
244
|
+
*/
|
|
245
|
+
async function promptTransformChain({ field, targetTypes, compat, from, P }) {
|
|
246
|
+
// A collection feeding a scalar field is reported as a cast, but `first()` is
|
|
247
|
+
// a poor guess there — the usual intent is to measure or reshape it (`len`,
|
|
248
|
+
// `join`, `pick`). So collections go straight to the catalogue, which offers
|
|
249
|
+
// `first` anyway. A genuine scalar clash keeps its original single question:
|
|
250
|
+
// one tap to fix a type error beats making everyone walk a picker for it.
|
|
251
|
+
if (from === "array" || from === "object") {
|
|
252
|
+
let chain = "";
|
|
253
|
+
for (;;) {
|
|
254
|
+
const more = await P.confirm({
|
|
255
|
+
message: chain ? `Add another transform to ${field}?` : `Apply a transform to ${field}?`,
|
|
256
|
+
default: false,
|
|
204
257
|
});
|
|
205
|
-
if (
|
|
258
|
+
if (!more) break;
|
|
259
|
+
const built = await promptOneTransform(P);
|
|
260
|
+
if (!built) break;
|
|
261
|
+
chain += built;
|
|
206
262
|
}
|
|
263
|
+
return chain;
|
|
264
|
+
}
|
|
207
265
|
|
|
208
|
-
|
|
266
|
+
if (compat.level === "cast" && compat.suggestion) {
|
|
267
|
+
const use = await P.confirm({
|
|
268
|
+
message: `${field} expects ${typeLabel(targetTypes)} — apply ${chalk.cyan(compat.suggestion)}?`,
|
|
269
|
+
default: true,
|
|
270
|
+
});
|
|
271
|
+
if (use) return await buildSuggestedTransform(compat.suggestion, P);
|
|
209
272
|
}
|
|
210
273
|
|
|
211
|
-
return
|
|
274
|
+
return "";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Turn a `compatibility()` suggestion into a real transform call.
|
|
279
|
+
*
|
|
280
|
+
* Suggestions arrive as `int()`, `first()` — but also `pick(field)`, which
|
|
281
|
+
* names an argument it cannot fill in. Anything inside the parens is a
|
|
282
|
+
* placeholder rather than a value, so it prompts. `default()` is the awkward
|
|
283
|
+
* case: the suggestion shows no argument even though the transform needs one,
|
|
284
|
+
* so the arity is topped up from `TRANSFORM_ARITY` instead of emitting
|
|
285
|
+
* `|default()`, which `parseTransform` rejects.
|
|
286
|
+
*/
|
|
287
|
+
async function buildSuggestedTransform(suggestion, P) {
|
|
288
|
+
const m = String(suggestion).match(/^([a-z_][a-z0-9_]*)(?:\((.*)\))?$/i);
|
|
289
|
+
if (!m) return "";
|
|
290
|
+
const name = m[1];
|
|
291
|
+
if (!Object.hasOwn(TRANSFORMS, name)) return "";
|
|
292
|
+
const needsArg = Boolean((m[2] || "").trim()) || transformArgCount(name) > 0;
|
|
293
|
+
|
|
294
|
+
if (!needsArg) return formatTransformCall(name);
|
|
295
|
+
|
|
296
|
+
const args = [];
|
|
297
|
+
for (let i = 0; i < Math.max(1, transformArgCount(name)); i++) {
|
|
298
|
+
args.push(
|
|
299
|
+
await P.input({
|
|
300
|
+
message: `${name}() argument ${i + 1}`,
|
|
301
|
+
validate: (v) => validateTransformArg(v),
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return formatTransformCall(name, args);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Pick one transform from the catalogue and ask for whatever it needs.
|
|
310
|
+
* @returns {Promise<string>} e.g. `|len()` or `|get(0)`
|
|
311
|
+
*/
|
|
312
|
+
async function promptOneTransform(P) {
|
|
313
|
+
const names = transformNames();
|
|
314
|
+
const name = await P.select({
|
|
315
|
+
message: "Transform",
|
|
316
|
+
source: async (term) => {
|
|
317
|
+
const hits = rankTransforms(term);
|
|
318
|
+
return (term ? hits : names).slice(0, 50).map((n) => ({
|
|
319
|
+
value: n,
|
|
320
|
+
name: `${chalk.cyan(n.padEnd(10))} ${chalk.dim(TRANSFORM_DOCS[n] || "")}`,
|
|
321
|
+
}));
|
|
322
|
+
},
|
|
323
|
+
pageSize: 15,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
let args = [];
|
|
327
|
+
for (let i = 0; i < transformArgCount(name); i++) {
|
|
328
|
+
const answer = await P.input({
|
|
329
|
+
message: `${name}() argument ${i + 1}`,
|
|
330
|
+
validate: (v) => validateTransformArg(v),
|
|
331
|
+
});
|
|
332
|
+
args.push(answer);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return formatTransformCall(name, args);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Rank transforms by name and description, so "array" finds `join`. */
|
|
339
|
+
function rankTransforms(term) {
|
|
340
|
+
if (!term) return [];
|
|
341
|
+
const t = String(term).toLowerCase();
|
|
342
|
+
return transformNames().filter(
|
|
343
|
+
(n) => n.includes(t) || (TRANSFORM_DOCS[n] || "").toLowerCase().includes(t)
|
|
344
|
+
);
|
|
212
345
|
}
|
|
213
346
|
|
|
214
347
|
function formatLiteral(value) {
|
|
@@ -246,7 +379,6 @@ export async function buildWizard(opts = {}) {
|
|
|
246
379
|
|
|
247
380
|
const steps = [];
|
|
248
381
|
const outputs = [];
|
|
249
|
-
const usedSlugs = [];
|
|
250
382
|
|
|
251
383
|
for (;;) {
|
|
252
384
|
renderHeader(
|
|
@@ -254,7 +386,7 @@ export async function buildWizard(opts = {}) {
|
|
|
254
386
|
{ subtitle: steps.length ? "Pick the next tool to feed" : "Start with any tool" }
|
|
255
387
|
);
|
|
256
388
|
|
|
257
|
-
const tool = await pickTool(tools,
|
|
389
|
+
const tool = await pickTool(tools, [], P);
|
|
258
390
|
if (!tool) break;
|
|
259
391
|
|
|
260
392
|
const info = await getTool(tool.slug);
|
|
@@ -280,7 +412,6 @@ export async function buildWizard(opts = {}) {
|
|
|
280
412
|
|
|
281
413
|
const spec = `${tool.slug} ${assignments.map((a) => a.text).join(" ")}`.trim();
|
|
282
414
|
steps.push({ slug: tool.slug, spec, assignments });
|
|
283
|
-
usedSlugs.push(tool.slug);
|
|
284
415
|
|
|
285
416
|
console.log(chalk.gray(`\n Running ${spec}\n`));
|
|
286
417
|
const started = Date.now();
|
|
@@ -294,7 +425,6 @@ export async function buildWizard(opts = {}) {
|
|
|
294
425
|
// Drop the step either way: only steps that actually ran belong in the
|
|
295
426
|
// replay command we hand back.
|
|
296
427
|
steps.pop();
|
|
297
|
-
usedSlugs.pop();
|
|
298
428
|
const again = await P.confirm({ message: "Change the inputs and retry?", default: true });
|
|
299
429
|
if (again) continue;
|
|
300
430
|
break;
|
|
@@ -386,4 +516,4 @@ function literalInput(assignments, upstream) {
|
|
|
386
516
|
return input;
|
|
387
517
|
}
|
|
388
518
|
|
|
389
|
-
export { transformNames, parsePath };
|
|
519
|
+
export { transformNames, parsePath, rankTransforms };
|